俺とプログラミング

某IT企業でエンジニアをしてます。このブログではプログラミングに関わることを幅広く発信します。

Chainerチュートリアル の和訳【リカレントネットと計算グラフ編】

前回に引き続きChainerチュートリアルのリカレントネットと計算グラフ編を和訳したので公開します。なにか問題があったら教えてください。

Recurrent Nets and their Computational Graph

この節では、次のことを学びます:

  • リカレントネットでのfull backprop
  • リカレントネットでのtruncated backprop
  • 少ないメモリでのネットワークの評価

また、この節を読むことで次のことができるようになります:

  • 様々な長さの時系列データを扱う
  • forward計算中のネットワーク上部のストリームを切り取る
  • volatile変数を使うことによって、ネットワークの構築を防ぐ

Recurrent Nets

リカレントネットはループの構造を持つニューラルネットワークです。主に時系列データの入出力を学習させるために使用されます。入力列 x1,x2,…,xt,…と初期状態h0を与えると、リカレントネットは自身の状態を数式ht=f(xt,ht−1)によって更新します。そしてある間隔、もしくは毎時刻において、yt=g(ht)を出力します。もし、時間軸においてその手続きを展開すると、周期的に同じパラメータが使われることを除けば、通常のfeed-forwardネットワークのようにみえます。


ここでは、どうやってシンプルな一層のリカレントネットを記述するか学びます。課題は、言語のモデリングです:ある有限の単語の列を与えられたとき、それぞれの位置において連続する単語をのぞき見せずに、次に来る単語を予測したいといったシチュエーションを考えましょう。1000個の違う単語のタイプがあることを想定します。それらの単語を100次元の実際のベクトルを使って表現します(word embeddingとして知られます)。

chainとしてリカレントネット言語モデル(RNNLM:Recurrent Neural Net Language Model)を定義することから始めましょう。それには全結合で状態をもつLSTM層を実装した、chainer.links.LSTMが使えます。このlinkは通常の全結合層のように見えます。入力と出力のサイズをコンストラクタに与えることで、インスタンスを生成できます:

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

そうしたら、l(x)を呼びだすことで、LSTM層の1ステップが実行できます:

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

forward計算の前にLSTM層の内部状態を初期化するのを忘れないように!すべてのリカレント層は内部状態(すなわち前回の呼び出しでの出力)を保持しています。リカレント層の初めのアプリケーションでは、必ず内部状態を初期化しなければなりません。そうしたら、次の入力はLSTMインスタンスに直接食わせることができます:

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

このLSTMリンクをもとに、新しい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はword embeddingのためのリンクです。入力された数字を対応する固定次元のベクトルに変換します。最後の線形link(out)はfeed-forwardの出力層を表現します。

このRNNは一回のforward計算を実装します。それ自身では系列データを扱わないが、単純にひとつづつ入力を与えることで系列データを処理することができます。


ここに単語のリスト変数であるx_listがあるとしましょう。単にループを使うことで、この単語列の損失の値を計算することができます。

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

もちろん、蓄積された損失の値には計算の履歴が全て残っています。なので、単に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_loossを損失関数に使うことができます:

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

Truncate the Graph by Unchaining

とても長い系列データから学習させることもリカレントネットの典型的なユースケースです。入力と状態列がメモリーに載らないほど長いとしましょう。このようなケースでは、我々はよくバックプロパゲーションを短い時間幅で、切り取るということをします。このテクニックはtruncated backpropと呼ばれます。ヒューリスティックで勾配にバイアスがかかるが、このテクニックは時間幅が長すぎる時に実用上役に立ちます。

Chainerでいかにしてtruncated backpropを実装することができるのでしょうか? Chainerはこれを達成するのにbackward unchainingというスマートなメカニズムをもっています。これは、Variable.unchain_backward()メソッドに実装されています。Backward unchainingはVariableオブジェクトから始まり、variableから計算の履歴を断ち切ります。断ち切られた変数は自動的に解放されます(もし、他のオブジェクトからの明示的な参照がなかった時)。結果として、それらは計算の履歴からなくなり、以降のbackpropに巻き込まれることはなくなります。

truncated backpropの例を記述しましょう。ここではさきほど使用したのと同じネットワークを使います。ここで、とても長い系列データを与えられたとしましょう。そして、30回に一回の間隔でbackprop truncatedを走らせたいです。上記に述べた方法で、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変数に蓄積されます。30ステップ毎に、backpropが蓄積された損失をつかって行われます。そして、 unchain_backward()メソッドが呼ばれることによって、蓄積された損失から計算の履歴が削除されます。最後のmodelの状態はRNNインスタンツがそれを保持しているため、失われていません。

truncated backpropの実装はシンプルであり、難しいトリックはないため、この方法を他の状況において一般化できます。例えば、backpropのタイミングと切り取るベクトルの長さの間で、異なるスケジュールにおいても使えるように上記のコードを簡単に拡張することができます。

Network Evaluation without Storing the Computation History

リカレントネットの評価において、計算の履歴を残す必要はありません。unchainingが制限のあるメモリにおいて制限のない長さの系列データを使うことを可能にするが、それにはちょっとした処置が必要になります。

代わりの方法として、Chainerは計算の履歴を残さないforward計算の評価モードを提供します。これは、単にvolatileフラグを全ての入力変数に与えることで実現できます。このような変数はvolatile変数と呼ばれます。

Volatile変数は宣言時にvolatile='on'を与えることで作られます:

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

Volatile変数は計算の履歴を覚えていないので、勾配を計算するためにloss.backward()を呼び出すことはできません。

Volatile変数はメモリの消費を防ぐ目的で、feed-forwardネットワークの評価にも有用です。

変数の揮発性はVariable.volatileを直接変更することもできます。これにより、ある学習済み特徴抽出ネットワークと学習可能な予測ネットワークを組み合わせることができます。例えば、ある学習済みのネットワークfixed_funcの上部に位置するfeed-forwardネットワークpredictor_funcを学習させたい、という場面を想定します。ここで、predictor_funcを、fixed_funcの計算の履歴を残さずに学習したいです。これは、シンプルに以下のコードで実現できます(x_dataとy_dataはそれぞれ入力値とそのラベルと想定してください):

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

まずはじめに、入力変数xはvolatileであるので、fixed_funcはvolatileモードで実行されるため計算の履歴は残りません。中間変数featは手動でnon-volatileにセットされるため、predictor_funcはnon-volatileモードで実行されるため計算の履歴を保持します。すなわち、計算の履歴はfeatyの間でのみ保持されるため、backward計算は変数featでストップします。

!注意

同じ関数の引数にvolatileとnon-volatile変数を混ぜることは許されていません。もしnon-volatile変数のように振る舞い、同時にvolatile変数と混ぜることができる変数を作りたい場合、'off'フラグの代わりに'auto'フラグを与えることで実現できます。

この節ではChainerでいかにしてリカレントネットを記述するかを説明し、計算の履歴(計算グラフとして知られる)を扱うための基盤となるテクニックを紹介しました。examples/ptbディレクトリにはtruncated backpropでのPenn Treebank LSTM言語モデルの学習の実装の例があります。次の説ではChainerでいかにしてGPUを使うかを議論します。



深層学習 (機械学習プロフェッショナルシリーズ)
深層学習: Deep Learning

Copyright © 2016 ttlg All Rights Reserved.