【フレームワークを使用せず】ゼロから作る物体検出AIモデル
【No.11】ニューラルネットワークの実装(3層のNN構築-実装)
1. 本記事について
本記事は、これまでの内容(ニューラルネットワークの理解その1〜4)を踏まえ、ニューラルネットワークをpythonプログラムに落とし込んでいきます。
本記事はこれまで実装してきた処理を使用して、簡単なニューラルネットワーク(NN)を実装していきます。
(前回は、3層のNN構築-概要について解説しました。)
ニューラルネットワークの実装(入力層・Affine前半〜3層のNN構築-概要)を見てから、本記事を見ることをお勧めします。
注意事項として、本シリーズではpythonの基礎的な解説、numpyの基礎的な解説は割愛させていただきます。特に、numpy配列やshape等頻出なので、その辺の知識が不十分の場合は、そちらの習得を先に行うことをお勧めします。
本シリーズを進めていくにあたり、参考にさせていただく書籍があります。オライリー・ジャパンから出版されている「ゼロから作るDeep Learning ーPythonで学ぶディープラーニングの理論と実装」です。
本記事は、上記書籍を参考にさせていただいております。
2. NNの実装
前回迄実装してきた各処理を使って、簡単なニューラルネットワーク(以下NN)を構築し、実際に学習と推論を行ってみます。
これから行うことは、①NNの概要を決定(前回)、②NNをpythonで構築、③構築したNNで楽手と推論
です。今回は②を行います。
では、実際にNNをpythonで実装していきます。
使用するクラス
まず、前回迄に実装してきた4つのクラスを以下にまとめます。SGDは今回使用しないので省略します。
・Affine(中間層)・Leaky ReLU(活性化関数)
・loss(SSE)
・Momentum(重み更新)
import numpy as np
class Affine:
# 各値の初期化
def __init__(self, W, b):
self.W = W
self.b = b
self.x = None
self.dW = None
self.db = None
# 順伝播
def forward(self, x):
self.x = x # 順伝播の入力を保持
out = np.dot(x, self.W) + self.b
return out
# 逆伝播
def backward(self, dout):
dx = np.dot(dout, self.W.T) # δの計算
self.dW = np.dot(self.x.T, dout) # 重みの計算
self.db = dout # バイアスの計算
return dx
class LeakyReLU:
# 各値の初期化
def __init__(self, alpha):
self.x = None# 逆伝播の時に使う
self.alpha = alpha
# 順伝播
def forward(self, x):
self.x = x# 順伝播の入力を保持
out = np.where(x > 0, x, self.alpha*x)
return out
# 逆伝播
def backward(self, dout):
dx = np.where(self.x > 0, dout*1, dout*self.alpha)# δの計算
return dx
class loss():
# 各値の初期化
def __init__(self):
self.loss = None
self.y = None# 逆伝播の時に使う
self.t = None# 逆伝播の時に使う
# 順伝播
def forward(self, y, t):
self.y = y# 順伝播のyを保持
self.t = t# 教師データを保持
self.loss = np.sum((y-t)**2) / 2
return self.loss
# 逆伝播
def backward(self):
dx = self.y - self.t
return dx
class Momentum:
# 各値の初期化
def __init__(self, lr=0.01, momentum=0.9):
self.lr = lr
self.momentum = momentum# 摩擦
self.v = None# 次の更新時に使う
def update(self, params, grads):
# 初回時はv = 0に指定する
if self.v is None:
self.v = {}
for key, val in params.items():
self.v[key] = np.zeros_like(val)# 重みの配列と同一形状
# パラメータごとに値を更新
for key in params.keys():
self.v[key] = self.momentum * self.v[key] - self.lr * grads[key]
params[key] += self.v[key]
上記クラスを順次使用していきます。
重みとバイアスの値
重みとバイアスの値を初期化します。
import numpy as np
# 重みとバイアスの初期化
weight_init_std = 0.01
params = {}
params['W1'] = weight_init_std * np.random.randn(784, 500)
params['b1'] = np.zeros((1, 500))
params['W2'] = weight_init_std * np.random.randn(500, 100)
params['b2'] = np.zeros((1, 100))
params['W3'] = weight_init_std * np.random.randn(100, 3)
params['b3'] = np.zeros((1, 3))
for i in params:
print(f'key: {i}, shape of values: {params[i].shape}')
key: W1, shape of values: (784, 500)
key: b1, shape of values: (1, 500)
key: W2, shape of values: (500, 100)
key: b2, shape of values: (1, 100)
key: W3, shape of values: (100, 3)
key: b3, shape of values: (1, 3)
np.random.randnは平均0、標準偏差1の標準正規分布に従う乱数を生成します。
生成された乱数に対して0.01を掛けています。
重みの初期値に関しては、結構奥が深いので、後に記事にさせていただこうと思います。
とりあえずこのNNは一旦上記で進めます。
中間層の構築
中間層を構築していきます。以下の様にします。
import numpy as np
from collections import OrderedDict
layers = OrderedDict()
layers['Affine1'] = Affine(params['W1'], params['b1'])
layers['LeakyReLU1'] = LeakyReLU(0.1)
layers['Affine2'] = Affine(params['W2'], params['b2'])
layers['LeakyReLU2'] = LeakyReLU(0.1)
layers['Affine3'] = Affine(params['W3'], params['b3'])
for i in layers:
print(f'key: {i}, values: {layers[i]}')
key: Affine1, values: <__main__.Affine object at 0x7fab8f387040>
key: LeakyReLU1, values: <__main__.LeakyReLU object at 0x7fab8f387100>
key: Affine2, values: <__main__.Affine object at 0x7fab8f387160>
key: LeakyReLU2, values: <__main__.LeakyReLU object at 0x7fab8f3871c0>
key: Affine3, values: <__main__.Affine object at 0x7fab8f387220>
順伝播
順伝播、要は推論を行う関数を構築します。
import numpy as np
def predict(x):
for layer in layers.values():
x = layer.forward(x)
return x
入力をxとして、forで各中間層のforward関数に渡しています。
xを指定して結果をみてみます。一応各中間層のshapeもみてみます。
import numpy as np
def predict(x):
for layer in layers.values():
x = layer.forward(x)
print(f'layer: {layer.__class__.__name__}, x: {x.shape}')
return x
x = np.random.rand(1, 784)
x = predict(x)
print()
print(x)
layer: Affine, x: (1, 500)
layer: LeakyReLU, x: (1, 500)
layer: Affine, x: (1, 100)
layer: LeakyReLU, x: (1, 100)
layer: Affine, x: (1, 3)
[[0.0024374 0.0003439 0.00143628]]
ロスの計算
ロスの計算処理を構築していきます。といっても、ロス関数のクラスをほぼそのまま使うだけです。
import numpy as np
def loss_func(x, t):
return Loss.forward(x, t)
関数名をloss_funcにしました(class lossと被るのを防ぐため)。いまいちな名前なので、適当に変更をお願いいたします。
関数内のLossは、loss_func()の外側でインスタンス宣言します。
教師データ(t)を、適当にそれぞれ1、2、3にして、ロスを算出してみます。
import numpy as np
def loss_func(x, t):
return Loss.forward(x, t)
x = np.random.rand(1, 784)
t = np.array([1, 2, 3])
Loss = loss()# loss()をインスタンス化
x = predict(x)# 順伝播
loss_calc = loss_func(x, t)# ロス
print(loss_calc)
6.993676714274571
ロスが算出されました。この6.993...をなるべく0に近づける為に、これから重みの変化量を算出し、そこから重みの更新をしていくことになります。
逆伝播
逆伝播で重みの変化量(dW)を算出し、変数に格納していきます。
import numpy as np
def gradient():
dout = Loss.backward()
layerss = list(layers.values())
layerss.reverse()
for layer in layerss:
dout = layer.backward(dout)
grads = {}# 各重みの勾配を格納
grads['W1'] = layers['Affine1'].dW
grads['b1'] = layers['Affine1'].db
grads['W2'] = layers['Affine2'].dW
grads['b2'] = layers['Affine2'].db
grads['W3'] = layers['Affine3'].dW
grads['b3'] = layers['Affine3'].db
return grads
Loss.backward()のLossは先ほどインスタンス化したloss()です。
各中間層の関数を逆順にし、 backward()関数を実行します。
backward()関数実行によって、各dWとdbが算出されるので、それらの数値をgradsに格納していきます。
実際に処理を実行してみます。
import numpy as np
def gradient():
dout = Loss.backward()
layerss = list(layers.values())
layerss.reverse()
for layer in layerss:
dout = layer.backward(dout)
grads = {}# 各重みの勾配を格納
grads['W1'] = layers['Affine1'].dW
grads['b1'] = layers['Affine1'].db
grads['W2'] = layers['Affine2'].dW
grads['b2'] = layers['Affine2'].db
grads['W3'] = layers['Affine3'].dW
grads['b3'] = layers['Affine3'].db
return grads
x = np.random.rand(1, 784)
t = np.array([1, 2, 3])
Loss = loss()# loss()をインスタンス化
x = predict(x)# 順伝播
loss_calc = loss_func(x, t)# ロス
grads = gradient()# 逆伝播→勾配計算
for grad in grads:
print(f'grad: {grad}, shape: {grads[grad].shape}')
grad: W1, shape: (784, 500)
grad: b1, shape: (1, 500)
grad: W2, shape: (500, 100)
grad: b2, shape: (1, 100)
grad: W3, shape: (100, 3)
grad: b3, shape: (1, 3)
重みの更新(学習)
gradsを使って重みを更新していきます。今回は更新関数としてMomentumを使用します。
import numpy as np
def update_func(params, grads):
momentum.update(params, grads)
関数内のmomentumは、update_func()の外側でインスタンス宣言します。
実際に重み更新を実行してみます。
import numpy as np
def update_func(params, grads):
momentum.update(params, grads)
x = np.random.rand(1, 784)
t = np.array([1, 2, 3])
Loss = loss()# loss()をインスタンス化
momentum = Momentum()# Momentumをインスタンス化
x = predict(x)# 順伝播
loss_calc = loss_func(x, t)# ロス
grads = gradient()# 逆伝播→勾配計算
print('before params')
print(params['W1'][:5])
update_func(params, grads)# 重みの更新
print()
print('after params')
print(params['W1'][:5])
before params
[[-0.00392147 -0.0309712 -0.01221141 ... 0.00014648 -0.0189872
-0.0044456 ]
[ 0.00184462 0.01221464 0.00316603 ... 0.00059381 -0.00482479
0.00367312]
[-0.00134839 0.00961074 -0.00311371 ... -0.00329958 -0.00207449
-0.00275487]
[-0.00141299 0.00885205 -0.0060326 ... 0.00947855 -0.009482
0.00932961]
[-0.00425406 -0.00767978 0.00624049 ... 0.00038459 -0.00063964
-0.01193417]]
after params
[[-0.00392336 -0.03097432 -0.01221867 ... 0.00014639 -0.01898411
-0.00444544]
[ 0.00183779 0.01220338 0.00313978 ... 0.00059349 -0.0048136
0.00367371]
[-0.00135149 0.00960564 -0.0031256 ... -0.00329972 -0.00206942
-0.0027546 ]
[-0.00142168 0.00883773 -0.00606597 ... 0.00947814 -0.00946777
0.00933036]
[-0.00425487 -0.00768111 0.00623739 ... 0.00038455 -0.00063831
-0.0119341 ]]
重みが更新されています。
ネットワークを構築
最後に、これまでの入力〜重みの更新を使用し、NNクラスを実装してみます。
import numpy as np
from collections import OrderedDict
class NeuralNetwork1:
def __init__(self):
# 重み・バイアスの初期化
weight_init_std = 0.01
self.params = {}
self.grads = {}
self.params['W1'] = weight_init_std * np.random.randn(784, 500)
self.params['b1'] = np.zeros((1, 500))
self.params['W2'] = weight_init_std * np.random.randn(500, 100)
self.params['b2'] = np.zeros((1, 100))
self.params['W3'] = weight_init_std * np.random.randn(100, 3)
self.params['b3'] = np.zeros((1, 3))
# 中間層の構築
self.layers = OrderedDict()
self.layers['Affine1'] = Affine(self.params['W1'], self.params['b1'])
self.layers['LeakyReLU1'] = LeakyReLU(0.1)
self.layers['Affine2'] = Affine(self.params['W2'], self.params['b2'])
self.layers['LeakyReLU2'] = LeakyReLU(0.1)
self.layers['Affine3'] = Affine(self.params['W3'], self.params['b3'])
self.loss = loss()# ロス関数
self.update = Momentum()# 更新関数
def predict(self, x):
for layer in self.layers.values():
x = layer.forward(x)
return x
def loss_func(self, x, t):
return self.loss.forward(x, t)
def gradient(self):
dout = self.loss.backward()
layers = list(self.layers.values())
layers.reverse()
for layer in layers:
dout = layer.backward(dout)
self.grads['W1'] = self.layers['Affine1'].dW
self.grads['b1'] = self.layers['Affine1'].db
self.grads['W2'] = self.layers['Affine2'].dW
self.grads['b2'] = self.layers['Affine2'].db
self.grads['W3'] = self.layers['Affine3'].dW
self.grads['b3'] = self.layers['Affine3'].db
return self.grads
def update_func(self):
self.update.update(self.params, self.grads)
実際に入力して処理を実行してみます。
import numpy as np
from collections import OrderedDict
class NeuralNetwork1:
def __init__(self):
# 重み・バイアスの初期化
weight_init_std = 0.01
self.params = {}
self.grads = {}
self.params['W1'] = weight_init_std * np.random.randn(784, 500)
self.params['b1'] = np.zeros((1, 500))
self.params['W2'] = weight_init_std * np.random.randn(500, 100)
self.params['b2'] = np.zeros((1, 100))
self.params['W3'] = weight_init_std * np.random.randn(100, 3)
self.params['b3'] = np.zeros((1, 3))
# 中間層の構築
self.layers = OrderedDict()
self.layers['Affine1'] = Affine(self.params['W1'], self.params['b1'])
self.layers['LeakyReLU1'] = LeakyReLU(0.1)
self.layers['Affine2'] = Affine(self.params['W2'], self.params['b2'])
self.layers['LeakyReLU2'] = LeakyReLU(0.1)
self.layers['Affine3'] = Affine(self.params['W3'], self.params['b3'])
self.loss = loss()# ロス関数
self.update = Momentum()# 更新関数
def predict(self, x):
for layer in self.layers.values():
x = layer.forward(x)
return x
def loss_func(self, x, t):
return self.loss.forward(x, t)
def gradient(self):
dout = self.loss.backward()
layers = list(self.layers.values())
layers.reverse()
for layer in layers:
dout = layer.backward(dout)
self.grads['W1'] = self.layers['Affine1'].dW
self.grads['b1'] = self.layers['Affine1'].db
self.grads['W2'] = self.layers['Affine2'].dW
self.grads['b2'] = self.layers['Affine2'].db
self.grads['W3'] = self.layers['Affine3'].dW
self.grads['b3'] = self.layers['Affine3'].db
return self.grads
def update_func(self):
self.update.update(self.params, self.grads)
x = np.random.rand(1, 784)
t = np.array([1, 2, 3])
nn = NeuralNetwork1()# ネットワークをインスタンス化
y = nn.predict(x)# 順伝播
loss_calc = nn.loss_func(y, t)# ロス
grads = nn.gradient()# 逆伝播→勾配計算
print('before params')
print(nn.params['W1'][:5])
nn.update_func()# 重みの更新
print()
print('after params')
print(nn.params['W1'][:5])
before params
[[-0.00070941 0.00989282 -0.00737298 ... 0.00100678 -0.00224317
-0.00253225]
[ 0.00559211 0.01140472 -0.01884446 ... 0.006631 0.00107669
-0.00779534]
[ 0.00549107 0.00580318 -0.00859119 ... -0.00111083 -0.01898
0.0102331 ]
[-0.01654702 -0.00496326 -0.00104003 ... -0.0066198 0.0016084
0.00713683]
[ 0.00904293 0.01121062 0.0080445 ... 0.0268196 -0.01556447
0.00033156]]
after params
[[-0.00071018 0.00989378 -0.00737459 ... 0.00100624 -0.00224486
-0.00252682]
[ 0.0055919 0.01140498 -0.01884489 ... 0.00663085 0.00107623
-0.00779388]
[ 0.00549029 0.00580415 -0.00859282 ... -0.00111139 -0.0189817
0.01023861]
[-0.0165481 -0.00496194 -0.00104227 ... -0.00662056 0.00160605
0.00714441]
[ 0.00904189 0.0112119 0.00804234 ... 0.02681886 -0.01556674
0.00033887]]
重みが更新されていきます。
今は入力が適当な乱数ですが、これを画像データにして複数枚入力してあげれば、それらに最適化された重み更新が実行されるという形です。
次回は実際に画像データを用意して学習し、画像認識の推論を行ってみます。
3. まとめ
今回はニューラルネットワークの実装(3層のNN構築-実装)について解説しました。
次回も是非みてみてください!
4. 参考文献
斎藤康毅 著 「ゼロから作るDeep Learning ーPythonで学ぶディープラーニングの理論と実装」
Link
Search