PyTorch强化学习实战(27)------进化策略在强化学习中的应用
0. 前言
在本节中,我们将改变对强化学习 (Reinforcement Learning, RL)训练的视角,转而讨论黑盒优化方法,它们在大规模 RL 问题中具有适用性,并能与价值迭代和策略梯度方法竞争。尽管存在时间较长,这类方法在某些情境下仍然更具效率。具体而言,本节将介绍黑盒优化方法:进化策略。
1. 黑盒优化方法
首先,我们探讨整个黑盒方法类别及其与其他方法的区别。黑盒优化方法是解决优化问题的通用途径,它将待优化目标视为黑盒,无需考虑可微性、价值函数、目标平滑度等任何假设。这类方法唯一的要求是能够计算适应度函数,该函数应能评估当前优化实体特定实例的适用性。此类方法中最简单的例子是随机搜索:随机采样寻找目标(在强化学习 (Reinforcement Learning, RL)中即策略 π ( a ∣ s ) π(a|s) π(a∣s)),检验该候选方案的适应度,若结果足够好(符合某些奖励标准)则完成优化,否则持续重复此过程。尽管这种方法简单甚至略显朴素------尤其与复杂方法相比------但它能很好地阐释黑盒方法的核心思想。
更重要的是,通过一些改进,这种简单方法在效率与最终策略质量方面可与深度深度Q网络 (Deep Q-Network, DQN)和策略梯度方法相媲美。此外,黑盒方法还具有若干显著优势:
- 速度快:速度至少比基于梯度的方法快两倍,因为无需执行反向传播计算梯度
- 对优化目标和策略的假设非常少:对被视为黑盒的优化目标和策略几乎不做任何假设,传统方法难以处理奖励函数非平滑或策略包含随机选择步骤的情况,而这对黑盒方法完全不构成问题,因为它们对黑盒内部机制没有要求
- 易于并行化:例如前述随机搜索能轻松扩展到数千个中央处理器 (
Central Processing Unit,CPU) 或图形处理器 (Graphics Processing Unit,GPU) 并行工作,且彼此间无任何依赖。相比之下,DQN或策略梯度方法需要累积梯度并将当前策略传播至所有并行工作节点,这会降低并行效率
前述方法的主要缺点通常是样本效率较低。特别是对于参数规模达 50 万的神经网络策略而言,朴素随机搜索的成功概率极低。
2. 进化策略
进化策略 (Evolution Strategies, ES) 是黑盒优化方法的一个子集,其灵感源于进化过程。在 ES 中,最成功的个体对整体搜索方向具有最大影响力。该类包含多种不同方法,本节我们将重点讨论 Salimans 等人于 2017 年发表的论文《Evolution strategies as a scalable alternative to reinforcement learning》中采用的方法。
ES 方法的基本思想是:在每次迭代中,对当前策略参数执行随机扰动,并评估所得策略的适应度函数值。然后根据相对适应度函数值按比例调整策略权重。
Salimans 等人采用的具体方法称为协方差矩阵自适应进化策略 (Covariance Matrix Adaptation Evolution Strategy, CMA-ES)。该方法通过从均值为零、方差为单位矩阵的正态分布中采样随机噪声作为扰动,计算原始策略权重加上缩放噪声后的新策略适应度函数值,随后根据所得值调整原始策略权重------将噪声乘以适应度函数值后加至原权重,使策略向适应度函数值更高的权重方向移动。为了提高稳定性,权重的更新通过对包含不同随机噪声的批次步骤取平均值来实现。更形式化地,该方法可表述为以下步骤:
- 初始化学习率 α α α、噪声标准差 σ σ σ 和初始策略参数 𝜃 0 𝜃_0 𝜃0
- 对于t = 0, 1, ... 循环执行:
- 从均值为
0、方差为1的正态分布中采样与权重形状相同的噪声批次: ε 1 , ... , ε n ∼ N ( 0 , 1 ) ε_1, ..., ε_n\sim N(0, 1) ε1,...,εn∼N(0,1) - 计算返回值: F i = F ( θ t + σ ε i ) F_i = F(θ_t + σε_i) Fi=F(θt+σεi) ,其中 i = 1 , ... , n i = 1, ..., n i=1,...,n
- 更新权重:
θ t + 1 ← θ t + α 1 n σ ∑ i = 1 n F i ε i \theta_{t+1}\leftarrow\theta_t+\alpha\frac 1{n\sigma}\sum_{i=1}^nF_iε_i θt+1←θt+αnσ1i=1∑nFiεi
- 从均值为
该算法是论文所述方法的核心,但正如 RL 领域的常见情况,仅靠算法本身不足以获得良好结果。因此论文还包含若干改进技巧,但核心思想保持不变。
3. 在 CartPole 环境中实现进化策略
接下来,我们在 CartPole 环境中实现并测试论文中的方法。完整代码参见 cartpole_es.py。本节将使用单一环境来检验扰动后网络权重的适应度。我们的适应度函数将采用回合的未折扣总奖励。
(1) 首先导入必要的库:
python
import gymnasium as gym
import time
import numpy as np
import typing as tt
import torch
import torch.nn as nn
from torch.utils.tensorboard.writer import SummaryWriter
import common
由于完全不执行反向传播,我们不需要使用 PyTorch 优化器。事实上,我们完全可以避免使用 PyTorch 而仅依赖 NumPy,因为使用 PyTorch 的唯一目的是执行前向传播和计算网络输出。
(2) 接下来,定义超参数:
python
MAX_BATCH_EPISODES = 100
MAX_BATCH_STEPS = 10000
NOISE_STD = 0.001
LEARNING_RATE = 0.001
TNoise = tt.List[torch.Tensor]
超参数数量也很少,包含以下值:
MAX_BATCH_EPISODES和MAX_BATCH_STEPS:训练使用的回合数和步数上限NOISE_STD:用于权重扰动的噪声标准差 σ σ σLEARNING_RATE:训练步骤中调整权重的系数
我们还为包含权重噪声的张量列表定义了类型别名,这将简化代码处理噪声的逻辑。
(3) 定义网络:
python
class Net(nn.Module):
def __init__(self, obs_size: int, action_size: int):
super(Net, self).__init__()
self.net = nn.Sequential(
nn.Linear(obs_size, 32),
nn.ReLU(),
nn.Linear(32, action_size),
nn.Softmax(dim=1)
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.net(x)
我们使用的模型是一个简单的单隐藏层神经网络,它根据观测状态给出要采取的动作。此处使用 PyTorch 神经网络模块仅出于便利性考虑,因为我们只需要前向传播过程,但这完全可以被矩阵乘法和非线性变换所替代。
(3) evaluate() 函数使用给定策略运行完整回合,并返回总奖励和步数:
python
def evaluate(env: gym.Env, net: nn.Module, get_max_action: bool = True,
device: torch.device = torch.device('cpu')) -> tt.Tuple[float, int]:
obs, _ = env.reset()
reward = 0.0
steps = 0
while True:
obs_v = torch.FloatTensor(np.expand_dims(obs, 0)).to(device)
act_v = net(obs_v)
if get_max_action:
act = act_v.max(dim=1)[1].data.numpy()[0]
else:
act = act_v.data.cpu().numpy()[0]
obs, r, done, is_tr, _ = env.step(act)
reward += r
steps += 1
if done or is_tr:
break
return reward, steps
奖励值将作为适应度值使用,而步数计数用于限制我们组建批次所花费的时间。动作选择通过计算网络输出的 argmax 以确定性方式执行。原则上,我们可以从分布中进行随机采样,但通过对网络参数添加噪声已经实现了探索过程,因此这里使用确定性动作选择是可行的。
(4) 在 sample_noise() 函数中,我们创建与网络参数形状相同的零均值单位方差随机噪声:
python
def sample_noise(
net: nn.Module,
device: torch.device = torch.device('cpu')
) -> tt.Tuple[TNoise, TNoise]:
pos = []
neg = []
for p in net.parameters():
noise = np.random.normal(size=p.data.size())
pos.append(torch.FloatTensor(noise).to(device))
neg.append(torch.FloatTensor(-noise).to(device))
return pos, neg
该函数返回两组噪声张量:一组为正噪声,另一组为相同随机值但取负号。这两个样本将作为独立样本在批次中使用。这种技术称为镜像采样,用于提高收敛稳定性。事实上,若没有负噪声,收敛会变得非常不稳定,因为正噪声会将权重推向单一方向。
(5) eval_with_noise() 函数接收由 sample_noise() 创建的噪声数组,并评估添加噪声后的网络表现:
python
def eval_with_noise(env: gym.Env, net: nn.Module, noise: TNoise, noise_std: float,
get_max_action: bool = True, device: torch.device = torch.device("cpu")
) -> tt.Tuple[float, int]:
old_params = net.state_dict()
for p, p_n in zip(net.parameters(), noise):
p.data += noise_std * p_n
r, s = evaluate(env, net, get_max_action=get_max_action, device=device)
net.load_state_dict(old_params)
return r, s
为实现这一目标,我们将噪声添加到网络参数中,并调用 evaluate 函数获取奖励值和所采取步数。此后,需要通过加载网络的状态字典将权重恢复至原始状态。
(6) 该方法最后也是核心的函数是 train_step(),它接收包含噪声和对应奖励的批次,并通过以下公式计算网络参数的更新:
θ t + 1 ← θ t + α 1 n σ ∑ i = 1 n F i ε i \theta_{t+1}\leftarrow\theta_t+\alpha\frac 1{n\sigma}\sum_{i=1}^nF_iε_i θt+1←θt+αnσ1i=1∑nFiεi
具体实现如下:
python
def train_step(net: Net, batch_noise: tt.List[common.TNoise], batch_reward: tt.List[float],
writer: SummaryWriter, step_idx: int):
weighted_noise = None
norm_reward = np.array(batch_reward)
norm_reward -= np.mean(norm_reward)
s = np.std(norm_reward)
if abs(s) > 1e-6:
norm_reward /= s
首先,我们对奖励进行归一化处理(使其具有零均值和单位方差),这有助于提升方法的稳定性。接着,遍历批次中的每对(噪声,奖励)数据,将噪声值与归一化后的奖励相乘,并对策略中每个参数的对应噪声进行累加:
python
for noise, reward in zip(batch_noise, norm_reward):
if weighted_noise is None:
weighted_noise = [reward * p_n for p_n in noise]
else:
for w_n, p_n in zip(weighted_noise, noise):
w_n += reward * p_n
最后,使用累积的缩放后噪声来调整网络参数:
python
m_updates = []
for p, p_update in zip(net.parameters(), weighted_noise):
update = p_update / (len(batch_reward) * NOISE_STD)
p.data += LEARNING_RATE * update
m_updates.append(torch.norm(update))
writer.add_scalar("update_l2", np.mean(m_updates), step_idx)
从技术角度看,我们执行的是梯度上升操作,尽管梯度并非通过反向传播获得,而是通过随机采样(也称为蒙特卡洛采样)得到。Salimans 等人也证实了这一事实,他们指出 CMA-ES 与策略梯度方法非常相似,区别仅在于获取梯度估计的方式。
(7) 创建环境和网络:
python
if __name__ == "__main__":
writer = SummaryWriter(comment="-cartpole-es")
env = gym.make("CartPole-v1")
net = Net(env.observation_space.shape[0], env.action_space.n)
print(net)
**(8)**训练循环的每次迭代从创建批次开始,我们在此过程中采样噪声并获取正向和负向噪声对应的奖励:
python
step_idx = 0
while True:
t_start = time.time()
batch_noise = []
batch_reward = []
batch_steps = 0
for _ in range(MAX_BATCH_EPISODES):
noise, neg_noise = common.sample_noise(net)
batch_noise.append(noise)
batch_noise.append(neg_noise)
reward, steps = common.eval_with_noise(
env, net, noise, NOISE_STD)
batch_reward.append(reward)
batch_steps += steps
reward, steps = common.eval_with_noise(
env, net, neg_noise, NOISE_STD)
batch_reward.append(reward)
batch_steps += steps
if batch_steps > MAX_BATCH_STEPS:
break
当达到批次中的回合数上限或总步数上限时,我们停止收集数据并执行训练更新。
**(9)**为了执行网络的更新,调用 train_step() 函数:
python
step_idx += 1
m_reward = float(np.mean(batch_reward))
if m_reward > 199:
print("Solved in %d steps" % step_idx)
break
train_step(net, batch_noise, batch_reward, writer, step_idx)
train_step() 函数的目标是根据总奖励对噪声进行缩放,然后沿着平均噪声的方向调整策略权重。
**(10)**训练循环中的最后,指标写入 TensorBoard 并在控制台显示训练进度:
python
writer.add_scalar("reward_mean", m_reward, step_idx)
writer.add_scalar("reward_std", np.std(batch_reward), step_idx)
writer.add_scalar("reward_max", np.max(batch_reward), step_idx)
writer.add_scalar("batch_episodes", len(batch_reward), step_idx)
writer.add_scalar("batch_steps", batch_steps, step_idx)
speed = batch_steps / (time.time() - t_start)
writer.add_scalar("speed", speed, step_idx)
print("%d: reward=%.2f, speed=%.2f f/s" % (
step_idx, m_reward, speed))
直接运行程序(无需参数)即可开始训练:
shell
$ python3 cartpole_es.py

ES 通常需要 40-60 个批次才能解决 CartPole 问题。上述运行的收敛动态如下图所示:

可以看到,智能体能够在 30 秒内解决环境问题,这与交叉熵方法性能相当。
小结
本节介绍了进化策略 (Evolution Strategies, ES) 这一黑盒优化方法在强化学习中的应用。与传统梯度方法不同,ES 通过对策略参数添加随机噪声并评估适应度来估计更新方向,无需计算梯度,因此具有速度快、假设少、易于并行化等优势。我们在 CartPole 环境中实现了镜像采样与奖励归一化等改进技巧,实验表明 ES 能够在 30 秒内稳定解决该任务,效率与交叉熵方法相当。
系列链接
PyTorch强化学习实战(1)------强化学习(Reinforcement Learning,RL)详解
PyTorch强化学习实战(2)------强化学习环境库Gymnasium
PyTorch强化学习实战(3)------Gymnasium API扩展功能
PyTorch强化学习实战(4)------PyTorch基础
PyTorch强化学习实战(5)------PyTorch Ignite 事件驱动机制与实践
PyTorch强化学习实战(6)------交叉熵方法详解与实现
PyTorch强化学习实战(7)------表格学习与贝尔曼方程
PyTorch强化学习实战(8)------Q学习详解与实现
PyTorch强化学习实战(10)------强化学习高级组件
PyTorch强化学习实战(11)------N步DQN(N-step DQN)
PyTorch强化学习实战(12)------Double DQN(DDQN)
PyTorch强化学习实战(13)------噪声网络(NoisyNet-DQN)
PyTorch强化学习实战(14)------优先经验回放机制
PyTorch强化学习实战(15)------Dueling DQN
PyTorch强化学习实战(16)------Categorical DQN
PyTorch强化学习实战(17)------强化学习训练加速
PyTorch强化学习实战(18)------基于DQN处理股票交易问题
PyTorch强化学习实战(20)------优势演员-评论家(Advantage Actor-Critic, A2C)
PyTorch强化学习实战(21)------异步优势演员-评论家(Asynchronous Advantage Actor-Critic, A3C)
PyTorch强化学习实战(22)------将强化学习应用于TextWorld互动小说游戏
PyTorch强化学习实战(23)------强化学习在网页导航中的应用
PyTorch强化学习实战(24)------连续动作空间中的强化学习