Signate - 【練習問題】モノクロ顔画像の感情分類

# モノクロ表情認識データセットへの挑戦：深層学習アプローチ

初のQiita投稿！Signateのモノクロ表情認識データセットに挑戦した過程と、その結果についてシェアしたいと思います。このプロジェクトでは、表情認識のために深層学習のCNNモデルを構築し、試行錯誤の末にたどり着いたコードについて解説していきます。以下のSignateサイトにてデータセットの確認が可能です：

https://signate.jp/competitions/404

### データセットの概要
Signateが提供するこのデータセットは、モノクロの顔画像と対応する表情ラベルから構成されています。画像はグレースケールで、表情は「喜び」「悲しみ」「怒り」「驚き」の4種類に分類されています。このデータセットの特徴は、カラー情報がないため、表情の微妙な変化を捉えることが求められる点です。

### 手法の選択：なぜ深層学習なのか
表情認識タスクに取り組むにあたり、深層学習、特に畳み込みニューラルネットワーク（CNN）を選択しました。その理由は以下の通りです：

1. 画像の特徴抽出能力：CNNは画像から自動的に重要な特徴を抽出できます。これは、表情の微妙な違いを捉えるのに適しています。
1. パターン認識の優位性：深層学習モデルは、大量のデータから複雑なパターンを学習する能力に優れています。
1. モノクロ画像への適応性：CNNは、カラー情報がなくてもグレースケールの濃淡から重要な情報を抽出できます。

### 解析方法：モノクロ画像の扱い

モノクロ画像をプログラムに組み込む際、以下のアプローチを取りました：

1. グレースケール読み込み：`cv2.IMREAD_GRAYSCALE`を使用して、画像をグレースケールとして読み込みます。
1. ピクセル値の正規化：画像のピクセル値を0から1の範囲に正規化します。これにより、モデルの学習が安定します。
1. 入力シェイプの設定：モデルの入力層を`(48, 48, 1)`と設定し、1チャンネルのグレースケール画像であることを明示します。

## データの読み込みと前処理

まずは、トレーニングデータを読み込みました。

```python
import pandas as pd
import numpy as np
import os
import cv2
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout, BatchNormalization
from tensorflow.keras.preprocessing.image import ImageDataGenerator
from tensorflow.keras.callbacks import ReduceLROnPlateau
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import train_test_split

# Load the train data and labels
train_df = pd.read_csv('train_master.tsv', sep='\t')
```

ここでは、トレーニングデータを`train_master.tsv`から読み込んでいます。

次に、ファイル名の列を更新します。

```python
# Update the file name column based on the actual column name
file_name_column = 'id'
```

表情を数値ラベルにエンコードします。

```python
# Encode the 'expression' column
label_encoder = LabelEncoder()
train_df['expression'] = label_encoder.fit_transform(train_df['expression'])
```

## 画像の読み込みと前処理

次に、画像を読み込んで前処理します。

```python
# Load and preprocess images
def load_images(image_files, folder):
    images = []
    for file in image_files:
        img = cv2.imread(os.path.join(folder, file), cv2.IMREAD_GRAYSCALE)
        img = cv2.resize(img, (48, 48))  # Resize to 48x48 pixels
        img = img.astype('float32') / 255.0  # Normalize pixel values
        images.append(img)
    return np.array(images).reshape(-1, 48, 48, 1)

X = load_images(train_df[file_name_column].values, 'train')
y = train_df['expression'].values
```

ここでは、画像を読み込み、48x48ピクセルにリサイズし、ピクセル値を0から1の範囲に正規化しています。

## トレーニングと検証データの分割

次に、データをトレーニングセットと検証セットに分割します。

```python
# Split the data into training and validation sets
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=42)
```

## CNNモデルの構築

CNNモデルを構築します。

```python
# Build the CNN model
model = Sequential([
    Conv2D(32, (3, 3), activation='relu', input_shape=(48, 48, 1)),
    BatchNormalization(),
    MaxPooling2D((2, 2)),
    Dropout(0.25),
    
    Conv2D(64, (3, 3), activation='relu'),
    BatchNormalization(),
    MaxPooling2D((2, 2)),
    Dropout(0.25),
    
    Conv2D(128, (3, 3), activation='relu'),
    BatchNormalization(),
    MaxPooling2D((2, 2)),
    Dropout(0.25),
    
    Flatten(),
    Dense(256, activation='relu'),
    BatchNormalization(),
    Dropout(0.5),
    
    Dense(4, activation='softmax')
])

model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy'])
```

ここでは、3つの畳み込み層とプーリング層を持つCNNモデルを構築し、全結合層を追加して分類タスクを実行します。

## データジェネレーターの作成

データ拡張を行うためのジェネレーターを作成します。

```python
# Create data generators
train_datagen = ImageDataGenerator(
    rotation_range=15,
    zoom_range=0.2,
    width_shift_range=0.2,
    height_shift_range=0.2,
    shear_range=0.2,
    horizontal_flip=True
)
val_datagen = ImageDataGenerator()
```

トレーニングデータの拡張を行うために、`ImageDataGenerator`を使用しています。

## モデルのトレーニング

モデルをトレーニングします。

```python
# Fit the model
reduce_lr = ReduceLROnPlateau(monitor='val_loss', factor=0.2, patience=3, min_lr=0.0001)
model.fit(train_datagen.flow(X_train, y_train, batch_size=32), 
          validation_data=val_datagen.flow(X_val, y_val),
          epochs=50,
          callbacks=[reduce_lr])
```

学習率を減少させるために`ReduceLROnPlateau`コールバックを使用し、50エポックでモデルをトレーニングします。

## テストデータの読み込みと予測

最後に、テストデータを読み込み、表情を予測します。

```python
# Load the test data
test_files = os.listdir('test')
X_test = load_images(test_files, 'test')

# Predict the emotions for test data
predictions = model.predict(X_test)
predicted_labels = label_encoder.inverse_transform(np.argmax(predictions, axis=1))
```

テストデータの画像を読み込み、モデルを使って予測を行います。

## 提出ファイルの作成

予測結果を提出ファイルとして保存します。

```python
# Create the submission file
submission = pd.DataFrame({
    'file_name': test_files,
    'expression': predicted_labels
})

submission.to_csv('Mono_ver2.csv', index=False, header=False)
```

ここでは、予測結果をCSVファイルとして保存しています。


## プログラミングの考察
#### モデルアーキテクチャ
最終的に3つの畳み込み層を持つモデルを採用しました。この選択には以下の理由があります：

1. 段階的な特徴抽出：最初の層で基本的な特徴（エッジや線）を、次の層でより複雑な特徴（目や口の形状）を、最後の層で表情全体のパターンを捉えることを意図しています。

1. モデルの複雑さとデータサイズのバランス：データセットの規模を考慮し、過学習を避けつつ十分な表現力を持つモデルを目指しました。
1. 計算効率：3層構造は、精度と計算時間のバランスが良いと判断しました。

#### バッチ正規化の導入
各畳み込み層の後にバッチ正規化層を追加しました。これにより：

1. 学習の安定化：各層の出力を正規化することで、勾配消失問題を軽減します。
1. 学習速度の向上：正規化により、より高い学習率を使用できる可能性があります。
1. 過学習の抑制：バッチ正規化は正則化効果も持ち、モデルの汎化性能向上に寄与します。

#### データ拡張の強化
`ImageDataGenerator`を用いて、以下のデータ拡張技術を適用しました：

- 回転
- ズーム
- シフト
- せん断
- 水平反転

これらの技術により、モデルがより多様な表情パターンを学習し、汎化性能が向上することを期待しています。

#### なぜepochs=50なのか
エポック数を50に設定した理由は以下の通りです：

1. 十分な学習機会：50エポックは、モデルが十分に学習するための時間を提供します。
1. 過学習の監視：検証セットでの性能をモニタリングし、過学習の兆候が見られた場合に早期停止できるようにしています。
1. 計算資源の制約：無制限に学習を続けることはできないため、適切な打ち切りポイントとして50エポックを選択しました。
1. 学習率の調整：`ReduceLROnPlateau`コールバックを使用しているため、50エポック中に学習率が適切に調整される機会があります。


## 結論
このプロジェクトを通じて、モノクロ画像からの表情認識という興味深い課題に挑戦しました。深層学習の手法を用いることで、カラー情報がなくても表情の微妙な違いを捉えられることが分かりました。モデルの設計や学習プロセスの各段階で、理論的根拠に基づいた選択を行い、試行錯誤を重ねてこの結果に辿り着きました。

今後の改善点としては、さらなるハイパーパラメータの調整や、異なるモデルアーキテクチャの試行などが考えられます。また、アンサンブル学習や転移学習の導入も、精度向上の可能性がある手法です。

以上が、モノクロ表情認識データセットに挑戦した過程とその結果です。読んでいただきありがとうございました！これからも新しいことに挑戦し、その経験をシェアしていきたいと思います。皆さんもぜひ挑戦してみてくださいね！