资讯详情

Q-Learning MATLAB实现与调参指南:从Q表到收敛

📅 2026/10/17 17:31:17 | 华诺云谱 👁 阅读
Q-Learning MATLAB实现与调参指南:从Q表到收敛
简介这是一份面向强化学习初学者的Q-Learning算法Matlab源码及注释文档适用于课程设计、期末复习或自学实践。资源用王子寻公主的网格游戏讲解算法环境被定义为16个格子通过可移动矩阵与奖赏矩阵建模障碍、陷阱及终点代码完整实现了epsilon-greedy策略、Q值贝尔曼更新和训练循环同时输出每百次迭代的训练进度并最终基于Q矩阵规划最优路径。每段代码旁均有中文注释从环境搭建到策略选择再到路径回溯过程十分清晰适合对照《强化学习》教材边读边跑实验。包内仅有1个doc文件大小约33KB但信息密度较高打开即可查看完整源码和算法说明。该资源已被浏览学习1378次对有志于理解无模型强化学习机制并动手复现经典算法的读者十分实用。1. Q-Learning 源代码与注释先读懂这套 Matlab 实现再决定怎么改很多人在网上复制过 Q-Learning 的 MATLAB 代码但真正能一次跑通的并不多。常见翻车现场是Q 值矩阵越更新越大然后突然变成 NaN或者智能体在网格世界里原地转圈步数曲线一百个回合都不降。标题这份“Q-Learning 源代码及注释(matlab).doc”本质上就是一套把算法闭环讲完整的带注释实现——它不堆论文公式而是把状态、动作、奖励、Q 表更新这四个核心模块的每一行代码都配上说明。这篇文章就顺着这个方向帮你把这类实现拆开、跑通、调出收敛效果再告诉你什么时候该改环境、什么时候该改参数、什么时候该换算法思路。适合正在学强化学习入门、需要交课程仿真作业、或者想在 MATLAB 里快速验证一个控制想法的人。2. 先看懂 Q-Learning 的决策闭环Q 表、贝尔曼更新与奖励设计在代码里长什么样2.1 Q 表的维度设计状态怎么编码动作怎么编号Q-Learning 的核心数据结构就是一张 Q 表。Q 表的行数是状态总数列数是动作总数。放在网格世界里行数就是格子数列数就是智能体可以执行的动作个数。我在处理这类问题时第一件事永远是先把状态编号和动作编号定死再动手写代码。n_rows 5; % 网格行数 n_cols 5; % 网格列数 n_actions 4; % 动作数上、下、左、右 Q zeros(n_rows * n_cols, n_actions); % 状态数 x 动作数这里最关键的一点是状态编号公式。MATLAB 的矩阵索引从 1 开始所以格子 (row, col) 对应的状态编号是(row-1)*n_cols col。如果写成row*n_cols col第 1 行就会占用编号 6 到 10整个 Q 表的行索引就和实际状态对不上后边所有查表都会错位。我习惯在代码开头就用注释把这个公式标清楚避免索引错乱。动作编号也要和坐标变化严格对应。我常用的约定是1 代表上、2 代表下、3 代表左、4 代表右。这样在环境函数里做坐标更新时只需要判断动作值就能算出下一格的坐标。这个对应关系在调试阶段非常重要因为一旦动作方向和实际移动方向不一致智能体学到的最优策略一定是错的而且这种错误在训练曲线上很难看出来。2.2 更新公式的每一部分如何跟代码对应Q-Learning 的更新公式是Q(s,a) ← Q(s,a) α [ r γ·max_{a} Q(s,a) − Q(s,a) ]。读代码之前一定要清楚这里每个符号对应什么s 是当前状态a 是当前执行的动作s 是执行动作后到达的下一状态r 是获得的即时奖励max_{a} Q(s,a) 是下一状态里所有动作中最大的那个 Q 值。在 MATLAB 里公式翻译过来就是下面这行Q(state, action) Q(state, action) alpha * ... (reward gamma * max(Q(next_state, :)) - Q(state, action));注意max(Q(next_state, :))取的是下一状态所有动作的最大值而不是当前状态。这是 Q-Learning 和 SARSA 最核心的差别Q-Learning 在更新时假设下一步采取贪婪动作而不管实际下一步到底执行了哪个动作。正因为这种“更新在前、执行在后”的错位Q-Learning 属于 off-policy 算法。很多新手在这里把 max 写在当前状态上导致更新公式失去意义学习曲线看起来在降但策略完全没有改进。还有一个工程细节如果奖励全部是负的而 Q 表初始化为全零那么每次更新都在拉低 Q 值所有动作的 Q 值长期为负但相对大小依然有效。如果奖励既有正又有负初始化全零就相当于“乐观初始化”——给每个未知状态一个高于预期的初始值这本身是一种促进探索的手段。这个特性在调参时经常被忽略但它直接影响前几十个回合的行为。2.3 奖励设计的两种思路稀疏奖励与密集奖励对收敛的影响奖励函数决定了智能体往哪个方向优化是整个 Q-Learning 设计的灵魂。常见做法是分成稀疏奖励和密集奖励两类。稀疏奖励只在到达目标时给一个大的正值其他时候奖励为零密集奖励则每一步都给一个小的负值比如 -0.1让智能体倾向于用更短的路径到达目标。% 稀疏奖励版本 if map(next_row, next_col) 2 reward 10; else reward 0; end % 密集奖励版本 if map(next_row, next_col) 2 reward 10; else reward -0.1; % 每一步都扣一点鼓励少走弯路 end这两种设计的区别非常明显。稀疏奖励下智能体前期几乎得不到任何有效的反馈信号Q 表更新缓慢可能要几百个回合才开始出现持续的正收益密集奖励则让智能体每一步都能感知到“走这一步划不划算”学习曲线下降更快适合在地图不大、路径不复杂的场景里用。但密集奖励不是没有代价。负奖励的绝对值如果太大智能体可能宁可原地撞墙也不愿走远路——因为撞墙的惩罚和前进的惩罚没有拉开差距。我一般把撞墙惩罚设为 -0.5普通步进惩罚设为 -0.1目标奖励设为 10。这个比例能让智能体明确感受到三种行为的差别撞墙最亏、绕路小亏、到达目标大赚。3. 把带注释的主循环拆开初始化、ε-greedy 策略与 Q 值更新的核心代码3.1 初始化部分地图矩阵、超参数与 Q 表的建立先给出一个可以直接跑的初始化块。地图我用 5×5 网格两个障碍、一个目标点起点默认在左上角 (1,1)。障碍用 1 标记目标用 2 标记空地用 0 标记。这样的地图矩阵直观后边扩展成 10×10 或者带更多障碍物时也只需要修改 map 矩阵本身主循环不用动。% 环境与超参数初始化 n_rows 5; n_cols 5; n_actions 4; alpha 0.1; % 学习率控制新信息覆盖旧信息的程度 gamma 0.9; % 折扣因子越接近 1 越看重远期收益 epsilon 0.3; % 初始探索率 epsilon_decay 0.995;% 每回合衰减系数 min_epsilon 0.05; % 探索率下限 map zeros(n_rows, n_cols); map(2, 4) 1; % 障碍 1 map(4, 2) 1; % 障碍 2 map(5, 5) 2; % 目标点 Q zeros(n_rows * n_cols, n_actions); total_rewards zeros(1000, 1); steps_per_episode zeros(1000, 1);这里的超参数我解释一下。alpha 取 0.1 是大多数小规模网格问题的安全起点取太大 Q 值会来回震荡太小则前几百回合几乎看不出进步。gamma 取 0.9 表示智能体把未来 10 步左右的累积奖励纳入当前决策在这个 5×5 地图里够用如果地图更大、路径更长我会把 gamma 调到 0.95 以上。epsilon 从 0.3 开始再按每回合 0.995 的系数衰减目的是让智能体前期多探索、后期多利用。3.2 ε-greedy 动作选择探索与利用的平衡动作选择的代码块是强化学习里最容易写错又最难发现错误的地方。ε-greedy 的意思是以概率 epsilon 随机选一个动作以概率 1-epsilon 选当前 Q 值最大的动作。if rand() epsilon action randi(n_actions); % 探索随机动作 else [~, action] max(Q(state, :)); % 利用选 Q 值最大的动作 end这段代码本身很短但有两个细节值得多说。第一rand()每次调用都会返回 0 到 1 之间的新随机数所以“以概率 epsilon 探索”是靠每一次独立的随机判断实现的不需要额外维护计数器。第二max(Q(state, :))返回两个值第一个是最大值第二个是最大值的下标我用~丢弃最大值、用action接收下标。如果 MATLAB 版本较老不支持~占位符可以用[max_val, action]的写法只是max_val会多占用一个变量。这里还有一个隐患如果 Q 表的某一行恰好所有动作的 Q 值都相同max会返回第一个最大值对应的下标也就是动作 1。这意味着一旦进入一个所有 Q 值相等的状态智能体永远偏向动作 1这会破坏探索的均匀性。处理办法是在max之前给 Q 表行加一个很小的随机扰动或者手动从最大值相等的动作里随机挑一个。我在不太重要的实验里一般只做注释提醒不做额外处理。3.3 环境交互函数 step_env 的完整实现与边界处理环境函数负责把“执行一个动作”这件事翻译成“新的坐标 即时奖励 是否结束”。这部分最容易被忽略的是边界和障碍的判定顺序错了会直接影响学习结果。function [next_row, next_col, reward, done] step_env(row, col, action, map) [n_rows, n_cols] size(map); next_row row; next_col col; % 动作映射1上2下3左4右 if action 1 next_row row - 1; elseif action 2 next_row row 1; elseif action 3 next_col col - 1; elseif action 4 next_col col 1; end % 边界检查撞墙则留在原地并给负奖励 if next_row 1 || next_row n_rows || next_col 1 || next_col n_cols next_row row; next_col col; reward -0.5; done false; return; end % 障碍检查和撞墙同样的处理逻辑 if map(next_row, next_col) 1 next_row row; next_col col; reward -0.5; done false; return; end % 目标检查 if map(next_row, next_col) 2 reward 10; done true; return; end % 普通空地每步微小负奖励 reward -0.1; done false; end这段函数的核心逻辑是先尝试移动再统一做合法性检查。我把撞墙和撞障碍分成两个分支但处理结果完全一样这样看图的人能直接看清两种情况的判定路径。reward 和 done 的赋值覆盖了所有可能的分支非法移动、到达目标、普通移动。如果某个分支漏写 return函数就会继续往下执行reward 可能被覆盖成错误的值这类 bug 在调试时很难发现因为在统计累计奖励时数值差异不大但策略完全学歪。在脚本中使用function时要注意位置。新版 MATLAB 允许在脚本文件末尾定义函数旧版本则必须把函数单独存成 m 文件。我一般把step_env写在主脚本末尾因为这样整个项目只有一个文件复制到别的机器上跑不需要额外整理文件关系。如果遇到函数定义报错优先检查 MATLAB 版本是否支持scripts内嵌函数。3.4 训练主循环与学习曲线记录主循环是整份代码的骨架。每个回合从起点出发在最大步数限制内反复执行“选动作 → 走一步 → 更新 Q 表”走到目标或超步数则结束当前回合。n_episodes 1000; max_steps 100; for ep 1:n_episodes row 1; col 1; state (row - 1) * n_cols col; total_reward 0; for step 1:max_steps % ε-greedy 选动作 if rand() epsilon action randi(n_actions); else [~, action] max(Q(state, :)); end % 与环境交互 [next_row, next_col, reward, done] step_env(row, col, action, map); next_state (next_row - 1) * n_cols next_col; % Q 值更新 Q(state, action) Q(state, action) alpha * ... (reward gamma * max(Q(next_state, :)) - Q(state, action)); % 状态推进 state next_state; row next_row; col next_col; total_reward total_reward reward; if done break; end end steps_per_episode(ep) step; total_rewards(ep) total_reward; % 回合结束衰减探索率 epsilon max(min_epsilon, epsilon * epsilon_decay); end主循环里有几个细节值得停下来看一看。第一state变量和row, col是同步更新的这一步容易漏。如果只更新state不更新row, col下一回合传给step_env的坐标还是旧值智能体就永远在原地打转。第二step在 MATLAB 中如果内层循环正常跑完它的值等于max_steps如果中途 break它的值就是实际走的步数。用这个值记录步数曲线非常方便不需要额外计数器。第三探索率衰减放在回合结束而不是每一步结束这样每个回合内的探索概率均匀曲线也更平滑。训练结束后把total_rewards和steps_per_episode两个数组画出来就能直观看到是否收敛。画图代码很简单两条线分别在两个子图里展示。累计奖励曲线应该从负值逐步升高并稳定到接近正值步数曲线应该从上百步逐步降到二十步以内。4. 参数怎么调才不玄学α、γ、ε 的取值范围与学习曲线的判读方法4.1 三个核心参数的影响逻辑Q-Learning 的超参数不多真正影响行为的只有三个学习率 α、折扣因子 γ、探索率 ε。很多人调参靠感觉改一个数跑一次看曲线涨了就以为成功了其实没搞清参数背后的作用方向。α 控制的是每次更新时新信息覆盖旧信息的比例。α 太大Q 值会被最近一步的奖励剧烈拉动表现为学习曲线大幅震荡α 太小Q 值变化缓慢策略需要上千回合才能稳定。网格世界这种小型问题里α0.1 是我的默认值调到 0.05 能让曲线更平滑但需要更多回合调到 0.3 以上则几乎必然出现曲线抖动。γ 控制的是智能体对远期收益的重视程度。γ0 时只看立即奖励策略会变得短视γ0.9 时把未来约 10 步的累积奖励纳入考量γ 越接近 1智能体越愿意为了远处的目标忍受眼前的负奖励。在 5×5 这种地图里最优路径长度不超过十几步γ0.9 足够如果地图扩展到 20×20γ 建议升到 0.95 以上否则远处目标的正奖励经过多次折扣后会小到无法抵消每步的负惩罚。ε 控制探索和利用的平衡。它不是一个固定值而是一个衰减过程。初始 ε 决定智能体前期的“好奇心”衰减系数决定好奇心消退的速度。如果初始 ε 太低智能体在 Q 表还很粗糙的时候就急于利用容易陷进次优路径如果衰减太快同样的问题也会在几十回合后出现。我更多地把 ε 的调节重点放在衰减曲线上而不是初始值。4.2 参数对比表与推荐起点下表是我在类似地图上反复实验后总结的参数起点以及每个参数的调整方向。参数推荐起点太小的影响太大的影响学习率 α0.1收敛慢需要更多回合Q 值震荡曲线毛刺多折扣因子 γ0.9策略短视只看眼前奖励收敛变慢奖励累计易膨胀初始 ε0.3探索不足容易陷局部最优浪费大量回合在随机动作上ε 衰减系数0.995探索过快结束策略定型过早长期维持随机行为无法收敛这张表不是死规矩而是一个调试起点。遇到不收敛的情况我会先固定 α 和 γ只调 ε 的衰减系数如果曲线震荡明显再调 α如果路径总是绕远路再调 γ。一次只动一个参数否则出了变化也无法定位是哪个参数造成的。4.3 用学习曲线判断收敛看步数、看累积奖励学习曲线是判断调参方向的核心工具不要只看最终策略对不对。我一般同时画两张图每个回合的总奖励曲线和每个回合的步数曲线。前者反映整体收益趋势后者直接反映策略质量。figure; subplot(2,1,1); plot(total_rewards); xlabel(回合); ylabel(累计奖励); title(每个回合的总奖励); subplot(2,1,2); plot(steps_per_episode); xlabel(回合); ylabel(步数); title(每个回合的步数);步数曲线比奖励曲线更直观。一个能收敛的实验步数曲线应在几十个回合内快速下降然后在一个低位附近小幅波动。如果步数曲线前期下降但后期反弹说明探索率衰减后智能体可能困在了一条次优路线上。如果步数曲线从头到尾不降问题大概率不在参数而是奖励设计里负奖励过重或正奖励不够突出智能体完全没有动力走向目标。5. Q-Learning 调试避坑收敛不稳定、Q 值爆炸、原地转圈的 4 条排查记录5.1 现象Q 值增长到极大甚至 NaN训练几十个回合后Q 表里的数值变得非常大甚至直接变成 NaN之前学到的策略全部归零。原因奖励值设置太大或者 γ 设置的期望收益累积无上限。比如目标奖励设为 1000γ 设为 0.99Q 值就会在一轮又一轮的更新中不断膨胀最终超过双精度浮点上限变成 NaN。解决把奖励量级控制在 ±1 到 ±10 之间γ 优先取 0.9 而不是 0.99。如果一定要用较大的 γ就在更新公式后做一次数值检查发现abs(Q(state, action)) 1e6就停止训练并打印错误信息。这个检查写起来很快但能省下大量对着 NaN 发愣的时间。5.2 现象智能体一直在起点附近转圈步数曲线不降训练了 200 个回合步数曲线始终在 90 步以上智能体反复在起点周围来回打转偶尔撞墙但就是不往目标方向走。原因探索率衰减过快前几十回合 Q 表还没有形成有效梯度探索就被降到了很低的水平智能体开始频繁利用一个粗糙的错误策略于是陷入局部循环。解决调低衰减系数比如从 0.995 改成 0.99或者把初始 ε 从 0.3 提高到 0.5。也可以做一个最低探索率保护当步数曲线连续 N 个回合没有下降时临时把 ε 拉高到 0.3给智能体一次“打破循环”的机会。这个招数在工程里很实用。5.3 现象MATLAB 报错索引超出边界运行中断训练到中途控制台弹出 “Index exceeds the number of array elements” 错误位置指向 Q 表更新那一行。原因状态编号和 Q 表行数不一致。最常见的情况是环境函数返回的next_row或next_col出现了 0 或 6 这类越界值导致计算出的next_state超过了n_rows * n_cols。或者地图里某个格子被标记成目标但主循环里没有把目标状态对应的行排除在外目标位置也被当作普通状态更新。解决在step_env里先做边界检查再做障碍检查最后做目标检查顺序不能乱。同时打印出越界时的row, col, action, next_state定位是哪个动作组合出了问题。我通常在调试阶段写一个小测试脚本把每个状态、每个动作都跑一遍确保返回值全部合法。5.4 现象Q 值震荡不收敛奖励曲线起伏剧烈每回合奖励值一会儿高一会儿低没有明显的收敛趋势但步数曲线却逐步下降。原因学习率 α 偏大导致新一步的奖励过度覆盖已有的 Q 值。当智能体偶然走出一条好路径Q 表被大幅拉高下一回合如果走了坏路径又被大幅拉低形成剧烈震荡。解决把 α 从 0.1 降到 0.05 或 0.03试跑 500 回合再观察曲线。如果震荡依然存在检查奖励函数是否在相同状态给出不一致的返回值。另一个隐蔽原因是随机种子没有固定导致每次运行结果都不同——我建议训练时手动固定rng(2024)这样每次跑出来的曲线可以精确对比。rng(2024); % 固定随机种子保证实验结果可复现这条经验虽然是老生常谈但在调试参数时极为重要。没有固定随机种子你根本无法区分曲线变化是因为调参生效了还是随机性造成的。6. 从 Demo 到自己的任务改环境、加可视化、往 SARSA 演进的三个切入点网格世界只是 Q-Learning 的最小载体。把这份代码迁移到自己的任务里我一般从三个方向入手。第一个方向是改环境。把 5×5 地图换成自己的二维栅格地图只需改map矩阵的行列数和数值标记如果是连续状态空间比如机器人导航里的距离读数则需要做状态离散化把连续量按区间切分后编号再填入 Q 表。离散化粒度太粗会丢失关键信息太细会让 Q 表膨胀到内存爆炸这个平衡需要根据实际任务反复试。第二个方向是加可视化。训练结束后从 Q 表提取每个状态的最优动作画成策略箭头图可以直观看到智能体是不是真的学会了绕开障碍走向目标。policy zeros(n_rows, n_cols); for r 1:n_rows for c 1:n_cols state (r-1)*n_cols c; if map(r,c) 1 policy(r,c) 0; elseif map(r,c) 2 policy(r,c) 9; else [~, policy(r,c)] max(Q(state,:)); end end end imagesc(policy);第三个方向是往 SARSA 演进。把更新公式里max(Q(next_state, :))换成Q(next_state, next_action)其中next_action是实际执行的下一个动作Q-Learning 就变成了 SARSA。这一行改动就能让智能体从 off-policy 变成 on-policy行为更保守适合安全敏感的任务。理解这行差异后再去读进阶资料会顺畅很多。这套代码我前前后后跑过很多次现在养成的习惯是每次改动只动一个环节固定随机种子先看步数曲线再看奖励曲线确认稳定后才继续下一步。Q-Learning 的魅力就在于足够简单你可以用肉眼看清每一个组件是怎么配合的。希望帮到你照着这份思路跑通一次后面再接触深度强化学习时就不会觉得概念悬空了。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑