【Keras入門(5)】単純なRNNモデル定義

入門者に向けてKerasを使ったRNN(Recurrentニューラルネットワーク)の初歩を解説します。RNNは時系列データの予測やNLP(自然言語処理)などに強く、使いどころが多い便利な手法です。
[Google Colaboratory](https://colab.research.google.com/notebooks/welcome.ipynb?hl=ja)を使っているのでローカルでの環境準備すらしていません。Google Colaboratoryについては[「Google Colaboratory概要と使用手順(TensorFlowもGPUも使える)」](https://qiita.com/FukuharaYohei/items/18bc4465923bdda81826)の記事を参照ください。

以下のシリーズにしています。

-[ 【Keras入門(1)】単純なディープラーニングモデル定義](https://qiita.com/FukuharaYohei/items/579cc0184885b4ba75df)
- [【Keras入門(2)】訓練モデル保存(KerasモデルとSavedModel)](https://qiita.com/FukuharaYohei/items/ac6333391b8abda94bdc)
- [【Keras入門(3)】TensorBoardで見える化](https://qiita.com/FukuharaYohei/items/fe73cdb63ee82a7ea318)
- [【Keras入門(4)】Kerasの評価関数(Metrics)](https://qiita.com/FukuharaYohei/items/f7df70b984a4c7a53d58)
- 【Keras入門(5)】単純なRNNモデル定義 <- 本記事
- [【Keras入門(6)】単純なRNNモデル定義(最終出力のみ使用)](https://qiita.com/FukuharaYohei/items/39f865bb53cdd5052179)
- [【Keras入門(7)】単純なSeq2Seqモデル定義](https://qiita.com/FukuharaYohei/items/27cd247342a0f7006511)

##使ったPythonパッケージ
[Google Colaboratory](https://colab.research.google.com/notebooks/welcome.ipynb?hl=ja)でインストール済の以下のパッケージとバージョンを使っています。KerasはTensorFlowに統合されているものを使っているので、ピュアなKerasは使っていません。Pythonは3.6です。

- tensorflow: 1.14.0
- Numpy: 1.16.4
- matplotlib: 3.0.3

# 処理概要
時系列データ予測として、正弦関数(sine)の値を予測します。
[正弦関数](https://www.wdic.org/w/SCI/%E6%AD%A3%E5%BC%A6%E9%96%A2%E6%95%B0)は、こんなウネウネした値が変わっていくやつです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134273/3f15aada-ae1d-5bd4-a60f-060850079e6b.png)


以下の2種類の配列を作って、Kerasを使ったディープラーニングで予測モデルを作ります。

|種類|Shape|データ|
|:-:|:-:|:-:|
|説明変数(配列x_train)|10 X 40|xが-4.9から4.9までの0.2間隔の正弦関数の値|
|目的変数(配列y_train)|10 X 40|説明変数の1ずれた値|

言葉だとわかりにくいですが、表とグラフにするとこんな感じ。目的変数の次の値を予測しています(つまり時系列データ予測)。そして、2つ目のデータセットでは、1つ目のデータセットから1つずつずらしています。!
<img width="401" alt="50.Keras_RNN_list01.JPG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134273/b7e5f2e8-7ff8-1764-3a5a-f5aa976d4932.jpeg">

そして、訓練時のRNNへのデータの渡し方は下記のような形です。
<img wieth="401" alt="51.Keras_RNN_Overview01.JPG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134273/dbe9a031-e90a-d7ba-c395-cbac6da51c22.jpeg">

でもって、予測時は下記のように予測結果を次の予測データ元へ渡します。**モデルが予測した値を再度使うあたりが再帰(Recurrent)たる所以**ですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134273/21ac09da-7335-69d7-13fb-dc330e3ccac3.png)

# 処理プログラム
プログラム全体は[GitHub](https://github.com/YoheiFukuhara/keras-for-beginner/blob/master/RNN01_simple_RNN.ipynb)を参照ください。

## 1. ライブラリインポート
今回はmatplotlibとnumpyとtensorflowに統合されているkerasを使います。ピュアなkerasでも問題なく、インポート元を変えるだけです。

```python
import numpy as np
import matplotlib.pyplot as plt

# TensorFlowに統合されたKerasを使用
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, SimpleRNN
```

##2. 前処理 
###2.1. 正弦関数配列作成
-4.9から4.9までの50要素の等差数列(0.2間隔)の配列(`x_sin`)を作成し、対応する正弦関数の値の配列(`y_sin`)も
作成します。

```python
# -4.9から4.9までの50要素の等差数列(0.2間隔)
x_sin = np.linspace(-4.9, 4.9)
y_sin = np.sin(x_sin)
plt.plot(x_sin, y_sin)
plt.show()
```

matplotlibでグラフ出力するとこんなです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134273/6e5cfef5-9e8f-2ef3-5580-ae6a98f1254e.png)

###2.2. 説明変数(`x_train`)と目的変数(`y_train`)作成
RNNに流すための形に整形します。

```python
NUM_RNN = 10  # 1時系列のデータ数
NUM_DATA = len(x_sin) - NUM_RNN # 今回は40(=50-10)
x = []
y = []

for i in range(NUM_DATA):
    x.append(y_sin[i:i+NUM_RNN])      # 説明変数
    y.append(y_sin[i+1:i+NUM_RNN+1])  # 正解データなので1ずらした値
    
else:
    x_train = np.array(x).reshape(NUM_DATA, NUM_RNN, 1) # 入力を(サンプル数、時系列の数、入力層のニューロン数)にする
    y_train = np.array(y).reshape(NUM_DATA, NUM_RNN, 1) # 説明変数(x_train)と同様のshape
```

## 3. モデル定義
記事[「【Keras入門(1)】単純なディープラーニングモデル定義」](https://qiita.com/FukuharaYohei/items/579cc0184885b4ba75df#3-%E3%83%A2%E3%83%87%E3%83%AB%E5%AE%9A%E7%BE%A9)のときと違い、`simpleRNN`を使っています。実際にはLSTMやGRUなどを使うことが多いかと思いますが、今回は`simpleRNN`で十分な精度が出ます。また、LSTMやGRUを使う場合も呼び出し方はほとんど変わりません。

```python
NUM_DIM = 8  # 中間層の次元数

model = Sequential()

# return_sequenceがTrueなので全RNN層が出力を返す(Falseだと最後のRNN層のみが出力を返す)
model.add(SimpleRNN(NUM_DIM, input_shape=(NUM_RNN, 1), return_sequences=True))
model.add(Dense(1, activation="linear"))  #全結合層
model.compile(loss="mean_squared_error", optimizer="sgd")
model.summary()
```

`summary`関数で以下のようなモデルサマリを出してくれます。

```bash
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
simple_rnn (SimpleRNN)       (None, 10, 8)             80        
_________________________________________________________________
dense (Dense)                (None, 10, 1)             9         
=================================================================
Total params: 89
Trainable params: 89
Non-trainable params: 0
_________________________________________________________________
```

##4. 訓練実行
[fit関数](https://keras.io/ja/models/model/#fit)を使って訓練実行です。20epoch程度でそこそこいい精度が出ます。

```python
history = model.fit(x_train, y_train, epochs=20, batch_size=8)

# Lossをグラフ表示
loss = history.history['loss']
plt.plot(np.arange(len(loss)), loss) # np.arangeはlossの連番数列を生成(今回はepoch数の0から19)
plt.show()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134273/f1185450-94dd-4acf-229e-7c751624d958.png)


## 5. テスト
最後にテストです。

## 5.1. テストデータ作成
訓練データの最初の10件をテストデータとします。

```python
# x[0]は最初の入力(時系列10個の数)。reshape(-1)で一次元のベクトルにする。
x_test = x_train[0].reshape(-1)  
```

##4.2. 訓練済モデルを使ったテストデータの答え合わせ
[predict関数](https://keras.io/ja/models/model/#predict)を使ってテストデータから予測値を出力し、さらに次の予測の元データとします。

```python
# データ数(40回)ループ
for i in range(NUM_DATA):
    y_pred = model.predict(x_test[-NUM_RNN:].reshape(1, NUM_RNN, 1))  # 直近データ(最後から10要素)を使って予測
    x_test = np.append(x_test, y_pred[0][NUM_RNN-1][0])  # 出力結果をx_testに追加(n_rnn-1が10番目を意味している)

# 最初の10要素は完全に同じ
plt.plot(x_sin, y_sin, label="Training data")
plt.plot(x_sin, x_test, label="Predicted")
plt.legend()
plt.show()
```

予測値と訓練データを比較でグラフ表示してみます。-4.9から-2.9までの値は完全に同じです。それ以降は、微妙にずれてはいますが、それっぽい値が出ています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134273/c242e88a-9ae6-565b-0f4a-cd627ed7816d.png)

# 簡単な変更でモデルを複雑に変更
simpleRNNではなく、実務上はLSTMを使うことが多いはずです。
その場合、モデルの層をこんな感じにするだけです。
GPU向けの[CuDNNLSTM](https://keras.io/ja/layers/recurrent/#cudnnlstm)もあります(確かモデル間の互換性がなかったような気がします(未確認))。

```python
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, LSTM

model.add(LSTM(NUM_DIM, input_shape=(NUM_RNN, 1), return_sequences=True))
```

GRUだとこう。GPU向けの[CuDNNGRU](https://keras.io/ja/layers/recurrent/#cudnngru)もあります(確かモデル間の互換性がなかったような気がします(未確認))。

```python
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, GRU

model.add(GRU(NUM_DIM, input_shape=(NUM_RNN, 1), return_sequences=True))
```

双方向にしたい場合は[`Bidirectional`](https://keras.io/ja/layers/wrappers/#bidirectional)を使います。

```python
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, simpleRNN, Bidirectional

model.add(Bidirectional(SimpleRNN(NUM_DIM, return_sequences=True), input_shape=(NUM_RNN, 1)))
```

複数層に重ねることもできます。

```python
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, simpleRNN

model.add(SimpleRNN(NUM_DIM, input_shape=(NUM_RNN, 1), return_sequences=True))
model.add(SimpleRNN(NUM_DIM, input_shape=(NUM_RNN, 1), return_sequences=True))
```