ニューラルネットワーク

ディープラーニングを学ぶ上で最も基礎となる概念がこのニューラルネットワークだ。
これは、脳のニューロンとかシナプスとかをモデルにできたものだそうだ。
まあ、その辺はここではあまり触れないことにする。Wikipediaとかを見れば良いと思う。

ニューラルネットワークを調べると最も出てくるのが以下の図だろう。

f:id:atily529:20190826220210p:plain
ニューラルネットワーク

この図の各円をユニット(またはニューロン)とよぶ。
最も左側のユニットに入力 \boldsymbol{x}を入れ、最も右側のユニットから \boldsymbol{y}を取り出す。
そして、真ん中の矢印やユニットが \boldsymbol{f}に相当する。

この図を見てみるとわかるように、縦に四列、ユニットが並んでいる。
このようなニューラルネットワークの各列をと呼ぶ。
最も左の層が入力層、右の層が出力層。それ以外の真ん中にある二列の層は中間層(隠れ層)と呼ぶ。

また、各矢印は脳のシナプスに相当する。
よく見ると、各層のユニットが、次の層のすべてのユニットがシナプスでつながっていることがわかる。
このような層を全結合層と呼ぶ。

実際にはこんなに綺麗に矢印を引かなければならないわけではなく、ユニット同士を複雑に絡ませた矢印を引いたネットワークでもよい(例えば中間層のものを再帰させてループさせたりとか…)。
ただ、上のような全結合層は計算も説明も楽なので、最初はこのような全結合層で説明していこう。



さて、上の図を例に定式化していこう。
入力層のユニットは三つ、出力層のユニットも三つなので、入力 \boldsymbol{x}、出力 \boldsymbol{y}は共に3次元のベクトル、もしくは 1\times3の行列であるといえる。
これを次のように定義する。
  \boldsymbol{x}= \left[ \begin{matrix}x_1 \\ x_2 \\ x_3 \end{matrix} \right] ,  \boldsymbol{y}= \left[ \begin{matrix}y_1 \\ y_2 \\ y_3 \end{matrix} \right]
この入力・出力はニューラルネットワーク全体のものである。

各ユニットにもそれぞれ入力と出力がある。
これを、 u vという文字を当てる。ここは文献によって結構違う文字が多いので混乱しないように。
これも、上の \boldsymbol{x}, \boldsymbol{y}と同じように、添字をつけて分類することにする。
さらに、層を区別するために例えば第l層の場合文字の右上に < l >のように添える。
例えば、 l層の k番目のユニットの入力は u_k^{< l >}となる。
入力、出力と同様に l層におけるユニットの入力 uもベクトル表記として、上の図のユニットを表現しよう。
 \boldsymbol{u}^{< 1 >} = \left[ \begin{matrix}u_1^{< 1 >} \\ u_2^{< 1 >} \\ u_3^{< 1 >} \end{matrix} \right] ,  \boldsymbol{u}^{< 2 >} = \left[ \begin{matrix}u_1^{< 2 >} \\ u_2^{< 2 >} \\ u_3^{< 2 >} \\ u_4^{< 2 >} \end{matrix} \right] ,  \boldsymbol{u}^{< 3 >} = \left[ \begin{matrix}u_1^{< 3 >} \\ u_2^{< 3 >} \\ u_3^{< 3 >} \\ u_4^{< 3 >} \end{matrix} \right] ,  \boldsymbol{u}^{< 4 >} = \left[ \begin{matrix}u_1^{< 4 >} \\ u_2^{< 4 >} \\ u_3^{< 4 >} \end{matrix} \right]
出力 vも同様である。
ここで、入力 \boldsymbol{x}=\boldsymbol{u}^{< 1 >}、出力 \boldsymbol{y}=\boldsymbol{v}^{< 4 >}である。


各ユニットの入力は活性化関数 \phiを通して出力へ向かう。この活性化関数がどんな形をしているのかはとりあえず置いといて、これは次式で表せる。
 v_k^{< l >} = \phi (u_k^{< l >})
また、各ユニットの出力から、重みを乗じてから、次の層の入力へと向かう。つまり、以下のようになる。
 u_j^{< l+1 >} = w_{k,j}^{ < l > }v_k^{< l >}
この重み w_{k,j}^{ < l > }はユニットの出力 v_k^{< l >} から次の層のユニットの入力 u_j^{< l+1 >}に結ばれる矢印に設定される。
教師あり学習ディープラーニングでは、この w_{k,j}を上手いこと設定(最適化)することが目的である。
以上2つは、ベクトルと行列表記で次のように表現できる。

 \boldsymbol{v}^{< l >} = \boldsymbol{\phi} (\boldsymbol{u}^{< l >})
 \boldsymbol{u}^{< l+1 >} = \boldsymbol{W}^{ < l > }\boldsymbol{v}^{< l >}

なお、この2つ目の式にはバイアス \boldsymbol{b}を足すことが多い。つまり、次式のように表すことができる。
 \boldsymbol{u}^{< l+1 >} = \boldsymbol{W}^{ < l > }\boldsymbol{v}^{< l >} + \boldsymbol{b}^{< l >}


活性化関数 \phiはいろいろな関数が選ばれるが、とりあえず、以下の4つを紹介する。
シグモイド関数:  \displaystyle\phi (x) = \frac{1}{1+e^{x}}
双曲線正接関数:  \phi (x) = \tanh(x)
ReLU:  \phi (x) = \left\{ \begin{matrix} x \hspace{10pt} \text{(if  $x>0$)} \\ 0 \hspace{10pt} \text{(if  $x\le0$)} \end{matrix} \right.
恒等関数:   \phi(x)=x

これで大体のニューラルネットワークと、順伝播は説明終わった。
簡単にプログラムを作ってみよう

まず、Layerクラスをつくろう。
nUnitにその層のユニット数を、actFuncにはその層の活性化関数を渡す。
ユニットの入力 u、出力 vの初期値は0とする。

class Layer(object):
    def __init__(self,nUnit,actFunc='identity'):
        self.nUnit   =nUnit
        self.inUnit  =np.zeros((nUnit)) #ユニットの入力u
        self.outUnit =np.zeros((nUnit)) #ユニットの出力v
        self.actFunc =actFunc

    def activate(self): #活性化
        if   self.actFunc == "sigmoid": #シグモイド関数
            self.outUnit = 1/(1+np.exp(-self.inUnit))
        elif self.actFunc == "tanh": #双曲線正接関数
            self.outUnit = np.tanh(self.inUnit)
        elif self.actFunc == "ReLU": #ReLU
            self.outUnit = np.where(self.inUnit <= 0.0, 0.0, self.inUnit)
        else: #恒等関数
            self.outUnit = self.inUnit

次に、Weightクラスを作る。
今後説明するが、重み wは全て同じ値にするのは良くないことがわかっている。
そのため、重みの初期値はランダムな値にし、バイアスは0とする。
引数としては、まず、nUnit1は出力 v^{< l >}のユニット数、nUnit2は入力 u^{< l+1 >}のユニット数である。
また、randomWidthは重みの初期値の範囲である。

class Weight(object):
    def __init__(self,nUnit1,nUnit2,randomWidth = 1):
        self.weight  = np.random.rand(nUnit2,nUnit1)*randomWidth
        self.bias    = np.zeros((nUnit2))

最後に、上の2つのクラスをまとめて、一つのニューラルネットワークを作ろう。
引数については、nUnitsが各層のユニット数。actFuncsは各層の活性化関数を与える。
また、calcForwardPropagation関数では、順伝播の計算をする。
この関数のinVecは最初の入力 \boldsymbol{x}とする。

N_UNITS=[3,4,4,3]
ACT_FUNCS=['identity','ReLU','ReLU','identity']

class NeuralNetwork(object):
    def __init__(self,nUnits=N_UNITS, actFuncs=ACT_FUNCS, randomWidth=1):
        self.nLayer  =len(nUnits)
        self.nUnits=nUnits
        self.actFuncs=actFuncs
        self.layers  =[Layer (self.nUnits[i], self.actFuncs[i]) for i in range(self.nLayer) ]
        self.weights =[Weight(self.nUnits[i], self.nUnits[i+1],randomWidth) for i in range(self.nLayer-1)]

    def calcForwardPropagation(self,inVec):  # 順伝播
        self.layers[0].inUnit=inVec
        for i in range(self.nLayer - 1):
            self.layers[i].activate()
            self.layers[i+1].inUnit = np.dot(self.weights[i].weight,self.layers[i].outUnit)+self.weights[i].bias
        self.layers[-1].activate()
        return self.layers[-1].outUnit

これで、ニューラルネットワークは完成だ。
完成したものは下に上げる。
1_ForwardPropagation.py - Google ドライブ

適当に入力[1,1,1]を与えると、[6.49604473 6.02350685 5.87241463]と帰ってきた。
重みはランダムだからやるたびに値は変わるし、この値自体に意味はまったくない。

次は誤差(損失)関数について話していこう