RNN求解過程推導與實現
BPTT,Back Propagation Through Time.
首先來看看怎么處理RNN。
RNN展開網絡如下圖


現令第t時刻的輸入表示為,隱層節點的輸出為
,輸出層的預測值
,輸入到隱層的權重矩陣
,隱層自循環的權重矩陣
,隱層到輸出層的權重矩陣
,對應的偏執向量分別表示為
,輸入層的某一個節點使用i標識,如
,類似的隱層和輸出層某一節點表示為
。這里我們僅以三層網絡為例。
那么首先正向計算

其中分別表示激活前對應的加權和,
表示激活函數。
然后看看誤差如何傳遞。
假設真實的輸出應該是,那么誤差可以定義為
,
是訓練樣本的index。整個網絡的誤差
我們將RNN再放大一些,看看細節

令則

矩陣向量化表示

所以梯度為:

其中是點乘符號,即對應元素乘。
代碼實現:
我們可以注意到在計算梯度時需要用到的之前計算過的量,即需要保存的量包括,前向計算時隱層節點和輸出節點的輸出值,以及由時刻累積的
。
人人都能用Python寫出LSTM-RNN的代碼![你的神經網絡學習最佳起步]這篇文章里使用python實現了基本的RNN過程。代碼功能是模擬二進制相加過程中的依次進位過程,代碼很容易明白。
這里改寫成matlab代碼
- function error = binaryRNN( )
- largestNumber=256;
- T=8;
- dic=dec2bin(0:largestNumber-1)-'0';% 將uint8表示成二進制數組,這是一個查找表
- %% 初始化參數
- eta=0.1;% 學習步長
- inputDim=2;% 輸入維度
- hiddenDim=16; %隱層節點個數
- outputDim=1; % 輸出層節點個數
-
- W=rand(hiddenDim,outputDim)*2-1;% (-1,1)參數矩陣
- U=rand(hiddenDim,hiddenDim)*2-1;% (-1,1)參數矩陣
- V=rand(inputDim,hiddenDim)*2-1; % (-1,1)參數矩陣
-
- delta_W=zeros(hiddenDim,outputDim); % 時刻間中間變量
- delta_U=zeros(hiddenDim,hiddenDim);
- delta_V=zeros(inputDim,hiddenDim);
- error=0;
- for p=1:10000
- aInt=randi(largestNumber/2);
- bInt=randi(largestNumber/2);
- a=dic(aInt+1,:);
- b=dic(bInt+1,:);
- cInt=aInt+bInt;
- c=dic(cInt+1,:);
- y=zeros(1,T);
-
- preh=zeros(1,hiddenDim);
- hDic=zeros(T,hiddenDim);
- %% 前向計算
- for t=T:-1:1 % 注意應該從最低位計算,也就是二進制數組最右端開始計算
- x=[a(t),b(t)];
- h=sigmoid(x*V+preh*U);
- y(t)=sigmoid(h*W);
- hDic(t,:)=h;
- preh=h;
- end
-
- err=y-c;
- error=error+norm(err,2)/2;
- next_delta_h=zeros(1,hiddenDim);
- %% 反饋
- for t=1:T
- delta_y = err(t).*sigmoidOutput2d(y(t));
- delta_h=(delta_y*W'+next_delta_h*U').*sigmoidOutput2d(hDic(t,:));
-
- delta_W=delta_W+hDic(t,:)'*delta_y;
- if t<T
- delta_U=delta_U+hDic(t+1,:)'*delta_h;
- end
- delta_V=delta_V+[a(t),b(t)]'*delta_h;
- next_delta_h=delta_h;
- end
- % 梯度下降
- W=W-eta*delta_W;
- U=U-eta*delta_U;
- V=V-eta*delta_V;
-
- delta_W=zeros(hiddenDim,outputDim);
- delta_U=zeros(hiddenDim,hiddenDim);
- delta_V=zeros(inputDim,hiddenDim);
-
- if mod(p,1000)==0
- fprintf('Samples:%d\n',p);
- fprintf('True:%d\n',cInt);
- fprintf('Predict:%d\n',bin2dec(int2str(round(y))));
- fprintf('Error:%f\n',norm(err,2)/2);
- end
- end
- end
-
- function sx=sigmoid(x)
- sx=1./(1+exp(-x));
- end
-
- function dx=sigmoidOutput2d(output)
- dx=output.*(1-output);
- end
為了更深入理解RNN過程,這里我想用OpenCV和C++實現自己的RNN,簡單的單隱層網絡。同樣類似的功能,模擬多個十進制數的加法進位過程。
- # include "highgui.h"
- # include "cv.h"
- # include <iostream>
- #include "math.h"
- #include<cstdlib>
- using namespace std;
-
- # define random(x) ((rand()*rand())%x) //生成0-x的隨機數
-
- void Display(CvMat* mat)
- {
- cout << setiosflags(ios::fixed);
- for (int i = 0; i < mat->rows; i++)
- {
- for (int j = 0; j < mat->cols; j++)
- cout << cvmGet(mat, i, j) << " ";
- cout << endl;
- }
-
- }
-
-
- // sigmoid 函數
- float sigmoid(float x)
- {
- return 1 / (1 + exp(-x));
- }
- CvMat* sigmoidM(CvMat* mat)
- {
- CvMat*mat2 = cvCloneMat(mat);
-
- for (int i = 0; i < mat2->rows; i++)
- {
- for (int j = 0; j < mat2->cols; j++)
- cvmSet(mat2, i, j, sigmoid(cvmGet(mat, i, j)));
- }
- return mat2;
- }
- //sigmoid 函數的導數
- float diffSigmoid(float x)
- {
- //注意,這里的x已經是sigmoid的結果
- return x*(1 - x);
- }
- CvMat* diffSigmoidM(CvMat* mat)
- {
- CvMat* mat2 = cvCloneMat(mat);
-
- for (int i = 0; i < mat2->rows; i++)
- {
- for (int j = 0; j < mat2->cols; j++)
- {
- float t = cvmGet(mat, i, j);
- cvmSet(mat2, i, j, t*(1 - t));
- }
-
- }
- return mat2;
-
- }
-
-
- /**************隨機生成inputdim個整數,並求和******************
- * inputdim 整數的個數
- * MAX 整數的最大范圍
- * Sample 存放整數
- * 返回 整數和
- **************************************************************/
- int sample(int inputdim, CvMat* Sample,int MAX)
- {
- int sum = 0;
- for (int i = 0; i < inputdim; i++)
- {
- int t = random(MAX);
- cvmSet(Sample, 0, i, t);
- sum += cvmGet(Sample,0,i);
- }
- return sum;
- }
- /********將整數拆分成10以內的數,作為每個時刻的輸入*************
- * Sample 存放的整數 大小 1*inputdim
- * 返回 拆分后的輸入數據 大小 inputdim*9
- ****************************************************************/
- CvMat* splitM( CvMat*Sample)
- {
- CvMat* mat = cvCreateMat(Sample->cols, 8, CV_32F);
- cvSetZero(mat);
- for (int i = 0; i < mat->rows; i++)
- {
- int x = cvmGet(Sample,0,i);
- for (int j = 0; j < 8; ++j)
- {
- cvmSet(mat,i,j, x % 10);
- x = x / 10;
- }
- }
- return mat;
- }
-
- /***************將數字數組整合成一個整數******************************
- *mat 數字數組,即每個元素是十以內的整數,大小1*9
- *返回 整合后的整數
- *********************************************************************/
- int merge(CvMat* mat)
- {
- double d = 0;
- for (int i = mat->cols; i >0; i--)
- {
- d = 10 * d + round(10*(cvmGet(mat,0,i-1)));
-
- }
- return int(d);
- }
- /*****************將輸出的數值拆分**************************************
- * y 輸出的數值
- * 返回 長度為9的數組,這里轉換成了0,1之間的數
- ***********************************************************************/
- CvMat* split(int y)
- {
- CvMat* mat = cvCreateMat(1, 8, CV_32F);
- for (int i = 0; i < 8; i++)
- {
- cvmSet(mat,0,i, (y % 10) / 10.0);
- y = y / 10;
- }
- return mat;
-
- }
-
- /**********************產生隨機矩陣******************************
- * rows, cols, 矩陣的規模
- * a, b, 區間
- * 返回 返回[a,b]之間的隨機矩陣
- *****************************************************************/
- CvMat*randM(int rows,int cols, float a,float b)
- {
- CvMat* mat = cvCreateMat(rows, cols, CV_32FC1);
- float* ptr;
- for (int i = 0; i < mat->rows; i++)
- {
- for (int j = 0; j < mat->cols; j++)
- {
- cvmSet(mat, i, j, random(1000) / 1000.0*(b - a) + a);
- }
- }
- return mat;
- }
-
- int main()
- {
- srand(time(NULL));
- //首先,先定義網絡
- int inputdim = 2;//不超過10
- int hiddendim = 16;
- int outputdim = 1;
- float eta = 0.1;
- int MAX = 100000000;//令整數最多八位
- //初始化參數矩陣
- CvMat* V = randM(inputdim, hiddendim,-1,1);
- CvMat* U = randM(hiddendim, hiddendim, -1, 1);
- CvMat* W = randM(hiddendim, outputdim, -1, 1);
- CvMat* bh = randM(1, hiddendim, -1, 1);
- CvMat* by = randM(1, outputdim, -1, 1);//偏置
-
- CvMat*Sample = cvCreateMat(1, inputdim, CV_32F);
- cvSetZero(Sample);
- CvMat* delta_V = cvCloneMat(V);
- CvMat* delta_U = cvCloneMat(U);
- CvMat* delta_W = cvCloneMat(W);
- CvMat* delta_by = cvCloneMat(by);
- CvMat* delta_bh = cvCloneMat(bh);
-
- //開始訓練,訓練集大小10000
- for (int p = 0; p < 20000; p++)
- {
- int sum = sample(inputdim,Sample,MAX);
- CvMat* sampleM = splitM(Sample);//每一行對應着一個整數的拆分,個位在前
- CvMat* d = split(sum);//真實結果拆分,每位存放的是除以10后的小數
- //正向計算
- CvMat* pre_h = cvCreateMat(1, hiddendim, CV_32F);
- cvSetZero(pre_h);//初始化最開始的h_{-1}
- CvMat* y = cvCreateMat(1, 8, CV_32F);
- cvSetZero(y);//定義輸出量
- CvMat* h = cvCreateMat(8, hiddendim, CV_32F);//每一行存儲一個時刻的隱變量輸出
-
- CvMat* temp1 = cvCreateMat(1, hiddendim, CV_32F);
- CvMat* temp2 = cvCreateMat(1, outputdim, CV_32F);
- CvMat* xt = cvCreateMatHeader(inputdim, 1, CV_32S);
- for (int t = 0; t < 8; t++)
- {
- cvGetCol(sampleM, xt, t);//獲取第t時刻輸入值
- cvGEMM(xt, V, 1,bh, 1, temp1, CV_GEMM_A_T);
- cvGEMM(pre_h, U, 1, temp1, 1, pre_h);// t時刻隱層輸出
- pre_h = sigmoidM(pre_h);
-
- cvGEMM(pre_h, W, 1, by, 1, temp2);
- float yvalue = sigmoid(cvmGet(temp2, 0, 0));
- cvmSet(y, 0, t, yvalue);//t時刻的輸出
-
- //保存隱層輸出
- for (int j = 0; j < hiddendim; j++)
- {
- cvmSet(h, t, j, cvmGet(pre_h, 0, j));
- }
- }
- cvReleaseMat(&temp1);
- cvReleaseMat(&temp2);
-
- //觀察代碼
- int oy = merge(y);
- CvMat* temp = cvCreateMat(1, 8, CV_32F);
- cvSub(y, d, temp);
- double error = 0.5*cvDotProduct(temp, temp);
- if ((p+1)%1000==0)
- {
- cout << "************************第" << p + 1 << "個樣本***********" << endl;
- cout << "真實值:" << sum%MAX << endl;
- cout << "預測值:" << oy << endl;
- cout << "誤差:" << error << endl;
- }
- //反向傳遞誤差
- cvSetZero(delta_V);
- cvSetZero(delta_U);
- cvSetZero(delta_W);
- cvSetZero(delta_bh);
- cvSetZero(delta_by);
-
- CvMat* delta_h = cvCreateMat(1, hiddendim, CV_32F);
- cvSetZero(delta_h);
- CvMat* delta_y = cvCreateMat(1, outputdim, CV_32F);
- cvSetZero(delta_y);
- CvMat* next_delta_h = cvCreateMat(1, hiddendim, CV_32F);
- cvSetZero(next_delta_h);
-
- for (int t = 7; t > 0; t--)
- {
- cvmSet(delta_y, 0, 0, (cvmGet(y, 0, t) - cvmGet(d, 0, t))*diffSigmoid(cvmGet(y, 0, t)));
- cvGEMM(delta_y, W, 1, delta_h, 0, delta_h, CV_GEMM_B_T);
- cvGEMM(next_delta_h, U, 1, delta_h, 1, delta_h, CV_GEMM_B_T);
- cvMul(delta_h, diffSigmoidM(cvGetRow(h, temp, t)), delta_h);
- //更新delta_y,delta_h
- cvGEMM(cvGetRow(h, temp, t), delta_y, 1, delta_W, 1, delta_W, CV_GEMM_A_T);
- if (t>0)
- cvGEMM(cvGetRow(h, temp, t - 1), delta_h, 1, delta_U, 1, delta_U, CV_GEMM_A_T);
- cvGetCol(sampleM, xt, t);
- cvGEMM(xt, delta_h, 1, delta_V, 1, delta_V);
- cvAddWeighted(delta_by, 1, delta_y, 1, 0, delta_by);
- cvAddWeighted(delta_bh, 1, delta_h, 1, 0, delta_bh);
-
- cvAddWeighted(delta_h, 1, next_delta_h, 0, 0, next_delta_h);
-
- }
- cvAddWeighted(W, 1, delta_W, -eta, 0, W);
- cvAddWeighted(V, 1, delta_V, -eta, 0, V);
- cvAddWeighted(U, 1, delta_U, -eta, 0, U);
-
- cvAddWeighted(by, 1, delta_by, -eta, 0, by);
- cvAddWeighted(bh, 1, delta_bh, -eta, 0, bh);
-
- cvReleaseMat(&sampleM);
- cvReleaseMat(&d);
- cvReleaseMat(&pre_h);
- cvReleaseMat(&y);
- cvReleaseMat(&h);
- cvReleaseMat(&delta_h);
- cvReleaseMat(&delta_y);
- }
- cvReleaseMat(&U);
- cvReleaseMat(&V);
- cvReleaseMat(&W);
- cvReleaseMat(&by);
- cvReleaseMat(&bh);
- cvReleaseMat(&Sample);
- cvReleaseMat(&delta_V);
- cvReleaseMat(&delta_U);
- cvReleaseMat(&delta_W);
- cvReleaseMat(&delta_by);
- cvReleaseMat(&delta_bh);
- system("PAUSE");
- return 0;
- }
下面是代碼結果,並沒有完全一致。分析下主要原因可能是由於輸出層是(0,1)的小數,但我們希望得到的是[0,10]的整數,而通過round或者強制類型轉換總會帶來較大誤差,所以會出現預測值和真實值差別很大,這時候其實比較值的差異意義不大,應該對比每一位上數字的差異。

再下面是3個輸入,32個隱層節點的結果

PS. 作為opencv新手,覺得matlab半小時搞定的東西,opencv要搗鼓兩個小時。。。