python中forward的作用_基于numpy的前馈神经网络(feedforward neural network)
***
這幾天在上Andrew Ng教授開的Coursera系列課程Deep Learning,總覺得光是看視頻和做作業還不夠,還是得自己動手寫寫代碼,親自實現課程里提到的算法內容,于是便有了這篇博客,作為自己入門深度學習的里程碑吧。
前饋神經網絡
機器學習有兩個基本問題,一是回歸,二是分類,神經網絡大多用于解決分類問題,前饋神經網絡(feedforward neural network)是整個神經網絡家族中較為常見和較為基礎的一種,如下圖右上角的DFF所示。圖片來源是Cheat Sheets for AI, Neural Networks, Machine Learning, Deep Learning & Big Data。
神經網絡中的基本元素是神經元,每層都有一定數量的神經元,神經元組合的多樣性決定了神經網絡的豐富性。下面是一個簡單的前饋神經網絡,總共有三層,從左到右分別是輸入層、隱層和輸出層,輸入層的x1和x2表示這個樣本只有兩個特征(自變量),因為輸入層通常不計入內,所以這是一個兩層的神經網絡,第一層有4個神經元,第二層只有1個。注意,隱層可以不止一層,隱層設置得越多,整個神經網絡越龐大。
這個神經網絡的工作原理是,給定一個樣本的數據,將數據傳輸到第一層,進行線性變換和激活變換,得到加工過后的數據,這份新數據傳到第二層,作為第二層的輸入,接著進行線性變換和激活變換,又得到一份新的數據,因為第二層是最后一層了(如果不止兩層就一直進行這樣的操作直到抵達最后一層為止),所以最終的輸出作為我們對該樣本的預測值y_hat。
每一個神經元如同工廠的流水車間的機器,它重復做著四件事情:【接受上一層數據作為輸入>>線性變換>>激活變換>>輸出數據到下一層】,每個神經元中有三個組成部分:權重(weight)矩陣W,偏置(bias)向量b,以及激活函數(activation function) g,用公式表達為下圖,其中上標(i)表示這是第i個樣本數據,上標[1]和[2]分別表示這是神經網絡的第一層與第二層:
公式(1)中,通過簡單的線性變換得到了z[1],z稱為prev_activation,接著通過激活變換(這里用的激活函數是tanh函數,下面會講到),得到了a[1],稱為activation,公式(3)(4)表達的是第二層的線性變換和激活變換,和第一層大同小異,只不過第二層的激活函數用的不是tanh函數而是sigmoid函數。
激活函數有下面三種,都是執行了非線性變換,實現的效果都是將prev_activation轉化為activation。
每一個神經元都可以從上面三種激活函數中選取一個作為自己的激活函數g,經驗表明,使用tanh函數的效果總是碾壓使用sigmoid函數,所以人們大多使用tanh作為激活函數,近年來人們發現了relu函數,發現它的性能比tanh更好,relu成為了廣受歡迎的激活函數。既然sigmoid性能最差,為什么還要介紹它?在一開始的時候說到,神經網絡通常用于分類,比方說,給定一張圖片,去識別預測它是不是一只貓:
我們的返回值應該是范圍在0~1之間的概率值,sigmoid的函數范圍是(0, 1), tanh的范圍是(-1, 1), relu的是[0, +∞),使用sigmoid顯然更合適些。所以通常一個神經網絡的配置是,中間的隱層的所有神經元使用tanh或者relu作為激活函數,輸出層的神經元使用sigmoid。
之所以要在線性變換之后進行非線性變化,是因為,如果沒有非線性變換,純粹使用線性變化的話,不管使用了多少層的線性變換,最終的結果通過合并同類項之后仍然是線性變換,100層的神經網絡和1層的沒有任何差別。神經網絡從本質上來說就是一系列的非線性變換。
講完了基本概念,我們現在的問題是,如何訓練這個神經網絡。
和其他的監督學習方法一樣,神經網絡同樣是需要定義一個損失函數(cost function),通過對它進行最小化,得到最優的參數W和b,即每個神經元的權重矩陣和偏置向量。神經網絡用的損失函數是交叉熵:
其中m為樣本總數,y_hat為最終預測值,是通過每一層的神經元進行層層加工處理得到的,L表示整個神經網絡的層數,即[L]表示最后一層:
優化損失函數的方法是梯度下降法,梯度下降的原理這里就不說了,就貼一下參數的更新公式:
以前在看書的時候,看到神經網絡部分,總是在說“神經網絡訓練使用的是反向傳播(back propagation)算法”,一直搞不明白這四個字究竟是什么意思,現在總算是有些理解了。
既然有反向傳播,就有正向傳播,所謂正向,就是沿著神經網絡從輸入層到輸出層,就是沿著每個神經元執行【接受上一層數據作為輸入>>線性變換>>激活變換>>輸出數據到下一層】的方向,就是下圖中從左到右的方向,而反向傳播,指的是從輸出層到輸入層,即從右到左。
所謂的傳播,其實就是把整個神經網絡運算的過程抽象成了流程圖的形式。正向傳播時,數據通過第一層神經元,被處理成了新的數據,傳輸到下一層,再加工成新數據,再傳到下一層,這便是數據的傳播,而在反向傳播時,傳播的是梯度,從最后一層(輸出層)出發,首先根據正向傳播得到的預測值,計算得出最后一個神經元處的A(activation)的梯度,進而得到Z(prev_activation)的梯度,根據公式再算出W, b以及倒數第二層的A的梯度,就像多米諾骨牌,不斷進行下去,直到將梯度傳播到第一層的神經網絡處。
在正向傳播時,每個神經元執行了兩步操作,線性變化和激活變換,線性變換是由W[l], b[l], A[l-1]得到了z[l],激活變化是由z[l]得到了A[l];而在反向傳播時,在每個神經元中,根據相應的激活函數由dA[l]得到dz[l],再由dz[l]得到dW[l], db[l], dA[l-1],這可以看做是線性變化和激活變換的反向操作。
由此得到了每一層的神經元的W和b的梯度,參數W和b也由此得以更新,反向傳播時傳播的是梯度,也可以理解為傳播的是誤差,是當前模型的不足,參數W和b借助這個信息來進行修正,從而優化模型。于是整個神經網絡就按照下圖的流程進行迭代,直到達到理想的模型效果:
所以整個神經網絡的操作流程是這樣的:
確定神經網絡的層數和每層的神經元數
初始化參數W和b
進行循環迭代
前向傳播,得到預測值
計算損失函數
反向傳播,得到參數W和b的梯度
對參數W和b進行更新
對新數據做預測
代碼實現
課程里有給出前饋神經網絡的python代碼,但是我覺得寫得過于繁瑣(簡單的幾步操作居然寫了300多行?),太注重形式,太刻意地去遵循代碼規范,反倒增加了閱讀負擔,不太合我心意。
于是我按照自己對算法的理解寫了一份代碼,進行了精簡,只保留最重要的部分,并且加入了L2正則化的部分。為了測試代碼的正確性,這里使用的神經網絡的層數和神經元數和Coursera上的是一樣的,共有四層,每層的神經元數量分別是20/ 7/ 5/ 1,用的數據也是課程的數據。
下面進入代碼部分。
首先要調用numpy。為什么要使用numpy?因為不想使用一層又一層的for循環,使用向量化的計算能夠大幅度地降低運算時間。
準備好激活函數,隱層統一使用relu,輸出層使用sigmoid:
def sigmoid(z):
'''
z為prev_activation, size為 nl * m
'''
return 1 / (1 + np.exp(-z))
def relu(z):
'''
z為prev_activation, size為 nl * m
'''
return np.maximum(0, z)
設置好神經網絡的結構:
n0, m = X.shape
n1 = 20
n2 = 7
n3 = 5
n4 = 1
layers_dims = [n0, n1, n2, n3, n4] # [12288, 20, 7, 5, 1]
L = len(layers_dims) - 1 # 4層神經網絡,不計輸入層
構建神經網絡模型:
##### neural network model
def neural_network(X, Y, learning_rate=0.01, num_iterations=2000, lambd=0):
m = X.shape[1]
### initialize forward propagation
param_w = [i for i in range(L+1)]
param_b = [i for i in range(L+1)]
np.random.seed(10)
for l in range(1, L+1):
if l < L:
param_w[l] = np.random.randn(layers_dims[l], layers_dims[l - 1]) * np.sqrt(2 / layers_dims[l - 1])
if l == L:
param_w[l] = np.random.randn(layers_dims[l], layers_dims[l - 1]) * 0.01
param_b[l] = np.zeros((layers_dims[l], 1))
activations = [X, ] + [i for i in range(L)]
prev_activations = [i for i in range(L+1)]
dA = [i for i in range(L+1)]
dz = [i for i in range(L+1)]
dw = [i for i in range(L+1)]
db = [i for i in range(L+1)]
for i in range(num_iterations):
### forward propagation
for l in range(1, L+1):
prev_activations[l] = np.dot(param_w[l], activations[l-1]) + param_b[l]
if l < L:
activations[l] = relu(prev_activations[l])
else:
activations[l] = sigmoid(prev_activations[l])
cross_entropy_cost = -1/m * (np.dot(np.log(activations[L]), Y.T) \
+ np.dot(np.log(1-activations[L]), 1-Y.T))
regularization_cost = 0
for l in range(1, L+1):
regularization_cost += np.sum(np.square(param_w[l])) * lambd/(2*m)
cost = cross_entropy_cost + regularization_cost
### initialize backward propagation
dA[L] = np.divide(1-Y, 1-activations[L]) - np.divide(Y, activations[L])
assert dA[L].shape == (1, m)
### backward propagation
for l in reversed(range(1, L+1)):
if l == L:
dz[l] = dA[l] * activations[l] * (1-activations[l])
else:
dz[l] = dA[l].copy()
dz[l][prev_activations[l] <= 0] = 0
dw[l] = 1/m * np.dot(dz[l], activations[l-1].T) + param_w[l] * lambd/m
db[l] = 1/m * np.sum(dz[l], axis=1, keepdims=True)
dA[l-1] = np.dot(param_w[l].T, dz[l])
assert dz[l].shape == prev_activations[l].shape
assert dw[l].shape == param_w[l].shape
assert db[l].shape == param_b[l].shape
assert dA[l-1].shape == activations[l-1].shape
param_w[l] = param_w[l] - learning_rate * dw[l]
param_b[l] = param_b[l] - learning_rate * db[l]
if i % 100 == 0:
print("cost after iteration {}: {}".format(i, cost))
Andrew Ng教授是用一個dict來保存每層神經元的參數的,比如說,在調用第三層的參數W3和b3時,他的寫法是:parameters['W' + str(3)]和parameters['b' + str(3)],這樣寫沒有錯誤,雖然直觀,但是很麻煩,我的做法是,分別使用list來保存W和b,根據位置讀取,對應上面的就是param_w[3]和param_b[3]。
注意到,python(以及其他編程語言)是從0開始計數的,而不是從1開始,這意味著,當神經網絡共有4層的時候,我的param_w和param_b的長度是5,我對activation、prev_activation以及各個梯度dA/dz/dw/db都是用長度為5的list來保存的,而Ng教授記錄這些變量的長度都是4。
我認為我的寫法是更容易理解的寫法,因為整個神經網絡包括輸入層在內一共有5層,但是因為習慣上我們不計輸入層,所以這是個4層網絡,如果我們把輸入層稱為第0層,輸出層稱為第4層,沒有什么問題,并且恰好符合了編程語言從0開始計數的習慣。所以在我的寫法下,activations[3]就表示第三層的輸出,param_b[2]表示第二層的偏置向量,不會有什么誤解,很直觀。
而對于Ng教授的做法,獲取每個參數時是基于字符串的,比如說grads["db" + str(4)]表示第四層的b的梯度,而在用for循環遍歷每一層神經元的時候,又是基于位置的,這么一來,你就會在+1 和 -1之后迷失自我,即便你確保了自己沒有出錯,你也已經花費了不少精力來判斷這個地方究竟是該+1還是-1還是保持原樣。
下面是我在做Ng教授布置的編程作業時,需要填寫的反向傳播的部分,要求我在里面填入5行代碼來完成,這導致了我在 l 和 l+1和 l-1三者之間糾結猶豫了很久才終于填寫正確。我個人認為不是一段user-friendly的代碼。
for l in reversed(range(L - 1)):
# lth layer: (RELU -> LINEAR) gradients.
# Inputs: "grads["dA" + str(l + 1)], current_cache".
# Outputs: "grads["dA" + str(l)] , grads["dW" + str(l + 1)] , grads["db" + str(l + 1)]
### START CODE HERE ### (approx. 5 lines)
current_cache = caches[l]
dA_prev_temp, dW_temp, db_temp = linear_activation_backward(grads['dA' + str(l + 1)], current_cache, activation="relu")
grads["dA" + str(l)] = dA_prev_temp
grads["dW" + str(l + 1)] = dW_temp
grads["db" + str(l + 1)] = db_temp
### END CODE HERE ###
Ng教授的代碼中,對于不同的子函數,層數L時而等于5,時而等于4,很是混亂,而我的層數L始終等于4,符合python從0開始計數的習慣,使得整個算法寫起來很快,也很好理解,一看就明白當前迭代到了哪一層。
另外,在寫算法的時候,要重點關注每個參數的size,注意看它是幾乘幾的矩陣,很多時候出bug都來自于參數的size弄錯了,下面是各參數的size表,里面的12288和209表示我們使用的訓練集的數據有209個樣本(圖片),每個樣本有12288個特征(每張圖片是64像素*64像素的,所以有64*64*3=12288)。
關于size的規律是,梯度和參數的size相同,同一層的W和dW的size相同,b和db的size相同,其實也很好理解,看參數更新的公式就知道了;另外,同一層的A/z/dA/dz的size都是相同的,這個要記住。可以這么理解,原數據的size是p×m,即m個p維的樣本(注意在神經網絡中,每一列代表一個樣本,和其他機器學習方法中每一行代表一個樣本是反過來的),經過每一層的神經元處理之后,得到了新的數據,可以理解為是降維的過程,由一開始的12288×209的數據,通過第一層后變為了20×209的數據,即把高維的12288個特征濃縮為20個新特征,再濃縮為7個特征,最后得到了1×209的預測值。
最后是預測函數,將訓練好的神經網絡用于一套新的數據上:
def predict(X_new, parameters, threshold=0.5):
param_w = parameters["param_w"]
param_b = parameters["param_b"]
activations = [X_new, ] + [i for i in range(L)]
prev_activations = [i for i in range(L + 1)]
m = X_new.shape[1]
for l in range(1, L + 1):
prev_activations[l] = np.dot(param_w[l], activations[l - 1]) + param_b[l]
if l < L:
activations[l] = relu(prev_activations[l])
else:
activations[l] = sigmoid(prev_activations[l])
prediction = (activations[L] > threshold).astype("int")
return prediction
經驗與收獲
這是我第一次用python寫一個具體的算法,雖然一直在python做數據分析,但是一直沒有寫過一個邏輯完整的、能應用于實際場合的算法(自己動手寫完整算法的經歷,在今天之前,是用R語言寫了逐步回歸和隨機森林),今天算是填補了我在python上的這塊空白。
使用assert語句來確保每個參數的shape正確,能夠減少出現bug的幾率
numpy中有一些操作和函數是element-wise的,要留意
整個算法沒一會兒就寫完了,但是訓練的時候cost一直沒有收斂,檢查了老半天,才發現是由兩個問題導致的:
relu的導函數寫錯了,返回值是0與1沒錯,但是我用來判斷的參數竟然是A而不是z,真是寫得莫名其妙;
初始化不正確(我后來才發現初始化是神經網絡的關鍵),對于權重W我一直使用的寫法是param_w[l] = np.random.randn(layers_dims[l], layers_dims[l - 1]) * 0.01,導致cost沒能實現收斂,應該是由vanishing gradients導致的,后來我使用了He初始化和Xavier初始化,即把上面的0.01改成np.sqrt(2 / layers_dims[l - 1])或者np.sqrt(1 / layers_dims[l - 1]),效果明顯。
怎么知道自己的最終算法寫對了?我和課程使用的是同一套數據,如果使用我的算法,在相同的神經網絡結構下,和Ng教授使用的算法得到的精確度差不多,就說明沒問題了。
比起寫代碼,理解代碼背后的算法內容更重要。
總結
以上是生活随笔為你收集整理的python中forward的作用_基于numpy的前馈神经网络(feedforward neural network)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 常用并发工具类(锁和线程间通信工具类)
- 下一篇: Python IO 基本编程示例