首页 / 资讯中心 / 文章详情

可分离卷积与深度可分离卷积:原理、计算量对比与移动端部署实践

可分离卷积与深度可分离卷积:原理、计算量对比与移动端部署实践 ★ FEATURED ARTICLE
1. 从一次模型瘦身说起为什么卷积层是优化重点做过移动端或嵌入式端模型部署的朋友大概率都经历过这样的场景在服务器上跑得好好的模型一搬到手机或者边缘设备上推理速度直接掉到没法用内存占用也高得离谱。我最早接触这个问题是在一个图像分类的小项目上当时用的是一个标准的卷积网络参数量看着不算夸张但实际部署到资源受限的设备上单帧推理时间超过了预期好几倍。排查下来问题几乎全部集中在卷积层上。标准卷积的计算量和参数量在深层网络里是成倍累积的。一个普通的3x3卷积输入通道256、输出通道256光这一层的参数量就是 3×3×256×256 ≈ 59万如果再堆几十层参数量轻松上千万。对于算力和内存都有限的设备来说这基本是不可接受的。于是就有了两条非常经典的优化路线可分离卷积和深度可分离卷积。这两个概念经常被混着讲很多人第一次听到会以为是同一个东西其实它们有明确的区别适用的场景也不完全一样。这篇文章我就把这两个东西从头拆一遍讲清楚它们各自的计算方式、参数量对比、为什么能省算力、实际写代码时怎么落地以及我在实操中踩过的那些坑。如果你正在做模型轻量化、移动端部署或者只是单纯想搞明白为什么深度可分离卷积能大幅减少计算量这篇内容应该能帮你把这块知识补完整。我会尽量用生活化的类比加上具体的数字计算让即使没怎么接触过卷积优化的人也能看懂。2. 先把标准卷积的账算清楚2.1 标准卷积到底在做什么在讲可分离卷积之前必须先把标准卷积的计算过程捋清楚不然后面所有的对比都没有参照物。假设输入特征图是H × W × C_in卷积核大小是K × K输出通道数是C_out。标准卷积的做法是每一个输出通道都用一个K × K × C_in的卷积核在输入特征图上滑动把对应位置的K × K × C_in个元素和卷积核逐元素相乘再求和得到一个输出值。这里有个关键点很多人一开始会忽略标准卷积的每一个卷积核都是跨所有输入通道的。也就是说一个卷积核的深度等于输入通道数。它一次性把输入的所有通道信息融合到一起输出一个通道的值。用生活化的类比来说标准卷积就像是一个厨师做一道菜把所有食材输入通道全部倒进锅里一起炒最后炒出一盘菜一个输出通道。要做C_out盘不同的菜就得有C_out个厨师每个厨师都把所有食材重新处理一遍。2.2 参数量和计算量的公式标准卷积的参数量公式很直接Params K × K × C_in × C_out计算量这里用乘加次数 MAC 来衡量是MACs K × K × C_in × C_out × H_out × W_out其中H_out、W_out是输出特征图的高和宽。我拿一个具体的例子算一遍方便后面做对比。假设输入是224 × 224 × 3第一层卷积用3 × 3核输出通道 64参数量3 × 3 × 3 × 64 1728计算量3 × 3 × 3 × 64 × 224 × 224 ≈ 8.7 亿次乘加再看一个深层的情况输入14 × 14 × 512卷积3 × 3输出通道还是 512参数量3 × 3 × 512 × 512 ≈ 236 万计算量3 × 3 × 512 × 512 × 14 × 14 ≈ 4.6 亿次乘加可以看到光是这一层就贡献了几百万参数和几亿次计算。深层网络里这样的层有几十个累积起来就是天文数字。这就是为什么卷积层的优化如此重要。提示算计算量的时候一定要注意是 MACs乘加还是 FLOPs浮点运算。一次乘加通常算作 2 次 FLOPs很多论文里两个指标混用对比的时候要统一口径否则数字会差一倍。3. 可分离卷积把空间和通道拆开处理3.1 可分离卷积的核心思想可分离卷积Separable Convolution这个词其实有点笼统它最原始的含义是指空间可分离卷积也就是把一个二维的卷积核拆成两个一维卷积核的组合。举个最经典的例子Sobel 算子或者高斯核一个K × K的卷积核如果它是可分离的就能写成两个向量的外积一个K × 1的列向量和一个1 × K的行向量。这样原本一次K × K的卷积可以拆成先做一次K × 1的卷积再做一次1 × K的卷积。这个思路的数学基础是卷积的可分离性。不是所有卷积核都能这样拆只有满足特定条件的核比如秩为1的矩阵才行。高斯核、均值核这类对称的核通常可以拆但任意学习出来的卷积核不一定满足这个条件。3.2 空间可分离卷积省了多少还是用数字说话。假设一个K × K的卷积输入输出通道都是 1先不考虑通道输入特征图H × W标准卷积计算量K × K × H × W可分离卷积计算量先做K × 1得到H × W再做1 × K得到H × W总共K × H × W K × H × W 2K × H × W两者的比值是(K × K) / (2K) K / 2也就是说3 × 3的核能省到原来的 1/1.55 × 5的核能省到 1/2.57 × 7能省到 1/3.5。核越大省得越多。但这里有个现实问题空间可分离卷积在实际的深度学习网络里用得并不多。原因是网络里的卷积核是通过反向传播学出来的它们的值不受约束很难保证学出来的核恰好是可分离的。强行做可分离分解会损失模型的表达能力精度通常会掉。所以现在大家说“可分离卷积”很多时候其实指的是深度可分离卷积这也是下一节要重点讲的内容。但作为概念空间可分离卷积是理解后面内容的基础不能跳过。3.3 一个容易混淆的点我见过不少人把“可分离卷积”和“深度可分离卷积”当成同义词严格来说这是不对的。可分离卷积是一个更大的范畴深度可分离卷积是其中一种具体实现。深度可分离卷积拆的不是空间维度而是通道维度和空间维度。这个区别很关键因为两者的计算量节省比例、适用场景都不一样。下面进入正题。4. 深度可分离卷积真正在用的轻量化利器4.1 深度可分离卷积的两步走深度可分离卷积Depthwise Separable Convolution把标准卷积拆成两个独立的步骤逐通道卷积Depthwise Convolution对每一个输入通道单独用一个K × K的卷积核做卷积输出通道数等于输入通道数。这一步只做空间维度的融合不跨通道。逐点卷积Pointwise Convolution用1 × 1的卷积核把逐通道卷积的输出在通道维度上做线性组合输出指定的通道数。这一步只做通道维度的融合不涉及空间。还是用厨师类比逐通道卷积相当于每个食材单独处理一遍每个通道一个厨师只负责自己那口锅逐点卷积相当于把处理好的所有食材按配方重新调配成最终的几盘菜。分工明确各司其职。4.2 参数量和计算量的详细推导这部分是重点我把每一步都写清楚。逐通道卷积的参数量每个输入通道对应一个K × K的核共C_in个所以是K × K × C_in。逐点卷积的参数量1 × 1 × C_in × C_out也就是C_in × C_out。深度可分离卷积总参数量Params_dw K × K × C_in C_in × C_out对比标准卷积的K × K × C_in × C_out两者的比值是(K × K × C_in C_in × C_out) / (K × K × C_in × C_out) 1/C_out 1/(K × K)当C_out比较大比如 256、512时1/C_out可以忽略比值约等于1/(K × K)。也就是说3 × 3的深度可分离卷积参数量大约是标准卷积的 1/9。计算量同样推导。逐通道卷积的 MACs 是K × K × C_in × H_out × W_out逐点卷积的 MACs 是C_in × C_out × H_out × W_out加起来MACs_dw K × K × C_in × H_out × W_out C_in × C_out × H_out × W_out和标准卷积K × K × C_in × C_out × H_out × W_out的比值同样是1/C_out 1/(K × K)还是那个结论3 × 3核下大约是 1/9 的计算量。4.3 用具体数字感受一下差距拿前面那个深层卷积的例子输入14 × 14 × 5123 × 3核输出 512 通道。标准卷积参数量3 × 3 × 512 × 512 ≈ 236 万计算量3 × 3 × 512 × 512 × 14 × 14 ≈ 4.6 亿 MACs深度可分离卷积逐通道参数量3 × 3 × 512 4608逐点参数量512 × 512 26.2 万总参数量约 26.7 万是标准卷积的约 1/8.8逐通道计算量3 × 3 × 512 × 14 × 14 ≈ 90 万 MACs逐点计算量512 × 512 × 14 × 14 ≈ 5140 万 MACs总计算量约 5230 万 MACs是标准卷积的约 1/8.8这个差距在深层网络里会被放大到非常可观的程度。MobileNet 系列就是靠这个把参数量和计算量压到能在手机上实时运行的。注意虽然理论比值是 1/9但实际部署时因为逐点卷积是1 × 1卷积对内存访问模式不友好硬件利用率可能不如标准卷积高所以实测加速比往往达不到 9 倍通常在 3 到 6 倍之间。这一点在做性能预估时一定要留出余量。5. 代码落地从零实现一遍5.1 用 PyTorch 手写深度可分离卷积光看公式容易飘直接上代码最实在。下面是一个用 PyTorch 实现的深度可分离卷积模块我把它拆成逐通道和逐点两步方便对照理解。import torch import torch.nn as nn class DepthwiseSeparableConv(nn.Module): def __init__(self, in_channels, out_channels, kernel_size3, stride1, padding1): super().__init__() # 逐通道卷积groups 等于输入通道数每个通道独立卷积 self.depthwise nn.Conv2d( in_channels, in_channels, kernel_sizekernel_size, stridestride, paddingpadding, groupsin_channels, biasFalse ) # 逐点卷积1x1 卷积负责通道融合 self.pointwise nn.Conv2d( in_channels, out_channels, kernel_size1, stride1, padding0, biasFalse ) self.bn nn.BatchNorm2d(out_channels) self.relu nn.ReLU(inplaceTrue) def forward(self, x): x self.depthwise(x) x self.pointwise(x) x self.bn(x) x self.relu(x) return x这里最关键的一行是groupsin_channels。PyTorch 的Conv2d里groups参数控制分组卷积的组数。当groups等于输入通道数时就变成了逐通道卷积每个通道用自己的卷积核通道之间不交互。这是实现深度可分离卷积的核心技巧很多人第一次看会反应不过来。5.2 参数量验证写个脚本验证一下参数量确保理论和代码对得上def count_params(model): return sum(p.numel() for p in model.parameters() if p.requires_grad) # 标准卷积 standard nn.Conv2d(512, 512, kernel_size3, padding1, biasFalse) # 深度可分离卷积 dw_sep DepthwiseSeparableConv(512, 512, kernel_size3) print(标准卷积参数量:, count_params(standard)) print(深度可分离参数量:, count_params(dw_sep))实测下来标准卷积是 2359296深度可分离是 267776含 BatchNorm 参数比值约 8.8和理论推导完全吻合。这个验证步骤我强烈建议每个人都做一遍比看十遍公式都管用。5.3 一个常见的实现误区很多人第一次写的时候会把逐通道卷积和逐点卷积的顺序搞反或者忘了逐点卷积。顺序其实无所谓先逐点后逐通道也能work但主流实现都是先逐通道再逐点因为逐通道卷积输出的通道数少等于输入通道数先做它能让后续逐点卷积的输入更小整体更省。另一个误区是给逐通道卷积加 bias。逐通道卷积通常不加 bias因为后面跟着 BatchNormbias 会被 BN 的均值减掉加了也是浪费参数。逐点卷积同理。这个细节在复现论文时经常被忽略导致参数量和论文对不上。6. 常见问题与排查技巧实录6.1 精度掉了怎么办这是问得最多的问题。深度可分离卷积因为把通道融合和空间融合拆开了表达能力确实比标准卷积弱一些直接替换精度通常会掉 1 到 3 个百分点。我的处理经验是分三步走先做结构搜索不是所有层都适合替换。通常浅层通道数少保留标准卷积深层通道数多替换成深度可分离这样精度损失最小。MobileNet 就是这么做的第一层是标准卷积。加宽网络深度可分离卷积省下来的计算量可以拿出一部分用来增加通道数。MobileNet 的宽度因子width multiplier就是干这个的用 1.0 的宽度可能精度不够换成 1.2 或 1.4 往往能补回来。配合训练技巧知识蒸馏、更长的训练轮数、更强的数据增强都能帮助恢复精度。我实测下来知识蒸馏对深度可分离网络的精度恢复效果最明显。6.2 实测加速比不达预期前面提过理论 9 倍实测可能只有 3 到 6 倍。原因主要有两个一是逐点卷积是1 × 1卷积计算密度低GPU 或 NPU 的利用率上不去。标准卷积3 × 3有更多的数据复用硬件更友好。二是内存访问瓶颈。深度可分离卷积的层数变多了一层变两层中间特征图的读写次数增加在内存带宽受限的设备上这部分开销会吃掉一部分计算节省。排查的时候可以用 profiler 工具看每一层的时间占比如果逐点卷积占了大部分时间说明是计算密度问题如果时间花在内存搬运上那就是带宽瓶颈。针对性地做算子融合或者调整数据布局能改善一部分。6.3 常见问题速查表问题现象可能原因排查方向解决思路参数量和论文对不上逐通道卷积加了 bias检查 Conv2d 的 bias 参数逐通道和逐点卷积都设 biasFalse精度掉得厉害全网络无差别替换逐层对比精度贡献浅层保留标准卷积深层替换实测加速比低逐点卷积硬件利用率低用 profiler 看层耗时算子融合或换用对 1x1 友好的后端训练不收敛学习率没调整看 loss 曲线深度可分离网络通常需要更小的学习率输出通道数不对groups 参数设错检查 groups 是否等于 in_channelsgroups 必须等于输入通道数6.4 几个容易被忽略的实操心得第一个心得逐点卷积的通道数扩展比例很关键。MobileNetV2 里引入了倒残差结构先升维再逐通道卷积再降维这个升维比例expansion ratio通常取 6。我试过 4 和 86 在精度和速度之间平衡最好但这个值跟具体任务相关需要自己调。第二个心得BatchNorm 的位置很重要。逐通道卷积后面接 BN 再接 ReLU和逐点卷积后面接 BN 再接 ReLU效果不一样。主流做法是两步都接 BN 和 ReLU但有些实现只在最后接一次精度会差一些。这个细节在论文里经常一笔带过实际影响不小。第三个心得部署时记得做算子融合。训练时逐通道卷积、BN、ReLU 是分开的推理时可以把 BN 的参数融合进卷积核ReLU 融合进激活这样能减少内存访问实测能再快 10% 到 20%。大部分推理框架都支持这个优化但需要手动开启。7. 两种卷积的选型建议7.1 什么场景用哪种把选型逻辑整理成一张表方便对照场景推荐方案理由服务器端算力充足标准卷积精度优先算力不是瓶颈移动端实时推理深度可分离卷积算力和内存都受限省算力是刚需嵌入式极低功耗深度可分离 宽度缩减进一步压缩牺牲部分精度换功耗精度敏感任务标准卷积为主深层替换浅层保精度深层省算力大核卷积5x5以上空间可分离或深度可分离核越大可分离的收益越明显7.2 一个实用的混合策略实际项目里我很少全网络用一种卷积通常是混合的。具体做法是先用标准卷积搭一个基线测出精度和速度然后逐层替换成深度可分离每替换一层测一次精度和速度画出精度-速度曲线最后在曲线上找一个满足速度要求、精度损失最小的点。这个过程听起来麻烦但用脚本自动化之后跑一轮也就几个小时。比起拍脑袋决定数据驱动的选型靠谱得多。我做过的一个项目里最终方案是前 3 层标准卷积、中间 20 层深度可分离、最后 2 层标准卷积精度只掉了 0.8 个点速度提升了 4 倍多。7.3 关于可分离卷积的一个补充回到最开始说的空间可分离卷积虽然它在学习型网络里用得少但在一些特定场景下还是有价值的。比如手工设计的滤波器、传统的图像处理算法或者一些对卷积核有约束的网络结构里空间可分离能进一步省算力。如果你做的不是端到端的深度学习而是传统图像处理那空间可分离卷积值得认真考虑。我在一个图像预处理的项目里用过空间可分离的高斯滤波把7 × 7的核拆成两个一维核在 CPU 上跑速度提升了将近 3 倍效果和标准卷积完全一致。所以不要因为深度学习里用得少就完全忽略它工具没有好坏关键看场景。最后分享一个我在调深度可分离网络时的小技巧训练初期先用标准卷积预热几个 epoch再切换到深度可分离继续训练收敛速度和最终精度都比从头用深度可分离要好。这个技巧在一些轻量化论文里也有提到原理是标准卷积在初期能更快地学到有效的特征表示给后续的深度可分离层一个更好的起点。
阅读完成 · 觉得有帮助?
咨询建站