用 QLoRA 微调 Qwen2.5-VL 做驾驶场景理解:从环境搭建到三轮训练踩坑全记录
tags: 人工智能 大模型 LoRA微调 Qwen 自动驾驶 视觉语言模型 VLM 深度学习 category: 人工智能
@[toc]
前言
先说说我为什么做这个项目。
我之前在智能驾驶公司做感知相关的工作,每天打交道的就是车道线可视化、点云处理。干了挺久,活儿其实挺重复的——标注、可视化、再标注、再可视化。后来大模型爆发,我就在想:我每天手动看那么多驾驶场景图片,能不能直接让一个视觉大模型帮我把场景里关键的信息给提取出来?车道线几条、什么类型、前面几辆车、交通灯什么状态、有没有风险——这些东西,人看一眼就能说出来,模型是不是也行?
带着这个想法我开干了:输入一张驾驶场景图片,系统自动输出四项结构化描述——车道线、车辆、交通标志、驾驶风险。
听起来不复杂。但做下来发现,光"让模型输出变好"这件事就训了三轮——第一轮 prompt 没对齐,模型学的是数物体不是做分析;第二轮训练数据里有大量重复模板句,模型学会了背模板不看图;第三轮用 pseudo-label 补全缺失标注才终于走上正轨。每一轮都暴露了不同的关键问题。今天把整个过程写出来,给做类似事情的朋友当个参考。
一、技术选型
选模型的时候纠结过一阵子。7B 的模型精度肯定更高,但项目开始时我手头只有一块 RTX 3050 笔记本显卡,4GB 显存。7B 模型光加载就得十几个 GB,想都别想。后来换了 Legion Y9000P 带 RTX 5070,12GB 显存终于能做本地推理和评估了,但训练还是得上云。
最终选了 Qwen2.5-VL-3B-Instruct,理由很实际:3B 参数量,4bit 量化后大概 2GB 显存就能加载,3050 勉强能推理;阿里的模型,中文理解和生成都很强,社区也活跃,遇到问题能搜到解决方案。
微调方案选了 QLoRA(4bit 量化 + LoRA)。这也是没得选——显存就那么多,全量微调想都不用想。LoRA 只训练很少的参数(大概占总参数量的 0.1%),显存需求大幅降低,训练出来的权重就几 MB。
数据集选的是 nuScenes,自动驾驶领域的标杆数据集,有完整的 2D/3D 标注,前视摄像头(CAM_FRONT)的图片直接就能用。
整体技术栈:Qwen2.5-VL-3B-Instruct 做基座模型,PEFT 库做 QLoRA 微调,nuScenes CAM_FRONT 做训练数据,Gradio 做 Web Demo,云端用 AutoDL 和恒源云(本地显存不够训练)。
二、环境搭建:还没写代码就开始踩坑
你以为做项目第一步是写代码?不,第一步是装环境。而装环境的痛苦程度,往往超出你的想象。
2.1 PyTorch CUDA 版的下载噩梦
我的开发环境经历了一次硬件切换。一开始项目是放在一块 WD Elements 外接机械硬盘上的,系统是 Ubuntu。后来换了新笔记本,原来机器里的美光 P3 Plus NVMe SSD 退了下来,装进佳翼雷电4硬盘盒(40Gbps)当外接存储用,项目才迁到这块盘上。两个盘的体验差距巨大——后面会细说。
国内清华源、阿里云源上的 PyTorch 全是 CPU 版本,装完 torch.cuda.is_available() 返回 False。你得去 PyTorch 官方源下载 CUDA 版的 whl 文件——一个 800MB+ 的包,国内下载速度感人。
我试过直接 pip 装,下载了 20 分钟超时。然后换成浏览器手动下载 whl 文件,结果打开链接显示 AccessDenied——PyTorch 官方限流了。最后只能手动下载 whl 到本地,再用 pip install ./torch-xxx.whl 本地安装,同时指定清华源拉 nvidia 依赖。装个 PyTorch 花了大半天。
2.2 机械硬盘的坏道问题
前面说了,项目一开始在 WD Elements 机械硬盘上搞的。这块盘用了好几年,有了坏道,而且坏道影响范围比想象的大。Python 包装到一半卡死、训练图片解压到一半失败、某些文件读取时直接 IOError。排查了好一阵子才确定是硬盘问题。后面每次打包图片都要跳过坏道区域,还专门写了个脚本 pack_training_images.py 来处理。如果你也在外接机械硬盘上开发,遇到莫名其妙的 IO 错误,先 badblocks 检查一下硬盘。
后来换了美光 P3 Plus 装在雷电4盒子里,体验直接质变——模型加载从几分钟变成几十秒,训练图片解压再也不报错了。SSD 和机械硬盘做机器学习开发,差距不是一点半点。
2.3 模型下载:7.1GB 的耐心考验
Qwen2.5-VL-3B-Instruct 模型一共 11 个文件,总共 7.1GB。最大的两个 safetensors 文件分别 3.7GB 和 3.3GB。
从 HuggingFace 国内镜像 hf-mirror.com 下载,速度大概 2-4MB/s。下载两个大文件花了差不多一个小时。中间 curl 超时了一次,换 wget 加 -c 断点续传搞定。模型文件下载完后要校验大小——我遇到过下载不完整的情况,文件在但 size 不对,加载直接报错。
最终环境验证通过:
torch: 2.6.0+cu126 | CUDA: True | GPU: RTX 3050 4GB transformers: 5.7.0 accelerate: 1.13.0 qwen_vl_utils: OK
看到 CUDA: True 那一刻,差点感动得哭出来。
三、Baseline 搭建与评估
3.1 第一次推理:图片太大爆显存
环境搞定后,激动地写了第一个推理脚本 01_inference.py。加载模型、准备 prompt、传入图片——
CUDA out of memory.
4GB 显存连原图都处理不了。nuScenes 的 CAM_FRONT 图片分辨率是 1600x900,Vision Encoder 处理这么大的图片直接把显存撑爆了。
解决方案很简单粗暴——把图片缩到 800x800 以内。写了个 00_resize_images.py,用 Pillow 的 thumbnail 方法等比例缩小,quality=90 保证画质。缩小后单张推理占用大概 3.5GB 显存,刚好塞进 3050。
3.2 评估框架搭建
为了后面能对比微调效果,我需要一个 Baseline 基线。从 nuScenes 的 200 多张图里挑了 50 张,覆盖 8 种场景(白天、夜晚、雨天、路口、高速等),写了批量推理脚本,结果存到 eval_results.json。
然后是手动打分环节——纯人工。50 张图,每张按四项分别打分(每项 0-5 分,总分 20 分),这活儿干了一整个下午。
Baseline 得分:平均 14.10 / 20,车道线 3.19、车辆 3.36、标志/信号灯 3.64、风险分析 3.86,幻觉率 4%(2/50)。
Baseline 的表现比预期的好。模型能正确识别车道线类型、描述车辆位置,甚至能注意到交通锥和行人。但很多描述过于笼统,比如"前方有几辆车"——有几辆?多远?说不清楚。这就是微调要解决的问题。
四、LoRA 微调:三轮训练实录
这是整个项目最核心的部分,也是我学到最多东西的部分。一共训练了三轮,每一轮都暴露了不同的关键问题,逐轮修复后才得到最终可用的模型。
4.1 Round 1:训练 prompt 和推理 prompt 不一致
第一轮训练的数据是用 04_prepare_training_data.py 从 nuScenes 标注生成的。格式是这样的:
用户问:请描述这张驾驶场景图片中的车辆、行人和关键物体。 模型答:3个小汽车, 1个卡车, 2个行人
看起来挺正常的对吧?问题在于——我推理时问的是:
请分析这张驾驶场景图片,输出以下信息: 1. 车道线数量和类型 2. 前方车辆位置和距离估计 3. 交通标志/信号灯状态 4. 潜在驾驶风险
训练时问的是"描述物体",推理时问的是"四项分析",问题完全不同。
我当时没意识到这个问题的严重性。训练 Loss 从 66.80 降到 30.55,看着很漂亮。但评估结果出来直接傻眼——模型输出变得模板化、模糊,大量出现"未知"、"未显示",得分从 Baseline 的 14.5 分降到 10.75 分。
模型学的是"数物体",但推理时被问"四项分析",它就懵了。Loss 下降不代表输出质量变好,它只是在学习如何回答训练时的那个问题。同样的 500 张图片,问答格式不同,Loss 下降的方向就不同,最终权重完全不同。
这一轮最大的教训:训练数据的 prompt 格式必须和推理时完全一致。
4.2 Round 2:模板句导致模型背模板
修复了 prompt 不一致的问题后,我把训练数据改成了四项格式:
用户问:请分析这张驾驶场景图片,输出以下信息: 1. 车道线数量和类型 2. 前方车辆位置和距离估计 3. 交通标志/信号灯状态 4. 潜在驾驶风险 模型答: 1. 车道线:根据图片判断车道线数量和类型。 2. 车辆:3辆小汽车、1辆卡车。无行人。 3. 交通标志/信号灯:交通锥。 4. 潜在驾驶风险:根据场景判断潜在风险。
发现问题了吗?第一项和第四项——"根据图片判断车道线数量和类型"、"根据场景判断潜在风险"——500 条训练数据里,这两句完全一模一样。
原因也很简单:nuScenes 标注只有物体类别(车、行人、交通锥),没有车道线信息和风险分析。我在 04 脚本里对缺失项填了占位模板。结果模型把这两句模板当正确答案背下来了。每张图不管什么场景,车道线永远是"根据图片判断车道线数量和类型",风险永远是"根据场景判断潜在风险"。它学会了"在这个位置输出这句话",而不是"看图后描述"。
Round 2 得分:平均 9.0 / 20,车道线从 3.19 降到 1.0,风险从 3.86 降到 1.0,退步明显。好在车辆和标志两项有 nuScenes 真实标注,所以表现还行。
这一轮的教训:训练数据不能有大量完全相同的答案,哪怕答案不准确也比全部一样好。模型学的是行为模式,不是具体答案。
顺便说一个 Round 2 同时踩到的隐蔽的坑——训练和推理的量化配置不一致:
| 配置项 | 训练 | 推理 |
|---|---|---|
bnb_4bit_compute_dtype |
bfloat16 |
float16 |
bnb_4bit_quant_type |
"nf4" |
默认 "fp4" |
torch_dtype |
bfloat16 |
float16 |
这个 bug 最恶心——不会报错,不会崩溃,只是静默降低效果。QLoRA 的 LoRA 修正量是针对特定量化配置学的,推理时配置变了,修正量部分失配。后来把推理脚本的量化配置改成和训练完全一致(bf16 + nf4)才解决。
4.3 Round 3:Pseudo-label 方案修复模板句问题
前两轮暴露了两个关键问题后,剩下的核心挑战是:nuScenes 没有车道线和风险标注,训练数据怎么补全?
我考虑了三种方案。第一种是去掉缺失项,只保留车辆+标志两项——但这会让模型学会跳过车道线和风险。第二种是改模板措辞,换个说法引导模型看图——但 500 条还是完全一样,照样背模板,换汤不换药。第三种是用 Baseline 模型对 500 张图推理,提取输出的车道线和风险描述作为训练答案——pseudo-label 不完美,但每张图内容不同。
最终选了第三种。思路很直接:用 Baseline 模型(不挂 LoRA)对 500 张训练图跑一遍推理,把输出的车道线和风险描述提取出来,替换掉模板句。车辆和标志两项保留 nuScenes 真实标注。
具体操作上,写了 03_extra_baseline_train_inference.py 让 Baseline 对 500 张训练图跑推理,又写了 04_extra_merge_pseudo_labels.py 把 Baseline 输出解析拼接成新的训练数据,生成 training_data_round3.json。
pseudo-label 不完美——Baseline 车道线得分才 3.19/5,大概 60% 的准确率。但关键不在于准确率,而在于行为:模板句教会模型"遇到这个位置就输出这句话",pseudo-label 教会模型"你应该看图后描述你看到的"。即使 pseudo-label 有噪声,梯度方向是对的。这是 self-training 在标注缺失场景下的标准应用。
Round 3 训练在恒源云 3090 上完成,Loss 从 64 降到 29,训练 36 分钟。下面是 Round 3 推理后的部分输出,可以看到 pseudo-label 修复后模型确实在做"看图描述"而不是背模板了:

4.4 推理重复循环:4bit 模型的坑
Round 3 训练完后评估,又遇到了新问题——约 20% 的图片模型陷入重复循环:
- 一辆银色轿车停在左侧路边。- 一辆银色轿车停在左侧路边。- 一辆银色轿车停在左侧路边。
同一句话重复 30-40 遍,直到 max_tokens 耗尽。这是 LLM 生成的经典退化问题——4bit 量化模型加 greedy decoding,在生成到某个 token 时概率分布变得很尖,导致反复选同一个 token。
我试了 5 种参数组合:无 penalty + 512 tokens,16% 循环最长 974 字;penalty=1.2,仍有循环;penalty=1.5,换措辞说废话更长;penalty=1.5 加 no_repeat_ngram_size=3,完全乱码输出 ASCII art 和代码块;最后试了无 penalty 加 max_new_tokens=256 硬截断——终于正常了,轻微循环被截断控制住。
过程挺折腾的。repetition_penalty=1.5 会把 4bit 模型的概率分布推歪,导致输出乱码;no_repeat_ngram_size 在 4bit 模型上更不能用,模型被迫换措辞反而输出更乱。最后发现最简单有效的方案就是 max_new_tokens=256 硬截断,不改概率分布,只是物理截断。
最终效果:80% 的图正常输出,20% 的轻微循环被截断控制。
4.5 后处理:清理模型的"聊天废话"
即使截断到 256 tokens,模型偶尔还会在末尾生成"希望我的回答对你有所帮助"、"祝您旅途愉快"之类的聊天废话,甚至还有 emoji。不影响核心内容,但影响 Demo 展示效果。
写了个 _clean_output 方法,定义了一组垃圾 pattern(特定短语 + emoji 范围),从匹配位置截断。不过滤正常内容,只切明确垃圾。新垃圾 pattern 可以随时追加。
4.6 云端训练踩坑小结
本地 4GB 显存根本训不了(连 vision encoder 前向传播都 OOM),只能上云。三个平台都试过:
-
AutoDL vGPU-32GB,1.68元/时,训练 15 分钟——贵但快,经常没空位
-
恒源云 2080Ti,0.59元/时,训练 3-4 小时——太慢了,Turing 架构 bfloat16 是软件模拟
-
恒源云 3090,0.69元/时,训练 36 分钟——性价比最高,Ampere 原生 bfloat16
云端踩了一堆坑:镜像自带 PyTorch 2.4.0 太旧和 transformers 5.x 不兼容,得手动升级;pip 装 torch 时 nccl 包卡死,用 --no-deps 跳过(单卡训练不需要 nccl);清华源下载 torch 403,改用阿里云或华为云镜像;hf-mirror 下载模型只有 97KB/s,改用 modelscope 直接 25MB/s;镜像自带 torchvision 绑死旧版 torch,pip 装新版 torch 会被 torchvision 拖回去,又是用 --no-deps 打破循环。
每次云端训练都是:上传数据、修路径、装依赖、跑训练、下载结果。看着简单,实际上每一步都可能出问题。
五、Gradio Demo 封装
三轮训练搞定后,最后一步是做一个 Web Demo 展示效果。
用 Gradio 搭了个界面:左侧是图片上传区域,右侧是结构化分析结果(四项分块显示),底部一个蓝色按钮"开始分析"。后端封装了一个 DrivingSceneAnalyzer 类,加载基座模型加 LoRA 权重,推理参数最终确定为 max_new_tokens=256 配合 _clean_output 后处理。
写了通用版和本地适配版两个版本。python 08_gradio_demo_local.py 一跑,浏览器打开 http://127.0.0.1:7860——看到界面的那一刻,整个项目的感觉就对了。
六、最终效果展示
下面是 Gradio Demo 的几张效果截图,覆盖了不同场景:
场景一:城市道路

城市道路场景下,系统能正确识别黄色双实线车道线、停在路边的车辆、以及行人过街标志。
场景二:停车场/小区道路

停车场场景,系统正确判断没有明显车道线,识别出两侧停放和行驶的车辆。
场景三:夜间道路

夜间场景,系统能识别白色车道线,并正确指出夜晚视线不佳的风险。
场景四:夜间路口

夜间路口场景,识别出多条车道线、停车标志,并提示夜晚能见度低的风险。
场景五:雨天道路

雨天场景,系统识别出虚线车道线,并明确指出"天气条件不佳、能见度低"的驾驶风险。
七、踩坑总结
回顾整个项目,几个印象最深的坑:
训练 prompt 和推理 prompt 不一致。 这是最致命的。训练数据问"描述物体",推理时问"四项分析",Loss 从 66 降到 30 看着很美,但模型学的是回答另一个问题。方向错了,跑得越快离目标越远。
训练数据里大量重复模板句。 500 条数据里车道线和风险两项全是同一句话,模型学会了不看图直接输出模板。哪怕答案不准确也比全部一样好——模型学的是行为模式,不是具体答案。
量化配置不一致。 训练用 bf16+nf4,推理用 fp16+fp4。不报错不崩溃,只是静默降低效果。这种 bug 最隐蔽,排查了挺久。
repetition_penalty 在 4bit 模型上副作用大。 概率分布本身就脆弱,加 penalty 会推歪导致乱码。no_repeat_ngram_size 更是完全不可用。最后发现 max_new_tokens 硬截断是最简单有效的方案。
八、项目不足与反思
这个项目还有很多可以改进的地方。
训练数据量太少——500 条对于一个 3B 参数的模型来说远远不够,业界 VLM 微调通常用几千到几万条。数据量不足直接导致泛化能力差,以及训练数据格式过度拟合(列举循环)。
Pseudo-label 有噪声——车道线和风险两项的标注来自 Baseline 模型自己的推理结果,不是人工标注。Baseline 车道线准确率大概 60%,意味着 40% 的训练数据在这两项上是有偏差的。
评估体系粗糙——只有 50 张图的手动打分,没有自动化评估指标。理想的方案是配合 ROUGE、BLEU 等文本相似度指标,评估结果会更客观。
没做消融实验——面试官最爱问的"LoRA rank 选 8 和 16 有什么区别"、"4bit 和 8bit 量化效果差多少",这些我都没做,只跑了一组固定参数。
但这个项目的价值不在于最终得分有多高,而在于完整的工程闭环——从环境搭建、数据准备、训练、评估、到 Demo 展示,每一步都亲自动手,每个问题都经历了"发现→排查→修复"的完整过程。面试不是看你做得多完美,而是看你在遇到问题时的分析和解决能力。"每一轮都发现了不同的关键问题并逐一修复——prompt 对齐、模板句陷阱、量化配置一致性——最终让模型学会了看图描述的正确行为",这种认知深度的展示,比"训了一次就成功"更有说服力。
九、技术细节速查
如果你想做类似的项目,这里是我最终用的一些关键参数。
训练配置(QLoRA):
BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_compute_dtype=torch.bfloat16, bnb_4bit_use_double_quant=True, bnb_4bit_quant_type="nf4", ) LoraConfig( r=8, lora_alpha=16, target_modules=["q_proj", "v_proj"], lora_dropout=0.05, bias="none", task_type="CAUSAL_LM", ) # AdamW, lr=2e-4, epochs=3, gradient_accumulation=8
推理参数:
model.generate(**inputs, max_new_tokens=256) # 不加 repetition_penalty
训练数据格式:
{
"messages": [
{
"role": "user",
"content": [
{"type": "image", "image": "图片路径"},
{"type": "text", "text": "请分析这张驾驶场景图片,输出以下信息:\n1. 车道线数量和类型\n2. 前方车辆位置和距离估计\n3. 交通标志/信号灯状态\n4. 潜在驾驶风险"}
]
},
{
"role": "assistant",
"content": "1. 车道线:...\n2. 车辆:...\n3. 标志/信号灯:...\n4. 风险:..."
}
]
}
十、项目文件结构
VLM-Driving-Scene-Understanding/ ├── scripts/ │ ├── 00_resize_images.py # 图片预处理(缩放到 800px) │ ├── 01_inference.py # 单张推理 │ ├── 02_prepare_eval_data.py # 评估数据准备(50张) │ ├── 03_eval_inference.py # Baseline 批量推理 │ ├── 04_prepare_training_data.py # 训练数据生成(四项格式) │ ├── 05_lora_train_cloud.py # 云端 LoRA 训练 │ ├── 06_lora_eval.py # LoRA 评估推理 │ ├── 07_inference_api.py # 推理模块封装 │ ├── 08_gradio_demo.py # Gradio Web Demo │ └── ... ├── data/ │ ├── samples/CAM_FRONT/ # nuScenes 图片(499张) │ ├── eval_images/ # 50 张评估图片 │ ├── training_data_round3.json # Round3 训练数据 │ └── lora_eval_results.json # 评估结果 ├── models/ │ ├── Qwen2.5-VL-3B-Instruct/ # 基座模型(7.1GB) │ └── lora_round3/ # Round3 LoRA 权重 └── docs/ # 详细文档
写在最后
这个项目从 4 月底开始做,到 6 月初完成,断断续续花了一个多月。中间经历过无数次"为什么又报错了"的崩溃,也经历过"终于跑通了"的喜悦。
最大的收获不是最终效果有多好,而是整个过程中的认知提升。以前觉得微调就是"准备数据、跑训练、看 Loss",现在才知道每一步都有无数细节可以踩坑。训练数据格式不一致、量化配置不匹配、模板句污染、重复循环……每一个坑都是一堂课。
做之前先跑通 Baseline,微调之前先把训练数据和推理 prompt 对齐,训练过程中每一步都记录下来——这样出了问题才能回溯,面试时才能讲清楚。
自知此项目有很多不足和需要改进的点,希望各位大佬点拨指正。
GitHub 仓库:https://github.com/Apageoflove/VLM-Driving-Scene-Understanding
仓库包含完整代码、训练数据格式、评估结果、以及详细的踩坑文档。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)