##環境
GPU GTX1070
ubuntu 14.04
chainer 1.14.0
など
##はじめに
chainerで最新のモデルを実装する際には、links/connectionやfunctions/connectionをいじる必要がある。
そこで最も単純なlinear.pyをいじって、新しい層を作ってみる。第1回めはlinear.pyの中身を確認する。
##MNISTで性能を確認する
まず、MNISTで性能を確認する。sampleのtrain_mnist.pyには以下のように3層のlinearが使われている。
# Network definition
class MLP(chainer.Chain):
def __init__(self, n_in, n_units, n_out):
super(MLP, self).__init__(
l1=L.Linear(n_in, n_units), # first layer
l2=L.Linear(n_units, n_units), # second layer
l3=L.Linear(n_units, n_out), # output layer
)
def __call__(self, x):
h1 = F.relu(self.l1(x))
h2 = F.relu(self.l2(h1))
return self.l3(h2)
n_unitsはdefaultで1000に設定されている。つまり、1層目のlinearはユニット数1,000、2層目のlinearはユニット数1,000、3層目のlinearはユニット数10。これを50回学習させて、学習時間とaccuracyを計測する。
python train_mnist.py -g=0 -e=50
GPU: 0
# unit: 1000
# Minibatch-size: 100
# epoch: 50
epoch main/loss validation/main/loss main/accuracy validation/main/accuracy
1 0.190119 0.0948303 0.941934 0.9713
2 0.070093 0.0740304 0.978332 0.9747
3 0.0504763 0.0662532 0.983782 0.9799
・・・・・
48 0.00491283 0.140848 0.998749 0.9825
49 0.00535585 0.172546 0.998999 0.9838
50 0.00589774 0.162305 0.998733 0.9847
学習時間1分54秒でaccuracyは0.971から0.985になった。
##Linearクラスの初期化を確認する
初期化の段階でL.linearつまりchainer/links/connection/linear.pyのLinearクラスが初期化されている。このlinear.pyの概要は以下。
import math
from chainer.functions.connection import linear
from chainer import initializers
from chainer import link
class Linear(link.Link):
def __init__(self, in_size, out_size, wscale=1, bias=0, nobias=False,
initialW=None, initial_bias=None):
super(Linear, self).__init__()
self.initialW = initialW
self.wscale = wscale
self.out_size = out_size
if in_size is None:
self.add_uninitialized_param('W')
else:
self._initialize_params(in_size)
if nobias:
self.b = None
else:
self.add_param('b', out_size)
if initial_bias is None:
initial_bias = bias
initializers.init_weight(self.b.data, initial_bias)
def _initialize_params(self, in_size):
self.add_param('W', (self.out_size, in_size))
initializers.init_weight(self.W.data, self.initialW,
scale=math.sqrt(self.wscale))
def __call__(self, x):
_initialize_params関数でinitializers.init_weight()が呼び出され、(out_size, in_size)の大きさのWが生成されているようだ。initializers.init_weight()は以下のようになっている。
def init_weight(weights, initializer, scale=1.0):
"""Helper function for initialization of the weight tensor.
This function accepts several types of initializer, prepares
the appropriate ``~chainer.Initializer`` if necessary,
and does the initialization.
Args:
weights (numpy.ndarray or cupy.ndarray):
Weight tensor to be initialized.
initializer: The value used to initialize the data.
May be ``None`` (in which case
:class:`~chainer.initializers.HeNormal`
is used as an initializer), a scalar to set all values to,
an ``numpy.ndarray`` to be assigned,
or a callable that takes :class:`numpy.ndarray`
or :class:`cupy.ndarray` and edits its value.
scale (scalar): A constant to multiply initializer by.
"""
if initializer is None:
initializer = HeNormal(1 / numpy.sqrt(2))
elif numpy.isscalar(initializer):
initializer = Constant(initializer)
elif isinstance(initializer, numpy.ndarray):
initializer = Constant(initializer)
assert callable(initializer)
initializer(weights)
weights *= scale
今回送られてきたのは引数はweightsだけ。なんかW初期化するのにいろいろと面倒くさいことしてるね。知りたいのはWに相当するnumpy行列の形状なんだが・・・
とりあえずW(in_size, out_size)と考えて進める。
##call関数を確認する
train_mnist.py内MLPクラスのcall関数ではh1 = F.relu(self.l1(x))
などとlinear(l1)にxが入力されている。
そこでchainer/links/connection/linear.pyのcall関数を確認すると、
def __call__(self, x):
"""Applies the linear layer.
Args:
x (~chainer.Variable): Batch of input vectors.
Returns:
~chainer.Variable: Output of the linear layer.
"""
if self.has_uninitialized_params:
self._initialize_params(x.shape[1])
return linear.linear(x, self.W, self.b)
xはVariableのオブジェクトでbatch of imput vectorsとなっている。chainer/functions/connection/linear.pyを確認すると、概略以下のようになっている。
from chainer import function
from chainer.utils import type_check
def _as_mat(x):
class LinearFunction(function.Function):
def check_type_forward(self, in_types):
def forward(self, inputs):
def backward(self, inputs, grad_outputs):
def linear(x, W, b=None):
if b is None:
return LinearFunction()(x, W)
else:
linear関数でLinearFunction()(x,W)へ送られている。このLinearFunctionクラスでforward計算とbackward計算が行われている。そこでforward計算を確認する。
##forwardの演算処理を確認する
forward関数を見て演算処理を確認する。
def forward(self, inputs):
x = _as_mat(inputs[0])
W = inputs[1]
y = x.dot(W.T).astype(x.dtype, copy=False)
if len(inputs) == 3:
b = inputs[2]
y += b
return y,
inputs[0]に入力値x、inputs[1]に重み行列Wが送られてきている。xはまず_as_mat()関数に送られて、次元を整えられている。
def _as_mat(x):
if x.ndim == 2:
return x
return x.reshape(len(x), -1)
入力xが2次元に整えられているが、0次元目はbatch方向、1次元目は前のユニット数(もしくは入力画素数)ということだろう。そして実際の演算は
y = x.dot(W.T).astype(x.dtype, copy=False)
になる。これを図示するとこんな感じ。
ここでWはW(out_size, in_size)の構成だったと判明。
次にbackwardを確認する。
##backwardを確認する。
backward関数を見てback propagationの演算を確認する。
def backward(self, inputs, grad_outputs):
x = _as_mat(inputs[0])
W = inputs[1]
gy = grad_outputs[0]
gx = gy.dot(W).astype(x.dtype, copy=False).reshape(inputs[0].shape)
gW = gy.T.dot(x).astype(W.dtype, copy=False)
if len(inputs) == 3:
gb = gy.sum(0)
return gx, gW, gb
else:
return gx, gW
まず、forwardの時と同様にxを2次元に整形している。またこれも同様にinputs[0]がx、inputs[1]がWとなる。
gyがいわゆる
\delta=\frac{\partial E}{\partial w}
だろう。
gx = gy.dot(W).astype(x.dtype, copy=False).reshape(inputs[0].shape)
gW = gy.T.dot(x).astype(W.dtype, copy=False)
の部分を図示するとこんな感じだろう。
gxはreshapeでxと同じ形になっているが、もともとgxを算出した段階でxと同型になるはずだが、どのような例外が存在するか、不明。
次回はこの演算部分を改良して新しいconnectionを作る。