python实现神经网络的正向传播(fp)函数_如何自己从零实现一个神经网络?
我是從高三開始入門的,一直是用C++來做神經(jīng)網(wǎng)絡(luò)。從造輪子開始,到實(shí)現(xiàn)模型,到封裝模型,再到真正用seq2vec,seq2seq模型訓(xùn)練成功一些小玩意,所有的東西都是自己寫的,但是資料都是從網(wǎng)上搜,學(xué)習(xí)也遇到好多坑。我將會(huì)在這里具體說說造輪子的過程。
造輪子之前,必須要廣泛查閱資料,自己推導(dǎo)前向傳播和反向傳播的所有過程,這個(gè)過程需要的知識(shí)點(diǎn)是偏導(dǎo)數(shù)以及鏈?zhǔn)椒▌t,高中生其實(shí)是可以理解的(將偏導(dǎo)數(shù)理解為對(duì)多元函數(shù)中其中一個(gè)變量求導(dǎo),把其他所有量都看作常數(shù)即可),不過稍微有點(diǎn)困難,這直接導(dǎo)致了我高三其實(shí)學(xué)的一知半解。
首先入門實(shí)現(xiàn)要從最簡單的BP開始,公式等內(nèi)容不再一一贅述,網(wǎng)上有非常多的資料,個(gè)人草稿紙上推導(dǎo)的內(nèi)容也早就丟失。確認(rèn)理解后,就可以著手用C++開始寫B(tài)P的輪子了。
首先是各種激勵(lì)函數(shù)。一開始你需要了解的激勵(lì)函數(shù)可能只有sigmoid,但是隨著學(xué)習(xí)深入,了解其他的激勵(lì)函數(shù)很有必要。
在激勵(lì)函數(shù)方面需要寫的是這個(gè)函數(shù)本身,以及它的導(dǎo)函數(shù)。sigmoid和tanh,elu等激勵(lì)函數(shù)需要用到exp()函數(shù),這個(gè)函數(shù)需要cmath頭文件。IDE自選吧,怎么方便怎么來,我高中參加NOIP用Dev,一直到現(xiàn)在我才改用VScode(這個(gè)只能算工作臺(tái))。sigmoidtanh
tanh其實(shí)在cmath庫里就有寫好的函數(shù),可以直接調(diào)用的。ReLU
relu用三目運(yùn)算符就很舒服。elu和leaky relu也可以這么玩兒。不過我比較推薦leaky relu吧,因?yàn)閞elu很容易出現(xiàn)神經(jīng)元死亡的情況(神經(jīng)元無論接受什么樣的數(shù)據(jù),其總和都是負(fù)值,那么這個(gè)神經(jīng)元沒有任何輸出,在反向過程中也無法進(jìn)行權(quán)值更新,具體看自己推導(dǎo))。
以上是激勵(lì)函數(shù)的一些例子。下面是寫神經(jīng)元的例子。
寫神經(jīng)元的話,你可以單純只用二維數(shù)組來寫,直接做矩陣運(yùn)算,但是當(dāng)時(shí)我沒有接觸線性代數(shù),所以用了個(gè)非常直觀但是后期效率低下的方法。一般用struct
首先定義輸入神經(jīng)元個(gè)數(shù),隱藏層神經(jīng)元個(gè)數(shù),輸出神經(jīng)元個(gè)數(shù),分別對(duì)應(yīng)InputNum,HideNum,OutputNum。如果你覺得這個(gè)寫法有點(diǎn)長,可以縮成INUM,HNUM,ONUM等等,怎么舒服怎么寫。
然后用struct來分別定義隱藏層神經(jīng)元,輸出層神經(jīng)元。顯然這個(gè)用C++的class有點(diǎn)大炮打蚊子的感覺,當(dāng)然還是那一句,怎么舒服怎么寫。如果你覺得我這么寫有點(diǎn)重復(fù)的內(nèi)容,可以使用template。用template靈活編寫,還可復(fù)用
然后bp必須要有的一些變/常量有:
常量const double learningrate,大小自己定
input[INUM]用于存儲(chǔ)輸入數(shù)據(jù)(單個(gè)batch),expect[ONUM]用于存儲(chǔ)期望數(shù)據(jù)(單個(gè)batch)
error,sigma_error,分別用于記錄單個(gè)batch的誤差,以及對(duì)所有batch的誤差求和,記得初始化為一個(gè)很大的值,這是進(jìn)入訓(xùn)練循環(huán)的必要條件。
hidden[HNUM],output[ONUM]這兩個(gè)是結(jié)構(gòu)體數(shù)組,之后在寫前向和反向過程中是必要的。
以上所有變量都是全局變量。最基本的需要的函數(shù)
TxtCheck()用于檢查神經(jīng)網(wǎng)絡(luò)的數(shù)據(jù)是否存在,不存在的話進(jìn)行INIT()并且輸出數(shù)據(jù)保存為一個(gè)文件。INIT()即為初始化函數(shù)。INIT函數(shù),第一句先確定隨機(jī)數(shù)種子
INIT()函數(shù)首先要先開始生成隨機(jī)數(shù),srand()加上unsigned (time(NULL))很不錯(cuò),需要頭文件ctime。在下面的語句里,對(duì)每個(gè)神經(jīng)元的weight和bia進(jìn)行初始化,具體內(nèi)容按照需求來,這個(gè)自己可以靈活編輯,我這里只給個(gè)大框架。
Datain和Dataout不多說了,顯而易見是進(jìn)行數(shù)據(jù)的輸出和讀取用的,用于在幾個(gè)epoch后保存數(shù)據(jù)文件,避免下一次打開的時(shí)候重復(fù)訓(xùn)練。
用ifstream和ofstream進(jìn)行讀取和輸出,需要頭文件fstream。上圖雙引號(hào)內(nèi)填文件名(文件在該文件夾下)或者絕對(duì)地址(文件在其他地方)。
Mainwork()函數(shù)用于讀取訓(xùn)練集,首先sigma_error=0,接著依次循環(huán)對(duì)訓(xùn)練集中每個(gè)batch進(jìn)行處理,讀入input和expect中,然后調(diào)用Calc()函數(shù)進(jìn)入前向傳播階段,再調(diào)用ErrorCalc()進(jìn)入本次誤差計(jì)算階段,error在本次計(jì)算中被賦值。然后進(jìn)行Training()反向傳播,接著sigma_error+=error。Mainwork預(yù)覽
Calc()里面進(jìn)行前向傳播,基本上都是循環(huán),不用我多說了吧?
ErrorCalc()也是如此,同理Training()也是,所以寫的這些東西里面,占了絕大多數(shù)的語句都是循環(huán)語句。*0.5比/2快不少哦,尤其是需要很多步驟的時(shí)候
把整個(gè)過程都寫下來啦。。。這是我個(gè)人喜好的寫法,要是覺得看不明白或者覺得效率很低,也可以自己寫的,反正能實(shí)現(xiàn)功能是關(guān)鍵!
(有個(gè)小trick上圖沒體現(xiàn)出來,一般bia的增量是2*learningrate*diff,親測(cè)效果不錯(cuò))
main里面基本上寫一些調(diào)用的內(nèi)容
然后在C++里面,如果數(shù)據(jù)里出現(xiàn)了Inf,很有可能下面會(huì)出現(xiàn)NaN,然后循環(huán)會(huì)被動(dòng)停止,給你輸出含有一堆NaN的垃圾數(shù)據(jù),為了避免這個(gè),C++其實(shí)是有一個(gè)宏可以檢測(cè)Inf和NaN的。
isnan()和isinf()是cmath/math.h庫里的宏,可以直接調(diào)用來判斷
到這里,我已經(jīng)把寫簡單BP的訣竅說完了,如果你想寫深度的,框架其實(shí)也差不多。以后我可能會(huì)更新的內(nèi)容里面也基本上都是建立在這個(gè)框架體系之上的,希望能有所幫助。即使你可能不太能接受我這種不用矩陣運(yùn)算的寫法,但這也是一個(gè)用C++造輪子從零開始的例子,希望能給予你鼓勵(lì)。
下面貼個(gè)代碼,當(dāng)然不能直接復(fù)制了用,要自己修改的哦
#include#include#include#define INUM 2#define HNUM 5#define ONUM 2using namespace std;
template
struct neuron
{
double w[NUM],bia,diff;
double in,out;
};
neuron hide[HNUM];
neuron output[ONUM];
const double learningrate=0.1;
double input[INUM];
double expect[ONUM];
double sigma_error=1e8;
double error=1e8;
double sigmoid(double x)
{
return 1.0/(1.0+exp(-x));
}
double diffsigmoid(double x)
{
x=1.0/(1.0+exp(-x));
return x*(1-x);
}
double tanh(double x)
{
return (exp(x)-exp(-x))/(exp(x)+exp(-x));
}
double difftanh(double x)
{
x=tanh(x);
return 1-x*x;
}
double relu(double x)
{
return x>0? x:0;
}
double diffrelu(double x)
{
return x>0? 1:0;
}
void TxtCheck();
void INIT();
void Datain();
void Dataout();
void Mainwork();
void Calc();
void ErrorCalc();
void Training();
int main()
{
int epoch=0;
TxtCheck();
while(sigma_error>0.001)
{
epoch++;
Mainwork();
if(epoch%(一個(gè)數(shù))==0)
Dataout();
//也可以寫其他操作}
Dataout();
return 0;
}
void INIT()
{
srand(unsigned(time(NULL)));
/*statement*/
return;
}
void Datain()
{
ifstream fin(" ");
fin>>...
fin.close();
}
void Dataout()
{
ofstream fout(" ");
fout<<...>
fout.close();
}
void Mainwork()
{
ifstream fin("數(shù)據(jù)集");
sigma_error=0;
for(int b=0;b
{
/*處理batch數(shù)據(jù),讀入input和expect*/
Calc();
ErrorCalc();
Training();
sigma_error+=error;
}
fin.close();
return;
}
void Calc()
{
for(int i=0;i
{
hide[i].in=0;
hide[i].in+=hide[i].bia;
for(int j=0;j
hide[i].in+=input[j]*hide[i].w[j];
hide[i].out=sigmoid(hide[i].in);
}
/*other statements*/
}
void ErrorCalc()
{
double trans;
error=0;
for(int i=0;i
{
trans=output[i].out-expect[i];
error+=trans*trans;
}
error*=0.5;
}
void Training()
{
for(int i=0;i
output[i].diff=(expect[i]-output[i].out)*diffsigmoid(output[i].in);
//負(fù)號(hào)直接舍棄,因?yàn)檎麄€(gè)傳遞過程這里的負(fù)號(hào)不帶來影響//而且在最后更新數(shù)據(jù)的時(shí)候也不需要再*(-1)for(int i=0;i
{
hide[i].diff=0;
for(int j=0;j
hide[i].diff+=output[j].diff*output[j].w[i];
hide[i].diff*=diffsigmoid(hide[i].in);
}
for(int i=0;i
{
output[i].bia+=learningrate*output[i].diff;
for(int j=0;j
output[i].w[j]+=learningrate*output[i].diff*hide[j].out;
}
for(int i=0;i
{
hide[i].bia+=learningrate*hide[i].diff;
for(int j=0;j
hide[i].w[j]+=learningrate*hide[i].diff*input[j];
}
return;
}
2019/3/14 21:59更新AutoEncoder
最近進(jìn)軍深度學(xué)習(xí),少不了自動(dòng)編碼器,于是在LSTM的seq2seq模型上加入了AutoEncoder部分,由于初期的架構(gòu),循環(huán)很多,代碼量很大,不過可以從以前的代碼里復(fù)制,然后微微修改,再粘貼下來,等到有空之后,我會(huì)把自己RNN和LSTM的東西也分享分享的。
2019/3/15更新
功能函數(shù)的大體結(jié)構(gòu)都如之前寫的那樣,現(xiàn)在講述的都是其他一些神經(jīng)元單元的設(shè)計(jì)和使用。我是做NLP自然語言處理的,自然語言處理必然少不了RNN,LSTM,GRU這些基本單元,那么按照上面的思路,RNN和LSTM的寫法應(yīng)該不難得出,不過變成了下面這樣:
#define MAXTIME 100
template
struct rnn_neuron
{
double wi[InputNum],wh[HideNum];
double bia,diff[Maxtime];
double in[Maxtime],out[Maxtime];
};
template
struct nor_neuron
{
double w[InputNum],bia,diff[Maxtime];
double in[Maxtime],out[Maxtime];
};
const double learningrate=0.1;
rnn_neuron hide[HNUM];
nor_neuron output[ONUM];
double input[INUM][MAXTIME];
double expect[ONUM][MAXTIME];
double sigma_error=1e8;
double error=1e8;
可以看出來出現(xiàn)了MAXTIME這個(gè)東西,這個(gè)輔助量是用于記錄時(shí)間序列中每個(gè)時(shí)間刻的數(shù)據(jù)的,因?yàn)槊總€(gè)數(shù)據(jù)在最后BPTT的過程中都是必需的。rnn中的wi是對(duì)輸入端的權(quán)重,wh是對(duì)前一時(shí)間刻隱藏層輸出的權(quán)重。
但是這樣寫還有個(gè)缺陷。struct中diff是記錄這個(gè)單元在t時(shí)刻的訓(xùn)練增量的,顯然如果直接遍歷所有時(shí)間,把增量依次賦給數(shù)據(jù)是不太行的。因?yàn)槊總€(gè)時(shí)間刻內(nèi),增量可能數(shù)量級(jí)很小很小,甚至有可能到1e-8以及更小(在非常長的時(shí)間序列下,可以到1e-20的級(jí)別),直接賦給數(shù)據(jù),就相當(dāng)于給數(shù)據(jù)加上了0,丟失了精度。
舉個(gè)例子:double x=0.1,y=1e-10;
x+y后,得出的結(jié)果仍然是0.1,顯然是丟失了精度。
那么為了避免出現(xiàn)這個(gè)問題,我們還需要再加上一個(gè)sigmadiff用于把所有時(shí)間刻的diff累加起來一起賦給數(shù)據(jù)。不過這樣做的話,就要對(duì)每個(gè)時(shí)間下的每個(gè)數(shù)據(jù)(包括權(quán)重)做sigmadiff了,因?yàn)橐婚_始求的diff是對(duì)bia的偏導(dǎo)數(shù),如果直接全部加起來,獲得的sigmadiff僅僅是對(duì)bia的sigmadiff。
于是
template
struct rnn_neuron
{
double wi[InputNum],wh[HideNum],sigmawi[InputNum],sigmawh[HideNum];
double bia,diff[Maxtime],sigmabia;
double in[Maxtime],out[Maxtime];
};
template
struct nor_neuron
{
double w[InputNum],bia,diff[Maxtime],sigmaw[InputNum],sigmabia;
double in[Maxtime],out[Maxtime];
};
就變成了這樣。
那么同理,lstm是一樣的思路,不過數(shù)據(jù)更加多,而且隨著數(shù)據(jù)量增加,訓(xùn)練速度也明顯會(huì)變得非常慢(真的非常顯著的變化!)
template
struct LSTM_neuron
{
double cell[Maxtime];
double out[Maxtime];
double fog_in[Maxtime],fog_out[Maxtime],fog_bia,fog_wi[InputNum],fog_wh[HideNum],fog_diff[Maxtime];
double sig_in[Maxtime],sig_out[Maxtime],sig_bia,sig_wi[InputNum],sig_wh[HideNum],sig_diff[Maxtime];
double tan_in[Maxtime],tan_out[Maxtime],tan_bia,tan_wi[InputNum],tan_wh[HideNum],tan_diff[Maxtime];
double out_in[Maxtime],out_out[Maxtime],out_bia,out_wi[InputNum],out_wh[HideNum],out_diff[Maxtime];
double fog_transbia,fog_transwi[InputNum],fog_transwh[HideNum];
double sig_transbia,sig_transwi[InputNum],sig_transwh[HideNum];
double tan_transbia,tan_transwi[InputNum],tan_transwh[HideNum];
double out_transbia,out_transwi[InputNum],out_transwh[HideNum];
};
那么針對(duì)rnn和lstm的Calc()和Training()函數(shù)都要重新編寫哦!
接著就是利用這些單元來寫一些模型,然后對(duì)測(cè)試好的模型進(jìn)行封裝。
先拿一開始的BP做個(gè)例子吧。思想其實(shí)是很簡單的,BP的神經(jīng)元我們已經(jīng)有個(gè)一個(gè)struct來定義了。那么我們用這個(gè)struct做一個(gè)class,把一些函數(shù)也包含進(jìn)去。bp.h用于放template和class
/*bp.h header file by ValK*/
/* 2019/3/15 15:25 */
#ifndef __BP_H__#define __BP_H__#include #include #include #include #include #include using namespace std;
template
struct neuron
{
double w[NUM],bia,diff;
double in,out;
};
class ActivateFunction
{
public:
double sigmoid(double x)
{
return 1.0/(1.0+exp(-x));
}
double diffsigmoid(double x)
{
x=1.0/(1.0+exp(-x));
return x*(1-x);
}
double tanh(double x)
{
return (exp(x)-exp(-x))/(exp(x)+exp(-x));
}
double difftanh(double x)
{
x=tanh(x);
return 1-x*x;
}
double relu(double x)
{
return x>0? x:0;
}
double diffrelu(double x)
{
return x>0? 1:0;
}
};
ActivateFunction fun;
template
class bp_neural_network
{
private:
neuron hide[HNUM];
neuron output[ONUM];
double learningrate;
double input[INUM];
double expect[ONUM];
int batch_size;
double sigma_error;
double error;
public:
int epoch;
void TxtCheck()
{
if(!fopen("data.ai","r"))
{
INIT();
Dataout();
}
if(!fopen("trainingdata.txt","r"))
{
cout<
cout<
exit(0);
}
}
bp_neural_network()
{
epoch=0;
sigma_error=1e8;
error=1e8;
batch_size=1;
learningrate=0.01;
TxtCheck();
}
void SetBatch(int Batch)
{
batch_size=Batch;
return;
}
void INIT()
{
srand(unsigned(time(NULL)));
/*statement*/
return;
}
void Datain()
{
ifstream fin("data.ai");
/*statement*/
fin.close();
}
void Dataout()
{
ofstream fout("data.ai");
/*statement*/
fout.close();
}
void Mainwork()
{
ifstream fin("trainingdata.txt");
sigma_error=0;
for(int b=0;b
{
/*處理batch數(shù)據(jù),讀入input和expect*/
Calc();
ErrorCalc();
Training();
sigma_error+=error;
}
fin.close();
return;
}
void Calc()
{
for(int i=0;i
{
hide[i].in=hide[i].bia;
for(int j=0;j
hide[i].in+=input[j]*hide[i].w[j];
hide[i].out=fun.sigmoid(hide[i].in);
}
for(int i=0;i
{
output[i].in=output[i].bia;
for(int j=0;j
output[i].in+=hide[j].out*output[i].w[j];
output[i].out=fun.sigmoid(output[i].in);
}
}
void ErrorCalc()
{
double trans;
error=0;
for(int i=0;i
{
trans=output[i].out-expect[i];
error+=trans*trans;
}
error*=0.5;
}
void Training()
{
for(int i=0;i
output[i].diff=(expect[i]-output[i].out)*fun.diffsigmoid(output[i].in);
//負(fù)號(hào)直接舍棄,因?yàn)檎麄€(gè)傳遞過程這里的負(fù)號(hào)不帶來影響//而且在最后更新數(shù)據(jù)的時(shí)候也不需要再*(-1)for(int i=0;i
{
hide[i].diff=0;
for(int j=0;j
hide[i].diff+=output[j].diff*output[j].w[i];
hide[i].diff*=fun.diffsigmoid(hide[i].in);
}
for(int i=0;i
{
output[i].bia+=learningrate*output[i].diff;
for(int j=0;j
output[i].w[j]+=learningrate*output[i].diff*hide[j].out;
}
for(int i=0;i
{
hide[i].bia+=learningrate*hide[i].diff;
for(int j=0;j
hide[i].w[j]+=learningrate*hide[i].diff*input[j];
}
return;
}
};
#endif
bpneuralnetwork這個(gè)template初始三個(gè)傳參便是建立一個(gè)網(wǎng)絡(luò)必須要的參數(shù),這種思想在寫其他template封裝時(shí)很重要。
neuron是struct單元,包括了基本bp神經(jīng)元需要的數(shù)據(jù),ActivateFunction類包括了一些需要使用的激勵(lì)函數(shù)。
省時(shí)間,一些函數(shù)的內(nèi)容就不多寫了。設(shè)計(jì)構(gòu)造函數(shù)的時(shí)候可以自己創(chuàng)新,想怎么寫怎么寫,我這里構(gòu)造函數(shù)先初始化了epoch,sigmerror,error,batch_size,還有l(wèi)earningrate。(直接把函數(shù)內(nèi)容寫class里面是被template逼的……教授要是看到了會(huì)罵死我)
Mainwork函數(shù)一般推薦你不要封裝進(jìn)去。。因?yàn)閎p可能會(huì)被用來處理各種各樣的問題,為了保證靈活性,Mainwork還是自己在外面寫吧,要什么功能再加進(jìn)去就是了。
寫個(gè)小bug(誤)來看看是否運(yùn)行正常:
沒有問題,因?yàn)槲覜]有訓(xùn)練集,所以在構(gòu)造函數(shù)里判斷出來了,直接退出了程序。
更新內(nèi)容基本結(jié)束~
2019.5.14更新
這次課設(shè)就寫了相關(guān)的代碼,不過和答案里提供的方法不太一樣,這個(gè)頭文件庫里面所有的網(wǎng)絡(luò)建立都是通過constructor傳參+內(nèi)存分配完成的,沒有使用template。https://github.com/ValKmjolnir/easyNLP
《新程序員》:云原生和全面數(shù)字化實(shí)踐50位技術(shù)專家共同創(chuàng)作,文字、視頻、音頻交互閱讀總結(jié)
以上是生活随笔為你收集整理的python实现神经网络的正向传播(fp)函数_如何自己从零实现一个神经网络?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mysql web备份软件_Window
- 下一篇: python 惰性_Python学习--