ARTICLE DETAIL

资讯详情

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

HER事后经验回放:稀疏奖励强化学习的目标重标注实战指南

HER事后经验回放:稀疏奖励强化学习的目标重标注实战指南 看到“hindsight”这个词做过强化学习的老哥们可能最先想到的是“事后诸葛亮”这个日常翻译。但在RL圈子里这个词背后站着一个相当能打的算法——Hindsight Experience Replay事后经验回放简称HER。我第一次认真啃这个算法的时候正好卡在一个机械臂抓取项目上奖励函数怎么设计都不收敛靠HER直接盘活了整个训练过程。这篇文章就围绕这个项目把事后经验回放这整套东西掰开揉碎讲清楚包括它解决了什么问题、核心机制怎么生效、代码怎么落地以及我在实操中踩过的几个坑。适合正在搞稀疏奖励任务、对Off-Policy算法有基础、但被奖励设计折磨到想骂人的朋友。1. 什么是hindsight从“事后诸葛亮”到一项正经算法1.1 稀疏奖励环境下RL为什么这么难先说句实在话强化学习最烦人的地方不是网络结构设计而是奖励函数。环境复杂一点之后你给智能体设的奖励一旦不够密集它基本上就是原地打转。想象一下你让一个小孩在完全黑暗的房间里找一把钥匙唯一反馈是“找到钥匙才算成功”其他任何动作都没有提示。小孩只能瞎摸大概率摸到天荒地老也找不到。这就是稀疏奖励问题。我之前做机械臂抓取的时候就是这个感觉。机械臂要从桌面抓一个方块放到目标位置初始状态和目标位置相距几厘米但智能体每一步的奖励几乎全是0只有在精确到达目标时才拿到正反馈。训练了20万步成功率还是0奖励曲线一条直线。这种场景下任何基于梯度的更新都很难生效因为样本里几乎没有正向信号可供学习。业内主流的应对方案无非这么几个一是手动设计密集奖励比如按距离给一个负奖励离得越近负得越少。但人工搓出来的奖励函数往往带偏见智能体很容易钻空子比如“手够近但没抓稳”也能骗到高分。二是用课程学习从容易的目标逐步过渡到难目标但任务一多课程本身又是一门玄学。三是用模仿学习需要有专家数据很多场景下根本拿不到。HER走的是第四条路不修奖励不换任务而是改变“经验本身”把失败的轨迹重新解释成成功轨迹。思路清奇但又非常合理。1.2 核心思路目标重标注怎么“变废为宝”HER的思想用一个词概括就是“重标记”。它不把那些没达到目标的数据当作垃圾丢掉而是换个角度去看虽然没达到我指定的目标但至少到达了某个位置。那我干脆把那个位置当作这次的目标来看待这条轨迹不就变成了一条“成功轨迹”了吗这就是典型的事后审视。还是拿机械臂抓取举例。假设我设定的目标是“把方块放到桌面的右上角”智能体实际操作的结果是“方块被推到了右下角”。按原始奖励函数这条轨迹是失败的奖励全0。HER的做法是把目标从“右上角”改成“右下角”然后重新计算奖励。因为最终方块确实到了右下角所以按新目标来看这条轨迹的每一步都能被解释为“朝目标靠近”尤其是最后一步直接获得成功奖励。再把这个目标重写过的轨迹丢进经验回放池当成有效经验去训练。这么一来即使是完全随机的探索也能不断产生带有正向奖励的样本。经验池里成功数据的比例大幅提升价值网络和策略网络也有了稳定梯度。所谓“失败是成功之母”在HER这里变成了一句工程方法论每条失败轨迹都能被回收成一条合成成功轨迹。也正是因为这种“回过头来重新解释”的思路项目代号才直接用了hindsight这个词。1.3 项目定位与适用范围在做完那个机械臂项目之后我把HER整理成了一个通用的训练模块后续好几个稀疏奖励任务都复用了同一套逻辑。这个项目本身定位很明确不是要替代底层强化学习算法而是作为一个经验回放层的增强组件直接对接DDPG、TD3、SAC这类Off-Policy算法。使用时你只需要保证环境中能获取到“实际已达成的目标”这个字段就能在几乎不改动原有训练代码的前提下把稀疏奖励任务练起来。这里有个关键前提一定得是Off-Policy算法。因为在HER里经验会被重新标注后反复用于更新这天然要求算法能够使用历史数据。PPO这类On-Policy算法跑一轮就把数据扔掉了HER根本发挥不了作用。另外对于完全随机、毫无记忆力的奖励设计HER也不是万能药它只能缓解“奖励稀疏”解决不了“奖励完全错误”。不过对于大多数机械控制、导航、抓取类任务HER基本是首选组件没有之一。2. 核心机制拆解目标重标注为什么成立2.1 状态、目标、已达成目标三者怎么组织要动手实现HER首先得理解它依赖的数据结构。普通强化学习的每条经验长这样状态、动作、奖励、下一个状态、是否结束。但HER要求在环境里额外暴露两个字段目标goal和已达成目标achieved_goal。这里的目标不是指“想把机械臂末端移动到坐标(0.5, 0.2, 0.3)这种向量”而是一个和状态描述维度可对齐的张量。比如在机械臂环境里目标就是目标位置的坐标已达成目标就是当前方块实际位置的坐标。在导航任务里目标就是终点经纬度已达成目标就是智能体现在所在的位置。道理都一样环境必须能够返回“当前智能体在目标空间中的实际落点”。有了这两个字段重标注的流程就很简单了。一条原始经验里的目标是goal实际结果是achieved_goal。HER把新的目标new_goal设为achieved_goal或者轨迹中未来某个时刻的achieved_goal然后用同样的奖励函数重新计算奖励。注意这里的奖励函数本身可以非常简单比如“当已达成目标距离目标小于某个阈值时奖励为0否则为-1”。因为重标注后achieved_goal和new_goal完全匹配或者距离为0这条经验就会变成一条成功的正向样本。2.2 重标注之后奖励怎么重新计算重标注的关键在于奖励函数必须是“目标条件”的。也就是说奖励是根据当前状态下的已达成目标和目标计算出来的不能是一成不变的常数。一个典型的目标条件奖励长这样def compute_reward(achieved_goal, desired_goal): # 返回一个标量奖励距离越小奖励越高 dist np.linalg.norm(achieved_goal - desired_goal, axis-1) return -(dist threshold).astype(np.float32)这个函数输入的是已达成目标和期望目标输出奖励。注意它跟当前观测状态无关只和目标空间中的距离有关。这就是HER能够重写目标的数学基础既然奖励只取决于goal和achieved_goal那我换一个goal奖励自然就变了。实际操作中真正存进经验池的那条重标数据长这样保持状态、动作、下一状态、结束标志完全不变只把目标从原来的goal改成new_goal然后重新过一遍奖励函数得到新的reward。这样一条数据从“失败”变成了“成功”但它仍然是一条真实发生过的状态转移只是换了一个解释角度。这个解释角度并不会破坏学习因为策略本来就是在目标条件下做决策的多学一种目标条件下的成功路径对原始目标条件下的策略也是一种间接强化。2.3 四条采样策略final / future / episode / random重标注不是什么状态都能拿来当目标选哪些状态当新目标对效果影响很大。论文里给了四种策略我用大白话翻译一下顺便说我实测下来的体感。final整条轨迹结束后只用最后一步的achieved_goal作为新目标重写整条轨迹的所有transition。这是最简单粗暴的适合那种“最终状态就是目标状态”的任务比如打开门、把物体推到位。future轨迹中某个时刻t的transition从后面的时刻中随机抽一个状态的achieved_goal作为新目标。这是最推荐、实验效果也最好的策略。原因在于从t到被抽中的未来时刻之间的那些步骤天然就是一条“朝这个目标移动”的真实路径状态转移逻辑是自洽的。episode从整条轨迹的所有状态中随机抽一个作为新目标。这个随机范围更大但可能抽到跟当前时刻距离很远的状态造成“这条经验是在走向那个目标”这个假设不太成立。random从整个经验池中随机抽一个状态作为新目标。这个我没有细测因为感觉它已经脱离了轨迹内部的时间结构一般论文里也比较少用主要做对比。我实际用下来final和future是最稳的。final适合任务目标相对单一、终点状态明确的场景future则适合所有场景几乎是开箱即用。你现在去GitHub上翻很多现代机器人学习项目默认配置基本都是future策略。2.4 一个容易理解的例子机械臂推方块把上面这些机制串到机械臂推方块这个经典例子里再走一遍。环境目标是把桌面上的方块推到指定位置。状态是机械臂关节角和方块当前坐标目标是目标坐标已达成目标是方块当前坐标动作是关节力矩。智能体随机尝试了几百步最终方块停在(0.3, 0.5)离目标位置(0.7, 0.5)差一大截。原始奖励全是-1或者0。HER进场把目标改成(0.3, 0.5)重新计算奖励。那些“让方块从(0.1, 0.5)挪到(0.3, 0.5)”的transition现在被标记为“让方块从(0.1, 0.5)朝(0.3, 0.5)移动”末端状态的奖励变成0成功。这个重标后的轨迹进入经验池DDPG的Critic网络从这个样本里学到一个信息只要当前方块在(0.1, 0.5)目标是(0.3, 0.5)那么执行这个动作是好的。这些经验不断累积Critic对“靠近目标”这件事的评估越来越准Actor也跟着修正策略。训练几万步之后智能体竟然开始表现出一种“知道要朝目标移动”的行为。其实是HER把随机探索中的有效片段给挖掘出来了。3. 手把手实操在gym环境里跑通HER3.1 环境准备与依赖先交代一下我复现时的环境Ubuntu 20.04Python 3.8PyTorch 1.12gym 0.21。如果你机器上有NVIDIA显卡建议装好CUDA和cuDNN训练速度能快好几倍。没有显卡也能跑就是慢一些毕竟网络规模不大CPU也能跑通整个流程。依赖清单很简单numpy、torch、gym、opencv-python后续可视化会用到。如果你要跑机器人环境还需要安装mujoco_py或gym的机器人套件。我这里为了让小白也能跑用一个简单的二维点导航环境来做演示不用装任何物理引擎代码逻辑和HER完全一致。3.2 核心代码HER缓冲区与重标注逻辑我自己在项目中是把HER封装成一个单独的缓冲层不跟具体算法耦合。这样DDPG、TD3、SAC都能复用。核心是重标注的流程伪代码长这样。import numpy as np import random class HERBuffer: def __init__(self, capacity100000, reward_funcNone, k4, strategyfuture): self.capacity capacity self.reward_func reward_func self.k k self.strategy strategy self.buffer [] self.pos 0 def add_episode(self, episode): # episode: list of dict每个dict包含 # obs, action, reward, next_obs, done, goal, achieved_goal self._relabel_and_add(episode) def _relabel_and_add(self, episode): length len(episode) for t in range(length): trans episode[t] # 原始经验先直接入池 self._store(trans[obs], trans[action], trans[reward], trans[next_obs], trans[done], trans[goal], trans[achieved_goal]) # 再生成k条重标注经验 for _ in range(self.k): new_goal self._sample_goal(episode, t, length) new_reward self.reward_func(trans[achieved_goal], new_goal) self._store(trans[obs], trans[action], new_reward, trans[next_obs], trans[done], new_goal, trans[achieved_goal]) def _sample_goal(self, episode, t, length): if self.strategy final: return episode[-1][achieved_goal] elif self.strategy future: # 从t1到轨迹末尾中随机抽一个未来的已达成目标 future_idx random.randint(t 1, length - 1) return episode[future_idx][achieved_goal] elif self.strategy episode: return random.choice(episode)[achieved_goal] elif self.strategy random: return random.choice(self.buffer)[achieved_goal] def _store(self, *args): data (args[0], args[1], args[2], args[3], args[4], args[5], args[6]) if len(self.buffer) self.capacity: self.buffer.append(data) else: self.buffer[self.pos] data self.pos (self.pos 1) % self.capacity def sample_batch(self, batch_size): batch random.sample(self.buffer, batch_size) obs np.array([x[0] for x in batch]) action np.array([x[1] for x in batch]) reward np.array([x[2] for x in batch]) next_obs np.array([x[3] for x in batch]) done np.array([x[4] for x in batch]) goal np.array([x[5] for x in batch]) achieved_goal np.array([x[6] for x in batch]) return obs, action, reward, next_obs, done, goal, achieved_goal这个缓冲区有个特点它按照轨迹来存储而不是按照transition来存储。原因前面说过重标注需要知道整条轨迹的最终状态至少是未来状态所以一条轨迹必须先完整跑完再整体入库。如果你用的是增量式经验池在transition产生的瞬间就入池那就没法拿到未来的achieved_goalHER就失效了。3.3 训练主循环HER如何接入DDPG算法层面直接用DDPG做演示。主循环逻辑特别清晰跑一个回合收集整条轨迹调用add_episode然后从经验池采样训练。这一步看起来简单但要注意几个顺序上的细节。先看代码。def train_her_ddpg(env, agent, her_buffer, episodes2000, batch_size128, warmup1000): total_steps 0 for episode_idx in range(episodes): obs_dict env.reset() episode [] done False while not done: # obs_dict包含observation, achieved_goal, desired_goal obs obs_dict[observation] goal obs_dict[desired_goal] achieved obs_dict[achieved_goal] action agent.select_action(obs, goal) # 策略输入包含观测和目标 next_obs_dict, reward, done, info env.step(action) next_obs next_obs_dict[observation] next_achieved next_obs_dict[achieved_goal] episode.append({ obs: obs, action: action, reward: reward, next_obs: next_obs, done: done, goal: goal, achieved_goal: achieved, }) obs_dict next_obs_dict total_steps 1 if total_steps warmup: break # warmup策略请自行实现这里只做示意 # 一个回合结束后才交给HER库 her_buffer.add_episode(episode) # 训练若干步 if total_steps warmup: agent.update(her_buffer, batch_size)注意一个细节agent.select_action的输入除了状态还要加上goal。也就是说策略网络的输入是状态和目标拼起来的一个向量。HER重标后的经验其目标已经被替换所以策略网络看到的输入也必须是重标后的目标。在实现时网络forward要支持一个额外的goal输入而不是只吃状态。还有一个容易出错的点环境的done标志也要慎重处理。重标后的经验中如果新的目标真的是轨迹最终状态那么原本的done往往是1例如任务超时或失败结束。但当你把目标替换成最终状态后这个done就变成一个“成功终止”的done训练时不能把这个信号直接当作失败来处理。我的做法是在重标时将done保持不变但同一时刻原始经验仍然在池中原始done1针对原始目标就是失败两者并存让Critic自己学会区分。3.4 超参数选择与训练节奏HER相关超参数不多但每一个都值得认真调。第一个是k也就是每条原始轨迹生成多少条重标轨迹。论文里常用k4我的实测是k从1到8范围内收益逐渐上升但边际效应递减。k太大会导致经验池被重标数据淹没原始目标分布的样本比例降低反而可能把“原始任务”给淹没掉。所以k4是一个性价比很高的默认值。第二个是采样策略。我强烈建议先从future开始。它兼顾了轨迹内部的时间一致性而且在大多数任务里都比final更稳。final策略也不是没用比如有些环境里目标空间非常大随机挪动一下位置就天差地别此时final让整条轨迹都指向同一个终点稳定性反而更好。第三个是经验池容量。HER会额外生成k倍的样本量假设你原来设的经验池是10万加了HER之后就相当于要装50万条transition。内存不大的话建议直接把池容量再放大一倍并且注意淘汰策略。我的缓冲类是循环覆盖式旧的失败经验会被逐渐挤掉。这里有个反直觉的现象经验池太小时HER的优势反而发挥不出来因为重标产生的多样性不足但池子太大也有问题老数据和新策略之间差异大学习进展会变慢。我自己常用的配置是20万至50万条过渡数据的容量。训练节奏上还有一个心得warmup阶段很重要。HER虽然能把失败样本变成成功样本但前提是得有足够的失败样本进入经验池。开局前几百步最好用随机策略或带噪声的策略去探索环境先攒出几条像样的轨迹。等经验池里有了一定量的重标样本再正式切换到Actor-Critic更新效果会明显比一上来就更新稳。4. 常见问题与排查技巧实录4.1 奖励曲线平坦得像心电图是哪里出了问题这是遇到最多的现象。训练跑了几万步奖励曲线几乎不动更像是一条没有波动的水平线。我排查这个问题的第一反应是检查经验池里有没有正样本。HER的意义就在于提供正样本如果经验池里全部是负奖励那说明重标模块压根没生效。重点检查三个地方。第一个环境是否返回了achieved_goal。很多自定义环境只给observation和reward没有单独暴露achieved_goal字段这时候重标无从谈起。第二个奖励函数是否是目标条件式的。如果你的奖励函数完全不接受goal参数那换目标之后奖励根本不会变等于HER白做。第三个重标后的经验是否真的入池了。我有时会写一个简单的统计脚本定期打印池里奖励为0或正数的样本占比如果一直是0%那就说明逻辑断掉了。4.2 成功率上去了但动作很奇怪疑似奖励黑客有一次训练机械臂抓取成功率确实提上来了但看回放视频发现机械臂的动作极其诡异先把方块推离目标再绕回来。原因在于重标后的目标太密集导致Critic把某些中间状态误判为成功状态策略就走捷径绕弯。这个问题的本质是奖励设计的粒度太粗。如果阈值距离设得过大比如“方块离目标5厘米就算成功”重标之后的“成功样本”其实并没有真正到达目标只是在阈值范围内。解决办法是把阈值调小同时检查训练过程的轨迹可视化。不要只看成功率曲线要隔一段时间存一段真实执行轨迹亲眼看机械臂在干什么。4.3 重标注是否会动摇策略的稳定性好问题。我自己也担心过每次都把失败轨迹改成成功轨迹会不会让策略学到错误的目标分布后来仔细分析了才明白重标并没有把“任意状态”都变成目标它只是把“在单条轨迹中实际到达的某个状态”当作目标。目标空间中的样本量虽然增加了但每个新目标都对应于一条真实存在的、可执行的状态转移路径。换句话说重标没有凭空捏造物理学它只是换了一个目标坐标转移本身依然真实发生。不过要注意经验池里原始目标的经验比例要控制好。如果全池子都是重标数据策略可能会对新目标分布过拟合对原始目标泛化变差。所以我才强调k别取太大。每次新增一条原始轨迹同时新增k条重标轨迹k4就是让重标样本占多数但原始样本仍然存在。这个比例在绝大多数任务里都比较好用。4.4 HER什么时候真的不适合用HER并不是银弹。有些场景下它完全发挥不了作用。首先是On-Policy算法前面已经提过不赘述。其次是目标空间太大且没有结构性比如一个离散目标有1万个选项实际去过的目标永远只是其中极小一部分重标出来的新目标根本覆盖不到有效区域。第三是环境返回的achieved_goal噪声特别大重标本身会放大噪声此时可以考虑先对achieved_goal做滤波或平滑再用于重标。还有一类任务要小心多阶段任务。比如“先开抽屉再拿杯子最后回到起点”这种任务的目标空间不是一个简单的向量而是一个序列。HER对整个序列做重标几乎不可能因为它需要重写的是“目标序列”不只是“一个目标向量”。遇到这种情况更好的方案是拆成多个子任务每个子任务单独用HER或者用分层强化学习。4.5 几个容易被忽略的工程小坑最后分享几个代码层面容易踩的坑。一个是数据类型的坑numpy数组维度不齐目标拼接时维度出错这类报错在调试阶段特别常见。建议统一用float32并且在一个入口处做reshape。另一个是随机种子的坑HER会随机抽取未来状态作为新目标如果种子不固定同一轮实验的结果偶然性会很大。复现实验时一定要设好np.random.seed、random.seed和torch.manual_seed。还有一个很有意思的坑重标后奖励函数中的done逻辑要重算。有些环境在超出某些边界时会提前终止但重标后的目标可能恰恰在那个边界附近。如果沿用原来的done标志相当于告诉Critic“到达那个位置就是终止”策略可能因此回避那条路线。我在代码里的做法是当重标后的目标来自轨迹内部某个状态时把该状态的结束标志重置为False除非已经到了时间上限。5. 说句实在话HER的边界和我踩过的坑项目跑完回头再看我最大的体会是HER不是用来替代奖励设计的它解决的是“奖励存在但太稀疏”这个特定问题。在设计一个新任务时我还是会先花力气把奖励函数本身理清楚尽量保证奖励和目标空间有一定相关性然后再叠HER。把HER当饭后甜点可以当正餐不行。实操中的另一个体会是HER算法的调试重点其实不在算法本身而在经验比例。那个k参数我一开始觉得越大越好结果训练出来的策略有个明显的毛病它对已经去过的大片状态空间都很熟但一遇到没去过的新区域就手足无措。后来把k从8降到4并且增加了原始目标轨迹的比例整体探索性和最终成功率反而都提升了。这种“调参玄学”背后其实是有逻辑的经验池像一家餐厅如果菜谱全是重标菜客户策略的味觉就被带偏了。最后再给个实用小技巧在做完HER训练后用原始目标任务再做几千步微调。因为重标目标比较“讨巧”最终部署时还是要回归真实目标。我通常的做法是HER训练到成功率接近稳定之后把重标机制关掉只用原始目标奖励再训一小段时间让策略从“会过关”变成“更准确地过关”。这一步往往会让最终成功率再涨个几个百分点而且执行轨迹看起来也正常多了。这个做法不算复杂但很多人会忽略算是我在这个项目上最有价值的一条经验了。
返回列表