[ML筆記]Rossenblatt 感知器

Rossenblatt 感知器(Rossenblatt Perceptron)

人造神經元

想像一個生物的神經元(neuron),他有一個輸入端和一個輸出端,化學反應訊號透過不同的樹突(dendrites)由別的細胞接收而來,經過細胞核、由軸突(axon)末端傳送到另外一個神經元(可參見 McCulloch-Pitts neuron)。那如果我們把他想像成一個二元的邏輯閥,多個訊號由遠處傳送到這個神經元,神經元可以選擇要不要再傳送到別的神經元。試想,如果任何的化學訊號都要反應、傳送一次,我們走在空氣中撞到塵埃就會痛一次,所以神經元必須決定這件事情是不是該傳送下去。而它所決定的「要(T)跟不要(F)」就形成了一個分類(classification)方法。

感知器

換句話說,我們可以把這樣的問題想成一個分類的問題,這個訊號是要讓他通過(T)呢?還是不讓他通過(F)呢?就像這個樣本是該讓他分去這邊($+1$,正類別)呢?還是那邊($-1$,負類別)呢?

在這裡,我們定義一個激勵函數(activation function)$\phi (z)$,這個 $z$ 被稱作淨輸入(net input),是多個輸入的值(input) $\textbf{x}$ 和每個輸入值的權重(weight) $\textbf{w}$ 的線性組合(linear combination)。為什麼要是線性組合?那是因為一個神經元可以有很多個樹突接收不同的訊號,但每個訊號對於我們要做的決定「份量」不一定都一樣重要。

舉「我等一下要吃什麼」當例子,雖然「今天是雨是晴」可能會跟我等一下要吃什麼有關係,但比起「我中午已經吃什麼」而言,看起來似乎就沒那麼重要了,所以後者在這個淨輸入中所佔的權重會大於前者。

數學推導

\begin{equation} z = w_{1}x_{1} + w_{2}x_{2}+ \dots + w_{n}x_{n}=\textbf{w}^{\mathrm{T}}\cdot\textbf{x} \end{equation}

其中,可以用矩陣表達:

\begin{equation} \textbf{w} = \left(\begin{array}{ccc}w_{1} \\ w_{2} \\ \vdots \\ w_{n}\end{array}\right) , \textbf{x} = \left(\begin{array}{ccc}x_{1} \\ x_{2} \\ \vdots \\ x_{n}\end{array}\right) \end{equation}

而這個激勵函數 $\phi (z)$,我們定義他當我們的淨輸入 $z$ 瀕臨某個臨界值時,例如 $\theta$,我們會把這個輸入訊號劃分到正類別($+1$),反之,則把他會分到負類別($-1$),也就是說這個函數可以從輸入的值大小判斷要把他分到哪裡去。也就是說,

\begin{equation} \phi (z) = \phi (\textbf{w}^\mathrm{T} \cdot \textbf{x}) = \phi (w_{1}x_{1}+w_{2}x_{2}+ \dots +w_{n}x_{n}) = \left\{ \begin{array}{ll} +1 & {z \geq \theta}\\ -1 & \textrm{otherwise}\\ \end{array} \right. \end{equation}

其實Rossenblatt 感知器的激勵函數就是單位階梯函數(unit step function),對,就是拉普拉斯轉換的那個 $u(\cdot)$

跟腦內神經元決定要不要「傳送訊息」的概念一樣,Rossenblatt 感知器也是一種判斷是否「啟動分類」的東西。而他的迭代(iteration)的原則就是:

  • 把權重先預設為零,或是一個很小小的隨機數字。
  • 迭代出所有給定的樣本 $x_{i}$,計算輸出值 $\hat{y}$,並更新一次權重。

這裡的輸出(output)$\hat{y}$,是指分出來的類別(也就是正類別或是負類別),之所以給他袋上帽子(hat),是因為這是我們預計出來的值而非真正的值(統計學常常這樣用,因為真正的值是 $y$)。而每次對於權重的更新方法是:

\begin{equation} w_{i}:=w_{i}+\Delta w_{i} \end{equation}

這裡(4)的 $:=$ 就是 assign(指派) 的意思,就是把等號右邊的值更新到等號左邊(就是大部份程式語言裡 = 的功能),那這個 $\Delta w_{j}$ 又是怎麼得到的呢?

\begin{equation} \Delta w_{i} = \eta (y_{i}-\hat{y_{i}})x_{i} \end{equation}

中間的 $(y_{j}-\hat{y_{j}})$ 就是每次計算出的類別($\hat{y_{j}}$)和實際類別($y_{j}$)的誤差,而$\eta$則是學習速率,介於零與一之間,我們可以控制他每次應該要改變多少權重,如果要敏感一點就調高這個值,反之則調低。有了上面的通式(5)我們很快可以推到:

\begin{equation} \Delta w_{1} = \eta (y_{1}-\hat{y_{1}})x_{1}\\ \Delta w_{2} = \eta (y_{2}-\hat{y_{2}})x_{2}\\ \vdots \end{equation}

舉例而言,我們試算「分類正確」下的結果,會發現只要分類正確,則權重不會更新。

\begin{equation} \textrm{都是負類別的情況} :\Delta w_{i} = \eta (({-1})-({-1}))x_{i}=0\\ \textrm{都是正類別的情況} :\Delta w_{j} = \eta (1-1)x_{2}=0\\ \end{equation}

而「分類錯誤」的話呢?我們會發現權重會被更新往正確類別的方向:

\begin{equation} \textrm{應是正類別卻分到負類別的情況,往正類別移動} :\Delta w_{i} = \eta ({1}-({-1}))x_{i}=\eta(2)x_{i}\\ \textrm{應是負類別卻分到正類別的情況,往負類別移動} :\Delta w_{j} = \eta (1-(-1))x_{2}=\eta(-2)x_{j}\\ \end{equation}

接著帶入其他值試試看,我們假定學習速率 $\eta = 1$$x_{i}=0.5$,分類器誤把正類別分到負類別,即 $y_{i}=+1$$\hat{y_i}={-1}$,則根據通式權重會增加 1 ,以保證以後如果以後又出現,會正確的分類到正類別($+1$)。

\begin{equation} \Delta w_{i} = \eta (y_{i}-\hat{y_{i}})x_{i} = 1 \cdot (1-(-1)) \cdot 0.5 = 1\\ \end{equation}

若今天的 $x_{i}=2$ 呢?我們會發現權重更新的更快,代表邊界現更大幅度的移動了,因為權重的更新速度和 $x_{i}$ 呈正比,以保證下次能正確的分類。

\begin{equation} \Delta w_{i} = \eta (y_{i}-\hat{y_{i}})x_{i} = 1 \cdot (1-(-1)) \cdot 2 = 4\\ \end{equation}

雛型

好了,知道背後的數學意義之後,我們來寫一個 Rossenblatt 感知器的雛型ㄅ。

class Rossenblatt_Perceptron(object):
	def __init__(self, eta, n_iteration):
		self.eta = eta # 學習速率,介於 0 與 1
		self.n_iteration = n_iteration # 迭代次數,為了避免線性不可分永遠的不到解,設置一個上限
		# 初始化這個感知器class
		
	def fit(self,X,y):
		"""
		這邊定義一下X和y啊
		X(矩陣用大寫):一個 nxn 二維的array,訓練用的,[n_sample(就是x_{i}),n_feature(就是y_{i})]
		y(清單用小寫):一個 n 一維的list,目標[n_sample]
		"""
		slef.weight_ = np.zeros(1 + x.shape[1]) # 權重初始化成一個零向量
		self.errors_ = [] 
		# 每次迭代中,「分類錯誤」的樣本我們把他集中在一起,最後可以用來判斷這個感知器的精準度
        for ct in range(self.n_iteration): # 在限定的次數上限中迭代
            errors = 0
            for x_i, target in zip(X, y):
                update = self.eta * (target - self.perdict(x_i))
                self.weight_[1:] = self.weight_[1:] + update*x_i
                # 以上兩行就是(5)式,其實也可以寫成一行
                self.weight_[0] = self.weight_[0] + update
                # (4)式,更新權重
                errors = errors + int(update != 0)
            self.errors_.append(errors)
        return self
	
	def net_input(self, X):
		return np.dot(X, self.weight_[1:] + self.w[0])
		# 一開始說了激勵函數可以用x和w的矩陣表示之,所以這裡計算矩陣內積
        # a.dot(b) 和 np.dot(a,b) 和 sum(i*j for i,j in zip(a,b)) 是一樣的概念
		
	def predict(self, X):
		return np.where(self.net_input(X) >= 0, 1, -1)
		# 預測未知資料的類別,這個就是最前面的激勵函數
		# np.where的用法是(條件,滿足前面條件輸出的值,不滿足前面時輸出的值)

又要來分iris惹!

有了以上的感知器,就可以來分東西了。

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

df = pd.read_csv("./iris.data", header = None)

我們抓前 100 筆花的資料,其中有 50 個 setosa(山鳶尾)、50 個 versicolor(變色鳶尾)。我們讓 +1 代表變色鳶尾,讓 -1 代表山鳶尾,用第一行的花萼長度當作 y 軸、第三行的花瓣長度當作 x 軸,並合併成矩陣X。

然後把他畫出來。

# 抓出兩種花
y = df.iloc[0:100, 4].values # iloc 取位置
y = np.where(y == 'Iris-setosa', -1, 1) # np.where 用法上面提過了

# 取出花萼長度(第一行 = row 0)和花瓣長度(第三行 = row 2)
X = df.iloc[0:100, [0, 2]].values

# 畫出散佈圖
plt.scatter(X[:50, 0], X[:50, 1],
            color = 'red', marker = 'o', label = 'setosa')
plt.scatter(X[50:100, 0], X[50:100, 1],
            color = 'blue', marker = 'x', label = 'versicolor')
# 補上兩軸標籤
plt.xlabel('花瓣長度(公分))
plt.ylabel('花萼長度(公分)')
plt.legend(loc = 'upper left')
plt.show()

很明顯,這是一個線性可分的東西,可以看出分成兩坨東西在左上跟右下。所以我們可以嘗試使用我們的感知器了。

ppn = Rossenblatt_Perceptron(eta = 0.1, n_iteration = 10)
# 創立一個class

ppn.fit(X, y)
# 計算(1)(2)式

plt.plot(range(1, len(ppn.errors_) + 1), ppn.errors_, marker = 'o')
plt.ylabel('分類錯誤的數量')
plt.xlabel('迭代次數')
plt.show()

會發現到了第六次之後,就再也沒有分類錯誤的數量了,此時可以視為一個收斂。這個感知器成功的把東西分成兩邊了。但是怎麼分的?我們把這條分類的楚河漢界畫出來。

# 一個畫畫的東西
from matplotlib.colors import ListedColormap

def plot_decision_regions(X, y, classifier, resolution = 0.02):
    # 決定會用到的標記 maker 和符號 colors
    markers = ('s', 'x', 'o', '^', 'v')
    colors = ('red', 'blue', 'lightgreen', 'gray', 'cyan')
    cmap = ListedColormap(colors[:len(np.unique(y))])
    # np.unique() 會自動刪掉重複的東西,並且重新排列回傳

    # 為了不讓圖看起來很擠或是太散,決定畫布邊界的最大值和最小值
    # 不然如果太偏一邊,可能整張圖只有右下角有圖之類的
    x1_min = X[:, 0].min() - 1
    x1_max = X[:, 0].max() + 1
    x2_min = X[:, 1].min() - 1
    x2_max = X[:, 1].max() + 1

    # np.meshgrid() 生成二維的格子點
    xx1, xx2 = np.meshgrid(np.arange(x1_min, x1_max, resolution),
                           np.arange(x2_min, x2_max, resolution))
    # np.arange(1,2,0,2)會生成1, 1.2, 1.4, 1.6,1.8

    # 做出預測
    Z = classifier.predict(np.array([xx1.ravel(), xx2.ravel()]).T)
    Z = Z.reshape(xx1.shape)

    # 畫圖
    plt.contourf(xx1, xx2, Z, alpha=0.3, cmap = cmap)

    # 設置座標軸範圍
    plt.xlim(xx1.min(), xx1.max())
    plt.ylim(xx2.min(), xx2.max())
    for idx, cl in enumerate(np.unique(y)):
        plt.scatter(x=X[y == cl, 0], 
                    y=X[y == cl, 1],
                    alpha = 0.8, 
                    c = colors[idx],
                    marker = markers[idx], 
                    label = cl, 
                    edgecolor = 'black')

plot_decision_regions(X, y, classifier=ppn)
plt.xlabel('花瓣長度(公分)')
plt.ylabel('花萼長度(公分)')
plt.legend(loc = 'upper left')
plt.show()

應用上的限制

要注意的是,Rossenblatt 感知器的收斂前提(也就是最終得到一條分類邊界)是兩個類別必須是線性可分的,否則如果樣本們沒辦法透過一個線性決策進行線性劃分,上面權重的通式會不停的更新,永遠停不下來。