深層学習 学習まとめ: 再帰型ニューラルネットワーク

# 再帰型ニューラルネットワーク(RNN, Recurrent NN)

## 再帰型ニューラルネットワークの概念
- 時系列データに対応可能なニューラルネットワーク
    - 時系列データとは、時間的順序を追って一定間隔ごとに観察され、しかも相互に統計的依存関係が認められるようなデータの系列
        - 例えば、音声データ、株価データやテキストデータ
        - テキストデータは単語が前の単語とつながっていると考える
- 入力層〜中間層〜出力層というニューラルネットワークの基本構造は変わらないが、中間層同士に時間的なつながり(数珠つながり)を持つ
    - 時系列モデルを扱うには、初期の状態と過去の時間$ t-1 $の状態を保持し、そこから次の時間での$ t $を再帰的に求める再帰構造が必要となる
    - 中間層の出力を次の時点の中間層の入力として活用する
        - エルマンネットワーク
            - 出力層を戻すジョーダンネットワークというものも存在する
        - 入力$ x_n $と前の時点の中間層 $ z_{n-1} $から、新しい中間層 $ z_n $が得られ、そこから出力$ y_n $を得る
        - 重みは入力の重み$ W_{(in)} $と出力の重み$ W_{(out)} $に加え、前の中間層からの重み$ W $がある
- 数学的な記述
    - $ u^t = W_{(in)}x^t + Wz^{t-1} + b $
        - $x^t$はt時点の入力、$z^{t-1}$は前の中間層の出力
    - $ z^t = f(W_{(in)}x^t + Wz^{t-1} + b) $
        - $f$は中間層の活性化関数
    - $ v^t = W_{(out)}z^t + c $
    - $ y^t = g(W_{(out)}z^t + c) $
        - $g$は出力層の活性化関数
- 実装(バイアスb, cは省略)
    - 数式上は現在が$ t $、前が$ t-1 $だが、Pythonでの実装では$ t $が0から始まるため、現在が$ t+1 $、前が$ t $とする
```Python
u[:,t+1] = np.dot(X, W_in ) + np.dot(z[:,t].reshape(1, -1), W)
z[:,t+1] = functions.sigmoid (u[:,t+1])
y[:,t] = functions.sigmoid(np.dot(z[:,t+1].reshape(1, -1), W_out))
```

## BPTT(Back Propagation Through Time)
- RNNにおけるパラメーター調整方法の一種
    - 誤差逆伝播法の一種
- 数学的な記述
    - Wが3つあるので、3つの偏微分を行う
    - $ \dfrac{∂E}{∂W_{(in)}} = \dfrac{∂E}{∂u^t} \lbrack \dfrac{∂u^t}{∂W_{(in)}} \rbrack ^T = δ^t[x^t]^T$
        - $ δ $は、$E$から$u$までの微分をひとまとめにしたもの
        - RNNにおいて、$ \lbrack &ensp; \rbrack ^T $は時間を遡って全て微分することを表す
    - $ \dfrac{∂E}{∂W_{(out)}} = \dfrac{∂E}{∂v^t} \lbrack \dfrac{∂v^t}{∂W_{(out)}} \rbrack ^T = δ^{out,t}[z^t]^T$
    - $ \dfrac{∂E}{∂W} = \dfrac{∂E}{∂u^t} \lbrack \dfrac{∂u^t}{∂W} \rbrack ^T = δ^t[z^{t-1}]^T$
    - バイアスは2つ
    - $ \dfrac{∂E}{∂b} = \dfrac{∂E}{∂u^t} \dfrac{∂u^t}{∂b} = δ^t &ensp; as &ensp; \dfrac{∂u^t}{∂b} = 1 $
    - $ \dfrac{∂E}{∂c} = \dfrac{∂E}{∂v^t} \dfrac{∂v^t}{∂v} = δ^{out,t} &ensp; as &ensp; \dfrac{∂v^t}{∂c} = 1 $
- コードでの記述(バイアスb, cは省略)
```Python
np.dot(X.T, delta[:,t].reshape(1, -1))
np.dot(z[:,t+1].reshape(-1, 1), delta_out[:,t].reshape(-1,1))
np.dot(z[:,t].reshape(-1, 1), delta[:,t].reshape(1, -1))
```

- $δ^t$は、$E$から$u$までの微分をひとまとめにしたもの
    - $ δ^{t-n-1} = δ^{t-n}　\cdots $ は一つ前の時点とつながっていることを示す
```Python
delta_out[:,t] = functions.d_mean_squared_error(dd, y[:,t]) * functions.d_sigmoid(y[:,t])

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])
```

- パラメーターの更新式
```Python
# 勾配更新
W_in_grad += np.dot(X.T, delta[:,t].reshape(1,-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 -= learning_rate * W_in_grad
W_out -= learning_rate * W_out_grad
W -= learning_rate * W_grad
```
    - 時間的なつながりを辿る部分は$ \sum $がつく
- BPTTの全体像
    - $ z^t $を開くと$ z^{t-1} $が出てきて、$ z^{t-1} $を開くと$ z^{t-2} $が出てきてと、zが数珠つながりになっている(=再帰)

## LSTM(Long Short-Term Memory)
- RNNの課題
    - 時系列を遡れば遡るほど、勾配が消失していく
        - 結果的に長い時系列の学習が困難
    - 逆の勾配爆発という現象もある
        - 勾配が層を逆伝搬するごとに指数関数的に大きくなってしまい、結果的に学習がうまく行かない
- LSTMは構造自体を変えて勾配消失問題を解決するRNNの一種
- LSTMの構造
![20240215LSTM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3574945/cce8bd6d-870c-faec-f701-f0440653f580.png)

- CEC: Constant Error Carousel(記憶セル)
    - CECは記憶機能しかない(考える機能は持たない)
    - 勾配消失および勾配爆発の解決方法として、理屈上、勾配が1であれば解決できる
        - 勾配を1にするために導入されたのがCEC
    - 逆に勾配が1ということは、ニューラルネットワークの学習特性がないということ(覚えるが、学習しない)
        - CECの周りに学習機能を配置する
- 入力ゲートと出力ゲート
    - CECは記憶機能しか持たず、学習機能を持たないが、入力ゲートと出力ゲートを追加することにより、全体として学習機能を持たせる
        - 入力・出力ゲートを追加することで、それぞれのゲートへの入力値の重みを、重み行列$ \boldsymbol{W_x}, \boldsymbol{U_h} $で可変可能とする
    - 入力ゲートは入力された情報をどのように記憶するかをCECに指示する
    - 出力ゲートはCECが保管している情報(記憶)をどのように使うかを指示する
    - 今回の入力値と前回の出力値(隠れ層)をもとに、入力ゲート、出力ゲート共に学習する
        - 入力ゲートの重み$ \boldsymbol{W_x}^{(i)} $(今回の入力値に対する重み), $ \boldsymbol{U_h}^{(i)} $(前回の出力値に対する重み)を学習する
            - 入力ゲートの出力(どれだけCECに記憶させるか)$ \boldsymbol{I} $
    - 出力ゲートの重み$ \boldsymbol{W_x}^{(o)} $(今回の入力値に対する重み), $ \boldsymbol{U_h}^{(o)} $(前回の出力値に対する重み)を学習する
        - 出力ゲートの出力(CECの記憶をどう使うか)$ \boldsymbol{O} $
- 忘却ゲート
    - CECには過去の情報が全て保管されているが、過去の情報が入らなくなっても、削除することができず、保管され続ける
        - 忘却ゲートは、過去の情報が要らなくなった時点で、CECからその情報を忘却させる機能を持つ
        - 忘却ゲートの重み$ \boldsymbol{W_x}^{(f)} $(今回の入力値に対する重み), $ \boldsymbol{U_h}^{(f)} $(前回の出力値に対する重み)を学習する
            - 忘却ゲートの出力(どれだけCECに忘却させるか)$ \boldsymbol{F} $
- 処理の流れ
    - 学習されるLSTMのパラメーターは、入力に対する重み行列である$ \boldsymbol{W_x} = [\boldsymbol{W_x}^{(f)}, \boldsymbol{W_x}^{(g)}, \boldsymbol{W_x}^{(i)}, \boldsymbol{W_x}^{(o)}] $、前の時刻からの隠れ状態に対する重み行列である$ \boldsymbol{U_h} = [\boldsymbol{U_h}^{(f)}, \boldsymbol{U_h}^{(g)}, \boldsymbol{U_h}^{(i)}, \boldsymbol{U_h}^{(o)}] $、バイアスベクトルである$ \boldsymbol{B} = [\boldsymbol{b}^{(f)}, \boldsymbol{b}^{(g)}, \boldsymbol{b}^{(i)}, \boldsymbol{b}^{(o)}] $からなる
    - 入力と前の時刻からの隠れ状態から、忘却ゲートの値を計算する
        - $ \boldsymbol{F} = sigmoid(\boldsymbol{X_t} \boldsymbol{W_x}^{(f)} + \boldsymbol{H_{t-1}} \boldsymbol{U_h}^{(f)} + \boldsymbol{b}^{(f)}) $
            - ゲートは度合い(忘却ゲートの場合は、忘れる度合い)を調整するので、シグモイド関数で0〜1とする
    - 入力と前の時刻からの隠れ状態から、入力の値を計算する
        - $ \boldsymbol{G} = tanh(\boldsymbol{X_t} \boldsymbol{W_x}^{(g)} + \boldsymbol{H_{t-1}} \boldsymbol{U_h}^{(g)} + \boldsymbol{b}^{(g)}) $
            - 入力の値は情報量を一定程度維持しつつ勾配爆発を避けるために、tanh関数で-1〜1とする
    - 入力と前の時刻からの隠れ状態から、入力ゲートの値を計算する
        - $ \boldsymbol{I} = sigmoid(\boldsymbol{X_t} \boldsymbol{W_x}^{(i)} + \boldsymbol{H_{t-1}} \boldsymbol{U_h}^{(i)} + \boldsymbol{b}^{(i)}) $
            - ゲートは度合い(入力ゲートの場合は、入力を加える度合い)を調整するので、シグモイド関数で0〜1とする
    - 入力と前の時刻からの隠れ状態から、出力ゲートの値を計算する
        - $ \boldsymbol{O} = sigmoid(\boldsymbol{X_t} \boldsymbol{W_x}^{(o)} + \boldsymbol{H_{t-1}} \boldsymbol{U_h}^{(o)} + \boldsymbol{b}^{(o)}) $
            - ゲートは度合い(出力ゲートの場合は、出力を伝える度合い)を調整するので、シグモイド関数で0〜1とする
    - 前の時刻の記憶$ C_{t-1} $に対し、忘却ゲートの値(度合い)$ F $を作用させ、続いて入力の値$ G $に対し入力ゲートの値(度合い)$ I $を作用させたものを加え、現時点での記憶$ C_t $を得る
        - $ \boldsymbol{C_t} = \boldsymbol{F} \odot \boldsymbol{C_{t-1}} + \boldsymbol{I} \odot \boldsymbol{G} $
            - ゲートによる作用は、アダマール積であることに注意
    - 現時点での記憶$ C_t $をtanh関数に通した上で、出力ゲートの値(度合い)$ O $を作用させることで、現時点での隠れ状態 $ H_t $を得る
        - $ \boldsymbol{H_t} = \boldsymbol{O} \odot tanh(\boldsymbol{C_t}) $
            - 入力と同様に記憶も情報量を維持するためにtanh関数を使用する
            - ゲートによる作用は、アダマール積であることに注意
    - 実装に際しては、入力に対する重み行列である$ \boldsymbol{W_x} $、前の時刻からの隠れ状態に対する重み行列である$ \boldsymbol{U_h} $、バイアスベクトルである$ \boldsymbol{B} $それぞれを一まとまりの行列として$ \boldsymbol{X_t} \boldsymbol{W_x} + \boldsymbol{H_{t-1}} \boldsymbol{U_h} + \boldsymbol{b} $の計算を行い、結果を$ f, g, i, o $に分割する処理とすることが一般的
        - 前述のように、ゲート$ f, i, o $にはシグモイド関数を、$ g $にはtanh関数を適用し、$ F, G, I, O $を得る
- 覗き穴(peep hole)結合
    - CEC自体の値は、ゲート(入力/出力/忘却)制御に影響を与えていないが、与えることはできないか
        - 覗き穴結合はCEC自体の値を重み行列を介してゲートに伝播可能にした構造
        - 例えば、入力と前の時刻からの隠れ状態と、前の時刻の記憶から、忘却ゲートの値を計算する
            - $ \boldsymbol{F} = sigmoid(\boldsymbol{X_t} \boldsymbol{W_x}^{(f)} + \boldsymbol{H_{t-1}} \boldsymbol{U_h}^{(f)}　+ \boldsymbol{C_{t-1}} \odot \boldsymbol{V_c}^{(f)} + \boldsymbol{b}^{(f)}) $
                - 覗き穴の作用は、アダマール積であることに注意
        - ただし出力ゲートの計算においては、出力ゲートがCECより後ろに存在するので、現在の記憶($ C_t $)を作用させる
            - $ \boldsymbol{O} = sigmoid(\boldsymbol{X_t} \boldsymbol{W_x}^{(o)} + \boldsymbol{H_{t-1}} \boldsymbol{U_h}^{(o)}　+ \boldsymbol{C_{t}} \odot \boldsymbol{V_c}^{(o)} + \boldsymbol{b}^{(o)}) $
        - ただ実際にはあまり大きな効果は得られていない
- LSTMでは、勾配爆発を避けるために、勾配クリッピングを使うことがある
    - 大きい勾配に対し、ノルムを閾値以下に正規化する

## GRU(Gated Recurrent Unit)
- LSTMの改良版
    - LSTMではパラメーター数が多く、計算負荷が高くなる
    - GRUはパラメーターを減らすことによって、計算負荷が低い
- CEC、入力ゲート、出力ゲート、忘却ゲートがなく、その代わりに、リセットゲートと更新ゲートがある
- GRUの構造
![20240215GRU.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3574945/f3d95347-17a4-c544-a88b-479325257771.png)

- 処理の流れ
    - 学習されるGRUのパラメーターは、入力に対する重み行列である$ \boldsymbol{W_x} = [\boldsymbol{W_x}^{(r)}, \boldsymbol{W_x}^{(z)}, \boldsymbol{W_x}^{(\tilde{h})}] $、前の時刻からの隠れ状態に対する重み行列である$ \boldsymbol{U_h} = [\boldsymbol{U_h}^{(r)}, \boldsymbol{U_h}^{(z)}, \boldsymbol{U_h}^{(\tilde{h})}] $、バイアスベクトルである$ \boldsymbol{B} = [\boldsymbol{b}^{(r)}, \boldsymbol{b}^{(z)}, \boldsymbol{b}^{(\tilde{h})}] $からなる
    - 入力と前の時刻からの隠れ状態から、リセットゲートの値を計算する
        - $ \boldsymbol{R} = sigmoid(\boldsymbol{X_t} \boldsymbol{W_x}^{(r)} + \boldsymbol{H_{t-1}} \boldsymbol{U_h}^{(r)} + \boldsymbol{b}^{(r)}) $
            - ゲートは度合い(リセットゲートの場合は、前の隠れ状態を忘れる度合い)を調整するので、シグモイド関数で0〜1とする
    - 入力と前の時刻からの隠れ状態から、更新ゲートの値を計算する
        - $ \boldsymbol{Z} = sigmoid(\boldsymbol{X_t} \boldsymbol{W_x}^{(z)} + \boldsymbol{H_{t-1}} \boldsymbol{U_h}^{(z)} + \boldsymbol{b}^{(z)}) $
            - ゲートは度合い(更新ゲートの場合は、前の隠れ状態を混ぜる度合い)を調整するので、シグモイド関数で0〜1とする
    - 入力と前の時刻からの隠れ状態、およびリセットゲートの値から、仮の隠れ状態の値$ \boldsymbol{\tilde{H}} $を計算する
        - $ \boldsymbol{\tilde{H}} = tanh \lbrace \boldsymbol{X_t} \boldsymbol{W_x}^{(\tilde{h})} + (\boldsymbol{R} \odot \boldsymbol{H_{t-1}}) \boldsymbol{U_h}^{(\tilde{h})} + \boldsymbol{b}^{(\tilde{h})} \rbrace $
            - 入力の値は情報量を一定程度維持しつつ勾配爆発を避けるために、tanh関数で-1〜1とする
            - ゲートによる作用は、アダマール積であることに注意
    - 最後に、(1- 更新ゲートの値$ \boldsymbol{Z}$)と$\boldsymbol{H_{t-1}}　$、 $ \boldsymbol{Z}　$と$ \boldsymbol{\tilde{H}} $から、新たな隠れ状態の値$ \boldsymbol{H_t}　$を計算する
        - $ \boldsymbol{H_t} = (1 - \boldsymbol{Z}) \odot \boldsymbol{H_{t-1}} + \boldsymbol{Z} \odot \boldsymbol{\tilde{H}} $
            - ゲートによる作用は、アダマール積であることに注意
        - なお、$ 1- \boldsymbol{Z} $と$ \boldsymbol{Z} $をひっくり返した数式で記載されていることもあるので、注意が必要(基本的には上記が正しいと思われる)

## その他の派生RNNモデル
### エコーステートネットワーク
- 入力の重み$ \boldsymbol{U} $と、隠れ層の重み$ \boldsymbol{V} $をランダムな値で固定し、出力の重み$ \boldsymbol{W} $のみを学習する
    - シンプルな線形回帰の問題となり、勾配消失や勾配爆発が発生しなくなり、また、学習が速くなる
### スキップ接続
- 長期依存性の課題への対策の一つとして、隠れ層を1時刻以上スキップした接続を追加する
### Leaky unit
- 隠れ層に線型結合を導入して移動平均の効果を得る
    - $ h_t = (1 - \frac{1}{τ})h_{t-1} + \frac{1}{τ} f( \boldsymbol{W_h} h_{t-1} + \boldsymbol{W}_{in}x_t) $
### 双方向RNN
- 過去の情報だけでなく、未来の情報を加味することで、精度を向上させるためのモデル
    - 文章の推敲や、機械翻訳等に活用される
    - 文章は過去(文の前段)も未来(文の後段)も同時に情報が得られるので、過去から未来への情報の伝達だけでなく、未来から過去への情報の伝達も活用できる
- 時間に対し順方向に隠れ状態を伝播させる層と、逆方向に隠れ状態を伝播させる層は直接的には結合していない


# RNNの応用例
## seq2seq
- 2つのネットワークがつながれている自然言語処理用のネットワーク
    - 系列変換モデル: 単語列などの系列(sequence)を受け取り、別の系列へ変換するモデル
        - 時系列の入力をもとに、出力を時系列で予測する
    - 一つ目のネットワーク(Encoder)で文の意味(=文脈)を抽出・保持し、それを二つ目のネットワーク(Decoder)に渡す
    - seq2seqはEncoder-Decoderモデルの一種で、機械対話や機械翻訳などに使用される
- Encoder RNN
    - ユーザーがインプットしたテキストデータを、単語等のトークンに区切って渡す構造
    - Tokening: 文章を単語等のトークン毎に分割し、トークンごとのIDに分割する
        - IDはone hot vectorで表現できる
    - Embedding: IDから、そのトークンを表す分散表現ベクトルに変換
        - one hot vectorは数万列(単語数だけ)あるが、それを数百列の数字行列に圧縮する
            - 意味が似ている単語が似た行列になるようにする
            - 結果的に単語の意味を抽出していることになる
    - Encoder RNN: ベクトルを順番にRNNに入力していく
        - 処理手順
            1. vec1をRNNに入力し、hidden stateを出力する
            1. このhidden stateと次の入力vec2をまたRNNに入力し、結果のhidden stateを出力という流れを繰り返す
            1. 最後のvecを入れたときのhidden stateをfinal stateとしてとっておく
            1. このfinal stateがthought vectorと呼ばれ、入力した文の意味を表すベクトルとなる
        - 意味が似ている単語が似た行列になるようにする(=特徴量の抽出)という処理が難しい
            - GoogleのBERTはMLM(Masked Language Model)で
                - 単語に分解した上で、ある単語をマスク、前後の単語から予測させる
                - それによって、似た意味の単語が似たような意味ベクトルになるようになる
                - 人間が学習データを作る必要がないので、大量のデータを学習できる
- Decoder RNN
    - システムがアウトプットデータを単語等のトークンごとに生成する構造
    - 処理手順
        1. Decoder RNN: Encoder RNNのfinal state(thought vector)から、各tokenの生成確率を出力
        1. Sampling: 生成確率にもとづいてtokenをランダムに選ぶ
        1. Embedding: 2で選ばれたtokenをEmbeddingして Decoder RNNへの次の入力とする
        1. Detokenize: 1~3を繰り返し、2で得られたtokenを文字列に直す

## HRED
- seq2seqでは一問一答しかできない(それまでの文脈が活かされない)
- HREDでは、過去n-1個の発話から次の発話を生成する
    - 一文一文も単語と同様にseq2seqにかける
    - 文の意味ベクトルを次に引き継ぐ
        - seq2seqでは単語の意味ベクトルだったが、その上にさらに文レベルでのRNNを構築する
- つまり、HREDとは、seq2seq + Context RNN
    - Context RNN: Encoderのまとめた各文章の系列をまとめて、これまでの会話コンテキスト全体を表すベクトルに変換する構造
        - これによって、過去の発話の履歴を加味した返答をできる
- HREDの課題
    - HREDは確率的な多様性が字面にしかなく、会話の「流れ」のような多様性が無い
    - HREDは「うん」「そうだね」など、短く情報量に乏しい答えをしがちである
- VHRED
    - HREDに、VAE(Variational Auto Encoder)の潜在変数の概念を追加したもの
        - HREDの課題をVAEの潜在変数の概念を追加することで解決した構造
        - VAEについては、生成モデルを参照

## word2vec
- RNNでは、単語のような可変長の文字列をNNに与えることができず、固定長形式で単語を表す必要がある
- word2vecでは、学習データからボキャブラリーを作成
    - 辞書の単語数だけone-hot vectorができる
- これにボキャブラリ数 × 単語ベクトルの次元数の重み行列を導入することで、現実的な計算速度とメモリ量で大規模データの分散表現の学習を実現可能にした
- 埋め込み行列を獲得するための方法としてContinuous Bag-of-Words(CBOW)と、skip-gramの二種類のニューラルネットワークがある
    - CBOWは前後の単語からある単語を予測するNN
    - skip-gramはある単語が与えられた時に、その周辺の単語を予測するNN
- word2vecで巨大なコーパスを扱う場合、通常の他クラス分類を行うと計算速度が問題になるため、負例サンプリング(negative sampling)という手法で、損失関数の計算時間の短縮を図る
    - 通常のCBOWまたはskip-gramにおける出力層では、すべての語彙を対象として、ソフトマックス関数と交差エントロピー誤差関数の計算を行うが、これは非常に重くなる
    - 負例サンプリングを適用した場合の出力層では、1個の正例とk個の負例だけを対象として、シグモイド関数と交差エントロピー誤差関数の計算を行う
        - つまり膨大な多クラス分類問題をk+1個の2クラス分類問題としている
        - k個の負例は、コーパスの中から単語の出現頻度に基づいてサンプリングする
            - kはハイパーパラメーター

## Attention Mechanism
- seq2seqは、文章が長くなるほどそのシーケンスの内部表現の次元も大きくなっていくため、長い文章への対応が難しい
- Attention Mechanismは、「入力と出力のどの単語が関連しているのか」という関連性の重みを学習する仕組み
    - 一文の中で特に重要な単語を自力で見出す