Keras で顔写真から年齢が学習できるのか

深層学習で回帰をしてみたいときとかあると思います。そこで、顔写真からその人の年齢を回帰してみます。ただ、何のデータをつかえばいいかわからなかったので、Berryz工房のジャケ写からキャプチャした画像をつかうことにします。結論を先にいうとこの記事の学習は全然駄目です。全然駄目ですがデータが決定的に足りないのとプレ処理が雑なのでその辺を改善すれば何とかなるのかもしれません。何とかならないかもしれません。

データの準備

データはWebの画像検索から調達します。ベリメンの年齢は4学年にわたるので、だいたい3, 4年置きのシングルのジャケ写から顔写真をキャプチャすればだいたいの年齢がカバーできると思います。なので、適当に「あなたなしでは生きてゆけない」「告白の噴水広場」「本気ボンバー!!」「ロマンスを語って」を訓練データにチョイスします。テストデータは「ライバル」にします。適当です。テストデータ数をちょうど30にしたかったので、「ライバル」の梨沙子のデータだけ訓練データに含めます。なお、キャプチャはだいたい正方形になるように手動で切り取っています。訓練時に 128x128 にリサイズします。

訓練

面倒なので以下の記事のネットワーク構造を使いまわします。変更点は、今回は年齢そのものの出力を目指すので最終層を活性化しないというところです。損失関数と最適化アルゴリズムも適当に変更します。
 Keras で少ない画像データから学習したい - クッキーの日記

結果

何かを学習したような雰囲気はありつつも概して駄目でした。データが足りません。
全体的に幼い方に誤っているので最終層のバイアスを大きくするだけでもっと最小2乗誤差が小さくなりそうです(そうなっていないのは学習を早々に止めすぎました)。

f:id:cookie-box:20170814235401p:plain:w560

スクリプト

画像は最初に全て読み込まず、ミニバッチ学習のたびにミニバッチの分だけ読み込むという方法にしています(というか今回の目的がそのような処理の練習でした)。keras.preprocessing.image.ImageDataGenerator の flow_from_directory や flow を活用したかったのですが上手くできなかったので愚直な書き方になりました(flow_from_directory はクラス分類学習の際に cat や dog などというサブフォルダを切って猫画像データや犬画像データを配置しておけば勝手にミニバッチ毎の読み込みを正解データ生成含めてやってくれますが、今回はクラス分類ではなく回帰であり、個々の画像に固有の数値の正解データを生成したいため利用できませんでした)。

from keras.models import Sequential
from keras.layers import Conv2D, MaxPooling2D
from keras.layers import Dropout, Flatten, Dense
from keras import backend as K
import os
import numpy
import cv2
import pandas

# 顔画像ファイル名から年齢を抽出
def filename2age(filename):
  tempstr = filename.replace('.PNG', '')
  tempstrs = tempstr.split('_')
  return(int(tempstrs[2]))

# ファイル名のリストからデータをロード
def load_data(dirname, filenames, img_width, img_height):
  x = []
  y = []
  for filename in filenames:
    image = cv2.imread(dirname + filename)
    image = cv2.resize(image, (img_width, img_height))
    age = filename2age(filename)
    x.append(image)
    y.append(age)
  x = numpy.array(x)
  x = x.astype('float32') / 255.
  y = numpy.array(y)
  return(x, y)

# メイン処理
if __name__ == '__main__':
  img_width, img_height = 128, 128
  if K.image_data_format() == 'channels_first':
    input_shape = (3, img_width, img_height)
  else:
    input_shape = (img_width, img_height, 3)
  
  # モデル構築
  model = Sequential()
  model.add(Conv2D(32, (3, 3), activation='relu', input_shape=input_shape))
  model.add(MaxPooling2D(pool_size=(2, 2)))
  model.add(Conv2D(32, (3, 3), activation='relu'))
  model.add(MaxPooling2D(pool_size=(2, 2)))
  model.add(Conv2D(64, (3, 3), activation='relu'))
  model.add(MaxPooling2D(pool_size=(2, 2)))
  model.add(Flatten())
  model.add(Dense(64, activation='relu'))
  model.add(Dense(1))
  model.compile(loss='mean_squared_error', optimizer='rmsprop')
  
  # 学習
  filenames = numpy.array(os.listdir('data/')) # 30 files
  batch_size = 10
  n_batch_loop = 3 # 30 / 10
  epochs = 50
  for e in range(epochs):
    print('Epoch', e)
    indices = numpy.repeat(range(n_batch_loop), batch_size)
    numpy.random.shuffle(indices)
    print(indices)
    for i_batch_loop in range(n_batch_loop):
      filenames_temp = filenames[indices == i_batch_loop]
      x, y = load_data('data/', filenames_temp, img_width, img_height)
      model.fit(x, y, epochs=1, batch_size=batch_size)
  
  # 学習結果の確認 (訓練データ)
  x, y = load_data('data/', filenames, img_width, img_height)
  result = pandas.DataFrame(filenames, columns = ['filename'])
  result['actual'] = y
  result = pandas.concat([result,
    pandas.DataFrame(model.predict(x), columns = ['predict'])], axis=1)
  result.to_csv('train.csv', index=False, encoding='utf-8')
  
  # 学習結果の確認 (テストデータ)
  filenames = numpy.array(os.listdir('test_data/'))
  x, y = load_data('test_data/', filenames, img_width, img_height)
  result = pandas.DataFrame(filenames, columns = ['filename'])
  result['actual'] = y
  result = pandas.concat([result,
    pandas.DataFrame(model.predict(x), columns = ['predict'])], axis=1)
  result.to_csv('test.csv', index=False, encoding='utf-8')