概要
- 表情認識ができるDNNモデルを構築しました
- とりあえずアーキテクチャを作って学習しただけのものから始めて、だいたいval_accuracyが0.8くらいまで向上させた時の備忘録です
- 転用先はこちらの記事です
@Link - コードはこちらをご覧ください
https://github.com/yutobomberair/RasPi_Symphonia
やりたいこと
- 顔の画像から感情ラベルを推定したい
- 特に"楽しい"を推定したい
- 最終的にはRasPiで実装するので、なるべく軽量なモデルで実装したい
環境
- ライブラリ
- Tensorflow2.13系
- Tensorflow LITEで量子化したかったため(まだできていない)
- アーキテクチャ
- EfficientetB0を使用
- 軽量であることに軸を置いてモデル選定をすると、EfficientNetまたはMobileNetの二択となった
- MobileNetは以前使用したことがあったのでEfficientNetを試してみた
- データセット
- FER2013を使用
- 7クラスの表情画像をグレースケールで提供されているオープンデータ
- ラベル: {"Angry", "Disgust", "Fear", "Happy", "Sad", "Surprise", "Neutral"}
育成開始(レベルは私の感触で恣意的に付けたものです...)
Lv.0 モデルの誕生
-
簡単なCNNのモデルを組み、えいやと学習させてみる
-
入力前に正規化処理を実施
-
optimizer="Adam", loss_function="categorical_crossentropy"
-
trainとvalidationは9:1で分割
-
学習はfit関数を用いて行う(パラメータはデフォルト値)
引数名 役割 デフォルト値 x 入力データ なし(必須) y 出力ラベル なし(必須) batch_size 1ステップあたりの学習サンプル数 32 epochs 学習回数 1 -
学習結果→val_accuracyが約0.15くらい
EmotionalClassifierLearning.pydef __create_model(self) model = tf.keras.Sequential([ tf.keras.layers.InputLayer(input_shape=(48, 48, 1)), tf.keras.layers.Conv2D(32, 3, activation='relu'), tf.keras.layers.MaxPooling2D(), tf.keras.layers.Conv2D(64, 3, activation='relu'), tf.keras.layers.MaxPooling2D(), tf.keras.layers.Flatten(), tf.keras.layers.Dense(128, activation='relu'), tf.keras.layers.Dense(7, activation='softmax') ]) return model
Lv.5 callback関数の設定
-
下記3つのcallback関数を使用してきちんとパラメータを管理する
Callback名 説明 EarlyStopping 検証精度や損失が改善されない場合に学習を打ち切る ReduceLROnPlaeau 指定回数改善がなかった場合、自動で学習率を減少させる TensorBoard 学習経過の可視化を行う -
fit関数のcallback引数に下記関数の返り値を指定する
- ※loggerは別途記述しています
- ※興味がある方はgitからコードを確認してください
-
今回からepoch数は50に変更
-
学習結果→val_accuracyが0.5くらいまで向上
EmotionalClassifierLearning.pydef __callbacks(self): early_stopping = EarlyStopping( monitor='val_accuracy', # 検証用ロスを監視 min_delta=1e-3, # 「改善」と見なす最小の変化 patience=15, # 改善が見られないエポック数 verbose=1, # ログ出力あり mode='auto', # 自動で最小化 or 最大化を判定 baseline=None, # 指標の初期基準値(指定しない) restore_best_weights=True # 学習終了時に最良の重みに戻す ) reduce_lr = ReduceLROnPlateau( monitor='val_accuracy', # 検証用ロスを監視 factor=0.5, # 学習率を半分にする patience=7, # 改善が見られないエポック数 verbose=1, # ログ出力あり mode='auto', # 自動判定('min' or 'max') min_delta=1e-3, # 「改善」と見なす最小の差分 cooldown=2, # 学習率変更後、待機するエポック数 min_lr=1e-6 # 最小の学習率(これ以下には下げない) ) tensorboard = TensorBoard(log_dir='./TensorBoard', histogram_freq=1) logging_callback = LoggingCallback(self.logger) return [early_stopping, reduce_lr, tensorboard, logging_callback]
Lv.25 ラベルの縮約とdata-augmentation
- ラベルの縮約
- "楽しい"を推定したいのに対して、7クラスの分類問題は厳しすぎるのでは?と推測
- 縮約しすぎるとそれはそれで誤検出などの問題が起きそうだったので、{’happy’, ‘neutral+surprise’, ‘angry+disgust+fear+sad’}とまとめて、
- 新たに{'happy', 'neutral', 'negative'}の3クラス分類問題とした
- data-augmentation
- データセットを分析してみると各ラベルのサンプル数がかなりバラバラ
- ラベル縮約をした状態で、
- {’happy’, ‘neutral+surprise’, ‘angry+disgust+fear+sad’}={8989, 4002+6198, 4953+547+5121+6077}
- これだと学習に偏りが出てしまうと思ったので、左右反転・並行移動・輝度変化・コントラスト変化をつけてサンプル数を水増しを実施
- この2つが見事にハマって学習精度は→val_accuracyが0.75くらいまで向上
- だいぶ使えるレベル感になってきました
EmotionalClassifierLearning.py
# ラベルの縮約 def __data_loader(self, dpath, input_size): data = pd.read_csv(dpath / "fer2013.csv") # kaggleから取得したCSV形式を想定 pixels = data["pixels"].tolist() X = np.array([cv2.resize(np.fromstring(p, sep=' ').reshape(48, 48, 1), (input_size, input_size)) for p in pixels]) X = X.astype('uint8') # 変換マップ(7→3クラス) conversion_map = { 0: 2, # Angry → Negative 1: 2, # Disgust → Negative 2: 2, # Fear → Negative 3: 0, # Happy → Happy 4: 2, # Sad → Negative 5: 1, # Surprise → Neutral 6: 1 # Neutral → Neutral } Y_mapped = np.vectorize(conversion_map.get)(data["emotion"]) # remap # log出力 values, counts = np.unique(Y_mapped, return_counts=True) self.logger.info(f"Train-Dataset->{self.emotion_label[values[0]]}:{counts[0]}, {self.emotion_label[values[1]]}:{counts[1]}, {self.emotion_label[values[2]]}:{counts[2]}") return X, Y_mapped # 水増し def __data_augmentation(self, X, Y): # Augmentationパイプライン augmentor = augmenters.Sequential([ augmenters.Fliplr(0.5), # 左右反転 augmenters.Affine(translate_percent={"x": (-0.1, 0.1), "y": (-0.1, 0.1)}), # 平行移動 augmenters.Add((-20, 20)), # 輝度の変化 augmenters.Multiply((0.8, 1.2)) # コントラスト ]) # 0:Happyと1:Neutralだけを選択 indices_to_augment_0 = np.where(np.isin(Y, [0]))[0] indices_to_augment_1 = np.where(np.isin(Y, [1]))[0] aug_0 = len(Y) - 2 * len(indices_to_augment_0) - len(indices_to_augment_1) aug_1 = len(Y) - len(indices_to_augment_0) - 2 * len(indices_to_augment_1) # データを抽出: 最大サンプル数に合わせてデータ拡張 indices_to_augment_0_smp = np.random.choice(indices_to_augment_0, size=aug_0, replace=False) indices_to_augment_1_smp = np.random.choice(indices_to_augment_1, size=aug_1, replace=False) # augmentation X_aug_target_0 = X[indices_to_augment_0_smp] Y_aug_target_0 = Y[indices_to_augment_0_smp] X_augmented_0 = augmentor(images=X_aug_target_0) X_aug_target_1 = X[indices_to_augment_1_smp] Y_aug_target_1 = Y[indices_to_augment_1_smp] X_augmented_1 = augmentor(images=X_aug_target_1) X_augmented = np.concatenate([X_augmented_0, X_augmented_1], axis=0) Y_aug_target = np.concatenate([Y_aug_target_0, Y_aug_target_1], axis=0) # 元データと結合 X_combined = np.concatenate([X, X_augmented], axis=0) Y_combined = np.concatenate([Y, Y_aug_target], axis=0) # log出力 values, counts = np.unique(Y_combined, return_counts=True) self.logger.info(f"Train-Dataset(after augmentationed)->{self.emotion_label[values[0]]}:{counts[0]}, {self.emotion_label[values[1]]}:{counts[1]}, {self.emotion_label[values[2]]}:{counts[2]}") return X_combined, Y_combined
Lv.35 EfficientNet導入
-
ここまではChatGPTに適当に作ってもらったモデルを使っていたので、実績のあるモデルを導入してみる
-
RasPiに実装するという制約があるので、軽量という観点でモデルを探したところ、EfficientNetとMobileNetの2つがヒットし、MobileNetは触ったことがあるという理由でEfficientNetを使用
-
中でも最も軽量なB0を選択しました
- 下記128x128でbatch=32で学習した際の目安です
モデル パラメータ数 GPU使用メモリ EfficientNetB0 約5M 約1~1.5GB EfficientNetB3 約12M 約2~3GB EfficientNetB7 約66M 約8GB以上 -
後の量子化の観点など諸々検討し、一旦192x192入力に設定
-
これにより学習結果→val_accuracyが0.78程度まで向上
-
今回はここまで。
EmotionalClassifierLearning.pydef __create_model_efficient_lite0(self, input_size=128, num_classes=3): inp = tf.keras.layers.Input(shape=(input_size, input_size, 1)) # 入力:128×128×3 inp_3 = tf.keras.layers.Concatenate()([inp, inp, inp]) base = tf.keras.applications.EfficientNetB0( # EfficientNetB0 を Lite0 相当に軽量化 input_tensor=inp_3, include_top=False, weights=None, # ImageNet 事前学習不要 or 利用不可なら None pooling='avg' ) x = base.output x = tf.keras.layers.Dropout(0.2)(x) # Dropout で過学習防止 out = tf.keras.layers.Dense(num_classes, activation='softmax')(x) # 出力層 model = tf.keras.Model(inputs=inp, outputs=out, name='EfficientLite0') return model
最終精度
感想
- 入力画像がグレースケールなのに対して、EfficientNetがRGB入力を想定している。その辺の対応関係を考えてもMobileNetを素直に使っておけば良かったかも。
- あと48x48で十分視認できる解像感だったので入力サイズにはそこまでこだわらなくて良いかもしれない。
- 一旦、頭で思いつく一般的な施策を試してみたところなのでKaggleのページなどを見てみてさらに成長させたい。
- ただ現状検知処理がかなり遅い。量子化対応は必須。
- (次は評価ConfusionMatrixとか使ってちゃんとやります)