网站首页 > 技术文章 正文
首先列出本系列博文的链接:
卷积神经网络原理及其C++/Opencv实现(4)—误反向传播法
卷积神经网络原理及其C++/Opencv实现(5)—参数更新
在以上文章中,我们基本把5层网络的原理、公式推导讲过了,从本文开始,我们来讲一下基于C++和Opencv的5层卷积神经网络实现吧~
1. 结构体定义
(1) 卷积层的结构体
typedef struct convolutional_layer
{
int inputWidth; //输入图像的宽
int inputHeight; //输入图像的长
int mapSize; //卷积核的尺寸
int inChannels; //输入图像的数目
int outChannels; //输出图像的数目
vector<vector<Mat>> mapData; //四维float数组,卷积核本身是二维数据,m*n哥卷积核就是四维数组
Mat basicData; //偏置,个数为outChannels, 一维float数组
bool isFullConnect; //是否为全连接
vector<Mat> v; //进入激活函数的输入值,三维数组float型
vector<Mat> y; //激活函数后神经元的输出,三维数组float型
vector<Mat> d; // 网络的局部梯度,三维数组float型
}CovLayer;
(2) 池化层的结构体
typedef struct pooling_layer
{
int inputWidth; //输入图像的宽
int inputHeight; //输入图像的长
int mapSize; //卷积核的大小
int inChannels; //输入图像的数目
int outChannels; //输出图像的数目
int poolType; //池化的方法
Mat basicData; //偏置, 一维float数组
vector<Mat> y; //采样函数后神经元的输出,无激活函数,三维数组float型
vector<Mat> d; //网络的局部梯度,三维数组float型
vector<Mat> max_position; // 最大值模式下最大值的位置,三维数组float型
}PoolLayer;
(3) 输出层的结构
typedef struct nn_layer
{
int inputNum; //输入数据的数目
int outputNum; //输出数据的数目
Mat wData; // 权重数据,为一个inputNum*outputNum大小
Mat basicData; //偏置,大小为outputNum大小
Mat v; // 进入激活函数的输入值
Mat y; // 激活函数后神经元的输出
Mat d; // 网络的局部梯度
bool isFullConnect; //是否为全连接
}OutLayer;
(4) 5层网络的结构体
typedef struct cnn_network
{
int layerNum;
CovLayer C1;
PoolLayer S2;
CovLayer C3;
PoolLayer S4;
OutLayer O5;
Mat e; // 训练误差
Mat L; // 瞬时误差能量
}CNN;
(5) 训练参数的结构体
typedef struct train_opts
{
int numepochs; // 训练的迭代次数
float alpha; // 学习率
}CNNOpts;
2. 5层网络的初始化
(1) 卷积层结构体初始化
CovLayer initCovLayer(int inputWidth, int inputHeight, int mapSize, int inChannels, int outChannels)
{
CovLayer covL;
covL.inputHeight = inputHeight;
covL.inputWidth = inputWidth;
covL.mapSize = mapSize;
covL.inChannels = inChannels;
covL.outChannels = outChannels;
covL.isFullConnect = true; // 默认为全连接
// 权重空间的初始化,先行再列调用,[r][c]
srand((unsigned)time(NULL)); //设置随机数种子
for(int i = 0; i < inChannels; i++) //输入通道数
{
vector<Mat> tmp;
for(int j = 0; j < outChannels; j++) //输出通道数
{
Mat tmpmat(mapSize, mapSize, CV_32FC1); //初始化一个mapSize*mapSize的二维矩阵
for(int r = 0; r < mapSize; r++) //卷积核的高
{
for(int c = 0; c < mapSize; c++) //卷积核的宽
{
//使用随机数初始化卷积核
float randnum=(((float)rand()/(float)RAND_MAX)-0.5)*2; //生成-1~1的随机数
tmpmat.ptr<float>(r)[c] = randnum*sqrt(6.0/(mapSize*mapSize*(inChannels+outChannels)));
}
}
tmp.push_back(tmpmat.clone());
}
covL.mapData.push_back(tmp);
}
covL.basicData = Mat::zeros(1, outChannels, CV_32FC1); //初始化卷积层偏置的内存
int outW = inputWidth - mapSize + 1; //valid模式下卷积层输出的宽
int outH = inputHeight - mapSize + 1; //valid模式下卷积层输出的高
Mat tmpmat2 = Mat::zeros(outH, outW, CV_32FC1);
for(int i = 0; i < outChannels; i++)
{
covL.d.push_back(tmpmat2.clone()); //初始化局部梯度
covL.v.push_back(tmpmat2.clone()); //初始化输入激活函数之前的值
covL.y.push_back(tmpmat2.clone()); //初始化输入激活函数之后的值
}
return covL; //返回初始化之后的卷积层结构体
}
(2) 池化层结构体初始化
PoolLayer initPoolLayer(int inputWidth, int inputHeight, int mapSize, int inChannels, int outChannels, int poolType)
{
PoolLayer poolL;
poolL.inputHeight=inputHeight; //输入高度
poolL.inputWidth=inputWidth; //输入宽度
poolL.mapSize=mapSize; //卷积核尺寸,池化层相当于做一个特殊的卷积操作
poolL.inChannels=inChannels; //输入通道
poolL.outChannels=outChannels; //输出通道
poolL.poolType=poolType; //最大值模式1/平均值模式0
poolL.basicData = Mat::zeros(1, outChannels, CV_32FC1); //池化层无偏置,无激活,这里只是预留偏置内存
int outW = inputWidth/mapSize; //池化层的卷积核为2*2
int outH = inputHeight/mapSize;
Mat tmpmat = Mat::zeros(outH, outW, CV_32FC1);
Mat tmpmat1 = Mat::zeros(outH, outW, CV_32SC1);
for(int i = 0; i < outChannels; i++)
{
poolL.d.push_back(tmpmat.clone()); //局域梯度
poolL.y.push_back(tmpmat.clone()); //采样函数后神经元输出,无激活函数
poolL.max_position.push_back(tmpmat1.clone()); //最大值模式下最大值在原矩阵中的位置
}
return poolL;
}
(3) 输出层结构体初始化
OutLayer initOutLayer(int inputNum, int outputNum)
{
OutLayer outL;
outL.inputNum = inputNum;
outL.outputNum = outputNum;
outL.isFullConnect = true;
outL.basicData = Mat::zeros(1, outputNum, CV_32FC1); //偏置,分配内存的同时初始化为0
outL.d = Mat::zeros(1, outputNum, CV_32FC1);
outL.v = Mat::zeros(1, outputNum, CV_32FC1);
outL.y = Mat::zeros(1, outputNum, CV_32FC1);
// 权重的初始化
outL.wData = Mat::zeros(outputNum, inputNum, CV_32FC1); // 输出行,输入列,权重为10*192矩阵
srand((unsigned)time(NULL));
for(int i = 0; i < outputNum; i++)
{
float *p = outL.wData.ptr<float>(i);
for(int j = 0; j < inputNum; j++)
{
//使用随机数初始化权重
float randnum = (((float)rand()/(float)RAND_MAX)-0.5)*2; // 产生一个-1到1的随机数,rand()的取值范围为0~RAND_MAX
p[j] = randnum*sqrt(6.0/(inputNum+outputNum));
}
}
return outL;
}
(4) 5层网络结构体初始化
void cnnsetup(CNN &cnn, int inputSize_r, int inputSize_c, int outputSize) //cnn初始化
{
cnn.layerNum = 5;
//C1层
int mapSize = 5;
int inSize_c = inputSize_c; //28
int inSize_r = inputSize_r; //28
int C1_outChannels = 6;
cnn.C1 = initCovLayer(inSize_c, inSize_r, mapSize, 1, C1_outChannels); //卷积层1
//S2层
inSize_c = inSize_c - cnn.C1.mapSize + 1; //24
inSize_r = inSize_r - cnn.C1.mapSize + 1; //24
mapSize = 2;
cnn.S2 = initPoolLayer(inSize_c, inSize_r, mapSize, cnn.C1.outChannels, cnn.C1.outChannels, MaxPool); //池化层
//C3层
inSize_c = inSize_c / cnn.S2.mapSize; //12
inSize_r = inSize_r / cnn.S2.mapSize; //12
mapSize = 5;
int C3_outChannes = 12;
cnn.C3 = initCovLayer(inSize_c, inSize_r, mapSize, cnn.S2.outChannels, C3_outChannes); //卷积层
//S4层
inSize_c = inSize_c - cnn.C3.mapSize + 1; //8
inSize_r = inSize_r - cnn.C3.mapSize + 1; //8
mapSize = 2;
cnn.S4 = initPoolLayer(inSize_c, inSize_r, mapSize, cnn.C3.outChannels, cnn.C3.outChannels, MaxPool); //池化层
//O5层
inSize_c = inSize_c / cnn.S4.mapSize; //4
inSize_r = inSize_r / cnn.S4.mapSize; //4
cnn.O5 = initOutLayer(inSize_c*inSize_r*cnn.S4.outChannels, outputSize); //输出层
cnn.e = Mat::zeros(1, cnn.O5.outputNum, CV_32FC1); //输出层的输出值与标签值之差
}
3. 二维图像的卷积实现
调用Opencv的filter2D函数,可以很方便、很快速地实现二维卷积运算。我们首先实现full模式,valid和same模式的卷积结果可以直接从full模式的结果中截取。
需要注意的是,在卷积神经网络中,我们说的卷积运算其实是互相关运算,也即开始卷积运算之前卷积核不需要做顺时针180°的旋转。
Mat correlation(Mat map, Mat inputData, int type)
{
const int map_row = map.rows;
const int map_col = map.cols;
const int map_row_2 = map.rows/2;
const int map_col_2 = map.cols/2;
const int in_row = inputData.rows;
const int in_col = inputData.cols;
//先按full模式扩充图像边缘
Mat exInputData;
copyMakeBorder(inputData, exInputData, map_row_2, map_row_2, map_col_2, map_col_2, BORDER_CONSTANT, 0);
Mat OutputData;
filter2D(exInputData, OutputData, exInputData.depth(), map);
if(type == full) //full模式
{
return OutputData;
}
else if(type == valid) //valid模式
{
int out_row = in_row - (map_row - 1);
int out_col = in_col - (map_col - 1);
Mat outtmp;
OutputData(Rect(2*map_col_2, 2*map_row_2, out_col, out_row)).copyTo(outtmp);
return outtmp;
}
else //same模式
{
Mat outtmp;
OutputData(Rect(map_col_2, map_row_2, in_col, in_row)).copyTo(outtmp);
return outtmp;
}
}
4. 池化层的实现
(1) 均值池化
void avgPooling(Mat input, Mat &output, int mapSize)
{
const int outputW = input.cols/mapSize; //输出宽=输入宽/核宽
const int outputH = input.rows/mapSize; //输出高=输入高/核高
float len = (float)(mapSize*mapSize);
int i,j,m,n;
for(i = 0;i < outputH; i++)
{
for(j = 0; j < outputW; j++)
{
float sum=0.0;
for(m = i*mapSize; m < i*mapSize+mapSize; m++) //取卷积核大小的窗口求和平均
{
for(n = j*mapSize; n < j*mapSize+mapSize; n++)
{
sum += input.ptr<float>(m)[n];
}
}
output.ptr<float>(i)[j] = sum/len;
}
}
}
(2) 最大值池化
void maxPooling(Mat input, Mat &max_position, Mat &output, int mapSize)
{
int outputW = input.cols / mapSize; //输出宽=输入宽/核宽
int outputH = input.rows / mapSize; //输出高=输入高/核高
int i, j, m, n;
for (i = 0; i < outputH; i++)
{
for (j = 0; j < outputW; j++)
{
float max = -999999.0;
int max_index = 0;
for (m = i*mapSize; m<i*mapSize + mapSize; m++) //取卷积核大小的窗口的最大值
{
for (n = j*mapSize; n<j*mapSize + mapSize; n++)
{
if (max < input.ptr<float>(m)[n]) //求池化窗口中的最大值,并记录最大值位置
{
max = input.ptr<float>(m)[n];
max_index = m*input.cols + n;
}
}
}
output.ptr<float>(i)[j] = max; //求得最大值作为池化输出
max_position.ptr<int>(i)[j] = max_index; //记录最大值在原矩阵中的位置,用于反向传播
}
}
}
5. 激活函数与向量点乘函数的实现
(1) Relu函数
float activation_Sigma(float input, float bas)
{
float temp = input + bas;
return (temp > 0 ? temp: 0);
}
(2) Softmax函数
void softmax(OutLayer &O)
{
float sum = 0.0;
float *p_y = O.y.ptr<float>(0);
float *p_v = O.v.ptr<float>(0);
float *p_b = O.basicData.ptr<float>(0);
for (int i = 0; i < O.outputNum; i++)
{
float Yi = exp(p_v[i]+ p_b[i]);
sum += Yi;
p_y[i] = Yi;
}
for (int i = 0; i < O.outputNum; i++)
{
p_y[i] = p_y[i]/sum;
}
}
(3) 两个一维向量的点乘函数
以下函数中,vec1和vec2是两个长度相同的一维向量,点乘的结果就是它们对应位置的值相乘,然后把所有乘积相加的结果。
float vecMulti(Mat vec1, float *vec2)// 两向量相乘
{
float *p1 = vec1.ptr<float>(0);
float m = 0;
for (int i = 0; i < vec1.cols; i++)
m = m + p1[i] * vec2[i];
return m;
}
6. 5层网络前向传播的实现
(1) 卷积层前向传播
//输入的inputData有可能是一张图像,也有可能是多张图像,如果是多张图像,则把它们的卷积结果累加起来
void cov_layer_ff(vector<Mat> inputData, int cov_type, CovLayer &C)
{
for (int i = 0; i < (C.outChannels); i++)
{
for (int j = 0; j < (C.inChannels); j++)
{
//计算卷积,mapData为四维矩阵
Mat mapout = correlation(C.mapData[j][i], inputData[j], cov_type);
C.v[i] += mapout; //所有输入通道的卷积结果累加
}
int output_r = C.y[i].rows;
int output_c = C.y[i].cols;
for (int r = 0; r < output_r; r++)
{
for (int c = 0; c < output_c; c++)
{
C.y[i].ptr<float>(r)[c] = activation_Sigma(C.v[i].ptr<float>(r)[c], C.basicData.ptr<float>(0)[i]); //先加上偏置,再输入激活函数
}
}
}
}
(2) 池化层前向传播
#define AvePool 0
#define MaxPool 1
void pool_layer_ff(vector<Mat> inputData, int pool_type, PoolLayer &S)
{
if (pool_type == AvePool) //均值池化
{
for (int i = 0; i < S.outChannels; i++)
{
avgPooling(inputData[i], S.y[i], S.mapSize);
}
}
else if(pool_type == MaxPool) //最大值池化
{
for (int i = 0; i < S.outChannels; i++)
{
maxPooling(inputData[i], S.max_position[i], S.y[i], S.mapSize);
}
}
else
{
printf("pool type erroe!\n");
}
}
(3) 输出层前向传播
void nnff(Mat input, Mat wdata, Mat &output)
{
for (int i = 0; i < output.cols; i++) //分别计算多个向量相乘的乘积
output.ptr<float>(0)[i] = vecMulti(input, wdata.ptr<float>(i)); //由于输入激活函数之前就有加上偏置的操作,所以此处不再加偏置
}
void out_layer_ff(vector<Mat> inputData, OutLayer &O)
{
Mat OinData(1, O.inputNum, CV_32FC1); //输入192通道
float *OinData_p = OinData.ptr<float>(0);
int outsize_r = inputData[0].rows;
int outsize_c = inputData[0].cols;
int last_output_len = inputData.size();
for (int i = 0; i < last_output_len; i++) //上一层S4输出12通道的4*4矩阵
{
for (int r = 0; r < outsize_r; r++)
{
for (int c = 0; c < outsize_c; c++)
{
//将12通道4*4矩阵展开成长度为192的一维向量
OinData_p[i*outsize_r*outsize_c + r*outsize_c + c] = inputData[i].ptr<float>(r)[c];
}
}
}
//192*10个权重
nnff(OinData, O.wData, O.v); //10通道输出,1个通道的输出等于192个输入分别与192个权重相乘的和:∑in[i]*w[i], 0≤i<192
//Affine层的输出经过Softmax函数,转换成0~1的输出结果
softmax(O);
}
(4) 5层网络前向传播
void cnnff(CNN &cnn, Mat inputData)
{
//C1
//5*5卷积核
//输入28*28矩阵
//输出(28-25+1)*(28-25+1) = 24*24矩阵
vector<Mat> input_tmp;
input_tmp.push_back(inputData);
cov_layer_ff(input_tmp, valid, cnn.C1);
//S2
//24*24-->12*12
pool_layer_ff(cnn.C1.y, MaxPool, cnn.S2);
//C3
//12*12-->8*8
cov_layer_ff(cnn.S2.y, valid, cnn.C3);
//S4
//8*8-->4*4
pool_layer_ff(cnn.C3.y, MaxPool, cnn.S4);
//O5
//12*4*4-->192-->1*10
out_layer_ff(cnn.S4.y, cnn.O5);
}
好了,本文就讲到这里,接下来的文章我们来讲反向传播的实现和参数更新的实现,敬请期待~
欢迎扫码关注以下微信公众号,接下来会不定时更新更加精彩的内容噢~
猜你喜欢
- 2024-10-28 使用卷积神经网络构建图像分类模型检测肺炎
- 2024-10-28 机器不学习:卷积神经网络CNN与Keras实战
- 2024-10-28 我是如何用5个概念理解的卷积神经网络?(Hadoop大数据)
- 2024-10-28 深度学习笔记:图像识别和卷积网络
- 2024-10-28 Tensorflow Conv2D和MaxPool2D原理
- 2024-10-28 PyTorch中傅立叶卷积:计算大核卷积的数学原理和代码实现
- 2024-10-28 一文看完卷积神经网络及实现机制 卷积神经网络的原理与实现
- 2024-10-28 「周末AI课堂」卷积之上的新操作|机器学习你会遇到的“坑”
- 2024-10-28 谈谈CNN中的位置和尺度问题 cnn例题
- 2024-10-28 使用Keras进行深度学习:(一)Keras 入门
你 发表评论:
欢迎- 最近发表
- 标签列表
-
- oraclesql优化 (66)
- 类的加载机制 (75)
- feignclient (62)
- 一致性hash算法 (71)
- dockfile (66)
- 锁机制 (57)
- javaresponse (60)
- 查看hive版本 (59)
- phpworkerman (57)
- spark算子 (58)
- vue双向绑定的原理 (68)
- springbootget请求 (58)
- docker网络三种模式 (67)
- spring控制反转 (71)
- data:image/jpeg (69)
- base64 (69)
- java分页 (64)
- kibanadocker (60)
- qabstracttablemodel (62)
- java生成pdf文件 (69)
- deletelater (62)
- com.aspose.words (58)
- android.mk (62)
- qopengl (73)
- epoch_millis (61)
本文暂时没有评论,来添加一个吧(●'◡'●)