Skip to content

DQN

参考代码:cart_pole_v1

DQN算法是强化学习领域的划时代产物,直接带来了深度强化学习(Deep Reinforcement Learning, DRL)的爆发。在 DQN 出现之前,强化学习主要依赖 表格法(Table-based RL) 。在DQN之前,研究者们一直没有找到合适的近似替代方法。

2013年,DeepMind 在 NIPS 发表初版论文,2015年正式登上《Nature》。DQN 的核心思想是:用一个深度神经网络(卷积神经网络)来代替那张死板的 Q 表格。

DQN引入了三个核心机制:

  1. 经验回放 Experience Replay
  2. 目标网络 Target Network
  3. 端到端学习 End-to-End

DeepMind 用同一套 DQN 算法和超参数,在 49 款 Atari 游戏中大部分都超过了人类专业玩家。这证明了 AI 不需要为每个任务定制规则,而是具备了 通用学习能力 。这直接引出了AGI的可能性,进而开启了“超人”AI时代。

DQN 的成功直接催生了后来的 AlphaGo 。虽然 AlphaGo 后来使用了策略梯度(Policy Gradient)等更高级的算法,但其底层“深度学习 + 强化学习”的思想完全继承自 DQN。

Q网络

Q网络

在传统的 Q-learning 中,我们维护一张 Q 表格 ,横轴是动作,纵轴是状态。以经典的CartPole任务为例,状态(位置、速度、角度等)是连续的,显然表格是无法存下的。

由于状态有无限多个,我们不再记录具体的值,而是训练一个带参数 \(\omega\) 的函数 \(Q_\omega(s, a)\) 来逼近真实的 Q 值。这个函数就是由神经网络实现的,所以叫 Q 网络

1772799413774

我们从输入输出的角度来看Q网络:

  1. 输入:State,比如游戏画面、CartPole的状态(角度、速度等)
  2. 中间层:比如CNN架构,我们熟悉的卷积、池化等
  3. 输出:向量,比如上面CartPole例子中,输出就是2个神经元:向左移动或者向右移动。这两个神经元的值,就代表当前状态下,采取这两个动作的Q值。

传统Q-learning的迭代逻辑:

\[ Q(s, a) \leftarrow Q(s, a) + \alpha [ \underbrace{r + \gamma \max_{a' \in \mathcal{A}} Q(s', a')}_{\text{TD 目标}} - Q(s, a) ] \]

其中:

  • \(r + \gamma \max_{a'} Q(s', a')\) :这是我们根据当前奖励和对未来的最佳预期算出来的“理想目标”(TD Target)。
  • \(Q(s, a)\) :这是我们现在的“主观猜测”。
  • 学习率 \(\alpha\) 让我们的猜测不断向理想目标靠近。

当我们把 Q 表格换成神经网络 \(Q_\omega\) 后,更新规则就变成了 最优化问题 。在机器学习里,让一个值靠近另一个值最常用的方法就是 均方误差(MSE)

为了让神经网络的预测值 \(Q_\omega(s_i, a_i)\) 接近 TD 目标,我们构造了如下损失函数(Loss Function):

\[ \omega^* = \arg \min_{\omega} \frac{1}{2N} \sum_{i=1}^N \left[ Q_\omega(s_i, a_i) - \left( r_i + \gamma \max_{a'} Q_\omega(s'_i, a') \right) \right]^2 \]

其中:

  • \(Q_\omega(s_i, a_i)\) :神经网络当前的输出(预测值)。
  • \(r_i + \gamma \max_{a'} Q_\omega(s'_i, a')\) :TD 目标值(我们希望网络达到的标杆)。
  • \(\frac{1}{2N} \sum [... ]^2\) :这就是典型的均方误差,我们要通过梯度下降来寻找最优参数 \(\omega^*\)

虽然有了损失函数,但直接训练神经网络会非常不稳定。两个让 DQN 真正“封神”的黑科技:

  1. 经验回放(Experience Replay)
  2. 目标网络(Target Network)

损失函数

我们再来重新看一下损失函数。在传统的深度学习(比如图像分类)中,损失函数计算的是:

\[ Loss = (\text{标准答案} - \text{网络预测值})^2 \]

这里的“标准答案”是人类提前标注好的(比如这张图确定是猫)。但在强化学习里, 我们没有标准答案 。小车在某个状态向左推到底能拿多少分?没有人提前告诉神经网络。

那么,DQN 是怎么解决“没有标准答案”这个问题的呢?答案就是:用 Q-learning 的状态转移逻辑,自己给自己“算”出一个相对更准的标准答案。

具体结合过程可以分为以下三个概念的映射:

1. 网络预测值 (Prediction) = “我目前的猜测”

当小车处于状态 \(s\),准备执行动作 \(a\) 时,当前的神经网络会输出一个预测值:\(Q(s, a; \theta)\)\(\theta\) 代表网络当前的参数)。

  • 物理意义 :这就是神经网络基于目前的训练水平,做出的“纯猜测”。

2. 理论目标值 (Target) = “事后诸葛亮的更准猜测”

小车真的执行了动作 \(a\) 后,环境会给出一个真实的即时奖励 \(r\),并且小车进入了下一个状态 \(s'\)

根据 Q-learning 的核心(贝尔曼最优方程),在已知下一步状态的情况下,当前动作的真正价值应该是:

\(\text{Target} = r + \gamma \max_{a'} Q(s', a'; \theta^-)\)

  • 物理意义 :这个值虽然也包含一部分猜测(即对下一步状态价值的猜测 \(\max Q(s', a')\)),但它包含了一个 绝对真实的、环境给的客观真理 ——即时奖励 \(r\)
  • 正因为有这个真实的 \(r\) 兜底,加上走了一步后看到的信息更多了,所以这个 Target 值一定比纯靠脑补的预测值 \(Q(s, a; \theta)\) 更接近真理 。DQN 就把它强行当成了传统的“标准答案”。

3. 损失函数 (Loss) = “强行拉近两者的距离”

现在有了“当前猜测”(预测值)和“更准的猜测”(目标值),DQN 的损失函数就水到渠成了:计算两者的均方误差(MSE)。

因此,我们得到了Loss:

损失 = 状态s时的预测值 - (走这一步真实拿到的奖励 + 折扣后的状态s'时的最大预测值)

公式如下:

\[ Loss = \left( (r + \gamma \max_{a'} Q(s', a')) - Q(s, a) \right)^2 \]
  • 结合的精妙之处 :把上面这个差值扔给优化器(比如 Adam),神经网络就会通过反向传播调整参数,使得左边的 \(Q(s, a)\) 努力向右边的理论值靠拢。每次环境交互,我们都用状态转移产生的新经验,去纠正网络之前旧的、不准确的 Q 值评估。

总结来说:Q-learning 的状态转移逻辑负责“制造出正确的靶子” (Target),而损失函数负责计算偏差,并通过深度学习的机制 “让神经网络的输出无限逼近这个靶子”

我们一定要注意,由于强化学习没有标准答案,所以真实的奖励 \(r\) 才是整个公式里唯一的“锚点” 。假设没有真实的奖励 \(r\),目标只剩下 \(Q(s', a')\)。这就相当于要求网络让“当前状态的价值”完全等于“下一个状态的价值”。如果这样训练,神经网络最偷懒的办法就是: 把所有状态的 Q 值都输出为 0 ,这样损失直接就变成 0 了!小车根本学不到任何要拿高分的目标。

  • \(Q(s, a)\) 是纯粹的瞎猜。
  • \(\max Q(s', a')\) 也是瞎猜。
  • 只有 \(r\) 是环境给的 铁板钉钉的事实

经验回放(experience replay)

强化学习的数据是连续产生的,前后样本关联性太强(比如车杆正在往左倒,接连几个样本都是往左倒),这违反了机器学习样本独立同分布(IID)的假设。——所有的神经网络优化(比如 Adam、SGD)都有一个极其严格的数学假设——输入的数据必须是 独立同分布的(i.i.d) 。也就是说,每次喂给网络的一批数据,最好是随机打乱的、互不相关的。我们在做图像分类时,每次抽出来的 Batch 里面肯定既有猫、又有狗、也有汽车,这样网络才能均衡地学习。

假设小车连续 100 步都在向右倾斜。如果你立刻用这 100 步实时训练网络,神经网络就会严重过拟合于“向右倾斜”这个局部场景,把权重大幅调整为“疯狂向左推”。等小车稍微向左倾斜时,网络已经“忘记”了怎么处理左倾,结果直接翻车。

因此搞个“缓存池”,把经历过的 \(\{(s_i, a_i, r_i, s'_i)\}\) 存起来,训练时随机抽取(Shuffle)。这样能打破数据相关性,提高效率。具体做法就是:把数据存进池子,每次训练时随机抽样(Random Sample)出一个 Batch。这个 Batch 里,可能有第 5 步的数据,有第 100 步的数据,有向左倒的,有向右倒的。这样就硬生生把具有时间顺序的强化学习数据,变成了深度学习最喜欢的“随机打乱的数据集”。

在cartpole_v1中,回放池里存的仅仅是 纯粹的物理客观事实 ,也就是我们代码里写的五元组:

\((s, a, r, s', \text{done})\)

  • \(s\): 当时屏幕上的画面(小车位置、角度)。
  • \(a\): 当时你决定向左推还是向右推。
  • \(r\): 环境当场给了你几分。
  • \(s'\): 推完之后,下一帧画面的状态。
  • \(\text{done}\): 游戏是不是在那一刻结束了。

经验池就像是一本“航海日志” 或者 “行车记录仪”。它只客观记录“在什么时间点、看到了什么画面、踩了油门还是刹车、有没有撞车”,里面没有任何“对错”或“价值”的评价。

当我们开始训练时:

  1. 从池子里随机抓取 64 条“历史客观事实(日志)”。
  2. 把这 64 个历史状态 \(s\) 作为一个 Batch, 当场喂给现在的策略网络 (Policy Net) 。网络通过矩阵乘法和激活函数,实时吐出当前它对这 64 个状态的预测 Q 值。
  3. 把这 64 个历史下一状态 \(s'\) 当场喂给现在的目标网络 (Target Net) ,实时算出目标 Q 值。

我们绝对不能把Q值存在经验池中。因为神经网络在不断进化,旧的 Q 值是过期的垃圾。

假设在第 1 个回合,小车是个纯白痴,它当时对状态 \(s_1\) 的 Q 值估算可能是 0.1。把这个状态存进池子。到了第 100 个回合,小车已经变成了老司机。当我们从池子里重新把 \(s_1\) 抽出来时,我们绝不能用它第 1 回合那个愚蠢的 0.1 分作为依据。我们要做的是:把 \(s_1\) 这个客观画面,交给第 100 回合的、最新版本的神经网络去重新审视一遍。现在的网络一看:“哦,这个状态其实很好,Q 值应该是 50!”这正是深度强化学习能成功的精髓所在:用最新的大脑(网络权重 \(\theta\)),去反复复盘旧的记忆(回放池里的 \(s\)\(s'\)),从而获得越来越准确的认知。

经验回放还可以提高样本利用率:

  • 小车好不容易在第 50 步经历了一次极其惊险的“极限救车”,拿到了一次非常关键的 Reward。如果我们不用经验回放,网络算了一次 Loss,稍微更新了一点点权重(受限于极小的学习率 \(LR = 10^{-3}\)),然后就把这条珍贵的数据 永远丢弃了
  • 回放池的解法 :这条“极限救车”的经验被存进了池子。在接下来的几千步训练中,它可能会被随机抽中十几次、甚至几十次。神经网络可以通过不断地“复盘”这段关键记忆,真正把这个动作的价值刻在权重的 DNA 里。

“神经网络就像一个极其容易偏科的笨学生。如果让他边做题边学(实时输入),他做了 10 道几何题就会把代数全忘了。经验回放池就是帮他建立了一个 错题本(历史题库) ,每次从题库里随机抽 64 道不同类型的题给他做,这样他才能全面发展。”

目标网络

回顾一下 Q 网络的损失函数(Loss):

\[ L(\omega) = \mathbb{E} \left[ (\underbrace{r + \gamma \max_{a'} Q_\omega(s', a')}_{\text{目标值}} - \underbrace{Q_\omega(s, a)}_{\text{预测值}})^2 \right] \]

你会发现,等号左边的“预测值”和右边的“目标值”里都带着参数 \(\omega\)。这就像你在考场上写卷子,每写对一道题,标准答案就跟着变一次。这怎么可能考及格?

解决方案(目标网络)

  1. 我们克隆出两个长得一模一样的Q网络:一个叫 “在线网络” (Online Network) ,一个叫 “目标网络” (Target Network)
  2. 算损失函数时, 右边的目标值由“目标网络”来算 ,它的参数 \(\omega^-\) 是固定的。
  3. 我们只更新左边“在线网络”的参数 \(\omega\)
  4. 每隔 1000 步或者一段时间,把“在线网络”的参数同步给“目标网络”。

我们来深度理解一下为什么要两个网络。我们回顾一下DQN的训练过程:

  1. 初始化一个DQN网络,用CNN架构或者任何神经网络架构都行
  2. 设置经验回放,存储实际步数
  3. 在agent行动时,每行动一步,就从经验回放中选取一个batch,训练dqn网络:输入action, state对,输出q值,然后拿输出的q值和实际值对比算loss

这里就出现问题了,我们是有输出的q值,但是我们没有实际值,因为强化学习中并没有label,我们的数据都是在和环境交互的时候获取的。在RL中,我们把这个真实值叫做目标值(Target)。在RL中,我们没有Target,必须自己造Target,这里便产生了上面提到的双网络架构:

  1. Predicted Q,也就是我们第一反应想到的dqn,即我们训练的策略网络
  2. Target Q,目标网络,这个就是我们人为设置的用来算loss的网络。

目标网络怎么设置呢?既然我们要用Target来算loss,我们肯定就不能胡乱设置。这里有一套非常巧妙的设计方法:(我们用经验回放池满一个batch说起)

我们来深度理解一下为什么要两个网络。我们回顾一下DQN的训练过程:

  1. 初始化一个DQN网络,用CNN架构或者任何神经网络架构都行
  2. 设置经验回放,存储实际步数
  3. 在agent行动时,每行动一步,就从经验回放中选取一个batch,训练dqn网络:输入action, state对,输出q值,然后拿输出的q值和实际值对比算loss

这里就出现问题了,我们是有输出的q值,但是我们没有实际值,因为强化学习中并没有label,我们的数据都是在和环境交互的时候获取的。在RL中,我们把这个真实值叫做目标值(Target)。在RL中,我们没有Target,必须自己造Target,这里便产生了上面提到的双网络架构:

  1. Predicted Q,也就是我们第一反应想到的dqn,即我们训练的策略网络
  2. Target Q,目标网络,这个就是我们人为设置的用来算loss的网络。

目标网络怎么设置呢?既然我们要用Target来算loss,我们肯定就不能胡乱设置。这里有一套非常巧妙的设计方法:(我们用经验回放池满一个batch说起)

  1. 首先在初始化的时候,我们设置两个一模一样的网络,分别叫做policy net和target net。
  2. 当经验回放满一个batch的时候,我们第一次取出一个batch进行训练。我们要训练的一定是policy net,接下来的细节一定要看清楚。
  3. 假设我们拿到的第一个batch中的第一条数据是(s,a,r,s', not_goal),也就是 状态 s,执行了 动作 a(向右),拿到奖励 r = 1分,进入了下一 状态 s',游戏没结束。
  4. 我们把状态 s 喂给policy net。policy net前向传播计算出动作a(向右)的q值。这个叫做predict q。 由于我们的policy net现在还是初始化的状态,我们就假设这个瞎猜出来的值是0.5.这个值就是我们的predict Q。
  5. 接着,我们把下一状态s'喂给target net. target net看了看s',根据q-learning的原理,我们要找到s'状态下所有可能动作的最大q值的那个动作对应的q值。此时由于target net也是白痴,所以假设这里瞎猜了一个0.8。我们把这个叫做next q
  6. 现在,我们得到了两个q值:predict q, next q。前者来自于s,后者来自于s'。现在,我们要来做出真实label,不然没办法算loss。我们这里用拿到的真实奖励r来捏造答案——r是RL学习过程中唯一真实的东西。假设这里r=1,那么我们就捏造标准答案target q = reward + gamma * next q = 1 + gamma * 0.8,假设折扣因子gamma=0.99,那么target q = 1.792
  7. 现在我们可以计算loss了。loss = (target q - predict q)^2 = (1.792 - 0.5)^2
  8. 接下来,我们根据这个loss进行反向传播,更新policy net的参数。这里的目的很明确:让它下次在遇到 \(s\) 时,输出的值能从 \(0.5\)\(1.792\) 靠拢。
  9. 在这一刻,魔法发生了:policy net的参数被修改了,学到了一丝丝关于r=1的经验!但是,我们没有更新target net,因此target net没有任何的改变。

但是,我们不可能永远冻结target net。我们一般会设定一个周期,在这个周期内,target net是冻结的;当周期满了以后,我们就让target net = policy net,即将策略网络的参数直接覆盖掉目标网络。

双网络的底层原理

我们已经知道要使用两个网络来构造计算loss的必要组件,那么为什么我们不能只用一个网络呢?在数学和优化理论中,这被称为“移动目标问题”(Moving Target Problem)。我们可以通过对比标准监督学习和单网络 Q-learning 的数学本质来解释。

在标准的监督学习中,我们在最小化均方误差(MSE)时,目标值 \(y\)固定不变的(常数)。

如果我们的神经网络参数为 \(\theta\),预测值为 \(f(x; \theta)\),那么损失函数为:

\[ L(\theta) = \frac{1}{2} (y - f(x; \theta))^2 \]

当我们对 \(\theta\) 求导进行梯度下降时:

\[ \nabla_\theta L(\theta) = - (y - f(x; \theta)) \nabla_\theta f(x; \theta) \]

因为 \(y\) 只是一个真实的标签,它和 \(\theta\) 毫无关系,所以这个梯度下降的方向是极其稳定和明确的——就是逼近 \(y\)

如果我们只用一个网络,Predict Q 和 Target Q 都由同一个网络(参数为 \(\theta\))计算。此时,我们要优化的损失函数变成了:

\[ L(\theta) = \mathbb{E} \left[ \left( \underbrace{r + \gamma \max_{a'} Q(s', a'; \theta)}_{\text{Target } Y(\theta)} - \underbrace{Q(s, a; \theta)}_{\text{Predict}} \right)^2 \right] \]

发现问题了吗?你的目标值 \(Y(\theta)\) 包含了你正在更新的参数 \(\theta\)

当我们进行梯度下降,试图调整 \(\theta\) 让 Predict \(Q(s, a; \theta)\) 靠近 Target 时,Target \(Y(\theta)\) 本身也因为 \(\theta\) 的改变而发生了移动。

在实际操作中,Q-learning 使用的是 半梯度下降(Semi-gradient Descent) 。我们在计算梯度时,会强行忽略目标值对 \(\theta\) 的依赖,假装它是常数。

但这在单网络中会导致严重的不稳定性:

  • 状态 \(s\) 和下一个状态 \(s'\) 在特征空间上通常非常接近。
  • 当你为了提高 \(Q(s, a)\) 的值而更新参数 \(\theta\) 时,由于神经网络的泛化特性,\(Q(s', a')\) 的值通常也会跟着 同步变大
  • 结果就是:你往前追了一步,你的目标也往前跑了一步。这会导致 反馈循环(Feedback Loop) ,网络权重可能会不断累加,最终导致 \(Q\) 值发散(Divergence)或剧烈震荡,根本无法收敛。

DeepMind 在 2015 年引入了 Target Net,分配了一组独立的、被冻结的参数 \(\theta^{-}\)

引入 \(\theta^{-}\) 后,损失函数变成了:

\[ L(\theta) = \mathbb{E} \left[ \left( \underbrace{r + \gamma \max_{a'} Q(s', a'; \theta^{-})}_{\text{固定目标 } Y} - Q(s, a; \theta) \right)^2 \right] \]

在这个公式里,目标值 \(Y\) 完全由 \(\theta^{-}\) 计算得出。因为 \(\theta^{-}\) 在一段时间内是被冻结(不参与反向传播)的,所以目标值 \(Y\) 在数学上变成了一个 真正的常数

这就把强化学习中不稳定的自举(Bootstrapping)问题,暂时降维成了一个 标准的监督学习问题 。网络可以安心地朝着一个固定的目标去拟合。等 Policy Net 拟合得差不多了,我们再把 \(\theta\) 的值硬拷贝给 \(\theta^{-}\)(或者做软更新),给它一个新的固定目标,继续下一轮拟合。

半梯度下降

半梯度下降(Semi-gradient Descent) 是强化学习(特别是基于时间差分 TD 的算法)在数学上与传统深度学习分道扬镳的地方。

理想状态下,或者说监督学习任务中,我们使用的是完全的梯度下降(Full Gradient Descent)。

假设我们在做一个回归任务,预测值是 \(V(S; \theta)\),而我们有一个完美的、固定不变的真实目标值 \(U\)。我们的均方误差(MSE)损失函数是:

\[ J(\theta) = \frac{1}{2} [U - V(S; \theta)]^2 \]

如果我们对参数 \(\theta\) 求导来更新权重,根据链式法则,完整的梯度是:

\[ \nabla_\theta J(\theta) = - [U - V(S; \theta)] \nabla_\theta V(S; \theta) \]

这非常完美,数学上保证了每次更新都在朝着损失减小的方向走。

在强化学习(比如 Q-learning 或 TD 学习)中,我们通常没有一个“真实的”目标值 \(U\)。我们使用的是 自举(Bootstrapping) ——用当前的估计值去更新过去的估计值。

这时候,我们的目标值变成了 \(R + \gamma V(S'; \theta)\)。注意, 目标值里也包含了参数 \(\theta\)**** 。

此时,真实的损失函数变成了:

\[ J(\theta) = \frac{1}{2} [ \underbrace{R + \gamma V(S'; \theta)}_{\text{Target}} - \underbrace{V(S; \theta)}_{\text{Predict}} ]^2 \]

如果我们要对这个真正的函数求 完全梯度 ,结果应该是:

\[ \nabla_\theta J(\theta) = - [R + \gamma V(S'; \theta) - V(S; \theta)] \left( \nabla_\theta V(S; \theta) - \gamma \nabla_\theta V(S'; \theta) \right) \]

看到后面那个 \(-\gamma \nabla_\theta V(S'; \theta)\) 了吗?这就是目标值对 \(\theta\) 求导产生的部分。

在实际的 RL 算法中,计算后面那个 \(-\gamma \nabla_\theta V(S'; \theta)\) 非常困难,甚至在很多情况下是不可行的(因为它牵扯到环境状态转移的复杂性)。

所以,强化学习界做了一个简单粗暴的妥协:我们在求导时,假装目标值 \(R + \gamma V(S'; \theta)\) 是一个常数,强行忽略它对 \(\theta\) 的依赖。

这就意味着我们丢弃了目标值产生的那部分梯度,只保留了预测值产生的梯度。更新公式变成了:

\[ \text{更新梯度} \approx - [R + \gamma V(S'; \theta) - V(S; \theta)] \nabla_\theta V(S; \theta) \]

这就是为什么它叫 半梯度(Semi-gradient) ——因为我们只对等式的一半(预测部分)求了导,而忽略了另一半(目标部分)。


这个数学上的“偷懒”带来了极其深远的影响:

  • 优点 :计算效率大幅提升,使得算法可以通过 Bootstrapping 在线学习,不需要走完整个 Episode 就能更新网络。
  • 致命缺点 :因为它不是真正的梯度下降,它 丧失了收敛性保证 。这就像你在下山,但你手里的指南针只考虑了一半的磁场,你大概率会走偏。

这也是为什么强化学习中有一个著名的“死亡三角”(Deadly Triad)理论:当函数逼近(如神经网络)、 自举 (Bootstrapping/半梯度)和 离策略 (Off-policy,如 Q-learning)这三个条件同时出现时,训练极易崩溃。

当然,在后续的改进中,如到了PPO算法时代,因为使用on policy方法,所以就解决了死亡三角问题。

训练Q网络

训练过程

Q网络的伪代码如下:

我们来用人话梳理一下DQN的过程:
1、我们用CNN或者任何神经网络架构来作为DQN,也就是说我们不再用表格存储Q值,而是通过将state输入到网络中,来得到Q值
※这里要注意,虽然理论上q函数要输入(s,a),但是在实际工程中,我们只输入状态,然后输出所有可能动作的q值。
2、我们设置一个经验回放区,记录真实的小车轨迹
3、假设episode = 500,那么每一个episode就是小车从开始行动到到达目标点,或者达到最大episode步数
4、在每一个episode中,我们让小车开始行动,然后记录状态、动作、状态'、奖励、是否达到目标这么几个内容。
5、我们把状态s输入到policy net中,得到predict q。这里注意,我们选取小车实际执行的那个动作对应的q值,作为predict q。
我们把状态'也就是s'输入到target net中,得到next q。这里用q-learning,取所有动作中q值最大的那个选项。
6、我们根据实际的r,计算出target q。然后用target q和predict q算出loss,接着用loss更新policy net的参数。
7、我们设置一个冻结周期。达到冻结周期后,我们把policy net的权重复制给target net。
8、训练的时候,小车每走一步,我们就取一次batch进行训练。第一次取batch于经验回放区第一次达到一个batch的大小时进行。经验回放区相当于一个先进先出的队列。

策略崩塌

灾难性遗忘

我们看一下programming/cart_pole_v1的结果:

1772805036757

可以看到,reward在60-100回合间就已经达到了满分,结果反而在后面暴跌并在100-200分之间震荡。

我们把这种现象称为策略崩塌 (Policy Collapse / Performance Collapse)。只要你的模型原本表现很好,突然有一天“变傻了”,分数暴跌并且恢复不过来,都可以叫策略崩塌。这就好比说一个病人“休克”了,它是一个结果描述,而不是病因。

在本例中,策略崩塌的原因是灾难性遗忘 (Catastrophic Forgetting)。因为经验回放池的容量有限,当小车连续几十个回合保持完美平衡时,池子里充满了“怎么直行”的安逸数据,而早期“倾斜了怎么救回来”的数据被挤出去了。这直接导致,网络“忘记”了如何处理危机。一旦出现微小的倾斜,它就完全不知道怎么应对,直接崩塌。

过高估计

过高估计导致的散度 (Divergence due to Overestimation) 是导致策略崩塌的 另一个常见病因 (这就是 DDQN 要解决的问题)。网络对某些动作的 Q 值预估像吹气球一样越来越大,最后数值爆炸或者把错误的动作顶到了最高分。网络开始自信地执行极其愚蠢的动作(比如在安全状态下突然猛推小车导致翻车)。

在 PyTorch 的具体实现中,它们的核心区别仅仅在于计算目标值(Target Q)的 1 到 2 行代码:

传统 DQN 的盲目自信

\[ Y = r + \gamma \max_{a'} Q_{target}(s', a') \]

这就好比:让“目标网络”既当 裁判 (评估动作好坏),又当 运动员 (挑选得分最高的动作)。因为每次都挑自己认为最高的,极容易产生“过高估计(Overestimation)”。

而在Double DQN中,目标公式变成了:

\[ Y = r + \gamma Q_{target}(s', \arg\max_{a'} Q_{policy}(s', a')) \]

这就好比:让正在训练的“策略网络”当运动员去挑出最好的动作,然后让冻结的“目标网络”当裁判去打分。即使运动员盲目自信挑了个错的,冷静的裁判也会给它打个低分,从而打破“过高估计”的死循环。

在cart_pole_v1中,只需要改一行代码:

# ==========================================
# 【原版 DQN 的代码】(既当运动员又当裁判)
# ==========================================
with torch.no_grad():
    # 直接用 target_net 选出最大值
    next_q_values = self.target_net(next_state_batch).max(1)[0].view(-1, 1)

# ==========================================
# 【修改为 Double DQN 的代码】(策略网络挑选,目标网络打分)
# ==========================================
with torch.no_grad():
    # 1. 策略网络 (policy_net) 挑出下一个状态的最优动作 (运动员)
    best_actions = self.policy_net(next_state_batch).max(1)[1].view(-1, 1)
    # 2. 目标网络 (target_net) 评估这个动作的价值 (裁判)
    next_q_values = self.target_net(next_state_batch).gather(1, best_actions)

就是这么简单!在教学时把这两段代码放在一起对比,学生瞬间就能明白 DDQN 的精妙之处。具体的方法参见Double DQN章节。

Double DQN

Target Q改进

在DQN中,我们有以下过程:

我们来用人话梳理一下DQN的过程:
1、我们用CNN或者任何神经网络架构来作为DQN,也就是说我们不再用表格存储Q值,而是通过将state输入到网络中,来得到Q值
※这里要注意,虽然理论上q函数要输入(s,a),但是在实际工程中,我们只输入状态,然后输出所有可能动作的q值。
2、我们设置一个经验回放区,记录真实的小车轨迹
3、假设episode = 500,那么每一个episode就是小车从开始行动到到达目标点,或者达到最大episode步数
4、在每一个episode中,我们让小车开始行动,然后记录状态、动作、状态'、奖励、是否达到目标这么几个内容。
5、我们把状态s输入到policy net中,得到predict q。这里注意,我们选取小车实际执行的那个动作对应的q值,作为predict q。
我们把状态'也就是s'输入到target net中,得到next q。这里用q-learning,取所有动作中q值最大的那个选项。
6、我们根据实际的r,计算出target q。然后用target q和predict q算出loss,接着用loss更新policy net的参数。
7、我们设置一个冻结周期。达到冻结周期后,我们把policy net的权重复制给target net。
8、训练的时候,小车每走一步,我们就取一次batch进行训练。第一次取batch于经验回放区第一次达到一个batch的大小时进行。经验回放区相当于一个先进先出的队列。

其中的第五步:

5、我们把状态s输入到policy net中,得到predict q。这里注意,我们选取小车实际执行的那个动作对应的q值,作为predict q。 我们把状态'也就是s'输入到target net中,得到next q。这里用q-learning,取所有动作中q值最大的那个选项。

Double DQN唯一修改的地方就是上述第五步。Double DQN修改后的第五步:

5'、我们把状态s输入到policy net中,得到predict q。这里注意,我们选取小车实际执行的那个动作对应的q值,作为predict q。 我们把状态s'输入policy net,选取最大Q值的动作\(a^*\);然后把状态s'输入target net,强制选取出刚才挑选出来的\(a^*\)对应的Q值,作为next q。

我们把上述过程涉及的符号统一记录为:

  • \(r\):当前步的真实奖励 (Reward)
  • \(\gamma\):折扣因子 (Discount Factor)
  • \(s'\):下一个状态 (Next State)
  • \(a'\):在下一个状态可能采取的动作
  • \(\theta\)Policy Net (当前正在训练的网络)的参数
  • \(\theta^-\)Target Net (被冻结的网络)的参数

则原版DQN的Target Q公式:

\[ Y^{DQN} = r + \gamma \max_{a'} Q(s', a'; \theta^-) \]

Double DQN的Target Q公式:

\[ Y^{DDQN} = r + \gamma Q(s', \arg\max_{a'} Q(s', a'; \theta); \theta^-) \]

这里要注意,在马尔可夫决策过程(MDP)和强化学习的底层理论中,Q 函数(Action-Value Function)的绝对定义是:在状态 \(s\) 下,执行某一个特定的动作 \(a\),未来能获得的回报期望。既然它评估的是一个“状态-动作对(State-Action Pair)”,数学上就必须把它写成一个拥有两个自变量的函数:\(Q(s, a)\)

然而,在实际工程中,我们出于效率的考虑,在DQN第一次发表时,便采用了仅输入状态,然后输出该状态下所有可能动作的Q值。这样无论有多少个动作,神经网络只需要做一次前向传播。

TD Error优化

当我们刚开始学习 DQN 时,我们通常喜欢用“让 Predict Q 逼近 Target Q”这种说法,因为它非常直观,就像射击游戏里“让子弹(预测)击中靶心(目标)”。但当你深入阅读经典的强化学习顶会论文,或者真正在大厂里做 RL 工程落地时,你会发现大佬们和论文里几乎全在讨论 TD Error(\(\delta\)

\[ \text{TD Error} = \text{Target Q} - \text{Predict Q} \]

人们经常说优化TD Error,这是因为Loss函数就是由TD Error直接构建出来的,所以最小化TD Error就是整个网络更新的最终优化目标。

这里我们区分两个中文都叫做“目标”的概念:

  1. Target,即TD Target:\(Y = r + \gamma \max_{a'} Q(s', a'; \theta^-)\)
  2. Objective,通常等价于Loss Function:
\[ \min_\theta L(\theta) = \min_\theta \frac{1}{2} (\text{TD Error})^2 \]

前者是一个具体的数值(计算TD误差时所使用的具体的目标值),而后者则是一个动作或任务(让Loss变小的任务)。


在普通的 DQN 中,经验回放池(Replay Buffer)是均匀采样的(Uniform Sampling)。但你想想人类是怎么学习的?如果你做了一道题,你的预测和标准答案(Target)完全一样(TD Error = 0),你还需要再复习这道题吗?不需要了。你真正需要反复练习的,是那些 你错得最离谱的题

TD Error 的绝对值 \(|\delta|\),在物理意义上代表了智能体的“惊讶程度(Surprise)”或“学习潜力”。

  • \(|\delta|\) 很大:说明现实(Target)和我的预期(Predict)差距极大,这条数据里蕴含着巨大的信息量,我必须多拿出来学几次。
  • \(|\delta|\) 接近 0:说明我已经完全掌握了这个状态,再学也是浪费算力。

所以在 PER(Prioritized Experience Replay) 算法中,我们直接用 TD Error 的大小来决定经验回放池中每条数据的 采样概率 。TD Error 越大的样本,越优先被抽出来训练。


如果你以后去学 Actor-Critic(比如 PPO 算法),你会遇到一个极其重要的概念叫 Advantage(优势函数,\(A(s, a)\) 。它衡量的是“我采取动作 \(a\),比我通常在这个状态下盲选,到底好多少?”

而在数学推导上可以证明:TD Error 其实就是 Advantage 函数的一个无偏估计(Unbiased Estimate)!

\[ \delta_t = r_t + \gamma V(s_{t+1}) - V(s_t) \approx A(s_t, a_t) \]

从这个语境往下看,整个策略梯度(Policy Gradient)家族的优化过程,其实本质上就是顺着 TD Error 指引的方向,去提高那些 \(\delta > 0\)(表现超预期)的动作的出现概率。

Dueling DQN

Dueling DQN 是由 Google DeepMind 团队在 2016 年的 ICML(国际机器学习大会)上提出的。

价值分流

在传统的 DQN 中,不管当前是什么状态 \(s\),网络都要老老实实地去评估每一个动作 \(a\) 的绝对价值 \(Q(s, a)\)

但现实生活和游戏中,有很多情况是“采取什么动作根本不重要”的:

  • 场景 A(前面没车的大直道): 你现在处于一个非常好的状态,不管是踩油门、微微向左打方向、还是微微向右打方向,未来的收益都很高。
  • 场景 B(前方悬崖,刹车失灵): 你现在处于一个极其糟糕的状态,不管你是按喇叭、打方向盘还是拉手刹,你大概率都要掉下去了,所有动作的收益都极低。

对于传统 DQN 来说,它在场景 B 里,必须一遍又一遍地去学:“按喇叭得 -100 分”、“打方向盘得 -100 分”……它把算力全浪费在评估这些“毫无意义的动作差异”上了,而没有敏锐地察觉到:是这个“状态”本身太烂了,跟选什么动作没关系!

为了解决这个问题,Dueling DQN 在神经网络靠近输出层的最后阶段,把原本的一条全连接层, 劈成了两个并行的分支(这就是 Dueling“决斗/双流”这个词的由来)

  1. 价值流(Value Stream)—— 评估“状态好不好” 输出一个标量 \(V(s)\)。它的物理意义是: 不管接下去做什么动作,仅仅是身处这个状态 \(s\),能得多少分? (类似于场景 B 的基础分就是 -100)。
  2. 优势流(Advantage Stream)—— 评估“动作优不优” 输出一个向量 \(A(s, a)\),包含了每个动作的优势值。它的物理意义是: 在这个特定的状态下,采取动作 \(a\),比采取平均动作要“好多少”或者“差多少”? (它是一个相对的附加分)。

最后,在网络的输出端,把这两个流 加起来 ,合并成最终的 Q 值:

\[ Q(s, a) = V(s) + A(s, a) \]

即:

\[ Q = 基础分 + 附加分 \]

通过这个结构,网络如果发现某个状态下所有动作结果都一样,它就会直接把 \(V(s)\) 学准,而把所有的 \(A(s, a)\) 都变成 0。这极大地提高了网络的学习效率!

不可识别性

假设网络最终输出的 \(Q\) 值是 10。

  • 它是怎么来的?可能是 \(V=10, A=0\)
  • 也可能是 \(V=0, A=10\)
  • 甚至是 \(V=-100, A=110\)

因为 \(V\)\(A\) 的组合有无限种可能,神经网络在反向传播时就会“精神分裂”,不知道该更新 \(V\) 的权重还是 \(A\) 的权重,导致训练极其震荡。这便是“不可识别性(Unidentifiability)”。

为了强制网络把 \(V(s)\) 当作真正的基础分,把 \(A(s, a)\) 当作纯粹的相对优势,作者在最后相加的时候, 强制让优势流减去它的平均值

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

这样一来,所有动作的“优势”加起来平均值必定为 0。

更多的细节

Q:网络怎么就能学到“两种价值”了?网络有意识吗?

网络当然没有意识,它之所以能学出 \(V\)(状态价值)和 \(A\)(动作优势),完全是被“反向传播(梯度)”和“聚合公式”强行逼出来的。

我们再看一眼那个神级修复公式:

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

网络的 Loss 依然是拿最终算出来的 \(Q\) 去和 Target Q 算均方误差。网络并不“知道”左边那个叫状态价值,右边那个叫动作优势,它只知道要 降低 Loss

假设现在有一个状态,所有 4 个动作的 Target Q 突然整体上升了 10 分。网络为了降低 Loss,必须让自己的输出也整体变大。此时网络有两个选择来更新权重:

  • 选择 A(笨办法): 去修改 Advantage 分支的权重,让 \(A_1, A_2, A_3, A_4\) 每个都增加 10。但是!因为公式里有那个“减去均值”的操作,你如果四个动作都加 10,均值也会跟着变大 10,一减之后等于白加了!梯度在这里会被抵消掉很大一部分。
  • 选择 B(走捷径): 直接去修改 Value 分支的权重,让 \(V(s)\) 增加 10。这个操作畅通无阻,极其高效,瞬间就把 Loss 降下来了。

因为“减去均值”这个紧箍咒的存在,反向传播的梯度会 自动寻找阻力最小的路径 。如果是整体得分上升,梯度自然会流向 \(V\) 分支;如果是某一个特定动作得分特别突出,梯度才会流向 \(A\) 分支。这就是深度学习里最迷人的 表征解耦(Representation Decoupling)


Q:两个流是怎么做到同样输入换取不同输出的?(物理结构与初始化)

我们可以把前向传播(Forward)的过程用代码逻辑具象化:

  1. 共享层(Shared Representation): 输入状态 \(s\)(比如一张图片),先经过几层 CNN 提取特征。到了最后一层 CNN,输出一个长度为 512 的一维向量 feature_vector。到这里为止,大家用的是同一套权重。
  2. 物理分流(Branching): 这个 512 维的向量,接下来会被同时送进两个截然不同的全连接层中。这两个层的权重矩阵尺寸都不一样!
    • Value 分支(评估状态): 它是一个形状为 [512, 1] 的线性层。它负责把 512 个特征压缩成 1 个数字 (也就是 \(V\) 值)。
    • Advantage 分支(评估动作): 假设环境有 4 个动作,它就是一个形状为 [512, 4] 的线性层。它负责把 512 个特征映射成 4 个数字 (也就是 4 个动作的 \(A\) 值)。

可以看到,输出不一样的原因包括:

  • 结构不同: 一个输出 1 维,一个输出 4 维,计算矩阵本来就不一样。
  • 随机初始化: 在训练的最开始,这两个分支的权重矩阵(Weights)和偏置(Biases)都是随机初始化的。即使输入相同的 512 维特征,它们算出来的初始结果也绝对不同。
  • 独立更新: 在随后的训练中,由于前面提到的“聚合公式”把梯度分发成了不同的任务,这两套独立权重的更新方向就彻底分道扬镳了。一套拼命去拟合平均底薪(\(V\)),另一套拼命去拟合绩效奖金(\(A\))。

物理上,它们是被拆分开的两套独立网络参数;数学上,它们是被随机初始化赋予了不同的起点;机制上,它们是被特殊的“聚合公式”逼迫着承担了不同的语义功能。

D3QN

D3QN 不是一个什么全新的跨时代算法,它就是字面意思: Double DQN + Dueling DQN = D3QN

由于Double DQN和Dueling DQN解决的是完全不冲突的两个痛点,而且切入点完全不同:

  • Double DQN 动的是 目标公式(数学算式) ,为了解决 Q 值高估问题。
  • Dueling DQN 动的是 网络结构(物理层) ,把输出劈成两半(\(V\)\(A\)),为了提高网络对状态评估的效率。

那把它们强强联手简直是顺理成章。在写代码的时候,你只需要用 Dueling 的网络结构去输出 Q 值,然后把这些 Q 值套进 Double 的“挑选与打分分离”的公式里算 Loss 就可以了。现在的工程落地中,基础的 DQN 至少都是从 D3QN 起步的。

Rainbow DQN

我们在DQN笔记中已经学习了朴素DQN思想和Double DQN与Dueling DQN的改进。现在,我们来看一下更多的改进思想,以及DQN算法的结局。

PER

PER (Prioritized Experience Replay 优先经验回放)是对 DQN 的经验回放池(Replay Buffer) 进行的一次史诗级改造。

原版 DQN 的回放池是一个“大盲盒”。小车在环境里跑了 10000 步,数据全塞进去,每次训练时随机闭着眼睛抓一把(均匀采样) 出来学。但这极度反人类:有的步数小车走得很完美(网络预测极准,TD Error 接近 0),有的步数小车直接掉沟里了(网络预测大错特错,TD Error 极大)。闭着眼睛抓,会导致网络把大量算力浪费在“已经学会的废话数据”上。

PER 的核心逻辑(错题本机制):

它引入了我们在上一个问题中重点讨论的 TD Error(\(\delta\)

  • 每次算完 Loss 后,PER 会把这条数据的 TD Error 绝对值 \(|\delta|\) 记下来。
  • \(|\delta|\) 越大的数据,说明网络在这里错得越离谱(惊讶度越高)。
  • PER 会给这些“错题”赋予 更高的被采样概率 。下次抓数据的时候,网络会优先把这些错得离谱的数据抓出来“重点复习”。

这就像学霸考前不看全书,只刷错题本一样,极大地加速了网络的收敛速度。

Rainbow DQN

Rainbow(彩虹)DQN 是 DeepMind 在 2017 年发表的集大成之作。它不是某一个具体的微操,它是强化学习界的一场“终极缝合实验”。

当时 DeepMind 的研究员心想:既然这几年大家提出了这么多对 DQN 的改进,如果我把它们 全部塞进同一个网络里 ,会召唤出什么神仙?

于是,他们把当时公认最有效的 7 种 DQN 变体融为一体,就像集齐了无限手套上的宝石:

  1. DQN 本尊: 基础框架。
  2. Double DQN: 解决高估(改公式)。
  3. Dueling DQN: 提高效率(分流网络 \(V+A\))。
  4. PER(优先经验回放): 加速收敛(抓取高 TD Error 的错题)。
  5. Multi-step Learning(多步自举): 算 Target Q 时不再只看未来 1 步,而是看未来 \(n\) 步的真实奖励,让视野更广。
  6. Distributional RL(分布式强化学习): 不再只预测 Q 值的平均数,而是预测 Q 值的“概率分布”(比如输出一个直方图),极其硬核。
  7. Noisy Nets(噪声网络): 在神经网络的权重里人为加入随机噪声,让小车自动去探索未知环境,彻底抛弃传统的 \(\epsilon\)-greedy 探索策略。

这 7 种颜色的技术拼在一起形成的 Rainbow DQN,在 Atari 游戏测试中打出了碾压所有前置算法的统治级表现,直接把基于价值(Value-based)的强化学习推向了巅峰。

至此,DQN这条科技树就算是通关了。


评论 #