C++實現神經網路之貳—前向傳播和反向傳播
前言
前一篇文章C++實現神經網路之壹—Net類的設計和神經網路的初始化中,大部分還是比較簡單的。因為最重要事情就是生成各種矩陣並初始化。神經網路中的重點和核心就是本文的內容——前向和反向傳播兩大計算過程。每層的前向傳播分別包含加權求和(卷積?)的線性運算和激活函數的非線性運算。反向傳播主要是用BP演算法更新權值。本文也分為兩部分介紹。
前向過程
如前所述,前向過程分為線性運算和非線性運算兩部分。相對來說比較簡單。
線型運算可以用Y = WX+b
來表示,其中X是輸入樣本,這裡即是第N層的單列矩陣,W是權值矩陣,Y是加權求和之後的結果矩陣,大小與N+1層的單列矩陣相同。b是偏置,默認初始化全部為0。不難推知(鬼知道我推了多久!)
,W的大小是(N+1).rows * N.rows
。正如上一篇中生成weights矩陣的代碼實現一樣:
weights[i].create(layer[i + 1].rows, layer[i].rows, CV_32FC1); n
非線性運算可以用O=f(Y)
來表示。Y就是上面得到的Y。O就是第N+1層的輸出。f就是我們一直說的激活函數。激活函數一般都是非線性函數。它存在的價值就是給神經網路提供非線性建模能力。激活函數的種類有很多,比如sigmoid函數,tanh函數,ReLU函數等。各種函數的優缺點可以參考更為專業的論文和其他更為專業的資料。
我們可以先來看一下前向函數forward()的代碼:
//Forwardn void Net::farward()n {n for (int i = 0; i < layer_neuron_num.size() - 1; ++i)n {n cv::Mat product = weights[i] * layer[i] + bias[i];n layer[i + 1] = activationFunction(product, activation_function);n }n }n
for循環裡面的兩句就分別是上面說的線型運算和激活函數的非線性運算。
激活函數activationFunction()
裡面實現了不同種類的激活函數,可以通過第二個參數來選取用哪一種。代碼如下:
//Activation functionn cv::Mat Net::activationFunction(cv::Mat &x, std::string func_type)n {n activation_function = func_type;n cv::Mat fx;n if (func_type == "sigmoid")n {n fx = sigmoid(x);n }n if (func_type == "tanh")n {n fx = tanh(x);n }n if (func_type == "ReLU")n {n fx = ReLU(x);n }n return fx;n }n
各個函數更為細節的部分在Function.h
和Function.cpp
文件中。在此略去不表,感興趣的請君移步Github
。
需要再次提醒的是,上一篇博客中給出的Net類是精簡過的,下面可能會出現一些上一篇Net類里沒有出現過的成員變數。完整的Net類的定義還是在Github
里。
反向傳播過程
反向傳播原理是鏈式求導法則,其實就是我們高數中學的複合函數求導法則。這只是在推導公式的時候用的到。具體的推導過程我推薦看看下面這一篇教程,用圖示的方法,把前向傳播和反向傳播表現的清晰明了,強烈推薦!
Principles of training multi-layer neural network using backpropagation。
一會將從這一篇文章中截取一張圖來說明權值更新的代碼。在此之前,還是先看一下反向傳播函數backward()的代碼是什麼樣的:
//Forwardn void Net::backward()n {n calcLoss(layer[layer.size() - 1], target, output_error, loss);n deltaError();n updateWeights();n }n
可以看到主要是是三行代碼,也就是調用了三個函數:
- 第一個函數
calcLoss()
計算輸出誤差和目標函數,所有輸出誤差平方和的均值作為需要最小化的目標函數。 - 第二個函數
deltaError()
計算delta誤差,也就是下圖中delta1*df()那部分。 - 第三個函數
updateWeights()
更新權值,也就是用下圖中的公式更新權值。
下面是從前面強烈推薦的文章中截的一張圖:
就看下updateWeights()函數的代碼:
//Update weightsn void Net::updateWeights()n {n for (int i = 0; i < weights.size(); ++i)n {n cv::Mat delta_weights = learning_rate * (delta_err[i] * layer[i].t());n weights[i] = weights[i] + delta_weights;n }n }n
核心的兩行代碼應該還是能比較清晰反映上圖中的那個權值更新的公式的。圖中公式里的eta常被稱作學習率。訓練神經網路調參的時候經常要調節這貨。
計算輸出誤差和delta誤差的部分純粹是數學運算,乏善可陳。但是把代碼貼在下面吧。
calcLoss()
函數在Function.cpp
文件中:
//Objective functionn void calcLoss(cv::Mat &output, cv::Mat &target, cv::Mat &output_error, float &loss)n {n if (target.empty())n {n std::cout << "Cant find the target cv::Matrix" << std::endl;n return;n }n output_error = target - output;n cv::Mat err_sqrare;n pow(output_error, 2., err_sqrare);n cv::Scalar err_sqr_sum = sum(err_sqrare);n loss = err_sqr_sum[0] / (float)(output.rows);n }n
deltaError()
在Net.cpp
中:
//Compute delta errorn void Net::deltaError()n {n delta_err.resize(layer.size() - 1);n for (int i = delta_err.size() - 1; i >= 0; i--)n {n delta_err[i].create(layer[i + 1].size(), layer[i + 1].type());n //cv::Mat dx = layer[i+1].mul(1 - layer[i+1]);n cv::Mat dx = derivativeFunction(layer[i + 1], activation_function);n //Output layer delta errorn if (i == delta_err.size() - 1)n {n delta_err[i] = dx.mul(output_error);n }n else //Hidden layer delta errorn {n cv::Mat weight = weights[i];n cv::Mat weight_t = weights[i].t();n cv::Mat delta_err_1 = delta_err[i];n delta_err[i] = dx.mul((weights[i + 1]).t() * delta_err[i + 1]);n }n }n }n
需要注意的就是計算的時候輸出層和隱藏層的計算公式是不一樣的。
至此,神經網路最核心的部分已經實現完畢。剩下的就是想想該如何訓練了。這個時候你如果願意的話仍然可以寫一個小程序進行幾次前向傳播和反向傳播。還是那句話,鬼知道我在能進行傳播之前到底花了多長時間調試!
源碼鏈接
神經網路源碼的Github鏈接:
LiuXiaolong19920720/simple_net推薦閱讀:
※為什麼說 Python 是數據科學的發動機(一)發展歷程(附視頻中字)
※通俗 Python 設計模式——享元模式
※為什麼Python類成員的調用和聲明必須有"this"?
※python及numpy,pandas易混淆的點
※python2.7的sort函數默認採用什麼排序演算法,適用於怎樣的數列的排序?