1. 先搞懂为什么要引入“事后聪明”
1.1 强化学习中的一个老大难:稀疏奖励
做强化学习的同学,十有八九都会被同一个问题折磨过:稀疏奖励。举个例子,你让一个机械臂去抓取桌面上的方块,只有把方块送到指定位置才给+1奖励,其他任何动作都是0。听起来简单,但机械臂的关节自由度太高,随机动作下,它可能几百次、几千次都碰不到目标位置。没有奖励信号,策略梯度就是零,价值网络就是一团浆糊,训练曲线就是一根水平的直线,纹丝不动。
这就是稀疏奖励的杀伤力。它不是“学得慢”,而是“根本不知道往哪里学”。我用过一个更直白的类比去跟朋友解释:在一个巨大的迷宫里找一颗糖,你没有任何气味线索,每次走完一步只知道“没找到”,那你永远是在瞎转。强化学习里的agent也是一样,奖励为0的反馈只告诉它“这次不对”,却没告诉它“你离目标近了多少”,于是探索变成了纯粹的随机事件。
很多入门教程喜欢拿CartPole、MountainCar举例,这些环境之所以好跑,恰恰是因为奖励不算太稀疏。一旦换成真实的机器人操作任务——抓取、堆叠、插孔——稀疏奖励就会立刻暴露出来。我早期做机械臂仿真时,第一次看到训练3000个episode成功率还是0,一度以为是代码写错了,排查了一整天。
1.2 常见解法为什么总差一口气
面对稀疏奖励,大家通常有三种解法。
第一种是奖励塑形(Reward Shaping),也就是人工设计中间奖励。比如机械臂离目标越近,奖励越大。这个思路直观,但坑很深:你设计的中间奖励本质上是在“告诉”agent怎么走,而不是让它自己学。稍微复杂一点的任务,塑形项很容易引入局部最优,甚至还会出现agent“刷中间奖励”而不做正经事的情况。我见过一个机器人仿真项目,因为给了一个“靠近物体就给分”的塑形奖励,机器人最后学会了把物体推到角落然后一直贴着它,因为那样能无限刷接近奖励。
第二种是课程学习(Curriculum Learning),也就是先让agent在简单目标上练,再逐步加大难度。这个思路本身没问题,但它要求你事先知道任务的难度排序。抓取任务可以按“距离目标远近”排,但很多任务你根本排不出来,排错了反而起反作用。
第三种是内在动机(Intrinsic Motivation),比如给agent加一个“好奇心奖励”,鼓励它探索新状态。这个方法在某些游戏场景有效,但对需要精准完成某个目标的机器人任务来说,“好奇”和“完成任务”不是一回事,加了好奇心模块,训练开销变大,收益却不一定明显。
这些方法不够好的共同点在于:它们都在试图“修复”稀疏奖励这个症状,却没有改变“大量失败经验被白白扔掉”这个事实。失败经验真的没用吗?HER的出发点恰恰是反对这个假设的。
1.3 为什么偏偏是“hindsight”这个词
如果你经常逛机器人学习和多目标强化学习的圈子,看到hindsight这个词,应该会直接想到OpenAI那篇经典的《Hindsight Experience Replay》。hindsight本身是英文里“后见之明”的意思——事情发生之后,你才意识到“原来当时应该这么做”。
把这个词用在强化学习里,有一种幽默感:agent明明没有完成目标,但在“事后”回头看,它至少成功到达了某个位置。那么,为什么不把“实际到达的位置”重新定义为目标,把这次失败变成一次成功经验呢?
这个想法初听有点绕,但它恰恰解决了稀疏奖励最核心的症结:如果每次失败都能被转化为一次“成功”的示范,那正样本就不再稀缺,梯度不再消失,agent也就能从大量失败中学到“怎么靠近目标”,而不是干瞪眼。
HER不是某个花里胡哨的新框架,它是一套简单得让人惊讶的“经验重标记”技术,可以和DQN、DDPG、TD3、SAC这些主流off-policy算法直接结合。这也是为什么它在OpenAI提出之后,很快成为多目标机器人强化学习任务中的标配组件。下面我拆开讲。
2. HER核心思路:把失败经验当成宝贵教材
2.1 一个打篮球的类比
我想先用一个不需要任何数学基础就能懂的类比来进入HER的直觉。
假设你练习投篮,目标是把球投进篮筐。你投了十次,全都没进,但是球落点的分布是有信息的——五次偏左,三次偏右,两次太短。一个聪明的教练不会说“你今天一个都没进,等于没练”,而是会说“你注意到了吗?你大部分球都偏左,说明你出手角度有问题;那几次太短,说明你力量不够”。这就是人类学习里的“后见之明”:你没有达成原定目标,但你获得了一份关于“如何接近目标”的事实报告。
HER做的就是这件事。它把“没进球”这个episode里的“球实际落点”提取出来,把这些落点当作目标,重新给这次经验算奖励。这样一来,agent就知道“从这个状态出发,落到那个位置,是可行的,而且会得到正奖励”。多次迭代之后,agent先学会把动作控制在“篮筐附近”,再逐渐从“篮筐附近”收敛到“进球”。
这个类比里最关键的一句话是:失败不等于没信息,失败只是“没有达成特定目标”的信息,而它依然是关于“如何达成某个目标”的成功信息。
2.2 多目标强化学习与目标重标记
HER工作在“多目标强化学习”的框架下。所谓多目标,就是环境里不止有一个任务,而是有一组共享状态转移规律的、仅靠“目标”区分的任务。
举个例子:机械臂抓取任务。状态空间是机械臂关节角度、物体的位置;目标是“把物体放到位置A”。换一个目标“把物体放到位置B”,环境物理没变,但任务变了。在多目标强化学习的设定下,policy不仅接收状态s,还要接收目标g,输出动作a。奖励函数就是“是否达成了这个目标”的判断:达成了给0(表示成功),没达成给-1(表示失败)。注意这里的奖励是二值的,不包含任何距离信息——这就是最纯粹的稀疏奖励。
HER的完整称呼是“事后经验回放”,核心操作是“目标重标记”。具体来说,在采集完一整条episode之后,不要只按原来的目标g存储经验。当你发现agent没达到g时,可以从这条episode中挑一个“其实到达了的状态”,把这个状态对应的feature当成新的目标g',然后重新用奖励函数计算这个transition在新的目标下的奖励。
因为agent“确实到达了”g'对应的状态,所以重新计算出的奖励几乎一定是成功奖励。一个原本全是-1的episode,经过重标记之后,会多出一批+0(成功)的样本。这些样本告诉你:看到这个状态、做出这个动作,是能实现g'的。这就是HER的全部秘密。
2.3 为什么重定目标能缓解稀疏奖励
从原理上说,HER至少做了三件了不起的事。
第一,它把负样本变成了正样本。这是最直接的效果。稀疏奖励环境最缺的就是“成功经验”,而HER人为“制造”了成功经验。虽然这些成功是相对重标记后的目标而言的,但对于智能体学习“如何对齐状态和目标”这件事,它们完全有效。
第二,它自动形成了一个“从易到难”的课程。在一整条episode里,早期状态离目标远,后期状态离目标近。如果用future策略(后面细说),给早期transition分配一个后期状态当目标,那它就是在学习“从很远的起点走到较近的目标”。随着episode推进,目标离起点越来越近,难度逐渐平滑。这个课程不需要你手动设计,环境自己就提供了。
第三,它让数据利用率大幅提升。同样的一个episode,在普通经验回放里只能贡献一条经验;在HER里,可以额外生成k条不同目标的经验。同等采样成本下,可学习的信息量翻了几倍。对真实机器人或昂贵仿真环境来说,这等于节省了大量采集成本。
2.4 HER和普通经验回放的差异
做强化学习的同学对“经验回放”都不陌生:把transition存到buffer里,训练时随机抽batch,打破样本相关性。HER用的buffer本质上还是这个,唯一的区别是,它会把“原始目标经验”和“重标记目标经验”一起存进去,而不是把原始经验替换掉。
这个差异容易被忽略,但其实很重要。如果你只存重标记后的经验,agent会忘了“原任务是什么”,在测试时用原始目标评估就会翻车。反之,如果只存原始经验,那HER就没有存在的意义。标准做法是:每条transition保留原始目标,同时额外采样k个新目标,生成k条重标记经验,一共k+1条进buffer。
当你看到这里,HER的整个理论框架其实已经装进脑子里了。下一节进入实操层面的细节,这些细节才是决定HER能不能真正跑出效果的分水岭。
3. HER实战:目标重标记的完整流程与参数选择
3.1 HER目标重标记的标准流程
先说一个完整的HER训练循环长什么样。以episode为单位,每采集完一条完整轨迹,做如下操作:
- 把原始轨迹按原目标g存入replay buffer。
- 对轨迹中的每一个transition (s_t, a_t, s_{t+1}, g),以一定概率选择一个新的目标g'。
- 根据g'重新计算奖励r' = reward_function(s_{t+1}, g')。
- 把新的transition (s_t, a_t, s_{t+1}, g') 也存入buffer。
- 正常从buffer里采样训练。
这里最关键的一点是:新目标必须是从“这条episode实际经历过的状态”里选出来的。你选了agent从来没见过、没到达过的状态当目标,得到的奖励大概率还是失败,重标记就失去了意义。
用代码表达,核心逻辑大致是这样:
# episode: 一条完整轨迹,包含(s, a, r, next_s, achieved_goal) # k: 每条transition额外重标记的样本数 def her_relabel(episode, k=4, strategy="future"): transitions = [] for t in range(len(episode) - 1): s, a, r, next_s, done, achieved = episode[t] transitions.append((s, a, r, next_s, done, achieved)) for t, trans in enumerate(transitions): # 原始目标样本,必须保留 buffer.add(trans) # 额外生成k个重标记样本 for _ in range(k): if strategy == "future": # 从t+1之后的状态中随机选一个作为新目标 future_idx = np.random.randint(t + 1, len(transitions)) new_goal = transitions[future_idx][-1] # achieved_goal elif strategy == "final": # 用最终状态作为新目标 new_goal = transitions[-1][-1] elif strategy == "random": # 从整个episode中随机选一个状态 rand_idx = np.random.randint(0, len(transitions)) new_goal = transitions[rand_idx][-1] new_reward = compute_reward(new_goal) new_trans = (trans[0], trans[1], new_reward, trans[3], trans[4], new_goal) buffer.add(new_trans)注意,这里的achieved_goal指的是“在这条transition结束时,agent实际到达的状态特征”,它和desired_goal是两码事。很多入门者在这里翻车,就是把这两个搞混了。
3.2 四种目标选择策略对比与推荐
HER原论文里提出了几种选择新目标的方式,最常见的四种是random、final、episode和future。它们的区别在于“从哪些状态里选新目标”:
- random:从整条episode的所有状态中随机挑一个当新目标。
- final:只用episode最后一个状态,也就是终点状态当新目标。
- episode:把整条episode里的所有已访问状态都作为候选集合。
- future:对第t个transition,从t之后(未来)的状态里随机挑一个当新目标。
实际效果上,future策略几乎总是最优的。原因很简单:它天然形成了一个时间上的课程——越靠前的transition,被分配到的目标就越靠后,距离越远;越靠后的transition,分配到的目标离自己越近,越容易成功。final策略太极端,所有transition都用同一个远端目标,早期样本高度稀疏;random策略虽然覆盖广,但缺乏课程性。我做过的实验里,random大约只有future一半的成功率提升速度,final略好一些,但稳定性不如future。
3.3 关键参数k怎么定
k表示每条原始经验额外生成多少条重标记经验。原论文里的默认推荐值是k=4,我沿用了很久,基本没出过大问题。
k太小(比如k=1),buffer里重标记样本占比低,稀疏问题缓解有限;k太大(比如k=16),虽然正样本更多了,但你会遇到两个新麻烦:一是buffer里大量样本高度重复或相关性过强,训练会变得不稳定;二是内存和算力开销成倍增加。每条transition要额外算k次奖励和k次目标编码,在复杂环境里这个成本不能忽视。
我个人的调参习惯是:先固定k=4跑通,看曲线如果正样本仍不足以支撑学习(表现是成功率长时间为0),再上调到8。如果出现了训练震荡、价值函数发散,检查是不是k太大导致buffer同质化严重。
还有一个容易被忽略的点:重标记目标的比例其实是一个超参数组合。原始目标样本和重标记样本在buffer里的比例,决定了agent“记住原任务”和“从重标记任务中学”的平衡。k=4意味着重标记样本约占80%,这个比例在大部分环境里是健康的。
3.4 适合与不适合HER的算法清单
HER不是独立算法,它是一层可以贴在经验回放上的“贴片”,所以对底层算法有明确的要求。
能用HER的算法,必须是off-policy的,原因很简单:HER会修改目标的奖励,生成的新经验对应的是一个策略已经改变过的行为分布,只有off-policy算法才能从这种混合了不同目标分布的buffer里采样学习。典型的组合是:
- DQN(离散动作空间,比如格子世界、部分棋盘游戏)
- DDPG(连续控制,机械臂、无人机)
- TD3(DDPG的稳定版,我目前最常用的组合)
- SAC(最大熵版本,探索性好,和HER搭配效果也不错)
不能用HER的算法,主要是on-policy系列:PPO、A2C、TRPO这些。它们要求当前训练的数据必须来自当前策略,重标记目标相当于篡改了数据来源,直接用会产生严重的bias。我曾经试过在PPO里强上HER,结果不仅没加速,原本勉强收敛的任务反而彻底凉了。
如果你非要做on-policy+HER的组合,也不是完全没办法——需要通过重要性采样修正分布偏移,但工程复杂度和收益不成正比,我建议直接换off-policy算法。
4. 完整实操:机械臂抓取场景的HER+DDPG实现
4.1 环境与工具准备
理论讲完了,拿一个具体环境把代码跑起来。我推荐用OpenAI Gym里的FetchReach-v1作为入门环境。它要控制一个7自由度机械臂,把末端执行器移动到目标点,目标空间是三维坐标,连续控制,奖励极其稀疏——不达到阈值就是-1。
为什么推荐它?因为FetchReach是“最难但最简单”的环境:目标空间是连续三维,但动力学相对友好,HER+DDPG通常几十个epoch就能看到明显提升,非常适合验证思路。等你看懂曲线在动,再升级到FetchPush-v1或FetchPickAndPlace-v1,那些才真正考验目标重标记的威力。
需要的工具如下:
- Python 3.8+
- PyTorch 1.10+
- gym(版本注意,旧版需要用mujoco_py,新版推荐用gymnasium+MuJoCo的wheel版本)
- 如果你不想折腾MuJoCo,社区里也有开源的高性能纯Python实现,比如gymnasium-robotics,功能一致。
4.2 核心代码:HER怎么接进DDPG
DDPG本身包含Actor网络、Critic网络、目标网络和replay buffer。HER要改动的核心是buffer的存储逻辑和采样逻辑。我写一个简化但可运行的版本思路。
首先是经验的结构。DDPG的transition通常是(s, a, r, done, s_next),但多目标场景下,s本身由三部分组成:observation、desired_goal、achieved_goal。obs是机械臂状态,desired_goal是任务目标(比如目标位置),achieved_goal是当前实际位置。网络输入要把obs和goal拼接起来,否则它看不到“我要完成什么目标”。
class HerReplayBuffer: def __init__(self, capacity, k=4, strategy="future"): self.capacity = capacity self.k = k self.strategy = strategy self.buffer = deque(maxlen=capacity) self.episode = [] def add_transition(self, obs, action, reward, next_obs, done, achieved_goal, desired_goal): self.episode.append((obs, action, reward, next_obs, done, achieved_goal)) def end_episode(self): # episode结束时统一做目标重标记 episode = self.episode for t in range(len(episode) - 1): obs, action, reward, next_obs, done, achieved = episode[t] self.buffer.append((obs, action, reward, next_obs, done, achieved, episode[t][-2])) # 这里desired_goal只用于原始样本 for _ in range(self.k): if self.strategy == "future": future_idx = np.random.randint(t + 1, len(episode)) new_goal = episode[future_idx][-1] elif self.strategy == "final": new_goal = episode[-1][-1] else: rand_idx = np.random.randint(0, len(episode)) new_goal = episode[rand_idx][-1] obs_, action_, _, next_obs_, done_, achieved_ = episode[t] new_reward = compute_reward(next_obs_, new_goal) self.buffer.append((obs_, action_, new_reward, next_obs_, done_, new_goal, new_goal))这里有几个容易忽略的细节:
一是原始样本里desired_goal要保持原目标,不要误改。二是新目标g'其实是和achieved_goal重叠的——我们选择新目标时用的是achieved_goal作为g',这样在采样时网络才能看到“目标等于实际到达位置”的超级简单案例。三是done标志怎么办?重标记样本里done一般设False,因为“成功达成新目标”不等于“原任务终结”。如果你把完成新目标也算done,episode会过早终止,产生过短的轨迹。
4.3 训练过程与效果观察
我在FetchReach上跑过一版HER+DDPG,用以下配置:
- 每个epoch包含50个cycle,每个cycle里跑16个episode采集数据,然后训练40次,每次batch size是256。
- Actor和Critic都是256x256的全连接网络,激活函数ReLU,最后一层Actor用tanh限幅。
- Actor学习率1e-3,Critic学习率1e-3,gamma=0.98,tau=0.05。
- 探索噪声用高斯噪声,std从0.2逐渐衰减到0.05。
- k=4,future策略。
训练30个epoch后,测试成功率开始爬升;60个epoch左右,成功率稳定在90%以上。作为对照,关闭HER,只用原始目标训练DDPG,跑200个epoch成功率依然是0。这不是DDPG太弱,而是稀疏奖励下的必然结果——没有正样本,Critic永远在输出负值,Actor永远偏离。
如果你的环境和这个不完全一样,不要死磕epoch数量。核心观察指标是:replay buffer里正样本的比例是否在上升,以及测试成功率曲线的斜率是否不再水平。
4.4 调参经验:三个最容易影响成败的细节
第一,目标特征一定要归一化。FetchReach的目标是三维位置,量纲一致没问题。但很多自定义任务里,目标可能包含位置、速度、角度,量纲完全不同。角度要用sin/cos表示,不要直接用欧拉角。我曾在一个无人机任务里把目标和obs直接拼接,发现位置维度主导了loss,角度维度完全没学到,归一化之后训练速度提升明显。
第二,Critic网络能不能拟合“目标”很关键。DDPG的Critic输入是(state, action),在HER里要把goal编码进state,所以网络要足够宽。我建议不要低于256。如果遇到Q值发散,检查是不是没有用目标网络(target network)更新滞后系数tau调太大;tau=0.05在FetchReach够了,换TD3时记得改回0.005。
第三,探索策略需要在训练中动态变化。固定噪声会让后期策略不稳定。我在实践中习惯用“前20%的cycle用大方差探索,之后按指数衰减”,这样早期能积累足够多样化的经验供HER重标记,后期再专注利用。
5. 常见问题与排查技巧实录
5.1 加了HER还是学不动,先查这三处
“我加了HER,为什么测试成功率还是0?”这是我在各种技术社区里被问最多的问题。根据我踩过的坑,极大概率是下面三个原因之一。
第一个原因:observation里根本没有achieved_goal信息。HER重标记的核心是“把实际到达的状态当成新目标”,如果观察向量里没有当前的实际位置,agent连“我现在在哪”都不知道,新目标对它来说就没有意义。检查你的obs空间,确认它包含足够的状态信息来反推位置。
第二个原因:reward函数写错了。常见错误有两种。一种是你用了“距离越近奖励越大”的塑形奖励,然后又叠加HER的稀疏奖励,两个信号在打架。另一种更隐蔽:你计算“是否达成目标”用的阈值太严格,导致即使是重标记后的样本,奖励也经常是-1。比如目标空间是三维位置,你却不小心把整条episode的achieved_goal都存成了同一时刻的值,这样重标记出来的新目标其实根本不在轨迹上。
第三个原因:buffer太小或采样策略有问题。HER需要一定规模的buffer来容纳重标记后的多样样本。如果capacity只有几万,很快就存满了,旧经验被覆盖,新经验又高度相似,训练效果和没加HER差不多。我建议FetchReach至少用10万容量,复杂任务建议50万以上。
5.2 训练震荡、不稳定的处理思路
HER跑起来之后,最大的拦路虎是训练不稳定。我自己遇到过最典型的情况是:前20个epoch成功率稳步上升,然后突然断崖下跌,再恢复,反复横跳。
处理思路按优先级排:
- 降低学习率,尤其Critic。HER产生的重标记样本相关性较高,过大的学习率容易让Q值发散。
- 检查replay buffer里重标记样本的比例。如果k设置过大(比如16),buffer里的“成功”样本过于同质,Critic过拟合到特定目标分布上。调回k=4。
- 稳定目标网络。DDPG里tau不宜过大,TD3建议0.005。
- 增加batch size。我试过把batch从128提到512,震荡幅度明显变小。
如果还是不稳定,就检查一个我从TD3里学来的习惯:给Critic的输出加一点小噪声作为正则化,或者直接用TD3的clipped double-Q。在HER场景里,重标记后的正样本数量多但分布窄,double-Q能有效减少高估问题。
5.3 问题速查表
| 现象 | 可能原因 | 处理建议 |
|---|---|---|
| 成功率长时间为0 | obs缺少achieved_goal | 检查状态空间是否包含当前位置信息 |
| 成功率刚开始升又崩 | 学习率过高/k过大 | 降低学习率,k回调到4 |
| Q值不断增长但成功率低 | Critic高估 | 换TD3或给Critic输出加正则 |
| 训练慢、GPU占用低 | 目标重标记太耗时 | 向量化compute_reward,避免循环内逐条算 |
| 测试时成功率低于训练 | 探索噪声太大 | 测试时关闭噪声,或在训练后期衰减噪声 |
| 重标记样本全是-1 | reward阈值设置不当 | 检查新目标是否真的在轨迹上,阈值是否过严 |
5.4 一条来自实操的忠告
最后说说我自己的体会。HER不是一个“装上就能用”的银弹,它适合的目标,必须是能用状态特征合理表达的目标。如果你要教机器人“整理桌面”这种目标空间极其抽象、难以用几个坐标描述的任务,HER会非常别扭,硬上只会让自己痛苦。
我的建议是:先老老实实在FetchReach这类标准环境上把HER跑通,亲眼看一遍成功率曲线从0爬到90%的全过程,再去碰你自己的自定义任务。因为HER的失败模式特别多,而且很多失败看起来都像“算法没用”,实际上是“目标表示不对”或“奖励算错了”。把基础环境吃透,你才能分清楚到底是HER不适合你的场景,还是你的代码哪里出了问题。
如果以后你想进一步提高上限,可以关注一下HER和自动课程学习的结合方向,例如Plappert等人在多任务机器人操作上的工作,他们把HER从单目标扩展到多任务并行,思路一脉相承但工程复杂度高了不止一个量级。先把手头这个跑好,再谈扩展。