摘要
《Playing Atari with Deep Reinforcement Learning》是首篇将强化学习与深度学习结合起来的深度强化学习经典论文,由DeepMind团队设计开发,算法在Atari 2600 游戏环境进行测试,在部分游戏中的测试表现优于人类玩家。
论文网址:https://paperswithcode.com/paper/playing-atari-with-deep-reinforcement
01 用Pycharm创建虚拟环境项目
项目代码和训练结果上传到百度网盘了,可以先下载下来,但是由于虚拟环境太大了所以没有上传,需要自己下载安装一遍,具体操作可以查看下文介绍。
链接:https://pan.baidu.com/s/1zoh0glqH4xcNSbOUuR2r7g?pwd=00wd
提取码:00wd
首先使用Pycharm创建一个新项目,然后如下图所示在设置中添加虚拟环境:
创建虚拟环境项目的目的在于使当前项目的运行环境与自己的Python环境分开,后续会在虚拟环境中安装需要的包,以免影响自己之前的Python环境。我用的Pycharm版本是2019版的,新版Pycharm的设置应该是类似的,可以根据自身情况百度。每个人的Anaconda路径不同,需要根据自己安装位置选择基本解释器。
虚拟环境的配置参考CSDN文章:Pycharm 创建并管理虚拟环境。
虚拟环境创建完成后,还需要在设置里面把终端程序设置一下:
这个时候打开Pycharm下面的终端选项卡,可以看到终端前面提示(venv),表示当前终端是处于虚拟环境中的:
这个时候我们需要的包都可以在这个终端这里通过pip进行安装了。
记得把从百度云下载的文件code、Imgs、model这三个文件夹给复制到当前的项目文件夹里面。项目需要的Python包已经包含在code文件夹下的requirements.tx文件中了,打开Pycharm的终端选项卡,通过cd命令进入code文件夹:
cd code
然后pip安装需要的包:
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
正常来讲上面的环境配置完之后,code文件夹下的代码应该都可以正常运行了。如果无法正常运行,有可能是Atari的游戏环境的问题,具体可以参考这篇CSDN文章:【gym】新版安装(0.21以上)以及配置Atari环境,超简单(Windows)。
02 论文模型解释
简单来讲,论文设计了一个DQN网络,将连续4帧、裁切为84X84的游戏画面堆叠成4X84X84的输入,然后通过卷积+ReLU、卷积+ReLU、Flatten、全连接+ReLU、全连接得到与动作维数相对应的输出。这里主要对 BreakOut (弹球打方块) 这款游戏进行了训练和测试,这款游戏对应的动作有4种,所以这里的输出维数为4。
输出的4维数组,分别代表4种动作对应的Q(s,a)值,选择最大的Q值所对应编号作为网络所输出的动作代号:
0:表示不移动
1:表示开始游戏 (如果游戏已经开始,那么1仍然不移动)
2:表示右移
3:表示左移
卷积的大小计算:
输出大小 = (输入大小 - 卷积核大小 + 2 x padding) / 步长 + 1
03 昇思MindSpore代码实现
打开 code文件夹中 playing_atari.py文件,代码的具体含义如下:
3.1 游戏环境创建
在导入相应的库之后,首先创建游戏环境env:
env = gym.make("BreakoutNoFrameskip-v4") # 游戏环境
env = gym.wrappers.RecordEpisodeStatistics(env)
env = gym.wrappers.ResizeObservation(env, (84, 84)) # 设置图片放缩
env = gym.wrappers.GrayScaleObservation(env) # 设置图片为灰度图
env = gym.wrappers.FrameStack(env, 4) # 4帧图片堆叠在一起作为一个观测
env = MaxAndSkipEnv(env, skip=4) # 跳帧,一个动作维持4帧
这里已经对
env
环境进行了封装,对其输出的图片进行了预处理,每一次的观测输出都是4X84X84的堆叠的灰度图片。
3.2 DQN网络定义
利用昇思MindSpore定义DQN网络,直接利用nn.SequentialCell(),按设计的网络进行定义即可:
class DQN(nn.Cell):
def __init__(self, nb_actions):
super().__init__()
self.network = nn.SequentialCell(
nn.Conv2d(in_channels=4, out_channels=16, kernel_size=8, stride=4, pad_mode='valid'),
nn.ReLU(),
nn.Conv2d(in_channels=16, out_channels=32, kernel_size=4, stride=2, pad_mode='valid'),
nn.ReLU(),
nn.Flatten(),
nn.Dense(in_channels=2592, out_channels=256),
nn.ReLU(),
nn.Dense(in_channels=256, out_channels=nb_actions),
)
def construct(self, x):
return self.network(x / 255.)
construct() 表示网络的输出,类似于Pytorch框架里面的forward()
3.3 设计经验存放池
class ReplayBuffer():
def __init__(self, replay_memory_size):
def add(self, obs, next_obs, action, reward, done):
def sample(self, sample_num):
...
return Tensor(temp_obs, ms.float32), Tensor(temp_next_obs, ms.float32), Tensor(temp_action, ms.int32), Tensor(temp_reward, ms.float32), Tensor(temp_done, ms.float32)
这里不贴出具体代码了,简单来说就是实现了经验元组的保存,以及批量采样方便用于后续神经网络的训练。
3.4 损失函数、优化器、训练函数的定义
首先对定义的DQN类实例化一个网络
q_network
,然后定义优化器为nn.Adam
,定义损失函数为nn.HuberLOss()
q_network = DQN(nb_actions=env.action_space.n) # 网络实例化
optimizer = nn.Adam(params=q_network.trainable_params(), learning_rate=1.25e-4) # 优化器
loss_fn = nn.HuberLoss() # 损失函数
后面是昇思MindSpore定义网络训练时特有的步骤,叫函数式自动微分,可以参考官网关于函数式自动微分的教程。具体而言就是先定义一个Loss计算函数forward_fn
,然后根据Loss计算函数生成梯度计算函数grad_fn
,然后利用梯度计算函数来定义网络训练一步的函数train_step
。这样利用train_step
函数,只需要输入所需要的数据,就可以对网络的参数进行一次更新,完成一步训练。
# 损失值计算函数
def forward_fn(observations, actions, y):
current_q_value = q_network(observations).gather_elements(dim=1, index=actions).squeeze() # 把经验对中这个动作对应的q_value给提取出来
loss = loss_fn(current_q_value, y)
return loss
ms.ops.value_and_grad
利用定义好的Loss计算函数forward_fn
,可以返回得到一个梯度计算函数grad_fn。然后在训练函数
train_step
中,我们就可以利用grad_fn
计算梯度,然后利用优化器optimizer
进行梯度反向传播,更新网络参数,完成一步训练。
3.4 网络训练
接下来就可以对网络进行训练了,这里对主要一些关键代码做出解释:
def Deep_Q_Learning(env, replay_memory_size=100_000, nb_epochs=40000_000, update_frequency=4, batch_size=32,
discount_factor=0.99, replay_start_size=5000, initial_exploration=1, final_exploration=0.01,
exploration_steps=100_000):
首先定义好训练需要的相关参数,包括经验池容量大小100_000,总训练epochs=40000_000,每4个epoch更新一次网络参数,折扣因子为0.99,经验池满5000时开始训练,初始探索概率为1,总探索epochs为100_000
这里的探索是指为了DQN学到更好的策略,在训练之前先随机产生动作进行探索,探索概率会逐渐减小,然后就会完全依靠DQN产生动作,称这个策略为ε−greedy策略。
在训练之前要将网络设置为训练模式:
q_network.set_train() # 设置网络为训练模式
然后就是让DQN与游戏进行交互,产生动作的相应的代码为(随机探索或者由DQN产生动作):
if random.random() < epsilon: # With probability ε select a random action a
action = np.array(env.action_space.sample())
else: # Otherwise select a = max_a Q∗(φ(st), a; θ)
temp_input = Tensor(obs, ms.float32).unsqueeze(0)
q_values = q_network(temp_input)
action = q_values.argmax(axis=1).item().asnumpy()
保存每次经验元组到经验池:
rb.add(obs, real_next_obs, action, reward, done)
data_obs, data_next_obs, data_action, data_reward, data_done = rb.sample(batch_size)
# 这一部分不用求梯度,所以写在forward_fn和train_step函数之外
max_q_value = q_network(data_next_obs).max(1)
y = data_reward.flatten() + discount_factor * max_q_value * (1 - data_done.flatten())
loss = train_step(data_obs, data_action, y)
04 实验结果
可以看出训练之后的DQN已经学会玩这个游戏了,一般能得150分左右,运气好的的话像这样能打到300分:
往期回顾
项目分享 | 如何通过MindNLP将HuggingFace Datasets嫁接到昇思MindSpore
项目分享 | 昇思MindSpore接入强化学习的新环境和新算法