3. 卷积神经网络基础回顾:卷积层、池化层、全连接层、激活函数(ReLU/Sigmoid)、BN层原理
各位同学,欢迎来到第三讲。在咱们正式动手写Verilog之前,我觉得有必要把CNN的几个核心构件再捋一遍。你可能会问:“这些基础东西,网上教程不是一大堆吗?” 嗯,确实很多。但咱们做FPGA加速,关注点和纯软件不一样。软件工程师看的是精度,咱们看的是——能不能用硬件高效实现。
我个人习惯,在开始一个加速项目前,先把算法里的每个算子拆开,问自己三个问题:
1. 这个算子计算量大不大?
2. 能不能并行?
3. 能不能用定点数代替浮点数?
带着这三个问题,咱们开始今天的回顾。
3.1 卷积层:FPGA加速的主战场
卷积层,说白了就是“滑动窗口乘加”。输入一张特征图,用一个卷积核(也叫滤波器)在上面滑,每滑到一个位置,就把对应位置的像素和卷积核的权重相乘,然后全部加起来。
数学形式:
Output(x, y) = Σ Σ Input(x+i, y+j) * Weight(i, j) + Bias
这里有个关键点:乘加操作。一个3x3的卷积核,一次卷积需要9次乘法和8次加法。如果输入是224x224的图像,有64个卷积核,那计算量就是:
224 * 224 * 64 * 9 ≈ 28.9 百万次乘法。
FPGA视角:卷积层天然适合并行。因为每个输出像素的计算是独立的。你可以同时计算多个输出通道,也可以同时计算多个输入位置。我在项目中遇到过,用DSP48E1硬核做乘加,一个时钟周期就能搞定一次乘加,效率极高。
避坑指南:我曾经在做一个YOLOv3-tiny项目时,忽略了输入特征图的边界填充(padding)。结果卷积核滑到边缘时,数据不够了,导致输出尺寸不对。后来我专门写了一个“边界处理模块”,用零填充或者复制填充。记住:硬件里没有“自动补零”这回事,你得自己算好地址。
3.2 池化层:降采样,省资源
池化层的作用很简单:缩小特征图尺寸。常见的有最大池化(Max Pooling)和平均池化(Average Pooling)。
- 最大池化:取窗口内的最大值。比如2x2的窗口,步长为2,输出尺寸减半。
- 平均池化:取窗口内的平均值。
在YOLO里,池化层用得不多,但早期的VGG、ResNet里很常见。为什么FPGA喜欢池化?因为它没有乘法。最大池化只需要比较器,平均池化只需要加法器和移位器(除以2的幂次)。
我记得有一次,一个同事非要用除法器做平均池化,结果资源爆了。我告诉他:“除以4就是右移2位,你写个移位器不就行了?” 他恍然大悟。嗯,这就是硬件思维和软件思维的区别。
注意:池化层的步长和窗口大小必须匹配。比如2x2窗口,步长必须是2,否则会有重叠。重叠不是不行,但计算量会增加。我建议:在FPGA上,尽量用无重叠池化,省事。
3.3 全连接层:参数大户
全连接层,就是“每个输入连到每个输出”。数学上就是矩阵乘法:
Output = Input * Weight + Bias
全连接层的参数量巨大。比如输入是4096维,输出是1000维,那权重矩阵就是4096x1000 ≈ 4百万个参数。这在FPGA上是个噩梦——BRAM和DSP根本不够用。
好在YOLO这类目标检测网络,全连接层很少。YOLOv3甚至完全用卷积层代替了全连接层(称为全卷积网络)。但如果你要加速分类网络(比如VGG16),那全连接层就是瓶颈。
我的经验:在FPGA上实现全连接层,我一般用“分块矩阵乘法”。把大矩阵切成小块,一块一块算。虽然延迟大了点,但资源可控。另外,权重一定要量化,8位定点数就够了,别用32位浮点,否则BRAM会哭的。
3.4 激活函数:ReLU vs Sigmoid
激活函数的作用是引入非线性。没有它,神经网络就是一堆线性变换的叠加,表达能力有限。
3.4.1 ReLU:硬件的最爱
ReLU的公式简单到令人发指:
f(x) = max(0, x)
在FPGA上,ReLU就是一个比较器和一个多路选择器。输入大于0就输出原值,否则输出0。没有乘法,没有除法,没有指数运算。我常说:ReLU是上帝送给FPGA工程师的礼物。
3.4.2 Sigmoid:硬件的噩梦
Sigmoid的公式:
f(x) = 1 / (1 + e^(-x))
这里有指数运算和除法。在FPGA上实现指数,要么用CORDIC算法,要么用查找表(LUT)。无论哪种,资源消耗都很大。而且Sigmoid的输出范围是(0, 1),这意味着你需要浮点数或者定点数的小数部分。
我曾经在一个项目里被迫用了Sigmoid(因为算法要求),结果一个激活函数模块占了整个芯片20%的LUT。后来我改用分段线性逼近,才把资源降下来。所以我的建议是:能用ReLU就别用Sigmoid。如果非用不可,考虑用硬件友好的近似实现。
对比总结:
| 激活函数 | 硬件实现难度 | 资源消耗 | 推荐场景 |
|---|---|---|---|
| ReLU | 极低 | 几乎为零 | 隐藏层首选 |
| Sigmoid | 高 | 高 | 输出层(二分类) |
3.5 BN层:训练时有用,推理时合并
Batch Normalization(批归一化)在训练时很重要,它能加速收敛、防止梯度消失。但在推理阶段(也就是咱们FPGA部署的阶段),BN层可以合并到前面的卷积层或全连接层。
BN的公式:
y = γ * (x - μ) / √(σ² + ε) + β
其中μ是均值,σ²是方差,γ和β是可学习参数,ε是一个小常数防止除零。
在推理时,μ、σ²、γ、β都是固定的。所以我们可以把BN的系数合并到卷积的权重和偏置里:
// 合并后的权重
W_merged = W * γ / √(σ² + ε)
// 合并后的偏置
B_merged = (B - μ) * γ / √(σ² + ε) + β
这样一来,推理时就不需要单独计算BN了。直接算卷积,用合并后的权重和偏置就行。这能省掉一大块硬件资源。
重要提醒:合并BN时,一定要用训练好的参数。我见过有人用随机初始化的参数去合并,结果推理结果完全不对。另外,注意数值精度。如果卷积权重是8位定点,BN合并后的权重可能会超出范围,需要重新量化。
好了,这一章的内容就到这里。咱们回顾了卷积层、池化层、全连接层、激活函数和BN层。下一章,我会带大家看看如何把这些算子映射到FPGA的硬件结构上。到时候,咱们会聊到行缓存(Line Buffer)、脉动阵列(Systolic Array)这些实战技巧。
记住:理解算法是第一步,理解硬件是第二步,把两者结合起来才是真本事。