ARTICLE DETAIL

资讯详情

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

稀疏奖励下的强化学习难题:Hindsight Experience Replay(HER)原理与实战

稀疏奖励下的强化学习难题:Hindsight Experience Replay(HER)原理与实战 “hindsight”这个热词在机器人操作和强化学习圈子里几乎已经和“事后经验重放”画上等号。我第一次认真接触它是复现 FetchReach 任务的时候当时智能体死活学不会把机械臂末端推到目标点奖励全部是 0训练曲线像一条死人的心电图。加了 hindsight 之后奇迹般地开始收敛那一刻我才意识到这玩意根本不是某个小 trick而是一整套重新定义“目标”的思维转换。这篇内容我打算完完整整拆开讲一遍它解决什么问题、算法内部的三个关键步骤、怎么手写一个能跑的简化实现、以及我在实际调参过程中踩过的坑和总结出来的经验。无论你是刚接触强化学习的学生还是已经在做机械臂抓取部署的工程师按这篇的思路走一遍基本就能把 hindsight 吃透。1. hindsight 到底是解决什么问题的“后悔药”1.1 稀疏奖励场景的真实形态绝大多数真实世界的强化学习任务尤其是机器人操作都逃不开一个尴尬环境给你的奖励绝大多数时候是 0。你让机械臂去抓一个杯子机械臂在空中乱动每一步的奖励都是 0只有最后真的抓到了环境才给一个 1。问题来了如果在几十万步探索里一次都没抓到过杯子那这个 1 就永远不出现整个训练过程就像在黑暗里摸一个不存在的东西梯度根本不知道往哪传。这种设定在论文里叫 sparse reward中文一般叫稀疏奖励。它不是一个什么新奇的学术玩具而是工程现场最常见的困境。公司的机械臂项目老板不可能像游戏打怪一样给你人工设计一堆连续奖励。你没法精确告诉机器人“手腕偏离目标 1 厘米扣 0.1 分转动角度偏差 5 度扣 0.01 分”因为这样的规则要么撑不起复杂环境的全部状态空间要么设计出来的奖励函数本身就是错的会把机器人训练成钻规则漏洞的“作弊高手”。1.2 稠密奖励设计的痛苦和工程陷阱要是你天真地想那我手动设计一个稠密奖励不就行了我劝你先冷静。我见过太多人在 reward shaping 上花了两周时间最后的结局都是惨烈的。稠密奖励最坑的地方在于它引入了人的先验偏见。你以为让机械臂离目标越近奖励越高结果机械臂学会了把末端怼到目标附近就“抖”在那里因为离得越近越安全越远反而风险越大你又加了一个惩罚项结果它开始采用一种你完全想不到的别扭姿态既保持距离适中又恰好规避了惩罚。这个过程实际上是在和“奖励黑客”赛跑对手是梯度下降它永远能找到你奖励函数的漏洞。所以从这个角度看稀疏奖励反而更“诚实”要么做到了要么没做到没有模糊地带。代价是训练困难回报是训练出来的策略不会钻空子。hindsight 就是在这个矛盾点上出现的破局思路。它不去费劲设计奖励而是换个角度既然这一次没达成原始目标那我们就“事后诸葛亮”地想如果把刚才的实际落点当作目标那刚才这一整条轨迹不就成功了吗1.3 一个能秒懂的“运动员练球”类比很多教材爱用“篮球入筐”来解释 hindsight我想换一个更贴切的例子学打羽毛球。你站在原地挥拍一百次可能一次也没打到球教练在旁边记你每一拍的姿势数据。如果你只看“球有没有过网”这一条奖励信号一百次全是 0你根本不知道练了啥。但 hindsight 的做法是每次挥拍之后不管球飞去哪都把它当成“目标”——这次我们假装就是要打向那个偏左的落点。然后仔细看球拍角度和发力哦原来要打出那个偏左的落点这个挥拍轨迹就是可以用的经验。这样零奖励的训练数据就被重新标记成了正样本。你保留了失败的肌肉记忆却从中提炼出了某种“如果目标是这里动作该怎么做”的宝贵信息。这个“重新解读失败”的视角就是整个 hindsight 思路的灵魂。它让智能体把每一次失败的轨迹都转化为学习材料而不是简单丢弃。2. 算法机制拆解HER 做了哪三步“小事”2.1 标准经验回放的致命缺陷先把名字对齐一下。hindsight 对应的英文术语是 Hindsight Experience Replay缩写 HER中文通常叫事后经验回放。它不是一个独立算法而是可以叠加在 DQN、DDPG、SAC、TD3 这类 off-policy 算法之上的“数据增强层”。为什么只能配 off-policy 算法因为 HER 的核心操作是“改历史数据”把过去已经发生的 transition状态转移重新贴标签。这要求算法能从经验池里反复采样历史数据来训练也就是 off-policy 的典型特征。而 PPO、TRPO 这种 on-policy 算法每次采样完就更新策略更新完就把数据扔了根本没有“回放”的机会自然也就谈不上“事后重新解读”。标准经验回放的做法很简单把状态转移 (状态, 动作, 奖励, 下一状态) 存进一个 buffer训练时捞一把出来算损失。对稀疏奖励来说这个 buffer 里绝大多数样本的奖励都是 0。一个小 batch 里全是 0 奖励算出来的梯度要么不更新要么就是纯噪声长时间下来策略纹丝不动。问题不在回放机制本身而在回放的数据质量太单一——你存了一万条全失败的轨迹却没有从中提炼出任何正面信息。2.2 目标重标记把“失败轨迹”改成“成功轨迹”HER 做的第一件事就是给失败轨迹重新找一个“事后看来能成立的目标”。想象一个 goal-conditioned 的任务状态里包含了目标信息比如机械臂末端目标位置。假设原始目标是 A 点机械臂实际跑到了 B 点并停留了一会最终没有达到 A奖励是 0。HER 会这样操作取出这条轨迹把目标字段从 A 点临时替换成 B 点。接着重新计算这一小段轨迹的奖励。因为机械臂最终确实到了 B 点也就是现在的新目标所以这段轨迹的最后一步奖励就变成了成功奖励 1。环境结构完全没变只是“目标”这一个变量的赋值被换了。这一招的高明之处在于它并没有造假它只是改变了看问题的角度。从原始目标 A 的视角看这条轨迹是失败的但从事后目标 B 的视角看这分明是一条教科书式的成功示范。网络通过这种数据学到的不是一个具体动作而是“状态与目标之间的映射关系”。之后遇到真正想达到的目标 B 时它已经见过“如何从附近状态抵达 B”的样例了。2.3 final 与 future 策略该拿哪个状态当事后目标怎么挑选“事后目标”论文 Andrychowicz 2017 里给出了几种策略实际用得最多的有两种final 和 future。final 策略最简单直接每条 episode 结束时把最终状态 grab 下来作为整条轨迹的目标重新标记。它的问题是如果一条轨迹非常长前半段和最终状态差得十万八千里那前半段的经验被标记为“正在前往最终状态”就会有点牵强。不过 final 的一致性最好实现最简单在目标空间变化不剧烈的任务里效果很稳。future 策略则是在当前时间步之后随机挑一个未来状态作为目标。举个具体例子一条轨迹长度为 50 步在处理第 10 步的转移时future 会从第 11 步到第 50 步之间随机选一个状态把这个状态当作目标然后重新计算奖励。这样做的好处是每个转移都能配一个“近在眼前”的成功目标训练信号更强。论文里推荐 k4意思是为每个 transition 额外生成 4 个未来目标样本。我自己的实践经验是如果目标空间是稠密的连续空间future 比 final 好用很多收敛明显更快如果目标空间就两个离散状态比如“拿住”和“没拿住”final 就够了。很多入门教程只讲 HER 的机制却不说这一层选择差异导致不少人复现时用了未来策略却调不出效果实际上就是目标生成策略和任务性质不匹配。2.4 与 DDPG、SAC 这类 off-policy 算法的搭配逻辑HER 本身不做策略更新它只负责在采样阶段把 transition 的目标字段“替换”好然后正常交给底层的 DDPG 或 SAC 算损失。在实现层面每个经验样本要包括目标字段而 HER 的改动集中在“采样时如何构造样本”本质上是一个比较透明的数据管道。这里有一个很多人忽略的细节当 HER 把一条轨迹重新标记成多个成功样本后会造成经验池里“成功样本”比例异常偏高。这时候底层算法的探索能力就得跟得上不然智能体会逐渐倾向于重复某种固定动作因为这种动作成功了太多次。实际调参时要保证探索噪声的存在尤其是在机器人操作这种连续控制任务里DDPG 的 OU 噪声或者 SAC 的熵正则要适当大一点否则很容易陷入局部最优。3. 从零写一个能跑的 HER 简化实现3.1 先搭一个“有点难又没那么难”的玩具环境为了把机制讲透我建议环境越简单越好。我常用的一个 demo 环境是一个一维连续控制任务智能体控制一个位置变量从 0 出发目标是到达随机给定的位置目标范围在 [0, 10]。每个 episode 的初始位置固定为 0目标位置每次随机采样。如果做到 |当前位置 - 目标| 0.5奖励为 1否则奖励为 0。动作幅度限制在 [-1, 1]。这个任务如果不用 HER直接上 DDPG在稀疏奖励下几乎不可能收敛因为随机探索到目标附近的概率太低了几十万步里可能一次都碰不到。而用了 HER通常几千步就能看到策略明显变好。它能非常直观地展示 HER 带来的巨大效果差异又不涉及高维状态的干扰。3.2 核心代码目标重标记的完整实现思路下面我给一套我实际跑通的简化 Python 伪代码。它不是完整的 DDPG 实现而是聚焦在 HER 独有的那一部分经验抽取与目标重标记。你把它嵌进任意 off-policy 框架都可以。import numpy as np import random class HERBuffer: def __init__(self, capacity, future_k4): self.capacity capacity self.future_k future_k self.buffer [] self.episode_cache [] # 暂存当前episode的所有transition def add_transition(self, state, action, reward, next_state, goal): # 注意这里先不直接进buffer先放进episode_cache self.episode_cache.append({ state: state, action: action, reward: reward, next_state: next_state, goal: goal }) def end_episode(self): # 在episode结束时将缓存中的转移做目标重标记后存入buffer episode self.episode_cache for transition in episode: self._store(transition, transition[goal]) # future策略随机选取当前时刻之后的某个状态作为新目标 current_t episode.index(transition) future_indices list(range(current_t 1, len(episode))) if len(future_indices) 0: sampled_indices random.sample( future_indices, min(self.future_k, len(future_indices)) ) for idx in sampled_indices: new_goal episode[idx][state] self._store(transition, new_goal) self.episode_cache [] def _store(self, transition, goal): # 根据新目标重新计算奖励 done (abs(transition[next_state] - goal) 0.5) new_reward 1.0 if done else 0.0 self.buffer.append({ state: transition[state], action: transition[action], reward: new_reward, next_state: transition[next_state], goal: goal }) if len(self.buffer) self.capacity: self.buffer.pop(0) def sample(self, batch_size): return random.sample(self.buffer, min(batch_size, len(self.buffer)))这段代码里最值得注意的地方是_store函数它接收一个原始 transition 和一个新的 goal然后立刻基于新 goal 重新计算 reward。这就完成了 HER 的全部本质逻辑。底层 DDPG 从这个 buffer 采样时并不知道这些目标字段是后来被替换过的它只需要照着正常的(s, a, r, s, g)格式去更新即可。我自己写的时候犯过一个低级错误把episode_cache清空的操作放在了_store之前结果整个 episode 的所有数据都丢掉了。这里提醒一下顺序千万别搞反。先重标记完再清缓存。3.3 DDPG 侧三个最关键的对接点如果底层用 DDPG有几个对接点必须小心。第一是网络输入Critic 网络的输入必须是(状态, 目标, 动作)Actor 网络输入是(状态, 目标)。这意味着目标信息不能只是存在 buffer 里而是要真正拼接到网络输入向量中。很多复现失败的人就是把目标这个维度漏掉了导致网络完全没有感知目标的能力HER 再神也发挥不出来。第二是目标归一化如果状态量级在 [0, 10]目标也在 [0, 10]直接拼接没问题。但如果状态很大比如像素值 0-255目标也要跟着状态一起归一化到 [-1, 1] 或 [0, 1] 的范围内否则网络输入量级差距太大梯度容易不稳定。第三是 Bellman 更新里的 done 标志DDPG 在计算 target Q 值时如果doneTrue就不应该再加未来回报。HER 重标记后的成功样本done必须同时设置为 True。如果done没同步改算法会误以为这个成功状态后面还会继续导致 Q 值估计偏悲观收敛变慢。这个 bug 非常隐蔽因为代码能正常跑loss 也会下降但最终策略成功率上不去。3.4 一维环境下的实验结果观察我用上面的简化环境跑了一个实验。DDPG 超参数固定actor 网络两个隐藏层 256 单元replay buffer 100 万每步探索噪声是高斯噪声标准差 0.1。在没有 HER 的情况下训练到 5 万步成功率全程为 0。打开 HER 并设置 future_k4 后大概在 6000 步左右成功率开始爬升2 万步后能稳定到 80% 以上。这个对比已经足够震撼了同样的网络结构、同样的探索设置只是改变了数据标注方式效果就是天壤之别。如果你在自己的代码里复现时发现 HER 的效果提升不明显建议先检查三件事一是 future_k 是否设成了 0二是 Critic 是否真的吃到了 goal 输入三是有没有在重标记时同步 done 标志。这三个点是我见过的最常见的“假 HER”源头。4. 实战常见问题与排查手段实录4.1 一张实战问题速查表我在机械臂抓取、桌面推箱子等任务上用过不少次 HER下面这张表是我从实际排障经验里整理出来的比读论文里的 appendix 有用得多症状最容易的原因怎么处理训练曲线完全不动目标没传入网络检查 Critic/Actor 的输入是否拼接了 goal训练很久后成功率突然崩溃探索噪声太小策略陷入局部最优调大噪声或提高 SAC 熵系数用 future 策略反而不如 final目标空间是离散类别换成 final 策略成功样本占比过高但泛化很差只重标记了最后一小段轨迹检查是否对整条轨迹都做了重标记明明重标记成功Q loss 却爆炸奖励没归一化将 0/1 奖励除以一个常数或者调低学习率策略能到目标但动作很“抖”重标记目标选取过于“未来”降低 future_k 到 2 或者 1这张表不是万能的但能覆盖我遇到过的 90% 问题场景。特别是“成功样本占比过高”那一条很多入门者容易忽略HER 会制造大量成功样本如果底层的 replay buffer 也以很高的比例采样它们策略很容易只记住成功区域附近的状态映射而对远离目标的区域一无所知。解决办法是控制 HER 重标记样本占总样本的比例比如 50% 原始样本加 50% 重标记样本而不是把重标记样本无脑全塞进去。4.2 关于“HER 很耗内存”的真相经验池里每一个 transition 都带了一份 goal 副本。对于高维状态来说比如图像输入这会让内存占用直接翻倍甚至翻几倍。我在一个视觉抓取环境里试过每张图像是 84x84x3一份 sample 大概 20KB一千万条 transition 直接吃掉 200GB 内存这还不算 goal 的重复拷贝。工程上有两个缓解办法。一是只在采样时才动态进行目标重标记而不在存储阶段存储所有重标记副本。也就是 buffer 里只存原始轨迹每次采完一个 batch在内存里临时对每个样本调用重标记逻辑这样省掉了大量重复存储。二是压缩状态表示先训练一个自编码器把图像压缩成低维向量再在这上面跑 HER。第二种方案在真实机器人项目里几乎是必做的因为在 100 维以下的状态空间里 HER 效率极高而原始图像直接进去会让目标重标记的语义变得非常怪——某个像素级别的“目标状态”对机器人操作毫无意义。4.3 怎样算“成功”评估指标的坑HER 重标记出来的成功样本是基于“虚拟目标”的成功不是真实目标的成功。很多人训练到后期发现 training reward 曲线巨高但真实目标成功率只有 20%就开始怀疑 HER 在造假。其实不是HER 的 training reward 评估指标本来就没什么参考价值它只反映“重标记目标下的回报”不代表真实任务完成情况。正确做法是每 N 步用一个固定的真实目标集做 evaluation比如随机采样 100 个真实目标关掉探索噪声用当前 actor 去跑然后统计真实目标下的成功率。这个指标才能真正反映策略好不好。我在工程里一般设置 eval 周期为环境采样步数的 1/10比如每 1000 步 eval 一次记录到 tensorboard 里对比训练曲线时才不会自欺欺人。4.4 三个容易被忽略但极其好用的细节第一个细节是“目标也要去探索”。HER 的一个隐性好处是它让智能体在目标空间上展开了隐式探索。如果你用的是 goal-conditioned policy那么除了动作要加探索噪声目标也可以加一点噪声效果会更好。具体操作是在训练时给 goal 叠加一个小高斯噪声但注意 eval 时不能加。第二个细节是“多次经验回放”。HER 在每条轨迹结束后可以对同一条轨迹重复做多次目标重标记每次都随机选不同的事后目标。这相当于是对每条轨迹做数据增强增加样本多样性。但不是越多越好我试过 k20训练速度反而慢了因为目标的采样开始重复收益递减明显。第三个细节是“先热身后 HER”。在训练初期策略完全是随机动作这时候强行加 HER 的 future 策略会制造大量“从任意状态到任意状态”的噪声目标反而干扰学习。我在实际项目里提到过一种做法前 2 万步只存原始样本不重标记等 buffer 里积累了一些有意义的轨迹再开始启用 HER。这个 trick 在论文里没有明确写但我试过几次之后发现收敛更稳。5. hindsight 的应用边界与后续扩展5.1 哪些机器人任务适合用 hindsight直观来说凡是“目标可以用状态空间中的某个点来表示”的任务hindsight 都适用。机械臂抓取、推箱子、插孔、开门、移动导航、操作夹具这些任务的目标都能表示成“姿态”或“位置”天然适合目标重标记。另一类常见任务是“到达某个区域内”判断条件是一个距离阈值这也很好用。比较棘手的任务是目标无法用状态点表示或者目标空间和状态空间不对齐。比如“把杯子放在桌上指定位置”这种任务表面上是位置目标但实际上“摆放姿态”也是一个隐变量如果只以位置作为目标重标记出来的样本会误导策略。这种情况需要先手动设计 goal representation把“位置姿态周边物体约束”一起编码成目标向量。机器人领域通常用 keypoint 或者 object pose 来做而不是直接用像素。这么做之后 HER 依然有效毕竟它的逻辑不关心目标的长相只关心目标字段能不能被重新赋值。5.2 与行为克隆、演示学习的联用思路我最近比较看好的方向是 HER 与人类演示数据的结合。纯 HER 在极高的随机探索任务里早期成功率依然很低因为“事后目标”虽然能给出正样本但是这些正样本对应的动作并不一定是合理的——一个随机动作恰好把机械臂甩到了某个位置这并不能说明这个动作是“好”的。但如果我们把人类演示也注入经验池那么从“当前状态到目标状态”的转移就有了高质量示范HER 负责填充中间稀疏的部分行为克隆或者 GAIL 负责提供初步的模仿信号。这种混合训练在真实机械臂项目中效果非常好原因是 HER 负责广度演示数据负责精度两者互补。有一点要提醒的是演示数据不要也做目标重标记否则会稀释原始演示中的语义。我之前的经验是原始演示数据在 replay buffer 里占比 10% 到 20% 就足够了太多反而让 HER 的重标记样本失去意义智能体开始无脑模仿演示动作。5.3 从 HER 到其他目标条件强化学习算法HER 只是目标条件强化学习的一小步。后来的模型如 HGGHard Goal Generation、DCCL、GoalGAN 等都在想办法自动生成“有挑战性的目标”而不是依赖随机状态作为事后目标。它们的共同点都是把“目标”当作可以生成和改造的对象HER 是这条思路的起点而不是终点。如果你在实项目中感觉 HER 不够用了我的建议是往“困难目标生成”方向升级而不是回头去设计稠密奖励。比如 HGG 会维护一个目标分布专门采样那些“当前策略刚好学不会但又不是完全不可能”的目标这样每一个训练样本都踩在能力边界上学习效率比纯 HER 的高不少。从工程角度讲这个升级不会太难只要你能把 HER 的“目标重标记函数”替换成“目标生成网络”就行。我自己对一个机械臂抓取项目做过对比纯 HER 训练 100 万步成功率 60%HERHGG 之后80 万步就到了 75%而且训练前期曲线更稳定。这些数字不代表所有环境都这样但至少说明目标生成策略这一层确实有增量空间。结尾从“事后”走向“事前”我在实际调 HER 的过程里最大的体会是不要把 hindsight 只当成一个算法而要把它当成一种数据视角任何一条轨迹不管成功还是失败站在合适的目标视角下都可能包含正面信息。这种“别急着否定失败数据”的思路比它本身带来的指标提升更值得借鉴。最后再补一个小技巧如果你在验证自己的 HER 实现先不要上机械臂仿真就在 CartPole 或者一维 Push 这种两分钟就能跑完的环境里验证逻辑是否正确。因为 HER 的代码结构并不复杂绝大部分问题都出在数据管道的逻辑上小环境里一眼就能看出哪里对不上。等逻辑验证通过再迁移到真实仿真场景你会少走很多弯路。
返回列表