
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实现如下:
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 对上述网络模型进行训练后,将模型参数进行保存,方便我们之后对参数的读取。
save_path = './Lenet.pth'
torch.save(net.state_dict(), save_path) 读取模型参数代码如下:
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文件可以看到,数据被以数个"[ ]"所分隔,如下:
其中最里层的"[ ]"是5x5卷积核参数,"[[ ]]"是3个通道(第一层输入则表示RGB3个通道),"[[[ ]]]"是16个卷积核。
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维数组在文件的形式,如下:
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格式如下:
{{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}},
...... 除了第一层卷积与第二层卷积是上述格式外,其余的数据均为如下:
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中的半浮点型half(16位,能较为节省空间,需要更小的话可以使用8位定点数)。
input : 输入的图像数据(conv1是32x32,conv2是14x14)
weight : 输入权重
in_row : 输入大小(32或14)
out_row : 输出大小 (28或10)
core : 卷积核大小 (5)
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];
}
}
}
}
} 这个函数是采用最大值池化
input : 输入数据(28或10)
output : 输出数据 (14或5)
in_row : 输入大小 (28或10)
out_row : 输出大小 (14或5)
#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)]);
}
}
}
}
} sigmoid激活函数
a : 需要激活的数据
size : 输入大小 (28x28等)
void Sigmoid1(half* a, int size)
{
for (int i = 0; i < size; i++)
a[i] = 1 / (1 + expf(-a[i]));
} 通过axi 总线调用模块,并借助bram来传递数据。
#pragma HLS INTERFACE bram port=output
#pragma HLS INTERFACE bram port=input
#pragma HLS INTERFACE s_axilite port=return bundle=CRTL_BUS 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 ***************/ 
需要用到的:SD卡、uart、HLS生成的IP核、两个bram模块以及bram controller(分别控制输入与输出)
如图:

Data address editor :

(没有读卡器 T_T)所以就模拟一下读卡的场景:将图像数据存入SD卡里成为txt文件,再从SD卡中读取并传到bram0里作为输入数据传输到lenet IP核中,再从bram1中读取结果并打印出来。
其中SD卡的操作部分就不赘述了,不熟悉的可以去看看正点原子的教程,里面讲得很详细。
将数组传入bram0:
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数据并打印:
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 神经网络