强化学习Model Free入门:从Q-Learning到DQN与PPO算法详解 1. 从零搞懂Model Free为什么它是强化学习入门的必修课刚接触强化学习那会儿我最大的困惑就是明明有模型可以学为什么还要搞一套“无模型”的方法后来踩了不少坑才明白Model Free不是“没有模型”这么简单它代表的是一整套完全不同的解题哲学。这篇文章就是把我自己学习Model Free方法的完整思路整理出来从核心概念到算法实现再到调参避坑尽量说透。如果你正在入门强化学习或者被Q-Learning、SARSA、DQN这些名词搞得头晕那这篇内容应该能帮你省下不少查资料的时间。先说清楚Model Free到底解决什么问题。强化学习的核心目标是让智能体在与环境交互的过程中学会做决策最终获得最大的累积回报。这里有个关键分岔路如果智能体知道环境的完整动力学模型——也就是知道在某个状态下执行某个动作后会以多大概率转移到哪个新状态、获得多少奖励——那它就可以直接用动态规划来求解最优策略。但现实情况是绝大多数场景下这个模型是未知的甚至根本没法建模。比如让一个机械臂学会抓取物体你很难精确写出它的动力学方程再比如让AI玩一个复杂的视频游戏游戏内部的判定逻辑你也不一定完全清楚。这时候就需要Model Free方法登场了。Model Free的核心思路很直接不去费劲估计环境模型而是直接从与环境的交互经验中学习价值函数或者策略。换句话说它跳过了“先理解世界再行动”这一步变成“边行动边理解”。这个思路的优势在于通用性强几乎不依赖对环境的先验知识代价则是样本效率通常比较低需要大量的交互数据才能学好。这也是为什么深度强化学习往往在仿真环境里训练因为真实世界里试错成本太高了。从分类上看Model Free方法主要分成三大类基于价值的方法、基于策略的方法、以及两者结合的行动者-评论家方法。基于价值的方法代表就是Q-Learning和SARSA它们学习的是每个状态-动作对的价值然后根据价值选动作。基于策略的方法比如REINFORCE直接参数化策略函数通过梯度上升来优化期望回报。行动者-评论家方法则是把两者结合起来用价值函数来降低策略梯度的方差典型代表是A2C、PPO这些。这三类方法各有各的适用场景后面我会逐一拆解。还有一个非常重要的概念需要提前说清楚On-Policy和Off-Policy的区别。这个区分在Model Free方法里特别关键因为它直接决定了你能不能用旧数据来训练。On-Policy方法要求行为策略和目标策略是同一个也就是说你只能用它当前正在执行的策略产生的数据来更新SARSA就是典型的On-Policy。Off-Policy方法则允许行为策略和目标策略不同你可以用旧策略甚至别人的策略产生的数据来训练当前策略Q-Learning就是典型的Off-Policy。这个区别看起来只是学术上的分类但在实际工程中影响巨大——Off-Policy通常样本效率更高但训练稳定性更难保证On-Policy虽然浪费数据但收敛性往往更好。我刚开始学的时候总觉得这些概念太抽象后来发现用一个简单的类比就很好理解。想象你在学开车On-Policy就像是你必须自己开一遍才能学会某个路口的处理方式而Off-Policy则是你可以看别人的驾驶录像来学习。显然看录像更高效但录像里的操作不一定完全适合你的车况和反应速度所以需要更复杂的修正机制。这个类比虽然不严谨但能帮你快速建立直觉。2. 核心算法拆解从Q表到神经网络2.1 Q-Learning与SARSA表格型方法的双雄Q-Learning应该是大多数人接触的第一个Model Free算法它的核心就是一个Q表记录每个状态-动作对的预期回报。更新公式是Q(s,a) ← Q(s,a) α [r γ max Q(s,a) - Q(s,a)]这个公式看起来简单但里面有几个关键点值得展开说。首先是max Q(s,a)这一项它表示的是在下一个状态s下选择所有可能动作中Q值最大的那个。这个“max”操作就是Q-Learning作为Off-Policy方法的根本原因——它更新时用的是最优策略的估计值而不是实际执行策略的值。这意味着即使你当前在用探索策略比如ε-greedy选动作Q-Learning仍然在朝着最优策略的方向更新。SARSA的更新公式只差一个符号Q(s,a) ← Q(s,a) α [r γ Q(s,a) - Q(s,a)]这里用的是实际在s状态下执行的动作a的Q值而不是最大值。所以SARSA是On-Policy的它评估和改进的是当前实际执行的策略。这个区别在悬崖行走这个经典问题上体现得特别明显Q-Learning会学到沿着悬崖边缘走的最优路径因为它假设之后都会选最优动作而SARSA会学到离悬崖远一点的更安全路径因为它考虑了探索过程中可能掉下去的风险。我在实际项目中用过这两种方法做简单的路径规划实测下来SARSA在训练过程中的表现更稳定因为它的更新目标更保守。但最终收敛后Q-Learning往往能找到更优的策略。选择哪个取决于你的应用场景如果试错成本很高SARSA更安全如果追求最终最优性能且可以承受训练风险Q-Learning更合适。2.2 函数逼近与DQN当状态空间爆炸时怎么办表格型方法有个致命问题状态空间稍微大一点Q表就存不下了。比如一个简单的视频游戏屏幕像素作为状态的话状态数量是天文数字。这时候就需要函数逼近用参数化的函数比如神经网络来近似Q值。DQN是DeepMind在2013年提出的里程碑式工作它用深度神经网络来近似Q函数并且引入了两个关键技巧来稳定训练。第一个是经验回放把智能体的交互经验存到一个缓冲区里训练时随机采样一批数据。这个做法打破了数据之间的时间相关性让训练更接近独立同分布假设同时提高了样本利用率。第二个是目标网络用一个单独的网络来计算TD目标每隔一段时间才同步一次参数。这避免了“自己追自己”导致的不稳定问题。DQN的损失函数是L(θ) E[(r γ max Q(s,a;θ⁻) - Q(s,a;θ))²]其中θ是当前网络的参数θ⁻是目标网络的参数。这个损失函数看起来就是均方误差但实际实现时有几个坑。首先是奖励的尺度问题如果奖励值很大梯度也会很大容易导致训练发散。常见的做法是对奖励做裁剪比如把所有正奖励截断为1负奖励截断为-1。其次是探索策略DQN通常用ε-greedyε从1逐渐衰减到0.1或0.01。衰减速度很关键太快会导致探索不足太慢则收敛慢。我在复现DQN的时候还遇到过一个经典问题Q值被高估。这是因为max操作会倾向于选择被高估的动作导致估计值越来越大。后来提出的Double DQN通过解耦动作选择和动作评估来解决这个问题——用当前网络选动作用目标网络算价值。实测下来Double DQN确实更稳定尤其是在训练后期。2.3 策略梯度与Actor-Critic直接优化策略的思路基于价值的方法有个局限它们学的是价值函数策略是隐式地从价值函数导出的比如选Q值最大的动作。但在连续动作空间里这个“选最大”的操作就没法直接做了因为动作是连续的不可能枚举所有动作。这时候就需要基于策略的方法。策略梯度方法直接参数化策略π(a|s;θ)然后通过梯度上升来最大化期望回报。最基础的REINFORCE算法用蒙特卡洛采样来估计梯度∇J(θ) ≈ E[∇log π(a|s;θ) * G_t]其中G_t是从时间t开始的累积回报。这个公式的直觉是如果某个动作带来的回报高就增加它被选中的概率反之则降低。但REINFORCE的方差非常大因为G_t本身就是一个随机变量而且它包含了整个轨迹的随机性。为了降低方差就引入了Actor-Critic架构。Actor是策略网络负责选动作Critic是价值网络负责评估动作的好坏。Critic提供的价值估计代替了蒙特卡洛回报大大降低了方差。A2CAdvantage Actor-Critic是其中的经典代表它用优势函数A(s,a) Q(s,a) - V(s)来代替Q值进一步降低了方差。PPOProximal Policy Optimization是目前最流行的策略梯度方法之一它的核心思想是限制每次策略更新的幅度避免更新太大导致性能崩溃。PPO的损失函数里有一个裁剪项L min(ratio * A, clip(ratio, 1-ε, 1ε) * A)其中ratio是新旧策略的概率比。这个裁剪操作让策略更新保持在一个信任域内实测下来PPO的稳定性和最终性能都很不错这也是为什么它成为了OpenAI等机构做强化学习实验的默认选择。3. 实操过程从环境搭建到算法调参3.1 环境选择与基础框架搭建动手实现Model Free算法之前选一个合适的实验环境很重要。对于入门来说OpenAI Gym现在叫Gymnasium是最常用的选择它提供了大量标准化的环境接口从简单的CartPole到复杂的Atari游戏都有。我建议从CartPole-v1开始这个环境状态空间是4维连续值动作空间是2个离散动作训练几分钟就能看到效果非常适合验证算法实现是否正确。安装Gymnasium很简单pip install gymnasium pip install gymnasium[classic-control]然后就可以创建环境并查看基本信息import gymnasium as gym env gym.make(CartPole-v1) obs, info env.reset() print(fObservation space: {env.observation_space}) print(fAction space: {env.action_space})对于表格型方法需要把连续状态离散化。CartPole的4个状态变量分别是小车位置、小车速度、杆子角度、杆子角速度。每个变量可以分成10到20个区间这样总状态数就是10^4到20^4对于Q表来说还是可以接受的。离散化的边界需要根据环境的实际取值范围来定比如小车位置通常在-2.4到2.4之间角度在-0.2到0.2弧度之间。注意离散化的粒度是个权衡。太粗会导致状态区分度不够学不到精细策略太细会导致Q表太大而且很多状态根本访问不到。我一般先用中等粒度跑通流程再根据效果调整。3.2 Q-Learning的完整实现与调参下面是一个完整的Q-Learning实现我加了详细注释import numpy as np import gymnasium as gym # 状态离散化函数 def discretize_state(state, bins): return tuple(np.digitize(s, b) for s, b in zip(state, bins)) # 初始化环境 env gym.make(CartPole-v1) n_bins [10, 10, 10, 10] # 每个维度的区间数 bins [np.linspace(-2.4, 2.4, n_bins[0]), np.linspace(-3.0, 3.0, n_bins[1]), np.linspace(-0.2, 0.2, n_bins[2]), np.linspace(-3.0, 3.0, n_bins[3])] # 初始化Q表 q_table np.zeros([n1 for n in n_bins] [env.action_space.n]) # 超参数 alpha 0.1 # 学习率 gamma 0.99 # 折扣因子 epsilon 1.0 # 初始探索率 epsilon_min 0.01 epsilon_decay 0.995 n_episodes 5000 for episode in range(n_episodes): state, _ env.reset() state_disc discretize_state(state, bins) done False total_reward 0 while not done: # ε-greedy策略 if np.random.random() epsilon: action env.action_space.sample() else: action np.argmax(q_table[state_disc]) # 执行动作 next_state, reward, terminated, truncated, _ env.step(action) done terminated or truncated next_state_disc discretize_state(next_state, bins) # Q-Learning更新 best_next_q np.max(q_table[next_state_disc]) td_target reward gamma * best_next_q * (not done) td_error td_target - q_table[state_disc][action] q_table[state_disc][action] alpha * td_error state_disc next_state_disc total_reward reward # 衰减探索率 epsilon max(epsilon_min, epsilon * epsilon_decay) if (episode 1) % 500 0: print(fEpisode {episode1}, Total Reward: {total_reward}, Epsilon: {epsilon:.3f})这个实现里有几个调参经验值得分享。学习率α我一般从0.1开始试如果训练曲线震荡厉害就降到0.05或0.01。折扣因子γ对于CartPole这种需要长期平衡的任务0.99是个不错的起点。ε的衰减速度很关键衰减太快比如0.99会导致探索不足衰减太慢比如0.999则前期浪费太多时间在随机探索上。我实测0.995左右比较平衡。还有一个容易忽略的点是奖励设计。CartPole原始奖励是每存活一步给1分这个设计其实挺好的不需要额外修改。但如果你自己定义环境奖励设计会极大影响学习效果。奖励太稀疏会导致学不到东西奖励太密集又可能让智能体学会“刷分”而不是完成真正目标。3.3 DQN的实现要点与训练技巧从Q-Learning过渡到DQN最大的变化是用神经网络代替Q表。下面是一个精简版的DQN实现框架import torch import torch.nn as nn import torch.optim as optim import random from collections import deque class QNetwork(nn.Module): def __init__(self, state_dim, action_dim): super().__init__() self.fc nn.Sequential( nn.Linear(state_dim, 128), nn.ReLU(), nn.Linear(128, 128), nn.ReLU(), nn.Linear(128, action_dim) ) def forward(self, x): return self.fc(x) class ReplayBuffer: def __init__(self, capacity10000): self.buffer deque(maxlencapacity) def push(self, state, action, reward, next_state, done): self.buffer.append((state, action, reward, next_state, done)) def sample(self, batch_size): batch random.sample(self.buffer, batch_size) states, actions, rewards, next_states, dones zip(*batch) return (torch.FloatTensor(states), torch.LongTensor(actions), torch.FloatTensor(rewards), torch.FloatTensor(next_states), torch.FloatTensor(dones)) def __len__(self): return len(self.buffer)训练循环里有两个网络在线网络和目标网络。在线网络每步都更新目标网络每隔C步才从在线网络复制参数。C一般取100到1000。经验回放的容量一般取10000到100000太小会导致数据相关性太强太大则旧数据可能过时。实操心得DQN训练初期loss可能会很大这是正常的因为Q值初始化是随机的。但如果loss持续增大不收敛大概率是学习率太大了。我一般从1e-3开始如果不行就降到1e-4。另外梯度裁剪也是个好习惯把梯度范数限制在10以内可以防止梯度爆炸。还有一个细节是奖励裁剪。Atari游戏里通常把奖励裁剪到[-1, 1]因为不同游戏的分数尺度差异很大。对于CartPole这种奖励本身就是1的任务不需要额外裁剪。但如果你发现Q值增长过快可以试试把奖励乘以一个小于1的系数。3.4 策略梯度方法的实操注意事项策略梯度方法和基于价值的方法在实现上有很大不同。首先是输出层策略网络输出的是动作概率分布所以最后一层要用softmax离散动作或者输出均值和方差连续动作。其次是损失函数策略梯度的损失是负的期望回报乘以log概率def compute_policy_loss(log_probs, returns): return -(log_probs * returns).mean()这里returns需要做标准化处理减去均值除以标准差这样可以降低方差。另外策略梯度方法通常需要跑完整个轨迹才能更新一次所以数据利用率比较低。Actor-Critic通过引入价值函数来实现在线更新但引入了新的超参数需要调。PPO的实现相对复杂一些核心是要计算新旧策略的概率比然后做裁剪。我建议先用A2C跑通流程理解了Actor-Critic的基本框架后再上PPO。PPO的超参数里裁剪系数ε一般取0.1或0.2GAE的λ一般取0.95这些值在大多数任务上表现都不错。4. 常见问题与排查技巧实录4.1 训练不收敛的排查思路训练不收敛是Model Free方法最常见的问题原因可能有很多。我整理了一个排查清单按优先级排序问题现象可能原因排查方法解决方案奖励持续为负或很低探索不足检查ε衰减速度减慢ε衰减或提高ε最小值奖励震荡剧烈学习率太大打印loss曲线降低学习率加梯度裁剪Q值爆炸增长奖励尺度太大打印Q值范围裁剪奖励降低学习率训练后期性能下降过拟合或灾难性遗忘对比训练和测试表现增加经验回放容量降低更新频率完全学不到东西状态表示有问题检查状态离散化或归一化调整离散化粒度做状态归一化我遇到最多的情况是探索不足。特别是用DQN的时候如果ε衰减太快智能体很快就陷入局部最优。一个实用的技巧是让ε衰减到0.1就停住保持一定的持续探索。另一个技巧是用Boltzmann探索代替ε-greedy根据Q值的相对大小来分配探索概率这样在Q值差异大的状态下更倾向于选好的动作差异小的状态下探索更均匀。4.2 样本效率低下的优化策略Model Free方法普遍样本效率不高但有一些技巧可以改善。首先是经验回放的优先级采样给TD误差大的样本更高的采样概率这样能更快地学到“意外”的经验。其次是n-step回报用多步的累积回报来代替单步TD目标可以在偏差和方差之间取得更好的平衡。n一般取3到5效果比较好。另一个重要技巧是奖励塑形。如果环境给的奖励很稀疏可以人为设计一些中间奖励来引导学习。比如在迷宫任务里可以用到目标的距离作为额外的奖励信号。但要注意奖励塑形不能改变最优策略否则智能体会学会“刷”中间奖励而不是完成真正目标。Potential-based reward shaping是一个理论上保证不改变最优策略的塑形方法值得了解。注意奖励塑形是把双刃剑。我见过不少项目因为奖励设计不当导致智能体学会了各种“作弊”行为。比如一个机器人抓取任务如果给“靠近物体”奖励机器人可能会学会悬停在物体上方而不去抓。设计奖励时一定要想清楚这个奖励会不会被某种非预期行为最大化4.3 从仿真到现实的迁移问题如果你打算把Model Free方法用到真实系统上仿真到现实的迁移是个绕不开的坎。仿真环境里训练好的策略直接放到真实环境往往性能大幅下降。原因有很多仿真和现实的动力学差异、传感器噪声、执行器延迟等等。Domain Randomization是常用的解决方案在仿真训练时随机化各种环境参数质量、摩擦系数、延迟等让策略学会适应各种条件。这样训练出来的策略对真实环境的差异更鲁棒。另一个思路是System Identification先用真实数据估计仿真模型的参数让仿真更接近现实然后再在仿真里训练。但说实话完全零样本迁移到现实仍然很难。更务实的做法是在真实环境里做少量微调用仿真预训练的策略作为起点然后在真实环境里用On-Policy方法比如PPO做在线微调。这样既利用了仿真的高效率又保证了最终性能。4.4 超参数调优的实用建议Model Free方法的超参数很多全部调一遍不现实。我的经验是优先调这几个学习率、折扣因子、探索率衰减。学习率的影响最大通常先固定其他参数把学习率调到一个让训练曲线比较平滑的值。折扣因子取决于任务的时间尺度如果任务需要长期规划γ要接近1如果任务比较短视γ可以小一些。批量大小对DQN的影响也很大。太小的批量比如32会导致梯度噪声大训练不稳定太大的批量比如1024虽然稳定但样本效率低。我一般用64或128。目标网络的更新频率C也很关键太小会导致目标不稳定太大则学习太慢。C取1000左右是个不错的起点。还有一个容易被忽略的超参数是网络结构。对于简单的任务两层128维的全连接网络就够了。层数太多反而容易过拟合而且训练更慢。激活函数用ReLU就行没必要上更复杂的。输出层对于离散动作直接用线性输出不需要softmax因为Q值不需要归一化。5. 进阶方向与个人学习路径建议学完基础的Model Free方法后下一步可以往几个方向深入。第一个方向是更先进的算法比如SACSoft Actor-Critic在连续控制任务上表现很好TD3解决了DDPG的Q值高估问题。第二个方向是分布式强化学习比如IMPALA、APEX这些架构通过并行采样和分布式训练大幅提高样本效率。第三个方向是结合模型的方法比如MBPOModel-Based Policy Optimization用学到的环境模型来生成虚拟经验样本效率比纯Model Free方法高一个数量级。从我个人的学习路径来看最有价值的做法是“实现一遍再调一遍”。看论文和博客只能建立概念框架真正动手实现才会遇到各种细节问题。我建议每个算法至少完整实现一次然后在至少两个不同环境上跑通。第一个环境用来验证实现正确性第二个环境用来观察算法的泛化能力。调参的过程比看十篇论文学到的都多。另外不要一上来就追求SOTA。把Q-Learning和REINFORCE这两个最基础的算法吃透理解它们各自的优缺点和适用场景比囫囵吞枣地跑通十个新算法更有价值。我见过不少初学者直接上PPO结果遇到问题完全不知道从哪排查因为对底层原理不理解。基础打牢了后面学新算法就是换个损失函数的事。最后分享一个我常用的调试技巧把训练过程中的关键指标可视化出来包括每回合奖励、Q值均值、策略熵、TD误差等。很多问题从曲线上一眼就能看出来比盯着代码找bug高效得多。TensorBoard或者Weights Biases都是很好的工具花半小时配置一下后面能省几十个小时的排查时间。