今回は、「Pythonでゼロから作るニューラルネットワーク」の第3回目です。
- Pythonでゼロからニューラルネットワークをつくる
- Pythonでゼロからニューラルネットワークを作る。隠れ層の追加
- Pythonでゼロからニューラルネットワークを作る。マルチクラス分類
ニューラルネットワークの経験がない方は、まずシリーズのパート1、パート2(上記リンク)をお読みいただくとよいと思います。それらの記事で説明されている概念に慣れたら、この記事に戻ってきて続きを読むことができます。
前回は、2値分類問題を解くニューラルネットワークをPythonで一から作る方法を紹介しました。2値分類問題には2つの出力しかありません。しかし、現実の問題はもっと複雑です。
例えば、数字の画像を入力とし、分類器が対応する数字の数字を予測する、数字認識問題を考えてみましょう。桁は0から9の間の任意の数である。これは多クラス分類問題の典型的な例で、入力は10の可能な出力のいずれかに属する可能性があります。
この記事では、多クラス分類問題を解くことができる簡単なニューラルネットワークをPythonでゼロから作成する方法を紹介します。
データセット
まず、データセットについて簡単に説明します。データセットは2つの入力特徴と3つの出力候補のうちの1つを持っています。この記事では、データセットを手動で作成します。
そのために、以下のスクリプトを実行します。
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(42)
cat_images = np.random.randn(700, 2) + np.array([0, -3])
mouse_images = np.random.randn(700, 2) + np.array([3, 3])
dog_images = np.random.randn(700, 2) + np.array([-3, 3])
上のスクリプトでは、まずライブラリをインポートし、次にサイズ700 x 2の2次元配列を3つ作成する。配列の1セットの各要素は、特定の動物の画像と考えることができる。配列の各要素は、3つの出力クラスのうちの1つに対応します。
ここで重要なことは、 cat_images
配列の要素を2次元平面上にプロットすると、 x=0 と y=-3 を中心とした位置になることです。同様に、配列 mouse_images
の要素は x=3 と y=3 を中心とし、最後に配列 dog_images
の要素は x=-3 と y=3 を中心とします。これは、データセットをプロットしてみるとわかるでしょう。
次に、これらの配列を垂直方向に結合して、最終的なデータセットを作成する必要があります。以下のスクリプトを実行してください。
feature_set = np.vstack([cat_images, mouse_images, dog_images])
特徴セットを作成したので、次に特徴セット内の各レコードに対応するラベルを定義する必要がある。次のスクリプトを実行する。
labels = np.array([0]*700 + [1]*700 + [2]*700)
上のスクリプトは2100の要素からなる一次元の配列を作成する。最初の700個の要素には0、次の700個の要素には1、最後の700個の要素には2というラベルが付けられています。これは、対応するデータのラベルを素早く作成するためのショートカット方法です。
多クラス分類問題では、出力層は3つのノードを持ち、各ノードが1つの出力クラスに対応するため、出力ラベルを1ホットの符号化ベクトルとして定義する必要があります。ある出力が予測されたとき、対応するノードの値が1になり、残りのノードの値が0になるようにしたい。そのためには、各レコードの出力ラベルとして3つの値が必要である。そのため、出力ベクトルをワンホットエンコードされたベクトルに変換する。
以下のスクリプトを実行し、データセットの1ホットエンコードベクトル配列を作成する。
one_hot_labels = np.zeros((2100, 3))
for i in range(2100):
one_hot_labels[i, labels[i]] = 1
上記のスクリプトでは、サイズ2100 x 3 の配列 one_hot_labels
を作成し、各行には特徴量セット内の対応するレコードの1ホットエンコードベクトルを格納する。そして、対応する列に 1 を挿入する。
上記のスクリプトを実行すると、one_hot_labels
配列は、最初の700レコードのインデックス0に1、次の700レコードのインデックス1に1、最後の700レコードのインデックス2に1を持つことがわかる。
それでは、先ほど作成したデータセットをプロットしてみましょう。次のスクリプトを実行しなさい。
plt.scatter(feature_set[:,0], feature_set[:,1], c=labels, cmap='plasma', s=100, alpha=0.5)
plt.show()
上のスクリプトを実行すると、次のような図が表示される。
3つの異なるクラスに属する要素があることがよくわかる。これらのクラスに分類できるニューラルネットワークを開発することが課題である。
複数の出力クラスを持つニューラルネットワーク
これから設計するニューラルネットワークは以下のようなアーキテクチャである。
このニューラルネットワークは、シリーズ第2回で開発したものと非常によく似ていることがわかります。2つの入力特徴を持つ入力層と、4つのノードを持つ隠れ層があります。しかし、出力層には3つのノードがあることがわかります。これは、このニューラルネットワークが、可能な出力の数が3である多クラス分類問題を解くことができることを意味します。
ソフトマックスとクロスエントロピー関数
コードセクションに移る前に、多クラス分類のためのニューラルネットワークを作る際に最もよく使われる活性化関数と損失関数であるソフトマックス関数とクロスエントロピー関数をそれぞれ簡単におさらいしておきましょう。
ソフトマックス機能
ニューラルネットワークの構造から、出力層には3つのノードがあることがわかります。出力層の活性化関数にはいくつかのオプションがあります。一つは、以前の記事で行ったようにシグモイド関数を使用することです。
しかし、入力としてベクトルを受け取り、出力として同じ長さの別のベクトルを生成するソフトマックスという形で、より便利な活性化関数があります。出力には3つのノードが含まれるので、各ノードからの出力を入力ベクトルの1要素と考えることができます。出力は同じベクトルの長さで、すべての要素の値の合計が1になります。数学的には、ソフトマックス関数は次のように表すことができる。
yi(zi)=ezi∑kk=1ezkyi(zi)=ezi∑k=1kezk
y_i(zi) = \frac{e^{z}}{ \sum}^{k=1}^{k}{e^{z}_k}} }
ソフトマックス関数は、各入力要素の指数を全入力要素の指数の和で割るだけの関数です。この簡単な例を見てみよう。
def softmax(A):
expA = np.exp(A)
return expA / expA.sum()
nums = np.array([4, 5, 6])
print(softmax(nums))
上のスクリプトでは、1つのベクトルを入力とし、そのベクトル内のすべての要素の指数を取り、得られた個々の数値を入力ベクトル内のすべての数値の指数の合計で割るソフトマックス関数を作成しています。
入力ベクトルには、4、5、6の要素が含まれていることがわかります。出力では、0と1の間で3つの数が潰され、その和が1になることがわかります。出力は次のようになります。
[0.09003057 0.24472847 0.66524096]
ソフトマックス活性化関数は、他の活性化関数と比較して、特に多クラス分類問題で2つの大きな利点があります。第一の利点は、ソフトマックス関数はベクトルを入力とし、第二の利点は、0と1の間の出力を生成することです。我々のデータセットでは、出力ラベルが1ホットエンコーディングされており、出力は0と1の間の値を持つことを意味します。 しかし、フィードフォワード処理の出力は1より大きくなり得るので、出力を0と1の間に圧縮するソフトマックス関数が出力層で理想的な選択になります。
クロスエントロピー関数
出力層のソフトマックス活性化関数では、前回と同様に平均二乗誤差コスト関数を用いてコストを最適化することができる。しかし、ソフトマックス関数の場合、より便利なコスト関数としてクロスエントロピー関数があります。
数学的にはクロスエントロピーの関数は次のようなものです。
H(y,^y)=-∑iyilog^yiH(y,y^)=-∑iyilogyi^ となる。
H(y,\hat{y}) = -hinthesum_i y_i \log \hat{y_i}
クロスエントロピーは単純に全ての実確率と予測確率の負対数の積の和である。多クラス分類問題では、クロスエントロピー関数は勾配分散関数より優れていることが知られています。
これで、多クラス分類問題を解くニューラルネットワークを作るのに十分な知識が得られました。それでは、作成したニューラルネットワークがどのように機能するか見てみましょう。
いつものように、ニューラルネットワークは2つのステップで実行されます。フィードフォワードとバックプロパゲーションである。
フィードフォワード
フィードフォワードの段階は、前回の記事で見たものとほぼ同じです。唯一の違いは、出力層でシグモイド関数ではなくソフトマックス活性化関数を使用することです。
隠れ層の出力には、前回と同じようにシグモイド関数を使うことを覚えておいてください。ソフトマックス関数は出力層の活性化のみに使用します。
フェーズ1
隠れ層と出力層に2つの異なる活性化関数を使用するので、フィードフォワードのフェーズを2つのサブフェーズに分けました。
最初のフェーズでは、隠れ層からの出力をどのように計算するかを見る。各入力レコードに対して、2つの特徴量「x1」と「x2」を用意する。隠れ層の各ノードの出力値を計算するためには、入力に、値を計算する隠れ層ノードの対応する重みを掛けなければならない。ここでバイアス項を追加していることに注意してください。そして、その内積をシグモイド活性化関数に通して最終的な値を得ます。
例えば、隠れ層の最初のノード “ah1 “の最終値を計算するには、次のような計算が必要です。
zh1=x1w1+x2w2+bzh1=x1w1+x2w2+b
zh1=x1w1+x2w2+bとなります。
ah1=11+e−zh1ah1=11+e−zh1
ah1 = \frac{mathrm{1}} }{mathmrm{1} + e^{-zh1} }
これが隠れ層の最上位ノードの結果値です。同様に、隠れ層の2番目、3番目、4番目のノードの値を計算することができます。
フェーズ2
出力層の値を計算するために、隠れ層のノードの値は入力として扱われます。したがって、出力を計算するためには、隠れ層のノードの値に対応する重みを掛け、その結果を活性化関数(この場合はソフトマックス)に通す。
この操作は、数学的には以下の式で表すことができる。
zo1=ah1w9+ah2w10+ah3w11+ah4w12zo1=ah1w9+ah2w10+ah3w11+ah4w12
zo1=ah1w9+ah2w10+ah3w11+ah4w12になります。
zo2=ah1w13+ah2w14+ah3w15+ah4w16zo2=ah1w13+ah2w14+ah3w15+ah4w16
zo2=ah1w13+ah2w14+ah3w15+ah4w16になります。
zo3=ah1w17+ah2w18+ah3w19+ah4w20zo3=ah1w17+ah2w18+ah3w19+ah4w20
zo3=ah1w17+ah2w18+ah3w19+ah4w20とする。
ここで、zo1、zo2、zo3はシグモイド関数の入力として使うベクトルを形成します。このベクトルを “zo “と名付けることにする。
zo = [zo1, zo2, zo3]
さて、出力値a01を求めるには、次のようにソフトマックス関数を使えばよい。
ao1(zo)=ezo1∑kk=1ezokao1(zo)=ezo1∑k=1kezok
ao1(zo) = \frac{e^{zo1}}{ \sumnolimits}{k=1}^{k}{e^{zok}}} }
ここで、”a01 “は出力層の最上位ノードに対する出力です。同様に、ソフトマックス関数を用いてao2、ao3の値を計算することができます。
多クラス出力のニューラルネットワークのフィードフォワードステップは、2値分類問題のニューラルネットワークのフィードフォワードステップとかなり似ていることがわかります。唯一の違いは、ここでは出力層にシグモイド関数ではなく、ソフトマックス関数を使用していることです。
バックプロパゲーション
バックプロパゲーションの基本的な考え方は同じである。コスト関数を定義し、そのコストが最小になるように重みを更新することで、コスト関数を最適化する。しかし、前回の記事ではコスト関数として平均二乗誤差を使用しましたが、今回はその代わりにクロスエントロピー関数を使用します。
バックプロパゲーションは最適化問題であり、コスト関数に対する関数の極小値を見つけなければなりません。
関数の極小値を求めるには、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)
勾配関数がどのようにコストを最小化するかについては、既に述べたとおりである。ここでは、必要な数学的演算を見るだけにしておきます。
コスト関数は
H(y,^y)=-∑iyilog^yiH(y,y^)=-∑iyilogyi^ となります。
H(y,\hat{y}) = -SUM_i y_i \log \hat{y_i}
このニューラルネットワークでは、出力ベクトルの各要素が出力層の1つのノードからの出力に対応する。出力ベクトルはソフトマックス関数を用いて計算される。ao “を全出力ノードからの予測出力のベクトル、”y “を出力ベクトル中の対応するノードの実際の出力のベクトルとすると、基本的にはこの関数を最小化しなければならない。
cost(y,ao)=-∑iyilogaoicost(y,ao)=-∑iyilogaoi
cost(y, {ao}) = -᷅sum_i y y_i \log {ao_i}.
フェーズ1
最初のフェーズでは、重み w9 から w20 までを更新する必要がある。これは出力層ノードの重みである。
前回の記事から、コスト関数を最小化するためには、コストが減少するように重みの値を更新する必要があることがわかる。そのためには、各重みに対するコスト関数の導関数を取る必要がある。数学的には次のように表すことができる。
dcostdwo=dcostdao∗,daodzo∗dzodwo……(1)dcostdwo=dcostdao∗,daodzo∗dzodwo……(1)
\Ȃ {dcost}{dwo}={dcost}{dao}である。*ЪЪЪЪ * \frac {dzo}{dwo} …. (1)
ここで、”wo “は出力層における重みを表す。
式の前半は次のように表すことができる。
dcostdao∗ daodzo……(2)dcostdao∗ daodzo……(2)
\Ȃ{dcost}{dao}のようになります。* \frac {dao}{dzo} …… (2)
ソフトマックス活性化関数を用いたクロスエントロピー損失関数の詳細な導出は、こちらのリンクにあります。
式(2)の微分値は
dcostdao∗ daodzo=ao-y……(3)dcostdao∗ daodzo=ao-y……..(3)
\Ȃ {dcost}{dao}… *\ Ȃ {dao}{dzo} = ao – y …… (3)ここで、”ao “は予測値です。
ここで、”ao “は予測出力、”y “は実際の出力である。
最後に、式1から “dwo “に対する “dzo “を求める必要がある。この導関数は、以下のように隠れ層からの出力を単純化したものである。
dzodwo=ahdzodwo=ah
\dzodwo=ahdzodwo=ah
新しい重み値を求めるには、式1が返す値に学習率を掛け、現在の重み値から減算すればよい。
また、出力層のバイアス “bo “を更新する必要がある。以下のように、コスト関数をバイアスに対して微分し、新しいバイアス値を得る必要がある。
dcostdbo=dcostdao∗ daodzo∗dzodbo……(4)dcostdbo=dcostdao∗ daodzo∗dzodbo……(4)
\dcostdbo=dcostdao∗ ・・・(4)dcostdbo=dcostdao∗ ・・・(4). *\ ȂȂ {dao}{dzo} Ȃ * \frac {dzo}{dbo} ……。(4)
式4の前半は、式3ですでに計算済みです。ここでは、単純に1である「bo」に対して「dzo」を更新すればよいわけです。
dcostdbo=ao-y……(5)dcostdbo=ao-y………….(5)
\ⅷ{dcost}{dbo} = ao – y ……(5)
出力層の新しいバイアス値を求めるには、式5で返される値に学習率を乗じ、現在のバイアス値から減算すればよい。
フェーズ2
ここでは、誤差を前の層に逆伝播し、隠れ層の重み、すなわち重みw1〜w8の新しい値を求める。
隠れ層の重みを総称して「wh」と呼ぶことにする。基本的には、コスト関数をwhに関して微分する必要があります。
数学的には微分の連鎖法則を使って次のように表すことができる。
dcostdwh=dcostdah∗,dahdzh∗dzhdwh……(6)dcostdwh=dcostdah∗,dahdzh∗dzhdwh……(6)
\⑭{dcost}{dwh}=⑯{dcost}{dah}である。*ЪЪЪЪ * ⑭frac {dzh}{dwh} …… (6)
ここで再び、式6を個々の項に分割する。
最初の項「dcost」は、「dah」に対して微分の連鎖法則を用いて次のように微分できる。
dcostdah=dcostdzo∗ dzodah……(7)dcostdah=dcostdzo∗ dzodah……(7)
\Ȃ{dcost}{dah} = Ȃ{dcost}{dzo} Ȃ{dcost}{dzo} Ȃ * \frac {dzo}{dah} …… (7)
再び、式7を各項目に分解してみよう。式3から、次のことがわかる。
dcostdao∗ daodzo=dcostdzo==ao-y……(8)dcostdao∗ daodzo=dcostdzo==ao-y……….(8)
\Ȃ {dcost}{dao}Ȃ * \frac {dao}{dzo} == ao – y …… (8)
ここで、式7からdzo/dahを求める必要があるが、これは以下のように出力層の重みに等しい。
dzodah=wo ……(9)dzodah=wo ……(9)
\dzo}{dah} = wo ……(9) ⑭frac {dzo}{dah} = wo ……(9)。
ここで、式8と式9の値を式7に置き換えて、dcost/dahの値を求めることができる。
式6に戻ると、まだdah/dzhとdzh/dwhが見つかっていない。
第1項のdah/dzhは、次のように計算できる。
dahdzh=sigmoid(zh)*(1-sigmoid(zh))……(10)dahdzh=sigmoid(zh)*(1-sigmoid(zh))………(10)
\Ȃ{dah}{dzh} = sigmoid(zh) * (1-sigmoid(zh)) ……(10)。
そして最後に、dzh/dwhは単純に入力値である。
dzhdwh=入力特徴量……(11)dzhdwh=入力特徴量……..(11)
\frac {dzh}{dwh} = 入力特徴量 ……(11)
式6に式7、10、11の値を入れ替えると、隠れ層重みの更新行列が得られる。隠れ層の重み「wh」の新しい重み値を求めるには、式6が返す値に学習率を掛け、現在の隠れ層の重み値から減算すればよい。
同様に、隠れ層バイアス “bh “に関するコスト関数の微分は、単純に次のように計算できる。
dcostdbh=dcostdah∗,dahdzh∗dzhdbh……(12)dcostdbh=dcostdah∗,dahdzh∗dzhdbh……(12)
\⑭{dcost}{dbh}={dcost}{dah}となる。*ЪЪЪЪ * ⅳ{dzh}{dbh} …… (12)
となり、これは単純に等しい。
dcostdbh=dcostdah∗,dahdzh ……(13)dcostdbh=dcostdah∗,dahdzh ……(13)となる。
\⑯{dcost}{dbh}=⑯{dcost}{dah}である。*, \frac {dah}{dzh} …… (13)
となるからである。
dzhdbh=1dzhdbh=1
\frac {dzh}{dbh} = 1となる。
隠れ層の新しいバイアス値を求めるには、式13で返される値に学習率を掛けて、現在の隠れ層のバイアス値から減算すればよく、これでバックプロパゲーションは終了である。
フィードフォワードとバックプロパゲーションの処理は、前回の記事で見たものとよく似ていることがおわかりいただけると思います。変更したのは活性化関数とコスト関数だけです。
多クラス分類のためのニューラルネットワークのコード
多クラス分類のためのニューラルネットワークの理論を説明しましたが、次はその理論を実践する番です。
次のスクリプトを見てください。
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(42)
cat_images = np.random.randn(700, 2) + np.array([0, -3])
mouse_images = np.random.randn(700, 2) + np.array([3, 3])
dog_images = np.random.randn(700, 2) + np.array([-3, 3])
feature_set = np.vstack([cat_images, mouse_images, dog_images])
labels = np.array([0]*700 + [1]*700 + [2]*700)
one_hot_labels = np.zeros((2100, 3))
for i in range(2100):
one_hot_labels[i, labels[i]] = 1
plt.figure(figsize=(10,7))
plt.scatter(feature_set[:,0], feature_set[:,1], c=labels, cmap='plasma', s=100, alpha=0.5)
plt.show()
def sigmoid(x):
return 1/(1+np.exp(-x))
def sigmoid_der(x):
return sigmoid(x) *(1-sigmoid (x))
def softmax(A):
expA = np.exp(A)
return expA / expA.sum(axis=1, keepdims=True)
instances = feature_set.shape[0]
attributes = feature_set.shape[1]
hidden_nodes = 4
output_labels = 3
wh = np.random.rand(attributes,hidden_nodes)
bh = np.random.randn(hidden_nodes)
wo = np.random.rand(hidden_nodes,output_labels)
bo = np.random.randn(output_labels)
lr = 10e-4
error_cost = []
for epoch in range(50000):
############# feedforward
# Phase 1
zh = np.dot(feature_set, wh) + bh
ah = sigmoid(zh)
# Phase 2
zo = np.dot(ah, wo) + bo
ao = softmax(zo)
########## Back Propagation
########## Phase 1
dcost_dzo = ao - one_hot_labels
dzo_dwo = ah
dcost_wo = np.dot(dzo_dwo.T, dcost_dzo)
dcost_bo = dcost_dzo
########## Phases 2
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)
dcost_bh = dcost_dah * dah_dzh
# Update Weights ================
wh -= lr * dcost_wh
bh -= lr * dcost_bh.sum(axis=0)
wo -= lr * dcost_wo
bo -= lr * dcost_bo.sum(axis=0)
if epoch % 200 == 0:
loss = np.sum(-one_hot_labels * np.log(ao))
print('Loss function value: ', loss)
error_cost.append(loss)
このコードは前回の記事で作成したものとかなり似ています。フィードフォワードの部分では、最終的な出力である「ao」が「softmax」関数を使って計算されているのが唯一の違いである。
同様に、バックプロパゲーションの部分では、出力層の新しい重みを求めるために、コスト関数が sigmoid
関数ではなく softmax
関数を用いて導出されています。
上のスクリプトを実行すると、最終的なエラーコストは0.5となることがわかります。次の図は、エポック数に応じてコストが減少する様子を示している。
このように、最終的なエラーコストに到達するのに必要なエポック数はそれほど多くはないことがわかります。
同様に、出力層にシグモイド関数を用いて同じスクリプトを実行すると、50000エポック後の最小誤差コストは約1.5となり、softmaxで達成した0.5より大きくなります。
結論
実際のニューラルネットワークは、多クラス分類の問題を解くことができます。この記事では、多クラス分類のための非常にシンプルなニューラルネットワークを、Pythonでゼロから作成する方法を見ました。今回で連載は最終回です。今回は、「Pythonでゼロから作るニューラルネットワーク」の最終回です。今後は、リカレントニューラルネットワークや畳み込みニューラルネットワークなど、より専門的なニューラルネットワークをPythonでゼロから作成する方法を解説していく予定です。