飞道的博客

卷积神经网络原理及其C++/Opencv实现(6)—前向传播代码实现

504人阅读  评论(0)

首先列出本系列博文的链接:

1. 卷积神经网络原理及其C++/Opencv实现(1)

2. 卷积神经网络原理及其C++/Opencv实现(2)

3. 卷积神经网络原理及其C++/Opencv实现(3)

4. 卷积神经网络原理及其C++/Opencv实现(4)—误反向传播法

5. 卷积神经网络原理及其C++/Opencv实现(5)—参数更新

在以上文章中,我们基本把5层网络的原理、公式推导讲过了,从本文开始,我们来讲一下基于C++和Opencv的5层卷积神经网络实现吧~

1. 结构体定义

(1) 卷积层的结构体


   
  1. typedef struct convolutional_layer
  2. {
  3. int inputWidth; //输入图像的宽
  4. int inputHeight; //输入图像的长
  5. int mapSize; //卷积核的尺寸
  6. int inChannels; //输入图像的数目
  7.    int outChannels;   //输出图像的数目
  8.   
  9.    vector< vector<Mat>> mapData;   //四维float数组,卷积核本身是二维数据,m*n哥卷积核就是四维数组
  10.   Mat basicData;  //偏置,个数为outChannels, 一维float数组
  11.    bool isFullConnect;  //是否为全连接
  12. vector<Mat> v; //进入激活函数的输入值,三维数组float型
  13.    vector<Mat> y;    //激活函数后神经元的输出,三维数组float型
  14. vector<Mat> d; // 网络的局部梯度,三维数组float型
  15. }CovLayer;

(2) 池化层的结构体


   
  1. typedef struct pooling_layer
  2. {
  3. int inputWidth; //输入图像的宽
  4. int inputHeight; //输入图像的长
  5. int mapSize; //卷积核的大小
  6. int inChannels; //输入图像的数目
  7. int outChannels; //输出图像的数目
  8. int poolType; //池化的方法
  9. Mat basicData; //偏置, 一维float数组
  10. vector<Mat> y; //采样函数后神经元的输出,无激活函数,三维数组float型
  11. vector<Mat> d; //网络的局部梯度,三维数组float型
  12. vector<Mat> max_position; // 最大值模式下最大值的位置,三维数组float型
  13. }PoolLayer;

(3) 输出层的结构


   
  1. typedef struct nn_layer
  2. {
  3. int inputNum; //输入数据的数目
  4.    int outputNum;   //输出数据的数目
  5.   
  6. Mat wData; // 权重数据,为一个inputNum*outputNum大小
  7. Mat basicData; //偏置,大小为outputNum大小
  8. Mat v; // 进入激活函数的输入值
  9. Mat y; // 激活函数后神经元的输出
  10.   Mat d;      // 网络的局部梯度
  11. bool isFullConnect; //是否为全连接
  12. }OutLayer;

(4) 5层网络的结构体


   
  1. typedef struct cnn_network
  2. {
  3. int layerNum;
  4. CovLayer C1;
  5. PoolLayer S2;
  6. CovLayer C3;
  7. PoolLayer S4;
  8. OutLayer O5;
  9. Mat e; // 训练误差
  10.   Mat L;   // 瞬时误差能量
  11. }CNN;

(5) 训练参数的结构体


   
  1. typedef struct train_opts
  2. {
  3. int numepochs; // 训练的迭代次数
  4.    float alpha;  // 学习率
  5. }CNNOpts;

2. 5层网络的初始化

(1) 卷积层结构体初始化


   
  1. CovLayer initCovLayer(int inputWidth, int inputHeight, int mapSize, int inChannels, int outChannels)
  2. {
  3. CovLayer covL;
  4. covL.inputHeight = inputHeight;
  5. covL.inputWidth = inputWidth;
  6. covL.mapSize = mapSize;
  7. covL.inChannels = inChannels;
  8. covL.outChannels = outChannels;
  9. covL.isFullConnect = true; // 默认为全连接
  10. // 权重空间的初始化,先行再列调用,[r][c]
  11. srand(( unsigned)time( NULL)); //设置随机数种子
  12. for( int i = 0; i < inChannels; i++) //输入通道数
  13. {
  14. vector<Mat> tmp;
  15. for( int j = 0; j < outChannels; j++) //输出通道数
  16. {
  17. Mat tmpmat(mapSize, mapSize, CV_32FC1); //初始化一个mapSize*mapSize的二维矩阵
  18. for( int r = 0; r < mapSize; r++) //卷积核的高
  19. {
  20. for( int c = 0; c < mapSize; c++) //卷积核的宽
  21. {
  22. //使用随机数初始化卷积核
  23. float randnum=((( float)rand()/( float)RAND_MAX) -0.5)* 2; //生成-1~1的随机数
  24.           tmpmat.ptr< float>(r)[c] = randnum* sqrt( 6.0/(mapSize*mapSize*(inChannels+outChannels)));
  25. }
  26. }
  27. tmp.push_back(tmpmat.clone());
  28. }
  29. covL.mapData.push_back(tmp);
  30. }
  31. covL.basicData = Mat::zeros( 1, outChannels, CV_32FC1); //初始化卷积层偏置的内存
  32. int outW = inputWidth - mapSize + 1; //valid模式下卷积层输出的宽
  33. int outH = inputHeight - mapSize + 1; //valid模式下卷积层输出的高
  34. Mat tmpmat2 = Mat::zeros(outH, outW, CV_32FC1);
  35. for( int i = 0; i < outChannels; i++)
  36. {
  37. covL.d.push_back(tmpmat2.clone()); //初始化局部梯度
  38.     covL.v.push_back(tmpmat2.clone());   //初始化输入激活函数之前的值
  39. covL.y.push_back(tmpmat2.clone()); //初始化输入激活函数之后的值
  40. }
  41.    return covL;    //返回初始化之后的卷积层结构体
  42. }

(2) 池化层结构体初始化


   
  1. PoolLayer initPoolLayer(int inputWidth, int inputHeight, int mapSize, int inChannels, int outChannels, int poolType)
  2. {
  3. PoolLayer poolL;
  4. poolL.inputHeight=inputHeight; //输入高度
  5. poolL.inputWidth=inputWidth; //输入宽度
  6. poolL.mapSize=mapSize; //卷积核尺寸,池化层相当于做一个特殊的卷积操作
  7. poolL.inChannels=inChannels; //输入通道
  8. poolL.outChannels=outChannels; //输出通道
  9. poolL.poolType=poolType; //最大值模式1/平均值模式0
  10. poolL.basicData = Mat::zeros( 1, outChannels, CV_32FC1); //池化层无偏置,无激活,这里只是预留偏置内存
  11. int outW = inputWidth/mapSize; //池化层的卷积核为2*2
  12. int outH = inputHeight/mapSize;
  13. Mat tmpmat = Mat::zeros(outH, outW, CV_32FC1);
  14. Mat tmpmat1 = Mat::zeros(outH, outW, CV_32SC1);
  15. for( int i = 0; i < outChannels; i++)
  16. {
  17. poolL.d.push_back(tmpmat.clone()); //局域梯度
  18. poolL.y.push_back(tmpmat.clone()); //采样函数后神经元输出,无激活函数
  19. poolL.max_position.push_back(tmpmat1.clone()); //最大值模式下最大值在原矩阵中的位置
  20. }
  21. return poolL;
  22. }

(3) 输出层结构体初始化


   
  1. OutLayer initOutLayer(int inputNum, int outputNum)
  2. {
  3. OutLayer outL;
  4. outL.inputNum = inputNum;
  5. outL.outputNum = outputNum;
  6. outL.isFullConnect = true;
  7. outL.basicData = Mat::zeros( 1, outputNum, CV_32FC1); //偏置,分配内存的同时初始化为0
  8. outL.d = Mat::zeros( 1, outputNum, CV_32FC1);
  9. outL.v = Mat::zeros( 1, outputNum, CV_32FC1);
  10. outL.y = Mat::zeros( 1, outputNum, CV_32FC1);
  11. // 权重的初始化
  12. outL.wData = Mat::zeros(outputNum, inputNum, CV_32FC1); // 输出行,输入列,权重为10*192矩阵
  13. srand(( unsigned)time( NULL));
  14. for( int i = 0; i < outputNum; i++)
  15. {
  16. float *p = outL.wData.ptr< float>(i);
  17. for( int j = 0; j < inputNum; j++)
  18. {
  19. //使用随机数初始化权重
  20. float randnum = ((( float)rand()/( float)RAND_MAX) -0.5)* 2; // 产生一个-1到1的随机数,rand()的取值范围为0~RAND_MAX
  21.       p[j] = randnum* sqrt( 6.0/(inputNum+outputNum));
  22. }
  23. }
  24. return outL;
  25. }

(4) 5层网络结构体初始化


   
  1. void cnnsetup(CNN &cnn, int inputSize_r, int inputSize_c, int outputSize) //cnn初始化
  2. {
  3. cnn.layerNum = 5;
  4. //C1层
  5. int mapSize = 5;
  6. int inSize_c = inputSize_c; //28
  7. int inSize_r = inputSize_r; //28
  8. int C1_outChannels = 6;
  9. cnn.C1 = initCovLayer(inSize_c, inSize_r, mapSize, 1, C1_outChannels); //卷积层1
  10. //S2层
  11. inSize_c = inSize_c - cnn.C1.mapSize + 1; //24
  12. inSize_r = inSize_r - cnn.C1.mapSize + 1; //24
  13. mapSize = 2;
  14. cnn.S2 = initPoolLayer(inSize_c, inSize_r, mapSize, cnn.C1.outChannels, cnn.C1.outChannels, MaxPool); //池化层
  15.   
  16.    //C3层
  17. inSize_c = inSize_c / cnn.S2.mapSize; //12
  18. inSize_r = inSize_r / cnn.S2.mapSize; //12
  19. mapSize = 5;
  20. int C3_outChannes = 12;
  21. cnn.C3 = initCovLayer(inSize_c, inSize_r, mapSize, cnn.S2.outChannels, C3_outChannes); //卷积层
  22. //S4层
  23. inSize_c = inSize_c - cnn.C3.mapSize + 1; //8
  24. inSize_r = inSize_r - cnn.C3.mapSize + 1; //8
  25. mapSize = 2;
  26. cnn.S4 = initPoolLayer(inSize_c, inSize_r, mapSize, cnn.C3.outChannels, cnn.C3.outChannels, MaxPool); //池化层
  27. //O5层
  28. inSize_c = inSize_c / cnn.S4.mapSize; //4
  29. inSize_r = inSize_r / cnn.S4.mapSize; //4
  30. cnn.O5 = initOutLayer(inSize_c*inSize_r*cnn.S4.outChannels, outputSize); //输出层
  31. cnn.e = Mat::zeros( 1, cnn.O5.outputNum, CV_32FC1); //输出层的输出值与标签值之差
  32. }

3. 二维图像的卷积实现

调用Opencv的filter2D函数,可以很方便、很快速地实现二维卷积运算。我们首先实现full模式,valid和same模式地卷积结果可以直接从full模式的结果中截取。

需要注意的是,在卷积神经网络中,我们说的卷积运算其实是互相关运算,也即开始卷积运算之前卷积核不需要做顺时针180°的旋转。


   
  1. Mat correlation(Mat map, Mat inputData, int type)
  2. {
  3. const int map_row = map.rows;
  4. const int map_col = map.cols;
  5. const int map_row_2 = map.rows/ 2;
  6. const int map_col_2 = map.cols/ 2;
  7. const int in_row = inputData.rows;
  8. const int in_col = inputData.cols;
  9. //先按full模式扩充图像边缘
  10. Mat exInputData;
  11. copyMakeBorder(inputData, exInputData, map_row_2, map_row_2, map_col_2, map_col_2, BORDER_CONSTANT, 0);
  12. Mat OutputData;
  13. filter2D(exInputData, OutputData, exInputData.depth(), map);
  14. if( type == full) //full模式
  15. {
  16. return OutputData;
  17. }
  18. else if( type == valid) //valid模式
  19. {
  20. int out_row = in_row - (map_row - 1);
  21. int out_col = in_col - (map_col - 1);
  22. Mat outtmp;
  23. OutputData(Rect( 2*map_col_2, 2*map_row_2, out_col, out_row)).copyTo(outtmp);
  24. return outtmp;
  25. }
  26. else //same模式
  27. {
  28. Mat outtmp;
  29. OutputData(Rect(map_col_2, map_row_2, in_col, in_row)).copyTo(outtmp);
  30. return outtmp;
  31. }
  32. }

4. 池化层的实现

(1) 均值池化


   
  1. void avgPooling(Mat input, Mat &output, int mapSize)
  2. {
  3. const int outputW = input.cols/mapSize; //输出宽=输入宽/核宽
  4. const int outputH = input.rows/mapSize; //输出高=输入高/核高
  5. float len = ( float)(mapSize*mapSize);
  6. int i,j,m,n;
  7. for(i = 0;i < outputH; i++)
  8. {
  9. for(j = 0; j < outputW; j++)
  10. {
  11. float sum= 0.0;
  12. for(m = i*mapSize; m < i*mapSize+mapSize; m++) //取卷积核大小的窗口求和平均
  13. {
  14. for(n = j*mapSize; n < j*mapSize+mapSize; n++)
  15. {
  16. sum += input.ptr< float>(m)[n];
  17. }
  18. }
  19. output.ptr< float>(i)[j] = sum/len;
  20. }
  21. }
  22. }

(2) 最大值池化


   
  1. void maxPooling(Mat input, Mat &max_position, Mat &output, int mapSize)
  2. {
  3. int outputW = input.cols / mapSize; //输出宽=输入宽/核宽
  4. int outputH = input.rows / mapSize; //输出高=输入高/核高
  5. int i, j, m, n;
  6. for (i = 0; i < outputH; i++)
  7. {
  8. for (j = 0; j < outputW; j++)
  9. {
  10. float max = -999999.0;
  11. int max_index = 0;
  12. for (m = i*mapSize; m<i*mapSize + mapSize; m++) //取卷积核大小的窗口的最大值
  13. {
  14. for (n = j*mapSize; n<j*mapSize + mapSize; n++)
  15. {
  16. if (max < input.ptr< float>(m)[n]) //求池化窗口中的最大值,并记录最大值位置
  17. {
  18. max = input.ptr< float>(m)[n];
  19. max_index = m*input.cols + n;
  20. }
  21. }
  22. }
  23.       output.ptr< float>(i)[j] = max;     //求得最大值作为池化输出
  24. max_position.ptr< int>(i)[j] = max_index; //记录最大值在原矩阵中的位置,用于反向传播
  25. }
  26. }
  27. }

5. 激活函数与向量点乘函数的实现

(1) Relu函数


   
  1. float activation_Sigma( float input, float bas)
  2. {
  3. float temp = input + bas;
  4.    return (temp > 0 ? temp: 0);
  5. }

(2) Softmax函数


   
  1. void softmax(OutLayer &O)
  2. {
  3. float sum = 0.0;
  4. float *p_y = O.y.ptr< float>( 0);
  5. float *p_v = O.v.ptr< float>( 0);
  6. float *p_b = O.basicData.ptr< float>( 0);
  7. for ( int i = 0; i < O.outputNum; i++)
  8.   {
  9.      float Yi =  exp(p_v[i]+ p_b[i]);
  10. sum += Yi;
  11. p_y[i] = Yi;
  12. }
  13. for ( int i = 0; i < O.outputNum; i++)
  14. {
  15. p_y[i] = p_y[i]/sum;
  16. }
  17. }

(3) 两个一维向量的点乘函数

以下函数中,vec1和vec2是两个长度相同的一维向量,点乘的结果就是它们对应位置的值相乘,然后把所有乘积相加的结果。


   
  1. float vecMulti(Mat vec1, float *vec2)// 两向量相乘
  2. {
  3. float *p1 = vec1.ptr< float>( 0);
  4. float m = 0;
  5. for ( int i = 0; i < vec1.cols; i++)
  6. m = m + p1[i] * vec2[i];
  7. return m;
  8. }

6. 5层网络前向传播的实现

(1) 卷积层前向传播


   
  1. //输入的inputData有可能是一张图像,也有可能是多张图像,如果是多张图像,则把它们的卷积结果累加起来
  2. void cov_layer_ff(vector<Mat> inputData, int cov_type, CovLayer &C)
  3. {
  4. for ( int i = 0; i < (C.outChannels); i++)
  5. {
  6. for ( int j = 0; j < (C.inChannels); j++)
  7. {
  8.        //计算卷积,mapData为四维矩阵 
  9. Mat mapout = correlation(C.mapData[j][i], inputData[j], cov_type);
  10.       C.v[i] += mapout;      //所有输入通道的卷积结果累加
  11. }
  12. int output_r = C.y[i].rows;
  13. int output_c = C.y[i].cols;
  14. for ( int r = 0; r < output_r; r++)
  15. {
  16. for ( int c = 0; c < output_c; c++)
  17. {
  18. C.y[i].ptr< float>(r)[c] = activation_Sigma(C.v[i].ptr< float>(r)[c], C.basicData.ptr< float>( 0)[i]); //先加上偏置,再输入激活函数
  19. }
  20. }
  21. }
  22. }

(2) 池化层前向传播


   
  1. #define AvePool 0
  2. #define MaxPool 1
  3. void pool_layer_ff(vector<Mat> inputData, int pool_type, PoolLayer &S)
  4. {
  5. if (pool_type == AvePool) //均值池化
  6. {
  7. for ( int i = 0; i < S.outChannels; i++)
  8. {
  9. avgPooling(inputData[i], S.y[i], S.mapSize);
  10. }
  11. }
  12. else if(pool_type == MaxPool) //最大值池化
  13. {
  14. for ( int i = 0; i < S.outChannels; i++)
  15. {
  16. maxPooling(inputData[i], S.max_position[i], S.y[i], S.mapSize);
  17. }
  18. }
  19. else
  20. {
  21. printf( "pool type erroe!\n");
  22. }
  23. }

(3) 输出层前向传播


   
  1. void nnff(Mat input, Mat wdata, Mat &output)
  2. {
  3. for ( int i = 0; i < output.cols; i++) //分别计算多个向量相乘的乘积
  4. output.ptr< float>( 0)[i] = vecMulti(input, wdata.ptr< float>(i)); //由于输入激活函数之前就有加上偏置的操作,所以此处不再加偏置
  5. }
  6. void out_layer_ff(vector<Mat> inputData, OutLayer &O)
  7. {
  8. Mat OinData(1, O.inputNum, CV_32FC1); //输入192通道
  9. float *OinData_p = OinData.ptr< float>( 0);
  10. int outsize_r = inputData[ 0].rows;
  11. int outsize_c = inputData[ 0].cols;
  12. int last_output_len = inputData.size();
  13. for ( int i = 0; i < last_output_len; i++) //上一层S4输出12通道的4*4矩阵
  14. {
  15. for ( int r = 0; r < outsize_r; r++)
  16. {
  17. for ( int c = 0; c < outsize_c; c++)
  18. {
  19.          //将12通道4*4矩阵展开成长度为192的一维向量
  20. OinData_p[i*outsize_r*outsize_c + r*outsize_c + c] = inputData[i].ptr< float>(r)[c];
  21. }
  22. }
  23. }
  24. //192*10个权重
  25.   nnff(OinData, O.wData, O.v);    //10通道输出,1个通道的输出等于192个输入分别与192个权重相乘的和:∑in[i]*w[i], 0≤i<192
  26.    //Affine层的输出经过Softmax函数,转换成0~1的输出结果
  27. softmax(O);
  28. }

(4) 5层网络前向传播


   
  1. void cnnff(CNN &cnn, Mat inputData)
  2. {
  3. //C1
  4. //5*5卷积核
  5. //输入28*28矩阵
  6.    //输出(28-25+1)*(28-25+1) = 24*24矩阵
  7. vector<Mat> input_tmp;
  8. input_tmp.push_back(inputData);
  9. cov_layer_ff(input_tmp, valid, cnn.C1);
  10.   
  11.    //S2
  12. //24*24-->12*12
  13. pool_layer_ff(cnn.C1.y, MaxPool, cnn.S2);
  14. //C3
  15. //12*12-->8*8
  16. cov_layer_ff(cnn.S2.y, valid, cnn.C3);
  17. //S4
  18. //8*8-->4*4
  19. pool_layer_ff(cnn.C3.y, MaxPool, cnn.S4);
  20. //O5
  21. //12*4*4-->192-->1*10
  22. out_layer_ff(cnn.S4.y, cnn.O5);
  23. }

好了,本文就讲到这里,接下来的文章我们来讲反向传播的实现和参数更新的实现,敬请期待~

欢迎扫码关注以下微信公众号,接下来会不定时更新更加精彩的内容噢~


转载:https://blog.csdn.net/shandianfengfan/article/details/115388578
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场