(著)山たー
Kerasで掛け算を実行するニューラルネットワークを学習させた話。
ニューラルネットワークによる掛け算
脳とベイズ推定
「脳はベイズ推定を用いている」と言われます。唐突な話題ですが、今回の話に関係することです。ベイズ推定を行っていることの分かりやすい例として、錯視を考えてみましょう。
凹凸の錯視
次の画像を見てください。
となり、真ん中が凸で左右が凹んでいるように見えます。これは「光源は上にある」ということと、「光は直進する」という先見的な知識があるために起こることです。言い換えれば、「光源の位置」と「光の直進性」に関する事前分布が偏っており、影の位置を見た時に、影が下なら出っ張っている、影が上なら凹んでいるということの事後確率が高くなっているのです。このことをもう少し数学的に説明することにします。
錯視とベイズ推定
光源は上である、という先見的知識があれば、影が下のとき、凸のように見えることを示します。なお、光の直進性はややこしくなるので「光源は1つで直進する」と仮定します。また、以下の話は
柴田和久, (2007)『ベイズで読み解く知覚世界』日本神経回路学会誌 Vol. 14, No. 4,313–318
を参考にしました。
尤度 $$ 1=P(I|S_1)=P(I|S_4)>P(I|S_2)=P(I|S_3)=0 $$ 事前確率 $$ P(S_1)=P(S_2)>P(S_3)=P(S_4) $$ となると考えられます。尤度がこのようになるのは、光源が1つで光が直進すると仮定するならば、下に影ができるのは$S_1$か$S_4$の場合だけだからです。事前確率がこのようになるのは、「光源は上に存在する」という先見的知識によります。ここで事後確率は $$ P(S|I)=\frac{P(I,S)}{P(I)}=\frac{P(I|S)P(S)}{P(I)}\propto P(I|S)P(S) $$ で計算できます。ゆえに事後確率は $P(S_1|I)>P(S_4|I)>P(S_2|I)=P(S_3|I)$ となり、最大となるのは$S=S_1$のときです。
よって、影が下にある場合、事後確率が最大となる「凸で上から照明」という事象が私たちには知覚されるということです。
ベイズの定理と掛け算
ベイズ推定に近いこと、ではなく本当に脳がベイズ推定を行うならば、
ここで「掛け算をする」といいましたが、ニューラルネットワークで掛け算はできるのでしょうか。もしできなければ、ベイズの式を脳は使っていないということになります。逆に掛け算ができるのならば、ベイズの式を使っていないとは言い切れない、ということになります。
掛け算を学習させる方針
さて、いよいよ本題に入ります。やみくもにモデルを作ってもうまくいかないので、うまくいくようなモデルと訓練データを考えましょう。
よって対数関数と指数関数を学習させれば掛け算を学習することが可能です。一気に学習させたいところですが、実際にしてみると学習が進まなかったので、2つのモデルを組み合わせることにします。
Kerasでの実装
モデルと計算の流れ
初めに全体のモデルを示しておきます。
なお、パラメータの数は全部で106個です。
訓練データの準備
まずはライブラリをインポートし、訓練データを作ります。1~100の自然数からランダムに数を選び、Nx2の2次元配列にします。次に同じ行の数同士をかけたものを正解とします。正の数同士の掛け算のみを考えるのは、確率の積において負の値が存在しないからです。
見やすさを重視して初めに整数のデータセットを作りましたが、モデルに入力するときは0~1に正規化しています。
#ライブラリのimport import numpy as np from keras.layers import Input, Dense,Add from keras.models import Model from keras.callbacks import EarlyStopping ''' データの生成 ''' N = 10000 N_train = int(N * 0.9) N_validation = N - N_train #1~100の数を設定 X=np.random.randint(1,100,N*2) #配列の次元を変更 X=np.reshape(X,(N,2)) #正解データを作る Y=X[:,0]*X[:,1] #train, validationを分割し、正規化 X_train=X[:N_train]/100 Y_train=Y[:N_train]/10000 X_validation=X[N_train:]/100 Y_validation=Y[N_train:]/10000 #対数変換した訓練データ兼正解データ lnX_train=np.log(X_train)
対数変換のモデル
def log_nn_model(): log_inputs = Input(shape=(1,)) log_x = Dense(4, activation='relu')(log_inputs) log_x = Dense(4, activation='relu')(log_x) log_x = Dense(4, activation='relu')(log_x) log_outputs = Dense(1, activation='linear')(log_x) model = Model(inputs=log_inputs,outputs=log_outputs) model.compile(loss='mean_squared_error', optimizer='adam') model.summary() return model
指数変換のモデル
def exp_nn_model(): lnx1_input=Input(shape=(1,)) lnx2_input=Input(shape=(1,)) #2つの入力を足し合わせる added = Add()([lnx1_input, lnx2_input]) exp_x = Dense(4, activation='relu')(added) exp_x = Dense(4, activation='relu')(exp_x) exp_x = Dense(4, activation='relu')(exp_x) output = Dense(1, activation='relu')(exp_x) model = Model(inputs=[lnx1_input,lnx2_input],outputs=output) model.compile(loss='mean_squared_error', optimizer='adam') model.summary() return model
学習と結果表示
対数変換、指数変換の順に学習させます。train()を実行してください。局所解に陥りやすかったので、何度か実行してみるのがよいと思います。
学習後、2つの数を別々に対数変換のモデルに通し、出てきた出力を指数変換のモデルに通すことで積の推定値を得ます。
def train(): early_stopping = EarlyStopping(monitor='val_loss', patience=10, verbose=1) epochs = 500 batch_size = 128 #対数変換の学習 log_model=log_nn_model() #log_model.load_weights('log_nn_model_weights.h5') log_model.fit(X_train[:,0], lnX_train[:,0], batch_size=batch_size, epochs=epochs, validation_split=0.2, shuffle=True, callbacks=[early_stopping]) log_model.save_weights('log_nn_model_weights.h5') print('save weights') #指数変換の学習 exp_model=exp_nn_model() #exp_model.load_weights('exp_nn_model_weights.h5') exp_model.fit([lnX_train[:,0], lnX_train[:,1]], Y_train, batch_size=batch_size, epochs=epochs, validation_split=0.2, shuffle=True, callbacks=[early_stopping]) exp_model.save_weights('exp_nn_model_weights.h5') print('save weights') # 検証データからランダムに問題を選んで答え合わせ for i in range(20): index = np.random.randint(N_validation) question = X_validation[index] answer = Y_validation[index] predict_log_x1 = log_model.predict([question[0]]) predict_log_x2 = log_model.predict([question[1]]) prediction = exp_model.predict([predict_log_x1 ,predict_log_x2]) print('-' * 10) print('Q: '+str(question[0]*100)+'x'+str(question[1]*100)) print('A: '+str(answer*10000)) print('P: '+str(prediction[0][0]*10000)) print('-' * 10)
学習結果
学習結果は以下のようになりました。Qが計算式、Aが正解、Pが予測値です。
----------
Q: 66.0x16.0
A: 1056.0
P: 1049.43178594
----------
Q: 34.0x81.0
A: 2754.0
P: 2634.07468796
----------
Q: 37.0x70.0
A: 2590.0
P: 2511.4107132
----------
Q: 26.0x47.0
A: 1222.0
P: 1434.26731229
----------
Q: 81.0x35.0
A: 2835.0
P: 2676.93728209
----------
:
:
----------
Q: 56.0x80.0
A: 4480.0
P: 4670.55916786
----------
Q: 29.0x11.0
A: 319.0
P: 290.143042803
----------
完全に一致はしませんでしたが、概ね一致しています。ただし、
----------
Q: 7.0x82.0
A: 574.0
P: 302.201136947
----------
Q: 17.0x2.0
A: 34.0
P: 302.201136947
----------
のように、一桁の数(実際には0.01オーダーの数)が入った掛け算になると、予測値と正解値が大きくずれます(しかも予測値が一致してしまう)。もしかすると、自然対数ではなく、常用対数など、底を変えたものを学習させるともっと良くなるかもしれません。
よって、完全に掛け算に成功しているとは言い切れない結果となりました。しかし、その他であれば概ね一致します。今回のようなモデルではないかもしれませんが、ニューラルネットワークに掛け算を学習させることは可能であると思います(もちろん、層を増やして、ユニット数を増やせば一致度はあがります)。
学習した重み
今回、学習させて最も良かったパラメータをダウンロードできるようにしました。ただ、これも大域的最適解ではないと思います。
実際のニューロンによる掛け算
こうして、ニューラルネットワークで掛け算の計算を学習させることができました。それでは、実際の脳の中で掛け算は行えるのでしょうか。参考として次の論文を挙げておきます。
Nezis, P. (2008). Multiplication with Neurons. MSc in Informatics, University of Edinburgh, School of Informatics
「ニューラルネットワークで掛け算ができるから、脳がベイズの式を使っている」とは言い切れません。あくまで使っているということが否定できないというだけです。ベイジアンニューラルネットワークが流行り始めていますが、ベイジアンネットワークにニューラルネットワークを組み込むのではなく、ニューラルネットワークでベイズを再現した方が実際の脳に近いのかもしれません。すなわち、事前分布と尤度をニューラルネットワークで学習させ、その積である事後分布を今回のようなニューラルネットワークで算出し、認識に用いるということです。
コメントをお書きください
U.Minor (月曜日, 04 6月 2018 17:43)
ニューラルネットワークのツール(keras)を用いた掛け算の記事を書いてみたのですが、
本稿を大変参考にさせていただき、リンクさせていただきました。
https://qiita.com/uminor/items/6d62991d8917ac853eaf
よろしくお願いします。