CS285 深度强化学习(六):Value-based 方法——DQN 家族
CS285 深度强化学习(六):Value-based 方法——DQN 家族
0. 写在前面
本篇概要
前面几篇我们深入学习了策略梯度和 Actor-Critic 方法。这一篇我们暂时离开策略优化,转向另一个重要的算法家族:基于价值的方法(Value-based Methods)。
2013 年,DeepMind 的 DQN(Deep Q-Network)在 Atari 游戏上取得了惊人的成功,直接从像素输入学习控制策略,达到甚至超越人类水平。这篇论文被认为是深度强化学习的开山之作,开启了一个新时代。
这一篇我们将学习:
- Q-learning 的基本原理
- DQN 的两个核心技术:经验回放和目标网络
- DQN 的各种改进:Double DQN、Dueling DQN、Prioritized Experience Replay
- HW3 作业解析
系列导航
| 序号 | 标题 | 状态 |
|---|---|---|
| 1 | 开篇:强化学习的世界观 | ✅ 已完成 |
| 2 | 模仿学习:让 AI 学会"抄作业" | ✅ 已完成 |
| 3 | 策略梯度(上):从直觉到公式 | ✅ 已完成 |
| 4 | 策略梯度(下):让训练更稳定 | ✅ 已完成 |
| 5 | Actor-Critic:双剑合璧 | ✅ 已完成 |
| 6 | Value-based 方法:DQN 家族(本文) | 📖 当前 |
| 7 | Model-based RL(上):学会想象 | ⏳ 待更新 |
| … | … | … |
前置知识
- 阅读过本系列第一篇(MDP、贝尔曼方程)
- 了解神经网络和梯度下降
预计阅读时间
约 35 分钟
1. 从 Q-learning 说起
1.1 回顾:Q 函数与最优策略
在第一篇中,我们定义了动作价值函数 Q π ( s , a ) Q^\pi(s, a) Qπ(s,a):在状态 s s s 采取动作 a a a,之后遵循策略 π \pi π 的期望回报。
最优 Q 函数 Q ∗ ( s , a ) Q^*(s, a) Q∗(s,a) 满足贝尔曼最优方程:
Q ∗ ( s , a ) = R ( s , a ) + γ ∑ s ′ P ( s ′ ∣ s , a ) max a ′ Q ∗ ( s ′ , a ′ ) Q^*(s, a) = R(s, a) + \gamma \sum_{s'} P(s'|s, a) \max_{a'} Q^*(s', a') Q∗(s,a)=R(s,a)+γs′∑P(s′∣s,a)a′maxQ∗(s′,a′)
一旦我们知道了 Q ∗ Q^* Q∗,最优策略就是简单地选择 Q 值最大的动作:
π ∗ ( s ) = arg max a Q ∗ ( s , a ) \pi^*(s) = \arg\max_a Q^*(s, a) π∗(s)=argamaxQ∗(s,a)
1.2 表格型 Q-learning
Q-learning 是一种经典的无模型算法,通过迭代更新来逼近最优 Q 函数。
核心更新规则:
Q ( s t , a t ) ← Q ( s t , a t ) + α [ r t + γ max a ′ Q ( s t + 1 , a ′ ) − Q ( s t , a t ) ] Q(s_t, a_t) \leftarrow Q(s_t, a_t) + \alpha \left[ r_t + \gamma \max_{a'} Q(s_{t+1}, a') - Q(s_t, a_t) \right] Q(st,at)←Q(st,at)+α[rt+γa′maxQ(st+1,a′)−Q(st,at)]
其中:
- α \alpha α 是学习率
- r t + γ max a ′ Q ( s t + 1 , a ′ ) r_t + \gamma \max_{a'} Q(s_{t+1}, a') rt+γmaxa′Q(st+1,a′) 是 TD 目标(一步 bootstrap 估计)
- r t + γ max a ′ Q ( s t + 1 , a ′ ) − Q ( s t , a t ) r_t + \gamma \max_{a'} Q(s_{t+1}, a') - Q(s_t, a_t) rt+γmaxa′Q(st+1,a′)−Q(st,at) 是 TD 误差
1.3 Q-learning 的特点
Off-policy:Q-learning 是 off-policy 的,因为它学习的是最优策略 max a Q \max_a Q maxaQ,而不管实际采样时用的是什么策略(行为策略)。
探索策略:通常使用 ε-greedy 策略来采样:
a = { arg max a Q ( s , a ) 以概率 1 − ϵ 随机动作 以概率 ϵ a = \begin{cases} \arg\max_a Q(s, a) & \text{以概率 } 1-\epsilon \\ \text{随机动作} & \text{以概率 } \epsilon \end{cases} a={argmaxaQ(s,a)随机动作以概率 1−ϵ以概率 ϵ
1.4 表格型方法的局限
表格型 Q-learning 需要为每个 ( s , a ) (s, a) (s,a) 对维护一个 Q 值。
问题:当状态空间很大或连续时,这是不可行的。
- Atari 游戏: 210 × 160 × 3 210 \times 160 \times 3 210×160×3 的图像输入,状态数量天文数字
- 机器人控制:连续的关节角度,无限多的状态
解决方案:用函数逼近!用神经网络 Q θ ( s , a ) Q_\theta(s, a) Qθ(s,a) 来近似 Q 函数。
2. DQN:深度 Q 网络
2.1 从表格到神经网络
最直接的想法:用神经网络 Q θ ( s , a ) Q_\theta(s, a) Qθ(s,a) 来代替 Q 表格。
损失函数(基于 TD 误差):
L ( θ ) = E [ ( r + γ max a ′ Q θ ( s ′ , a ′ ) − Q θ ( s , a ) ) 2 ] L(\theta) = \mathbb{E} \left[ \left( r + \gamma \max_{a'} Q_\theta(s', a') - Q_\theta(s, a) \right)^2 \right] L(θ)=E[(r+γa′maxQθ(s′,a′)−Qθ(s,a))2]
直接优化这个损失函数,应该就可以学到好的 Q 函数了吧?
然而,直接这样做会失败! 原因是两个问题:
2.2 问题一:数据相关性
在线采样的连续经验是高度相关的: ( s 1 , a 1 , r 1 , s 2 ) , ( s 2 , a 2 , r 2 , s 3 ) , … (s_1, a_1, r_1, s_2), (s_2, a_2, r_2, s_3), \ldots (s1,a1,r1,s2),(s2,a2,r2,s3),…
相邻的转移来自同一条轨迹,状态非常相似。
为什么有问题?
神经网络的训练假设数据是独立同分布(i.i.d.)的。相关数据会导致:
- 网络过拟合到最近的经验
- 训练不稳定
- 容易"灾难性遗忘"之前学到的东西
2.3 问题二:目标不稳定
注意损失函数中的 TD 目标: y = r + γ max a ′ Q θ ( s ′ , a ′ ) y = r + \gamma \max_{a'} Q_\theta(s', a') y=r+γmaxa′Qθ(s′,a′)
这个目标依赖于当前的网络参数 θ \theta θ!
当我们更新 θ \theta θ 时,目标 y y y 也在变化——我们在追逐一个移动的目标。
这就像是:你在追一个人,但每次你迈出一步,那个人也移动了。你可能永远追不上,或者在原地打转。
2.4 DQN 的两个关键技术
DeepMind 在 2013/2015 年的论文中提出了两个简单但极其有效的技术来解决这些问题:
3. 经验回放(Experience Replay)
3.1 核心思想
经验回放:把交互经验存储在一个回放缓冲区(Replay Buffer) 中,训练时从中随机采样。
3.2 为什么有效?
-
打破相关性:随机采样的 batch 中,经验来自不同的时间点和轨迹,近似 i.i.d.
-
数据复用:每个经验可以被多次使用,提高样本效率
-
平滑分布变化:buffer 中包含旧策略的经验,使训练分布变化更平滑
3.3 实现
import numpy as np
from collections import deque
import random
class ReplayBuffer:
"""经验回放缓冲区"""
def __init__(self, capacity):
self.buffer = deque(maxlen=capacity)
def push(self, state, action, reward, next_state, done):
"""添加一条经验"""
self.buffer.append((state, action, reward, next_state, done))
def sample(self, batch_size):
"""随机采样一个 batch"""
batch = random.sample(self.buffer, batch_size)
states, actions, rewards, next_states, dones = zip(*batch)
return (
np.array(states),
np.array(actions),
np.array(rewards, dtype=np.float32),
np.array(next_states),
np.array(dones, dtype=np.float32)
)
def __len__(self):
return len(self.buffer)
3.4 注意事项
- 缓冲区大小:通常设为 10 5 10^5 105 到 10 6 10^6 106。太小会丢失有价值的经验,太大会占用太多内存
- 最小采样数:训练前需要先收集足够的经验(如 10000 步)
- Off-policy 的必然:使用经验回放意味着训练数据来自旧策略,所以必须使用 off-policy 算法
4. 目标网络(Target Network)
4.1 核心思想
目标网络:维护一个参数固定的网络 Q θ − Q_{\theta^-} Qθ− 来计算 TD 目标,定期从主网络同步参数。
TD 目标变为:
y = r + γ max a ′ Q θ − ( s ′ , a ′ ) y = r + \gamma \max_{a'} Q_{\theta^-}(s', a') y=r+γa′maxQθ−(s′,a′)
注意:目标使用 θ − \theta^- θ−(固定),而不是 θ \theta θ(正在更新)。
4.2 参数更新方式
硬更新(Hard Update):每隔 C C C 步,完全复制参数
θ − ← θ 每 C 步 \theta^- \leftarrow \theta \quad \text{每 } C \text{ 步} θ−←θ每 C 步
软更新(Soft Update):每步进行指数移动平均
θ − ← τ θ + ( 1 − τ ) θ − 每步 \theta^- \leftarrow \tau \theta + (1 - \tau) \theta^- \quad \text{每步} θ−←τθ+(1−τ)θ−每步
其中 τ \tau τ 是一个小的值(如 0.005)。
4.3 为什么有效?
-
目标稳定:在 C C C 步内(或软更新时),目标基本不变,训练更稳定
-
减少震荡:避免 Q 值估计和目标之间的"追逐"
-
类似于监督学习:在短期内,问题变成了一个(近似)固定目标的回归问题
4.4 实现
import copy
class DQN:
def __init__(self, ...):
# 主网络
self.q_network = QNetwork(...)
# 目标网络(初始化为主网络的副本)
self.target_network = copy.deepcopy(self.q_network)
# 冻结目标网络的梯度
for param in self.target_network.parameters():
param.requires_grad = False
def hard_update_target(self):
"""硬更新目标网络"""
self.target_network.load_state_dict(self.q_network.state_dict())
def soft_update_target(self, tau=0.005):
"""软更新目标网络"""
for target_param, param in zip(
self.target_network.parameters(),
self.q_network.parameters()
):
target_param.data.copy_(
tau * param.data + (1 - tau) * target_param.data
)
5. DQN 完整算法
5.1 算法伪代码
算法:DQN (Deep Q-Network)
初始化:
- Q 网络 Q_θ,随机参数 θ
- 目标网络 Q_θ⁻,参数 θ⁻ ← θ
- 回放缓冲区 D,容量 N
- 探索率 ε
for episode = 1 to M:
初始化状态 s₁
for t = 1 to T:
# 1. 选择动作(ε-greedy)
以概率 ε 选择随机动作 a_t
否则 a_t = argmax_a Q_θ(s_t, a)
# 2. 执行动作,观察奖励和下一状态
执行 a_t,得到 r_t, s_{t+1}
# 3. 存储经验
将 (s_t, a_t, r_t, s_{t+1}, done) 存入 D
# 4. 采样并训练
从 D 中随机采样 batch {(s_j, a_j, r_j, s_j', done_j)}
计算目标:
y_j = r_j 如果 done_j
y_j = r_j + γ max_a' Q_θ⁻(s_j', a') 否则
计算损失:
L = (1/batch_size) Σ (y_j - Q_θ(s_j, a_j))²
梯度下降更新 θ
# 5. 更新目标网络
每 C 步:θ⁻ ← θ
5.2 PyTorch 完整实现
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import copy
class QNetwork(nn.Module):
"""Q 网络"""
def __init__(self, obs_dim, act_dim, hidden_dim=256):
super().__init__()
self.net = nn.Sequential(
nn.Linear(obs_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, act_dim)
)
def forward(self, obs):
"""返回所有动作的 Q 值"""
return self.net(obs)
class DQNAgent:
"""DQN 智能体"""
def __init__(
self,
obs_dim,
act_dim,
lr=1e-4,
gamma=0.99,
epsilon_start=1.0,
epsilon_end=0.01,
epsilon_decay=100000,
buffer_size=100000,
batch_size=32,
target_update_freq=1000,
device='cpu'
):
self.act_dim = act_dim
self.gamma = gamma
self.batch_size = batch_size
self.target_update_freq = target_update_freq
self.device = device
# 探索参数
self.epsilon_start = epsilon_start
self.epsilon_end = epsilon_end
self.epsilon_decay = epsilon_decay
# 网络
self.q_network = QNetwork(obs_dim, act_dim).to(device)
self.target_network = copy.deepcopy(self.q_network)
# 冻结目标网络
for param in self.target_network.parameters():
param.requires_grad = False
# 优化器
self.optimizer = optim.Adam(self.q_network.parameters(), lr=lr)
# 回放缓冲区
self.buffer = ReplayBuffer(buffer_size)
# 计数器
self.train_step = 0
def get_epsilon(self, step):
"""计算当前的 ε(线性衰减)"""
epsilon = self.epsilon_end + (self.epsilon_start - self.epsilon_end) * \
max(0, (self.epsilon_decay - step) / self.epsilon_decay)
return epsilon
def select_action(self, obs, step, eval_mode=False):
"""选择动作(ε-greedy)"""
if eval_mode:
epsilon = 0.0
else:
epsilon = self.get_epsilon(step)
if np.random.random() < epsilon:
return np.random.randint(self.act_dim)
else:
with torch.no_grad():
obs_tensor = torch.FloatTensor(obs).unsqueeze(0).to(self.device)
q_values = self.q_network(obs_tensor)
return q_values.argmax(dim=1).item()
def update(self):
"""执行一次训练更新"""
if len(self.buffer) < self.batch_size:
return None
# 采样
states, actions, rewards, next_states, dones = self.buffer.sample(self.batch_size)
states = torch.FloatTensor(states).to(self.device)
actions = torch.LongTensor(actions).to(self.device)
rewards = torch.FloatTensor(rewards).to(self.device)
next_states = torch.FloatTensor(next_states).to(self.device)
dones = torch.FloatTensor(dones).to(self.device)
# 计算当前 Q 值
q_values = self.q_network(states)
q_values = q_values.gather(1, actions.unsqueeze(1)).squeeze(1)
# 计算目标 Q 值
with torch.no_grad():
next_q_values = self.target_network(next_states)
max_next_q = next_q_values.max(dim=1)[0]
targets = rewards + self.gamma * max_next_q * (1 - dones)
# 计算损失
loss = nn.functional.mse_loss(q_values, targets)
# 更新
self.optimizer.zero_grad()
loss.backward()
# 梯度裁剪(可选但推荐)
nn.utils.clip_grad_norm_(self.q_network.parameters(), 10)
self.optimizer.step()
# 更新目标网络
self.train_step += 1
if self.train_step % self.target_update_freq == 0:
self.target_network.load_state_dict(self.q_network.state_dict())
return loss.item()
def train_dqn(env, agent, total_steps=500000, start_learning=10000):
"""训练 DQN"""
episode_rewards = []
episode_reward = 0
obs = env.reset()
for step in range(total_steps):
# 选择并执行动作
action = agent.select_action(obs, step)
next_obs, reward, done, info = env.step(action)
# 存储经验
agent.buffer.push(obs, action, reward, next_obs, done)
episode_reward += reward
if done:
episode_rewards.append(episode_reward)
episode_reward = 0
obs = env.reset()
else:
obs = next_obs
# 训练
if step >= start_learning:
loss = agent.update()
# 打印进度
if step % 10000 == 0 and len(episode_rewards) > 0:
avg_reward = np.mean(episode_rewards[-100:])
epsilon = agent.get_epsilon(step)
print(f"Step {step}, Avg Reward: {avg_reward:.2f}, Epsilon: {epsilon:.3f}")
return episode_rewards
6. DQN 的问题:过估计
6.1 什么是过估计?
DQN 使用 max a ′ Q ( s ′ , a ′ ) \max_{a'} Q(s', a') maxa′Q(s′,a′) 来估计下一状态的价值。
问题:由于神经网络的逼近误差,某些动作的 Q 值可能被高估。而 max \max max 操作会选择性地挑选这些高估的值!
E [ max a Q ( s , a ) ] ≥ max a E [ Q ( s , a ) ] \mathbb{E}[\max_a Q(s, a)] \geq \max_a \mathbb{E}[Q(s, a)] E[amaxQ(s,a)]≥amaxE[Q(s,a)]
随着训练进行,这种过估计会通过 bootstrap 传播和积累。
6.2 过估计的影响
后果:
- 学到次优策略(选择了被高估的动作)
- Q 值膨胀,训练不稳定
- 性能下降
7. Double DQN:解决过估计
7.1 核心思想
Double DQN 的关键洞察:过估计来自于用同一个网络既选择动作又评估价值。
解决方案:解耦选择和评估
- 选择动作:用主网络 Q θ Q_\theta Qθ
- 评估价值:用目标网络 Q θ − Q_{\theta^-} Qθ−
7.2 公式对比
DQN:
y
=
r
+
γ
max
a
′
Q
θ
−
(
s
′
,
a
′
)
y = r + \gamma \max_{a'} Q_{\theta^-}(s', a')
y=r+γa′maxQθ−(s′,a′)
目标网络既选择 a ′ = arg max a' = \arg\max a′=argmax,又评估 Q ( s ′ , a ′ ) Q(s', a') Q(s′,a′)。
Double DQN:
y
=
r
+
γ
Q
θ
−
(
s
′
,
arg
max
a
′
Q
θ
(
s
′
,
a
′
)
)
y = r + \gamma Q_{\theta^-}(s', \arg\max_{a'} Q_\theta(s', a'))
y=r+γQθ−(s′,arga′maxQθ(s′,a′))
主网络选择动作,目标网络评估价值。
7.3 为什么有效?
如果主网络高估了某个动作 a a a,目标网络不一定也高估它。反之亦然。
通过使用两个不同的网络,高估的概率大大降低。
7.4 实现修改
只需要修改目标计算部分:
def compute_double_dqn_target(self, next_states, rewards, dones):
"""计算 Double DQN 目标"""
with torch.no_grad():
# 用主网络选择动作
next_q_values = self.q_network(next_states)
best_actions = next_q_values.argmax(dim=1)
# 用目标网络评估这些动作
next_q_target = self.target_network(next_states)
max_next_q = next_q_target.gather(1, best_actions.unsqueeze(1)).squeeze(1)
targets = rewards + self.gamma * max_next_q * (1 - dones)
return targets
8. Dueling DQN:更好的网络架构
8.1 核心思想
Dueling DQN 提出了一种新的网络架构,将 Q 值分解为两部分:
Q ( s , a ) = V ( s ) + A ( s , a ) Q(s, a) = V(s) + A(s, a) Q(s,a)=V(s)+A(s,a)
其中:
- V ( s ) V(s) V(s):状态价值,“这个状态有多好”
- A ( s , a ) A(s, a) A(s,a):优势函数,“这个动作比平均好多少”
8.2 为什么要分解?
直觉:在很多状态下,动作的选择并不重要(比如游戏中的空闲状态)。这时候学习 V ( s ) V(s) V(s) 比学习每个 Q ( s , a ) Q(s, a) Q(s,a) 更有效。
例子:在 Atari 游戏的某些帧中,不管采取什么动作,结果都差不多。Dueling DQN 可以快速学到这些状态的价值,而不需要尝试每个动作。
8.3 网络架构
8.4 实现细节
直接使用 Q = V + A Q = V + A Q=V+A 有一个问题:给定 Q Q Q, V V V 和 A A A 不唯一(可以给 V V V 加常数, A A A 减同样的常数)。
解决方案:让 A A A 的均值为 0:
Q ( s , a ) = V ( s ) + ( A ( s , a ) − 1 ∣ A ∣ ∑ a ′ A ( s , a ′ ) ) Q(s, a) = V(s) + \left( A(s, a) - \frac{1}{|A|} \sum_{a'} A(s, a') \right) Q(s,a)=V(s)+(A(s,a)−∣A∣1a′∑A(s,a′))
或者使用 max \max max:
Q ( s , a ) = V ( s ) + ( A ( s , a ) − max a ′ A ( s , a ′ ) ) Q(s, a) = V(s) + \left( A(s, a) - \max_{a'} A(s, a') \right) Q(s,a)=V(s)+(A(s,a)−a′maxA(s,a′))
8.5 PyTorch 实现
class DuelingQNetwork(nn.Module):
"""Dueling DQN 网络"""
def __init__(self, obs_dim, act_dim, hidden_dim=256):
super().__init__()
# 共享特征层
self.features = nn.Sequential(
nn.Linear(obs_dim, hidden_dim),
nn.ReLU(),
)
# 价值流
self.value_stream = nn.Sequential(
nn.Linear(hidden_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, 1)
)
# 优势流
self.advantage_stream = nn.Sequential(
nn.Linear(hidden_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, act_dim)
)
def forward(self, obs):
features = self.features(obs)
value = self.value_stream(features) # (batch, 1)
advantage = self.advantage_stream(features) # (batch, act_dim)
# 组合:Q = V + (A - mean(A))
q_values = value + advantage - advantage.mean(dim=1, keepdim=True)
return q_values
9. 优先经验回放(Prioritized Experience Replay)
9.1 核心思想
基本的经验回放均匀随机采样。但直觉上,有些经验比其他经验更"有价值"——那些 TD 误差大的经验。
优先经验回放(PER):根据 TD 误差的大小来决定采样概率,TD 误差大的经验更可能被采样。
9.2 采样概率
经验 i i i 的采样概率:
P ( i ) = p i α ∑ k p k α P(i) = \frac{p_i^\alpha}{\sum_k p_k^\alpha} P(i)=∑kpkαpiα
其中 p i = ∣ δ i ∣ + ϵ p_i = |\delta_i| + \epsilon pi=∣δi∣+ϵ 是优先级(TD 误差的绝对值加一个小常数)。
- α = 0 \alpha = 0 α=0:退化为均匀采样
- α = 1 \alpha = 1 α=1:完全按优先级采样
9.3 重要性采样校正
非均匀采样会引入偏差。需要用重要性采样权重来校正:
w i = ( 1 N ⋅ 1 P ( i ) ) β w_i = \left( \frac{1}{N} \cdot \frac{1}{P(i)} \right)^\beta wi=(N1⋅P(i)1)β
其中 β \beta β 从一个小值(如 0.4)逐渐增加到 1。
更新规则:
L = 1 N ∑ i w i ⋅ ( y i − Q ( s i , a i ) ) 2 L = \frac{1}{N} \sum_i w_i \cdot (y_i - Q(s_i, a_i))^2 L=N1i∑wi⋅(yi−Q(si,ai))2
9.4 实现要点
高效实现 PER 需要使用Sum Tree 数据结构,可以在 O ( log N ) O(\log N) O(logN) 时间内完成采样和更新。
class PrioritizedReplayBuffer:
"""优先经验回放(简化版)"""
def __init__(self, capacity, alpha=0.6):
self.capacity = capacity
self.alpha = alpha
self.buffer = []
self.priorities = np.zeros(capacity)
self.position = 0
def push(self, *experience):
# 新经验给予最高优先级
max_priority = self.priorities.max() if self.buffer else 1.0
if len(self.buffer) < self.capacity:
self.buffer.append(experience)
else:
self.buffer[self.position] = experience
self.priorities[self.position] = max_priority
self.position = (self.position + 1) % self.capacity
def sample(self, batch_size, beta=0.4):
# 计算采样概率
priorities = self.priorities[:len(self.buffer)]
probs = priorities ** self.alpha
probs /= probs.sum()
# 采样
indices = np.random.choice(len(self.buffer), batch_size, p=probs)
experiences = [self.buffer[i] for i in indices]
# 重要性权重
weights = (len(self.buffer) * probs[indices]) ** (-beta)
weights /= weights.max()
return experiences, indices, weights
def update_priorities(self, indices, td_errors):
"""更新优先级"""
for idx, td_error in zip(indices, td_errors):
self.priorities[idx] = abs(td_error) + 1e-6
10. DQN 变体总结
10.1 主要改进对比
| 改进 | 解决的问题 | 核心思想 |
|---|---|---|
| 经验回放 | 数据相关性 | 存储并随机采样经验 |
| 目标网络 | 目标不稳定 | 固定目标网络参数 |
| Double DQN | Q 值过估计 | 解耦动作选择和评估 |
| Dueling DQN | 状态价值学习效率 | 分解 Q 为 V + A |
| PER | 采样效率 | 按 TD 误差优先采样 |
10.2 Rainbow:集大成者
Rainbow DQN(2017)将多种改进组合在一起:
- Double DQN
- Dueling DQN
- Prioritized Experience Replay
- Multi-step Learning
- Distributional RL (C51)
- Noisy Nets
实验表明,这些改进是互补的,组合后效果显著优于单独使用。
11. HW3 作业解析
11.1 作业概述
HW3 要求实现:
- 基本的 DQN
- Double DQN
- 在 Atari 游戏和 LunarLander 等环境上测试
11.2 关键实现点
(1)Atari 预处理
Atari 游戏的输入是原始像素,需要预处理:
import cv2
class AtariPreprocessor:
"""Atari 预处理"""
def __init__(self, frame_size=84):
self.frame_size = frame_size
def process(self, frame):
# 转灰度
gray = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY)
# 缩放
resized = cv2.resize(gray, (self.frame_size, self.frame_size))
# 归一化
normalized = resized / 255.0
return normalized
(2)帧堆叠
单帧图像无法表示运动(比如球的方向),需要堆叠多帧:
from collections import deque
class FrameStack:
"""帧堆叠"""
def __init__(self, n_frames=4):
self.n_frames = n_frames
self.frames = deque(maxlen=n_frames)
def reset(self, frame):
for _ in range(self.n_frames):
self.frames.append(frame)
return self._get_state()
def step(self, frame):
self.frames.append(frame)
return self._get_state()
def _get_state(self):
return np.stack(self.frames, axis=0) # (4, 84, 84)
(3)卷积网络
处理图像输入需要使用 CNN:
class AtariQNetwork(nn.Module):
"""Atari DQN 网络(Nature DQN 架构)"""
def __init__(self, n_actions, n_frames=4):
super().__init__()
self.conv = nn.Sequential(
nn.Conv2d(n_frames, 32, kernel_size=8, stride=4),
nn.ReLU(),
nn.Conv2d(32, 64, kernel_size=4, stride=2),
nn.ReLU(),
nn.Conv2d(64, 64, kernel_size=3, stride=1),
nn.ReLU(),
)
# 计算卷积输出大小
conv_out_size = self._get_conv_out_size((n_frames, 84, 84))
self.fc = nn.Sequential(
nn.Linear(conv_out_size, 512),
nn.ReLU(),
nn.Linear(512, n_actions)
)
def _get_conv_out_size(self, shape):
with torch.no_grad():
dummy = torch.zeros(1, *shape)
return self.conv(dummy).view(1, -1).size(1)
def forward(self, obs):
# obs: (batch, n_frames, 84, 84)
conv_out = self.conv(obs)
conv_out = conv_out.view(conv_out.size(0), -1)
return self.fc(conv_out)
11.3 实验分析
实验 1:DQN vs Double DQN
- 在 LunarLander 上比较学习曲线
- 预期:Double DQN 更稳定,最终性能可能更好
实验 2:目标网络更新频率
- 测试不同的 target_update_freq
- 预期:太频繁会不稳定,太少会学得慢
实验 3:探索策略
- 比较不同的 ε 衰减方案
- 预期:适当的探索对性能很重要
11.4 常见问题
| 问题 | 可能原因 | 解决方法 |
|---|---|---|
| Q 值爆炸 | 没有梯度裁剪 | 添加 clip_grad_norm |
| 性能突然下降 | 探索不足 | 调整 ε 衰减 |
| 训练很慢 | batch_size 太小 | 增加 batch_size |
| 不收敛 | 学习率太大 | 降低学习率 |
12. 论文延伸
核心论文
-
Playing Atari with Deep Reinforcement Learning (Mnih et al., 2013)
- DQN 的首篇论文(NIPS Workshop)
- 开启深度强化学习时代
-
Human-level Control through Deep Reinforcement Learning (Mnih et al., 2015)
- DQN 的 Nature 论文
- 更完整的实验和分析
-
Deep Reinforcement Learning with Double Q-learning (van Hasselt et al., 2016)
- Double DQN 论文
- 分析并解决过估计问题
-
Dueling Network Architectures for Deep Reinforcement Learning (Wang et al., 2016)
- Dueling DQN 论文
- 提出 V + A 分解
-
Rainbow: Combining Improvements in Deep Reinforcement Learning (Hessel et al., 2018)
- Rainbow 论文
- 组合多种改进
推荐阅读
-
Prioritized Experience Replay (Schaul et al., 2016)
- PER 论文
- 优先采样重要经验
-
Noisy Networks for Exploration (Fortunato et al., 2018)
- Noisy Nets 论文
- 参数空间探索
13. 思考题
Q1: Off-policy 的代价
DQN 是 off-policy 算法,可以使用经验回放。但 off-policy 也有代价。思考:
- 为什么 Actor-Critic(如 A2C)不能直接使用经验回放?
- 有什么方法可以让策略梯度方法也用上历史数据?
关键词:重要性采样(Importance Sampling)
策略梯度的期望是在当前策略下计算的,用旧策略的数据需要校正…
Q2: 目标网络的必要性
有人说:既然 Double DQN 用主网络选动作、目标网络评估,那目标网络是不是就不那么必要了?
分析 Double DQN 中目标网络的作用,它和 DQN 中目标网络的作用相同吗?
💡 提示考虑两个独立的问题:
- 目标稳定性(bootstrap 目标变化)
- 过估计(max 操作的偏差)
Double DQN 解决的是哪个问题?
Q3: 连续动作的困难
DQN 只能处理离散动作空间,因为需要计算 max a Q ( s , a ) \max_a Q(s, a) maxaQ(s,a)。
思考:
- 如果动作空间是连续的, max \max max 操作有什么困难?
- 有什么方法可以解决这个问题?(提示:考虑 Actor-Critic)
DDPG(Deep Deterministic Policy Gradient)的思路:
- 用一个 Actor 网络 μ ( s ) \mu(s) μ(s) 直接输出"最优"动作
- max a Q ( s , a ) ≈ Q ( s , μ ( s ) ) \max_a Q(s,a) \approx Q(s, \mu(s)) maxaQ(s,a)≈Q(s,μ(s))
参考资料
- [1] CS 285 Fall 2023 Lecture 7-8: Value Function Methods
- [2] Mnih, V., et al. (2015). Human-level Control through Deep Reinforcement Learning. Nature, 518(7540), 529-533.
- [3] van Hasselt, H., Guez, A., & Silver, D. (2016). Deep Reinforcement Learning with Double Q-learning. AAAI.
- [4] Wang, Z., et al. (2016). Dueling Network Architectures for Deep Reinforcement Learning. ICML.
- [5] Hessel, M., et al. (2018). Rainbow: Combining Improvements in Deep Reinforcement Learning. AAAI.
下一篇预告:CS285 深度强化学习(七):Model-based RL(上)——学会想象
我们将进入另一个重要的领域:基于模型的强化学习。学习如何让智能体"学会想象"——通过学习环境模型来进行规划,大幅提高样本效率。
📝 本文基于 UC Berkeley CS 285 Fall 2023 课程内容整理
如有错误或建议,欢迎在评论区指出!
更多推荐
所有评论(0)