VLA-Touch 方法原理详解

1. 方法要解决什么问题
传统 Vision-Language-Action(VLA) 策略主要依赖视觉、语言指令和机器人本体状态来生成动作。它们适合处理许多开放任务,但在接触密集的操作里会遇到两个问题:
- 视觉看不到物体的内部或接触属性,例如芒果软硬、海绵粗糙度、杯子是否装水。
- 基础 VLA 生成的动作块可能在宏观方向上正确,但接触阶段需要更精细的实时修正,例如抓取力度、末端位姿和贴近物体时的轨迹微调。
VLA-Touch 的核心思想是:不直接重新训练一个触觉版大 VLA,而是在基础 VLA 外面加两层触觉反馈。
第一层是高层规划反馈:把触觉观测转成语言或语义属性,反馈给视觉语言规划器,让它决定下一步该触摸、比较、抓取还是放置。
第二层是低层执行反馈:基础 VLA 先产生一个未来动作块,然后触觉条件控制器把这个动作块修正成更接近专家执行轨迹的动作块。
因此,VLA-Touch 是一个双层触觉增强框架:
text
高层:视觉 + 任务目标 + 触觉语义反馈 -> 下一条原子操作指令
低层:视觉 + 机器人状态 + 触觉力/位移 + VLA动作块 -> refined动作块
2. 总体架构
一个完整闭环由四类模型或算法组成:
-
基础 VLA 策略
- 输入:当前/历史视觉图像、语言指令、机器人状态。
- 输出:未来一段时间的动作块。
- 在本项目里基础策略沿用 RDT 风格的 diffusion transformer,动作表示为末端位姿、6D 旋转和夹爪等 10 维量。
-
触觉语义模型或触觉反馈接口
- 输入:GelSight 触觉图像序列、属性值或低维力向量。
- 输出:硬度、粗糙度、重量/力大小等语义反馈。
- 对规划器来说,这些反馈最终以自然语言或结构化文本进入上下文。
-
VLM 任务规划器
- 输入:初始场景图像、任务目标、历史动作和触觉反馈。
- 输出:下一条简短、可执行的原子动作指令。
- 每次只规划一步,执行后等待反馈,再继续规划。
-
触觉条件动作修正器
- 输入:基础 VLA 的动作块、当前图像、当前机器人状态、触觉低维信号。
- 输出:修正后的动作块。
- 它不替代基础 VLA,而是学习从"VLA 动作分布"桥接到"专家动作分布"。
整体数据流如下:
text
任务目标 g + 场景图像 s_t
|
v
VLM planner 生成原子指令 I_k
|
v
基础 VLA: pi(a_t | s_t, I_k) 生成动作块 a_t
|
v
触觉条件插值控制器: pi_I(a_hat_t | s_t, a_t, m_t) 修正动作块
|
v
机器人执行 a_hat_t 的若干步
|
v
GelSight 触觉反馈 o^m_t
|
+--> 高层:转成语言/属性 L^m_t,反馈给 VLM planner
|
+--> 低层:转成 force/displacement m_t,条件化动作修正器
3. 高层触觉反馈:用触觉补足视觉无法判断的信息
高层模块解决的是"该做什么"的问题。
许多任务不能只靠视觉一步完成。例如:
- 在两块海绵中选择更光滑的一块,需要触摸后判断粗糙度。
- 在两个芒果中选择更熟的一颗,需要触摸后判断软硬。
- 判断杯子是否装水,可以通过抓取或移动时的力反馈推断重量。
VLA-Touch 把这些触觉信息放进规划闭环。规划器不是一次性输出完整长程计划,而是像交互式机器人一样,每次输出一个原子动作:
text
触摸左侧海绵以判断粗糙度
执行后,系统把触觉反馈返回给规划器:
text
左侧海绵触觉反馈:粗糙度较低,表面较光滑
然后规划器继续输出下一步:
text
触摸右侧海绵以判断粗糙度
直到信息足够,再生成最终操作:
text
抓取更光滑的左侧海绵
3.1 触觉语义反馈的形式
项目中支持几种反馈形式,它们本质上都服务于同一个接口:让规划器获得视觉之外的物理属性。
-
触觉图像反馈
- 取触摸前和触摸后的 GelSight 图像。
- 让多模态模型分析两张图像差异。
- 输出类似"更硬""更软""更粗糙""更光滑"的描述。
-
属性回归反馈
- 触觉视频帧进入 ViFiCLIP/CLIP 风格编码器。
- 时间维特征平均池化,经过 adapter。
- 属性回归头输出硬度和粗糙度两个连续值。
- 再用阈值或相对比较转成任务可用语义。
-
力向量反馈
- GelSight marker 位移被转换成低维力向量。
- 杯子任务中,较大的力幅值可作为"更重/更可能装水"的证据。
- 反馈文本中可包含参考阈值,例如空杯和满杯的大致力幅值范围。
3.2 高层规划伪代码
python
def tactile_planning_loop(scene_image, task_goal):
# messages 保存完整交互上下文:初始任务、历史动作、历史触觉反馈。
# VLM planner 每次都基于这个上下文决定下一步,而不是一次性写完整计划。
messages = [
system_prompt(
"You are a robot planner. Output one primitive action at a time. "
"Use tactile feedback to retrieve unknown physical properties."
),
user_prompt(task_goal, image=scene_image),
]
while not task_done:
# 规划器只输出一个原子动作,例如"触摸左侧海绵"。
# 这样可以先主动获取未知物理属性,再决定最终操作。
primitive_action = vlm_planner(messages)
# 该动作可以由机器人真实执行,也可以在离线实验中由用户输入反馈模拟。
execution_result = execute_or_request_action(primitive_action)
if execution_result.has_tactile_images:
# 触觉图像用于判断粗糙度、软硬等视觉不可见属性。
# before/after 的差异通常比单帧更能体现接触形变。
tactile_feedback = tactile_model.describe(
before_image=execution_result.tactile_before,
after_image=execution_result.tactile_after,
query=property_query_for_task(task_goal),
)
elif execution_result.has_force_vector:
# 对杯子重量等任务,低维力向量比触觉图像更直接。
# reference 给规划器一个任务相关阈值或经验范围。
tactile_feedback = format_force_feedback(
magnitude=execution_result.force_magnitude,
direction=execution_result.force_xy_direction,
reference=task_specific_force_reference,
)
elif execution_result.has_property_values:
# 如果触觉编码器已经输出硬度/粗糙度数值,直接转成文本反馈。
tactile_feedback = format_property_feedback(
hardness=execution_result.hardness,
roughness=execution_result.roughness,
)
else:
# 兜底情况:使用人工文本反馈或执行状态反馈。
tactile_feedback = execution_result.text_feedback
# 把本轮动作和触觉反馈写回上下文,下一轮规划会使用这些信息。
messages.append(assistant_message(primitive_action))
messages.append(user_message(tactile_feedback))
# 最终可以返回完成动作,也可以保存整段交互日志用于评估。
return final_action_or_session_log
这里的关键不是具体使用哪个 VLM API,而是闭环结构:
text
规划一步 -> 执行/触摸 -> 得到触觉反馈 -> 更新上下文 -> 再规划一步
4. 低层触觉反馈:把 VLA 动作修正成专家动作
低层模块解决的是"怎么做得更准"的问题。
基础 VLA 已经能根据视觉和语言输出一个动作块:
text
a_vla = [a_t, a_{t+1}, ..., a_{t+H-1}]
其中每个动作大致是 10 维:
text
[eef_pos_x, eef_pos_y, eef_pos_z, eef_rotation_6d, gripper]
但是在接触场景下,基础 VLA 的动作可能缺少触觉响应。VLA-Touch 不让低层控制器从零生成动作,而是让它学习一个条件变换:
text
VLA 动作块 -> 专家动作块
这个变换由 Stochastic Interpolants / bridge diffusion 实现。直观理解是:把 VLA 动作当作起点,把专家动作当作终点,在两者之间训练一条可采样的随机桥。推理时,从 VLA 动作起点出发,沿着学到的桥走若干步,得到更接近专家分布的 refined 动作。
4.1 低层控制器输入输出
低层控制器输入:
text
current_state: 当前机器人末端状态和夹爪状态
front_image: 当前正面相机图像
right_image: 当前右腕/侧视相机图像
tactile_force: GelSight 低维力向量,通常是 [magnitude, dir_x, dir_y]
vla_action_chunk: 基础 VLA 输出的 H 步动作块
低层控制器输出:
text
refined_action_chunk: H 步修正动作块
条件编码方式:
- 两路 RGB 图像分别输入 DINOv2,得到视觉特征。
- 当前机器人状态和触觉力向量拼接。
- 将
视觉特征1 + 视觉特征2 + 状态 + 触觉拼接后输入 MLP。 - MLP 输出一个条件向量
obs_cond。 obs_cond条件化 1D U-Net 插值网络,网络在动作序列维度上工作。
text
obs_cond = MLP([DINO(front), DINO(right), state, tactile_force])
refined_actions = StochasticInterpolants.sample(x_prior=vla_actions, cond=obs_cond)
5. GelSight 低维触觉力如何得到
低层控制器不直接吃完整 GelSight 图像,而是使用 marker tracking 得到的低维触觉信号。
处理过程:
- 用无接触或初始触觉图像作为校准帧。
- 在校准帧中检测 GelSight 表面的标记点。
- 每一帧触觉图像都经过灰度化、模糊、自适应阈值和形态学处理。
- 通过轮廓检测找到当前标记点中心。
- 使用最近邻把当前标记点和校准帧标记点匹配。
- 计算每个 marker 的位移。
- 对所有 marker 位移求平均,得到整体剪切方向。
- 平均位移的 L2 范数作为力幅值,单位向量作为方向。
伪代码:
python
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]]
这个设计的优点是简单、实时、可解释。它牺牲了触觉图像的丰富细节,但给低层控制器提供了稳定的接触强度和方向信号。
6. 控制器训练数据如何构造
低层控制器需要成对数据:
text
(当前观测, VLA动作块, 专家动作块, 触觉信号)
构造方式:
- 先收集专家演示轨迹,包括相机图像、末端状态、夹爪状态、GelSight 数据和语言指令。
- 用已训练或微调好的基础 VLA 离线跑每个时间步。
- 对每个时间步保存基础 VLA 预测出的动作块
vla_action。 - 专家动作块直接来自演示数据中当前时间之后的真实末端轨迹。
- GelSight 图像离线转换成 marker displacement 和 force。
- 训练样本按滑动窗口切片。
一个训练样本可以表示为:
text
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]。
数据构造伪代码:
python
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
7. Stochastic Interpolants 动作修正原理
这是低层控制器最核心的算法。它听起来抽象,但在这个项目里可以先按一句话理解:
text
训练时,模型反复看"VLA 粗动作"和"专家精动作"的配对,
学习怎样把前者一步步修成后者。
也就是说,它不是从零学"看到图像后该怎么动",而是学"基础 VLA 已经给出一段动作后,我该怎么改这段动作"。
7.1 它为什么叫一座"桥"
训练数据里,每个时间点都有两段动作:
text
VLA 动作块: 基础 VLA 预测出来的未来动作,记作 x0
专家动作块: 人类/专家演示中的真实未来动作,记作 x1
这两段动作都表示同一个任务、同一个时刻附近的未来轨迹,但质量不同:
text
x0:能表达任务意图,但接触细节可能粗糙
x1:真实专家轨迹,接触细节更可靠
Stochastic Interpolants 做的事,就是在 x0 和 x1 之间搭一座桥:
text
x0 ---- 中间状态 ---- 中间状态 ---- 中间状态 ---- x1
VLA粗动作 专家精动作
训练时,模型不只看桥的起点和终点,还会随机抽桥上的一个中间位置,让模型学习"在这个位置时,应该往哪里修"。
7.2 中间状态 x_t 是什么
训练时会随机采样一个 t,范围是 0 到 1:
text
t 接近 0:中间动作更像 VLA 动作
t 接近 1:中间动作更像专家动作
t 在中间:动作介于 VLA 和专家之间
用最直观的写法:
text
x_t = (1 - t) * x0 + t * x1
这表示把 x0 和 x1 按比例混合。例如:
text
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% 专家动作
真实训练里还会加一点噪声:
text
x_t = (1 - t) * x0 + t * x1 + gamma(t) * z
这里:
text
z:随机噪声
gamma(t):控制噪声强度
为什么要加噪声?因为真实推理时 VLA 动作可能不完美,动作分布也不只是一条干净直线。加噪声后,模型学到的是"在一片可能的中间动作附近如何往专家动作修",而不是死记一条固定线段。
项目默认的噪声强度是:
text
gamma(t) = sqrt(2) * t * (1 - t)
这个函数在 t=0 和 t=1 附近接近 0,在中间较大。直观含义是:
text
靠近起点/终点时少加噪声,避免破坏明确的 VLA 或专家动作;
在桥中间多加一点噪声,让模型学会处理不确定的中间状态。
7.3 条件 c 有什么用
同样的 VLA 动作,不同接触状态下可能要修成不同结果。例如:
text
触觉力很小:可能还没真正接触到物体,需要继续靠近
触觉力很大:可能压得太重,需要减小推进或调整姿态
触觉方向偏一侧:可能发生滑动,需要修正末端方向
所以模型不能只看动作 x_t,还要看当前观测条件 c:
text
c = MLP([DINO前视图像, DINO侧视图像, 当前机器人状态, 当前触觉力])
这个 c 就是在告诉控制器:
text
当前场景是什么样?
机械臂现在在哪里?
现在接触到了吗?
接触力往哪个方向?
因此,Stochastic Interpolants 学到的是:
text
在当前观测条件 c 下,
如何把中间动作 x_t 往专家动作方向修。
7.4 三个网络头分别学什么
模型由条件 1D U-Net 实现。它输入的是整段动作块 x_t,再加上时间 t 和条件 c。输出有三个头:
text
v_net:学"主要应该往哪个方向修"
s_net:学"如何把带噪声的动作拉回合理动作分布"
b_net:学"采样过程里实际采用的漂移方向"
可以这样理解:
v_net像老师在说:"从当前动作看,专家动作大概在这个方向。"s_net像老师在说:"这个动作里有些不自然的抖动/噪声,要往合理轨迹拉回来。"b_net像老师在说:"如果你按随机微分方程一步步走,这一步实际该怎么漂移。"
三者不是三个独立控制器,而是同一个 bridge diffusion 采样过程里的三个训练目标。
7.5 三个 loss 分别在约束什么
训练损失是:
text
loss = velocity_loss + score_loss + drift_loss
更白话地说:
velocity_loss:让模型学会"专家动作相对 VLA 动作,主要差在哪里"。score_loss:让模型学会"中间动作被噪声扰乱后,怎样回到合理动作区域"。drift_loss:让模型学会"推理时每一小步采样应该怎样走才稳定"。
如果只学 velocity_loss,模型会像普通残差模型,只知道大致往哪里改。
如果加入 score_loss 和 drift_loss,模型不只是学一个差值,而是学一个可逐步采样的修正过程。这就是它和普通 residual controller 的区别。
7.6 训练伪代码
下面的伪代码可以按"老师给模型出题"的方式理解:
text
题目:这里有一个 VLA 动作 x0 和专家动作 x1。
我随机取它们之间的一个中间动作 x_t。
请模型判断:从 x_t 出发,应该怎样往专家动作方向修?
python
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 在这里的作用是:
text
训练时学习"VLA 动作块到专家动作块之间的修正桥";
推理时从 VLA 动作块出发,
沿着这座桥一步步走,
得到更像专家接触操作的 refined 动作块。
8. 推理时如何从 VLA 动作得到 refined 动作
推理时最容易混淆的一点是:系统并不知道"正确专家动作"是什么。专家动作只在训练时出现。真正上线执行时,系统手里只有三类信息:
text
1. 基础 VLA 刚预测出来的一段动作块
2. 当前相机图像和机器人状态
3. 当前触觉力/接触信息
所以推理阶段的问题是:
text
基础 VLA 已经给了一条"能做任务的大致轨迹",
触觉控制器要把这条轨迹改得更像训练集中专家真正执行过的轨迹。
可以把它理解成"轨迹修图":
text
VLA 动作块: 大概往目标物体过去,但接触细节可能不准
触觉条件: 现在碰到了没有?力大不大?往哪个方向滑?
refined 动作: 在保持原任务意图的基础上,调整末端轨迹和夹爪动作
这里的 refined 不是重新规划一个全新动作,而是对基础 VLA 的动作块做连续修正。
8.1 为什么是"一步步修",不是一次修完
训练时,模型见过很多这样的配对:
text
VLA 动作块 x0 ------> 专家动作块 x1
但模型不是直接学一个简单函数:
text
refined = f(VLA动作, 当前观测)
而是学"从 VLA 动作分布走到专家动作分布的过程"。这个过程被拆成若干小步:
text
第 0 步:完全是 VLA 动作
第 1 步:稍微往专家动作方向修一点
第 2 步:继续修
...
最后一步:得到更像专家动作的 refined 动作
这就是文中 t=0 到 t=1 的含义:
text
t 接近 0:动作还很像基础 VLA
t 接近 1:动作越来越像专家轨迹
项目里 diffusion_steps 默认常用 10,可以理解成"把一次大修正拆成 10 次小修正"。小步修正比一次性改完更稳定,也更符合 diffusion / bridge model 的采样方式。
8.2 每一步到底在改什么
每一步更新时,控制器会问三个问题。
第一个问题:当前动作应该往哪里移动?
text
v_net 输出 velocity
可以把 velocity 理解成"主要修改方向"。例如 VLA 的末端轨迹偏高,velocity 可能提示轨迹往下压一点;夹爪闭合时机不合适,velocity 可能提示调整后续动作。
第二个问题:当前动作里哪些部分像噪声或不稳定?
text
s_net 输出 score
可以把 score 理解成"去噪和稳定化方向"。它不是直接告诉机器人去哪,而是帮助采样过程避开不自然、训练集中少见的动作形状。
第三个问题:把前两个方向合起来,这一步实际怎么更新?
text
drift = velocity 和 score 组合后的最终确定性更新方向
drift 是这一步真正推动动作块变化的主方向。代码里的 gamma、epsilon、gamma_derivative 都是在控制这个更新过程的强弱和噪声比例,不需要把它们理解成新的模型模块。
更白话地说:
text
velocity:告诉动作块"往专家轨迹靠近"
score:告诉动作块"别跑到奇怪、不像真实动作的地方"
drift:把这两种建议合成当前这一步的实际修改量
noise:保留 diffusion 采样需要的少量随机性
8.3 一个动作块如何被修正
假设基础 VLA 给出 16 步动作:
text
vla_chunk = [a0, a1, ..., a15]
bridge 控制器不是只修 a0,而是把整个动作块一起修:
text
[a0, a1, ..., a15] -> [a0_hat, a1_hat, ..., a15_hat]
这样做的好处是未来动作之间仍然连贯。否则如果只修当前一步,后面的动作可能和当前接触状态不一致,执行起来会抖或不连续。
8.4 推理伪代码
下面的伪代码省略了张量形状细节,只保留核心逻辑:
python
def refine_action_chunk(current_state, front_image, right_image, tactile_force, vla_chunk):
# 第一步:把当前观测编码成条件向量。
# 这个条件向量告诉控制器:现在看到什么、机械臂在哪、接触力是什么。
visual_1 = dinov2(front_image)
visual_2 = dinov2(right_image)
obs_cond = mlp(concat(visual_1, visual_2, current_state, tactile_force))
# 第二步:把 VLA 动作块作为修正起点。
# 这里的 x 还没有被修正,基本等同于基础 VLA 的原始输出。
x = normalize(vla_chunk, stats="vla")
# diffusion_steps 表示修正多少小步;例如 10 就是连续修 10 次。
dt = 1.0 / diffusion_steps
with ema_parameters():
for i in range(1, diffusion_steps + 1):
# t 表示当前修正进度。
# t 越小,x 越像原始 VLA 动作;t 越大,x 应该越像专家动作。
t = i / diffusion_steps
t = clip(t, t_min, 1 - t_min)
# 第三步:模型判断当前动作块应该怎么改。
# v 负责"往专家轨迹靠近"的主方向。
# s 负责"让动作保持在合理动作分布里"的去噪方向。
v = v_net(x, t, obs_cond)
s = s_net(x, t, obs_cond)
# score 需要按当前噪声强度缩放,才能和 velocity 放在同一更新公式里。
score_scaled = s / gamma(t)
# drift 是这一小步真正采用的确定性修改方向。
drift = v - gamma_derivative(t) * gamma(t) * epsilon(t) * score_scaled
# diffusion 采样保留少量随机扰动,避免只得到僵硬的确定性输出。
stochastic_noise = beta_max * normal_like(x)
# 第四步:把动作块往 refined 方向推进一小步。
# 更新后的 x 仍是整个动作块,而不是单个动作。
x = x + (drift + epsilon(t) * score_scaled) * dt
x = x + dt * sqrt(2 * epsilon(t)) * stochastic_noise
# 第五步:x 已经从 VLA 分布附近移动到专家动作分布附近。
# 所以反归一化时使用 expert 的统计量,而不是 VLA 的统计量。
refined_chunk = denormalize(x, stats="expert")
return refined_chunk
8.5 最后为什么用 expert 统计量反归一化
这一点也容易误解。
输入动作来自 VLA,所以一开始要用 VLA 动作的统计范围归一化:
text
x = normalize(vla_chunk, stats="vla")
但经过 bridge diffusion 修正后,目标已经不是"仍然像 VLA 动作",而是"像专家动作"。因此最后要用专家动作的统计范围还原:
text
refined_chunk = denormalize(x, stats="expert")
直观类比:
text
输入语言是 VLA 的坐标系;
模型把它翻译到专家动作坐标系;
输出时当然要按专家动作坐标系读数。
8.6 用一句话再说一遍
第 8 节可以压缩成一句话:
text
推理时,控制器把基础 VLA 给出的整段粗动作当作起点,
结合当前视觉、机器人状态和触觉力,
用训练好的 bridge diffusion 过程连续修正若干小步,
最后得到更像专家接触操作的 refined 动作块。
9. 在线机器人执行闭环
在线执行时,高层和低层可以同时发挥作用:
- 高层 VLM 根据任务目标选一条当前原子指令。
- 基础 VLA 根据这条指令输出动作块。
- 机器人每隔若干步重新推理一个动作块。
- 若启用 bridge 控制器:
- 取当前两路图像、机器人状态和 GelSight 力向量。
- 修正整个动作块。
- 执行修正动作块的前若干步。
- 执行产生新的触觉反馈。
- 高层规划器可根据触觉反馈更新下一条指令。
完整伪代码:
python
def online_vla_touch_control(task_goal):
# 高层先根据场景和任务目标给出第一条原子指令。
instruction = planner.initialize(scene_image, task_goal)
# 基础 VLA 使用短历史窗口;这里保留最近两帧观测。
observation_window = deque(maxlen=2)
while not task_complete:
# 从机器人系统读取最新图像、末端状态、夹爪状态和触觉信号。
obs = ros.read_latest_observation()
observation_window.append(obs)
if need_new_high_level_instruction():
# 把最新触觉观测转成规划器能理解的语言/属性反馈。
tactile_language = summarize_tactile_feedback(obs.tactile)
# 新指令会显式利用触觉反馈,例如"左侧更硬,所以改触摸右侧"。
instruction = planner.next_action(
scene_image=obs.front_image,
task_goal=task_goal,
tactile_feedback=tactile_language,
)
if need_new_vla_chunk():
# 基础 VLA 根据当前指令和观测生成未来动作块。
vla_chunk = base_vla.predict_action_chunk(
images=observation_window.images,
proprio=obs.robot_state,
instruction=instruction,
)
if use_touch_controller:
# bridge 控制器不重新规划目标,只修正基础 VLA 已给出的动作块。
action_chunk = bridge_controller.refine(
state=obs.robot_state,
front_image=obs.front_image,
right_image=obs.right_image,
tactile_force=obs.gelsight_force,
vla_actions=vla_chunk,
)
else:
# 关闭触觉控制器时,直接执行基础 VLA 动作,作为消融基线。
action_chunk = vla_chunk
# 动作块通常只执行前若干步,然后重新感知、重新预测,形成闭环控制。
action = select_next_action(action_chunk)
ros.publish_robot_action(action)
10. 为什么不直接微调整个 VLA
VLA-Touch 的设计重点是"外挂式增强",而不是把触觉塞进基础 VLA 重新训练。这样做有几个原因:
- 基础 VLA 通常很大,重新训练或多模态改造成本高。
- 高层触觉反馈更适合以语言形式进入规划器,因为它本质上是"任务相关信息补全"。
- 低层接触修正需要实时、局部、连续的控制能力,用小控制器更容易训练和部署。
- 把高层和低层分开后,可以分别调试:
- 高层错了,通常是触觉语义或规划上下文问题。
- 低层错了,通常是动作修正、触觉力估计或数据分布问题。
11. 方法里的关键设计细节
11.1 动作修正器学习的是分布桥,不是普通残差
普通残差控制器可能学习:
text
delta = expert_action - vla_action
refined = vla_action + delta
VLA-Touch 的 bridge 控制器学习的是从 VLA 动作分布到专家动作分布的随机过程。它能表达多模态和不确定性,并且天然支持动作块级别的序列修正。
11.2 VLA 动作和专家动作使用不同归一化统计
训练中 x0 来自 VLA 动作分布,x1 来自专家动作分布。二者可能范围不同,因此分别统计 min/max。推理时先按 VLA 统计量归一化输入,采样后按专家统计量反归一化输出。
11.3 条件向量只用当前观测,但动作输出是未来序列
控制器的 obs_cond 来自当前状态、当前图像和当前触觉力;输出却是未来 H 步动作块。这意味着模型要学会用当前接触信息修正一段未来轨迹,而不是只修正下一步。
11.4 触觉在两层中的表达不同
同一个 GelSight 传感器在两层里扮演不同角色:
- 高层:触觉被解释成硬度、粗糙度、重量等语义,用于决策。
- 低层:触觉被压缩成力幅值和方向,用于连续控制。
这正是"双层触觉反馈"的含义。
11.5 高层规划是一步一反馈,而不是一次性计划
规划器提示词要求每次只输出一个物理动作,并等待反馈。这避免了在未知物理属性上提前做错误假设。
12. 典型任务流程示例
12.1 选择更光滑的海绵
text
输入任务:触摸并识别两个海绵的粗糙度,拿起更光滑的那个。
步骤 1:VLM 看到两个海绵,但不知道粗糙度。
步骤 2:规划器输出"触摸左侧海绵"。
步骤 3:机器人执行,GelSight 产生触觉图像。
步骤 4:触觉模型判断左侧较光滑/粗糙度数值较低。
步骤 5:规划器输出"触摸右侧海绵"。
步骤 6:触觉模型判断右侧粗糙度。
步骤 7:规划器比较两者,输出"抓取更光滑的海绵"。
步骤 8:基础 VLA 生成抓取动作块。
步骤 9:低层触觉控制器根据接触力修正轨迹和夹爪动作。
12.2 判断杯子是否装水
text
输入任务:判断杯子是否为空;如果不为空,把它放到盘子上。
步骤 1:视觉无法可靠判断杯子是否装水。
步骤 2:规划器输出抓取或轻微移动杯子的动作。
步骤 3:GelSight force 估计当前剪切力幅值。
步骤 4:系统把力幅值和参考范围反馈给规划器。
步骤 5:若力较大,规划器判断更可能是满杯,并输出放到盘子上的动作。
步骤 6:执行阶段,低层控制器用力反馈修正移动轨迹,减少因重量变化导致的偏差。
13. 方法边界和局限
- 高层触觉语义依赖触觉模型或 VLM 对触觉图像的解释质量。如果属性判断错了,规划器可能做出错误决策。
- 低层控制器依赖训练集中同时存在 VLA 动作和专家动作。若基础 VLA 或任务分布变化很大,需要重新生成 VLA 动作数据并训练控制器。
- Marker tracking 得到的是低维剪切力近似,不包含完整触觉图像中的所有空间细节。
- 低层控制器当前主要修正接触执行,不负责重新决定任务目标;任务级决策仍由高层规划器负责。
- 高层和低层反馈之间没有端到端联合优化,它们通过接口组合。优点是模块化,缺点是误差不会自动全局反传。
14. 一句话总结
VLA-Touch 的方法可以概括为:
text
让基础 VLA 继续负责通用视觉语言动作能力;
让触觉语言反馈负责补全"看不见的物理属性";
让触觉条件 bridge diffusion 控制器负责把 VLA 动作块修正得更接近专家接触操作。
它的关键不是简单把触觉作为又一个输入拼到大模型里,而是在任务规划和动作执行两个层级分别使用最合适的触觉表示:高层用语义,低层用连续接触信号。