微调qwen2.5-vl的踩坑实录

之前业务需要目标检测能力,要微调qwen2.5-vl-3b,但是在微调的过程中发现很多坑,发现了很多视觉大模型和传统视觉模型之间的差异、视觉大模型和大语言模型的训练差异,以下是我总结的几点

1. 数据集及标注的大坑

目标检测是视觉能力,要同时预测一个类别和边界框。在准备数据集的时候,需要准备图像样本和一个jsonl标注文件。图像样本这没什么问题,和传统的视觉模型一样准备就行了,但是标注这就有了大坑。

  • 标签需要语义明确且简洁

视觉大模型和传统的视觉模型(如YOLO等)有本质的区别。在训练时,视觉模型不理解标签的真正含义,仅仅是将真实值和预测值做判断,它们只是将看到的图像特征和标签做匹配。也就是说,标签是什么和它没关系,标签是超级无敌长一串字在视觉模型看来也只是一个int而已,比如0代表人、1代表狗。

但是在大模型训练范式中,标签是以**自然语言文本(Token)**的形式被传入到大模型的对话中的,所以大模型会去理解这个标签的含义。如果标签的含义和你边界框里的东西不符合,这会破坏对话的逻辑连贯性,导致大模型的训练出现问题,大模型无法理解提问与回答之间的关系。所以我们应该使用符合人类直觉且能准确描述图像特征的类别名称。

另外一个需要注意的点是User Message应该保持统一,因为我的业务场景只是传入一个图片并得到一个检测结果,所以提示词应该统一。同时,千问团队建议在非Agent工具调用的对话场景下面,不设置System Message,而是只使用User Message

由于我的目的是要本地部署qwen2.5-vl-3b,所以我在设计之初就考虑了大模型的输出问题,我需要尽可能得缩短大模型的回答耗时,让大模型吐出更少的token是加速的关键。所以设计类别标签时,在保证语义清晰的前提下,应考虑更加简洁的类别标签。

  • 数据集分布问题和大模型的“拒答”逻辑

和大多数任务一样,数据集中不应只包括有类别的正样本,也应该适当加入一些无标签的负样本,否则大模型会产生严重的先验偏见:它默认样本中一定包含某个目标。但是负样本的标注应该怎么设置?大模型很难空输出,因为大模型是根据前面的提示词来预测下一个token,所以它没法什么都不输出。如果标注数据简单地留空,模型在微调时会失去学习目标,甚至可能导致生成陷入死循环。

因此,我们不能让大模型不说话,而是显式地承认“没看到”。如果一个图片中什么都不包含,千万不要留空,而是设置一个固定的短语,比如”未检测到目标”

  • 边界框(bbox)的坐标系异同

这是个历史遗留问题,qwen的不同系列对边界框的坐标有不同的处理方式

Qwen2.5-VL:返回的坐标相对于缩放后的图像左上角的绝对值,单位为像素。

Qwen3-VL:返回的坐标为相对坐标,坐标值会归一化到[0, 999]

在标注的时候,需要特别注意这个点,不然会出现边界框漂移的现象。

  • 利用特殊 Token 提速

标注的格式直接决定了大模型的输出效率。众所周知,大模型的输出分两个部分:prefill和decode。decode就是大模型吐字的过程。在qwen的分词器中tokenizer_config.json中可以看到<|object_ref_start|><|object_ref_end|><|box_start|><|box_end|> 这几个特殊token。在构造微调数据时,强烈建议使用这些特殊 Token 来包裹类别和坐标。这不仅能帮助模型结构化输出,还能将长串的字符串压缩成极少的 Token,大幅提升推理速度。

最终,我们得到了一个标准的标注格式:<|object_ref_start|>类别标签<|object_ref_end|><|box_start|>(x1,y1),(x2,y2)<|box_end|>

2. 训练时的精度计算

在处理视觉任务时,我们不能使用语言类任务的精度计算逻辑。

传统大语言模型微调看重的是交叉熵损失(Cross-Entropy Loss)。但在目标检测中,坐标数值的微小差异在文本层面会被认为是“完全预测错误”。例如,真实框的坐标是 (100, 200),模型预测的是 (100, 205)。在 Token 级别上,这可能代表了完全不同的词表索引,导致计算出的 Loss 居高不下。但从视觉层面来看,这两个框的交并比(IoU)极高,几乎是完美的预测。

避坑指南:如果仅仅依靠训练日志中的文本 Loss 下降来判断模型是否收敛,会导致误判。必须要在验证阶段引入自定义的 CV 指标计算脚本。通过正则提取模型输出文本中的坐标点,将其转换为标准数值,再调用 COCO API 或自定义逻辑计算 mAP 和 IoU,才能真实反映模型的检测能力。