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;
}
  • 7
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
### 回答1: LeNet-5是一种经典的卷积神经网络模型,常用于手写数字识别任务。HLS(High-Level Synthesis)是针对FPGA设计的一种高级综合工具,它可以将高级语言如C/C++代码转化为硬件描述语言。 要实现LeNet-5模型的HLS版本,我们可以按照以下步骤进行: 1. 首先,我们需要将LeNet-5的网络结构进行转换,以适应HLS工具的要求。LeNet-5由两个卷积层、两个池化层和三个全连接层组成。我们需要将这些层的相应操作转化为HLS中的硬件描述。例如,卷积操作可以使用HLS中的乘法器和加法器实现,池化操作可以使用选择器进行。 2. 接下来,我们可以使用HLS工具中的数据流和并行指令来优化LeNet-5的实现。通过合理地划分和并行化计算以及使用流水线技术,可以提高模型的效率和速度。 3. 在转换为HLS代码后,我们可以使用HLS工具提供的仿真功能进行测试和验证。通过在测试数据上运行HLS代码,我们可以确保模型在硬件上的实现与预期输出一致。 4. 最后,我们可以将HLS代码综合为目标FPGA设备上的位文件。通过将生成的位文件加载到FPGA上,我们就可以在硬件平台上运行LeNet-5模型进行手写数字识别任务了。 总之,通过使用HLS工具,我们可以将LeNet-5模型从C语言代码转化为硬件实现,并在FPGA上运行,以提高其性能和效率。这种转换过程需要注意HLS工具的特性和限制,并进行适当的优化和测试,以确保模型的正确性和可靠性。 ### 回答2: Lenet-5是一种经典的卷积神经网络CNN结构,由Yann LeCun在1998年提出。要在HLS(高层次综合)中实现Lenet-5,主要需要将Lenet-5网络结构转换为可在HLS工具中使用的硬件描述语言(HDL)。 Lenet-5网络结构主要包含七个层级:两个卷积层,两个池化层和三个全连接层。在HLS实现Lenet-5时,每个层级都需要进行适当的转换。 首先是卷积层。在HLS中,可以使用具有适当参数的卷积内核来表示卷积层。通过在HDL描述文件中定义卷积核的大小、步长和填充,可以实现卷积层。 第二是池化层。池化层的操作可以通过在HDL描述文件中定义池化区域的大小和类型来实现。常用的是最大池化和平均池化。 第三是全连接层。全连接层可以通过使用适当的权重矩阵和偏置向量来实现。在HLS中,可以使用乘法和加法操作来实现全连接层。 最后,在HLS工具中,需要将输入数据流和输出数据流与适当的处理单元(如DSP、BRAM等)进行连接,以实现完整的Lenet-5网络结构实现完成后,可以对HLS代码进行综合、优化和验证,以生成可在FPGA上运行的硬件实现。在FPGA上运行Lenet-5可以进行图像分类、目标检测等任务。 总而言之,要在HLS中实现Lenet-5,需要将Lenet-5网络结构转化为HDL代码,并对其进行综合、优化和验证,最终生成可在FPGA上运行的硬件实现。这样就能够利用硬件加速的方式快速处理Lenet-5网络的图像识别任务。 ### 回答3: Lenet-5是一个经典的卷积神经网络模型,常用于手写数字识别任务。HLS(High-Level Synthesis)是一种用于FPGA(Field-Programmable Gate Array)的高级综合工具,可以将C/C++代码转化为硬件描述语言(HDL)。 实现Lenet-5模型的HLS过程主要包括以下几个步骤: 1. 定义Lenet-5的网络结构Lenet-5由两个卷积层、两个池化层和三个全连接层组成。在C代码中,我们需要定义网络的各层结构、参数和激活函数。 2. 数据预处理:在Lenet-5中,输入图像为32x32的灰度图像。我们可以使用C代码读取输入图像,并对其进行预处理,如图像缩放和归一化等操作。 3. 卷积计算:在C代码中,我们可以使用循环嵌套来实现卷积计算。对于每个卷积层,我们需要定义卷积核的大小和数量,并对输入图像进行卷积操作。卷积操作可以通过计算每个卷积核与输入图像的点积得到输出特征图。 4. 池化计算:在C代码中,我们可以使用循环嵌套来实现池化计算。对于每个池化层,我们需要定义池化窗口的大小和步长,并对卷积层的输出特征图进行池化操作。常用的池化操作有最大池化和平均池化,可以根据需求选择适合的池化方法。 5. 全连接计算:在C代码中,我们可以使用矩阵乘法和激活函数来实现全连接计算。对于每个全连接层,我们需要定义权重矩阵和偏置向量,并对前一层的输出进行矩阵乘法运算,然后通过激活函数进行非线性映射。 6. 输出结果:在C代码中,我们可以将最后一个全连接层的输出结果与标签进行比较,计算损失并进行反向传播。 通过以上步骤,我们可以使用C语言编写Lenet-5模型的HLS实现。在完成C代码编写后,可以使用HLS工具将C代码转化为HDL代码,以便在FPGA上进行硬件加速计算。这样可以实现Lenet-5模型的高效部署和运行,提高模型的执行速度和计算效率。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值