第一章:Dueling DQN(双流架构)
Dueling DQN 将 Q 网络拆分为状态价值分支 V(s)与动作优势分支 A(s,a),用两条并行的子网络分别估计状态的整体价值与动作的相对优势,再将二者组合得到 Q 值。这种设计在一些状态下动作差异较小(或动作无关)的场景下能显著提升估计稳定性与收敛速度。
1. 核心结构与数学形式
主干网络(卷积或前馈)提取共享特征后分为两条分支:
- Value Stream:输出标量 $V(s)$,表示状态本身的价值;
- Advantage Stream:输出向量 $A(s,a)$,表示在状态 $s$ 下各动作的相对优势。
为使分解可辨识(identifiable),通常用下式重组 Q 值:
$$Q(s, a) = V(s) + \Big(A(s, a) - \frac{1}{|\mathcal{A}|} \sum_{a'} A(s, a')\Big)$$
减去优势均值能保证 $\sum_a A(s,a)=0$,从而避免任意常数在 V 与 A 之间转移导致的不可辨识问题(见下面的唯一分解证明)。
2. 直觉与优点
- 当某些状态下不同动作收益接近时,专门学习 $V(s)$ 会更稳定;
- $A(s,a)$ 聚焦动作间差异,能更快捕捉微小策略优势;
- 可与 Double DQN、PER、NoisyNet 等改进方法直接结合,常见于强化学习竞技平台(Atari、ProcGen 等)。
3. 唯一分解(Identifiability)证明
问题:给定 Q(s,a),将其写成 $Q(s,a)=V(s)+A(s,a)$ 是否唯一?答案:在没有约束时不是唯一的;若对每个 s 施加约束 $\sum_a A(s,a)=0$(或等价的常数约束),则分解唯一。
证明步骤:
假设存在两组分解 $(V, A)$ 与 $(V', A')$ 满足对所有 $(s,a)$:
$$Q(s,a)=V(s)+A(s,a)=V'(s)+A'(s,a)$$
令 $D(s)=V(s)-V'(s)$,$E(s,a)=A(s,a)-A'(s,a)$,则对所有 $(s,a)$ 有
$$D(s)+E(s,a)=0 \quad \Rightarrow \quad E(s,a) = -D(s)$$
对动作集合求和并使用约束 $\sum_a E(s,a)=0$:
$$\sum_a E(s,a) = -\sum_a D(s) = -|\mathcal{A}|\, D(s) = 0 \Rightarrow D(s)=0$$
于是 $D(s)=0$,进而 $E(s,a)=0$,即 $(V,A)$ 与 $(V',A')$ 在施加均值约束下相同,分解唯一。
4. 网络架构详解
Dueling DQN 的网络由三部分组成:
- 共享特征提取层:卷积或全连接层用于提取状态特征(作为后两个分支的输入);
- Value 分支:独立的神经网络结构,最终输出单个标量 $V(s)$;
- Advantage 分支:独立的神经网络结构,最终输出 $|\mathcal{A}|$ 维向量 $A(s,a)$。
前向传播时,对 Advantage 层输出进行去均值处理后与 Value 层组合:
$$Q(s,a) = V(s) + \Big(A(s,a) - \frac{1}{|\mathcal{A}|}\sum_a A(s,a)\Big)$$
5. 初始化与训练细节
为确保网络稳定性和可辨识性,需要特别注意初始化:
- 权重初始化:使用 Xavier uniform 或 Kaiming 初始化;对所有线性层的偏置初始化为 0;
- 为什么需要特殊初始化:Advantage 分支初期应输出接近 0 的值,这样 $A - \text{mean}(A) \approx 0$,确保 Q 值初期主要由 $V(s)$ 主导,避免优势分支过大导致的数值不稳定;
- 主要优势:小初始优势保证了分解的可辨识性,使 V 与 A 不会在参数空间中互相抵消。
6. 实现步骤(逐步说明)
下面把 Dueling DQN 的训练过程拆成可直接落地的步骤,便于把代码映射到具体实现:
- 定义网络:共享主干提取特征;分出两个头——Value 输出标量 V(s),Advantage 输出向量 A(s,a)。前向时对 A 去均值再与 V 重构 Q。
- 经验收集:使用 ε-greedy 或可学习噪声策略采样 transition $(s,a,r,s',\text{done})$,并存入经验回放池 𝒟。
- 小批量采样:从 𝒟 中随机采样 batch;若使用 PER 请用重要性权重修正损失。
- 目标计算:用目标网络 $\theta^-$ 前向得到 $V'(s')$ 与 $A'(s',\cdot)$,对 $A'$ 去均值并重构 Q';目标 $y = r + \gamma \cdot \max_{a'} Q'(s',a')$(若 done 则 $y = r$)。
- 损失与更新:计算 $L = \text{MSE}(y, Q(s,a;\theta))$ 或 Huber Loss,反向传播并用优化器 step 更新主网络 $\theta$。
- 目标网络同步:采用硬更新(每 C 步复制)或软更新($\theta^- \leftarrow \tau\theta + (1-\tau)\theta^-$)。
7. PyTorch 代码示例
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
from collections import deque
class DuelingNetwork(nn.Module):
def __init__(self, state_dim, action_dim):
super(DuelingNetwork, self).__init__()
# 共享特征提取层
self.feature = nn.Sequential(
nn.Linear(state_dim, 128),
nn.ReLU(),
nn.Linear(128, 128),
nn.ReLU()
)
# Value 分支:输出单个标量
self.value_stream = nn.Sequential(
nn.Linear(128, 64),
nn.ReLU(),
nn.Linear(64, 1)
)
# Advantage 分支:输出 action_dim 维向量
self.advantage_stream = nn.Sequential(
nn.Linear(128, 64),
nn.ReLU(),
nn.Linear(64, action_dim)
)
# 权重初始化
for module in self.modules():
if isinstance(module, nn.Linear):
nn.init.xavier_uniform_(module.weight)
nn.init.constant_(module.bias, 0)
def forward(self, state):
# 提取共享特征
features = self.feature(state)
# Value 与 Advantage 独立前向
v = self.value_stream(features)
a = self.advantage_stream(features)
# 重组 Q 值(去均值保证可辨识)
a_mean = a.mean(dim=1, keepdim=True)
q = v + (a - a_mean)
return q
class DuelingDQN:
def __init__(self, state_dim, action_dim, lr=1e-4, gamma=0.99, epsilon=0.1):
self.state_dim = state_dim
self.action_dim = action_dim
self.gamma = gamma
self.epsilon = epsilon
# 主网络和目标网络
self.q_net = DuelingNetwork(state_dim, action_dim)
self.target_q_net = DuelingNetwork(state_dim, action_dim)
self.target_q_net.load_state_dict(self.q_net.state_dict())
self.optimizer = torch.optim.Adam(self.q_net.parameters(), lr=lr)
self.loss_fn = nn.MSELoss()
# 经验回放池
self.memory = deque(maxlen=10000)
def remember(self, state, action, reward, next_state, done):
"""存储经验"""
self.memory.append((state, action, reward, next_state, done))
def act(self, state):
"""ε-贪婪策略选择动作"""
if np.random.rand() < self.epsilon:
return np.random.randint(self.action_dim)
else:
with torch.no_grad():
q_values = self.q_net(torch.FloatTensor(state).unsqueeze(0))
return q_values.max(1)[1].item()
def train_batch(self, batch_size):
"""训练一个批次"""
if len(self.memory) < batch_size:
return
# 随机采样
indices = np.random.choice(len(self.memory), batch_size, replace=False)
batch = [self.memory[i] for i in indices]
states = torch.FloatTensor(np.array([x[0] for x in batch]))
actions = torch.LongTensor([x[1] for x in batch])
rewards = torch.FloatTensor([x[2] for x in batch])
next_states = torch.FloatTensor(np.array([x[3] for x in batch]))
dones = torch.FloatTensor([x[4] for x in batch])
# 当前 Q 值
q_values = self.q_net(states).gather(1, actions.unsqueeze(1)).squeeze(1)
# 目标 Q 值
with torch.no_grad():
max_next_q = self.target_q_net(next_states).max(1)[0]
target_q = rewards + self.gamma * max_next_q * (1 - dones)
# 损失和反向传播
loss = self.loss_fn(q_values, target_q)
self.optimizer.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_norm_(self.q_net.parameters(), 1.0)
self.optimizer.step()
return loss.item()
def update_target_network(self):
"""更新目标网络"""
self.target_q_net.load_state_dict(self.q_net.state_dict())
# 训练示例
if __name__ == "__main__":
state_dim = 4
action_dim = 2
agent = DuelingDQN(state_dim, action_dim)
# 模拟环境交互
for episode in range(100):
state = np.random.randn(state_dim)
episode_reward = 0
for step in range(50):
action = agent.act(state)
next_state = np.random.randn(state_dim)
reward = np.random.randn()
done = step == 49
agent.remember(state, action, reward, next_state, float(done))
agent.train_batch(batch_size=32)
episode_reward += reward
state = next_state
if (episode + 1) % 20 == 0:
agent.update_target_network()
print(f"Episode {episode+1}, Reward: {episode_reward:.2f}")
| 问题 | 解答 |
|---|---|
| 为什么要对 Advantage 去均值? | 去均值保证了分解的唯一性。若不去均值,可以在 V 与 A 间任意转移常数值而产生相同 Q,导致参数不可辨识。 |
| V 与 A 是否需要同时训练? | 是的。两个分支共享主干,反向传播会同时更新所有参数。V 学习状态价值,A 学习动作相对优势,两者分工明确。 |
| 初始化为什么很重要? | 初期 Advantage 应接近 0,使 Q ≈ V,确保网络稳定启动。过大的初始 A 会导致数值震荡,影响收敛。 |
| 如何与 Double DQN 结合? | 目标计算时用主网络选择动作,用目标网络评估价值,与标准 Double DQN 类似,只是网络结构从单个 Q 改为 V+A。 |
9. 实践要点与组合策略
- 与 Double DQN 结合:目标计算使用主网络选择最优动作,用目标网络评估价值,消除过估计偏差;
- 与 PER 结合:Prioritized Experience Replay 能更快聚焦高 TD 误差样本,与 Dueling 架构相辅相成;
- 离散 vs 连续动作:Dueling DQN 在离散动作问题上表现优异;连续控制需与确定性策略(如 DDPG)或策略分支改造;
- 调参建议:避免优势分支输出过大导致数值不稳(可加 L2 正则或梯度裁剪);监控 V 与 A 的大小比例。
第二章:PER
PER:优先经验回放
问题:普通经验回放是均匀随机采样,但有些样本信息量大(比如 TD 误差很大),有些没啥用。
解决:给每个样本打分,TD 误差越大 → 优先级越高 → 越容易被采样。
采样概率
$$P(i) = \frac{(|\delta_i| + \varepsilon)^\alpha}{\sum_k (|\delta_k| + \varepsilon)^\alpha}$$
| 参数 | 含义 | 常用值 |
|---|---|---|
| α | 控制优先级影响力度 | 0.6 |
| ε | 防止优先级为 0 | 1e-6 |
重要性权重
因为改变了采样分布,需要用权重修正,避免偏差:
$$w_i = \left(\frac{1}{N \cdot P(i)}\right)^\beta$$
β 从 0.4 逐渐升到 1.0,训练前期允许有点偏差,后期严格修正。
最终损失:
$$L = \text{mean}(w_i \cdot \delta_i^2)$$
三、整合流程:Dueling + PER + Double
- 初始化 online 网络(Dueling 结构)、target 网络、PER 缓冲区
- 与环境交互,得到 (s, a, r, s', done)
- 计算 TD 误差: $$\delta = \big|r + \gamma \cdot Q_{\text{target}}\big(s', \arg\max_{a'} Q_{\text{online}}(s',a')\big) - Q_{\text{online}}(s,a)\big|$$
- 存入 PER,优先级 = δ + ε
- 按优先级采样 batch,得到权重 w
- 计算加权损失,反向传播
- 更新 PER 中各样本的优先级
- 定期同步 target 网络
注意:这里用的是 Double DQN 的目标计算方式(online 选动作,target 评估),防止 Q 值过估计。
四、完整代码实现
Dueling 网络
import torch
import torch.nn as nn
class DuelingDQN(nn.Module):
def __init__(self, state_dim, action_dim):
super().__init__()
# 共享特征层
self.feature = nn.Sequential(
nn.Linear(state_dim, 128),
nn.ReLU(),
nn.Linear(128, 128),
nn.ReLU()
)
# 分支 1:状态价值
self.value = nn.Linear(128, 1)
# 分支 2:动作优势
self.advantage = nn.Linear(128, action_dim)
def forward(self, x):
feat = self.feature(x)
v = self.value(feat)
a = self.advantage(feat)
# 去均值组合
q = v + (a - a.mean(dim=1, keepdim=True))
return q
PER 缓冲区(SumTree 实现)
import numpy as np
import random
class SumTree:
"""SumTree 用于快速采样,时间复杂度 O(log N)"""
def __init__(self, capacity):
self.capacity = capacity
self.tree = np.zeros(2 * capacity - 1)
self.data = [None] * capacity
self.write = 0
self.n_entries = 0
def _propagate(self, idx, change):
parent = (idx - 1) // 2
self.tree[parent] += change
if parent != 0:
self._propagate(parent, change)
def update(self, idx, priority):
change = priority - self.tree[idx]
self.tree[idx] = priority
self._propagate(idx, change)
def add(self, priority, data):
idx = self.write + self.capacity - 1
self.data[self.write] = data
self.update(idx, priority)
self.write = (self.write + 1) % self.capacity
self.n_entries = min(self.n_entries + 1, self.capacity)
def get(self, s):
idx = 0
while True:
left = 2 * idx + 1
if left >= len(self.tree):
break
if s <= self.tree[left]:
idx = left
else:
s -= self.tree[left]
idx = left + 1
data_idx = idx - self.capacity + 1
return idx, self.tree[idx], self.data[data_idx]
@property
def total(self):
return self.tree[0]
class PERBuffer:
def __init__(self, capacity, alpha=0.6, eps=1e-6):
self.tree = SumTree(capacity)
self.alpha = alpha
self.eps = eps
def add(self, td_error, sample):
priority = (abs(td_error) + self.eps) ** self.alpha
self.tree.add(priority, sample)
def sample(self, batch_size, beta=0.4):
batch, idxs, priorities = [], [], []
segment = self.tree.total / batch_size
for i in range(batch_size):
a, b = segment * i, segment * (i + 1)
s = random.uniform(a, b)
idx, p, data = self.tree.get(s)
batch.append(data)
idxs.append(idx)
priorities.append(p)
# 计算重要性权重
probs = np.array(priorities) / self.tree.total
weights = (self.tree.n_entries * probs) ** (-beta)
weights /= weights.max() # 归一化
return batch, idxs, torch.FloatTensor(weights)
def update_priority(self, idx, td_error):
priority = (abs(td_error) + self.eps) ** self.alpha
self.tree.update(idx, priority)
训练流程
import torch.optim as optim
import torch.nn.functional as F
# 初始化
online_net = DuelingDQN(state_dim=4, action_dim=2)
target_net = DuelingDQN(state_dim=4, action_dim=2)
target_net.load_state_dict(online_net.state_dict())
optimizer = optim.Adam(online_net.parameters(), lr=1e-4)
buffer = PERBuffer(capacity=10000, alpha=0.6)
gamma = 0.99
beta = 0.4
for episode in range(1000):
state = env.reset()
done = False
while not done:
# ε-greedy 选动作
if random.random() < epsilon:
action = random.randint(0, action_dim-1)
else:
with torch.no_grad():
q = online_net(torch.FloatTensor(state).unsqueeze(0))
action = q.argmax().item()
next_state, reward, done, _ = env.step(action)
# 计算 TD 误差
with torch.no_grad():
q_next = target_net(torch.FloatTensor(next_state).unsqueeze(0))
best_action = online_net(torch.FloatTensor(next_state).unsqueeze(0)).argmax()
td_target = reward + (1 - done) * gamma * q_next[0, best_action]
q_current = online_net(torch.FloatTensor(state).unsqueeze(0))[0, action]
td_error = abs(td_target - q_current).item()
# 存入 PER
buffer.add(td_error, (state, action, reward, next_state, done))
state = next_state
# 训练
if buffer.tree.n_entries > 64:
batch, idxs, weights = buffer.sample(32, beta)
states = torch.FloatTensor([x[0] for x in batch])
actions = torch.LongTensor([x[1] for x in batch])
rewards = torch.FloatTensor([x[2] for x in batch])
next_states = torch.FloatTensor([x[3] for x in batch])
dones = torch.FloatTensor([x[4] for x in batch])
# Double DQN 目标
q_eval = online_net(states).gather(1, actions.unsqueeze(1)).squeeze()
with torch.no_grad():
best_actions = online_net(next_states).argmax(1)
q_next = target_net(next_states).gather(1, best_actions.unsqueeze(1)).squeeze()
q_target = rewards + (1 - dones) * gamma * q_next
# 加权损失
td_errors = (q_target - q_eval).abs()
loss = (weights * F.mse_loss(q_eval, q_target, reduction='none')).mean()
optimizer.zero_grad()
loss.backward()
optimizer.step()
# 更新优先级
for idx, err in zip(idxs, td_errors.detach().numpy()):
buffer.update_priority(idx, err)
# 定期同步 target 网络
if episode % 10 == 0:
target_net.load_state_dict(online_net.state_dict())
五、效果对比
- Dueling:网络更高效,尤其在动作影响小的状态
- PER:收敛更快,样本利用率更高
- Double:防止 Q 值过估计
三者组合是 Rainbow DQN 的核心,在 Atari 游戏上表现优异。
改进路线:DQN → Double DQN → Dueling DQN → PER → Rainbow DQN
第三章:NoisyNet DQN(自适应探索)
前面通过 Dueling 和 PER 改进了网络结构和采样效率,但探索问题还没解决好。
传统的 ε-greedy 探索靠人工设定 ε,它的随机性跟网络学习没什么关系,很难自适应调整探索强度。
NoisyNet DQN(Fortunato et al., 2017)换了个思路 —— 直接在网络参数里加可学习的噪声,让探索变成网络学习的一部分。
一、核心想法
用带噪声的线性层替代普通全连接层:
y = (W + σ_W ⊙ ε_W) · x + (b + σ_b ⊙ ε_b)
W, b:标准权重和偏置σ_W, σ_b:可学习的噪声强度(网络训练时会调整)ε_W, ε_b:每次前向传播重新采样的高斯噪声
这样网络输出自带随机性,噪声大小随训练自动调整。不用手动调 ε 了,探索强度网络自己学。
二、代码实现
import torch
import torch.nn as nn
import torch.nn.functional as F
import math
class NoisyLinear(nn.Module):
def __init__(self, in_features, out_features, std_init=0.5):
super().__init__()
self.in_features = in_features
self.out_features = out_features
self.std_init = std_init
# 可学习参数
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_eps", 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_eps", torch.empty(out_features))
self.reset_parameters()
self.sample_noise()
def reset_parameters(self):
mu_range = 1 / math.sqrt(self.in_features)
self.weight_mu.data.uniform_(-mu_range, mu_range)
self.weight_sigma.data.fill_(self.std_init / math.sqrt(self.in_features))
self.bias_mu.data.uniform_(-mu_range, mu_range)
self.bias_sigma.data.fill_(self.std_init / math.sqrt(self.out_features))
def sample_noise(self):
"""重新采样噪声"""
self.weight_eps.normal_()
self.bias_eps.normal_()
def forward(self, x):
if self.training:
# 训练时加噪声
w = self.weight_mu + self.weight_sigma * self.weight_eps
b = self.bias_mu + self.bias_sigma * self.bias_eps
else:
# 测试时不加噪声
w, b = self.weight_mu, self.bias_mu
return F.linear(x, w, b)
三、与 Dueling DQN 结合
class NoisyDuelingDQN(nn.Module):
def __init__(self, state_dim, action_dim):
super().__init__()
# 共享特征层(普通层)
self.feature = nn.Sequential(
nn.Linear(state_dim, 128),
nn.ReLU(),
nn.Linear(128, 128),
nn.ReLU()
)
# 价值流和优势流用 NoisyLinear
self.value = NoisyLinear(128, 1)
self.advantage = NoisyLinear(128, action_dim)
def forward(self, x):
feat = self.feature(x)
v = self.value(feat)
a = self.advantage(feat)
q = v + (a - a.mean(dim=1, keepdim=True))
return q
def sample_noise(self):
"""重采样所有噪声层"""
self.value.sample_noise()
self.advantage.sample_noise()
四、训练流程
- 把 Dueling DQN 的
Linear层换成NoisyLinear - 去掉 ε-greedy,探索完全靠噪声驱动
- 每次前向传播前调用
sample_noise()重新采样噪声 - 损失函数、TD 目标、target 更新都跟 DQN 一样
# 训练时无需 ε-greedy
for episode in range(1000):
state = env.reset()
online_net.sample_noise() # 每个 episode 开始重采样噪声
while not done:
# 直接选最优动作,噪声已经在网络里了
with torch.no_grad():
q = online_net(torch.FloatTensor(state).unsqueeze(0))
action = q.argmax().item()
next_state, reward, done, _ = env.step(action)
buffer.add((state, action, reward, next_state, done))
state = next_state
# 训练步骤
if len(buffer) > batch_size:
batch = buffer.sample(batch_size)
online_net.sample_noise() # 训练前重采样
target_net.sample_noise()
# 计算损失并更新...
五、优点
- 自适应探索:网络在不确定的状态自动增大噪声
- 减少超参:不用手动调 ε 和衰减策略了
- 性能更好:在 Atari 和 MuJoCo 上普遍优于 ε-greedy
六、典型组合
NoisyNet 经常和这些一起用:
- Dueling DQN → 提升特征表达
- Double DQN → 减少过估计
- PER → 加速样本利用
组合起来就是 Rainbow DQN 的第三个核心模块。
七、小结
NoisyNet 把探索机制融入网络参数,让智能体自己调节探索强度。
相比传统 ε-greedy,这才是真正的"可学习探索",在复杂环境里更高效、更智能。
改进路线:DQN → Double DQN → Dueling DQN → PER → NoisyNet DQN → Rainbow DQN
第四章:Rainbow DQN(集大成者)
预备知识:N-Step Learning(多步时序差分)
核心动机: 相比 1-step TD 只看下一步奖励,N-step 将未来 N 步的真实奖励一次性纳入目标,让奖励信号更快地回传到当前状态,从而加速学习、提高样本利用率。
1. 理论背景:介于 TD 与 Monte Carlo 之间的折中
传统时序差分(TD)每步更新一次,只利用下一步奖励:
$$y_t^{(1)} = r_t + \gamma \max_{a} Q_{\text{target}}(s_{t+1}, a)$$而 Monte Carlo 方法等整局结束后用完整回报更新:
$$y_t^{(MC)} = r_t + \gamma r_{t+1} + \gamma^{2} r_{t+2} + \dots + \gamma^{T-t} r_T$$二者特点如下:
- TD: 更新快、低方差,但仅看一步,传播慢。
- Monte Carlo: 利用完整信息、无偏,但需等整局结束、方差大。
N-step Learning 介于两者之间,通过固定长度 N 的滚动窗口,将未来 N 步的真实奖励纳入更新目标:
$$y_t^{(N)} = \sum_{i=0}^{N-1} \gamma^{i} r_{t+i} + \gamma^{N} \max_{a} Q_{\text{target}}(s_{t+N}, a)$$这样可以同时保留 TD 的在线更新能力,又引入 Monte Carlo 的长远视角,兼顾稳定性与高效性。
2. 实现逻辑(滑动窗口机制)
- 维护长度为 N 的队列
n_step_buffer,每次环境交互后入队样本(s, a, r, s_next, done)。 - 当队列满 N 时,计算累计折扣奖励: $$R = \sum_{i=0}^{N-1} \gamma^{i} r_{t+i}$$
- 取队首状态
(s_t, a_t)和队尾下一个状态s_{t+N}构造 N-step 样本: $$(s_t, a_t, R, s_{t+N}, done)$$ - 存入经验回放池,训练时以目标: $$y_t^{(N)} = R + \gamma^{N}\max_{a} Q_{\text{target}}(s_{t+N}, a)$$
3. 参数与注意事项
- N 通常取 3~5(Rainbow 默认 N=3)。
- 若回合提前结束(不足 N 步),直接用已观测到的奖励累加。
- 与 PER、NoisyNet、Dueling 可无缝结合,增强训练效率与稳定性。
预备知识:C51(Categorical DQN,分布式价值学习)
核心动机: DQN 只学习回报的期望 Q(s,a),而 C51 直接学习回报分布 Z(s,a),捕捉风险与不确定性,使智能体的策略更稳健、更具表达力。
1. 固定支持区间与 51 个离散原子(atoms)
- C51 将未来回报的范围固定在
[V_min, V_max]上,并均匀划分为 51 个取值点:
$$z_i = V_{\min} + i\,\frac{V_{\max}-V_{\min}}{50},\quad i=0,1,\dots,50$$
- 每个动作 a 输出 51 个概率
p_i(s,a),表示回报落在 z_i 处的概率。
2. Vmin / Vmax 的选取依据
- 理论上:若每步奖励范围为
[r_min, r_max],折扣因子为 γ,则未来累计回报的上下限为: $$V_{\min} = \frac{r_{\min}}{1-\gamma},\quad V_{\max} = \frac{r_{\max}}{1-\gamma}$$ - 实践中可根据任务经验选取更紧区间。例如:
- Atari 环境:通常设为 [-10, 10] 或 [-100, 100]
- 连续控制任务(如 MuJoCo):根据奖励尺度适当缩放
- 固定
[V_min, V_max]使网络输出维度稳定,便于分布投影。
3. 网络输出与动作选择
- 输出:每个动作对应 51 维概率分布,经 softmax 归一化。
- 训练:优化整条分布;执行动作时取期望: $$Q(s,a) = \sum_i p_i(s,a) z_i$$
4. 分布式 Bellman 更新与投影
- 用 Double DQN 思想选择下一动作: $$a^* = \arg\max_{a} \sum_i p_i(s',a) z_i$$
- 构造目标分布: $$T Z = r + \gamma \; Z_{target}(s', a^*)$$
- 将其线性投影回固定支持区间,得到目标概率分布 m_i。
5. 损失函数:交叉熵(Cross-Entropy)
训练目标:让预测分布 p 逼近目标分布 m。
交叉熵最小值证明(基于 Jensen 不等式)
定义交叉熵:$$H(p,q) = -\sum_i p_i \ln q_i$$ ,证明当 $p=q$ 时 $H(p,q)$ 最小。
$$H(p,q)-H(p,p)=\sum_i p_i \ln \frac{q_i}{p_i}$$由于 $\ln(x)$ 为凹函数,Jensen 不等式:
$$\sum_i p_i \ln x_i \le \ln\Big(\sum_i p_i x_i\Big),\; \sum_i p_i = 1$$取 $x_i = q_i/p_i$ 得:
$$\sum_i p_i \ln \frac{q_i}{p_i} \le \ln\Big(\sum_i q_i\Big)=\ln 1=0$$ $$\Rightarrow\; H(p,p) - H(p,q) \le 0 \;\Rightarrow\; H(p,p) \le H(p,q)$$当 $p=q$ 时等号成立。
因此,预测分布等于目标分布時,交叉熵取得最小值,这就是分布学习稳定的理论基础。
6. 小结
- N-Step: 是 TD 与 Monte Carlo 的中间形态,结合短期稳定与长期视野,加快奖励传播。
- C51: 直接学习回报分布(51 个概率),通过交叉熵优化预测分布与目标分布的一致性。
- Vmin/Vmax: 表示环境可能回报的全局上下界,通常设为
[r_min/(1-γ), r_max/(1-γ)]或经验范围。 - 两者均为 Rainbow DQN 的关键组成模块,与 Double、Dueling、PER、NoisyNet 共同组成完整强化学习彩虹体系。
强化学习进阶:Rainbow DQN 🌈(终极 DQN 改进版)
论文: Hessel et al., "Rainbow: Combining Improvements in Deep Reinforcement Learning", DeepMind, 2018
核心思想: Rainbow 将六大强化学习改进模块整合入 DQN 框架,使智能体在稳定性、样本效率、探索能力与表达能力上全面提升。
1. Rainbow 的六大组成模块
| 模块 | 改进方向 | 主要作用 |
|---|---|---|
| Double DQN | 目标计算 | 减少 Q 值过估计偏差 |
| Dueling DQN | 网络结构 | 分离状态价值与动作优势 |
| PER(Prioritized Experience Replay) | 数据采样 | 提高关键样本采样频率 |
| NoisyNet | 探索策略 | 在参数中注入可学习噪声,替代 ε-greedy |
| N-Step Learning | 学习信号 | 加速奖励传播,兼顾 TD 与 Monte Carlo |
| C51 | 价值表示 | 预测回报分布而非单一期望值 |
这六个模块构成了 DQN 的"彩虹"增强体系。
2. 网络结构(Dueling + NoisyNet + C51)
- Dueling 结构: 共享特征提取层后分为两支:
使网络能区分"状态本身价值"与"动作带来的增益",提升训练稳定性。Q(s,a) = V(s) + [A(s,a) - mean(A(s,·))] - NoisyNet: 将全连接层替换为带噪声线性层:
每次前向传播前采样随机噪声 ε,σ 为可学习参数,形成可学习探索机制。W' = W + σ_W ⊙ ε_W, b' = b + σ_b ⊙ ε_b - C51 输出层: 每个动作输出 51 个 logits,经 softmax 得到分布概率:
最终 Q 值为期望:p_i(s,a) = softmax(z_i)$$Q(s,a) = \sum_i p_i(s,a)\, z_i$$
3. 数据与目标构建(N-Step + Double + C51 Projection)
3.1 N-Step 滑动窗口机制
维护长度为 N 的队列 n_step_buffer,每次交互后存入样本:
(s_t, a_t, r_t, s_{t+1}, done_t)
当队列满 N 步时,计算累计折扣奖励:
并生成 N-Step 样本 (s_t, a_t, R, s_{t+N}, done_{t+N}) 存入回放池。
3.2 Double DQN 目标动作选择
- 在线网络(Qonline)选动作: $$a^* = \arg\max_{a} \sum_i p_i(s',a) z_i$$
- 目标网络(Qtarget)用来评估分布:
Z_target = Z_target(s', a*)
3.3 C51 分布式 Bellman 投影
对目标分布的每个原子 zj 计算仿射平移:
$$t_z = \operatorname{clip}(r_{acc} + \gamma^{N} z_j, V_{\min}, V_{\max})$$将其投影回固定支持点集合 {z_i} 上,按线性权重分摊概率质量,得到目标分布 m。
4. 损失函数与 PER 优先采样
4.1 交叉熵与 KL 散度的关系
给定目标分布 p(真实)与预测分布 q(模型输出),有:
展开后可得:
$$H(p,q)=H(p)+D_{KL}(p\Vert q)$$由于 H(p) 是常数,最小化交叉熵 H(p,q) 等价于最小化 KL 散度 D_KL(p||q)。
因此,C51 使用的交叉熵损失,本质上就是最小化目标分布与预测分布之间的 KL 差异。
4.2 三种常见损失函数设计方式
| 类型 | 定义 | 说明 |
|---|---|---|
| (1) 分布交叉熵损失 | $$L = -\sum_i m_i \log p_i$$ | 最常用,与 C51 原论文一致;直接度量分布差异,等价于最小化 KL 散度。 |
| (2) 期望 TD 误差损失 | L = (R + γ^N·Q_target - Q_online)^2 |
将分布取期望后回退到传统 TD 形式,计算更直观,但丢失分布信息。 |
| (3) 混合损失 | L = λ·L_CE + (1−λ)·L_TD |
融合交叉熵与 TD 误差信号,兼顾分布拟合与稳定性(λ≈0.5 常用)。 |
一般推荐使用分布交叉熵损失(方案 1),与 C51 理论完全一致;在某些任务中,为增强稳定性可使用混合损失。
4.3 PER 优先级计算(分布式版本)
- 方法 1: 使用样本交叉熵 $$CE_i = -\sum_i m_i \log p_i$$ 作为优先级。
- 方法 2: 取分布期望计算 TD 误差
|δ| = |y - Q|。 - 方法 3: 混合优先级
prio_i = λ·CE_i + (1−λ)·|δ|。
采样概率与重要性权重如下:
P(i) = prio_i^α / Σ_j prio_j^α
w_i = (N·P(i))^-β
其中 α≈0.6 控制采样偏好强度,β 从 0.4 线性上升至 1.0 用于校正采样偏差。
5. 目标网络更新与噪声刷新
- Target 网络参数每 K 步"硬更新":
θ^- ← θ(例如每 2000 步)。 - 或采用软更新:
θ^- ← τθ + (1−τ)θ^−,τ≈1e−3。 - 每次前向传播前
sample_noise(),以刷新探索噪声。
6. 训练主流程(伪代码)
# Rainbow DQN 主循环
Initialize Q_online, Q_target ← Q_online
Initialize prioritized replay buffer
for each episode:
s ← env.reset()
n_step_buffer ← deque(maxlen=N)
while not done:
# 1. 使用 NoisyNet 选择动作
a ← argmax_a E[Z(s,a;θ)]
s_next, r, done ← env.step(a)
n_step_buffer.append((s, a, r, s_next, done))
# 2. 若 n_step_buffer 满 N 步,生成 N-Step 样本
if len(n_step_buffer) == N:
R ← Σ γ^i·r_{t+i}
store_transition(s, a, R, s_{t+N}, done)
s ← s_next
# 3. 每步训练
batch ← replay_buffer.sample(B)
compute target distribution m
compute loss \(L = -\sum_i m_i \log p_i\)
update θ with weighted loss
update priorities with per-sample loss
periodically update Q_target
7. 超参数参考
| 参数 | 典型值 | 说明 |
|---|---|---|
| 折扣 γ | 0.99 | 长期奖励折扣 |
| N-Step | 3 | 奖励传播步数 |
| α | 0.6 | PER 采样偏好强度 |
| β | 0.4 → 1.0 | 重要性权重修正 |
| V_min, V_max | [-10, 10] 或 [-100, 100] | C51 支持区间 |
| 学习率 | 1e−4 | Adam 优化器 |
| 批量大小 | 32 / 64 | 训练批次大小 |
8. 总结
- Rainbow 是 DQN 的全面整合版本,融合六大模块:
Rainbow = Dueling + NoisyNet + PER + N-Step + Double + C51
第五章:DPG & DDPG(从确定性策略梯度到深度实现)
深入理解 DPG(Deterministic Policy Gradient):从随机到确定性策略的桥梁
强化学习的策略梯度方法(Policy Gradient)家族中,从最早的 REINFORCE 到 Actor-Critic(AC)、A2C/A3C 再到 DPG/DDPG/TD3/SAC,其核心思想一直围绕一个问题:
如何直接优化一个参数化的策略,使得长期回报最大?
DPG(Deterministic Policy Gradient)是其中的重要分支,它通过将随机策略简化为确定性函数,大幅降低方差、提高学习效率,成为连续动作控制任务(如机械臂、无人驾驶、仿真控制等)的关键算法。
一、从随机到确定性:DPG 的诞生动机
在经典的 Actor-Critic (AC) 算法中,策略是随机的:
π_θ(a|s)
也就是说,给定状态 $s$,Actor 输出一个动作分布(通常是高斯分布),再从中采样动作 $a$。
这样的好处是——可以自然地实现探索。但问题也很明显:
- 方差高:梯度估计依赖采样,更新方向噪声大;
- 效率低:连续动作空间下的积分极难估计;
- 不稳定:动作采样引入的随机性增加了策略学习的不确定性。
于是,Silver 等人在 2014 年提出了 DPG(论文:Deterministic Policy Gradient Algorithms, ICML 2014),核心思想是:
在连续控制任务中,我们可以让策略直接输出一个确定性的动作 $a = \mu_\theta(s)$,而不是动作分布。
二、DPG 定理(Deterministic Policy Gradient Theorem)
DPG 理论的根基就是这个定理:
Theorem (Deterministic Policy Gradient Theorem)
设策略为确定性函数 $\mu_\theta(s)$,其性能目标为:
$$ J(\mu_\theta) = \mathbb{E}_{s \sim \rho^\mu}[Q^\mu(s, \mu_\theta(s))] $$
其中 $\rho^\mu` 是策略 $\mu$ 下的折扣状态分布。
则在满足可导条件下,有:
$$ \nabla_\theta J(\mu_\theta) = \mathbb{E}_{s \sim \rho^\beta}\left[\nabla_\theta \mu_\theta(s) \, \nabla_a Q^\mu(s,a)\Big|_{a=\mu_\theta(s)}\right] $$
其中 $\rho^\beta$ 为任意行为策略的状态分布,因此该梯度可离策略(off-policy)估计。
(详细证明见论文:Silver et al., 2014, ICML)
📘 定理要点解读
- 不再需要 $\log \pi_\theta(a|s)$;
- 梯度通过链式法则直接计算:$\nabla_\theta \mu_\theta(s) \times \nabla_a Q(s,a)$;
- 不需要对动作空间积分;
- 计算方差大大降低;
- 可以离策略训练(使用经验回放)。
三、为什么可以确定性输出 μ(s)?
在随机策略下:
$$a \sim \pi_\theta(a\mid s)$$在连续动作任务中,通常假设 $\pi_\theta(a|s) = \mathcal{N}(\mu_\theta(s), \sigma^2 I)$。
如果我们让 $\sigma \rightarrow 0$,则分布收敛为 $\delta$ 分布:
$$\pi_\theta(a\mid s) \;\Rightarrow\; \delta\big(a - \mu_\theta(s)\big)$$此时梯度从
$$\nabla_\theta J = \mathbb{E}\big[\nabla_\theta \log \pi_\theta(a\mid s)\; Q(s,a)\big]$$自然过渡到
$$\nabla_\theta J = \mathbb{E}\big[\nabla_\theta \mu_\theta(s)\; \nabla_a Q(s,a)\big]$$这正是 DPG 的梯度形式。
因此:DPG 是随机策略梯度的零方差极限形式。
四、为什么 DPG 可以 off-policy?
DPG 之所以可以 off-policy,是因为它的梯度形式不依赖动作分布,只依赖状态分布;而状态分布的偏差可以用经验回放近似补齐。
具体来说:
- 随机策略梯度依赖 $\pi(a|s)$,必须是当前策略采样的数据;
- 确定性策略梯度只依赖 $Q(s, \mu(s))$ 的梯度方向,与采样策略无关;
- 只要状态分布 $\rho^\beta$ 覆盖了 $\rho^\mu$,梯度估计就是无偏的;
- 经验回放池中的历史数据可以近似提供这种覆盖。
五、从理论上如何验证"确定性策略假设"是合理的?
你可能会问:为什么我们可以把策略"限定"为确定性函数 $\mu_\theta(s)$?这个假设是否丢失了最优解?
验证它"有用"且"正确",其实要看三层逻辑:
层次 ① 存在性层面:最优策略是否可以是确定性的?
结论:✅ 是的(由 Bellman 最优性原理保证)
理论依据:
在 MDP 框架下,对于任何随机策略 $\pi(a|s)$,总存在一个确定性策略 $\mu(s)$ 使得:
$$ V^{\mu}(s) \geq V^{\pi}(s), \quad \forall s $$
这是因为最优策略可以通过 Bellman 最优方程直接构造:
$$ \mu^*(s) = \arg\max_a Q^*(s, a) $$
也就是说,至少存在一个确定性策略是最优的。
直觉解释:
- 如果某个状态下,动作 $a_1$ 的 Q 值最高,那么总是选 $a_1$ 就是最优的;
- 没有必要按概率"掷骰子"在 $a_1$ 和次优动作之间随机选择;
- 随机性只会降低期望回报(除非是为了探索)。
层次 ② 可优化性层面:在确定性策略类中,性能目标 $J(\mu_\theta)$ 是否可微、可优化?
结论:✅ 是的(DPG 定理给出梯度表达式)
理论依据:
Silver et al. (2014) 证明了,在确定性策略下,性能目标关于参数 $\theta$ 的梯度为:
$$ \nabla_\theta J(\mu_\theta) = \mathbb{E}_{s \sim \rho}\left[\nabla_\theta \mu_\theta(s) \, \nabla_a Q(s,a)\Big|_{a=\mu_\theta(s)}\right] $$
这个梯度:
- ✅ 可计算:通过链式法则从神经网络反向传播;
- ✅ 无偏:期望与真实梯度方向一致;
- ✅ 低方差:不需要采样动作,方差远低于随机策略梯度。
直觉解释:
- 我们可以把 $J(\mu_\theta)$ 看作是一个关于 $\theta$ 的函数;
- 只要 $\mu_\theta(s)$ 和 $Q(s,a)$ 可微,$J$ 就可微;
- 因此可以用梯度上升法优化它。
层次 ③ 逼近性层面:参数化函数 $\mu_\theta$ 是否足够表达丰富策略?
结论:✅ 若神经网络足够大/非线性足够强,则可逼近任意确定性策略
理论依据:
根据万能逼近定理(Universal Approximation Theorem):
一个足够宽的单层神经网络(或足够深的多层网络)可以逼近任意连续函数 $f: \mathbb{R}^n \to \mathbb{R}^m$。
因此,对于任意确定性策略 $\mu^*(s)$,只要它是连续的(或分段连续),就存在参数 $\theta$ 使得:
$$ \mu_\theta(s) \approx \mu^*(s) $$
直觉解释:
- 神经网络是一个"函数逼近器";
- 只要网络够大、训练足够,它可以学到复杂的从状态到动作的映射;
- 实践中,这个假设在高维连续控制任务上表现很好(如 MuJoCo、机器人控制)。
三层验证总结表
| 层次 | 检验内容 | 结论 |
|---|---|---|
| ① 存在性层面 | 最优策略是否可以是确定性的? | ✅ 是(由 Bellman 原理保证) |
| ② 可优化性层面 | 在确定性策略类中,性能目标 $J(\mu_\theta)$ 是否可微、可优化? | ✅ 是(DPG 定理给出梯度表达式) |
| ③ 逼近性层面 | 参数化函数 $\mu_\theta$ 是否足够表达丰富策略? | ✅ 若神经网络足够大/非线性足够强,则可逼近任意确定性策略 |
结论:确定性策略假设是理论上合理、实践上可行的。
这三层验证告诉我们:
- 我们没有丢失最优解(至少存在一个确定性最优策略);
- 我们可以找到它(梯度可计算、方向正确);
- 我们可以用神经网络表示它(万能逼近定理保证)。
六、DPG 与 AC 的对比
| 特征 | AC / A2C / A3C | DPG / DDPG |
|---|---|---|
| 策略类型 | 随机策略 $\pi(a|s)$ | 确定性策略 $\mu(s)$ |
| 动作选择 | 采样 $a \sim \pi(a|s)$ | 直接输出 $a = \mu(s)$ |
| 更新形式 | $\nabla_\theta \log \pi(a|s) A(s,a)$ | $\nabla_\theta \mu(s) \nabla_a Q(s,a)$ |
| 方差 | 高 | 低 |
| 数据类型 | on-policy | off-policy |
| 方差抑制 | 值函数基线 | 目标网络 + 回放池 |
| 探索来源 | 策略采样 | 外部噪声 |
| 稳定机制 | Advantage + 多线程 | Target Net + Replay Buffer |
| 典型任务 | 离散/低维连续 | 高维连续控制 |
| 核心理论 | 随机策略梯度定理 | 确定性策略梯度定理 |
七、DDPG 算法流程(工程版)
DPG 理论很优雅,但在实践中通常与 DQN 的稳定化技巧结合,形成 DDPG(Deep DPG)。以下流程是带目标网络和回放池的可用版。
初始化
- 随机初始化 Actor $\mu_\theta$、Critic $Q_\phi$
- 复制到目标网络 $\mu_{\theta'}, Q_{\phi'}$
- 建立经验回放池 $\mathcal{D}$
- 设定超参数:学习率、$\gamma$、$\tau$、批大小、噪声标准差等
交互采样(带探索噪声)
在每个时间步:
$$a_t = \operatorname{clip}\big(\mu_\theta(s_t) + \epsilon_t,\; \text{bounds}\big),\quad \epsilon_t \sim \mathcal{N}(0,\, \sigma_{\text{explore}}^{2} I)$$执行 $a_t$,收集 $(s_t, a_t, r_t, s_{t+1}, d_t)$,加入回放池。
采样批次
从 $\mathcal{D}$ 中随机采样 $N$ 条经验:
$$(s_i, a_i, r_i, s'_i, d_i)$$Critic 更新
目标值(TD 目标):
$$y_i = r_i + \gamma\,(1 - d_i) \, Q_{\phi'}\!\big(s'_i,\, \mu_{\theta'}(s'_i)\big)$$Critic 损失:
$$L_Q = \frac{1}{N} \sum_i \big(Q_{\phi}(s_i,a_i) - y_i\big)^2$$优化:
Actor 更新(确定性策略梯度)
$$\nabla_{\theta} J \;\approx\; \frac{1}{N} \sum_i \nabla_{\theta} \mu_{\theta}(s_i)\; \nabla_{a} Q_{\phi}(s_i, a)\big|_{a=\mu_{\theta}(s_i)}$$通常通过最小化:
$$L_\pi = -\frac{1}{N} \sum_i Q_{\phi}\big(s_i, \mu_{\theta}(s_i)\big)$$优化:
$$\theta \leftarrow \theta - \eta_\pi \, \nabla_{\theta} L_\pi$$目标网络软更新
$$\theta' \leftarrow \tau\,\theta + (1-\tau)\,\theta',\quad \phi' \leftarrow \tau\,\phi + (1-\tau)\,\phi'$$重复直到收敛
定期在无噪声条件下评估策略性能。
八、DDPG 伪代码
# DDPG 算法伪代码
Initialize Actor μ_θ, Critic Q_φ
Initialize target networks μ_θ' ← μ_θ, Q_φ' ← Q_φ
Initialize replay buffer D
Set hyperparameters: γ, τ, σ, batch_size, max_episodes
for episode = 1 to max_episodes:
s ← env.reset()
done ← False
while not done:
# 1. 选择动作(带探索噪声)
a ← clip(μ_θ(s) + ε), ε ∼ N(0, σ²)
# 2. 执行动作
s', r, done ← env.step(a)
# 3. 存储经验
D.store((s, a, r, s', done))
# 4. 采样批次训练
if |D| ≥ batch_size:
batch ← D.sample(batch_size)
# 5. 计算 Critic 目标
for (s_i, a_i, r_i, s'_i, d_i) in batch:
y_i ← r_i + γ(1 - d_i) * Q_φ'(s'_i, μ_θ'(s'_i))
# 6. 更新 Critic
L_Q ← mean((Q_φ(s_i, a_i) - y_i)²)
φ ← φ - η_Q * ∇_φ L_Q
# 7. 更新 Actor
L_π ← -mean(Q_φ(s_i, μ_θ(s_i)))
θ ← θ - η_π * ∇_θ L_π
# 8. 软更新目标网络
θ' ← τθ + (1-τ)θ'
φ' ← τφ + (1-τ)φ'
s ← s'
九、直觉总结
在 AC 中:
- Actor 学习一个分布,Critic 评估动作的好坏。
- Actor 更新依赖 $\log \pi$,存在采样方差。
在 DPG 中:
- Actor 直接给出动作,Critic 通过 $Q(s,a)$ 曲面告诉 Actor 哪个方向更好。
- Actor 顺着 $\nabla_a Q$ 的上升方向更新,像是在爬价值函数的"山丘"。
一句话记住:
AC 在掷骰子学策略;
DPG 直接爬山找最优;
两者的区别,就是从"期望意义的随机上升"变为"确定性方向的精确上升"。
十、常见问题与扩展
| 问题 | 解决方式 |
|---|---|
| 过估计 Q | 用 TD3:双 Q 网络取最小值 |
| 探索不足 | 调整噪声强度或使用参数噪声 |
| 发散 | 降低学习率、使用目标平滑 |
| 训练慢 | 增大批大小、归一化状态/奖励 |
十一、总结
| 模块 | 变化要点 |
|---|---|
| 策略形式 | 从随机分布 → 确定性函数 |
| 学习信号 | 从 log π → 链式法则 (μ, Q) |
| 稳定性来源 | 从 Advantage → 目标网络/回放池 |
| 数据类型 | 从 on-policy → off-policy |
| 核心优点 | 低方差、高数据效率、适合连续控制 |
| 理论支撑 | Deterministic Policy Gradient Theorem (Silver et al., 2014) |
结语
DPG 是"从随机到确定"的重要桥梁。它让强化学习在连续控制领域拥有了实用可行的路径,并成为后续 DDPG、TD3、SAC 等算法的理论基础。
第六章:TD3(Twin Delayed DDPG)
0. 一句话与定位
TD3 = DDPG 的三件套稳态升级:
- Clipped Double Q(双 Q 取最小):抑制 Q 的系统性过估计
- Delayed Policy Update(延迟策略更新):先稳 Critic 再动 Actor
- Target Policy Smoothing(目标策略平滑):让 TD 目标对尖锐 Q 峰不敏感
在连续控制任务(MuJoCo、机械臂等)里,TD3 通常比 DDPG 明显更稳、更高效。
1. 背景:为什么需要 TD3?
1.1 DDPG 的三大痛点
- 过估计偏差:单 Critic + “最大化 Q” 的结构,会把噪声当优势放大,长期偏乐观。
- 策略—值函数步调失衡:Critic 还没学稳,Actor 就跟着不稳定的信号移动,易发散。
- 目标值对动作微扰极敏感:目标用 Q(s', μ'(s')),在尖峰处对微小动作变化剧烈,过拟合“脆弱峰”。
1.2 TD3 的三剂药
- 双 Q 取最小:用两套独立的目标 Q,TD 目标里取 min(Q1', Q2'),数值上“向下裁剪”掉高估。
- 延迟策略更新:Critic 每步都更;Actor/目标网每 d 步(常为 2)才更,避免策略追逐未收敛的 Q。
- 目标平滑:在目标端给 μ'(s') 加小高斯噪声并裁剪,再送入 Q',降低对尖锐峰值的依赖。
2. 算法细节与公式
2.1 组件
- Actor(确定性策略):a = μθ(s),输出缩放到动作边界。
- 两个 Critic:Qϕ1(s,a), Qϕ2(s,a);以及各自的目标网络 Qϕ1', Qϕ2'。
- 目标 Actor:μθ'。
- 回放池 D 存 (s,a,r,s',d)。
2.2 行为策略(探索)
训练交互时执行:
$$a_t = \operatorname{clip}\big(\mu_{\theta}(s_t) + \epsilon_t,\; \text{bounds}\big),\quad \epsilon_t \sim \mathcal{N}(0,\sigma_{\text{explore}}^2 I)$$评估/测试时去掉噪声,只用 μθ(s)。
2.3 TD 目标(核心三件套)
目标动作平滑:
$$\tilde a' = \operatorname{clip}\big(\mu_{\theta'}(s') + \epsilon,\; \text{bounds}\big),\quad \epsilon \sim \operatorname{clip}\big(\mathcal{N}(0,\sigma_{\text{targ}}^2),\,-c,\,c\big)$$Clipped Double Q:
$$y = r + \gamma(1-d)\, \min\!\big(Q_{\phi_1'}(s', \tilde a'),\; Q_{\phi_2'}(s', \tilde a')\big)$$Critic 损失:
$$L_{Q_j} = \frac{1}{N} \sum_i \Big(Q_{\phi_j}(s_i, a_i) - y_i\Big)^2,\quad j\in\{1,2\}$$Actor 损失(延迟更新):
$$L_\pi = -\frac{1}{N} \sum_i Q_{\phi_1}(s_i, \mu_{\theta}(s_i))$$软更新目标网络(与 Actor 同步延迟):
$$\theta' \leftarrow \tau\,\theta + (1-\tau)\,\theta',\quad \phi_j' \leftarrow \tau\,\phi_j + (1-\tau)\,\phi_j'$$3. 完整训练流程(工程化笔记)
- 初始化:随机初始化 μθ, Qϕ1, Qϕ2;复制到目标网 θ', ϕj';建回放池 D;设超参 γ, τ, σ_explore, σ_targ, c, d。
- 交互 & 存储:用 a_t = clip(μθ(s_t) + ϵ_t) 与环境交互,存 (s_t, a_t, r_t, s_{t+1}, d_t) 到 D。
- 采样批次:从 D 均匀采样 N 条样本。
- 目标动作平滑 & 构造 TD 目标:按 2.3。
- 更新两个 Critic:最小化 L_Q1, L_Q2。
- 每隔 d 步:更新 Actor(最大化 Q1);软更新 θ', ϕ1', ϕ2'。
- 评估:定期在无噪声条件下运行若干回合求平均回报、保存最佳模型。
- 循环至收敛。
4. TD3 伪代码(高密度工程版)
# TD3 算法伪代码
init μθ, Qφ1, Qφ2; targets μθ', Qφ1', Qφ2'; replay D
for t in 1..T:
a = clip( μθ(s) + N(0, σ_explore^2), bounds )
s', r, done = env.step(a)
replay.add(s,a,r,s',d)
s ← (s' if not d else reset)
if timestep > warmup:
batch = replay.sample(batch_size)
# --- Critic update ---
with torch.no_grad():
a2, logp2 = policy.sample_with_logp(batch.s2)
target_q = r + gamma * (1 - done) * (
torch.min(q1_target(batch.s2,a2), q2_target(batch.s2,a2)) - alpha * logp2
)
q1_loss = mse(q1(batch.s,a), target_q)
q2_loss = mse(q2(batch.s,a), target_q)
opt_q.zero_grad(); (q1_loss+q2_loss).backward(); opt_q.step()
# --- Actor update ---
a_pi, logp = policy.sample_with_logp(batch.s)
q_pi = torch.min(q1(batch.s,a_pi), q2(batch.s,a_pi))
pi_loss = (alpha * logp - q_pi).mean()
opt_pi.zero_grad(); pi_loss.backward(); opt_pi.step()
# --- Alpha update ---
alpha_loss = -(alpha_log * (logp + target_entropy).detach()).mean()
opt_alpha.zero_grad(); alpha_loss.backward(); opt_alpha.step()
alpha = alpha_log.exp()
# --- Soft update ---
for p, tp in zip(q1.parameters(), q1_target.parameters()):
tp.data.copy_(tau * p.data + (1 - tau) * tp.data)
5. 总结与工程建议
- TD3 的三大技巧本质都是“稳住Critic,慢慢动Actor”,让策略更新更可靠。
- 实际用 TD3,建议先用默认参数,优先关注 Q 估计和策略收敛曲线。
- 如遇训练不稳定,优先检查噪声、目标平滑、延迟步数等超参。
- TD3 适合大多数连续控制任务,是 DDPG 的工程升级版。
第七章:TRPO(Trust Region Policy Optimization)
广义优势估计(GAE)简介
在策略梯度方法中,准确且稳定地估计优势函数 $A_t$ 是关键。广义优势估计(Generalized Advantage Estimation, GAE) 提供了一种在偏差与方差之间进行平衡的有效方法,广泛用于 TRPO、PPO、A2C/A3C 及其它 actor-critic 变体。
GAE 本质上就是: “把多步 TD 误差按照衰减权重混合在一起” → 得到一个既不太偏,也不太吵的 Advantage。
下面是对 GAE 的详细说明:
1. TD(0):一步 TD
最经典的 TD(0):
$$A_t = r_t + \gamma V(s_{t+1}) - V(s_t)$$
优点:计算快,方差低。
缺点:偏差非常大(太短视),基本用不了,高难度任务会崩。
2. MC(蒙特卡洛):
Monte Carlo Advantage:
$$A_t = \sum_{l=0}^{\infty} \gamma^l r_{t+l} - V(s_t)$$
优点:无偏(最准确)。
缺点:方差大到离谱 → 大多数 RL 会发散。
3. GAE:介于 TD 和 MC 之间的“折中版本”
GAE 定义:
$$A_t = \sum_{l=0}^{\infty} (\gamma\lambda)^l \delta_{t+l}$$
也就是:用多步 TD,但不是平均,而是用 $\gamma\lambda$ 逐步下降的权重混合所有未来 TD。
最终效果:像一个“偏差-方差平衡器”。
4. 结论:“优势函数比较适中一点”
这是对的,而且非常准确。
GAE 的 Advantage: 不像 TD 那样太短视(偏差大),不像 MC 那样方差巨高(发散),不像 n-step 那样强依赖固定步长,不会突然震荡。
所以 PPO、MAPPO 训练才会稳定。
5. 用一句“程序员能立刻懂”的话说明:
GAE = 把 $\delta$(TD-error)递推加权平滑一下,得到既不吵又不偏的 Advantage。
gae = 0
for t in reversed(range(T)):
delta = r[t] + γ * V[t+1] - V[t]
gae = delta + γ * λ * gae
A[t] = gae
这就是:一步 TD:$\delta_t$,二步 TD:$\gamma\lambda \delta_{t+1}$,三步 TD:$\gamma^2\lambda^2 \delta_{t+2}$,全部混合 → 得到一个“非常适中”的 Advantage。
6. 为什么 PPO / MAPPO 必须用 GAE?
因为:PPO 非常依赖 Advantage 的稳定性。
Advantage 如果波动太大 → KL 会爆裂。
Advantage 如果偏差太多 → 策略会越学越差。
Multi-Agent 更需要稳定(你的任务 3v1 尤其如此)。
GAE 是唯一同时满足:低方差、可控偏差、可递推计算、稳定性极强的 Advantage 方法。
7. 最终总结:
GAE 就是“多步 TD-error 的指数加权混合”。 它本质是一步步 TD 的扩展版。 得到的 Advantage 比 TD 更准,比 MC 更稳,所以是 PPO/MAPPO 的标准做法。
你已经抓住要点:GAE = 适中的 Advantage。
在实践中,GAE 通常对优势做归一化后用于策略更新。
一、为什么需要 TRPO?
策略梯度方法在参数空间上做更新时,网络的非线性可能导致一次较大的更新使策略性能骤降(policy collapse)。 TRPO 的目标是保证每次更新都能改善策略性能,同时限制策略分布的变化幅度,做到“稳步改进”。
二、核心思想与目标
TRPO 基于 Performance Difference Lemma,使用替代目标(surrogate objective)并对策略变化施加 KL 约束:
$$L(\theta)=\mathbb{E}_{s,a \sim \pi_{\text{old}}}\big[ r_{\theta}(s,a) A_{\pi_{\text{old}}}(s,a)\big],\quad r_{\theta}(s,a)=\frac{\pi_{\theta}(a|s)}{\pi_{\text{old}}(a|s)}\,.$$
约束形式为期望 KL 散度不超过阈值:
$$\mathbb{E}_{s\sim d_{\pi_{\text{old}}}}\big[ D_{KL}(\pi_{\text{old}}(\cdot|s)\|\pi_{\theta}(\cdot|s))\big] \le \delta.$$
三、解析解与自然梯度
在参数微小变化假设下,对 KL 做二阶泰勒展开可得约束近似为二次形式:
$$\bar{D}_{KL}(\pi_{\text{old}},\pi_{\theta}) \approx \tfrac{1}{2}(\theta-\theta_{\text{old}})^T F (\theta-\theta_{\text{old}}),$$
其中 $F$ 为 Fisher 信息矩阵。最大化线性近似目标并满足二次约束,得到方向为自然梯度:
$$\Delta\theta \propto F^{-1} g,\quad g=\nabla_\theta L(\theta)\big|_{\theta_{\text{old}}}.$$
四、数值实现要点
- 共轭梯度(Conjugate Gradient, CG):用于近似求解 $F^{-1}g$,避免显式构造 $F$。
- Fisher 向量积(FVP):用 Pearlmutter trick 计算 $Fv$(向量乘积),仅需两次前向/反向传播。
- 回溯线搜索(Backtracking Line Search):沿自然梯度方向缩放步长,直到 surrogate 目标增加且实际 KL 未超阈值。
五、算法伪代码
# TRPO 高层伪代码
for iter = 1..N:
# 1) 收集样本
collect trajectories using \pi_{old}
# 2) 估计优势 A (通常用 GAE)
compute \hat{A}_t
# 3) 计算梯度 g = \nabla_\theta L
g = policy_gradient_surrogate(\hat{A})
# 4) 用共轭梯度求解 F x = g 的近似解 x ≈ F^{-1} g
x = conjugate_gradient(Fvp, g)
# 5) 计算步长系数
stepdir = x
shs = 0.5 * stepdir.dot(Fvp(stepdir))
max_step = sqrt(2 * delta / (shs + 1e-8))
fullstep = max_step * stepdir
# 6) 回溯线搜索,直到满足 surrogate 提升且 KL 限制
theta_new = line_search(fullstep)
update policy with theta_new
# 7) 更新 value 函数
fit value function to returns
六、TRPO 算法流程(逐步)
下面给出一份便于工程实现的逐步流程,便于把理论落地为可运行代码:
- 采样:用当前策略 \(\pi_{\text{old}}\) 与环境交互,收集一批 on-policy 轨迹(例如若干条完整或固定长度的轨迹)。
- 优势估计:用 GAE 计算优势 \(\hat{A}_t\),并对其进行归一化(减均值,除以标准差)。
- 构造替代目标并计算梯度:计算 surrogate objective 的梯度 \(g=\nabla_\theta L\)。
- 实现 FVP(Fisher 向量积):定义一个函数能在不显式构造 Fisher 矩阵的情况下计算 \(Fv\)(使用 Pearlmutter 技巧)。
- 共轭梯度求解:用共轭梯度方法求解线性方程组 \(F x = g\),得到近似方向 \(x\approx F^{-1}g\)。
- 计算步长:按二次约束计算步长上限 $$\alpha_{max}=\sqrt{\dfrac{2\delta}{x^T F x + 1e-8}}\,, $$ 并得到候选步长向量 \(\Delta\theta = \alpha_{max} x\)。
- 回溯线搜索:沿方向 \(\Delta\theta\) 做回溯(如乘以 0.5^k),直到满足 surrogate 目标提高且平均 KL 不超过阈值 \(\delta\)。若多次回退仍不满足,则放弃本次更新。
- 更新策略:将通过线搜索得到的参数作为新策略权重 \(\theta_{new}\)。
- 更新值函数:用回归方法(例如 MSE)拟合值函数参数,使其逼近目标回报。
- 循环:返回第 1 步,继续下一次迭代。
工程提示: 1) 共轭梯度迭代次数通常取 10~20 次;2) FVP 中加入阻尼(damping)项提高稳定性;3) 采样批量通常较大(几千步或更多),以减小梯度/KL 的噪声。
七、关键实现细节与工程建议
- 批量大小与样本质量:TRPO 对样本质量敏感,通常需要较大的 on-policy batch(例如几千到上万步)。
- FVP 的实现:在框架(PyTorch/TF)中用 Pearlmutter 技巧实现高效的 Fisher 向量积。
- 线搜索容忍度:KL 的估计是有噪声的,允许少量的冗余(例如将阈值乘以 0.9)以避免频繁回退。
- 数值稳定性:共轭梯度与 FVP 中加入小的阻尼项(damping)可以提高鲁棒性。
- 值函数与优势归一化:对 \hat{A} 做标准化(减均值除以标准差)通常能显著稳定训练。
七、与 PPO 的对比
TRPO 提供了严格的理论保证,但工程实现复杂且计算开销大; PPO 用一阶方法和 clip 替代二阶求解,保留了限制更新的核心思想,同时在实践中更易调优。
八、常见问题与调试建议
- 若训练不稳定,先检查 Advantage 的计算与归一化;
- 若 KL 总是超阈值,适当减小目标 delta 或增加共轭梯度的迭代次数以获得更精确方向;
- 若收敛缓慢,增大采样步数或改进基准网络结构。
参考:Schulman et al., "Trust Region Policy Optimization", ICML 2015。
第八章:PPO 全解析——让策略优化又稳又简单
“TRPO 给了我们理论上的安全感,PPO 把它变成了能跑在显卡上的现实。”
核心洞察:PPO 是"小范围 Off-Policy"算法
虽然 PPO 通常被归类为 on-policy 算法,但严格来说,它是一种 "局部 off-policy(small off-policy)" 策略优化方法。
为什么这么说?
在一次训练循环中:
- 我们用当前策略 $\pi_{\text{old}}$ 与环境交互,收集一批轨迹;
- 然后固定这些数据,在此基础上多轮更新新策略 $\pi_{\theta}$。
这意味着在优化时:$\pi_{\theta} \neq \pi_{\text{old}}$,因此当前的优化步骤严格来说已经是 off-policy 更新。
但 PPO 通过重要性比率 $r_{\theta}=\frac{\pi_{\theta}(a|s)}{\pi_{\text{old}}(a|s)}$ 以及剪切约束 $\text{clip}(r_{\theta},1-\epsilon,1+\epsilon)$, 强行限制新旧策略差异在一个局部信赖域(trust region)内。
于是:
- PPO 的更新可以被理解为一次 "小范围 off-policy、全局近似 on-policy" 的优化过程;
- 它既允许一定程度的数据复用,又避免了 off-policy 方法常见的分布偏移问题;
- 这也是 PPO 之所以能在"稳定性与效率之间"取得极佳平衡的原因。
一、从策略梯度说起
策略梯度方法的目标是直接优化参数化策略 $\pi_{\theta}(a|s)$,最大化期望回报:
$$J(\theta)=\mathbb{E}_{\pi_{\theta}}\Big[\sum_{t} \gamma^{t} r_t\Big].$$
基本的梯度估计为:
$$\nabla_{\theta} J(\theta)=\mathbb{E}\big[\nabla_{\theta} \log \pi_{\theta}(a|s)\; A^{\pi}(s,a)\big],$$
其中 $A^{\pi}(s,a)$ 是优势函数,表示某个动作相比平均决策的增益。
二、从 TRPO 到 PPO 的动机
TRPO 通过对平均 KL 散度施加约束,保证策略不“跳得太远”。然而 TRPO 的实现依赖二阶信息(Fisher 矩阵)、共轭梯度与线搜索,工程上复杂且计算开销大。
PPO 的设计原则是保留 TRPO 的“限制更新幅度”思想,但用简单可靠的一阶方法替代复杂的二阶步骤,从而易实现且高效。
核心目标函数:
$$L_{\text{CLIP}}(\theta)=\mathbb{E}_t\Big[\min\big(r_{\theta}(t)\hat{A}_t,\; \text{clip}(r_{\theta}(t),1-\epsilon,1+\epsilon)\hat{A}_t\big)\Big],$$
其中 $r_{\theta}(t)=\dfrac{\pi_{\theta}(a_t|s_t)}{\pi_{\text{old}}(a_t|s_t)}$ 用于修正采样分布的差异,$\hat{A}_t$ 为优势函数(通常使用 GAE 计算)。
直觉解释:
- 如果 $A>0$:说明这个动作比平均好,希望 $r_{\theta}>1$(放大概率);但若 $r_{\theta}$ 太大,就被 clip 限制在 $1+\epsilon$。
- 如果 $A<0$:说明这个动作不好,策略应减少概率;若 $r_{\theta}$ 太小,同样被 clip 限制。
这样一来:PPO 在"希望更新的方向"上前进,但不会走太远。clip 就像给策略加了一个安全带。
四、完整训练目标(策略 + 值函数 + 熵正则)
在实际训练时,PPO 的最终优化目标包含三部分:
$$L(\theta,\psi)=\underbrace{L_{\text{CLIP}}(\theta)}_{\text{策略目标}}-c_v\underbrace{\mathbb{E}\big[(V_{\psi}(s)-\hat{R})^2\big]}_{\text{值函数回归}}+c_e\underbrace{\mathbb{E}\big[\mathcal{H}(\pi_{\theta}(\cdot|s))\big]}_{\text{熵奖励}}.$$
各部分作用:
| 部分 | 作用 |
|---|---|
| 策略项 $L_{\text{CLIP}}$ | 主目标:稳定地提高期望回报 |
| 值函数项 $L_V$ | 提供更稳定的 Advantage 估计,减少方差 |
| 熵正则项 $\mathcal{H}$ | 保持探索性,防止策略塌缩 |
这样可以共享同一网络骨干(特征层),一次反向传播同时更新策略与价值网络。
五、完整算法流程详解
Step 1 — 采样阶段
用当前策略 $\pi_{\text{old}}$ 与环境交互,采集 T 条轨迹数据:
$$\mathcal{D}=\{(s_t,a_t,r_t,s_{t+1},d_t,\log\pi_{\text{old}}(a_t|s_t))\}_{t=1}^T$$
行为策略通常为:$a_t\sim\pi_{\text{old}}(a_t|s_t)$。若为连续动作,则动作分布为高斯分布 $\mathcal{N}(\mu_{\theta}(s),\sigma_{\theta}(s))$。
Step 2 — 优势估计(GAE)
采用 GAE(Generalized Advantage Estimation) 计算优势函数:
- 计算 TD 残差:
$$\delta_t=r_t+\gamma(1-d_t)V_{\psi}(s_{t+1})-V_{\psi}(s_t)$$
- 向后递推得到优势:
$$\hat{A}_t=\delta_t+\gamma\lambda(1-d_t)\hat{A}_{t+1}$$
- 计算目标回报:
$$\hat{R}_t=\hat{A}_t+V_{\psi}(s_t)$$
- 对优势做归一化处理:
$$\hat{A}_t \leftarrow \frac{\hat{A}_t-\text{mean}(\hat{A})}{\text{std}(\hat{A})+10^{-8}}$$
Step 3 — 策略与价值函数更新
对每个小批量数据(minibatch)计算以下损失函数:
① 策略损失(剪切形式)
$$L_{\text{CLIP}}(\theta)=\mathbb{E}\Big[\min\big(r_{\theta}\hat{A},\;\text{clip}(r_{\theta},1-\epsilon,1+\epsilon)\hat{A}\big)\Big]$$
② 值函数损失
$$L_V(\psi)=\frac{1}{2}\mathbb{E}\big[(V_{\psi}(s_t)-\hat{R}_t)^2\big]$$
③ 熵正则
$$L_H(\theta)=\mathbb{E}\big[\mathcal{H}(\pi_{\theta}(\cdot|s_t))\big]$$
④ 总损失
$$L_{\text{total}}=-L_{\text{CLIP}}+c_v L_V-c_e L_H$$
其中 $c_v,c_e$ 分别为值函数和熵正则的权重系数。
Step 4 — 小批量多轮优化
- 将 T 条采样数据打乱成 minibatch(大小通常为 64–256);
- 用 Adam 优化器在这批数据上训练 K 轮(3–10 epochs);
- 若实际 KL 偏差超过阈值(如 0.02),提前停止当前 epoch;
- 完成优化后,更新旧策略:$\pi_{\text{old}}\leftarrow\pi_{\theta}$;
- 然后重新采样新一轮数据。
六、伪代码
Initialize θ, ψ
for iteration = 1, 2, ... do
# Step 1: 采样
Collect {s_t, a_t, r_t, s_{t+1}, done_t, logp_old_t} using π_old
# Step 2: 计算优势和回报
Compute advantages Â_t and targets Ŕ_t via GAE
Normalize Â_t: Â ← (Â - mean(Â)) / (std(Â) + 1e-8)
# Step 3-4: 多轮小批量更新
for epoch in 1..K do
for each minibatch in D do
# 计算比率
Compute ratio r = exp(logπ_θ - logp_old)
# 剪切目标
L_clip = mean(min(r·Â, clip(r,1-ε,1+ε)·Â))
# 值函数损失
L_value = ½(V_ψ(s)-Ŕ)²
# 熵正则
entropy = H(π_θ(·|s))
# 总损失
loss = -L_clip + c_v*L_value - c_e*entropy
# 优化
Update θ, ψ using Adam
# KL 早停
if mean_KL > threshold:
break
# 更新旧策略
π_old ← π_θ
end for
七、关键超参数建议
| 参数 | 含义 | 推荐值 |
|---|---|---|
| γ | 折扣系数 | 0.99 |
| λ | GAE 参数 | 0.95 |
| ε | Clip 范围 | 0.1–0.2 |
| c_v | 值函数系数 | 0.5 |
| c_e | 熵正则系数 | 0.01 (连续动作更大) |
| 学习率 | Adam LR | 3e-4(任务依赖) |
| 批量大小 | 每次更新样本数 | 64–256 |
| T | 每次采样步数 | 2048 或更多 |
| K (Epochs) | 每批数据重复更新次数 | 3–10 |
八、熵正则项要不要保留?
这是一道经典问题。
- 在简单离散任务(如 CartPole)中,可以设为 0;
- 在连续控制任务(MuJoCo、PyBullet)中,强烈建议保留;
- 稀疏奖励任务中(如探索类),甚至需要较大熵系数;
- 常见做法是前期较大,后期逐步衰减(例如从 0.02 → 0)。
熵项的本质是防止策略方差过早收缩,可以理解为"让智能体保留一点点犹豫"。
九、常见问题与工程建议
| 问题 | 原因 | 解决思路 |
|---|---|---|
| 训练发散 | 学习率或 $\epsilon$ 太大 | 减小学习率或 clip 边界 |
| 提升后又退步 | epoch 太多、过拟合当前采样 | 减少 epoch 或监控 KL |
| 策略塌缩 | 熵太低 | 提高 entropy_coef |
| 收敛太慢 | 优势估计偏差大 | 检查 GAE;增加 rollout 步数 |
| 值函数不准 | 学习率过大或未 clip | 开启 value clipping |
十、PPO 与 TRPO 的关系与区别
| 方面 | TRPO | PPO |
|---|---|---|
| 策略约束 | 硬 KL 约束 | 比率剪切 / KL 惩罚 |
| 优化方法 | 二阶(Fisher + 共轭梯度 + 线搜索) | 一阶(SGD / Adam) |
| 理论性质 | 单调改进保证 | 无保证,但经验稳 |
| 实现复杂度 | 高 | 低 |
| 样本利用率 | 低(单次) | 高(多轮复用) |
| 通常表现 | 非常稳定,收敛慢 | 稳定 + 高效,是主流基线 |
PPO 就是 TRPO 的一阶化近似:TRPO 控制 KL;PPO 直接控制比率。两者目标几乎等价,只是 PPO 更便于梯度下降实现。
十一、总结
PPO 是一种 "局部 off-policy、整体近似 on-policy" 的策略优化算法。
它在旧策略数据上做多轮剪切更新,用比率修正轻微分布差异,用 Clip 或 KL 限制策略偏移, 兼顾了样本利用率、训练稳定性与实现简洁性, 成为目前强化学习中最通用、最稳定的 baseline 之一。
十二、工程实践建议
PPO 之所以被广泛采用,不仅因为它在多数基准上表现良好,更重要的是它提供了一条工程可行的路径: 用简单的剪切与多轮小批训练替代复杂的二阶求解,使得策略优化可以直接受益于深度学习现有的一阶优化器与并行化实现。
我的经验是:
- 先用推荐的超参数跑通(例如 Adam lr=3e-4,$\epsilon=0.1$,T=2048,Epochs=4),观察学习曲线;
- 若发散,先调小 lr,其次减小 $\epsilon$;
- 在连续控制任务中保留熵正则并逐步衰减;
- 借助现成实现(OpenAI Spinning Up、Stable-Baselines3)作为基线再做改进;
- 监控关键指标:policy loss、value loss、entropy、approx_kl、clip_fraction。
参考实现:OpenAI Spinning Up、OpenAI 原始实现与 Stable-Baselines3 的 PPO 模块均是很好的工程模板。
第九章:SAC 全解析——最大熵强化学习的黄金标准
Soft Actor-Critic (SAC):最大熵强化学习的黄金标准
在强化学习的世界里,Soft Actor-Critic(SAC) 是目前连续控制领域中最受欢迎、最稳定、也是最通用的算法之一。 它兼具 高样本效率(off-policy)、稳定性强(双 Q 网络 + 目标网络)和 强探索能力(最大熵框架)。
SAC 可以被看作是:
"DDPG + 熵正则化 + 双 Q + 自适应探索"的升级版。
一、从最大期望到最大熵
传统强化学习目标是最大化回报:
$$J(\pi)=\mathbb{E}_{\pi}\Big[\sum_{t} \gamma^{t} r(s_t,a_t)\Big]$$
而 SAC 在此基础上引入了策略熵(entropy),目标变为:
$$J(\pi)=\mathbb{E}_{\pi}\Big[\sum_{t} \gamma^{t} \big(r(s_t,a_t)+\alpha \mathcal{H}(\pi(\cdot|s_t))\big)\Big]$$
其中:
$$\mathcal{H}(\pi(\cdot|s))=-\mathbb{E}_{a\sim\pi}\big[\log \pi(a|s)\big]$$
- α:温度系数,用于平衡"回报"和"熵";
- 熵项:鼓励策略更随机,从而更具探索性。
直观地说,SAC 让智能体既追求高分,也保持"多样化选择",避免陷入局部最优。
二、SAC 与传统 Actor-Critic 的根本区别
很多人第一次看到 SAC 名字时会以为它就是 AC 的一个小变体,但其实它是 off-policy + value-based 的 AC。
| 对比项 | 传统 AC(如 A2C / PPO) | SAC |
|---|---|---|
| 采样方式 | On-policy(每次新数据) | Off-policy(可重用旧数据) |
| 值函数 | V(s) 或 Q(s,a) | 双 Q 网络 |
| 策略类型 | 随机策略(稳定但探索弱) | 最大熵随机策略(探索强) |
| 目标网络 | ❌ 无需 | ✅ 必需,用于稳定 TD 目标 |
| 是否自适应探索 | ❌ 固定 | ✅ α 自动调节探索强度 |
SAC 继承了 DDPG 的 Off-policy 架构(含目标网络),又融合了随机策略和熵最大化,形成一个更鲁棒的框架。
三、算法结构与核心组件
| 模块 | 作用 |
|---|---|
| Actor(策略网络) | 输出高斯分布参数 μ、σ,经 Tanh 压缩采样动作 |
| Critic(Q 网络 ×2) | 估计动作价值,取最小值减少过估计 |
| 目标 Critic 网络(Target Q) | 提供稳定目标值,缓慢更新 |
| 温度参数 α | 自适应调节探索强度 |
SAC 中存在 两套 Critic 网络:
- 在线 Q 网络:$Q_{\theta_1}, Q_{\theta_2}$
- 目标 Q 网络:$Q_{\bar{\theta}_1}, Q_{\bar{\theta}_2}$
目标网络参数来源于在线网络的滑动平均:
$$\bar{\theta}_i \leftarrow \tau \theta_i + (1-\tau) \bar{\theta}_i$$
(一般 τ=0.005)
四、目标网络的由来与必要性
传统 AC(如 PPO、A2C)是 on-policy 算法,用的数据都是当前策略刚采的,不复用旧样本,因此目标计算稳定,不需要目标网络。
但 SAC 是 off-policy:
- 使用经验回放池复用旧数据;
- 当前 Q 网络不断更新;
- 若直接用最新 Q 网络计算 TD 目标,会出现"自我反馈",造成目标漂移。
为避免这种不稳定,SAC 借鉴了 DQN 的做法:
用一个缓慢更新的目标 Q 网络来生成稳定的 TD 目标。
这样一来,Critic 的更新目标更平滑,训练过程更稳定。
五、SAC 的核心优化目标
Critic 更新
目标值:
$$y = r + \gamma \mathbb{E}_{a' \sim \pi_{\phi}}\Big[\min_i Q_{\bar{\theta}_i}(s',a') - \alpha \log \pi_{\phi}(a'|s')\Big]$$
损失函数:
$$L_Q(\theta_i) = \mathbb{E}\Big[\big(Q_{\theta_i}(s,a) - y\big)^2\Big]$$
Actor 更新
$$L_{\pi}(\phi) = \mathbb{E}_{s \sim D, a \sim \pi_{\phi}}\Big[\alpha \log \pi_{\phi}(a|s) - \min_i Q_{\theta_i}(s,a)\Big]$$
目标:提高高 Q 动作的概率,同时保持熵。
温度 α 更新
$$L_{\alpha} = \mathbb{E}_{a \sim \pi_{\phi}}\Big[-\alpha \big(\log \pi_{\phi}(a|s) + H_{\text{target}}\big)\Big]$$
让平均熵接近目标熵,自动调节探索。
六、SAC 完整训练流程
初始化
- 初始化 Actor、Critic、目标 Critic 网络;
- 初始化经验回放池 D;
- 设置目标熵 $H_{\text{target}}$。
主循环
- 从当前策略采样动作 $a_t \sim \pi_{\phi}(a|s_t)$,与环境交互;
- 存储 $(s_t, a_t, r_t, s_{t+1}, \text{done})$;
- 从 D 中采样批量数据;
- 更新 Critic:用目标网络计算目标 $y$,最小化 Q 损失;
- 更新 Actor:通过重参数化技巧采样动作,最小化策略损失;
- 更新温度 α;
- 软更新目标网络:$\bar{\theta}_i \leftarrow \tau \theta_i + (1-\tau) \bar{\theta}_i$。
七、核心实现要点
重参数化技巧
通过
$$a = \tanh\big(\mu_{\phi}(s) + \sigma_{\phi}(s) \odot \epsilon\big), \quad \epsilon \sim \mathcal{N}(0,I)$$
使采样过程可微,从而直接对 Actor 反传梯度。
Tanh 修正 log-prob
因动作经过 Tanh 压缩,需对 $\log \pi(a|s)$ 加雅可比修正项。
双 Q 网络防过估计
使用 $\min(Q_1, Q_2)$ 而非平均。
目标网络稳定更新
使用 Polyak 平滑,避免目标抖动。
八、伪代码(PyTorch 风格)
for each timestep:
a_t = policy.sample_action(s_t)
s2, r, done = env.step(a_t)
replay.add(s_t, a_t, r, s2, done)
s_t = s2
if timestep > warmup:
batch = replay.sample(batch_size)
# --- Critic update ---
with torch.no_grad():
a2, logp2 = policy.sample_with_logp(batch.s2)
target_q = r + gamma * (1 - done) * (
torch.min(q1_target(batch.s2,a2), q2_target(batch.s2,a2)) - alpha * logp2
)
q1_loss = mse(q1(batch.s,a), target_q)
q2_loss = mse(q2(batch.s,a), target_q)
opt_q.zero_grad(); (q1_loss+q2_loss).backward(); opt_q.step()
# --- Actor update ---
a_pi, logp = policy.sample_with_logp(batch.s)
q_pi = torch.min(q1(batch.s,a_pi), q2(batch.s,a_pi))
pi_loss = (alpha * logp - q_pi).mean()
opt_pi.zero_grad(); pi_loss.backward(); opt_pi.step()
# --- Alpha update ---
alpha_loss = -(alpha_log * (logp + target_entropy).detach()).mean()
opt_alpha.zero_grad(); alpha_loss.backward(); opt_alpha.step()
alpha = alpha_log.exp()
# --- Soft update ---
for p, tp in zip(q1.parameters(), q1_target.parameters()):
tp.data.copy_(tau * p.data + (1 - tau) * tp.data)
九、超参数建议
| 参数 | 推荐值 | 说明 |
|---|---|---|
| γ | 0.99 | 折扣因子 |
| τ | 0.005 | 目标网络软更新系数 |
| 批量大小 | 256 | 较大批次更稳定 |
| 学习率 | 3e-4 | 所有网络一致 |
| 目标熵 | $-\text{dim}(A)$ | 通常取动作维度的负值 |
| 回放池大小 | 1e6 | 支持长期经验复用 |
十、为什么 SAC 稳定又强大?
- 最大熵目标 - 自动平衡探索与利用
- 双 Q 网络 - 抑制过估计偏差
- 目标网络 - 平滑更新目标,稳定训练
- Off-policy 架构 - 极高样本效率
- α 自适应调节 - 减少手动调参
十一、算法对比
PPO 限制策略变化(防止太乱);SAC 鼓励策略保持随机性(防止太"死")。
PPO 采用"小心前进"的策略;SAC 采用"广泛探索"的方法。
SAC 的最大熵思想让智能体在不确定性中保持探索性,既不会过于贪婪,也不会过于保守。
十二、总结
Soft Actor-Critic = Off-policy Actor-Critic + 双 Q + 目标网络 + 最大熵探索。
它把稳定性、效率与探索性融合在一起, 成为目前连续控制任务中最强的 RL 算法之一。
第十章:策略梯度增强组合
占位:本章节内容待补充(简要保留标题以保持目录结构)。
进阶总结:从入门到精通的完整路径
本篇涵盖了深度强化学习从价值函数改进到策略优化的完整演进历程,包含十个核心算法的理论分析与实现细节。
一、算法演化地图
价值函数优化分支(第1-4章)
DQN → Dueling DQN → PER → NoisyNet → Rainbow
| 算法 | 核心创新 | 解决问题 | 适用场景 |
|---|---|---|---|
| Dueling DQN | 双流架构 V(s) + A(s,a) | 状态价值估计精度 | 动作价值差异小的环境 |
| PER | 优先经验回放 | 样本利用效率低 | 稀疏奖励、复杂探索 |
| NoisyNet | 参数噪声探索 | ε-贪婪策略局限 | 需要深度探索的任务 |
| Rainbow | 集成多种改进 | 单一技术瓶颈 | 综合基准测试 |
连续控制分支(第5-6章)
DPG → DDPG → TD3
| 算法 | 核心创新 | 解决问题 | 关键技术 |
|---|---|---|---|
| DDPG | 确定性策略梯度 | 连续动作空间 | Actor-Critic + 目标网络 |
| TD3 | 双 Critic + 延迟更新 | 过估计偏差 | 目标策略平滑 + Clipped Q |
策略优化分支(第7-9章)
TRPO → PPO → SAC
| 算法 | 核心创新 | 哲学思路 | 工程特点 |
|---|---|---|---|
| TRPO | 信赖域约束 | "稳步前进,不走弯路" | 理论保证,实现复杂 |
| PPO | 比率剪切 | "小心翼翼,局部off-policy" | 简单高效,广泛应用 |
| SAC | 最大熵框架 | "保持好奇,多走几条路" | 自适应探索,连续控制王者 |
二、算法选择指南
根据任务类型选择
| 任务特征 | 首选算法 | 替代方案 | 理由 |
|---|---|---|---|
| 离散动作 + 稠密奖励 | Rainbow DQN | Dueling DQN + PER | 样本效率高,探索充分 |
| 离散动作 + 稀疏奖励 | PPO + ICM | NoisyNet DQN | 内在动机,参数噪声探索 |
| 连续控制 + 稳定性优先 | PPO | TRPO | 实现简单,训练稳定 |
| 连续控制 + 效率优先 | SAC | TD3 | 样本效率极高,自适应探索 |
| 高维连续控制 | SAC | TD3 | 最大熵防止过早收敛 |
| 实时控制系统 | DDPG / TD3 | SAC | 确定性输出,低延迟 |
根据计算资源选择
| 资源情况 | 推荐算法 | 配置建议 |
|---|---|---|
| GPU充足 | SAC, Rainbow | 大批量,深网络,长训练 |
| GPU有限 | PPO, TD3 | 中等批量,适度网络深度 |
| 仅CPU | PPO | 小批量,浅网络,多进程 |
| 边缘设备 | DQN 变体 | 轻量网络,量化推理 |
三、工程实践路线图
入门阶段(1-3个月)
- 基础实现:从 Dueling DQN 开始,掌握 PyTorch/TensorFlow 基本框架
- 环境熟悉:在 CartPole, LunarLander 等简单环境调试
- 指标监控:学会观察 loss curves, episode rewards, exploration metrics
- 超参调节:理解学习率、batch size、网络结构对性能的影响
进阶阶段(3-6个月)
- 连续控制:掌握 DDPG → TD3 → SAC 的实现与调优
- 复杂环境:在 MuJoCo, PyBullet 等环境验证算法
- 稳定性技巧:目标网络、经验回放、梯度裁剪等工程细节
- 并行化:多进程采样、GPU 加速、分布式训练
精通阶段(6个月+)
- 算法改进:结合具体任务特点,融合多种技术
- 自定义环境:构建真实应用场景,解决实际问题
- 理论深化:理解收敛性、样本复杂度、泛化能力
- 前沿跟踪:关注最新研究,贡献开源社区
四、性能基准与期望
经典环境基准表现
| 环境 | 任务类型 | PPO | SAC | TD3 | Rainbow |
|---|---|---|---|---|---|
| CartPole-v1 | 离散控制 | 500 (稳定) | N/A | N/A | 500 (快速) |
| LunarLander-v2 | 离散控制 | 200+ | N/A | N/A | 250+ |
| HalfCheetah-v3 | 连续控制 | 1000-3000 | 9000-12000 | 8000-10000 | N/A |
| Walker2d-v3 | 连续控制 | 1500-3000 | 4000-5500 | 3500-4500 | N/A |
| Humanoid-v3 | 高维控制 | 1000-2000 | 5000-8000 | 4000-6000 | N/A |
训练效率对比
| 指标 | PPO | SAC | TD3 | TRPO |
|---|---|---|---|---|
| 样本效率 | 中等 | 极高 | 高 | 低 |
| 训练稳定性 | 极高 | 高 | 高 | 极高 |
| 实现复杂度 | 低 | 中等 | 中等 | 高 |
| 调参难度 | 低 | 中等 | 中等 | 高 |
| 收敛速度 | 中等 | 快 | 中等 | 慢 |
五、关键调参经验
通用超参数建议
| 参数类别 | PPO | SAC | TD3 | 经验法则 |
|---|---|---|---|---|
| 学习率 | 3e-4 | 3e-4 | 1e-3 | 连续控制可适当增大 |
| Batch Size | 64-256 | 256-1024 | 256 | Off-policy 可用更大批量 |
| 网络深度 | 2-3层 | 2-4层 | 2-3层 | 避免过深,重点在宽度 |
| Hidden Size | 64-256 | 256-512 | 256-400 | 复杂任务适当增大 |
| Replay Buffer | N/A | 1M | 1M | 内存允许尽量大 |
常见问题诊断
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 训练不收敛 | 学习率过大 / 网络过深 | 降低学习率 / 简化网络 |
| 性能抖动大 | 探索噪声过大 / 批量太小 | 衰减噪声 / 增大batch size |
| 收敛后退步 | 过拟合 / 灾难性遗忘 | 早停 / 经验回放 |
| 探索不充分 | 熵系数过小 / 噪声不足 | 增大熵权重 / 调整噪声策略 |
| 训练速度慢 | 网络过大 / 采样效率低 | 模型压缩 / 并行采样 |
总结
本篇系统地介绍了深度强化学习的核心算法,从 Dueling DQN 的架构创新到 SAC 的最大熵探索,每个算法都针对特定问题提供了有效的解决方案。
掌握强化学习算法的关键在于理解其设计思路和适用场景:
- 目标导向思维 - 明确奖励函数,设计合理目标
- 平衡权衡意识 - 探索与利用,稳定与效率之间的取舍
- 迭代改进精神 - 持续优化算法性能
- 实验验证习惯 - 理论结合实践,用数据验证效果
- 系统性思考 - 综合考虑算法、环境、应用的整体因素
强化学习仍在快速发展中,掌握这些经典算法为后续学习更前沿的技术打下了坚实基础。在实际应用中,选择合适的算法、合理的超参数设置,以及充分的实验验证是成功的关键。