强化学习入门:从零开始实现Dueling DQN
在之前的文章中,已经依次介绍了
- 《深度强化学习入门:从零开始实现DQN》
- 《深度强化学习入门:从零开始实现DDQN》
并通过gymnasium中的 CartPole 环境从零开始实现了智能体的训练与测试。本文将继续这一系列,聚焦于 Dueling DQN(对抗网络结构),它是对 DQN 的进一步改进,主要解决 DQN 在价值估计上的冗余与模糊问题。
一、核心问题:传统 DQN 的价值模糊性
在标准 DQN 中,网络直接输出每个动作的 Q 值,这带来两个问题:
-
状态价值与动作价值混淆
- 例如在自动驾驶场景:
- 状态价值(V):当前道路的安全性(所有动作共享)
- 动作优势(A):左转/右转的额外收益
- DQN 无法区分这两类信息
- 例如在自动驾驶场景:
-
冗余学习
# 当状态价值相同时,DQN 仍需为每个动作重复学习: Q(直行) = 道路安全值 + 直行优势 Q(左转) = 道路安全值 + 左转优势 Q(右转) = 道路安全值 + 右转优势
→ 网络浪费容量学习重复的状态特征
二、Dueling DQN 的提出
Dueling DQN 的核心思想是:将 Q 值分解为状态价值和动作优势两部分。
数学公式:
Q(s,a)=V(s)+(A(s,a)−1∣A∣∑a′A(s,a′))Q(s,a) = V(s) + \Big(A(s,a) - \frac{1}{|\mathcal{A}|}\sum_{a'} A(s,a')\Big) Q(s,a)=V(s)+(A(s,a)−∣A∣1a′∑A(s,a′))
其中:
- V(s)V(s)V(s):状态价值函数(标量)
- A(s,a)A(s,a)A(s,a):动作优势函数(向量)
为什么需要减去均值?
如果直接写成:
Q(s,a)=V(s)+A(s,a)Q(s,a) = V(s) + A(s,a) Q(s,a)=V(s)+A(s,a)
就会导致 VVV 和 AAA 之间存在无穷多解,学习过程不稳定。例如:
- V′(s)=V(s)+cV'(s) = V(s) + cV′(s)=V(s)+c
- A′(s,a)=A(s,a)−cA'(s,a) = A(s,a) - cA′(s,a)=A(s,a)−c
它们可以得到相同的 Q(s,a)Q(s,a)Q(s,a)。
解决办法:在 A(s,a)A(s,a)A(s,a) 上做归一化,减去平均值或最大值。
Dueling dqn的详细内容可以看我之前的文章 深度强化学习Dueling DQN,本文重点分享代码。
代码结构(参考自腾讯开悟平台)
📦 项目根目录
├── 📂 agent_dueling_dqn # Dueling DQN智能体核心模块
│ ├── 📂 algorithm # 算法实现目录
│ └── 📄 __init__.py
│ └── 📄 algorithm.py # 算法核心逻辑,包含经验回放、采样、更新网络等方法
│ ├── 📂 conf # 配置管理目录
│ └── 📄 __init__.py
│ └── 📄 conf.py # 参数配置**文件,集中管理模型结构、训练参数、路径等,便于调参
│ ├── 📂 feature # 特征处理目录
│ └── 📄 __init__.py
│ └── 📄 monitor.py # 训练过程监控模块,负责记录奖励、损失等指标并可实现可视化
│ └── 📄 processor.py # 数据预处理模块,负责对环境状态进行标准化、转换Tensor等操作
│ ├── 📂 model # 神经网络模型目录
│ └── 📄 __init__.py
│ └── 📄 model.py # 网络模型定义文件
│ ├── 📂 workflow # 工作流目录
│ └── 📄 __init__.py
│ └── 📄 train_workflow.py # 训练工作流,封装了与环境交互、训练循环等完整流程
│ ├── 📄 __init__.py
│ └── 📄 agent.py # 智能体接口,提供选择动作、学习、保存/加载模型等功能
├── 📂 env # 环境管理目录
│ ├── 📄 __init__.py
│ └── 📄 envManager.py # 环境管理器,封装Gymnasium环境的创建、重置、步进等操作
└── 📄 train_test.py # 主程序入口,用于启动训练或测试模式的脚本
网络结构实现
相较于标准 DQN,Dueling DQN 只是在网络的最后增加了 两条分支:一个估计 V(s)V(s)V(s),一个估计 A(s,a)A(s,a)A(s,a)。
import torch
import torch.nn as nn
import torch.nn.functional as Fclass DuelingDQN(nn.Module):def __init__(self, state_dim, action_dim):super().__init__()# 共享特征提取层self.feature_layer = nn.Sequential(nn.Linear(state_dim, 128),nn.ReLU(),nn.Linear(128, 128),nn.ReLU())# 状态价值分支 V(s)self.V_branch = nn.Sequential(nn.Linear(128, 64),nn.ReLU(),nn.Linear(64, 1))# 动作优势分支 A(s,a)self.A_branch = nn.Sequential(nn.Linear(128, 64),nn.ReLU(),nn.Linear(64, action_dim))def forward(self, x):features = self.feature_layer(x)V = self.V_branch(features) # [batch_size, 1]A = self.A_branch(features) # [batch_size, action_dim]# 组合 Q 值:V + (A - mean(A))Q = V + (A - A.mean(dim=1, keepdim=True))return Q
只需在原 DQN 框架中替换网络部分,其他流程(经验回放、训练循环、目标网络更新)均保持不变。
完整代码请看这里