再帰型ニューラルネットワーク(RNN)とLSTM

時系列データ解析において広く使用されているニューラルネットワークである、RNNとLSTMの解説を行います。

Recurrent Neural Network (RNN)

RNNとは、自己回帰型の構造をもつニューラルネットワークの総称であり下図のような構造をしています。

この自己回帰構造のおかげで前の情報を取り入れた解析が可能となり、音声認識等の時系列データ解析でその有効性が知られています。

RNNは、長さ T の時系列データ \{x_1,\ldots,x_T\} を入力とし、次時刻の観測値 x_{T+1} の予測値 y_{T+1} を出力します。

上図での伝播式は、

\begin{split}\begin{align*} z_t &= f^{\rm{(hidden})}(W_{xh}x_t + \color{red}{W_{hh}z_{t-1}} + b_{xh} + b_{hh})\\ y_{t+1} &= f^{(\rm{out})}(W_{hy}z_t + b_{hy}) \end{align*}\end{split}

です。通常のニューラルネットワークとの違いは時間情報を保持した項 \color{red}{W_{hh}z_{t-1}} となります。

ただしそれぞれ以下のように定義しました:

W_{xh} \in \mathbb{R}^{|\rm{hidden}|\times |\rm{input}|}, b_{xh} \in \mathbb{R}^{|\rm{hidden}|} : 入力層から隠れ層への重みとバイアス

W_{hh} \in \mathbb{R}^{|\rm{hidden}|\times |\rm{hidden}|}, b_{hh} \in \mathbb{R}^{|\rm{hidden}|} : 隠れ層から隠れ層への重みとバイアス

W_{hy} \in \mathbb{R}^{|\rm{output}|\times |\rm{hidden}|}, b_{hy} \in \mathbb{R}^{|\rm{output}|} : 隠れ層から出力層への重みとバイアス

f^{(\rm{hidden})}, f^{(\rm{out})} : 隠れ層と出力層の活性化関数

しかし、RNNは動画や文章のような長い時系列データではネットワークが時系列長に比例して非常に深くなってしまい、情報が上手く伝達されないことがしばしば生じます(勾配消失と呼ばれる現象)。

そこで、ある程度長い時系列データに対しても学習できるよう考案されたモデルがLSTMです。

Long Short Term Memory (LSTM)

LSTMはある程度長い時系列データに対しても学習ができるよう考案されたモデルです。

RNNとの違いは、中間層の各ユニットを下図のメモリユニットという要素で置き換えた点にあります。

メモリユニットではユニットの値 c_{t,j} とユニットの出力値 z_{t,j} , j\in\{1,2,\ldots,|hidden|\} を、各ゲートによって時系列に渡って調整します。具体的には、各ゲートの値( \in [0,1] )と積を取ることで、

  • どの程度、前の層からの入力をユニット内に入れるか
  • どの程度、前時刻のユニットの値を現時刻に持ち越すか
  • どの程度、ユニットの値を出力値として外に出すか

といった調整が可能です。RNNでは全情報をそのまま次時刻へ渡していたので、この柔軟性によりLSTMはより長い間情報を伝達できると考えられます。

上図での伝播式は、

\begin{split}\begin{align*} c_{t,j} &= \sigma((W_{in}x_t)_j+(R_{in}z_{t-1})_j)f((W_cx_t)_j+(R_cz_{t-1})_j)+\sigma((W_{for}x_t)_j+(R_{for}z_{t-1})_j)c_{t-1,j}\\ z_{t,j} &= \sigma((W_{out}x_t)_j+(R_{out}z_{t-1})_j)f(c_{t,j}) \end{align*}\end{split}

ただしそれぞれ以下のように定義しました:

W_c \in \mathbb{R}^{|hidden|\times |input|} : 前の層からメモリユニットへの結合の重み

W_{in} \in \mathbb{R}^{|hidden|\times |input|} : 前の層から入力ゲートへの結合の重み

W_{for} \in \mathbb{R}^{|hidden|\times |input|} : 前の層から忘却ゲートへの結合の重み

W_{out} \in \mathbb{R}^{|hidden|\times |input|} : 前の層から出力ゲートへの結合の重み

R_c \in \mathbb{R}^{|hidden|\times |hidden|} : 1時刻前の中間層の出力から現時刻のメモリユニットへの結合の重み

R_{in} \in \mathbb{R}^{|hidden|\times |hidden|} : 1時刻前の中間層の出力から現時刻の入力ゲートへの結合の重み

R_{for} \in \mathbb{R}^{|hidden|\times |hidden|} : 1時刻前の中間層の出力から現時刻の忘却ゲートへの結合の重み

R_{out} \in \mathbb{R}^{|hidden|\times |hidden|} : 1時刻前の中間層の出力から現時刻の出力ゲートへの結合の重み

数値実験(LSTMによるSin波の予測)

最後にLSTMによるSin波の予測を行います。

問題設定

LSTMを下図のように、前5つのデータから次のデータを予測するように学習します。

Required Libraries

  • numpy 1.13.1
  • matplotlib 2.0.2
In [1]:
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
from copy import deepcopy

import renom as rm
from renom.optimizer import Adam
from renom.cuda import set_cuda_active

# set True when using GPU
set_cuda_active(False)

データの準備

時系列データから指定の長さの部分列を生成する関数を以下で定義します。

In [2]:
# create [look_back] length array from [time-series] data.
# e.g.) ts:{1,2,3,4,5}, lb=3 => {1,2,3},{2,3,4},{3,4,5}
def create_dataset(ts, look_back=1):
    sub_seq, nxt = [], []
    for i in range(len(ts)-look_back):
        sub_seq.append(ts[i:i+look_back])
        nxt.append([ts[i+look_back]])
    return sub_seq, nxt

今回は区間 [-10,10] のSin波を50分割し、長さ5の部分列から次の観測を予測するようにデータを生成します。

In [3]:
# making a Sine curve data
x = np.linspace(-10,10,50)
y = np.sin(x)
look_back = 5

sub_seq, nxt = create_dataset(y, look_back=look_back)
In [4]:
# split data into train and test set
def split_data(X, y, train_ratio=.5):
    train_size = int(len(y)*train_ratio)
    X_train, y_train  = X[:train_size], y[:train_size]
    X_test, y_test    = X[train_size:], y[train_size:]

    X_train = np.array(X_train)
    X_test = np.array(X_test)
    y_train = np.array(y_train)
    y_test = np.array(y_test)
    return X_train, y_train, X_test, y_test
In [5]:
X_train, y_train, X_test, y_test = split_data(sub_seq, nxt)
train_size = X_train.shape[0]
test_size = X_test.shape[0]
print('train size : {}, test size : {}'.format(train_size, test_size))
train size : 22, test size : 23

予測結果の描画

前の部分列での予測結果を次の観測値とみなし、その次の予測を行います。つまり、

\begin{split}\begin{align*} \{x_t,\ldots,x_{t+4}\} &\rightarrow \hat{x}_{t+5} \\ \{x_{t+1},\ldots,\hat{x}_{t+5}\} &\rightarrow \hat{x}_{t+6} \\ \{x_{t+2},\ldots,\hat{x}_{t+6}\} &\rightarrow \hat{x}_{t+7} \\ \end{align*}\end{split}

と順次計算し、 \{\hat{x}_{t+5},\ldots,\hat{x}_{t+T}\} を描画します。

In [6]:
def draw_pred_curve(e_num):
    pred_curve = []
    arr_now = X_test[0]
    for _ in range(test_size):
        for t in range(look_back):
            pred = model(np.array([arr_now[t]]))
        model.truncate()
        pred_curve.append(pred[0])
        arr_now = np.delete(arr_now, 0)
        arr_now = np.append(arr_now, pred)
    plt.plot(x[:train_size+look_back], y[:train_size+look_back], color='blue')
    plt.plot(x[train_size+look_back:], pred_curve, label='epoch:'+str(e_num)+'th')

モデルの定義

In [7]:
# model definition
model = rm.Sequential([
   rm.Lstm(2),
   rm.Dense(1)
])

各パラメータの設定

In [8]:
# params
batch_size = 5
max_epoch = 2000
period = 200 # early stopping checking and drawing predicted curve period
optimizer = Adam()

Train Loop

In [9]:
i = 0
loss_prev = np.inf

# learning curves
learning_curve = []
test_curve = []

plt.figure(figsize=(15,10))

# train loop
while(i < max_epoch):
    i += 1
    # perm is for getting batch randomly
    perm = np.random.permutation(train_size)
    train_loss = 0

    for j in range(train_size // batch_size):
        batch_x = X_train[perm[j*batch_size : (j+1)*batch_size]]
        batch_y = y_train[perm[j*batch_size : (j+1)*batch_size]]

        # Forward propagation
        l = 0
        z = 0
        with model.train():
            for t in range(look_back):
                z = model(batch_x[:,t].reshape(len(batch_x),-1))
                l = rm.mse(z, batch_y)
            model.truncate()
        l.grad().update(optimizer)
        train_loss += l.as_ndarray()

    train_loss = train_loss / (train_size // batch_size)
    learning_curve.append(train_loss)

    # test
    l = 0
    z = 0
    for t in range(look_back):
        z = model(X_test[:,t].reshape(test_size, -1))
        l = rm.mse(z, y_test)
    model.truncate()
    test_loss = l.as_ndarray()
    test_curve.append(test_loss)

    # check early stopping
    # if test loss doesn't reduce by 1% of that at period epoch before, early stopping is done.
    if i % period == 0:
        print('epoch:{}, train loss:{}, test loss:{}'.format(i, train_loss, test_loss))
        draw_pred_curve(i)
        if test_loss > loss_prev*0.99:
            print('Stop learning')
            break
        else:
            loss_prev = deepcopy(test_loss)
# predicted curve
plt.xlabel('x')
plt.ylabel('y')
plt.legend(loc='upper left', fontsize=20)
plt.show()
epoch:200, train loss:0.040894584730267525, test loss:0.04207055643200874
epoch:400, train loss:0.002047195433988236, test loss:0.005259166471660137
epoch:600, train loss:0.0006290220735536423, test loss:0.0025398710276931524
epoch:800, train loss:0.00030508961572195403, test loss:0.002104206010699272
epoch:1000, train loss:0.0001259945884157787, test loss:0.0019810707308351994
epoch:1200, train loss:8.218832681450294e-05, test loss:0.002142632845789194
Stop learning
../../../_images/notebooks_basic_algorithm_LSTM_notebook_21_1.png

上図において、左半分の青い曲線は学習データ、右の各色の曲線はそれぞれのepoch後での予測を表します。学習が進むにつれてLSTMはSin波を上手く近似できていることがわかります。

損失関数の描画

In [10]:
plt.figure(figsize=(15,10))
plt.plot(learning_curve, color='blue', label='learning curve')
plt.plot(test_curve, color='orange', label='test curve')
plt.xlabel('epoch')
plt.ylabel('loss')
plt.legend(fontsize=30)
plt.show()
../../../_images/notebooks_basic_algorithm_LSTM_notebook_24_0.png

まとめ

本チュートリアルでは、RNNとLSTMを説明しました。RNNは時系列解析に有効なニューラルネットワークであり、LSTMはより長い時系列データの解析を可能とする意味でRNNの拡張となっています。

LSTMはその時系列データ解析への有効性から、時系列データの回帰問題や異常検知等に応用がされています。

References

[1] 岡谷貴之.『機械学習プロフェッショナルシリーズ 深層学習』, 講談社, 2015.