← 报告首页 / reports index
2026-09-05 · 策略改进方向 · 每条结论都标了证据等级

B0:把画面按物体切开,而不是按方格切开

这一页讲清楚 B0 的逻辑:现在的模型到底在学什么、为什么它换个杯子就不行、 B0 在机制上改了哪一步、以及在什么条件下应该判它「不做」。 接口部分已经读通并实测过;耗时部分还没测,那是能不能做的唯一决定项。

一句话:B0 是在 SmolVLA 的输入里额外加几个「物体 token」, 让模型学「那个物体在哪」而不是「画面第 137 号方格亮起来了」。 接口已确认可行(约 15 行改动,不用解冻主干、不用重采数据); 它唯一站得住的理由是换杯子的泛化,不是速度; 做不做取决于一个还没量的数——物体检测器在 Jetson 上的单帧耗时,预算只有 337 ms。

1. 起点:现在的模型在学什么

所有逻辑都从这一步来。SmolVLA 每次推理,输入端是这样拼起来的 (modeling_smolvla.py:552 的 embed_prefix,读过源码):

embs = []
for img in images:                      # 三路相机
    embs.append(self.vlm_with_expert.embed_image(img))   # 每路 64 个 token
embs.append(lang_emb)                   # 语言:"Pick up the cup with the right arm."
embs.append(self.state_proj(state))     # 17 维关节 → 960 维,1 个 token
embs = torch.cat(embs, dim=1)           # 拼成 prefix,action expert 在上面做注意力

关键在那 192 个视觉 token 是怎么来的:512×512 的图,切成 16×16 的 patch 得 32×32=1024 块, 再经 scale_factor=4 的 pixel shuffle 压 16 倍 → 每路 64 个,三路共 192 个。

这就是问题的根:这 192 个 token 是按画面位置切的方格,不是按物体切的。 第 137 号 token 永远代表「画面右下那一块」——不管那块地方现在是杯子、是桌面、还是操作员的手。

所以模型能学到的关联只能是「当第 137 号方格呈现某种纹理时,动作应该是这样」。 杯子换个外观,那个方格的纹理就变了,关联随之失效。 这就是它换杯子脆弱的机制层面的原因,不是「训练不够」那么笼统。

2. B0 改的是哪一步

B0 不动那 192 个方格 token,而是在它们之外再加 N 个物体 token:

  1. 先用一个类别无关的检测/分割器把画面切成若干「物体」——不问它是什么,只问「这儿有一个东西,它占这块区域」
  2. 每个物体变成一个向量(框的几何 + 该区域的特征)
  3. 过一个新的 object_proj 投到 960 维,append 进上面那个 list

模型于是同时拿到两样东西:方格里的纹理(原来就有),以及「这儿有一个物体,它在这个位置」。 后者对外观变化不敏感——磨砂杯和透明杯,检测器都只报「这儿有个物体」,位置一样,token 就一样。

学的东西从「画面第 137 号格子亮起来时该动」变成「那个物体在哪」。这是 B0 的全部内容。

先例两条,成功率不要照抄

VIOLA

CoRL 2022 · 预训练 RPN 出类别无关候选框,transformer 策略在候选框上做注意力
报 +45.8%。
我们的判断:思路可借,但它不是 VLA 架构,那个数字不能直接搬到 SmolVLA 上。

Oat-VLA

同思路做在 OpenVLA 上 · FT-Dinosaur 出物体 token,256→16 · 真机 29/49 vs 20/49
我们自己重算过:按它自己给的计数做 Fisher 精确检验,p=0.106,不显著。 所以引用它的方法,不引用它的成功率。它的 256 也是 OpenVLA 的数(224×224,patch 14)—— 我们的起点是 192,本来就比它低。

3. 为什么现在只剩这一条

2026-09-04 把四个分支逐一排除:

方案结论依据
B0物体 token 当结构先验主攻不需要内参、不需要手眼、不需要重采数据
B1深度在策略外用(记杯位、做诊断)降级最便宜的那层诊断当天用已有轨迹文件免费拿到了;剩下的要手眼
B2Depth Helps 式注入搁置深度必须以原生格式重录,现有数据是旧旁路格式
B3点云策略(DP3/iDP3)已排除换策略家族,整条链路重来,操作者判定不可能
顺带澄清一条常见误解:手眼标定不是以上任何一条的前置。 它挡住的是视觉伺服、朝人递出、人身安全区,都不是当前瓶颈。 不要因为手眼没做成而阻塞 B0。

4. ★ 核心逻辑:它只解决两种泛化里的一种 ★

这是判断 B0 值不值最关键的一条区分。之前的表述把两者混为一谈,是错的:

换杯子
换一只不同外观的杯子,位置不变
换位置
同一只杯子挪到新位置
需要的关节轨迹和数据里的完全一样数据里可能根本没有
受 35° 覆盖限制吗不受受
B0 能不能解决能,这正是它对口的问题不能
要不要新数据不要要

换杯子是外观泛化:同一个位置换一只杯子,动作一模一样,变的只是画面。 B0 把「这个位置有个物体」和「这个物体长什么样」拆开,模型学前者——正是 B0 对口的问题,且不需要任何新数据。

换位置才受数据覆盖限制。实测(合并集 202 集,方法见文末):

35.1°
右臂 shoulder_pan 范围
std 只有 5.15°——现役模型就是右臂的
57.3°
左臂 shoulder_pan 范围
std 11.05°
202 集
合并集总量
右臂 91 / 左臂 111
对演示的含义:如果演示只要做到「换一只杯子照样抓」, B0 是对的工具,而且数据覆盖不构成障碍。 要同时做到「挪到任意位置也抓」,那是 B0 + 补数据两件事, 而补数据更确定——架构变不出数据里没有的东西。

5. 接口:具体改哪十几行

embed_prefix 就是一个 list 最后 concat,插物体 token 等于再 append 一项 [B, N_obj, 960]:

# 新增(照抄 state_proj 的写法,它在 modeling_smolvla.py:508)
self.object_proj = nn.Linear(obj_feat_dim, 960)

# embed_prefix 里,state 之后
embs.append(self.object_proj(object_feats))    # [B, N_obj, 960]

连带要改的两处(不改会静默出错):

★ 2026-09-05 读源码后的三条更正 ★

① 「加法不干扰预训练」可以严格成立,而且能证明。 policies/common/vla_utils.py:87-90 里 att_masks 不是开关,是块边界标记: cumsum 给出块号,token i 能看到 token j ⟺ 块号(j) ≤ 块号(i)。 现在图像和语言是块 0、state 是块 1(源码注释:"image and language inputs do not attend to state or actions")。 把物体 token append 在最后并标 att_mask=1(块 2),图像和语言就看不到它, 它们的输出逐比特不变——不是「影响很小」,是数学上完全不变。

但写错位置性质就反了:插在 state 之前且标 0,物体 token 进块 0, 图像和语言会注意到它,预训练特征直接被改写。 全部风险集中在这一个数上。详见 深度·点云·VLA 那一页的第 7 节。
② 「prefix_length 的 padding 分支」这条顾虑不存在,可以划掉。 部署 checkpoint 的 config.json 里 prefix_length = 0, 而代码是 if seq_len < self.prefix_length——永远不会触发。加 token 不涉及 padding 或截断。
③ 一条原本没写的真约束:N_obj 必须固定。 att_masks 是 python list,att_masks[None,:].expand(bsize,-1) 对整个 batch 共享同一长度。 检出不足时用 pad_masks=0 补;pad_2d_masks 会把 padding token 在 key 和 query 两侧都屏蔽,所以空物体不污染任何东西。

另外两处第 0 步就会发生、但不致命的影响:action expert 必然受影响(那正是目的,多了 N 个 key, softmax 分母变了);object_proj 随机初始化会在训练最初喂噪声—— 把最后一层零初始化可缓解,但即使 value 全零,多出的 key 仍参与 softmax 归一化、会稀释原有权重, 所以「零初始化 = 恒等」是错的,仍需 warmup。

好消息:和现有训练模式兼容。部署中的 config.json 是 freeze_vision_encoder: True、train_expert_only: True、train_state_proj: True—— 现在就只训 action expert,SigLIP 一个参数不动。加一个 object_proj 不用解冻主干。 而且物体 token 可以从已录的 RGB 离线算,所以不用重采数据。 按现有配置重训约 1.3 小时 / $1。

6. 为什么走「加法」不抄 Oat-VLA 的「替换」

Oat-VLA 用 N 个物体 token 取代原来的视觉 token,图的是推理变快。 我们实测过,那个动机在我们这儿不成立:

1329 ms
SmolVLA 单次推理(实测)
13%
三路视觉合计占比
177.7 ms
很可能为负
替换的净收益
省下的一部分,再减去检测器本身开销
做法代价风险
加法(选它)192 个视觉 token 之外再加 N 个推理略慢低,预训练视觉通路完全不动
替换用 N 个物体 token 取代 192 个推理变快高,丢掉预训练的视觉对齐

逻辑很简单:替换的唯一好处是速度,而速度收益已经被实测否掉; 那就只剩下「丢掉预训练视觉对齐」这个确定的风险,去换一个可能为负的收益。加法保留 B0 真正那条理由(泛化),不去赌那个不显著的数字。

7. 还没做的三件事,以及判据

T1 · 量检测器在 Jetson 上的单帧耗时 ★ 决定性

算力预算:每 1667 ms 的动作,扣掉 SmolVLA 自己的 1329 ms,只剩 337 ms。

实测单帧耗时结论
< 150 ms可行,每帧都跑
150 – 337 ms勉强,要考虑降频(不必每帧都跑)
> 337 msB0 直接否掉,除非改成每 N 帧跑一次
陷阱:必须用 train310 环境。lerobot 那个 torch 是 CPU-only(实测 torch.cuda.is_available() 为 False),用错环境会量出一个假的「跑不动」然后错误地否掉 B0。

候选优先级:先量 OWLv2——不是因为它更好,而是因为我们已经在自己数据上验过它 (24/36 检出,阈值 0.30 时非杯误检为 0)。它出的是框不是掩码,但框也完全够当物体先验用。 FT-Dinosaur 是 Oat-VLA 的选择,更重,作为第二顺位。

T2 · 写出并跑通接口改动

先在不加载真实物体 token(喂零向量)的情况下跑通训练一步,确认形状和 mask 都对。 形状对了再接检测器——两个错混在一起最难查。

T3 · 算净收益并决定

把 T1 的耗时、T2 的实现代价、训练成本放一起,给出「做 / 不做」。 允许的结论包括「不做」——如果 T1 就否掉了,把结论写下来,别硬上。

8. 动手之前的前置门:两个还没排除的输入框

B0 是一次要花掉训练时间和现场时间的投入。在这两件验证完之前不要开始。 2026-09-04 试跑 0/3,但在归因到「模型能力不足」之前,有两个填错的输入框还没排除。
当时用的应该是为什么要紧
任务指令串...with the left arm....with the right arm. 现役模型训练集 tasks.parquet 里只有右臂那一句。SmolVLA 是语言条件模型,喂 left = 它没见过的任务
speed0.31.0 把安全层步长上限乘了 0.3,实测削掉 shoulder_lift 的 11–20%——正是「放下去够杯子」那个关节

为什么要做那两张对照表

它们不是例行统计,是两个证伪实验,查的是两个不同的东西,不能互相替代:

要出的表查什么证伪谁判据
raw vs safe 削减安全层砍掉了策略想做的多少动作执行侧:模型想对了但被节流speed 1.0 后削减从 11–20% 掉到接近 0
夹爪开合次数策略输出本身连不连贯认知侧:模型压根没想对从 1、1、2、3 回到恒为 1

削减是拿 raw(安全层之前)和 safe(放行后)比,差值完全归安全层。 开合次数只看 raw,那是策略的原始输出、安全层碰不到,所以它是纯粹的「模型在想什么」的读数。

试跑(09-04 四次)训练示范(同一只臂 40 集)
一次里开合几次1、1、2、3中位 1,最大 1
只闭合 1 次的比例50%100%

示范里 40 集全是「张开 → 闭合一次 → 提起」,没有一集有第二次开合。 试跑里一半出现多次开合——这是反复试探的形状,和「喂了没学过的指令串」高度吻合。

四种结果对应四个不同结论:

重跑 10 次的结果该得出的结论
削减降了 + 开合回到 1 + 成功率上去两个输入框就是全部原因,B0 的紧迫性下降(但泛化那条理由仍在)
开合回到 1,但还是抓不到指令串确实错了,模型能力也确实不够 → B0 / 补数据有了明确动机
开合还是 2、3 次指令串不是原因,得另找(视角?训练不足?)
全都正常,成功率还是 0这才轮到「模型不行」——而且是在最有利条件下都不行,比现在强得多的证据
这两张表的价值不在数字本身,在于它们能把「模型不行」这个昂贵的结论挡在门外,直到它是唯一剩下的解释。
指令串这一项现在已有硬拦截:tctlRun() 在指令不一致时直接阻止实跑启动。空跑和重放不拦。

9. 会让 B0 失败的三件事

① 检测器锁错目标。物体 token 指向错的东西,比没有还糟——它给模型一个自信的错误先验。 我们项目里数字好看但锁错目标已经发生三次(VR 手柄、桌面圆孔被当成杯子)。 所以纪律是:每个感知方案都要过人工看图核对,不能只看检出率。
② 训练数据里可能只有一只杯子。如果 202 集全程只用了同一只杯子, 物体 token 和方格 token 在训练时高度相关,模型完全可能干脆忽略新加的 token——训了等于没训。 这一条 09-04 没查,是 T2 之前该补的。反过来说,如果真的只有一只,B0 的收益空间更大, 但评估时必须准备至少三种外观差异明显的杯子(磨砂、透明、彩色)。
③ 把「换位置仍然不行」误读成 B0 失败。见第 4 节: 右臂横向覆盖只有 35°,那是数据问题,B0 从设计上就不解决它。 评估 B0 时杯子位置必须固定,只换外观,否则测的是两件事混在一起。

10. 每条结论的证据等级

结论等级怎么来的
embed_prefix 是 list + concat,约 15 行可插入读过源码modeling_smolvla.py:552
我们的视觉 token 是 192 不是 256按配置推算512×512 / patch 16 / scale_factor=4,三路
推理 1329 ms,视觉占 13%实测本机计时
视觉编码器本来就冻结读过配置部署中的 config.json
右臂横向覆盖 35.1°,std 5.15实测合并集 202 集逐集提取,方法见下
试跑开合 1/1/2/3 vs 示范恒为 1实测四条轨迹 + 40 集示范,同一个函数算
安全层削掉 11–20%实测traces/*.npz 的 raw vs safe
Oat-VLA 成功率不显著(p=0.106)我们自己重算按它自己给的 29/49 vs 20/49 做 Fisher
OWLv2 24/36 检出、阈值 0.30 非杯误检 0实测我们自己的数据
检测器在 Jetson 上的单帧耗时未测T1,这是唯一的决定项
202 集里用了几种杯子未查见第 9 节 ②

11. 复现方法

数据覆盖:读合并集 data/**/*.parquet,按 episode_index 分组, 找每条臂夹爪值第一次降到 (max+min)/2 以下的帧,取当时的五个臂关节。 合拢时夹爪就在杯子上,所以这个构型代表杯位——不需要手眼标定。

开合次数:取夹爪序列,以 min+0.25*range 和 min+0.75*range 为双阈值做迟滞计数, 统计 open→closed 的转换次数。示范必须用同一个函数算,才可比。

★ 数据分界:2026-08-26 换过夹爪硬件 ★
换代前夹爪读数能到 −13.6 和 65.0,换代后区间是 0.3~59.2,再没出现过。 同一个动作在两代硬件上是不同的数字,任何跨这条线的对比或混训都是错的。 已核实:两个合并集的 merge_sources.json 第一个来源都是 0826, xlerobot-cup-grasp-20260820-0230 没有进去,所以本页所有基于合并集的数字都成立。 但这是碰巧对的,不是有意为之——以后加数据、做示范对照、算统计量之前,都要先确认数据集是否跨 0826。

12. 一句话

B0 是现在唯一不需要标定、不需要重采数据的策略改进方向,接口已经确认可行; 但它唯一站得住的理由是「换杯子」的泛化,不是速度,也不是「换位置」。 动手之前先花五分钟排除那两个填错的输入框;动手之后要记住, 数据覆盖只有 35° 的横向范围,架构变不出数据里没有的东西。