ARTICLE DETAIL

资讯详情

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

强化学习稀疏奖励难题:HER回溯经验回放原理与代码实践

强化学习稀疏奖励难题:HER回溯经验回放原理与代码实践 我最早接触 hindsight 这个词是在 RL 的论文标题里Hindsight Experience Replay中文一般翻译成“回溯经验回放”。当时第一反应是——这不就是“事后诸葛亮”吗但读进去之后发现这个“事后诸葛亮”恰恰是解决稀疏奖励问题最朴素也最有效的思路之一。后来在机器人操作任务里用它训练抓取、推块策略确实是肉眼可见地比原始 DDPG 稳得多。这篇文章不打算从头科普强化学习而是从“为什么 hindsight 有用”讲起把 HER 的设计思路、采样策略、代码实现、复现踩坑一次说清。如果你已经在跑 RL 实验被稀疏奖励虐得想摔键盘或者在研究如何让 agent 从失败中学到东西这篇应该正好是你需要的。1. 为什么 hindsight 是“事后诸葛亮”式学习1.1 稀疏奖励问题到底难在哪先描述一下痛点。假设你在训练一个机械臂完成“把方块推到目标位置”的任务状态是机械臂关节角度和方块位置动作是关节力矩奖励函数是如果方块和目标位置的距离小于某个阈值奖励为 1否则为 0。这个设定在真实机器人操作里非常常见因为很多任务就是“完成或没完成”的二值反馈没法像玩游戏那样每一步都有分数。问题随之而来如果策略一开始是随机的绝大多数 episode 里方块从头到尾都没碰到目标位置那么整个 episode 的回报就是 0。DDPG、PPO 这类算法更新时看不到任何梯度方向因为“所有动作都同样糟糕”。结果就是训练了几十万步策略依然在原地转圈。这不是网络容量不够也不是学习率没调好而是信号本身太稀疏agent 根本没机会学到“哪个动作让方块更接近目标”。学术界和工业界围绕这个问题做了很多尝试课程学习、奖励塑形、逆强化学习、分层强化学习。这些方法确实有效但要么需要对任务做额外设计要么需要一份专家示范或额外的人类先验。HER 走的是另一条路既然信号稀疏那我就把失败的数据“改写”成看起来成功的数据让 agent 从自己的失败中挤出学习信号。1.2 核心套路重新标记目标HER 的想法直白得惊人。原本这条 episode 的目标是 g但最终没有达成。可是 agent 在轨迹中某个时刻 t 实际上到过某个状态 s_t如果把这个 s_t 当作目标那么这个 episode 里在 t 之后就变成了“成功”的轨迹。于是我们可以把这一条失败的轨迹复制一份把目标改成 s_t重新算一遍奖励放进回放缓冲区。这样做的效果很奇妙原本整个 episode 都没有正奖励现在后半段全是正奖励而且“动作导致目标达成”这件事是真的发生了只是发生在一个替代目标上。agent 就从中学会了这种动作模式可以导向那个状态而那个状态是从当前状态可以到达的。用一句大白话来说现实目标是 A没做到但你在某个瞬间达到了 B。那就先假装目标是 B让 agent 学会“如何到 B”。等它学会了到各种易于到达的 B它也就慢慢逼近了最终目标 A。这就是 hindsight 的核心哲学不要只盯着你没做到的先利用你已经做到的那些。这个思想还有一个更直观的类比你想追一个高冷的人从没成功过但你每次约人出来、聊天、一起逛街这些“失败”其实让你更懂怎么和异性相处。下次遇到相似的人你成功的机会就大了。强化学习里的 HER 就是干这个的。1.3 为什么它能解决梯度消失问题单纯把目标改掉看起来只是换了标签为什么就能解决稀疏奖励下的梯度消失关键在于HER 并没有改变真实任务的难度它改变的是数据分布。原始回放缓冲区里100% 的样本都是零奖励而在 HER 构造出的样本里相当大比例的 transition 都带有正奖励因为替代目标是沿着轨迹自动选取的轨迹自然要向那个目标靠近。一旦缓冲区里同时混合了“真实目标下的失败样本”和“替代目标下的成功样本”价值函数就能学习到一个相对平滑的 Q 曲面状态-动作对是否在通往目标的有用路径上不再是一个全零的谜。策略梯度方向也变成了既有 push 的动作也有 pull 的动作策略网络才有机会迭代。这个逻辑可以用课程学习来理解但 HER 不需要显式定义课程。目标是从探索轨迹里自动生成的探索得越多高质量的目标候选就越多课程自然形成。而且越往后越来越多的轨迹能够接近真实目标重标记的目标也就越来越接近真实分布agent 的能力是循序渐进提升的不是一下扔到最难的最终目标上。2. 技术细节采样策略、超参数与算法搭配2.1 四种目标采样策略选错就事倍功半HER 论文里定义了四种从 episode 中选取“替代目标”的策略。很多复现失败不是因为算法写得不对而是这里选错了策略。final只取 episode 最后一个状态的观测作为替代目标。实现最简单也最常用因为最后一个状态通常能反映 agent 这次探索的“最终成果”。缺点是如果 episode 很长、轨迹很丰富只用最后一步浪费了不少中间信息。random从整条轨迹里随机取 k 个状态作为替代目标。信息利用更充分但随机采样可能选到一些没什么代表性的状态导致噪声偏大。future从当前时间步之后的状态里随机选 k 个作为替代目标。这个策略有一个特性在 t 时刻看到未来某状态作为目标意味着从 t 到那个未来状态的这一段轨迹天然就是“达成目标”的正例时序因果完全一致学习信号最干净。episode和 future 类似但只从当前时间步之后且只在同一个 episode 中选择。区别在于它不额外引入其他 trajectory 的信息纯粹拆解当前轨迹。实际工程中final 和 future 是最常用的。final 省事适合拿来当做 baselinefuture 效果好适合真正追求性能的小项目。random 在目标空间维数低的时候可以用但维数一高随机选的目标可能距离当前状态太近或太远Q 值估计会不稳定。episode 策略更特殊通常只在特定任务里能发挥优势。这么多策略本质上是要回答一个问题替代目标应该从哪里来选得离真实目标太近学习的都是简单情况学不到真实任务的精髓选得太远agent 又很难在经验里找到达成路径。采样策略就是在简单性和信息量之间找平衡。2.2 超参数 k 和奖励函数设计HER 里有一个关键超参数 k从轨迹中采多少条替代目标轨迹。论文里默认 k 4也就是每个 episode 额外生成 4 条重标记数据加上原始数据共 5 条。k 太小buffer 里成功样本占比低效果不明显k 太大buffer 膨胀真实目标下的数据被稀释反而让 agent 偏离原任务。我在实际跑 FetchReach 时做过一组小实验k 1 时训练曲线震荡很明显最终成功率约 60%k 4 时逼近 90% 以上k 16 时成功率反而降到 80% 左右而且训练时间翻倍。原因很好理解替代目标过多之后agent 过度专注于“去够那些容易够到的替代目标”真实目标反而成了少数派。奖励函数方面HER 一般用二值奖励就够了不要自己在上面叠加距离惩罚。原因在于重标记后的数据需要“看起来”和真实数据同分布如果你给“靠近真实目标”额外加分、给“靠近替代目标”不加分那么替代样本的奖励分布会和真实样本不一致价值函数会被带偏。保持奖励函数的目标相关性一致是 HER 训练不跑偏的前提。如果你实在想加连续奖励我的建议是真实目标和替代目标共用同一个距离度量函数并且保证奖励是目标状态距离的单调函数这样才能让重标记样本的奖励分布保持一致性。2.3 算法搭配HER 只适合 off-policy这一点必须重点提醒HER 是给 off-policy 算法用的典型搭配是 DDPG、SAC、TD3、DQN 这类能反复使用历史数据的算法。on-policy 算法如 PPO、TRPO理论上不能用 HER因为重标记后的数据来自不同的行为策略不满足 on-policy 的采样要求。为什么 off-policy 才行HER 本质上是往 replay buffer 里塞了一条“原轨迹被修改过的副本”。on-policy 算法更新时需要的是当前策略下的样本这条副本虽然是同一条轨迹改的但目标和奖励都变了等价于另一条完全不同的轨迹不再属于当前策略的分布。强行使用会出现严重的策略偏差甚至发散了。实操里最常见的组合是DDPG HER论文标配也是我在机械臂任务上验证最多的组合简单可靠但对超参数敏感critic 学习率要小心。SAC HER稳定性和 sample efficiency 更好适合复杂任务代价是训练耗时增加调参难度略高。TD3 HER如果动作维度较高TD3 的 target policy smoothing 能抑制 Q 值过估计和 HER 叠加效果通常优于 DDPG。我个人更推荐在真实机器人任务里用 TD3 或 SAC 作为底层算法因为它们对随机种子的敏感度更低不容易出现“换个种子从 90 分掉到 40 分”的情况。3. 从零实现 HER核心代码拆解3.1 整体数据流设计HER 的实现不复杂但对数据结构的要求很高。核心思路是在 replay buffer 里存的不仅仅是一条 transition而是一整条 episode 的观测序列这样才能在做目标重标记时拿到任意时刻的观测。常用做法是这样的采样一个 episode得到完整的观测序列 obs_seq、动作序列 action_seq、真实目标 goal。为这条 episode 生成 n 个替代目标比如用 future 策略采 k 个。对真实目标和每个替代目标分别算出每个时间步的奖励然后把“状态-动作-目标-奖励-下一状态”的 transition 拆开存进 buffer。于是一个长度为 T 的 episode在 k 4 时会生成 (k 1) × T 条 transition。这也解释了为什么 HER 的 sample efficiency 高——一条探索轨迹被榨出了五份学习样本。这里有三个容易出错的点观测 obs 和目标 goal 必须分开存。你在做目标重标记时只能替换 goal 部分不能把 obs 里的目标信息也换了否则状态表示和目标表示互相污染训出来的策略会乱套。下一观测 next_obs 也要跟着替换目标。transition 里的 next_obs 是环境返回的下一时刻状态它不包含“目标”信息所以重标记时只改 reward 和 goal 字段。奖励必须重新计算。很多人会忘记这一点直接复用原始 reward结果重标记后的数据仍然全是 0白忙活。3.2 核心代码实现这里给出一个简化版但完整可用的 HER 采样核心代码用 PyTorch 风格实现重点是理解目标重标记的流程。import numpy as np from collections import deque class EpisodeBuffer: 缓存单条 episode 的所有信息用于 HER 重标记 def __init__(self, capacity1000): self.obs [] self.actions [] self.rewards [] self.next_obs [] self.dones [] self.goal None def store_transition(self, obs, action, reward, next_obs, done): self.obs.append(obs) self.actions.append(action) self.rewards.append(reward) self.next_obs.append(next_obs) self.dones.append(done) def set_goal(self, goal): self.goal goal def clear(self): self.obs.clear() self.actions.clear() self.rewards.clear() self.next_obs.clear() self.dones.clear() self.goal None def her_sample(episode: EpisodeBuffer, k4, strategyfuture): 从一条 episode 中生成重标记后的 transition 列表。 strategy: final / random / future / episode transitions [] T len(episode.obs) original_goal episode.goal # 原始目标下的 transition 也要保留 for t in range(T): transitions.append({ obs: episode.obs[t], action: episode.actions[t], reward: episode.rewards[t], next_obs: episode.next_obs[t], goal: original_goal, done: episode.dones[t], }) if strategy final: goal_candidates [episode.next_obs[-1]] elif strategy in (future, episode): # 对每个时间步从未来状态中采样 k 个目标 # 这里简化从整条轨迹的未来状态集合里随机选 k 个 # 实际使用时逐时间步采样更精细 future_indices np.arange(T) selected np.random.choice(future_indices, sizek, replaceFalse) goal_candidates [episode.next_obs[i] for i in selected] elif strategy random: indices np.random.choice(T, sizek, replaceFalse) goal_candidates [episode.next_obs[i] for i in indices] # 对每个替代目标重新计算奖励 for goal in goal_candidates: for t in range(T): # 注意这里 reward 要换成基于新目标的奖励 new_reward compute_reward(episode.next_obs[t], goal) transitions.append({ obs: episode.obs[t], action: episode.actions[t], reward: new_reward, next_obs: episode.next_obs[t], goal: goal, done: float(new_reward 0), # 二值奖励时这样算 done }) return transitions这段代码里有一个关键细节done的计算。在稀疏二值奖励场景下当替代目标被达成时这个 transition 实际上就是终态需要把 done 置为 1否则 value function 会错误地把“成功后再推一步”当成普通 transition导致 Q 值低估。再单独解释一下 compute_reward 的实现。在机器人操作任务里通常有一个距离阈值 d_threshold奖励函数就是def compute_reward(state, goal, thresh0.05): # 假设 state 是 agent 当前实际位置goal 是目标位置 dist np.linalg.norm(np.array(state) - np.array(goal)) return float(dist thresh)注意阈值不能太小。如果设置得太严格比如 0.01那替代目标下很多轨迹仍然全部是 0HER 又回到了信号稀疏的问题。0.05 是机器人操作任务里比较通用的值实际任务需要根据状态尺度调整。3.3 网络与训练配置参考以 FetchReach-v2 为例状态维度是 10 维左右动作是 4 维连续控制目标是 3 维位置。这种简单任务不需要太大网络。Actor两层 MLP隐藏层 256ReLU 激活输出层 tanh。Critic两层 MLP隐藏层 256但输入要把 obs 和 goal 拼接起来这在实现里很容易被忽略。很多人的 critic 只输入了 obs导致 agent 完全不知道目标是什么自然是瞎学。学习率Actor 1e-3Critic 1e-3这个组合在多数任务上比较稳妥。如果发现 Q 值发散优先把 Critic 学习率降到 1e-4。目标网络更新soft updateτ 0.05 在 HER 里比 DDPG 默认的 0.005 要好。回放缓冲区大小HER 的数据量会暴涨一个 episode 产生好几倍 transition所以 buffer 至少要设 100 万起步。episode 长度50 步。这里有个有趣的现象HER 对 episode 长度不敏感长短都能学但过长会稀释替代目标密度过短又来不及完成任务50 步算是一个普适值。实际训练时我会在每 10 个 episode 结束后跑一次评估用确定性策略 rollout 20 次统计成功率。建议用成功率而不是平均回报来评估因为在稀疏奖励下平均回报很可能全是 0看不出策略到底有没有进步。4. 实操中踩过的坑与排查方法4.1 回报全零的常见原因HER 最容易出现的现象就是训练了很久平均回报纹丝不动看起来像是没在学习。这背后通常有一个隐蔽的原因目标重标记后新 reward 没有正确写入 transition。我排查过很多次发现代码里最常见的错误是从 episode buffer 里取原始 reward 直接用了没有用新的替代目标重算。结果 HER 的大部分样本依然全是 0和普通 DDPG 没有任何区别。排查方法是打印每个 mini-batch 里正奖励的比例。如果 k 4 且策略为 future正例比例应该在 20% 以上如果只有百分之几说明重标记逻辑有问题。还有一个更隐蔽的错误obs 和 next_obs 里的目标信息处理不一致。很多环境比如 Gym 的 Fetch 系列返回的 obs 字典里同时包含 observation 和 achieved_goal 两个字段。有的实现会把 achieved_goal 拼接进 observation 作为输入这在普通 RL 里没毛病但 HER 重标记时obs 里就残留了和 goal 相关的旧信息导致同一状态在不同目标下输入不一致策略完全学不到稳定映射。解决办法是把 observation 和 achieved_goal或 goal解耦网络输入永远只接“当前状态 目标”的拼接不要让状态里内嵌目标信息。如果环境本身返回的 obs 已经包含目标那就做一个特征选择把目标维度剔除。4.2 采样策略选错导致训练崩溃future 和 random 策略在实现上就差一个采样区间但效果差距很大。我当时在 FetchPush 任务上对比过future 策略 20 万步成功率接近 80%random 策略 30 万步才到 50%final 策略则一直卡在 30% 左右。原因前面也提到过future 策略保证了时间上的因果性。以 t 时刻的状态为起点用未来某个状态作为目标从 t 到这个未来状态之间的轨迹是真实发生的也就是说“在这个状态通过这些动作确实达到了那个目标”。这个因果关系是可信的价值函数学起来很顺。而 random 策略从整条轨迹里任意选目标可能选到 t 时刻之前已经到过的状态那种情况下从 t 到目标状态的轨迹其实并不存在因果链断了Q 值估计自然有偏。所以如果你的任务里轨迹不是很长建议优先用 future不要图省事用 random。4.3 超参数和网络结构导致的不收敛k 值我们已经讨论过了再说一个更隐蔽的Critic 输入对 HER 是否收敛影响极大。很多 DDPG 实现里 Critic 接受的输入是(obs, action)如果沿用这个结构HER 状态里的obs没有拼接goalCritic 根本无法区分“推方块到 red 目标”和“推方块到 green 目标”训练不收敛是必然的。正确做法是 Critic 输入(obs, action, goal)或者把obs和goal在特征维度上拼接后作为输入。最好在实现里把拼接过程写清楚不要偷偷在某个地方把 goal 丢掉。另外注意 DDPG 的 target policy smoothing 参数。TD3 引入的噪声在 HER 里同样重要因为重标记样本的目标多样Q value 的方差天然偏大如果没有 smooth 机制critic 很容易过估计导致策略在探索时出现大量过度激进的 action。4.4 训练成功的标志与评估建议什么时候算训练成功我的标准很简单连续 5 次评估成功率都超过 90%且曲线不再明显上升。这时候可以停下来做模型导出但别急着部署到真实机器人因为仿真里的成功标准和真机有差距特别是方块位置误差仿真里 0.05 的阈值在真机上可能只有 0.03 的重复精度直接上手一定会翻车。我习惯的流程是先用仿真环境训练保存成功率最高的模型。在仿真里加随机扰动初始位置、物理参数、目标位置验证模型鲁棒性。再做 sim-to-real 迁移用 domain randomization 或者直接在真机上做一小批微调。HER 的好处是微调阶段能继续从失败中学习这比一次性部署稳妥得多。如果只是做算法研究跑到仿真效果不错就足够了。但如果你想把这个技术用在真实项目里一定要把评估环节打造成“仿真可行 真机可以快速验证”的组合否则实验室里再好看的成功率到了现场都可能变成空谈。5. 我实际跑过的那些实验与体会说点个人经验。我在 FetchSlide 上做过 HER 和 SAC 的融合实验一个 500 万步的预算里跑了约 30 组不同配置最大的体会是HER 不是一个“开箱即用”的算法它对数据管线的整洁度要求极高。只要你把 goal 字段和 achieved_goal 字段混用一个向量或者忘记在重标记后重算 reward好的结果立刻变成糟糕的结果。这不是网络结构的问题也不是调参能救回来的而是逻辑硬伤。所以我建议所有尝试 HER 的人先把数据管线的代码写好、写稳、写清晰再去想算法优化。另一个让我意外的地方是HER 对随机种子的敏感度比想象中低。同样一套代码跑 5 个不同种子成功率方差在 5% 左右这比 PPO 在连续控制任务上的方差小得多。原因也好理解重标记后的正样本比例提升削弱了随机探索带来的偶然性训练过程自然更稳。这个特性让 HER 很适合成为稀疏奖励任务中各种 trick 的“底座”比如在上面叠加 stage-wise 训练、添加辅助任务甚至做 multi-goal 的课程学习都能取得比原始方法更好的成绩。如果你现在正准备在稀疏奖励的控制任务上用 RL我建议你好好把 HER 的每个细节理清楚不要急着换更复杂的算法。先把目标重标记、采样策略、奖励重算这三件事做对你会发现很多“学不动”的问题自然就消失了。这套思路放到真实任务里也一样人不能只盯着最终目标还得学会从已经做到的事情里复盘、提炼、转化成自己的经验。算法如此工程如此做项目也如此。
返回列表