451 views
# 機械学習でアホガールのさやかちゃんを判定してみる しらすです。この記事は[みす51代 Advent Calendar 2017](https://adventar.org/calendars/2304)(14日目)の記事です。「通学について語ります」はやめました。※時間なかったんで殴り書きしてる&コード汚い&ネタ要素なくてスミマセン^^;あとでちゃんとはてなとかにまとめます。 ## アニメ「アホガール」 2017年夏クールで放送してたコメディアニメです。**さやかちゃん is My Angel.** 手前からよしこ、さやかちゃん、あっくん、委員長です。 [公式サイト](http://ahogirl.jp/) ![](https://i.imgur.com/DI1AAHh.jpg) ## 今回の試み アホガールのアニメから抽出したランダムなキャラクターの顔を、教師あり機械学習を用いてキャラクターごとに分類するという試みです。さやかちゃんを〜と銘を打ちましたがせっかくなので主要キャラ全員でやります。今話題の機械学習ってやつです。なんでもできちゃうんでしょうね。知らないけど。 具体的な手順は以下のようにしました。 1. アホガールの第1話から無作為にキャラクターを抽出する。 2. 教師用のデータ作成、キャラクターごとに手動で分類する。 3. 教師用のデータを用いて機械学習、学習済みモデルを作成する。 4. アホガールの第2話から無作為にキャラクターを抽出する。 5. 学習済みモデルを用いて無作為に抽出したキャラクターを**機械学習で**分類する。 ## 機械学習 ニューラルネットワークをChainerで作って、やります。(ごめんなさい、ここはまとまってない) ## 手順1 キャラクターの無作為抽出(学習用) opencvを使います。単なるアニメの顔認識であれば、lbpcascade_animefaceという最強のものがあるので、それをありがたく使わせて頂きました。 ```python= import cv2 ESC_KEY = 27 # Escキー INTERVAL = 1 # 待ち時間 VIDEO_FILE = "video/ahogirl.mp4" WINDOW_NAME = "video" CASCADE_FILE = "lbpcascade_animeface.xml" cascade = cv2.CascadeClassifier(CASCADE_FILE) def detectFace(image): gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) gray = cv2.equalizeHist(gray) faces = cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5, minSize=(50, 50)) return faces video = cv2.VideoCapture(VIDEO_FILE) cv2.namedWindow(WINDOW_NAME) end_flag, frame = video.read() frame_cnt = 0 face_frame_cnt = 0 while end_flag: frame_cnt = frame_cnt + 1 print(frame_cnt) if frame_cnt % 20 == 0: for (x, y, w, h) in detectFace(frame): face_frame_cnt = face_frame_cnt + 1 croped = frame[y:y+h, x:x+w] croped = cv2.resize(croped, (50, 50)) cv2.imshow(WINDOW_NAME, croped) cv2.imwrite("out/1_" + str(face_frame_cnt) + ".jpg", croped) print('croped!') # Escキーで終了 key = cv2.waitKey(INTERVAL) if key == ESC_KEY: break # 次のフレーム読み込み end_flag, frame = video.read() # 終了処理 cv2.destroyAllWindows() video.release() print('finished') ``` outフォルダに50x50に調整されたjpgファイルが出力されます。結果です。アニメ1話からは全部で562枚の画像が得られました。 ![](https://i.imgur.com/WRNMf7O.png) ## 手順2 教師データの作成 機械学習をするために、教師用のデータとして無作為抽出した画像が誰なのか分類しておく必要があります。ここは人力です(地味に大変でした)。フォルダ構成はこんな感じです。 ![](https://i.imgur.com/hRs9caM.png) ## 手順3 教師あり機械学習、学習済みデータの作成 chainerを使って学習させてみます。v3.1.0を使っていたのですが、ググってもchainer(v1)の役に立たないコードの情報が錯綜してて結構手こずりました。よわよわプログラマーなので... my_chain_model.py ```python= import chainer import chainer.functions as F import chainer.links as L class MyChain(chainer.Chain): def __init__(self): super(MyChain, self).__init__() with self.init_scope(): self.conv1 = L.Convolution2D(None, 16, 5, pad=2) self.conv2 = L.Convolution2D(None, 32, 5, pad=2) self.l3 = L.Linear(None, 256) self.l4 = L.Linear(None, 5) def __call__(self, x): h = F.max_pooling_2d(F.relu(self.conv1(x)), ksize=5, stride=2, pad=2) h = F.max_pooling_2d(F.relu(self.conv2(h)), ksize=5, stride=2, pad=2) h = F.dropout(F.relu(self.l3(h))) y = self.l4(h) return y ``` train.py ```python= import os import glob from itertools import chain import chainer from chainer.datasets import LabeledImageDataset from chainer import iterators, training, optimizers, datasets, serializers from chainer.training import extensions, triggers from chainer.dataset import concat_examples import chainer.functions as F import chainer.links as L from my_chain_model import MyChain chainer.config.train = True # 画像フォルダのパス IMG_DIR = 'classed_image' # 各キャラクターごとのフォルダ dnames = glob.glob('{}/*'.format(IMG_DIR)) # 画像ファイルパス一覧 fnames = [glob.glob('{}/*.jpg'.format(d)) for d in dnames] fnames = list(chain.from_iterable(fnames)) # それぞれにフォルダ名から一意なIDを付与 labels = [os.path.basename(os.path.dirname(fn)) for fn in fnames] dnames = [os.path.basename(d) for d in dnames] labels = [dnames.index(l) for l in labels] d = LabeledImageDataset(list(zip(fnames, labels))) def transform(data): img, label = data img = img / 255. return img, label train = chainer.datasets.TransformDataset(d, transform) epoch = 10 batch = 5 model = L.Classifier(MyChain()) optimizer = optimizers.Adam() optimizer.setup(model) train_iter = iterators.SerialIterator(train, batch) updater = training.StandardUpdater(train_iter, optimizer) trainer = training.Trainer(updater, (epoch, 'epoch'), out='result') trainer.extend(extensions.dump_graph('main/loss')) trainer.extend(extensions.snapshot(), trigger=(epoch, 'epoch')) trainer.extend(extensions.LogReport()) trainer.extend(extensions.PrintReport(['epoch', 'main/loss', 'main/accuracy'])) trainer.extend(extensions.ProgressBar()) trainer.extend(extensions.PlotReport(['main/loss'], 'epoch', file_name='loss.png')) trainer.extend(extensions.PlotReport(['main/accuracy'], 'epoch', file_name='accuracy.png')) trainer.run() serializers.save_npz("mymodel.npz", model) ``` 実行コマンド ``` python train.py ``` mymodel.npz(学習済みモデル)が出力されました。 ## 手順4キャラクターの無作為抽出(実験用) アニメ第2話から手順2と全く同じように無作為抽出しました。全部で601枚のキャラクターの顔画像が得られました。 ## 手順5 学習済みモデルを用いて画像分類 手順3で得られたmymodel.npzを使って画像が誰かを判定します。判定結果によって画像をフォルダ分けします。以下プログラムです。 predict.py ```python= import os import shutil import glob import chainer from itertools import chain from chainer import iterators, training, optimizers, datasets, serializers from chainer.dataset import concat_examples import chainer.functions as F import chainer.links as L from my_chain_model import MyChain chainer.config.train = False # モデルの読み込み model = L.Classifier(MyChain()) serializers.load_npz("mymodel.npz", model) image_files = [glob.glob('predicted_image/in/*.jpg')] d = chainer.datasets.ImageDataset(list(chain.from_iterable(image_files))) def transform(data): return data / 255. test = chainer.datasets.TransformDataset(d, transform) folder_names = ['predicted_image/01_akkun', 'predicted_image/02_iincho', 'predicted_image/03_sayakachan', 'predicted_image/04_yoshiko', 'predicted_image/99_others'] for i in range(len(test)): x = test[i] y = F.softmax(model.predictor(x[None, ...])) shutil.copy(image_files[0][i], folder_names[int(y.data.argmax())]) print("{0}: {1}".format(image_files[0][i], folder_names[int(y.data.argmax())])) ``` 最初はinフォルダに全部入っていたものが... ![](https://i.imgur.com/l9p7YIv.png) フォルダごとにコピーされました! ![](https://i.imgur.com/5SwJzfq.png) ## 結果 ### 01_akkun ![](https://i.imgur.com/R3u82oN.png) うーん、モブの黒髪ガキンチョとか、あっくんの妹とか紛れ込んじゃってますねえ...まあ仕方ないのかな、髪の色おんなじだし。 ### 02_iincho ![](https://i.imgur.com/07wKJWS.png) これが一番微妙、半分くらいあっくんの妹とかあっくんが混ざってる。まあ委員長はそもそも1話であんまり出てなかったし学習のパターンが少ないってものありそう。 ### 03_sayakachan ![](https://i.imgur.com/6tfwwuW.png) あああああ!さやかちゃんが画面にいっぱいにぃぃぃぃ!!!!…とまでは集まりませんでしたね。ですが判定は概ね良好、黄色い服のガキンチョが混ざってしまいましたがまあそこは仕方なし。 ### 04_yoshiko ![](https://i.imgur.com/q2qCU2U.png) さすが主人公、いい感じです。言うことなし。 ### 99_others ![](https://i.imgur.com/9qvPMR9.png) 結構さやかちゃんとかが混ざってますね、ってかよく考えたらその他の学習ってなんだ。 ### lossとaccuracyの遷移 loss(正解データとの誤差)とaccuracy(正確さ)遷移です。10エポックでした、簡単には10回学習を繰り返したということです。ちゃんと学習を繰り返すごとに誤差が減って正確さが上がっています。 ![](https://i.imgur.com/YzM9OxO.png) ![](https://i.imgur.com/Ui94HyO.png) ## 結論 教師用画像少なかった?わりにまあまあ分類できた。機械学習ってすごい!(小並) ## 今後やりたいこと - 画像を水増し(回転させたり)すること。 - GPUで計算させるようにすること。 - アニメを流しながらリアルタイムにこのキャラクターは誰か判定すること。 ## 参考 - [ChainerでMTGのカードを分類する(識別フェーズ)](http://catalina1344.hatenablog.jp/entry/2017/05/04/171318) - [ChainerのTrainerを使ってみた](http://nonbiri-tereka.hatenablog.com/entry/2016/10/25/081405) - [Chainerでアニメキャラクターの顔画像を分類する](https://qiita.com/mitmul/items/5502ecdd2f0b444c427f) - [python: chainerを使って化物語キャラを認識させるよ!](http://www.mathgram.xyz/entry/chainer/bake/part0) - とっても参考になったが、載っているサンプルコードがv1系で使えなかった。ニューラルネットの構成は丸パクリさせていただきました。 次回はくまちゃんです。