RNNをスクラッチで実装してみる① 

の記事では、ループなしでRNNの**順伝播(Forward Propagation)逆伝播(Backpropagation)**を1エポックだけ実装し、RNNがどのようにパラメータを更新するかをみてみます。まず、RNNの動作をステップごとに計算し、最後に手動で勾配計算を行い、パラメータを更新します。

ソフトマックス関数の定義

python

import numpy as np

# softmax 関数の定義
def softmax(x):
    e_x = np.exp(x - np.max(x))  # 数値の安定性を保つために最大値を引いています
    return e_x / np.sum(e_x, axis=0, keepdims=True)

クロスエントロピー損失関数の定義

python

# クロスエントロピー損失の定義
def cross_entropy_loss(y_pred, y_true):
    return -np.sum(y_true * np.log(y_pred + 1e-9))  # 数値安定性のために小さな値を加

重みとバイアスの初期化

python

# パラメータの設定
epochs = 1
hidden_size = 2  # 隠れ層の次元
alpha = 0.01  # 学習率
batch = 0

# one-hotエンコード済みのデータの形状
print(one_hot_inputs.shape)  # (3, 4, 5, 1)

# バッチのデータを取得
batch_inputs = one_hot_inputs[batch]  # (4, 5, 1)
print(batch_inputs.shape)  # (4, 5, 1)

# 重み行列とバイアスの定義
size = one_hot_inputs.shape[2]  # 特徴量数(5)

# 重み行列 U と W、バイアスの初期化
U = np.random.uniform(low=0, high=1, size=(hidden_size, size))  # (hidden_size, size)
W = np.random.uniform(low=0, high=1, size=(hidden_size, hidden_size))  # 隠れ層の重み行列
B = np.random.uniform(low=0, high=1, size=(hidden_size, 1))  # 隠れ層のバイアス

# 出力層の重み行列 V とバイアスの初期化
V = np.random.uniform(low=0, high=1, size=(size, hidden_size))  # (size, hidden_size)
C = np.random.uniform(low=0, high=1, size=(size, 1))  # 出力層のバイアス

# 前のタイムステップの隠れ層の初期状態をゼロベクトルで初期化
a_t_minus_1 = np.zeros((hidden_size, 1))  # (hidden_size, 1)

順伝播(Forward Propagation)

RNNは、各タイムステップで隠れ層の状態を更新し、出力を計算します。ここでは、ループを使わず、4つのタイムステップごとに処理を実装します。

# タイムステップ 0 の処理
time_step = 0
inputs = batch_inputs[time_step, :, :]
S0 = W @ a_t_minus_1 + U @ inputs + B
A0 = np.tanh(S0) 
O0 = V @ A0 + C  
Y0 = softmax(O0)  
Yt0 = one_hot_expected[batch, time_step, :, :]
Loss0 = cross_entropy_loss(Y0, Yt0)

# タイムステップ 1 の処理
time_step = 1
inputs = batch_inputs[time_step, :, :] 
S1 = W @ A0 + U @ inputs + B
A1 = np.tanh(S1)
O1 = V @ A1 + C
Y1 = softmax(O1)
Yt1 = one_hot_expected[batch, time_step, :, :]
Loss1 = cross_entropy_loss(Y1, Yt1)

# タイムステップ 2 の処理
time_step = 2
inputs = batch_inputs[time_step, :, :]
S2 = W @ A1 + U @ inputs + B
A2 = np.tanh(S2)
O2 = V @ A2 + C
Y2 = softmax(O2)
Yt2 = one_hot_expected[batch, time_step, :, :]
Loss2 = cross_entropy_loss(Y2, Yt2)

# タイムステップ 3 の処理
time_step = 3
inputs = batch_inputs[time_step, :, :]
S3 = W @ A2 + U @ inputs + B
A3 = np.tanh(S3)
O3 = V @ A3 + C
Y3 = softmax(O3)
Yt3 = one_hot_expected[batch, time_step, :, :]
Loss3 = cross_entropy_loss(Y3, Yt3)

# 各タイムステップの損失の合計
total_loss = Loss0 + Loss1 + Loss2 + Loss3
print("Total Loss:", total_loss)

逆伝播(Backpropagation)

次に、手動で逆伝播を行い、勾配を計算します。各タイムステップで誤差を伝搬させ、重み行列を更新します。

# タイムステップ 3 の勾配計算
dLt_dyt3 = Y3 - Yt3  
dLtdc3 = dLt_dyt3  
dLtdV3 = dLt_dyt3 @ A3.T  
dLtdat3 = V.T @ dLt_dyt3  

dLtdst3 = dLtdat3 * (1 - np.tanh(S3) ** 2)  

dLtdb3 = dLtdst3  
dLtdU3 = dLtdst3 @ inputs.T  
dLtdW3 = dLtdst3 @ A2.T  
dLtda_prev3 = W.T @ dLtdst3  

# タイムステップ 2 の勾配計算
inputs = batch_inputs[2, :, :]  
dLt_dyt2 = Y2 - Yt2  
dLtdc2 = dLt_dyt2  
dLtdV2 = dLt_dyt2 @ A2.T  
dLtdat2 = V.T @ dLt_dyt2  

dLtdst2 = (dLtdat2 + dLtda_prev3) * (1 - np.tanh(S2) ** 2)  

dLtdb2 = dLtdst2  
dLtdU2 = dLtdst2 @ inputs.T  
dLtdW2 = dLtdst2 @ A1.T  
dLtda_prev2 = W.T @ dLtdst2  

# タイムステップ 1 の勾配計算
inputs = batch_inputs[1, :, :]  
dLt_dyt1 = Y1 - Yt1  
dLtdc1 = dLt_dyt1  
dLtdV1 = dLt_dyt1 @ A1.T  
dLtdat1 = V.T @ dLt_dyt1  

dLtdst1 = (dLtdat1 + dLtda_prev2) * (1 - np.tanh(S1) ** 2)  

dLtdb1 = dLtdst1  
dLtdU1 = dLtdst1 @ inputs.T  
dLtdW1 = dLtdst1 @ A0.T  
dLtda_prev1 = W.T @ dLtdst1  

# タイムステップ 0 の勾配計算
inputs = batch_inputs[0, :, :]  
dLt_dyt0 = Y0 - Yt0  
dLtdc0 = dLt_dyt0  
dLtdV0 = dLt_dyt0 @ A0.T  
dLtdat0 = V.T @ dLt_dyt0  

dLtdst0 = (dLtdat0 + dLtda_prev1) * (1 - np.tanh(S0) ** 2)  

dLtdb0 = dLtdst0  
dLtdU0 = dLtdst0 @ inputs.T  
dLtdW0 = dLtdst0 @ a_t_minus_1.T  

勾配の合計とパラメータ更新

# 勾配の合計
dLtdV_total = dLtdV0 + dLtdV1 + dLtdV2 + dLtdV3
dLtdW_total = dLtdW0 + dLtdW1 + dLtdW2 + dLtdW3
dLtdU_total = dLtdU0 + dLtdU1 + dLtdU2 + dLtdU3
dLtdb_total = dLtdb0 + dLtdb1 + dLtdb2 + dLtdb3
dLtdc_total = dLtdc0 + dLtdc1 + dLtdc2 + dLtdc3

# パラメータの更新
V -= alpha * dLtdV_total
W -= alpha * dLtdW_total
U -= alpha * dLtdU_total
B -= alpha * dLtdb_total
C -= alpha * dLtdc_total

# 結果を表示
print("Updated V:", V)
print("Updated W:", W)
print("Updated U:", U)
print("Updated Hidden_bias:", B)
print("Updated Output_bias:", C)

これで1エポックの順伝播と逆伝播によるRNNのパラメータ更新が完了しました。このコードではループを使わずに、1つのバッチ(4つのタイムステップ)について順伝播と逆伝播を手動で行っています。

更新されたパラメータ

上記のコードを実行すると、更新されたパラメータ V, W, U, B, C の結果が表示されます。これらのパラメータは次のエポックで再利用され、学習が進んでいきます。

結論

この記事では、RNNの順伝播と逆伝播を手動で計算し、パラメータの更新を行う方法を解説しました。特に、タイムステップごとに行われる勾配の伝播や、各パラメータの勾配計算を1つずつ確認しました。

次のステップとして、拡張して、複数のエポックでRNNをトレーニングするループを実装したり、バッチごとに学習させる方法を追加することが考えられます。




いいなと思ったら応援しよう!