しいたげられたしいたけ

空気を読まない。他人に空気を読むことを要求しない。

O'REILLY『ゼロから作るDeep Learning』4章のクラスを使ったら4セグメントLEDどころか7セグメントLEDの機械学習ができた!(その1)

 

前回の記事から1ヶ月以上経ってしまった。『ゼロから作るDeep Learning ―Pythonで学ぶディープラーニングの理論と実装』という本を読んでいる。3章でとても難易度の高いと感じるカベにぶち当たったので、自分自身の理解の度合いを確認するために、難易度の低い練習問題を作って解いてみたという内容のエントリーだ。

www.watto.nagoya

上掲書3章には、「ニューラルネットワーク」すなわち行列積といくつかの関数の組み合わせを用いて、MNISTデータセットすなわち手書き数字のテストデータを認識させる方法について説明がある。それが初読時にはとても難しく感じられたので、ごく簡単な例として「4セグメントLED」というのを考えてみたのだ。それをテキストP84~86でいうところの「機械学習」ではない「人の考えたアルゴリズム」で、認識させることはできた。

では、それを「機械学習」によって実現できるかが、自分自身に与えた次なる課題であった。

結論を先に書くと、実現できた。しかも驚くべきことに、4章P113以降に掲げられた「2層ニューラルネットワークのクラス」に対し、適切と思われるデータおよび引数(上掲書で言うところの「ハイパーパラメータ」?)を与えるだけで、実現できてしまった!

さらに驚くべきことに、4セグメントどころか、現実に使用されている7セグメントLEDに対しても、同じクラスを用いた機械学習による認識が実現できてしまった!

そのことを説明するため、何回かにわたるエントリーを起こす。対象読者は『ゼロから作るDeep Learning』(以下テキストと表記する)読者限定なので、いつものように遠慮して、新着エントリーから目立ちにくくするため日付をさかのぼって公開する。

スポンサーリンク

 

テキスト4章では、勾配法による2変数の関数の最小値の探索が説明される(P109まで)。それに続いて2次元6変数のニューラルネットワークに対して勾配を計算する例が示される(P110~112)。このニューラルネットワークは、単なる数値例に過ぎず、何か具体的なモデルがあるわけではない。

さらに続いて、2層ニューラルネットワークのクラスが示され、このテキストの核心部とも言うべきMNISTデータセットに対するミニバッチ学習の実装の説明が始まるのである(P117~)。ここでまた、断崖絶壁ともいうべき難易度の変化を感じた。「また」と書いたのは、3章でも感じた感覚に似ていたからだ。

そこで今回もまた、自分でごく簡単な例題を作って解いてみようと考えた。断崖絶壁にハシゴをかけるようなつもりである。

しかし、前回やった4セグメントLEDでも難易度高いと感じた。そこで2章までさかのぼって、パーセプトロンの実装を機械学習でやらせられないかと考えた。パーセプトロンとは、おおざっぱに言うと「重み」と「バイアス」による論理回路の実現である。

テキストP110には、2行×3列の「重みパラメータ」のみを有するニューラルネットワークに対して勾配を求めるクラス "simpleNet" として、次のようなコードが示されている。

import sys, os
sys.path.append(os.pardir)
import numpy as np
from common.functions import softmax, cross_entropy_error
from common.gradient import numerical_gradient
class simpleNet:
    def __init__(self):

        self.W = np.random.randn(2,3) # ガウス分布で初期化
    def predict(self, x):

        return np.dot(x, self.W)
    def loss(self, x, t):
        z = self.predict(x)
        y = softmax(z)
        loss = cross_entropy_error(y, t)
        return loss

(Windowsコマンドプロンプトのpython対話モードにコピー&ペーストできるよう、一部改行を削除しています)

このコードを実行することにより、テキストP111に示されるような「重みパラメータ」「最大値のインデックス」「損失関数の値」を計算することができるが、前述のとおりこれらの値には特段の意味があるわけではない。

そこで「重みパラメータ」を2行×3列から1行3列に変更し、変数名としてはちょっと変だが、W[0]とW[1]を改めて「重み」、W[2]を「バイアス」とすることにより、2章パーセプトロンを機械学習できないかと考えたのだ。

少なからぬ苦労をしたが、できた。結果のコードを示す。クラス名 "Perceptrn" としてみた。メソッド "predict" は推定値を求める関数で、この値が正なら "1"、負なら "0"。"loss" は推定値にシグモイド関数(テキストP45、48)を噛ませた上で、正解との二乗誤差により損失を計算する関数である。

import sys, os
sys.path.append(os.pardir)
import numpy as np
from common.functions import sigmoid
from common.gradient import numerical_gradient

class Perceptrn:
    def __init__(self):
        self.W = np.random.randn(3)
    def predict(self, x):
        return x[0]* self.W[0]+ x[1] *self.W[1] + self.W[2]
    def loss(self, x, t):
        z1= sigmoid(self.predict(x))
        y = (z1-t)**2
        return y

テキストP110のクラス "simpleNet" が実行できる環境であれば、実行させられるはずである。

Windows の Anaconda Prompt で実行させたところ、ガウス分布で初期化した重みWの初期値は、こうだった。

f:id:watto:20170629144414p:plain

推定値を見やすくするためステップ関数(テキストP45)をインポートする。

学習率 "learning_rate" は 1.0 としてみた。

from common.functions import step_function

pct = Perceptrn()

f = lambda w: pct.loss(x, t)
learning_rate = 1.0 

python に不慣れなので、for文をこんなふうに記述するのが適切かどうかは、わからない。真理値表みたいに見えないかという工夫のつもりである。

for (x, t) in [\
(np.array([0, 0]), 0),(np.array([0, 1]), 0),(np.array([1, 0]), 0),(np.array([1, 1]), 1)]:
    print("p = " + str(pct.predict(x)) + " : " + str(step_function(pct.predict(x))))
    print(“l = " + str(pct.loss(x,t)))
    dW = numerical_gradient(f,pct.W)
    print("dW = " + str(dW))
    pct.W -= learning_rate * dW
    print("W = " + str(pct.W))

これを [Ctrl]+[v] で Anaconda Prompt に連続して貼り付けた。

貼り付け1回目の結果。 "p" は推定値で正なら "1"、負なら "0"。ちょっとは見やすくならないかなと、ステップ関数による "1"/"0"への変換を付け加えてみた。"l" は損失、"dW" は損失に勾配法を適用して求めた重みの変化、"W" は重みである。

当然ながら真理値表はメチャクチャである。

f:id:watto:20170629202912p:plain

2回目の結果。損失 "l" が減っていることが確認できる。いや、局所的には増える場合もあるのか。重み "W" の微調整を繰り返すたびに、トータルでは減少するはずである。

f:id:watto:20170629202908p:plain

18回目の貼り付け結果。ついに正しい真理値表が得られた! 何度も試したところ、だいたい20回目くらいまでには正しい結果が得られるようだ。

f:id:watto:20170629202905p:plain

W[0]、W[1]、W[2] の比は 1:1:-1.51 くらいの値に収束する。これがパーセプトロンによるANDゲートの、機械学習によって求められた重みとバイアスと言えよう。

もちろん繰り返しペーストする必要はなく、for文を2重化することにより、何度でも繰り返すことができる。ただペーストするたびに値が変化するのが、見ていて面白かったのだ。

この面白さ加減をなんとか他人に伝えられないかと、グラフを描いてみたりgifアニメ化しようとしたりしたが、どうもイマイチである。やはり自分でスクリプトを動作させるにしくはないようだ。

グラフだけ貼ってみようかな。上側の青い実線が W[0]、下側の緑の点線が W[2] である。横軸は繰り返し回数だ。

f:id:watto:20170629213638p:plain

なお当然ながら、for 文の繰り返し条件を

(np.array([0, 0]), 0),(np.array([0, 1]), 0),(np.array([1, 0]), 0),(np.array([1, 1]), 1)]:

から

(np.array([0, 0]), 0),(np.array([0, 1]), 1),(np.array([1, 0]), 1),(np.array([1, 1]), 1)]:

に変えるとORゲートを実現する重みとバイアスが、また

(np.array([0, 0]), 1),(np.array([0, 1]), 1),(np.array([1, 0]), 1),(np.array([1, 1]), 0)]:

に変えるとNANDゲートを実現する重みとバイアスが得られた。OR の場合、W[0]、W[1]、W[2] の比は 1:1:-0.47 くらいに収束した。また NAND の場合は、比は AND と同じくらいだったが W[0]、W[1] 、W[2] の符号が反転した。

   *       *       *

この結果に勇気を得て、5月21日のエントリーで自分自身に与えた宿題、すなわち機械学習による「4セグメントLED」の認識にチャレンジした。

この項つづく。 

ゼロから作るDeep Learning ―Pythonで学ぶディープラーニングの理論と実装

ゼロから作るDeep Learning ―Pythonで学ぶディープラーニングの理論と実装