双延迟深度确定性策略梯度算法 (TD3) 详解
TD3(双延迟深度确定性策略梯度算法)是针对连续控制任务的强化学习算法,旨在解决 DDPG 中的 Q 值过估计和不稳定性问题。文章详细介绍了 TD3 的三大核心改进:双 Critic 网络减少偏差、延迟策略更新提高稳定性以及目标策略平滑增强鲁棒性。内容涵盖理论背景、数学推导及完整的 PyTorch 代码实现,包括 Actor-Critic 架构、经验回放机制及训练循环逻辑,适合希望深入理解并复现 TD3 算法的技术人员阅读。

TD3(双延迟深度确定性策略梯度算法)是针对连续控制任务的强化学习算法,旨在解决 DDPG 中的 Q 值过估计和不稳定性问题。文章详细介绍了 TD3 的三大核心改进:双 Critic 网络减少偏差、延迟策略更新提高稳定性以及目标策略平滑增强鲁棒性。内容涵盖理论背景、数学推导及完整的 PyTorch 代码实现,包括 Actor-Critic 架构、经验回放机制及训练循环逻辑,适合希望深入理解并复现 TD3 算法的技术人员阅读。

双延迟深度确定性策略梯度算法,TD3(Twin Delayed Deep Deterministic Policy Gradient)是强化学习中专为解决连续动作空间问题设计的一种算法。TD3 算法的提出是在深度确定性策略梯度(DDPG)算法的基础上改进而来,用于解决强化学习训练中存在的一些关键挑战。
TD3 的提出基于以下几个强化学习的理论与技术发展:
TD3 算法由 Fujimoto 等人在 2018 年提出,对深度确定性策略梯度(Deep Deterministic Policy Gradient, DDPG)算法的改进。DDPG 是一种结合策略(Actor)和价值函数(Critic)的强化学习方法,可以在连续动作空间中表现出色。然而,DDPG 存在以下问题:
为了解决上述问题,TD3 通过以下三点创新改进了 DDPG。
TD3 在 DDPG 的基础上提出了三项关键改进:
TD3(Twin Delayed Deep Deterministic Policy Gradient)适用于连续动作空间问题,主要基于 Actor-Critic 框架和深度确定性策略梯度(DDPG)。以下是 TD3 的数学基础与推导。
Actor-Critic 方法的核心在于将策略学习(Actor)与价值评估(Critic)结合。Actor 负责生成动作,Critic 负责评估当前策略的表现。Actor 网络优化目标是通过 Critic 网络的反馈提高策略质量。
Actor 通过最大化累计奖励学习最优策略:
$$ \nabla_\phi J(\pi_\phi) = \mathbb{E}{s \sim \rho^\pi} \left[ \nabla\phi \pi_\phi(s) \nabla_a Q^\pi(s, a) \big|{a=\pi\phi(s)} \right] $$
其中:
Critic 通过最小化时间差分(Temporal Difference, TD)误差,学习状态 - 动作值函数:
$$ L(\theta) = \mathbb{E}{(s, a, r, s') \sim \mathcal{D}} \left[ \big( Q\theta(s, a) - y \big)^2 \right] $$
其中目标值 $y$ 定义为:
$$ y = r + \gamma Q_{\theta'}(s', \pi_{\phi'}(s')) $$
TD3 在 DDPG 的基础上,针对 Q 值过估计和策略训练不稳定问题,提出了三项核心改进。
TD3 引入两个 Critic 网络 $Q_{\theta_1}$ 和 $Q_{\theta_2}$,通过取最小值来降低 Q 值的高估偏差:
$$ y = r + \gamma \min \big( Q_{\theta_1'}(s', \pi_{\phi'}(s')), Q_{\theta_2'}(s', \pi_{\phi'}(s')) \big) $$
为了避免 Actor 网络频繁更新导致策略不稳定,TD3 在 Critic 更新 $n$ 次后才更新 Actor 一次(通常 $n=2$)。Actor 的优化目标为:
$$ L(\phi) = -\mathbb{E}{s \sim \mathcal{D}} \big[ Q{\theta_1}(s, \pi_\phi(s)) \big] $$
Critic 网络训练稳定后,Actor 的策略梯度才会更加准确。
在计算目标值 $y$ 时,对动作加入高斯噪声 $\epsilon \sim \mathcal{N}(0, \sigma)$ 并进行裁剪,防止策略过拟合到极端动作:
$$ a' = \pi_{\phi'}(s') + \text{clip}(\epsilon, -c, c) $$
伪代码如下:
目标网络软更新:
$$ \theta_i' \leftarrow \tau \theta_i + (1 - \tau) \theta_i' $$ $$ \phi' \leftarrow \tau \phi + (1 - \tau) \phi' $$
延迟更新 Actor:每隔 $d$ 步,更新 Actor 策略。
更新 Critic:通过最小化损失函数更新 $Q_{\theta_1}$ 和 $Q_{\theta_2}$:
$$ L(\theta_i) = \mathbb{E}{(s, a, r, s')} \big[ (Q{\theta_i}(s, a) - y)^2 \big] $$
从 $\mathcal{D}$ 中随机抽取一个批量数据 $(s, a, r, s')$:
TD3 使用两个 Critic 网络,损失函数为:
$$ L(\theta) = \mathbb{E} \big[ (Q_\theta(s, a) - y)^2 \big] $$
其中目标值:
$$ y = r + \gamma \min \big( Q_{\theta_1'}(s', a'), Q_{\theta_2'}(s', a') \big) $$
Actor 通过最大化 Critic 网络的输出优化策略:
$$ \nabla_\phi J(\phi) = \mathbb{E}{s \sim \rho^\pi} \big[ \nabla_a Q{\theta_1}(s, a) \big|{a=\pi\phi(s)} \nabla_\phi \pi_\phi(s) \big] $$
延迟更新使 Actor 网络只在 Critic 网络收敛后才更新,减少了 Actor 网络梯度被不准确 Q 值引导的风险,从而提高了稳定性。
# Python 3.11.5
# torch 2.1.0
# gym 0.26.2
import argparse
from collections import deque
import os
import random
import numpy as np
import gym
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.distributions import Normal
device = 'cuda' if torch.cuda.is_available() else 'cpu'
class ReplayBuffer:
def __init__(self, max_size):
self.storage = []
self.max_size = max_size
self.ptr = 0
def push(self, data):
if len(self.storage) == self.max_size:
self.storage[int(self.ptr)] = data
self.ptr = (self.ptr + 1) % self.max_size
else:
self.storage.append(data)
def sample(self, batch_size):
ind = np.random.randint(0, len(.storage), size=batch_size)
x, y, u, r, d = [], [], [], [], []
i ind:
X, Y, U, R, D = .storage[i]
x.append(np.array(X, copy=))
y.append(np.array(Y, copy=))
u.append(np.array(U, copy=))
r.append(np.array(R, copy=))
d.append(np.array(D, copy=))
np.array(x), np.array(y), np.array(u), np.array(r).reshape(-, ), np.array(d).reshape(-, )
(nn.Module):
():
(Actor, ).__init__()
.fc1 = nn.Linear(state_dim, )
.fc2 = nn.Linear(, )
.fc3 = nn.Linear(, action_dim)
.max_action = max_action
():
a = F.relu(.fc1(state))
a = F.relu(.fc2(a))
a = torch.tanh(.fc3(a)) * .max_action
a
(nn.Module):
():
(Critic, ).__init__()
.fc1 = nn.Linear(state_dim + action_dim, )
.fc2 = nn.Linear(, )
.fc3 = nn.Linear(, )
():
state_action = torch.cat([state, action], )
q = F.relu(.fc1(state_action))
q = F.relu(.fc2(q))
q = .fc3(q)
q
:
():
.actor = Actor(state_dim, action_dim, max_action).to(device)
.actor_target = Actor(state_dim, action_dim, max_action).to(device)
.critic_1 = Critic(state_dim, action_dim).to(device)
.critic_1_target = Critic(state_dim, action_dim).to(device)
.critic_2 = Critic(state_dim, action_dim).to(device)
.critic_2_target = Critic(state_dim, action_dim).to(device)
.actor_optimizer = optim.Adam(.actor.parameters())
.critic_1_optimizer = optim.Adam(.critic_1.parameters())
.critic_2_optimizer = optim.Adam(.critic_2.parameters())
.actor_target.load_state_dict(.actor.state_dict())
.critic_1_target.load_state_dict(.critic_1.state_dict())
.critic_2_target.load_state_dict(.critic_2.state_dict())
.max_action = max_action
.memory = ReplayBuffer(max_size=)
.num_critic_update_iteration =
.num_actor_update_iteration =
.num_training =
():
state = torch.tensor(state.reshape(, -)).().to(device)
.actor(state).cpu().data.numpy().flatten()
():
i (num_iteration):
x, y, u, r, d = .memory.sample(args.batch_size)
state = torch.FloatTensor(x).to(device)
action = torch.FloatTensor(u).to(device)
next_state = torch.FloatTensor(y).to(device)
done = torch.FloatTensor(d).to(device)
reward = torch.FloatTensor(r).to(device)
noise = torch.ones_like(action).data.normal_(, args.policy_noise).to(device)
noise = noise.clamp(-args.noise_clip, args.noise_clip)
next_action = (.actor_target(next_state) + noise).clamp(-.max_action, .max_action)
target_Q1 = .critic_1_target(next_state, next_action)
target_Q2 = .critic_2_target(next_state, next_action)
target_Q = torch.(target_Q1, target_Q2)
target_Q = reward + (( - done) * args.gamma * target_Q).detach()
current_Q1 = .critic_1(state, action)
loss_Q1 = F.mse_loss(current_Q1, target_Q)
.critic_1_optimizer.zero_grad()
loss_Q1.backward()
.critic_1_optimizer.step()
current_Q2 = .critic_2(state, action)
loss_Q2 = F.mse_loss(current_Q2, target_Q)
.critic_2_optimizer.zero_grad()
loss_Q2.backward()
.critic_2_optimizer.step()
i % args.policy_delay == :
actor_loss = - .critic_1(state, .actor(state)).mean()
.actor_optimizer.zero_grad()
actor_loss.backward()
.actor_optimizer.step()
param, target_param (.actor.parameters(), .actor_target.parameters()):
target_param.data.copy_(( - args.tau) * target_param.data + args.tau * param.data)
param, target_param (.critic_1.parameters(), .critic_1_target.parameters()):
target_param.data.copy_(( - args.tau) * target_param.data + args.tau * param.data)
param, target_param (.critic_2.parameters(), .critic_2_target.parameters()):
target_param.data.copy_(( - args.tau) * target_param.data + args.tau * param.data)
.num_actor_update_iteration +=
.num_critic_update_iteration +=
.num_training +=
__name__ == :
parser = argparse.ArgumentParser()
parser.add_argument(, default=)
parser.add_argument(, default=, =)
parser.add_argument(, default=, =)
parser.add_argument(, default=, =)
parser.add_argument(, default=, =)
parser.add_argument(, default=, =)
parser.add_argument(, default=, =)
parser.add_argument(, default=, =)
parser.add_argument(, default=, =)
parser.add_argument(, default=, =)
parser.add_argument(, default=, =)
parser.add_argument(, default=, =)
parser.add_argument(, default=, =)
parser.add_argument(, default=, =)
args = parser.parse_args()
env = gym.make(args.env_name)
state_dim = env.observation_space.shape[]
action_dim = env.action_space.shape[]
max_action = (env.action_space.high[])
agent = TD3(state_dim, action_dim, max_action)
args.mode == :
ep_r =
i (args.num_iteration):
state = env.reset()
t ():
action = agent.select_action(state)
action = action + np.random.normal(, args.exploration_noise, size=env.action_space.shape[])
action = action.clip(env.action_space.low, env.action_space.high)
next_state, reward, done, info = env.step(action)
ep_r += reward
agent.memory.push((state, next_state, action, reward, np.(done)))
(agent.memory.storage) >= args.capacity - :
agent.update(, args)
state = next_state
done t == args.max_episode - :
()
ep_r =
TD3 不仅改进了 DDPG 的不足,还为强化学习的稳定性研究提供了重要的理论和实践参考。其成功之处在于:
作为一个里程碑式的算法,TD3 推动了连续动作空间强化学习的发展,为后续算法(如 SAC、PPO 等)提供了宝贵的启发。
参考文献:Addressing Function Approximation Error in Actor-Critic Methods

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online