3日目は、PythonでつくったニューラルネットワークをChainerで変換することで、Chainerのファンクションを理解していくことを勉強します。
参考にしたのは、このページです。非常に勉強になります。
(本当は、ページそのものを貼り付けたいのですが、Markdownでの記述方法がわからないため、単純にリンクとしています。)
ネットワークは、単純に前の記事と同じく、入力層、隠れ層、出力層が3層のネットワークです。ニューラルネットワークとしての説明は、上記のページにて参考にしてください。
Numpyも理解していないところもあるため、コマンドの理解もふくめてみます。
まずは、Chainerを使わずにNumpyでこのネットワークを実装していきます。
# -*- coding: utf-8 -*-
import numpy as np #numpyをインポート
EPOCHS = 300
M = 64 #次元数
N_I = 1000 #入力層のユニット数は1000
N_H = 100 #隠れ層のユニット数は100
N_O = 10 #出力層のユニット数は10
LEARNING_RATE = 1.0e-04 #学習レートは一般的な値。この値を小さくすると精度はあがるが、計算時間が大きくなる。
# 乱数生成器を初期化し、同じ順の乱数は同じ値になるようにする。
np.random.seed(1)
# 入力X、出力Y、重み付けWに値を割り当てる。
X = np.random.randn(M, N_I).astype(np.float32) #M行、N_I列の行列
Y = np.random.randn(M, N_O).astype(np.float32)
W1 = np.random.randn(N_I, N_H).astype(np.float32)
W2 = np.random.randn(N_H, N_O).astype(np.float32)
numpyの理解のためにも、それぞれの関数を調べて理解し、それを記述しています。
例えば、np.random.seed(1)にて、乱数を初期化し、の意味がわからなかったのため、
np.random.seed(1)
X = np.random.randn(2,3).astype(np.floas32)
print(X)
X = np.random.randn(2,3).astype(np.floas32)
print(X)
と各コマンドを実行し、その意味を理解しました。
>>> np.random.seed(1)
>>> X=np.random.randn(2, 3).astype(np.float32)
>>> print(X)
[[ 1.62434542 -0.61175638 -0.52817178]
[-1.0729686 0.86540765 -2.30153871]]
>>> X=np.random.randn(2, 3).astype(np.float32)
>>> print(X)
[[ 1.74481177 -0.76120692 0.31903911]
[-0.24937038 1.4621079 -2.06014061]]
>>> np.random.seed(1)
>>> X=np.random.randn(2, 3).astype(np.float32)
>>> print(X)
[[ 1.62434542 -0.61175638 -0.52817178]
[-1.0729686 0.86540765 -2.30153871]]
これをみると、np.random.seed(1)を再び実行することで、Xの値がはいじめに実行したときと同じ値になっているのがわかります。
つまり、乱数ではるが、その組み合わせと順はつねに同じ、という入力を生成することができたとわかります。
次に、順方向伝搬計算、誤差計算、逆方向伝搬計算をおこない、重み付けのWを更新してモデルを構築します。
def sample_1():
# 設定したX,Yの入力、出力を変数、x,yとしてセットする。
x = X
y = Y
# 重み付けWも、変数w1,w2としてセットする。
w1 = W1
w2 = W2
y_size = np.float32(M * N_O) #出力のサイズは、M☓N_0のサイズであることを設定する。Cahinerはこれをセットすることが必要。
#EPOCHS数まで繰り返し学習をさせる。
for t in range(EPOCHS):
# 順方向のニューラルネットワークの式を定義
h = x.dot(w1) #隠れ層は、入力xと、主見つけW1の積
h_r = np.maximum(h, 0) #隠れ層の各値の0以上の場合はそのまま、0以下は0にする。ReLUの活性化関数
y_p = h_r.dot(w2) #隠れ層の行列とW2の積で、出力y_pを求める。
ここで、
h_r = np.maximum(h, 0)
の意味がわからなかったのですが、これは隠れ層の活性化関数ReLUを示していることが、下記の試し計算からわかりました。
>>> np.random.seed(1)
>>> X=np.random.randn(2, 3).astype(np.float32)
>>> Y=np.random.randn(2,3).astype(np.float32)
>>> W1=np.random.randn(3,2).astype(np.float32)
>>> W2=np.random.randn(2,3).astype(np.float32)
>>> h=X.dot(W1)
>>> print(h)
[[-1.12623584 0.51268864]
[ 1.72396648 1.48064899]]
>>> h_r=np.maximum(h,0)
>>> print(h_r)
[[ 0. 0.51268864]
[ 1.72396648 1.48064899]]
>>> y_p=h_r.dot(W2)
>>> print(y_p)
[[ 0.58688682 0.4622353 0.25762314]
[ 1.76770902 2.33969331 -1.15341294]]
ここで、h_r=np.maximum(h,0)を実行すると、1行1列目の-1.12623584が0になっており、ほかはそのままの値が維持されています。ReLUですね。
次に誤差の計算です。
# 二乗差の平方根をロスとして計算する。y_sizeで割るのは規格化のため。
loss = np.square(y_p - y).sum() / y_size
print(loss)
これも、実際の値を代入して式を理解します。
>>> Y_size=np.float32(2*2)
>>> loss = np.square(y_p - Y)/Y_size
>>> print(loss)
[[ 3.35197508e-01 3.74202698e-01 9.42980347e-04]
[ 1.01715231e+00 1.92539036e-01 2.05538765e-01]]
Y_sizeを定義していなかったため、まずはこれを定義します。
そのあと、lossを計算しprintで出力しました。
y_pとYの二乗の差の平方根をY_sizeで除して規格化しています。
一部はすでにかなり小さい値になっていますが、一以上のものもあり、まだ答えと現状のパラメータでは差(loss)が大きいことがわかりました。
次に、これを逆伝搬して、重み付けWの値を更新し、lossがどのように変化するのか確認します。誤差逆伝搬法。
式そのものの導出は、参考にしているページを見てください。
>>> grad_y_p=2.0 * (y_p-Y)/Y_size
>>> grad_w2 = h_r.T.dot(grad_y_p)
>>> print(grad_y_p)
[[-0.57896245 0.6117211 -0.03070799]
[ 1.00853968 0.43879271 0.45336384]]
>>> print(grad_w2)
[[ 1.73868859 0.75646394 0.78158408]
[ 1.19646585 0.96332037 0.65552908]]
これにより、思いつけW2の微分を求めることができました。
どうように、導いた式をもちいてW1の微分も求めます。
>>> grad_h_r= grad_y_p.dot(W2.T)
>>> grad_h = grad_h_r
>>> grad_h[h < 0] - 0
array([ 0.36587802], dtype=float32)
>>> grad_w1=X.T.dot(grad_h)
>>> print(grad_w1)
[[ 0.80962664 -2.11339641]
[-0.39749098 1.6161139 ]
[ 0.26860714 -4.02506113]]
これで、W1とW2のそれぞれの微分値(誤差の傾き)を求めました。この値に、学習率をかけて新しいW1とW2をセットします。
これによって、どれだけ誤差が小さくなったのか、確認してみます。
>>> W1 -=0.0001 *grad_w1
>>> W2 -=0.0001 *grad_w2
>>> h=X.dot(W1)
>>> h_r=np.maximum(h,0)
>>> y_p=h_r.dot(W2)
>>> loss = np.square(y_p - Y)/Y_size
>>> print(loss)
[[ 3.34928960e-01 3.74425739e-01 9.36939847e-04]
[ 1.01370442e+00 1.91491380e-01 2.04662815e-01]]
ここでは、学習率を0.0001としています。
前にもとめた下記のlossと比較すると、2行一列目の値が若干小さくなっています。それ以外は大きな変化がありません。
1回目の学習時のloss
[[ 3.35197508e-01 3.74202698e-01 9.42980347e-04]
[ 1.01715231e+00 1.92539036e-01 2.05538765e-01]]
どこまで下がるのか、コマンドを繰り返して確認しようと思いましたが、それを機械(パソコン)に任せないと、どこかで間違えそうなので、下記のプログラムを実行しました。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import numpy as np
EPOCHS = 300
M = 2
N_I = 3
N_H = 2
N_O = 3
LEARNING_RATE = 1.0e-04
# set a specified seed to random value generator in order to reproduce the same results
np.random.seed(1)
X = np.random.randn(M, N_I).astype(np.float32)
Y = np.random.randn(M, N_O).astype(np.float32)
W1 = np.random.randn(N_I, N_H).astype(np.float32)
W2 = np.random.randn(N_H, N_O).astype(np.float32)
def sample_1():
# create random input and output data
x = X
y = Y
# randomly initialize weights
w1 = W1
w2 = W2
y_size = np.float32(M * N_O)
for t in range(EPOCHS):
# forward pass
h = x.dot(w1)
h_r = np.maximum(h, 0)
y_p = h_r.dot(w2)
# compute mean squared error and print loss
loss = np.square(y_p - y).sum() / y_size
print(loss)
# backward pass: compute gradients of loss with respect to w2
grad_y_p = 2.0 * (y_p - y) / y_size
grad_w2 = h_r.T.dot(grad_y_p)
# backward pass: compute gradients of loss with respect to w1
grad_h_r = grad_y_p.dot(w2.T)
grad_h = grad_h_r
grad_h[h < 0] = 0
grad_w1 = x.T.dot(grad_h)
# update weights
w1 -= LEARNING_RATE * grad_w1
w2 -= LEARNING_RATE * grad_w2
if __name__ == '__main__':
sample_1()
これを実行すると、lossが1.41だったのが、300回めの計算では1.094まで低下することが確認できました。
折角なので、行列のそれぞれの値がどうであったのか、lossの計算をいかに修正して再実行してみました。
loss = np.square(y_p - y)/ y_size
sum()をなくして、行列が維持できる形にしています。
1回目の結果
[[ 2.23465011e-01 2.49468461e-01 6.28653564e-04]
[ 6.78101540e-01 1.28359362e-01 1.37025848e-01]]
300回目の結果
[[ 2.09887639e-01 2.60629386e-01 3.69301037e-04]
[ 4.68264222e-01 6.91347048e-02 8.65766481e-02]]
各行列ともにlossが小さくなくなっているのがわかりません。でも、その低減率は行列の要素によって異なりますね。2行1列目の低下は少しですが、2行2列目は半分近く下がっていますし。
数字でみると、どういうものか理解しやすいですし、ここからパラメータを変えることによって、lossがどのように変化するのか、その(肌)感覚がわかります。エンジニアとしてはとても大事な部分で、私はこれを知りたくて、ハードエンジニアですが、ソフトのこの部分を独学で学んでいます。
次に、これをChainerに変換し実装してみます。
4日目 ニューラルネットワークをChainerに変換しながら学ぶ(その2)