news 2026/10/21 5:09:42

用Q-learning解随机迷宫:Matlab实现与参数调优全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用Q-learning解随机迷宫:Matlab实现与参数调优全解析

很多初学者接触强化学习,都是从看公式、推导Bellman方程开始的,但翻完整本教材,合上书还是想不清楚这套东西到底怎么跑起来。我建议这类朋友直接上手做一个小项目,比如这个用Q-learning解随机迷宫的问题。

项目名字很直白,就是解决一个随机生成的方形迷宫,核心方法不用深度学习那套复杂的东西,只靠一张Q表和两条关键策略:Q-learning算法和ε-greedy策略,实现工具是Matlab。虽然是个教学级Demo,但它把强化学习最精髓的部分全占齐了——状态怎么编码、动作怎么定义、奖励怎么设计、智能体怎么通过试错学会"不走回头路",每一步都有得玩。这篇文章就带你完整过一遍这个项目的设计与实现,代码逻辑、参数调优、踩坑记录都会讲到,边看边写,跑通之后再回头看RL课本,你会觉得很轻松。

1. 项目概述与整体设计思路

1.1 为什么选迷宫作为入门场景

迷宫问题在强化学习里的地位,几乎等同于"Hello World"在编程里的地位。它天然具备强化学习的三个核心要素:状态空间可枚举、动作空间离散且有限、回报信号稀疏但有明确指向。你需要做的事就是让一个"什么都不知道"的智能体在一个完全陌生的迷宫里摸索,从随机乱撞开始,经过大量回合的试错,最终学会一条从起点到终点的最优路径。

这种问题的好处是验证成本极低。一个格子地图,状态都用坐标表示,动作只有上下左右四种,Q表的大小也不过是格子数乘以4。相比自动驾驶、游戏AI那种动辄上百万状态的高维问题,迷宫让你把注意力完全集中在算法本身,而不是工程细节上。

而且它和真实问题有一个很强的共性:回报稀疏,探索困难。迷宫不是每走一步都有奖励的,很多时候走了一百步什么都没发生,只有踩到终点或撞到墙上才能获得反馈。这种"长时间没有反馈也得坚持探索"的特性,就是强化学习和监督学习最不一样的地方。

1.2 技术方案整体拆解

从实现上看,这个项目可以拆成五个模块:

  • 迷宫生成模块:随机生成一张方形迷宫地图,保证从起点到终点存在一条通路,并且路径本身带有一定的复杂度。
  • 环境交互模块:就是智能体脚下的"物理世界",接收当前状态和动作,返回下一个状态、即时奖励和是否终止的标记。
  • Q-learning训练模块:维护一张Q表,按照时序差分公式迭代更新,这是整个项目的核心大脑。
  • ε-greedy策略模块:在"探索尝试新路径"和"利用已有经验"之间平衡,负责在每一步选择动作。
  • 结果可视化模块:把迷宫画出来,把训练过程的曲线画出来,最重要的是把智能体最终学到的路径动画展示出来。

我个人比较建议用Matlab来做,原因有几个。一是它的矩阵操作非常顺手,迷宫地图本身就是二维矩阵,状态坐标直接可以当索引用。二是Matlab自带的绘图工具做可视化和动画极其方便,几行代码就能把迷宫和智能体路径画出来,不用像Python那样折腾一堆绘图库。三是调试方便,你可以在训练循环里随时停下来查看任何变量的值。

1.3 核心难点在哪

看着模块不少,真正的核心难点其实就一个:如何让智能体在稀疏奖励的环境下持续学习,而不是随便撞几下就放弃。

这个问题直接决定了你的代码能不能收敛。解决办法主要靠两条腿走路:一条是设计合理的奖励函数,让智能体在迷宫中行走时获得足够的引导信号;另一条是设计合理的ε-greedy策略,让智能体前期多探索,后期多用经验。这两块的参数设置互相影响,是整个项目最需要反复调整的地方。

2. 迷宫生成与环境建模解析

2.1 用深度优先搜索生成随机迷宫

迷宫生成方式有很多种,比如递归分割、随机Prim算法、深度优先搜索回溯法。我选择的是DFS回溯法,它的特点是生成的迷宫路径比较蜿蜒、死角多、结构复杂,很适合用来测试Q-learning的收敛能力。

DFS回溯法的核心思路是:从起点方格开始,随机选择一个方向前进,把访问过的格子标记掉。如果当前格子的所有相邻格子都被访问过,就往回退一步,继续探索。每次在两个格子之间打通墙壁,就能形成一棵生成树,保证任意两个格子之间有且仅有一条通路。

在Matlab里,迷宫可以用一个二维矩阵表示:

  • 0表示墙壁,不可通行
  • 1表示通道,可通行

一个N x N的迷宫总共有N*N个格子,但要注意边界处理。更常见的做法是生成一个2*N+1大小的矩阵,奇数索引是格子,偶数索引是墙壁。比如maze(1, 1)是起点格,maze(2, 1)是墙壁,maze(1, 2)也是墙壁。

我生成的迷宫默认是从左上角到右下角,起点和终点固定。这里有个小设计决策:不用随机选终点,固定右下角作为目标,方便控制训练难度,也让可视化更直观。

2.2 状态空间、动作空间与状态编码

在强化学习中,状态空间指的是智能体所能感知到的所有情况的集合。在迷宫问题里,状态就是格子坐标,即一个二元组(row, col)。

为了效率,我在Matlab里没有直接用二元组,而是把状态编码成一个线性索引。假设迷宫是50x50,坐标为(r, c)的格子,线性索引就是(r-1)*cols + c。这样做的好处是Q表可以直接用状态索引作为行号,不需要自己去维护坐标和索引之间的映射表。

动作空间定义相对简单,就是四个离散动作:上、下、左、右。在Matlab代码里我用数字1到4来表示。状态转移规则就是根据当前坐标计算新坐标,然后查迷宫矩阵判断新坐标是不是墙壁。

这里有一个容易忽略但特别重要的细节:如果智能体撞墙了,应该怎么处理?

很多初学者的做法是:撞墙就不动,原地停留,给一个负奖励。但这样做有个毛病,就是智能体容易被"粘"在墙边,反复尝试撞墙。我的处理方式稍微不同:撞墙时状态保持不变,给一个明显的负奖励,比如-10或者-5。这样做的意图是让智能体学会"撞墙是一个动作结果,而不仅仅是一个惩罚信号",它需要把"在当前状态下选择这个动作会导致负回报"关联起来。

2.3 奖励函数的设计思路

奖励函数是这个项目成败的关键。在迷宫问题里,常见的做法是:

  • 到达终点:奖励+100
  • 撞墙:奖励-10
  • 每走一步:奖励-1

这个设计里最微妙的是"每走一步扣一分"。表面上看是惩罚,实际目的是压缩路径长度。如果没有任何步数惩罚,智能体完全可能找到一条路径,但这条路绕了远路——因为到达终点的奖励是一样的,走再远也没有额外损失。加了步数惩罚之后,走的路越短,累计奖励越高,Q-learning就会自动偏好短路径。

有人会问,步数惩罚会不会导致智能体连探索都畏首畏尾?确实有这个问题,所以-1这个值不能太大。我实测下来,用-1作为每步惩罚、+100作为终点奖励、-10作为撞墙惩罚,在10x10到50x50规模的迷宫上都能顺利收敛。如果迷宫很大,比如边长超过30,可以把撞墙惩罚适当调小,否则智能体前期会因为频繁撞墙而变得过度保守。

2.4 起点与终点的可达性保证

DFS生成的是完美迷宫,理论上有且仅有一条通路连接任意两个格子。但这里有一个实现细节得特别注意:DFS生成迷宫时,起点和终点如果都在边界角落,路径长度往往会非常长且曲折。

迷宫越大,路径越长,Q-learning需要探索的状态空间就越大,训练回合数相应也会增加。所以我建议,初学阶段先用10x10或者15x15的迷宫来验证代码逻辑,跑通之后,再逐步增大到20x20、30x30。不要一上来就整50x50,等一下训练半天看不到效果,会严重打击学习积极性。

3. Q-learning核心原理与ε-greedy策略深度解析

3.1 Q-learning的迭代公式到底在干嘛

Q-learning的核心是一个叫做Q表的数据结构,它是"状态-动作"的价值估计表。Q(s, a)表示在状态s下执行动作a的长期期望回报。这个值不是一开始就知道的,而是通过不断试错迭代出来的。

每次智能体执行一个动作,环境会反馈一个即时奖励r和下一个状态s'。Q-learning使用如下公式更新Q表:

Q(s, a) = Q(s, a) + α * [r + γ * max(Q(s', a')) - Q(s, a)]

这个公式看着有点吓人,拆开其实很直观。r + γ * max(Q(s', a'))是目标值——即当前动作带来的即时奖励加上未来能获得的"期望最大收益"。Q(s, a)是当前估计值。两者相减就是"预测误差",也叫TD误差。α是学习率,控制误差以多大幅度修正当前的估计值。

一个生活化的类比:你第一次去一家新餐馆,凭直觉点了招牌菜,吃完感觉不错。下次你想再去,但不确定点什么好,于是你回忆上次的体验,结合菜单上看起来最诱人的选项来做决定。Q表的更新就是这样——它不只是在记录发生过的事,而是在不断修正"这条路径到底值不值得选"的判断。

在这个公式里,max(Q(s', a'))这个操作特别关键。它意味着我们总是用下一个状态中最好的那个动作的价值来更新当前状态的价值,这是Q-learning作为离线策略算法的精髓——学的不一定是你正在执行的动作,而是你理论上应该选择的动作。

3.2 ε-greedy:探索与利用的平衡术

如果智能体只知道选择当前Q值最大的动作,它很可能一开始就死磕某条不是最优的路径,因为它的经验还不够丰富。反之,如果它完全随机选择动作,那么它永远学不会任何稳定策略。这就是强化学习里著名的"探索与利用困境"。

ε-greedy策略简单粗暴地解决了这个问题:

  • 生成一个随机数
  • 如果小于ε,就随机选择一个动作(探索)
  • 否则选择当前Q值最大的动作(利用)

在训练开始时,ε一般设置得比较大,比如0.9,让智能体像无头苍蝇一样到处乱逛,尽可能多地覆盖状态空间。随着训练进行,ε慢慢衰减,智能体逐渐从"探索者"变成"老司机"——大部分时间走已知的好路径,偶尔尝试新可能。

我用一个例子说明为什么会需要这个策略。想象你每天都固定走同一条路上下班,时间长了肯定不会堵车、时间最优吗?不一定。真正熟悉一个城市的人,会在某个路口偶尔尝试一条没走过的路线,也许发现了一条更近的捷径。这就是ε的价值,它让智能体不满足于现有方案,永远保留"发现更好路径"的可能性。

3.3 目标策略与行为策略的区别

很多初学者完全忽略了这个点,但它是理解Q-learning的必经之路。

在训练时,智能体实际执行动作用的是ε-greedy策略,这叫做行为策略。但在Q表更新时,我们对下一个状态取的是max(Q(s', a')),也就是假设下一步采取贪心策略时能获得的价值,这叫做目标策略。

行为策略可以探索,目标策略始终贪心,两者互相独立。这个"离策略"特性就是Q-learning名字里"off-policy"的由来。它能用一个相对随意的探索策略,去学习一个相对确定的目标策略,实际效果往往比边执行边反思的在线策略算法更快、更稳定。

在实现上,这意味着你不需要修改Q表更新的核心公式,只需要把动作选择逻辑单独封装成一个函数,在训练时传入不同的ε值就可以了。

3.4 三个关键参数的理解与设置

这里我把Q-learning的三个核心参数整理成一个表格,方便对照理解:

参数含义取值范围我的经验取值设置思路
α (学习率)新信息对旧估计的修正力度0到10.1 ~ 0.5越大学习越快但容易震荡,越小越稳定但收敛慢
γ (折扣因子)未来奖励的权重0到10.9 ~ 0.99越接近1越重视远期收益,在迷宫中应设大
ε (探索率)随机探索的概率0到1初始0.9,衰减到0.1前期探索,后期利用,衰减方式也影响收敛

学习率α的选择比较微妙。理论上,当环境是确定性的(迷宫就是确定性的,同样的状态下执行同样的动作,结果完全一样),α取大一点没关系,比如0.5甚至0.9都行。我是习惯取0.1,虽然慢一点,但胜在稳定,尤其在迷宫规模较大时,不容易出现Q值来回震荡的情况。

折扣因子γ在迷宫问题上应该设得比较大。因为终点奖励是+100,而这个奖励可能要隔几十步才能拿到,如果γ太小,未来奖励会被打很多折扣,智能体可能"看不清"远处的终点。我用0.9起步,在20x20以上的迷宫测试时会调到0.95到0.99。

4. Matlab代码实现与关键细节

4.1 迷宫生成的Matlab实现

先看迷宫生成函数。下面是用DFS回溯法生成方形迷宫的代码,我加了一些中文注释说明关键步骤:

function maze = generateMaze(n) % n 是迷宫边长(格子数),生成 (2*n+1) x (2*n+1) 的迷宫矩阵 % 矩阵元素 0 表示墙,1 表示通道 totalSize = 2 * n + 1; maze = zeros(totalSize, totalSize); % 挖空所有奇数索引的格子(这些是真正的通道候选) for r = 2 : 2 : totalSize - 1 for c = 2 : 2 : totalSize - 1 maze(r, c) = 1; end end % 记录每个格子的访问状态 visited = false(n, n); % DFS回溯法 % 用栈存储路径 stack = [1, 1]; % 当前在(1,1)格子,对应迷宫矩阵(2,2) visited(1, 1) = true; % 方向:上、下、左、右 (对应格子坐标变化) dirs = [-1, 0; 1, 0; 0, -1; 0, 1]; while ~isempty(stack) cur = stack(end, :); % 查看栈顶 r = cur(1); c = cur(2); % 找出所有未访问的邻居 candidates = []; for d = 1 : 4 nr = r + dirs(d, 1); nc = c + dirs(d, 2); if nr >= 1 && nr <= n && nc >= 1 && nc <= n && ~visited(nr, nc) candidates = [candidates; nr, nc, d]; end end if isempty(candidates) % 没有未访问邻居,回溯 stack(end, :) = []; else % 随机选择一个邻居 idx = randi(size(candidates, 1)); nr = candidates(idx, 1); nc = candidates(idx, 2); dIdx = candidates(idx, 3); % 打通当前格子与邻居格子之间的墙壁 % 当前格子映射到迷宫矩阵:(2*r, 2*c) % 邻居格子映射到迷宫矩阵:(2*nr, 2*nc) % 中间墙壁:(r+nr, c+nc) maze(r + nr, c + nc) = 1; % 将邻居格子标记为通道 maze(2 * nr, 2 * nc) = 1; % 标记访问并入栈 visited(nr, nc) = true; stack = [stack; nr, nc]; end end end

这段代码的核心逻辑在while循环里。每走一步,先收集所有未访问邻居;如果有,随机挑一个,打通相邻墙壁,把邻居入栈;如果没有,就出栈回到上一个格子继续找岔路。

这里有个细节值得注意:maze(r + nr, c + nc) = 1这一行实际打通的是两个格子之间的那面墙。因为在迷宫矩阵中,两个相邻格子分别位于(2*r, 2*c)和(2*nr, 2*nc),它们的中点恰好在(r+nr, c+nc)。把这里置为1,就把两个格子连通了。

4.2 环境交互与移动函数

生成迷宫之后,需要编写一个环境函数来模拟智能体的动作。这里我没有把所有逻辑塞进一个函数,而是拆成了两段:一段是纯粹的坐标移动判断,另一段是完整的训练循环。

动作执行的代码:

function [nextState, reward, isTerminal] = mazeStep(state, action, maze, goalState, n) % state 是线性索引,换算成行列坐标 [r, c] = ind2sub([n, n], state); % 根据动作计算新坐标 switch action case 1 % 上 nr = r - 1; nc = c; case 2 % 下 nr = r + 1; nc = c; case 3 % 左 nr = r; nc = c - 1; case 4 % 右 nr = r; nc = c + 1; end % 边界检查 if nr < 1 || nr > n || nc < 1 || nc > n nextState = state; reward = -10; isTerminal = false; return; end % 墙壁检查(迷宫矩阵坐标要乘以2) if maze(2*nr, 2*nc) == 0 nextState = state; reward = -10; isTerminal = false; return; end % 正常移动到新格子 nextState = sub2ind([n, n], nr, nc); % 判断是否到终点 if nextState == goalState reward = 100; isTerminal = true; else reward = -1; isTerminal = false; end end

这段代码的逻辑非常直白,但有几个容易踩的坑。第一个坑是sub2ind和ind2sub的维度参数必须一致。我在调试时曾犯过一个错误:生成迷宫用的是更大的矩阵尺寸,结果用n去转换索引,匹配不上,程序直接报错。第二个坑是墙壁判断,迷宫矩阵里格子的位置是(2*nr, 2*nc),而不是(nr, nc),少乘2就会让智能体"穿墙"。

4.3 Q-learning训练主循环实现

有了环境和迷宫,训练主循环的代码就清爽多了:

function [Q, episodeRewards, bestPath] = trainQlearning(maze, n, params) % 参数解析 alpha = params.alpha; % 学习率 gamma = params.gamma; % 折扣因子 epsilon = params.epsilon; % 初始探索率 epsilonMin = params.epsilonMin; epsilonDecay = params.epsilonDecay; numEpisodes = params.numEpisodes; % 状态数 = 格子数 numStates = n * n; numActions = 4; Q = zeros(numStates, numActions); % Q表初始化 % 起终点状态编码 startState = 1; % 左上角 goalState = n * n; % 右下角 episodeRewards = zeros(numEpisodes, 1); bestReward = -inf; bestPath = []; for episode = 1 : numEpisodes state = startState; totalReward = 0; path = []; while true % 使用epsilon-greedy选择动作 if rand() < epsilon action = randi(numActions); else [~, action] = max(Q(state, :)); end % 执行动作 [nextState, reward, isTerminal] = mazeStep(state, action, maze, goalState, n); % 计算TD误差并更新Q表 % 注意状态结束时不考虑未来奖励 if isTerminal tdError = reward - Q(state, action); else tdError = reward + gamma * max(Q(nextState, :)) - Q(state, action); end Q(state, action) = Q(state, action) + alpha * tdError; % 记录路径 if ismember(state, path) == false path = [path; state]; end totalReward = totalReward + reward; state = nextState; if isTerminal break; end end episodeRewards(episode) = totalReward; % 记录最佳路径(按累计奖励判断) if totalReward > bestReward bestReward = totalReward; bestPath = path; end % 探索率衰减 epsilon = max(epsilonMin, epsilon * epsilonDecay); end end

关于这段代码,有几个细节我想解释清楚。第一个是Q表初始化为0。这里其实有一个很经典的设计选择:如果你初始化为全0,那么智能体在第一次探索时,对"未尝试过的状态"没有特殊偏好;如果你初始化成一个较大的正值(比如50),智能体会倾向于先把所有状态都逛一遍,因为反正每一条路的估计值都是高的,只有尝试过才知道真实价值。这叫做乐观初始化。它对探索是有帮助的,尤其在奖励稀疏问题里,能避免智能体因为初期Q值太低而彻底放弃访问某些区域。

第二个细节是tdError的计算在终止状态下不要加gamma * max(Q(nextState, :))。原因是终止状态之后没有后续奖励了,如果硬用公式,会引入一个根本不存在的"未来价值"。

第三个细节是探索率衰减。常见做法是epsilon = epsilon * epsilonDecay,每回合缩小一点,epsilonDecay取0.995到0.999。这意味着训练前期探索多,后期探索少,但始终保留一小部分随机性(epsilonMin通常取0.01到0.1),防止过早收敛到局部最优。

4.4 训练收敛的判断方法

训练完之后怎么判断模型是否真的学会了?最直观的方法是看每回合的总奖励曲线。如果episodeRewards随训练进行逐渐上升并趋于稳定,说明智能体在稳定地找到终点。但只靠"到达终点"还不够,因为到达终点的路径可能很长。更精细的判断指标有两个:

  • 平均每回合步数是否在下降并稳定
  • 最佳路径长度是否收敛到一个稳定的数值

我在项目中额外添加了一个步数统计变量,每走一步就加一,最后画出"步数随回合数变化"的曲线。通常你会看到这条曲线先高后低,最后在一个平台期波动,那个平台期对应的就是当前迷宫下能找到的近似最优路径长度。

5. 可视化实现与参数调优实战

5.1 迷宫与路径的绘制方法

可视化的价值被严重低估了。代码能跑通是一回事,亲眼看智能体在迷宫里一步步走向终点,是另一种完全不同的体验,而且调试效率会高很多。

在Matlab里画迷宫很简单。迷宫矩阵可以当作黑白图像直接显示:

function plotMaze(maze) figure; colormap(gray); imagesc(maze); axis equal; axis off; end

其中maze矩阵中的1显示为白色(通道),0显示为黑色(墙壁)。

要把智能体的路径叠加上去,只要把路径上每个格子的坐标换算成迷宫矩阵坐标,然后用plot函数画一条折线:

function plotPathOnMaze(maze, path, n) hold on; x = zeros(length(path), 1); y = zeros(length(path), 1); for i = 1 : length(path) [r, c] = ind2sub([n, n], path(i)); y(i) = 2 * r - 1; % 格子中心在迷宫矩阵中的行坐标 x(i) = 2 * c - 1; % 列坐标 end plot(x, y, 'r-', 'LineWidth', 2); end

这里有个容易搞混的点:图像坐标系里,imagesc显示矩阵时,第一行在最上面,第二行在下面,这和我们直觉里的"上北下南"是一致的。但plot画折线时,y轴默认向上,所以如果直接用矩阵索引当坐标,画出来的路线会上下颠倒。正确的做法是只关注相对位置,只要你画的路径和迷宫对齐显示即可。我在代码中特意用了格子中心坐标(2*r-1, 2*c-1),就是为了让路径点和迷宫的通道中心精确重合。

如果你想看完整的动画过程,可以在每次选择动作后更新智能体的位置标记,然后加上短暂的延迟:

h = plot(x_current, y_current, 'bo', 'MarkerSize', 8); drawnow; pause(0.01);

5.2 一组实测参数与收敛效果对比

我在不同迷宫规模下做了几组测试,下面这张表是其中比较有代表性的结果。迷宫规模指格子边长,路径长度指智能体最终学会的最短路径步数,收敛回合数指累计奖励开始保持稳定的回合序号:

迷宫规模格子总数学到的最短路径步数收敛所需回合数采用的α/γ/ε初始值
10x1010022 ~ 30200 ~ 4000.1 / 0.9 / 0.9
20x2040045 ~ 701000 ~ 20000.1 / 0.95 / 0.9
30x3090080 ~ 1303000 ~ 60000.1 / 0.99 / 0.9

注意,这里的路径步数是"格子到格子的移动次数",不是曼哈顿距离,因为迷宫本身是曲折的。可以看到,随着迷宫规模增大,收敛回合数增长得非常快。这就是为什么我一直强调从小迷宫入手。3000回合的训练在Matlab里虽然也就几十秒,但如果你连逻辑错误都没排除掉,一次调试就要跑几十秒,体验非常煎熬。

5.3 超参数对训练结果的影响实录

我自己调整超参数时记录过几组有意思的现象。第一组是学习率α的影响:

  • α=0.01时,训练明显变慢,2000回合的10x10迷宫仍未完全收敛。
  • α=0.1时,收敛顺畅,曲线平稳。
  • α=0.9时,前期快速上升,但后期奖励曲线有轻微震荡,路径偶尔会跳变。

这说明在确定性环境中,虽然理论上α取大了没问题,但实际会因为偶尔的探索动作而产生较大的Q值修正,导致已学到的稳定策略被偶尔打断。所以中庸的0.1其实是很合理的选择。

第二组是折扣因子γ的影响:

  • γ=0.5时,10x10迷宫中智能体经常学到一条"短视"路径,因为它只关注眼前几步的奖励,远处的终点在价值评估里被严重压缩。
  • γ=0.9时,路径明显更优,收敛也快。
  • γ=0.99时,收敛速度和γ=0.9差不多,但训练曲线更加平滑。

第三组是ε初始值的影响:

  • ε从0.1开始:智能体前期几乎不探索,容易陷在起点附近的死胡同里,收敛极慢。
  • ε从1.0开始:前期完全是随机游走,但探索充分,一旦Q表建立起基本认知,后续收敛非常快。
  • ε初始0.9、衰减率0.99:综合效果好,曲线最平稳。

我现在的建议是:10x10迷宫用α=0.1, γ=0.9, ε=0.9, ε衰减=0.995, εMin=0.1跑;20x20以上迷宫把γ调高到0.95或0.99。

5.4 动画里看出的训练阶段

如果你把训练过程录成动画,会观察到智能体的行为模式大概分三个阶段。第一个阶段是"乱撞期",智能体就像喝了酒的醉汉,到处碰壁,偶尔路过终点却浑然不知,绕着圈走了。这个阶段对应的Q表几乎还是全零,动作全凭ε决定。

第二个阶段是"萌芽期",智能体碰巧成功到达终点几次之后,终点附近的Q值开始变高,在终点周围几个格子,智能体会明显表现出"直奔终点"的倾向,但离终点远的地方仍然是随机乱逛。

第三个阶段是"稳定期",Q表已经形成了从起点到终点的一条清晰的"价值走廊"。当你把ε降到0.1以下时,智能体几乎每次都能走出一条相同的最优路径。这时候把迷宫画出来看,那条红线和迷宫里的唯一路径完全重合,非常治愈。

这个三阶段现象很有价值。它说明了Q-learning不是一步到位学会整个任务,而是从目标开始,一点一点把价值信息"反向传播"回起点附近的区域。理解了这一点,你就理解了为什么迷宫越大收敛越慢——价值信息传播的距离更长了。

6. 常见问题与排查技巧实录

6.1 智能体一直原地打转,走不出起点附近

新手最常见的问题就是训练了半天,智能体始终在起点附近几个格子来回移动,根本走不远。我在调试时碰到的第一个此类问题,原因是奖励设计不合理。一旦每步惩罚太狠,比如-10,智能体会倾向于"干脆不动"来减少总损失;或者碰撞惩罚太狠,智能体害怕探索任何未知区域。

解决思路是先检查奖励曲线。如果每个回合的总奖励都在前几步就停住,说明智能体是在原地打转。此时可以调低每步的负奖励,比如设为-0.1,或者调低撞墙惩罚。另一个裹乱因素是ε衰减过快,前期还没来得及探索,ε就已经很低了,导致智能体过早进入纯利用阶段。把ε初始值改大,衰减率改慢,给智能体更多探索机会。

6.2 Q值爆炸或跳变

有时候你会看到Q表里的数值突然变得非常大,比如几千甚至几万,这通常是更新公式里出现了循环依赖。在迷宫这种确定性环境里,如果某个状态通过一个动作能回到自己自己身上(比如原地撞墙),而且γ接近1,那么Q(s, a) = reward + γ * max(Q(s', :))中的max(Q(s', :))可能会包含Q(s, a)本身。当奖励为正时,这个自举循环会导致Q值无限上涨。

解决办法是检查撞墙时的状态转移。在我的设计中,撞墙后nextState = state,所以更新公式会写成Q(s, a) += α * (reward + γ * max(Q(s, :)) - Q(s, a))。如果max(Q(s, :))刚好就是Q(s, a)本身,Q值就会反复累加。要规避这个问题,可以在撞墙时引入惩罚并强制回合结束,或者保证方程里始终有一个正的终止条件来切断自举循环。一个简单有效的做法是:撞墙惩罚设为较大的负数,使得这个循环的固定点变成了一个负值,而不是正值。我用的是-10,在迷宫场景下测试下来基本不会出问题。

6.3 迷宫生成结果不可复现

因为迷宫是随机生成的,每次运行代码得到的迷宫都不一样。训练结果也会因此不同。这个特性在设计上不是bug,但调试时很麻烦——你用昨天的逻辑跑出了好结果,今天再跑一遍发现不收敛了。

解决办法是在脚本开头设置随机数种子:

rng(42);

固定种子后,每次生成的迷宫和训练过程都完全一致。我调试时习惯先固定种子,等逻辑完全没问题了,再移除种子测试多组随机迷宫下的泛化能力。这样既能复现问题,又能验证算法的稳定性。

6.4 训练曲线来回震荡不下降

训练曲线震荡通常有两个诱因:ε衰减太快和初始Q值与真实值差距过大。特别是迷宫较大时,Q表刚开始全是零,智能体随机探索的成功率很低,一旦偶尔成功一次,Q值大幅度上升;后续如果连续多次失败,Q值又被拉低,曲线就来回抖动。

我的经验是:先不追求单次回合的稳定,而是关注滑动平均。在Matlab里用movmean(episodeRewards, 50)画平滑曲线,能够更清楚地看到整体趋势。另外检查一下ε衰减,如果50个回合内ε已经降到了0.1以下,那就说明衰减太快了。

6.5 状态空间爆炸时的调参方向

当迷宫边长超过50的时候,状态数量达到2500个,每个状态4个动作,Q表规模是10000个元素。在Matlab里这种规模完全不算大,内存毫无压力。但训练时间会变得漫长,因为需要探索的状态空间太大。

此时有两条路。一条是调整迷宫生成参数,生成更加"稀疏"的迷宫——减少通道数,增加墙壁密度,让实际可到达的状态数降低。另一条是优化Q表更新的策略,比如加入经验回放、使用Double Q-learning来降低Q值过估计。但对于教学项目,我的建议是控制在30x30以内,专注把基础逻辑和可视化做好,大于这个规模的问题更适合交给深度强化学习去解决。

7. 项目扩展方向与个人心得

做这个项目的过程中,我最大的感触是:强化学习入门最怕的不是数学,而是感觉不到"进展"。迷宫项目的好处在于,你能通过可视化非常直观地看到智能体每个阶段的行为变化。这种"它今天居然学会了绕开那堵墙"的正反馈,是坚持学下去的巨大动力。

如果你已经把这个基础版本完全跑通了,我强烈建议在它的基础上去做几个小扩展,难度递增,收获也不小。

第一个扩展是把迷宫从方形改成矩形,甚至不规则形状。这只需要调整状态编码和迷宫生成函数,不需要动Q-learning核心逻辑,但能帮助你把"环境建模"和"算法逻辑"分离的思维建立起来。

第二个扩展是把离散动作改为连续动作空间。比如让智能体可以选择任意方向的移动距离,这就涉及连续动作空间的Q值估计,基本方法是将Q表换成函数逼近器。这一步踏出去,你就从基础版Q-learning迈向了DQN。

第三个扩展是引入多个终点或动态障碍物,它会让问题变成非稳态环境,Q-learning在这种场景下表现会差一些,这反而能让你更深刻地理解强化学习"基于马尔可夫决策过程"的假设条件到底意味着什么。

第四个扩展是我个人觉得最有意思的,从迷宫问题迁移到网格世界中的路径规划,然后给每个格子设置不同的通行代价,比如沼泽地带代价高、道路代价低。这时Q-learning学到的不再是最短路径,而是总代价最小的路径——本质上就是在做加权最短路径的学习。这个扩展把迷宫从平面几何问题变成了真正的决策问题,你会看到强化学习在路径规划领域的实际价值。

最后再分享一个小技巧:在调试Q-learning的时候,别急着跑几百回合。把回合数临时设成50,把每步都画出来,一行一行地跟着智能体走。看它在某一步选了哪个动作,看Q表对应位置更新了多少——这个过程能帮你抓住90%的bug。我教过几个初学强化学习的朋友,他们最大的问题就是"不敢打断程序来看中间变量"。Matlab调试器的断点功能在这个项目里特别管用,你在Q表更新那一行打一个断点,每次更新都能看到所有变量的变化,对理解公式绝对有奇效。

用一句话总结我的体会:Q-learning代码本身不到一百行,真正花时间的是让智能体"学会跑"的过程——调奖励、调探索、画图看行为。这个过程走完一遍,你对强化学习的理解会远远超过只看教材的收获。

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

Linux服务器性能优化实战:从CPU、内存到磁盘网络的系统调优指南

服务器又卡了&#xff0c;top一看负载飙到几十&#xff0c;CPU使用率却只有不到一半&#xff1b;或者CPU满得冒烟&#xff0c;业务却迟迟没有响应。这种场景&#xff0c;凡是正经做过线上运维的基本都遇到过。Linux服务器性能优化这个主题&#xff0c;说大很大&#xff0c;CPU、…

作者头像 李华
网站建设 2026/10/21 5:09:11

Canvas 2D旋转动画实战:从数学原理到性能优化

1. 从一行标题拆出来的真实需求“可视化2D动画—cavas旋转的圈”&#xff0c;这个标题第一次看到的时候&#xff0c;我脑子里冒出来的画面特别具体&#xff1a;一个用 Canvas 画出来的圆环&#xff0c;可能是一圈点、一圈线段、一圈渐变色块&#xff0c;然后它在那儿匀速转&…

作者头像 李华
网站建设 2026/10/21 5:08:44

知识工作插件体系实战:五类插件选型、配置与工作流串联

1. 先从“知识工作插件”这个标题说起&#xff0c;它到底在解决什么我最初看到"knowledge-work-plugins"这个词组时&#xff0c;第一反应是&#xff1a;这不是某个单一插件&#xff0c;而是一个插件集合&#xff0c;甚至是一整套针对知识工作者打造的效率工具链。所谓…

作者头像 李华
网站建设 2026/10/21 5:07:21

跨平台人脸识别三端实战:原理、差异与部署避坑指南

1. 跨平台下的人脸识别原理&#xff1a;为什么实现方式各不相同&#xff0c;底层逻辑却一致先把我遇到的实际场景说清楚。前段时间接手了一个涉及 Windows、Linux、Android 三端的人脸识别门禁项目&#xff0c;项目方案里明确要求三端都必须支持摄像头实时抓拍、人脸检测、特征…

作者头像 李华
网站建设 2026/10/21 5:07:19

LoRA微调实战:消费级显卡低显存微调7B大模型全指南

想在手头这张消费级显卡上微调一个7B模型&#xff0c;却被告知全参数微调要占80GB以上显存&#xff1f;我猜你已经听过LoRA这个名字了——LoRA微调的核心&#xff0c;就是把原本几百亿参数的训练量&#xff0c;压缩成只额外训练几百万到几千万个参数。这篇文章不绕弯子&#xf…

作者头像 李华