這個學期要學DM&ML,用的是《數據挖掘算法原理與實現》王振武 本着造福同學的思想,開一個DM&ML的筆記系列,打算給書上的源代碼添加一點註釋,方便閱讀和理解。
前置知識要求
C++
關鍵點
1.源碼中關於閾值的存儲和相關訪問是越界的,本註釋版本已進行了修改
2.學習和理解的關鍵點要看懂值在不同層之間的傳遞,尤其是誤差的向後傳遞,需要看懂書上的式子。
3.大白話版本:BP網絡由輸入層,若干隱含層,輸出層組成,相鄰兩層之間的不同結點連接起來的邊有權值。數據每經過一個節點,就被節點上的“激發函數”所修改,最終流到輸出層。BP網絡就是通過不斷的計算數據在層間的流動,得到誤差,修正邊上的權值,以達到“學習”和擬合的效果。
具體實現
#include<iostream>/*hiro:修改一下頭文件,去掉.h,下面的fstream一樣*/
#include<math.h>
#include<stdlib.h>
#include<time.h>
#include<fstream>
//---------------------------------------------------------------------
#define RANDOM rand()/32767.0 //0~1隨機數生成函數
const int Layer_Max = 5;//神經網絡的層數
const double PI = 3.1415927;//圓周率
const int Layer_number[Layer_Max] = { 2, 4, 4, 2, 1 }; //神經網絡各層的神經元個數
const int Neural_Max = 4;//神經網絡各層最大神經元個數
const int InMax = 21;//樣本輸入的個數
std::ofstream Out_W_File("All_W.txt", std::ios::out);/*hiro:添加命名空間*/
std::ofstream Out_Error("Error.txt", std::ios::out);
/*hiro:添加命名空間,*/
using namespace std;
//定義類 BP
/*hiro:這個BP網絡貌似沒有設定每個結點的閾值*/
class BP
{
public:
BP(); //BP類的構造函數
void BP_Print();//打印權係數
double F(double x);//神經元的激發函數
double Y(double x1, double x2);//要逼近的函數
//
double NetWorkOut(int x1, int x2);//網絡輸出,他的輸入爲第input個樣本
void AllLayer_D(int x1, int x2);//求所有神經元的輸出誤差微分
void Change_W(); //改變權係數
void Train(); //訓練函數
void After_Train_Out(); //經過訓練後,21樣本的神經網絡輸出
double Cost(double out, double Exp);//代價函數
private:
//保存權係數
//規定W[i][j][k]表示網絡第i層的第j個神經元連接到第i-1層第k個神經元的權係數
/*hiro:它的閾值貌似也存在了W[][][]裏,,,
具體是保存在W[][][Layer_number[i - 1]]裏
你確定這樣沒有越界???*/
double W[Layer_Max][Neural_Max][Neural_Max];
/*hiro:添加的存儲閾值的私有成員
取締原有的存儲在W的越界做法,修改相關的所有修改*/
double Q[Layer_Max][Neural_Max];
//21個樣本輸入,
//約定Input_Net[0][i]表示第i個樣本的輸入x1
//而 Input_Net[1][i]表示第i個樣本的輸入x2
double Input_Net[2][InMax];
double Out_Exp[InMax][InMax];//期望輸出
//保存各神經元的輸出
//規定Layer_Node[i][j]表示第i層的第j個神經元的輸出
double Layer_Node[Layer_Max][Neural_Max];
/*保存各神經元的誤差微分
規定D[i][j]表示第i層第j個神經元的誤差微分*/
double D[Layer_Max][Neural_Max];//
double Study_Speed;//學習速度
double e;//誤差
};
//構造函數,用來初始化權係數,輸入,期望輸出和學習速度
BP::BP()
{
srand(time(NULL));//播種,以便產生隨即數
for (int i = 1; i<Layer_Max; i++)
{
for (int j = 0; j<Layer_number[i]; j++)
{
/*hiro:↓↓↓!!!越界了!!!
Layer_number[i - 1] + 1
改爲Layer_number[i - 1]*/
for (int k = 0; k<Layer_number[i - 1] ; k++)
{
W[i][j][k] = RANDOM;//隨機初始化權係數
}
Q[i][j] = RANDOM ;//初始化各神經元的閥值
/*hiro:那啥,我特地查了一下,閾值是正確的版本
閥值是錯字版本來着,,*/
}
}
//輸入和輸出歸一化
for (int l = 0; l<InMax; l++)
{
Input_Net[0][l] = l * 0.05;//把0~1分成21等分,表示x1
Input_Net[1][l] = 1 - l * 0.05;//表示x2
}
for (int i = 0; i<InMax; i++)/*hiro:i忘記定義,,*/
{
for (int j = 0; j<InMax; j++)
{
Out_Exp[i][j] = Y(Input_Net[0][i], Input_Net[1][j]);//期望輸出
Out_Exp[i][j] = Out_Exp[i][j] / 3.000000;//期望輸出歸一化
}
}
Study_Speed = 0.5;//初始化學習速度
e = 0.0001;//誤差精度
}//end
//激發函數F()
double BP::F(double x)
{
/*hiro:1/(1+e^-x)*/
return(1.0 / (1 + exp(-x)));
}//end
//要逼近的函數Y()
//輸入:兩個浮點數
//輸出:一個浮點數
/*hiro:f(a,b)=(a-1)^4+2*(b^2)*/
double BP::Y(double x1, double x2)
{
double temp;
temp = pow(x1 - 1, 4) + 2 * pow(x2, 2);
return temp;
}//end
//--------------------------------------------------------
//代價函數
/*hiro:用方差衡量*/
double BP::Cost(double Out, double Exp)
{
return(pow(Out - Exp, 2));
}//end
//網絡輸出函數
//輸入爲:第input個樣本
double BP::NetWorkOut(int x1, int x2)
{
int i, j, k;
//約定N_node[i][j]表示網絡第i層的第j個神經元的總輸入
//第0層的神經元爲輸入,不用權係數和閥值,即輸進什麼即輸出什麼
double N_node[Layer_Max][Neural_Max];
/*hiro:因爲這個BP網絡結構裏第0層有兩個神經元
所以是[0][1],[0][0],反正數量少,稍微偷懶一下也
沒有太大問題,只是本書的代碼都不怎麼一般化*/
N_node[0][0] = Input_Net[0][x1];
Layer_Node[0][0] = Input_Net[0][x1];
N_node[0][1] = Input_Net[1][x2];
Layer_Node[0][1] = Input_Net[1][x2];
for (i = 1; i<Layer_Max; i++)//神經網絡的第i層
{
for (j = 0; j<Layer_number[i]; j++)//Layer_number[i]爲第i層的神經元個數
{
N_node[i][j] = 0.0;
//Layer_number[i-1]表示與第i層第j個神經元連接的上一層的神經元個數
for (k = 0; k<Layer_number[i - 1]; k++)
{
//求上一層神經元對第i層第j個神經元的輸入之和
N_node[i][j] += Layer_Node[i - 1][k] * W[i][j][k];
}
/*hiro:不用原來的W來存儲閾值,*/
//N_node[i][j] = N_node[i][j] - W[i][j][k];//減去閥值
/*hiro:修改後↓*/
N_node[i][j] -= Q[i][j];
//求Layer_Node[i][j],即第i層第j個神經元的輸出
Layer_Node[i][j] = F(N_node[i][j]);
}
}
return Layer_Node[Layer_Max - 1][0];//最後一層的輸出
}//end
//求所有神經元的輸出誤差微分函數
//輸入爲:第input個樣本
//計算誤差微分並保存在D[][]數組中
/*hiro:待確認*/
void BP::AllLayer_D(int x1, int x2)
{
int i, j, k;
double temp;
/*hiro:P129 式子6-27
*/
D[Layer_Max - 1][0] =Layer_Node[Layer_Max - 1][0] *
(1 - Layer_Node[Layer_Max - 1][0])*
(Layer_Node[Layer_Max - 1][0] - Out_Exp[x1][x2]);
for (i = Layer_Max - 1; i>0; i--)
{
for (j = 0; j<Layer_number[i - 1]; j++)
{
temp = 0;
for (k = 0; k<Layer_number[i]; k++)
{
temp = temp + W[i][k][j] * D[i][k];
}
D[i - 1][j] =Layer_Node[i - 1][j] * (1 - Layer_Node[i - 1][j])*temp;
}
}
}//end
//修改權係數和閥值
/*hiro:待確認*/
void BP::Change_W()
{
int i, j, k;
for (i = 1; i<Layer_Max; i++)
{
for (j = 0; j<Layer_number[i]; j++)
{
for (k = 0; k<Layer_number[i - 1]; k++)
{
//修改權係數
W[i][j][k] = W[i][j][k] - Study_Speed* D[i][j] * Layer_Node[i - 1][k];
}
/*hiro:修改爲存儲到Q*/
Q[i][j] += Study_Speed*D[i][j] ;//修改閥值
}
}
}//end
//訓練函數
void BP::Train()
{
int i, j;
int ok = 0;
double Out;
long int count = 0;
double err;
ofstream Out_count("Out_count.txt", ios::out);
//把其中的5個權係數的變化保存到文件裏
ofstream outWFile1("W[2][0][0].txt", ios::out);
ofstream outWFile2("W[2][1][1].txt", ios::out);
ofstream outWFile3("W[1][0][0].txt", ios::out);
ofstream outWFile4("W[1][1][0].txt", ios::out);
ofstream outWFile5("W[3][0][1].txt", ios::out);
while (ok<441)//hiro:21*21=441...在程序裏寫常數是個bad choice
{
count++;
//20個樣本輸入
/*hiro:明明是21*21=441個輸入*/
for (i = 0, ok = 0; i<InMax; i++)
{
for (j = 0; j<InMax; j++)
{
Out = NetWorkOut(i, j);
AllLayer_D(i, j);
err = Cost(Out, Out_Exp[i][j]);//計算誤差
if (err<e)
ok++; //是否滿足誤差精度
else
Change_W();//否修改權係數和閥值
}
}
if ((count % 1000) == 0)//每1000次,保存權係數
{
cout << count << " " << err << endl;
Out_count << count << ",";
Out_Error << err << ",";
outWFile1 << W[2][0][0] << ",";
outWFile2 << W[2][1][1] << ",";
outWFile3 << W[1][0][0] << ",";
outWFile4 << W[1][1][0] << ",";
outWFile5 << W[3][0][1] << ",";
for (int p = 1; p<Layer_Max; p++)
{
for (int j = 0; j<Layer_number[p]; j++)
{
for (int k = 0; k<Layer_number[p - 1] + 1; k++)
{
Out_W_File << 'W' << '[' << p << ']'
<< '[' << j << ']'
<< '[' << k << ']'
<< '=' << W[p][j][k] << ' ' << ' ';
}
}
}
Out_W_File << '\n' << '\n';
}
}
cout << err << endl;
}//end
//打印權係數
void BP::BP_Print()
{
//打印權係數
cout << "訓練後的權係數" << endl;
for (int i = 1; i<Layer_Max; i++)
{
for (int j = 0; j<Layer_number[i]; j++)
{
/*hiro:繼續修改爲Q*/
cout << Q[i][j] << " " << endl;;
}
}
/*hiro:↓↓....有點看不懂,,*/
cout << endl << endl;
}//end
//把結果保存到文件
void BP::After_Train_Out()
{
int i, j;
ofstream Out_x1("Out_x1.txt", ios::out);
ofstream Out_x2("Out_x2.txt", ios::out);
ofstream Out_Net("Out_Net.txt", ios::out);
ofstream Out_Exp("Out_Exp.txt", ios::out);
ofstream W_End("W_End.txt", ios::out);
ofstream Q_End("Q_End.txt", ios::out);
ofstream Array("Array.txt", ios::out);
ofstream Out_x11("x1.txt", ios::out);
ofstream Out_x22("x2.txt", ios::out);
ofstream Result1("result1.txt", ios::out);
ofstream Out_x111("x11.txt", ios::out);
ofstream Out_x222("x22.txt", ios::out);
ofstream Result2("result2.txt", ios::out);
for (i = 0; i<InMax; i++)
{
for (j = 0; j<InMax; j++)
{
Out_x11 << Input_Net[0][i] << ',';
Out_x22 << Input_Net[1][j] << ",";
Result1 << 3 * NetWorkOut(i, j) << ",";
Out_x1 << Input_Net[0][i] << ",";
Array << Input_Net[0][i] << " ";
Out_x2 << Input_Net[1][j] << ",";
Array << Input_Net[1][j] << " ";
Out_Net << 3 * NetWorkOut(i, j) << ",";
Array << Y(Input_Net[0][i], Input_Net[1][j]) << " ";
Out_Exp << Y(Input_Net[0][i], Input_Net[1][j]) << ",";
Array << 3 * NetWorkOut(i, j) << " ";
Array << '\n';
}
Out_x1 << '\n';
Out_x2 << '\n';
Out_x11 << '\n';
Out_x22 << '\n';
Result1 << '\n';
}
for (j = 0; j<InMax; j++)
{
for (i = 0; i<InMax; i++)
{
Out_x111 << Input_Net[0][i] << ',';
Out_x222 << Input_Net[1][j] << ",";
Result2 << 3 * NetWorkOut(i, j) << ",";
}
Out_x111 << '\n';
Out_x222 << '\n';
Result2 << '\n';
}
//把經過訓練後的權係數和閥值保存到文件裏
for (i = 1; i<Layer_Max; i++)
{
for (int j = 0; j<Layer_number[i]; j++)
{
/*hiro:繼續修改邊界處理*/
for (int k = 0; k<Layer_number[i - 1]; k++)
{
W_End << W[i][j][k] << ",";//保存權係數
}
}
}//end for
}//end
void main(void)
{
BP B;//生成一個BP類對象B
B.Train();//開始訓練
B.BP_Print();//把結果打印出來
B.After_Train_Out();//把結果保存到文件
}
感想
說實話到目前爲止還不算特別的理解透徹BP網絡,但是對人工神經網絡算法總算是有了個比較具體的理解。
從人工神經網絡算法上,我想到:或許我們的神經細胞之間的突觸,連接,刺激的強弱,微觀看來是沒有什麼邏輯意義的,遠遠不如人體的內分泌系統那樣的負反饋正反饋。但是,它們卻能組成網絡,在宏觀意義上存在功能和邏輯。這些神經的連接,可能只是長此以往的環境選擇下得出的結果。人類大腦的“隱含層”和學習率進化得比目前地球上其他已知生物要複雜,豐富和強。
同時我也親身體驗到所謂的神經網絡,不過是個無窮逼近,不斷擬合的過程。即使每個運算單元的運算速度不高,由於人類具有大量的神經元,相當於是大規模的並行運算。所以在做這類運算時人類大腦的確是比電腦的高頻低並行度計算要強。這讓我隱隱感覺到我們的感覺,思維,是以這些神經細胞爲物理載體,存在於這些細胞的連接當中。好比古代複雜的鐘表,他們能存儲,表現出複雜的效果和算法,但這些思想不存在於鐘錶裏任何一個單獨的齒輪,這些思想存在與這些物理構建之間的“聯繫”。
最後說幾句
本書不穩定的代碼質量至今是個堪憂的問題。