(2021-12-11追記)
##はじめに
本記事はAidemy Premium Plan AIアプリ開発講座(6ヶ月)の最終成果物として、AIを利用した画像認識アプリを制作する過程を記録したものです。
▼Aidemy Premium Planとは
未経験からでもAIが学べる AI特化型プログラミングスクールAidemy Premium
##目次
テーマ
実行環境
画像収集
正解ラベルの作成と配列化
モデルの学習
フロント部分の作成
アプリの公開
まとめ
参考サイト
追記-画像の水増し
##テーマ
今回はCASIOの腕時計であるG-SHOCKの画像から「どのブランドか」を判定するアプリを制作します。
現在、G-SHOCKのブランドは12種類あるようです。
▼G-SHOCK製品検索
https://g-shock.jp/products/finder/
ブランド名が付いていないG-SHOCKもあるので、全G-SHOCKが対象ではありません。
ところで、なぜG-SHOCKかというと、私は腕時計好き、G-SHOCK好きですがパッと見ただけではブランドがわかりません。
G-SHOCKの型番は裏蓋に書いてありますが、どのブランドかはわからないため、判定するアプリがあればいいなと思ったので成果物のテーマにしました。
前述の通り12種類とブランドが限られているため、成果物の難易度には丁度良い量であると考えたのも理由です。
##実行環境
・Windows 11 21H2
・Google Colaboratory
・Visual Studio Code 1.62.3
・Python 3.8.8
##画像収集
学習用の画像を収集します。
目標は200〜300点/1種類とします。
画像収集が簡単にできるicrawlerライブラリを使用して、Bing画像検索、Google画像検索から目的の画像を取得します。
Bing画像検索から目的の数を収集できれば良かったのですが、せいぜい150点程度しか集まらなかったため、Google画像検索からも取得しました。
▼参考サイト
Pythonで画像データを手軽に収集したい方必読! icrawler入門 | AI Academy Media
#icrawlerライブラリをインストール、今回はBing画像検索から収集
#ブランド一覧のリストを作成
#for文でブランド名のディレクトリを作成→収集した画像を格納、を繰り返し
!pip install icrawler
from icrawler.builtin import BingImageCrawler
watches = ["MT-G","MR-G","G-LIDE","FROGMAN","G-STEEL","GRAVITYMASTER","MUDMASTER","RANGEMAN","MUDMAN","GULFMASTER","G-SQUAD","Dolphin and Whale Eco-Reserch Network"]
for i in watches:
crawler = BingImageCrawler(storage={"root_dir": i + "_Bing"})
crawler.crawl(keyword=i, max_num=300)
小一時間で完了。
取得した画像をざっと見てみると、全然関係ない画像ばかり集まっているキーワードがありました。
例えば「FROGMAN」のときに「潜水士」の画像ばっかり集まっているなど。(そりゃそうだ)
そういうキーワードは「FROGMAN,G-SHOCK」で取得し直して無事目的の画像を取得しました。
次に、取得した画像から明らかに無関係な画像や腕時計部分が小さい画像を削除。
そしてそれぞれ正方形になるようにトリミングして、約200点/1種類としました。
今回の制作のなかで、この作業が一番時間がかかったと思います。
処理が完了した画像は、Googleドライブの「watches」内に、各ブランド名ごとのディレクトリを作成して格納しました。
##正解ラベルの作成と配列化
収集した画像のサイズを調整し、正解ラベルを付与した上でモデルに学習させるためにNumpy配列にします。
ここで自分が詰まったところを2点書いておきます。
いずれも初歩的な内容だと思いますが、講座内ではデータセットばかり使ってた自分には初めてだったので。
1.集めた画像を1つのリストにすること
この各ディレクトリから画像を1点ずつ読み込んで、指定した幅と高さにリサイズした上で1つの画像リストにするところで詰まりました。
四苦八苦した挙句、以下のようにfor文をネストしてクリアしました。
# ブランド一覧のリストを作成
watches = ["MT-G","MR-G","G-LIDE","FROGMAN","G-STEEL","GRAVITYMASTER","MUDMASTER","RANGEMAN","MUDMAN","GULFMASTER","G-SQUAD","DolphinandWhaleEcoReserchNetwork"]
# 画像リスト用の空リストを作成
img_list = []
# 画像サイズを指定
width = 150
height = 150
# 各ディレクトリ内の画像ファイルパスをpath_listに格納
# 各ディレクトリ名+画像ファイルパスで画像を呼び出し、リサイズして画像リストに追加
for i in watches:
path_list = os.listdir("./drive/MyDrive/watches/" + i)
for j in path_list:
img = cv2.imread("./drive/MyDrive/watches/" + i + "/" + j)
img = cv2.resize(img, (width,height))
img_list.append(img)
2.正解ラベルを作成して、Numpy配列にすること
教師あり学習のため、画像ごとに正解ラベルが必要です。
画像に対して各ブランドごとに0~11の正解ラベルを付与した上でNumpy配列にすればいいことはわかっていましたが、やり方がわからず。
これはチューターさんに質問して解消しました。
# 正解ラベル用の空リストを作成
len_list = []
# os.listdirで各ブランドごとのディレクトリ内の要素数を取得してlen_wに格納
# index(0~11)×len_w(各ディレクトリの要素数分)を正解ラベル用のリストに追加
for index,i in enumerate(watches):
len_w = len(os.listdir("/content/drive/MyDrive/watches/" + i))
len_list+=[index]*len_w
X = np.array(img_list)
y = np.array(len_list).reshape(-1,1)
##モデルの学習
学習用の画像の準備が整ったので、モデルを作って学習させます。
講座で習ったVGG16による転移学習を利用してモデルを作成します。
※転移学習・・・学習済みのモデルを使って新たなモデルの学習をすること。
VGGとはTensorFlowで公開されている、学習済みの機械学習モデルの1種です。
※ TensorFlow・・・Googleがオープンソースで公開している機械学習用のソフトウェアライブラリ。
オックスフォード大学のVGGチームが作り世界的な画像認識コンペで2位に入賞した優秀なモデルで、特徴抽出の層までを使うことで転移学習に用いることができます。
import os
import cv2
import numpy as np
import matplotlib.pyplot as plt
import pickle
from tensorflow.keras.utils import to_categorical
from tensorflow.keras.layers import Dense, Dropout, Flatten, Input, BatchNormalization
from tensorflow.keras.applications.vgg16 import VGG16
from tensorflow.keras.models import Model, Sequential
from tensorflow.keras import optimizers
# ブランド一覧
watches = ["MT-G","MR-G","G-LIDE","FROGMAN","G-STEEL","GRAVITYMASTER","MUDMASTER","RANGEMAN","MUDMAN","GULFMASTER","G-SQUAD","DolphinandWhaleEcoReserchNetwork"]
# 画像リスト用の空リストを作成
img_list = []
# 画像サイズを指定
width = 150
height = 150
# 各ブランド名のディレクトリの画像のファイルパスをpath_listに格納
# 各ブランド名のディレクトリ+画像のファイルパスで呼び出し、リサイズして画像リストに格納
for i in watches:
path_list = os.listdir("./drive/MyDrive/watches/" + i)
for j in path_list:
img = cv2.imread("./drive/MyDrive/watches/" + i + "/" + j)
img = cv2.resize(img, (width,height))
img_list.append(img)
# 正解ラベル作成
# 各ブランド内の画像数の空リストを作成
# 各ブランド名のディレクトリ内の画像数を画像数リストに格納
len_list = []
for index,i in enumerate(watches):
len_w = len(os.listdir("/content/drive/MyDrive/watches/" + i))
len_list+=[index]*len_w
X = np.array(img_list)
y = np.array(len_list).reshape(-1,1)
# データセットの保存と呼び出し
with open("/content/drive/MyDrive/Colab Notebooks/dataset.pkl", "wb") as f:
pickle.dump((X, y), f)
with open("/content/drive/MyDrive/Colab Notebooks/dataset.pkl", "rb") as f:
X, y = pickle.load(f)
# 画像データをランダムにシャッフル
rand_index = np.random.permutation(np.arange(len(X)))
X = X[rand_index]
y = y[rand_index]
# トレーニングデータとテストデータに分割。全画像のうち8割を学習、残りを学習済モデルの検証に使用
X_train = X[:int(len(X)*0.8)]
y_train = y[:int(len(y)*0.8)]
X_test = X[int(len(X)*0.8):]
y_test = y[int(len(y)*0.8):]
# ラベルデータをone-hotベクトルに変換
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)
# VGG16のインスタンス生成
input_tensor = Input(shape = (width,height, 3))
vgg16 = VGG16(include_top = False, weights = 'imagenet', input_tensor = input_tensor)
top_model = Sequential()
top_model.add(Flatten(input_shape = vgg16.output_shape[1:]))
top_model.add(Dense(512, activation = 'relu'))
top_model.add(Dropout(0.2))
top_model.add(BatchNormalization())
top_model.add(Dense(12, activation = 'softmax'))
# モデルの連結
model = Model(inputs = vgg16.input, outputs = top_model(vgg16.output))
# VGG16の重みの固定
for layer in model.layers[:19]:
layer.trainable = False
# コンパイル
model.compile(loss = 'categorical_crossentropy',
optimizer = optimizers.SGD(lr = 1e-4, momentum = 0.9),
metrics = ['accuracy'])
# 学習(バッチサイズ=100、epochs=30)
history = model.fit(X_train, y_train, batch_size = 100, epochs = 30, validation_data=(X_test, y_test))
# 学習のスコア
scores = model.evaluate(X_test, y_test, verbose=1)
print('Test loss:', scores[0])
print('Test accuracy:', scores[1])
# 正解率の可視化
plt.plot(history.history['accuracy'], label='accuracy')
plt.plot(history.history['val_accuracy'], label='val_accuracy')
plt.ylabel('accuracy')
plt.xlabel('epoch')
plt.legend(loc='best')
plt.show()
print(history.history)
# 学習モデルを保存
model.save("watches12.h5")
多少試行錯誤し、画像サイズを150×150に、バッチサイズを100に、ドロップアウト層を0.2と調整してみました。
結果は画像の通り、正解率約68%でした。
※モデルを調整して複数回学習させるときのために、以下でnp配列にした画像をデータセットとして保存し、次回以降は呼び出して使うようにすると無駄が省けます。
# データセットの保存
with open("/content/drive/MyDrive/Colab Notebooks/dataset.pkl", "wb") as f:
pickle.dump((X, y), f)
# データセットの呼び出し
with open("/content/drive/MyDrive/Colab Notebooks/dataset.pkl", "rb") as f:
X, y = pickle.load(f)
さて、もう少し精度を上げたいと思ったので、画像を水増しをして再学習をしようと考えましたが、問題が発生しました。
Google colabratory のGPU使用量上限に達してしまったのです。
WEB上には12時間以上経過すればまた使える、という記述もありましたが、自分の場合3日以上経過してもGPUが使えるようにならず。
(その間GPU無しで動かしていたせいもあるかもしれません)
GPU無しだとモデルの学習により多くの時間がかかります。
私の場合、前述の学習では1epochあたり10分だったものが約40分、約4倍かかるように。
講座の最終成果物として制作する以上、納期(=講座終了日)までにアプリの制作が完了して提出できるかも大事なので、一旦画像の水増しによる精度の向上は後回しにすることにしました。
追記・・・10日くらいして再びcolabを見た見たら、再びGPUが使えるようになっていたので画像の水増しを試してみました。
追記-画像の水増し
##フロント部分の作成
アプリのフロント部分(表示部分)をhtml,CSSで作り、判定部分(画像をモデルに渡して判定結果を返す)をFlaskで作ります。
講座内で作った数字認識アプリを流用しつつ部分的に変えていきます。
アプリの見た目は本家G-SHOCKのサイトを参考にしながら、白と黒を基調にしたシンプルなデザインにしました。
前述の数字認識アプリは判定結果のみ表示するものでしたが、判定結果の正解率を表示するようにしました。
以下ソース。
<!DOCTYPE html>
<html lang='ja'>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content="device-width, initial-scale=1.0">
<meta http-equiv='X-UA-Compatible' content="ie=edge">
<title>G-SHOCK ブランド判定(12種類)</title>
<link rel='stylesheet' href="./static/stylesheet.css">
</head>
<body>
<header>
<a class='header-logo' href="#">G-SHOCK判定</a>
</header>
<div class='main'>
<h2> AIが送信されたG-SHOCK画像のブランドを判定します</h2>
<p>画像を送信してください</p>
<form method='POST' enctype="multipart/form-data">
<input class='file_choose' type="file" name="file">
<input class='btn' value="submit!" type="submit">
</form>
<div class='answer'>{{answer}}</div>
</div>
<footer>
<small>© 2021 S.Mizno</small>
</footer>
</body>
</html>
header {
background-color: #000;
height: 70px;
margin: -8px;
display: flex;
justify-content: space-between;
}
.header-logo {
color: #fff;
font-size: 30px;
margin: 15px 25px;
text-align: left;
}
.main {
height: 370px;
}
h2 {
color: #444444;
margin: 90px 0px;
text-align: center;
}
p {
color: #444444;
margin: 70px 0px 30px 0px;
text-align: center;
}
.answer {
color: #444444;
margin: 70px 0px 30px 0px;
text-align: center;
}
form {
text-align: center;
}
footer {
background-color: #F7F7F7;
height: 110px;
margin: -8px;
position: fixed;
}
small {
margin: 15px 25px;
position: fixed;
left: 0;
bottom: 0;
}
import os
from flask import Flask, request, redirect, render_template, flash
from werkzeug.utils import secure_filename
from tensorflow.keras.models import Sequential, load_model
from tensorflow.keras.preprocessing import image
import numpy as np
classes = ["MT-G","MR-G","G-LIDE","FROGMAN","G-STEEL","GRAVITYMASTER","MUDMASTER","RANGEMAN","MUDMAN","GULFMASTER","G-SQUAD","DolphinandWhaleEcoReserchNetwork"]
image_size = 28
UPLOAD_FOLDER = "uploads"
ALLOWED_EXTENSIONS = set(['png', 'jpg', 'jpeg', 'gif'])
app = Flask(__name__)
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
model = load_model('./watches12.h5')#学習済みモデルをロード
@app.route('/', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
if 'file' not in request.files:
flash('ファイルがありません')
return redirect(request.url)
file = request.files['file']
if file.filename == '':
flash('ファイルがありません')
return redirect(request.url)
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
file.save(os.path.join(UPLOAD_FOLDER, filename))
filepath = os.path.join(UPLOAD_FOLDER, filename)
#受け取った画像を読み込み、np形式に変換
img = image.load_img(filepath, grayscale=False, target_size=(image_size,image_size))
img = image.img_to_array(img)
data = np.array([img])
#変換したデータをモデルに渡して予測する
result = model.predict(data)[0]
predicted = result.argmax()
# 確率の表示
per = int(result[predicted]*100)
#判定結果の表示
pred_answer = "これは " + classes[predicted] + "です(確率: {}%)".format(per)
return render_template("index.html",answer=pred_answer)
return render_template("index.html",answer="")
if __name__ == "__main__":
port = int(os.environ.get('PORT', 8080))
app.run(host ='0.0.0.0',port = port)
##アプリの公開
出来上がったアプリをHerokuで公開していきます。
詳細は割愛しますが以下の流れです。
・Herokuにアカウント作成、gitをインストール
・git - Herokuアカウント を紐付け
・デプロイ
公開できました。
表示をPC、スマホで確認したところ、問題なさそうです。
次に機能確認します。
「ファイルの選択」→G-SHOCK画像をローカルから選択→「submit!」
するとエラーが出ました。
ほう。
WEBで調べると、まずはHerokuの再起動を試せということなので以下実行。
# Herokuの再起動(appディレクトリに移動して)
$ heroku restart
効果なし。
ならHerokuのログからエラー内容を確認せよということでログの表示。
# Herokuのログを表示
$ heroku logs -t
色々書いてありましたが、エラー部分はこの辺。
※Heroku→app→More→View logsからも見られるっぽい。
ValueError: Input 0 of layer block1_conv1 is incompatible with the layer: expected axis -1 of input
うーん、なんか予期せぬaxisがどうこう...。axis...軸...うっ、頭が...。
小芝居は早々にやめて、エラーメッセージをそのまま検索して出てきたサイトを参照すると、原因がわかりました。
(たまたま同講座を受講した人でした。先輩ありがとう。)
▼参考サイト
”犬か猫か”を判別する画像認識アプリを作ってみた
学習に使用した画像のサイズとアプリ上でモデルに渡す画像のサイズとか配列が違うと。要は条件が違うから判定できません、ということのようです。
画像を渡す部分を以下の通り修正して再デプロイ。
# 画像サイズを150に合せる
image_size = 150
~~~
# 画像を高さ、幅、RGBで渡すように修正
img = image.load_img(filepath, grayscale=False, target_size=(image_size,image_size,3))
改めて動作を確認。
55%って...と思いつつも、ちゃんと判定されたので一安心。
見た目の簡素さや精度はさておき、想定通り機能するアプリが公開できました。
##まとめ
時間はかかりましたが、なんとか最終成果物であるAIアプリの公開ができました。
講座の学習とアプリの公開を通じてPythonー機械学習モデルーフレームワークを利用したアプリ制作、など一通り経験することができ、日頃の業務に役立つツールや機械学習、ディープラーニング、画像認識などの概念などを学ぶことができました。
講座の学習やアプリ制作を進めるなかで、忍耐、根気と1つの方法にこだわらないことを教訓として得たような気がします。
今後の課題としては、やはり画像の水増しによるモデルの精度の向上が第一だと思います。
colabのGPU制限が解除されたら早速取り組みたいところ。
それから、効率的な開発のためには省コスト、省メモリで実行できるコードの書き方も必要になってくると感じました。
そのためにはPythonをもっと日常的に使用して習熟しないといけないですし、その過程でライブラリや連携サービスもより幅広くいろいろなものを知っていかなくてはと思います。
モデルの精度を含めたアプリの完成度といい、理解度といいまだまだ未熟で初心者の域を出ませんが、今後さらに復習や別のAI学習関連の勉強を重ねて成長したいと思います。
アイデミーのチューターの皆さん、参考サイトの作者の皆さん、ありがとうございました。
##参考サイト
1. 科学技術計算のために Python を始めよう。
"春に咲く花の画像" を判別するAIアプリを作ってみた
”犬か猫か”を判別する画像認識アプリを作ってみた
オープンデータセットとVGGを使って、釣れたアジ、タイ、スズキを認識させる。
Pythonで画像データを手軽に収集したい方必読! icrawler入門 | AI Academy Media
herokuのログ情報の出力方法
Markdown記法/書き方(見出し・表・リンク・画像・文字色など)
##追記-画像の水増し
10日ほど経ってcolabをのぞいてみたらGPUが使えるようになっていたので画像の水増しをしてみました。
※画像の水増し・・・学習に使う画像数を増やすことでモデルの精度を上げる手法
今回使用したのはImageDataGeneratorライブラリによる水増しです。
以下の4手法を使って、訓練データを水増しして2640点の画像を11900枚とします。
・画像をランダムに回転・・・rotation_range
・ランダムに水平方向に反転・・・horizontal_flip
・ランダムに垂直方向に反転・・・vertical_flip
・画像を斜め方向に押しつぶしたり伸ばしたりする・・・shear_range
訓練データだけを水増しするのは汎化性能の高さを担保するためです。
※汎化性能・・・未知の検証データに対する認識能力の高さ。汎化性能が低いと訓練データだけに高い精度を誇る機械学習モデルになる。
以下、保存していたデータセットの呼び出し→画像の水増し→モデルの学習
import os
import cv2
import numpy as np
import matplotlib.pyplot as plt
import pickle
from tensorflow.keras.utils import to_categorical
from tensorflow.keras.layers import Dense, Dropout, Flatten, Input, BatchNormalization
from tensorflow.keras.applications.vgg16 import VGG16
from tensorflow.keras.models import Model, Sequential
from tensorflow.keras import optimizers
from tensorflow.keras.preprocessing.image import ImageDataGenerator
# データセットの呼び出し
with open("/content/drive/MyDrive/Colab Notebooks/dataset.pkl", "rb") as f:
X, y = pickle.load(f)
# 画像データをランダムにシャッフル
rand_index = np.random.permutation(np.arange(len(X)))
X = X[rand_index]
y = y[rand_index]
# トレーニングデータとテストデータに分ける
X_train = X[:int(len(X)*0.8)]
y_train = y[:int(len(y)*0.8)]
X_test = X[int(len(X)*0.8):]
y_test = y[int(len(y)*0.8):]
# ラベルデータをone-hotベクトルに変換
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)
# 画像を水増しする関数
def img_extend(x, y):
X_extend = []
y_extend = []
i = 0
# 水増しを5回繰り返す
while i < 5:
datagen = ImageDataGenerator(rotation_range = 30, horizontal_flip = True, vertical_flip=True, shear_range = 0.2)
extension = datagen.flow(X_train, y_train, shuffle = False, batch_size = len(X_train))
X_extend.append(extension.next()[0])
y_extend.append(extension.next()[1])
i += 1
# numpy配列に変換
X_extend = np.array(X_extend).reshape(-1, 150, 150, 3)
y_extend = np.array(y_extend).reshape(-1, 12)
return X_extend, y_extend
img_add = img_extend(X_train, y_train)
# 元の画像データと水増しデータを統合
X_train_add = np.concatenate([X_train, img_add[0]])
y_train_add = np.concatenate([y_train, img_add[1]])
# VGG16のインスタンス生成
input_tensor = Input(shape = (150, 150, 3))
vgg16 = VGG16(include_top = False, weights = 'imagenet', input_tensor = input_tensor)
top_model = Sequential()
top_model.add(Flatten(input_shape = vgg16.output_shape[1:]))
top_model.add(Dense(512, activation = 'relu'))
top_model.add(Dropout(0.2))
top_model.add(BatchNormalization())
top_model.add(Dense(12, activation = 'softmax'))
# モデルの連結
model = Model(inputs = vgg16.input, outputs = top_model(vgg16.output))
# VGG16の重みの固定
for layer in model.layers[:19]:
layer.trainable = False
# コンパイル
model.compile(loss = 'categorical_crossentropy',
optimizer = optimizers.SGD(learning_rate = 1e-4, momentum = 0.9),
metrics = ['accuracy'])
# 学習 20回
history = model.fit(X_train_add, y_train_add, batch_size = 100, epochs = 20, validation_data=(X_test, y_test))
# 学習のスコア
scores = model.evaluate(X_test, y_test, verbose=1)
print('Test loss:', scores[0])
print('Test accuracy:', scores[1])
# 正解率の可視化
plt.plot(history.history['accuracy'], label='accuracy')
plt.plot(history.history['val_accuracy'], label='val_accuracy')
plt.ylabel('accuracy')
plt.xlabel('epoch')
plt.legend(loc='best')
plt.show()
print(history.history)
# 学習モデルを保存
model.save("watches12.h5")
結果は以下の画像の通り、約75%でした。
元が68%でしたのでイマイチな結果となりました。
原因はいろいろ考えられます。
・そもそも元の画像が適していない。
・画像の水増しの手法が合っていない、回数が足りない(画像の水増しを5回以上繰り返すとcolabがメモリ不足でクラッシュする事象が起きたため5回にしてます。Pythonの書き方が省メモリではないのかと。)
・モデルが最適化されていない など。
個人的には水増しの手法と回数に改善の余地があるように感じますが、そこまでは現状の私の実力ではたどり着かず。
やや残念な気持ちは残りますが、とりあえず講座で習ったことを一通りやってのアプリ開発となったため、現時点の実力だと思ってこの件はここまでにしようと思います。
今後に課題がある方が学習意欲が湧きますし。
以上。