目录
1.2、Dueling DQN:拆分 “环境价值” 和 “动作价值”
1、通俗理解Double DQN和Dueling DQN
1.1、Double DQN:避免 “过度自信” 的选择
1.1.1、传统 DQN 的问题
想象你是一个餐厅评论家,需要预测每家餐厅的 “最佳菜品”。传统 DQN 的做法是:
- 尝遍所有菜后,直接选当前认为最好吃的(
)。
- 但如果某些菜的评分有误差(比如你今天状态不好),你可能会高估某道菜的价值,导致错误选择。
1.1.2、Double DQN 的改进
Double DQN 相当于:
- 先选菜:用 “当前的评分表”(主网络)选出你认为最好吃的菜。
- 再评估:用 “更稳定的历史评分表”(目标网络)来实际打分。
例子: 你先用自己最近的评分表选出 “宫保鸡丁”,然后用餐厅的历史平均分(更可靠)来确定这道菜到底多好吃。这样可以避免因为某次偶然的高分而过度推荐某道菜。
1.2、Dueling DQN:拆分 “环境价值” 和 “动作价值”
1.2.1、传统 DQN 的局限性
假设你在玩《王者荣耀》,传统 DQN 会直接告诉你:“在中路遇到敌人时,选貂蝉能赢”。但它没有解释:
- 是 “中路这个位置本身就容易赢”(环境价值)?
- 还是 “貂蝉在中路比其他英雄更强”(动作价值)?
1.2.2、Dueling DQN 的改进
Dueling DQN 把评分拆成两部分:
- 环境价值 V (s):中路本身的优势(比如兵线好、防御塔多)。
- 动作优势 A (s,a):貂蝉在中路比其他英雄强多少。
公式: 总价值 = 环境基础分 + (英雄优势分 - 所有英雄平均优势) 比如:中路基础分 80 分,貂蝉优势分 + 15 分,所有英雄平均优势 5 分 → 貂蝉总价值 = 80+(15-5)=90 分。
好处:
- 如果中路优势变化(比如防御塔被推了),所有英雄的评分会同时调整,不用重新训练每个英雄。
- 如果貂蝉被削弱,只需要更新她的优势分,不影响其他英雄。
1.3、两者结合:更聪明的决策系统
Double DQN + Dueling DQN 就像是:
- 用 Dueling DQN 的 “环境分 + 动作分” 系统来更细致地评估选择。
- 用 Double DQN 的 “先选后评估” 机制避免过度自信。
生活例子: 你要选一家餐厅吃饭,
Dueling DQN 会告诉你:
- 这家餐厅的环境基础分(位置好、氛围好)。
- 每道菜的优势分(比其他餐厅的同类菜好多少)。
而 Double DQN 会帮你:
- 先用当前的评分选出 “辣子鸡”。
- 再用历史评分(比如朋友的评价)来确认这道菜是否真的好吃。
1.4、总结
- Double DQN:避免 “过度自信”,通过 “先选后评估” 提高稳定性。
- Dueling DQN:拆分 “环境价值” 和 “动作价值”,提高学习效率。
- 结合使用:在复杂环境中更准确、更稳定地学习最优策略。
2、Double DQN(双深度 Q 网络)详解
2.1、 传统 DQN 的问题
传统 DQN 在估计目标 Q 值时使用公式:
这种方式存在最大化偏差(Maximization Bias):当 Q 值估计存在误差时,直接取最大值会系统性地高估真实价值,导致训练不稳定。
2.2、 Double DQN 的核心思想
Double DQN 通过解耦动作选择和动作评估来减少偏差:
- 动作选择:使用主网络(当前策略)选择最优动作。
- 动作评估:使用目标网络评估该动作的 Q 值。
2.3、 目标 Q 值计算
Double DQN 的目标 Q 值公式为:
其中:
:主网络选择的最优动作。
:目标网络对该动作的 Q 值评估。
2.4、 代码实现对比
传统 DQN:
# 传统DQN目标Q值计算
next_q_values = target_network(next_state)
target = reward + gamma * torch.max(next_q_values)
Double DQN:
# Double DQN目标Q值计算
next_actions = main_network(next_state).argmax(dim=1) # 主网络选动作
next_q_values = target_network(next_state) # 目标网络评估Q值
target = reward + gamma * next_q_values.gather(1, next_actions.unsqueeze(1)).squeeze(1)
2.5、 优势
- 显著减少最大化偏差,提高 Q 值估计的准确性。
- 在 Atari 游戏等环境中表现更稳定,收敛性更好。
3、Dueling DQN(对决网络架构)详解
3.1、 传统 DQN 的局限性
传统 DQN 直接输出每个动作的 Q 值,但在许多场景中:
- 状态价值(State Value)对所有动作相同。
- 动作优势(Advantage)体现不同动作的相对价值。
3.2、 Dueling DQN 的核心架构
将 Q 值分解为状态价值函数 V (s)和动作优势函数 A (s,a):
但直接分解会导致参数冗余(V 和 A 不唯一),因此实际使用:
通过减去平均优势,强制唯一解。
3.3、网络结构
- 共享层:提取状态特征。
- 价值流(Value Stream):输出标量 V (s)。
- 优势流(Advantage Stream):输出每个动作的 A (s,a)。
3.4、 代码实现
import torch
import torch.nn as nn
class DuelingDQN(nn.Module):
def __init__(self, state_dim, action_dim):
super(DuelingDQN, self).__init__()
self.feature = nn.Sequential(
nn.Linear(state_dim, 128),
nn.ReLU()
)
self.value_stream = nn.Sequential(
nn.Linear(128, 128),
nn.ReLU(),
nn.Linear(128, 1) # 输出V(s)
)
self.advantage_stream = nn.Sequential(
nn.Linear(128, 128),
nn.ReLU(),
nn.Linear(128, action_dim) # 输出A(s,a)
)
def forward(self, x):
features = self.feature(x)
values = self.value_stream(features)
advantages = self.advantage_stream(features)
q_values = values + (advantages - advantages.mean(dim=1, keepdim=True))
return q_values
3.5、 优势
- 样本效率更高:通过分解,更高效地学习状态价值和动作优势。
- 泛化能力更强:在状态价值变化但动作优势不变的场景中表现更好(如迷宫导航)。
- 训练更稳定:减少了对无关动作的估计误差传播。
4、Double DQN 与 Dueling DQN 结合
两者可结合形成更强的算法(如 DDDQN):
- 使用 Dueling 架构计算 Q 值。
- 在目标 Q 值计算中采用 Double DQN 的解耦策略。
这种结合在许多基准测试中取得了 SOTA 结果,同时解决了高估偏差和样本效率问题。
5、Double DQN实验
5.1、Double DQN实验代码
"""
文件名: 8.1
作者: 墨尘
日期: 2025/7/22
项目名: d2l_learning
备注: 实现DQN和Double DQN算法,解决Pendulum-v1环境(钟摆平衡问题)
"""
# 导入必要的库
import random # 用于随机选择动作(探索行为)
import gym # OpenAI Gym库,提供强化学习环境
import numpy as np # 数值计算库,处理数组和矩阵
import torch # PyTorch深度学习框架,用于构建神经网络
import torch.nn.functional as F # 神经网络功能函数(如激活函数、损失函数)
import matplotlib.pyplot as plt # 绘图库,用于可视化训练结果
import rl_utils # 自定义强化学习工具库(包含经验回放缓冲区等)
from tqdm import tqdm # 进度条库,显示训练进度
class Qnet(torch.nn.Module):
''' 只有一层隐藏层的Q网络(价值网络)'''
def __init__(self, state_dim, hidden_dim, action_dim):
super(Qnet, self).__init__() # 继承父类构造函数
# 第一层全连接层:输入状态维度 -> 隐藏层维度(提取状态特征)
self.fc1 = torch.nn.Linear(state_dim, hidden_dim)
# 第二层全连接层:隐藏层维度 -> 动作维度(输出每个动作的Q值)
self.fc2 = torch.nn.Linear(hidden_dim, action_dim)
def forward(self, x):
''' 前向传播:输入状态,输出所有动作的Q值 '''
x = F.relu(self.fc1(x)) # 隐藏层用ReLU激活函数(增加非线性)
return self.fc2(x) # 输出层直接输出Q值(无激活函数)
class DQN:
''' DQN算法实现,支持基础DQN和Double DQN '''
def __init__(self,
state_dim, # 状态维度(Pendulum环境为3:角度、角速度等)
hidden_dim, # 神经网络隐藏层维度
action_dim, # 动作维度(离散化后的动作数量)
learning_rate, # 学习率(控制参数更新速度)
gamma, # 折扣因子(权衡当前奖励和未来奖励)
epsilon, # 探索率(ε-贪婪策略中的概率)
target_update, # 目标网络更新频率(每多少步更新一次)
device, # 计算设备(CPU或GPU)
dqn_type='VanillaDQN'): # 算法类型(基础DQN或Double DQN)
self.action_dim = action_dim # 动作数量
# 主网络(当前策略网络,实时更新)
self.q_net = Qnet(state_dim, hidden_dim, self.action_dim).to(device)
# 目标网络(稳定目标值,定期更新)
self.target_q_net = Qnet(state_dim, hidden_dim, self.action_dim).to(device)
# 优化器(Adam优化器,用于更新主网络参数)
self.optimizer = torch.optim.Adam(self.q_net.parameters(), lr=learning_rate)
self.gamma = gamma # 折扣因子
self.epsilon = epsilon # 探索率
self.target_update = target_update # 目标网络更新频率
self.count = 0 # 计数器(记录更新步数,用于触发目标网络更新)
self.dqn_type = dqn_type # 算法类型标记
self.device = device # 计算设备
def take_action(self, state):
''' 根据当前状态选择动作(ε-贪婪策略)'''
# 以ε概率随机探索(尝试新动作)
if np.random.random() < self.epsilon:
action = np.random.randint(self.action_dim) # 随机选择一个动作
# 以1-ε概率利用当前策略(选Q值最高的动作)
else:
# 将状态转为张量并传入设备
state = torch.tensor([state], dtype=torch.float).to(self.device)
# 主网络输出所有动作的Q值,选最大Q值对应的动作
action = self.q_net(state).argmax().item()
return action
def max_q_value(self, state):
''' 计算当前状态的最大Q值(用于跟踪训练效果)'''
state = torch.tensor([state], dtype=torch.float).to(self.device)
return self.q_net(state).max().item() # 返回最大Q值
def update(self, transition_dict):
''' 根据经验回放缓冲区的数据更新主网络参数 '''
# 从字典中提取数据并转换为张量,传入计算设备
states = torch.tensor(transition_dict['states'], dtype=torch.float).to(self.device)
actions = torch.tensor(transition_dict['actions']).view(-1, 1).to(self.device) # 动作转为列向量
rewards = torch.tensor(transition_dict['rewards'], dtype=torch.float).view(-1, 1).to(self.device) # 奖励转为列向量
next_states = torch.tensor(transition_dict['next_states'], dtype=torch.float).to(self.device)
dones = torch.tensor(transition_dict['dones'], dtype=torch.float).view(-1, 1).to(self.device) # 终止标志转为列向量
# 计算当前状态-动作对的Q值(从主网络输出中提取对应动作的Q值)
q_values = self.q_net(states).gather(1, actions) # gather函数按动作索引提取Q值
# 计算下一状态的最大Q值(基础DQN与Double DQN的核心区别)
if self.dqn_type == 'DoubleDQN':
# Double DQN:主网络选动作,目标网络评价值(避免高估)
# 1. 主网络选出下一状态的最优动作
max_action = self.q_net(next_states).max(1)[1].view(-1, 1) # 取最大值对应的索引(动作)
# 2. 目标网络对该动作的Q值进行评估
max_next_q_values = self.target_q_net(next_states).gather(1, max_action)
else: # 基础DQN
# 目标网络同时负责选动作和评价值(可能高估Q值)
max_next_q_values = self.target_q_net(next_states).max(1)[0].view(-1, 1) # 直接取最大Q值
# 计算目标Q值(TD目标:当前奖励 + 折扣后的下一状态最大Q值)
# (1 - dones):如果终止,则下一状态无奖励(乘以0)
q_targets = rewards + self.gamma * max_next_q_values * (1 - dones)
# 计算损失(当前Q值与目标Q值的均方误差)
dqn_loss = torch.mean(F.mse_loss(q_values, q_targets))
# 梯度清零(避免累积梯度影响更新)
self.optimizer.zero_grad()
# 反向传播计算梯度
dqn_loss.backward()
# 应用梯度更新主网络参数
self.optimizer.step()
# 每更新target_update步,将主网络参数复制到目标网络
if self.count % self.target_update == 0:
self.target_q_net.load_state_dict(self.q_net.state_dict())
self.count += 1 # 计数器加1
def dis_to_con(discrete_action, env, action_dim):
''' 将离散动作转换为连续动作(因为Pendulum环境需要连续控制力)'''
action_lowbound = env.action_space.low[0] # 连续动作的最小值(Pendulum中为-2)
action_upbound = env.action_space.high[0] # 连续动作的最大值(Pendulum中为2)
# 线性映射:离散动作索引 -> 连续动作值
return action_lowbound + (discrete_action / (action_dim - 1)) * (action_upbound - action_lowbound)
def train_DQN(agent, env, num_episodes, replay_buffer, minimal_size, batch_size):
''' 训练DQN智能体 '''
return_list = [] # 记录每回合的总奖励(用于评估训练效果)
max_q_value_list = [] # 记录每步的最大Q值(用于观察学习进度)
max_q_value = 0 # 最大Q值的平滑值
# 分10轮训练(每轮训练num_episodes/10回合)
for i in range(10):
# 进度条显示当前轮次的训练进度
with tqdm(total=int(num_episodes / 10), desc='Iteration %d' % i) as pbar:
for i_episode in range(int(num_episodes / 10)):
episode_return = 0 # 记录当前回合的总奖励
state, _ = env.reset() # 初始化环境,获取初始状态(忽略info)
done = False # 回合是否结束的标志
while not done: # 循环直到回合结束
# 智能体根据当前状态选择动作
action = agent.take_action(state)
# 计算当前状态的最大Q值(平滑处理,避免波动过大)
max_q_value = agent.max_q_value(state) * 0.005 + max_q_value * 0.995
max_q_value_list.append(max_q_value) # 记录平滑后的Q值
# 将离散动作转换为连续动作(适应环境需求)
action_continuous = dis_to_con(action, env, agent.action_dim)
# 执行动作,获取下一状态、奖励等信息(Gym v0.26+返回5个值)
next_state, reward, terminated, truncated, _ = env.step([action_continuous])
# 合并终止条件:达到目标(terminated)或超时(truncated)
done = terminated or truncated
# 将当前经验(状态、动作、奖励等)存入经验回放缓冲区
replay_buffer.add(state, action, reward, next_state, done)
# 更新当前状态为下一状态
state = next_state
# 累积当前回合的奖励
episode_return += reward
# 当缓冲区数据量超过最小阈值时,开始采样更新网络
if replay_buffer.size() > minimal_size:
# 从缓冲区随机采样一批数据
b_s, b_a, b_r, b_ns, b_d = replay_buffer.sample(batch_size)
# 构造经验字典
transition_dict = {
'states': b_s,
'actions': b_a,
'next_states': b_ns,
'rewards': b_r,
'dones': b_d
}
# 调用智能体的update方法更新网络
agent.update(transition_dict)
# 记录当前回合的总奖励
return_list.append(episode_return)
# 每10回合显示一次平均奖励(评估训练效果)
if (i_episode + 1) % 10 == 0:
pbar.set_postfix({
'episode': '%d' % (num_episodes / 10 * i + i_episode + 1),
'return': '%.3f' % np.mean(return_list[-10:]) # 最近10回合的平均奖励
})
pbar.update(1) # 进度条更新
return return_list, max_q_value_list # 返回所有回合的奖励和Q值记录
if __name__ == '__main__':
# 超参数设置
lr = 1e-2 # 学习率
num_episodes = 200 # 总训练回合数
hidden_dim = 128 # 神经网络隐藏层维度
gamma = 0.98 # 折扣因子
epsilon = 0.01 # 探索率(ε-贪婪策略)
target_update = 50 # 目标网络更新频率(每50步更新一次)
buffer_size = 5000 # 经验回放缓冲区大小
minimal_size = 1000 # 开始更新网络所需的最小缓冲区数据量
batch_size = 64 # 每次更新的样本批次大小
# 选择计算设备(优先GPU,无则用CPU)
device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
# 环境设置(Pendulum-v1为钟摆平衡环境)
env_name = 'Pendulum-v1'
# 创建环境,指定渲染模式(可选,用于可视化)
env = gym.make(env_name, render_mode="rgb_array")
# 初始化环境种子(保证实验可复现)
env.reset(seed=0)
# 获取状态维度(Pendulum环境为3)
state_dim = env.observation_space.shape[0]
# 动作离散化数量(将连续动作分为11个离散选项)
action_dim = 11
# 设置随机种子(保证实验可复现)
random.seed(0)
np.random.seed(0)
torch.manual_seed(0)
# 初始化经验回放缓冲区
replay_buffer = rl_utils.ReplayBuffer(buffer_size)
# 初始化DQN智能体(默认基础DQN,可改为DoubleDQN)
agent = DQN(state_dim, hidden_dim, action_dim, lr, gamma, epsilon, target_update, device)
#agent = DQN(state_dim, hidden_dim, action_dim, lr, gamma, epsilon, target_update, device, 'DoubleDQN')
# 开始训练,获取奖励记录和Q值记录
return_list, max_q_value_list = train_DQN(agent, env, num_episodes, replay_buffer, minimal_size, batch_size)
# 绘制训练奖励曲线(移动平均,平滑波动)
episodes_list = list(range(len(return_list)))
mv_return = rl_utils.moving_average(return_list, 5) # 5回合移动平均
plt.plot(episodes_list, mv_return)
plt.xlabel('Episodes') # 横轴:回合数
plt.ylabel('Returns') # 纵轴:总奖励
plt.title('DQN on {}'.format(env_name)) # 标题:环境名称
plt.show()
# 绘制最大Q值曲线(观察学习进度)
frames_list = list(range(len(max_q_value_list)))
plt.plot(frames_list, max_q_value_list)
plt.axhline(0, c='orange', ls='--') # 参考线:Q值=0
plt.axhline(10, c='red', ls='--') # 参考线:Q值=10
plt.xlabel('Frames') # 横轴:步数
plt.ylabel('Q value') # 纵轴:最大Q值
plt.title('DQN on {}'.format(env_name))
plt.show()