ARTICLE DETAIL

资讯详情

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

Hindsight经验回放:用失败轨迹破解稀疏奖励难题

Hindsight经验回放:用失败轨迹破解稀疏奖励难题 你盯着训练曲线看了半天loss 一点没降agent 还在原地转圈。这种情况我太熟了。之前我做机械臂抓取仿真环境反馈特别稀疏抓到了才算 reward没抓到一律 0。模型训了半天完全不动最后帮上大忙的是一个叫 Hindsight 的项目思路也就是 Hindsight Experience Replay通常简写成 HER。这篇就聊一聊 Hindsight 这个项目背后的设计逻辑它到底是怎么把“失败经验”变成训练信号的哪些细节直接影响效果以及我在复现过程中踩过的坑。适合跑强化学习遇到稀疏奖励、做机器人目标条件控制或者刚开始接触 HER 的人参考。Hindsight 直译过来是“后见之明”生活中常指“事后聪明”在强化学习里它变成了一个非常反直觉的训练技巧既然这次任务失败了那就把失败这段轨迹“重新解释”成另一个目标下的成功。这个想法第一次看很奇怪但你把它的数学逻辑拆开就会发现它解决的正是强化学习里最让人头疼的奖励稀疏问题。这篇文章不会堆公式我会尽量把每个关键点都讲清楚包括为什么有效、代码怎么落地、以及常规文档里不会写的那种“看起来没效果”的排查思路。1. 内容整体设计与思路拆解1.1 “事后诸葛”如何变成训练信号HER 的核心思想可以用一句话概括如果 agent 没有到达原始目标那就把这段轨迹中某个已经到达的状态重新标记成一个新的目标并在这个新目标下重新给奖励。举个例子机械臂想把方块推到位置 A结果推到了位置 B。从原始目标看这是一条失败轨迹奖励全程是 -1。但如果我们把“目标”改成位置 B那么这条轨迹就变成了一个成功示范机械臂确实把方块推到了 B 点最后一步的奖励可以变成 0。这个过程不是简单的安慰自己而是把一条原本毫无学习信号的轨迹拆成了多条带有正反馈的子轨迹。HER 在训练时会对同一条失败轨迹采样多个“事后目标”每采样一个目标就要重算一次奖励然后把这份新经验放进 replay buffer 里。这样 agent 学到的不是“怎么推到 A 点失败”而是“学会如何到达 B 点”。随着目标空间里各种状态都被作为事后目标学习过agent 就慢慢建立起一个通用的、以目标为条件的控制策略最终也能泛化到原始目标 A。我之前刚接触这个思路时总觉得有点“作弊”明明没完成任务怎么能把失败当成功呢但从数学角度看这不是在歪曲事实而是在做一种非常漂亮的数据重标注。强化学习本身不关心轨迹是从哪里来的它只关心在同一目标下哪些状态动作序列能获得更高回报。HER 只是把“多目标学习”这个维度加入到了经验回放里让 agent 对目标空间里的任意状态都学习“如何到达”而不是只盯着一个固定目标。1.2 它解决的是哪一种“难”目标条件强化学习里最典型的问题是奖励稀疏。什么叫稀疏就是绝大多数时间步的 reward 都是 0 或者同一个负常数只有在任务完成的那一刻才有一个明显信号。比如机械臂推方块目标位置的容差只有几厘米你让随机初始化的策略去推几十万个 episode 里可能一次成功都碰不到。这种情况下常规的 DDPG、TD3、SAC 几乎学不到任何东西因为所有采样到的数据回报都一样梯度方向不明确。HER 不改变环境也不改变奖励函数它只修改经验数据。原本一条 episode 只有一个目标回报稀疏HER 把一条 episode 里的很多状态都当作潜在目标于是每次训练都能采到大量“目标已经达成”的正样本。这是一个典型的数据增强思路不需要任务设计者手工设计课程也不需要给中间过程加额外奖励而是从失败经验里自动挖掘出可学习的信号。需要强调的是HER 解决的“难”是“目标明确但奖励稀疏”而不是“完全没有任何目标信息”。如果任务根本没有明确的 achieved goal 可以提取HER 就不太好用。这个以后面章节再展开。总之它特别适合那种状态空间里天然包含“我想去哪、我现在在哪”这类信息的控制任务。1.3 和普通经验回放的本质差别很多人觉得 HER 无非是在 replay buffer 里多存了几条数据其实差别很大。普通经验回放里一条 transition 被存下来它的目标、奖励、done 都是按采样时的情况固定好的。训练时模型反复学习这部分数据只会知道“在某个状态采取某个动作对原始目标来说是好是坏”。如果原始目标大多失败buffer 里就堆满了负样本学习自然慢。HER 的核心操作发生在采样阶段从一条经验里不仅要保留原始目标下的 transition还要把这条 transition 的目标字段替换成同一 episode 未来某个时刻的 achieved goal然后按新目标重新计算 reward 和 done。因为新目标是从“将来实际到达的状态”中选的所以这条新经验至少不是“绝对失败的”它能给策略提供更密集的正反馈。下面这个表比较直观对比项普通经验回放HER 经验回放目标固定为采样时的 desired goal可以替换成轨迹后续状态奖励原始奖励按新目标重新计算正样本来源极少取决于原始成功率丰富每条轨迹都能生成多个正样本学习信号稀疏、慢密集、快适用条件通用 off-policy 算法需要目标可提取、奖励可达HER 也因此对 buffer 的数据组织方式有要求你必须把每条 episode 的完整序列存下来而不是只存一个个独立的 transition。因为只有整条轨迹都还在你才能从“这条轨迹当前步之后的任意状态”里挑出新目标。如果你依然用那种随采随丢的平铺 bufferHER 就很难落地。2. 核心细节解析与实操要点2.1 虚拟目标从哪里来四种采样策略HER 原论文里比较了四种从轨迹中选取虚拟目标的方法分别是 final、future、episode 和 random。final 的做法最简单直接拿 episode 最后一个状态作为整条轨迹所有 transition 的虚拟目标future 是在每个 transition 之后的状态里随机挑 k 个作为虚拟目标episode 是在同一条 episode 的任意位置选 k 个random 是从整个 replay buffer 里随便选 k 个。实际效果上future 是论文中默认采用、也是我复现时表现最稳的策略。原因是它利用了时间上的因果结构对于轨迹中的某个 transition它后面真实会到达的状态大概率是这个 transition 之后“顺理成章”出现的结果。你给这条 transition 设定“未来某个状态”作为目标是在告诉策略虽然你现在还没到那里但接下来确实有路可走。这个目标不是凭空想象的而是这条轨迹自己走出来的所以数据分布更符合策略的实际动态。final 的问题是只在 episode 结尾提供一个目标对于长轨迹来说前面大部分 transition 距离 final 状态太远奖励依然稀疏。random 的问题更明显从 buffer 里随机抽一个其他 episode 的状态当作当前目标很可能和目标空间毫无关系学起来反而更慢。episode 会在同一轨迹中抽“历史状态”作为目标这个目标在当前 transition 之前已经到过了时间上有点“往回看”效果一般不如 future。你可以把 future 理解为从“接下来会去哪”这个信息里偷师。2.2 目标怎么编码奖励怎么重算实现 HER 前一定要把两个概念区分清楚desired_goal 是外部给定的当前目标achieved_goal 是智能体在每一步实际到达的状态。HER 只允许替换 desired_goal而 achieved_goal 必须来自环境反馈。很多环境里 observation 是一个字典包含观测、desired_goal、achieved_goal 三部分也有环境把 achieved_goal 直接写进观测向量里。不管哪种你需要保证当模型输入 goal 时传入的是被替换后的 desired_goal而不是 obs 里的固定字段。奖励重算是比较容易出错的地方。如果你的环境原本用的是二值稀疏奖励到达目标附近为 0其他为 -1那么重算的时候也要用同一个公式只不过把 desired_goal 替换成新的虚拟目标。这里最容易犯的错是采样出了新目标但忘记重新计算奖励直接用了原始奖励。这样 model 看到的 transition 是“目标 A 没成功奖励 -1但标记的目标却是 B”数据就完全错乱了。调试时我建议把重算奖励的函数单独抽出来比如import numpy as np def compute_reward(achieved_goal, desired_goal, threshold0.05): dist np.linalg.norm(achieved_goal - desired_goal) return 0.0 if dist threshold else -1.0在训练循环里每条真实 transition 存下来时除了 obs、action、next_obs、done还一定要额外记录 achieved_goal 和 desired_goal。采样阶段要的是这两个字段而不是 reward 字段。2.3 为什么必须是 off-policy 算法HER 对算法结构是有要求的它天然绑定 off-policy 强化学习。原因其实很简单HER 把一条经验重新解释成另一个目标下的经验这条经验对应的实际行为是由旧的、生成这条轨迹时的策略产生的。如果算法是 on-policy 的比如 PPO那么每次更新完策略后旧数据就不能继续使用了因为旧数据代表的分布和当前策略不再匹配。强行用替换目标后的旧数据会产生严重偏差训练反而更不稳定。而 DDPG、TD3、SAC 这类 off-policy 算法本来就是靠 replay buffer 打破样本相关性的它们允许用旧策略产生的数据更新当前策略只要数据足够多样偏差是可控的。HER 正好在这个框架下做目标替换等于是给本来就使用的 replay buffer 又加了一层更强的数据增强。我自己的项目里主用 TD3 和 SAC两者都能平稳吃下 HER 带来的额外样本。如果你非要在 PPO 上试 HER也不是完全不能但往往需要额外纠正目标分布偏移工程复杂度会高不少。2.4 什么样的任务适合 HERHER 不是银弹。我把它列进“先看适不适合再用”的名单里。首先任务必须是一个多目标问题而且目标可以从状态中提取出来。比如机器人推箱子、导航到某个位置、控制小游戏里的角色移动这类任务的 achieved_goal 是很明确的坐标或状态向量。如果目标本身是抽象概念比如“让对话更自然”就没法轻易从轨迹里提取 achieved_goalHER 就很难套用。其次奖励函数主要应该由“是否到达目标”决定。如果奖励还包含大量过程约束比如不允许撞到障碍、每步都要付出代价那么仅靠替换目标会忽略这些约束。你可以强行用但虚拟目标样本可能会在“过程奖励”上给策略传递误导信号。最后目标空间不宜太大且过于离散。假设目标空间是一张高分辨率图像你从轨迹里采一个目标很难形成有效泛化反而容易让网络容量全花在记忆不同目标上了。HER 最适合的是那种“状态空间连续、目标定义清晰、奖励二值或接近二值”的控制任务。3. 实操过程与核心环节实现3.1 做一个最小的验证环境二维点到目标为了不引入 MuJoCo 这种重型仿真我先用一个小例子验证 HER 流程。环境逻辑很简单一个点在二维平面上移动每次 episode 开始时点坐标和目标坐标都随机生成动作是坐标的增量回报为到达目标 0 分、否则 -1 分。这个环境如果不加 HER随机策略基本学不出来非常适合拿来观察 HER 的作用。我把环境写得短一点重点放在状态表示上。这里的 pos 就是 achieved_goalgoal 就是 desired_goal每次 step 都会返回新的 pos。便于演示我只保留核心逻辑import numpy as np class PointEnv: def reset(self): self.pos np.random.uniform(-1, 1, size2) self.goal np.random.uniform(-1, 1, size2) return self.pos.copy(), self.goal.copy() def step(self, action): self.pos np.clip(self.pos action, -1, 1) dist np.linalg.norm(self.pos - self.goal) reward 0.0 if dist 0.3 else -1.0 done dist 0.3 return self.pos.copy(), reward, done, {}实际跑的时候你会发现即使目标空间很小随机策略能达到目标的概率也极低。因为初始位置和目标位置都是随机采样连续空间里命中一个半径 0.3 的圆概率本身就低。不借助 HER策略完全拿不到正样本所有 transition 的 reward 都是 -1梯度方向被噪声淹没。3.2 带虚拟目标替换的 replay bufferHER 的落地重点不在算法主体而在 buffer 的设计。你需要存储整个 episode 的结构而不是只把 transition 平铺进一个环形队列。我会在 buffer 里维护一个 episode 列表每个 episode 存一组按时间排序的 dict每个 dict 包含 obs、action、desired_goal、achieved_goal、next_obs、done 等字段。采样时对每个选中的 transition先保留原始目标样本再从该 transition 之后的所有 achieved_goal 里随机抽 k 个作为虚拟目标。每个虚拟目标都会重算一次奖励并和原 transition 的 obs、action、next_obs 组成一条新样本。下面是一个很接近论文风格的采样逻辑import random from collections import deque class HindsightReplayBuffer: def __init__(self, capacity, k_future4, threshold0.3): self.episodes deque(maxlencapacity) self.k_future k_future self.threshold threshold def add_episode(self, episode): # episode: list of dicts, 至少包含 obs, act, desired_goal, # achieved_goal, next_obs, next_achieved_goal, done self.episodes.append(episode) def compute_reward(self, achieved_goal, desired_goal): dist np.linalg.norm(achieved_goal - desired_goal) return 0.0 if dist self.threshold else -1.0 def sample(self, batch_size): samples [] for _ in range(batch_size): ep random.choice(self.episodes) t random.randrange(len(ep)) trans ep[t] # 原始样本目标不变、奖励不变 samples.append(( trans[obs], trans[act], trans[desired_goal], trans[reward], trans[next_obs], trans[done] )) # HER 虚拟目标从当前 transition 之后的状态里采样 future_goals [e[achieved_goal] for e in ep[t 1:]] if future_goals: new_goals random.sample( future_goals, min(self.k_future, len(future_goals)) ) for new_goal in new_goals: new_reward self.compute_reward( trans[next_achieved_goal], new_goal ) samples.append(( trans[obs], trans[act], new_goal, new_reward, trans[next_obs], False )) return samples注意我把 new_reward 用trans[next_achieved_goal]和新目标来计算而不是用新目标自己和自己算。这很重要当前 transition 的下一步状态并没有到达新目标所以这条样本的 reward 依然可能是 -1。但同样一段轨迹中后续那些更靠近新目标的 transition 会提供正奖励相当于整条轨迹为“到达新目标”这个子任务提供了多级信号。另外虚拟目标样本的 done 被我强制设成了 False。原因很简单当前 transition 的 next_obs 大概率还没到新目标不应该提前终止。如果你选的新目标恰好就是当前 transition 的 next_achieved_goal那理论上可以设 doneTrue但没必要为了这种边界情况把逻辑搞复杂让强化学习自己去判断即可。3.3 训练主流程buffer 准备好以后训练主流程就相对常规了。你仍然可以选择 DDPG、TD3、SAC 中的任意一种作为基础算法这里我给出一个和框架无关的伪代码级流程方便你对照自己的代码结构去改for episode in range(total_episodes): obs, goal env.reset() episode_data [] achieved_goal obs # 当前坐标即 achieved_goal for step in range(max_steps): action actor(obs, goal) exploration_noise next_obs, reward, done, info env.step(action) next_achieved_goal next_obs episode_data.append({ obs: obs, act: action, desired_goal: goal, achieved_goal: achieved_goal, next_obs: next_obs, next_achieved_goal: next_achieved_goal, reward: reward, done: done, }) obs next_obs achieved_goal next_achieved_goal if done: break her_buffer.add_episode(episode_data) if len(her_buffer) warmup_steps: for _ in range(train_steps): batch her_buffer.sample(batch_size) update_actor_critic(batch) # 用你选定的 off-policy 算法更新有一个细节容易忽略HER 样本是采样时生成的所以它不会直接放进 buffer 里反复重放而是每次训练从 buffer 里的原始 episode 上临时生成。这个设计有好处因为虚拟目标可以从最新达到的状态集合中动态生成避免旧 sample 在 buffer 里被重复存储太多次导致经验过度重复。当然也有实现会把 HER 样本直接写回 buffer这样可以增大采样量但内存开销也会增加。3.4 超参数怎么定k 值、buffer 大小与目标替换概率HER 原论文默认设置是 k4也就是每个 transition 额外生成 4 个虚拟目标样本。k 太小正样本不够密集k 太大同一个 transition 会被反复使用训练更新时可能让某些目标过拟合而且每个 batch 的计算量也会增大。我在多个任务上试下来k4 确实是个稳健的起点任务复杂度高一点可以试 k8但很少需要超过 8。buffer 大小也很关键。因为 HER 依赖整个 episode 存档你的 buffer 空间占用量比普通经验回放大得多尤其是在长 episode 环境下。我建议直接按 episode 数量来控制容量而不是按 transition 数量。比如保存最近 5000 条完整 episode每个 episode 平均 50 步就等于 25 万条 transition。这个量级够大多数小型控制任务用内存也基本可控。“目标替换概率”也是一个可以调的旋钮。有些实现里不是对每个 transition 都额外生成 HER 样本而是有一定概率触发这样做的目的是保留一部分原始目标下的经验防止 agent 完全忘了原始任务。我通常的做法是每 batch 里按 0.8 的概率加入 HER 样本、剩余 0.2 保留原始样本或者更简单一点采样时每条 transition 都生成 k 个 HER 样本但原始样本也一定保留在 batch 里。这样既保证正反馈密度又不丢失原始目标信息。4. 常见问题与排查技巧实录4.1 我的 HER 没效果常见原因排查HER 最经典的“看起来没效果”场景是训练了几天成功率还是零。我遇到过几次最后查出来的原因都挺低级。第一个要查的是采样后有没有重算奖励。如果你只在目标字段上做了替换却没有用新目标重新调用 compute_reward那模型拿到的是“目标被篡改但奖励没跟上”的坏数据。我建议在训练刚开始时打印一个诊断统计采样出来的 batch 里 reward0 的样本占比是多少。如果 HER 生效这个比例会明显高于原始策略的成功率如果还是 0基本可以断定奖励重算环节出了问题。第二个要查的是目标字段和观测字段有没有冲突。有些封装好的环境会把 desired_goal 直接塞进 observation 的向量里。这时候如果你单独给 actor 传 goal但 obs 里也自带了原始 goal替换目标后双方就会打架。解决办法是把 observation 里的原始 goal 字段去掉或者在替换目标时同步更新 obs 向量里的对应维度。我建议干脆在环境接口层就统一observation 只包含与目标无关的状态goal 作为一个独立输入传进网络。第三个要查的是采样逻辑写错。比如把 future_goals 写成了ep[:t]相当于拿当前 transition 之前的状态当作未来目标时间顺序完全反了又比如没有保存整条 episode平铺 buffer 导致采样时拿不到未来状态。这些 bug 都让 HER 形同虚设。调试时可以写一个最小的单测喂一个简单的成功轨迹检查给定 transition 能生成哪些虚拟目标以及对应的 reward 是否正确。还有一个更隐蔽的问题done 标志。如果你的环境在原始任务失败但 episode 因步数限制结束时done 被设为 True那么当你替换目标并希望这条 transition 能继续学习时done 一定要重新判断。在 HER 采样里虚拟目标样本的 done 我通常直接置为 False除非 next_obs 真的到达了新目标。否则一个“提前终止”的假 done 会告诉模型这个状态是终止状态不需要继续往下走从而破坏整个回放序列的学习。4.2 哪些任务不适合 HERHER 也有明确的能力边界。第一种情况是目标无法从观测中提取比如一个抽象的目标标签“红球在绿球左边还是右边”而且这个标签不能从当前状态直接算出来只能由环境外部给出这时候你就没有 achieved_goal 用来替换HER 无从谈起。第二种情况是目标空间非常大并且没有平滑结构。拿图像目标来说目标是一整张画面状态空间维度可能高达数千从一个轨迹里采到的虚拟目标很难覆盖到另一个轨迹的相关区域策略学到的东西无法有效泛化。这时候即使强行用 HER收益也会被维数灾难吃掉。第三种情况是奖励函数包含大量过程约束。假设环境不仅有目标到达奖励还有“每走一步扣 0.1、碰到障碍扣 1 分”这样的惩罚HER 在替换目标后只重算了“是否到达”这一部分但过程惩罚仍然保留着。这就可能导致某些被替换成虚拟目标的样本在过程惩罚上非常差模型学到的策略可能会为了减少过程惩罚而绕远路反而偏离真实任务。如果你一定要在带约束环境里用 HER至少要把约束信息编码进奖励重算逻辑里而不是只替换目标坐标。4.3 实际操作中的几个提效技巧我自己的经验里有几个值得分享的小技巧。第一采样虚拟目标时尽量不要只选最远的未来状态也不要只选近的你可以混合着采样。future 策略默认是等概率抽所有后续状态但实际效果可以稍微调整对距离当前 transition 较近的未来状态多给一点权重因为这些目标更容易在短时间内达到学习信号更直接同时保留一小部分远期目标让策略学会长距离导航。第二可以给 achieved_goal 加一点观测噪声。如果环境是完美的仿真环境achieved_goal 是精确值那直接把精确状态当目标会让策略过度依赖“一步到位”的控制遇到真实传感器噪声就容易失灵。在采样虚拟目标时对 achieved_goal 加一点很小的高斯噪声再当作目标能在一定程度上提升策略的泛化能力。第三HER 最好和并行环境一起用。因为 HER 的效果依赖于 buffer 里覆盖足够多样的目标空间单个环境滚数据速度太慢目标覆盖会偏向最近采样的区域。开 8 到 16 个并行环境buffer 里的目标多样性会快速提升HER 的收益也会更明显。我之前在二维点的实验里8 并行比单环境收敛快了接近两倍。第四别把 HER 样本全部塞进同一个 batch。我建议 batch 里原始样本和 HER 样本混合比例大概在 1:4 到 1:8 之间这样模型既能看到真实目标任务又能从虚拟目标里获得密集反馈。如果你让 HER 样本占比太高模型可能会过度关注“怎么到达各种状态”反而忘了原始目标到底是什么。4.4 和课程学习的取舍解决稀疏奖励问题还有一条常见路线是 curriculum learning也就是课程学习从简单目标开始训练逐渐过渡到难目标。课程学习需要人工设计难度分布或者用自动课程算法去调整目标采样概率。HER 的优点是不需要这种设计它直接利用失败轨迹自己生成课程。比如一开始 agent 随机乱动它的轨迹很短能到达的状态离起点近这些近状态就成了简单目标随着策略变强轨迹变长能到达的远状态也越来越多HER 虚拟目标就自动变得更难。不过两者不是互斥的。我见过不少项目把 HER 和课程学习结合先用课程学习控制目标难度分布让 agent 不至于一上来就面对完全不可达的目标再用 HER 在每个难度区间里制造足够多的正样本。对于极端稀疏、目标非常远的任务这种组合确实比单独用 HER 更快。如果你的任务里随机策略几乎无法到达目标附近那么即使 HER 也未必能提供有效样本因为虚拟目标都是从轨迹实际到达过的状态里选的轨迹太短虚拟目标距离原始目标太远泛化困难。先用课程学习拉近目标HER 就能发挥威力。我在实际跑实验时有一个很深的体会HER 的代码实现并不复杂但它的拟合过程很“脆”只要数据流里有一个字段传递错了效果就完全消失。它不像某些网络结构那样错了还能凑合着收敛HER 错一点就真的什么都学不到。所以每一步 debug 都要围绕“目标、奖励、done”这三个字段反复检查。最后再分享一个小技巧每次加入 HER 后先别急着跑长实验先打印一批采样样本的 reward 分布和目标替换情况。如果看到虚拟目标样本里 reward0 的比例在几个 episode 后明显上升说明 HER 的数据通路是通的。这个习惯帮我排掉了至少一半的隐藏 bug。Hindsight 这个思路真正强大之处是它逼迫你去重新审视那些“失败”数据里隐藏的结构而不是简单把它们扔掉。
返回列表