からっぽのしょこ

読んだら書く!書いたら読む!同じ事は二度調べ(たく)ない

2章:パーセプトロン【ゼロつく1のノート(実装)】

はじめに

 「プログラミング」初学者のための『ゼロから作るDeep Learning』攻略ノートです。『ゼロつくシリーズ』学習の補助となるように適宜解説を加えています。本と一緒に読んでください。

 関数やクラスとして実装される処理の塊を細かく分解して、1つずつ実行結果を見ながら処理の意図を確認していきます。

 この記事は、2章「パーセプトロン」の内容です。パーセプトロンをPythonで実装します。

【前節の内容】

www.anarchive-beta.com

【他の節の内容】

www.anarchive-beta.com

【この節の内容】

2章 パーセプトロン

 2章では、4つの論理回路AND・NAND・OR・XORの実装を通じて、パーセプトロンのアルゴリズムを解説します。

 利用するライブラリを読み込みます。

# 2章で利用するライブラリ
import numpy as np


2.2 単純な論理回路

 パーセプトロンを用いた実装の前に、論理演算子を使って3つの論理回路AND・NAND・ORの出力を確認します。

・論理演算子

 まずは、論理演算子andornotの機能を確認しましょう。ちなみに、この本では終盤に(1・2回だけ)条件式(if文)の中で使われます。
 論理演算子は、bool型(ブーリアン,真偽値,論理値)や条件式に対して利用します。論理値は、真Trueと偽Falseの2つの値をとるデータ型です。また、True1False0としても扱われます(論理値を使って計算もできます)。

 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 == 1010 < 11True10 > 11Falseとして処理されています。

 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


 論理演算子を確認できました。

・論理演算子を用いた論理回路

 続いて、論理演算子andornotを用いて、単純な論理回路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

 True1False0に対応しています。

 文字列(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ゲートを例として、パーセプトロンで行う処理を確認していきます。

 パーセプトロンは、次の式の計算(処理)を行います。

$$ y = \begin{cases} 0 \quad (w_1 x_1 + w_2 x_2 \leq \theta) \\ 1 \quad (w_1 x_1 + w_2 x_2 > \theta) \end{cases} \tag{2.1} $$

 この式は、「重み付き和$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

 これは実装に必要な処理を確認するのが目的なので、01の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以下であれば0thetaより大きければ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項では、閾値が出力に影響しました。以降は、閾値の代わりにバイアスを用います。

・数式の確認

 まずは、数式からパーセプトロンを確認します。

 これまでのパーセプトロンでは、次の計算を行っていました。

$$ y = \begin{cases} 0 \quad (w_1 x_1 + w_2 x_2 \leq \theta) \\ 1 \quad (w_1 x_1 + w_2 x_2 > \theta) \end{cases} \tag{2.1} $$

 この式は、「重み付き和$w_1 x_1 + w_2 x_2$」と「閾値$\theta$」を比較して、出力が$y = 1$または$y = 0$に決まることを表しているのでした。
 式(2.1)の$\theta$を左辺に移項します(両辺から$\theta$を引きます)。

$$ y = \begin{cases} 0 \quad (w_1 x_1 + w_2 x_2 - \theta \leq 0) \\ 1 \quad (w_1 x_1 + w_2 x_2 - \theta > 0) \end{cases} \tag{2.1'} $$

 この式変形によって、「左辺」が「0以下か0より大きいか」で出力$y$の値が決まるようになりました。勿論、式(2.1)と式(2.1')とで結果は変わりません。
 さらに、この式(2.1')を$b = - \theta$で置き換えます($- \theta$を$b$に置き換えます)。

$$ y = \begin{cases} 0 \quad (w_1 x_1 + w_2 x_2 + b \leq 0) \\ 1 \quad (w_1 x_1 + w_2 x_2 + b > 0) \end{cases} \tag{2.2} $$

 $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}$で表現できます。

$$ \begin{aligned} w_1 x_1 + w_2 x_2 + b &= \mathbf{x} \mathbf{w} \\ &= \sum_{i=0}^2 w_i x_i \\ &= w_0 x_0 + w_1 x_1 + w_2 x_2 \\ &= w_0 + w_1 x_1 + w_2 x_2 \\ \end{aligned} $$

 ベクトルや内積については次章で説明します。また、今後登場する$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を出力します。
 先ほどの実装例では、ifelifを使いました。しかし、0以下でない場合は0より大きい値なので、2つ目の条件を調べる必要はありません。よって、ifelseを使います。

# 条件に従って値を出力
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()の出力をs1OR()の出力を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章では、機械的にパラメータを調整することを意味する「学習」について解説します。

参考文献

github.com

おわりに

 順調です!

  • 2021.07.23:加筆修正しました。

 パーセプトロンと論理回路の関係、パーセプトロンとニューラルネットワークの関係を整理できた。そして文字数が倍に増えてしまった。

【次節の内容】

www.anarchive-beta.com