2019年12月23日月曜日

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

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

ニューラルネットワーク

いよいよ、機械学習です。と言っても機械学習で画像の分類課題を実現する方法には色々あります。例えば文字の画像が大量にあって、それらの画像を平仮名と片仮名に分類したいとします。機械学習で分類する場合のひとつの方法として次のようなやり方が考えられます。各画像の中に特徴点を見出し、その特徴を数値化してグラフ上にプロットしていきます。すると散布図ができますが、そのひとつひとつの点が平仮名なら青、片仮名なら赤で表しましょう。青の点が集まっている箇所と赤の点が集まっている場所がきれいにその散布図上に別れてくれれば話は簡単です。2つに分類するための境界線を数式で表すことができれば、それが人工知能の脳になります。新しい画像をその脳に通せば、平仮名か片仮名かをうまく分類してくれるでしょう。

ただ、このアプローチには大きな問題があります。プログラムを書いてみて実感したのですが『ツマラナイ』です。数学は嫌いじゃないのですが、それでも。。。理由は明白で、結局うまく分類するためのルールを考えるのは人間だからでしょう。数学の勉強としては良いてテーマですが『いやいや、機械に勉強してほしいのよ』という気持ちになります。

その点、やっぱり面白そうなのはニューラルネットワークです。人間の脳の神経細胞(ニューロン)とその連携の様子をコンピュータ上に再現することで、何故かうまく分類できちゃうという気持ち悪さが最高です。

では早速(ようやく?)、今回作成するニューラルネットワークの説明に入ります。


一番左が入力層、くずし字の画像データです。そして、右端の出力層では『あ』である確率が一番高いことを見抜いています。この区間でなにが起きているのかを3つに分けて解説します。

まず、入力層では 32 x 32 の二次元配列のデータを 1024 個の直列なデータに変換しています。これは、入力データが単に数値の羅列にすぎないと捉えて、より扱いやすいシンプルな形に変換しているだけです。

続いて中間層ですが、80 個のニューロンを用意することにしました。入力層からの矢印が一番上のニューロンに集中しているのは、入力層の全てのデータをひとつのニューロンにインプットしている様子を表しています。次の瞬間には2つ目のニューロンに対しても、入力層の全てのデータが入力されます。白い丸は、このニューロンが反応しなかったことを表現しています。反応したニューロンは赤で表現しています。ところで、各ニューロンは1024個のデータを同等には扱っていません。例えば、「1個目のデータに対しては最も小さい 0.1の重みをつけていて、54個目のデータには最も大きい2.7の重みをつけていた」と仮定しましょう。このニューロンは 1024 個のデータのうち54個目のデータをとても重要視していることになります。全入力データにそれぞれの重みをかけた値の総和を求めて、閾値を超えているかどうかで2値を出力します。各ニューロンには、この重み付けと閾値が個別に(初めはランダムに)決められています。そのため、全てのニューロンが同じ入力を受け付けているにも関わらず、結果が異なってくるのです。

なお、この「閾値で1/0(反応有無)の振り分けを行う処理」を活性化関数と呼びます。微分を使う都合上なだらかな曲線だけでできている関数にする必要があるので、シグモイド関数を使うことにします。他に使える関数は、keras のドキュメントに掲載されています。

最後に出力層です。中間層から出力された値は、出力層にとっての入力データになります。つまり、出力層のそれぞれのニューロンに対して、80 個のデータが等しく入力されることになります。そして、中間層のニューロンと同じように、個別の反応を見せるわけです。ひとつだけ違う点は、中間層の出力が2値だったのに対し、出力層は自分が正解である確率を出力する点です。「自分」と書いてしまいましたが、実は出力層のニューロンは 48 個にしてあって、これは 48 文字の平仮名の個数です。つまり1つ目のニューロンは "あ" 、2つ目は "い" …… というようにニューロンのひとつひとつが平仮名一文字に相当しています。確率を出力したいので、シグモイド関数を使うことはできません。この場合に相応しいのは、ソフトマックス関数です。これを使うと、48個のニューロンのそれぞれが正解である確率を全て足し合わせると 100% になるようにしてくれます。

全体を通して、入力データの何に着目してどう分類するかについては、人が関与していないのがポイントです。では、実際にプログラムを見てみましょう。
  1 #!/usr/local/bin/python
  2 # coding: utf-8
  3 
  4 from ImageLoader2 import LoadDataSet
  5 from ShowPrediction import ShowPrediction
  6 from keras.utils import np_utils
  7 from keras.models import Sequential
  8 from keras.layers import Dense, Activation
  9 from keras.optimizers import Adam
 10 import numpy as np
 11 import time
 12 
 13 datas = LoadDataSet()
 14 datas.get_data('save.pkl')
 15 
 16 num_classes = 48
 17 
 18 x_train = datas.train_img
 19 y_train = datas.train_label
 20 print(y_train.size)
 21 x_train = x_train.reshape(y_train.size, 1024)
 22 x_train = x_train.astype('float32')
 23 x_train = x_train / 255
 24 y_train = np_utils.to_categorical(y_train, num_classes)
 25 
 26 x_test = datas.test_img
 27 y_test = datas.test_label
 28 print(y_test.size)
 29 x_test = x_test.reshape(y_test.size, 1024)
 30 x_test = x_test.astype('float32')
 31 x_test = x_test / 255
 32 y_test = np_utils.to_categorical(y_test, num_classes)
 33 
 34 np.random.seed(1)
 35 
 36 model = Sequential()
 37 model.add(Dense(80, input_dim=1024, activation='sigmoid'))
 38 model.add(Dense(48, activation='softmax'))
 39 model.compile(loss='categorical_crossentropy',
 40               optimizer=Adam(), metrics=['accuracy'])
 41 startTime = time.time()
 42 history = model.fit(x_train, y_train, epochs=100, batch_size=100,
 43                     verbose=1, validation_data=(x_test, y_test))
 44 score = model.evaluate(x_test, y_test, verbose=0)
 45 print('Test loss:', score[0])
 46 print('Test accuracy:', score[1])
 47 print("Computation time:{0:.3f} sec".format(time.time() - startTime))

パッと見で、ひとつもネストしていないのが特徴です。これがすべてを物語っていますが、人が制御せず機械に丸投げしているので、結果的に for や if などの制御構文が使われない結果になっています。それでは、上記の絵の内容をどのように書いたら実現できるのか、順番に見ていきましょう。

11〜24行目では、Loader クラスを使って学習用のデータを取得しています。その時、ついでに 32 x 32 の二次元配列を 1024 の一次元配列に変換しています。入力層の処理はこれだけで、絵で示したとおりのことをしています。26〜32行目では、評価用のデータを同じように取得しています。

36行目では model を作っていますが、これが人工知能の脳にあたります。続く2行で中間層と出力層を追加していて、これもまた絵で示した通りになっています。もっと多層にしたければ add() を続ければ良いのですが、今回は2つにしておきます。

39行目で compile をしていますが、ここは絵では表現していなかったものです。実は、絵で説明したのは学習済みのニューラルネットワークの挙動です。というのも、学習していない状態のニューロンは適当な重み付けや閾値を持っているにすぎないので、"あ" というデータを入力したとしても、どのニューロンが一番確率が高くなるかはわかりません。一番最初はテキトーな結果になるでしょう。ここで必要になるのが学習用データの正解ラベルです。"あ" という画像データと一緒に、その画像が "あ" であることを示す情報も入力しています。たまたま出た結果が正解ラベルと一致しなかったときには、ニューロンの重み付けや閾値を微調整して正解に近づけるということを繰り返します(バックプロパゲーションと言います)。ニューロンが育っていくイメージが伝わるでしょうか。

このバックプロパゲーション、いつ止めるかが問題です。モデルの設計次第で過学習にも学習不足にもなりえるので、最適な手法を採用したいところです。学習の程度を示す目安に、損失関数という概念があります。全て正解した場合は0になり、間違いが多いほど大きくなる値なのですが、この値が 0 に近づくように学習を進めます。もし、学習時間が経過するとともに損失関数が徐々に0に近くなり、途中から過学習の状態になって数値が 上がっていくというなだらかな曲線ならば、微分を使って極小値を超えた時点でやめてしまえば良いのです。しかし、現実には損失関数のグラフはそんなに単純ではありません。一度極小値に達したら上がり続けるわけではなく、再び0に向かって下降していき、先ほどの極小値よりも0に近いところに達する可能性も充分にあるのです。そのように、局所最適解にはまり込まずに、全体最適解に辿り着くための方法が色々と考えられてきましたが、ここでは adam という最適化関数を使用します。

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

勾配降下法の最適化アルゴリズムを概観する
https://postd.cc/optimizing-gradient-descent/

42行目が、学習を丸投げしているところです。batch_size=100 というのは、入力データ数を 100 個ずつに分けてその batch 毎に学習を行うことを指示しています。全データ数を batch に分け、その全部の batch を1回ずつ学習するのを、1エポックと呼びます。epochs=100 というのは、エポックを 100 回繰り返すことを指示しています。

44行目は、評価を丸投げしているところです。42行目の学習で育ったニューロンに対して評価用のデータを入力して、損失や正解率をみています。

実行結果がちょっと見にくいですが、Epoch 100/100 と書かれたところに学習の成果が出力されています。
loss: 0.5358
acc: 0.8542
とありますので、損失が 0.53 とかなり大きめの数字です。もっと 0 に近づけたかったところですが、Epoch を増やしてもあまり良い結果にはなりませんでした。それでも正解率は 85% 程にはなっているので及第点くらいでしょうか。

そのすぐ下に評価結果も出力されています。
Test loss: 0.6641609928329454
Test accuracy: 0.8296347975340643
とありますので、損失が 0.66 でかなり大きいですね。正解率も 83% くらいにしかなっていません。学習用データに特化してしまった過学習の状況なのかもしれません。

数値的にちょっと残念な結果なのですが、それでも 80% 以上は正解しています。そこで、もっとビジュアルに結果が見られるようにしてみました。

評価に使われた画像のうち先頭の 96 枚について、実際の画像を表示しました。そして左下には青い文字で正解を、右下には赤い文字で人工知能が回答した結果を表示しました(こんなことも Python なら簡単にできるところがスゴイ!)。さらに不正解だったものは画像の上に赤い斜線を入れてみました。

一番上の段の左から2個目と、一番下の段の右端の画像は、どちらも「は」なのですが、きちんと読めているようです。対して、右上の「ふ」はちょっと難しかったようです。天ぷら屋さんの看板によく書かれている文字なので、これくらいは読めて欲しいところです。


ちょっと悔しい結果。次回、さらなる高みを目指す。

0 件のコメント:

コメントを投稿