- 前言
- 第六章 DQN
- 表格化的Q-learning
- Deep Q-learning
- 和环境的交互
- SGD优化
- 步骤间的相关性
- 总结:规范的DQN流程
- 用DQN方法,解决Pong游戏
- 对Gym游戏的装饰
- DQN模型代码
- 经验池
- Agent
- 计算损失函数
- 主程序部分
重读《Deep Reinforcemnet Learning Hands-on》, 常读常新, 极其深入浅出的一本深度强化学习教程。 本文的唯一贡献是对其进行了翻译和提炼, 加一点自己的理解组织成一篇中文笔记。
原英文书下载地址: 传送门 原代码地址: 传送门
第六章 DQNDQN,其实是我看这本书的初衷, 大名鼎鼎的改变了强化学习领域的方法 。前一章中,我们熟悉了贝尔曼方程, 并介绍了值迭代方法。 虽然我们在FrokenLake游戏中, 很好地使用值迭代的方法解决了问题, 但在许多更复杂的情况下, 例如Atari游戏中, 状态的维度可能会爆炸——比如一张图片。 同时, 其中99%的状态可能只是在浪费时间——他们可能并不会出现在游戏中或很少出现。 总之, 这一系列问题使得看似成功的Q-learning方法很难找到适用的问题。 维度爆炸是其面临的最大挑战。
表格化的Q-learning在值迭代算法中, 我们真的需要关心那些几乎不出现的state吗? 因此, 我们其实可以省略掉这些值的迭代,或者说,我们更在意实际中出现的状态的动作Q值。 Q-learning就是这样的:
- 初始化一张空的Q表,行为状态, 列为动作, 每个格子代表当前状态时采用不同动作的Q值。
- 不断和环境交互, 得到s, a, r, s’ (状态,动作, 奖励, 新状态)。与环境的交互,我们一般采用这样的策略: 一定概率按当前已迭代的Q表进行决策, 一定概率按随机动作进行探索。
- 根据贝尔曼方程,更新Q值。 (利用上一步中获取的经验, 即s, a, r, s’) Q s , a ← r + γ max a ′ ∈ A Q s ′ , a ′ Q_{s, a} \leftarrow r+\gamma \max _{a^{\prime} \in A} Q_{s^{\prime}, a^{\prime}} Qs,a←r+γa′∈AmaxQs′,a′
- 一直重复步骤2,3, 完善Q表
为了训练更稳定, 我们也往往在第三步中采用如下的更新Q值方式: Q s , a ← ( 1 − α ) Q s , a + α ( r + γ max a ′ ∈ A Q s ′ , a ′ ) Q_{s, a} \leftarrow(1-\alpha) Q_{s, a}+\alpha\left(r+\gamma \max _{a^{\prime} \in A} Q_{s^{\prime}, a^{\prime}}\right) Qs,a←(1−α)Qs,a+α(r+γa′∈AmaxQs′,a′) 有一个 α \alpha α参数代表学习率, 当 α = 1 \alpha=1 α=1时, Q值完全更新为新计算得到的Q值。 否则, 将在保留一部分历史Q值的基础上,进行修改微调。
Deep Q-learning一如文章开头提到, 上一节中所说的Q-learning方法, 在面对复杂问题时显得非常挣扎——比如Atari游戏,存在的状态实在过多——而且均可能在游戏中出现,因此,用Q-learning直接去做,非常的复杂。 甚至在有些游戏如CartPole中, 状态的数目可能是无限的——因为部分参数值是连续值。
为了解决这个问题,我们可以换一种思路: 我们不再维护一张Q表, 并通过查表得到Q值。 相反, 我们试图得到一种非线性变换, 可以将输入的状态转变为相应动作的Q值。 许多读者可能已经猜到了, 这种在机器学习中很常见的“回归问题”,当下最流行的方法,就是使用神经网络来解决。 根据这一思路,我们提出了DQN的雏形:
- 基于一些初始的估计, 初始化Q网络 Q(s,a)
- 和环境交互, 获得 (s, a, r, s’)
- 计算损失值, L = ( Q s , a − ( r + γ max a ′ ∈ A Q s ′ , a ′ ) ) 2 \mathcal{L}=\left(Q_{s, a}-\left(r+\gamma \max _{a^{\prime} \in A} Q_{s^{\prime}, a^{\prime}}\right)\right)^{2} L=(Qs,a−(r+γa′∈AmaxQs′,a′))2
- 使用梯度下降法,优化Q网络, 降低损失值
- 从2步骤开始重复,直到收敛。
现在,传统Q-learning方法中的Q表,在这里就变成了神经网络, 其他部分其实是不变的——3和4步骤其实就是对应了Q-learning中的第3步——让更新的Q值(DQN中通过训练网络)尽可能满足贝尔曼方程。 这个算法看起来非常巧妙和简单, 但是,存在一些问题。
和环境的交互显然,我们需要和环境进行交互, 获取足够的经验。 但是,如果简单地用随机的策略进行与环境的交互, 很可能会得到许多无用的经验——FrokenLake例子还好, 比如Atari的Pong游戏(乒乓游戏),赢一球才能得分——如果随机出板的话, 基本没有战胜电脑得分的可能性,也就是说, 绝大部分的经验都是无效的。 因此, 为了获取更多有效的策略, 我们可以用自己正在训练的Q网络来进行决策。
这样的话也有一个弊端, 如果Q网络本身训练的不够好,或者只是个局部优解, 因此,我们也需要对环境进行一些尝试和探索。
常用的就是 ϵ \epsilon ϵ-greedy方法——每次与环境交互时,有 ϵ \epsilon ϵ概率随机选择动作, 1 − ϵ 1-\epsilon 1−ϵ的概率通过Q网络选择动作。 训练开始的时候, 我们会设定 ϵ = 1 \epsilon=1 ϵ=1,即随机选择动作,进行初始化的经验积累。 后面随着训练的推进, 逐渐降低 ϵ \epsilon ϵ值, 慢慢降低到 2 % ∼ 5 % 2\% \sim 5\% 2%∼5%。
SGD优化在DQN中,我们建立深度网络,而我们采用的优化方法也就是 最常用的 SGD, 随机梯度下降法。 但需要注意的是, SGD方法要求,训练样本应当满足 i.i.d.分布, 即independent and identically distributed, 独立同分布。
然而,在我们刚刚提出的算法中,无法满足这一条件:
- 我们的样本来自于同一局游戏,那么他们之间有很强的关联性。
- 我们的样本可能并不支撑我们训练最好的网络——样本并不来源于最优策略,而是通过 ϵ \epsilon ϵ-greedy策略获得。
为解决这一问题, 一种有效的方法叫 经验池 方法, 我们首先获取了大量的经验样本, 并将它们存在内存中(经验池中)。 然后在训练中, 我们每次从中取出一批样本,进行训练。同时,随着训练的推进,我们会不断更新新的经验, 而经验池的总大小是固定的,也就是说, 我们会加入新的经验,然后排出旧的经验。
经验池方法让我们可以尽可能地在不相关的数据上进行训练, 同时保持更新。
步骤间的相关性根据贝尔曼公式:Q(s,a)的价值其实通过Q(s’, a’)给出。 然而,s和s’之间只相隔一个步骤,这就使得他们非常相似,且网络很难区分。 为了使得Q(s,a)的值更接近想要的结果, 我们会间接地改变了Q(s’, a’)的值, 这使得训练极其不稳定, 就像自己追着自己的尾巴。
因此,为了使训练更加稳定, 我们采取的策略是使用两个网络——Target目标网络和真正的Q网络。 我们训练的时候是训练真正的Q网络, 而此时产生的标签Q值,则是通过Target网络得到。 Target网络是真正的Q网络的复制, 但是有一个同步的时间差——即每经过N步训练后, 将Q网络复制给Target网络进行同步。 一般会选择10000步之后。
总结:规范的DQN流程根据刚刚提到的DQN的挑战和相应的调整, 我们可以总结出规范的DQN流程:
- 随机初始化Q(s,a)网络和 Q ^ \hat{Q} Q^网络,清空经验池
- ϵ \epsilon ϵ的概率随机选择动作, 否则 a = a r g m a x a Q ( s , a ) a=\mathrm{argmax}_a Q(s,a) a=argmaxaQ(s,a)
- 执行动作a, 获得s’ 和 r
- 将 (s,a,r,s’)保存到经验池中。
- 从经验池中采用一组随机的经验 ( s i , a i , r i , s i ′ s_i,a_i,r_i,s'_i si,ai,ri,si′) i = 0 , . . . , n i=0, ...,n i=0,...,n
- 对于每一条经验,计算其标签值: y = r + γ max a ′ ∈ A Q ^ s ′ , a ′ y=r+\gamma \max _{a^{\prime} \in A} \hat{Q}_{s^{\prime}, a^{\prime}} y=r+γmaxa′∈AQ^s′,a′
- 计算损失函数值: L = ( Q s , a − y ) 2 \mathcal{L}=\left(Q_{s, a}-y\right)^{2} L=(Qs,a−y)2
- 用SGD方法,更新Q(s,a)网络来最小化损失值。
- 每N步后, Q ^ = Q \hat{Q}=Q Q^=Q
- 重复步骤2
代码详见 Chapter06/02_dqn_pong.py
由于pytorch版本的不同等原因, 有一些常见的bug,这里列出来,方便大家修改:
- 开头路径的修改 如果是直接clone的github库,路径上会报错, 在lib前加上Chapter06就行了。
#!/usr/bin/env python3
from Chapter06.lib import wrappers
from Chapter06.lib import dqn_model
- 类型报错
state_action_values = net(states_v).gather(1, actions_v.unsqueeze(-1)).squeeze(-1)
会报错。 这是因为类型不合,根据提示条件,如下修改即可:
b = actions_v.unsqueeze(-1).type(torch.LongTensor)
state_action_values = net(states_v).gather(1, b).squeeze(-1)
使用torch张量的内置方法 .type()
,可以直接将张量修改成需要的类型。
还有一个会报警告的问题: next_state_values[done_mask] = 0
会报这样的错误: UserWarning: indexing with dtype torch.uint8 is now deprecated, please use a dtype torch.bool instead.
在此之前插入一句类型转换即可:
done_mask = done_mask.bool()
next_state_values[done_mask] = 0
对Gym游戏的装饰
- 每局Gym游戏可以被切分成几个部分——比如某些游戏,玩家有多条命,可以将一个episode拆分成多个部分。
- 每K帧做一个动作决策——K经常取3或4, 可以理解为这3,4帧中, 重复了同一个动作。 这可以加速训练——因为用神经网络去处理每一帧会显得很耗时,而且提升极小。
- 取最新的两帧的每个像素点的最大值作为观测。可以有效解决Atari游戏偶尔的闪烁情况。
- 有些游戏要求玩家开始游戏时要按下一个“FIRE”button,这个显然可以让网络去学习去按。 但从方便角度出发,我们之间用Gym的装饰器来实现。
- 把每帧 210 × 160 210\times 160 210×160大小的三色图片, 缩小处理为 84 × 84 84\times 84 84×84的单色图片。
- 把几个连续帧叠加在一起给网络,让网络获得动态的游戏信息。
- 把不同游戏的奖励, 统一归一化到 -1到1之间。
- 0~255的颜色值, 会被归一化到0 ~1之间。
import torch
import torch.nn as nn
import numpy as np
class DQN(nn.Module):
def __init__(self, input_shape, n_actions):
super(DQN, self).__init__()
self.conv = nn.Sequential(
nn.Conv2d(input_shape[0], 32, kernel_size=8, stride=4),
nn.ReLU(),
nn.Conv2d(32, 64, kernel_size=4, stride=2),
nn.ReLU(),
nn.Conv2d(64, 64, kernel_size=3, stride=1),
nn.ReLU()
)
conv_out_size = self._get_conv_out(input_shape)
self.fc = nn.Sequential(
nn.Linear(conv_out_size, 512),
nn.ReLU(),
nn.Linear(512, n_actions)
)
def _get_conv_out(self, shape):
o = self.conv(torch.zeros(1, *shape))
return int(np.prod(o.size()))
def forward(self, x):
conv_out = self.conv(x).view(x.size()[0], -1)
return self.fc(conv_out)
使用pytorch库, 将整个网络分成了两部分:输入的图像先经过卷积网络,即self.conv
,然后通过self.conv(x).view(x.size()[0], -1)
,将卷积层提取的3维张量,展开成一维张量,也就是类似于tensorflow中Flatten层的工作。 最后,再通过定义的全连接层即可。最终输出的是一个一维张量,维度为动作总数, 每个值就代表对应动作的Q值。
训练的代码详见Github库, 这里具体说下几个重要的部分:
经验池Experience = collections.namedtuple('Experience', field_names=['state', 'action', 'reward', 'done', 'new_state'])
class ExperienceBuffer:
def __init__(self, capacity):
self.buffer = collections.deque(maxlen=capacity)
def __len__(self):
return len(self.buffer)
def append(self, experience):
self.buffer.append(experience)
def sample(self, batch_size):
indices = np.random.choice(len(self.buffer), batch_size, replace=False)
states, actions, rewards, dones, next_states = zip(*[self.buffer[idx] for idx in indices])
return np.array(states), np.array(actions), np.array(rewards, dtype=np.float32), \
np.array(dones, dtype=np.uint8), np.array(next_states)
首先,这里用collections.deque()
来创建经验池。collections.deque()
的特点是一个队列——有一个固定的长度,当经验池的大小超出队列长度时,会自动将最老的经验踢出,保持总长度不变。 其余用法类似列表。同时,经验池的每条经验用collections.namedtuple
命名元组来实现。 经验池通过append方法,往池里添加新经验。 最后,经验池实现了sample方法, 从经验池中提取一小组训练样本。
class Agent:
def __init__(self, env, exp_buffer):
self.env = env
self.exp_buffer = exp_buffer
self._reset()
def _reset(self):
self.state = env.reset()
self.total_reward = 0.0
def play_step(self, net, epsilon=0.0, device="cpu"):
done_reward = None
if np.random.random() args.reward:
print("Solved in %d frames!" % frame_idx)
break
if len(buffer)
关注
打赏
最近更新
- 深拷贝和浅拷贝的区别(重点)
- 【Vue】走进Vue框架世界
- 【云服务器】项目部署—搭建网站—vue电商后台管理系统
- 【React介绍】 一文带你深入React
- 【React】React组件实例的三大属性之state,props,refs(你学废了吗)
- 【脚手架VueCLI】从零开始,创建一个VUE项目
- 【React】深入理解React组件生命周期----图文详解(含代码)
- 【React】DOM的Diffing算法是什么?以及DOM中key的作用----经典面试题
- 【React】1_使用React脚手架创建项目步骤--------详解(含项目结构说明)
- 【React】2_如何使用react脚手架写一个简单的页面?