简介:这份资源是面向强化学习初学者与算法实践者的Q-Learning入门案例,以Matlab源代码加中文注释的形式,讲解无模型环境下如何通过试错学习最优决策策略。案例将抽象算法落地为“王子寻找公主”的16x1网格游戏,涵盖可移动矩阵TT、奖赏矩阵R、Q矩阵初始化、折扣因子gamma与epsilon-greedy策略等核心概念,并演示Q值按贝尔曼方程迭代更新、训练结束后回溯最优路径的完整流程。资源包共1个doc文件,约33KB,内容为可直接运行的Matlab脚本与逐行注释,便于对照理解环境建模、状态动作定义、奖励机制与策略选择各环节。目前已有1378人学习下载,适合希望用最小案例快速吃透Q-Learning基本流程、并动手复现调参的读者参考。
1. 从一份 MATLAB 脚本说起:16 格网格里的 Q-Learning 到底怎么跑起来
很多人第一次接触强化学习,是从一份几十行的 MATLAB 脚本开始的。这份名为 Q-Learning 源代码及注释的文档,核心就是一个 16 状态网格世界:王子从任意格子出发,避开障碍和陷阱,走到第 16 格找到公主。它没有神经网络,没有环境模拟器,只有两个矩阵 T 和 R,外加一个 Q 表。但恰恰是这种“裸写”的实现,把 Q-Learning 的贝尔曼更新、ε-greedy 探索、折扣因子这些概念全部暴露在代码里,没有黑匣子。如果你正在找一份能逐行读懂、能直接跑出路径的强化学习入门代码,这份 MATLAB 脚本就是那个“最小可运行系统”。它适合刚学完理论但不知道怎么落地的新手,也适合想快速验证 Q 表更新逻辑的熟手。下面我从矩阵定义开始,把这份代码拆成可复现的步骤。
2. 环境建模:T 矩阵和 R 矩阵怎么定义才不出错
2.1 可移动矩阵 T 的构造逻辑
这份代码最巧妙的地方,是用一个 16×16 的 T 矩阵描述状态转移。T(i,j)=1 表示可以从位置 i 移动到位置 j,0 表示不可移动。对于 16 个格子排成一列的情况,上下左右四个动作对应的是索引加减 1 和加减 4。代码里用四个条件分支分别处理:
clear % 定义可移动矩阵 T T = zeros(16); % T(i,j)=0 表示不可从 i 位置移动至 j 位置,1 则为可移动 for i = 1:16 % 定义上行动作 N if(not(ismember(i,[1,5,9,13]))) T(i,i-1) = 1; end % 定义下行动作 S if(not(ismember(i,[4,8,12,16]))) T(i,i+1) = 1; end % 定义左行动作 W if(i > 4) T(i,i-4) = 1; end % 定义右行动作 E if(i < 13) T(i,i+4) = 1; end end这段循环里,ismember(i,[1,5,9,13])判断的是当前格子是否在第一列。如果是,就不能再往上走,因为 i-1 会跳到上一行的末尾,造成错误连接。同理,[4,8,12,16]是第四列,不能往下走。左动作要求 i>4,右动作要求 i<13,分别对应不能从第一行往上和最后一行往下。参数上,16 是状态总数,4 是每行格子数,这两个数决定了索引运算的边界。我一般会先把 T 矩阵用spy(T)画出来看一眼,确认没有跨行错连,再往下写奖励。
2.2 障碍物、跳跃和奖赏矩阵 R 的设置
T 矩阵定义完基本移动后,代码用三行覆盖了特殊规则:
% 定义不可达到区域(障碍物) T(:,[10,11,14]) = 0; % 定义跳跃 T(13,:) = 0; T(13,15) = 1;第一行把第 10、11、14 列全部置零,意味着任何格子都不能移动到这三个位置,它们成了障碍物。第二、三行先把第 13 行清空,再单独设置 T(13,15)=1,表示从 13 号格子可以一步跳到 15 号格子,这是一个特殊传送通道。这种“先清后设”的写法很常见,避免遗漏其他动作。
奖赏矩阵 R 的维度与 T 一致,初始化为 -1:
% 定义奖赏矩阵 R R = -1 * ones(16); R(:,6) = 0; R(:,7) = -5; R(:,16) = 10; R(13,15) = 5;这里 R(:,6)=0 表示移动到 6 号格子奖励为 0,R(:,7)=-5 表示掉入 7 号陷阱扣 5 分,R(:,16)=10 表示到达公主得 10 分,R(13,15)=5 是跳跃的额外奖励。注意 R 是 16×16 矩阵,但代码只按列赋值,意味着从任何状态移动到第 6 列都是 0 奖励。这种简化在网格世界里没问题,因为状态转移只依赖目标位置。参数上,-1 是每一步的基础代价,促使智能体尽快到达终点;10 和 -5 是正负锚点,让 Q 值有区分度。
提示:T 和 R 的维度必须一致,且 T 中为 0 的位置在 R 中对应的值不会被用到,但建议也置零或保持默认,避免调试时混淆。
3. Q 表更新与 ε-greedy 策略:训练循环里的每一步
3.1 初始化参数与训练主循环
Q 表初始化为全零,折扣因子 gamma=0.8,探索概率 epsilon=0.3:
% 将 Q 矩阵初始化为 0 Q = zeros(16); % 折扣因子(γ) gamma = 0.8; % 策略选择概率,也就是学习率 epsilon = 0.3;gamma=0.8 表示未来奖励打八折,这个值在 16 步以内的网格里比较合适,太大容易震荡,太小会短视。epsilon=0.3 意味着 30% 的概率随机探索,70% 的概率选当前 Q 最大的动作。训练循环跑 900 个 episode:
for episode = 1:900 % 随机化初始状态 state = unidrnd(16); while ismember(state,[10,11,14]) state = unidrnd(16); end 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 endunidrnd(16)随机取 1 到 16 的整数,while ismember确保初始状态不在障碍物上。find(T(state,:)==1)找出当前状态所有可行动作。rand()<epsilon决定探索还是利用。更新公式Q(state,action) = R(state,action) + gamma * max(Q(action,:))是 Q-Learning 的核心:当前状态动作的 Q 值等于即时奖励加上下一状态最大 Q 值的折扣。注意这里直接用了max(Q(action,:)),没有乘以转移概率,因为 T 矩阵是确定性的,转移概率为 1。
3.2 路径提取与结果验证
训练结束后,从起点 1 开始,每次选 Q 值最大的动作,直到到达 16:
start_0 = 1; path = start_0; while path(end) ~= 16 [~,next] = max(Q(start_0,:)); path = [path,next]; start_0 = next; end path这段代码输出一条从 1 到 16 的路径。如果训练充分,路径应该避开 10、11、14 和 7 号陷阱,并且可能利用 13 到 15 的跳跃。我一般会跑三次看路径是否稳定,如果每次路径都不一样,说明 epsilon 偏大或者训练次数不够。参数上,900 次 episode 对这个 16 状态问题是足够的,但如果你改了障碍物位置,可能需要增加到 2000 次。
注意:路径提取时用的是
max(Q(start_0,:)),如果多个动作 Q 值相同,MATLAB 会返回第一个最大值的索引,这可能导致路径不是唯一最优,但仍然是合法路径。
4. 避坑与排查:这份代码跑不通时先查这五处
4.1 现象:训练结束后路径绕远路或卡在障碍物前
原因:T 矩阵的障碍物设置与 R 矩阵的奖励冲突。比如 T(:,[10,11,14])=0 禁止进入这些格子,但 R 矩阵没有对应置零,导致 Q 更新时仍然可能选到这些动作。解决:在定义 R 之后,加一行R(T==0) = 0;,把所有不可移动位置的奖励清零,避免 Q 值被污染。
4.2 现象:Q 值发散到很大或 NaN
原因:gamma 设置过大,或者更新公式里用了max(Q(action,:))但 action 本身是障碍物。解决:检查 gamma 是否在 0.5 到 0.9 之间,并确保possible_actions非空。如果某个状态没有可行动作,find返回空,unidrnd(0)会报错。可以在循环里加if isempty(possible_actions), break; end。
4.3 现象:每次运行路径都不同,无法复现
原因:unidrnd和rand没有固定随机种子。解决:在训练循环前加rng(42),这样每次运行结果一致。注意 MATLAB 版本不同,rng的默认生成器可能不同,但固定种子后至少同一版本内可复现。
4.4 现象:训练 900 次后仍然找不到终点
原因:epsilon 太大,智能体一直在随机探索;或者奖励设置太稀疏,只有终点有正奖励。解决:把 epsilon 降到 0.1,或者在中间状态加一些小的正奖励引导。另外检查 T(13,15)=1 是否被后面的T(13,:)=0覆盖,顺序不能反。
4.5 现象:路径提取时陷入死循环
原因:Q 表在某些状态上所有动作的 Q 值相同,max每次返回同一个索引,导致在几个状态之间循环。解决:在路径提取时加一个访问计数,如果某个状态重复出现超过 3 次,就强制随机选一个未访问过的动作。或者训练时增加 episode 数,让 Q 值拉开差距。
提示:MATLAB 的
unidrnd在较新版本中推荐用randi替代,但这份代码用的是旧版函数,如果你在 R2016b 之后运行,unidrnd仍然可用,但会有警告。可以全局替换为randi(16)。
5. 进阶技巧:把 Q 表导出成策略表,以及三个参数调优习惯
这份代码跑通之后,最有价值的不是那条路径,而是 Q 矩阵本身。你可以把 Q 表导出成 CSV,用writematrix(Q,'q_table.csv'),然后在 Python 或 Excel 里画热力图,直观看到每个状态的最优动作。我一般会做三件事:第一,固定rng(1)跑 10 次,统计到达终点的平均步数,如果步数波动超过 2,说明 epsilon 还是偏大;第二,把 gamma 从 0.8 调到 0.9 和 0.7 各跑一遍,看路径是否变化,通常 0.8 在这个网格里最稳;第三,把 epsilon 改成动态衰减,比如epsilon = 0.3 * (1 - episode/900),前期多探索,后期多利用,路径会更短。
还有一个容易忽略的点:这份代码的 Q 更新用的是max(Q(action,:)),而不是max(Q(action, possible_actions))。在标准 Q-Learning 里,下一状态的最大 Q 值应该只考虑可行动作。如果 action 的下一状态有障碍物,max(Q(action,:))可能取到不可达位置的值。虽然在这个 16 格问题里影响不大,但换成更大网格就会翻车。我后来养成的习惯是,每次写 Q 更新,都强制用next_actions = find(T(action,:)==1); max(Q(action, next_actions)),多写一行,少一个玄学 bug。
从那以后,我每次拿到一份 Q-Learning 代码,都先跑一遍固定种子,导出 Q 表,再手动算一遍贝尔曼更新,确认代码和公式对得上。希望这份拆解能帮你把这份 MATLAB 脚本真正跑起来,而不是只停留在收藏夹里。
本文还有配套的精品资源,点击获取