4. 强化学习入门

好,咱们进入策略梯度家族的第一个成员——REINFORCE。说实话,这个算法在理论推导上非常优雅,但我在实际项目中第一次跑通它时,差点被方差问题搞到怀疑人生。别急,咱们一步步来。

为什么需要策略梯度?

之前咱们聊的值函数方法,比如Q-learning,本质上是“先评估动作好坏,再选最好的”。但你想过没有,如果动作空间是连续的,或者策略本身是随机性的,那这套思路就有点吃力了。

策略梯度方法换了个思路——直接优化策略本身。说白了,就是让智能体在“好”的动作上增加概率,在“坏”的动作上减少概率。不需要维护一个值函数,也不需要argmax操作。

核心思想:策略梯度方法直接对策略参数θ求梯度,目标是最大化期望累积回报 J(θ) = E[∑γᵗ rₜ]。

REINFORCE算法推导

REINFORCE是策略梯度里最基础的一个,也叫“蒙特卡洛策略梯度”。它的推导其实就一行核心公式:

∇J(θ) = E[ ∑ ∇log π(aₜ|sₜ, θ) · Gₜ ]

这里Gₜ是从t时刻开始的折扣回报。为什么会有log?我简单解释一下——因为我们要对策略的期望回报求导,而期望里面有个概率分布,取log是为了把乘法变成加法,方便采样估计。

我在做机器人力矩控制时,第一次看到这个公式觉得挺简单,但真正写代码时才发现:Gₜ的方差大得离谱。同一个任务跑十次,梯度方向可能都不一样。嗯,这个坑后面会讲怎么填。

算法流程

REINFORCE的执行步骤其实很直观:

  1. 采样:用当前策略π(θ)与环境交互,收集一条完整的轨迹 (s₁, a₁, r₁, s₂, a₂, r₂, ..., s_T)
  2. 计算回报:对每个时间步t,计算折扣回报 Gₜ = ∑_{k=t}^{T} γ^{k-t} rₖ
  3. 计算梯度:对每个时间步,计算 ∇log π(aₜ|sₜ, θ) · Gₜ,然后求和取平均
  4. 更新参数:θ ← θ + α · ∇J(θ)

个人经验:我建议在计算Gₜ时,先收集完整episode再统一算。在线更新虽然看起来更“实时”,但实际效果往往不如离线批量更新稳定。

从策略采样到梯度更新——代码实战

咱们直接上代码。这里我用PyTorch实现一个简单的REINFORCE,环境用CartPole,策略网络用两层MLP。

import torch
import torch.nn as nn
import torch.optim as optim
import gym
import numpy as np

class PolicyNetwork(nn.Module):
    def __init__(self, state_dim, action_dim, hidden_dim=128):
        super().__init__()
        self.fc = nn.Sequential(
            nn.Linear(state_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, action_dim),
            nn.Softmax(dim=-1)
        )
    
    def forward(self, x):
        return self.fc(x)

def compute_returns(rewards, gamma=0.99):
    returns = []
    G = 0
    for r in reversed(rewards):
        G = r + gamma * G
        returns.insert(0, G)
    return torch.tensor(returns, dtype=torch.float32)

def reinforce(env_name='CartPole-v1', num_episodes=1000, lr=1e-3, gamma=0.99):
    env = gym.make(env_name)
    state_dim = env.observation_space.shape[0]
    action_dim = env.action_space.n
    
    policy = PolicyNetwork(state_dim, action_dim)
    optimizer = optim.Adam(policy.parameters(), lr=lr)
    
    for episode in range(num_episodes):
        states, actions, rewards = [], [], []
        state, _ = env.reset()
        done = False
        
        # 采样一条完整轨迹
        while not done:
            state_tensor = torch.FloatTensor(state).unsqueeze(0)
            probs = policy(state_tensor)
            dist = torch.distributions.Categorical(probs)
            action = dist.sample().item()
            
            next_state, reward, terminated, truncated, _ = env.step(action)
            done = terminated or truncated
            
            states.append(state)
            actions.append(action)
            rewards.append(reward)
            state = next_state
        
        # 计算回报和梯度
        returns = compute_returns(rewards, gamma)
        returns = (returns - returns.mean()) / (returns.std() + 1e-8)  # 归一化
        
        loss = 0
        for t in range(len(states)):
            state_t = torch.FloatTensor(states[t]).unsqueeze(0)
            probs = policy(state_t)
            dist = torch.distributions.Categorical(probs)
            log_prob = dist.log_prob(torch.tensor(actions[t]))
            loss += -log_prob * returns[t]
        
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        
        if episode % 100 == 0:
            print(f'Episode {episode}, Total Reward: {sum(rewards):.2f}')
    
    env.close()
    return policy

# 运行
# policy = reinforce()

我曾经踩过的坑:回报归一化那一步千万别省。我第一次跑的时候没做归一化,梯度直接爆炸,loss变成NaN。后来加了 (returns - mean) / std,训练才稳定下来。

REINFORCE的优缺点

优点 缺点
推导简单,实现容易 方差大,收敛慢
适用于离散和连续动作空间 只能处理episodic任务
策略是随机性的,天然探索 样本利用率低
不需要值函数近似 对奖励尺度敏感

核心逻辑流程图

下面这张图展示了REINFORCE从采样到更新的完整流程,我建议你对照代码一起看:

REINFORCE算法流程 初始化策略网络θ 采样完整轨迹 (s,a,r) 计算折扣回报 Gₜ 计算 ∇logπ·Gₜ 并求和 θ ← θ + α·∇J(θ) 重复直到收敛 或达到最大episode数

关于基线(Baseline)的一点补充

REINFORCE最大的问题就是方差大。为什么?因为Gₜ本身波动很大,导致梯度估计不稳定。一个经典的改进是引入基线b(sₜ):

∇J(θ) = E[ ∑ ∇log π(aₜ|sₜ) · (Gₜ - b(sₜ)) ]

基线可以是任何与动作无关的函数,最常用的就是状态值函数V(sₜ)。减去基线后,梯度的期望不变,但方差会显著降低。我在做机械臂力矩控制时,加了基线之后训练曲线直接从“心电图”变成了“平滑下降”。

小技巧:如果你不想额外维护一个值网络,直接用Gₜ的滑动平均作为基线也能凑合。虽然不精确,但比没有强。

什么时候该用REINFORCE?

说实话,在2024年的今天,纯REINFORCE用得不多。但它作为策略梯度家族的“Hello World”,价值在于让你理解最核心的思想——用采样回报来调整策略参数。后面的Actor-Critic、PPO、SAC,本质上都是在解决REINFORCE的方差问题。

我个人建议:如果你刚接触策略梯度,先把这个算法跑通,观察一下方差带来的影响。等你亲眼看到loss曲线像过山车一样上下翻飞时,你才会真正理解为什么后来要引入那么多“花里胡哨”的改进。