選自Data Optimal

  機器之心編譯

  參與:李詩萌、路

  

  本文以感知器爲例,介紹了從零實現機器學習方法的具體步驟以及重要性。

  

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

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

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

  對算法有基本的瞭解

  找到不同的學習資源

  將算法分解成塊

  從簡單的例子開始

  用可信的實現進行驗證

  寫下你的過程

  基本瞭解

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

  它是什麼?

  它一般用在什麼地方?

  什麼時候不能用它?

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

  單層感知器是最基礎的神經網絡,一般用於二分類問題(1 或 0,「是」或「否」)。

  它可以應用在一些簡單的地方,比如情感分析(積極反應或消極反應)、貸款違約預測(「會違約」,「不會違約」)。在這兩種情況中,決策邊界都是線性的。

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

  藉助不同的學習資源

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

  如果是學數學細節的話,書的效果很好(參見: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,此例中將一直採用該初始化值。

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

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

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

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

  3. 和閾值比較

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

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

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

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

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

  4. 更新權重

  我們要用到下面的等式:

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

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

  目前本教程主要介紹了:

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

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

  5. 重複

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

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

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

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

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

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

  從簡單的例子開始

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

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

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

  以下是輸入數據的代碼:

  #Importinglibraries#NANDGate#Note:x0isadummyvariableforthebiasterm#x0x1x2x=[[1.,0.,0.],[1.,0.,1.],[1.,1.,0.],[1.,1.,1.]] y=[1.,1.,0.]

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

  1. 初始化權重

  第一步是初始化權重。

  #Initializetheweightsimportnumpyasnpw=np.zeros(len(x[0]))

  Out:[0.0.0.]

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

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

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

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

  #DotProductf=np.dot(w,x[0])printf

  Out:0.0

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

  3. 與閾值相比較

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

  #ActivationFunctionz=0.0iff>z:yhat=1.else:yhat=0. printyhat

  Out:0.0

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

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

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

  4. 更新權重

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

  #Updatetheweightseta=0.1w[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] printw

  Out:[0.10.0.]

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

  權重更新完成。

  5. 重複

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

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

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

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

  #Perceptronfunctiondefperceptron(x,y,z,eta,t):'''InputParameters:x:datasetofinputfeaturesy:actualoutputsz:activationfunctionthresholdeta:learningratet:numberofiterations #initializingtheweightsw=np.zeros(len(x[0]))n=0 #initializingadditionalparameterstocomputesum-of-squarederrorsyhat_vec=np.ones(len(y))#vectorforpredictionserrors=np.ones(len(y))#vectorforerrors(actual-predictions)J=[]#vectorfortheSSEcostfunction whilen=z:yhat=1.else:yhat=0.yhat_vec[i]=yhat #updatingtheweightsforjinxrange(0,len(w)):w[j]=w[j]+eta*(y[i]-yhat)*x[i][j] n+=1#computingthesum-of-squarederrorsforiinxrange(0,len(y)):errors[i]=(y[i]-yhat_vec[i])**2J.append(0.5*np.sum(errors)) returnw,J

  現在已經編寫了完整的感知器代碼,接着是運行代碼:

  z=0.0t=50 print"Theweightsare:"printperceptron(x,y,z,eta,t)[0] print"Theerrorsare:"

  Out:Theweightsare:[0.2-0.2-0.1]Theerrorsare:[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)得到數據集的副本。這是我創建的線性可分數據集,確保感知器可以起作用。爲了確認,我們還將數據繪製成圖。

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

  importpandasaspdimportmatplotlib.pyplotasplt df=pd.read_csv("dataset.csv")plt.scatter(df.values[:,1],df.values[:,2],c=df['3'],alpha=0.8)

  text

  在繼續之前,我先解釋一下繪圖的代碼。我用 Pandas 導入 csv,它可以自動將數據放入 DataFrame 中。爲了繪製數據,我要將值從 DataFrame 中取出來,因此我用了 .values 方法。特徵在第一列和第二列,因此我在散點圖函數中用了這些特徵。第 0 列是值爲 1 的虛擬特徵,這樣就能計算截距。這與上一節中的 NAND 門操作相似。最後,在散點圖函數中令 c = df['3'], alpha = 0.8 爲兩個類着色。輸出是第三列數據(0 或 1),所以我告訴函數用列「3」給這兩個類着色。

  你可以在此處(https://matplotlib.org/api/_as_gen/matplotlib.pyplot.scatter.html)找到更多關於 Matplotlib 散點圖函數的信息。

  2. 將數據分割成訓練集/測試集

  現在我們已經確定數據可線性分割,那麼是時候分割數據了。

  在與測試集不同的數據集上訓練模型是很好的做法,這有助於避免過擬合。還有不同的方法,但是簡單起見,我要用一個訓練集和一個測試集。首先打亂數據。

  df=df.values np.random.seed(5)np.random.shuffle(df)

  先將數據從 DataFrame 變爲 numpy 數組。這樣就可以更容易地使用 numpy 函數了,比如 .shuffle。爲了結果的可重複性,我設置了隨機種子 (5)。完成後,我試着改變隨機種子,並觀察結果會產生怎樣的變化。接下來,我將 70% 的數據分爲訓練集,將 30% 的數據作爲測試集。

  train=df[0:int(0.7*len(df))]test=df[int(0.7*len(df)):int(len(df))]

  最後一步是分離訓練集和測試集的特徵和輸出。

  x_train=train[:,0:3]y_train=train[:,3] x_test=test[:,0:3]y_test=test[:,3]

  我在這個例子中將 70% 的數據作爲訓練集,將 30% 的數據作爲測試集,你們可以研究 k 折交叉驗證等其他方法。

  3. 訓練感知器

  我們可以重複使用之前的章節中構建的代碼。

  defperceptron_train(x,y,z,eta,t): whilen=z: perceptron_train(x_train,y_train,z,eta,t)

  接下來看權重和誤差項平方和。

  w=perceptron_train(x_train,y_train,z,eta,t)[0]J=perceptron_train(x_train,y_train,z,eta,t)[1]printJ

  Out:[-0.5-0.298501220.35054929]

  現在權重對我們來說意義不大了,但是我們在測試感知器時還要再使用這些數值,以及用這些權重比較我們的模型和 scikit-learn 的模型。

  根據誤差項平方和可以看出,感知器已經收斂了,這是我們預料中的結果,因爲數據是線性可分的。

  4. 測試感知器

  現在是時候測試感知器了。我們要建立一個小的 perceptron_test 函數來測試模型。與前文類似,這個函數取我們之前用 perceptron_train 函數和特徵計算出的權重的點積以及激活函數進行預測。之前唯一沒見過的只有 accuracy_score,這是 scikit-learn 中的評估指標函數。

  將所有的這些放在一起,代碼如下:

  fromsklearn.metricsimportaccuracy_score defperceptron_test(x,w,z,eta,t):y_pred=[]foriinxrange(0,len(x-1)):f=np.dot(x[i],w) #activationfunctioniff>z:yhat=1else:yhat=0y_pred.append(yhat)returny_pred y_pred=perceptron_test(x_test,w,z,eta,t) print"Theaccuracyscoreis:"printaccuracy_score(y_test,y_pred)

  得分爲 1.0 表示我們的模型在所有的測試數據上都做出了正確的預測。因爲數據集明顯是可分的,所以結果正如我們所料。

  5. 和 scikit-learn 感知器進行比較

  最後一步是將我們的感知器和 scikit-learn 的感知器進行比較。下面的代碼是 scikit-learn 感知器的代碼:

  fromsklearn.linear_modelimportPerceptron #trainingthesklearnPerceptronclf=Perceptron(random_state=None,eta0=0.1,shuffle=False,fit_intercept=False)clf.fit(x_train,y_train)y_predict=clf.predict(x_test)

  現在我們已經訓練了模型,接下來要比較這個模型的權重和我們的模型計算出來的權重。

  Out:sklearnweights:myperceptronweights:

  scikit-learn 模型中的權重和我們模型的權重完全相同。這意味着我們的模型可以正確地工作,這是個好消息。

  在結束之前還有一些小問題。在 scikit-learn 模型中,我們將隨機狀態設置爲「None」而且沒有打亂數據。這是因爲我們已經設置了隨機種子,而且已經打亂過數據,不用再做一次。還需要將學習率 eta0 設置爲 0.1,和我們的模型相同。最後一點是截距。因爲我們已經設置了值爲 1 的虛擬特徵列,因此模型可以自動擬合截距,所以不必在 scikit-learn 感知器中打開它。

  這些看似都是小細節,但是如果不設置它們的話,我們的模型就無法重複得到相同的結果。這是重點。在使用模型之前,閱讀文檔並瞭解不同的設置有什麼作用非常重要。

  寫下你的過程

  這是該過程的最後一步,可能也是最重要的一步。

  你剛剛經歷了學習、做筆記、從頭開始寫算法以及用可信實現進行比較的流程。不要浪費這些努力!

  寫下過程原因有二:

  你要更深刻地理解這個過程,因爲你還要將你學到的東西教給別人。

  你要向潛在僱主展示這個過程。

  從機器學習庫中實現算法是一回事,從頭開始實現算法是另一回事,它會給人留下深刻印象。

  GitHub 個人資料是展示你所做工作的一種很好的方法。

  總結

  原文鏈接:https://www.dataoptimal.com/machine-learning-from-scratch/

  本文爲機器之心編譯,轉載請聯繫本公衆號獲得授權

  ------------------------------------------------

  加入機器之心(全職記者 / 實習生):[email protected]

  投稿或尋求報道:[email protected]

  廣告 & 商務合作:[email protected]

查看原文 >>
相關文章