一文读懂DQN改进算法(Double DQN+Dueling DQN)—强化学习(7)

发布于:2025-07-23 ⋅ 阅读:(14) ⋅ 点赞:(0)

目录

1、通俗理解Double DQN和Dueling DQN

1.1、Double DQN:避免 “过度自信” 的选择

1.1.1、传统 DQN 的问题

1.1.2、Double DQN 的改进

1.2、Dueling DQN:拆分 “环境价值” 和 “动作价值”

1.2.1、传统 DQN 的局限性

1.2.2、Dueling DQN 的改进

1.3、两者结合:更聪明的决策系统

1.4、总结

2、Double DQN(双深度 Q 网络)详解

2.1、 传统 DQN 的问题

2.2、 Double DQN 的核心思想

2.3、 目标 Q 值计算

2.4、 代码实现对比

2.5、 优势

3、Dueling DQN(对决网络架构)详解

3.1、 传统 DQN 的局限性

3.2、 Dueling DQN 的核心架构

3.3、网络结构

3.4、 代码实现

3.5、 优势

4、Double DQN 与 Dueling DQN 结合

5、Double DQN实验

5.1、Double DQN实验代码

5.2、Double DQN实验对比结果

5.2.1、DQN实验结果

​编辑 5.2.2、Double DQN实验结果

6、Dueling DQN实验

6.1、Dueling DQN实验代码

6.2、Dueling DQN实验结果


1、通俗理解Double DQN和Dueling DQN

1.1、Double DQN:避免 “过度自信” 的选择

1.1.1、传统 DQN 的问题

想象你是一个餐厅评论家,需要预测每家餐厅的 “最佳菜品”。传统 DQN 的做法是:

  • 尝遍所有菜后,直接选当前认为最好吃的(\max Q)。
  • 但如果某些菜的评分有误差(比如你今天状态不好),你可能会高估某道菜的价值,导致错误选择。

1.1.2、Double DQN 的改进

Double DQN 相当于:

  1. 先选菜:用 “当前的评分表”(主网络)选出你认为最好吃的菜。
  2. 再评估:用 “更稳定的历史评分表”(目标网络)来实际打分。

例子你先用自己最近的评分表选出 “宫保鸡丁”,然后用餐厅的历史平均分(更可靠)来确定这道菜到底多好吃。这样可以避免因为某次偶然的高分而过度推荐某道菜。

1.2、Dueling DQN:拆分 “环境价值” 和 “动作价值”

1.2.1、传统 DQN 的局限性

假设你在玩《王者荣耀》,传统 DQN 会直接告诉你:“在中路遇到敌人时,选貂蝉能赢”。但它没有解释:

  • 是 “中路这个位置本身就容易赢”(环境价值)?
  • 还是 “貂蝉在中路比其他英雄更强”(动作价值)?

1.2.2、Dueling DQN 的改进

Dueling DQN 把评分拆成两部分:

  1. 环境价值 V (s):中路本身的优势(比如兵线好、防御塔多)。
  2. 动作优势 A (s,a):貂蝉在中路比其他英雄强多少。

公式 总价值 = 环境基础分 + (英雄优势分 - 所有英雄平均优势) 比如:中路基础分 80 分,貂蝉优势分 + 15 分,所有英雄平均优势 5 分 → 貂蝉总价值 = 80+(15-5)=90 分。

好处

  • 如果中路优势变化(比如防御塔被推了),所有英雄的评分会同时调整,不用重新训练每个英雄。
  • 如果貂蝉被削弱,只需要更新她的优势分,不影响其他英雄。

1.3、两者结合:更聪明的决策系统

Double DQN + Dueling DQN 就像是:

  1. 用 Dueling DQN 的 “环境分 + 动作分” 系统来更细致地评估选择。
  2. 用 Double DQN 的 “先选后评估” 机制避免过度自信。

生活例子: 你要选一家餐厅吃饭,

Dueling DQN 会告诉你:

  • 这家餐厅的环境基础分(位置好、氛围好)。
  • 每道菜的优势分(比其他餐厅的同类菜好多少)。

而 Double DQN 会帮你:

  1. 先用当前的评分选出 “辣子鸡”。
  2. 再用历史评分(比如朋友的评价)来确认这道菜是否真的好吃。

1.4、总结

  • Double DQN:避免 “过度自信”,通过 “先选后评估” 提高稳定性。
  • Dueling DQN:拆分 “环境价值” 和 “动作价值”,提高学习效率。
  • 结合使用:在复杂环境中更准确、更稳定地学习最优策略。

2、Double DQN(双深度 Q 网络)详解

2.1、 传统 DQN 的问题

传统 DQN 在估计目标 Q 值时使用公式:

y_t = r_t + \gamma \max_{a'} Q_{\theta^-}(s_{t+1}, a')

这种方式存在最大化偏差(Maximization Bias)当 Q 值估计存在误差时,直接取最大值会系统性地高估真实价值,导致训练不稳定

2.2、 Double DQN 的核心思想

Double DQN 通过解耦动作选择和动作评估来减少偏差:

  • 动作选择:使用主网络(当前策略)选择最优动作。
  • 动作评估:使用目标网络评估该动作的 Q 值。

2.3、 目标 Q 值计算

Double DQN 的目标 Q 值公式为:

y_t = r_t + \gamma Q_{\theta^-}(s_{t+1}, \arg\max_{a'} Q_\theta(s_{t+1}, a'))

其中:

  • \arg\max_{a'} Q_\theta(s_{t+1}, a'):主网络选择的最优动作。
  • Q_{\theta^-}(s_{t+1}, \cdot):目标网络对该动作的 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)

Q(s,a) = V(s) + A(s,a)

但直接分解会导致参数冗余(V 和 A 不唯一),因此实际使用:

Q(s,a) = V(s) + \left( A(s,a) - \frac{1}{|A|} \sum_{a'} A(s,a') \right)

通过减去平均优势,强制唯一解。

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):

  1. 使用 Dueling 架构计算 Q 值。
  2. 在目标 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()

5.2、Double DQN实验对比结果

5.2.1、DQN实验结果

 5.2.2、Double DQN实验结果

6、Dueling DQN实验

6.1、Dueling DQN实验代码

6.2、Dueling DQN实验结果


网站公告

今日签到

点亮在社区的每一天
去签到