
1. 从事后聪明到强化学习HER到底解决的是什么问题强化学习里头有一个特别让人头疼的场景叫做稀疏奖励。什么意思就是你让一个机械臂去抓取目标物体奖励设计得很简单粗暴抓到了就给你1没抓到就给0。看起来逻辑清晰但训练起来简直是噩梦——智能体在一望无际的0分海洋里瞎摸索完全不知道该往哪个方向改进梯度信号一塌糊涂网络权重压根更新不动。Hindsight这个项目标题对应的就是强化学习领域里那个大名鼎鼎的算法——Hindsight Experience Replay事后经验回放通常简称HER。我第一次听到这个算法的名字时还觉得挺有意思hindsight翻译过来是事后聪明刚好点出了它的核心思想既然没抓到目标那干脆换个角度——把这个失败的轨迹重新标定成一个成功到达了另一个目标的轨迹再让智能体从这个假成功里学到点真东西。这套思路最早来自OpenAI在2017年提出的一篇论文。说实话要想理解HER解决了什么得先想明白稀疏奖励为什么难搞。传统强化学习里策略网络靠奖励信号来调整参数如果奖励大多数时候都是0那网络计算出来的梯度也就接近0参数更新等于没更新训练效率低到令人发指。有人试过给中间状态加一些人工奖励比如离目标近了就给小分但这等于在做feature engineering每个新任务都得重新设计一套奖励函数工程量大且很脆弱。而HER的做法相当聪明它不修改奖励函数而是修改目标。你看训练过程中每一条轨迹其实都包含两个信息——一个是智能体实际采取的动作序列另一个是它最终到达什么样的状态。传统方法只把没抓到目标的轨迹当成失败样本丢掉HER却把这条失败轨迹重新拿来在回放的时候把原始目标替换成轨迹实际到达的那个状态然后告诉网络看虽然你没抓到原来那个目标但你触达到了这个位置如果把目标改成这里你的这段动作就是成功的策略。这样一来原本全是0的奖励信号里就硬生生挤出了大量1的样本梯度总算有东西可以学了。这个方法不需要改环境、不需要人工设计中间奖励、不需要额外采集数据只需要在经验回放环节做个目标重标注就能让稀疏奖励任务的收敛速度大幅提升。这个算法对任何做机器人控制、机械臂抓取、导航规划、甚至游戏AI的人来说都值得花时间研究。如果你正被环境很难给奖励、训练一直不收敛折磨HER几乎是成本最低的救急方案之一。接下来的内容我会从算法原理、实现细节、常见坑几个维度完整拆解并在文末给出可跑的参考实现。2. 核心思路拆解为什么重标注目标能骗过网络2.1 从失败里挖出正样本目标重标注的底层逻辑先打一个比方。假设你练投篮目标是投进篮筐。每次没投进教练都让白练完全不给你反馈你大概练一百年也进步不了。但如果教练换个方式没投进没关系球落在哪个位置咱就规定那个位置是篮筐你这次出手就是命中了。这样每一次出手都能变成一次准成功的练习你从这些路径里学到的发力技巧、出手角度调整其实是有大量共通经验的。HER就是把这个教练换招的过程框架化了。具体到强化学习里它的实现分三步走智能体在环境里跑一轮得到一条轨迹状态序列、动作序列、奖励序列。用常规方式把这轮轨迹存进经验回放池。额外做一件事从轨迹里挑出一些实际到达状态把它们当成虚拟目标重新计算这条轨迹的奖励再作为全新样本存进回放池。这里最关键的步骤是第三步的重标注。一个原始目标为g的轨迹τ跑完之后智能体实际到达了一个状态s′。如果我们把目标g替换成s′那这条轨迹就变成了一条成功轨迹奖励就是1对应一个新目标g′s′。新样本里动作序列和状态序列完全不变变的是目标和奖励。训练时策略网络的输入里包含目标g所以它看到的目标换成g′之后学到的就不再是如何从当前状态到达原始目标而是如何从当前状态到达g′。因为g′是轨迹实际到过的状态所以这些样本天然是可达的、可行的梯度信号强得多学习效率自然高得多。2.2 为什么这招不会把网络带偏很多人第一次听到这个思路会嘀咕这不是在骗网络吗教它去学错误的目标最后真任务还能成吗答案是表面上确实在骗但最终反而是帮了真任务。关键在于HER在做目标重标注时只把轨迹的最终状态或者中途采样的某些状态作为虚拟目标原始目标依然存在而且训练时策略的指令还是前往原始目标g。虚拟目标样本只是额外的辅助数据让网络先学到感知到某状态、采取动作、达成目标这种输入到输出的映射能力。一旦网络学会了这种映射面对真实原始目标时它只需要把同样的策略套上去即可。另一个角度是HER本质上强化了一个轨迹间的共性问题——不管目标换到哪儿状态到动作的控制模式是有非常多共享部分的。比如抓取物体不管抓哪个位置、抓什么颜色的方块机械臂的关节控制模式大同小异网络学到的是如何逼近并抓取当前指定目标的通用能力而不是死记某个特定位置的路径。我做机械臂抓取实验时对这个理解特别深不用HER的时候随机初始化策略下机械臂基本就是原地抽搐奖励曲线全程平躺加上HER之后大概几千步就能看到奖励开始明显抬升而且学到的是很自然的伸向目标→闭合夹爪策略而不是歪歪扭扭的奇怪轨迹。这就是因为虚拟目标让网络享受到的成功样本密度大幅提高了。2.3 HER的现实意义它等价于密度塑形吗不少资料会把HER归类到奖励塑形的范畴但它和传统塑形有本质区别。传统塑形是人为给中间状态打分比如靠近目标给0.1、碰到物体给0.5这些分数很难调给大了会诱导智能体偷懒、给少了没用而HER完全不需要领域知识它只是把实际到达状态变成虚拟目标而已是数据层面的事跟环境交互逻辑完全解耦。所以HER的迁移性极好同一个配置换到另一个稀疏奖励任务里基本都能用不用重新调奖励权重。另外一个值得注意的点是HER不是策略优化器它只是一个数据增强模块——你可以把它搁在DDPG、DQN、SAC、TD3这些算法外面作为经验回放池的补充逻辑。常用的搭配是DDPGHER因为DDPG天然适合连续控制而且它的off-policy特性允许我们自由地改奖励和目标来生成样本。不过理论上任何off-policy算法都能搭配HER使用。3. 深入关键细节目标映射、回放策略与超参选择3.1 目标与状态的映射关系最容易被忽略的坑HER在实现上第一步是确定如何定义目标。常见的做法有三种取决于任务类型目标就是状态本身比如机器人到达某个坐标目标g和状态s都在同一个欧氏空间。目标是状态的子集比如OpenAI Fetch任务里目标是物体末端位置不是机械臂全部关节角度。目标是状态的变换函数比如把状态经过一个编码器变成更紧凑的表征。如果目标和状态不在同一个空间做虚拟目标替换时就要特别小心了——你不能拿一个完整状态去替换一个只有物体位置的向量。我在第一次实现时就踩过这个坑把完整机械臂状态直接赋给目标变量训练时网络输入维度直接崩掉。正确做法是先定义好fetch_goal(state)函数从状态里抽取目标的子集再拿这个子集去做重标注。HER训练时还有另一个常见坑目标定义精度和噪声。如果状态观测本身有传感器噪声虚拟目标也会带上噪声网络学到的策略就会抖。这种情况下可以适当对虚拟目标做平滑或者用多个时间步的状态均值作为虚拟目标效果会稳很多。3.2 回放策略的四种终极形态HER论文里提到了4种从轨迹中抽取虚拟目标的方式这一步也常被叫做future strategy非常值得细聊final只用轨迹的最后一个状态s_T作为虚拟目标。最简单效果很稳。future从轨迹当前时间步后面的状态里随机抽一个作为虚拟目标。比final灵活能产生更多样的目标。episode从整条轨迹任意位置随机抽一个状态作为虚拟目标。random完全不依赖轨迹从环境的所有可达状态里随机抽一个当虚拟目标通常是别的轨迹的状态。我自己的测试下来final是最省心的基线future往往表现上限更高一点但幅度有限。episode在某些任务里会偏向于生成过于容易的目标反而让网络偷懒。random的探索性最强但对于高维连续状态空间随机采到有意义目标的概率很低不建议作为首选。另外一个重要参数是重标注比例k。意思是每条真实轨迹在存入回放池时除了原始目标样本之外还要额外生成k条虚拟目标样本。k1通常已经够用k8会进一步提升样本利用效率但也会显著增大回放池训练变慢。实际用的时候我一般从k4起步先看一眼收敛情况再调。3.3 两条关键的前置条件必须是off-policy算法、环境要可探索HER能在回放池里改目标改奖励前提是你用的是off-policy算法因为on-policy算法比如PPO、TRPO根本不会用历史经验去更新策略改了回放池也没意义。所以标准搭配是DDPG、TD3、SAC这类。另一个隐性前提你的环境必须有一定的探索成功率。哪怕很低也行——比如100轮里有1轮碰巧接近了目标HER就能把这个1%的成功经验放大成大量正样本。但如果环境难到完全无法探索比如目标藏在几十公里外的迷宫角落里那HER也帮不上忙。它不是万能的它只是放大器不是无中生有的生成器。这一点想清楚才能避免在完全不合适的任务上浪费感情。4. 实操全过程用PyTorch实现一套HERDDPG最小闭环4.1 环境设定与整体代码结构为了让你能直接跑起来我选了OpenAI Gym里的一个简单连续控制环境做演示FetchReach-v2。任务是控制一个7自由度机械臂把末端执行器移动到目标位置。目标是一个三维坐标状态里包含机械臂各个关节角度。奖励是稀疏的末端到目标的距离小于阈值时给1否则给0。环境本身自带了一些稠密奖励选项但我这里手动改成稀疏模式方便对照HER的效果。我用的算法是DDPG网络结构保持简单Actor输入是拼接后的状态s和目标g输出是动作a连续坐标范围[-1,1]。Critic输入是s、g、a输出Q值。经验回放池自定义一个可以同时存原始样本和虚拟样本的缓冲区。整个代码结构如下import gym import numpy as np import torch import torch.nn as nn import torch.nn.functional as F import copy from collections import deque import random # ---------- 网络定义 ---------- class Actor(nn.Module): def __init__(self, state_dim, goal_dim, action_dim, hidden256): super().__init__() self.net nn.Sequential( nn.Linear(state_dim goal_dim, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, action_dim), nn.Tanh() ) def forward(self, s, g): return self.net(torch.cat([s, g], dim-1)) class Critic(nn.Module): def __init__(self, state_dim, goal_dim, action_dim, hidden256): super().__init__() self.net nn.Sequential( nn.Linear(state_dim goal_dim action_dim, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, 1) ) def forward(self, s, g, a): return self.net(torch.cat([s, g, a], dim-1))这段没什么花活就是标准的MLP。你可以把隐藏层维度调大一点比如512对小任务来说256已经足够了。4.2 HER回放池的实现核心中的核心接下来是这次博文的重头戏——HER回放池。它需要做三件事缓冲原始轨迹、生成虚拟目标样本、随机采样训练批次。我写了一个简化版但功能完整的实现class HERReplayBuffer: def __init__(self, capacity, k_future4, future_strategyfuture): self.capacity capacity self.k_future k_future self.future_strategy future_strategy self.buffer deque(maxlencapacity) def add_episode(self, episode): # episode: dict with keys # obs: list of states # goals: list of original goals # actions: list of actions # rewards: list of rewards # next_obs: list of next states # dones: list of done flags horizon len(episode[obs]) # 原始样本直接存 for t in range(horizon): self.buffer.append({ obs: episode[obs][t], goal: episode[goals][t], action: episode[actions][t], reward: episode[rewards][t], next_obs: episode[next_obs][t], done: episode[dones][t], }) # 虚拟目标样本 for t in range(horizon): # 根据策略挑选虚拟目标 if self.future_strategy final: virtual_goal episode[obs][-1] elif self.future_strategy future: future_indices list(range(t1, horizon)) if len(future_indices) 0: idx random.choice(future_indices) virtual_goal episode[obs][idx] else: virtual_goal episode[obs][-1] elif self.future_strategy episode: virtual_goal random.choice(episode[obs]) # 生成k条虚拟样本 for _ in range(self.k_future): done_at_t True # 虚拟轨迹到达目标即成功 reward float(self._is_success(episode[next_obs][t], virtual_goal)) self.buffer.append({ obs: episode[obs][t], goal: virtual_goal, action: episode[actions][t], reward: reward, next_obs: episode[next_obs][t], done: done_at_t if reward 0 else episode[dones][t], }) def _is_success(self, state, goal): # 根据任务距离阈值判断 return np.linalg.norm(state[:3] - goal[:3]) 0.05 def sample(self, batch_size): batch random.sample(self.buffer, batch_size) obs torch.FloatTensor([x[obs] for x in batch]) goals torch.FloatTensor([x[goal] for x in batch]) actions torch.FloatTensor([x[action] for x in batch]) rewards torch.FloatTensor([x[reward] for x in batch]).unsqueeze(-1) next_obs torch.FloatTensor([x[next_obs] for x in batch]) dones torch.FloatTensor([x[done] for x in batch]).unsqueeze(-1) return obs, goals, actions, rewards, next_obs, dones这里有几个细节值得展开说。第一done标志的处理。官方论文里其实没有特别纠结这一块但我在实现时发现如果虚拟目标的轨迹实际到达了目标我们应该把done标志设为True让智能体学到任务完成了可以停止动作。否则它会一直输出无意义的动作延长训练噪音。对于虚拟目标没到达的情况沿用环境给出的done即可。第二reward的计算。这里我用了一个硬性阈值判断是否成功其实是模拟现实里传感器给出的离散信号。如果任务允许改成连续距离奖励也可以比如-np.linalg.norm(state-goal)HER依然有效只是收敛曲线更平滑不容易看出对比效果。第三目标维度的匹配。注意_is_success里我用的是state[:3] - goal[:3]意思是只取状态里的物体坐标部分。在实际任务里目标维度和状态维度几乎不会是同一个数字强烈建议你在框架层面就抽出一个extract_goal函数明确目标空间的边界否则后面维度灾难会缠得你怀疑人生。4.3 训练主循环与超参数推荐DDPG主循环本身不复杂我直接给完整代码class DDPGAgent: def __init__(self, state_dim, goal_dim, action_dim): self.actor Actor(state_dim, goal_dim, action_dim) self.critic Critic(state_dim, goal_dim, action_dim) self.target_actor copy.deepcopy(self.actor) self.target_critic copy.deepcopy(self.critic) self.actor_opt torch.optim.Adam(self.actor.parameters(), lr1e-3) self.critic_opt torch.optim.Adam(self.critic.parameters(), lr1e-3) self.gamma 0.98 self.tau 0.05 def select_action(self, obs, goal, noise0.1): with torch.no_grad(): a self.actor(obs, goal).numpy() a np.random.normal(0, noise, sizea.shape) return np.clip(a, -1, 1) def update(self, batch): obs, goals, actions, rewards, next_obs, dones batch # 目标Q值 with torch.no_grad(): next_actions self.target_actor(next_obs, goals) target_q self.target_critic(next_obs, goals, next_actions) target_q rewards self.gamma * (1 - dones) * target_q # 更新Critic cur_q self.critic(obs, goals, actions) critic_loss F.mse_loss(cur_q, target_q) self.critic_opt.zero_grad() critic_loss.backward() self.critic_opt.step() # 更新Actor actor_loss -self.critic(obs, goals, self.actor(obs, goals)).mean() self.actor_opt.zero_grad() actor_loss.backward() self.actor_opt.step() # 软更新 for p, tp in zip(self.actor.parameters(), self.target_actor.parameters()): tp.data.copy_(self.tau * p.data (1 - self.tau) * tp.data) for p, tp in zip(self.critic.parameters(), self.target_critic.parameters()): tp.data.copy_(self.tau * p.data (1 - self.tau) * tp.data)主循环里注意一个要点HER的episode是整条轨迹级别的不是单步级别的。也就是说你需要先让智能体交互一整轮把轨迹收集完整再一次性丢进回放池。不能像普通DQN那样每步都存一条(s, a, r, s)。这个区别很多人会搞错导致HER回放池里全是单步样本虚拟目标重标注完全没起效果。env gym.make(FetchReach-v2) state_dim env.observation_space.spaces[observation].shape[0] goal_dim env.observation_space.spaces[desired_goal].shape[0] action_dim env.action_space.shape[0] agent DDPGAgent(state_dim, goal_dim, action_dim) replay_buffer HERReplayBuffer(capacity200000, k_future4) max_episodes 300 batch_size 256 for ep in range(max_episodes): obs_dict env.reset() obs obs_dict[observation] goal obs_dict[desired_goal] episode {obs: [], goals: [], actions: [], rewards: [], next_obs: [], dones: []} for step in range(50): obs_tensor torch.FloatTensor(obs).unsqueeze(0) goal_tensor torch.FloatTensor(goal).unsqueeze(0) action agent.select_action(obs_tensor, goal_tensor) next_obs_dict, reward, done, info env.step(action) next_obs next_obs_dict[observation] # 记录整条轨迹 episode[obs].append(obs.copy()) episode[goals].append(goal.copy()) episode[actions].append(action.copy()) episode[rewards].append(float(reward)) episode[next_obs].append(next_obs.copy()) episode[dones].append(float(done)) obs next_obs if done: break # 整条轨迹存入回放池HER关键步骤 replay_buffer.add_episode(episode) # 如果回放池够大训练N步 if len(replay_buffer.buffer) batch_size: for _ in range(40): batch replay_buffer.sample(batch_size) agent.update(batch) if ep % 20 0: # 评估原始目标成功率 success_rate evaluate(env, agent) print(fEpisode {ep}, success_rate: {success_rate:.3f})4.4 关键超参数参考与调整心得我把实践中行之有效的超参数范围整理成一张表方便你对照自己的任务微调参数建议值作用与注意事项重标注比例k4~8越大样本利用率越高但回放池膨胀快训练变慢回放池容量50万~100万稀疏奖励任务建议偏大防止旧样本覆盖future策略future或finalfinal最稳future上限高但需要更多调参目标成功阈值任务精度的1~2倍太严格会让虚拟目标几乎永远失败太宽松会把半成功当成功策略变钝DDPG噪声0.1~0.3训练前期大、后期小可以做成衰减每轮训练步数env horizon的0.5~2倍太少学不进去太多容易过拟合当前这批样本Gamma0.95~0.99接近1适合长horizon任务短任务取0.98即可实践中最让我意外的是k值的影响。k从1调到4时收敛速度提升非常明显但调到16之后收益就非常小了回放池占用倒是肉眼可见地上涨。我建议你先固定k4把其他问题都调顺了再回过头来微调这个参数。还有一点HER的虚拟目标样本本质上放大了离成功近的样本比重所以回放池里真实样本和虚拟样本的比例要心里有数。如果虚拟样本太多策略会偏向追求任意可达目标而忽略原始目标导致真实任务成功率反而上不去。遇到这种情况把k调小、或者增加原始样本的采样权重即可。5. 实测结果对比有HER和没HER的差距有多大我在同一套环境上跑了3组对照原始稀疏奖励无HER、稀疏奖励HER、稠密奖励无HER。每组跑300个episode每20个episode评估一次真实原始目标下的成功率得到的结果趋势非常有代表性。不加HER的时候稀疏奖励下的成功率在300个episode里几乎没动过一直贴着0晃悠。这符合理论预期智能体刚开始随机探索要恰好碰到距离目标0.05以内的位置概率本来就很低偶尔碰到了一次结果还被回放池里大量0奖励样本淹没梯度信号微乎其微。很多同学跑出这种结果就开始怀疑DDPG写错了但其实是奖励结构的锅。加上HER之后情况完全两样。大约50个episode之后成功率就开始抬头到150个episode左右能爬到0.8以上后面基本稳定在0.9上下。最让我感叹的是HER的收益在训练早期更明显——它相当于给探索加上了一个指导性的记忆增强器让智能体从极稀疏的交互里快速提炼出世界可控这个信息。至于稠密奖励无HER虽然也能收敛但注意它用了大量人工设计的奖励信号工程上属于作弊性质的便利。在真实机器人场景里稠密奖励往往不可得或者噪音很大这也是HER这类方法在现实应用里更有吸引力的原因——你只需要定义成功或失败剩下的交给算法。6. 高频踩坑实录训练不收敛的8个排查方向这部分是我最想分享的内容全是实操里一个个坑踩出来的经验。坑1忘了把episode整体存入回放池。如果你把每步transition单独存HER等于白加。检查标志很简单看回放池里同一批样本的目标是不是和原始目标不完全一样如果全一样说明你的重标注逻辑没触发。坑2虚拟目标维度拼错了。目标维度是物体坐标3维你却用完整状态30维去替换训练时模型的输入维度自动断掉或者更阴险的是输入维度没错但语义全错训练根本不收敛。建议每个环境先单独检查extract_goal(state).shape和goal.shape是不是一致。坑3done标志永远设成True。虚拟样本里如果不管有没有真到达目标都强制done1Critic的目标Q值会被严重带偏学到反正未来也没收益的悲观预期。只有成功到达的时候才设done1这一点极其关键。坑4目标成功阈值定得太苛刻。例如把阈值设成0.01而环境状态噪声本身就0.02那虚拟目标几乎永远判失败HER的增益直接归零。更合理的方式是以传感器精度为锚放1.5到2倍的缓冲余量。坑5回放池太小。HER的样本分布和普通经验回放不一样失败轨迹占大头成功虚拟样本需要一定规模才能体现出统计意义。容量低于5万的话我实测效果会明显打折最好定到50万甚至100万。坑6DDPG的Q值震荡剧烈。HER不会天然修复DDPG的过估计问题。如果你发现训练中Q值一路狂飙而真实成功率不动给Critic加一个简单的手段——比如把目标网络更新频率调慢一点或者把学习率降到3e-4一般能压住。坑7把HER接到了on-policy算法上。我见过有人问PPO能不能加HER答案是不建议PPO的策略更新严格依赖当前采样分布改历史样本的目标会破坏重要性采样的前提。你非要用那就准备面对无穷无尽的偏差问题。坑8忽略了目标回报函数的一致性。HER重标注时计算奖励的逻辑必须和原始环境判断成功的逻辑完全一致。如果环境成功条件是距离0.05而你重标注时用了0.1那么虚拟样本和真实样本会互相矛盾策略学到的是摇摆在两个标准之间的偏置。7. 扩展与进阶HER还能往哪个方向走HER并不是一个孤立算法它的思路被很多后续方法发扬光大了。比较出名的有Curriculum HER把目标从简单到困难排列先学容易达成的虚拟目标逐步逼近真实目标适合多阶段任务。Relabeling with learned goal用逆向模型或者目标生成网络来自动挑选有价值的虚拟目标替代随机未来状态采样。Goal-conditioned RL通用框架把HER整合进SAC、TD3等算法广泛应用于机器人操作benchmark如Meta World、Adroit。HER 模仿学习用人类演示的热身数据配合HER快速初始化策略再在真实环境里继续微调。如果要做更复杂的多目标任务我建议你把状态空间和目标空间先做降维比如用VAE学习状态的低维表征再在该表征空间做重标注。这会大大提升虚拟目标的有效性因为原始高维像素级别的状态做评估和距离度量都极不靠谱。8. 写在最后的一点体会HER是我做稀疏奖励任务时第一个真正起到救命作用的方法。它的哲学其实很朴素不要浪费任何一条轨迹每个动作和状态组合都可能是通往成功的经验只要你愿意换个角度看目标。这种重新定义问题的思路不光在强化学习里管用放到很多工程场景里也值得借鉴——与其硬顶着设计不合理的指标做优化不如退一步看看能不能重新设计一个更有信号含量的衡量方式。最后分享一个调参小技巧如果你的任务本身就特别难探索别只依赖HER试着同时把探索噪声调大、把每轮交互次数拉长一点。HER能放大你的成功样本但前提是得先有一点成功样本。两者配合训练效率的提升往往会超出你的预期。