小言_互联网的博客

卷积神经网络原理及其C++/Opencv实现(8)—手写数字图像识别

341人阅读  评论(0)

本文是本系列的第8篇文章,也是终结篇章。在本文中我们主要讲5层卷积神经网络参数更新和训练的代码实现,以及如何使用5层卷积神经网络来实现0~9的手写数字图像的识别。

首先还是列出本系列其它博文的超链接,方便读者跳转查阅:

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

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

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

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

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

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

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

下面我们还是分别讲5层网络其余部分的代码实现吧~

1. 训练过程中参数的更新

(1) O5层参数更新

本层需要更新的参数为192*10个权重值,以及10个偏置值。更新公式如下,其中α为学习率,Y为Softmax函数的输出,t为标签,x为Affine层的输入,0≤i<10,0≤j<192。


本层的参数更新代码实现如下:


   
  1. void update_full_para(vector<Mat> inputData, CNNOpts opts, OutLayer &O)
  2. {
  3. int outSize_r = inputData[ 0].rows;
  4. int outSize_c = inputData[ 0].cols;
  5. Mat OinData(1, outSize_r*outSize_c*inputData.size(), CV_32FC1);
  6. for ( int i = 0; i < inputData.size(); i++) //12通道
  7. {
  8. for ( int r = 0; r < outSize_r; r++) //4
  9. {
  10. for ( int c = 0; c < outSize_c; c++) //4
  11. {
  12. //把本层输入的12个4*4图像展开成长度为192的一维向量
  13. OinData.ptr< float>( 0)[i*outSize_r*outSize_c + r*outSize_c + c] = inputData[i].ptr< float>(r)[c];
  14. }
  15. }
  16. }
  17. for ( int j = 0; j < O.outputNum; j++) //10通道
  18. {
  19. for ( int i = 0; i < O.inputNum; i++) //192通道
  20. {
  21.        //w = w - α。dE/dw
  22. O.wData.ptr< float>(j)[i] = O.wData.ptr< float>(j)[i] - opts.alpha*O.d.ptr< float>( 0)[j] * OinData.ptr< float>( 0)[i];
  23. }
  24.      //b = b - α。dE/db
  25. O.basicData.ptr< float>( 0)[j] = O.basicData.ptr< float>( 0)[j] - opts.alpha*O.d.ptr< float>( 0)[j];
  26. }
  27. }

(2) C3层参数更新

本层需要更新的参数为6*12个5*5卷积核,以及12个偏置值。更新公式如下,其中α为学习率,k为本层的卷积核,b为本层的偏置,YS2为S2层的输出,dC3为C3层的局部梯度,sum为求矩阵中所有元素和的操作,0≤i<12,0≤j<6。dC3的计算可参考上篇博文:

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

本层的参数更新代码实现如下:


   
  1. void update_cov_para(vector<Mat> inputData, CNNOpts opts, CovLayer &C)
  2. {
  3. for ( int i = 0; i < C.outChannels; i++) //6通道
  4. {
  5. for ( int j = 0; j < C.inChannels; j++) //1通道
  6.     {
  7. Mat Cdk = correlation(C.d[i], inputData[j], valid); //计算YS2*dC3
  8. Cdk = Cdk*(-opts.alpha); //矩阵乘以系数-α.dE/dk
  9. C.mapData[j][i] = C.mapData[j][i] + Cdk; //计算k = k - α.dE/dk
  10. }
  11. float d_sum = ( float)cv::sum(C.d[i])[ 0]; //计算sum(dC3),这里有6个24*24的d,6个偏置b,一个偏置b对应一个24*24矩阵d的所有元素和
  12.     C.basicData.ptr< float>( 0)[i] = C.basicData.ptr< float>( 0)[i] - opts.alpha*d_sum;   //计算b = b - α.dE/db
  13. }
  14. }

(3) C1层参数更新

本层需要更新的参数为6个5*5卷积核,以及6个偏置值。更新公式如下,其中α为学习率,k为本层的卷积核,b为本层的偏置,IC1为C1层的28*28输入图像(也即5层网络的一张28*28输入图像),dC1为C1层的局部梯度,sum为求矩阵中所有元素和的操作,0≤i<6。dC1的计算也可参考上篇博文。

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

由于本层的参数更新代码操作与C3层一样,只是输入、输出参数不一样而已,因此本层的参数更新也可以调用上述update_cov_para函数来实现。

(4) 所有参数的更新

综上,C1、C3、O5层的参数更新代码如下,其中inputdata为5层网络的单张28*28手写数字图像。


   
  1. void cnnapplygrads(CNN &cnn, CNNOpts opts, Mat inputData) // 更新权重
  2. {
  3. vector<Mat> input_tmp;
  4. input_tmp.push_back(inputData);
  5. update_cov_para(input_tmp, opts, cnn.C1);
  6. update_cov_para(cnn.S2.y, opts, cnn.C3);
  7. update_full_para(cnn.S4.y, opts, cnn.O5);
  8. }

2. 训练过程中参数的清零

由于训练是一个多轮迭代的过程,且训练时会有参数累加的操作,下一轮训练开始之前需要将参数清零,否则累加操作会出问题。


   
  1. //清零卷积层的参数
  2. void clear_cov_mid_para(CovLayer &C)
  3. {
  4. int row = C.d[ 0].rows;
  5. int col = C.d[ 0].cols;
  6. for ( int j = 0; j < C.outChannels; j++)
  7. {
  8. for ( int r = 0; r < row; r++)
  9. {
  10. for ( int c = 0; c < col; c++)
  11. {
  12. C.d[j].ptr< float>(r)[c] = 0.0;
  13. C.v[j].ptr< float>(r)[c] = 0.0;
  14. C.y[j].ptr< float>(r)[c] = 0.0;
  15. }
  16.     }
  17. }
  18. }
  19. //清零池化层的参数
  20. void clear_pool_mid_para(PoolLayer &S)
  21. {
  22. int row = S.d[ 0].rows;
  23. int col = S.d[ 0].cols;
  24. for ( int j = 0; j < S.outChannels; j++)
  25. {
  26. for ( int r = 0; r < row; r++)
  27. {
  28. for ( int c = 0; c < col; c++)
  29. {
  30. S.d[j].ptr< float>(r)[c] = 0.0;
  31. S.y[j].ptr< float>(r)[c] = 0.0;
  32. }
  33.     }
  34. }
  35. }
  36. //清零输出层的参数
  37. void clear_out_mid_para(OutLayer &O)
  38. {
  39. for ( int j = 0; j < O.outputNum; j++)
  40. {
  41. O.d.ptr< float>( 0)[j] = 0.0;
  42. O.v.ptr< float>( 0)[j] = 0.0;
  43. O.y.ptr< float>( 0)[j] = 0.0;
  44.   }
  45. }
  46. //调用上述函数实现5层网络的参数清零
  47. void cnnclear(CNN &cnn)
  48. {
  49. clear_cov_mid_para(cnn.C1);
  50. clear_pool_mid_para(cnn.S2);
  51. clear_cov_mid_para(cnn.C3);
  52. clear_pool_mid_para(cnn.S4);
  53. clear_out_mid_para(cnn.O5);
  54. }

2. 手写数字图像的读取

从网上下载的手写数字图像,是gz压缩文件,需要将其解压:

解压gz文件之后得到以下4个对应文件,其中train-images.idx3-ubyte为训练数据文件,train-labels.idx1-ubyte为训练数据的标签文件,t10k-images.idx3-ubyte为测试数据文件,t10k-labels.idx1-ubyte为测试数据的标签文件。

(1) 训练数据文件与测试数据文件的格式如下图所示:

文件格式:该区域的4个字节数据组成一个int数据,如果该int数据为2051,表示该文件是图像文件,如果是2049表示该文件是文本文件。因此对于训练数据和测试数据文件,本区域的值为2051。

图像总数:该区域的4个字节数据组成一个int数据,该int数据为文件中包含的图像总数。

图像行数:该区域的4个字节数据组成一个int数据,该int数据为每张图像的行数。

图像列数:该区域的4个字节数据组成一个int数据,该int数据为每张图像的列数。

需要注意的是,如果运行程序的处理器为英特尔处理器,需要把读到的4个字节数据按相反顺序排序,再组成int数据,比如首先我们读取到的int数据由byte0、byte1、byte2、byte3这4个字节数据组成(<<为左移运算):

d=(byte3<< 24)) + (byte2<< 16) + (byte1<< 8) + byte0

那么需要把4个数据按照相反顺序排序,重新组成int数据,这个重组的int数据才是我们想要的数据:

d'=(byte0<< 24)) + (byte1<< 16) + (byte2<< 8) + byte3

根据上述格式,训练数据文件与测试数据文件的读取代码如下,我们将同一个文件中的多张图像都读成Opencv的Mat格式,然后将多个Mat格式图像保存进vector数组中:


   
  1. //将int数据中的4个字节数据按相反顺序重新排列,重组成一个int数据
  2. int ReverseInt(int i)
  3. {
  4. unsigned char ch1, ch2, ch3, ch4;
  5. ch1 = i & 0xff;
  6. ch2 = (i >> 8) & 0xff;
  7. ch3 = (i >> 16) & 0xff;
  8. ch4 = (i >> 24) & 0xff;
  9. return (( int)(ch1 << 24)) + (( int)(ch2 << 16)) + (( int)(ch3 << 8)) + ( int)ch4;
  10. }
  11. vector<Mat> read_Img_to_Mat(const char* filename)
  12. {
  13. FILE *fp = NULL;
  14. fp = fopen(filename, "rb");
  15. if (fp == NULL)
  16. printf( "open file failed\n");
  17. assert(fp);
  18. int magic_number = 0;
  19. int number_of_images = 0;
  20. int n_rows = 0;
  21. int n_cols = 0;
  22. fread(&magic_number, sizeof( int), 1, fp); //从文件中读取sizeof(int) 个字符到 &magic_number
  23. magic_number = ReverseInt(magic_number);
  24. fread(&number_of_images, sizeof( int), 1, fp); //获取训练或测试image的个数number_of_images
  25. number_of_images = ReverseInt(number_of_images);
  26. fread(&n_rows, sizeof( int), 1, fp); //获取训练或测试图像的高度Heigh
  27. n_rows = ReverseInt(n_rows);
  28. fread(&n_cols, sizeof( int), 1, fp); //获取训练或测试图像的宽度Width
  29. n_cols = ReverseInt(n_cols);
  30.  
  31. //获取第i幅图像,保存到vec中
  32. int i, r, c;
  33. int img_size = n_rows*n_cols;
  34. vector<Mat> img_list;
  35. for (i = 0; i < number_of_images; ++i)
  36.   {
  37. Mat tmp(n_rows, n_cols, CV_8UC1);
  38. fread(tmp.data, sizeof(uchar), img_size, fp); //读取一张图像
  39. tmp.convertTo(tmp, CV_32F); //将图像转换为float数据
  40.     tmp = tmp /  255.0;    //将数据转换成0~1的数据
  41. img_list.push_back(tmp.clone());
  42. }
  43. fclose(fp);
  44. return img_list;
  45. }

(2) 标签文件的格式如下图所示:

文件格式:该区域的4个字节数据组成一个int数据,如果该int数据为2051,表示该文件是图像文件,如果是2049表示该文件是文本文件。标签文件属于文本文件,因此本区域的值为2049。

图像总数:该区域的4个字节数据组成一个int数据,该int数据为文件中包含的图像总数。

如果运行程序的处理器为英特尔处理器,同样需要把读到的4个字节数据按相反顺序排序,再重组成int数据。

每张图像表示的数字为0~9中的一个数字,因此图像标签就是0~9之中的一个数字,且该数字与图像表示的数字相对应。

由于卷积神经网络使用的是"one-hot"码,因此我们需要把0~9的标签数字转换为"one-hot"码:

0-->1 0 0 0 0 0 0 0 0 0

1-->0 1 0 0 0 0 0 0 0 0

2-->0 0 1 0 0 0 0 0 0 0

3-->0 0 0 1 0 0 0 0 0 0

4-->0 0 0 0 1 0 0 0 0 0

5-->0 0 0 0 0 1 0 0 0 0

6-->0 0 0 0 0 0 1 0 0 0

7-->0 0 0 0 0 0 0 1 0 0

8-->0 0 0 0 0 0 0 0 1 0

9-->0 0 0 0 0 0 0 0 0 1

根据上述格式,标签文件的读取代码如下,我们将同一个标签文件中的每个标签数字转换成"one-hot"码,然后再将"one-hot"码保存到一个1行10列的Mat结构当中,再将Mat保存到vector中:


   
  1. vector<Mat> read_Lable_to_Mat(const char* filename)
  2. {
  3. FILE *fp = NULL;
  4. fp = fopen(filename, "rb");
  5. if (fp == NULL)
  6. printf( "open file failed\n");
  7. assert(fp);
  8. int magic_number = 0;
  9. int number_of_labels = 0;
  10. int label_long = 10;
  11. fread(&magic_number, sizeof( int), 1, fp); //从文件中读取sizeof(magic_number) 个字符到 &magic_number
  12. magic_number = ReverseInt(magic_number);
  13. fread(&number_of_labels, sizeof( int), 1, fp); //获取训练或测试image的个数number_of_images
  14. number_of_labels = ReverseInt(number_of_labels);
  15. int i, l;
  16. vector<Mat> label_list;
  17. for (i = 0; i < number_of_labels; ++i)
  18. {
  19.     
  20.     Mat tmp = Mat::zeros( 1, label_long, CV_32FC1);
  21. unsigned char temp = 0;
  22. fread(&temp, sizeof( unsigned char), 1, fp);
  23.     tmp.ptr< float>( 0)[( int)temp] =  1.0; //将0~9的数字转换成one-hot码
  24. label_list.push_back(tmp.clone());
  25. }
  26. fclose(fp);
  27. return label_list;
  28. }

3. 训练过程的实现代码


   
  1. void cnntrain(CNN &cnn, vector<Mat> inputData, vector<Mat> outputData, CNNOpts opts, int trainNum)
  2. {
  3.    // 学习训练误差曲线,记录交叉熵误差函数的值
  4. cnn.L = Mat( 1, trainNum, CV_32FC1).clone();
  5. for ( int e = 0; e < opts.numepochs; e++) //opts.numepochs表示需要训练次数
  6. {
  7.      for ( int n =  0; n < trainNum; n++)    //trainNum表示由多少张图片,训练完这些图片相当于完成一次训练
  8. {
  9.        //学习率递减0.03~0.001
  10. opts.alpha = 0.03 - 0.029*n / (trainNum - 1);
  11.                      
  12.       cnnff(cnn, inputData[n]);   // 前向传播
  13. cnnbp(cnn, outputData[n]); // 后向传播
  14. cnnapplygrads(cnn, opts, inputData[n]); // 更新参数
  15. // 计算交叉熵误差函数的值
  16. float l = 0.0;
  17. for ( int i = 0; i < cnn.O5.outputNum; i++)
  18. {
  19. l = l - outputData[n].ptr< float>( 0)[i] * log(cnn.O5.y.ptr< float>( 0)[i]);
  20. }
  21. cnn.L.ptr< float>( 0)[n] = l;
  22. cnnclear(cnn); //清零参数
  23. printf( "n=%d, f=%f, α=%f\n", n, cnn.L.ptr< float>( 0)[n], opts.alpha);
  24. }
  25. }
  26. }

4. 对手写数字图像分类的实现代码


   
  1. //1行n列的向量
  2. int vecmaxIndex(Mat vec) //返回向量最大数的序号
  3. {
  4. int veclength = vec.cols;
  5. float maxnum = -1.0;
  6. int maxIndex = 0;
  7. float *p = vec.ptr< float>( 0);
  8. for( int i= 0; i < veclength; i++)
  9. {
  10. if(maxnum < p[i])
  11. {
  12. maxnum = p[i];
  13. maxIndex = i;
  14. }
  15. }
  16. return maxIndex;
  17. }
  18. //测试函数
  19. float cnntest(CNN cnn, vector<Mat> inputData, vector<Mat> outputData)
  20. {
  21. int incorrectnum = 0; //错误预测的数目
  22. for ( int i = 0; i < inputData.size(); i++) //inputData.size()为测试图像的总数
  23. {
  24. cnnff(cnn, inputData[i]); //前向传播
  25.      //检查神经网络输出的最大概率的序号是否等于标签中1值的序号,如果等于则表示分类成功
  26. if (vecmaxIndex(cnn.O5.y) != vecmaxIndex(outputData[i]))
  27. {
  28. incorrectnum++;
  29. printf( "i = %d, 识别失败\n", i);
  30. }
  31. else
  32. {
  33. printf( "i = %d, 识别成功\n", i);
  34. }
  35. cnnclear(cnn);
  36. }
  37. printf( "incorrectnum=%d\n", incorrectnum);
  38. printf( "inputData.size()=%d\n", inputData.size());
  39. return ( float)incorrectnum / ( float)inputData.size();
  40. }

5. 总体测试的实现代码

以下函数就是5层网络的测试代码,在mian函数中调用。


   
  1. void minst_cnn_test(void)
  2. {
  3. vector<Mat> traindata_list;
  4. vector<Mat> traindata_label;
  5. vector<Mat> testdata_list;
  6. vector<Mat> testdata_label;
  7. //读取训练数据标签
  8. traindata_label = read_Lable_to_Mat( "Minst/train-labels.idx1-ubyte");
  9. //读取训练数据
  10. traindata_list = read_Img_to_Mat( "Minst/train-images.idx3-ubyte");
  11. //读取测试数据标签
  12. testdata_label = read_Lable_to_Mat( "Minst/t10k-labels.idx1-ubyte");
  13.    //读取测试数据
  14. testdata_list = read_Img_to_Mat( "Minst/t10k-images.idx3-ubyte");
  15. int train_num = traindata_list.size();
  16. int test_num = testdata_list.size();
  17. int outSize = testdata_label[ 0].cols;
  18. int row = traindata_list[ 0].rows;
  19. int col = traindata_list[ 0].cols;
  20. CNNOpts opts;
  21. opts.numepochs = 1;
  22.   opts.alpha =  0.03; //学习率初始值
  23. int trainNum = 60000;
  24. CNN cnn;
  25. cnnsetup(cnn, row, col, outSize); //cnn初始化
  26. cnntrain(cnn, traindata_list, traindata_label, opts, train_num); //训练
  27. float success = cnntest(cnn, testdata_list, testdata_label); //分类
  28. printf( "success=%f\n", 1 - success); //打印分类的成功率
  29. }

运行以上函数对5层网络进行手写数字图像的训练和分类测试,得到的结果如下,对10000张图像进行分类,分类失败170张,准确率达到98.3%,还是相当高的。

本系列的基于VS2015与Opencv3.4.1的完整代码工程,读者可在以下网址下载:

https://download.csdn.net/download/shandianfengfan/16392246

好了,本系列的文章就更新到这里啦,有人可能会说我重复造轮子没有意义,我倒不这么认为,因为这是一个学习的过程,自己去实现一遍会加深自己的理解。在深度理解之后,再去使用别人现成的深度学习框架,也会顺手得多。接下来的文章我们就不自己实现网络了,而是使用别人现成的深度学习框架,我们把主要精力放在网络的构建与训练模型的构建上面。

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


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