LSTM/RNNの実装

通常のニューラルネットワーク

一般のニューラルネットワークにおける順伝搬は下記のように表せる。
 h^l=f^l(W^lh^{l-1}+b^l)
下図のニューラルネットワークでは、下記の式になる。
 h=f(W^{(1)}x+b{(1)})
 y=g(W^{(2)}h+b{(2)})
f:id:tibet:20211226102409p:plain
 u=W^{(1)}x+b^{(1)}
 v=W~{(2)}h+b~{(2)}
と置けば、下記のように書ける。
 h=f(u)...(1)
 y=g(v)...(2)

誤差逆伝搬法

損失関数をLとすると、誤差の勾配は(1), (2)式を用いて下記のように書ける。
 δ^{(v)}=\dfrac{\partial L}{\partial v}=\dfrac{\partial L}{\partial y}\dfrac{\partial y}{\partial v}=\dfrac{\partial L}{\partial y}g'(v)
 δ^{(u)}=\dfrac{\partial L}{\partial u}=\dfrac{\partial L}{\partial v}\dfrac{\partial v}{\partial h}\dfrac{\partial h}{\partial u}=(W^{(2)})^Tδ^{(2)}f'(u)

各パラメータの勾配は下記のようになる。
 \dfrac{\partial L}{\partial W^{(2)}}=\dfrac{\partial L}{\partial v}\dfrac{\partial v}{\partial W^{(2)}}=δ^{(v)}\bigotimes h
 \dfrac{\partial L}{\partial b^{(2)}}=\dfrac{\partial L}{\partial v}\dfrac{\partial v}{\partial b^{(2)}}=δ^{(v)}
 \dfrac{\partial L}{\partial W^{(1)}}=\dfrac{\partial L}{\partial u}\dfrac{\partial u}{\partial W^{(1)}}=δ^{(u)}\bigotimes x
 \dfrac{\partial L}{\partial b^{(1)}}=\dfrac{\partial L}{\partial v}\dfrac{\partial v}{\partial b^{(1)}}=δ^{(u)}

 \bigotimesテンソル積。

RNN

  • 過去の隠れ層の状態も入力につかう
  • 系列データを扱うのに適したニューラルネットワークである。
    • 過去の情報を保持する
    • 可変長入力

f:id:tibet:20211125172417p:plain

RNNの順伝搬

 h_t=f(W^{(1)}x_t+Uh_{t-1}+b^{(1)})
 y_t=g(W^{(2)}h_t+b^{(2)})

RNNの誤差逆伝搬

  • BPTT

時間展開したうえでの誤差逆でパン

  • Truncated BPTT

遡るステップ数を限定したBPTT

RNNの問題

  • 勾配消失の問題があり、長期的な依存関係を学習できないことである。

 δ^{(u)}=\dfrac{\partial L_t}{\partial u_{t-1}}=\dfrac{\partial L_t}{\partial u_t}\dfrac{\partial u_t}{\partial h_{t-1}}\dfrac{\partial h_{t-1}}{\partial u_{t-1}}=U_Tδ_t^{(u)}f'(h_{t-1})
上記のように勾配計算に活性化関数の微分値が残る。

LSTM

RNNの問題を解決し、長期的な依存関係を学習できるようにしたものがLSTMである。
セルとゲートを導入した。
f:id:tibet:20211129195005p:plain

LSTMの誤差逆伝搬

  • RNNと同じくBPTT
  • RNNに比べてパラメータが多い
  • 勾配爆発を避けるために勾配クリッピングがある
  • 大きい勾配に対してノルムを閾値以下に正規化

LSTMが勾配消失を避けられる理由

cについての勾配は、1ステップ前においても活性化関数の微分を通さず、忘却ゲートの値倍となる。
忘却ゲートの値が1に近ければ、誤差は消失しない。
 δ_t^{(c)}=\dfrac{\partial L_t}{\partial c_t}
 δ_{t-1}^{(c)}=\dfrac{\partial L_t}{\partial c_{(t-1)}=δ_t^{(c)}f_t]

主要フレームワークAPI

RNNのAPI(TensorFlow)

  • tf.contrib.rnn.BasicRNNcell(num_units, activation=tanh)

RNNセルを生成

  • tf.contrib.rnn.BasicLSTMCell(num_units,forget_bias=1.0,activation=tanh)

LSTMセルを生成

  • tf.contrib.rnn.static_rnn(cell, inputs,initial_state=None)

時間方向に展開。出力と最後の内部状態を返す。

LSTMのAPI(Chainer)

  • chainer.links.LSTM(in_size,out,forget_bias_init=1.0)

LSTM層を生成
__call__(x)
内部情報を更新
 LSTMの出力を返す

実装演習

TensorFlowによるRNN・LSTMの実装

環境準備

%tensorflow_version 1.x
import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt
% matplotlib inline
import seaborn as sns
sns.set()

学習用データを設定する

# sin曲線+ノイズ
ts = np.linspace(0, 10 * np.pi, 500)
ys = np.sin(ts) + np.random.normal(scale=0.05, size=len(ts))
plt.plot(ts, ys)

f:id:tibet:20211226120327p:plain
学習データの設定(n_stepsごとに分割した訓練・テストデータの準備)

# 学習設定
batch_size = 32    # ミニバッチサイズ
n_steps = 50       # 入力系列の長さ
input_size = 1     # 入力の次元
hidden_size = 50   #  中間層の次元
output_size = 1    # 出力層の次元

lr = 0.005         # 学習率(SGD)
n_iter = 500       # イテレーション回数
# 訓練データとテストデータに分割
train_ratio = 0.8
data = []
for i in range(len(ys) - n_steps - 1):
    data.append(ys[i: i+n_steps+1])
data = np.array(data, dtype=np.float32)
n_train = int(train_ratio * len(data))
x_train, y_train = np.split(data[:n_train], [-1], axis=1)
x_test, y_test = np.split(data[n_train:], [-1], axis=1)

x_train = np.reshape(x_train, [-1, n_steps, input_size])
x_test = np.reshape(x_test, [-1, n_steps, input_size])

モデル構築

def inference(x):
    """
    推論(順伝播)
    x: (batch_size, n_steps, input_size)
    """
    # static_rnn関数の引数に適切な形に変形
    x = tf.transpose(x, [1, 0, 2])  # (n_steps, batch_size, input_size)
    x = tf.reshape(x, [-1, input_size])  # (n_steps*batch_size, input_size)
    x = tf.split(x, n_steps, axis=0)  # [(batch_size, input_size)] * n_steps
    
    # RNN(LSTM)セルを定義
    rnn_cell = tf.contrib.rnn.BasicRNNCell(hidden_size)
#     rnn_cell = tf.contrib.rnn.BasicLSTMCell(hidden_size, forget_bias=1.0)
    
    # RNNセルを時間方向に伝播
    hs, _ = tf.contrib.rnn.static_rnn(rnn_cell, x, dtype=tf.float32)
    
    # 出力層の定義
    W_out = tf.Variable(tf.random_normal([hidden_size, output_size]))
    b_out = tf.Variable(tf.random_normal([output_size]))
    return tf.matmul(hs[-1], W_out) + b_out
tf.reset_default_graph()

# 入出力プレースホルダーの定義
x = tf.placeholder("float", [None, n_steps, input_size])
y = tf.placeholder("float", [None, output_size])

# オペレーションの定義
pred = inference(x)
loss = tf.losses.mean_squared_error(y, pred)
# train_step = tf.train.GradientDescentOptimizer(learning_rate=lr).minimize(loss)
train_step = tf.train.AdamOptimizer().minimize(loss)

学習

init = tf.global_variables_initializer()
# セッション
sess = tf.Session()
# 変数初期化
sess.run(init)

# 訓練データのインデックスをランダムに
perm = np.random.permutation(len(x_train))

for i in range(n_iter):
    idx = (i * batch_size) % len(x_train)
    batch_x, batch_y = x_train[perm[idx: idx+batch_size]], y_train[perm[idx: idx+batch_size]]
    
    feed_dict = {x: batch_x, y: batch_y}
    sess.run(train_step, feed_dict=feed_dict)
    if i % 50 == 0:
        l = sess.run(loss, feed_dict=feed_dict)
        print("step: {}, loss {:.5f}".format(i, l))

結果

step: 0, loss 2.82473
step: 50, loss 0.01717
step: 100, loss 0.02267
step: 150, loss 0.01304
step: 200, loss 0.01130
step: 250, loss 0.01892
step: 300, loss 0.00806
step: 350, loss 0.00597
step: 400, loss 0.00722
step: 450, loss 0.00508

テストデータに対する予測

# テストデータに対する予測
prediction = sess.run(pred, feed_dict={x: x_test})

# 1次元配列に
prediction = prediction.reshape(-1)
true_y = y_test.reshape(-1)
# テストデータに対する予測を可視化
xx = np.arange(len(prediction))
plt.plot(xx, true_y, label='true')
plt.plot(xx, prediction, label='prediction')
plt.legend()

f:id:tibet:20211226121522p:plainf:id:tibet:20211226121522p:plain

テストデータの最初の値からスタートして、モデルの予測結果を利用して再帰的に予測をする。
モデルのトレーニング回数は2000回で行っている。

# テストデータの最初のデータからスタートし、
# モデルの予測を利用し再帰的に予測
curr_x = x_test[0]
predicted = []
# 予測するステップ数
N = 200
for i in range(N):
    # 予測
    predicted.append(model.predict(curr_x[None]))
    # 入力を更新
    curr_x = np.insert(curr_x, n_steps, predicted[-1], axis=0)[1:]

# 1次元配列に
predicted = np.array(predicted).reshape(-1)

#再帰的な予測を可視化
plt.plot(xx, true_y, label='true')
plt.plot(np.arange(N), predicted, label='prediction')
plt.legend()

f:id:tibet:20211226124725p:plain

為替(USD/JPY)のLSTMによる予測

準備

import pandas as pd
import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt
% matplotlib inline
import seaborn as sns
sns.set()

データ読み込み

!mkdir data
!wget https://dl.dropbox.com/s/rzzcc89lokvzu3r/usd_jpy.csv -O data/usd_jpy.csv

データ構造は以下。

日付 始値 高値 安値 終値
0 2007/04/02 117.84 118.08 117.46 117.84
1 2007/04/03 117.84 118.98 117.72 118.96
2 2007/04/04 118.92 119.08 118.56 118.72
3 2007/04/05 118.72 118.99 118.44 118.72
4 2007/04/06 118.72 119.39 118.67 119.27

終値を図示してみる。

plt.figure(figsize = (18,9))
plt.plot(np.arange(df.shape[0]), df['終値'].values)
plt.xticks(np.arange(0, df.shape[0], 260), df['日付'].loc[::260], rotation=45)

f:id:tibet:20211226125457p:plain
終値をターゲットに、予測モデルを作成する。
データを作成する。

close_prices = df['終値'].values

train_ratio = 0.8
n_train = int(len(close_prices) * train_ratio)
train, test = close_prices[:n_train], close_prices[n_train:]
train = train.reshape(-1, 1)
test = test.reshape(-1, 1)

MinMaxScalerを使ってデータを0-1に正規化する。

from sklearn import preprocessing
scaler = preprocessing.MinMaxScaler()
train = scaler.fit_transform(train)
test = scaler.transform(test)

レーニングデータ数は2340である。

データジェネレータのクラスを用意する。

import math

class DataGenerator(tf.keras.utils.Sequence):
    def __init__(self, data, batch_size, n_steps, input_size, output_size):
        self.data = data
        self.batch_size = batch_size
        self.n_steps = n_steps
        self.input_size = input_size
        self.output_size = output_size

    def __len__(self):
        return math.floor((len(self.data) - self.n_steps) / self.batch_size)
    
    def __getitem__(self, idx):
        xs = np.zeros((self.batch_size, self.n_steps, self.input_size))
        ys = np.zeros((self.batch_size, self.output_size))

        for i in range(self.batch_size):
            step_begin = (idx * self.batch_size) + i
            step_end = (idx * self.batch_size) + i + self.n_steps
            x = np.zeros((self.n_steps, self.input_size))
            y = np.zeros((self.output_size))

            if step_end >= len(self.data):
                break
            else:
                x = self.data[step_begin: step_end]
                y = self.data[step_end]

            xs[i] = x
            ys[i] = y

        return xs, ys

学習の設定をする。

# 学習設定
batch_size = 32    # ミニバッチサイズ
n_steps = 50       # 入力系列の長さ
input_size = 1     # 入力の次元
hidden_size = 50   #  中間層の次元
output_size = 1    # 出力層の次元

n_epochs = 30       # エポック数

train_gen = DataGenerator(train, batch_size, n_steps, input_size, output_size)

モデル構築を行う

def create_model():
    inputs = tf.keras.layers.Input(shape=(n_steps, input_size))
    lstm = tf.keras.layers.LSTM(hidden_size)(inputs)
    output = tf.keras.layers.Dense(output_size)(lstm)

    model = tf.keras.Model(inputs, output)
    model.compile(optimizer='adam', loss='mse', metrics='accuracy')

    return model

model = create_model()

model.summary()
Model: "model"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_1 (InputLayer)        [(None, 50, 1)]           0         
                                                                 
 lstm (LSTM)                 (None, 50)                10400     
                                                                 
 dense (Dense)               (None, 1)                 51        
                                                                 
=================================================================
Total params: 10,451
Trainable params: 10,451
Non-trainable params: 0

上記のようにパラメータ数10,451のLSTMを作成した。
学習を行う

model.fit_generator(train_gen, epochs=30, shuffle=True)
Epoch 3/30
71/71 [==============================] - 1s 13ms/step - loss: 0.0018 - accuracy: 8.8028e-04
Epoch 4/30
71/71 [==============================] - 1s 13ms/step - loss: 0.0013 - accuracy: 8.8028e-04
Epoch 5/30
71/71 [==============================] - 1s 14ms/step - loss: 0.0014 - accuracy: 8.8028e-04
Epoch 6/30
71/71 [==============================] - 1s 13ms/step - loss: 0.0014 - accuracy: 8.8028e-04
Epoch 7/30
71/71 [==============================] - 1s 14ms/step - loss: 0.0010 - accuracy: 8.8028e-04
Epoch 8/30
71/71 [==============================] - 1s 13ms/step - loss: 0.0011 - accuracy: 8.8028e-04
Epoch 9/30
71/71 [==============================] - 1s 13ms/step - loss: 0.0012 - accuracy: 8.8028e-04
Epoch 10/30
71/71 [==============================] - 1s 14ms/step - loss: 9.7010e-04 - accuracy: 8.8028e-04
Epoch 11/30
71/71 [==============================] - 1s 14ms/step - loss: 9.1534e-04 - accuracy: 8.8028e-04
Epoch 12/30
71/71 [==============================] - 1s 14ms/step - loss: 0.0011 - accuracy: 8.8028e-04
Epoch 13/30
71/71 [==============================] - 1s 14ms/step - loss: 9.7462e-04 - accuracy: 8.8028e-04
Epoch 14/30
71/71 [==============================] - 1s 14ms/step - loss: 8.6392e-04 - accuracy: 8.8028e-04
Epoch 15/30
71/71 [==============================] - 1s 14ms/step - loss: 8.6688e-04 - accuracy: 8.8028e-04
Epoch 16/30
71/71 [==============================] - 1s 14ms/step - loss: 0.0010 - accuracy: 8.8028e-04
Epoch 17/30
71/71 [==============================] - 1s 14ms/step - loss: 0.0011 - accuracy: 8.8028e-04
Epoch 18/30
71/71 [==============================] - 1s 14ms/step - loss: 8.6142e-04 - accuracy: 8.8028e-04
Epoch 19/30
71/71 [==============================] - 1s 14ms/step - loss: 7.6914e-04 - accuracy: 8.8028e-04
Epoch 20/30
71/71 [==============================] - 1s 14ms/step - loss: 8.8667e-04 - accuracy: 8.8028e-04
Epoch 21/30
71/71 [==============================] - 1s 14ms/step - loss: 6.5316e-04 - accuracy: 8.8028e-04
Epoch 22/30
71/71 [==============================] - 1s 14ms/step - loss: 6.6321e-04 - accuracy: 8.8028e-04
Epoch 23/30
71/71 [==============================] - 1s 14ms/step - loss: 6.9309e-04 - accuracy: 8.8028e-04
Epoch 24/30
71/71 [==============================] - 1s 14ms/step - loss: 6.6656e-04 - accuracy: 8.8028e-04
Epoch 25/30
71/71 [==============================] - 1s 13ms/step - loss: 7.9177e-04 - accuracy: 8.8028e-04
Epoch 26/30
71/71 [==============================] - 1s 13ms/step - loss: 7.3700e-04 - accuracy: 8.8028e-04
Epoch 27/30
71/71 [==============================] - 1s 13ms/step - loss: 7.8668e-04 - accuracy: 8.8028e-04
Epoch 28/30
71/71 [==============================] - 1s 13ms/step - loss: 5.7617e-04 - accuracy: 8.8028e-04
Epoch 29/30
71/71 [==============================] - 1s 13ms/step - loss: 5.7106e-04 - accuracy: 8.8028e-04
Epoch 30/30
71/71 [==============================] - 1s 13ms/step - loss: 6.8287e-04 - accuracy: 8.8028e-04
<keras.callbacks.History at 0x7f655020a450>

テスト及び予測結果の可視化を行う

test_gen = DataGenerator(test, batch_size, n_steps, input_size, output_size)
predicts = model.predict_generator(test_gen)
fixed_predicts = scaler.inverse_transform(predicts)
fixed_test = scaler.inverse_transform(test)
# テストデータに対する予測を可視化
mod = (len(fixed_test) - n_steps) % batch_size
xx = np.arange(len(fixed_predicts))
plt.figure(figsize=(18, 9))
plt.plot(xx, fixed_test[n_steps:-mod], label='true')
plt.plot(xx, fixed_predicts, label='prediction')
plt.legend()

f:id:tibet:20211226132152p:plain