C++ 實現backpropagation神經網絡
0x00 神經元與神經網絡
神經網絡是機器學習的一種重要方法,如果不去看具體的數學推導,其整體結構和思想還是很容易理解的。實現起來也很方便。
從總體上看,神經網絡是這樣的樣子 (圖片來自wikipedia)
網絡分為輸入層,隱藏層和輸出層三個部分,每層都有若干神經元節點。
輸入層的神經元只是一個緩沖,把輸入的數據傳遞給隱藏層。而隱藏層和輸出層,則是把前一層的輸出作為自己的輸入并進行計算,可以用下面的公式表示。
y=f(∑wi?xi+θ)
其中xi表示輸入,wi表示輸入的權重,也就是對前一層的每一個神經元的輸出,乘以一個權重進行求和,最后加上一個常數。把結果作為函數f的自變量,f的運算結果就是自己的輸出。而對于函數f,通常取Sigmoid function
f=11+e?x
其圖像為
(圖片來自wikipedia)
也就是把輸入映射到(0,1)這樣的一個區間內。
0x01 backpropagation反向傳播算法
神經網絡是一種有監督的學習算法。對于訓練集中的元素,我們已經知道了輸入和對應的輸出。輸入的數據經過網絡以后得到的輸出會與應有的輸出進行比較,據此可以對神經元的權重進行調整。 定義誤差
e=∑(xi?xo)2
其中xo表示實際的輸出,xi應該的輸出
為了使得誤差快速的減小,一般的方法是讓所有的變量都沿著梯度的方向下降。對于輸出層可以直接求出梯度,而對于中間層也可以通過鏈式法則求出。本文重點在于實現,不做具體的數學推導。 這里有一篇很形象的文章說明了具體的過程。
0x02 實現
1.定義神經元結構體
由最開始的圖可以知道,每個神經元都有輸入和輸出,而權重則是兩個神經元之間的關系,可以保存在輸入端也可以保存在輸出端。這里采用了保存在輸入節點的方式。
struct Node{ vector < double > weights; double output, grid,old_grid; Node(){ output = grid = old_grid = 0.0; } };
weights表示前一層的每一個節點作為輸入對該神經元的權重,output是輸出,grid和oldgrid則用來對權重進行修正,其中oldgrid與歷史修正有關。
2.定義層
神經網絡中的一個層是由一些神經元組成的,就是結構圖中的一列,簡單定如下。
typedef vector<Node> Layer;
3.定義網絡
一個神經網絡保存了若干個層,包括輸入層,隱藏層和中間層。同時提供了正向和方向接口來訓練。
class Net{ public: Net(const vector<int>& shape); void front(const vector<double>& input); static double sig(const double& x) ; void backprop(const vector<double>& target); Layer getResult() const; private: static double eta,alpha; vector<Layer> net; };
構造函數接受一個vector來描述網絡的形狀,vecrtor中的每一個元素代表了每一層有多少個神經元,這樣就能唯一的確定網絡的結構。
Net::Net(const vector<int>& shape){
int n = shape.size();//總的層數 for (int i = 0; i < n; i++){ net.push_back(Layer()); for (int j = 0; j <= shape[i]; j++){//構造第i層的所有神經元 net.back().push_back(Node()); if (i > 0){ net[i][j].weights = vector < double > (shape[i - 1]+1); for (auto& val : net[i][j].weights){ val = rand()(rand()%2?1:-1)1.0/RAND_MAX; } } }} for (int i = 0; i < n; i++){ net[i].back().output = 1; }
}</pre>
考慮到公式中的常數項θ,在每一層的最后都加了一個固定輸出為1的神經元。同時將權重都置成[?1,1]區間的一個數隨機數。
4.前向過程
為了計算每個神經元的輸出,先定義sigmod函數,也就是上面說的 y=11+e?x
double Net::sig(const double& x){ return 1.0 / (1.0 + exp(-x)); }而前向操作的定義如下
void Net::front(const vector<double>& input){ //將輸入層每個節點的輸出設置為網絡的輸入 for (int i = 0; i < net[0].size() - 1; i++){ net[0][i].output = input[i]; } int n = net.size(); //對于隱藏層和輸出層 for (int i = 1; i < n; i++){ //對于層中每一個節點(除了最后一個固定輸出唯一的節點) for (int j = 0; j < net[i].size() - 1; j++){ double& res = net[i][j].output; res = 0.0; //對前一層的輸出和權重的乘積求和 for (int k = 0; k < net[i - 1].size(); k++){ res += net[i - 1][k].output * net[i][j].weights[k]; } res = sig(res); } } }也就是輸入層簡單的把輸入傳遞進來。隱藏層和輸出層根據前一層的輸出和權重計算自己的輸出。
5.反向傳播
而反向傳播分為三個循環。
void Net::backprop(const vector<double>& target){
//計算每一個輸出節點的誤差 for (int i = 0; i < target.size(); i++){ Node& node = net.back()[i]; node.grid = target[i] - node.output; } //修正輸出層的權重 for (int i = net.size() - 2; i > 0; i--){ Layer& cur_layer = net[i]; Layer& nex_layer = net[i + 1]; Layer& prv_layer = net[i - 1]; for (int j = 0; j < cur_layer.size()-1; j++){ Node& node = cur_layer[j]; node.grid = 0; for (int k = 0; k < nex_layer.size() - 1; k++){ node.grid += nex_layer[k].grid nex_layer[k].weights[j]; } } } //修正隱藏層的權重 for (int i = 1; i < net.size(); i++){ Layer& layer = net[i]; Layer& prev = net[i - 1]; for (int j = 0; j < layer.size(); j++){ Node& node = layer[j]; double d = node.output (1 - node.output); for (int k = 0; k < node.weights.size(); k++){node.weights[k] += (eta * node.grid + alpha * node.old_grid) * d * prev[k].output; } node.old_grid = node.grid; } }
}</pre>
在函數的第一個循環,計算了每一個輸出節點的誤差。 在第二個循環,計算了所有輸出層神經元的梯度并且更新了權重值。 在第三個循環則更新了隱藏層的權重。 這里不僅考慮了當前的梯度grid還考慮了上一次的梯度。他們分別與常數eta和alpha相乘。
double Net::eta = 1; double Net::alpha = 0.3;這兩個常數控制了學習的速率。如果太小可能會收斂的很慢,如果太大會導致不穩定。
6.獲取結果。
很簡單,直接返回輸出層。
Layer Net::getResult()const { return net.back(); }0x03 驗證
這里采取異或網絡對剛才的程序進行驗證。
(圖片來自http://www.52ml.net/15427.html)
由于異或(XOR)的結果不是線性可分的。然而用神經網絡只需要一個中間層就可以很容易的解決這一個問題。
srand( time(NULL) ); vector<int> shape{ 2, 3, 1 }; Net net(shape); int p, q; for (int i = 0; i < 20000; i++){ p = rand() % 2; q = rand() % 2; vector<double> input{ (double)p, (double)q }; net.front(input); int res = p^q; vector<double> tag{ (double)res }; net.backprop(tag); } for (int i = 0; i < 4; i++){ p = i % 2; q = i / 2; vector<double> input{ (double)p, (double)q }; net.front(input); int res = p^q; Layer& result = net.getResult(); cout << i << ": " << "p = " << p << ", q = " << q << ", res = " << res << " , result = " << result[0].output << " , dis = " << abs(result[0].output - res) << endl; }程序首先初始化一個神經網絡,每次隨機取值為0或1的兩個數p,q并計算結果res = p xor q,將輸入和結果送給網絡進行學習。最終對學習的結果進行檢驗,輸出如下
0: p = 0, q = 0, res = 0 , result = 0.0013878 , dis = 0.0013878 1: p = 1, q = 0, res = 1 , result = 0.982337 , dis = 0.0176629 2: p = 0, q = 1, res = 1 , result = 0.98569 , dis = 0.0143104 3: p = 1, q = 1, res = 0 , result = 0.0218435 , dis = 0.0218435可以看到誤差已經低到完全可以接受的水平。
0x04 參考
一個很好的視頻教程 https://vimeo.com/19569529
來自:http://www.functor.me/cplusplus-implementon-of-bp-network/