低层控制器不直接吃完整 GelSight 图像,而是使用 marker tracking 得到的低维触觉信号。

处理过程:

用无接触或初始触觉图像作为校准帧。
在校准帧中检测 GelSight 表面的标记点。
每一帧触觉图像都经过灰度化、模糊、自适应阈值和形态学处理。
通过轮廓检测找到当前标记点中心。
使用最近邻把当前标记点和校准帧标记点匹配。
计算每个 marker 的位移。
对所有 marker 位移求平均,得到整体剪切方向。
平均位移的 L2 范数作为力幅值,单位向量作为方向。
伪代码:

def estimate_gelsight_force(calibration_frame, current_frame):
# calibration_frame 是无接触或初始接触很弱的参考帧。
# 当前帧中的 marker 位移都相对于这个基准来计算。
baseline_markers = detect_markers(preprocess(calibration_frame))
current_markers = detect_markers(preprocess(current_frame))

displacements = []
for marker in current_markers:
    # 用最近邻把当前 marker 匹配回参考帧中的 marker。
    # 这个近似要求相邻帧 marker 位移不要大到发生严重错配。
    nearest_baseline = nearest_neighbor(marker, baseline_markers)
    displacements.append(marker - nearest_baseline)

# 平均位移代表整体剪切方向;局部复杂形变在这里被压缩成低维量。
avg_displacement = mean(displacements)
magnitude = l2_norm(avg_displacement)

if magnitude > 0:
    # 单位方向只描述 xy 平面上的剪切方向。
    direction = avg_displacement / magnitude
else:
    # 没有明显位移时,方向设为零向量,避免除零。
    direction = [0, 0]

# 低层控制器最终使用 3 维触觉信号:[力幅值, x方向, y方向]。
return [magnitude, direction[0], direction[1]]

这个设计的优点是简单、实时、可解释。它牺牲了触觉图像的丰富细节,但给低层控制器提供了稳定的接触强度和方向信号。

  1. 控制器训练数据如何构造
    低层控制器需要成对数据:

(当前观测, VLA动作块, 专家动作块, 触觉信号)
构造方式:

先收集专家演示轨迹,包括相机图像、末端状态、夹爪状态、GelSight 数据和语言指令。
用已训练或微调好的基础 VLA 离线跑每个时间步。
对每个时间步保存基础 VLA 预测出的动作块 vla_action。
专家动作块直接来自演示数据中当前时间之后的真实末端轨迹。
GelSight 图像离线转换成 marker displacement 和 force。
训练样本按滑动窗口切片。
一个训练样本可以表示为:

states = [q_t-C+1, …, q_t, q_{t+1}, …, q_{t+H}]
vla_actions = VLA(s_t, I)[0:H]
expert_actions = [q_{t+1}, …, q_{t+H}]
forces = [m_t-C+1, …, m_t, m_{t+1}, …, m_{t+H}]
images = 当前上下文图像
其中:

C 是上下文帧数,项目默认常用 2。
H 是预测 horizon,训练脚本里可设为 16、32、64 等。
vla_actions 和 expert_actions 分别有自己的 min/max 统计量,用 padded normalization 归一化到近似 [-1, 1]。
数据构造伪代码:

def build_controller_dataset(episodes, base_vla, instruction_embedding):
# 这个数据集的目的不是训练基础 VLA,而是训练“VLA动作 -> 专家动作”的修正器。
controller_samples = []

for episode in episodes:
    # 基础 VLA 使用短历史视觉上下文,因此每条 episode 维护一个两帧窗口。
    observation_window = init_two_frame_window()

    for t in range(len(episode)):
        # 将机器人原始末端位姿和夹爪状态转换成统一 10 维动作/状态表示。
        q_t = convert_eef_pose_with_gripper(episode.state[t])

        # 图像 resize/pad 到基础 VLA 视觉编码器期望的尺寸。
        front, right = resize_for_vla(episode.images[t])

        # 触觉可以离线预处理;若已经存在 force 文件,这里直接读取。
        tactile_force = estimate_or_load_gelsight_force(episode.gelsight[t])

        # 更新观测窗口后,用同一个时间点的观测跑基础 VLA。
        observation_window.append(front, right, q_t)

        # x0:基础 VLA 在当前状态下预测的未来 H 步动作块。
        vla_chunk = base_vla.predict_action_chunk(
            images=observation_window.images,
            proprio=q_t,
            instruction=instruction_embedding,
        )

        # x1:专家演示中同一时间点之后的真实未来 H 步轨迹。
        expert_chunk = episode.future_states(t, horizon=H)

        # 保存成控制器训练样本;训练时会从 hdf5 中切片读取。
        save_to_hdf5(
            timestep=t,
            vla_action=vla_chunk,
            expert_action=expert_chunk,
            images=[front, right],
            state=q_t,
            tactile_force=tactile_force,
        )

        # 伪代码中保留这个列表,实际实现主要写入 hdf5。
        controller_samples.append((q_t, front, right, tactile_force, vla_chunk, expert_chunk))

return controller_samples
  1. Stochastic Interpolants 动作修正原理
    这是低层控制器最核心的算法。它听起来抽象,但在这个项目里可以先按一句话理解:

训练时,模型反复看“VLA 粗动作”和“专家精动作”的配对,
学习怎样把前者一步步修成后者。
也就是说,它不是从零学“看到图像后该怎么动”,而是学“基础 VLA 已经给出一段动作后,我该怎么改这段动作”。

7.1 它为什么叫一座“桥”
训练数据里,每个时间点都有两段动作:

VLA 动作块: 基础 VLA 预测出来的未来动作,记作 x0
专家动作块: 人类/专家演示中的真实未来动作,记作 x1
这两段动作都表示同一个任务、同一个时刻附近的未来轨迹,但质量不同:

x0:能表达任务意图,但接触细节可能粗糙
x1:真实专家轨迹,接触细节更可靠
Stochastic Interpolants 做的事,就是在 x0 和 x1 之间搭一座桥:

x0 ---- 中间状态 ---- 中间状态 ---- 中间状态 ---- x1
VLA粗动作 专家精动作
训练时,模型不只看桥的起点和终点,还会随机抽桥上的一个中间位置,让模型学习“在这个位置时,应该往哪里修”。

7.2 中间状态 x_t 是什么
训练时会随机采样一个 t,范围是 0 到 1:

t 接近 0:中间动作更像 VLA 动作
t 接近 1:中间动作更像专家动作
t 在中间:动作介于 VLA 和专家之间
用最直观的写法:

x_t = (1 - t) * x0 + t * x1
这表示把 x0 和 x1 按比例混合。例如:

t = 0.0:x_t = 100% VLA动作
t = 0.3:x_t = 70% VLA动作 + 30% 专家动作
t = 0.8:x_t = 20% VLA动作 + 80% 专家动作
t = 1.0:x_t = 100% 专家动作
真实训练里还会加一点噪声:

x_t = (1 - t) * x0 + t * x1 + gamma(t) * z
这里:

z:随机噪声
gamma(t):控制噪声强度
为什么要加噪声?因为真实推理时 VLA 动作可能不完美,动作分布也不只是一条干净直线。加噪声后,模型学到的是“在一片可能的中间动作附近如何往专家动作修”,而不是死记一条固定线段。

项目默认的噪声强度是:

gamma(t) = sqrt(2) * t * (1 - t)
这个函数在 t=0 和 t=1 附近接近 0,在中间较大。直观含义是:

靠近起点/终点时少加噪声,避免破坏明确的 VLA 或专家动作;
在桥中间多加一点噪声,让模型学会处理不确定的中间状态。
7.3 条件 c 有什么用
同样的 VLA 动作,不同接触状态下可能要修成不同结果。例如:

触觉力很小:可能还没真正接触到物体,需要继续靠近
触觉力很大:可能压得太重,需要减小推进或调整姿态
触觉方向偏一侧:可能发生滑动,需要修正末端方向
所以模型不能只看动作 x_t,还要看当前观测条件 c:

c = MLP([DINO前视图像, DINO侧视图像, 当前机器人状态, 当前触觉力])
这个 c 就是在告诉控制器:

当前场景是什么样?
机械臂现在在哪里?
现在接触到了吗?
接触力往哪个方向?
因此,Stochastic Interpolants 学到的是:

在当前观测条件 c 下,
如何把中间动作 x_t 往专家动作方向修。
7.4 三个网络头分别学什么
模型由条件 1D U-Net 实现。它输入的是整段动作块 x_t,再加上时间 t 和条件 c。输出有三个头:

v_net:学“主要应该往哪个方向修”
s_net:学“如何把带噪声的动作拉回合理动作分布”
b_net:学“采样过程里实际采用的漂移方向”
可以这样理解:

v_net 像老师在说:“从当前动作看,专家动作大概在这个方向。”
s_net 像老师在说:“这个动作里有些不自然的抖动/噪声,要往合理轨迹拉回来。”
b_net 像老师在说:“如果你按随机微分方程一步步走,这一步实际该怎么漂移。”
三者不是三个独立控制器,而是同一个 bridge diffusion 采样过程里的三个训练目标。

7.5 三个 loss 分别在约束什么
训练损失是:

loss = velocity_loss + score_loss + drift_loss
更白话地说:

velocity_loss:让模型学会“专家动作相对 VLA 动作,主要差在哪里”。
score_loss:让模型学会“中间动作被噪声扰乱后,怎样回到合理动作区域”。
drift_loss:让模型学会“推理时每一小步采样应该怎样走才稳定”。
如果只学 velocity_loss,模型会像普通残差模型,只知道大致往哪里改。

如果加入 score_loss 和 drift_loss,模型不只是学一个差值,而是学一个可逐步采样的修正过程。这就是它和普通 residual controller 的区别。

7.6 训练伪代码
下面的伪代码可以按“老师给模型出题”的方式理解:

题目:这里有一个 VLA 动作 x0 和专家动作 x1。
我随机取它们之间的一个中间动作 x_t。
请模型判断:从 x_t 出发,应该怎样往专家动作方向修?
def train_bridge_controller(batch):
# 1. 取出当前观测。
# state 是机器人当前位置/姿态/夹爪状态。
# tactile 是当前 GelSight 估计出的低维触觉力。
state = batch.states[:, current_context_index]
tactile = batch.forces[:, current_context_index]
future_tactile = batch.forces[:, context_frames:]

# 2. 准备桥的起点和终点。
# x0 是基础 VLA 预测的动作块,代表“粗动作”。
# x1 是专家演示中的动作块,代表“希望修成的精动作”。
x0 = normalize(batch.vla_actions, stats="vla")
x1 = normalize(batch.expert_actions, stats="expert")

# 3. 把当前视觉、状态、触觉编码成条件 c。
# 条件 c 决定这次修正应该考虑什么接触状态。
visual_1 = dinov2(batch.current_front_image)
visual_2 = dinov2(batch.current_right_image)
obs_cond = mlp(concat(visual_1, visual_2, state, tactile))

# 4. 随机选择桥上的一个位置 t。
# 这相当于随机问模型:“如果动作已经修到 30% 或 70%,下一步该怎么走?”
t = uniform(0, 1)

# 5. 构造训练题目 x_t。
# x_t 是 VLA 动作和专家动作的中间版本,再加少量噪声。
z = normal_like(x0)
gamma = sqrt(2) * t * (1 - t)
x_t = (1 - t) * x0 + t * x1 + gamma * z

# 6. 让模型看 x_t、t 和条件 c,预测三种修正信息。
# v_pred:主要修正方向。
# s_pred:去噪/回到合理动作分布的方向。
# b_pred:SDE 采样时的一步漂移方向。
v_pred = v_net(x_t, t, obs_cond)
s_pred = s_net(x_t, t, obs_cond)
b_pred = b_net(x_t, t, obs_cond)

# 7. 用三个损失告诉模型哪里预测错了。
# velocity loss 对齐“从 x0 到 x1 的主方向”。
# score loss 对齐“噪声应该如何被消掉”。
# drift loss 对齐“采样过程中的实际漂移方向”。
v_loss = velocity_matching_loss(v_pred, x1 - x0)
s_loss = score_matching_loss(s_pred, z)
b_loss = bridge_drift_loss(b_pred, x0, x1, z, t)

# 8. 联合优化三类能力。
# 训练完成后,推理时就可以从 x0 出发,一小步一小步走向专家动作分布。
loss = v_loss + s_loss + b_loss
loss.backward()
optimizer.step()

# 9. 保存参数的滑动平均版本,推理采样时更平滑稳定。
ema.update()

7.7 用一句话再说一遍
Stochastic Interpolants 在这里的作用是:

训练时学习“VLA 动作块到专家动作块之间的修正桥”;
推理时从 VLA 动作块出发,
沿着这座桥一步步走,
得到更像专家接触操作的 refined 动作块。
8. 推理时如何从 VLA 动作得到 refined 动作
推理时最容易混淆的一点是:系统并不知道“正确专家动作”是什么。专家动作只在训练时出现。真正上线执行时,系统手里只有三类信息:

  1. 基础 VLA 刚预测出来的一段动作块
  2. 当前相机图像和机器人状态
  3. 当前触觉力/接触信息
    所以推理阶段的问题是:

基础 VLA 已经给了一条“能做任务的大致轨迹”,
触觉控制器要把这条轨迹改得更像训练集中专家真正执行过的轨迹。
可以把它理解成“轨迹修图”:

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐