NStepLSTM

はじめに

今回は、先に示したChainerによるLSTMの実装を、chainer.links.NStepLSTMを用いて書き換える。さらに、GPU上でも動作する仕組みを導入し、CPU上との速度比較を行う。

実行環境

GPU環境

  • Amazon EC2 g2.2xlarge
  • OS: Ubuntu 16.04.1 LTS
  • CPU: Intel(R) Xeon(R) CPU E5-2650 v2 @ 2.60GHz
  • Core数: 8
  • Memory: 15GiB
  • GPU: Tesla GRID K520搭載(video memory 4GB)
  • Python: Python 3.4.3
  • Chainer version: 3.0.0

CPU環境

  • MacBook Pro (15-inch, Late 2016)
  • OS: macOS Sierra
  • CPU: 2.9GHz Intel Core i7
  • Core数: 4
  • Memory: 16GB
  • Python: anaconda3-5.0.0 (Python 3.6.2)
  • Chainer version: 3.0.0

全ソース

今回用いたソースはこちらに置いてある。

NStepLSTM

NStepLSTMはChainer1.16.0からサポートされているクラスである。従来のLSTM(chainer.links.LSTM)との違いは以下の通りである。

  • 先のLSTMの解説Lと置いたもの(シーケンス長)を可変長にできる。
  • cuDNNを用いて最適化されているので、chainer.links.LSTMと比べ高速である。
  • LSTMの多層化を引数ひとつで実現できる。

chainer.links.LSTMとchainer.links.NStepLSTMのインターフェースを次に示す。

これらの間の大きな違いは以下の通りである。

  • NStepLSTM.__init__の第1引数n_layersにLSTM層の数を指定する。
  • NStepLSTM.__call__に隠れ層の初期状態(hx)とセルの初期状態(cx)を渡す必要がある。
  • NStepLSTM.__call__の第4引数xsは、(seq_size,n_in)のサイズを持つtupleの配列である。ここでseq_sizeはひとつのシーケンスの長さ(=L)、n_inは入力データの次元である。配列の要素数はバッチサイズに相当する。

LSTM.__call__に渡すxとNStepLSTM.__call__に渡すxsとの違いを以下に図で示す。

LSTMの場合(左図)、矩形で囲んだ各列を左から右に向かって順にxに渡していく。一方、NStepLSTMの場合(右図)は、矩形で囲んだ各行を上から順に配列に納めてxsに渡し一括処理を行う。今回示す例ではseq_sizeを固定長(=L)とするが、このサイズを可変長にできるのがNStepLSTMの利点である。

chainer.links.LSTMに似たインタフェースとするため、以下のクラス(nstep_lstm/nstep_lstm.py)を定義する。

  • hx,cxの値は明示的に与えず、デフォルト値(全要素が0の配列)を利用する。
  • to_cpuとto_gpuはCPU/GPU上の計算をサポートするための仮想関数である。オーバライドする。
  • 常に同じ結果が得られるように、16,22,23行目にあるseed関数で乱数値を固定する。
  • 12行目から19行目の記述でGPU/CPUを切り替えている。

定数SEED,GPUは以下のnstep_lstm/params.py内で定義される。

GPU上で計算するときはGPU=0(デバイス番号)、CPU上で計算するときはGPU=-1(任意の負の値)とすれば良い。前者の場合はcupyが、後者の場合はnumpyが使われる。

訓練コード

上で定義したクラスLSTMを用いた訓練時のコードを以下に示す(nstep_lstm/nstep_lstm_using_chainer_with_fibonacci.py)。以前のコードとほぼ同じである。

  • CPU/GPUの切り替えを行なっている部分は、17-19行目、140-141行目、157-158行目である。
  • 誤差の蓄積は、35行目と56行目のconcat周辺のコードで実現している。concatすることで計算の手間を削減していることに注意する。

予測コード

GPU環境では(EC2上では)予測のあとグラフを描画するために、Jupyterを用いた。コードは、上のセルから順に以下のようになる(nstep_lstm/draw_results.ipynb)。

一番最初のセルでインポートしているモジュールpredictの中身は以下の通りである(nstep_lstm/predict.py)。

予測時のロジックは先の解説を参照のこと。予測時はひとつずつ順に計算していくので、GPUではなくCPU上で計算した。GPU上で計算するとGPUデバイス上へのメモリ転送のオーバヘッドが大きくなるためである。

計算結果

params.py内の定数N_LAYERSだけを変えて計算を行なった。SEQUENCE_SIZE(=L=30)とした。予測結果を以下に示す。図の見方は以前と同じである。最初のシーケンスとして正解値を与え、ひとつずつ予測値に置き換えていく。最初のシーケンスは予測値でないので0としてある。

今回の例では、明らかにLSTM層は1層の方が精度が良いことが分かる。多層化が常に精度向上に繋がるわけではないのだろう。以下に示すのは訓練時における損失値のepoch依存性である。

速度比較

CPU/GPU上でLSTMとNStepLSTMの訓練時の速度比較を行なった。NStepLSTMの層数は1、その他のパラメータは上に示したparams.pyと同じである。結果を以下に示す(単位はsec)。3回測定し平均した。ここに掲げていないLSTMのソース(lstm/lstm_using_chainer_with_fibonacci.py)はこちらに置いてある。このスクリプトもGPU版のときはGPU=0、CPU版のときはGPU=-1として使う。

GPU上での結果を見ると、NStepLSTMは確かに高速であることが分かるが(cuDNNのおかげだろう)、LSTMはCPU上のものと比べてもかなり遅いことが分かる。実装の仕方に改良の余地がありそうである(おそらく、__call__に順に渡すxのオーバヘッドが原因だろう)。一方、CPU上での結果を見ると、LSTMとNStepLSTMの間にはそれほど差がないだけでなく、前者の方が速くなっている。

まとめ

今回は、chainer.links.NStepLSTMを用いた実装例を示した。chainer.links.LSTMとはデータの与え方が異なることに注意しなければならない。さらに、GPU上でも動作する仕組みを導入した。GPU上での速度は、NStepLSTMの方が高速であることを確認した。
今回示した例では、多層化による精度向上は見られなかった。しかし、GPU上では高速であること、Lを可変長にできること、問題によっては多層化が必要になることを考慮すると、NStepLSTMの方を用いるべきであろう。

参考文献

Kumada Seiya

Kumada Seiya

仕事であろうとなかろうと勉強し続ける、その結果”中身”を知ったエンジニアになれる

最近の記事

  • 関連記事
  • おすすめ記事
  • 特集記事

アーカイブ

カテゴリー

PAGE TOP