"機械学習","信号解析","ディープラーニング"の勉強

HELLO CYBERNETICS

深層学習、機械学習、強化学習、信号処理、制御工学などをテーマに扱っていきます

深層学習のフレームワーク「dynet」

 

 

 

 dynet

深層学習の新しいフレームワークです。開発元は「カーネギーメロン大学」で、コンピュータ・サイエンスではトップレベルの研究機関です。

 

特徴

Define by Runの方

dynetはDefine by Runのタイプです。

ChainerがTensorFlowと完全に差別化できていた武器が、もうアメリカで登場してしまうとは、なんだか悲しい。

Define by Runについては以下の記事を。

s0sem0y.hatenablog.com

 

Chainerよりはモジュールの粒度は低い

Chainerにはtrainerがあるが、それに相当するものはdynetにはないです。ですから、Chainerに比べたら少しコードを書くのが面倒になりそうです。

 

もちろんtrainerみたいなモジュール化が必ずしも良いとは限らなくて、例えばChainerでは現状「Truncated BPTT」のような処理をしたい場合は結局は自分でコードを書くことになります。そうなると、普段から学習のコード自分で書いとけばよかったとなりかねません。

KerasはChainer以上にゴリゴリモジュール化しているため、一見直感的ですが、細かい変更はできない小回りの効かないものとなっています(とは言っても、大抵はそれで十分)。

 

dynetはだいたいTensorFlowと同じくらいの粒度ように見えました(ちゃんと見たら、TensorFlowより便利機能付けられてるかも)。将来的なニューラルネットの流行の変動に対応しやすいのは粒度が低い方です。純粋に大学という研究機関が開発元ですから、新しい構造を試しやすいフレームワークを開発したのでしょう。

 

コードの例

xorのデータを分類するニューラルネットワーク

 

 y = σ \{ {\bf V} \left( \tanh ({\bf Wx + b}) \right) \}

 

中間層は1つで、活性化関数は(なぜか)ハイパボリックタンジェント。

出力層の活性化関数はロジスティックシグモイド関数。

(出力ユニット数が1の場合は2クラス分類をロジスティックシグモイド関数で行います。)

 

 

 

# define the parameters
m = Model()
pW = m.add_parameters((8,2))
pV = m.add_parameters((1,8))
pb = m.add_parameters((8))

# renew the computation graph
renew_cg()

# add the parameters to the graph
W = parameter(pW)
V = parameter(pV)
b = parameter(pb)

# create the network
x = vecInput(2) # an input vector of size 2.
output = logistic(V*(tanh((W*x)+b)))
# define the loss with respect to an output y.
y = scalarInput(0) # this will hold the correct answer
loss = binary_log_loss(output, y)

# create training instances
def create_xor_instances(num_rounds=2000):
    questions = []
    answers = []
    for round in xrange(num_rounds):
        for x1 in 0,1:
            for x2 in 0,1:
                answer = 0 if x1==x2 else 1
                questions.append((x1,x2))
                answers.append(answer)
    return questions, answers

questions, answers = create_xor_instances()

# train the network
trainer = SimpleSGDTrainer(m)

total_loss = 0
seen_instances = 0
for question, answer in zip(questions, answers):
    x.set(question)
    y.set(answer)
    seen_instances += 1
    total_loss += loss.value()
    loss.backward()
    trainer.update()
    if (seen_instances > 1 and seen_instances % 100 == 0):
        print "average loss is:",total_loss / seen_instances

 

何かしらのフレームワークを使っている人はなんとなく分かるでしょう。

 

パラメータの準備
# define the parameters
m = Model()
pW = m.add_parameters((8,2))
pV = m.add_parameters((1,8))
pb = m.add_parameters((8))

重み行列もバイアスベクトルも自分で設定するところはTensorFlowに似ていますね。

しかし、これはまだ使用すると決定しているパラメータではありません。chainerで言えば「__init__」の部分に相当する準備段階。

 

計算グラフ構築
# renew the computation graph
renew_cg()

# add the parameters to the graph
W = parameter(pW)
V = parameter(pV)
b = parameter(pb)

# create the network
x = vecInput(2) # an input vector of size 2.
output = logistic(V*(tanh((W*x)+b)))
# define the loss with respect to an output y.
y = scalarInput(0) # this will hold the correct answer
loss = binary_log_loss(output, y)
 

 

計算グラフの構築を「renew_cg()」で行っています。

そしてその後初めてパラメータを計算グラフに渡しています。

更にその下で具体的な計算式を書いております。

 

x = vecInput(2)

この処理は、TensorFlowの「placeholder」に相当する概念でしょうか。

予めネットワークに与えられるデータの次元を与えています。

 

具体的にフィードフォワードの計算式を書き下す部分は、chainerの__call__に相当する部分と言えるでしょう。結局この処理が呼び出されて初めて計算グラフが構築されるのでした。

 

TensorFlowでは、パラメータの準備をすることとフィードフォワード計算を設定することを行い、それで計算グラフを構築してしまいます。その後変更は不可能です。

パラメータの準備と、計算グラフ構築を分離してしまうことでDefine by Runが実現できます。

 

 

 

コードのデータの作成部分は飛ばしてしまいます。

 

学習部分
# train the network
trainer = SimpleSGDTrainer(m)

total_loss = 0
seen_instances = 0
for question, answer in zip(questions, answers):
    x.set(question)
    y.set(answer)
    seen_instances += 1
    total_loss += loss.value()
    loss.backward()
    trainer.update()
    if (seen_instances > 1 and seen_instances % 100 == 0):
        print "average loss is:",total_loss / seen_instances

 

以下のコード、trainerという名のオプティマイザー設定。chainerのtrainerとは全然違いますよ。

trainer = SimpleSGDTrainer(m)

 

初期化のあとの学習の中身について

for question, answer in zip(questions, answers):
    x.set(question)
    y.set(answer)
    seen_instances += 1
    total_loss += loss.value()
    loss.backward()
    trainer.update()

 

以下の処理で、入力データとラベルを獲得していますね。

   x.set(question)
   y.set(answer)

 

一行飛ばして、以下のコード

    total_loss += loss.value()
    loss.backward()
    trainer.update()

多分見慣れたもんですねこのコードは。lossを計算し、逆伝搬して更新。

 

Define by Runを支えているのは結局ここ
# renew the computation graph
renew_cg()

# add the parameters to the graph
W = parameter(pW)
V = parameter(pV)
b = parameter(pb)

# create the network
x = vecInput(2) # an input vector of size 2.
output = logistic(V*(tanh((W*x)+b)))
# define the loss with respect to an output y.
y = scalarInput(0) # this will hold the correct answer
loss = binary_log_loss(output, y)

この部分を学習のコードの中に入れてしまえば、計算グラフの構築を学習中に変えられることになります。

 

動的にネットワークを変える

上記の計算グラフ構築の部分を関数で書いてしまって、学習中に呼び出すようにすればいいわけです。

def create_xor_network(pW, pV, pb, inputs, expected_answer):
    renew_cg() # new computation graph
    W = parameter(pW) # add parameters to graph as expressions
    V = parameter(pV)
    b = parameter(pb)
    x = vecInput(len(inputs))
    x.set(inputs)
    y = scalarInput(expected_answer)
    output = logistic(V*(tanh((W*x)+b)))
    loss =  binary_log_loss(output, y)
    return loss

m2 = Model()
pW = m2.add_parameters((8,2))
pV = m2.add_parameters((1,8))
pb = m2.add_parameters((8))
trainer = SimpleSGDTrainer(m2)

seen_instances = 0
total_loss = 0
for question, answer in zip(questions, answers):
    loss = create_xor_network(pW, pV, pb, question, answer)
    seen_instances += 1
    total_loss += loss.value()
    loss.backward()
    trainer.update()
    if (seen_instances > 1 and seen_instances % 100 == 0):
        print "average loss is:",total_loss / seen_instances

 

パラメータと訓練データを引数にしておいて、条件毎に関数に渡すパラメータを変えるようにすれば動的に計算グラフを変えられることになります。

上記は実際には計算グラフは変更を受けませんが、以下の部分

 

for question, answer in zip(questions, answers):
    loss = create_xor_network(pW, pV, pb, question, answer)

 

で、lossを計算する手前で、questionに応じて代入するpWなどを変更するように条件文を書いておけば良さそうです。

 

まとめ

明示的に計算グラフの構築やloss計算を書く部分もchainerに比べモジュール粒度が低いと言えるでしょう。下手にモジュール化するよりも、独自の損失関数を準備したい時とかは、この方法で書けるようにしといたほうがいいでしょうね。

 

class使えば、パラメータ準備する部分を__init__で書いて、__call__に計算グラフ構築からロスの計算(今回関数だった部分)を書けばchainerっぽくもなりそう。

 

 

 

TensorFlow覇権なのは正直GoogleだからってのとGPU楽ってことくらいになってきましたね。

コード見てて思ったけども、chainerが完全に喰われそうな感じです。

 

dynetドキュメント

ドキュメント

Working with the python DyNet package — Dynet 1.0 documentation

 

技術論文

[1701.03980] DyNet: The Dynamic Neural Network Toolkit