非情報系大学生が、画像認識についてゼロから学んだことを順にまとめた記事です。今回のゴールはニューラルネットワークの大雑把な理解と、MNISTの一番単純な解き方の理解です。
環境:Python 3.8.5、Jupyter 1.0.0、Tensorflow 2.4.0
理論
これに関しては次の連載を読んである程度理解しました。線形代数のある程度の知識を前提とします。【要約】に書いたキーワードが理解できれば、画像認識のチュートリアルとして使用されるMNISTのコードを理解できます。機械に学習させると言っても、人間が手作業で設定しなければいけない項目(ハイパーパラメータ)が複数あるので基礎の理解が必要になります。
-
AIを学ぶ前にー「空前のAIブーム」を冷静に捉えなおす - Think IT
【要約】機械学習では、写像の意味での「関数」を頑張ってコンピュータに近似させるよ。 -
「複雑」をたくさんの「単純」に分解する〜順伝播は「1次関数」と「単純な非線形」の繰り返し - Think IT
【要約】どんな関数でも「線形変換」と「活性化関数」の繰り返しで近似できるよ。 -
ニューラルネットワークの学習を司る「微分」を学ぶ - Think IT
【要約】「誤差関数」を小さくする方向に何度も「重み」を調整するよ。 -
「線形代数」でニューラルネットワークを記述する? - Think IT
【要約】(おまけ)重みは行列になるよ。 -
連鎖律の原理で、誤差を「後ろ」に伝えよう - Think IT
【要約】(おまけ)合成関数の微分はコンピューターに任せてね。
MNIST
「Hello World」の解読
TensorflowのHPにて「初心者向けのHello World」として紹介されているコードの解読を行いました。HPを見ると複数の書き方(API)があるみたいです。MNISTとは、0から9の手書き文字画像データセットです。
参考:MNIST:手書き数字の画像データセット - @IT
import tensorflow as tf
mnist = tf.keras.datasets.mnist
(x_train, y_train),(x_test, y_test) = mnist.load_data()
x_train, x_test = x_train / 255.0, x_test / 255.0
こうやるとMNISTのデータを貰えるみたいです。各ピクセルは0~255の値を持っているので、割り算で0~1に潰します。
model = tf.keras.models.Sequential([
tf.keras.layers.Flatten(input_shape=(28, 28)),
tf.keras.layers.Dense(128, activation='relu'),
tf.keras.layers.Dropout(0.2),
tf.keras.layers.Dense(10, activation='softmax')
])
モデルの層をそれぞれ作ってます。
Flatten(input_shape=(28, 28))
28×28だった画像の行列を、平らにして(Flatten)ベクトルに変えます。784個の数値を持ったベクトルです。
Dense(128, activation='relu')
784個の入力を128個の神経細胞に全部繋ぎます。活性化関数はReLUにしてます。
参考:活性化関数一覧 - Qiita
Dropout(0.2)
2割の値を0にしてます。過学習防止になります。
参考:Dropout:ディープラーニングの火付け役、単純な方法で過学習を防ぐ - DeepAge
Dense(10, activation='softmax')
128個の入力を10個の神経細胞に全部繋ぎます。10個の神経細胞はそれぞれ、0から9までのグループを表します。活性化関数はSoftmaxにしてます。Softmaxは全部足すと1になるので、確率みたいな感じの出力になります。確率が高い群を分類結果として使う予定です。
model.compile(optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
model.fit(x_train, y_train, epochs=5)
optimizer='adam'
Adamというアルゴリズムで求める設定です。
参考:【決定版】スーパーわかりやすい最適化アルゴリズム -損失関数からAdamとニュートン法- - Qiita
loss='sparse_categorical_crossentropy'
誤差関数(損失関数)を交差エントロピーに設定しています。分類問題ではほぼこれ一択らしい(?)。調べると色々な損失関数がヒットするけど、大体は回帰問題の話。
sparseというのは、y_trainデータの形です。「8群」を表したい時に、8
なのか[0,0,0,0,0,0,0,0,1,0]
なのかの違い。あまり気にしなくていい。
参考:損失関数とは??〜機械学習の用語まとめ〜 - Qiita
metrics=['accuracy']
評価関数を正解率に設定しています。出力で正解率を確認できます。
epochs=5
5回繰り返して重みを更新させます。やりすぎると過学習になるので注意。
model.evaluate(x_test, y_test)
[0.07569769769906998, 0.9785000085830688]
x_textを分類器に入れてみたときの、損失関数と評価関数の値を教えてくれます。97%超え、驚くほど高い数字です。
結果を表示する
素人が見ても解釈できるように表示してみます。どこからコピペしたのか忘れました。
import numpy as np #行列やベクトルの計算用
import matplotlib.pyplot as plt #描画用
predictions = model.predict(x_test)
def plot_image(i):
prediction, true_label, img = np.argmax(predictions[i]), y_test[i], x_test[i]
plt.grid(False)
plt.xticks([])
plt.yticks([])
plt.imshow(x_test[i], cmap='gray')
if prediction == true_label:
color = 'blue' #不正解は赤文字
else:
color = 'red' #正解は青文字
plt.xlabel(f"pred:{prediction},{100*np.max(predictions[i]):.2f}% (true:{true_label})",
color=color)
def plot_value_array(i):
predictions_array, true_label = predictions[i], y_test[i]
plt.grid(False)
plt.xticks([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
plt.yticks([])
thisplot = plt.bar(range(10), predictions_array, color="#777777")
plt.ylim([0, 1])
predicted_label = np.argmax(predictions_array)
thisplot[predicted_label].set_color('red') #不正解は赤の棒グラフ
thisplot[true_label].set_color('blue') #正解は青の棒グラフ
i = 1180
plt.figure(figsize=(6, 3))
plt.subplot(1, 2, 1)
plot_image(i)
plt.subplot(1, 2, 2)
plot_value_array(i)
正解してます。
i = 1182
plt.figure(figsize=(6, 3))
plt.subplot(1, 2, 1)
plot_image(i)
plt.subplot(1, 2, 2)
plot_value_array(i)
5や8と間違えてしまったみたいです。
import seaborn as sns #綺麗な用
from sklearn.metrics import confusion_matrix #混同行列作成用
y_pred = np.argmax(model.predict(x_test), axis=-1)
cm = confusion_matrix(y_test, y_pred) #混同行列を作る
fig, ax = plt.subplots(figsize = (10,7))
sns.heatmap(cm, annot=True, square=True, cmap='Blues', fmt="d") #図示
ax.set(xlabel = 'predict', ylabel = 'true' )
plt.show()
特に偏って間違えやすい文字はないですね。
自分で描いた画像を判定する
他のデータを判定するために作った分類器ですから、やっぱりこれが重要。正解率が低かったので、切り取って拡大してから判定させたものと比較しています。
from PIL import Image #画像操作用
def plot_value_array(predictions_array):
plt.grid(False)
plt.xticks([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
plt.yticks([])
thisplot = plt.bar(range(10), predictions_array, color="#777777")
plt.ylim([0, 1])
predicted_label = np.argmax(predictions_array)
thisplot[predicted_label].set_color('blue')
plt.xlabel(f"Your figure is \"{predicted_label}\" !! ({100*np.max(predictions_array):.7f}%)")
同じディレクトリにmy_imageというディレクトリを作り、黒地に白で書いた画像の0.png、1.png、……、 9.pngを入れています。
def predict_image(i):
img = Image.open(f'my_image/{i}.png').convert('L') #インポート、グレースケール化
img_resize = img.resize((28, 28)) #リサイズ(793, 793) -> (28, 28)
img_array = np.array(img_resize) #配列化
img_array = img_array / 255 #正規化
imgs = img_array[np.newaxis, :, :] #(28, 28) -> (1, 28, 28)
#予測させる
pred = model.predict(imgs)
#描画
plt.figure(figsize=(20, 4))
plt.subplot(1, 5, 1)
plt.imshow(img, cmap='gray')
plt.subplot(1, 5, 2)
plt.imshow(img_resize, cmap='gray')
plt.subplot(1, 5, 3)
plot_value_array(pred[0])
#切り取ってみる
img = img.crop((100, 100, 693, 693)) #真ん中を切り取って拡大
img_resize = img.resize((28, 28))
img_array = np.array(img_resize)
img_array = img_array / 255
imgs = img_array[np.newaxis, :, :]
pred = model.predict(imgs)
#描画
plt.subplot(1, 5, 4)
plt.imshow(img_resize, cmap='gray')
plt.subplot(1, 5, 5)
plot_value_array(pred[0])
img_array[np.newaxis, :, :]
が謎に思うかもしれません。今回のmodel.predict(引数)
は、28×28画像がn個入ったリストを引数にとります。そのため、(28,28)行列を1つ並べた(1,28,28)配列を作っています。
for i in range(10):
predict_image(i)
png | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
拡大前 | ○ | ○ | ○ | × | ○ | × | × | ○ | ○ | × |
拡大後 | × | × | ○ | ○ | × | ○ | × | ○ | ○ | × |
3、9をそれぞれ別の知り合いに書いてもらって、残りは自分で書きました。3、9が小さかったのかなと思い、拡大してもう一度試しています。結果、拡大によって判定が大きく変わりました。どうやらMNISTデータに見慣れていて、無意識に似た字を書いていたみたいです。
考察
文字の大きさや位置ズレによって判定精度が変わります。理論からわかるように、画像が1ピクセルでもずれるとFlatten層で全く違うベクトルになってしまうからです。これを克服するために、Flattenで1次元にする前に2次元のままうまく神経を繋ぐ、畳み込みニューラルネットワーク(CNN)という方法があるようです。
次回、CNNを勉強してMNISTを改良していきたいと思います。