项目分享 | 如何通过昇思MindSpore实现强化学习玩游戏

作者:Hamwon 

摘要

《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 创建并管理虚拟环境。

图片

虚拟环境创建完成后,还需要在设置里面把终端程序设置一下:

扫描二维码关注公众号,回复: 17388962 查看本文章

图片

这个时候打开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

图片

将上面的DQN网络作为代理agent agentagent,就可以实现代理与游戏环境的互动了,网络根据当前的观测图片生成动作图片,操控底部的滑块,环境发生变化产生新的观测图片, 当滑块成功将小球反弹并击中上面的方块,每击中一个方块就可以获得奖励reward = 1, 否则没有奖励reward = 0。

接下来需要解决的就是强化学习算法如何通过代理与环境的互动,来不断更新代理DQN网络的参数,使得代理学会玩这个游戏。

强化学习算法将代理与环境的互动经验保存下来,可以得到一系列的经验元组,分别是 (当前观测、动作、下次观测、奖励、结束标志),按照论文里面的写法可以表示为:

图片

我们将经验元组中当前的观测图片输入进网络,网络的输出为一个4维数组,分别对应当前观测下采取4种动作的价值。根据经验元组中当前观测所采取的实际的的动作图片,我们可以从这个数组里面拿到当前观测图片采取动作图片所对应的价值图片,这个价值显然与网络当前的参数θ有关。

事实上,根据强化学习所依据的Bellman方程,我们还可以通过采取动作图片后所获得的奖励图片,以及相应下一个观测的期望价值来估计当前的价值,这个估计值为:

图片

对于游戏结束的情况,只有采取动作之后获得的奖励,没有下一个观测,因此当前的价值估计就是奖励。

对于游戏未结束的情况,当前的价值估计包括获得的奖励,加上在下一个观测中采取4种动作中最大的估计价值,再乘以折扣因子γ,这个折扣因子表示当前的价值与后面的价值之间的联系是否紧密。0的话表示不紧密,当前的价值只看当前的奖励,γ 越大表示联系越紧密。

现在我们已经有:

利用DQN网络得到的当前观测图片采取动作图片所对应的估计价值 图片

利用奖励和下一个观测的最大估计价值乘以折扣因子所估计的当前价值图片。按照Bellman方程,两种估计应该是相等的,然而由于网络对价值的估计不准确,因此这两个估计之间存在差异:

图片

深度强化学习算法对网络训练的目标就在于减小两个估计之间的差异,使得DQN网络的策略满足Bellman方程,从而让DQN网络学会这个游戏的最优策略。在代理DQN网络与环境进行互动的过程中,利用保存的经验元组来计算上面的Loss,然后对DQN网络的参数进行梯度下降更新,就可以实现上述目的。

下面将给出Python环境下利用昇思MindSpore框架的具体代码实现,并给出相应解释。

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

forward_fn函数里面,完成了对价值估计值图片的计算,在代码中为current_q_value,在这个函数里面计算Q值需要用到DQN网络,后续Loss的梯度会反向传播到这个计算过程中DQN的网络参数上,用于神经网络的更新。

注意y的是函数的输入,图片是在函数外计算好之后再输入的,因为关于图片的计算也需要DQN网络,而Loss的梯度不应该反向传播到关于图片的计算过程中,否则会导致网络参数更新不稳定。因此关于图片的计算需要在函数外计算好之后,再输入给forward_fn

# 损失梯度函数
grad_fn = ms.ops.value_and_grad(forward_fn, None, optimizer.parameters)  
# 参考:https://www.mindspore.cn/tutorials/zh-CN/r2.1/beginner/autograd.html

# 训练一步的函数
def train_step(observations, actions, y):
    loss, grads = grad_fn(observations, actions, y)
    optimizer(grads)
    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)

从经验池中采出一批经验元组,计算图片值,利用train_step函数进行一次网络参数更新:

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)

注意图片是在这里计算的,因为不需要梯度反向传播到计算图片过程中,所以先在这里计算好,然后输入给前面定义train_step函数,即可完成一次训练。

接下来就是漫长的训练过程了,使用自己的笔记本大概训练了10天左右。测试了一下在自己笔记本上的训练速度大概是和Pytorch差不多的。昇思MindSpore是可以利用华为昇腾910(华为研发的AI芯片)加速训练的,但是作为穷学生党想想还是算了,所以有没有大佬vivo50让我试试速度(`・ω・´)。

训练曲线图如下,github上大佬用Pytorch训练出来的结果大概在200左右,受限于笔记本内存为16g,在当前的经验池容量下能训练的的最好结果大概在150左右。

图片

04

实验结果

可以看出训练之后的DQN已经学会玩这个游戏了,一般能得150分左右,运气好的的话像这样能打到300分:

图片

90后程序员开发视频搬运软件、不到一年获利超 700 万,结局很刑! 谷歌证实裁员,涉及 Flutter、Dart 和 Python 团队 中国码农的“35岁魔咒” Xshell 8 开启 Beta 公测:支持 RDP 协议、可远程连接 Windows 10/11 ​MySQL 的第一个长期支持版 8.4 GA 开源日报 | 微软挤兑Chrome;阳痿中年的福报玩具;神秘AI能力太强被疑GPT-4.5;通义千问3个月开源8模型 Arc Browser for Windows 1.0 正式 GA Windows 10 市场份额达 70%,Windows 11 持续下滑 GitHub 发布 AI 原生开发工具 GitHub Copilot Workspace JAVA 下唯一一款搞定 OLTP+OLAP 的强类型查询这就是最好用的 ORM 相见恨晚
{{o.name}}
{{m.name}}

猜你喜欢

转载自my.oschina.net/u/4736317/blog/11072521