FPGA学习笔记——在ZYNQ上部署一个简易的卷积神经网络(附源码)
喝凉茶m
编辑于 2022年09月12日 13:10

一、Lenet神经网络

Lenet 是一系列网络的合称,包括 Lenet1 - Lenet5,由 Yann LeCun 等人在 1990 年《Handwritten Digit Recognition with a Back-Propagation Network》中提出,是卷积神经网络的 HelloWorld。

神经网络部分我是看这位up主学习的。

Lenet是一个 7 层的神经网络,包含 2 个卷积层,2 个池化层,3 个全连接层。其中所有卷积层的所有卷积核都为 5x5,步长 strid=1,池化方法都为全局 pooling,激活函数为 Sigmoid。

使用pytorch实现如下:

代码块
Python
自动换行
复制代码
class LeNet(nn.Module):
    def __init__(self):
        super(LeNet, self).__init__()
        self.conv1 = nn.Conv2d(3, 16, 5)
        self.pool1 = nn.MaxPool2d(2, 2)
        self.conv2 = nn.Conv2d(16, 32, 5)
        self.pool2 = nn.MaxPool2d(2, 2)
        self.fc1 = nn.Linear(32*5*5, 120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)

    def forward(self, x):
        x = F.relu(self.conv1(x))    # input(3, 32, 32) output(16, 28, 28)
        x = self.pool1(x)            # output(16, 14, 14)
        x = F.relu(self.conv2(x))    # output(32, 10, 10)
        x = self.pool2(x)            # output(32, 5, 5)
        x = x.view(-1, 32*5*5)       # output(32*5*5)
        x = F.relu(self.fc1(x))      # output(120)
        x = F.relu(self.fc2(x))      # output(84)
        x = self.fc3(x)              # output(10)
        return x
复制成功

对上述网络模型进行训练后,将模型参数进行保存,方便我们之后对参数的读取。

代码块
Python
自动换行
复制代码
save_path = './Lenet.pth'
torch.save(net.state_dict(), save_path)
复制成功

读取模型参数代码如下:

代码块
Python
自动换行
复制代码
pthfile = r'Lenet.pth' #faster_rcnn_ckpt.pth
net = torch.load(pthfile,map_location=torch.device('cpu'))
torch.set_printoptions(threshold=np.Inf) #确保读取的参数不会因为太长被隐藏
for k in net.keys():
    print(k) # 查看四个键,分别是 model,optimizer,scheduler,iteration
print(len(net["conv1.weight"]))
print(net["conv1.weight"]) # 返回的是一个OrderedDict 对象
a = net["conv1.weight"];
f = open("conv1_weight.dat", "w")
f.writelines(str(a))
f.close()
复制成功

打印出来后,我们可以看到主要的几个参数模块分为:

conv1.weight conv1.bias conv2.weight conv2.bias fc1.weight fc1.bias fc2.weight fc2.bias fc3.weight fc3.bias 之后我们便只需要一个一个打印并保存为单独的dat文件即可。

二、参数格式

打开我们的conv1_weight.dat文件可以看到,数据被以数个"[ ]&#​34;所分隔,如下:

其中最里层的"[ ]&#​34;是5x5卷积核参数,"[[ ]]&#​34;是3个通道(第一层输入则表示RGB3个通道),"[[[ ]]]&#​34;是16个卷积核。

代码块
clike
自动换行
复制代码
tensor([[[[ 9.5803e-02,  5.5239e-02,  1.0288e-01,  7.5048e-02,  1.6195e-01],
          [ 5.1581e-03,  2.3891e-01,  2.9071e-01,  7.2389e-02,  3.2870e-02],
          [ 1.2892e-01, -2.4238e-02,  2.2148e-02,  4.4590e-02, -1.7992e-01],
          [-8.1017e-02, -2.2018e-01, -1.9814e-01, -1.4945e-01, -1.9766e-01],
          [ 4.8322e-02, -2.7193e-03, -1.1966e-01, -4.9346e-02, -1.5816e-02]],

         [[-1.0290e-01, -2.5498e-02, -4.0403e-02,  1.3622e-02, -5.6298e-02],
          [ 2.1140e-02,  1.4635e-01, -1.2658e-02,  9.6755e-02,  8.4838e-03],
          [ 1.5997e-02, -3.9567e-02, -9.2227e-02, -1.3738e-01, -1.9495e-02],
          [-1.4476e-01, -2.8756e-02, -1.8236e-01, -6.7748e-02, -2.7482e-02],
          [-4.3342e-02, -9.1109e-02,  7.4808e-02,  3.6240e-02,  8.8675e-02]],

         [[-1.6678e-01, -2.8875e-03, -1.2045e-01,  4.2056e-02, -1.0014e-01],
          [ 9.2429e-02,  7.2163e-03,  1.5210e-01,  8.0952e-02,  7.4164e-02],
          [ 6.2761e-02,  1.4735e-01, -2.9008e-02,  1.0105e-01, -1.2899e-02],
          [-1.0417e-01, -1.2973e-02,  2.5826e-02, -8.4108e-02, -7.9515e-02],
          [-8.8542e-03,  3.5951e-02,  2.2815e-02,  7.3007e-03,  7.6190e-02]]],
        	......
复制成功

但这种格式不利于我们在HLS中调取,因此需要对数据格式进行单独的调整,在HLS设计中卷积层的权重是由3维数组组成的,因此我们也要将dat数据也改成3维数组可读的形式。首先,我们需要知道3维数组在文件的形式,如下:

代码块
clike
自动换行
复制代码
  	int a[3][2][1] = { { {1},{2} } ,{ {3},{4} } ,{ {5},{6} } };
	printf("%d",a[0][0][0]); # 1
 	printf("%d",a[0][1][0]); # 2    
 	printf("%d",a[1][0][0]); # 3
        ......
 	printf("%d",a[2][1][0]); # 6
复制成功

所以dat格式如下:

代码块
clike
自动换行
复制代码
{{9.5803e-02,5.5239e-02,1.0288e-01,7.5048e-02,1.6195e-01,
5.1581e-03,2.3891e-01,2.9071e-01,7.2389e-02,3.2870e-02,
1.2892e-01,-2.4238e-02,2.2148e-02,4.4590e-02,-1.7992e-01,
-8.1017e-02,-2.2018e-01,-1.9814e-01,-1.4945e-01,-1.9766e-01,
4.8322e-02,-2.7193e-03,-1.1966e-01,-4.9346e-02,-1.5816e-02},
{-1.0290e-01,-2.5498e-02,-4.0403e-02,1.3622e-02,-5.6298e-02,
2.1140e-02,1.4635e-01,-1.2658e-02,9.6755e-02,8.4838e-03,
1.5997e-02,-3.9567e-02,-9.2227e-02,-1.3738e-01,-1.9495e-02,
-1.4476e-01,-2.8756e-02,-1.8236e-01,-6.7748e-02,-2.7482e-02,
-4.3342e-02,-9.1109e-02,7.4808e-02,3.6240e-02,8.8675e-02},
{-1.6678e-01,-2.8875e-03,-1.2045e-01,4.2056e-02,-1.0014e-01,
9.2429e-02,7.2163e-03,1.5210e-01,8.0952e-02,7.4164e-02,
6.2761e-02,1.4735e-01,-2.9008e-02,1.0105e-01,-1.2899e-02,
-1.0417e-01,-1.2973e-02,2.5826e-02,-8.4108e-02,-7.9515e-02,
-8.8542e-03,3.5951e-02,2.2815e-02,7.3007e-03,7.6190e-02}},
......
复制成功

除了第一层卷积与第二层卷积是上述格式外,其余的数据均为如下:

代码块
clike
自动换行
复制代码
0.006391,
-0.002437,
0.012572,
0.014400,
-0.030008,
-0.021943,
0.000175,
-0.039245,
0.022859,
-0.003466,
0.006915,
-0.038161,
......
复制成功

将其保存在同一目录下:由于第一层全连接层参数过多,我将其分成了上下两部分,分开调用

三、HLS设计

1)、conv函数设计

输入类型的是HLS中的半浮点型half(16位,能较为节省空间,需要更小的话可以使用8位定点数)。

input : 输入的图像数据(conv1是32x32,conv2是14x14)

weight : 输入权重

in_row : 输入大小(32或14)

out_row : 输出大小 (28或10)

core : 卷积核大小 (5)

代码块
clike
自动换行
复制代码
void conv2(half* input,const half* weight, half* output, int in_row, int out_row, int core) {
Row:
	for (int r = 0; r < out_row; r++)
	{
	Column:
		for (int c = 0; c < out_row; c++)
		{
		Kernel_Row:
			for (int kr = 0; kr < core; kr++)
			{
			Kernel_Column:
				for (int kc = 0; kc < core; kc++)
				{
					output[r * out_row + c] += input[(r + kr) * in_row + (c + kc)] * weight[kr * core + kc];
				}
			}
		}
}
}
复制成功

3)、pool函数设计

这个函数是采用最大值池化

input : 输入数据(28或10)

output : 输出数据 (14或5)

in_row : 输入大小 (28或10)

out_row : 输出大小 (14或5)

代码块
clike
自动换行
复制代码
#define MAX(A,B) ((A<B)?B:A);
void pool(half* input, half* output, int in_row, int out_row) { //6 3
	for (int i = 0; i < out_row; i++) {
		for (int j = 0; j < out_row; j++) {
			output[i * out_row + j] = input[i * 2 * in_row + j * 2]; 
			for (int k = 0; k < 2; k++) {
				for (int m = 0; m < 2; m++) {
					output[i * out_row + j] = MAX(output[i * out_row + j], input[(i*2 + k) * in_row + (j*2 + m)]);
				}
			}
		}
    }
}
复制成功

4)、激活函数设计

sigmoid激活函数

a : 需要激活的数据 

size : 输入大小 (28x28等)

代码块
clike
自动换行
复制代码
void Sigmoid1(half* a, int size)
{
	for (int i = 0; i < size; i++)
		a[i] = 1 / (1 + expf(-a[i]));
}
复制成功

5)、端口设计

通过axi 总线调用模块,并借助bram来传递数据。

代码块
ts
自动换行
复制代码
	#pragma HLS INTERFACE bram port=output
	#pragma HLS INTERFACE bram port=input
	#pragma HLS INTERFACE s_axilite port=return bundle=CRTL_BUS
复制成功

6)、仿真测试

代码块
clike
自动换行
复制代码
int main(){
	float output[10] = {0};
	int input[3072] = {
		#include "./niao2.dat"
	};
	my_net(input,output);
	for(int j=0;j<10;j++){
		printf("%f\n",output[j]);
	}
	printf("end");
	return 0;
}
//'plane', 'car', 'bird', 'cat','deer', 'dog', 'frog', 'horse', 'ship', 'truck'
/*结果:INFO: [SIM 2] *************** CSIM start ***************
INFO: [SIM 4] CSIM will launch GCC as the compiler.
make: 'csim.exe' is up to date.
0.246582
0.155396
0.437012
0.332520
0.431885
0.254883
0.226074
0.196045
0.239136
0.187134
end
INFO: [SIM 1] CSim done with 0 errors.
INFO: [SIM 3] *************** CSIM finish ***************/
复制成功

7)、HLS设计报告

四、Block Design设计

需要用到的:SD卡、uart、HLS生成的IP核、两个bram模块以及bram controller(分别控制输入与输出)

如图:

Data address editor :

五、SDK设计

(没有读卡器 T_T)所以就模拟一下读卡的场景:将图像数据存入SD卡里成为txt文件,再从SD卡中读取并传到bram0里作为输入数据传输到lenet IP核中,再从bram1中读取结果并打印出来。

其中SD卡的操作部分就不赘述了,不熟悉的可以去看看正点原子的教程,里面讲得很详细。

将数组传入bram0:

代码块
clike
自动换行
复制代码
for(i=0;i<3072;i++)
	 {
		result = data[i];
		Xil_Out32(XPAR_AXI_BRAM_CTRL_0_S_AXI_BASEADDR+i*4,result);

	 }
复制成功

初始化lenet IP核以及读取bram1数据并打印:

代码块
clike
自动换行
复制代码
XMy_net HlsXMem_test;
	XMy_net_Config *ExamplePtr;
	printf("Look Up the device configuration.\n");
	ExamplePtr = XMy_net_LookupConfig(XPAR_XMY_NET_0_DEVICE_ID);
	if (!ExamplePtr)
	{
		printf("ERROR: Lookup of accelerator configuration failed.\n\r");
	return XST_FAILURE;
	}
	printf("Initialize the Device\n");
	status = XMy_net_CfgInitialize(&HlsXMem_test, ExamplePtr);
	if (status != XST_SUCCESS)
	{
		printf("ERROR: Could not initialize accelerator.\n\r");
	return(-1);
	}

	XMy_net_Start(&HlsXMem_test);
	while (XMy_net_IsDone(&HlsXMem_test) == 0);
	for(i=0;i<10;i++)
	{
		revs=Xil_In32(XPAR_AXI_BRAM_CTRL_1_S_AXI_BASEADDR+4*i);
		float recvf = u32_to_float(revs);
		printf("Recognize result:%f\n",recvf);
	}
	printf("all vision over\n");
复制成功

最终结果如图:

结果与仿真测试一致。

参考文章:

如何从零开始将神经网络移植到FPGA(ZYNQ7020)加速_Jarvis码员的博客-CSDN博客_zynq 神经网络