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 核心设计要点

  1. 干预动作直接替换actions = info.pop("intervene_action") 使 transition 中的 actions 字段始终是实际执行的动作,learner 直接学习人类动作,无需额外标签。
  2. 双通道数据:干预 transition 同时写入两个 buffer,RLPD 采样时干预数据权重保持 50%,不会被在线数据稀释。
  3. 异步解耦:Actor 通过 agentlace 的 ZMQ REQ-REP(数据) + ZMQ Broadcast(参数)与 Learner 完全异步通信。
  4. 参数更新闭环:从人类干预 → 数据上传 → 模型训练 → 参数下发 → Policy 接管,整个闭环延迟取决于 steps_per_update 和网络延迟。
  5. 不存在意图检测: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(环境装饰器层)逻辑流程图如下:

HIL-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.001wrappers.pyL2 范数超过此值 = 人类在操作
HID 缩放350.0pyspacemouse.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

硬切换能成立有五个前提:

  1. 策略动作与 SpaceMouse 动作处在同一动作空间——策略输出 7D [-1,1] 基坐标系,SpaceMouse 6D+1D [-1,1] 基坐标系,维度和范围对齐。
  2. 动作是 delta pose 而非绝对位姿跳变——new_pose = current_pose + delta * ACTION_SCALE,切换只改变移动方向,不会跳到绝对位置。
  3. 动作会被 clip 到安全范围——即使切换瞬间动作差异大,物理执行也被安全裁剪。
  4. 低层阻抗控制器会进一步平滑物理执行
  5. 坐标系一致性保证——RelativeFrame wrapper 确保存入训练 buffer 的干预动作与策略动作在同一坐标系:
策略动作: 末端坐标系 → transform_action() → 基坐标系 → 执行
人类动作: SpaceMouse 直接输出基坐标系       → 执行
存入 buffer: 基坐标系干预动作 → transform_action_inv() → 末端坐标系

硬切换不出问题的核心原因:增量控制 + 低频率(10Hz)。策略在人干预期间持续运行,切回时输出已经是基于最新观测的合理动作。

Logo

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

更多推荐