寒假在家做一點項目,這也是最後一塊比較多的時間能做項目了,下學期主要投入時間考研,之所以寫這篇文章是因爲想記錄一下自己的學習歷程,若是能幫助到恰好需要幫助的人那是更好不過。
因爲我對Python不怎麼了解,因此我選擇用c++實現bp網絡,雖然都說機器學習用得比較多的是Python。像很多人一樣,我也是一邊學編程一邊做一些項目,之前做過adaboost人臉檢測和pca的人臉識別,所以我實現這個神經網絡主要是用來做人臉識別用的,那麼廢話不多說直接進入主題。
首先要實現這個網絡就要先對bp神經網絡的算法有一定的瞭解,可以不需要了解那麼透徹,反正能用代碼將那些公式過程實現出來即可,編程的主要目的還是偏重於應用,解決實際問題。首先我們知道bp神經網絡分爲三個層:輸入層,隱層,輸出層。
大致結構如上圖所示,它的主要工作機制就是:在輸入層輸入n個數,經過隱層和輸出層的兩次計算,最後在輸出層能得到m個結果,你事先給定這m個結果分別是什麼,神經網絡通過它計算的結果和你希望它出來的結果進行誤差計算,並返回調整權重值,經過一次又一次的計算,神經網絡能輸出的結果和你最終希望得到的結果會越來越接近。
神經網絡的理論知識我先扯到這裏,接下來開始實踐。用c++的話,我就選擇了設計類來實現,類結構如下所示
class BPNet
{
static double eta;
vector<layer>network;
public:
BPNet();
void set(const vector<int> & network_);
~BPNet();
static double sig(double &x) { return 1.0 / (1.0 + exp(-x)); }
void front(vector<double> &input_, const vector<int> & network_);
void back_p(const vector<double> & predict);
void update_weight();
void show() const;
};
私有成員包括學習率eta(這個有點類似步長的意思)這裏將它用static修飾因爲它屬於神經網絡類,還有一個vector數組代表“層”(這個層的結構我接下來會給出),再來看看方法:默認構造函數析構函數就不多說了,我這裏定義了一個set用於初始化,然後一個前向傳播函數一個反向傳播函數再加一個權重更新函數就是主要的方法了,至於激活函數,它也屬於類,因此也用static修飾,因爲它比較短小而且使用次數比較多因此聲明爲內聯函數。
struct neuron
{
vector<double>weight;
vector<double>update_w;
double input;
double output = 0;
double bias;
};
typedef vector<neuron>layer;
這是神經元結構,你可以這樣想,一個大的神經網絡有很多層組成,而每一層又由許多神經元組成,因此神經元是最小的單位,神經元裏需要儲存的數據有權重,輸入,輸出,偏置。這裏用vector的原因是事先並不能確定個層需要幾個神經元,用vector方便改變大小。類總體結構說得差不多了,接下來我們看看方法具體的實現吧void BPNet::set(const vector<int> & network_)
{
int layer_num = network_.size();
for (int i = 0; i < layer_num; i++)
{
network.push_back(layer());
for (int j = 0; j < network_[i]; j++)
{
network.back().push_back(neuron());
if (i > 0)
{
network[i][j].bias = 0.5;
}
if (i < layer_num - 1)
{
for (int k = 0; k < network_[i+1]; k++)
{
network[i][j].weight.push_back(rand()*(rand() % 2 ? 1 : -1)*1.0 / RAND_MAX);
network[i][j].update_w.push_back(0);
}
}
}
}
}
首先是網絡的初始化函數,初始化的部分主要包含以下幾部分:網絡層數(簡單的一般就是三層),每層所含的神經元個數,每個神經元的初始權重和初始偏置。對於三層的網絡而言,權重其實需要兩組就夠了,輸入和隱層之間一組權重,隱層和輸出之間一組權重,那麼你的權重可以保存在輸入層和隱層也可以保存在隱層和輸出層,我採用的是前者,因爲感覺運算方便一點。偏置的設置很簡單,同一層的偏置是一樣的,也是設置兩層就好,權重的初始值都設爲-1到 1之間的隨機值就好。
void BPNet::front(vector<double> &input_, const vector<int> & network_)
{
for (int t = 0; t < network_[0]; t++)
{
network[0][t].output = input_[t];
}
for (int i = 1; i < network_.size(); i++)
{
for (int j = 0; j < network_[i]; j++)
{
network[i][j].output = 0;
for (int k = 0; k < network_[i-1]; k++)
{
network[i][j].output += network[i-1][k].output * network[i-1][k].weight[j];
}
network[i][j].output += network[i][j].bias;
network[i][j].output = sig(network[i][j].output);
}
}
}
接下來我們來看一下前向傳播函數,這個函數其實不難實現,輸入層不用管,輸入層的輸出就等於你的輸入值,主要是計算隱層和輸出層。隱層的輸出值應該是等於 :(輸入層的每個神經元的輸出乘以對應的權重然後加上偏置)把這個整體放入激活函數 得到的值,輸出層也是一樣的,具體的可以看代碼。最後輸出層可以得到幾個輸出,那個就是整個神經元的輸出。
void BPNet::back_p(const vector<double> & predict, double & error)
{
double delta_total = 0.0;
double delta = 0.0;
double sum;
for (int i = 0; i < predict.size(); i++)
{
delta_total += 0.5*pow((predict[i] - network[2][i].output),2);
}
error = delta_total;
//std::cout <<"total delta is "<< delta_total << std::endl;
for (int i = 0; i < network[1].size(); i++)
{
for (int j = 0; j < network[2].size(); j++)
{
delta = -(predict[j] - network[2][j].output)*network[2][j].output*(1 - network[2][j].output)*network[1][i].output;
network[1][i].update_w[j] = network[1][i].weight[j] - eta*delta;
}
}
delta = 0.0;
for (int i = 0; i < network[0].size(); i++)
{
for (int j = 0; j < network[1].size(); j++)
{
sum = 0.0;
delta = network[1][j].output*(1 - network[1][j].output)*network[0][i].output;
for (int k = 0; k < network[2].size(); k++)
{
sum += -(predict[k] - network[2][k].output)*network[2][k].output*(1 - network[2][k].output)*network[1][j].weight[k];
}
delta *= sum;
network[0][i].update_w[j]=network[0][i].weight[j] - eta*delta;
}
}
}
這是反向誤差函數,看起來有點複雜,因爲裏面涉及到了鏈式法則求導的問題,這塊我也看了挺多便的,但其實看懂了就還好。首先要清楚一點反向傳播函數的主要目的是爲了修正權重,而且輸出層和隱層的權重修正是不一樣的,修正的公式就是w_update=w_old-eta*delta,關鍵是delta要求出來,這個delta是你要修正的那個權重對總誤差求偏導數得到的值,單個誤差就是(predict-out)的平方,也就是某個輸出層神經元預期的輸出和真實輸出的差值的平方,總誤差就是把它們加起來,對於輸出層的神經元,權重只會影響到單個的輸出,而對於隱層而言,一個權重的改變會對所有神經元的輸出均有影響,具體公式這部分的內容有一個網站說的特別詳細(yongyuan.name/blog/back-propagtion.html)我也是參考他的公式並自己總結出來編寫了代碼。最後一部分比較簡單,更新權重,要注意的是權重更新一定要等反向傳播完成了以後再更新,因爲在修正隱層權重的時候,需要用到輸出層原來的那組權重,這個具體看那部分教程就明白了
void BPNet::update_weight()
{
for (int i = 0; i < network.size() - 1; i++)
{
for (int j = 0; j < network[i].size(); j++)
{
network[i][j].weight = network[i][j].update_w;
}
}
}
最後一個顯示信息函數,沒什麼可說的,驗證結果是否正確需要它
void BPNet::show() const
{
for (int i = 0; i < network[0].size(); i++)
{
std::cout << "input" << i + 1 << "=" << network[0][i].output << std::endl;
}
for (int i = 0; i < network[2].size(); i++)
{
std::cout << "output" << i + 1 << "=" << network[2][i].output << std::endl;
}
}
寫一個小程序來測試它是否正確
#include<iostream>
#include<vector>
#include"Net.h"
int main()
{
BPNet bpnetwork;
vector<int>ly{ 3,3,4};
vector<double>input{ 0.3,0.7,0.4 };
vector<double>output{0.5,0.4,0.5,0.7};
bpnetwork.set(ly);
for (int i = 0; i < 100; i++)
{
bpnetwork.front(input, ly);
bpnetwork.back_p(output);
bpnetwork.update_weight();
bpnetwork.show();
}
return 0;
}