4、轻量级CNN基础:深度可分离卷积、分组卷积、通道混洗——轻量化设计的三大法宝。
做嵌入式视觉这几年,我踩过最大的坑就是——模型在服务器上跑得飞快,一上板子就卡成PPT。你想想看,一个普通的3x3卷积,参数量和计算量都大得吓人。那时候我就在想,有没有办法让卷积“瘦身”呢?
后来我接触到了轻量化CNN的三大法宝:深度可分离卷积、分组卷积、通道混洗。说白了,它们就是给标准卷积做“拆解”和“重组”。今天我就把这几个家伙的底细给你讲透。
4.1 标准卷积的“奢侈”之处
先看一个标准卷积。假设输入是HxWxC,输出是HxWxN,卷积核大小是KxK。那么参数量是多少?
答案是:K x K x C x N。
计算量呢?H x W x K x K x C x N。
这个数字有多大?举个例子,输入是112x112x32,输出32个通道,3x3卷积。参数量就是3x3x32x32 = 9216。计算量是112x112x9216 ≈ 1.15亿次乘法。嗯,这在服务器上不算什么,但在Cortex-M4上,这基本就是灾难。
我在项目中遇到过,一个标准卷积层就把STM32H7的算力吃掉了80%。当时我就意识到,必须换思路。
4.2 深度可分离卷积:把卷积拆成两步走
深度可分离卷积的核心思想很简单:把标准卷积拆成两步——深度卷积和逐点卷积。
- 深度卷积:每个输入通道单独用一个卷积核处理。说白了,就是C个通道,每个通道用一个KxK的卷积核,输出还是C个通道。
- 逐点卷积:用1x1卷积把通道数从C变成N。这一步负责跨通道的信息融合。
参数量是多少?深度卷积:K x K x C。逐点卷积:1 x 1 x C x N。加起来就是K²C + CN。
对比标准卷积的K²CN,你算算节省了多少?
以刚才的例子:K=3, C=32, N=32。标准卷积参数量9216。深度可分离卷积:3x3x32 + 1x1x32x32 = 288 + 1024 = 1312。节省了大约86%!
关键点:深度可分离卷积的参数量大约是标准卷积的 1/N + 1/K²。当N和K都比较大时,节省效果非常明显。
我曾经在MobileNetV1上做过测试,把标准卷积全部替换成深度可分离卷积,模型大小从30MB降到了4.2MB,推理速度提升了将近5倍。代价是精度掉了不到1%。这个trade-off,在嵌入式场景下完全值得。
4.3 分组卷积:把通道分成几组来处理
分组卷积其实更早出现,最早是在AlexNet里用的。当时因为显存不够,所以把卷积分到两个GPU上算。后来发现,分组卷积本身就有正则化效果,还能减少参数量。
它的做法很简单:把输入通道分成G组,每组独立做卷积。每组输入通道数是C/G,输出通道数是N/G。最后把G组的结果拼起来。
参数量:K x K x (C/G) x (N/G) x G = K²CN/G。
你看,参数量直接除以G。G越大,模型越小。
我的经验:分组卷积的G一般取2、4、8。G太大,组与组之间信息无法流通,精度会掉得厉害。我建议G不要超过输入通道数的1/4。
举个例子,输入64通道,输出64通道,3x3卷积。标准卷积参数量36864。分组卷积G=4时,参数量9216。节省了75%。
但这里有个问题——分组卷积的组之间是隔离的。这就引出了下一个法宝。
4.4 通道混洗:让组与组之间“串门”
分组卷积有个天然缺陷:组与组之间没有信息交换。你想想看,如果每个组只处理自己那部分通道,那不同组的特征就永远无法融合。这会导致模型表达能力下降。
通道混洗就是来解决这个问题的。它的操作很简单:
- 假设有G组,每组有C/G个通道。
- 把通道重新排列,让每个组都包含其他组的通道。
- 具体做法:把通道reshape成(G, C/G),然后转置,再flatten。
说白了,就是让不同组的特征“串个门”。
# 通道混洗的伪代码
def channel_shuffle(x, groups):
batch, channels, height, width = x.shape
channels_per_group = channels // groups
# reshape成 (batch, groups, channels_per_group, height, width)
x = x.reshape(batch, groups, channels_per_group, height, width)
# 转置:交换groups和channels_per_group维度
x = x.transpose(0, 2, 1, 3, 4)
# flatten回原始形状
x = x.reshape(batch, channels, height, width)
return x
这个操作没有参数,计算量也极小。但它带来的收益却很大。ShuffleNet就是靠这个技巧,在同等计算量下比MobileNet精度高了2-3个点。
注意:通道混洗在推理时要注意内存访问模式。如果实现不当,会导致缓存不命中,反而拖慢速度。我在NCNN上就遇到过这个问题,后来通过手动优化内存布局解决了。
4.5 三大法宝的对比与选择
我把这三个方法放在一起对比一下:
| 方法 | 参数量缩减 | 计算量缩减 | 精度影响 | 适用场景 |
|---|---|---|---|---|
| 深度可分离卷积 | 约1/N + 1/K² | 约1/N + 1/K² | 较小(1-2%) | 通用,MobileNet系列 |
| 分组卷积 | 1/G | 1/G | 中等(G越大精度越低) | ResNeXt, RegNet |
| 通道混洗 | 无(仅重组) | 极小 | 提升精度 | ShuffleNet, 分组卷积后必用 |
我个人习惯是:优先用深度可分离卷积,因为它最稳定,精度损失最小。如果模型需要更轻量,我会加上分组卷积,但一定要配合通道混洗。记住,分组卷积和通道混洗是“黄金搭档”,缺一不可。
4.6 实战建议:如何组合使用
在实际项目中,我一般这样组合:
- 第一层:标准卷积(输入通道少,没必要轻量化)
- 中间层:深度可分离卷积 + 逐点卷积
- 深层:分组卷积(G=2或4)+ 通道混洗
- 最后一层:标准卷积或全连接(输出维度小)
举个例子,一个适合Cortex-M7的模型结构:
Input: 224x224x3
Conv1: 3x3, stride=2, 32通道 (标准卷积)
Block1: 深度可分离卷积 x 2, 64通道
Block2: 深度可分离卷积 x 3, 128通道
Block3: 分组卷积(G=4) + 通道混洗 x 2, 256通道
Block4: 分组卷积(G=4) + 通道混洗 x 2, 512通道
FC: 1000类
这个模型在STM32H743上能做到30fps,精度比MobileNetV1略高一点。
避坑指南:我曾经在分组卷积后忘了加通道混洗,结果模型精度掉了5个点。找了三天bug才发现。所以,分组卷积和通道混洗一定要成对出现。
好了,三大法宝就讲到这里。下一章我会讲如何用这些技巧搭建一个完整的轻量级目标检测网络。到时候咱们再细聊。