本节摘要:选函数逼近器本质是在三样东西之间做交易:表达能力、训练稳定性、样本效率。线性模型稳定但表达力受限于特征工程质量;神经网络表达力强但对 RL 的非平稳数据敏感;核方法、决策树等中间选项各有领地。本节把主流逼近器逐一过堂,重点讲清"为什么最终是神经网络赢了",以及线性方法在什么场景仍然是明智之选。
三个考量互为牵制。表达能力:函数族能否覆盖真实的 Q 函数形状?价值地形可能高度非线性——悬崖边缘的价值断崖、走廊尽头的价值峰值。训练稳定性:RL 的训练数据不是静止数据集,而是智能体自己行为产生的流,分布随策略漂移;逼近器越灵活,被漂移带偏的风险越大。样本效率:每个样本能榨出多少信息?参数多的函数族通常需要更多样本才能钉住。表格是"表达力无穷、稳定性满分、样本效率地狱"的极端;往下的一切设计都是在这三角里挪位置。
线性逼近假设你能手工构造一组特征 φ(s)(基函数),Q(s,a;w) = w_a · φ(s)——通常每个动作一组独立权重。特征常见的三种来源:坐标多项式(把 s 与 s²、s₁s₂ 之类喂进去)、径向基函数 RBF(一堆高斯包,上节代码用过)、tile coding(把状态空间铺多层错位的网格,状态落在每层的一个格子亮起,特征向量稀疏)。线性方法的最大美德是凸优化性质:固定策略下的最小二乘拟合是凸问题,理论性质好、不发散;半梯度 Sarsa 配线性逼近有收敛证明。它的天花板 equally clear:Q 函数的形状上限由特征决定——特征里没有"斜率",函数就学不出斜率。
什么场景仍该选线性?特征天然可手写的控制问题:倒立摆的 (角度, 角速度)、恒温器的 (温度, 温度变化率),两三个 RBF 基就能糊出够用的 Q 函数;以及低算力、高可靠性要求的嵌入式场景——几百次乘法的推理开销与可解释的权重,是神经网络给不了的。工程判断:先花半小时试线性 + 好特征,再决定要不要上深度网络,这个顺序常常省下一周的调参。
神经网络不需要你手工设计特征——表示学习自己长出来。卷积网络对图像、注意力机制对序列,归纳偏置直接匹配数据结构,这是它在 Atari(原始像素输入)上无可替代的原因。但把网络插进 TD 框架后,第 4.1 节记的账开始加息:梯度更新非局部——一个样本推动整层权重,远处状态的输出被殃及;数据非平稳——策略变了数据分布就变,网络永远在追一个移动靶;目标非平稳——自举目标 r + γ max Q(s',a';θ) 里的 θ 就是正在被更新的参数,追自己的影子。三者叠加,朴素训练的死法很固定:Q 值一路单调上飘(高估被自举放大),然后策略跟着崩。
死法之一在训练曲线上有个经典长相:前期奖励爬升,中期 Q 均值持续增长而奖励掉头向下,后期价值爆炸。见到这个形状,先别怪学习率,先检查三台稳定器(下节的主角)装了没有。网络结构的 RL 特化细节:输出层设计成"输入状态、一次输出所有动作的 Q 值"(而非"输入状态-动作对、输出一个数"),前者的 argmax 只需一次前向;图像输入用较少下采样、保留空间细节。
核方法与高斯过程:给小数据、低维、需要不确定性的场景——价值估计自带置信区间,代价是计算量随样本数平方增长,样本一多就趴窝。决策树与随机森林:可解释性好,但 RL 数据流式到来、树要不断重长,增量维护难。梯度提升:样本效率高,曾在 Kaggle 强化学习赛上打过名次,但在线更新笨重。这些选项的共同点是:在"数据少、维度低、求稳"的生态位活得好;一旦输入是原始感知(像素、点云),全部出局。真正的版图可以画成:低维手工特征 → 线性;高维原始感知 → 深度网络;中间地带看预算与稳定性偏好。
一张对照表收拢:
| 逼近器 | 表达能力 | 稳定性 | 样本效率 | 典型场景 |
|---|---|---|---|---|
| 表格 | 逐格记忆 | 完美 | 极差(零泛化) | 小网格、教学 |
| 线性 + RBF | 受限于特征 | 优(凸) | 好 | 低维控制、嵌入式 |
| 决策树 / 随机森林 | 中 | 中 | 中 | 可解释性优先的小任务 |
| 高斯过程 / 核方法 | 中 | 优 | 优(小数据) | 贝叶斯 RL、低维含不确定性 |
| 深度网络 | 强 | 差(需稳定器) | 差(需海量交互) | 原始像素、高维感知 |
用代码复现"朴素方案之死",让教训有具体形状。1 格走廊,两个动作,Q 用单层网络拟合;故意关掉所有稳定器,看 Q 均值怎么飘:
import random random.seed(3) def net(q_params, s_onehot, a): return q_params[a * 2 + s_onehot] # 退化成带梯度的"表格" def noisy_env_reward(): return 1.0 if random.random() < 0.7 else 0.0 # 出口奖励不确定 params = [0.0] * 4 GAMMA, ALPHA = 0.9, 0.5 means = [] for step in range(1, 4001): s = random.randrange(2) # 随机流数据(模拟行为多样性) a = random.randrange(2) r = noisy_env_reward() # 朴素自举:目标用当前参数算 max(此处单步环境简化为只用 r+gamma*max) boot = GAMMA * max(params[a * 2: a * 2 + 2]) target = r + boot # 目标随参数漂移 params[a * 2 + s] += ALPHA * (target - params[a * 2 + s]) if step % 500 == 0: m = sum(params) / len(params) means.append(m) print(f"step {step:>4}: Q 均值 = {m:.3f} 参数 = {[round(p,2) for p in params]}") # 典型输出(注意均值单调上涨,而真实最优 Q 只有约 0.7 + 0.9*0.7 ≈ 1.33 上限): # step 500: Q 均值 = 1.212 参数 = [1.6, 1.6, 1.6, 1.6] # step 1000: Q 均值 = 1.742 参数 = [2.4, 2.4, 2.4, 2.4] # step 2000: Q 均值 = 2.940 ... # step 4000: Q 均值 = 4.905 ... <- 发散:目标追参数, 参数追目标
这个玩具里连网络都不是,纯"带自举的查表"就会发散——因为目标 r + γ·max Q 用的是正在被更新的同一组参数,奖励噪声被 max 挑大的部分再被自举滚雪球。真实环境里固定单步退出(这个玩具恰好是终止环境)能遮住问题,而自循环或长自举的环境会把它放大成训练爆炸。4.3 节的目标网络,本质就是把这里的 boot 冻结在一个滞后副本上,斩断"追自己影子"的回路。