做强化学习项目的时候很多人一开始会纠结一个很基础但又绕不开的问题环境的转移动态我们到底知不知道如果不知道还能不能做控制这就是 model free 控制出场的时机。简单说model free 控制就是在没有环境模型也就是不清楚状态转移概率和奖励函数的精确表达式的情况下只靠与环境交互产生的样本数据直接学出一个能拿到高累积回报的策略。这篇内容我打算把无模型控制的知识体系、核心算法、实现细节和踩坑经验一次性讲透适合刚入门强化学习、或者已经在跑实验但总觉得哪里不对劲的读者。很多人会把 model free 控制和“无模型预测”混在一起实际上预测是评估一个给定策略的价值函数控制才是要在这个基础上找到最优策略。我们在实操中用到的 Q-learning、SARSA、策略梯度这些算法本质上都是在做无模型控制只是它们找最优策略的路径完全不同。把这层关系理清楚后面看任何算法的代码都不会晕。1. 无模型控制到底在解决什么问题1.1 从“知道规则”到“只能试错”在有模型的情况下比如经典的网格世界你明确知道每个格子向四个方向移动后分别会落到哪里也知道每一步的即时奖励是多少那么动态规划里的策略迭代和价值迭代可以直接用。因为我们可以枚举所有状态、所有动作用贝尔曼最优方程一遍遍更新直到价值函数收敛。但真实场景没这么友好。玩Atari游戏的时候你不知道仿真器的内部状态转移矩阵控制机械臂的时候摩擦力、关节柔性带来的动力学偏差很难精确建模哪怕是物流调度订单到达的随机分布也很难用解析式写出来。环境不是给你一个公式而是给你一个黑盒接口你输入动作它返回下一个状态和一个奖励。这种场景下动态规划直接失效因为缺少转移概率和目标状态分布。于是我们只能退回到最原始也最有效的办法不断尝试收集经验从经验里估计价值。这就是无模型控制的起点。它不假设环境规律只假设你可以通过交互得到样本。所有算法都围绕一个问题如何用有限的经验可靠地估计“采取某个动作到底值多少”并据此优化策略。理解了这一点你就能理解为什么无模型算法普遍比有模型算法数据效率低但也正因为不需要模型适用范围反而更大。1.2 核心要素策略、价值函数和回报先快速对齐几个术语。强化学习里我们讨论的是马尔可夫决策过程由状态集、动作集、转移概率、奖励函数和折扣因子组成。有模型时转移概率和奖励函数是已知的无模型时这两样都未知我们只能依靠轨迹样本一串状态、动作、奖励的序列。在无模型控制里价值函数依然是核心。状态价值函数 V(s) 表示从状态 s 出发按照当前策略能获得的期望折扣回报动作价值函数 Q(s,a) 表示在状态 s 先执行动作 a之后按照当前策略继续能获得的期望折扣回报。做控制的时候我们更关心 Q(s,a)因为决策时只要比较同状态下不同动作的 Q 值就能选出更好的动作。回报的定义也值得说一句。折扣回报是累加每一步奖励再乘上折扣因子 gamma 的幂次。gamma 越接近 1智能体越看重长期收益越小越短视。实操中 gamma 通常取 0.9 到 0.999 之间取值大小会明显影响学习的稳定性后面我还会细说。无模型控制算法的共同模式是先通过采样得到大量数据然后用某种方法把 Q 函数估计出来最后基于 Q 函数改进策略。区别只在于“如何估计”和“如何改进”这两个环节的具体做法。2. 两大算法家族基于价值与基于策略2.1 基于价值把 Q 函数一点点算准基于价值的方法不直接管策略长什么样它只维护一个价值函数表或近似函数理想情况下最后拿到最优价值函数 Q* 然后从中导出贪心策略。学习 Q 函数有两条技术路线蒙特卡洛和时序差分。蒙特卡洛方法最简单跑完一个完整的回合拿到真实的折扣回报然后用它来更新这个回合中出现过的状态动作对。它的缺点是必须等回合结束才能更新而且方差很大因为一条轨迹里各种随机性都会被算进去。时序差分方法则只用一步或 n 步的即时奖励加上对下一状态价值的估计就可以更新当前状态的 Q 值。比如 Q-learning 的核心更新就是Q(s,a) ← Q(s,a) α [ r γ·max_{a} Q(s,a) - Q(s,a) ]这个式子最大的特点是 max 操作和实际采取的动作没有关系。更新时用的目标包含“下一个状态处所有动作里最大的 Q 值”这相当于假设自己接下来一定会采取最优动作。因此 Q-learning 被称作 off-policy 算法它学习最优价值函数但采样的过程可以用任何行为策略。这意味着你可以用一个比较“探索性”的策略去收集数据同时让 Q 值收敛到最优策略的价值这就是为什么它这么受欢迎。而 SARSA 不一样。SARSA 的更新用的是当前策略实际可能会采取的下一个动作 a Q(s,a) ← Q(s,a) α [ r γ·Q(s,a) - Q(s,a) ]也就是说你估计的是“现在这个正在采样的策略”本身的价值而不是最优策略的价值。这叫作 on-policy。SARSA 的行为会更保守更安全但学出来的策略也往往受限于采样策略的探索程度。后面我会用一个具体的例子说明两者在悬崖边上的表现差异。2.2 基于策略直接在参数空间里爬山价值函数方法先估计价值再导出策略感觉有点绕。如果我们把策略本身表示成一个带参数的分布 π_θ(a|s)那能不能直接调整参数 θ让累计回报的期望最大化这就是策略梯度方法的核心思路。策略梯度定理给了我们一个可行的梯度估计方向。它可以写成∇_θ J(θ) E_τ [ ∑_{t} ∇_θ log π_θ(a_t|s_t) · G_t ]直观理解就是一个完整轨迹跑完之后如果某个动作之后拿到的回报 G_t 很高那就提高选择这个动作的概率如果回报很低就降低概率。这个 log 概率项就是“让这个动作出现得更频繁”的上升方向G_t 则决定这个方向被推多快。REINFORCE 是最简单的策略梯度算法它需要跑完完整回合才能更新和蒙特卡洛一样存在高方差的问题。解决高方差的方式之一是引入 baseline也就是从 G_t 里减掉一个与动作无关的量比如当前状态的优势估计。这样既能保持梯度的无偏性又能显著降低方差。Actor-Critic 方法就是在这个方向上走得更远用一个 Critic 网络来估计价值实时给出比单步回报更低的方差引导Actor 则用这个引导来更新策略。现在主流深度强化学习算法比如 PPO、SAC都是在 Actor-Critic 框架上做改进。2.3 如何选型从应用目标反推算法很多人一上来就问“哪个算法最厉害”我的建议是先看问题属性。如果状态空间和动作空间都很小比如棋盘游戏、网格导航、机械臂的离散动作控制用表格型 Q-learning 或 SARSA 就够了实现简单、调试容易、不需要GPU。如果状态是高维图像或者连续数据比如玩视频游戏、自动驾驶感知决策那就必须上深度神经网络来近似 Q 函数经典 DQN 及其变种是稳妥的起点。如果动作空间是连续的比如输出关节力矩、油门大小基于价值的方法就不那么顺手因为 max_{a} Q(s,a) 要求在连续动作空间里找一个最大值这一步不好算。这时候策略梯度类算法更合适它们直接输出动作分布或确定性的动作值PPO 是目前综合稳定性和实现难度最好的选择。问题特征推荐算法主要理由离散状态/动作、小规模Q-learning / SARSA表格方式即可调试方便高维离散观测、离散动作DQN 系算法借助神经网络拟合 Q 函数连续观测、连续动作PPO / SAC策略梯度天然支持连续动作需要在线保守策略SARSA学到的是安全策略本身的价值需要最优策略但可离线收集Q-learning可以利用所有经验数据效率高当然这只是初始选择具体问题还要看奖励形态、采样成本、实时性要求。但把这两大家族的核心逻辑搞清楚后面看任何算法论文都会顺畅很多。3. 核心算法实现细节Q-learning 与 SARSA 的实战对比3.1 环境交互与经验收集先跑通最小闭环我之前写一个模拟机器人寻路项目时第一步不是直接上复杂网络而是先实现一个最基础的表格型 Q-learning。从环境的角度看我们唯一能调用的接口就是 step(action) 和 reset()。step 会返回下一个状态、即时奖励、回合是否结束reset 把环境恢复到初始状态。伪代码里最关键的循环大概是for episode in range(num_episodes): state env.reset() done False total_reward 0 while not done: action choose_action(state) # epsilon-greedy next_state, reward, done, _ env.step(action) # 更新 Q 值 best_next np.max(Q[next_state, :]) # Q-learning td_target reward gamma * best_next * (1 - done) td_error td_target - Q[state, action] Q[state, action] lr * td_error state next_state total_reward reward这里很多初学者会忽略一个点当 done 为真时中断状态之后没有真实存在的下一状态所以应该把目标直接设为 reward而不是 reward gamma * best_next。你可以在上面的伪代码里看到我用 (1 - done) 把这一项清零避免最后一帧的 V(s) 被错误地算进去。这个细节在表格方法里可能只是慢一点收敛到了神经网络版本就会直接造成训练不稳。epsilon-greedy 是收集探索性数据最常用的手段。以 epsilon 的概率随机选择动作以 1-epsilon 的概率选择当前 Q 值最大的动作。训练初期 epsilon 高一点比如 0.3 到 0.5让智能体多探索后期衰减到 0.01 甚至更低让策略稳定复现最优行为。如果你想要更系统地探索还可以用 UCB、汤普森采样不过表格型方法里 epsilon-greedy 完全够用了。3.2 学习率和折扣因子的直觉Q-learning 的学习率 alpha 决定新信息覆盖旧信息的速度。alpha 太大单次更新引入的噪声会被放大Q 表很难稳定太小收敛慢样本效率低。我通常先从 0.1 开始调如果发现训练曲线像心电图一样大幅震荡就继续降低学习率如果曲线虽然平滑但进步很慢适当调大。折扣因子 gamma 的意义在处理长期收益的时候特别明显。假设奖励只会在终点给一个 1中途全是 0如果 gamma0.9那么终点前一步的 Q 值大约是 0.9两步前是 0.81距离越远信号越弱。这会导致一个实际问题奖励传播速度慢智能体需要很多回合才能学会把远方的奖励“扩散”到一步之前的状态。在稀疏奖励环境里gamma0.99 或者更高能帮助加速传播但代价是会放大未来估计值的不确定性可能造成价值震荡。你可以把 Q-learning 更新的过程想象成在未知地图上画等高线。每走一步你只修正当前位置的高度但修正后的高度里用了“前方最高的下一格”的高度作为参考所以信息会逐层向前传播。gamma 就是衡量“前方信息对你现在决策的影响有多远”的尺子。3.3 悬崖行走SARSA 与 Q-learning 的本质差别我在学习 on-policy 和 off-policy 的时候发现悬崖行走是最直观的例子。一条窄路通往终点路的两侧是悬崖进入悬崖获得 -100 的惩罚并回到起点。Q-learning 在更新时总是取下一个状态的最大 Q 值哪怕下一个状态实际处在悬崖边缘如果它显示边缘格子的某个方向有较高的 Q 值来自更远终点的传播Q-learning 会认为这个边缘状态“很香”把这种乐观信号往回传。它最终学到的是理论上最优但贴着悬崖边的路径。因为更新目标里的最优值和你实际采样的策略不一致所以哪怕智能体经常掉下悬崖Q 值还是在向往最优方向收敛。SARSA 不一样。它评估的是实际采样策略的价值。如果当前行为策略大概率会走到悬崖边缘SARSA 就会把这个糟糕的后续结果计入当前 Q 值于是智能体会学到一条离悬崖远一点的保守路径。在训练早期探索率还很高的时候SARSA 的路径会显得更“怕死”哪怕最优路径贴着悬崖它也更倾向于绕道。这是 on-policy 的代价也是它的安全优势。实际项目里的选择准则是如果环境失败代价很高比如真实机器人、生产调度我会优先考虑 on-policy 算法或者把 Q-learning 里的行为策略设计得极度保守比如压低探索率、加安全护栏。如果是在仿真里训练失败无非是重新开始那就大胆用 Q-learning 这类 off-policy 算法因为它们的数据利用率更高能更快逼近最优策略。4. 从表格到近似连续空间和深度网络的坑4.1 表格方法的边界表格型 Q-learning 的优点是精确、可控但它把每个状态动作对当作一个独立参数来存储。假设状态是位置和速度每个维度都归一化到 100 个区间两个维度就有 10000 种状态再乘上动作数量表格就已经很庞大了。如果状态是图像每个像素的灰度值组合更是天文数字。表格方法还有泛化能力为零的问题某个状态没被访问过它的 Q 值就是初始猜测值无法从附近状态推断。这时候我们必须用函数逼近器 g_w(s,a) 来近似 Q(s,a)最经典的工具就是深度神经网络。用神经网络的好处是它可以利用状态之间的相似性相似的输入特征会被映射到相似的输出从而把有限样本学到的信息泛化到整个状态空间。但这也带来了新的问题神经网络训练对数据独立性要求高而强化学习相邻样本高度相关直接做梯度下降会导致灾难性震荡。4.2 DQN 的两个关键技术经验回放与目标网络2015 年前后 DQN 的成功很大程度上归功于两个实践技巧。第一个是经验回放。把每一步产生的转移数据 (s, a, r, s, done) 存进一个缓冲区训练时随机采样一个小批量做更新。这样可以打乱样本间的时间相关性让更新假设更像独立同分布数据稳定训练。缓冲区大小通常从几万到几百万不等根据问题复杂度和内存资源决定。第二个是目标网络单独维护一份更新频率较慢的 Q 网络参数计算 TD 目标时使用这个旧参数网络来给出 Q(s)而不是使用正在更新的网络。如果直接用同一个网络计算目标又更新它目标会随参数变化而移动形成“追自己的尾巴”的问题训练极易发散。一个简洁的 DQN 更新伪代码可以这样写batch replay_buffer.sample(batch_size) states, actions, rewards, next_states, dones batch q_values q_net(states).gather(1, actions) with torch.no_grad(): max_next_q target_net(next_states).max(1).values targets rewards gamma * max_next_q * (1 - dones) loss smooth_l1(q_values, targets.unsqueeze(1)) optimizer.zero_grad() loss.backward() optimizer.step()注意我在 loss 里用了 smooth_l1 而不是 MSE这是从 Faster R-CNN 里借来的经验。MSE 对离群误差惩罚更重在强化学习中容易被极端奖励干扰smooth_l1 对误差大于 1 的部分梯度恒定为 1对小于 1 的部分用二次函数训练更平稳。两种损失都能用但我实测下来 smooth_l1 在奖励尺度变化大的环境里更省心。每过若干步才把 q_net 的权重拷贝到 target_net我记得常用做法是每隔几百步硬拷贝一次或者用软更新target_net τ * q_net (1-τ) * target_netτ 取 0.005 左右。软更新在 DDPG、SAC 这类算法里更常见效果也更平滑。4.3 深度无模型控制的调参心得用神经网络做 Q-learning 的时候很多在表格方法里无所谓的问题会被放大。首先是奖励尺度。神经网络输出的 Q 值如果动辄上百而梯度更新基于数值差的平方数量级不匹配就会导致训练发散。我习惯先把奖励通道梳理一遍把即时奖励缩放到 [-1, 1] 或者 [0, 1] 的范围。如果奖励本身很稀疏可以在环境层加人工塑形奖励但要小心不要引入与真实目标冲突的局部最优。第二个是探索策略。epsilon-greedy 里 epsilon 的衰减速度要匹配任务长度。短回合任务可以衰减得快一些长回合、稀疏奖励任务最好让 epsilon 保持较高值更长时间。我在多个任务里用过指数衰减 eps max(0.01, initial_eps * decay_step)初始值 1.0decay 到 0.01 需要大约总训练步数的三分之二。这样前期敢闯后期纪律好。第三个是梯度裁剪。尤其是刚起步时Q 值估计偏差可能很大一次更新的梯度过大直接就把参数推向灾难区。给梯度范数设一个上限例如 max_norm10能有效避免这种崩溃。这三个技巧组合起来大多数小型深度强化学习实验都能得到可复现的结果。5. 策略梯度与 Actor-Critic从采样到优雅引导5.1 为什么策略梯度可以直接输出连续动作如果我们想让机械臂输出连续力矩需要策略网络输出动作分布的参数比如高斯分布的均值和标准差。采集动作时从分布中采样一个值作为动作这个采样过程是可微的。策略梯度告诉我们优化目标是最大化期望回报梯度方向可以用 log π(a|s) 的梯度乘以回报来估计。一个最小化的 REINFORCE 更新大概是这样的for trajectory in trajectories: for t in range(len(trajectory)): G_t sum(rewards[t:] * (gamma ** np.arange(len(rewards[t:])))) log_prob dist.log_prob(actions[t]) loss -(log_prob * G_t).mean()这个写法看起来没问题但方差大到难以忍受。因为不同轨迹之间的回报差异很大有的轨迹运气好拿到高分即使动作一般也会被错误地推高。减掉一个 baseline 可以把“绝对回报”换成“相对回报”。常用的 baseline 是状态价值 V(s)于是 G_t - V(s_t) 被称作优势估计表示这个动作比平均水平好多少还是差多少。5.2 Critic 帮 Actor 画出一条更稳的上坡路Actor-Critic 的思路就是让 Critic 网络学习价值函数 V(s)或者 Q(s,a)然后给 Actor 提供优势信号。Actor 的目标函数不再是原始的期望回报而是最大化优势函数乘以 log 概率。Critic 的更新则用 TD 误差比如价值网络的最小化目标是 (r gamma V(s) - V(s))^2。这个过程有很优雅的直觉Critic 就像一个经验丰富的教练他在每个状态告诉你“目前你做得怎么样”Actor 则根据“这个动作带来的结果是否比预期好”来调整。因为 Critic 使用时序差分可以不等到回合结束就给出信号方差比蒙特卡洛低很多。同时 Actor 依然保留了策略梯度对连续动作的天然支持。5.3 从零写一个简化版 Actor-Critic这里我给出一个既简单又能跑通的伪代码级示例用的是 PyTorch 风格。它适用于只包含一个连续状态输入、离散动作输出的简单 Gym 环境。Policy 网络输出离散动作的 logitsValue 网络输出一个标量。class ActorCritic(nn.Module): def __init__(self): super().__init__() self.common nn.Sequential(nn.Linear(state_dim, 64), nn.ReLU()) self.policy nn.Linear(64, num_actions) self.value nn.Linear(64, 1) def forward(self, x): h self.common(x) action_logits self.policy(h) value self.value(h) return action_logits, value def compute_loss(batch): states, actions, rewards, next_states, dones batch logits, values model(states) dist Categorical(logitslogits) log_probs dist.log_prob(actions) _, next_values model(next_states) td_target rewards gamma * next_values.squeeze() * (1 - dones) advantages td_target - values.squeeze() actor_loss -(log_probs * advantages.detach()).mean() critic_loss F.mse_loss(values.squeeze(), td_target.detach()) return actor_loss critic_loss每次从环境采样一批经验后就做一步优化。这是最简单的 online Actor-Critic不是最新的算法但作为理解框架已经足够。真正稳定好用的 PPO 其实是在这个基础上加了重要性采样和裁剪目标限制了策略更新的步长防止一步走太远导致性能崩塌。你掌握这个小例子之后再去看 PPO 的伪代码会发现完全没有障碍。6. 常见问题与排查技巧实录6.1 训练不收敛按顺序检查这四个原因我每次调试无模型控制算法遇到不收敛的时候会像体检一样按顺序排查。第一检查数据流向环境返回的 state 是否被正确归一化奖励的数值是否在合理的尺度done 标志是否在终止状态时被正确传递这些 bug 听起来低级实际上消耗了我最多时间。第二检查 Q 值或者优势函数是否出现严重高估。Q-learning 里 max 操作自带高估偏差如果你发现 Q 值一路向上而实际回报没有同步上升大概率是深度网络的表达能力太强、更新过于激进。可以考虑用 Double DQN 的思路用当前网络选动作、目标网络给价值能显著减少高估。第三检查探索是否过早结束。epsilon 很快降到 0.01 之后策略几乎不再探索如果初始策略不好就会卡在局部最优曲线看起来是平的。把 epsilon 的衰减曲线放慢或者改成自适应探索往往能救回来。第四检查随机种子和初始状态分布。有些环境初始状态非常随机而智能体还没学会应对各种初始位置前期波动会很大。适当固定初始状态或者让初始分布更集中训练曲线会更好看。6.2 稀疏奖励环境里的生存法则稀疏奖励是 model free 控制最大的现实阻碍。游戏里可能几十步都没有奖励机器人只有最终抓取成功才给一个 1。这时候直接跑 Q-learning奖励信号完全传不回来因为所有中间状态的价值都是 0没有任何梯度可言。我常用的手段有三个。第一个是奖励塑形。在环境里增加与目标成单调关系的中间奖励比如机器人距离目标点越近给一个小的正向奖励。这等于手工给智能体铺了一条缓坡。但千万注意塑形奖励不能与最终目标冲突否则智能体会发现“原地转圈”比“走向目标”更容易积累近距离奖励。第二个是课程学习先给一个简单的初始状态分布让智能体偶尔能碰到目标有了正向样本之后再逐步扩大初始化范围。这比直接上稀疏大任务高效得多。第三个是充分利用离线的表现出色的轨迹做模仿学习或者将价值函数初始化为偏向这些轨迹的分布相当于让智能体先知道“哪条路至少能得到分数”再开始强化学习优化。6.3 算法调参速查表无模型控制算法的超参数多到劝退我把自己常用的初始值整理成一张表方便你照着先跑通再微调。超参数表格 Q-learningDQNActor-Critic/PPO学习率0.1 ~ 0.50.0001 ~ 0.00050.0003 左右折扣因子 gamma0.9 ~ 0.990.990.99探索率 epsilon0.1逐步衰减1.0 → 0.01无需 epsilon用策略分布噪声更新步数每步更新采样 4 步更新 1 步依据环境几百步后更新经验回放大小无10000 ~ 100000on-policy 不需要目标网络更新频率无每 1000 步硬更新或软更新软更新 τ0.005批量大小无32 或 64256梯度裁剪无max_norm10max_norm0.5PPO 常用这张表不是金科玉律但如果你完全没有调参方向从这些值起步通常不会太离谱。我见过太多人一上来就把学习率设成 0.01 导致发散或者把 gamma 设成 0.5 让长期奖励完全传不动调整之后训练立刻活过来。6.4 离线训练时最容易骗到你的统计陷阱用无模型算法评估效果时很多人只看训练曲线里的“总奖励”这一条线。总奖励上升确实说明策略在变好但幅度和方差要单独分析。某一次评估得到高回报可能是碰巧探索出了好路径不代表策略稳定。我在项目里的做法是每隔固定步数暂停训练让智能体在纯贪心模式下跑 20 个回合记录平均回报和标准差。纯贪心模式就是完全不做随机探索此时的表现才是真实可用水平。还有一个坑是训练和评估用的随机种子。如果你只跑了一个种子就得出结论运气成分很大。至少跑 5 个不同随机种子观察均值趋势和中位数。在深度强化学习里中位数比均值更可靠因为个别种子可能彻底发散把均值拉得很难看。如果你想发论文或者生产应用最好把多次种子的结果都展示出来并写清楚每个种子的环境配置。我自己就吃过亏某次实验换了个种子效果完全反转排查半天才意识到是初始随机性导致的脆弱性。说到底model free 控制是一个经验密集型的领域书本公式只是第一层真正的功力来自在大量实验里感受每个参数如何影响学习动态。不要指望第一次跑实验就能顺利收敛把调试过程当成正常节奏一边调一边记录你会慢慢形成一套属于自己的避坑直觉。这也是我觉得强化学习最迷人的地方即使环境给不了你模型你依然可以靠数据、耐心和对算法的理解一点点把最优策略挖出来。