大约在 2017 年前后,深度强化学习在游戏、机器人控制等领域频繁刷榜,DQN 的论文也是那时候开始被反复阅读。很多初学者一开始接触的是“Q-Learning 表格法”,在简单环境里用二维表记录每个状态和动作的价值,效果还行,一旦环境状态变多,比如连续坐标、像素画面,表就存不下了。这时候就需要用神经网络来拟合 Q 值函数,这就是 DQN 的核心思想。
本文是这个强化学习入门系列的第 49 篇,主题是“再聊 Q-Learning”。所谓“再聊”,是把大家熟悉的表格型 Q-Learning 放到深度学习视角下重新审视:表格为什么失效,神经网络如何替代表格,DQN 在实现上解决了哪些 Q-Learning 原生的问题。文章会从核心概念、环境准备、原理拆解、完整可运行的 DQN 实战代码、常见问题、工程建议几个方面展开。适合有一定 Python 基础、了解基本强化学习要素,但还不太清楚 DQN 代码细节的学习者。
1. 背景与核心概念
1.1 强化学习的基本要素回顾
强化学习的任务是让智能体通过与环境交互来学习一种最优策略。一个标准流程是:智能体观察环境状态,选择一个动作,环境返回下一个状态和奖励,循环往复。
这里有几个核心名词需要先统一:
- 状态:智能体当前观察到的环境信息。
- 动作:智能体可以选择的操作。
- 奖励:环境对动作的即时反馈。
- 策略:状态到动作的映射关系。
- 回报:未来所有奖励的折扣累计值。
在 Q-Learning 中,我们直接估计一个函数 Q(s,a),表示在状态 s 下执行动作 a,之后一直采取最优策略所能获得的期望回报。有了这个函数,智能体的决策就变得非常简单:在状态 s 下选择 Q 值最大的动作。
1.2 表格型 Q-Learning 的基本方法
经典的 Q-Learning 使用一张二维表格来存储 Q 值:
- 行代表状态。
- 列代表动作。
- 单元格代表 Q(s,a)。
更新公式是:
Q(s,a) <- Q(s,a) + alpha * (r + gamma * max_a' Q(s',a') - Q(s,a))这个公式中:
- alpha 表示学习率。
- gamma 表示折扣因子。
- r 是当前奖励。
- s' 是下一个状态。
- max_a' Q(s',a') 表示下一状态所有动作中的最大 Q 值。
表格法在小规模离散状态空间里非常好用,比如经典的迷宫问题、悬崖行走问题。状态数量有限,表格能完整记录所有情况,而且更新公式简单,实现难度低。
但表格法有一个天然瓶颈:状态和动作的组合数量一旦变大,表格容量会爆炸。CartPole 这种环境的状态本身就包含车的位置、速度、杆的角度、角速度四个连续值,无法直接枚举;Atari 游戏的画面更是超过 10 万像素,表格连存储都不可能。
1.3 从 Q-Learning 到 DQN 的转变
DNQ,也就是 Deep Q-Network,做的事情是“用一个神经网络来替代那张 Q 值表”。神经网络的输入是状态,输出是每个可能的动作对应的 Q 值。
这种做法有几个明显好处:
- 神经网络可以处理连续状态输入。
- 神经网络可以通过训练自动提取特征,不需要人工设计状态特征。
- 神经网络具备泛化能力,相似的状态会得到相近的 Q 值输出。
但直接用神经网络替换表格并不是那么简单。2013 年 DeepMind 提出 DQN 时,主要解决了两个关键问题:一是经验回放,二是目标网络。这两个机制让神经网络的训练过程变得稳定,也让 DQN 能够在许多高维状态空间中真正跑起来。
2. 环境准备与版本说明
本文的实战以 CartPole 环境为例。CartPole 是强化学习领域最常用的入门环境,目标是通过左右移动小车,让杆保持竖直不倒。
建议的环境和依赖如下:
- 操作系统:Windows / macOS / Linux 均可。
- Python 版本:3.9 及以上。
- 深度学习框架:PyTorch 2.x。
- 环境库:Gymnasium。
- 辅助库:NumPy、Matplotlib。
版本需要根据你的项目实际情况调整。早期版本的 OpenAI Gym 和现在的 Gymnasium 在 API 上有一些差别,最典型的是env.reset()返回值的差异。本文示例以较新的 Gymnasium 接口为准。如果你还在使用旧版 gym,需要把代码中的环境交互部分改成对应格式,后面会在常见问题里说明。
安装命令如下:
pip install torch pip install gymnasium pip install numpy matplotlib如果使用国内镜像源,可以加速安装:
pip install torch -i https://pypi.tuna.tsinghua.edu.cn/simple安装完成后,可以用下面这段代码确认 Gymnasium 能正常创建环境:
import gymnasium as gym env = gym.make("CartPole-v1") state, info = env.reset() print("状态维度:", len(state)) print("动作空间:", env.action_space) print("初始状态:", state) env.close()正常输出大致如下:
状态维度: 4 动作空间: Discrete(2) 初始状态: [-0.01234 0.02103 -0.01903 -0.00221]这里状态是一个长度为 4 的数组,包含小车位置、小车速度、杆角度、杆角速度。动作空间只有两个动作:向左推和向右推。
3. DQN 核心原理拆解
3.1 经验回放:打破样本相关性
深度神经网络训练的前提是样本尽量独立同分布。但强化学习的数据来自连续交互,相邻时间步的状态、动作、奖励高度相关。如果每拿到一条经验就立刻更新一次网络,模型会在最近一段轨迹上反复“偏科”,导致训练不稳定甚至遗忘之前学到的内容。
经验回放的思想是维护一个固定容量的缓冲区,把每次交互得到的五元组存储起来,训练时从这个缓冲区中随机采样一批数据来更新网络。这样做有两个作用:
- 打乱数据之间的相关性,让训练更接近监督学习中的随机梯度下降。
- 提高数据利用率。同一条经验可以多次被采样使用,而不是用一次就丢弃。
缓冲区容量是经验回放的关键参数。容量太小,采样数据仍然偏向近期经验;容量太大,又可能导致大量旧经验占据缓冲区,新经验的占比过低。一般推荐用 10000 到 100000 之间的值,具体需要根据环境和设备内存调整。
3.2 目标网络:稳定学习目标
表格型 Q-Learning 的更新目标就是 Q 值表本身,更新一步,表格里的值变一点,问题不大。但神经网络更新时,如果训练目标也在不断变化,模型就很难收敛。DQN 更新的目标是:
r + gamma * max_a' Q_target(s', a')这里的 Q_target 如果还是同一个网络,那么每次梯度更新都会同时改变目标值,相当于“边追边移动的靶子”,训练容易发散。
DQN 的做法是维护一个与主网络结构相同、初始参数也相同的目标网络。目标网络不参与梯度更新,只用来计算 TD 目标。每隔一定步数,把主网络的参数复制给目标网络。
一个常见困惑是:
- 为什么不能直接让目标网络每步都跟着主网络更新?
- 为什么要隔一段时间才同步一次?
答案很直接:如果每步都同步,目标网络和主网络就完全一样,失去了“固定目标”的意义。隔一段时间同步,能让目标在短期内保持相对稳定,网络在朝着一个相对固定的方向训练一段时间后,再切换到新的目标。这个周期一般设为 500 到 2000 步之间。
3.3 DQN 的损失函数
DQN 的损失函数本质上是一个均方误差:
Loss = E[(r + gamma * max_a' Q_target(s', a') - Q_policy(s, a))^2]这里的 Q_policy 是主网络对当前状态下实际执行动作 a 的预测 Q 值,目标是计算出的 TD 目标。训练过程就是让主网络的预测值逐步逼近 TD 目标。
需要特别留意的是,目标中的 max 运算,是 DQN 在后续被提出来的一些问题的重要根源。使用 max 会选择所有动作中 Q 值最大的那个,但因为 Q 值是估计值,存在噪声,max 会把噪声往大的方向放大,造成 Q 值过高估计。这个问题在后续的 Double DQN 中会专门解决,本文不做过多展开,后面会根据情况适当提及。
3.4 探索与利用的平衡
Q-Learning 类和 DQN 类方法都面临探索与利用的平衡问题。如果智能体总是选择当前 Q 值最大的动作,它就永远发现不了更好的策略。如果一直乱选,又无法积累有效经验。
最常见的策略是 epsilon-greedy:
- 以概率 epsilon 随机选择动作进行探索。
- 以概率 1 - epsilon 选择 Q 值最大的动作进行利用。
训练初期 epsilon 通常设置得比较高,比如 1.0,让智能体充分探索环境;随着训练进行,逐步衰减到某个最小值,比如 0.01。衰减方式可以按固定乘子逐 episode 衰减,也可以按训练步数衰减,取决于具体场景。
epsilon 衰减速度对训练效果影响很大。衰减太快,智能体可能还没充分探索就进入“利用模式”,陷入局部最优。衰减太慢,智能体始终在随机试错,奖励曲线提升会很慢。对于 CartPole 这类简单环境,每 episode 乘以 0.995 左右是一个比较常见的起点配置。
3.5 表格方法与函数逼近的对比
为了让大家更直观地理解,这里把表格型 Q-Learning 和 DQN 放在一起对比:
| 维度 | 表格型 Q-Learning | DQN |
|---|---|---|
| Q 值存储 | 二维表格 | 神经网络参数 |
| 状态输入 | 离散索引 | 连续向量或图像 |
| 泛化能力 | 无,未访问状态无从估计 | 有,相近状态输出相近 |
| 训练数据 | 实时更新 | 经验回放随机采样 |
| 稳定性 | 天然稳定 | 需要目标网络等机制 |
| 适用规模 | 小规模离散状态 | 大规模高维状态 |
这个对比可以帮助理解文章的标题为什么要用“再聊”:Q-Learning 的核心思想在 DQN 中并没有改变,改变的只是 Q 函数的表现形式和训练方式。
4. 完整实战案例
下面进入本文的核心部分:用 PyTorch 实现一个可以训练的 DQN,并在 CartPole 环境中验证效果。代码会拆成多个文件,方便后续扩展和维护。
4.1 创建项目结构
在任意目录下创建项目文件夹,名称建议为dqn_cartpole。项目结构如下:
dqn_cartpole/ ├── dqn_model.py # 神经网络定义 ├── replay_buffer.py # 经验回放缓冲区 ├── dqn_agent.py # DQN 智能体 ├── train.py # 训练主循环 └── requirements.txt # 依赖清单如果只想快速验证效果,也可以把所有代码写在一个文件里。但按上述结构拆分,逻辑更清晰,也更接近工程项目的组织方式。
4.2 添加依赖配置
在 requirements.txt 中写入:
torch gymnasium numpy matplotlib安装依赖:
pip install -r requirements.txt4.3 编写神经网络模型
文件路径:dqn_model.py
DQN 的输入是状态向量,输出是所有动作的 Q 值。对于 CartPole 来说,输入维度是 4,输出维度是 2。中间使用两个隐藏层,每层 128 个神经元,激活函数选择 ReLU。
import torch.nn as nn class DQN(nn.Module): """将状态映射为各个动作的 Q 值。""" def __init__(self, state_dim, action_dim, hidden_dim=128): super(DQN, self).__init__() self.net = nn.Sequential( nn.Linear(state_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, action_dim) ) def forward(self, x): return self.net(x)这里模型结构相对简单,因为 CartPole 本身复杂度不高。如果后面想尝试 Atari 游戏,则需要换成卷积神经网络,输入也会从向量变成图像张量。
4.4 编写经验回放缓冲区
文件路径:replay_buffer.py
回放缓冲区使用双端队列实现,容量到达上限后,新的数据会覆盖最旧的数据。采样时使用随机采样,保证训练数据的随机性。
import random from collections import deque 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 = random.sample(self.buffer, batch_size) states = [item[0] for item in batch] actions = [item[1] for item in batch] rewards = [item[2] for item in batch] next_states = [item[3] for item in batch] dones = [item[4] for item in batch] return states, actions, rewards, next_states, dones def __len__(self): return len(self.buffer)这里选择直接返回 Python 列表,方便在训练循环中转换为张量。实践中也有人用 NumPy 存储来提升效率,但对 CartPole 这种小规模任务,列表已经足够。
4.5 编写 DQN 智能体
文件路径:dqn_agent.py
这个类是整个 DQN 算法的核心。它需要完成几件事:
- 维护主网络和目标网络。
- 根据 epsilon-greedy 策略选择动作。
- 执行一次梯度更新。
- 定期同步目标网络。
import torch import torch.nn as nn import torch.optim as optim import numpy as np from dqn_model import DQN class DQNAgent: def __init__(self, state_dim, action_dim, lr=1e-3, gamma=0.99, epsilon=1.0, epsilon_min=0.01, epsilon_decay=0.995, target_update=100): self.action_dim = action_dim self.gamma = gamma self.epsilon = epsilon self.epsilon_min = epsilon_min self.epsilon_decay = epsilon_decay self.target_update = target_update self.update_counter = 0 self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") self.policy_net = DQN(state_dim, action_dim).to(self.device) self.target_net = DQN(state_dim, action_dim).to(self.device) self.target_net.load_state_dict(self.policy_net.state_dict()) self.target_net.eval() self.optimizer = optim.Adam(self.policy_net.parameters(), lr=lr) self.loss_fn = nn.MSELoss() def select_action(self, state): if np.random.rand() < self.epsilon: return np.random.randint(self.action_dim) state_tensor = torch.FloatTensor(state).unsqueeze(0).to(self.device) with torch.no_grad(): q_values = self.policy_net(state_tensor) return int(torch.argmax(q_values).item()) def update(self, replay_buffer, batch_size): if len(replay_buffer) < batch_size: return states, actions, rewards, next_states, dones = replay_buffer.sample(batch_size) states = torch.FloatTensor(np.array(states)).to(self.device) actions = torch.LongTensor(np.array(actions)).unsqueeze(1).to(self.device) rewards = torch.FloatTensor(np.array(rewards)).unsqueeze(1).to(self.device) next_states = torch.FloatTensor(np.array(next_states)).to(self.device) dones = torch.FloatTensor(np.array(dones)).unsqueeze(1).to(self.device) current_q = self.policy_net(states).gather(1, actions) with torch.no_grad(): next_q = self.target_net(next_states).max(1, keepdim=True)[0] target_q = rewards + self.gamma * next_q * (1 - dones) loss = self.loss_fn(current_q, target_q) self.optimizer.zero_grad() loss.backward() nn.utils.clip_grad_norm_(self.policy_net.parameters(), max_norm=10) self.optimizer.step() self.update_counter += 1 if self.update_counter % self.target_update == 0: self.target_net.load_state_dict(self.policy_net.state_dict()) def update_epsilon(self): self.epsilon = max(self.epsilon_min, self.epsilon * self.epsilon_decay)几个实现细节需要重点解释。
第一,next_q = self.target_net(next_states).max(1, keepdim=True)[0]计算的是下一状态的最大 Q 值,对应 Q-Learning 中的 max 操作。
第二,dones的作用非常关键。当某个经验到达终止状态时,下一状态不存在真实的后续回报,TD 目标应该等于当前奖励,而不是加上下一状态的 Q 值。代码中用(1 - dones)把终止状态的期望回报置零。
第三,clip_grad_norm_做梯度裁剪,防止梯度值过大导致训练震荡。这在强化学习中是一个常用的稳定训练手段。
4.6 编写训练主循环
文件路径:train.py
训练主循环负责创建环境、创建智能体、交互采样、更新网络,并记录每个 episode 的回报,最后绘制奖励曲线。
import gymnasium as gym import matplotlib.pyplot as plt from replay_buffer import ReplayBuffer from dqn_agent import DQNAgent def train(episodes=400, batch_size=64, buffer_capacity=10000): env = gym.make("CartPole-v1") state_dim = env.observation_space.shape[0] action_dim = env.action_space.n agent = DQNAgent(state_dim, action_dim) replay_buffer = ReplayBuffer(buffer_capacity) reward_history = [] for episode in range(episodes): state, _ = env.reset() total_reward = 0 done = False while not done: action = agent.select_action(state) next_state, reward, terminated, truncated, _ = env.step(action) done = terminated or truncated replay_buffer.push(state, action, reward, next_state, done) agent.update(replay_buffer, batch_size) state = next_state total_reward += reward agent.update_epsilon() reward_history.append(total_reward) if (episode + 1) % 50 == 0: recent_avg = sum(reward_history[-50:]) / 50 print(f"Episode: {episode + 1}, Reward: {total_reward}, Avg(50): {recent_avg:.2f}, Epsilon: {agent.epsilon:.3f}") env.close() plt.plot(reward_history) plt.xlabel("Episode") plt.ylabel("Total Reward") plt.title("DQN Training Curve on CartPole") plt.show() if __name__ == "__main__": train()这里需要说明一下 Gymnasium 的接口。新版环境在env.step()返回四个值:next_state、reward、terminated、truncated。terminated 表示因为达到终止条件而结束,比如杆掉落了;truncated 表示因为超过步数限制而结束,比如 CartPole 达到 500 步上限。这两种情况在 DQN 训练中都视为“本回合结束”,但严格来说只有 terminated 才表示真实终止。
CartPole 官方规定每步存活奖励为 1,达到 500 回合步数上限时会输出 truncated 为 True。在简单实现中,两者合并处理即可。
4.7 运行与验证
在项目目录中执行:
python train.py运行时会输出类似下面的日志:
Episode: 50, Reward: 42, Avg(50): 52.10, Epsilon: 0.778 Episode: 100, Reward: 122, Avg(50): 95.36, Epsilon: 0.605 Episode: 150, Reward: 212, Avg(50): 146.28, Epsilon: 0.470 Episode: 200, Reward: 500, Avg(50): 284.55, Epsilon: 0.366 Episode: 250, Reward: 500, Avg(50): 476.12, Epsilon: 0.284如果你的环境显示 CartPole 的最大分数是 500,说明训练效果已经达到环境上限。不同机器的训练速度不同,CPython 环境下几百个 episode 通常只需要几分钟。
如果希望更直观地观察智能体的表现,可以在训练结束后添加一段保存模型和推理演示的代码。保存模型最简单的做法是:
torch.save(agent.policy_net.state_dict(), "dqn_cartpole.pth")加载的时候只需要恢复同样的网络结构:
model = DQN(state_dim, action_dim) model.load_state_dict(torch.load("dqn_cartpole.pth")) model.eval()4.8 基于训练结果的分析
从训练曲线可以看到一个典型现象:在一开始,奖励值停留在很低的水平,因为 epsilon 很大,智能体频繁随机动作;随着训练推进,奖励开始波动上升;最后稳定在最大值附近。
这种波动是正常的,不需要因为某个 episode 分数下降就立刻怀疑代码有问题。DQN 的样本来自经验回放,网络更新是非平稳的,奖励曲线本身就会有较大噪声。评价训练效果应该看平移平均,而不是单次结果。
如果训练了很多轮后平均奖励仍然无法提升,大概率不是环境或代码“没跑起来”,而是某个关键参数不太合适,这在下一节会详细说明。
5. 常见问题与排查思路
在实际训练 DQN 的过程中,几乎每个人都会遇到下面几个问题。这里整理成表格,方便遇到类似报错或异常现象时快速定位。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 训练不收敛,奖励始终在低水平 | epsilon 衰减太慢或学习率太大 | 增大 epsilon_decay 的衰减速度,减小 lr,尝试 lr=1e-4 |
| 训练前期很稳,后期突然崩掉 | 目标网络更新太频繁或梯度爆炸 | 增大 target_update,增加梯度裁剪或调低 lr |
| 经验回放缓冲区占用内存过高 | capacity 设置过大 | 调小容量,或改用数组实现减少 Python 对象开销 |
| 报错 env.reset() 返回太多值 | 使用了旧版 gym 但代码按 Gymnasium 写 | 改用obs = env.reset()的方式,或升级 gymnasium |
| 报错 env.step() 返回值不匹配 | 新旧版 API 差异 | 新版返回 5 个值,旧版返回 4 个值,按版本调整 |
| Q 值异常大,不断增长 | 过高估计问题,DQN 的 max 操作导致 | 换用 Double DQN 或用更小的学习率 |
| 训练曲线上下剧烈震荡 | 样本批次大小太小、缓冲区容量太小 | 增大 batch_size 和 buffer_capacity |
除了表格里的内容,再补充几个常见细节。
第一,CartPole 的奖励恒为 1,累计奖励最大是 500。如果训练日志中 episode 的最大奖励已经是 500,不要误以为是代码 bug。这是评分上限。
第二,enp.step()返回的 reward 在 CartPole 里每一帧都是 1,所以“总奖励高”直接等价于“存活时间长”。但在其他环境中,这个对应关系不一定成立。
第三,如果使用旧版 gym 而不是 gymnasium,环境创建方式可能是gym.make("CartPole-v1"),但reset()返回的只有一个值,step()返回的是四元组。对老代码做兼容处理时,需要判断环境的 API 类型,或者干脆统一使用新版 gymnasium。
第四,关于“训练过程中 loss 不下降是不是正常”这个问题。强化学习中的 loss 不像普通监督学习那样单调下降,因为每一次更新的目标值也在变化。loss 本身不是判断训练好坏的唯一标准,更重要的是累积回报曲线和实际策略表现。
6. 最佳实践与工程建议
6.1 网络结构的选择
DQN 的网络结构并没有特别严格的规则。对于 CartPole 这类低维连续状态输入,两层 128 个神经元的全连接网络已经足够。不要一上来就堆大模型,更大的模型在网络容量足够之后并不会带来额外收益,反而容易引起训练不稳定。
对于图像输入,一般使用卷积神经网络,具体结构可以参考早期 DQN 论文中的设计,但输入输出维度需要根据实际环境调整。模型参数量的增加会直接拉长训练时间,经验回放缓冲区也会更快被填满。
6.2 超参数的配置建议
超参数是 DQN 训练最需要花时间的部分。基于常见实验经验,推荐从下面这组默认值开始:
- 学习率:1e-3 到 1e-4。
- 折扣因子:0.99。
- 经验回放容量:10000 到 100000。
- batch_size:32 到 128。
- epsilon 初始值:1.0。
- epsilon 最小值:0.01 到 0.05。
- epsilon 衰减:按 episode 乘 0.99 到 0.995。
- 目标网络更新频率:100 到 1000 步。
调试时每次只改一个超参数,观察效果后再调整下一个。如果同时改多个参数,很难判断是谁导致了变化。
6.3 训练过程的稳定性处理
稳定 DQN 训练的一个常用技巧是梯度裁剪。前面的代码中已经加入了clip_grad_norm_,这是一种简单有效的防爆炸手段。另一个常用技巧是学习率预热,即在前几百步使用较小的学习率,让网络先适应一下数据分布,再恢复正式学习率。
如果训练过程波动过大,可以尝试加大 batch_size。较大的 batch 能降低梯度估计方差,让更新方向更稳定。代价是训练变慢,内存占用变大。
6.4 实验记录与模型管理
强化学习实验的随机性很大,同一组参数在不同随机种子下跑的 reward 曲线可能差别明显。工程上建议:
- 固定随机种子,确保实验可复现。
- 记录每次实验的配置参数、代码版本、训练日志。
- 不只保存最后一个模型的权重,也保存历史中表现最好的模型权重。
- 用平均回报和最大回报两个指标同时评价模型。
固定随机种子可以在训练代码开头加上:
import random import numpy as np import torch random.seed(0) np.random.seed(0) torch.manual_seed(0)这并不能完全消除所有不确定因素,但至少能让实验在相同软硬件环境下更接近可复现的状态。
6.5 安全与部署注意事项
把 DQN 模型部署到真实系统时,需要考虑几件事。首先,模型是离线训练出来的,它只见过训练环境,真实环境可能存在分布偏移,需要在部署前用小范围测试验证。其次,探索阶段的随机动作在真实系统中可能有安全风险,生产环境下通常要关闭探索,让模型直接采用贪心策略运行。最后,要保留人工干预的退路,不要让模型在失控情况下一直自动执行,这在机器人控制、工业自动化场景中尤为重要。
7. 总结与学习路线
这篇文章从 Q-Learning 表格法出发,梳理了 DQN 的核心动机和实现方式。关键点可以归纳为四条:神经网络替代表格来拟合 Q 函数;经验回放打破样本相关性;目标网络稳定训练目标;epsilon-greedy 平衡探索与利用。实战部分给出了一个可以直接运行的 CartPole DQN 代码,并解释了每一步的用途和潜在问题。
下一步的学习方向可以考虑:
- Double DQN:解决 Q 值过高估计问题,把动作选择和 Q 值计算解耦。
- Dueling DQN:把 Q 值拆成状态价值和动作优势,更细粒度地描述环境。
- Prioritized Experience Replay:让重要经验更容易被采样到。
- Rainbow DQN:把多种改进结合在一起,形成更完整的方法体系。
如果你已经能独立训练一个 CartPole DQN,这说明你已经跨过了 DQN 实现的门槛。接下来可以尝试更换环境,比如 LunarLander、FrozenLake,或者尝试用图像输入重新实现一遍,体验从低维状态到高维状态的接入过程。训练过程中遇到波动和失败是完全正常的,重要的是能够通过观察、分析、调整参数来定位问题。如果本文对你有帮助,可以收藏备用,下一篇可以继续深入 Double DQN 和 Dueling DQN 的对比实验。