表格型Q-Learning收敛、探索与最大化偏差避坑指南
简介面向强化学习入门与进阶学习者这份 Q Learning 完整实现压缩包以 Python 构建并迭代更新 Q 表为主线通过奖励回传与 Q 值迭代逼近最优策略覆盖状态-动作价值估计、epsilon-greedy 探索、学习率与折扣因子调整等关键环节适合希望理解算法流程并动手复现实验的读者。包内共 5 个文件整体约 852KB包含 py 源码、pdf 报告、md 说明以及 license 和 tmp 临时文件各文件分工较清晰其中 py 源码提供智能体实现骨架md 说明与 pdf 报告可辅助梳理实验设置和结果。已有 1111 人学习下载可作为课程作业、算法入门或小型实验的参考。读者能从中获得可运行的代码框架、Q 函数更新思路、参数调节与调试参考并掌握常见参数调试方法借助 gym 环境如 CartPole-v1观察策略收敛过程理解探索与利用的平衡适合对照学习与二次修改。1. Q Learning 的悖论为什么它不必知道环境长什么样也能收敛Q Learning 有个特别反直觉的性质它更新的不是「这一步拿到多少奖励」而是「假设从下一步起永远走最优路线那一步值多少」。强化学习里管这叫自举bootstrap——智能体还没学会走路就已经在用「自己未来会很强」的假设去修正当下的判断。用 python 写出来算法核心就是一行赋值Q[s][a] alpha * (r gamma * max(Q[s2]) - Q[s][a])。看着人畜无害但收敛性、探索、系统性高估这三件麻烦事全挤在这一行里。搞懂它到底在跟什么较劲你再去翻一份完整实现就能分清哪些参数是在调性能哪些是在保命。2. Q 表与 Bellman 不动点一个二维数组怎么装下整个策略表格型 Q Learning 的全部家当就是一个|S| × |A|的数组。状态数乘以动作数每个格子存一个浮点数整个策略就压缩在里面了——推理时对某一行取argmax动作就出来了。这种「策略即查表」的形态之所以成立靠的是 Bellman 最优方程给出的一个不动点只要每个格子的值都等于「即时奖励 折扣后的最优后继值」这张表就自洽了而这组自洽的值唯一对应最优策略。整个训练过程本质是在用一条条带噪声的采样轨迹去逼近这个不动点。理解这一点后面所有的坑才好定位——收敛慢、值虚高、策略鬼打墙说到底都是「逼近方式出了问题」而不是「公式写错了」。2.1 状态-动作索引表里存的不是奖励是一句承诺初学者最容易把Q[s][a]理解成「在 s 做 a 能拿多少分」这个理解会直接导致后面一系列误判。准确的说法是从状态 s 出发、先执行动作 a、此后一直按最优策略行动所能获得的期望折扣累计回报。两个关键词不能丢——「期望」因为环境可能有随机性「折扣累计」因为它把未来所有步的收益都折进来了。所以一个格子的值可以是 8.7而单步奖励上限只有 1.0这完全正常多出来的部分全是未来收益的折现。正因为 Q 值是「承诺」而不是「实测」它才需要被反复修正。同一条路径第一次走通时回传的值往往是错的因为沿途大半格子的初始值是 0折现链条从终点往回传的过程中会被 0 稀释。多跑几十个 episode链式回传逐步把真值填进来值才会稳定。我一般会把这个过程看成「从终点往回渗水」离终点越远需要渗的轮数越多这也是为什么稀疏奖励任务里起点附近的 Q 值最后才收敛。工程上状态到索引的映射是个容易翻车的细节。字典状态、元组坐标、浮点向量都得先离散化成一个整数下标映射必须是确定且无冲突的。常见的实现思路是用state → idx的字典缓存第一次见到某状态时分配新索引同时扩容 Q 表这么做能省内存代价是 Q 表大小随探索动态变化任何按固定形状初始化的代码比如提前np.zeros((n_states, n_actions))都会索引越界。2.2 TD 误差一次更新里同时装着预测和修正Q Learning 的更新式可以拆成三块Q(s,a)是当前预测r γ·max Q(s,·)是目标两者之差δ就是时序差分误差TD error。整个更新就是Q(s,a) α·δ学习率 α 决定这次修正用多少。关键在于目标里含有 Q 表自己的估计——这就是自举也是它和监督学习最本质的区别监督学习的标签是外部给定的真值这里的「标签」是模型自己算出来的会随着训练一起漂移。自举带来两个直接好处。第一不需要等 episode 结束就能更新每一步都能改表样本利用率高第二不需要环境模型不需要知道转移概率只要能从环境里采样出(s, a, r, s, done)就够了——所谓 model-free说的就是这个。代价是目标不稳定早期 Q 值全是噪声时你等于在用噪声教噪声收敛路径会很难看回报曲线前几百个 episode 往上蹿下跳属于正常现象。下面这段是最小可跑的环境与 Q 表骨架把「索引映射」和「终止态必须砍掉自举项」这两个要点都装进去了import numpy as np class GridWorld: 极简网格环境0 空地1 障碍2 终点3 陷阱。动作上右下左 def __init__(self, layout, start(0, 0)): self.grid np.array(layout, dtypeint) self.n_rows, self.n_cols self.grid.shape self.start start self.moves [(-1, 0), (0, 1), (1, 0), (0, -1)] self.n_actions 4 def step(self, state, action): dr, dc self.moves[action] # 越界按撞墙处理位置不变给一个小惩罚 nxt (min(max(state[0] dr, 0), self.n_rows - 1), min(max(state[1] dc, 0), self.n_cols - 1)) cell self.grid[nxt] if cell 1: return state, -1.0, False if cell 2: return nxt, 10.0, True # 终点回合结束 if cell 3: return nxt, -10.0, True # 陷阱回合结束 return nxt, -0.1, False # 普通移动小步惩罚逼出最短路 class QTable: def __init__(self, n_actions, init0.0): self.q {} # state(tuple) - np.ndarray(n_actions) self.n {} # 访问计数用于 1/n 学习率衰减 self.n_actions n_actions self.init init def row(self, state): if state not in self.q: # 惰性分配省内存 self.q[state] np.full(self.n_actions, self.init, dtypenp.float64) self.n[state] np.zeros(self.n_actions, dtypenp.int64) return self.q[state] def update(self, s, a, r, s2, done, alpha, gamma): q_sa self.row(s)[a] # 终止态没有后继自举项必须置零否则会凭空多出一截虚假价值 bootstrap 0.0 if done else gamma * self.row(s2).max() td_target r bootstrap td_error td_target - q_sa self.q[s][a] alpha * td_error self.n[s][a] 1 return td_errorrow()里的惰性分配是个实用选择状态空间未知或很大时预分配整张表既浪费内存又需要提前枚举所有状态。update()里done那个分支是整段代码里最容易被漏掉的一行——漏掉的话终止态的 Q 值不会被约束在「本回合终局奖励」上而是会持续被后继的估计值污染。bootstrap与td_target分开写是为了在调试时能单独打印这两项看看到底是奖励信号没传回来还是自举项在放大。2.3 更新式的三根旋钮α、γ 和 max 各自在管什么学习率 α 管「信新数据多少」。它要满足两个条件才能保证收敛所有状态-动作对被无限次访问且步长满足Σα ∞、Σα² ∞。固定 α 不满足第二条只能在非平稳环境里求个近似实践中常用α 1/(1 n(s,a))的计数衰减或者干脆用一个 0.1~0.5 的常数跑有限步数。α 设 1.0 等于完全用新目标覆盖旧值回报曲线会剧烈震荡看上去像算法坏了其实是你把所有历史信息都扔了。折扣因子 γ 管「视野有多远」。有效视野大概是1/(1-γ)步γ0.9 约 10 步γ0.99 约 100 步。这个估算很实用——如果你的迷宫最短路有 40 步γ0.9 意味着终点那 10 分传到起点时只剩0.9^40 ≈ 0.015跟没传差不多。这也是「奖励明明设置了但学不出来」的头号原因不是算法不灵是折扣把信号掐断了。max算子管的是「学谁」。Q Learning 用下一状态的最大值做目标学的是最优策略贪心策略而采样用的却是 ε-greedy 的探索策略两者不一致——这叫 off-policy。好处是行为策略可以随便探索甚至用旧数据回放学到的仍然是最优解这正是它后来能扩展到经验回放的根基。同门的 SARSA 用实际执行的下一个动作做目标是 on-policy学出来的是「带探索噪声的策略」在悬崖边上会更保守。选哪个不是口味问题取决于你部署时能不能关掉探索。3. ε-greedy 不是随手撒随机探索率衰减到底在跟谁做交易表格型 Q Learning 有一个隐蔽的前提所有状态-动作对要被无限次访问。如果策略从一开始就只挑当前最优动作那些「暂时看起来不好、长期其实更优」的动作永远拿不到数据Q 值也永远是错的于是策略继续不选它——循环闭合算法锁死。ε-greedy 就是打破这个闭环的最朴素手段以 1-ε 的概率走当前最优以 ε 的概率均匀随机。它不聪明但它保证每个动作都有非零概率被尝试从而让「无穷次访问」这个前提不至于落空。3.1 纯贪心的死亡螺旋为什么 Q 值全 0 时智能体会一直撞同一面墙把 ε 设成 0 跑一遍你会看到一个很典型的现象智能体沿着某个固定方向反复撞墙几十个 episode 之后行为几乎没有变化。原因不神秘——Q 表初始全 0argmax遇到全并列时返回下标 0比如「上」于是它只执行「上」。撞墙后 Q 值被更新成一个负数但其他三个方向依然是 0比负数大按理该换动作了可如果奖励设计成全负比如每步 -1连「不动」都比其他动作好值之间的微小差异会把策略推向一个诡异的行为原地打转或者贴边走。这里有个反直觉的技巧叫乐观初始化optimistic initialization。把 Q 表初始值设成一个比真实可达回报更高的正数比如 1.0那么每个动作第一次被尝试后都会「失望」地掉下来智能体天然倾向于去试那些没试过的动作。这本质上是把探索写进了初值用「虚假希望」换探索动力在确定性环境里配合纯贪心都很有效。代价是它对奖励尺度敏感奖励范围一变初值就得重调。实际工程里更常见的是 ε 从 1.0 起步。第一个 episode 完全随机相当于先做一轮状态空间扫描把表填出个大概轮廓之后再逐步收敛。这个「先瞎跑一阵」的阶段看着浪费实际上把后续所有更新都变成了有意义的更新总体样本效率反而更高。3.2 三种衰减调度线性、指数和 GLIE 的收敛前提ε 是常数还是一直衰减决定了算法最终收敛到哪个策略。常数 ε 下策略永远带着ε·(1-1/|A|)的随机概率回报曲线在高位抖动收敛不到最优ε 衰减到 0 才能保证最优性但衰减太快又退回到死亡螺旋。理论上给出的条件是 GLIEGreedy in the Limit with Infinite Explorationε_t → 0且Σε_t ∞。前一条保证极限处是贪心后一条保证探索总量无穷任何状态都会被反复访问。这两个条件同时约束了「衰减必须慢于 1/t」这条下限。三种常见调度的取舍可以列成这样调度公式特点适用线性ε max(ε_min, ε_0 - k·t)可预测到达下限后恒为常数步数预算明确的任务指数ε ε_min (ε_0-ε_min)·exp(-t/τ)前期降得快后期拖得久大部分离线实验的默认选择GLIEε_t min(1, c/t)或1/sqrt(t)满足理论收敛条件需要论证收敛性的场景import math class EpsilonScheduler: 三种衰减方式统一接口t 为已交互步数 def __init__(self, modeexp, eps_start1.0, eps_end0.05, decay_steps20000, c1.0): self.mode, self.eps_start, self.eps_end mode, eps_start, eps_end self.decay_steps, self.c decay_steps, c def value(self, t): if self.mode linear: # 走完 decay_steps 后钉在 eps_end避免掉到 0 后完全没有探索 ratio min(1.0, t / self.decay_steps) return self.eps_start ratio * (self.eps_end - self.eps_start) if self.mode exp: # tau 取 decay_steps/5约在 decay_steps 步时衰减到 0.7% 残量 tau max(1.0, self.decay_steps / 5.0) return self.eps_end (self.eps_start - self.eps_end) * math.exp(-t / tau) if self.mode glie: return max(self.eps_end, min(1.0, self.c / (t 1.0))) raise ValueError(self.mode)decay_steps是唯一需要按任务规模调的量它应当覆盖「智能体把状态空间跑一遍并初见成效」所需的步数经验上取总训练步数的 30%~50%。eps_end不设 0 是个务实的妥协——留 5% 的随机性可以在环境有微小随机扰动时避免策略僵死代价是极限回报略低于理论最优。切到glie模式时eps_end只作为数值保护别指望它满足严格的Σε∞。3.3 探索够不够看四个指标比看回报曲线靠谱回报曲线是滞后指标它不动的时候你根本分不清是探索不足、学习率不对还是奖励设计有问题。我一般会在训练循环里额外记四个量直接把问题定位到具体环节状态覆盖率即已访问过的状态数除以可达状态总数。它上不去就说明探索策略有问题跟学习率无关。第二是平均 episode 长度如果它卡在一个固定值不动通常是绕圈或撞墙属于策略层面的病。第三是 TD 误差的绝对值滑动均值正常应该震荡下降如果它持续放大多半是 γ 太大或 α 太大导致发散。第四是 Q 表的最大值与最小值γ 接近 1 又没正确切断终止态自举时最大 Q 值会一路飙升到荒谬的量级。覆盖率这个指标尤其值得单独盯着。它只看「哪些状态至少被访问过一次」与回报高低无关因此能在回报还没起来之前就告诉你探索是否在推进。跑迷宫类任务时我通常在训练日志里画一条覆盖率的曲线如果它在第 200 个 episode 就平了后面几万个 episode 基本是在重复劳动该做的是加大 ε 或延长衰减窗口而不是加训练轮数。4. 最大化偏差max 这个操作凭什么把 Q 值越抬越高如果只能记住 Q Learning 的一个固有缺陷那应该是这个它系统性地高估动作价值而且高估程度随动作数量和估计噪声一起增长。这不是 bug是max算子的数学后果。当你对每个动作的 Q 值估计都带噪声时取最大值等价于「专挑那些被噪声往上抬的估计」于是E[max Q̂] ≥ max E[Q̂]——这个不等式对任何凸函数都成立而 max 就是凸的。样本越少、噪声越大、动作越多这个缝就撑得越开。4.1 高估的数学来源Jensen 不等式和一个凸算子把它具体化。假设某状态有三个动作真实价值都是 1.0但估计各自带 ±0.5 的噪声。取 max 之后期望大概在 1.3 附近系统性偏高的部分就是偏差。这个偏高的值会被回传到前一个状态成为那里自举目标的一部分再被下一次 max 放大——误差沿着轨迹一路上扬最终让整张表的值尺度整体虚高。危险的地方在于这种偏差不容易被察觉。因为argmax的顺序往往还是对的高估对每个动作是近似均匀的策略本身可能正常但值是错的。一旦你在这个基础上做任何依赖绝对值的事情——比如设一个「Q 值大于阈值就停」的规则、用 Q 值做动作排序的置信度、或者把 Q 表迁移到新任务——就会翻车。稀疏奖励任务里这个偏差尤其大因为绝大多数动作只被采样过一两次估计方差极大而 max 恰好专门捡这种样本。还有个容易忽视的加重因素ε-greedy 采样下最优动作被采到的频率远高于其他动作但被采频率低不代表不参与 max 运算——恰恰相反那些只被采过一次、恰好碰上高奖励的动作会长时间霸占 max 的位置把整个状态的价值抬在不合理的高度上。经验上这种「一次性幸运动作」留下的虚假高值最难消因为贪心策略不会再选它它也就再没机会被修正下来。4.2 Double Q-Learning 的解耦用一张表选动作用另一张评估解法思路很干净把「选动作」和「评价值」这两件事拆给两个独立的估计器。用表 A 选出argmax的动作再拿表 B 在这个动作上的值作为评估反之亦然。因为两张表的估计噪声互相独立那个被 A 噪声抬高的动作在 B 里的值大概率是正常的高估就被抵消掉了。数学上可以证明这个估计是无偏的。工程实现上有两种写法。一种是维护两个完整 Q 表每步以 0.5 概率随机决定更新哪一张另一种是只存两份数组用同一个索引通过在目标里换表来实现解耦。前者实现直观但内存翻倍后者省内存但代码里两组下标容易写混。无论哪种评估时取两张表的平均值(Q_A Q_B)/2作为动作价值比单看一张稳。要注意这不等于「跑两个 Q Learning 取平均」。两个独立训练的 Q Learning 各自都带高估平均之后高估还在只是被平滑了Double Q 的收益来自「选择与评估用不同的噪声源」这个结构性解耦跟集成平均是两码事。我第一次理解这点时才发现自己在某个项目里用错了很久把两个 Q 表一起更新等于什么都没做。4.3 代码实现与对照实验的设计要点下面这段是 Double Q 更新的核心关键在两个分支里argmax和取值用的不是同一张表import numpy as np def double_q_update(qa, qb, s, a, r, s2, done, alpha, gamma, rng): qa / qb 均为 dict: state - np.ndarray两表独立初始化 row_a, row_b qa[s], qb[s] row_a2, row_b2 qa[s2], qb[s2] if rng.random() 0.5: # 用 A 选动作用 B 评估解耦估计噪声 best_a int(np.argmax(row_a2)) target r (0.0 if done else gamma * row_b2[best_a]) row_a[a] alpha * (target - row_a[a]) else: # 反过来用 B 选用 A 评 best_b int(np.argmax(row_b2)) target r (0.0 if done else gamma * row_a2[best_b]) row_b[a] alpha * (target - row_b[a])rng用独立的np.random.Generator实例而不是全局随机源这是为了在做对照实验时能精确复现「哪一步选了哪张表」。done分支同样必须切断自举双表版本里这一点更容易漏因为代码路径分叉多。best_a只用下标绝不能用row_a2[best_a]去评估——那就退化成普通 Q Learning 了整个解耦白做。验证收益的方法不用复杂构造一个动作数较多比如 8~10 个、单步奖励带高斯噪声的环境同一随机种子下分别跑普通 Q Learning 和 Double Q记录训练过程中「Q 表对某几个固定状态的估计值」与「多次采样得到的经验均值」之间的差。普通版本的估计会稳定高于经验均值Double 版本则贴着走。这比看回报曲线敏感得多因为回报在小环境里两者都能跑满差别只在值上。代价也要心里有数Double Q 每步只更新两张表之一同等工作量下每张表的更新次数减半前期收敛更慢短预算实验里它甚至可能表现更差——这是用样本效率换估计精度不是白捡的。5. 避坑与排查表格型 Q-Learning 的五类翻车现场上面几章讲的是机制但真正耗时间的是「跑出来不对」的那几个小时。表格型 Q Learning 的好处是每一处异常都能追溯到一行具体代码坏处是症状和病因之间往往隔着一层误导。下面这五类问题基本覆盖了我在小规模强化学习实验里见过的绝大多数情况每一条都按现象、原因、解决三步走。5.1 终止态没砍 bootstrapQ 值永远差一截现象是策略看起来「差一点点」智能体会走到终点旁边然后来回横跳或者绕远路避开最后的几步。看 Q 表会发现终点的 Q 值并不等于终局奖励而是持续在变甚至会随着训练越变越大。原因就是更新式里没区分done。终止态没有后继状态max Q(s,·)这一项应该在逻辑上取 0但如果代码里无脑拿gamma * max(Q[s2])就等于给终点后接了一个凭空存在的虚拟状态这个状态的估计值会随训练浮动于是终点的值被这个虚构未来污染。如果那张虚拟行的值恰好为正尤其是用了乐观初始化终点的 Q 值会一路被抬策略自然会在终点附近犹豫。解决很直接在更新函数里显式判断done或者更保险的做法是环境只在非终止转移时返回s_next终止时返回None让None在更新函数里直接触发bootstrap 0把「忘记处理」这条路径从代码里物理删除。改完以后可以用一个检查器断言「终止态出现的 Q 值序列单调收敛到终局奖励」这是我会强制保留的一个回归检查。5.2 全零初始化配全负奖励智能体学会了原地转圈现象是 episode 长度稳定在某个很小的值覆盖率不涨回报曲线贴在一条水平线上。打开轨迹回放智能体在一个两格之间来回走或者在墙角反复撞。原因是 Q 表全 0而所有动作的奖励都是负的撞墙 -1、普通移动 -0.1。任何一个动作被尝试一次后其 Q 值变成负数而没试过的动作还是 0贪心策略自然优先选「没试过的」——它不是在探索是在躲避已知的负值。如果所有动作都被试过并都变成负数那它就在一堆负数里挑最不烂的配合高 ε 就变成了随机游走式的转圈。这个现象在高 ε 的早期看不出来等 ε 降下来策略立刻显形。解决有三条路径选一条即可改成乐观初始化让未访问动作显得「有希望」把奖励结构改成以惩罚为主但保留正终点奖励保证存在正的价值可以传播或者给未访问的(s,a)对加访问计数惩罚把「探索不确定性」显式写进选择规则类似 UCB 的思路。我一般选第一条改动最小一行初值搞定。5.3 ε 降得太早策略被锁死在次优路径上现象是策略学得飞快前几十个 episode 回报就跳上去并稳定下来但你换一个种子重跑收敛到的路径长度差很多有的明显绕路。把 ε 曲线画出来会看到它在总训练预算的 10% 以内就降到了下限。原因是探索窗口太短智能体只摸清了起点附近的一小片区域就锁定了策略之后 ε 太小那些没访问过的状态几乎不可能再被访问到Q 表里它们永远是初值。这不是「收敛到了次优解」是「根本没看过解空间」。表格法的收敛前提要求无限次访问实践中被砍成有限次以后这个前提就变成了对探索窗口长度的硬约束。解决办法是让decay_steps与任务规模挂钩而不是拍脑袋至少覆盖「随机策略跑通一次全图所需的步数」的若干倍。一个可操作的判据是观察状态覆盖率的增速等它明显放缓再开始降 ε这个策略叫「先探索到位再收敛」比固定步数表靠谱得多。另外换种子重跑至少三次看结果方差如果方差大得离谱八成就是这个问题。5.4 学习率固定成 0.9回报曲线锯齿到看不出趋势现象是训练日志里的回报曲线像心电图相邻 episode 之间能差出十几分而且怎么加训练步数都不平滑。Q 表里同一个状态的值在两个数之间反复跳。原因是 α 太接近 1每次更新几乎完全丢弃旧估计。Q Learning 的目标本身带噪声采样只有一条轨迹α0.9 意味着这条噪声几乎全额写进表里加上自举把噪声往后传整张表在真值附近持续抖动。如果 γ 也很大抖动还会被放大极端情况下值会发散。解决办法是把 α 降到 0.1~0.3 区间或者改用α 1/(1 n(s,a))的计数衰减让每个格子自己决定步长——访问多的格子自动变稳访问少的格子保持灵活。后者在状态访问极不均匀的任务里效果最好实现只需要在 Q 表里多维护一个计数数组。调完以后不要只看回报曲线要同时看 TD 误差的滑动均值它应该呈现清晰的下降趋势而不是横盘震荡。5.5 稀疏奖励配小 γ信号传不回起点现象是只有终点附近的几个状态 Q 值不为 0起点附近一片空白或者全是初始值episode 长度不下降。很多人这时会怀疑探索不够加大 ε 也没用。原因是折扣衰减把远期奖励抹平了。假设最短路 40 步、只有终点给 1、γ0.9那么起点附近的折扣因子是0.9^40 ≈ 0.015信号要经过 40 次自举回传才能到达每一跳都在乘 0.9。再加上中间步骤每步 -0.1 的惩罚到达终点的那点正收益早就被抵消干净了。这不是探索问题是数值尺度问题。解决办法按优先级先把 γ 提到 0.99有效视野约 100 步或者缩短到终点的距离再不行就加中间引导奖励把「离终点更近」这件事显式编码进奖励但引导奖励要小而单调防止智能体学会「蹭奖励」而不去终点。改 γ 之后记得同步调 α因为更大的 γ 意味着自举项更大值尺度会整体抬高原来的 α 可能就偏大了。提示以上五条里5.1 和 5.5 是最容易被误判成「算法本身不行」的。排查顺序建议固定为「先看终止态处理再看 γ 与路径长度的匹配最后才动学习率和探索」。6. 从 Q 表到函数逼近什么条件下这种「查表」必然退场表格法的所有优点都建立在一个假设上状态空间小且可枚举。一旦状态维度上去这个假设立刻碎掉——连续状态要离散化每个维度切 20 份5 个维度就是 320 万行再加上动作维度内存直接撑爆而且绝大部分格子永远访问不到。更致命的是泛化缺失两个数值上很接近的状态在表里是完全无关的两行一个学过、一个没学过学过的经验一点也帮不到没学过的。这就是维度诅咒在强化学习里的具体形态也是函数逼近必然接管的理由。函数逼近把Q(s,a)换成一个带参数 θ 的模型输出更新目标不变仍然是r γ·max Q(s,·)只是更新从「改一个格子」变成「对 θ 求一次半梯度」。这一步跨出去前面讲的五个坑一个都不会消失反而会以更隐蔽的方式重现。最典型的是自举目标不稳定表格里目标只依赖下一行而函数逼近下目标依赖的是同一套参数参数一动所有状态的目标跟着一起动训练极易发散。工程上对此的两个补丁——用目标网络固定自举目标、用经验回放打散样本相关性——本质上都是在给自举加「延迟」和「去相关」让目标变化慢于预测变化。顺带一提回放池能用正是因为 Q Learning 是 off-policy换成 SARSA 那条路就不成立了这是第 2 章埋下的伏笔在这里兑现。我一般会这么理解这条演进线表格法是基准所有更复杂的算法都是在它的同一套更新式上针对某一类具体缺陷加补丁而不是另起炉灶。所以拿一份表格型的完整实现先把机制跑通性价比很高——你能亲眼看到 Q 值怎么从 0 开始蔓延、ε 降下来以后策略怎么变贪、终止态那行代码删掉以后现象怎么变这些体感在只看公式的时候是拿不到的。关于取舍我现在的判断是状态数在一万以内、且需要严格可解释和可复现的实验表格法仍然是首选不要为了「先进」上神经网络超过这个量级或者状态是连续的才考虑函数逼近并且要准备好面对自举发散这个新战场。验证的方法很朴素先在能枚举的小规模版本上跑通并记录基线再逐步放大状态规模观察回报与覆盖率是否同步退化。这个习惯是被坑出来的——有次我在一个连续状态任务上直接上函数逼近调了两天以为是超参数问题最后发现是状态归一化漏了而我本来可以在小规模表格版本上五分钟就看出是观测尺度的事。从那以后我每次开新任务都强制先跑一个能打印出整张 Q 表的迷你版本确认数据链路是通的再往上堆模型。希望帮到你。本文还有配套的精品资源点击获取