Pythonでゼロから作るニューラルネットワーク

本記事は、「Pythonでゼロから作るニューラルネットワーク」の連載の第1回目です。

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

Siri、Alexa、Cortonaのようなチャットボットがどのようにしてユーザーの問い合わせに応答しているのか不思議に思ったことはありませんか?あるいは、自律走行車が人間の手を借りずに自動運転できるのはなぜだろう?これらの製品に共通しているのは、「人工知能(AI)」です。AIがあるからこそ、人間の監視や制御を受けずに、こうしたタスクを実行できるのです。しかし、疑問は残ります。”AIとは何なのか?” この問いに対するシンプルな答えは、”AIとは、代数学、微積分学、確率・統計学など、さまざまな数学領域の複雑なアルゴリズムの組み合わせである “です。

この記事では、人工知能の主要な構成要素の1つである単純な人工ニューラルネットワークを学びます。人工ニューラルネットワークには、特定の問題を解決するために様々な種類が存在します。例えば、畳み込みニューラルネットワークは画像認識の問題によく使われ、リカレントニューラルネットワークはシーケンス問題の解決に使われます。

一行のコードでニューラルネットワークを作ることができる深層学習ライブラリはたくさんあります。しかし、ニューラルネットワークの詳細な動作を本当に理解したいのであれば、任意のプログラミング言語でゼロからコーディングする方法を学ぶことをお勧めします。この演習を行うことで、多くのコンセプトが理解できるようになります。そして、これこそが、この記事で行うことなのです。

問題点

今回は入門編ということで、解く問題はとても簡単です。5人の肥満、喫煙習慣、運動習慣に関する情報があるとする。また、これらの人々が糖尿病であるかどうかも知っている。データセットは次のようなものである。

| 人|喫煙|肥満|運動|糖尿病患者
| — | — | — | — | — |
| 人1|0|1|0|1|||。
| 人物2|0|0|1|0|0
| 人3|1|0|0|0|0|0|0|1
| 人物4|1|1|0|1||||||||||||||||||||||||||||||||||||||||||||||◆人物
| 人物5|1|1|1|1|1|1|1|1

上の表では、5つのカラムがあります。Person、Smoking、Obesity、Exercise、Diabeticです。ここで、1は真、0は偽を意味します。例えば、最初の人物は0, 1, 0の値を持ち、これはその人物がタバコを吸わず、肥満で、運動をしないことを意味します。また、この人は糖尿病である。

このデータセットから、その人の肥満が糖尿病であることを示していることは明らかです。私たちの課題は、運動習慣、肥満、喫煙習慣に関するデータがあれば、未知の人物が糖尿病かどうかを予測できるニューラルネットワークを作ることです。これは教師あり学習問題の一種で、入力とそれに対応する正しい出力が与えられ、入力と出力の対応関係を見つけることが課題です。

注:これはあくまで架空のデータセットであり、現実には肥満の人が必ずしも糖尿病であるとは限りません。

解決策

入力層と出力層を1つずつ持つ、非常にシンプルなニューラルネットワークを作成します。実際のコードを書く前に、まずニューラルネットワークが理論上どのように実行されるかを見てみましょう。

ニューラルネットワークの理論

ニューラルネットワークは教師あり学習アルゴリズムであり、独立変数を含む入力データと従属変数を含む出力データを与えることを意味する。例えば、この例では、独立変数が喫煙、肥満、運動である。従属変数は、糖尿病であるかどうかです。

まず、ニューラルネットワークはランダムに予測を行い、その予測と正しい出力を照合し、予測値と実際の値との差(誤差)を計算します。実際の値と伝搬された値の差を求める関数をコスト関数と呼ぶ。ここでいうコストとは、誤差のことである。我々の目的はコスト関数を最小化することである。ニューラルネットワークの学習とは、基本的にコスト関数を最小化することを指します。この作業をどのように行うか見ていきましょう。

これから作成するニューラルネットワークは、以下のような視覚的な表現をしています。

ニューラルネットワークは2つのステップで実行されます。フィードフォワードとバックプロパゲーションである。この2つのステップの詳細について説明します。

フィードフォワード

ニューラルネットワークのフィードフォワード部分では、入力ノードと重みの値に基づいて予測が行われます。上図のニューラルネットワークを見ると、データセットには喫煙、肥満、運動の3つの特徴があるので、第1層(入力層)には3つのノードがあることがわかる。上の図では一般性のために、特徴量の名前を変数 x に置き換えています。

ニューラルネットワークの重みは、基本的に出力を正しく予測するために調整しなければならない文字列です。とりあえず、各入力特徴に対して1つの重みがあることだけは覚えておいてください。

以下は、ニューラルネットワークのフィードフォワード段階で実行されるステップである。

ステップ 1: (入力と重みの内積を計算する)

入力層のノードは、3つの重みパラメータを介して出力層と接続される。出力層では、入力ノードの値に対応する重みが掛けられ、足し合わされる。最後に、バイアス項が加算される。上図中の「b」はバイアス項を指す。

ここで、バイアス項は非常に重要である。例えば、タバコを吸わず、肥満でもなく、運動もしない人がいたとすると、入力ノードと重みの積の和は0になる。この場合、いくらアルゴリズムを学習させても、出力はゼロになってしまいます。そこで、その人に関するゼロでない情報を持っていなくても予測を行えるようにするために、バイアス項が必要になる。バイアス項は、ロバストなニューラルネットワークを作るために必要である。

数学的には、ステップ1において、以下の計算を行う。

X.W=x1w1+x2w2+x3w3+bX.W=x1w1+x2w2+x3w3+b
X.W=x1w1+x2w2+x3w3+bとする。

ステップ 2: (ステップ 1 の結果を活性化関数に通す)

ステップ1の結果は、任意の値の集合とすることができる。しかし、出力では1と0という形の値が入っている。そのような活性化関数として、シグモイド関数がある。

シグモイド関数は、入力が0の場合は0.5を返し、入力が大きな正の数の場合は1に近い値を返します。負の入力の場合、シグモイド関数は0に近い値を出力する。

数学的にシグモイド関数は次のように表すことができる。

θX.W=11+e−X.WθX.W=11+e−X.W
\theta_{X.W} = \frac{Mathrm{1} }{mathrm{1} + e^{-X.W} }

ここで、シグモイド関数をプロットしてみます。

input = np.linspace(-10, 10, 100)


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


from matplotlib import pyplot as plt
plt.plot(input, sigmoid(input), c="r")


上のスクリプトでは、まず、-10と10の間の100個の直線的な点をランダムに生成します。これを行うには、NumPyライブラリの linspace メソッドを使用します。次に、sigmoid 関数を定義します。最後に、 matplotlib ライブラリを使用して、 sigmoid 関数が返す値に対して入力値をプロットします。出力はこのようになります。

入力が負の数であれば、出力は0に近く、入力が正であれば、出力は1に近いことがわかります。

これがニューラルネットワークのフィードフォワード部分のまとめです。これは非常に簡単です。まず、入力特徴行列と重み行列の内積を求めなければなりません。次に、出力の結果を活性化関数に通しますが、この場合はシグモイド関数です。活性化関数の結果は、基本的には入力特徴量に対する予測出力である。

バックプロパゲーション

学習する前の最初のうちは、ニューラルネットワークは正解とは程遠いランダムな予測をします。

ニューラルネットワークの動作原理は簡単です。まず、ネットワークに出力に関するランダムな予測をさせる。そして、ニューラルネットワークの予測した出力と実際の出力を比較します。次に、予測された出力が実際の出力に近づくように、重みとバイアスを微調整します。これが基本的に「ニューラルネットワークの学習」と呼ばれるものです。

逆伝播のセクションでは、アルゴリズムを学習します。それでは、逆伝播セクションのステップを見てみましょう。

ステップ 1: (コストの計算)

逆伝播セクションの最初のステップは、予測値の「コスト」を求めることである。予測のコストは、予測された出力と実際の出力の差を求めることで簡単に計算することができます。差が大きければ大きいほど、コストは高くなります。

コストを求める方法は他にもいくつかありますが、ここでは平均二乗誤差のコスト関数を使用します。コスト関数とは、簡単に言うと与えられた予測値のコストを求める関数です。

平均二乗誤差コスト関数は、数学的に次のように表すことができる。

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

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

ステップ2:(コストの最小化)

最終的な目的は、コストが最小になるようにニューラルネットワークのツマミを細かく調整することです。ニューラルネットワークを見ると、制御できるのは重みとバイアスだけであることに気づくだろう。それ以外は制御できない。入力は制御できないし、ドット積も制御できないし、シグモイド関数も操作できない。

コストを最小化するためには、コスト関数が最小の値を返す重みとバイアスの値を見つける必要があります。コストが小さければ小さいほど、予測はより正確になります。

これは最適化問題で、関数の最小値を見つけなければならない。

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

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

ここで、上式中の J はコスト関数である。基本的に上の式が言っていることは、各重みとバイアスに関するコスト関数の偏導関数を求め、その結果を既存の重み値から引いて、新しい重み値を得るということである。

関数の導関数は、任意の点におけるその傾きを与えます。重みの値が与えられたときにコストが増加するか減少するかを調べるには、その特定の重みの値における関数の導関数を求めればよい。重みの増加に伴いコストが増加する場合、導関数は正の値を返し、既存の値から差し引かれます。

一方、重量の増加とともにコストが減少する場合は、負の値が返され、負の中に正があるため、既存の重量値に加えられることになる。

式1では、α記号があり、これに勾配を掛けていることがわかる。これは学習率と呼ばれる。学習率はアルゴリズムがどの程度の速さで学習するのかを定義する。学習率がどのように定義されるかの詳細については、こちらの記事をご覧ください。

コストが望ましいレベルまで最小化されるまで、すべての重みとバイアスについて式1の実行を繰り返す必要があります。言い換えれば、コスト関数がゼロに近い値を返すようなバイアスと重みの値が得られるまで、式1を実行し続ける必要があるのです。

と、だいたいこんなところだろうか。さて、いよいよこれまで勉強してきたことを実践してみましょう。Pythonで入力層と出力層を1つずつ持つ簡単なニューラルネットワークを作成します。

Pythonによるニューラルネットの実装

まず、特徴量セットとそれに対応するラベルを作成しましょう。以下のスクリプトを実行する。

import numpy as np
feature_set = np.array([[0,1,0],[0,0,1],[1,0,0],[1,1,0],[1,1,1]])
labels = np.array([[1,0,0,1,1]])
labels = labels.reshape(5,1)


上のスクリプトでは、特徴セットを作成している。このセットには5つのレコードが含まれる。同様に、特徴セット内の各レコードに対応するラベルを含むlabelsセットも作成した。ラベルはニューラルネットワークで予測しようとする答えである。

次のステップは、ニューラルネットワークのハイパーパラメータを定義することです。以下のスクリプトを実行してください。

np.random.seed(42)
weights = np.random.rand(3,1)
bias = np.random.rand(1)
lr = 0.05


上のスクリプトでは random.seed 関数を使用し、スクリプトが実行されるたびに同じランダム値を取得できるようにしています。

次のステップでは、正規分布に基づく乱数で重みを初期化する。入力には3つの特徴があるので、3つの重みのベクトルを用意する。次に、バイアスを別の乱数で初期化する。最後に、学習率を0.05に設定する。

次に、活性化関数とその導関数を定義する必要があります(なぜ活性化の導関数を求める必要があるのかは後述します)。活性化関数は、先ほど説明したシグモイド関数です。

次のPythonスクリプトはこの関数を作成するものです。

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


そして、シグモイド関数の微分を計算するメソッドは次のように定義されています。

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


シグモイド関数の導関数は、単純に「sigmoid(x) * sigmoid(1-x)」となります。

さて、これで人が肥満かどうかを予測するニューラルネットワークを学習させる準備が整いました。

次のスクリプトを見てください。

for epoch in range(20000):
    inputs = feature_set


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


#feedforward step2
    z = sigmoid(XW)


# backpropagation step 1
    error = z - labels


print(error.sum())


# 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


このコードを見て怖がらないでください。一行ずつ説明します。

最初のステップでは、エポック数を定義する。エポックとは、基本的にアルゴリズムをデータで学習させる回数のことです。ここでは、20,000回アルゴリズムを学習させます。この回数でテストしたところ、20,000回反復すると誤差がかなり小さくなることがわかりました。別の回数で試すことも可能です。最終的な目標は誤差を最小にすることです。

次に、feature_set の値を input 変数に格納します。そして、次の行を実行する。

XW = np.dot(feature_set, weights) + bias


ここで、入力と重みベクトルの内積を求め、それにバイアスを加える。これがフィードフォワード部のステップ1である。

この行では

z = sigmoid(XW)


フィードフォワードのステップ2で説明したように、この内積をシグモイド活性化関数に渡します。これでフィードフォワードの部分が完成した。

さて、いよいよバックプロパゲーションを開始する。変数 z には予測された出力が格納されます。バックプロパゲーションの最初のステップは、誤差を求めることです。次の行でそれを行います。

error = z - labels


そして、その誤差を画面に表示する。

さて、いよいよバックプロパゲーションのステップ2を実行します。

私たちのコスト関数は

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

この関数を各重みに関して微分する必要があります。その際、微分の連鎖法則を利用する。重みwに関するコスト関数の微分をd_costとすると、以下のように鎖の法則で微分を求めることができる。

d_costdw=d_costd_preddz,dzdw d_costdw=d_costd_preddz,dzdw
\frac {d}cost}{dw} = \frac {d}cost}{d}cpred}. \, \frac {d___pred}{dz}, \frac {dz}{dw}.

ここで

d_costd_predd
\ここで、d_costd_predd, d_costd_predd, d_costd_pred

は次のように計算できる。

2(predicted-observed)2(predicted-observed)
2 (予測値-観測値)

ここで、2は定数であるため、無視できる。これは基本的にすでに計算した誤差です。コードの中に、このような行があります。

dcost_dpred = error # ........ (2)


次に求めるのは

dd_preddzd_preddz
\Ίταμμα ταμμα ταμμα ταμμα ταμμα

ここで、”d_pred “は単にシグモイド関数で、入力のドット積 “z “に対して微分したものである。スクリプトでは次のように定義されています。

dpred_dz = sigmoid_der(z) # ......... (3)


最後に

d_zdwd
\Ίταμμα ταμμα ταμμα

ということがわかる。

z=x1w1+x2w2+x3w3+bz=x1w1+x2w2+x3w3+b
z = x1w1 + x2w2 + x3w3 + b となります。

したがって、任意の重みに関する導関数は、単純に対応する入力となる。したがって、コスト関数の最終的な導関数は、任意のウェイトに関して

slope = input x dcost_dpred x dpred_dz


次の3行を見てください。

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


これは dcost_dpreddpred_dz の積を格納した z_delta 変数を持っています。各レコードをループして入力に対応する z_delta を乗じる代わりに、入力特徴行列の転置を取り、それに z_delta を乗じる。最後に、収束速度を上げるために、学習率変数 lr に導関数を乗じる。

その後、このスクリプトに示すように、各誘導値の間をループし、バイアス値を更新する。

ループが始まると、以下のように総誤差が減少し始めることがわかります。

0.001700995120272485
0.001700910187124885
0.0017008252625468727
0.0017007403465365955
0.00170065543909367
0.0017005705402162556
0.0017004856499031988
0.0017004007681529695
0.0017003158949647542
0.0017002310303364868
0.0017001461742678046
0.0017000613267565308
0.0016999764878018585
0.0016998916574025129
0.00169980683555691
0.0016997220222637836
0.0016996372175222992
0.0016995524213307602
0.0016994676336875778
0.0016993828545920908
0.0016992980840424554
0.0016992133220379794
0.0016991285685766487
0.0016990438236577712
0.0016989590872797753
0.0016988743594415108
0.0016987896401412066
0.0016987049293782815


このように、ニューラルネットワークの学習が終わった時点で、誤差が非常に小さくなっていることが分かります。この時点では、重みとバイアスは次のような値になっています。

リソース

複雑な問題を解決するためのニューラルネットワークの作成についてもっと知りたいですか?その場合は、このオンラインコースのような他のリソースもチェックしてみてください。

ディープラーニングA-Z。ハンズオン人工ニューラルネットワーク

このコースでは、畳み込みニューラルネットワーク、リカレントニューラルネットワークなど、ニューラルネットワークをより詳しく学びます。

結論

今回は、入力層と出力層が1層ずつの非常にシンプルなニューラルネットワークをPythonでゼロから作成しました。このようなニューラルネットワークを簡単にパーセプトロンと呼びます。パーセプトロンは、線形分離可能なデータを分類することができます。線形分離可能なデータとは、n次元空間において超平面によって分離できるデータのことです。

実際の人工ニューラルネットワークはもっと複雑で強力で、複数の隠れ層と隠れ層内の複数のノードで構成されています。このようなニューラルネットワークは、非線形の実決定境界を識別することができる。Pythonでゼロから多層ニューラルネットワークを作成する方法については、次回の記事で説明する予定です。

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