1. 程式人生 > >感知機的 python 實現

感知機的 python 實現

本文的主要內容是感知機的python實現。在閱讀程式之前,如果對感知機的原理不瞭解,可以參考我的上一篇文章:感知機演算法原理(PLA原理)及 Python 實現

建立一些用於測試的線性可分資料

機器學習是資料驅動的學科,如果您在網路上很難找到線性可分的資料的話,不妨自己來“捏造一些”。順便提一下,因為我沒有給我的 ubuntu 上的 sublime 新增中文支援,無法輸入中文,所以註釋使用英文寫的。我會在程式碼中解釋。

首先,新建一個名為 pla.py 的檔案,將下面的程式碼新增進去,以匯入 numpy 科學計算模組。

from numpy import *

然後,我們來定義一個函式makeLinearSeparableData

以產生我們需要的線性可分的資料。將下面的程式碼新增到 pla.py 中:

def makeLinearSeparableData(weights, numLines):
    ''' (list, int) -> array

    Return a linear Separable data set. 
    Randomly generate numLines points on both sides of 
    the hyperplane weights * x = 0.

    Notice: weights and x are vectors.

    >>> data = pla.makeLinearSeparableData([2,3],5)
    >>> data
    array([[ 0.54686091,  3.60017244,  1.        ],
           [ 2.0201362 ,  7.5046425 ,  1.        ],
           [-3.14522458, -7.19333582, -1.        ],
           [ 9.72172678, -7.99611918, -1.        ],
           [ 9.68903615,  2.10184495,  1.        ]])
>>> 
data = pla.makeLinearSeparableData([4,3,2],10) >>> data array([[ -4.74893955e+00, -5.38593555e+00, 1.22988454e+00, -1.00000000e+00], [ 4.13768071e-01, -2.64984892e+00, -5.45073234e-03, -1.00000000e+00], [ -2.17918583e+00, -6.48560310e+00, -3.96546373e+00, -1.00000000e+00], [ -4.34244286e+00, 4.24327022e+00, -5.32551053e+00, -1.00000000e+00], [ -2.55826469e+00, 2.65490732e+00, -6.38022703e+00, -1.00000000e+00], [ -9.08136968e+00, 2.68875119e+00, -9.09804786e+00, -1.00000000e+00], [ -3.80332893e+00, 7.21070373e+00, -8.70106682e+00, -1.00000000e+00], [ -6.49790176e+00, -2.34409845e+00, 4.69422613e+00, -1.00000000e+00], [ -2.57471371e+00, -4.64746879e+00, -2.44909463e+00, -1.00000000e+00], [ -5.80930468e+00, -9.34624147e+00, 6.54159660e+00, -1.00000000e+00]]) '''
w = array(weights) numFeatures = len(weights) dataSet = zeros((numLines, numFeatures + 1)) for i in range(numLines): x = random.rand(1, numFeatures) * 20 - 10 innerProduct = sum(w * x) if innerProduct <= 0: dataSet[i] = append(x, -1) else: dataSet[i] = append(x, 1) return dataSet

程式碼解釋如下:

  1. weights 是一個列表,裡面儲存的是我們用來產生隨機資料的那條直線的法向量。
  2. numLines 是一個正整數,表示需要建立多少個數據點。
  3. numFeatures 是一個正整數,代表特徵的數量
  4. dataSet = zeros((numLines, numFeatures + 1)) 用於建立一個規模為numLines x (numFeatures + 1) 的陣列,且內容全為 0。注意:numFeatures + 1 是為了在最後一列可以儲存該資料點的分類(+1或者-1)。
  5. 然後我們在 for迴圈裡面填充 dataSet 的每一行。
  6. x = random.rand(1, numFeatures) * 20 - 10 產生一個數組,規模為一行,numFeatures 列, 每個數都是 -10 到 10 的隨機數。
  7. innerProduct = sum(w * x) 計算內積
  8. 接下來的 if 語句判斷如果內積小於等於 0,則是負例,否則是正例
  9. numpy 提供的 append 函式可以擴充一維陣列,可以自己實驗一下。
  10. 最後返回資料集合。

函式的 docstring裡面提供了使用例子,可以自己試一下,因為是隨機數,所以結果不會相同。

我們可以實驗一下。在Linux中,首先在pla.py所在目錄開啟 python 命令提示符,輸入如下圖所示命令:

這裡寫圖片描述

Windows中只需直接執行我們編寫的模組調,然後用函式 makeLinearSeparableData 即可。

將資料集視覺化

得到了隨機產生的資料集,當然要畫畫圖看看是不是真的是線性可分的。下面是繪製散點圖的程式碼。我們需要用到 matplotlib 模組。將如下程式碼新增到 pla.py 中:

def plotData(dataSet):
    ''' (array) -> figure

    Plot a figure of dataSet

    '''

    import matplotlib.pyplot as plt 
    fig = plt.figure()
    ax = fig.add_subplot(111)
    ax.set_title('Linear separable data set')
    plt.xlabel('X')
    plt.ylabel('Y')
    labels = array(dataSet[:,2])
    idx_1 = where(dataSet[:,2]==1)
    p1 = ax.scatter(dataSet[idx_1,0], dataSet[idx_1,1], marker='o', color='g', label=1, s=20)
    idx_2 = where(dataSet[:,2]==-1)
    p2 = ax.scatter(dataSet[idx_2,0], dataSet[idx_2,1], marker='x', color='r', label=2, s=20)
    plt.legend(loc = 'upper right')
    plt.show()

matplotlib 用起來比較複雜,但是能夠精確控制影象的顯示。注意程式碼中的 where 函式是用來找出正例的行的下標,然後我們可以把正例和反例用不同的顏色和形狀表示出來。如果有其他函式使用的問題,可以自行百度解決,很容易就能找到函式的用法。

下面我們來測試一下(注意,我們編寫的函式 plotData 只能繪製二維影象,所以我們需要產生只有兩個特徵的資料集。如果你想繪製三維影象,可以自己摸索一下)。因為在 pla.py 中添加了新程式碼,所以首先要重新載入我們的模組:
>>> reload(pla)
輸入如下命令以產生 100 個數據點:
>>> data = pla.makeLinearSeparableData([4,3],100)
然後輸入如下命令繪製散點圖(引數為我們產生的資料集):
>>> pla.plotData(data)
如圖所示:

這裡寫圖片描述

繪製的散點圖如下圖所示:
這裡寫圖片描述

可以看到,我們產生的隨機的資料集合是沒有問題的,是線性可分的。

訓練感知機,視覺化分類器及其法向量

已經有了線性可分的資料,接下來,我們就可訓練感知機了。將如下程式碼新增到 pla.py 中:

def train(dataSet, plot = False):
    ''' (array, boolean) -> list

    Use dataSet to train a perceptron
    dataSet has at least 2 lines.

    '''

    numLines = dataSet.shape[0]
    numFeatures = dataSet.shape[1]
    w = zeros((1, numFeatures - 1))         # initialize weights
    separated = False

    i = 0;
    while not separated and i < numLines:
        if dataSet[i][-1] * sum(w * dataSet[i,0:-1]) <= 0:
            w = w + dataSet[i][-1] * dataSet[i,0:-1]
            separated = False
            i = 0;
        else:
            i += 1

    if plot == True:
        import matplotlib.pyplot as plt
        from matplotlib.lines import Line2D
        fig = plt.figure()
        ax = fig.add_subplot(111)
        ax.set_title('Linear separable data set')
        plt.xlabel('X')
        plt.ylabel('Y')
        labels = array(dataSet[:,2])
        idx_1 = where(dataSet[:,2]==1)
        p1 = ax.scatter(dataSet[idx_1,0], dataSet[idx_1,1], 
            marker='o', color='g', label=1, s=20)
        idx_2 = where(dataSet[:,2]==-1)
        p2 = ax.scatter(dataSet[idx_2,0], dataSet[idx_2,1], 
            marker='x', color='r', label=2, s=20)
        x = w[0][0] / abs(w[0][0]) * 10
        y = w[0][1] / abs(w[0][0]) * 10
        ann = ax.annotate(u"",xy=(x,y), 
            xytext=(0,0),size=20, arrowprops=dict(arrowstyle="-|>"))
        ys = (-12 * (-w[0][0]) / w[0][1], 12 * (-w[0][0]) / w[0][1])
        ax.add_line(Line2D((-12, 12), ys, linewidth=1, color='blue'))
        plt.legend(loc = 'upper right')
        plt.show()

    return w

程式碼解釋:

  1. 該函式有兩個引數,地一個是資料集 dataSet,第二個是 plot,如果不提供值,有預設值 False,意思是隻返回最後的結果,不繪製散點圖。我這樣設計這個訓練函式,是為了方便檢視訓練完成後的結果。

  2. 首先獲得資料集的行數 numLines 和特徵的數目 numFeatures,減一是因為最後一列是資料點的分類標籤,分類標籤並不是特徵。

  3. 建立一個數組 w 儲存權重向量。

  4. while 迴圈只要滿足任何一個條件就結束:已經完全將正例和負例分開,或者 i 的值超過樣本的數量。其實第二個條件是不會發生的,因為感知機的訓練演算法是收斂的,所以一定會將資料完全分開,證明可見我的另一篇文章:感知機演算法原理(PLA原理)及 Python 實現,但前提是資料集必須是線性可分的。

  5. 下面的程式碼是訓練演算法的核心,即隨機梯度下降:

 if dataSet[i][-1] * sum(w * dataSet[i,0:-1]) <= 0: # 如果分類錯誤
            w = w + dataSet[i][-1] * dataSet[i,0:-1] # 更新權重向量
            separated = False # 設定為未完全分開
            i = 0; # 重新開始便利每個資料點
        else:
            i += 1 # 如果分類正確,檢查下一個資料點

簡單解釋一下繪圖部分的程式碼:

  1. 接下來的 if plot == True: 程式碼塊內的程式碼使用來繪製分類器及其法向量(權重向量)的。
  2. 需要解釋的地方是 w 是一個二位陣列,所以 w的第一個元素是 w[0][0],第二個元素是 w[0][1]
  3. x = w[0][0] / abs(w[0][0]) * 10y = w[0][1] / abs(w[0][0]) * 10 是為了避免求得的權重向量長度過大在散點圖中無法顯示,所以將它按比例縮小了。
  4. 如下的程式碼用來產生兩個點的 y 值,以繪製一條直線(感知機):
    ys = (-12 * (-w[0][0]) / w[0][1], 12 * (-w[0][0]) / w[0][1])
  5. annotate 函式用於繪製法向量(帶箭頭的直線)

下面我們來測試一下:

>>> data = pla.makeLinearSeparableData([4,3],100)
>>> w = pla.train(data)
>>> w
array([[ 16.32172416,  11.54429628]])

函式 train 正確返回了一個權重向量。在條用 train 函式時我並沒有給第二個引數 plot 賦值,所以預設不會繪製散點圖以及感知機。下面再來測試一下,並使第二個引數為 True

w = pla.train(data, True)

首先會顯示散點圖,關閉散點圖後,才會返回權重向量 w

這裡寫圖片描述

注意,如果看不見法向量(權重向量),可以使用左下角第四個按鈕拖動散點圖,如果發現法向量和分類器不是垂直的,是因為橫縱座標的比例不同,改變圖片視窗的尺寸調整為正方形即可。

由散點圖可以看出,我們的程式是正確的。另外需要注意的是我們產生資料用的權重向量 (3,4) 和我們訓練得到權重向量 (16.32172416,11.54429628) 是不成比例的。為什麼呢?因為隨機梯度下降得到一般不是最優解,但確實可行解。

以上就是 python 實現感知機的全部內容,如有錯誤,請批評指正,謝謝。

宣告: