云计算百科
云计算领域专业知识百科平台

Qwen2-VL 微调实战:用 LLaMA-Factory 让视觉大模型学会无人机航拍目标检测(附避坑指南)

前言:把检测任务交给视觉语言模型(VLM)做,是最近很多项目的尝鲜方向——不用改架构、不用设计检测头,只要构造好对话数据,大模型就能"看图说话"式地输出目标框。但真正上手之后你会发现:微调能跑通,不代表推理能用。本文完整记录一次 Qwen2-VL-7B 在无人机航拍图像上的微调实践:从数据构造、训练参数选择,到推理阶段翻车的四类问题与切图补救方案,全部踩过、全部给出解法。可直接抄配置。

一、项目概况

项目配置
任务 无人机俯视航拍城市问题识别(7 类目标检测)
基础模型 Qwen2-VL-7B-Instruct
微调框架 LLaMA-Factory(LoRA / SFT)
硬件 AutoDL 单卡 vGPU 32GB
原图分辨率 3840 × 2160
数据规模 11469 张(有目标 9293 + 空标签 2176)

7 个类别:店外经营、占道经营、屋顶乱堆物料、临街乱堆物料、暴露垃圾、工地扬尘未苫盖、交通标线不清晰。其中"屋顶乱堆物料"与"临街乱堆物料"位置不同、"店外经营"与"占道经营"核心区别是有无实体店面——这些易混淆类的设计会在 Prompt 里显式给出。

二、数据准备

2.1 先看数据分布,再决定训练策略

类别照片数框数
屋顶乱堆物料 5014 12749
交通标线不清晰 2381 6551
暴露垃圾 2273 4077
占道经营 1563 3567
店外经营 1044 1508
临街乱堆物料 1029 1412
工地扬尘未苫盖 306 506
合计 13610 30370

两个结论直接决定了后面的坑:

  • 类别极不均衡:最大类是最小类的 16 倍。这是后来"模型只学会猜占道经营"的直接原因。
  • 空标签(负样本)只保留了 20%(2176 张)。这是后来误检率偏高的直接原因。建议空标签比例提到 40%~50%,且必须是"困难负样本":路面、斑马线、阴影、路边堆物、停车位、广告牌、水渍、反光——这些才是误检的真正来源。
  • 2.2 标注格式转换

    原始标注是 YOLO 格式(归一化的中心点坐标 + 宽高),而 Qwen2-VL 原生支持的是绝对像素坐标 [x1, y1, x2, y2],转换时注意两点:

    • YOLO 的 (cx, cy, w, h) 都是 0~1 归一化值,要先乘原图宽高再转左上/右下角点;
    • 空标签样本保留 has_issue=false 的对话(输出空列表),让模型学会"没有问题就输出空"。

    2.3 数据格式:ShareGPT + 图像路径

    LLaMA-Factory 的 VLM 训练数据用 ShareGPT 格式,images 字段放图片的绝对路径:

    {
    "conversations": [
    {
    "from": "human",
    "value": "<image>请分析这张无人机航拍图像……"
    },
    {
    "from": "gpt",
    "value": "{\\"has_issue\\": true, \\"issue_count\\": 2, \\"issues\\": [{\\"type\\": \\"占道经营\\", \\"bbox\\": [1213, 856, 1892, 1290]}, {\\"type\\": \\"暴露垃圾\\", \\"bbox\\": [2431, 1520, 2688, 1745]}]}"
    }
    ],
    "images": ["/root/autodl-tmp/VLM_Fine_tuning/LLaMA-Factory/data/images/xxx.jpg"]
    }

    !!!再在 dataset_info.json 里注册 urban_train / urban_val 即可。

    2.4 Prompt 设计(这步决定输出格式的稳定性)

    System Prompt 负责定义任务、类别边界和易混淆区分,User Prompt 负责强制 JSON 输出结构:(我这里是few-shot,可以让vlm做参考)

    你是一个专业的城市管理问题识别助手,用于分析无人机俯视航拍图像。
    图像来源为固定高度的无人机俯拍,目标以俯视角度呈现,可能较小,
    同一图像中可能存在多个同类或不同类问题,也可能不存在任何问题。

    你需要识别以下七类城市管理问题:
    1. 店外经营:有实体店面,经营物品从店铺门口向外延伸摆放……
    2. 占道经营:在马路、人行道或小道上摆放经营,无对应实体店面……
    3. 屋顶乱堆物料:建筑屋顶存在杂乱堆放的物品……
    4. 临街乱堆物料:临街区域杂乱堆放建筑材料、杂物或货物……
    5. 暴露垃圾:生活垃圾、废弃物等垃圾类物品未经覆盖或容纳直接暴露在外……
    6. 工地扬尘未苫盖:建筑工地的物料、土方或沙石未进行覆盖苫盖……
    7. 交通标线不清晰:道路交通标线存在磨损、褪色或模糊不清……

    注意事项:
    – 店外经营和占道经营的核心区别在于有无实体店面
    – 临街乱堆物料和暴露垃圾的核心区别在于堆放物是否为垃圾类物品
    – 屋顶乱堆物料和临街乱堆物料的区别在于堆放位置是否在建筑屋顶
    请严格按照指定JSON格式输出结果,不要输出任何额外内容。

    请分析这张无人机航拍图像,识别其中存在的城市管理问题,
    并严格按照以下JSON格式输出:
    {
    "has_issue": true或false,
    "issue_count": 问题数量,
    "issues": [
    { "type": "问题类型中文名称", "bbox": [x1, y1, x2, y2] }
    ]
    }
    其中bbox为目标在图像中的绝对像素坐标,[x1,y1]为左上角,[x2,y2]为右下角。
    如果图像中不存在任何问题,has_issue输出false,issue_count输出0,issues输出空列表。

    几个实测有效的细节:

    • 易混淆类的判别标准要写进 Prompt("核心区别在于……"),比让模型自己悟有效得多;
    • bbox 明确声明"绝对像素坐标",并在 User Prompt 里定义左上/右下角,减少模型自由发挥;
    • "不要输出任何额外内容"这句话能显著降低 JSON 前后混入废话的概率。

    三、训练配置

    3.1 关键训练参数(调了数十遍效果最佳版)

    ### model
    model_name_or_path: /path/to/Qwen2-VL-7B-Instruct
    template: qwen2_vl

    ### method
    stage: sft
    finetuning_type: lora
    lora_target: all-linear
    lora_rank: 64
    lora_alpha: 128
    lora_dropout: 0.05

    ### 关键:解冻视觉侧
    freeze_vision_tower: false # 解冻视觉编码器
    freeze_multi_modal_projector: false # 解冻投影层
    freeze_language_model: false

    ### dataset
    dataset: urban_train
    eval_dataset: urban_val
    cutoff_len: 8192
    image_max_pixels: 1003520 # 约 1120×896
    image_min_pixels: 1024

    ### train
    per_device_train_batch_size: 1
    gradient_accumulation_steps: 8
    learning_rate: 1.0e-4
    lr_scheduler_type: cosine
    warmup_ratio: 0.05
    num_train_epochs: 3.0
    bf16: true
    gradient_checkpointing: true
    flash_attn: fa2
    eval_steps: 500
    save_steps: 100

    3.2 这些参数为什么这么设(每一行背后都是一次翻车)

    freeze_vision_tower: false——最重要的一行。 第一版训练把视觉塔冻结了,结果模型的"眼睛"完全没参与学习,坐标定位一塌糊涂。解冻视觉编码器和投影层,让视觉特征真正向定位任务对齐,是坐标精度提升最明显的单点改动。

    lora_rank: 64(初版 16)。 rank=16 对"看图描述"够用,对"输出像素级坐标"容量明显不足,框的大小和位置都很随意。定位任务建议从 32 起步,条件允许直接 64。alpha 保持 2 倍 rank 关系。

    image_max_pixels: 1003520(约 1120×896)。 这是 32GB 显存下能稳定跑的分辨率上限。注意一个关键事实:原图 3840×2160,送进模型会被下采样到这个尺度——原图里 10×10 像素的小目标,下采样后只剩 1~2 个 patch。这个矛盾就是后面所有推理问题的根源,训练参数解决不了,只能靠推理方案补救(见第五节)。

    batch_size 1 × 梯度累积 8。 32GB 显存下 7B 模型 + 图像输入,实际 batch 只能开 1,用梯度累积把有效 batch 撑到 8。配合 gradient_checkpointing 和 flash_attn: fa2,显存才压得住。

    learning_rate: 1e-4。 首轮从零训 LoRA 用 1e-4 没问题;但如果是从已有 checkpoint 续训,这个学习率偏危险,建议降到 1e-5 ~ 3e-5。

    3.3 训练结果

    • 单个 epoch 约 7 小时 20 分钟(10321 张训练图);
    • epoch 1 结束 train_loss = 0.4676;
    • eval_loss 从 0.4302 一路降到 0.3771,收敛平稳;
    • 训练 3 epoch(中途服务器关机过一次,从 checkpoint 的 LoRA 权重续跑,步数从头计)。

    四、整图推理:四类问题,一次翻车现场

    训练指标很健康,但拿整张 3840×2160 原图直接推理,效果离可用还差得远:

  • 框不准:框整齐地堆在一起、大小一致,但就是不贴目标——模型在"猜"一个看起来合理的坐标,而不是在做几何回归;
  • 框偏大:普遍比真实目标大 3~4 倍;
  • 误检:把马路纹理、阴影检成"暴露垃圾""占道经营";
  • 漏检:小目标密集的图,检出数量明显不足。
  • 根因分析(这部分是全文最值钱的认知)

    VLM 不是检测模型。 传统检测器(YOLO、DINO)有 FPN 多尺度特征金字塔和 anchor 机制专治小目标;Qwen2-VL 的视觉编码器把任意尺寸输入映射到固定数量的 patch token,小目标在下采样后特征几乎被淹没。它输出 bbox 的方式是让语言模型把坐标"说"出来——本质是序列预测,不是几何回归。框整齐排列、大小一致,正是语言模型在按"统计规律"输出坐标格式的典型症状。

    剩下的问题来自数据:

    • 误检:空标签只占 20%,且缺"路面/阴影/斑马线"这类困难负样本;
    • 类别偏科:第一轮训练 1 个 epoch 时模型只输出"占道经营"——多数类样本多,模型学到了"猜多数类永远不会太差"的捷径,典型的类别不平衡 + 欠训练组合。


    示例:重叠很多

    五、切图推理:不改一行训练代码,效果大幅提升

    既然小目标的矛盾是"下采样后特征被淹没",那就在推理侧把目标在输入图中的相对尺寸放大——把原图切块,逐块推理,坐标还原后合并去重。

    5.1 方案设计

    • 原图 3840×2160 切成 2×3 = 6 块,每块约 1280×960,目标相对尺寸放大约 2.4 倍;
    • 块间保留 15% overlap,防止目标正好压在切割边界上被截断;
    • 小目标特别密集的图进一步切成 15 块;
    • 每块独立推理后,把坐标归一化 → 映射回子图像素尺度 → 加子图偏移量,还原为原图坐标;
    • 最后 NMS 去重,去掉 overlap 区域产生的重复框。

    5.2 核心代码

    import cv2
    import numpy as np

    def slice_inference(image, predict_fn, rows=2, cols=3, overlap=0.15):
    """切图推理:逐块推理 + 坐标还原 + NMS 合并"""
    H, W = image.shape[:2]
    # 带重叠的切块:每块的实际起止范围
    ys = np.linspace(0, H, rows + 1).astype(int)
    xs = np.linspace(0, W, cols + 1).astype(int)
    pad_y, pad_x = int(H * overlap), int(W * overlap)

    all_boxes, all_scores, all_labels = [], [], []
    for i in range(rows):
    for j in range(cols):
    y0, y1 = max(0, ys[i] – pad_y), min(H, ys[i + 1] + pad_y)
    x0, x1 = max(0, xs[j] – pad_x), min(W, xs[j + 1] + pad_x)
    tile = image[y0:y1, x0:x1]
    tw, th = tile.shape[1], tile.shape[0]

    for box, score, label in predict_fn(tile):
    x1_, y1_, x2_, y2_ = box
    # 模型偶尔输出 0~1 相对坐标,统一转成子图像素坐标
    if max(x1_, y1_, x2_, y2_) <= 1.5:
    x1_, y1_, x2_, y2_ = x1_*tw, y1_*th, x2_*tw, y2_*th
    # 加上子图左上角偏移,还原到原图坐标系
    all_boxes.append([x1_+x0, y1_+y0, x2_+x0, y2_+y0])
    all_scores.append(score)
    all_labels.append(label)

    keep = nms(all_boxes, all_scores, iou_thr=0.5) # 去掉重叠区重复框
    return [(all_labels[k], all_boxes[k]) for k in keep]

    坐标还原的完整链路:模型输出(相对子图)→ 归一化判断 → 乘子图实际尺寸 → 加偏移量 → 原图坐标 → NMS。实测中模型输出坐标制式偶尔漂移(有时绝对像素、有时 0~1 相对坐标),所以代码里保留了一次 <=1.5 的判断做自适应。

    5.3 效果

    小目标的定位与识别精度明显改善,密集小目标场景的漏检也大幅缓解,整体达到可演示水平。代价是推理次数变成 6~15 倍,需要按业务延迟做权衡。

                          

                                            密集小目标场景,切 6 块 vs 切 15 块对比

    六、避坑清单(都是真金白银换来的)

  • 坐标系要全链路对齐。训练数据是原图绝对像素坐标,模型看到的是缩放后的图;推理时模型输出有时是 0~1 相对坐标——每个环节都显式确认坐标制式,能省掉一整天的 debug。
  • 视觉塔必须解冻。freeze_vision_tower: false,不然模型的"眼睛"不参与学习,定位全靠蒙。
  • LoRA rank 别低于 32。定位任务对容量比描述任务敏感得多,16 会明显不够用。
  • 空标签不是垃圾数据,是压制误检的主力。20% 不够,40%~50% 更稳,且优先补充"长得像目标"的困难负样本。
  • 类别不平衡要在数据层处理(过采样少数类 / 加权 loss),否则模型会走"猜多数类"的捷径。
  • 密集目标的 JSON 输出可能被 max_new_tokens 截断(一图 15 个目标的 JSON 很容易超 1024 token),漏检排查时记得检查这一项——虽然多数时候主因还是小目标特征太弱,模型根本没看见。
  • OpenCV 画中文标签会变方块。服务器装 fonts-wqy-zenhei,或者改用 PIL 绘制中文。
  • 长训会遇上断点续训。云端实例关机中断很常见,从 checkpoint 的 LoRA 权重续跑即可;续训时把学习率降到 1e-5 ~ 3e-5,防止把已收敛的权重冲飞。
  • 训练 loss 正常 ≠ 推理可用。SFT 收敛只说明模型学会了输出格式,VLM 做检测的天花板由架构决定:没有 FPN,就没有小目标精度上限的保证。
  • 七、后续优化方向

    按投入产出排序:

  • 推理后处理(零训练成本):NMS 的 IoU 阈值从 0.5 收紧到 0.35;按面积过滤明显异常的框(超过原图 30% 基本是误检,小于 0.01% 基本是噪声)。
  • YOLO 两阶段流水线(最推荐):用现成的 YOLO 格式标注直接训一个 YOLOv8,负责"在哪里";Qwen2-VL 只对 YOLO 框出的 crop 做分类确认,负责"是什么"。检测器的 FPN 结构天然适配小目标,VLM 的语义理解能力用在刀刃上,现有标注零额外处理。
  • GRPO 强化训练(长期):在 SFT 基础上设计 IoU 奖励 + 分类奖励 + 格式奖励,让定位从"语言模型猜坐标"进化到"真正优化几何精度"。前提是先做 rule-based reward 离线验证,否则 bbox 格式还漂着就上 RL,会直接学歪。
  • 总结

    这次实践完整的路线是:ShareGPT 格式构造带严格 JSON 输出模板的检测数据 → LLaMA-Factory 上解冻视觉塔的 LoRA 微调(rank=64)→ 面对高分辨率小目标,用切图推理 + 坐标还原 + NMS 补救。

    最想留给你的一句话:VLM 做检测,微调解决的是"输出格式和语义对齐",解决不了"小目标几何定位"——后者要么靠切图推理缓解,要么老实上两阶段流水线。认清这个边界,能帮你省下大量调参时间。

    如果这篇对你有帮助,欢迎点赞收藏;踩过别的坑也可以在评论区交流。我做的不一定是最好的,欢迎大家讨论~


    环境说明:Qwen2-VL-7B-Instruct / LLaMA-Factory / AutoDL vGPU 32GB / PyTorch + flash-attn

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Qwen2-VL 微调实战:用 LLaMA-Factory 让视觉大模型学会无人机航拍目标检测(附避坑指南)
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!