# 十、构建类似 AlphaZero 的手机游戏应用 尽管现代**人工智能**(**AI**)的日益普及基本上是由 2012 年深度学习的突破引起的,但 2016 年 3 月,Google DeepMind 的 AlphaGo 以 4-1 击败围棋世界冠军 Lee Sedol,然后在 2017 年 5 月以 3-0 击败了目前排名第一的围棋玩家 Ke Jie 的历史性事件,这在很大程度上使 AI 家喻户晓。 由于围棋游戏的复杂性,人们普遍认为任务无法实现,或者至少十年内计算机程序不可能击败顶级围棋玩家。 在 2017 年 5 月 AlphaGo 和 Ke Jie 的比赛之后,Google 退役了 AlphaGo; 谷歌(DeepMind)是 Google 因其开创性的深度强化学习技术而收购的创业公司,也是 AlphaGo 的开发商,决定将其 AI 研究重点放在其他领域。 然后,有趣的是,在 2017 年 10 月,DeepMind 在游戏上发表了另一篇论文[《围棋:在没有人类知识的情况下掌握围棋游戏》](https://deepmind.com/research/publications/mastering-game-go-without-human-knowledge),它描述了一种称为 AlphaGo Zero 的改进算法,该算法仅通过自我强化学习来学习如何玩围棋,而无需依赖任何人类专家知识,例如大量玩过的专业的围棋游戏,AlphaGo 用它来训练其模型。 令人惊讶的是,AlphaGo Zero 完全击败了 AlphaGo,后者在几个月前以 100-0 击败了世界上最好的人类 GO 玩家! 事实证明,这只是朝着 Google 更雄心勃勃的目标迈出的一步,该目标是将 AlphaGo 背后的 AI 技术应用和改进到其他领域。 2017 年 12 月,DeepMind 发表了另一篇论文,[即使用通用强化学习算法通过自学掌握国际象棋和将棋](https://arxiv.org/pdf/1712.01815.pdf),对 AlphaGo 进行了概括。 将零程序归类为一个称为 AlphaZero 的算法,并使用该算法从头开始快速学习如何玩象棋和将棋的游戏,从除了游戏规则之外没有任何领域知识的随机游戏开始,并在 24 小时内实现了超人级别并击败世界冠军。 在本章中,我们将带您浏览 AlphaZero 的最新最酷的部分,向您展示如何构建和训练类似 AlphaZero 的模型来玩一个简单而有趣的游戏,称为 [Connect4](https://en.wikipedia.org/wiki/Connect_Four),在 TensorFlow 和 Keras 中使用,这是我们在第 8 章,“使用 RNN 预测股价”的流行的高级深度学习库。 我们还将介绍如何使用训练有素的 AlphaZero 模型来获得训练有素的专家策略,以指导移动游戏的玩法,以及使用该模型玩 Connect4 游戏的完整 iOS 和 Android 应用的源代码。 总之,本章将涵盖以下主题: * AlphaZero – 它如何工作? * 为 Connect4 构建和训练类似于 AlphaZero 的模型 * 在 iOS 中使用模型玩 Connect4 * 在 Android 中使用模型玩 Connect4 # AlphaZero – 它如何工作? AlphaZero 算法包含三个主要组件: * 一个深度卷积神经网络,它以棋盘位置(或状态)为输入,并从该位置输出一个值作为预测的博弈结果,该策略是输入棋盘状态下每个可能动作的移动概率列表。 * 一种通用的强化学习算法,该算法通过自玩从头开始学习,除了游戏规则外,没有特定的领域知识。 通过自增强学习学习深度神经网络的参数,以使预测值与实际自游戏结果之间的损失最小,并使预测策略与搜索概率之间的相似性最大化,这来自以下算法。 * 一种通用(与域无关)的**蒙特卡洛树搜索**(**MCTS**)算法,该算法从头至尾模拟自玩游戏,并通过考虑到从深度神经网络返回的预测值和策略概率值,以及访问节点的频率—有时,选择访问次数较少的节点称为强化学习中的探索(与采取较高预测值和策略的举动相反,这称为利用)。 探索与利用之间的良好平衡可以带来更好的结果。 强化学习的历史可以追溯到 1960 年代,当时该术语在工程文献中首次使用。 但是突破发生在 2013 年,当时 DeepMind 将强化学习与深度学习相结合,并开发了深度强化学习应用,该应用学会了从头开始玩 Atari 游戏,以原始像素为输入的,并随后击败了人类。 与监督学习不同,监督学习需要标记数据进行训练,就像我们在前几章中建立或使用的许多模型中所看到的那样,强化学习使用反复试验的方法来获得更好的效果:智能体与环境交互并接收在每个状态上采取的每个动作的奖励(正面或负面)。 在 AlphaZero 下象棋的示例中,只有在游戏结束后才能获得奖励,获胜的结果为 +1,失败的为 -1,平局为 0。强化学习 AlphaZero 中的算法对我们前面提到的损失使用梯度下降来更新深层神经网络的参数, 就像一个通用函数近似来学习和编码游戏技巧。 学习或训练过程的结果可以是由深度神经网络生成的策略,该策略说出对任何状态应采取的行动,或者是将每个状态以及该状态的每个可能动作映射到长期奖励的值函数 。 如果深层神经网络使用自我玩法强化学习所学习的策略是理想的,则我们可能无需让程序在游戏过程中执行任何 MCTS,而程序总是可以最大可能地选择移动。 但是在诸如象棋或围棋的复杂游戏中,无法生成完美的策略,因此 MCTS 必须与训练有素的深度网络一起工作,以指导针对每种游戏状态的最佳可能动作的搜索。 如果您不熟悉强化学习或 MCTS,则在互联网上有很多关于强化学习或 MCTS 的信息。 考虑查看 Richard Sutton 和 Andrew Barto 的经典著作《强化学习:简介》,该书可在[以下网站](http://incompleteideas.net/book/the-book-2nd.html)上公开获得。 您还可以在 YouTube 上观看 DeepMind 的 AlphaGo 的技术负责人 David Silver 的强化学习课程视频(搜索“强化学习 David Silver”)。 一个有趣且有用的强化学习工具包是 [OpenAI Gym](https://gym.openai.com)。 在本书的最后一章中,我们将更深入地学习强化学习和 OpenAI Gym。 对于 MCTS,请查看其[维基页面](https://en.wikipedia.org/wiki/Monte_Carlo_tree_search),以及[此博客](http://tim.hibal.org/blog/alpha-zero-how-and-why-it-works)。 在下一节中,我们将研究以 TensorFlow 为后端的 Keras 实现 AlphaZero 算法,其目标是使用该算法构建和训练模型以玩 Connect4。您将看到模型架构是什么样,以及构建模型的 Keras 关键代码。 # 训练和测试适用于 Connect4 的类似 AlphaZero 的模型 如果您从未玩过 Connect4,则可以在[这个页面](http://www.connectfour.org)上免费玩它。 这是一个快速有趣的游戏。 基本上,两个玩家轮流从一列的顶部将不同颜色的光盘放入六行乘七列的网格中。 如果尚未在该列中放入任何光盘,则新放置的光盘将位于该列的底部,或者位于该列中最后放置的光盘的顶部。 谁先在三个可能的方向(水平,垂直,对角线)中的任何一个方向上拥有自己颜色的四个连续光盘赢得比赛。 Connect4 的 AlphaZero 模型基于[存储库](https://github.com/jeffxtang/DeepReinforcementLearning),这是[这个页面](https://github.com/AppliedDataSciencePartners/DeepReinforcementLearning)的分支, 有一个不错的博客,[如何使用 Python 和 Keras 构建自己的 AlphaZero AI](https://applied-data.science/blog/how-to-build-your-own-alphazero-ai-using-python-and-keras),您可能应该在继续之前阅读它,因此以下步骤更有意义。 # 训练模型 在我们看一些核心代码片段之前,让我们首先看一下如何训练模型。 首先,通过在终端上运行以下命令来获取存储库: ```py git clone https://github.com/jeffxtang/DeepReinforcementLearning ``` 然后,如果尚未在第 8 章,“使用 RNN 预测股价”中设置,则设置 Keras 和 TensorFlow 虚拟环境: ```py cd mkdir ~/tf_keras virtualenv --system-site-packages ~/tf_keras/ cd ~/tf_keras/ source ./bin/activate easy_install -U pip #On Mac: pip install --upgrade https://storage.googleapis.com/tensorflow/mac/cpu/tensorflow-1.4.0-py2-none-any.whl #On Ubuntu: pip install --upgrade https://storage.googleapis.com/tensorflow/linux/gpu/tensorflow_gpu-1.4.0-cp27-none-linux_x86_64.whl easy_install ipython pip install keras ``` 您也可以在前面的`pip install`命令中尝试 TensorFlow 1.5-1.8 下载 URL。 现在,先按`cd DeepReinforcementLearning`打开`run.ipynb`,然后按`jupyter notebook`打开-根据您的环境,如果发现任何错误,则需要安装缺少的 Python 包。 在浏览器上,打开`http://localhost:8888/notebooks/run.ipynb`,然后运行笔记本中的第一个代码块以加载所有必需的核心库,并运行第二个代码块以开始训练—该代码被编写为永远训练,因此经过数小时的训练后,您可能要取消`jupyter notebook`命令。 在较旧的 Mac 上,要花一个小时才能看到在以下目录中创建的模型的第一个版本(较新的版本,例如`version0004.h5`,其权重比旧版本中的权重要微调,例如 `version0001.h5`): ```py (tf_keras) MacBook-Air:DeepReinforcementLearning jeffmbair$ ls -lt run/models -rw-r--r-- 1 jeffmbair staff 3781664 Mar 8 15:23 version0004.h5 -rw-r--r-- 1 jeffmbair staff 3781664 Mar 8 14:59 version0003.h5 -rw-r--r-- 1 jeffmbair staff 3781664 Mar 8 14:36 version0002.h5 -rw-r--r-- 1 jeffmbair staff 3781664 Mar 8 14:12 version0001.h5 -rw-r--r-- 1 jeffmbair staff 656600 Mar 8 12:29 model.png ``` 带有`.h5`扩展名的文件是 HDF5 格式的 Keras 模型文件,每个文件主要包含模型架构定义,训练后的权重和训练配置。 稍后,您将看到如何使用 Keras 模型文件生成 TensorFlow 检查点文件,然后将其冻结为可在移动设备上运行的模型文件。 `model.png`文件包含深度神经网络架构的详细视图。 卷积层的许多残差块之后是批量归一化和 ReLU 层,以稳定训练,它的深度非常大。 该模型的顶部如下图所示(中间部分很大,因此我们将不显示其中间部分,建议您打开`model.png`文件以供参考): ![](img/99dc16a9-52c9-4adf-bd8f-1a244504de79.png) 图 10.1:深度残差网络的第一层 值得注意的是,神经网络称为残差网络(ResNet),由 Microsoft 于 2015 年在 ImageNet 和 COCO 2015 竞赛的获奖作品中引入。 在 ResNet 中,使用身份映射(图 10.1 右侧的箭头)可避免在网络越深时出现更高的训练误差。 有关 ResNet 的更多信息,您可以查看原始论文[《用于图像识别的深度残差学习》](https://arxiv.org/pdf/1512.03385v1.pdf), 以及博客[《了解深度残差网络》](https://blog.waya.ai/deep-residual-learning-9610bb62c355) - 一个简单的模块化学习框架,它重新定义了构成最新技术的内容。 深度网络的最后一层如图 10.2 所示,您可以看到,在最后的残差块和具有批量归一化和 ReLU 层的卷积层之后,将应用密集的全连接层以输出`value_head and policy_head`值: ![](img/e3383760-5f06-4d10-a903-8978f41281de.png) 图 10.2:深度 Resnet 的最后一层 在本节的最后部分,您将看到一些使用 Keras API 的 Python 代码片段,该片段对 ResNet 有着很好的支持,以构建这样的网络。 现在让我们让这些模型首先互相对抗,然后与我们一起对抗,看看这些模型有多好。 # 测试模型 例如,要让模型的版本 4 与版本 1 竞争,请首先通过运行`mkdir -p run_archive/connect4/run0001/models`创建新的目录路径,然后将`run/models`文件从`run/models`复制到`run0001/models`目录。 然后将`DeepReinforcementLearning`目录中的`play.py`更改为: ```py playMatchesBetweenVersions(env, 1, 1, 4, 10, lg.logger_tourney, 0) ``` 参数`1,1,4,10`的第一个值表示运行版本,因此 1 表示模型位于`run_archive/connect4`的`run0001/models`中。 第二个和第三个值是两个玩家的模型版本,因此 1 和 4 表示该模型的版本 1 将与版本 4 一起玩。10 是玩的次数或剧集。 运行`python play.py`脚本按照指定的方式玩游戏后,可以使用以下命令找出结果: ```py grep WINS run/logs/logger_tourney.log |tail -10 ``` 对于与版本 1 对抗的版本 4,您可能会看到与以下内容相似的结果,这意味着它们处于大致相同的水平: ```py 2018-03-14 23:55:21,001 INFO player2 WINS! 2018-03-14 23:55:58,828 INFO player1 WINS! 2018-03-14 23:56:43,778 INFO player2 WINS! 2018-03-14 23:56:51,981 INFO player1 WINS! 2018-03-14 23:57:00,985 INFO player1 WINS! 2018-03-14 23:57:30,389 INFO player2 WINS! 2018-03-14 23:57:39,742 INFO player1 WINS! 2018-03-14 23:58:19,498 INFO player2 WINS! 2018-03-14 23:58:27,554 INFO player1 WINS! 2018-03-14 23:58:36,490 INFO player1 WINS! ``` `config.py`中有一个设置`MCTS_SIMS = 50`(MCTS 的模拟次数)会对游玩时间产生重大影响。 在每个状态下,MCTS 都会进行`MCTS_SIMS`次仿真,并与受过训练的网络一起提出最佳方案。 因此,将`MCTS_SIMS`设置为 50 会使`play.py` 脚本运行更长的时间,但如果训练的模型不够好,并不一定会使玩家更强大。 在使用特定版本的模型时,可以将其更改为不同的值,以查看其如何影响其强度水平。 要手动玩一个特定版本,请将`play.py`更改为: ```py playMatchesBetweenVersions(env, 1, 4, -1, 10, lg.logger_tourney, 0) ``` 在这里,-1 表示人类玩家。 因此,上一行会要求您(玩家 2)与该模型的玩家 1,版本 4 对抗。 现在运行`python play.py`后,您会看到输入提示`Enter your chosen action:`; 打开另一个终端,转到`DeepReinforcementLearning`目录,然后键入 `tail -f run/logs/logger_tourney.log`命令,您将看到这样打印的电路板网格: ```py 2018-03-15 00:03:43,907 INFO ==================== 2018-03-15 00:03:43,907 INFO EPISODE 1 OF 10 2018-03-15 00:03:43,907 INFO ==================== 2018-03-15 00:03:43,908 INFO player2 plays as X 2018-03-15 00:03:43,908 INFO -------------- 2018-03-15 00:03:43,908 INFO ['-', '-', '-', '-', '-', '-', '-'] 2018-03-15 00:03:43,908 INFO ['-', '-', '-', '-', '-', '-', '-'] 2018-03-15 00:03:43,908 INFO ['-', '-', '-', '-', '-', '-', '-'] 2018-03-15 00:03:43,909 INFO ['-', '-', '-', '-', '-', '-', '-'] 2018-03-15 00:03:43,909 INFO ['-', '-', '-', '-', '-', '-', '-'] 2018-03-15 00:03:43,909 INFO ['-', '-', '-', '-', '-', '-', '-'] ``` 请注意,最后 6 行代表 6 行乘 7 列的板格:第一行对应于 7 个动作编号 0、1、2、3、4、5、6,第二行对应于 7、8、9 10、11、12、13 等,因此最后一行映射到 35、36、37、38、39、40、41 动作编号。 现在,在运行`play.py`的第一个终端中输入数字 38,该模型的版本 4 的玩家 1(打为 O)将移动,显示新的棋盘格,如下所示: ```py 2018-03-15 00:06:13,360 INFO action: 38 2018-03-15 00:06:13,364 INFO ['-', '-', '-', '-', '-', '-', '-'] 2018-03-15 00:06:13,365 INFO ['-', '-', '-', '-', '-', '-', '-'] 2018-03-15 00:06:13,365 INFO ['-', '-', '-', '-', '-', '-', '-'] 2018-03-15 00:06:13,365 INFO ['-', '-', '-', '-', '-', '-', '-'] 2018-03-15 00:06:13,365 INFO ['-', '-', '-', '-', '-', '-', '-'] 2018-03-15 00:06:13,365 INFO ['-', '-', '-', 'X', '-', '-', '-'] 2018-03-15 00:06:13,366 INFO -------------- 2018-03-15 00:06:15,155 INFO action: 31 2018-03-15 00:06:15,155 INFO ['-', '-', '-', '-', '-', '-', '-'] 2018-03-15 00:06:15,156 INFO ['-', '-', '-', '-', '-', '-', '-'] 2018-03-15 00:06:15,156 INFO ['-', '-', '-', '-', '-', '-', '-'] 2018-03-15 00:06:15,156 INFO ['-', '-', '-', '-', '-', '-', '-'] 2018-03-15 00:06:15,156 INFO ['-', '-', '-', 'O', '-', '-', '-'] 2018-03-15 00:06:15,156 INFO ['-', '-', '-', 'X', '-', '-', '-'] ``` 在玩家 1 移至游戏结束后继续输入新动作,直到可能的新游戏开始: ```py 2018-03-15 00:16:03,205 INFO action: 23 2018-03-15 00:16:03,206 INFO ['-', '-', '-', '-', '-', '-', '-'] 2018-03-15 00:16:03,206 INFO ['-', '-', '-', 'O', '-', '-', '-'] 2018-03-15 00:16:03,206 INFO ['-', '-', '-', 'O', 'O', 'O', '-'] 2018-03-15 00:16:03,207 INFO ['-', '-', 'O', 'X', 'X', 'X', '-'] 2018-03-15 00:16:03,207 INFO ['-', '-', 'X', 'O', 'X', 'O', '-'] 2018-03-15 00:16:03,207 INFO ['-', '-', 'O', 'X', 'X', 'X', '-'] 2018-03-15 00:16:03,207 INFO -------------- 2018-03-15 00:16:14,175 INFO action: 16 2018-03-15 00:16:14,178 INFO ['-', '-', '-', '-', '-', '-', '-'] 2018-03-15 00:16:14,179 INFO ['-', '-', '-', 'O', '-', '-', '-'] 2018-03-15 00:16:14,179 INFO ['-', '-', 'X', 'O', 'O', 'O', '-'] 2018-03-15 00:16:14,179 INFO ['-', '-', 'O', 'X', 'X', 'X', '-'] 2018-03-15 00:16:14,179 INFO ['-', '-', 'X', 'O', 'X', 'O', '-'] 2018-03-15 00:16:14,180 INFO ['-', '-', 'O', 'X', 'X', 'X', '-'] 2018-03-15 00:16:14,180 INFO -------------- 2018-03-15 00:16:14,180 INFO player2 WINS! 2018-03-15 00:16:14,180 INFO ==================== 2018-03-15 00:16:14,180 INFO EPISODE 2 OF 5 ``` 这样便可以手动测试模型特定版本的强度。 了解前面板上的表示形式还可以帮助您稍后了解 iOS 和 Android 代码。 如果您过于轻易地击败模型,可以采取几种措施来尝试改善模型: * 在`run.ipynb`(第二个代码块)Python 笔记本中运行模型几天。 在我们的测试中,该模型的版本 19 在较旧的 iMac 上运行了大约一天后,击败了版本 1 或 4 10:0(回想一下版本 1 和版本 4 处于相同水平) * 为了提高 MCTS 评分公式的强度:MCTS 在模拟过程中使用上置信度树(UCT)评分来选择要做出的举动,并且仓库中的公式是这样的(请参见[博客](http://tim.hibal.org/blog/alpha-zero-how-and-why-it-works)以及 AlphaZero 官方论文以获取更多详细信息): ```py edge.stats['P'] * np.sqrt(Nb) / (1 + edge.stats['N']) ``` 如果我们将其更改为更类似于 DeepMind 的用法: ```py edge.stats['P'] * np.sqrt(np.log(1+Nb) / (1 + edge.stats['N'])) ``` 然后,即使将`MCTS_SIMS`设置为 10,版本 19 仍以 10:0 完全击败版本 1。 * 微调深度神经网络模型以尽可能接近地复制 AlphaZero 关于模型的细节不在本书的讨论范围之内,但让我们继续看看如何在 Keras 中构建模型,以便在以后在 iOS 和 Android 上运行它时更加欣赏它(您可以查看其余部分)。 `agent.py`,`MCTS.py`和`game.py`中的主要代码,以更好地了解游戏的工作方式)。 # 研究模型构建代码 在`model.py`中,Keras 的导入如下: ```py from keras.models import Sequential, load_model, Model from keras.layers import Input, Dense, Conv2D, Flatten, BatchNormalization, Activation, LeakyReLU, add from keras.optimizers import SGD from keras import regularizers ``` 四种主要的模型构建方法是: ```py def residual_layer(self, input_block, filters, kernel_size) def conv_layer(self, x, filters, kernel_size) def value_head(self, x) def policy_head(self, x) ``` 它们都具有一个或多个`Conv2d`层,然后激活`BatchNormalization`和`LeakyReLU`,如图 10.1 所示,但是`value_head`和`policy_head`也具有完全连接的层,如图 10.2 所示。 卷积层以生成我们之前谈到的输入状态的预测值和策略概率。 在`_build_model`方法中,定义了模型输入和输出: ```py main_input = Input(shape = self.input_dim, name = 'main_input') vh = self.value_head(x) ph = self.policy_head(x) model = Model(inputs=[main_input], outputs=[vh, ph]) ``` `_build_model`方法中还定义了深度神经网络以及模型损失和优化器: ```py if len(self.hidden_layers) > 1: for h in self.hidden_layers[1:]: x = self.residual_layer(x, h['filters'], h['kernel_size']) model.compile(loss={'value_head': 'mean_squared_error', 'policy_head': softmax_cross_entropy_with_logits}, optimizer=SGD(lr=self.learning_rate, momentum = config.MOMENTUM), loss_weights={'value_head': 0.5, 'policy_head': 0.5}) ``` 为了找出确切的输出节点名称(输入节点名称指定为`'main_input'`),我们可以在`model.py`中添加`print(vh)`和`print(ph)`; 现在运行的`python play.py`将输出以下两行: ```py Tensor("value_head/Tanh:0", shape=(?, 1), dtype=float32) Tensor("policy_head/MatMul:0", shape=(?, 42), dtype=float32) ``` 冻结 TensorFlow 检查点文件并将模型加载到移动应用时,我们将需要它们。 # 冻结模型 首先,我们需要创建 TensorFlow 检查点文件–只需取消注释`funcs.py`中`player1`和`player2`的两行,然后再次运行`python play.py`: ```py if player1version > 0: player1_network = player1_NN.read(env.name, run_version, player1version) player1_NN.model.set_weights(player1_network.get_weights()) # saver = tf.train.Saver() # saver.save(K.get_session(), '/tmp/alphazero19.ckpt') if player2version > 0: player2_network = player2_NN.read(env.name, run_version, player2version) player2_NN.model.set_weights(player2_network.get_weights()) # saver = tf.train.Saver() # saver.save(K.get_session(), '/tmp/alphazero_4.ckpt') ``` 您可能会觉得很熟悉,因为我们在第 8 章,“使用 RNN 预测股票价格”做了类似的操作。 确保将`alphazero19.ckpt`和`alphazero_4.ckpt`中的版本号(例如 19 或 4)与`play.py`中定义的内容(例如`playMatchesBetweenVersions(env, 1, 19, 4, 10, lg.logger_tourney, 0)`)以及 `run_archive/connect4/run0001/models`目录中的版本号匹配。在这种情况下, `version0019.h5`和 `version0004.h5`都需要存在。 运行`play.py`后,将在`/tmp`目录中生成`alphazero19`检查点文件: ```py -rw-r--r-- 1 jeffmbair wheel 99 Mar 13 18:17 checkpoint -rw-r--r-- 1 jeffmbair wheel 1345545 Mar 13 18:17 alphazero19.ckpt.meta -rw-r--r-- 1 jeffmbair wheel 7296096 Mar 13 18:17 alphazero19.ckpt.data-00000-of-00001 -rw-r--r-- 1 jeffmbair wheel 8362 Mar 13 18:17 alphazero19.ckpt.index ``` 现在,您可以转到 TensorFlow 根源目录并运行`freeze_graph`脚本: ```py python tensorflow/python/tools/freeze_graph.py \ --input_meta_graph=/tmp/alphazero19.ckpt.meta \ --input_checkpoint=/tmp/alphazero19.ckpt \ --output_graph=/tmp/alphazero19.pb \ --output_node_names="value_head/Tanh,policy_head/MatMul" \ --input_binary=true ``` 为简单起见,由于它是小型模型,因此我们不会我们不会进行图变换和内存映射变换,就像第 6 章,“用自然语言描述图像”和第 9 章,“用 GAN 生成和增强图像”。 现在,我们准备在移动设备上使用该模型并编写代码以在 iOS 和 Android 设备上玩 Connect4。 # 在 iOS 中使用模型玩 Connect4 对于新冻结的,可选的经过转换和映射的模型,您始终可以将其与 TensorFlow Pod 一起尝试,以查看是否有幸能够以简单的方式使用它。 在我们的案例中,当使用 TensorFlow Pod 加载它时,我们生成的`alphazero19.pb` 模型会导致以下错误: ```py Couldn't load model: Invalid argument: No OpKernel was registered to support Op 'Switch' with these attrs. Registered devices: [CPU], Registered kernels: device='GPU'; T in [DT_FLOAT] device='GPU'; T in [DT_INT32] device='GPU'; T in [DT_BOOL] device='GPU'; T in [DT_STRING] device='CPU'; T in [DT_INT32] device='CPU'; T in [DT_FLOAT] [[Node: batch_normalization_13/cond/Switch = Switch[T=DT_BOOL, _output_shapes=[[], []]](batch_normalization_1/keras_learning_phase, batch_normalization_1/keras_learning_phase)]] ``` 您应该已经知道如何解决这种类型的错误,因为前面的章节已经对此进行了讨论。 回顾一下,只需确保`tensorflow/contrib/makefile/tf_op_files.txt`文件中包含`Switch`操作的内核文件。 您可以通过运行`grep 'REGISTER.*"Switch"' tensorflow/core/kernels/*.cc`来查找哪个`Switch`内核文件,该文件应显示`tensorflow/core/kernels/control_flow_ops.cc`。 默认情况下,从 TensorFlow 1.4 开始, `tf_op_files.txt`中包含 `control_flow_ops.cc` 文件,因此您所需要做的就是通过运行`tensorflow/contrib/makefile/build_all_ios.sh`来构建 TensorFlow iOS 自定义库。 如果您已在上一章中成功运行了 iOS 应用,则该库已经不错,您不需要或不想再次运行耗时的命令。 现在,只需创建一个名为 AlphaZero 的新 Xcode iOS 项目,然后将上一章中的 iOS 项目中的`tensorflow_utils.mm`和`tensorflow_utils.h`文件以及上一节中生成的`alphazero19.pb`模型文件拖放到项目。 将`ViewController.m`重命名为`ViewController.mm`,并添加一些常量和变量。 您的项目应如图 10.3 所示: ![](img/a0639a2a-55f3-485a-92cd-4ffdc99b398c.png) 图 10.3:在 Xcode 中显示 AlphaZero iOS 应用 我们只需要使用三个 UI 组件: * 一个`UIImageView`,显示棋盘和演奏的棋子。 * 显示游戏结果并提示用户采取措施的`UILabel`。 * 一个`UIButton`可以玩或重玩游戏。 和以前一样,我们以编程方式在`viewDidLoad`方法中创建和定位它们。 轻按游玩或重放按钮时,随机决定谁先走,重置表示为整数数组的棋盘,清除存储我们的移动和 AI 的移动的两个向量,以及重新绘制原始板格: ```py int n = rand() % 2; aiFirst = (n==0); if (aiFirst) aiTurn = true; else aiTurn = false; for (int i=0; i max) { max = probs[i]; action = i; } } ``` 我们将所有`probs`数组元素初始化为 -100.0 的原因是,在`getProbs`方法内部(我们将很快显示),`probs`数组将仅针对允许的操作更改为由策略返回的值(所有 -1.0 到 1.0 之间的小值),因此所有非法行为的`probs`值将保持为 -100.0,并且在`softmax`函数之后,这使得非法移动的可能性基本为零,我们可以使用合法行动的可能性。 我们仅使用最大概率值来指导 AI 的移动,而不使用 MCTS,如果我们希望 AI 在象棋或围棋这样的复杂游戏中真正强大,这将是必要的。 如前所述,如果从经过训练的模型返回的策略是完美的,则无需使用 MCTS。 我们将在书的源代码存储库中保留 MCTS 实现,以供您参考,而不是显示 MCTS 的所有实现细节。 `playGame`方法中的其余代码根据模型返回的所有合法动作中的最大概率,以选定的动作来更新木板,将`printBoard` 辅助方法调用来在 Xcode 输出面板上打印板以进行更好的调试,将动作添加到 `aiMoves` 向量中,以便可以正确重绘板,并在游戏结束时返回正确的状态信息。 通过将 `aiTurn` 设置为 `false` ,您将很快看到的触摸事件处理器将接受人类的触摸手势,作为人类打算采取的动作; 如果 `aiTurn` 为 `true` ,则触摸处理器将忽略所有触摸手势: ```py board[action] = AI_PIECE; printBoard(board); aiMoves.push_back(action); delete []probs; if (aiWon(board)) return "AI Won!"; else if (aiLost(board)) return "You Won!"; else if (aiDraw(board)) return "Draw"; } else { delete []probs; } aiTurn = false; return "Tap the column for your move"; } ``` `printBoard`辅助方法如下: ```py void printBoard(int bd[]) { for (int i = 0; i<6; i++) { for (int j=0; j<7; j++) { cout << PIECE_SYMBOL[bd[i*7+j]] << " "; } cout << endl; } cout << endl << endl; } ``` 因此,在 Xcode 输出面板中,它将打印出如下内容: ```py - - - - - - - - - - - - - - - - O - - - - X - O - - - O O O O X X - X X X O O X - X ``` 在`getProbs`键方法中,首先定义输入和输出节点名称,然后使用`binary`中的值准备输入张量: ```py bool getProbs(int *binary, float *probs) { std::string input_name = "main_input"; std::string output_name1 = "value_head/Tanh"; std::string output_name2 = "policy_head/MatMul"; tensorflow::Tensor input_tensor(tensorflow::DT_FLOAT, tensorflow::TensorShape({1,2,6,7})); auto input_mapped = input_tensor.tensor(); for (int i = 0; i < 2; i++) { for (int j = 0; j<6; j++) { for (int k=0; k<7; k++) { input_mapped(0,i,j,k) = binary[i*42+j*7+k]; } } } ``` 现在使用输入运行模型并获取输出: ```py std::vector outputs; tensorflow::Status run_status = tf_session->Run({{input_name, input_tensor}}, {output_name1, output_name2}, {}, &outputs); if (!run_status.ok()) { LOG(ERROR) << "Getting model failed:" << run_status; return false; } tensorflow::Tensor* value_tensor = &outputs[0]; tensorflow::Tensor* policy_tensor = &outputs[1]; const Eigen::TensorMap, Eigen::Aligned>& value = value_tensor->flat(); const Eigen::TensorMap, Eigen::Aligned>& policy = policy_tensor->flat(); ``` 仅设置允许动作的概率值,然后调用`softmax`以使允许动作的`probs`值之和为 1: ```py vector actions; getAllowedActions(board, actions); for (int action : actions) { probs[action] = policy(action); } softmax(probs, PIECES_NUM); return true; } ``` `getAllowedActions`函数定义如下: ```py void getAllowedActions(int bd[], vector &actions) { for (int i=0; i=PIECES_NUM-7) { if (bd[i] == 0) actions.push_back(i); } else { if (bd[i] == 0 && bd[i+7] != 0) actions.push_back(i); } } } ``` 以下是`softmax`函数,它们都很简单: ```py void softmax(float vals[], int count) { float max = -FLT_MAX; for (int i=0; i endY) return; int column = (point.x-startX)/BOARD_COLUMN_WIDTH; for (int i=0; i<6; i++) if (board[35+column-7*i] == 0) { board[35+column-7*i] = HUMAN_PIECE; humanMoves.push_back(35+column-7*i); break; } ``` 其余触摸处理器通过调用`createBoardImageInRect`来重绘`ImageView`,它使用`BezierPath`绘制或重绘棋盘和所有已玩过的棋子,检查游戏状态并在游戏结束时返回结果,或者继续玩游戏,如果没有: ```py _iv.image = [self createBoardImageInRect:_iv.frame]; aiTurn = true; if (gameEnded(board)) { if (aiWon(board)) _lbl.text = @"AI Won!"; else if (aiLost(board)) _lbl.text = @"You Won!"; else if (aiDraw(board)) _lbl.text = @"Draw"; return; } dispatch_async(dispatch_get_global_queue(0, 0), ^{ std::string result = playGame(withMCTS)); dispatch_async(dispatch_get_main_queue(), ^{ NSString *rslt = [NSString stringWithCString:result.c_str() encoding:[NSString defaultCStringEncoding]]; [_lbl setText:rslt]; _iv.image = [self createBoardImageInRect:_iv.frame]; }); }); } ``` 其余的 iOS 代码全部在`createBoardImageInRect`方法中,该方法使用 `UIBezierPath` 中的`moveToPoint`和`addLineToPoint`方法绘制面板: ```py - (UIImage *)createBoardImageInRect:(CGRect)rect { int margin_y = 170; UIGraphicsBeginImageContextWithOptions(CGSizeMake(rect.size.width, rect.size.height), NO, 0.0); UIBezierPath *path = [UIBezierPath bezierPath]; startX = (rect.size.width - 7*BOARD_COLUMN_WIDTH)/2.0; startY = rect.origin.y+margin_y+30; endY = rect.origin.y - margin_y + rect.size.height; for (int i=0; i<8; i++) { CGPoint point = CGPointMake(startX + i * BOARD_COLUMN_WIDTH, startY); [path moveToPoint:point]; point = CGPointMake(startX + i * BOARD_COLUMN_WIDTH, endY); [path addLineToPoint:point]; } CGPoint point = CGPointMake(startX, endY); [path moveToPoint:point]; point = CGPointMake(rect.size.width - startX, endY); [path addLineToPoint:point]; path.lineWidth = BOARD_LINE_WIDTH; [[UIColor blueColor] setStroke]; [path stroke]; ``` `bezierPathWithOvalInRect`方法绘制由 AI 和人工移动的所有碎片–根据谁先采取行动,它开始交替绘制碎片,但顺序不同: ```py int columnPieces[] = {0,0,0,0,0,0,0}; if (aiFirst) { for (int i=0; i endY) return true; int column = (int)((x-MARGINX)/columnWidth); for (int i=0; i<6; i++) if (mActivity.getBoard()[35+column-7*i] == 0) { mActivity.getBoard()[35+column-7*i] = MainActivity.HUMAN_PIECE; mActivity.getHumanMoves().add(35+column-7*i); break; } invalidate(); ``` 之后,将回合设置为 AI,如果游戏结束则返回。 否则,在人类可以触摸并选择下一步动作之前,让 AI 根据模型的策略返回进行下一步动作,以启动新线程继续玩游戏: ```py mActivity.setAiTurn(); if (mActivity.gameEnded(mActivity.getBoard())) { if (mActivity.aiWon(mActivity.getBoard())) mActivity.getTextView().setText("AI Won!"); else if (mActivity.aiLost(mActivity.getBoard())) mActivity.getTextView().setText("You Won!"); else if (mActivity.aiDraw(mActivity.getBoard())) mActivity.getTextView().setText("Draw"); return true; } Thread thread = new Thread(mActivity); thread.start(); break; default: return false; } return true; } ``` UI 的主要布局是在`activity_main.xml`中定义的,它由三个 UI 元素组成:`TextView`,自定义`BoardView`和`Button`: ```py