[番外編]機械学習プログラミング 日向坂46で顔認識してみた。@Python (3)

(番外編)日向坂46で顔認識
 最近流行のAI(機械学習)でプログラムを書いてみました。
 尚、この一連の記事ではOracleは全く登場しません。

 この記事は、前回までの続きです。

[番外編]機械学習プログラミング 日向坂46で顔認識してみた。@Python (1)
https://oracle.tf17.net/plsql/?page_id=581
[番外編]機械学習プログラミング 日向坂46で顔認識してみた。@Python (2)
https://oracle.tf17.net/plsql/?page_id=607

4. 学習~モデル作成処理

いよいよ学習処理を行います。機械学習っぽくなってきました。

今回の学習処理ではKeras(ケラス)というライブラリを使用します。
機械学習のライブラリというと、Scikit-Learn(サイキット・ラーン)やTensorflow(テンソルフロー)、Pytorch(パイトーチ)が有名ですが、Kerasはニューラルネットワークを構築するためのライブラリでTensorflowをバックエンドで使用することができます(他にもいくつかのライブラリを使用できます)。

 学習処理として行う内容は、前の処理で加工した全ての画像ファイルの一覧を配列(リスト)に格納します。
 ただし、作成したモデルの検証(テスト)用のデータを分ける必要があります。

 そのため、サイズ加工のみを行った「_norm.jpg」で終わるファイルのうち、9割のデータをモデル作成のための学習用(train)と、検証用(test)に分けます。
 学習用データには、サイズ加工以外の加工も行った全ファイルを含めます。
 実装としては、サイズ加工のみを行ったうちの1割をテスト用に。全ファイルからテスト用ファイルを除いた分を学習用データとします。

 作成した画像データは、numpy配列形式バイナリデータとして「images_obj.npy」というファイル名で一旦保存します。
(後から気付きましたが、参考元にあるように、学習用・検証用に分ける部分とデータの水増しは同時に行った方が効率が良かったですね・・・)

 コード中、X、X_train、X_testに格納されるのがイメージデータです。

 Y、y_train, y_testには、データのラベルを保持します。
 Kerasでは、このラベルもnp_utils.to_categoricalを用いてベクトル化して保持するそうです。
【技術】kerasのnp_utils.to_categoricalについて
http://hatakazu.hatenablog.com/entry/2017/06/08/045953

train()関数では、学習データで学習処理を行い、そこで作成されたモデルを用いて、検証用データで検証します。
実行すると、その結果が、

 loss=0.010307279205
 accuracy=0.9956011772

というような形で出力されます。

機械学習の肝は、下記ソースコード中の model = Sequential() で生成したインスタンスに対してひたすらADDしていく、このADDの内容なのですが、、、下記サイトの内容を参考にしています。

<参考サイト> 機械学習で乃木坂46メンバーの誰に似ているかを判定する簡易Webアプリケーションを作った一連の流れ (https://qiita.com/yu8muraka3/items/81360c48a9c067c2ccdb)

認識率が約90%を目指して実行してみました。パラメータは多少調整しています。
GPU使用した場合は、メモリが3Gしか使用できなかったため、下記が限界でした。

 * batch_size=80
 * epochs=300

CPUを使用した場合は、メモリが32GBあったので下記のパラメータでも大丈夫でした。あまり無闇矢鱈に上げても効果は無いようです。
計算が収束すると、epochsの値に到達していなくても処理を終了します。

 * batch_size=128
 * epochs=700

batch_sizeはあまり大きくしすぎるとメモリの少ないマシンではエラーになってしまいます。
epochsは、大きければ大きいほど深く学習します。少なければそれだけ精度が下がります。

ここで作成されたモデルは「model_hitnatazaka.h5」という名称で保存します。

facerecg4_train.py
#========================================
# 日向坂46メンバーの顔認識
# 5.学習〜モデルの作成
# ※numpy は新しすぎるとNG。1.18→1.16.2にダウングレードした。
# How To
# Exec Command Sample.
#   python3 facerecg5_train.py
# Options
# パラメータなし。
#========================================
import matplotlib.pyplot as plt
import sys
import os
import datetime as dt
import argparse
import numpy as np
from keras.models import Sequential
from keras.layers import Conv2D, MaxPooling2D
from keras.layers import Activation, Dropout, Flatten, Dense
from keras.utils import np_utils
from keras.optimizers import Adam
from keras.callbacks import EarlyStopping
import facerecg_image_comn as fic
  
#分類対象のカテゴリーを選ぶ
# categories = ["加藤史帆","金村美玖","佐々木久美","佐々木美玲","上村ひなの","丹生明里","潮紗理菜","東村芽依","齊藤京子"]
# IMAGE_SIZE_X = 128
# IMAGE_SIZE_Y = 128
# nb_classes = len(fic.categories)
  
# モデル作成
def train():
    # 開始時刻出力
    starttime = dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    print("Start:" + starttime)
  
    #画像データ(.npy)を読み込み
    X_train, X_test, y_train, y_test = np.load(fic.NPY_FILENAME)
    X_train = X_train.astype("float") / 256
    X_test = X_test.astype("float") / 256
    y_train = np_utils.to_categorical(y_train, fic.NB_CLASSES)
    y_test = np_utils.to_categorical(y_test, fic.NB_CLASSES)
  
    # tensorflow のGPU使用メモリ制限→必要な分だけ確保するようにする
    config = tf.ConfigProto()
    config.gpu_options.allow_growth = True
    config.gpu_options.per_process_gpu_memory_fraction=0.9 # 最大値の80%まで
    set_session(tf.Session(config=config))
  
    # モデルの定義
    model = Sequential()  # 層を積み重ねたモデルの定義
    model.add(Conv2D(input_shape=(fic.IMAGE_SIZE, fic.IMAGE_SIZE, 3), filters=32, kernel_size=(2, 2), strides=(1, 1), padding="same"))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Dropout(0.25))
    model.add(Conv2D(filters=32, kernel_size=(2, 2), strides=(1, 1), padding="same"))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Flatten())
    model.add(Dense(256))
    model.add(Activation("sigmoid"))
    model.add(Dropout(0.5))
    model.add(Dense(fic.NB_CLASSES)) # カテゴリの数(11)に合わせる
    model.add(Activation('softmax'))
  
    model.compile(loss='binary_crossentropy',
        optimizer=Adam(lr=1e-5),
        metrics=['accuracy'])
  
    early_stopping = EarlyStopping(monitor='val_loss', patience=20, verbose=0)
    # early_stopping = EarlyStopping(monitor='val_loss', patience=15, verbose=0)
  
    model.fit(X_train, y_train, batch_size=80, epochs=300, validation_split=0.1, callbacks=[early_stopping])  # GPU
  
    score = model.evaluate(X_test, y_test)
    print('loss=', score[0])
    print('accuracy=', score[1])
  
    #モデルを保存
    model.save(fic.MODEL_FILENAME)
  
    # 終了時刻出力
    print("Start:" + starttime)
    print("Finished:" + dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
  
# サンプルデータの追加
def add_sample(cat, fname):
    global X, Y
    # データ読込
    img = Image.open(fname)
    img = img.convert("RGB")    # RGB(8bit x 3)に変換
    # サイズ変更したオリジナル画像をリストに追加
    X.append(np.asarray(img))
    Y.append(cat)   # 画像に紐付けるメンバーの名前を登録
  
# ファイルごとにループ、add_sampleでサンプルに登録
def make_sample(files):
    global X, Y
    X = []; Y = []
    for cat, fname in files:
        add_sample(cat, fname)
        # add_sample(cat, fname, is_train)
    return np.array(X), np.array(Y)
  
def arg_parse():
    parser = argparse.ArgumentParser(description='image increase.')
    parser.add_argument('-i', '--indir', default='./increased', type=str, help='input directory path')
    return parser.parse_args()
  
def main():
    # 引数制御
    args = arg_parse()
  
    allfiles = []
    testfiles = []
    # 全メンバーのリサイズしたファイルの一覧を取得しリストに保存
    for idx, cat in enumerate(fic.CATEGORIES):
        # 非加工ファイル名一覧を取得しリストに追加
        for fn in glob.glob(os.path.join(args.indir, cat, "*_resz.jpg")):
            testfiles.append((idx, fn))
        # 全ファイル名一覧を取得しリストに追加
        for fn in glob.glob(os.path.join(args.indir, cat, "*.jpg")):
            allfiles.append((idx, fn))
  
    # ファイル名をランダムに並べ変える
    random.shuffle(testfiles)
  
    # TEST用のデータ件数を算出(全体件数×(1−トレーニングデータ割合))
    th = math.floor(len(testfiles) * (1.0 - fic.TRAIN_DATA_PER))
    # TEST用データファイルのリストを作成
    test_data_files = testfiles[0:th]
  
    # 全体ファイル一覧からTEST用データの差分をトレーニング用データとする
    train_data_files = list(set(allfiles) - set(test_data_files))
  
    # Train用、Test用、それぞれサンプルを作成
    X_train, y_train = make_sample(train_data_files)
    X_test, y_test = make_sample(test_data_files)
    xy = (X_train, X_test, y_train, y_test)
    # すべてのデータをファイルに保存
    np.save(fic.NPY_FILENAME, xy)
    print("ok", len(y_train))
  
    # トレーニング
    train()
    print("train and test finished.")
  
if __name__ == '__main__':
    main()
  

5. 顔認識処理

作成したモデルと画像から、いよいよ顔認識処理を行います。
モデルファイルと、認識を行う画像ファイルを読み込み、model.predictを実行します。結果はlistで戻されます。
それだけです。あとは結果を表示します。

というわけで実行してみました。

ココイチのポスター画像から顔部分のみを抽出し、検証データとして使用します。
最初はこちらの方です。

引用元:ココイチ de もっとHAPPY!キャンペーン
https://www.ichibanya.co.jp/cp/hinatazaka46/

加藤史帆さん・・・のはずなのですが。

	Input Filename:out_kokoichi-hinatazaka46_001_kato.jpg
	['0.55324596' '金村美玖']
	['0.23876919' '加藤史帆']
	['0.07724109' '上村ひなの']
	['0.05002388' '齊藤京子']
	['0.031508967' '小坂菜緒']

・・・おや?

ほ、他の人で試してみましょう。
こちらは東村芽依さん。

引用元:ココイチ de もっとHAPPY!キャンペーン
https://www.ichibanya.co.jp/cp/hinatazaka46/
	Input Filename:out_kokoichi-hinatazaka46_002_higashi.jpg
	['0.7443388' '東村芽依']
	['0.116943486' '齊藤京子']
	['0.04051612' '潮紗理菜']
	['0.038030796' '佐々木美玲']
	['0.018660467' '加藤史帆']

・・・若干数値が低い気がしますが、当たってます!

こんな感じで続けていくと。

・河田陽菜さん
 Input Filename:out_kokoichi-hinatazaka46_003_kawada.jpg
 ['0.4973656' '上村ひなの']!
・小坂菜緒さん
 Input Filename:out_kokoichi-hinatazaka46_004_kosaka.jpg
 ['0.3463321' '金村美玖']!
・丹生明里さん
 Input Filename:out_kokoichi-hinatazaka46_005_nibu.jpg
 ['0.96843094' '丹生明里']
・佐々木美玲さん
 Input Filename:out_kokoichi-hinatazaka46_006_sasamirei.jpg
 ['0.48137724' '佐々木美玲']
・齊藤京子さん
 Input Filename:out_kokoichi-hinatazaka46_007_saito.jpg
 ['0.8567822' '齊藤京子']
・金村美玖さん
 Input Filename:out_kokoichi-hinatazaka46_008_kanemura.jpg
 ['0.9127813' '金村美玖']

という結果に。
丹生さんの数値がとても良いです。
一方で、加藤史帆さんと河田陽菜さん、小坂菜緒さんの3人が正しく認識できませんでした。
8人中3人も外れてしまうとは・・。

 色々試しましたが、モデルの作成には実績のあるものを使っているので、データに問題があるのかもしれません。
 確かに、私自身、本当にこの人で正しいのか迷いながら分類したデータがあったような・・・。

 あとは、いろいろ検証していて思ったのですが、画像データはある程度統一感を持たせた方が良さそうに思えます。
 たとえ正しい人物の写真であっても、他の写真と系統が違う写真は、敢えて含めない方が良いのかも知れません。
(例えば笑顔の写真9枚に驚きの表情1枚混ざるより、笑顔10枚の方が良いとか。)

 正解の加藤史帆さんの写真はこちら(オフィシャルサイトへのリンクのみ)。
 https://www.hinatazaka46.com/s/official/artist/5

 著作権の都合で写真は載せられませんが、確かにラベリングをするときに迷った写真がいくつもありました・・・。
 目検で確信のあるデータだけを対象にして、再度モデルの作成からやり直しました。
 いくつか加藤さんかどうかあやしげな写真や、インスタ等向けに加工されたと思われる写真を除外して再学習しました。
 それらの写真を除外し、再度認識処理を行ってみました。
・・・すると。

	Input Filename:out_kokoichi-hinatazaka46_001_kato.jpg
	['0.6895172' '加藤史帆']
	['0.23512027' '金村美玖']
	['0.027899567' '上村ひなの']
	['0.014477699' '丹生明里']
	['0.011526121' '齊藤京子']

無事正しく認識できました!(^^)
(数字は相変わらず低いですが・・・)

他のメンバーも

Input Filename:./test/out_kokoichi-hinatazaka46_002_higashi.jpg
['0.9800908' '東村芽依']
['0.0056627304' '河田陽菜']
  
Input Filename:./test/out_kokoichi-hinatazaka46_003_kawada.jpg
['0.9627408' '河田陽菜']
['0.031601068' '上村ひなの']
  
Input Filename:./test/out_kokoichi-hinatazaka46_004_kosaka.jpg
['0.985734' '小坂菜緒']
['0.007566488' '上村ひなの']
  
Input Filename:./test/out_kokoichi-hinatazaka46_005_nibu.jpg
['0.99909985' '丹生明里']
['0.00032555318' '金村美玖']
  
Input Filename:./test/out_kokoichi-hinatazaka46_006_sasamirei.jpg
['0.8101059' '佐々木美玲']
['0.12910224' '佐々木久美']
  
Input Filename:./test/out_kokoichi-hinatazaka46_007_saito.jpg
['0.97464097' '齊藤京子']
['0.016450714' '河田陽菜']
  
Input Filename:./test/out_kokoichi-hinatazaka46_008_kanemura.jpg
['0.93976444' '金村美玖']
['0.030080764' '丹生明里']

無事、全員正しく認識できました!(^^)
精度も前回から大幅に向上しています。

Posted by tfurukaw