書籍「15Stepで踏破 自然言語処理アプリケーション開発入門」をやってみる - 3章Step13メモ「Recurrent Neural Networks」

# 内容
[15stepで踏破 自然言語処理アプリケーション入門](https://www.amazon.co.jp/dp/4865941320) を読み進めていくにあたっての自分用のメモです。
今回は3章Step13で、自分なりのポイントをメモります。

# 準備
- 個人用MacPC：MacOS Mojave バージョン10.14.6
- docker version：Client, Server共にバージョン19.03.2

# 章の概要
前章では単語の分散表現を文に対応する形で並べた列を入力とした畳み込みニューラルネットワーク（CNN）を構築した。
この章では、同じく単語の分散表現を文に対応する形で並べた列を入力とした再帰型ニューラルネットワーク（RNN）を構築する。
仕組みの細かい部分の説明は省略されている。

# 13.1 Recurrent layer
## 1つ前の出力を次の出力に接続
多層パーセプトロンの1層分（全結合層）に、特徴ベクトルの一番左の列を入力とする。
次に入力するニューロンを右に一列ずらして、同様に全結合層に入力するが、ここで使う**全結合層の重みは1つ前で使ったものと同じ**にする。と同時に**1つ前の出力ニューロンを別の全結合層を介して**接続する。

- CNN：一連の出力をmax pooling layerに入力してベクトルを得ることにより、特徴ベクトルの全列の情報を含める
- RNN：1つ前の出力が次の出力に接続されているので、最後で得られた1つのベクトルが特徴ベクトルの全列の情報を含んでいる（ただし、最初の方の特徴が小さくなってしまうことに注意）

## RNNの別の表現
全結合層に、「その出力を自分に戻す接続」を加えたものを用意し、それに順々にベクトルを入力していく、という表現でも説明できる。
自分は元々こちらのイメージで、ループの部分を展開すると先ほどの構成になるイメージ。

# 13.2 LSTM
long short-term memoryの略で、RNNでは最初の方の特徴が小さくなってしまう問題があったが、古い情報も保持できるよう改良したものがLSTMである。（いずれLSTMについてもまとめたい）

# 13.3 KerasによるRNNの実装
前章(Step12)からの追加・変更点

- ニューラルネットワークの構造：CNN -> RNN
- sequence中の0の扱い：特別なし -> ゼロ埋めのための数字として特別扱いするようになる
    - embedding layerに続くレイヤーも対応している必要がある（LSTMは対応、CNNは未対応）

```python:rnn_sample.py
    model = Sequential()
    model.add(get_keras_embedding(we_model.wv,
                                  input_shape=(MAX_SEQUENCE_LENGTH, ),
                                  mask_zero=True,
                                  trainable=False))
    model.add(LSTM(units=256))
    model.add(Dense(units=128, activation='relu'))
    model.add(Dense(units=n_classes, activation='softmax'))
    model.compile(loss='categorical_crossentropy',
                  optimizer='rmsprop',
                  metrics=['accuracy'])
```

```bash:実行結果
# CNN
$ docker run -it -v $(pwd):/usr/src/app/ 15step:latest python cnn_sample.py
Epoch 50/50
917/917 [==============================] - 0s 303us/step - loss: 0.0357 - acc: 0.9924
0.6808510638297872

Epoch 100/100
917/917 [==============================] - 0s 360us/step - loss: 0.0220 - acc: 0.9902
0.6808510638297872

# LSTM
$ docker run -it -v $(pwd):/usr/src/app/ 15step:latest python rnn_sample.py
Epoch 50/50
917/917 [==============================] - 4s 4ms/step - loss: 0.2530 - acc: 0.9378
0.6063829787234043

Epoch 100/100
917/917 [==============================] - 4s 4ms/step - loss: 0.0815 - acc: 0.9793
0.5851063829787234

# Bi-directional RNN
$ docker run -it -v $(pwd):/usr/src/app/ 15step:latest python bid_rnn_sample.py
Epoch 50/50
917/917 [==============================] - 2s 2ms/step - loss: 0.2107 - acc: 0.9487
0.5851063829787234

Epoch 100/100
917/917 [==============================] - 2s 2ms/step - loss: 0.0394 - acc: 0.9858
0.5851063829787234

# GRU
Epoch 50/50
917/917 [==============================] - 1s 1ms/step - loss: 0.2947 - acc: 0.9368
0.4787234042553192

Epoch 100/100
917/917 [==============================] - 1s 1ms/step - loss: 0.0323 - acc: 0.9869
0.5531914893617021
```

Epoch数は50で比較。CNN以外はEpoch50でも損失関数が落ちきっていなかったので、Epoch100でも検証。

|NNの種類 |実行結果 |実行速度 |
|--- |--- |--- |
|CNN |Epoch50：68.1%<br>Epoch100：68.1% |平均300us/step -> 0.27s/epoch |
|LSTM |Epoch50：60.6%<br>Epoch100：58.5% |平均4ms/step -> 3.6s/epoch |
|Bi-directional RNN |Epoch50：58.5%<br>Epoch100：58.5% |平均2ms/step -> 1.8s/epoch |
|GRU |Epoch50：47.9%<br>Epoch100：55.3% |平均1ms/step -> 0.9s/epoch |

次章以降のハイパーパラメータの探索等ニューラルネットワークのチューニングは必要になるが、CNNは高速で識別率もなかなか良い。

- 通常実装(Step01)：37.2%
- 前処理追加(Step02)：43.6%
- 前処理＋特徴抽出変更(Step04)：58.5%
- 前処理＋特徴抽出変更＋識別器変更RandomForest(Step06)：61.7%
- 前処理＋特徴抽出変更＋識別器変更NN(Step09)：66.0%
- 前処理＋特徴抽出変更(Step11)：40.4%
- 前処理＋特徴抽出変更＋識別器変更CNN(Step12)：68.1%
- 前処理＋特徴抽出変更＋識別器変更RNN(Step13)：60.6%

# 13.4 まとめ
多層パーセプトロンと同様の全結合層を転用しただけの単純なRNNはうまく動作しないため、LSTMを導入した。

# 13.5 より発展的な学習のために
本書の3章の内容は初歩の初歩で、実応用のための利用方法にフォーカスしている。
理論的に深く理解するためには、ニューラルネットワークの理論をしっかり固めてから取り掛かるべきである。
Kaggleのコンペに挑戦するのもいいかも。