KerasでLSTM AutoEncoderを実装する

# 1.はじめに

この記事は[KerasのLSTMのフィードフォワードをnumpyで実装する](http://qiita.com/kazetof/items/8e93c35a26ac46b56a6c)の続きみたいなものです．
KerasでLSTM AutoEncoderを実装し，得られた特徴量から2値分類を試します．
データは，周波数の異なる2つのsin波を生成し，それを識別します．

# 2.環境
- Python 3.6.1 :: Anaconda 4.4.0 (64-bit)
- Keras 2.0.5 (backend : tensorflow)

# 3.データの作成
## 3.1 概要
データの生成は以下の記事を参考にしています．ありとうございます．
- [深層学習ライブラリKerasでRNNを使ってsin波予測](http://qiita.com/yukiB/items/5d5b202af86e3c587843)
- [RNNにsin波を学習させて予測してみた](http://qiita.com/yuyakato/items/ab38064ca215e8750865)

今回は2値分類をするため，データの生成方法を少し変更しています．
周波数の異なるsin波を二つ準備し，それを識別させます．

## 3.2 コード
今回使うデータを生成するコードです．

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

def make_data(random_factor, number_of_cycles, \
                timesteps, sampling_num_pair):
    """
    sampling_num_pair : 2 elements of tupple
        1周期の間にサンプリングする数
        Ex. (20,80)
    """
    def _load_data(data, n_prev = 100):  
        """
        data should be pd.DataFrame()
        """
        docX, docY = [], []
        for i in range(len(data)-n_prev):
            docX.append(data.iloc[i:i+n_prev].as_matrix())
        alsX = np.array(docX)

        return alsX

    def _train_test_split(df, test_size=0.1, n_prev = 100):  
        """
        This just splits data to training and testing parts
        """
        ntrn = round(len(df) * (1 - test_size))
        ntrn = int(ntrn)
        X_train = _load_data(df.iloc[0:ntrn], n_prev)
        X_test = _load_data(df.iloc[ntrn:], n_prev)

        return X_train, X_test

    np.random.seed(0)

    sampling_num1, sampling_num2 = sampling_num_pair

    df1 = pd.DataFrame(np.arange(sampling_num1 * number_of_cycles + 1), columns=["t"])
    df1["sin_t"] = df1.t.apply(lambda x: np.sin(x * (2 * np.pi / sampling_num1)+ np.random.uniform(-1.0, +1.0) * random_factor))

    df2 = pd.DataFrame(np.arange(sampling_num2 * number_of_cycles + 1), columns=["t"])
    df2["sin_t2"] = df2.t.apply(lambda x: np.sin(x * (2 * np.pi / sampling_num2)+ np.random.uniform(-1.0, +1.0) * random_factor))

    X_train1, X_test1 = _train_test_split(df1[["sin_t"]], n_prev=timesteps) 
    X_train2, X_test2 = _train_test_split(df2[["sin_t2"]], n_prev=timesteps) 

    # concatenate　X and make y
    X_train = np.r_[X_train1, X_train2]
    y_train = np.r_[np.tile(0, X_train1.shape[0]), np.tile(1, X_train2.shape[0])]

    X_test = np.r_[X_test1, X_test2]
    y_test = np.r_[np.tile(0, X_test1.shape[0]), np.tile(1, X_test2.shape[0])]

    return X_train, y_train, X_test, y_test

# 乱数の係数
random_factor = 0.05
# 生成するサイクル数
number_of_cycles = 200
# 1サイクルの中で，何点サンプリングするか．
sampling_num_pair=(20,80)
# windowの長さ．一つの系列の長さになる．
timesteps = 100

X_train, y_train, X_test, y_test = make_data(random_factor, number_of_cycles, timesteps, sampling_num_pair)
print("X_train.shape : ", X_train.shape) #X_train.shape :  (17802, 100, 1)
print("y_train.shape : ", y_train.shape) #y_train.shape :  (17802,)
print("X_test.shape : ", X_test.shape) #X_test.shape :  (1800, 100, 1)
print("y_test.shape : ", y_test.shape) #y_test.shape :  (1800,)
```

## 3.3 プロット
生成したsin波をプロットしてみます．

```python
# make_data関数のdf1, df2を抜き出してプロットする．
fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot(df1["sin_t"][0:80], label="class0 sampling_num 20", color="red")
ax.plot(df2["sin_t2"][0:80], label="class1 sampling_num 80", color="blue")
ax.legend(loc="upper right")
fig.savefig("./sin_plot.png")
plt.close("all")
```

![sin_plot.png](https://qiita-image-store.s3.amazonaws.com/0/164264/f7faae68-dd2a-791b-0692-c0207aabcab2.png "sin_plot.png")

class0はclass1の4倍の周波数であることがわかります．

# 4.model構築
## 4.1 概要
モデルの構築には[The Keras Blog](https://blog.keras.io/building-autoencoders-in-keras.html)を参考にしました．Sequence-to-sequence autoencoderの章にヒントっぽいものがあったのでそれに適宜追加しています．

元になる論文は[Unsupervised Learning of Video Representations using LSTMs](https://arxiv.org/abs/1502.04681)です．この論文ではcondtional（decode時に入力を逆順にしたものを渡す）とunconditinal（何も渡さない）を提案していますが，今回はcondtionalの方を実装しています．

## 4.2 コード
LSTM AutoEncoderのモデルを定義します．
ついでに訓練させて，元のX_trainを再構成します．

```python
from keras.layers import Input, LSTM, RepeatVector, concatenate, Dense
from keras.models import Model

input_dim = 1
latent_dim = 10

# encode
inputs = Input(shape=(timesteps, input_dim))
encoded = LSTM(latent_dim, activation="tanh", recurrent_activation="sigmoid", return_sequences=False)(inputs)

#decode
hidden = RepeatVector(timesteps)(encoded)
reverse_input = Input(shape=(timesteps, input_dim))
hidden_revinput = concatenate([hidden, reverse_input])
decoded = LSTM(latent_dim, activation="tanh", recurrent_activation="sigmoid", return_sequences=True)(hidden_revinput)
decoded = Dense(latent_dim, activation="relu")(decoded)
decoded = Dense(input_dim, activation="tanh")(decoded)

# train
LSTM_AE = Model([inputs, reverse_input], decoded)
LSTM_AE.compile(optimizer='rmsprop', loss='mse')
X_train_rev = X_train[:,::-1,:]
LSTM_AE.fit([X_train, X_train_rev], X_train, epochs=30, batch_size=500, shuffle=True, validation_data=([X_train, X_train_rev], X_train))

X_hat = LSTM_AE.predict([X_train, X_train_rev])
```
このコードではdecode時にフルコネクト層を追加していますが，特に深い意味はありません．Kerasすげえ！こんな簡単に層を追加できるんだ！と遊んでいた結果です．

次に，class0とclass1について，どのようにAutoEncoderで再構成されているか確認するため，classごとに分割します．

```python
def split_X(X, y):
    y_inv = np.abs(y - 1.)
    X_0 = X[y_inv.astype(bool),:,:]
    X_1 = X[y.astype(bool),:,:]
    return X_0, X_1

X_train_0, X_train_1 = split_X(X_train, y_train)
X_hat_0, X_hat_1 = split_X(X_hat, y_train)

print("X_train_0.shape : ", X_train_0.shape) #X_train_0.shape :  (3501, 100, 1)
print("X_train_1.shape : ", X_train_1.shape) #X_train_1.shape :  (14301, 100, 1)
print("X_hat_0.shape : ", X_hat_0.shape) #X_hat_0.shape :  (3501, 100, 1)
print("X_hat_1.shape : ", X_hat_1.shape) #X_hat_1.shape :  (14301, 100, 1)
```

## 4.3 プロット
AutoEncoderで再構成されたものをプロットして確認します．

```python
# reconstruct したX_trainがどんな感じか見てみる
def plot_save(start_index, X_hat, X_train, X_class):
    fig = plt.figure()
    ax = fig.add_subplot(111)
    for i in np.arange(start_index,start_index+5):
        #５個ずつプロットする．
        ax.plot(X_hat[i,:,0], label="X hat", color="red")
        ax.plot(X_train[i,:,0], label="X train", color="blue")
    savename = "./AE_reconst_30ep_start_" + str(start_index) + "_cls" + str(X_class) + ".png"
    fig.savefig(savename)
    plt.close("all")

start_list = np.arange(0,len(X_train_0), 1000)
for start_index in start_list:
    plot_save(start_index, X_hat_0, X_train_0, X_class=0)

start_list = np.arange(0,len(X_train_1), 1000)
for start_index in start_list:
    plot_save(start_index, X_hat_1, X_train_1, X_class=1)
```

### class0
![AE_reconst_30ep_start_0_cls0.png](https://qiita-image-store.s3.amazonaws.com/0/164264/0a8e68c4-b034-fbb5-42ef-0ead0ffe59f9.png "AE_reconst_30ep_start_0_cls0.png")

### class1
![AE_reconst_30ep_start_6000_cls1.png](https://qiita-image-store.s3.amazonaws.com/0/164264/278ca849-1563-4071-057b-d501c8eb3d36.png "AE_reconst_30ep_start_6000_cls1.png")

青が元のデータ，赤が再構築したものです．
これらは，何枚もプロットした中で適当に選んだものです．どれもだいたいこんな感じでした．

ちなみにまだ収束していない様子だったので，epochを増やせばもっと良い近似になると思いますが，マシンスペック的にまあこのくらいでいいだろうということにしました．ちなみに30epochで15分ほどでした．（もっと頑張れよと言われそう）

# 5.獲得した特徴量の抽出
## 5.1 概要
このへんは[前回の記事](http://qiita.com/kazetof/items/8e93c35a26ac46b56a6c)と同じです．
encoderで得た特徴量をmodelから取り出します．

## 5.2 コード
特徴量を抜き出すのに必要な関数を定義します．

```python

def split_params(W, U, b):
    Wi = W[:,0:latent_dim]
    Wf = W[:,latent_dim:2*latent_dim]
    Wc = W[:,2*latent_dim:3*latent_dim]
    Wo = W[:,3*latent_dim:]

    print("Wi : ",Wi.shape)
    print("Wf : ",Wf.shape)
    print("Wc : ",Wc.shape)
    print("Wo : ",Wo.shape)

    Ui = U[:,0:latent_dim]
    Uf = U[:,latent_dim:2*latent_dim]
    Uc = U[:,2*latent_dim:3*latent_dim]
    Uo = U[:,3*latent_dim:]

    print("Ui : ",Ui.shape)
    print("Uf : ",Uf.shape)
    print("Uc : ",Uc.shape)
    print("Uo : ",Uo.shape)

    bi = b[0:latent_dim]
    bf = b[latent_dim:2*latent_dim]
    bc = b[2*latent_dim:3*latent_dim]
    bo = b[3*latent_dim:]
    print("bi : ",bi.shape)
    print("bf : ",bf.shape)
    print("bc : ",bc.shape)
    print("bo : ",bo.shape)

    return (Wi, Wf, Wc, Wo), (Ui, Uf, Uc, Uo), (bi, bf, bc, bo)


def calc_ht(params):
    x, latent_dim, W_, U_, b_ = params
    Wi, Wf, Wc, Wo = W_
    Ui, Uf, Uc, Uo = U_
    bi, bf, bc, bo = b_ 
    n = x.shape[0]

    ht_1 = np.zeros(n*latent_dim).reshape(n,latent_dim) #h_{t-1}を意味する．
    Ct_1 = np.zeros(n*latent_dim).reshape(n,latent_dim) #C_{t-1}を意味する．

    ht_list = []

    for t in np.arange(x.shape[1]):
        xt = np.array(x[:,t,:])
        it = sigmoid(np.dot(xt, Wi) + np.dot(ht_1, Ui) + bi)
        Ct_tilda = np.tanh(np.dot(xt, Wc) + np.dot(ht_1, Uc) + bc)
        ft = sigmoid(np.dot(xt, Wf) + np.dot(ht_1, Uf) + bf)
        Ct = it * Ct_tilda + ft * Ct_1
        ot = sigmoid( np.dot(xt, Wo) + np.dot(ht_1, Uo) + bo)
        ht = ot * np.tanh(Ct)
        ht_list.append(ht)
        ht_1 = ht
        Ct_1 = Ct

    ht = np.array(ht)
    return ht
    
def sigmoid(x):
    return 1.0 / (1.0 + np.exp(-x))
        
```

実際に抜き出します．

```python
W, U, b = LSTM_AE.layers[1].get_weights()
Ws, Us, bs = split_params(W, U, b)

params = [X_train, latent_dim, Ws, Us, bs]
ht_train = calc_ht(params)

params = [X_test, latent_dim, Ws, Us, bs]
ht_test = calc_ht(params)
```

LSTM_AE.layers[1]はencodeのLSTM層です．そこから重みを抜き出しています．

## 5.3プロット

ht_trainは ```(17802, 10)``` の行列で，10次元の特徴量になっています.
元の次元が```timesteps = 100```で設定した100次元なので，100次元から10次元に圧縮されています．
この10次元の特徴量をとりあえずプロットしてみましょう．

### class0
![hidden_two_class0_2.png](https://qiita-image-store.s3.amazonaws.com/0/164264/2d87850d-b232-3a0e-95c6-4d440a637d40.png "hidden_two_class0_2.png")

### class1
![hidden_two_class1_4.png](https://qiita-image-store.s3.amazonaws.com/0/164264/5ba7b6c2-59cc-ca27-217b-8be740f1a723.png "hidden_two_class1_4.png")

なんだかよく分からないですね．周期的な信号なので，1周期の特徴量の動きが全て表示されるようにプロットしています．全体的な挙動としてはだいたい似ているけれども，まあ一個ずつみたら違うかもなあと言う感じですね．（大変なので細かくはみない）

# 6.獲得した特徴量で識別

このLSTM AutoEncoderによって獲得した特徴量によってclass0とclass1を分類してみましょう．とりあえずここでは，iidじゃないじゃんと言うことは置いておいて，ロジスティック回帰を試します．

```python
from sklearn.linear_model import LogisticRegression 
from sklearn.metrics import accuracy_score
model = LogisticRegression(n_jobs=-1)
model.fit(ht_train, y_train)
y_hat_test = model.predict(ht_test)
accuracy_score(y_hat_test, y_test)
# 1.0
```

テストデータに対するaccuracyが1.0になりました．高すぎて逆に心配なのですが，かなり波形の違う信号でしたしこんなものなんでしょうか．

# 7.終わりに
- Kerasで初めてモデル構築をしたが，高レイヤーすぎて中で実際にやっていることが自分のやりたいこととあっているのか心配になる．結果だけ見ると期待通りになっているので，できていると信じたい．（誰か教えてください）
- これの元の論文もそうだが，そこで提案されているモデルが数式で書かれていなかったりして，機械学習界隈の人はどうやって理解したり実装したりしているのだろうか．computational graphという考え方が広がっていて，図によって伝達されているのだろうか．それとも，論文読んだだけではモデルが想像できず，再現性がないと問題になっているのだろうか．よく分からない．
- Kerasはtendorflowのラッパーなので当然なのだが，tensolflow触ったことがない僕のような人間は困難にぶち当たるのでとりあえずtensorflowを触るのがいいかもしれない．