MindSpore实现强化学习玩游戏《Playing Atari with Deep Reinforcement Learning》

《Playing Atari with Deep Reinforcement Learning》是首篇将强化学习与深度学习结合起来的深度强化学习经典论文,由DeepMind团队设计开发,算法在Atari 2600 游戏环境进行测试,在部分游戏中的测试表现优于人类玩家。论文网址:https://paperswithcode.com/paper/playing-atari-with-deep-reinforcement

本文基于Python的MindSpore框架对该算法进行复现,MindSpore是继Tensorflow、Pytorch之后,由华为昇腾设计的一类深度学习AI框架,旨在实现易开发、高效执行、全场景覆盖三大目标。具体特性可前往MindSpore官网进一步了解。

1 用Pycharm创建虚拟环境项目

项目代码和训练结果上传到百度网盘了,可以先下载下来,但是由于虚拟环境太大了所以没有上传,需要自己下载安装一遍,具体操作可以查看下文介绍。
链接:https://pan.baidu.com/s/1zoh0glqH4xcNSbOUuR2r7g?pwd=00wd
提取码:00wd

首先使用Pycharm创建一个新项目,然后如下图所示在设置中添加虚拟环境:

创建虚拟环境项目的目的在于使当前项目的运行环境与自己的Python环境分开,后续会在虚拟环境中安装需要的包,以免影响自己之前的Python环境。我用的Pycharm版本是2019版的,新版Pycharm的设置应该是类似的,可以根据自身情况百度。每个人的Anaconda路径不同,需要根据自己安装位置选择基本解释器。

虚拟环境的配置参考了CSDN文章:Pycharm 创建并管理虚拟环境

在这里插入图片描述

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

在这里插入图片描述

这个时候打开Pycharm下面的终端选项卡,可以看到终端前面提示(venv),表示当前终端是处于虚拟环境中的:

在这里插入图片描述

这个时候我们需要的包都可以在这个终端这里通过pip进行安装了。

记得把从百度云下载的文件 codeImgsmodel 这三个文件夹给复制到当前的项目文件夹里面。项目需要的Python包已经包含在 code 文件夹下的 requirements.txt 文件中了,打开Pycharm的终端选项卡,通过cd命令进入 code 文件夹:

cd code

然后pip安装需要的包:

pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple

正常来讲上面的环境配置完之后,code文件夹下的代码应该都可以正常运行了。如果无法正常运行,有可能是Atari的游戏环境的问题,具体可以参考这篇CSDN文章:

Gym Atari环境的配置参考了CSDN文章:【gym】新版安装(0.21以上)以及配置Atari环境,超简单(Windows)

2 论文模型解释

简单来讲,论文设计了一个DQN网络,将连续4帧、裁切为84X84的游戏画面堆叠成4X84X84的输入,然后通过卷积+ReLU卷积+ReLUFlatten全连接+ReLU全连接得到与动作维数相对应的输出。这里主要对 BreakOut (弹球打方块) 这款游戏进行了训练和测试,这款游戏对应的动作有4种,所以这里的输出维数为4。

输出的4维数组,分别代表4种动作对应的Q(s,a)值,选择最大的Q值所对应编号作为网络所输出的动作代号:
0:表示不移动
1:表示开始游戏 (如果游戏已经开始,那么1仍然不移动)
2:表示右移
3:表示左移

卷积的大小计算
输出大小 = (输入大小 - 卷积核大小 + 2 x padding) / 步长 + 1

在这里插入图片描述

将上面的DQN网络作为代理 a g e n t agent agent,就可以实现代理与游戏环境的互动了,网络根据当前的观测 ϕ j \phi_j ϕj 生成动作 a j a_j aj,操控底部的滑块,环境发生变化产生新的观测 ϕ j + 1 \phi_{j+1} ϕj+1, 当滑块成功将小球反弹并击中上面的方块,每击中一个方块就可以获得奖励 r e w a r d = 1 reward=1 reward=1, 否则没有奖励 r e w a r d = 0 reward=0 reward=0

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

强化学习算法将代理与环境的互动经验保存下来,可以得到一系列的经验元组,分别是 (当前观测、动作、下次观测、奖励、结束标志),按照论文里面的写法可以表示为:
( ϕ j , a j , r j , ϕ j + 1 , d o n e ) (\phi_j , a_j , r_j , \phi_{j+1},done) (ϕj,aj,rj,ϕj+1,done)

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

事实上,根据强化学习所依据的Bellman方程,我们还可以通过采取动作 a j a_j aj 后所获得的奖励 r j r_j rj,以及相应下一个观测的期望价值来估计当前的价值,这个估计值为:
y j = { r j , d o n e = 1 ( 游戏结束 ) r j + γ max ⁡ a ′ Q ( ϕ j + 1 , a ′ ; θ ) , d o n e = 0 ( 游戏未结束 ) y_j=\begin{cases} r_j& ,done=1\left( \text{游戏结束} \right)\\ r_j+\gamma \max _{a\prime}Q\left( \phi _{j+1},a\prime;\theta \right)& ,done=0\left( \text{游戏未结束} \right)\\ \end{cases} yj={rjrj+γmaxaQ(ϕj+1,a;θ)done=1(游戏结束)done=0(游戏未结束)

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

现在我们已经有:
利用DQN网络得到的当前观测 ϕ j \phi_j ϕj采取动作 a j a_j aj所对应的估计价值 Q ( ϕ j , a j ; θ ) Q(\phi_j , a_j ; θ) Q(ϕj,aj;θ)
利用奖励和下一个观测的最大估计价值乘以折扣因子所估计的当前价值 y j y_j yj

按照Bellman方程,两种估计应该是相等的,然而由于网络对价值的估计不准确,因此这两个估计之间存在差异:
L o s s = ( y j − Q ( ϕ j , a j ; θ ) ) 2 Loss=(y_j-Q(\phi_j , a_j ; θ))^2 Loss=(yjQ(ϕj,aj;θ))2
深度强化学习算法对网络训练的目标就在于减小两个估计之间的差异,使得DQN网络的策略满足Bellman方程,从而让DQN网络学会这个游戏的最优策略。在代理DQN网络与环境进行互动的过程中,利用保存的经验元组来计算上面的Loss,然后对DQN网络的参数进行梯度下降更新,就可以实现上述目的。

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

3 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.3 损失函数、优化器、训练函数的定义

首先对定义的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函数里面,完成了对价值估计值 Q ( ϕ j , a j ; θ ) Q(\phi_j , a_j ; θ) Q(ϕj,aj;θ)的计算,在代码中为current_q_value,在这个函数里面计算Q值需要用到DQN网络,后续Loss的梯度会反向传播到这个计算过程中DQN的网络参数上,用于神经网络的更新。

注意y的是函数的输入, y j y_j yj是在函数外计算好之后再输入的,因为关于 y j y_j yj的计算也需要DQN网络,而Loss的梯度不应该反向传播到关于 y j y_j yj的计算过程中,否则会导致网络参数更新不稳定。因此关于 y j y_j yj的计算需要在函数外计算好之后,再输入给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产生动作,称这个策略为 ε − g r e e d y \varepsilon-greedy ε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)

从经验池中采出一批经验元组,计算 y j y_j yj值,利用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)

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

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

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

4 测试结果

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

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值