关键数据字段流转
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(stateagent.state.replace(paramsparams)) │ ▼ Policy 接管 sample_actions(obs) → (7,) → 若无人干预则直接发送到机器人3.5 核心设计要点干预动作直接替换actions info.pop(intervene_action)使 transition 中的actions字段始终是实际执行的动作learner 直接学习人类动作无需额外标签。双通道数据干预 transition 同时写入两个 bufferRLPD 采样时干预数据权重保持 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, 标记 intervenedTrue (丢弃策略动作返回人类动作) └─ NO → 返回 policy_action, 标记 intervenedFalse (丢弃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.001wrapper 就直接返回 policy 的动作Step 1: 人类移动 SpaceMouse → expert_a, intervenedTrue Step 2: 人类移动 SpaceMouse → expert_a, intervenedTrue Step 3: 人类松手 → norm0 → policy_action, intervenedFalse ← 自动切回 Step 4: policy_action, intervenedFalse ← 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 6D1D [-1,1] 基坐标系维度和范围对齐。动作是 delta pose 而非绝对位姿跳变——new_pose current_pose delta * ACTION_SCALE切换只改变移动方向不会跳到绝对位置。动作会被 clip 到安全范围——即使切换瞬间动作差异大物理执行也被安全裁剪。低层阻抗控制器会进一步平滑物理执行。坐标系一致性保证——RelativeFrame wrapper 确保存入训练 buffer 的干预动作与策略动作在同一坐标系:策略动作: 末端坐标系 → transform_action() → 基坐标系 → 执行 人类动作: SpaceMouse 直接输出基坐标系 → 执行 存入 buffer: 基坐标系干预动作 → transform_action_inv() → 末端坐标系硬切换不出问题的核心原因增量控制 低频率10Hz。策略在人干预期间持续运行切回时输出已经是基于最新观测的合理动作。