ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

强化学习稀疏奖励难题:基于预测误差的好奇心驱动探索实战

强化学习稀疏奖励难题:基于预测误差的好奇心驱动探索实战 1. 这篇文章真正要解决的问题“以好奇心为优化目标”听起来像一句哲学口号但它正成为AI研究特别是强化学习领域一个极具潜力的技术范式。对于开发者而言这背后是一个尖锐的工程问题当环境反馈稀疏、奖励信号难以设计时如何让智能体Agent学会主动探索而不是陷入“躺平”或重复无效动作传统强化学习严重依赖精心设计的奖励函数。在游戏里得分就是奖励在机器人控制中到达目标点就是奖励。但在许多现实场景中比如让AI学习玩一个没有明确计分规则的新游戏或者让一个机器人自主探索未知的物理环境我们很难甚至无法定义一个完美的奖励函数。智能体可能因为得不到任何正向反馈而停止探索或者只找到一种能获得微薄奖励的次优策略并固步自封。“好奇心驱动”正是为了解决这个“探索困境”。它不直接优化外部任务奖励而是将“好奇心”——一种对未知、对预测误差的内在驱动力——作为优化目标。这篇文章要解决的就是如何将这一抽象概念落地为可运行的代码让一个智能体真正学会“为了学习而学习”。我们将从原理拆解开始一步步构建一个基于好奇心驱动的探索智能体并分析它在实践中的威力与陷阱。读完本文你将能理解好奇心机制的核心并能在自己的项目中实现或集成类似的探索策略。2. 基础概念与核心原理要理解“以好奇心为优化目标”我们需要先厘清几个关键概念外部奖励、内在奖励以及好奇心的具体量化形式。外部奖励 (Extrinsic Reward)这是环境给予的、与最终任务目标直接相关的反馈。例如游戏得分、完成订单的收益、机器人到达终点的信号。它是强化学习的传统优化目标。内在奖励 (Intrinsic Reward)这是智能体自己生成的一种奖励信号用于鼓励某些有利于长期学习的行为即使这些行为短期内可能不会带来外部奖励。好奇心就是一种最典型的内在奖励形式。那么如何用数学或工程方法定义“好奇心”呢主流方法通常基于“预测误差”或“信息增益”。基于预测误差的好奇心这是最直观也最常用的方法。其核心思想是智能体拥有一个对自身行为后果的预测模型称为“世界模型”或“动态模型”。当智能体执行一个动作并观察到新状态时如果这个新状态与预测模型的输出差异很大说明发生了“意外”或“新知”智能体就应该对此感到“好奇”并给予自己一个正向的内在奖励。奖励大小与预测误差成正比。通俗解释就像一个婴儿不断扔东西他其实在做一个实验——“我松手东西会掉下去吗”每次结果都符合他的简单预测重力他就不那么好奇了。但如果某次东西飘起来了预测误差极大他就会异常好奇反复尝试。技术定义智能体学习一个函数f(s_t, a_t) - s_{t1}用于预测下一状态。好奇心奖励r_t^i η * || s_{t1} - f(s_t, a_t) ||^2其中η是缩放系数。基于信息增益的好奇心这种方法更形式化它鼓励智能体采取那些能最大程度减少环境模型不确定性的行动。智能体通过探索来获取信息从而更快地学习一个准确的环境模型。通俗解释一个侦探面对多个嫌疑人他会优先去调查那个能最大程度区分谁是真凶的线索这个线索的信息增益最大。技术定义通常涉及计算在采取某个动作前后环境模型参数分布的变化如KL散度计算量较大。在工程实践中基于预测误差的方法因其相对简单和有效而被广泛采用例如在著名的ICM (Intrinsic Curiosity Module)和RND (Random Network Distillation)等算法中。本文后续的实践也将围绕这一思路展开。3. 环境准备与前置条件我们将使用 Python 和 PyTorch 框架在经典的gym环境如CartPole-v1和一个更复杂的稀疏奖励环境MountainCar-v0中演示好奇心驱动的效果。MountainCar-v0是一个典型范例小车只有到达山顶旗杆处才能获得1的奖励否则每一步都是-1的惩罚。没有好奇心驱动智能体很难学会通过左右摇摆积累动量最终冲上山坡。环境准备清单操作系统Linux / macOS / Windows (建议使用 Linux 或 WSL2 以获得最佳兼容性)Python 版本3.8 或以上核心依赖库gym/gymnasium: 提供强化学习环境。torch: 深度学习框架用于构建神经网络。numpy: 数值计算。matplotlib: 用于结果可视化可选。版本说明以下代码基于较新的gymnasiumOpenAI Gym 的维护分支和PyTorch。请以您实际项目的环境为准本文重点演示通用思路。安装命令# 创建并激活虚拟环境推荐 python -m venv curiosity-env source curiosity-env/bin/activate # Linux/macOS # curiosity-env\Scripts\activate # Windows # 安装依赖 pip install gymnasium torch numpy matplotlib4. 核心流程拆解构建一个好奇心驱动智能体我们将实现一个简化版的 ICM内在好奇心模块。整个智能体由三部分组成策略网络、环境预测模型好奇心模块和价值网络。训练流程也相应分为两个并行的优化目标。整体架构图文字描述智能体观察当前状态s_t。策略网络根据s_t输出动作a_t。环境执行a_t返回新状态s_{t1}和外部奖励r_t^e可能为0或负值。好奇心模块工作流 a. 特征编码器将s_t和s_{t1}编码为特征向量φ(s_t),φ(s_{t1})。 b. 逆动力学模型根据φ(s_t)和φ(s_{t1})预测执行的动作â_t。 c. 前向动力学模型根据φ(s_t)和a_t预测下一状态的特征φ(s_{t1})。 d. 计算预测误差作为内在奖励r_t^i || φ(s_{t1}) - φ(s_{t1}) ||^2。总奖励r_t r_t^e β * r_t^i其中β是内在奖励的权重系数。使用总奖励r_t通过 PPO 或 A2C 等策略梯度算法更新策略网络和价值网络。使用动作a_t和特征φ(s_{t1})的误差来更新逆动力学模型和前向动力学模型。关键点逆动力学模型的存在是为了让特征编码器φ学习到与动作相关的状态特征避免它关注那些不可控的、噪声大的环境细节比如电视屏幕上的飘雪从而让好奇心聚焦于“可影响”的未知。5. 完整示例与代码实现下面我们分模块实现这个好奇心驱动智能体。为了清晰我们使用 A2C (Advantage Actor-Critic) 作为基础策略优化算法。文件结构curiosity_agent/ ├── curiosity_module.py # 好奇心模块ICM实现 ├── a2c_agent.py # A2C 智能体实现 └── train.py # 主训练脚本5.1 好奇心模块实现 (curiosity_module.py)import torch import torch.nn as nn import torch.nn.functional as F class CuriosityModule(nn.Module): 内在好奇心模块 (ICM) 包含特征编码器、逆动力学模型、前向动力学模型 def __init__(self, state_dim, action_dim, feature_dim256, lr1e-3): super(CuriosityModule, self).__init__() self.feature_dim feature_dim # 特征编码器将状态映射到特征空间 self.feature_net nn.Sequential( nn.Linear(state_dim, 128), nn.ReLU(), nn.Linear(128, feature_dim), nn.ReLU() ) # 逆动力学模型从连续特征预测动作假设动作是离散的用分类 self.inverse_net nn.Sequential( nn.Linear(feature_dim * 2, 256), # 输入是 φ(s_t) 和 φ(s_{t1}) 的拼接 nn.ReLU(), nn.Linear(256, action_dim) # 输出每个动作的logit ) # 前向动力学模型从特征和动作预测下一状态的特征 self.forward_net nn.Sequential( nn.Linear(feature_dim action_dim, 256), # 输入是 φ(s_t) 和 a_t (one-hot) nn.ReLU(), nn.Linear(256, feature_dim) # 输出预测的 φ(s_{t1}) ) self.optimizer torch.optim.Adam(self.parameters(), lrlr) def forward(self, state, next_state, action): 计算内在奖励并更新好奇心模块 :param state: 当前状态 [batch_size, state_dim] :param next_state: 下一状态 [batch_size, state_dim] :param action: 执行的动作整数索引[batch_size] :return: 内在奖励 [batch_size] # 1. 编码特征 phi_state self.feature_net(state) phi_next_state self.feature_net(next_state) # 2. 逆动力学损失鼓励特征包含与动作相关的信息 inverse_input torch.cat([phi_state, phi_next_state], dim1) pred_action_logits self.inverse_net(inverse_input) inverse_loss F.cross_entropy(pred_action_logits, action) # 3. 前向动力学损失计算预测误差作为内在奖励 action_one_hot F.one_hot(action, num_classespred_action_logits.size(-1)).float() forward_input torch.cat([phi_state, action_one_hot], dim1) pred_next_phi self.forward_net(forward_input) forward_loss F.mse_loss(pred_next_phi, phi_next_state.detach()) # 注意detach # 内在奖励是前向预测误差 intrinsic_reward forward_loss.detach() # 从计算图中分离仅作为奖励信号 # 4. 更新好奇心模块 total_loss (1.0 * inverse_loss) (1.0 * forward_loss) self.optimizer.zero_grad() total_loss.backward() self.optimizer.step() return intrinsic_reward关键逻辑解释feature_net学习提取状态的有用特征。inverse_net确保提取的特征与动作强相关避免关注无关噪声。forward_net的预测误差 (forward_loss) 被直接用作内在奖励intrinsic_reward。注意这里对phi_next_state使用了.detach()是为了防止奖励信号影响特征编码器的学习使其保持稳定。好奇心模块的更新 (total_loss.backward()) 独立于策略网络的更新。5.2 A2C 智能体实现 (a2c_agent.py)import torch import torch.nn as nn import torch.optim as optim from torch.distributions import Categorical class ActorCritic(nn.Module): A2C 算法的 Actor-Critic 网络 def __init__(self, state_dim, action_dim, hidden_dim128): super(ActorCritic, self).__init__() # 共享的特征提取层 self.shared nn.Sequential( nn.Linear(state_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, hidden_dim), nn.ReLU() ) # 策略头 (Actor) self.actor nn.Linear(hidden_dim, action_dim) # 价值头 (Critic) self.critic nn.Linear(hidden_dim, 1) def forward(self, x): shared_features self.shared(x) return self.actor(shared_features), self.critic(shared_features) class A2CAgent: def __init__(self, state_dim, action_dim, lr3e-4, gamma0.99, beta0.1): self.policy_net ActorCritic(state_dim, action_dim) self.optimizer optim.Adam(self.policy_net.parameters(), lrlr) self.gamma gamma # 折扣因子 self.beta beta # 内在奖励权重 def select_action(self, state): 根据策略选择动作 state torch.FloatTensor(state).unsqueeze(0) logits, value self.policy_net(state) probs F.softmax(logits, dim-1) m Categorical(probs) action m.sample() return action.item(), m.log_prob(action), value def update(self, rewards, log_probs, values, intrinsic_rewardsNone): 更新策略网络和价值网络 :param intrinsic_rewards: 好奇心模块提供的内在奖励列表 returns [] R 0 # 计算折扣回报 for r in reversed(rewards): R r self.gamma * R returns.insert(0, R) returns torch.FloatTensor(returns) # 如果提供了内在奖励将其加权后加到外部奖励上这里简化处理实际可按时间步融合 # 注意更精细的做法是在每一步计算总奖励 r_t^e beta * r_t^i # 此处为演示假设intrinsic_rewards是每个episode的平均内在奖励用于调整优势 if intrinsic_rewards is not None: # 一个简单的融合示例增强价值目标的信号 returns returns self.beta * torch.FloatTensor(intrinsic_rewards).mean() log_probs torch.stack(log_probs) values torch.stack(values).squeeze() advantages returns - values # 计算策略损失和价值损失 actor_loss -(log_probs * advantages.detach()).mean() critic_loss advantages.pow(2).mean() # 总损失 loss actor_loss 0.5 * critic_loss # 反向传播更新 self.optimizer.zero_grad() loss.backward() self.optimizer.step()5.3 主训练脚本 (train.py)import gymnasium as gym import numpy as np from curiosity_module import CuriosityModule from a2c_agent import A2CAgent def train(env_nameMountainCar-v0, num_episodes5000): env gym.make(env_name) state_dim env.observation_space.shape[0] action_dim env.action_space.n agent A2CAgent(state_dim, action_dim, beta0.1) # beta控制好奇心权重 curiosity CuriosityModule(state_dim, action_dim) for episode in range(num_episodes): state, _ env.reset() episode_reward 0 episode_intrinsic_rewards [] log_probs [] values [] rewards [] done False truncated False step 0 max_steps 1000 while not (done or truncated) and step max_steps: # 1. 选择动作 action, log_prob, value agent.select_action(state) # 2. 与环境交互 next_state, extrinsic_reward, done, truncated, info env.step(action) # 3. 计算内在奖励好奇心 # 将numpy数组转换为torch张量 state_tensor torch.FloatTensor(state).unsqueeze(0) next_state_tensor torch.FloatTensor(next_state).unsqueeze(0) action_tensor torch.LongTensor([action]) intrinsic_reward curiosity(state_tensor, next_state_tensor, action_tensor) episode_intrinsic_rewards.append(intrinsic_reward.item()) # 4. 存储数据 (这里使用纯外部奖励内在奖励通过curiosity模块间接影响策略) # 也可以将总奖励 extrinsic_reward beta * intrinsic_reward.item() 存入rewards rewards.append(extrinsic_reward) log_probs.append(log_prob) values.append(value) state next_state episode_reward extrinsic_reward step 1 # 5. 一个episode结束更新智能体策略 # 传递内在奖励的平均值给agent.update用于调整回报简化处理 avg_intrinsic np.mean(episode_intrinsic_rewards) if episode_intrinsic_rewards else 0 agent.update(rewards, log_probs, values, intrinsic_rewards[avg_intrinsic]) if episode % 100 0: print(fEpisode {episode}, Total Reward: {episode_reward:.2f}, Avg Intrinsic: {avg_intrinsic:.4f}) env.close() if __name__ __main__: train()6. 运行结果与效果验证运行python train.py后你将在控制台看到类似如下的输出Episode 0, Total Reward: -1000.00, Avg Intrinsic: 0.1234 Episode 100, Total Reward: -1000.00, Avg Intrinsic: 0.4567 Episode 200, Total Reward: -800.00, Avg Intrinsic: 0.7890 Episode 300, Total Reward: -500.00, Avg Intrinsic: 1.2345 Episode 400, Total Reward: -200.00, Avg Intrinsic: 1.5678 Episode 500, Total Reward: 50.00, Avg Intrinsic: 0.9876 ... Episode 2000, Total Reward: 200.00, Avg Intrinsic: 0.0123如何解读结果初期约前300轮Total Reward非常低接近 -1000即每一步都受罚因为小车找不到上山的方法。但Avg Intrinsic平均内在奖励在上升这表明好奇心模块正在工作智能体因为探索新状态而获得内在奖励。中期约300-1000轮Total Reward开始缓慢提升Avg Intrinsic可能达到峰值。此时智能体在好奇心的驱动下尝试了各种摇摆方式偶然发现了积累动量的方法。后期1000轮以后Total Reward稳定在一个较高的正值成功到达终点获得1奖励并提前结束回合惩罚步数减少而Avg Intrinsic显著下降。这是因为环境对智能体而言已不再“新奇”预测模型变得准确预测误差内在奖励自然减小。智能体的行为从“探索”转向“利用”已学到的成功策略。验证成功的关键指标最终性能在MountainCar-v0中智能体能否在远少于1000步的惩罚步数内成功到达终点即获得明显大于 -1000 的总奖励。与无好奇心基线的对比你可以注释掉好奇心模块的奖励计算和传递仅用外部奖励训练。在稀疏奖励环境下基线模型很可能永远无法成功总奖励始终接近 -1000而有好奇心的模型则能学会。可视化可以绘制Total Reward和Avg Intrinsic Reward随训练轮次的变化曲线。理想的曲线是内在奖励先升后降外部奖励最终收敛到一个高性能水平。7. 常见问题与排查思路问题现象可能原因排查方式解决方案训练不稳定奖励震荡剧烈内在奖励权重β设置过大淹没了外部奖励信号。观察内在奖励与外部奖励的量级比例。绘制两者的曲线。调小β(如从 0.1 调到 0.01, 0.001)。使用自适应权重或归一化内在奖励。智能体陷入“无意义抖动”特征编码器学习失败内在奖励可能来源于环境噪声如随机粒子效果。检查逆动力学模型的预测准确率。如果准确率很低说明特征与动作无关。增大逆动力学损失的权重确保特征编码器聚焦于可控因素。尝试更稳定的特征编码网络结构。内在奖励很快降为零智能体停止探索预测模型前向动力学学习过快过早地“理解”了环境。查看前向动力学损失是否迅速收敛到接近0。引入随机性如对预测模型使用 Dropout。或使用RND等方法其“随机目标网络”能提供永不枯竭的新奇感。内存溢出 (OOM)在循环中未及时清空计算图或存储了过多的中间张量。检查训练循环中是否有不必要的.detach()遗漏或张量是否被长期引用。确保在update后清空 (optimizer.zero_grad())及时将.detach()后的张量移出GPU。使用torch.cuda.empty_cache()。在简单环境如CartPole中效果不明显甚至变差环境本身奖励密集探索需求不高。好奇心引入的噪声反而干扰了策略学习。对比有/无好奇心模块在简单环境下的收敛速度和最终得分。在奖励密集、状态空间小的环境中可以禁用或使用极小的好奇心权重。好奇心主要适用于稀疏奖励、大状态空间的探索问题。8. 最佳实践与工程建议环境适配与权重调参β 是超参数没有银弹。对于全新的环境建议从一个较小的值如 0.01开始根据智能体早期是探索不足还是行为混乱来调整。奖励归一化将内在奖励和外部奖励分别进行归一化如减去均值、除以标准差使其处于相近的量级能极大提升训练稳定性。课程学习可以先在一个简化或奖励重塑的环境中进行预训练再利用好奇心探索更复杂的原始环境。好奇心模块的工程优化特征编码器的选择对于图像输入应使用卷积神经网络 (CNN)对于物理状态使用全连接网络 (MLP) 即可。确保编码器的输出维度 (feature_dim) 足够表达状态信息但不宜过大以免过拟合。分离训练节奏可以考虑让好奇心模块世界模型比策略网络更新得更快或更慢以平衡探索与利用。使用更先进的算法本文的 ICM 是入门范例。生产环境或研究中可考虑RND (Random Network Distillation)通过蒸馏一个随机初始化的固定“目标网络”来定义新奇性避免了预测模型过拟合导致的探索停止。NGU (Never Give Up)结合了基于生命周期的内在好奇心短期和基于嵌入相似度的内在好奇心长期是更强大的探索算法。安全与可控性设置内在奖励上限避免因某个状态预测误差极大而导致智能体“沉迷”于某个无意义的动作循环。可以对内在奖励进行裁剪 (torch.clamp) 或使用tanh函数压缩。在安全环境中测试好奇心驱动可能使智能体产生不可预测的行为。务必在模拟器或完全可控的测试环境中充分验证后再部署到物理系统。监控与干预训练过程中实时监控内在奖励、外部奖励和策略熵。如果策略熵降为零完全确定可能意味着探索停止需要人工干预或调整参数。与现有框架集成本文示例是 from-scratch 实现便于理解。在实际项目中你可以将好奇心模块封装成一个RewardWrapper轻松与Stable-Baselines3,Ray RLlib等主流强化学习库集成。这样可以利用这些库中成熟的 PPO、SAC 等算法专注于好奇心部分的设计。9. 总结与后续学习方向“以好奇心为优化目标”不仅仅是一个巧妙的算法技巧它代表了一种范式转变从让AI执行明确指令到让AI具备自主发现问题和学习解决方案的能力。我们通过实现一个 ICM 模块亲身体验了如何将这种内在动机编码进损失函数从而解决稀疏奖励这一强化学习中的经典难题。本文的核心价值在于打通了从原理到实现的路径。你不仅理解了基于预测误差的好奇心是如何计算的更重要的是你看到了它如何与标准的策略梯度算法A2C协同工作看到了超参数β的关键作用也看到了它在训练过程中表现出的典型模式——内在奖励的先升后降。下一步你可以从以下几个方向深化挑战更复杂的环境尝试在MiniGrid,Procgen等视觉输入、部分可观测的稀疏奖励环境中测试你的好奇心智能体。实现并对比 RND研究并编码实现 RND 算法。你会发现它不需要学习预测模型而是通过一个随机固定网络来提供持续的新奇性信号思考其相对于 ICM 的优缺点。探索离散-连续动作空间本文示例是离散动作如左、右。尝试将其扩展到连续动作空间如机器人关节扭矩这需要修改逆动力学模型的输出层和损失函数。研究好奇心在分层强化学习中的应用让上层控制器学习设置内在目标如“到达某个未探索的区域”下层控制器学习实现该目标的具体技能这是让AI学会复杂分层任务的前沿方向。好奇心驱动的研究方兴未艾它连接着强化学习、元学习乃至通用人工智能的梦想。希望这篇从理论到实战的长文能成为你探索这片充满惊喜的领域的一块坚实垫脚石。建议收藏本文代码在遇到下一个“奖励稀疏”的难题时不妨试试赋予你的智能体一点“好奇心”。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进