CSPNet骨干网:从Cross Stage Partial到YOLO C3
我最早注意到 CSPNet不是因为它在 ImageNet 上又刷了多少个点而是在翻一份 YOLO 的配置文件时发现CSPNet、Backbone 这两个词几乎出现在每一代的骨干网定义里。后来顺藤摸瓜读回原论文《CSPNet: A New Backbone that can Enhance Learning Capability of CNN》才意识到它解决的问题比又出了一个新结构要实在得多。这篇论文的核心其实就一句话在不牺牲精度的前提下把 CNN 骨干网里的计算和显存开销打下来同时让网络的学习能力不掉队。它提出的 Cross Stage Partial 结构思路朴素到有点反直觉——把特征图在通道维度上切一刀一半走深层堆叠一半直接旁路过去。就这么一个动作后来被 YOLOv4、YOLOv5、YOLOv8 一路沿用并演化成了检测领域最主流的骨干网设计范式之一。这篇笔记适合三类人看正在啃 CNN 经典结构、想弄明白为什么改一个 concat 位置就能提点的学生准备把骨干网换到自己检测或分割项目里、需要一套能直接跑起来的代码的工程师以及被 FLOPs 和实际推理延迟之间的差距坑过、想搞清楚算力账到底怎么算的人。下面我按问题是什么—结构怎么设计—代码怎么写—实验怎么读—坑在哪的顺序展开中间会把论文里没写透、但复现时会撞上的细节一并补上。1. 这个Backbone到底改了什么CSPNet的问题出发点读一篇结构类论文最容易犯的错是直接跳到网络结构图。我自己的习惯是先看作者在抱怨什么因为抱怨决定了他们愿意牺牲什么。CSPNet 的出发点非常明确当输入分辨率变大、任务变复杂比如检测里的高分辨率输入CNN 的计算量会以让人难受的速度膨胀而这件事在移动端和边缘设备上尤其致命。作者把这个问题拆成了两块一块是推理时的算力瓶颈另一块是优化过程中的梯度信息重复后者才是这篇论文真正的着眼点。1.1 两块被长期忽略的成本梯度信息重复与推理瓶颈推理瓶颈好理解就是 FLOPs 和内存访问量。真正有意思的是梯度信息重复这个提法。作者观察到在 DenseNet 这类密集连接结构里反向传播时同一份梯度信息会经由多条路径反复作用在权重上。你可以把它想象成一个团队里所有人都在转抄同一份会议纪要看起来信息量很大实际上有效新增信息很少而且每个人都在为这份重复的纪要消耗算力。梯度更新也是这样重复的梯度信息会让权重更新的方向变得不那么差异化优化效率反而下降。这个观察在当年是有点反常识的。因为大家习惯性地认为DenseNet 那种把前面所有层输出都 concat 上来的做法是为梯度提供了更多通路属于好事。CSPNet 的贡献在于指出通路多和梯度信息多样化不是一回事。如果每一条通路传回来的都是同一份东西那增加通路的收益会迅速衰减成本却一直涨。我在自己做小数据集训练时确实有类似体感——把 DenseNet 堆得很深训练集上很快过拟合但验证集提升很慢loss 曲线抖得厉害。当时归因于数据量不够现在回头看梯度信息的高度冗余可能也是原因之一。需要说明的是梯度信息重复在论文里给出的是一套基于前向/反向公式的直观推导作者用来解释 CSP 结构为什么有效但它本身还称不上一个被严格证明的定理。所以读的时候把它当成一个非常有启发性的设计直觉而不是一条物理定律心态会摆得比较正。1.2 Cross Stage Partial 字面翻译背后的真实含义Cross Stage Partial这三个词我第一次看到也觉得绕。拆开看就好懂了。Stage 是指骨干网里分辨率相同的那个阶段比如 ResNet 里从 56×56 到 28×28 之间的那一串残差块一个 stage 就是一个战斗单元。Partial 是部分指特征图在通道维度上被分成两份。Cross 是交叉指这两份特征不是各自独立走下去而是在 stage 的输出端重新汇合。所以 CSP 的完整动作是stage 的输入特征图在通道维度上一分为二一份经过这个 stage 的主体计算堆叠的残差块或者 dense 块另一份什么也不做、直接旁路到 stage 末端两边 concat 之后再做一次融合卷积论文里叫 partial transition layer输出给下一个 stage。写成伪代码就是四行输入 x沿通道切分成 x1 和 x2y1 主体计算(x1)主体可以是一串残差块y2 x2原样保留输出 融合卷积(concat(y1, y2))。这个设计最妙的地方在于它同时干了两件事。前向方向上主体只处理一半的通道计算量直接下降反向方向上旁路那半边的梯度不需要穿过主体计算于是主体收到的梯度更新不再包含这部分重复信息。一个动作同时缓解了算力和梯度冗余两个问题这就是为什么它后来被大量工程化项目采纳——改造成本极低收益却是双份的。1.3 论文给自己定的三个目标以及它实际做到了什么作者在论文里明说了三个目标第一增强 CNN 的学习能力让同等参数下精度更高第二去掉计算瓶颈尤其是让推理时各层之间的计算量更均衡第三降低显存占用。第三个目标经常被读者忽略但对部署来说它可能比前两个都值钱因为显存往往是移动端先撞上的那堵墙。我把这三个目标和实际效果对应一下。学习能力方面论文报告的 ImageNet 分类结果里CSP 版本相对对应的基线骨干网在参数量和计算量都下降的前提下Top-1 精度还有小幅提升——注意是下降前提下还提升不是用更多算力换更高精度这个方向性的差别很重要。计算瓶颈方面CSP 结构把一个大 stage 的计算量摊到了两条分支上避免了单个超长残差堆叠带来的算力尖峰。显存方面由于旁路分支不参与主体计算中间激活的显存占用也有明显下降。我把话说得保守一点论文里给出的具体小数位我不在这里逐条背诵因为不同变体CSPResNet、CSPResNeXt、CSPDarknet和不同配置下的数字差别不小而且检测任务的 AP 受检测头和训练策略影响很大。真正值得记住的是量级和方向——参数量、计算量、显存三项同时下降精度基本持平或小幅上升。这个三降一平的组合才是 CSPNet 能活到今天的原因。2. CSPNet的结构原理拆解一次切分带来了什么理解 CSP 的结构不能只看那张切成两半再拼起来的示意图否则很容易得出这不就是把网络变窄了吗的错误结论。要讲清楚它和单纯减通道的区别得从三个层面看前向的计算路径、反向的梯度路径以及它和 ResNet、DenseNet、ResNeXt 这三种主流设计的血缘关系。我尽量把这几层拆开讲透因为后面写代码和调参时所有的坑几乎都出在这三个层面。2.1 基座-切分-合并CSP模块的最小结构抛开具体实现CSP 模块的最小骨架可以归纳成基座—切分—合并三步。基座是 stage 的输入投影通常是一个 1×1 卷积把输入通道映射到 stage 需要的工作宽度切分是沿通道一分为二合并是把两路 concat 之后做一次 1×1 卷积把通道数收敛到目标输出。这里有个细节值得单独说切分和基座投影的先后顺序在论文给出的不同变体里并不完全一致。有些变体是先做基座 1×1 再切分有些是先切分再分别投影。这两种写法在参数上略有差别在实际精度上差别通常很小但会影响到卷积融合的算子调度效率。我自己写代码时更倾向于先 1×1 再切分因为这样切分点之前的计算是共享的可以少一次小的卷积启动开销。另一个容易被忽略的点是两路合并之后的那个 1×1 融合卷积是 CSP 模块里最贵的一层。当输入输出通道都是 C 时这一层的计算量是 C² 量级而主体分支省下来的算力有可能还不到这个数。所以论文里管它叫 partial transition layer是把它当成一个有节制的过渡层来设计的——它的输出通道往往比 concat 后的总通道数小用来控制下一个 stage 的输入规模。如果你照抄结构但把这一层的输出通道设成和输入一样大会发现算力没省下来多少这就是原因。2.2 为什么Partition能减少计算而不是简单砍参数很多人第一反应是切一半通道主体计算量当然减半这不就是变相砍宽度吗凭什么精度不掉这个质疑是有道理的单纯砍宽度确实会掉点。CSP 的关键差别在于被切出去的那一半并没有消失它以零计算成本的方式参与了最终输出的 concat。拿一个具体的例子算账。假设某个残差块输入输出都是 C 通道内部隐藏宽度是 C/4。标准做法的逐位置乘加量是1×1 的 C→C/4 是 0.25C²3×3 的 C/4→C/4 是 9×C²/16 即 0.5625C²1×1 的 C/4→C 是 0.25C²加起来 1.0625C²。如果改成 CSP 形态旁路那一半直接跳过这个块主体只在 C/2 通道上工作隐藏宽度取 C/4那么主体的逐位置乘加量是 0.125C² 0.5625C² 0.125C² 0.8125C²大约是原来的 76%。看起来只省了四分之一好像不多。但把视角放大到一整个 stage 就完全不一样了。假设这个 stage 里有 6 个这样的块标准形态是 6×1.0625C² 6.375C²CSP 形态下主体的 6 个块只有 6×0.8125C² 4.875C²旁路分支几乎不花钱只在合并处多一次 1×1。也就是说在整个 stage 的尺度上省下来的绝对量是可观的而且省下来的这部分并没有把信息丢掉——被旁路的那一半特征原封不动地进入了输出。这就是为什么 CSP 不像单纯砍宽度。砍宽度是信息量的净损失CSP 是把一部分通道的计算从必须处理变成直接传递信息量没少只是处理方式变了。2.3 从反向传播看梯度路径的变化前向的账算完再来看反向。这是 CSPNet 论文里最有价值、也最容易被跳过的一段。在标准 DenseNet 里每个 dense 块的输出都 concat 到后面所有层的输入上反向传播时某一层的权重梯度会从后续所有路径汇集上来而这些路径在数值上高度相关。用一个不严谨但好记的说法同一个梯度信号被数了好几遍导致权重更新的有效学习率在一些方向上被放大在另一些方向上被稀释优化的信噪比下降。CSP 的做法是把旁路分支从主体计算里摘出去。这样一来合并层的梯度有一部分直接回传到 stage 输入完全绕过了主体里的那些卷积层。而主体分支的权重收到的梯度只来自它自己那条路径不再叠加旁路那一份重复信号。用一句话概括局部上梯度变稀疏了但稀疏带来的是差异化差异化带来的是更好的学习效率。我自己的观察是这个效应在小 batch、小数据集上会更明显。大 batch 训练时梯度本身噪声就大这点差异容易被淹没而在数据量只有几万张、batch 又开不大的场景里CSP 结构的训练曲线往往比基线更稳。这不是论文的结论是我复现时的一个体感供你参考。2.4 与ResNet、DenseNet、ResNeXt的结构对照表把 CSP 和三种主流结构放在一起对照能更清楚地看出它站在哪个位置。结构跨层连接方式反向梯度路径主要开销来源通道复用方式ResNet逐元素相加shortcut有恒等通路梯度可直达3×3 卷积本身无显式复用DenseNet通道维 concat 全部前层通路极多梯度信息重复concat 后的通道膨胀全量复用代价是通道数爆炸ResNeXt分组卷积 相加与 ResNet 类似分组卷积的等效宽度组内复用CSPNetstage 内切分 末端 concat旁路直达主体梯度不叠加合并层的 1×1部分复用通道数可控这张表里我最想让你注意的是最后一行部分复用。DenseNet 是全量复用好处是特征极度丰富代价是通道数随深度线性甚至更快膨胀concat 那一步的内存拷贝非常伤ResNet 是完全不复用好处是干净代价是每一层都得自己从头学特征。CSP 在这个光谱上找到了一个中间点只复用一部分而且复用是跨 stage 级别的、不是逐层的所以通道数可以被精确控制。也正因为这个定位CSP 天然是可加装的——它不改变残差块内部的结构只是在 stage 的组织方式上动手。这一点在工程上价值巨大意味着你可以拿一个现成的 ResNet 实现花二三十行代码就改成 CSP 版本不需要重写整个骨干网。下一节讲变体的时候你会看到作者也是这样做的。3. 三个变体与两个配套模块工程落地时的选型论文里 CSPNet 不是一个固定的网络而是三个变体加两个可插拔模块的组合。搞不清楚这五个东西分别解决什么问题选型时就会瞎猜。我把它们的关系梳理成这样三个变体是把 CSP 套到谁身上的问题两个模块是检测任务里额外补什么的问题。前者决定骨干网后者决定脖子和头这一段怎么接。3.1 CSPResNet、CSPResNeXt、CSPDarknet的差别在哪CSPResNet 是最基础的版本把 ResNet 的每个 stage 换成 CSP 形态。改动集中在两处stage 的入口做通道切分stage 的出口做 concat 加融合卷积。瓶颈块内部结构完全不变。这种改法最保守也最容易验证效果我第一次复现就是从这个版本入手的。CSPResNeXt 是在 CSPResNet 基础上把瓶颈块里的 3×3 卷积换成分组卷积。分组卷积本来就是为了在同等参数下扩大等效宽度套上 CSP 之后两条分支各自是分组卷积算力分布更均匀。论文里 CSPResNeXt-50 对比 ResNeXt-50 的数据是三个变体里最漂亮的参数量和计算量都下降精度还略高。CSPDarknet 是给检测任务准备的。Darknet 系列本身的结构比 ResNet 更重每个 stage 的残差堆叠更长所以套 CSP 之后省下来的算力比例也更大。这也是为什么后来 YOLOv4 选的是 CSPDarknet53 而不是 CSPResNet50——不是因为精度绝对更高而是因为在检测这个具体场景下CSPDarknet 的算力-精度曲线更划算。这里补充一个重要的事实论文里的 CSPDarknet 和后来 YOLOv5 里那个被大家叫做 C3 的模块细节上并不完全一致。C3 是工程化之后的变体切分方式更简洁用两个 1×1 卷积隐式实现切分通道比例也做过调整。你如果拿 YOLOv5 的 C3 去和论文里的 CSPDarknet 逐层对会发现对不上这是正常的。写代码时以你实际用的框架为准别死抠论文。3.2 Partial Transition Layer那个容易被忽略的关键层在 CSP 模块的末端concat 之后那一次 1×1 卷积论文叫 partial transition layer。这个名字里的 partial 是有讲究的——它不只是过渡它同时承担了三个职责。第一个职责是通道收敛。两路 concat 之后通道数是输入的两倍如果不收敛下一个 stage 的输入会翻倍几层下去通道数就失控了。第二个职责是特征融合。旁路那半边的特征没经过任何变换和主体输出直接拼在一起两者在数值分布上有差异需要一个可学的层把它们对齐。第三个职责也是最少被提到的是梯度分流。这一层是旁路分支梯度回传的必经之路它的存在让旁路的梯度有了一个可控的闸门。我踩过一次坑早期复现时为了省参数把这个融合层去掉直接 concat 完就送进下一个 stage。结果训练能跑但精度比基线还低了一截。回头分析问题就出在第二个职责上——两路特征的分布差异没有被对齐之后的 BN 层要花很多精力去纠正这个分布等于把工作从一个 1×1 卷积转移到了 BN代价更大。所以这一层千万别省。它的输出通道怎么定也有讲究。常见做法是设成和输入相同的 C这样每个 stage 前后通道数不变堆叠时不用额外考虑也有做法是设成 C 的一半让通道数阶梯式增长。前者稳定后者更省。我的建议是先用 C跑通了再尝试压缩。3.3 EFM特征金字塔融合的成本控制EFM 全称 Exact Fusion Model是论文里针对检测任务的配套模块解决的是特征金字塔融合阶段的算力问题。检测网络一般要把不同尺度的特征图上采样或下采样到同一分辨率再相加这个阶段的算力开销在总体里占比不小尤其是输入分辨率大的时候。EFM 的做法是给融合引入注意力式的加权不同来源的特征图不是简单地相加而是通过一个可学习的方式决定各自的贡献同时用较大的感受野来做融合避免小卷积核反复堆叠带来的开销。论文报告这一块能把融合阶段的计算量压到原来的四分之一左右。这里我要提醒一句EFM 和后来大家更熟悉的 FPN、PAN、BiFPN 属于同一类东西都是脖子部分的融合设计。如果你现在用的是 YOLOv5 或 YOLOv8 这类成熟框架它们的脖子已经做过大量优化没必要再手动把 EFM 塞进去。EFM 的价值更多是提供一种思路融合阶段是可以做减法的不是只能一味加层。3.4 SAM空间注意力在骨干网里加在哪儿SAM 全称 Spatial Attention Module在论文里的用法比较克制——它不是加到每个 stage 上而是加在低层特征图上目的是在不增加太多计算的前提下扩大感受野。低层特征图分辨率高、通道数少在这里做注意力比在高层做便宜得多而且低层特征对定位更重要放大感受野对检测框的回归有直接帮助。我实测下来SAM 这类模块的收益比较看任务。如果你的任务是检测小目标、低层特征的重要性高加 SAM 通常能看到收益如果是纯分类任务加它的收益就非常有限有时候甚至因为额外参数导致过拟合。所以别把它当成必选项要做消融再决定。4. 计算量手算与代码实现把CSPNet落到自己的项目里前面都是纸面分析这一节开始动手。我按先算账、再写码、最后实测验证的顺序来因为算账这一步能帮你判断一个改动值不值得做。很多人写代码是一边改一边试改到后面自己都说不清是哪一步起了作用根子就在于没提前算账。4.1 单个Bottleneck的MAC手算先把三种常见瓶颈块的逐位置乘加量MAC算清楚。假设输入输出通道都是 C只统计卷积不算 BatchNorm 和激活函数也不考虑偏置项。所谓逐位置是指每个空间位置上的乘加次数这样算出来的量再乘以特征图面积就是总 FLOPs。结构内部宽度1×1 降维3×3 卷积1×1 升维合计单位 C²ResNet式瓶颈C/40.250.56250.251.0625Darknet式瓶颈C/20.52.250.53.25CSP主体ResNet式C/40.1250.56250.1250.8125CSP主体Darknet式C/20.251.1250.251.625算的原理很简单一个输入通道数 a、输出通道数 b 的卷积在每个空间位置上的乘加次数就是 a×b卷积核是 3×3 的话再乘以 9。所以 1×1 的 C→C/4 就是 0.25C²3×3 的 C/4→C/4 是 9×C²/16 0.5625C²。从表里能看出两件事。第一3×3 卷积是绝对的耗粮大户在 ResNet 式瓶颈里它占了全部算力的 53%在 Darknet 式瓶颈里占到了 69%。第二CSP 主体之所以便宜一半功劳来自通道切分输入通道从 C 变成 C/2另一半来自内部宽度的变化。这两者的贡献是可以分别控制的这给了你调参的空间想省算力就多切一点想保精度就少切一点。再算一下合并层的账把它和主体省的算力放一起对比你会看得更清楚。合并层是 1×1 卷积输入是两路 concat 的 C输出也是 C算下来是 C²。而主体分支通过切分省下的算力在 ResNet 式瓶颈里是 1.0625C² − 0.8125C² 0.25C²。也就是说光看一个块合并层的 C² 比省下的 0.25C² 大了四倍。这个对比第一次算出来的时候我是有点懵的感觉 CSP 是不是白折腾了。但放到 stage 尺度就理解了一个 stage 有 6 到 8 个块每个块省 0.25C²累积起来是 1.5C² 到 2C²而合并层只在整个 stage 的出口出现一次成本是 C²。所以 stage 越长CSP 越划算stage 只有一两个块的时候改造成 CSP 反而可能亏本。这也是为什么后来所有工程化实现都只在长 stage上做 CSP短 stage 保持原样。4.2 C3模块的PyTorch实现与逐行说明说完账上代码。先给一个最贴近工程实践的版本也就是 YOLOv5 系列里那个被广泛使用的 C3 模块。它用一个很巧的写法隐式实现了切分。import torch import torch.nn as nn def autopad(k, pNone): 让卷积输出尺寸与输入保持一致k 为奇数时 p k // 2 if p is None: p k // 2 if isinstance(k, int) else [x // 2 for x in k] return p class Conv(nn.Module): Conv BN 激活 的标准组合激活默认用 SiLU def __init__(self, c1, c2, k1, s1, pNone, g1, actTrue): super().__init__() self.conv nn.Conv2d(c1, c2, k, s, autopad(k, p), groupsg, biasFalse) self.bn nn.BatchNorm2d(c2) self.act nn.SiLU() if act else nn.Identity() def forward(self, x): return self.act(self.bn(self.conv(x))) class Bottleneck(nn.Module): 残差瓶颈块1x1 降维 - 3x3 卷积 - 残差相加 def __init__(self, c1, c2, shortcutTrue, g1, e0.5): super().__init__() c_ int(c2 * e) # 内部隐藏宽度默认是输出的 1/2 self.cv1 Conv(c1, c_, 1, 1) self.cv2 Conv(c_, c2, 3, 1, gg) self.add shortcut and c1 c2 # 通道不一致时自动关掉残差 def forward(self, x): return x self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x)) class C3(nn.Module): CSP 结构的工程化实现两条支路各走一半通道末端 concat 1x1 融合 def __init__(self, c1, c2, n1, shortcutTrue, g1, e0.5): super().__init__() c_ int(c2 * e) # 每条支路的工作通道数 self.cv1 Conv(c1, c_, 1, 1) # 支路一进入堆叠瓶颈块 self.cv2 Conv(c1, c_, 1, 1) # 支路二直接旁路只做一次投影 self.cv3 Conv(2 * c_, c2, 1, 1) # 融合把两路拼起来压回目标通道 self.m nn.Sequential( *[Bottleneck(c_, c_, shortcut, g, e1.0) for _ in range(n)] ) def forward(self, x): y1 self.m(self.cv1(x)) y2 self.cv2(x) return self.cv3(torch.cat((y1, y2), dim1))逐行说几个关键点。c_ int(c2 * e)里的 e 默认 0.5意思是每条支路只处理输出通道数的一半两条加起来正好等于 c2。cv1和cv2都是从同一个输入 c1 出发的两个 1×1 卷积各自输出 c_ 个通道——这在数学上等价于先把输入沿通道一分为二再各自做 1×1 变换但写起来只有两行而且两条支路的通道数可以独立调整灵活度更高。这是工程实现比论文描述更讨巧的地方。self.m里那一串瓶颈块注意传给 Bottleneck 的e1.0。因为外层已经做过一次 0.5 的降维了如果块内再来一次 0.5内部宽度会变成 c2 的四分之一网络会变得很窄、表达力不足。这个双重 e 的坑我第一次写的时候踩过网络能训但精度明显偏低盯了很久才发现是这里。记住一条经验e 这个缩放因子在一个 C3 里只应该生效一次。最后cv3是融合层把 2×c_ 压回 c2。如果你把 c2 设得比 2×c_ 大那这层就在做升维反过来就是降维。绝大多数配置里两者相等融合层只做通道对齐。4.3 替换ResNet的Bottleneck改造步骤如果你手上有个现成的 ResNet 想改成 CSP 版本改法比想象中简单核心就是把Bottleneck换成C3这类 stage 级模块。步骤我列一下找到 ResNet 的_make_layer函数它负责生成一个 stage 里堆叠的残差块把每个 stage 的第一个块保留为下采样块stride2其余块保持不变在 stage 的入口和出口外面包一层 C3 结构或者直接替换整个 stage 的实现检查通道数变化CSP 版本里 stage 的输入会被切分所以入口的 1×1 投影要按输入通道算别用错。第 4 步是高频出错点。C3 的cv1和cv2的输入通道是c1也就是 stage 的输入通道不是切分之后的通道数。因为切分是在这两个卷积内部隐式发生的。如果你按切分后的通道数去设 c1通道就对不上了跑起来会直接报维度错误。另外一个建议是分批改先只改 stage 3 和 stage 4 这两个最长的 stage跑一遍看精度和算力变化确认没问题再考虑改 stage 2。理由是前面算过账短 stage 改 CSP 收益很低有可能得不偿失。4.4 用thop实测参数量与FLOPs手算能帮你判断方向但手算有两个系统误差一是没算 BN 和激活二是没算上采样、concat 这些算子。所以最终还是要实测。用 thop 这个库最省事import torch from thop import profile # 这里的 CSPResNet50 换成你自己的模型实例 model CSPResNet50(num_classes1000).eval() dummy torch.randn(1, 3, 224, 224) with torch.no_grad(): macs, params profile(model, inputs(dummy,), verboseFalse) print(f参数量: {params / 1e6:.2f} M) print(fMACs: {macs / 1e9:.2f} G) print(fFLOPs: {macs * 2 / 1e9:.2f} G) # 一次乘加算两次浮点运算这里有三个实测时容易搞混的地方我一个个说。注意MACs 和 FLOPs 是两个不同的量很多论文和开源实现混着用。一次乘加是一乘一加算两次浮点运算所以 FLOPs 一般是 MACs 的 2 倍。看到别人报多少 GFLOPs时先确认他说的是哪一个不然对比出来的差距可能全是口径问题。第一个坑是输入分辨率。FLOPs 和输入尺寸是线性关系224×224 和 256×256 差着 30%跨分辨率比 FLOPs 没有意义。第二个坑是thop对某些自定义算子会漏算尤其是 concat、split 这类纯内存操作所以它给出的数字是卷积部分的算力和推理框架实际测出来的延迟不是一回事。第三个坑是分组卷积thop在旧版本里对 grouped conv 的统计有偏差如果你用的是 CSPResNeXt最好用新版或者交叉验证一下。5. 训练与复现消融实验怎么做才有说服力结构和代码都有了接下来是训练和验证。这一节我想重点说消融实验的设计因为结构类论文的结论可信度几乎完全取决于消融做得干不干净。我见过太多复现文章改了三个地方一起上涨点了就归功于自己最想证明的那一处这种结论没法指导别人。5.1 训练配置的常见选择ImageNet 分类的标准配置大致是这样优化器用带动量的 SGD动量 0.9权重衰减 5e-4学习率用余弦退火初始值随 batch size 线性缩放batch 256 时初始学习率在 0.1 到 0.2 之间训练 100 到 300 轮数据增强标配随机裁剪加翻转想要更好效果再叠 mixup 或 cutmix标签平滑 0.1权重指数移动平均EMA通常能白拿 0.2 个点左右的提升。这套配置不是必须照抄但它经过大量验证是个不容易出错的起点。我要特别提一句权重衰减CSP 结构的参数量比同深度基线少同样的权重衰减系数在 CSP 上的相对约束会更强一点。如果你发现 CSP 版本比基线更容易欠拟合可以试着把权重衰减调小一些比如从 5e-4 降到 3e-4再看曲线。5.2 消融实验的设计要点做结构消融最容易犯的错误是用同深度对比。CSP 版本和基线在同样的层数下参数量和算力是不同的这样对比出来的精度差异说不清是结构带来的还是容量带来的。正确的做法有两个要么控制算力相同比较同等 FLOPs 下的精度要么控制参数量相同比较同等参数下的精度。论文里用的是前者因为它更贴近部署时的真实约束。第二个要点是随机性控制。ImageNet Top-1 的训练噪声通常在 0.1 到 0.3 个百分点之间如果你的改动只带来 0.2 个点的提升那很可能落在噪声范围里。稳妥的做法是至少跑两个随机种子取平均如果一个种子涨 0.3、另一个掉 0.1说明这个改动不可靠。第三个要点是训练轮数要足够。我在复现时遇到过这种情况前 60 轮 CSP 版本落后基线80 轮之后反超。如果只训 60 轮就下结论会得出完全相反的答案。结构类改动往往影响的是收敛速度而不是最终上限训练不充分时看到的是速度差异不是容量差异。5.3 论文里几组关键数据的读法读实验结果表的时候我建议关注三列而不是一列参数量、FLOPs或者 MACs、Top-1 精度。只看精度的提升会漏掉用更少算力拿到同样精度这个更有价值的结论。CSPNet 的三个变体在论文里的共同特点就是这三点同时往好的方向走。具体数字我不在这里逐个复述因为不同变体、不同输入尺寸、是否用多裁剪测试数字差别都能有零点几到一点几逐个抄反而容易误导。更实际的做法是把论文里的表格结构和自己的实验表格对齐用同样的口径同样的输入尺寸、同样的评测方式去算一遍自己的数字这样得到的结论才对你有用。检测任务上的迁移结果也一样。CSP 结构的检测器在 COCO 上的表现受检测头、训练策略、是否用预训练权重影响很大。我自己的经验是在检测任务上把骨干网换成 CSP 版本收益往往比在分类任务上更明显因为检测用的输入分辨率更高算力瓶颈本来就更突出CSP 缓解的正是这个瓶颈。6. 踩坑记录与常见问题速查最后这一节写复现时真正会撞上的东西。前面几节讲的是应该怎么做这里讲的是为什么我按应该做的做了还是不行。6.1 不收敛与掉点的几个典型原因第一个原因是之前提到的双重 e。c_ c2 * e在外层算了 0.5块内又算了一次 0.5内部宽度变成 c2 的四分之一。表现是 loss 下降很慢、最终精度比基线低一到两个点但因为网络能跑很容易被忽略。排查方法很简单打印每个模块的实际通道数对着看。第二个原因是融合层缺 BN。两路 concat 之后特征分布不一致如果融合卷积后面没有 BN下一个 stage 的 BN 要承担纠偏的工作训练会不稳定。标准实现里融合卷积永远是 ConvBN激活不要为了省参数去掉。第三个原因是学习率没跟着改。CSP 版本参数量更少同样的学习率下每步更新的相对幅度更大容易训飞。表现是前几轮 loss 振荡甚至变成 NaN。解决办法是先把学习率降一半试稳定了再慢慢往上调。第四个原因是旁路分支的通道数太少。如果为了省算力把切分比例设成 0.25/0.75旁路只留四分之一通道信息量不足精度会掉。经验值是切分比例不要超过 0.5也就是主体至少拿到一半通道。6.2 通道切分比例、下采样位置、BN位置的细节切分比例是最值得调的参数但它的影响没有想象中大。我做过一组对比比例从 0.5 调到 0.6ImageNet 精度的变化在噪声范围内。所以别在这上面花太多时间0.5 是个稳妥的默认值。真正需要按 stage 调的是哪些 stage 用 CSP长 stage 用、短 stage 不用这个收益比调比例明显。下采样位置有一个隐性的坑。如果 stage 的首个块带 stride2而这个下采样发生在主体分支里那么旁路分支也需要对应的下采样否则 concat 时空间尺寸对不上。常见做法是把下采样放在 stage 入口的 1×1 投影上两条支路共享这次下采样这样最省事也不容易出错。如果你把下采样放在主体里一定要记得给旁路也加一个 stride2 的操作。BN 的位置也有讲究。两路分别做 BN 再 concat和 concat 之后统一做 BN效果是不一样的。分别做 BN 的好处是各自归一化分布更干净统一做 BN 的好处是计算量少而且两路之间有了耦合。工业实现里两种都有我倾向于统一做 BN因为它在推理时更容易被融合进卷积部署友好。还有一个部署侧的细节值得提前知道CSP 结构里的 concat 会增加内存访问。在桌面 GPU 上算力是瓶颈concat 的影响不大但在移动端或者一些 NPU 上内存带宽才是瓶颈concat 造成的额外拷贝会让实际延迟比 FLOPs 预测的要高。所以如果你是在端侧部署不要只看 FLOPs 的下降一定要在目标设备上实跑一遍延迟。6.3 常见问题速查表把这一节的内容整理成表方便你遇到问题时直接对照。现象可能原因排查方法处理建议loss 下降慢最终精度低于基线双重 e 导致内部通道过窄打印各模块实际通道数检查缩放因子是否只生效一次训练前几轮 loss 振荡或 NaN学习率相对参数量偏大降低学习率重跑初始学习率降为原来的 0.5 倍训练不稳定曲线抖动大融合层缺 BN 或分布未对齐检查融合层是否 ConvBNAct补齐 BN不要省这一层concat 时维度报错下采样只做在了主体分支打印两路输出的 shape下采样移到 stage 入口共享同深度对比精度反而下降对比口径不公平核对参数量与 FLOPs改为同 FLOPs 或同参数对比精度提升在 0.3 个点以内训练噪声覆盖了真实差异换随机种子重跑两次多次取平均后再下结论FLOPs 降了但端侧延迟没降concat 带来额外内存访问在目标设备实测延迟端侧部署时以实测延迟为准我个人的体会是这张表里前三行占了复现失败原因的一大半。结构本身其实很简单出问题几乎都出在通道数、学习率和归一化这三个地方。把这三个守住CSP 的复现基本不会翻车。再补一个我后来才想明白的点CSP 这类组织层面的改动和改变卷积核、改变激活函数这类算子层面的改动收益是叠加的但叠加效果不是简单的相加。我试过在已经用了 CSP 的骨干网上再加注意力模块收益明显比在基线骨干网上加要小。合理的解释是两者都在改善梯度流动和信息复用边际效用递减。所以如果你已经在用 CSP 结构下一步的优化方向可能不是继续堆结构改动而是回到数据和训练策略上找空间。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →