在 VLA(Vision-Language-Action)模型的训练和部署中,“L无用”的说法每隔一段时间就会出现一次。现象通常很一致:把语言指令从输入里删掉,动作预测的成功率没有明显下降,甚至个别任务上还会更好。接触过具身操作项目的工程师,多数会在某个阶段怀疑语言分支是不是只是占位符。我自己的判断是,“L无用”这个结论大概率把因果看反了。真正的问题不是语言模态没有价值,而是语言指令没有被放进正确的位置。这里的“位置”既指句子里的空间语义,也指它在模型序列、注意力层和训练数据中的角色。当位置指令真正落到视觉区域,并把文本 token 注入到决策层能够持续访问的地方,任务级泛化和组合泛化会明显拉开差距。在抓取、摆放和排序这类常见拆解任务里,这种差距可以达到 20%-40% 的相对提升。这篇文章会把概念、代码、实验和排错串起来,讲清楚这条链路到底应该怎么搭。
1. VLA 里的「L」到底承担什么,为什么会被判无用
1.1 “L”是语言条件,不是任务标题
VLA 模型的输入通常是三部分:视觉观测、文本指令和动作标签。视觉观测可以是单帧图像,也可以是多帧短视频序列;文本指令用自然语言描述当前要完成的目标;模型输出的是离散动作 token 或连续动作序列。这里的“L”就是 Language,也就是语言条件。
很多人会把语言指令理解成任务标题,比如“把红色方块放到左侧托盘”。这句话表面是一个标题,实际在训练和推理中起到的是条件约束作用。模型面对同一个桌面场景时,可能存在多个可执行任务:把方块放到左侧托盘、把方块放到右侧托盘、把方块按颜色分类摆好。如果只有图像,模型无法确定当前应该执行哪一个意图;语言指令的作用就是把多任务空间压缩到当前一个分支上。
语言条件真正重要的地方在于抽象性。视觉上“红色方块”可以因为光照、角度、背景发生变化,但指令文本“红色方块”是一个稳定的符号;视觉上“左侧托盘”会因为相机视角不同而改变像素坐标,但语言里“左侧”提供的是相对空间关系。模型如果能学会从语言符号到视觉区域的映射,就能在未见过的颜色、物体组合和摆放模式下依然做出正确动作。这也是谈论 VLA 泛化能力时最核心的支撑点。
1.2 “无用感”通常来自三个具体机制
如果语言真的没用,那 VLA 完全可以叫 VA。但项目里出现“删掉语言后成功率不降”的现象,往往不是模态没用,而是语言信号在传播链路上被截断了。
第一个机制是语言 token 被长序列稀释。许多实现会把文本 token 和视觉 token 拼接成一个整体序列,文本放在最前面。Transformer 处理长序列时,越靠后的 token 携带的信息越强。如果视觉 token 有几十甚至上百个,动作 token 在后面生成,语言 token 对最终动作的影响会被大幅压缩。
第二个机制是视觉捷径在训练时占据主导。若训练数据中同一个物体颜色和同一个目标位置反复出现,模型不需要读语言也能从视觉特征推断出动作。例如数据集里红色方块永远只出现在左侧托盘附近,那么模型只要看到红色,就能猜出要放到左侧。训练损失会把视觉分支推到更优解,语言分支的梯度贡献趋近于零,最终表现为“语言没用”。
第三个机制是位置指令语义模糊。指令写“把红色方块放到托盘里”,但桌面上有两个托盘。模型不知道放到哪一个,语言条件变成一个无效信息。这种情况下模型只能靠视觉猜测,或者干脆忽略语言。它不是在学习语言,而是在学习一个没有区分度的噪声标签。
这三类问题叠加,很容易让评估者得出“L无用”的结论。但拆开看,第一类是注入位置问题,第二类是训练数据问题,第三类是指令内容问题,都不是语言本身失效。
1.3 语言条件的不同形态决定了泛化上限
语言条件在模型里的形态不同,最终表现差异很大。下面对比几种常见处理方式。
| 语言处理方式 | 模型实际看到的信息 | 典型风险 | 泛化表现 |
|---|---|---|---|
| 只编码成一个全局 CLS 向量 | 语言被压缩成一条向量,信息密度极低 | 决策层基本读不到细节 | 组合泛化差 |
| 文本 token 与视觉 token 前后拼接 | 开头位置的语言对后续注意力递减 | 长序列稀释语言信号 | 中等但不稳定 |
| 每层交叉注意力访问语言 token | 每个动作 token 都能查询语言特征 | 实现成本偏高 | 稳定提升 |
| 语言 token 与视觉区域显式绑定 | 语言中名词对应具体 bbox | 需要额外标注或检测器 | 组合泛化上限最高 |
从工程角度看,先不要争论“语言有没有用”,而是先确认语言条件以什么形态进入决策层。如果连语言特征都无法被后续头部访问,那无论模型多大,都会得到 L 无用的结论。下一节会把“位置”这个概念拆细,因为标题里的“用对位置指令”其实包含三个不同维度。
2. 把“位置指令”拆开:文本空间、token 序列和时间线都要对齐
2.1 文本中的空间位置:指令要能落回视觉坐标
“位置指令”最容易联想到的是文本里表达空间关系的词:左、右、上、下、内侧、外侧、旁边、附近。位置指令的目标是让模型知道物体应该去往哪个空间区域,同时把文本里的方位词映射到图像坐标上。
举个例子。指令 A 是“把红色方块放到托盘里”,视觉画面里有两个托盘。这个指令对模型来说没有提供足够的位置约束,它必须在画面里寻找“哪个托盘”。指令 B 是“把红色方块放到左侧托盘”,这里多了一个方位词“左侧”,模型需要把“左侧”翻译成图像中 x 坐标较小的托盘区域。指令 C 是“以左侧托盘为参照,把红色方块放到托盘内侧”,这种相对位置表达更复杂,需要同时理解参照物和内在方向。
位置指令的类型和训练数据形式可以按下面表格整理。
| 指令类型 | 例子 | 需要的视觉锚点 | 理想训练数据形式 |
|---|---|---|---|
| 绝对方位 | “放到图像右下角” | 图像四等分边界 | 目标区域 bbox 与方位标签 |
| 相对方位 | “放到左侧托盘” | 托盘 bbox | 参照物 bbox + 左右关系 |
| 顺序关系 | “从左到右依次摆放” | 多个目标 bbox | 排序序列 |
| 距离关系 | “放到离红色方块更近的位置” | 两个物体 bbox | 距离比较结果 |
| 嵌套关系 | “放到托盘内侧” | 托盘内外轮廓 | 内外区域 mask |
如果训练数据里只有“放到托盘里”这类弱位置指令,语言的价值就会被抹平。要让语言起作用,第一步是保证指令里真的包含位置约束,并且这种约束可以被自动化标注成视觉区域。
2.2 序列里的 token 位置:语言不能只在开头出现
第二个“位置”是模型序列中的位置。很多 VLA 实现会把文本 token 放在输入序列开头,后面接视觉 token,再接动作 token。从因果注意力角度看,越靠后的 token 只能看到它前面的信息,最终动作 head 在计算时,语言 token 已经隔了很长一段距离。
这里要区分两种情况。如果视觉 token 数量很少,比如只有 16 或 32 个,语言放开头的问题还不明显;如果视觉 token 有 256 个以上,并且动作是逐 token 自回归生成的,语言影响很容易被视觉中间特征稀释。更严重的是,有些实现只在最后拿语言编码器的 pooler_output 做一次粗粒度条件,相当于把整句话压成一个向量,所有的方位细节都丢掉了。
推荐做法是让语言 token 出现在每个决策点的可访问范围内,而不是只出现在开头。一种有效手段是使用交叉注意力:把动作 token 或视觉 token 的隐状态当 query,把语言 token 的 key/value 引入每一层 Transformer 块。这样每个动作 token 在生成时都能直接查询语言特征,语言信号不会因为序列长度而衰减。
如果把语言条件放在开头,又希望保住信息,可以考虑在关键层重复插入语言 token。这种设计虽然增加计算量,但能明显提升语言利用率。
2.3 时间线上的条件位置:每次决策都要能读到语言
第三个“位置”是时间维度。机器人操作任务通常是序列决策,模型在一个时间窗口内连续输出动作。如果语言指令只在第一帧输入时拼接,后续帧决策时语言特征只能依赖初始上下文,长程任务很容易出现“把语言忘了”的情况。
假设任务要求“先抓红色方块,再放到左侧托盘,最后把蓝色方块放到右侧托盘”。模型在第一步看到了语言指令,但执行到第三步时,中间已经过了很多视觉帧和动作步骤。如果语言条件没有持续进入每一帧的编码器,模型可能只靠当前视觉信息猜测下一步,最后表现就是“语言对最终结果影响有限”。
正确做法是把语言指令作为整个轨迹的全局条件,每一帧决策时都要保持可读。具体实现可以每帧拼接语言 token,也可以在策略模型的解码器内部通过 cross-attention 持续注入语言特征。如果模型是视频级别的联合编解码,要确保语言特征参与每一帧的注意力计算,而不是只作为视频开头的一个额外 token。
3. 最小验证:先写一个判断 L 有没有真正参与决策的样例
3.1 选定可控任务:把物体放到指定位置
与其争论 L 有没有用,不如先做一个最小验证。建议选一个非常容易控制的桌面操作任务:场景里有若干物体和两个托盘,模型根据语言指令把指定物体放到指定托盘。这类任务视觉差异清晰、空间关系可标注、动作序列短,非常适合排查语言条件是否真正发挥作用。
一个样本可以设计成下面这样的 JSON 结构。实际项目中,rgb是保存图像路径,instruction是文本指令,anchors是语言中提到的对象与视觉区域的绑定关系,action是与训练环境对齐的动作参数。
{ "task": "place_object", "rgb": "obs_frame_001.jpg", "instruction": "把红色方块放到左侧托盘", "anchors": [ { "label": "红色方块", "bbox": [180, 220, 260, 310] }, { "label": "左侧托盘", "bbox": [120, 340, 210, 430] } ], "target_move": { "from": "红色方块", "to": "左侧托盘" }, "action": [128, 180, 220, 320, 0.0] }要点不是 JSON 本身,而是anchors这一层。它把“红色方块”“左侧托盘”这些语言中的名词,显式映射到图像的 bbox。有了它,模型才有机会学习“语言符号 -> 视觉区域”的对应关系。
3.2 构造带位置锚点的指令数据
接下来要把语言指令和视觉区域绑定起来。训练时锚点可以来自人工标注或现成检测器;推理时如果检测器不可用,也可以用指代分割模型或 attention 热图回调。关键在于,不能只给文本 token,就期待模型自动学会复杂的指代关系,特别是当训练数据量不够大的时候。
下面是区域特征抽取的示意代码。它把视觉特征图上的检测框映射为区域特征,供后续与文本 token 拼接。
import torch import torchvision.ops as ops def extract_anchor_features(visual_feat, boxes, object_detector): """从视觉特征中提取语言锚点对应区域的特征。 visual_feat: [B, C, H, W] boxes: dict[str, list[int]],如 {"红色方块": [180, 220, 260, 310]} """ anchor_features = {} for label, box in boxes.items(): box_tensor = torch.tensor([box], dtype=torch.float32) roi_feat = ops.roi_align( visual_feat, box_tensor, output_size=(1, 1), spatial_scale=1.0, ) anchor_features[label] = roi_feat.flatten(1) # [B, C] return anchor_features注意,roi_align的spatial_scale必须与视觉特征图的缩放比例一致。常见错误是特征图是原图的 1/32,但 box 仍然使用原图坐标,导致区域特征完全错位。
3.3 用两个指标量化语言利用率
做完训练后,需要评估语言到底有没有被用到。推荐两个指标。
第一个是语言利用率。正常条件下得到成功率normal_sr,然后把语言特征清零,只保留视觉特征,得到remove_lang_sr。语言利用率定义为:
def language_coverage(normal_sr, remove_lang_sr): return 1.0 - (remove_lang_sr / max(normal_sr, 1e-6))如果语言利用率接近 0,说明模型不看语言也能完成动作,语言分支实际没有参与决策。如果语言利用率为正值,说明移除语言后成功率下降,模型确实依赖语言信息。
第二个是组合泛化分数。单独在“训练中未出现过的新颜色 + 新位置组合”测试集上计算成功率,再和普通测试集成功率相除。组合泛化分数越低,说明模型越依赖视觉捷径,语言没有提供抽象泛化能力。
建议每次训练跑完,先记录这两组指标。如果语言利用率小于 0.05,就要回去检查语言注入位置,而不是继续调模型容量。
3.4 为什么必须先做这个最小验证
很多项目直接进大规模 VLA 训练,结果语言分支对不对都不知道。先跑一个最小验证的收益是能快速定位问题。下面是一个可行的运行流程:
python train_vla.py --config configs/position_instruct.yaml python eval_refbench.py --split normal --n-trials 50 python eval_refbench.py --split remove-lang --n-trials 50正常成功率、无语言成功率和组合泛化成功率三组数字一出来,就能判断“L无用”是真实结论还是工程假象。如果正常成功率和无语言成功率几乎一样,说明语言没有进入决策链,后面要优先改注入机制,而不是继续加数据。
4. 让「L」真正进入决策链:规范化工程做法
4.1 用多层跨模态融合代替开头拼接
开头拼接是最简单的实现方式,但它只在模型输入层做了一次融合。为了让语言在整个 Transformer 网络中保持影响,推荐在多层解码块中加入 cross-attention。下面是一个 PyTorch 风格的简化实现。
class VLADecoderLayer(nn.Module): def __init__(self, hidden_size, use_lang_cross_attention=True): super().__init__() self.self_attn = nn.MultiheadAttention(hidden_size, num_heads=8) if use_lang_cross_attention: self.lang_attn = nn.MultiheadAttention(hidden_size, num_heads=8) else: self.lang_attn = None self.ffn = nn.Sequential( nn.Linear(hidden_size, hidden_size * 4), nn.GELU(), nn.Linear(hidden_size * 4, hidden_size), ) self.norm1 = nn.LayerNorm(hidden_size) self.norm2 = nn.LayerNorm(hidden_size) def forward(self, x, lang_feat): # x: [seq_len, B, hidden_size] x = x + self.self_attn(x, x, x)[0] x = self.norm1(x) if self.lang_attn is not None: # 当前 token 作为 query,语言 token 作为 key/value x = x + self.lang_attn(x, lang_feat, lang_feat)[0] x = self.norm1(x) x = x + self.ffn(x) return x这里的关键是lang_feat全程可访问。每个动作 token 在生成时都能查询语言特征,语言就不会被前面的视觉 token 遮挡。实际项目中可以对一部分层加lang_attn,而不是所有层都加,先看语言利用率是否有变化。
4.2 训练阶段对语言做随机丢弃
如果训练时语言特征一直存在,模型可能学到一种捷径:视觉信息足够就忽略语言,语言信息足够就不看视觉。为了逼模型同时利用两种模态,可以在训练时随机丢弃语言条件。
def forward_with_lang_dropout(text_embed, visual_embed, lang_dropout=0.15): if torch.rand(1) < lang_dropout: lang_embed = torch.zeros_like(text_embed) else: lang_embed = text_embed seq_feat = torch.cat([lang_embed, visual_embed], dim=-2) return seq_feat语言丢弃的做法参考了多模态 Dropout 的思路。比例不要太高,0.1 到 0.2 是常见区间。过高会导致训练不稳定,因为模型在测试时看到的语言条件不充分;过低又无法形成约束。同时,推理时要关闭丢弃,否则语言条件会随机被清空。
4.3 位置锚定:从文本指代到视觉区域
如果只做 cross-attention,模型仍然可能需要自己学到“左侧托盘”对应哪个视觉区域。更直接的办法是把语言中提到的名词锚定到某个视觉区域特征上,替换或增强对应的文本 token。这样可以减小学习难度。
在训练数据里,位置锚点已经以 bbox 形式存在。推理时,可以先用一个轻量级指代分割模型找到“左侧托盘”的 mask 或 bbox,再提取区域特征。下面是一个简化流程:
prompt = "把红色方块放到左侧托盘" anchors = referring_segmenter(visual_frame, prompt) # anchors 示例: {"红色方块": [bbox], "左侧托盘": [bbox]} visual_regions = extract_anchor_features(visual_feat, anchors, detector=None) # 将语言 token 中的对象名区域做替换,或额外拼接区域 token lang_token = text_encoder(prompt).last_hidden_state region_token = torch.stack(list(visual_regions.values()), dim=-2) seq_feat = torch.cat([lang_token, region_token, visual_feat], dim=-2)这里并不要求把所有名词都解析出来。只要位置短语能被定位,模型就能得到更强的空间条件。实际项目中可以先从规则开始,例如检测到文本中包含“左侧/右侧/上方/下方”,再结合检测器找到对应参照物。用规则跑通后再替换成神经网络解析器。
4.4 构造位置指令的完整模板与负样本
仅仅把“位置指令”理解为“包含左、右”是不够的。训练时需要用不同表达方式覆盖同一个位置关系,并且要加入负样本,让模型学会拒绝错误指令。
| 类型 | 指令示例 | 说明 |
|---|---|---|
| 无位置弱指令 | “把红色方块放到托盘里” | 通用指令,便于学习任务主流程 |
| 绝对位置强指令 | “把红色方块放到右侧托盘” | 提供明确空间约束 |
| 相对位置强指令 | “把红色方块放到红色托盘左侧” | 需要理解参照物 |
| 顺序位置指令 | “先放红色,再放蓝色” | 提供时间顺序信息 |
| 负样本 | “把蓝色方块放到左侧托盘” | 当前动作与指令矛盾 |
| 同义改写 | “red block to the left tray” | 增加语言多样性 |
负样本的主要作用是防止模型把语言当作可忽略标签。如果模型看到“蓝色方块”指令仍然执行红色方块动作,说明语言没有影响决策。训练时可按一定比例混入负样本,让模型在动作与语言不一致时学会降低对应动作的概率。
5. 如何做消融实验,理解“20%-40%”这个量级
5.1 正确切分组合,而不是随机切分
很多项目复现不出语言带来的提升,原因是测试集切分方式有问题。如果训练集和测试集都来自同一个数据分布,颜色组合、位置组合高度重合,那么模型靠视觉记忆就能拿到高成功率,语言价值很难体现。
正确做法是做组合切分。例如训练集里出现“红色方块 -> 左侧托盘”和“蓝色方块 -> 右侧托盘”,测试集里只出现“红色方块 -> 右侧托盘”和“蓝色方块 -> 左侧托盘”。这样测试集要求模型把“颜色”和“位置”两个属性重新组合,而不是记住训练时的固定搭配。
切分伪代码如下:
train_samples = [ s for s in samples if (s["color"], s["position"]) in train_combinations ] test_samples = [ s for s in samples if (s["color"], s["position"]) in test_combinations ]关键是保证test_combinations中的组合在训练集中从未同时出现。否则测试结果只能说明模型记住了场景,不能说明语言泛化能力。
5.2 典型消融表:从无语言到完整位置锚定
下面这张表展示了不同条件下可能出现的结果排序。关键不是绝对数值,而是不同方案在“普通任务”和“组合泛化任务”上的差距。
| 方案 | 普通任务成功率 | 组合泛化成功率 | 语言利用率 | 结论 |
|---|---|---|---|---|
| A. 无语言,仅视觉 | 0.82 | 0.58 | 0 | 视觉捷径强,组合能力弱 |
| B. 语言压缩成单向量 | 0.81 | 0.55 | 0.02 | 语言信息几乎丢失 |
| C. 语言 token 开头拼接 | 0.85 | 0.68 | 0.18 | 有提升但不稳定 |
| D. 多层交叉注意力 + 位置锚定 | 0.90 | 0.83 | 0.33 | 组合泛化差距明显 |
方案 D 相比方案 A,在组合泛化成功率上从 0.58 提升到 0.83,相对提升大约 43%。如果报告的是普通任务上的相对提升,数字会小一些;如果报告的是组合泛化任务,20%-40% 的量级很常见。这里的关键不是同一个固定数值,而是“语言在正确位置时,组合泛化能力会显著高于无语言和弱语言基线”。
5.3 为什么这个量级不违反直觉
20%-40% 不是指所有任务都能提升这么多,而是指“语言本该起作用但没用起来”的场景中,修正位置指令后的收益。具体幅度受以下因素影响:
- 任务本身的歧义程度。任务越复杂,语言约束越重要,提升越明显。
- 视觉捷径强度。如果物体颜色和目标位置在数据中高度相关,视觉分支配得起所有动作,语言收益被压缩。
- 指令表达多样性。训练时同一种位置关系如果只有一种写法,模型很难把语言泛化到新表达。
- 模型规模。小模型可能没有足够容量学习跨模态绑定,语言收益体现不充分。
因此,如果看论文或项目报告时发现提升只有 5%,不一定是语言无用,可能是测试集没有做组合切分。如果看到提升超过 40%,也要检查是否出现了数据泄漏,比如测试集中出现了与训练集相同的位置短语或相同背景。
6. 生产环境引入前的排错清单和落地建议
6.1 指令在推理时失效,按六步排查
如果在推理阶段发现 VLA 模型没有执行语言指令,不要直接调大模型,按下表顺序排查。
| 排查顺序 | 检查内容 | 常见问题 | 处理建议 |
|---|---|---|---|
| 1 | 输入文本 | tokenizer 截断后位置短语被切掉 | 打印 token 序列,确认左右托盘等词存在 |
| 2 | 位置锚点 | 检测器没有找到“左侧托盘” | 检查 bbox 坐标和锚点名称是否对齐 |
| 3 | 注意力依赖 | 语言 token 在 attention 分数中是零权重 | 输出各层 attention,观察语言 token 是否被访问 |
| 4 | 训练推理一致性 | 训练用“left tray”,推理用“左侧托盘” | 统一文本模板,或加入同义改写增强 |
| 5 | 网络结构 | 语言只在开头嵌入,后续层不可见 | 增加 cross-attention 或每帧条件注入 |
| 6 | 采样策略 | 温度过高导致动作随机性掩盖语言影响 | 降低采样温度,或先使用贪心解码验证 |
这六步覆盖了从输入、特征、结构到解码的完整链路。大多数“语言指令不生效”的问题,都出在第 2 步和第 5 步。
6.2 常见坑与对应解决方式
第一个坑是训练固定模板、推理换同义表达。训练集里只写“左侧托盘”,推理时用户说“左手边那个盘子”,模型没见过这种表达,语言利用率就会下降。解决方式是训练时做指令改写增强,至少用三种不同句式表达同一个位置关系。
第二个坑是坐标与视觉特征图尺度不一致。roi_align的spatial_scale配错,区域特征取到完全错误的像素,位置锚定变成噪声。建议把所有 bbox 先从原图坐标归一化到 0-1,再根据特征图尺寸换算。
第三个坑是语言丢弃比例过大。0.5 的语言丢弃会让模型长期在缺少语言的条件下训练,最终模型学会完全不依赖语言。建议从 0.1 开始调,优先检查语言利用率和组合泛化分数,而不是只看训练 loss。
第四个坑是长任务只把语言放开头。一个 50 步的机器人任务,在最初的输入里拼接语言 token,后续 40 步每一步都在没有语言约束的情况下决策。长任务场景里应该每帧或每个动作解码步都持有一个语言条件向量。
6.3 上架前的可复用检查清单
生产环境与学习环境差异很大。模型在开发阶段能跑通,不代表部署后语言条件依然有效。下面清单可以直接作为发布前检查项。
- 数据层面:每个位置关系至少有 20 组不同坐标;每种语言表达至少 3 种不同写法;测试集包含训练集未出现的新组合。
- 模型层面:确认语言 embedding 在反向传播中有梯度;确认 cross-att