LoginSignup
4
5

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-10-12

環境

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)

になる。これを図示するとこんな感じ。
img_161013_2.png
ここで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)

の部分を図示するとこんな感じだろう。
img_161013_3.png
gxはreshapeでxと同じ形になっているが、もともとgxを算出した段階でxと同型になるはずだが、どのような例外が存在するか、不明。

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

4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5