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中的问题。

相关推荐
INGNIGHT11 分钟前
单词搜索 II · Word Search II
数据结构·c++·算法
黄卷青灯7732 分钟前
标定系数为什么会存储在相机模组里面,在标定的时候,算法是在割草机的X3板上运行的啊?
数码相机·算法·相机内参
螺丝钉的扭矩一瞬间产生高能蛋白1 小时前
PID算法基础知识
算法
HVACoder2 小时前
复习下线性代数,使用向量平移拼接两段线
c++·线性代数·算法
爱coding的橙子2 小时前
每日算法刷题Day77:10.22:leetcode 二叉树bfs18道题,用时3h
算法·leetcode·职场和发展
Swift社区2 小时前
LeetCode 404:左叶子之和(Sum of Left Leaves)
算法·leetcode·职场和发展
南枝异客2 小时前
查找算法-顺序查找
python·算法
QuantumLeap丶2 小时前
《数据结构:从0到1》-06-单链表&双链表
数据结构·算法
李牧九丶2 小时前
从零学算法59
算法
一匹电信狗3 小时前
【C++】手搓AVL树
服务器·c++·算法·leetcode·小程序·stl·visual studio