CNN画像認識で　感情識別

<h1>
はじめに
</h1>
初めまして、機械学習について勉強中のシステムエンジニアです。<br>
今回はCNN(Conbolutional Neural Network)を使用して、人間の顔写真から感情を分析するアプリを作成したいと思います。
<h2>
今回の目的
</h2>
・機械学習の実施するための大まかな流れを把握する<br>
・機械学習の勉強によって得た知識から実際にAI作成してみる<br>
・機械学習の精度を上げるために何をする必要があるがあるのかを学ぶ<br>

<h1>
事前準備
</h1>
機械学習を始める前に、まずは開発のための実行環境と<br>
機械学習用のデータの準備について記載します。
<h2>
実行環境
</h2>
実行環境は以下の2つを使用しています。<br>
・Google Colaboratory<br>
・Visual Studio Code<br>
Google Colaboratoryを使用して機械学習を行うコードを実行し、学習済モデルを生成します。
<br>
そしてVisual Studio Codeで作成したWebページから、任意の画像を受け取り感情分析の結果を表示させます。
<h2>
学習用データの用意
</h2>
今回は学習用データとして無料で公開されているデータセットFER-2013を使用しました。<br>
https://www.kaggle.com/datasets/msambare/fer2013

画像は以下でラベル付けされています。
・0=Angry（怒り）
・1=Disgust（嫌悪）
・2=Fear（恐怖）
・3=Happy（喜び）
・4=Sad（悲しみ）
・5=Surprise（驚き）
・6=Neutral（平静）
実際の画像は以下の画像です。基本的に人間の顔をアップの写真で、白黒になっています。
![anger.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3326252/1e760759-32dd-df56-2e1b-304dccd0b549.png)

<h1>Google Colaboratoryで機械学習を実行</h1>
早速Colaboratoryでプログラムを作成し、モデルを生成を行っていきます。

<h2>
ライブラリのインポート～データの格納まで
</h2>

```
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Dropout, Flatten, Conv2D, MaxPooling2D, BatchNormalization
from tensorflow.keras.preprocessing.image import ImageDataGenerator
from tensorflow.keras.utils import to_categorical

data = pd.read_csv('/content/drive/My Drive/fer2013/fer2013.csv',delimiter=',')

train_data = data[data['Usage']=='Training']
val_data = data[data['Usage']=='PublicTest']
test_data = data[data['Usage']=='PrivateTest']

train_X = train_data['pixels'].values
val_X = val_data['pixels'].values
test_X = test_data['pixels'].values

train_X = np.array([np.array(x.split(' '), dtype=int) for x in train_X])
val_X = np.array([np.array(x.split(' '), dtype=int) for x in val_X])
test_X = np.array([np.array(x.split(' '), dtype=int) for x in test_X])

train_X = train_X.reshape(train_X.shape[0], 48, 48, 1)
val_X = val_X.reshape(val_X.shape[0], 48, 48, 1)
test_X = test_X.reshape(test_X.shape[0], 48, 48, 1)

train_Y = train_data['emotion'].values
val_Y = val_data['emotion'].values
test_Y = test_data['emotion'].values

train_Y = to_categorical(train_Y)
val_Y = to_categorical(val_Y)
test_Y = to_categorical(test_Y)
```

<h2>
学習実行
</h2>
今回は、学習方法を改良しながら３回実行してみます。
<h3>
１回目
</h3>
CNNの２階層で実行します。
本来であれば、ImageDataGeneratorによってデータの水増しを行うことで<br>
学習精度を向上させますが、まずはデータの水増しを行うことなく学習を行ってみます。

```
model = Sequential()
model.add(Conv2D(32, kernel_size=(3, 3), activation='relu', input_shape=(48, 48, 1)))
model.add(Conv2D(64, kernel_size=(3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))

model.add(Conv2D(128, kernel_size=(3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(128, kernel_size=(3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))

model.add(Flatten())
model.add(Dense(1024, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(7, activation='softmax'))

model.compile(loss='categorical_crossentropy', optimizer='adadelta', metrics=['accuracy'])
train_datagen = ImageDataGenerator()
train_generator = train_datagen.flow(train_X, train_Y, batch_size=128)
history = model.fit(train_generator, steps_per_epoch=len(train_X) / 128, epochs=100, validation_data=(val_X, val_Y))
```
実行結果は以下のとおりです。
val_accuracyが２割強しかありません。
学習率も上がっていないようです。
![kekka1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3326252/891a7a20-6e9a-2895-f6e2-afddc4aa1dfd.png)
<h3>
２回目
</h3>
次はImageDataGeneratorで引数を指定し、データの水増しを行います。<br>
上記以外は１回目と変わりありません。

```
model = Sequential()

model.add(Conv2D(32, kernel_size=(3, 3), activation='relu', input_shape=(48, 48, 1)))
model.add(Conv2D(64, kernel_size=(3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))

model.add(Conv2D(128, kernel_size=(3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(128, kernel_size=(3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))

model.add(Flatten())
model.add(Dense(1024, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(7, activation='softmax'))

model.compile(loss='categorical_crossentropy', optimizer='adadelta', metrics=['accuracy'])
train_datagen = ImageDataGenerator(rotation_range=10, zoom_range=0.1, horizontal_flip=True)
train_generator = train_datagen.flow(train_X, train_Y, batch_size=128)
history = model.fit(train_generator, steps_per_epoch=len(train_X) / 128, epochs=100, validation_data=(val_X, val_Y))
```
実行結果は以下の通り。
あまり変わらないようですね。
他に問題がありそうです。
![kekka2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3326252/8e8ffbad-2f55-fd19-d323-c020b83a6747.png)

<h3>
３回目
</h3>
有識者に相談したところ、BatchNormalization()を追加する必要があるのではないかとのことでした。<br>
また、今回では２層から３層に増やして実行します。

```
model = Sequential()

model.add(Conv2D(32, kernel_size=(3, 3), activation='relu', input_shape=(48,48,1)))
model.add(BatchNormalization())
model.add(Conv2D(64, kernel_size=(3, 3), activation='relu'))
model.add(BatchNormalization())
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))

model.add(Conv2D(128, kernel_size=(3, 3), activation='relu'))
model.add(BatchNormalization())
model.add(Conv2D(128, kernel_size=(3, 3), activation='relu'))
model.add(BatchNormalization())
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))

model.add(Conv2D(256, kernel_size=(3, 3), activation='relu'))
model.add(BatchNormalization())
model.add(Conv2D(256, kernel_size=(3, 3), activation='relu'))
model.add(BatchNormalization())
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))

model.add(Flatten())
model.add(Dense(256, activation='relu'))
model.add(BatchNormalization())
model.add(Dropout(0.5))
model.add(Dense(7, activation='softmax'))

model.compile(loss="categorical_crossentropy", optimizer= tf.keras.optimizers.Adam(lr=0.0001), metrics=['accuracy'])

train_datagen = ImageDataGenerator(rotation_range=10, zoom_range=0.1, horizontal_flip=True)
train_generator = train_datagen.flow(train_X, train_Y, batch_size=128)

history = model.fit(train_generator, steps_per_epoch=len(train_X) / 128, epochs=100, validation_data=(val_X, val_Y))
```
実行結果は以下のとおり。
val_accuracyは6割を超えることができました。
また、Epoch数が大きくなるたびに精度が向上していることが分かります。
これを学習済モデルとして利用したいと思います。
![kekka3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3326252/da2330d3-625d-d4a7-87dd-4cedc3735c2f.png)


<h1>実行結果</h1>
「ファイルを選択」ボタンで画像を選択し、<br>
「submit!」ボタンで、生成済のモデルを使用して感情分析の結果を表示させます。

![kekka4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3326252/9b277ccb-fd63-8125-c9f7-0c3bd07187d5.png)

怒りにジャンル分けされた画像で試してみたところ、ちゃんと「怒り」と表示されました。
うまく感情分析ができているのではないかと思います。
但し、試行回数を増やせば正解率は６割ほどに収束していくのだと思います。

<h1>
結果の考察
</h1>
実装してみる前は、学習精度の向上にはパラメータの調整が主であるイメージでいましたが、<br>
実際に実装してみると先ず気にする箇所は別であることが分かりました。<br>
・バッチサイズが少なすぎないか<br>
・画像のサイズが、実装内容とマッチしているのか<br>
・BatchNormalizationなど、追加する必要がある処理はないか<br>
まずは上記３つあたりから考える方が良さそうです<br>
そして、<br>
・ImageDataGeneratorによりデータの水増しが妥当か<br>
・relu関数以外の方法で精度が上がらないか<br>
といったところを検討してさらなる精度向上を図ることになるかなと思いました。<br>
この辺りは今後の学習によって理解を深めていきたいとおもいます。<br>

<h1>
今後の展望
</h1>
今回作成したアプリでは、既存のデータセットをそのまま使用しているため<br>
実用的であるとは言えない状態のままであると思います。<br>
実際の写真はカラー写真で、写真の中の顔の位置もばらばらなのです。<br>
なので、アプリの実用的なものに改良するために以下の課題をクリアする必要がありそうです。<br>
・感情分析の対象となる画像のカラー写真への対応<br>
・人間の全身の写真から、顔の位置を切り取り<br>

<h1>
終わりに
</h1>
今回では、CNNを使用した画像分類で簡単なものを実装しました。<br>
データの準備からモデル生成とアプリでの使用方法を実装をとおして学ぶことができたかと思います。<br>
個人的にはそこそこの正解率のものを作れて良かったと思っています。<br>
余談ですが、使用するデータセットを選定する際は有識者に相談しながら使えそうなものをWebで見つけだしましたが、
その後で最近話題のChatGPTをに質問してみると、すぐにFER-2013を見つけ出すことができました。<br>
ChatGPT便利ですね。