Define by Run
今回は、Chainerの特徴の1つであるDefine by Runについて、どんなものであるかを理解できるようにします。
Define by Run
Define by Runでは、計算グラフ(ニューラルネットの構造)の構築をデータを流しながら行います。
Define and Run
Define and Runでは、計算グラフを構築してから、そこにデータを流していきます。
これだけで違いが理解できるでしょうか?
おそらく難しいかと思います。
上記の2つの違いを認識するのが目標です。
具体例を見る
やはり、具体例を見たほうがつかみやすいかと思います。
Chainerの以下のコードを見てください。
どのようなネットワーク構造が想定できるでしょうか。
__init__はインスタンスを生成したときに一度だけ呼び出され、パラメータを準備するだけの部分です。パラメータを準備するだけの段階では、一切計算グラフは用意されていません。
データを入力したときに呼び出される処理は __call__の中身の方です。こちらの方で初めて計算グラフが構築されていきます。
上記のコードのネットワークにおける動作
Chainerで流されるデータxは全て、「chainer.Variable(x)」のように、一度変換をされています。このオブジェクトは自身がどのような計算をされたかを完全に記憶することができます。
この記憶そのものが計算グラフになるということです。
記憶機能を持ったデータが、例えば、と計算をされたならば、そのとき初めてこの変換を行う計算グラフが構築されます。その後も何らかの変換が行われた場合には続いて計算グラフを構築していきます。
そして、構築された計算グラフを基にバックプロパゲーションを行って学習するということです。
従って、次に違うデータを入れた時には計算グラフは一旦リセットされていると思えばいいでしょう。再び、データが計算グラフを構築していき、それを基にバックプロパゲーションを行います。
従ってミニバッチ毎に(あるいは1データ毎に)計算グラフは毎回違うものを準備することもできるのです。それが学習として実りある成果が出るかは謎ですが、ともかくそのような柔軟性を持っているということです。
上記のコードの例では、__call__が呼び出される毎に、「prob」という値を正規分布から発生させます。そして入力データ「x」を線形変換し「relu」で活性化した後、probの値が正か負でその後の計算を分岐させています。
つまりデータが流れる毎に、半々の確率でネットワーク構造が変わるのです。これが役立つシーンを見い出せれば、chainerでの実装は非常に楽ということです。
仮にあるミニバッチのデータが入力された際に、その「prob = -0.4」などとなったとしましょう。すると条件分岐によって、以下の実践矢印の方向にデータが進みます。
当然、データは自身が通った計算のルートを覚えている(逆にそのことしか情報に残っていない)ので、その計算グラフに該当するパラメータのみが更新されます。
そもそも、通らなかった部分は計算グラフすら構築されません(図では便宜上描いていますが)。