はじめに
こんにちは、さかぱ(@zacapa_23)です。
先日、情報セキュリティスペシャリスト試験を受けてきました。これはIT系の国家資格試験、情報処理技術者試験の1つで合格率は16%前後です。
その試験会場の女性比率は学部4年に基本情報を受けた3年前に比べて、だいぶ上がったと思います。 綺麗なお姉さんがいると試験中もチラチラみてしまい、あんまり集中できません(カンニングはしてません)。
さて、本記事のテーマは前回のLSTMをNStepLSTMに書き換えることです。
www.monthly-hack.com
主に3つの点を取り上げます。
- NStepLSTMの特徴
- コードの違い
- 未解決エラー
そんなことどうでもいい。とにかくNStepLSTMに触らせろ。という方はこちら。
github.com
NStepLSTMとは
NStepLSTMの特徴はLSTMの多層化と可変長データのミニバッチができることです。
LSTMの多層化
次のようなLSTMをNStepLSTMで表現するとこんな感じ。dropoutも含んでいます。
import chainer.links as L import chainer.functions as F x = input_data # LSTM l1 = L.LSTM(n_in, n_out) l2 = L.LSTM(n_out, n_out) tmp = l1(F.dropout(x)) y = l2(F.dropout(tmp)) # NStepLSTM h = hidden_states c = cell_states l1 = L.NStepLSTM(n_layer=2, n_in, n_out, dropout_ratio) y = l1(h, c, x)
しかし次のLSTMの場合、NStepLSTMでは表現できなさそうです。何故かと言うとNStepLSTMに渡すh, cのshapeは(n_layer, batchsize, n_out)なので、2層目以降の入力素子数はn_outになるからす。つまり素子数が全ての層で同じ(但し入力層を除く)になるようなLSTMしか表現できません*1。
# LSTM (n_unit != n_out)
l1 = L.LSTM(n_in, n_unit)
l2 = L.LSTM(n_unit, n_out)
tmp = l1(F.dropout(x))
y = l2(F.dropout(tmp))
可変長データのミニバッチ
従来(Chainer 1.15.0以前)では最長のデータに合わせて0埋めをする必要がありましたが、NStepLSTMではそのまま入力することができます。
データ | 従来 | NStepLSTM | |
---|---|---|---|
A | 1 2 | 1 2 0 0 0 | 1 2 |
B | 1 3 5 4 3 | 1 3 5 4 3 | 1 3 5 4 3 |
C | 2 4 3 | 2 4 3 0 0 | 2 4 3 |
詳しくは、こちらの記事が参考になるかと思います。
studylog.hateblo.jp
また、次の文字や単語を予測するタスクでは、全ての系列データを1つに連結し、分割することがあります。
# D = A + B + C D = [1, 2, 1, 3, 5, 4, 3, 2, 4, 3] # m1 = D[:len(D)/2] # m2 = D[len(D)/2:] m1 = [1, 2, 1, 3, 5] m2 = [4, 3, 2, 4, 3]
データのつなぎ目を表すために疑似ラベル(E=-1)を加えることもあります。
# D = A + E + B + E + C + E D = [1, 2, -1, 1, 3, 5, 4, 3, -1, 2, 4, 3, -1] # m1 = D[:len(D)/2] # m2 = D[len(D)/2:] m1 = [1, 2, -1, 1, 3] m2 = [5, 4, 3, -1, 2] ...
こういった前処理をせずとも、NStepLSTMでは入力データをそのまま扱うことができます。
cudnnの利用
cuDNN 5以降でのcuDNN-RNNを使うことができ、こちらのほうが計算速度が速いそうです。
devblogs.nvidia.com
このあとで記述するのですが、Chainer 1.17.0ではNStepLSTMで(n_layer>=2)の場合、cudnn部分でエラーが発生してしまいます。つまりcudnnを使った多層LSTMができないということです。執筆時では修正版がリリースされてはいませんが、修正コードはissueに上がっています。
github.com
コード解説
ソースコードはGithubで公開しています。前回の記事で使っているのがnico_lstm.py、今回のNStepLSTM実装はnico_nslstm.pyです*2。
github.com
データセット
コメントの開始から終了までを予測するため、辞書に終了フラグを追加。各コメント"train"を辞書"vocab"を用いてid化。入力データ"train_now"と終了フラグを末尾に加えた次文字(正解)データ"train_next"をnumpy.arrayで用意します。一度numpy.arrayに変換するのはpermutationを利用するためで、ミニバッチは各コメントをnumpy or cupy配列に変換したlist型配列です。
with open('../sample_vocab.pkl', 'rb') as f: vocab = pickle.load(f) vocab.append('<EOF>') vocab2index = {word: i for i, word in enumerate(vocab)} with open('../sample_texts.pkl', 'rb') as f: texts = pickle.load(f) train = [[vocab2index[word] for word in text] for text in texts if text != ''] train_now = np.asarray(train) train_next = np.asarray([item[1:] + [vocab2index['<EOF>']] for item in train]) perm = np.random.permutation(n_train) xs = [xp.asarray(item, dtype=np.int32) for item in train_now[perm[i:i + args.batchsize]]] hx = chainer.Variable( xp.zeros((args.layer, len(xs), args.unit), dtype=xp.float32)) cx = chainer.Variable( xp.zeros((args.layer, len(xs), args.unit), dtype=xp.float32)) t = [xp.asarray(item, dtype=np.int32) for item in train_next[perm[i:i + args.batchsize]]]
モデル
NStepLSTMの引数は3つで、hs, cxは先述のデータセットで生成。xsはid化された各コメントを長さn_unitsのベクトルに変換したもの。L.EmbedIDを通すと勝手にchainer.Variable型になる。可変長ミニバッチなのでリスト内包表記を使い、各コメントをL.EmbedIDやL.Linearの層に通すのがポイントです。
- hx (~chainer.Variable): Initial hidden states.
- cx (~chainer.Variable): Initial cell states.
- xs (list of ~chianer.Variable): List of input sequences.
class RNN(chainer.Chain): def __init__(self, n_layer, n_vocab, n_units, dropout, cudnn): super(RNN, self).__init__( embed=L.EmbedID(n_vocab, n_units), l1=L.NStepLSTM(n_layer, n_units, n_units, dropout, use_cudnn=cudnn), l2=L.Linear(n_units, n_vocab), ) def __call__(self, hx, cx, xs, train=True): xs = [self.embed(item) for item in xs] hy, cy, ys = self.l1(hx, cx, xs, train=train) y = [self.l2(item) for item in ys] return y
誤差計算
可変長ミニバッチに対応するために、for文を使って各コメント毎に誤差を計算する。
self.loss = None self.y = self.predictor(*x, train) for yi, ti in zip(self.y, t): if self.loss is not None: self.loss += self.lossfun(yi, ti) else: self.loss = self.lossfun(yi, ti) * エラー ** CUDNN_STATUS_BAD_PARAM n_step_lstmのパラメータがn_layer>=2のとき、<span style="color: #ff5252">cudnnでエラーが発生</span>します。 >|bash| File "/anaconda3/lib/python3.5/site-packages/chainer/links/connection/n_step_lstm.py", line 96, in __call__ train=train, use_cudnn=self.use_cudnn) File "/anaconda3/lib/python3.5/site-packages/chainer/functions/connection/n_step_lstm.py", line 468, in n_step_lstm ret = rnn(*inputs) File "/anaconda3/lib/python3.5/site-packages/chainer/function.py", line 197, in __call__ outputs = self.forward(in_data) File "/anaconda3/lib/python3.5/site-packages/chainer/functions/connection/n_step_lstm.py", line 267, in forward self.reserve_space.data.ptr, reserve_size) File "cupy/cuda/cudnn.pyx", line 983, in cupy.cuda.cudnn.RNNForwardTraining (cupy/cuda/cudnn.cpp:13268) File "cupy/cuda/cudnn.pyx", line 1002, in cupy.cuda.cudnn.RNNForwardTraining (cupy/cuda/cudnn.cpp:13008) File "cupy/cuda/cudnn.pyx", line 321, in cupy.cuda.cudnn.check_status (cupy/cuda/cudnn.cpp:1718) cupy.cuda.cudnn.CuDNNError: CUDNN_STATUS_BAD_PARAM: b'CUDNN_STATUS_BAD_PARAM'
拙い英語でChainer Githubにissueを投稿したところ、解決しました。ありがとうございます。
github.com
おわりに
今回はLSTMをNStepLSTMで書き直すということでした。最初はネットワークを書き換えれば平気かなと思っていたら、hやcの初期値を与えないとダメ。ミニバッチはchainer.Variableのリストじゃないとダメ。その後の全結合層を別々に計算しないとダメ。誤差関数もそれぞれやらないとダメ。といろいろ面倒でした。もしかしたら、もっとスマートな効率のいいコードがあるのかも知れません。何か指摘があればコメントにお願いします。大変なことが多かったけれども、1つ嬉しかったのは、エラーを見つけてchainerの改善に貢献できたことです。