☰
深度强化学习DQN实战:从迷宫建模到Python代码实现
2026/9/30 5:56:12 网站建设 项目流程

简介:面向深度学习与强化学习入门者的技术讲解PDF文档,围绕深度Q网络(DQN)的核心原理,以迷宫寻路为案例,逐步说明如何用神经网络替代传统Q表,并结合Experience Replay与epsilon-greedy策略训练智能体。资源从Q-Learning的存储瓶颈切入,详细解释状态、动作、奖励及损失函数(target_q与q的逼近)的对应关系,再过渡到DQN的建模与训练流程。包内仅含1个PDF文件,压缩包大小约205KB,内容以原理讲解为主,同时附有完整的TensorFlow示例代码与关键步骤注释,涵盖状态矩阵/动作矩阵定义、经验池构建、小批量采样、参数更新等内容,方便对照源码理解网络搭建与优化细节。目前已有1774人学习下载,资料短小精悍,适合快速建立DQN整体框架后进一步深入源码实践,也适合作为课程报告或自学笔记的参考资料。

1. 深度强化学习DQN:为什么迷宫问题是最好的入门试验场

深度强化学习DQN(Deep 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_actions=4): super(DQN, self).__init__() # 输入: 2 x height x width (地图 + 当前位置) self.conv = nn.Sequential( nn.Conv2d(2, 16, kernel_size=3, padding=1), nn.ReLU(), nn.Conv2d(16, 32, kernel_size=3, padding=1), 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)

卷积层负责提取空间特征,比如识别墙的分布、发现通道结构,全连接层负责把这些特征映射到每个动作的价值。这里使用了padding=1保持特征图尺寸不变,方便后续全连接层的维度计算。如果迷宫非常小比如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(maxlen=capacity) 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.005:

def 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(), lr=LR) 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, dtype=torch.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, dtype=torch.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, dtype=torch.float32) batch_next_states = torch.cat(batch_next_states) batch_dones = torch.tensor(batch_dones, dtype=torch.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(f"Episode {episode+1}, 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突然变成NaN,reward从正常值骤降。

原因:最常见的是Q值目标过大导致梯度爆炸。尤其是到达终点给+10奖励,如果γ接近1,Q值的量级会被不断累加,几次episode后数值就可能溢出。另一个常见原因是学习率太高,Adam的默认学习率在DQN上经常偏大。

解决:把学习率降到1e-3以下,如果还不行就试5e-4。对奖励做裁剪也很有效,把TD误差限制在[-1,1]范围内。PyTorch里可以用torch.clamp(loss, -1, 1)或者在计算Q值后做归一化。还有一种稳妥做法是改用Huber loss:

loss = 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, runs=100): model.eval() successes = 0 steps_list = [] for _ in range(runs): env = MazeEnv(maze) state = env.reset() state = torch.tensor(state, dtype=torch.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, dtype=torch.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, dtype=torch.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, cmap='hot', interpolation='nearest') plt.title(f"Trajectory ({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,我以为收工了,结果可视化一看智能体现在的结果是走出了迷宫但绕着终点画了三个圈才进去。后来把终点奖励调大、重复访问惩罚加重,问题才消失。这种问题只靠指标是发现不了的。

希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询