はじめに
『ゼロから作るDeep Learning 3』の初学者向け攻略ノートです。『ゼロつく3』の学習の補助となるように適宜解説を加えていきます。本と一緒に読んでください。
本で省略されているクラスや関数の内部の処理を1つずつ解説していきます。
この記事は、主にステップ43「ニューラルネットワーク」を補足する内容です。
DeZeroのモジュール利用して簡単なニューラルネットワークを実装します。
【前ステップの内容】
【他の記事一覧】
【この記事の内容】
・簡単なニューラルネットワークの実装
DeZeroのモジュールを使って、2層のニューラルネットワークを実装し、勾配降下法によりパラメータを推定します。
・実装する前に
この例では、ニューラルネットワークにSigmoid関数を利用します。Sigmoid
クラスまたはsigmoid_simple()
関数の実装には、指数関数のクラスExp
を利用します。
functions.py
に実装されているExp
とSigmoid
を見ると、ステップ52「GPU対応」で登場する内容が含まれています。(それとも既にExp
を実装しましたっけ?私が忘れてるだけならお騒がせしました…)
先に52.1、52.2、52.4節を読んでcuda
モジュールを実装するか、少し手を加える必要があります。cuda.***()
とxp.***()
を含む行をコメントアウトして(または消して)、xp.
をnp.
に置き換えた(関数名の部分はそのまま)処理を追加します。
これで動作するはずです。
次のライブラリを利用します。
# 利用するライブラリ import numpy as np import matplotlib.pyplot as plt import matplotlib.animation as animation
animation
モジュールは、アニメーション(gif画像)を作成するためのモジュールです。最後に推移をアニメーションで確認するのに利用します。
また、これまでに実装済したクラスを利用します。dezero
フォルダの親フォルダまでのパスをsys.path.append()
に指定します。
# 実装済みモジュールの読込用設定 import sys sys.path.append('..') # 利用する実装済みモジュール from dezero import Variable import dezero.functions as F
・データセットの作成
まずは、$N$個のトイ・データセット$\mathbf{x},\ \mathbf{y}$を作成します。
この例では、入力$x_n$を0から1の一様乱数とします。また、出力$y_n$を$\sin(2 \pi x_n)$に更に0から1の一様乱数を加えたものとします。
# データを作成 x = np.random.rand(100, 1) y = np.sin(2 * np.pi * x) + np.random.rand(100, 1) # データを確認 print(x[:5]) print(x.shape) print(y[:5]) print(y.shape)
[[0.63555237]
[0.81716768]
[0.06558533]
[0.65312297]
[0.50278928]]
(100, 1)
[[-0.7335832 ]
[-0.82478956]
[ 1.19801536]
[ 0.10725132]
[ 0.91577637]]
(100, 1)
0
から1
の一様乱数はnp.random.rand()
で生成できます。第1引数に行数、第2引数に列数を指定します。sin関数は、np.sin()
で計算できます。円周率$\pi$は、np.pi
で使えます。
作成したデータセットを散布図で確認しておきましょう。
# データの散布図を作成 plt.figure(figsize=(10, 7.5)) plt.scatter(x, y) # 散布図 plt.xlabel('x') # x軸ラベル plt.ylabel('y') # y軸ラベル plt.suptitle('dataset', fontsize=20) # 図全体のタイトル plt.title('N=' + str(len(x)), loc='left') # タイトル plt.grid() # グリッド線 plt.show()
非線形なデータセットになるのを確認できます。
・ニューラルネットワークの実装
2層のニューラルネットワークを作成して、勾配降下法により学習を行います。
入力x
と出力y
の次元数が1
なので、入力層I
と出力層O
の次元数も1
です。
各層のパラメータを作成します。重みの初期値は、np.random.randn()
を使って標準正規分布(平均0・標準偏差1の正規分布)に従う乱数を生成して、そこに0.01
を掛けることで標準偏差を0.01
に調整します。重みの初期値については、「6.2:重みの初期値【ゼロつく1のノート(実装)】 - からっぽのしょこ」を参照してください(ただし、応用的な内容です)。
重みとバイアス、損失の推移を確認するために、学習を行う(値を更新する)ごとに値をリストtrace_*
に格納していきます。各パラメータの形状によって、値の取り出し方が異なります。
2層のニューラルネットワークを関数predict()
として作成します。(引数にパラメータを指定できるように変更したのは、アニメーションを作成する際に必要になるためです。不要でしたら本の通りに実装してください。)
繰り返し回数をiters
、学習率をlr
として値を指定します。
損失の計算には平均2乗誤差の関数F.mean_squared_error()
を利用します。勾配降下法については、「ステップ29:勾配降下法とニュートン法の比較【ゼロつく3のノート(数学)】 - からっぽのしょこ」を参照してください。
# 各層の次元数を指定 I = 1 # 入力層:(固定) H = 10 # 隠れ層 O = 1 # 出力層:(固定) # パラメータを初期化 W1 = Variable(0.01 * np.random.randn(I, H)) # 1層の重み b1 = Variable(np.zeros(H)) # 1層のバイアス W2 = Variable(0.01 * np.random.randn(H, O)) # 2層の重み b2 = Variable(np.zeros(O)) # 2層のバイアス # 2層のニューラルネットを作成 def predict(x, W1, b1, W2, b2): # 第1層 y = F.linear(x, W1, b1) # 線形変換 y = F.sigmoid(y) # 活性化 # 第2層 y = F.linear(y, W2, b2) # 線形変換 return y # 学習率を指定 lr = 0.2 # 試行回数を指定 iters = 10000 # 推移の確認用のリストを初期化 trace_W1 = [W1.data.flatten()] trace_b1 = [b1.data.copy()] trace_W2 = [W2.data.flatten()] trace_b2 = [b2.data.item()] trace_L = [] # 勾配降下法 for i in range(iters): # 予測値を計算 y_pred = predict(x, W1, b1, W2, b2) # 損失を計算 loss = F.mean_squared_error(y, y_pred) # 勾配を初期化 W1.cleargrad() b1.cleargrad() W2.cleargrad() b2.cleargrad() # 勾配を計算 loss.backward() # パラメータを更新 W1.data -= lr * W1.grad.data b1.data -= lr * b1.grad.data W2.data -= lr * W2.grad.data b2.data -= lr * b2.grad.data # パラメータを記録 trace_W1.append(W1.data.flatten()) trace_b1.append(b1.data.copy()) trace_W2.append(W2.data.flatten()) trace_b2.append(b2.data.item()) trace_L.append(loss.data.item()) # 指定した回数ごとに結果を表示 if i % 1000 == 0: # iが1000で割り切れる場合 print('iter:' + str(i) + ', loss=' + str(loss.data))
iter:0, loss=0.8903493620091869
iter:1000, loss=0.26039555467856623
iter:2000, loss=0.25759254041755164
iter:3000, loss=0.25335787299751145
iter:4000, loss=0.24143476662898156
iter:5000, loss=0.21027932823833684
iter:6000, loss=0.1508995987745554
iter:7000, loss=0.10016149619986767
iter:8000, loss=0.09434767515303094
iter:9000, loss=0.0942130433323213
試行(学習)を繰り返すごとに損失の値が小さくなっています。
最後に更新したパラメータを使って損失を確認しておきます。
# 平均2乗誤差を計算 loss = F.mean_squared_error(y, predict(x, W1, b1, W2, b2)) trace_L.append(loss.data.item()) print(loss.data)
0.09338799286853623
0に近い値になっていることから、実際の値と予測値との誤差が小さくなっているのが分かります。(ここで損失を計算した本当の目的は、trace_W, trace_b
とtrace_L
の要素数を合わせるためです。アニメーション作成時に面倒になるので。)
・推定結果の確認
パラメータの推定値を用いて予測値を計算します。
# 作図用のx軸の値を作成 x_line = np.arange(0.0, 1.01, 0.01) print(x_line[:5]) # 作図用のxに対する予測値を計算 y_line = predict(x_line.reshape((len(x_line), 1)), W1, b1, W2, b2).data.flatten() print(y_line[:5])
[0. 0.01 0.02 0.03 0.04]
[0.69968214 0.74067538 0.78139102 0.82178173 0.86179639]
データx
の最小値から最大値までを範囲として、作図用の$x$の値を作成します。作成した値ごとに予測値$\hat{y}$を計算します。
predict()
の第1引数に渡すx_line
を縦に要素を並べた2次元配列に変換しています。また、出力を1次元配列に変換しています。
予測値を散布図と重ねて作図します。
# 予測値を作図 plt.figure(figsize=(10, 7.5)) plt.scatter(x.flatten(), y.flatten(), label='data') # データ plt.plot(x_line, y_line, color='red', label='predict') # 予測曲線 plt.xlabel('x') # x軸ラベル plt.ylabel('y') # y軸ラベル plt.suptitle('Neural Network', fontsize=20) # 図全体のタイトル plt.title('iter:' + str(iters) + ', loss=' + str(np.round(loss.data, 5)) + ', lr=' + str(lr) + ', N=' + str(len(x)), loc='left') # タイトル plt.legend() # 凡例 plt.grid() # グリッド線 plt.show()
データへの当てはまりのよい曲線を引けています。
損失(平均2乗誤差)の推移を確認します。
# 平均2乗誤差の推移を作図 plt.figure(figsize=(10, 7.5)) plt.plot(np.arange(len(trace_L)), trace_L, label='loss') plt.xlabel('iteration') # x軸ラベル plt.ylabel('value') # y軸ラベル plt.suptitle('Neural Network', fontsize=20) # 図全体のタイトル plt.title('N=' + str(len(x)), loc='left') # タイトル plt.legend() # 凡例 plt.grid() # グリッド線 #plt.xlim(9500, 10000) # x軸の表示範囲 #plt.ylim(0.0, 0.2) # y軸の表示範囲 plt.show()
試行回数が増えるにしたがって損失が下がっています。(何だこの不整脈は??)
パラメータの推移も確認しておきましょう。
・コード(クリックで展開)
リストに格納した試行ごとのパラメータの値をNumPy配列に変換した上で、次元ごとにプロットしています。
# NumPy配列に変換 trace_W1_arr = np.array(trace_W1).T # 第1層の重みの推移を作図 plt.figure(figsize=(10, 7.5)) for h in range(H): plt.plot(np.arange(iters + 1), trace_W1_arr[h], label='W1,' + str(h + 1)) plt.xlabel('iteration') # x軸ラベル plt.ylabel('value') # y軸ラベル plt.suptitle('Neural Network', fontsize=20) # 図全体のタイトル plt.title('lr=' + str(lr) + ', N=' + str(len(x)), loc='left') # タイトル plt.legend() # 凡例 plt.grid() # グリッド線 plt.show()
# NumPy配列に変換 trace_b1_arr = np.array(trace_b1).T # 第1層のバイアスの推移を作図 plt.figure(figsize=(10, 7.5)) for h in range(H): plt.plot(np.arange(iters + 1), trace_b1_arr[h], label='b1,' + str(h + 1)) plt.xlabel('iteration') # x軸ラベル plt.ylabel('value') # y軸ラベル plt.suptitle('Neural Network', fontsize=20) # 図全体のタイトル plt.title('lr=' + str(lr) + ', N=' + str(len(x)), loc='left') # タイトル plt.legend() # 凡例 plt.grid() # グリッド線 plt.show()
# NumPy配列に変換 trace_W2_arr = np.array(trace_W2).T # 第2層の重みの推移を作図 plt.figure(figsize=(10, 7.5)) for h in range(H): plt.plot(np.arange(iters + 1), trace_W2_arr[h], label='W2,' + str(h + 1)) plt.xlabel('iteration') # x軸ラベル plt.ylabel('value') # y軸ラベル plt.suptitle('Neural Network', fontsize=20) # 図全体のタイトル plt.title('lr=' + str(lr) + ', N=' + str(len(x)), loc='left') # タイトル plt.legend() # 凡例 plt.grid() # グリッド線 plt.show()
# 第2層のバイアスの推移を作図 plt.figure(figsize=(10, 7.5)) plt.plot(np.arange(iters + 1), np.array(trace_b2), label='b2') plt.xlabel('iteration') # x軸ラベル plt.ylabel('value') # y軸ラベル plt.suptitle('Neural Network', fontsize=20) # 図全体のタイトル plt.title('lr=' + str(lr) + ', N=' + str(len(x)), loc='left') # タイトル plt.legend() # 凡例 plt.grid() # グリッド線 plt.show()
最後に、予測値の曲線の推移をアニメーションで確認します。
・コード(クリックで展開)
# フレーム数を指定 frame_num = 100 # 画像サイズを指定 fig = plt.figure(figsize=(10, 7.5)) # 縦ベクトルに変換 x_line_arr = x_line.reshape((len(x_line), 1)) # 作図処理を関数として定義 def update(i): # i番目のフレームに何回目の結果を使うかを計算 idx = i * iters // frame_num # idx回目のパラメータを用いて予測値を計算 y_line = predict( x_line_arr, trace_W1[idx].reshape(W1.shape), trace_b1[idx], trace_W2[idx].reshape(W2.shape), np.array([trace_b2[idx]]) ).data.flatten() # 前フレームのグラフを初期化 plt.cla() # 作図 plt.scatter(x, y, label='data') # 散布図 plt.plot(x_line, y_line, color='red', label='predict') # 予測曲線 plt.xlabel('x') # x軸ラベル plt.ylabel('y') # y軸ラベル plt.suptitle('Neural Network', fontsize=20) # 図全体のタイトル plt.title('iter:' + str(idx) + ', loss=' + str(np.round(trace_L[idx], 5)) + ', lr=' + str(lr) + ', N=' + str(len(x)), loc='left') # タイトル plt.ylim(-1.5, 2.5) # y軸の表示範囲 plt.legend() # 凡例 plt.grid() # グリッド線 # gif画像を作成 nn_anime = animation.FuncAnimation(fig, update, frames=frame_num, interval=100) # gif画像を保存 nn_anime.save("step43_NeuralNet.gif")
iters
フレームのアニメーションを作成するのは厳しいので、フレーム数をframe_num
として指定します。除算演算子//
は、割り算の整数部分を返します。これを利用して各フレームに対応する結果の回数を割り当てています。
predict()
に各試行におけるパラメータを指定します。ただし、trace_*
には1次元配列またはスカラに形状を変えて格納しました。そのため、元の形状に戻す必要があります。
試行回数が増えるにしたがって、データへの当てはまりが良くなっていくのを確認できます。
これまでに実装してきたパーツを組み合わせて、簡単なニューラルネットワークを組んで学習を行えました。次からは、より便利で簡単にニューラルネットワークを構築できるように実装していきます。
参考文献
おわりに
ようやくNNですね。
【次ステップの内容】