CNN编程实践: C++实现LeNet-5全记录

后期需要用FPGA实现神经网络的硬件加速,开发语言准备选择C++,由于是初学者,需找一不是很复杂的网络进行学习,故尝试对CNN经典网络LeNet-5进行分析和C++语言实现。

一、论文解析

LeNet-5 由LeCun于1998年提出,论文全篇共43页,非常详细的对CNN和LeNet进行了说明,本文仅对部分内容进行分析

1 LeNet-5 网络结构
在这里插入图片描述

上图是LeNet-5的网络结构,计输入层和输出层共计为8层:

  1. input layer --> 输入层(图中INPUT)
  2. convulational layer -->卷积层(图中C1)
  3. pooling layer --> activation function –池化层(图中S2)
  4. convulational layer -->卷积层(图中C3)
  5. pooling layer --> activation function --> 池化层(图中S4)
  6. convulational layer --> 卷积层(图中C5)
  7. fully connect layer --> 全连接层(图中F6)
  8. output layer–>输出层(图中output)

二、LeNet-5 网络与代码

1 前期准备:
(1)思路

因LeNet采用的是5 x 5的卷积核,故先定义卷积计算的函数以方便调用
在这里插入图片描述

(2)C++代码

函数: Conv_5x5()
功能:实现步长为1 的卷积中的一次滑动计算
参数1: 输入input,在此拉成1维的数组 input[25]
参数2: 卷积核参数,在此拉成1维的数组 kernel[25]
返回值: 一次卷积中一次滑动的计算结果,在此定义为 result

float Conv_5x5(float input[25], float kernel[25]){
	int x,y;
	float result = 0;
	for(y = 0; y < 5; y++){
		for(x = 0; x < 5; x++){
			result += input[x+y*5] * kernel[x+y*5];
		}
	}
	return result;
}

2 input层

输入为 32*32 pixel的图像

3 卷积层C1层
(1)思路

C1层为卷积层,kernel size = 5 * 5,步长为1,无填充,生成6个feature map(即depth = 6),最终得到的feature map为28 * 28

该层参数量:(5 * 5+1) * 6 = 156
其中5 * 5对应kernel size,1对应为bias,6是feature map的个数

连接数 156 * 28 * 28 = 122304。
其中156为参数个数,feature map上每个像素点对应5 * 5+1 = 26个连接,
算上将深度为6的6个像素点作为一个的话,这一个对应的连接数为(5*5+1 )*6 = 156个

INPUTC1,即完成32 x 32的输入图像 与 5 x 5 的卷积核之间的一次完整计算,此过程涉及卷积计算的整个滑动过程,示意图如下图所示(注:仅为示意图,尺寸与本文不同,第3次滑动没显示全)
在这里插入图片描述

(2)C++代码

函数: ConvLayer_1()
功能:实现一次步长为1 的完整卷积计算
参数1: 输入input,原始输入图像为32 x 32 ,在此拉成1维的数组 input[1024]
参数2: 卷积核参数,在此拉成1维的数组 kernel[25]
返回值: 一次卷积中一次滑动的计算结果,在此定义维 result

程序流程图
在这里插入图片描述
(2)C++代码

void ConvLayer_1(float input[1024],float * C1_value,float * weights){
	int i_y,i_x,matrix_y,matrix_x;
	int k_num,mat_i = 0;
	top_loop:for(int k_num = 0; k_num < 6; k_num+=1){
		//TODO 内存kernel
		float matrix_2[25];
		for(mat_i = 0;mat_i<25;mat_i++){
			matrix_2[mat_i] = weights[mat_i + k_num*25];
		}
		i_y_loop:for(i_y = 0; i_y < 28; i_y++){
			for(i_x = 0; i_x < 28; i_x++){
				float matrix[25];
				int pic_value_index = i_x + i_y * 32;
				matrix_loop:for(matrix_y = 0; matrix_y <5; matrix_y++){
					caculate:for(matrix_x = 0; matrix_x <5; matrix_x++){
//						图片索引  0 ~ 24
						int matrix_index = matrix_x + matrix_y * 5;
//						图片像素索引 0 ~ 1024,与matrix_x,matrix_y相关,x、y=32
						int input_value_index = pic_value_index + matrix_x + matrix_y * 32;
						matrix[matrix_index] = input[input_value_index];
					}
				}
				int out_pic_index = i_x + i_y * 28 + k_num * 784;
				C1_value[out_pic_index] = Conv_5x5(matrix,matrix_2);
			}
		}
	}
}

4 池化层S2层
(1)思路

  • S2层为降采样层,kernel size为2 x 2,步长为2,无填充
  • S2层相当于降采样层+激活层。先是降采样,然后激活函数sigmoid非线性输出
  • 新生成的feature map大小为(( 28 - 2) / 2 + 1 ) * (( 28 - 2) / 2 + 1 )= 14 x 14 = 196
  • LeNet-5中,先对2 x 2的视野进行平均,然后进入激活函数,即sigmoid ( a * sigmoid(x) + b),故参数的个数为 6 * (1 + 1) = 12
  • 连接个数为6 * 14 * 14 * (2 * 2 + 1) = 5880

定义平均池化函数

//函数:AvgPool_2x2()
//功能:对传入的4个数据取平均值,即平均池化
float AvgPool_2x2(float input[4]){
	float res = 0;
	int i;
	for(i = 0; i < 4 ; i++){
		res += input[i];
	}
	res /= 4;
	return res;
}

定义sigmoid激活函数
在这里插入图片描述

float sigmoid(float x)
{
    return (1 / (1 + expf(-x)));
}

池化层S2
在这里插入图片描述
(2)C++代码

void AvgpoolLayer_2(float input[4704],float *A2_value){
	int k_num,i_y,i_x,matrix_x,matrix_y;
	int count = 0;
	for(k_num = 0; k_num < 6; k_num++){
		for(i_y = 0; i_y < 27; i_y+=2){
			for(i_x = 0;  i_x < 27; i_x+=2){
				float matrix[4];
				int index_now = i_x + i_y * 28 + k_num * 784;
				for(matrix_y = 0; matrix_y < 2; matrix_y++){
					for(matrix_x = 0; matrix_x < 2; matrix_x++){
						int input_index = index_now + matrix_x + matrix_y * 28 ;
						matrix[matrix_x + matrix_y*2] = input[input_index];
					}
				}
				A2_value[count] = sigmoid(AvgPool_2x2(matrix));
				count++;
			}
		}
	}
}

5 卷积层C3
(1)思路

  • C3层为卷积层,kernel size为5 * 5,步长为1,生成16个feature map
    注: C3的feature map并不是由全部S1的feature map生成的
  • 并没有全部连接的原因:
    a. 控制参数个数
    b.打破对称性,期望学到互补的特征
  • 新得到的feature map大小为14 - 5 + 1 = 10,即10 * 10
  • 参数个数 (5 * 5 * 3 + 1) * 6 + (5 * 5 * 4 +1) * 9 + ( 5 * 5 * 6+1) = 1516个
    kernel_size * kernel_size * feature_map_num + bias_num,表示从feature_map_size卷积得到的feature map所需要的参数个数;括号外为相应得到feature map的数目。
  • 连接的个数是1516 * 10 * 10 = 151,600,10 * 10为新feature map的大小,feature map上的每个点(此处的点对应的16个像素点)都对应着1516个连接。
    卷积层C3
    在这里插入图片描述
    (2)C++代码
//kernel 5x5x6x16 = 25x6x16 =2400
void ConvLayer_3(float input[1176],float *C3_value,float * weights){
	int k_num,nk_num,i_y,i_x,matrix_x,matrix_y;
	int mat_i;
    for(nk_num = 0; nk_num < 16; nk_num++){
		for(i_y = 0; i_y < 10; i_y++){
			for(i_x = 0; i_x < 10; i_x++){
				float res = 0;
				float res_total_6 = 0;
				float matrix[25];
				int index_now = i_x + i_y * 10 + nk_num * 100;
				for(k_num = 0; k_num < 6; k_num++){
					float matrix_2[25];
					for(mat_i = 0;mat_i<25;mat_i++){
						int weights_index = mat_i + k_num*25 + (nk_num+1)*150;
						matrix_2[mat_i] = weights[weights_index];
					}
					for(matrix_y = 0; matrix_y <5; matrix_y++){
						for(matrix_x = 0; matrix_x <5; matrix_x++){
							int matrix_index = matrix_x + matrix_y * 5;
							int input_value_index = index_now + matrix_x + matrix_y * 14;
							matrix[matrix_index] = input[input_value_index];
						}
					}
					res_total_6 += Conv_5x5(matrix,matrix_2);
				}
				C3_value[index_now] = res_total_6;
			}
		}
	}
}

6 池化层S4
(1)思路

  • S4层为降采样层,kernel size为2 * 2,步长均为2,无填充
  • 新生成的feature map为5 * 5,depth为 16
  • 参数个数为16 * 2 = 32
  • 连接个数为5 * 5 * 16 * (2 * 2 + 1) = 2000,其中,2 * 2为feature map的数目,1为bias

在这里插入图片描述
(2)C++代码

void AvgpoolLayer_4(float input[1600],float *A4_value){
	int k_num,i_y,i_x,matrix_x,matrix_y;
	int count = 0;
	for(k_num = 0; k_num < 16; k_num++){
		for(i_y = 0; i_y < 10; i_y+=2){
			for(i_x = 0;  i_x < 10; i_x+=2){
				float matrix[4];
				int index_now = i_x + i_y * 10 + k_num * 100;
				for(matrix_y = 0; matrix_y < 2; matrix_y++){
					for(matrix_x = 0; matrix_x < 2; matrix_x++){
						int input_index = index_now + matrix_x + matrix_y * 10 ;
						matrix[matrix_x + matrix_y*2] = input[input_index];
					}
				}
				A4_value[count] = sigmoid(AvgPool_2x2(matrix));
				count++;
			}
		}
	}
}

7 卷积层C5
(1)思路

  • C5为卷积层,kernel size为5 * 5,步长为1,无填充,全连接生成120个feature map
  • C5层可以理解为两层,第一层是卷积层,经过5 * 5的卷积,从depth16,5 * 5的feature map变为depth为16,大小为1 * 1的feature map,即16个点;第二层就是全连接层,16个点与120个点全连接
  • 参数个数 120 * (5 * 5+1) = 48120,1为偏置bias
  • 连接个数等于48120 * 1 = 48120

(2)C++代码
在这里插入图片描述

//kernel 400x120 = 48000
void FullyConnLayer_5(float input[400],float *F5_value,float * weights){
	int i_y,i_x;
	for(i_y = 0; i_y < 120; i_y++){
		float res = 0;
		for(i_x = 0;  i_x < 400; i_x++){
			int index = i_x + i_y * 400;
			res += input[i_x] * weights[index + 2550];
		}
		F5_value[i_y] = res;
	}
}

8 全连接层F6
(1)思路

  • F6层为全连接层
  • 参数个数 = 连接个数 = (120 + 1) * 84 = 10164

(2)C++代码
在这里插入图片描述

//kernel 84x120 = 10080
void FullyConnLayer_6(float input[120],float *F6_value,float * weights){
	int i_y,i_x;
	for(i_y = 0; i_y < 84; i_y++){
		float res = 0;
		for(i_x = 0;  i_x < 120; i_x++){
			int index = i_x + i_y * 120;
			res += input[i_x] * weights[index + 50550];
		}
		F6_value[i_y] = res;
	}
}

8 输出层output
(1)思路

  • 从F6层到output层,使用的公式如下:
  • 连接数84 * 10 = 840
int Softmax_1_8(float input[10],float *probability,float *res){
	int index;
	float sum = 0;
	for(index = 0; index < 10; index++ ){
		probability[index] = expf(input[index]/1000);
		sum += probability[index];
	}
	int max_index = 0;
	for(index = 0; index < 10; index++ ){
			res[index] = probability[index]/sum;
			float res1 = res[index];
			float res2 = res[max_index];
			if(res1 > res2){
				max_index = index;
			}
	}
	return max_index;
}
LeNet-5神经网络 C源代码,这个写的比较好,可以用gcc编译去跑,结合理论可以对深度学习有更深刻的了解 介绍 根据YANN LECUN的论文《Gradient-based Learning Applied To Document Recognition》设计的LeNet-5神经网络,C语言写成,不依赖任何第三方库。 MNIST手写字符集初代训练识别率97%,多代训练识别率98%。 DEMO main.c文件为MNIST数据集的识别DEMO,直接编译即可运行,训练集60000张,测试集10000张。 项目环境 该项目为VISUAL STUDIO 2015项目,用VISUAL STUDIO 2015 UPDATE1及以上直接打开即可编译。采用ANSI C编写,因此源码无须修改即可在其它平台上编译。 如果因缺少openmp无法编译,请将lenet.c中的#include和#pragma omp parallel for删除掉即可。 API #####批量训练 lenet: LeNet5的权值的指针,LeNet5神经网络的核心 inputs: 要训练的多个图片对应unsigned char二维数组的数组,指向的二维数组的batchSize倍大小内存空间指针。在MNIST测试DEMO中二维数组为28x28,每个二维数组数值分别为对应位置图像像素灰度值 resMat:结果向量矩阵 labels:要训练的多个图片分别对应的标签数组。大小为batchSize batchSize:批量训练输入图像(二维数组)的数量 void TrainBatch(LeNet5 *lenet, image *inputs, const char(*resMat)[OUTPUT],uint8 *labels, int batchSize); #####单个训练 lenet: LeNet5的权值的指针,LeNet5神经网络的核心 input: 要训练的图片对应二维数组 resMat:结果向量矩阵 label: 要训练的图片对应的标签 void Train(LeNet5 *lenet, image input, const char(*resMat)[OUTPUT],uint8 label); #####预测 lenet: LeNet5的权值的指针,LeNet5神经网络的核心 input: 输入的图像的数据 labels: 结果向量矩阵指针 count: 结果向量个数 return 返回值为预测的结果 int Predict(LeNet5 *lenet, image input, const char(*labels)[LAYER6], int count); #####初始化 lenet: LeNet5的权值的指针,LeNet5神经网络的核心
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值