☰
从零看懂Q-Learning:16×16网格世界王子找公主源码拆解与避坑指南
2026/10/17 17:47:18 网站建设 项目流程

简介:面向强化学习初学者的Q-Learning算法MATLAB实现与注释文档,以王子寻找公主的16格网格世界为实验环境,演示无模型场景下如何通过试错逐步优化决策策略。文档为1个doc文件,压缩包仅33KB,覆盖可移动矩阵、奖赏矩阵、Q矩阵初始化、折扣因子与探索率设置、ε-greedy策略选择以及贝尔曼方程更新核心代码,并配有逐行中文注释,便于对照理论理解。训练过程设置900次迭代,随机初始状态会避开障碍物,每100次输出一次进度,最终通过最大Q值回溯生成从起点到公主所在位置的最优路径,完整展示强化学习“探索-利用”平衡和累积奖励设计思路。已有1378人学习,适合机器学习入门阶段对Q-Learning原理感兴趣,或需要轻量化MATLAB示例辅助实验的读者;阅读后能获得可运行调试的算法骨架、参数调节经验以及从矩阵建模到策略输出的排错思路。

1. 强化学习入门就从这个 16×16 网格世界开始:Q-Learning 王子找公主源码拆解

这份 Q-Learning 源码把强化学习最经典的算法压在了不到百行的 MATLAB 代码里,场景是一个 16 格的编号网格世界:王子从任意位置出发,上下左右移动,目标是走到 16 号格找到公主,同时躲开 7 号格的陷阱,还能利用 13 号格跳到 15 号格的“近道”。代码不长,但环境矩阵、奖励矩阵、epsilon-greedy 策略、贝尔曼更新、路径回溯全齐了,非常适合刚接触强化学习、想把 Q 表更新公式落到矩阵运算上的读者。比看公式推导更直接的是:你能亲眼看到 Q 矩阵从全零变成一条可走的路径,这也是我做强化学习 Demo 时最常用的一份复现底稿。

2. 环境建模:可移动矩阵、障碍物与跳跃规则是怎么写进代码的

强化学习三要素在源码里的映射很清晰:状态就是 16 个格子,动作只有上、下、左、右四种,奖励则全部收在奖赏矩阵 R 里。这个例子没有用复杂的仿真环境,而是用两个 16×16 矩阵把整个游戏规则“编码”进代码,理解清楚这一步,后面训练循环读起来会顺很多。

2.1 四个移动方向与 16×16 状态转移矩阵

源码先用一个 16×16 的全零矩阵来存“从 i 号格子能否移动到 j 号格子”,1 表示可移动,0 表示不可移动。这个矩阵我一般称它为状态转移矩阵,它其实就是环境的静态规则表。构建部分的代码我加了注释:

% 定义可移动矩阵 T,T(i,j)=1 表示从 i 可以移动到 j T = zeros(16); for i = 1:16 % 上行动作 N:从 i 移动到 i-1 % 1,5,9,13 是每一列的最上方一格,不能再往上 if ~ismember(i, [1,5,9,13]) T(i, i-1) = 1; end % 下行动作 S:从 i 移动到 i+1 % 4,8,12,16 是每一列的最下方一格,不能再往下 if ~ismember(i, [4,8,12,16]) T(i, i+1) = 1; end % 左行动作 W:从 i 移动到 i-4 if i > 4 T(i, i-4) = 1; end % 右行动作 E:从 i 移动到 i+4 if i < 13 T(i, i+4) = 1; end end

这里的编号规则是按列排的:1-4 是第一列,5-8 是第二列,9-12 是第三列,13-16 是第四列。所以 i-1 和 i+1 是在同一列内上下走,i-4 和 i+4 才是左右跨列。边界条件写得很明确:最上方一格不能再上、最下方一格不能再下、第一列不能左、第四列不能右。判断ismember(i,[1,5,9,13])其实就是判断 i 是否落在每一列的顶部。

我每次复现这段代码时,都会顺手把 T 矩阵打印出来看一眼,确认对角线方向上有没有多余的 1 或者漏掉的 0。因为T(i,i-1)这类赋值方式如果边界判断写错,很容易把对角线上的非法动作放进去,而这个错在训练阶段很难察觉,只会表现为某些路径特别怪。

2.2 障碍、陷阱与跳跃:奖赏矩阵 R 的设定逻辑和三个隐藏细节

环境规则定义完后,源码接着做了三件事:设置障碍物、设置跳跃、设置奖励。代码不长,但信息量很大:

% 定义不可达到区域(障碍物):10,11,14 三列全部置 0 % 表示任何状态都不能移动到这三个格子 T(:,[10,11,14]) = 0; % 定义跳跃:13 号位置只能跳到 15 号位置 T(13,:) = 0; T(13,15) = 1; % 定义奖赏矩阵 R,默认进入任意格子都是 -1 R = -1 * ones(16); % 进入 6 号格奖励 0(安全通道) R(:,6) = 0; % 进入 7 号格惩罚 -5(陷阱区域) R(:,7) = -5; % 到达 16 号格奖励 +10(终点,公主所在地) R(:,16) = 10; % 从 13 跳到 15 额外奖励 +5 R(13,15) = 5; % 初始化 Q 矩阵为全零 Q = zeros(16);

这里要注意 R 的赋值方式是R(:,k),意思是“所有状态执行动作后进入 k 号格”都拿到对应奖励。也就是说,奖励绑定的是目标格子,不是动作本身。这是强化学习里很常见的设定——进入某个状态本身带有好坏属性。

三个容易忽略的细节:

第一,T(:,[10,11,14])=0清零的是列,不是行。也就是说,10、11、14 这三个格子“不能作为目的地”,但它们作为出发地的动作行还保留着。底层逻辑是训练起点用while循环避开了这三个格子,所以 Q 更新几乎不会从它们开始启动。如果你直接把这段环境代码拿去做别的实验,最好把T(10,:)、T(11,:)、T(14,:)也置零,否则会出现“从障碍物出发”这种逻辑上说不通的动作。

第二,跳跃的设计是单向的:13 号只能跳到 15 号,但 15 号不能跳回 13 号。T(13,:)=0先把 13 号所有出边清空,再单独打开T(13,15)=1。这种写法比逐个赋值干净,是环境建模里值得借鉴的小技巧。

第三,R 矩阵默认全是 -1,意味着几乎所有移动都会带来一个小的负奖励。这个设定会让 Q 矩阵大部分值是负数,但这是正常的,不是 bug。负奖励相当于给王子一个“时间成本”的暗示,让它更倾向于走短路径,而不是毫无目的地乱逛。

3. 训练循环:epsilon-greedy 策略、Q 值更新与两个核心参数怎么调

环境矩阵定义好后,真正跑强化学习的核心是训练循环。源码的循环结构很标准:随机起点、循环直到终点、在每个状态选动作、执行动作后立即更新 Q 值。这一章把训练主循环逐段拆开讲,再聊两个关键参数的取值逻辑。

3.1 主循环拆解:随机起点、可选动作和贝尔曼更新的一次往返

训练部分源码我加上了行级注释:

% 折扣因子 gamma,控制未来奖励的重要程度 gamma = 0.8; % epsilon 是探索概率,30% 的概率随机动作,70% 的概率选当前最优 epsilon = 0.3; % 训练 900 轮 for episode = 1:900 % 随机初始化起点,并避开障碍格 10,11,14 state = unidrnd(16); while ismember(state, [10,11,14]) state = unidrnd(16); end % 一直训练到到达终点 16 为止 while state ~= 16 % 找出当前状态下所有可执行的动作 possible_actions = find(T(state,:) == 1); possible_Q = Q(state, possible_actions); % epsilon-greedy:以 epsilon 概率随机探索 if rand() < epsilon choose = unidrnd(length(possible_actions)); action = possible_actions(choose); else % 否则选择 Q 值最大的动作 [~, index] = max(possible_Q); action = possible_actions(index); end % 贝尔曼更新 Q(state, action) = R(state, action) + gamma * max(Q(action,:)); % 移动到下一个状态 state = action; end % 每 100 轮输出一次进度 if mod(episode, 100) == 0 fprintf('训练次数:%d\n', episode); end end

这段代码最值得看的是更新那一行:Q(state,action) = R(state,action) + gamma * max(Q(action,:))。它对应的就是贝尔曼最优方程里“当前奖励 + 折扣后的未来最优价值”这一项。max(Q(action,:))是在计算下一状态所有可能动作的 Q 值里最大的那个,代表“假设下一步也采取最优策略”的价值。

需要注意的是,这个更新是直接赋值,不是增量更新。标准 Q-Learning 更新式其实是Q(s,a) = Q(s,a) + alpha * (R + gamma * max(Q(s',a')) - Q(s,a)),源码相当于把学习率 alpha 直接取成了 1。这样的好处是收敛快,坏处是如果奖励矩阵噪声大,Q 值会抖动。不过在这个 16 格小世界里,状态空间小、规则确定,用 alpha=1 完全没有问题。

unidrnd(16)是均匀随机取 1 到 16 的整数,这里就是随机初始化起点。find(T(state,:)==1)返回当前状态所有可移动目标格的索引。possible_Q只取出当前状态下可行动作对应的 Q 值,避免在不可行动作上做 argmax。这几个小细节新手很容易抄错,尤其是possible_Q的索引过滤,漏掉它会让算法去更新根本不能执行的动作。

3.2 gamma 与 epsilon 的选择:收敛速度与探索深度的权衡

源码里 gamma=0.8、epsilon=0.3 其实是两个非常保守但常用的默认值。我把参数的作用和调法整理成了一张表:

参数当前值作用调大调小
gamma0.8折扣因子,控制未来奖励相对当前奖励的权重更看重远期收益,适合长路径规划更看重眼前收益,适合短视策略
epsilon0.3探索概率,决定随机动作的占比探索更多,能发现新路径但收敛变慢更早利用已有策略,可能过早陷入局部最优

gamma 在这个例子里调到 0.9 后,路径会更倾向于绕开 7 号陷阱、优先走 13→15 的跳跃,因为远期收益被放大了。而调到 0.5 以下时,王子很容易被 6 号格的 0 奖励吸引,停留在“安全但离终点远”的区域,因为未来收益打个五折后,终点那 10 的奖励已经不够诱人了。

epsilon 的调节要结合训练轮数看。如果 900 轮训练后起点 1 到终点 16 的路径还是绕远,我一般先把 epsilon 提到 0.4 增加探索,而不是急着加训练轮数。反过来,如果路径已经稳定但偶尔会抽到随机动作绕路,说明 epsilon 偏大,降到 0.2 会更快收敛。

这个案例里还有一个容易被误解的点:源码注释把 epsilon 叫作“策略选择概率,也就是学习率”,但它并不是严格意义上的学习率。真正的学习率是更新公式里控制“旧 Q 值保留多少、新信息吸收多少”的 alpha,源码里没有显式定义。我在下面第四章节里会专门讲这个。

4. 避坑与常见问题:这个 Q-Learning 案例里最容易被误解的五个细节

这一段写的是我复现这份源码时真实踩过的坑,以及从结构上看起来不明显、但跑起来会让人困惑的地方。每条都按“现象 → 原因 → 解决”的线索整理,新手直接照做,能少走很多弯路。

4.1 “TT 定义、T 使用”的变量名不一致:复制代码后第一处报错

现象:源码开头写的是TT=zeros(16),但后面所有逻辑用的都是T,第一次运行会报“未定义变量 T”或者“索引超出矩阵维度”。

原因:原文档可能是从某个调试版本里摘出来的,声明变量的名字和实际使用变量没统一。MATLAB 工作区如果之前跑过别的脚本,恰好残留了同名变量,这种错误还不会第一时间暴露,而是带着旧数据往下跑,结果更隐蔽。

解决:复制代码后先统一变量名。我一般直接在头部把声明改成T = zeros(16),然后全选搜索一遍确保没有TT残留。如果是在已有工作区里反复调试,先执行一次clear all或clear T,避免旧变量串台。

4.2 epsilon 被叫成“学习率”:探索率不是学习率,源码里没有 α

现象:很多读者看到注释写着“策略选择概率,也就是学习率”,就把 epsilon 当成了 Q 值更新幅度,调大后路径反而上下乱跳。

原因:epsilon 控制的是“有多大概率随机选动作”,它只影响策略,不直接影响 Q 值更新的步长。源码里没有学习率 alpha,更新行是直接赋值Q(state,action) = R + gamma * max(Q(_,:)),相当于 alpha=1,每次更新都完全覆盖旧值。

解决:把这个概念在脑子里拆开。如果想让 Q 值更新更平滑,按标准形式写:Q(s,a) = Q(s,a) + alpha * (R + gamma * max(Q(s',:)) - Q(s,a)),alpha 取 0.1 到 0.5 之间。在状态空间大的场景下,alpha=1 会导致 Q 值震荡,但在 16 格的例子里直接用源码写法没问题。

4.3 负奖励矩阵让 Q 值全为负?这不代表训练失败

现象:900 轮训练完,打印 Q 矩阵发现绝大多数格子是负值,怀疑是不是奖励设计错了、Q-Learning 没收敛。

原因:R 默认是 -1,只有进入 6 号是 0、进入 16 号是 10,所以大部分状态-动作对的 Q 值算出来都是负的。反复加总负值和打折的折扣因子时,这些负数相互叠加,Q 矩阵整体偏负是正常现象。

解决:不要看 Q 值的正负,看相对高低。以起点 1 为例,通往 16 方向的动作 Q 值会明显高于朝反方向的动作。我一般会打印终点的邻接状态:比如 15 号状态中往 16 走的 Q 值接近 10,而往 14 走的 Q 值则远低于它,这就是收敛的微观证据。

4.4 路径回溯可能原地绕圈:只看 max(Q) 不处理平局

现象:设置起点为 1 后,回溯出来的路径出现 1→2→1→2 这类反复横跳,甚至始终到不了 16。

原因:原回溯代码的伪逻辑是“每次取 Q 值最大的动作,一直走到终点”。但如果有多个动作的 Q 值相同,max返回的是第一个最大值的索引,不一定是逻辑上的最优方向。当两个格子互相把对方当作“次优解”时,回溯就会在两个状态间循环,终点条件永远触发不了。

解决:给回溯加一个访问记录和步数上限。我在复现时习惯这样改:

visited = zeros(1,16); step = 0; max_step = 20; while path(end) ~= 16 && step < max_step [~, next] = max(Q(start_0,:)); % 发现访问过同一个格子,说明陷入了循环 if visited(next) == 1 fprintf('路径在第 %d 步进入循环,建议重新训练\n', step); break; end visited(next) = 1; path = [path, next]; start_0 = next; step = step + 1; end

加了max_step后,即使陷入平局也能快速退出,不会让脚本卡死。如果路径在中途 break,我多半会把 epsilon 调大或者增加 episode 数量重新训练。

4.5 障碍格列清零但行没清零:从障碍“出发”的动作仍然存在

现象:训练完手动查看 Q 矩阵时,发现 10、11、14 这几个格子对应的行里还有非零值,好像“障碍物也会主动行动”。

原因:T(:,[10,11,14])=0只清零了这三列,也就是禁止所有状态“移动到”它们;但T(10,:)、T(11,:)、T(14,:)这几行没动,从障碍格出发的动作仍是 1。训练里随机起点被 while 循环避开,所以不影响学习;但 Q 矩阵里如果恰好有其他路径经过它们,就会产生语义矛盾。

解决:如果你想把障碍做彻底,在环境初始化阶段把障碍状态对应的行也置零:

T([10,11,14],:) = 0; T(:,10) = 0; T(:,11) = 0; T(:,14) = 0;

这样障碍格就变成了真正不可进也不可出的“死块”。不过要注意,这样改之后,原本从某些格子能移动到 10/11/14 的动作全部失效,等价于改变了原有地图结构,训练出的路径会和原版有差异。

5. 路径回溯与结果验证:把训练好的 Q 矩阵变成一条可直接巡检的路线

训练只是让 Q 矩阵收敛,真正检验训练效果的是把 Q 矩阵“翻译”成一条路径。这个案例的一个优点是环境足够小,训练完成后可以立刻从任意起点回溯到终点,整个过程肉眼可见,非常适合用来验证对 Q-Learning 的理解。

5.1 从任意起点回溯路径:源码怎么用

回溯部分的源码很短,核心逻辑就是不断选择 Q 值最大的动作:

% 设定起点 start_0 = 1; % path 以起点开始 path = start_0; % 反复选择 Q 值最大的动作,直到到达终点 16 while path(end) ~= 16 [~, next] = max(Q(start_0,:)); path = [path, next]; start_0 = next; end disp(path);

以起点 1 为例,训练正常的话这条路通常会走成 1→5→9→13→15→16,也就是:第一列向上走到 13 号,利用跳跃到 15 号,再向下移动到 16 号终点。这个路径避开了 7 号陷阱,也没有绕到 10/11/14 障碍区,说明奖励矩阵里的负惩罚和跳跃正值共同起了作用。

我一般在跑完回溯后,会顺手验证两个边界:一是把起点改到 2、3 等中间位置,确认任意起点都能收敛到终点;二是检查路径长度,如果起点 1 走出来的路径超过 8 步,我会认为训练没有充分收敛。

5.2 验证技巧:打印 Q 矩阵并观察关键位置的数值梯度

回溯路径只是“结果正确”,想确认“训练过程正确”,我还会打印一次 Q 矩阵的关键行。重点看 15 号状态和 13 号状态:15 号往 16 方向的动作 Q 值应该最高,13 号往 15 方向的动作 Q 值也应该显著高于其他方向。

如果 15 号状态里往 16 的 Q 值只有 3 左右,说明 gamma 设置的未来奖励权重偏低,或者训练轮数不够,终点的 10 分奖励还没完全传导到前面几个状态。这时候我一般会做两件事:把 gamma 提到 0.9,或者把 episode 从 900 加到 1500。

还有一个很实用的技巧:把 Q 矩阵转成热力图看。16×16 的矩阵直接看数字不够直观,但画成图像后,能一眼看出终点 16 列上方块颜色明显更深,而 7 号陷阱附近颜色偏暗。这种视觉检查在碰到更大的状态空间时尤其好用,能快速定位“哪个区域没有被训练覆盖”。

从那以后,我每次复现 Q-Learning 的 Demo,不管状态空间多小,都强制走一遍“打印 Q 矩阵关键行 + 带步数上限的路径回溯 + 热力图检查”这条流程。很多以为训练失败的情况,其实只是循环或平局没处理;很多以为成功的路径,也可能只是因为运气好跳过了一个未覆盖状态。这套验证习惯帮我省了不少调试时间。希望帮到你。

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

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

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

立即咨询