资讯动态

1×1与3×3卷积的“两步走”设计:从瓶颈结构到深度可分离卷积

发布时间:2026/10/16 20:28:10 来源:尧图企业网站定制
做卷积网络的人十有八九都用过 1×1 和 3×3 这两种卷积核但真正把它们放在一起推敲过设计逻辑的其实不多。很多人知道 1×1 能降维、3×3 能提取空间特征却说不清楚为什么成熟的网络里总是“1×1 → 3×3 → 1×1”这种结构反复出现。这篇文章想把这件事彻底讲透1×1 和 3×3 各自在做什么把它们拆成“两步走”到底图什么以及这个套路在工程落地时有哪些必须注意的细节。内容会比较硬核但我会尽量把公式、代码和直觉都放在一起让刚接触卷积的新手能跟上也让有经验的读者能从中获得一些新视角。如果你正在设计自己的网络结构、做模型压缩或者只是想搞清楚经典网络为什么长这样这篇文章应该对你有用。1. 先把两个基础算子彻底搞清楚想理解“两步走”哲学第一步不是看它们怎么组合而是把 1×1 和 3×3 各自的本职工作掰开揉碎。1.1 1×1卷积的真正作用通道维度的“信息整理”很多人把 1×1 卷积简单理解成“降维工具”这个说法不算错但特别容易误导。1×1 卷积的卷积核空间尺寸只有 1×1意味着它在空间上什么都不看只是在每个像素位置上把输入的所有通道做一次线性加权求和。它的作用域是通道维不是空间维。举个例子输入一张特征图尺寸是 H×W×C_in经过一个 1×1 卷积输出为 H×W×C_out本质上就是把原来 C_in 个通道的值通过一组权重重新组合成 C_out 个通道。这个过程有点像你在整理一堆表格每一列的数值是原始多个字段的加权混合字段数量可以变多、也可以变少但每行对应的空间位置没有变。所以 1×1 卷积至少能干三件事调整通道数既能压缩降维也能扩张升维。跨通道信息融合让不同通道之间先发生交互。控制整体计算量通过先降维再计算把后续操作的运算量压下来。这三件事里第一件最直观第二件常常被忽略第三件则是“两步走”哲学的立足点。我早期做模型压缩时就犯过一个典型错误以为 1×1 降维只是省内存后来才发现它更大的价值是“让后续的空间卷积在一个更经济的信息空间里运行”。1.2 3×3卷积空间特征与参数效率的甜点3×3 卷积恰恰相反它关注的是局部空间关系。每个输出像素由输入特征图对应位置周围 3×3 邻域的值加权得到因此它能捕捉边缘、纹理、局部模式这类空间信息。为什么业界最终普遍选 3×3 而不是 5×5 或 7×7一个重要原因是参数效率和感受野的可叠加性。两层 3×3 卷积堆叠起来的感受野等价于一层 5×5 卷积但参数更少两层 3×3 的参数量是 2×918 个乘加系数忽略通道数一层 5×5 是 25 个。同时两层之间还可以插入非线性激活表达能力反而更强。同样的道理三层 3×3 等价于一层 7×7 的感受野参数依旧更少。但 3×3 也有代价当输入通道数和输出通道数都很大时它的计算量会爆炸式增长。我习惯用一个口语化的类比3×3 卷积是空间维度的“深度侦察”1×1 卷积是通道维度的“资源调度”。真正高效的网络应该是两项工作分工配合而不是让一次卷积同时承担两种职责否则计算开销很快就会失控。2. “两步走”哲学拆开、压缩、再融合把 1×1 和 3×3 放到一个块里就是所谓“两步走”的基本形态。简单说就是把“空间特征提取”和“通道信息变换”拆成两个独立步骤而不是混在一次卷积里。这个思路听起来简单背后的道理却很深。2.1 为什么要把一步拆成两步三个理由第一个理由是计算量。假设输入输出通道都是 C直接用一层 3×3 卷积处理计算量是 C×C×9×H×W。如果先用 1×1 降到 C/4再在低维空间做 3×3最后再用 1×1 升回来计算量会大幅下降。这个数学账算下来非常直观后面我会专门推导。第二个理由是非线性。原本一次 3×3 卷积如果后面只跟一次激活信息变换是非线性的程度有限。拆成“1×1 → 3×3 → 1×1”以后中间至少有两个位置可以插入激活函数和归一化网络对这个局部特征的表达能力会更强。每一步的线性变换之后都跟着一次非线性特征空间的弯曲能力自然就上来了。第三个理由是信息流控制。直接做一次大卷积信息流的路径是完全固定的结果好坏很难干预。拆成两步以后你可以在中间主动决定“保留多少信息、丢弃多少信息”这就是所谓信息瓶颈设计。好比写文章先列提纲再分段落展开而不是一口气把所有细节堆在同一段里。2.2 用计算量公式看清收益这里强烈建议把公式自己推导一遍推导完之后你会有种“原来如此”的感觉。标准 3×3 卷积输入通道 C_in输出通道 C_out特征图空间尺寸 H×W计算量大致为FLOPs_standard H×W × C_in × C_out × 3 × 3bottleneck 结构先 1×1 降到 C_mid再 3×3 保持 C_mid最后 1×1 升到 C_outFLOPs_bottleneck H×W × (C_in × C_mid C_mid × 9 × C_mid C_mid × C_out)取个常见例子C_in C_out 256C_mid 64。标准卷积H×W × 256 × 256 × 9 H×W × 589824bottleneckH×W × (256×64 64×9×64 64×256) H×W × (16384 36864 16384) H×W × 69632两者相比bottleneck 的计算量大约是原来的 1/8.5。这就是为什么很多经典残差网络敢把网络做得很深因为每个 block 的实际计算量并不像你想的那么夸张。如果把“两步走”换成“深度可分离卷积”收益更夸张。深度可分离卷积把标准 3×3 拆成两步第一步是 depthwise 卷积每个输入通道单独做 3×3 空间卷积第二步是 pointwise 卷积也就是 1×1 卷积用来融合通道。计算量FLOPs_depthwise H×W × C_in × 9FLOPs_pointwise H×W × C_in × C_out总计算量大约只有标准卷积的 1/C_out 量级。当然这个对比是理论上的实际硬件上 depthwise 卷积的访存效率不一定好但这个数学结论已经足够说明“拆分”的威力。2.3 顺序问题1×1 前后放到底有没有区别既然“两步走”有先后顺序一个很自然的问题就来了1×1 应该放在 3×3 前面还是后面先说结论感受野不受顺序影响因为 1×1 不改变空间信息。但计算量和信息流会受顺序影响尤其是输入输出通道数不对称的时候。设输入通道 C_in输出通道 C_out中间通道 C_mid。方案 A1×1 先降维再做 3×3 升到 C_out。FLOPs_A H×W × (C_in × C_mid C_mid × 9 × C_out)方案 B3×3 先升到 C_mid再做 1×1 输出 C_out。FLOPs_B H×W × (C_in × 9 × C_mid C_mid × C_out)当 C_in C_out 时两个式子数值完全一样。但 C_in 不等于 C_out 时差别就出来了。比如 C_in 128C_out 256C_mid 64方案 A128×64 64×9×256 8192 147456 155648方案 B128×9×64 64×256 73728 16384 90112方案 B 足足少了四成计算量。为什么因为方案 B 把高计算量的 3×3 放在低维空间之前但它的总通道规模实际上更小。不过这个结论不能机械套用。如果 C_mid 表示的是瓶颈宽度你还需要考虑信息流的方向。先压缩再做空间卷积会让空间卷积只能看到压缩后的通道信息如果压缩比例太大细节信息很容易丢。先做空间卷积再做压缩理论上空间卷积看到的通道更完整但中间通道数会变多计算量也随之上涨。实际设计时要同时看计算预算和信息保留程度没有绝对最优。3. 从理论到代码手工实现一个“两步走”卷积块理论讲再多最后都要落到代码上。我自己最常用的框架是 PyTorch下面两个卷积块基本涵盖了“两步走”的两条主线残差瓶颈块和深度可分离卷积块。3.1 基础 Bottleneck 实现一个标准的 bottleneck 是“1×1 降维 → 3×3 空间提取 → 1×1 升维”import torch import torch.nn as nn class Bottleneck(nn.Module): def __init__(self, in_channels, out_channels, hidden_channelsNone): super().__init__() if hidden_channels is None: hidden_channels out_channels // 4 self.conv1 nn.Conv2d(in_channels, hidden_channels, kernel_size1, biasFalse) self.bn1 nn.BatchNorm2d(hidden_channels) self.conv2 nn.Conv2d(hidden_channels, hidden_channels, kernel_size3, padding1, biasFalse) self.bn2 nn.BatchNorm2d(hidden_channels) self.conv3 nn.Conv2d(hidden_channels, out_channels, kernel_size1, biasFalse) self.bn3 nn.BatchNorm2d(out_channels) self.relu nn.ReLU(inplaceTrue) def forward(self, x): identity x out self.conv1(x) out self.bn1(out) out self.relu(out) out self.conv2(out) out self.bn2(out) out self.relu(out) out self.conv3(out) out self.bn3(out) if hasattr(self, downsample): identity self.downsample(x) out identity out self.relu(out) return out这段代码里有几个细节值得单独强调第一卷积层的 bias 我设置成了 False。因为后面紧跟 BatchNormBN 层自带可学习的偏置项卷积层的 bias 在这里是冗余的留着反而增加参数。第二padding 必须设成 1这样才能保证 3×3 卷积前后特征图空间尺寸不变让后面的残差连接可以直接相加。第三hidden_channels 默认取 out_channels 的 1/4这是实践里比较稳妥的比例。如果你把 hidden_channels 设得太小比如 1/16计算量更低但信息瓶颈会明显影响精度如果设得太大计算节省又不够明显。3.2 深度可分离卷积块实现MobileNet 系列使用的核心结构是“3×3 depthwise 1×1 pointwise”也就是把 3×3 空间卷积和 1×1 通道卷积拆成两步class DepthwiseSeparableConv(nn.Module): def __init__(self, in_channels, out_channels, stride1): super().__init__() self.depthwise nn.Conv2d(in_channels, in_channels, kernel_size3, stridestride, padding1, groupsin_channels, biasFalse) self.pointwise nn.Conv2d(in_channels, out_channels, kernel_size1, biasFalse) self.bn1 nn.BatchNorm2d(in_channels) self.bn2 nn.BatchNorm2d(out_channels) self.relu nn.ReLU(inplaceTrue) def forward(self, x): out self.depthwise(x) out self.bn1(out) out self.relu(out) out self.pointwise(out) out self.bn2(out) out self.relu(out) return outgroupsin_channels 是 depthwise 卷积的关键参数它让每个输入通道只和自己的 3×3 邻域计算不和其他通道发生交互。通道之间的交互全部交给后面的 1×1 卷积来做。这里有个容易混乱的地方depthwise 卷积的输出通道数和输入通道数是一样的所以它本身无法改变通道数真正改变通道数的只有后面的 pointwise。3.3 做一个 AB 实验的思路很多人知道“两步走”理论后想自己验证一下顺序的影响。我建议这样设计实验固定同样的 C_in、C_mid、C_out分别实现“1×1 → 3×3”和“3×3 → 1×1”两种块在同一个数据集上做对比。实验时要注意几个变量控制两块的总通道数保持一致。确保 3×3 卷积的 padding 一致保持空间尺寸稳定。用相同的初始化种子、优化器、学习率调度。至少训练相同 epoch或者采用早停避免因为训练时长不同影响比较。我自己做这种对比时习惯把每个块封装成同样的接口用字典配置不同结构写起来很省事block_config { first_1x1_then_3x3: True, hidden_channels: 64, }最终结果通常不是某一方绝对胜出而是取决于你的数据集和任务特点。如果任务主要依赖空间细节先 3×3 再 1×1 往往更稳如果任务对通道压缩不敏感先 1×1 再 3×3 的性价比更高。要我说别指望一个结论吃遍所有场景老老实实跑一遍对比实验记录训练曲线和最终指标比任何人的经验都靠谱。4. 这里面的坑我几乎全踩过“两步走”结构看着简单实际落地时坑非常多。以下这些是我在真实项目里反复踩过、又花时间填上的问题。4.1 压缩过狠信息瓶颈的真实代价瓶颈设计的核心是“合理的通道压缩比”但这个“合理”很难拍脑袋。我第一次在某个图像识别任务里把 bottleneck 的中间通道从 1/4 降到 1/8 时计算量倒是实打实降了但准确率掉了将近两个点。原因很简单信息瓶颈太小空间卷积在压缩后的通道空间里根本找不到足够的特征组合。经验做法是先保持一个较宽的瓶颈训练一个基线模型观察中间特征图的通道能量分布。如果发现很多中间通道激活值非常稀疏再逐步压缩。千万别一开始就追求极致压缩。4.2 BN、ReLU 的顺序和位置“两步走”块里有两个位置可以插 BN 和 ReLU卷积之后、残差相加之前。不同顺序会造成明显差异。我自己用过两种主流排布Conv → BN → ReLU经典 post-activation实现简单收敛也稳定。BN → ReLU → Convpre-activation激活放在卷积之前残差路径更干净某些任务上收敛更快、准确率更高。关键教训是保证同一个网络里所有 block 的排布一致不要混用。我见过有人前面用 pre-activation后面换成 post-activation训练时 loss 直接飘了。4.3 分组卷积、空洞卷积与 1×1 搭配时容易忽视的细节如果你在“两步走”中间用分组卷积比如把 3×3 改成 group32 的组卷积那 1×1 卷积的通道数必须能被组数整除。这个细节摆弄过 ResNeXt 风格结构的人应该深有体会channel 数设成奇数后组卷积直接报错。空洞卷积也是同样的道理。3×3 空洞卷积的 dilation2 时感受野变成 5×5但有些新手会忘记同时调整 padding导致特征图尺寸对不上残差相加直接崩。正确做法是保持 paddingdilation这样才能维持空间分辨率。4.4 感受野的幻觉你以为 3×3 看到了局部其实问题在通道还有一个很容易被忽略的坑深度可分离卷积虽然计算量低但 depthwise 卷积是逐通道进行的信息密度很低。很多人在小模型上直接套 MobileNet 结构结果精度上不去误以为是模型容量不够其实真正的问题是通道数太少导致每个通道携带的信息过于碎片化。这种情况下我一般会先增加 pointwise 卷积的输出通道数观察精度回升情况再反向评估是空间卷积能力不足还是通道融合不够。这个过程有点像排查流水线先确认瓶颈出在哪个环节再针对性扩展。下面这个速查表基本覆盖了我遇到的大多数问题问题现象可能原因排查建议训练 loss 不降BN 位置不一致、激活放在残差相加前统一块内顺序检查前向代码推理时尺寸对不上空洞卷积 padding 设置错误padding 设为 dilation 值组卷积报错通道数无法被组数整除调整中间通道为组的整数倍精度低但计算量省瓶颈压缩过狠先放宽中间通道逐步压缩小模型效果差depthwise 信息密度不足增大 pointwise 输出通道数4.5 不要忽略硬件特性“两步走”在理论上的计算节省到了真实硬件上未必能等额兑现。原因是 1×1 卷积本质上是矩阵乘对 GPU 和 NPU 非常友好但 depthwise 卷积的访存模式是逐通道的计算密度低内存带宽往往成为瓶颈。我在手机上部署模型时深度可分离卷积的实际耗时常比理论计算量高得多原因就在这里。如果你的目标是端侧部署建议先用 profiling 工具测一下各类算子实际耗时再决定是走 bottleneck 还是深度可分离结构。别看纸面 FLOPs 好看硬件不认识 FLOPs它只认实际执行时间。5. 延伸思考两步走如何演化成现代架构的通用语法“两步走”不是一个孤立技巧它已经演变成了现代卷积架构的通用语法。看懂这个演化过程你才有能力设计自己的结构。5.1 Inception 的多路分支把两步走变成并行两路Inception 家族最早大规模使用 1×1 卷积但它的思路不是“两步走”而是“多路并联”。它把输入特征图同时送到 1×1、3×3、5×5 和池化分支再用 1×1 对所有分支的通道做整合。这里 1×1 的主要职责是控制每路的通道数避免多路拼接后通道爆炸。虽然不再是严格的两步但每一步内部的“先 1×1 降维再空间卷积”逻辑没有变。这种结构的哲学是与其选择一种固定的空间感受野不如让网络自己决定每条路径的权重。5.2 从 bottleneck 到 inverted residual方向反转的背后逻辑经典 bottleneck 是“先降维、再空间、再升维”但 MobileNetV2 的 inverted residual 恰好反过来先升维、再做 depthwise 空间卷积、最后降维。把这个方向反转的底层逻辑很有意思。普通卷积中3×3 卷积本身就有很强的跨通道融合能力所以可以先把通道压缩减少后续计算量。而 depthwise 卷积没有跨通道能力如果先压缩通道空间特征提取的信息来源就太少了所以要先升维让每个通道有足够的表达空间再逐通道提取空间特征最后用 1×1 融合并压缩回目标通道。这就是“两步走”思想在不同算子特性下的自适应变化不是一成不变的教条。5.3 重参数化把多步融合回一步还有一类思路走的是“两步走”的反方向代表就是重参数化。训练时构造多分支结构比如让一个块同时包含 1×1、3×3 和恒等映射支路训练完成后再把多个卷积核融合成一个 3×3 卷积推理时只有单分支。这个过程相当于把“两步走”甚至“三步走”压缩回“一步走”但训练时的多步结构让网络获得了更强的优化路径推理时又享受单步结构的低延迟。从这里也能看出卷积设计的核心变量永远是“信息流路径”和“计算资源分配”具体几步不重要重要的是每一步承担了什么职责。6. 一些我个人的实操心得说实话我刚接触卷积结构时也走过一段弯路总喜欢把网络搭得很花哨各种分支和模块堆上去效果反而不如一个老老实实的 bottleneck 堆叠。后来做模型压缩被逼着看每一步的计算量和信息流才真正理解了 1×1 和 3×3 配合的本质。如果现在让我给新手一个最务实的建议我会说先别急着追求新结构找个经典 bottleneck把每一层的输入输出通道数、特征图尺寸、FLOPs、感受野全部手算一遍再在代码里实际替换不同顺序、不同压缩比跑几组实验。等你真正感受到“两步走”的计算收益和精度代价你才算入门了。最后分享一个我最近在用的小技巧设计新模块时我会先画一张信息流表格横向是每个算子的输入输出通道数纵向是整体路径任何一步如果找不到圆得过去的理由就删掉。这个习惯帮我砍掉了不少无效模块也让模型结构更接近“每一步都有意义”的理想状态。卷积的世界里少即是多前提是每一步都踩在正确的点上。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑