Q-learning 与 DQN 强化学习算法解析

1. 背景介绍

强化学习(Reinforcement Learning, RL)是一种机器学习方法,广泛应用于游戏、机器人控制、自动驾驶等领域。强化学习的核心思想是智能体(Agent)通过与环境(Environment)交互,不断学习策略(Policy),以最大化累积奖励(Reward)。

在强化学习中,最经典的方法之一是 Q-learning,它是一种无模型(Model-free)的值迭代算法。而 DQN(Deep Q-Network) 则是 Q-learning 在深度学习中的扩展,解决了 Q-learning 在高维状态空间中难以应用的问题。


2. Q-learning 介绍

2.1 Q-learning 的动机

Q-learning 旨在学习一个最优策略,使得智能体在给定状态下选择最优动作,以获得最大的长期奖励。

在 Q-learning 中,智能体维护一个 Q 值表 Q(s, a),表示在状态 s 下执行动作 a 后所能获得的长期累积奖励。

2.2 Q-learning 算法公式

Q-learning 的核心更新公式如下:

Q(s,a)←Q(s,a)+α[r+γmax⁡a′Q(s′,a′)−Q(s,a)] Q(s, a) \leftarrow Q(s, a) + \alpha [r + \gamma \max_{a'} Q(s', a') - Q(s, a)] Q(s,a)Q(s,a)+α[r+γamaxQ(s,a)Q(s,a)]

其中:

  • Q(s,a)Q(s, a)Q(s,a) 是 Q 值函数,表示状态 sss 下采取动作 aaa 的价值。
  • α\alphaα 是学习率(Learning rate),控制更新步长。
  • rrr 是即时奖励(Reward)。
  • γ\gammaγ 是折扣因子(Discount Factor),衡量未来奖励的重要性(0 ≤ γ ≤ 1)。
  • s′s's 是执行动作后的新状态。
  • max⁡a′Q(s′,a′)\max_{a'} Q(s', a')maxaQ(s,a) 是新状态 s′s's 下最优动作的 Q 值。

2.3 Q-learning 的使用场景

Q-learning 适用于以下场景:

  • 低维离散状态空间的任务(如网格世界、棋类游戏)。
  • 需要无模型(Model-free)方法的环境。
  • 训练过程中允许大量探索。

2.4 Q-learning 的局限性

  • 状态-动作空间较大时,Q 值表会变得难以存储和更新。
  • 难以处理连续状态空间,因为 Q 值需要在表中存储所有可能的状态-动作对。
  • 在高维任务中收敛速度慢,且难以泛化到未见过的状态。

3. DQN(Deep Q-Network)介绍

3.1 DQN 的动机

DQN 通过 深度神经网络(DNN) 近似 Q 值函数,解决了 Q-learning 在高维状态空间中存储 Q 值表的限制问题。

在 DQN 中,我们使用 神经网络 Q(s,a;θ)Q(s, a; \theta)Q(s,a;θ) 来代替 Q 值表,并通过 梯度下降 进行参数更新。

3.2 DQN 算法核心思想

DQN 主要引入了以下两项技术来提升 Q-learning 的稳定性和收敛速度:

经验回放(Experience Replay)

DQN 训练时不会直接使用最新经验更新 Q 值,而是将过去的经验(状态、动作、奖励、下一个状态)存储在 回放缓冲区(Replay Buffer),并在训练时 随机采样小批量数据 进行更新。这样可以减少样本之间的相关性,提高训练稳定性。

目标网络(Target Network)

为了避免目标值(Target Q 值)剧烈波动,DQN 维护两个神经网络:

  • 当前 Q 网络(Q-Network):用于计算当前动作的 Q 值。
  • 目标 Q 网络(Target Q-Network):用于计算目标 Q 值,参数每隔一段时间更新一次。

DQN 的目标值计算如下:

Yt=r+γmax⁡a′Q(s′,a′;θ−) Y_t = r + \gamma \max_{a'} Q(s', a'; \theta^-) Yt=r+γamaxQ(s,a;θ)

其中 θ−\theta^-θ 是目标网络的参数。

3.3 DQN 算法流程

  1. 初始化 Q 网络和目标网络,经验回放缓冲区。
  2. 采样初始状态 s
  3. 采用 ε-贪心策略 选择动作 a(以概率 ε 进行探索,以概率 1-ε 选择当前 Q 值最大的动作)。
  4. 执行动作 a,观察奖励 r 和新状态 s’,存入回放缓冲区。
  5. 从回放缓冲区随机采样小批量经验 (s, a, r, s’) 进行训练。
  6. 计算目标 Q 值 Y_t 并最小化损失函数:
    L(θ)=E[(Yt−Q(s,a;θ))2] L(\theta) = \mathbb{E}[(Y_t - Q(s, a; \theta))^2] L(θ)=E[(YtQ(s,a;θ))2]
  7. 每隔固定步数,将 Q 网络的参数更新到目标网络。
  8. 迭代至收敛。

3.4 DQN 的使用场景

DQN 适用于:

  • 高维状态空间(如 Atari 游戏、机器人控制)。
  • 状态离散但动作空间较大 的任务。
  • 无模型(Model-free)强化学习任务。

3.5 DQN 的局限性

  • 过估计问题:DQN 可能会高估某些动作的 Q 值,导致策略不稳定。
  • 训练不稳定:深度神经网络的训练可能会导致不稳定收敛。
  • 样本利用效率低:DQN 在高维任务中仍然需要大量数据才能学习良好的策略。

为了解决这些问题,后续提出了 Double DQN、Dueling DQN、Rainbow DQN 等改进算法

Double DQN算法

动机:DQN 在计算目标 Q 值时,使用同一个网络既选择动作又评估动作的 Q 值,这容易导致 Q 值偏高(过估计)。

生活上的类别:假设你想买一台新手机,你在网上查找各种测评,每款手机都显示一个“评分”(类似于 Q 值)。如果你只看评分最高的一款,那么可能会遇到评分被夸大的情况(比如某个品牌营销过度,让分数虚高)。

DQN 的问题类似于只相信最高评分,而 Double DQN 的改进就像是:

先用一个神经网络(online q net)选择最好的手机。
再用另一个更客观的神经网络(target q net)去验证这个选择的质量,避免高估。

# 下个状态的最大Q值
# q_net input: state ouput: all action value
if self.dqn_type == 'DoubleDQN': # DQN与Double DQN的区别
    max_action = self.q_net(next_states).max(1)[1].view(-1, 1)
    max_next_q_values = self.target_q_net(next_states).gather(1, max_action)
else: # DQN的情况
    max_next_q_values = self.target_q_net(next_states).max(1)[0].view(-1, 1)
Dueling DQN

传统DQN算法存在的问题(动机)

  1. 不同动作的 Q 值可能非常接近(例如,站在炸弹旁边时,所有方向的 Q 值都差不多)。
  2. Q 值的学习效率较低,因为网络要同时学习“状态的好坏” 和 “各个动作的相对优势”。

Dueling DQN 通过将 Q 值拆分为两部分
Q(s,a)=V(s)+A(s,a) Q(s, a) = V(s) + A(s, a) Q(s,a)=V(s)+A(s,a)

  • 状态价值 ( V(s) ):表示在状态 ( s ) 下整体有多好,不考虑具体动作。
  • 动作优势 ( A(s, a) ):表示在状态 ( s ) 下,执行动作 ( a ) 相对于其他动作有多大优势。

这种结构能让智能体更容易判断状态的好坏,而不是必须依赖于具体的动作 Q 值

Dueling DQN 在 DQN 网络的全连接层后,分成两条分支:

  • Value Stream(状态价值流):计算 ( V(s) )
  • Advantage Stream(动作优势流):计算 ( A(s, a) )

然后,它们通过以下方式合并成 Q 值:
Q(s,a)=V(s)+A(s,a)−1∣A∣∑a′A(s,a′) Q(s, a) = V(s) + A(s, a) - \frac{1}{|\mathcal{A}|} \sum_{a'} A(s, a') Q(s,a)=V(s)+A(s,a)A1aA(s,a)
其中,∑a′A(s,a′)\sum_{a'} A(s, a')aA(s,a) 表示所有动作的优势均值,这样做的目的是确保 Q 值的唯一性,防止 Value 和 Advantage 之间的相互影响。

import torch
import torch.nn as nn

class DuelingDQN(nn.Module):
    def __init__(self, state_dim, action_dim):
        super(DuelingDQN, self).__init__()
        self.feature_layer = nn.Sequential(
            nn.Linear(state_dim, 128),
            nn.ReLU(),
        )
        # 状态价值流
        self.value_stream = nn.Sequential(
            nn.Linear(128, 64),
            nn.ReLU(),
            nn.Linear(64, 1)  # 只输出 V(s)
        )
        # 动作优势流
        self.advantage_stream = nn.Sequential(
            nn.Linear(128, 64),
            nn.ReLU(),
            nn.Linear(64, action_dim)  # 输出 A(s, a)
        )

    def forward(self, state):
        features = self.feature_layer(state)
        V = self.value_stream(features)
        A = self.advantage_stream(features)
        Q = V + (A - A.mean(dim=1, keepdim=True))  # 计算 Q(s, a)
        return Q

特性 传统 DQN Dueling DQN
Q 值结构 直接输出每个动作的 Q 值 拆分为 状态价值 ( V(s) ) 和 动作优势 ( A(s, a) )
泛化能力 需要为每个动作学习 Q 值 只需学习状态的价值,泛化更好
学习效率 可能过度关注某些动作 更快学会哪些状态重要
适用场景 适用于所有强化学习问题 适用于动作影响不明显的情况(如 Atari 游戏)

Dueling DQN 适用于:

  • 状态比动作更重要的环境,比如:
    • Atari 游戏:如《太空侵略者》,在屏幕边缘和中间的情况不同,但具体按键可能影响不大。
    • Bomberman-like 游戏:智能体可能需要先判断“这个状态危险吗?”而不是“往左还是往右更好?”
  • 离散动作空间的强化学习问题,适用于 DQN 改进。
Rainbow DQN

Rainbow DQN 是 DeepMind 提出的综合强化学习算法,它结合了多个 DQN 的改进方法,大幅提升了强化学习的性能和稳定性。

Rainbow DQN 介绍 🌈

Rainbow DQN 是 DeepMind 提出的综合强化学习算法,它结合了多个 DQN 的改进方法,大幅提升了强化学习的性能和稳定性。


1. Rainbow DQN = 多种强化学习技术的结合

Rainbow DQN 之所以叫 “Rainbow”(彩虹),是因为它结合了6 种 DQN 改进方法,就像不同颜色的光融合成了一个强大的算法。

Rainbow DQN 结合了哪些技术?

技术 作用
Double DQN (DDQN) 解决 DQN 的 过估计 问题,提高学习稳定性
Dueling DQN 分解 Q 值为状态价值 (V(s)) 和 动作优势 (A(s, a)),提高学习效率
Prioritized Experience Replay (PER) 让更重要的经验(例如 TD 误差大的经验)更容易被采样,提高学习效率
Noisy Networks 在网络中加入可学习的噪声,替代 ε-greedy,提升探索能力
Multi-step Learning 采用 n 步 Q 学习,增强长期奖励信号
Distributional RL (C51) 预测 Q 值分布而不是单一值,使学习更稳健
(可选) Categorical DQN (C51) 进一步优化 Q 值分布估计,提升决策能力

1. Double DQN (DDQN)

  • 解决 DQN 高估 Q 值 的问题。
  • 方法一个网络选动作,另一个网络评估 Q 值
  • 作用:提高训练稳定性,让智能体避免被误导。

2. Dueling DQN

  • 将 Q 值分解为:
    Q(s,a)=V(s)+A(s,a) Q(s, a) = V(s) + A(s, a) Q(s,a)=V(s)+A(s,a)
  • 作用
    • 让智能体更快学习到哪些状态重要,而不是仅关注动作的好坏。
    • 在 Bomberman 这种游戏里,AI 先判断“这个位置安全吗?”再决定“往哪走?”。

3. Prioritized Experience Replay (PER)

  • 让经验回放(Experience Replay)优先选择重要的样本,而不是均匀随机采样。
  • 计算 TD 误差 ∣r+γQ(s′,a′)−Q(s,a)∣|r + \gamma Q(s', a') - Q(s, a)|r+γQ(s,a)Q(s,a),误差越大,样本被采样的概率越高。
  • 作用
    • 让 AI 更快学习到关键经验(比如“哪种操作最容易赢”)。
    • 更高效地利用数据,加快训练速度。

4. Noisy Networks

  • 在神经网络的权重中加入可学习的噪声,替代 ε-greedy 进行探索:
    W=μ+σ⋅ϵ W = \mu + \sigma \cdot \epsilon W=μ+σϵ
    其中 ϵ\epsilonϵ 是随机噪声,σ\sigmaσ 是可训练参数。
  • 作用
    • 智能体自动调节探索 vs. 开发,无须人为设定 ε-greedy。
    • 适用于复杂环境(如 Bomberman),避免 AI 过早收敛到次优策略。

5. Multi-step Learning (n-step TD)

  • 采用多步回报
    Gt=rt+γrt+1+γ2rt+2+...+γn−1rt+n−1+γnQ(sn,an) G_t = r_t + \gamma r_{t+1} + \gamma^2 r_{t+2} + ... + \gamma^{n-1} r_{t+n-1} + \gamma^n Q(s_n, a_n) Gt=rt+γrt+1+γ2rt+2+...+γn1rt+n1+γnQ(sn,an)
  • 作用
    • 让 AI 关注更长远的奖励,提高学习稳定性。
    • 在 Bomberman 里,AI 可以考虑到“放炸弹”后的多个时间步的结果,而不是只关注短期奖励。

6. Distributional RL (C51)

  • 预测Q 值的概率分布,而不是单个数值:
    Q(s,a)≈∑i=1Npizi Q(s, a) \approx \sum_{i=1}^{N} p_i z_i Q(s,a)i=1Npizi
    • 其中 ziz_izi 是离散化的 Q 值可能取值,pip_ipi 是它们的概率。
  • 作用
    • 让 AI 不仅知道“哪个动作最好”,还能估计“风险”。
    • 在 Bomberman 里,AI 能更好地评估炸弹爆炸的不确定性
  1. 离散化 Q 值分布
    C51 假设 Q 值的分布是有限的离散值,比如:
    zi=minr+i⋅maxr−minrN−1,i=0,1,...,N−1 z_i = \text{min}_r + i \cdot \frac{\text{max}_r - \text{min}_r}{N - 1}, \quad i = 0, 1, ..., N-1 zi=minr+iN1maxrminr,i=0,1,...,N1
  • 这里 minr\text{min}_rminrmaxr\text{max}_rmaxr 是奖励的最小值和最大值,比如 [-100, 100]。
  • 我们将 Q 值分成 ( N = 51 ) 个离散的值(这就是 C51 这个名字的来源)。
  • 每个 ziz_izi 代表 Q 值的一个可能取值。
  1. 预测 Q 值的概率分布
    C51 让神经网络输出一个 N 维的概率向量 ( p ),其中:
    pi=P(Z(s,a)=zi) p_i = P(Z(s, a) = z_i) pi=P(Z(s,a)=zi)
    也就是说,智能体不再只输出一个 Q 值,而是输出 51 个概率值,表示不同 Q 值的可能性。

  2. C51 使用 Bellman 更新:
    Z(s,a)=R+γZ(s′,a′) Z(s, a) = R + \gamma Z(s', a') Z(s,a)=R+γZ(s,a)
    但因为 Z(s,a)Z(s, a)Z(s,a) 可能不是在 ziz_izi 里,我们需要把它投影回 C51 的离散支持上,具体方法:
    (1) 计算目标分布
    (2) 把目标 Q 值映射到 ziz_izi
    (3) 计算新的概率分布 p′p'p
    (4) 最小化 KL 散度 DKL(p∣∣p′)D_{KL}(p || p')DKL(pp) 进行训练

具体参照下面的代码:

import torch
import torch.nn as nn
import torch.nn.functional as F

class C51DQN(nn.Module):
    def __init__(self, state_dim, action_dim, atom_size=51, v_min=-10, v_max=10):
        super(C51DQN, self).__init__()
        self.action_dim = action_dim
        self.atom_size = atom_size # 公式中的N
        self.v_min = v_min
        self.v_max = v_max
        self.support = torch.linspace(v_min, v_max, atom_size)

        self.feature_layer = nn.Sequential(
            nn.Linear(state_dim, 128),
            nn.ReLU(),
        )
        self.advantage_layer = nn.Linear(128, action_dim * atom_size)
        self.value_layer = nn.Linear(128, atom_size)

    def forward(self, state):
        q_distribution = self.forward(state)
        q_values = torch.sum(q_distribution * self.support, dim=2)  # 计算期望值
        return q_values

    def dist(self, state):
        feature = self.feature_layer(state)
        advantage = self.advantage_layer(feature).view(-1, self.action_dim, self.atom_size)
        value = self.value_layer(feature).view(-1, 1, self.atom_size)

        q_distribution = value + advantage - advantage.mean(dim=1, keepdim=True)
        q_distribution = F.softmax(q_distribution, dim=2)  # 归一化成概率分布
        return q_distribution


class DQNAgent:
	def _compute_dqn_loss(self, samples: Dict[str, np.ndarray]) -> torch.Tensor:
        """Return categorical dqn loss."""
        device = self.device  # for shortening the following lines
        state = torch.FloatTensor(samples["obs"]).to(device)
        next_state = torch.FloatTensor(samples["next_obs"]).to(device)
        action = torch.LongTensor(samples["acts"]).to(device)
        reward = torch.FloatTensor(samples["rews"].reshape(-1, 1)).to(device)
        done = torch.FloatTensor(samples["done"].reshape(-1, 1)).to(device)
        
        # Categorical DQN algorithm
        delta_z = float(self.v_max - self.v_min) / (self.atom_size - 1)

        with torch.no_grad():
            next_action = self.dqn_target(next_state).argmax(1)
            next_dist = self.dqn_target.dist(next_state)
            next_dist = next_dist[range(self.batch_size), next_action]
			
			# self.support : [-1.0, -0.9, ..., 1.0]
			# reward : 0.5
			# self.gamma : 1.0
            t_z = reward + (1 - done) * self.gamma * self.support # [atom_size]
            t_z = t_z.clamp(min=self.v_min, max=self.v_max)
            # t_z : [-0.5, -0.4, ..., 0.9, 1.0, 1.0, 1.0, 1.0, 1.0]
            b = (t_z - self.v_min) / delta_z
            # b : [5, 6, 7, ..., 19, 20, 20, 20, 20, 20]
            l = b.floor().long()
            u = b.ceil().long()

            offset = (
                torch.linspace(
                    0, (self.batch_size - 1) * self.atom_size, self.batch_size
                ).long()
                .unsqueeze(1)
                .expand(self.batch_size, self.atom_size)
                .to(self.device)
            )
            # [[0, 0, 0, ..], [20, 20, 20, ...], ...]

            proj_dist = torch.zeros(next_dist.size(), device=self.device)
            proj_dist.view(-1).index_add_(
                0, (l + offset).view(-1), (next_dist * (u.float() - b)).view(-1)
            )
            proj_dist.view(-1).index_add_(
                0, (u + offset).view(-1), (next_dist * (b - l.float())).view(-1)
            )

        dist = self.dqn.dist(state)
        log_p = torch.log(dist[range(self.batch_size), action])

        loss = -(proj_dist * log_p).sum(1).mean()

        return loss

参考网址:https://github.com/Curt-Park/rainbow-is-all-you-need


4. 结论

Q-learning 和 DQN 是强化学习领域的重要算法,各自适用于不同的任务场景。Q-learning 适用于小规模离散状态任务,而 DQN 通过神经网络扩展了 Q-learning,使其能够应用于高维状态空间的任务。尽管 DQN 仍有一些局限性,但后续改进算法进一步提升了其性能,使其在强化学习应用中发挥重要作用。

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐