技術をかじる猫

適当に気になった技術や言語、思ったこと考えた事など。

RNN に計算問題を解かせる

white-azalea.hatenablog.jp

この辺の続き。
参考は

実装したあれこれは後述

RNN が時系列データから次の値を予測するものならば、「時系列データ=足し算するべき二つのビット列を指定、予測したい値=足した結果のビット列」で学習させれば、確かに計算器を作成することは可能かもしれない。
この使い方は思いつきませんでした。

これ、次元数増やせばそれだけでかなり複雑な判定とか出せそうな気がします。
例えば入力系統を増やして、オペレータを指定してみるとか…

引数の状態二つ(時系列データ二つ)からのみ結果が導き出される前提があり、2 回目以降の学習で前回までの勾配を持っていると正常に機能しないので、 reset_sum_grad で学習をリセットしてる。

なるほどなって感じ

    def reset_sum_grad(self):
        self.grad_w = np.zeros_like(self.w)
        self.grad_b = np.zeros_like(self.b)

実装の本編はこんな感じ。
出力層と RNN レイヤーだが、出力層はシグモイド関数てだけで、中間層の実装は前回のパクリ。

eta      = 0.1  # 学習係数
n_learn = 5001  # 学習回数
interval = 500  # 経過の表示間隔

class OutputLayer:
    def __init__(self, n_upper, n):
        self.w = np.random.randn(n_upper, n) / np.sqrt(n_upper)
        self.b = np.zeros(n)
    
    def activate_func(self, u):
        # sigmoid function
        return 1 / (1 + np.exp(-u))
    
    def diff_func(self, grad_y, y):
        # differencial sigmoid
        return grad_y * (1 - y) * y
    
    def forward(self, x):
        self.x = x
        u = np.dot(x, self.w) + self.b
        self.y = self.activate_func(u)
        return self.y
    
    def backward(self, x, y, t):
        delta = self.diff_func(y - t, y)
        self.grad_w = np.dot(x.T, delta)
        self.grad_b = np.sum(delta, axis=0)
        self.grad_x = np.dot(delta, self.w.T)
        return self.grad_x

    def reset_sum_grad(self):
        self.grad_w = np.zeros_like(self.w)
        self.grad_b = np.zeros_like(self.b)
    
    def update(self, eta):
        self.w -= eta * self.grad_w
        self.b -= eta * self.grad_b

class RnnBaseLayer:
    def __init__(self, n_upper, n):
        self.w = np.random.randn(n_upper, n) / np.sqrt(n_upper)
        self.v = np.random.randn(n, n) / np.sqrt(n)
        self.b = np.zeros(n)
    
    def forward(self, x, prev_y):
        u = np.dot(x, self.w) + np.dot(prev_y, self.v) + self.b
        self.y = np.tanh(u)
        return self.y
    
    def backward(self, x, y, prev_y, grad_y):
        delta = grad_y * (1 - y**2)

        self.grad_w += np.dot(x.T, delta)
        self.grad_v += np.dot(prev_y.T, delta)
        self.grad_b += np.sum(delta, axis=0)

        self.grad_x = np.dot(delta, self.w.T)
        self.grad_prev_y = np.dot(delta, self.v.T)
        return self.grad_prev_y

    def reset_sum_grad(self):
        self.grad_w = np.zeros_like(self.w)
        self.grad_v = np.zeros_like(self.v)
        self.grad_b = np.zeros_like(self.b)

    def update(self, eta):
        self.w -= eta * self.grad_w
        self.v -= eta * self.grad_v
        self.b -= eta * self.grad_b

ここに食わせる学習データだけど、これで学習させたのは、2進数の足し算訓練。
この時の学習データの形状は

n_time = 8   # 時系列の数(今回は最大ビット数)
n_in   = 2   # 入力層ニューロン数(二つの値を足し合わせる目的なので)
n_mid  = 32  # 中間層ニューロン数(適当)
n_out  = 1   # 出力層ニューロン数(真実はいつも一つ!)

max_num = 2**n_time  # = 256
binaries = np.zeros((max_num, n_time), dtype=int)
for i in range(max_num):
    num10 = i
    for j in range(n_time):
        pow2 = 2 ** (n_time - 1 - j)
        binaries[i, j] = num10 // pow2
        num10 %= pow2

こんな感じに書いていますが、binaries の中身は

[0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 1]
[0 0 0 0 0 0 1 0]
[0 0 0 0 0 0 1 1]
...
[1 1 1 1 1 1 1 1]

と、256 行 8 列のビット列。
この行列は先頭から 0, 1, 2, 3, ... と続いているので、32 の値はインデックス 32 に当たる。

足し合わせる二つの値は MAX(256) を 2 で割った値(これで 128 以下の値二つに化ける)

    num1 = np.random.randint(max_num//2)
    num2 = np.random.randint(max_num//2)

    # これをビット配列に置き換えて
    x1= binaries[num1]
    x2= binaries[num2]

    # 引数の形状にまとめる
    x_in = np.zeros((1, n_time, n_in))
    x_in[0, :, 0] = x1
    x_in[0, :, 1] = x2
    x_in  = np.flip(x_in, axis=1)

形状的にはこんな感じ

13 と 33 の例

[[[1. 1.]
  [0. 0.]
  [1. 0.]
  [1. 0.]
  [0. 0.]
  [0. 1.]
  [0. 0.]
  [0. 0.]]]

13 + 33 = 46 = b00101110 となるので、

    # 結果データ
    t = binaries[num1+num2]
    t_in = t.reshape(1, n_time, n_out)
    t_in = np.flip(t_in , axis=1)

コレの結果は

[[[0],
  [1],
  [1],
  [1],
  [0],
  [1],
  [0],
  [0]]]

これを食わせてシグモイド関数出力(MAX =1)で、0.5 を閾値に 01 判定して学習させていくという様式。

学習部分の全体像はこんな感じ

rnnLayer = RnnBaseLayer(n_in, n_mid)
outputLayer = OutputLayer(n_mid, n_out)

def train(x_mb, t_mb):
    y_rnn = np.zeros((len(x_mb), n_time+1, n_mid))
    y_out = np.zeros((len(x_mb), n_time, n_out))

    # Forward propergation
    y_prev = y_rnn[:, 0, :]
    for i in range(n_time):
        # RNN layer
        x = x_mb[:, i, :]
        y = rnnLayer.forward(x, y_prev)
        y_rnn[:, i + 1, :] = y
        y_prev = y

        # output layer
        y_out[:, i, :] = outputLayer.forward(y)
    
    # back propergation
    outputLayer.reset_sum_grad()
    rnnLayer.reset_sum_grad()
    grad_y = 0
    for i in reversed(range(n_time)):
        # output layer
        x = y_rnn[:, i+1, :]
        y = y_out[:, i, :]
        t = t_mb[:, i, :]
        grad_x_out = outputLayer.backward(x, y, t)

        # Rnn layer
        x = x_mb[:, i, :]
        y = y_rnn[:, i+1, :]
        y_prev = y_rnn[:, i, :]
        grad_y = rnnLayer.backward(x, y, y_prev, grad_y + grad_x_out)

    # update
    rnnLayer.update(eta)
    outputLayer.update(eta)
    return y_out

def get_error(y, t):
    return 1.0/2.0*np.sum(np.square(y - t))

for i in range(n_learn):
    # ランダムなインデックスを作成
    num1 = np.random.randint(max_num//2)
    num2 = np.random.randint(max_num//2)

    # これをビット配列に置き換えて
    x1= binaries[num1]
    x2= binaries[num2]

    # 引数の形状にまとめる
    x_in = np.zeros((1, n_time, n_in))
    x_in[0, :, 0] = x1
    x_in[0, :, 1] = x2
    x_in  = np.flip(x_in, axis=1)

    # 結果データ
    t = binaries[num1+num2]
    t_in = t.reshape(1, n_time, n_out)
    t_in = np.flip(t_in , axis=1)

    # 学習
    y_out = train(x_in, t_in)
    y = np.flip(y_out, axis=1).reshape(-1)

    error = get_error(y_out, t_in)

    if i % interval == 0:
        y2 = np.where(y<0.5, 0, 1)
        y10 = 0
        for j in range(len(y)):
            pow2 = 2 ** (n_time-1-j)
            y10 += y2[j] * pow2

        print("learn count:", i)
        print("error rate:", error)
        c = "Success : " if (y2 == t).all() else "Failure : "
        print(c + str(num1) + " + " + str(num2) + " = " + str(y10))
        print("========================")