python实现神经网络的正向传播(fp)函数_如何自己从零实现一个神经网络?
我是從高三開始入門的,一直是用C++來做神經網絡。從造輪子開始,到實現模型,到封裝模型,再到真正用seq2vec,seq2seq模型訓練成功一些小玩意,所有的東西都是自己寫的,但是資料都是從網上搜,學習也遇到好多坑。我將會在這里具體說說造輪子的過程。
造輪子之前,必須要廣泛查閱資料,自己推導前向傳播和反向傳播的所有過程,這個過程需要的知識點是偏導數以及鏈式法則,高中生其實是可以理解的(將偏導數理解為對多元函數中其中一個變量求導,把其他所有量都看作常數即可),不過稍微有點困難,這直接導致了我高三其實學的一知半解。
首先入門實現要從最簡單的BP開始,公式等內容不再一一贅述,網上有非常多的資料,個人草稿紙上推導的內容也早就丟失。確認理解后,就可以著手用C++開始寫BP的輪子了。
首先是各種激勵函數。一開始你需要了解的激勵函數可能只有sigmoid,但是隨著學習深入,了解其他的激勵函數很有必要。
在激勵函數方面需要寫的是這個函數本身,以及它的導函數。sigmoid和tanh,elu等激勵函數需要用到exp()函數,這個函數需要cmath頭文件。IDE自選吧,怎么方便怎么來,我高中參加NOIP用Dev,一直到現在我才改用VScode(這個只能算工作臺)。sigmoidtanh
tanh其實在cmath庫里就有寫好的函數,可以直接調用的。ReLU
relu用三目運算符就很舒服。elu和leaky relu也可以這么玩兒。不過我比較推薦leaky relu吧,因為relu很容易出現神經元死亡的情況(神經元無論接受什么樣的數據,其總和都是負值,那么這個神經元沒有任何輸出,在反向過程中也無法進行權值更新,具體看自己推導)。
以上是激勵函數的一些例子。下面是寫神經元的例子。
寫神經元的話,你可以單純只用二維數組來寫,直接做矩陣運算,但是當時我沒有接觸線性代數,所以用了個非常直觀但是后期效率低下的方法。一般用struct
首先定義輸入神經元個數,隱藏層神經元個數,輸出神經元個數,分別對應InputNum,HideNum,OutputNum。如果你覺得這個寫法有點長,可以縮成INUM,HNUM,ONUM等等,怎么舒服怎么寫。
然后用struct來分別定義隱藏層神經元,輸出層神經元。顯然這個用C++的class有點大炮打蚊子的感覺,當然還是那一句,怎么舒服怎么寫。如果你覺得我這么寫有點重復的內容,可以使用template。用template靈活編寫,還可復用
然后bp必須要有的一些變/常量有:
常量const double learningrate,大小自己定
input[INUM]用于存儲輸入數據(單個batch),expect[ONUM]用于存儲期望數據(單個batch)
error,sigma_error,分別用于記錄單個batch的誤差,以及對所有batch的誤差求和,記得初始化為一個很大的值,這是進入訓練循環的必要條件。
hidden[HNUM],output[ONUM]這兩個是結構體數組,之后在寫前向和反向過程中是必要的。
以上所有變量都是全局變量。最基本的需要的函數
TxtCheck()用于檢查神經網絡的數據是否存在,不存在的話進行INIT()并且輸出數據保存為一個文件。INIT()即為初始化函數。INIT函數,第一句先確定隨機數種子
INIT()函數首先要先開始生成隨機數,srand()加上unsigned (time(NULL))很不錯,需要頭文件ctime。在下面的語句里,對每個神經元的weight和bia進行初始化,具體內容按照需求來,這個自己可以靈活編輯,我這里只給個大框架。
Datain和Dataout不多說了,顯而易見是進行數據的輸出和讀取用的,用于在幾個epoch后保存數據文件,避免下一次打開的時候重復訓練。
用ifstream和ofstream進行讀取和輸出,需要頭文件fstream。上圖雙引號內填文件名(文件在該文件夾下)或者絕對地址(文件在其他地方)。
Mainwork()函數用于讀取訓練集,首先sigma_error=0,接著依次循環對訓練集中每個batch進行處理,讀入input和expect中,然后調用Calc()函數進入前向傳播階段,再調用ErrorCalc()進入本次誤差計算階段,error在本次計算中被賦值。然后進行Training()反向傳播,接著sigma_error+=error。Mainwork預覽
Calc()里面進行前向傳播,基本上都是循環,不用我多說了吧?
ErrorCalc()也是如此,同理Training()也是,所以寫的這些東西里面,占了絕大多數的語句都是循環語句。*0.5比/2快不少哦,尤其是需要很多步驟的時候
把整個過程都寫下來啦。。。這是我個人喜好的寫法,要是覺得看不明白或者覺得效率很低,也可以自己寫的,反正能實現功能是關鍵!
(有個小trick上圖沒體現出來,一般bia的增量是2*learningrate*diff,親測效果不錯)
main里面基本上寫一些調用的內容
然后在C++里面,如果數據里出現了Inf,很有可能下面會出現NaN,然后循環會被動停止,給你輸出含有一堆NaN的垃圾數據,為了避免這個,C++其實是有一個宏可以檢測Inf和NaN的。
isnan()和isinf()是cmath/math.h庫里的宏,可以直接調用來判斷
到這里,我已經把寫簡單BP的訣竅說完了,如果你想寫深度的,框架其實也差不多。以后我可能會更新的內容里面也基本上都是建立在這個框架體系之上的,希望能有所幫助。即使你可能不太能接受我這種不用矩陣運算的寫法,但這也是一個用C++造輪子從零開始的例子,希望能給予你鼓勵。
下面貼個代碼,當然不能直接復制了用,要自己修改的哦
#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%(一個數)==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("數據集");
sigma_error=0;
for(int b=0;b
{
/*處理batch數據,讀入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);
//負號直接舍棄,因為整個傳遞過程這里的負號不帶來影響//而且在最后更新數據的時候也不需要再*(-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
最近進軍深度學習,少不了自動編碼器,于是在LSTM的seq2seq模型上加入了AutoEncoder部分,由于初期的架構,循環很多,代碼量很大,不過可以從以前的代碼里復制,然后微微修改,再粘貼下來,等到有空之后,我會把自己RNN和LSTM的東西也分享分享的。
2019/3/15更新
功能函數的大體結構都如之前寫的那樣,現在講述的都是其他一些神經元單元的設計和使用。我是做NLP自然語言處理的,自然語言處理必然少不了RNN,LSTM,GRU這些基本單元,那么按照上面的思路,RNN和LSTM的寫法應該不難得出,不過變成了下面這樣:
#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;
可以看出來出現了MAXTIME這個東西,這個輔助量是用于記錄時間序列中每個時間刻的數據的,因為每個數據在最后BPTT的過程中都是必需的。rnn中的wi是對輸入端的權重,wh是對前一時間刻隱藏層輸出的權重。
但是這樣寫還有個缺陷。struct中diff是記錄這個單元在t時刻的訓練增量的,顯然如果直接遍歷所有時間,把增量依次賦給數據是不太行的。因為每個時間刻內,增量可能數量級很小很小,甚至有可能到1e-8以及更小(在非常長的時間序列下,可以到1e-20的級別),直接賦給數據,就相當于給數據加上了0,丟失了精度。
舉個例子:double x=0.1,y=1e-10;
x+y后,得出的結果仍然是0.1,顯然是丟失了精度。
那么為了避免出現這個問題,我們還需要再加上一個sigmadiff用于把所有時間刻的diff累加起來一起賦給數據。不過這樣做的話,就要對每個時間下的每個數據(包括權重)做sigmadiff了,因為一開始求的diff是對bia的偏導數,如果直接全部加起來,獲得的sigmadiff僅僅是對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是一樣的思路,不過數據更加多,而且隨著數據量增加,訓練速度也明顯會變得非常慢(真的非常顯著的變化!)
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];
};
那么針對rnn和lstm的Calc()和Training()函數都要重新編寫哦!
接著就是利用這些單元來寫一些模型,然后對測試好的模型進行封裝。
先拿一開始的BP做個例子吧。思想其實是很簡單的,BP的神經元我們已經有個一個struct來定義了。那么我們用這個struct做一個class,把一些函數也包含進去。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數據,讀入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);
//負號直接舍棄,因為整個傳遞過程這里的負號不帶來影響//而且在最后更新數據的時候也不需要再*(-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這個template初始三個傳參便是建立一個網絡必須要的參數,這種思想在寫其他template封裝時很重要。
neuron是struct單元,包括了基本bp神經元需要的數據,ActivateFunction類包括了一些需要使用的激勵函數。
省時間,一些函數的內容就不多寫了。設計構造函數的時候可以自己創新,想怎么寫怎么寫,我這里構造函數先初始化了epoch,sigmerror,error,batch_size,還有learningrate。(直接把函數內容寫class里面是被template逼的……教授要是看到了會罵死我)
Mainwork函數一般推薦你不要封裝進去。。因為bp可能會被用來處理各種各樣的問題,為了保證靈活性,Mainwork還是自己在外面寫吧,要什么功能再加進去就是了。
寫個小bug(誤)來看看是否運行正常:
沒有問題,因為我沒有訓練集,所以在構造函數里判斷出來了,直接退出了程序。
更新內容基本結束~
2019.5.14更新
這次課設就寫了相關的代碼,不過和答案里提供的方法不太一樣,這個頭文件庫里面所有的網絡建立都是通過constructor傳參+內存分配完成的,沒有使用template。https://github.com/ValKmjolnir/easyNLP
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀總結
以上是生活随笔為你收集整理的python实现神经网络的正向传播(fp)函数_如何自己从零实现一个神经网络?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mysql web备份软件_Window
- 下一篇: java开发数据库属于后端吗_JavaW