chainerのliniar.pyの演算処理を確認する

##環境
GPU GTX1070
ubuntu 14.04
chainer 1.14.0
など
##はじめに
chainerで最新のモデルを実装する際には、links/connectionやfunctions/connectionをいじる必要がある。

そこで最も単純なlinear.pyをいじって、新しい層を作ってみる。第１回めはlinear.pyの中身を確認する。

##MNISTで性能を確認する
まず、MNISTで性能を確認する。sampleのtrain_mnist.pyには以下のように３層のlinearが使われている。

```py
# 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に設定されている。つまり、１層目のlinearはユニット数1,000、２層目のlinearはユニット数1,000、３層目の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    
```

学習時間１分５４秒でaccuracyは0.971から0.985になった。

##Linearクラスの初期化を確認する
初期化の段階でL.linearつまりchainer/links/connection/linear.pyのLinearクラスが初期化されている。このlinear.pyの概要は以下。

```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()は以下のようになっている。

```py
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関数を確認すると、

```py
    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を確認すると、概略以下のようになっている。

```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関数を見て演算処理を確認する。

```py
    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()関数に送られて、次元を整えられている。

```py
def _as_mat(x):
    if x.ndim == 2:
        return x
    return x.reshape(len(x), -1)
```

入力xが２次元に整えられているが、０次元目はbatch方向、１次元目は前のユニット数（もしくは入力画素数）ということだろう。そして実際の演算は

```py
y = x.dot(W.T).astype(x.dtype, copy=False)
```

になる。これを図示するとこんな感じ。
![img_161013_2.png](https://qiita-image-store.s3.amazonaws.com/0/116706/64823034-70c1-31ab-6f7c-ff2def6633d9.png)
ここでWはW(out_size, in_size)の構成だったと判明。
次にbackwardを確認する。
##backwardを確認する。
backward関数を見てback propagationの演算を確認する。

```py
    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を２次元に整形している。またこれも同様にinputs[0]がx、inputs[1]がWとなる。

gyがいわゆる

```math
\delta=\frac{\partial E}{\partial w}
```

だろう。

```py
        gx = gy.dot(W).astype(x.dtype, copy=False).reshape(inputs[0].shape)
        gW = gy.T.dot(x).astype(W.dtype, copy=False)
```

の部分を図示するとこんな感じだろう。
![img_161013_3.png](https://qiita-image-store.s3.amazonaws.com/0/116706/79c540a1-ba6d-7435-9f7c-3a18c9aa3447.png)
gxはreshapeでxと同じ形になっているが、もともとgxを算出した段階でxと同型になるはずだが、どのような例外が存在するか、不明。

次回はこの演算部分を改良して新しいconnectionを作る。