獲取程式碼:接下來,為了匹配文章的內容,所有的程式碼都會在Github上以iPython筆記的形式提供。
本文中我們會從頭實現一個簡單的3層神經網路。我們不會推導所有的數學公式,但會給我們正在做的事情一個相對直觀的解釋。我也會給出你研讀所需的資源連結。
這裡假設你已經比較熟悉微積分和機器學習的概念了。比如,你知道什麼是分類和正則化。當然你也應該瞭解一點優化技巧,如梯度下降是如何工作的。但是即使你對上面提到的任何一個概念都不熟悉,你仍然會發現本文的有趣所在。
但是為什麼要從頭實現一個神經網路呢?即使你打算將來使用像PyBrain這樣的神經網路庫,從頭實現神經網路仍然是一次非常有價值的練習。它會幫助你理解神經網路的工作原理,而這是設計有效模型的必備技能。
需要注意的是這裡的示例程式碼並不是十分高效,它們本就是用來幫助理解的。在接下來的文章中,我會探索如何使用Theano寫一個高效的神經網路實現。
產生資料集
讓我們從力所能及的產生資料集開始吧。幸運的是,scikit-learn提供了一些很有用的資料集產生器,所以我們不需要自己寫程式碼了。我們將從make_moons 函式開始。
1 2 3 4 |
# 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軸表示藥物治療。
我們的目標是,在給定x和y軸的情況下訓練機器學習分類器以預測正確的分類(男女分類)。注意,資料並不是線性可分的,我們不能直接畫一條直線以區分這兩類資料。這意味著線性分類器,比如Logistic迴歸,將不適用於這個資料集,除非手動構建在給定資料集表現很好的非線性特徵(比如多項式)。
事實上,這也是神經網路的主要優勢。你不用擔心特徵構建,神經網路的隱藏層會為你學習特徵。
Logistic迴歸
為了證明這個觀點,我們來訓練一個Logistic迴歸分類器。它的輸入是x和y軸的值,輸出預測的分類(0或1)。為了簡單,我們使用scikit-learn庫裡的Logistic迴歸類。
1 2 3 4 5 6 7 |
# 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迴歸分類器學習到的決策邊界。使用一條直線儘量將資料分離開來,但它並不能捕捉到資料的“月形”特徵。
訓練神經網路
讓我們來建立具有一個輸入層、一個隱藏層、一個輸出層的三層神經網路。輸入層的結點數由資料維度決定,這裡是2維。類似地,輸出層的結點數由類別數決定,也是2。(因為我們只有兩類輸出,實際中我們會避免只使用一個輸出結點預測0和1,而是使用兩個輸出結點以使網路以後能很容易地擴充套件到更多類別)。網路的輸入是x和y座標,輸出是概率,一個是0(女性)的概率,一個是1(男性)的概率。它看起來像下面這樣:
我們可以為隱藏層選擇維度(結點數)。放入隱藏層的結點越多,我們能訓練的函式就越複雜。但是維度過高也是有代價的。首先,預測和學習網路的引數就需要更多的計算。引數越多就意味著我們可能會過度擬合資料。
如何選擇隱藏層的規模?儘管有一些通用的指導和建議,但還是依賴於具體問題具體分析,與其說它是一門科學不如說是一門藝術。我們稍後會在隱藏層的結點數上多做一點事情,然後看看它會對輸出有什麼影響。
我們還需要為隱藏層挑選一個啟用函式。啟用函式將該層的輸入轉換為輸出。一個非線性啟用函式允許我們擬合非線性假設。常用的啟用函式有tanh、the sigmoid函式或者是ReLUs。這裡我們選擇使用在很多場景下都能表現很好的tanh函式。這些函式的一個優點是它們的導數可以使用原函式值計算出來。例如,tanh x的導數是1-tanh^2 x。這個特性是很有用的,它使得我們只需要計算一次tanh x值,之後只需要重複使用這個值就可以得到導數值。
因為我們想要得到神經網路輸出概率,所以輸出層的啟用函式就要是softmax。這是一種將原始分數轉換為概率的方法。如果你很熟悉logistic迴歸,可以把softmax看作是它在多類別上的一般化。
神經網路如何預測
神經網路使用前向傳播進行預測。前向傳播只不過是一堆矩陣相乘並使用我們上面定義的啟用函式了。假如x是該網路的2維輸入,我們將按如下計算預測值(也是二維的):
zi是輸入層、ai是輸出層。W1,b1,W2,b2是需要從訓練資料中學習的網路引數。你可以把它們看作是神經網路各層之間資料轉換矩陣。看著上文的矩陣相乘,我們可以計算出這些矩陣的維度。如果我們的隱藏層中使用500個結點,那麼有
現在你明白了為什麼增大隱藏層的規模會導致需要訓練更多引數。
學習引數
學習該網路的引數意味著要找到使訓練集上錯誤率最小化的引數(W1,b1,W2,b2)。但是如何定義錯誤率呢?我們把衡量錯誤率的函式叫做損失函式(loss function)。輸出層為softmax時多會選擇交叉熵損失(cross-entropy loss)。假如我們有N個訓練例子和C個分類,那麼預測值(hat{y})相對真實標籤值的損失就由下列公式給出:
這個公式看起來很複雜,但實際上它所做的事情不過是把所有訓練例子求和,然後加上預測值錯誤的損失。所以,hat{y}(預測值)距離 hat{y}(真實標籤值)越遠,損失值就越大。
要記住,我們的目標是找到能最小化損失函式的引數值。我們可以使用梯度下降方法找到最小值。我會實現梯度下降的一種最普通的版本,也叫做有固定學習速率的批量梯度下降法。諸如SGD(隨機梯度下降)或minibatch梯度下降通常在實踐中有更好的表現。所以,如果你是認真的,這些可能才是你的選擇,最好還能逐步衰減學習率。
作為輸入,梯度下降需要一個與引數相關的損失函式的梯度(導數向量):,。為了計算這些梯度,我們使用了著名的後向傳播演算法。這個演算法是從輸出計算梯度的一種很高效的方法。在這裡我不會深入講解後向傳播如何工作,但是在網路上流傳有很多很優秀的講解(參見這裡或是這裡)。
應用後向傳播公式我們發現以下內容(這點你要相信我):
實現
現在我們要準備開始實現網路了。我們從定義梯度下降一些有用的變數和引數開始:
1 2 3 4 5 6 7 |
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 |
首先要實現我們上面定義的損失函式。以此來衡量我們的模型工作得如何:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# 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 |
還要實現一個輔助函式來計算網路的輸出。它的工作就是傳遞前面定義的前向傳播並返回概率最高的類別。
1 2 3 4 5 6 7 8 9 10 |
# 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) |
最後是訓練神經網路的函式。它使用上文中發現的後向傳播導數實現批量梯度下降。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
# 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 |
隱藏層規模為3的神經網路
一起來看看假如我們訓練了一個隱藏層規模為3的神經網路會發生什麼。
1 2 3 4 5 6 |
# 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") |
耶!這看起來結果相當不錯。我們的神經網路能夠找到成功區分類別的決策邊界。
變換隱藏層的規模
在上述例子中,我們選擇了隱藏層規模為3。現在來看看改變隱藏層規模會對結果造成怎樣的影響。
1 2 3 4 5 6 7 8 |
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() |
可以看到,低維隱藏層能夠很好地捕捉到資料的總體趨勢。更高的維度則更傾向於過擬合。它們更像是在“記憶”資料而不是擬合資料的大體形狀。假如我們打算在獨立測試集上評測該模型(你也應當這樣做),隱藏層規模較小的模型會因為能更好的泛化而表現更好。雖然我們可以使用更強的泛化來抵消過擬合,但是為隱藏層選擇一個合適的規模無疑是更加“經濟”的方案。
練習
這裡有一些練習幫助你進一步熟悉這些程式碼:
使用minibatch梯度下降代替批量梯度下降來訓練網路(更多資訊)。minibatch梯度下降在實際中一般都會表現的更好。
例子中我們使用的是固定學習速率的梯度下降。請為梯度下降的學習速率實現一個退火演算法(更多資訊)。
這裡我們使用的是tanh 作為隱藏層的啟用函式。請嘗試一下其他啟用函式(比如上文中提到過的那些)。注意改變啟用函式就意味著改變後向傳播導數。
請擴充套件網路從兩類輸出到三類輸出。你還需要為此產生一個近似資料集。
將這個網路擴充套件到四層。實驗一下每層的規模。新增另一個隱藏層意味著前向傳播和後向傳播的程式碼都需要調整。
所有的程式碼都在Github上以iPython手冊的形式提供。如有疑問或反饋,請留在評論中。