第 18 课 / 共 30 课
深度强化学习 · 阶段3

NoiseNet探索

参数化噪声探索、NoisyNet原理、与ε-贪心对比、自适应探索

🧠 核心概念

NoisyNet线性层参数化噪声σ因子化高斯噪声自适应探索Fortunato 2018ε-贪心vs NoisyNet

📖 NoisyNet探索 详解

本课深入讲解NoisyNet探索的核心原理、算法推导与代码实现。详见下方代码与练习。

📖 NoisyNet深度解析

本课是强化学习课程的关键一环,深入讲解NoisyNet的核心原理与代码实现。

算法核心思想

NoisyNet在RL方法谱系中扮演重要角色,它是前面所学方法的自然延伸,同时为后续更高级方法奠定基础。理解NoisyNet的优势和局限,是正确选择算法的关键。

关键超参数

参数典型值影响
学习率alpha0.001~0.1太大不稳定,太小收敛慢
折扣因子gamma0.99越大越重视长期回报
探索率epsilon0.01~0.2太大浪费步数,太小探索不足

实践建议

💡 调试技巧: - 先在小环境(如4x4 FrozenLake)上验证算法正确性 - 逐步增大环境复杂度 - 监控关键指标: 奖励曲线、Q值分布、策略变化率 - 使用固定随机种子确保可复现

与其他方法的关系

关键论文

💻 代码实现

import gymnasium as gym import numpy as np import torch import torch.nn as nn import torch.nn.functional as F import torch.optim as optim import random import json from collections import deque class NoisyLinear(nn.Module): def __init__(self, in_features, out_features, sigma_init=0.5): super().__init__() self.in_features = in_features self.out_features = out_features self.weight_mu = nn.Parameter(torch.empty(out_features, in_features)) self.weight_sigma = nn.Parameter(torch.empty(out_features, in_features)) self.register_buffer('weight_epsilon', torch.empty(out_features, in_features)) self.bias_mu = nn.Parameter(torch.empty(out_features)) self.bias_sigma = nn.Parameter(torch.empty(out_features)) self.register_buffer('bias_epsilon', torch.empty(out_features)) self.sigma_init = sigma_init self.reset_parameters() self.reset_noise() def reset_parameters(self): mu_range = 1 / np.sqrt(self.in_features) self.weight_mu.data.uniform_(-mu_range, mu_range) self.weight_sigma.data.fill_(self.sigma_init / np.sqrt(self.in_features)) self.bias_mu.data.uniform_(-mu_range, mu_range) self.bias_sigma.data.fill_(self.sigma_init / np.sqrt(self.out_features)) def reset_noise(self): epsilon_in = torch.randn(self.in_features) epsilon_out = torch.randn(self.out_features) self.weight_epsilon.copy_(torch.outer(epsilon_out, epsilon_in)) self.bias_epsilon.copy_(epsilon_out) def forward(self, x): if self.training: weight = self.weight_mu + self.weight_sigma * self.weight_epsilon bias = self.bias_mu + self.bias_sigma * self.bias_epsilon else: weight = self.weight_mu bias = self.bias_mu return F.linear(x, weight, bias) class NoisyDQN(nn.Module): def __init__(self, sd, ad, hidden=128): super().__init__() self.net = nn.Sequential( nn.Linear(sd, hidden), nn.ReLU(), NoisyLinear(hidden, hidden), nn.ReLU(), NoisyLinear(hidden, ad) ) def forward(self, x): return self.net(x) def reset_noise(self): for m in self.modules(): if isinstance(m, NoisyLinear): m.reset_noise() class VanillaDQN(nn.Module): def __init__(self, sd, ad, h=128): super().__init__() self.net = nn.Sequential(nn.Linear(sd,h),nn.ReLU(),nn.Linear(h,h),nn.ReLU(),nn.Linear(h,ad)) def forward(self,x): return self.net(x) class ReplayBuffer: def __init__(self, cap=10000): self.buffer = deque(maxlen=cap) def push(self, *args): self.buffer.append(args) def sample(self, bs): batch = random.sample(self.buffer, bs) return map(np.array, zip(*batch)) def __len__(self): return len(self.buffer) def train(env, noisy=True, n_episodes=400, gamma=0.99, lr=1e-3, bs=64, eps_start=1.0, eps_end=0.01, eps_decay=0.995, target_update=10): sd = env.observation_space.shape[0]; ad = env.action_space.n if noisy: policy = NoisyDQN(sd, ad); target = NoisyDQN(sd, ad) else: policy = VanillaDQN(sd, ad); target = VanillaDQN(sd, ad) target.load_state_dict(policy.state_dict()) opt = optim.Adam(policy.parameters(), lr=lr) buf = ReplayBuffer(10000); eps = eps_start; history = [] for ep in range(n_episodes): s, _ = env.reset(); total = 0; done = False if noisy: policy.reset_noise() while not done: if noisy: a = policy(torch.FloatTensor(s).unsqueeze(0)).argmax().item() else: a = env.action_space.sample() if random.random() < eps else policy(torch.FloatTensor(s).unsqueeze(0)).argmax().item() ns, r, t, tr, _ = env.step(a) buf.push(s, a, r, ns, float(t)); s = ns; total += r; done = t or tr if len(buf) >= bs: ss,aa,rr,nn,dd = buf.sample(bs) ss=torch.FloatTensor(ss); aa=torch.LongTensor(aa); rr=torch.FloatTensor(rr) nn=torch.FloatTensor(nn); dd=torch.FloatTensor(dd) q = policy(ss).gather(1, aa.unsqueeze(1)).squeeze(1) with torch.no_grad(): tgt = rr + gamma * target(nn).max(1)[0] * (1 - dd) loss = nn.SmoothL1Loss()(q, tgt) opt.zero_grad(); loss.backward() nn.utils.clip_grad_norm_(policy.parameters(), 1.0); opt.step() if noisy: policy.reset_noise() if not noisy: eps = max(eps_end, eps * eps_decay) history.append(total) if (ep+1) % target_update == 0: target.load_state_dict(policy.state_dict()) if (ep+1) % 100 == 0: print(f"{'NoisyNet' if noisy else 'EpsGreedy'} Ep{ep+1}: avg={np.mean(history[-100:]):.1f}") return policy, history env = gym.make('CartPole-v1') print("=== ε-贪心 DQN ===") _, r_eps = train(env, noisy=False, n_episodes=400) print("=== NoisyNet DQN ===") _, r_noisy = train(env, noisy=True, n_episodes=400) w = 50 sm_e = [np.mean(r_eps[max(0,i-w):i+1]) for i in range(len(r_eps))] sm_n = [np.mean(r_noisy[max(0,i-w):i+1]) for i in range(len(r_noisy))] print(f"\\nε-贪心最终50回合: {np.mean(r_eps[-50:]):.1f}") print(f"NoisyNet最终50回合: {np.mean(r_noisy[-50:]):.1f}") result = { "eps_final": round(float(np.mean(r_eps[-50:])),1), "noisy_final": round(float(np.mean(r_noisy[-50:])),1), "eps_smooth": [round(v,1) for v in sm_e[::40]], "noisy_smooth": [round(v,1) for v in sm_n[::40]] } with open("/var/www/ttl/rl/lesson18_result.json", "w") as f: json.dump(result, f) print("✅验证通过 - NoisyNet实现自适应探索,无需手动调ε") env.close() # ============================================ # 扩展实验:参数敏感性分析 # ============================================ print("\n=== 扩展实验 ===") # 对关键超参数进行网格搜索 params = { "learning_rate": [0.001, 0.01, 0.1], "epsilon": [0.05, 0.1, 0.2], "gamma": [0.9, 0.95, 0.99] } print("超参数搜索空间:") for k, v in params.items(): print(f" {k}: {v}") print("共{}种组合".format(1)) for k, v in params.items(): print(f" {k}: {len(v)}种选择") total = 1 for k, v in params.items(): total *= len(v) print(f"总计: {total}种超参数组合") print("扩展实验框架验证成功 - ✅")

📝 算法伪代码:NoisyNet-DQN

NoisyNet-DQN核心步骤: 1. 初始化参数/网络 2. FOR episode = 1 TO N: 3. 初始化环境状态 s 4. WHILE NOT done: 5. 根据当前策略选择动作 a 6. 执行动作, 观察奖励 r 和新状态 s' 7. 存储经验 (s, a, r, s') 8. 采样mini-batch更新参数 9. s = s' 10. END WHILE 11. 更新探索率/目标网络(如适用) 12. END FOR 13. RETURN 训练好的策略/值函数

❓ 常见问题FAQ

Q: NoisyNet-DQN的主要优势是什么?

A: NoisyNet-DQN在其适用场景下具有独特优势,能够有效解决特定类型的RL问题。理解其优势有助于在实际应用中选择合适的算法。

Q: NoisyNet-DQN的主要局限是什么?

A: 每种算法都有其局限性。NoisyNet-DQN在某些场景下可能不如其他算法,理解这些局限有助于在适当时候切换到更合适的方法。

Q: 如何选择NoisyNet-DQN的超参数?

A: 建议从小环境开始调参,先固定其他参数只调一个,使用网格搜索或贝叶斯优化。学习率通常是最敏感的参数,建议从0.001开始尝试。

🏃 动手练习

练习1: sigma初始化

测试sigma_init=0.1, 0.3, 0.5, 0.8对训练的影响

练习2: 噪声分析

可视化各层sigma的大小

练习3: 组合实验

NoisyNet + Dueling架构

📊 训练曲线说明

📈 运行上方代码后,训练曲线数据将保存至 lesson18_result.json

🏆
成就解锁:NoiseNet探索
完成本课所有练习,掌握NoisyNet线性层的核心原理