1. 先把 SPAR 类比推理的定位搞清楚
小目标检测这件事,我做了几年,最大的感受是:它不难在“检测”,难在“小”。同样一个网络,正常尺寸的目标能稳定跑到 0.4 以上的 AP,换到面积小于 32×32 的那一档,指标直接掉到 0.15 上下,而且你换 backbone、加数据、调学习率,收益都不明显。原因不复杂——小目标在主干网络里能留下的像素太少,下采样几次之后,特征图上一个目标可能就剩一两个格子,分类和回归都在“凭感觉猜”。SPAR 类比推理模型之所以值得拿出来单讲,就是因为它换了一条思路:不去硬抠这个小目标本身的绝对特征,而是去学“目标和它周围环境之间的关系模式”,再用这种关系模式去做推断。这套思路在小目标、少样本、跨域迁移这几个场景里都能挂上去用,适合已经能跑通常规检测流程、但被小目标指标卡住的同学参考,也适合刚接触类比推理、想知道这东西落地长什么样的朋友。
先把话说明白:SPAR 这个缩写在不同文献里对应过不同全称,我这里拆解的是“结构化关系表征 + 类比推理”这一条主干思路,也就是把一个四元组 A:B::C:D 的关系结构学出来,再迁移到新样本上。标题里同时挂了“常见小目标检测方法总结”,说明这两块是被放在一起看的——它们不是并列关系,而是一个提出问题、一个提供新解法。下面的内容里,凡属于原文没写死、由我按常见工程实践补全的部分,我都会明确标出来,避免把推测当结论。
1.1 类比推理的数学骨架:从四元组到关系向量
类比推理最原始的形式就是 A 之于 B,如同 C 之于 D。落到模型里,它不是一个端到端的分类器,而是三段式的:先把 A、B 各自编码成特征,再算出一个“关系向量”,最后把这个关系向量作用到 C 上,去预测 D。写成式子大致是 r = φ(E(A), E(B)),D̂ = ψ(E(C), r),其中 E 是共享权重的编码器,φ 是关系聚合模块,ψ 是解码或检索模块。
这里面最容易被新手忽略的一点是:φ 的设计决定了整个模型的上限。如果 φ 只是简单做减法或者拼接,那模型学到的就是“A 和 B 差多少”,这种线性关系在视觉任务里非常脆弱,光照一变、尺度一变就失效。实践里更稳的做法是用交叉注意力,让 B 的每个 token 去查询 A 的所有 token,得到一组带权的关系描述,再池化成固定长度的关系向量。这样学到的是“B 相对于 A 在哪些维度上发生了变化”,而不是一个死板的差值。
还有一个细节:E 必须共享权重。我见过有人给 A、B、C、D 分别建了四个编码分支,训到最后模型根本学不出可比的关系空间,因为四个分支的特征根本不在同一个坐标系里。共享编码器是类比推理能成立的前提,这点在改网络结构时千万别动。
损失函数一般由三块拼起来:关系一致性损失(让同一类关系在不同样本对上的向量尽量靠近)、重构损失(从关系向量和 C 恢复 D 的特征)、以及一个对比损失(把不匹配的 D 推开)。温度系数 τ 一般取 0.05 到 0.1,我实测 0.07 是比较舒服的起点。
1.2 为什么小目标检测特别需要“关系”而不是“绝对特征”
这里打个生活化的比方。你手里有一张很糊的合影,要找出“某人的弟弟”。单看脸,糊成一片,认不出来;但如果告诉你“他比他哥矮半个头,站在他哥左边”,你反而能锁定。小目标检测的困境和这个一模一样:目标本身的像素信息已经退化到不可靠,但它和周围物体的相对位置、相对大小、遮挡关系,在这些退化条件下反而更稳定。
从网络层面看更直观。假设输入 640×640,主干下采样 32 倍,得到 20×20 的特征图;一个 16×16 像素的小目标,映射到这个尺度上还不到一个格子,回归分支连边界在哪都说不清。但如果这个目标总是出现在某个大目标的右下方、并且宽度大约是大目标的 1/5,那这种比例关系在三十二倍下采样之后依然能保留下来,因为它是相对量,不随分辨率绝对值衰减。
我在一个工业零件缺陷检测的项目里验证过这点:缺陷本身只有十几个像素,但每个缺陷都紧贴零件边缘、且与边缘的夹角在固定范围内。把这种“相对边缘的位置关系”显式建模进特征里,比单纯把输入分辨率翻倍带来的收益还大,而且推理成本只增加了不到 15%。这也是我后来愿意花时间去啃类比推理的原因——它提供的正是这种“关系层面”的表达能力。
1.3 SPAR 的整体结构:四个模块与数据流
按我复现时拆出来的结构,SPAR 大致分四块。第一块是共享特征提取器,通常直接拿现成的检测主干,取 P3 到 P5 三层特征做融合,保留一定的空间分辨率;第二块是关系编码器,负责把支持对 (A, B) 压成一个关系向量,我一般用两层交叉注意力加一个池化头;第三块是投影头,把关系向量映射到一个归一化的嵌入空间,方便后续做相似度检索;第四块是匹配与解码模块,把查询样本 C 的特征和关系向量拼起来,输出 D 的位置偏移和类别。
数据流走一遍是这样的:支持集里的一对样本先过主干,得到多尺度特征;关系编码器在每一层上分别算关系,再跨层融合成一个统一的关系向量;查询样本 C 过主干后,与关系向量做交叉调制,输出预测。整个流程里,关系向量相当于一个“提示”,告诉模型“这次要关注什么样的变化模式”。
要注意的是,训练阶段和推理阶段的输入形式不一样。训练时四元组是齐的,可以算重构损失;推理时只有 C,D 是靠关系向量检索出来的。这种不对称性会导致一个典型问题:训练时模型偷懒,直接从 D 的特征里反推答案,而不是真的去用关系向量。解决办法是在训练时对 D 的特征做随机遮挡或者加噪,强制模型依赖关系通路。这个技巧我在三个不同的类比推理任务上都用过,效果稳定。
2. 常见小目标检测方法的分层拆解与选型逻辑
在把 SPAR 接进流程之前,得先把手头这套小目标检测的活儿摸清楚。市面上讲小目标的方法很多,但真正落到工程里,能动的其实就四个层面:特征层、标签分配层、数据层、推理层。这四层的作用机制完全不同,混着改最容易出现“改了一堆、指标不动”的情况。
2.1 特征层:分辨率、感受野与上下文的三方拉扯
特征层是最直观的一层,核心矛盾是分辨率。目标小,就得要更高分辨率的特征图,但分辨率一高,显存和计算量就顶不住,而且高分辨率层的语义信息很弱,误检会明显上升。常见的折中方案是保留 P2 层(步长 4),只在 P2 上挂一个小目标专用的检测头,其余层不动。我实测下来,P2 头的收益在小目标上大概能带来 1.5 到 3 个点的 AP_S,代价是显存涨 20% 左右。
第二条路是超分辨率辅助。做法是先对输入做一次轻量的超分,再送进检测网络。这条路听起来美好,实际坑不少:超分模型自己会产生伪影,伪影在检测网络里会被当成纹理特征,导致误检率上升。我的经验是,如果一定要用,别用生成式的超分,用简单的双三次插值配合一个可学习的锐化卷积,效果更可控。
第三条路是注意力与上下文建模。小目标的判别信息很大一部分来自周围环境,所以像非局部注意力、可变形卷积这类能扩大有效感受野的模块,在小目标上往往有正收益。但要注意,注意力模块加在浅层比加在深层更有效,因为浅层还保留着空间细节,深层加注意力基本是在做语义重排,对小目标帮助有限。
第四条路是多尺度融合的改进。标准 FPN 是自顶向下单向融合,浅层拿到的语义信息其实很有限。换成双向融合或者带权重的自适应融合(每个尺度学一组融合权重),在小目标上通常还能再榨出零点几个点。这块的性价比挺高,改动量小,风险低,建议优先试。
2.2 标签分配与损失函数:小目标真正吃亏的地方
这一层是我认为收益最高、也最容易被忽视的。小目标的问题不只是特征弱,更关键的是正样本太少。一个 16×16 的目标,在步长 8 的特征图上只占 2×2 个格子,按传统的 IoU 阈值分配,能分到的正样本可能就三五个。正样本一少,回归分支的梯度就特别稀疏,模型学出来的框要么偏大要么偏小。
解决办法有两条。一条是改分配策略,用自适应的方式给每个目标分配正样本,比如基于统计量确定阈值的那类方法,能让每个目标至少分到十几个正样本。另一条是改回归损失,别用纯 IoU 系列的损失。小目标对位置偏移极度敏感,框偏一个像素,IoU 就掉一大截,梯度会剧烈震荡。这时候用带高斯建模的损失(把框建模成二维高斯分布,用分布距离代替重叠度)会稳很多。
具体来说,归一化 Wasserstein 距离这类损失对小目标的友好度明显高于 GIoU。它的核心是把框的中心点和宽高分别建模,计算两个高斯分布之间的距离,再取指数归一化。常数 C 的选取很关键,一般取数据集里目标平均绝对尺寸的量级,小目标数据集上取 10 到 15 之间比较合适。我试过 C 取太小,损失对小目标过度敏感,训练前期直接发散;取太大又退化成近似 IoU,白改。
分类损失这边,Focal Loss 依然是标配,但小目标场景下 γ 别设太大,1.5 到 2.0 之间就够了。γ 设到 3 以上,模型会把大量小目标当成难负样本压掉,召回率掉得很厉害。另一个值得加的是质量感知的分类损失,把预测框和真值的匹配质量乘到分类目标上,让分类分支也感知定位质量,对小目标的 AP 提升有实证效果。
2.3 数据层与推理层:最便宜也最有效的两招
数据层的核心思路是“人为把小目标变大或者变多”。Mosaic 类的增强把四张图拼一张,等价于缩小了目标尺度,这对小目标其实是反向操作,所以小目标任务里 Mosaic 的概率要调低,我一般设 0.3 到 0.5。与之相对,Copy-Paste 类的增强是把小目标抠出来贴到别的图上,直接增加了小目标的样本数量和出现位置,这条对小目标非常有效,但要注意粘贴时的尺度一致性和光照一致性,贴得不自然反而引入噪声。
推理层的杀招是切片推理。把大图切成小块分别检测,再把结果拼回去。这块的参数计算有讲究,不能拍脑袋定。假设原图 4000×3000,切片 640×640,重叠率 0.2,那么步长就是 640×(1-0.2)=512。横向切片数 = ceil((4000-640)/512)+1 = ceil(6.56)+1 = 8;纵向 = ceil((3000-640)/512)+1 = ceil(4.61)+1 = 6;总共 48 张切片。如果重叠率提到 0.3,步长变 448,横向变成 ceil(3360/448)+1 = 8+1 = 9,纵向 ceil(2360/448)+1 = 6+1 = 7,总共 63 张,推理耗时涨了三成多,但边缘目标的召回提升有限。所以重叠率 0.2 是我常用的甜点值。
切片推理的坑在于拼接。重叠区域的重复框要靠 NMS 合并,但不同切片里的同一个目标,置信度可能差很多,NMS 阈值设高了会留下重复框,设低了会把相邻的两个小目标误合并。我的做法是对重叠区域做加权融合,越靠近切片中心权重越高,然后再统一做一次 NMS,阈值放到 0.6 左右。
2.4 四层方案的收益与代价对照
| 层面 | 典型手段 | 小目标 AP 预期收益 | 主要代价 | 落地难度 |
|---|---|---|---|---|
| 特征层 | 增加 P2 检测头 | +1.5 ~ 3.0 | 显存 +20%,误检上升 | 低 |
| 特征层 | 双向/加权特征融合 | +0.5 ~ 1.5 | 参数量小幅增加 | 低 |
| 特征层 | 浅层注意力/可变形卷积 | +0.8 ~ 2.0 | 推理延迟 +10% | 中 |
| 标签分配 | 自适应正样本分配 | +1.0 ~ 2.5 | 训练前期不稳定 | 中 |
| 损失函数 | 分布距离回归损失 | +1.0 ~ 2.0 | 需调常数 C | 低 |
| 数据层 | Copy-Paste 增强 | +1.0 ~ 2.5 | 粘贴不自然引入噪声 | 中 |
| 推理层 | 切片推理 | +3.0 ~ 8.0 | 推理耗时翻数倍 | 低 |
| 推理层 | 多尺度测试增强 | +0.5 ~ 1.5 | 耗时 ×3 | 低 |
这张表的使用方式是:先做低难度高收益的(P2 头、切片推理、换损失),再看中难度的。别一上来就堆注意力模块,那是最容易白干的一层。
3. 把 SPAR 接进检测流程的完整实操
前面铺垫了这么多,现在进入正题:SPAR 怎么和小目标检测拼到一起。我的做法不是把 SPAR 当成一个独立的检测器,而是把它当成一个“关系增强模块”挂在现有检测流程旁边,主检测头负责常规目标,SPAR 分支负责给小目标提供关系层面的补充信号。这样做的理由是,纯用类比推理做检测,在样本量不足时非常不稳,而把它作为辅助分支,风险可控,收益也看得见。
3.1 数据构造:类比四元组怎么造才不废
这是整个流程里最耗时的一步,也是最容易做错的一步。类比四元组的构造逻辑是:A 和 B 构成一个“变化关系”,C 和 D 要构成同一种变化关系。落到小目标检测上,我用的构造方式有三种。
第一种是基于尺度的。A 是某个目标在正常尺度下的裁剪,B 是同一个目标缩小后的裁剪,关系就是“尺度缩小 N 倍”;C 是另一个类别目标的正常尺度裁剪,D 就是它缩小 N 倍后的样子。这种四元组能教会模型“尺度变化”这个关系,对多尺度检测的泛化有帮助。
第二种是基于上下文的。A 是目标单独出现,B 是同一目标带上一块固定的邻域上下文,关系是“增加邻域”;C 换一个目标,D 就是它带上对应邻域。这种构造直接对应小目标最缺的上下文信息。
第三种是基于位置的。A 是目标位于图像中心,B 是目标位于图像边缘,关系是“位置偏移”;C 和 D 同理。这条对切片推理特别有用,因为切片边缘的目标经常被截断。
构造时要注意两个硬性约束:一是同一四元组里的 A、B、C、D 必须来自同一批次的数据增强参数,否则光照、色彩抖动会污染关系;二是每个四元组里只能有“一种”变化关系,如果 A→B 既变了尺度又变了位置,模型学到的关系向量会混杂,下游效果会明显变差。我一开始图省事,让增强随机组合,结果关系向量的类内方差特别大,检索准确率只有六成多,改成单一变化之后提到了八成五以上。
3.2 关系编码器与投影头的实现
代码层面,关系编码器的核心是交叉注意力。下面这段是我常用的骨架,PyTorch 写法,去掉了具体业务相关的部分,可以直接拿来改。
import torch import torch.nn as nn import torch.nn.functional as F class RelationEncoder(nn.Module): def __init__(self, dim=256, heads=8, depth=2): super().__init__() self.layers = nn.ModuleList([ nn.TransformerDecoderLayer(d_model=dim, nhead=heads, dim_feedforward=dim * 4, batch_first=True) for _ in range(depth) ]) self.norm = nn.LayerNorm(dim) self.pool = nn.AdaptiveAvgPool1d(1) def forward(self, feat_a, feat_b): # feat_a / feat_b: [B, N, C],N 为展平后的空间位置数 r = feat_a for layer in self.layers: r = layer(r, feat_b) # 用 B 的 token 查询 A r = self.norm(r) r = self.pool(r.transpose(1, 2)).squeeze(-1) return F.normalize(r, dim=-1) # 归一化到单位球面 class ProjectionHead(nn.Module): def __init__(self, in_dim=256, out_dim=128): super().__init__() self.net = nn.Sequential( nn.Linear(in_dim, in_dim), nn.GELU(), nn.Linear(in_dim, out_dim) ) def forward(self, r): return F.normalize(self.net(r), dim=-1)这里有两个实现细节值得说。第一,F.normalize一定要加,而且关系向量和查询特征都要归一化到单位球面,否则相似度会被向量模长主导,学出来的关系毫无可比性。第二,Transformer 解码层的顺序别搞反,是把 B 当 query、A 当 key/value,这样得到的才是“B 相对于 A 的变化”,反过来就变成“A 相对于 B 的变化”,语义完全是两回事。
投影头的作用是把关系向量压到一个紧致的检索空间。out_dim 不必太大,128 维足够,太大反而容易过拟合。投影头后面一般接一个温度缩放的对比损失,把正样本对拉近、负样本对推远。
3.3 三阶段训练与损失配比
训练不能一把梭,我分成三个阶段。第一阶段只训主干和检测头,把基础检测能力打扎实,这一步和常规训练没区别,按正常的学习率策略走。第二阶段冻结主干,只训关系编码器和投影头,用构造好的四元组做对比学习,学习率设成主干的 5 到 10 倍,因为这部分是随机初始化的,需要更快收敛。第三阶段解冻全部,用很小的学习率(主干的 0.1 倍)做联合微调,让关系分支和检测分支对齐。
损失配比这块,我用的是分类 1.0、回归 2.0、关系对比 0.5、特征对齐 0.3。关系对比损失的权重不能给太高,超过 1.0 之后检测指标会开始掉,因为模型会过度优化关系空间,把检测特征带偏。特征对齐损失的作用是让关系分支输出的查询特征和检测头的输入特征尽量一致,避免两个分支各自为政。
还有一个实操细节:第二阶段的数据加载器要和第一阶段分开,因为四元组构造需要成对的样本,用常规的 batch 采样拿不到。我的做法是自定义一个 Dataset,每个样本返回一个四元组,batch 内部再随机打乱 A/B 和 C/D 的配对来做负样本。这样负样本的构造是零成本的。
3.4 关键参数的计算过程
参数这块我不喜欢给一堆“经验值”就完事,还是把算法写清楚,大家按自己的数据算。
先说切片尺寸。切片的选取原则是让切片内的目标尺度分布尽量接近训练集。具体做法是统计训练集里目标框的宽高分布,取 95 分位数作为参考目标尺寸 S_ref,然后切片边长设成 S_ref 的 3 到 4 倍。比如训练集里目标宽高 95 分位是 160 像素,那切片边长设 512 到 640 之间都合理,我一般取 640,留出足够上下文。
再说重叠率。重叠率的下限由最大目标尺寸决定:重叠区域的宽度必须大于数据集中最大目标的宽度,否则跨越切片边界的目标会被切成两半,两边都检不出来。设最大目标宽度 W_max,切片边长 T,重叠率下限就是 W_max / T。如果 W_max 是 200,T 是 640,下限就是 0.31。这个时候就得把重叠率提到 0.3 以上,或者干脆把 W_max 那批大目标交给全图推理分支处理。很多人做切片推理时 AP 反而下降,八成是踩了这个坑。
再说分布距离损失里的常数 C。它的物理含义是“多大的中心偏移算一个标准单位”。取值方法是统计训练集里所有目标框的平均绝对尺寸,即 (w+h)/2 的均值,然后 C 取这个值的一半到一倍之间。我一般直接用均值,效果稳定。取完之后可以用前 100 个 iteration 的损失值检查一下,如果损失一直在 0.9 以上不降,说明 C 取小了,梯度太弱;如果损失瞬间掉到 0.1 以下,说明 C 取大了,退化成近似 IoU。
最后是关系对比损失的温度系数。这个没有解析解,只能网格搜,范围 0.05 到 0.15,步长 0.02。搜的时候不要只看对比损失本身,要看下游的 AP_S,因为对比损失小不代表关系空间好用。
3.5 推理阶段的双分支融合
推理阶段,我的流程是全图分支和切片分支并行跑,然后统一融合。全图分支负责中大目标,切片分支负责小目标,融合时按目标面积做门限:面积小于 32×32 的,以切片分支结果为主;大于 96×96 的,以全图分支为主;中间的按置信度加权。
SPAR 分支在这里起的作用是给切片分支的结果加一道关系校验。具体做法是,对每个候选小目标,取它周围固定邻域的特征,和关系向量一起过匹配模块,输出一个 0 到 1 的关系一致性分数,把这个分数乘到原有置信度上。这一步能有效压掉一批“长得像但关系不对”的误检。我在一个包含大量规则排列零件的场景里测过,误检率降了将近三成,召回几乎没掉。
融合之后还要做一次跨切片的框合并。这里我不用标准 NMS,用前面提到的中心加权融合:对重叠区域里的同类框,按到切片中心的距离做高斯加权,算一个加权平均框。这样做的效果比 NMS 稳,尤其是在目标被切片边界切开的场景下。
4. 排查实录:那些指标不动的真实原因
4.1 小目标检测与 SPAR 联合调试速查表
| 现象 | 最可能的原因 | 排查动作 | 处理方式 |
|---|---|---|---|
| AP_S 长期不动 | 正样本数量不足 | 统计每个 GT 分配到的正样本数 | 换自适应分配,或降低分配阈值 |
| 训练 loss 正常但 AP_S 低 | 回归损失对小目标过敏感 | 看不同尺寸目标的分项损失 | 换分布距离损失,调常数 C |
| 切片推理后 AP 反降 | 重叠率小于最大目标宽度占比 | 算 W_max/T 的比值 | 提高重叠率,或大目标交给全图 |
| 关系检索准确率高但检测没提升 | 关系分支与检测分支特征不对齐 | 比对两分支的特征分布 | 加特征对齐损失,第三阶段微调 |
| 误检集中在规则纹理区域 | 关系向量被纹理主导 | 可视化关系注意力图 | 加纹理抑制增强,降低关系损失权重 |
| 小目标召回高但精度低 | 切片边缘截断目标产生重复框 | 统计框中心到切片边界的距离分布 | 用中心加权融合替代 NMS |
| 训练前期直接发散 | 温度系数过小或学习率过大 | 看前 50 iter 的损失曲线 | τ 提到 0.1,学习率降一半 |
4.2 三个我亲自踩过的坑
第一个坑是关系向量的“捷径学习”。训练到中期,关系检索的准确率能到九成,但下游检测一点没动。查了两天才发现,模型在训练时直接从 D 的特征里反推出了答案——因为 D 的特征在编码后还残留着类别信息,模型根本不需要关系向量就能做对。解决办法是对 D 的特征做随机通道遮挡,遮挡比例 30% 左右,逼着模型走关系通路。改完之后检索准确率掉到八成出头,但下游 AP_S 涨了两个多点。这件事让我记住一个判断标准:辅助任务的指标涨得太顺,往往说明它在偷懒。
第二个坑是切片推理的边界效应。一开始我用的是标准切片加 NMS,结果切片边缘的目标要么漏检,要么出现两三个重叠框。后来统计了一下,漏检的目标里有七成中心点距离切片边界不到 20 像素。改成重叠率 0.3 加中心加权融合之后,这部分漏检基本消掉了。代价是推理耗时涨了四成,但对于离线批处理的场景完全能接受。
第三个坑是损失配比。我一开始把关系对比损失的权重给到 1.0,想着多学点关系总没坏处,结果检测指标掉了三个点。逐层看了梯度才发现,关系分支的梯度反传回主干后,把主干特征往“关系可分”的方向拉,而这个方向和“检测可分”的方向并不一致。权重降到 0.5 之后两边就平衡了。这件事的教训是,辅助分支的权重不是越大越好,它本质上是在和主任务抢主干的表达能力。
4.3 评估环节容易被忽略的细节
小目标的评估指标一定要分尺寸档看,只看总 AP 会掩盖问题。我一般至少看三档:小于 32×32、32 到 96、大于 96。如果小目标档涨了但中档掉了,说明模型在“把中目标当小目标处理”,这时候要检查特征融合的权重是不是偏了。
另一个细节是标注质量。小目标的标注误差在大目标上可以忽略,但在小目标上是致命的。一个 16×16 的目标,标注框偏 3 个像素,IoU 就从 0.8 掉到 0.6 以下,模型学到的东西全是噪声。我在做小目标项目时,会先抽 200 个样本做标注复核,把明显偏移的框修一遍再训。这一步花的时间不多,但对最终指标的影响经常超过调参。
还有一点,验证集里的极小目标(比如小于 8×8)建议单独统计,不要混进主指标。这类目标在很多场景下本来就检不出来,混进去只会拉低整体数字,让你误判模型退化了。
5. 从单点验证到批量落地,我的推进节奏
这套东西我从最初跑通到现在能稳定用在项目里,大概花了小半年,中间走过不少弯路。回头看,最有价值的不是最后那个融合方案,而是推进的节奏。我把它拆成四步,每步都有明确的验收标准,任何一步没过就不往下走。
第一步是纯检测基线。不加任何小目标专属改动,先把常规检测跑通,记录分尺寸档的 AP。这一步的验收标准是训练稳定、没有明显过拟合,小目标档的 AP 哪怕只有 0.1 也算过关。很多人跳过这步直接上复杂方案,最后出了问题根本不知道是哪一块导致的。
第二步是单个模块验证。从收益最高、改动最小的开始,我选的是 P2 检测头加分布距离损失。每加一个模块,单独训一次,记录增量和副作用。这一步的关键是“一次只改一处”,我见过有人一次改五个地方,指标涨了不知道是谁的功劳,指标掉了也不知道该回退哪个。验收标准是每个模块的增量可复现,且在验证集和测试集上方向一致。
第三步是关系分支的接入。这一步风险最高,因为引入了新分支和新损失。我的做法是先在不影响主检测的前提下,把关系分支当成一个独立的对比学习任务训起来,确认关系检索的准确率能到八成以上,再接入联合训练。接入后如果主检测指标掉超过一个点,就说明融合方式有问题,这时候优先降关系损失的权重,而不是去改网络结构。
第四步是推理链路优化。切片推理的耗时要实测,不能拍脑袋。我会在不同分辨率、不同切片参数下各跑一轮,画出耗时和指标的曲线,找拐点。多数场景下,重叠率从 0.2 提到 0.3 带来的指标增益已经很小,但耗时涨得明显,这时候就停在 0.2。
最后分享一个我觉得挺实用的小习惯:每次实验都保存关系向量的可视化结果,不用多,随机抽 20 个样本把关系向量的降维散点图画出来。这个东西在指标正常的时候看着没什么用,但一旦指标异常,看一眼散点图基本就知道是关系空间塌了还是类别混了,比翻日志快得多。我现在的习惯是训练脚本里直接挂一个回调,每 10 个 epoch 存一张图,事后回溯特别方便。