Policy Gradient 极简教程

一、前言

Policy Gradient 是一种非常强大的强化学习算法,正如其名,Policy Gradient 通过直接对 Policy 本身计算梯度来实现学习的目的。

Policy Gradient 算法非常简洁,除了 Loss 的计算,其余和常规深度学习训练差别不大。今天我们聚焦 Loss 的实现(而非数学推导),介绍 Policy Gradient,并解决 CartPole 问题。

二、损失函数

Policy Gradient 的损失函数如下:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> L ( θ ) = − E [ l o g π ( a ∣ s ; θ ) × A ( s , a ) ] L(θ) = -E[log π(a|s; θ) × A(s,a)] </math>L(θ)=−E[logπ(a∣s;θ)×A(s,a)]

可以看到这个损失就是计算一个期望。期望内部分为两个部分,log π(a|s; θ)表示策略π(·;θ),在给定状态 s 时,执行动作 a 的概率的对数。而 A(s, a)表示在给定状态 s 的情况下,执行动作 a 的价值。

2.1 对数概率

首先要知道,策略π(·;θ)是一个输出概率分布的神经网络,假设动作空间为 3,则策略会输出 3 个概率值,且概率值和为一。

在我们从策略获取动作时,会伴随一个概率值p,对这个概率执行对数操作就是对数概率了。

下面用伪代码理解这个过程:

python 复制代码
# 根据当前状态,让策略返回动作的概率分布
action_probs = policy(state)
# 获取动作概率分布
dist = torch.distributions.Categorical(action_probs)
# 采样一个动作
action = dist.sample()
# 获取采样出来动作的对数概率
log_prob = log(action_probs[action])

2.2 动作价值

在一个游戏中,我们游玩的整个流程如下:

  1. 游戏开始,初始化为状态 s0
  2. 在状态 s0 下执行动作 a1,转换到状态 s1,并获得奖励 r1
  3. 在状态 s1 下执行动作 a2,转换到状态 s2,并获得奖励 r2
  4. 在状态 s2 下执行动作 a3,转换到状态 s3,并获得奖励 r3,并结束游戏

现在来明确一下,动作 a1 和哪些奖励有关?在执行 a1 后,我们陆续获得了 r1、r2、r3 三个奖励,其中 r1 是直接相关,而 r2、r3 是间接相关,并且 r2 与 a1 的相关性更高。

由此我们可以算出动作a1 的价值:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> A ( s 0 , a 1 ) = r 1 + γ ( r 2 + γ r 3 ) A(s0, a1) = r_1 + \gamma (r_2 + \gamma r_3) </math>A(s0,a1)=r1+γ(r2+γr3)

同理可以计算动作 a2、a3 的价值:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> A ( s 1 , a 2 ) = r 2 + γ r 3 A ( s 2 , a 3 ) = r 3 A(s1, a2) = r_2 + \gamma r_3 \\ A(s2, a3) = r_3 </math>A(s1,a2)=r2+γr3A(s2,a3)=r3

这部分计算代码如下:

python 复制代码
# 在完成一次游戏后,会得到n个rewards
rewards = [...]
discounted_rewards = []
R = 0
for r in reversed(rewards):
    R = r + GAMMA * R
    discounted_rewards.insert(0, R)

有了这两部分的理解,后面的内容就简单了。

三、Policy Gradient的实现

下面我们使用Policy Gradient解决 CartPole 问题。

整个流程非常简单,就是初始化环境和 Policy,然后利用现有的 Policy 玩一遍游戏,并收集 log_prob和 rewards,然后再利用已有数据更新 Policy,然后一直循环即可。

3.1 创建Policy网络

CartPole 是一个相对简单的问题,因此我们使用简单的 MLP 网络,代码如下:

python 复制代码
import torch
from torch import nn


class PolicyNetwork(nn.Module):
    def __init__(self, input_dim, output_dim):
        super(PolicyNetwork, self).__init__()
        self.fc1 = nn.Linear(input_dim, 128)
        self.fc2 = nn.Linear(128, output_dim)

    def forward(self, x):
        x = F.relu(self.fc1(x))
        return F.softmax(self.fc2(x), dim=-1)

这里的 input_dim 和 output_dim 稍后从环境中获取。

3.2 运行游戏

运行游戏需要环境和 Policy 两个东西,因此我们定义一个接收两个参数的函数 run_episode,另外更新 Policy 需要 log_probs 和 rewards,因此我们需要收集这两个东西并返回。完整代码如下:

ini 复制代码
def run_episode(env, policy):
    state = env.reset()[0]
    done = False
    log_probs = []
    rewards = []

    while not done:
        # 选择 action
        state_tensor = torch.FloatTensor(state).unsqueeze(0).to(device)
        action_probs = policy(state_tensor)
        dist = torch.distributions.Categorical(action_probs)
        action = dist.sample()

        # 执行 action
        next_state, reward, done, _, _ = env.step(action.item())

        # 计算 log prob
        log_prob = dist.log_prob(action)
        log_probs.append(log_prob)
        rewards.append(reward)

        state = next_state
    return log_probs, rewards

这里需要注意我们必须使用 Policy 来采样动作,否则收集的数据无法用于更新 Policy。

3.3 更新Policy

更新 Policy 则是将 rewards 转换成动作价值,然后代入 Policy Gradient 的损失函数即可。完整代码如下:

python 复制代码
def update_policy(optimizer, log_probs, rewards):
    discounted_rewards = []
    R = 0
    for r in reversed(rewards):
        R = r + GAMMA * R
        discounted_rewards.insert(0, R)

    # 归一化
    discounted_rewards = torch.FloatTensor(discounted_rewards).to(device)
    discounted_rewards = (discounted_rewards - discounted_rewards.mean()) / (discounted_rewards.std() + 1e-7)

    policy_loss = []
    for log_prob, reward in zip(log_probs, discounted_rewards):
        policy_loss.append(-log_prob * reward)

    optimizer.zero_grad()
    loss = torch.stack(policy_loss).sum()
    loss.backward()
    optimizer.step()

在这里我们计算动作价值后,对动作价值进行归一化,以稳定训练。

-log_prob * reward是计算损失的核心部分。常规情况下我们是执行梯度下降,而在 Policy Gradient 中,我们的损失函数说明了回报的期望,而我们的目的是希望回报最大化,因此这里使用负号改成梯度上升。

3.4 整合

最后,我们将整个流程整合:

ini 复制代码
import numpy as np
import torch
from torch import nn
import torch.nn.functional as F
import gymnasium as gym

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
if torch.backends.mps.is_available():
    device = torch.device("mps")
# 超参数
GAMMA = 0.99
LEARNING_RATE = 0.01
EPISODES = 3000
RENDER_EVERY = 100


...


def train():
    # env = gym.make('CartPole-v1', render_mode='human')
    env = gym.make('CartPole-v1')
    input_dim = env.observation_space.shape[0]
    output_dim = env.action_space.n
    
    # 初始化 Policy
    policy = PolicyNetwork(input_dim, output_dim).to(device)
    optimizer = torch.optim.Adam(policy.parameters(), lr=LEARNING_RATE)

    scores = []

    for episode in range(EPISODES):
        # 玩一轮游戏
        log_probs, rewards = run_episode(env, policy)
        # 更新Policy
        update_policy(optimizer, log_probs, rewards)
        
        score = sum(rewards)
        scores.append(score)

        print(f"Episode {episode + 1}, Score: {score:.2f}")
        
        # 当分数达到某个阈值时,认为已经解决了该问题
        if len(scores) > 50 and np.mean(scores[-50:]) > 200:
            print("Environment solved!")
            break

    env.close()
    torch.save(policy.cpu().state_dict(), 'policy.pth')


if __name__ == "__main__":
    train()

五、总结

Policy Gradient 是一种非常简洁且强大的强化学习算法,使用 Policy Gradient 可以很快解决 CartPole 问题。另外在只需修改几行代码的情况下,Policy Gradient 还可以解决其他许多问题,感兴趣的读者可以尝试用上述代码解决更多gymnasium中的问题。

相关推荐
YuTaoShao7 小时前
【LeetCode 热题 100】141. 环形链表——快慢指针
java·算法·leetcode·链表
小小小新人121238 小时前
C语言 ATM (4)
c语言·开发语言·算法
你的冰西瓜9 小时前
C++排序算法全解析(加强版)
c++·算法·排序算法
এ᭄画画的北北9 小时前
力扣-31.下一个排列
算法·leetcode
绝无仅有10 小时前
企微审批对接错误与解决方案
后端·算法·架构
用户50408278583911 小时前
1. RAG 权威指南:从本地实现到生产级优化的全面实践
算法
Python×CATIA工业智造12 小时前
详细页智能解析算法:洞悉海量页面数据的核心技术
爬虫·算法·pycharm
无聊的小坏坏13 小时前
力扣 239 题:滑动窗口最大值的两种高效解法
c++·算法·leetcode
黎明smaly13 小时前
【排序】插入排序
c语言·开发语言·数据结构·c++·算法·排序算法