【学习笔记】PyTorch实战 | 强化学习 | DQN | CartPole 倒立摆

简要声明


  1. 课程学习相关网址
    1. PyTorch Tutorials
    2. Reinforcement Learning (DQN) Tutorial
    3. Gymnasium Documentation
    4. Cart Pole
  2. 由于教程内容为英文,文本会进行谷歌翻译并保留部分英文专有名词,采用中文进行简要解释。
  3. 本学习笔记单纯是为了能对学到的内容有更深入的理解,如果有错误的地方,恳请包容和指正。

CartPole 倒立摆


在这里插入图片描述

  • Task:Agent 必须在两个动作(向左或向右移动推车)之间做出决定,以便连接到它的杆子保持直立。
  • 当 Agent 观察 Environment state 并选择 Action 时,Environment 会转换到新的 State,并返回 Action 的 Reward 。 在此 Task 中,每个增量 Timestep 的 Reward 为 +1,如果杆子倒下太远或小车偏离中心超过 2.4 个单位,环境就会终止。 这意味着性能更好的场景将运行更长的Duration,积累更大的回报。
  • CartPole 任务的设计使得 Agent 的 Inputs 是代表 Environment State(位置、速度等)的 4 个实值。 在不进行任何缩放的情况下获取这 4 个Inputs,并将它们传递到一个具有 2 个 Outputs的小型 Fully-connected network(全连接网络),每个 Outputs 对应一个 Action 。 Network 经过训练,可以在给定 Input state 的情况下预测每个 Action 的期望值。 然后选择具有最高期望值的Action。

环境配置


TyTorch

# create conda env
conda create -n py39pytorch201 python=3.9
conda info --envs
conda activate py39pytorch201

# install pytorch
conda install pytorch torchvision torchaudio cpuonly -c pytorch

# install jupyter notebook
conda install nb_conda -y

# install matplotlib
conda install matplotlib

具体安装过程可参考:【经验总结】NUC11的Ubuntu系统安装PyTorch | 无独显

Gymnasium

# gymnasium classic control env
pip install gymnasium[classic_control]

# install pygame
pip install pygame

# libstdcxx-ng update (这一步决定了是否能成功打开 pygame window)
conda install libstdcxx-ng=12.1.0 --channel conda-forge

Import

# 自2021年以来维护Gym的团队已将所有未来的开发转移到Gymnasium
import gymnasium as gym
import math
import random
import matplotlib
import matplotlib.pyplot as plt
from collections import namedtuple, deque
from itertools import count

import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F

# 这里一定要添加参数render_mode="human",不然无法打开pygame window
env = gym.make("CartPole-v1",render_mode="human")

# 设置 matplotlib
is_ipython = 'inline' in matplotlib.get_backend()
if is_ipython:
    from IPython import display

plt.ion()

# 如果可以使用GPU,由于本人没有GPU,所以使用CPU跑程序
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

Replay Memory


使用 Experience replay 来训练 DQN。 它存储 Agent 观察到的 Transitions,允许重用这些数据。 通过随机采样,构建 Batch 的 Transitions 是不相关的。 事实证明,这极大地稳定并改进了 DQN 训练过程。

Transition

Transition - 代表 Environment 中的单个 Transition 的命名元组。 它本质上将(state,action)对映射到它们的(next_state,reward)结果。

Transition = namedtuple('Transition',('state', 'action', 'next_state', 'reward'))

ReplayMemory

ReplayMemory - 有界大小的循环缓冲区,用于保存最近观察到的 Transitions。 它还实现了 .sample() 方法,用于选择随机 Batch 的 Transitions 进行训练。

class ReplayMemory(object):

    def __init__(self, capacity):
        self.memory = deque([], maxlen=capacity)

    def push(self, *args):
        # 保存一个transition
        self.memory.append(Transition(*args))

    def sample(self, batch_size):
        return random.sample(self.memory, batch_size)

    def __len__(self):
        return len(self.memory)

DQN algorithm


Policy - Greedy

  • Environment 是确定性的,因此为了简单起见,这里提出的所有方程也都是确定性的。 在强化学习文献中,它们还包含对 Environment 的随机 Transitions 的期望。
  • 我们的目标是训练一种试图最大化折扣累积 Reward 的政策

R t 0 = ∑ t = t 0 ∞ ( γ t − t 0 ⋅ r t ) R_{t_0}=\sum_{t=t_0}^{\infty}(\gamma^{t-t_0}\cdot r_t) Rt0=t=t0(γtt0rt)

  • 折扣 γ 应该是一个 0 和 1 之间常数,来确保总和收敛。较低的 γ 使得来自不确定的遥远未来的 Reward 对于 Agent 来说不如那些它可以相当有信心的不久的将来的 Reward 那么重要。它还鼓励 Agent 在更近的时间收集奖励,而不是在未来遥远的时间上收集同等 Agent。(说人话:贪心算法 - 只关心它目前最优的解决方案,不考虑未来的发展。)

Q-learning

如果有一个函数Q :State×Action→R,可以告诉我们的 Reward 是什么,如果在给定的状态下采取 Action,那么可以轻松地构建一个最大化我们的 Reward 的策略:

π ∗ ( s ) = arg max ⁡ a Q ∗ ( s , a ) \pi^*(s)=\argmax\limits_{a}Q^*(s,a) π(s)=aargmaxQ(s,a)

然而,我们并不了解世界的一切,因此我们无法使用 Q。但是,由于神经网络是通用函数逼近器,我们可以简单地创建一个并训练它来近似于 Q。对于训练更新规则,我们将使用这样一个事实:某些政策的每个Q函数遵循贝尔曼方程

Q π ( s , a ) = r + γ Q π ( s ′ , π ( s ′ ) ) Q^{\pi}(s,a)=r+\gamma Q^{\pi}(s',\pi(s')) Qπ(s,a)=r+γQπ(s,π(s))

等式两侧之间的差异称为时间差异误差 δ

δ = Q ( s , a ) − ( r + γ max ⁡ a Q ( s ′ , a ) ) \delta=Q(s,a)-(r+\gamma\max\limits_aQ(s',a)) δ=Q(s,a)(r+γamaxQ(s,a))

Huber loss

当误差较小时,Huber 损失的作用类似于均方误差,但当误差较大时,则类似于平均绝对误差 - 当 Q 的估计值非常嘈杂时,这使得它对异常值更加稳健。 我们通过从 Replay memory 中采样的一 Batch 的 Transitions (B) 来计算:

L = 1 ∣ B ∣ ∑ ( s , a , s ′ , r ) ∈ B L ( δ ) w h e r e L = { 1 2 δ 2 ∣ δ ∣ − 1 2 \mathcal{L}=\frac{1}{|B|}\sum_{(s,a,s',r)\in B}\mathcal{L}(\delta) \qquad where \qquad \mathcal{L}=\begin{cases} \frac{1}{2}\delta^2 \\ |\delta|-\frac{1}{2} \end{cases} L=B1(s,a,s,r)BL(δ)whereL={21δ2δ21

Q-network

模型将是一个 feed forward neural network (前馈神经网络),它吸收当前和之前的屏幕之间的差异。它有两个 Outputs,分别代表 Q(s,left) 和 Q(s,right) (其中 s 是 Network 的 Input)。 实际上,Network 正在尝试预测在给定当前 Input 的情况下采取每个 Action 的预期回报。

class DQN(nn.Module):

    def __init__(self, n_observations, n_actions):
        super(DQN, self).__init__()
				# 输入层/隐藏层/输出层 - 三层神经网络
        self.layer1 = nn.Linear(n_observations, 128)
        self.layer2 = nn.Linear(128, 128)
        self.layer3 = nn.Linear(128, n_actions)

    # 使用一个元素调用以确定下一个action,或者调用一个batch
    # 优化期间,返回 tensor([[left0exp,right0exp]...]).
    def forward(self, x):
        x = F.relu(self.layer1(x))
        x = F.relu(self.layer2(x))
        return self.layer3(x)

Training


Hyperparameters and utilities

BATCH_SIZE = 128 # 从重播缓冲区采样的transitions数量
GAMMA = 0.99 # γ 折扣系数
EPS_START = 0.9 # ε(epsilon)的起始值
EPS_END = 0.05 # ε(epsilon)的最终值
EPS_DECAY = 1000 # 控制ε(epsilon)的指数衰减速率,越高意味着衰减越慢
TAU = 0.005 # τ 目标网络的更新率
LR = 1e-4 # LR 是 AdamW 优化器的学习率

# 从gym的action空间获取actions数量
n_actions = env.action_space.n
# 获取state观测值的数量
state, info = env.reset()
n_observations = len(state)

# 创建policy的DQN网络和target的DQN网络
policy_net = DQN(n_observations, n_actions).to(device)
target_net = DQN(n_observations, n_actions).to(device)
target_net.load_state_dict(policy_net.state_dict())

# 创建优化其和存储器
optimizer = optim.AdamW(policy_net.parameters(), lr=LR, amsgrad=True)
memory = ReplayMemory(10000)

select_action - 将根据 ε-贪心算法选择一个 Action。 简而言之,有时会使用模型来选择Action,有时会统一采样一个。 选择随机 Action 的概率将从 EPS_START 开始,并以指数方式衰减至 EPS_END。 EPS_DECAY 控制衰减率。

ϵ t h r e s h o l d = ϵ s t a r t + ( ϵ s t a r t − ϵ e n d ) ∗ e − s t e p s d o n e ϵ d e c a y \epsilon_{threshold}=\epsilon_{start}+(\epsilon_{start}-\epsilon_{end})*e^{-\frac{steps_{done}}{\epsilon_{decay}}} ϵthreshold=ϵstart+(ϵstartϵend)eϵdecaystepsdone

steps_done = 0

def select_action(state):
    global steps_done
    sample = random.random()
    eps_threshold = EPS_END + (EPS_START - EPS_END) * \ math.exp(-1. * steps_done / EPS_DECAY)
    steps_done += 1
    if sample > eps_threshold:
        with torch.no_grad():
            # t.max(1) 将返回每行的最大列值。最大结果的第二列是找到最大元素的索引,因此选择具有较大预期reward的action。
            return policy_net(state).max(1)[1].view(1, 1)
    else:
        return torch.tensor([[env.action_space.sample()]], device=device, dtype=torch.long)

plot_durations - 用于绘制集的 duration (持续时间) 以及过去 100 集的平均值(官方评估中使用的衡量标准)的助手。 该图将位于包含主训练循环的单元格下方,并将在每集后更新。

def plot_durations(show_result=False):
    plt.figure(1)
    durations_t = torch.tensor(episode_durations, dtype=torch.float)
    if show_result:
        plt.title('Result')
    else:
        plt.clf()
        plt.title('Training...')
    plt.xlabel('Episode')
    plt.ylabel('Duration')
    plt.plot(durations_t.numpy())
    # 取 100 集的平均值并绘制它们
    if len(durations_t) >= 100:
        means = durations_t.unfold(0, 100, 1).mean(1).view(-1)
        means = torch.cat((torch.zeros(99), means))
        plt.plot(means.numpy())

    plt.pause(0.001)  # 暂停一下以便更新绘图
    if is_ipython:
        if not show_result:
            display.display(plt.gcf())
            display.clear_output(wait=True)
        else:
            display.display(plt.gcf())

Training loop

optimize_model 函数执行单步优化。 它首先对一个 batch 进行采样,将所有 tensors 连接成一个,计算 Q(s_t, a_t) 和 V(s_t+1) =max_a Q(s_t=1, a) ,并将它们组合到我们的损失中。 根据定义,如果 s 是终止状态,设置 V(s)=0。 还使用 target network 来计算 V(s_t+1) 以增加稳定性。target network 在每一步都会通过由先前定义的超参数 τ (TAU) 控制的软更新进行更新。

def optimize_model():
    if len(memory) < BATCH_SIZE:
        return
    transitions = memory.sample(BATCH_SIZE)
    # 转置batch - 这会将Transitions的batch-array转换为batch-array的Transitions
    batch = Transition(*zip(*transitions))

    # 计算非最终states的mask并连接batch元素(最终state将是模拟结束后的state)
    non_final_mask = torch.tensor(tuple(map(lambda s: s is not None,batch.next_state)), device=device, dtype=torch.bool)
    non_final_next_states = torch.cat([s for s in batch.next_state if s is not None])
    state_batch = torch.cat(batch.state)
    action_batch = torch.cat(batch.action)
    reward_batch = torch.cat(batch.reward)

    # 计算 Q(s_t,a) - 模型计算Q(s_t),然后选择所采取的action的列。 这些是根据policy_net对每个batch的state采取的action
    state_action_values = policy_net(state_batch).gather(1, action_batch)

    # 计算所有接下来的state的 V(s_{t+1})
		# non_final_next_states的action预期值是根据旧target_net计算的,使用 max(1)[0] 选择最佳reward。
		# 这是根据mask进行合并的,这样就会得到预期的state,或者在state为最终state时得到0
    next_state_values = torch.zeros(BATCH_SIZE, device=device)
    with torch.no_grad():
        next_state_values[non_final_mask] = target_net(non_final_next_states).max(1)[0]
    # 计算预期Q值
    expected_state_action_values = (next_state_values * GAMMA) + reward_batch

    # 计算Huber loss
    criterion = nn.SmoothL1Loss()
    loss = criterion(state_action_values, expected_state_action_values.unsqueeze(1))

    # 优化模型
    optimizer.zero_grad()
    loss.backward()
    # In-place gradient clipping(就地梯度裁剪)
    torch.nn.utils.clip_grad_value_(policy_net.parameters(), 100)
    optimizer.step()

一开始我们重置 environment 并获取初始 state 的 Tensor。 然后,我们对一个action进行采样,执行它,观察下一个state和reward(始终为 1),并优化我们的模型一次。 当集结束时(我们的模型失败),我们重新开始循环。

num_episodes = 600

for i_episode in range(num_episodes):
		# 初始化environment并获取其state
    state, info = env.reset()
    state = torch.tensor(state, dtype=torch.float32, device=device).unsqueeze(0)
    for t in count():
        action = select_action(state)
        observation, reward, terminated, truncated, _ = env.step(action.item())
        reward = torch.tensor([reward], device=device)
        done = terminated or truncated

        if terminated:
            next_state = None
        else:
            next_state = torch.tensor(observation, dtype=torch.float32, device=device).unsqueeze(0)

				# 将transition存储在memory中
        memory.push(state, action, next_state, reward)

        # 移动到下一个state
        state = next_state

        # 执行一步优化(在policy network上)
        optimize_model()

				# target network的weights的软更新 - θ′ ← τ θ + (1 −τ )θ′
        target_net_state_dict = target_net.state_dict()
        policy_net_state_dict = policy_net.state_dict()
        for key in policy_net_state_dict:
            target_net_state_dict[key] = policy_net_state_dict[key]*TAU + target_net_state_dict[key]*(1-TAU)
        target_net.load_state_dict(target_net_state_dict)

        if done:
            episode_durations.append(t + 1)
            plot_durations()
            break

print('Complete')
plot_durations(show_result=True)
plt.ioff()
plt.show()

Overall resulting data flow

在这里插入图片描述

Actions 可以随机选择,也可以根据 policy 选择,从 gym environment 中获取下一步样本。
将结果记录在 replay memory 中,并在每次迭代时运行优化步骤。
优化从 replay memory 中选择随机 batch 来训练新 policy。
旧的 target_net 也用于优化来计算预期 Q 值。 每一步都会对其 weights 进行软更新。

结果展示


在这里插入图片描述
在这里插入图片描述

最终CartPole倒立摆能持续使连接到它的杆子保持直立,说明其已经学会了如果稳定杆子直立。

问题解决


Jupyter: Cannot execute code, session has been disposed. Please try restarting the Kernel.

PyCharm: libGL error: MESA-LOADER: failed to open iris: /usr/lib/dri/iris_dri.so: cannot open shared object file: No such file or directory (search paths /usr/lib/x86_64-linux-gnu/dri:$${ORIGIN}/dri:/usr/lib/dri, suffix _dri) libGL error: failed to load driver: iris

Gymnasium: CartPole-v1渲染动画无法正常打开

上述均为同一个问题:conda中的libstdcxx-ng版本太低。

1.更新conda中的libstdcxx-ng

conda install libstdcxx-ng=12.1.0 --channel conda-forge

2.初始化env的时候需要加入另一个实参:render_mode

# 这里一定要添加参数render_mode="human",不然无法打开pygame window
env = gym.make("CartPole-v1",render_mode="human")

# env=gym.make("CartPole-v1",render_mode="rgb_array")也不会打开pygame window

3.conda初始化会自动安装pygame,若没有请自行安装

pip install pygame

4.gym已经不再维护,请安装gymnasium库,其兼容python3.9

# gymnasium classic control env
pip install gymnasium[classic_control]
  • 5
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Benjamin Chen.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值