ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

稀疏奖励强化学习实战:HER事后经验回放原理与代码实现

稀疏奖励强化学习实战:HER事后经验回放原理与代码实现 “hindsight”这个词放在强化学习圈子里基本就是 Hindsight Experience Replay事后经验回放的代号。我第一次读那篇论文的时候最大的感触是一个困扰我很久的稀疏奖励问题竟然被一个如此“反直觉”的思路给绕过去了。机械臂抓取、推箱子这类任务里奖励信号稀少到几乎全是零随机探索半天都碰不上一回成功训练曲线像一条心电图上的直线。HER 的解法一句话就能概括既然这次没做到预设目标就别死盯着那个没完成的目标不放把实际到达的终点“重新定义”成目标然后把这整条轨迹当作成功样本喂回给智能体。这篇文章我会从算法原理一路聊到代码落地把目标重标注策略、网络结构、训练参数和几个真实踩过的坑全部摊开适合正在做机器人操作、稀疏奖励控制或多目标强化学习的朋友参考。1. 从“事后视角”到可回放的样本HER 到底解决了什么问题1.1 强化学习里的“稀疏奖励死局”先说一个典型场景。我要训练一个机械臂任务是抓住桌面上的方块移动到指定位置。环境给的奖励很吝啬每一步只要没成功就返回 -1只有到达目标区域的那一步才给 1 或者 0。整个 episode 可能持续 50 步也就是说初始阶段几乎每一个样本的奖励都是负的策略梯度算出来的期望几乎全被负值淹没根本找不到向哪里调整才能提高收益。换一种说法奖励函数是“二值稀疏”的成功区域在连续状态空间里只占一个微不足道的小球随机初始化策略能碰到这个小球的概率低到可以忽略。没有命中就没有正信号没有正信号梯度就基本无效策略就一直原地打转。这不是某个具体算法的问题DDPG、PPO、SAC 遇到这种环境都会集体歇菜因为它们在样本层面缺少“正向反馈”的引导。有人会想那把奖励换成稠密的不行吗比如用距离作为惩罚。可以但有两个隐患。第一稠密奖励需要精心设计距离度量稍微定义错了智能体会学到“手伸得很近但不碰物体”这种投机行为。第二真实机器人场景里很多任务根本没有现成的距离度量你只能靠二值判定比如物体是否进入目标框。所以稀疏奖励问题本质上绕不开必须在算法层面解决。1.2 “目标可以事后补”这个反直觉设计HER 的做法非常特别。假设机械臂运行了一条轨迹 τ这条轨迹的真实目标是 g但 episode 结束时智能体到达了状态 s_T而这个 s_T 离 g 十万八千里所以奖励全是负的。现在关键一步来了我们把 g s_T 当作这条轨迹的新目标然后用 g 重新计算每一步的奖励。由于轨迹终点恰好等于 g最后一步的奖励一定是正信号整条轨迹也从“失败案例”变成了“成功案例”。这里要理解为什么这个操作是合法的。在目标条件强化学习里策略的形式是 π(s, g) → a也就是说动作由“当前状态”和“目标”共同决定。目标 g 只是策略的输入不参与环境的状态转移动力学。所以当我修改目标、修改奖励时并没有改变任何一条物理状态转移的真实性只是重新解释了“这条轨迹原本在尝试完成什么目标”。这相当于在说我虽然没完成你交代的任务 A但我完成了一件事 B那么这条经验对学习“如何完成 B”就非常有价值。更细节的一点是目标重标注通常不是随便选一个状态而是优先选择轨迹中“未来某个时刻真实到达的状态”。为什么因为一条轨迹里的状态本身是因果链串起来的后半段的动作确实把机械臂从状态 A 带到了状态 B所以用 B 作为目标去给前半段打标签网络学到的就是“一步步接近 B”的正确因果模式。如果随机选一个从未到达过的状态当目标你等于在教智能体完成一个不存在于现实中的目标样本就成了噪声。1.3 它和普通经验回放、逆强化学习的本质区别普通经验回放只是把历史数据存下来重复使用解决的是样本利用率问题但它没有改变“正样本稀少”这个事实。HER 则是在不改变状态转移的前提下重新标记了轨迹的“成功属性”把原本被判定为失败的大量数据改造成目标条件策略下的正样本这是质变而不是量变。逆强化学习是从专家演示中反推奖励函数它依赖高质量示教数据。HER 完全不依赖专家环境奖励函数也不动它改的是“目标条件缓冲区内数据的目标标签”。站在经验回放的角度HER 就像是给每一条失败轨迹贴上了一张新便签“这条轨迹实际证明了‘把状态从 s 变到 s_T’是可完成的”。这种反事实标注逻辑才是 HER 真正的核心创新。2. 核心细节拆解目标重标注策略怎么选、网络怎么设计2.1 目标条件强化学习的基本框架要理解 HER先要把目标条件 MDP 的符号理清。环境里每个 episode 会采样一个目标 g∈G通常 G 是状态空间的一个子集或某个特征空间的子集比如物体位置坐标。策略 π(a|s, g) 根据当前状态和目标决定动作。每一步环境根据状态 s、动作 a、新状态 s 和目标 g 返回奖励 r_g(s, a, s)最常见的设定是稀疏二值形式r_g(s, a, s) 1如果 ||φ(s) - g|| δ否则为 0或 -1。这里的 φ(s) 是状态到目标特征的映射δ 是成功判定阈值。在 OpenAI Gym 的 Fetch 系列环境里这个 φ(·) 就是 achieved_goal也就是环境中实际达到的目标特征。所以 HER 里其实有一个隐藏前提我们需要能随时拿到“当前状态对应的目标特征”也就是 achieved_goal否则没法重标注。网络结构上目标条件策略和价值函数都要把 goal 作为额外输入。早期做法是直接把 (s, g) 拼接起来喂给 MLP效果基本够用。更复杂的做法是给 goal 单独设计特征提取分支再和状态特征融合。对大部分任务来说拼接就够了真正的关键在于训练数据的分布而不是网络有多花哨。2.2 HER 四种重标注策略的对比论文里给出了四种从轨迹中选择新目标的方式策略新目标来源优势明显短板final轨迹终点 s_T简单稳定把“失败轨迹”变成完整成功轨迹目标多样性差所有轨迹都指向各自的终点容易让策略只学到“奔向终点”future当前步 t 之后随机选 K 个状态 s_jjt作为目标覆盖轨迹后半段多种中间状态数据分布更丰富论文推荐实现稍复杂K 值需要调参episode从同一条 episode 里随机选一个状态目标分布覆盖整条轨迹会选到“当前步之前”的状态造成目标在时间上颠倒因果混乱random从整个回放缓冲区的所有状态中随机选目标最多样与当前轨迹的状态分布差异大大部分新目标距离过远学习信号信噪比低从我的实测经验看final 策略在任务较简单时也能收敛但训练曲线更抖random 和 episode 策略在长 horizon 任务里经常直接哑火。future 策略是最稳的因为它在“时间一致性”和“目标多样性”之间取到了平衡。每个原始转移配 K4 个重标注转移是论文里的常用配置我复现时也基本沿用没有遇到大问题。2.3 网络的输入输出与目标编码目标条件策略常见的设计是actor 输入是拼接后的 (state, goal)输出动作critic 输入是拼接后的 (state, goal, action)输出 Q 值。这里有几个容易被忽视的坑。第一目标特征必须和状态特征做同样的归一化。FetchPush 环境里物体坐标和手爪坐标在 0~1 范围还行但有些环境里角度、速度的量纲差异巨大直接拼接会导致梯度被大数值维度主导。我的做法是把状态和目标统一 clip 到 [-1, 1]或者做 z-score 标准化。第二目标条件网络很容易把 goal“当成噪声”。原因是如果目标分布太散网络会发现无论 goal 怎么变Q 值平均一下也能混过去。future 策略之所以强就是因为它会大量生成“和当前状态非常接近”的目标迫使网络必须真正利用 goal 信息来区分价值高低从而学到“距离目标越近 Q 值越高”这个隐含结构。第三目标不要只在训练初期变化太大。HER 里重标注目标每次都来自真实轨迹所以目标的分布是随着训练逐渐演化的——一开始模型只会奔向随手可达的近距离目标后来慢慢能完成更远的目标这天然形成了一种自适应的课程学习。如果你把目标分布固定死反而会破坏这个自举过程。3. 从零实操在 FetchPush 上用 DDPGHER 训练一个机械臂3.1 环境选择与观察空间解读OpenAI Gym 的 Fetch 系列是 HER 最常见的验证环境四个任务分别是 Reach、Push、PickAndPlace、Slide难度递增。入门我推荐 FetchPush-v1因为它动作维度和状态维度适中训练速度快能很快看到 HER 的效果。进环境之后观察 space 是一个 dict包含三个字段observation机械臂的关节角度、角速度、手爪位置、物体位置、物体相对手爪的位置等achieved_goal当前实际到达的目标特征在 Push 任务里就是物体的当前二维坐标desired_goal本次 episode 想要到达的目标坐标HER 重标注时我们最关心的就是 achieved_goal。它的时序变化就是轨迹的“真实成就记录”新的目标从这里面挑新奖励也按新目标重新算。环境每一步会返回是不是成功内部判定就是用 desired_goal 和 achieved_goal 的距离阈值。3.2 DDPGHER 的核心循环代码我用 PyTorch 复现过一版最简实现逻辑清晰适合拿来当骨架改成自己的任务。先定义一个 episode 级别的缓冲区注意这里和普通经验回放有个关键区别必须按 episode 存因为重标注需要用到一整条轨迹的 achieved_goal 序列。class EpisodeBuffer: def __init__(self, capacity): self.capacity capacity self.episodes [] # 每个元素是 dict包含 states, actions, achieved_goals, desired_goals self.idx 0 def push(self, ep): if len(self.episodes) self.capacity: self.episodes.append(ep) else: self.episodes[self.idx] ep self.idx (self.idx 1) % self.capacity def sample_her_transitions(buffer, batch_size, k4, strategyfuture): transitions [] for _ in range(batch_size): ep random.choice(buffer.episodes) t random.randint(0, ep[states].shape[0] - 1) # 原始转移先保存 for _ in range(k): if strategy future: # 从 t1 到 episode 末尾之间随机选一个状态索引 j random.randint(t 1, ep[states].shape[0]) new_goal ep[achieved_goals][j] elif strategy final: new_goal ep[achieved_goals][-1] else: raise NotImplementedError reward compute_sparse_reward(ep[achieved_goals][t 1], new_goal) transitions.append({ state: ep[states][t], action: ep[actions][t], next_state: ep[states][t 1], goal: new_goal, reward: reward }) return transitions需要注意compute_sparse_reward 要基于下一时刻的 achieved_goal 与目标之间的差距距离小于阈值给正奖励否则给负奖励。在 Fetch 环境里这个阈值一般是 0.05。这是一个容易被忽略的点HER 里重标注并不是只改目标不改奖励而是目标和奖励都要重算否则时序就乱了。训练主循环也不复杂每收集完一个 episode就丢进缓冲区然后每次梯度更新时做一次 HER 采样for epoch in range(max_epochs): # 收集一个 episode obs env.reset() ep_states, ep_actions, ep_achieved [], [], [] for step in range(episode_len): action actor.select_action(obs[observation], obs[desired_goal]) noise 0.2 * np.random.randn(*action.shape) next_obs, reward, done, _ env.step(action noise) ep_states.append(obs[observation]) ep_actions.append(action) ep_achieved.append(obs[achieved_goal]) obs next_obs if done: break ep_states.append(next_obs[observation]) ep_achieved.append(next_obs[achieved_goal]) buffer.push({states: np.array(ep_states), actions: np.array(ep_actions), achieved_goals: np.array(ep_achieved)}) # 更新每次至少采样一组 HER 转移做 DDPG 更新 transitions sample_her_transitions(buffer, batch_size128, k4, strategyfuture) update_ddpg(transitions)这里没有贴完整的 actor/critic 更新代码但核心就是普通 DDPG 的三件套critic 回归 TD 误差、actor 最大化 Q 值、目标网络软更新。HER 只是改变了“喂给 DDPG 的数据到底是什么”算法本体的优化方式不需要大改。3.3 关键参数配置参考我复现时用过的配置如下在 FetchPush-v1 上大约 30~50 万个时间步能看到成功率爬升如果机器性能好可以更快参数取值备注优化器Adamactor 和 critic 都可用lr 1e-3折扣因子 γ0.98稀疏奖励下偏小一点更好回放缓冲区容量2000 个 episode比普通回放池小很多但按 episode 存HER 策略future, k4每个原始转移补 4 条重标转移批量大小128太大太小的都试过128 比较稳探索噪声高斯噪声std0.2训练后期可以降到 0.1目标网络更新τ0.05 软更新每步都更新episode 长度50Fetch 环境默认值这里有个经验之谈HER 对回放缓冲区的大小没那么敏感但对“按 episode 存储”这件事很敏感。如果你图省事把转移一条条丢进普通回放池再从中采样做重标注会因为转移动态失去轨迹结构future 策略根本没法实现final 策略也会变得非常稀碎。所以一定得维护 episode 级的数据结构。3.4 训练曲线怎么读刚上手的时候看到前几千步环境内成功率还是 0很容易慌。但 HER 有个典型现象回放缓冲区里的“重标转移”成功率会先涨因为大量失败轨迹被贴上了成功标签所以 critic 的 Q 值会先学会“区分接近目标和远离目标”。这时候环境内成功率仍然 0 是完全正常的critic 正在构建对目标的距离感知能力。我判断训练是不是真的在推进通常会看两个额外指标一是 actor 输出的动作是否开始随目标变化二是用固定目标做测试时机械臂末端是否在向目标方向移动即便没进阈值。只要这两点出现说明 HER 的自举机制已经启动了后面成功率爬坡只是时间问题。4. 常见问题与排查技巧实录4.1 训练完全不动成功率一直为零这个问题我遇到过不止一次。最典型的检查顺序是先确认环境本身在动。打印每一步的 achieved_goal看手爪和物体坐标是否随时间变化。如果物体根本没动那可能是随机种子或者动作空间理解错了。再看奖励函数Fetch 环境里 reward 已经从 step 返回但有一些封装过的环境会把奖励弄丢如果你发现 reward 恒等于 0那问题在环境层不在算法层。然后检查重标注是否真的在采样里生效。很多初版实现会有个隐蔽 bug代码逻辑上重标了但采样时用的是原始目标而非新目标。最直接的排查方法是在训练循环里打印一条 sample 出来的 (goal, reward)手动算一下如果 goal 不等于 desired_goal 但 reward 却按新目标算对了说明重标注生效。也可以临时关闭 HER把 k 设成 0看训练是否立刻崩溃——如果崩溃说明之前能学完全是靠 HER也侧面说明重标注逻辑是通的。4.2 future 策略的 k 值和重标注比例怎么调k4 是论文的默认值但不同任务的最佳值不一样。任务越复杂、horizon 越长k 可以适当增大到 8 或 16因为需要更多“中间态目标”的样本来铺满轨迹上的学习梯度。但也不是越大越好k 太大会让缓冲区里同一原始轨迹的重复样本过多多样性下降训练变慢。还有重标注比例的设置。论文里常见做法是每条原始转移都配 k 个重标转移相当于重标注比例是 k:1。我自己试过“只重标注最后几步”的变体效果很差因为中间步骤缺少正样本引导整个轨迹前面的状态还是“没有信号”的死区。所以一开始就全轨迹重标注别只在尾部做文章。4.3 目标与观察空间的归一化陷阱我能踩这个坑踩得很深。有一次在自定义机械臂环境里用 HER状态量包含关节角度弧度、末端速度带负号和物体像素坐标数值范围从 -3 到 1000 都有。直接拼接进网络之后前几轮训练 loss 直接变成 NaN。解决方法很粗暴但有效收集一批初始状态和目标计算每个维度的 min、max然后所有输入统一缩放到 [-1, 1]。注意这里不是简单除以某个数而是要对 state 和 goal 分开统计因为它们的范围可能完全不同。另外如果观测里有“相对位置”和“绝对位置”两种特征建议只保留一种否则网络要额外学会做减法才徒增负担。4.4 从仿真到真机目标重标注的方向偏差仿真环境里你可以拿到完美的全局坐标但真机上你通常只能拿到视觉估计或者相对位姿。HER 里常见的问题是把“实际到达的终点状态”直接当目标可能会学到一些不该学的行为。举个例子推物体任务里如果只看手爪的位置物体本身的位置变化才是真正的任务结果。如果重标注时把目标定义在手爪坐标上策略可能学到“手爪随便移到某个位置都算成功”完全忽略了物体。我自己在仿真里就见过这种走捷径的案例。真机部署时建议只对“与任务意图强相关的特征”做重标注比如物体最终位置而不是把所有观测维度都纳入目标向量。另一个思路是增加约束项在重标注奖励里加一个边界惩罚防止策略把目标定义到不可行的区域。5. 从算法到方法论hindsight 的启示和延伸用法5.1 为什么“失败样本转正”能大幅提高数据利用率HER 的精髓可以这样理解在普通强化学习里一条轨迹只有对“预定的那个目标”产生正信号时才具有学习价值而 HER 通过目标重标注把同样一条轨迹对多个“反事实目标”同时赋予了学习价值。数据没有变多但单位数据的有效信息量大幅上升。打个生活中的比方项目经理复盘一个失败的项目如果只盯着“当初的目标没达成”那这场复盘只剩教训但如果换个角度把过程中实际产出的成果当作“已经实现的目标”来归档这个项目就对“如何做到类似产出”提供了完整的方法论。强化学习里这个类比完全成立——只是项目目标变成了 goal成果变成了轨迹终点。5.2 把 HER 思想延伸到离线学习和多任务学习HER 不只是一种回放算法更是一种数据构造哲学。在离线强化学习里如果固定数据集里全是稀疏失败轨迹直接训练策略几乎学不到东西。你可以用 HER 的方式给离线数据补充目标标签对每一条转移从它所属轨迹的未来状态里选一个新目标重算奖励再把原始轨迹和重标轨迹都放进训练集。这样离线数据的利用率会好很多。在多任务和分层强化学习中HER 里的“终点状态作为子目标”思路也经常用到。高层策略负责在事后从状态空间里挑一个有意义的子目标底层策略负责完成它——这其实就是把 HER 的 future 重标注从训练技巧升级成了任务分解机制。再配合自动目标生成比如从历史轨迹中聚类出常见终点可以让智能体在无人工标注的情况下自己发现可学会的中间阶段。5.3 一个项目复盘的小例子我以前做过一个调度系统改造引入强化学习之后最大的问题就是“大多数 episode 都以超时失败告终”。一开始我只盯着成功率整条日志几乎全是负样本怎么看都觉得没救。后来受 HER 启发我把失败 episode 里“实际完成的调度次数”当作新的目标重新标注了一部分数据专门训练模型“如何在限时内完成尽量多的调度”。效果意外得好——模型确实学会了在资源受限时优先完成简单任务而这个行为在原来的单一目标下是永远学不到的。当然这种“转正”要谨慎不是所有失败结果都能被当作有价值的目标。重新标注之后你要用业务规则再审查一遍确认这个“被实现的目标”本身是合理、通用、可复用的。如果只是为了凑正样本把毫无意义的终点状态标成目标那就是给模型灌输错觉反而伤性能。我实际做 HER 项目时最直观的触动是第一次看到机械臂在稀疏奖励下从“完全没希望”到突然出现向目标靠近的雏形那个临界点往往来得比想象中快。所以我现在每做一个新的 RL 任务都会先问自己一句如果这个行为失败了它实际完成了哪个有价值的目标能回答清楚这个问题的环境无论奖励多稀疏都值得先试试 HER。等这条路跑通了再往离线学习、分层决策甚至自动目标生成方向延伸都是顺水推舟的事。
返回列表