からっぽのしょこ

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

7.2:線形回帰【ゼロつく4のノート】

はじめに

 『ゼロから作るDeep Learning 4 ――強化学習編』の独学時のまとめノートです。初学者の補助となるようにゼロつくシリーズの4巻の内容に解説を加えていきます。本と一緒に読んでください。

 この記事は、7.3節の内容です。DeZeroを利用して線形回帰の学習を実装します。

【前節の内容】

www.anarchive-beta.com

【他の記事一覧】

www.anarchive-beta.com

【この記事の内容】

7.2 線形回帰

 DeZeroライブラリを使って、線形回帰の学習を実装します。DeZeroフレームワークについては「『ゼロから作るDeep Learning 3』の学習ノート:記事一覧 - からっぽのしょこ」を参照してください。

 利用するライブラリを読み込みます。

# ライブラリを読み込み
import numpy as np
from dezero import Variable
import dezero.functions as F

# 追加ライブラリ
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation

 更新推移をアニメーションで確認するのにmatplotlibのモジュールを利用します。不要であれば省略してください。

7.2.1 トイ・データセット

 まずは、学習に利用するトイ・データセットを作成します。

 乱数を使ってデータセットを作成します。

# データ数を指定
N = 100

# トイ・データを生成
x_data = np.random.rand(N)
y_data = 5 + 2 * x_data + np.random.rand(N)
print(np.round(x_data[:10], 2))
print(np.round(y_data[:10], 2))
[0.89 0.8  0.83 0.44 0.75 0.95 0.86 0.63 0.25 0.47]
[7.03 6.96 7.34 6.38 6.61 7.04 7.69 6.54 6.43 6.92]

 0から1の一様乱数をnp.random.rand()で生成して、データを作成します。

 データセットを散布図で確認します。

# トイ・データセットの散布図を作成
plt.figure(figsize=(8, 6), facecolor='white')
plt.scatter(x_data, y_data)
plt.xlabel('x')
plt.ylabel('y')
plt.title('N=' + str(N), loc='left')
plt.suptitle('$x_n \sim U(0, 1),\ y_n = 2 x_n + 5 + r_n,\ r_n \sim U(0, 1)$', fontsize=15)
plt.grid()
plt.show()

トイ・データセット

 このデータに合うような直線を求めます。($x$の係数の2が重み、5に一様乱数の期待値0.5を加えた5.5がバイアスの真の値なんだと思う。)

7.2.3 線形回帰の実装

 次は、勾配降下法による線形回帰の学習を実装します。Linearクラスや線形回帰については「ステップ42:線形回帰の実装【ゼロつく3のノート(実装)】 - からっぽのしょこ」、2乗和誤差については「4.2.1:2乗和誤差の実装【ゼロつく1のノート(実装)】 - からっぽのしょこ」や「ステップ42:平均2乗誤差の逆伝播の導出【ゼロつく3のノート(数学)】 - からっぽのしょこ」を参照してください。

 勾配降下法により損失関数(平均2乗誤差)が最小となるパラメータを求めます。

# 試行回数を指定
iters = 100

# 学習率を指定
lr = 0.1

# トイ・データセットをVariableインスタンス化
x = Variable(x_data.reshape(N, 1))
y = Variable(y_data.reshape(N, 1))

# パラメータを初期化
W = Variable(np.zeros((1, 1))) # 重み
b = Variable(np.zeros(1)) # パラメータ

# 推移の確認用のリストを初期化
trace_loss = []
trace_W = [W.data.item()]
trace_b = [b.data.item()]

# 繰り返し学習
for i in range(iters):
    # 予測値(線形変換)を計算
    y_pred = F.linear(x, W, b)
    
    # 損失を計算
    loss = F.mean_squared_error(y, y_pred)
    
    # 勾配を初期化
    W.cleargrad()
    b.cleargrad()
    
    # 勾配(逆伝播)を計算
    loss.backward()
    
    # 勾配降下法により値を更新
    W.data -= lr * W.grad.data
    b.data -= lr * b.grad.data
    
    # 更新値を保存
    trace_loss.append(loss.data)
    trace_W.append(W.data.item())
    trace_b.append(b.data.item())
    
    # 損失を表示
    print('iter '+str(i+1) + ': loss='+str(loss.data))
iter 1: loss=43.491510711393914
iter 2: loss=23.802291836166432
iter 3: loss=13.054705383037911
iter 4: loss=7.1877490128664645
iter 5: loss=3.9848037739746314
(省略)
iter 96: loss=0.09034900821461835
iter 97: loss=0.09021797442828561
iter 98: loss=0.09009019172859968
iter 99: loss=0.08996557945266682
iter 100: loss=0.08984405893892493

 処理の内容については本を参照してください。

 学習したパラメータを使って、データごとの予測値と平均2乗誤差を計算します。

# 予測値を計算
y_pred = F.linear(x, W, b)
print(np.round(y_pred.data[:10].flatten(), 3))

# 損失を計算
last_loss = F.mean_squared_error(y, y_pred).data
print(last_loss)
[7.355 7.151 7.222 6.327 7.036 7.497 7.279 6.765 5.916 6.413]
0.08972555347748817

 最終的なパラメータW, bにおける損失は、trace_lossに含まれていないので、計算してlast_lossとします。

 損失関数の推移をグラフで確認します。

# 損失関数の推移を作図
plt.figure(figsize=(8, 6), facecolor='white')
plt.plot(np.arange(iters+1), trace_loss+[last_loss])
plt.xlabel('iteration')
plt.ylabel('loss')
plt.suptitle('Mean Squared Error', fontsize=20)
plt.title('lr='+str(lr), loc='left')
plt.grid()
plt.show()

損失関数の推移

 最後の値last_lossをリストに格納して、過去の値trace_loss+演算子で結合して描画します。
 損失が0に近付いていくことから学習が進んでいるのが分かります。

 回帰直線を計算します。

# 回帰直線を計算
x_line = np.linspace(0, 1, num=101)
y_line = F.linear(x_line.reshape((-1, 1)), W, b).data.flatten()
print(np.round(x_line[:10], 3))
print(np.round(y_line[:10], 3))
[0.   0.01 0.02 0.03 0.04 0.05 0.06 0.07 0.08 0.09]
[5.343 5.366 5.388 5.411 5.433 5.456 5.479 5.501 5.524 5.547]

 x軸の値x_lineを作成して、要素ごとに予測値を計算します。内積を計算できるように形状を合わせる必要があります。

 トイ・データセットの散布図に、回帰直線を重ねて描画します。

# 残差を計算
y_residual = y_pred.data.flatten() - y_data

# パラメータラベルを作成
param_label = 'iter:' + str(iters)
param_label += ', lr=' + str(lr)
param_label += ', W='+str(np.round(W.data.item(), 2))
param_label += ', b='+str(np.round(b.data.item(), 2))
param_label += ', loss='+str(np.round(last_loss, 3))

# 回帰直線を作図
plt.figure(figsize=(12, 9), facecolor='white')
plt.quiver(x_data, y_data, np.repeat(0, N), y_residual, 
           scale_units='xy', scale=1, units='dots', width=0.1, headwidth=0.1, 
           fc='none', ec='gray', linewidth=1.5, linestyle=':', label='residual') # 残差
plt.scatter(x_data, y_data, label='data') # データ
plt.plot(x_line, y_line, color='red', label='predict') # 回帰直線
plt.xlabel('x')
plt.ylabel('y')
plt.suptitle('Linear Regression', fontsize=20)
plt.title(param_label, loc='left')
plt.grid()
plt.legend()
plt.show()

回帰直線

 データごとに観測値と予測値の差を計算して、plt.quiver()で線分を描画します。

 回帰直線の推移をアニメーションで確認します。

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

# 図を初期化
fig = plt.figure(figsize=(12, 9), facecolor='white')
fig.suptitle('Linear Regression', fontsize=20)

# 作図処理を関数として定義
def update(i):
    # 前フレームのグラフを初期化
    plt.cla()
    
    # i回目のパラメータを取得
    W = np.array([[trace_W[i]]])
    b = np.array([trace_b[i]])
    
    # 予測値を計算
    y_pred = F.linear(x, W, b)

    # 残差を計算
    y_residual = y_pred.data.flatten() - y_data
    
    # 回帰直線を計算
    y_line = F.linear(x_line.reshape((-1, 1)), W, b).data.flatten()
    
    # パラメータラベルを作成
    param_label = 'iter:' + str(i)
    param_label += ', lr=' + str(lr)
    param_label += ', W='+str(np.round(trace_W[i], 3))
    param_label += ', b='+str(np.round(trace_b[i], 3))
    if i < iters:
        param_label += ', loss='+str(np.round(trace_loss[i], 5))
    else:
        param_label += ', loss='+str(np.round(last_loss, 5))
    
    # 回帰直線を描画
    plt.quiver(x_data, y_data, np.repeat(0, N), y_residual, 
               scale_units='xy', scale=1, units='dots', width=0.1, headwidth=0.1, 
               fc='none', ec='gray', linewidth=1.5, linestyle=':', label='residual') # 残差
    plt.scatter(x_data, y_data, label='data') # データ
    plt.plot(x_line, y_line, color='red', label='predict') # 回帰直線
    plt.xlabel('x')
    plt.ylabel('y')
    plt.title(param_label, loc='left')
    plt.grid()
    plt.legend()
    #plt.ylim(0, 10)

# gif画像を作成
ani = FuncAnimation(fig=fig, func=update, frames=iters+1, interval=100)

# gif画像を保存
ani.save('LinearRegression.gif')

 フレームごとにtrace_*からi番目の値を取り出して回帰直線を描画する処理を関数update()として定義して、FuncAnimation()でアニメーション(gif画像)を作成します。


回帰直線の推移

 データへの当てはまりがよくなる様子を確認できます。

 パラメータの更新値の推移をグラフで確認します。

# パラメータの更新値の推移を作図
plt.figure(figsize=(8, 6), facecolor='white')
plt.plot(np.arange(iters+1), trace_W, label='W')
plt.plot(np.arange(iters+1), trace_b, label='b')
plt.xlabel('iteration')
plt.ylabel('value')
plt.suptitle('parameters', fontsize=20)
plt.title('lr='+str(lr), loc='left')
plt.grid()
plt.legend()
plt.show()

パラメータの推移

 (重みが2、バイアスが5.5に近付いていますし、始めの解釈でいいですよね。)

 この節では、線形回帰を実装しました。次節では、ニューラルネットワークによる非線形回帰を実装します。

参考文献


おわりに

 ほぼ同じ内容を書いたことがある気がしますがまぁ、、

【次節の内容】

www.anarchive-beta.com