强化学习从零搭建:Q-learning到PPO算法原理与实战调参指南
1. 从零搭建强化学习知识框架为什么我选择这条路线搞强化学习的人大概都有过这种体验打开一篇论文满屏的贝尔曼方程、策略梯度、时序差分每个字都认识连在一起就不知道在说什么。我最初接触RL的时候也是这样Q-learning、REINFORCE、DQN这些名词在脑子里搅成一锅粥完全理不清它们之间的关系。后来我花了大半年时间把强化学习的核心算法从头到尾手推了一遍又用Python逐个复现才算真正把这些东西串起来了。这篇笔记就是把我踩过的坑、想通的逻辑、以及实际编码中那些文档里不会写的细节完整地整理出来。强化学习本质上解决的是一个序贯决策问题智能体在环境中不断采取动作环境给出反馈智能体根据反馈调整自己的行为最终学会一套能最大化长期收益的策略。这个定义听起来简单但里面有几个关键词值得拆开看。“序贯”意味着决策不是一次性的当前的选择会影响未来的状态和收益“反馈”通常以奖励的形式出现但奖励往往是稀疏的、延迟的“策略”则是智能体从状态到动作的映射也是我们最终要学的东西。这套框架能解决的问题范围其实非常广。小到游戏AI打砖块大到机器人控制、交通信号灯调度、甚至量化交易策略的优化底层逻辑都是相通的。适合阅读这篇笔记的人我大致分三类第一类是有机器学习基础、想系统入门强化学习的开发者第二类是做控制、运筹、量化等方向、需要把RL用到实际项目里的工程师第三类是对RL感兴趣但被数学公式劝退、想先建立直觉再深入理论的学习者。不管你是哪一类我都建议你跟着文章里的代码动手跑一遍光看是看不会的。提示这篇笔记假设你有基本的Python编程能力和一点线性代数基础。如果连梯度下降是什么都不太清楚建议先补一下深度学习的基础知识再回来。2. 核心概念拆解状态、动作、奖励与策略的底层逻辑2.1 马尔可夫决策过程强化学习的数学骨架所有强化学习算法都建立在马尔可夫决策过程MDP之上。MDP用一个五元组来描述状态空间S、动作空间A、转移概率P、奖励函数R、折扣因子γ。这个框架的核心假设是马尔可夫性——下一个状态只取决于当前状态和当前动作与更早的历史无关。这个假设为什么重要因为它把复杂的序贯决策问题简化成了一个可以用动态规划求解的问题。如果没有马尔可夫性智能体就需要记住整个历史才能做出最优决策计算量会爆炸。实际项目中我们遇到的环境往往不严格满足马尔可夫性这时候就需要做状态设计把足够多的历史信息编码进状态里让它在近似意义上满足马尔可夫性。折扣因子γ是我见过最多人困惑的参数。它的取值范围是0到1衡量的是未来奖励相对于当前奖励的重要性。γ接近0时智能体变得短视只关心眼前的奖励γ接近1时智能体变得有远见愿意为了长期收益牺牲短期利益。我通常的做法是如果任务有明显的阶段性目标比如下棋最终要赢γ设0.95到0.99如果任务更看重即时反馈比如实时控制γ设0.8到0.9。这个参数没有标准答案需要根据具体任务调。2.2 价值函数与动作价值函数Q-learning的理论根基状态价值函数V(s)衡量的是从状态s出发按照某个策略走下去能获得的期望累积奖励。动作价值函数Q(s,a)则更进一步衡量的是在状态s下先采取动作a之后再按照某个策略走下去的期望累积奖励。两者的关系很直观V(s)就是Q(s,a)在策略π下对动作a的期望。Q-learning的核心思想就是直接学习最优动作价值函数Q*。它用一个表格或者神经网络来存储每个状态-动作对的Q值然后通过时序差分更新不断逼近真实值。更新公式是这样的Q(s,a) ← Q(s,a) α[r γ·max Q(s,a) - Q(s,a)]这个公式看起来简单但每一部分都有讲究。α是学习率控制每次更新幅度r是即时奖励γ·max Q(s,a)是对未来最优价值的估计中括号里的整体是TD误差衡量的是当前估计和实际体验之间的差距。我刚开始学的时候总觉得这个公式是凭空冒出来的后来自己推了一遍贝尔曼最优方程才明白它其实就是贝尔曼方程的一个随机近似版本。2.3 策略梯度与REINFORCE另一条完全不同的路Q-learning走的是值函数逼近的路线先学Q值再导出策略。REINFORCE走的是策略梯度的路线直接参数化策略π(a|s;θ)然后通过梯度上升来优化策略参数。这两条路线的哲学完全不同前者是“先评估再行动”后者是“边行动边调整”。REINFORCE的更新公式是θ ← θ α·∇log π(a|s;θ)·G其中G是从当前时刻到回合结束的累积奖励。这个公式的直觉是如果某个动作带来的累积奖励高就增大它被选中的概率如果奖励低就减小概率。听起来很合理但实际用起来有个大问题——方差极高。因为G是整个回合的累积奖励不同回合之间波动很大导致梯度估计非常不稳定。我第一次跑REINFORCE的时候在CartPole上跑了上千个回合都不收敛后来加了基线baseline才稳定下来。基线的做法是从G中减去一个基准值通常是状态价值函数V(s)这样梯度就变成了∇log π(a|s;θ)·(G - V(s))。这个改动不改变梯度的期望但能显著降低方差。这个技巧后来演变成了优势函数的概念也是A2C、PPO等算法的核心。2.4 值函数与策略梯度的融合Actor-Critic架构Actor-Critic把值函数和策略梯度结合在了一起。Actor是策略网络负责选择动作Critic是价值网络负责评估Actor选的动作好不好。Actor根据Critic的评估来更新策略Critic根据实际奖励来更新价值估计。这种架构既保留了策略梯度直接优化策略的优点又利用值函数降低了方差。我在实际项目中发现Actor-Critic的调参比纯Q-learning或纯策略梯度都要复杂因为两个网络的学习率需要匹配。如果Critic学得太慢Actor就拿不到准确的反馈如果Critic学得太快又容易过拟合到当前策略上。我的经验是Critic的学习率设成Actor的2到3倍同时用目标网络target network来稳定Critic的训练。3. 核心算法实操从Q-learning到PPO的完整实现3.1 Q-learning表格法最适合入门的第一个算法如果你刚开始学强化学习我强烈建议从表格版Q-learning入手。状态和动作都是离散的、数量有限的情况下用一个二维数组就能存下所有Q值。我当年是在FrozenLake环境上跑通的第一个Q-learning代码不到50行但把整个流程跑通了。import numpy as np import gym env gym.make(FrozenLake-v1, is_slipperyFalse) n_states env.observation_space.n n_actions env.action_space.n Q np.zeros((n_states, n_actions)) alpha 0.1 gamma 0.99 epsilon 1.0 epsilon_decay 0.995 epsilon_min 0.01 n_episodes 5000 for episode in range(n_episodes): state, _ env.reset() done False while not done: if np.random.random() epsilon: action env.action_space.sample() else: action np.argmax(Q[state]) next_state, reward, done, _, _ env.step(action) Q[state, action] alpha * (reward gamma * np.max(Q[next_state]) - Q[state, action]) state next_state epsilon max(epsilon_min, epsilon * epsilon_decay)这段代码里有几个关键点值得展开说。epsilon-greedy策略是探索和利用的平衡以epsilon的概率随机探索以1-epsilon的概率选择当前最优动作。epsilon从1.0逐渐衰减到0.01意味着智能体从完全随机探索逐渐过渡到完全利用。这个衰减速度很关键衰减太快会导致探索不充分衰减太慢会导致收敛太慢。注意FrozenLake的is_slippery参数默认是True意味着智能体采取动作后可能滑到其他方向。新手建议先设成False跑通再改成True增加难度。这个环境虽然简单但把Q-learning的核心逻辑体现得很清楚。3.2 DQN当状态空间大到表格装不下表格版Q-learning的致命缺陷是状态空间必须离散且有限。一旦状态是连续的比如图像、传感器读数表格就无能为力了。DQNDeep Q-Network的思路是用神经网络来近似Q函数输入状态输出每个动作的Q值。DQN相比原始Q-learning有两个关键改进。第一个是经验回放experience replay把智能体的经历(s, a, r, s, done)存进一个缓冲区训练时从中随机采样。这样做打破了样本之间的时间相关性让训练更稳定。第二个是目标网络target network用一个单独的网络来计算TD目标每隔一定步数才从主网络同步一次参数。这样做避免了“自己追自己”的不稳定问题。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.net 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.net(x) class DQNAgent: def __init__(self, state_dim, action_dim): self.q_net QNetwork(state_dim, action_dim) self.target_net QNetwork(state_dim, action_dim) self.target_net.load_state_dict(self.q_net.state_dict()) self.optimizer optim.Adam(self.q_net.parameters(), lr1e-3) self.buffer deque(maxlen10000) self.batch_size 64 self.gamma 0.99 self.epsilon 1.0 self.epsilon_min 0.01 self.epsilon_decay 0.995 self.update_target_every 100 self.step_count 0 def select_action(self, state): if random.random() self.epsilon: return random.randint(0, self.q_net.net[-1].out_features - 1) with torch.no_grad(): return self.q_net(torch.FloatTensor(state)).argmax().item() def store(self, transition): self.buffer.append(transition) def train(self): if len(self.buffer) self.batch_size: return batch random.sample(self.buffer, self.batch_size) states, actions, rewards, next_states, dones zip(*batch) states torch.FloatTensor(np.array(states)) actions torch.LongTensor(actions) rewards torch.FloatTensor(rewards) next_states torch.FloatTensor(np.array(next_states)) dones torch.FloatTensor(dones) q_values self.q_net(states).gather(1, actions.unsqueeze(1)).squeeze() with torch.no_grad(): next_q self.target_net(next_states).max(1)[0] target rewards self.gamma * next_q * (1 - dones) loss nn.MSELoss()(q_values, target) self.optimizer.zero_grad() loss.backward() self.optimizer.step() self.step_count 1 if self.step_count % self.update_target_every 0: self.target_net.load_state_dict(self.q_net.state_dict()) self.epsilon max(self.epsilon_min, self.epsilon * self.epsilon_decay)这段代码里有个细节很多人会忽略计算TD目标时如果当前状态是终止状态done1那么未来价值应该为0所以要用(1-dones)把next_q乘掉。我第一次写的时候忘了这个结果智能体在终止状态附近的行为完全乱套。3.3 REINFORCE与策略梯度直接优化策略的实践REINFORCE的实现比DQN简单但训练起来更不稳定。核心代码就几行class PolicyNetwork(nn.Module): def __init__(self, state_dim, action_dim): super().__init__() self.net nn.Sequential( nn.Linear(state_dim, 128), nn.ReLU(), nn.Linear(128, action_dim), nn.Softmax(dim-1) ) def forward(self, x): return self.net(x) def reinforce_update(policy, optimizer, log_probs, rewards, gamma0.99): returns [] G 0 for r in reversed(rewards): G r gamma * G returns.insert(0, G) returns torch.FloatTensor(returns) returns (returns - returns.mean()) / (returns.std() 1e-8) loss 0 for log_prob, G in zip(log_probs, returns): loss - log_prob * G optimizer.zero_grad() loss.backward() optimizer.step()这里我对returns做了标准化这是降低方差的一个实用技巧。不做标准化的话不同回合的累积奖励差异可能很大导致梯度更新幅度忽大忽小。标准化之后梯度更新更平稳收敛速度明显加快。3.4 PPO目前工业界最常用的策略梯度算法PPOProximal Policy Optimization是目前实际项目中最常用的算法没有之一。它的核心思想是策略更新时不要走太远限制新旧策略之间的差异。具体做法是在目标函数里加一个裁剪项L min(ratio · A, clip(ratio, 1-ε, 1ε) · A)其中ratio是新策略和旧策略在某个动作上的概率比A是优势函数。当ratio偏离1太多时裁剪项会限制梯度的贡献防止策略更新过猛。这个设计让PPO在保持策略梯度方法灵活性的同时获得了接近值函数方法的稳定性。我在机械臂控制项目里用PPO的时候发现优势函数的估计方式对结果影响很大。用GAEGeneralized Advantage Estimation比用简单的TD误差效果好很多尤其是当奖励稀疏的时候。GAE通过一个参数λ在偏差和方差之间做权衡λ接近0时偏差大方差小λ接近1时偏差小方差大。实践中λ设0.95是个不错的起点。4. 训练调参与问题排查那些文档里不会写的事4.1 奖励设计强化学习中最容易被低估的环节我见过太多人把强化学习当成一个纯算法问题觉得只要算法选对了就能跑出好结果。实际上奖励设计才是决定项目成败的关键因素。奖励设计得不好再先进的算法也学不出想要的行为。奖励设计有几个常见陷阱。第一个是奖励稀疏智能体跑了很久才拿到一个奖励信号中间没有任何反馈学习效率极低。解决办法是设计稠密奖励在中间过程中给出引导性信号。比如机械臂抓取任务不要只在抓取成功时给奖励可以在靠近目标、夹爪闭合等中间步骤也给小奖励。第二个陷阱是奖励黑客智能体找到了某种钻空子的方式拿到了高奖励但并没有完成你真正想要的任务。比如赛跑任务中如果奖励是“移动距离”智能体可能学会原地转圈来累积距离。解决办法是仔细检查奖励函数确保它和真实目标一致必要时加入惩罚项。第三个陷阱是奖励尺度不同来源的奖励量级差异太大导致某些奖励主导了学习过程。我的经验是把所有奖励归一化到相近的量级比如都在-1到1之间。4.2 超参数调优学习率、折扣因子与网络结构强化学习的超参数比监督学习敏感得多因为训练数据是随着策略变化而不断变化的不是固定的。以下是我总结的几个关键超参数的经验值超参数常用范围影响调参建议学习率α1e-4 ~ 1e-3太大不收敛太小收敛慢从3e-4开始试折扣因子γ0.95 ~ 0.99影响智能体的远见程度有明确终点的任务用0.99经验回放容量1e4 ~ 1e6太小样本相关性高太大训练慢根据任务复杂度选批量大小32 ~ 256影响梯度估计的稳定性从64开始目标网络更新频率100 ~ 1000步太频繁不稳定太慢学得慢从200步开始GAE参数λ0.9 ~ 0.98偏差方差权衡从0.95开始学习率是最重要的超参数。我通常先用一个较大的学习率快速验证算法是否能跑通然后再降低学习率精细调优。如果训练曲线震荡得很厉害首先考虑降低学习率如果训练曲线太平缓考虑提高学习率。4.3 常见问题速查表问题现象可能原因排查方法解决方案奖励不上升学习率太小、奖励设计有问题打印每回合奖励曲线调大学习率、检查奖励函数奖励震荡剧烈学习率太大、批量太小观察奖励的滑动平均降低学习率、增大批量训练后期性能崩溃过拟合、策略更新过猛对比训练和测试表现加正则化、用PPO的裁剪机制智能体行为单一探索不足、熵太小统计动作分布增大熵系数、提高epsilon值函数估计偏差大网络容量不够、训练不充分对比预测值和实际回报增大网络、增加训练步数训练速度极慢环境交互是瓶颈计时各环节耗时并行化环境、用向量化环境4.4 实操心得那些我踩过的坑第一个坑是环境随机性。我早期做实验的时候发现同样的代码跑两次结果差异很大一度以为是算法不稳定。后来才发现是环境本身的随机性导致的。解决办法是固定随机种子并且用多个种子跑取平均。如果条件允许至少跑5个种子报告均值和标准差。第二个坑是观测归一化。连续状态空间的任务中不同维度的观测值范围可能差异很大。如果不做归一化神经网络会被大数值的维度主导小数值的维度几乎不起作用。我通常用RunningMeanStd来动态计算观测的均值和标准差训练过程中持续更新。第三个坑是梯度裁剪。策略梯度方法中梯度爆炸是常见问题尤其是RNN结构或者长回合任务。PyTorch里用torch.nn.utils.clip_grad_norm_把梯度范数限制在0.5到1.0之间能显著提升训练稳定性。这个技巧在PPO里几乎是标配。第四个坑是评估频率。训练过程中需要定期评估策略的表现但评估太频繁会拖慢训练太少又可能错过最佳模型。我的做法是每训练N个回合评估一次保存表现最好的模型而不是最后一个模型。因为强化学习训练后期性能崩溃的情况太常见了。5. 从算法到落地强化学习的应用场景与扩展方向5.1 连续控制与离散决策不同场景的算法选型强化学习的应用场景大致分两类离散动作空间和连续动作空间。离散场景包括游戏AI、推荐系统、交通信号灯控制等动作是有限个选项。连续场景包括机器人控制、自动驾驶、无人机导航等动作是连续的数值。离散场景下DQN及其变体Double DQN、Dueling DQN、Prioritized Replay是首选。连续场景下DDPG、TD3、SAC这些算法更合适。PPO则两者都能用这也是它成为工业界首选的原因之一。我在交通信号灯控制的项目里用的是DQN因为动作空间是离散的每个相位的绿灯时长有几个档位可选。但在机械臂抓取的项目里DQN完全没法用因为关节角度是连续的必须用DDPG或PPO。选型的时候一定要先看清楚动作空间的性质这是第一步。5.2 离线强化学习当交互成本太高时怎么办很多实际场景中让智能体在真实环境中试错成本太高甚至不可能。比如医疗决策、金融交易你不可能让一个未训练好的智能体去实际操作。这时候就需要离线强化学习Offline RL也叫批量强化学习。离线RL的核心挑战是分布偏移训练数据是由某个行为策略收集的但学到的策略可能跑到数据分布之外导致价值估计严重偏差。CQL、IQL这些算法通过保守的价值估计来缓解这个问题。IQL的思路尤其巧妙它不直接估计Q值而是通过期望回归来学习价值函数避免了查询分布外动作的问题。5.3 与大模型结合强化学习的新前沿最近一年强化学习和大型语言模型的结合成了热门方向。RLHF基于人类反馈的强化学习就是用PPO来微调语言模型让模型的输出更符合人类偏好。这个方向的核心思路是用一个奖励模型来模拟人类的偏好判断然后用PPO来优化语言模型的生成策略。如果你对这方面感兴趣我建议先把手头的PPO吃透因为RLHF的底层就是PPO。区别在于状态是对话历史动作是生成的token奖励来自奖励模型。理解了PPO的裁剪机制和优势函数再看RLHF的代码就不会觉得陌生了。5.4 学习路线建议从入门到实战的路径如果你刚开始学强化学习我建议按这个顺序走先跑通表格版Q-learning理解时序差分更新的逻辑然后学DQN理解神经网络如何近似Q函数接着学REINFORCE和Actor-Critic理解策略梯度这条路线最后学PPO这是目前最实用的算法。每学一个算法都要自己动手写一遍代码不要直接抄库。框架方面入门用Gymnasium原OpenAI Gym就够了环境丰富、接口简单。进阶可以用PettingZoo做多智能体用MuJoCo做连续控制。如果要做实际项目Stable-Baselines3提供了高质量的算法实现可以直接调用但建议先自己实现一遍再去看它的源码收获会大很多。最后分享一个我自己的习惯每次实验都记录完整的配置和结果包括随机种子、超参数、训练曲线、评估指标。强化学习的实验复现性很差不记录的话过两周自己都不记得当时是怎么跑出来的。我用的是Weights Biases做实验跟踪免费版对个人项目完全够用。这个习惯看起来麻烦但长期来看能省下大量重复实验的时间。