Edited at

順伝播型ニューラルネットワークを実装してみた


はじめに

初投稿です

MLPの深層学習を読んだので理解を深めるためにPythonで順伝播型ニューラルネットワークを実装してみました


順伝播型ニューラルネットワークとは

まず、順伝播型ニューラルネットワークの説明の前に単純パーセプトロンについて説明します。


単純パーセプトロン

単純パーセプトロンは入力を受け取ると入力それぞれに対して重み付けをした値とバイアスを合計します。その後、合計した値に活性化関数を通した値を出力します。活性化関数は入力の総和がどのように活性化するか決定する役割を持ちます。単純パーセプトロンは活性化関数にステップ関数を使用します。ステップ関数は入力が0より大きい場合に1、入力が0以下の場合は0を出力する関数です。単純パーセプトロンの動作を、入力個数を$ n $、入力を$ x_1,x_2,\cdots,x_n $、重みを$ w_1,w_2,\cdots,w_n $、バイアスを$ b $、出力を$ u $とし、式で表すと

\begin{eqnarray}

u
=
\begin{cases}
0 & ( w_1 x_1+w_2 x_2+\cdots+w_n x_n +b \leqq 0 ) \\
1 & ( w_1 x_1+w_2 x_2+\cdots+w_n x_n +b \gt 0 )
\end{cases}
\end{eqnarray}


順伝播型ニューラルネットワーク

順伝播型ニューラルネットワーク(feedforward neural network)は単純パーセプトロンを並べたものを一つの層とし、隣接した層を結合したものです。

三層ニューラルネットワーク

上の図は三層の例で左から入力層、中間層あるいは隠れ層、出力層と呼ばれます。各層のノードのことをユニットといいます。上の図の場合は入力層のユニットは3つ、隠れ層のユニットは3つ、出力層のユニットは2つとなっています。第$ l $層のユニット数を$ I $、ユニットを$ i(i=1,2,\cdots,I) $、第$ l+1 $層のユニット数を$ J $、ユニットを$ j(j=1,2,\cdots,J) $とすると第$ l+1 $層の重みは

{{\bf W}^{(l+1)} = 

\begin{pmatrix}
w_{11} & \cdots & w_{1I} \\
\vdots & \ddots & \vdots \\
w_{J1} & \cdots & w_{JI} \\
\end{pmatrix}
}

と行列を用いて表されます。

ニューラルネットワークは他にも畳み込みニューラルネットワーク(CNN)や再帰型ニューラルネットワーク(RNN)等があります。


順伝播

順伝播は入力層の入力を$ {\bf z}^{(1)}={\bf x} $と置き、重み$ W $と前の層の出力$ {\bf z} $の積にバイアス$ {\bf b} $を加えたベクトル$ {\bf u} $に活性化関数$ {\bf f}({\bf u}) $を適用することを各層$ l(l=1,2,\cdots,L-1)$で計算します。式で表すと

{\bf u}^{(l+1)}={\bf W}^{(l+1)}{\bf z}^{(l)}+{\bf b}^{(l+1)} \\

{\bf z}^{(l+1)}={\bf f}({\bf u}^{(l+1)})

となります。


活性化関数

活性化関数はユニットをどのように活性化させるかを決める関数です。問題に合わせて適切な関数を選びます。もし、活性化関数が線形だったりそもそも設定されていない場合(やらかしたことがある)はネットワークは線形分離不可能な問題を解くことはできなくなってしまいます。

よく使われる活性化関数としてロジスティック関数

z = f(u) = \frac{1}{1+\exp(-u)}

正規化線形関数

z = f(u) = max(0,u)

があります。

回帰では出力層に恒等関数

z = f(u) = u

を選び、多クラス分類は出力層にソフトマックス関数

y_k=z^{(L)}_k=\frac{\exp(u^{(L)}_k)}{\sum_{j=1}^{K}\exp(u^{(L)}_j)}

を選びます。

ソフトマックス関数は出力層の各ユニットの出力を受け取り、出力層の各ユニット$ u_k $が各クラス$ C_k $に属する確率を返します。この関数の出力の総和は1になります。


誤差関数

誤差関数は入力を順伝播したネットワークの出力$ y $と入力に対する正解$ d $を受け取り、出力$ y $と正解$ d $の近さの指標となる関数で損失関数ともいいます。

この関数の値が小さいほどデータを学習していることになりますが学習の最終的なゴールは汎化性能であるため訓練データでの誤差が小さくても学習がうまくいっているとはいえません。学習を進めていくと訓練データに対する誤差は減りますがテストデータに対する誤差は途中まで減少するものの、訓練データに対する誤差と離れていき、誤差が大きくなる場合があります。訓練データに対する誤差とテストデータに対する誤差が離れている状態を過学習と呼びます。

回帰でよく使われる誤差関数として二乗誤差

E({\bf w}) = \frac{1}{2}\sum_{n=1}^{N}\| d_n-y_n \|^2

があります。$ n=1,2,\cdots,N $は全サンプルです。各サンプルについて正解とネットワークの出力の差の絶対値を二乗した値を足して2で割っています。2で割っているのはwについて微分したときに2乗の2と相殺するためです。

多クラス分類で使われる誤差関数としては交差エントロピー関数

E({\bf w})=-\sum^N_{n=1}\sum^K_{k=1}d_{nk}\log{y}_{nk}

があります。$ {\bf d}_n $は$ K $クラス分類で正解であるk番目の値のみを1にし、不正解を0にしたベクトルです。例えば$ n $番目のサンプルで10クラスの分類問題(K=10)で正解が5番目のクラスの場合は

$$ {\bf d}_n = [0\,0\,0\,0\,1\,0\,0\,0\,0\,0]^T $$

となります。正解ラベルを1としてそれ以外を0で表す表記をone-hot表現などと言います。


学習

順伝播型ニューラルネットワークの学習は順伝播で計算した誤差関数$ E({\bf w}) $を重みとバイアス$ {\bf w}\,{\bf b} $について最小化することです。しかし、最小値を求めるのは困難なため極小値をもとめます。

誤差関数の勾配$ \frac{\partial E}{\partial {\bf w}}$を求め、現在の$ {\bf w} $を、負の勾配方向に少し動かすことを繰り返して解を求めます。現在の重みを$ {\bf w}^{(t)} $、動かした後の重みを$ {\bf w}^{(t+1)} $とすると

{\bf w}^{(t+1)}={\bf w}^{(t)}-\alpha \frac{\partial E}{\partial {\bf w}}

と更新します。$ \alpha $は学習率と呼ばれ、更新量の大きさを決めるハイパーパラメータです。このように勾配を求めてそれをもとに極小値を求める方法を勾配降下法と呼びます。


バッチ学習

バッチ学習は全訓練サンプル$ n $に対して計算した誤差の和

E({\bf w})=\sum^N_{n=1}E_n({\bf w})

を用いて重みを更新する学習をバッチ学習と呼びます。これに対してサンプルから一つ選んで重みを更新する方法を確率的勾配降下法といいます、また、ランダムに複数のサンプルを選びひとまとめとして誤差の合計を求め、ひとまとめにしたサンプルの数で割った値を用いて重みを更新する方法もあります。ひとまとめにしたサンプルの集合をミニバッチと呼びます。


誤差逆伝播法

学習には勾配を求めることが必要ですがニューラルネットワークの勾配を求めるには微分を計算します。しかし、微分の計算は計算量が増え、実装も大変になります。そこで誤差逆伝播法という方法で勾配を計算することで計算を簡単にできます。


重みの初期値

重みの初期値は学習において非常に重要な要素となります。重みの初期値によっては学習の進み方が大きく変わることがあります。重みの初期値を大きくすると学習が速く進むが誤差関数の減少が早く停止してしまい、小さくすると学習の進みは遅くなりますが過学習を抑えられます。

今回使用したXavierの初期値は前の層のユニット数をnとした場合に重みの初期値を$\frac{1}{\sqrt{n}}$の標準偏差に従う分布とする初期値です。活性化関数によりますが重みの値が偏るのを軽減することができます。

他にも様々な重みの初期値の設定方法があります。


コード

順伝播型のニューラルネットワークをクラスにしました

class FNN():

"""
入力、隠れ、出力層の順伝播型ニューラルネットワーク

"""

def __init__(self, inputnodes, hiddennodes, outputnodes, learningrate, batch_size):
# 入力層、隠れ層、出力層のノード数を設定
self.inodes = inputnodes
self.hnodes = hiddennodes
self.onodes = outputnodes
self.batch_size = batch_size

# 平均0、標準偏差1の正規分布に従う
# self.w2 = np.random.randn(self.hnodes, self.inodes)
# self.b2 = np.zeros((self.hnodes, 1))
# self.w3 = np.random.randn(self.onodes, self.hnodes)
# self.b3 = np.zeros((self.onodes, 1))

# Xavierの初期値
self.w2 = np.random.normal(0.0, pow(1 / self.inodes, 0.5), (self.hnodes, self.inodes))
self.b2 = np.zeros((self.hnodes,1))
self.w3 = np.random.normal(0.0, pow(1 / self.hnodes, 0.5), (self.onodes, self.hnodes))
self.b3 = np.zeros((self.onodes,1))

# 学習率の設定
self.lr = learningrate

def train(self, inputs_list, target_list):
inputs = np.array(inputs_list).reshape( self.inodes, -1)
targets = np.array(target_list).reshape(-1,self.onodes)

z2, u2, z3, loss = self.forward(inputs_list, target_list, train=True)
print("loss", loss)

o = z3

# deltaの計算
# 出力層の誤差 = (最終出力 - 目標出力)
delta_3 = o - targets.T
delta_2 = self.d_sigmoid(u2) * (np.dot(self.w3.T, delta_3))

# 重みを更新
self.b3 += self.lr * (-np.dot(delta_3, np.ones((self.batch_size,1))))
self.w3 += self.lr * (-np.dot(delta_3, z2.T))
self.b2 += self.lr * (-np.dot(delta_2, np.ones((self.batch_size,1))))
z1 = inputs
self.w2 += self.lr * (-np.dot(delta_2, z1.T))

return z3, loss

def forward(self, inputs_list, target_list=None, train=False):
"""
順方向計算
"""

# 入力リストを行列に変換
inputs = np.array(inputs_list).reshape( self.inodes, -1)

# 隠れ層
u2 = np.dot(self.w2, inputs) + np.dot(self.b2, np.ones((inputs.shape[1],1)).T)

# 隠れ層で結合された信号を活性化関数により出力
z2 = self.sigmoid(u2)

# 出力層
u3 = np.dot(self.w3, z2) + np.dot(self.b3, np.ones((inputs.shape[1],1)).T)

# 出力層で結合された信号を活性化関数により出力
z3 = self.softmax(u3)

if target_list is not None:
targets = np.array(target_list).reshape(-1,self.onodes)
loss = self.cross_entropy_error(z3,targets)
if train:
return z2, u2, z3, loss
return z3, loss

return z3

def sigmoid(self, x):
return 1 / (1 + np.exp(-x))

def d_sigmoid(self, x):
"""
シグモイド関数の導関数
"""

return self.sigmoid(x)*(1 - self.sigmoid(x))

def softmax(self, x):
x = x - np.max(x, axis=0)
y = np.exp(x) / np.sum(np.exp(x), axis=0)
return y

def cross_entropy_error(self, y, t):
"""
交差エントロピー誤差を計算する

Parameters
----------
y : numpy.ndarray
ニューラルネットの出力

t : numpy.ndarray
教師データ(one-hot表現)

Returns
-------
error : numpy.ndarray
計算した誤差
"""

batch_size = y.shape[1]

# 教師データがone-hot-vectorの場合、正解ラベルのインデックスに変換
if t.size == y.size:
t = t.argmax(axis=1)

delta = 1e-7
y = y.T
error = -np.sum(np.log(y[np.arange(batch_size), t] + delta)) / batch_size

return error

コンストラクタの引数に学習率と入力層、隠れ層、出力層のユニット数、バッチサイズ渡します

input_nodes = 784  #  mnistの場合28*28=794

hidden_nodes = 500 # 隠れ層のサイズ
output_nodes = 10 # mnistの場合0 ~ 9

learning_rate = 0.01 # 学習率

batch_size = 100 # バッチサイズ
epoch = 10 # 学習回数

network = FNN(input_nodes, hidden_nodes, output_nodes, learning_rate, batch_size)

このニューラルネットワークを使用してmnistデータセットを学習させました

train_loss = []  # 学習誤差

test_loss = [] # テスト誤差
train_accuracy = []
test_accuracy = []

for i in range(epoch):
loss = 0
score = 0
idx = np.random.permutation(len(training_data))
print("epoch:", i)
for j in range(0,60000, batch_size): # ランダムに並べ直したすべてのデータで学習する
data = training_data[idx[j:j+batch_size]]
x_batch = []
t = []
for k in data:
x_batch.append(list(map(lambda x: int(x) , k.split(",")[1:])))
t.append(int(k.split(",")[0]))
x_batch = np.array(x_batch).T / 255.0
y_batch = [ int(k[0]) for k in data]

target = np.zeros(( batch_size, output_nodes))

# one-hot表現に変更
for k in range(batch_size):
target[k, y_batch[k]] = 1

# 学習
output, loss_ = network.train(x_batch, target)
# 訓練誤差と正解率を格納
loss += loss_
labels = np.argmax(output.T, axis=1)
score += np.sum(labels == t)

train_loss.append(loss/(60000/batch_size))
train_accuracy.append(score / 60000)

# テスト誤差を求める
loss = 0
score = 0
test_size = 10000
for j in range(0,test_size,batch_size):
data = test_data[j:j+batch_size]

x_batch = []
t = []
for k in data:
x_batch.append(list(map(lambda x: int(x) , k.split(",")[1:])))
t.append(int(k.split(",")[0]))
x_batch = np.array(x_batch).T / 255.0
y_batch = [ int(k[0]) for k in data]

target = np.zeros(( batch_size, output_nodes))

# one-hot表現に変更
for k in range(batch_size):
target[k, y_batch[k]] = 1
output, loss_ = network.forward(x_batch, target)
loss += loss_
labels = np.argmax(output.T, axis=1)
score += np.sum(labels == t)

test_loss.append(loss/(test_size/batch_size))
test_accuracy.append(score / test_size)

隠れ層の数が500個、学習率を0.01、ミニバッチサイズを100と設定した場合の学習結果です(左がテスト誤差と学習誤差、右が訓練データとテストデータそれぞれの正確度、グラフの横軸はepoch数)

loss.pngaccuracy.png


おわりに

深層学習フレームワーク等を使わずに3層の順伝播ニューラルネットワークを実装しましたが他の機械学習の手法にも通ずる部分があるので自分で実装することも大事だと感じました。


参考図書

ニューラルネットワーク自作入門

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

深層学習 (機械学習プロフェッショナルシリーズ)