DQN
参考代码:cart_pole_v1
DQN算法是强化学习领域的划时代产物,直接带来了深度强化学习(Deep Reinforcement Learning, DRL)的爆发。在 DQN 出现之前,强化学习主要依赖 表格法(Table-based RL) 。在DQN之前,研究者们一直没有找到合适的近似替代方法。
2013年,DeepMind 在 NIPS 发表初版论文,2015年正式登上《Nature》。DQN 的核心思想是:用一个深度神经网络(卷积神经网络)来代替那张死板的 Q 表格。
DQN引入了三个核心机制:
- 经验回放 Experience Replay
- 目标网络 Target Network
- 端到端学习 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 网络 。

我们从输入输出的角度来看Q网络:
- 输入:State,比如游戏画面、CartPole的状态(角度、速度等)
- 中间层:比如CNN架构,我们熟悉的卷积、池化等
- 输出:向量,比如上面CartPole例子中,输出就是2个神经元:向左移动或者向右移动。这两个神经元的值,就代表当前状态下,采取这两个动作的Q值。
传统Q-learning的迭代逻辑:
其中:
- \(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):
其中:
- \(Q_\omega(s_i, a_i)\) :神经网络当前的输出(预测值)。
- \(r_i + \gamma \max_{a'} Q_\omega(s'_i, a')\) :TD 目标值(我们希望网络达到的标杆)。
- \(\frac{1}{2N} \sum [... ]^2\) :这就是典型的均方误差,我们要通过梯度下降来寻找最优参数 \(\omega^*\)。
虽然有了损失函数,但直接训练神经网络会非常不稳定。两个让 DQN 真正“封神”的黑科技:
- 经验回放(Experience Replay)
- 目标网络(Target Network)
损失函数
我们再来重新看一下损失函数。在传统的深度学习(比如图像分类)中,损失函数计算的是:
这里的“标准答案”是人类提前标注好的(比如这张图确定是猫)。但在强化学习里, 我们没有标准答案 。小车在某个状态向左推到底能拿多少分?没有人提前告诉神经网络。
那么,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'时的最大预测值)
公式如下:
- 结合的精妙之处 :把上面这个差值扔给优化器(比如 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}\): 游戏是不是在那一刻结束了。
经验池就像是一本“航海日志” 或者 “行车记录仪”。它只客观记录“在什么时间点、看到了什么画面、踩了油门还是刹车、有没有撞车”,里面没有任何“对错”或“价值”的评价。
当我们开始训练时:
- 从池子里随机抓取 64 条“历史客观事实(日志)”。
- 把这 64 个历史状态 \(s\) 作为一个 Batch, 当场喂给现在的策略网络 (Policy Net) 。网络通过矩阵乘法和激活函数,实时吐出当前它对这 64 个状态的预测 Q 值。
- 把这 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):
你会发现,等号左边的“预测值”和右边的“目标值”里都带着参数 \(\omega\)。这就像你在考场上写卷子,每写对一道题,标准答案就跟着变一次。这怎么可能考及格?
解决方案(目标网络) :
- 我们克隆出两个长得一模一样的Q网络:一个叫 “在线网络” (Online Network) ,一个叫 “目标网络” (Target Network) 。
- 算损失函数时, 右边的目标值由“目标网络”来算 ,它的参数 \(\omega^-\) 是固定的。
- 我们只更新左边“在线网络”的参数 \(\omega\)。
- 每隔 1000 步或者一段时间,把“在线网络”的参数同步给“目标网络”。
我们来深度理解一下为什么要两个网络。我们回顾一下DQN的训练过程:
- 初始化一个DQN网络,用CNN架构或者任何神经网络架构都行
- 设置经验回放,存储实际步数
- 在agent行动时,每行动一步,就从经验回放中选取一个batch,训练dqn网络:输入action, state对,输出q值,然后拿输出的q值和实际值对比算loss
这里就出现问题了,我们是有输出的q值,但是我们没有实际值,因为强化学习中并没有label,我们的数据都是在和环境交互的时候获取的。在RL中,我们把这个真实值叫做目标值(Target)。在RL中,我们没有Target,必须自己造Target,这里便产生了上面提到的双网络架构:
- Predicted Q,也就是我们第一反应想到的dqn,即我们训练的策略网络
- Target Q,目标网络,这个就是我们人为设置的用来算loss的网络。
目标网络怎么设置呢?既然我们要用Target来算loss,我们肯定就不能胡乱设置。这里有一套非常巧妙的设计方法:(我们用经验回放池满一个batch说起)
我们来深度理解一下为什么要两个网络。我们回顾一下DQN的训练过程:
- 初始化一个DQN网络,用CNN架构或者任何神经网络架构都行
- 设置经验回放,存储实际步数
- 在agent行动时,每行动一步,就从经验回放中选取一个batch,训练dqn网络:输入action, state对,输出q值,然后拿输出的q值和实际值对比算loss
这里就出现问题了,我们是有输出的q值,但是我们没有实际值,因为强化学习中并没有label,我们的数据都是在和环境交互的时候获取的。在RL中,我们把这个真实值叫做目标值(Target)。在RL中,我们没有Target,必须自己造Target,这里便产生了上面提到的双网络架构:
- Predicted Q,也就是我们第一反应想到的dqn,即我们训练的策略网络
- Target Q,目标网络,这个就是我们人为设置的用来算loss的网络。
目标网络怎么设置呢?既然我们要用Target来算loss,我们肯定就不能胡乱设置。这里有一套非常巧妙的设计方法:(我们用经验回放池满一个batch说起)
- 首先在初始化的时候,我们设置两个一模一样的网络,分别叫做policy net和target net。
- 当经验回放满一个batch的时候,我们第一次取出一个batch进行训练。我们要训练的一定是policy net,接下来的细节一定要看清楚。
- 假设我们拿到的第一个batch中的第一条数据是(s,a,r,s', not_goal),也就是
状态 s,执行了动作 a(向右),拿到奖励r = 1分,进入了下一状态 s',游戏没结束。 - 我们把状态 s 喂给policy net。policy net前向传播计算出动作a(向右)的q值。这个叫做predict q。 由于我们的policy net现在还是初始化的状态,我们就假设这个瞎猜出来的值是0.5.这个值就是我们的predict Q。
- 接着,我们把下一状态s'喂给target net. target net看了看s',根据q-learning的原理,我们要找到s'状态下所有可能动作的最大q值的那个动作对应的q值。此时由于target net也是白痴,所以假设这里瞎猜了一个0.8。我们把这个叫做next q。
- 现在,我们得到了两个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
- 现在我们可以计算loss了。loss = (target q - predict q)^2 = (1.792 - 0.5)^2
- 接下来,我们根据这个loss进行反向传播,更新policy net的参数。这里的目的很明确:让它下次在遇到 \(s\) 时,输出的值能从 \(0.5\) 往 \(1.792\) 靠拢。
- 在这一刻,魔法发生了: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)\),那么损失函数为:
当我们对 \(\theta\) 求导进行梯度下降时:
因为 \(y\) 只是一个真实的标签,它和 \(\theta\) 毫无关系,所以这个梯度下降的方向是极其稳定和明确的——就是逼近 \(y\)。
如果我们只用一个网络,Predict Q 和 Target Q 都由同一个网络(参数为 \(\theta\))计算。此时,我们要优化的损失函数变成了:
发现问题了吗?你的目标值 \(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^{-}\) 后,损失函数变成了:
在这个公式里,目标值 \(Y\) 完全由 \(\theta^{-}\) 计算得出。因为 \(\theta^{-}\) 在一段时间内是被冻结(不参与反向传播)的,所以目标值 \(Y\) 在数学上变成了一个 真正的常数 。
这就把强化学习中不稳定的自举(Bootstrapping)问题,暂时降维成了一个 标准的监督学习问题 。网络可以安心地朝着一个固定的目标去拟合。等 Policy Net 拟合得差不多了,我们再把 \(\theta\) 的值硬拷贝给 \(\theta^{-}\)(或者做软更新),给它一个新的固定目标,继续下一轮拟合。
半梯度下降
半梯度下降(Semi-gradient Descent) 是强化学习(特别是基于时间差分 TD 的算法)在数学上与传统深度学习分道扬镳的地方。
理想状态下,或者说监督学习任务中,我们使用的是完全的梯度下降(Full Gradient Descent)。
假设我们在做一个回归任务,预测值是 \(V(S; \theta)\),而我们有一个完美的、固定不变的真实目标值 \(U\)。我们的均方误差(MSE)损失函数是:
如果我们对参数 \(\theta\) 求导来更新权重,根据链式法则,完整的梯度是:
这非常完美,数学上保证了每次更新都在朝着损失减小的方向走。
在强化学习(比如 Q-learning 或 TD 学习)中,我们通常没有一个“真实的”目标值 \(U\)。我们使用的是 自举(Bootstrapping) ——用当前的估计值去更新过去的估计值。
这时候,我们的目标值变成了 \(R + \gamma V(S'; \theta)\)。注意, 目标值里也包含了参数 \(\theta\)**** 。
此时,真实的损失函数变成了:
如果我们要对这个真正的函数求 完全梯度 ,结果应该是:
看到后面那个 \(-\gamma \nabla_\theta V(S'; \theta)\) 了吗?这就是目标值对 \(\theta\) 求导产生的部分。
在实际的 RL 算法中,计算后面那个 \(-\gamma \nabla_\theta V(S'; \theta)\) 非常困难,甚至在很多情况下是不可行的(因为它牵扯到环境状态转移的复杂性)。
所以,强化学习界做了一个简单粗暴的妥协:我们在求导时,假装目标值 \(R + \gamma V(S'; \theta)\) 是一个常数,强行忽略它对 \(\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的结果:

可以看到,reward在60-100回合间就已经达到了满分,结果反而在后面暴跌并在100-200分之间震荡。
我们把这种现象称为策略崩塌 (Policy Collapse / Performance Collapse)。只要你的模型原本表现很好,突然有一天“变傻了”,分数暴跌并且恢复不过来,都可以叫策略崩塌。这就好比说一个病人“休克”了,它是一个结果描述,而不是病因。
在本例中,策略崩塌的原因是灾难性遗忘 (Catastrophic Forgetting)。因为经验回放池的容量有限,当小车连续几十个回合保持完美平衡时,池子里充满了“怎么直行”的安逸数据,而早期“倾斜了怎么救回来”的数据被挤出去了。这直接导致,网络“忘记”了如何处理危机。一旦出现微小的倾斜,它就完全不知道怎么应对,直接崩塌。
过高估计
过高估计导致的散度 (Divergence due to Overestimation) 是导致策略崩塌的 另一个常见病因 (这就是 DDQN 要解决的问题)。网络对某些动作的 Q 值预估像吹气球一样越来越大,最后数值爆炸或者把错误的动作顶到了最高分。网络开始自信地执行极其愚蠢的动作(比如在安全状态下突然猛推小车导致翻车)。
在 PyTorch 的具体实现中,它们的核心区别仅仅在于计算目标值(Target Q)的 1 到 2 行代码:
传统 DQN 的盲目自信 :
这就好比:让“目标网络”既当 裁判 (评估动作好坏),又当 运动员 (挑选得分最高的动作)。因为每次都挑自己认为最高的,极容易产生“过高估计(Overestimation)”。
而在Double DQN中,目标公式变成了:
这就好比:让正在训练的“策略网络”当运动员去挑出最好的动作,然后让冻结的“目标网络”当裁判去打分。即使运动员盲目自信挑了个错的,冷静的裁判也会给它打个低分,从而打破“过高估计”的死循环。
在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公式:
Double DQN的Target Q公式:
这里要注意,在马尔可夫决策过程(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\)) 。
人们经常说优化TD Error,这是因为Loss函数就是由TD Error直接构建出来的,所以最小化TD Error就是整个网络更新的最终优化目标。
这里我们区分两个中文都叫做“目标”的概念:
- Target,即TD Target:\(Y = r + \gamma \max_{a'} Q(s', a'; \theta^-)\)
- Objective,通常等价于Loss Function:
前者是一个具体的数值(计算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)!
从这个语境往下看,整个策略梯度(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“决斗/双流”这个词的由来) :
- 价值流(Value Stream)—— 评估“状态好不好” 输出一个标量 \(V(s)\)。它的物理意义是: 不管接下去做什么动作,仅仅是身处这个状态 \(s\),能得多少分? (类似于场景 B 的基础分就是 -100)。
- 优势流(Advantage Stream)—— 评估“动作优不优” 输出一个向量 \(A(s, a)\),包含了每个动作的优势值。它的物理意义是: 在这个特定的状态下,采取动作 \(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)\) 当作纯粹的相对优势,作者在最后相加的时候, 强制让优势流减去它的平均值 :
这样一来,所有动作的“优势”加起来平均值必定为 0。
更多的细节
Q:网络怎么就能学到“两种价值”了?网络有意识吗?
网络当然没有意识,它之所以能学出 \(V\)(状态价值)和 \(A\)(动作优势),完全是被“反向传播(梯度)”和“聚合公式”强行逼出来的。
我们再看一眼那个神级修复公式:
网络的 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)的过程用代码逻辑具象化:
- 共享层(Shared Representation):
输入状态 \(s\)(比如一张图片),先经过几层 CNN 提取特征。到了最后一层 CNN,输出一个长度为 512 的一维向量
feature_vector。到这里为止,大家用的是同一套权重。 - 物理分流(Branching):
这个 512 维的向量,接下来会被同时送进两个截然不同的全连接层中。这两个层的权重矩阵尺寸都不一样!
- Value 分支(评估状态): 它是一个形状为
[512, 1]的线性层。它负责把 512 个特征压缩成 1 个数字 (也就是 \(V\) 值)。 - Advantage 分支(评估动作): 假设环境有 4 个动作,它就是一个形状为
[512, 4]的线性层。它负责把 512 个特征映射成 4 个数字 (也就是 4 个动作的 \(A\) 值)。
- Value 分支(评估状态): 它是一个形状为
可以看到,输出不一样的原因包括:
- 结构不同: 一个输出 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 变体融为一体,就像集齐了无限手套上的宝石:
- DQN 本尊: 基础框架。
- Double DQN: 解决高估(改公式)。
- Dueling DQN: 提高效率(分流网络 \(V+A\))。
- PER(优先经验回放): 加速收敛(抓取高 TD Error 的错题)。
- Multi-step Learning(多步自举): 算 Target Q 时不再只看未来 1 步,而是看未来 \(n\) 步的真实奖励,让视野更广。
- Distributional RL(分布式强化学习): 不再只预测 Q 值的平均数,而是预测 Q 值的“概率分布”(比如输出一个直方图),极其硬核。
- Noisy Nets(噪声网络): 在神经网络的权重里人为加入随机噪声,让小车自动去探索未知环境,彻底抛弃传统的 \(\epsilon\)-greedy 探索策略。
这 7 种颜色的技术拼在一起形成的 Rainbow DQN,在 Atari 游戏测试中打出了碾压所有前置算法的统治级表现,直接把基于价值(Value-based)的强化学习推向了巅峰。
至此,DQN这条科技树就算是通关了。