HIL-SERL 算法篇:HG-DAgger 与 RLPD —— 从模仿到超越的训练双阶段
个问题的本质是 训练分布 ≠ 部署分布。BC 只学过在正确状态下做正确动作没学过在错误状态下如何恢复。1.2 DAgger 的解法让专家在你犯错的地方教你DAggerDataset Aggregation是模仿学习的经典算法要解决的正是 behavior cloning 的 covariate shift 问题learner 在 expert 数据上训练部署时自己执行小错误把它带进 expert 数据没覆盖的状态策略随即失效。DAgger的思路是反过来不再只让人演示正确答案而是让机器人自己跑人在机器人实际访问到的状态上给出标注。第一步机器人按当前策略去执行。第二步当机器人跑到它不熟悉的状态时人类专家针对这个状态给出纠正动作。第三步把状态-纠正动作对加入数据集。第四步用扩展后的数据集重新训练。这个循环的关键词是 “在哪跌倒在哪补课”。传统 BC 像是给学生一整套满分卷子让学生背DAgger 像是让学生去考试做错一题老师当场纠正把错题加入错题集。DAgger 有一个参数β控制人类参与度β1时完全听人的β逐渐衰减到 0 时完全听机器人的。如果β衰减太快机器人还没建立起对状态空间的正确理解就被迫全权负责会迅速滑向未知区域。1.3 HG-DAgger 的进化从事后纠正到事前拦截原始的 DAgger 是先跑、再标——机器人先犯错哪怕是虚拟的人再对着回放标注。这对于真实物理机器人来说成本太高一个错误动作可能已经损坏了硬件。HG-DAggerHuman-Gated DAgger引入了 人类门控Human Gating 机制把干预时机从事后提前到事中乃至事前Human-Gated人类门禁代码中没有自动触发接管的逻辑完全依赖于外部 env.step 返回的 info。这意味着接管闸门掌握在拿着 SpaceMouse 的人类手中。硬件触发操作者手持 SpaceMouse3D 鼠标当检测到手对摇杆的推力超过阈值L2 范数 0.001系统立即执行门控开启。零延迟控制权转移在毫秒级时间内屏蔽策略输出将控制权 100% 交给人类。干预即标注接管动作被系统自动识别为高价值纠正数据——接管前的状态标记为潜在失败接管后的动作为正确示范。这套机制的精髓在于 “只学错题”——机器人做得对时人类不动摇杆系统不产生数据只有当机器人跑偏了人类才介入产生的每一条数据都精准落在策略最薄弱的边界场景上每一条数据都是针对机器人当前弱点的特效药。1.4 HG-DAgger 的三大优势极度安全人在实时监控一旦有危险立刻接管对昂贵的真实机械臂非常友好。数据极简只收集机器人不会做的那部分数据避免海量重复数据的干扰。学习风险意识通过人类干预的时机可以间接学习哪些状态是危险的。0x02 HG-DAgger 训练范式全解析HIL-SERL 把训练拆成两个阶段每个阶段使用不同的算法范式范式 算法 数据策略 适用场景HG-DAgger 纯 BC行为克隆 只存干预数据 快速纠偏冷启动RLPD SAC BC 在线数据 干预数据 50/50 采样 持续优化高精度第一阶段跑 train_hgdagger.py 或 train_bc.py。机器人学习基本的视觉-动作映射保存为 Checkpoint。这相当于让机器人小学毕业——能走通任务但还很笨拙。第二阶段启动 train_rlpd.py。脚本会加载第一阶段的 Checkpoint开始强化学习优化。这相当于让机器人上大学——在已有基础上去探索更优的策略最终超越人类。2.1 HG-Dagger的算法伪代码这是 HG-Dagger 的算法伪代码。它的核心在于引入了一个由人类控制的门禁开关G。在实际代码实现中Gt通常不是一个生硬的 if/else而是一个平滑插值afinalβ⋅aexpert(1−β)⋅π(s)。当人类按下摇杆时β迅速从 0 升到 1。由于数据集 D 里存的全是人类纠错的数据训练出来的模型会天然地在这些危险区域拥有更强的回归能力。输入初始策略 π₋₁空数据集 D ∅训练轮数 N每轮步数 T。初始化通过行为克隆BC训练初始策略 π₋₁可选。循环 对于每一轮 i 1$ 到 N交互采样对于每一步t1t 1t1到 T观察当前状态sts_tst。人类评估人类专家实时观察sts_tst并决定门控信号GtG_tGt如果人类觉得危险或需要纠正Gt1G_t 1Gt1人类接管否则Gt0G_t 0Gt0机器人自主动作执行# 数据只在人类接管时被记录。如果Gt1G_t 1Gt1执行人类动作aexperta_{expert}aexpert并记录数据{(st,aexpert)}\{(s_t, a_{expert})\}{(st,aexpert)}到 D。如果Gt0G_t 0Gt0执行机器人动作atπi−1(st)a_t \pi_{i-1}(s_t)atπi−1(st)。环境更新st1←Step(st,Executed Action)s_{t1} \gets \text{Step}(s_t, \text{Executed Action})st1←Step(st,Executed Action)。策略更新将本轮新收集的数据并入总数据集D←D∪DnewD \gets D \cup D_{\text{new}}D←D∪Dnew。在 D 上使用监督学习Supervised Learning训练新策略πi1\pi_{i1}πi1。输出最终训练好的策略πN1\pi_{N1}πN1。2.2 异步双进程架构HG-DAgger 的 train_hgdagger.py 采用 Actor-Learner 异步架构Actor 进程负责与环境/硬件交互。从 Learner 接收最新模型参数执行策略并采样动作。当人类通过 SpaceMouse 介入时覆盖策略动作并记录纠错对。Learner 进程负责训练。从 Replay Buffer 中抓取干预数据进行 BC 训练并将新参数发布给 Actor。2.3 干预检测机制HG-DAgger 的核心逻辑是一个拦截协议1. 机器人先算出一个它想做的动作actions agent.sample_actions(…)2. 执行环境步进注意返回的 infonext_obs, reward, done, truncated, info env.step(actions)3. 拦截逻辑核心if “intervene_action” in info:actions info.pop(“intervene_action”) # 人类覆盖动作already_intervened Trueif already_intervened:data_store.insert(transition) # 只存被干预的 transition这里有两个关键设计为什么用 pop 而不是 get pop 取走并删除了 intervene_action 键防止它残留在 info 字典中被后续的日志记录或 buffer 存储误读。每次干预在当前帧被消费后下一帧必须由环境重新判定是否存在新的干预。如何过滤误触 底层的环境包装器通常有两种过滤方案一是设置死区阈值如 5%SpaceMouse 推力小于阈值则忽略二是幅度判定只有当人类指令强度明显超过机器人当前指令时才判定为有效干预。2.4 BCAgent 组件架构与 BC 预训练BCAgent 是 HG-DAgger 使用的核心 Agent结构极为精简——只有单一 Actor 网络BCAgent 只有单一 Actor 网络├── Encoder: ResNet-10预训练参数冻结│ ├── pooling_method: “spatial_learned_embeddings”│ ├── num_spatial_blocks: 8│ └── bottleneck_dim: 256├── MLP Head可训练:│ ├── activations: tanh│ ├── use_layer_norm: True│ ├── hidden_dims: [512, 512, 512]│ └── dropout_rate: 0.25└── Policy Layer:├── action_dim: 根据任务确定└── tanh_squash_distribution: False参数统计ResNet-10 编码器约 10M 参数预训练且冻结MLP Head 约 0.8M 可训练参数。BC 预训练是 HG-DAgger 的冷启动步骤。先用离线演示数据训练 pretrain_steps20000 步让机器人一出门就不是瞎子def learner(rng, agent: BCAgent, demo_buffer, wandb_loggerNone):update_step 0if FLAGS.pretrain_steps:if os.path.isdir(os.path.join(FLAGS.checkpoint_path, fcheckpoint_{FLAGS.pretrain_steps})):# 从 checkpoint 恢复ckpt checkpoints.restore_checkpoint(…)agent agent.replace(stateckpt)update_step FLAGS.pretrain_stepselse:# 从头做 BC 预训练for step in tqdm.tqdm(range(FLAGS.pretrain_steps), …):update_step 1batch next(demo_iterator)agent, bc_update_info agent.update(batch)checkpoints.save_checkpoint(…)3.4 数据存储策略干预数据以原始步长逐条存入 buffer不做任何分段或截断if already_intervened:data_store.insert(transition) # 一步一个 transitiondemo_transitions.append(copy.deepcopy(transition))只存人类动作策略动作完全丢弃actions agent.sample_actions(obs)next_obs, reward, done, truncated, info env.step(actions)if “intervene_action” in info:actions info.pop(“intervene_action”) # ← 策略动作被覆盖未保存这意味着无法做 Q-filter、Value-Intended 或策略偏差分析——HG-DAgger 完全是监督学习范式不关心 Q 值。为什么不需要切分片段 SERL 使用 TD(0) 单步学习每步 transition 独立。Episode 边界由 masks 字段保护doneTrue → mask0防止跨 episode 的 Q 值 bootstrap。2.6 干预频率监控每个 episode 结束时只记录两个原始数字info[“episode”][“intervention_count”] intervention_count # 干预段数info[“episode”][“intervention_steps”] intervention_steps # 干预步数指标 含义 示例intervention_count 干预段数连续干预算 1 段 3 段intervention_steps 干预步数每步 1 15 步没有计算 intervention_rate也没有基于干预率的自动调整。如需干预率在 WandB 上手动计算intervention_steps / episode_length。2.7 HG-DAgger 完整训练流程图·HIL-SERL-HG-DAgger 完整训练流程图硬件交互被完全封装在 config.get_environment() 中——所有的机械臂驱动、摄像头读取、SpaceMouse 信号处理都对外透明从脚本层面看不到具体硬件型号。所有的硬件逻辑如何读取摄像头、如何给机械臂发指令、如何读取 SpaceMouse 信号都被封装在了 config.get_environment 返回的那个 env 对象里。配置映射运行脚本时输入的 --exp_name如 panda_pcb_insert决定了系统加载哪个硬件配置。轮询频率系统在运行 env.step 时环境包装器Environment Wrapper会以极快的频率如 100Hz轮询 SpaceMouse。触发阈值一旦人类推动了 SpaceMouse即便只是推了一点点包装器就会判定“人类要接管了”。动作覆盖包装器会立即弃掉机器人刚才发出的 actions改用人类的指令发给电机。同时它会在 info 字典里塞入一个 intervene_action 键。无感切换因为这一切发生在毫秒级机器人在还没来得及执行那个错误的动作前就已经被人类的正确动作校正了。0x03 RLPD 训练范式全解析HIL-SERL-buffer3.1 三合一 Buffer 设计RLPDRL with Prior Data是 HIL-SERL 真正的算法内核。它维护一个包含三类经验的混合池Offline Demo初期录制的高质量专家演示pkl 文件。Autonomous Rollouts机器人自己尝试的数据有好有坏。Human Interventions训练过程中人类实时纠错产生的数据。系统在训练时以 50/50 方式采样50% online replay batch 50% demo/intervention batch数据来源 Buffer 采样比例在线交互策略 人类 replay_buffer 50%离线 Demopkl 文件 demo_buffer 50%在线干预SpaceMouse 汇入 demo_buffer 50%干预数据没有独立的 Buffer——它通过 actor_env_intvn 通道汇入 demo_buffer与离线演示数据混在一起享受同样的 50% 采样比例。这意味着在整个训练周期内人类干预数据始终保持 50% 的曝光率不会被海量自主试错数据淹没。3.2 双缓冲区数据管理Actor 端处理干预和双流插入机器人的 “自主试错” 被当作普通经验而人类的 “拨乱反正” 被当作专家演示。这种做法实现了 RL自我进化与 IL向人学习的完美融合。if “intervene_action” in info:actions info.pop(“intervene_action”) # 用干预动作替换intervention_steps 1所有 transition → data_store在线 bufferdata_store.insert(transition)干预 transition → intvn_data_storedemo bufferif already_intervened:demo_transitions.append(copy.deepcopy(transition))Learner 端做 50/50 采样。这正是 RLPDReinforcement Learning from Prior Data的核心逻辑。无论在线数据变得多么庞大系统始终给 “人类老师的教导” 留出一半的席位确保它不会学废。一半从 RL Buffer 采样一半从 Demo Buffer 采样batch replay_buffer.sample(batch_size // 2)demo_batch demo_buffer.sample(batch_size // 2)batch concat_batches(batch, demo_batch) # 拼接为完整 batch关键设计Agent 完全无感知数据来源。在 sac.py 的 update() 中没有任何标记区分 demo vs online vs interventiondef update(self, batch, *, networks_to_update):# 两部分数据在 loss 计算中被完全同等对待loss_fns self.loss_fns(batch) # 统一计算 loss3.3 CTA Ratio用计算换效率cta_ratioCritic-to-Actor Ratio是 RLPD 采样效率的核心引擎。它代表每次 Actor 更新前 Critic 更新的次数cta_ratio2HIL-SERL 默认先做 n-1 次纯 critic 更新最后 1 次做 critic actor 全更新for critic_step in range(cta_ratio - 1):batch concat_batches(online, demo)agent.update(batch, networks_to_update{“critic”})第 cta_ratio 次全网络更新batch concat_batches(online, demo)agent.update(batch, networks_to_update{“critic”, “actor”, “temperature”})cta_ratio critic 更新次数 actor 更新次数 每次都用 50/50 batch2HIL-SERL 默认 2 1 是4serl-main 默认 4 1 是为什么先更新 critic 而不是一起更新因为 actor 依赖 critic 提供的 Q 值来更新策略。如果 critic 还没通过多次拟合看清 Q 值的分布让 actor 对着半吊子 Q去更新就是瞎改。每次 critic 更新都用 50/50 混合 batch保证两类数据对 Q 值估计的影响力始终保持均衡。3.4 SAC 与 DQN 的混合训练RLPD 训练脚本能同时处理纯 SAC连续动作和混合 SACDQN连续离散动作if isinstance(agent, SACAgent):train_networks_to_update frozenset({“critic”, “actor”, “temperature”})else:train_networks_to_update frozenset({“critic”, “grasp_critic”, “actor”, “temperature”})通过 config.setup_mode 自动决定更新哪些网络——‘single-arm-learned-gripper’ 时启用 GraspCritic更新 4 个网络纯 SAC 时只更新 3 个网络。3.5 RLPD 完整训练流程图这是 HIL-SERL 的 “完全体” 脚本。它通过异步架构将极致的计算压力UTD和实时的人类干预缝合在一起。HIL-SERL train_rlpd.py 核心流程图0x04 对比4.1 Dagger vs HG-DaggerDagger 的解法事后补救Dagger 的原始逻辑是先跑、再标。机器人跑人坐在旁边等它跑完然后人对着回放录像说“这里你该往左那里你该往右”。即让人去给这个慌乱状态打标签。这意味着机器人必须先犯错哪怕是虚拟的或者受控的你才能去纠正。HG-Dagger 的解法事先预防HG-Dagger 让人在机器人开始慌的一瞬间就把它拉回来。即人类门禁Human Gating强调在发生偏差之前或者发生瞬间接管它是为真实世界里经不起摔的机器人量身定制的。机器人跑人手握控制柄比如 SpaceMouse。人一旦觉得哎呀要撞墙了或者走错了就直接上手接管。Gated门禁控制的开关门掌握在人手里。人决定什么时候救教什么。DAgger缺乏提升性能的机制。相比之下HG-DAgger 在相同任务下分布更加稀疏。各状态的访问更加均匀。为了达到类似的性能DAgger 可能需要显著更多的演示和纠正并且需要人类操作员更加细致地关注数据质量。4.2 RLPD vs HG-DAgger奖励的两种角色RLPD 和 HG-DAgger 最本质的区别在于对奖励的使用方式。我们可以把这种区别理解为奖励作为燃料 vs “奖励作为终点”。范式 使用数据 主要学习方式 适用理解HG-DAgger 主要使用干预数据 行为克隆 / supervised-style 快速纠偏、冷启动、学习恢复动作RLPD / HIL-SERL 主线 在线数据 demo intervention off-policy RL 持续优化策略性能以学习过程中的阶段为例BC背诵课本死记硬背。HG-DAgger老师在旁边盯着写错一个字就被打手心实时纠错。RLPD刷历年真题并尝试寻找比标准答案更简便的解法自我进化。核心差异train_hgdagger.py 和 train_rlpd.py这两个训练脚本的核心差异主要体现在以下几个方面算法类型不同train_hgdagger.py使用 BCAgent行为克隆而 train_rlpd.py 使用 SACAgent强化学习训练数据源不同HG-Dagger仅使用演示数据demo_buffer在train_hgdagger.py:中只从 demo_iterator 采样RLPD同时使用演示数据和在线经验在train_rlpd.py 中混合 replay_iterator 和 demo_iterator采用50/50采样人机干预处理方式不同HG-Dagger干预数据直接进入演示数据集用于监督学习RLPD干预数据同时进入在线buffer和演示buffer用于强化学习流程对比RLPD强化学习vs HG-DAgger模仿学习流程对比如下HIL-SERL-HG-DAgger-vs-RLPDHG-DAgger奖励作为终点HG-DAgger —— 纯粹的模仿者当你运行 train_hgdagger.py 时你就是在用这种手段跑 HG-DAgger 算法。逻辑机器人只学一件事—“像人一样思考”。目标模仿人类。机器人不看奖励它只学一件事“在这一刻如果是那个人他会怎么推杆”。局限它不看奖励Reward。如果人教错了它就学错。它很难超越人类。在 train_hgdagger.py 中BCAgent 的 loss 完全不看奖励def loss_fn(params, rng):pi_actions dist.mode()log_probs dist.log_prob(batch_actions)actor_loss -(log_probs).mean() # 监督学习只看动作对齐这里的 VICE 奖励只扮演一个角色当画面显示任务成功时通过 MultiCameraBinaryRewardClassifierWrapper 将 reward 转化为 doneTrue 信号触发环境重置。人在不需要腾出手去按键盘实现全自动示教周期。奖励是红绿灯——告诉系统任务结束了别录了但不参与梯度计算。RLPD奖励作为燃料HIL-SERL / RLPD —— 进化的学习者当你运行 train_rlpd.py 时你依然在使用实时干预这个手段但你的算法变成了 RLPD。逻辑干预产生的数据被塞进了 demo_buffer。机器人不仅学人的动作它还盯着 VICE 的奖励分数看。进化如果人教的动作能拿到 0.8 分而机器人自己试出了一个动作能拿 1.0 分比如更快的路径它会通过 RL 的 Q 值优化抛弃人的动作选择更强的动作。在 train_rlpd.py 中分类器输出的[0,1]奖励信号被用作 Bellman 目标的核心输入target_next_min_q target_next_qs.min(axis0)target_q batch[“rewards”] discount * batch[“masks”] * target_next_min_qcritic_loss jnp.mean((predicted_qs - target_qs) ** 2)这是一个时序差分目标——奖励直接决定了 Q 值的更新方向。如果一个动作导致 VICE 分数变高Q 值升高Actor 倾向于再次尝试类似动作。即使机器人还没完全成功奖励信号也能通过画面的微妙变化如接近目标提供微弱的正反馈。奖励是引擎驱动策略持续优化探索比人类更好的路径。深度对比表维度 RLPD HG-DAgger算法类型 SAC BC强化学习 纯 BC行为克隆非干预数据 存入 online buffer 丢弃干预数据 存入 online demo buffer 存入唯一 buffer采样比例 50% online 50% demo 100% 干预数据分类器功能 生成奖励信号Reward Generation 判定任务完成Termination Detection对 Reward 的依赖 极高无奖励则 Q 网络无法更新 极低训练只看人怎么做干预的作用 辅助探索Exploration Help 唯一数据源Data Source最终目的 在奖励信号下寻找最优解 在人类指导下寻找人类解奖励的本质 梯度信号0 到 1 的概率流 二值判定0 或 1失败后果 收到 0 分Critic 降低该动作的身价 任务重置重新开始一次演示学习阶段理解 刷历年真题寻找比标准答案更简便的解法 老师在旁盯着写错字就被打手心两个脚本的衔接关系HG-Dagger 和 RPLD 有一个完美的衔接点HG-Dagger 告诉你什么时候该切换控制权。RPLD 告诉你拿到了人的数据后如何用 RL 进一步超越人。维度 train_hgdagger.py train_rlpd.pyAgent BCAgent SACAgent / SACAgentHybridSingleArm预训练 内置 BC 预训练从零或从 demo 无 BC 预训练加载前阶段权重数据源 仅 demo_buffer replay_buffer demo_buffer 50/50干预目标 产生监督学习数据 产生强化学习数据train_rlpd.py 不内置 BC 预训练因为它假设你已经完成了第一阶段。代码中会尝试加载 Checkpointif FLAGS.checkpoint_path is not None and os.path.exists(FLAGS.checkpoint_path):# 从 hgdagger 的 checkpoint 恢复