ougarairin’s diary

技術的なことと読んだ本とV

単純パーセプトロンの設計と実装 -学習もするよ-

真面目にプログラムをやってみる

はじめに

ここでは, 単純パーセプトロンについて理論的なところからpython による実装までやってみます.

初心者のため, 間違いや罵詈雑言等ありましたらコメント欄までお願いします. (ただし聞くとは言ってない)

理論編 (設計編)

ここでは, 後のpython 実装に向けて単純パーセプトロンの設計をしていきます. 多少の数学は使いますが, 厳密な議論はしません.(そんな数学力をもっていない...)

単純パーセプトロンとは

ニューラルネットワークの一種. 入力と出力の2層からなる.

推論部分の設計

ニューロンのモデルを図に示すと以下になります. (今回は単純パーセプトロンを対象とするためこれ一つだけです)

f:id:ougarairin:20200429180617p:plain

ここで,  x_0, x_1 は入力,  w は重み,  b はバイアス,  z が計算の途中結果,  y が出力とします.

このとき,  y が推論結果となります.

では, 計算方法です.

まず中間結果である z を求めてみます.

\displaystyle{
\begin{align}
    z &= x_0 w_0 + x_1 w_1  + b \\
      &= b + \sum_i x_i w_i
\end{align}
}

もし,  x, w をベクトルとして表す場合, すなわち  \boldsymbol{x}=(x_0, x_1),  \boldsymbol{w}=(w_0, w_1) とすると,  z

\displaystyle{
z = b + \boldsymbol{x} \cdot \boldsymbol{w}
}

となります. (ベクトルの内積として計算できます)

次に, 出力 y を計算します. 
y = f(z)
(いや f ってなんだよ) ここで f は活性化関数と呼ばれる関数です. 実際にはいくつも種類がありますが, それは今回割愛します. 今回はステップ関数を使用します. すなわち

\displaystyle{
f(x) = 
\begin{cases}
            1 \quad x \geqq 0 \\
            0 \quad x < 0 \\
\end{cases}
}

を使用します.

学習部分の設計

学習をするうえでその指標となる関数を定義するところから始めます.(この関数のことを損失関数 あるいは誤差関数といいます)

損失関数の設計

損失関数を E(y_p, t_p) とすると, 今回は

\displaystyle{
E(y_p, t_p) = \frac12 (y_p - t_p)^2
}

を使用します.(ちなみにこれは2乗和誤差です)

この時, 損失関数は

\displaystyle{
E(\boldsymbol{w}) = \frac12 (f(b + \boldsymbol{x_p} \cdot \boldsymbol{w})-t_p)^2 \\
E(b) = \frac12 (f(b + \boldsymbol{x_p} \cdot \boldsymbol{w})-t_p)^2
}

とも表すことができます.

さて, 損失関数は「予測値-真値」 で構成されています. 予測値が真値と一致している場合, 損失関数は0となり, 不一致の場合その差分が損失関数の値となります. すなわち, 損失関数は小さいほうがより学習できていることになります. これより, 損失関数が最小となる重み \boldsymbol{w} バイアス b を求めることがここでの学習ということになります.

まとめると

損失関数 E(\boldsymbol{w}), E(b) が最小となる  \boldsymbol{w}, b が知りたい

となります. これを解決するためのアイデアの一つとして, 勾配降下法が存在します.

勾配降下法

f:id:ougarairin:20200429180708p:plain

例えば下に凸の放物線の最小値を勾配降下法を用いて求めてみることにします. 手順はざっくりいうと以下のステップです.

  1. ランダムに x をとる
  2. 座標 (x, f(x)) を求める
  3. 求めた座標での接線の傾きを求める
  4. 求めた傾きに従って x を更新する
  5. 条件を満たしていない場合2 に戻る

さて, 上記の手順の中で意味不明 引っかかるポイントは「4. 求めた傾きに従って x を更新する」です. これについて少し見ていきます.

「1. ランダムに x をとる 」「2. 座標 (x, f(x)) を求める」にて求めた座標がグラフ上の青い点だったとし, 「3. 求めた座標での接線の傾きを求める」にて求めた接線が青色の直線だったとします. 接線を確認してみるとその傾きは右肩上がり, すなわち傾きの値は正です. ここで一つ確認しておくことは, 今回の関数 f(x) の最小値はこの関数の頂点, すなわちグラフ上では黒い点です. つまりこの青い点での「4. 求めた傾きに従って x を更新する」では今より左, すなわち負の方向に x を更新する必要があります. これをまとめると 
x^{(new)} = x - \frac{df(x)}{dx}
となります.(更新する量についてはとりあえず置いておきます)

次に, 赤い点についても同様に確認してみましょう. 赤色の線(赤色の座標での接線) は右肩下がりのグラフであり, 最小値は赤色の点より右にあります. すなわち,  x は正の方向に更新する必要があります. しかし, この時の傾きは負であるため結局のところ青の点のときと同様の式にて記述できます.

また, 各点における傾きの大きさを見てみましょう. 黒色の点から遠い青色の点のほうが傾きが大きくなっています. ここで, 最小値から遠いほど大きく, 最小値から近いときは小さく x を更新することが好ましいと考えられます. 以上より結局は上記の式にて更新する量もよいことになります. ただし実際には学習する量(段階) は調節できるようにしたいため, 学習率(learning rate):  lr を用いて 
x^{(new)} = x - lr \cdot \frac{df(x)}{dx}
となります.

勾配降下法による学習

勾配降下法を用いれば

損失関数 E(\boldsymbol{w}), E(b) が最小となる  \boldsymbol{w}, b が知りたい

も解決できます. 単純に 
\boldsymbol{w}^{(new)} = \boldsymbol{w} - lr \cdot \frac{dE(\boldsymbol{w})}{d\boldsymbol{w}} \
b^{(new)} = b - lr \cdot \frac{dE(b)}{db}
を更新式として勾配降下法を適用すればいいためです.

また, 今回は「5. 条件を満たしていない場合2 に戻る」での条件は重みやバイアスの値が変化しなくなった場合とします.

損失関数の微分

なんかできた雰囲気あるけどさっぱりわからないよ...

さて,  \frac{dE(\boldsymbol{w})}{d\boldsymbol{w}},  \frac{dE(b)}{db} とはどんな値でしょうか... あとは微分のお時間です.

まず, 連鎖率を用いると

\displaystyle{
\begin{align}
    \frac{dE(\boldsymbol{w})}{d\boldsymbol{w}} &= \frac{dE(y_p, t_p)}{d\boldsymbol{w}} = \frac{dE(y_p, t_p)}{dy_p}\cdot \frac{dy_p}{dz} \cdot \frac{dz}{d\boldsymbol{w}} \\
    \frac{dE(b)}{db} &= \frac{dE(y_p, t_p)}{db} =\frac{dE(y_p, t_p)}{dy_p}\cdot \frac{dy_p}{dz} \cdot \frac{dz}{db} \\
\end{align}
}

一つひとつ見てみます. まず一つ目

\displaystyle{
\frac{dE(y_p, t_p)}{dy_p} = \frac{d}{dy_p} \cdot \frac12(y_p-t_p)^2 = y_p - t_p
}

二つ目

\displaystyle{
\frac{dy_p}{dz} = \frac{d}{dz} \cdot f(z) = 1
}

ここでこの微分は活性化関数の微分にあたります. 今回はとりあえず1 です.(多分)

三つ目

\displaystyle{
\frac{dz}{d\boldsymbol{w}} = \frac{d}{d\boldsymbol{w}} \cdot(b + \boldsymbol{x_p} \cdot \boldsymbol{w}) = \boldsymbol{x_p}
}

バイアスの場合

\displaystyle{
\frac{dz}{db} = \frac{d}{db} \cdot(b + \boldsymbol{x_p} \cdot \boldsymbol{w}) = 1
}

以上をまとめると

\displaystyle{
\begin{align}
    \frac{dE(\boldsymbol{w})}{d\boldsymbol{w}} &= (y_p - t_p) \cdot 1 \cdot \boldsymbol{x_p} \\
    &= (y_p - t_p) \cdot \boldsymbol{x_p} \\
    \frac{dE(b)}{db} &= (y_p - t_p) \cdot 1\\
    &= (y_p - t_p)
\end{align}
}

まとめ(設計)

ハイパラメータ

  •  lr: 学習率

パラメータ

  •  \boldsymbol{x}: 入力データ
  •  \boldsymbol{t} : 教師データ
  •  f : 活性化関数 今回はステップ関数
  •  \boldsymbol{w} : 重みパラメータ 今回初期値は (1, 2) とする
  •  b : バイアス 今回初期値は 0 とする
\displaystyle{
f(x) = 
\begin{cases}
            1 \quad x \geqq 0 \\
            0 \quad x < 0 \\
\end{cases}
}

関数

  • predict

    • 推論を行う関数
    • 入力:  \boldsymbol{x_p}, (\boldsymbol{w}, b)
    • 出力:  y_p 推論結果
    • 処理は以下

    
y_p = f(b + \boldsymbol{x_p} \cdot \boldsymbol{w})

  • fit

    • 学習を行う関数
    • 入力:  \boldsymbol{x}, \boldsymbol{t}, lr, (\boldsymbol{w}, b)
    • 出力:  \boldsymbol{w}, b 更新後の重み, バイアス
    • 処理は以下をすべての \boldsymbol{x_p} について  \boldsymbol{w}, b が変化しなくなるまで繰り返す
\displaystyle{
\begin{align}
    \boldsymbol{w}^{(new)} &= \boldsymbol{w} - lr \cdot (y_p - t_p) \cdot \boldsymbol{x_p} \\
    b^{(new)} &= b - lr \cdot (y_p - t_p)
\end{align}
}

実装編

さて, 理論編ではいろいろごちゃごちゃありましたが, ここからは理論編にて作成された設計をもとに実装するだけです. ざっくりまとめるとしたの感じです.

f:id:ougarairin:20200429180751p:plain

これをもとに実装したものが以下です. 簡単にするため numpy を使用しています.

import numpy as np
class Perceptron:
    def __init__(self):
        self.w = np.array([1, 2])
        self.b = 0
        self.f = self.step_function

    def predict(self, x_p):
        return self.f(self.b + np.dot(x_p, self.w))

    def fit(self, x, t, lr):
        w_old, b_old = None, None
        while not ((w_old == self.w).all() and (b_old == self.b).all()):
            w_old, b_old = self.w, self.b
            for x_p, t_p in zip(x, t):
                y_p = self.predict(x_p)
                self.w = self.w - lr * (y_p - t_p) * x_p
                self.b = self.b - lr * (y_p - t_p)
        else:
            return (self.w, self.b)

    def step_function(self, x):
        if x >= 0:
            return 1
        else:
            return 0

ほぼ数式通りですが一応簡単に解説をします.

__init__() では, 重み, バイアスを初期化し, 使用する活性化関数をセットします. predict() では, 入力データを一つ受け取りその推論結果を返します. これの計算には内積を使用しています. fit() では, 入力データと教師データ, 学習係数を受け取り, 学習によって重みとバイアスを更新し, 更新した重みとバイアスを返します. 重みとバイアスの更新はこれらが変化しなくなるまで行います. step_function() では, ステップ関数を実装しています.

これを論理回路のAND演算にてテストしてみます. このクラスの使い方は以下のとおりです.

# 入力データ
x = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
# 教師データ
t = np.array([0, 0, 0, 1])
# モデル
model = Perceptron()

# 学習する前に一度推論してみる
print("学習前")
for x_p, t_p in zip(x, t):
    y_p = model.predict(x_p)
    print(f"x_p: {x_p}, y_p: {y_p}, t_p: {t_p}")
print(model.w, model.b)
print("*"*10)

# 学習
model.fit(x, t, 0.01)

# 学習後に推論を実行
print("学習後")
for x_p, t_p in zip(x, t):
    y_p = model.predict(x_p)
    print(f"x_p: {x_p}, y_p: {y_p}, t_p: {t_p}")
print(model.w, model.b)

実行結果は以下のとおりです.

学習前
x_p: [0 0], y_p: 1, t_p: 0
x_p: [0 1], y_p: 1, t_p: 0
x_p: [1 0], y_p: 1, t_p: 0
x_p: [1 1], y_p: 1, t_p: 1
[1 2] 0
**********
学習後
x_p: [0 0], y_p: 0, t_p: 0
x_p: [0 1], y_p: 0, t_p: 0
x_p: [1 0], y_p: 0, t_p: 0
x_p: [1 1], y_p: 1, t_p: 1
[0.67 1.17] -1.1700000000000008

学習前はすべての推論結果が「1」であったのに対し, 学習後は適切に推論できています. ひとまずよさそうです.

次はOR演算です. 先ほどのテストプログラムにて, 教師データを変更して実行します.

学習前
x_p: [0 0], y_p: 1, t_p: 0
x_p: [0 1], y_p: 1, t_p: 1
x_p: [1 0], y_p: 1, t_p: 1
x_p: [1 1], y_p: 1, t_p: 1
[1 2] 0
**********
学習後
x_p: [0 0], y_p: 0, t_p: 0
x_p: [0 1], y_p: 1, t_p: 1
x_p: [1 0], y_p: 1, t_p: 1
x_p: [1 1], y_p: 1, t_p: 1
[1. 2.] -0.01

よさそうです.

まとめ

パーセプトロンについて, 設計から実装まで行うことが出来ました. 非常に単純なコードで実装できることも確認できました.

今後の課題

XOR演算を行った場合, 果たしてどのような結果を得ることとなるでしょうか?

おまけ

はてなブログTexを書く方法

ano3.hatenablog.com