はじめに
pythonで3層のニューラルネットワークを実装し,XNORの識別をしてみました.
数式も載せたので,興味のある方は読んでみてください.
教科書として『深層学習』を使いました.
本記事の構成
- はじめに
- ニューラルネットワーク
- 重みの更新
- 誤差逆伝播
- XNOR
- pythonでの実装
- 結果
- おわりに
ニューラルネットワーク
ニューラルネットワークとは,人間の脳の神経回路を模したモデルです.このモデルを使うことで,画像認識や音声認識が可能となります.
今回実装したネットワークは入力層,中間層(1層),出力層の3層構造です.
重みの更新
下の図で説明していきます.
$l-1$ 層目の $i$ 番目のユニットから $l$ 層目の $j$ 番目のユニットへの重みを $w_{ji}^{(l)}$ とします.
また,$l-1$ 層目の $i$ 番目のユニットが保持している値を $u_{i}^{(l-1)}$ とします.
活性化関数としてシグモイド関数 $g(x)$ を用います.微分値が微分する前の値を使って求められるので便利です.
g(x) = \cfrac{1}{1 + \exp(-x)} \\
g'(x) = g(x)(1 - g(x))
2乗誤差 $E_n$ を目的関数とし,下の式で重みを更新することで最小化を図ります.$E_n$ は1サンプルによって生じる誤差で,この $E_n$ を使って重みの更新をする方法を確率的勾配降下法と呼びます.
確率的勾配降下法は,目的関数が更新されるごとに変化するため,局所解にトラップされにくくなるというメリットがあります.
$\epsilon$ は学習率と呼ばれ,学習の速度を決めるパラメータです.
偏微分の項をどのように計算するかがポイントとなります.
{{w}_{ji}^{(l)}}_{new} = {w_{ji}^{(l)}}_{old} - \epsilon\cfrac{\partial E_n}{\partial {w_{ji}^{(l)}}_{old}}
誤差逆伝播
では,$\cfrac{\partial E_n}{\partial w_{ji}^{(l)}}$ の求め方を説明していきます.出力層と中間層とで求め方が若干異なるので,分けて説明していきます.
< 出力層 >
教師を $t_j$ で表します.2乗誤差 $E_n$ を出力層の重み $w_{ji}^{(l)}$ で偏微分すると,
\begin{align}
\cfrac{\partial E_n}{\partial w_{ji}^{(l)}} &= \cfrac{\partial}{\partial w_{ji}^{(l)}}\cfrac{1}{2}(t_j - g(u_{j}^{(l)}))^{2} \\
&= (t_j - g(u_{j}^{(l)}))\cdot\cfrac{\partial}{\partial w_{ji}^{(l)}}(t_j - g(u_{j}^{(l)})) \\
&= (t_j - g(u_{j}^{(l)}))\cdot\cfrac{\partial}{\partial u_{j}^{(l)}}(t_j - g(u_{j}^{(l)}))\cdot\cfrac{\partial u_{j}^{(l)}}{\partial w_{ji}^{(l)}} \\
\\
&= (g(u_{j}^{(l)}) - t_j)\cdot g'(u_{j}^{(l)})\cdot g(u_{i}^{(l-1)}) \\
\\
&= (g(u_{j}^{(l)}) - t_j)g(u_{j}^{(l)})(1 - g(u_{j}^{(l)}))g(u_{i}^{(l-1)})
\end{align}
< 中間層 >
2乗誤差 $E_n$ を中間層の重み $w_{ji}^{(l)}$ で偏微分すると,
\begin{align}
\cfrac{\partial E_n}{\partial w_{ji}^{(l)}} &= \cfrac{\partial E_n}{\partial u_{j}^{(l)}}\cfrac{\partial u_{j}^{(l)}}{\partial w_{ji}^{(l)}} \\
\\
&= \delta_{j}^{(l)}\cfrac{\partial u_{j}^{(l)}}{\partial w_{ji}^{(l)}}
\end{align}
右辺第1項は,
\begin{align}
\delta_{j}^{(l)} &= \cfrac{\partial E_n}{\partial u_{j}^{(l)}} \\
&= \sum_{k}\cfrac{\partial E_n}{\partial u_{k}^{(l+1)}}\cfrac{\partial u_{k}^{(l+1)}}{\partial u_{j}^{(l)}} \\
\\
&= \sum_{k}\delta_{k}^{(l+1)}w_{kj}^{(l+1)}g'(u_{j}^{(l)}) \\
\\
&= \Bigl(\sum_{k}\delta_{k}^{(l+1)}w_{kj}^{(l+1)}\Bigr)g(u_{j}^{(l)})(1 - g(u_{j}^{(l)}))
\end{align}
右辺第2項は,
\begin{align}
\cfrac{\partial u_{j}^{(l)}}{\partial w_{ji}^{(l)}} &= g(u_{i}^{(l-1)})
\end{align}
以上より,
\begin{align}
\cfrac{\partial E_n}{\partial w_{ji}^{(l)}} &= \delta_{j}^{(l)}\cfrac{\partial u_{j}^{(l)}}{\partial w_{ji}^{(l)}} \\
\\
&= \Bigl(\sum_{k}\delta_{k}^{(l+1)}w_{kj}^{(l+1)}\Bigr)g(u_{j}^{(l)})(1 - g(u_{j}^{(l)}))g(u_{i}^{(l-1)})
\end{align}
中間層 $l$ の重みを更新するときは,その次の層 $l+1$ の $\delta_{k}^{(l+1)}$ が必要となります.
出力層での $\delta$ は,出力値と教師の差として求めることができ,これを出力層から入力層の方向に順番に伝播させることで,中間層の重みを更新します.これが誤差逆伝播と呼ばれる所以です.
XNOR
XNORは入力値が等しいとき $1$ を出力し,入力値が異なるとき $0$ を出力します.これは線形識別することができません.
$x_1$ | $x_2$ | $t$ |
---|---|---|
0 | 0 | 1 |
0 | 1 | 0 |
1 | 0 | 0 |
1 | 1 | 1 |
pythonでの実装
XNORを識別するニューラルネットワークを実装しました.
中間層の数は1つ,中間層のユニットの数は2つにしています.学習率 $\epsilon = 0.1$,モメンタムの係数 $\mu = 0.9$ としています.
(モメンタムは収束性能を向上させる方法の一つで,重みの修正量に,前回の重みの修正量に係数をかけたものを加算します.)
import numpy
import math
import random
from matplotlib import pyplot
class Neural:
# constructor
def __init__(self, n_input, n_hidden, n_output):
self.hidden_weight = numpy.random.random_sample((n_hidden, n_input + 1))
self.output_weight = numpy.random.random_sample((n_output, n_hidden + 1))
self.hidden_momentum = numpy.zeros((n_hidden, n_input + 1))
self.output_momentum = numpy.zeros((n_output, n_hidden + 1))
# public method
def train(self, X, T, epsilon, mu, epoch):
self.error = numpy.zeros(epoch)
N = X.shape[0]
for epo in range(epoch):
for i in range(N):
x = X[i, :]
t = T[i, :]
self.__update_weight(x, t, epsilon, mu)
self.error[epo] = self.__calc_error(X, T)
def predict(self, X):
N = X.shape[0]
C = numpy.zeros(N).astype('int')
Y = numpy.zeros((N, X.shape[1]))
for i in range(N):
x = X[i, :]
z, y = self.__forward(x)
Y[i] = y
C[i] = y.argmax()
return (C, Y)
def error_graph(self):
pyplot.ylim(0.0, 2.0)
pyplot.plot(numpy.arange(0, self.error.shape[0]), self.error)
pyplot.show()
# private method
def __sigmoid(self, arr):
return numpy.vectorize(lambda x: 1.0 / (1.0 + math.exp(-x)))(arr)
def __forward(self, x):
# z: output in hidden layer, y: output in output layer
z = self.__sigmoid(self.hidden_weight.dot(numpy.r_[numpy.array([1]), x]))
y = self.__sigmoid(self.output_weight.dot(numpy.r_[numpy.array([1]), z]))
return (z, y)
def __update_weight(self, x, t, epsilon, mu):
z, y = self.__forward(x)
# update output_weight
output_delta = (y - t) * y * (1.0 - y)
_output_weight = self.output_weight
self.output_weight -= epsilon * output_delta.reshape((-1, 1)) * numpy.r_[numpy.array([1]), z] - mu * self.output_momentum
self.output_momentum = self.output_weight - _output_weight
# update hidden_weight
hidden_delta = (self.output_weight[:, 1:].T.dot(output_delta)) * z * (1.0 - z)
_hidden_weight = self.hidden_weight
self.hidden_weight -= epsilon * hidden_delta.reshape((-1, 1)) * numpy.r_[numpy.array([1]), x]
self.hidden_momentum = self.hidden_weight - _hidden_weight
def __calc_error(self, X, T):
N = X.shape[0]
err = 0.0
for i in range(N):
x = X[i, :]
t = T[i, :]
z, y = self.__forward(x)
err += (y - t).dot((y - t).reshape((-1, 1))) / 2.0
return err
from neuralnetwork import *
if __name__ == '__main__':
X = numpy.array([[0, 0], [0, 1], [1, 0], [1, 1]])
T = numpy.array([[1, 0], [0, 1], [0, 1], [1, 0]])
N = X.shape[0] # number of data
input_size = X.shape[1]
hidden_size = 2
output_size = 2
epsilon = 0.1
mu = 0.9
epoch = 10000
nn = Neural(input_size, hidden_size, output_size)
nn.train(X, T, epsilon, mu, epoch)
nn.error_graph()
C, Y = nn.predict(X)
for i in range(N):
x = X[i, :]
y = Y[i, :]
c = C[i]
print x
print y
print c
print ""
結果
正しく識別できています.
$x_1$ | $x_2$ | $t$ | $y$ |
---|---|---|---|
0 | 0 | [ 1, 0 ] | [ 0.92598739, 0.07297757 ] |
0 | 1 | [ 0, 1 ] | [ 0.06824915, 0.93312514 ] |
1 | 0 | [ 0, 1 ] | [ 0.06828438, 0.93309010 ] |
1 | 1 | [ 1, 0 ] | [ 0.92610205, 0.07220633 ] |
誤差が減少していく様子をグラフにしました.横軸がエポック数,縦軸が誤差です.
おわりに
3層のニューラルネットワークを実装し,XNORを識別することができました.
学習のトリック(重みの初期値の決定方法,学習率の決定方法,AdaGrad,モメンタム)など勉強すべきことはまだまだありますが,一旦これで終わりにします.