はじめに
「プログラミング」初学者のための『ゼロから作るDeep Learning』攻略ノートです。『ゼロつくシリーズ』学習の補助となるように適宜解説を加えています。本と一緒に読んでください。
関数やクラスとして実装される処理の塊を細かく分解して、1つずつ実行結果を見ながら処理の意図を確認していきます。
この記事は、2章「パーセプトロン」の内容です。パーセプトロンをPythonで実装します。
【前節の内容】
【他の節の内容】
【この節の内容】
2章 パーセプトロン
2章では、4つの論理回路AND・NAND・OR・XORの実装を通じて、パーセプトロンのアルゴリズムを解説します。
利用するライブラリを読み込みます。
# 2章で利用するライブラリ import numpy as np
2.2 単純な論理回路
パーセプトロンを用いた実装の前に、論理演算子を使って3つの論理回路AND・NAND・ORの出力を確認します。
・論理演算子
まずは、論理演算子and
・or
・not
の機能を確認しましょう。ちなみに、この本では終盤に(1・2回だけ)条件式(if
文)の中で使われます。
論理演算子は、bool
型(ブーリアン,真偽値,論理値)や条件式に対して利用します。論理値は、真True
と偽False
の2つの値をとるデータ型です。また、True
は1
、False
は0
としても扱われます(論理値を使って計算もできます)。
and
は、論理積と呼ばれ、次の形式で使います。
論理値(条件式) and 論理値(条件式)
2つの論理値がどちらもTrue
のときTrue
を、それ以外のときFalse
となります。
# 論理値を使った例 print(True and True) print(True and False) print(False and True) print(False and False)
True
False
False
False
条件式でも同じ結果になります。
# 条件式を使った例 print(10 == 10 and 10 < 11) print(True and 10 > 11)
True
False
10 == 10
と10 < 11
はTrue
、10 > 11
はFalse
として処理されています。
or
は、論理和と呼ばれ、次の形式で使います。
論理値(条件式) or 論理値(条件式)
2つの論理値が1つでもTrue
のときTrue
を、どちらもFalse
のときFalse
となります。
# 論理値を使った例 print(True or True) print(True or False) print(False or True) print(False or False)
True
True
True
False
条件式でも同様です。
# 条件式を使った例 print(10 == 10 or 10 < 11) print(True or 10 > 11)
True
True
not
は、否定と呼ばれ、次の形式で使います。
not 論理値(条件式)
論理値がTrue
のときFalse
を、False
のときTrue
となります。つまり、渡された値と反対の値を返します。
# 論理値を使った例 print(not True) print(not False)
False
True
条件式でも同様です。
# 条件式を使った例 print(not 6 == 6) print(not 11 > 13)
False
True
論理演算子を確認できました。
・論理演算子を用いた論理回路
続いて、論理演算子and
・or
・not
を用いて、単純な論理回路AND・NAND・ORを再現していきます。
ANDゲート(論理積)は、2つの入力がどちらも1
のとき1
を、それ以外のとき0
を出力します。つまり、and
をそのまま使って処理できます。
# ANDゲート print(0 and 0) print(1 and 0) print(0 and 1) print(1 and 1)
0
0
0
1
NANDゲート(否定論理積)は、ANDゲートの出力を反転させたものです。2つの入力がどちらも1
のとき0
を、それ以外のとき1
を出力します。and
の出力をnot
で処理します。
# NANDゲート print(int(not (0 and 0))) print(int(not (1 and 0))) print(int(not (0 and 1))) print(int(not (1 and 1)))
1
1
1
0
int()
を使って、論理値True, False
から整数1, 0
に変換しています。
# 論理値から整数に変換 print(int(True)) print(int(False))
1
0
True
は1
、False
は0
に対応しています。
文字列(str
)型や浮動小数点(float
)型からも整数(int
)型に変換できます。
# 文字列から整数に変換 print(int('10')) # 浮動小数点から整数に変換 print(int(10.0)) print(int(10.5))
10
10
10
ただし、小数点以下は切り捨てられます。int()
も終盤に、データ数や次元数など整数を扱う計算にて使われます。
ORゲート(論理和)は、2つの入力が1つでも1
のとき1
を、どちらも0
のとき0
を出力します。これも、or
をそのまま使って処理できます。
# ORゲート print(0 or 0) print(1 or 0) print(0 or 1) print(1 or 1)
0
1
1
1
以上で、3つの単純な論理回路を確認できました。次からは、パーセプトロンを用いて論理回路を再現していきます。
2.3 パーセプトロンを用いた論理回路の実装
パーセプトロンを用いて論理回路AND・NAND・ORを実装します。
2.3.1 簡単な実装
まずは、ANDゲートを例として、パーセプトロンで行う処理を確認していきます。
パーセプトロンは、次の式の計算(処理)を行います。
この式は、「重み付き和$w_1 x_1 + w_2 x_2$」と「閾値$\theta$」を比較して、等しいまたは重み付き和が大きいとき出力$y$が1、重み付き和が小さいとき$y$が0であることを表しています。
・処理の確認
パーセプトロンによるANDゲートで行う処理を確認します。
2つの入力$x_1,\ x_2$をx1, x2
として値を指定します(変数を作成します)。
# 仮の入力を指定 x1 = 0 x2 = 1 print(x1) print(x2)
0
1
次のようにして、1行で複数の変数を作成できます。
# 仮の入力を指定 x1, x2 = 0, 1 print(x1) print(x2)
0
1
これは実装に必要な処理を確認するのが目的なので、0
と1
の4パターンの組み合わせからどれか1つ指定します。
同様に、重み$w_1,\ w_2$をw1, w2
、閾値$\theta$をtheta
として値を指定します。
# 重みと閾値を設定 w1, w2, theta = 0.5, 0.5, 0.7 print(w1) print(w2) print(theta)
0.5
0.5
0.7
この例では、$w_1 = 0.5,\ w_2 = 0.5,\ \theta = 0.7$とします。
これで必要な変数(オブジェクト)を作成できました。続いて、出力に関する処理を行います。
$w_1 x_1 + w_2 x_2$を計算して、結果をtmp
に代入します。この計算結果は、theta
との大小関係を調べるために利用する一時的なデータなので、変数名をtmp
(temporary:一時的)とします(よく使われる表現です)。
# 重み付き和を計算 tmp = x1 * w1 + x2 * w2 print(tmp)
0.5
入力ごとに対応する重みを掛けた総和を重み付き和と呼びます。
重み付き和tmp
が、閾値theta
以下であれば0
、theta
より大きければ1
を出力します。条件によって異なる処理をする場合(条件分岐)にはif
文を使います。
# 条件に従って値を出力 if tmp <= theta: # 閾値以下の場合 # 0を出力 print(0) elif tmp > theta: # 閾値より大きい場合 # 1を出力 print(1)
0
重み付き和0.5
が閾値0.7
より小さいので、0
が出力されました。
以上がパーセプトロンを用いたANDゲートで行う処理です。
・実装
処理の確認ができたので、ANDゲートを関数として実装します。
入力x1, x2
はこの時点では何か分からない(関数を使うときに渡す)値なので、引数として受け取れるように実装します。重みと閾値は固定の値なので、関数内で指定します。
# ANDゲートの実装 def AND(x1, x2): # 重みと閾値を設定 w1, w2, theta = 0.5, 0.5, 0.7 # 重み付き和を計算 tmp = x1 * w1 + x2 * w2 # 条件に従って値を出力 if tmp <= theta: # 閾値以下の場合 return 0 elif tmp > theta: # 閾値より大きい場合 return 1
関数を定義できたので、全ての入力パターンで確認してみましょう。
# ANDゲートの処理を確認 print(AND(0, 0)) print(AND(1, 0)) print(AND(0, 1)) print(AND(1, 1))
0
0
0
1
正しく出力されました!
さて、目的通りの出力が得られましたが、もっと効率的な実装方法もあります。次は、他の論理回路も含めて効率化した処理で実装していきます。
2.3.2 重みとバイアスの導入
2.3.1項では、閾値が出力に影響しました。以降は、閾値の代わりにバイアスを用います。
・数式の確認
まずは、数式からパーセプトロンを確認します。
これまでのパーセプトロンでは、次の計算を行っていました。
この式は、「重み付き和$w_1 x_1 + w_2 x_2$」と「閾値$\theta$」を比較して、出力が$y = 1$または$y = 0$に決まることを表しているのでした。
式(2.1)の$\theta$を左辺に移項します(両辺から$\theta$を引きます)。
この式変形によって、「左辺」が「0以下か0より大きいか」で出力$y$の値が決まるようになりました。勿論、式(2.1)と式(2.1')とで結果は変わりません。
さらに、この式(2.1')を$b = - \theta$で置き換えます($- \theta$を$b$に置き換えます)。
$b$をバイアスと呼びます。つまり、「重み付き和にバイアスを加えた値」が「0以下か0より大きいか」で出力$y$が決まります。勿論、式(2.1)と計算結果は変わりません。
$w_1 x_1 + w_2 x_2 + b$の計算は、この本を通して最も基本的なそして重要な計算です。以降はこれを「重み付き入力とバイアスの和」または単に「重み付き和」と呼びます。
また、この本では登場しない表現(なので読み飛ばして問題ないの)ですが、次のように重みとバイアスをまとめて表記することもあります。
・むしろややこしくなるので省略(クリックで展開)
バイアス$b$を0番目の重み$w_0$とみなして、重みを$\mathbf{w} = (w_0, w_1, w_2)$とします。また、$w_0$に対応する入力を$x_0 = 1$として(0番目の入力を1で固定して)、入力を$\boldsymbol{\mathrm{x}} = (x_0, x_1, x_2) = (1, x_1, x_2)$とします。
これによって、重み付き入力とバイアスの和を、入力$\mathbf{x}$と重み$\mathbf{w}$の内積$\mathbf{x} \mathbf{w}$で表現できます。
ベクトルや内積については次章で説明します。また、今後登場する$x_0$や$w_0$は、ここで登場したものとは別のものです。
・処理の確認
次は、効率化したパーセプトロンによるANDゲートで行う処理(式(2.2)の計算)を確認します。
先ほどは、$x_1,\ x_2$に対応する2つの変数(オブジェクト)x1, x2
を用意しました。ここでは、1つのNumPy配列を用いて$\boldsymbol{\mathrm{x}} = (x_1, x_2)$の形式で扱います。
# 仮の入力を指定 x = np.array([0, 1])
$x_1$がx[0]
、$x_2$がx[1]
に対応します。
同様に、重み$\mathbf{w} = (w_1, w_2)$とバイアス$b$の値を指定します。
# 重みを設定 w = np.array([0.5, 0.5]) # バイアスを設定 b = -0.7
$w_1$がw[0]
、$w_2$がw[1]
です。
これで必要な変数(オブジェクト)を作成できました。続いて、出力に関する処理を行います。
入力と対応する重みの積は、配列同士の掛け算で行えます。
# 入力を重み付け print(x * w)
[0. 0.5]
1つ目の要素同士、2つ目の要素同士で計算されているのを確認できます。
対応する要素同士の掛け算の結果をnp.sum()
で合計します。
# 重み付き和を計算 print(np.sum(x * w))
0.5
NumPy配列として扱うことで、NumPyの関数を利用しやすくなります。
さらに、バイアスを加えた値をtmp
とします。
# 重み付き和とバイアスの和 tmp = np.sum(x * w) + b print(tmp)
-0.19999999999999996
重み付き和が求まりました。-0.2
にならないのはコンピュータ上で数値を扱う際に生じる誤差です。この実装では問題にならないのでこのまま進めます。
複数の入力を配列として扱うことで、今後入力の要素数が増えて$\mathbf{x} = (x_1, x_2, \cdots, x_n)$となっても、データ数に関わらず同じ処理を行えます。x1 * w1 + x2 * w2 + ・・・
とは書きたくないですよね。
重み付き和tmp
の値が、0
以下であれば0
を、0
より大きければ1
を出力します。
先ほどの実装例では、if
とelif
を使いました。しかし、0
以下でない場合は0
より大きい値なので、2つ目の条件を調べる必要はありません。よって、if
とelse
を使います。
# 条件に従って値を出力 if tmp <= 0: # 0以下の場合 # 0を出力 print(0) else: # 0より大きい場合 # 1を出力 print(1)
0
目的通りの出力が得られました。
以上がパーセプトロンを用いたANDゲートで行う(効率化した)処理です。
・実装
処理の確認ができたので、3つの論理回路AND・NAND・ORを関数として実装します。
ANDゲートを実装します。引数として与えられる2つの値を、NumPy配列のx
に変換する処理を加えます。
# ANDゲートの実装 def AND(x1, x2): # 入力をNumPy配列にまとめる x = np.array([x1, x2]) # 重みとバイアスを設定 w = np.array([0.5, 0.5]) b = -0.7 # 重み付き和を計算 tmp = np.sum(x * w) + b # 条件に従って値を出力 if tmp <= 0: # 0以下の場合 # 0を出力 return 0 else: # 0より大きい場合 # 1を出力 return 1
実装した関数を試してみましょう。
# ANDゲートの処理を確認 print(AND(0, 0)) print(AND(1, 0)) print(AND(0, 1)) print(AND(1, 1))
0
0
0
1
問題ないですね!
このように、様々な方法で目的の処理や計算を行えます。時間やマシンスペックといった制限の中で、効率的な実装を考えることも重要になってきます(追々ね)。
同様に、NANDゲートも実装します。AND()
のパラメータの値を変更するだけで実装できます。
# NANDゲートの実装 def NAND(x1, x2): # 入力をNumPy配列にまとめる x = np.array([x1, x2]) # 重みとバイアスを設定 w = np.array([-0.5, -0.5]) b = 0.7 # 重み付き和を計算 tmp = np.sum(x * w) + b # 条件に従って値を出力 if tmp <= 0: # 0以下の場合 return 0 else: # 0より大きい場合 return 1
# NANDゲートの処理を確認 print(NAND(0, 0)) print(NAND(1, 0)) print(NAND(0, 1)) print(NAND(1, 1))
1
1
1
0
ORゲートもパラメータの値を変更するだけで実装できます。
# ORゲートの実装 def OR(x1, x2): # 入力をNumPy配列にまとめる x = np.array([x1, x2]) # 重みとバイアスを設定 w = np.array([0.5, 0.5]) b = -0.2 # 重み付き和を計算 tmp = np.sum(x * w) + b # 条件に従って値を出力 if tmp <= 0: # 0以下の場合 return 0 else: # 0より大きい場合 return 1
# ORゲートの処理を確認 print(OR(0, 0)) print(OR(1, 0)) print(OR(0, 1)) print(OR(1, 1))
0
1
1
1
単純な論理回路を実装できました。次は、同じ方法では実装できない論理回路を実装します。
2.5 多層パーセプトロン
パラメータの調整だけでは対応できない場合は、層を重ねる(処理を組み合わせる)ことで出力を調整できます。
これまでに実装したAND()
・NAND()
・OR()
を2層に組み合わせてXORゲート(排他的論理和)を実装します。
まずは、図2-11を参考にして処理を確認します。
これまでのように、入力$x_1,\ x_2$を作成します。
# 仮の入力を指定(第0層) x1, x2 = 1, 1
4つのパターンから1つの組み合わせを指定します。
第1層の計算を行います。NAND()
の出力をs1
、OR()
の出力をs2
とします。
# 第1層の演算 s1 = NAND(x1, x2) s2 = OR(x1, x2) print(s1) print(s2)
0
1
s1, s2
が第1層の出力であり、第2層の入力でもあります。
第1層の出力s1, s2
を次の層AND()
に入力します。
# 第2層の演算 y = AND(s1, s2) print(y)
0
全体の出力y
が得られました。
処理の確認ができたので、XORゲートを関数として実装します。
# XORゲートの実装 def XOR(x1, x2): # 第1層の演算 s1 = NAND(x1, x2) s2 = OR(x1, x2) # 第2層の演算 y = AND(s1, s2) # 結果を出力 return y
実装した関数を試してみましょう。
# XORゲートの処理を確認 print(XOR(0, 0)) print(XOR(1, 0)) print(XOR(0, 1)) print(XOR(1, 1))
0
1
1
0
AND()
・NAND()
・OR()
のそれぞれが1層のパーセプトロンです。パーセプトロンを2層にすることでXORを実装できました。
・重みの役割
最後におまけとして、少し違う視点から重みの役割を見てみます(おまけなので飛ばしても問題ないです)。
・話がそれるので省略(クリックで展開)
2つの入力を$x_1 = 10,\ x_2 = 5$とします。これを$\boldsymbol{\mathrm{x}} = (10, 5)$と書くことにします。また、2つの重みを$w_1 = 0.1,\ w_2 = 0.9$とします。こちらも$\boldsymbol{\mathrm{w}} = (0.1, 0.9)$と書きます。
# 入力を指定 x = np.array([10, 5]) # 重みを指定 w = np.array([0.1, 0.9])
入力をそれぞれ重み付けしてみましょう。
# 各入力を重み付け print(x * w)
[1. 4.5]
1つ目の値は10
から1
に、2つ目の値は5
から4.5
になりました。重みによって大小関係が入れ替わっていますね。出力に用いる入力の情報を重みによって調整したと言えます。
入力と重みと出力の関係は、例えば「マンガの魅力」として「キャラクター」と「ストーリー」のどちらを重視するのかというイメージです。重視する方の重みが大きくなります。
キャラ点を$x_{\mathrm{chara}}$、ストーリー点を$x_{\mathrm{story}}$で表すことにします。同様に、キャラとストーリーの重み(重要度)をそれぞれ$w_{\mathrm{chara}},\ w_{\mathrm{story}}$で表すことにします。
ある作品の点が$\mathbf{x} = (x_{\mathrm{chara}}, x_{\mathrm{story}}) = (40, 60)$だったとします。この作品を、キャラとストーリーのどちらも重視する人$\mathbf{w} = (w_{\mathrm{chara}}, w_{\mathrm{story}}) = (0.5, 0.5)$が評価すると、魅力度は次のようになります。
# 入力を指定 x = np.array([40, 60]) # 重みを指定 w = np.array([0.5, 0.5]) # 重み付き和(魅力)を計算 print(np.sum(x * w))
50.0
マンガの魅力は50
となりました。
また、ストーリー重視の人$\mathbf{w} = (0.3, 0.7)$にとっては次にようになります。
# 重みを指定 w = np.array([0.3, 0.7]) # 重み付き和(魅力)を計算 np.sum(x * w)
54.0
先ほどの人よりも高く評価(出力)されました。重視している(重みが大きい)「ストーリー」の点が高いからですね。
さらに、バイアスも加えて考えてみましょう。
例えば、$b = -50$とすれば、魅力とバイアスの和が0を越えれば買う($y = 1$)、0以下だと買わない($y = 0$)となります。閾値$\theta = -b = 50$でいうと、魅力が50を超えると買う、50以下だと買わないということですね。
別の例として、好きな雑誌によって加点するイメージです。ジャンプマンガであればキャラとストーリーとは別に10点加算($b = 10$)する働きをします。
パラメータ(重みとバイアス)をイメージできたでしょうか。
以上で、2章の内容は完了です。2章では、パーセプトロンを用いて目的の値を出力(論理回路を実装)しました。また、「パラメータを調整する」ことと「層を重ねる」ことで出力を調整できることが分かりました。
3章では、より多様な出力を表現できるようにパーセプトロンを発展させた「ニューラルネットワーク」について解説します。このニューラルネットワークが深層学習の肝になります。また、ニューラルネットワークの層を深く重ねることを深層と表現しています。
4章では、機械的にパラメータを調整することを意味する「学習」について解説します。
参考文献
おわりに
順調です!
- 2021.07.23:加筆修正しました。
パーセプトロンと論理回路の関係、パーセプトロンとニューラルネットワークの関係を整理できた。そして文字数が倍に増えてしまった。
【次節の内容】