
近两年做目标检测的人应该都有同感模型越来越大、指标越刷越高但一旦把模型部署到真实场景漏检的总是那些“没有见过”的类别。YOLO系列把速度做到极致DETR也把NMS和锚框丢进了历史垃圾桶但大家都绕不开一个前提——训练之前类别列表已经被写死。WeDetect是我们团队在做的内部项目核心思路不复杂把目标检测当成一次视觉-语言检索。用户给一句描述比如“一只站在电线上的鸟”模型不再去分类整张图里有什么而是检索这句话对应的图像区域然后返回一个框。这篇文章把我们从原理拆解、训练细节到落地踩坑的过程完整捋一遍适合正在做目标检测、开放词汇检测、多模态项目或者被长尾类别和小目标检测折磨得够呛的算法工程师参考。1. 当目标检测从“分类器”变成“搜索引擎”1.1 锚框和查询向量解决不了的那类漏检传统目标检测流程大家都很熟悉YOLO先把图像切分成网格再用预设锚框去匹配目标每个锚框输出一个类别概率分布和边界框偏移量。DETR看起来更简洁去掉锚框和NMS用一组object queries让Transformer自己去“学习应该关注哪里”但本质上它还是在和固定数量的潜在物体做匹配。这些方法有一个共同点类别数量在训练时已经锁死。你训练了COCO的80类部署到现场如果出现第81种物体输出层根本没有对应神经元。哪怕用上多模态目标检测的预训练特征模型能识别的词表依然受训练数据约束。WeDetect换了一个问法把分类头删掉换成文本编码器用描述性文本作为条件让检测变成“找到与这段话匹配的像素区域”。这个转变带来的真正变化不是“多支持几个类”而是把目标检测从封闭集合分类问题彻底变成了开放集合的对齐问题。用户可以在推理时任意给一段文本无论是一个类别名还是一整句自然语言都不需要重新训练模型。长尾类别、小样本类别甚至训练时从未见过的新组合都有机会被检测出来。当然不是所有描述都能检准这跟文本表示能力和训练数据配比有很大关系后面我会详细说。1.2 WeDetect到底在检索什么很多人第一次听到“视觉-语言检索”会有一个误解以为是用图像去搜图库或者用文字去搜相似图片。WeDetect里说的检索是建立“图像区域”与“文本短语”之间的对应关系。具体而言图像侧会输出一组区域候选特征文本侧会输出一个句子或短语的特征然后计算两者之间的相似度。相似度高的区域就是这句描述所在的位置相似度低的位置就是不可用的负样本。整个过程可以理解成“区域找词词找区域”。这种设计与传统目标检测器的区别在于传统检测器在分类阶段用的是线性分类头每个类别对应一个固定向量WeDetect在匹配阶段用的是文本编码器动态生成的表示类别信息被压缩在文本特征里而不是模型参数里。因此改变类别描述不会改动检测头的任何权重只需要重新编码文本。它还顺带解决了“类别语义重叠”的问题比如“人”“行人”“路人”在传统检测器里是三个独立类别互相竞争在检索范式里它们可以指向同一种视觉概念甚至能通过描述模板融合成一个更鲁棒的提示。2. 拆开WeDetect检索式检测的模块与数据流2.1 文本提示编码器与类别描述库WeDetect的文本侧通常使用预训练视觉-语言模型里的文本编码器例如CLIP的text encoder也可以换更轻量的Sentence-BERT。选择编码器的关键不是参数量而是它的文本表示和图像表示是否已经被预训练对齐过。如果随机初始化一个BERT再接MLP图像和文本的相似度根本没有统一尺度跨模态对齐几乎要从零学起训练成本会高好几个量级。描述库的构建也很关键。最简单的方式是把类别名直接作为文本提示比如“bird”“person”。但实测中类别名带来的歧义非常大同一个词在不同视觉语境下可能对应完全不同的概念。所以我们会为每个类别生成一组描述模板。举个例子做鸟类目标检测的数据集时不只写“bird”而是写成“一只站在树枝上的鸟”“一只翅膀展开正在飞行的鸟”“一只嘴部特写的鸟”。模板可以由领域专家手工写也可以让大语言模型批量生成。但注意模板之间的语义不能重叠太多否则文本特征会拥挤在一个很窄的区域里检索时区分度会明显下降。文本编码器的输出会做L2归一化这样所有描述向量都落在同一个单位超球面上方便和图像特征计算余弦相似度。我建议把类别描述库单独缓存成二进制文件推理时直接加载避免每次启动都重新编码文本。对视觉-语言模型来说文本编码耗时通常比图像编码短不了多少缓存之后启动时间能省下一大半。2.2 跨模态对齐模块让框和词出现在同一空间图像侧的处理比传统检测器多一步不是直接从CNN输出feature map接分类和回归头而是从feature map里采样一组区域候选。WeDetect的做法和DETR类似先用一个轻量区域提议模块产生一组候选框每个候选框区域内部再通过ROI Align或可变形注意力提取视觉特征向量。之后这组视觉特征和文本token一起送入跨模态Transformer通过self-attention和cross-attention做信息交互。这个跨模态对齐模块是整条链路的核心。因为简单地把图像特征和文本特征拼接后再做点积很难处理“一个描述涉及多个物体”的情况。比如输入“桌子上的杯子和旁边的手机”这句话里有两个实体理想的输出应该有两个框。如果视觉特征和文本特征没有深度交互模型很难判断哪个区域该分配给哪个词。加上cross-attention之后模型可以学会“杯子”这个token去重点关注杯子的区域“手机”这个token去关注手机的区域从而实现同一个描述下的多目标联合检索。我在实际训练中发现跨模态Transformer的层数不必太深4到6层足够。层数过深很容易在小数据集上过拟合而且推理速度会成倍下降。相反图像骨干网络的选择对精度的影响更大。如果追求速度用ResNet50如果追求开放词表的泛化能力用Swin Transformer或ConvNeXt效果会更好。2.3 从相似度矩阵到边界框输出训练时WeDetect会为每张图生成若干文本描述并与区域候选计算相似度矩阵。推理时则使用预先定义好的类别文本集合或者用户实时输入的自然语言查询。输出层要做的事情非常朴素对每个区域候选计算区域特征和所有文本特征的余弦相似度超过阈值就保留该区域。如果多个文本同时匹配同一个区域就选分数最高的类别再对同一类别的框做非极大值抑制。这里的边界框回归和传统检测器有个细微差别区域提议本身就需要回归一次。很多检索式检测方法把回归标签定义为“文本对应的真实框”和“提议框”之间的差值然后让回归头直接预测偏移量。为了让回归任务和对齐任务共享同一套语义特征我们会在得到跨模态特征之后额外加一个轻量回归头而不直接在视觉特征上回归。这一步如果省略训练时图像侧梯度和文本侧梯度容易互相干扰表现为loss下降很快但检测框始终在抖动。我提供一个推理侧的简化伪代码方便理解整体数据流# text_encoder: CLIP text encoder # image_encoder: ResNet/Swin backbone # proposal_head: region proposal network # cross_modal: transformer fusion # box_head: bounding box regression text_feats text_encoder(class_descriptions) # [C, D] img_feats image_encoder(image) # [H, W, D] proposals proposal_head(img_feats) # [N, 4] region_feats ROI_Align(img_feats, proposals) # [N, D] fused_feats cross_modal(region_feats, text_feats) # [N, D] # 文本特征和区域特征的相似度 scores fused_feats text_feats.T # [N, C] keep torch.nonzero(scores threshold) boxes box_head(fused_feats)[keep]这套流程最舒服的一点是N和C的大小在训练和推理时都可以动态变化不需要重新设计网络结构。于是“开放词表”变成了一个工程配置问题而不是网络结构问题。3. 训练和推理里那些决定成败的细节3.1 数据配比检测标注、图文对与模板文本怎么搭WeDetect的训练数据最好同时包含两类。一类是有边界框和类别标注的检测数据比如COCO、LVIS它们告诉模型“区域和类别名的对应关系”。另一类是只有图像和文本描述的图文对数据比如COCO Captions、Conceptual Captions它们告诉模型“更自然、更丰富的语言表达长什么样”。如果只有检测数据模型能学会把“bird”这个单词和区域关联但对“站在树枝上的褐色小鸟”这种复杂描述会束手无策如果只有图文对数据模型有语言理解能力却不知道如何输出精确边界框。我们实践下来比较稳的配比是检测数据与图文对数据按1:1或2:1混合。如果图文对数据占比太高模型会对自由文本过度敏感看到一句没有实体指向的描述也强行找框容易产生幻觉检测如果占比太低开放词表的优势又体现不出来。图文对样本进入训练时还要做负采样随机挑选图像中不存在的类别词或句子作为负文本。如果只用正样本模型很快会退化成“局部匹配器”只要区域特征和文本特征落在同一片空间不管语义是否对应都会输出高相似度。3.2 损失函数设计对齐损失和回归损失的拉扯WeDetect的训练损失主要由三部分组成对齐损失、回归损失和对象性损失。对齐损失通常用InfoNCE或对比损失构建。对每个区域候选它对应的文本描述是正样本同一个batch里其他区域的描述和随机替换的文本是负样本。反过来每条文本也要去区分它应该匹配哪个区域。回归损失用L1或GIoU约束预测框和真实框之间的差异。对象性损失用来判断区域候选内是否有物体避免模型在空白背景上硬凑一个框。我在实战里发现三块损失之间的权重不需要调得太复杂但有一个关键细节对齐损失的梯度一定要控制住不能让它一开始就主导整个训练。跨模态Transformer刚初始化时图像特征和文本特征的分布差异极大如果对齐损失权重过大模型会拼命把两种特征强行拉近但边界框回归却被带偏训练曲线看上去在收敛实际框的质量越来越差。我们最终使用的权重大约是对齐损失1.0、回归损失0.5、对象性损失0.2同时前5个epoch用较小的学习率做warmup整体稳定性明显改善。另外温度系数Temperature也叫logit scale在对比损失里非常敏感。它控制相似度分布的锐利程度温度太小模型会过度关注困难负样本训练初期容易震荡温度太大所有相似度都被拉平模型失去区分度。我们参考了一些大规模视觉-语言预训练的做法训练初期用0.07让模型先学会大致的语义对齐训练中期逐步降到0.02再把正负样本的边界磨清楚。固定温度训练也不是不行但我们在验证集上看到这种“先松后紧”的调度比固定温度平均高出2到3个mAP。3.3 推理提速把“检索”变成稀疏打分很多人会担心输入一句自然语言要先编码文本再对几千个区域候选做相似度计算会不会比YOLO慢一个量级。确实如果每张图都重新编码一段长文本开销不小。但工程上有两个妥协手段。第一如果应用场景的类别集合固定就先把所有类别描述编码成一个文本矩阵推理时只需要一次矩阵乘法文本编码的时间完全省掉。第二如果类别集合不固定也尽量让文本编码和图像编码并行执行文本编码的耗时藏在图像特征提取后面。区域候选不需要一开始就全部参与检索。先用对象性分数或者类别无关的注意力分数筛出top 256个候选再做文本匹配这样可以把不必要的计算量砍掉一大截。我们在NVIDIA V100上把单张图的总时延控制在了40毫秒以内基本能追上同输入尺寸下带NMS的YOLOv5。但要注意如果用户输入的是“红外小目标检测中的评价参数”这类高度领域化的主题文本文本编码器如果没有经过对应领域微调检索出来的分数会和视觉特征不匹配这种情况下就不要只依赖零样本能力了。4. 我们在COCO和自采数据上的实测结果4.1 封闭词表 vs 开放词表的精度变化我们在COCO上做了对照实验用COCO train2017训练WeDetect在COCO val上按80类封闭词表评估不经过任何针对这80类的特殊调优mAP大约比同体量的DETR低1到2个点。原因并不难理解DETR的分类头是专门为这80类调优过的而WeDetect的文本编码器从预训练模型继承来对COCO类别名的理解未必能对齐到最佳状态。但一旦把评估模式切换成“开放词表”情况就反过来了。我们在LVIS的1200多类上做zero-shot测试WeDetect不训练新增类别mAP依然能有十几个点传统DETR由于输出维度固定连评估都跑不起来。这个对比给我的启发是如果业务目标就是死磕某几个类别的精度传统检测器仍然是最优解但如果面对的是不断变化的类别体系或者经常要临时增加新类别那用1到2个mAP的精度损失去交换“不需要重训模型”的能力非常划算。尤其是在新类别出现频率很频繁的领域省下来的数据标注和训练时间远大于精度损失。4.2 小目标和密集场景下文本线索到底帮了什么忙小目标检测一直是检测任务里最磨人的问题。YOLO系列在小目标上的困难很真实锚框分配不稳定、深层特征图分辨率不够、语义信息弱。WeDetect在处理小目标时多了一层“文本修正机制”当文本特征明确指向“小油漆桶”时它会反过来增强图像中原本响应很弱的局部区域相当于给视觉特征开了一路语义反馈。在自采的鸟类目标检测数据集上许多只有十几个像素的小鸟也能被框出来单独看小目标召回率比有监督微调的YOLOv5提升了大约7个百分点。但副作用也很明显。在密集场景下文本线索有时会把多个相似物体混为一谈。比如画面里有一群麻雀输入“一群正在飞行的麻雀”模型倾向于把整群麻雀框成一个大框而不是拆成多个独立的小框。问题的根因不在文本而在区域提议层对密集目标的分离能力不足。我们尝试把区域候选数量从300提高到500同时在后处理时对低置信度小框采用更保守的NMS参数密集场景的误检和漏检都有改善。4.3 如果换成中文描述或领域黑话会怎样为了验证检索式检测在中文场景下的表现我们做了两个额外实验。第一个实验是用中文描述比如“停在电线上的鸟”“穿红色衣服的小男孩”去检测图片中的目标。只要文本编码器本身具备中文对齐能力WeDetect依然可以工作。我们测试发现中文精度比英文描述低10%到15%主要原因还是预训练模型的中文视觉语言对齐语料少很多语义细节没有被充分学到。第二个实验更接近真实业务输入“焊点偏位”“磨玻璃结节”“跟踪标记点”这类领域黑话。这些词在通用图文对数据里非常罕见直接做零样本检索基本找不到目标。我们的做法是先用大语言模型生成一批包含黑话的专家描述再把这些描述和对应图像加入图文对数据做增量微调。训练之后模型能够把“焊点偏位”和特定视觉特征关联起来zero-shot的mAP从几乎为零提高到可用的水平。这说明开放词表不是一劳永逸领域落地时文本侧依然需要一次轻量定制而不是换了场景就直接套。5. 工程落地踩过的五个坑以及对应的解法5.1 描述质量直接决定精度天花板这是最容易忽视的坑。我们曾经把大部分精力放在调图像骨干网络和跨模态模块上结果发现换了一种文本描述写法精度变化比换骨干网络还大。同一个类别只写“dog”和写“一只张着嘴吐舌头的棕色狗”检测结果差别明显因为预训练文本编码器对短语的语义表达能力远强于单词具体描述能激活更多与视觉外观相关的特征。后来我们总结出一套文本模板规范每条描述至少包含“主体类别 外观属性 位置/动作状态”至少要有两个修饰词。比如“穿着格子衬衫的男人”就比“person”效果好得多。推理时我们还会为同一个目标生成多个描述让它们之间做置信度投票最终框的分数用top-k平均值而不是只信任单条描述。这个操作属于纯工程优化不用改模型成本极低但对开放词表任务的稳定性和召回率提升非常明显。5.2 温度系数不是固定常数要随训练阶段调整温度系数的坑比较隐蔽。训练刚开始时模型学到的是粗糙的全局语义分布此时温度设置太小模型会拼命去区分那些本就难以区分的负样本梯度震荡严重训练后期模型已经有足够判别力温度太大又会把正负样本的边界磨平精度上不去。我们最终采用分段调度前10个epoch用0.07中间10个epoch降到0.04最后10个epoch降到0.02。另外温度系数的尺度必须和文本特征的归一化方式配套。有些预训练模型的特征范数很大直接沿用默认温度会失效。我们专门写了一个校准脚本用一批验证样本计算所有正负样本对相似度的标准差然后根据标准差反推合适的初始温度。这个脚本在每次更换预训练特征时都跑一遍基本能避免训练一开始就出现loss爆炸或者相似度全部挤在0.99附近的尴尬情况。5.3 负样本拼图没有跟文本冲突的框就是无效负样本负样本构造质量直接影响模型会不会“胡说八道”。最偷懒的方式是从图像里随机选一些区域和当前文本组成负样本对但这种方式在早期有效很快模型就会发现这些区域和文本没有任何语义关联相似度轻轻松松就能压得很低。真正的困难负样本是“看起来像但并不是”的样本文本是“一只白色的猫”负样本区域里出现一只白色毛球或者一个白色玩偶模型必须学会拒绝这种视觉混淆。为了构造这种负样本我们会把同一个batch里其他图像的区域也拉进来做跨图负样本因为这些区域天然存在类别和外观上的语义混淆。同时对同一条真实描述我们会生成若干条“伪描述”比如把“红衣服的人”换成“蓝衣服的人”让模型学会区分细微属性差异。负样本的难度要逐步提升不能在训练初期就把最难的组合全放进去否则模型会直接躺平把所有区域都判为负。5.4 多卡并行时文本编码器的同步问题WeDetect训练时同时涉及图像编码器、文本编码器和跨模态Transformer。如果只用数据并行每张卡上的文本编码器参数是独立维护的梯度同步配置稍微不对图像侧和文本侧的更新步调就会不一致典型表现是loss一直抖得像心电图或者曲线平得像条直线。我们早期就有一次训练loss曲线前20个epoch几乎不动排查下来发现是文本编码器的学习率被单独设得过大图像侧还在缓慢适应文本侧已经跳来跳去。解决办法有两个。一是在分布式训练时把所有GPU的文本batch顺序固定使用完全相同的随机种子确保梯度同步时每个step的有效信息是对齐的。二是把文本编码器的学习率设为图像骨干网络的0.1倍甚至干脆冻结文本编码器只训练图像侧和跨模态模块等跨模态模块成熟后再解冻文本编码器。我们在数据量不大的情况下使用分阶段解冻效果比直接联合训练稳定很多。5.5 与YOLO流水线共存时的后处理冲突最后说一个部署层面的具体问题。很多现成系统的检测主链路是YOLO它足够快也足够成熟WeDetect并不适合一上来就完全替换它。我们采用的方式是“YOLO出候选框 WeDetect做开放词汇重打分”先用YOLO跑出一批候选框再让WeDetect的文本检索模块判断每个候选框和用户描述是否匹配替换掉YOLO原本输出的80类置信度。这个混合方案有两个容易踩的地方。一是YOLO的候选框质量直接决定检索上限如果YOLO本身漏检WeDetect再强也找不回来。因此需要把YOLO的conf阈值调低让更多候选框进入检索阶段宁可多检一些区域也不要漏掉真实目标。二是两套置信度分布的尺度不一致不能直接沿用同一个NMS阈值。我们最后写了一个自适应阈值模块根据每个类别的检索分数分布动态决定NMS保留阈值上线后误检率比手工调参时下降了一半。6. 从WeDetect看目标检测的未来走向6.1 检索范式与视频、3D点云和毫米波雷达的结合WeDetect目前处理的是单帧2D图像但这个范式的迁移价值远不止于此。视频目标检测里有一个常见的任务叫运动目标检测MTD过去主流方法靠帧差法和光流估计对“目标到底是什么”完全没有语义感知。如果把WeDetect的文本检索机制扩展到时间维度让模型直接拿“正在移动的红色汽车”去视频帧序列里检索对应区域等于把运动检测和语义识别合到了一起。这个方向我们做过最小验证困难在于时间维度的对齐数据不好构造但范式上是通的。类似思路也能搬进三维目标检测和毫米波雷达目标检测中。比如从点云数据里检索“停车位上停着的白色轿车”或者把毫米波雷达点云和视觉特征融合后用文本描述“被遮挡的行人”作为条件去定位目标。这些问题真正的瓶颈在于跨模态对齐需要三维空间信息、雷达特征和自然语言之间的配对数据目前公开数据集太少标注成本也高。但方法论的框架是清晰的只要能把多种传感器特征映射到一个与文本共享的语义空间检测任务就能从“固定类别分类”走向“开放语义定位”。6.2 下一步检测与生成不分家WeDetect最让我兴奋的点不是它比某个模型高几个点而是它把检测和文本生成连进了同一个语义空间。传统目标检测输出“类别ID 框”类别ID在推理完后基本就断了检索式检测输出的是“文本片段与区域的对应关系”这种结果天然可以被语言模型消费。比如检测结果可以直接作为视觉问答的输入也可以传给大语言模型让它生成一句“画面中央有一只褐色的鸟嘴里叼着树枝”的自然语言描述。如果沿着这条思路再往前走一步检测甚至不需要显式的区域提议。可以让模型直接生成描述句子再由区域解码器从句子里的提示向量还原边界框。到那一刻目标检测和图像描述之间的边界就真的消失了。我觉得这才是目标检测下一步最重要的演变方向检测不会再是一个孤立的视觉任务而是多模态理解和生成体系里的一个环节。WeDetect只是把第一步踩实了后面的工程问题和学术问题还很多但至少方向对了。做这个项目给我最深的体会是别再把目标检测当成一个封闭分类问题来设计把它当成一个检索问题来做很多卡了很久的瓶颈会突然变得可解。如果你也准备尝试建议先把文本描述库和温度系数调度做好否则很容易被“看起来能work”的假象带偏。