NeuralNetwork - Kerasy Documentation
Author: Shuto
Description: Brief explanation of NeuralNetwork and introduction of modules that can be used in Kerasy

### Neural Network

- 2019-08-18(日)

# Neural Network¶

## Table of contents¶

1. 単純パーセプロトン (Perceptron)
2. 多層パーセプロトン (Multilayer Perceptron)
3. 誤差逆伝播法 (Back Propagation)
4. 実装 (Implementation)

## 1. 単純パーセプロトン (Perceptron)

ニューラルネットワークは 多数の素子（パーセプトロン）の集まり で表現されます。そこで、まずは１つ１つの素子、パーセプロトンを見ていきます。

単一の素子は以下の図のように表され、入力を受け取り、

- その値（の和）がある閾値 $\theta$ を超えたら発火して出力
- $\theta$ 以下だったら出力しない

という性質を持ちます。

この性質を数学的に記述すると、

- 重みパラメータ：$w_0,w_1,\ldots,w_m$
- 活性化関数：$h$

から構成される以下のような関数として書けます。

$$f(x_1,x_2,\ldots,x_m) = h(w_1x_1+w_2x_2+\cdots+w_Nx_N+w_0)$$

この時 $w_0$ はバイアスパラメータ(bias parameter)と呼ばれ、関数を平行移動させる役割を果たします。

### 活性化関数(activation function)¶

活性化関数は、「ある閾値 $\theta$ を超えたら発火して出力し、$\theta$ 以下だったら出力しない」という神経の性質を表現するための関数であり、最も単純なものとしては以下で表されるヘヴィサイド関数(Heaviside function)があります。

$$h(a) = \left\{\begin{array}{cc}0 & (\theta < 0) \\1 & (\theta > 0)\end{array}\right.$$

この性質は表したい能力を如実に表してはいますが、不連続であり扱いづらいため、実際は

- ロジスティックシグモイド関数： $$h(a) = \frac{1}{1+\exp(-a)}\qquad(4.59)$$
- ハイパボリックタンジェント関数： $$\begin{aligned}h(a) &= \tanh (a)\\&=\frac{e^a-e^{-1}}{e^1+e^{-1}} &(5.59)\end{aligned}$$
- ソフトマックス関数： $$h(\mathbf{a}) = \frac{\exp(a_i)}{\sum_j \exp(a_i)}\qquad (4.63)$$

などが用いられることが多いです。

※ Kerasyではここで定義しています。

In [1]:

    class Linear():
        def forward(input):
            return input

        def diff(a):
            dhda = a
            return dhda

    class Tanh():
        def forward(input):
            return np.tanh(input)

        def diff(a):
            dhda = 1-np.tanh(a)**2
            return dhda

    ActivationHandler = {
        'linear' : Linear,
        'tanh'   : Tanh,
    }

    def ActivationFunc(activation_func_name):
        return ActivationHandler[activation_func_name]

## 2. 多層パーセプロトン (Multilayer Perceptron)

それでは、複数の単子（単純パーセプロトン）を繋げて２層のパーセプトロンを作ります。なお、２層以上のパーセプトロンをニューラルネットワークと言います。

例えば１層目に $m$ 個の素子を用意し、入力が $D$ 次元の場合、関数で表すと

$$y = h_2\left(\sum_{i=0}^{m} w^{(2)}_ih_1\left(\sum_{j=0}^{D}w^{(1)}_{ij} x_j\right)\right)\qquad(5.9)$$

となります。

記号 | 意味
--- | ---
$w^{(1)}_{ij}$ | $1$ 層目の $i$ 番目の素子への入力 $x_j$ の重み
$w^{(2)}_i$ | $1$ 層目の $i$ 番目の素子から $2$ 層目の素子への入力の重み
$h_1,h_2$ | それぞれの層の活性化関数

### 多層にする意味¶

- 単純パーセプロトン： $$ y=f\left(\sum_i w_i\textcolor{red}{\phi_i(\mathbf{x})}\right) \qquad(5.1)$$
- $2$ 層パーセプトロン： $$ y = h_2\left(\sum_{i=0}^{m} w^{(2)}_i\textcolor{red}{h_1\left(\sum_{j=0}^{D}w^{(1)}_{ij} x_j\right)}\right)\qquad(5.9)$$

この式から分かるように、基底 $\phi_i(\mathbf{x})$ が $\displaystyle h_1\left(\sum_{j=0}^{D}w^{(1)}_{ij} x_j\right)$ に置き換わっていることがわかります。

これによって、単一だと「固定」されていた基底関数が、多層になることで「適応的に変動」するようになります。ゆえに、十分大きな $m$ をとり、活性化関数が非線形な多層パーセプトロンは、任意の関数を任意の精度で近似することができるという性質を持ちます。

## 3. 誤差逆伝播法 (Back Propagation)

ニューラルネットワークについて語る上で避けては通れなく、理解が難しいのが誤差逆伝播法（back propagation）です。

しかし、誤差逆伝播法とは「ニューラルネットワークを学習させる際に用いられる効率的な計算方法」のことで、簡単に言ってしまえば「合成関数の微分則」です。また、ネットワークの訓練とは、「（重みを変化させることで）訓練データの出力と正解の誤差 $E_n(\mathbf{w})$ を最小化すること」を指します。

以下、

- 素子 $i$ の出力を $z_i$
- 素子 $j$ への入力和を $a_j$

として、説明します。

したがって、学習のプロセスにおいては「素子 $i$ から素子 $j$ への接続の重み $w_{ji}$ として、全ての $i,j$ の組合せ（＝全ノードの重み）に対して$ \frac{\partial E_n}{\partial w_{ji}} $を計算すること」が目標となります。

すると、上の図のように「$E_n$ は $a_j$ を介してのみ $w_{ji}$ に依存する」（＝ $w_{ji}$ が変化すると $a_j$ が変化し、それが $E_n$ に影響を及ぼす）ことに注意すれば

$$ \frac{\partial E_n}{\partial w_{ji}} = \frac{\partial E_n}{\partial a_j}\frac{\partial a_j}{\partial w_{ji}}\qquad(5.50)$$

となります。また、ここで $\delta_j = \partial E_n/\partial a_j$ と書くことにし、これを誤差と呼びます。

続いて、$\delta_i$ について考えると、$E_n$ は素子 $i$ の出力を受け取る素子 $j$ の入力 $a_j$ を介して $a_i$ に依存するので、合成微分則より

$$ \delta_i = \frac{\partial E_n}{\partial a_i} = \sum_j \frac{\partial E_n}{\partial a_j}\frac{\partial a_j}{\partial a_i} = \sum_j\delta_j\frac{\partial a_j}{\partial a_i}\qquad(5.55)$$

となります。（下図参照）

さらに、素子 $i$ の活性化関数を $h_i$ とすると、

$$a_j = \sum_i w_{ji} h_i(a_i)\qquad(5.48)$$

だった（素子 $i$ への入力和を活性化関数に通しそれぞれに重み $w_{ji}$ をかけた和を素子 $j$ に伝える）ので、

$$ \frac{\partial a_j}{\partial a_i} = w_{ji} h_i'(a_i)$$

となります。

ここまでをまとめると、誤差 $\delta_i$ の逆伝播公式

$$ \delta_i = h_i'(a_i) \sum_j w_{ji}\delta_j\qquad(5.56)$$

が得られます。

また、ネットワークの出力部における $\delta$ の値は直接計算する事が出来るので、そこから逆にネットワークを辿りながら各 $\delta$ を計算する事が可能になります。

このネットワークを逆に辿る過程で各 $w_{ji}$ に対する $W$ 個の偏導関数を一回の誤差伝播で求めてしまう事が出来るので、効率の良いアルゴリズムとなっています。

### 【まとめ】¶

1. データ $\mathbf{x}_n$ を入力した時の、各素子への入力 $a_i$ 出力 $z_i$ を求める。（順伝播）
2. ネットワークの出力部における誤差 $\delta_i$ を計算する。
3. 逆伝播公式を利用して各素子における $\delta_i$ を計算する。 $$\delta_i = h_i'(a_i) \sum_j w_{ji}\delta_j$$
4. 以下を利用して必要な偏微分係数を求める。 $$\frac{\partial E_n}{\partial w_{ji}} = \delta_j z_i$$

## 4. 実装 (Implementation)

In [2]:

    import numpy as np
    import matplotlib.pyplot as plt

### 初期化¶

In [3]:

    def Zeros(shape, dtype=None):
        return np.zeros(shape=shape, dtype=dtype)

    def RandomNormal(shape):
        return np.random.normal(loc=0, scale=0.05, size=shape)

    InitializeHandler = {
        'zeros': Zeros,
        'random_normal': RandomNormal,
    }

    def Initializer(initializer_name):
        return InitializeHandler[initializer_name]

### 誤差関数¶

In [4]:

    class mean_squared_error():
        def loss(y_true, y_pred):
            return np.mean(np.square(y_pred - y_true), axis=-1)
        def diff(y_true, y_pred):
            return y_pred-y_true

    LossHandler = {
        'mean_squared_error' : mean_squared_error,
    }

    def LossFunc(loss_func_name):
        return LossHandler[loss_func_name]

### 各層¶

In [5]:

    class Layers():
        NLayers=1

        def __init__(self, name, units):
            self.name=f"Layer{Layers.NLayers}.{name}"
            self.outdim=units
            self.w = None
            Layers.NLayers+=1

        def build(self, indim):
            self.indim = indim
            self.w = np.c_[
                self.kernel_initializer(shape=(self.outdim,self.indim)),
                self.bias_initializer(shape=(self.outdim,1))
            ]

    class Input(Layers):
        def __init__(self, inputdim):
            super().__init__("inputs", inputdim)

    class Dense(Layers):
        def __init__(self, units, activation='linear', kernel_initializer='random_normal', bias_initializer='zeros'):
            """
            @param units             : (tuple) dimensionality of the (input space, output space).
            @param activation        : (str) Activation function to use.
            @param kernel_initializer: (str) Initializer for the `kernel` weights matrix.
            @param bias_initializer  : (str) Initializer for the bias vector.
            """
            super().__init__("dense", units)
            self.kernel_initializer = Initializer(kernel_initializer)
            self.bias_initializer   = Initializer(bias_initializer)
            self.h = ActivationFunc(activation)
            self.z = None
            self.a = None

        def forward(self, input):
            """ @param input: shape=(Din,) """
            z_in = np.append(input,1) # shape=(Din+1,)
            a = self.w.dot(z_in)      # (Dout,Din+1) @ (Din+1,) = (Dout,)
            z_out = self.h.forward(a) # shape=(Dout,)
            self.z = z_in
            self.a = a
            return z_out

        def backprop(self, dEdz_out):
            """ @param dEdz_out: shape=(Dout,) """
            dEda = self.h.diff(self.a)*dEdz_out # δ, shape=(Dout,)
            dEdz_in = self.w.T.dot(dEda)        # (Din+1,Dout) @ (Dout,) = (Din+1,)
            self.update(dEda)
            return dEdz_in[:-1]                 # shape=(Din,) term of bias is not propagated.

        def update(self, delta, ALPHA=0.01):
            """ @param delta: shape=(Dout,) """
            dw = np.outer(delta, self.z) # (Dout,) × (Din+1,) = (Dout,Din+1)
            self.w -= ALPHA*dw # update. w → w + ALPHA*dw

### モデル（各層のスタック）¶

In [6]:

    class Sequential():
        def __init__(self, layer=None):
            self.layers = []
            self.epochs = 0
            if layer is not None: self.add(layer)
            self.loss = None
            self.config = None

        def add(self, layer):
            """Adds a layer instance."""
            self.layers.append(layer)

        def compile(self, loss, input_shape=None):
            """ Creates the layer weights. """
            self.loss = LossFunc(loss)
            units = [l.outdim for l in self.layers]
            for i,l in enumerate(self.layers):
                if l.name[-6:] == "inputs": continue
                l.build(indim=units[i-1])

        def fit(self, x_train, y_train, epochs=1000):
            goal_epochs = self.epochs+epochs
            digit=len(str(goal_epochs))
            for e in range(epochs):
                for x,y in zip(x_train,y_train):
                    out = self.forward(x)
                    self.backprop(y, out)
                self.epochs+=1
                y_pred = self.predict(x_train)
                mse = np.mean((y_pred-y_train)**2)
                if self.epochs % 100 == 99: print(f'[{self.epochs+1:{digit}d}/{goal_epochs:{digit}d}] mse={mse:{4}f}')

        def forward(self, input):
            out=input
            for l in self.layers:
                if l.name[-6:] == "inputs": continue
                out=l.forward(out)
            return out

        def backprop(self, y_true, out):
            dEdz_out = self.loss.diff(y_true, out)
            for l in reversed(self.layers):
                if l.name[-6:] == "inputs": continue
                dEdz_out = l.backprop(dEdz_out)

        def predict(self, x_train):
            if np.ndim(x_train) == 1:
                return self.forward(x_train)
            else:
                return np.array([self.forward(x) for x in x_train])

### 実装例¶

In [7]:

    model = Sequential(Input(1))
    model.add(Dense(3, activation="tanh", kernel_initializer="random_normal", bias_initializer="zeros"))
    model.add(Dense(3, activation="tanh", kernel_initializer="random_normal", bias_initializer="zeros"))
    model.add(Dense(1, activation="tanh"))

In [8]:

    print("Units:   {}".format([l.outdim for l in model.layers]))
    print("Layers:  {}".format([l.name for l in model.layers]))
    print("Weights: {}".format([l.w for l in model.layers]))
    model.compile(loss="mean_squared_error")
    print("Weights: {}".format([l.w.shape if l.w is not None else None for l in model.layers]))

    Units:   [1, 3, 3, 1]
    Layers:  ['Layer1.inputs', 'Layer2.dense', 'Layer3.dense', 'Layer4.dense']
    Weights: [None, None, None, None]
    Weights: [None, (3, 2), (3, 4), (1, 4)]

In [9]:

    N = 1000
    func = lambda x:x**2
    X = np.linspace(-1, 1, N).reshape(-1,1)
    Y = np.vectorize(func)(X)

In [10]:

    model.fit(X, Y)

    [ 100/1000] mse=0.027993
    [ 200/1000] mse=0.007164
    [ 300/1000] mse=0.001297
    [ 400/1000] mse=0.000846
    [ 500/1000] mse=0.000842
    [ 600/1000] mse=0.000807
    [ 700/1000] mse=0.000766
    [ 800/1000] mse=0.000728
    [ 900/1000] mse=0.000694
    [1000/1000] mse=0.000664

In [11]:

    Y_pred = model.predict(X)

In [12]:

    plt.plot(X, Y_pred, label="Neural Network", color="red")
    plt.scatter(X, Y, s=1, label="data", color="blue")
    plt.xlabel("x")
    plt.ylabel("y")
    plt.legend()
    plt.show()

In [ ]:

« Previous Next »