资讯详情

资讯详情

基于MMYOLO的YOLO主干CoTAttention插入实践

做目标检测的朋友大概都遇到过这种场景模型在标准测试集上 mAP 挺好看一放到真实业务画面里遮挡的、背景杂乱的、目标只露出小半截的检测框就开始飘。我上个月拿到的一批工业质检图就是这样密集小目标和背景纹理高度相似改数据增强收效有限于是把目光转向了注意力模块。翻了一圈资料最终选定的是 CoTAttention把它往 YOLO 主干网络的 Stage 后面插。这篇就把基于 MMYOLO 做这件事的完整链路写清楚为什么选它、代码怎么落地、插在哪个位置、训练时要动哪些参数、以及我在过程中踩到的几个坑。适合已经能跑通 MMYOLO 官方配置、想自己做结构改进但不知道怎么下手的读者也适合只听过 CoTAttention 名字、想看看它到底值不值得试的朋友。1. 拿 CoTAttention 换掉老一套注意力组合之前该想清楚的事1.1 SE、CBAM 与 CoTAttention 在主干网络里的行为差异先说说大多数人改 YOLO 的第一反应往 C3 或者 SPPF 前面塞一个 SE 模块或者干脆上 CBAM。这两个东西实现简单、代码到处都能抄跑起来也不容易报错所以成了默认动作。但它们的本质是通道重标定和空间注意力掩码做的事情是给已有的特征图重新分配权重特征本身并没有产生新的交互。CoTAttention 的路子不一样。它先把输入用 3×3 卷积处理后得到一份带局部上下文的 key再把这部分和原始输入拼起来通过两层 1×1 卷积生成一组动态的注意力权重最后用 softmax 归一化后去加权 value。关键在于这个权重不是固定形状的池化结果而是由输入内容自己算出来的而且算的过程中 key 已经融合了 3×3 邻域的信息等于把静态上下文和动态自注意力揉在了一次操作里。用生活化的说法SE 像是给每个部门打分然后按分数发预算CoTAttention 像是让部门之间先开个会互相通气再根据通气结果重新分配资源。前者只看自己后者看邻居也看自己。在背景杂乱、目标与背景纹理相近的场景中这种邻域交互带来的增益通常比单纯的通道重标定更明显。模块核心操作参数量增量主要收益方向落地难度SE全局池化 全连接 Sigmoid极小通道重要性重分配低CBAM通道注意力 空间 7×7 卷积掩码小通道与空间二维重标定低CoTAttention3×3 上下文编码 动态注意力矩阵中等局部上下文与长程依赖融合中多头自注意力标准 QKV 全局矩阵乘大全局依赖建模高这张表里的参数量增量是相对同通道数的普通卷积层来估的。CoTAttention 的额外参数主要来自那个两层 1×1 的注意力生成分支其中第二层输出通道是kernel_size * kernel_size * C在 C 比较大比如 512的时候这一层会明显变胖。这也是后面要讲通道对齐时得留意的点。1.2 为什么在主干预算力预算时要先算清楚这笔账有个很容易忽略的事实注意力模块加在主干的不同位置算力代价差得非常远。YOLO 主干的特征图分辨率是逐级下降的P3 阶段stride 8的特征图面积是 P5stride 32的 16 倍。同样一个 CoTAttention 模块挂在 P3 后面和在 P5 后面前向 FLOPs 能差出一个数量级。我自己的经验是先在 P4 和 P5 上做实验确认有效再考虑往 P3 挪。原因很实际——P3 的特征图太大CoTAttention 里那个输出通道为k*k*C的 1×1 卷积会把显存吃掉一大块训练 batch size 一旦被迫压到 4 以下BN 层的统计量就开始不稳最后你分不清是注意力没效果还是训练本身崩了。举个具体数字以输入 640×640、主干通道配置接近 YOLOv8-s 的情况为例P3 输出 80×80×128CoTAttention 的注意力生成卷积输出通道 9×1281152这一层的计算量约80×80×128×1152 ≈ 9.4×10^8MACsP5 输出 20×20×512注意力生成卷积输出通道 9×5124608计算量约20×20×512×4608 ≈ 9.4×10^8MACs诶算下来反而差不多这是因为分辨率降了 16 倍但通道涨了 4 倍输出通道涨了 4 倍正好抵消。所以真正决定代价的是H×W×C×9C这个量H×W和C²在互相拉扯。这也说明一件事不能凭直觉判断哪个位置更贵必须把数字代进去算。我在第一次实验时就因为想当然觉得 P3 一定更贵结果把实验顺序排错了白跑了两轮。1.3 一个反直觉的观察注意力不是叠得越多越好这一点值得单独拎出来说。我最初的想法很朴素——每个 Stage 后面都挂一个效果总该更好吧实测下来完全不是。在 P3、P4、P5 全挂的版本上验证集 mAP 反而比只挂 P4 的版本低了 1.2 个点而且训练后期的 loss 曲线抖动明显变大。回头分析原因大概有两个方向。一是主干本身已经有足够的非线性表达能力额外的注意力在浅层引入了过强的归纳偏置把本可以自由学习的特征模式给约束住了。二是参数变多之后在数据量有限的情况下过拟合信号提前出现——训练集 loss 一路往下验证集在第 60 轮左右就开始抬头。所以我后来固定的策略是主干只加一到两处优先 P4其次是 P5P3 谨慎尝试。这个结论不一定适用于所有数据集但对于中等规模几万张量级的检测任务是个比较安全的起点。2. MMYOLO 里给主干动刀的入口到底在哪2.1 从 BaseBackbone 的继承关系说起很多人第一次改 MMYOLO 主干时会直接去mmyolo/models/backbones/csp_darknet.py里改源码。这个做法能跑通但后续升级框架或者复现别人实验时会非常痛苦因为你的改动和官方代码缠在一起了。正确姿势是新建一个 backbone 类继承BaseBackbone然后在配置文件里用type指过去。BaseBackbone的设计思路是把主干拆成stem 层 若干个 stage两段forward里统一做stem → stage1 → stage2 → ... → 输出多尺度特征的流程子类只需要实现三个方法——build_stem_layer、build_stage_layer以及可选的_make_stage。这样你完全不用碰前向传播的控制流只要告诉框架每一层怎么造就行。我在实际项目里的写法大致是这样省略了部分非关键参数from mmyolo.models.backbones import BaseBackbone from mmyolo.registry import MODELS from ..layers.cot_attention import CoTAttention MODELS.register_module() class CoTDarknet(BaseBackbone): def __init__(self, archP5, deepen_factor1.0, widen_factor1.0, cot_indices(3,), out_indices(2, 3, 4), **kwargs): self.cot_indices cot_indices super().__init__(archarch, deepen_factordeepen_factor, widen_factorwiden_factor, out_indicesout_indices, **kwargs) def build_stem_layer(self): # 与原生 CSPDarknet 保持一致 ... def build_stage_layer(self, stage_idx, in_channels, out_channels): # 构造 C2f / C3 等 stage 主体 layers [...] # 关键在这个 stage 末尾追加 CoTAttention if stage_idx in self.cot_indices: layers.append(CoTAttention(out_channels)) return layers这里cot_indices是我自己加的参数用元组控制挂在哪些 stage 后面。这样一套代码就能通过配置文件切换插入位置不用改代码就能做消融实验效率高很多。2.2 注册链路从 layers 的__init__.py到 registry自定义模块要被 MMYOLO 认出来注册这一步不能漏。完整的链路是这样的在mmyolo/models/layers/下新建cot_attention.py用MODELS.register_module()装饰你的CoTAttention类在同目录的__init__.py里加一行from .cot_attention import CoTAttention在mmyolo/models/layers/__init__.py的__all__列表里补上CoTAttention三处少一处配置文件里写typeCoTAttention的时候就会报KeyError。我第二次踩这个坑是因为只改了__init__.py忘了补__all__报错信息还挺隐晦写的是模块找不到排查了十几分钟才定位到。提示如果不想动框架源码目录也可以在自己项目的模块里定义类然后在配置文件的custom_imports里把模块路径加进去框架启动时会自动导入并完成注册。这种方式对多项目并行更友好。2.3 配置文件挂载的三种方式与各自的真实代价改结构有三条路我按推荐度排一下第一种自定义 backbone 类 配置里换type。这是最干净的所有逻辑集中在自己的类里官方代码零改动。代价是要多写几十行构造 stem 和 stage 的代码如果对原主干结构不熟容易写错通道数。第二种用BaseBackbone的arch参数拼接式配置。MMYOLO 支持把主干的每一层写成字典列表直接在配置文件的backbone字段里列出。优点是零代码缺点是表达能力有限比如只在 stage3 后面插注意力这种逻辑用纯字典列表写不出来除非你能接受每个 stage 都插。第三种直接改官方csp_darknet.py。最快能跑通但后续把这套改进迁到别的模型比如 YOLOv9、RTMDet上时代码得重新抄一遍而且和别人协作时容易冲突。只建议用来做快速验证验证完必须重构成第一种。我自己现在的流程是先用第三种花半小时确认效果方向对不对有希望了再花两小时重构成第一种正式跑实验。这样时间利用效率最高。3. CoTAttention 的落地写法与张量形状的三个硬约束3.1 单头版本的最小可用实现直接上代码这是我实际在用的版本注释里标了几个关键点import torch import torch.nn as nn import torch.nn.functional as F from mmyolo.registry import MODELS MODELS.register_module() class CoTAttention(nn.Module): Contextual Transformer Attention (single head). def __init__(self, dim, kernel_size3, reduction4): super().__init__() self.dim dim self.kernel_size kernel_size self.k2 kernel_size * kernel_size # 1) 局部上下文编码3x3 卷积提取邻域信息 self.key_embed nn.Sequential( nn.Conv2d(dim, dim, kernel_size, paddingkernel_size // 2, biasFalse), nn.BatchNorm2d(dim), nn.ReLU(inplaceTrue), ) # 2) value 变换保持通道数不变 self.value_embed nn.Sequential( nn.Conv2d(dim, dim, 1, biasFalse), nn.BatchNorm2d(dim), ) # 3) 动态注意力生成中间降维再升到 k*k*C hidden max(dim // reduction, 16) self.attention_embed nn.Sequential( nn.Conv2d(2 * dim, hidden, 1, biasFalse), nn.BatchNorm2d(hidden), nn.ReLU(inplaceTrue), nn.Conv2d(hidden, self.k2 * dim, 1, biasFalse), ) def forward(self, x): B, C, H, W x.shape k1 self.key_embed(x) # B, C, H, W v self.value_embed(x).view(B, C, -1) # B, C, HW # 上下文 key 与原始输入拼接得到 2C 通道 y torch.cat([k1, x], dim1) # B, 2C, H, W att self.attention_embed(y) # B, k*k*C, H, W # 拆成 (B, C, H, W, k*k) 后对 k*k 维度求均值 att att.reshape(B, C, self.k2, H, W) att att.permute(0, 1, 3, 4, 2) # B, C, H, W, k*k att att.mean(dim-1, keepdimFalse) # B, C, H, W att att.view(B, C, -1) # B, C, HW # softmax 归一化后加权 value out F.softmax(att, dim-1) * v # B, C, HW out out.view(B, C, H, W) return out x最后那句out x是残差连接不能省。去掉之后我实测训练在 30 轮左右就出现梯度爆炸loss 直接变 NaN因为注意力输出和输入的数值分布差异比较大直接替换会让后续 BN 层的统计量剧烈波动。另外有一点要说明上面是先reshape成(B, C, k*k, H, W)再 permute 到(B, C, H, W, k*k)求均值。这个操作顺序对应的是原论文里对 k×k 个邻域位置的注意力分别求平均的语义。如果你直接 reshape 成(B, k*k*C, H, W)然后按通道求均值语义是不一样的结果会明显变差。我在第一版实现里就犯了这个错跑出来的 mAP 比不加注意力还低查了半天才发现是维度语义搞混了。3.2 多头版本与 Bottleneck 封装的取舍原论文里除了单头版本还给了一个多头版本和配套的CoTBottleneck用在 ResNet 系列主干上做替换。我的建议是YOLO 主干上先用单头别急着上多头。理由很实际。多头版本的通道切分会把每个头的维度压到C/heads在 YOLO-s 这种小模型上通道数普遍在 64~256 之间切 4 个头之后每个头只有十几到几十通道注意力矩阵的表达能力被削得很厉害。而且多头要做多组矩阵运算在移动端部署时算子融合会变麻烦导出 ONNX 之后节点数明显增加。CoTBottleneck更适合的场景是替换整个 BasicBlock——也就是你想把 ResNet 的残差块换成 CoT 残差块。YOLO 主干用的是 CSP/C2f 这类结构直接套 Bottleneck 会破坏原有的跨阶段特征复用逻辑除非你打算重写整个 stage 的组成方式。我试过一版把 C2f 里的瓶颈单元换成 CoTBottleneck参数量涨了 18%mAP 只涨了 0.4性价比不高就放弃了。3.3 通道对齐、stride 与下采样位置的三个硬约束插入位置有三条不能破的规则破了就是各种形状不匹配报错约束一输入通道必须等于该 stage 的输出通道。CoTAttention 是通道保持的模块输入 C输出还是 C所以放在 stage 后面时dim必须填这个 stage 的out_channels。注意 MMYOLO 里out_channels会被widen_factor缩放如果你写的是 base 通道数实际运行时对不上。正确写法是int(out_channels * widen_factor)。约束二不要改变 stride。CoTAttention 全是 1×1 和 3×3 padding1 的卷积stride 都是 1特征图尺寸不变。这是好事但要注意别把它塞在需要下采样的位置——比如某些主干在 stage 之间用 stride2 的卷积做降采样把注意力插在降采样之前和之后特征图尺度是不同的后续的通道对齐逻辑也会变。约束三加在 stage 内部还是外部要提前定好。加在 stage 的最后一个 block 之后stage 外部输入输出尺度一致最安全。加在 stage 内部某个 block 之间则需要保证前后通道匹配且如果那个 stage 内部有跨层连接注意不要破坏残差路径。我目前的做法是统一加在 stage 外部即layer.append(CoTAttention(out_c))这样逻辑最清晰也方便通过cot_indices参数控制开关。4. 四个候选插入位置的实测对比4.1 Stage3 之后、SPPF 之前这两处的差异这是最常被拿来比较的两个位置。Stage3 之后接的是 P4 尺度输出stride 16SPPF 之前接的是主干最后一个 stage 的输出stride 32。先说结论在这个数据集上Stage3 之后P4插的效果最好SPPF 之前次之两者都明显优于 P3。具体数据同一份数据、同一套超参、训练 100 epoch、三次取平均插入位置特征图尺度mAP0.5mAP0.5:0.95参数量增量单张推理延迟同硬件基线无注意力-0.7120.458-12.3 msP3 之后80×800.7060.4510.6M15.1 msP4 之后40×400.7310.4722.1M14.8 msP5 / SPPF 之前20×200.7240.4658.4M14.2 msP4 P5 双插-0.7280.46910.5M16.4 ms几个有意思的观察P3 那个位置不仅没涨还略微掉了。我分析是因为 P3 的通道数最少这个配置下是 128而 CoTAttention 的注意力生成层输出是9*1281152通道等于在浅层强行做了个非常宽的变换信息被过度压缩又过度扩张反而引入了噪声。P5 位置参数量暴涨8.4M这是因为通道数大5129*5124608层的参数量是hidden * 4608一下就上去了。mAP 涨了但性价比一般。P4 位置是甜蜜点通道数适中特征图尺度也能覆盖到中等目标参数量增量可控。双插P4P5比单插 P4 还低了 0.3 个点这和前面说的不是越多越好是一致的。4.2 把 C3 内部的 Conv 换成 CoT 的代价与收益另一条思路是不新增模块而是替换——把主干里某些 C3/C2f 模块内部的 3×3 卷积换成 CoTAttention 的核心结构。这条路我试过一次感受是收益不稳定调参成本高。原因在于 C3 内部本来就有残差和分支结构你替换掉其中的卷积之后整个 block 的数值尺度和原来差别很大BN 的初始统计量对不上需要额外几十轮才能收敛。而且替换之后原主干的结构先验被破坏预训练权重不能直接加载了。如果你的场景很强依赖预训练权重比如数据量只有几千张我建议不要走替换路线走新增路线。新增模块可以随机初始化主干权重完整保留收敛快得多。4.3 小数据集上怎么识别过拟合信号这一点很实务。加了注意力模块之后模型容量变大小数据集上过拟合来得更早。几个可观测的信号训练 loss 和验证 loss 的分离点提前。基线可能在第 80 轮分开加了注意力之后在第 55 轮就分开了。验证集 mAP 曲线出现双峰。第一峰之后回落第二峰更低。分类分支的 loss 下降比回归分支快很多。说明模型在记忆类别标签而不是学定位。对应的处理手段按我实际使用效果排序加 EMA 缩短训练轮数这是最快的止损手段通常能拿回 0.5~1 个点提高weight_decay从 0.0005 调到 0.001配合 cosine 退火加强正则类数据增强比如扩充 Mosaic 的概率、加 RandomAffine 的旋转幅度降低注意力模块的学习率给 backbone 和注意力模块分组设 lr注意力模块的 lr 设成主 lr 的 0.5 倍第 4 条是我后来才想到的效果不错。原因是注意力模块是随机初始化的而主干是预训练的用同一个 lr 去更新注意力模块前期更新步长偏大容易把主干的稳定特征分布打乱。5. 训练链路里那些必须改的参数与排查路径5.1 学习率和 warmup 需要做怎样的修正默认配置大多是按纯预训练主干调的加了随机初始化的新模块之后需要做两处修正。一是延长 warmup。默认 warmup 通常是 1000 iter我习惯加到 1500~2000 iter。目的是让新模块在主干特征比较稳定的状态下逐步找到自己的位置而不是一开始就用大梯度去冲。二是考虑分组学习率。MMYOLO 的优化器配置支持paramwise_cfg可以按模块名给不同 lr。一个可用的写法optim_wrapper dict( typeOptimWrapper, optimizerdict(typeSGD, lr0.01, momentum0.9, weight_decay0.0005), paramwise_cfgdict( custom_keys{ cot: dict(lr_mult0.5), # 注意力模块降半速 backbone: dict(lr_mult0.8), # 主干略微降速 }, norm_decay_mult0.0, ), )这里cot这个 key 匹配的是模块名里包含 cot 的参数。所以你的 CoTAttention 模块命名要保证能被匹配到比如backbone.stage3_cot.key_embed.0.weight这种包含 cot 就能命中。norm_decay_mult0.0是常规操作——BN 的 weight 和 bias 不做 weight decay否则会把归一化的尺度压小。5.2 loss 震荡和显存溢出的定位顺序遇到 loss 震荡我的排查顺序是固定的第一步确认是不是 BN 的问题。把 batch size 临时改成 2 跑 200 iter如果震荡消失基本就是 batch 太小导致 BN 统计量不稳。解决办法是换 SyncBN 或者用更大的cumulative_iters梯度累加。第二步看震荡出现在哪个 loss 分量。MMYOLO 训练日志会分loss_cls、loss_bbox、loss_dfl三项。如果只有loss_bbox在跳那多半是回归分支的数值范围问题检查一下注意力输出的数值尺度可以在out x之后加个nn.LayerNorm或者简单除以个常数试试。如果是loss_cls在跳看是不是类别不平衡被放大了。第三步把注意力模块暂时关掉cot_indices()重跑。如果震荡消失那问题就在注意力模块里。这时候再单独测去掉 softmax 看看、去掉残差看看、把reduction从 4 改成 8 看看。逐个排除。显存溢出相对好定位。用torch.cuda.max_memory_allocated()打印峰值对比基线。如果峰值涨得比预期多多半是中间张量没及时释放——CoTAttention 里那个(B, C, k*k, H, W)的 reshape 会产生一个不小的中间张量在 P3 尺度上尤其明显。解决办法是把attention_embed的输出 reshape 改成 in-place 的 view 操作或者用torch.utils.checkpoint做梯度检查点代价是慢 20% 左右。5.3 验证阶段 mAP 反降时的排查清单这是最让人抓狂的情况训练 loss 明明降得更好验证 mAP 反而低了。我总结了一份排查清单按命中率排序排查项具体表现处理方式过拟合验证 loss 曲线抬头早加强正则、缩短训练、加 EMA预训练权重被破坏前 10 轮 mAP 明显低于基线检查是否误替换了主干层BN 统计量偏移训练时正常推理时崩检查track_running_stats确认model.eval()被调用注意力模块输出饱和softmax 输出接近 one-hot检查 softmax 前的 logits 量级考虑加温度系数尺度不匹配小目标 mAP 掉、大目标涨换插入位置往 P3 挪数据增强过强训练 loss 高但验证也高降低 Mosaic 概率我遇到过一次验证 mAP 反降 1.5 个点的情况最后定位到 softmax 输出饱和——注意力权重的 logits 量级到了 30 以上softmax 之后几乎变成 one-hot等于只关注了一个位置。在 softmax 前面除以一个温度系数比如 8就解决了mAP 反而涨了 0.8。这个细节在大部分教程里都不会提但它是真实存在的坑。6. 结构改完之后还得过的那几道关6.1 导出 ONNX 时会撞上的算子兼容问题训练跑通不等于能部署。CoTAttention 里有几个算子在导出 ONNX 时会出状况reshapepermute的组合。如果 reshape 的维度里有动态值比如从配置推导出来的而不是硬编码的常量ONNX 导出可能会失败或者导出一堆DynamicReshape节点影响推理速度。建议把k*k和C都写成常量不要用self.k2 * C这种动态计算而是直接在__init__里算好存成 int。mean操作。att.mean(dim-1)在 ONNX 里会转成ReduceMean这个算子在新版 opset 里支持良好但老版本opset 11 以下在 5 维张量上会出问题。导出时指定opset_version12以上比较稳妥。softmax 的 dim 参数。一定要显式指定dim-1不能省略。省略之后 PyTorch 会警告ONNX 导出时行为可能和预期不一致。导出验证的流程我一般是这样python tools/deploy.py \ configs/deploy/yolov8_s_cot.py \ work_dirs/yolov8_s_cot/best.pth \ demo/demo.jpg \ --work-dir work_dirs/deploy \ --device cuda:0 \ --dump-info然后用onnxruntime跑一遍推理和 PyTorch 的输出做逐元素比对。经验阈值是最大绝对误差 1e-3超过这个数就说明某一步的数值精度出了问题通常是某个卷积的 padding 或者算子顺序对不上。6.2 FLOPs 和延迟的真实增长到底有多少这里有个容易被忽略的点FLOPs 涨了 5% 不代表延迟涨了 5%。CoTAttention 里有很多小卷积1×1和元素级操作这类算子在 GPU 上的计算效率并不高实际延迟增长通常会比 FLOPs 增长更明显。我实测的一组数据同一硬件、batch1、TensorRT FP16配置GFLOPs延迟ms延迟增幅基线8.712.3-P4 单插9.610%14.820%明显高于 FLOPs 增幅P4 单插 算子融合9.613.16.5%融合后大幅改善算子融合指的是把Conv BN在推理前 fuse 掉。MMYOLO 的部署工具默认会做这个但如果你自己手写推理脚本忘了这一步延迟会很不理想。另一个优化点attention_embed里的ReLU可以换成SiLU或者干脆去掉对精度影响很小但能让 TensorRT 少一层。我在一次边缘端部署里就这么干了延迟降了 0.6msmAP 掉了 0.1可以接受。6.3 实验记录怎么记才真的能复现最后说个方法论的事。做结构改进实验最怕的是三个月后别人问你你那个 P4 的版本具体是怎么配的而你翻遍笔记也说不清。我现在的记录模板长这样每次实验填一遍exp_id: cot_p4_v3 date: 2024-xx-xx base_config: yolov8_s_syncbn_fast_8xb16-500e_coco.py backbone: CoTDarknet cot_indices: [3] cot_kernel_size: 3 cot_reduction: 4 dataset: 自建检测集train 24800 / val 3100 batch_size: 16 lr: 0.01 (cot 分支 x0.5) warmup: 2000 iter epochs: 100 seed: 2024 results: mAP50: 0.731 mAP50_95: 0.472 params: 11.9M latency_ms: 14.8 notes: 第 55 轮出现验证 loss 抬头启用 EMA 后稳定关键是要记随机种子和cot 参数的具体值。我一开始没记种子后面复现的时候连续三次跑出来的 mAP 差了 0.4以为是改坏了折腾一下午才发现是随机性导致的。还有一个小建议把cot_indices、kernel_size、reduction这些改成配置文件的顶层变量而不是写死在 backbone 类的默认参数里。这样一套代码配不同配置文件就是不同的实验做消融时改一行配置就能跑不用碰 Python 代码效率高一个档次。真正跑下来CoTAttention 这个改动给我的最大感受不是那 1.9 个点的 mAP 提升而是它逼着我把整个改结构 → 训练 → 验证 → 导出的链路捋了一遍中间暴露出来的通道对齐问题、BN 稳定性问题、ONNX 算子问题在后续做别的模块尝试时全都复用上了。如果你也在做类似的结构改进建议先从小规模数据、短训练轮次跑一轮端到端流程把链路打通再上正式实验——我见过太多人一上来就冲 500 epoch跑到第 200 轮报个形状错误前期时间全白费。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →