MNIST、リンゴorバナナでKerasでの画像判別の仕組みの初歩は分かりました。でも、画像の判定をする時にはどれが「リンゴ」か「バナナ」か、だけではなくて「どっちでもない」が欲しい。
難しい言葉で言うと、正常値のデータだけで異常値の判別をさせたい。
これができれば、カメラで顔認証するアプリケーションも作れるのでは?というのが今回のテーマ。
考え方
ディープラーニングが話題になり始めた頃からある、オートエンコーダという仕組みを利用します。
オートエンコーダは、入力されたデータを圧縮して、それを復元して出力するというもの。
コンピュータで画像を扱ったことがある人なら分かると思いますが、解像度を小さくした後に元の解像度に拡大するような処理です。
これだけだと、単純にデータを劣化させるだけなので無意味な操作なのですが、ポイントになるのは入力されたデータが何であろうと、学習したデータに復元しようとする、というところ。
つまりリンゴの画像で学習させれば「絶対にリンゴに戻そうとするマン」が出来上がる。
当然、入力された画像がリンゴでなければ上手く復元できないので、猫とか入れるとよく分からない何かが出力されてくるはず。
これを利用すれば、出力された画像がリンゴっぽければリンゴ、そうじゃなければリンゴじゃない、という判定ができるようになる。
詳しい人なら知ってると思いますが、Googleの猫で話題になったアレですね。
コーディング
Kerasにオートエンコーダ用の命令等が特別に用意されているわけではないです。今までは畳み込んで敷き詰めるイメージで学習データセットを作っていましたが、今回は畳み込んだものを元に戻すようにモデルを組みます。
なお、Web上のリファレンスではModel()
を使っていることが多いので、ここではあえてSequential()
でモデルを構築します。Sequential()
なら今まで慣れ親しんできていますし、これが理解できればModel()
を使った組み方にスムーズにアップグレードできるのでは?ということで。
学習データ読み込み
今回は簡単のために画像処理にPillowを使っています。次回以降、カメラから画像を収集する際はOpenCVに組み替えると思います。
# numpy
import numpy as np
# 画像ファイル処理用
from PIL import Image
import glob
# 画像の読み込み、" "内は任意の(学習させたいデータが入っている)フォルダ名
folder = ["apple"]
# 画像を格納するための配列を準備
X = []
# ファイルオープンを関数化
# 画像サイズは128×128に強制的に圧縮
def img(x):
y = Image.open(x)
y = y.convert("RGB")
w, h = y.size
if w == h:
r = y
elif w > h:
r = Image.new(y.mode, (w, w), (0, 0, 0))
r.paste(y, (0, (w - h) // 2))
else:
r = Image.new(y.mode, (h, h), (0, 0, 0))
r.paste(y, ((h - w) // 2, 0))
r = r.resize((128, 128))
return r
# 画像の読み込み処理。
# folder名に一致するフォルダの中身を1次元配列化しながらXに格納
for _, name in enumerate(folder):
dir = "./" + name
files = glob.glob(dir + "/*.jpg")
for i, file in enumerate(files):
image = img(file)
for j in range(0, 36):
imr = image.rotate(j*10)
imr = np.asarray(imr)
X.append(imr)
X = np.array(X)/255
以前作ったコードを使い回しているため、無駄な処理が入っていますが、本題ではないのでご容赦ください。appleフォルダにはgoogleから収集してきた、真ん中にリンゴが1個映っているjpg画像を100枚程度保存しています。これが学習用のデータとなります。
なお、画像のサイズは224×224にしたかったのですが、ぼくのPCではスペックが足りず断念しました。
モデル構築
from keras.models import Sequential
from keras.layers import Conv2D, MaxPooling2D, UpSampling2D
model = Sequential()
#エンコーダ部分
model.add(Conv2D(64,(3, 3), input_shape=(128, 128, 3), activation="relu", padding="same"))
model.add(MaxPooling2D((2,2)))
model.add(Conv2D(128,(3, 3), activation="relu", padding="same"))
model.add(Conv2D(128,(3, 3), activation="relu", padding="same"))
model.add(MaxPooling2D((2,2)))
model.add(Conv2D(256,(3, 3), activation="relu", padding="same"))
model.add(Conv2D(256,(3, 3), activation="relu", padding="same"))
model.add(MaxPooling2D((2,2)))
model.add(Conv2D(512,(3, 3), activation="relu", padding="same"))
model.add(Conv2D(512,(3, 3), activation="relu", padding="same"))
model.add(MaxPooling2D((2,2)))
#デコーダ部分
model.add(Conv2D(512,(3, 3), activation="relu", padding="same"))
model.add(Conv2D(512,(3, 3), activation="relu", padding="same"))
model.add(UpSampling2D((2,2)))
model.add(Conv2D(256,(3, 3), activation="relu", padding="same"))
model.add(Conv2D(256,(3, 3), activation="relu", padding="same"))
model.add(UpSampling2D((2,2)))
model.add(Conv2D(128,(3, 3), activation="relu", padding="same"))
model.add(Conv2D(128,(3, 3), activation="relu", padding="same"))
model.add(UpSampling2D((2,2)))
model.add(Conv2D(64,(3, 3), activation="relu", padding="same"))
model.add(UpSampling2D((2,2)))
#出力
model.add(Conv2D(3,(3, 3), activation="sigmoid", padding="same"))
UpSampling2D()
はMaxPooling2D()
とは逆に、次元数を増やす役割を持つ層です。
エンコーダ部分では、今までのモデル構築同様、畳み込みをしていく処理を行っています。デコーダ部分はそれを遡って復元していっているのが分かると思います。
最後は、画像として出力して欲しいのでDense
ではなくConv2D
で出力層を作っています。今まで出力層の活性化関数として使っていたのはソフトマックスでしたが、今回は画像として出力するので出力の合計値が1になると困るためシグモイドを使います。
シグモイドはソフトマックス同様出力を0~1の間の値にしますが、その合計は1になりません。つまり、これに255をかけてやれば画像に戻るはず。
Web上のTipsでは畳み込みオートエンコーダの場合でも出力ノードを減らしていく方向で学習させるモデルが紹介されていることが多いのですが、今回はVGGモデルを参考に、ノードを増やす方向で構築してみました。
コンパイルと学習
model.compile(loss="binary_crossentropy", optimizer="adam")
hist = model.fit(X, X, batch_size=32, verbose=1, epochs=200, validation_split=0.1)
クロスエントロピーは確率問題用、MSEはソフトマックスやシグモイドと一緒に使うのに向かない、ということで損失計算はバイナリクロスエントロピー。オプティマイザーはSGDよりadamの方が精度が上がるらしい。この辺もよく分かってないので今後要勉強。
今回のモデルは自分のマシンで結構スペックギリギリのところで設定したのでエラーが出る可能性が割とあります。もしメモリエラー等出た場合は層を減らすなり入力データサイズを小さくするなりでご対応いただければ。
テスト
# 学習完了したモデルで画像判別を試してみる
Xt = []
testimg = img("ringo.jpg")
testimg.show()
testimg = np.asarray(testimg)
Xt.append(testimg)
Xt = np.array(Xt)/255
result = model.predict(Xt)
result = np.round(result*255)
imgres = Image.fromarray(np.uint8(result[0]))
imgres.show()
学習に使っていない、未知のデータを使って実験してみます。比較用にオートエンコーダに通す前後で画像を表示させるようにしています。
最後、画像として表示させるために255をかけて整数に戻す処理を加えた後に配列データを画像に再変換する処理を加えています。
実行結果
試しに、リンゴとバナナの画像を通してみます。
リンゴの画像はかなりしっかりと復元されていますが、バナナの画像はリンゴとして復元しようとした結果色や形がおかしくなりぼやけてしまっているのが分かると思います。学習は200回程度の試行・100枚程度の画像で行いましたがここまでの判別ができるようになりました。
まとめ
ほぼ想定どおりの稼働をしてくれました。ということで次はカメラから取得した画像で学習するようコードを組んでみます。これも、想定どおりに行ければそんなに難しくないはず。
問題は、メモリとの戦いになりそうって部分ですが。