ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Q-Learning王子寻公主:MATLAB网格世界从环境建模到路径推断解析

Q-Learning王子寻公主:MATLAB网格世界从环境建模到路径推断解析

简介:一份Q-Learning算法在MATLAB中的完整实现与逐行注释文档,面向强化学习初学者、算法爱好者及课程实验者。文档以王子寻找公主的16格网格世界为案例,细致讲解了可移动矩阵TT、奖赏矩阵R的构建思路,并给出Q矩阵初始化、折扣因子gamma与epsilon-greedy策略如何设置,包含900轮训练循环、训练进度输出以及基于学到的Q矩阵反向规划最优路径的完整MATLAB代码。通过障碍物、陷阱与终点奖励的设定,直观展示无模型环境下智能体如何通过试错更新Q值、权衡探索与利用。资源为单个doc文档,体积仅33KB,便于阅读与对照运行。已有1379人学习下载,适合边读注释边动手调试,快速掌握Q-learning核心步骤。

1. 需求与方案:16 格网格世界里的 Q-Learning 王子寻公主

Q-Learning 是强化学习里最经典的入门算法,但网上能直接拿到、还带逐行注释的 MATLAB 实现反而不多。这段源码把“王子找公主”的场景压缩进了一个 4x4 的网格世界:一共 16 个格子,王子可以做上、下、左、右四个动作,某些格子是走不过去的障碍物,某个格子是扣分陷阱,目标是从任意起点到达第 16 格公主身边。算法用一张 Q 矩阵记录“每个状态下每个动作值多少分”,经过 900 次回合训练后,只需要沿着 Q 值最大的方向走,就能得到一条可行路径。这篇方案会把环境建模、训练循环、路径推断和验收技巧拆开讲,重点标出这段源码里最容易踩坑的几个地方,适合拿来改造成课程设计或者强化学习入门实验。

2. 环境建模与奖励机制:可移动矩阵 T 的四个方向构造与 R 值陷阱

先明确一件事:Q-Learning 被称为无模型算法,指的是它不需要环境的状态转移概率,但并不是说不需要定义环境本身。这段 MATLAB 代码把环境全部编码进了两个矩阵:可移动矩阵 T 和奖赏矩阵 R。T 决定了王子能从哪个状态走到哪个状态,R 决定了每一步转移能拿到多少奖励。许多同学第一次做强化学习实验时,跳过这一步直接写训练循环,最后出了问题根本分不清是环境设计错了,还是 Q 值更新公式写错了。正确的做法是先手动模拟一步转移:给定当前状态 s,枚举合法动作,算即时奖励,再走一步,确认环境没有矛盾,再让算法去学。

2.1 状态编号与可移动矩阵 T 的构造逻辑

这段代码的编号方式是理解整个环境的关键,我建议在答辩时直接画出这张编号图。16 个格子按 4x4 排列,但编号不是常见的行优先,而是列优先:

第一行:1 5 9 13
第二行:2 6 10 14
第三行:3 7 11 15
第四行:4 8 12 16

也就是说,同一列上下两个格子的编号相差 1,同一行左右两个格子编号相差 4。向上走是编号减 1,向下走是编号加 1,向左走是编号减 4,向右走是编号加 4。如果把这个编号方式理解成行优先,四个方向的边界条件就会全部错位。

T = zeros(16); % T(i,j)=0 表示不可从 i 移动到 j,1 为可移动 for i = 1:16 % 上行动作 N:编号减 1,排除每列首行的 1,5,9,13 if ~ismember(i, [1,5,9,13]) T(i, i-1) = 1; end % 下行动作 S:编号加 1,排除每列末行的 4,8,12,16 if ~ismember(i, [4,8,12,16]) T(i, i+1) = 1; end % 左行动作 W:编号减 4,只在编号大于 4 时可行 if i > 4 T(i, i-4) = 1; end % 右行动作 E:编号加 4,只在编号小于 13 时可行 if i < 13 T(i, i+4) = 1; end end

上行动作排除 1、5、9、13,因为这四个编号是每一列的第一行,再减 1 就越界到上一列或者变成 0。下行动作排除 4、8、12、16,这是每一列的末行。左行动作要求编号大于 4,右行动作要求编号小于 13,这样不会跨出网格边界。这种写法比用 mod 函数判断边界更直观,也是这段代码值得保留的地方。

2.2 障碍物剔除与跳跃规则:T 矩阵的列清零细节

网格世界里不允许出现“走进障碍物”的转移,对应的做法是把障碍物编号作为目标状态的动作全部清零,也就是清列不清行。

% 障碍物:状态 10、11、14 不可作为目标状态 T(:, [10,11,14]) = 0; % 定义跳跃:状态 13 清空原有动作后,只保留到状态 15 的传送门 T(13, :) = 0; T(13, 15) = 1;

第一行T(:,[10,11,14])=0会把所有“以 10、11、14 为目标”的动作置为不可达。注意这里清的是列,不是行。意思是王子可以从任意位置出发,但就是不能到达这几个障碍格子。T(13,:)=0则把状态 13 的所有出边清空,然后单独设置T(13,15)=1,让 13 只能单向跳到 15。

这里有一个非常容易翻车的设计点:T(13,:)=0会连同 13 到 9、13 到 12 这两个合法出边一起删掉。如果只是想禁止进入障碍 14,更精确的写法是T(13,14)=0,保留 13 向上的 9 和向左的 12。原代码之所以直接用整行清零,可能是故意把 13 设计成一个单向传送点,后面的奖赏矩阵里也给了R(13,15)=5的正奖励。如果你的场景不需要传送门,建议改成列清零,否则训练出来的路径在状态 13 附近会出现“只能跳过去”的奇怪现象,答辩时被问住就很被动。

2.3 奖赏矩阵 R 的取值与设计意图

奖赏矩阵 R 的大小同样是 16×16,R(state, action) 表示从 state 转移到 action 这个动作获得的即时奖励。这里 action 直接用目标状态编号表示,所以 R 和 T 的行列含义一致。

R = -1 * ones(16); % 默认每走一步扣 1 分,鼓励最短路径 R(:, 6) = 0; % 进入状态 6 不扣分 R(:, 7) = -5; % 进入状态 7 陷阱扣 5 分 R(:, 16) = 10; % 到达公主所在状态 16 得 10 分 R(13, 15) = 5; % 从状态 13 跳到 15 得 5 分

默认值 -1 意味着所有没有被特殊指定的转移都要付出代价,这是一种非常朴素但有效的“最短路径”引导:走的路越多,累积扣分越多。状态 6 设置为 0,相当于一个中立安全区;状态 7 是陷阱,进入即 -5;状态 16 是目标,获得 +10。R(13,15)=5和前面的跳跃规则对应,给传送门一个单独奖励。

一个值得注意的地方是,目标奖励放在“进入 16 的动作”上,而不是放在“状态 16 本身”上。Q-Learning 更新的是状态-动作对的价值,动作的即时奖励来自转移之后的结果,所以R(:,16)=10这种写法是正确的。如果把奖励放在状态编号上,写成R(16,:)=10,那么从 16 出发的动作才会拿到 10,而训练循环在到达 16 后立即结束,这个奖励永远学不到,算法就会认为公主那里没有价值。

关于折扣因子:gamma=0.8 意味着未来奖励每往后一步打八折。在只有 4x4 的网格里,这个值足够让算法兼顾“眼前收益”和“长远目标”。如果 gamma 太接近 1,Q 值收敛会变慢;如果太小,比如 0.5,王子可能更愿意选择即时奖励高但离终点更远的路线。epsilon=0.3 是探索率,表示每一步有 30% 的概率随机挑选合法动作,70% 的概率选择当前 Q 值最大的动作。原注释里写“策略选择概率,也就是学习率”,这里其实是个概念混淆:epsilon 是探索率,不是学习率,真正的学习率应该出现在 Q 值更新公式里,后文会专门说这个问题。

3. 训练循环与策略更新:epsilon-greedy 如何从试探到会走路

环境定义好之后,接下来就是 Q-Learning 的核心:训练循环。这一段源码的流程非常清晰,每个 episode 从随机起点出发,不断选动作、执行转移、更新 Q 值,直到到达状态 16。理解这段训练代码的关键是区分两个阶段:动作选择阶段决定“走哪一步”,Q 值更新阶段决定“这一步学到的经验如何写回价值表”。两者在同一个循环里交替发生,但职责完全不同。

3.1 训练循环主体结构与终止条件

先看训练循环的整体结构:

for episode = 1:900 % 随机初始化起点,并排除障碍物状态 state = unidrnd(16); while ismember(state, [10,11,14]) state = unidrnd(16); end % 一个 episode 持续到到达目标状态 16 while state ~= 16 possible_actions = find(T(state,:) == 1); possible_Q = Q(state, possible_actions); if rand() < epsilon choose = unidrnd(length(possible_actions)); action = possible_actions(choose); else [~, index] = max(possible_Q); action = possible_actions(index); end % 贝尔曼更新 Q(state, action) = R(state, action) + gamma * max(Q(action,:)); % 转移到下一个状态 state = action; end if mod(episode, 100) == 0 disp('-------------------') fprintf('训练的次数为:%d\n', episode) end end disp('训练结束')

外层循环控制训练回合数,900 次是读者可以自行调整的参数。内层循环是一个完整 episode:从初始状态出发,不断执行动作,直到state == 16。注意终点状态 16 没有继续展开动作,因为 Q 更新的目标是当前状态选择的动作,到达终点后整个 episode 结束,不需要更新“从 16 出发”的动作价值。

起点排除障碍物状态[10,11,14]是一个必要的保护。如果不排除,可能让训练从不可达状态开始,导致 Q 矩阵某些行被反复写入无意义的值。unidrnd(16)是 MATLAB 自带的离散均匀随机整数函数,返回 1 到 16 之间的整数,用法上等同于自己写randi(16)。

这里我建议加一个最大步数限制,比如step = 0; while state ~= 16 && step < 200。理由很简单:在训练初期,Q 矩阵全为 0,epsilon=0.3 的随机探索很容易让王子陷入环圈,万一某个状态的动作选择总是反复横跳,内层循环会变成死循环。原始代码能跑通,是因为 900 次训练足够让 Q 值逐渐修正,但如果你把 epsilon 调大、把 gamma 调得接近 1,死循环的概率会明显上升。加上步数限制之后,即使一回合没走到终点,也能结束本轮继续训练,不打断整个流程。

3.2 epsilon-greedy 动作选择:探索与利用的权衡

动作选择是 Q-Learning 里最容易写错的部分,也是答辩时一定会被问到的地方。原代码用rand() < epsilon作为随机条件:当随机数小于 0.3 时,从合法动作里均匀随机选一个;否则,在合法动作中选 Q 值最大的那个。

possible_actions = find(T(state,:) == 1); possible_Q = Q(state, possible_actions); if rand() < epsilon choose = unidrnd(length(possible_actions)); action = possible_actions(choose); else [~, index] = max(possible_Q); action = possible_actions(index); end

注意这里非常关键的一点:possible_actions是从 T 矩阵当前行里挑出的所有合法动作,possible_Q是这些合法动作对应的 Q 值。利用阶段只在合法动作里找最大值,不会选中障碍物。随机探索阶段同样只在合法动作列表里随机选,不会产生非法转移。这套过滤逻辑给后续 Q 值更新提供了安全前提。

epsilon=0.3 是一个比较常见的经验值。如果 epsilon 太大,王子大部分时间在乱走,Q 矩阵更新会受到大量无效样本干扰;如果 epsilon 太小,算法过早进入“只用当前 Q 值最大动作”的状态,可能在统计意义上不够充分。900 个 episode 对 16 个状态来说并不多,0.3 的探索率可以保证每个状态-动作对都被访问到足够多次。

这里还隐藏着一个细节:unidrnd(length(possible_actions))返回的是 1 到 N 之间的随机索引,用它去 possible_actions 里取具体动作。很多初学者会直接把随机数当作动作编号,比如写出action = unidrnd(16),那就可能选中被 T 屏蔽的障碍物。正确的做法是先取合法动作列表再随机索引,这段原代码的做法值得保留。

3.3 Q 值更新公式:为什么贝尔曼方程长这样

每次执行一个动作后,代码立刻更新 Q 值:

Q(state, action) = R(state, action) + gamma * max(Q(action,:));

这个公式是标准 Q-Learning 更新式的简化版本。完整写法通常带一个学习率 alpha:

Q(s,a) ← Q(s,a) + α [ R(s,a) + γ max Q(s',a') - Q(s,a) ]

当 α=1 时,完整公式就变成Q(s,a) = R(s,a) + γ max Q(s',a'),也就是原代码的写法。它把旧估计值完全抛弃,直接用当前的即时奖励加上下一个状态的最大折扣价值去覆盖。这个更新逻辑的含义是:状态 s 下选择动作 a 的价值,等于这一步能拿到的即时奖励,加上从下一个状态出发能获得的所有动作价值里最大的那个,再乘以折扣因子。

为什么是“下一个状态的最大值”?因为 Q-Learning 是离策略算法,它评估的是“假设从下一个状态开始执行最优策略”可以获得的价值。它不需要知道环境转移概率,只需要在到达下一个状态后,查一下这一行的 Q 值最大值即可。这个特性让 Q-Learning 可以无模型运行。

原代码没有 alpha 参数,这在奖励完全确定、状态数量很小的场景下是可以收敛的,因为每个状态-动作对被反复访问,覆盖更新的极限仍然是正确的动作价值。但代价是收敛过程会有明显震荡:某一次采样得到的奖励会直接把旧值顶掉,下一个 episode 可能又被另一个采样顶回来。如果你准备把这个实验扩展成更复杂的场景,或者想让收敛曲线更平滑,就应该引入一个 0.1 到 0.5 之间的学习率 alpha。这部分作为优化项放在验收演示里,效果会比直接交原代码好很多。

3.4 训练监控与收敛观察

原代码在每 100 个 episode 输出一次“训练的次数为”,实际上对判断是否收敛帮助不大。它只告诉你训练进行到了第几轮,没有告诉你 Q 矩阵是否还在剧烈变化,也没有告诉你王子找到公主平均需要多少步。如果要交一份能说明“算法真的学到了”的方案,最简单的方法是记录每个 episode 结束时的累计步数,或者记录 Q 矩阵相邻两次更新的最大变化量。

prev_Q = Q; Q(state, action) = R(state, action) + gamma * max(Q(action,:)); delta = max(abs(Q - prev_Q), [], 'all');

上面这段是监控 Q 变化量的思路。把每个 episode 的平均步数和 delta 画成两条曲线,就能直观看到:前 100 个回合步数很长、delta 很大,之后步数逐渐下降并稳定,delta 收敛到接近 0。这个趋势是你答辩时最有力的证据,比单纯打印“训练结束”更能说明问题。原代码的监控逻辑不算错,但可以在此基础上扩展,这正是把一份课后练习改造成完整方案最容易加分的地方。

4. 路径推断与实验验证:从 Q 矩阵反推王子路线

训练结束后,Q 矩阵里已经保存了每个状态-动作对的长期价值。接下来的任务是根据 Q 矩阵生成一条从指定起点到状态 16 的路径。这个过程不需要再更新 Q 值,只需要做贪心决策:每到一个状态,选择当前 Q 值最大的合法动作,然后走到下一个状态。路径推断代码很短,但里面藏着一个非常隐蔽的逻辑问题,很多人在复现时都在这条路上翻过车。

4.1 路径推断的朴素实现

原代码给出的路径推断逻辑如下:

start_0 = 1; % 设置起始点,这里是状态 1 path = start_0; % 路径向量以起点开始 while path(end) ~= 16 [~, next] = max(Q(start_0,:)); % 选择 Q 值最大的动作 path = [path, next]; % 把动作追加到路径 start_0 = next; % 移动到下一个状态 end path

这段代码的思路是对的:从起点开始,反复查询当前状态这一行里 Q 值最大的列编号,然后将其追加到路径末尾,直到路径终点是 16。max函数的第一个返回值是最大值,第二个返回值是最大值所在的位置,MATLAB 中[~, next]表示忽略第一个返回值,只取索引。path = [path, next]是向量拼接,类似于 Python 列表的 append。

在训练收敛之后,Q 矩阵会形成一个从任意状态指向最终目标的“价值梯度”,贪心决策就能沿着梯度一路走到终点。因为第 16 个状态在训练循环里不会被更新出边,所以路径到达 16 后,max(Q(16,:))不会被调用,循环自然停止。

4.2 一次典型路径与结果解读

用原始参数跑完 900 个 episode 后,从状态 1 出发的路径可能是1 5 6 2 3 4 8 12 16,也可能是更短的1 2 3 4 8 12 16,具体取决于随机种子。路径无论如何都会避开 7 号陷阱和 10、11、14 三个障碍物。如果路径经过了状态 6,说明算法认为绕到 6 拿 0 奖励比直接走普通格子拿 -1 更划算,即使多走了一步,在折扣因子 0.8 下仍然可能被接受。

从数值上看,路径越长,累积的 -1 奖励越多,Q 值越小。目标奖励 +10 只出现在最后一步“进入 16”的动作上,所以路径价值主要由“步数惩罚”和“终点奖励”共同决定。这也是为什么默认 R 值都设置为负数的原因:它想让王子在有限步内到达公主,而不是在网格里闲逛。

如果你发现路径没有避开某个障碍物,或者出现了跳变到奇怪格子的情况,不用急着怀疑训练代码,首先检查路径推断代码是否对动作做了合法过滤。下面的 4.3 节就是最常见的坑。

4.3 路径推断的隐藏问题:没有过滤不可达动作

原代码最明显的问题是max(Q(start_0,:))把 Q 矩阵整行都纳入了比较,包括那些 T 矩阵明确标记为不可达的动作。由于不可达动作从未被训练更新,它的 Q 值永远停留在初始值 0。如果某个状态所有合法动作的 Q 值都是负数,那么max会选中一个 Q 值为 0 的不可达动作,路径就会“走入障碍物”。

举例来说,假设状态 1 的合法动作是 2 和 5,训练后 Q(1,2) 和 Q(1,5) 都是负数,而 Q(1,10) 仍然是 0,那么max(Q(1,:))会返回 10。王子就直接跳到障碍物 10 上。虽然 10 仍然有出边可以继续走,但这条路径明显违反环境规则,在演示环节会被一眼看穿。

修复方式很简单:路径推断同样要使用find(T(s,:)==1)过滤合法动作。

start_0 = 1; path = start_0; while path(end) ~= 16 s = path(end); legal_actions = find(T(s, :) == 1); % 只允许合法动作 [~, idx] = max(Q(s, legal_actions)); next = legal_actions(idx); path = [path, next]; end path

修改后的代码先取出当前状态的合法动作列表,再在这个列表范围内找 Q 值最大的动作。这样路径推断就和训练阶段的动作选择逻辑保持一致,不会再选中障碍物。这是一个非常值得写进实验报告的修复点,因为很多网上的 Q-Learning 示例在训练部分用possible_actions过滤,却在路径推断时忘记过滤,导致结果不稳定。你在验收时一定要把这一点讲出来,说明你真正理解了这个算法。

4.4 Q 矩阵策略的复用与限制

训练好的 Q 矩阵可以看作一张策略查找表:给任意起点,只需要重复贪心查询就能得到一条路径。这是 Q-Learning 相比动态规划的优势之一,不需要重新计算,直接查表即可。但它也有一个明显限制:当两个动作的 Q 值相等时,max函数会默认选择索引较小的那个,这会让它偏向编号小的格子,可能掩盖掉同样优秀的其他路线。如果你希望展示多解,需要在选择动作时加随机打破平局的逻辑,比如在最大值集合里随机选一个。这个细节不会影响主流程,但能体现你对算法行为边界的理解。

5. 常见问题与避坑:不收敛、死循环和奖励尺度

这一段专门讲复现和扩展这段代码时最容易翻车的五个问题。每个问题都会按“现象、原因、解决”的顺序拆开,你在课程设计报告里可以直接改写成调试日志,答辩时如果被问到“你遇到过什么问题”,这些素材足够撑起一段有说服力的回答。

5.1 现象:Q 值震荡,训练后路径仍然不稳定

现象:修改 epsilon 或 gamma 后,同样的代码有时能跑到终点,有时在某个格子里反复横跳,Q 矩阵直到 900 个 episode 结束也没有稳定下来。

原因:原代码的 Q 值更新是 α=1 的覆盖更新,没有任何平滑机制。每一步的新估计值直接覆盖旧值,等价于相信最近一次采样是完全准确的。在小网格、无随机奖励的环境里可以收敛,但一旦奖励有噪声,或者某个状态-动作对访问次数不均衡,Q 值就会剧烈波动。另一个常见原因是在训练过程中手动把 epsilon 调得很大,比如 0.9,导致大部分时间都在随机试探,有效利用经验的次数太少。

解决:引入学习率 α,把更新公式改成Q(s,a)=Q(s,a)+α*(R(s,a)+γ*max(Q(s',a'))-Q(s,a)),一般取 0.1 到 0.5。α 控制新旧信息的融合程度,让 Q 值平滑下降而不是被单次采样顶飞。同时可以把 epsilon 做成衰减策略,比如每 50 个 episode 乘以 0.95,前期多探索,后期多利用,收敛效果会明显改善。

5.2 现象:路径推断时走进障碍物或进入死循环

现象:训练本身没有报错,但从起点生成路径时,输出的 path 出现了 10、11、14 这些障碍物编号,或者程序长时间不结束,像是走进了环。

原因:路径推断用了max(Q(start_0,:)),没有像训练阶段那样用find(T(s,:)==1)过滤合法动作。不可达动作对应的 Q 值始终保持初始值 0,只要合法动作的 Q 值小于 0,max就会选中不可达动作,导致路径非法。如果从非法状态继续走,还可能在不同状态之间反复循环。

解决:路径推断的代码要和训练阶段保持一致,先取legal_actions = find(T(s,:)==1),再在Q(s, legal_actions)里找最大值。修改后路径的每一步都在环境允许的动作范围内。另外一个保险手段是加最大路径长度限制,比如超过 50 步就终止并输出警告,方便定位问题。

5.3 现象:初始状态随机到障碍物

现象:训练刚开始时报错,或者 Q 矩阵某些行一直为 0,路径在这些行附近表现异常。

原因:unidrnd(16)均匀随机产生 1 到 16 的整数,其中 10、11、14 是障碍物。如果直接把这些状态作为训练起点,王子可能从无法通过的位置开始,环境逻辑不自洽。原代码用while ismember(state,[10,11,14])主动规避了这个问题,但如果你修改了障碍物编号,或者复制代码时漏掉了这几行,就会复现这个错误。

解决:把障碍物状态定义成常量,比如obstacles = [10,11,14],在初始化起点和路径起点时都做检查。路径推断的起点也不能是障碍物,否则即使 Q 矩阵正确,也生成不了合法路线。更稳妥的做法是从所有非障碍状态里随机选起点,使用setdiff(1:16, obstacles)去掉非法状态,再从中随机抽取。

5.4 现象:把 epsilon 当作学习率修改,训练结果不如预期

现象:有同学看到注释里写着“策略选择概率,也就是学习率”,于是把 epsilon 从 0.3 改成 0.9,以为能让算法学得更快,结果训练曲线反而变差,甚至无法稳定到达终点。

原因:epsilon 是探索率,不是学习率。探索率控制的是动作选择的随机程度,学习率控制的是 Q 值更新的步幅。把 epsilon 调大,意味着每一步有 90% 概率随机乱走,训练样本的有效性大幅降低;而 Q 值更新仍然没有平滑机制,整体收敛自然更差。

解决:在报告里明确两个参数的分工:epsilon 属于策略选择,alpha 属于价值更新。如果想让算法学得更保守,降低 epsilon,比如 0.1;如果想让 Q 值更平滑,引入 alpha,比如 0.2。修改后再观察路径是否变稳定,你就会发现之前把两个概念混在一起是问题根源。

5.5 现象:T 矩阵整行清零导致合法动作丢失

现象:设置了障碍物后,状态 13 明明还有向上的 9 和向左的 12 两个合法转移,但训练出来的路径在 13 附近总是绕远路,或者只能依赖跳到 15 的传送门。

原因:T(13,:)=0把状态 13 的所有出边全部清空,然后再恢复T(13,15)=1。这不是“只删除 13 到 14 的动作”,而是把 13 变成了一个单向传送点。如果场景设计并不需要传送门,那么 9 和 12 这两个合法连接就被误杀了。

解决:如果只是想禁止进入障碍物 14,应该写T(13,14)=0,或者通过列清零已经覆盖了T(13,14),不需要整行操作。只有确实想做“跳跃”时,才用T(13,:)=0; T(13,15)=1;。在代码注释里注明这里是单向传送门,而不是简单的障碍处理,这样后续维护时不会有人误改。

6. 验收与答辩:用三张图和两个提问证明算法真的有效

课程设计交上去,不能只扔一段能跑通的代码。你需要用可量化的结果证明训练过程收敛了、路径合法了、参数选择有依据。我的建议是准备三张图:第一个是每个 episode 到达终点的步数曲线,第二个是 Q 矩阵相邻两次更新的最大变化量曲线,第三个是画在 4x4 网格上的最终路径图。步数曲线逐渐下降并趋于稳定,说明王子找公主越来越熟练;Q 值变化量趋近 0,说明价值表收敛;路径图避开障碍物和陷阱,说明策略合法。这三张图放在实验报告里,比任何文字描述都有说服力。

答辩时有两个问题几乎必问:第一,“Q 矩阵为什么能收敛?”你要回答贝尔曼更新是在逐步逼近动作价值函数的不动点,因为每次更新都在用真实奖励和下一步的估计价值修正当前估计,误差会逐轮缩小。第二,“epsilon 改成 0 会怎样?”如果一开始就设 0,Q 矩阵全 0 时王子只会选择索引最小的合法动作,永远不会探索其他路线,也就学不到障碍和陷阱的惩罚。这说明探索率在训练初期是不可或缺的。如果能把这两个问题回答清楚,整个算法的核心就算过关了。

演示环节有一个实用技巧:先从一个固定起点跑路径,展示一条合法路线;再从所有非障碍状态随机抽几个起点批量跑,统计成功率;最后展示加 alpha 和不加 alpha 的收敛曲线对比。这一套操作下来,方案就从“跑通了”变成了“验证过了”。从那以后我每次做强化学习实验,都会强制自己先跑一遍随机策略确认环境没写错,再交给 Q-Learning 学习,路径推断时也一定先过滤合法动作。这个习惯帮我避开了很多隐蔽的坑。希望这篇 Q-Learning 的 MATLAB 实现解析,能帮你把课程设计做得更完整,也能在验收时讲清楚每一步到底在做什么。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进