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

仓库包含完整代码、训练数据格式、评估结果、以及详细的踩坑文档。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐