Edited at

簡単なニューラルネットの理論と実装

More than 3 years have passed since last update.

ニューラルネットワーク(以下、NN)のコードを説明する必要があったので、これを機に簡単にまとめておく事にしました。

以下、NNに関する基本的な知識に関しては既知として進めます。


参考資料

学習とニューラルネットワーク (電子情報通信工学シリーズ)


やったこと


  • バックプロパゲーション(以下、BP)の理論整理

  • 3層ニューラルネットワークの実装


BPの理論


記号

ネットワークへの入力:$( x_{1},x_{2},...,x_{N} )$

ネットワークからの出力:$( y_{1},y_{2},...,y_{M} )$

ネットワークの入出力関係:$( y_{1},y_{2},...,y_{M} ) = F( x_{1},x_{2},...,x_{N} )$

結合重み:{$w_{ij}$}

閾値:{${\theta_{i}}$}


BPの概要


  • フィードフォワード型NNの学習方法の1つ(リカレント型NN用に拡張可能)

  • $( y_{1},y_{2},...,y_{M} ) = F( x_{1},x_{2},...,x_{N} )$が、指定された入出力関係を実現するように、ネットワーク内部のパラメタ{$w_{ij}, \theta_{i}$}を定める手続きを与える手法
    (ただし、学習の結果、目標の入出力関係を完全に実現出来るとは限らない。)

  • 関数Fにはsigmoid関数などが使用される

  • BPでは勾配法に基づいて{$w_{ij}, \theta_{i}$}を定める

  • 勾配法では目標の達成度合いを測るために評価尺度を用いる(今回は2乗誤差。以下、Eとする。)

  • ネットワーク内部のパラメタ{$w_{ij}, \theta_{i}$}に適当な初期値を与え、誤差評価尺度Eを小さくするようにパラメタの微小修正を繰り返す

  • 結果、パラメタ値の組は評価尺度の極小点の1つに収束する


BPの利点

上の参考書籍にはBPが広範に利用されるようになった理由について以下のような記載がありあます。



  • 各パラメータに関する評価関数の偏微分の計算は、一般の非線形システムでは複雑になりがちであるが、BPでは体系的、組織的な計算方法が存在する。しかも、各パラメタの修正量を決定する上で必要となる情報は、ネットワークが既知に持っている結線構造を使用して必要箇所にまで伝える事ができる。この事は、学習用に特別な通信線を設ける必要が無いことを意味し、アルゴリズムやハードウェアの簡略化に都合が良い。

  • ネットワークの関数近似能力が理論的に保証されている。中間層素子を充分多数用いれば、適当にパラメタ値(結線重み、閾値)を定めることで任意の関数を任意に高い精度で近似できる。BPによって、最適な近似を与えるパラメタ値を見つけ出すことが出来るとは限らないが、一種の存在定理が証明されていることで、安心感が与えられる。



関数近似

以下の操作を行う。


  • パラメタ初期値設定

  • 収束判定

  • 学習データの投入

  • 勾配計算

  • パラメタ修正


単一ニューロンにおける関数近似

逐次更新学習法を導くので、訓練データ($x^{(l)}, y^{(l)}$)毎に次式の評価尺度を小さくすることを目標として勾配法を適用する。

$E(w_0, w_1, ..., w_N)=|y^{(l)}-\hat{y}^{(l)}|^2$

$s=\sum_{n=0}^{N}w_nx_n^{(l)}$

$\hat{y}^{(l)}=sigmoid(s)$

誤差評価尺度$E(w_0, w_1, ..., w_N)$を$w_n$で微分すると次式が得られる。

$\frac{\partial E(w_0, w_1, ..., w_N)}{\partial w_n}=-2(y^{(l)}-\hat{y}^{(l)})sigmoid'(s)x_n^{(l)}$

ここで、

$sigmoid(s)=\frac{1}{1+e^{-\alpha s}}$

これを微分すると、

$sigmoid'(s)=\frac{\alpha e^{-\alpha s}}{(1+e^{-\alpha s})^2}$

更にこれをまとめると、

$\frac{\alpha e^{-\alpha s}}{(1+e^{-\alpha s})^2}$

$=\alpha \frac{1}{1+e^{-\alpha s}}(1-\frac{1}{1+e^{-\alpha s}})$

$=\alpha sigmoid(s)(1-sigmoid(s))$

$=\alpha \hat{y}^{(l)}(1-\hat{y}^{(l)})$

以上によって求まる$\partial E(w_0, w_1, ..., w_N)/\partial w_n$の値を用いて、結線重みを次式で修正することを繰り返す。

$w_n=w_n-\epsilon \frac{\partial E(w_0, w_1, ..., w_N)}{\partial w_n}$


導出


チェーンルール



xの微小変化は上の図の変数の依存関係に従い、連鎖的に他の変数値の変化へと波及していく。チェーンルールによればこれらの変数の微小変化量$\Delta x,\Delta z_1,\Delta x_2,\Delta y$の間に次の関係式が成り立つ。

$\Delta z_1=\frac{\partial z_1}{\partial x}\Delta x$

$\Delta z_2=\frac{\partial z_2}{\partial x}\Delta x$

$\Delta y=\frac{\partial y}{\partial z_1}\Delta z_1 + \frac{\partial y}{\partial z_2}\Delta z_2$

上の式を整理すると、

$\frac{\partial y}{\partial x}=\frac{\partial y}{\partial z_1}\frac{\partial z_1}{\partial x} + \frac{\partial y}{\partial z_2}\frac{\partial z_2}{\partial x}$


3層NN

上のチェーンルールを踏まえて、入力層、中間層、出力層からなるNNのBPの導出を行います。

3層全てユニット数は3とします(バイアス項を考慮すると、入力層、中間層は4ユニット)。

出力層のユニットから順に、出力層:{1,2,3}、中間層:{4,5,6}、入力層:{7,8,9}と番号を振ります。

そして、


  1. 中間層4番ユニットから出力層2番ユニットへの重み$w_2$が微小に変化した場合

  2. 入力層5番ユニットから中間層4番ユニットへの重み$w_4$が微小に変化した場合

の2つの場合を例に、BPの導出を行います。

それぞれの伝搬のイメージを以下に記載します。


上記1の場合

$w_2$の変化量$\Delta w_2$と$s_2$の変化量$\Delta s_2$の関係が次式で示される。

$\Delta s_2=\Delta w_2y_4$

また、

$y_2=sigmoid(s_2)$

より

$\Delta y_2=sigmoid'(s_2)\Delta s_2$

この$y_2$の変化によって、誤差評価尺度の値が変化し、次の関係式が成り立つ。

$\Delta E=2(y_2-t_2)\Delta y_2$

ここで、$t_2$は$y_2$の正解データのことを指す。

式を整理すると、

$\Delta E=2(y_2-t_2)sigmoid'(s_2)y_4\Delta w_2$

$\frac{\partial E}{\partial w_2}=2(y_2-t_2)sigmoid'(s_2)y_4$

上式から、勾配法で$w_2$の修正に必要となる偏微分係数$\partial E/\partial w_2$が求まった。

また、$\Delta s_2=y_4 \Delta w_2$より

$\frac{\partial E}{\partial s_2}=2(y_2-t_2)sigmoid'(s_2)$

も同時に求まる。

一階層上のweightが変化した場合の偏微分係数を求める時にこれらの値をそのまま用いる事が出来る。


上記2の場合

$w_4$を変化させた場合の変化量の関係は次の式で求められる。

$\Delta s_4=\Delta w_4 y_5$

$\Delta y_4=sigmoid'(s_4) \Delta s_4$

こうして生じる$y_4$の変化は、接続先の$s_1,s_2,s_3$に変化を及ぼす。したがって変化量の間に次の関係式が成立する。

$\Delta s_1=w_1 \Delta y_4$

$\Delta s_2=w_2 \Delta y_4$

$\Delta s_3=w_3 \Delta y_4$

上の変化は誤差評価尺度にも次式のように変化を与える。

$\Delta E=\frac{\partial E}{\partial s_1} \Delta s_1+\frac{\partial E}{\partial s_2} \Delta s_2+\frac{\partial E}{\partial s_3} \Delta s_3$

これを整理し、両辺を$\Delta s_4$で割ると、

$\frac{\partial E}{\partial s_4}=(\frac{\partial E}{\partial s_1}w_1+\frac{\partial E}{\partial w_2}s_2+\frac{\partial E}{\partial s_3}w_3)sigmoid'(s_4)$

が求まる。

上の式は、中間層の$\partial E/\partial s_i$を出力層の$\partial E/\partial s_j$によって求める一種の漸化式になっている事がわかる。同様にして、何層のNNでも最終層の$\partial E/\partial s$さえ求めれば、下の層に誤差を伝搬する事が出来る事がわかる。

最後に、

$\frac{\partial s_4}{\partial w_4}=y_5$



$\frac{\partial E}{\partial w_4}=\frac{\partial E}{\partial s_4}\frac{\partial s_4}{\partial w_4}$

に代入し、

$\frac{\partial E}{\partial w_4}=\frac{\partial E}{\partial s_4}y_5$

を得る。

$sigmoid'(s)$は、単一ニューロンの部分で示した$y=sigmoid(s)$を用いる事で計算を簡単化する。


参考コード

かなり汚いコードで申し訳有りませんが、以下貼り付けます。

閾値(bias)は簡単のため全ニューロン固定としています。実際コードを作成される場合は、入力ベクトルの先頭に値1の要素を追加して、weightベクトルにbiasの要素を追加し、weightの一部としてパラメタ調整を行って下さい。

# coding: utf-8


import numpy as np
Afrom numpy.random import randint
import sys

class NN:
def __init__(self):
self.alph = 0.04
self.mu = 0.01
self.theta = 0.1
self.w = []
self.output = []
self.output_sigm = []
self.T = 0

def create_data(self, input_n_row, input_n_col, layer_sizes):
self.x = randint(2, size=(input_n_row, input_n_col))
self.y = randint(2, size=(input_n_row, layer_sizes[-1]))
for i_layer, size in enumerate(layer_sizes):
if i_layer == 0:
self.w.append(np.random.randn(input_n_col, size))
self.output.append(np.zeros((input_n_row, size)))
self.output_sigm.append(np.zeros((input_n_row ,size)))
else:
self.w.append(np.random.randn(layer_sizes[i_layer-1], size))
self.output.append(np.zeros((input_n_row, size)))
self.output_sigm.append(np.zeros((input_n_row ,size)))

def fit(self, eps=10e-6):
error = sys.maxint
self.forward()
while error>eps:
self.update( self.backword() )
self.forward()
error = self.calculate_error()
self.T += 1
print "T=", self.T
print "error", error

def calculate_error(self):
return np.sum( np.power(self.y - self.output_sigm[-1], 2) )

def forward(self):
for i_layer in xrange(len(self.output)):
if i_layer == 0:
self.output[i_layer] = self.x.dot(self.w[i_layer])
self.output_sigm[i_layer] = self.sigmoid(self.output[i_layer])
else:
self.output[i_layer] = self.output_sigm[i_layer-1].dot(self.w[i_layer])
self.output_sigm[i_layer] = self.sigmoid(self.output[i_layer])

def backword(self):
result = []
for i_layer in range(len(self.w))[::-1]:
if i_layer==len(self.w)-1:
result.insert(0, self.diff(self.output_sigm[i_layer], self.y) )
else:
result.insert(0, self.diff_mult( self.output_sigm[i_layer], result[0].dot(self.w[i_layer+1].T)) )
return result

def update(self, diff):
for i_layer in range(len(self.w))[::-1]:
if i_layer==0:
for i_row in xrange(len(diff[i_layer])):
self.w[i_layer] -= self.get_incremental_update_value(
self.x[i_row].reshape(len(self.w[i_layer]),1),
diff[i_layer][i_row,:].reshape(1,self.w[i_layer].shape[1])
)
else:
for i_row in xrange(len(diff[i_layer])):
self.w[i_layer] -= self.get_incremental_update_value(
self.output_sigm[i_layer-1][i_row,:].reshape(len(self.w[i_layer]),1),
diff[i_layer][i_row,:].reshape(1,self.w[i_layer].shape[1])
)

def get_incremental_update_value(self, input_data, diff):
return np.kron(input_data, self.mu*diff)

def diff(self, y, t):
return self.alph * 2*(y - t) * self.dsigmoid(y)

def diff_mult(self, y, prp_value):
return self.alph * self.dsigmoid(y) * prp_value

def sigmoid(self, s, alph=0.01):
return 1/(1+np.exp(-self.alph*(s-self.theta)))

def dsigmoid(self, y):
return y * (1 - y)

if __name__=='__main__':
layer_sizes = (4,3)
input_layer_size = 3
input_data_size = 1000

nn = NN()
nn.create_data(input_data_size, input_layer_size, layer_sizes)
nn.fit()

お手数ですが間違いがありましたらご指摘いただけますと助かります。