プログラミング素人のはてなブログ

プログラミングも電気回路も専門外の技術屋の末端が勉強したことや作品をアウトプットするブログ。コードに間違いなど見つけられたら、気軽にコメントください。 C#、Python3、ラズパイなど。

ディープラーニングでAKB48を見分けられるか?

AKB48の顔認識に挑戦してみました。

人の目には認識の難しい(と言ったら怒られるか?)メンバーの認識も機械学習なら出来るのか?というチャレンジです。

学習用画像の収集

学習用画像はgoogle画像検索を用いて収集しました。
メンバーはいきなり48人に挑戦するのは、CPU的に過酷と推定されるので、10人程度集めることにしました。
Wikipediaを参考に総選挙の上位のメンバーを中心に13人に絞り収集しました。

def google_image_selenium(keyword):
    url = "https://www.google.com/search?q=" + keyword + "&source=lnms&tbm=isch&sa=X&ved=0ahUKEwjRpbL78LTbAhXIfrwKHTaVBLQQ_AUICigB&biw=1280&bih=620"
    driver=browserOpen("h") #ブラウザのインスタンスを起動
    driver.get(url)
    html=driver.page_source
    soup = BeautifulSoup(html, 'lxml')  ##lxmlを指定するほうがよい
    for link in soup.find_all("img"):
        try:
            image_link=""
            if "src" in link.attrs:
                image_link=link.attrs["src"]
            elif "data-src" in link.attrs:
                image_link = link.attrs["data-src"]
            if "http" in image_link:
                print(image_link)
                data = urllib.request.urlopen(image_link).read()
                tdatetime = dt.now()
                filename=keyword+'/'+tdatetime.strftime('%Y%m%d%H%M%S')+'.jpg'
                with open(filename, mode="wb") as f:
                    f.write(data)
            time.sleep(1)
        except urllib.error.URLError as e:
            print(e)

if __name__ == '__main__':
    menber_list = ["Kojima Mako","Yokoyama Yui", "Hashimoto Haruna", "Yokoyama Yui", "Kashiwagi Yuki", "Mutō Tomu", "Iriyama Anna","Takahashi Juri", "Megu Taniguchi", "Okada Nana", "Kojima Mako", "Miyazaki Miho","Kato Rena","Sasaki Yukari","Ōmori Miyū"]
    for keyword in menber_list:
        savedir = "./" + keyword
        if not os.path.exists(savedir):
            os.mkdir(savedir)  # フォルダーを作る
        google_image_selenium(keyword)

ブラウザのインスタンスは以前の記事参照。
s51517765.hatenadiary.jp

ここでは、普通のPCとCPUを使っているので、読み込ませる画像をなるべく小さくしたいので、Googleのサムネイルをそのまま取得しました。
高解像度で実施したい場合は、このサムネイルからLinkを取得してLink先の画像を取得する必要があります。
明らかに別人だったり、人でなかったり、他の人が写りこんでいたりする写真は削除してメンバー当たり80枚ぐらい集めました。
google画像検索結果のhtmlからimageのリンクを取得しますが、urllibでは少ししか取れなかったので、seleniumを使用しました。
seleniumで画面スクロールを入れてからhtmlを取得すればさらにたくさん取得することも可能だと思います。

顔の切り取りと水増し

次にOpen CVで顔検出をして、顔の部分を切り取ります。
note.nkmk.me
顔認識なんてこれでもすでにAIといっても通用するぐらいだと思うが、これがフリーで使えるとはすごい!
顔認識は最近のデジカメなんか必ず入っている技術ですね。

顔検出したものを切り取って、水増しのために左右に18度づつ振った画像を作成しました。画像の回転はOpen cvまたはPILでできます。PILでのコードは以前のAIプログラマ養成ギプスの記事参照下さい。

なぜわからないのですが、open cvで開けないファイルが結構ありました。
原因を調べたいところだが、長くなりそうなのでとりあえずあきらめました。
これによって、サンプル数が少なくなったメンバーは除外して、最低120枚得られたのが、11人になりました。

OpenCV(3.4.1) Error: Assertion failed (scn == 3 || scn == 4) in cv::cvtColor, file C:\projects\opencv-python\opencv\modules\imgproc\src\color.cpp, line 11147

ディープラーニングで学習

これをMnistの認識で使ったプログラムの改造で学習させます。
s51517765.hatenadiary.jp

Mnistでは画像を数値にしたデータを入力として使えたが、今度は普通の画像ファイルなので、同じようなnumpyの配列に変換する必要があります。

globというメソッドを用いて、フォルダのファイルにアクセスし、フォルダをカテゴリとして、ファイル(picture)を読み込み、X-trainという配列に加えていきます。

filelist = glob.glob("./*")
for picture in filelist:
      img = img_to_array(load_img(picture, target_size=(pixel, pixel)))
      X_train.append(img)

これをMnistと同じニューラルネットワークに入れてみました。

    model = Sequential()
    model.add(Dense(64, activation='relu', input_dim=pixel*pixel*3))
    model.add(Dense(len(inputlist), activation='softmax'))
    model.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['accuracy'])
    model.fit(x_train, y_train, batch_size=100, epochs=30, verbose=1)  # データを学習
>>> acc: 0.23636363636363636

→約23%

入出力が多くなったので、中間層を増やすといいのではないかとおもい、一層増やしてみます。

model = Sequential()
    model.add(Dense(512, activation='relu', input_dim=pixel*pixel*3))
    model.add(Dense(64, activation='relu', input_dim=512))
    model.add(Dense(64, activation='relu', input_dim=64))
    model.add(Dense(len(inputlist), activation='softmax'))
    model.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['accuracy'])
    model.fit(x_train, y_train, batch_size=100, epochs=30, verbose=1)  # データを学習
>>> acc: 0.33560606060606063

→約34%

もう一層増やしても、ほとんど変わらなかった。

ディープラーニングで学習 その2

違うモデルも試してみようと思います。
Kerasの公式サイトより、”VGG-likeなconvnet” を試してみます。
こちらは「畳み込み」をしているのが特徴です。Conv2Dのところ。
これによって、ぼかしやエッジ強調の効果が入ることで画像認識に強くなるそうです。
このサンプルコードにaccuracyの表示を追加して使用しました。
Sequentialモデルのガイド - Keras Documentation

    model = Sequential()
    # 入力: サイズが100x100で3チャンネルをもつ画像 -> (100, 100, 3) のテンソル
    # それぞれのlayerで3x3の畳み込み処理を適用している
    model.add(Conv2D(32, (3, 3), activation='relu', input_shape=(40, 40, 3)))
    model.add(Conv2D(32, (3, 3), activation='relu'))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Dropout(0.25))

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

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

    sgd = SGD(lr=0.01, decay=1e-6, momentum=0.9, nesterov=True)
    model.compile(loss='categorical_crossentropy', optimizer=sgd,metrics=['accuracy']) #accuracy を表示するのを追加

    model.fit(x_train, y_train, batch_size=32, epochs=10)
    os.chdir(root)
    model.save_weights("akb.hdf5") #学習結果を保存
    #score = model.evaluate(x_test, y_test, batch_size=32)
    score = model.evaluate(x_test, y_test, verbose=0)
    #score = model.evaluate(x_test, y_test)
    print(score[0])
    print(score[1])
>>> Loss 2.19148811860518
>>>acuracy 0.3

→30%

Dropoutを0.1に変更

>>> Loss 1.79073445124523
>>>acuracy 0.39099876634567

→約39%

Dropoutを0.0に変更

>>> Loss 1.8360017863186924
>>>acuracy 0.4090909094973044

→約41%
あまり変わらないのでこの間をとって、

Dropout=0.05にさらにepochs =30に。

>>> loss:  1.9628488020463424
>>> accuracy:  0.681818183443763

このときの学習履歴をグラフにすると、学習結果は98%まで行っているがテストは68%となり、過学習になっていると推定されます。
f:id:s51517765:20180614224106p:plain
繰り返し(epochs)は15ぐらいでいいのではないか?と思われます。
とりあえず、この(過学習気味の)学習データを用いて「中締め」として評価してみます。

評価

Twitterから適当な画像を探し、ディープニューラルネットワークに入れてみます。学習に使ったときと同様に、顔認識と切り取り、リサイズをして入力します。

f:id:s51517765:20180614224807j:plain
Prediction is Hashimoto Haruna【正解】


f:id:s51517765:20180614224843j:plain
Prediction is Iriyama Anna【正解】


f:id:s51517765:20180614224920j:plain
Prediction is Hashimoto Harun【不正解】→Kojima Mako


f:id:s51517765:20180614225028j:plain
Prediction is Megu Taniguchi【不正解】→Miyazaki Miho


f:id:s51517765:20180614225119j:plain
Prediction is Okada Nana【不正解】→Megu Taniguchi


f:id:s51517765:20180614225229j:plain
Prediction is Sasaki Yukari【不正解】→Yokoyama Yui


f:id:s51517765:20180614225306j:plain
Prediction is Kashiwagi Yuki【正解】


f:id:s51517765:20180614225337j:plain
Prediction is Kato Rena【正解】


f:id:s51517765:20180614225411j:plain
Prediction is Takahashi Juri【不正解】→Sasaki Yukari


f:id:s51517765:20180614225501j:plain
Prediction is Okada Nana【正解】

まとめ

テスト結果は5勝5敗で50%でした。
そうはいっても、Kerasを使うことで簡単にディープラーニングができました。
最適化には経験が必要なのではないかと思いますが、普通のPCでもここまでできるとは思いませんでした。
最大で300epochsを学習させましたが、これでも15分程度でした。
これで、11分の1の分類が50%の精度なら遊びとしては十分すぎますね。

最終的なディープラーニングコード

from keras.utils.np_utils import to_categorical
from keras.models import Sequential
from keras.layers import Dense
import os
import glob
from keras.preprocessing.image import array_to_img, img_to_array, list_pictures, load_img
import numpy as np
from keras.layers import Conv2D, MaxPooling2D
from keras.layers import Dense, Dropout, Flatten
from keras.optimizers import SGD

#顔認識のための
import cv2
face_cascade_path = 'C:/Users/***/Anaconda3/Lib/site-packages/cv2/data'
cascade_fie='haarcascade_frontalface_default.xml'
face_cascade_path=face_cascade_path +"/"+cascade_fie
face_cascade = cv2.CascadeClassifier(face_cascade_path)

root = "C:/Users/***/Projects/Akb48"
pixel = 40
input_image_count=110 #max120
test_image_count=10

def model_build():
    X_train,Y_train=[],[]
    X_test,Y_test=[],[]
    inputlist = ["OutHashimoto Haruna", "OutIriyama Anna", "OutKashiwagi Yuki", "OutKato Rena", "OutKojima Mako", "OutMegu Taniguchi", "OutMiyazaki Miho", "OutOkada Nana", "OutSasaki Yukari", "OutTakahashi Juri", "OutYokoyama Yui"]
    no=0 #正解ラベル
    print(len(inputlist))
    for name in inputlist:
        print("No=",no, " ",name)
        image_count=0
        os.chdir(root)
        folder = name
        os.chdir(folder)
        filelist = glob.glob("./*")
        for picture in filelist:
            img = img_to_array(load_img(picture, target_size=(pixel, pixel)))
            image_count += 1
            if image_count <=input_image_count:
                X_train.append(img)
                Y_train.append(no)
            elif image_count <= input_image_count+test_image_count: #max 120
                X_test.append(img)
                Y_test.append(no)
            elif image_count > input_image_count+test_image_count: #max 120
                break
        no+=1

    # arrayに変換
    x_train = np.asarray(X_train)
    x_test = np.asarray(X_test)
    y_train = np.asarray(Y_train)
    y_test = np.asarray(Y_test)

    # 画素を0~1の範囲に変換(正規化)
    x_train = x_train.astype('float32') / 255
    x_test = x_test.astype('float32') / 255

    # 正解ラベルをone-hot-encoding
    y_train = to_categorical(y_train, 11)
    y_test = to_categorical(y_test, 11)

    # VGG-likeなconvnet
    model = Sequential()
    # それぞれのlayerで3x3の畳み込み処理を適用している
    model.add(Conv2D(32, (3, 3), activation='relu', input_shape=(40, 40, 3)))
    model.add(Conv2D(32, (3, 3), activation='relu'))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Dropout(0.05))

    model.add(Conv2D(64, (3, 3), activation='relu'))
    model.add(Conv2D(64, (3, 3), activation='relu'))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Dropout(0.05))

    model.add(Flatten())
    model.add(Dense(256, activation='relu'))
    model.add(Dropout(0.05))
    model.add(Dense(11, activation='softmax'))

    sgd = SGD(lr=0.01, decay=1e-6, momentum=0.9, nesterov=True)
    model.compile(loss='categorical_crossentropy', optimizer=sgd,metrics=['accuracy'])

    model.fit(x_train, y_train, batch_size=32, epochs=15)
    os.chdir(root)
    model.save_weights("akb.hdf5") #学習結果を保存
    score = model.evaluate(x_test, y_test, verbose=0)

    print("------------------------------")
    print("Test loss: ",score[0])
    print("Test accuracy: ",score[1])

def model_read():
  # モデルを読み込む
  model = Sequential()
  model.add(Conv2D(32, (3, 3), activation='relu', input_shape=(40, 40, 3)))
  model.add(Conv2D(32, (3, 3), activation='relu'))
  model.add(MaxPooling2D(pool_size=(2, 2)))
  model.add(Dropout(0.05))

  model.add(Conv2D(64, (3, 3), activation='relu'))
  model.add(Conv2D(64, (3, 3), activation='relu'))
  model.add(MaxPooling2D(pool_size=(2, 2)))
  model.add(Dropout(0.05))

  model.add(Flatten())
  model.add(Dense(256, activation='relu'))
  model.add(Dropout(0.05))
  model.add(Dense(11, activation='softmax'))

  sgd = SGD(lr=0.01, decay=1e-6, momentum=0.9, nesterov=True)
  model.compile(loss='categorical_crossentropy', optimizer=sgd, metrics=['accuracy'])

  # 学習済みモデルの重みのデータを読み込み
  model.load_weights('akb_300.hdf5')
  return model

def check(file):
    try:
        src = cv2.imread(file)
        src_gray = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY)
        faces = face_cascade.detectMultiScale(src_gray)
        x_test=[]
        inputlist = ["Hashimoto Haruna", "Iriyama Anna", "Kashiwagi Yuki", "Kato Rena", "Kojima Mako","Megu Taniguchi", "Miyazaki Miho", "Okada Nana", "Sasaki Yukari", "Takahashi Juri", "Yokoyama Yui"]
        if len(faces) > 0:  # 顔認識できたら
            for rect in faces:
                # 顔だけ切り出して保存
                x = rect[0]
                y = rect[1]
                width = rect[2]
                height = rect[3]
            if width > 40 and height > 40:  # 一定サイズに
                src = src[y:y + height, x:x + width]
                resize_src = cv2.resize(src, (40, 40))
                cv2.imwrite("temp.jpg", resize_src) #なぜかうまくいかないので一旦保存して読み込む
            img = img_to_array(load_img("temp.jpg", target_size=(pixel, pixel)))
            model = model_read()
            x_test.append(img)
            x_test = np.asarray(x_test)  # テストデータをセット
            x_test = x_test.astype('float32') / 255

            # 判定
            res = model.predict(x_test)
            print("res = ", res)
            y = res.argmax()  # 値の中で最も値が大きいものが答え
            print(y)
            print("Prediction is ",inputlist[y])
        else:
            print("Face recognition failed!")
    except:
        pass

if __name__ == "__main__":
    #model_build()
    check("kashiwagi.jpg")
    check("kato.jpg")
    check("sasaki.jpg")
    check("okada.jpg")