代碼均在這裏:
Get the code: To follow along, all the code is also available as an iPython notebook on Github.
在這篇文章中,我們將從頭開始實現一個簡單的3層神經網絡。
我們不會推導出所有需要的數學內容,但是我會嘗試直觀地解釋我們正在做什麼,我也會給出資源給你詳細閱讀用。
在這裏,我假設你熟悉基本的微積分和機器學習概念,例如,你知道什麼是分類和正則化。理想情況下,你還知道一些關於如何使用梯度下降優化的技術。但是即使你不熟悉上述任何列出來的,仍然可以是很有趣的 : )
然而,爲什麼要從頭開始實現一個神經網絡呢?即使您計劃在未來使用像PyBrain這樣的神經網絡庫,至少一次從頭開始實現一個網絡是非常有價值的練習。它可以幫助您瞭解神經網絡的工作原理,這對設計有效的模型至關重要。
有一點需要注意的是,這裏的代碼示例並不是非常有效。它們被設計成比較容易理解。在即將發佈的帖子中,我將探討如何使用Theano編寫一個高效的神經網絡實現。 (更新:now available)
Generating a dataset 生成數據集
讓我們從生成一個數據集給我們使用開始。幸運的是,scikit-learn有一些有用的數據集生成器,所以我們不需要自己編寫代碼。 我們將使用make_moons函數。
import numpy as np
from sklearn import datasets, linear_model
import matplotlib.pyplot as plt
%matplotlib inline
# Generate a dataset and plot it
np.random.seed(0)
X, y = sklearn.datasets.make_moons(200, noise=0.20)
plt.scatter(X[:,0], X[:,1], s=40, c=y, cmap=plt.cm.Spectral)
我們生成的數據集有兩個類,繪製爲紅色和藍色點。你可以將藍點作爲男性患者,將紅點視爲女性患者,X軸和Y軸是醫學測量。
我們的目標是訓練一個機器學習分類器,根據給出的xy座標來預測正確的分類(男性或女性)。請注意數據不是線性分離的,我們不能繪製分開兩個類的直線。這意味着線性分類器像邏輯迴歸,將無法適應數據,除非你手工設計適用於給定數據集的非線性特徵(如多項式)。
事實上,這是神經網絡的主要優點之一。 您不需要擔心特徵工程。 神經網絡的隱藏層將爲你學習feature。
Logistic Regression 邏輯迴歸
爲了說明我們訓練一個邏輯迴歸分類器,它的輸入是xy值,輸出預測類(0或1)。爲了方便簡單,我們從scikit-learn中使用 Logistic Regression class.
!!! 代碼是GitHub上的那個文件 simple_classification.py
# Train the logistic rgeression classifier
clf = sklearn.linear_model.LogisticRegressionCV()
clf.fit(X, y)
# Plot the decision boundary
plot_decision_boundary(lambda x: clf.predict(x))
plt.title("Logistic Regression")
該圖顯示了由Logistic迴歸分類器學到的判定界限。 它將數據分離成可以使用直線,但它無法捕獲我們的數據的“月亮形狀”。
Training a Neural Network
現在我們來構建一個三層神經網絡,一個輸入層,一個隱藏層和一個輸出層。
輸入層中的節點數由我們的數據的維度決定,就是2;
類似地,輸出層中的節點數由我們所擁有的類的數量所決定,也是2。(因爲我們只有2個類, 實際上可以只有一個輸出節點預測0或1,但是有2個可以更容易地將網絡擴展到更多的類)。
網絡的輸入將是x和y座標,其輸出將是兩個概率,一個用於class 0(“female”),一個用於class 1(“male”)。
看起來像這樣:
我們可以選擇隱藏層的維數(節點數)。我們放入隱藏層中的節點越多,我們將能夠適應的更復雜的功能。但更高的維度會帶來成本。
首先,需要更多的計算來進行預測並學習網絡的參數。
更多的參數也意味着我們更容易過度擬合我們的數據。
那麼如何選擇隱藏層的大小呢?雖然有一些一般的指導方針和建議,但它總是取決於你的具體問題,更像是一門藝術而不是一門科學。稍後我們會着手於隱藏的節點數,看看它是如何影響我們的輸出的。
我們還需要給我們的隱藏層選擇激活函數。激活函數(The activation function)將層的輸入轉換爲其輸出。非線性激活函數是允許我們擬合非線性假設的。常見選擇的激活函數是tanh, the sigmoid function, 或者ReLUs.
我們將使用tanh,在許多情況下表現相當好。這些函數的一個很好的屬性是它們的倒數可以使用原始函數值來計算。
例如,
因爲我們希望我們的network輸出概率,輸出層的激活函數將是softmax,一種方法簡單地將原始score轉換爲概率。
如果你熟悉logistic function,you can think of softmax as its generalization to multiple classes.(這句還是放原文比較好 T_T).
How our network makes predictions
我們的網絡使用正向傳播去預測,正向傳播只是一堆矩陣乘法和上面定義的激活函數的應用。
x是我們的網絡的二維輸入,那麼我們如下計算我們的預測
看着上面的矩陣乘法,我們可以計算出這些矩陣的維數:
如果我們使用500個節點在隱含層,那麼
現在你可以看到了爲什麼我們需要更多的參數,如果我們提高了隱藏層的size。
Learning the Parameters
學習我們網絡參數意味着我們需要找到能最小化錯誤,在我們的訓練數據上。(the error on our training data)。不過,我們該如何定義我們的error?
We call the function that measures our error the loss funtion. 我們叫它 loss function。
softmax輸出的一個常見的選擇是分類交叉熵損失(也稱負對數似然值)。
the categorical cross-entropy loss
如果我們有N個訓練樣本 和 C個類,那麼 預測值 y’ 相對於 真實的 labels y 的損失(loss):
Implementation:
We start by defining some useful variables and parameters for gradient descent:
num_examples = len(X) # training set size
nn_input_dim = 2 # input layer dimensionality
nn_output_dim = 2 # output layer dimensionality
# Gradient descent parameters (I picked these by hand)
epsilon = 0.01 # learning rate for gradient descent
reg_lambda = 0.01 # regularization strength
首先讓我們來實現我們上面定義的損失函數。 我們用這個來評估我們的模型在做什麼:
# Helper function to evaluate the total loss on the dataset
def calculate_loss(model):
W1, b1, W2, b2 = model['W1'], model['b1'], model['W2'], model['b2']
# Forward propagation to calculate our predictions
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)
# Calculating the loss
corect_logprobs = -np.log(probs[range(num_examples), y])
data_loss = np.sum(corect_logprobs)
# Add regulatization term to loss (optional)
data_loss += reg_lambda/2 * (np.sum(np.square(W1)) + np.sum(np.square(W2)))
return 1./num_examples * data_loss
我們還實現了一個輔助函數來計算網絡的輸出。 它按前面定義的那樣進行前向傳播,並以最高概率返回類。
# Helper function to predict an output (0 or 1)
def predict(model, x):
W1, b1, W2, b2 = model['W1'], model['b1'], model['W2'], model['b2']
# Forward propagation
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)
return np.argmax(probs, axis=1)
最後,這裏是訓練我們的神經網絡的功能。 它使用我們在上面提到的反向傳播實現批量梯度下降。
# This function learns parameters for the neural network and returns the model.
# - nn_hdim: Number of nodes in the hidden layer
# - num_passes: Number of passes through the training data for gradient descent
# - print_loss: If True, print the loss every 1000 iterations
def build_model(nn_hdim, num_passes=20000, print_loss=False):
# Initialize the parameters to random values. We need to learn these.
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))
# This is what we return at the end
model = {}
# Gradient descent. For each batch...
for i in xrange(0, num_passes):
# Forward propagation
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)
# Backpropagation
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)
# Add regularization terms (b1 and b2 don't have regularization terms)
dW2 += reg_lambda * W2
dW1 += reg_lambda * W1
# Gradient descent parameter update
W1 += -epsilon * dW1
b1 += -epsilon * db1
W2 += -epsilon * dW2
b2 += -epsilon * db2
# Assign new parameters to the model
model = { 'W1': W1, 'b1': b1, 'W2': W2, 'b2': b2}
# Optionally print the loss.
# This is expensive because it uses the whole dataset, so we don't want to do it too often.
if print_loss and i % 1000 == 0:
print "Loss after iteration %i: %f" %(i, calculate_loss(model))
return model
A network with a hidden layer of size 3
讓我們看看如果我們訓練隱藏層大小爲3的網絡會發生什麼:
# Build a model with a 3-dimensional hidden layer
model = build_model(3, print_loss=True)
# Plot the decision boundary
plot_decision_boundary(lambda x: predict(model, x))
plt.title("Decision Boundary for hidden layer size 3")
這看起來很不錯。 我們的神經網絡能夠找到一個成功分離類的決策邊界。
Varying the hidden layer size 改變隱藏層的大小
在上面的例子中,我們選擇了隱藏層大小3.現在讓我們瞭解隱藏層大小如何改變結果。
plt.figure(figsize=(16, 32))
hidden_layer_dimensions = [1, 2, 3, 4, 5, 20, 50]
for i, nn_hdim in enumerate(hidden_layer_dimensions):
plt.subplot(5, 2, i+1)
plt.title('Hidden Layer size %d' % nn_hdim)
model = build_model(nn_hdim)
plot_decision_boundary(lambda x: predict(model, x))
plt.show()
我們可以看到隱藏的低維度層很好地捕捉了我們數據的總體趨勢。 更高的維度容易過度擬合。 他們是“記憶”數據而不是擬合一般的形狀。 如果我們要在一個單獨的測試集上評估我們的模型(而且您應該!),那麼具有較小隱藏層大小的模型由於更好的泛化可能會更好。 我們可以用更強的正則化來抵消過度擬合,但是爲隱藏層選擇正確的大小是一個更“economical”的解決方案。
英文原博網址在:
http://www.wildml.com/2015/09/implementing-a-neural-network-from-scratch/