EN

Neural Network Libraries Step by Step 4 後編

2019年11月5日 火曜日

チュートリアル

Posted by Takuya Yashima

前編ではnnabla.functions(F)とnnabla.parametric_functions(PF)の違いを説明しました。後編では、これらのより発展的な使い方を紹介していきます。

PFを使うときの注意点

PFを使うことで、重みパラメータを意識することなく計算グラフを構築していくことが可能であることがわかりましたが、PFを使う際には注意しないといけない点があります。以下がその例です。

def construct_wrong_CNN(x, init_filter, num_class):
    initializer = I.UniformInitializer((-0.1, 0.1))

    with nn.parameter_scope("PF/conv"):
        h = PF.convolution(x, init_filter, kernel=(3, 3),
                           pad=(1, 1), stride=(2, 2),
                           w_init=initializer, with_bias=False)
        h = F.relu(h)
        h = PF.convolution(h, 2*init_filter, kernel=(3, 3),
                           pad=(1, 1), stride=(2, 2),
                           w_init=initializer, with_bias=False)
        h = F.relu(h)

    with nn.parameter_scope("PF/affine"):
        pred = PF.affine(h, num_class, w_init=initializer, with_bias=False)

    return pred

これはすでに利用したものと似ていますが、このグラフを作成してみるとどうなるでしょうか。

y_wrong = construct_wrong_CNN(x, 16, num_class)
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
in
----> 1 y_wrong = construct_wrong_CNN(x, 16, num_class)

in construct_wrong_CNN(x, init_filter, num_class)
5 h = PF.convolution(x, init_filter, kernel=(3, 3), pad=(1, 1), stride=(2, 2), w_init=initializer, with_bias=False)
6 h = F.relu(h)
----> 7 h = PF.convolution(h, 2*init_filter, kernel=(3, 3), pad=(1, 1), stride=(2, 2), w_init=initializer, with_bias=False)
8 h = F.relu(h)
9

in convolution(inp, outmaps, kernel, pad, stride, dilation, group, channel_last, w_init, b_init, base_axis, fix_parameters, rng, with_bias, apply_w, apply_b, name)

/opt/miniconda3/envs/nnabla-build/lib/python3.6/site-packages/nnabla/parametric_functions.py in convolution(inp, outmaps, kernel, pad, stride, dilation, group, channel_last, w_init, b_init, base_axis, fix_parameters, rng, with_bias, apply_w, apply_b)
628 w = get_parameter_or_create(
629 "W", (outmaps,) + filter_shape,
--> 630 w_init, True, not fix_parameters)
631 if apply_w is not None:
632 w = apply_w(w)

/opt/miniconda3/envs/nnabla-build/lib/python3.6/site-packages/nnabla/parameter.py in get_parameter_or_create(name, shape, initializer, need_grad, as_need_grad)
269 'size of new parameter {}.\n'
270 'To clear all parameters, call nn.clear_parameters().'.format(
--> 271 name, param.shape, tuple(shape)))
272
273 if need_grad != param.need_grad:

ValueError: The size of existing parameter "W" (16, 3, 3, 3) is different from the size of new parameter (32, 16, 3, 3).
To clear all parameters, call nn.clear_parameters().

このようにValueErrorが出てしまいます。

エラーメッセージを見てみると、2つ目のPF.convolutionの処理で問題が起きており、 さらに最後のメッセージからは、重みパラメータのサイズが異なるということがわかります。

この原因は、異なる重みを用いる演算が同じ名前空間の中に存在する点にあります。 コードを見てみると、最初の(名前空間が'PF/conv'である)withブロックでは、2つのPF.convolutionがあります。

本来なら、1つ目のPF.convolutionによってvariableのshapeは(1, 3, 8, 8)->(1, 16, 4, 4)となっています。

また、ここで使われた重みは、名前が'PF/conv/conv/W'であり、そのshapeは(16, 3, 3, 3)です。 しかしながら、次のPF.convolutionも同じ名前空間'PF/conv'に属しているため、 内部では'PF/conv/conv/W'の名前をもつ重みパラメータが使われることになります。

このレイヤーにおいては、入出力の前後でVariableはそのshapeが(1, 16, 4, 4)->(1, 32, 2, 2)となるような変換を行おうとしているため、 必要な重みのshapeは(32, 16, 3, 3)でなければなりません。 しかしながら、実際に呼び出されたのは1つ目のPF.convolutionでも用いられた重みパラメータであり、 そのサイズは(16, 3, 3, 3)であるため、エラーが起きてしまいます。

このように、PFを用いることで、重みパラメータの命名を気にしなくてもよくなる反面、名前の衝突には気をつける必要が生じます。この場合は重みパラメータのshapeが異なることでエラーが出てきてくれましたが、異なる場所で使われる重みパラメータのshapeがたまたま同じだった場合、異なる重みを使うことを意図しているにも関わらず同じ重みが使用されてしまい、結果学習がうまく進まなくなるなど、思わぬ挙動を示す可能性があります。

 

結局どういった場合に使い分けるべきか

基本的にはPFを使えば十分です。しかし、例外もあります。次のようなケースです。

学習中などに任意の重みパラメータの値を操作をしたい場合や、ネットワーク内の別の場所で同じ重みを使用したい(重み共有を行いたい)場合などは、その度に対象となる重みパラメータを指定する(すなわち名前を指定する)必要がありますが、PFによって自動的に命名された重みの名前はときに複雑なものになり、名前の指定が面倒なときがあります。例えば、複数のwithブロックによって多重ネストされた中でPFを使う際は、パラメータの指定が難しい場合があります。

そのようなときは、そういった処理が必要となる重みパラメータはあえてPFを使わずに(シンプルな名前で)自分で事前に用意し、 それを演算に用いることで後々にその重みパラメータを再指定しやすくするといったことも選択肢の1つです。

また、Convolutionレイヤーなどに重みとして渡すパラメータに対し、演算の前に特殊な前処理をかけたいときなどは、PFを利用しないほうが書きやすい場合もあります。
以下の例は、Convolutionで用いる重みに対し制約をかける場合です。

def CNN_with_preprocessed_weight_F(x, init_filter, num_class):
    initializer = I.UniformInitializer((-0.1, 0.1))
    kernel_1 = nn.parameter.get_parameter_or_create(
               name="F/conv1/conv/W", shape=(init_filter, channels, 3, 3),
               initializer=initializer, need_grad=True)
    kernel_1 = F.clip_by_value(kernel_1,
                               F.constant(0.25, shape=kernel_1.shape),
                               F.constant(0.75, shape=kernel_1.shape))
    h = F.convolution(x, kernel_1, pad=(1, 1), stride=(2, 2))
    h = F.relu(h)

    kernel_2 = nn.parameter.get_parameter_or_create(
               name="F/conv2/conv/W", shape=(2*init_filter, init_filter, 3, 3),
               initializer=initializer, need_grad=True)
    kernel_2 = F.clip_by_norm(kernel_2, 1)
    h = F.convolution(h, kernel_2, pad=(1, 1), stride=(2, 2))
    h = F.relu(h)

    weight = nn.parameter.get_parameter_or_create(
             name="F/affine/affine/W", shape=(h.size, num_class),
             initializer=initializer, need_grad=True)
    pred = F.affine(h, weight)

    return pred

PFではこのような処理はできないのでしょうか? 一部の関数に限るとするなら、答えは「可能」です。
PF.convolutionやPF.affineに関しては、apply_wを使うことで上記の処理が可能です。しかし、apply_wに渡せるのはラムダ関数、もしくは引数を一つだけ受け取る関数なので、
入力値であるVariableだけでなくクリッピングに用いる最小値と最大値を受け取るF.clip_by_valueを使用したい場合にはfunctools.partialを利用する必要があります。

import functools

def CNN_with_preprocessed_weight_PF(x, init_filter, num_class):
    initializer = I.UniformInitializer((-0.1, 0.1))
    with nn.parameter_scope("PF/conv1"):
        kernel_1_shape = (init_filter, x.shape[1], 3, 3)
        reg1 = functools.partial(F.clip_by_value,
                                 min=F.constant(0.25, shape=kernel_1_shape),
                                 max=F.constant(0.75, shape=kernel_1_shape))
        h = PF.convolution(x, init_filter, kernel=(3, 3),
                           pad=(1, 1), stride=(2, 2), with_bias=False,
                           w_init=initializer, apply_w=reg1)
        h = F.relu(h)

    with nn.parameter_scope("PF/conv2"):
        reg2 = functools.partial(F.clip_by_norm, clip_norm=1)
        h = PF.convolution(h, 2*init_filter, kernel=(3, 3),
                           pad=(1, 1), stride=(2, 2), with_bias=False,
                           w_init=initializer, apply_w=reg2)
        h = F.relu(h)

    with nn.parameter_scope("PF/affine"):
        pred = PF.affine(h, num_class, w_init=initializer, with_bias=False)

    return pred
y_F = CNN_with_preprocessed_weight_F(x, 16, num_class)
y_PF = CNN_with_preprocessed_weight_PF(x, 16, num_class)

y_F.forward()
y_PF.forward()

np.all(y_PF.d==y_F.d) # 初期値を揃えているためTrueを返す

 

まとめ

ここまで、Neural Network LibrariesにおけるVariableの使い方、パラメータの更新に用いるSolverとGradientの算出方法、そして計算グラフの内部での演算を行うnnabla.functionsおよびnnabla.parametric_functionsの基本的な使い方を見てきました。

次のステップとしては、NNabla Examplesに目を通すことをおすすめします。いくつかのExampleを見ることで、基本的なニューラルネットワークモデルの作り方・使い方をマスターできるでしょう。