こんにちは!
本格的な夏を目前に、湿度と気温で寝苦しい日々を過ごしています、ウーオエンジニアインターン生の林です!
インターンも4ヶ月を迎えようとしていて、いくつかプロダクトを進めている中で、
機械学習を用いたプロダクトのひとつである、
魚種画像分類のプロトタイプの開発が一段落したので、途中経過としてまとめたものをご紹介したいと思います!
プロダクト発足の経緯
ウーオのエンジニアは、UUUO Base で働くバイヤーチームのオペレーションの効率化を目指し日々開発しています。
僕はバイヤーチームが魚の写真をアップロードしている作業の瞬間に注目し、
現在自分が大学院での研究領域である、深層学習の画像応用の技術を使って、
その作業を軽減できないかと考えました。
そこで、漁港で撮られた魚介類の写真を、自動で分類する機能があれば、
バイヤーチームがより本質的な仕事に集中できるのでは!と思い画像分類のプロトタイプの開発をスタートしました。
今回はプロトタイプで実装した、web上で画像を投稿すると、魚種判別結果が表示されるところまでの手順を紹介したいと思います!
- キーワード -
- Rails
- Python
- Keras
- TensorFlow
- Flask
- Ajax
- Heroku
- 深層学習
まずはデータ収集
深層学習の中で、分類のタスクを行う際に最も一般的な手法は教師あり学習です。
教師あり学習とは、あらかじめ入力する画像に対して、その答えのデータを用意しておいて
そのデータセットを使って学習する手法です。
なので今回はuuuo.jpで配信されている過去の画像データ(写真に対する魚種の種類がわかっているもの)を集めてデータセットを作りました!
本来であれば自動でスクレイピングするコードを書いて、楽に画像を収集したかったのですが、
今回は手作業で画像のデータを集めました(お恥ずかしい、、、大変だった、、、)
今後は更にデータも増えてくるので、画像収集の自動化は次回の課題です!
深層学習のモデルでは、学習に用いた画像とその答えの判別しかできないので、
今回はこの12種類に絞って判別を行いました!
- 赤ガレイ
- エテガレイ
- ハタハタ
- ノドグロ・赤ムツ
- アジ
- カワハギ
- クロザコエビ
- 松葉ガニ
- コウイカ
- スルメイカ
- バイ貝(赤、白、黒)
- サザエ
また一般的に学習に用いるデータの量に比例して精度も高くなると言われているので、
学習に使える画像の枚数が多かったものから魚種を選びました!(各50枚程度)
データの前処理
次に集めたデータの前処理を行います。
画像の入力サイズを固定するために集めた写真をリサイズしていきます。
from PIL import Image, ImageOps
WIDTH = 256
HEIGHT = 144
img = Image.open(img_path)
img_resize = img.resize((WIDTH,HEIGHT))
また集めた写真の向きによって、判別精度を下げない(ロバスト性)と、データ量水増しのために
写真を上下と左右に反転させたデータも学習に使用します。
img_flip = ImageOps.flip(img_resize)
img_mirror = ImageOps.mirror(img_resize)
これで各種類150枚程度に水増しできました!このデータセットを使って学習を進めていきます!
モデルの作成
次に分類モデルを作成していきます。
今回は機械学習のフレームワークであるKerasを使用しました!
[models.py]
from keras.applications.vgg16 import VGG16
from keras.optimizers import Adam
from keras.layers import Activation, Dense, Dropout, Flatten, Input
from keras.models import Model
def model_vgg(class_num,height,width):
out_num = class_num
input_tensor = Input(shape=(height,width,3))
vgg = VGG16(include_top=False,input_tensor=input_tensor,weights=None)
x = vgg.output
x = Flatten()(x)
x = Dense(2048,activation="relu")(x)
x = Dropout(0.5)(x)
x = Dense(2048,activation="relu")(xT
x = Dropout(0.5)(x)
x = Dense(out_num)(x)
x = Activation("softmax")(x)
model = Model(inputs=vgg.inputs,outputs=x)
model.compile(optimizer=Adam(lr=1e-4),loss='categorical_crossentropy',metrics=['accuracy'])
return m
Kerasには代表的なモデルがあらかじめ用意されているので、その中で有名なVGG16
というモデルを使って学習を行いました。
https://arxiv.org/abs/1409.1556
全結合層に2048個のノードの層を2層追加し、学習率は0.0001としています。
Kerasでは上記のような簡単な記述でモデルを組めますし、初心者のかたも直感的にモデルを作ることができるのではないでしょうか?
学習してみる
さっそく集めた画像を使って学習しています!
ここで注意するのが、学習の精度を確認するために、
集めたデータを「学習用」と「テスト用」に分ける必要があります。
モデルは学習したデータを元に判断するため、学習に使った写真は正しく分類できるようになっています。
そのため学習で用いない画像、つまりモデルが初めて見るデータに対して
正しく判断できるかどうかが肝心です!
今回は、
【学習用】150枚 × 12種類 = 1800枚
【テスト用】3枚 × 12種類 = 36枚
という風にデータを切り分けて学習・テストを行います。
[train.py]
from PIL import Image
import numpy as np
from keras.utils import np_utils
import models
# paramaters -------------------
WIDTH = 256
HEIGHT = 144
epochs = 15
batch_size = 16
class_num = 12
# ------------------------------
X_train = []
Y_train = []
for img_path in akagarei_list:
img = Image.open(img_path)
img_array = np.array(img)
X_test.append(img_array)
# 赤ガレイにはラベル0を与える
Y_test.append([0])
.
.
.
.
for img_path in sazae_list:
img = Image.open(img_path)
img_array = np.array(img)
X_train.append(img_array)
Y_train.append([11])
# 画素値0~255を0~1の間に標準化する
X_train = np.asarray(X_train) / 255.
Y_train = np.asarray(Y_train)
# ラベルをone-hotなベクトルに変換する
Y_train = np_utils.to_categorical(Y_train, class_num)
# modelを読み込む
model = models.model_vgg(class_num, HEIGHT, WIDTH)
# 学習する
history = model.fit(
X_train,
Y_train,
batch_size=batch_size,
epochs=epochs,
verbose=1,
shuffle=True
)
# 学習済みの重みを保存する
model.save_weights("weights%d_%d.h5"%(class_num, epochs))
同じデータセットで何回繰り返し学習するかのエポック数やバッチサイズを設定し、
学習用の画像をnumpyで処理し、各画像に教師ラベルを与えます。それぞれの種類別に0~11までの数字でラベル付を行い、
Y_train = np_utils.to_categorical(Y_train, class_num)
ここでone-hotなベクトルに変換します。
one-hotなベクトルとは、例えばクラスが0であるならば
[1, 0, 0, 0, ...., 0]
クラスが2であるならば、
[0, 0, 1, 0, ...., 0]
というように各インデックスの要素のみが1となるようなベクトルのことです。
(インデックスは0から始まることに注意)
出力層の部分でこのベクトルとモデルによって予測された確率との誤差をとって学習を進めていきます。
そしてmodelを読み込み、historyで決められた設定とデータで学習が行われます!
学習が終わると、その学習済みの重みを保存するようにしておきます。
次回はこの重みを読み込むことで学習済みのモデルで識別が行えます。
これで学習の完了です!
テスト画像で各画像の精度はモデルを呼び出し、先ほどの重みをロードして
以下のようにして分類結果を見ることができます!
僕の用意したテストデータでは9割以上の精度で、それなりに高い精度で分類できていました!
[test.py]
# modelの呼び出し
model = models.model_vgg(class_num, HEIGHT, WIDTH)
# 学習済みの重みのロード
model.load_weights("all_weights12_25.h5")
Yp = model.predict(X_test)
predict = np.argmax(Yp)
Flaskを使った魚種判別APIをHerokuにデプロイする
これらのモデルをAPIとして利用するために、Flask(Pythonのフレームワーク)を使って
深層学習モデルを利用できるAPIを実装していきます。
モデルを載せたサーバーに対して、画像をPOSTすると
APIがモデルを呼び出し画像を分類、そして得られた結果を画像の送信源へと返すように作成しました!
[app.py]
from flask import Flask, request, jsonify
from flask_cors import COR
import numpy as np
from PIL import Image
import models
import io
HEIGHT = 144
WIDTH = 256
class_num = 12
.
.
.
app = Flask(__name__)
# 重要
CORS(app)
@app.route("***EndPointのURL***", methods=['POST'])
def upload():
if request.files and 'image' in request.files:
img = request.files['image'].read()
img = Image.open(io.BytesIO(img))
img = img.resize((WIDTH,HEIGHT))
img_array = np.array(img)
X_test = []
X_test.append(img_array)
X_test = np.asarray(X_test) / 255.
model = models.model_vgg(class_num, HEIGHT, WIDTH)
model.load_weights("all_weights12_25.h5")
Yp = model.predict(X_test)
predict = np.argmax(Yp)
name = all_list[predict]
data = dict(name=str(name),item_id=str(predict))
return jsonify(data)
return 'Picture info did not get.'
if __name__ == "__main__":
app.run()
ここでつまづいたのがCORSのエラーでした。
Flask側で CORS (Cross-Origin Resource Sharing)を有効にする必要があることに留意。
そしてこのAPIを動かすために必要なrequirements.txt, runtime.txt, Procfileを設定し
Herokuへdeploy!
heroku login
git init
git add .
git commit -m "deploy!!!"
heroku create MyProduct
git push heroku master
無料版のHerokuではデータ容量に制限があるため、大きなモデルを利用できないことも現在の課題です。
今後はAWSやGCPを利用して、精度が高く、よりたくさんの魚種を判定できる
モデルを利用できるようにしたいと思っています。
Railsから画像をPOSTしてAPIを使ってみる
ここからは既存のRailsアプリ(uuuo.jp)からFlaskに画像をPOSTして分類結果を返し、ページに表示します。
まずは結果から
テストとしてかわはぎの写真をファイル選択します。
画像のプレビュー機能もjQueryで実装しました。
「AIで認識」ボタンを押すとAjaxの非同期通信が行われ、判定中のgifが動き出します。
数秒待つと、、、
正しく判定されました!やったね!!!
ページを遷移させずに非同期的な処理を行うAjaxで実装をしました。
僕自身、HTML, CSS, JavaScriptを使ったフロントサイド側の実装を本格的に行ったのも初めてで
いろいろと苦戦しましたが、とってもいい経験になりました!目指せフルスタックエンジニア。
コードはこんな感じ
[MyProduct.js]
$("#myForm").submit(evt => {
evt.preventDefault();
let imgPath;
const formData = new FormData($("#myForm")[0]);
// 判定中アイコンを表示する
document.getElementById("result_unit").src = "loading.gif";
document.getElementById("title").innerText = "判定中...";
// Ajaxで送信
$.ajax({
url: "***FlaskAPIのURL***",
method: "POST",
data: formData,
processData: false,
contentType: false
})
.done(res => {
// 送信成功!
const itemId = Number(res.item_id) + 1;
if (itemId === 0) {
document.getElementById("result_unit").src = "fail_class.png";
$("#result_unit").css("width", "200px");
document.getElementById("title").innerHTML =
"写真を入力してください!";
} else {
imgPath = `${itemId}.jpg`;
document.getElementById("result_unit").src = imgPath;
$("#result_unit").css("width", "200px");
document.getElementById("title").innerHTML =
`入力された写真は<br>「${itemList[res.item_id]}」<br>` +
`と判別されました!`;
}
})
.fail(() => {
document.getElementById("result_unit").src = "fail_class.png";
$("#result_unit").css("width", "200px");
document.getElementById("title").innerHTML =
"通信に失敗しました。<br>しばらく経ってから再度ご利用ください!";
});
return false;
});
クリックイベントが起こると、HTMLに表示させる該当箇所のIDを取り
Ajaxで通信を行い条件分岐して分類結果毎に表示を変えるようになっています。
また写真が選択されていない場合や、通信エラーの場合分けもきちんと行います。
【ファイル未選択時】
【通信失敗時】
いらすとや、万能。
まとめ
ウーオにインターンとして参加し、業務システムの改善のみならず、
自分でアイデアを考えて、こんなものあったらいいんじゃないか!という思いがあれば
快く挑戦を後押ししてもらえる環境がウーオにはあります!
また行き詰まった時には、社員の方や同じインターン生にもサポートしてもらえますし、
何より自分発のプロダクトなので主体的に学び、手を動かし、プロダクトを作っていく楽しさを経験する中で大きな成長を実感しています!
将来エンジニアとして働こうと考えているかた、
インターンでスキルを身に着けたいけど、
そもそも広島にインターンってないし、、、と諦めている広島の学生の方、
ウーオではそんな思いを叶える環境があると思います!
興味を持った方、簡単にお話だけでも聞いてみたい方
ぜひぜひ気軽にお問い合わせください〜!
僕は松葉ガニでした。
精進します。