后期需要用FPGA实现神经网络的硬件加速,开发语言准备选择C++,由于是初学者,需找一不是很复杂的网络进行学习,故尝试对CNN经典网络LeNet-5进行分析和C++语言实现。
一、论文解析
LeNet-5 由LeCun于1998年提出,论文全篇共43页,非常详细的对CNN和LeNet进行了说明,本文仅对部分内容进行分析
1 LeNet-5 网络结构
上图是LeNet-5的网络结构,计输入层和输出层共计为8层:
- input layer --> 输入层(图中INPUT)
- convulational layer -->卷积层(图中C1)
- pooling layer --> activation function –池化层(图中S2)
- convulational layer -->卷积层(图中C3)
- pooling layer --> activation function --> 池化层(图中S4)
- convulational layer --> 卷积层(图中C5)
- fully connect layer --> 全连接层(图中F6)
- 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个
从 INPUT 到C1,即完成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;
}