Pythonでゼロからニューラルネットワークを作る。隠れ層の追加

今回は、「Pythonでゼロから作るニューラルネットワーク」の第2回目です。

  • Pythonでゼロから始めるニューラルネットワークの作り方
  • Pythonでゼロからニューラルネットワークを作る。隠れ層の追加
  • Pythonでゼロからニューラルネットワークを作る。マルチクラス分類

ニューラルネットワークの全くの初心者は、まずこのシリーズのパート1(上のリンク)を読んでください。その記事で説明されている概念に慣れたら、この記事に戻ってきて続きを読むことができます。

前回は、人工ニューラルネットワークの話を始め、Pythonで入力層と出力層が1つのシンプルなニューラルネットワークをゼロから作成する方法を見ました。このようなニューラルネットワークはパーセプトロンと呼ばれています。しかし、画像の分類や株式市場の分析など複雑なタスクを実行できる現実のニューラルネットワークは、入力層と出力層に加え、複数の隠れ層を持っています。

前回は、パーセプトロンは線形決定境界を見つけることができると結論づけました。私たちはパーセプトロンを使って、おもちゃのデータセットを使って、ある人が糖尿病かどうかを予測しました。しかし、パーセプトロンは非線形の境界を見つけることはできません。

今回は、この連載の第1回で学んだ概念をもとに、1つの入力層、1つの隠れ層、1つの出力層を持つニューラルネットワークを開発します。これから開発するニューラルネットワークは、非線形の境界を見つけることができるようになることを確認します。

データセット

この記事では、非線形に分離可能なデータが必要である。言い換えれば、直線を用いて分類できないデータセットが必要です。

幸運なことに、PythonのScikit Learnライブラリには、様々なタイプのデータセットを自動的に生成するためのツールが付属しています。

以下のスクリプトを実行し、ニューラルネットワークの学習とテストに使用するデータセットを生成してください。

from sklearn import datasets


np.random.seed(0)
feature_set, labels = datasets.make_moons(100, noise=0.10)
plt.figure(figsize=(10,7))
plt.scatter(feature_set[:,0], feature_set[:,1], c=labels, cmap=plt.cm.winter)


上のスクリプトでは、sklearn ライブラリから datasets クラスをインポートしている。100 個のデータからなる非線形データセットを作成するために、make_moons メソッドを使い、最初のパラメータとして 100 を渡します。このメソッドはデータセットを返し、プロットすると下図のように2つの半円が交互に並ぶ。

このデータは1本の直線で区切ることができないので、パーセプトロンでは正しく分類できないことがよくわかります。

では、この考え方を検証してみましょう。そこで、入力層1層、出力層1層のシンプルなパーセプトロン(前回作成したもの)を使って、「月」データセットを分類してみましょう。以下のスクリプトを実行してください。

from sklearn import datasets
import numpy as np
import matplotlib.pyplot as plt


np.random.seed(0)
feature_set, labels = datasets.make_moons(100, noise=0.10)
plt.figure(figsize=(10,7))
plt.scatter(feature_set[:,0], feature_set[:,1], c=labels, cmap=plt.cm.winter)


labels = labels.reshape(100, 1)


def sigmoid(x):
    return 1/(1+np.exp(-x))


def sigmoid_der(x):
    return sigmoid(x) *(1-sigmoid (x))


np.random.seed(42)
weights = np.random.rand(2, 1) 
lr = 0.5
bias = np.random.rand(1)


for epoch in range(200000):
    inputs = feature_set


# feedforward step 1
    XW = np.dot(feature_set,weights) + bias


# feedforward step 2
    z = sigmoid(XW)


# backpropagation step 1
    error_out = ((1 / 2) * (np.power((z - labels), 2)))
    print(error_out.sum())


error = z - labels


# backpropagation step 2
    dcost_dpred = error
    dpred_dz = sigmoid_der(z)


z_delta = dcost_dpred * dpred_dz


inputs = feature_set.T
    weights -= lr * np.dot(inputs, z_delta)


for num in z_delta:
        bias -= lr * num


何をやっても平均二乗誤差の値が4.17%を超えて収束しないことがわかると思います。これは、このパーセプトロンを使っても、どうやってもデータセットの全ポイントを正しく分類することはできないことを示しています。

1つの隠れ層を持つニューラルネットワーク

このセクションでは、1つの入力層、1つの隠れ層、1つの出力層からなるニューラルネットワークを作成します。ニューラルネットワークのアーキテクチャは以下のようになる。

上の図では、2つの入力、1つの隠れ層、1つの出力層からなるニューラルネットワークが示されています。隠れ層には4つのノードがあります。出力層には1つのノードがあります。これは、2つの出力しかありえない2値分類問題を解くためです。このニューラルネットワークのアーキテクチャは、非線形の境界を見つけることができる。

ニューラルネットワークのノードと隠れ層がいくつあっても、基本的な動作原理は変わりません。まずフィードフォワードの段階で、前の層からの入力に対応する重みが掛けられ、活性化関数に渡され、次の層の対応するノードの最終値が得られます。このプロセスは、出力が計算されるまで、すべての隠れ層で繰り返されます。バックプロパゲーションの段階では、予測された出力が実際の出力と比較され、誤差のコストが計算される。目的はコスト関数を最小化することである。

これは、前回見たように隠れ層が関係しなければ、とても簡単なことです。

しかし、隠れ層が1つ以上ある場合、すべての層の重みが最終出力に寄与するため、誤差を複数の層に伝搬させなければならず、少し複雑な処理になる。

この記事では、1つ以上の隠れ層を持つニューラルネットワークに対して、フィードフォワードとバックプロパゲーションのステップをどのように実行するかを見ていきます。

フィードフォワード

各レコードに対して、2つの特徴量「x1」と「x2」がある。隠れ層の各ノードの値を計算するために、入力と値を計算するノードの対応する重みを掛け合わせる必要がある。そして、その内積を活性化関数に通して、最終的な値を得る。

例えば、隠れ層の最初のノード(”ah1 “と表記)の最終値を計算するには、次のような計算を行う必要があります。

zh1=x1w1+x2w2zh1=x1w1+x2w2
zh1=x1w1+x2w2

ah1=11+e−zh1ah1=11+e−zh1
ah1 = \frac{mathrm{1}} }{mathmrm{1} }{mathmrm{1} + e^{-zh1} }

これが隠れ層の最上位ノードの結果値です。同様に、隠れ層の2番目、3番目、4番目のノードの値を計算することができます。

同様に、出力層の値を計算する場合は、隠れ層のノードの値を入力として扱います。したがって、出力を計算するには、隠れ層のノードの値に対応する重みを掛け合わせ、その結果を活性化関数に渡します。

この演算は、数学的には以下の式で表すことができる。

zo=ah1w9+ah2w10+ah3w11+ah4w12zo=ah1w9+ah2w10+ah3w11+ah4w12
zo = ah1w9 + ah2w10 + ah3w11 + ah4w12

a0=11+e-z0a0=11+e-z0
a0 = \frac{mathrm{1}} }{mathrm{1} + e^{-z0} }

ここで、”a0 “はニューラルネットワークの最終出力です。使用する活性化関数は、前回と同様にシグモイド関数であることを思い出してください。

注:簡単のために、各重みにバイアス項を追加していません。バイアス項がなくても、隠れ層を持つニューラルネットワークの方がパーセプトロンより性能が良いことが分かると思います。

バックプロパゲーション

フィードフォワードのステップは比較的簡単である。しかし、バックプロパゲーションは本シリーズのパート1のように簡単ではありません。

バックプロパゲーションでは、まず損失関数を定義する。ここでは、平均二乗誤差のコスト関数を使用します。これは数学的に次のように表すことができる。

MSE=1n∑ni=1(predicted-observed)2MSE=1n∑i=1n(predicted-observed)2
MSE =
\ΓΓΓΓΓΓΓΓΓΓΓΓΓΓΓΓΓΓΓΓΓΓΓΓΓΓΓΓΓΓΓΓΓΓΓΓ! }{mathrm{n}} しい。
\sum\nolimits_{i=1}^{n}
(予測値-観測値)^{2}。

ここで、nは観測値の数である。

フェーズ1

逆伝播の最初のフェーズでは、出力層の重みを更新する必要があります。とりあえず、このニューラルネットは以下のような部分を持っていると考えてください。

これは前回開発したパーセプトロンに似ています。逆伝播の第一段階の目的は、最終的な誤差が最小になるように重みw9、w10、w11、w12を更新することです。これは最適化問題であり、コスト関数の関数極小値を求める必要があります。

関数の極小値を求めるには、gradient decent アルゴリズムを使用します。グラディエントディセントアルゴリズムは数学的に以下のように表すことができる。

収束するまで繰り返す:{wj:=wj-α∂wjJ(w0,w1)}……(1)repeat until convergence:{wj:=wj-α∂wjJ(w0,w1…wn)}………(1)repeat if convergence: {wj-α_2202↩ w250, sm_2202} …漸近
を収束するまで繰り返す。\begin{Bmatrix} w_j := w_j – \frac{partial }{partial w_j}. J(wguen_0,wguen_1 ……wguen_n) \end{Bmatrix} …………. (1)

勾配関数がどのようにコストを最小化するかについては、すでに前稿で述べた。ここでは、必要な数学的演算を見るだけにしておきます。

我々のコスト関数は

MSE=1n∑ni=1(predicted-observed)2MSE=1n∑i=1n(predicted-observed)2
MSE= \frac{mathrm{1}} }{mathrm{n}} しい。\Ȃ(予測値-観測値)^{2}となる。

今回のニューラルネットワークでは、予測された出力を “ao “で表現しています。ということは、基本的にはこの関数を最小化すればいいことになる。

cost=1n∑ni=1(ao-observed)2cost=1n∑i=1n(ao-observed)2
cost = \frac{mathmathrm{1} }{mathrm{n}} のようになります。(ao – observed)^{2})

前回の記事から、コスト関数を最小化するためには、コストが減少するように重み値を更新する必要があることがわかります。そのためには、各重みに関してコスト関数の導関数を取る必要がある。この段階では、出力層の重みを扱っているので、w9、w10、w11、w2に関してコスト関数を微分する必要があります。

出力層の重みに対するコスト関数の微分は、微分の連鎖法則を用いて次のように数学的に表すことができる。

dcostdwo=dcostdao∗,daodzo∗dzodwo……(1)dcostdwo=dcostdao∗,daodzo∗dzodwo……(1)
\Ъ{dcost}{dwo}=Ъ{dcost}{dao}である。*ЪЪЪЪ * \frac {dzo}{dwo} …… (1)

ここで、”wo “は出力層の重みのことである。また、各項の先頭にある “d “は微分を意味する。

式1の各式に対応する値を求めてみよう。

ここでは

dcostdao=2n*(ao-labels)dcostdao=2n*(ao-labels)である。
\dcost}{dao} = \frac {2}{n} となります。* (ao – labels)

ここで、2nは定数である。これらを無視すると次の式が成り立つ。

dcostdao=(ao-labels)……(5)dcostdao=(ao-labels)……..(5)
\dcost}{dao}=(ao-labels)……(5)である。

次に、”dzo “に対する “dao “を以下のように求めることができる。

daodzo=sigmoid(zo)*(1-sigmoid(zo)) ……(6)daodzo=sigmoid(zo)*(1-sigmoid(zo)) …….(6)
\Ȃ{dao}{dzo} = sigmoid(zo) Ȃ (1-sigmoid(zo)) ……(6)

最後に “dwo “に対する “dzo “を求める。微分は、以下のように隠れ層から来る入力を単純化したものである。

dzodwo=ahdzodwo=ah
\dzo}{dwo} = ah

ここで、”ah “は隠れ層からの4つの入力を意味する。式1は、出力層の重みの更新された重み値を求めるために用いることができる。新しいウェイト値を求めるには、式1が返す値に学習率を掛け、現在のウェイト値から減算すればよい。これは簡単なことで、私たちは以前にもこれを行いました。

フェーズ2

前節では、出力層の重み w9, w10, w11, 12 の更新値を求める方法を見ました。このセクションでは、前のレイヤーに誤差を逆伝播し、隠れ層の重み、すなわち重みw1〜w8の新しい値を求めます。

隠れ層の重みを総称して “wh “と表記することにします。基本的には、コスト関数をwhに関して微分する必要がある。数学的には微分の鎖の法則を使って次のように表すことができる。

dcostdwh=dcostdah∗,dahdzh∗dzhdwh……(2)dcostdwh=dcostdah∗,dahdzh∗dzhdwh……(2)
\⑭{dcost}{dwh}=⑯{dcost}{dah}である。*ЪЪЪЪ * \frac {dzh}{dwh} …… (2)

ここで再び、式2を個々の項に分割する。

最初の項「dcost」は「dah」に対して微分の連鎖法則を用いて次のように微分することができる。

dcostdah=dcostdzo∗,dzodah……(3)dcostdah=dcostdzo∗,dzodah……(3)
\Ȃ{dcost}{dah} = Ȃ{dcost}{dzo}… *, \frac {dzo}{dah} …… (3)

再び式3を各項目に分解してみよう。再び連鎖法則を用いて、「dcost」を「dzo」に対して微分すると、次のようになる。

dcostdzo=dcostdao∗,daodzo……(4)dcostdzo=dcostdao∗,daodzo……(4)
\Ȃ{dcost}{dzo} = Ȃ{dcost}{dao}… *, \frac {dao}{dzo} …… (4)

dcost/daoの値は式5で、dao/dzoの値は式6で既に計算済みです。

今度は、式3からdzo/dahを求める必要がある。zoに注目すると、次のような値になっている。

zo=a01w9+a02w10+a03w11+a04w12zo=a01w9+a02w10+a03w11+a04w12
zo = a01w9 + a02w10 + a03w11 + a04w12

ao “で示される隠れ層からのすべての入力に関して微分すると、”wo “で示される出力層からのすべての重みが残されることになります。したがって

dzodah=wo……(7)dzodah=wo……(7)となる。
\dzo}{dah}=wo・・・・・・・(7)

ここで、式7と式4の値を式3に置き換えることで、dcost/dahの値を求めることができる。

式2に戻ると、まだdah/dzhとdzh/dwhが見つかっていない。

第1項のdah/dzhは、次のように計算できる。

dahdzh=sigmoid(zh)*(1-sigmoid(zh))……(8)dahdzh=sigmoid(zh)*(1-sigmoid(zh))……..(8)
\frac {dah}{dzh} = sigmoid(zh) * (1-sigmoid(zh)) …… (8)

そして最後に、dzh/dwhは単純に入力値である。

dzhdwh=入力特徴量……(9)dzhdwh=入力特徴量……..(9)
\frac {dzh}{dwh} = 入力特徴量 ……(9)

式3、8、9の値を式3で置き換えると、隠れ層重みの更新行列を得ることができる。隠れ層の重み「wh」の新しい重み値を求めるには、式2が返す値に学習率を掛け、現在の重み値から減算すればよい。と、だいたいこんな感じだ。

計算が多いので、疲れるように見えるかもしれない。しかし、よく見ると、導出と乗算の2つの演算が連鎖的に行われているだけなのです。

ニューラルネットワークが他の機械学習アルゴリズムより遅い理由の1つは、バックエンドで多くの計算が行われていることです。私たちのニューラルネットワークは、4つのノード、2つの入力と1つの出力を持つ1つの隠れ層だけでしたが、1回の反復で重みを更新するために、長い導関数と乗算を実行する必要がありました。現実の世界では、ニューラルネットワークは何百もの層を持ち、何百もの入力と出力の値を持つことがあります。そのため、ニューラルネットワークの実行速度は遅い。

1つの隠れ層を持つニューラルネットワークのコード

では、先ほど説明したニューラルネットワークをPythonで一から実装してみましょう。コードスニペットと前節で説明した理論との対応がよくわかると思います。今回もデータセット編で作成した非線形データの分類に挑戦します。次のスクリプトを見てください。

# -*- coding: utf-8 -*-
"""
Created on Tue Sep 25 13:46:08 2018


@author: usman
"""


from sklearn import datasets
import numpy as np
import matplotlib.pyplot as plt


np.random.seed(0)
feature_set, labels = datasets.make_moons(100, noise=0.10)
plt.figure(figsize=(10,7))
plt.scatter(feature_set[:,0], feature_set[:,1], c=labels, cmap=plt.cm.winter)


labels = labels.reshape(100, 1)


def sigmoid(x):
    return 1/(1+np.exp(-x))


def sigmoid_der(x):
    return sigmoid(x) *(1-sigmoid (x))


wh = np.random.rand(len(feature_set[0]),4) 
wo = np.random.rand(4, 1)
lr = 0.5


for epoch in range(200000):
    # feedforward
    zh = np.dot(feature_set, wh)
    ah = sigmoid(zh)


zo = np.dot(ah, wo)
    ao = sigmoid(zo)


# Phase1 =======================


error_out = ((1 / 2) * (np.power((ao - labels), 2)))
    print(error_out.sum())


dcost_dao = ao - labels
    dao_dzo = sigmoid_der(zo) 
    dzo_dwo = ah


dcost_wo = np.dot(dzo_dwo.T, dcost_dao * dao_dzo)


# Phase 2 =======================


# dcost_w1 = dcost_dah * dah_dzh * dzh_dw1
    # dcost_dah = dcost_dzo * dzo_dah
    dcost_dzo = dcost_dao * dao_dzo
    dzo_dah = wo
    dcost_dah = np.dot(dcost_dzo , dzo_dah.T)
    dah_dzh = sigmoid_der(zh) 
    dzh_dwh = feature_set
    dcost_wh = np.dot(dzh_dwh.T, dah_dzh * dcost_dah)


# Update Weights ================


wh -= lr * dcost_wh
    wo -= lr * dcost_wo


上のスクリプトでは、まず必要なライブラリをインポートし、データセットを作成します。次に、シグモイド関数とその導関数を定義します。そして、隠れ層と出力層の重みをランダムな値で初期化します。学習率は0.5である。異なる学習率を試してみたところ、0.5が良い値であることがわかった。

次に、このアルゴリズムを2000エポック実行する。各エポック内部では、まずフィードフォワード演算を行う。フィードフォワード演算のコードスニペットは以下の通りである。

zh = np.dot(feature_set, wh)
ah = sigmoid(zh)


zo = np.dot(ah, wo)
ao = sigmoid(zo)


理論編で述べたように、逆伝播は2つのフェーズから構成される。最初のフェーズでは、出力層の重みの勾配が計算される。以下のスクリプトはバックプロパゲーションの最初のフェーズで実行される。

error_out = ((1 / 2) * (np.power((ao - labels), 2)))
print(error_out.sum())


dcost_dao = ao - labels
dao_dzo = sigmoid_der(zo) 
dzo_dwo = ah


dcost_wo = np.dot(dzo_dwo.T, dcost_dao * dao_dzo)


第二のフェーズでは、隠れ層の重みの勾配を計算する。バックプロパゲーションの第二段階では、以下のスクリプトが実行される。

dcost_dzo = dcost_dao * dao_dzo
dzo_dah = wo
dcost_dah = np.dot(dcost_dzo , dzo_dah.T)
dah_dzh = sigmoid_der(zh) 
dzh_dwh = feature_set
dcost_wh = np.dot( dzh_dwh.T, dah_dzh * dcost_dah)


最後に、以下のスクリプトで重みが更新される。

wh -= lr * dcost_wh
wo -= lr * dcost_wo


上記のスクリプトを実行すると、平均二乗誤差の最小値が1.50となり、パーセプトロンで得られた平均二乗誤差4.17より小さくなっていることがわかります。これは、非線形分離可能なデータの場合、隠れ層を持つニューラルネットワークがより良い性能を発揮することを示しています。

結論

この記事では、Pythonで隠れ層が1層のニューラルネットワークをゼロから作成する方法を見ました。作成したニューラルネットワークは、非線形データの2値分類において、隠れ層のないニューラルネットワークよりも優れていることを確認しました。

しかし、データを2つ以上のカテゴリーに分類する必要がある場合もあります。次回は、多クラス分類の問題に対して、Pythonでゼロからニューラルネットワークを作成する方法について見ていきます。

タイトルとURLをコピーしました