☰
Q-Learning MATLAB源码详解:Q表更新与epsilon-greedy策略
2026/10/11 16:33:54 网站建设 项目流程

简介:这是一份带逐行中文注释的Q-Learning MATLAB源代码文档,聚焦强化学习中的经典无模型算法,适合人工智能课程学生、竞赛选手或算法入门者阅读;资源以“王子找公主”的网格寻路游戏为案例,演示智能体如何利用状态转移矩阵、奖赏矩阵以及Q值迭代,逐步学会避开障碍物和陷阱并找到最短路径。压缩包共1个doc文件,大小约33KB,包含完整训练脚本、关键参数说明和路径回溯代码,目前已有1378人学习下载。文档从可移动矩阵与奖赏矩阵的设计出发,解释折扣因子gamma和选择概率epsilon的意义,并展示epsilon-greedy策略下随机探索与利用最优动作的平衡;训练循环部分清晰给出了贝尔曼方程更新Q值的实现,还通过每100次迭代输出状态便于观察收敛。读者不仅能复现该案例的最优路径规划,还可将同样的代码结构迁移到其他栅格环境或决策问题中,是快速上手Q-Learning的理想参考资料。

1. 拿到这份《Q-Learning 源代码及注释(matlab).doc》,先搞清三件事

第一次拿到《Q-Learning 源代码及注释(matlab).doc》这类文档时,多数人做的事一模一样:打开MATLAB,把代码一段段敲进去,点运行,然后看着一个小方块在网格上乱撞,撞着撞着某一次莫名其妙走到了终点。这个doc确实帮很多人入了强化学习的门,但也坑过不少人——它附带的注释只告诉你“这一行在算什么”,不说“为什么这么写”,于是你改一个参数就可能全盘翻车。下面把这类MATLAB源码拆开讲透:Q表怎么初始化、epsilon-greedy为什么是两行 if-else、Bellman 更新那一行为什么非要取下一状态的最大值,以及doc里最容易抄错的几个细节。适合刚接触强化学习、要交课程作业或做小型路径规划验证的读者;如果你已经在跑深度强化学习,也可以用这套表格型 Q-Learning 当基准对照。

2. 建模先行:把 Q-Learning 的数学公式落成 MATLAB 能跑的数据结构

2.1 从 MDP 到网格世界:状态、动作、回报怎么映射成变量

Q-Learning 虽然号称 model-free,但你要在 MATLAB 里跑起来,第一件事不是写公式,而是把状态空间、动作空间、回报函数翻译成数组和矩阵。以这类 doc 里最常见的 5×5 网格世界为例:左上角是起点,右下角是终点,智能体每一步可以上、下、左、右移动,撞墙原地不动并吃一个负回报,到达终点获得正回报。

状态用 1 到 25 的编号表示,而不是直接存坐标。这样 Q 表天然就是一个 25 行 4 列的矩阵Q(state, action),第 i 行第 j 列存储“在状态 i 执行动作 j 的长期价值估计”。动作 1/2/3/4 分别对应上/下/左/右,这是这套源码里最常见的约定,也是零基础入门最容易忽略的地方:MATLAB 下标从 1 开始,你没有办法像 Python 那样让状态从 0 编号,写zeros(25, 4)还是zeros(24, 4)决定了后面所有坐标转换的边界逻辑。

% q_learning_gridworld.m % 网格尺寸与状态编码: 第 r 行第 c 列的格子编号为 (r-1)*cols + c rows = 5; cols = 5; S = rows * cols; % 状态总数 25 A = 4; % 动作数: 1=上, 2=下, 3=左, 4=右 % Q 表: 状态数 × 动作数, 全部初始化为 0 Q = zeros(S, A); % 起点编号 1, 终点编号 25 startState = 1; goalState = 25;

这段代码背后的选型逻辑要讲清楚:为什么用“编号”而不是直接用(r, c)坐标?因为 Q 表的下标必须是整数,Q(3,2)表示状态 3 执行动作 2,如果你存的是坐标,就得自己维护一套“坐标→行号”的映射,复杂度完全不必要。MATLAB 自带的sub2ind和ind2sub就是干这个的,后面状态转移时会大量用到。另外S = rows*cols这种写法比直接写25强得多——一旦你想改成 10×10 或 8×6 的网格,只改rows和cols两个变量,后面所有边界判断和索引都自动适配。

回报函数不建议像一些老代码那样先铺一张 25×25 的R矩阵,因为当动作有 4 个时,回报本质上是“状态 + 动作”的函数,即R(s,a),而不是简单的“状态到状态”的转移回报。写成嵌套函数或 switch-case 会直观得多,这点在 2.2 展开。

2.2 回报函数怎么设计:稀疏、步数惩罚与危险规避三张牌

回报设计直接决定算法学成什么样,但恰恰是 doc 注释里写得最少的部分。同一个 5×5 网格,回报函数不同,收敛速度和路径形态天差地别。业内常说的“Reward Shaping”说的就是这件事。下面三种设计风格我都在工程里见过,放在一起对比:

设计风格普通步回报到达终点撞墙行为效果
稀疏回报0+1000收敛慢,前期几乎学不到东西
步数惩罚-1+100-10自动倾向最短路径,收敛快
危险规避0+100-100特别怕墙,但可能在开阔区域徘徊

稀疏回报理论上是 Q-Learning 的“标准设定”,但表格型 Q-Learning 在完全零奖励的环境里只能靠随机游走碰运气碰到终点,5×5 网格还好,网格一大基本就学不动了。所以我给学生和同事的建议是:先上“步数惩罚”方案,把每一步的非终止回报设为 -1。这么做有一个数学上的妙处:智能体从起点到终点,路径每多一步就多扣 1 分,最终学习到的最优策略天然就是最短路径,不需要你额外写任何“找最短路径”的逻辑。

在 MATLAB 里,我一般不把回报写成一个提前算好的矩阵,而是放在状态转移函数里即时计算,原因很简单:回报往往依赖“目标状态”和“是否撞墙”,这两者在运行时才确定。具体代码留在第 3 章讲状态转移时给出,这里先强调一个设计原则:回报必须是状态转移结果(s, a, s')的函数,不是当前状态 s 的静态属性。这也是新手最容易犯的错——把“终点格子”的回报直接写进了 R 矩阵,导致智能体在终点附近反复横跳也能吃奖励。

2.3 先拆 Bellman 更新公式:三段各自负责什么

doc 里通常会把核心更新公式写成一行,类似Q(s,a) = Q(s,a) + alpha * (r + gamma * max(Q(s2,:)) - Q(s,a))。这一行看着简单,实则包含三个职责完全不同的部分:目标值(target)、时间差误差(TD error)、以及旧估计的保留比例。

把公式拆开看:r + gamma * max(Q(s2,:))是“现实”与“未来”的合成。r是环境给的即时回报,它告诉智能体这一步走得值不值;max(Q(s2,:))是“下一个状态能带来的最大未来价值”,注意这里必须取最大,因为 Q-Learning 是 off-policy 算法,它在估计价值时假设后续每一步都采用最优策略,不管当前实际策略是什么。gamma是折扣因子,负责把未来价值折算到现在——未来 10 步的奖励折算到现在,如果 gamma=0.9,只剩大约 0.35。

Q(s,a)是旧估计,r + gamma * max(Q(s2,:))是带噪声的新观测,两者之差就是 TD error。智能体每走一步,只把旧估计往目标方向挪一小步,挪动的幅度由alpha控制。理解了这三段,你才能看懂后面所有调参建议:alpha管的是“新信息顶掉旧信息的力度”,gamma管的是“智能体看得多远”,回报设计管的是“什么叫做好行为”。这三者互相牵制,不存在“某个参数万能最优”这种好事。

3. 源码主线:把主循环、动作选择、Q 表更新逐行讲透

3.1 程序骨架:episode 与 step 两层循环怎么搭

几乎所有表格型 Q-Learning 的 MATLAB 实现都是两层循环:外层是episode,代表“完整走完一轮从起点到终点的尝试”;内层是step,代表一轮尝试中的单步。episode 结束的条件有两个:一是到达终点,二是步数超过maxSteps上限。后者是保命条款,没有它,一旦智能体陷入死循环,程序就卡死了。

% 主循环: 训练 maxEpisodes 轮 maxEpisodes = 800; maxSteps = 200; % 预分配数组, 记录每轮到达终点用了多少步 stepsPerEpisode = zeros(maxEpisodes, 1); for episode = 1:maxEpisodes s = startState; % 每轮初始状态都是起点 totalReward = 0; % 可选: 用来观察每轮累计回报 % 内层循环: 走一步算一步, 直到终点或步数耗尽 for step = 1:maxSteps % 动作选择、状态转移、Q更新三件事都发生在这里 % 具体实现见 3.2 - 3.4 if s == goalState break; % 到达终点, 本轮结束 end end % 记录本轮实际步数 stepsPerEpisode(episode) = step; % 若提前到达终点, 这里记录的 step 不是 maxSteps end

这段骨架里有几个细节,老手可能觉得啰嗦,新手照着抄却容易抄错。第一,for step = 1:maxSteps和while s ~= goalState都能写,但 for 版本自带步数上限,少写一个循环判断条件,直觉上更安全。第二,stepsPerEpisode必须在主循环外预分配,否则 MATLAB 会在循环里动态增长数组,网格一大、episode 一多,性能拖累非常明显——这一点在第 5 章还会再提。第三,记录步数时要放在“判断是否到达终点”之后,否则break之前的值和实际步数会差 1,画出来的学习曲线会系统性偏移一格。

3.2 动作选择:epsilon-greedy 的两行代码与随机种子

Q-Learning 的动作选择策略一般是 epsilon-greedy:大部分时候选择当前 Q 表里价值最高的动作(利用),小部分时候随机选一个动作(探索)。没有探索,智能体永远发现不了新路径;没有利用,学到的经验全被随机抖动稀释。这个平衡是整套算法的灵魂。

% epsilon-greedy 动作选择 % rand() 返回 (0,1) 均匀分布随机数 if rand() < epsilon act = randi(A); % 探索: 随机选一个动作 else [~, act] = max(Q(s, :)); % 利用: 取 Q 表当前最大值对应的动作 end

参数说明:epsilon初始值常见取 0.1 到 0.3。如果你是做迷宫、寻路这类单目标任务,建议用“衰减版”而不是固定值,公式写在 4.3;如果你只是想把 doc 里的示例跑通,固定 0.1 也能出结果。randi(A)返回 1 到 4 的均匀随机整数,等价于ceil(rand() * A),但randi可读性更好。max(Q(s,:))返回两个值,第一个是最大值,第二个是最大值出现的位置——这里用~丢掉第一个值,第 5 章会讲这个~在旧版 MATLAB 里的兼容性坑。

一个容易被忽略的工程细节是随机种子。MATLAB 默认每次启动随机数流都不同,所以同一份代码两次运行,学习曲线的形状完全不一样,有人会误以为“算法不稳定”。做实验前建议在脚本开头加一行rng(0),固定随机种子,这样至少能让每次训练结果可复现,调参才能对比出真实差异。

3.3 状态转移:用 ind2sub 处理网格边界和撞墙

状态转移是源码里最“无聊”却最容易写崩的部分。常见的错误写法是直接用s±1和s±5表示上下左右——在 5×5 网格里碰巧是对的,但一旦改成非矩形地图就全乱套。正确做法是先把当前编号转成行列坐标,移动后再检查边界,最后转回编号。

% 由当前状态编号求行列坐标 [r, c] = ind2sub([rows, cols], s); % 根据动作计算目标行列 switch act case 1, r2 = r - 1; c2 = c; % 上 case 2, r2 = r + 1; c2 = c; % 下 case 3, r2 = r; c2 = c - 1; % 左 case 4, r2 = r; c2 = c + 1; % 右 end % 边界检查: 出界则原地不动, 并给予负回报 if r2 < 1 || r2 > rows || c2 < 1 || c2 > cols s2 = s; % 撞墙, 状态不变 reward_ = -10; % 撞墙惩罚 else s2 = sub2ind([rows, cols], r2, c2); if s2 == goalState reward_ = 100; % 到达终点 else reward_ = -1; % 普通步惩罚 end end

逻辑说明:ind2sub把 25 拆成 (5,5) 这样的行列对,sub2ind再把行列对拼回 25,这两个函数是 MATLAB 处理网格索引的标准工具,比手写取模运算直观,也不容易错。撞墙时状态s2必须等于s,不能是某个空洞的非法状态——如果这里写错,Q 表会出现很多“从未访问过”的行,行为异常且难以排查。回报统一放在状态转移分支里计算,避免了单独维护 R 矩阵的不一致风险。

这段代码是后续所有魔改的锚点。你想加障碍物,就在边界检查里多一个obstacle(r2,c2)==1的判断;你想让终点不只有一个,就把if s2 == goalState换成ismember(s2, goalStates);你想把网格改成八方向移动,只需扩充 switch-case 分支。后面第 6 章就是基于这个结构做扩展的。

3.4 核心一步:Q(s,a) 更新的 target 与 error

状态转移算完,终于到了最核心的一步:更新 Q 表。这一步在 doc 里通常只占一行注释“Q值更新”,但这里藏着 Q-Learning 和 SARSA 的本质区别,也是源码阅读者最容易混淆的地方。

% 取出下一状态的最优估计价值 % max 返回两个输出: 第一个是最大值, 第二个是对应动作编号 [vmax, ~] = max(Q(s2, :)); % 计算 TD target = 即时回报 + 折扣 × 未来价值 tdTarget = reward_ + gamma * vmax; % 计算 TD error = 目标 - 当前估计 tdError = tdTarget - Q(s, act); % 更新: 沿着误差方向走一步, 步长由 alpha 控制 Q(s, act) = Q(s, act) + alpha * tdError;

这里的max(Q(s2,:))是 Q-Learning 的身份证。它评估“下一个状态能带来的最好情况”,认为未来会采取最优动作。如果你把这一行替换成Q(s2, act2)——也就是用“实际执行的下一步动作”的价值来更新——算法就变成了 SARSA。SARSA 是 on-policy,Q-Learning 是 off-policy,两者的区别在新手阶段可能感觉不大,但在随机环境或带惩罚的环境里,学出来的策略保守程度会明显不同。之前有人把 doc 里的代码抄串行,把max的取法抄成了实际动作估值,训练出来的智能体总是绕远路,就是因为这个细节。

参数说明:gamma在 5×5 网格里取 0.9 就够了;alpha取 0.5 时,新旧信息各占一半,收敛较快,但最终会在最优值附近抖动;取 0.1 时更平滑,代价是收敛慢。这些数字不是越接近 1 越好,第 4 章单独展开。另外注意这里用s2计算未来价值时,如果s2恰好是终点,max(Q(s2,:))取的是 Q 表那一行里最大的数。训练初期这一行还是 0,所以到达终点的学习信号主要靠即时回报reward_撑起来,这就是为什么回报设计太稀疏时,环境“教”得太慢。

3.5 观察收敛:记每轮步数而不是盯着 Q 表发呆

训练跑起来了,怎么判断有没有学对?很多刚接触的人习惯盯着 Q 表看数字变化,这不是好习惯。Q 表里的数值会一直变,但它变大变小都不代表策略变好——真正该看的是“每轮走到终点用了多少步”。

% 训练结束后画学习曲线: 每轮到达终点的步数 figure; plot(1:maxEpisodes, stepsPerEpisode, 'LineWidth', 1.2); xlabel('Episode'); ylabel('Steps to Goal'); title('学习曲线: 步数应随训练下降'); grid on; % 平滑视图: 看最近50轮均值, 忽略单轮波动 window = 50; smoothed = movmean(stepsPerEpisode, window); hold on; plot(1:maxEpisodes, smoothed, 'r', 'LineWidth', 2); legend('原始步数', '50轮移动平均', 'Location', 'northeast');

如果每轮步数在前 100 轮里快速下降,然后进入平台期,说明智能体真的在变聪明。如果 500 轮之后步数依然在 200 左右振荡,也就是每次都没能到达终点,问题大概率出在回报设计或 epsilon 没衰减,而不是随机种子。movmean是 MATLAB 自带的移动平均函数,比手写滑动窗口方便得多,画出来的曲线能把单次回合并发的噪音抹掉,适合判断“趋势是否已经平台化”。

4. 参数不调好,算法不收敛:四个必填超参

4.1 alpha 学习率:固定值还是随 episode 衰减

alpha 控制每一轮更新时“新信息顶掉旧估计”的力度,取值在 0 到 1 之间。alpha 太大,Q 值会被最近的几次高回报或低回报带着剧烈抖动,训练曲线像锯齿;alpha 太小,收敛慢,网格稍大就浪费大量算力。

在 doc 的示例代码里,固定 alpha 是最常见的,一般取 0.1 到 0.5。我的建议是:如果你只是在固定网格、固定回报的静态环境里跑,直接固定 0.5 没毛病;如果你的环境带有随机性(比如对手行为在变、回报有噪声),最好让 alpha 随训练递减。一个不复杂的经验公式:

% alpha 衰减: 前期大步学, 后期小步微调 alpha = 0.5 / (1 + episode / 50);

episode / 50是衰减速率,50 表示大约 50 轮之后 alpha 降到 0.25,500 轮之后降到 0.05 左右。这个值要跟maxEpisodes匹配:训练轮数越多,分母该调大,否则 alpha 过早衰减到接近 0,后半段训练基本停止。

4.2 gamma 折扣因子:任务越长它越要接近 1

gamma 决定智能体“看多远”。gamma=0 时,智能体只看即时回报,永远学不会为远期目标做出局部牺牲;gamma=1 时,远期奖励和即时奖励等权,但在某些非确定性环境下可能出现价值高估不收敛。多数任务取 0.9 到 0.99 之间。

有一个快速估算方法:如果你希望“10 步之后的奖励对当前决策仍有约 50% 的影响力”,那么 gamma 的取值应满足gamma^10 ≈ 0.5,即 gamma ≈ 0.93。网格世界任务路径一般不超过几十步,0.9 起步即可;如果你的自定义地图是一条长走廊,终点在 50 步开外,gamma 至少要推到 0.98,否则终点奖励折算到起点附近已经接近于零,智能体完全没有出发的动力。

表格型 Q-Learning 的一个常见失误是把 gamma 和 alpha 混在一起调,看到不收敛就同时乱改。正确的排查顺序是:先固定 alpha=0.5、epsilon 衰减,只动 gamma,观察学习曲线的下降速度是否有变化。gamma 对“是否走最短路径”的影响远大于 alpha,而 alpha 影响的是收敛平顺度而非策略质量。

4.3 epsilon 探索率:线性衰减公式与终值

epsilon 是最直观但最容易被写成“固定值”的参数。固定 epsilon=0.1 时,训练后期每 10 步就有 1 步在随机乱走,学到的策略再好也会被随机动作干扰得不像样。成熟的工程做法是让 epsilon 从较大的初始值线性衰减到很小的终值。

% epsilon 线性衰减: 从 1.0 衰减到 0.01 epsilon = max(0.01, 1.0 - episode / maxEpisodes * 0.99);

这段公式的意思:第 1 轮 epsilon≈1,几乎全随机探索;第 500 轮左右降到 0.5;最后一轮约 0.01,只剩 1% 的随机动作兜底。max(0.01, ...)是保底条款,保证训练后期仍然留一点点探索,防止环境变化时策略彻底僵死。对 5×5 网格,这个衰减速度已足够;网格更大、路径更曲折时,建议把衰减终值保持 0.05,探索保留得多一点,否则容易困在局部最优。

4.4 episode 次数与网格规模的匹配经验

episode 数量没有万能答案,但它跟状态空间大小强相关。一个很粗略的经验值:episode 数至少是状态数的 20 倍。25 个状态的网格,500 轮起步;100 个状态的网格,2000 轮才算合理。下面是几个常用规模的经验配置表,可以直接照抄起步值:

网格规模状态数建议 episode 数每轮最大步数起步 alpha
3×39200500.5
5×525500 - 8002000.5
10×101001500 - 30004000.3
20×204005000+8000.3

这里有个容易起冲突的细节:每轮最大步数设太大,死循环未命中时单轮耗时会很高;设太小,合法路径被截断,终点永远走不到。经验做法是把最大步数设为“网格对角线步数的 3 倍”,比如 5×5 网格对角线 8 步,上限 200 已经留足了余量。如果发现训练后段步数曲线贴着上限走,别急着加步数上限,先检查回报函数里是否有某个状态在“原地打转吃惩罚”也优于往前走的逻辑漏洞。

5. 排错笔记:Q-Learning MATLAB 代码的经典翻车现场

5.1 翻车现场:Q 表训练完仍是全零

现象:训练几百轮结束,disp(Q)一看全是 0,智能体完全没学习。

原因:最常见的不是算法错了,而是“更新代码根本没有被执行”。我见过好几次都是把 Q 更新写在了if s2 == goalState的分支里——只有到达终点那一轮才更新,平时步则跳过,Q 表里能学到东西的行只有终点附近那几格。另一个高频原因是Q(s, act)里act是空的,比如max的第二个返回值被写成了~,而旧版 MATLAB 根本不支持~占位,直接报错,你以为是语法问题,其实是对应关系没对上。

解决:在 Q 更新那一行打一个条件断点,条件是episode > 10 && step > 100,跑一次看s、act、s2三个变量形状对不对。确认更新每步都执行,再确认act在 1 到 4 之间。强烈建议把 Q 更新代码单独抽成一个函数,传(Q, s, act, s2, reward_, alpha, gamma),这样既能单测,又能避免主循环里变量覆盖导致“改了没生效”。

5.2 翻车现场:几千轮还在原地乱撞

现象:学习曲线在 10 步上下振荡,永远不下降,几百轮过去还是这个德行。

原因:epsilon 固定在 0.9,智能体 90% 的动作都在随机走,相当于随机策略;或者是回报设计得太稀疏,智能体从起点出发一百多步都碰不到一次终点,Q 表里只有“撞墙”和“普通步”两种负反馈,没有任何正向信号引导它朝终点移动。

解决:先把 epsilon 改成衰减版,用 4.3 的公式;再把普通步回报从 0 改成 -1,稀疏回报改成步数惩罚。如果还不行,做一个“人工演示”:手写一段固定动作序列让智能体从起点走到终点,把每一站的(s, a, s2, reward)喂到 Q 更新公式里,相当于预置了一组有正回报的经验。这不是作弊,在真实工程里叫 “expert demonstration”,很多机器人强化学习项目就是这么冷启动的。

5.3 翻车现场:doc 里复制的中文注释变成乱码

现象:从《Q-Learning 源代码及注释(matlab).doc》里把代码复制到 MATLAB 编辑器,中文注释变成“锟斤拷”或者一堆 Ã©ï¼ 的乱码,字符串里的引号还自动变成了中文全角引号,报错“字符串未结束”。

原因:doc 文件里的中文注释通常按 Word 的本地编码(中文系统里通常是 GBK/GB2312)保存,而新版 MATLAB 编辑器默认按 UTF-8 解析文本,编码不匹配就乱码。加上 Word 的自动排版会把"替换成中文引号“”,MATLAB 不认识这俩全角字符,直接把字符串语法炸了。

解决:不要直接复制 doc 里的代码块,先粘到 Windows 自带的记事本,另存为 UTF-8 编码,再用 MATLAB 打开。更稳的做法是:只从 doc 里复制纯代码部分(字母、数字、运算符),中文注释全部手敲,因为注释不重要,复制乱码得不偿失。如果你用的是学校机房的老版 MATLAB,还要检查编辑器的“预设 > 编辑器/调试器 > 语言 > 文件编码”,把编码设成 UTF-8,这是一次踩坑后我能给的最实际建议。

5.4 翻车现场:智能体总是“绕远路”到达终点

现象:策略学会了,每次都能到终点,但路径明显绕弯,从 5 步能走到 12 步,怎么看怎么别扭。

原因:三个可能性按概率排序。第一,回报里普通步是 0 不是 -1,所有路径的长期回报一样,算法随机选了一条,不一定选最短的;第二,gamma 太小,远一点的终点奖励折算到起点几乎为零,智能体更看重眼前少撞墙,而不是尽早到达;第三,epsilon 后期没有衰减,策略里混入了随机的冗余动作,造成视觉上的“绕路”。

解决:把普通步回报改成 -1,把 gamma 从 0.9 试到 0.95,观察步数曲线有没有进一步下降。还有一个检查技巧:训练结束后用纯贪心策略回放一遍路径,也就是永远取max(Q(s,:))对应的动作,如果贪心路径还是绕,问题在回报和 Q 表;如果贪心路径正常,问题就在 epsilon 没降下来。

5.5 翻车现场:训练极慢,一个 5×5 网格都要跑几十秒

现象:800 个 episode 跑下来要等半天,风扇狂转,进度却走得很慢。

原因:十有八九是内层循环没有步数上限。while s ~= goalState死循环,在某几个状态之间反复横跳永远停不下来,每个 episode 都空转到天荒地老;或者是在循环里写了disp(s)、clc清屏这类输出语句,MATLAB 在循环里刷新控制台的代价比你想象的高得多。

解决:内层循环一律换成for step = 1:maxSteps,配上if s == goalState, break; end,从根本上杜绝死循环。把一切调试输出移出主循环,只保留stepsPerEpisode这种必要记录。另外,Q 表更新的 if-else 链里不要每次都调用disp或者重新计算rows*cols,把常量提到循环外。5×5 网格 800 轮,正常 MATLAB 应该在几秒内跑完,如果超过 20 秒,按照这条逐项检查,基本一抓一个准。

6. 把它改造成自己的场景:地图替换、路径回放与可复现实验

6.1 改地图的最小改动清单

前面这套代码最大的价值是“可替换性”。你想把它从 5×5 网格改成自己的地图,只需要动四个地方:rows、cols、起点编号、终点编号。但改成真实场景后,通常还会遇到一个实际问题:地图里有障碍物。障碍物不能简单用“撞墙惩罚”表达,因为障碍物在网格内部,不在边界上,智能体可能穿过去。

常见做法是维护一个障碍物矩阵,和网格同尺寸,有障碍物的格子为 1,否则为 0。状态转移时,把“出界原地不动”扩展为“出界或落入障碍物都原地不动并给惩罚”。改动量不大,但价值很大——它把玩具示例变成了能用在仓储、游戏寻路里的微型路径规划器。

6.2 训练结束后做一次贪心路径回放

学习曲线下降只代表“数值上学到了”,不代表智能体规划的路径真的合理。训练结束后做一个纯贪心回放,把路径画出来,一眼就能看出策略质量。回放代码和训练时的状态转移逻辑基本一样,只是动作选择不再用 epsilon-greedy,而是每次都取 Q 表最大值:

% 用训练好的 Q 表做纯贪心策略回放 s = startState; path = s; % 记录路径上的状态编号 while s ~= goalState [~, act] = max(Q(s, :)); % 只利用, 不探索 [r, c] = ind2sub([rows, cols], s); switch act case 1, r2 = r - 1; c2 = c; case 2, r2 = r + 1; c2 = c; case 3, r2 = r; c2 = c - 1; case 4, r2 = r; c2 = c + 1; end if r2 < 1 || r2 > rows || c2 < 1 || c2 > cols s2 = s; % 出界, 原地不动 else s2 = sub2ind([rows, cols], r2, c2); end path(end+1) = s2; % 记录新状态 s = s2; if numel(path) > rows * cols * 4 error('回放未收敛, 检查Q表或回报设计'); end end % 状态编号转坐标并画路径 [rPts, cPts] = ind2sub([rows, cols], path); figure; plot(cPts, rPts, 'b-o', 'LineWidth', 1.5); xlabel('列'); ylabel('行'); title('训练后贪心策略路径回放');

这段回放代码里有一个我特意留下的保护逻辑:numel(path)超过状态数四倍就报错,防止回放也陷入死循环。路径回放是验证训练效果的最后一关——曲线漂亮但路径盘绕,大概率是回报函数鼓励了坏行为;曲线一般但路径直接,说明算法本身没问题,只是超参没调透。

6.3 我的习惯:固定随机种子 + commit 注释写清参数

最后分享一个这几年养成的习惯,它多次帮我避免了“自己坑自己”。第一,每次跑实验前在脚本开头写rng(0)固定随机种子,确保同一份代码两次运行结果一致。没有这一步,你调参前后的差异可能是随机噪声造成的,怎么调都觉得“没效果”。第二,把每次实验用的 alpha、gamma、epsilon、episode 数记下来,作为代码文件头部的注释块。第三,把这个.m文件纳入源代码管理,每一次提交到仓库时,commit 注释里写明“这次改了 Q 表更新逻辑”还是“只调整了 gamma 从 0.9 到 0.95”——这点在机器学习代码里特别重要,因为算法代码的每一次微调,只有配合当时的参数和随机种子才能复现,commit 注释是你给自己留的后悔药。

我早年跑这份 doc 时,因为偷懒没记录参数,一周后想在原来的基础上把网格改大,怎么都想不起来当时 0.8 的收敛效果是用 alpha=0.1 还是 0.3 跑出来的。后来被迫用 git 重看历史版本,翻了半天才对齐。现在每做一次实验,我都会顺手在 commit 注释里写全超参,哪怕只是改了一个数字。这套表格型 Q-Learning 跑通之后,你再看 DQN、PPO 的 MATLAB 实现,会发现很多结构都能对号入座——主循环、回放缓冲、策略更新,无非是把 Q 表换成了神经网络,核心思路一脉相承。希望帮到你。

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

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

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

立即咨询