8
6

ゼロからの画像認識①(理論とMNIST)

Last updated at Posted at 2022-03-09

非情報系大学生が、画像認識についてゼロから学んだことを順にまとめた記事です。今回のゴールはニューラルネットワークの大雑把な理解と、MNISTの一番単純な解き方の理解です。

環境:Python 3.8.5、Jupyter 1.0.0、Tensorflow 2.4.0

理論

これに関しては次の連載を読んである程度理解しました。線形代数のある程度の知識を前提とします。【要約】に書いたキーワードが理解できれば、画像認識のチュートリアルとして使用されるMNISTのコードを理解できます。機械に学習させると言っても、人間が手作業で設定しなければいけない項目(ハイパーパラメータ)が複数あるので基礎の理解が必要になります。

  1. AIを学ぶ前にー「空前のAIブーム」を冷静に捉えなおす - Think IT
    【要約】機械学習では、写像の意味での「関数」を頑張ってコンピュータに近似させるよ。

  2. 「複雑」をたくさんの「単純」に分解する〜順伝播は「1次関数」と「単純な非線形」の繰り返し - Think IT
    【要約】どんな関数でも「線形変換」と「活性化関数」の繰り返しで近似できるよ。

  3. ニューラルネットワークの学習を司る「微分」を学ぶ - Think IT
    【要約】「誤差関数」を小さくする方向に何度も「重み」を調整するよ。

  4. 「線形代数」でニューラルネットワークを記述する? - Think IT
    【要約】(おまけ)重みは行列になるよ。

  5. 連鎖律の原理で、誤差を「後ろ」に伝えよう - 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') #正解は青の棒グラフ
1180番目を表示
i = 1180

plt.figure(figsize=(6, 3))
plt.subplot(1, 2, 1)
plot_image(i)
plt.subplot(1, 2, 2)
plot_value_array(i)

1180.png

正解してます。

1182番目を表示
i = 1182

plt.figure(figsize=(6, 3))
plt.subplot(1, 2, 1)
plot_image(i)
plt.subplot(1, 2, 2)
plot_value_array(i)

1182.png

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()

cm.png

特に偏って間違えやすい文字はないですね。

自分で描いた画像を判定する

他のデータを判定するために作った分類器ですから、やっぱりこれが重要。正解率が低かったので、切り取って拡大してから判定させたものと比較しています。

必要なライブラリの追加
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)

0.png
1.png
2.png
3.png
4.png
5.png
6.png
7.png
8.png
9.png

png 0 1 2 3 4 5 6 7 8 9
拡大前 × × × ×
拡大後 × × × × ×

3、9をそれぞれ別の知り合いに書いてもらって、残りは自分で書きました。3、9が小さかったのかなと思い、拡大してもう一度試しています。結果、拡大によって判定が大きく変わりました。どうやらMNISTデータに見慣れていて、無意識に似た字を書いていたみたいです。

考察

文字の大きさや位置ズレによって判定精度が変わります。理論からわかるように、画像が1ピクセルでもずれるとFlatten層で全く違うベクトルになってしまうからです。これを克服するために、Flattenで1次元にする前に2次元のままうまく神経を繋ぐ、畳み込みニューラルネットワーク(CNN)という方法があるようです。

次回、CNNを勉強してMNISTを改良していきたいと思います。

8
6
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
6