在做强化学习项目的时候很多人都会撞上同一个坎奖励函数设计不好智能体怎么都学不会。尤其是涉及机器人控制、导航避障这类连续控制任务稀疏奖励问题几乎是绕不过去的拦路虎。我去年做一个机械臂抓取项目时训练了整整两天成功率始终在个位数徘徊后来换了hindsight后见之明的思路重新设计经验回放只用了一个晚上就把成功率拉到了八成以上。这篇文章就把我踩过的坑和最终落地的方法完整拆开讲讲。这里说的hindsight不是英文单词“事后诸葛”的字面意思而是强化学习里面一个很经典的技术思想Hindsight Experience Replay简称HER中文常译作事后经验回放。这个思路最早由OpenAI的研究者在论文《Hindsight Experience Replay》中提出专门用来解决奖励稀疏、目标达成困难的环境训练问题。它最核心的一句话就是如果这次没能达成目标那就把没能达成的结果当作已经达成的目标重新构造一条经验用于学习。这个想法初听起来有点绕但实际落地之后效果立竿见影特别适合那些目标明确但奖励信号极少的任务。这篇博文主要面向正在做强化学习算法落地、或者入门之后被reward shaping折磨到崩溃的工程师和研究者。我会从原理层面的why讲起再给出一份可以直接照抄的PyTorch实现代码最后整理我实际操作中遇到的典型问题和排查技巧。不管你是想复现一篇论文的实验还是想在真实机器人平台上把算法跑起来这篇文章都能给你省下至少两周的试错时间。1. 核心思路拆解为什么hindsight能解决稀疏奖励问题1.1 稀疏奖励场景的困境不是算法不行是“样本”没有营养先说清楚稀疏奖励到底难在哪里。假设我们训练一个机械臂任务是把一个积木推到桌面上某个目标位置。目标位置用坐标系中的一个点表示比如(0.5, 0.3, 0.1)。如果机械臂推动积木后积木落在距离目标点1厘米以内我们给奖励1分否则奖励0分。这样一个二值奖励函数就是典型的稀疏奖励。在这种设定下智能体初期完全是随机探索的它几乎不可能在有限步数内正好把积木推到目标点附近。这就带来一个致命问题经验池里面存下来的样本绝大多数都是“奖励为0”的失败轨迹。算法去更新Q网络或者策略网络时看到的数据全部是“做什么都没奖励”梯度信号几乎为零网络根本学不到任何有用的东西只会慢慢退化成随机策略。有人会说那我改成距离奖励不就行了比如奖励等于目标距离的负值距离越近奖励越高。这个思路确实能缓解稀疏性问题但会产生新的麻烦奖励的尺度怎么定距离用什么范数需不需要归一化而且人为设计的距离奖励往往会引入偏差智能体可能学会“偷懒”走捷径而不是真正学会完成任务。HER的出发点恰恰不是去精心设计奖励函数而是从“经验怎么用”的角度切入让失败轨迹也能被转化为有学习价值的样本。这个思路转变很关键。1.2 后见之明的本质用“已经发生的结果”反推“本可达成目标”HER的思想其实非常符合人类的学习方式。想象一个人学投篮他投出去十次一次都没进。每一次失败后他如果只记住了“没进奖励0”那练再多也是白练。但如果他每次投完都观察一下篮球实际落到了什么位置然后把“下一次把球投到这个实际落点”当作一个阶段性目标来练习久而久之他就掌握了对不同位置发力大小的控制能力。HER做的事情一模一样。每一条采样轨迹s1, a1, s2, a2, ..., sT虽然对应的原始目标g没有达成但这条轨迹最终到达的状态sT是确定的。我们重新选一个虚拟目标g令g等于这条轨迹中的某个状态那么从这条轨迹的视角看这个虚拟目标“被达成了”。于是原本全零奖励的失败轨迹可以立刻被改写成一条带正奖励的成功轨迹。这些改写后的样本再扔回经验池里用于训练本质上等价于大幅提高了正样本的比例从而让策略网络能学到“如何达成某个具体状态”的行为模式。这里面有一个很有意思的点HER并没有改变环境也没有改变原始任务它只是在经验回放时对“目标空间”做了重采样。目标分布从“只有一个严格的原始目标”变成了“原始目标加一串轨迹内的虚拟目标”学习信号一下子就丰满了。用大白话说算法自己给自己找了一套“虽然没有成功顶到终点但每一步都有阶段里程碑”的训练课程。1.3 适用边界与局限不是所有任务都能无脑用HERHER虽然好用但也不是万能的。我在具体项目里测试下来它最适合的是goal-conditioned任务也就是目标状态能被明确定义、并且环境状态能直接反映目标是否达成的任务。比如机械臂抓取、机器人导航、迷宫寻路、拼图类游戏这类任务天然符合“状态即目标”的设定。反过来如果是纯粹的非目标型任务比如经典的CartPole平衡杆、游戏得分最大化这类没有明确目标状态的任务HER就没有用武之地因为你没法定义“虚拟目标”到底是什么。另外HER对探索能力还是有一定要求的。如果环境状态空间非常大而且初始策略完全不具备任何基础行为仅仅靠HER改写经验是不够的智能体可能一直随机乱走改写的虚拟目标也都是在同一个很小的局部区域打转。这种情况下通常需要配合更强的探索策略比如在动作空间里加噪声、使用参数空间噪声或者结合curiosity机制。2. 原理深入从目标重标记到经验回放的完整链路2.1 目标条件策略下的马尔可夫决策过程设定要真正把HER用起来首先要把问题建模为目标条件马尔可夫决策过程Goal-Conditioned MDP。这个MDP和我平时写常规强化学习代码时的MDP有一个关键区别状态空间里多了目标变量g。每一步的转移不仅取决于当前状态s和动作a还依赖于目标g奖励函数同样是以目标是否达到为条件的。具体地定义一个元组 ( M_g (S, A, P, R_g, \gamma) )其中 ( S ) 是状态空间( A ) 是动作空间( P ) 是转移概率( \gamma ) 是折扣因子奖励函数 ( R_g(s, a, s) ) 由目标g决定通常写成 ( R_g(s,a,s) 1(\phi(s) g) ) 这种二值形式其中 ( \phi(\cdot) ) 是把状态映射到目标空间的函数。比如机械臂任务里如果目标空间就是积木的xy坐标那么策略的目标就是输出动作让 ( \phi(s) ) 逼近g。目标条件策略则写作 ( \pi(a | s, g) )也就是说智能体做决策时既要看当前状态也要看当前目标。这一点在代码实现里很容易被忽略很多人直接把目标拼到状态里当作扩展状态输入维度是 state_dim goal_dim这样做本身没错但需要确保采样和更新时都是按目标条件来做的否则策略会混淆不同目标下的行为模式。2.2 目标重标记的四种常用策略final、future、episode、randomHER论文里给出了四种从轨迹中选择虚拟目标 ( g ) 的策略这几种策略在真实项目里的效果差异非常大。我逐一说明一下方便你根据自己的任务选择final直接把轨迹的最终状态映射为目标空间选 ( g \phi(s_T) )。实现最简单但缺点是一个轨迹只能产生一条正样本。如果轨迹很长中间很多状态其实也很有价值只取最终状态会浪费信息。future从轨迹中随机选一个时间步k要求 ( k t )然后取 ( g \phi(s_k) )。这是我在实践中用得最多的策略。它保证重标记目标对应的状态出现在当前时间步之后也就是“未来某个时刻确实到达了这个状态”在时间因果关系上是成立的。论文里默认使用的就是future策略经验上看效果也最好。episode从当前轨迹中随机选一个状态作为目标不要求时间先后。好处是目标覆盖范围更广但坏处是可能出现目标在轨迹早期出现、而当前t时刻已经远离该状态的情况生成的样本在时序上会有点混乱。random从整个经验池中随机选一个历史状态作为目标。这个策略我试下来效果最不稳定虽然能极大拓展目标覆盖范围但也引入了大量与当前轨迹毫无关联的目标训练起来噪声很大。实战中我基本固定用future策略。有个细节是future策略的k到底怎么选——我一般从 ( [t1, T] ) 区间均匀采样。如果轨迹结束得太早整个区间太短重标记目标的选择空间就小这时候建议适当增加每个episode的最大步数让轨迹更长一些。2.3 HER与DDPG/TD3的融合方式不只是改一下经验池HER并不是一个独立的算法它是一种经验回放技巧需要嫁接在某个基础强化学习算法上才能发挥作用。我用的最多的是DDPG和TD3。以TD3为例HER融合后的整体训练流程变成了这样Agent在环境中按照当前策略 ( \pi(a|s,g) ) 采样一条完整轨迹记录每一步的 ( (s_t, a_t, r_t, s_{t1}, g) )。轨迹采集完成后对轨迹中的每一步额外采样K个虚拟目标 ( g )每个虚拟目标都生成一条新的四元组 ( (s_t, a_t, rt, s{t1}, g) )。这里 ( r_t ) 是根据 ( g ) 重新计算的奖励。将原始经验和新生成的经验全部放入经验池。训练时从经验池中随机采样一个batch用TD3的更新规则分别更新Critic和Actor。这里有个容易犯的错原始经验里奖励是 ( r_t )它对应原始目标g而重标记经验里的 ( r_t ) 可能等于1但它对应的是虚拟目标 ( g )。这两种经验必须分开存放目标字段否则训练时的目标条件奖励会张冠李戴。我在代码里用一个经验字典结构来保证这一点。3. 实操一份可以直接套用的PyTorch实现3.1 环境封装让任意稀疏奖励环境快速支持HER动手写HER之前得先把环境改造成标准的目标条件接口。以机械臂环境为例我建议封装一个GoalEnvWrapper让reset()返回初始状态和目标step()返回状态、奖励、终止标志和附加信息。伪代码如下import gym import numpy as np class GoalEnvWrapper(gym.Env): def __init__(self, env, goal_threshold0.05): super().__init__() self.env env self.goal_threshold goal_threshold obs env.reset() self.observation_space gym.spaces.Box( low-np.inf, highnp.inf, shape(obs[observation].shape[0] obs[desired_goal].shape[0],) ) self.action_space env.action_space def reset(self): self.raw_obs self.env.reset() return np.concatenate([self.raw_obs[observation], self.raw_obs[desired_goal]]) def step(self, action): raw_next_obs, _, done, info self.env.step(action) if isinstance(raw_next_obs, dict): achieved_goal raw_next_obs[achieved_goal] desired_goal raw_next_obs[desired_goal] distance np.linalg.norm(achieved_goal - desired_goal) reward 1.0 if distance self.goal_threshold else 0.0 done False obs np.concatenate([raw_next_obs[observation], desired_goal]) self.raw_obs raw_next_obs return obs, reward, done, {achieved_goal: achieved_goal, desired_goal: desired_goal} return raw_next_obs, reward, done, info注意一个很关键的细节这里的done我强制设为False。很多稀疏奖励环境会设置成“只要没达到目标就永不终止直到最大步数”这种设计是对的。因为如果让环境提前终止那些失败的短轨迹会大量堆积经验池被无效数据污染HER的效果会大打折扣。3.2 目标重标记器的核心逻辑这个模块是HER的灵魂。我在实现时专门写了一个ReplayBuffer类里面包含了经验存储和重标记两个功能。核心逻辑是把轨迹先缓存到一个临时列表里等一整条轨迹采完之后再执行重标记。逐条插入经验池的做法在HER里行不通因为你无法在当前时刻预知未来状态也就没法选future目标。class HERReplayBuffer: def __init__(self, capacity, state_dim, goal_dim, action_dim, k_future4, strategyfuture): self.capacity capacity self.state_dim state_dim self.goal_dim goal_dim self.action_dim action_dim self.k_future k_future self.strategy strategy self.buffer [] self.pos 0 self.episode_buffer [] def store_episode(self, episode): episode是一个list每个元素是(s, a, r, s_next, g, achieved_goal) for t, (s, a, r, s_next, g, ag) in enumerate(episode): self._store_transition(s, a, r, s_next, g, ag) for _ in range(self.k_future): if self.strategy future: future_idx np.random.randint(t 1, len(episode)) elif self.strategy final: future_idx len(episode) - 1 elif self.strategy episode: future_idx np.random.randint(0, len(episode)) else: future_idx np.random.randint(0, len(self.buffer)) new_goal episode[future_idx][5] # achieved goal new_reward self.compute_reward(new_goal, ag, threshold0.05) self._store_transition(s, a, new_reward, s_next, new_goal, ag) self.episode_buffer.clear() def compute_reward(self, desired_goal, achieved_goal, threshold0.05): distance np.linalg.norm(desired_goal - achieved_goal) return 1.0 if distance threshold else 0.0 def _store_transition(self, s, a, r, s_next, g, ag): if len(self.buffer) self.capacity: self.buffer.append(None) self.buffer[self.pos] (s, a, r, s_next, g) self.pos (self.pos 1) % self.capacity这个实现里有几个我自己踩过坑之后才加上的点第一compute_reward用了阈值判断而不是严格相等否则浮点误差会让你永远得不到正奖励第二store_episode里的future_idx是随机选的但我加了一个判断确保t1不超过len(episode)这在缩短轨迹时很容易越界第三每个transition额外扩展k_future条重标记经验一般取4如果你想训练更激进一点可以调大到8但注意经验池会被很快填满有必要相应加大capacity。3.3 与TD3算法结合的完整训练循环把HER和TD3结合核心是每一次环境交互后即使某条轨迹还没结束也要把已经采到的轨迹片段暂存起来。等整条轨迹terminate或者到达max_steps之后一次性调用store_episode做重标记。训练主循环的骨架如下def train_her_td3(env, her_buffer, agent, episodes10000, max_steps200, warmup1000): for episode in range(episodes): obs env.reset() g obs[desired_goal] if isinstance(obs, dict) else obs[state_dim:state_dimgoal_dim] episode_transitions [] for step in range(max_steps): if len(her_buffer.buffer) warmup: action env.action_space.sample() else: action agent.select_action(obs, g) action np.random.normal(0, 0.1, sizeaction.shape) action np.clip(action, env.action_space.low, env.action_space.high) next_obs, reward, done, info env.step(action) achieved_goal info[achieved_goal] episode_transitions.append((obs, action, reward, next_obs, g, achieved_goal)) obs next_obs if done: break her_buffer.store_episode(episode_transitions) if len(her_buffer.buffer) warmup: for _ in range(40): batch_s, batch_a, batch_r, batch_s_next, batch_g her_buffer.sample(batch_size256) agent.update(batch_s, batch_a, batch_r, batch_s_next, batch_g)注意动作噪声的处理。我在warmup阶段用的是纯随机动作之后是TD3的标准做法在actor输出的动作上叠加高斯噪声然后clip到动作边界。这里有个细节如果环境动作范围是[-1,1]噪声标准差我常设0.1到0.2之间。设太大会让智能体学会的输出动作偏随机设太小探索不足HER的目标重标记再怎么强也救不了探索盲区。3.4 训练参数与实际效果参考我基于自己的项目经验整理了一组经过调优的默认参数你可以直接用参数项推荐取值说明经验池容量1e6机械臂这样高维连续任务建议至少50万起步k_future4每个transition额外生成4条重标记样本batch_size256太小会导致目标重标记样本的方差很大策略噪声0.2TD3中target policy smoothing的sigma动作噪声0.1训练时叠加在actor输出上的探索噪声warmup1000先随机采样上千条transition再开始训练每episode更新次数40一条轨迹攒够之后多更新几次加速利用actor学习率3e-4Adam默认基本不用动critic学习率3e-4同上这些参数用在我做的FetchReach和FetchPush这两个任务上训练1万到2万个episode后成功率能到90%以上。对比没有HER的纯TD3同样的条件下成功率不到10%。如果你在自己的环境里复现建议先跑FetchReach这种相对简单的任务验证一下pipeline是否通再上复杂环境。4. 常见问题与排查技巧实录4.1 训练完全不上涨先检查目标重标记是否真的生效这个现象最常见。如果你发现训练了成百上千个episode成功率一直贴着0不涨首先要做的不是调学习率而是确认HER的经验重标记到底有没有真的往经验池里写数据。建议写一段调试代码打印经验池中奖励为1的样本比例。正常来说一旦启用HER经验池里正样本即reward1的比例应该在20%到40%左右。如果你发现这个比例几乎为0说明你的compute_reward一直没返回1大概率是阈值设得太严或者距离函数写错了。举个例子我之前在FetchPush环境里观察空间是三维坐标距离函数用了欧氏距离阈值一开始设成0.01结果重标记样本里几乎没有正样本因为机械臂推动滑块后哪怕到了目标附近误差也难以压到1厘米内。后来把阈值调到0.05训练很快就跑通了。另一个常见原因是achieved_goal字段取错。有些环境的achieved_goal并不是状态的全部而是state中提取出的某几个维度。如果你的环境返回的achieved_goal是空数组或者维度不对compute_reward计算出来的距离毫无意义。可以打印一条轨迹检查achieved_goal的shape和数值范围是否合理。4.2 训练能涨但波动极大问题多半出在目标空间分布HER重标记之后经验池里的目标分布会发生变化。如果目标空间的范围太大而虚拟目标又集中在少数几个区域训练波动就会非常剧烈。一个典型的例子是导航任务目标空间是整个地图的连续坐标。如果智能体早期的探索路径都在地图左下角那重标记出来的目标也都在左下角经验池对于右下角区域完全没有覆盖策略更新就会顾此失彼。排查方法也很简单定期tensorboard一下actor的loss和critic的loss如果critic loss忽高忽低说明batch里样本的目标分布太不均衡。这时候有两个解决方向一是增加探索强度让轨迹覆盖更大的状态空间二是调整future策略中k的取值范围比如把future_idx改成只在后半段轨迹里选让目标尽量靠近轨迹较晚的状态这样虚拟目标会更接近“已经探索到的状态”。4.3 训练末期成功率卡在80%上下这类瓶颈的突破技巧我在两个项目里都遇到过训练后期成功率卡住的情况。HER擅长解决稀疏奖励但到了训练后期大部分经验其实已经能顺利达成虚拟目标了HER带来的新信息变少模型开始进入瓶颈期。这种情况下我试过两种有效方案第一种方案是动态调整重标记比例。前期用大的k_future比如8等评估成功率超过50%之后把k_future降到2让模型更多地从原始目标样本中学习精细化控制。这个调整相当于给训练课程降温让目标难度逐渐回归原始任务。第二种方案是引入目标生成器或课程学习。简单做法是按照当前策略评估时的失败案例统计经常失败的轨迹状态分布把这些状态作为额外的虚拟目标加入经验池。这个思路有点接近goal gan的思想但不是每个项目都需要如果只是工程落地第一种方案通常已经够用。4.4 一个容易被忽视的隐性bug目标没有拼进输入向量最后提醒一个我在code review时经常发现的bug。很多参考资料里说HER的状态空间是原始状态加上目标但实际实现中在actor和critic的输入侧目标g没有和状态s做concat或者在计算出动作后Policy网络并行的多个head中有一个没有使用条件目标g。这种bug比较隐蔽因为训练初期的loss和普通的TD3看起来几乎一样只是怎么都学不好。建议的做法是把状态和目标拼接成一个tensor后记录一下输入维度state_dim goal_dim。如果模型结构上用了两个独立的编码器分别处理state和goal那也要确保critic的Q(s, a, g)计算时确实把g用于了条件化。检查一下代码里是否有类似q_value critic(torch.cat([state, action, goal]))这样的写法如果少了goal那就是bug所在。5. 进一步优化方向与扩展思路5.1 HER与课程学习结合从简单目标到复杂目标的递进前面提到HER的虚拟目标都是在探索轨迹内部选出来的这可能造成“目标难易程度”一直停留在某个中低水平。如果想让智能体挑战更难的目标可以叠加一个简单的课程机制随着训练进行逐渐把重标记目标中距离轨迹末端较远的样本比例增加。这个逻辑相当于让人先学投篮站近一点练熟了再退远。具体实现时我一般维护一个课程系数beta从0.1缓慢升到0.5。每个transition重标记时以beta的概率从整个轨迹随机选一个状态作为虚拟目标以1-beta的概率从future策略中选目标。这样训练前期虚拟目标主要集中在轨迹后半段的“已达成状态”比较容易学训练后期虚拟目标覆盖到更广的状态空间逼迫智能体学习跨区域的控制能力。5.2 稀疏奖励之外的拓展把HER当作多目标预训练工具很多人在目标任务上练好一个策略后就收工了其实HER产出的模型有一个额外价值它能当作多目标任务的基础策略。因为训练过程中模型见过大量虚拟目标相当于隐式地学了一个“从任意状态到任意目标状态的转移能力”的广义控制器。这个被训练好的actor网络在迁移到新任务时往往比在单一目标上训练出来的模型更容易finetune。我在一个实际项目中用过这个思路先在一个合成环境上把机械臂控制策略用HERTD3训练到较高成功率然后把它拿去初始化一个真实机械臂的数字孪生环境训练微调了几千个episode就达到了预期效果比从零开始训练省了大约60%的时间。如果你的项目有sim-to-real迁移需求强烈建议保留HER训练好的checkpoint而不是只保留最高评估分数的那一个。5.3 当HER遇见多智能体系统一种示教学习的变体最后的这个方向比较开放如果你的项目涉及多智能体协同比如两个机械臂配合完成搬运任务HER也有变通用法。做法是把一个智能体的轨迹当作另一个智能体重标记目标的来源。比如智能体A在某个时刻成功到达了目标位置可以把A到达位置的瞬间状态作为智能体B的一条虚拟目标经验。这在共享经验池的设定下能明显提升协同训练的效率。这个方向我在论文里见到过一些雏形自己也在木块堆叠任务上做过简单尝试。不过这种跨智能体的HER设定还不算成熟如果你打算在这个方向上做研究建议先在小规模场景验证虚拟目标的语义是否合理再逐步放大。回头看整个HER的落地过程我最大的体会是强化学习项目里很多问题不是模型容量不够而是“经验”本身营养不良。HER这个思路最大的价值不在于它有多么精巧的数学推导而在于它用最贴近人类直觉的方式朴素地解决了稀疏奖励这个老大难问题。每次有人问我RL落地用什么技巧最划算我首先推荐的还是HER——它改造小、收益大、逻辑清晰只要你把目标条件建模做对了训练一把跑通是大概率的事。希望这篇文章能把你在HER上可能会踩的坑提前排掉让你的实验一次就出来好结果。
阅读完成 · 觉得有帮助?