深度可分离卷积这个概念,我第一次认真去啃是在做一个移动端图像分类任务的时候。当时模型体积压不下去,推理速度也上不来,试了各种剪枝量化效果都一般,后来把主干网络换成基于深度可分离卷积的结构,参数量直接掉了一个数量级,精度却没怎么掉。从那以后我就养成了一个习惯:只要是在算力受限的场景下做卷积网络,第一反应就是先看看能不能用深度可分离卷积来替代标准卷积。
这篇文章主要想聊清楚三件事:深度可分离卷积到底是怎么算的、它为什么能省参数省算力、以及以 Xception 为代表的这类结构在实际落地时有哪些容易踩的坑。不管你是刚接触卷积神经网络的新手,还是已经做过几个模型但没深究过底层计算的老手,应该都能从里面找到对自己有用的东西。我会尽量把公式拆开讲,配上具体的数字例子,让你看完能自己手算一遍参数量。
1. 从标准卷积的计算量说起
1.1 标准卷积到底在算什么
要理解深度可分离卷积的价值,得先把标准卷积的账算清楚。假设输入特征图尺寸是 $D_f \times D_f$,通道数是 $M$,卷积核大小是 $D_k \times D_k$,输出通道数是 $N$。那么一个标准卷积层的参数量是:
$$D_k \times D_k \times M \times N$$
计算量(乘加次数)是:
$$D_k \times D_k \times M \times N \times D_f \times D_f$$
这两个公式看着简单,但里面藏着一个关键信息:标准卷积在每一个空间位置上,都是把所有输入通道和所有输出通道做全连接式的组合。也就是说,输入有多少通道,每个卷积核就要有多深;输出要多少通道,就要有多少个这样的卷积核。
举个具体例子。输入是 $224 \times 224 \times 3$ 的彩色图像,用 $3 \times 3$ 卷积核输出 64 通道,那么参数量是 $3 \times 3 \times 3 \times 64 = 1728$。这个数字还不算大。但如果中间层输入是 $56 \times 56 \times 256$,输出 512 通道,$3 \times 3$ 卷积核,参数量就是 $3 \times 3 \times 256 \times 512 = 1,179,648$,超过一百万个参数。计算量更是要再乘上 $56 \times 56 = 3136$,达到约 37 亿次乘加。
这就是问题所在:标准卷积的参数量和计算量随着通道数增长是平方级的。通道数翻倍,参数量翻四倍。在深层网络里,通道数动辄几百上千,这个开销就非常可观了。
1.2 通道相关性与空间相关性被耦合在一起
标准卷积之所以这么费参数,根本原因在于它把两件事绑在一起做了:空间维度的特征提取和通道维度的特征组合。
你可以这样理解:一个 $3 \times 3 \times 256$ 的卷积核,它在做的是——在 $3 \times 3$ 的局部窗口内,把 256 个通道的信息加权求和,输出一个值。这个操作同时完成了"看局部空间模式"和"跨通道混合信息"两个任务。
问题在于,这两个任务其实是可以解耦的。空间模式的学习,每个通道可以独立进行;通道之间的信息融合,可以用更便宜的方式来做。把这两件事拆开,就是深度可分离卷积的核心思想。
我个人的经验是,很多人在初学的时候会把深度可分离卷积和分组卷积搞混。分组卷积是把输入通道分成几组,每组独立做标准卷积,组内仍然是全通道连接。而深度可分离卷积是先做逐通道卷积(每个通道一个独立的卷积核),再做逐点卷积($1 \times 1$ 卷积)来融合通道。两者的计算模式和参数量都不一样,后面我会用表格对比。
2. 深度可分离卷积的两步拆解
2.1 第一步:逐通道卷积(Depthwise Convolution)
逐通道卷积的做法很直接:输入有多少个通道,就用多少个卷积核,每个卷积核只负责一个通道,在空间维度上滑动。
具体来说,输入是 $D_f \times D_f \times M$,我们用 $M$ 个 $D_k \times D_k \times 1$ 的卷积核,每个卷积核只在对应的一个通道上做卷积,输出 $M$ 个通道的特征图。这一步的参数量是:
$$D_k \times D_k \times M$$
计算量是:
$$D_k \times D_k \times M \times D_f \times D_f$$
对比标准卷积的 $D_k \times D_k \times M \times N$,逐通道卷积少了 $N$ 这个因子。因为每个卷积核只处理一个通道,不需要跨通道组合,所以参数量大幅下降。
但这里有个明显的局限:逐通道卷积完全没有跨通道的信息交互。每个输出通道只包含对应输入通道的空间滤波结果,通道之间是孤立的。如果只用这一步,网络就失去了通道间特征组合的能力,表达能力会严重受限。
2.2 第二步:逐点卷积(Pointwise Convolution)
逐点卷积就是 $1 \times 1$ 卷积,它的作用是做通道融合。输入是逐通道卷积输出的 $D_f \times D_f \times M$,我们用 $N$ 个 $1 \times 1 \times M$ 的卷积核,输出 $D_f \times D_f \times N$。
这一步的参数量是:
$$1 \times 1 \times M \times N = M \times N$$
计算量是:
$$M \times N \times D_f \times D_f$$
逐点卷积在空间上不做任何滤波(核大小是 $1 \times 1$),它只做一件事:把 $M$ 个通道线性组合成 $N$ 个通道。这本质上是一个作用在每个空间位置上的全连接层。
把两步合起来,深度可分离卷积的总参数量是:
$$D_k \times D_k \times M + M \times N$$
总计算量是:
$$D_k \times D_k \times M \times D_f \times D_f + M \times N \times D_f \times D_f$$
2.3 和标准卷积的量化对比
现在来算一笔账。标准卷积的参数量是 $D_k \times D_k \times M \times N$,深度可分离卷积是 $D_k \times D_k \times M + M \times N$。两者的比值是:
$$\frac{D_k \times D_k \times M + M \times N}{D_k \times D_k \times M \times N} = \frac{1}{N} + \frac{1}{D_k^2}$$
当 $D_k = 3$、$N$ 比较大(比如 256)时,这个比值大约是 $\frac{1}{256} + \frac{1}{9} \approx 0.115$。也就是说,参数量降到了原来的约 11.5%,接近九分之一的水平。
计算量的比值也是类似的:
$$\frac{D_k \times D_k \times M \times D_f \times D_f + M \times N \times D_f \times D_f}{D_k \times D_k \times M \times N \times D_f \times D_f} = \frac{1}{N} + \frac{1}{D_k^2}$$
同样的,$3 \times 3$ 卷积下计算量也降到约 11.5%。这个节省是非常可观的。
我用一个表格把关键对比列出来,方便你直观感受:
| 对比项 | 标准卷积 | 深度可分离卷积 | 比值(3x3, N=256) |
|---|---|---|---|
| 参数量 | $D_k^2 \cdot M \cdot N$ | $D_k^2 \cdot M + M \cdot N$ | 约 11.5% |
| 计算量 | $D_k^2 \cdot M \cdot N \cdot D_f^2$ | $(D_k^2 \cdot M + M \cdot N) \cdot D_f^2$ | 约 11.5% |
| 通道交互 | 一步完成 | 分两步完成 | - |
| 空间滤波 | 跨通道同时进行 | 逐通道独立进行 | - |
注意:这里的比值是理论值,实际推理速度的提升不一定和计算量下降成正比,因为逐通道卷积的访存模式对硬件不够友好,后面会详细讲。
3. Xception 里的深度可分离卷积是怎么组织的
3.1 从 Inception 到 Xception 的演化逻辑
Xception 这个结构,名字本身就是 "Extreme Inception" 的缩写。它的核心观点是:Inception 模块里的多分支结构,本质上是在近似一个深度可分离卷积。
传统的 Inception 模块会把输入通道分成几组,每组做不同大小的卷积,然后拼接。Xception 的作者认为,如果把这个分组做到极致——每个通道一组——那就变成了先逐通道卷积、再逐点卷积的形式。所以 Xception 干脆把 Inception 模块替换成了深度可分离卷积的堆叠。
这个演化逻辑很重要,因为它解释了为什么深度可分离卷积不是凭空拍脑袋想出来的,而是从"分组卷积做到极致"这个思路自然推导出来的。理解这一点,你在设计自己的网络时就能更灵活地运用这个工具,而不是死记硬背。
3.2 Xception 的基本模块结构
Xception 的基本模块叫 SeparableConv 模块,它的典型结构是这样的:
- 输入经过 $1 \times 1$ 卷积(可选,用于调整通道数)
- 逐通道卷积($3 \times 3$ depthwise)
- 逐点卷积($1 \times 1$ pointwise)
- 残差连接(如果输入输出维度一致)
实际实现中,通常会加上 Batch Normalization 和 ReLU 激活。需要注意的是,Xception 里用的是"先逐点后逐通道"还是"先逐通道后逐点",不同实现有差异。原始论文里主要采用的是先逐通道卷积再逐点卷积的顺序,但有些工程实现会调整顺序以适配特定硬件。
我在实际项目里试过两种顺序,在 GPU 上差异不大,但在某些移动端推理框架上,先逐点后逐通道的访存效率会稍好一些。这个细节后面讲部署的时候再展开。
3.3 为什么 Xception 要用残差连接
Xception 模块普遍带有残差连接,这不是随便加的。深度可分离卷积虽然省参数,但它的表达能力相比标准卷积是有损失的。残差连接可以让信息绕过卷积层直接传递,缓解梯度消失,同时给网络一个"退路"——如果某一层的卷积没学到有用东西,至少恒等映射还能保住基本性能。
从另一个角度看,残差连接让网络可以堆得更深。Xception 有 36 个卷积层,如果没有残差连接,训练起来会非常困难。这一点和 ResNet 的思路是一致的。
我个人的体会是,在用深度可分离卷积搭网络时,残差连接几乎是标配。如果你去掉它,模型收敛会明显变慢,精度也会掉一截。这个结论我在几个不同数据集上都验证过,不是个例。
4. 参数量与计算量的手算实例
4.1 一个具体层的完整计算
光看公式可能还不够直观,我们拿一个具体的层来手算一遍。
假设输入特征图是 $56 \times 56 \times 256$,要输出 $56 \times 56 \times 512$,卷积核 $3 \times 3$。
标准卷积:
- 参数量:$3 \times 3 \times 256 \times 512 = 1,179,648$
- 计算量:$1,179,648 \times 56 \times 56 = 3,699,855,360$(约 37 亿次乘加)
深度可分离卷积:
- 逐通道卷积参数量:$3 \times 3 \times 256 = 2,304$
- 逐点卷积参数量:$1 \times 1 \times 256 \times 512 = 131,072$
- 总参数量:$2,304 + 131,072 = 133,376$
- 逐通道计算量:$2,304 \times 56 \times 56 = 7,225,344$
- 逐点计算量:$131,072 \times 56 \times 56 = 411,041,792$
- 总计算量:$7,225,344 + 411,041,792 = 418,267,136$(约 4.18 亿次乘加)
对比一下:参数量从 118 万降到 13.3 万,降到约 11.3%;计算量从 37 亿降到 4.18 亿,降到约 11.3%。和公式推导的比值完全吻合。
4.2 整个网络的账怎么算
单层省 89% 已经很夸张了,但整个网络堆起来,节省会更明显。因为深层网络的通道数大,标准卷积的平方级增长在深层尤其致命,而深度可分离卷积的参数量增长是线性的。
我做过一个粗略统计:一个 20 层左右的标准卷积网络,如果把所有 $3 \times 3$ 卷积替换成深度可分离卷积,参数量通常能降到原来的 10% 到 15%,计算量降到 10% 到 20%。具体数字取决于通道数分布和网络深度。
但这里要提醒一句:参数量少不等于推理快。这是很多人容易误解的地方。深度可分离卷积的逐通道部分计算密度低,在 GPU 这种擅长并行矩阵运算的硬件上,反而可能因为访存瓶颈跑不满算力。所以实际加速比往往低于理论计算量下降比。这个坑我在移动端部署时踩过,后面会详细说。
4.3 不同卷积核大小的影响
从比值公式 $\frac{1}{N} + \frac{1}{D_k^2}$ 可以看出,卷积核越大,深度可分离卷积的优势越明显。
- $D_k = 3$ 时,$\frac{1}{D_k^2} = \frac{1}{9} \approx 0.111$
- $D_k = 5$ 时,$\frac{1}{D_k^2} = \frac{1}{25} = 0.04$
- $D_k = 7$ 时,$\frac{1}{D_k^2} = \frac{1}{49} \approx 0.020$
也就是说,$5 \times 5$ 卷积用深度可分离形式,参数量比值可以低到约 4%(假设 $N$ 很大)。这也是为什么很多轻量级网络喜欢用大卷积核配深度可分离卷积——既扩大了感受野,又不会带来太大的参数负担。
不过实际中 $3 \times 3$ 仍然是主流,因为两个 $3 \times 3$ 堆叠的感受野等于一个 $5 \times 5$,但参数更少、非线性更强。这个取舍要看具体任务。
5. 实际落地时的性能陷阱
5.1 理论计算量和实际延迟的差距
这是我最想强调的一点。深度可分离卷积的理论计算量确实低,但在实际硬件上,延迟不一定按比例下降。
原因在于逐通道卷积的计算密度太低。标准卷积里,每个输出值需要 $D_k^2 \times M$ 次乘加,计算密度高,GPU 的矩阵运算单元能跑满。而逐通道卷积里,每个输出值只需要 $D_k^2$ 次乘加,大部分时间花在读取和写入内存上,计算单元反而闲着。
我实测过一个对比:在桌面级 GPU 上,把标准卷积换成深度可分离卷积,理论计算量降到 12%,但实际推理时间只降到 30% 到 40%。在移动端 CPU 上差距小一些,因为 CPU 本来就不是靠大规模并行取胜,访存瓶颈没那么突出。
所以如果你做轻量化是为了减少参数量(比如模型要存到手机里),深度可分离卷积非常有效。但如果是为了降低推理延迟,就要实际测一下,不能只看计算量。
5.2 通道数对硬件效率的影响
逐通道卷积的效率还和通道数强相关。通道数太少时,并行度不够,硬件利用率低;通道数太多时,访存压力大。一般来说,通道数在 64 到 512 之间时,逐通道卷积的效率相对较好。
Xception 的设计里,通道数普遍偏大(很多层是 728、1024 这种),部分原因就是为了让逐通道卷积有足够的并行度。如果你自己设计网络,把通道数压得太小(比如 32、16),深度可分离卷积的加速效果会打折扣。
5.3 内存布局与算子融合
在实际部署时,逐通道卷积和逐点卷积之间的内存布局很关键。如果两步之间要写回内存再读出来,访存开销会吃掉不少收益。好的推理框架会把这两步融合成一个算子,中间结果留在寄存器或共享内存里,避免来回搬运。
我在用某个移动端推理框架时发现,它默认没有融合这两个算子,手动开启融合后,单层延迟降了将近 20%。这个细节在文档里往往不会写,得自己看框架的算子实现或者 profiling 工具才能发现。
提示:部署深度可分离卷积时,优先选择支持 depthwise 算子融合的推理框架,并确认融合选项已开启。这是提升实际性能最直接的手段之一。
6. 和其他轻量化方案的横向对比
6.1 深度可分离卷积 vs 分组卷积
分组卷积是把输入通道分成 $g$ 组,每组独立做标准卷积。参数量是标准卷积的 $\frac{1}{g}$。深度可分离卷积可以看作分组卷积的极端情况($g = M$),再加上一个逐点卷积做通道融合。
两者的区别在于:分组卷积组内仍然做跨通道组合,而深度可分离卷积的逐通道部分完全不做跨通道组合,全靠后面的逐点卷积补回来。从参数量看,深度可分离卷积通常更省,但分组卷积的硬件效率往往更好,因为组内计算密度更高。
| 方案 | 参数量 | 通道交互 | 硬件友好度 |
|---|---|---|---|
| 标准卷积 | $D_k^2 MN$ | 完全 | 高 |
| 分组卷积 | $D_k^2 MN / g$ | 组内完全,组间无 | 中高 |
| 深度可分离卷积 | $D_k^2 M + MN$ | 逐点卷积阶段完全 | 中 |
6.2 深度可分离卷积 vs 模型剪枝
模型剪枝是在训练好的标准卷积网络上,去掉不重要的权重或通道。它和深度可分离卷积是正交的思路:剪枝是在已有结构上做减法,深度可分离卷积是从结构设计上就减少冗余。
实际项目中,两者可以叠加使用。我试过先在标准网络上做通道剪枝,再把剩余的标准卷积替换成深度可分离卷积,最终模型比单独用任何一种方案都小。但要注意,剪枝后的通道数往往不规整,替换成深度可分离卷积时可能需要重新调整通道数,否则硬件效率会很差。
6.3 什么时候不该用深度可分离卷积
不是所有场景都适合。如果你的算力充足(比如服务器端推理),精度是唯一指标,那标准卷积往往能给出更好的精度,没必要为了省参数牺牲性能。深度可分离卷积的精度损失在小模型上可能不明显,但在大模型上,尤其是需要精细特征区分的任务(比如细粒度分类),损失会比较明显。
另外,如果任务对延迟极其敏感,而硬件又是 GPU,深度可分离卷积的实际加速可能达不到预期,这时候可能需要考虑其他方案,比如用标准卷积但减少层数,或者用更激进的量化。
7. 自己动手实现一个深度可分离卷积层
7.1 用 PyTorch 实现
下面是一个可以直接用的 PyTorch 实现,包含逐通道卷积、逐点卷积、BN 和激活:
import torch import torch.nn as nn class DepthwiseSeparableConv(nn.Module): def __init__(self, in_channels, out_channels, kernel_size=3, stride=1, padding=1): super().__init__() # 逐通道卷积:groups=in_channels,每个通道独立卷积 self.depthwise = nn.Conv2d( in_channels, in_channels, kernel_size=kernel_size, stride=stride, padding=padding, groups=in_channels, bias=False ) # 逐点卷积:1x1 卷积做通道融合 self.pointwise = nn.Conv2d( in_channels, out_channels, kernel_size=1, stride=1, padding=0, bias=False ) self.bn = nn.BatchNorm2d(out_channels) self.relu = nn.ReLU(inplace=True) def forward(self, x): x = self.depthwise(x) x = self.pointwise(x) x = self.bn(x) x = self.relu(x) return x关键点在groups=in_channels这个参数。PyTorch 的Conv2d支持分组卷积,当groups等于输入通道数时,就变成了逐通道卷积。这个写法比手动循环每个通道要高效得多,底层会调用优化过的算子。
7.2 带残差连接的完整模块
如果要搭 Xception 风格的模块,需要加上残差连接:
class XceptionBlock(nn.Module): def __init__(self, in_channels, out_channels, stride=1): super().__init__() self.conv1 = DepthwiseSeparableConv(in_channels, out_channels, stride=stride) self.conv2 = DepthwiseSeparableConv(out_channels, out_channels) # 残差分支:如果维度不一致,用 1x1 卷积调整 self.skip = nn.Sequential() if stride != 1 or in_channels != out_channels: self.skip = nn.Sequential( nn.Conv2d(in_channels, out_channels, 1, stride=stride, bias=False), nn.BatchNorm2d(out_channels) ) def forward(self, x): residual = self.skip(x) out = self.conv1(x) out = self.conv2(out) return self.relu(out + residual)这里有个细节:残差分支的 stride 要和主分支对齐,否则相加时尺寸对不上。这是新手最容易犯的错误之一,报错信息往往不直观,得仔细检查每一层的输出尺寸。
7.3 验证参数量是否符合预期
写完实现后,建议打印一下参数量验证:
model = DepthwiseSeparableConv(256, 512, kernel_size=3) total_params = sum(p.numel() for p in model.parameters()) print(f"Total parameters: {total_params}") # 预期:3*3*256 + 256*512 = 2304 + 131072 = 133376如果打印出来是 133376,说明实现正确。如果数字对不上,多半是某个卷积的 bias 没关掉,或者 groups 参数设错了。这个验证步骤花不了几分钟,但能帮你快速定位实现问题。
8. 训练与调参的实战经验
8.1 学习率需要适当调整
深度可分离卷积的参数量少,梯度更新的幅度和标准卷积不一样。我自己的经验是,同样的网络结构,换成深度可分离卷积后,初始学习率可以稍微调大一点,或者用更温和的 warmup。因为参数少,模型对学习率没那么敏感,但收敛速度会快一些。
具体调多少,没有固定答案。我的做法是先沿用原来的学习率跑几个 epoch,看 loss 曲线。如果下降太慢,就调大 1.5 到 2 倍;如果震荡厉害,就调小。这个比盲目套用别人的配置靠谱。
8.2 权重初始化要注意
逐通道卷积的每个卷积核只处理一个通道,如果初始化不当,某些通道的输出可能一直很小,相当于被"饿死"了。用标准的 Kaiming 初始化通常没问题,但如果发现训练后期某些通道的权重几乎为零,可以考虑换用更均匀的初始化,或者加一点权重衰减来防止。
8.3 数据增强不能省
深度可分离卷积的表达能力比标准卷积弱,这意味着它更容易过拟合,也更依赖数据增强来提升泛化。我在一个小数据集上做过对比:不加数据增强时,深度可分离卷积的精度比标准卷积低 3 个百分点;加上随机裁剪、翻转、颜色抖动后,差距缩小到 1 个百分点以内。
所以如果你用深度可分离卷积,数据增强的投入回报比会更高。这一点值得在项目排期时就考虑进去。
9. 部署阶段的几个关键决策
9.1 推理框架的选择
不同推理框架对 depthwise 算子的优化程度差别很大。选框架时,重点看它有没有针对逐通道卷积做专门优化,比如算子融合、内存复用、特定硬件的指令加速。
我的建议是,在选型阶段就拿一个包含深度可分离卷积的小模型,在目标硬件上跑一遍 benchmark,对比几个候选框架的实际延迟。不要只看框架宣传的理论性能,实测数据才是真的。
9.2 量化对深度可分离卷积的影响
量化(比如 INT8)能进一步压缩模型、加速推理,但对深度可分离卷积要格外小心。逐通道卷积的每个通道只有一个卷积核,量化误差在通道间可能分布不均,导致某些通道的精度损失特别大。
我遇到过的情况是:标准卷积量化后精度掉 0.5%,深度可分离卷积量化后掉了 2%。解决办法是采用逐通道量化(per-channel quantization),给每个通道单独算量化参数,而不是整个层共用一个。这个选项在很多量化工具里都有,记得打开。
9.3 输入分辨率与通道数的权衡
深度可分离卷积的计算量对输入分辨率是线性的(乘 $D_f^2$),对通道数也是线性的。所以在算力受限时,降低输入分辨率和减少通道数都能省算力,但两者的影响不同。
降低分辨率会直接损失空间细节,对小目标检测影响大;减少通道数会损失特征表达能力,对细粒度分类影响大。具体怎么取舍,要看任务对空间细节和语义信息的依赖程度。我的经验是,分类任务可以适当降分辨率,检测和分割任务则要谨慎。
10. 一些容易被忽略的细节
10.1 逐通道卷积的 padding 问题
逐通道卷积的 padding 设置和标准卷积一样,但要注意 stride 大于 1 时,输出尺寸的计算。如果 padding 设得不对,特征图尺寸可能和残差分支对不上。我习惯在写模块时先把每一层的输出尺寸算清楚,或者用工具自动推导,避免运行时才发现维度错误。
10.2 BN 层的位置
深度可分离卷积后面接 BN,通常放在逐点卷积之后。但也有实现放在逐通道卷积之后、逐点卷积之前。两种做法我都试过,差异不大,但放在逐点卷积之后更常见,因为逐点卷积是最后一步通道融合,BN 在这里做归一化更合理。
10.3 激活函数的选择
ReLU 是默认选择,但在深度可分离卷积里,有些实现会用 ReLU6 或者 Swish。ReLU6 在量化时更友好,因为它的输出范围有界,量化误差更小。如果你的模型要量化部署,可以考虑把 ReLU 换成 ReLU6,精度损失很小,但量化后的稳定性会好很多。
10.4 不要盲目堆叠
深度可分离卷积虽然省参数,但堆太多层也会导致精度下降。我见过有人为了追求极致轻量,把网络堆到几十层深度可分离卷积,结果精度惨不忍睹。合理的做法是控制深度,配合残差连接和适当的通道数,在轻量和精度之间找平衡。
11. 我踩过的几个坑
第一个坑是以为参数量少就一定快。前面说过,逐通道卷积的访存瓶颈会让实际加速打折扣。我第一次做移动端部署时,看到参数量降了 90%,满心以为推理时间也会降 90%,结果实测只降了 40%。后来才明白计算密度和访存模式的影响。
第二个坑是量化时没开逐通道量化。模型量化后精度掉得厉害,排查了很久才发现是逐通道卷积的量化误差问题。开启逐通道量化后,精度恢复了大半。
第三个坑是残差分支的 stride 没对齐。这个错误在搭建 Xception 风格模块时特别容易犯,因为主分支经过两次卷积,残差分支只有一次,stride 设置不一致就会导致尺寸不匹配。解决办法是在残差分支里用和主分支总 stride 相同的卷积。
第四个坑是通道数设得太小。有一次为了压模型,把中间层通道数压到 32,结果逐通道卷积的并行度不够,推理速度反而比通道数 64 时更慢。这个反直觉的现象让我意识到,轻量化不是简单地减参数,还要考虑硬件特性。
12. 后续可以继续深挖的方向
深度可分离卷积本身已经比较成熟了,但围绕它的优化还在继续。一个方向是神经架构搜索,自动搜索逐通道卷积和逐点卷积的最佳组合方式,而不是人工设计。另一个方向是硬件感知的算子优化,针对特定芯片的访存特性定制逐通道卷积的实现。
还有一个我觉得很有意思的方向是混合使用标准卷积和深度可分离卷积。不是所有层都适合深度可分离,浅层特征提取用标准卷积可能更有效,深层用深度可分离卷积省参数。这种混合策略在实际项目中往往比全盘替换效果更好,值得试试。
我自己在最近的一个项目里就采用了这种混合方案:前几层用标准卷积保证特征提取质量,后面全部换成深度可分离卷积控制参数量。最终模型比全标准卷积小了 70%,精度只掉了 0.8%,比全深度可分离卷积的精度高了 1.5%。这个结果让我更倾向于"按需混合"而不是"一刀切"。
最后分享一个小技巧:在替换卷积类型时,不要一次性全换,而是逐层替换、逐层验证。这样能快速定位是哪一层的替换导致了精度下降,也方便做针对性的调整。全换完再排查,工作量会大很多。