【机器人 / 强化学习】HIL-SERL 算法篇:DQN + SAC 混合架构的实现哲学

📅 发布时间:2026/7/21 18:43:08
【机器人 / 强化学习】HIL-SERL 算法篇:DQN + SAC 混合架构的实现哲学 0x00 概要HIL-SERL 不是单一算法而是一套高度集成的混合 RL 系统。它用 SAC 给机器人灵动的手臂、用 DQN 给机器人果断的夹爪、用二值分类器替代手工奖励设计、用人类干预数据弥合 RL 探索与 BC 模仿之间的鸿沟。这篇我们从算法角度逐层拆解它的设计逻辑。HIL-SERL-train0x01 HIL-SERL 的总体算法思路1.1 HIL-SERL 的算法基因图谱HIL-SERL 的核心算法血统是[MDP 框架]│↓[SAC (Off-policy, 双Q网络)] → [最大熵RL: 探索 鲁棒性]││ 离线专家数据↓[Offline RL (BCQ/BEAR/CQL 思想)] → [Q_exp 作为先验: 解决稀疏奖励 冷启动]││ SAC Q_exp 先验│ offline demonstration replay RLPD↓[SERL] → [带专家先验的在线RL: 解决离线RL无法超越专家]││ 人类实时纠正↓[DAgger / Interactive Learning] → [在策略分布下收集纠正数据: 解决分布偏移]││ SERL HIL↓[HIL-SERL] → [高精度灵巧操作的完整解决方案]这不是一条算法演进的树形图谱而是一条明确的工程叠加链——HIL-SERL 的每一个组件都在解决真实世界 RL 的一个具体痛点SAC 提供样本高效的连续控制基类通过熵正则化保证探索稳定性。RLPD 通过 50/50 混合采样online demo和高 UTD 比率让 SAC 能同时利用离线专家数据和在线交互数据训练解决从零开始 RL 样本效率不足的问题。HIL-SERL 在 RLPD 之上叠加三层工程创新人类实时纠正解决 RL 在真实环境中探索成本过高的问题、混合动作空间解决夹爪连续输出犹豫不决的问题、二值分类器奖励解决复杂视觉任务中奖励工程困难的问题。1.2 三类预训练/先验机制HIL-SERL 包含三类预训练/先验机制每一类解决不同层面的冷启动问题视觉 encoder 使用 ImageNet-1K 预训练 ResNet-10解决真实图像复杂度高、从头训练数据效率太低的问题每个任务单独训练 reward classifier解决奖励函数难以手工设计的问题少量人类 demonstrations 做 BC 预训练 / RLPD 加速解决 RL 冷启动探索盲目性的问题但这些都不是跨机器人形态、跨任务的通用价值函数。它们是任务特定的、工程导向的先验。1.3 HIL-SERL 的训练流程整个训练过程可以分为四个阶段阶段一离线准备人类通过遥操作收集两类数据200 张成功 1000 张失败图像 → 训练 Reward Classifier约 5 分钟20-30 条成功演示轨迹 → 初始化 Demo Buffer阶段二专家 Q 函数预训练Offline在离线演示数据上执行标准 Bellman Backup 训练 Q_exp(s, a)用于初始化在线 Critic。这防止了在线训练启动时 Critic 从零开始的盲目性。阶段三在线 HIL-SERL 训练策略执行 π(a|s) → 人类观察├─ 机器人正常执行 → 继续自主└─ 机器人出错/卡住 → 人类通过 SpaceMouse 接管↓干预数据存入 D D_exp ∪ D_RL ∪ D_HIL双通道↓SAC 策略更新含 RLPD 50/50 采样 Q_exp 先验引导↓干预频率随策略提升逐步下降直至策略自主完成任务阶段四部署训练好的策略部署到真实机器人。由于策略是通过 RL 而非纯 BC 训练的它学会了比人类操作更快的动作模式论文报告约 1.8x faster。训练循环具体训练循环如下HIL-SERL-HIL-SERL 训练循环训练数据流训练数据流如下HIL-SERL-训练数据流1.4 为什么 HIL-SERL 能超越模仿学习论文报告 HIL-SERL 相比 imitation learning baseline 有显著提升。我们从机制上拆解原因模仿学习的根本局限BC 训练的是在专家状态下输出专家动作。一旦机器人偏离专家状态BC 没见过这些 OOD 状态无法恢复。HIL-SERL 的打破方式人类纠偏正好发生在策略出错时。这些数据不是普通演示而是从错误状态恢复的演示。这类数据对解决 compounding errors 非常有效。RL 还能优化速度和路径论文还强调 HIL-SERL 不只是提高成功率也能降低 cycle time。RL 不只是模仿人类路径它可以在任务 reward 驱动下探索更快、更适合机器人的动作模式。人在操作时会有冗余动作手抖、犹豫、绕路SAC 的目标是最大化 Q 值在训练中会发现更短、更直的路径。0x02 核心算法RLPD 人类干预2.1 RLPD 的干预版HIL-SERL 依然以 RLPD 为核心但数据来源变了。传统的 RLPD 只用开始采集好的 Demo而 HIL-SERL 在训练过程中通过人类干预不断注入新的高质量数据。人在训练过程中如果发现机器人要搞砸了可以用 SpaceMouse 实时接管。这些接管的数据会被存入 Buffer。算法本质依然是带有高 UTD 和 LayerNorm 的 SAC但它通过人类干预解决了 RL 在复杂任务如插拔正时皮带、组装仪表中探索不到成功状态的难题。HIL-SERL-RLPD为什么 50/50 采样有效 在线数据提供最新的状态覆盖让 Critic 学习到当前策略分布下的价值Demo 数据含干预提供高价值 recovery 轨迹防止 Critic 忘记专家先验。两者缺一不可。2.2 BC Loss 的作用在 rlpd.py 中有一行 bc_lossBehavioral Cloning Loss。在升级 Actor 时如果不仅让它最大化 Q 值还强制让它模仿演示数据里的动作这对训练初期的稳定性有很大帮助。为什么 BC Loss 能稳定初期训练因为模仿学习的真理信号来自人类演示而不是来自不一定靠谱的自奖励网络。BC 的 LossLoss(a−ademo)2——目标很明确动作要离人类近SAC 的 LossLossαlogπ(a|s)−Q(s,a)——目标是熵要大且 Q 值要大当 Q 值萎缩到 0 时SAC 的 Loss 变成了αlogπ(a|s)−0。为了最小化这个 Loss智能体必须最大化熵随机性。后果是智能体开始胡乱甩动彻底忘记人类教过什么。BC Loss 的引入正是为了防止这种退化。2.3 训练数据流的双通道设计HIL-SERL-双流0x03 混合动作空间SAC DQNHIL-SERL 作者发现用 SAC 去夹爪的开关这种二进制动作动作效率很低所以单独拆了一个 DQN 出来专门练抓取评价。SAC负责控制机械臂的 6D 末端位姿连续动作。通过正则化SAC 的探索是平滑的。它在动作周围进行微小的、有目的的试探。这也是为什么 SAC 更适合精细动作。DQNDeep Q-Network专门负责控制夹爪的开/关离散动作。DQN 的探索是抽风式。大部分时间选最好的小部分时间随机乱选一个动作。这比较生硬。这解决了机器人操作中一个非常现实的矛盾手臂需要丝滑的连续移动而手指夹爪通常只需要果断的开关动作。3.1 为什么要分而治之机器人操作的动作由两个截然不同的部分组成机械臂运动连续空间手臂需要在三维空间中精确移动位置坐标(x,y,z)是连续的实数。SAC 的 Gaussian policy 通过输出均值μ和方差σ天然适合这类具有无限可能性的平滑运动。夹爪动作离散空间夹爪通常只有两到三个离散状态——张开、保持、闭合。如果用 SAC 的连续输出去拟合会产生类似 0.13、-0.27 的中间值夹爪执行时被阈值化学习信号不稳定。更重要的是何时闭合夹爪是一个非常关键的离散决策——闭早了抓空闭晚了错过物体。这就引出了一个自然的设计用 SAC 给机器人灵动的手臂用 DQN 给机器人果断的夹爪。3.2 SAC DQN 如何协同工作在 HIL-SERL 的网络中SAC 和 DQN 不是两个独立的进程而是同一个主干下的两个分支HIL-SERL-网络架构共享主干所有相机图像经过 ResNet-10共享权重编码后与 proprioception 的 MLP 编码拼接输入共享的 MLP Head。SAC 分支输入特征 → 输出均值 μ 和方差 σ → 采样得到连续的 Delta 位移向量单臂 6 维双臂 12 维。DQN 分支GraspCritic输入特征 → 输出 3 个 Q 值关/保持/开→ argmax 选出最大 Q 的动作。统一更新虽然算法不同但它们在同一个训练循环中被同步优化。具体来说HIL-SERL 在这些任务中分别求解两个马尔可夫决策过程MDPM1{S,A1,ρ1,P1,r,γ}M2{S,A2,ρ2,P2,r,γ}其中A1和A2分别是连续和离散动作空间。它们都接收来自环境的相同状态观测如图像、本体感受、夹爪状态等。对于M2的critic更新遵循标准的DQN方法并引入额外的目标网络以稳定训练。在训练或推理时HIL-SERL 首先从策略M1中查询连续动作然后在M2中通过对评论家critic的输出取argmax来获得离散动作最后作者将连接的动作应用于机器人。3.3 Rollout 时的配合在 sample_actions 方法中两个分支的协作流程非常清晰def sample_actions(self, observations, *, seed, argmaxFalse, **kwargs):# SAC 部分生成连续动作位置方向dist self.forward_policy(observations, rngseed, trainFalse)if argmax:ee_actions dist.mode()else:ee_actions dist.sample(seedseed)# DQN 部分选择离散抓取动作 grasp_q_values self.forward_grasp_critic(observations, rnggrasp_key, trainFalse) grasp_action grasp_q_values.argmax(axis-1) # 贪心选择 # 组合动作 return jnp.concatenate([ee_actions, grasp_action[..., None]], axis-1)连续控制分支的 Rollout 符合经典 SAC 范式——只依赖 Actor 网络Critic 不参与推理。离散控制分支则不同——由于没有独立的 Actor 网络GraspCriticDQN在推理时直接充当决策者通过 argmax 选出最优离散动作。3.4 训练时的配合训练时SAC 和 DQN 使用不同的损失函数和不同的回传目标但共享同一套观测编码Critic 训练只使用连续动作部分actions batch[“actions”][…, :-1] # 去掉最后一维夹爪target_q rewards γ · min_i Q_target_i(s’, a’)critic_loss MSE(predicted_q, target_q)GraspCritic 训练只使用离散夹爪部分 grasp_penaltygrasp_rewards batch[“rewards”] batch[“grasp_penalty”]target_grasp_q grasp_rewards γ · max Q_target(s’, a’_grasp)grasp_critic_loss MSE(predicted_q, target_grasp_q)Actor 训练采样连续动作最大化 Q(s,a) - α·log π(a|s)actor_loss -mean(predicted_q - temperature * log_probs)这种设计确保了 reward 的分工SAC Critic 只关心末端执行器的连续动作价值GraspCritic 则额外学习不要做无意义夹爪动作的惩罚信号。3.5 输出维度对比设计者把动作空间拆成了两部分前 6 维连续动作由 SAC actor 输出最后 1 维夹爪动作由 GraspCriticDQN 风格选择在 update 中agent 明确要求单臂动作维度为 7chex.assert_shape(batch[“actions”], (batch_size, 7))具体对比如下Agent 类型 SAC 输出 DQN 输出 最终动作维度SingleArm 6 维 (3位置3方向) 3 维 (关/保持/开) 7 维DualArm 12 维 (双臂各6维) 9 维 (3×3双臂组合) 14 维3.6 SAC vs DQN靶向设计两者在 Target 计算上的差异反映了各自面对的问题域完全不同DQN 的 Target硬最大Targetrγmaxa′Qtarget(s′,a′)它假设下一时刻一定选分数最高的动作。因为动作空间是离散且极小的2-3 个通过 target_net 的缓慢更新已经能抵消大部分高估问题。SAC 的 Target软预期TargetrγEa′∼π(⋅|s′)[Qtarget(s′,a′)−αlogπ(a′|s′)]它不选最高的而是对当前策略输出的所有可能动作求期望并加上熵奖励。面对无穷多个连续动作SAC 必须用双网络取最小值Clipped Double-Q来暴力压制高估。把这两个差异总结成一句话DQN 是轻量级防守离散空间够小target_net 足矣SAC 是重量级防守连续空间高估无限放大必须双 Q min。这个区别反映在代码上就是DQN 找最大值SAC 则需要从 Actor 里采样一个动作a′出来算 Q 值。0x04 GraspCritic夹爪离散决策网络GraspCritic 是 HIL-SERL 中一个关键但容易被忽视的设计。它不是普通 SAC 里的 Critic(s, a)而是一个只输入 observation、不显式输入 action 的离散动作 Q 网络。4.1 定位与架构GraspCritic 不是普通 SAC 里的 Critic(s, a)而是一个只输入 observation、不显式输入 action的离散动作 Q 网络。GraspCritic 的输入输出输入: observation (无 action)输出: 3 个 Q 值 → 夹爪开(0) / 保持(1) / 关(2)训练: DQN 式目标在线网络选动作 目标网络评估奖励: grasp_rewards batch[“rewards”] batch[“grasp_penalty”]网络结构同样采用 ResNet-10 MLP但 MLP 维度比 Critic 小单臂为 [128, 128]因为它只需要学习夹爪的离散决策不需要建模连续动作的精细价值。class GraspCritic(nn.Module):encoder: Optional[nn.Module]network: nn.Moduleoutput_dim: Optional[int] 3 # 默认3个离散动作def __call__(self, observations, trainFalse): obs_enc self.encoder(observations) # ResNet编码 outputs self.network(obs_enc, train) # MLP value nn.Dense(self.output_dim)(outputs) return value # (batch_size, 3)核心前向逻辑是输入 observation经过视觉 / proprioception encoder经过 MLP输出 output_dim 个 Q 值。默认 output_dim3对应单臂夹爪的 3 个离散动作0 - 环境动作 -11 - 环境动作 02 - 环境动作 1代码里训练时把环境动作最后一维从 {-1, 0, 1} 映射到 {0, 1, 2}grasp_action jnp.round(batch[“actions”][…, -1]).astype(jnp.int16) 1因此GraspCritic(obs) 的输出可以理解为[Q_grasp(obs, close_or_negative),Q_grasp(obs, keep),Q_grasp(obs, open_or_positive)]也就是说它在回答一个很具体的问题在当前图像和机器人状态下夹爪应该关、保持还是开每个选择未来能带来多大价值4.2 为什么需要单独的 GraspCritic考虑一个完整的动作向量[x, y, z, roll, pitch, yaw, gripper]前 6 维是连续控制SAC 的 Gaussian policy 天然适合。但最后一维 gripper 本质上不是平滑连续信号。如果把夹爪也塞进连续的 SAC actor 里动作语义离散但 policy 输出连续SAC 会输出类似 0.13、-0.27 这种值夹爪真实执行时被阈值化导致学习信号不稳定夹爪动作稀疏且关键抓取任务里什么时候闭合夹爪是一个关键离散决策。闭早了抓空闭晚了错过物体夹爪误操作需要额外惩罚代码里有 grasp_penalty惩罚不必要的开合动作。例如 USB 插入任务中夹爪已经接近闭合还继续执行关闭动作会产生惩罚所以这份实现把动作空间拆开连续末端执行器动作SAC actor SAC critic 学离散夹爪动作GraspCritic 用 DQN 风格学4.3 学习原理Double DQN 风格的离散动作价值学习GraspCritic 的训练采用的是 DQN 式 Bellman 回归Qθ(st,agt)←rgtγ⋅maxagt1Q¯θ(st1,agt1)但代码里更接近 Double DQNStep 1用 online grasp critic 选择下一步最优夹爪动作next_grasp_qs self.forward_grasp_critic(batch[“next_observations”], rngrng)best_next_grasp_action next_grasp_qs.argmax(axis-1)Step 2用 target grasp critic 评估这个动作的 Q 值target_next_grasp_qs self.forward_target_grasp_critic(…)target_next_grasp_q target_next_grasp_qs[jnp.arange(batch_size), best_next_grasp_action]Step 3构造目标值含 grasp_penaltygrasp_rewards batch[“rewards”] batch[“grasp_penalty”]target_grasp_q grasp_rewards discount * masks * target_next_grasp_qStep 4当前网络只取实际执行的夹爪动作对应的 Qpredicted_grasp_q predicted_grasp_qs[jnp.arange(batch_size), grasp_action]Step 5用 MSE 做 TD 回归grasp_critic_loss jnp.mean((predicted_grasp_q - target_grasp_q) ** 2)