“学习不是装满一桶水,而是点燃一把火。” —— 叶芝
我的博客主页: https://lizheng.blog.csdn.net
好的!我会按照你的要求,认真完成翻译任务,确保内容完整、准确且符合要求。以下是翻译后的 Markdown 文档:
强化学习(Reinforcement Learning, RL)的目标是训练智能体(agent),使其能够在环境中做出一系列决策,以最大化累积奖励。虽然基于价值的方法(如 Q-learning 和 DQN)会学习状态-动作对的价值,但基于策略的方法会直接学习策略,即从状态到动作(或动作概率)的映射。REINFORCE,也称为蒙特卡洛策略梯度,是一种基础的策略梯度算法。
REINFORCE 是一种直接学习参数化策略 π ( a ∣ s ; θ ) \pi(a|s; \theta) π(a∣s;θ) 的算法,而无需先显式学习一个价值函数。它的原理如下:
它被称为蒙特卡洛方法,因为它使用整个轨迹的完整回报 G t G_t Gt 来更新策略,而不是像 Q-learning 或 Actor-Critic 方法那样从估计值中进行引导(bootstrapping)。
策略梯度方法相比纯基于价值的方法(如 DQN)具有以下优势:
然而,像 REINFORCE 这样的基础策略梯度方法通常由于蒙特卡洛采样而导致梯度估计的方差较高,这可能导致收敛速度比 DQN 或 Actor-Critic 方法更慢或更不稳定。
REINFORCE 是理解更高级的策略梯度和 Actor-Critic 方法的基础。由于其高方差限制了其在复杂、大规模问题中的直接应用,相比最先进的算法,它更适合以下场景:
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 使用蒙特卡洛回报 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)]
在实践中,我们通过当前策略生成的样本(轨迹)来近似期望。
对于单个轨迹 τ \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)
这不会改变期望梯度,但可以显著降低其方差。
我们将使用与 DQN 示例相同的简单自定义网格世界环境来进行比较,并保持风格一致。
环境描述:
(row, col)
位置。表示为归一化向量 [row/10, col/10]
,用于网络输入。导入必要的库并设置环境。
# 导入用于数值计算、绘图和实用功能的库
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 = rows
self.cols: int = cols
self.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_state
return 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=True
if 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.state
next_row, next_col = current_row + dr, current_col + dc
# 默认步进成本
reward: float = -0.1
hit_wall: bool = False
# 检查该动作是否会导致移出边界
if not (0 <= next_row < self.rows and 0 <= next_col < self.cols):
# 保持在相同状态并受到惩罚
next_row, next_col = current_row, current_col
reward = -1.0
hit_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, done
def get_action_space_size(self) -> int:
"""
返回动作空间的大小。
返回:
int:可能的动作数量(4)。
"""
return self.action_dim
def 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
现在,让我们实现核心组件:策略网络、动作选择机制(采样)、回报计算和策略更新步骤。
我们使用 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_return
returns[t] = discounted_return
# 标准化回报(可选但通常有帮助)
if standardize:
mean_return = torch.mean(returns)
std_return = torch.std(returns) + 1e-8 # 添加小 epsilon 以防止除以零
returns = (returns - mean_return) / std_return
return 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 在自定义网格世界的超参数
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 学习曲线分析(自定义网格世界):
轨迹奖励(左图):
轨迹长度(中图):
轨迹损失(右图):
总体结论:
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.rows
cols: int = env.cols
policy_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 学习到的策略可视化:
通过可视化策略网格,我们可以直观地看到代理在每个状态下的最可能动作。从图中可以看出,策略在大部分状态下都指向目标位置(右下角),并且在靠近目标时,策略能够正确地引导代理避开墙壁并快速到达目标。
挑战 1:梯度估计的高方差
挑战 2:收敛速度慢
挑战 3:在线策略数据效率低
REINFORCE 是强化学习中一种基础的策略梯度算法。它通过根据轨迹中获得的完整折扣回报调整动作概率,直接优化参数化的策略。其核心优势在于概念简单,能够处理各种动作空间并学习随机策略。
正如在自定义网格世界中所展示的,REINFORCE 可以学习到有效的策略。然而,由于其蒙特卡洛梯度估计的固有高方差特性,其实际应用通常受到限制,可能导致不稳定或收敛速度慢。通过使用基线减法和回报标准化等技术可以缓解这一问题。REINFORCE 为理解更先进且广泛使用的策略梯度和 Actor-Critic 方法(如 A2C、A3C、DDPG、PPO、SAC)奠定了基础,这些方法在保持其核心原理的同时,解决了其局限性,尤其是在方差和样本效率方面。