引言
既然是要用C++來實現,那么我們自然而然的想到設計一個神經網絡類來表示神經網絡,這里我稱之為Net類。由于這個類名太過普遍,很有可能跟其他人寫的程序沖突,所以我的所有程序都包含在namespace liu中,由此不難想到我姓劉。在之前的博客反向傳播算法資源整理中,我列舉了幾個比較不錯的資源。對于理論不熟悉而且學習精神的同學可以出門左轉去看看這篇文章的資源。這里假設讀者對于神經網絡的基本理論有一定的了解。
一、Net類的設計與神經網絡初始化
神經網絡的要素
在真正開始coding之前還是有必要交代一下神經網絡基礎,其實也就是設計類和寫程序的思路。簡而言之,神經網絡的包含幾大要素:
-
神經元節點
-
層(layer)
-
權值(weights)
-
偏置項(bias)
神經網絡的兩大計算過程分別是前向傳播和反向傳播過程。每層的前向傳播分別包含加權求和(卷積?)的線性運算和激活函數的非線性運算。反向傳播主要是用BP算法更新權值。 雖然里面還有很多細節,但是對于作為第一篇的本文來說,以上內容足夠了。
Net類的設計
Net類——基于Mat
神經網絡中的計算幾乎都可以用矩陣計算的形式表示,這也是我用OpenCV的Mat類的原因之一,它提供了非常完善的、充分優化過的各種矩陣運算方法;另一個原因是我最熟悉的庫就是OpenCV......有很多比較好的庫和框架在實現神經網絡的時候會用很多類來表示不同的部分。比如Blob類表示數據,Layer類表示各種層,Optimizer類來表示各種優化算法。但是這里沒那么復雜,主要還是能力有限,只用一個Net類表示神經網絡。
還是直接讓程序說話,Net類包含在Net.h中,大致如下。
#ifndef NET_H
#define NET_H
#endif // NET_H
#pragma once
#include <iostream>
#include<opencv2\core\core.hpp>
#include<opencv2\highgui\highgui.hpp>
//#include<iomanip>
#include"Function.h"
namespace liu
{class Net{public:std::vector<int> layer_neuron_num;std::vector<cv::Mat> layer;std::vector<cv::Mat> weights;std::vector<cv::Mat> bias;public:Net() {};~Net() {};//Initialize net:genetate weights matrices、layer matrices and bias matrices// bias default all zerovoid initNet(std::vector<int> layer_neuron_num_);//Initialise the weights matrices.void initWeights(int type = 0, double a = 0., double b = 0.1);//Initialise the bias matrices.void initBias(cv::Scalar& bias);//Forwardvoid forward();//Forwardvoid backward();protected://initialise the weight matrix.if type =0,Gaussian.else uniform.void initWeight(cv::Mat &dst, int type, double a, double b);//Activation functioncv::Mat activationFunction(cv::Mat &x, std::string func_type);//Compute delta errorvoid deltaError();//Update weightsvoid updateWeights();};
}
說明
以上不是Net類的完整形態,只是對應于本文內容的一個簡化版,簡化之后看起來會更加清晰明了。
成員變量與成員函數
現在Net類只有四個成員變量,分別是:
-
每一層神經元數目(layer_neuron_num)
-
層(layer)
-
權值矩陣(weights)
-
偏置項(bias)
權值用矩陣表示就不用說了,需要說明的是,為了計算方便,這里每一層和偏置項也用Mat表示,每一層和偏置都用一個單列矩陣來表示。
Net類的成員函數除了默認的構造函數和析構函數,還有:
-
initNet():用來初始化神經網絡
-
initWeights():初始化權值矩陣,調用initWeight()函數
-
initBias():初始化偏置項
-
forward():執行前向運算,包括線性運算和非線性激活,同時計算誤差
-
backward():執行反向傳播,調用updateWeights()函數更新權值。
這些函數已經是神經網絡程序核心中的核心。剩下的內容就是慢慢實現了,實現的時候需要什么添加什么,逢山開路,遇河架橋。
神經網絡初始化
initNet()函數
先說一下initNet()函數,這個函數只接受一個參數——每一層神經元數目,然后借此初始化神經網絡。這里所謂初始化神經網絡的含義是:生成每一層的矩陣、每一個權值矩陣和每一個偏置矩陣。聽起來很簡單,其實也很簡單。
實現代碼在Net.cpp中。
這里生成各種矩陣沒啥難點,唯一需要留心的是權值矩陣的行數和列數的確定。值得一提的是這里把權值默認全設為0。
//Initialize netvoid Net::initNet(std::vector<int> layer_neuron_num_){layer_neuron_num = layer_neuron_num_;//Generate every layer.layer.resize(layer_neuron_num.size());for (int i = 0; i < layer.size(); i++){layer[i].create(layer_neuron_num[i], 1, CV_32FC1);}std::cout << "Generate layers, successfully!" << std::endl;//Generate every weights matrix and biasweights.resize(layer.size() - 1);bias.resize(layer.size() - 1);for (int i = 0; i < (layer.size() - 1); ++i){weights[i].create(layer[i + 1].rows, layer[i].rows, CV_32FC1);//bias[i].create(layer[i + 1].rows, 1, CV_32FC1);bias[i] = cv::Mat::zeros(layer[i + 1].rows, 1, CV_32FC1);}std::cout << "Generate weights matrices and bias, successfully!" << std::endl;std::cout << "Initialise Net, done!" << std::endl;}
權值初始化
initWeight()函數
權值初始化函數initWeights()調用initWeight()函數,其實就是初始化一個和多個的區別。
偏置初始化是給所有的偏置賦相同的值。這里用Scalar對象來給矩陣賦值。
//initialise the weights matrix.if type =0,Gaussian.else uniform.void Net::initWeight(cv::Mat &dst, int type, double a, double b){if (type == 0){randn(dst, a, b);}else{randu(dst, a, b);}}//initialise the weights matrix.void Net::initWeights(int type, double a, double b){//Initialise weights cv::Matrices and biasfor (int i = 0; i < weights.size(); ++i){initWeight(weights[i], 0, 0., 0.1);}}
偏置初始化是給所有的偏置賦相同的值。這里用Scalar對象來給矩陣賦值。
//Initialise the bias matrices.void Net::initBias(cv::Scalar& bias_){for (int i = 0; i < bias.size(); i++){bias[i] = bias_;}}
至此,神經網絡需要初始化的部分已經全部初始化完成了。
初始化測試
我們可以用下面的代碼來初始化一個神經網絡,雖然沒有什么功能,但是至少可以測試下現在的代碼是否有BUG:
#include"../include/Net.h"
//<opencv2\opencv.hpp>
using namespace std;
using namespace cv;
using namespace liu;
int main(int argc, char *argv[])
{//Set neuron number of every layervector<int> layer_neuron_num = { 784,100,10 };// Initialise Net and weightsNet net;net.initNet(layer_neuron_num);net.initWeights(0, 0., 0.01);net.initBias(Scalar(0.05));getchar();return 0;
}
二、前向傳播與反向傳播
在Net類的設計和神經網絡的初始化中,大部分還是比較簡單的。因為最重要事情就是生成各種矩陣并初始化。神經網絡中的重點和核心就是本文的內容——前向和反向傳播兩大計算過程。每層的前向傳播分別包含加權求和(卷積?)的線性運算和激活函數的非線性運算。反向傳播主要是用BP算法更新權值。
前向過程
如前所述,前向過程分為線性運算和非線性運算兩部分。相對來說比較簡單。
線型運算可以用 來表示,其中X是輸入樣本,這里即是第N層的單列矩陣,W是權值矩陣,Y是加權求和之后的結果矩陣,大小與N+1層的單列矩陣相同。b是偏置,默認初始化全部為0。不難推知
(鬼知道我推了多久!)
,W的大小是 。正前面中生成weights矩陣的代碼實現一樣:
weights[i].create(layer[i + 1].rows, layer[i].rows, CV_32FC1);
非線性運算可以用O