4. Deep Q-Network (DQN):为什么需要深度网络、DQN架构、经验回放、目标网络

好,咱们接着聊。上一章我们把 Q-Learning 讲透了,你可能会想:这玩意儿不是挺好吗?表格一画,更新公式一算,完事。嗯,我当年也是这么想的。直到我在项目里遇到一个实际问题——状态空间太大了。

举个例子。假设我要控制一个机械臂抓取物体。光是关节角度,每个关节连续取值,状态空间就是无穷大。你拿什么表格去存?别说存了,遍历都遍历不完。这就是所谓的“维度灾难”。

那怎么办?深度学习来了。说白了,就是用神经网络去逼近那个 Q 值函数。你不需要记住每个状态-动作对的 Q 值,你只需要训练一个网络,给它状态,它就能输出各个动作的 Q 值。这就是 DQN 的核心思想。

为什么需要深度网络

我简单总结一下,为什么非要用深度网络不可:

  • 连续状态空间:表格方法要求状态离散且有限。但现实世界,比如图像、传感器数据,都是连续的。神经网络天然适合处理连续输入。
  • 泛化能力:你不可能见过所有状态。但网络能学到规律,遇到没见过的状态也能给出合理的 Q 值估计。我在做机器人抓取时,训练集里只有 10 万张图片,但测试时面对全新的物体摆放,网络依然能给出不错的动作选择。
  • 特征提取:尤其是用卷积网络处理图像时,网络能自动提取有用的特征,比如物体的位置、形状。你不需要手工设计特征,省大事了。

核心一句话:DQN 就是用深度神经网络代替 Q 表,把“查表”变成了“函数拟合”。

DQN 架构

DQN 的架构其实不复杂。输入是状态(比如一帧游戏画面),输出是每个动作的 Q 值。中间是几层卷积或全连接层。我习惯用三层卷积加两层全连接,当然具体看你的任务。

这里给一个简单的 PyTorch 代码示意:

import torch.nn as nn

class DQN(nn.Module):
    def __init__(self, state_dim, action_dim):
        super(DQN, self).__init__()
        self.fc1 = nn.Linear(state_dim, 128)
        self.fc2 = nn.Linear(128, 128)
        self.fc3 = nn.Linear(128, action_dim)

    def forward(self, x):
        x = torch.relu(self.fc1(x))
        x = torch.relu(self.fc2(x))
        return self.fc3(x)  # 输出每个动作的 Q 值

你看,结构很简单。但光有网络还不够,直接训练会出大问题。什么问题?我踩过坑,下面说。

经验回放

直接拿在线数据训练 DQN,你会发现训练极其不稳定。为什么?因为数据之间有强相关性。你想想看,连续几帧画面几乎一样,动作也差不多,网络一直在学相似的东西,很容易过拟合,甚至发散。

解决办法就是经验回放。说白了,搞一个缓冲区,把智能体跟环境交互的每一步(状态、动作、奖励、下一状态)都存进去。训练时,从缓冲区里随机采样一小批数据来更新网络。

这样做有两个好处:

  • 打破相关性:随机采样让数据变得独立同分布,训练更稳定。
  • 提高数据利用率:一条经验可以反复用,不像在线学习那样用完就扔。

我的经验:缓冲区大小一般设 100000 到 1000000。太小了采样多样性不够,太大了旧数据太多,网络学不到新东西。我一般先设 200000,然后根据训练曲线调整。

代码实现也很简单:

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)
        return zip(*batch)

    def __len__(self):
        return len(self.buffer)

目标网络

经验回放解决了数据相关性问题,但还有一个问题:训练目标本身在变。

你想想看,DQN 的更新公式里,目标 Q 值是用当前网络算出来的。网络参数一变,目标 Q 值也跟着变。这就像你追着一个移动的靶子打,永远打不准。训练很容易震荡甚至发散。

解决办法是引入目标网络。什么意思?就是搞两个网络:

  • 评估网络:用来选择动作和计算当前 Q 值,参数实时更新。
  • 目标网络:用来计算目标 Q 值,参数固定一段时间,然后从评估网络复制过来。

这样做的好处是,目标 Q 值在一段时间内是稳定的,训练起来就稳多了。

注意:目标网络不是完全不更新,而是每隔 C 步(比如 1000 步)才把评估网络的参数复制过去。C 太小了起不到稳定作用,太大了目标网络太滞后。我一般设 1000 到 5000,具体看任务。

更新流程大致是这样:

  1. 从经验回放中采样一个 batch。
  2. 用评估网络计算当前状态下的 Q 值。
  3. 用目标网络计算下一状态的最大 Q 值。
  4. 计算目标 Q 值:reward + gamma * max Q_next。
  5. 计算损失(比如 MSE),反向传播更新评估网络。
  6. 每 C 步,把评估网络的参数复制给目标网络。

我曾经在训练一个倒立摆任务时,没用目标网络,结果 loss 曲线像心电图一样上下乱跳。加上目标网络后,曲线立马变得平滑,收敛速度也快了。嗯,这个坑我替你们踩过了。

知识体系结构图

下面我用一张 SVG 图把 DQN 的核心逻辑串起来,方便你理解整体流程:

环境 智能体 经验回放 缓冲区 评估网络 目标网络 状态 s, 奖励 r 动作 a 存储 (s,a,r,s') 随机采样 每 C 步复制参数 计算目标 Q 值 DQN 核心架构流程图

从图上你能看到,整个流程是闭环的:智能体跟环境交互,数据存进经验回放,然后从回放中采样训练评估网络,评估网络定期复制给目标网络,目标网络用来计算稳定的目标 Q 值。嗯,就是这么回事。

避坑指南:我曾经在实现 DQN 时,忘了把目标网络设成 eval 模式,结果训练时目标网络也在更新,相当于没用目标网络。loss 直接炸了。所以记得,训练时评估网络用 train 模式,目标网络用 eval 模式。

好了,这一章就到这里。DQN 的三个核心组件——经验回放、目标网络、深度网络架构,你都清楚了。下一章我们会聊 DQN 的改进版本,比如 Double DQN 和 Dueling DQN,看看怎么让训练更高效、更稳定。

可可精品资料库