ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

020、LLaVA指令微调视觉语言模型:从零训练机器人专用VLM实战

020、LLaVA指令微调视觉语言模型:从零训练机器人专用VLM实战 020、LLaVA指令微调视觉语言模型从零训练机器人专用VLM实战开篇一个让人抓狂的loss曲线上个月调试一个机械臂抓取项目用现成的LLaVA-1.5-7B做视觉语言指令跟随模型在公开数据集上表现不错但一上真实机器人就翻车——让它把红色杯子放到蓝色托盘里它回答得头头是道但视觉编码器输出的特征和机器人控制信号完全对不上。后来发现问题出在预训练VLM的视觉特征分布和机器人相机采集的真实场景差异太大CLIP backbone在仿真渲染图上训练到了真实光照、遮挡、反光环境下特征漂移严重。当时我盯着那张loss曲线图前500步死活不降后来才意识到是学习率设置和冻结策略的问题。这篇文章就把我踩过的坑和最终跑通的完整流程记录下来从数据准备到指令微调再到部署到机器人上的全链路实战。核心思想为什么机器人需要专属VLM而不是直接用开源模型很多人觉得LLaVA这种通用VLM已经能理解图像和语言直接拿来用不就行了但机器人场景有个本质区别——通用VLM回答的是这是什么机器人需要的是我该怎么做。举个例子你问LLaVA桌上有几个苹果它能答对但你说请把苹果放到篮子里它输出的文本token根本无法直接映射到机械臂的关节角度或末端位姿。更关键的是机器人任务中的视觉输入往往带有强烈的时序性和空间关系比如抓住杯柄和抓住杯身在图像上可能只差几个像素但动作完全不同。通用VLM的视觉编码器是在ImageNet级别的分类任务上预训练的它提取的特征更偏向语义类别而不是精细的几何结构。所以我们需要在机器人采集的真实数据上做指令微调让VLM学会把视觉特征和机器人动作指令对齐。数据准备比模型训练更耗时的环节我用的数据集来自自家实验室的UR5e机械臂采集了大约5000条真实操作轨迹每条轨迹包含多帧RGB图像、关节角度、末端位姿和对应的自然语言指令。这里有个关键点——数据质量直接决定微调效果宁缺毋滥。我第一版数据用了自动标注结果模型学到的全是噪声后来改成人工逐条审核虽然耗时但效果立竿见影。数据格式上我参考了LLaVA的对话模板但做了机器人场景的适配。每条样本是一个多轮对话比如{ image: frame_0001.jpg, conversations: [ {from: human, value: 观察当前场景将红色方块推到目标位置。}, {from: gpt, value: 动作序列: 移动到(0.32, 0.45, 0.12)抓取移动到(0.58, 0.62, 0.10)释放。} ] }注意这里的动作序列不是自然语言而是结构化的控制指令。我试过让模型直接输出关节角度数值但效果很差因为数值回归对LLM来说不是强项。后来改成输出离散的动作原语序列比如移动-抓取-移动-释放每个原语附带坐标参数这样模型只需要学会选择正确的原语组合和参数难度大大降低。模型架构LLaVA的视觉-语言桥接机制LLaVA的核心架构分三部分视觉编码器CLIP ViT-L/14、语言模型Vicuna-7B、以及连接两者的投影层MLP。在机器人微调时我选择冻结视觉编码器和语言模型的大部分参数只训练投影层和语言模型的LoRA适配器。这里有个重要的调参经验——视觉编码器千万别解冻我试过一次解冻全部参数结果模型直接过拟合到训练集的背景纹理上换了个场景就完全失效。视觉编码器在CLIP上已经学到了很强的语义特征我们只需要让投影层学会把这些特征映射到机器人动作空间。投影层的设计也有讲究。原始LLaVA用的是简单的两层MLP但在机器人任务中我发现这个MLP的容量不够因为动作指令需要同时融合空间位置和语义信息。我改成了三层MLP中间加了一层LayerNorm效果提升明显。具体结构classProjectionLayer(nn.Module):def__init__(self,vision_dim1024,text_dim4096):super().__init__()self.linear1nn.Linear(vision_dim,2048)self.ln1nn.LayerNorm(2048)self.linear2nn.Linear(2048,2048)self.ln2nn.LayerNorm(2048)self.linear3nn.Linear(2048,text_dim)self.dropoutnn.Dropout(0.1)defforward(self,x):# 这里踩过坑原始LLaVA的投影层没有LayerNorm导致特征分布不稳定xF.gelu(self.linear1(x))xself.ln1(x)xF.gelu(self.linear2(x))xself.ln2(x)xself.dropout(x)returnself.linear3(x)指令微调从零训练的关键步骤训练过程我分两个阶段。第一阶段是特征对齐只训练投影层用对比学习的方式让视觉特征和文本特征在同一个空间里。这个阶段学习率要小我用的是1e-4batch size 16训练2000步。第二阶段是端到端微调同时训练投影层和语言模型的LoRA学习率降到5e-5batch size 8训练5000步。这里有个容易犯的错误——直接用全量参数微调。我一开始用全量微调显存直接爆掉7B模型全量微调需要至少80GB显存而且训练速度极慢。后来改用LoRArank设为16alpha设为32参数量只有原来的0.5%但效果几乎一样。LoRA的配置frompeftimportLoraConfig,get_peft_model lora_configLoraConfig(r16,lora_alpha32,target_modules[q_proj,v_proj,k_proj,o_proj],lora_dropout0.05,biasnone,)训练损失函数我用的是标准的交叉熵但加了一个动作序列的mask——只计算动作原语部分的loss忽略指令文本的loss。这样模型会更专注于学习动作生成而不是重复指令。实验与调优那些让人头秃的细节第一个实验跑完模型在训练集上loss降到0.8但验证集loss还在2.3明显过拟合。我检查了数据发现训练集里80%的样本都是抓取-放置任务其他动作类型太少。后来我做了数据增强——对图像做随机裁剪、颜色抖动、旋转同时把动作序列做同义替换比如抓取换成拾起过拟合问题缓解了不少。还有一个细节是学习率调度。我用了cosine退火但warmup步数设得太短200步导致前期loss震荡剧烈。后来改成warmup 500步loss曲线平滑多了。另外梯度裁剪也很重要我设了max_grad_norm1.0不然偶尔会出现loss突然跳到NaN的情况。最让我头疼的是动作参数的数值稳定性。模型输出的坐标参数有时候会超出合理范围比如x坐标输出0.9但机械臂的工作空间只有0.5。我加了一个后处理层用tanh激活函数把输出限制在[-1, 1]再映射到实际工作空间。这个改动让成功率从62%提升到78%。部署到机器人从模型到真实控制的最后一公里模型训练好之后部署到机器人上又是一堆坑。我用的推理框架是vLLM但vLLM对LoRA的支持不太好需要先合并LoRA权重再加载。合并之后模型大小从7B变成7.1B显存占用多了2GB但推理速度反而快了——因为vLLM的continuous batching对完整权重更友好。推理流程是机器人相机采集图像 → 预处理resize到336x336归一化→ 输入VLM → 输出动作序列文本 → 解析成控制指令 → 发送给机械臂。这里有个延迟问题VLM推理一次大约需要300ms加上图像传输和指令执行整个闭环延迟在500ms左右。对于静态抓取任务够用但如果是动态目标就有点吃力。我后来用TensorRT对视觉编码器做了加速延迟降到了180ms。还有一个容易忽略的点——指令文本的格式必须和训练时完全一致。我训练时用的指令是观察当前场景将红色方块推到目标位置部署时如果用户说把红方块推过去模型就懵了。我加了一个指令规范化模块把用户输入映射到训练时的模板格式效果立竿见影。落地经验总结给后来者的几点建议第一数据比模型重要。我花了两周时间清洗和标注数据训练只花了两天。如果你的机器人任务效果不好先检查数据别急着调模型。第二冻结策略要谨慎。视觉编码器冻结是安全的但语言模型可以尝试部分解冻比如只解冻最后几层有时候能提升动作序列的连贯性。第三动作表示方式很关键。直接回归数值坐标效果差离散动作原语加参数的方式更稳定。第四别迷信大模型。7B的LLaVA在机器人任务上已经够用13B的模型推理延迟翻倍但成功率只提升2-3%性价比不高。第五做好失败案例的收集。我建了一个失败样本库每次部署后把失败的图像和指令存下来定期用这些数据做增量训练模型会越来越强。最后说个真实感受——做机器人VLM和做通用VLM完全是两个思路。通用VLM追求知识广度和泛化能力机器人VLM追求的是在特定场景下的动作准确性和鲁棒性。所以别把通用VLM的评估指标比如VQA准确率直接套到机器人任务上我们更关心的是任务成功率、动作执行时间、以及在不同光照和遮挡条件下的稳定性。这些指标需要你自己定义和统计没有现成的benchmark。希望这篇笔记能帮你少踩几个坑有具体问题欢迎在评论区交流。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进