AlphaGo在人機(jī)大戰(zhàn)中戰(zhàn)勝李世乭讓深度學(xué)習(xí)這個(gè)名詞在大眾口中廣為傳播,深度學(xué)習(xí)的強(qiáng)大,也讓其增加了一層神秘色彩,似乎這是名校博士才能理解的算法。雖然相關(guān)的論文自己也無(wú)法看懂,然而,在網(wǎng)絡(luò)查閱了無(wú)數(shù)資料之后,才發(fā)現(xiàn)如果不去糾結(jié)理論的證明,只關(guān)心結(jié)論的話,神經(jīng)網(wǎng)絡(luò)算法也并非高不可攀。
在查閱資料的過(guò)程中,有幸看到iamtrask的一篇文章A Neural Network in 11 lines of Python (Part 1),文章拋棄推理,直接從結(jié)論出發(fā),用簡(jiǎn)單的Python實(shí)現(xiàn)了一個(gè)簡(jiǎn)易的神經(jīng)網(wǎng)絡(luò)算法,對(duì)于像我這樣數(shù)學(xué)基礎(chǔ)一般的初學(xué)者,具有很大的啟發(fā)作用。本文主要參考這篇文章,并加入自己的理解,試圖以易于理解的方式闡述其思路,如果有表述不當(dāng)?shù)牡胤剑梢詤⒖荚?,或者參考?a target="_blank" rel="nofollow">中文翻譯。
什么是神經(jīng)網(wǎng)絡(luò)算法
在解釋什么是神經(jīng)網(wǎng)絡(luò)之前,我們先明確幾個(gè)機(jī)器學(xué)習(xí)中的術(shù)語(yǔ):特征,樣本,監(jiān)督學(xué)習(xí)
- 樣本:可以用于學(xué)習(xí)的個(gè)體,我們可以獲得這些個(gè)體的其某些信息,并且我們已知這些個(gè)體的類(lèi)別
- 特征:同一屬性在不同個(gè)體所體現(xiàn)出來(lái)的特點(diǎn),在神經(jīng)網(wǎng)絡(luò)中,特征應(yīng)該是易于提取的
- 監(jiān)督學(xué)習(xí):根據(jù)樣本中個(gè)體的特征以及類(lèi)別,然后創(chuàng)建我們的判定規(guī)則,并依靠這些調(diào)查所得信息,調(diào)整我們的規(guī)則,使得規(guī)則的預(yù)測(cè)結(jié)果盡可能接近我們的樣本,這樣,我們利用這個(gè)規(guī)則和新個(gè)體的特征信息,來(lái)判定其所屬分類(lèi)
舉個(gè)例子,我們想要構(gòu)建一套性別識(shí)別算法,而我們能夠獲得的特征信息只有三種:這個(gè)人不是長(zhǎng)頭發(fā);衣服顏色是紅色;身高是否超過(guò)了178cm。那以一個(gè)正常成年人的思維,我們應(yīng)該采用什么策略來(lái)判斷這個(gè)人的性別呢?
我想大部分人都會(huì)采用和我一樣的策略:依據(jù)我們平時(shí)的經(jīng)驗(yàn),一個(gè)人如果留長(zhǎng)頭發(fā),很大可能這個(gè)人是女的,而一個(gè)人穿紅色衣服,很難判斷其性別,一個(gè)人身高超過(guò)175cm,在中國(guó)人里面可能都屬于偏高的,而依據(jù)我們的經(jīng)驗(yàn),男性的平均身高要大于女性,所以單純通過(guò)身高這個(gè)信息,我們推斷這個(gè)人是男性的可能性較大。
以上推測(cè)過(guò)程中,我們對(duì)每一個(gè)條件的推斷都基于一個(gè)詞,經(jīng)驗(yàn),這里的經(jīng)驗(yàn)就是我們長(zhǎng)期以來(lái)觀察身邊的人的特征所留下的記憶。而我們的推測(cè)結(jié)果主要用了一個(gè)詞匯,可能性,其代表基于某條已知信息推測(cè)出某個(gè)未知結(jié)論的概率。
科學(xué)家將我們的思維方法歸納為信息在神經(jīng)元之間的傳導(dǎo)過(guò)程,這些神經(jīng)元每一個(gè)都處理及簡(jiǎn)單的信息,但是通過(guò)無(wú)數(shù)個(gè)神經(jīng)元之間錯(cuò)綜復(fù)雜的連接傳導(dǎo),使得我們的大腦能夠處理及其復(fù)雜的信息。神經(jīng)網(wǎng)絡(luò)算法就是對(duì)我們大腦思維方式的抽象,比如在上面的例子中,我們將每一個(gè)特征,輸入到一個(gè)神經(jīng)元,這些接受輸入的神經(jīng)元構(gòu)成了第一層神經(jīng)網(wǎng)絡(luò),也叫做輸入層,我們的目標(biāo)是判斷一個(gè)人的性別,是男是女分屬于兩個(gè)不同類(lèi)別,我們將其抽象為一個(gè)神經(jīng)元,這個(gè)神經(jīng)元構(gòu)成神經(jīng)網(wǎng)絡(luò)的輸出層,每一個(gè)特征(輸入層神經(jīng)元),都有到分類(lèi)(輸出層神經(jīng)元)的連接,也就是從特征轉(zhuǎn)化為分類(lèi)的概率(權(quán)重),通過(guò)綜合各個(gè)連接的權(quán)重,傳送到輸出神經(jīng)元。

以上便是神經(jīng)網(wǎng)絡(luò)算法最簡(jiǎn)單的模型,神經(jīng)網(wǎng)絡(luò)算法的學(xué)習(xí)過(guò)程中,首先隨機(jī)初始化各層神經(jīng)元之間的連接權(quán)重(上文中的可能性),然后輸入樣本進(jìn)行預(yù)測(cè),根據(jù)預(yù)測(cè)的誤差,調(diào)整神經(jīng)網(wǎng)絡(luò)中的權(quán)重,完成這個(gè)過(guò)程之后,我們的神經(jīng)網(wǎng)絡(luò)就訓(xùn)練好了。所以,神經(jīng)網(wǎng)絡(luò)訓(xùn)練的結(jié)果,就是得到各個(gè)連接的權(quán)重(經(jīng)驗(yàn)),這樣,一旦有新的數(shù)據(jù)輸入神經(jīng)網(wǎng)絡(luò)的時(shí)候,就能推測(cè)出其分類(lèi)了。
神經(jīng)網(wǎng)絡(luò)算法實(shí)例
問(wèn)題定義
還是用上面的預(yù)測(cè)性別的例子,現(xiàn)在將其數(shù)學(xué)化,假設(shè)在一群人中,我們只能獲得每個(gè)人的三個(gè)特征:
- 特征1:長(zhǎng)發(fā)(1)還是短發(fā)(0)
- 特征2:衣服顏色是紅色(1)還是不是紅色(0)
- 特征3:身高大于178cm(1)還是不超過(guò)178(0)
假設(shè)我們只知道其中四個(gè)人的性別(男:0,女:1),我們需要依據(jù)這四個(gè)人的三個(gè)特征以及性別訓(xùn)練一個(gè)神經(jīng)網(wǎng)絡(luò),用于預(yù)測(cè)一個(gè)人的性別。樣本信息如下:
| 頭發(fā) | 衣服 | 身高 | 性別 |
|---|---|---|---|
| 0 | 0 | 1 | 0 |
| 1 | 1 | 1 | 1 |
| 1 | 0 | 1 | 1 |
| 0 | 1 | 1 | 0 |
下面我們先實(shí)現(xiàn)最簡(jiǎn)單的單層神經(jīng)網(wǎng)絡(luò)。我們用X表示輸入的特征向量,由于每個(gè)樣本有三個(gè)特征,一共有四個(gè)樣本,所以我們定義一個(gè)4X3的矩陣,每一行代表一個(gè)樣本,如下代碼所示。其中,NumPy是Python語(yǔ)言的一個(gè)擴(kuò)充程序庫(kù)。支持高級(jí)大量的維度數(shù)組與矩陣運(yùn)算,此外也針對(duì)數(shù)組運(yùn)算提供大量的數(shù)學(xué)函數(shù)庫(kù)。
#import numpy
import numpy as np
# input dataset
X = np.array([ [0,0,1],
[0,1,1],
[1,0,1],
[1,1,1] ])
而四個(gè)樣本對(duì)應(yīng)輸出(分類(lèi)結(jié)果)我們用一個(gè)1X4的矩陣表示。“.T” 為轉(zhuǎn)置函數(shù),轉(zhuǎn)置后變成了4X1的矩陣。同我們的輸入一致,每一行是一個(gè)訓(xùn)練實(shí)例,而每一列(僅有一列)對(duì)應(yīng)一個(gè)輸出節(jié)點(diǎn)。因此,這個(gè)網(wǎng)絡(luò)還有三個(gè)輸入和一個(gè)輸出。代碼如下所示:
# output dataset
y = np.array([[0,0,1,1]]).T
訓(xùn)練開(kāi)始之前,我們先要初始化神經(jīng)網(wǎng)絡(luò)的權(quán)重,由于輸入層有三個(gè)神經(jīng)元,而輸出結(jié)果只有一個(gè)神經(jīng)元,所以權(quán)重矩陣為3X1。由于一般初始化權(quán)重是隨機(jī)選擇的,因此要為隨機(jī)數(shù)設(shè)定產(chǎn)生的種子,如下第一行代碼所示。這樣可以使每次訓(xùn)練開(kāi)始時(shí),得到的訓(xùn)練隨機(jī)數(shù)都是一致的。這樣便于觀察策略變動(dòng)是如何影響網(wǎng)絡(luò)訓(xùn)練的,消除初始權(quán)重的影響。
對(duì)于第二行代碼,這里由于我們要將隨機(jī)初始化的權(quán)重矩陣均值設(shè)定為 0 (至于權(quán)重矩陣的初始化,大家有興趣的話,請(qǐng)查看相關(guān)資料)。因此使用第二行代碼來(lái)計(jì)算syn0(第一層網(wǎng)絡(luò)間的權(quán)重矩陣),如下所示:
# seed random numbers to make calculation
# deterministic (just a good practice)
np.random.seed(1)
# initialize weights randomly with mean 0
syn0 = 2*np.random.random((3,1)) - 1
為了將輸出的權(quán)重歸一化,定義一個(gè)sigmoid函數(shù),其定義為:
sigmoid函數(shù)可以用以下Python代碼實(shí)現(xiàn),其中,deriv參數(shù)表示是否計(jì)算的是其導(dǎo)數(shù)值:
# sigmoid function
def nonlin(x,deriv=False):
if(deriv==True):
return x*(1-x)
return 1/(1+np.exp(-x))
其函數(shù)圖像如下圖所示:

sigmoid函數(shù)的特點(diǎn)是,其導(dǎo)數(shù)可以用其自身表示出來(lái),在計(jì)算的時(shí)候,我們只需要計(jì)算出其函數(shù)值,就可以計(jì)算出其導(dǎo)數(shù)值,從而可以減少浮點(diǎn)運(yùn)算次數(shù),提高效率,其導(dǎo)數(shù)如下:
接下來(lái),我們開(kāi)始訓(xùn)練神經(jīng)網(wǎng)絡(luò):
for iter in range(10000):
# forward propagation
l0 = X
l1 = nonlin(np.dot(l0,syn0))
# how much did we miss?
l1_error = y - l1
# multiply how much we missed by the
# slope of the sigmoid at the values in l1
l1_delta = l1_error * nonlin(l1,True)
# update weights
syn0 += np.dot(l0.T,l1_delta)
我們的訓(xùn)練過(guò)程迭代10000次,以得到一個(gè)較優(yōu)的結(jié)果,每一次迭代的過(guò)程可以描述為:
- 計(jì)算輸入層的加權(quán)和,即用輸入矩陣L0乘以權(quán)重矩陣syn0,并通過(guò)sigmid函數(shù)進(jìn)行歸一化。得到輸出結(jié)果l1;
- 計(jì)算輸出結(jié)果L1與真實(shí)結(jié)果y之間的誤差L1_error;
- 計(jì)算權(quán)重矩陣的修正L1_delta,即用誤差乘以sigmoid在L處的導(dǎo)數(shù);
- 用L1_delta更新權(quán)重矩陣syn0
此處著重解釋 下第三步,這里利用的是梯度下降法,算法的原理我們暫不深究,只需要明白其目的是為了使迭代后的誤差逐漸減小即可。
一次訓(xùn)練過(guò)程的參數(shù)更新如下圖所示:

由于我們的輸入X中一共有四個(gè)樣本,我們進(jìn)行“批量的訓(xùn)練”,所以其過(guò)程類(lèi)似于下圖所示:

在上述代碼中,我們通過(guò)10000次迭代,我們得到的輸出結(jié)果如下:
Output syn0 After Training:
[[ 9.67299303]
[-0.2078435 ]
[-4.62963669]]
可以看出,syn0的第一個(gè)元素,也就是第一個(gè)輸入特征(長(zhǎng)發(fā))的權(quán)重最大,而第二個(gè)和第三個(gè)特征都很小,所以神經(jīng)網(wǎng)絡(luò)學(xué)習(xí)的結(jié)果是加重第一個(gè)特征的權(quán)重,而其他兩個(gè)特征對(duì)于是女性這個(gè)推測(cè)的貢獻(xiàn)較小,所以減小其權(quán)重。為了驗(yàn)證訓(xùn)練結(jié)果,我們加入兩組新數(shù)據(jù),(短頭發(fā),紅衣服,矮個(gè)子),(長(zhǎng)頭發(fā),不是紅衣服,矮個(gè)子),并用神經(jīng)網(wǎng)絡(luò)來(lái)進(jìn)行分類(lèi):
X_new = np.array([[0,1,0],
[1,0,0]])
y_new = np.dot(X_new,syn0)
計(jì)算結(jié)果如下:
Predicte With syn0:
[[-0.2078435 ]
[ 9.67299303]]
二層神經(jīng)網(wǎng)絡(luò)
上面的例子中,我們只用了一層神經(jīng)網(wǎng)絡(luò),這只能解決線性問(wèn)題,而現(xiàn)實(shí)中,一個(gè)孤立的特征并不是對(duì)應(yīng)一個(gè)分類(lèi),還是用上面的例子說(shuō)明:上面的問(wèn)題中,我們假定了長(zhǎng)頭發(fā)是女性的概率一定大于男性,高個(gè)子是男性的概率一定大于女性,這種假設(shè)中,特征和分類(lèi)是一種確定的關(guān)系,而特征之間沒(méi)有依賴(lài)關(guān)系。而現(xiàn)在,我修改這種假設(shè),在穿紅衣服的人群中,長(zhǎng)頭發(fā)更可能是女性,而在穿其他顏色的衣服中,短頭發(fā)更有可能是女性,此時(shí),我們上面的神經(jīng)網(wǎng)絡(luò)模型就失效了,因?yàn)槲覀儫o(wú)法直接建頭發(fā)這個(gè)輸入特征到性別這個(gè)輸入的直接聯(lián)系。
為了解決上面的問(wèn)題,我們需要在加入一層神經(jīng)網(wǎng)絡(luò),將輸入層的特征進(jìn)行組合,然后在傳導(dǎo)到輸出層,這就是二層神經(jīng)網(wǎng)絡(luò)的模型,其示意圖如下:
中間加入的這一層佳作隱含層,由于這一層的加入,我們多了一層傳導(dǎo),所以初始化的時(shí)候需要再加入一個(gè)權(quán)重矩陣:
syn1 = 2*np.random.random((4,1)) - 1
兩層神經(jīng)網(wǎng)絡(luò)的學(xué)習(xí)更新過(guò)程如下:
for j in range(60000):
# Feed forward through layers 0, 1, and 2
l0 = X
l1 = nonlin(np.dot(l0,syn0))
l2 = nonlin(np.dot(l1,syn1))
# how much did we miss the target value?
l2_error = y - l2
if (j% 10000) == 0:
print("Error:" + str(np.mean(np.abs(l2_error))))
# in what direction is the target value?
# were we really sure? if so, don't change too much.
l2_delta = l2_error*nonlin(l2,deriv=True)
# how much did each l1 value contribute to the l2 error (according to the weights)?
l1_error = l2_delta.dot(syn1.T)
# in what direction is the target l1?
# were we really sure? if so, don't change too much.
l1_delta = l1_error * nonlin(l1,deriv=True)
syn1 += l1.T.dot(l2_delta)
syn0 += l0.T.dot(l1_delta)
結(jié)合前面的單層神經(jīng)網(wǎng)絡(luò)的實(shí)現(xiàn),就很容易理解上面的代碼了,代碼中,L0的輸出沒(méi)有直接作為最終輸出層,而是傳導(dǎo)給了L2層,L2層以相同的方式傳導(dǎo)到輸出層。而更新權(quán)重的時(shí)候,采用的是相反的過(guò)程,先依據(jù)L2輸出的誤差,更新syn1,再用L2的誤差乘以syn1,作為L(zhǎng)1層的誤差,最后用同樣的方法更新第一層權(quán)重矩陣syn0
結(jié)語(yǔ)
這篇文章以最簡(jiǎn)單的方式構(gòu)建了一個(gè)基本的神經(jīng)網(wǎng)絡(luò),雖然離實(shí)用還相去甚遠(yuǎn),但是已經(jīng)初現(xiàn)神經(jīng)網(wǎng)絡(luò)的雛形框架,如果需要構(gòu)建一個(gè)實(shí)用級(jí)別的神經(jīng)網(wǎng)絡(luò),還需要加入一些其他的功能,原作者建議我們從以下這些概念開(kāi)始入手,優(yōu)化我們的神經(jīng)網(wǎng)絡(luò)):
- Alpha
- Bias Units
- Mini-Batches
- Delta Trimming
- Parameterized Layer Sizes
- Regularization
- Dropout
- Momentum
- Batch Normalization
- GPU Compatability
- 其他腦洞