ラビットチャレンジレポート 深層学習Day3 その1

Section1 RNN

1-1 RNN全体像

1-1-1 RNNとは

 RNNとは、時系列データに対応可能なニューラルネットワークである。

1-1-2 時系列データ

 時系列データとは、時間的順序を追って一定間隔ごとに観察され、
 しかも相互に統計的依存関係が認められるようなデータの系列

    • 音声データ
    • テキストデータ etc

時系列データは物理時間に従って現れるものだけでなく、テキストデータのようにある順序で系列として処理するデータも含まれる。
 CNNでは、このような系列処理は難しいため、RNNの技術が登場するようになった。これにより、自然言語処理技術が大きく飛躍することになった。

1-1-3 RNNとは

RNNは、以前の中間層の状態も利用して学習する構造になっている。
下図に、RNNの概念図を示す。
f:id:tibet:20211125172959p:plain
上図の左部が、RNNのネットワークの模式図で、入力層x、中間層z、出力層で構成されている。
RNNの特徴は、中間層zが自己回帰していることである。
分かりやすく展開したものが右部になる。
中間層の初期状態がz0とし、
時系列で[x1,x2,x3,x4....]というデータが入力されるとすると、
y1の処理には、中間層状態z1と前の中間状態に重みをかけたW×z0の線形結合を使用し、
y2の処理には、中間層状態z2と前の中間状態に重みをかけたW×z1の線形結合を使用し、
y3の処理には、中間層状態z3と前の中間状態に重みをかけたW×z2の線形結合を使用し、
.....
という形になる。
数式としては、下記のように書ける。
 u^t=W_{(in)}x^t+Wz^{t-1}+b
 z^t=f(W_{(in)}x^t+Wz^{t-1}+b)
 y^t=g(W_{(out)}z^t+c)
f(x):活性化関数
g(x):活性化関数
コードとしては、下記のように表せる。

u[:,t+1]=np.dot(X,W_in)+np.dot(z[:,t].reshape(1,-1),W)
z[:,t+1]=functions.sigmoid(u[:,t+1])
np.dot(z[:,t+1].reshape(1,-1),W_out)
y[:,t]=functions.sigmoid(np.dot(z[:,t+1].reshape(1,-1), W_out))
  • 確認テスト

RNNのネットワークには大きく分けて3つの重みがある。一つは入力から現在の中間層を定義する際にかけられる重み、一つは中間層から出力を定義する際にかけられる重みである。
残り1つの重みについて説明せよ。
[回答]
中間層から中間層に回帰する際の重み。

  • 演習チャレンジ

以下は再帰ニューラルネットワークにおいて、構文木を入力として、再帰的に文全体の表現ベクトルを得るプログラムである。ただし、ニューラルネットワークの重みパラメータはグローバル変数として定義してあるものとし、activation関数は何らかの活性化関数であるとする。木構造再帰的な辞書で定義してあり、rootが最も外側の辞書であると仮定する。
(く)にあてはまるコードは何か
f:id:tibet:20211126154417p:plain
[回答]
(2)
隣接単語から表現ベクトルを作るという処理は、隣接している表現leftとrightを合わせたものを特徴量としてそこに重みをかけることで実現する。

1-2 BPTT

1-2-1 BPTTとは

 Back Propagation Through Timeで、RNNにおける逆誤差伝搬法である。計算結果から微分を逆算することで、不要な再帰的計算を避けることが出来る。

1-2-2 BPTTの数学的記述

RNNのモデル式を再掲する。
 u^t=W_{(in)}x^t+Wz^{t-1}+b
 z~t=f(W_{(in)}x^t+Wz^{t-1}+b)
 v^t=W_{(out)}z~t+c
y^t=g(W_{(out)}z^t+c)

u[:,t+1]=np.dot(X,W_in)+np.dot(z[:,t].reshape(1,-1),W)
z[:,t+1]=functions.sigmoid(u[:,t+1])
np.dot(z[:,t+1].reshape(1,-1),W_out)
y[:,t]=functions.sigmoid(np.dot(z[:,t+1].reshape(1,-1), W_out))

BPTTの微分の導出と、対応するコードを示す。
[]は、すべてのtに渡る計算をすることを示す。
まず、損失関数Eの入力重みWinによる微分
 \dfrac{\partial E}{\partial W_{(in)}}=\dfrac{\partial E}{\partial u^t}[\dfrac{u^t}{W_{(in)}}]^T=δ^t[x^t]^T

np.dot(X.T,delta[:t].reshape(1,-1))

Eの出力重みWoutによる微分
 \dfrac{\partial E}{\partial W_{(out)}}{\partial W}=\dfrac{\partial E}{\partial v^t}[\dfrac{v^t}{W_{(out)}}]^T=δ^{out,t}[x^t]^T

np.dot(z[:,t+1].reshape(-1,1),delta_out[:,t].reshape(-1,1))

Eの中間層重みによる微分
 \dfrac{\partial E}{\partial W}=\dfrac{\partial E}{\partial u^t}[\dfrac{\partial u^t}{\partial W}]^T=δ^t[z^{t-1}]^T

np.dot(z[:,t].reshape(-1,1),delta[:,t].reshape(1,-1))

Eのバイアスbによる微分
 \dfrac{\partial E}{\partial b}=\dfrac{\partial E}{\partial u^t}\dfrac{\partial u^t}{\partial b}^T=δ^t
Eのバイアスcによる微分
 \dfrac{\partial E}{\partial b}=\dfrac{\partial E}{\partial u^t}\dfrac{\partial u^t}{\partial c}^T=δ^{out,t}

  • 確認テスト

下図のy1をx,s0,s1,w,woutを用いて数式で表せ。
*バイアスは任意の文字で定義せよ。
*また中間層の出力にシグモイド関数g(x)を作用させよ。
f:id:tibet:20211127225100p:plain
[回答]
 y_1=g(W_{(out)}s_1+c)
 s_1=f(W_{(in)}x_1+Ws_0+b)

ここで、時間的に遡っての微分δt-z-1について計算してみる。
 \dfrac{\partial E}{\partial u^t}=\dfrac{\partial E}{\partial v^t}\dfrac{\partial v^t}{\partial u^t}=\dfrac{\partial E}{\partial v^t}\dfrac{\partial(W_{(out)}f(u^t)+c)}{\partial u^t}=δ^{out,t}W_{(out)}^Tf'(u^t)=δ^t
 δ^{t-1}=\dfrac{\partial E}{\partial u^{t-1}}=\dfrac{\partial E}{\partial u^t}\dfrac{\partial u^t}{\partial u^{t-1}}=δ^t\left(\dfrac{\partial u^t}{\partial z^{t-1}}\dfrac{\partial z^{t-1}}{\partial u^{t-1}}\right)=δ^t{Wf'(u^{t-1})}
 δ^{t-z-1}=δ^{t-z}(Wf'(u^{t-z-1})
第一式のコードは下記のように書ける

delta[:,t]=(np.dot(delta[:,t+1].t,W.T)+np.dot(delta_out[:,t].T,W_out.T))*functions.d_sigmoid(u[:,t+1])

最終的に各パラメータの更新式は以下のようになる。
 W^{t+1}_{(in)}=W^t_{(in)}-ε\dfrac{\partial E}{\partial W_{(in)}}=W^t_{(in)}-ε\displaystyle\sum^{T_t}_{z=0}δ^{t-z}[x^{t-z}]^T
 W^{t+1}_{(out)}=W^t_{(out)}-ε\dfrac{\partial E}{\partial W_{(out)}}=W^t_{(in)}-εδ^{out,t}[z^{t}]^T
 W^{t+1}=W^t-ε\dfrac{\partial E}{\partial W}=W^t-ε\displaystyle\sum^{T_t}_{z=0}δ^{t-z}[z^{t-z-1}]^T
 b^{t+1}=b^t-ε\dfrac{\partial E}{\partial b}=b^t-ε\displaystyle\sum^{T_t}_{z=0}δ^{t-z}
 c^{t+1}=c^t-ε\dfrac{\partial E}{\partial c}=c^t-εδ^{out,t}
上部3つの式に関するコードは下記のように書ける

W_in-=learning_rate*W_in_grad
W_out=learnign_rate*W_out_grad
W-=learning_rate*W_grad
BPTTの全体像

最後に、損失関数の中で、時間にわたる計算がどのようにあらわされるかを示す。
損失関数をE=loss(x1,x2)、教師データをdとすると、RNNの損失関数は下記のように表せる。
 E^t=loss(y^t,d^t)
             =loss(g(W_{(out)}z^t+c),t^t)
             =loss(g(W_{(out)}f(W_{(in)}x^t+Wz_{t-1}+b);c),dt)
上記のように、yを展開すると、 z^{t-1}の項が出てくる。さらに、 z^{t-1}を展開すると、 z^{t-2}の項が出てくる。このように、時間をさかのぼって計算することが出来る。

RNN,BPTTの実装演習

バイナリを加算して桁上げする操作を、RNNで実行する。

import numpy as np
from common import functions
import matplotlib.pyplot as plt

def d_tanh(x):
    return 1/(np.cosh(x) ** 2)

# データを用意
# 2進数の桁数
binary_dim = 8
# 最大値 + 1
largest_number = pow(2, binary_dim)
# largest_numberまで2進数を用意
binary = np.unpackbits(np.array([range(largest_number)],dtype=np.uint8).T,axis=1)

input_layer_size = 2
hidden_layer_size = 16
output_layer_size = 1

weight_init_std = 1
learning_rate = 0.1

iters_num = 10000
plot_interval = 100

# ウェイト初期化 (バイアスは簡単のため省略)
W_in = weight_init_std * np.random.randn(input_layer_size, hidden_layer_size)
W_out = weight_init_std * np.random.randn(hidden_layer_size, output_layer_size)
W = weight_init_std * np.random.randn(hidden_layer_size, hidden_layer_size)
# Xavier
# W_in = np.random.randn(input_layer_size, hidden_layer_size) / (np.sqrt(input_layer_size))
# W_out = np.random.randn(hidden_layer_size, output_layer_size) / (np.sqrt(hidden_layer_size))
# W = np.random.randn(hidden_layer_size, hidden_layer_size) / (np.sqrt(hidden_layer_size))
# He
# W_in = np.random.randn(input_layer_size, hidden_layer_size) / (np.sqrt(input_layer_size)) * np.sqrt(2)
# W_out = np.random.randn(hidden_layer_size, output_layer_size) / (np.sqrt(hidden_layer_size)) * np.sqrt(2)
# W = np.random.randn(hidden_layer_size, hidden_layer_size) / (np.sqrt(hidden_layer_size)) * np.sqrt(2)


# 勾配
W_in_grad = np.zeros_like(W_in)
W_out_grad = np.zeros_like(W_out)
W_grad = np.zeros_like(W)

u = np.zeros((hidden_layer_size, binary_dim + 1))
z = np.zeros((hidden_layer_size, binary_dim + 1))
y = np.zeros((output_layer_size, binary_dim))

delta_out = np.zeros((output_layer_size, binary_dim))
delta = np.zeros((hidden_layer_size, binary_dim + 1))

all_losses = []

for i in range(iters_num):
    
    # A, B初期化 (a + b = d)
    a_int = np.random.randint(largest_number/2)
    a_bin = binary[a_int] # binary encoding
    b_int = np.random.randint(largest_number/2)
    b_bin = binary[b_int] # binary encoding
    
    # 正解データ
    d_int = a_int + b_int
    d_bin = binary[d_int]
    
    # 出力バイナリ
    out_bin = np.zeros_like(d_bin)
    
    # 時系列全体の誤差
    all_loss = 0    
    
    # 時系列ループ
    for t in range(binary_dim):
        # 入力値
        X = np.array([a_bin[ - t - 1], b_bin[ - t - 1]]).reshape(1, -1)
        # 時刻tにおける正解データ
        dd = np.array([d_bin[binary_dim - t - 1]])
        
        u[:,t+1] = np.dot(X, W_in) + np.dot(z[:,t].reshape(1, -1), W)
        z[:,t+1] = functions.sigmoid(u[:,t+1])
#         z[:,t+1] = functions.relu(u[:,t+1])
#         z[:,t+1] = np.tanh(u[:,t+1])    
        y[:,t] = functions.sigmoid(np.dot(z[:,t+1].reshape(1, -1), W_out))


        #誤差
        loss = functions.mean_squared_error(dd, y[:,t])
        
        delta_out[:,t] = functions.d_mean_squared_error(dd, y[:,t]) * functions.d_sigmoid(y[:,t])        
        
        all_loss += loss

        out_bin[binary_dim - t - 1] = np.round(y[:,t])
    
    
    for t in range(binary_dim)[::-1]:
        X = np.array([a_bin[-t-1],b_bin[-t-1]]).reshape(1, -1)        

        delta[:,t] = (np.dot(delta[:,t+1].T, W.T) + np.dot(delta_out[:,t].T, W_out.T)) * functions.d_sigmoid(u[:,t+1])
#         delta[:,t] = (np.dot(delta[:,t+1].T, W.T) + np.dot(delta_out[:,t].T, W_out.T)) * functions.d_relu(u[:,t+1])
#         delta[:,t] = (np.dot(delta[:,t+1].T, W.T) + np.dot(delta_out[:,t].T, W_out.T)) * d_tanh(u[:,t+1])    

        # 勾配更新
        W_out_grad += np.dot(z[:,t+1].reshape(-1,1), delta_out[:,t].reshape(-1,1))
        W_grad += np.dot(z[:,t].reshape(-1,1), delta[:,t].reshape(1,-1))
        W_in_grad += np.dot(X.T, delta[:,t].reshape(1,-1))
    
    # 勾配適用
    W_in -= learning_rate * W_in_grad
    W_out -= learning_rate * W_out_grad
    W -= learning_rate * W_grad
    
    W_in_grad *= 0
    W_out_grad *= 0
    W_grad *= 0
    

    if(i % plot_interval == 0):
        all_losses.append(all_loss)        
        print("iters:" + str(i))
        print("Loss:" + str(all_loss))
        print("Pred:" + str(out_bin))
        print("True:" + str(d_bin))
        out_int = 0
        for index,x in enumerate(reversed(out_bin)):
            out_int += x * pow(2, index)
        print(str(a_int) + " + " + str(b_int) + " = " + str(out_int))
        print("------------")

lists = range(0, iters_num, plot_interval)
plt.plot(lists, all_losses, label="loss")
plt.show()

結果の一部表示

iters:0
Loss:1.245858363786796
Pred:[0 0 0 0 0 0 0 0]
True:[1 0 1 0 0 0 0 1]
82 + 79 = 0
------------
iters:100
Loss:0.9697292981990132
Pred:[0 1 1 1 1 0 1 1]
True:[1 0 0 1 1 0 1 1]
55 + 100 = 123
------------
.....(途中一部省略)
.------------
iters:9800
Loss:0.0009243998471599557
Pred:[0 1 1 0 0 1 0 1]
True:[0 1 1 0 0 1 0 1]
68 + 33 = 101
------------
iters:9900
Loss:0.0025480327477791424
Pred:[0 0 1 0 0 0 0 1]
True:[0 0 1 0 0 0 0 1]
19 + 14 = 33
------------

f:id:tibet:20211126154215p:plain
学習回数の増加に従って、PredとTrueの値が一致してきていることも分かる。
また、繰り返して学習することで、損失関数が低減していることがわかる。

  • コード演習問題

下図は、BPTTを行うプログラムである。なお簡単化のため活性化関数は恒等関数であるとする。
また、calculate_dout関数は損失関数を出力に関して偏微分した値を返す関数であるとする。
(お)にあてはまる値はどれか
f:id:tibet:20211128170542p:plain
[解答]
中間出力 h_{t}は過去の中間層出力 h_{t-1},h_[t-2},,,h_1に依存する。
 \dfrac{dh_{t}}{dh_{t-1}}=U
であることに注意すると、過去にさかのぼるたびにUがかけられる。つまり、(お)は、delta_t=delta_t.dot(U)となる。