機器學習 | 剖析感知器算法 & Python實現


前言:本系列博客參考於 《機器學習算法導論》和《Python機器學習

如有侵權,敬請諒解。本書盡量用總結性的語言重述本書內容,避免侵權。

上一篇已經初步介紹了機器學習相關知識,簡短介紹了機器學習的分類等等,本篇介紹其中監督學習中的分類領域下的感知器算法。


\[QAQ \]


本篇將循序漸進的實現一個感知器,並且通過訓練使其具備對鳶尾花數據集中數據進行分類的能力。

早期的機器學習

在詳細討論感知器和相關算法之前,先大體了解一下早期機器學習的起源。 為了理解大腦的工作原理以涉及人工智能系統,沃倫*麥卡洛克 和 沃爾特*皮茨 在 \(1943\)​ 年 神經元是大腦中相互連接的神經細胞,這些細胞可以處理和傳遞化學和電信號。

神經元示意圖 from 維基百科

麥卡洛克-皮茨 將神經細胞描述為一個具備二進制輸出的邏輯門。

樹突接收多個輸入信號,如果累加的信號超過某一閾(yu)值,經細胞體的整合就會生成一個輸出信號,並通過軸突進行傳遞。

正是基於以上 \(MCP\) 模型,感知器學習法則被提出來。

感知器

Description

$MCP$ 模型出現幾年后,弗蘭克*羅森布拉特提出了第一個感知器學習法則。 在此感知器規則中,羅森布拉特提出了一個自學習算法,此算法可以自動 在監督學習與分類中,類似算法可用於預測樣本所屬的類別。
更嚴謹的講,我們可以把這個問題看作一個二值分類,為了簡單起見,把兩類分別記為 $1$(正類別)和 $-1$(父類別)。 定義一個激活函數(activation function), 它以特定的輸入值x與相應的權值向量w的線性組合作為輸入。


轉化成公式長這個樣子:

\[\begin{array}{c} \phi(z)=w_{1} * x_{1}+w_{2} * x_{2}+\ldots+w_{m} * x_{m}=\sum_{m=0}^{m} w_{j} x_{j}=w^{T} x \\ w=\left\{\begin{array}{c} w_{1} \\ w_{2} \\ \cdots \\ w_{m} \end{array}\right\}, x=\left\{\begin{array}{c} x_{1} \\ x_{2} \\ \cdots \\ x_{m} \end{array}\right\} \end{array} \]

此時,對於一個特定樣本 \(x_i\) 的激活,如果其值大於預設的閾值 \(a\),我們將其划為 \(1\) 類,否則為 \(-1\) 類。在感知器算法中,激活函數(如下公式)

\[\phi(z)=w_{1} * x_{1}+w_{2} * x_{2}+\ldots+w_{m} * x_{m}=\sum_{m=0}^{m} w_{j} x_{j}=w^{T} x \]

\(\phi(z)\) 是一個簡單的分段函數

\[\phi(z)=\left\{\begin{array}{ll} 1, & \text { 若 } \mathrm{z}>=\mathrm{a} \\ -1, & \text { 其他 } \end{array}\right. \]

\(MCP\) 神經元和羅森布拉特閾值感知器的理念就是,

通過模擬的方式還原大腦中單個神經元的工作方式:他是否被激活。

總結的來說,羅森布拉特感知器最初的規則非常簡單,可總結如下幾步:

  1. 將權重初始化為 \(0\)​ 或者是一個極小的隨機數
  2. 迭代所有訓練樣本,執行以下操作:
    1. 根據以上公式,計算輸出值
    2. 更新權重

這里的輸出值是指通過前面定義的單位階躍函數預測得出的類標,而每次對權重向量中每一權重 \(w\) 的更新方式為:

\[w_{j}:=w_{j}+\Delta w_{j} \]

對於用於更新權重的值可以通過感知器學習規則計算獲得:

\[\Delta w_{j}=\eta\left(y^{(i)}-\hat{y}^{(i)}\right) x_{j}^{(i)} \]

其中 \(η\) 是學習速率(一個介於 \(0.0\)\(1.0\) 之間的常數)

\(y^{(i)}\) 是第 \(i\) 個樣本的真實類標(即真實值)

\(\hat{y}^{(i)}\) 是第 \(i\) 個樣本的預測類標(預測值)。需要注意的是,權重向量中的所有權重值是同時更新的,這意味着在所有的權重 \(\Delta w_{j}\) 更新前,我們無法重新計算 \(\hat{y}^{(i)}\)

具體的,對於一個二維數據集,可通過下世進行更新:

\[\begin{array}{c} \Delta w_{0}=\eta\left(y^{(i)}-\text { output }^{(i)}\right) \\ \Delta w_{1}=\eta\left(y^{(i)}-\text { output }^{(i)}\right) x_{1}^{(i)} \\ \Delta w_{2}=\eta\left(y^{(i)}-\text { output }^{(i)}\right) x_{2}^{(i)}\\ ... \end{array} \]


\[QAQ \]


接下來介紹感知器的內核(推導過程),體驗一下感知器規則的簡潔之美(TQL

  1. 對於如下式所示的兩種場景,若感知器對類標的預測正確,權重可不做更新:

    \[\begin{array}{l} \Delta w_{j}=\eta\left(-1^{(i)}-(-1)^{(i)}\right) x_{j}^{(i)}=0 \\ \Delta w_{j}=\eta\left(1^{(i)}-1^{(i)}\right) x_{j}^{(i)}=0 \end{array} \]

  2. 在類標預測錯誤的情況下,權重的值會分別趨向於正類別或者負類別的方向:

    \[\begin{array}{l} \Delta w_{j}=\eta\left(-1^{(i)}-1^{(i)}\right) x_{j}^{(i)}=-2 \eta x_{j}^{(i)} \\ \Delta w_{j}=\eta\left(1^{(i)}-(-1)^{(i)}\right) x_{j}^{(i)}=2 \eta x_{j}^{(i)} \end{array} \]

解釋:假定 \(x_{j}^{(i)}=0.5\) 且模型將此樣本錯誤的分類到了 \(-1\) 類別內。在此情況下,我們應將相應的權值增 \(1\) ,以保證下次遇到此樣本時使得激活函數:\(x_{j}^{(i)}=w_{j}^{(i)}\)

能將其更多的判定為正類別,這也相當於增大其值大於單位階躍函數閾值的概率,以使得樣本被判定為 \(+1\) 類:\(\Delta w_{j}^{(i)}=\left(1^{(i)}-(-1)^{(i)}\right) * 0.5^{(i)}=2 * 0.5=1\)

權重的更新與 \(x_{j}^{(i)}=0.5\) 成比例。例如另外一個樣本 \(x_{j}^{(i)}=2\)

被錯誤的分類到 \(-1\) 類別中,我們應更大幅度的移動決策邊界,以保證下次遇到此樣本時能正確分類。

\[\Delta w_{j}^{(i)}=\left(1^{(i)}-(-1)^{(i)}\right) * 2^{(i)}=2 * 2=4 \]

注意:感知器收斂的前提是兩個類別必須是線性可分的,且學習速率足夠小。

如果兩個類別無法通過一個線性決策邊界進行划分,可以為模型在訓練數據集上的學習迭代次數設置一個最大值,或者設置一個允許錯誤分類樣本數量的閾值,否則,感知器訓練算法將永遠不停的更新權值

下圖是感知器流程圖,很權威的一張圖。

Networking, Fundamental, Activities

上圖說明了感知器如何接收樣本 \(x\) 的輸入,並將其與權值 \(w\) 進行加權以計算凈輸入(net_input),進而凈輸入被傳遞到激活函數(在此為單位階躍函數),然后生成值為 \(+1\) 或者 \(-1\) 的二值輸出,並以其作為樣本的預測類標。在學習階段,此輸出用來計算預測的誤差並更新權重。


Python實現

上述已經深入講解感知器的規則,下面我們用代碼實現它。

我們封裝一個感知器類,對外提供訓練和預測接口。

具體細節可見注釋。

import numpy as np

class Perceptron(object):
    """
    Perceptron:感知器
        感知器收斂的前提是:兩個類別必須是線性可分的,且學習速率必須足夠小,否則感知器算法會永遠不停的更新權值
    """

    def __init__(self, eta=0.01, n_iter=10):
        """
        初始化感知器對象
        :param eta: float 學習速率
        :param n_iter: int 在訓練集進行迭代的次數
        """
        self.eta = eta
        self.n_iter = n_iter

    def net_input(self, xi):
        """
        計算凈輸入
        :param xi: list[np.array] 一維數組數據集
        :return: 計算向量的點積
            向量點積的概念:
                {1,2,3} * {4,5,6} = 1*4+2*5+3*6 = 32

        description:
            sum(i*j for i, j in zip(x, self.w_[1:])) python計算點積
        """
        print(xi, end=" ")
        print(self.w_[:], end=" ")
        x_dot = np.dot(xi, self.w_[1:]) + self.w_[0]
        print("的點積是:%d" % x_dot, end="  ")
        return x_dot

    """ 計算類標 """
    def predict(self, xi):
        """
        預測方法
        :param xi: list[np.array] 一維數組數據集
        :return:
        """
        target_pred = np.where(self.net_input(xi) >= 0.0, 1, -1)
        print("預測值:%d" % target_pred, end="; ")
        return target_pred

    def fit(self, x, y):
        """
        學習、訓練方法
        :param x: list[np.array] 一維數組數據集
        :param y: 被訓練的數據集的實際結果
        :return:
          權值,初始化為一個零向量R的(m+1)次方,m代表數據集中緯度(特征)的數量
          x.shape[1] = (100,2) 一百行2列:表示數據集中的列數即特征數

          np.zeros(count) 將指定數量count初始化成元素均為0的數組 self.w_ = [ 0.  0.  0.]
        """

        """
        按照python開發慣例,對於那些並非在初始化對象時創建但是又被對象中其他方法調用的屬性,可以在后面添加一個下划線.
        將權值初始化為一個零向量,x.shape[1] 是特征的維度數量,如鳶尾花數據 (150, 5)  self.w_ = [0,0,0,0,0,0]
        w_[0]是初始權重值0  w_[1:] 每次更新的權重值 
        """
        self.w_ = np.zeros(1 + x.shape[1])
        print(self.w_)
        # 收集每輪迭代過程中錯誤分類樣本的數量,以便后續對感知器在訓練中表現的好壞做出判定
        self.errors_ = []

        for _ in range(self.n_iter):
            errors = 0
            """
            迭代所有樣本,並根據感知器規則來更新權重
           """
            for x_element, target in zip(x, y):
                """ 如果預測值(self.predict(x_element))和實際值(target)一致,則update為0 """
                update = self.eta * (target - self.predict(x_element))
                print("真實值:%d" % target)
                self.w_[1:] += update * x_element
                self.w_[0] += update
                errors += int(update != 0.0)
            self.errors_.append(errors)
        return self

另外附上 《機器學習算法導論》書中的簡易代碼版本

import numpy as np

class Perceptron:
    def fit(self, X, y):
        # 學習、訓練方法
        m, n = X.shape
        w = np.zeros((n, 1))
        b = 0
        done = False
        while not done:
            done = True
            for i in range(m):
                x = X[i].reshape(1, -1)
                if y[i] * (x.dot(w) + b) <= 0:
                    w = w + y[i] * x.T
                    b = b + y[i]
                    done = False
        self.w = w
        self.b = b

    def predict(self, X):
        # 預測方法
        return np.sign(X.dot(self.w) + self.b)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM