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をトレーニングするループを実装したり、バッチごとに学習させる方法を追加することが考えられます。