1. 策略的本质:从“小马过河”说起如果只用一句话点破强化学习的灵魂,我会说是“策略”。智能体怎么决策、面对某个状态选哪个动作、走一步看几步、是保守还是激进,统统归结为策略。很多教程习惯先讲奖励、价值函数、Q表,但回头你会发现,那些东西最终都是为了一件事服务:找到更好的策略。所以第一篇笔记,我决定先把策略拎出来讲透。先抛一个生活化的类比。你让一个新手做菜,他面对一筐食材会手足无措:先放油还是先放盐?火开大还是开小?他需要一份“菜谱”来告诉他,在“锅热了”这个状态下,应该“倒油”而不是“倒水”。菜谱,就是策略。强化学习里的智能体,本质上就是在一遍遍试错中,自己写出一份越来越好的菜谱——不是靠人教的,而是靠吃出来的经验。策略的定义其实非常朴素:策略是从环境状态到动作选择的映射,用数学符号写就是 π(a|s),读作“在状态 s 下选择动作 a 的概率(或确定值)”。它回答的问题是“我现在该干什么”。这里的“现在”不是某一刻,而是每一个时间步。整个过程就是在和环境不断交互:观察状态 s_t,按策略选动作 a_t,环境返回奖励 r_t 和下一个状态 s_{t+1},然后周而复始。那策略为什么值得单独拿出来深入研究?因为它是整个强化学习问题的求解目标。价值函数也好、Q函数也好,它们是辅助工具,用来“评估”策略的。真正的“决策者”就是策略本身。业务上真正上线部署的也是策略:自动驾驶的车该往左打多少方向盘、推荐系统该推哪件商品、游戏 AI 该释放哪个技能,全部是策略在做最终拍板。把策略搞明白了,强化学习的一半就算入了门。还有一点新手容易忽略:策略形式的选择直接决定了你能用哪类算法。你选了确定性策略,网络输出就是一个具体的动作值;你选了随机性策略,网络输出就是动作的概率分布。前者适合动作空间连续的机器人控制,后者在博弈类场景里天然具备探索能力。后面会详细说,这里先记住一个结论:策略是强化学习的目标,也是算法分类的一条主线。2. 策略的两副面孔:确定性策略与随机性策略2.1 确定性与随机性的区别策略表面看就是“映射”,但映射有两种风格。确定性策略记作 a = μ(s),同一个状态永远输出同一个动作,没有花招,输入相同输出必相同。随机性策略记作 a ~ π(a|s),给定状态后输出的是一个概率分布,从这个分布里采样得到动作;同样的状态,两次采样可能得到不同的动作。这两种风格没有绝对的优劣,但适用场景差异很大。拿围棋举例,职业棋手面对同一个棋局,落子其实是带概率的——他会考虑几种候选着法,而不是每次只认死一个点。这就是随机性策略的价值:它保留了不确定性,给了智能体探索的空间。反过来看机器人关节控制,你让机械臂以 0.5 的概率往左、0.5 的概率往右,那机械臂会原地抽搐,生产线上根本没法用。这种场景就要确定性策略。从数学表达上看,随机性策略的概率分布可以是离散的,比如分类问题常见的 softmax 输出;也可以是连续的,比如高斯分布,用均值 μ(s) 和方差 σ(s) 刻画一个概率密度。确定性策略大多直接用一个回归网络输出动作向量,简单直接。2.2 随机策略中的探索与利用随机性策略还有一个隐藏福利:它把“探索”这件事内建在了决策过程里。经典强化学习里有著名的探索-利用困境:你是用当前已知最好的动作去“利用”,还是尝试一些次优动作去“探索”,赌它们未来会带来更高回报?如果策略是确定性的,你得额外加 ε-greedy 之类的扰动;如果策略天生带熵,比如输出一个概率分布,那么每次采样天然就有可能选到非最优动作,探索就自动发生了。但随机策略的探索也不是白来的。它的问题在于方差:如果输出分布的熵太大,动作太分散,训练过程会像喝醉了酒走路——明明知道方向,每一步都在踉跄。所以后来的算法普遍要控制熵的权重,比如在损失函数里加一个熵正则项,让策略既保持一定的随机性,又不至于散得太离谱。这个熵正则的系数,我建议新手从 0.01 起步,调参时观察训练曲线,如果策略太早就收敛到确定性分布,说明探索不足,可以适当调大;如果一直随机乱试不收敛,就调小。2.3 策略的参数化:从表格到神经网络早期的强化学习用表格存策略:状态有多少个,动作有多少个,表格就有多大。比如 10 个状态 5 个动作,就是一个 10 行 5 列的矩阵,每格存一个概率。这种方法在小规模环境中很直观,但一碰到连续状态就崩了——自动驾驶里车速、位置、角度是连续值,状态空间接近无穷大,表格根本开不出来。所以现代强化学习几乎都走参数化路线:用一个参数为 θ 的神经网络来表示策略,记作 π_θ(a|s)。网络的输入是状态特征,输出是动作或者动作分布。这意味着“策略学习”问题变成了“参数优化”问题——我们真正要操心的是怎么调这组 θ。这也是所有深度强化学习算法的共性问题:目标函数怎么设计、梯度怎么算、网络怎么更新。参数化还有一层好处:泛化能力。表格策略没见过某个状态就彻底抓瞎,神经网络对相近状态能给出相近的输出,有一定的外推能力。当然这既是优点也是隐患,神经网络的泛化有时候会“自信过头”,对没见过的状态输出一个离谱的动作,这个问题到后面实际部署时要想办法兜底。3. 策略的标尺:用价值函数给策略打分3.1 状态价值与动作价值策略本身没有高下,得靠一把尺子量。强化学习里的尺子就是价值函数。两个最核心的概念:状态价值 V^π(s):从状态 s 出发,按照策略 π 行动,未来能拿到的累计折扣奖励的期望。动作价值 Q^π(s, a):在状态 s 先执行动作 a,之后按照策略 π 继续行动,未来累计奖励的期望。区别就在那个“先”。V 问的是“我现在这个局面值多少钱”,Q 问的是“我现在做这个动作值多少钱”。一个类比:你站在十字路口,V 是“站在这个路口本身的价值”,Q 是“选择左转这个具体动作的价值”。显然 Q 比 V 多了一维信息,也因此更难算。价值函数和策略的关系像一个嵌套循环。给定策略 π,我们能用它去估 V 和 Q;反过来,有了准确的 V 和 Q,我又能改进策略——看到哪个动作的 Q 值高,就把它的概率调大。这个循环就是强化学习的核心引擎。3.2 策略评估:怎么算出当前策略的价值给定一个策略 π,算出它的 V^π 和 Q^π,这个操作叫策略评估。原理上就是解贝尔曼方程,方程的核心思想是递归:当前状态的价值 = 即时奖励 + 折扣后的下一状态价值。数学上写成:V^π(s) = Σ_a π(a|s) Σ_{s′,r} p(s′,r|s,a) [r + γV^π(s′)]这个式子看着吓人,拆开就是:按概率对当前状态下所有可能动作求期望,再对环境的转移概率求期望。真实环境里你往往不知道 p(s′, r|s, a),所以没人真的去解这个期望,而是靠采样。让智能体按策略 π 跑很多轮,把每一轮得到的回报记下来,平均一下,这就是蒙特卡洛策略评估。它简单、无偏,但是方差大,需要跑很多轮才稳定。另一种做法是时序差分(TD),它用单步的奖励加上对下一状态的估计,来更新当前状态的价值。一个经典的更新式是:V(s) ← V(s) + α [r + γV(s′) − V(s)]方括号里那项叫 TD 误差。它用 r + γV(s′) 作为对真实价值的“有偏估计”,替代完整回报。这种做法方差小、也能在线学习,所以被绝大多数现代算法采用。代价是引入了偏差,尤其是在函数近似(比如用神经网络做价值函数)的情况下,TD 容易出现估计偏高或偏低的问题。3.3 策略改进:从“打分”到“换动作”估完价值之后怎么办?答案自然是改进策略。当一个动作的价值比当前策略下所有动作的平均价值都高,那就应该提高选它的概率。贪心策略改进就是最直接的思路:状态 s 下,选 Q(s, a) 最大的那个动作。这个逻辑好懂,但它直接把随机策略变成确定性策略,在探索和博弈场景里不一定好用。更温和的做法是软策略迭代:用 softmax 把 Q 值变成动作概率,温度参数 T 控制“软”的程度。T 大,各动作概率接近,探索多;T 小,策略趋于贪心,稳定但探索少。这就是后续很多算法里 softmax 策略的雏形。现在很多强化学习框架里,你看到的“行动选择概率”基本就是从这派生的。这个评估-改进循环持续迭代,理论上会一步一步逼近最优策略 π*。整个过程就是广义策略迭代,几乎所有基于价值的方法(Q-learning、DQN 那一脉)都默默遵循这个框架。理解了这条主线,后面看 DQN、PPO 的论文,你会发现它们都只是这个循环的某个环节做了工程化改良。4. 策略的优化器:策略梯度方法的原理与实战4.1 为什么直接调策略会有困难价值函数方法走过的路线是:先学价值函数,再从价值函数里提取策略。策略梯度方法跳过了中间步骤,直接对策略参数 θ 求梯度,往让期望回报增大的方向更新。直觉上这更直接,但是有一个坎:期望回报 J(θ) 是轨迹上的积分,可轨迹的生成过程里,动作是从策略里采样得到的,而采样这个操作是不可导的。你不能直接对“随机采样出来的动作”求梯度,这是策略梯度方法最初让人困惑的地方。解决办法很漂亮:把对数似然这个技巧用起来。对期望回报的梯度,可以写成对每一步的“对数策略概率 × 回报”求期望。最终形式就是策略梯度定理:∇J(θ) = E[∇_θ log π_θ(a_t|s_t) · R(τ)]这个公式的意义是:回报高的轨迹,我们要增大其中所有动作的概率;回报低的轨迹,要减小其中的动作概率。这跟人类学习很像——考得好的那次,把当时的解题策略记住;考砸了,就把当时的选择忘掉。4.2 REINFORCE 算法的完整拆解最朴素的策略梯度算法是 REINFORCE,也叫蒙特卡洛策略梯度。它的流程我拆成了四个步骤:用当前策略 π_θ 与环境交互,采集一整条轨迹,记录状态、动作、奖励。计算这条轨迹的总回报 R(τ),可以带折扣。对轨迹中的每个时间步,计算梯度项 ∇_θ log π_θ(a_t|s_t) × R(τ)。用梯度上升更新参数 θ。写个简化伪代码,大家感受一下全貌:for episode in range(num_episodes): trajectory = [] state = env.reset() done = False while not done: action = policy.sample(state) # 从当前策略采样动作 next_state, reward, done, _ = env.step(action) trajectory.append((state, action, reward)) state = next_state # 计算折扣回报 returns = [] G = 0 for _, _, reward in reversed(trajectory): G = reward + gamma * G returns.append(G) returns = list(reversed(returns)) # 更新策略参数 policy.zero_grad() for (state, action), G in zip(trajectory, returns): log_prob = policy.log_prob(state, action) loss = -log_prob * G # 梯度上升转梯度下降取负号 loss.backward() policy.step()这段代码有一个很值得注意的地方:梯度是每个时间步单独算,但 PyTorch 默认会把梯度累积,所以更新完要 zero_grad;同时 loss 取负号是因为优化器默认做梯度下降。REINFORCE 的实现很简单,但方差极大,经常会出现“训练一整个 epoch,loss 在跳,策略就是不提升”的现象。那个时候我踩坑踩得非常深,后来才知道核心问题是高方差。4.3 方差控制的核心手段:基线技术与 Actor-CriticREINFORCE 的方差大,根因在于 R(τ) 本身波动很大:同样一个策略,这次拿 100 分,下次可能只有 -50 分,直接把梯度项推得忽正忽负。解决思路是引入基线 b(s),把梯度改成:∇J(θ) = E[∇_θ log π_θ(a_t|s_t) · (R(τ) − b(s_t))]这个减法不会改变梯度的期望(因为 E[∇log π · b(s)] = 0),但能大幅降低方差。直观理解:如果回报是 100,基线是 90,那“这步选择比平均好多少”的信息是 10;如果回报是 100,基线是 0,那信息是 100,里面很大一部分是环境本身带来的噪声,不是策略的功劳。减去基线,相当于把“环境的固有难度”剔除出去,只留“策略的增量贡献”。最经典的做法是用价值函数 V^π(s) 做基线,于是被减项就变成了 Q^π(s, a) − V^π(s),这就是优势函数 A(s, a) 的定义。把基线从“还不错的常数”升级成“动态估计的价值函数”之后,REINFORCE 就进化成了 Actor-Critic 架构:Actor 是策略网络,负责输出动作;Critic 是价值网络,负责给动作打分。Actor-Critic 是当下几乎所有主流算法的底座。A2C/A3C 是同步/异步版本的 Advantage Actor-Critic;PPO 是把更新步长控制住的 Actor-Critic;SAC 是加了熵正则和软更新的 Actor-Critic。所以我的建议是:如果你想在 2025 年认真学强化学习,REINFORCE 可以自己手写一遍加深理解,但真正要熟练掌握的是 Actor-Critic 这种双网络架构。4.4 PPO 的截断机制:让策略更新别太“飘”PPO 全称是近端策略优化,它解决了一个实际工程问题:策略梯度更新的步长太难选了。步长太小,训练慢到怀疑人生;步长太大,一次更新把策略推得太远,性能直接崩盘。PPO 的做法是用重要性采样把新旧策略的比值算出来,然后加一个 clip 操作,把比值限制在 [1−ε, 1+ε] 的区间里。ratio = torch.exp(new_log_prob - old_log_prob) surr1 = ratio * advantage surr2 = torch.clamp(ratio, 1 - clip_epsilon, 1 + clip_epsilon) * advantage loss = -torch.min(surr1, surr2).mean()这个 clip 的直觉是:新策略比旧策略好太多?我只给你这么多信任;坏太多?我也只扣这么多。允许小幅改进但不允许冒进,训练就稳定得多。我在实际项目中用过 PPO 训练机器人运动控制,默认参数下很少出现“训练到一半突然变成瘸子”的情况,这在深度强化学习里是难能可贵的。5. 实操环节:从零搭建一个策略梯度训练流程5.1 环境准备与工具选型工欲善其事,必先利其器。我推荐的学习路径是先用一个轻量级环境跑通逻辑,再上重型框架。环境选择上,OpenAI Gym(现在的 Gymnasium)依然是最好的入门选择,API 稳定、环境丰富。拿 CartPole 这样的经典控制问题来练手,动作空间只有 2 个,状态是 4 维连续向量,非常适合验证策略梯度算法。框架层面,PyTorch 是当前做强化学习的主力,动态计算图对调试友好。尽量不要直接用 RLlib、Stable-Baselines3 这类封装过高的库来入门——先用原生 PyTorch 手写一遍 REINFORCE 和 Actor-Critic,你会对数据流、梯度流有真正的体感。等理解了底层原理,再用现成框架提效也不迟。5.2 策略网络的设计细节策略网络的结构可以很简单,但输入输出的设计要谨慎。以 CartPole 为例:class PolicyNetwork(nn.Module): def __init__(self, state_dim, action_dim, hidden_dim=128): super().__init__() self.fc1 = nn.Linear(state_dim, hidden_dim) self.fc2 = nn.Linear(hidden_dim, hidden_dim) self.fc_out = nn.Linear(hidden_dim, action_dim) def forward(self, state): x = torch.relu(self.fc1(state)) x = torch.relu(self.fc2(x)) logits = self.fc_out(x) return logits注意最后一层不用激活函数。它输出的是 logits(未归一化的分数),在采动作时用 softmax 转成概率分布。采样用torch.multinomial或者torch.distributions.Categorical,后者更方便,直接附带log_prob()方法:dist = Categorical(logits=logits) action = dist.sample() log_prob = dist.log_prob(action)写到这里必须提醒一个隐蔽坑:梯度更新时,必须保证这个 log_prob 与采样的动作保持在同一张计算图里。很多人图省事,采样之后把动作和 log_prob 都存成 numpy 数组,下一轮再转回 tensor,结果反向传播时梯度根本传不到策略网络,loss 不降反升,你还找不到原因。5.3 训练超参数的选择与调试方法训练一轮强化学习代码,你会遇到很多让人抓狂的参数。我给出一个从实际项目里总结的起始配置:参数推荐起始值说明学习率1e-3 ~ 3e-4REINFORCE 用偏大的,PPO 用偏小的折扣因子 γ0.99任务越长越接近 1,CartPole 默认 0.99隐藏层宽度128小任务足够,复杂任务建议 256更新回合数1000 ~ 2000CartPole 一般 500 个 episode 能看出收敛趋势熵正则系数0.01探索不足就加大,太散就减小我的调试黄金法则是:先固定所有参数,只改一个变量,跑完画图再看。不要一上来就调三个参数,否则出了问题你根本不知道是谁惹的祸。画图强烈推荐直接记录每一轮的 episode reward,跑完用平滑处理(比如滑动平均)看趋势。如果曲线像心电图一样剧烈跳动,说明方差大,接基线、上 Actor-Critic;如果曲线整体缓慢上升但到平台期就上不去,可能是步长太小或网络容量不够。5.4 完整流程演示:REINFORCE 训练 CartPole这里放一段我实际调试通过的 REINFORCE 完整实现,可以作为你跑通第一个策略梯度算法的参考模板:import torch import torch.nn as nn import gymnasium as gym from torch.distributions import Categorical class PolicyGradientAgent: def __init__(self, state_dim, action_dim, lr=1e-2, gamma=0.99): self.gamma = gamma self.policy = PolicyNetwork(state_dim, action_dim) self.optimizer = torch.optim.Adam(self.policy.parameters(), lr=lr) def select_action(self, state): state = torch.FloatTensor(state).unsqueeze(0) logits = self.policy(state) dist = Categorical(logits=logits) action = dist.sample() return action.item(), dist.log_prob(action) def update(self, log_probs, rewards): returns = [] G = 0 for r in reversed(rewards): G = r + self.gamma * G returns.insert(0, G) returns = torch.FloatTensor(returns) # 用回报均值作为基线,降低部分方差 baseline = returns.mean() loss = - (torch.cat(log_probs) * (returns - baseline)).sum() self.optimizer.zero_grad() loss.backward() self.optimizer.step() env = gym.make("CartPole-v1") agent = PolicyGradientAgent(state_dim=4, action_dim=2) reward_history = [] for episode in range(500): state, _ = env.reset() done = False log_probs, rewards = [], [] while not done: action, log_prob = agent.select_action(state) next_state, reward, terminated, truncated, _ = env.step(action) done = terminated or truncated log_probs.append(log_prob) rewards.append(reward) state = next_state agent.update(log_probs, rewards) total_reward = sum(rewards) reward_history.append(total_reward) if episode % 50 == 0: avg_reward = sum(reward_history[-50:]) / 50 print(f"Episode {episode}, avg reward (last 50): {avg_reward:.1f}")这个版本的 REINFORCE 我用一个简单的均值基线,已经把纯度很高的梯度下降换成了带方差削减的版本。输出里如果看到最近 50 轮平均回报超过 400,恭喜,CartPole 已经被你驯服了。别急着开心,这只是入门。把这份感觉迁移到 Actor-Critic 会很快,因为里面唯一的区别就是拿一个神经网络代替均值基线,实时地估 V(s)。6. 常见问题排查与避坑指南6.1 损失函数不下降甚至上升这是新手遇到最多的现象。看到 loss 一路飙升,第一反应是哪里写错了,但其实策略梯度的 loss 和监督学习里的 loss 含义完全不同——它只是一个代数值,并不代表“错误程度”。真正要看的指标是每轮的平均回报曲线,而不是 loss 曲线。如果你的 loss 在涨但 reward 也在涨,那就是正常的,因为策略熵变小了、log_prob 更集中了。6.2 回报曲线剧烈震荡震荡的根源几乎都是方差。我排查的顺序是这样的:第一,检查是不是基线没加;第二,检查回报归一化(advantage normalization)有没有做;第三,看学习率是不是太大。PPO 里还有第四步,检查 clip 范围。还有一个常被忽略的原因:更新频率太高。每个 episode 都更新,模型在早几步的过渡期,新旧策略差异太大,容易出现震荡。我的建议是收集多个 episode 的样本,打包成一个 batch 再做更新,batch 大小 64 到 256 之间效果都不错。6.3 策略过早收敛到次优策略训练很快,回报曲线也稳定了,但最终效果就是不如别人论文里的好。这是典型的“早熟”问题:策略太早就失去了探索能力,输出的分布几乎变成了确定性分布,但那个解不是最优的。对策很简单:调大熵正则系数,或者降低学习率,让更新更慢、更细。实战里我会开一个探针:打印每个 batch 的平均熵值,如果熵值掉到某个很小阈值以下,就自动加一点熵系数,这个技巧在 SAC 和 PPO 里非常管用。6.4 环境重置与 done 信号的坑CartPole 这种环境有两个布尔量,一个叫terminated(真终止,比如杆子倒了),一个叫truncated(截断,比如超时)。很多代码教程只判断done,不区分两者,这在简单环境里问题不大,但真实任务里区别巨大:超时截断不应该算作“策略失败”。如果你在训练一个长期任务,建议把truncated当成独立的结束标记,不要在奖励累计和回报计算上把它和失败混为一谈。否则策略会错误地学会“在封顶的回合里故意拖延”,训练目标都被带偏了。6.5 实战排查速查表现象可能原因排查手段loss 不降,reward 也不动梯度未回传 / 网络输出恒为常数检查 log_prob 是否在计算图中,打印梯度范数reward 波动巨大方差高、未加基线 / 未做优势归一化用 V(s) 做基线,对 advantage 做标准化reward 上限很低探索不足、策略早熟调大熵正则系数,或减小学习率训练后期突然崩溃更新步长过大、策略被推过头换 PPO 加 clip,或降低学习率与文档环境不一致Gym 版本 API 差异检查 reset 返回值、done 的语义,跑 smoke test有一条我做项目时反复验证的心得:先在玩具环境里把代码跑通,再换到自己的场景。如果你连 CartPole 都稳定不了,直接上真实业务环境只会更绝望。别觉得玩具环境简单,把玩具环境调教得又快又稳,本身就是对算法理解和调参功力的考验。7. 策略学习的进阶方向与我的经验总结现阶段把这些基础吃透之后,你的下一步可以往两个方向走。一个是模型无关的策略优化家族:PPO、SAC、TD3 这些工程上极其成熟的算法。另一个是离线强化学习,比如 IQL、CQL——它们关注的是如何从已有数据里学策略,而不需要实时与环境交互,这在真实业务里价值极高,因为很多场景不允许你在线试错。我个人的学习路径建议是:先手写 REINFORCE,再手写 A2C,然后跳到 PPO。不要一上来就啃 SAC 的论文,它的数学推导看着优雅,但工程实现里熵系数调节、目标网络软更新的细节能把人折磨到怀疑人生。先把 PPO 调稳,整个策略梯度家族的理解就能立起来。最后分享一个自己踩过的大坑:强化学习代码里有一类非常隐蔽的逻辑错误——one-step return 的计算。REINFORCE 里如果折扣回报算错了一个符号或者下标,训练不会直接报错,而是表现为“明明奖励在改善,但 loss 数值和直觉对不上”。这让我排查了整整两个晚上,后来通过在状态里加了一个已知的探针单元才定位到问题。我的建议是:写回报计算函数时,先用一个手算的正确小例子做单元测试,再往主流程里接。强化学习项目 debug 的难点不在于语法,而在于数据流中的逻辑错位,这一点一定要有预期。策略,说到底是“面对某个局面选择怎么做”的学问。看起来仿佛简单,但把这个问题在数学、算法和工程三个层面都解决好,需要理解的细节远比你想象的要多。把这篇文章里的每一段代码亲手敲一遍、每一个参数亲手调一遍,策略梯度就从“看起来懂了”变成“真的会用了”。
1. 策略的本质:从“小马过河”说起如果只用一句话点破强化学习的灵魂,我会说是“策略”。智能体怎么决策、面对某个状态选哪个动作、走一步看几步、是保守还是激进,统统归结为策略。很多教程习惯先讲奖励、价值函数、Q表,但回头你会发现,那些东西最终都是为了一件事服务:找到更好的策略。所以第一篇笔记,我决定先把策略拎出来讲透。先抛一个生活化的类比。你让一个新手做菜,他面对一筐食材会手足无措:先放油还是先放盐?火开大还是开小?他需要一份“菜谱”来告诉他,在“锅热了”这个状态下,应该“倒油”而不是“倒水”。菜谱,就是策略。强化学习里的智能体,本质上就是在一遍遍试错中,自己写出一份越来越好的菜谱——不是靠人教的,而是靠吃出来的经验。策略的定义其实非常朴素:策略是从环境状态到动作选择的映射,用数学符号写就是 π(a|s),读作“在状态 s 下选择动作 a 的概率(或确定值)”。它回答的问题是“我现在该干什么”。这里的“现在”不是某一刻,而是每一个时间步。整个过程就是在和环境不断交互:观察状态 s_t,按策略选动作 a_t,环境返回奖励 r_t 和下一个状态 s_{t+1},然后周而复始。那策略为什么值得单独拿出来深入研究?因为它是整个强化学习问题的求解目标。价值函数也好、Q函数也好,它们是辅助工具,用来“评估”策略的。真正的“决策者”就是策略本身。业务上真正上线部署的也是策略:自动驾驶的车该往左打多少方向盘、推荐系统该推哪件商品、游戏 AI 该释放哪个技能,全部是策略在做最终拍板。把策略搞明白了,强化学习的一半就算入了门。还有一点新手容易忽略:策略形式的选择直接决定了你能用哪类算法。你选了确定性策略,网络输出就是一个具体的动作值;你选了随机性策略,网络输出就是动作的概率分布。前者适合动作空间连续的机器人控制,后者在博弈类场景里天然具备探索能力。后面会详细说,这里先记住一个结论:策略是强化学习的目标,也是算法分类的一条主线。2. 策略的两副面孔:确定性策略与随机性策略2.1 确定性与随机性的区别策略表面看就是“映射”,但映射有两种风格。确定性策略记作 a = μ(s),同一个状态永远输出同一个动作,没有花招,输入相同输出必相同。随机性策略记作 a ~ π(a|s),给定状态后输出的是一个概率分布,从这个分布里采样得到动作;同样的状态,两次采样可能得到不同的动作。这两种风格没有绝对的优劣,但适用场景差异很大。拿围棋举例,职业棋手面对同一个棋局,落子其实是带概率的——他会考虑几种候选着法,而不是每次只认死一个点。这就是随机性策略的价值:它保留了不确定性,给了智能体探索的空间。反过来看机器人关节控制,你让机械臂以 0.5 的概率往左、0.5 的概率往右,那机械臂会原地抽搐,生产线上根本没法用。这种场景就要确定性策略。从数学表达上看,随机性策略的概率分布可以是离散的,比如分类问题常见的 softmax 输出;也可以是连续的,比如高斯分布,用均值 μ(s) 和方差 σ(s) 刻画一个概率密度。确定性策略大多直接用一个回归网络输出动作向量,简单直接。2.2 随机策略中的探索与利用随机性策略还有一个隐藏福利:它把“探索”这件事内建在了决策过程里。经典强化学习里有著名的探索-利用困境:你是用当前已知最好的动作去“利用”,还是尝试一些次优动作去“探索”,赌它们未来会带来更高回报?如果策略是确定性的,你得额外加 ε-greedy 之类的扰动;如果策略天生带熵,比如输出一个概率分布,那么每次采样天然就有可能选到非最优动作,探索就自动发生了。但随机策略的探索也不是白来的。它的问题在于方差:如果输出分布的熵太大,动作太分散,训练过程会像喝醉了酒走路——明明知道方向,每一步都在踉跄。所以后来的算法普遍要控制熵的权重,比如在损失函数里加一个熵正则项,让策略既保持一定的随机性,又不至于散得太离谱。这个熵正则的系数,我建议新手从 0.01 起步,调参时观察训练曲线,如果策略太早就收敛到确定性分布,说明探索不足,可以适当调大;如果一直随机乱试不收敛,就调小。2.3 策略的参数化:从表格到神经网络早期的强化学习用表格存策略:状态有多少个,动作有多少个,表格就有多大。比如 10 个状态 5 个动作,就是一个 10 行 5 列的矩阵,每格存一个概率。这种方法在小规模环境中很直观,但一碰到连续状态就崩了——自动驾驶里车速、位置、角度是连续值,状态空间接近无穷大,表格根本开不出来。所以现代强化学习几乎都走参数化路线:用一个参数为 θ 的神经网络来表示策略,记作 π_θ(a|s)。网络的输入是状态特征,输出是动作或者动作分布。这意味着“策略学习”问题变成了“参数优化”问题——我们真正要操心的是怎么调这组 θ。这也是所有深度强化学习算法的共性问题:目标函数怎么设计、梯度怎么算、网络怎么更新。参数化还有一层好处:泛化能力。表格策略没见过某个状态就彻底抓瞎,神经网络对相近状态能给出相近的输出,有一定的外推能力。当然这既是优点也是隐患,神经网络的泛化有时候会“自信过头”,对没见过的状态输出一个离谱的动作,这个问题到后面实际部署时要想办法兜底。3. 策略的标尺:用价值函数给策略打分3.1 状态价值与动作价值策略本身没有高下,得靠一把尺子量。强化学习里的尺子就是价值函数。两个最核心的概念:状态价值 V^π(s):从状态 s 出发,按照策略 π 行动,未来能拿到的累计折扣奖励的期望。动作价值 Q^π(s, a):在状态 s 先执行动作 a,之后按照策略 π 继续行动,未来累计奖励的期望。区别就在那个“先”。V 问的是“我现在这个局面值多少钱”,Q 问的是“我现在做这个动作值多少钱”。一个类比:你站在十字路口,V 是“站在这个路口本身的价值”,Q 是“选择左转这个具体动作的价值”。显然 Q 比 V 多了一维信息,也因此更难算。价值函数和策略的关系像一个嵌套循环。给定策略 π,我们能用它去估 V 和 Q;反过来,有了准确的 V 和 Q,我又能改进策略——看到哪个动作的 Q 值高,就把它的概率调大。这个循环就是强化学习的核心引擎。3.2 策略评估:怎么算出当前策略的价值给定一个策略 π,算出它的 V^π 和 Q^π,这个操作叫策略评估。原理上就是解贝尔曼方程,方程的核心思想是递归:当前状态的价值 = 即时奖励 + 折扣后的下一状态价值。数学上写成:V^π(s) = Σ_a π(a|s) Σ_{s′,r} p(s′,r|s,a) [r + γV^π(s′)]这个式子看着吓人,拆开就是:按概率对当前状态下所有可能动作求期望,再对环境的转移概率求期望。真实环境里你往往不知道 p(s′, r|s, a),所以没人真的去解这个期望,而是靠采样。让智能体按策略 π 跑很多轮,把每一轮得到的回报记下来,平均一下,这就是蒙特卡洛策略评估。它简单、无偏,但是方差大,需要跑很多轮才稳定。另一种做法是时序差分(TD),它用单步的奖励加上对下一状态的估计,来更新当前状态的价值。一个经典的更新式是:V(s) ← V(s) + α [r + γV(s′) − V(s)]方括号里那项叫 TD 误差。它用 r + γV(s′) 作为对真实价值的“有偏估计”,替代完整回报。这种做法方差小、也能在线学习,所以被绝大多数现代算法采用。代价是引入了偏差,尤其是在函数近似(比如用神经网络做价值函数)的情况下,TD 容易出现估计偏高或偏低的问题。3.3 策略改进:从“打分”到“换动作”估完价值之后怎么办?答案自然是改进策略。当一个动作的价值比当前策略下所有动作的平均价值都高,那就应该提高选它的概率。贪心策略改进就是最直接的思路:状态 s 下,选 Q(s, a) 最大的那个动作。这个逻辑好懂,但它直接把随机策略变成确定性策略,在探索和博弈场景里不一定好用。更温和的做法是软策略迭代:用 softmax 把 Q 值变成动作概率,温度参数 T 控制“软”的程度。T 大,各动作概率接近,探索多;T 小,策略趋于贪心,稳定但探索少。这就是后续很多算法里 softmax 策略的雏形。现在很多强化学习框架里,你看到的“行动选择概率”基本就是从这派生的。这个评估-改进循环持续迭代,理论上会一步一步逼近最优策略 π*。整个过程就是广义策略迭代,几乎所有基于价值的方法(Q-learning、DQN 那一脉)都默默遵循这个框架。理解了这条主线,后面看 DQN、PPO 的论文,你会发现它们都只是这个循环的某个环节做了工程化改良。4. 策略的优化器:策略梯度方法的原理与实战4.1 为什么直接调策略会有困难价值函数方法走过的路线是:先学价值函数,再从价值函数里提取策略。策略梯度方法跳过了中间步骤,直接对策略参数 θ 求梯度,往让期望回报增大的方向更新。直觉上这更直接,但是有一个坎:期望回报 J(θ) 是轨迹上的积分,可轨迹的生成过程里,动作是从策略里采样得到的,而采样这个操作是不可导的。你不能直接对“随机采样出来的动作”求梯度,这是策略梯度方法最初让人困惑的地方。解决办法很漂亮:把对数似然这个技巧用起来。对期望回报的梯度,可以写成对每一步的“对数策略概率 × 回报”求期望。最终形式就是策略梯度定理:∇J(θ) = E[∇_θ log π_θ(a_t|s_t) · R(τ)]这个公式的意义是:回报高的轨迹,我们要增大其中所有动作的概率;回报低的轨迹,要减小其中的动作概率。这跟人类学习很像——考得好的那次,把当时的解题策略记住;考砸了,就把当时的选择忘掉。4.2 REINFORCE 算法的完整拆解最朴素的策略梯度算法是 REINFORCE,也叫蒙特卡洛策略梯度。它的流程我拆成了四个步骤:用当前策略 π_θ 与环境交互,采集一整条轨迹,记录状态、动作、奖励。计算这条轨迹的总回报 R(τ),可以带折扣。对轨迹中的每个时间步,计算梯度项 ∇_θ log π_θ(a_t|s_t) × R(τ)。用梯度上升更新参数 θ。写个简化伪代码,大家感受一下全貌:for episode in range(num_episodes): trajectory = [] state = env.reset() done = False while not done: action = policy.sample(state) # 从当前策略采样动作 next_state, reward, done, _ = env.step(action) trajectory.append((state, action, reward)) state = next_state # 计算折扣回报 returns = [] G = 0 for _, _, reward in reversed(trajectory): G = reward + gamma * G returns.append(G) returns = list(reversed(returns)) # 更新策略参数 policy.zero_grad() for (state, action), G in zip(trajectory, returns): log_prob = policy.log_prob(state, action) loss = -log_prob * G # 梯度上升转梯度下降取负号 loss.backward() policy.step()这段代码有一个很值得注意的地方:梯度是每个时间步单独算,但 PyTorch 默认会把梯度累积,所以更新完要 zero_grad;同时 loss 取负号是因为优化器默认做梯度下降。REINFORCE 的实现很简单,但方差极大,经常会出现“训练一整个 epoch,loss 在跳,策略就是不提升”的现象。那个时候我踩坑踩得非常深,后来才知道核心问题是高方差。4.3 方差控制的核心手段:基线技术与 Actor-CriticREINFORCE 的方差大,根因在于 R(τ) 本身波动很大:同样一个策略,这次拿 100 分,下次可能只有 -50 分,直接把梯度项推得忽正忽负。解决思路是引入基线 b(s),把梯度改成:∇J(θ) = E[∇_θ log π_θ(a_t|s_t) · (R(τ) − b(s_t))]这个减法不会改变梯度的期望(因为 E[∇log π · b(s)] = 0),但能大幅降低方差。直观理解:如果回报是 100,基线是 90,那“这步选择比平均好多少”的信息是 10;如果回报是 100,基线是 0,那信息是 100,里面很大一部分是环境本身带来的噪声,不是策略的功劳。减去基线,相当于把“环境的固有难度”剔除出去,只留“策略的增量贡献”。最经典的做法是用价值函数 V^π(s) 做基线,于是被减项就变成了 Q^π(s, a) − V^π(s),这就是优势函数 A(s, a) 的定义。把基线从“还不错的常数”升级成“动态估计的价值函数”之后,REINFORCE 就进化成了 Actor-Critic 架构:Actor 是策略网络,负责输出动作;Critic 是价值网络,负责给动作打分。Actor-Critic 是当下几乎所有主流算法的底座。A2C/A3C 是同步/异步版本的 Advantage Actor-Critic;PPO 是把更新步长控制住的 Actor-Critic;SAC 是加了熵正则和软更新的 Actor-Critic。所以我的建议是:如果你想在 2025 年认真学强化学习,REINFORCE 可以自己手写一遍加深理解,但真正要熟练掌握的是 Actor-Critic 这种双网络架构。4.4 PPO 的截断机制:让策略更新别太“飘”PPO 全称是近端策略优化,它解决了一个实际工程问题:策略梯度更新的步长太难选了。步长太小,训练慢到怀疑人生;步长太大,一次更新把策略推得太远,性能直接崩盘。PPO 的做法是用重要性采样把新旧策略的比值算出来,然后加一个 clip 操作,把比值限制在 [1−ε, 1+ε] 的区间里。ratio = torch.exp(new_log_prob - old_log_prob) surr1 = ratio * advantage surr2 = torch.clamp(ratio, 1 - clip_epsilon, 1 + clip_epsilon) * advantage loss = -torch.min(surr1, surr2).mean()这个 clip 的直觉是:新策略比旧策略好太多?我只给你这么多信任;坏太多?我也只扣这么多。允许小幅改进但不允许冒进,训练就稳定得多。我在实际项目中用过 PPO 训练机器人运动控制,默认参数下很少出现“训练到一半突然变成瘸子”的情况,这在深度强化学习里是难能可贵的。5. 实操环节:从零搭建一个策略梯度训练流程5.1 环境准备与工具选型工欲善其事,必先利其器。我推荐的学习路径是先用一个轻量级环境跑通逻辑,再上重型框架。环境选择上,OpenAI Gym(现在的 Gymnasium)依然是最好的入门选择,API 稳定、环境丰富。拿 CartPole 这样的经典控制问题来练手,动作空间只有 2 个,状态是 4 维连续向量,非常适合验证策略梯度算法。框架层面,PyTorch 是当前做强化学习的主力,动态计算图对调试友好。尽量不要直接用 RLlib、Stable-Baselines3 这类封装过高的库来入门——先用原生 PyTorch 手写一遍 REINFORCE 和 Actor-Critic,你会对数据流、梯度流有真正的体感。等理解了底层原理,再用现成框架提效也不迟。5.2 策略网络的设计细节策略网络的结构可以很简单,但输入输出的设计要谨慎。以 CartPole 为例:class PolicyNetwork(nn.Module): def __init__(self, state_dim, action_dim, hidden_dim=128): super().__init__() self.fc1 = nn.Linear(state_dim, hidden_dim) self.fc2 = nn.Linear(hidden_dim, hidden_dim) self.fc_out = nn.Linear(hidden_dim, action_dim) def forward(self, state): x = torch.relu(self.fc1(state)) x = torch.relu(self.fc2(x)) logits = self.fc_out(x) return logits注意最后一层不用激活函数。它输出的是 logits(未归一化的分数),在采动作时用 softmax 转成概率分布。采样用torch.multinomial或者torch.distributions.Categorical,后者更方便,直接附带log_prob()方法:dist = Categorical(logits=logits) action = dist.sample() log_prob = dist.log_prob(action)写到这里必须提醒一个隐蔽坑:梯度更新时,必须保证这个 log_prob 与采样的动作保持在同一张计算图里。很多人图省事,采样之后把动作和 log_prob 都存成 numpy 数组,下一轮再转回 tensor,结果反向传播时梯度根本传不到策略网络,loss 不降反升,你还找不到原因。5.3 训练超参数的选择与调试方法训练一轮强化学习代码,你会遇到很多让人抓狂的参数。我给出一个从实际项目里总结的起始配置:参数推荐起始值说明学习率1e-3 ~ 3e-4REINFORCE 用偏大的,PPO 用偏小的折扣因子 γ0.99任务越长越接近 1,CartPole 默认 0.99隐藏层宽度128小任务足够,复杂任务建议 256更新回合数1000 ~ 2000CartPole 一般 500 个 episode 能看出收敛趋势熵正则系数0.01探索不足就加大,太散就减小我的调试黄金法则是:先固定所有参数,只改一个变量,跑完画图再看。不要一上来就调三个参数,否则出了问题你根本不知道是谁惹的祸。画图强烈推荐直接记录每一轮的 episode reward,跑完用平滑处理(比如滑动平均)看趋势。如果曲线像心电图一样剧烈跳动,说明方差大,接基线、上 Actor-Critic;如果曲线整体缓慢上升但到平台期就上不去,可能是步长太小或网络容量不够。5.4 完整流程演示:REINFORCE 训练 CartPole这里放一段我实际调试通过的 REINFORCE 完整实现,可以作为你跑通第一个策略梯度算法的参考模板:import torch import torch.nn as nn import gymnasium as gym from torch.distributions import Categorical class PolicyGradientAgent: def __init__(self, state_dim, action_dim, lr=1e-2, gamma=0.99): self.gamma = gamma self.policy = PolicyNetwork(state_dim, action_dim) self.optimizer = torch.optim.Adam(self.policy.parameters(), lr=lr) def select_action(self, state): state = torch.FloatTensor(state).unsqueeze(0) logits = self.policy(state) dist = Categorical(logits=logits) action = dist.sample() return action.item(), dist.log_prob(action) def update(self, log_probs, rewards): returns = [] G = 0 for r in reversed(rewards): G = r + self.gamma * G returns.insert(0, G) returns = torch.FloatTensor(returns) # 用回报均值作为基线,降低部分方差 baseline = returns.mean() loss = - (torch.cat(log_probs) * (returns - baseline)).sum() self.optimizer.zero_grad() loss.backward() self.optimizer.step() env = gym.make("CartPole-v1") agent = PolicyGradientAgent(state_dim=4, action_dim=2) reward_history = [] for episode in range(500): state, _ = env.reset() done = False log_probs, rewards = [], [] while not done: action, log_prob = agent.select_action(state) next_state, reward, terminated, truncated, _ = env.step(action) done = terminated or truncated log_probs.append(log_prob) rewards.append(reward) state = next_state agent.update(log_probs, rewards) total_reward = sum(rewards) reward_history.append(total_reward) if episode % 50 == 0: avg_reward = sum(reward_history[-50:]) / 50 print(f"Episode {episode}, avg reward (last 50): {avg_reward:.1f}")这个版本的 REINFORCE 我用一个简单的均值基线,已经把纯度很高的梯度下降换成了带方差削减的版本。输出里如果看到最近 50 轮平均回报超过 400,恭喜,CartPole 已经被你驯服了。别急着开心,这只是入门。把这份感觉迁移到 Actor-Critic 会很快,因为里面唯一的区别就是拿一个神经网络代替均值基线,实时地估 V(s)。6. 常见问题排查与避坑指南6.1 损失函数不下降甚至上升这是新手遇到最多的现象。看到 loss 一路飙升,第一反应是哪里写错了,但其实策略梯度的 loss 和监督学习里的 loss 含义完全不同——它只是一个代数值,并不代表“错误程度”。真正要看的指标是每轮的平均回报曲线,而不是 loss 曲线。如果你的 loss 在涨但 reward 也在涨,那就是正常的,因为策略熵变小了、log_prob 更集中了。6.2 回报曲线剧烈震荡震荡的根源几乎都是方差。我排查的顺序是这样的:第一,检查是不是基线没加;第二,检查回报归一化(advantage normalization)有没有做;第三,看学习率是不是太大。PPO 里还有第四步,检查 clip 范围。还有一个常被忽略的原因:更新频率太高。每个 episode 都更新,模型在早几步的过渡期,新旧策略差异太大,容易出现震荡。我的建议是收集多个 episode 的样本,打包成一个 batch 再做更新,batch 大小 64 到 256 之间效果都不错。6.3 策略过早收敛到次优策略训练很快,回报曲线也稳定了,但最终效果就是不如别人论文里的好。这是典型的“早熟”问题:策略太早就失去了探索能力,输出的分布几乎变成了确定性分布,但那个解不是最优的。对策很简单:调大熵正则系数,或者降低学习率,让更新更慢、更细。实战里我会开一个探针:打印每个 batch 的平均熵值,如果熵值掉到某个很小阈值以下,就自动加一点熵系数,这个技巧在 SAC 和 PPO 里非常管用。6.4 环境重置与 done 信号的坑CartPole 这种环境有两个布尔量,一个叫terminated(真终止,比如杆子倒了),一个叫truncated(截断,比如超时)。很多代码教程只判断done,不区分两者,这在简单环境里问题不大,但真实任务里区别巨大:超时截断不应该算作“策略失败”。如果你在训练一个长期任务,建议把truncated当成独立的结束标记,不要在奖励累计和回报计算上把它和失败混为一谈。否则策略会错误地学会“在封顶的回合里故意拖延”,训练目标都被带偏了。6.5 实战排查速查表现象可能原因排查手段loss 不降,reward 也不动梯度未回传 / 网络输出恒为常数检查 log_prob 是否在计算图中,打印梯度范数reward 波动巨大方差高、未加基线 / 未做优势归一化用 V(s) 做基线,对 advantage 做标准化reward 上限很低探索不足、策略早熟调大熵正则系数,或减小学习率训练后期突然崩溃更新步长过大、策略被推过头换 PPO 加 clip,或降低学习率与文档环境不一致Gym 版本 API 差异检查 reset 返回值、done 的语义,跑 smoke test有一条我做项目时反复验证的心得:先在玩具环境里把代码跑通,再换到自己的场景。如果你连 CartPole 都稳定不了,直接上真实业务环境只会更绝望。别觉得玩具环境简单,把玩具环境调教得又快又稳,本身就是对算法理解和调参功力的考验。7. 策略学习的进阶方向与我的经验总结现阶段把这些基础吃透之后,你的下一步可以往两个方向走。一个是模型无关的策略优化家族:PPO、SAC、TD3 这些工程上极其成熟的算法。另一个是离线强化学习,比如 IQL、CQL——它们关注的是如何从已有数据里学策略,而不需要实时与环境交互,这在真实业务里价值极高,因为很多场景不允许你在线试错。我个人的学习路径建议是:先手写 REINFORCE,再手写 A2C,然后跳到 PPO。不要一上来就啃 SAC 的论文,它的数学推导看着优雅,但工程实现里熵系数调节、目标网络软更新的细节能把人折磨到怀疑人生。先把 PPO 调稳,整个策略梯度家族的理解就能立起来。最后分享一个自己踩过的大坑:强化学习代码里有一类非常隐蔽的逻辑错误——one-step return 的计算。REINFORCE 里如果折扣回报算错了一个符号或者下标,训练不会直接报错,而是表现为“明明奖励在改善,但 loss 数值和直觉对不上”。这让我排查了整整两个晚上,后来通过在状态里加了一个已知的探针单元才定位到问题。我的建议是:写回报计算函数时,先用一个手算的正确小例子做单元测试,再往主流程里接。强化学习项目 debug 的难点不在于语法,而在于数据流中的逻辑错位,这一点一定要有预期。策略,说到底是“面对某个局面选择怎么做”的学问。看起来仿佛简单,但把这个问题在数学、算法和工程三个层面都解决好,需要理解的细节远比你想象的要多。把这篇文章里的每一段代码亲手敲一遍、每一个参数亲手调一遍,策略梯度就从“看起来懂了”变成“真的会用了”。