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

RNN(Recurrent Neural Network)は時系列データやシーケンスデータに適したニューラルネットワークです。今回は、Pythonを使ってRNNのスクラッチ実装に挑戦してみます。この記事では、ループを使った複数のエポックでのトレーニングと、順伝播と逆伝播を通じた学習を行い、損失関数の減少をプロットします。

準備:データと前処理

まず、サンプルデータを用意します。A, B, C, D, Eという文字を5クラスとして、それをone-hotエンコーディングしたデータを入力として使用します。

import numpy as np

# A~Eを5クラスと仮定
inputs = np.array([
    ["A", "B", "C", "D"],
    ["C", "D", "E", "A"],
    ["B", "C", "D", "E"],
])

expected = np.array([
    ["B", "C", "D", "E"],
    ["D", "E", "A", "B"],
    ["C", "D", "A", "A"],
])

One-hotエンコーディングの実装

次に、文字をone-hotエンコーディングに変換する関数を定義します。

# A~E を5クラスに対応付け
def string_to_one_hot(inputs: np.ndarray) -> np.ndarray:
    char_to_index = {'A': 0, 'B': 1, 'C': 2, 'D': 3, 'E': 4}

    one_hot_inputs = []
    for row in inputs:
        one_hot_list = []
        for char in row:
            if char.upper() in char_to_index:
                one_hot_vector = np.zeros((5, 1))  # 5次元に変更
                one_hot_vector[char_to_index[char.upper()]] = 1
                one_hot_list.append(one_hot_vector)
        one_hot_inputs.append(one_hot_list)

    return np.array(one_hot_inputs)

# 実行
one_hot_inputs = string_to_one_hot(inputs)
one_hot_expected = string_to_one_hot(expected)

# 結果の形状を確認
print(one_hot_inputs.shape)
print(one_hot_expected.shape)

softmax関数とクロスエントロピー損失関数

ニューラルネットワークの出力層に使用するsoftmax関数と、損失関数として使うクロスエントロピー損失を定義します。

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

# クロスエントロピー損失の定義
def cross_entropy_loss(y_pred, y_true):
    y_pred = np.clip(y_pred, 1e-12, 1.0)  # 数値安定性のために予測値を制限
    return -np.sum(y_true * np.log(y_pred)) / y_true.shape[0]  # バッチの平均損失を計算

モデルの初期化

次に、RNNのモデルを初期化します。ここでは、隠れ層のサイズ、重み行列、バイアスの初期化を行います。

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

# クロスエントロピー損失の定義
def cross_entropy_loss(y_pred, y_true):
    y_pred = np.clip(y_pred, 1e-12, 1.0)  # 数値安定性のために予測値を制限
    return -np.sum(y_true * np.log(y_pred)) / y_true.shape[0]  # バッチの平均損失を計算

モデルの初期化

次に、RNNのモデルを初期化します。ここでは、隠れ層のサイズ、重み行列、バイアスの初期化を行います。

# パラメータの設定
epochs = 1500
hidden_size = 2  # 隠れ層の次元
alpha = 0.005
batch = 0

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

# 重み行列 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=0, size=(size, hidden_size))  # (size, hidden_size)
C = np.random.uniform(low=0, high=0, size=(size, 1))  # 出力層のバイアス

順伝播と逆伝播の実装

ここでは、順伝播による出力の計算と、逆伝播による勾配の計算を行います。これをエポックごとに繰り返し学習していきます。

# 損失を記録するリストを初期化
epoch_losses = []

# ループでトレーニングを行う
for epoch in range(epochs):
    print(f"Epoch {epoch + 1}/{epochs}")
    
    total_loss = 0
    
    # バッチごとにループ
    for batch in range(one_hot_inputs.shape[0]):
        batch_inputs = one_hot_inputs[batch]  # (4, 5, 1)
        
        # 各タイムステップの順伝播計算
        for time_step in range(windows):
            inputs = batch_inputs[time_step, :, :]
            if time_step == 0:
                S[time_step] = W @ np.zeros((hidden_size, 1)) + U @ inputs + B
            else:
                S[time_step] = W @ A[time_step-1] + U @ inputs + B
            A[time_step] = np.tanh(S[time_step])  # 隠れ層のアクティベーション
            O[time_step] = V @ A[time_step] + C  # 出力
            Yhat[time_step] = softmax(O[time_step])  # softmax適用
            Ytrue[time_step] = one_hot_expected[batch, time_step, :, :]
            
        # 損失の計算
        Loss = cross_entropy_loss(Yhat[time_step], Ytrue[time_step])
        total_loss += Loss
    
        # 逆伝播による勾配計算
        for r_time_step in reversed(range(windows)):
            dLt_dyhat = Yhat[r_time_step] - Ytrue[r_time_step]
            dLtdc = dLt_dyhat
            dLtdV = dLt_dyhat @ A[r_time_step].T
            dLtdat = V.T @ dLt_dyhat
            dLtdst = dLtdat * (1 - np.tanh(S[r_time_step]) ** 2)
            
            if r_time_step > 0:
                dLtdW = dLtdst @ A[r_time_step-1].T
            else:
                dLtdW = dLtdst @ np.zeros((hidden_size, 1)).T
            
            dLtdU = dLtdst @ inputs.T
            dLtdb = dLtdst
        
        # パラメータの更新
        V -= alpha * dLtdV
        W -= alpha * dLtdW
        U -= alpha * dLtdU
        B -= alpha * dLtdb
        C -= alpha * dLtdc

    epoch_losses.append(total_loss)
    print(f"Total Loss after epoch {epoch + 1}: {total_loss}")

学習曲線の可視化

最後に、エポックごとの損失をプロットして、モデルがどのように学習しているかを確認します。


結果

上記のコードを実行すると、エポックごとに損失が減少していく様子が確認でき、RNNの学習が進んでいることがわかります

結論

この記事では、RNNをスクラッチで実装し、順伝播と逆伝播による学習を行いました。隠れ層の状態を保持しつつ、時系列データを扱うために必要な計算がどのように行われるかを確認しました。

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