Help us understand the problem. What is going on with this article?

機械学習で乃木坂46メンバーの誰に似ているかを判定する簡易Webアプリケーションを作った一連の流れ

More than 1 year has passed since last update.

はじめに

乃木坂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を使って画像収集

※ただスクレイピングで集めるのは上限値がある上に、重複することがままあったので、手動でも集めてデータを補強しました。

2. 画像加工

今回の場合は

顔画像の切り抜き+画像の水増し

の2ステップです。

①顔画像の切り抜き

これはお決まりのOpenCVを利用しました。

※参考リンク
大量の画像から顔の部分のみトリミングして保存する方法

②画像の水増し

集めた画像だけではデータ数が少ないので、画像にエフェクトを掛けて水増しをします。あんまりやると、過学習を引き起こすようなのでやりすぎないようにしましょう。また、水増ししたデータがテストデータに入らないように注意する必要があります。水増ししたデータは訓練データのみに使用しましょう。

機械学習で乃木坂46を顏分類してみたの画像の水増しの項を参考にしました。

makedata.py
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を顏分類してみた

train.py
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だと動かないみたいなことがありました。

こんなエラー文でしたが

Error
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 として上がっていたので無事解決しました。もし同じ症状の方がいらっしゃったら参考に。

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のポート開放のやり方

・学習済みモデルのバイナリファイルが破損

Error
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で画像アップローダー

リンク先にはログイン機能もあるのですが、それは省いて適宜カスタムして最終的にはこんな感じに。

app.py
# -*- 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側でそれを出力したり出来ます。便利ですね。

Example
<p><span class = "name">「{{name}}」</span>({{rate}}%)です!!</p>

{{変数}}のカタチで受け取れます。

まとめ

以上が一連の流れです。ざっくりとしか説明してなかったり、無駄に丁寧だったり読みにくいかもしれませんが、参考になったら嬉しいです。

(実は作ったWebアプリケーションを初めて公開までこぎつけたのですが、やっぱりモノづくりは楽しいですね。達成感も最高です。ぜひ作ってみて下さい!)

そして遊んでみてください! http://www.nogidare.net

yu8muraka3
慶應SFC在学。プログラミングはまだまだです。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした