跳到主要内容
反事实不是想出来的,是跑出来的

反事实不是想出来的,是跑出来的

图南
图南

· 阅读约 9 分钟

你有没有碰到过这种事:一个由好几个现成模块拼起来的系统挂了,你翻日志翻了半天,最后发现挂掉的那个模块本身没毛病,是上游给它喂了一个它根本处理不了的状态。而当时还有别的选择,只是没人知道。

这个月 8 号 arXiv 上挂了一篇叫 RoboAware 的文章(2610.11480,cs.RO),干的就是这件事:让系统学会判断“当初要是换一条路会怎样”。这篇拆完,你会拿到一个心智模型——反事实在工程上不是个哲学词,它是一批可以花算力造出来的数据。而造它的成本,决定了这套方法能用在哪儿、不能用在哪儿。

先把问题的形状摆出来。

具身编码智能体这类系统,结构上是两层。底下是若干个冻结的端到端策略,抓取、推、双臂协同各管一段。上面是一个会写代码的智能体,把这些策略的调用拼成代码块,一块一块往下执行。冻结的意思是底下不动,不微调、不重训,上层能做的只有选,不能改。

那上层怎么学?每次选一个策略族往下走,走完,环境给一个结果:成了,或者挂了。它手上拿到的训练信号就这一条。

这里最容易被搞混的地方是,“失败了”这个信号不能直接用来学“这个策略不行”。你实际学到的是“这个策略在这个状态下不行”。同一状态下的其他选项你根本没跑过,它们会成还是会挂,日志里一个字都没有。这不是样本不够,是样本的结构本身就带着偏。所有经验都被“当时选了谁”这一个动作污染过。

我一开始想把这件事归到样本效率问题上,后来觉得不对。样本效率是说数据少,多跑跑就有了。这个是可观测性问题:有些数据在原理上就采不到,跑一万次也还是只有被选中那一支的结果。你加样本,只会让这个偏更精确。

打个比方,一个项目经理手里五个人,每次只派一个出去干活,回来只看这一个人干得怎么样,然后据此判断谁行谁不行。他缺的不是样本量,他缺的是同一件事让五个人各干一遍、比一比。人做不到这个,时间不倒流,活儿干过一次世界就变了。仿真可以。

这个类比到这儿就该收了,往下撑不住,因为“五个人各干一遍”隐含了一个前提:五个人互不干扰。而技能之间是互相改状态的,抓过一次,物体的位置就变了。所以真正要做的事不是“各干一遍”,是“每次干之前先把世界倒回去”。这条是全文的枢纽,下面单独拆。

先统一尺子,再谈谁该负责

在能造分支之前,还有个更靠前的问题:你凭什么拿 A 的结果去跟 B 的结果比。

抓取滑脱和导航超时,这两个“失败”不在同一个粒度上。把它们塞进同一张价值表里学,学出来的数是没意义的,因为单位不一样。这一步特别容易被跳过——大家默认“技能”就是个平级的列表,实际上不是。列表是平的,责任是有层级的。

论文的做法是先把技能归到五个语义阶段里去,这个 schema 叫 P^5,思路是从 REPL 那种交互式编程借来的:一个会话里每一步有明确的角色,读、求值、输出各归各位,不能混着来。归完阶段,再在这个分层结构上建一个分层马尔可夫决策过程。说人话就是:先确定这两个技能在不在同一层,只有同层之间,责任才是可定义、可比较的。

(P^5 这个借法其实挺老派的,REPL 这东西六十年代就在了,被拿来当技能分层的隐喻。打住,这条扯远了,拉回来。)

所以顺序是:先统一尺子,再产生数字。反过来做——先跑一堆结果,再想办法把它们凑成可比——就是白算。

状态锁定分支:反事实是跑出来的,不是想出来的

核心机制叫 State-Locked Counterfactual Branching,名字长,做法意外地朴素:把训练环境恢复到同一个状态,然后让每一个可用的策略族各自生成一个代码块、各自执行一遍,把每个分支的结果都记下来。

锁定 state s
├─ family = grasp    → 执行 → 失败(滑脱)
├─ family = push     → 执行 → 成功
└─ family = regrasp  → 执行 → 失败(超时)

这三行里,只有第一行是你按原来的方式能观测到的。后两行是造出来的。不是靠模型推理出来的,是靠真跑一遍跑出来的——这一点很关键,因为推理出来的反事实没有约束,模型说“要是选 push 就成功了”,你没法证伪。跑出来的有约束,环境会打它的脸。

代价也很直白:分支数乘以每个分支的执行开销,再加上每一次状态重置的开销。所以这套东西的经济性,跟“状态重置有多便宜”直接绑死。仿真里重置是一行代码的事。真机上重置一个被抓乱的桌面,可能要人过去摆半天。这不是实现细节,这是这套方法能不能成立的前提条件,后面我还会绕回来说。

插一句,20 个作者,第一作者 Bohan Zhou。这个规模大概能解释他们为什么有能力在 100 个任务上开分支跑——分支是真烧算力的,不是纸上推的。

蒸馏:学的是“哪一族”,不是“哪个动作”

分支跑完,手上是一堆散点:(状态, 策略族, 结果)。要把它变成部署时能用的东西,论文用的办法是蒙特卡洛树搜索加 Q 学习,先在这些分支上做前向展开,把回报回传,蒸馏成一个以策略族为条件的价值估计。形状大致是 Q(s, 策略族),不是 Q(s, 原子动作)。

这个条件的粒度,是整个设计里最见功力的一处。学族级,意味着协调器根本不需要知道“手腕转多少度”“夹爪开几厘米”,它只需要知道“这个状态下该调用哪一族”。粒度选对了,上层才可能完全不碰底层。你要是把粒度定到参数级,那这套东西就必须跟策略模型耦合,冻结这个前提就没了。

族级是不是最优粒度,我给不出答案。粗有粗的好处——样本效率高、跟底层解耦干净。粗也有粗的代价——同一族内部的差异被抹平了,“该抓但抓的位置不对”和“压根不该抓”在 Q 值上长得一样。这条我没在论文里看到消融,只能存疑。

部署时的顺序值得单独说一句,因为它跟很多人的直觉是反的:协调器先从可观测上下文里选出策略族,然后才轮到那个冻结的编码智能体生成下一个局部代码块。也就是先决定用谁,再决定怎么写。协调层完全不碰 token 级生成,它不写代码,只挑族。这是“不微调底层”这个设计的必然结果——底下冻住了,上面能动的只有选择权,那选择权就得单独学一套。

数字,以及我不打算把数字当重点

100 个任务,单回合评测,总体 77.0%。分项:RoboSuite 90.0%,RoboTwin 双臂 90.0%,LIBERO-Pro 上多个任务簇平均 73.8%。论文说这套优于 code-as-policy 和 VLA-harness 两类基线。

77.0% 大概率是会被转发的那个数字,但我不觉得它是这篇的重点。三个分项里掉得最狠的是 73.8%,而这恰恰是唯一一个“跨多个任务簇取平均”的项。单场景能到 90,一跨簇就掉十几个点——这说明协调器学到的族级偏好,泛化半径是有限的。不奇怪。Q(s, family) 学到的是“这类状态配这类族”,状态分布一挪,价值表就得重新趟一遍。

另外“单回合”这个设定得说一句:100 个任务,每个只跑一次,没有重复采样。这个设定下 77% 这个数字方差不会小,如果跟基线的差距只有几个点,是不太经得起推敲的。我手上没有逐任务的方差报告,这条只能挂着。

这篇的价值在“切分”,不在分数

这篇真正干的事,是把“协调”从“能力”里切出来了。

现在的路子基本是反的:组合不好使,那就把单体策略训得更大更强,指望能力上去了组合自然就顺。我不太信这个。组合失败的分布和单点失败的分布压根不是一回事——单点失败是“这个动作做不好”,组合失败很多时候是“这个动作本身做得挺好,但在这个状态下不该做”。后者你靠加强单体能力治不了,你只会得到一个更强的、在错误时机被调用的策略,而且失败得更自信。

RoboAware 的赌注是:这两件事可以分开学,而且分开学的成本比合在一起低。冻结执行器,全部训练预算压在协调这一层,参数量和算力都不流向底层。这个赌注成立与否,卡在一个很硬的条件上——你能不能便宜地把状态倒回去。

所以回到开头那个心智模型。想让系统学会“当初要是选另一条路会怎样”,你得真的把那条路走一遍,而这件事的价格,等于状态重置的价格。仿真里便宜到可以随便造,真机上贵到可能根本造不起。

这套东西真正的迁移边界不在算法,在那个重置按钮上。再往下就是真机上怎么做近似重置、或者怎么在压根不能重置的前提下估反事实——这个坑先留着,改天填。