
简介这份资源面向强化学习初学者与算法实践者聚焦Q学习与SARSA两类经典时序差分算法的代码实现帮助读者通过可运行脚本理解离策略与同策略更新的差异。压缩包共10个文件以8个MATLAB源码.m为主另含1份教学演示文稿.pptx和1个参数数据文件.mat整体约337KB体积轻便便于本地调试与二次修改。源码覆盖基础Q学习、SARSA、带资格迹的改进版本、动作选择策略、收敛判断及参数配置等模块演示文稿则补充了自适应干扰样式选择中强化学习的应用背景。已有241人学习适合希望从公式推导过渡到动手复现的读者可借助参数调整与环境修改进一步探索策略迭代、深度Q网络等进阶方向。1. 从一份名为「强化学习.rar_earn6w6_q学习_sarsa_sarsa算法_强化学习q算法」的压缩包说起很多人第一次接触强化学习是从一个命名混乱的压缩包开始的里面塞着几个.py文件文件名里带着earn6w6、q学习、sarsa这些词打开一看是几百行没有注释的循环。你隐约知道它跟「智能体在环境里试错、拿奖励、更新策略」有关但真要把 Q 学习和 SARSA 的区别讲清楚或者把代码跑起来看到收益曲线往上走中间隔着一堆具体问题状态怎么离散化、ε-greedy 的 ε 怎么退火、Q 表和 SARSA 的更新到底差在哪一步、为什么同样的环境一个收敛一个震荡。这篇笔记就围绕这个标题里的核心——Q 学习与 SARSA 算法——把强化学习入门阶段最常被问到的四件事讲透它们是什么、代码怎么写、参数怎么调、坑在哪。适合两类人刚学完马尔可夫决策过程想动手写第一个智能体的新手以及写过 DQN 但回头发现表格法基础不牢、想补课的从业者。全文不依赖任何特定框架纯 NumPy 就能复现最后会落到一个能直接跑的悬崖行走Cliff Walking对比实验上。2. Q 学习与 SARSA同源异路的两个更新公式2.1 从贝尔曼方程到「谁来选下一个动作」Q 学习和 SARSA 都属于时序差分Temporal DifferenceTD控制方法目标都是学一个动作价值函数 Q(s,a)表示在状态 s 下执行动作 a 之后按某种策略继续走下去能拿到的期望累计回报。两者共用同一个更新骨架Q(s,a) ← Q(s,a) α · [目标 - Q(s,a)]差别全在「目标」怎么算。Q 学习的目标是目标_Q r γ · max_a Q(s, a)SARSA 的目标是目标_SARSA r γ · Q(s, a)其中 a 是实际在 s 状态下按当前策略选出来的下一个动作。这个区别看着只有一行但它决定了两个算法的性格Q 学习用max直接拿下一状态的最优估计不管当前策略实际会不会选那个动作所以它是off-policy——学习的策略目标策略和行为的策略行为策略可以不同。SARSA 用实际选出的 a学的是「我当前这套带探索的策略到底值多少」所以它是on-policy。名字的来历也在这里SARSA 是 (s, a, r, s, a) 五元组的缩写更新时必须先知道下一个动作 a 才能算目标所以它的循环结构天然是「先选动作、执行、观察、再选下一个动作、更新」。Q 学习不需要提前知道 a它直接对 s 所有动作取最大循环结构更自由。2.2 为什么这个差别在悬崖行走里被放大悬崖行走是 Sutton Barto 书里的经典环境一个 4×12 的网格起点在左下终点在右下最下面一行除了起点和终点都是悬崖。掉下悬崖奖励 -100 并回到起点每走一步奖励 -1。在这个环境里Q 学习会学到一条贴着悬崖边走的最优路径——因为它的目标是max Q(s,a)它假设之后永远选最优动作于是敢贴着悬崖走最短路线。但行为策略带 ε 探索一旦在悬崖边随机探索一步就掉下去扣 100。SARSA 因为把探索也算进目标里会学到一条离悬崖远一点的安全路径——它「知道」自己会随机乱走所以主动避开危险区。这就是为什么很多人跑完两个算法发现 SARSA 的累计奖励反而更高不是它更优而是它更「保守」把探索的代价算进去了。理解这一点比背十个公式都有用。2.3 用 NumPy 写一个最小可跑的 Q 学习先不急着上环境把 Q 学习的核心循环写出来。下面这段是纯表格实现状态用整数索引动作空间固定为 4 个方向import numpy as np class QLearningAgent: def __init__(self, n_states, n_actions, alpha0.1, gamma0.99, epsilon0.1): # Q 表初始化为 0形状 [状态数, 动作数] self.Q np.zeros((n_states, n_actions)) self.alpha alpha # 学习率 self.gamma gamma # 折扣因子 self.epsilon epsilon # 探索率 def choose_action(self, state): # ε-greedy以 ε 概率随机探索否则选当前最优 if np.random.rand() self.epsilon: return np.random.randint(self.Q.shape[1]) return int(np.argmax(self.Q[state])) def update(self, s, a, r, s_next, done): # Q 学习目标r γ·max Q(s, ·)终止状态无未来回报 best_next 0.0 if done else np.max(self.Q[s_next]) td_target r self.gamma * best_next td_error td_target - self.Q[s, a] self.Q[s, a] self.alpha * td_error逻辑说明choose_action负责行为策略update负责目标策略。注意done的处理——终止状态之后没有未来回报best_next必须置 0否则智能体会在终点附近学到虚高的价值。参数上alpha控制每次更新迈多大步gamma决定看得多远epsilon是探索比例。这三个是后面调参的主角。2.4 SARSA 只改一行但循环顺序变了SARSA 的更新函数几乎一样只把max换成实际的下一个动作价值class SarsaAgent(QLearningAgent): def update(self, s, a, r, s_next, a_next, done): # SARSA 目标r γ·Q(s, a)a 是实际选出的动作 next_q 0.0 if done else self.Q[s_next, a_next] td_target r self.gamma * next_q td_error td_target - self.Q[s, a] self.Q[s, a] self.alpha * td_error关键差异在调用方式SARSA 必须在进入下一步之前就把a_next选好所以主循环是「选 a → 执行 → 观察 s, r → 选 a → 更新 → s,a s,a」。Q 学习则是「选 a → 执行 → 观察 s, r → 更新 → s s」下一步再选动作。这个顺序差异在写训练循环时最容易搞混一旦写错SARSA 就退化成了 Q 学习实验对比也就失去意义。3. 把悬崖行走跑起来环境、训练循环与参数3.1 用 30 行搭一个悬崖行走环境不依赖 Gym自己写一个最小环境方便看清状态转移class CliffWalking: def __init__(self, rows4, cols12): self.rows, self.cols rows, cols self.start (rows - 1, 0) self.goal (rows - 1, cols - 1) self.n_states rows * cols self.n_actions 4 # 上 下 左 右 self.reset() def reset(self): self.pos self.start return self._idx(self.pos) def _idx(self, pos): return pos[0] * self.cols pos[1] def step(self, action): r, c self.pos # 动作映射0 上, 1 下, 2 左, 3 右 dr, dc [(-1, 0), (1, 0), (0, -1), (0, 1)][action] nr min(max(r dr, 0), self.rows - 1) nc min(max(c dc, 0), self.cols - 1) self.pos (nr, nc) # 掉悬崖扣 100 并回起点 if nr self.rows - 1 and 0 nc self.cols - 1: self.pos self.start return self._idx(self.pos), -100.0, False if self.pos self.goal: return self._idx(self.pos), -1.0, True return self._idx(self.pos), -1.0, False逻辑说明状态用行*列列压成一维整数方便直接索引 Q 表。动作越界时用min/max夹住相当于撞墙不动。掉悬崖返回 -100 且不终止回到起点继续到终点返回 -1 并终止。这套奖励设计和 Sutton 书里一致方便对照。3.2 训练循环Q 学习与 SARSA 的写法差异下面把两个算法的训练循环并排写出来注意 SARSA 多了一次choose_actiondef train_qlearning(env, episodes500, alpha0.1, gamma0.99, epsilon0.1): agent QLearningAgent(env.n_states, env.n_actions, alpha, gamma, epsilon) rewards [] for _ in range(episodes): s env.reset() total, done 0.0, False while not done: a agent.choose_action(s) s_next, r, done env.step(a) agent.update(s, a, r, s_next, done) s s_next total r rewards.append(total) return agent, rewards def train_sarsa(env, episodes500, alpha0.1, gamma0.99, epsilon0.1): agent SarsaAgent(env.n_states, env.n_actions, alpha, gamma, epsilon) rewards [] for _ in range(episodes): s env.reset() a agent.choose_action(s) # 先选第一个动作 total, done 0.0, False while not done: s_next, r, done env.step(a) a_next agent.choose_action(s_next) # 提前选下一个动作 agent.update(s, a, r, s_next, a_next, done) s, a s_next, a_next total r rewards.append(total) return agent, rewards逻辑说明Q 学习在循环内只选一次动作SARSA 在循环内选两次当前和下一个。total累计的是每回合原始奖励不做折扣用来观察策略好坏。跑 500 回合后把rewards做滑动平均画出来通常能看到 Q 学习前期波动大、SARSA 更平稳。3.3 三个必调参数alpha、gamma、epsilon表格法里真正影响收敛的就这三个下面给出经验范围和调整方向参数含义常用范围调大后果调小后果alpha学习率0.05 ~ 0.3更新激进Q 值震荡收敛慢前期几乎不动gamma折扣因子0.9 ~ 0.99看得远但方差大短视容易困在局部epsilon探索率0.05 ~ 0.2探索多前期奖励低易陷入次优路径实操建议gamma先固定 0.99alpha从 0.1 起调epsilon用固定值先跑通再考虑退火。很多人一上来就把epsilon设成 0.5结果 500 回合根本看不出收敛误以为算法有问题其实是探索太猛。3.4 ε 退火从固定探索到逐步收敛固定epsilon的缺点是训练后期还在乱走Q 表已经学好了却拿不到高奖励。常见做法是线性退火def epsilon_by_episode(ep, start1.0, end0.05, decay_episodes300): # 前 decay_episodes 回合从 start 线性降到 end之后保持 if ep decay_episodes: return end return start - (start - end) * ep / decay_episodes逻辑说明start给足前期探索end保证后期仍有少量探索避免锁死decay_episodes控制退火速度。在悬崖行走里退火太快比如 100 回合会导致 SARSA 还没学会避开悬崖就停止探索反而表现变差退火太慢则收敛曲线拖尾很长。300 回合是个稳妥起点。4. 避坑与排查五个真实翻车现场4.1 现象Q 值全部爆炸成 NaN原因alpha设得过大比如 1.0叠加gamma接近 1TD 误差被反复放大Q 值指数增长溢出。解决把alpha降到 0.1 以下并在更新后加一句np.clip兜底或者检查奖励是否没有归一化——悬崖的 -100 和每步 -1 量级差 100 倍容易让更新失稳。4.2 现象SARSA 学到的路径和 Q 学习一模一样原因训练循环写错了SARSA 的a_next用了argmax而不是choose_action等于把行为策略换成了贪心策略退化成 Q 学习。解决确认a_next必须来自同一个带 ε 的choose_action这是 on-policy 的命根子。4.3 现象两个算法都学不会一直掉悬崖原因状态索引和 Q 表维度对不上。比如环境返回的状态是(row, col)元组直接拿去索引self.Q[state]会报错或静默取错行。解决统一在环境里把状态压成一维整数或者在 agent 里做state pos[0]*cols pos[1]的转换别在两个地方各转一次。4.4 现象训练奖励曲线剧烈震荡看不出趋势原因单回合奖励方差太大尤其 Q 学习前期探索多偶尔掉悬崖扣 100。解决画图前做滑动平均窗口 20~50或者直接看每 50 回合的平均奖励。别拿单回合原始值判断收敛那是玄学。4.5 现象epsilon 退火后奖励反而下降原因退火太快智能体还没充分探索就锁定了次优策略后期epsilon太小又无法纠正。解决把decay_episodes拉长到总回合数的一半以上或者改用指数退火但保留end不低于 0.05。这个坑我在第一次跑 SARSA 时踩过曲线先升后降查了半天才发现是退火参数的问题。5. 进阶技巧用同策略评估验证你到底学到了什么跑完训练多数人只看奖励曲线就下结论但奖励高不代表 Q 表学对了。一个更硬的验证方法是同策略评估固定当前 Q 表把epsilon设为 0纯贪心跑 100 回合不更新看平均奖励和路径。这一步能暴露「训练时靠探索蒙对、贪心时立刻翻车」的情况。具体做法是给 agent 加一个eval_mode开关或者在评估时临时把epsilon置 0def evaluate(agent, env, episodes100): old_eps agent.epsilon agent.epsilon 0.0 # 关闭探索纯贪心 total 0.0 for _ in range(episodes): s env.reset() done False while not done: a agent.choose_action(s) s, r, done env.step(a) total r agent.epsilon old_eps # 恢复避免影响后续训练 return total / episodes逻辑说明评估阶段不调用updateQ 表冻结只测策略表现。old_eps的保存和恢复是必须的否则评估完忘了还原后续训练就变成纯贪心探索直接没了。这个细节很小但我在复现别人代码时见过好几次因为漏了恢复导致「训练到一半突然不探索了」的 bug。把 Q 学习和 SARSA 的评估结果并排看通常会得到这样一组对照指标Q 学习SARSA贪心路径贴悬崖最短离悬崖一格训练期平均奖励波动大较平稳评估期平均奖励略低探索代价略高收敛速度快稍慢这张表不是让你背结论而是让你在自己的环境里复现出来。如果跑出来 SARSA 的贪心路径也贴着悬崖那大概率是 4.2 里的循环写错了如果 Q 学习评估奖励远高于 SARSA检查一下epsilon是不是没退火导致 SARSA 一直背着探索代价。最后一个习惯每次改参数只改一个把alpha、gamma、epsilon、decay_episodes记在一张表里跑完对比。强化学习的随机性会让「凭感觉调参」变成纯玄学只有控制变量才能把经验沉淀下来。我现在的做法是固定随机种子先跑三组不同alpha确认趋势后再动epsilon这样至少能保证每次改动都有据可查。希望帮到你。本文还有配套的精品资源点击获取