☰
Q-Learning MATLAB实现与调参指南:从Q表到收敛
2026/10/17 17:31:15 网站建设 项目流程

简介:这是一份面向强化学习初学者的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.99,Q 值就会在一轮又一轮的更新中不断膨胀,最终超过双精度浮点上限变成 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 的魅力就在于足够简单,你可以用肉眼看清每一个组件是怎么配合的。希望帮到你,照着这份思路跑通一次,后面再接触深度强化学习时就不会觉得概念悬空了。

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

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

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

立即咨询