news 2026/9/30 5:56:10

DQN深度强化学习实战:从Q-Learning到迷宫路径规划

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
DQN深度强化学习实战:从Q-Learning到迷宫路径规划

简介:面向深度学习与人工智能方向的PDF资料,系统讲解深度强化学习DQN(DeepQNetwork)的核心原理,并结合经典迷宫问题演示如何用神经网络替代Q-Learning中的Q表,解决状态与动作空间过大带来的存储和计算难题。内容涵盖Q-Learning基础、DQN训练流程、Experience Replay经验回放机制、损失函数推导、epsilon-greedy策略以及基于TensorFlow的完整代码实现,对想入门强化学习或理解DQN落地细节的开发者具有较好的参考价值。资源为单个PDF文档,压缩包大小205KB,通过迷宫例子贯穿原理与代码讲解,目前已有1774人学习下载,适合机器学习初学者与人工智能方向学生阅读。

1. 深度强化学习 DQN 是什么,为什么迷宫问题是最适合的入门落地点

第一次接触深度强化学习 DQN(Deep Q-Network)的人,很多不是倒在公式上,而是倒在“不知道这部分在解决什么问题”上。DQN 本质上是用一个神经网络去拟合 Q 值函数,替代传统 Q-Learning 里那张 Q 表;凡是状态空间大到表格装不下、或者状态输入是图像和连续量的场景,它就成了更合理的选择。迷宫问题恰恰把这件事压缩到了最容易观察的尺度:状态是离散格子,动作只有四个方向,奖励清清楚楚,模型有没有学会一眼就能看出来。换句话说,DQN 解决迷宫问题不是为了炫技,而是为了让你在源码层面把“经验回放怎么存、目标网络怎么更新、epsilon 怎么退火”这三个核心机制彻底看懂。这篇笔记适合刚入门深度强化学习算法、跟着源码跑通第一个路径规划任务的开发者,也适合手头有《深度学习原理.pdf》但缺一份落地代码的读者。

2. 先理解 DQN 的原理:从 Q-Learning 到 Deep Q-Network 的演进

强化学习的最终目的是找到一个策略,让智能体在环境里获得的累计奖励最大。Q-Learning 的思路是记住“在状态 s 下执行动作 a,未来能拿到多少折扣累计奖励”,这个值记作 Q(s,a)。每走一步就按公式更新一次:

Q(s,a) ← Q(s,a) + α × (r + γ × max a′ Q(s′,a′) − Q(s,a))

其中 α 是学习率,γ 是折扣因子,r 是当前奖励,s′ 是下一个状态。这个更新公式把所有线索都写在一个式子里:当前预测和“真实回报 + 下一步最优价值”之间的差距,叫作 TD 误差(时间差分误差)。问题在于,Q 表只能覆盖遍历过的状态,状态一多,内存、时间、泛化能力三头都堵死。

我倾向于把 DQN 理解为“用网络换表格”的升级:输入状态,输出每个动作的 Q 值。它保留了 Q-Learning 的更新逻辑,但把更新对象从表格里的一格变成一个可微的神经网络。于是原本对每个格子单独更新一次的方案,变成了“用一批样本更新网络权重,让整个状态空间都能被学到”。这个换法,让 DQN 能处理图像输入、连续状态、以及像迷宫这类离散但尺寸可变的任务。

后面这两节会拆开讲清楚:为什么一个看似不大的迷宫也能暴露出 Q 表的短板,以及 DQN 能稳定训练靠的四个结构设计。

2.1 为什么 Q-Table 在迷宫这种小状态空间里也能露馅

很多刚接触迷宫问题的同学会先质疑:一个 10×10 的迷宫只有 100 个格子,每个格子 4 种动作,Q 表最多 400 项,为什么不用 Q-Learning?这个质疑是对的,小规模迷宫确实不需要上深度强化学习。但真实的价值在于,当迷宫尺寸变大、起点和目标随机化之后,Q 表会立刻失效。

比如一个 20×20 的迷宫,墙和通道是有结构的,Q 表只能逐个格子记录路径,墙的变化、入口的变化都要求重新探索和重新存储。换成神经网络后,相邻格子因为输入特征相似,Q 值会被网络自然地“平滑”出来;即使地图换了一张同风格的迷宫,网络早期学到的特征也能迁移一部分。这个差异在代码里体现为:Q-Learning 需要一个二维数组,遍历全部状态;DQN 只需要一个前向推理函数。

另一个隐性问题在探索效率上。Q-Learning 每走到一个新格子,只更新当前格子的 Q 值,如果终点在迷宫另一边,早期完全碰不到奖励信号,Q 表就会一直处于稀疏状态。DQN 即使同样拿不到奖励,也会因为网络是全连接的,把“靠近墙还是远离墙”这类特征从大量 visited 状态里提出来。至少在路径规划这类任务上,神经网络在探索阶段的泛化优势是 Q 表无法覆盖的。

下面用 NumPy 跑一个 4×4 小迷宫的单步 Q-Learning 更新,看 TD 误差是怎么算的,这样后面理解 DQN loss 会顺很多。

import numpy as np # 4x4 迷宫,0 可走,1 是墙,最后一个是终点 maze = np.array([[0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 1, 0, 0]]) Q = np.zeros((4 * 4, 4)) # 状态数 * 动作数 alpha = 0.1 gamma = 0.99 # 假设当前在状态 0,执行动作 3(左移)到状态 1,奖励为 0 s, a, r, s_next = 0, 3, 0.0, 1 td_target = r + gamma * np.max(Q[s_next]) td_error = td_target - Q[s, a] Q[s, a] += alpha * td_error print("TD error:", td_error) print("Updated Q[0]:", Q[0])

这段代码里的gamma * np.max(Q[s_next])就是“下一步最优价值的估计”。如果终点奖励没被探索到,Q[s_next]全是 0,TD 误差只来自当前的小幅更新,学习会非常慢。这就是为什么迷宫问题里的奖励函数设计,比 Q-Learning 时代更重要。

2.2 DQN 的四个核心部件:Q 网络、目标网络、经验回放、奖励函数

DQN 的训练流程可以压缩成四个部件。第一个是 Q 网络,它的输入是状态特征向量,输出是每个动作的 Q 值。迷宫问题里状态就是格子坐标,一般用 One-Hot 或归一化后的坐标值。

第二个是目标网络。如果只有一个 Q 网络,计算 TD 误差时max Q(s_next)也来自同一个网络,相当于自己给自己打分,每次更新后评分标准就变,训练容易震荡甚至发散。目标网络的参数每隔若干步才从 Q 网络复制一次,让 TD 目标在一段时间内保持稳定。这个“延迟更新”是 DQN 能够在 2015 年训练出稳定效果的关键改进。

第三个是经验回放缓冲区。强化学习采样天然连续相关:刚走完格子 1,下一步多半是格子 2,如果直接用相邻样本来更新网络,梯度方向会反复横跳。经验回放把每一条(s, a, r, s_next, done)存进一个队列,训练时随机抽一批,相当于打断时间关联。迷宫环境的状态数量少,常被误以为不重要;实际上 DQN 在整个训练阶段能看到的数据都是靠这个缓冲区生成的,容量不够或采样方式不对,网络会反复忘记刚学到的东西。

第四个是奖励函数。迷宫任务的“本质目标”是到达终点,但奖励函数决定了网络从哪里获得梯度。如果只有到达终点给 1、其余全是 0,那么早期全部样本的 TD 目标都是 0,网络没有任何梯度方向的引导。需要在“到达终点”的大奖励之外,给每一步一个小的负惩罚,或者用势能差来给接近终点一个渐进信号。这一点我会在第三章具体展开。

我一般会建议新手在跑通代码前,先用这个框架去读源码:找到 Q 网络的定义、目标网络更新函数、回放缓冲区数据结构、奖励计算位置。四个位置能对上,DQN 就算入门了。

3. 把迷宫问题转成强化学习环境:状态、动作、奖励与终止条件

DQN 不是写一个模型然后喂数据就能跑,它需要一个完整的交互环境。环境定义方式直接决定训练的难度和代码的调试成本。迷宫问题虽然简单,但至少有六个细节要提前定好:状态如何编码、动作空间是几维、墙壁碰撞怎么处理、到达终点后怎么终止、奖励按什么规则给、是不是要把随机起点和随机终点也加进来。

3.1 迷宫的离散状态与动作空间设计

迷宫最直观的状态是(row, col)坐标。如果迷宫尺寸不大,直接把坐标归一化后拼成长度 2 的向量,网络也能学会;但更稳定的做法是用idx = row * width + col转成一个整数,再 One-Hot 成一个长度为width * height的稀疏向量。One-Hot 的缺点是维度会随迷宫尺寸变大,但好处是给网络提供了明确的“位置”语义,不会出现坐标归一化后在两个不同格子输入完全相同的情况。

动作空间通常定义为四个方向:0 上、1 右、2 下、3 左。这里有一个容易踩的细节:如果智能体执行动作后撞到墙或迷宫边界,一般有两种处理方式。第一种是原地不动,状态不变,给一个小负奖励;第二种是直接给终止信号。对 DQN 来说,我更推荐前一种,因为“撞墙”本身是有信息量的负反馈,能让网络学到边界的存在。直接终止则会减少一次有效探索,而且会让智能体倾向于待着不动。

终止条件只有两个:“走到终点”和“超出最大步数”。最大步数建议设为迷宫格子数的 3 到 5 倍,比如 10×10 的迷宫设 300 步。太小会切掉正常路径,太大会让智能体在无限绕圈中浪费时间。下面这个MazeEnv类可以直接用来跑 DQN,逻辑上跟 OpenAI Gym 风格一致。

3.2 用 Python 实现一个最简迷宫环境类

import numpy as np class MazeEnv: def __init__(self, maze, start=(0, 0), goal=(4, 4), max_steps=200): self.maze = np.array(maze) self.start = np.array(start) self.goal = np.array(goal) self.max_steps = max_steps self.action_space = 4 # 顺序:上、右、下、左 self.dirs = [(-1, 0), (0, 1), (1, 0), (0, -1)] self.state = None self.steps = 0 self.reset() def reset(self): self.state = self.start.copy() self.steps = 0 return self._get_obs() def step(self, action): self.steps += 1 row, col = self.state dr, dc = self.dirs[action] new_row, new_col = row + dr, col + dc # 撞墙或越界:原地不动,给一个小的负奖励 if (new_row < 0 or new_row >= self.maze.shape[0] or new_col < 0 or new_col >= self.maze.shape[1] or self.maze[new_row][new_col] == 1): reward = -0.1 done = False else: self.state = np.array([new_row, new_col]) if np.array_equal(self.state, self.goal): reward = 1.0 done = True else: reward = -0.01 done = False # 超过最大步数也直接结束 if self.steps >= self.max_steps: done = True return self._get_obs(), reward, done def _get_obs(self): row, col = self.state # 返回 One-Hot 向量 obs = np.zeros(self.maze.shape[0] * self.maze.shape[1]) obs[row * self.maze.shape[1] + col] = 1 return obs

这段代码里的关键设计有两个。第一,_get_obs返回的是长度为“格子总数”的 One-Hot 向量,而不是坐标元组;这样后面训练网络的输入层维度就和地图尺寸绑定,换地图时只需要改maze数组,不用重写网络。第二,奖励按“撞墙 -0.1、普通步 -0.01、到达终点 +1.0”设置,这个数量级差能防止 Q 值被单次大奖励带飞,同时又不至于让负奖励太大而掩盖终点信号。

max_steps这个参数在迷宫环境里不是可有可无。没有步数上限,智能体完全有可能在 oss 路径上反复横跳,训练永远不会终止,经验回放里会堆满无意义的重复样本。上限设成 200 步,至少保证一条完整 episode 的时间可控。

3.3 奖励函数怎么设:稀疏奖励与势能引导的取舍

迷宫任务的天然奖励只在终点出现,这是典型的稀疏奖励问题。稀疏奖励最直接的后果是:早期 epsilon 大,智能体乱走,能碰巧走到终点的概率极低;经验回放里存下来的样本绝大多数收益为 0,网络只能学到“原地待着”或“不要撞墙”,学不到方向。

最简单的修复方案是给每一步一个负惩罚。这个负惩罚的绝对值很敏感。如果设成 -0.01,100 步的 episode 累计也只有 -1,和终点 +1 接近,网络需要仔细权衡;如果设成 -0.1,智能体会倾向尽快结束游戏,甚至撞墙专门制造终止。我一个比较常用的策略是:普通步 -0.01,撞墙 -0.1,终点 +1.0。撞墙的惩罚要重于普通步,因为撞墙意味着行为错误,而且它会打断移动节奏。

进阶做法是势能引导(reward shaping),也就是根据“当前格子到终点的曼哈顿距离”来给奖励增量。每走一步,如果距离终点更近了,给一个小的正奖励;离终点更远则给负奖励。公式可以写成:

potential_old = distance(state, goal) potential_new = distance(next_state, goal) shaping = 0.1 × (potential_old − potential_new)

这种基于势能的奖励能显著加快收敛,但它有一个副作用:如果 shaping 系数设得太大,智能体会贪图近处的小奖励,忽略真正到达终点的大目标,甚至走出一条“距离越来越近但被墙挡住”的死路。我的经验是把 shaping 系数控制在 0.05~0.2 之间,并且只在训练的前半段开启,后半段逐渐关闭,让智能体回归到稀疏奖励下的真实目标。

4×4 迷宫可以不用势能引导,但如果你的目标是 20×20 以上的地图,强烈建议加 shaping。下面这段代码展示了怎么在原有step里嵌入势能奖励:

def _distance_to_goal(self, state): return abs(state[0] - self.goal[0]) + abs(state[1] - self.goal[1]) # 在 step 中,移动到新格子后: old_dist = self._distance_to_goal(self.state) new_dist = self._distance_to_goal(np.array([new_row, new_col])) shaping = 0.1 * (old_dist - new_dist) reward += shaping

加 shaping 之后要注意:网络 Q 值的目标不再是“纯终点奖励”,而是“势能差值 + 终点奖励”的组合。最后评估策略时,一定要去掉 shaping 再看成功率,避免被引导信号蒙蔽。

4. 用 PyTorch 实现 DQN 解决迷宫:核心代码与参数说明

环境就绪后,下一步是搭 DQN 训练脚本。很多人喜欢把训练逻辑和模型定义混在一起,迷宫问题本来就小,混着写能跑,但换任务就废。我的习惯是拆成四块:网络结构、回放缓冲区、动作选择策略、训练循环。下面按这个顺序给出可直接抄的代码,并标注每个参数的意义。

4.1 网络结构选择:几层全连接才够用

迷宫状态是 One-Hot 向量,动作只有 4 个,网络不需要很深。我的默认结构是三层全连接:输入层到 64、64 到 64、64 到 4。迷宫数量级在几百个格子以内,这个规模足够。层数再加深、神经元再加宽,对迷宫没有任何正向收益,只会拖慢训练并增加过拟合风险。

激活函数建议用 ReLU,最后一层不加激活,因为 Q 值需要输出任意实数。这里有个新手常犯的错误:在输出层加 Sigmoid 或 Tanh,把 Q 值限制在 [0,1] 或 [-1,1] 范围内,导致奖励大一些时梯度始终饱和,训练不收敛。迷宫奖励的绝对值通常小于 2,不用特殊缩放,但如果换到复杂任务,Q 值范围会远大于 [-1,1],输出层激活会让模型彻底失效。

import torch import torch.nn as nn class DQN(nn.Module): def __init__(self, state_size, action_size): super(DQN, self).__init__() self.net = nn.Sequential( nn.Linear(state_size, 64), nn.ReLU(), nn.Linear(64, 64), nn.ReLU(), nn.Linear(64, action_size) ) def forward(self, x): return self.net(x)

state_size就是迷宫格子总数,action_size固定为 4。如果换到图像输入,这个全连接网络需要替换成卷积层;但在迷宫路径规划中,One-Hot 输入用全连接就够了。

4.2 训练主循环:采样、回放、软更新/硬更新

训练主循环是 DQN 最核心的骨架。先看整体流程,再解释为什么每一步都不能省。代码里以下replay是一个简单的回放缓冲区,先用列表存数据,容量超过buffer_size后覆盖最老数据;采样时随机抽batch_size条。

import random from collections import deque class ReplayBuffer: def __init__(self, capacity=20000): 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) states, actions, rewards, next_states, dones = zip(*batch) return (torch.FloatTensor(np.array(states)), torch.LongTensor(actions).unsqueeze(1), torch.FloatTensor(rewards).unsqueeze(1), torch.FloatTensor(np.array(next_states)), torch.FloatTensor(dones).unsqueeze(1)) def __len__(self): return len(self.buffer)

采样后要把数据类型对齐:状态用 FloatTensor,动作必须要 LongTensor,因为后面要用gather按动作索引 Q 值;奖励和 done 都改成列向量,方便广播。done这个值尤其重要,如果终点的下一状态也参与 Q 值计算,网络会被错误地引导:终点之后没有任何动作,不能再往前看一步。

训练主循环如下:

def train_dqn(env, q_net, target_net, replay, optimizer, episodes=1000, batch_size=32, gamma=0.99, epsilon_start=1.0, epsilon_end=0.01, epsilon_decay=5000, target_update=500): epsilon = epsilon_start steps = 0 for episode in range(episodes): state = env.reset() done = False total_reward = 0 while not done: # 根据 epsilon 贪心选动作 if random.random() < epsilon: action = random.randrange(env.action_space) else: with torch.no_grad(): q_values = q_net(torch.FloatTensor(state).unsqueeze(0)) action = q_values.argmax(dim=1).item() next_state, reward, done = env.step(action) replay.push(state, action, reward, next_state, done) if len(replay) >= batch_size: states, actions, rewards, next_states, dones = replay.sample(batch_size) q_pred = q_net(states).gather(1, actions) with torch.no_grad(): q_next = target_net(next_states).max(dim=1, keepdim=True)[0] q_target = rewards + gamma * q_next * (1 - dones) loss = nn.MSELoss()(q_pred, q_target) optimizer.zero_grad() loss.backward() optimizer.step() steps += 1 epsilon = max(epsilon_end, epsilon_start * (1 - steps / epsilon_decay)) if steps % target_update == 0: target_net.load_state_dict(q_net.state_dict()) state = next_state total_reward += reward if episode % 50 == 0: print(f"episode {episode}, reward {total_reward:.2f}, epsilon {epsilon:.2f}")

这段代码里最难理解的可能是q_target = rewards + gamma * q_next * (1 - dones)。q_next是目标网络给出的下一状态最大 Q 值,但如果下一状态是终止状态,那么就不该再有未来回报,所以要用(1 - dones)把它清零。没有这一步,终点状态会被当成熟知世界里的普通状态,Q 值会被错误抬升。

主循环里还有两个关键机制没写全,但必须知道:一是经验回放只在len(replay)超过batch_size后才开始训练,否则缓冲区里样本太相关,梯度方向会抖动;二是目标网络必须用load_state_dict硬更新,更新频率我一般取 500 步。软更新(EMA)也可以用,但迷宫场景下硬更新更直观,代码也更好排查。

4.3 三个必调参数:学习率、epsilon 衰减、batch size

学习率是 DQN 里最玄学的参数之一。迷宫任务我一般从1e-3起步,如果 loss 反复震荡或训练曲线剧烈跳动,降到3e-4。学习率太大,Q 网络会忘记早期学到的稳定策略;学习率太小,500 步的目标网络更新根本来不及传播。推荐先用 TensorBoard 记录 loss,如果 loss 在 100 帧内上下浮动超过 5 倍,优先调低学习率。

epsilon 衰减步长epsilon_decay决定探索和利用的节奏。迷宫起点固定、地图固定,5000 步的衰减足够;如果起点随机,衰减步长要加倍,否则前期没探索全地图,后期 epsilon 已经很小,就会锁死在一条次优路径上。epsilon_start=1.0意味着开局完全随机,epsilon_end=0.01意味着后期几乎全用贪心策略。这个“几乎”很重要,因为环境或网络结构一变,保留 1% 的探索能帮助策略跳出局部最优。

batch size 在迷宫任务里影响不大,32是个稳妥值。调大64会让单步训练更稳定但会拖慢每步更新速度;调小16则收敛更快但更容易受异常样本干扰。经验回放缓冲区容量20000对迷宫足够,这个值的核心作用是避免旧样本被过度覆盖导致模型遗忘。如果你的迷宫更大、episode 更长,可以提高到50000。

5. 训练与评估:从模型不学到能走出迷宫,我踩过的四个坑

这部分内容不是理论推演,是我用迷宫任务调 DQN 时真实翻车后记下来的排查清单。每一条都按照“现象 → 原因 → 解决”的顺序写,希望在你能直接对照自己的训练曲线。

5.1 loss 在降,智能体却一直撞墙:目标网络更新频率设错了

我遇到过最迷惑的场景是:loss 从 1.2 稳定降到 0.3,但把 epsilon 调到 0 之后,智能体还是原地乱撞,成功率始终为零。表面看模型“学得越好”,实际行为越糟糕。排查后发现,问题出在目标网络更新频率上。

原因:目标网络每 50 步就复制一次 Q 网络参数,TD 目标里的max Q_next和预测 Q 值来自几乎同一个网络,每步更新后目标也同步漂移,梯度的“靶心”一直在动。Q 值看似被压平,实际网络只是把当前路径上的 Q 值统一压低,并没有区分好与坏。

解决:把target_update调到 500 到 1000 步。迷宫每个 episode 大概 100 步左右,500 步相当于让目标网络稳定 5 个完整回合,足够让它成为“慢半拍”的稳定参考。如果还不行,可以改成软更新,每步做target_net = tau * target_net + (1 - tau) * q_net,tau 取 0.005。硬更新适合迷宫,软更新适合你已经确认是自己写训练循环有问题的场景。

5.2 epsilon 衰减过快,智能体没探索完地图就“锁死”了

另一种经典翻车是:训练中期成功率到 60%,后期反而掉到 20%,而且策略固定在一条绕远路线上。把 epsilon 打印出来才发现,训练还没走到 3000 步,epsilon 已经衰减到了 0.01。此时几乎没有随机动作,智能体只能沿着已经学会的次优路径反复跑,永远跳不出局部最优。

原因:我一开始把epsilon_decay设成 1000,原以为 1000 步足够,但忽略了一条路径上的有效探索步数只有几十步,随机探索真正覆盖的状态范围非常小。尤其是终点在迷宫对角线方向时,靠随机行走碰到终点的概率极低。

解决:把衰减步长调成5000甚至10000,同时让 epsilon 的下降曲线不是一条直线,而是先用 20% 的步长从 1.0 降到 0.5,剩余步长再慢慢降到 0.01。最简单做法是在train_dqn里打印每 100 步的 epsilon,确保 epsilon 从 1.0 到 0.01 至少经过了 3000 个有效训练步。如果训练到中途成功率不升反降,先别调网络,先把 epsilon 曲线拉出来看。

5.3 固定随机种子之后结果依然不同:环境与网络用两套随机源

很多人问我,为什么设置了torch.manual_seed(42)和np.random.seed(42),训练结果还是每次都不同。这个问题我排查了很久,最后发现是 Python 的random库没有设种子。

原因:训练代码里选 epsilon 随机动作和回放缓冲区采样用的都是random.random()与random.sample,它们属于 Python 的全局随机源。PyTorch 的torch.manual_seed管不到它,np.random.seed也管不到它。只要random库种子不固定,环境的探索路径和采样分布每次都会变,训练结果自然天差地别。

解决:在训练前把三个随机源一起设上:

random.seed(42) np.random.seed(42) torch.manual_seed(42)

如果使用 CUDA,还要额外设置torch.cuda.manual_seed_all(42)。重置环境后,再跑两次同样代码,loss 和成功率曲线应当完全一致。这个操作不是“为了复现而复现”,而是让你在调参时能确定改动的是参数还是运气。固定种子后,如果发现同一组参数跑两次结果还不同,说明代码里有未覆盖的随机源,继续排查,不要急着调参。

5.4 Q 值一路涨到上万,loss 变成 inf:奖励没缩放、梯度也没裁剪

迷宫奖励设计成 +1 和 -0.01,理论上 Q 值不会超过 2。但有次我把奖励改成“距离差×10”之后,Q 值直接在 300 帧内冲到 5000,loss 变成inf。这就是奖励量级被放大的连锁反应。

原因:神经网络的输出没有天然边界,如果 TD 目标里的奖励项大到几十甚至一百,网络就会不断放大预测值去追赶,权重变得巨大,梯度也进入爆炸区间。迷宫这种小任务尤其容易忽略这个问题,因为 +1 的奖励太温和,换任务后才爆发。

解决:第一条防线是奖励缩放,把所有奖励除以一个常数,把量级压到 [-2, 2] 之间。第二条防线是梯度裁剪,在loss.backward()后加一行:

nn.utils.clip_grad_norm_(q_net.parameters(), max_norm=1.0)

这样即使个别样本产生超大梯度,更新步长也受限,网络权重不会一次性被冲崩。第三条防线是调整学习率,如果 reward 量级已经缩好但 Q 值还在涨,再考虑降学习率。这三条要按顺序检查,不要一上来就换网络结构。

6. 把迷宫换成连续任务前,先用可视化验证 Q 值是不是真学会了

训练曲线在下降不代表策略正确,成功率达标也不能排除过拟合。走完迷宫任务后,我还会做三件事,用来确认模型真的学到了空间结构而不是背下了路径。

第一,画 Q 值热力图。把迷宫里每个格子单独作为输入,通过网络得到四个动作的 Q 值,取最大值,然后按格子位置填色。理论上终点附近格子热力高,起点附近热力低,墙边和死角热力明显偏低。如果热力图一片模糊,说明网络根本没学到空间关系,只是靠大概率走到终点。

import matplotlib.pyplot as plt q_map = np.zeros((env.maze.shape[0], env.maze.shape[1])) for row in range(env.maze.shape[0]): for col in range(env.maze.shape[1]): idx = row * env.maze.shape[1] + col obs = np.zeros(env.maze.shape[0] * env.maze.shape[1]) obs[idx] = 1 with torch.no_grad(): q_value = q_net(torch.FloatTensor(obs).unsqueeze(0)).max().item() q_map[row][col] = q_value plt.imshow(q_map, cmap='hot') plt.colorbar() plt.show()

第二,用贪心策略连续跑 100 局,统计成功率和平均步数。注意这里要把epsilon设成 0,并且关闭任何 reward shaping,只按 Q 值最大动作走。成功率要高于 95%,平均步数要比随机策略有明显下降。如果成功率达标但平均步数异常高,多半是策略里还有绕圈动作。

第三,把训练曲线、Q 值热力图、成功局数三条信息放到一起看。训练曲线的 loss 和成功率往往不完全同步,loss 下降快但成功率提升慢是正常的;反过来,如果成功率提升但 Q 值热力图混乱,可能就是过拟合当前初始位置。我会习惯用%形式记录一次调参前后这三项指标的变化,而不是只记录 loss。

这套“可视化 Q 值 + 贪心评估 + 多指标对比”的习惯,在我后来做更复杂的路径规划和连续控制任务时也一直在用。很多训练问题如果只盯曲线,会被表面看似正常的 loss 骗过;把 Q 值和行为结果直接画出来,往往一眼就能看出问题。

希望你也能在这份源码基础上,先跑通迷宫,再去改地图、改奖励、改网络结构。只要训练脚本骨架不被破坏,DQN 这套机制在更多任务里就能直接复用。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 5:55:49

随身WiFi AT指令调试:多芯片串口通信适配方案

随身WiFi AT指令调试&#xff1a;多芯片串口通信适配方案 做随身WiFi开发的同学都知道&#xff0c;这块产品的芯片方案太杂了。中兴微、ASR、展锐三大阵营各自的AT指令集不完全兼容&#xff0c;调试时经常遇到同一个功能在不同芯片上返回格式不一样的问题。2026年随身WiFi市场…

作者头像 李华
网站建设 2026/9/30 5:55:26

Wireshark抓包分析HTTP协议:从实验到实战的完整指南

简介&#xff1a;这是一份面向计算机网络课程学习者与实验备考学生的Wireshark HTTP协议分析实验报告&#xff0c;围绕抓包工具的实际使用与协议报文解析展开&#xff0c;适合正在完成课程实验、准备网络原理考核或希望夯实应用层协议基础的读者。压缩包内共1个docx文档&#x…

作者头像 李华
网站建设 2026/9/30 5:54:37

GTK入门实战:从零打造Linux原生图形界面

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 5:53:59

企业级AI日报系统:微信服务号合规触达全链路实践

1. 项目概述&#xff1a;这不是一个“发消息”的功能&#xff0c;而是一套轻量级企业级通知中枢“我给 WorkBuddy 设了个闹钟&#xff1a;每天上午十点半&#xff0c;一份 AI 日报自动送进微信”——这句话乍听像极了个人效率小技巧&#xff0c;但实际落地时&#xff0c;它瞬间…

作者头像 李华
网站建设 2026/9/30 5:53:21

医疗AI落地实战:从合规切入到私有化部署的避坑指南

1. 医疗AI落地的第一道门槛&#xff1a;先搞清楚什么能碰、什么不能碰医疗这个行业跟别的行业有个本质区别&#xff1a;别的行业做AI&#xff0c;做错了顶多是用户体验差一点、效率低一点&#xff1b;医疗AI做错了&#xff0c;可能直接涉及患者的生命健康和合规红线。我见过不少…

作者头像 李华
网站建设 2026/9/30 5:53:20

FreeRTOS任务设计本质:不是多线程,而是确定性并发建模

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华