强化学习?DQN?
OMPL,Movelt,RRT*,Lattice
ADAM是什么
Deep Q-Network (DQN)
什么是 DQN (Deep Q-Network)?一份初学者指南
DQN(深度Q网络)是一种强化学习(Reinforcement Learning)算法,它巧妙地将神经网络的强大“估算”能力与一种经典的决策理论Q-Learning结合了起来。
您可以把 DQN 想象成一个正在学习玩电子游戏的 AI 🤖。
1. 核心思想:从“作弊表”到“估算器”
a. 传统方法:Q-Learning (制作“作弊表”)
在 DQN 出现之前,一种流行的方法叫 Q-Learning。它试图为游戏建立一张巨大的“作弊表”(称为 Q-Table)。
- 状态 (State, ): 游戏中的每一种可能情况(如:“玩家在位置X,敌人在位置Y”)。
- 动作 (Action, ): 玩家能做的每一种动作(如:“向左”、“向右”、“跳”)。
- Q值 (Q-Value), : 这张表里的“分数”。它代表:
“在状态 下,如果执行动作 ,那么从这一刻到游戏结束,我**未来能获得的总奖励(总分)**的最佳估计值。”
如何决策: AI 只需要查表,在当前状态 下,选择那个 分数最高的动作 。
b. Q-Learning 的“次元诅咒”
这个“作弊表”在简单游戏(如“井字棋”)中很有效。但如果用在自动驾驶或《星际争霸》中:
- 状态空间爆炸: 摄像头的每一帧图像都是一个独特的状态。状态的数量是无限的!
- 内存灾难: 我们根本不可能创建或存储一个“无限行”的表格。
c. DQN 的解决方案:用“大脑”估算
既然我们不能存储一个无限大的表格,DQN 提出:我们可以训练一个神经网络 🧠 来估算(或称“拟合”)这个表格!
这个神经网络就是一个函数逼近器 (Function Approximator)。
- DQN模型 (): 一个以参数 (即权重)为特征的神经网络。
- 输入: 游戏状态 (比如摄像头的图像,或
CartPole的4个数字)。 - 输出: 一个列表,包含每一个可能动作的Q值。
- 例如,在
CartPole(动作:0=左, 1=右) 中:- 输入:
[0.01, 0.02, -0.03, 0.04](一个状态 ) - 输出:
[15.2, 18.7](即 , )
- 输入:
- 例如,在
2. 核心公式:DQN 如何学习?
DQN 的学习目标是让它的“估算” 尽可能地接近“真实”的Q值 。这个“真实”的Q值是由贝尔曼最优方程 (Bellman Optimality Equation) 定义的。
a. 贝尔曼最优方程(AI 的“指导思想”)
这个公式定义了一个“完美”的Q值应该是什么样的:
用大白话解释这个公式:
“在状态 执行动作 的未来总分 (Q*),等于:
- 你立刻拿到的奖励 ()
- 加上
- 你到达下一个状态 () 后,从那个状态出发所能拿到的未来总分的最大值 ( )。”
- (gamma) 是**折扣因子** (Discount Factor),一个0到1之间的数字(比如0.99)。它代表了“未来的奖励有多重要”(越接近1越重要)。
b. DQN 的“学习目标”与“损失函数”
DQN 不能直接解这个方程,它使用这个方程来创造自己的“学习目标”。
DQN 的训练依赖两个关键技术:
1. 经验回放 (Experience Replay)
AI 把它的所有“经验” (s, a, r, s') 存储在一个巨大的“记忆库” 中。训练时,它从这个库中随机抽取一批(mini-batch)经验来学习,而不是使用刚发生的经验。这打破了经验之间的相关性,使训练更稳定。
2. 目标网络 (Target Network)
DQN 使用两个神经网络:
- (策略网络): 我们正在训练的“主网络”,用来做决策。
- (目标网络): 一个“主网络”的旧版本(它的权重 会定期从 复制而来,然后保持“冻结”)。
我们使用这个“冻结”的 来计算我们的“学习目标”,这能防止目标值在训练中疯狂摆动,让学习更稳定。
c. 核心公式:损失函数 (The Loss Function)
对于从“记忆库” 中抽取的每一个经验 ,我们定义:
1. “目标Q值” (The Target / “正确答案”)
我们使用“目标网络” 来计算贝尔曼方程的右侧:
- (如果 是一个终止状态,则 )
2. “预测Q值” (The Prediction)
这是我们“主网络” 对我们当时所做动作的估算:
3. 损失函数 (The “Error”)
我们使用均方误差 (MSE) 来衡量“预测值”和“目标值”之间的差距。这就是我们要优化的目标:
训练过程就是通过梯度下降 (Gradient Descent) 来调整“主网络”的权重 ,以最小化这个损失函数 。
3. 总结:DQN 算法流程
- 初始化“主网络” 和“目标网络” (权重相同)。
- 初始化“记忆库” 。
- 循环(玩 局游戏):
- 获取游戏初始状态 。
- 循环(直到游戏结束):
- 用 评估当前状态 ,然后使用“-greedy”策略选择一个动作 。
- (“-greedy”:有 的概率随机选动作(探索),有 的概率选 估分最高的动作(利用))
- 在游戏中执行动作 ,获得即时奖励 和下一个状态 。
- 将这个“经验” 存入“记忆库” 。
- 更新 。
- 开始学习:
- 从 中随机抽取一批(mini-batch)经验。
- 对于抽取的每一条经验,使用“目标网络” 计算**“目标Q值” **。
- 使用“主网络” 计算**“预测Q值” **。
- 计算这批数据的均方误差 (MSE) 。
- 执行一次梯度下降,更新“主网络”的权重 。
- 用 评估当前状态 ,然后使用“-greedy”策略选择一个动作 。
- (关键) 每隔 步,将“主网络”的权重复制给“目标网络”:。
一个简易的dqn_cartpole验证
环境部署
创建虚拟环境
Windows:
1 | |
macOS / Linux:
1 | |
激活虚拟环境
Windows (cmd):
1 | |
Windows (PowerShell):
1 | |
macOS / Linux:
1 | |
安装必须的库
1 | |
退出环境
1 | |
python代码
1. Q-Value (Q值) —— AI的“价值判断”
理论:
Q-Learning(Q学习)的核心是学习一个叫做 “Q值” (Q-Value) 的函数。
Q(s, a) 函数代表:在“状态 s”(State)下,执行“动作 a”(Action),未来能获得的总回报(奖励)的期望值是多少。
简单说,Q(s, a) 就是一个“评分”。在某个状态下:
Q(s, '向左') = 10Q(s, '向右') = 50
AI会选择“向右”,因为它“预见”到这个动作的“价值”(Q值)更高。
由于CartPole的“状态s”(小车位置、杆子角度等)是连续的数字,有无限多种,我们不能用一张“表”(Q-Table)来存储。因此,我们用一个神经网络来**近似(approximate)**这个Q函数。
代码对应:
① 神经网络的“输出”就是Q值:
在 DQN 类中,网络的forward方法返回的就是Q值。
1 | |
当你调用 policy_net(state) 时,它的返回值 [1.5, -0.3] 就分别代表 Q(s, '向左') 和 Q(s, '向右') 的Q值。
② AI如何“使用”Q值做决策:
在 select_action 函数中,当AI决定“利用”(Exploitation)时:
1 | |
policy_net(state).max(1)[1] 这行代码,就是Q-Learning理论的核心执行步骤:“选择那个Q值最高的动作”。
2. Experience Replay (经验回放) —— AI的“记忆宫殿”
理论:
如果我们让AI“学完一步就忘掉一步”,训练会非常不稳定(因为相邻的经历高度相关)。
为了解决这个问题,我们给AI一个“记忆库”(Replay Buffer)。AI把它的所有经历 (state, action, reward, next_state) 都存进去。这个“经历”在我们的代码里被定义为 Transition。
当AI要“学习”(训练)时,它不是学习“上一步”的经历,而是从“记忆库”里随机抽取一批(比如128个)旧经历来进行“反思”。
好处: 打破了经历之间的相关性,让训练更稳定、高效。
代码对应:
① 记忆库的“数据结构”:
ReplayMemory 类和 Transition 命名元组。
1 | |
② 存入记忆 (push):
在主训练循环(Main Training Loop)中,AI的每一步行动都会被存入记忆。
1 | |
③ 随机抽取 (sample):
在 optimize_model 函数(AI的“学习”函数)的开头:
1 | |
这就是“经验回放”的实现。AI不是在学 memory.push() 刚存进去的那个,而是在学 memory.sample() 随机抽出来的128个。
3. Deep Q-Network (DQN) —— AI的“学习过程”
理论:
AI如何“学习”?它需要一个“目标”和“现实”的差距(即 Loss 损失)。
DQN的“学习”就是不断调整神经网络的权重,让这个“差距”变小。
-
“现实” (Reality):
Q(s, a)。这是我们的
policy_net当前认为的Q值。 -
“目标” (Target):
r + γ * max(Q(s', a'))。这就是著名的贝尔曼方程 (Bellman Equation)。
它的大白话意思是:一个“完美”的Q(s, a)值,应该等于:你马上拿到的奖励r,加上 (γ是折扣因子),你在下一步s'能拿到的“未来最大Q值”max(Q(s', a'))。
DQN的训练,就是强迫 policy_net 去满足这个等式。
Loss = ( "目标" - "现实" )²
我们希望这个Loss最小。
代码对应:
整个 optimize_model 函数就是DQN的“学习过程”!
① 计算“现实”:Q(s, a)
1 | |
② 计算“目标”:r + γ * max(Q(s', a'))
1 | |
注: 代码里用了一个
target_net来计算“目标”,这是DQN的一个高级技巧 (Fixed Q-Targets),它能让训练更稳定。
③ 计算“差距”(Loss)并学习:
1 | |
optimizer.step() 这一行,就是AI在“学习”!它在根据loss调整 policy_net 的权重,让它下一次的预测 (Q(s, a)) 能更接近“目标” (r + ...)。
总结:DQN的“飞轮”
你现在可以把整个流程串起来了:
- AI行动 (
select_action): 用policy_net预测Q值,选一个动作a。 - AI记忆 (
memory.push): 把经历(s, a, r, s')存入记忆库。 - AI反思 (
optimize_model):- 从记忆库随机抽取 (
memory.sample) 一批旧经历。 - 计算这批经历的“现实Q值” (来自
policy_net)。 - 计算这批经历的“目标Q值” (来自
target_net和reward)。 - 计算两者的差距
Loss。 - 更新
policy_net(optimizer.step()),让“现实”更接近“目标”。
- 从记忆库随机抽取 (
这个“行动-记忆-反思”的飞轮不断旋转,AI的 policy_net 对Q值的估算就越来越准,它的决策(select_action)也就越来越好了。
Atari游戏
环境配置
运行以下命令(它会安装gymnasium的Atari依赖,并自动接受ROM许可证,避免版本问题,指定版本):
1 | |
1. 核心挑战:AI的“视觉系统” (环境封装器)
我们面临的主要问题是:CartPole 的状态是 4 个数字,而 Atari 的状态是 (210, 160, 3) 的彩色图像。AI 必须学会“看懂”像素。
解决方案:环境封装器 (Environment Wrappers)
我们没有直接将原始图像喂给 AI,而是通过一系列“封装器”搭建了一个高效的“视觉预处理流水线”。
-
AtariPreprocessing(官方 v0.29.1 封装器): 这是你解决问题的关键。这一个封装器替我们完成了三件大事:- 灰度化 (Grayscale): 抛弃颜色信息,将
(H, W, 3)降维到(H, W, 1)。 - 缩放 (Resize): 将高分辨率图像缩小到
(84, 84),大幅减少计算量。 - 跳帧 (Frame Skip): AI 不需要分析每一帧。我们设置
skip=4,让 AI 每 4 帧才决策一次,这极大加快了训练速度。
- 灰度化 (Grayscale): 抛弃颜色信息,将
-
FrameStack(自定义封装器): 这是为了解决“运动感知”问题。- 理论: 单张静态图片没有“速度”信息。AI 无法判断球是在移动还是静止。
- 实现: 我们将
k=4帧预处理过的(84, 84)图像堆叠在一起,形成一个(4, 84, 84)的张量(Tensor)。 - 结果: AI 现在可以通过对比这 4 帧的差异,来“感知”物体(如球和挡板)的运动方向和速度。
代码对应: wrap_atari 函数就是这个流水线的“总装厂”。
1 | |
2. 算法升级:从 MLP “大脑” 到 CNN “视觉皮层”
AI 的“大脑”必须升级,才能处理 (4, 84, 84) 这样的图像数据。
-
为什么?
nn.Linear(全连接层/MLP)无法处理空间信息。它会把84x84的像素粗暴地拉平,完全丢失所有“形状”和“位置”信息(例如,“球在挡板上方”)。nn.Conv2d(卷积层/CNN)专门用于提取局部特征(如边缘、角落、形状),并通过层次堆叠来理解复杂的空间关系,完美符合视觉任务的需求。
-
代码对应:
CNN_DQN类1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37class CNN_DQN(nn.Module):
def __init__(self, h, w, n_actions):
super(CNN_DQN, self).__init__()
# 1. 卷积层 (提取图像特征)
self.conv1 = nn.Conv2d(4, 32, kernel_size=8, stride=4)
self.bn1 = nn.BatchNorm2d(32)
self.conv2 = nn.Conv2d(32, 64, kernel_size=4, stride=2)
self.bn2 = nn.BatchNorm2d(64)
self.conv3 = nn.Conv2d(64, 64, kernel_size=3, stride=1)
self.bn3 = nn.BatchNorm2d(64)
# 2. 辅助函数:计算卷积层的输出尺寸
self.feature_size = self._feature_size(h, w)
# 3. 全连接层 (将特征拉平后,计算最终的Q值)
self.fc = nn.Linear(self.feature_size, n_actions)
def _feature_size(self, h, w):
with torch.no_grad():
x = torch.zeros(1, 4, h, w)
x = F.relu(self.bn1(self.conv1(x)))
x = F.relu(self.bn2(self.conv2(x)))
x = F.relu(self.bn3(self.conv3(x)))
return x.numel() # 返回特征总数
def forward(self, x):
# 1. 卷积与激活
x = F.relu(self.bn1(self.conv1(x)))
x = F.relu(self.bn2(self.conv2(x)))
x = F.relu(self.bn3(self.conv3(x)))
# 2. 展平 (展平成一维向量)
x = x.view(x.size(0), -1)
# 3. 全连接输出 Q值
return self.fc(x)
2.1 深度解析:CNN 架构的“魔法数字”
你可能会问:kernel_size=8, stride=4, out_channels=32 这些数字是哪来的?
它们不是随机的,而是直接来源于 DeepMind 2015 年的DQN论文,是久经考验的经典架构。
-
in_channels=4(输入通道):- 对应
FrameStack输出的 4 帧堆叠图像,这是 AI 感知运动的“眼睛”。
- 对应
-
conv1(k=8, s=4, out=32):- 大步长 (s=4), 大视野 (k=8): 在网络初期进行“积极降维”。快速将
84x84的图像大幅缩小,提取最粗糙、最全局的特征,并减少计算量。 - out=32: 尝试从原始图像中找出 32 种不同的基础特征(如边缘、角落)。
- 大步长 (s=4), 大视野 (k=8): 在网络初期进行“积极降维”。快速将
-
conv2(k=4, s=2, out=64):- 中步长 (s=2): 降维力度放缓,专注于将
conv1找到的 32 种基础特征组合成 64 种更复杂的特征(如“挡板”的轮廓)。
- 中步长 (s=2): 降维力度放缓,专注于将
-
conv3(k=3, s=1, out=64):- 小步长 (s=1): 不再缩小图像尺寸。它充当一个“精炼厂”,将 64 种特征进行复杂的混合与精加工,输出最纯粹的特征图,准备交给全连接层。
-
nn.BatchNorm2d(BN层):- 这是对原论文的改进。BN 层可以标准化每一批数据在网络中的激活值,能有效防止梯度爆炸/消失,让训练更稳定、更快收敛。
2.2 深度解析:_feature_size 的工程技巧
- 问题: 卷积层
conv3输出的是一个 3D 特征图([通道, 高, 宽]),而全连接层fc的输入必须是一个 1D 向量。我们如何知道conv3输出的C*H*W到底等于多少? - 笨办法: 手动计算。根据
84x84的输入,用复杂的卷积公式去计算conv1->conv2->conv3后的最终(C, H, W)。这非常繁琐且容易出错。 - 聪明办法 (即本函数):
with torch.no_grad():关闭梯度,进入“测量模式”。x = torch.zeros(1, 4, h, w): 创建一个和真实数据一模一样的“虚拟测试品”。x = F.relu(...): 让“测试品”流过所有的卷积层。return x.numel(): 测量“测试品”流出时总共有多少个元素(例如 5184 个)。这个数字就是我们需要的feature_size。
2.3 深度解析:“拉平”(Flatten) 操作与数学原理
“拉平”是连接 CNN(视觉皮层)和 FC(决策大脑)的桥梁。
- 目的: 将 CNN 输出的 3D“空间特征地图”
(C, H, W)转换成 FC 层需要的 1D“抽象特征清单”(N)。 - 代码:
x = x.view(x.size(0), -1) - 数学原理 (索引映射):
x.size(0)保留了Batch维度,我们不对其操作。-1告诉 PyTorch 自动计算 。- 在底层,计算机按照“行主序”(Row-Major Order)将 3D 张量展开。
- 一个在
T[c, h, w]的元素,会被映射到 1D 向量 中的第 个位置:
2.4 深度解析:全连接层 (FC) 的尺寸
- 问题:
self.fc = nn.Linear(self.feature_size, n_actions)的尺寸是如何决定的? n_actions(输出尺寸):- 这是最关键的参数,它必须等于环境的动作总数(例如
Pong是 6)。 - 它定义了
nn.Linear要输出多少个神经元,每个神经元对应一个动作的 Q 值。
- 这是最关键的参数,它必须等于环境的动作总数(例如
self.feature_size(输入尺寸):- 它必须等于上一步“拉平”操作后的 1D 向量长度(例如 5184)。
- 澄清误区: 全连接层的输出尺寸(
n_actions)不是输入尺寸(self.feature_size)的总和。 - 数学原理 (矩阵乘法):
- 全连接层执行的是一次矩阵乘法,将一个长向量 (尺寸 5184)“压缩”成一个小向量 (尺寸 6)。
- (输入) 的形状是 `[Batch, 5184]`。
- (权重) 的形状是 `[5184, 6]`。
- (偏置) 的形状是 `[6]`。
- (输出) 的形状是 `[Batch, 6]`。