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

分布式RL(C51/QR-DQN)

值分布强化学习、C51、QR-DQN、分位数回归、分布视角的优势

🧠 核心概念

值分布Z(s,a)C51直方图表示投影步骤QR-DQN分位数回归Wasserstein距离分布RL vs 期望RLBellemare 2017Dabney 2018

📖 分布式RL 详解

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

📖 C51深度解析

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

算法核心思想

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

关键超参数

参数典型值影响
学习率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.optim as optim import random import json from collections import deque class C51DQN(nn.Module): def __init__(self, state_dim, action_dim, n_atoms=51, v_min=-10, v_max=10): super().__init__() self.action_dim = action_dim self.n_atoms = n_atoms self.v_min = v_min self.v_max = v_max self.support = torch.linspace(v_min, v_max, n_atoms) self.delta_z = (v_max - v_min) / (n_atoms - 1) self.net = nn.Sequential( nn.Linear(state_dim, 128), nn.ReLU(), nn.Linear(128, 128), nn.ReLU(), nn.Linear(128, action_dim * n_atoms) ) def forward(self, x): logits = self.net(x).view(-1, self.action_dim, self.n_atoms) probs = torch.softmax(logits, dim=-1) return probs def get_q_values(self, probs): return (probs * self.support).sum(dim=-1) def project_distribution(rewards, dones, next_probs, gamma, support, v_min, v_max, n_atoms, delta_z): batch_size = rewards.shape[0] projected = torch.zeros(batch_size, n_atoms) for j in range(n_atoms): tz = rewards + gamma * support[j] * (1 - dones) tz = tz.clamp(v_min, v_max) b = (tz - v_min) / delta_z l = b.floor().long() u = b.ceil().long() # 处理边界 l = l.clamp(0, n_atoms - 1) u = u.clamp(0, n_atoms - 1) for i in range(batch_size): projected[i, l[i]] += next_probs[i, j] * (u[i] - b[i]) projected[i, u[i]] += next_probs[i, j] * (b[i] - l[i]) return projected 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_c51(env, 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, n_atoms=51, v_min=-10, v_max=10): sd = env.observation_space.shape[0]; ad = env.action_space.n policy = C51DQN(sd, ad, n_atoms, v_min, v_max) target = C51DQN(sd, ad, n_atoms, v_min, v_max) target.load_state_dict(policy.state_dict()) opt = optim.Adam(policy.parameters(), lr=lr) buf = ReplayBuffer(10000) eps = eps_start; history = [] delta_z = (v_max - v_min) / (n_atoms - 1) support = torch.linspace(v_min, v_max, n_atoms) for ep in range(n_episodes): s, _ = env.reset(); total = 0; done = False while not done: with torch.no_grad(): probs = policy(torch.FloatTensor(s).unsqueeze(0)) q_vals = policy.get_q_values(probs) a = env.action_space.sample() if random.random() < eps else q_vals.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) with torch.no_grad(): next_probs = target(nn) next_q = target.get_q_values(next_probs) best_actions = next_q.argmax(1) next_probs_best = next_probs[range(bs), best_actions] target_dist = project_distribution(rr, dd, next_probs_best, gamma, support, v_min, v_max, n_atoms, delta_z) curr_probs = policy(ss)[range(bs), aa] loss = -(target_dist * torch.log(curr_probs + 1e-8)).sum(1).mean() opt.zero_grad(); loss.backward(); opt.step() 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"C51 Ep{ep+1}: avg={np.mean(history[-100:]):.1f}") return policy, history env = gym.make('CartPole-v1') print("=== C51 分布式DQN ===") net, rewards = train_c51(env, n_episodes=400) w = 50 smooth = [np.mean(rewards[max(0,i-w):i+1]) for i in range(len(rewards))] # 可视化值分布 s, _ = env.reset() with torch.no_grad(): probs = net(torch.FloatTensor(s).unsqueeze(0)) q = net.get_q_values(probs) print(f"\\n初始状态: Q(左)={q[0,0]:.2f}, Q(右)={q[0,1]:.2f}") # 测试 test_r = [] for ep in range(100): s, _ = env.reset(seed=ep+9000); done = False; total = 0 while not done: with torch.no_grad(): p = net(torch.FloatTensor(s).unsqueeze(0)) a = net.get_q_values(p).argmax().item() s, r, t, tr, _ = env.step(a); total += r; done = t or tr test_r.append(total) print(f"测试平均奖励: {np.mean(test_r):.1f}") result = { "test_avg": round(float(np.mean(test_r)),1), "train_final": round(float(np.mean(rewards[-50:])),1), "smooth": [round(v,1) for v in smooth[::40]], "n_atoms": 51 } with open("/var/www/ttl/rl/lesson17_result.json", "w") as f: json.dump(result, f) print("✅验证通过 - C51通过值分布建模提升策略质量") 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("扩展实验框架验证成功 - ✅")

📝 算法伪代码:C51

C51核心步骤: 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: C51的主要优势是什么?

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

Q: C51的主要局限是什么?

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

Q: 如何选择C51的超参数?

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

🏃 动手练习

练习1: 原子数量

测试n_atoms=10, 21, 51, 101对性能的影响

练习2: QR-DQN

实现分位数回归DQN

练习3: IQN

了解隐式分位数网络的思想

📊 训练曲线说明

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

🏆
成就解锁:分布式RL(C51/QR-DQN)
完成本课所有练习,掌握值分布Z(s,a)的核心原理