关键数据字段流转
paceMouse 读取 → expert_a (6,) + buttons [2]
│
▼
干预检测拼接 → expert_a (7,) = concat(expert_a(6,), gripper_action(1,))
│
▼
action() 返回 → new_action (7,) + replaced: bool
│
▼
info 字典标记 → info["intervene_action"] = new_action (7,)(仅干预时存在)
│
▼
Actor 替换 actions = info.pop("intervene_action") → actions(7,) 始终=实际执行动作
│
▼
transition {obs, actions(7,), next_obs, reward, mask, done, grasp_penalty?}
│
▼
双 buffer data_store ← 所有 transition
intvn_data_store ← 仅干预 transition
│
▼
ZMQ 传输 List[dict] → TrainerServer
│
▼
Learner buffer replay_buffer(200K) ← 所有经验
demo_buffer(200K) ← 干预经验
│
▼
50/50 采样 batch(128 online + 128 demo) → concat → (256, ...)
│
▼
模型更新 critic_loss(actions[..., :-1]) + grasp_critic_loss(actions[..., -1])
│
▼
参数广播 agent.state.params → ZMQ Broadcast → Actor
│
▼
参数替换 agent.replace(state=agent.state.replace(params=params))
│
▼
Policy 接管 sample_actions(obs) → (7,) → 若无人干预则直接发送到机器人
3.5 核心设计要点
- 干预动作直接替换:
actions = info.pop("intervene_action")使 transition 中的actions字段始终是实际执行的动作,learner 直接学习人类动作,无需额外标签。 - 双通道数据:干预 transition 同时写入两个 buffer,RLPD 采样时干预数据权重保持 50%,不会被在线数据稀释。
- 异步解耦:Actor 通过 agentlace 的 ZMQ REQ-REP(数据) + ZMQ Broadcast(参数)与 Learner 完全异步通信。
- 参数更新闭环:从人类干预 → 数据上传 → 模型训练 → 参数下发 → Policy 接管,整个闭环延迟取决于
steps_per_update和网络延迟。 - 不存在意图检测:HIL-SERL 中没有任何基于模型置信度来预测何时需要人类救场的模块。干预完全依赖人类肉眼观察 + 主动操作 SpaceMouse。
0x04 SpaceMouse 干预工程细节
我们通过 SpaceMouse 来具体看看人类干预机制。
4.1 Wrapper 设计与分层
SpacemouseIntervention 是 gym.ActionWrapper 的派生类,它完美体现了 Wrapper 模式在机器人系统中的核心价值:
- 硬件隔离:FrankaEnv 不需要知道 SpaceMouse 的存在,通过增删包装器即可切换算法模式。
- 透明层:训练脚本感知不到 SpaceMouse——它只负责拿 obs 并返回动作。
- 容错:如果没有视觉分类器,就用
HumanClassifierWrapper让人在控制台输入成功与否。
具体分层:
- 底层环境(BaseEnv):只负责发指令给机械臂。
- 第一层(SpaceMouseIntervention):监听 SpaceMouse,有信号就往 info 塞
intervene_action。 - 第二层(ImageStacking):把摄像头连拍的 3 张图叠在一起。
- 最终形态:
env = ImageStacking(SpaceMouseIntervention(BaseEnv()))
好处:如果想把机器人从 Panda 换成 UR5,只需换掉最底层的 BaseEnv,上面的干预逻辑和图像处理逻辑直接套上去就能复用。
SERL Wrappers(环境装饰器层)逻辑流程图如下:

4.2 干预检测逻辑
检测逻辑极其简洁——SpaceMouse 输出向量的 L2 范数超过 0.001 即判定为人类干预。每一步都是独立决策。关键在 SpacemouseIntervention.action() 的逻辑:
策略输出 action(末端坐标系)
↓
RelativeFrame.transform_action() → 转到基坐标系
↓
SpacemouseIntervention.action(policy_action) ← 切换点
↓
每一步 env.step(policy_action) 被调用时:
① 读取 SpaceMouse 当前状态
② norm(expert_a) > 0.001 或有按键?
├─ YES → 返回 expert_a, 标记 intervened=True (丢弃策略动作,返回人类动作)
└─ NO → 返回 policy_action, 标记 intervened=False (丢弃SpaceMouse,返回策略动作)
↓
FrankaEnv.step(action) → HTTP POST 发给机器人
具体 SpacemouseIntervention 代码如下:
class SpacemouseIntervention(gym.ActionWrapper):
def action(self, action: np.ndarray) -> np.ndarray:
# 1. 实时读取 SpaceMouse 状态
expert_a, buttons = self.expert.get_action()
# 2. 判断是否干预(死区逻辑)
intervened = False
if np.linalg.norm(expert_a) > 0.001:
intervened = True
# 3. 处理夹爪(左右按键)
if self.left: # 左键关
gripper_action = np.random.uniform(-1, -0.9, size=(1,))
intervened = True
elif self.right: # 右键开
gripper_action = np.random.uniform(0.9, 1, size=(1,))
intervened = True
# 4. 如果是干预,返回专家动作;否则返回机器人动作
if intervened:
return expert_a, True # 人类动作,策略动作直接丢弃
return action, False # 策略动作
检测流程:
HID 硬件(16位有符号整数,范围 [-350, 350])
│
▼ pyspacemouse.py: to_int16(data) / 350.0 → 归一化到 [-1.0, 1.0]
│
▼ spacemouse_expert.py: 后台进程持续读取
│ 输出 6DoF: [-y, x, z, -roll, -pitch, -yaw]
│ 存入 multiprocessing.Manager.dict(共享内存)
│
▼ SpacemouseIntervention.action():
├─── np.linalg.norm(expert_a) > 0.001 → 干预!返回专家动作
└─── 按钮按下(左键夹爪/右键夹爪) → 干预!
否则 → 返回策略动作
三个层次的阈值:
| 阈值 | 值 | 位置 | 用途 |
|---|---|---|---|
| 干预检测 | 0.001 | wrappers.py | L2 范数超过此值 = 人类在操作 |
| HID 缩放 | 350.0 | pyspacemouse.py | 原始整数 / 350 → 浮点归一化 |
| EggFlip 冷却期 | 0.5 秒 | egg_flip/wrapper.py | 输入停止后 0.5 秒内仍视为干预 |
0.001 看起来极低,但实际合理:SpaceMouse 静止时输出接近 0,任何实际操作的 L2 范数通常在 0.01~1.0 量级,远超 0.001。这个阈值本质上只是过滤 HID 噪声,而非区分"轻微触碰"和"有意操作"。
4.3 控制权切换:硬替换
控制权切换发生在每个控制步的 wrapper 层,是硬替换而非平滑混合:
如果检测到干预:执行 SpaceMouse 动作
如果没有检测到干预:执行 policy 动作
每一步都是独立决策。没有"交接动作"——人类松开 SpaceMouse 的那一刻,下一步 norm(expert_a) 自然 ≤ 0.001,wrapper 就直接返回 policy 的动作:
Step 1: 人类移动 SpaceMouse → expert_a, intervened=True
Step 2: 人类移动 SpaceMouse → expert_a, intervened=True
Step 3: 人类松手 → norm=0 → policy_action, intervened=False ← 自动切回
Step 4: policy_action, intervened=False ← Policy 完全接管
这就是硬切换——没有过渡期、没有混合、没有信号通知。每一步独立判断。
EggFlip 的 0.5 秒冷却期是唯一的"软化"——不是 fade-in,而是延迟切回。0.5 秒内 SpaceMouse 输出已接近 0,机器人基本静止,等于给策略一个准备时间:
if np.linalg.norm(expert_a) > 0.001:
self.last_intervene = time.time()
if time.time() - self.last_intervene < 0.5:
return expert_a, True
硬切换能成立有五个前提:
- 策略动作与 SpaceMouse 动作处在同一动作空间——策略输出 7D [-1,1] 基坐标系,SpaceMouse 6D+1D [-1,1] 基坐标系,维度和范围对齐。
- 动作是 delta pose 而非绝对位姿跳变——
new_pose = current_pose + delta * ACTION_SCALE,切换只改变移动方向,不会跳到绝对位置。 - 动作会被 clip 到安全范围——即使切换瞬间动作差异大,物理执行也被安全裁剪。
- 低层阻抗控制器会进一步平滑物理执行。
- 坐标系一致性保证——RelativeFrame wrapper 确保存入训练 buffer 的干预动作与策略动作在同一坐标系:
策略动作: 末端坐标系 → transform_action() → 基坐标系 → 执行
人类动作: SpaceMouse 直接输出基坐标系 → 执行
存入 buffer: 基坐标系干预动作 → transform_action_inv() → 末端坐标系
硬切换不出问题的核心原因:增量控制 + 低频率(10Hz)。策略在人干预期间持续运行,切回时输出已经是基于最新观测的合理动作。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)