読者です 読者をやめる 読者になる 読者になる

studylog/北の雲

chainer/python/nlp

Chainer1.5の変更点についてのメモ

あけましておめでとうございます。今年もChainer中心に遊んでいきたいと思います。

過去最大のアップデート 1.5。あまりにも変わりすぎていて以前のコードを1.5対応に書き換えるが結構大変です。
正直1.4のままでいいかな…と思いましたが、最新版についていかないと損しそうなので箱根駅伝見ながら勉強しました。1.5でCuPyがだいぶ速くなったようですし。

未解決なところ、曖昧・あやふやな情報がありますがご了承下さい。一部1.4以前に既にあったものも含まれてるかもしれません。
自信が無いところはこの色で書いてます。

まずは1.5版チュートリアル和訳から、後はサンプルコードとドキュメントを見ながら試行錯誤しました。
i101330.hatenablog.com

一番変わったのがこれ。重みなどのパラメータがあるのがLink、無いのがFunctionなのかな。まだ自分もイマイチわかってないです。特にChain…。再利用性が高まったらしいですがそういう高度な事は自分には理解できないのでもう少し勉強してここは加筆したいところ。

__call__

最初に躓いたのがここ。forwardじゃなくて__call__でやる流儀になったそうです。何年もpython触ってるんですが__call__なんて初めて使いました。__call__のおかげで、

loss = model.forward( x , t )
こんな感じだったのが
loss = model(x , t)

とスタイリッシュに。でもぱっと見なにをやってるのか分かり辛くなった気がします。

WeightDecay、GradientClippingはadd_hookで

以前は以下のように学習ループ中に記述してました。

loss.backward()
optimizer.weight_decay(0.0001) #ここ
optimizer.update()

1.5では

optimizer = optimizers.Adam()
optimizer.setup(model)
optimizer.add_hook(chainer.optimizer.WeightDecay(0.0001)) #ここ

こうやってoptimizerの初期化後に一度だけ指定するようになりました。学習ループ中に指定するとエラーでます。一度だけでいい模様。optimizer._hooksに記録されます。
古い書き方でもエラーは出ませんがduplicatedだそう。古い書き方だとそもそも有効にならないのかな?不明。

ハマりポイントになりそうなので書いておきますが、optimizerを保存(serialize)してもadd_hookの情報は記録されない模様。自信は無いですがoptimizer._hooksが空になってるんですよね。これは後でまた書きます。

optimizer.update()

Recurrent Nets and their Computational Graph — Chainer 1.5.1 documentation
ここによると

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
model.zerograds()
loss = compute_loss(x_list)
loss.backward()
optimizer.update()

これを

optimizer.update(compute_loss, x_list)

と短く書けるようです。
これもぱっと見どこで学習してるのか分かり辛くなる可能性があるので自分は従来の書き方でいこうと思います。
また短い書き方だとRNNでloss.unchain_backward()が使えない?
lossはどこから取ればいいのか?(グラフ描画とかに使いたい)
という疑問も。フォーラムで聞いてみたい。

モデルの公式save/load

1.5目玉の一つ。モデル保存時はGPUでもGPUの方のメモリを使わない?これは嬉しい

load時に注意するところがいくつかあります。ちゃんとやらないとハマります。モデル読み込んだ後に学習再開してlossが増えちゃった場合はsave/loadが適切に行われていない可能性大。

失敗した実例。
学習中断時はlossが6300付近で順調に下がっていた。以下のグラフは再開後のlossですがいきなり急上昇して結局元のlossに戻るまで数時間かかってしまった。
f:id:kitanokumo:20160105203328p:plain
こんな風になっちゃってる人は保存読み込みを失敗してます。

保存

#初期化
lm = net.RNNLM(n_units = 512) #RNNLMは自作ネットワーク
model = L.Classifier(lm)
#ここ要注意
model.compute_accuracy = False  # accuracyが必要ない場合はFalseに Falseにした方が学習が速い?

# Setup optimizer
optimizer = optimizers.Adam()
optimizer.setup(model)
optimizer.add_hook(chainer.optimizer.WeightDecay(0.0001))

〜ここで学習して〜

#save
#modelとoptimizerの二つ保存する必要あり
serializers.save_hdf5('test.model', model)
serializers.save_hdf5('test.state', optimizer)

#GPU/CPU共通の保存方法で大丈夫です
#to_gpuとかto_cpuしなくてよし 保存の時に圧縮率を指定できる  やり方はドキュメントを

読み込み

#初期化
lm = net.RNNLM(n_units = 512) #RNNLMは自作ネットワーク
model = L.Classifier(lm)
#ここ要注意
model.compute_accuracy = False  # 再度指定しないと引き継がれない

# Setup optimizer
optimizer = optimizers.Adam()
optimizer.setup(model) #ここでsetupする 一度だけする
#ここ要注意
#add_hookはserializeされない?ので再指定しないといけないっぽい
#指定しないとoptimizer._hooksが空のままになる
optimizer.add_hook(chainer.optimizer.WeightDecay(0.0001))  # 再度指定しないと引き継がれない

#load
serializers.load_hdf5('2ch_model_15_2.model', model)
serializers.load_hdf5('2ch_model_15_2.state', optimizer)

#なんとなくmodelを読み込んでからもう一度setupした方がいいような気がしたので
#ここでもう一度setupしちゃってたけどこれは駄目
#optimizerの一部の情報がリセットされちゃいます かなりはまった
#optimizer.setup(model)   これは駄目 やらなくていい

読み込み時のsetup、add_hook、modelのcompute_accuracyなどに注意ですね。
add_hookは明確に指定しないとoptimizerの_hooksが空のままになってるので無効になっちゃってる模様。自信は無い。後でフォーラムで聞いてみようと思います。

1.4以前のモデルを1.5にアップデートする

Capitalicoでのchainer 1.1 → 1.5 バージョンアップ事例 のP18〜
https://groups.google.com/forum/#!topic/chainer/eXyL11thcNY

この辺を参考に。自分はやってません。

Classifier

これを使うとlossやaccを管理しなくていい。隠遁される。

model = L.Classifier(MyChain(), lossfun=mean_squared_error)

lossfunはデフォルトではsoftmax_cross_entropy。
lossやaccの値はmodel.lossなどで取れます。

ただ簡潔・洗練されすぎて他人の書いたコードの場合何をやってるのか一目でわかりにくいかも。

optimizerのtとepoch(1.4以前からあったかも)

optimizer.t #これはoptimizer.updateした回数が自動的に入る模様
optimizer.epoch #これは手動でnew_epoch()を呼ばないと0のまま?自動でインクリメントされない

volatileがon offに?

x = chainer.Variable(xp.asarray(x),  volatile='on’)

従来のBoolean(True False)でもいける模様。なぜこうなったのかよくわからない。自分はtrainにTrue Falseを入れてそれをnotする方が好きなので従来のやり方で。

Flagってのが新しく追加されてるけどこっち使うのかな?サンプルでは使われていなかった。
http://docs.chainer.org/en/stable/reference/core/flag.html

Linkにxp(cuda.cupy or numpy)が自動的に入る模様

type_check_enableが有効になってるかの確認

参考:Chainerメモ13 type_checkをオフにして高速化 - studylog
無効の方が学習が速い。

Linkにtypecheckは無いようなのでFunctionで
print(F.Concat.type_check_enable)

こんなのじゃなくて専用の確認方法がある気がする。知ってる人教えてください。

初期重みをランダムで設定

#1.4まで
for param in model.parameters:
    param[:] = np.random.uniform(-0.1, 0.1, param.shape)

#1.5
for param in model.params():
    data = param.data
    data[:] = np.random.uniform(-0.1, 0.1, data.shape)

デフォルトでもそれなりに小さい値をセットしてくれてます。
n_unitsが大きくなるほど重みは小さい値になる模様。
デフォルトではbiasは0になってるので、上記のように自分で設定しない方がいいのかも(青い深層学習本によると初期biasは0がいいらしい)。

参考

#Linearでの初期化コード
    def __init__(self, in_size, out_size, wscale=1, bias=0, nobias=False,
                 initialW=None, initial_bias=None):
        super(Linear, self).__init__(W=(out_size, in_size))
        if initialW is None:
            #ここでランダムに設定 in_sizeの大きさによって上限値が変わってる?
            initialW = numpy.random.normal( 0, wscale * numpy.sqrt(1. / in_size), (out_size, in_size))
        self.W.data[...] = initialW

以上です。間違いがあれば指摘してください。よろしくお願いします。

(追記)

LSTMのreset_state

サンプルではLSTMの層を指定してreset_stateしている(以下ptbサンプル)。

    def __init__(self, n_vocab, n_units, train=True):
        super(RNNLM, self).__init__(
            embed=L.EmbedID(n_vocab, n_units),
            l1=L.LSTM(n_units, n_units),
            l2=L.LSTM(n_units, n_units),
            l3=L.Linear(n_units, n_vocab),
        )
        self.train = train

    def reset_state(self):
        self.l1.reset_state() #ここでl1と指定
        self.l2.reset_state()

これだとLSTMの層を増やしたときにreset_stateでの指定漏れの恐れがある。例えばl5まで作ったときにreset_stateを書き換えないとl3,l4がreset_state()されないけどエラーも出ないので気づかない(減らしたときはエラーが出る)。実際それで数時間無駄にしてしまったので、層を増やしても自動的に検知してreset_stateしたいと思って次のようにしてみた。

    def __init__(self, n_vocab, n_units, train=True):
        super(RNNLM, self).__init__(
            embed=L.EmbedID(n_vocab, n_units),
            l1=L.LSTM(n_units, n_units),
            l2=L.LSTM(n_units, n_units),
            l3=L.Linear(n_units, n_vocab),
        )
        self.train = train
        #LSTMの層だけ入れておく もっと賢い入れ方ありそう
        self.lstm_layers = [ getattr(self, c) for c in self._children if isinstance(getattr(self, c), chainer.links.LSTM)]

    #ここで自動的にLSTMを検知してreset
    def lstm_reset_state(self):
        for layer in self.lstm_layers:
            getattr(layer , "reset_state")()