誤差関数(損失関数)

入力を適当なニューラルネットワークを順伝播した出力は当然狙ったものと大きな違いがあるだろう
その得られた出力と正解データの差を学習を通して縮めていくのが教師あり学習だ。

つまり、適当なニューラルネットワークから得られる入力 \boldsymbol{x}と出力 \boldsymbol{y}のペアが、入力 \boldsymbol{x}と正解データ \tilde{\boldsymbol{y}}に近づくように、重み \boldsymbol{W}やバイアス \boldsymbol{b}を変化させながら0に近づけていく必要がある。
このニューラルネットワークの出力と、正解データの差は誤差関数もしくは損失関数と表現する。今回は誤差関数で統一しようと思う。

この誤差関数も(その差が縮まるにつれて値が0に近づくならば)自分の好きなように設定していいのだが、一般的に使われる誤差関数というのは限られている。
今回は、分類(データの種類をよそくすること)ではなく、回帰(データの値を予測すること)でよく使われている二乗誤差関数というものを紹介する。

二乗誤差関数:  \displaystyle E(\boldsymbol{y},\tilde{\boldsymbol{y}}) = \frac{1}{2} (\boldsymbol{y} - \tilde{\boldsymbol{y}})^2 =\frac{1}{2} \sum_{j} (y_j - \tilde{y}_j)^2

まあ、見慣れた人にとってはよく見る式だろう。等式の二つ目はベクトルで、三つ目はベクトルの要素で表現した式である。
式の頭の1/2が気になった人もいるだろう。これは E(\boldsymbol{y},\tilde{\boldsymbol{y}})偏微分すると、
 \displaystyle \frac{E(\boldsymbol{y},\tilde{\partial \boldsymbol{y}})}{\partial y_i} = y_i-\tilde{y}_i
と上手く消えてくれるから慣習的につけているだけであって、特になくても問題ない。

なお、この結果から
 \displaystyle \nabla E(\boldsymbol{y},\tilde{\boldsymbol{y}}) = \left[ \begin{matrix} \frac{\partial E}{\partial  y_1} \\ \frac{\partial E}{\partial y_2}  \\ \vdots \\ \frac{\partial E}{\partial y_n} \end{matrix} \right] =\left[ \begin{matrix} y_1-\tilde{y}_1 \\ y_2-\tilde{y}_2  \\ \vdots \\ y_n-\tilde{y}_n \end{matrix} \right] =\boldsymbol{y}-\tilde{\boldsymbol{y}}
ということがわかる。この \nabla E(\boldsymbol{y},\tilde{\boldsymbol{y}})さえあれば、どの[\boldsymbol{y}]の要素で偏微分したとしても、すぐに取り出すことができる。

プログラムは次のようになる。短いのでまとめて書いた。

class ErrorFunction(object):
    def __init__(self, nOutput, errFunc='SqueredError'):
        self.errFunc=errFunc
        self.error=0
        self.diffError=np.array([0 for i in range(nOutput)])

    def calculate(self, outUnit,testData):
        trainData=np.array(trainData)
        if self.errFunc=='SqueredError':
            self.error=0.5*np.sum((outUnit-trainData)**2)

    def calcDiff(self, outUnit,trainData):
        trainData=np.array(trainData)
        if self.errFunc=='SqueredError':
            self.diffError=outUnit-trainData

難しいことはなにもないが、一応説明していこう。
まず、クラスErrorFunctionはnOutput(出力の個数)とerrFunc(誤差関数の種類)を引数に持つ。
今回は二乗誤差の一つしかないが、今後増える可能性があるのでこのように引数を取るよう設定しておこう。
このクラスがオブジェクト生成された時点で、errorとdiffErrorという変数が生成される。
これはそれぞれ、誤差関数の計算結果と、その勾配( \nabla E(\boldsymbol{y},\tilde{\boldsymbol{y}}))の値を格納する変数だ。

次にcalculateは誤差関数を計算する関数で、calcDiffはその勾配を計算する関数だ。
それぞれ引数に出力と正解データを入れれば勝手に計算してくれる。

さて、次はニューラルネットワークに学習させ、重みを少しずつ変えてこの誤差を減少させていこう。
そのためには誤差逆伝播法という、数式が多くなり理解が難しい、ちょっとした壁のような手法を学んでいく。
先に行っておくが、この誤差逆伝播法というのは、ディープラーニングの一つの高級なアルゴリズム、というわけではなく、単なる偏微分を漸化式的に楽に解くための手法と考えていい。逆に言えば、使わなくても理論的には問題ない。そう聞くとちょっと気が楽になるだろう。

もちろん、そのためには偏微分のちょっとした知識が必要なのだが、それはまたそのときに説明しよう。

ニューラルネットワーク

ディープラーニングを学ぶ上で最も基礎となる概念がこのニューラルネットワークだ。
これは、脳のニューロンとかシナプスとかをモデルにできたものだそうだ。
まあ、その辺はここではあまり触れないことにする。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]と帰ってきた。
重みはランダムだからやるたびに値は変わるし、この値自体に意味はまったくない。

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

教師あり学習

きっかけ

近年流行っているディープラーニングについて、自分に定着させるためのノート的にここに記す。
ここでは、実装のための記事というよりも、どちらかというと理論的なことの話をしてこうと思う。

教師あり学習

機械学習という大きな枠組みの中の教師あり学習教師なし学習強化学習という3つのアルゴリズムが現在主流であることはこれを読んでいる人ならわかっていると思う。
巷で言われているディープラーニングとは、普通、教師あり学習のことを指している事が多い。
今回はその教師あり学習について記述していこうと思う

教師あり学習がなにか、ということについてもネットにゴロゴロと情報が転がっているので、特に必要ないと思うが、より理論っぽく書くとこういうふうに言えると思う。

教師あり学習
入力 \boldsymbol{x}に対する正解値 \boldsymbol{y}を大量に与え、 \boldsymbol{y}=\boldsymbol{f}(\boldsymbol{x})となる関数 \boldsymbol{f}を予想するアルゴリズム

最初はこの定義に伴って、 y=\sin(x)のような、簡単な関数系をディープラーニングで予想するプログラムを作っていこう。
今では、いろいろなライブラリがあり、実装がかなり簡単にかけるようになっているが、この記事では、理論の勉強ということもあり、pythonのnumpyだけで作っていこうと思う。


1. ニューラルネットワークと順伝播
2. 損失(誤差)関数
3. 誤差逆伝播
4. 最適化

自由落下運動のシミュレーションと可視化

最初は高校生の物理で一番最初に習うであろう、自由落下運動を例にシミュレーションしてみよう。
簡単すぎると思うかもしれないが、初めての人にもとっつきやすいように誰でもわかるようなものからシミュレーションしていく。
そのうち、複雑なシミュレートもしていくから、とりあえずこれで我慢してほしい。

ということで、以下の問題について考えよう。

初期位置(x_0,y_0)から初期速度(v_{x0},v_{y0})で質点を投げ出したとき、
その質点はどのような動くだろうか?
ただし、空気抵抗は考えないこととする。

自由落下運動では、ある質点に下向きにg加速度が働いている。
上向きを正とすることによって、時刻tにおける変位は

\begin{equation}
y=v_0t-\frac{1}{2}gt^2
\end{equation}

となる。
上の式は、t=0のときの位置を考慮していないため、それをy_0とすることで、正しくは次のようにあらわされる。

\begin{equation}
y=y_0+v_0t-\frac{1}{2}gt^2
\end{equation}

大学では、ベクトル表記を使うことの方が多い。早めになれるために、今のうちから使っておこう。このベクトルはプログラムにおける配列の考え方と一致してる。
上記の加速度はx方向に0,y方向に-gと考えることができるため、初期位置、初期速度、加速度をベクトルで表現すると次のようにあらわせる。

なお、大学風に、ベクトルは\vec{x}のように上付き矢印ではなく、\boldsymbol{x}のように、太字で表記する。
(筆者も最初はこの表記に違和感を覚えていたが、高校で学習するベクトル=矢印というイメージが必ずしも本質的ではないことを理解して、解消されてきた)。
\begin{equation}
\boldsymbol{a}=\left( \begin{array}{cc} 0\\ -g\\ \end{array} \right),\ \
\boldsymbol{x}_0=\left( \begin{array}{cc} x_0\\ y_0\\ \end{array} \right),\ \
\boldsymbol{v}_0=\left( \begin{array}{cc} v_{x0}\\ v_{y0}\\ \end{array} \right),\ \
\end{equation}

これにより、等加速度運動の式をベクトルで表記すると次のようにあらわすことができる。

\begin{equation}
\boldsymbol{x}=\boldsymbol{x}_0+\boldsymbol{v}_0t+\frac{1}{2}\boldsymbol{a}t^2
\end{equation}

さて、長々と書いてしまったが、上式をそのままプラグラムにぶち込めば、自由落下のシミュレーションは完成だ。めちゃくちゃ簡単!!
念のため、コードを全文書いておく

# -*- coding utf-8 -*-

import numpy as np

First_pos=np.array([0.0,0.0])   #初期位置(m)
First_vel=np.array([4.0,5.0])   #初期速度(m/s)

G=-9.80665      #重力加速度(m/s^2)

def main():
    print("|*時刻|*位置[x,y]|*速度[vx,vy]|")
    PM1=PointMass(First_pos,First_vel)
    for t in np.arange(0,5,0.2):
        xy,v=PM1.fall(t)
        xy=np.array2string(xy,precision=5,separator=',')
        v =np.array2string(v,precision=5,separator=',')
        print("| %.2f |" % (t),xy,"|",v,"|")

class PointMass:
    def __init__(self,xy0,v0):
        self.xy0=xy0
        self.v0=v0
        self.a=np.array([0.0,G])
    def fall(self,t):
        xy=self.xy0+self.v0*t+0.5*self.a*t**2
        v=self.v0+self.a*t
        return xy,v

if __name__=='__main__':
    main()

この結果は次のようになる。

時刻 位置[x,y] 速度[vx,vy]
0.00 [0.,0.] [4.,5.]
0.20 [0.8 ,0.80387] [4. ,3.03867]
0.40 [1.6 ,1.21547] [4. ,1.07734]
0.60 [2.4 ,1.2348] [ 4. ,-0.88399]
0.80 [3.2 ,0.86187] [ 4. ,-2.84532]
1.00 [4. ,0.09668] [ 4. ,-4.80665]

それぞれのデータを"|"でわけていたり(ブログ掲載用)、無駄にclassを使っていたり(自分の練習のため)、
学習するにあたって無駄なところが多いが、重要なのは25,26行目だ。

念のため、簡単に説明すると、5,6行目で初期位置、初期速度を与えている。
初期位置は原点、初期速度は(4,5)を与えている。
そして、25行目で速度を、26行目で位置を計算している。
上の式をそのままぶち込んでいるだけだ。

上の解析結果の数値による情報は重要な時も多いが、どう言う振る舞いであるのかをイメージしにくい思う。
数値を見ただけでは質点がどのようなふるまいをするかは(この問題は簡単すぎてできると思うけど)イメージするのは難しい。

そこで、上のデータを何らかの図によってあらわそう。このことを可視化(visualization)という。
実はこの記事のテーマとして、可視化の重要性について論じようとも思っていた。

上のプログラムをmatplotlibパッケージを使ってアニメーションにしてみた。

f:id:atily529:20181005024657g:plain
自由落下の結果1
おお~アニメーションを作ったのは初めてなので、なんかうれしい。
このように図で表わすことで、質点の動きがとてもイメージしやすくなったと思う(こんなことしなくてもわかるとか言わないで)。

ここに、更なる情報を加えることもできる。例えば、軌跡を表示してみよう。

f:id:atily529:20181005025408g:plain
自由落下の結果2
このように軌跡を表示するだけで、なんとなく質点が放物線上に動きそうって予想できそう!

さらに、軌跡に速いところでは赤色に、遅くなるにつれて青色にするという情報も付け加えてみよう。

f:id:atily529:20181005042207g:plain
自由落下の結果3
ここまでくれば、頂点で速度が最も遅くなるだろうと予測できそう!

ここまで説明すれば、可視化の重要性について説明できたと思う。そして、単に可視化といっても色々な工夫の仕方があるということも。
上のプログラムを一応アップロードした。
free-fall_animation1.py - Google ドライブ
色々稚拙で恥ずかしいが、いろいろ試してみてもいいかも?

pythonと、numpy,matplotlibのパッケージ、pillowがあれば動かせると思う。
暇ならやってみてね(修正案とかももしあったら教えてください。上のプログラムに反映するかはわからないけど、参考にさせていただきます!)

pythonを用いた数値計算

現代、数値解析・シミュレーションはとても重要な学問である。
それは、コンピューターマシンの性能の向上だったり、理論体系の確立、進歩によって従来よりも複雑な問題をより高速に解くことができるようになったためであろう。
しかし、この数値解析は大学の、しかも専門的な勉強をし始めて、初めて習う学問で、とっつきにくく感じてしまう気がする。その上、人によってはプログラミングの勉強もしなければいけないため、その学問の重要性に反して、挫折する人が多いと思う。
この数値計算の要となる数学の理論は「テイラー展開」、「(少し専門的な)行列計算(線形計算)」、「フーリエ変換級数展開」、「微分方程式」など、どれも高校生の段階で学ばないというのも強い理由な気がする。

そこで、このサイトでは、数値解析のことについて、(自分用のメモという意味も含めて)書き連ねていこうと思う。
このサイトでのプログラムの説明は基本的にpythonを使う。
使いやすいだとかいろいろな理由は考えられるが、単純に筆者が使って練習したいからである。(筆者もpythonを使い始めて日が浅い。プログラムの稚拙さは勘弁して)

なお、プログラム言語自体の説明はあまりしないことにする。これは本サイトのメインが数値解析であるということもあるが、単に筆者自身の知識に不安があるため、間違ってることを多く書いてしまいそうだからである。

また、数値計算という学問を扱う以上、数学的な知識は必要になってくる。
この辺も基礎的な部分については詳細まで書かない。もしかしたら気が向いてら書くかも…?
厳密性を欠いた議論も多々すると思う。その辺は許して。

まあとにかく、数値解析やシミュレーションに特化したブログにしようと思っている。

目次

1. 落下シミュレーション
1.1. 自由落下運動のシミュレーションと可視化