C++ 實現backpropagation神經網絡

jopen 9年前發布 | 68K 次閱讀 神經網絡

0x00 神經元與神經網絡

神經網絡是機器學習的一種重要方法,如果不去看具體的數學推導,其整體結構和思想還是很容易理解的。實現起來也很方便。

從總體上看,神經網絡是這樣的樣子 wiki (圖片來自wikipedia)

網絡分為輸入層,隱藏層和輸出層三個部分,每層都有若干神經元節點。

輸入層的神經元只是一個緩沖,把輸入的數據傳遞給隱藏層。而隱藏層和輸出層,則是把前一層的輸出作為自己的輸入并進行計算,可以用下面的公式表示。

y=f(∑wi?xi+θ)

其中xi表示輸入,wi表示輸入的權重,也就是對前一層的每一個神經元的輸出,乘以一個權重進行求和,最后加上一個常數。把結果作為函數f的自變量,f的運算結果就是自己的輸出。而對于函數f,通常取Sigmoid function

f=11+e?x

其圖像為 wiki

(圖片來自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/

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!