· 

掛け算のニューラルネットワークとベイズ推定

(著)山たー

Kerasで掛け算を実行するニューラルネットワークを学習させた話。

ニューラルネットワークによる掛け算

目標は、数$x, y$が与えられたときに積$xy$を出力するようなニューラルネットワークを構成することです。 簡単でつまらない問題に思えますが、実は難しい問題で、神経科学的に意味がある問題です。 まずは、その意義についてお話します。

脳とベイズ推定

脳はベイズ推定を用いている」と言われます。唐突な話題ですが、今回の話に関係することです。ベイズ推定を行っていることの分かりやすい例として、錯視を考えてみましょう。

 

凹凸の錯視

次の画像を見てください。

画像は北岡明佳の錯視のページからお借りしました。

 

この画像は普通に見ると左右が凸で、真ん中が凹んでいるように見えます。しかし、画像を上下ひっくり返すと、

となり、真ん中が凸で左右が凹んでいるように見えます。これは「光源は上にある」ということと、「光は直進する」という先見的な知識があるために起こることです。言い換えれば、「光源の位置」と「光の直進性」に関する事前分布が偏っており、影の位置を見た時に、影が下なら出っ張っている、影が上なら凹んでいるということの事後確率が高くなっているのです。このことをもう少し数学的に説明することにします。

 

錯視とベイズ推定

光源は上である、という先見的知識があれば、影が下のとき、凸のように見えることを示します。なお、光の直進性はややこしくなるので「光源は1つで直進する」と仮定します。また、以下の話は

柴田和久, (2007)『ベイズで読み解く知覚世界』日本神経回路学会誌 Vol. 14, No. 4,313–318

を参考にしました。

 

事象を \begin{align*} I &:\text{下に影がある}\\ S_1 &:\text{凸で上から照明}\\ S_2 &:\text{凹で上から照明}\\ S_3 &:\text{凸で下から照明}\\ S_4 &:\text{凹で下から照明} \end{align*} とします。ここで$I$は網膜像(image), $S$は物理世界(Scene)を表します。各事象に対する尤度と事前確率は、
尤度 $$ 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$のときです。

 

よって、影が下にある場合、事後確率が最大となる「凸で上から照明」という事象が私たちには知覚されるということです。

 

ベイズの定理と掛け算

ベイズ推定に近いこと、ではなく本当に脳がベイズ推定を行うならば

$$P(\theta|x)\propto P(x|\theta)P(x)$$ というベイズの定理により、事前分布と尤度を掛け算する必要があります。

 

ここで「掛け算をする」といいましたが、ニューラルネットワークで掛け算はできるのでしょうか。もしできなければ、ベイズの式を脳は使っていないということになります。逆に掛け算ができるのならば、ベイズの式を使っていないとは言い切れない、ということになります。

 

掛け算を学習させる方針

さて、いよいよ本題に入ります。やみくもにモデルを作ってもうまくいかないので、うまくいくようなモデルと訓練データを考えましょう。

 

掛け算を足し算として実行するには、対数変換を用います。 \begin{align*} x &\to \ln(x)\\ y &\to \ln(y) \end{align*} として、2つの出力を足し合わせ、 $$z:=\ln(x)+\ln(y)=\ln(xy)$$ 最後に指数変換を $$ xy=\exp(z) $$ というように掛ければ、目的の$xy$が得られます。

 

よって対数関数と指数関数を学習させれば掛け算を学習することが可能です。一気に学習させたいところですが、実際にしてみると学習が進まなかったので、2つのモデルを組み合わせることにします。

 

Kerasでの実装

モデルと計算の流れ

初めに全体のモデルを示しておきます。

Illustration

なお、パラメータの数は全部で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()を実行してください。局所解に陥りやすかったので、何度か実行してみるのがよいと思います。

私が確認したところ、成功した場合には対数変換、指数変換共に$10^{-4}$オーダーまでlossが下がりました。 ただし、成功するまでに20回程度実行しました。確率が良ければ良い局所解にたどり着けるということです。

 

学習後、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オーダーの数)が入った掛け算になると、予測値と正解値が大きくずれます(しかも予測値が一致してしまう)。もしかすると、自然対数ではなく、常用対数など、底を変えたものを学習させるともっと良くなるかもしれません。

 

よって、完全に掛け算に成功しているとは言い切れない結果となりました。しかし、その他であれば概ね一致します。今回のようなモデルではないかもしれませんが、ニューラルネットワークに掛け算を学習させることは可能であると思います(もちろん、層を増やして、ユニット数を増やせば一致度はあがります)。

 

学習した重み

今回、学習させて最も良かったパラメータをダウンロードできるようにしました。ただ、これも大域的最適解ではないと思います。

 

log_nn_model_weights5.h5

exp_nn_model_weights5.h5

 

実際のニューロンによる掛け算

こうして、ニューラルネットワークで掛け算の計算を学習させることができました。それでは、実際の脳の中で掛け算は行えるのでしょうか。参考として次の論文を挙げておきます。

Nezis, P. (2008). Multiplication with Neurons. MSc in Informatics, University of Edinburgh, School of Informatics

 

「ニューラルネットワークで掛け算ができるから、脳がベイズの式を使っている」とは言い切れません。あくまで使っているということが否定できないというだけです。ベイジアンニューラルネットワークが流行り始めていますが、ベイジアンネットワークにニューラルネットワークを組み込むのではなく、ニューラルネットワークでベイズを再現した方が実際の脳に近いのかもしれません。すなわち、事前分布と尤度をニューラルネットワークで学習させ、その積である事後分布を今回のようなニューラルネットワークで算出し、認識に用いるということです。

コメントをお書きください

コメント: 1
  • #1

    U.Minor (月曜日, 04 6月 2018 17:43)

    ニューラルネットワークのツール(keras)を用いた掛け算の記事を書いてみたのですが、
    本稿を大変参考にさせていただき、リンクさせていただきました。
    https://qiita.com/uminor/items/6d62991d8917ac853eaf
    よろしくお願いします。