1. 从“试错”到“策略”:为什么我们需要策略学习?
聊到强化学习,很多刚入门的朋友第一反应可能是“试错”——智能体在环境里瞎逛,撞了南墙就回头,运气好就找到宝藏。这确实是强化学习最直观的体现,比如经典的“悬崖漫步”问题。但如果你真这么去实现,很快就会发现效率低得令人发指。智能体像个无头苍蝇,大部分时间都在重复犯错,或者在一个局部最优解里打转。这背后的核心问题是:我们只告诉了智能体“什么状态下的什么动作是好是坏”(价值函数),却没有直接告诉它“在某个状态下,到底应该采取哪个动作”。
这就是“策略学习”登场的时刻。如果说基于价值的强化学习(Value-Based RL,比如Q-Learning)是在地图上标出每个位置的“潜在宝藏价值”,然后让智能体自己根据价值去选择方向,那么策略学习(Policy-Based RL)就是直接给智能体一张“行动指南”,明确告诉它:“站在这里,你应该往左走。”这个“行动指南”,就是我们要学习的策略函数 π(a|s)。它输入一个状态 s,直接输出在该状态下采取每个动作 a 的概率分布。智能体根据这个概率分布进行抽样,来决定实际执行的动作。
为什么这很重要?我举两个在实际项目中让我印象深刻的例子。第一个是多智能体协作游戏,比如《王者荣耀》里的英雄配合。一个英雄的价值(比如输出、生存)高度依赖于队友的行动。用价值函数去刻画这种复杂的、动态的相互依赖关系极其困难,因为状态空间会随着队友策略的变化而爆炸。但策略学习可以直接学习一个条件概率:“看到队友冲上去了,我有多大概率应该跟上控制,有多大概率应该后撤保命?”这种直接输出动作概率的方式,在处理高维、连续动作空间(比如机器人控制中,机械臂每个关节的扭矩是一个连续值)时,优势更是碾压性的。你无法用一张离散的Q表去枚举所有可能的扭矩组合,但一个策略网络可以输出一个高斯分布的均值和方差,直接生成连续的动作。
所以,策略学习的核心价值在于它的直接性和灵活性。它绕过了学习复杂价值函数这一步,直接优化我们最终想要的东西——行为策略。这对于动作空间复杂、随机策略最优(比如石头剪刀布游戏,固定出拳必输,必须随机出)、或者需要平滑行为变化的场景来说,几乎是唯一的选择。接下来,我们就深入这个“行动指南”的内部,看看它是如何被构建和优化的。
2. 策略函数的“心脏”:参数化与梯度构建
策略 π(a|s) 本身可以是一个简单的查找表(对于极小状态空间),但更普遍的是用一个带参数 θ 的函数来近似,记作 π_θ(a|s)。这个函数就是策略的“心脏”。在深度学习时代,它通常是一个神经网络,我们称之为策略网络(Policy Network)。网络的输入是状态 s(可能是图像像素、传感器数据、游戏画面等经过编码的向量),输出则根据动作空间类型有所不同:
- 离散动作空间:输出层通常是一个Softmax层,每个神经元对应一个动作,输出值代表选择该动作的概率。所有动作的概率之和为1。
- 连续动作空间:输出层通常输出一个概率分布的参数。最常见的是高斯分布(正态分布),此时网络输出两部分:均值 μ 和方差 σ(或对数方差 log_σ 以保证正值)。动作 a 则从这个分布中采样得到:a ~ N(μ, σ²)。这样做既保证了输出的连续性,又通过采样引入了探索性。
有了参数化的策略函数,我们的目标就变成了:找到一组参数 θ,使得遵循这个策略所获得的期望总回报 J(θ)最大化。期望总回报 J(θ) 可以理解为,从某个起始状态出发,智能体按照策略 π_θ 与环境交互,所得到的所有奖励(考虑折扣因子 γ)的平均值。我们的优化问题就是:max_θ J(θ)。
如何优化?我们自然想到梯度上升(Gradient Ascent)。如果我们能计算出目标函数 J(θ) 关于参数 θ 的梯度 ∇_θ J(θ),那么就可以沿着梯度方向更新参数:θ ← θ + α * ∇_θ J(θ),其中 α 是学习率。这个梯度 ∇_θ J(θ),就是著名的策略梯度(Policy Gradient)。
策略梯度的推导是策略学习的数学核心。其最终形式(一种常见形式)是: ∇_θ J(θ) = E_τ~π_θ [ (Σ_{t=0}^{T} ∇_θ log π_θ(a_t|s_t)) * (Σ_{t‘=t}^{T} γ^{t’-t} r_{t‘}) ] 这个公式看起来复杂,但直观理解非常关键:它通过智能体实际走过的轨迹 τ 来估计梯度。公式中的 Σ_{t=0}^{T} ∇_θ log π_θ(a_t|s_t) 是整条轨迹上,每个时刻所选动作的对数概率关于参数 θ 的梯度之和。而 Σ_{t‘=t}^{T} γ^{t’-t} r_{t‘} 是从时刻 t 到结束的回报(Return),它衡量了从时刻 t 开始往后所有决策的“好坏”。
注意:这里回报的计算方式有多种变体,比如减去一个基线(Baseline)来减少方差,这就是后续Actor-Critic框架的思想雏形。最朴素的方法是使用从t时刻开始的累计奖励。
这个梯度的意义是什么?它实际上在做一件非常符合直觉的事:放大那些带来高回报的动作的概率,抑制那些带来低回报的动作的概率。如果某条轨迹的最终回报很高,那么这条轨迹上每一步动作的“功劳”都会被强化(梯度为正,更新会使这些动作的概率增加);反之,如果回报很低,这些动作的概率就会被削弱。
然而,这个最朴素的策略梯度方法(常被称为REINFORCE算法)有一个致命缺点:高方差(High Variance)。因为我们的梯度估计依赖于通过随机采样得到的单条或多条轨迹的回报,而交互过程本身具有随机性(环境动态、策略采样),导致估计的梯度噪声很大,训练极其不稳定,收敛速度慢。这就引出了我们对策略梯度的一系列重要改进。
3. 驯服“高方差”猛兽:基线、Actor-Critic与信赖域
直接使用蒙特卡洛回报估计的策略梯度方差太大,这在实践中几乎是不可用的。我们必须想办法“驯服”这头方差猛兽。下面介绍三种核心且层层递进的技术。
3.1 引入基线:一个简单的“及格线”
第一个直观的改进是引入基线(Baseline)。我们不再使用原始回报 G_t 作为权重,而是使用 (G_t - b(s_t))。这里的 b(s_t) 是一个只与状态 s_t 有关(与动作 a_t 无关)的函数,通常我们用一个价值函数 V(s_t) 的估计来充当基线。 梯度公式变为:∇_θ J(θ) ≈ E [ Σ_t ∇_θ log π_θ(a_t|s_t) * (G_t - V(s_t)) ]。
为什么减去基线能减少方差?想象一下给学生打分。如果直接用绝对分数(回报)评价,一个在简单题上得90分的学生可能比在难题上得70分的学生获得更高评价。但如果我们引入一个“平均分”或“期望分数”(基线 V(s)),用“优势”(A_t = G_t - V(s_t))来评价,就能更好地衡量动作的“相对好坏”。一个动作带来的回报只要超过在该状态下的平均期望,它就是“好”动作,就应该被鼓励。减去基线不改变梯度的期望(无偏估计),但能显著降低方差,因为优势值 A_t 的波动范围通常比原始回报 G_t 小得多。
3.2 Actor-Critic框架:价值网络的协同进化
引入基线 V(s) 后,一个自然的问题是如何得到这个 V(s)?最经典和强大的答案就是Actor-Critic(演员-评论家)框架。这个框架将策略学习和价值学习融为一体:
- Actor(演员): 即我们的策略网络 π_θ(a|s),负责生成动作。它根据Critic的评价来更新自己,目标是最大化期望回报。
- Critic(评论家): 即我们的价值网络 V_φ(s)(或 Q_φ(s, a)),负责评价状态(或状态-动作对)的好坏。它通过时序差分(TD)等方法来学习,目标是准确预测状态价值或优势。
在Actor-Critic中,策略梯度中的优势函数 A(s, a) 就由Critic来提供。例如,使用TD误差 δ_t = r_t + γV(s_{t+1}) - V(s_t) 作为优势函数 A_t 的估计。Actor的更新公式变为:θ ← θ + α * ∇_θ log π_θ(a_t|s_t) * δ_t。
这个框架的美妙之处在于在线学习和单步更新。REINFORCE需要等到一个回合(episode)结束才能更新,而Actor-Critic在每一步(t)都可以立即更新,学习效率高得多。Critic不断学习更准确的价值估计,为Actor提供更精准的“指导”;Actor则根据指导调整策略,产生新的数据供Critic学习,形成一个良性循环。
实操心得:在实现Actor-Critic时,一个常见的坑是Actor和Critic的学习率(α_actor 和 α_critic)设置不当。通常,Critic需要学得更快、更准一些,这样它给Actor的“评分”才可靠。我的一般经验是,将 α_critic 设置为 α_actor 的2到5倍,并密切监控价值损失(Value Loss)的收敛情况。如果价值损失震荡很大,说明Critic还没学好,此时Actor的更新方向可能是错的。
3.3 信赖域与PPO:让更新步伐更稳健
即使有了Actor-Critic,策略梯度方法还有一个深层次问题:策略的更新步长(学习率)很难选择。步长太大,一次更新可能让策略“跳”到一个性能差很多的新区域,导致训练崩溃(性能骤降);步长太小,学习速度又太慢。
如何保证每次更新后,新策略 π_θ’ 不会离旧策略 π_θ 太远,从而稳定提升性能?这就是信赖域策略优化(Trust Region Policy Optimization, TRPO)和其更流行的改进版近端策略优化(Proximal Policy Optimization, PPO)要解决的核心问题。
TRPO通过复杂的二阶优化(自然梯度)来强制约束新旧策略的KL散度(一种分布距离度量),确保更新在“信赖域”内。虽然理论漂亮,但实现复杂,计算量大。
PPO则提出了两种更工程化、更高效的解决方案来近似这个约束:
- PPO-Clip(裁剪): 这是最流行的方法。它直接在新旧策略的概率比值上动手术。定义概率比 r_t(θ) = π_θ(a_t|s_t) / π_θ_old(a_t|s_t)。如果这个比值偏离1太远,说明策略变化太大。PPO-Clip的目标函数是:L^{CLIP}(θ) = E_t [ min( r_t(θ) * A_t, clip(r_t(θ), 1-ε, 1+ε) * A_t ) ]。其中 ε 是一个小超参(如0.1或0.2)。这个 min 操作保证了,当优势 A_t 为正时,我们鼓励该动作,但概率比最多增长到 (1+ε);当 A_t 为负时,我们抑制该动作,但概率比最多减少到 (1-ε)。这样就巧妙地限制了每次更新的幅度。
- PPO-Penalty(惩罚): 在目标函数中直接添加一个KL散度的惩罚项:L^{KLPEN}(θ) = E_t [ r_t(θ) * A_t - β * KL[π_θ_old || π_θ] ],并通过自适应调整系数 β 来控制约束强度。
在我经历的大多数项目中,PPO-Clip因其实现简单、效果稳定,成为了策略学习算法的首选。你几乎可以在任何主流的RL库(如Stable-Baselines3, Ray RLlib)中找到它的高效实现。它很好地平衡了采样效率、实现复杂度和训练稳定性。
4. 策略学习实战:以连续控制任务为例的完整流程
理论说了这么多,我们来看一个具体的实战例子:让一个机械臂(或一个模拟的机器人)学习将末端移动到目标位置。这是一个典型的连续状态、连续动作的控制问题。
4.1 环境与问题定义
我们使用MuJoCo或PyBullet物理仿真环境中的HalfCheetah(猎豹)或Ant(蚂蚁)机器人模型。当然,更简单的可以从OpenAI Gym的Pendulum-v1(倒立摆)开始。
- 状态空间(State Space): 连续。包括关节角度、角速度、末端执行器坐标等。例如在
Pendulum-v1中,状态是[cos(θ), sin(θ), θ_dot]。 - 动作空间(Action Space): 连续。每个关节的扭矩,范围通常在[-1, 1]或[-2, 2]之间。
- 奖励函数(Reward): 设计是关键。对于移动到目标点,奖励通常包含:到达目标的稀疏奖励(如+100)、每一步距离目标减少的稠密奖励、以及惩罚过大动作幅度的项(防止抖动)。例如:
reward = -distance_to_target - 0.1 * (action**2).sum()。
4.2 策略与价值网络设计
我们采用Actor-Critic框架,用神经网络同时近似策略函数和价值函数。
import torch import torch.nn as nn import torch.nn.functional as F class ActorNetwork(nn.Module): """策略网络(Actor),输出连续动作的高斯分布参数。""" def __init__(self, state_dim, action_dim, hidden_dim=256): super(ActorNetwork, self).__init__() self.fc1 = nn.Linear(state_dim, hidden_dim) self.fc2 = nn.Linear(hidden_dim, hidden_dim) self.mu_head = nn.Linear(hidden_dim, action_dim) # 均值 self.log_std_head = nn.Linear(hidden_dim, action_dim) # 对数标准差 def forward(self, state): x = F.relu(self.fc1(state)) x = F.relu(self.fc2(x)) mu = torch.tanh(self.mu_head(x)) * 2 # 假设动作范围[-2,2] log_std = self.log_std_head(x) log_std = torch.clamp(log_std, -20, 2) # 限制标准差范围,防止数值不稳定 std = torch.exp(log_std) return mu, std def sample_action(self, state): """根据状态采样一个动作,并返回其对数概率。""" mu, std = self.forward(state) dist = torch.distributions.Normal(mu, std) action = dist.rsample() # 使用rsample以支持重参数化技巧和反向传播 log_prob = dist.log_prob(action).sum(dim=-1) # 由于tanh变换,需要对概率进行修正(如果mu是tanh输出,此处简化处理) action = torch.tanh(action) # 确保动作在有效范围内 return action.detach(), log_prob class CriticNetwork(nn.Module): """价值网络(Critic),评估状态价值V(s)。""" def __init__(self, state_dim, hidden_dim=256): super(CriticNetwork, self).__init__() self.fc1 = nn.Linear(state_dim, hidden_dim) self.fc2 = nn.Linear(hidden_dim, hidden_dim) self.value_head = nn.Linear(hidden_dim, 1) def forward(self, state): x = F.relu(self.fc1(state)) x = F.relu(self.fc2(x)) value = self.value_head(x) return value4.3 训练循环与PPO-Clip实现核心
训练采用经典的“收集数据-更新网络”循环。我们使用广义优势估计(GAE)来更平滑地估计优势函数 A_t。
def compute_gae(rewards, values, next_value, dones, gamma=0.99, gae_lambda=0.95): """计算广义优势估计(GAE)。""" advantages = torch.zeros_like(rewards) gae = 0 for t in reversed(range(len(rewards))): if t == len(rewards) - 1: next_non_terminal = 1.0 - dones[t] next_values = next_value else: next_non_terminal = 1.0 - dones[t] next_values = values[t + 1] delta = rewards[t] + gamma * next_values * next_non_terminal - values[t] gae = delta + gamma * gae_lambda * next_non_terminal * gae advantages[t] = gae returns = advantages + values return advantages, returns # PPO-Clip 核心更新步骤(伪代码逻辑) def ppo_update(actor, critic, optimizer_actor, optimizer_critic, states, actions, old_log_probs, returns, advantages, clip_epsilon=0.2, value_coef=0.5, entropy_coef=0.01): # 将数据转换为Tensor states = torch.FloatTensor(states) actions = torch.FloatTensor(actions) old_log_probs = torch.FloatTensor(old_log_probs).detach() returns = torch.FloatTensor(returns).detach() advantages = torch.FloatTensor(advantages).detach() # 归一化优势,这是一个稳定训练的关键技巧 advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8) # 通常进行多轮(如10轮)小批量更新 for _ in range(10): # 重新计算当前策略下的动作概率和状态价值 mu, std = actor(states) dist = torch.distributions.Normal(mu, std) new_log_probs = dist.log_prob(actions).sum(dim=-1) entropy = dist.entropy().sum(dim=-1).mean() # 计算熵,用于鼓励探索 values = critic(states).squeeze() # 计算概率比 ratio = torch.exp(new_log_probs - old_log_probs) # PPO-Clip 目标函数 surr1 = ratio * advantages surr2 = torch.clamp(ratio, 1.0 - clip_epsilon, 1.0 + clip_epsilon) * advantages actor_loss = -torch.min(surr1, surr2).mean() - entropy_coef * entropy # Critic 损失(价值函数拟合) critic_loss = F.mse_loss(values, returns) # 更新网络 optimizer_actor.zero_grad() actor_loss.backward() torch.nn.utils.clip_grad_norm_(actor.parameters(), max_norm=0.5) # 梯度裁剪 optimizer_actor.step() optimizer_critic.zero_grad() critic_loss.backward() torch.nn.utils.clip_grad_norm_(critic.parameters(), max_norm=0.5) optimizer_critic.step()4.4 关键超参数与调试经验
策略学习,尤其是PPO,对超参数比较敏感。以下是一些经验值和个人调试心得:
- 折扣因子 γ: 通常在0.99左右。它决定了未来奖励的重要性。对于回合制任务,可以设为0.99或0.995;对于没有明确终止的持续任务,可能需要更接近1,如0.999。
- GAE参数 λ: 通常在0.95到0.98之间。λ=1等价于蒙特卡洛回报,λ=0等价于单步TD误差。0.95是一个很好的起点,它平衡了偏差和方差。
- 裁剪系数 ε: PPO-Clip的核心。通常设在0.1到0.3之间。0.2是最常用的默认值。调参心得:如果训练初期性能提升很快但很快崩溃,可能是ε太小,更新太激进,可以尝试增大到0.3。如果学习一直很慢,可能是ε太大限制了更新,可以尝试减小到0.1。
- 学习率: Actor和Critic的学习率需要分别设置。通常Critic的学习率是Actor的2-5倍。例如,Actor lr=3e-4, Critic lr=1e-3。使用学习率衰减策略(如线性衰减)通常有益。
- 熵系数: 用于鼓励探索。初始可以设一个较小的正值(如0.01)。随着训练进行,策略趋于确定,可以逐渐减小或衰减熵系数。一个常见技巧:观察训练过程中策略的熵值,如果熵下降过快,可能导致早熟收敛到次优解,此时可以适当增大熵系数或减缓其衰减速度。
- 批量大小与更新轮数: 每次从经验回放缓冲区(或直接收集的轨迹)中采样一个批次(如64, 128, 256)的数据,然后对这个批次的数据进行多轮(如5-10轮)的PPO更新。批次越大,梯度估计越准,但内存消耗越大。更新轮数(K)太多可能导致过拟合当前批次的数据。
踩坑记录:在训练一个机械臂抓取任务时,我曾遇到智能体很快学会快速抖动机械臂来获得微小但频繁的奖励(因为奖励函数设计有缺陷,包含了连续的正奖励),而完全忽略了真正的抓取目标。这就是典型的奖励函数设计不当导致的“奖励黑客”(Reward Hacking)问题。解决方案是重新设计奖励函数,大幅提高成功抓取的稀疏奖励,并减少或移除可能导致短视行为的稠密奖励项。奖励函数的设计是强化学习成功的一半,它需要精确地传达你的最终目标,而不是中间过程的某些容易钻空子的指标。
策略学习从最朴素的策略梯度出发,通过引入基线、构建Actor-Critic框架、再到使用PPO等信赖域方法约束更新步伐,一步步解决了高方差、采样效率低、训练不稳定等核心难题。它让我们能够直接优化复杂、高维甚至连续的动作策略,成为解决机器人控制、游戏AI、自动驾驶等复杂决策任务的利器。理解其背后的“为什么”——为什么需要梯度、为什么方差高、为什么要裁剪——远比记住公式更重要。在实际操作中,耐心地调试超参数、精心设计奖励函数、并利用TensorBoard等工具可视化训练过程(如回报曲线、价值损失、策略熵),是最终成功的关键。