#はじめに
乃木坂46の一部メンバーの顔画像を機械学習に掛けて作成した学習済みモデルを利用して、ユーザーが顔画像をアップすると誰に似ているかを判定してくれる一連のWebアプリケーションを作りました。
大学の春休みで暇を持て余していたので作ったのですが、思ったより時間がかかり1ヶ月半ほどの製作期間になりました。
僕は雰囲気でDeepLearningに触れた程度の初学者なので、同じくらいの方向けに、機械学習からWebアプリケーションまで落とし込む一連の"流れ"の参考になればと思い、書かせて頂きます。ちなみにググりまくってゴリ押しで作ったので、スマートでない所あるかと思いますが、ご了承ください。
Web上に少ない情報は詳しめに書きますが、原則まとめ的に書くので詳細が知りたい方は各種リンクを参考に。
完成品(1ヶ月後はリンク切れしてるかも): http://www.nogidare.net
ソースコード: https://github.com/yu8muraka3/DeepNogizaka
#環境構成
全体の環境は以下のとおり。
言語は機械学習とWebアプリはPythonで、フロントエンドはHTML+CSS+JavaScriptです。
##機械学習側
・学習ライブラリ
Keras (Tensorflow)
・学習環境
AWS Deep Learning AMI (Ubuntu) Version 5.0 p2.xlarge
##アプリケーション側
・サーバー
Conoha VPS メモリ 1GB/CPU 2Core SSD 50GB (CentOS7)
・アプリケーション構成
Flask + Nginx + uWSGI
・環境
Python 3.6.3, anaconda3-4.3.1
#機械学習
たくさん記事が転がっていたので、それらを組み合わせて作りました。良い時代に生まれました。Googleが無い時代にGoogle作ったラリー・ペイジとセルゲイ・ブリンって凄いなぁと毎度感心します。
##1. 画像収集
画像はGoogleのAPIを使い収集しました。以下のリンクを参考にすれば出来ると思います。
[Google Custom Search APIを使って画像収集]
(https://qiita.com/onlyzs/items/c56fb76ce43e45c12339)
※ただスクレイピングで集めるのは上限値がある上に、重複することがままあったので、手動でも集めてデータを補強しました。
##2. 画像加工
今回の場合は
顔画像の切り抜き+画像の水増し
の2ステップです。
###①顔画像の切り抜き
これはお決まりのOpenCVを利用しました。
※参考リンク
大量の画像から顔の部分のみトリミングして保存する方法
###②画像の水増し
集めた画像だけではデータ数が少ないので、画像にエフェクトを掛けて水増しをします。あんまりやると、過学習を引き起こすようなのでやりすぎないようにしましょう。**また、水増ししたデータがテストデータに入らないように注意する必要があります。**水増ししたデータは訓練データのみに使用しましょう。
機械学習で乃木坂46を顏分類してみたの画像の水増しの項を参考にしました。
from PIL import Image
import os, glob
import numpy as np
import random, math
import cv2
root_dir = "./photo_select_out64/"
categories = ["asuka_out", "ikoma_out", "ikuta_out", "maiyan_out", "nanase_out", "yasushi_out"]
nb_classes = len(categories)
image_size = 64
# 画像データを読み込む
X = [] # 画像データ
Y = [] # ラベルデータ
def data_append(data, cat):
X.append(data)
Y.append(cat)
def add_sample(cat, fname, is_train):
img = Image.open(fname)
img = img.convert("RGB")
img = img.resize((image_size, image_size))
data = np.asarray(img)
X.append(data)
Y.append(cat)
if not is_train: return
filter1 = np.ones((3, 3))
flip = lambda x: cv2.flip(x, 1)
thr = lambda x: cv2.threshold(x, 100, 255, cv2.THRESH_TOZERO)[1]
filt = lambda x: cv2.GaussianBlur(x, (5, 5), 0)
methods = [flip, thr, filt]
for f in methods:
processing_data = f(data)
data_append(processing_data, cat)
for f in methods[1:]:
processing_data = flip(f(data))
data_append(processing_data, cat)
data_append(thr(filt(data)), cat)
data_append(flip(thr(filt(data))), cat)
def make_sample(files, is_train):
global X, Y
X = []; Y = []
for cat, fname in files:
add_sample(cat, fname, is_train)
return np.array(X), np.array(Y)
allfiles = []
for idx, cat in enumerate(categories):
image_dir = root_dir + "/" + cat
files = glob.glob(image_dir + "/*.jpg")
for f in files:
allfiles.append((idx, f))
random.shuffle(allfiles)
th = math.floor(len(allfiles) * 0.8)
train = allfiles[0:th]
test = allfiles[th:]
X_train, y_train = make_sample(train, True)
X_test, y_test = make_sample(test, False)
xy = (X_train, X_test, y_train, y_test)
np.save("./6obj.npy", xy)
print("ok", len(y_train))
###※つまづいたポイント
・保存したファイルが大容量のバイナリファイルで、通常通りにGitHubでpushすると弾かれました
→コチラを参考にGit LFSを活用すれば上手くいきました。GitHubに100MB超のファイルを置く -- git push に失敗してからの対処方法
##3. 機械学習
手元のMacBookではさすがに無理だったので、AWSにインスタンスを立てて学習させました。立てられるインスタンス上限が0の場合はリクエストすれば開いてくれます。
上限は↓で確認できます。
EC2 Service Limits
・使ったインスタンス
Deep Learning AMI (Ubuntu) Version 5.0 p2.xlarge
AWSにあらかじめ設定されたanaconda仮想環境(リンク先はKeras)に入るコマンド一覧です。起動時のコマンドラインにも記載されてますが。
https://docs.aws.amazon.com/ja_jp/dlami/latest/devguide/tutorial-keras.html
僕の場合は以下コマンドでKeras(バックエンドにTensorflow) + CUDA8環境に入りました。
$ source activate tensorflow_p36
初めて使ってみましたが、ストレス無く使えました。
###学習アルゴリズム
コチラの記事をベースにしつつ、書籍や様々なサイトを見て調整を施しました。
※参考リンク
機械学習で乃木坂46を顏分類してみた
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
#分類対象のカテゴリーを選ぶ
categories = ["asuka", "ikoma", "ikuta", "maiyan", "nanase", "yasushi"]
nb_classes = len(categories)
#画像データを読み込み
X_train, X_test, y_train, y_test = np.load("./6obj.npy")
X_train = X_train.astype("float") / 256
X_test = X_test.astype("float") / 256
y_train = np_utils.to_categorical(y_train, nb_classes)
y_test = np_utils.to_categorical(y_test, nb_classes)
# モデルの定義
model = Sequential()
model.add(Conv2D(input_shape=(64, 64, 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(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(Dense(128))
model.add(Activation('sigmoid'))
model.add(Dropout(0.5))
model.add(Dense(6))
model.add(Activation('softmax'))
model.compile(loss='binary_crossentropy',
optimizer=Adam(lr=1e-5),
metrics=['accuracy'])
early_stopping = EarlyStopping(monitor='val_loss', patience=15, verbose=0)
model.fit(X_train, y_train, batch_size=64, epochs=500, validation_split=0.1, callbacks=[early_stopping])
score = model.evaluate(X_test, y_test)
print('loss=', score[0])
print('accuracy=', score[1])
#モデルを保存
model.save("my_model.h5")
精度は89.2%出てくれました。当初過学習が起きていたのですが、Dropoutやearlystopping等を駆使しながら解消しました。
accuracyがいくら高くても訓練データに対してだけで汎化性能が劣っている可能性があるので、val_loss等の指標も見ながら作っていくのが良さそうです。
過学習に関しては様々な記事を読んで参考にしましたが、コチラの記事はコンパクトに纏まっていて良いです。
※参考リンク
Dropout:ディープラーニングの火付け役、単純な方法で過学習を防ぐ
###※つまづいたポイント
AWSのUbuntuのp2.xlargeインスタンスで上記のanaconda仮想環境に入って学習をしたのですが、ローカルでは動くのにAWSだと動かないみたいなことがありました。
こんなエラー文でしたが
ImportError: /home/ubuntu/anaconda3/envs/tensorflow_p36/bin/../lib/libstdc++.so.6: version `CXXABI_1.3.9' not found (required by /home/ubuntu/anaconda3/envs/tensorflow_p36/lib/python3.6/site-packages/scipy/sparse/_sparsetools.cpython-36m-x86_64-linux-gnu.so)
ググったらgithubに[issue] (https://github.com/NLeSC/root-conda-recipes/issues/32) として上がっていたので無事解決しました。もし同じ症状の方がいらっしゃったら参考に。
#Webアプリケーション
WebアプリケーションはPythonの軽量フレームワークであるFlaskを使いました。ちょっとしたアプリケーション作りたいだけなら十分ですね。とても使いやすかったです。
##1. サーバー設定
自分にプレッシャーかける意味でもConohaで有料サーバーを立ててやりました。SSHの鍵設定などはこの記事が分かりやすかったです。
※参考リンク
ConoHa と CentOS7 でサーバー構築
##2. アプリケーション構築
構造はNginx+uWSGI
サーバーの知識がほぼ皆無だった(今も)ので、実は1番手こずりました。
この程度の簡易アプリならサーバーレスでも作れそうということに途中で気付きましたが、経験だと割り切りました。
※参考リンク
uwsgi + nginx + flaskで簡単なWEBアプリの構築
conohaVPSでDjango開発環境を構築する
How To Serve Flask Applications with uWSGI and Nginx on Ubuntu 16.04
flask を uWSGI と Nginx でデプロイする
###※つまづいたポイント
・NginxとuWSGIの連携
→PATHの設定がどうやらおかしかったようです。
・ちゃんと起動しているのに繋がらない
→ポートが開いていなかったようです。【すぐわかる】CentOSのポート開放のやり方
・学習済みモデルのバイナリファイルが破損
File "h5py/_objects.pyx", line 54, in h5py._objects.with_phil.wrapper
File "h5py/_objects.pyx", line 55, in h5py._objects.with_phil.wrapper
File "h5py/h5f.pyx", line 78, in h5py.h5f.open
OSError: Unable to open file (file signature not found)
→GitHubからpullして動かそうとしていたのですが、何度やってもバイナリファイルのファイルサイズがローカルやリポジトリよりも明らかに少なく、エラー文を調べた所ファイルが破損しているっぽいと。なのでファイルアップローダのFileZillaで直接ローカルからアップロードしたら普通に動きました。
・ブラウザキャッシュを消してもCSS等の変更が反映されない
→これは僕の設定によるものかもしれませんが、uWSGIをrestartすればちゃんと反映してくれます。
$ systemctl restart uwsgi.service
##3. Flaskアプリケーション
###①Flaskセットアップ
かなり扱いやすく、誰でも簡単に出来ると思います。
※参考リンク
Flaskの簡単な使い方
###②画像アップロード機能実装
画像アップロードもらくらくです。
※参考リンク
Flaskで画像アップローダー
リンク先にはログイン機能もあるのですが、それは省いて適宜カスタムして最終的にはこんな感じに。
# -*- coding: utf-8 -*-
import os, re
from flask import Flask, render_template, request, redirect, url_for, send_from_directory, session
from werkzeug import secure_filename
from detect import start_detect
app = Flask(__name__)
@app.route('/')
def index():
name = "Hello World"
return render_template('index.html', title='flask test', name=name)
###########################
# 以下画像アップロード処理
##########################
#iPhoneの画像ファイル形式への対応
#画像アップロード
UPLOAD_FOLDER = './static/asset/uploads'
ALLOWED_EXTENSIONS = set(['jpg'])
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['SECRET_KEY'] = os.urandom(24)
#アップロードを許可する画像ファイル
def allowed_file(filename):
# ファイル名の拡張子より前を取得し, フォーマット後のファイル名に変更
filename = re.search("(?<!\.)\w+", filename).group(0) + "." + 'jpg'
return '.' in filename and \
filename.rsplit('.', 1)[1] in ALLOWED_EXTENSIONS
#画像POST処理
@app.route('/send', methods=['GET', 'POST'])
def send():
if request.method == 'POST':
img_file = request.files['img_file']
if img_file and allowed_file(img_file.filename):
filename = secure_filename(img_file.filename)
img_file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
img_url = '/uploads/' + filename
predict_name, predict_enname, rate = start_detect(filename)
if predict_name != None:
if predict_enname != "Akimoto Yasushi":
predict_enname = predict_enname.lower()
enname = predict_enname.split()
url_enname = enname[1] + "." + enname[0]
else:
url_enname = None
print(predict_name)
# return render_template('index.html', img_url=img_url)
return render_template('result.html', name=predict_name, urlname=url_enname, rate=rate, filename=filename)
else:
return render_template('index.html', error="error")
else:
return ''' <p>許可されていない拡張子です</p> '''
else:
return redirect(url_for('index'))
#アップロードされた画像ファイル
@app.route('/uploads/<filename>')
def uploaded_file(filename):
return send_from_directory(app.config['UPLOAD_FOLDER'], filename)
## おまじない
if __name__ == "__main__":
app.run()
###③HTMLやCSS等で見た目を整える。
Flaskはtemplateをrenderする時に、いくつも戻り値を渡せるようなのでHTML側でそれを出力したり出来ます。便利ですね。
<p><span class = "name">「{{name}}」</span>({{rate}}%)です!!</p>
{{変数}}のカタチで受け取れます。
#まとめ
以上が一連の流れです。ざっくりとしか説明してなかったり、無駄に丁寧だったり読みにくいかもしれませんが、参考になったら嬉しいです。
(実は作ったWebアプリケーションを初めて公開までこぎつけたのですが、やっぱりモノづくりは楽しいですね。達成感も最高です。ぜひ作ってみて下さい!)
そして遊んでみてください! http://www.nogidare.net