☰
强化学习从零搭建:Q-learning到PPO算法原理与实战调参指南
2026/9/30 9:12:45 网站建设 项目流程

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_slippery=False) 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的致命缺陷是状态空间必须离散且有限。一旦状态是连续的(比如图像、传感器读数),表格就无能为力了。DQN(Deep 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(), lr=1e-3) self.buffer = deque(maxlen=10000) 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目标时,如果当前状态是终止状态(done=1),那么未来价值应该为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, gamma=0.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:目前工业界最常用的策略梯度算法

PPO(Proximal Policy Optimization)是目前实际项目中最常用的算法,没有之一。它的核心思想是:策略更新时不要走太远,限制新旧策略之间的差异。具体做法是在目标函数里加一个裁剪项:

L = min(ratio · A, clip(ratio, 1-ε, 1+ε) · A)

其中ratio是新策略和旧策略在某个动作上的概率比,A是优势函数。当ratio偏离1太多时,裁剪项会限制梯度的贡献,防止策略更新过猛。这个设计让PPO在保持策略梯度方法灵活性的同时,获得了接近值函数方法的稳定性。

我在机械臂控制项目里用PPO的时候,发现优势函数的估计方式对结果影响很大。用GAE(Generalized 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做实验跟踪,免费版对个人项目完全够用。这个习惯看起来麻烦,但长期来看能省下大量重复实验的时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询