无框架 使用c++从零开始实现 卷积 全连接 softmax 入门深度学习 CPU版 梯度下降

博主分享了使用C++从零开始编写卷积神经网络(CNN)的过程,涵盖了卷积层、池化层、全连接层和softmax层的实现。通过学习线性代数、微积分和概率论,博主逐步构建了CNN模型,并在MNIST数据集上取得了99.33%的准确率。此外,还讨论了梯度下降和参数更新,以及如何处理数据集转换。
摘要由CSDN通过智能技术生成

经过一段时间的努力,用C++把卷积写完了。因为深度学习太火了,所以 忍不住就试试。先是找资料看看需要什么,然后进行学习,恶补线性代数、微积分、概率论,感觉能用到的知识点学差不多就开始代码【真要等到全学会再动手写代码,以我的智商估计就没有然后了】。现在成熟框架很多,为了更好的理解原理选择自己造轮子,能力有限,所以这个过程很漫长也很痛苦!

太多的技术细节网上都有资料,我也不用多说,因为没有大神讲的透彻,理论是一回事,实际做又是另一回事。理论上的事儿我只说两点,第一个是多层卷积卷积核的数量,第二个是梯度下降和调参的关系,因为我在学习和编码的过程中出现了错误的想法,所以说一下。剩下的看代码。

一. 卷积核

先说一下多层卷积卷积核,程序用的都是数组,所以卷积的数据存储都是4维,分别用block、depth、row、column,可以理解为盒子里面插卡片,block 表示盒子个数,depth表示盒子里卡片张数、row 和column 表示卡片大小,看下面的示意图,这只是个简单的例子,注意红框里面的卷积核,是3个block,每个block里面有2个depth,应该是2*3个卷积核,我在最开始理解的时候理解为每个block里面有1个depth,所以正确率一直上不去。
如果说有2个input 希望输出10个map,那卷积核应该有多少呢?
2x10 = 20

二. 梯度下降

再说一下梯度下降,可能数学好的同志们这都不叫事儿,但是不好的就另当别论了。
假如我们的目标函数:(X-1)^2+1,为了是目标函数的导数最小,更新X的值 ,开始我的想法是X越小越接近答案,现在想起来都觉得郁闷,居然有这种想法。实际情况是随着X变化找到最小的斜率,其实就是X=1,当学习速率是0.6的时候用计算机计算效果如下:

在这里插入图片描述
在这里插入图片描述
当我们设定学习速率是0.4的时候,应该是下面的效果:
在这里插入图片描述
在这里插入图片描述
目标就是X=1,程序很简单,就几行代码,有兴趣的可以自己试试,体会一下调参的感觉,网络再复杂也就这个意思吧!

long double  x = 20;
	long double a = 0.4;
	for (int i = 0; i < 10000;i++){
   
		cout << "X" << i + 1 << "=" << "X" << i << "-" << "a* df(" << "X" << i <<")"<< endl;
		cout <<"  ="<< x << "-" << a << "*" << "(2*" << x << "-2" << ")" << endl;
		cout << "  =" << x - a*(2 * x - 2) << endl;
		x = x - a*(2 * x - 2);
	}

三. 测试效果

先是拿MNIST数据集测试程序效果,都说这个数据集被玩坏了,我感觉这个挺好。
我用了三层卷积,每个卷积后面加POOL,POOL用的是平均值,没用最大值,有兴趣可以自己改一下就可以,三层全连接还有结尾的softmax,正确率99.33%,然后我又自己随意写了一些进行测试。代码可以调整,卷积和全连接可以增加或者减少,也可以只用全连接,测试各种搭配的效果。
在这里插入图片描述

这个样子的测试正确率最高94%,这是在PS上面用柔性笔写的,还有一些模仿MNIST数据集,在模仿的像一点儿准确率会更高。

下面这个样子的数据测试正确率70%,这个跟MNIST的样式差距已经很大了,能到70%的正确率还算不错,MNIST这个是黑底白字,我写的是白底黑子,所以要对数据进行处理,代码里面只要修改一下ARRAY.cpp文件中的一个值就能完成转换。
在这里插入图片描述
这是训练时更新W B 参数效果,速度还可以,正确率到80%左右的时候会变慢,90%之后会更慢,这也是符合规律的,数据太长了,我只复制了一部分。这里不得不说一下,最开始的时候程序到处都是错误,运行好久正确率没任何变化,后来随着优化和修改,正确率提升的越来越快,那种感觉就一个字“爽”。
在这里插入图片描述
下图是softmax层loss函数曲线,整体还算可以,但是那几凸起看着还是挺扎心的,我在全连接的输入层和隐藏层都加入了Dropout处理,Dropout丢弃概率是50%,所以曲线一直跳动,曲线很难看但是识别效果确实不错。
下面是画曲线的代码,用的python,就这几行很简单

#!/usr/bin/env python
# -*- coding: GBK -*-
from numpy import *
import numpy as np
from matplotlib import pyplot as plt
yw=loadtxt('Arr_delta_layer_output.txt',dtype='float')
xw = np.arange(1,len(yw)+1)
plt.title("")
plt.xlabel("count")
plt.ylabel("value")
plt.plot(xw,yw)
plt.show()

在这里插入图片描述
汉字识别
代码也可以进行汉字的识别,但是不能识别太多类别,因为程序是基于CPU运行的,没有做加速GPU处理,所以对好几千分类来说太困难了,速度是不能接受的,如果也做个10分类,20分类都还能接受,准确率不低于数字识别,随着分类得增加,训练速度越来越慢,到50分类的运行速度就已经不行了,这个实验过程也不错,你会体会到特征的数量直接影响分类的数量和准确率。10分类的情况下,三层卷积结束,数据传入全连接输入层有几千个特征,准确率肯定能很高,如果到全连接有几千的特征值,非要做3755个汉字分类那肯定是不行的。有空再造个轮子,写个GPU的版本,主要是学习,如果使用GPU速度会有极大的提升,那时候就有底气做几千分类的训练了。
下图是我做50分类的训练曲线截图,是不是很崩溃
在这里插入图片描述
在这里插入图片描述

四. 代码
如果已经看了很多资料,依然有很多不明白的地方,看下面的代码应该会有很大的帮助,技术文档对照代码,我感觉对程序员来说是再好不过的快速学习方法了。当然实践最重要,但是要踩很多坑。

开发环境:win10 、vs2010、 opencv2.4.8
Opencv配置可以看这个,写的很棒文章链接: http://blog.csdn.net/poem_qianmo/article/details/19809337
使用opencv为的是在开始做数据归一化的时候好处理,因为数据集的加载、图片大小的改变都很方便。
先看一下代码总体结构
在这里插入图片描述

废话少说上代码
1、 ARRAY.h
将数组的操作进行封装方便调用,训练的时候可以在控制台输出,也可以输出到txt文件,看效果也方便,cpp文件太长了,后面会随工程打包,方便下载。

#ifndef ARRAY_H
#define ARRAY_H
#include <iostream>
#include <vector>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/opencv.hpp>
#include<opencv2/highgui/highgui.hpp>
#include<opencv2/imgproc/imgproc.hpp>
using namespace cv;
/**
  @njm
  说明:该对象包括了一维、二维、三维、四维 矩阵的初始化、销毁、输出等功能,初始化的时候传入相应维度的数组指针,然后
        就能使用该对象将传入的数组指针带到其他想用使用或者操作的地方
        
      1、初始化:可以根据选择构造函数的不同定义不同维度的数组,【“A”初始值0,"B"初始值位1,否则为随机数】
	  2、销毁:可以使用声明的arry对象直接调用相应的方法对初始化的矩阵进行销毁,释放内存
	  3、输出:可以使用声明的arry对象直接调用相应方法输出当前的数组,查看数组数据

 */
class Array{
   

public:
	Array();
	/*一维构造函数
	 */
	Array( int COL_NUM, long double *Arr1D,char init);
	/*二维构造函数
	*/
	Array( int ROW_NUM, int COL_NUM, long double **Arr2D, char init);
	/*三维构造函数
	*/
	Array(int Depth, int ROW_NUM, int COL_NUM, long double ***Arr3D, char init);
	//Array(int Depth, int ROW_NUM, int COL_NUM,  char init);
	Array(Mat *mat,int Depth, int ROW_NUM, int COL_NUMC, long double ***Arr3D, char init);
	/*四维构造函数
	*/
	Array(int block,int Depth, int ROW_NUM, int COL_NUM, long double ****Arr4D, char init);
	Array(Mat *mat, int block, int Depth, int ROW_NUM, int COL_NUMC, long double ****Arr4D, char init);
	~Array();
	//Mat转数组的函数
	void MatToArray(Mat *mat,int Kernel_C, long double ***Arr3D);
	void MatToArray4D(Mat *mat, int block, int Depth, long double ****Arr4D);

	//值为【-1,1】的一维数组
	void getArray1D(int COL_NUM, long double *Arr1D);
	//值为【0】的一维数组
	void getArray1D0(int COL_NUM, long double *Arr1D);
	void getArray1D1(int COL_NUM, long double *Arr1D);

	//值为【-1,1】的二维数组
	void getArray2D(int ROW_NUM, int COL_NUM, long double **Arr2D);
	//值为【0】的二维数组
	void getArray2D0(int ROW_NUM, int COL_NUM, long double **Arr2D);
	//值为【1】的二维数组
	void getArray2D1(int ROW_NUM, int COL_NUM, long double **Arr2D);
	//值为【-1,1】的三维数组
	void getArray3D(int Kernel_C,int ROW_NUM,int COL_NUM,long double ***Arr3D);
	//3D数组 值都为0
	void getArray3D0(int Kernel_C,int ROW_NUM,int COL_NUM,long double ***Arr3D);
		//3D数组 值都为1
	void getArray3D1(int Kernel_C,int ROW_NUM,int COL_NUM,long double ***Arr3D);
	//平均值池化 数值为0.25
	void getArray3D25(int Kernel_C,int ROW_NUM,int COL_NUM,long double ***Arr3D);
	void Delete1D(long double *arr1D, Array* OBJ);
	void Delete2D(int x, long double **arr2D, Array* OBJ);
	void Delete3D(int x, int y, long double ***arr3D,Array* OBJ);
	void Delete3D_1(long double ***arr3D);
	void Delete4D(int b, int x, int y, long double ****arr4D, Array* OBJ);

	//输出 返回值为void是直接输出到控制台,返回值为string类型的是将文件写到本地的txt文件
	/*一维输出函数*/ 
	void OutArr1D();
	string OutArr1DTXT(int index);
	string OutArr1DTXT();
	/*二维输出函数*/
	void OutArr2D();
	string OutArr2DTXT();
	/*二维输出函数*/
	void OutArr3D();
	string OutArr3DTXT();
	/*四维输出函数*/
	void OutArr4D();
	void Array::OutArr4D(long double****Mat4D);
	string OutArr4DTXT();
public:
	//属性
	// 记录下当前对象声明的时候构造函数设定的当前矩阵的维度,调用arry对象的时候可以直接进行传递
	int block = 0;
	int depth = 0;
	int row = 0;
	int column = 0;
	int flag = 0;
	//声明ARRAY对象时 用来存储需要的维度矩阵,这样可以再调用arry对象的时候直接操作该矩阵。
	//这个地方的注释我没有删除掉,是因为我开始的时候声明数组居然声明了数组的大小,导致训练过程中内存占用越来越大,导致程序崩溃,以此为戒。
	long double *Mat1D;// = new long double[1];
	long double **Mat2D;// = new long double*[1];
	long double ***Mat3D;// = new long double**[1];
	long double ****Mat4D;// = new long double***[1];

};
#endif ARRAY_H

2、 Conv.h
文件中有同名函数有带下划线和不带下划线的,带下划线的是正确的,不带下划线的是错误的,为自己当时产生如此愚蠢的想法做个纪念,没有删除,有兴趣的可以看看,如果是你来写,会不会有那样错误的想法。

#ifndef CONV_H
#define CONV_H

class Array;
class Conv
{
   
public:
	Conv(); 
	Conv(int _depth, int _row, int _column);
	~Conv();

	//深度
	int depth;
	//行
	int row;
	//列
	int column;

	double jz[10];
	double *dt;// = new double[1];
	long double *Matrix1D;
	long double **Matrix2D;
	long double ***Matrix3D;

	/*
	函数说明:卷积向前传播函数
	参数说明:
	ARR_inMat:卷积输入矩阵
	ARR_kernel:卷积核矩阵
	ARR_outMat:卷积运算后的Feature Map
	ARR_bias:偏置值
	stride:卷积步幅
	ZeroPadd:矩阵外层补零层数。
	0代表不做处理,1代表外围补一圈0,2代表补两圈
	【一般是再最开始对原始图像处理的时候添加,以便更好的获取边缘特征,提高识别率】
	*/
	int forword_(Array *ARR_inMat, Array *ARR_kernel, Array *ARR_outMat, Array *ARR_bias, int stride, int ZeroPadd, char Activation);

	/*
	  函数说明:卷积向前传播函数
	  参数说明:
	      ARR_inMat:卷积输入矩阵
		  ARR_filter:卷积核矩阵
		  ARR_outMat:卷积运算后的Feature Map
		  ARR_bias:偏置值
		  stride:卷积步幅
		  ZeroPadd:矩阵外层补零层数。
		            0代表不做处理,1代表外围补一圈0,2代表补两圈
					【一般是再最开始对原始图像处理的时候添加,以便更好的获取边缘特征,提高识别率】
	*/
	int forword(Array *ARR_inMat, Array *ARR_filter, Array *ARR_outMat, Array *ARR_bias, int stride, int ZeroPadd);

	/* 函数说明:卷积反向梯度计算,更新ARR_filter和 ARR_bias,
	   参数说明:
	       向前计算输出: *ARR_outMat_forword_Out
		   样本标签:    *lable
		   过滤层:  *ARR_filter
		   修正值:  *ARR_bias
		   误差值δ:  *ARR_datle
		   步幅  :  stride
		   速率μ : rate
	*/
	int backwordUpdateGradient(Array *ARR_inMat, Array *ARR_outMat_forword_Out, Array *ARR_filter, Array *ARR_bias, Array *ARR_delta_feature_map, int stride, double rate);


	/* 函数说明:卷积反向梯度计算,更新ARR_filter和 ARR_bias,
	参数说明:
	向前计算输出: *ARR_outMat_forword_Out
	样本标签:    *lable
	过滤层:  *ARR_filter
	修正值:  *ARR_bias
	误差值δ:  *ARR_datle
	步幅  :  stride
	速率μ : rate
	*/
	int backwordUpdateGradient_(Array *ARR_inMat, Array *ARR_outMat_forword_Out, Array *ARR_kernel, Array *ARR_bias, Array *ARR_delta_feature_map, int stride, double rate);
	
	/* 函数说明:反向网络误差传播
	             因为二层以后的卷积涉及到卷积层从FeatureMap反向传播误差的操作,
				 第一层卷积不涉及到这步操作,为了使单个函数看起来更简洁,逻辑更简单,特意添加这个函数
	参数说明:
	向前计算输出: *ARR_outMat_forword_Out
	样本标签:    *lable
	过滤层:  *ARR_kernel
	修正值:  *ARR_bias
	误差值δ:  *ARR_datle
	pool反向δ:ARR_delta_feature_map 从pool层传递到feature_map的网络误差
	步幅  :  stride
	速率μ : rate
	*/
	int backwordSpreadGradient(Array *ARR_inMat, Array *ARR_outMat_forword_Out, Array *ARR_filter, Array *ARR_bias, Array *ARR_datle, Array *ARR_delta_feature_map, int stride, double rate);
	

	/* 函数说明:反向网络误差传播 误差从 fuetureMay 传递到 卷积输入层 input
	             函数是为多层卷积设计的,一层卷积时用不到这个函数,因为一层卷积
				 不涉及到featuremap层向input层传播误差δ
	参数说明:
	卷积输入矩阵:ARR_inMat  (方法中暂时没用到)
	向前计算输出: *ARR_outMat_forword_Out
	样本标签:    *lable
	过滤层:  *ARR_kernel
	修正值:  *ARR_bias
	卷积输入矩阵误差值δ:  *ARR_datle
	pool反向传回δ:ARR_delta_feature_map (从pool层传递到feature_map的网络误差)
	步幅  :  stride
	速率μ : rate
	激活函数:char Activation
	*/
	int backwordSpreadGradient_(Array *ARR_inMat, Array *ARR_outMat_forword_Out, Array *ARR_filter, Array *ARR_bias, Array *ARR_datle, Array *ARR_delta_feature_map, int stride, double rate, char Activation);

	/* 函数说明:单层卷积测试中的反向计算参数说明
	   参数说明:
			向前计算输出: *ARR_outMat_forword_Out
			样本标签:    *lable
			过滤层:  *ARR_filter
			修正值:  *ARR_bias
			误差值δ:  *ARR_datle
			步幅  :  stride
			速率μ : rate
	*/
	int backword_CS(Array *ARR_inMat, Array *ARR_outMat_forword_Out, Array *ARR_lable, Array *ARR_filter, Array *ARR_bias, Array *ARR_datle, int stride, double rate);
	
};

#endif CONV_H

3、Conv.cpp 向前卷积
这个图做的太好了,引用一下,如有冒犯我会删除
在这里插入图片描述

可以随便在网上找一个讲的差不多的卷积例子,然后对照代码看一下就能明白,后面的POOL 层、全连接层、softmax层 都能找到解释,然后根据自己的理解和代码进行比对,再跑代码试试,很快就能理解!

#include "stdafx.h"
#include "Conv.h"
#include <iostream>
#include "ARRAY.h"
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值