Bp神經網路+C++實現
0 前言
神經網路在我印象中一直比較神祕,正好最近學習了神經網路,特別是對Bp神經網路有了比較深入的瞭解,因此,總結以下心得,希望對後來者有所幫助。
神經網路在機器學習中應用比較廣泛,比如函式逼近,模式識別,分類,資料壓縮,資料探勘等領域。神經網路本身是一個比較龐大的概念,從網路結構類別來劃分,大概有:多層前饋神經網路、徑向基函式網路(RBF)、自適應諧振理論網路(ART)、自組織對映網路(SOM)、級聯相關網路、Elman網路、Boltzmann機、受限Boltzmann機等等。
下面一張圖是最近比較流行的網路結構:
今天我們要介紹的是Bp神經網路,準確的說是採用Bp演算法進行訓練的多層前饋神經網路,Bp演算法應用比較廣泛。
1基本概念:
1.1神經元模型
機器學習中所談論的神經網路源於生物上的神經網路,實際上指的是“神經網路“與”機器學習“的交叉部分,一個最簡單的M-P神經元模型如下圖所示:
該神經元收到來自其他n個輸入神經元傳遞過來的輸入訊號(加權和的形式),然後將其與神經元的閾值進行比較,通過啟用函式進行處理,產生神經元的輸出。
1.2 常用啟用函式
啟用函式的作用是對其他所有神經元傳過來的所有訊號加權和進行處理,產生神經元輸出。
下圖是常用的啟用函式,最簡單的是:階躍函式,它簡單,最理想,但是性質最差(不連續/不光滑),因此在實際中,最常用的是Sigmoid函式。
1.3前饋神經網路
多層前饋神經網路的準確定義:每一層神經元與下一層神經元全互連,神經元之間不存在同層連線,不存在跨層連線,如下圖所示就是一個經典的前饋神經網路,
(隨便插一句,當神經網路中隱層數越來越多,達到8-9層時,就變成了一個深度
學習模型,我曾在一篇論文中看到網路結構有達128層的,關於下面這塊,下面還會再敘述)。
2.標準Bp演算法
2.0 關於梯度
首先我們應該清楚,一個多元函式的梯度方向是該函式值增大最陡的方向。具體化到1元函式中時,梯度方向首先是沿著曲線的切線的,然後取切線向上增長的方向為梯度方向,2元或者多元函式中,梯度向量為函式值f對每個變數的導數,該向量的方向就是梯度的方向,當然向量的大小也就是梯度的大小。 梯度下降法(steepest descend method)用來求解表示式最大或者最小值的,屬於無約束優化問題。梯度下降法的基本思想還是挺簡單的,現假設我們要求函式f的最小值,首先得選取一個初始點後,然後下一個點的產生時是沿著梯度直線方向,這裡是沿著梯度的反方向(因為求的是最小值,如果是求最大值的話則沿梯度的正方向即可),如下圖所示:
2.1神經網路學習過程
神經網路在外界輸入樣本的刺激下不斷改變網路的連線權值,以使網路的輸出不斷地接近期望的輸出,講幾個要點:
(1)學習過程可以簡述為:
(2)學習的本質: 對各連線權值以及所有功能神經元的閾值動態調整
注:可以將權值與閾值的學習統一為權值的學習,即將閾值看成一個”啞節點“,如下圖所示:
(3) 權值調整規則:即在學習過程中網路中各神經元的連線權變化所依據的一定的調整規則 ,(Bp 演算法中權值調整採用的是梯度下降策略,下面會詳細介紹)
Bp網路的學習流程如下圖所示:
(百度相簿裡搜的,能說明問題就行)
2.2權值調整策略:
首先說明一句,神經網路學習屬於監督學習的範疇。每輸入一個樣本,進行正向傳播(輸入層→隱層→輸出層),得到輸出結果以後,計算誤差,達不到期望後,將誤差進行反向傳播(輸出層→隱層→輸入層),採用梯度下降策略對所有權值和閾值進行調整。
注:上面的Ek是根據第k個樣本資料算出的誤差,可以看出:標準Bp演算法每次迭代更新只針對單個樣例。
權值與閾值的調整公式如下:
上述公式的詳細推導過程見下圖:
2.3 BP神經網路總結
(1)BP神經網路一般用於分類或者逼近問題。
如果用於分類,則啟用函式一般選用Sigmoid函式或者硬極限函式,如果用於函式逼近,則輸出層節點用線性函式。
(2) BP神經網路在訓練資料時可以採用增量學習或者批量學習。
—增量學習要求輸入模式要有足夠的隨機性,對輸入模式的噪聲比較敏感,即對於劇烈變化的輸入模式,訓練效果比較差,適合線上處理。
—批量學習不存在輸入模式次序問題,穩定性好,但是隻適合離線處理。
(3)如何確定隱層數以及每個隱含層節點個數
Pre隱含層節點個數不確定,那麼應該設定為多少才合適呢(隱含層節點個數的多少對神經網路的效能是有影響的)?
有一個經驗公式可以確定隱含層節點數目: ,(其中h:隱含層節點數目,m:為輸入層節點數目,n:為輸出層節點數目,a:為之間的調節常數)。
2.4 標準BP神經網路的缺陷
(1)容易形成區域性極小值而得不到全域性最優值。
(採用梯度下降法),如果僅有一個區域性極小值=>全域性最小,多個區域性極小=>不一定全域性最小。這就要求對初始權值和閥值有要求,要使得初始權值和閥值隨機性足夠好,可以多次隨機來實現。
(2)訓練次數多使得學習效率低,收斂速度慢。
每次更新只針對單個樣本;不同樣例出現”抵消“現象。
(3)過擬合問題
通過不斷訓練,訓練誤差達到很低,但測試誤差可能會上升(泛化效能差)。
解決策略:
1,”早停”:
即將樣本劃分成訓練集和驗證集,訓練集用來算梯度,更新權值和閾值,驗證集用來估計誤差,當訓練集誤差降低而驗證集誤差升高時就停止訓練,返回具有最小驗證集誤差的權值和閾值。
2,”正則化方法“:,即在誤差目標中增加一個用於描述網路複雜程度的部分,其中引數λ常用交叉驗證來確定。
2.5 BP演算法的改進
(1)累積BP演算法
目的:為了減小整個訓練集的全域性誤差,而不針對某一特定樣本
(更新策略做相應調整)
(2)利用動量法改進BP演算法
(標準Bp學習過程易震盪,收斂速度慢)
增加動量項,引入動量項是為了加速演算法收斂,即如下公式:
α為動量係數,通常0<α<0.9。
(3)自適應調節學習率η
調整的基本指導思想是:在學習收斂的情況下,增大η,以縮短學習時間;當η偏大致使不能收斂(即發生震盪)時,要及時減小η,直到收斂為止。
3 工程搭建與C++實現
實驗平臺:vs2013
專案包含檔案:
專案流程如下圖所示:
(1)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>);
void ForCastFromFile(BP * &);
void ReadFile(const char * InutFileName,int m, int n);
void ReadTestFile(const char * InputFileName, int m, int n);
void WriteToFile(const char * OutPutFileName);
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的值
void split(char *buffer, Vector<Type> &vec);
private:
int in_num; //輸入層節點數
int ou_num; //輸出層節點數
int hd_num; //隱含層節點數
Vector<Data> data; //樣本資料
Vector<Vector<Type>> testdata;//測試資料
Vector<Vector<Type>> result; //測試結果
int rowLen; //樣本數量
int restrowLen; //測試樣本數量
Type w[LAYER][NUM][NUM]; //BP網路的權值
Type b[LAYER][NUM]; //BP網路節點的閥值
Type x[LAYER][NUM]; //每個神經元的值經S型函式轉化後的輸出值,輸入層就為原值
Type d[LAYER][NUM]; //記錄delta學習規則中delta的值,使用delta規則來調整聯接權重 Wij(t+1)=Wij(t)+α(Yj-Aj(t))Oi(t)
};
#endif //_BP_H_
(2)Bp.cpp
#include <string.h>
#include <stdio.h>
#include <math.h>
#include <assert.h>
#include <cstdlib>
#include <fstream>
#include <iostream>
using namespace std;
#include "Bp.h"
//獲取訓練所有樣本資料
void BP::GetData(const Vector<Data> _data)
{
data = _data;
}
void BP::split(char *buffer, Vector<Type> &vec)
{
char *p = strtok(buffer, " ,"); //\t
while (p != NULL)
{
vec.push_back(atof(p));
p = strtok(NULL, " \n");
}
}
void BP::ReadFile(const char * InutFileName, int m ,int n)
{
FILE *pFile;
//Test
//pFile = fopen("D:\\testSet.txt", "r");
pFile = fopen(InutFileName, "r");
if (!pFile)
{
printf("open file %s failed...\n", InutFileName);
exit(0);
}
//init dataSet
char *buffer = new char[100];
Vector<Type> temp;
while (fgets(buffer, 100, pFile))
{
Data t;
temp.clear();
split(buffer, temp);
//data[x].push_back(temp);
for (int i = 0; i < temp.size(); i++)
{
if (i < m)
t.x.push_back(temp[i]);
else
t.y.push_back(temp[i]);
}
data.push_back(t);
}
//init rowLen
rowLen = data.size();
}
void BP::ReadTestFile(const char * InputFileName, int m, int n)
{
FILE *pFile;
pFile = fopen(InputFileName, "r");
if (!pFile)
{
printf("open file %s failed...\n", InputFileName);
exit(0);
}
//init dataSet
char *buffer = new char[100];
Vector<Type> temp;
while (fgets(buffer, 100, pFile))
{
Vector<Type> t;
temp.clear();
split(buffer, temp);
for (int i = 0; i < temp.size(); i++)
{
t.push_back(temp[i]);
}
testdata.push_back(t);
}
restrowLen = testdata.size();
}
void BP::WriteToFile(const char * OutPutFileName)
{
ofstream fout;
fout.open(OutPutFileName);
if (!fout)
{
cout << "file result.txt open failed" << endl;
exit(0);
}
Vector<Vector<Type>> ::iterator it = testdata.begin();
Vector<Vector<Type>>::iterator itx = result.begin();
while (it != testdata.end())
{
Vector<Type> ::iterator itt = (*it).begin();
Vector<Type> ::iterator ittx = (*itx).begin();
while (itt != (*it).end())
{
fout << (*itt) << ",";
itt++;
}
fout << "\t";
while (ittx != (*itx).end())
{
fout << (*ittx) << ",";
ittx++;
}
it++;
itx++;
fout << "\n";
}
}
//開始進行訓練
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(); //每一輪學習的均方誤差E
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::ForCastFromFile(BP * &pBp)
{
Vector<Vector<Type>> ::iterator it = testdata.begin();
Vector<Type> ou;
while (it != testdata.end())
{
ou = pBp->ForeCast(*it);
result.push_back(ou);
it++;
}
}
//獲取網路節點數
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]);//對第i個樣本算均方誤差
}
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));
}
(3)Test.cpp
#include <iostream>
#include <string.h>
#include <stdio.h>
using namespace std;
#include "Bp.h"
int main()
{
unsigned int Id, Od; //樣本資料的輸入維數/輸出維數
int select = 0;
BP *bp = new BP();
const char * inputDataName = "exercisedata.txt";//訓練資料檔名稱
const char * testDataName = "testdata.txt"; //測試資料檔名稱
const char * outputDataName = "result.txt"; //輸出檔名稱
printf("please input sample input dimension and output dimension:\n");
scanf("%d%d", &Id, &Od);
bp->ReadFile(inputDataName,Id,Od);
//exercise
bp->Train();
//Test
printf("\n******************************************************\n");
printf("*1.使用測試檔案中國的資料測試 2.從控制檯輸入資料測試 \n");
printf("******************************************************\n");
scanf("%d", &select);
switch (select)
{
case 1:
bp->ReadTestFile(testDataName,Id,Od);
bp->ForCastFromFile(bp);
bp->WriteToFile(outputDataName);
printf("the result have been save in the file :result.txt.\n");
break;
case 2:
printf("\n\nplease input the Test Data(3 dimension ):\n");
while (1)
{
Vector<Type> in;
for (int i = 0; i < Id; i++)
{
Type v;
scanf_s("%lf", &v);
in.push_back(v);
}
Vector<Type> ou;
ou = bp->ForeCast(in);
printf("%lf\n", ou[0]);
}
break;
default:
printf("Input error!");
exit(0);
}
return 0;
}