策略梯度算法reinforce算法原理理解和代码实现

策略梯度算法

在强化学习领域,早期有基于值函数的方法,如 Q-learning 等,这些方法通过估计状态-动作对的值函数来确定最优策略。然而,当动作空间是连续的或者非常大时,基于值函数的方法会面临一些挑战。REINFORCE 算法,也被称为策略梯度算法(Policy Gradient Algorithm),是一种基于策略的强化学习算法,REINFORCE 算法直接对策略进行优化,为解决这类问题提供了新的思路。

在一场游戏里面,我们把环境输出的状态 s 与策略输出的动作 a 全部组合起来,就是一个轨迹,即:

\begin{align}\tau = \{s_1, a_1, s_2, a_2, \cdots, s_t, a_t\}\end{align}

给定策略的参数 θ,我们可以计算某个轨迹 τ 发生的概率为(假设状态转移符合马尔可夫假设) :

\begin{align} p_{\theta}(\tau) &= p(s_1) p_{\theta}(a_1 | s_1) p(s_2 | s_1, a_1) p_{\theta}(a_2 | s_2) p(s_3 | s_2, a_2) \cdots \\ &= p(s_1) \prod_{t = 1}^{T} p_{\theta}(a_t | s_t) p(s_{t + 1} | s_t, a_t)  \end{align}

在某一场游戏的某一个回合里面,我们会得到 R(τ)。我们要做的就是调整策略的参数 θ,使得 R(τ) 的值越大越好。R(τ) 是一个随机变量,给定策略参数 θ,我们能够计算的是 R(τ) 的期望值:

\begin{align}\bar{R}_\theta = \sum_{\tau} R(\tau) p_\theta(\tau) = \mathbb{E}_{\tau \sim p_\theta(\tau)}[R(\tau)]\end{align}

我们现在的目标就是最大化R(τ)的期望奖励,可以采用梯度上升法来优化,要进行梯度上升,我们先要计算期望奖励$\bar{R}_\theta$的梯度:

\[ \begin{align} \nabla \bar{R}_\theta &= \sum_{\tau} R(\tau) \nabla p_\theta(\tau) \\ &= \sum_{\tau} R(\tau) p_\theta(\tau) \frac{\nabla p_\theta(\tau)}{p_\theta(\tau)} \\ &= \sum_{\tau} R(\tau) p_\theta(\tau) \nabla \log p_\theta(\tau) \\ &= \mathbb{E}_{\tau \sim p_\theta(\tau)} [R(\tau) \nabla \log p_\theta(\tau)] \end{align} \] 

实际上期望值 $\mathbb{E}_{\tau \sim p_\theta(\tau)}[R(\tau) \nabla \log p_\theta(\tau)]$ 无法计算,所以我们用采样的方式采样 N 个 τ 并计算每一个的值,把每一个的值加起来,就可以得到梯度:

\[ \begin{align} \mathbb{E}_{\tau \sim p_\theta(\tau)} [R(\tau) \nabla \log p_\theta(\tau)] &\approx \frac{1}{N} \sum_{n = 1}^{N} R(\tau^n) \nabla \log p_\theta(\tau^n) \\ &= \frac{1}{N} \sum_{n = 1}^{N} \sum_{t = 1}^{T_n} R(\tau^n) \nabla \log p_\theta(a_t^n \mid s_t^n)  \end{align} \] \[ \begin{align} \nabla \log p_\theta(\tau) &= \nabla \left( \log p(s_1) + \sum_{t = 1}^{T} \log p_\theta(a_t \mid s_t) + \sum_{t = 1}^{T} \log p(s_{t + 1} \mid s_t, a_t) \right) \\ &= \nabla \log p(s_1) + \nabla \sum_{t = 1}^{T} \log p_\theta(a_t \mid s_t) + \nabla \sum_{t = 1}^{T} \log p(s_{t + 1} \mid s_t, a_t) \\ &= \nabla \sum_{t = 1}^{T} \log p_\theta(a_t \mid s_t) \\ &= \sum_{t = 1}^{T} \nabla \log p_\theta(a_t \mid s_t) \end{align} \]

注意,$p(s_1)$ 和 $p(s_{t + 1}|s_t, a_t)$ 来自环境,$p_\theta(a_t|s_t)$ 来自智能体。$p(s_1)$ 和 $p(s_{t + 1}|s_t, a_t)$ 由环境决定,与 $\theta$ 无关,因此 $\nabla \log p(s_1) = 0$ ,$\nabla \sum_{t = 1}^T \log p(s_{t + 1}|s_t, a_t) = 0$。

因此可以得到R(τ)的期望奖励的最终梯度公式为:

\begin{align}
\nabla \bar{R}_\theta & = \frac{1}{N} \sum_{n = 1}^N \sum_{t = 1}^{T_n} R \big( \tau^n \big) \nabla \log p_\theta \big( a_t^n \mid s_t^n \big)
\end{align}

我们可以直观地理解,也就是在我们采样到的数据里面,采样到在某一个状态 \( s_t \) 要执行某一个动作 \( a_t \),\((s_t, a_t)\) 是在整个轨迹 \( \tau \) 的里面的某一个状态和动作的对。假设我们在 \( s_t \) 执行 \( a_t \),最后发现 \( \tau \) 的奖励是正的,我们就要增加在 \( s_t \) 执行 \( a_t \) 的概率。反之,如果在 \( s_t \) 执行 \( a_t \) 会导致 \( \tau \) 的奖励变成负的,我们就要减少在 \( s_t \) 执行 \( a_t \) 的概率

最终的参数更新公式为:

\begin{align}\theta \leftarrow \theta + \eta \nabla \bar{R}_\theta\end{align}

注意,一般策略梯度(policy gradient,PG) 采样的数据只会用一次,因为更新后策略的参数 θ变了,采样的轨迹的概率也会变,旧数据不适合用于新模型的训练

 

优化技巧

1. 添加基线

朴素的策略梯度是unbiased的,但是方差较大,可以减去基线b来减少方差,我们可以对 τ 的值取期望,计算 τ 的平均值,令 b ≈ E[R(τ)]。所以在训练的时候,我们会不断地把 R(τ) 的值记录下来,会不断地计 算 R(τ ) 的平均值,把这个平均值当作 b 来使用:

\begin{align} \nabla \bar{R}_\theta \approx \frac{1}{N} \sum_{n = 1}^{N} \sum_{t = 1}^{T_n} \left( R(\tau^n) - b \right) \nabla \log p_\theta \left( a_t^n \mid s_t^n \right) \end{align}

 

2. 给每一个动作分配合适的分数

朴素的策略梯度中,在同一个轨迹里所有动作的权重都是一样的,这显然是不合理的,因为在同一场游戏里面,也许有些动作是好的,有些动作是不好的,我们希望可以给每一个不同的动作前面都乘上不同的权重,每一个动作的不同权重反映了每一个动作到底是好的还是不好的。

原来的权重是整场游戏的奖励的总和,现在改成从某个时刻 t 开始,假设这个动作是在 t 开始执行的,从 t 一直到游戏结束所有奖励的总和才能代表这个动作的好坏:

\begin{align} \nabla \bar{R}_\theta \approx \frac{1}{N} \sum_{n = 1}^{N} \sum_{t = 1}^{T_n} \left( \sum_{t' = t}^{T_n} r_{t'}^n - b \right) \nabla \log p_\theta \left( a_t^n \mid s_t^n \right) \end{align}

接下来更进一步,我们把未来的奖励做一个折扣,因为虽然在某一时刻,执行某一个动作,会影响接下来所有的结果 ,但在一般的情况下,时间拖得越长,该动作的影响力就越小:

\begin{align} \nabla \bar{R}_\theta \approx \frac{1}{N} \sum_{n = 1}^{N} \sum_{t = 1}^{T_n} \left( \sum_{t' = t}^{T_n} \gamma^{t' - t} r_{t'}^n - b \right) \nabla \log p_\theta \left( a_t^n \mid s_t^n \right) \end{align}

可以把t时刻开始的累积折扣奖励记为 Gt,代入上式得到:

\begin{align} \nabla \bar{R}_\theta \approx \frac{1}{N} \sum_{n = 1}^{N} \sum_{t = 1}^{T_n} G_t^n \nabla \log \pi_\theta \left( a_t^n \mid s_t^n \right) \end{align}

\begin{align} G_t &= \sum_{k = t + 1}^{T} \gamma^{k - t - 1} r_k \\ &= r_{t + 1} + \gamma G_{t + 1} \end{align}

 

算法流程

 

代码实现

下面我们来实现用REINFORCE 算法控制CartPole游戏,CartPole-v0`是OpenAI Gym库中一个经典的强化学习环境,属于控制类问题。

游戏描述

在 CartPole-v0 环境里,有一个小车在无摩擦的轨道上,杆的一端通过无摩擦的关节连接在小车上,另一端则是自由的。目标是左右移动小车,让杆尽可能长时间地保持直立。

环境构成

  • 小车:可在水平轨道上左右移动。
  • 杆:通过关节连接在小车上,在重力作用下会倾倒。

状态空间

该环境的状态空间是一个四维的连续空间,每个维度代表的信息如下:
  1. 小车的位置:小车在轨道上的位置,是一个连续值。
  2. 小车的速度:小车在轨道上的移动速度,也是连续值。
  3. 杆的角度:杆相对于垂直方向的倾斜角度,为连续值。
  4. 杆的角速度:杆倾斜的速度,同样是连续值。

动作空间

动作空间是离散的,包含两个动作:
  • 0:表示小车向左移动。
  • 1:表示小车向右移动。

奖励机制

每一个时间步,只要杆保持直立,智能体就会获得 +1 的奖励。当杆的倾斜角度超过某个阈值(±12 度)、小车移出轨道边界(±2.4 个单位)或者达到最大时间步数(通常为 200 步)时,回合结束。

目标

智能体的目标是最大化整个回合内获得的总奖励,也就是让杆尽可能长时间地保持直立。如果智能体能够在 200 个时间步内都让杆保持直立,就可以认为它成功解决了这个问题。

代码

import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
import numpy as np
import gym
from torch.autograd import Variable
import matplotlib.pyplot as plt
import imageio

# 折扣因子,用于计算折扣回报
GAMMA = 0.99

# 定义策略网络类,继承自nn.Module
class PolicyNetwork(nn.Module):
    def __init__(self, num_inputs, num_actions, hidden_size, learning_rate=5e-4):
        # 调用父类的构造函数
        super(PolicyNetwork, self).__init__()

        # 记录动作的数量
        self.num_actions = num_actions
        # 定义第一个全连接层,输入维度为num_inputs,输出维度为hidden_size
        self.linear1 = nn.Linear(num_inputs, hidden_size)
        # 定义第二个全连接层,输入维度为hidden_size,输出维度为num_actions
        self.linear2 = nn.Linear(hidden_size, num_actions)
        # 定义Adam优化器,用于更新网络参数
        self.optimizer = optim.Adam(self.parameters(), lr=learning_rate)

    def forward(self, x):
        # 通过第一个全连接层
        x = self.linear1(x)
        # 使用ReLU激活函数
        x = F.relu(x)
        # 通过第二个全连接层
        x = self.linear2(x)
        # 使用Softmax函数将输出转换为动作概率分布
        return F.softmax(x, dim=1)

    def get_action(self, state):
        # 将NumPy数组的状态转换为PyTorch张量,并添加一个维度以匹配网络输入要求
        state = torch.from_numpy(state).float().unsqueeze(0)
        # 将状态包装为Variable(在较新版本的PyTorch中,Variable已可省略,但为了兼容旧代码保留)
        state = Variable(state)
        # 通过前向传播得到动作概率分布
        probs = self.forward(state)
        # 将概率分布从PyTorch张量转换为NumPy数组,并去除多余的维度
        probs_np = np.squeeze(probs.detach().numpy())
        # 根据概率分布随机选择一个动作
        action = np.random.choice(self.num_actions, p=probs_np)
        # 计算所选动作的对数概率
        log_prob = torch.log(probs.squeeze(0)[action])
        return action, log_prob

    def update_policy(self, rewards, log_probs):
        # 用于存储每个时间步的折扣回报
        discounted_rewards = []

        # 遍历每个时间步
        for t in range(len(rewards)):
            # 初始化折扣回报
            Gt = 0
            # 初始化折扣幂次
            pw = 0
            # 从当前时间步开始,计算后续所有奖励的折扣回报
            for r in rewards[t:]:
                Gt = Gt + GAMMA ** pw * r
                pw = pw + 1
            # 将计算得到的折扣回报添加到列表中
            discounted_rewards.append(Gt)
        # 将折扣回报列表转换为PyTorch张量
        discounted_rewards = torch.tensor(discounted_rewards)
        # 对折扣回报进行标准化处理,减去均值并除以标准差
        discounted_rewards = (discounted_rewards - discounted_rewards.mean()) / (
                discounted_rewards.std() + 1e-9)

        # 用于存储每个时间步的策略梯度
        policy_gradient = []
        # 遍历每个时间步的对数概率和折扣回报
        for log_prob, Gt in zip(log_probs, discounted_rewards):
            # 相当于最小化loss = -log_prob * Gt,通过深度学习框架自动优化
            policy_gradient.append(-log_prob * Gt)

        # 清空优化器中的梯度信息
        self.optimizer.zero_grad()
        # 将所有时间步的策略梯度堆叠成一个张量,并求和
        policy_gradient = torch.stack(policy_gradient).sum()
        # 进行反向传播,计算梯度
        policy_gradient.backward()
        # 使用优化器更新网络参数
        self.optimizer.step()

# 创建CartPole-v0环境,指定渲染模式为'rgb_array'
env = gym.make('CartPole-v0', render_mode='rgb_array')
# 初始化策略网络,输入维度为环境观测空间的维度4,输出维度为环境动作空间的维度2,隐藏层大小为128
policy_net = PolicyNetwork(env.observation_space.shape[0], env.action_space.n, 128) 

# 最大训练回合数
max_episode_num = 5000
# 每个回合的最大时间步数
max_steps = 1000
# 用于记录每个回合的步数
numsteps = []
# 用于记录每个回合的平均步数
avg_numsteps = []
# 用于记录每个回合的总奖励
all_rewards = []

# 开始训练循环
for episode in range(max_episode_num):
    # 重置环境,获取初始状态
    state, _ = env.reset()
    # 用于存储每个时间步的动作对数概率
    log_probs = []
    # 用于存储每个时间步的奖励
    rewards = []

    # 在每个回合内进行时间步循环
    for steps in range(max_steps):
        # 渲染环境,显示当前状态
        env.render()
        # 根据当前状态获取动作和动作的对数概率
        action, log_prob = policy_net.get_action(state)
        # 执行动作,获取新的状态、奖励、终止标志等信息
        new_state, reward, done, _, _ = env.step(action)
        # 将动作的对数概率添加到列表中
        log_probs.append(log_prob)
        # 将奖励添加到列表中
        rewards.append(reward)
        # 如果环境达到终止条件
        if done:
            # 调用更新策略的方法,更新网络参数
            policy_net.update_policy(rewards, log_probs)
            break
        # 更新当前状态为新的状态
        state = new_state

    # 计算当前回合的总奖励
    total_reward = sum(rewards)
    all_rewards.append(total_reward)
    if episode % 100 == 0:
        print(f"Episode {episode}: Total Reward = {total_reward}")

# 绘制训练损失曲线(使用总奖励曲线近似表示)
plt.plot(all_rewards)
plt.xlabel('Episode')
plt.ylabel('Total Reward')
plt.title('Training Loss Curve (Approximated by Total Reward)')
plt.savefig('training_loss_curve.png')
plt.show()

# 最后运行一次游戏并保存效果图
frames = []
state, _ = env.reset()
for steps in range(max_steps):
    frame = env.render()  # 去掉mode参数
    frames.append(frame)
    action, _ = policy_net.get_action(state)
    new_state, _, done, _, _ = env.step(action)
    if done:
        break
    state = new_state
env.close()

imageio.mimsave('game_play.gif', frames, fps=30)    
View Code

reward训练曲线:

最终效果:

 

参考资料

《Easy RL:强化学习教程》

遇强则强(一):REINFORCE

从头理解策略梯度(Policy Gradient)算法及定理

 
posted @ 2025-04-02 14:40  AI_Engineer  阅读(1093)  评论(1)    收藏  举报