TensorFlow实现好奇心驱动强化学习:基于ICM模块解决稀疏奖励问题

发布时间:2026/8/27 6:36:54
TensorFlow实现好奇心驱动强化学习:基于ICM模块解决稀疏奖励问题 1. 项目概述当智能体学会“好奇”在强化学习的传统范式里我们习惯于用明确的奖励信号来“驯化”智能体。比如在《毁灭战士》Doom里击中敌人给正分被击中扣分智能体就朝着高分目标去优化。但现实世界尤其是开放、复杂的环境往往没有这么清晰的“记分牌”。想象一下你让一个机器人去探索一个未知的废墟没有“找到宝藏100分”的提示它可能走两步就停在原地因为原地不动是最“安全”、最“节能”的策略——这被称为“稀疏奖励”问题。“noreward-rl”这个项目直译就是“无奖励强化学习”它挑战的正是这个核心难题。其背后的思想是让智能体学会“好奇”。它不再被动等待外部奖赏而是主动去探索环境中那些它“不理解”或“预测不准”的部分将预测误差本身转化为一种内在驱动力。这就像人类婴儿会不自觉地伸手去抓晃动的物体不是因为有人告诉他“抓到玩具1分”而是因为那个物体的运动轨迹超出了他当前的预测模型这种“认知失调”驱动他去探索和学习。这次我们要用TensorFlow将这套“好奇心驱动”的智能体放进《毁灭战士》这个经典的3D第一人称射击游戏环境里。Doom环境视觉复杂、动作空间连续、状态信息丰富且奖励极其稀疏只有杀死敌人或捡到物品才有奖励是检验这类内在动机算法绝佳的试验场。对于刚接触强化学习尤其是对“稀疏奖励”和“内在动机”感到好奇的开发者来说这个项目能让你亲手搭建一个不依赖外部奖励也能自主探索的智能体理解其核心原理并看到它在复杂环境中的实际表现。2. 核心原理拆解好奇心从何而来要构建一个好奇心驱动的智能体我们首先要理解它的“燃料”是什么。在noreward-rl的框架中核心是基于预测误差的内在好奇心模块Intrinsic Curiosity Module, ICM。这个模块为智能体提供了一个永不枯竭的内在奖励源。2.1 智能体的“双重大脑”一个完整的好奇心驱动智能体通常由两个并行的神经网络模块构成策略网络Policy Network这是智能体的“决策脑”。它接收当前的环境观测比如游戏的一帧画面输出一个动作概率分布比如向前走、左转、开火等。它的目标是最大化总奖励而这个总奖励是外部奖励如果有和内在奖励之和。内在好奇心模块ICM这是智能体的“好奇脑”。它专门负责计算内在奖励。ICM本身又包含三个子网络特征编码器Φ将高维、冗余的原始观测如图像压缩成一个低维、抽象的特征向量。这个步骤至关重要它过滤掉了环境中与智能体动作无关的、变化无常的视觉噪声比如墙上的纹理闪烁、光影变化让智能体关注于可被自身动作影响的实体部分。逆向模型Inverse Model输入当前状态的特征和下一个状态的特征试图预测出导致这个状态转移的动作。它的训练目标是让预测的动作与实际执行的动作尽可能接近。这个模型帮助智能体理解“我的动作会对环境产生什么影响”。正向模型Forward Model输入当前状态的特征和智能体执行的动作试图预测出下一个状态的特征。它的训练目标是让预测的下一个状态特征与实际的下一个状态特征尽可能接近。2.2 内在奖励的计算预测误差即惊喜内在奖励r^i的核心计算公式非常简单r^i_t η * || Φ(s_{t1}) - \hat{Φ}(s_{t1}) ||^2其中Φ(s_{t1})是下一个状态经过特征编码器得到的真实特征。\hat{Φ}(s_{t1})是正向模型根据当前状态特征和动作预测出的下一个状态特征。|| ... ||^2代表计算二者之间的均方误差MSE。η是一个缩放系数用于平衡内在奖励和外部奖励的量级。这个误差就是“好奇心”的量化体现。智能体去到那些它当前模型预测不准的状态时就会获得高的内在奖励。这激励它去探索那些新颖的、未曾充分理解的区域。注意为什么需要特征编码器这是ICM设计中最精妙的一点。如果没有特征编码器正向模型去直接预测原始的像素级下一帧图像那么环境中任何不可控的、随机的变化比如树叶摇动、无关的NPC移动都会产生巨大的预测误差。智能体就会被这些“视觉干扰”所吸引陷入一种“看电视雪花屏”式的无意义探索。特征编码器通过自监督学习学会了只编码与智能体自身动作相关的信息从而确保了内在奖励真正来源于对“自身行为影响世界”的理解误差。2.3 整体训练流程整个系统的训练是一个双线程的优化过程ICM模块的优化最小化逆向模型的动作预测误差和正向模型的状态预测误差。这相当于在教“好奇脑”更好地理解环境动力学。策略网络的优化使用诸如PPO、A3C等策略梯度算法最大化由r^e外部奖励 r^i内在奖励构成的总奖励。策略网络利用“好奇脑”提供的内在奖励信号学习如何主动探索。3. 环境搭建与预处理工欲善其事必先利其器。在写代码之前我们需要把实验环境搭建好。3.1 VizDoom游戏环境配置我们使用vizdoom这个Python库来创建Doom环境。它通过一套API允许我们以程序控制的方式运行Doom游戏。# 安装vizdoom可能需要先安装一些系统依赖如CMake和SDL2 # Ubuntu/Debian示例 sudo apt-get update sudo apt-get install cmake libsdl2-dev pip install vizdoom接下来我们需要一个.cfg配置文件和一个.wad游戏资源文件。VizDoom提供了一些基础场景。对于好奇心探索实验一个经典的场景是“MyWayHome”或“Explore”。这些场景地图不大目标明确找到出口或特定物品但没有任何中间奖励完美契合稀疏奖励设定。import vizdoom as vzd def create_doom_env(scenario_path): game vzd.DoomGame() # 加载场景配置文件 game.load_config(scenario_path) # 设置屏幕分辨率为了训练效率通常不会用原生分辨率 game.set_screen_resolution(vzd.ScreenResolution.RES_160X120) # 设置灰度模式减少输入维度 game.set_screen_format(vzd.ScreenFormat.GRAY8) # 关闭游戏声音和自动渲染提升速度 game.set_sound_enabled(False) game.set_window_visible(False) # 训练时关闭可视化评估时再打开 # 设置动作空间。例如一个简单的离散动作集[前进 左转 右转] actions [[True, False, False], [False, True, False], [False, False, True]] game.set_available_actions(actions) game.init() return game, actions3.2 观测预处理管道从Doom获取的原始图像是(120, 160, 1)的灰度图。直接扔给神经网络效率低下且包含大量无关信息。我们需要一个预处理管道降采样与裁剪将图像缩放到更小的尺寸如(84, 84)。有时还会裁剪掉上下部分不包含游戏信息的区域。帧堆叠单张静态图片无法提供速度、方向等动态信息。标准的做法是连续取4帧在通道维度上堆叠起来形成(84, 84, 4)的输入。这相当于给了网络一个短暂的“记忆”。归一化将像素值从[0, 255]归一化到[0, 1]或[-1, 1]有助于训练稳定性。import cv2 import numpy as np class Preprocessor: def __init__(self, frame_size(84, 84)): self.frame_size frame_size self.frame_stack [] def process(self, observation): # observation 是 vizdoom 返回的屏幕缓冲区 # 1. 转换为numpy数组并调整大小 gray_frame cv2.resize(observation, self.frame_size, interpolationcv2.INTER_AREA) # 2. 归一化 normalized_frame gray_frame.astype(np.float32) / 255.0 # 3. 帧堆叠管理 if len(self.frame_stack) 0: # 如果是第一帧用同一帧初始化堆叠 self.frame_stack [normalized_frame] * 4 else: self.frame_stack.pop(0) # 移除最旧的一帧 self.frame_stack.append(normalized_frame) # 加入最新帧 # 堆叠成一个张量 (H, W, C) 其中 C4 stacked_state np.stack(self.frame_stack, axis-1) # 调整维度顺序为 (C, H, W) 以适应某些框架 # stacked_state np.transpose(stacked_state, (2, 0, 1)) return stacked_state def reset(self): self.frame_stack []3.3 TensorFlow与依赖项确保你的Python环境已安装TensorFlow 2.x。对于强化学习我们通常还会用到gym或gymnasium来封装环境接口虽然VizDoom有自己的API但用gym包装可以保持代码与其他RL环境的一致性。此外numpy,opencv-python也是必需的。pip install tensorflow gym numpy opencv-python实操心得虚拟环境是必须的。强化学习项目依赖多版本易冲突。强烈建议使用conda或venv创建一个独立的Python环境。例如conda create -n doom_rl python3.8然后在该环境中安装所有包。这能避免无数令人头疼的依赖问题。4. 神经网络模型构建我们将用TensorFlow的Keras API来构建策略网络和好奇心模块。这里采用一个相对简洁但有效的架构。4.1 特征编码器Φ网络特征编码器的任务是将预处理后的图像(84, 84, 4)映射到一个低维特征向量比如256维。我们使用一个小型CNN。import tensorflow as tf from tensorflow.keras import layers, Model class FeatureEncoder(Model): def __init__(self, feature_dim256): super(FeatureEncoder, self).__init__() self.conv1 layers.Conv2D(32, kernel_size8, strides4, activationrelu, paddingsame) self.conv2 layers.Conv2D(64, kernel_size4, strides2, activationrelu, paddingsame) self.conv3 layers.Conv2D(64, kernel_size3, strides1, activationrelu, paddingsame) self.flatten layers.Flatten() self.dense layers.Dense(feature_dim, activationlinear) # 可选加入层归一化稳定训练 self.ln layers.LayerNormalization() def call(self, inputs): # 输入形状: (batch, 84, 84, 4) x self.conv1(inputs) # - (batch, 21, 21, 32) x self.conv2(x) # - (batch, 11, 11, 64) x self.conv3(x) # - (batch, 11, 11, 64) x self.flatten(x) # - (batch, 11*11*647744) x self.dense(x) # - (batch, feature_dim) x self.ln(x) # 归一化特征 return x4.2 逆向模型与正向模型逆向模型和正向模型都是全连接网络它们以特征向量为输入。class InverseModel(Model): 预测动作输入 φ(s_t), φ(s_{t1}) 输出动作logits def __init__(self, num_actions): super(InverseModel, self).__init__() # 将当前特征和下一时刻特征拼接 self.dense1 layers.Dense(256, activationrelu) self.dense2 layers.Dense(128, activationrelu) self.output_layer layers.Dense(num_actions, activationlinear) # 未归一化的logits def call(self, state_feat, next_state_feat): x tf.concat([state_feat, next_state_feat], axis-1) x self.dense1(x) x self.dense2(x) action_logits self.output_layer(x) return action_logits class ForwardModel(Model): 预测下一状态特征输入 φ(s_t), a_t 输出预测的 φ(s_{t1}) def __init__(self, feature_dim, num_actions): super(ForwardModel, self).__init__() # 将状态特征和动作的one-hot编码拼接 self.dense1 layers.Dense(256, activationrelu) self.dense2 layers.Dense(256, activationrelu) self.output_layer layers.Dense(feature_dim, activationlinear) # 预测特征无激活 def call(self, state_feat, action): # action 是 one-hot 编码后的形状 (batch, num_actions) x tf.concat([state_feat, action], axis-1) x self.dense1(x) x self.dense2(x) pred_next_feat self.output_layer(x) return pred_next_feat4.3 策略网络Actor-Critic我们采用经典的Actor-Critic架构。策略网络Actor输出动作概率价值网络Critic评估当前状态的价值。class ActorCritic(Model): def __init__(self, num_actions, feature_dim256): super(ActorCritic, self).__init__() # 共享的特征提取层可以与ICM的特征编码器共享也可以独立 self.shared_conv FeatureEncoder(feature_dim) # 这里为了清晰独立定义一个实际可复用 # Actor 头 self.actor_dense layers.Dense(128, activationrelu) self.actor_output layers.Dense(num_actions, activationsoftmax) # Critic 头 self.critic_dense layers.Dense(128, activationrelu) self.critic_output layers.Dense(1, activationlinear) # 状态价值 V(s) def call(self, inputs): features self.shared_conv(inputs) # Actor 路径 a self.actor_dense(features) action_probs self.actor_output(a) # Critic 路径 c self.critic_dense(features) state_value self.critic_output(c) return action_probs, state_value注意事项特征共享的权衡。一种设计是让策略网络和ICM的特征编码器共享权重这样可以减少参数量迫使特征表示同时服务于策略优化和好奇心预测。但这也可能带来冲突策略可能学会操纵特征来“欺骗”正向模型产生虚假的好奇心。在初期实验中我建议先让它们独立稳定后再尝试共享并观察效果变化。5. 训练循环与核心算法实现这是整个项目的引擎。我们将实现一个包含ICM的PPO近端策略优化训练循环。PPO因其稳定性和易于调参而成为首选。5.1 数据收集与存储我们需要一个缓冲区来存储一个批次或一个回合的经验数据。class RolloutBuffer: def __init__(self): self.states [] self.actions [] self.rewards [] # 总奖励 外部 内在 self.ext_rewards [] # 仅外部奖励用于监控 self.int_rewards [] # 仅内在奖励用于监控 self.action_probs [] self.state_values [] self.dones [] self.next_states [] def clear(self): self.states.clear() self.actions.clear() self.rewards.clear() self.ext_rewards.clear() self.int_rewards.clear() self.action_probs.clear() self.state_values.clear() self.dones.clear() self.next_states.clear() def add(self, state, action, reward, ext_reward, int_reward, action_prob, state_value, done, next_state): self.states.append(state) self.actions.append(action) self.rewards.append(reward) self.ext_rewards.append(ext_reward) self.int_rewards.append(int_reward) self.action_probs.append(action_prob) self.state_values.append(state_value) self.dones.append(done) self.next_states.append(next_state) def get_batch(self): # 转换为numpy数组方便计算 return (np.array(self.states), np.array(self.actions), np.array(self.rewards), np.array(self.ext_rewards), np.array(self.int_rewards), np.array(self.action_probs), np.array(self.state_values), np.array(self.dones), np.array(self.next_states))5.2 核心训练步骤训练循环包含三个关键部分与环境交互收集数据、计算GAE和回报、更新策略网络和ICM网络。import tensorflow as tf class CuriosityDrivenAgent: def __init__(self, state_shape, num_actions, lr_actor3e-4, lr_critic1e-3, lr_icm1e-3, gamma0.99, gae_lambda0.95, clip_ratio0.2, feature_dim256): self.num_actions num_actions self.gamma gamma self.gae_lambda gae_lambda self.clip_ratio clip_ratio # 初始化网络 self.actor_critic ActorCritic(num_actions, feature_dim) self.feature_encoder FeatureEncoder(feature_dim) self.inverse_model InverseModel(num_actions) self.forward_model ForwardModel(feature_dim, num_actions) # 优化器 self.actor_optimizer tf.keras.optimizers.Adam(learning_ratelr_actor) self.critic_optimizer tf.keras.optimizers.Adam(learning_ratelr_critic) self.icm_optimizer tf.keras.optimizers.Adam(learning_ratelr_icm) # 内在奖励缩放系数 self.eta 0.01 def compute_intrinsic_reward(self, state, action, next_state): 计算单步内在奖励 # 将状态转换为特征 state_feat self.feature_encoder(state) next_state_feat self.feature_encoder(next_state) # 动作需要是one-hot形式 action_onehot tf.one_hot(action, depthself.num_actions) # 正向模型预测 pred_next_feat self.forward_model(state_feat, action_onehot) # 计算预测误差作为内在奖励 intrinsic_reward self.eta * tf.reduce_mean(tf.square(next_state_feat - pred_next_feat), axis-1) return intrinsic_reward.numpy() def update_icm(self, states, actions, next_states): 更新好奇心模块ICM with tf.GradientTape() as tape: # 编码特征 state_feat self.feature_encoder(states) next_state_feat self.feature_encoder(next_states) action_onehot tf.one_hot(actions, depthself.num_actions) # 1. 逆向模型损失预测动作的交叉熵 pred_action_logits self.inverse_model(state_feat, next_state_feat) inverse_loss tf.reduce_mean( tf.keras.losses.sparse_categorical_crossentropy(actions, pred_action_logits, from_logitsTrue) ) # 2. 正向模型损失预测下一状态的MSE pred_next_feat self.forward_model(state_feat, action_onehot) forward_loss tf.reduce_mean(tf.square(next_state_feat - pred_next_feat)) # ICM总损失是两者加权和通常权重为1:1 icm_loss inverse_loss forward_loss # 计算梯度并更新 grads tape.gradient(icm_loss, self.feature_encoder.trainable_variables self.inverse_model.trainable_variables self.forward_model.trainable_variables) self.icm_optimizer.apply_gradients(zip(grads, self.feature_encoder.trainable_variables self.inverse_model.trainable_variables self.forward_model.trainable_variables)) return icm_loss.numpy(), inverse_loss.numpy(), forward_loss.numpy() def update_actor_critic(self, buffer): 使用PPO算法更新策略网络和价值网络 states, actions, rewards, old_action_probs, old_values, dones, next_states buffer.get_batch()[:7] # 取前7个 # 计算GAE和回报 advantages, returns self._compute_gae_and_returns(rewards, old_values, dones) # 将数据转换为TensorFlow张量 states tf.convert_to_tensor(states, dtypetf.float32) actions tf.convert_to_tensor(actions, dtypetf.int32) old_action_probs tf.convert_to_tensor(old_action_probs, dtypetf.float32) advantages tf.convert_to_tensor(advantages, dtypetf.float32) returns tf.convert_to_tensor(returns, dtypetf.float32) # 多次PPO更新迭代例如10次 for _ in range(10): with tf.GradientTape(persistentTrue) as tape: # 前向传播获取新的动作概率和状态价值 new_action_probs, new_values self.actor_critic(states) # 计算动作分布分类分布 dist tfp.distributions.Categorical(probsnew_action_probs) # 新策略下该动作的对数概率 new_log_probs dist.log_prob(actions) # 旧策略下该动作的对数概率从buffer来 old_dist tfp.distributions.Categorical(probsold_action_probs) old_log_probs old_dist.log_prob(actions) # 计算概率比 ratio tf.exp(new_log_probs - old_log_probs) # PPO-Clip 目标函数Actor损失 surr1 ratio * advantages surr2 tf.clip_by_value(ratio, 1.0 - self.clip_ratio, 1.0 self.clip_ratio) * advantages actor_loss -tf.reduce_mean(tf.minimum(surr1, surr2)) # Critic损失价值网络均方误差 critic_loss tf.reduce_mean(tf.square(returns - tf.squeeze(new_values))) # 可选加入熵奖励鼓励探索 entropy tf.reduce_mean(dist.entropy()) entropy_bonus -0.01 * entropy # 系数为负因为我们要最大化熵即损失减小 total_actor_loss actor_loss entropy_bonus # 分别计算Actor和Critic的梯度并更新 actor_grads tape.gradient(total_actor_loss, self.actor_critic.actor_trainable_variables) critic_grads tape.gradient(critic_loss, self.actor_critic.critic_trainable_variables) self.actor_optimizer.apply_gradients(zip(actor_grads, self.actor_critic.actor_trainable_variables)) self.critic_optimizer.apply_gradients(zip(critic_grads, self.actor_critic.critic_trainable_variables)) del tape # 释放持久化tape return actor_loss.numpy(), critic_loss.numpy() def _compute_gae_and_returns(self, rewards, values, dones): 计算广义优势估计GAE和回报 batch_size len(rewards) advantages np.zeros(batch_size, dtypenp.float32) returns np.zeros(batch_size, dtypenp.float32) last_gae 0 # 从后向前计算 for t in reversed(range(batch_size)): if t batch_size - 1: next_value 0 # 如果是回合结束下一个状态价值为0 next_non_terminal 1.0 - dones[t] else: next_value values[t1] next_non_terminal 1.0 - dones[t] delta rewards[t] self.gamma * next_value * next_non_terminal - values[t] advantages[t] last_gae delta self.gamma * self.gae_lambda * next_non_terminal * last_gae returns advantages values return advantages, returns5.3 主训练循环将以上所有部分串联起来形成完整的主训练循环。def train_agent(env, agent, total_episodes5000, max_steps_per_episode1000, update_frequency2048): preprocessor Preprocessor() buffer RolloutBuffer() episode_rewards [] ext_rewards_history [] int_rewards_history [] for episode in range(total_episodes): state env.reset() state preprocessor.process(state) episode_reward 0 episode_ext_reward 0 episode_int_reward 0 done False step 0 while not done and step max_steps_per_episode: # 1. 选择动作 state_tensor tf.expand_dims(tf.convert_to_tensor(state, dtypetf.float32), 0) action_probs, state_value agent.actor_critic(state_tensor) action_dist tfp.distributions.Categorical(probsaction_probs) action action_dist.sample().numpy()[0] log_prob action_dist.log_prob(action).numpy()[0] state_value state_value.numpy()[0][0] # 2. 执行动作 reward_ext env.make_action(agent.actions[action]) # vizdoom API next_state env.get_state().screen_buffer next_state_processed preprocessor.process(next_state) done env.is_episode_finished() # 3. 计算内在奖励 # 注意这里为了效率可以批量计算但单步演示更清晰 state_batch tf.expand_dims(tf.convert_to_tensor(state, dtypetf.float32), 0) next_state_batch tf.expand_dims(tf.convert_to_tensor(next_state_processed, dtypetf.float32), 0) reward_int agent.compute_intrinsic_reward(state_batch, action, next_state_batch)[0] # 4. 总奖励 total_reward reward_ext reward_int # 5. 存储经验 buffer.add(state, action, total_reward, reward_ext, reward_int, log_prob, state_value, done, next_state_processed) # 更新状态和统计 state next_state_processed episode_reward total_reward episode_ext_reward reward_ext episode_int_reward reward_int step 1 # 6. 如果缓冲区满了进行更新 if len(buffer.states) update_frequency: # 更新ICM icm_loss, inv_loss, fwd_loss agent.update_icm( tf.convert_to_tensor(buffer.states, dtypetf.float32), tf.convert_to_tensor(buffer.actions, dtypetf.int32), tf.convert_to_tensor(buffer.next_states, dtypetf.float32) ) # 更新Actor-Critic actor_loss, critic_loss agent.update_actor_critic(buffer) print(f[Update] ICM Loss: {icm_loss:.4f}, Actor Loss: {actor_loss:.4f}, Critic Loss: {critic_loss:.4f}) buffer.clear() # 清空缓冲区 # 回合结束记录 episode_rewards.append(episode_reward) ext_rewards_history.append(episode_ext_reward) int_rewards_history.append(episode_int_reward) print(fEpisode {episode1}: Total Reward{episode_reward:.2f}, Ext{episode_ext_reward:.2f}, Int{episode_int_reward:.2f}, Steps{step}) # 可选定期保存模型 if (episode 1) % 100 0: agent.actor_critic.save_weights(f./models/actor_critic_ep{episode1}.h5) agent.feature_encoder.save_weights(f./models/encoder_ep{episode1}.h5) return episode_rewards, ext_rewards_history, int_rewards_history6. 实验调试与性能分析理论很美好但实际训练中你会遇到各种问题。以下是基于我多次实验总结的关键调试点和分析维度。6.1 超参数调优寻找平衡点超参数对好奇心驱动的智能体性能影响巨大一个不合适的参数可能导致智能体原地打转或行为怪异。超参数推荐范围/值作用与影响调试建议内在奖励系数η0.001-0.1控制内在奖励的强度。这是最关键的参数。太大智能体沉迷于探索无关细节太小内在动机不足无法克服稀疏奖励。从0.01开始。观察训练曲线如果外部奖励几乎不增长而内在奖励很高可能η太大如果智能体很快停滞尝试增大η。特征维度feature_dim128-512特征向量的长度。维度太低信息丢失太高增加计算量且可能过拟合。256是一个不错的起点。可以尝试对比128和512下的探索效率。折扣因子γ0.99(常用)未来奖励的衰减率。影响智能体是“短视”还是“远见”。在稀疏奖励任务中保持较高的γ如0.99很重要让智能体能为长远回报等待。GAE参数λ0.90-0.98权衡偏差与方差的折中系数。0.95是PPO中的经典值通常不需要大调。PPO裁剪系数clip_ratio0.1-0.3限制策略更新的幅度保证稳定性。0.2是安全选择。如果学习不稳定奖励剧烈震荡可以尝试减小到0.1。学习率lr1e-4-3e-4控制参数更新步长。Actor和Critic可以不同。Actor的学习率通常比Critic小如3e-4vs1e-3。ICM的学习率可以设得稍高如1e-3让它快速学习环境动力学。熵系数ent_coef0.01鼓励探索的额外奖励系数。在好奇心驱动下这个系数可以设小甚至为0。初期可以设为0.01帮助探索后期可以衰减或设为0让好奇心主导。实操心得日志与可视化是关键。不要只盯着总奖励。一定要分开记录并绘制外部奖励和内在奖励的曲线。理想的趋势是训练初期内在奖励很高因为到处都新奇外部奖励为0随着探索智能体逐渐找到有外部奖励的区域外部奖励开始上升同时内在奖励在已探索区域下降但在新区域仍保持较高水平。如果内在奖励曲线一直居高不下且外部奖励不涨说明智能体在“瞎逛”。6.2 常见问题与排查技巧在实际运行中你几乎一定会遇到下面这些问题。问题1智能体原地转圈或重复无效动作。可能原因内在奖励系数η过高导致任何微小的动作变化都能产生可观的预测误差比如只是转动视角画面变化很大智能体发现了这个“刷分”漏洞。排查观察动作分布。如果智能体90%的时间都在执行同一个动作比如连续左转基本就是这个问题。解决大幅降低η比如从0.1降到0.001。同时检查特征编码器的输出是否稳定。可以冻结特征编码器训练几步看看行为是否改变。问题2训练初期奖励上升后期崩溃或震荡。可能原因PPO的更新步长太大或者批次数据相关性太强导致策略更新过于激进。排查观察策略损失actor_loss和价值损失critic_loss曲线。如果出现尖峰说明更新不稳定。解决减小PPO的clip_ratio如从0.2到0.1降低Actor学习率。增加PPO的更新迭代次数update_epochs让每次数据被更充分地利用。也可以尝试在优化器中使用梯度裁剪tf.clip_by_global_norm。问题3内在奖励很快降为零智能体停止探索。可能原因特征编码器过强或正向模型过弱导致环境被“完美预测”失去了好奇心。也可能是η太小。排查查看ICM的Forward Loss。如果它迅速下降到接近0并保持说明模型过早地“学会”了环境。解决可以尝试给特征编码器或正向模型添加Dropout层引入随机性防止过拟合。或者使用更复杂的、具有随机性的环境。问题4训练速度极慢。可能原因环境交互特别是VizDoom渲染是主要瓶颈。或者网络太大。排查使用代码分析工具如cProfile或简单计时找出耗时最长的部分。解决环境层面确保game.set_window_visible(False)。将屏幕分辨率设到最低如RES_160X120。使用ASYNC模式如果VizDoom支持进行多环境并行采样。网络层面减小CNN的通道数或特征维度。使用更小的批处理大小。算法层面考虑使用分布式框架如Ray RLlib或者采用同步多环境采样。6.3 评估与可视化训练完成后我们需要评估智能体的真实表现并可视化其探索过程。def evaluate_agent(env, agent, model_weights_path, num_episodes10, renderTrue): 加载模型并评估性能 # 加载权重 agent.actor_critic.load_weights(model_weights_path) preprocessor Preprocessor() if render: env.set_window_visible(True) # 评估时打开画面 total_ext_rewards [] for ep in range(num_episodes): state env.reset() state preprocessor.process(state) done False ep_ext_reward 0 frames [] # 用于录制视频 while not done: if render: frames.append(env.get_state().screen_buffer.copy()) state_tensor tf.expand_dims(tf.convert_to_tensor(state, dtypetf.float32), 0) action_probs, _ agent.actor_critic(state_tensor) action tf.argmax(action_probs, axis-1).numpy()[0] # 评估时选择最优动作 reward env.make_action(agent.actions[action]) next_state env.get_state().screen_buffer state preprocessor.process(next_state) done env.is_episode_finished() ep_ext_reward reward total_ext_rewards.append(ep_ext_reward) print(fEval Episode {ep1}: External Reward {ep_ext_reward}) # 可选保存第一个回合的视频 if ep 0 and len(frames) 0: save_video(frames, fevaluation_episode_{ep1}.mp4) avg_reward np.mean(total_ext_rewards) print(fAverage External Reward over {num_episodes} episodes: {avg_reward:.2f}) return avg_reward可视化内在关注点一个有趣的技巧是可视化智能体的“好奇心热点图”。我们可以计算每个位置或状态的内在奖励值并映射回原始图像上。虽然特征空间是抽象的但可以通过对正向模型的梯度进行可视化如Grad-CAM的简化版来近似查看哪些图像区域导致了高的预测误差从而吸引了智能体的注意。7. 进阶探索与优化方向当你成功运行基础版本后可以尝试以下方向进行优化和深化理解。7.1 好奇心机制的变体ICM只是内在动机的一种形式。还有其他强大的方法随机网络蒸馏RND使用一个随机初始化的固定“目标网络”和一个可训练的“预测网络”。预测网络的目标是匹配目标网络对状态的输出。状态越新颖预测网络越难匹配内在奖励越高。RND对视觉干扰更鲁棒在许多硬探索任务上表现优于ICM。基于计数的探索为每个访问过的状态或其特征维护一个计数访问越频繁的状态奖励越低。这需要处理连续状态空间通常通过密度模型或哈希函数实现。基于信息增益的探索智能体寻求能最大程度减少环境模型不确定性的状态。这通常需要贝叶斯神经网络来估计不确定性计算成本较高。7.2 与课程学习结合对于特别困难的任务可以设计一个由易到难的课程。例如在Doom中先在一个小房间、有密集奖励如每走一步给微小奖励的环境训练。然后转移到稀疏奖励但结构简单的迷宫。最后再放到目标大型、复杂的“MyWayHome”场景中。 智能体在早期阶段建立的基本导航技能能帮助它在后期更有效地利用好奇心进行探索。7.3 分布式训练加速单机训练一个复杂的3D环境智能体非常耗时。可以采用Ape-X或IMPALA等分布式架构。思路是一个中央Learner持有全局网络参数。多个Actor进程跑在多个CPU核心或机器上分别与环境交互收集经验并将数据放入一个共享的回放缓冲区。Learner持续从缓冲区采样数据并更新网络定期将新参数同步给各个Actor。 使用ray库可以相对容易地实现这种架构能将训练时间从数周缩短到数天甚至数小时。7.4 迁移学习与泛化训练好的好奇心驱动智能体其“好奇脑”特征编码器学习到的是关于环境动态的通用表示。你可以尝试跨场景迁移将在“MyWayHome”场景中学到的模型直接应用到另一个不同的Doom场景如“Defend the Center”中只微调策略网络观察其探索能力是否能够迁移。作为其他任务的预训练将ICM模块作为特征提取器固定在其之上训练一个需要密集奖励的特定任务如射击可以加速下游任务的学习因为特征已经包含了丰富的环境信息。好奇心驱动强化学习打开了一扇门让智能体在奖励信号稀缺甚至缺失的世界里依然能保持主动学习和探索的欲望。从在Doom迷宫中寻找出口到在《我的世界》里学习合成物品其核心思想一脉相承。实现它的过程就像在教一个AI如何像孩子一样为了“弄明白”而去触碰、观察和实验。这个过程里调试参数的挫败感和看到智能体突然灵光一现找到出路的兴奋感是纯监督学习难以比拟的。我个人的体会是开始训练后不要离开电脑太久因为你永远不知道下一个回合你的智能体会给你带来怎样的惊喜——或是令人哭笑不得的“鬼畜”行为。多观察多分析损失曲线和奖励成分耐心调整你最终会收获一个真正拥有“探索欲”的智能体伙伴。