Hindsight Experience Replay:用事后经验解决强化学习稀疏奖励难题 hindsight这个词日常意思是事后聪明、事后诸葛亮我以前总觉得它带点贬义。直到做强化学习做到深夜看着训练曲线从0出发、一路贴着0横着走几万步过去纹丝不动才真正意识到在机器学习里事后复盘恰恰是一种极其稀缺的能力。这篇想写的就是深度学习圈子里一个非常经典的trick——Hindsight Experience Replay业内简称HER。它要解决的是强化学习里最让人头疼的一类问题稀疏奖励。简单说就是环境几乎不给反馈agent乱试几千次也拿不到一分奖励最终什么都学不会。如果你正在做机器人控制、游戏AI、或者任何目标明确但反馈稀少的任务这篇值得往下看。1. 稀疏奖励为什么你的agent一直在原地打转1.1 一个全是零的奖励函数会让训练彻底停摆在强化学习的标准设定里agent靠奖励信号来学习。策略梯度算法要靠reward的梯度去更新策略Q-learning要靠reward去算TD误差。如果reward一直是0梯度就没东西可算Q值也学不到任何信息整个优化过程就像一个没有外界输入的闭环系统agent只能在环境里瞎跑。最典型的场景是机器人操控。比如你让机械臂把一个杯子推到指定位置环境只在杯子最终进入目标区域的那一刻返回一个reward其他所有动作都没有反馈。我调过这类环境结果就是agent每次都把杯子推到一个莫名其妙的位置推完就结束全程reward等于0学习算法完全学了个寂寞。你可能觉得推错了也算经验啊道理是这么回事但普通RL算法没法从这些经验中学到东西因为目标没达成所有transition都被标记为失败奖惩全为零没有任何学习信号。我自己最崩溃的一次是给一个避障导航任务配了稀疏奖励跑了一整夜第二天起来看loss曲线形状和初始化时一模一样好像训练根本没开始过。问题不在代码而在于环境根本没有给agent为什么失败的反馈。1.2 先别急着上HER看看常见方案贵在哪大多数人遇到稀疏奖励第一反应是奖励塑形reward shaping。给每一步都设计一个中间奖励比如距离目标越近奖励越高。这个方法在玩具环境里很好用但凡是稍微复杂一点的任务问题就来了需要领域专家手工设计中间奖励函数成本高塑形奖励容易引入局部最优agent学会刷中间奖励但完不成最终任务在真实机械臂、真实机器人上错误的塑形奖励不仅没用还可能让机械臂做出危险动作。还有一种思路是课程学习curriculum learning把任务从易到难拆成一系列子任务。想法不错但拆任务本身就是一门手艺任务链条一长难度呈指数上涨。至于exploration bonus这种鼓励agent探索新状态的思路在低维状态空间还行状态一高维光靠新奇度根本不足以引导agent学到目标导向的行为。所以HER能火不是偶然它和上面这些方案站在完全不同的角度不改变奖励函数、不拆任务、不做额外探索激励只靠改写在replay buffer里已有的经验就把稀疏奖励问题给绕过去了。这个思路我在没看懂之前觉得有点作弊看懂之后觉得是真的妙。2. HER凭什么有效把失败重新定义成成功2.1 核心操作给轨迹换一个目标重新算一次奖励HER的思想可以用一句话概括一条没达到原始目标的轨迹换个目标来看可能就是一条完美轨迹。举个例子。机械臂推球原始目标是把球推到红色区域。结果这一局球最后停在了蓝色区域。按照普通RL这整条轨迹都是失败经验每步reward都是负分或者0。HER的操作是既然球最终停在了蓝色区域那我们就假装这一局的目标本来就是把球推到蓝色区域。这样一来这条轨迹里的每一步都变成了为了实现蓝色目标而采取的合理动作。这一步不是自欺欺人。从物理上来讲这套动作确实把球从起点推到了蓝色区域它对于把球推到蓝色区域这个目标来说就是货真价实的成功样例动作和结果之间的因果关系是客观存在的只是目标被换掉了而已。放到数学上更清楚。原始的一个transition长这样(s_t, a_t, s_{t1}, g, r_t)其中g是原始目标r_t R(s_t, a_t, g)是基于g计算出来的奖励。HER做的事情是额外构造一个transition(s_t, a_t, s_{t1}, g, r_t)这里g是这条episode里未来某个时刻的achieved goal实际到达的状态r_t R(s_t, a_t, g)则是基于新目标重新计算的奖励。然后把这个新transition丢进replay buffer和原始transition一起用于训练。关键点在于这个新transition的所有组成部分都是真实的。s_t、a_t、s_{t1}是真实采样的g是真实到达过的状态r_t是按照真实的目标达成情况算出来的。改变的只有我们想让agent去达成哪个目标。2.2 为什么替换出来的目标不是自欺欺人我第一次看到这个思路时心里冒出的问题是这样强行换目标agent学到的技能真的能迁移回原始任务吗答案是能但有个前提条件。你让agent用这套推球动作去学把球推到任意位置的策略它其实是在学一个更一般性的能力如何通过控制机械臂把球从当前位置推到想要的位置。原始任务中的推到红色区域只是这个一般性策略在特定目标下的一个实例。HER没有让agent专门为红色区域这个目标死记硬背而是让它在大量替换目标中反复练习从A到B的动力学关系和控制能力。这就像练投篮。你可能投了一百个球都没进篮筐但如果你把篮筐位置挪到球的落点那一百次投篮就变成了一百次空心入网而你学到的发力手感、出手角度、身体协调性全都是真的。这种手感对以后投任何位置都有用。所以HER能够生效有一个核心前提目标可以用状态来表示并且达成目标这件事可以被定义为当前状态与目标状态足够接近。学术点的说法是环境要满足goal-conditioned结构且目标空间与状态空间存在某种对称性即把一个实际到达过的状态当成目标在物理上是自洽的。机器人推东西、抓东西、走到某个位置这类任务天然满足这个条件。2.3 新目标从哪来future策略和final策略确定了要替换目标下一个问题就是新目标g从episode里的哪个状态取。HER论文里提到了两种策略final策略直接把整条episode最后一个状态当新目标。简单粗暴但一条episode只有一个新目标所有transition都换成同一个目标样本多样性差。future策略对每个transition从它之后的时间步里随机挑一个状态当新目标。比如第t步的transition新目标从第t1步到第T步之间的某个achieved goal里随机选。论文的默认选择是future实际体验下来也确实是future更好。原因有两个一是多样性。future策略让同一条episode里能构造出多个不同的hindsight目标而不是只有一个终点replay buffer里目标分布丰富得多。二是课程效应。future策略里越靠前的transition可选的未来状态越多目标也越远越靠后的transition可选目标越接近当前位置目标也越近。这让agent在训练早期多接触远期目标后期多接触近期目标天然有一种渐进式学习的感觉不需要额外设计课程。还有一个超参数k代表每个原始transition额外生成多少个hindsight transition。我的经验是k4起步追求性能可以试k8但k太大收益会递减因为同一条transition反复复制信息增量有限还会把buffer里的原始经验稀释掉。3. 实操落地从零搭一个HER训练流程3.1 环境选型直接用Fetch系列验证效果理论讲得再多不如跑几个实验来得实在。HER最经典的验证环境是OpenAI的Fetch系列比如FetchPush、FetchPickAndPlace。这些环境当初就是为了验证goal-conditioned RL设计的每一个step的info里直接给出achieved_goal做HER落地非常方便。现在的安装方式是用gymnasium生态pip install gymnasium pip install gymnasium-robotics注意gymnasium-robotics依赖mujoco如果之前没装过还得先装mujoco。创建环境很简单import gymnasium as gym import gymnasium_robotics env gym.make(FetchPush-v3, render_modeNone)拿到环境后observation是一个字典包含三块observation机械臂和物体的完整状态、achieved_goal当前实际到达的目标状态、desired_goal原始目标。做HER最关键的就是把achieved_goal和desired_goal分开用。3.2 replay buffer改造必须存整条episodeHER和普通RL在数据流上一个非常大的区别是replay buffer的粒度。普通DDPG的训练里每个transition采完就可以直接丢进buffer马上采样学习。但HER不行因为新的目标g必须从这条轨迹的未来某个状态里取而未来状态在transition刚发生那一刻还没出现。所以buffer里必须先存完整的episode等这条轨迹走完之后再统一生成HER样本。我的做法是维护一个episode暂存结构episode_cache { states: [], # 每个step的state不含goal achieved_goals: [], # 每个step实际到达的目标 actions: [], next_states: [], dones: [], }环境跑完一条episode之后遍历里面的每个transition按照future策略生成新的样本。这里有个容易被新手忽略的细节obs往往是把state和goal拼接在一起的向量如果直接把obs存buffer后面做HER替换goal时会非常痛苦。所以存的时候要把state和goal拆开存构造transition时再拼接。3.3 基座算法选型DDPG配HER是经典组合HER本身不是一个完整的强化学习算法它是对replay buffer数据的改造方法需要配合一个value-based算法使用。论文里的主实验用的是DQN的连续控制变体和DDPG实操中DDPGHER是最顺手的组合。选择DDPG而不是PPO原因在于HER生成的是off-policy样本只能喂给off-policy算法。PPO这类on-policy算法需要当前策略采集的数据你从buffer里翻出几千条旧轨迹来更新PPO在数学上就直接不成立了。DDPG、DQN、SAC这类off-policy算法没有这个限制buffer里塞什么它学什么。如果你不想调DDPG的探索噪声换成SACHER也行replay buffer部分完全一样只是SAC的熵正则项能让训练更稳一些。但对于首次复现HER我建议还是先按DDPG的配置来毕竟论文里给的参考值就是基于DDPG的。3.4 核心训练循环伪代码级别的实现下面是HERDDPG训练流程的伪代码我已经把注释写得很详细了for episode in range(total_episodes): obs, _ env.reset() goal obs[desired_goal] # 初始目标 episode_cache initialize_cache() # 第一步正常执行一条episode for t in range(max_steps): state obs[observation] achieved obs[achieved_goal] action policy(state, goal) exploration_noise() next_obs, reward, terminated, truncated, info env.step(action) # 存储原始transition episode_cache[states].append(state) episode_cache[achieved_goals].append(achieved) episode_cache[actions].append(action) episode_cache[next_states].append(next_obs[observation]) episode_cache[dones].append(terminated or truncated) obs next_obs if terminated or truncated: break # 第二步遍历轨迹构造HER样本 her_transitions [] T len(episode_cache[states]) for i in range(T): for _ in range(k): # k个hindsight目标 future_idx random.randint(i, T - 1) new_goal episode_cache[achieved_goals][future_idx] new_reward compute_reward( episode_cache[achieved_goals][i], new_goal ) her_transitions.append(build_transition(episode_cache, i, new_goal, new_reward)) # 第三步原始transition和HER transition一起入buffer replay_buffer.add(episode_cache_to_transitions(episode_cache)) replay_buffer.add(her_transitions) # 第四步从buffer里采样一批均匀数据更新critic和actor if len(replay_buffer) batch_size: update_critic_actor(replay_buffer.sample(batch_size))这段流程里最容易被忽略的是compute_reward这一步。HER替换目标之后务必用新目标重新计算reward绝对不能沿用原始目标下的reward。我见过新手直接复用原来的reward结果学出来的Q函数一塌糊涂因为同样的(s, a, s)配上不同的目标奖励却还是一样模型等于在学一个自相矛盾的目标。3.5 超参数参考我把实测有效的配置列成表我复现HERDDPG时用过一套很稳的组合直接拿过来改一改能省不少时间超参数参考值理由隐藏层[256, 256]ReLU两层MLU对Fetch这类环境足够表达actor_lr1e-3论文参考偏低一点更稳critic_lr1e-3与actor一致不必特意调低gamma0.98Fetch任务不太长0.99也能跑0.98我个人觉得Q更稳tau0.05target网络软更新系数0.05对这类任务够了batch_size256太小容易震荡太大训练慢k4每个transition生成4条HER样本兼顾多样性和效率buffer_size1e6越大越好尤其目标分布差异大时更能缓冲探索噪声0.2高斯随训练衰减到0.05前期必须多探索后期逐渐利用有一个参数必须提醒buffer里HER样本和原始样本的比例。我一般保持HER样本数量是原始样本的3到4倍也就是k3或4。如果k设成10甚至20buffer里几乎全是替换目标的样本会导致critic对原始分布的目标失去敏感性训练后期成功率卡住上不去。3.6 训练时看什么指标别只盯loss很多人习惯盯着critic loss和actor loss看但在稀疏奖励HER这个设定下loss曲线很容易骗人。critic loss下降只说明它把reward拟合得更准了不代表策略变好了actor loss下降也不代表成功率上升因为actor可能只是学会了朝buffer里的Hindsight目标运动而原始目标的转换还没学好。我自己的习惯是每个episode都统计两个数一个是当前episode的累计奖励另一个是当前episode是否达成原始目标success。然后把success_rate作为主指标把reward曲线作为辅助。在FetchPush上普通DDPG加上了HER之后我记得论文和我的复现里大概几百个episode左右就能看到success_rate明显抬头而纯DDPG跑几千episode可能还是0。4. 我在训练HER时踩过的坑4.1 替换目标后critic的输入分布被撕裂了这是HER落地时最容易踩、也最隐蔽的坑。原始目标的分布一般是环境初始化时指定的比如目标区域在房间的某个角落分布很集中而HER替换出的目标来自轨迹上的achieved goal分布是整个状态空间里实际被走过的区域。这两者的概率分布差异非常大。当critic既要面对角落附近的原始目标又要面对轨迹沿途的各种目标时它其实是在拟合两个不同分布下的Q值输入分布一旦错位价值函数很容易学偏。我当时遇到过的情况是训练到一半Q值偶尔会突然爆炸一下然后success_rate跟着往下掉。解决思路有三条确保buffer里保留一部分原始transition不要全被HER样本淹没。原始:HER保持1:3或1:4比较稳。对state和goal都做归一化特别是位置量纲差异大时更要做。Fetch类任务自带的范围还好换成自定义环境尤其注意。buffer容量不要设太小1e6起步给critic充分的样本去平滑两个分布之间的差异。4.2 任务目标不是末态位置时HER大概率失效HER能work的前提是目标可以用状态表示且达成目标等于状态向量与目标向量足够接近。但现实里有很多任务不满足这个条件。比如目标是一个速度值——让小车以5m/s的速度通过检测区这种目标虽然也能变成向量但HER的做法是把某个未来状态设为目标而未来状态里agent可能早就不在那个速度上了替换出来的样本语义就乱了。再比如任务是先到A点再到B点这种时序性的组合目标HER没法只靠替换目标来构造正样本因为它可能在替换过程中把目标的顺序信息给丢了。所以我的建议是动手前先问自己三个问题——achieved goal能不能从observation里直接读出来目标是否只依赖当前状态不依赖历史接近目标是否可以定义成状态空间上的距离只要有一个回答是否定就别硬套HER。这不是HER的bug是适用范围的问题。4.3 奖励函数写法-1/0比0/1稳定得多HER任务里最常见的奖励函数写法是def compute_reward(achieved_goal, desired_goal, threshold0.05): distance np.linalg.norm(achieved_goal - desired_goal) return -1.0 if distance threshold else 0.0这里我用的是-1/0而不是更直观的0/1。原因很简单在稀疏奖励下如果失败时奖励是0、成功时奖励是1那么Q值就是在估计成功概率值域在[0,1]之间critic会倾向于把Q值压小因为大多数样本都是失败的0。而用-1/0Q值天然变成预期代价critic会尽量把Q值往上修训练信号更稳定。当然0/1的写法在论文里也有人用不是绝对不能用但我试下来-1/0收敛更平滑Q值爆炸的概率小很多。4.4 复现中的常见问题速查表整理一下我在复现过程中遇到过的各种问题按现象分类做成速查表现象可能原因检查与修复success_rate长期为0k设成了0等于没开HER检查k值至少设到2探索噪声太小agent根本探不到新状态把初始噪声调到0.2以上reward阈值设得过于严格检查阈值Fetch类默认0.05上下比较合适obs和goal拼接错误打印obs形状确认维度分离Q值偶尔暴跳原始目标与HER目标分布差异过大增加buffer容量检查归一化reward尺度太大用-1/0而不是0/1训练中途突然崩坏buffer里HER占比过高降低k值或划一部分比例只留原始样本loss一直不降没有用新目标重新计算reward检查compute_reward调用逻辑critic输入漏掉了goal确认state和goal都拼接进Q网络换环境就不work新环境目标不可状态化重新评估HER适用性别死磕4.5 关于复现论文结果的一点实话很多文章会贴一张成功率冲到100%的曲线看起来特别漂亮。但我实际复现下来FetchPush到接近100%确实不难FetchPickAndPlace就要耐心很多中间成功率还可能反复横跳。这不是代码错了而是这个任务里抓取本身有接触不稳定的问题HER能解决奖励稀疏解决不了模拟器里的物理随机性。所以如果你复现时发现成功率在某个区间震荡不要第一反应怀疑自己写错了先把训练拉长把噪声衰减调慢一点很多时候只是欠训练。我自己跑这类实验的习惯是先跑一个短版本确认代码链路没毛病再挂一个长版本去睡觉第二天看结果比起看着tensorboard焦虑熬夜要健康得多。5. 说点掏心窝的实操体会HER这套方法我最初接触时觉得它简单得不像一篇经典论文的核心贡献说白了就是换个目标重新计算reward嘛。但用久了才明白这种视角转换背后是RL里一个特别重要的思维习惯不要浪费任何一条轨迹的信息尤其是看起来失败的轨迹。一条失败的轨迹并不是没有价值它只是在一个错误的目标下没有价值而已。换个角度去看它就是一片被错放位置的金矿。这个思路后来影响了我设计奖励函数的习惯。我现在接到一个新任务会先问一个问题这个任务的目标能不能用状态来表示如果能哪怕环境是一个极度稀疏的二元奖励我也有底因为HER可以在背后撑起训练如果不能我就老老实实去设计奖励塑形或改任务表征绝不硬套HER。最后再分享一个我一直在用的小技巧如果你的任务目标空间本身范围很大别刻意把初始目标分布限定在容易的区域。HER的future策略天然会从轨迹里采样未来状态当目标当原始目标很难时agent反而会在训练中先学会那些碰巧到达过的目标形成一条隐式的课程路径。相当于你一边训练一边让环境自己给你出题、自己给你批改这套组合拳在实战中比很多精心设计的课程学习方案都要好用。下次你的训练曲线又是一条死线不妨先别急着加奖励、改网络问自己一句那些失败经验是不是也能被当成成功来学