2019年12月24日火曜日

くずし字を読む
(畳み込みニューラルネットワーク)

この記事は、ケーシーエスキャロット Advent Calendar 2019 の24日目の記事になります。

畳み込みニューラルネットワークモデル

前回の「ニューラルネットワーク編」では、文字を見分けるポイントを何も指示していないにも関わらず、80% 以上の正解率を叩き出してくれました。が、、、やっぱりもう少し上を目指したいところです。前回は画像ファイルを 1024 の直列に変換して、単なる数値データの羅列として学習しました。文字はあくまでも2次元平面に書かれた絵のようなものですので、32 x 32 のままで画像特有の特徴を捕まえられるようにすれば、もっと上を目指せるはずです。

今回は「空間フィルタ」「プーリング」「Dropout」を使いますので、それぞれのイメージを説明しておきます。


まずは「空間フィルタ」です。左上の 6 x 6 の図を見てください。数字が大きいほど濃い色になっています。左上の 3 x 3 の空間を赤い枠で囲んでありますが、この中心の値は 1 です。例えば、ピンポイントでこの位置の特徴を考えると、「濃さが1」というだけなのですが、人間が書いた文字の中にそんな小さな点だけを打つことはほぼなくて、その周辺とつながっているはずです。縦線の一部として存在するのか、それとも横線の一部として存在するのか、そんな情報こそが文字としての特徴であるはずです。

上記の図ではそういった特徴を抽出するためにフィルタを用いています。中央の1列だけが1で残りの列が0のフィルタにかけた時の様子です。縦のラインの特徴を拾うためのフィルタなので、上下の 2 と 1 も拾って合計し、この赤枠のエリアには 4 という価値を見出しています。言い換えると、縦ラインを抽出するフィルタを持っているニューロンは、このエリアに強く反応するということです。その隣の 3 x 3 の領域にも同じフィルタをかけると、今度は 3 という価値を見出しています。どちらも中央は 1 なのですが、上下の情報にも影響を受けることになるのです。この処理を画像全体に対して施していくことを「畳み込み」と呼びます。これが畳み込みニューラルネットワークと呼ばれる所以です。

ちなみに、右肩下がりの斜めのフィルタを通した場合は、フィルタ後の結果は大きく変わってきます。たくさんの種類のフィルタを用意して順番に適用することで、どのフィルタで強い特徴が現れたかを知ることができ、それこそが文字の特徴を読み取っていく作業ということになります。


つぎに「プーリング」です。上記の 6 x 6 の図は、人間なら数字の「2」として認識するところです。そして、下の画像も数字の「2」として認識することでしょう。左に寄っているか、右に寄っているかの差がありますが、どちらも「2」と認識するのが普通です。当たり前のようですが、このデータを数値の羅列として捉えてしまうと、かなり違うものとして解釈してしまうかもしれません。

では、上記のように上下左右にずれていても同じものとして認識してもらうにはどうしたら良いのでしょうか。意外とシンプルな方法で実現できます。絵を見ればわかると思いますので、説明は省略します。なお、上記の絵では偶々全く同じ結果になっていますが、実際にはこれ程理想的な結果にはなりません。それでも、かなり近いものとして認識されることにはなります。

最後は、「Dropout」です。機械学習を進めていくと、だんだんと正解率が上がるようになっていきます。しかし、あまり学習しすぎると、用意した学習用のデータに特化した脳が出来上がってしまい、評価用のデータで評価すると散々な結果になる可能性があります。これを過学習と呼びます。 適度な学習を促すための工夫が Dropout です。

より詳しく知りたい方には、以下のページをお勧めします。

【Deep Learning】過学習とDropoutについて
http://sonickun.hatenablog.com/entry/2016/07/18/191656

これらを駆使して、最終的なニューラルネットワークの構造は次のようにしました。


実は、参考にした本に載っていた構造とほとんど一緒です。まずはここから始めて試行錯誤するつもりでしたが、そのままでも良好な結果が得られたので。。。

  1 #!/usr/local/bin/python
  2 
  3 from ImageLoader2 import LoadDataSet
  4 from ShowPrediction import ShowPrediction
  5 import numpy as np
  6 
  7 from keras.models import Sequential
  8 from keras.layers import Dense, Dropout, Flatten
  9 from keras.layers import Conv2D, MaxPooling2D
 10 from keras.optimizers import Adam
 11 from keras.utils import np_utils
 12 import time
 13 
 14 model = Sequential() 
 15 model.add(Conv2D(16, (3, 3), input_shape=(32, 32, 1), activation='relu'))
 16 model.add(Conv2D(32, (3, 3), activation='relu'))
 17 model.add(MaxPooling2D(pool_size=(2, 2)))
 18 model.add(Conv2D(64, (3, 3), activation='relu'))
 19 model.add(MaxPooling2D(pool_size=(2, 2)))
 20 model.add(Dropout(0.25))
 21 model.add(Flatten()) 
 22 model.add(Dense(128, activation='relu'))
 23 model.add(Dropout(0.25))
 24 model.add(Dense(48, activation='softmax'))
 25 
 26 model.compile(loss='categorical_crossentropy', 
 27               optimizer=Adam(), metrics=['accuracy'])
 28 datas = LoadDataSet()
 29 datas.get_data('save.pkl')
 30 
 31 num_classes = 48
 32 
 33 x_train = datas.train_img
 34 y_train = datas.train_label
 35 print(y_train.size)
 36 y_train = np_utils.to_categorical(y_train, num_classes)
 37 
 38 x_test = datas.test_img
 39 y_test = datas.test_label
 40 print(y_test.size)
 41 y_test = np_utils.to_categorical(y_test, num_classes)
 42 
 43 startTime = time.time()
 44 
 45 history = model.fit(x_train, y_train, batch_size=3000, epochs=100,
 46                     verbose=1, validation_data=(x_test, y_test))
 47 score = model.evaluate(x_test, y_test, verbose=1)
 48 print('Test loss:', score[0])
 49 print('Test accuracy:', score[1])
 50 print("Computation time:{0:.3f} sec".format(time.time() - startTime))

14行目で model を作っているのは前回と同じです。この脳に対して、15〜24行目でいろいろな層を追加しています。この辺りは絵に書いた通りの作業です。前回と違う点として、活性化関数が sigmoid から relu に変わっています。sigmoid でもできなくはないのですが、そもそも入力データにマイナスがない(画像データなので)のに、マイナスを考慮した sigmoid を使うのはあまり相応しくないのと、relu の方が python にとっては都合が良く、計算が早くできるというメリットがあるので、こちらを採用しています。

26行目の compile 時に最適化関数として adam を指定しているのは前回と同じです。また、その後のデータロード処理も基本的には一緒です。前回は 1024 の直列データに変換していましたが、それはやらずに 32 x 32 のままで利用します。

45 行目で学習して、47 行目で評価しています。その結果を見てみましょう。



Epoch 100/100 と書かれたところに学習の成果が出力されています。
loss: 0.0668
acc: 0.9792
ですから、前回の投稿の時の結果と比較すると、損失が 0.5358 から 0.0668 に下がり、かなり 0 に近づきました。正解率も85.42% から 97.92% へと上がり、かなり満足のいく学習結果です。とはいえ、評価用のデータで評価した時に大きく成績が悪化するようだと過学習です。早速比較してみましょう。

Test loss: 0.1147308552731136
Test accuracy: 0.9756084471628312
となっていますので、評価用データでも97%台の成績を収めています。これなら良好な結果と言えるのではないでしょうか。

最後に、評価した時の画像を並べてその結果をビジュアルに見てみます。


今回も同じ 96 枚の画像の評価結果を表示しています。この 96 枚に限っては 100% の正解率になりました。右上の「ふ」は、前回読めなかった天ぷら屋さんの看板で見かける文字ですが、今回は正しく読めているようです。かしこい!
それにしても「ふ」が「婦」を崩したものだと教えたわけでもなく、書き順や画数を教えたわけでもないのに、見事に学習してくれました。

なんとか満足のいく結果にたどり着いたので、今年のアドベントカレンダーへの連続投稿はこれで終了にします。

一足早いですが、、、

メリィ
クリスマス


kawakin1975さん、あとはよろしくお願いします!

0 件のコメント:

コメントを投稿