Q-Learning强化学习入门:Q表、贝尔曼方程与DQN实战

发布时间:2026/10/2 22:56:18
Q-Learning强化学习入门:Q表、贝尔曼方程与DQN实战 第一次把 Q-Learning 跑起来的那天我盯着终端里打印出来的那张 5×5 的 Q 表看了很久——它丑得要命数字大小不一还有一半是零但顺着每行最大值的格子走一遍机器人真的绕开了陷阱、摸到了终点。那一刻我才算真正明白强化学习RL不是把数据喂给模型让它背答案而是让一个 agent 在不断试错中自己写出一张什么情况下该做什么的对照表。这篇内容讲的就是这张表是怎么来的、怎么算的、怎么写得动、以及写崩了该怎么救。它适合三类人刚接触强化学习入门概念、被回报价值函数绕晕的同学想把 Q-Learning 落到真实项目里做参数整定或调度决策的工程师以及已经会用深度强化学习框架、但说不清 DQN 为什么非要那两个技巧的人。全文以表格型 Q-Learning 为主线代码可以直接复制运行踩坑部分都是我自己在项目里撞过的墙。1. 没有标签的决策问题Q-Learning 到底在学什么1.1 监督学习那一套为什么在决策场景里失灵我们平时做分类、做回归套路很固定准备一批带标签的数据定义损失函数梯度下降把参数压到损失最小。这套方法成立的前提是——每个样本的正确答案是已知的、和样本本身一一对应的。图像里是不是猫标注员看一眼就能确定明天的销量是多少历史数据里也有真值可以拟合。但决策问题不是这样。假设你在做一个仓储机器人的路径规划机器人在某个货架拐角处有两个选择左转绕远路但绝对安全右转抄近道但可能撞到临时堆放的托盘。哪个选择是对的没有人能提前给出标签因为对与不对取决于后面发生的一连串事情。你左转了后面可能遇到拥堵你右转了可能撞上东西。更麻烦的是同一个动作在不同时刻的后果会互相影响——这就是所谓延迟回报。监督学习还有一个隐含假设样本之间是独立同分布的。可决策序列里下一步的状态完全由这一步做了什么决定前后强耦合。把这种数据硬塞进一个分类器它学到的只是历史上这种情况下别人怎么做的而不是怎么做才能让长期收益最大。这两件事看着像实际差得很远前者是模仿后者是优化。Q-Learning 的价值就在这儿。它不需要任何人告诉它每一步的正确动作只需要一个能告诉它这一步得了多少分、这局结束了没有的环境剩下的它自己试。这也是为什么机器人控制、资源调度、参数自整定这类没有标准答案、只有最终结果的场景天生就适合强化学习。1.2 把问题翻译成 MDP状态、动作、奖励、转移要让算法跑起来第一步是把现实问题翻译成数学语言。这个翻译器叫马尔可夫决策过程MDP它由五个要素构成缺一个都跑不动。状态State记作 sagent 在某一时刻能看到的、用于决策的全部信息。注意是能看到的不是存在的。你的传感器读不到的东西再重要也不能写进状态。动作Action记作 aagent 在某个状态下可以做的选择集合。离散动作空间好办比如前进、后退、左转、右转连续动作空间就要另想办法了。奖励Reward记作 r环境对动作的即时反馈是个标量。奖励设计是整个项目里最考验功力的一环我后面会专门讲。转移概率记作 P在状态 s 执行动作 a 之后环境跳到状态 s 的概率。如果环境是确定性的这个概率就是 1如果是随机环境它就是一个分布。Q-Learning 属于**无模型model-free**方法它不需要你显式写出 P靠采样就能学。折扣因子记作 γ这个后面单独说。马尔可夫这三个字经常吓到人其实意思很朴素下一个状态只跟当前状态和当前动作有关跟更早的历史无关。这既是一个假设也是一个设计要求。如果你发现自己的问题明显不满足这个性质——比如机器人是否卡住取决于过去 3 秒的连续动作——那你需要把历史信息打包进状态里比如把最近几帧观测拼起来把它改造成满足马尔可夫性的样子而不是假装它满足。1.3 回报与折扣因子为什么要有 γ单步奖励只是即时反馈agent 真正要最大化的是一整条轨迹上的累积折扣回报$$G_t r_{t1} \gamma r_{t2} \gamma^2 r_{t3} \cdots$$γ 取 0 到 1 之间的数。它干的事情可以用一句话概括给未来的奖励打折。折扣打多少γ0.9 意味着 10 步之后的奖励权重只剩约三分之一γ0.99 意味着 100 步之后还有约三分之一。工程上有个常用的估算有效视野大约等于 1/(1-γ)。γ0.9 看十步γ0.99 看百步γ0.999 看千步。为什么要打折而不是简单求和三个现实原因。第一无限步数的任务如果不打折累积回报会发散数学上就没法优化了。第二环境越远的事情越难预测打折相当于对不确定性做了一次保守估计。第三也是最实际的γ 是你控制agent 有多短视的旋钮。仓储机器人如果太短视会为了省三步路去钻危险的缝如果视野拉太长它又会变得畏首畏尾、宁可绕大圈。这个旋钮只能靠实验调没有公式能替你决定。提示很多新手把 γ 设成 0.999 然后抱怨训练慢得离谱。γ 越大价值信息的传播越慢需要的样本量越大。先在 0.9 到 0.99 之间选一个跑通再考虑拉长视野。2. Q 表与贝尔曼方程Q-Learning 的数学骨架2.1 从 V(s) 到 Q(s,a) 的那一步跳跃理解 Q-Learning先要理解两个价值函数的关系。状态价值函数 V(s)回答的是站在状态 s按照某个策略一直走平均能拿多少回报 它只跟状态有关不区分动作。动作价值函数 Q(s,a)回答的是在状态 s 执行动作 a之后按照某个策略走平均能拿多少回报两者之间差着一个动作的维度。Q(s,a) 比 V(s) 更细也更有用——因为你最终要输出的是该做什么动作而 Q 表一查就能比大小。V 表你还得额外知道环境动态才能反推动作。它们之间的桥梁是$$V(s) \max_a Q(s,a) \quad \text{就最优策略而言}$$Q-Learning 学的是最优动作价值函数 Q*满足贝尔曼最优方程$$Q^(s,a) \mathbb{E}\left[r \gamma \max_{a} Q^(s,a)\right]$$把这个方程念出来就是人话一个动作到底值多少分等于这一步立刻拿到多少分加上下一步我挑最好的动作能拿到的分数再打个折。整个 Q-Learning 算法本质上就是想方设法让 Q 表满足这个方程。2.2 时序差分更新公式逐项拆解知道方程了怎么让表满足它最笨的办法是枚举所有状态动作对反复迭代但那是动态规划需要知道转移概率。Q-Learning 用的是**时序差分TD**思路拿一次实际采样的结果去修正估计值。$$Q(s,a) \leftarrow Q(s,a) \alpha \left[ \underbrace{r \gamma \max_{a} Q(s,a)}_{\text{TD 目标}} - Q(s,a) \right]$$逐项看$r \gamma \max_{a} Q(s,a)$是 TD 目标也就是我现在认为这个动作值多少。它是用旧估计值拼出来的所以本身也带误差。TD 目标减当前 Q 值是 TD 误差。误差为正说明这个动作比我想的好往高调为负就往下调。α是学习率控制每次修正的步长。α0.1 就是只信 10%α1.0 就是完全覆盖。这个公式有个容易被忽略的细节max 里面用的是更新后立刻可用的旧 Q 值而不是等下个回合跑完再统一更新。这意味着 Q-Learning 是**离线策略off-policy**算法——它更新的目标是最优策略的动作价值而它实际执行的是探索性的行为策略。行为策略可以乱来、可以边学边探索甚至可以把以前别的策略产生的数据拿出来接着学这是 Q-Learning 相对 SARSA 这类在线策略算法最大的实用优势。配合上 ε-greedy 探索后的完整伪代码只有这么几行初始化 Q(s,a) 为任意值通常为 0 重复每一回合 初始化状态 s 重复直到 s 为终止状态 用 ε-greedy 从 Q 中选动作 a 执行 a观测 r 和 s Q(s,a) ← Q(s,a) α [r γ max_a Q(s,a) − Q(s,a)] s ← s注意终止状态的处理如果 s 是终止状态TD 目标里就没有 γ max Q(s,·) 这一项直接是 r。这一行代码写错agent 会学着为了继续拿分而拒绝结束。2.3 拿纸笔手算一次更新数字算一遍直觉就建立了。假设现在 Q(s,a)0初始值α0.5γ0.9执行动作后拿到 r−1到达的 s 上最大的 Q 值是 2.0。TD 目标 −1 0.9 × 2.0 0.8 TD 误差 0.8 − 0 0.8 新 Q 值 0 0.5 × 0.8 0.4状态 s 那个 2.0 是怎么来的它是更早的某次更新由 s 的价值传播过来的。整个学习过程就是价值从终止状态向外一圈一圈扩散的过程。这也解释了一个常见现象如果奖励只在终点给出训练初期所有 Q 值都是零agent 会像没头苍蝇一样乱撞很久直到某次碰巧到达终点最后一格的价值变成正数然后这个正数一步步往前传。这就是所谓的稀疏奖励冷启动问题。再算一次反向修正看 α 的作用。假设下次又经过同一个 (s,a)这次 r 还是 −1但 s 上的最大 Q 已经涨到 5.0TD 目标 −1 0.9 × 5.0 3.5 新 Q 值 0.4 0.5 × (3.5 − 0.4) 1.95一次修正跳了 1.55看起来很快。但这也暴露了风险如果那个 5.0 是噪声导致的虚高估计这个错误会以 α 的比例被吸收进来而且因为公式里带 max高估会被系统性地保留——这就是后面要讲的maximization bias。3. 从零手写一版能跑通的 Q-Learning3.1 为什么先拿网格世界练手我强烈建议所有人第一个 Q-Learning 都用网格世界GridWorld别一上来就上 Gym 的连续控制环境。原因很实在网格世界的状态数就那么多你可以在训练结束后把整张 Q 表打印出来肉眼检查。哪个格子该往右、哪个该往上看看数字就知道算法有没有学歪。换成机器人控制你只能看一条曲线出问题完全不知道从哪下手。自己要定义一个也不难四五十行代码import numpy as np import random class GridWorld: def __init__(self, n5, start(0, 0), goal(4, 4), traps((1, 2), (2, 2), (3, 2))): self.n n self.start, self.goal start, goal self.traps set(traps) self.n_states n * n self.n_actions 4 # 0:上 1:下 2:左 3:右 self.moves {0: (-1, 0), 1: (1, 0), 2: (0, -1), 3: (0, 1)} self.pos start def _s(self): return self.pos[0] * self.n self.pos[1] def reset(self): self.pos self.start return self._s() def step(self, a): dr, dc self.moves[a] r, c self.pos nr min(max(r dr, 0), self.n - 1) nc min(max(c dc, 0), self.n - 1) self.pos (nr, nc) if self.pos self.goal: return self._s(), 10.0, True if self.pos in self.traps: return self._s(), -10.0, True return self._s(), -0.1, False # 每走一步的小惩罚那个 −0.1 的每步惩罚是我特意加的。如果每步奖励为 0agent 学到绕圈也没关系策略会出现来回震荡的现象。加一点小惩罚它就会主动找最短路径。惩罚别设太大超过了终点奖励的一半agent 可能宁愿原地不动或者直接撞陷阱结束这一局这是个很反直觉但经常出现的坑。3.2 核心循环的代码实现def q_learning(env, episodes3000, alpha0.2, gamma0.95, eps_start1.0, eps_end0.05, eps_decay0.995, seed42): random.seed(seed) np.random.seed(seed) Q np.zeros((env.n_states, env.n_actions)) eps eps_start steps_log [] for ep in range(episodes): s env.reset() done False steps 0 while not done: # ε-greedy 选择动作 if random.random() eps: a random.randrange(env.n_actions) else: a tie_break_argmax(Q[s]) s_next, r, done env.step(a) # 终止状态没有 bootstrap 项 td_target r if done else r gamma * np.max(Q[s_next]) Q[s, a] alpha * (td_target - Q[s, a]) s s_next steps 1 steps_log.append(steps) eps max(eps_end, eps * eps_decay) return Q, steps_log里面有个必须自己写的小工具——tie_break_argmax。np.argmax在多个动作 Q 值完全相等时永远返回最小的那个索引而 Q 表初始化时全是 0所以训练最开始的几百步里 agent 会死死地只选上这一个动作白白浪费探索预算。正确做法是在所有最大值里随机挑一个def tie_break_argmax(q_row): best np.flatnonzero(q_row q_row.max()) return int(np.random.choice(best))这个问题非常隐蔽因为它不会报错只会让训练前期莫名其妙地慢。我第一次遇到的时候排查了两个小时最后靠打印动作分布才发现所有动作都集中在 0 号。评估阶段要切换成纯贪心def rollout(env, Q, max_steps100): s env.reset() path, done, t [s], False, 0 while not done and t max_steps: a tie_break_argmax(Q[s]) s, r, done env.step(a) path.append(s) t 1 return path评估时一定要关掉探索否则你看到的表现其实是随机动作的水平。3.3 超参数 α、γ、ε 怎么定这三个参数我踩过最多的坑直接给一张能用的参考表参数含义常用范围定值逻辑α学习率0.05 ~ 0.3状态多、噪声大就调小小表可以到 0.5γ折扣因子0.9 ~ 0.99有效视野约 1/(1−γ)按任务步数选ε_start初始探索率1.0一开始什么都不懂全探索最省事ε_end最终探索率0.02 ~ 0.1别降到 0环境有随机性时需要保底探索ε_decay每回合衰减0.99 ~ 0.999决定了探索到利用的切换速度γ 的选法给个具体例子如果最短路径大约 15 步走到终点γ 取 0.9 时 15 步之外的信号衰减到 0.9^15≈0.21勉强够用取 0.95 是 0.46比较稳妥取 0.99 是 0.86信号强度足够但需要更多样本收敛。先按最短路径步数的 2 到 3 倍来估视野是个不错的起点。ε 衰减速度要跟总训练回合数匹配。3000 回合配 0.995大约 1000 回合后 ε 降到 0.05 左右如果你只打算跑 500 回合还用 0.995 就相当于全程在乱走几乎没利用过学到的知识。有个简便判断ε 应该在总回合数的 30% 到 50% 处基本降到 ε_end 附近。3.4 训练结果怎么读Q 表和策略可视化跑完 3000 回合先别急着看步数曲线先把策略画出来。按 0/1/2/3 四个动作映射成 ↑↓←→ 四个符号打印成 5×5 的网格→ → ↓ ↓ ↓ ↑ ✗ → → ↓ ↑ → ✗ → ↓ ↑ → → ↓ ↑ → → → ★✗ 是陷阱★ 是终点看到这样的图说明学得基本到位了整体朝向终点收敛陷阱周围有明显绕行。如果看到箭头大面积乱指、或者所有箭头都指向同一个方向问题基本不出在算法本身而是超参数或者奖励设计。再看步数曲线。健康的曲线通常长这样前 20% 回合步数波动很大还在探索中间快速下降后 40% 稳定在接近最短路径的长度上小幅抖动。如果曲线一直贴着最大步数上限说明 agent 根本没找到终点八成是奖励太稀疏如果曲线降到很低之后又反弹通常是 α 太大导致的价值震荡。4. 探索策略ε-greedy 只是起点4.1 硬探索与软探索的取舍ε-greedy 的逻辑是以 ε 的概率完全随机选动作剩下 1−ε 的概率选当前最优。它是硬探索——要么全随机要么全贪心中间没有过渡。好处是简单、无参数、行为可解释。坏处有两个。第一随机选动作时它是均匀分布的在 100 个动作里随机挑一个拿到有用信息的概率极低。第二ε 固定不变时学得再好也会持续有 ε 比例的无用动作最终性能上限被 ε 本身压住了。软探索的代表是 Boltzmannsoftmax策略按 Q 值算一个概率分布Q 值高的动作概率大但每个动作都有机会被选中。温度参数 τ 控制分布的平缓程度τ 大就接近均匀随机τ 小就接近贪心。代码就一行def softmax_policy(q_row, tau0.5): z (q_row - q_row.max()) / tau # 减最大值防溢出 p np.exp(z) return p / p.sum()软探索的好处是它有偏好地探索——在明显较差的动作上浪费的机会少同时也不会完全排除它们。在动作数较多比如 20 个以上的场景里softmax 通常比 ε-greedy 收敛更快。代价是 τ 本身也需要调而且在 Q 值尺度变化剧烈的时候 τ 的含义会漂移需要配合归一化使用。4.2 ε 衰减曲线的几种写法和实测差异衰减方式看着是细节实际影响不小。我用同一个网格世界对比过三种常见写法衰减方式公式特点适用场景指数衰减ε ← max(ε_end, ε × decay)前期降得快后期平稳最通用默认选它线性衰减ε ← max(ε_end, ε − Δ)匀速下降容易控制回合数少、目标明确倒数衰减ε ← 1/(1 k·episode)前期缓慢、后期极慢需要长时间持续探索实测下来的结论是指数衰减配 0.995 到 0.999 基本能满足九成场景线性的价值主要在于你能精确预测第几回合降到多少。倒数衰减在理论上更符合保证每个状态动作对都被访问无穷多次的收敛条件但在有限训练预算下前期探索太慢反而不划算。还有个容易被忽视的点ε 是按回合衰减还是按步衰减。按回合衰减更常见但如果不同回合长度差异巨大有的 10 步结束有的 200 步按回合衰减会导致步数多的回合获得更多探索。我一般推荐按步数衰减把总探索步数而非总回合数作为预算来管理更可控。4.3 乐观初始化与计数奖励这两个技巧能在不加 ε 的情况下制造探索动力挺有意思。乐观初始化是把 Q 表初始值设成一个偏高的正数比如全设为 1.0 而不是 0。这样每个动作第一次被选中时TD 误差都是负的Q 值会往下掉——也就是说没试过的动作看起来比试过的更有吸引力agent 自然会去试新的东西。这招在确定性环境里效果很好几行代码就能把探索做得相当充分。缺点是环境有随机性时会失效因为你可能连续几次都拿到差结果把本来不错的动作值压得过低。计数奖励是给每个 (s,a) 配一个访问计数器访问越少的动作额外加一点奖励novelty 0.5 / np.sqrt(1 count[s, a]) # 越常访问加成越小 td_target r novelty gamma * np.max(Q[s_next])这就把去没去过的地方变成了一个显式的优化目标。在稀疏奖励环境里这招经常比调 ε 管用得多。注意计数奖励的系数别贪大。系数超过即时奖励的量级agent 会沉迷刷访问计数、完全不管任务目标表现为四处乱逛却永远到不了终点。5. Q-Learning 不收敛时的排查链路5.1 先看奖励稀疏奖励与奖励塑形Q-Learning 学不动第一个要查的永远是奖励。最常见的症状是无论训练多久步数曲线都贴着最大值不动。这几乎可以确定是agent 从来没碰巧到过终点导致所有 Q 值恒为 0梯度信息无处产生。排查方法很土但有效在所有 Q 值都为零的初始状态下跑 1000 次随机策略统计到达终点的比例。如果是 0 或者千分之几说明探索预算根本不够覆盖到目标再怎么调 α、γ 都是白费。解决办法是奖励塑形reward shaping给中间过程一点引导。两种常见做法一是基于距离的塑形每一步给的惩罚正比于离目标的距离变化靠近就少罚甚至给一点正奖励二是分阶段给中间里程碑奖励比如先学会走出去再学会绕过陷阱最后学会到达终点。奖励塑形有个理论坑要注意塑形项设计不当会改变最优策略。有个经典结论指出如果塑形奖励能写成状态势函数之差的形式最优策略不变。工程上更实用的检验方法很简单——先把塑形项拿掉用人工设计的最优路径算一遍总回报确认塑形后的最优策略仍是同一条路径。如果塑形完 agent 找到的是完全不同的走法那说明塑形把目标带偏了。还有一类症状是分数看着上去了但行为很怪比如一直贴着墙走、或者在某个格子反复打转。这基本是奖励结构有漏洞agent 找到了钻空子的办法。这种情况要去检查所有低成本高回报的路径而不是继续调参数。5.2 再看学习率和折扣因子的耦合排除了奖励问题接着查 α 和 γ 的组合。α 太大0.5 以上时每次更新都大幅覆盖旧值而 TD 目标本身带噪声结果是 Q 值在真实值附近剧烈震荡策略也跟着反复横跳。表现为步数曲线锯齿明显、最优路径来回变。α 太小0.01 以下时价值传播极慢尤其是稀疏奖励任务里终点那个正奖励要经过几十次更新才能传到起点附近。表现为步数曲线缓慢下降甚至看起来像平的。γ 太小时agent 只看眼前几步会为了省一步的惩罚而走进死胡同γ 太大时需要更多样本才能收敛而且容易放大噪声。有个实用经验固定 γ只扫 α。取 α ∈ {0.05, 0.1, 0.2, 0.3, 0.5}跑五次看哪个收敛最快最稳。因为 γ 是任务语义层面的参数决定视野不该为了收敛速度去动它。另外可以试试学习率衰减前期用大 α 快速铺开后期用小 α 精修。写法就是 α_t α_0 / (1 t/τ)其中 t 是累计更新次数。理论上 Q-Learning 收敛需要学习率满足两条所有状态下更新次数趋于无穷且学习率平方和有限。u 形下降的 α_t 正好符合这两条而固定 α 其实不满足——这也是为什么固定 α 的 Q-Learning 最后会在最优值附近小幅抖动而不是精确收敛。5.3 状态离散化的粒度问题把连续状态离散成格子粒度是最容易被忽略的一环。太粗不同的物理状态被塞进同一个格子agent 学到的策略在真实环境里根本执行不出来太细状态数爆炸每个状态被访问的次数急剧下降学习效率断崖式下跌。我做过一次对比同样的任务把每个维度的离散档数从 5 提到 20状态数从 125 涨到 8000需要的训练回合数涨了将近 30 倍而最终性能只提升了不到 5%。离散粒度不是越细越好它和训练预算之间有明确的权衡。判断粒度是否合适的实用方法统计每个状态的访问次数分布。如果超过一半的状态只被访问过不到 5 次粒度就是过细了。另一个方法是看策略图的平滑度——相邻状态的推荐动作应该大体一致如果相邻格子推荐的动作用力方向完全不同说明状态划分割裂了连续的物理空间。对于维度高的连续状态别用均匀网格维度一多就组合爆炸用tile coding或者径向基函数这类稀疏编码方式更划算。tile coding 的思路是用多组错位的粗网格叠加既保持了泛化能力又不会让状态数指数增长。5.4 Q 表膨胀与 max 偏差两个属于用久了才会遇到的问题。Q 表膨胀状态数到百万级时一个 float64 的 Q 表就要几百 MB如果动作有几十个内存直接顶不住。解决办法主要有三种——用哈希表存稀疏访问过的状态、用低精度浮点float32 甚至 float16、以及直接转向函数逼近也就是下一节的 DQN。工程上我一般先看内存占用超过 500 MB 就考虑换成函数逼近别硬撑。**maximization bias最大化偏差**这个问题更隐蔽。TD 目标里那个 max 操作会让估计值系统性地偏高。原因很简单你在一堆带噪声的估计值里挑最大的噪声高的那个更容易被选中于是挑出来的最大值的期望大于真实最大值。这会鼓舞 agent 去尝试那些被高估的动作在稀疏奖励、方差大的任务里尤其明显。有个经典的反例环境叫最大化偏差任务Q-Learning 在那里会学出一个次优策略而且一直不自知。解决办法是Double Q-Learning维护两张 Q 表用一张选动作、另一张评估价值把选和评这两件事解耦。代码改动很小# 用 Q1 选动作用 Q2 给价值 best_a tie_break_argmax(Q1[s_next]) td_target r gamma * Q2[s_next, best_a] # 交替更新 Q1 和 Q2 if random.random() 0.5: Q1[s, a] alpha * (td_target - Q1[s, a]) else: Q2[s, a] alpha * (td_target - Q2[s, a])付出的是双倍内存换来的是更准的价值估计。6. 从 Q 表到神经网络DQN 接棒时解决了什么6.1 连续状态空间下 Q 表为什么失效Q 表能用前提是状态可以枚举。一旦状态变成连续量——比如传感器读出的角度是 0.3721 弧度速度是 1.83 米每秒——你就没法建表了。硬要离散化就是上一节说的组合爆炸问题。更深层的问题是泛化。Q 表里状态 (3,4) 学到的经验完全无法帮助状态 (3,5)哪怕它们物理上只差一格。而现实世界里相近状态的最优动作通常是相近的。这个相似状态应该有相似价值的性质叫平滑性假设Q 表完全用不上它白白浪费了大量样本。函数逼近就是来解决这两件事的用一个参数化函数 Q(s,a;θ) 代替查表。输入状态输出每个动作的 Q 值。参数 θ 是共享的所以更新一个状态会同时影响与之相近的状态泛化自然就发生了。最常用的逼近器就是神经网络加上神经网络之后Q-Learning 就变成了DQNDeep Q-Network也就是大家说的深度强化学习。6.2 经验回放与目标网络各自解决什么直接把 Q-Learning 的更新公式套到神经网络上训练基本会崩。原因有两个对应的解决方案就是 DQN 的两个核心技巧。问题一样本强相关。强化学习的数据是一条轨迹接一条轨迹产生的相邻样本高度相关而且分布随着策略更新不断漂移。神经网络在独立同分布数据上表现最好喂给它这种数据会灾难性遗忘、震荡不收敛。经验回放Experience Replay把 (s, a, r, s, done) 存进一个环形缓冲区训练时随机采样一个 batch。这一步同时解决三件事打破时间相关性、提高样本利用率一个样本可以被反复用、让数据分布相对平稳。from collections import deque import random class ReplayBuffer: def __init__(self, capacity100000): self.buf deque(maxlencapacity) def push(self, s, a, r, s_next, done): self.buf.append((s, a, r, s_next, done)) def sample(self, batch_size): batch random.sample(self.buf, batch_size) s, a, r, s_next, done zip(*batch) return (np.array(s, dtypenp.float32), np.array(a, dtypenp.int64), np.array(r, dtypenp.float32), np.array(s_next, dtypenp.float32), np.array(done, dtypenp.float32)) def __len__(self): return len(self.buf)问题二目标在动。Q-Learning 的 TD 目标里包含 max Q(s,a)如果评估目标和被更新的网络是同一个那每次参数更新都会同时改变预测和目标相当于追着一个自己也在跑的靶子。这会导致训练发散。目标网络Target Network维护一份参数被冻结的副本专门用来算 TD 目标每隔 C 步才把主网络的参数复制过去。靶子在一段时间内固定了训练才稳定。def train_step(q_net, target_net, optimizer, batch, gamma0.99): s, a, r, s_next, done batch # 计算 TD 目标使用目标网络并且不传梯度 with torch.no_grad(): q_next target_net(s_next).max(dim1)[0] td_target r gamma * (1.0 - done) * q_next # 只取实际执行动作对应的 Q 值 q_sa q_net(s).gather(1, a.unsqueeze(1)).squeeze(1) loss F.smooth_l1_loss(q_sa, td_target) # Huber 损失更抗离群点 optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(q_net.parameters(), 10.0) optimizer.step()里面几个细节值得说。(1.0 - done)那个乘法是终止状态的处理等价于终止状态没有 bootstrap 项。用gather只取实际执行动作的 Q 值是因为网络输出的是所有动作的 Q而损失只关心执行过的那个。损失函数用 Huber 而不是 MSE是因为 TD 误差里带噪声离群点用 MSE 会放大成很大的梯度Huber 在大误差区退化成线性更稳。梯度裁剪是保险丝防止偶发的巨大误差把网络带跑偏。6.3 一个最小 DQN 的实现骨架把上面几块拼起来主循环大概是这个形状for episode in range(num_episodes): s env.reset() while True: # ε-greedy 在动作价值上做探索 if random.random() eps: a env.action_space.sample() else: with torch.no_grad(): a int(q_net(torch.tensor(s).unsqueeze(0)).argmax()) s_next, r, done, _ env.step(a) buffer.push(s, a, r, s_next, done) s s_next # 缓冲区里样本够了才开始学 if len(buffer) warmup_steps: batch buffer.sample(batch_size) train_step(q_net, target_net, optimizer, batch) # 每隔 C 步同步一次目标网络 if total_steps % target_sync 0: target_net.load_state_dict(q_net.state_dict()) total_steps 1 if done: break eps max(eps_end, eps * eps_decay)warmup_steps这个参数新手经常漏掉它指的是缓冲区里攒够多少样本才开始训练。我的经验值是至少 1000 到 10000 步太少的话前期全是随机策略产生的低质量数据网络会被带偏。另外要注意训练频率每一步都训一次在复杂环境里太慢可以改成每 4 步训一次效果差别不大但速度快不少。DQN 本身还有一堆变体Double DQN 解决上面的 max 偏差Dueling DQN 把 Q 拆成状态价值和优势两部分Prioritized Replay 按 TD 误差大小决定采样优先级。这些都是工程优化核心思路仍然是从 Q-Learning 那条更新公式里长出来的理解了这里看那些论文会轻松很多。7. 落到真实项目Q-Learning 能干什么、不该干什么7.1 参数自整定场景状态动作奖励怎么设计表格型 Q-Learning 在工业现场其实还有不少用武之地因为它可解释、可审计、算力要求低——这三点在很多项目里比性能指标更重要。典型场景之一是控制器参数的自整定比如让 agent 在线调节控制器的比例、积分、微分三个系数让系统在不同工况下都保持较好的响应。这类项目里三要素的设计是这样的状态不要直接把误差和误差变化率丢进去先做归一化和分段。误差分成正大、正小、接近零、负小、负大五档误差变化率同样五档组合出 25 个状态。25 个状态在实践中足够用而且每个状态的访问次数都能上百学习效率很高。动作把参数调节量离散成大幅增加、小幅增加、不变、小幅减小、大幅减小五档只对其中最关键的一到两个参数做调节。三个参数同时调动作空间立刻膨胀到 125样本效率会崩。奖励这是最需要动脑子的地方。常用形式是误差绝对值积分IAE或误差平方积分ISE在固定时间窗内的负值配上超调惩罚$$r -\left(w_1 \int |e| dt w_2 \cdot \max(0, y_{max} - y_{ref}) w_3 \cdot |\Delta u|\right)$$三项分别惩罚跟踪误差、超调量、动作幅度。第三项经常被忽略但很重要——没有它agent 会学到疯狂来回调参数这种让执行机构提前报废的策略。有个我在实际项目里总结的经验奖励里的权重不要靠拍脑袋先用一组基线参数跑一遍把三项的量级记录下来再按三项贡献相当的思路配权重。否则很容易出现某一项完全主导、其他项形同虚设的情况。7.2 与 MILP、离线强化学习的分工强化学习不是万能的搞清楚它和别的算法怎么分工比硬上要省事得多。和混合整数线性规划MILP这类精确求解方法相比强化学习的优势在于在线决策速度快。MILP 能给最优解但求解时间随规模增长很快很多场景下没法满足实时性要求。Q-Learning 训练好之后一次前向查表是毫秒级的。所以一个常见的组合是离线用 MILP 生成大量高质量解把它们作为示范数据喂给强化学习或者用 MILP 的求解结果作为奖励函数里的上界参考。这样既拿到了接近最优的性能又保证了在线速度。反过来说如果求解规模本来就很小、每次调用的实时性要求不苛刻那就用 MILP别绕这一圈。和离线强化学习比如 IQL 这类只从固定数据集学习的算法常被简称为离线 RL相比区别在于能不能和真实环境交互。线上 Q-Learning 需要不断试错这在很多业务场景里是不允许的——你不能让推荐系统在真实用户身上乱试也不能让生产线上的设备去撞南墙。离线强化学习只从历史数据里学不需要探索代价是要处理分布外动作和外推误差的问题。选哪个的判断标准很直接能不能安全地大量试错。能就用在线 Q-Learning 或 DQN不能就走离线路线。另外联邦式的分布式训练在一些多设备协同场景里也有用——多台设备各自本地训练 Q 表只交换参数而不是原始数据兼顾了效率和隐私。不过这类方案的通信开销和异构性问题不小不是所有场景都值得上。关于小参数模型到底用监督微调还是强化学习这个经常被问到的问题放到决策类任务里答案比较清楚如果存在明确的正确答案有标注数据优先用监督学习收敛快、稳定性好只有当目标是优化一个只能事后评估的指标比如最终收益、用户留存时才值得上强化学习。两者不是替代关系通常是先用监督方式把策略训到一个不错的起点再用强化学习去微调那个真正关心的指标。7.3 上线前的三条底线从实验代码到线上系统中间隔着几道必须过的关。第一永远保留一个安全兜底。学出来的策略一定要在接受之前做最坏情况检查。我的做法是在仿真环境里用带随机扰动的初始状态跑几千次统计最差表现。如果最差情况下会出现明显不合理的动作就给策略加一层硬约束——动作幅值限幅、变化率限制、危险动作白名单。Q-Learning 的策略是在训练分布内最优出了分布就是未知数兜底不是可选项。第二把 Q 表当模型资产管起来。Q 表是可以直接 JSON 化的它比神经网络权重好审查得多——你可以直接看出每个状态下的决策逻辑。上线时记好版本、训练环境配置、超参数、评估结果。我见过因为 Q 表版本混乱导致线上行为和测试结果不一致的情况排查起来极其痛苦。第三奖励函数要和业务指标对齐。这是最容易出问题也最难发现的一环。奖励是你在训练时给的业务指标是上线后衡量的两者不一致的时候agent 会精准地优化奖励然后让业务指标变差。上线前一定要做的事是把 agent 在仿真里的行为和对应的业务指标一起算出来人工确认方向一致。这一步花的时间比后面返工要少得多。我个人在几个项目里的体会是Q-Learning 落地最难的从来不是算法算法本身二十行代码就写完了难的是奖励设计、状态划分、安全边界这三件事它们决定了这套东西到底能不能用。如果刚开始做我的建议是先在一个能完整跑通的简化环境里把这三件事想清楚再往真实的复杂环境迁移——而不是反过来先上复杂环境再回头补这些基础。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询