Neural Network Libraries Step by Step 4 後編

Posted by Takuya Yashima

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),
w_init=initializer, with_bias=False)
h = F.relu(h)
h = PF.convolution(h, 2*init_filter, kernel=(3, 3),
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)

269 'size of new parameter {}.\n'
270 'To clear all parameters, call nn.clear_parameters().'.format(
--> 271 name, param.shape, tuple(shape)))
272

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があります。

また、ここで使われた重みは、名前が'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を使わずに（シンプルな名前で）自分で事前に用意し、 それを演算に用いることで後々にその重みパラメータを再指定しやすくするといったことも選択肢の1つです。

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

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),
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),
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),
pred = F.affine(h, weight)

return pred

PFではこのような処理はできないのでしょうか？　一部の関数に限るとするなら、答えは「可能」です。
PF.convolutionやPF.affineに関しては、apply_wを使うことで上記の処理が可能です。しかし、apply_wに渡せるのはラムダ関数、もしくは引数を一つだけ受け取る関数なので、

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),
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),
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を返す