Chainer公式ドキュメントのReccurentNetsの項を和訳してみました


Chainer TutorialのRecurrent Nets and their Computational Graph の(http://docs.chainer.org/en/stable/tutorial/recurrentnet.html)
を和訳してみました。バージョンは、2016/05/05現在のstable版です(v1.8.1?)。

間違い等ありましたら、ご指摘いただけると幸いです。

リカレントネットとその計算グラフ

このセクションでは以下のことを学ぶ

  • フルバックプロパゲーションを使用したリカレントネット
  • 切り捨てバックプロパゲーションを使用したリカレントネット
  • 少ないメモリでのネットワークの評価

この章を読むと、以下のことができるようになる

  • 可変長のインプットシーケンスを扱う
  • フォワード計算中に、ネットワークの上層を切り捨てる
  • ネットワーク構築をさせないように揮発性変数を使用する

リカレントネット

リカレントネットはループのあるニューラルネットワークである。連続的なインプット/アウトプットによる学習によく使用される。
インプット列はx1, x2, xt, ...と初期状態 h0 で与えられ、一つのリカレントネットはその状態をht = f(xt, ht-1)によって反復的にアップデートする。また、時間tにおけるいくつかの、またはすべての時点で、yt=g(ht) を出力する。
その順序を時間軸で拡張するなら、同じパラメータがネットワーク内で定期的に使われていることを除けば、
それは通常のフィードフォワードネットワークに見える。

では、どのようにシンプルな1レイヤーのリカレントネットを書くか学んでいく。タスクはランゲージモデリングである:
有限の単語列を与えられて、連続する単語を覗き見ることなしに、各ポジションにおける次の単語を予測したい。
1,000個の異なる単語の型があるとして、それぞれの単語を表すのに100次元の実数ベクトルを使う。(word embedding(単語埋め込み)とも言う。)

リカレントニューラルネットランゲージモデル(RNNLM)をcahinとして定義することからはじめよう。
完全に結合されたステートフルなLSTMレイヤーを実装するのにchainer.links.LSTMのlinkを使用できる。
このlinkは通常の完全に結合された1レイヤーのように見える。実装に当たり、コンストラクタに入力と出力のサイズを渡す。

>>> l = L.LSTM(100, 50)

それから、LSTMレイヤーの1ステップを実行するインスタンス l(x) を呼ぶ

>>> l.reset_state()
>>> x = Variable(np.random.randn(10, 100).astype(np.float32))
>>> y = l(x)

フォワードコンピュテーションの前にLSTMレイヤーの内部状態をリセットするのを忘れないように!すべてのカレントレイヤはその内部状態(すなわち、前の呼び出しの出力)を保持している。
リカレントレイヤの最初の適用時には必ず内部状態をリセットしなければならない。その後は、次の入力は直接LSTMインスタンスに供給される:

>>> x2 = Variable(np.random.randn(10, 100).astype(np.float32))
>>> y2 = l(x2)

このLSTM link をもとに、リカレントネットワークを新しいchainとして書いていこう:

class RNN(Chain):
    def __init__(self):
        super(RNN, self).__init__(
            embed=L.EmbedID(1000, 100),  # word embedding
            mid=L.LSTM(100, 50),  # the first LSTM layer
            out=L.Linear(50, 1000),  # the feed-forward output layer
        )

    def reset_state(self):
        self.mid.reset_state()

    def __call__(self, cur_word):
        # Given the current word ID, predict the next word.
        x = self.embed(cur_word)
        h = self.mid(x)
        y = self.out(h)
        return y

rnn = RNN()
model = L.Classifier(rnn)
optimizer = optimizers.SGD()
optimizer.setup(model)

ここで、EmbedID は単語埋め込みへのリンクになっている。これは入力の整数を対応する固定次元の埋め込みベクトルに変換する。
最後のリニアリンク out はフィードフォワード出力層を表す。

RNNchainは1ステップのフォワードコンピュテーションを実装する。これはそれ自身ではシーケンスを扱わないが、私たちはただchainに順にアイテムを送るだけで、シーケンスを処理するのに使用できる。

単語変数のリスト x_list があるとする。そのとき、単語列のloss値を単なる for ループで計算できる。

def compute_loss(x_list):
    loss = 0
    for cur_word, next_word in zip(x_list, x_list[1:]):
        loss += model(cur_word, next_word)
    return loss

もちろん、蓄積されたlossは計算の全経歴を持ったVariableオブジェクトである。
そのため、モデルのパラメータによるloss値の合計の傾きを計算するためには単にそのbackward()メソッドを呼ぶだけで良い:

# Suppose we have a list of word variables x_list.
rnn.reset_state()
model.zerograds()
loss = compute_loss(x_list)
loss.backward()
optimizer.update()

または、 compute_loss をloss関数として使っても同様である。

rnn.reset_state()
optimizer.update(compute_loss, x_list)

Unchainingによるグラフの切り捨て

非常に長いシーケンスからの学習もまたリカレントネットの典型的なユースケースである。
入力と状態のシーケンスがメモリに収まらないほど長かったとする。
そのような場合は、短い時間範囲でバックプロパゲーションを打ち切る事が多い。
この手法はtruncated backpropと呼ばれる。
これはヒューリスティックであり、かつ傾きにバイアスをかけてしまう。ところが、この手法は時間範囲が十分にながければ実際にはうまく動作する。

truncated backpropをどうやってChainerで実装するか?Chainerはbackward unchainingという、truncationを達成する優れたメカニズムを持っている。truncationを達成する優れたメカニズムを持っている。
これは Variable.unchain_backward() メソッドにより実装される。backward unchainingはVariableオブジェクトから始まり、変数から(variableから?)後ろ向きに計算履歴を切る。切られた変数は自動的に破棄される(それらがその他のいかなるユーザオブジェクトから明示的に参照されなかったとしても)。結果として、それらはもはや計算履歴の一部ではなく、バックプロパゲーションに関連することはない。

truncated backpropの例を書いていこう。ここでは前のサブセクションで使用したのと同じネットワークを用いる。非常に長いシーケンスが与えられたとして、truncated backpropを30タイムステップごとに実行したい。
上に定義したモデルを用いて、truncated backpropを記述できる:

loss = 0
count = 0
seqlen = len(x_list[1:])

rnn.reset_state()
for cur_word, next_word in zip(x_list, x_list[1:]):
    loss += model(cur_word, next_word)
    count += 1
    if count % 30 == 0 or count == seqlen:
        model.zerograds()
        loss.backward()
        loss.unchain_backward()
        optimizer.update()

状態は model() でアップデートされ、lossは loss変数に蓄積される。
30ステップごとに、蓄積されたlossにおけるバックプロパゲーションが起こる。
その時、unchain_backward() メソッドが呼ばれ、累積されたlossから後ろ向きに計算履歴を消去する。
modelの最後の状態は失われないことに注意。なぜならRNNのインスタンスはその(状態への)参照を保持しているから。

truncated backpropの実装はシンプルで複雑なトリックがないため、異なる状況にもこのメソッドを一般化できる。例えば、バックプロパゲーションのタイミングとtruncationの長さの間のスケジュールを違ったものにするように拡張できる。

計算履歴を保存せずにネットワークを評価する

リカレントネットを評価する場合、一般的に計算履歴を保存する必要はない。unchaining は有限のメモリで無限長のシーケンスをウォークスルーすることを可能にするが、それは僅かな回避策でしかない。

代わりに、Chainerは計算履歴を保存しないフォワードコンピュテーションの評価モードを提供している。
これは単にvolatileフラグをすべての入力変数に渡すだけで使用できる。
このような変数は揮発性変数(volatile variables)と呼ばれる。

揮発性変数は生成時にvolatile='on'を渡すことで作成される:

x_list = [Variable(..., volatile='on') for _ in range(100)]  # list of 100 words
loss = compute_loss(x_list)

揮発性変数は計算履歴を記憶していないため、
ここで傾きを計算するのにloss.backward()を使えないことに注意。

揮発性変数はメモリフットプリントを減らすためのフィードフォワードネットワークの評価にも有効。

変数の揮発性はVariable.volatile属性の設定で直接変更できる。これにより固定の特徴抽出器と訓練可能な予測器ネットワークを結合することができる。
すでに訓練された固定のネットワーク fixed_func の最上位に位置させた、あるフィードフォワードネットワークpredictor_func を訓練したいものとする。
fixed_funcの計算履歴を保存することなく、 predictor_func を訓練したい。
これは単に以下のコードスニペットで実行できる(x_datay_data はそれぞれ入力データとラベルを示すものとする):

x = Variable(x_data, volatile='on')
feat = fixed_func(x)
feat.volatile = 'off'
y = predictor_func(feat)
y.backward()

はじめに、入力変数 x は揮発性で、fixed_func は揮発性モードで実行される(計算履歴が保存されない)。
その後、中間変数 feat は手動で非揮発性に設定され、そのためpredictor_func は非揮発性モードで実行される
(計算履歴が保存される)。計算履歴は変数 feat と y の間でのみ記録されるため、バックワードコンピュテーションはfeat変数で停止する。


!警告

同じ関数の引数として揮発性と非揮発性変数を混在させることは許されない。もし揮発性変数と一緒に非揮発性変数のように振る舞う変数を作りたければ、'off'フラグの代わりに'auto' フラグを使用する。


このセクションではChainerでのリカレントネットの書き方と計算履歴(計算グラフと言われる)を管理するためのいくつかの基本的なテクニックを示した。
examples/ptb ディレクトリの例は Penn Treebank corpus のLSTM言語モデルのtruncated backprop 学習を実装している。次のセクションでは、ChainerでGPUを使う方法を見ていこう。