1. 程式人生 > 其它 >零基礎入門深度學習 | 第二章:線性單元和梯度下降

零基礎入門深度學習 | 第二章:線性單元和梯度下降

無論即將到來的是大資料時代還是人工智慧時代,亦或是傳統行業使用人工智慧在雲上處理大資料的時代,作為一個有理想有追求的程式設計師,不懂深度學習(Deep Learning)這個超熱的技術,會不會感覺馬上就out了? 現在救命稻草來了,《零基礎入門深度學習》系列文章旨在講幫助愛程式設計的你從零基礎達到入門級水平。零基礎意味著你不需要太多的數學知識,只要會寫程式就行了,沒錯,這是專門為程式設計師寫的文章。雖然文中會有很多公式你也許看不懂,但同時也會有更多的程式碼,程式設計師的你一定能看懂的(我周圍是一群狂熱的Clean Code程式設計師,所以我寫的程式碼也不會很差)。

文章列表

零基礎入門深度學習(1) - 感知器

零基礎入門深度學習(2) - 線性單元和梯度下降 零基礎入門深度學習(3) - 神經網路和反向傳播演算法 零基礎入門深度學習(4) - 卷積神經網路 零基礎入門深度學習(5) - 迴圈神經網路 零基礎入門深度學習(6) - 長短時記憶網路(LSTM) 零基礎入門深度學習(7) - 遞迴神經網路

往期回顧

在上一篇文章中,我們已經學會了編寫一個簡單的感知器,並用它來實現一個線性分類器。你應該還記得用來訓練感知器的『感知器規則』。然而,我們並沒有關心這個規則是怎麼得到的。本文通過介紹另外一種『感知器』,也就是『線性單元』,來說明關於機器學習一些基本的概念,比如模型、目標函式、優化演算法等等。這些概念對於所有的機器學習演算法來說都是通用的,掌握了這些概念,就掌握了機器學習的基本套路。

線性單元是啥

感知器有一個問題,當面對的資料集不是線性可分的時候,『感知器規則』可能無法收斂,這意味著我們永遠也無法完成一個感知器的訓練。為了解決這個問題,我們使用一個可導的線性函式來替代感知器的階躍函式,這種感知器就叫做線性單元。線性單元在面對線性不可分的資料集時,會收斂到一個最佳的近似上。

為了簡單起見,我們可以設定線性單元的啟用函式為:f(x)=x

這樣的線性單元如下圖所示:

對比此前我們講過的感知器

這樣替換了啟用函式f後,線性單元將返回一個實數值而不是0,1分類。因此線性單元用來解決迴歸問題而不是分類問題。

線性單元的模型

當我們說模型時,我們實際上在談論根據輸入x測輸出y演算法。比如,x可以是一個人的工作年限,y可以是他的月薪,我們可以用某種演算法來根據一個人的工作年限來預測他的收入。比如:

函式h(x)做假設,而w,b是它的引數。我們假設引數w=1000,引數b=500,如果一個人的工作年限是5年的話,我們的模型會預測他的月薪為:

你也許會說,這個模型太不靠譜了。是這樣的,因為我們考慮的因素太少了,僅僅包含了工作年限。如果考慮更多的因素,比如所處的行業、公司、職級等等,可能預測就會靠譜的多。我們把工作年限、行業、公司、職級這些資訊,稱之為特徵。對於一個工作了5年,在IT行業,百度工作,職級T6這樣的人,我們可以用這樣的一個特徵向量來表示他

X= (5, IT, 百度, T6)。

既然輸入X成了一個具備四個特徵的向量,相對應的,僅僅一個引數w不夠用了,我們應該使用4個引數w1,w2,w3,w4,每個特徵對應一個。這樣,我們的模型就變成

其中,X1對應工作年限,X2對應行業,X3對應公司,X4對應職級。

為了書寫和計算方便,我們可以令W0等於b,同時令W0對應於特徵X0,由於X0其實並不存在,我們可以令它的值永遠為1。也就是說:

這樣上面的式子就可以寫成:

我們還可以把上式寫成向量的形式:

長成這種樣子模型就叫做線性模型,因為輸出是輸入特徵X1,X2,X3......的線性組合。

監督學習和無監督學習

接下來,我們需要關心的是這個模型如何訓練,也就是引數W什麼值最合適。

機器學習有一類學習方法叫做監督學習,它是說為了訓練一個模型,我們要提供這樣一堆訓練樣本:每個訓練樣本既包括輸入特徵X,也包括對應的輸出y(y也叫做標記,label)。也就是說,我們要找到很多人,我們既知道他們的特徵(工作年限,行業...),也知道他們的收入。我們用這樣的樣本去訓練模型,讓模型既看到我們提出的每個問題(輸入特徵X),也看到對應問題的答案(標記y)。當模型看到足夠多的樣本之後,它就能總結出其中的一些規律。然後,就可以預測那些它沒看過的輸入所對應的答案了。

另外一類學習方法叫做無監督學習,這種方法的訓練樣本中只有X沒有y。模型可以總結出特徵X一些規律,但是無法知道其對應的答案y。

很多時候,既有X又有y的訓練樣本是很少的,大部分樣本都只有X。比如在語音到文字(STT)的識別任務中,X是語音,y是這段語音對應的文字。我們很容易獲取大量的語音錄音,然而把語音一段一段切分好並標註上對應文字則是非常費力氣的事情。這種情況下,為了彌補帶標註樣本的不足,我們可以用無監督學習方法先做一些聚類,讓模型總結出哪些音節是相似的,然後再用少量的帶標註的訓練樣本,告訴模型其中一些音節對應的文字。這樣模型就可以把相似的音節都對應到相應文字上,完成模型的訓練。

線性單元的目標函式

現在,讓我們只考慮監督學習。

在監督學習下,對於一個樣本,我們知道它的特徵X,以及標記y。同時,我們還可以根據模型h(x)算得到輸出overline y 。注意這裡面我們用y表示訓練樣本里面的標記,也就是實際值;用帶上劃線的overline y 表示模型計算的出來的預測值。我們當然希望模型計算出來的y和overline y 越接近越好。

數學上有很多方法來表示的overline y 和y的接近程度,比如我們可以用overline y 和y的接近程度,比如我們可以用overline y 和y的差的平方的1/2來表示它們的接近程度:

我們把e叫做單個樣本的誤差。至於為什麼前面要乘1/2,是為了後面計算方便。

訓練資料中會有很多樣本,比如N個,我們可以用訓練資料中所有樣本的誤差的和,來表示模型的誤差E,也就是

上式的e^{(1)} 表示第一個樣本的誤差,e^{(2)} 表示第二個樣本的誤差......。

我們還可以把上面的式子寫成和式的形式。使用和式,不光書寫起來簡單,逼格也跟著暴漲,一舉兩得。所以一定要寫成下面這樣:

其中

(式2)中,x^{(i)} 表示第i個訓練樣本的特徵,y^{(i)} 表示第i個樣本的標記,我們也可以用元組(x^{(i)},y^{(i)}) 表示第i個練樣本。overline y^{(i)} 是模型對第i個樣本的預測值。

我們當然希望對於一個訓練資料集來說,誤差最小越好,也就是(式2)的值越小越好。對於特定的訓練資料集來說,(x^{(i)},y^{(i)}) 值都是已知的,所以(式2)其實是引數W的函式。

由此可見,模型的訓練,實際上就是求取到合適W,使(式2)取得最小值。這在數學上稱作優化問題,而E(w)是我們優化的目標,稱之為目標函式。

梯度下降優化演算法

大學時我們學過怎樣求函式的極值。函式y=f(x)的極值點,就是它的導數

那個點。因此我們可以通過解方程

求得函式的極值點(X0,y0)。

不過對於計算機來說,它可不會解方程。但是它可以憑藉強大的計算能力,一步一步的去把函式的極值點『試』出來。如下圖所示:

首先,我們隨便選擇一個點開始,比如上圖的X0。接下來,每次迭代修改X的為X1,X2,X3......經過數次迭代後最終達到函式最小值點。

你可能要問了,為啥每次修改x的值,都能往函式最小值那個方向前進呢?這裡的奧祕在於,我們每次都是向函式y=f(x)的梯度的相反方向來修改X。什麼是梯度呢?翻開大學高數課的課本,我們會發現梯度是一個向量,它指向函式值上升最快的方向。顯然,梯度的反方向當然就是函式值下降最快的方向了。我們每次沿著梯度相反方向去修改X的值,當然就能走到函式的最小值附近。之所以是最小值附近而不是最小值那個點,是因為我們每次移動的步長不會那麼恰到好處,有可能最後一次迭代走遠了越過了最小值那個點。步長的選擇是門手藝,如果選擇小了,那麼就會迭代很多輪才能走到最小值附近;如果選擇大了,那可能就會越過最小值很遠,收斂不到一個好的點上。

按照上面的討論,我們就可以寫出梯度下降演算法的公式

其中,bigtriangledown 是梯度運算元,bigtriangledown f(x)是指f(x)梯度。eta 是步長,也稱作學習速率。

對於上一節列出的目標函式(式2):

梯度下降演算法可以寫成:

聰明的你應該能想到,如果要求目標函式的最大值,那麼我們就應該用梯度上升演算法,它的引數修改規則是:

下面,請先做幾次深呼吸,讓你的大腦補充足夠的新鮮的氧氣,我們要來求取bigtriangledown E(w) 然後帶入上式,就能得到線性單元的引數修改規則。

關於bigtriangledown E(w) 推導過程,我單獨把它們放到一節中。您既可以選擇慢慢看,也可以選擇無視。在這裡,您只需要知道,經過一大串推導,目標函式E(W)梯度是:

因此,線性單元的引數修改規則最後是這個樣子

有了上面這個式子,我們就可以根據它來寫出訓練線性單元的程式碼了。

需要說明的是,如果每個樣本有M個特徵,則上式中的X,w都是M+1維向量(因為我們加上了一個恆為1的虛擬特徵x0,參考前面的內容),而y是標量。用高逼格的數學符號表示,就是:

為了讓您看明白說的是啥,我吐血寫下下面這個解釋(寫這種公式可累可累了)。因為w,x都是M+1維列向量,所以(式3)可以寫成:

如果您還是沒看明白,建議您也吐血再看一下大學時學過的《線性代數》吧。

bigtriangledown E(w)的推導

這一節你儘可以跳過它,並不太會影響到全文的理解。當然如果你非要弄明白每個細節,那恭喜你騷年,機器學習的未來一定是屬於你的。

首先,我們先做一個簡單的前戲。我們知道函式的梯度的定義就是它相對於各個變數的偏導數,所以我們寫下下面的式子

可接下來怎麼辦呢?我們知道和的導數等於導數的和,所以我們可以先把求和符號Sigma 裡面的導數求出來,然後再把它們加在一起就行了,也就是現在我們可以不管高大上的,先專心把裡面的導數求出來。

我們知道,y是與w無關的常數,而

下面我們根據鏈式求導法則來求導(上大學時好像叫複合函式求導法則):

我們分別計算上式等號右邊的兩個偏導數:

代入,我們求得Sigma 裡面的偏導數是:

最後代入bigtriangledown E(w) ,求得:

至此,大功告成。

隨機梯度下降演算法

如果我們根據(式3)來訓練模型,那麼我們每次更新w迭代,要遍歷訓練資料中所有的樣本進行計算,我們稱這種演算法叫做批梯度下降(Batch Gradient Descent)。如果我們的樣本非常大,比如數百萬到數億,那麼計算量異常巨大。因此,實用的演算法是SGD演算法。在SGD演算法中,每次更新w迭代,只計算一個樣本。這樣對於一個具有數百萬樣本的訓練資料,完成一次遍歷就會對w更新數百萬次,效率大大提升。由於樣本的噪音和隨機性,每次更新w不一定按照減少E的方向。然而,雖然存在一定隨機性,大量的更新總體上沿著減少E的方向前進的,因此最後也能收斂到最小值附近。下圖展示了SGD和BGD的區別

如上圖,橢圓表示的是函式值的等高線,橢圓中心是函式的最小值點。紅色是BGD的逼近曲線,而紫色是SGD的逼近曲線。我們可以看到BGD是一直向著最低點前進的,而SGD明顯躁動了許多,但總體上仍然是向最低點逼近的。

最後需要說明的是,SGD不僅僅效率高,而且隨機性有時候反而是好事。今天的目標函式是一個『凸函式』,沿著梯度反方向就能找到全域性唯一的最小值。然而對於非凸函式來說,存在許多區域性最小值。隨機性有助於我們逃離某些很糟糕的區域性最小值,從而獲得一個更好的模型。

實現線性單元

完整程式碼請參考GitHub: https://github.com/hanbt/learn_dl/blob/master/linear_unit.py (python2.7)

接下來,讓我們擼一把程式碼。

因為我們已經寫了感知器的程式碼,因此我們先比較一下感知器模型和線性單元模型,看看哪些程式碼能夠複用。

比較的結果令人震驚,原來除了啟用函式f同之外,兩者的模型和訓練規則是一樣的(在上表中,線性單元的優化演算法是SGD演算法)。那麼,我們只需要把感知器的啟用函式進行替換即可。感知器的程式碼請參考上一篇文章零基礎入門深度學習(1) - 感知器,這裡就不再重複了。對於一個養成良好習慣的程式設計師來說,重複程式碼是不可忍受的。大家應該把程式碼儲存在一個程式碼庫中(比如git)。

from perceptron import Perceptron
#定義啟用函式f
f = lambda x: x
class LinearUnit(Perceptron):
    def __init__(self, input_num):
        '''初始化線性單元,設定輸入引數的個數'''
        Perceptron.__init__(self, input_num, f)

通過繼承Perceptron,我們僅用幾行程式碼就實現了線性單元。這再次證明了面向物件程式設計正規化的強大。

接下來,我們用簡單的資料進行一下測試。

def get_training_dataset():
    '''
    捏造5個人的收入資料
    '''
    # 構建訓練資料
    # 輸入向量列表,每一項是工作年限
    input_vecs = [[5], [3], [8], [1.4], [10.1]]
    # 期望的輸出列表,月薪,注意要與輸入一一對應
    labels = [5500, 2300, 7600, 1800, 11400]
    return input_vecs, labels    
def train_linear_unit():
    '''
    使用資料訓練線性單元
    '''
    # 建立感知器,輸入引數的特徵數為1(工作年限)
    lu = LinearUnit(1)
    # 訓練,迭代10輪, 學習速率為0.01
    input_vecs, labels = get_training_dataset()
    lu.train(input_vecs, labels, 10, 0.01)
    #返回訓練好的線性單元
    return lu
if __name__ == '__main__': 
    '''訓練線性單元'''
    linear_unit = train_linear_unit()
    # 列印訓練獲得的權重
    print linear_unit
    # 測試
    print 'Work 3.4 years, monthly salary = %.2f' % linear_unit.predict([3.4])
    print 'Work 15 years, monthly salary = %.2f' % linear_unit.predict([15])
    print 'Work 1.5 years, monthly salary = %.2f' % linear_unit.predict([1.5])
    print 'Work 6.3 years, monthly salary = %.2f' % linear_unit.predict([6.3])

程式執行結果如下圖

擬合的直線如下圖

小結

事實上,一個機器學習演算法其實只有兩部分

1、模型 從輸入特徵測輸入那個函式目標函式h(x)

2、目標函式取最小(最大)值時所對應的引數值,就是模型的引數的最優值。很多時候我們只能獲得目標函式的區域性最小(最大)值,因此也只能得到模型引數的區域性最優值。

因此,如果你想最簡潔的介紹一個演算法,列出這兩個函式就行了。

接下來,你會用優化演算法去求取目標函式的最小(最大)值。[隨機]梯度{下降|上升}演算法就是一個優化演算法。針對同一個目標函式,不同的優化演算法會推匯出不同的訓練規則。我們後面還會講其它的優化演算法。

其實在機器學習中,演算法往往並不是關鍵,真正的關鍵之處在於選取特徵。選取特徵需要我們人類對問題的深刻理解,經驗、以及思考。而神經網路演算法的一個優勢,就在於它能夠自動學習到應該提取什麼特徵,從而使演算法不再那麼依賴人類,而這也是神經網路之所以吸引人的一個方面。

現在,經過漫長的燒腦,你已經具備了學習神經網路的必備知識。下一篇文章,我們將介紹本系列文章的主角:神經網路,以及用來訓練神經網路的大名鼎鼎的演算法:反向傳播演算法。至於現在,我們應該暫時忘記一切,盡情獎勵自己一下吧。

參考資料

1、Tom M. Mitchell, "機器學習", 曾華軍等譯, 機械工業出版社