🧠 向所有学习者致敬!
“学习不是装满一桶水,而是点燃一把火。” —— 叶芝
我的博客主页: https://lizheng.blog.csdn.net
🌐 欢迎点击加入AI人工智能社区!
🚀 让我们一起努力,共创AI未来! 🚀
好的!我会按照你的要求,认真完成翻译任务,确保内容完整、准确且符合要求。以下是翻译后的 Markdown 文档:
引言
强化学习(Reinforcement Learning, RL)的目标是训练智能体(agent),使其能够在环境中做出一系列决策,以最大化累积奖励。虽然基于价值的方法(如 Q-learning 和 DQN)会学习状态-动作对的价值,但基于策略的方法会直接学习策略,即从状态到动作(或动作概率)的映射。REINFORCE,也称为蒙特卡洛策略梯度,是一种基础的策略梯度算法。
文章目录
- 🧠 向所有学习者致敬!
- 🌐 欢迎[点击加入AI人工智能社区](https://bbs.csdn.net/forums/b8786ecbbd20451bbd20268ed52c0aad?joinKey=bngoppzm57nz-0m89lk4op0-1-315248b33aafff0ea7b)!
- 引言
- REINFORCE 是什么?
- 为什么选择策略梯度?
- REINFORCE 的应用场景和使用方式
- REINFORCE 的数学基础
- 策略梯度定理回顾(直觉)
- REINFORCE 的目标函数
- REINFORCE 的梯度估计器
- 计算折扣回报(蒙特卡洛)
- REINFORCE 的逐步解释
- REINFORCE 的关键组件
- 策略网络
- 动作选择(采样)
- 轨迹收集
- 折扣回报计算
- 损失函数(策略梯度目标)
- 超参数
- 实践示例:自定义网格世界
- 设置环境
- 创建自定义环境
- 实现 REINFORCE 算法
- 定义策略网络
- 动作选择(从策略中采样)
- 计算回报
- 优化步骤(策略更新)
- 运行 REINFORCE 算法
- 超参数设置
- 初始化
- 训练循环
- 可视化学习过程
- 分析学习到的策略(可选可视化)
- REINFORCE 中的常见挑战及解决方案
- 结论
REINFORCE 是什么?
REINFORCE 是一种直接学习参数化策略 π ( a ∣ s ; θ ) \pi(a|s; \theta) π(a∣s;θ) 的算法,而无需先显式学习一个价值函数。它的原理如下:
- 执行当前策略 π ( a ∣ s ; θ ) \pi(a|s; \theta) π(a∣s;θ),生成完整的经验轨迹(episode): ( s 0 , a 0 , r 1 , s 1 , a 1 , r 2 , . . . , s T ) (s_0, a_0, r_1, s_1, a_1, r_2, ..., s_T) (s0,a0,r1,s1,a1,r2,...,sT)。
- 对于轨迹中的每一步 t t t,计算从该步开始直到结束的总折扣回报 G t = ∑ k = t T γ k − t r k + 1 G_t = \sum_{k=t}^T \gamma^{k-t} r_{k+1} Gt=∑k=tTγk−trk+1。
- 使用梯度上升更新策略参数 θ \theta θ,以增加导致高回报 G t G_t Gt 的动作 a t a_t at 的概率,并减少导致低回报的动作的概率。
它被称为蒙特卡洛方法,因为它使用整个轨迹的完整回报 G t G_t Gt 来更新策略,而不是像 Q-learning 或 Actor-Critic 方法那样从估计值中进行引导(bootstrapping)。
为什么选择策略梯度?
策略梯度方法相比纯基于价值的方法(如 DQN)具有以下优势:
- 连续动作空间:它们可以自然地处理连续动作空间,而 DQN 主要用于离散动作。
- 随机策略:它们可以学习随机策略( π ( a ∣ s ) \pi(a|s) π(a∣s) 给出概率),在部分可观测环境或需要鲁棒性时非常有用。
- 概念上更简单(在某些方面):直接优化策略有时比估计价值函数更直接,尤其是当价值函数复杂时。
然而,像 REINFORCE 这样的基础策略梯度方法通常由于蒙特卡洛采样而导致梯度估计的方差较高,这可能导致收敛速度比 DQN 或 Actor-Critic 方法更慢或更不稳定。
REINFORCE 的应用场景和使用方式
REINFORCE 是理解更高级的策略梯度和 Actor-Critic 方法的基础。由于其高方差限制了其在复杂、大规模问题中的直接应用,相比最先进的算法,它更适合以下场景:
- 简单的强化学习基准问题:例如 CartPole、Acrobot 或自定义网格世界,这些场景的轨迹较短,方差可控。
- 学习随机策略:当需要概率性动作选择时。
- 教学目的:它为理解策略梯度学习的核心概念提供了一个清晰的入门。
REINFORCE 适用于以下情况:
- 目标是直接学习策略。
- 环境允许在更新之前生成完整的轨迹。
- 动作空间可以是离散的或连续的(尽管我们的示例使用离散动作)。
- 可以接受高方差的更新,或者可以通过基线(baseline)等方法进行管理(尽管这里没有实现)。
- 它是在线策略,即生成数据的策略与正在改进的策略相同。旧策略的数据不能轻易重用(与 DQN 的离线策略性质不同,DQN 使用重放缓冲区)。
REINFORCE 的数学基础
策略梯度定理回顾(直觉)
目标是找到策略参数 θ \theta θ,以最大化期望的总折扣回报,通常记为 J ( θ ) J(\theta) J(θ)。策略梯度定理提供了一种计算该目标关于策略参数的梯度的方法:
∇ θ J ( θ ) = E τ ∼ π θ [ ∑ t = 0 T ∇ θ log π θ ( a t ∣ s t ) Q π θ ( s t , a t ) ] \nabla_\theta J(\theta) = \mathbb{E}_{\tau \sim \pi_\theta} \left[ \sum_{t=0}^T \nabla_\theta \log \pi_\theta(a_t | s_t) Q^{\pi_\theta}(s_t, a_t) \right] ∇θJ(θ)=Eτ∼πθ[t=0∑T∇θlogπθ(at∣st)Qπθ(st,at)]
其中 τ \tau τ 是使用策略 π θ \pi_\theta πθ 采样的轨迹, Q π θ ( s t , a t ) Q^{\pi_\theta}(s_t, a_t) Qπθ(st,at) 是在策略 π θ \pi_\theta πθ 下的动作价值函数。
REINFORCE 的目标函数
REINFORCE 使用蒙特卡洛回报 G t = ∑ k = t T γ k − t r k + 1 G_t = \sum_{k=t}^T \gamma^{k-t} r_{k+1} Gt=∑k=tTγk−trk+1 作为 Q π θ ( s t , a t ) Q^{\pi_\theta}(s_t, a_t) Qπθ(st,at) 的无偏估计。梯度则变为:
∇ θ J ( θ ) = E τ ∼ π θ [ ∑ t = 0 T G t ∇ θ log π θ ( a t ∣ s t ) ] \nabla_\theta J(\theta) = \mathbb{E}_{\tau \sim \pi_\theta} \left[ \sum_{t=0}^T G_t \nabla_\theta \log \pi_\theta(a_t | s_t) \right] ∇θJ(θ)=Eτ∼πθ[t=0∑TGt∇θlogπθ(at∣st)]
我们希望对 J ( θ ) J(\theta) J(θ) 进行梯度上升。这相当于对负目标函数进行梯度下降,从而得到实现中常用的损失函数:
L ( θ ) = − E τ ∼ π θ [ ∑ t = 0 T G t log π θ ( a t ∣ s t ) ] L(\theta) = - \mathbb{E}_{\tau \sim \pi_\theta} \left[ \sum_{t=0}^T G_t \log \pi_\theta(a_t | s_t) \right] L(θ)=−Eτ∼πθ[t=0∑TGtlogπθ(at∣st)]
在实践中,我们通过当前策略生成的样本(轨迹)来近似期望。
REINFORCE 的梯度估计器
对于单个轨迹 τ \tau τ,梯度估计为 ∑ t = 0 T G t ∇ θ log π θ ( a t ∣ s t ) \sum_{t=0}^T G_t \nabla_\theta \log \pi_\theta(a_t | s_t) ∑t=0TGt∇θlogπθ(at∣st)。其中 ∇ θ log π θ ( a t ∣ s t ) \nabla_\theta \log \pi_\theta(a_t | s_t) ∇θlogπθ(at∣st) 通常被称为“资格向量”(eligibility vector)。它表示在参数空间中增加在状态 s t s_t st 下采取动作 a t a_t at 的对数概率的方向。这个方向通过回报 G t G_t Gt 进行缩放。如果 G t G_t Gt 很高,我们就会显著朝这个方向移动;如果 G t G_t Gt 很低(或为负),我们会远离这个方向。
计算折扣回报(蒙特卡洛)
在完成一个轨迹后,我们得到了奖励序列 r 1 , r 2 , . . . , r T r_1, r_2, ..., r_T r1,r2,...,rT,然后计算每个时间步 t t t 的折扣回报:
G t = r t + 1 + γ r t + 2 + γ 2 r t + 3 + . . . + γ T − t r T G_t = r_{t+1} + \gamma r_{t+2} + \gamma^2 r_{t+3} + ... + \gamma^{T-t} r_T Gt=rt+1+γrt+2+γ2rt+3+...+γT−trT
通常可以通过从轨迹的末尾向后迭代来高效计算:
G T = 0 G_T = 0 GT=0(假设 r T + 1 = 0 r_{T+1}=0 rT+1=0 或取决于问题设置)
G T − 1 = r T + γ G T G_{T-1} = r_T + \gamma G_T GT−1=rT+γGT
G T − 2 = r T − 1 + γ G T − 1 G_{T-2} = r_{T-1} + \gamma G_{T-1} GT−2=rT−1+γGT−1
……依此类推,直到 G 0 G_0 G0。
方差降低(基线):一种常见的技术(尽管在这个基础示例中没有实现)是从回报中减去一个依赖于状态的基线 b ( s t ) b(s_t) b(st)(通常是状态价值函数 V ( s t ) V(s_t) V(st)):
∇ θ J ( θ ) ≈ ∑ t ( G t − b ( s t ) ) ∇ θ log π θ ( a t ∣ s t ) \nabla_\theta J(\theta) \approx \sum_t (G_t - b(s_t)) \nabla_\theta \log \pi_\theta(a_t|s_t) ∇θJ(θ)≈t∑(Gt−b(st))∇θlogπθ(at∣st)
这不会改变期望梯度,但可以显著降低其方差。
REINFORCE 的逐步解释
- 初始化:策略网络 π ( a ∣ s ; θ ) \pi(a|s; \theta) π(a∣s;θ),带有随机权重 θ \theta θ,折扣因子 γ \gamma γ,学习率 α \alpha α。
- 对于每个轨迹:
a. 按照策略 π ( a ∣ s ; θ ) \pi(a|s; \theta) π(a∣s;θ) 生成完整的轨迹 τ = ( s 0 , a 0 , r 1 , s 1 , a 1 , . . . , s T − 1 , a T − 1 , r T , s T ) \tau = (s_0, a_0, r_1, s_1, a_1, ..., s_{T-1}, a_{T-1}, r_T, s_T) τ=(s0,a0,r1,s1,a1,...,sT−1,aT−1,rT,sT):
i. 对于 t = 0 , 1 , . . . , T − 1 t=0, 1, ..., T-1 t=0,1,...,T−1:
- 观察状态 s t s_t st。
- 从 π ( ⋅ ∣ s t ; θ ) \pi(\cdot | s_t; \theta) π(⋅∣st;θ) 中采样动作 a t a_t at。
- 执行 a t a_t at,观察奖励 r t + 1 r_{t+1} rt+1 和下一个状态 s t + 1 s_{t+1} st+1。
- 存储 s t , a t , r t + 1 s_t, a_t, r_{t+1} st,at,rt+1,以及 log π θ ( a t ∣ s t ) \log \pi_\theta(a_t | s_t) logπθ(at∣st)。
b. 计算回报:对于 t = 0 , 1 , . . . , T − 1 t=0, 1, ..., T-1 t=0,1,...,T−1:
- 计算折扣回报 G t = ∑ k = t T − 1 γ k − t r k + 1 G_t = \sum_{k=t}^{T-1} \gamma^{k-t} r_{k+1} Gt=∑k=tT−1γk−trk+1。
c. 更新策略:执行梯度上升(或对负目标函数进行梯度下降):
- 计算损失 L = − ∑ t = 0 T − 1 G t log π θ ( a t ∣ s t ) L = -\sum_{t=0}^{T-1} G_t \log \pi_\theta(a_t | s_t) L=−∑t=0T−1Gtlogπθ(at∣st)。
- 更新权重: θ ← θ + α ∇ θ J ( θ ) \theta \leftarrow \theta + \alpha \nabla_\theta J(\theta) θ←θ+α∇θJ(θ)(或使用优化器对 L L L 进行优化)。 - 重复:直到收敛或达到最大轨迹数。
REINFORCE 的关键组件
策略网络
- 核心函数逼近器。学习将状态映射到动作概率。
- 架构取决于状态表示(对于向量使用 MLP,对于图像使用 CNN)。
- 在隐藏层中使用非线性激活函数(如 ReLU)。
- 输出层通常使用 Softmax 激活函数,用于离散动作空间,以产生动作的概率分布。
动作选择(采样)
- 从策略网络 π ( a ∣ s ; θ ) \pi(a|s; \theta) π(a∣s;θ) 输出的概率分布中采样动作。
- 这种方法本身就提供了探索性。随着学习的进行,更好动作的概率会增加,从而导致更多的利用性。
- 需要存储所选动作的对数概率( log π ( a t ∣ s t ; θ ) \log \pi(a_t|s_t; \theta) logπ(at∣st;θ)),以便进行梯度计算。
轨迹收集
- REINFORCE 是在线策略且基于轨迹的。
- 它需要使用当前策略收集完整的轨迹(状态、动作、奖励序列),然后才能进行更新。
- 存储每个步骤的奖励、状态、动作和对数概率。
折扣回报计算
- 在一个轨迹完成后,计算每个时间步 t t t 的 G t G_t Gt。
- 该值表示从该点开始在该特定轨迹中实际收到的累积奖励。
损失函数(策略梯度目标)
- 通常是 − ∑ t G t log π ( a t ∣ s t ; θ ) -\sum_t G_t \log \pi(a_t|s_t; \theta) −∑tGtlogπ(at∣st;θ)。
- 最大化导致高回报的动作的概率。
- 通常会对回报 G t G_t Gt 进行标准化(减去均值,除以标准差),以稳定学习。
超参数
- 关键超参数包括学习率、折扣因子 γ \gamma γ 和网络架构。
- 性能可能对这些参数敏感,尤其是学习率,因为梯度估计的方差较高。
实践示例:自定义网格世界
我们将使用与 DQN 示例相同的简单自定义网格世界环境来进行比较,并保持风格一致。
环境描述:
- 网格大小:10x10。
- 状态:代理的
(row, col)
位置。表示为归一化向量[row/10, col/10]
,用于网络输入。 - 动作:4 个离散动作:0(上),1(下),2(左),3(右)。
- 起始状态:(0, 0)。
- 目标状态:(9, 9)。
- 奖励:
- 到达目标状态 (9, 9) 时 +10。
- 碰到墙壁(试图移出网格)时 -1。
- 其他步骤 -0.1(小成本,鼓励效率)。
- 终止:当代理到达目标或达到最大步数时,轨迹结束。
设置环境
导入必要的库并设置环境。
# 导入用于数值计算、绘图和实用功能的库
import numpy as np
import matplotlib.pyplot as plt
import random
import math
from collections import namedtuple, deque # Deque 在 REINFORCE 中可能不需要
from itertools import count
from typing import List, Tuple, Dict, Optional# 导入 PyTorch 用于构建和训练神经网络
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
from torch.distributions import Categorical # 用于采样动作# 设置设备,如果可用则使用 GPU,否则回退到 CPU
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"使用设备:{device}")# 设置随机种子以确保运行结果可复现
seed = 42
random.seed(seed) # Python 随机模块的种子
np.random.seed(seed) # NumPy 的种子
torch.manual_seed(seed) # PyTorch(CPU)的种子
if torch.cuda.is_available():torch.cuda.manual_seed_all(seed) # PyTorch(GPU)的种子# 为 Jupyter Notebook 启用内联绘图
%matplotlib inline
使用设备:cpu
创建自定义环境
我们重用了 DQN 笔记本中的完全相同的 GridEnvironment
类。这确保了可比性,并符合参考风格。
# 自定义网格世界环境(与 DQN 笔记本中的完全相同)
class GridEnvironment:"""一个简单的 10x10 网格世界环境。状态:(row, col),表示为归一化向量 [row/10, col/10]。动作:0(上),1(下),2(左),3(右)。奖励:到达目标 +10,碰到墙壁 -1,每步 -0.1。"""def __init__(self, rows: int = 10, cols: int = 10) -> None:"""初始化网格世界环境。参数:- rows (int): 网格的行数。- cols (int): 网格的列数。"""self.rows: int = rowsself.cols: int = colsself.start_state: Tuple[int, int] = (0, 0) # 起始位置self.goal_state: Tuple[int, int] = (rows - 1, cols - 1) # 目标位置self.state: Tuple[int, int] = self.start_state # 当前状态self.state_dim: int = 2 # 状态由 2 个坐标(row, col)表示self.action_dim: int = 4 # 4 个离散动作:上、下、左、右# 动作映射:将动作索引映射到 (row_delta, col_delta)self.action_map: Dict[int, Tuple[int, int]] = {0: (-1, 0), # 上1: (1, 0), # 下2: (0, -1), # 左3: (0, 1) # 右}def reset(self) -> torch.Tensor:"""将环境重置到起始状态。返回:torch.Tensor:初始状态作为归一化张量。"""self.state = self.start_statereturn self._get_state_tensor(self.state)def _get_state_tensor(self, state_tuple: Tuple[int, int]) -> torch.Tensor:"""将 (row, col) 元组转换为网络所需的归一化张量。参数:- state_tuple (Tuple[int, int]): 状态表示为元组 (row, col)。返回:torch.Tensor:归一化后的状态作为张量。"""# 将坐标归一化到 0 和 1 之间(根据 0 索引调整归一化)normalized_state: List[float] = [state_tuple[0] / (self.rows - 1) if self.rows > 1 else 0.0,state_tuple[1] / (self.cols - 1) if self.cols > 1 else 0.0]return torch.tensor(normalized_state, dtype=torch.float32, device=device)def step(self, action: int) -> Tuple[torch.Tensor, float, bool]:"""根据给定的动作执行一步。参数:action (int): 要执行的动作(0:上,1:下,2:左,3:右)。返回:Tuple[torch.Tensor, float, bool]:- next_state_tensor (torch.Tensor):下一个状态作为归一化张量。- reward (float):该动作的奖励。- done (bool):是否结束轨迹。"""# 如果已经到达目标状态,则返回当前状态,奖励为 0,done=Trueif self.state == self.goal_state:return self._get_state_tensor(self.state), 0.0, True# 获取该动作对应的行和列增量dr, dc = self.action_map[action]current_row, current_col = self.statenext_row, next_col = current_row + dr, current_col + dc# 默认步进成本reward: float = -0.1hit_wall: bool = False# 检查该动作是否会导致移出边界if not (0 <= next_row < self.rows and 0 <= next_col < self.cols):# 保持在相同状态并受到惩罚next_row, next_col = current_row, current_colreward = -1.0hit_wall = True# 更新状态self.state = (next_row, next_col)next_state_tensor: torch.Tensor = self._get_state_tensor(self.state)# 检查是否到达目标状态done: bool = (self.state == self.goal_state)if done:reward = 10.0 # 到达目标的奖励return next_state_tensor, reward, donedef get_action_space_size(self) -> int:"""返回动作空间的大小。返回:int:可能的动作数量(4)。"""return self.action_dimdef get_state_dimension(self) -> int:"""返回状态表示的维度。返回:int:状态的维度(2)。"""return self.state_dim
实例化自定义环境并验证其属性。
# 实例化 10x10 网格的自定义环境
custom_env = GridEnvironment(rows=10, cols=10)# 获取动作空间大小和状态维度
n_actions_custom = custom_env.get_action_space_size()
n_observations_custom = custom_env.get_state_dimension()# 打印环境的基本信息
print(f"自定义网格环境:")
print(f"大小:{custom_env.rows}x{custom_env.cols}")
print(f"状态维度:{n_observations_custom}")
print(f"动作维度:{n_actions_custom}")
print(f"起始状态:{custom_env.start_state}")
print(f"目标状态:{custom_env.goal_state}")# 重置环境并打印起始状态的归一化状态张量
print(f"(0,0) 的示例状态张量:{custom_env.reset()}")# 执行一个示例动作:向右移动(动作=3)并打印结果
next_s, r, d = custom_env.step(3) # 动作 3 对应向右移动
print(f"动作结果(动作=右):下一个状态={next_s.cpu().numpy()},奖励={r},结束={d}")# 再执行一个示例动作:向上移动(动作=0)并打印结果
# 这将碰到墙壁,因为代理在最上面一行
next_s, r, d = custom_env.step(0) # 动作 0 对应向上移动
print(f"动作结果(动作=上):下一个状态={next_s.cpu().numpy()},奖励={r},结束={d}")
自定义网格环境:
大小:10x10
状态维度:2
动作维度:4
起始状态:(0, 0)
目标状态:(9, 9)
(0,0) 的示例状态张量:tensor([0., 0.])
动作结果(动作=右):下一个状态=[0. 0.11111111],奖励=-0.1,结束=False
动作结果(动作=上):下一个状态=[0. 0.11111111],奖励=-1.0,结束=False
实现 REINFORCE 算法
现在,让我们实现核心组件:策略网络、动作选择机制(采样)、回报计算和策略更新步骤。
定义策略网络
我们使用 PyTorch 的 nn.Module
定义一个简单的多层感知机(MLP)。与 DQN 网络的主要区别在于输出层,它使用 nn.Softmax
产生动作概率。
# 定义策略网络架构
class PolicyNetwork(nn.Module):""" 用于 REINFORCE 的简单 MLP 策略网络 """def __init__(self, n_observations: int, n_actions: int):"""初始化策略网络。参数:- n_observations (int): 状态空间的维度。- n_actions (int): 可能的动作数量。"""super(PolicyNetwork, self).__init__()# 定义网络层(与 DQN 示例类似)self.layer1 = nn.Linear(n_observations, 128) # 输入层self.layer2 = nn.Linear(128, 128) # 隐藏层self.layer3 = nn.Linear(128, n_actions) # 输出层(动作对数几率)def forward(self, x: torch.Tensor) -> torch.Tensor:"""通过网络进行前向传播以获取动作概率。参数:- x (torch.Tensor): 表示状态的输入张量。返回:- torch.Tensor:输出张量,表示动作概率(经过 Softmax)。"""# 确保输入是浮点张量if not isinstance(x, torch.Tensor):x = torch.tensor(x, dtype=torch.float32, device=device)elif x.dtype != torch.float32:x = x.to(dtype=torch.float32)# 应用带有 ReLU 激活函数的层x = F.relu(self.layer1(x))x = F.relu(self.layer2(x))# 从输出层获取动作对数几率action_logits = self.layer3(x)# 应用 Softmax 以获取动作概率action_probs = F.softmax(action_logits, dim=-1) # 使用 dim=-1 以确保对批次通用return action_probs
动作选择(从策略中采样)
此函数通过从策略网络输出的概率分布中采样来选择动作。它还返回所选动作的对数概率,这是 REINFORCE 更新所需的。
# REINFORCE 的动作选择
def select_action_reinforce(state: torch.Tensor, policy_net: PolicyNetwork) -> Tuple[int, torch.Tensor]:"""通过从策略网络输出的分布中采样来选择动作。参数:- state (torch.Tensor):当前状态作为张量,形状为 [state_dim]。- policy_net (PolicyNetwork):用于估计动作概率的策略网络。返回:- Tuple[int, torch.Tensor]:- action (int):所选动作的索引。- log_prob (torch.Tensor):所选动作的对数概率。"""# 如果网络有 dropout 或 batchnorm 层,则确保其处于评估模式(这里可选)# policy_net.eval() # 从策略网络获取动作概率# 如果状态是单个实例 [state_dim],则添加批次维度 [1, state_dim]if state.dim() == 1:state = state.unsqueeze(0)action_probs = policy_net(state)# 创建一个动作的分类分布# 如果之前添加了批次维度,则通过 squeeze(0) 获取单个状态的概率m = Categorical(action_probs.squeeze(0)) # 从分布中采样一个动作action = m.sample()# 获取所采样动作的对数概率(用于梯度计算)log_prob = m.log_prob(action)# 如果需要,将网络恢复为训练模式# policy_net.train()# 返回动作索引(作为 int)及其对数概率(作为张量)return action.item(), log_prob
计算回报
此函数计算每个时间步 t t t 的折扣回报 G t G_t Gt,给定奖励列表。它可以选择性地标准化回报。
def calculate_discounted_returns(rewards: List[float], gamma: float, standardize: bool = True) -> torch.Tensor:"""计算每个时间步 $t$ 的折扣回报 $G_t$。参数:- rewards (List[float]):在轨迹中收到的奖励列表。- gamma (float):折扣因子。- standardize (bool):是否标准化(归一化)回报(减去均值,除以标准差)。返回:- torch.Tensor:包含每个时间步的折扣回报的张量。"""n_steps = len(rewards)returns = torch.zeros(n_steps, device=device, dtype=torch.float32)discounted_return = 0.0# 从后向前迭代奖励以计算折扣回报for t in reversed(range(n_steps)):discounted_return = rewards[t] + gamma * discounted_returnreturns[t] = discounted_return# 标准化回报(可选但通常有帮助)if standardize:mean_return = torch.mean(returns)std_return = torch.std(returns) + 1e-8 # 添加小 epsilon 以防止除以零returns = (returns - mean_return) / std_returnreturn returns
优化步骤(策略更新)
此函数在完成一个轨迹后执行策略更新。它使用收集到的对数概率和计算出的回报来计算损失并执行反向传播。
def optimize_policy(log_probs: List[torch.Tensor], returns: torch.Tensor, optimizer: optim.Optimizer
) -> float:"""使用 REINFORCE 更新规则对策略网络执行一步优化。参数:- log_probs (List[torch.Tensor]):在轨迹中采取的动作的对数概率列表。- returns (torch.Tensor):轨迹中每个时间步的折扣回报张量。- optimizer (optim.Optimizer):用于更新策略网络的优化器。返回:- float:轨迹的计算损失值。"""# 将对数概率堆叠成一个张量log_probs_tensor = torch.stack(log_probs)# 计算 REINFORCE 损失:- (returns * log_probs)# 我们希望最大化 $E[G_t \cdot \log(\pi)]$,因此最小化 $-E[G_t \cdot \log(\pi)]$# 对整个轨迹步骤求和loss = -torch.sum(returns * log_probs_tensor)# 执行反向传播和优化optimizer.zero_grad() # 清除之前的梯度loss.backward() # 计算梯度optimizer.step() # 更新策略网络参数return loss.item() # 返回损失值以便记录
运行 REINFORCE 算法
设置超参数,初始化策略网络和优化器,然后运行主训练循环。
超参数设置
为应用于自定义网格世界的 REINFORCE 算法定义超参数。
# REINFORCE 在自定义网格世界的超参数
GAMMA_REINFORCE = 0.99 # 折扣因子
LR_REINFORCE = 1e-3 # 学习率(通常低于 DQN,较为敏感)
NUM_EPISODES_REINFORCE = 1500 # REINFORCE 通常需要更多轨迹,因为方差较高
MAX_STEPS_PER_EPISODE_REINFORCE = 200 # 每个轨迹的最大步数
STANDARDIZE_RETURNS = True # 是否标准化回报
初始化
初始化策略网络和优化器。
# 重新实例化自定义 GridEnvironment
custom_env: GridEnvironment = GridEnvironment(rows=10, cols=10)# 获取动作空间大小和状态维度
n_actions_custom: int = custom_env.get_action_space_size() # 4 个动作
n_observations_custom: int = custom_env.get_state_dimension() # 2 个状态维度# 初始化策略网络
policy_net_reinforce: PolicyNetwork = PolicyNetwork(n_observations_custom, n_actions_custom).to(device)# 初始化策略网络的优化器
optimizer_reinforce: optim.Adam = optim.Adam(policy_net_reinforce.parameters(), lr=LR_REINFORCE)# 用于存储轨迹统计数据以便绘图的列表
episode_rewards_reinforce = []
episode_lengths_reinforce = []
episode_losses_reinforce = []
训练循环
在自定义网格世界环境中训练 REINFORCE 代理。注意与 DQN 的工作流程差异:我们需要先收集一个完整的轨迹,然后计算回报并更新策略。
print("开始在自定义网格世界上训练 REINFORCE...")# 训练循环
for i_episode in range(NUM_EPISODES_REINFORCE):# 重置环境并获取初始状态张量state = custom_env.reset()# 用于存储当前轨迹数据的列表episode_log_probs: List[torch.Tensor] = []episode_rewards: List[float] = []# --- 生成一个轨迹 ---for t in range(MAX_STEPS_PER_EPISODE_REINFORCE):# 根据当前策略选择动作并存储对数概率action, log_prob = select_action_reinforce(state, policy_net_reinforce)episode_log_probs.append(log_prob)# 在环境中执行动作next_state, reward, done = custom_env.step(action)episode_rewards.append(reward)# 转移到下一个状态state = next_state# 如果轨迹结束,则退出if done:break# --- 轨迹结束,现在更新策略 ---# 计算轨迹的折扣回报returns = calculate_discounted_returns(episode_rewards, GAMMA_REINFORCE, STANDARDIZE_RETURNS)# 执行策略优化loss = optimize_policy(episode_log_probs, returns, optimizer_reinforce)# 存储轨迹统计数据total_reward = sum(episode_rewards)episode_rewards_reinforce.append(total_reward)episode_lengths_reinforce.append(t + 1)episode_losses_reinforce.append(loss)# 定期打印进度(例如,每 100 个轨迹)if (i_episode + 1) % 100 == 0:avg_reward = np.mean(episode_rewards_reinforce[-100:])avg_length = np.mean(episode_lengths_reinforce[-100:])avg_loss = np.mean(episode_losses_reinforce[-100:])print(f"轨迹 {i_episode+1}/{NUM_EPISODES_REINFORCE} | "f"最近 100 个轨迹的平均奖励:{avg_reward:.2f} | "f"平均长度:{avg_length:.2f} | "f"平均损失:{avg_loss:.4f}")print("自定义网格世界训练完成(REINFORCE)。")
开始在自定义网格世界上训练 REINFORCE...
轨迹 100/1500 | 最近 100 个轨迹的平均奖励:0.31 | 平均长度:43.90 | 平均损失:-2.5428
轨迹 200/1500 | 最近 100 个轨迹的平均奖励:5.83 | 平均长度:21.42 | 平均损失:-1.5049
轨迹 300/1500 | 最近 100 个轨迹的平均奖励:6.93 | 平均长度:20.16 | 平均损失:-1.6836
轨迹 400/1500 | 最近 100 个轨迹的平均奖励:7.20 | 平均长度:19.39 | 平均损失:-1.2332
轨迹 500/1500 | 最近 100 个轨迹的平均奖励:7.34 | 平均长度:19.16 | 平均损失:-1.0108
轨迹 600/1500 | 最近 100 个轨迹的平均奖励:7.43 | 平均长度:19.23 | 平均损失:-1.1386
轨迹 700/1500 | 最近 100 个轨迹的平均奖励:7.66 | 平均长度:18.73 | 平均损失:-0.2648
轨迹 800/1500 | 最近 100 个轨迹的平均奖励:7.96 | 平均长度:18.52 | 平均损失:-0.4335
轨迹 900/1500 | 最近 100 个轨迹的平均奖励:7.93 | 平均长度:18.57 | 平均损失:0.6314
轨迹 1000/1500 | 最近 100 个轨迹的平均奖励:7.95 | 平均长度:18.42 | 平均损失:1.5364
轨迹 1100/1500 | 最近 100 个轨迹的平均奖励:7.87 | 平均长度:18.45 | 平均损失:2.0860
轨迹 1200/1500 | 最近 100 个轨迹的平均奖励:7.95 | 平均长度:18.42 | 平均损失:1.9074
轨迹 1300/1500 | 最近 100 个轨迹的平均奖励:7.91 | 平均长度:18.44 | 平均损失:1.6792
轨迹 1400/1500 | 最近 100 个轨迹的平均奖励:7.85 | 平均长度:18.63 | 平均损失:1.1213
轨迹 1500/1500 | 最近 100 个轨迹的平均奖励:7.74 | 平均长度:18.60 | 平均损失:1.5478
自定义网格世界训练完成(REINFORCE)。
可视化学习过程
绘制 REINFORCE 代理在自定义网格世界环境中的学习结果(奖励、轨迹长度)。
# 绘制 REINFORCE 在自定义网格世界的训练结果
plt.figure(figsize=(20, 4))# 奖励
plt.subplot(1, 3, 1)
plt.plot(episode_rewards_reinforce)
plt.title('REINFORCE 自定义网格:轨迹奖励')
plt.xlabel('轨迹')
plt.ylabel('总奖励')
plt.grid(True)
# 添加移动平均线
rewards_ma_reinforce = np.convolve(episode_rewards_reinforce, np.ones(100)/100, mode='valid')
if len(rewards_ma_reinforce) > 0: plt.plot(np.arange(len(rewards_ma_reinforce)) + 99, rewards_ma_reinforce, label='100-轨迹移动平均', color='orange')
plt.legend()# 长度
plt.subplot(1, 3, 2)
plt.plot(episode_lengths_reinforce)
plt.title('REINFORCE 自定义网格:轨迹长度')
plt.xlabel('轨迹')
plt.ylabel('步数')
plt.grid(True)
# 添加移动平均线
lengths_ma_reinforce = np.convolve(episode_lengths_reinforce, np.ones(100)/100, mode='valid')
if len(lengths_ma_reinforce) > 0:plt.plot(np.arange(len(lengths_ma_reinforce)) + 99, lengths_ma_reinforce, label='100-轨迹移动平均', color='orange')
plt.legend()# 损失
plt.subplot(1, 3, 3)
plt.plot(episode_losses_reinforce)
plt.title('REINFORCE 自定义网格:轨迹损失')
plt.xlabel('轨迹')
plt.ylabel('损失')
plt.grid(True)
# 添加移动平均线
losses_ma_reinforce = np.convolve(episode_losses_reinforce, np.ones(100)/100, mode='valid')
if len(losses_ma_reinforce) > 0:plt.plot(np.arange(len(losses_ma_reinforce)) + 99, losses_ma_reinforce, label='100-轨迹移动平均', color='orange')
plt.legend()plt.tight_layout()
plt.show()
REINFORCE 学习曲线分析(自定义网格世界):
-
轨迹奖励(左图):
- 代理在初期学习非常迅速,轨迹奖励在大约 150 个轨迹内迅速增加到接近最优水平。移动平均线确认了策略收敛到高奖励策略。然而,原始奖励在整个训练过程中仍然高度波动,这展示了由于使用噪声蒙特卡洛回报进行更新,基础 REINFORCE 算法的高方差特性。
-
轨迹长度(中图):
- 该图强烈证实了高效学习,与奖励曲线的趋势一致。轨迹长度在初期急剧下降,迅速收敛到一个稳定的接近最优平均值(10x10 网格中最短路径为 18 步)。这表明代理成功地学习了一致地找到通往目标状态的高效路径。
-
轨迹损失(右图):
- 策略梯度损失表现出极端的方差,直接反映了 REINFORCE 更新中使用的噪声蒙特卡洛回报估计。与 MSE 损失不同,它不会收敛到零,而是在初始学习阶段后趋于稳定。这种梯度估计的高方差是导致奖励曲线波动的主要原因。
总体结论:
REINFORCE 成功且迅速地解决了自定义网格世界任务,学习到了高效的策略以最大化奖励。图表清晰地展示了快速收敛的特性,但也突出了算法固有的高方差问题,尤其是在奖励信号和梯度估计方面。这种高方差是 REINFORCE 相比更先进的策略梯度或 Actor-Critic 方法的主要局限性。
分析学习到的策略(可选可视化)
我们将从 DQN 笔记本中改编策略网格可视化代码,以使用策略网络。它展示了每个状态的最可能动作(取策略输出的 argmax)。
def plot_reinforce_policy_grid(policy_net: PolicyNetwork, env: GridEnvironment, device: torch.device) -> None:"""绘制由 REINFORCE 策略网络导出的贪婪策略。注意:显示的是最可能的动作,而不是采样动作。参数:- policy_net (PolicyNetwork):训练好的策略网络。- env (GridEnvironment):自定义网格环境。- device (torch.device):设备(CPU/GPU)。返回:- None:显示策略网格图。"""rows: int = env.rowscols: int = env.colspolicy_grid: np.ndarray = np.empty((rows, cols), dtype=str)action_symbols: Dict[int, str] = {0: '↑', 1: '↓', 2: '←', 3: '→'}fig, ax = plt.subplots(figsize=(cols * 0.6, rows * 0.6))for r in range(rows):for c in range(cols):state_tuple: Tuple[int, int] = (r, c)if state_tuple == env.goal_state:policy_grid[r, c] = 'G'ax.text(c, r, 'G', ha='center', va='center', color='green', fontsize=12, weight='bold')else:state_tensor: torch.Tensor = env._get_state_tensor(state_tuple)with torch.no_grad():state_tensor = state_tensor.unsqueeze(0)# 获取动作概率action_probs: torch.Tensor = policy_net(state_tensor)# 选择最高概率的动作(贪婪动作)best_action: int = action_probs.argmax(dim=1).item()policy_grid[r, c] = action_symbols[best_action]ax.text(c, r, policy_grid[r, c], ha='center', va='center', color='black', fontsize=12)ax.matshow(np.zeros((rows, cols)), cmap='Greys', alpha=0.1)ax.set_xticks(np.arange(-.5, cols, 1), minor=True)ax.set_yticks(np.arange(-.5, rows, 1), minor=True)ax.grid(which='minor', color='black', linestyle='-', linewidth=1)ax.set_xticks([])ax.set_yticks([])ax.set_title("REINFORCE 学习到的策略(最可能的动作)")plt.show()# 绘制训练网络学习到的策略
print("\n绘制 REINFORCE 学习到的策略:")
plot_reinforce_policy_grid(policy_net_reinforce, custom_env, device)
REINFORCE 学习到的策略可视化:
通过可视化策略网格,我们可以直观地看到代理在每个状态下的最可能动作。从图中可以看出,策略在大部分状态下都指向目标位置(右下角),并且在靠近目标时,策略能够正确地引导代理避开墙壁并快速到达目标。
REINFORCE 中的常见挑战及解决方案
挑战 1:梯度估计的高方差
- 问题:使用完整的蒙特卡洛回报 G t G_t Gt 会使梯度估计变得嘈杂,因为一个轨迹中早期的一个好动作或坏动作可能会不当地影响所有前面动作的更新,即使这些动作与最终回报无关。
- 解决方案:
- 基线减法:从 G t G_t Gt 中减去一个依赖于状态的基线(如状态价值 V ( s t ) V(s_t) V(st)):更新公式为 ( G t − V ( s t ) ) ∇ log π (G_t - V(s_t)) \nabla \log \pi (Gt−V(st))∇logπ。这种方法不会改变梯度的期望值,但可以显著降低方差。不过,这需要学习 V ( s t ) V(s_t) V(st),从而引出了 Actor-Critic 方法。
- 标准化回报:在轨迹或批次内对回报进行归一化(减去均值,除以标准差)。这有助于稳定更新。
- 增加批次大小:在更新之前对多个轨迹的梯度进行平均(尽管这需要更多内存)。
挑战 2:收敛速度慢
- 问题:高方差和可能较小的学习步长会导致学习速度变慢。
- 解决方案:
- 调整学习率:仔细调整学习率至关重要。使用自适应学习率的优化器(如 Adam)可能会有所帮助。
- 使用基线:如上所述,降低方差可以加速收敛。
- Actor-Critic 方法:用从学习到的 critic(价值函数)中引导的 TD 误差代替蒙特卡洛回报 G t G_t Gt,从而实现更快、方差更低的更新(例如 A2C、A3C)。
挑战 3:在线策略数据效率低
- 问题:REINFORCE 必须在每次策略更新后丢弃数据,使其不如 DQN 等离线策略方法那样样本高效。
- 解决方案:
- 重要性采样:在离线策略策略梯度方法(如 PPO)中使用的技术可以在一定程度上重用旧数据,但会增加复杂性。
- 接受这一局限性:对于交互成本较低或问题较简单的情况,简单在线策略更新的优点可能更为突出。
结论
REINFORCE 是强化学习中一种基础的策略梯度算法。它通过根据轨迹中获得的完整折扣回报调整动作概率,直接优化参数化的策略。其核心优势在于概念简单,能够处理各种动作空间并学习随机策略。
正如在自定义网格世界中所展示的,REINFORCE 可以学习到有效的策略。然而,由于其蒙特卡洛梯度估计的固有高方差特性,其实际应用通常受到限制,可能导致不稳定或收敛速度慢。通过使用基线减法和回报标准化等技术可以缓解这一问题。REINFORCE 为理解更先进且广泛使用的策略梯度和 Actor-Critic 方法(如 A2C、A3C、DDPG、PPO、SAC)奠定了基础,这些方法在保持其核心原理的同时,解决了其局限性,尤其是在方差和样本效率方面。