1. 程式人生 > >教程 | 僅需六步,從零實現機器學習演算法!

教程 | 僅需六步,從零實現機器學習演算法!

3376dd2b0db69c1ad19cb7573dc731fc2294086e

從頭開始寫機器學習演算法能夠獲得很多經驗。當你最終完成時,你會驚喜萬分,而且你明白這背後究竟發生了什麼。

有些演算法比較複雜,我們不從簡單的演算法開始,而是要從非常簡單的演算法開始,比如單層感知器。

本文以感知器為例,通過以下 6 個步驟引導你從頭開始寫演算法:

 ●  對演算法有基本的瞭解
 ●  找到不同的學習資源
 ●  將演算法分解成塊
 ●  從簡單的例子開始
 ●  用可信的實現進行驗證
 ●  寫下你的過程

基本瞭解

不瞭解基礎知識,就無法從頭開始處理演算法。至少,你要能回答下列問題:

 ●  它是什麼?
 ●  它一般用在什麼地方?
 ●  什麼時候不能用它?

就感知器而言,這些問題的答案如下:

 ●  單層感知器是最基礎的神經網路,一般用於二分類問題(1 或 0,「是」或「否」)。
 ●  它可以應用在一些簡單的地方,比如情感分析(積極反應或消極反應)、貸款違約預測(「會違約」,「不會違約」)。在這兩種情況中,決策邊界都是線性的。

 ●  當決策邊界是非線性的時候不能使用感知器,要用不同的方法。

714c5bb70524dfd340bafaa0124cedb2b871a1cc

藉助不同的學習資源

在對模型有了基本瞭解之後,就可以開始研究了。有人用教科書學得更好,而有人用視訊學得更好。就我而言,我喜歡到處轉轉,用各種各樣的資源學習。

如果是學數學細節的話,書的效果很好(參見:https://www.dataoptimal.com/data-science-books-2018/),但對於更實際的例子,我更推薦部落格和 YouTube 視訊。

以下列舉了一些關於感知器不錯的資源:

 ●  《統計學習基礎》(The Elements of Statistical Learning),第 4.5.1 節(https://web.stanford.edu/~hastie/Papers/ESLII.pdf)

 ●  《深入理解機器學習:從原理到演算法》,第 21.4 節(https://www.cs.huji.ac.il/~shais/UnderstandingMachineLearning/understanding-machine-learning-theory-algorithms.pdf)

部落格

 ●  Jason Brownlee 寫的《如何用 Python 從零開始實現感知器演算法》(https://machinelearningmastery.com/implement-perceptron-algorithm-scratch-python/)
 ●  Sebastian Raschka 寫的《單層神經網路和梯度下降》(https://sebastianraschka.com/Articles/2015_singlelayer_neurons.html)

視訊

 ●  感知器訓練(https://www.youtube.com/watch?v=5g0TPrxKK6o)
 ●  感知器演算法的工作原理(https://www.youtube.com/watch?v=1XkjVl-j8MM)

將演算法分解成塊

現在我們已經收集好了資料,是時候開始學習了。與其從頭讀一個章節或者一篇部落格,不如先瀏覽章節標題和其他重要資訊。寫下要點,並試著概述演算法。

在看過這些資料之後,我將感知器分成下列 5 個模組:

 ●  初始化權重
 ●  將輸入和權重相乘之後再求和
 ●  比較上述結果和閾值,計算輸出(1 或 0)
 ●  更新權重
 ●  重複

接下來我們詳細敘述每一個模組的內容。

1. 初始化權重

首先,我們要初始化權重向量。

權重數量要和特徵數量相同。假設我們有三個特徵,權重向量如下圖所示。權重向量一般會初始化為 0,此例中將一直採用該初始化值。

5df5461d7d3724325b9d1610ede2b8c9b547dcc7

2. 輸入和權重相乘再求和

接下來,我們就要將輸入和權重相乘,再對其求和。為了更易於理解,我給第一行中的權重及其對應特徵塗上了顏色。

bc0439956e285ddf7352a0db841bd17eda723ee3

在我們將特徵和權重相乘之後,對乘積求和。一般將其稱為點積。

6c49bbd182c622a0e0b446fd1211c53d9df1cd0f

最終結果是 0,此時用「f」表示這個暫時的結果。

3. 和閾值比較

計算出點積後,我們要將它和閾值進行比較。我將閾值定為 0,你可以用這個閾值,也可以試一下其他值。

7073062827661a1694b5ed8850f94df44cc0fb6f

由於之前計算出的點積「f」為 0,不比閾值 0 大,因此估計值也等於 0。

將估計值標記為「y hat」,y hat 的下標 0 對應的是第一行。當然你也可以用 1 表示第一行,這無關緊要,我選擇從 0 開始。

如果將這個結果和真值比較的話,可以看出我們當前的權重沒有正確地預測出真實的輸出。

2bdbdac1ea02bba718cae784004febb58071a7e1

由於我們的預測錯了,因此要更新權重,這就要進行下一步了。

4. 更新權重

我們要用到下面的等式:

636be0afe5b8bb1bd571f67a76a00f1b148e42e3

基本思想是在迭代「n」時調整當前權重,這樣我們將在下一次迭代「n+1」時得到新權重。

為了調整權重,我們需要設定「學習率」,用希臘字母「eta(η)」標記。我將學習率設為 0.1,當然就像閾值一樣,你也可以用不同的數值。

目前本教程主要介紹了:

65cbce46a4eb593969d819558b3e483ee4d67c1c

現在我們要繼續計算迭代 n=2 時的新權重了。

689c38ecd94f73b282caba0d18072de82e8e63f2

我們成功完成了感知器演算法的第一次迭代。

5. 重複

由於我們的演算法沒能計算出正確的輸出,因此還要繼續。

一般需要進行大量的迭代。遍歷資料集中的每一行,每一次迭代都要更新權重。一般將完整遍歷一次資料集稱為一個「epoch」。

我們的資料集有 3 行,因此如果要完成 1 個 epoch 需要經歷 3 次迭代。我們也可以設定迭代總數或 epoch 數來執行演算法,比如指定 30 次迭代(或 10 個 epoch)。與閾值和學習率一樣,epoch 也是可以隨意使用的引數。

在下一次迭代中,我們將使用第二行特徵。

63c99776530c1b8d53331da406ae39721ab8f5fa

此處不再重複計算過程,下圖給出了下一個點積的計算:

65cdd64ec1b09db825d7450df5ec1b4e772d40ce

接著就可以比較該點積和閾值來計算新的估計值、更新權重,然後再繼續。如果我們的資料是線性可分的,那麼感知器最終將會收斂。

從簡單的例子開始

我們已經將演算法分解成塊了,接下來就可以開始用程式碼實現它了。

簡單起見,我一般會以非常小的「玩具資料集」開始。對這類問題而言,有一個很好的小型線性可分資料集,它就是與非門(NAND gate)。這是數位電路中一種常見的邏輯閘。

cf7cfa713f1f11fa95882d81e36fb840c30da81f

由於這個資料集很小,我們可以手動將其輸入到 Python 中。我添加了一列值為 1 的虛擬特徵(dummy feature)「x0」,這樣模型就可以計算偏置項了。你可以將偏置項視為可以促使模型正確分類的截距項。

以下是輸入資料的程式碼:


# Importing libraries # NAND Gate # Note: x0 is a dummy variable for the bias term # x0 x1 x2 x = [[1., 0., 0.], [1., 0., 1.], [1., 1., 0.], [1., 1., 1.]]
y =[1., 1., 1., 0.]

與前面的章節一樣,我將逐步完成演算法、編寫程式碼並對其進行測試。

1. 初始化權重

第一步是初始化權重。


# Initialize the weights import numpy as np w = np.zeros(len(x[0]))

Out: [ 0. 0. 0.]

注意權重向量的長度要和特徵長度相匹配。以 NAND 門為例,它的長度是 3。

2. 將權重和輸入相乘並對其求和

我們可以用 Numpy 輕鬆執行該運算,要用的方法是 .dot()。

從權重向量和第一行特徵的點積開始。


# Dot Product f = np.dot(w, x[0]) print f

Out: 0.0

如我們所料,結果是 0。為了與前面的筆記保持連貫性,設點積為變數「f」。

3. 與閾值相比較

為了與前文保持連貫,將閾值「z」設為 0。若點積「f」大於 0,則預測值為 1,否則,預測值為 0。將預測值設為變數 yhat。


# Activation Function z = 0.0 if f > z: yhat = 1. else: yhat = 0.
print yhat

Out: 0.0

正如我們所料,預測值是 0。

你可能注意到了在上文程式碼的註釋中,這一步被稱為「啟用函式」。這是對這部分內容的更正式的描述。

從 NAND 輸出的第一行可以看到實際值是 1。由於預測值是錯的,因此需要繼續更新權重。

4. 更新權重

現在已經做出了預測,我們準備更新權重。


# Update the weights eta = 0.1 w[0] = w[0] + eta*(y[0] - yhat)*x[0][0] w[1] = w[1] + eta*(y[0] - yhat)*x[0][1] w[2] = w[2] + eta*(y[0] - yhat)*x[0][2]
print w

Out: [ 0.1 0. 0. ]

要像前文那樣設定學習率。為與前文保持一致,將學習率 η 的值設為 0.1。為了便於閱讀,我將對每次權重的更新進行硬編碼。

權重更新完成。

5. 重複

現在我們完成了每一個步驟,接下來就可以把它們組合在一起了。

我們尚未討論的最後一步是損失函式,我們需要將其最小化,它在本例中是誤差項平方和。

d9acff934797ea6a514a8cc8a11a542b9abe8636

我們要用它來計算誤差,然後看模型的效能。

把它們都放在一起,就是完整的函式:


import numpy as np

# Perceptron function def perceptron(x, y, z, eta, t): ''' Input Parameters: x: data set of input features y: actual outputs z: activation function threshold eta: learning rate t: number of iterations '''
# initializing the weights w = np.zeros(len(x[0])) n = 0
# initializing additional parameters to compute sum-of-squared errors yhat_vec = np.ones(len(y)) # vector for predictions errors = np.ones(len(y)) # vector for errors (actual - predictions) J = [] # vector for the SSE cost function
while n < t: for i in xrange(0, len(x)): # dot product f = np.dot(x[i], w) # activation function if f >= z: yhat = 1. else: yhat = 0. yhat_vec[i] = yhat
# updating the weights for j in xrange(0, len(w)): w[j] = w[j] + eta*(y[i]-yhat)*x[i][j]
n += 1 # computing the sum-of-squared errors for i in xrange(0,len(y)): errors[i] = (y[i]-yhat_vec[i])**2 J.append(0.5*np.sum(errors))
return w, J

現在已經編寫了完整的感知器程式碼,接著是執行程式碼:


# x0 x1 x2 x = [[1., 0., 0.], [1., 0., 1.], [1., 1., 0.], [1., 1., 1.]]
y =[1., 1., 1., 0.]
z = 0.0 eta = 0.1 t = 50
print "The weights are:" print perceptron(x, y, z, eta, t)[0]
print "The errors are:" print perceptron(x, y, z, eta, t)[0]

Out: The weights are: [ 0.2 -0.2 -0.1] The errors are: [0.5, 1.5, 1.5, 1.0, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]

我們可以看到,第 6 次迭代時誤差趨近於 0,且在剩餘迭代中誤差一直是 0。當誤差趨近於 0 並保持為 0 時,模型就收斂了。這告訴我們模型已經正確「學習」了適當的權重。

下一部分,我們將用計算好的權重在更大的資料集上進行預測。

用可信的實現進行驗證

到目前為止,我們已經找到了不同的學習資源、手動完成了演算法,並用簡單的例子測試了演算法。

現在要用可信的實現和我們的模型進行比較了。

我們使用的是 scikit-learn 中的感知器:http://scikit-learn.org/stable/modules/generated/sklearn.linear_model.Perceptron.html。

我們將按照以下幾步進行比較:

 ●  匯入資料
 ●  將資料分割為訓練集和測試集
 ●  訓練感知器
 ●  測試感知器
 ●  和 scikit-learn 感知器進行比較

1. 匯入資料

首先匯入資料。你可以在這裡(https://github.com/dataoptimal/posts/blob/master/algorithms from scratch/dataset.csv)得到資料集的副本。這是我建立的線性可分資料集,確保感知器可以起作用。為了確認,我們還將資料繪製成圖。

從圖中很容易看出來,我們可以用一條直線將資料分開。


import pandas as pd import numpy as np import matplotlib.pyplot as plt
df = pd.read_csv("dataset.csv") plt.scatter(df.values[:,1], df.values[:,2], c = df['3'], alpha=0.8)