今天來講BP神經網絡,神經網絡在機器學習中應用比較廣泛,比如函數逼近,模式識別,分類,數據壓縮,數據
挖掘等領域。接下來介紹BP神經網絡的原理及實現。
Contents
1. BP神經網絡的認識
2. 隱含層的選取
3. 正向傳遞子過程
4. 反向傳遞子過程
5. BP神經網絡的注意點
6. BP神經網絡的C++實現
1. BP神經網絡的認識
BP(Back Propagation)神經網絡分爲兩個過程
(1)工作信號正向傳遞子過程
(2)誤差信號反向傳遞子過程
在BP神經網絡中,單個樣本有個輸入,有
個輸出,在輸入層和輸出層之間通常還有若干個隱含層。實際
上,1989年Robert Hecht-Nielsen證明了對於任何閉區間內的一個連續函數都可以用一個隱含層的BP網
絡來逼近,這就是萬能逼近定理。所以一個三層的BP網絡就可以完成任意的維到
維的映射。即這三層分
別是輸入層(I),隱含層(H),輸出層(O)。如下圖示
2. 隱含層的選取
在BP神經網絡中,輸入層和輸出層的節點個數都是確定的,而隱含層節點個數不確定,那麼應該設置爲多少
才合適呢?實際上,隱含層節點個數的多少對神經網絡的性能是有影響的,有一個經驗公式可以確定隱含層
節點數目,如下
其中爲隱含層節點數目,
爲輸入層節點數目,
爲輸出層節點數目,
爲
之間的調節常數。
3. 正向傳遞子過程
現在設節點和節點
之間的權值爲
,節點
的閥值爲
,每個節點的輸出值爲
,而每個節點的輸出
值是根據上層所有節點的輸出值、當前節點與上一層所有節點的權值和當前節點的閥值還有激活函數來實現
的。具體計算方法如下
其中爲激活函數,一般選取S型函數或者線性函數。
正向傳遞的過程比較簡單,按照上述公式計算即可。在BP神經網絡中,輸入層節點沒有閥值。
4. 反向傳遞子過程
在BP神經網絡中,誤差信號反向傳遞子過程比較複雜,它是基於Widrow-Hoff學習規則的。假設輸出層
的所有結果爲,誤差函數如下
而BP神經網絡的主要目的是反覆修正權值和閥值,使得誤差函數值達到最小。Widrow-Hoff學習規則
是通過沿着相對誤差平方和的最速下降方向,連續調整網絡的權值和閥值,根據梯度下降法,權值矢量
的修正正比於當前位置上E(w,b)的梯度,對於第個輸出節點有
假設選擇激活函數爲
對激活函數求導,得到
那麼接下來針對有
其中有
同樣對於有
這就是著名的學習規則,通過改變神經元之間的連接權值來減少系統實際輸出和期望輸出的誤差,這個規
則又叫做Widrow-Hoff學習規則或者糾錯學習規則。
上面是對隱含層和輸出層之間的權值和輸出層的閥值計算調整量,而針對輸入層和隱含層和隱含層的閥值調
整量的計算更爲複雜。假設是輸入層第k個節點和隱含層第i個節點之間的權值,那麼有
其中有
這樣對學習規則理解更爲深刻了吧。
有了上述公式,根據梯度下降法,那麼對於隱含層和輸出層之間的權值和閥值調整如下
而對於輸入層和隱含層之間的權值和閥值調整同樣有
至此BP神經網絡的原理基本講完。
5. BP神經網絡的注意點
BP神經網絡一般用於分類或者逼近問題。如果用於分類,則激活函數一般選用Sigmoid函數或者硬極限函
數,如果用於函數逼近,則輸出層節點用線性函數,即。
BP神經網絡在訓練數據時可以採用增量學習或者批量學習。
增量學習要求輸入模式要有足夠的隨機性,對輸入模式的噪聲比較敏感,即對於劇烈變化的輸入模式,訓
練效果比較差,適合在線處理。批量學習不存在輸入模式次序問題,穩定性好,但是隻適合離線處理。
標準BP神經網絡的缺陷:
(1)容易形成局部極小值而得不到全局最優值。
BP神經網絡中極小值比較多,所以很容易陷入局部極小值,這就要求對初始權值和閥值有要求,要使
得初始權值和閥值隨機性足夠好,可以多次隨機來實現。
(2)訓練次數多使得學習效率低,收斂速度慢。
(3)隱含層的選取缺乏理論的指導。
(4)訓練時學習新樣本有遺忘舊樣本的趨勢。
BP算法的改進:
(1)增加動量項
引入動量項是爲了加速算法收斂,即如下公式
動量因子一般選取
。
(2)自適應調節學習率
(3)引入陡度因子
通常BP神經網絡在訓練之前會對數據歸一化處理,即將數據映射到更小的區間內,比如[0,1]或[-1,1]。
6. BP神經網絡的C++實現
BP神經網絡的C++文件如下
BP.h:
- #ifndef _BP_H_
- #define _BP_H_
- #include <vector>
- #define LAYER 3 //三層神經網絡
- #define NUM 10 //每層的最多節點數
- #define A 30.0
- #define B 10.0 //A和B是S型函數的參數
- #define ITERS 1000 //最大訓練次數
- #define ETA_W 0.0035 //權值調整率
- #define ETA_B 0.001 //閥值調整率
- #define ERROR 0.002 //單個樣本允許的誤差
- #define ACCU 0.005 //每次迭代允許的誤差
- #define Type double
- #define Vector std::vector
- struct Data
- {
- Vector<Type> x; //輸入數據
- Vector<Type> y; //輸出數據
- };
- class BP{
- public:
- void GetData(const Vector<Data>);
- void Train();
- Vector<Type> ForeCast(const Vector<Type>);
- private:
- void InitNetWork(); //初始化網絡
- void GetNums(); //獲取輸入、輸出和隱含層節點數
- void ForwardTransfer(); //正向傳播子過程
- void ReverseTransfer(int); //逆向傳播子過程
- void CalcDelta(int); //計算w和b的調整量
- void UpdateNetWork(); //更新權值和閥值
- Type GetError(int); //計算單個樣本的誤差
- Type GetAccu(); //計算所有樣本的精度
- Type Sigmoid(const Type); //計算Sigmoid的值
- private:
- int in_num; //輸入層節點數
- int ou_num; //輸出層節點數
- int hd_num; //隱含層節點數
- Vector<Data> data; //輸入輸出數據
- Type w[LAYER][NUM][NUM]; //BP網絡的權值
- Type b[LAYER][NUM]; //BP網絡節點的閥值
- Type x[LAYER][NUM]; //每個神經元的值經S型函數轉化後的輸出值,輸入層就爲原值
- Type d[LAYER][NUM]; //記錄delta學習規則中delta的值
- };
- #endif //_BP_H_
BP.cpp:
- #include <string.h>
- #include <stdio.h>
- #include <math.h>
- #include <assert.h>
- #include "BP.h"
- //獲取訓練所有樣本數據
- void BP::GetData(const Vector<Data> _data)
- {
- data = _data;
- }
- //開始進行訓練
- void BP::Train()
- {
- printf("Begin to train BP NetWork!\n");
- GetNums();
- InitNetWork();
- int num = data.size();
- for(int iter = 0; iter <= ITERS; iter++)
- {
- for(int cnt = 0; cnt < num; cnt++)
- {
- //第一層輸入節點賦值
- for(int i = 0; i < in_num; i++)
- x[0][i] = data.at(cnt).x[i];
- while(1)
- {
- ForwardTransfer();
- if(GetError(cnt) < ERROR) //如果誤差比較小,則針對單個樣本跳出循環
- break;
- ReverseTransfer(cnt);
- }
- }
- printf("This is the %d th trainning NetWork !\n", iter);
- Type accu = GetAccu();
- printf("All Samples Accuracy is %lf\n", accu);
- if(accu < ACCU) break;
- }
- printf("The BP NetWork train End!\n");
- }
- //根據訓練好的網絡來預測輸出值
- Vector<Type> BP::ForeCast(const Vector<Type> data)
- {
- int n = data.size();
- assert(n == in_num);
- for(int i = 0; i < in_num; i++)
- x[0][i] = data[i];
- ForwardTransfer();
- Vector<Type> v;
- for(int i = 0; i < ou_num; i++)
- v.push_back(x[2][i]);
- return v;
- }
- //獲取網絡節點數
- void BP::GetNums()
- {
- in_num = data[0].x.size(); //獲取輸入層節點數
- ou_num = data[0].y.size(); //獲取輸出層節點數
- hd_num = (int)sqrt((in_num + ou_num) * 1.0) + 5; //獲取隱含層節點數
- if(hd_num > NUM) hd_num = NUM; //隱含層數目不能超過最大設置
- }
- //初始化網絡
- void BP::InitNetWork()
- {
- memset(w, 0, sizeof(w)); //初始化權值和閥值爲0,也可以初始化隨機值
- memset(b, 0, sizeof(b));
- }
- //工作信號正向傳遞子過程
- void BP::ForwardTransfer()
- {
- //計算隱含層各個節點的輸出值
- for(int j = 0; j < hd_num; j++)
- {
- Type t = 0;
- for(int i = 0; i < in_num; i++)
- t += w[1][i][j] * x[0][i];
- t += b[1][j];
- x[1][j] = Sigmoid(t);
- }
- //計算輸出層各節點的輸出值
- for(int j = 0; j < ou_num; j++)
- {
- Type t = 0;
- for(int i = 0; i < hd_num; i++)
- t += w[2][i][j] * x[1][i];
- t += b[2][j];
- x[2][j] = Sigmoid(t);
- }
- }
- //計算單個樣本的誤差
- Type BP::GetError(int cnt)
- {
- Type ans = 0;
- for(int i = 0; i < ou_num; i++)
- ans += 0.5 * (x[2][i] - data.at(cnt).y[i]) * (x[2][i] - data.at(cnt).y[i]);
- return ans;
- }
- //誤差信號反向傳遞子過程
- void BP::ReverseTransfer(int cnt)
- {
- CalcDelta(cnt);
- UpdateNetWork();
- }
- //計算所有樣本的精度
- Type BP::GetAccu()
- {
- Type ans = 0;
- int num = data.size();
- for(int i = 0; i < num; i++)
- {
- int m = data.at(i).x.size();
- for(int j = 0; j < m; j++)
- x[0][j] = data.at(i).x[j];
- ForwardTransfer();
- int n = data.at(i).y.size();
- for(int j = 0; j < n; j++)
- ans += 0.5 * (x[2][j] - data.at(i).y[j]) * (x[2][j] - data.at(i).y[j]);
- }
- return ans / num;
- }
- //計算調整量
- void BP::CalcDelta(int cnt)
- {
- //計算輸出層的delta值
- for(int i = 0; i < ou_num; i++)
- d[2][i] = (x[2][i] - data.at(cnt).y[i]) * x[2][i] * (A - x[2][i]) / (A * B);
- //計算隱含層的delta值
- for(int i = 0; i < hd_num; i++)
- {
- Type t = 0;
- for(int j = 0; j < ou_num; j++)
- t += w[2][i][j] * d[2][j];
- d[1][i] = t * x[1][i] * (A - x[1][i]) / (A * B);
- }
- }
- //根據計算出的調整量對BP網絡進行調整
- void BP::UpdateNetWork()
- {
- //隱含層和輸出層之間權值和閥值調整
- for(int i = 0; i < hd_num; i++)
- {
- for(int j = 0; j < ou_num; j++)
- w[2][i][j] -= ETA_W * d[2][j] * x[1][i];
- }
- for(int i = 0; i < ou_num; i++)
- b[2][i] -= ETA_B * d[2][i];
- //輸入層和隱含層之間權值和閥值調整
- for(int i = 0; i < in_num; i++)
- {
- for(int j = 0; j < hd_num; j++)
- w[1][i][j] -= ETA_W * d[1][j] * x[0][i];
- }
- for(int i = 0; i < hd_num; i++)
- b[1][i] -= ETA_B * d[1][i];
- }
- //計算Sigmoid函數的值
- Type BP::Sigmoid(const Type x)
- {
- return A / (1 + exp(-x / B));
- }
Test.cpp:
- #include <iostream>
- #include <string.h>
- #include <stdio.h>
- #include "BP.h"
- using namespace std;
- double sample[41][4]=
- {
- {0,0,0,0},
- {5,1,4,19.020},
- {5,3,3,14.150},
- {5,5,2,14.360},
- {5,3,3,14.150},
- {5,3,2,15.390},
- {5,3,2,15.390},
- {5,5,1,19.680},
- {5,1,2,21.060},
- {5,3,3,14.150},
- {5,5,4,12.680},
- {5,5,2,14.360},
- {5,1,3,19.610},
- {5,3,4,13.650},
- {5,5,5,12.430},
- {5,1,4,19.020},
- {5,1,4,19.020},
- {5,3,5,13.390},
- {5,5,4,12.680},
- {5,1,3,19.610},
- {5,3,2,15.390},
- {1,3,1,11.110},
- {1,5,2,6.521},
- {1,1,3,10.190},
- {1,3,4,6.043},
- {1,5,5,5.242},
- {1,5,3,5.724},
- {1,1,4,9.766},
- {1,3,5,5.870},
- {1,5,4,5.406},
- {1,1,3,10.190},
- {1,1,5,9.545},
- {1,3,4,6.043},
- {1,5,3,5.724},
- {1,1,2,11.250},
- {1,3,1,11.110},
- {1,3,3,6.380},
- {1,5,2,6.521},
- {1,1,1,16.000},
- {1,3,2,7.219},
- {1,5,3,5.724}
- };
- int main()
- {
- Vector<Data> data;
- for(int i = 0; i < 41; i++)
- {
- Data t;
- for(int j = 0; j < 3; j++)
- t.x.push_back(sample[i][j]);
- t.y.push_back(sample[i][3]);
- data.push_back(t);
- }
- BP *bp = new BP();
- bp->GetData(data);
- bp->Train();
- while(1)
- {
- Vector<Type> in;
- for(int i = 0; i < 3; i++)
- {
- Type v;
- scanf("%lf", &v);
- in.push_back(v);
- }
- Vector<Type> ou;
- ou = bp->ForeCast(in);
- printf("%lf\n", ou[0]);
- }
- return 0;
- }
Makefile:
- Test : BP.h BP.cpp Test.cpp
- g++ BP.cpp Test.cpp -o Test
- clean:
- rm Test