强化学习稀疏奖励实战:HER事后经验回放原理与代码全解
“hindsight”这个词在中文语境里就是“后见之明”的意思但在强化学习圈子里一提到它大家脑子里冒出来的基本都是同一篇论文2017年OpenAI那篇《Hindsight Experience Replay》。这几年我做机器人抓取和稀疏奖励任务HER几乎是绕不开的标配甚至可以说是这类问题里性价比最高的算法改进——它不改变网络结构、不依赖额外奖励信号只需要在经验回放的时候多做一步“事后重标注”就能让一个在稀疏奖励下完全学不动的agent开始有实质进步。这篇东西我不打算复述论文公式而是以我的实操经验为主线从思路拆解、核心细节、可抄作业的代码结构、踩坑记录到能用到哪些场景一次讲清楚。适合正在做稀疏奖励RL的工程师、研究生也适合只听说过HER名字、想看它到底解决什么问题的同学。1. 内容整体设计与思路拆解1.1 稀疏奖励难题HER为什么值得学先聊一个最根本的问题为什么稀疏奖励会让强化学习这么难假设你的agent在一个100步的episode里探索只有最终状态恰好落在目标区域才给1奖励其他所有步骤都是0。这意味着agent在绝大部分时间里收到的都是一条“全零奖励”的轨迹无论它做了什么反馈都一样。对于基于梯度更新的策略网络来说全零奖励等于没有梯度信号策略更新基本退化成随机游走。哪怕agent真的碰巧到达了一次目标一条轨迹上的成功率也只有1/100对于需要大量样本来估计Q值的深度网络来说这点信号连杯水车薪都算不上。我之前做的机械臂推积木任务就是典型例子。机械臂要从初始位置把一块小积木推到桌面上的目标点episode长度是50步目标区域半径0.05米。随机策略下积木最终落在目标区域内的概率大概只有百分之零点几。用DDPG裸训跑了两百万步成功率一直趴在0%左右纹丝不动。后来换成HER六十万步左右成功率就上了60%。同一个任务、同一个网络差别只在于经验回放的方式。HER解决的核心问题就是不依赖人为设计的稠密奖励也不依赖“偶尔踩中目标”的运气而是从失败轨迹中挖掘出可用的学习信号。这就是它最值钱的地方。1.2 从失败中学习的直觉事后之明HER的直觉可以用一个很生活化的类比说清楚。想象你在练投篮十投九不中每一次球都差了一点。如果教练只在你投中时才给你反馈那你几乎得不到任何指导。但一个聪明的教练会在你每次出手后告诉你“你看这次球偏右了下次往左一点。”这个反馈不依赖于“进球”这个最终结果而是依赖于“球实际落点”和“篮筐位置”之间的差距。HER做的事情就是把“球实际落点”当作一个虚拟目标然后重新审视这条轨迹虽然你没能到达真正的目标g但你的确到达了某个状态s。如果我们把目标改成s那么这条轨迹上最后一步就是成功的。换句话说任何一条轨迹在事后看来都可以被解释为“成功达成某个目标的轨迹”。具体到强化学习的数据结构上这个思想落地得很干净。一条transition原本是四元组(s, a, r, s)在goal-conditioned设定下还要带上目标g变成(s, g, a, r, s)。HER的做法是对于一条没达成原目标g的transition额外采一个“事后目标”g这个g通常取当前episode未来某个时刻的实际状态然后重新计算奖励r reward(s, g)。因为g就是某个实际达到过的状态所以r大概率是1。这样一来一条原本全是零奖励的轨迹经过重标注之后变成了一批上面带正样本的轨迹。这就是“后见之明”的含义目标不是事先给定的而是事后根据实际发生的结果反推出来的。agent从每一次“失败”里都能拿到正反馈而不是干瞪眼等那一次成功。1.3 与其他方案对比选择HER的理由在HER出现之前人们对付稀疏奖励的主流手段无非是那么几种我简单总结一下大家对比着看方案需要的额外投入主要风险我的评价奖励塑形Reward Shaping大量领域知识手动设计中间奖励函数容易引入局部最优设计者很容易“教错”agent能用但可维护性差换任务等于重做课程学习Curriculum Learning手动设计任务难度序列每阶段调参阶段切换时机难把握训练不稳定有效但工程成本高不适合快速验证想法内在好奇心ICM/RND额外训练一个预测网络调好奇权重探索不稳定agent容易被“新奇”带偏适合纯探索场景和稀疏目标任务并不是绝配HER只改经验回放逻辑不需要额外网络只能用于off-policy算法on-policy用不了在我看来是性价比最高的第一步尝试选择HER的核心逻辑很简单它不碰你的reward function不依赖任何领域知识本质上是改变了“数据如何被使用”的方式而不是“数据如何产生”的方式。这也意味着它和绝大多数off-policy算法DDPG、TD3、SAC、DQN是即插即用的。你可以在现有代码上以很小的成本加上HER然后立刻看到效果。唯一的限制是它必须依赖经验回放因为重标注是对历史数据做操作on-policy算法没有这个环节所以用不了。2. 核心细节解析与实操要点2.1 核心机制目标重标注的三要素HER的实现看起来简单但要真正跑出论文里的效果有几个细节必须在代码里落实到位。第一明确“目标”到底是什么。在goal-conditioned RL里目标g可以是任何你想要agent达到的状态比如机械臂末端的位置、积木的目标位置、迷宫里的某个坐标。绝大多数情况下目标就是从状态s里提取出来的一个子向量。这里有一个很关键的约定你最好让环境同时返回observation完整状态和achieved_goal实际达成的目标相关状态。OpenAI Gym的Fetch系列环境就是这样的标准结构obs { observation: full_state, achieved_goal: actual_object_position, desired_goal: target_object_position, }我强烈建议自定义环境也按这个格式来。HER需要在每条transition上拿到“实际达成的目标”achieved_goal如果你的状态表示里没有显式暴露这个字段就得在代码里到处做索引提取既容易出错又不通用。第二奖励函数必须只依赖“最终状态”和“目标”。典型的稀疏奖励就是一个阈值判断def sparse_reward(achieved_goal, desired_goal, threshold0.05): return 1.0 if np.linalg.norm(achieved_goal - desired_goal) threshold else 0.0这个threshold选择很讲究。太大agent容易“蒙混过关”学到的是很粗糙的策略太小即使HER做了重标注正样本仍然极为稀少。我之前做过一组对比实验同一个FetchPickAndPlace任务threshold从0.05调到0.01训练收敛时间差不多翻了一倍。所以我的建议是先从环境本身的物理尺度出发取一个“人类肉眼觉得已经到位了”的距离通常0.03到0.1之间比较稳妥。第三重标注后要重新计算目标拼接向量。HER重标注的不是奖励这一个数字而是把整条transition的“目标部分”替换成g然后重新计算奖励。也就是说新transition写入buffer时形状应该是(s||g, a, r, s||g)。这里的state和goal要做拼接而不是直接用原始state。这个细节看起来不起眼但如果网络输入处理写错了后面全盘皆错而且这种错误很难通过看loss发现。2.2 目标重标注策略final与future怎么选HER论文里对比了几种采样“事后目标”的策略我用实际经验给你排个序策略名称做法实际效果final只用episode最后一个状态作为虚拟目标能用但每个episode只能产生一个额外虚拟目标数据利用效率低random从replay buffer随机采样一个状态作为目标效果很差虚拟目标和当前transition完全无关等于是噪声episode从当前episode里随机采样一个状态比random好步长不一定关联future从当前transition的未来状态里随机采样实测效果最好论文里也是冠军future策略代码如下这是整个HER最核心的采样逻辑def relabel_transition(episode, idx, k_future4): episode: list of transitions transition { obs: obs_vector, action: a, reward: r, next_obs: next_obs_vector, achieved_goal: achieved, desired_goal: desired } new_transitions [] horizon len(episode) for _ in range(k_future): future_idx np.random.randint(idx 1, horizon 1) future_goal episode[future_idx][achieved_goal] new_reward sparse_reward(episode[idx][next_achieved_goal], future_goal) new_transition { obs: np.concatenate([episode[idx][obs], future_goal]), action: episode[idx][action], reward: new_reward, next_obs: np.concatenate([episode[idx][next_obs], future_goal]), } new_transitions.append(new_transition) return new_transitionsfuture好在哪里它保证了“虚拟目标”一定在当前transition的未来状态里这意味着从s到g这一段轨迹是真实存在过的不是凭空捏造的。agent在s这个状态下确实见过未来会到达的那个状态所以这条正样本是有物理依据的。相比之下random策略采出来的目标可能和当前状态完全不搭边agent根本不知道该怎么做学到的就是纯粹的噪声。关于k_future这个参数论文和OpenAI的baseline实现里通常取4。它的含义是每条原始transition额外生成4条重标注数据。k_future越大buffer里的正样本密度越高但也会让“虚拟目标”数据占比过大压缩真实目标数据的空间。实测下来4是个很稳的甜点值新手直接照抄就行不需要一开始就调。2.3 实现中的边界与禁忌这个部分是我真正想让你少走弯路的内容全都是血泪教训。第一个禁忌不要把her_ratio设成1.0。her_ratio指的是每条transition有80%的概率额外生成一条虚拟目标数据。有人觉得“既然虚拟目标这么好那就每条都重标注呗”。实际跑下来你会发现当buffer里充满了虚拟目标数据后agent会越来越擅长“到达任意状态”这个泛化任务但会逐渐遗忘最初的指定目标g。成功率会在某个阶段开始回落越训越差。我遇到过一次这种情况训练到后期策略变成“哪里都不去原地乱转”因为重标注数据让agent觉得任何状态都是可接受的结果。合理的做法是her_ratio取0.8保留20%的真实目标数据让agent始终有一部分经历在追逐真正的目标。第二个禁忌未来采样越界。future采样时future_idx必须大于当前idx并且不能超过episode长度。很多人写代码时用np.random.randint(idx 1, horizon)这个右边界是开区间取不到horizon。也就是说最后一条transition永远不会有未来状态可用——如果未来是同一个状态倒也不是完全不行但会少掉一些有效正样本。正确的写法应该是np.random.randint(idx 1, horizon 1)。这个细节不仔细看很容易漏掉但它直接影响最后一个时间步的重标注质量。第三个禁忌忘记对虚拟目标做和state一致的归一化。如果状态向量里的不同维度量纲差异极大——比如位置坐标的范围是0到1而速度的范围是-5到5——那么训练前你需要对整个拼接后的输入(s||g)做归一化。很多人在归一化state时只处理了原始stategoal是后拼进去的归一化因子根本没覆盖到goal部分结果就是网络输入分布不一致训练极不稳定。这个问题我排查了整整一天最后发现是归一化模块里的shape写死了。第四个禁忌阈值设太小导致虚拟目标也稀疏。前面提到过如果threshold设得比环境随机扰动还小那么“虚拟目标未来某个状态”这条重标注数据的奖励仍然是0HER就退化成了普通回放没有任何作用。写代码前先在随机策略下跑一批episode看看“如果以最终状态为目标最后一步的奖励是不是1”如果是1说明阈值合格如果连这个都是0说明threshold比环境噪声还小赶紧调大。3. 实操过程与核心环节实现3.1 环境准备从自定义环境到OpenAI标准格式我先用一个最简化的2D点导航任务来演示HER的完整接入流程。假设agent是一个可以在二维平面上移动的点目标是到达某个目标位置。状态是(x, y, vx, vy)目标也是(x, y)。奖励是稀疏的到达目标半径0.1以内的位置给1否则给0。这个环境虽然简单但它具备HER需要的所有要素可提取的achieved_goal、明确的目标空间、稀疏奖励。环境返回的observation我建议按照OpenAI Fetch系列的标准来设计class PointNavEnv: def reset(self): self.state np.array([0.0, 0.0, 0.0, 0.0]) self.goal np.array([1.0, 1.0]) return self._get_obs() def _get_obs(self): achieved_goal self.state[:2] # 位置 obs { observation: self.state.copy(), achieved_goal: achieved_goal.copy(), desired_goal: self.goal.copy(), } return obs def step(self, action): # 动力学更新省略 reward 1.0 if np.linalg.norm(self.state[:2] - self.goal) 0.1 else 0.0 # ... return obs, reward, done, info这个环境本身简单到不值得用HER但它适合用来验证HER代码逻辑是否正确。我建议你先在这个上面跑通再迁移到复杂环境。3.2 HER封装器与Replay Buffer实现接下来是最核心的部分HERReplayBuffer。它和非HER buffer最大的区别在于普通buffer以“条”为单位写入transition而HER buffer以“episode”为单位写入然后在episode结束后统一做重标注。import numpy as np from collections import deque class HERReplayBuffer: def __init__(self, capacity100000, k_future4, her_ratio0.8, threshold0.1): self.capacity capacity self.k_future k_future self.her_ratio her_ratio self.threshold threshold self.buffer deque(maxlencapacity) self.episode_buffer [] def add_transition(self, transition): self.episode_buffer.append(transition) def end_episode(self): episode self.episode_buffer horizon len(episode) for idx in range(horizon): t episode[idx] # 原始transition直接进buffer self.buffer.append({ obs: np.concatenate([t[obs], t[desired_goal]]), action: t[action], reward: t[reward], next_obs: np.concatenate([t[next_obs], t[desired_goal]]), }) # 以her_ratio概率重标注 if np.random.rand() self.her_ratio: for _ in range(self.k_future): future_idx np.random.randint(idx 1, horizon 1) future_goal episode[future_idx][achieved_goal] new_reward 1.0 if np.linalg.norm( t[next_achieved_goal] - future_goal ) self.threshold else 0.0 self.buffer.append({ obs: np.concatenate([t[obs], future_goal]), action: t[action], reward: new_reward, next_obs: np.concatenate([t[next_obs], future_goal]), }) self.episode_buffer.clear() def sample(self, batch_size): batch np.random.choice(len(self.buffer), batch_size, replaceFalse) # 按batch索引收集数据并堆叠 # 具体堆叠逻辑略得到 obs_batch, action_batch, reward_batch, next_obs_batch return obs_batch, action_batch, reward_batch, next_obs_batch注意几个关键点第一原始transition和重标注transition都进buffer不是只进重标注后的版本。这对应前面说的her_ratio保证真实目标数据占比大约20%。第二重标注发生在end_episode时而不是采样时。这样写的好处是未来状态在episode结束时全部已知采样逻辑简单而且训练过程中buffer里的数据格式统一。第三achieved_goal必须存在episode的每条transition里。我建议在环境step函数里就把next_achieved_goal一并返回这样buffer不需要额外计算。3.3 训练主循环与网络输入设计HER本身不改变算法主循环你仍然可以用SAC或TD3。区别在于网络输入actor和critic的输入都是state和goal的拼接向量。这里需要特别注意一点——不是所有“状态”都需要和goal拼接。以FetchReach为例observation里包含了机械臂各关节角度、末端位置、物体位置等信息。而desired_goal就是目标位置。你需要在原始state和goal之间选择一个合理的拼接方式。最简单的做法是做向量拼接obs_full np.concatenate([state, desired_goal])。这个拼接在训练主循环里完成# 伪代码 for episode in range(total_episodes): obs env.reset() episode_transitions [] done False while not done: state obs[observation] goal obs[desired_goal] action actor(np.concatenate([state, goal])) next_obs, reward, done, _ env.step(action) episode_transitions.append({ obs: state, action: action, reward: reward, next_obs: next_obs[observation], achieved_goal: obs[achieved_goal], next_achieved_goal: next_obs[achieved_goal], desired_goal: goal, }) obs next_obs her_buffer.add_episode(episode_transitions) # 采样更新SAC for _ in range(40): batch her_buffer.sample(256) # 标准的SAC或TD3更新公式有些实现会把joint angle和goal分开做特殊编码比如对goal单独过一个embedding层。但在大多数连续控制任务中直接拼接效果就已经很好了。如果你发现训练不稳定可以优先怀疑其他环节拼接方式本身不是瓶颈。关于每episode更新次数这里有个经验值HER buffer里每个episode结束时会产生大量重标注数据所以训练循环往往是每个episode结束后“集中更新”一批而不是每步更新。我常用的配置是每个episode更新40次critic、20次actor。你也可以每步更新效果差距不大但集中更新能让梯度方向更稳定。3.4 关键参数配置一览我把一组在多个任务上都验证有效的参数贴出来方便直接抄作业参数推荐值说明k_future4每条transition额外生成的重标注样本数her_ratio0.8重标注概率别贪心调成1.0threshold0.05-0.1根据环境奖励判定阈值需要环境噪声小于它replay buffer容量1e6HER会产生更多数据buffer不宜太小batch size256比常规RL稍大因为正负样本混合critic更新/actor更新比2:1或4:1HER对Q值准确性要求更高学习率3e-4SAC/TD3的默认值通常够用episode长度50-100太短则重标注信息量少太长则学习慢4. 常见问题与排查技巧实录4.1 现象训练了几十万步成功率始终为0这是最让人崩溃的情况因为loss还在下降看起来一切正常就是学不会。我的排查优先级是这样的先检查buffer里的奖励分布。在训练初期跑几个episode后打印buffer中replay数据的reward分布看看有多少比例是1。如果全零问题几乎一定出在重标注环节要么是achieved_goal没有正确从环境返回要么是threshold比环境噪声还小要么是future_idx采样越界导致重标注目标都是旧状态。这里可以直接在episode结束后手动模拟一次重标注打印新奖励看看是不是有1这个操作重复两次就能定位问题。其次检查网络输入。很多人在拼接state和goal时写反了维度或者归一化只做了一半。一个很有效的验证方法找一个已知能成功的transition用重标注后的输入去算critic输出如果Q值预估明显偏低那大概率是输入分布出了问题。4.2 现象TensoBoard里TD loss一直接近0但策略不动这种情况通常不是HER的问题而是奖励信号确实全零——如果你在HER buffer里能看到正样本但TD loss还是零可能是目标网络和当前网络太接近导致Q值梯度很小。解决方法是调整SAC的soft update参数tau从默认的0.005调大到0.01让critic更新更“激进”一些。还有一个可能her_ratio设得太低比如说0.2那么buffer里80%都是原始全零数据正样本不够agent当然学不动。我建议开跑前检查一下buffer里正样本的真实占比理想情况下应该有30%-50%的重标注正样本。4.3 现象前期有效果后期成功率反而下降这个我前面提到过罪魁祸首通常是her_ratio太高。当一条episode的重标注数据量远超原始数据时agent会逐渐“忘了”原始目标变成一个状态空间里的漫游者。我遇到过一次训练到300万步时成功率从60%掉到20%。解决方法是动态降低her_ratio训练前半段用0.8后半段降到0.4。你也可以直接用一个简单的规则每训练一定步数让重标注概率乘一个衰减系数。这个做法的本质是前期靠虚拟目标引导探索后期逐渐把注意力拉回真实目标。另外一个相关原因是k_future太大。重标注数据过多虽然正样本密度上去了但每个虚拟目标都和当前transition的关联度变弱样本质量下降。k_future从4调到8不一定更好我试过反而会让收敛变慢。4.4 问题速查表症状可能原因快速验证方法解决方案成功率保持0reward全零打印buffer正样本比例检查threshold、achieved_goal、采样边界Loss下降但策略不动正样本占比过低统计buffer里reward1的占比调大her_ratio或k_future前升后降虚拟目标数据过多看buffer里虚拟数据占比动态衰减her_ratio训练波动剧烈归一化不规范检查拼接后输入分布做统一归一化最终精度不够threshold太大看最终轨迹离目标多远从0.1逐步缩到0.034.5 一个我每次开跑前必做的检查在正式训练前我会先跑100条随机策略的episode然后手动算一个指标如果按照future策略做重标注这批随机episode里有多少比例的最后一步transition会被重标注成奖励1。这个比例如果低于10%说明环境里正样本密度太低threshold或者episode长度设定有问题。如果高于60%说明任务其实没那么难可能不需要HER用普通buffer也能跑通。这个检查花不了5分钟但能让我提前发现那些“训练到一半才发现环境定义有问题”的悲剧。5. 扩展与应用场景分析5.1 机器人操控HER的主战场HER最早就是在机器人操控任务上验证的FetchReach、FetchPush、FetchPickAndPlace、FetchSlide这四个环境至今仍是评估多目标RL算法的标准benchmark。这些任务有一个共同特征奖励稀疏、目标明确、状态空间高维且连续。机械臂要完成“把物体推到指定位置”这个任务如果只在最后一步判定是否成功随机探索几乎没有可能踩中目标。在真实机器人上HER还有个额外的好处它天然适配模仿学习和示教数据。你可以把人类示教产生的轨迹当作“已完成的episode”然后用HER重标注给agent学习相当于从“虽然没完成任务目标但确实做出了一串有效动作”的示范中提取价值。这一点在sim-to-real迁移中很实用因为真实机器人很难像仿真环境那样大量试错而HER能把每一条真实轨迹的利用率放大好几倍。我做过一个真实的抓取实验用UR5机械臂夹取桌面上的随机物体目标位置由用户在相机图像上指定。仿真训练用HER部署到真机时只需要做简单的域随机化成功率从直接部署的30%出头上到接近70%。HER虽然不是唯一功臣但它是整个算法链路里最关键的一环——没有它仿真训练根本不收敛。5.2 跨领域迁移从RL到数据挖掘HER的思想其实可以脱离强化学习框架单独存在。凡是“有明确目标、但反馈极其稀疏”的问题都可以借用“事后重标注”的思路。比如推荐系统里的多步交互优化用户的完整购买旅程很长只有最后下单才是唯一正反馈。如果每次交互都以“最终是否下单”作为奖励绝大多数路径都是零信号。你完全可以借鉴HER的思路把“用户实际点了某个商品”这个中间状态当作虚拟目标去学习让模型从历史失败路径中也能学到“什么样的动作序列能引导用户进入下一个状态”。再比如对话系统里如果目标是“让用户满意”而满意度只能在整段对话结束后打分那中间每一轮都拿不到有效信号。把实际发生的对话转折当作虚拟目标来学习从“这句话确实引导用户继续聊下去了”这个事实中提取正信号本质上就是HER在非RL场景的另一种表达。我个人的观点是HER最大的价值不在于它是个“强化学习算法”而在于它提供了一种处理稀疏反馈的通用方法论把“实际发生了什么”当作“应该发生什么”的替代信号来学习。这个思路在数据驱动的各种场景里都值得试一下。5.3 与后续工作的结合HER之后出现了很多衍生工作比如把HER和课程学习结合先用easy goal训练再用hard goal微调再比如把HER和meta-RL结合让agent学会“自动选择虚拟目标”而不是随机采样未来状态。这些工作各有各的优势但对大多数人来说先在标准HER上跑通、掌握手感再去接触变体才是最稳的路径。如果你正在做一个reward稀疏、目标明确、数据可以离线复用的RL任务我建议你把HER当作第一个对照基线。它实现成本低、稳定性好、几乎不可能让结果比不用它更差。我自己现在做新任务时已经默认把SACHER当成起点配置再根据具体问题叠加其他改进。结尾最后分享一个我自己的小习惯。每次在自定义环境里准备用HER之前我都会写一个十来行的“假目标检查”脚本跑一堆随机episode打印分别用final和future策略重标注后正样本到底占到多少比例。这个数字能告诉我两件事——阈值设得合不合理以及这个任务到底值不值得上HER。看起来是个很简单的检查但它帮我省掉了好几次“跑了三百万步才发现环境定义有问题”的尴尬实验。还有一个体会想多说一句。接触HER时间长了你会发现它真正有意思的地方不在算法本身而在它对待失败的态度——它不回避失败而是把每一次失败重新解释成一次成功经验然后从里面提取有用的信息。这个思路在算法之外也同样值得琢磨。训练过程中我经常想很多事之所以学不会不是因为失败太多而是因为一直盯着那个没达到的目标忘了看自己到底走到了哪里。HER换个目标再看数据事情就通了。技术上是这样道理上也差不多。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →