純用NumPy實(shí)現(xiàn)神經(jīng)網(wǎng)絡(luò)的示例代碼
摘要: 純NumPy代碼從頭實(shí)現(xiàn)簡單的神經(jīng)網(wǎng)絡(luò)。
Keras、TensorFlow以及PyTorch都是高級別的深度學(xué)習(xí)框架,可用于快速構(gòu)建復(fù)雜模型。前不久,我曾寫過一篇文章,對神經(jīng)網(wǎng)絡(luò)是如何工作的進(jìn)行了簡單的講解。該文章側(cè)重于對神經(jīng)網(wǎng)絡(luò)中運(yùn)用到的數(shù)學(xué)理論知識進(jìn)行詳解。本文將利用NumPy實(shí)現(xiàn)簡單的神經(jīng)網(wǎng)絡(luò),在實(shí)戰(zhàn)中對其進(jìn)行深層次剖析。最后,我們會利用分類問題對模型進(jìn)行測試,并與Keras所構(gòu)建的神經(jīng)網(wǎng)絡(luò)模型進(jìn)行性能的比較。
Note:源碼可在我的GitHub中查看。
在正式開始之前,需要先對所做實(shí)驗(yàn)進(jìn)行構(gòu)思。我們想要編寫一個程序,使其能夠創(chuàng)建一個具有指定架構(gòu)(層的數(shù)量、大小以及激活函數(shù))的神經(jīng)網(wǎng)絡(luò),如圖一所示。總之,我們需要預(yù)先對網(wǎng)絡(luò)進(jìn)行訓(xùn)練,然后利用它進(jìn)行預(yù)測。
上圖展示了神經(jīng)網(wǎng)絡(luò)在被訓(xùn)練時的工作流程。從中我們可以清楚的需要更新的參數(shù)數(shù)量以及單次迭代的不同狀態(tài)。構(gòu)建并管理正確的數(shù)據(jù)架構(gòu)是其中最困難的一環(huán)。由于時間限制,圖中所示的參數(shù)不會一一詳解,有興趣可點(diǎn)擊此處進(jìn)行了解。
神經(jīng)網(wǎng)絡(luò)層的初始化
首先,對每一層的權(quán)重矩陣W及偏置向量b進(jìn)行初始化。在上圖中,上標(biāo)[l]表示目前是第幾層(從1開始),n的值表示一層中的神經(jīng)元數(shù)量。描述神經(jīng)網(wǎng)絡(luò)架構(gòu)的信息類似于Snippet 1中所列內(nèi)容。每一項都描述了單層神經(jīng)網(wǎng)絡(luò)的基本參數(shù):input_dim,即輸入層神經(jīng)元維度;output_dim,即輸出層神經(jīng)元維度;activation,即使用的激活函數(shù)。
nn_architecture = [ {"input_dim": 2, "output_dim": 4, "activation": "relu"}, {"input_dim": 4, "output_dim": 6, "activation": "relu"}, {"input_dim": 6, "output_dim": 6, "activation": "relu"}, {"input_dim": 6, "output_dim": 4, "activation": "relu"}, {"input_dim": 4, "output_dim": 1, "activation": "sigmoid"}, ]
Snippet 1.
從Snippet 1可看出,每一層輸出神經(jīng)元的維度等于下一層的輸入維度。對權(quán)重矩陣W及偏置向量b進(jìn)行初始化的代碼如下:
def init_layers(nn_architecture, seed = 99): np.random.seed(seed) number_of_layers = len(nn_architecture) params_values = {} for idx, layer in enumerate(nn_architecture): layer_idx = idx + 1 layer_input_size = layer["input_dim"] layer_output_size = layer["output_dim"] params_values['W' + str(layer_idx)] = np.random.randn( layer_output_size, layer_input_size) * 0.1 params_values['b' + str(layer_idx)] = np.random.randn( layer_output_size, 1) * 0.1 return params_values
Snippet 2.
在本節(jié)中,我們利用NumPy將權(quán)重矩陣W及偏置向量b初始化為小的隨機(jī)數(shù)。特別注意的是,初始化權(quán)重值不能相同,否則網(wǎng)絡(luò)會變?yōu)閷ΨQ的。也就是說,如果權(quán)重初始化為同一值,則對于任何輸入X,每個隱藏層對應(yīng)的每個神經(jīng)元的輸出都是相同的,這樣即使梯度下降訓(xùn)練,無論訓(xùn)練多少次,這些神經(jīng)元都是對稱的,無論隱藏層內(nèi)有多少個結(jié)點(diǎn),都相當(dāng)于在訓(xùn)練同一個函數(shù)。
初始化的值較小能夠使得算法第一次迭代的時候效率更高。Sigmoid函數(shù)圖像如下圖所示,它對中央?yún)^(qū)的信號增益較大,對兩側(cè)區(qū)的信號增益小。
激活函數(shù)(Activation functions)
激活函數(shù)在神經(jīng)網(wǎng)絡(luò)中至關(guān)重要,其原理簡單但功能強(qiáng)大,給神經(jīng)元引入了非線性因素,使得神經(jīng)網(wǎng)絡(luò)可以任意逼近任何非線性函數(shù),從而應(yīng)用于眾多的非線性模型?!叭绻麤]有激活函數(shù),每一層輸出都是上層輸入的線性函數(shù),無論神經(jīng)網(wǎng)絡(luò)有多少層,輸出都是輸入的線性組合?!奔せ詈瘮?shù)種類眾多,本文選取了最常用的兩種——ReLU及Sigmoid函數(shù),代碼如下:
def sigmoid(Z): return 1/(1+np.exp(-Z)) def relu(Z): return np.maximum(0,Z) def sigmoid_backward(dA, Z): sig = sigmoid(Z) return dA * sig * (1 - sig) def relu_backward(dA, Z): dZ = np.array(dA, copy = True) dZ[Z <= 0] = 0; return dZ;
Snippet 3.
前向傳播算法(Forward propagation)
本文所設(shè)計的神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu)簡單,信息流只有一個方向:以X矩陣的形式傳遞,穿過所有隱藏層單元,最終輸出預(yù)測結(jié)構(gòu)Y_hat。
def single_layer_forward_propagation(A_prev, W_curr, b_curr, activation="relu"): Z_curr = np.dot(W_curr, A_prev) + b_curr if activation is "relu": activation_func = relu elif activation is "sigmoid": activation_func = sigmoid else: raise Exception('Non-supported activation function') return activation_func(Z_curr), Z_curr
Snippet 4.
前向傳播就是上層處理完的數(shù)據(jù)作為下一層的輸入數(shù)據(jù),然后進(jìn)行處理(權(quán)重),再傳給下一層,這樣逐層處理,最后輸出。給定上一層的輸入信號,計算仿射變換(affine transformation)Z,然后應(yīng)用選定的激活函數(shù)。
前向傳播算法代碼如下,該函數(shù)不僅進(jìn)行預(yù)測計算,還存儲中間層A和Z矩陣的值:
def full_forward_propagation(X, params_values, nn_architecture): memory = {} A_curr = X for idx, layer in enumerate(nn_architecture): layer_idx = idx + 1 A_prev = A_curr activ_function_curr = layer["activation"] W_curr = params_values["W" + str(layer_idx)] b_curr = params_values["b" + str(layer_idx)] A_curr, Z_curr = single_layer_forward_propagation(A_prev, W_curr, b_curr, activ_function_curr) memory["A" + str(idx)] = A_prev memory["Z" + str(layer_idx)] = Z_curr return A_curr, memory
Snippet 5.
損失函數(shù)(Loss function)
損失函數(shù)是用來估量模型的預(yù)測值與真實(shí)值的不一致程度,它是一個非負(fù)實(shí)值函數(shù)。損失函數(shù)由我們想要解決的問題所決定。在本文中,我們想要測試神經(jīng)網(wǎng)絡(luò)模型區(qū)分兩個類別的能力,所以選擇了交叉熵?fù)p失函數(shù)(binary_crossentropy),其定義如下:
為了更加清楚的了解學(xué)習(xí)過程,我增添了一個用于計算精度的函數(shù):
def get_cost_value(Y_hat, Y): m = Y_hat.shape[1] cost = -1 / m * (np.dot(Y, np.log(Y_hat).T) + np.dot(1 - Y, np.log(1 - Y_hat).T)) return np.squeeze(cost) def get_accuracy_value(Y_hat, Y): Y_hat_ = convert_prob_into_class(Y_hat) return (Y_hat_ == Y).all(axis=0).mean()
Snippet 6.
反向傳播算法(Backward propagation)
許多缺乏經(jīng)驗(yàn)的深度學(xué)習(xí)愛好者認(rèn)為反向傳播是一種復(fù)雜且難以理解的算法。
def single_layer_backward_propagation(dA_curr, W_curr, b_curr, Z_curr, A_prev, activation="relu"): m = A_prev.shape[1] if activation is "relu": backward_activation_func = relu_backward elif activation is "sigmoid": backward_activation_func = sigmoid_backward else: raise Exception('Non-supported activation function') dZ_curr = backward_activation_func(dA_curr, Z_curr) dW_curr = np.dot(dZ_curr, A_prev.T) / m db_curr = np.sum(dZ_curr, axis=1, keepdims=True) / m dA_prev = np.dot(W_curr.T, dZ_curr) return dA_prev, dW_curr, db_curr
Snippet 7.
其實(shí),他們困惑的也就是反向傳播算法中的梯度下降問題,但二者并不可混為一談。前者旨在有效地計算梯度,而后者是利用計算得到的梯度進(jìn)行優(yōu)化。梯度下降可以應(yīng)對帶有明確求導(dǎo)函數(shù)的情況,我們可以把它看作沒有隱藏層的網(wǎng)絡(luò);但對于多隱藏層的神經(jīng)網(wǎng)絡(luò),應(yīng)先將誤差反向傳播至隱藏層,然后再應(yīng)用梯度下降,其中將誤差從最末層往前傳遞的過程需要鏈?zhǔn)椒▌t,反向傳播算法可以說是梯度下降在鏈?zhǔn)椒▌t中的應(yīng)用。對于單層的神經(jīng)網(wǎng)絡(luò),該過程如下所示:
本文省略的推導(dǎo)過程,但從上面的公式仍可看出A和Z矩陣值的重要性。
Snippet 7中所示代碼僅編寫了神經(jīng)網(wǎng)絡(luò)中某層的反向傳播算法,Snippet 8將展示神經(jīng)網(wǎng)絡(luò)中完整的反向傳播算法。
def full_backward_propagation(Y_hat, Y, memory, params_values, nn_architecture): grads_values = {} m = Y.shape[1] Y = Y.reshape(Y_hat.shape) dA_prev = - (np.divide(Y, Y_hat) - np.divide(1 - Y, 1 - Y_hat)); for layer_idx_prev, layer in reversed(list(enumerate(nn_architecture))): layer_idx_curr = layer_idx_prev + 1 activ_function_curr = layer["activation"] dA_curr = dA_prev A_prev = memory["A" + str(layer_idx_prev)] Z_curr = memory["Z" + str(layer_idx_curr)] W_curr = params_values["W" + str(layer_idx_curr)] b_curr = params_values["b" + str(layer_idx_curr)] dA_prev, dW_curr, db_curr = single_layer_backward_propagation( dA_curr, W_curr, b_curr, Z_curr, A_prev, activ_function_curr) grads_values["dW" + str(layer_idx_curr)] = dW_curr grads_values["db" + str(layer_idx_curr)] = db_curr return grads_values
Snippet 8.
參數(shù)更新(Updating parameters values)
該部分旨在利用計算得到梯度更新網(wǎng)絡(luò)中的參數(shù),同時最小化目標(biāo)函數(shù)。我們會使用到params_values,它存放當(dāng)前的參數(shù)值,以及grads_values,它存放存儲關(guān)于這些參數(shù)的損失函數(shù)的導(dǎo)數(shù)?,F(xiàn)在只需要在神經(jīng)網(wǎng)絡(luò)的每層應(yīng)用如下公式即可:
def update(params_values, grads_values, nn_architecture, learning_rate): for layer_idx, layer in enumerate(nn_architecture): params_values["W" + str(layer_idx)] -= learning_rate * grads_values["dW" + str(layer_idx)] params_values["b" + str(layer_idx)] -= learning_rate * grads_values["db" + str(layer_idx)] return params_values;
Snippet 9.
整合(Putting things together)
現(xiàn)在我們只需將準(zhǔn)備好的函數(shù)按照正確的順序整合到一起,若對正確的順序有疑問請參見圖2。
def train(X, Y, nn_architecture, epochs, learning_rate): params_values = init_layers(nn_architecture, 2) cost_history = [] accuracy_history = [] for i in range(epochs): Y_hat, cashe = full_forward_propagation(X, params_values, nn_architecture) cost = get_cost_value(Y_hat, Y) cost_history.append(cost) accuracy = get_accuracy_value(Y_hat, Y) accuracy_history.append(accuracy) grads_values = full_backward_propagation(Y_hat, Y, cashe, params_values, nn_architecture) params_values = update(params_values, grads_values, nn_architecture, learning_rate) return params_values, cost_history, accuracy_history
Snippet 10.
對比分析(David vs Goliath)
接下來,我們將利用所構(gòu)建的模型解決簡單的分類問題。如圖7所示,本次實(shí)驗(yàn)使用的數(shù)據(jù)集包含兩個類別。我們將訓(xùn)練模型對兩個不同的類別進(jìn)行區(qū)分。此外,我們還準(zhǔn)備了一個由Keras所構(gòu)建的神經(jīng)網(wǎng)絡(luò)模型以進(jìn)行對比。兩個模型具有相同的架構(gòu)和學(xué)習(xí)速率。雖然我們的模型很簡單,但結(jié)果表明,NumPy和Keras模型在測試集上均達(dá)到了95%的準(zhǔn)確率。只是我們的模型耗費(fèi)了更多的時間,未來工作可通過加強(qiáng)優(yōu)化改善時間開銷問題。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Python使用os模塊實(shí)現(xiàn)更高效地讀寫文件
os是python標(biāo)準(zhǔn)庫,包含幾百個函數(shù)常用路徑操作、進(jìn)程管理、環(huán)境參數(shù)等好多類。本文將使用os模塊實(shí)現(xiàn)更高效地讀寫文件,感興趣的可以學(xué)習(xí)一下2022-07-07詳解如何使用python實(shí)現(xiàn)猜數(shù)字游戲
“猜數(shù)字”游戲是一款簡單而有趣的小游戲,玩家需要在給定的范圍內(nèi)猜出一個由計算機(jī)隨機(jī)生成的數(shù)字,本文將使用Python語言來實(shí)現(xiàn)這款游戲,并詳細(xì)介紹其實(shí)現(xiàn)過程,文中有詳細(xì)的代碼示例供大家參考,需要的朋友可以參考下2024-04-04Python數(shù)值方法及數(shù)據(jù)可視化
這篇文章主要介紹了Python數(shù)值方法及數(shù)據(jù)可視化,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價值,需要的小伙伴可以參考一下2022-09-09Python實(shí)現(xiàn)的企業(yè)粉絲抽獎功能示例
這篇文章主要介紹了Python實(shí)現(xiàn)的企業(yè)粉絲抽獎功能,涉及Python數(shù)值運(yùn)算與隨機(jī)數(shù)生成相關(guān)操作技巧,需要的朋友可以參考下2019-07-07pyTorch深入學(xué)習(xí)梯度和Linear Regression實(shí)現(xiàn)
這篇文章主要介紹了pyTorch深入學(xué)習(xí),實(shí)現(xiàn)梯度和Linear Regression,文中呈現(xiàn)了詳細(xì)的示例代碼,有需要的朋友可以借鑒參考下,希望能夠有所幫助2021-09-09python爬蟲將js轉(zhuǎn)化成json實(shí)現(xiàn)示例
這篇文章主要為大家介紹了python爬蟲將js轉(zhuǎn)化成json實(shí)現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05