再帰的ネットワークRNN

RNN

RNNは時系列データ処理に適したNNで、1時刻前のリカレント層の状態を、次の時刻のリカレント層へ伝播する.
特徴:勾配消失や爆発を防ぐため、勾配クリッピングを行う.閾値より大きい時、勾配のノルムを閾値に正規化するため、勾配×(閾値/勾配のノルム)と計算する.
RNNの課題:入力された情報の影響が長期的に及ぶ場合と短期にしか及ばない場合を区別できない (LSTM).<順伝播の式>

# RNNレイヤの実装
class RNN:
    # 初期化メソッドの定義
    def __init__(self, Wx, Wh, b):
        self.params = [Wx, Wh, b] # パラメータ
        self.grads = [np.zeros_like(Wx), np.zeros_like(Wh), np.zeros_like(b)] # 勾配
        self.cache = None # 変数の保存用
    
    # 順伝播メソッドの定義
    def forward(self, x, h_prev):
        
        # パラメータを取得
        Wx, Wh, b = self.params
        
        # 順伝播を計算:式(5.10)
        t = np.dot(h_prev, Wh) + np.dot(x, Wx) + b # 重み付き和
        h_next = np.tanh(t) # 活性化
        
        # 逆伝播に用いる変数を保存
        self.cache = (x, h_prev, h_next)
        
        #隠れ層の状態の保存、隠れ層の出力を次時刻の入力に使用するため
        return h_next
    
    # 逆伝播メソッドの定義
    def backward(self, dh_next):
        # 変数を取得
        Wx, Wh, b = self.params # パラメータ
        x, h_prev, h_next = self.cache # データ
        
        # 勾配を計算
        dt = dh_next * (1 - h_next ** 2)
        db = np.sum(dt, axis=0)
        dWh = np.dot(h_prev.T, dt)
        dh_prev = np.dot(dt, Wh.T)
        dWx = np.dot(x.T, dt)
        dx = np.dot(dt, Wx.T)
        
        # 結果を格納
        self.grads[0][...] = dWx
        self.grads[1][...] = dWh
        self.grads[2][...] = db
        
        return dx, dh_prev

実装したクラスを確認する.

# インスタンスを作成
layer = RNN(Wx, Wh, b)

# 順伝播の計算
h1 = layer.forward(x1, h0)
print(h1.shape)

# 逆伝播の計算
dx1, dh0 = layer.backward(dh1)
print(dx1.shape)
print(dh0.shape)
for grad in layer.grads:
    print(grad.shape)
  • BPTT(BackPropagation Through Time)で時間展開した上での誤差逆伝播と同じ、一階微分である.
  • 全結合NNより、RNNは勾配消失や勾配爆発がおきやすい(系列長が長くなる).
  • 勾配消失や勾配爆発の起きやすさは、活性化関数による変わる.
  • 勾配爆発を防ぐため、ReLU関数よりも、Sigmoid関数やTanh関数が適している.→勾配消失の回避のため、勾配クリッピング

RNNでのSGDによって最適化を行う.最も単純に全時刻にわたって誤差逆伝搬を行うBPTTという手法がある.
サンプルコード

def bptt(xs, ys, W, U, V):
    hiddens, outputs = rnn_net(xs, W, U, V)
    dW = np.zeros_like(W)
    dU = np.zeros_like(U) 
    dV = np.zeros_like(V)
    do = _calculate_do(outputs, ys) #
    batch_size, n_seq = ys.shape[:2]
    for t in reversed(range(n_seq)):
          dV += np.dot(do[:,t].T, hiddens[:,t]) / batch_size
          delta_t = do[:, t].dot(V)
          for bptt_step in reversed(range(t+1)):
               dW += np.dot(delta_t.T, xs[:, bptt_step]) / batch_size
               dU += np.dot(delta_t.T, hiddens[:, bptt_step-1]) / batch_size
               delta_t = delta_t.dot(U) 
    # RNNでは中間層の出力h_tが過去の中間層出力 h_{t-1}...h_{1}に依存する.
    #   RNNにおいて損失関数をWとUに関して偏微分するとき、それを考慮する必要あり、 dh_{t} / dh_{t-1} = Uである 
    return dW,dU,dV
def rnn_net(xs, W, U, V):
    batch_size, n_seq = xs.shape[:2]
    hidden_size = W.shape[0]
    hiddens = np.zeros((batch_size, n_seq, hidden_size), dtype=xs.dtype)
    for t in xs.shape[1]:
         hiddens[:, t] = _activation(x[:, t].dot(W.T) + hiddens[:, t-1].dot(U.T)) 
    #入力xから中間層への重みW、1ステップ前の中間層出力h_{t-1}から中間層h_{t}への重みU、それを合わせた中間層出力
    outputs = _activations(hiddens.dot(V.T))
    return hiddens, outputs
def clip_grads(grads, max_norm):
     total_norm = 0
     for grad in grads: # Gradsは各パラメータの勾配をまとめたList
           total_norm += np.sum(grad ** 2) ##
     total_norm = np.sqrt(total_norm)  # 勾配のL2ノルムを取った値をまとめている変数
     rate = max_norm / (total_norm + 1e-6) 
     if rate < 1:
          for grad in grads:
               grad *= rate

RNN派生モデル<エコーステートネットワーク>
入力の重みUと隠れ層の重みVをランダム値で固定し、出力の重みのみを学習する。勾配消失が発生しにくく、学習が速くなる.<リーキーユニット>
隠れ層に線形結合を導入して移動平均の効果を得る.

双方向 RNN

文章内の文字の穴埋めタスクや過去から現在だけでなく未来から現在までの系列情報を用いることが有効である.
中間層の出力を、未来への順伝播と過去への逆伝播の両方向に伝播する.

同じ場所に同じものを合体する.一つの特徴が得られる.

def bindirectional_rnn_net(xs, W_f, U_f, W_b, U_b, V):
     xs_f = np.zeros_like(xs)
     xs_b = np.zeros_like(xs)
     for i, x in enumrate(xs):
          xs_f[i] = x
          xs_b[i] = x[ : :-1]
     hs_f = _rnn(xs_f, W_f, U_f)
     hs_b = _rnn(xs_b, W_b, U_b)
     hs = [np.concatenatie([h_f], h_b[::-1]], axis=1)] for h_f, h_b in zip(hs_f, hs_b)] ## 順方向と逆方向に伝播した時の中間層表現を合わせたものが特徴量となる
     ys = hs.dot(V.T)
     return ys

RNN Encoder-Decoder と Seq2Seq

長期依存性の課題

ゲート付きRNN

LSTM

構成要素:CEC、入力ゲート、出力ゲート、覗き穴結合

入力ゲート  i_t = \sigma (W_i X_t + U_i h_{t-1} +b_i) 
 W_i :今回の入力   X_t:今回の重み
 U_i :前回の出力   h_{t-1} :前回の重み

出力ゲート  O_t = \sigma (W_o X_t + U_o h_{t-1} +b_o)
 W_o :今回の入力   X_t:今回の重み
 U_o :前回の出力   h_{t-1} :前回の重み

忘却ゲート  f_t = \sigma (W_f X_t + U_f h_{t-1} +b_f)
 W_f :今回の入力   X_t:今回の重み
 U_f :前回の出力   h_{t-1} :前回の重み

入力と隠れ状態結合状態 
 Z_t = {\tanh} (W_g X_t + U_z h_{t-1} +b_z)

セルの値 
 C_t = f_t \circ C_{t-1} + i_t \circ z_t 今回覚えておくべき割合と判断した情報
 C_t = f_t \circ C_{t-1} + i_t \circ (W_g X_t + U_z h_{t-1}) (tanhは恒等写像に置き換え、バイアス省略)

出力値
 h_t = O_t \circ tanh(C_t)

CEC
中間層に記憶をため込む(記憶機能のみ).学習機能がない.
入力ゲート:CECの覚え方を身に付ける学習
出力ゲート:どんな感じでCECの情報を使うを学習
しかし、ずっと過去の情報が削除できないため、忘却ゲートを入れる

さらに、CEC自身の値は、ゲート制御に影響を与えないため、覗き穴結合を入れる.
  Z_t = {\tanh} (W_g X_t + U_z h_{t-1} +C^{t-1} W_c+b_g)
記憶セルの状態 C^{t-1}を結合させる.

サンプルコード

def lstm(x, h_prev, c_prev, W_in, W, b):
    n, m = h_prev.shape
    A= np.dot(x, Wx) + np.dot(h_prev,Wn) +b
    #全部の行と変数mの値分の列を指定
    f=A[ : ,     :    m]
    g=A[ : , m: 2*m]
    i=A[ : , 2*m: 3*m]
    o=A[ : , 3*m:     ]

    f = sigmoid(f) # forgetゲート
    g = np.tanh(g) # 新しい記憶ゲート
    i = sigmoid(i)  #inputゲート
    o = sigmoid(o) # outputゲート

    c = f * c_prev+ g * i #次の時刻の記憶セル
    h = o * np.tanh(c) #次の時刻の隠れ層におけるoutput

    return h, c

GRU

Gated Recurrent Unit
LSTMはゲートが3つで計算負荷が高い課題がある.
パラメータを大幅に削減し、精度はLSTMと同等.
記憶ゲートがなく、リセットゲートと更新ゲートがある.

GRU (Gated Recurrent Unit) | CVMLエキスパートガイド

リセットゲート r_t = \sigma (W_r X_t + U_r h_{t-1} +b_t)

更新ゲート  Z_t = \sigma (W_z X_t + U_z h_{t-1} +b_z)

次、仮の隠れ状態  \tilde{h_t} = {\tanh} (W_h X_t + U_h h_{t-1} r_t +b_t h_t)
出力値  h_t = Z_t \circ h_{t-1} + (1-Z_t) \circ \tilde{h_t}

前回:  Z_t \circ h_{t-1}
今回:  (1-Z_t) \circ \tilde{h_t}

サンプルコード

def gru(x, h_prev, W_in, W):
     n,m = W_in.shape
     W_in_z, W_in_r, W_in = W_in[:, :n], W_in[:, :n],W_in[:, 2*n:] 
     W_z, W_r, W = W[:, :n], W[:, n:2*n], W[:, 2*n:] 

     z = _sigmoid(np.dot(x, W_in_z) + np.dot(h_prev, W_z))  # updataゲートの出力値
     r = _sigmoid(np.dot(x, W_in_r) + np.dot(h_prev, W_r))  # resetゲートの出力値
     h_hat = np.tanh(np.dot(x, W_in) + np.dot(r*h_prev, W))  #過去の隠れ状態とリセットゲートの出力のアダマール積 r * h_prev
     h= (1-z) * h_prev + z * h_hat   #更新ゲートの出力を混合比とする過去の隠れ状態と仮の隠れ状態の混合が入る
     # or h_next = z*h_pre + (1-z) * h_hat 
     return h

長期依存性の最適化

Attention

  • Soft attention :

softmax関数で確率分布を求めてその確率分布を用いて重みつき平均を求める方法である.

  • Hard attention:

softmax関数で確率分布を求めてその確率分布に従って抽出された1点だけを得る方法である.


1.Source target attention: keyとValueがエンコーダ側の情報、Queryがデコーダ側の情報で分ける。2つの系列間の対応関係をとらえる.
2. Self attention:同じ情報源から計算するもの、同じ文章内の単語間の関係をとらえる.
3. Global attention:Attentionを使わない系列変換モデルでは長系列データ学習は難しい、エンコーダの初期に入力された情報がデコーダまで伝わりづらい
デコーダの対象時刻の隠れ層の出力」と「エンコーダの各時刻の隠れ層の出力」から重みベクトルを算出して、
その重みベクトルと掛け算して、Ctが得られる.<コード>
デコーダの隠れ状態のサイズ Nx H
h.reshape(N,1,H).repeat(T, axis=1)

アテンションの重みは N X Nとなる.
H方向に合計np.sum(t, axis=2)

参考文献
LSTM (Long Short-Term Memory) | CVMLエキスパートガイド