ニューラルネットワーク(neural network)は、人間の脳神経回路を真似たアルゴリズム(演算する手順)です。以前の記事でご紹介した単純パーセプトロンもニューラルネットワークの一種なのですが、構造が単純すぎるため複雑な処理をすることができませんでした。しかし、ニューラルネットワークでは、中間層を追加し、ステップ関数以外の活性化関数を使うことで、複雑な処理もできるようになります。そこで今回は、ニューラルネットワークの仕組みと Python(バージョン3)での実装方法を、できるだけわかりやすくまとめてみました。
単純パーセプトロンとニューラルネットワークの違い
まずはじめに、単純パーセプトロンとニューラルネットワークの違いについて確認しておきましょう。ニューラルネットワークの特徴は次の通りです。
活性化関数がステップ関数に限定されない
言い換えれば、入力と出力の層しか無く、活性化関数にステップ関数(ある値を基準として0か1を出力する関数)を使うニューラルネットワークを単純パーセプトロンと呼びます。
ニューラルネットワークは「多層パーセプトロン」と呼ばれることもありますが、単純パーセプトロン(活性化関数がステップ関数に限定される)を多層にしたものではないことに注意しましょう。
中間層の活性化関数は必ず非線形関数を使う
ニューラルネットワークの仕組みで重要なのは「中間層の活性化関数は必ず非線形関数を使う」ことです。(ただし、出力層は線形関数を使ってもOKです)
ニューラルネットワークの中間層を増やす(これを「層を深くする」とも言います)ことで、知識を定義する要素を人工知能自らが習得することのできる ディープラーニング(深層学習)を実現しています。
しかし、中間層の活性関数に線形関数を使ってしまうと、いくら中間層を増やしたとしても中間層の無いニューラルネットワークで同じことができるため、中間層を増やす意味が無くなってしまいます。そのためニューラルネットワークの中間層の活性化関数には必ず非線形関数を使います。
単純パーセプトロンの限界
冒頭にも書きましたが、単純パーセプトロンはその名の通り構造が単純なため複雑な処理ができません。直線でも分けられるような簡単な問題を解くのが単純パーセプトロン限界です。
例えば、論理回路の AND、OR、NAND は、出力の区切りを直線で分けることができますが、XOR の出力は直線では分けることができないため、単純パーセプトロンでは XORゲートを実現することができないのです。
上のような表で見ても「直線で分ける」の意味がさっぱり分からないと思いますので、論理回路をグラフにしてみます。
この論理回路のグラフは、縦軸が入力1、横軸は入力2の値を示し、グラフ上の青の丸または赤の丸が出力の値をあらわしています。上は ANDのグラフなので、入力1と入力2が共に「1」の場合のみ、すなわち右上の出力の丸のみが「1」になっています。
では、それぞれの論理回路の出力を直線で分けてみましょう。
XOR の出力は「0」と「1」が対角線上にあるため、どうがんばっても直線では分けることができません。XOR の出力は下のような複雑な曲線でなければ分けることができないのです。
そこで、ニューラルネットワークの登場です。ニューラルネットワークであれば XORゲートのような複雑な処理も実現することができるのです。
ニューラルネットワークの実装(Python)
それでは、XORゲートを実現するニューラルネットワークを Python(バージョン3)で実装してみましょう。
実装する XORゲートのニューラルネットワークは次の通りです。図にすると複雑に見えますが、順を追っていけば実装は簡単です。
青枠の数値は「重み」と呼ばれ入力の重要度を調整するための値です。オレンジ枠の数値は「バイアス」と呼び、中間や出力の「○」(これを「ニューロン」と呼びます)に集計された値を調整するための値です。バイアスはそのままの値を使うため、上の図では便宜上入力を1として表していますが実装では気にする必要はありません。(「1 × バイアス = バイアス」なので)
中間層の活性化関数は、最近のニューラルネットワークでよく使われている ReLU関数を使い、出力層の活性化関数は1か0を出力するため、ステップ関数を使います。
あれ?ニューラルネットワークは活性化関数がステップ関数に限定されないのでは? と思われたかもしれませんが、ニューラルネットワークでステップ関数を使ってはいけないということではありません。特に出力層の活性化関数は、ほしい結果に合わせて、線形、非線形問わず自由に活性化関数を選ぶことができます。
NumPy のインポート
Python でニューラルネットワークを実装するのに欠かせないのが NumPy(ナンパイ)という数値計算を効率的に行うためのライブラリです。
ニューラルネットワークでは、入力と重みの掛け算をたくさん行います。例えばこれから実装するXORゲートのニューラルネットワークでは「入力1× 0.7 + 入力2 × -0.4」「入力1× -0.8 + 入力2 × 0.7」のような少しややこしい掛け算を行うのですが、このような掛け算のことを「行列」の掛け算(行列の積)と呼びます。
関連記事:5分でわかる!「行列」の計算方法
NumPy は、この行列の掛け算を簡単かつ高速に行うことができるので、実装するコードがシンプルになり、実行速度も速くなります。
まずコードの先頭で NumPy をインポートします。NumPy の関数はなんども使うことになりますので import numpy as np としてインポートすることで numpy を np の2文字で呼び出せるようにしておきます。
import numpy as np
活性化関数の準備
中間層と出力層で使う活性化関数の ReLU関数とステップ関数を、コード上でも関数として実装しておきます。
# ReLU関数 def relu_function(x): return np.where(x > 0, x, 0) # ステップ関数 def step_function(x): return np.where(x > 0, 1, 0)
さっそく NumPy( np で呼び出しています)の numpy.where 関数を使って実装しています。わずか1行で実装できるのでわざわざ関数にする必要がなさそうですが、ニューラルネットワークのコードが読みやすくなりますので、活性化関数はコード上でも関数として実装しておきます。
関連記事:5分でわかる!活性化関数の実装方法(Python)
XORゲートのニューラルネットワーク
下準備ができたところで XORゲートのニューラルネットワークを実装していきます。
まず入力層の実装です。
XORゲートのニューラルネットワークを xor_gate 関数として定義し、入力1と入力2を引数 input1 と input2 で指定できるようにしておきます。入力された値は np.array で変数 x に配列として格納しておきます。
# XORゲートのニューラルネットワーク def xor_gate(input1, input2): x = np.array([input1, input2])
続いて中間層への重み(W1)とバイアス(b1)の指定です。
慣例に従って、重みは大文字の変数 W1 に格納し、バイアスは小文字の変数 b1 に格納します。
W1 = np.array([[0.7, -0.8], [-0.4, 0.7]]) b1 = np.array([-0.2, -0.3])
ニューラルネットワークの実装の慣例として、重みだけを W1 といったように大文字で表記し、それ以外(バイアスや中間結果など)は小文字で表記します。
ゼロから作るDeep Learning(P64)より引用
入力(x)、重み(W1)、バイアス(b1)がそろったところで、中間層の計算です。
NumPy を使わない場合「(入力1 × 0.7 + 入力2 × -0.4)+ -0.2 = 中間1」「(入力1 × -0.8 + 入力2 × 0.7)+ -0.3 = 中間2」のような面倒な計算をしなければならないのですが、行列の掛け算を行う NumPy の numpy.dot 関数を使えば、下のような簡単なコードで実装することができます。np.dot で 入力(x)と 重み(W1)の行列の掛け算を行い、バイアス(b)の値を加えた値を、中間層の計算結果(m1)に格納しています。
m1 = np.dot(x, W1) + b1
入力1を「1」、入力2も「1」を指定した場合、中間層の計算結果(m1)は次のようになります、中間層の計算結果の変数 m1 も配列であることがポイントです。1つめの要素(0.1)が中間1のニューロン、2つめの要素(-0.4)が中間2のニューロンに対応しています。
print(m1) [ 0.1 -0.4]
中間層の活性化関数である ReLU関数の処理です。
中間層の計算結果(m1)を、準備しておいた ReLU関数(relu_function)に渡して、その結果を中間層の出力として変数 z1 に格納します。
z1 = relu_function(m1)
ReLU関数は、渡された値が0を超えていればその値をそのまま出力し、渡された値が0以下なら0を出力します。入力1を「1」、入力2も「1」を指定した場合の中間層の出力(z1)は次のようになります。(表示されている「0.」は 0 のことです)
print(z1) [0.1 0. ]
出力層への重み(W2)とバイアス(b2)の指定です。
出力層へのバイアス(上の図の「-0.2」)は1つなので、配列ではなくそのまま変数 b2 に格納します。
W2 = np.array([0.5, 0.6]) b2 = -0.2
続いて出力層の計算です。あともう少しで完成です。
出力層の計算結果(out)には、中間層の出力(z1)と出力層への重み(W2)の掛け算の合計に、バイアス(b2)を加えるので numpy.sum 関数を使います。下のコードの np.sum では「中間1の出力 × 0.5 + 中間2の出力 × 0.6」のような計算を行っています。
out = np.sum(z1 * W2) + b2
最後に、出力層のステップ関数の処理です。このステップ関数により1または0が出力されるようになります。
出力層の計算結果(out)をステップ関数(step_function)に渡して、その結果(y)を xor_gate 関数の戻り値とします。
y = step_function(out) return y
完成したコード
完成した XORゲートのニューラルネットワークのコードは次の通りです。(最後に確認用のコードを追加しています)
xor_gate.py
import numpy as np # ReLU関数 def relu_function(x): return np.where(x > 0, x, 0) # ステップ関数 def step_function(x): return np.where(x > 0, 1, 0) # XORゲートのニューラルネットワーク def xor_gate(input1, input2): x = np.array([input1, input2]) W1 = np.array([[0.7, -0.8], [-0.4, 0.7]]) b1 = np.array([-0.2, -0.3]) m1 = np.dot(x, W1) + b1 z1 = relu_function(m1) W2 = np.array([0.5, 0.6]) b2 = -0.2 out = np.sum(z1 * W2) + b2 y = step_function(out) return y # 確認用コード print(xor_gate(0, 0)) print(xor_gate(1, 0)) print(xor_gate(0, 1)) print(xor_gate(1, 1))
上のコードをファイル名「xor_gate.py」として保存し、確認のため実行してみましょう。(Python バージョン3で実行してください)
$ python xor_gate.py 0 1 1 0
上のように出力されていれば、正しく XORゲートとして動作しています。
おわりに
ニューラルネットワークの実装おつかれさまでした! 今回はシンプルなニューラルネットワークの実装でしたが、入力や中間層が増えたとしても今回実装したコードを応用することで対応できます。
コメント
とても分かりやすい記事でした。勉強させていただきました。
>匿名さん
コメントありがとうざいます。
勉強がんばってください!