ARTICLE DETAIL

资讯详情

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

HER算法解析:用事后经验回放解决稀疏奖励强化学习难题

HER算法解析:用事后经验回放解决稀疏奖励强化学习难题 看到hindsight这个词我先说一句可能有点得罪人的话如果你把它当成心理学概念里的“事后诸葛”或者“复盘思维”那也就图一乐但在强化学习这个圈子里hindsight有一个分量极重、直接影响过机器人控制落地的算法身份——Hindsight Experience ReplayHER中文一般叫“事后经验回放”。这是2017年OpenAI那帮人做出来的东西核心思想就一句话别只盯着没完成的目标把“失败轨迹”换个目标重新解释立刻就能变成高质量训练数据。这篇东西我会从“为什么需要HER”讲起然后把它三种目标置换策略拆开揉碎再给出一套可以直接照着写的PyTorch实现最后把我自己在FetchReach这类稀疏奖励任务上跑出来的调参经验和踩坑记录都放出来。适合谁看如果你在做机械臂抓取、移动机器人导航这类稀疏奖励的强化学习任务或者你只是被reward shaping折磨到想骂人的研究生这篇文章应该能帮你省下至少一个月的试错时间。1. 为什么需要“后见之明”稀疏奖励下的学习困境1.1 从“考完对答案才恍然大悟”说起想象一下你参加一场考试试卷上有一道大题你完全没思路交卷之后对答案才发现——原来这题用到的公式你其实是会的只是当时没往那个方向想。如果让你重新考一次你很大概率能做出来因为你已经知道正确的“目标路径”是什么了。HER的核心直觉跟这个一模一样。智能体探索了半天没有碰到我们期望的目标但它在探索过程中其实“到达”了别的位置。这些位置虽然没有命中预设目标但并不意味着这些transition没价值——只要我们把“目标”这个概念从“预设的理想状态”替换成“它实际到达的状态”原本失败的样本立刻变成了一条成功经验。这就是“hindsight”在算法里的真正含义事后视角的重新标注比事前硬闯要高效得多。1.2 稀疏奖励的致命问题正样本密度几乎为0在稀疏奖励任务里环境只在“任务完成”那一瞬间给出奖励比如机械臂要抓取一个物体只有物体被精确抓到指定位置才返回1其余所有动作都是0。听起来很公平但对强化学习算法来说几乎是场灾难。举个具体例子FetchReach这类机械臂任务动作空间是4维或带夹爪的更高维目标位置是三维坐标判断成功的阈值通常是5厘米以内。假设每一步随机动作能让机械臂末端靠近目标5厘米的概率只有几十分之一那么一个50步的episode全程碰不到目标几乎是必然事件。更麻烦的是由于全程没有正奖励Q值网络的梯度完全没方向策略网络输什么都拿不到反馈训练就卡死了。很多人在这一步开始疯狂加reward shaping比如“离目标越近奖励越大”但shaping函数设计得好不好直接决定结果而且稍不留意就会让智能体学会“钻空子”——比如绕着目标画圈而不是真的去抓。我见过太多项目死在人工设计引导奖励上了费力不讨好。1.3 常见策略的局限以及HER破局的角度有人会说那用课程学习行不行让机械臂先学“靠近目标”再学“抓住目标”逐级递进。这个思路没问题但课程学习需要人为划分难度阶梯而且每个阶梯换任务时策略往往要重新适应。也有人用模仿学习采集专家演示数据来预热但演示数据成本高不说迁移到新目标时效果经常打折扣。HER走的是一条完全不同的路它不改变环境不精调奖励函数不引入课程只改变replay buffer里经验的组织方式。它把那些“失败的轨迹”通过目标重标定变成“成功的轨迹”等于把一个episode扩展出多条不同目标的有效样本。这种“自我课程”的机制不需要人工介入天然就解决了正样本稀疏的问题。2. HER核心原理拆解用失败经验反向学习2.1 目标重标定HER的唯一关键步骤先明确一点HER不是一个新的策略网络结构也不是新的损失函数它只修改replay buffer里样本的存储和采样方式。任何一个off-policy算法——DDPG、TD3、SAC——都可以直接套上HER使用因为它只影响“你拿什么数据去更新网络”。具体来说我们正常存一条transition长这样(state, action, reward, next_state, done, goal)其中goal是我们事先给定的目标。但这条transition没成功reward0。HER的做法是在这条transition所在的那一整条episode轨迹里挑一个“实际到达过的状态”当作新的目标goal然后把transition改写成(state, action, reward, next_state, done, goal)。由于这个新目标确实是轨迹上的真实状态我们知道在当前state下执行action之后至少能靠近甚至到达goal所以reward很可能就是1。一条原本毫无营养的失败数据就变成了一条“成功”数据。这还没完你可能会问如果智能体实际到达的状态五花八门用它们当目标训练出来的策略在面对真实目标时还有用吗答案是很有用因为现实世界里的目标通常也是三维空间里的任意点。你在训练时用各种“实际可达状态”当目标其实就是在学习一个覆盖整个目标空间的策略等到测试时给它一个具体目标它反而比只在单一目标上训练过的策略泛化能力更强。2.2 三种目标置换策略对比final、episode、future问题来了一个episode里有那么多时间步到底选哪个状态作为替代目标最合适HER论文给出了三种策略我将它们的差异整理成表格方便你一眼看明白。策略替代目标来源特点适用直觉final轨迹最后一个时间步的状态简单粗暴目标一定可达但可能离当前状态很远学习早期收敛偏慢结果导向只看最终结局episode从当前时间步之后的任意未来状态中均匀采样目标密度大样本丰富但如果采样到很近的目标策略可能倾向于“原地踏步”全程覆盖反正有机会到达future从当前时间步之后的k步内采样兼顾密度和难度目标既有挑战性又不至于遥不可及论文默认推荐近未来视角难度适中我在实际复现中最常用的是future策略原因其实很朴素final策略选的目标可能太远比如机械臂一开局乱甩最终停在了很远的地方那这个目标对当前action的“指导意义”就不大episode策略虽然全面但你有可能选到当前状态本身的附近造成大量“目标就在脚下”的简单样本让策略变得保守future策略把采样范围限制在接下来k步内目标既跟当前动作有因果关系又有一定难度学习信号最均衡。2.3 为什么HER能提升样本效率三条直觉解释第一正样本密度变高了。原本一个episode可能全部是0奖励现在通过重标定至少有一半以上的样本变成正样本Q网络的拟合目标不再是“永远为零”而是有了明确的梯度方向。第二单条轨迹的信息利用率变高了。一条长度为T的失败轨迹原始状态下只能提供T条“0奖励”样本HER重标定后你可以为其中每个时间步都生成一条目标不同的成功样本相当于把数据量放大了好几倍。第三目标空间被“自动覆盖”了。因为替代目标来自轨迹实际到达的状态随着exploration进行这些状态会逐渐铺满可达空间智能体等于在不断学习一个以“可达状态”为目标的最优策略之后再遇到新的目标点泛化自然比只见过单一目标的策略好。需要提醒的是HER在随机环境中的理论保证没有确定性环境那么完美但实践下来只要环境本身的随机扰动不是过于巨大HER的收益依然非常显著。论文里对此有形式化分析这里不展开工程上先跑起来是最重要的。2.4 适用边界HER不是什么任务都能救HER确实厉害但绝不是银弹。它成立的前提是“目标可以表示为状态空间中的一个可观测点”比如三维坐标、关节角。如果你的任务目标是“把红色物体放在蓝色盒子里”这类语义化目标或者目标依赖于无法从状态中直接读取的隐藏信息HER就有点力不从心——你没法从一个失败轨迹里挑出一个状态来充当这个语义目标。另外如果环境的状态包含大量与任务无关的噪声信息HER挑选替代目标时可能会把噪声信息也当作目标的一部分造成策略学到一些虚假的“成功特征”。我在实际落地中遇到这类问题时通常会对状态做一个目标相关的特征抽取只用抽取后的低维空间作为goal再做HER效果会稳定很多。3. 实操在PyTorch里实现一个HER-DDPG3.1 环境选择与整体思路这里我选用OpenAI Gym里的FetchReach-v1作为实验环境。这个任务的目标是控制7自由度机械臂把末端执行器移动到距离目标5厘米以内。环境自带稀疏奖励判定动作空间连续是HER论文的标准benchmark。选它的第二个原因是环境安装简单跑一个episode很快方便快速验证你的buffer实现是否正确。整体上我建议你先别急着把完整DDPG写出来而是先把HER的replay buffer写对。因为HER的一切都在buffer里网络结构完全可以借用你熟悉的任意actor-critic实现。下面这段代码就是用PyTorch风格写的一个可运行的EpisodeBuffer类核心逻辑和OpenAI baselines里的her实现一致性很高我加了详细的注释。3.2 核心代码EpisodeBuffer与HER采样逻辑import numpy as np from collections import deque class EpisodeBuffer: def __init__(self, capacity1000000, her_ratio0.8, future_k4): her_ratio: 采样时重标定目标的比例经验上0.8左右表现最佳 future_k: future策略中从当前时间步往后采样目标的时间窗口 self.capacity capacity self.her_ratio her_ratio self.future_k future_k # buffer里存的是一个个episode便于整条轨迹做目标重标定 self.episodes deque(maxlencapacity) # 下面这个列表用来缓存正在采集中的episode数据 self.current_episode [] def store_transition(self, obs, achieved_goal, desired_goal, action, reward, next_obs, next_achieved_goal, done): # 注意HER要求每次transition都要把achieved_goal一并存下来 self.current_episode.append({ obs: obs.copy(), achieved_goal: achieved_goal.copy(), desired_goal: desired_goal.copy(), action: action.copy(), reward: reward, next_obs: next_obs.copy(), next_achieved_goal: next_achieved_goal.copy(), done: done }) def end_episode(self): episode结束时调用把完整轨迹送入buffer self.episodes.append(list(self.current_episode)) self.current_episode [] def _future_goal_index(self, t, episode_len): # 从当前步之后采样注意下限是t1避免选到原地目标 upper min(t self.future_k 1, episode_len) return np.random.randint(t 1, upper) def sample(self, batch_size): # 首先随机选择batch_size个episode ep_indices np.random.choice(len(self.episodes), batch_size, replaceTrue) transitions [] for ep_idx in ep_indices: ep self.episodes[ep_idx] t np.random.randint(0, len(ep)) trans ep[t] # 按her_ratio概率做目标重标定 if np.random.rand() self.her_ratio: future_t self._future_goal_index(t, len(ep)) # 新目标 未来某时刻实际到达的状态 new_goal ep[future_t][achieved_goal] # 用新目标重算奖励 new_reward self._compute_reward( trans[achieved_goal], new_goal ) # 拷贝一份再修改避免污染原始episode trans dict(trans) trans[desired_goal] new_goal trans[reward] new_reward # 关键next transition的目标也要同步替换 trans[next_desired_goal] new_goal transitions.append(trans) return transitions staticmethod def _compute_reward(achieved_goal, desired_goal, threshold0.05): # 稀疏奖励距离小于阈值才算成功 dist np.linalg.norm(achieved_goal - desired_goal) return float(dist threshold)这段代码有三个细节我特别说明一下。第一new_goal是从achieved_goal里取的而不是从obs里取的虽然这两个在Fetch环境里有重叠但概念上一定区分开你的buffer里两者的含义必须明确。第二替换目标之后next时刻的desired_goal也要一并替换否则训练时critic网络会看到前后不一致的目标信息Q值会崩。第三_future_goal_index里我用的是t1作为时间下限这比包含当前步更稳定。有些开源实现会从t开始采样但那样会增加一步“目标已经达成”的简单样本对学习有干扰。3.3 训练流程怎么接DDPG加一个buffer循环HER本身不改变DDPG的更新公式所以训练流程很标准。每个episode结束时调用end_episode然后从buffer里多次采样更新网络。我常用的策略是每个episode结束后做40次更新batch_size取256这样数据利用率高训练曲线也更平滑。完整的DDPG代码太长我这里只贴关键循环episode_buffer EpisodeBuffer(her_ratio0.8, future_k4) for episode in range(max_episodes): obs env.reset() episode_reward 0 done False while not done: action policy(obs) exploration_noise() next_obs, reward, done, info env.step(action) # 从info里拿achieved_goal和desired_goal observed_achieved_goal info[achieved_goal] observed_desired_goal info[desired_goal] episode_buffer.store_transition( obs, observed_achieved_goal, observed_desired_goal, action, reward, next_obs, info[achieved_goal], done ) obs next_obs episode_buffer.end_episode() # 用HER采样更新actor-critic for _ in range(40): batch episode_buffer.sample(256) update_actor_critic(batch) # 这里接你熟悉的DDPG/TD3/SAC更新逻辑注意info[achieved_goal]这个来源。FetchReach环境在step()返回的info字典里直接给出了机械臂末端当前的实际位置这是做HER的必要条件。如果你的自定义环境没有这个字段那就需要自己从状态里抽取位置信息构造achieved_goal。3.4 训练曲线长什么样HER的“顿悟时刻”跑过HER的人都知道它的学习曲线跟普通策略差别很大。前一两百个episode成功率几乎贴着0你会开始怀疑自己是不是哪里写错了。但就在某个节点之后成功率会像台阶一样突然上涨我跑FetchReach时大概在400个episode左右从0跳到了接近70%再往后逐步逼近100%。这个“顿悟时刻”是因为前面那些episode堆积了大量重标定后的成功样本数量达到一定阈值后critic终于开始给出有意义的梯度策略迅速从“乱动”切换到“知道往哪动”。如果你跑了几百个episode曲线还是一条直线那八成是代码问题而不是HER失效。下面这一节我列的几类问题基本覆盖了我在这个阶段踩过的所有坑。4. 参数调优与踩坑实录三个参数和四类问题4.1 三个关键参数怎么看第一个是her_ratio也就是目标重标定的比例。我试过0.5、0.8、0.90.8是个很稳的默认值。比例太高会让策略过于偏重“事后成功”样本对原始目标本身的学习不足太低则又回到稀疏奖励的老路上提升不明显。如果你在复杂任务里觉得原始目标依赖过重适当往0.9调也可以但别超过0.95。第二个是future_k这个参数控制替代目标和当前状态之间的距离远近。论文默认是4但如果你发现训练后期成功率卡住、上不去很可能是目标选得太近策略只在“附近目标”上有效没有学会远距离长程规划。这时候把k加大到8或16强制算法从更远的目标学起通常有奇效。反过来如果早期完全不涨说明目标太难先降回2看看。第三个是buffer容量。HER非常吃buffer因为它要缓存整条episode做目标重标定容量太小会导致旧经验被过早丢弃。我在FetchReach上用的是100万容量跑起来毫无压力。如果你的机器内存紧张至少也要给20万以上否则样本多样性不够训练容易早期停摆。4.2 训练崩溃类问题的排查路径问题一训练曲线上来就NaN。先查target Q网络是否用了.detach()再查reward计算里有没有除零或log(0)最后查归一化——如果obs和goal的量纲差距太大一定要分别归一化到[-1,1]DDPG对未归一化输入极其敏感。问题二Q值震荡但成功率不动。这通常是buffer里新旧经验混在一起目标替换后reward重构不完全常见表现就是Q越来越离谱。检查一下我在3.2节强调的“next_desired_goal同步替换”这个坑我至少踩过三次每次都是训练到一半才发现Q值完全对不上。问题三HER采样里选到了当前步本身作为目标。这就是future索引边界问题如果你发现训练早期成功率反而比后期还高或者策略总在原地不动赶紧检查np.random.randint的上界和下界。记住替代目标必须是当前时间步之后的实际到达状态选到当前时刻等于告诉策略“你不动也能成功”。4.3 进阶组合与落地建议我在实际项目中很少单独只跑一个DDPGHER更常用的组合是SACHERSAC的熵项能维持更稳定的exploration配合HER的样本重标定在FetchPickAndPlace这种更复杂的任务上收敛速度明显比DDPG快。另外如果内存允许可以在HER基础上叠加PER优先经验回放用TD-error给重标定后的样本排序采样能再挤出一点效率但PER的实现复杂度较高建议先把纯HER跑熟再加。还有一个很多人忽略的细节训练结束后评估时不要用训练时带噪声的策略去跑要切换到确定性动作actor直接输出不加热噪声。我见过不少项目在训练曲线上好看一评估就拉胯结果发现是评估代码忘了去噪。问题四评估成功率高但部署到真机就不行。这个锅HER不背通常是sim2real的domain gap。解决思路是域随机化随机化机械臂连杆质量、摩擦力甚至目标位置的传感器噪声让策略在仿真里见过更宽的分布。HER在重标定目标时天然具备目标多样性配合域随机化部署成功率会高很多。5. 一点个人体会以及建议的复现路线想起最开始复现HER时我在采样函数里把new_goal写了进去但忘了同步改next时刻transition里的目标字段结果Q值曲线抖得跟心电图一样还以为是网络结构的问题白白排查了两天。后来把所有代码推到重来把transition统一成一个带desired_goal和achieved_goal的字典结构一切瞬间就正常了。所以如果你也打算动手跑HER我给你的建议是先把buffer数据结构设计清楚再谈网络、再调参这个顺序一定不要反过来。复现路线我也顺手串一下先装OpenAI Gym和Fetch环境用我最上面贴的EpisodeBuffer配合一套现成的DDPG实现跑通FetchReach。成功后再换FetchPush、FetchPickAndPlace把future_k从4调到8对比一下曲线差异。这一轮走完你对HER的理解会比读十篇论文都扎实。说到底让智能体学会“事后聪明”其实是把人类复盘思维中最高效的那部分搬进了算法而它最迷人的地方在于不需要任何外挂知识数据本身就能带来突破。
返回列表