第 24 课 / 共 30 课
策略梯度 · 阶段4

TD3

Twin Delayed DDPG、延迟策略更新、目标策略平滑、连续控制SOTA

🧠 核心概念

TD3三大创新裁剪双Q学习延迟策略更新目标策略平滑Fujimoto 2018DDPG改进

📖 TD3 详解

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

📖 TD3深度解析

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

算法核心思想

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

关键超参数

参数典型值影响
学习率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 json import random from collections import deque class Actor(nn.Module): def __init__(self, sd, ad, h=128, max_action=2.0): super().__init__() self.net = nn.Sequential(nn.Linear(sd,h),nn.ReLU(),nn.Linear(h,h),nn.ReLU(),nn.Linear(h,ad),nn.Tanh()) self.max_action = max_action def forward(self, x): return self.net(x) * self.max_action class Critic(nn.Module): def __init__(self, sd, ad, h=128): super().__init__() self.net = nn.Sequential(nn.Linear(sd+ad,h),nn.ReLU(),nn.Linear(h,h),nn.ReLU(),nn.Linear(h,1)) def forward(self, s, a): return self.net(torch.cat([s,a],dim=-1)) class DoubleCritic(nn.Module): def __init__(self, sd, ad, h=128): super().__init__() self.q1 = Critic(sd, ad, h); self.q2 = Critic(sd, ad, h) def forward(self, s, a): return self.q1(s,a), self.q2(s,a) class ReplayBuffer: def __init__(self, cap=50000): 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_td3(env, n_episodes=300, gamma=0.99, tau=0.005, lr=3e-4, bs=64, policy_delay=2, noise_clip=0.5, policy_noise=0.2, max_action=2.0, expl_noise=0.1): sd = env.observation_space.shape[0]; ad = env.action_space.shape[0] actor = Actor(sd, ad, max_action=max_action) actor_target = Actor(sd, ad, max_action=max_action) actor_target.load_state_dict(actor.state_dict()) critic = DoubleCritic(sd, ad) critic_target = DoubleCritic(sd, ad) critic_target.load_state_dict(critic.state_dict()) opt_a = optim.Adam(actor.parameters(), lr=lr) opt_c = optim.Adam(critic.parameters(), lr=lr) buf = ReplayBuffer(50000) history = []; total_steps = 0 for ep in range(n_episodes): s, _ = env.reset(); done = False; total = 0 while not done: s_t = torch.FloatTensor(s).unsqueeze(0) with torch.no_grad(): a = actor(s_t).numpy()[0] a = a + np.random.normal(0, max_action * expl_noise, size=ad) a = a.clip(-max_action, max_action) ns, r, t, tr, _ = env.step(a) buf.push(s, a, r/10, ns, float(t or tr)) s = ns; total += r; done = t or tr; total_steps += 1 if len(buf) >= bs: ss,aa,rr,nn,dd = buf.sample(bs) ss=torch.FloatTensor(ss); aa=torch.FloatTensor(aa); rr=torch.FloatTensor(rr).unsqueeze(1) nn=torch.FloatTensor(nn); dd=torch.FloatTensor(dd).unsqueeze(1) with torch.no_grad(): noise = (torch.randn_like(aa) * policy_noise).clamp(-noise_clip, noise_clip) smoothed = (actor_target(nn) + noise).clamp(-max_action, max_action) q1_t, q2_t = critic_target(nn, smoothed) target = rr + gamma * (1-dd) * torch.min(q1_t, q2_t) q1, q2 = critic(ss, aa) c_loss = nn.MSELoss()(q1, target) + nn.MSELoss()(q2, target) opt_c.zero_grad(); c_loss.backward(); opt_c.step() if total_steps % policy_delay == 0: a_loss = -critic.q1(ss, actor(ss)).mean() opt_a.zero_grad(); a_loss.backward(); opt_a.step() for p,tp in zip(actor.parameters(), actor_target.parameters()): tp.data.copy_(tau*p.data + (1-tau)*tp.data) for p,tp in zip(critic.parameters(), critic_target.parameters()): tp.data.copy_(tau*p.data + (1-tau)*tp.data) history.append(total) if (ep+1) % 50 == 0: print(f"TD3 Ep{ep+1}: avg={np.mean(history[-50:]):.1f}") return actor, history def train_ddpg(env, n_episodes=300, gamma=0.99, tau=0.005, lr=3e-4, bs=64, max_action=2.0, expl_noise=0.1): sd = env.observation_space.shape[0]; ad = env.action_space.shape[0] actor = Actor(sd, ad, max_action) actor_t = Actor(sd, ad, max_action); actor_t.load_state_dict(actor.state_dict()) critic = Critic(sd, ad) critic_t = Critic(sd, ad); critic_t.load_state_dict(critic.state_dict()) opt_a = optim.Adam(actor.parameters(), lr=lr) opt_c = optim.Adam(critic.parameters(), lr=lr) buf = ReplayBuffer(50000); history = [] for ep in range(n_episodes): s, _ = env.reset(); done = False; total = 0 while not done: s_t = torch.FloatTensor(s).unsqueeze(0) with torch.no_grad(): a = actor(s_t).numpy()[0] a = a + np.random.normal(0, max_action*expl_noise, size=ad) a = a.clip(-max_action, max_action) ns, r, t, tr, _ = env.step(a) buf.push(s, a, r/10, ns, float(t or tr)) 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.FloatTensor(aa); rr=torch.FloatTensor(rr).unsqueeze(1) nn=torch.FloatTensor(nn); dd=torch.FloatTensor(dd).unsqueeze(1) with torch.no_grad(): target = rr + gamma*(1-dd)*critic_t(nn, actor_t(nn)) c_loss = nn.MSELoss()(critic(ss,aa), target) opt_c.zero_grad(); c_loss.backward(); opt_c.step() a_loss = -critic(ss, actor(ss)).mean() opt_a.zero_grad(); a_loss.backward(); opt_a.step() for p,tp in zip(actor.parameters(),actor_t.parameters()): tp.data.copy_(tau*p.data+(1-tau)*tp.data) for p,tp in zip(critic.parameters(),critic_t.parameters()): tp.data.copy_(tau*p.data+(1-tau)*tp.data) history.append(total) if (ep+1) % 50 == 0: print(f"DDPG Ep{ep+1}: avg={np.mean(history[-50:]):.1f}") return actor, history env = gym.make('Pendulum-v1') print("=== DDPG ===") _, r_ddpg = train_ddpg(env, n_episodes=200) print("=== TD3 ===") _, r_td3 = train_td3(env, n_episodes=200) w = 20 sm_d = [np.mean(r_ddpg[max(0,i-w):i+1]) for i in range(len(r_ddpg))] sm_t = [np.mean(r_td3[max(0,i-w):i+1]) for i in range(len(r_td3))] print(f"\\nDDPG最终50回合: {np.mean(r_ddpg[-50:]):.1f}") print(f"TD3最终50回合: {np.mean(r_td3[-50:]):.1f}") result = { "ddpg_final": round(float(np.mean(r_ddpg[-50:])),1), "td3_final": round(float(np.mean(r_td3[-50:])),1), "ddpg_smooth": [round(v,1) for v in sm_d[::20]], "td3_smooth": [round(v,1) for v in sm_t[::20]] } with open("/var/www/ttl/rl/lesson24_result.json", "w") as f: json.dump(result, f) print("✅验证通过 - TD3三大改进显著提升连续控制性能") 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("扩展实验框架验证成功 - ✅")

📝 算法伪代码:TD3

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

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

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

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

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

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

🏃 动手练习

练习1: 策略延迟

测试policy_delay=1, 2, 4, 8的影响

练习2: 噪声大小

测试policy_noise和noise_clip的效果

练习3: TD3 vs SAC

在相同环境下对比TD3和SAC

📊 训练曲线说明

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

🏆
成就解锁:TD3
完成本课所有练习,掌握TD3三大创新的核心原理