ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

深度强化学习DQN实战:从迷宫建模到Python代码实现

深度强化学习DQN实战:从迷宫建模到Python代码实现 简介面向深度学习与强化学习入门者的技术讲解PDF文档围绕深度Q网络DQN的核心原理以迷宫寻路为案例逐步说明如何用神经网络替代传统Q表并结合Experience Replay与epsilon-greedy策略训练智能体。资源从Q-Learning的存储瓶颈切入详细解释状态、动作、奖励及损失函数(target_q与q的逼近)的对应关系再过渡到DQN的建模与训练流程。包内仅含1个PDF文件压缩包大小约205KB内容以原理讲解为主同时附有完整的TensorFlow示例代码与关键步骤注释涵盖状态矩阵/动作矩阵定义、经验池构建、小批量采样、参数更新等内容方便对照源码理解网络搭建与优化细节。目前已有1774人学习下载资料短小精悍适合快速建立DQN整体框架后进一步深入源码实践也适合作为课程报告或自学笔记的参考资料。1. 深度强化学习DQN为什么迷宫问题是最好的入门试验场深度强化学习DQNDeep Q-Network是强化学习里第一个把深度学习与Q-learning成功结合的算法2015年Nature上那篇玩Atari游戏的文章让它彻底出圈。而迷宫问题恰好是DQN最友好的落地场景状态离散但规模不小动作只有四个方向奖励稀疏但目标明确。很多人学DQN时卡在看懂了公式、跑通了CartPole却不知道怎么迁移到自己的任务上迷宫就是一个能让你把状态设计、奖励函数、经验回放这些概念全部落到代码里的中间态项目。这篇笔记会从迷宫建模讲起把DQN的核心机制拆开给出可直接复现的Python源码和训练参数最后聊几个我实际踩过的坑。适合刚学完深度学习基础、想动手做第一个强化学习项目的读者也适合想快速评估DQN在自己路径规划任务上是否可行的工程师。2. 把迷宫问题建模成DQN状态、动作、奖励与网络结构的设计2.1 为什么迷宫任务不用Q表格而要用DQN经典Q-learning用一张表格存每个状态-动作对的Q值迷宫规模小的时候完全够用。但一旦迷宫变成20×20甚至更大状态数量就是格子数乘以四个动作表格会膨胀到难以维护而且泛化能力几乎为零——换个迷宫就要重新学。DQN的做法是用神经网络逼近Q函数输入是状态输出是每个动作的Q值预估。网络的泛化能力让它在相似的迷宫结构之间迁移成为可能同一个训练好的模型可以处理起点位置不同、墙体略有变化的迷宫。这一点在路径规划里尤其有价值因为现实世界的环境从来不会完全一样。我的建议是如果你的迷宫小于10×10且环境固定不变用Q表格更简单高效超过这个规模或有泛化需求果断上DQN。2.2 状态空间、动作空间和奖励函数的具体设计状态空间的设计直接决定网络能学到什么。我见过最朴素的写法是用一个二维数组表示迷宫每个格子取值0表示空地、1表示墙。把这个二维数组展平成一维向量送给网络输入维度就是迷宫的行乘列。一个更实用的做法是叠加智能体的当前位置信息。迷宫地图本身是静态信息但智能体在哪、离终点有多远这些信息需要网络自己从地图中推断。推荐使用通道拼接的方式用两个通道一个通道存迷宫地图另一个通道存当前位置当前位置为1其余为0组成一个2×H×W的输入张量。这样网络既能看到全局地图也能定位自己。动作空间很简单就是上下左右四个方向用0、1、2、3编号。要注意的是如果智能体撞墙这个动作算执行了但不产生移动Q值会被相应惩罚。奖励函数是迷宫DQN里最影响训练速度的设计。我常用的基准配置是事件奖励值到达终点10撞墙-0.1走一步普通空地-0.01原地转圈/重复访问-0.05终点给大正奖励让算法明确目标撞墙给轻微负奖励学会避障每走一步给微小负奖励是为了让智能体尽快到达终点避免绕路。注意不要给正常步数设置过大的负奖励否则智能体会倾向于原地不动。2.3 Q网络结构输入输出与参数设定迷宫DQN的网络不需要很深两到三层卷积加全连接就够用。如果迷宫很小也可以直接用全连接网络。import torch import torch.nn as nn class DQN(nn.Module): def __init__(self, height, width, num_actions4): super(DQN, self).__init__() # 输入: 2 x height x width (地图 当前位置) self.conv nn.Sequential( nn.Conv2d(2, 16, kernel_size3, padding1), nn.ReLU(), nn.Conv2d(16, 32, kernel_size3, padding1), nn.ReLU(), ) self.fc nn.Sequential( nn.Linear(32 * height * width, 128), nn.ReLU(), nn.Linear(128, num_actions) # 输出四个动作的Q值 ) def forward(self, x): x self.conv(x) x x.view(x.size(0), -1) return self.fc(x)卷积层负责提取空间特征比如识别墙的分布、发现通道结构全连接层负责把这些特征映射到每个动作的价值。这里使用了padding1保持特征图尺寸不变方便后续全连接层的维度计算。如果迷宫非常小比如5×5直接把所有格子输入全连接层效果也不差但10×10以上建议保留卷积层。3. DQN训练的核心机制经验回放、目标网络与ε-greedy探索3.1 经验回放打破样本相关性强化学习的样本是序列数据相邻两步的状态高度相关。如果像普通监督学习那样逐条喂给网络梯度更新会剧烈震荡训练很难收敛。经验回放的做法是把每次交互的转移四元组状态、动作、奖励、下一状态存进一个缓冲区训练时随机采样一批来更新网络。缓冲区大小我一般设置为10000到50000条。太小了采样的多样性不够太大了旧样本占比过高网络跟不上环境的变化。采样批量大小取32或64即可和普通深度学习训练一致。实现时用一个双端队列就行from collections import deque import random class ReplayBuffer: def __init__(self, capacity): self.buffer deque(maxlencapacity) 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)双端队列在容量满时自动丢弃最旧的数据符合经验回放保留近期经验的直觉。随机采样打破了时间相关性让网络更新时拿到的样本近似独立同分布。3.2 目标网络稳定训练的关键DQN的损失函数计算如下目标值是r γ * max(Q_target(下一状态))而当前Q值来自Q_online(当前状态)。如果用同一个网络同时计算这两个值每一轮更新后目标值也在变相当于追逐一个移动靶子容易发散。解法是维护一个目标网络它的参数定期从在线网络复制过来两次更新之间固定不动。常见做法是每隔固定步数硬复制或者用软更新目标网络每次往在线网络的方向移动一小步。我习惯用软更新超参数tau取0.005def soft_update(target_net, online_net, tau): for target_param, online_param in zip(target_net.parameters(), online_net.parameters()): target_param.data.copy_(tau * online_param.data (1.0 - tau) * target_param.data)软更新的好处是目标网络的变化是平滑的不会突然跳变导致训练震荡。tau控制跟随速度太大目标网络更新太快失去稳定作用太小则学习信号传递慢。实际调参中0.001到0.01之间都值得试。3.3 ε-greedy策略与探索-利用平衡训练开始时智能体什么都不知道需要大量随机探索来收集经验。随着训练推进又要逐渐倾向于选择当前最优动作。ε-greedy是最简单有效的策略以概率ε随机选动作以概率1-ε选择Q值最大的动作。ε的衰减方式直接决定训练效果。我见过不少翻车案例是ε衰减太快智能体还没来得及探索就锁定在次优路径上。推荐从1.0开始逐渐衰减到0.01衰减速度以每1000步为一个周期epsilon max(0.01, epsilon * 0.995) # 每个episode结束衰减一次乘以0.995大约1400个episode后衰减到0.01左右基本转为利用模式。如果你的迷宫比较大可以调成0.998甚至0.999让探索阶段更长。这一步属于DQN整套方案里最容易被忽略但最影响最终效果的参数值得单独花时间调试。4. 用Python从零搭建DQN迷宫求解核心代码与训练流程4.1 迷宫环境定义与状态编码写一个迷宫环境类负责维护地图、智能体位置、判定胜负。这里用一个10×10的迷宫示例起点在左上角(1,1)终点在右下角(8,8)墙体用随机生成加人工修正的方式保证存在通路。import numpy as np class MazeEnv: def __init__(self, maze): self.maze maze # 0表示空地1表示墙 self.height, self.width maze.shape self.start (1, 1) self.goal (8, 8) self.reset() def reset(self): self.agent_pos self.start self.steps 0 self.visited set([self.agent_pos]) return self._get_state() def step(self, action): # 动作: 0上, 1下, 2左, 3右 moves [(-1, 0), (1, 0), (0, -1), (0, 1)] delta moves[action] new_pos (self.agent_pos[0] delta[0], self.agent_pos[1] delta[1]) # 撞墙检测 if new_pos[0] 0 or new_pos[0] self.height or new_pos[1] 0 or new_pos[1] self.width: return self._get_state(), -0.1, False # 撞边界 if self.maze[new_pos] 1: return self._get_state(), -0.1, False # 撞墙 self.agent_pos new_pos self.steps 1 # 到达终点 if self.agent_pos self.goal: return self._get_state(), 10.0, True # 重复访问惩罚 reward -0.01 if self.agent_pos in self.visited: reward - 0.05 self.visited.add(self.agent_pos) # 步数上限防止死循环 if self.steps 500: return self._get_state(), -0.1, True return self._get_state(), reward, False def _get_state(self): # 双通道编码: 通道0为地图通道1为当前位置 state np.zeros((2, self.height, self.width)) state[0] self.maze state[1, self.agent_pos[0], self.agent_pos[1]] 1.0 return state这个环境类把地图、位置、碰撞、奖励全部封装好DQN训练循环不需要关心迷宫内部逻辑。动作撞墙时返回-0.1这个细节很关键它让智能体学会墙是不可穿越的同时不会因为撞墙就终止episode。步数上限500是兜底策略防止智能体在迷宫里无限游荡浪费训练时间。4.2 训练主循环完整代码训练循环可以分为四步选择动作、环境交互、存入回放、网络更新。下面是完整的训练脚本核心部分import torch import torch.optim as optim import random import numpy as np from collections import deque # 超参数配置 BATCH_SIZE 64 GAMMA 0.99 LR 1e-3 REPLAY_CAPACITY 20000 TAU 0.005 EPSILON_START 1.0 EPSILON_END 0.01 EPSILON_DECAY 0.995 NUM_EPISODES 2000 UPDATE_INTERVAL 4 # 网络与优化器 online_net DQN(10, 10) target_net DQN(10, 10) target_net.load_state_dict(online_net.state_dict()) optimizer optim.Adam(online_net.parameters(), lrLR) replay_buffer ReplayBuffer(REPLAY_CAPACITY) epsilon EPSILON_START total_steps 0 episode_rewards [] for episode in range(NUM_EPISODES): env MazeEnv(maze) state env.reset() state torch.tensor(state, dtypetorch.float32).unsqueeze(0) ep_reward 0 while True: # ε-greedy选择动作 if random.random() epsilon: action random.randint(0, 3) else: with torch.no_grad(): q_values online_net(state) action q_values.argmax().item() next_state, reward, done env.step(action) next_state_tensor torch.tensor(next_state, dtypetorch.float32).unsqueeze(0) # 存入经验回放 replay_buffer.push(state, action, reward, next_state_tensor, done) state next_state_tensor ep_reward reward total_steps 1 # 每4步更新一次网络 if len(replay_buffer) BATCH_SIZE and total_steps % UPDATE_INTERVAL 0: batch_states, batch_actions, batch_rewards, batch_next_states, batch_dones replay_buffer.sample(BATCH_SIZE) batch_states torch.cat(batch_states) batch_actions torch.tensor(batch_actions).unsqueeze(1) batch_rewards torch.tensor(batch_rewards, dtypetorch.float32) batch_next_states torch.cat(batch_next_states) batch_dones torch.tensor(batch_dones, dtypetorch.float32) # 计算当前Q值 current_q online_net(batch_states).gather(1, batch_actions).squeeze() # 计算目标Q值用目标网络 with torch.no_grad(): next_q target_net(batch_next_states).max(1)[0] target_q batch_rewards GAMMA * next_q * (1 - batch_dones) # 损失与反向传播 loss nn.MSELoss()(current_q, target_q) optimizer.zero_grad() loss.backward() optimizer.step() # 软更新目标网络 soft_update(target_net, online_net, TAU) if done: break epsilon max(EPSILON_END, epsilon * EPSILON_DECAY) episode_rewards.append(ep_reward) if (episode 1) % 50 0: print(fEpisode {episode1}, Reward: {ep_reward:.2f}, Epsilon: {epsilon:.3f}, Steps: {env.steps})代码里有两个关键细节。第一个是gather(1, batch_actions)它把网络输出的四个动作Q值中当前action对应的那个取出来这是PyTorch里做DQN的标准取法很多新手在这里用索引直接去取维度对不上会报错。第二个是目标值的计算GAMMA * next_q * (1 - batch_dones)这行做了终局处理如果done为1说明到达终点下一状态不存在Q值目标就只包含当前奖励。4.3 训练过程观察与结果分析跑起来之后你会在前100个episode看到reward始终是负数这非常正常因为智能体在盲目探索走一步就扣分频繁撞墙扣更多。200到500个episode之间 reward开始缓慢上升说明智能体找到了局部最优但还不够稳定。通常800个episode之后智能体能稳定到达终点。如果你想保存训练好的模型供后续使用torch.save(online_net.state_dict(), dqn_maze_model.pth)模型文件很小通常几十KB但保存时建议连超参数一起用字典形式存储否则下次加载时不知道网络结构是什么checkpoint { model_state: online_net.state_dict(), height: 10, width: 10, num_actions: 4 } torch.save(checkpoint, dqn_maze_checkpoint.pth)5. DQN迷宫求解避坑指南常见问题与排查5.1 损失爆炸或训练发散现象训练到一半loss突然变成NaNreward从正常值骤降。原因最常见的是Q值目标过大导致梯度爆炸。尤其是到达终点给10奖励如果γ接近1Q值的量级会被不断累加几次episode后数值就可能溢出。另一个常见原因是学习率太高Adam的默认学习率在DQN上经常偏大。解决把学习率降到1e-3以下如果还不行就试5e-4。对奖励做裁剪也很有效把TD误差限制在[-1,1]范围内。PyTorch里可以用torch.clamp(loss, -1, 1)或者在计算Q值后做归一化。还有一种稳妥做法是改用Huber lossloss nn.SmoothL1Loss()(current_q, target_q)Huber loss对离群点不那么敏感梯度不会因为单个异常样本爆炸。我现在的习惯是直接用Huber loss作为默认选择只有对数值稳定性有十足把握时才用MSE。5.2 智能体一直困在起点附近转圈现象训练了上千个episode智能体的轨迹始终停留在起点周围几个格子最远走不出去。原因探索严重不足。ε衰减太快是最常见的原因——智能体还没学会有用的动作模式随机探索就被关掉了只能靠初始Q值的微小差异做动作选择这形同盲走。另一个可能原因是撞墙的-0.1惩罚相对步数惩罚-0.01来说太大智能体学到不动比动更安全。解决把ε衰减系数从0.995调慢到0.998甚至0.999延长探索期。同时检查奖励结构撞墙惩罚不应超过步数惩罚的10倍否则算法会倾向保守策略。还有一个技巧是给起点附近的重复访问惩罚加重强制智能体往外走。5.3 到达终点附近但总是绕不过最后一道墙现象episode结束时智能体已经靠近终点但始终卡在某一面墙前面来回撞墙直到步数上限。原因这是典型的稀疏奖励探索终止问题。智能体知道终点方向但没有学到绕过墙的细微动作序列而随机探索又很难恰好在关键时刻组合出正确动作。目标网络更新太快也会让这个区域的Q值预估不稳定无法形成稳定的路径记忆。解决最直接的办法是加大到终点时的奖励力度比如从10提高到50让接近终点这个信号在Q值中更突出。也可以在每个episode结束后的最后N步尝试用动作序列回放填充经验把走到终点附近的轨迹教给经验池。更系统的做法是给智能体加一个距离衰减奖励每一步把与终点的曼哈顿距离作为负奖励的一部分鼓励逐步接近目标。5.4 换了迷宫结构后训练效果明显变差现象在A迷宫里训练好的模型放到B迷宫测试时成功率从90%掉到30%。原因过拟合到了特定迷宫布局。卷积网络学到的不完全是通用的避障和寻路能力而是A迷宫里的特殊墙体模式。这是DQN用在路径规划上的边界所在。解决训练时用随机生成的迷宫集合每个episode换一个迷宫让网络见过足够多种布局。如果你需要模型在真实环境中工作多做数据增强式的迷宫随机化同时加宽网络或加深网络增加容量。另外需要明确DQN学到的是策略不是通用路径规划算法泛化边界是客观存在的。5.5 训练正常但推理时动作选择明显错误现象训练时平均reward稳定上升但加载模型做推理时智能体随机乱走。原因加载模型时漏了设置eval模式网络里的Dropout和BatchNorm在训练模式和推理模式下行为不同。更深层的原因是训练时用的是ε-greedy混合了随机动作表现好可能是靠随机性Q值本身并没有收敛到准确。解决加载后调用online_net.eval()并用with torch.no_grad()包住推理过程。如果eval之后还是乱走需要在训练时降低最终ε到0.01以下让评估指标真正反映策略质量。我现在会在训练结束前固定几百个episode的ε为0.01再继续训练这一招能显著提升最终模型的可信度。6. 验证DQN效果并用可视化调试一个可复用的评估流程6.1 用成功率与平均步数评估模型只看reward曲线容易误判因为同样的reward可能对应不同质量的策略。我建议每次训练完成都跑三个指标成功率——100个episode里到达终点的比例平均步数——成功episode的步数均值路径长度——成功轨迹的曼哈顿距离比值越接近1说明绕路越少。def evaluate_model(model, maze, runs100): model.eval() successes 0 steps_list [] for _ in range(runs): env MazeEnv(maze) state env.reset() state torch.tensor(state, dtypetorch.float32).unsqueeze(0) for step in range(500): with torch.no_grad(): q_values model(state) action q_values.argmax().item() next_state, _, done env.step(action) state torch.tensor(next_state, dtypetorch.float32).unsqueeze(0) if done: if env.agent_pos env.goal: successes 1 steps_list.append(env.steps) break success_rate successes / runs avg_steps np.mean(steps_list) if steps_list else float(inf) print(f成功率: {success_rate:.2f}, 平均步数: {avg_steps:.1f}) return success_rate, avg_steps评估时每个episode单独跑使用固定的迷宫不要计入训练时的随机性。成功率低于80%说明训练未收敛高于95%说明策略已经稳定。如果成功率很高但平均步数明显多于最短路径说明智能体学会的是能到就行的绕路策略可以考虑增大每步的负奖励来压缩路径。6.2 可视化轨迹排除隐性问题数字指标只能告诉你行不行不能告诉你差在哪。一个简单的可视化函数把智能体的轨迹画出来一眼就能看出是绕远路、在局部死循环还是靠近终点失误import matplotlib.pyplot as plt def visualize_trajectory(model, maze): model.eval() env MazeEnv(maze) state env.reset() trajectory [env.agent_pos] for _ in range(200): with torch.no_grad(): q_values model(torch.tensor(state, dtypetorch.float32).unsqueeze(0)) action q_values.argmax().item() next_state, _, done env.step(action) state next_state trajectory.append(env.agent_pos) if done: break maze_viz maze.astype(float) for pos in trajectory: maze_viz[pos] 0.5 maze_viz[env.goal] 0.8 plt.imshow(maze_viz, cmaphot, interpolationnearest) plt.title(fTrajectory ({len(trajectory)} steps)) plt.show()轨迹图像里能看到很多训练时发现不了的问题。比如智能体反复在同一个区域来回走说明局部Q值分布不够尖锐或者起点和终点之间有明显捷径但智能体不走说明探索没有覆盖到那条路径。这个可视化调试过程比看十行reward曲线都有用是落地实践中我认为最值得做的一步。6.3 从固定迷宫到动态路径规划DQN的扩展方向迷宫跑通之后你完全可以沿着几个方向继续往下走。第一个方向是改用Double DQN消除Q值过估计——把目标值的动作选择和价值计算拆开代码改动只有几行训练稳定性提升显著。第二个方向是引入Dueling DQN把Q值拆成状态价值和动作优势两个分支在迷宫这类状态重要性差异大的场景收敛更快。第三个方向是换成Prioritized Experience Replay优先回放TD误差大的样本能显著提升样本利用率。如果你想把这个方案往实际路径规划上迁移迷宫里的离散网格可以换成连续坐标动作从四个方向换成速度和角度控制这时算法要升级到DDPG或TD3。但说实话这个迁移跨度不小建议先把DQN在迷宫上的每个细节吃透再考虑升级。回到我之前做过的几个DQN路径规划项目最大的教训是不要过早相信reward曲线一定要让模型跑完评估流程再下结论。有一次训练出了上升平稳的reward我以为收工了结果可视化一看智能体现在的结果是走出了迷宫但绕着终点画了三个圈才进去。后来把终点奖励调大、重复访问惩罚加重问题才消失。这种问题只靠指标是发现不了的。希望帮到你。本文还有配套的精品资源点击获取
返回列表