用Python從頭開始實現一個神經網絡

在這篇文章中,我們將從頭開始實現一個簡單的三層神經網絡,要實現這個三層神經網絡,所要求的數學知識也許你不是都會,但我會試著直觀地解釋我們正在做的事情。我也會為你提供一些資源,幫助你深入理解細節。

在這裡我假設你熟悉基礎微積分和機器學習的一些概念,並且你知道什麼是分類和正則化。最好你還稍微知道優化算法(比如梯度下降法)的工作原理。不過,即使你對上面的任何一條都不熟悉,這篇文章對你來說仍然會很有趣。;)

PS:如果你覺得這篇文章看起來稍微會有些吃力,沒關係,這裡有一個我朋友的人工智能教程。零基礎!通俗易懂!風趣幽默!大家可以看看是否對自己有幫助,接下來進入正文。

為什麼要從頭開始實現神經網絡呢?即使你打算以後使用PyBrain這樣的神經網絡庫,至少一次從頭開始實現一個神經網絡也是一個極具價值的練習,這會幫助你理解神經網絡是怎麼工作的,並且如果你想要設計出高效的神經網絡模型,做一個這樣的練習也是很有必要的。

需要注意的一件事情是,本篇文章的示例代碼效率並不高,它的目的是易於被人理解。在下一篇文章中我將帶你們探索怎樣用Theano寫出高效的神經網絡

生成數據集其次導包

首先我們生成一個可以操作的數據集,

用Python從頭開始實現一個神經網絡

我們生成的數據集中有兩種類型的數據,分別用紅點和藍點標識了出來。你可以將藍點視為男性患者,將紅點視為女性患者,並且將x軸和y軸視為醫療方式。

我們的目標是訓練一個機器學習分類器,讓它在x、y座標系下預測正確的類別(男性或者女性)。需要注意的是這些數據不能被線性分割,我們無法畫出一條直線將這兩種類型的數據分開,這意味著線性分類器(比如Logistic迴歸)將無法擬合這些數據,除非你手工設計的非線性特徵(如多項式),為給定的數據集工作良好。

事實上,這就是神經網絡主要的優點之一,你不必擔心特徵工程,神經網絡的隱藏層將會為你學習特徵。

Logistic迴歸

為了證明這一點,讓我們訓練一個Logistic迴歸分類器。這個分類器的輸入是座標x、y,它的輸出是預測的數據類型(0或1)。為了方便,我們使用scikit-learn中的Logistic Regression類。

用Python從頭開始實現一個神經網絡

上圖顯示了Logistic迴歸分類器學習的決策邊界,它用直線儘可能地把數據分開了,但是並沒有捕獲數據的月牙形狀。

訓練一個神經網絡

現在讓我們搭建一個3層的神經網絡,其中包含1個輸入層,1個隱藏層以及1個輸出層。輸出層的節點數取決於我們的數據的維度,也就是2;類似地,輸出層的節點數取決於我們有多少類數據,這裡也是2(因為我們只有兩類數據,所以實際上可以只有一個輸出節點,輸出0或1,但是有兩個輸出節點會使得以後有多類數據的時候神經網絡更容易擴展)。神經網絡的輸入是x、y座標,而輸出是兩個概率,一個是類型為0(女性患者)的概率,另一個是類型為1(男性患者)的概率。這個神經網絡看起來就像下面這樣:

用Python從頭開始實現一個神經網絡


我們可以自己選擇隱藏層的維度(節點數量),隱藏層節點的數量越多,我們能夠適應的功能就越複雜。但是高維度總是伴隨著高成本。如果節點數量很多,首先,為了預測結果以及學習神經網絡參數,就需要進行大量的計算;其次,更多的參數數量(譯者注:隱藏層的參數量是由該層節點數和前一層的節點數決定的)意味著更容易造成對數據的過度擬合。

那麼該如何選擇隱藏層的節點數呢?儘管有一些通用指南和建議,但是隱藏層的節點數通常取決於你要解決的特定的問題,與其說節點數的選擇是一門科學,不如說它是一門藝術。我們稍後會探究隱藏層節點的數量問題,並觀察它是如何影響神經網絡的輸出的。

我們還需要為隱藏層選擇一個激活函數,激活函數用來將該層的輸入轉換成輸出。非線性激活函數能夠使我們擬合非線性數據。在這裡我們使用tanh,因為它在很多場景中都表現得很好。這些函數都有一個很棒的特性,那就是它們的導數都能用它們本身表示,比如,tanh(x)的導數是1−tanh2(x)1-tanh^2(x)1−tanh2(x),這非常有用,因為我們只要計算tanh(x)的值一次,就可以在對tanh(x)求導的時候重複使用它。

因為我們想要讓神經網絡輸出概率,所以輸出層的激活函數我們使用softmax,它可以簡單地把原始數值轉換成概率。如果你對邏輯函數很熟悉,那麼你可以將softmax看作是邏輯函數在多類型中的一般化應用。

神經網絡如何做出預測

我們的神經網絡利用正向傳播來做出預測,所謂正向傳播,就是指一組矩陣的相乘以及我們之前提到的激活函數的應用。假設x是對神經網絡的二維輸入,那麼我們按照如下步驟計算我們的預測結果$ \\hat{y}$(維度也是2):
z1=xW1+b1a1=tanh(z1)z2=a1W2+b2a2=yˆ=softmax(z2)z_1=xW_1+b_1\\\\a_1=tanh(z_1)\\\\z_2=a_1W_2+b_2\\\\a_2=\\hat{y}=softmax(z_2)z1​=xW1​+b1​a1​=tanh(z1​)z2​=a1​W2​+b2​a2​=y^​=softmax(z2​)
ziz_izi​是第iii層的輸入,aia_iai​是第iii層使用激活函數計算後的輸出,W1,b1,W2,b2W_1,b_1,W_2,b_2W1​,b1​,W2​,b2​是我們這個神經網絡的參數,是需要通過訓練數據來讓神經網絡學習的。你可以將他們看作是在神經網絡的不同層之間轉換數據的矩陣。觀察上面的矩陣乘法運算,我們可以算出這些矩陣的維度,如果隱藏層有500個節點,那麼W1∈R2×500,b1∈R500,W2∈R500×2,b2∈R2W_1\\in\\mathbb{R}^{2\\times500},b_1\\in\\mathbb{R}^{500},W_2\\in\\mathbb{R}^{500\\times2},b_2\\in\\mathbb{R}^2W1​∈R2×500,b1​∈R500,W2​∈R500×2,b2​∈R2。現在你應該知道為什麼如果我們增加隱藏層的節點數量,我們就會有更多的參數了。

學習參數

為神經網絡學習參數意味著在訓練數據上尋找最佳參數(W1,b1,W2,b2)(W_1,b_1,W_2,b_2)(W1​,b1​,W2​,b2​),以此達到使錯誤最小化的目的。但是我們應該如何定義“錯誤”呢?我們將衡量錯誤的函數稱為損失函數(loss function)。對於softmax輸出來說,一個常用的選擇是分類交叉熵損失(又稱為負對數似然 negative log likelihood)。如果我們有N個訓練樣本,以及C個輸出類別,那麼我們的預測結果$ \\hat{y}相對於真值標籤相對於真值標籤相對於真值標籤 y$的損失是這樣定義的:


L(y,yˆ)=−1N∑n∈N∑i∈Cyn,ilogyˆn,iL(y,\\hat{y})=-\\frac{1}{N} \\sum_{n \\in N} \\sum_{i \\in C} y_{n,i} \\log \\hat{y}_{n,i}L(y,y^​)=−N1​n∈N∑​i∈C∑​yn,i​logy^​n,i​
這個公式看起來複雜,其實它做的事情就是總結訓練樣本,如果預測錯了類型,就增加損失(譯者注:粗體部分的原文如下:sum over our training examples and add to the loss if we predicted the incorrect class. 我不確定粗體部分這樣翻譯是否合適,如果有網友有更好的翻譯歡迎指正)。yyy(正確的標籤)和yˆ\\hat{y}y^​(我們的預測值)的數值相差越大,我們的損失就越大。通過尋找使損失最小化的參數,我們可以最大限度地提高訓練數據的似然。

我們可以用梯度下降法來尋找損失函數的最小值。我會用固定的學習率實現一個最普通版本的梯度下降法,也稱為批量梯度下降法,它的變化版本比如隨機梯度下降法和小批量梯度下降法在實踐中通常表現得更好,所以如果你對此要求嚴格,那麼你一定會想要使用它們,最好隨著時間的推移再配合以學習率衰減

作為輸入,梯度下降法需要計算損失函數對於參數的梯度(導數向量):∂L∂W1,∂L∂b1,∂L∂W2,∂L∂b2\\frac{\\partial{L}}{\\partial{W_1}},\\frac{\\partial{L}}{\\partial{b_1}},\\frac{\\partial{L}}{\\partial{W_2}},\\frac{\\partial{L}}{\\partial{b_2}}∂W1​∂L​,∂b1​∂L​,∂W2​∂L​,∂b2​∂L​。我們利用著名的反向傳播算法來計算這些梯度,從輸出開始用反向傳播計算梯度會很高效。對於反向傳播的工作原理,我不會深入講解其細節,但是網上有很多出色的解釋

實現

現在我們已經準備好實現一個神經網絡了,我們為梯度下降定義一些變量和參數:

首先我們實現之前定義的損失函數,我們用這個損失函數來衡量模型的工作成果是否令人滿意。

我們還實現了一個函數用來幫助我們計算神經網絡的輸出,就像我們之前定義的那樣,在函數內部進行正向傳播,然後返回概率最高的那個類別。

最後,這個函數用來訓練神經網絡,它利用我們之前提到的反向傳播導數來實現批量梯度下降。

# 這個函數為神經網絡學習參數並且返回模型
# - nn_hdim: 隱藏層的節點數
# - num_passes: 通過訓練集進行梯度下降的次數
# - print_loss: 如果是True, 那麼每1000次迭代就打印一次損失值
def build_model(nn_hdim, num_passes=20000, print_loss=False):

# 用隨機值初始化參數。我們需要學習這些參數
np.random.seed(0)
W1 = np.random.randn(nn_input_dim, nn_hdim) / np.sqrt(nn_input_dim)
b1 = np.zeros((1, nn_hdim))
W2 = np.random.randn(nn_hdim, nn_output_dim) / np.sqrt(nn_hdim)
b2 = np.zeros((1, nn_output_dim))

# 這是我們最終要返回的數據

model = {}

# 梯度下降
for i in xrange(0, num_passes):

# 正向傳播
z1 = X.dot(W1) + b1
a1 = np.tanh(z1)
z2 = a1.dot(W2) + b2
exp_scores = np.exp(z2)
probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True)

# 反向傳播
delta3 = probs
delta3[range(num_examples), y] -= 1
dW2 = (a1.T).dot(delta3)
db2 = np.sum(delta3, axis=0, keepdims=True)
delta2 = delta3.dot(W2.T) * (1 - np.power(a1, 2))
dW1 = np.dot(X.T, delta2)
db1 = np.sum(delta2, axis=0)

# 添加正則項 (b1 和 b2 沒有正則項)
dW2 += reg_lambda * W2
dW1 += reg_lambda * W1

# 梯度下降更新參數
W1 += -epsilon * dW1
b1 += -epsilon * db1
W2 += -epsilon * dW2
b2 += -epsilon * db2

# 為模型分配新的參數
model = { 'W1': W1, 'b1': b1, 'W2': W2, 'b2': b2}

# 選擇性地打印損失
# 這種做法很奢侈,因為我們用的是整個數據集,所以我們不想太頻繁地這樣做
if print_loss and i % 1000 == 0:
print "Loss after iteration %i: %f" %(i, calculate_loss(model))

return model

隱藏層節點數為3的神經網絡

當我們訓練一個隱藏層節點數為3的神經網絡時,讓我們看看會發生什麼。

用Python從頭開始實現一個神經網絡


Yes!這看起來很棒。我們的神經網絡發現的決策邊界能夠成功地區分數據類別。

改變隱藏層的大小

在上面的例子中,我們選擇了有三個節點的隱藏層,現在讓我們看看改變隱藏層的大小會如何影響最終的結果。

用Python從頭開始實現一個神經網絡


我們可以看到低維度的隱藏層可以很好地捕獲數據的大致邊界,而高維度的隱藏層則更易出現過度擬合。正如我們期待的那樣,它們“記住”了數據並適應了數據的大致形狀。如果我們接下來在一個獨立的測試集上評估我們的模型(事實上你也應該這麼做),低維度的隱藏層會表現得更好,因為它們更一般化。我們可以利用更高強度的正則化來抵消過度擬合,但是為隱藏層選擇一個正確的尺寸則是比較“經濟”的解決辦法。

練習

為了對代碼更熟悉,你可以嘗試做下面這些事:

  1. 用小批量梯度下降而不是批量梯度下降法來訓練神經網絡。在實踐中,小批量梯度下降法通常會表現得更好。
  2. 我們在梯度下降時用了固定的學習率ϵ\\epsilonϵ。你可以為梯度下降的學習率創建一個衰減過程
  3. 我們在隱藏層使用的激活函數是tanhtanhtanh。用其他的激活函數實踐一下(有一些在上面提到了)。注意:改變激活函數也就意味著要改變反向傳播導數。
  4. 把神經網絡的輸出類別從2增加到3。為此你需要生成一個類似的數據集。
  5. 將神經網絡擴展到4層。在神經網絡的層數上面做一下實踐。增加另外一個隱藏層意味著你不僅要調整正向傳播的代碼,還要調整反向傳播的代碼。

(譯者注:部分語句藉助了谷歌翻譯,另外有一些專業詞彙我是根據自己的理解翻譯的,如有不當之處歡迎廣大網友批評指正。)


分享到:


相關文章: