第三章 算子基础:卷积、池化、激活函数、全连接层在ISP中的角色
各位好,我是老周。今天咱们聊聊AI ISP里最基础的几个算子——卷积、池化、激活函数和全连接层。说实话,很多做ISP的工程师一听到“神经网络”就觉得头大,觉得那是算法工程师的事。但你要做算子移植和硬件加速,不懂这些算子的底层逻辑,那调试起来真的会抓瞎。
我个人习惯,先把这些算子在ISP里到底扮演什么角色搞清楚,然后再去抠计算量和访存量。这样你写代码的时候,心里才有底。
3.1 卷积层:ISP的“特征提取器”
卷积在ISP里干的事,说白了就是“看图说话”。它用一个小的窗口(卷积核)在图像上滑动,提取局部特征。比如边缘、纹理、颜色变化这些。
在传统ISP里,这些工作靠的是手工设计的滤波器——比如Sobel算子做边缘检测,高斯滤波做去噪。但在AI ISP里,卷积核的权重是训练出来的,不是人手工调的。
关键点:卷积核的大小决定了感受野。3×3的核看局部细节,5×5或7×7的核看更大范围的结构。在硬件实现时,3×3是最常见的,因为它在计算效率和特征提取能力之间取得了平衡。
我在项目中遇到过一个问题:用5×5的卷积核做去噪,效果确实好,但硬件资源消耗直接翻倍。后来我们改成两个3×3的级联,效果差不多,资源省了一大截。这就是所谓的“小核堆叠”技巧。
3.1.1 卷积的计算量分析
咱们来算笔账。假设输入特征图是H×W×Cin,输出是H×W×Cout,卷积核大小是K×K。
一次乘加操作算一次计算。那么总的计算量就是:
MACs = K × K × C_in × C_out × H × W
举个例子:输入是1920×1080的RGB图(3通道),用64个3×3的卷积核,输出也是1920×1080×64。算一下:
MACs = 3 × 3 × 3 × 64 × 1920 × 1080 ≈ 3.58 × 10^9
嗯,35.8亿次乘加操作。这还只是一个卷积层。你想想看,一个完整的AI ISP网络可能有几十层,这个计算量有多恐怖。
我的建议:在做硬件加速时,优先优化计算量最大的层。通常前几层的通道数少但分辨率高,后几层通道数多但分辨率低。你要根据实际硬件资源,决定是并行计算还是分时复用。
3.1.2 卷积的访存量分析
计算量是一回事,访存量是另一回事。很多时候,瓶颈不在计算,而在数据搬运。
卷积的访存包括三部分:
- 输入特征图:H × W × Cin × 数据类型大小
- 卷积核权重:K × K × Cin × Cout × 数据类型大小
- 输出特征图:H × W × Cout × 数据类型大小
还是刚才那个例子,假设用FP16(2字节):
输入访存:1920 × 1080 × 3 × 2 = 12.4 MB
权重访存:3 × 3 × 3 × 64 × 2 = 3.5 KB
输出访存:1920 × 1080 × 64 × 2 = 265 MB
看到了吗?输出特征图的访存量是265MB,而权重只有3.5KB。这就是为什么硬件加速器要设计大容量的片上缓存——不是为了存权重,而是为了存中间特征图。
注意:我曾经在一个项目里忽略了输出特征图的访存优化,结果DDR带宽被占满,帧率死活上不去。后来用了“输出驻留”策略——让部分输出数据留在片上缓存,等所有计算完成后再一次性写回DDR。帧率直接翻倍。
3.2 池化层:降维与特征压缩
池化层在ISP里干的事,就是“压缩”。把一张大图缩小,保留最重要的信息,丢掉不重要的细节。
最常见的两种池化:
- 最大池化:取窗口内的最大值。保留最显著的特征,适合边缘检测后的特征图。
- 平均池化:取窗口内的平均值。平滑特征,适合去噪后的特征图。
在AI ISP里,池化层通常放在卷积层后面,用来降低分辨率。比如从1920×1080降到960×540,计算量直接减少75%。
3.2.1 池化的计算量与访存量
池化的计算量比卷积小得多。以2×2最大池化为例,每个输出像素只需要做3次比较操作。对于H×W×C的输入:
比较次数 = (H/2) × (W/2) × C × 3 ≈ H × W × C × 0.75
访存量方面:
- 输入:H × W × C × 数据类型大小
- 输出:(H/2) × (W/2) × C × 数据类型大小
输出数据量只有输入的四分之一。这就是池化层的“压缩”效果——不仅计算量小,还能减少后续层的数据搬运量。
我的经验:在硬件实现时,池化层可以和卷积层“融合”在一起。比如在做卷积的同时,把池化的步长考虑进去,直接输出降采样后的结果。这样能省掉一次中间数据的读写。
3.3 激活函数:非线性变换
激活函数的作用,是给神经网络引入非线性。如果没有激活函数,再多层卷积也只是线性变换的堆叠,表达能力有限。
在AI ISP里,最常用的激活函数是ReLU和它的变种:
| 激活函数 | 公式 | 特点 | ISP中的应用场景 |
|---|---|---|---|
| ReLU | max(0, x) | 计算简单,梯度不饱和 | 大多数隐藏层 |
| Leaky ReLU | max(αx, x) | 避免神经元死亡 | 低光照增强网络 |
| PReLU | max(ax, x),a可学习 | 自适应负斜率 | HDR重建网络 |
| Sigmoid | 1/(1+e-x) | 输出范围(0,1) | 注意力机制、门控 |
ReLU的计算量几乎可以忽略——就是一个比较和选择。但它的访存量不小,因为要读写整个特征图的数据。
关键点:ReLU的硬件实现非常简单,只需要一个比较器。但要注意,ReLU会把负数全部置零,这会导致特征图变得稀疏。在硬件加速时,可以利用这种稀疏性跳过零值的计算,节省功耗和带宽。
我曾经在一个去噪网络里用了Sigmoid做最后的输出映射,结果硬件实现时发现指数运算太耗资源。后来换成ReLU加一个线性缩放,效果差不多,资源省了80%。
3.4 全连接层:特征聚合与决策
全连接层在ISP里用得不多,但也不是没有。它通常出现在网络的最后几层,用来做全局特征聚合和最终决策。
比如在自动白平衡网络中,输入是经过卷积提取的特征图,最后通过全连接层输出三个通道的增益系数。或者在一些质量评估网络中,全连接层输出一个分数。
3.4.1 全连接的计算量
全连接层的计算量公式很简单:
MACs = N_in × N_out
其中Nin是输入神经元数量,Nout是输出神经元数量。
举个例子:输入是经过全局平均池化后的512维特征向量,输出是3个白平衡增益系数:
MACs = 512 × 3 = 1536
这个计算量相比卷积层来说,简直可以忽略不计。
3.4.2 全连接的访存量
全连接的访存主要是权重矩阵:
权重访存 = N_in × N_out × 数据类型大小
还是刚才的例子,用FP16:
权重访存 = 512 × 3 × 2 = 3 KB
嗯,3KB,小得可怜。但要注意,如果全连接层的输入是展平后的特征图(比如把32×32×64展平成65536维),那权重矩阵就会变得非常大:
权重访存 = 65536 × 256 × 2 = 33.5 MB
这就很恐怖了。所以在AI ISP里,我们通常用全局平均池化代替全连接层,或者把全连接层放在分辨率最低的特征图后面。
避坑指南:我曾经接手过一个项目,算法工程师把全连接层直接接在32×32的特征图上,结果模型大小超过100MB,根本没法在嵌入式芯片上部署。后来我们改成全局平均池化+小全连接层,模型大小降到2MB,精度只掉了0.3%。
3.5 各算子计算量与访存量对比
咱们把四个算子放在一起对比一下,这样你心里有个谱:
| 算子 | 计算量(相对) | 访存量(相对) | 硬件实现难度 | ISP中的典型位置 |
|---|---|---|---|---|
| 卷积层 | 极高 | 高 | 中 | 特征提取前端 |
| 池化层 | 低 | 中 | 低 | 卷积层之间 |
| 激活函数 | 极低 | 中 | 极低 | 卷积层之后 |
| 全连接层 | 低(小输入) | 低(小输入) | 低 | 网络末端 |
说白了,卷积层是计算和访存的主力,也是硬件加速的重点。池化和激活函数是“搭便车”的,通常可以和卷积融合实现。全连接层在ISP里用得少,但用的时候要注意输入维度。
3.6 知识结构图:AI ISP算子全景
下面这张图展示了四个算子在AI ISP中的角色和关系:
这张图把四个算子的流程和关系画清楚了。从RAW输入开始,经过卷积→激活→池化的循环,最后通过全连接层输出结果。每个算子的计算量和访存量特点也标出来了。
3.7 小结:算子移植的核心思路
做算子移植和硬件加速,说白了就是三件事:
- 算清楚账:每个算子的计算量和访存量是多少,瓶颈在哪。
- 找对策略:计算瓶颈用并行化解决,访存瓶颈用缓存优化解决。
- 融合优化:能合并的算子尽量合并,减少中间数据的读写。
我个人习惯,拿到一个AI ISP网络,先跑一遍profile,看看每层的计算量和访存量分布。然后盯着最耗资源的那几层做优化。别一上来就想把所有层都加速,那样反而容易顾此失彼。