尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

稀疏奖励下的强化学习:HER目标重标注如何解锁失败轨迹的价值

稀疏奖励下的强化学习:HER目标重标注如何解锁失败轨迹的价值 1. 从稀疏奖励说起为什么失败的轨迹才是金矿1.1 一个让人崩溃的经典场景我第一次在 OpenAI Gym 的 FetchSlide 环境下跑 baseline 的时候连续两周都在盯一条纹丝不动的曲线。任务本身并不复杂机械臂把一个顶在桌面上的滑块推到指定的目标点完成一个 episode步数限制 50 步判定标准是滑块最终位置和目标点的距离小于 0.05 米。动作空间是四维的力矩控制观测向量大概 25 维左右目标是一个三维坐标。按理说这只是个入门级玩具任务我一开始也这么以为。现实很快教育了我。绝大多数 episode 里滑块连目标点一半的距离都没推过去奖励函数返回 0。所谓稀疏奖励说的就是这个状态——整条 episode 只有最后一步能知道成功还是失败而大多数时候最后由不成功所以前面四十八步里所有状态、动作、力矩组织全部得不到任何反馈。FetchSlide 是个连续控制问题策略输出的是四个关节的力矩值想靠瞎碰把滑块怼到目标点附近概率低到可以忽略不计。OpenAI 那篇 Hindsight Experience Replay 论文里有一句话我一直记得平均每五十条 rollouts 里才有一条能拿到非零奖励。我自己实测的情况比这还糟探索期产生的轨迹里绝大多数都是彻头彻尾的失败。而经典 off-policy 强化学习的全部学习机制恰恰依赖这些偶尔出现的正奖励把梯度往回传。一旦非零奖励出现的概率低到一定程度Q 函数对所有 (state, action, goal) 组合的估计都会趋近于同一个常数actor 的梯度方向几乎随机训练在物理意义上就是死了。1.2 hindsight 的核心思想把没做到的事重新定义为做到过于是 Hindsight Experience Replay 出现了熟悉这个领域的人通常直接叫它 HER。hindsight 在英文里是事后聪明的意思这个词用在它的身上再贴切不过——HER 用一种典型的事后诸葛亮方式处理失败轨迹既然你没能把滑块推到 A 点但你确实把滑块推到了 B 点那我们临时把任务改成推到 B 点你这条轨迹立刻从一条失败的垃圾轨迹变成了一条目标达成、奖励非零的成功轨迹。注意改的不是环境不是物理过程而是这条轨迹对应的任务定义。在数学上HER 做的事情是把单目标的强化学习重新框定为多目标goal-conditioned学习状态 s、动作 a、后续状态 s 全部是真实交互出来的唯独目标 g 被替换成这条轨迹自己到达过的某个状态对应的 achieved goal。效果非常作弊你原本想学如何推到 A 点而你的经验库里可能一百条都没有一条推到过 A 点但跑去学如何推到 B 点、如何推到 C 点、如何推到 D 点你能用的数据量会爆炸式增长。为什么这个操作能成立因为怎样推动滑块这一物理因果律才是学习里最珍贵的信号而推到哪个位置只是任务语义层面的不同标签。你从一百条推偏了的轨迹里照样能学到用力方向和滑块位移之间的大致关系。HER 的价值恰恰在于它绕开了必须成功一次这个在稀疏奖励环境里几乎不可能完成的前提把海量原本没有梯度可利用的探索数据重新赋予了学习价值。这句话值得再强调一遍让数据有信号比让算法更聪明往往才是解决稀疏奖励的关键。2. Hindsight 的算法机制目标重标注到底改了哪些东西2.1 从一条 transition 说起先给不熟悉 goal-conditioned 设置的读者补一个背景在这种设定下一条经验不是普通的 (s, a, s)而是 (s, a, g, r, s)。奖励 r 是根据当前这一步所达到的 achieved goal 与给定目标 g 的距离算出来的。拿 FetchSlide 举例achieved goal 就是滑块的三维位置给定目标 g 之后奖励一般是硬判定距离小于阈值给 0相当于成功否则给 -1。这和我们做二分类时的损失函数长得很像区别在于它只告诉你对错不告诉你偏了多少。HER 在采样时需要做的第一件事是决定当前这条 transition 要不要被改写。实际工程里最常用的做法是原始目标对应的那份数据保留另外按照一个比例 K 生成 K 份带替代目标的数据副本。对每个副本从某种采样策略中挑选一个新的目标 g然后用同一个 reward 函数重新计算奖励 r组成新的 transition (s, a, g, r, s) 塞进回放缓冲池参与后续的 Q 网络更新。实现层面有一个极其容易被忽略的关键点重标注只能改变目标和对应的奖励动作 a 和后续状态 s 都不能动。这意味着你必须要能从 s 中提取出 achieved goal。如果环境没把实际达到的目标量显式地塞给 agent重标注就无从下手。Fetch 系列环境里 achieved goal 通常直接取自 next_obs 的固定切片所以不费事但我在自建仿真任务里吃过一次亏观测里只有机械臂关节角没有显式的物体最终位置重标注的时候根本不知道该拿什么当替代目标。这类问题排查起来很隐蔽建议一开始就在环境定义层面把 achieved_goal 和 observation 一起预留好等出问题了再补往往牵扯一堆改动。2.2 四种替代目标的采样策略final、future、episode、randomOpenAI 论文里给出了四种挑选替代目标的方式我按照自己的实际使用体会逐个讲final直接取这个 episode 最后一步的 achieved goal 当替代目标。这是最简洁、信息密度也最高的办法因为它天然包含了离最终目标还有多远的分布信息。如果你的 episode 不长比如 50 步以内final 基本够用。future从当前时间步之后的某个状态里随机抽一个 achieved goal。相比 final目标不会永远指向 episode 远端的那个状态训练中后期不容易让 Q 函数对同一状态配一个远在天边的目标感到混乱长期训练更稳。episode从同一条 episode 的任意状态里随机抽一个。信息分布更均衡但目标与当前状态可能离得比较远噪声略大。random从整个 replay buffer 里随机选一个状态当替代目标。噪声最大基本不推荐单独使用。我自己的默认配比是 K4、策略选 future。理由很简单final 上线快但训练中后期抖动明显这个现象在目标跨度比较大的任务里尤其突出future 因为限定了只能选未来的状态目标与时序天然一致长期跑下来最省心。如果只是想快速验证 HER 是否有效直接用 final K4 也不会差太多别在选型上做太多纠结。2.3 为什么重标注不会毒害 Q 函数这是每一个第一次接触 HER 的人都会问的问题改了目标、改了奖励喂给 Q 网络的不就是假数据吗它凭什么不学歪关键要理解强化学习对训练数据的要求从来不是数据必须来自某个最优策略而是 off-policy 采样下的一致性条件。Q 网络学的是给定 (s, a, g) 时期望回报的条件期望它并没有任何能力去验证这条 (s, a, s) 到底是不是在试图达成 g 的过程中产生的。HER 改写的 transitions、a、s 全部来自真实环境交互只有 g 和 r 被按照同一个 reward 函数重新计算了一遍。你把一条本来想推到 A 点但实际推到 B 点的轨迹打上目标其实是 B 点的标签——这个标签下的奖励是合法的因为 B 点是真实到过的状态它只是改变了任务描述并没有捏造物理结果。这里最精妙的一点在于HER 限定新目标必须来自当前这条轨迹自身到过的状态。如果随机挑一个这条轨迹压根没到过的点当目标奖励虽然也能算但那条数据描述的行为是在没到过的情况下宣称到了这才会真正毒化 Q 函数。从轨迹自身上抽目标保持了物理因果的自洽却重构了任务语义。Q 函数在这种数据下学到的是如果目标设在这里、执行这个动作能带来什么——这恰恰是我们的目的。我自己一开始也有过这个顾虑所以专门做了一组对照实验把替代目标全部改成从随机状态里抽结果训练三千步后 Q estimate 明显虚高动作输出像个无头苍蝇换回 future 策略后很快恢复稳定。从那以后我才真正理解 HER 这条隐式约束到底在保护什么。3. 从论文到可跑代码在我自己的抓取任务里落地3.1 为什么我把选型放在 FetchSlide DDPG PyTorch先说明一个硬性前提HER 是 off-policy 算法的补丁不能直接用在 PPO 这类 on-policy 方法上。原因是 on-policy 方法要求当前策略生成的数据只能用来更新当前策略而你拿着旧策略产出的、目标被改写过的数据去更新新策略在理论假设上就站不住脚。所以选型的第一道坎就淘汰掉了一大半强化学习算法。我最后选了 DDPG 而不是 SAC 或 TD3主要原因是 HER 论文的原始实现就是 DDPG做对照复现最直接而且 Fetch 系列环境的动作空间只有四维DDPG 的探索噪声策略在低维连续控制里表现够用。基础网络就是两层 MLP每层 256 个隐藏单元Actor 输出加 tanh 限制在动作边界内Critic 的输入是把观测、动作、目标拼接起来输出一个 Q 值。这些细节不影响 HER 本身的移植——你完全可以把这个 buffer 逻辑原封不动地接到 SAC 或 TD3 上处理方式一模一样。环境我用的是 OpenAI Gym 老版本接口下的 FetchSlide-v0奖励是稀疏那版距离阈值 0.05 以内判定成功。这个环境最大的好处是重标注需要的 achieved goal 可以直接从 obs 的固定切片里取出来省掉了额外自定义观测的麻烦。3.2 Replay Buffer 改造核心代码逐段解释下面是我最终稳定运行的简化版 HER buffer贴出来不是让你抄完整项目而是想借代码把改写这一步的边界讲清楚import numpy as np import random class HerReplayBuffer: def __init__(self, capacity1_000_000, k4, strategyfuture): self.capacity capacity self.k k self.strategy strategy self.current_episode [] self.storage [] def add_transition(self, transition): # transition 结构: (obs, action, reward, next_obs, goal, achieved_goal) # 注意这里并不立刻入池而是等整个 episode 结束后统一处理 self.current_episode.append(transition) def end_episode(self): for idx, trans in enumerate(self.current_episode): # 原始目标的那份一定要保留 self._append(trans) # 额外生成 k 份替代目标数据 for _ in range(self.k): new_goal self._pick_alternative_goal(idx) rew self.compute_reward(trans[achieved_goal], new_goal) new_trans { obs: trans[obs], action: trans[action], reward: rew, next_obs: trans[next_obs], goal: new_goal, } self._append(new_trans) self.current_episode.clear() def _pick_alternative_goal(self, idx): if self.strategy final: return self.current_episode[-1][achieved_goal] elif self.strategy future: # 限定从 idx 之后的未来状态里选 future self.current_episode[idx:] return random.choice(future)[achieved_goal] elif self.strategy episode: return random.choice(self.current_episode)[achieved_goal] else: raise ValueError(unsupported strategy) def compute_reward(self, achieved_goal, goal): # 稀疏奖励距离小于阈值给 0成功否则 -1 dist np.linalg.norm(achieved_goal - goal) return 0.0 if dist 0.05 else -1.0 def sample(self, batch_size): return random.sample(self.storage, batch_size)有三处细节我想单独拎出来讲。第一是入池时机我故意不在 add_transition 里直接放数据而是先缓存整条 episode。因为 future 策略要求从未来状态里抽目标必须能访问到当前时间步之后的状态这就天然要求先把一条 episode 完整缓存起来再统一处理。千万别在 transition 级别直接改写否则你可能会选到同一时刻甚至更早的旧状态效果会明显变差。第二每一条原始 transition 最终会变成 1K 份。K4 意味着 buffer 里 80% 的数据都经过重标注。HER 实际上是用数据重复率换奖励信号密度代价是存储量和采样负担成倍上升。如果你的机器内存吃紧可以把 capacity 适当调小或者在 end_episode 时只保留最近几条 episode 的相关部分。第三个坑是 reward 函数的一致性上面 compute_reward 里的阈值 0.05必须和环境内部的判定保持一致。有些实现里环境判定和 HER 重标各写了一份一旦阈值不一致你会看到训练曲线莫名其妙往下掉实际是目标判定标准被悄悄改掉了。我的建议是直接在环境类里提供独立的 get_reward(achieved_goal, goal) 方法环境和 HER 都调用同一份代码从源头消灭不一致。3.3 归一化与训练入口容易被忽视的细节HER 论文里特别强调过要对观测和目标做归一化但我第一版代码偷懒没做结果训练到一百万步时成功率只有 10% 出头。问题出在 Fetch 系列的观测里混着关节角、线速度、角速度、物体位置、目标位置量纲和尺度差别极大网络要硬拟合这些输入要么需要更大容量要么会训练不稳。推荐的解法是给 observation 和 goal 各维护一个 running mean/std简单实现如下class Normalizer: def __init__(self, dim): self.n 0 self.mean np.zeros(dim) self.m2 np.ones(dim) def update(self, x): self.n 1 delta x - self.mean self.mean delta / self.n self.m2 delta * (x - self.mean) def normalize(self, x): std np.sqrt(self.m2 / max(self.n, 1)) return (x - self.mean) / (std 1e-3)我在 DDPG 里对 observation 和 goal 分别维护一个 normalizer网络输入用 norm(obs) 和 norm(goal) 拼接Critic 的输入链路也一样。这里有个容易踩的细节normalizer 必须用当前策略正在采样到的数据在线更新而不能用全量 buffer 的统计量。因为 HER 改写过的目标分布会随着策略探索变化在线更新才能跟得上这个分布漂移用全量统计容易被老数据拖慢归一化效果打折扣。训练入口就是一个标准 DDPG 循环每步采样后以一定概率用均匀随机扰动促进探索其余时间用 actor 输出加 OU 噪声。batch size 256gamma 0.98学习率 1e-3。完整 DDPG 代码就不贴了这里想特别提醒的是每次从 her_buffer.sample(256) 拿到 batch 后记得先把 batch 里的 obs 和 goal 分别过一遍 normalizer 再喂给网络。这个步骤漏掉的话上面所有归一化工作都白做了。4. 实测结果五百万步之后那一小块积木终于被拿起来了4.1 有 HER 和没有 HER 的收敛曲线对比我在 FetchSlide-v0 上跑到 500 万步做过多组对照实验先说结论没有 HER 的 DDPG 基本就是一条横线300 万步内成功率不超过 1%加上 HER 之后大约 150 万步时能看到明显的爬升300 万步时成功率稳定在 60% 到 70%。这个对比其实很有戏剧性也特别好解释。DDPG 单独跑 FetchSlide 时replay buffer 里几乎全是 0 奖励的失败轨迹Critic 学到的 Q 值到处都是相同的小负数Actor 的梯度方向近乎随机。HER 把 buffer 里一半以上数据的奖励改写成了非零信号Critic 立刻有了可以反传的信息。说白了HER 真正的贡献不在网络结构或策略更新方式而在于把本来没有信号的数据库变成了有信号的数据库。这也是它为什么能和 DDPG、SAC、TD3 这类算法无缝拼接的原因——它改的是数据入口不是学习算法本身。我经常跟人讲一个比较粗俗但好记的比喻在一个奖励稀疏到极点的任务里把奖励信号密度提升十倍比换任何花哨的 policy 更新算法都更管用。这既是 HER 最底层的原理也是它最容易被低估的工程价值。4.2 回放比例 K 与目标策略的影响K 值代表每条原始 transition 额外生成多少份改写数据它和替代目标策略共同决定了数据集的奖励密度和噪声水平。我在同一环境下做过一组消融实验结果是这样配置100万步成功率300万步成功率备注DDPG无 HER~0%~1%几乎学不动HER K1 final12%35%能学但信号仍然偏稀HER K4 future31%68%我日常使用的默认配置HER K4 final25%55%前期冲得快中后期抖动HER K8 episode22%52%K 到 8收益开始递减几个很明显的现象K1 时虽然也有提升但 50% 的数据还是原始目标密度提升有限K4 时 80% 的数据经过改写训练速度明显加快继续把 K 加到 8成功率增长反而放缓而内存和训练开销接近翻倍。原因在于替代目标生成得太激进会让不同 transition 之间的目标差异过大Critic 难以稳定收敛。我的建议是先用 K4 起步之后再根据任务复杂度在 2 到 6 之间微调。4.3 一个反直觉的细节探索噪声别调太小大部分人的第一反应是在策略逐渐稳定后把动作噪声调低我在早期实验里也这么做了。但在稀疏奖励加 HER 的场景下前期探索噪声太小等于自杀。agent 必须在早期多乱撞、多产生各种位置的终点状态HER 才有足够多的替代目标数据可以挖。我把 OU 噪声的 sigma 从 0.2 压到 0.05 之后前期学习速度直接掉了一半。当然噪声也不是越大越好。sigma 超过 0.5 时agent 轨迹太随机替代目标分布太散relabel 出来的数据几乎全是远离目标的成功Critic 会逐渐学出一种虚假的乐观主义Q 值越估越高策略表现却越来越差。我目前实践出来的做法是 sigma 在 0.1 到 0.3 之间动态调整训练前期保持 0.25 左右让 agent 充分探索当成功率过 30% 以后再逐渐降到 0.1。这个参数论文里基本不会写完全属于跑实验跑出来的土经验。5. 踩坑复盘那些论文不会写、但跑实验必踩的雷5.1 观测向量里忘了拼 goal这是新手最容易踩的坑也最隐蔽。如果你的观测没有把 goal 拼进去网络根本不知道任务目标是什么自然学不出往目标走的策略。很多实现里状态向量和 goal 是分开存储的最终送入网络前再拼接这本身没问题问题在于这个拼接点非常容易漏。我见过有人在拼接那一步少了一个维度导致 Actor 和 Critic 的输入维度对不上训练过程里没什么明显报错直到曲线飞了才发现原因。建议在网络入口那一行加个断言obs.shape[-1] goal.shape[-1] 必须等于 actor 输入维度让错误尽早暴露而不是拖到训练后期。5.2 episode 边界处理千万别跨 episode 选 future 目标我在前面代码里把 episode 整条缓存、结束后统一处理基本动机就是为了防止 future 策略选到另一个 episode 里的未来状态。有些人图省事直接在 replay buffer 里维护一个未来 N 步状态指针一不小心就会把不同 episode 的数据混在一起。这个错误会导致 relabel 目标在语义上完全错位拿 A episode 的行为数据配上 B episode 里才到过的目标。训练曲线在这种 bug 下往往不是不涨而是涨到一半突然崩盘排查起来非常头疼。我现在的推荐做法始终是上面那种一个 episode 维护一个列表缓存结束后统一入池从结构上杜绝跨界。5.3 监控替代目标的合理距离最后一个习惯是我后期才养成的但它帮我躲过了好几次看不见的爆雷。在训练循环里加一个自定义指标采样一个 batch 后计算每个替代目标和对应 transition 中 achieved_goal 的 L2 距离然后求平均。这个均值如果明显偏大比如超过任务特征尺寸的 1.2 倍说明 relabel 出来的目标离行为轨迹太远Q 函数很可能在被虚幻的成功带偏。具体到 FetchSlide 这块场景滑块活动范围大致在一个边长接近 1 的桌面上如果平均替代目标距离长期超过 0.8我就会考虑调低 K或者把目标策略从 future 换回 final。保持这个指标在训练后期逐渐收窄是一种很好的健康度信号。加了这个监控之后很多原本要等几百步训练崩掉才能发现的隐患现在几万步之内就能提前暴露出来调试效率完全不在一个量级。5.4 一点题外话事后聪明在工程里的另一层含义文章写到最后我想说点题外话。hindsight 这个词除了指 Hindsight Experience Replay日常含义是事后聪明也就是我们常说的复盘。在工程协作里其实每天都发生着类似 HER 的事一个算法跑崩了一个项目延期了事后复盘时总会有人说当时要是这样做就好了。但 HER 给了我一个更具体的视角复盘的价值不在后悔而在把失败的经验重新打上可学习的标签。你不需要等做成一件事才从经验里学习。只要能从没做到设定目标的经历里提炼出实际上接近了什么的信号你就在进步。我后来带项目时会刻意要求团队成员给每次失败的实验留一份复盘清单这次虽然没有成功但哪一步确实把任务向前推了一点哪一步是将来可复用的一块阶梯。这和 HER 的目标重标注逻辑如出一辙——把负样本转化成正样本数据集在算法层面和做事层面都是极其划算的一笔投资。这是我跑完 HER 之后最想分享的一层体会。
返回列表