前言:把检测任务交给视觉语言模型(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 |
两个结论直接决定了后面的坑:

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 原图直接推理,效果离可用还差得远:
根因分析(这部分是全文最值钱的认知)
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 块对比
六、避坑清单(都是真金白银换来的)
七、后续优化方向
按投入产出排序:
总结
这次实践完整的路线是:ShareGPT 格式构造带严格 JSON 输出模板的检测数据 → LLaMA-Factory 上解冻视觉塔的 LoRA 微调(rank=64)→ 面对高分辨率小目标,用切图推理 + 坐标还原 + NMS 补救。
最想留给你的一句话:VLM 做检测,微调解决的是"输出格式和语义对齐",解决不了"小目标几何定位"——后者要么靠切图推理缓解,要么老实上两阶段流水线。认清这个边界,能帮你省下大量调参时间。
如果这篇对你有帮助,欢迎点赞收藏;踩过别的坑也可以在评论区交流。我做的不一定是最好的,欢迎大家讨论~
环境说明:Qwen2-VL-7B-Instruct / LLaMA-Factory / AutoDL vGPU 32GB / PyTorch + flash-attn
网硕互联帮助中心




评论前必须登录!
注册