からっぽのしょこ

読んだら書く!書いたら読む!同じ事は二度調べ(たく)ない

ステップ43:簡単なニューラルネットワークの実装【ゼロつく3のノート(実装)】

はじめに

 『ゼロから作るDeep Learning 3』の初学者向け攻略ノートです。『ゼロつく3』の学習の補助となるように適宜解説を加えていきます。本と一緒に読んでください。

 本で省略されているクラスや関数の内部の処理を1つずつ解説していきます。

 この記事は、主にステップ43「ニューラルネットワーク」を補足する内容です。
 DeZeroのモジュール利用して簡単なニューラルネットワークを実装します。

【前ステップの内容】

www.anarchive-beta.com

【他の記事一覧】

www.anarchive-beta.com

【この記事の内容】

・簡単なニューラルネットワークの実装

 DeZeroのモジュールを使って、2層のニューラルネットワークを実装し、勾配降下法によりパラメータを推定します。

・実装する前に

 この例では、ニューラルネットワークにSigmoid関数を利用します。Sigmoidクラスまたはsigmoid_simple()関数の実装には、指数関数のクラスExpを利用します。
 functions.pyに実装されているExpSigmoidを見ると、ステップ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()

f:id:anemptyarchive:20210616203705p:plain
データセット

 非線形なデータセットになるのを確認できます。

・ニューラルネットワークの実装

 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_btrace_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()

f:id:anemptyarchive:20210616203733p:plain
予測値

 データへの当てはまりのよい曲線を引けています。

 損失(平均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()

f:id:anemptyarchive:20210616203757p:plain
損失の推移

 試行回数が増えるにしたがって損失が下がっています。(何だこの不整脈は??)

 パラメータの推移も確認しておきましょう。

・コード(クリックで展開)

 リストに格納した試行ごとのパラメータの値を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()


f:id:anemptyarchive:20210616203913p:plainf:id:anemptyarchive:20210616203921p:plain
第1層のパラメータの推移

f:id:anemptyarchive:20210616203948p:plainf:id:anemptyarchive:20210616203952p:plain
第2層のパラメータの推移


 最後に、予測値の曲線の推移をアニメーションで確認します。

・コード(クリックで展開)

# フレーム数を指定
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次元配列またはスカラに形状を変えて格納しました。そのため、元の形状に戻す必要があります。


f:id:anemptyarchive:20210616204144g:plain
予測値の推移

 試行回数が増えるにしたがって、データへの当てはまりが良くなっていくのを確認できます。

 これまでに実装してきたパーツを組み合わせて、簡単なニューラルネットワークを組んで学習を行えました。次からは、より便利で簡単にニューラルネットワークを構築できるように実装していきます。

参考文献

  • 斎藤康毅『ゼロから作るDeep Learning 3 ――フレームワーク編』オライリー・ジャパン,2020年.

おわりに

 ようやくNNですね。

【次ステップの内容】

www.anarchive-beta.com