LoginSignup
8
8

More than 5 years have passed since last update.

ニューラルネットワークの超概要を理解してpythonに落とし込んでみる

Last updated at Posted at 2018-05-04

レベル

  • ニューラルネットワークってなに?ってレベルの人向け(超初心者)
  • 主に自分への備忘録も兼ねてます

参考文献

ニューラルネットワーク自作入門
65933_ext_06_0.jpg

1.ニューラルネットワークの概要(入力〜出力まで)

  • 入力層・隠れ層・出力層の3*3のニューラルネットワークを想定
  • 図と実際の値があった方がわかりやすいので以下に記載

図1.概念図

スクリーンショット 2018-05-04 12.29.37.png

図2.リンクと重み

スクリーンショット 2018-05-04 12.29.31.png

図3.計算方法(入力層への入力から出力層の出力まで)

スクリーンショット 2018-05-04 14.16.43.png

シグモイド関数と計算方法

そもそも、ニューラルネットワークは動物のニューロンの組織図を模倣して作っている。シグモイド関数は「入力が閾値に達成すると発火する」というニューロンの特性を関数で表現しやすい。

その他の特徴としては、

  • ロジスティック関数ともいう
  • 計算が他のS字関数よりはるかに簡単
  • 0から1の値を「ちょうどよく」出力してくれるのに適している
  • 「入力が閾値に達成すると発火する」というニューロンの特性を関数で表現しやすい

シグモイド関数は以下の数式で表します。

y=\frac{1}{1+e^{-x}}

図3の(2)で$O_{hidden}$を計算したい時は、以下の計算を行います。

計算式は、図3の(2)通り、$O_{hidden} = sigmoid(X_{hidden})$である。

X_{hidden}=
\begin{pmatrix}
1.16\\
0.42\\
0.62
\end{pmatrix}\\

となっているので。

O_{hidden} =sigmoid * 
\begin{pmatrix}
1.16\\
0.42\\
0.62
\end{pmatrix}\\

例えば、1行1列目を計算するとx=1.16のとき、$e^{-1.16}=0.3135$なので、$y=\frac{1}{1+0.3135}=0.761$となる。

したがって、

O_{hidden} =
\begin{pmatrix}
0.761\\
0.603\\
0.650
\end{pmatrix}

となる。

2.誤差逆伝播と重み更新

誤差逆伝播

1.で導き出した実際の理想値targetsと実際の出力値$O_{output}$には、どうしても誤差が出てしまいます。

理想値に近づけるには重みなどのパラメータを調整する必要があります。ニューラルネットワークでは、最終的な誤差を逆向きに伝送(伝播)していって、重みを更新する、「誤差逆伝播(ごさぎゃくでんぱ)」という手法を用います。概念図を以下に記載します。
ここでは、今まで使っていた3*3の行列ではなく、話を簡潔にするために2*3の行列で説明します。

誤差を徐々に前向きに更新していっているのがわかると思います。

図4.誤差逆伝播
スクリーンショット 2018-05-04 14.55.29.png

エラーの計算

図4の下に記載している数式のように、前のノードのエラーはこのように重みづけwを用いて更新していきます。

ただ、一つ一つ計算するのは大変です。行列の掛け算で実現できないでしょうか?

行列の掛け算による誤差逆伝播の計算

図4のように、最終的な理想値targetsと実際の出力値$O_{output}$の差を誤差$error_{output,1}=0.8$と$error_{output,2}=0.5$ととする。

これを行列で書くとこんな感じの式になる

error_{output} =
\begin{pmatrix}
e_1\\
e_2
\end{pmatrix}

図4の下の計算式を見てもわかる通り、$error_{hidden}$の行列は、上の$error_{output}$を使って、以下の計算で成り立つことがわかります。


error_{hidden} =
\begin{pmatrix}
\frac{w_{11}}{w_{11}+w_{21}} & \frac{w_{12}}{w_{12}+w_{22}} \\
\frac{w_{21}}{w_{21}+w_{11}} & \frac{w_{22}}{w_{22}+w_{12}} 
\end{pmatrix}
*
\begin{pmatrix}
e_1\\
e_2
\end{pmatrix}

最も重要な部分はリンクの重み$w_{ij}$と出力誤差$e_n$との掛け算です。重みが大きいほどより多くの出力誤差が隠れ層に渡されます。ここが重要です。

思い切って行列の分母を省いてしまいます。

そうすると、以下のようなシンプルな式になります。


error_{hidden} =
\begin{pmatrix}
w_{11}&w_{12}\\
w_{21}&w_{22}
\end{pmatrix}
*
\begin{pmatrix}
e_1\\
e_2
\end{pmatrix}

行列の左の項は、出力値をだしたときの重みの計算($O=W*I$)の時の$W$の行列

\begin{pmatrix}
w_{11}&w_{21}\\
w_{12}&w_{22}
\end{pmatrix}

の並び順を変えたものです。これを転置行列と言います。$W^T$と書きます。

以上のことを以下の表現で表します。これで誤差逆伝播の誤差の計算は求められました。

$error_{hidden}=W^T_{hidden-output} * error_{output}$

なぜ分母を削除して良いかはおいおい勉強します。。

重みの更新

逆伝播した各層の誤差がこれで計算できたので、この誤差を用いて、いよいよ重みを修正します。

最終的な重みの更新の行列式は以下です。隠れ層と出力層を例にしています。同じことを入力層と隠れ層にも適用しますので、まずは隠れ層と出力層だけ考えます。
誤差は微分をとって傾きを計算し、最小値を求めます。

\Delta W_{jk} = 学習率\alpha * E_k * O_k(1-O_k) * O^T_j$

一つ一つ見ていきます。

$\Delta W_{jk}$は隠れ層のノードjと出力層のノードkの重みの変化率です。これが知りたい値です。

$\alpha$は学習率です。これは定数です。ここまでは簡単。

問題は「$E_k * O_k(1-O_k) * O^T_j$」この式です。
整理します。

  • 知りたいのは、重み$W_{jk}$を変化させると、誤差$E$はどのように変化するかということ
  • 誤差$E$が最小値をとれば、当然誤算がなくなるので理想値に近づく
  • 誤差$E$を重み$W_{jk}$で微分してあげれば、傾きを求められるので、最小値が求められる。

よって、知りたいのは以下の式です。理想値targetを$t_k$ 出力値を$o_k$としています

\frac{\delta E}{\delta W_{jk}} = \frac{\delta (t_k - o_k)^2}{\delta W_{jk}}

そのままだと微分するのが大変なので、合成関数(連鎖律)を使います。

  • 連鎖律
\frac{\delta y}{\delta x} = \frac{\delta y}{\delta u} * \frac{\delta u}{\delta x}

ここでは以下のような式になります。

\frac{\delta E}{\delta W_{jk}} = \frac{\delta E}{\delta o_k} * \frac{\delta o_k}{\delta W_{jk}}

一つ目の項を変形します。
$\delta (t_k - o_k)^2$を微分すると$-2 (t_k - o_k)$になります。なぜマイナスが付いているかというと、「正の勾配を持つときは重みを減少させたい、負の勾配を持つときは重みを増加させたい」からです。

二つ目の項も変形します。
図(3)で見た通り、出力値を出したいときはシグモイド関数を使いました。計算式は、図3の(2)通り、$O_{hidden} = sigmoid(X_{hidden})$です。$o_k$にこのシグモイド関数を使った式を代入します。

\frac{\delta o_k}{\delta W_{jk}}=\frac{\delta * sigmoido(\sum_j W_{jk}*o_j)}{\delta W_{jk}}

ここでいう$o_j$は隠れ層の出力なので図3の$O_{hidden}$と同義です。

シグモイド関数を微分することは至難の技なので、既知の公式を用います。

  • シグモイド関数の微分の公式
\frac{\delta * sigmoido(x)}{\delta x} = sigmoid(x)( 1-sigmoid(x))

これを使うと、

\frac{\delta E}{\delta W_{jk}}= -2 (t_k - o_k) * sigmoid(\sum_j W_{jk}*o_j)( 1-sigmoid(\sum_j W_{jk}*o_j)) * \frac{\delta (\sum_j W_{jk}*o_j)}{\delta W_{jk}}

となります。

最後の$\frac{\delta (\sum_j W_{jk}*o_j)}{\delta W_{jk}}$はsigmoid関数の中にある式をもう一度合成関数を使っているため、この項が出てきます。ただ、これは計算すると$o_j$と同じのため、以下の式に修正します。ついでに、-2も取り除きます。係数は2だろうが100だろうが微分では消えてしまうため、不要だからです。
まとめると以下の式になります。

\frac{\delta E}{\delta W_{jk}}= -(t_k - o_k) * sigmoid(\sum_j W_{jk}*o_j)( 1-sigmoid(\sum_j W_{jk}*o_j)) * o_j

日本語に直すと、
隠れ層jと出力層kの間の重み($W$) = 出力層kの誤差($E_{output}$)の微分 * 隠れ層jの出力値($O_{hidden}$)

これに学習率を戻してあげると、最終的な式は以下になります。

  • 誤差を重みに適用する計算式
\Delta W_{jk} = 学習率\alpha * E_k * O_k(1-O_k) * O^T_j

3.pythonのコードに落とし込む

前置きがだいぶ長くなりましたが、pythonのコードに落とすとこんな感じです。

neural_network.py
import numpy
import scipy.special #シグモイド関数expit()使用のため

# ニューラルネットワーククラスの定義
class neuralNetwork:

    # ニューラルネットワークの初期化
    def __init__(self, inputnodes, hiddennodes, outputnodes, learningrate):
        # 入力層 隠れ層 出力層のノード数の設定
        self.inodes = inputnodes
        self.hnodes = hiddennodes
        self.onodes = outputnodes

    # リンクの重み3*3の行列を作る 
    #input-hiddenのweight(wih) と hidden-outputのweight(who)の2種類
        # 行列内の重み weight_ノードi_ノードjへのリンクの重み
        # 重みの値は、0.0を平均としてリンクの数の平方根の逆数の範囲にする。pow(self.hnodes, -0.5)はself.hnodesを-1/2乗(power)している(つまり平方根の逆数を取る計算)
        # numpy.random.normal(loc(平均) = 0.0、scale(範囲) = 1.0、size = None(例えば(2,3)なら2行3列の行列を作る) )
        # w11 w21
        # w12 w22 など
        self.wih = numpy.random.normal(0.0, pow(self.hnodes, -0.5), (self.hnodes, self.inodes))
        self.who = numpy.random.normal(0.0, pow(self.onodes, -0.5), (self.onodes, self.hnodes))

    # 学習率の設定
        self.lr = learningrate

    # 活性化関数(シグモイド関数)の定義
        self.activation_function = lambda x: scipy.special.expit(x)


        pass

    # ニューラルネットワークの学習
    def train():

    #---------------------------------入力値と出力値の計算--------------------------------------------------------------
        # 入力リストを行列に変換(.Tは.transpose()と一緒で転置行列を表す)
            #入力は横並びのリストに入れるので、それを縦に並べる処理
            # 例:
            # input=>  a = ([1.0, 0.5, -1.5])
            # input=>  b = numpy.array(a, ndmin=2).T
            # input=>  b
            # output=>  array([[ 1. ],
            # output=>     [ 0.5],
            # output=>     [-1.5]])

            # numpy.array(object, dtype=None, copy=True, order='K', subok=False, ndmin=0)
            # ndminは結果の配列に必要な最小次元数を指定します。この要件を満たすために、必要に応じて形状にあらかじめペンディングされます。

        inputs = numpy.array(inputs_list, ndmin=2).T
        targets = numpy.array(targets_list, ndmin=2).T    #targetsは目標出力


        # 隠れ層に入ってくる信号の計算
        # 重みとinputの行列を計算して出力値をだす
          #numpy.dot(a, b, out = None)
          # aは左からかける行列 bは右からかける行列
        hidden_inputs = numpy.dot(self.wih,inputs)


        # 隠れ層で結合された信号を活性化関数により出力
        hidden_outputs = self.activation_function(hidden_inputs)

        # 出力層に入ってくる信号の計算
        final_inputs = numpy.dot(self.who,hidden_outputs)
        # 出力層で結合された信号を活性化関数により出力
        final_outputs = self.activation_function(final_inputs)

    #---------------------------------誤差逆伝播の計算--------------------------------------------------------------
        #目標出力(targets) と最終出力(final_outputs)の差分を計算して出力層の誤差を求め、それを使って転置行列をかけると隠れ層の誤差が更新される。

        #出力層の誤差 = (目標出力(targets) - 最終出力(final_outputs)) 
        output_errors = targets - final_outputs
        #隠れ層の誤差は出力層の誤差をリンクの重みの割合で更新(転置行列をかける)
        hidden_errors = numpy.dot(self.who.T, output_errors)

    #---------------------------------重みの更新の計算--------------------------------------------------------------

        #上で誤差が更新されたら、その誤差を使って重みを更新する

        #隠れ層と出力層の間のリンクの重みを更新
        self.who += self.lr * numpy.dot((output_errors * final_outputs * (1.0 - final_outputs)), numpy.transpose(hidden_outputs))
        #入力層と隠れ層の間のリンクの重みを更新
        self.wih += self.lr * numpy.dot((hidden_errors * hidden_outputs * (1.0 - hidden_outputs)), numpy.transpose(inputs))

        pass



    # ニューラルネットワークへの照会
    def query(self, inputs_list):

        # 入力リストを行列に変換
        inputs = numpy.array(inputs_list, ndmin=2).T

        # 隠れ層に入ってくる信号の計算
        hidden_inputs = numpy.dot(self.wih,inputs)
        # 隠れ層で結合された信号を活性化関数により出力
        hidden_outputs = self.activation_function(hidden_inputs)

        # 出力層に入ってくる信号の計算
        final_inputs = numpy.dot(self.who,hidden_outputs)
        # 出力層で結合された信号を活性化関数により出力
        final_outputs = self.activation_function(final_inputs)

        return final_outputs

        pass


パラメータは以下のように設定します。

para.py
#入力層 隠れ層 出力層のノード数
input_nodes = 3
hidden_nodes = 3
output_nodes = 3

#学習率
learning_rate = 0.3

#ニューラルネットワークのインスタンスの生成
n = neuralNetwork(input_nodes,hidden_nodes,output_nodes,learning_rate)

ここまで。次回は実際にこのコードを動かすところを記載します。

8
8
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
8
8