RNN(リカレントニューラルネットワーク)について調べてみた。(初心者がChatGPTを使ってAIを作って研究してみた)

はじめに

これまでAIの根幹となるところを勉強してきました。今回から実際にAIを開発していきたいと思います。中でも今回は時系列データや言語処理に強いといわれるリカレントニューラルネットワーク(RNN)について勉強してきたいと思います。

RNNについて調べてみた

RNNの概要

RNNとは多くの方が分かりやすく説明しています。例えば、こちらこちらなどが読んでいておもしろいです。

詳しいことはこれから勉強していきましょう。

ChatGPTにRNNのコードを聞いてみた(1回目)

とりあえず、ChatGPTにRNNのコードを聞いて、基礎原理を学んでいきましょう。

AI開発用のライブラリにtensorflowやkerasが有名ですが、それだと基礎原理の部分が見えないので、numpyというライブラリで計算の流れを聞いてみます。

J.A.
J.A.

numpyでrnnを実装して

ChatGPT
ChatGPT

NumPyを使用してRNN(Recurrent Neural Network)を実装するには、まず基本的なRNNセルを作成し、その後に時系列データに対してこれを展開する必要があります。以下に、シンプルなRNNの実装例を示します。

python
Copy code
import numpy as np

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

# RNNセルの定義
class SimpleRNNCell:
    def __init__(self, input_size, hidden_size):
        self.input_size = input_size
        self.hidden_size = hidden_size

        # 重み行列の初期化
        self.Wx = np.random.randn(hidden_size, input_size)
        self.Wh = np.random.randn(hidden_size, hidden_size)
        self.b = np.zeros((hidden_size, 1))

    def forward(self, x, h_prev):
        # 入力と前の隠れ状態から次の隠れ状態を計算
        self.x = x
        self.h_prev = h_prev
        self.a = np.dot(self.Wx, x) + np.dot(self.Wh, h_prev) + self.b
        self.h = np.tanh(self.a)
        return self.h

# サンプルデータの生成
seq_length = 5
input_size = 3
hidden_size = 4

# 入力データ (shape: (input_size, seq_length))
X = np.random.randn(input_size, seq_length)

# 初期隠れ状態 (shape: (hidden_size, 1))
h0 = np.zeros((hidden_size, 1))

# RNNセルの作成
rnn_cell = SimpleRNNCell(input_size, hidden_size)

# RNNの順伝播
h_t = h0
for t in range(seq_length):
    x_t = X[:, t].reshape(-1, 1)
    h_t = rnn_cell.forward(x_t, h_t)

print("入力データ X:\n", X)
print("\n最終的な隠れ状態 h_t:\n", h_t)

この例では、SimpleRNNCellクラスを使用して1つのRNNセルを実装し、サンプルデータに対して順伝播を行っています。このコードは非常に基本的なものであり、実際のアプリケーションではさらに複雑なネットワーク構造が必要かもしれません。実際の深層学習のライブラリ(例: TensorFlowやPyTorch)を使用すると、ネットワークの定義や学習が簡単になります。

ChatGPTにRNNのコードを聞いてみた(2回目)

うまく聞き出せましたが、学習や逆伝播が実装されていません。改めて逆伝播できるように聞いてみます。

J.A>
J.A>

逆伝搬も実装して

RNNの逆伝播を実装するには、基本的な勾配降下法(Gradient Descent)を使用して重みを更新する必要があります。以下に、簡単なRNNの逆伝播を含む実装例を示します。

python
Copy code
import numpy as np

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

# RNNセルの定義
class SimpleRNNCell:
    def __init__(self, input_size, hidden_size):
        self.input_size = input_size
        self.hidden_size = hidden_size

        # 重み行列の初期化
        self.Wx = np.random.randn(hidden_size, input_size)
        self.Wh = np.random.randn(hidden_size, hidden_size)
        self.b = np.zeros((hidden_size, 1))

    def forward(self, x, h_prev):
        # 順伝播
        self.x = x
        self.h_prev = h_prev
        self.a = np.dot(self.Wx, x) + np.dot(self.Wh, h_prev) + self.b
        self.h = np.tanh(self.a)
        return self.h

    def backward(self, dh_next):
        # 逆伝播
        da = dh_next * (1 - np.tanh(self.a)**2)
        self.db = np.sum(da, axis=1, keepdims=True)
        self.dWh = np.dot(da, self.h_prev.T)
        self.dWx = np.dot(da, self.x.T)
        self.dx = np.dot(self.Wh.T, da)
        self.dh_prev = np.dot(self.Wx.T, da)
        return self.dx, self.dh_prev

    def update_parameters(self, learning_rate):
        # 重みの更新
        self.Wx -= learning_rate * self.dWx
        self.Wh -= learning_rate * self.dWh
        self.b -= learning_rate * self.db

# サンプルデータの生成
seq_length = 5
input_size = 3
hidden_size = 4

# 入力データ (shape: (input_size, seq_length))
X = np.random.randn(input_size, seq_length)

# 目標出力 (仮の値)
y = np.random.randn(hidden_size, 1)

# 初期隠れ状態 (shape: (hidden_size, 1))
h0 = np.zeros((hidden_size, 1))

# RNNセルの作成
rnn_cell = SimpleRNNCell(input_size, hidden_size)

# 順伝播
h_t = h0
for t in range(seq_length):
    x_t = X[:, t].reshape(-1, 1)
    h_t = rnn_cell.forward(x_t, h_t)

# 仮の損失関数 (例: 平均二乗誤差)
loss = np.mean((h_t - y)**2)

# 逆伝播
dh_next = 2 * (h_t - y)  # 仮の勾配
dx, dh_prev = rnn_cell.backward(dh_next)

# 重みの更新
learning_rate = 0.01
rnn_cell.update_parameters(learning_rate)

この例では、逆伝播を実装するためにbackwardメソッドを追加し、勾配を計算しています。update_parametersメソッドでは、勾配と学習率を使用して重みを更新しています。これは非常に基本的な例であり、実際のアプリケーションではさらに改良や最適化が必要かもしれません。

ChatGPTのコードを修正

これもうまく聞き出せました。しかし、順伝播と逆伝播を1回しか行っていないので、学習を行っていません。そこで、ここからは自分で修正していきたいと思います。

import numpy as np
import matplotlib.pyplot as plt

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

# RNNセルの定義
class SimpleRNNCell:
    def __init__(self, input_size, hidden_size):
        self.input_size = input_size
        self.hidden_size = hidden_size

        # 重み行列の初期化
        self.Wx = np.random.randn(hidden_size, input_size)
        self.Wh = np.random.randn(hidden_size, hidden_size)
        self.b = np.zeros((hidden_size, 1))

    def forward(self, x, h_prev):
        # 順伝播
        self.x = x
        self.h_prev = h_prev
        self.a = np.dot(self.Wx, x) + np.dot(self.Wh, h_prev) + self.b
        self.h = np.tanh(self.a)
        return self.h

    def backward(self, dh_next):
        # 逆伝播
        da = dh_next * (1 - np.tanh(self.a)**2)
        self.db = np.sum(da, axis=1, keepdims=True)
        self.dWh = np.dot(da, self.h_prev.T)
        self.dWx = np.dot(da, self.x.T)
        self.dx = np.dot(self.Wh.T, da)
        self.dh_prev = np.dot(self.Wx.T, da)
        return self.dx, self.dh_prev

    def update_parameters(self, learning_rate):
        # 重みの更新
        self.Wx -= learning_rate * self.dWx
        self.Wh -= learning_rate * self.dWh
        self.b -= learning_rate * self.db

# サンプルデータの生成
seq_length = 5
input_size = 10
hidden_size = input_size

fig = plt.figure()
# 入力データ (shape: (input_size, seq_length))
X = np.random.randn(input_size, seq_length)
print(X)
ax1 = fig.add_subplot(3, 1, 1)
ax1.plot(range(len(X)), X, marker="o")
ax1.set_ylabel("input")

# 目標出力 (仮の値)
y = np.random.randn(hidden_size, 1)
print(y)
ax2 = fig.add_subplot(3, 1, 2)
ax2.plot(range(len(y)), y, label="original")
ax2.set_ylabel("output")

# 初期隠れ状態 (shape: (hidden_size, 1))
h0 = np.zeros((hidden_size, 1))

# RNNセルの作成
rnn_cell = SimpleRNNCell(seq_length, hidden_size)

# 順伝播
h_t = h0
epochs = 100
loss_list = []
for epoch in range(epochs):
    for t in range(input_size):
        x_t = X[t, :].reshape(-1, 1)
        h_t = rnn_cell.forward(x_t, h_t)
    
    # 仮の損失関数 (例: 平均二乗誤差)
    loss = np.mean((h_t - y)**2)
    loss_list.append(loss)
    
    # 逆伝播
    dh_next = 2 * (h_t - y)  # 仮の勾配
    dx, dh_prev = rnn_cell.backward(dh_next)
    
    # 重みの更新
    learning_rate = 0.01
    rnn_cell.update_parameters(learning_rate)
    
    print(loss)

ax2.plot(range(len(y)), h_t, marker="o", label="predict")
ax2.legend(loc="upper left")

ax3 = fig.add_subplot(3, 1, 3)
ax3.plot(range(epochs)[1:], loss_list[1:], marker="o")
ax3.set_ylabel("loss")
ax3.set_yscale('log')


x_t = X[-1, :].reshape(-1, 1)
pred = rnn_cell.forward(x_t, h_t)[-1]

とりあえず、順伝播と逆伝播をepochs回繰り返すようにしました。また、入力データと正解データをグラフ化し、正解データの方には予測したデータも追加しました。そして、正解の数値と予測したデータの差を可視化するようにし、精度の確認も行っています。

実行結果

この実行結果を以下に示します。

結果1

結果2

結果3

結果4

結果5

今回の入力データや正解データはランダムに毎回変わるので、結果も変わってきます。

しかし、loss(損失関数)だけ見ても色々なパターンが見ることができます。きれいに小さくなるパターンや徐々に小さくなるパターン、振動したりするパターンもあります。

これは私のプログラミングミスでしょうか?それともRNNの特徴でしょうか?

現状どちらの可能性もありますが、RNNの特徴に勾配爆発や勾配消失があるようです(こちら)。

自作に近いので、判断が難しいですが、今後開発していく上で気を付けていきたいと思います。

最後に

次回からコードをひとつひとつ読み解いていき、RNNの基礎原理を勉強していきます。

コメント

タイトルとURLをコピーしました