今話題のニューラルネットワーク深層学習をフレームワークなしで実装してみた。層ごとに考察を交えながら実装した。パイソン標準ライブラリーnumpyで頑張って実装したので、時間がある方は数式と見比べてみるとさらに理解が進むかも。
#入力層、中間層
ニューラルネットワークでは、入力層で説明変数を入力し、中間層を経ながらデータに変化を加え、出力層で出力をする。具体的には、重みとバイアスという値を使ってデータに変化を加える。特に4層以上のものを深層ニューラルネットワークと呼ぶ。
import numpy as np
from common import functions
w = np.array([[0, 3], [1,2]])
x = np.array([2,3])
b = 0.5
u = np.dot(x,w) + b
z = functions.relu(u)
print(u)
##考察
ニューラルネットワークとは複層に拡張したロジスティクス回帰である。
また、各中間層に接続するシナプスの重みが極端に大きくなることを防ぐために、L1、L2正則化という手法をとることがある。
#活性関数
活性関数とは、前層から重みとバイアスを経て受け渡されたデータに非線形な変化を加える関数である。非線形な活性関数を用いることで多層ニューラルネットワークを実現できることができる。逆に、恒等関数を用いた多層ニューラルネットワークは単層に近似できるため、表現力に乏しい。
パイソンでの実装
RELU関数
def relu(x):
return
np.maximum(0,x)
sigmoid関数
def sigmoid(x):
return 1/(1+np.exp(-x))
step関数
def step(x):
if x > 0:
return 1
else:
return 0
##考察
活性化関数には様々な種類があるが、非線形でなければならないという制約がある。しかし、非線形な関数は、ある領域で出力が広域に渡って0に近くなるという、勾配消失問題が存在し、現在ではrelu関数が広く使われている。
#出力層
出力層では、中間層での計算結果をもとに最終的な出力を求める。また、出力層では、中間層とは違う活性関数、ソフトマックス関数などを用いる。また、誤差関数を用いて予測の良さをフィードバックし、モデルを学習させていく。
def init_network():
print("##### ネットワークの初期化 #####")
network = {}
network['W1'] = np.array([
[0.1, 0.3, 0.5],
[0.2, 0.4, 0.6]
])
network['W2'] = np.array([
[0.1, 0.4],
[0.2, 0.5],
[0.3, 0.6]
])
network['W3'] = np.array([
[0.1, 0.3],
[0.2, 0.4]
])
network['b1'] = np.array([0.1, 0.2, 0.3])
network['b2'] = np.array([0.1, 0.2])
network['b3'] = np.array([1, 2])
return network
def forward(network, x):
W1, W2, W3 = network['W1'], network['W2'], network['W3']
b1, b2, b3 = network['b1'], network['b2'], network['b3']
u1 = np.dot(x, W1) + b1
z1 = functions.relu(u1)
u2 = np.dot(z1, W2) + b2
z2 = functions.relu(u2)
u3 = np.dot(z2, W3) + b3
y = u3
return y, z1, z2
x = np.array([1., 2.])
print_vec("入力", x)
network = init_network()
y, z1, z2 = forward(network, x)
##考察
出力層で用いられる活性関数は、中間層での活性化関数とは様子が異なり、例えば、数値分類を行うソフトマックス関数など、出力用に値の形を調節する。
#勾配降下法
勾配降下法はニューラルネットワークに用いられる最適化関数であり、誤差の微分を重み、バイアスパラメーターから引いていくことによって膨大な数の変数を効率よく近似していくことができる。また、学習の際には学習率という学習を抑制するパラメータを用いて学習をコントロールする。
def f(x):
y = 3 * x[0] + 2 * x[1]
return y
def init_network():
network = {}
nodesNum = 10
network['W1'] = np.random.randn(2, nodesNum)
network['W2'] = np.random.randn(nodesNum)
network['b1'] = np.random.randn(nodesNum)
network['b2'] = np.random.randn()
return network
def forward(network, x):
W1, W2 = network['W1'], network['W2']
b1, b2 = network['b1'], network['b2']
u1 = np.dot(x, W1) + b1
z1 = functions.relu(u1)
u2 = np.dot(z1, W2) + b2
y = u2
return z1, y
##考察
学習率の設計は職人技と言われていて、多すぎると勾配向上法になり学習が失敗し、少なすぎても局所解にはまったり、学習に時間がかかったりするため、効率の良いパラメーター設計が重要である。
#誤差逆伝播法
誤差逆伝播法は勾配降下法でのパラメーターを効率よく求めていく方法で、誤差の微分値を保存し前層に伝えていくことから誤差逆伝播法と呼ばれている。また、中間層の活性関数は、誤差逆伝播法のために、微分のしやすさで選ばれることもある。
実装:前項の勾配降下法に続けて
def backward(x, d, z1, y):
grad = {}
W1, W2 = network['W1'], network['W2']
b1, b2 = network['b1'], network['b2']
delta2 = functions.d_mean_squared_error(d, y)
grad['b2'] = np.sum(delta2, axis=0)
grad['W2'] = np.dot(z1.T, delta2)
delta1 = np.dot(delta2, W2.T) * functions.d_sigmoid(z1)
delta1 = delta1[np.newaxis, :]
grad['b1'] = np.sum(delta1, axis=0)
x = x[np.newaxis, :]
grad['W1'] = np.dot(x.T, delta1)
return grad
data_sets_size = 100000
data_sets = [0 for i in range(data_sets_size)]
for i in range(data_sets_size):
data_sets[i] = {}
data_sets[i]['x'] = np.random.rand(2)
data_sets[i]['d'] = f(data_sets[i]['x'])
losses = []
learning_rate = 0.07
epoch = 1000
network = init_network()
random_datasets = np.random.choice(data_sets, epoch)
for dataset in random_datasets:
x, d = dataset['x'], dataset['d']
z1, y = forward(network, x)
grad = backward(x, d, z1, y)
for key in ('W1', 'W2', 'b1', 'b2'):
network[key] -= learning_rate * grad[key]
# 誤差
loss = functions.mean_squared_error(d, y)
losses.append(loss)
lists = range(epoch)
plt.plot(lists, losses, '.')
# グラフの表示
plt.show()
##考察
ニューラルネットワークでは、調節しないといけないパラメーターが膨大なため、解析的に調節する代わりに誤差逆伝播法を行う。誤差逆伝播法では、前層で用いた微分値を再利用できるため、最低限の計算量で計算することが可能である。