TensorFlow-智能移动项目-全-
TensorFlow 智能移动项目(全)
零、前言
人工智能(AI),是计算机中人类智能的模拟,历史悠久。 自 1956 年正式诞生以来,人工智能经历了数次兴衰。 持续不断的 AI 复兴或新的 AI 革命始于深度学习的突破。深度学习是机器学习的一个分支,由于深度学习,机器学习的分支现已成为 AI 最热门的分支。 深度卷积神经网络(DCNN)赢得了 ImageNet 大规模视觉识别挑战赛,其错误率仅为 16.4%,相比之下,第二好的非 DCNN 模型错误率为 26.2%。 自 2012 年以来,基于 DCNN 的改进条目每年都赢得了 ImageNet 挑战,并且深度学习技术已应用于计算机视觉以外的许多硬 AI 问题,例如语音识别,机器翻译和围棋游戏,从而取得了接连而至的突破。 2016 年 3 月,谷歌 DeepMind 的 AlphaGo 通过深度强化学习构建,击败了 18 届人类世界围棋冠军 Lee Sedol 4:1。 在 2017 年 Google I/O 大会上,Google 宣布他们正从移动优先转变为 AI 优先。 其他领先的公司,例如亚马逊,苹果,Facebook 和微软,都对 AI 进行了大量投资,并推出了许多基于 AI 的产品。
TensorFlow 是 Google 的开放源代码框架,用于构建机器学习 AI 应用。 自 2015 年 11 月首次发布以来,TensorFlow 已经有数个流行的开源深度学习框架,在不到两年的时间内,它已迅速成为最受欢迎的开源深度学习框架。 每周都会构建新的 TensorFlow 模型,以解决需要人类甚至是超人类智能的所有任务。 关于 TensorFlow 的数十本书已经出版。 TensorFlow 上的更多在线博客,教程,课程和视频已可用。 很明显,AI 和 TensorFlow 很热门,但为什么还要再加上一本书标题中带有“ TensorFlow”一词呢?
这是一本独特的书,也是第一本将 TensorFlow 驱动的 AI 与移动设备相结合的书,将最光明的未来与现在最繁荣的世界联系在一起。 在过去的十年中,我们都见证并经历了 iOS 和 Android 智能手机的革命,而我们刚刚开始的 AI 革命将对我们周围的世界产生更深远的影响。 还有什么比将两个世界的精华融合在一起的主题更好,这本书展示了如何在随时随地的移动设备上构建 TensorFlow AI 应用?
的确,您可以使用许多现有的云 AI API 来构建 AI 应用,有时这很有意义。 但是,在移动设备上完全运行 AI 应用的好处是,即使没有可用的网络连接,无法承受往返云服务器的费用或用户不想发送云服务时,您也可以运行应用,将手机上的数据发送给其他任何人。
确实,在 TensorFlow 开源项目中已经有一些 TensorFlow iOS 和 Android 应用示例,可以帮助您开始使用移动 TensorFlow。 但是,如果您曾经尝试在 iOS 或 Android 设备上运行令您惊讶的酷 TensorFlow 模型,则很可能会遇到很多麻烦,然后才能在手机上成功看到运行的模型。
本书向您展示了如何解决在移动平台上运行 TensorFlow 模型时可能遇到的所有常见问题,从而可以节省大量时间和精力。 您会在一书中看到超过 10 个完整的从零开始构建的 TensorFlow iOS 和 Android 应用,它们运行着各种凉爽的 TensorFlow 模型,包括最新和最酷的生成对抗网络(GAN)和类似 AlphaZero 的模型。
这本书是给谁的
如果您是 iOS 和/或 Android 开发人员,对构建和重新训练他人的酷 TensorFlow 模型并在移动应用中运行它们感兴趣,或者您是 TensorFlow 开发人员,并且想要在移动设备上运行新的惊人的 TensorFlow 模型, 这本书是给你的。 如果您对 Raspberry Pi 上的 TensorFlow Lite,Core ML 或 TensorFlow 感兴趣,您也将从本书中受益。
本书涵盖的内容
第 1 章,“移动 TensorFlow 入门”,讨论如何在 Mac 和 Ubuntu 上设置 TensorFlow 以及在 Ubuntu 上设置 NVIDIA GPU,以及如何设置 Xcode 和 Android 工作室。 我们还将讨论 TensorFlow Mobile 和 TensorFlow Lite 之间的区别以及何时使用它们。 最后,我们将向您展示如何运行示例 TensorFlow iOS 和 Android 应用。
第 2 章,“通过迁移学习对图像进行分类” ,介绍了什么是迁移学习以及为什么要使用迁移学习,如何使用它来重新训练 Inception v3 和 MobileNet 模型,以更准确,更快地识别犬种,以及如何在示例 iOS 和 Android 应用中使用经过重新训练的模型。 然后,我们将向您展示如何在 Tensor-C 和 Swift 中将 TensorFlow 添加到自己的 iOS 应用中,以及如何将自己的 Android 应用添加到狗品种识别中。
第 3 章,“检测对象及其位置”,快速概述了对象检测,然后向您展示如何设置 TensorFlow 对象检测 API 并使用它来重新训练 SSD-MobileNet 和 Faster RCNN 模型。 我们还将通过手动构建 TensorFlow iOS 库以支持非默认 TensorFlow 操作的方式,向您展示如何在 iOS 或 Android 示例 TensorFlow 应用中使用模型。 最后,我们将向您展示如何训练 YOLO2(另一个流行的对象检测模型),该模型也在示例 TensorFlow Android 应用中使用,以及如何在 iOS 应用中使用它。
第 4 章,“变换具有惊人艺术风格的图片” 首先概述了神经样式迁移在过去几年中的快速发展。 然后,它向您展示了如何训练快速的神经样式迁移模型,以及如何在 iOS 和 Android 应用中使用它们。 之后,我们将介绍如何在您自己的 iOS 和 Android 应用中使用 TensorFlow Magenta 多样式模型轻松创建令人惊叹的艺术风格。
第 5 章,“了解简单语音命令”概述了语音识别,并向您展示了如何训练简单语音命令识别模型。 然后,我们将向您展示如何使用 Objective-C 和 Swift 在 Android 和 iOS 中使用该模型。 我们还将介绍有关如何解决移动设备上可能出现的模型加载和运行错误的更多提示。
第 6 章,“用自然语言描述图像”,介绍了图像字幕的工作原理,然后介绍了如何训练和冻结图像字幕模型 TensorFlow。 我们将进一步讨论如何转换和优化复杂的模型,以使其可以在移动设备上运行。 最后,我们将使用该模型提供完整的 iOS 和 Android 应用,以生成图像的自然语言描述。
第 7 章,“使用 CNN 和 LSTM 识别绘画”,解释了绘画分类的工作原理,并讨论了如何训练,预测和准备模型。 然后,我们将向您展示如何构建另一个自定义 TensorFlow iOS 库,以在有趣的 iOS 涂鸦应用中使用该模型。 最后,我们将向您展示如何构建自定义的 TensorFlow Android 库来修复新的模型加载错误,然后在您自己的 Android 应用中使用该模型。
第 8 章,“使用 RNN 预测股票价格”带领您完成 RNN 以及如何使用它预测股票价格。 然后,我们将向您介绍如何使用 TensorFlow API 构建 RNN 模型以预测股价,以及如何使用易于使用的 Keras API 构建 RNN LSTM 模型以实现相同的目标。 我们将测试并查看此类模型是否可以击败随机购买或出售策略。 最后,我们将向您展示如何在 iOS 和 Android 应用中运行 TensorFlow 和 Keras 模型。
第 9 章,“使用 GAN 生成和增强图像”,概述了 GAN 是什么以及为什么它具有如此大的潜力。 然后,它将探讨如何构建和训练可用于生成类似人的手写数字的基本 GAN 模型以及可以将低分辨率图像增强为高分辨率图像的更高级模型。 最后,我们将介绍如何在 iOS 和 Android 应用中使用两种 GAN 模型。
第 10 章,“构建类似 AlphaZero 的手机游戏应用”,从最新和最酷的 AlphaZero 的工作原理以及如何在 TensorFlow 作为后端的 Keras 中,训练和测试 AlphaZero 的模型,开始玩一个简单但有趣的游戏,名为 Connect4。 然后,我们将向您展示完整的 iOS 和 Android 应用,以使用该模型并在您的移动设备上玩 Connect4 游戏。
第 11 章,“在移动设备上使用 TensorFlow Lite 和 Core ML”演示 TensorFlow Lite,然后向您展示如何使用预先构建的 TensorFlow 模型(经过重新训练) TensorFlow Lite 的 TensorFlow 模型以及 iOS 中的自定义 TensorFlow Lite 模型。 我们还将向您展示如何在 Android 中使用 TensorFlow Lite。 之后,我们将概述 Apple 的 Core ML,并向您展示如何在使用 Scikit-Learn 的标准机器学习中使用 Core ML。 最后,我们将介绍如何在 TensorFlow 和 Keras 中使用 Core ML。
第 12 章,“在 Raspberry Pi 上开发 TensorFlow 应用”,首先介绍如何设置 Raspberry Pi 并使其移动以及如何设置 Raspberry Pi 上的 TensorFlow。 然后,我们将介绍如何使用 TensorFlow 图像识别和音频识别模型,以及文本到语音和机器人移动 API,来构建可以移动,看,听和说的 Raspberry Pi 机器人。 最后,我们将详细讨论如何使用 OpenAI Gym 和 TensorFlow 在模拟环境中从头开始构建和训练强大的基于神经网络的强化学习策略模型,以使机器人学会保持平衡。
充分利用这本书
我们建议您先按顺序阅读前四章,然后运行这里。 这将帮助您确保已为 TensorFlow 移动应用开发设置了全部开发环境,并且知道如何将 TensorFlow 集成到自己的 iOS 和/或 Android 应用中。 如果您是 iOS 开发人员,还将学习如何在 TensorFlow 中使用 Objective-C 或 Swift,以及何时以及如何使用 TensorFlow pod 或手动 TensorFlow iOS 库。
然后,如果您需要构建自定义的 TensorFlow Android 库,请转到第 7 章,“使用 CNN 和 LSTM 识别绘画”,以及如果您想学习如何在您的移动应用中使用 Keras 模型,请查看第 8 章,“使用 RNN 预测股价”,和第 10 章, “构建类似 AlphaZero 的手机游戏应用” 。
如果您对 TensorFlow Lite 或 Core ML 更感兴趣,请阅读第 11 章,“在移动设备上使用 TensorFlow Lite 和 Core ML”,以及您对 Raspberry Pi 上的 TensorFlow 或 TensorFlow 中的强化学习最感兴趣,请跳至第 12 章,“在 Raspberry Pi 上开发 TensorFlow 应用”。
除此之外,您可以阅读第 5 至 10 章,以了解如何训练不同类型的 CNN,RNN,LSTM,GAN 和 AlphaZero 模型以及如何在移动设备上使用它们,也许是运行 iOS 和/ 或每章的 Android 应用程式,然后再研究详细的实作。 另外,您可以直接跳到最感兴趣的模型的任何章节。 请注意,稍后的章节可能会参考较早的章节以获取一些重复的细节,例如将 TensorFlow 自定义 iOS 库添加到 iOS 应用中的步骤,或通过构建 TensorFlow 自定义库来修复某些模型加载或运行错误的步骤。 但是,请放心,您不会迷路,或者至少我们已尽力提供了用户友好的分步教程,并偶尔参考了先前教程的某些步骤,以帮助您避免在构建移动 TensorFlow 应用时可能会遇到的一切陷阱,同时也避免了重复自己。
什么时候看这本书
人工智能,或其最热门的分支机器学习,或其最热门的分支深度学习,近年来发展迅速。 由 Google 支持并在所有开源机器学习框架中最受欢迎的开发人员社区的 TensorFlow 新版本也以更快的速度发布。 当我们于 2017 年 12 月开始写书时,最新的 TensorFlow 版本为 1.4.0,于 2017 年 11 月 2 日发布,之后的 1.5.0 是 2018 年 1 月 26 日发布的,而 1.6.0 是 2018 年 2 月 28 日发布的,1.7 2018 年 3 月 29 日为.0,2018 年 4 月 27 日为 1.8.0。本书中所有的 iOS,Android 和 Python 代码均已使用所有这些 TensorFlow 版本进行了测试。 不过,到您阅读本书时,最新的 TensorFlow 版本可能会晚于 1.8.0。
事实证明,您无需太担心 TensorFlow 的新版本。 本书中的代码很可能将在最新的 TensorFlow 版本上无缝运行。 在我们的测试中,在 TensorFlow 1.4、1.5、1.6、1.7 和 1.8 上运行应用时,我们根本没有进行任何代码更改。 可能是在更高版本中,默认情况下将支持更多 TensorFlow 操作,因此您无需构建自定义 TensorFlow 库,也可以以更简单的方式构建自定义 TensorFlow 库。
当然,我们不能保证所有代码在将来的所有 TensorFlow 版本中都不会更改而运行,但是无论书本是在什么时候阅读,无论现在还是几个月后,本书中都包含了所有详细的教程和故障排除技巧,您应该使用 TensorFlow 1.4-1.8 或更高版本,可以使阅读本书和在本书中运行应用的过程顺利进行。
由于我们必须在某些时候停止使用特定的 TensorFlow 版本才能出版该书,因此我们将继续在每个新的主要 TensorFlow 版本中测试运行该书中的所有代码,并相应地在源代码存储库中更新该书的代码和测试结果。 如果您对代码或书籍有任何疑问,也可以直接在存储库中发布问题。
另一个问题是 TensorFlow Mobile 和 TensorFlow Lite 之间的选择。 该书在大多数章节(1 至 10)中介绍了 TensorFlow Mobile。 TensorFlow Lite 可能是在移动设备上运行 TensorFlow 的未来,它在 Google I/O 2018 上仍处于开发人员预览版中,这就是 Google 希望您“使用 TensorFlow Mobile 覆盖生产案例”的原因。 即使在 TensorFlow Lite 正式发布后,根据 Google 的说法,“ TensorFlow Mobile 不会很快消失”-实际上,在本书出版之前我们测试了最新的 TensorFlow 1.8.0 版本,我们发现使用 TensorFlow Mobile 变得更加简单。
如果 TensorFlow Lite 在所有用例中完全取代 TensorFlow Mobile 的那一天(具有 Lite 更好的性能和更小的大小)终于到了,那么您从书本中学到的技能将只会为您做好准备。 同时,在那个不可预见的未来到来之前,您可以阅读这本书并了解如何使用 TensorFlow Mobile 这样的老大哥在您的移动应用中运行所有这些功能强大的 TensorFlow 模型。
下载示例代码文件
您可以从 www.packtpub.com 的帐户中下载本书的示例代码文件。 如果您在其他地方购买了此书,则可以访问 www.packtpub.com/support 并注册以将文件直接通过电子邮件发送给您。
您可以按照以下步骤下载代码文件:
-
登录或登录 www.packtpub.com 。
-
选择支持标签。
-
单击代码下载&勘误表。
-
在搜索框中输入书籍的名称,然后按照屏幕上的说明进行操作。
下载文件后,请确保使用以下最新版本解压缩或解压缩文件夹:
- Windows 的 WinRAR / 7-Zip
- Mac 版 Zipeg / iZip / UnRarX
- 适用于 Linux 的 7-Zip / PeaZip
本书的代码包也托管在 Packt GitHub 帐户上。 如果代码有更新,它将在现有的 GitHub 存储库中进行更新。
在这个页面 中,我们还提供了丰富的图书和视频目录中的其他代码包。 去看一下!
使用约定
本书中使用了许多文本约定。
CodeInText:表示文本中的词,数据库表名称,文件夹名称,文件名,文件扩展名,路径名,伪 URL,用户输入和 Twitter 句柄。 示例:“ 安装 matplotlib , pillow , lxml 和 jupyter 库。在 Ubuntu 或 Mac 上,您可以运行。
代码块设置如下:
syntax = "proto2";
package object_detection.protos;
message StringIntLabelMapItem {
optional string name = 1;
optional int32 id = 2;
optional string display_name = 3;
};
message StringIntLabelMap {
repeated StringIntLabelMapItem item = 1;
};
任何命令行输入或输出的编写方式如下:
sudo pip install pillow
sudo pip install lxml
sudo pip install jupyter
sudo pip install matplotlib
粗体:表示您在屏幕上看到的新术语,重要单词或顺序。 例如,菜单或对话框中的单词会出现在这样的文本中。 这是一个示例:“现在,我们选择 Enhance Image 选项,您将看到结果 ”
警告或重要提示显示如下,提示和技巧显示如下。
一、移动 TensorFlow 入门
本章介绍如何设置开发环境,以使用 TensorFlow 构建所有 iOS 或 Android 应用,本书其余部分对此进行了讨论。 我们不会详细讨论可用于开发的所有受支持的 TensorFlow 版本,OS 版本,Xcode 和 Android Studio 版本,因为可以在 TensorFlow 网站或通过 Google。 相反,我们将在本章中简要讨论示例工作环境,以便我们能够快速了解可使用该环境构建的所有出色应用。
如果您已经安装了 TensorFlow,Xcode 和 Android Studio,并且可以运行和测试示例 TensorFlow iOS 和 Android 应用,并且如果您已经安装了 NVIDIA GPU 以进行更快的深度学习模型训练,则可以跳过本章。 或者,您可以直接跳到您不熟悉的部分。
我们将在本章涵盖以下主题(如何设置 Raspberry Pi 开发环境将在第 12 章,“在 Raspberry Pi 上开发 TensorFlow 应用”中进行讨论):
- 设置 TensorFlow
- 设置 Xcode
- 设置 Android Studio
- TensorFlow Mobile 与 TensorFlow Lite
- 运行示例 TensorFlow iOS 应用
- 运行示例 TensorFlow Android 应用
设置 TensorFlow
TensorFlow 是领先的机器智能开源框架。 当 Google 在 2015 年 11 月将 TensorFlow 作为一个开源项目发布时,已经有其他一些类似的深度学习开源框架:Caffe,Torch 和 Theano。 在 5 月 10 日的 Google I/O 2018 上,GitHub 上的 TensorFlow 已达到 99000 星,在 4 个月内增加了 14k 星,而 Caffe 仅增加了 2k 至 24k 星。 两年后,它已经成为最流行的开源框架,用于训练和部署深度学习模型(它对传统机器学习也有很好的支持)。 截至 2018 年 1 月,TensorFlow 在 GitHub 上拥有近 8.5 万颗星,而其他三个领先的开源深度学习框架 Caffe,CNTK 和 Mxnet 分别拥有 22k,13k 和 12k 颗星。
如果您对机器学习,深度学习,机器智能和人工智能(AI)的流行语有些困惑,这里有个简短的摘要:机器智能和 AI 确实是同一回事。 机器学习是 AI 的一个领域,也是最受欢迎的领域; 深度学习是机器学习的一种特殊类型,也是解决诸如计算机视觉,语音识别和合成以及自然语言处理之类的复杂问题的现代且最有效的方法。 因此,在本书中,当我们说 AI 时,我们主要是指深度学习,这是将 AI 从漫长的冬天带到夏天的救星。 有关 AI 冬季和深度学习的更多信息,您可以查看这里和这里。
我们假设您已经对 TensorFlow 有了基本的了解,但是如果您还没有,请查看入门和教程部分或 Awesome TensorFlow 教程。 关于该主题的两本好书是《Python 机器学习:Python , scikit-learn 和 TensorFlow 机器学习和深度学习》和《使用 Scikit-Learn 和 TensorFlow 动手进行机器学习》。
TensorFlow 可以安装在 MacOS,Ubuntu 或 Windows 上。 我们将介绍在 MacOS X El Capitan(10.11.6),macOS Sierra(10.12.6)和 Ubuntu 16.04 上从源代码安装 TensorFlow 1.4 的步骤。 如果您使用其他操作系统或版本,则可以参考 TensorFlow 安装文档以获取更多信息。 当您阅读本书时,可能会出现更新的 TensorFlow 版本。 尽管您仍然应该能够使用较新版本运行本书中的代码,但这并不能保证,因此我们在 Mac 和 Ubuntu 上使用 TensorFlow 1.4 发行源代码来设置 TensorFlow; 这样,您可以轻松地测试运行并与书中的应用一起玩。
自从我们于 2017 年 12 月撰写以上段落以来,TensorFlow 已有四个新的正式版本(1.5、1.6、1.7 和 1.8),以及截至 2018 年 5 月的新版本 Xcode(9.3),您可以在以下位置下载, 或在 TensorFlow 源代码仓库。 TensorFlow 的较新版本(例如 1.8)默认情况下支持 NVIDIA CUDA 和 cuDNN 的较新版本(有关详细信息,请参阅“在 Ubuntu 上设置基于 GPU 的 TensorFlow”部分),并且最好遵循官方的 TensorFlow 文档来安装具有 GPU 支持的最新 TensorFlow 版本。 在本章及以下各章中,我们将以特定的 TensorFlow 版本为例,但将对所有 iOS,Android 和 Python 代码进行测试,并在需要时针对其中的最新 TensorFlow,Xcode 和 Android Studio 版本进行更新。 本书的源代码仓库位于这里。
总体而言,我们将在 Mac 上使用 TensorFlow 开发 iOS 和 Android TensorFlow 应用,并在 Ubuntu 上使用 TensorFlow 训练应用中使用的深度学习模型。
在 MacOS 上设置 TensorFlow
通常,您应该使用 VirtualEnv,Docker 或 Anaconda 安装在单独的环境中安装 TensorFlow。 但是由于我们必须使用 TensorFlow 源代码构建 iOS 和 Android TensorFlow 应用,因此我们不妨从源代码构建 TensorFlow 本身,在这种情况下,使用本机 PIP 安装选择可能比其他选择更容易。 如果您想尝试不同的 TensorFlow 版本,我们建议您使用 VirtualEnv,Docker 和 Anaconda 选项之一安装其他 TensorFlow 版本。 在这里,我们将使用本地 PIP 和 Python 2.7.10 直接在 MacOS 系统上安装 TensorFlow 1.4。
请按照以下步骤在 MacOS 上下载并安装 TensorFlow 1.4:
- 从 GitHub 上的 TensorFlow 发布页面下载 TensorFlow 1.4.0 源代码(
zip或tar.gz) - 解压缩下载的文件并将
tensorflow-1.4.0文件夹拖到您的主目录 - 确保已安装 Xcode 8.2.1 或更高版本(否则,请先阅读“设置 Xcode”部分)
- 打开一个新的终端窗口,然后单击
cd tensorflow-1.4.0 - 运行
xcode-select --install以安装命令行工具 - 运行以下命令以安装构建 TensorFlow 所需的其他工具和包:
sudo pip install six numpy wheel
brew install automake
brew install libtool
./configure
brew upgrade bazel
- 从 TensorFlow 源代码构建,仅提供 CPU 支持(我们将在下一部分介绍 GPU 支持),并生成带有
.whl文件扩展名的 PIP 包文件:
bazel build --config=opt //tensorflow/tools/pip_package:build_pip_package
bazel-bin/tensorflow/tools/pip_package/build_pip_package /tmp/tensorflow_pkg
- 安装 TensorFlow 1.4.0 CPU 包:
sudo pip install --upgrade /tmp/tensorflow_pkg/tensorflow-1.4.0-cp27-cp27m-macosx_10_12_intel.whl
老实说,如果您在此过程中遇到任何错误,则搜索错误消息应该是修复该错误的最佳方法,因为我们打算在本书中重点介绍从我们长期积累的技巧和知识,从数小时的构建和调试实用的移动 TensorFlow 应用中获取,它们在其他地方不易获得。 运行sudo pip install命令时,您可能会看到的一个特定错误是Operation not permitted错误。 要解决此问题,您可以通过重新启动 Mac 并点击Cmd + R键来禁用 Mac 的系统完整性保护(SIP) 要进入恢复模式,请在工具终端下,在重新启动 Mac 之前运行csrutil disable。 如果您对禁用 SIP 不满意,可以按照 TensorFlow 文档尝试使用更复杂的安装方法之一,例如 VirtualEnv。
如果一切顺利,则应该能够在终端窗口,Python 或最好在 IPython 上运行,然后运行import tensorflow as tf和tf.__version__将 1.4.0 作为输出。
在 GPU 驱动的 Ubuntu 上设置 TensorFlow
使用良好的深度学习框架(例如 TensorFlow)的好处之一是在模型训练中无缝支持使用图形处理单元(GPU) 。 在 GPU 上训练非平凡的基于 TensorFlow 的模型要比在 CPU 上训练要快得多,并且当前 NVIDIA 提供 TensorFlow 支持的最佳和最具成本效益的 GPU。 Ubuntu 是使用 TensorFlow 运行 NVIDIA GPU 的最佳操作系统。 您可以花几百美元轻松购买一个 GPU,然后将其安装在带有 Ubuntu 系统的廉价台式机上。 您也可以在 Windows 上安装 NVIDIA GPU,但 TensorFlow 对 Windows 的支持不如对 Ubuntu 的支持。
为了训练本书中应用中部署的模型,我们使用 NVIDIA GTX 1070,您可以在 Amazon 或 eBay 上以大约 400 美元的价格购买。 蒂姆·戴特默斯(Tim Dettmers)有一个不错的博客,其中介绍了用于深度学习的 GPU。 在获得这样的 GPU 并将其安装在 Ubuntu 系统上之后,以及在安装启用 GPU 的 TensorFlow 之前,您需要安装 NVIDIA CUDA 8.0(或 9.0)和 cuDNN(CUDA-DeepNeuralNetwork)6.0 (或 7.0),两者均受 TensorFlow 1.4 支持。
使用 TensorFlow 设置自己的 GPU 驱动的 Ubuntu 的另一种方法是在支持 GPU 的云服务(例如 Google Cloud Platform 的 Cloud ML Engine)中使用 TensorFlow。 每个选项都有优点和缺点。 云服务通常是基于时间的计费。 如果您的目标是训练或重新训练要在移动设备上部署的模型,这意味着模型并不复杂,并且如果您计划长时间进行机器学习训练,那么拥有自己的 GPU 成本效益更高并且令人满意。
请按照以下步骤在 Ubuntu 16.04 上安装 CUDA 8.0 和 cuDNN 6.0(您应该能够以类似的方式下载并安装 CUDA 9.0 和 cuDNN 7.0):
- 在这个页面中找到 NVIDIA CUDA 8.0 GA2 版本,并进行以下屏幕截图中所示的选择:

图 1.1:准备在 Ubuntu 16.04 上下载 CUDA 8.0
- 下载基本安装程序,如以下屏幕快照所示:

图 1.2:为 Ubuntu 16.04 选择 CUDA 8.0 安装程序文件
- 打开一个新的终端并运行以下命令(您还需要将最后两个命令添加到
.bashrc文件中,以使两个环境变量在您下次启动新终端时生效):
sudo dpkg -i /home/jeff/Downloads/cuda-repo-ubuntu1604-8-0-local-ga2_8.0.61-1_amd64.deb
sudo apt-get update
sudo apt-get install cuda-8-0
export CUDA_HOME=/usr/local/cuda
export LD_LIBRARY_PATH=/usr/local/cuda/lib64:$LD_LIBRARY_PATH
- 通过这里下载用于 CUDA 8.0 的 NVIDIA cuDNN 6.0,您将被要求先免费注册一个 NVIDIA 开发者帐户(免费)。 您可以下载它,如下一个屏幕截图所示(选择突出显示的 cuDNN v6.0 Linux 版):

图 1.3:在 Linux 上为 CUDA 8.0 选择 cuDNN 6.0
- 假设下载的文件位于默认的
~/Downloads目录下,请解压缩该文件,然后您会看到一个名为cuda的文件夹,其中包含两个名为include和lib64的子文件夹 - 将 cuDNN
include和lib64文件复制到CUDA_HOME的lib64和include文件夹中:
sudo cp ~/Downloads/cuda/lib64/* /usr/local/cuda/lib64
sudo cp ~/Downloads/cuda/include/cudnn.h /usr/local/cuda/include
现在我们准备在 Ubuntu 上安装启用 GPU 的 TensorFlow 1.4(此处给出的前两个步骤与在 MacOS 上设置 TensorFlow 一节中描述的步骤相同):
- 从 GitHub 上的 TensorFlow 发布页面下载 TensorFlow 1.4.0 源代码(
zip或tar.gz) - 解压缩下载的文件并将文件夹拖到主目录
- 从这里下载 bazel 安装程序
- 打开一个新的终端窗口,然后运行以下命令以安装构建 TensorFlow 所需的工具和包:
sudo pip install six numpy wheel
cd ~/Downloads
chmod +x bazel-0.5.4-installer-linux-x86_64.sh
./bazel-0.5.4-installer-linux-x86_64.sh --user
- 从具有 GPU 支持的 TensorFlow 源进行构建,并生成带有
.whl文件扩展名的 pip 包文件:
cd ~/tensorflow-1.4.0
./configure
bazel build --config=opt --config=cuda //tensorflow/tools/pip_package:build_pip_package
bazel-bin/tensorflow/tools/pip_package/build_pip_package /tmp/tensorflow_pkg
- 安装 TensorFlow 1.4.0 GPU 包:
sudo pip install --upgrade /tmp/tensorflow_pkg/tensorflow-1.4.0-cp27-cp27mu-linux_x86_64.whl
现在,如果一切顺利,您可以启动 IPython 并输入以下脚本以查看 TensorFlow 使用的 GPU 信息:
In [1]: import tensorflow as tf
In [2]: tf.__version__
Out[2]: '1.4.0'
In [3]: sess=tf.Session()
2017-12-28 23:45:37.599904: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:892] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
2017-12-28 23:45:37.600173: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1030] Found device 0 with properties:
name: GeForce GTX 1070 major: 6 minor: 1 memoryClockRate(GHz): 1.7845
pciBusID: 0000:01:00.0
totalMemory: 7.92GiB freeMemory: 7.60GiB
2017-12-28 23:45:37.600186: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1120] Creating TensorFlow device (/device:GPU:0) -> (device: 0, name: GeForce GTX 1070, pci bus id: 0000:01:00.0, compute capability: 6.1)
恭喜你! 现在,您就可以训练本书中应用中使用的深度学习模型了。 在我们开始玩我们的新玩具并用它来训练我们的炫酷模型然后在移动设备上部署和运行它们之前,我们首先来看看准备好开发移动应用需要做什么。
设置 Xcode
Xcode 用于开发 iOS 应用,您需要 Mac 电脑和免费的 Apple ID 才能下载和安装它。 如果您的 Mac 相对较旧并且使用 OS X El Capitan(版本 10.11.6),则可以从这里下载 Xcode 8.2.1。 或者,如果您安装了 macOS Sierra(10.12.6 版)或更高版本,则可以从前面的链接下载 Xcode 9.2 或 9.3(截至 2018 年 5 月的最新版本)。 本书中的所有 iOS 应用均已在 Xcode 8.2.1、9.2 和 9.3 中进行了测试。
要安装 Xcode,只需双击下载的文件,然后按照屏幕上的步骤进行操作。 这很简单。 现在,您可以在 Xcode 随附的 iOS 模拟器或您自己的 iOS 设备上运行应用。 从 Xcode 7 开始,您可以在 iOS 设备上免费运行和调试 iOS 应用,但如果要分发或发布您的应用,则需要以每年 99 美元的价格参加 Apple 开发人员计划。
尽管您可以使用 Xcode 模拟器测试运行书中的许多应用,但是书中的某些应用需要使用实际的 iOS 设备上的相机拍摄照片,然后才能使用经过 TensorFlow 训练的深度学习模型对其进行处理。 此外,通常最好在实际设备上测试模型的准确表现和内存使用情况:在模拟器中运行良好的模型可能会崩溃或在实际设备中运行太慢。 因此,强烈建议或要求您(如果并非总是)至少在您的实际 iOS 设备上测试并运行本书中的 iOS 应用一次。
本书假定您熟悉 iOS 编程,但是如果您不熟悉 iOS 开发,则可以从许多出色的在线教程中学习,例如 Ray Wenderlich 的 iOS 教程。 我们不会介绍复杂的 iOS 编程; 我们将主要向您展示如何在我们的 iOS 应用中使用 TensorFlow C++ API 来运行 TensorFlow 训练有素的模型来执行各种智能任务。 Apple 的两种官方 iOS 编程语言 Objective-C 和 Swift 代码都将用于与我们的移动 AI 应用中的 C++ 代码进行交互。
设置 Android Studio
Android Studio 是开发 Android 应用的最佳工具,并且 TensorFlow 对其使用提供了强大的支持。 与 Xcode 不同,您可以在 Mac,Windows 或 Linux 上安装并运行 Android Studio。 有关详细的系统要求,请参阅 Android Studio 网站。 在这里,我们将介绍如何在 Mac 上设置 Android Studio 3.0 或 3.0.1-本书中的所有应用均已在两个版本上进行了测试。
首先,从前面的链接下载 Android Studio 3.0.1,如果最新版本是 3.0.1 以上,并且您不介意解决可能的小问题,请下载最新版本。 您也可以从这里的存档中下载 3.0.1 或 3.0。
然后,双击下载的文件并将Android Studio.app图标拖放到Applications。 如果您以前安装了 Android Studio,则系统会提示您是否要用较新的 Android Studio 替换它。 您只需选择替换即可。
接下来,打开 Android Studio,您需要提供 Android SDK 的路径,如果您安装了以前版本的 Android Studio,则默认情况下它位于~/Library/Android/sdk中,或者您可以选择打开现有的 Android Studio 项目,然后转到“在 MacOS 上设置 TensorFlow”一节中创建的 TensorFlow 1.4 源目录,然后打开tensorflow/examples/android文件夹。 之后,您可以通过单击安装工具消息的链接或转到 Android Studio 的Tools | Android | SDK Manager来下载 Android SDK,如以下屏幕截图所示。 从 SDK 工具标签中,您可以选中特定版本的 Android SDK 工具旁边的框,然后单击确定按钮以安装该版本:

图 1.4:Android SDK Manager,用于安装 SDK 工具和 NDK
最后,由于 TensorFlow Android 应用使用 C++ 中的本机 TensorFlow 库来加载和运行 TensorFlow 模型,因此您需要安装 Android 本机开发套件(NDK),您可以执行以下任一操作,从上一个屏幕快照中显示的 Android SDK Manager 中获取,或者直接从这里下载 NDK。 NDK 版本 r16b 和 r15c 均已通过测试可运行本书中的 Android 应用。 如果直接下载 NDK,则在打开项目并选择 Android Studio 的File | Project Structure后,您可能还需要设置 Android NDK 位置,如以下屏幕截图所示:

图 1.5:设置项目级别的 Android NDK 位置
安装并设置了 Android SDK 和 NDK 之后,您就可以测试运行示例 TensorFlow Android 应用了。
TensorFlow Mobile 与 TensorFlow Lite
在我们开始运行示例 TensorFlow iOS 和 Android 应用之前,让我们澄清一下。 TensorFlow 当前有两种在移动设备上开发和部署深度学习应用的方法:TensorFlow Mobile 和 TensorFlow Lite。 TensorFlow Mobile 从一开始就是 TensorFlow 的一部分,而 TensorFlow Lite 是开发和部署 TensorFlow 应用的较新方法,因为它具有更好的性能和更小的应用大小。 但是有一个关键因素可以让我们在本书中专注于 TensorFlow Mobile,同时仍在一个章节中介绍 TensorFlow Lite:从 TensorFlow 1.8 和 2018 年 5 月的 Google I/O 开始,TensorFlow Lite 仍在开发人员预览版中。 现在,准备投入生产的移动 TensorFlow 应用,您必须按照 Google 的建议使用 TensorFlow Mobile。
我们决定现在专注于 TensorFlow Mobile 的另一个原因是,虽然 TensorFlow Lite 仅对模型运算符提供了有限的支持,但 TensorFlow Mobile 支持自定义以添加默认情况下 TensorFlow Mobile 不支持的新运算符,您会发现它经常发生在我们的各种模型中 AI 应用的模型。
但是将来,当 TensorFlow Lite 不在开发人员预览版中时,它很可能会取代 TensorFlow Mobile,或者至少克服其当前的局限性。 为了为此做好准备,我们将在下一章中详细介绍 TensorFlow Lite。
运行示例 TensorFlow iOS 应用
在本章的最后两部分中,我们将测试运行 TensorFlow 1.4 随附的三个示例 iOS 应用和四个示例 Android 应用,以确保您正确设置了移动 TensorFlow 开发环境并快速预览了其中的一些内容 TensorFlow 移动应用可以做到。
三个示例 TensorFlow iOS 应用的源代码位于tensorflow/examples/ios: simple,camera和benchmark。 为了成功运行这些样本,您需要首先下载一个由 Google 训练的深度学习模型,称为 Inception,用于图像识别。 Inception 有多个版本:v1 到 v4,在每个较新的版本中准确率更高。 在这里,我们将使用 Inception v1 作为为其开发的示例。 下载模型文件后,将与模型相关的文件复制到每个样本的data文件夹中:
curl -o ~/graphs/inception5h.zip https://storage.googleapis.com/download.tensorflow.org/models/inception5h.zip
unzip ~/graphs/inception5h.zip -d ~/graphs/inception5h
cd tensorflow/examples/ios
cp ~/graphs/inception5h/* simple/data/
cp ~/graphs/inception5h/* camera/data/
cp ~/graphs/inception5h/* benchmark/data/
现在,在打开和运行应用之前,转到每个app文件夹并运行以下命令以下载每个应用所需的 Pod:
cd simple
pod install
open tf_simple_example.xcworkspace
cd ../camera
pod install
open tf_camera_example.xcworkspace
cd ../benchmark
pod install
open tf_benchmark_example.xcworkspace
然后,您可以在 iOS 设备上运行这三个应用,或者在 iOS 模拟器上运行简单和基准的应用。 如果在运行简单应用后点击运行模型按钮,您将看到一条文本消息,提示已加载 TensorFlow Inception 模型,随后是几个顶级识别结果以及置信度值。
如果在运行基准测试应用后点击 Benchmark Model 按钮,您将看到运行模型超过 20 次的平均时间。 例如,在我的 iPhone 6 上平均需要大约 0.2089 秒,在 iPhone 6 模拟器上平均需要 0.0359 秒。
最后,在 iOS 设备上运行照相机应用并将其对准相机可以向您实时显示该应用看到和识别的对象。
运行示例 TensorFlow Android 应用
tensorflow/examples/android中有四个样本 TensorFlow Android 应用,分别为 TF 分类,TF 检测,TF 语音和 TF 风格化。 运行这些示例的最简单方法是使用 Android Studio 在前面的文件夹中打开项目,如“设置 Android Studio”部分中所示,然后通过编辑项目的build.gradle文件进行单个更改,并将def nativeBuildSystem = 'bazel'更改为def nativeBuildSystem = 'none'。
现在,将 Android 设备连接到您的计算机,然后通过选择 Android Studio 的Run | Run 'android'构建,安装和运行该应用。这会在您的设备上安装四个名称为“TF 分类”,“TF 检测”,“TF 语音”和“TF 风格化”的 Android 应用。 “TF 分类”就像 iOS 相机应用一样,使用 TensorFlow Inception v1 模型对设备相机进行实时对象分类。 “TF 检测”使用另一种模型,称为单发多框检测器(SSD)和 MobileNet,这是 Google 发布的一组新的深度学习模型,专门针对移动和嵌入式设备, 要执行对象检测,请在检测到的对象上绘制矩形。 “TF 语音”使用另一种不同的深度学习(语音识别)模型来收听和识别一小部分单词,例如Yes,No,Left,Right,Stop和Start。 “TF 风格化”使用另一种模型来更改相机看到的图像样式。 有关这些应用的更多详细信息,您可以在这个页面中查看 TensorFlow Android 示例文档。
总结
在本章中,我们介绍了如何在 Mac 和 Ubuntu 上安装 TensorFlow 1.4,如何在 Ubuntu 上设置具有成本效益的 NVIDIA GPU 以便进行更快的模型训练以及如何为移动 AI 应用开发设置 Xcode 和 Android Studio。 我们还向您展示了如何运行一些很酷的 TensorFlow 示例 iOS 和 Android 应用。 在本书的其余部分,我们将详细讨论如何在基于 GPU 的 Ubuntu 系统上构建和训练或重新训练应用中使用的每个模型以及其他模型,并向您展示如何在以下环境中部署模型 iOS 和 Android 应用,并编写代码以在移动 AI 应用中使用模型。 现在我们已经准备就绪,我们已经迫不及待要上路了。 这将是一段激动人心的旅程,我们当然很乐意与朋友分享这一旅程。 那么,为什么不从我们最好的朋友开始,让我们看看构建狗品种识别应用需要什么呢?
二、通过迁移学习对图像进行分类
上一章中描述的示例 TensorFlow iOS 应用,Simple 和 Camera 以及 Android 应用“TF 分类”都使用了 Inception v1 模型,该模型是 Google 公开提供的预训练的图像分类深度神经网络模型。 该模型针对 ImageNet 进行了训练,ImageNet 是最大和最知名的图像数据库之一,其中有超过一千万个图像被标注为对象类别。 Inception 模型可用于将图像分类为列出的 1,000 个类别之一。 这 1000 个对象类别包括很多对象中的很多犬种。 但是,识别狗品种的准确率不是很高,约为 70%,因为模型经过训练可以识别大量对象,而不是像狗品种之类的特定对象。
如果我们想提高准确率并在使用改进模型的智能手机上构建移动应用怎么办,那么当我们四处走走并看到一只有趣的狗时,我们可以使用该应用告诉我们它是哪种狗。
在本章中,我们将首先讨论为什么对于这样的图像分类任务,迁移学习或重新训练经过预训练的深度学习模型是完成任务的最经济有效的方法。 然后,我们将向您展示如何使用良好的狗数据集对一些最佳图像分类模型进行再训练,以及在第 1 章,“移动 TensorFlow 入门”。 此外,我们还将分步说明如何将 TensorFlow 添加到基于 Objective-C 或 Swift 的 iOS 和 Android 应用中。
总而言之,我们将在本章中介绍以下主题:
- 迁移学习 - 什么和为什么
- 将 Inception v3 模型用于再训练
- 将 MobileNet 模型用于再训练
- 在示例 iOS 应用中使用经过重新训练的模型
- 在示例 Android 应用中使用经过重新训练的模型
- 将 TensorFlow 添加到您自己的 iOS 应用中
- 将 TensorFlow 添加到您自己的 Android 应用中
迁移学习 -- 什么和为什么
我们人类不会从头开始学习新事物。 取而代之的是,无论是否有意识地,我们都充分利用所学到的知识。 人工智能中的迁移学习试图做同样的事情-这种技术通常只需要训练的大型模型中的一小块,然后将其重新用于相关任务的新模型中,而无需访问大型训练数据和计算资源来训练原始模型。 总体而言,迁移学习仍然是 AI 中的一个开放问题,因为在许多情况下,仅需人类反复尝试几个例子,然后再学习掌握新事物,就会花很多时间来训练和学习 AI。 但是,在图像识别领域,迁移学习已被证明是非常有效的。
用于图像识别的现代深度学习模型通常是深度神经网络,或更具体地说,是具有许多层的深度卷积神经网络(CNN)。 这种 CNN 的较低层负责学习和识别较低层的特征,例如图像的边缘,轮廓和零件,而最后一层则确定图像的类别。 对于不同类型的对象,例如犬种或花朵类型,我们不需要重新学习网络较低层的参数或权重。 实际上,从头开始需要花费数周的训练来学习用于图像识别的现代 CNN 的所有权重,通常是数百万甚至更多。 在图像分类的情况下,迁移学习使我们能够使用特定的图像集重新训练此类 CNN 的最后一层,通常不到一小时,而所有其他层都保持不变,并且达到了几乎相同的精度,就像我们从头开始训练整个网络数周一样。
迁移学习的第二个主要好处是,我们只需要少量的训练数据就可以重新训练 CNN 的最后一层。 如果必须从头开始训练深层 CNN 的数百万个参数,则需要大量的训练数据。 例如,对于我们的狗品种再训练,我们只需要为每个狗品种提供 100 幅以上的图像,即可建立一个比原始图像分类模型更好的狗品种分类模型。
如果您不熟悉 CNN,请查看其中的最佳资源之一的视频和评论,这是 Stanford CS231n 课程“用于视觉识别的 CNN”。 CNN 的另一个很好的资源是 Michael Nielsen 的在线书籍《神经网络和深度学习》的第 6 章。
在接下来的两个部分中,我们将使用针对 TensorFlow 的两个最佳的经过预训练的 CNN 模型和一个犬种数据集来重新训练模型并生成更好的犬种识别模型。 第一个模型是 Inception v3,它是比 Inception v1 更准确的模型,已针对准确率进行了优化,但大小较大。 另一个模型是 MobileNet,它针对移动设备的大小和效率进行了优化。 TensorFlow 支持的预训练模型的详细列表位于这里。
将 Inception v3 模型用于再训练
在上一章中设置的 TensorFlow 源代码中,有一个 Python 脚本tensorflow/examples/image_retraining/retrain.py,可用于重新训练 Inception v3 或 MobileNet 模型。 在运行脚本以重新训练 Inception v3 模型以进行狗品种识别之前,我们需要首先下载斯坦福狗数据集, 120 个犬种的图片(您只需要在链接中下载图片,而不是标注即可)。
在~/Downloads中解压缩下载的狗images.tar文件,您应该在~/Downloads/Images中看到文件夹列表,如以下屏幕截图所示。 每个文件夹对应一个犬种,并且包含约 150 张图像(您无需为图像提供显式标签,因为文件夹名称用于标记文件夹中包含的图像):

图 2.1:由文件夹或狗的品种分开的狗集图像
您可以下载数据集,然后在 Mac 上运行retrain.py脚本,因为该脚本在相对位置上运行不会花费太长时间(少于一小时) 小型数据集(总共约 20,000 张图像),但是,如上一章所述,如果您在 GPU 驱动的 Ubuntu 上执行此操作,则该脚本仅需几分钟即可完成。 此外,当使用大型图像数据集进行再训练时,在 Mac 上运行可能需要花费数小时或数天,因此在 GPU 驱动的计算机上运行它是有意义的。
假设您已经创建了/tf_file目录和/tf_file/dogs_bottleneck目录,那么重新训练模型的命令如下:
python tensorflow/examples/image_retraining/retrain.py
--model_dir=/tf_files/inception-v3
--output_graph=/tf_files/dog_retrained.pb
--output_labels=/tf_files/dog_retrained_labels.txt
--image_dir ~/Downloads/Images
--bottleneck_dir=/tf_files/dogs_bottleneck
这五个参数在这里需要一些解释:
--model_dir指定应该由retrain.py自动下载 Inception v3 模型的目录路径,除非它已经在目录中。--output_graph表示再训练模型的名称和路径。--output_labels是由图像数据集的文件夹(标签)名称组成的文件,稍后将其与经过重新训练的模型一起使用以对新图像进行分类。--image_dir是用于重新训练 Inception v3 模型的图像数据集的路径。--bottleneck_dir用于缓存在瓶颈(最后一层之前的那一层)上生成的结果; 最后一层使用这些结果进行分类。 在重新训练期间,每个映像都使用了几次,但该映像的瓶颈值保持不变,即使将来重新运行重新训练脚本也是如此。 因此,第一次运行需要更长的时间,因为它需要创建瓶颈结果。
在再训练期间,您将每 10 步看到 3 个值,默认总计 4,000 步。 前 20 个步骤和后 20 个步骤以及最终精度如下所示:
INFO:tensorflow:2018-01-03 10:42:53.127219: Step 0: Train accuracy = 21.0%
INFO:tensorflow:2018-01-03 10:42:53.127414: Step 0: Cross entropy = 4.767182
INFO:tensorflow:2018-01-03 10:42:55.384347: Step 0: Validation accuracy = 3.0% (N=100)
INFO:tensorflow:2018-01-03 10:43:11.591877: Step 10: Train accuracy = 34.0%
INFO:tensorflow:2018-01-03 10:43:11.592048: Step 10: Cross entropy = 4.704726
INFO:tensorflow:2018-01-03 10:43:12.915417: Step 10: Validation accuracy = 22.0% (N=100)
...
...
INFO:tensorflow:2018-01-03 10:56:16.579971: Step 3990: Train accuracy = 93.0%
INFO:tensorflow:2018-01-03 10:56:16.580140: Step 3990: Cross entropy = 0.326892
INFO:tensorflow:2018-01-03 10:56:16.692935: Step 3990: Validation accuracy = 89.0% (N=100)
INFO:tensorflow:2018-01-03 10:56:17.735986: Step 3999: Train accuracy = 93.0%
INFO:tensorflow:2018-01-03 10:56:17.736167: Step 3999: Cross entropy = 0.379192
INFO:tensorflow:2018-01-03 10:56:17.846976: Step 3999: Validation accuracy = 90.0% (N=100)
INFO:tensorflow:Final test accuracy = 91.0% (N=2109)
训练精度是神经网络用于训练的图像上的分类精度,而验证精度是神经网络未用于训练的图像上的验证精度。 因此,验证准确率是衡量模型准确率的一种更可靠的度量,并且通常应该比训练准确率小一点,但是如果训练收敛并进行得很好,也就是说,训练的模型是否既不欠拟合也不过拟合。
如果训练精度很高,但验证精度仍然很低,则意味着模型过拟合。 如果训练精度仍然很低,则表明模型不适合。 同样,交叉熵是损失函数值,如果再训练顺利进行,则总体上应该越来越小。 最后,测试准确率取决于尚未用于训练或验证的图像。 通常,这是我们可以说出的关于重新训练模型的最准确的值。
如前面的输出所示,在再训练结束时,我们看到验证精度与训练精度相似(90% 和 93%,相比之初为 3% 和 21%),最终测试精度为 91% 。 交叉熵也从开始时的 4.767 下降到最后的 0.379。 因此,我们现在有了一个很好的再训练狗品种识别模型。
为了进一步提高准确率,您可以使用retrain.py的其他参数(例如训练步骤) (--how_many_training_steps),学习率(--learning_rate),和数据扩充(--flip_left_right, --random_crop, --random_scale, --random_brightness). 通常,这是一个乏味的过程,涉及到许多“肮脏的工作”,这是最著名的深度学习专家之一 Andrew Ng 在他的“应用深度学习的基本原理”演讲中提到的(视频可在以下位置找到)。
您可以使用另一个 Python 脚本label_image来对经过重新训练的模型进行快速测试,以测试自己的图像(例如/tmp/lab1.jpg中的 Labrador Retriever 图像),您可以在首先对其进行如下构建后运行该脚本:
bazel build tensorflow/examples/image_retraining:label_image
bazel-bin/tensorflow/examples/label_image/label_image
--graph=/tf_files/dog_retrained.pb
--image=/tmp/lab1.jpg
--input_layer=Mul
--output_layer=final_result
--labels=/tf_files/dog_retrained_labels.txt
您会看到与以下内容相似的前五个分类结果(但是,因为网络随机变化,可能不完全相同)如下:
n02099712 labrador retriever (41): 0.75551
n02099601 golden retriever (64): 0.137506
n02104029 kuvasz (76): 0.0228538
n02090379 redbone (32): 0.00943663
n02088364 beagle (20): 0.00672507
--input_layer (Mul)和--output_layer (final_result)的值非常重要–必须与模型中定义的值相同,这样分类才能完全起作用。 如果您想知道如何获取它们(从图,aka 模型,文件dog_retrained.pb中获取),则有两个 TensorFlow 工具可能会有所帮助。 第一个是适当命名的summarize_graph。 这是构建和运行它的方法:
bazel build tensorflow/tools/graph_transforms:summarize_graph
bazel-bin/tensorflow/tools/graph_transforms/summarize_graph --in_graph=/tf_files/dog_retrained.pb
您将看到类似于以下内容的摘要结果:
No inputs spotted.
No variables spotted.
Found 1 possible outputs: (name=final_result, op=Softmax)
Found 22067948 (22.07M) const parameters, 0 (0) variable parameters, and 99 control_edges
Op types used: 489 Const, 101 Identity, 99 CheckNumerics, 94 Relu, 94 BatchNormWithGlobalNormalization, 94 Conv2D, 11 Concat, 9 AvgPool, 5 MaxPool, 1 DecodeJpeg, 1 ExpandDims, 1 Cast, 1 MatMul, 1 Mul, 1 PlaceholderWithDefault, 1 Add, 1 Reshape, 1 ResizeBilinear, 1 Softmax, 1 Sub
有一个可能的输出,名称为final_result。 不幸的是,有时summarize_graph工具没有告诉我们输入名称,因为它似乎对用于训练的节点感到困惑。 删除仅用于训练的节点(我们将在稍后讨论)之后,summarize_graph工具将返回正确的输入名称。 另一个名为 TensorBoard 的工具为我们提供了更完整的模型图。 如果直接从二进制文件安装了 TensorFlow,则应该可以只运行 TensorBoard,因为默认情况下,它安装在/usr/local/bin中。 但是,如果像我们之前那样从源代码安装 TensorFlow,则可以运行以下命令来构建 TensorBoard:
git clone https://github.com/tensorflow/tensorboard
cd tensorboard/
bazel build //tensorboard
现在,确保您具有/tmp/retrained_logs,在运行retrain.py时自动创建并运行:
bazel-bin/tensorboard/tensorboard --logdir /tmp/retrain_logs
然后在浏览器中启动 URL http://localhost:6006。 首先,您将看到准确率图,如以下屏幕截图所示:

图 2.2:Inception v3 重新训练模型的训练和验证准确率
在下面的屏幕截图中的交叉熵图,和我们之前对于运行retrain.py的输出所描述的一样:

图 2.3:Inception v3 重新训练模型的训练和验证交叉熵
现在单击GRAPHS选项卡,您将看到一个名为Mul的操作,另一个名为final_result的操作 ,如下所示:

图 2.4:重新训练模型中的Mul和final_result节点
实际上,如果您希望与 TensorFlow 进行小的交互,则可以尝试几行 Python 代码来找出输出层和输入层的名称,如 iPython 交互所示:
In [1]: import tensorflow as tf
In [2]: g=tf.GraphDef()
In [3]: g.ParseFromString(open("/tf_files/dog_retrained.pb", "rb").read())
In [4]: x=[n.name for n in g.node]
In [5]: x[-1:]
Out[5]: [u'final_result']
请注意,由于无法保证节点的顺序,因此此代码段并不总是有效,但它通常会为您提供所需的信息或验证。
现在,我们准备讨论如何进一步修改重新训练的模型,以便可以在移动设备上部署和运行它。 重新训练的模型文件dog_retrained.pb的大小太大,大约 80MB,在部署到移动设备之前,应该经过两个步骤进行优化:
- 去除未使用的节点:删除模型中仅在训练期间使用但在推理期间不需要的节点。
- 量化模型:将模型参数的所有 32 位浮点数转换为 8 位值。 这样可以将模型大小减小到其原始大小的 25%,同时保持推理精度大致相同。
TensorFlow 文档提供有关量化及其工作原理的更多详细信息。
有两种方法可以执行前面的两个任务:使用strip_unused工具的旧方法和使用transform_graph工具的新方法。
让我们看看旧方法的工作原理:首先运行以下命令以创建一个模型,其中所有未使用的节点都将被删除:
bazel build tensorflow/python/tools:strip_unused
bazel-bin/tensorflow/python/tools/strip_unused
--input_graph=/tf_files/dog_retrained.pb
--output_graph=/tf_files/stripped_dog_retrained.pb
--input_node_names=Mul
--output_node_names=final_result
--input_binary=true
如果在输出图中运行前面的 Python 代码,则可以找到正确的输入层名称:
In [1]: import tensorflow as tf
In [2]: g=tf.GraphDef()
In [3]: g.ParseFromString(open("/tf_files/ stripped_dog_retrained.pb", "rb").read())
In [4]: x=[n.name for n in g.node]
In [5]: x[0]
Out[5]: [u'Mul']
现在运行以下命令来量化模型:
python tensorflow/tools/quantization/quantize_graph.py
--input=/tf_files/stripped_dog_retrained.pb
--output_node_names=final_result
--output=/tf_files/quantized_stripped_dogs_retrained.pb
--mode=weights
之后,可以在 iOS 和 Android 应用中部署和使用模型quantized_stripped_dogs_retrained.pb,我们将在本章的以下部分中看到。
剥离未使用的节点并量化模型的另一种方法是使用称为transform_graph的工具。 这是 TensorFlow 1.4 中推荐的新方法,并且可以在 Python label_image脚本中正常工作,但是在部署到 iOS 和 Android 应用时仍然会导致不正确的识别结果。
bazel build tensorflow/tools/graph_transforms:transform_graph
bazel-bin/tensorflow/tools/graph_transforms/transform_graph
--in_graph=/tf_files/dog_retrained.pb
--out_graph=/tf_files/transform_dog_retrained.pb
--inputs='Mul'
--outputs='final_result'
--transforms='
strip_unused_nodes(type=float, shape="1,299,299,3")
fold_constants(ignore_errors=true)
fold_batch_norms
fold_old_batch_norms
quantize_weights'
在测试中使用label_image脚本可以正确运行quantized_stripped_dogs_retrained.pb和transform_dog_retrained.pb。 但是只有第一个可以在 iOS 和 Android 应用中正常工作。
有关图转换工具的详细文档,请参见其 GitHub README。
将 MobileNet 模型用于再训练
上一节中生成的剥离和量化模型的大小仍超过 20MB。 这是因为用于再训练的预先构建的 Inception v3 模型是大规模的深度学习模型,具有超过 2500 万个参数,并且 Inception v3 并非以移动优先为目标。
2017 年 6 月,谷歌发布了 MobileNets v1,共 16 种针对 TensorFlow 的移动优先深度学习模型。 这些模型的大小只有几 MB,具有 47 万至 424 万个参数,仍然达到了不错的精度(仅比 Inception v3 低一点)。 有关更多信息,请参见其自述文件。
上一节中讨论的retrain.py脚本还支持基于 MobileNet 模型的重新训练。 只需运行如下命令:
python tensorflow/examples/image_retraining/retrain.py
--output_graph=/tf_files/dog_retrained_mobilenet10_224.pb
--output_labels=/tf_files/dog_retrained_labels_mobilenet.txt
--image_dir ~/Downloads/Images
--bottleneck_dir=/tf_files/dogs_bottleneck_mobilenet
--architecture mobilenet_1.0_224
生成的标签文件dog_retrained_labels_mobilenet.txt实际上与使用 Inception v3 模型进行再训练期间生成的标签文件相同。 --architecture参数指定 16 个 MobileNet 模型之一,而值mobilenet_1.0_224表示使用模型大小为 1.0 的模型(对于其他参数,其他三个可能的值分别为 0.75、0.50 和 0.25 – 1.0,相反,准确但最大的大小为 0.25)和 224 作为图像输入大小(其他三个值分别为 192、160 和 128)。 如果将_quantized添加到架构值的末尾,即--architecture mobilenet_1.0_224_quantized,则模型也将被量化,从而导致重新训练的模型大小约为 5.1MB。 非量化模型的大小约为 17MB。
您可以按以下步骤测试先前使用label_image生成的模型:
bazel-bin/tensorflow/examples/label_image/label_image
--graph=/tf_files/dog_retrained_mobilenet10_224.pb
--image=/tmp/lab1.jpg
--input_layer=input
--output_layer=final_result
--labels=/tf_files/dog_retrained_labels_mobilenet.txt
--input_height=224
--input_width=224
--input_mean=128
--input_std=128
n02099712 labrador retriever (41): 0.824675
n02099601 golden retriever (64): 0.144245
n02104029 kuvasz (76): 0.0103533
n02087394 rhodesian ridgeback (105): 0.00528782
n02090379 redbone (32): 0.0035457
请注意,在运行label_image时,input_layer名为input。 我们可以使用交互式 iPython 代码或之前看到的图摘要工具找到该名称:
bazel-bin/tensorflow/tools/graph_transforms/summarize_graph
--in_graph=/tf_files/dog_retrained_mobilenet10_224.pb
Found 1 possible inputs: (name=input, type=float(1), shape=[1,224,224,3])
No variables spotted.
Found 1 possible outputs: (name=final_result, op=Softmax)
Found 4348281 (4.35M) const parameters, 0 (0) variable parameters, and 0 control_edges
Op types used: 92 Const, 28 Add, 27 Relu6, 15 Conv2D, 13 Mul, 13 DepthwiseConv2dNative, 10 Dequantize, 3 Identity, 1 MatMul, 1 BiasAdd, 1 Placeholder, 1 PlaceholderWithDefault, 1 AvgPool, 1 Reshape, 1 Softmax, 1 Squeeze
那么,我们什么时候应该在移动设备上使用 Inception v3 或 MobileNet 重新训练的模型? 如果您想获得最高的准确率,则应该并且可以使用基于 Inception v3 的重新训练模型。 如果速度是您的首要考虑因素,则应考虑使用具有最小参数大小和图像输入大小的 MobileNet 重训练模型,以换取一些精度损失。
benchmark_model是为您提供模型精确基准的一种工具。 首先,将其构建如下:
bazel build -c opt tensorflow/tools/benchmark:benchmark_model
然后,针对基于 Inception v3 或 MobileNet v1 的重新训练模型运行它:
bazel-bin/tensorflow/tools/benchmark/benchmark_model
--graph=/tf_files/quantized_stripped_dogs_retrained.pb
--input_layer="Mul"
--input_layer_shape="1,299,299,3"
--input_layer_type="float"
--output_layer="final_result"
--show_run_order=false
--show_time=false
--show_memory=false
--show_summary=true
您将获得相当长的输出,最后会有一行像 FLOPS 的估计值:11.42B,这意味着它将使用基于 Inception v3 的重新训练模型约 11B FLOPS(浮点运算)进行推断。 iPhone 6 运行大约 2 B FLOPS,因此在 iPhone 6 上运行模型大约需要 5–6 秒。 其他现代智能手机可以运行 10B FLOPS。
通过将图文件替换为基于 MobileNet 模型的重新训练模型dog_retrained_mobilenet10_224.pb并重新运行基准测试工具,您将看到 FLOPS 估计值变为约 1.14B,大约快了 10 倍。
在示例 iOS 应用中使用经过重新训练的模型
我们在第 1 章,“移动 TensorFlow 入门”中看到的 iOS 简单示例使用了 Inception v1 模型。 为了使该应用使用我们经过重新训练的 Inception v3 模型和 MobileNet 模型来更好地识别狗的品种,我们需要对该应用进行一些更改。 首先,让我们看看在 iOS 简单应用中使用经过重新训练的quantized_stripped_dogs_retrained.pb会发生什么:
- 双击
tensorflow/examples/ios/simple中的tf_simple_example.xcworkspace文件以 Xcode 打开应用 - 拖动我们用来测试
label_image脚本的quantized_stripped_dogs_retrained.pb模型文件,dog_retrained_labels.txt标签文件和lab1.jpg图像文件,然后拖放到项目的数据文件夹中,并确保同时选中“按需复制项目”和“添加到目标”,如以下屏幕截图所示:

图 2.5:将重新训练的模型文件和标签文件添加到应用
- 单击 Xcode 中的
RunModelViewController.mm文件,该文件使用 TensorFlow C++ API 处理输入图像,通过 Inception v1 模型运行它,并获得图像分类结果,并更改行:
NSString* network_path = FilePathForResourceName(@"tensorflow_inception_graph", @"pb");
NSString* labels_path = FilePathForResourceName(@"imagenet_comp_graph_label_strings", @"txt");
NSString* image_path = FilePathForResourceName(@"grace_hopper", @"jpg");
使用正确的模型文件名,标签文件名和测试图像名称进行以下操作:
NSString* network_path = FilePathForResourceName(@"quantized_stripped_dogs_retrained", @"pb");
NSString* labels_path = FilePathForResourceName(@"dog_retrained_labels", @"txt");
NSString* image_path = FilePathForResourceName(@"lab1", @"jpg");
- 同样在
RunModelViewController.mm中,要匹配我们的 Inception v3(从 v1)重新训练模型所需的输入图像大小,请将const int wanted_width = 224;和const int wanted_height = 224;中的224值更改为299,并同时将const float input_mean = 117.0f;中的值 ]和const float input_std = 1.0f;至128.0f - 从以下项更改输入和输出节点名称的值:
std::string input_layer = "input";
std::string output_layer = "output";
为以下正确值:
std::string input_layer = "Mul";
std::string output_layer = "final_result";
- 最后,您可以编辑
dog_retrained_labels.txt文件以删除每行中的前导nxxxx字符串(例如,删除n02099712 labrador retriever中的n02099712)– 在 Mac 上,您可以通过按住Option键然后进行选择和删除–从而使识别结果更具可读性
立即运行应用,然后单击运行模型按钮,在 Xcode 的控制台窗口或应用的编辑框中,您将看到以下识别结果,与运行label_image脚本的结果非常一致:
Predictions: 41 0.645 labrador retriever
64 0.195 golden retriever
76 0.0261 kuvasz
32 0.0133 redbone
20 0.0127 beagle
要使用 MobileNet(mobilenet_1.0_224_quantized)训练模型dog_retrained_mobilenet10_224.pb,请按照与之前类似的步骤进行操作,而在步骤 2 和 3 中,我们使用dog_retrained_mobilenet10_224.pb,但是在步骤 4 中,我们需要保留const int wanted_width = 224;和 const int wanted_height = 224;,仅将const float input_mean和const float input_std更改为128。 最后,在步骤 5 中,我们必须使用std::string input_layer = "input";和std::string output_layer = "final_result";。 这些参数与dog_retrained_mobilenet10_224.pb的label_image脚本使用的参数相同。
再次运行该应用,您将看到类似的最佳识别结果。
在示例 Android 应用中使用经过重新训练的模型
在 Android 的“TF 分类”应用中使用经过重新训练的 Inception v3 模型和 MobileNet 模型也非常简单。 请按照此处的步骤测试两个重新训练的模型:
- 使用 Android Studio 打开位于
tensorflow/examples/android中的示例 TensorFlow Android 应用。 - 将两个重新训练的模型
quantized_stripped_dogs_retrained .pb和dog_retrained_mobilenet10_224.pb以及标签文件dog_retrained_labels.txt拖放到 android 应用的assets文件夹中。 - 打开文件
ClassifierActivity.java,以使用 Inception v3 训练后的模型,并替换以下代码:
private static final int INPUT_SIZE = 224;
private static final int IMAGE_MEAN = 117;
private static final float IMAGE_STD = 1;
private static final String INPUT_NAME = "input";
private static final String OUTPUT_NAME = "output";
这些行:
private static final int INPUT_SIZE = 299;
private static final int IMAGE_MEAN = 128;
private static final float IMAGE_STD = 128;
private static final String INPUT_NAME = "Mul";
private static final String OUTPUT_NAME = "final_result";
private static final String MODEL_FILE = "file:///android_asset/quantized_stripped_dogs_retrained.pb";
private static final String LABEL_FILE =
"file:///android_asset/dog_retrained_labels.txt";
- 或者,要使用 MobileNet 训练后的模型,请用以下代码行替换代码:
private static final int INPUT_SIZE = 224;
private static final int IMAGE_MEAN = 128;
private static final float IMAGE_STD = 128;
private static final String INPUT_NAME = "input";
private static final String OUTPUT_NAME = "final_result";
private static final String MODEL_FILE = "file:///android_asset/dog_retrained_mobilenet10_224.pb";
private static final String LABEL_FILE = "file:///android_asset/dog_retrained_labels.txt";
- 将 Android 设备连接到计算机并在其上运行该应用。 然后点击 TF 分类应用,将相机指向一些狗的照片,您将在屏幕上看到最佳结果。
这就是在示例 TensorFlow iOS 和 Android 应用中使用两个经过重新训练的模型所需要的全部。 既然您已经了解了如何在示例应用中使用经过重新训练的模型,那么您可能想知道的下一件事是如何将 TensorFlow 添加到自己的新的或现有的 iOS 或 Android 应用中,以便可以开始添加 AI 对您自己的移动应用的强大功能。 这就是本章其余部分将详细讨论的内容。
将 TensorFlow 添加到您自己的 iOS 应用
在 TensorFlow 的早期版本中,将 TensorFlow 添加到您自己的应用非常繁琐,需要使用 TensorFlow 的手动构建过程和其他手动设置。 在 TensorFlow 1.4 中,该过程非常简单,但在 TensorFlow 网站上并未详细记录详细步骤。 缺少的另一件事是缺少有关如何在基于 Swift 的 iOS 应用中使用 TensorFlow 的文档; 示例 TensorFlow iOS 应用都在 Objective-C 中,它们调用了 TensorFlow 的 C++ API。 让我们看看我们如何做得更好。
将 TensorFlow 添加到您的 Objective-C iOS 应用
首先,按照以下步骤将具有图像分类功能的 TensorFlow 添加到您的 Objective-C iOS 应用(我们将从一个新应用开始,但是如果需要将 TensorFlow 添加到现有应用,则可以跳过第一步):
- 在您的 Xcode 中,单击“文件 | 新增 | 项目 ...”,选择“Single View App”,然后选择接下来的,输入
HelloTensorFlow作为产品名称,选择 Obj-C 作为语言,然后单击接下来并选择项目的位置,然后单击创建。 关闭 Xcode 中的项目窗口(因为我们稍后将使用 Pod 来打开项目的工作区文件)。 - 打开一个终端窗口,即
cd到项目所在的位置,然后创建一个名为Podfile的新文件,其内容如下:
target 'HelloTensorFlow'
pod 'TensorFlow-experimental'
- 运行命令
pod install下载并安装 TensorFlow Pod。 - 在 Xcode 中打开
HelloTensorFlow.xcworkspace文件,然后将两个文件(ios_image_load.mm和ios_image_load.h)拖放到 TensorFlow iOS 示例目录tensorflow/examples/ios/simple到HelloTensorFlow项目文件夹中。 - 将两个模型
quantized_stripped_dogs_retrained.pb和dog_retrained_mobilenet10_224.pb,label file dog_retrained_labels.txt以及几个测试图像文件拖放到项目文件夹中,之后,您应该看到类似以下的内容:

图 2.6:添加工具文件,模型文件,标签文件和图像文件
- 将
ViewController.m重命名为ViewController.mm,因为我们将在该文件中混合使用 C++ 代码和 Objective-C 代码来调用 TensorFlow C++ API 并处理图像输入和推断结果。 然后,在@interface ViewController之前,添加以下#include和函数原型:
#include <fstream>
#include <queue>
#include "tensorflow/core/framework/op_kernel.h"
#include "tensorflow/core/public/session.h"
#include "ios_image_load.h"
NSString* RunInferenceOnImage(int wanted_width, int wanted_height, std::string input_layer, NSString *model);
- 在
ViewController.mm的末尾,添加从tensorflow/example/ios/simple/RunModelViewController.mm复制的以下代码,对函数RunInferenceOnImage稍作更改,以接受具有不同输入大小和输入层名称的不同再训练模型:
namespace {
class IfstreamInputStream : public ::google::protobuf::io::CopyingInputStream {
...
static void GetTopN(
...
bool PortableReadFileToProto(const std::string& file_name,
...
NSString* FilePathForResourceName(NSString* name, NSString* extension) {
...
NSString* RunInferenceOnImage(int wanted_width, int wanted_height, std::string input_layer, NSString *model) {
- 仍然在
viewDidLoad方法的ViewController.mm中,首先添加添加标签的代码,以使用户知道他们可以使用该应用执行的操作:
UILabel *lbl = [[UILabel alloc] init];
[lbl setTranslatesAutoresizingMaskIntoConstraints:NO];
lbl.text = @"Tap Anywhere";
[self.view addSubview:lbl];
然后将标签置于屏幕中央的约束:
NSLayoutConstraint *horizontal = [NSLayoutConstraint constraintWithItem:lbl attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual toItem:self.view
attribute:NSLayoutAttributeCenterX multiplier:1 constant:0];
NSLayoutConstraint *vertical = [NSLayoutConstraint constraintWithItem:lbl attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual toItem:self.view
attribute:NSLayoutAttributeCenterY multiplier:1 constant:0];
[self.view addConstraint:horizontal];
[self.view addConstraint:vertical];
最后,在此处添加点击手势识别器:
UITapGestureRecognizer *recognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapped:)];
[self.view addGestureRecognizer:recognizer];
- 在轻敲处理器中,我们首先创建两个
alert操作,以允许用户选择重新训练的模型:
UIAlertAction* inceptionV3 = [UIAlertAction actionWithTitle:@"Inception v3 Retrained Model" style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
NSString *result = RunInferenceOnImage(299, 299, "Mul", @"quantized_stripped_dogs_retrained");
[self showResult:result];
}];
UIAlertAction* mobileNet = [UIAlertAction actionWithTitle:@"MobileNet 1.0 Retrained Model" style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
NSString *result = RunInferenceOnImage(224, 224, "input", @"dog_retrained_mobilenet10_224");
[self showResult:result];
}];
然后创建一个none操作,并将所有三个alert操作添加到警报控制器并显示它:
UIAlertAction* none = [UIAlertAction actionWithTitle:@"None" style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {}];
UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"Pick a Model" message:nil preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:inceptionV3];
[alert addAction:mobileNet];
[alert addAction:none];
[self presentViewController:alert animated:YES completion:nil];
- 推断的结果在方法
showResult中显示为另一个警报控制器:
-(void) showResult:(NSString *)result {
UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"Inference Result" message:result preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction* action = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil];
[alert addAction:action];
[self presentViewController:alert animated:YES completion:nil];
}
与调用 TensorFlow 相关的核心代码在RunInferenceOnImage方法中,该方法基于 TensorFlow iOS 简单应用进行了略微修改,包括首先创建一个 TensorFlow 会话和一个图:
tensorflow::Session* session_pointer = nullptr;
tensorflow::Status session_status = tensorflow::NewSession(options, &session_pointer);
...
std::unique_ptr<tensorflow::Session> session(session_pointer);
tensorflow::GraphDef tensorflow_graph;
NSString* network_path = FilePathForResourceName(model, @"pb");
PortableReadFileToProto([network_path UTF8String], &tensorflow_graph);
tensorflow::Status s = session->Create(tensorflow_graph);
然后加载标签文件和图像文件,并将图像数据转换为适当的 Tensor 数据:
NSString* labels_path = FilePathForResourceName(@"dog_retrained_labels", @"txt");
...
NSString* image_path = FilePathForResourceName(@"lab1", @"jpg");
std::vector<tensorflow::uint8> image_data = LoadImageFromFile([image_path UTF8String], &image_width, &image_height, &image_channels);
tensorflow::Tensor image_tensor(tensorflow::DT_FLOAT, tensorflow::TensorShape({1, wanted_height, wanted_width, wanted_channels}));
auto image_tensor_mapped = image_tensor.tensor<float, 4>();
tensorflow::uint8* in = image_data.data();
float* out = image_tensor_mapped.data();
for (int y = 0; y < wanted_height; ++y) {
const int in_y = (y * image_height) / wanted_height;
...
}
最后,使用图像张量数据和输入层名称调用 TensorFlow 会话的run方法,获取返回的输出结果,并对其进行处理以获取置信度值大于阈值的前五个结果:
std::vector<tensorflow::Tensor> outputs;
tensorflow::Status run_status = session->Run({{input_layer, image_tensor}},{output_layer}, {}, &outputs);
...
tensorflow::Tensor* output = &outputs[0];
const int kNumResults = 5;
const float kThreshold = 0.01f;
std::vector<std::pair<float, int> > top_results;
GetTopN(output->flat<float>(), kNumResults, kThreshold, &top_results);
在本书的其余部分,我们将实现RunInferenceOnxxx方法的不同版本,以使用不同的输入来运行不同的模型。 因此,如果您不完全理解前面的一些代码,请不要担心; 通过构建更多的应用,您将为新的自定义模型编写自己的推理逻辑而感到自在。
此外,完整的 iOS 应用 HelloTensorFlow 也包含在本书的源代码存储库中。
现在,在模拟器中或实际的 iOS 设备上运行该应用,首先,您将看到以下消息框,要求您选择重新训练的模型:

图 2.7:选择不同的再训练模型进行推理
然后,您将在选择模型后看到推断结果:

图 2.8:基于不同再训练模型的推理结果
请注意,MobileNet 训练后的模型在同一款 iPhone 上的运行速度要快得多,在 iPhone 6 上要比 Inception v3 训练后的模型快约一秒钟。
将 TensorFlow 添加到您的 Swift iOS 应用
自 2014 年 6 月诞生以来,Swift 已成为最优雅的现代编程语言之一。因此,对于某些开发人员而言,将现代 TensorFlow 集成到其基于 Swift 的现代 iOS 应用中既有趣又有用。 这样做的步骤与基于 Objective-C 的应用的步骤相似,但具有一些与 Swift 相关的技巧。 如果您已经按照 Objective-C 部分的步骤进行操作,则可能会发现这里的某些步骤是重复的,但是对于那些可能会跳过 Objective-C 部分并直接进入 Swift 的用户而言,仍然提供了完整的步骤:
- 在您的 Xcode 中,单击“文件 | 新增 | 项目...”,选择“Single View App”,然后接下来的,输入
HelloTensorFlow_Swift作为产品名称,选择 Swift 将设置为语言,然后单击接下来并选择项目的位置,然后单击创建。 关闭 Xcode 中的项目窗口(因为稍后将使用 Pod 来打开项目的工作区文件)。 - 打开一个终端窗口,即
cd到项目所在的位置,然后创建一个名为Podfile的新文件,其内容如下:
target 'HelloTensorFlow_Swift'
pod 'TensorFlow-experimental'
-
运行命令
pod install下载并安装 TensorFlow Pod; -
在 Xcode 中打开
HelloTensorFlow_Swift.xcworkspace文件,然后将两个文件(ios_image_load.mm和ios_image_load.h)拖放到 TensorFlow iOS 示例目录tensorflow/examples/ios/simple到HelloTensorFlow_Swift项目文件夹中。 当将两个文件添加到项目中时,您将看到一个消息框,如以下屏幕截图所示,询问您是否要配置 Objective-C 桥接头,Swift 代码调用 C++ 头需要此头。 Objective-C 代码。 因此,单击创建桥接标题按钮:

图 2.9:添加 C++ 文件时创建桥接标题
- 同样将
quantized_stripped_dogs_retrained .pb和dog_retrained_mobilenet10_224.pb这两个模型,标签文件dog_retrained_labels.txt和几个测试图像文件拖放到项目文件夹中–之后,您应该会看到类似以下内容:

图 2.10:添加工具文件,模型文件,标签文件和图像文件
- 使用以下代码创建一个名为
RunInference.h的新文件(一个窍门是,我们必须在下一步中使用 Objective-C 类作为RunInferenceOnImage方法的包装,以便我们的 Swift 代码进行间接调用) 。否则,将发生构建错误):
#import <Foundation/Foundation.h>
@interface RunInference_Wrapper : NSObject
- (NSString *)run_inference_wrapper:(NSString *)name;
@end
- 创建另一个名为
RunInference.mm的文件,该文件以以下include对象和原型开头:
#include <fstream>
#include <queue>
#include "tensorflow/core/framework/op_kernel.h"
#include "tensorflow/core/public/session.h"
#include "ios_image_load.h"
NSString* RunInferenceOnImage(int wanted_width, int wanted_height, std::string input_layer, NSString *model);
- 在以下代码中添加
RunInference.mm,以实现在其.h文件中定义的RunInference_Wrapper:
@implementation RunInference_Wrapper
- (NSString *)run_inference_wrapper:(NSString *)name {
if ([name isEqualToString:@"Inceptionv3"])
return RunInferenceOnImage(299, 299, "Mul", @"quantized_stripped_dogs_retrained");
else
return RunInferenceOnImage(224, 224, "input", @"dog_retrained_mobilenet10_224");
}
@end
- 在
RunInference.mm的末尾,添加与 Objective-C 部分中ViewController.mm完全相同的方法,与tensorflow/example/ios/simple/RunModelViewController.mm中的方法略有不同:
class IfstreamInputStream : public namespace {
class IfstreamInputStream : public ::google::protobuf::io::CopyingInputStream {
...
static void GetTopN(
...
bool PortableReadFileToProto(const std::string& file_name,
...
NSString* FilePathForResourceName(NSString* name, NSString* extension) {
...
NSString* RunInferenceOnImage(int wanted_width, int wanted_height, std::string input_layer, NSString *model) {
- 现在打开
viewDidLoad method末尾的ViewController.swift,首先添加添加标签的代码,以使用户知道他们可以使用该应用做什么:
let lbl = UILabel()
lbl.translatesAutoresizingMaskIntoConstraints = false
lbl.text = "Tap Anywhere"
self.view.addSubview(lbl)
然后将标签置于屏幕中央的约束:
let horizontal = NSLayoutConstraint(item: lbl, attribute: .centerX, relatedBy: .equal, toItem: self.view, attribute: .centerX, multiplier: 1, constant: 0)
let vertical = NSLayoutConstraint(item: lbl, attribute: .centerY, relatedBy: .equal, toItem: self.view, attribute: .centerY, multiplier: 1, constant: 0)
self.view.addConstraint(horizontal)
self.view.addConstraint(vertical)
最后,在此处添加点击手势识别器:
let recognizer = UITapGestureRecognizer(target: self, action: #selector(ViewController.tapped(_:)))
self.view.addGestureRecognizer(recognizer)
- 在轻击处理器中,我们首先添加
alert动作,以允许用户选择 Inception v3 训练后的模型:
let alert = UIAlertController(title: "Pick a Model", message: nil, preferredStyle: .actionSheet)
alert.addAction(UIAlertAction(title: "Inception v3 Retrained Model", style: .default) { action in
let result = RunInference_Wrapper().run_inference_wrapper("Inceptionv3")
let alert2 = UIAlertController(title: "Inference Result", message: result, preferredStyle: .actionSheet)
alert2.addAction(UIAlertAction(title: "OK", style: .default) { action2 in
})
self.present(alert2, animated: true, completion: nil)
})
然后在展示之前,为 MobileNet 重训练模型创建另一个动作以及一个none动作:
alert.addAction(UIAlertAction(title: "MobileNet 1.0 Retrained Model", style: .default) { action in
let result = RunInference_Wrapper().run_inference_wrapper("MobileNet")
let alert2 = UIAlertController(title: "Inference Result", message: result, preferredStyle: .actionSheet)
alert2.addAction(UIAlertAction(title: "OK", style: .default) { action2 in
})
self.present(alert2, animated: true, completion: nil)
})
alert.addAction(UIAlertAction(title: "None", style: .default) { action in
})
self.present(alert, animated: true, completion: nil)
- 打开
HelloTensorFlow_Swift-Bridging-Header.h文件,并向其中添加一行代码:#include "RunInference.h"。
现在,在模拟器中运行该应用,您将看到一个警报控制器,要求您选择模型:

图 2.11:选择一个经过训练的模型进行推理
以及不同再训练模型的推理结果:

图 2.12:不同再训练模型的推理结果
继续,既然您知道了将强大的 TensorFlow 模型添加到 iOS 应用需要做什么,无论它是用 Objective-C 还是 Swift 编写的,都没有理由阻止您将 AI 添加到您的移动应用中,除非您是 Android。 但是您知道我们当然也会照顾 Android。
将 TensorFlow 添加到您自己的 Android 应用
事实证明,将 TensorFlow 添加到自己的 Android 应用比 iOS 容易。 让我们跳到步骤:
- 如果您有现有的 Android 应用,请跳过此步骤。 否则,在 Android Studio 中,选择“文件 | 新增 | 新项目...”并接受所有默认设置,然后单击完成。
- 打开
build.gradle(Module: app)文件,并在依赖项{...};内部和末尾添加编译'org.tensorflow:tensorflow-android:+'。 - 生成
gradle文件,您将在app目录的位置app/build/intermediates/transforms/mergeJniLibs/debug/0/lib的子文件夹内看到libtensorflow_inference.so,这是 Java 代码与之对话的 TensorFlow 本机库。 - 如果这是一个新项目,则可以通过首先切换到包,然后右键单击该应用并选择“新建 | 文件夹 |
assets文件夹”来创建assets文件夹。 ,如以下屏幕截图所示,然后从包切换回 Android:

图 2.13:将素材文件夹添加到新项目
- 将两个重新训练的模型文件和标签文件以及几个测试图像拖放到资产文件夹中,如下所示:

图 2.14:将模型文件,标签文件和测试图像添加到素材
- 按住选项按钮,将
tensorflow/examples/android/src/org/tensorflow/demo和Classifier.java从tensorflow/examples/android/src/org/tensorflow/demo拖放到项目的 Java 文件夹中,如下所示:

图 2.15:将 TensorFlow 分类器文件添加到项目中
- 打开
MainActivity,首先创建与重新训练的 MobileNet 模型相关的常数-输入图像大小,节点名称,模型文件名和标签文件名:
private static final int INPUT_SIZE = 224;
private static final int IMAGE_MEAN = 128;
private static final float IMAGE_STD = 128;
private static final String INPUT_NAME = "input";
private static final String OUTPUT_NAME = "final_result";
private static final String MODEL_FILE = "file:///android_asset/dog_retrained_mobilenet10_224.pb";
private static final String LABEL_FILE = "file:///android_asset/dog_retrained_labels.txt";
private static final String IMG_FILE = "lab1.jpg";
- 现在,在
onCreate方法内部,首先创建一个Classifier实例:
Classifier classifier = TensorFlowImageClassifier.create(
getAssets(),
MODEL_FILE,
LABEL_FILE,
INPUT_SIZE,
IMAGE_MEAN,
IMAGE_STD,
INPUT_NAME,
OUTPUT_NAME);
然后从assets文件夹中读取我们的测试图像,根据模型指定的大小进行调整,然后调用推理方法recognizeImage:
Bitmap bitmap = BitmapFactory.decodeStream(getAssets().open(IMG_FILE));
Bitmap croppedBitmap = Bitmap.createScaledBitmap(bitmap, INPUT_SIZE, INPUT_SIZE, true);
final List<Classifier.Recognition> results = classifier.recognizeImage(croppedBitmap);
为简单起见,我们没有向 Android 应用添加任何与 UI 相关的代码,但是您可以在获取结果后在此行设置一个断点,并调试运行该应用; 您将看到以下屏幕截图所示的结果:

图 2.16:使用 MobileNet 再训练模型的识别结果
如果您通过将MODEL_FILE更改为quantized_stripped_dogs_retrained.pb,将INPUT_SIZE更改为299,并且将INPUT_NAME更改为Mul来使用 Inception v3 训练后的模型,则调试该应用,您将获得如下所示的结果 :

图 2.17:使用 Inception v3 再训练模型的识别结果
既然您已经了解了如何将 TensorFlow 和经过重新训练的模型添加到自己的 iOS 和 Android 应用,那么如果想要添加非 TensorFlow 相关功能应该不会太难。例如使用手机的摄像头拍摄狗的照片和识别它的品种。
总结
在本章中,我们首先简要介绍了什么是迁移学习以及为什么我们能够并且应该使用它来重新训练经过预训练的深度学习图像分类模型。 然后,我们提供了有关如何重新训练基于 Inception v3 的模型和 MobileNet 模型的详细信息,以便我们可以更好地理解和认识我们最好的朋友。 之后,我们首先展示了如何在 TensorFlow 示例 iOS 和 Android 应用中使用经过重新训练的模型,然后给出了有关如何将 TensorFlow 添加到您自己的基于 Objective-C 和 Swift 的 iOS 应用中的分步教程,以及您自己的 Android 应用。
现在我们有了最好的朋友,其中涵盖了一些不错而干净的技巧,我们知道还有很多其他人,无论好坏。 在下一章中,我们将学习如何变得更聪明,如何识别图片中所有有趣的对象以及如何在智能手机上随时随地对其进行定位。
三、检测物体及其位置
对象检测比上一章中讨论的图像分类迈出了一步。 图像分类仅返回图像的类别标签,而对象检测返回图像中标识的对象列表以及每个标识对象的边界框。 现代的对象检测算法使用深度学习来构建可用于检测和定位单个图像中各种对象的模型。 在过去的几年中,更快,更准确的对象检测算法相继问世.2017 年 6 月,谷歌发布了 TensorFlow 对象检测 API,该 API 集成了几种领先的对象检测算法。
在本章中,我们将首先简要概述对象检测:创建有效的深度学习模型进行对象检测,然后使用该模型进行推理的过程。 然后,我们将详细讨论 TensorFlow 对象检测 API 的工作原理,如何使用其多个模型进行推理以及如何使用自己的数据集对其进行重新训练。 然后,我们将向您展示如何在 iOS 应用中使用预训练的对象检测模型以及重新训练的模型。 我们将介绍一些强大的技巧,使您可以手动构建自定义的 TensorFlow iOS 库,以解决使用 TensorFlow Pod 的问题; 这将帮助您准备好处理本书其余部分中介绍的任何受 TensorFlow 支持的模型。 在本章中,我们将不提供用于对象检测的 Android 示例应用,因为 TensorFlow 源代码已经附带了一个很好的示例,可以使用 TensorFlow 对象检测预训练模型以及 YOLO 模型进行操作。 我们将在本章最后介绍。 我们将向您展示如何在 iOS 应用中使用另一种领先的物体检测模型 YOLO v2。 总而言之,我们将在本章中介绍以下主题:
-
物体检测:快速概述
-
设置 TensorFlow 对象检测 API
-
重新训练 SSD-MobileNet 和更快的 RCNN 模型
-
在 iOS 中使用对象检测模型
-
使用 YOLO2:另一种物体检测模型
对象检测 -- 快速概述
自从 2012 年神经网络取得突破以来,当名为 AlexNet 的深层 CNN 模型通过大大降低错误率赢得了年度 ImageNet 视觉识别挑战时,许多计算机视觉和自然语言处理领域的研究人员就开始利用深度学习模型的强大功能这一优势。 基于深度学习的现代对象检测全部基于 CNN,并建立在诸如 AlexNet,Google Inception 或其他流行的 VGG 网络等预训练的模型之上。 这些 CNN 通常已经训练了数百万个参数,并且可以将输入图像转换为一组特征,这些特征可以进一步用于诸如上一章中涉及的图像分类以及对象检测以及其他与计算机视觉相关的任务。
2014 年,提出了一种最新的对象检测器,该对象检测器使用称为 RCNN(具有 CNN 特征的区域)的标记对象检测数据集对 AlexNet 进行了训练,与传统的检测方法相比,它在准确率上有了很大的提高。 RCNN 结合了一种称为区域提议的技术,该技术可生成大约 2,000 个可能的区域候选者,并在每个这些区域上运行 CNN 以进行分类和边界框预测。 然后,将这些结果合并以生成检测结果。 RCNN 的训练过程非常复杂,耗时数天,推理速度也很慢,在 GPU 上的图像上花费了将近一分钟。
自从提出 RCNN 以来,表现更好的对象检测算法纷至沓来:快速 RCNN,更快的 RCNN,YOLO(您只看一次),SSD(单发多框检测器)和 YOLO v2。
2014 年,Andrej Karpathy 对 RCNN 作了很好的介绍,“玩转 RCNN,先进的物体检测器”。 贾斯汀·约翰逊(Justin Johnson)在斯坦福大学 CS231n 课程中提供了一个很好的视频讲座“空间定位和检测”,内容涉及物体检测,其中包括 RCNN,Fast RCNN,Faster RCNN 和 YOLO 的详细信息。 YOLO2 网站是这里。
快速 RCNN 通过首先在整个输入图像上而不是数千个建议的区域上应用 CNN,然后处理区域建议,从而显着改善了训练过程和推理时间(10 小时的训练和 2.x 秒的推理)。 更快的 RCNN 通过使用区域建议网络进一步将推理速度提高到了实时(0.2 秒),因此在训练后,不再需要耗时的区域建议过程。
与 RCNN 检测系列不同,SSD 和 YOLO 都是单发方法,这意味着它们将单个 CNN 应用于完整的输入图像,而无需使用区域建议和区域分类。 这使这两种方法都非常快,它们的平均平均精度(mAP)约为 80%,优于 Faster RCNN。
如果这是您第一次听说这些方法,则可能会感到有些迷茫。 但是,作为对使用 AI 增强移动应用功能感兴趣的开发人员,您无需了解设置深度神经网络架构和训练对象检测模型的所有细节; 您应该只知道如何使用以及(如果需要)重新训练经过预训练的模型,以及如何在 iOS 和 Android 应用中使用经过预训练或重新训练的模型。
如果您真的对深度学习研究感兴趣,并且想知道每个检测器如何工作以决定使用哪种检测器的所有细节,那么您绝对应该阅读每种方法的论文,并尝试自己复制训练过程。 这将是一条漫长而有益的道路。 但是,如果您想听 Andrej Karpathy 的建议,“不要成为英雄”(在 YouTube 上搜索“Andrej 的计算机视觉深度学习”),那么您可以“采取最有效的方法,下载经过预训练的模型, 添加/删除其中的某些部分,然后在您的应用上对其进行微调”,这也是我们将在此处使用的方法。
在开始研究哪种方法最适合 TensorFlow 之前,让我们快速了解一下数据集。 有 3 个主要的数据集用于训练对象检测:PASCAL VOC,ImageNet 和 Microsoft COCO,它们具有的类数分别为 20、200 和 80。 TensorFlow 对象检测 API 当前支持的大多数预训练模型都在 80 级 MS COCO 数据集上进行了训练(有关预训练模型及其训练的数据集的完整列表,请参见这里。
尽管我们不会从头开始进行训练,但是您会经常提到 PASCAL VOC 或 MS COCO 数据格式,以及它们涵盖的 20 或 80 个通用类,它们都是在重新训练或使用经过训练的模型时使用的。 在本章的最后部分,我们将尝试使用 VOC 训练的 YOLO 模型和 COCO 训练的模型。
设置 TensorFlow 对象检测 API
TensorFlow 对象检测 API 在其官方网站上有详细记录,您一定要查看其“快速入门:用于现成的推断的 Jupyter 笔记本”指南,快速介绍了如何在 Python 中使用良好的预训练模型进行检测。 但是那里的文档分布在许多不同的页面上,有时难以理解。 在本节和下一节中,我们将通过重组在许多地方记录的重要细节并添加更多示例和代码说明来简化官方文档,并提供有关以下内容的两个分步教程:
- 如何设置 API 并使用其预训练的模型进行现成的推断
- 如何使用 API重新训练预训练模型以执行更具体的检测任务
快速安装和示例
执行以下步骤来安装和运行对象检测推断:
- 在第 1 章,“移动 TensorFlow 入门”中创建的 TensorFlow 源根中,获取 TensorFlow 模型存储库,其中包含 TensorFlow 对象检测 API 作为其研究模型之一:
git clone https://github.com/tensorflow/models
- 安装
matplotlib,pillow,lxml和jupyter库。 在 Ubuntu 或 Mac 上,您可以运行:
sudo pip install pillow
sudo pip install lxml
sudo pip install jupyter
sudo pip install matplotlib
- 转到
models/research目录,然后运行以下命令:
protoc object_detection/protos/*.proto --python_out=.
这将编译object_detection/protos目录中的所有 Protobuf,以使 TensorFlow 对象检测 API 满意。 Protobuf 或 Protocol Buffer 是一种自动序列化和检索结构化数据的方法,它比 XML 轻巧且效率更高。 您所需要做的就是编写一个描述数据结构的.proto文件,然后使用protoc(proto 编译器)生成自动解析和编码 protobuf 数据的代码。 注意--python_out参数指定了所生成代码的语言。 在本章的下一部分中,当我们讨论如何在 iOS 中使用模型时,我们将使用带有--cpp_out的协议编译器,因此生成的代码是 C++ 。 有关协议缓冲区的完整文档,请参见这里。
- 仍在模型/研究中,运行
export PYTHONPATH=$PYTHONPATH:pwd:pwd/slim,然后运行python object_detection/builders/model_builder_test.py以验证一切正常。 - 启动
jupyter notebook命令并在浏览器中打开http://localhost:8888。 首先单击object_detection,然后选择object_detection_tutorial.ipynb笔记本并逐个单元运行演示。
使用预训练的模型
现在让我们来看一下使用预训练的 TensorFlow 对象检测模型在 Python 笔记本中进行推理的主要组件。 首先,定义一些关键常量:
MODEL_NAME = 'ssd_mobilenet_v1_coco_2017_11_17'
MODEL_FILE = MODEL_NAME + '.tar.gz'
DOWNLOAD_BASE = 'http://download.tensorflow.org/models/object_detection/'
PATH_TO_CKPT = MODEL_NAME + '/frozen_inference_graph.pb'
PATH_TO_LABELS = os.path.join('data', 'mscoco_label_map.pbtxt')
NUM_CLASSES = 90
笔记本代码下载并使用了预训练的对象检测模型ssd_mobilenet_v1_coco_2017_11_17(使用 SSD 方法构建,我们在上一章中介绍的 MobileNet CNN 模型之上,在上一节中进行了简要介绍)。 TensorFlow 检测模型动物园中提供了 TensorFlow 对象检测 API 支持的预训练模型的完整列表,并且大多数都是使用 MS COCO 数据集进行训练的。 用于推理的确切模型是frozen_inference_graph.pb文件(在下载的ssd_mobilenet_v1_coco_2017_11_17.tar.gz文件中),该模型用于现成的推理以及重新训练。
位于models/research/object_detection/data/mscoco_label_map.pbtxt中的mscoco_label_map.pbtxt标签文件具有 90(NUM_CLASSES)个项目,用于ssd_mobilenet_v1_coco_2017_11_17模型可以检测到的对象类型。 它的前两个项目是:
item {
name: "/m/01g317"
id: 1
display_name: "person"
}
item {
name: "/m/0199g"
id: 2
display_name: "bicycle"
}
…
item {
name: "/m/03wvsk"
id: 89
display_name: "hair drier"
}
item {
name: "/m/012xff"
id: 90
display_name: "toothbrush"
}
我们在前面的步骤 3 中讨论了 Protobuf,描述mscoco_label_map.pbtxt中数据的 proto 文件是string_int_label_map.proto,位于models/research/object_detection/protos中,其内容如下:
syntax = "proto2";
package object_detection.protos;
message StringIntLabelMapItem {
optional string name = 1;
optional int32 id = 2;
optional string display_name = 3;
};
message StringIntLabelMap {
repeated StringIntLabelMapItem item = 1;
};
因此,基本上,协议编译器基于string_int_label_map.proto创建代码,然后可以使用该代码有效地序列化mscoco_label_map.pbtxt中的数据。 稍后,当 CNN 检测到对象并返回整数 ID 时,可以将其转换为name或display_name供人类阅读。
将模型下载,解压缩并加载到内存中后,标签映射文件也将加载,并且位于models/research/object_detection/test_images的一些测试图像可以在其中添加您自己的任何测试图像以进行检测测试。 。 接下来,定义适当的输入和输出张量:
with detection_graph.as_default():
with tf.Session(graph=detection_graph) as sess:
image_tensor = detection_graph.get_tensor_by_name('image_tensor:0')
detection_boxes = detection_graph.get_tensor_by_name('detection_boxes:0')
detection_scores = detection_graph.get_tensor_by_name('detection_scores:0')
detection_classes = detection_graph.get_tensor_by_name('detection_classes:0')
num_detections = detection_graph.get_tensor_by_name('num_detections:0')
再次,如果您想知道这些输入和输出张量名称来自models/research/object_detection/ssd_mobilenet_v1_coco_2017_11_17/frozen_inference_graph.pb中下载并保存的 SSD 模型中,那么您可以在 iPython 中使用以下代码来查找:
import tensorflow as tf
g=tf.GraphDef()
g.ParseFromString(open("object_detection/ssd_mobilenet_v1_coco_2017_11_17/frozen_inference_graph.pb","rb").read())
x=[n.name for n in g.node]
x[-4:]
x[:5]
The last two statements will return:
[u'detection_boxes',
u'detection_scores',
u'detection_classes',
u'num_detections']
and
[u'Const', u'Const_1', u'Const_2', u'image_tensor', u'ToFloat']
另一种方法是使用上一章中描述的汇总图工具:
bazel-bin/tensorflow/tools/graph_transforms/summarize_graph --in_graph= models/research/object_detection/ssd_mobilenet_v1_coco_2017_11_17/frozen_inference_graph.pb
这将生成以下输出:
Found 1 possible inputs: (name=image_tensor, type=uint8(4), shape=[?,?,?,3])
No variables spotted.
Found 4 possible outputs: (name=detection_boxes, op=Identity) (name=detection_scores, op=Identity (name=detection_classes, op=Identity) (name=num_detections, op=Identity)
加载每个测试映像后,将运行实际检测:
image = Image.open(image_path)
image_np = load_image_into_numpy_array(image)
image_np_expanded = np.expand_dims(image_np, axis=0)
(boxes, scores, classes, num) = sess.run(
[detection_boxes, detection_scores, detection_classes, num_detections],
feed_dict={image_tensor: image_np_expanded})
最后,使用matplotlib库将检测到的结果可视化。 如果使用tensorflow/models存储库随附的默认两个测试图像,则会在图 3.1 中看到结果:

图 3.1:检测到的对象及其边界框和置信度分数
在“在 iOS 中使用对象检测模型”部分,我们将了解如何在 iOS 设备上使用相同的模型并绘制相同的检测结果。
您还可以在前面提到的 Tensorflow 检测模型 Zoo 中测试其他预训练模型。 例如,如果使用faster_rcnn_inception_v2_coco模型,将object_detection_tutorial.ipynb笔记本中的MODEL_NAME = 'ssd_mobilenet_v1_coco_2017_11_17'替换为MODEL_NAME = ' faster_rcnn_inception_v2_coco_2017_11_08'(可从 TensorFlow 检测模型 Zoo 页面的 URL 或MODEL_NAME = ' faster_rcnn_resnet101_coco_2017_11_08'中获得),则可以看到类似于其他两个基于 Faster RCNN 的模型的检测结果的内容,但是它们花费的时间更长。
另外,在两个faster_rcnn模型上使用summarize_graph工具会在输入和输出上生成相同的信息:
Found 1 possible inputs: (name=image_tensor, type=uint8(4), shape=[?,?,?,3])
Found 4 possible outputs: (name=detection_boxes, op=Identity) (name=detection_scores, op=Identity) (name=detection_classes, op=Identity) (name=num_detections, op=Identity)
通常,与其他大型的基于 Inception 或 Resnet-CNN 的大型模型相比,基于 MobileNet 的模型速度最快,但准确率较低(mAP 值较小)。 顺便说一下,下载的ssd_mobilenet_v1_coco, faster_rcnn_inception_v2_coco_2017_11_08和faster_rcnn_resnet101_coco_2017_11_08文件的大小分别为 76MB,149MB 和 593MB。 稍后我们将看到,在移动设备上,基于 MobileNet 的模型(例如ssd_mobilenet_v1_coco)运行速度要快得多,有时,大型模型(例如faster_rcnn_resnet101_coco_2017_11_08)只会在较旧的 iPhone 上崩溃。 希望您可以使用基于 MobileNet 的模型,经过重新训练的 MobileNet 模型或将来可以提供更高准确率的ssd_mobilenet的将来版本解决问题,尽管ssd_mobilenet的 v1 在许多用例中已经足够好。
重新训练 SSD-MobileNet 和 Faster RCNN 模型
经过预训练的 TensorFlow 对象检测模型当然可以很好地解决某些问题。 但是有时候,您可能需要使用自己的带标注的数据集(在您特别感兴趣的对象或对象部分周围带有边界框)并重新训练现有模型,以便它可以更准确地检测不同的对象类别集合。
我们将使用 TensorFlow 对象检测 API 网站中记录的相同的 Oxford-IIIT Pets 数据集来重新训练本地计算机上的两个现有模型,而不是使用文档中介绍的 Google Cloud。 必要时,我们还将为每个步骤添加说明。 以下是有关如何使用 Oxford Oxford Pets 数据集重新训练 TensorFlow 对象检测模型的分步指南:
- 在终端窗口中,最好在我们的 GPU 驱动的 Ubuntu 上
cd models/research first,以加快重新训练的速度,然后运行以下命令下载数据集(images.tar.gz约为 800MB,annotations.tar.gz为 38MB):
wget http://www.robots.ox.ac.uk/~vgg/data/pets/data/images.tar.gz
wget http://www.robots.ox.ac.uk/~vgg/data/pets/data/annotations.tar.gz
tar -xvf images.tar.gz
tar -xvf annotations.tar.gz
- 运行以下命令以将数据集转换为 TFRecords 格式:
python object_detection/dataset_tools/create_pet_tf_record.py \
--label_map_path=object_detection/data/pet_label_map.pbtxt \
--data_dir=`pwd` \
--output_dir=`pwd`
该命令将在models/research目录中生成两个名为pet_train_with_masks.record(268MB)和pet_val_with_masks.record(110MB)的 TFRecord 文件。 TFRecords 是一种有趣的二进制格式,其中包含 TensorFlow 应用可用于训练或验证的所有数据,如果您想使用 TensorFlow 对象检测 API 重新训练自己的数据集,则 TFRecords 是必需的文件格式。
- 如果在上一节中测试对象检测笔记本时还没有下载
ssd_mobilenet_v1_coco模型和faster_rcnn_resnet101_coco模型并将其解压缩到models/research目录,请执行以下操作:
wget http://storage.googleapis.com/download.tensorflow.org/models/object_detection/ssd_mobilenet_v1_coco_2017_11_17.tar.gz
tar -xvf ssd_mobilenet_v1_coco_2017_11_17.tar.gz
wget http://storage.googleapis.com/download.tensorflow.org/models/object_detection/faster_rcnn_resnet101_coco_11_06_2017.tar.gz
tar -xvf faster_rcnn_resnet101_coco_11_06_2017.tar.gz
- 替换
object_detection/samples/configs/faster_rcnn_resnet101_pets.config文件中出现的PATH_TO_BE_CONFIGURED五次,因此它们变为:
fine_tune_checkpoint: "faster_rcnn_resnet101_coco_11_06_2017/model.ckpt"
...
train_input_reader: {
tf_record_input_reader {
input_path: "pet_train_with_masks.record"
}
label_map_path: "object_detection/data/pet_label_map.pbtxt"
}
eval_input_reader: {
tf_record_input_reader {
input_path: "pet_val_with_masks.record"
}
label_map_path: "object_detection/data/pet_label_map.pbtxt"
...
}
faster_rcnn_resnet101_pets.config文件用于指定模型检查点文件的位置,该文件包含模型的训练后权重,在步骤 2 中生成的用于训练和验证的 TFRecords 文件以及要检测的 37 类宠物的标签项。 object_detection/data/pet_label_map.pbtxt的第一项和最后一项如下:
item {
id: 1
name: 'Abyssinian'
}
...
item {
id: 37
name: 'yorkshire_terrier'
}
- 同样,在
object_detection/samples/configs/ssd_mobilenet_v1_pets.config文件中更改PATH_TO_BE_CONFIGURED的五次出现,因此它们变为:
fine_tune_checkpoint: "object_detection/ssd_mobilenet_v1_coco_2017_11_17/model.ckpt"
train_input_reader: {
tf_record_input_reader {
input_path: "pet_train_with_masks.record"
}
label_map_path: "object_detection/data/pet_label_map.pbtxt"
}
eval_input_reader: {
tf_record_input_reader {
input_path: "pet_val_with_masks.record"
}
label_map_path: "object_detection/data/pet_label_map.pbtxt"
...
}
- 创建一个新的
train_dir_faster_rcnn目录,然后运行重新训练命令:
python object_detection/train.py \
--logtostderr \
--pipeline_config_path=object_detection/samples/configs/faster_rcnn_resnet101_pets.config \
--train_dir=train_dir_faster_rcnn
在基于 GPU 的系统上,从最初的损失 5.0 到损失 0.2 左右,只需不到 25,000 步的训练即可:
tensorflow/core/common_runtime/gpu/gpu_device.cc:1030] Found device 0 with properties:
name: GeForce GTX 1070 major: 6 minor: 1 memoryClockRate(GHz): 1.7845
pciBusID: 0000:01:00.0
totalMemory: 7.92GiB freeMemory: 7.44GiB
INFO:tensorflow:global step 1: loss = 5.1661 (15.482 sec/step)
INFO:tensorflow:global step 2: loss = 4.6045 (0.927 sec/step)
INFO:tensorflow:global step 3: loss = 5.2665 (0.958 sec/step)
...
INFO:tensorflow:global step 25448: loss = 0.2042 (0.372 sec/step)
INFO:tensorflow:global step 25449: loss = 0.4230 (0.378 sec/step)
INFO:tensorflow:global step 25450: loss = 0.1240 (0.386 sec/step)
- 在大约 20,000 个步骤(大约 2 个小时)后,按
Ctrl + C结束上述重新训练脚本的运行。 创建一个新的train_dir_ssd_mobilenet目录,然后运行:
python object_detection/train.py \
--logtostderr \
--pipeline_config_path=object_detection/samples/configs/ssd_mobilenet_v1_pets.config \
--train_dir=train_dir_ssd_mobilenet
训练结果应如下所示:
INFO:tensorflow:global step 1: loss = 136.2856 (23.130 sec/step)
INFO:tensorflow:global step 2: loss = 126.9009 (0.633 sec/step)
INFO:tensorflow:global step 3: loss = 119.0644 (0.741 sec/step)
...
INFO:tensorflow:global step 22310: loss = 1.5473 (0.460 sec/step)
INFO:tensorflow:global step 22311: loss = 2.0510 (0.456 sec/step)
INFO:tensorflow:global step 22312: loss = 1.6745 (0.461 sec/step)
您可以看到,与Faster_RCNN模型相比,SSD_Mobilenet模型的重新训练在开始和结束时的损失都更大。
- 经过大约 20,000 个训练步骤,终止前面的再训练脚本。 然后创建一个新的
eval_dir目录并运行评估脚本:
python object_detection/eval.py \
--logtostderr \
--pipeline_config_path=object_detection/samples/configs/faster_rcnn_resnet101_pets.config \
--checkpoint_dir=train_dir_faster_rcnn \
--eval_dir=eval_dir
- 打开另一个终端窗口,在 TensorFlow 根目录下打开
cd,然后打开models/research,然后运行tensorboard --logdir=.。 在浏览器中,打开http://localhost:6006,您将看到损失图,如图 3.2 所示:

图 3.2:训练对象检测模型时的总损失趋势
您还将看到一些评估结果,如图 3.3 所示:

图 3.3:重新训练物体检测模型时评估图像检测结果
- 同样,您可以为
SSD_MobileNet模型运行评估脚本,然后使用 TensorBoard 查看其损失趋势和评估图像结果:
python object_detection/eval.py \
--logtostderr \
--pipeline_config_path=object_detection/samples/configs/ssd_mobilenet_v1_pets.config \
--checkpoint_dir=train_dir_ssd_mobilenet \
--eval_dir=eval_dir_mobilenet
- 您可以使用以下命令生成重新训练的图:
python object_detection/export_inference_graph.py \
--input_type image_tensor \
--pipeline_config_path object_detection/samples/configs/ssd_mobilenet_v1_pets.config \
--trained_checkpoint_prefix train_dir_ssd_mobilenet/model.ckpt-21817 \
--output_directory output_inference_graph_ssd_mobilenet.pb
python object_detection/export_inference_graph.py \
--input_type image_tensor \
--pipeline_config_path object_detection/samples/configs/faster_rcnn_resnet101_pets.config \
--trained_checkpoint_prefix train_dir_faster_rcnn/model.ckpt-24009 \
--output_directory output_inference_graph_faster_rcnn.pb
您需要用自己的特定检查点值替换--trained_checkpoint_prefix值(上述 21817 和 24009)。
到此为止-您现在拥有两个经过重新训练的对象检测模型output_inference_graph_ssd_mobilenet.pb和output_inference_graph_faster_rcnn.pb,可以在您的 Python 代码(上一节中的 Jupyter 笔记本)或移动应用中使用它们。 不用再拖延了,让我们跳到移动世界,看看如何使用我们拥有的预训练和重新训练的模型。
在 iOS 中使用对象检测模型
在上一章中,我们向您展示了如何使用 TensorFlow 实验性容器将 TensorFlow 快速添加到您的 iOS 应用中。 TensorFlow 实验性 Pod 在诸如 Inception 和 MobileNet 之类的模型或其经过重新训练的模型中工作良好。 但是,如果至少在撰写本文时(2018 年 1 月)使用 TensorFlow 实验荚,并使用SSD_MobileNet模型,则在加载ssd_mobilenet图文件时可能会收到以下错误消息:
Could not create TensorFlow Graph: Not found: Op type not registered 'NonMaxSuppressionV2'
除非将 TensorFlow 实验 Pod 更新为包括未在此处注册的操作,否则解决这些问题的唯一方法是通过从 TensorFlow 源构建自定义 TensorFlow iOS 库,这就是我们为什么向您展示第 1 章,“移动 TensorFlow 入门”中的内容,“如何从源代码获取和设置 TensorFlow”。 让我们看一下构建自己的 TensorFlow iOS 库并使用它来创建具有 TensorFlow 支持的新 iOS 应用的步骤。
手动构建 TensorFlow iOS 库
只需执行以下步骤即可构建自己的 TensorFlow iOS 库:
- 如果您将 TensorFlow 1.4 源 zip 解压缩到您的主目录,请在 Mac 上打开一个新终端,将
cd到 TensorFlow 源根目录,即~/tensorflow-1.4.0。 - 运行
tensorflow/contrib/makefile/build_all_ios.sh命令,此过程从 20 分钟到大约一个小时不等,具体取决于您的 Mac 速度。 构建过程成功完成后,您将创建三个库:
tensorflow/contrib/makefile/gen/protobuf_ios/lib/libprotobuf-lite.a
tensorflow/contrib/makefile/gen/protobuf_ios/lib/libprotobuf.a
tensorflow/contrib/makefile/gen/lib/libtensorflow-core.a
前两个库处理我们之前讨论的 protobuf 数据。 最后一个库是 iOS 通用静态库。
如果您运行该应用,请完成以下步骤,并在 Xcode 控制台中遇到错误,Invalid argument: No OpKernel was registered to support Op 'Less' with these attrs. Registered devices: [CPU], Registered kernels: device='CPU'; T in [DT_FLOAT],您需要在此处执行步骤 2 之前更改tensorflow/contrib/makefile/Makefile文件(请参阅第 7 章,“使用 CNN 和 LSTM 识别绘图”中的“为 iOS 构建自定义 TensorFlow 库”一节)。 使用新版本的 TensorFlow 时可能看不到错误。
在应用中使用 TensorFlow iOS 库
要在您自己的应用中使用库,请执行以下操作:
- 在 Xcode 中,单击“文件 | 新增 | 项目…”,选择“Single View App”,然后输入 TFObjectDetectionAPI 作为产品名称,然后选择 Objective-C 作为语言(如果您想使用 Swift,请参阅上一章有关如何将 TensorFlow 添加到基于 Swift 的 iOS 应用并进行此处所示的必要更改),然后选择项目的位置并单击“创建”。
- 在
TFObjectDetectionAPI项目中,单击项目名称,然后在构建设置下,单击+并添加用户定义的设置,然后输入TENSORFLOW_ROOT作为 TensorFlow 源根的路径(例如$HOME/tensorflow-1.4),如图 3.4 所示。 如果您要引用较新的 TensorFlow 来源,此用户定义的设置将在其他设置中使用,以便以后轻松更改项目设置:

图 3.4:添加TENSORFLOW_ROOT用户定义的设置
- 单击目标,然后在“构建设置”下搜索“其他链接器标志”。 向其添加以下值:
-force_load $(TENSORFLOW_ROOT)/tensorflow/contrib/makefile/gen/lib/libtensorflow-core.a $(TENSORFLOW_ROOT)/tensorflow/contrib/makefile/gen/protobuf_ios/lib/libprotobuf.a $(TENSORFLOW_ROOT)/tensorflow/contrib/makefile/gen/protobuf_ios/lib/libprotobuf-lite.a $(TENSORFLOW_ROOT)/tensorflow/contrib/makefile/downloads/nsync/builds/lipo.ios.c++11/nsync.a
需要第一个–force_load,因为它确保 TensorFlow 所需的 C++ 构造器将被链接,否则,您仍可以构建和运行该应用,但会遇到有关未注册会话的错误。
最后一个库用于nsync,这是一个 C 库,可导出互斥量和其他同步方法。 在新的 TensorFlow 版本中引入。
- 搜索“标题搜索路径”,并添加以下值:
$(TENSORFLOW_ROOT) $(TENSORFLOW_ROOT)/tensorflow/contrib/makefile/downloads/protobuf/src $(TENSORFLOW_ROOT)/tensorflow/contrib/makefile/downloads $(TENSORFLOW_ROOT)/tensorflow/contrib/makefile/downloads/eigen $(TENSORFLOW_ROOT)/tensorflow/contrib/makefile/gen/proto
之后,您会看到类似图 3.5 的内容:

图 3.5:为目标添加所有与 TensorFlow 相关的构建设置
- 在目标的“构建阶段”中,在带库的链接二进制文件中添加 Accelerate 框架,如图 3.6 所示:

图 3.6:添加加速框架
- 返回用于构建 TensorFlow iOS 库的终端,在
tensorflow/core/platform/default/mutex.h中找到以下两行代码:
#include "nsync_cv.h"
#include "nsync_mu.h"
然后将其更改为:
#include "nsync/public/nsync_cv.h"
#include "nsync/public/nsync_mu.h"
只需将手动构建的 TensorFlow 库 TensorFlow 添加到 iOS 应用即可。
使用从 TensorFlow 的更高版本(例如 1.4)手动构建的 TensorFlow 库在您的应用中加载 TensorFlow 对象检测模型时,将不会出现使用 TensorFlow 实验性 POD 或从早期版本构建的手动库时可能会看到的错误版。这是因为位于tensorflow/contrib/makefile中的名为tf_op_files.txt的文件用于定义应为 TensorFlow 库构建和包括哪些操作,在 TensorFlow 1.4 中定义的操作比早期版本更多。例如,TensorFlow 1.4 中的tf_op_files.txt文件有一行tensorflow/core/kernels/non_max_suppression_op.cc定义了NonMaxSuppressionV2操作,这就是为什么我们手动构建的库中定义了该操作,防止出现错误Could not create TensorFlow Graph: Not found: Op type not registered 'NonMaxSuppressionV2',我们将查看是否发生过使用 TensorFlow 窗格的错误。将来,如果遇到类似的Op type not registered错误,则可以通过在tf_op_files.txt文件中添加定义操作的正确的源代码文件,然后再次运行build_all_ios.sh来修复该错误。创建一个新的libtensorflow-core.a文件。
向 iOS 应用添加对象检测功能
现在执行以下步骤以将模型文件,标签文件和代码添加到应用,并运行以查看实际的对象检测:
- 拖放上一节中的三个物体检测模型图
ssd_mobilenet_v1_frozen_inference_graph.pb,faster_rcnn_inceptionv2_frozen_inference_graph.pb和faster_rcnn_resnet101_frozen_inference_graph.pb,以及mscoco_label_map.pbtxt标签映射文件和几个测试图像发送到TFObjectDetectionAPI项目。 - 将 TensorFlow iOS 示例简单应用或上一章中创建的 iOS 应用中的
ios_image_load.mm及其.h文件添加到项目中。 - 在这里(在 Mac 上为
protoc-3.4.0-osx-x86_64.zip文件)下载协议缓冲区版本 3.4.0。 要使用 TensorFlow 1.4 库需要确切的 3.4.0 版本,而更高版本的 TensorFlow 可能需要更高版本的协议缓冲区。 - 假设下载的文件解压缩到
~/Downloads目录中,请打开“终端”窗口并运行以下命令:
cd <TENSORFLOW_ROOT>/models/research/object_detection/protos
~/Downloads/protoc-3.4.0-osx-x86_64/bin/protoc string_int_label_map.proto --cpp_out=<path_to_your_TFObjectDetectionAPI_project>, the same location as your code files and the three graph files.
protoc编译器命令完成后,您将在项目的源目录中看到两个文件:string_int_label_map.pb.cc和string_int_label_map.pb.h。 将两个文件添加到 Xcode 项目中。- 在 Xcode 中,像在上一章中一样,将
ViewController.m重命名为ViewController.mm,然后类似于第 2 章,“通过迁移学习对图像进行分类”的HelloTensorFlow应用的ViewController.mm,在点击的处理器中为三个对象检测模型添加三个UIAlertAction,我们已将模型添加到项目中并将要测试。 现在,完整的项目文件应如图 3.7 所示:

图 3.7:TFObjectDetection API 项目文件
- 继续在
ViewController.mm中添加其余代码。 在viewDidLoad中,添加以编程方式创建新UIImageView的代码,以首先显示测试图像,并在选择了特定模型以在测试图像上运行之后显示检测到的结果,然后添加以下函数实现:
NSString* FilePathForResourceName(NSString* name, NSString* extension)
int LoadLablesFile(const string pbtxtFileName, object_detection::protos::StringIntLabelMap *imageLabels)
string GetDisplayName(const object_detection::protos::StringIntLabelMap* labels, int index)
Status LoadGraph(const string& graph_file_name, std::unique_ptr<tensorflow::Session>* session)
void DrawTopDetections(std::vector<Tensor>& outputs, int image_width, int image_height)
void RunInferenceOnImage(NSString *model)
下一步之后,我们将解释这些函数的实现,您可以在该书的源代码仓库的ch3/ios文件夹中获取所有源代码。
- 在 iOS 模拟器或设备中运行该应用。 首先,您会在屏幕上看到一张图片。 点按任意位置,您将看到一个对话框,要求您选择模型。 选择
SSD MobileNet模型,在模拟器中花费大约一秒钟,在 iPhone 6 上花费五秒钟,以在图像上绘制检测结果。 Faster RCNN Inception V2 需要更长的时间(在模拟器中大约需要 5 秒,在 iPhone 6 上大约需要 20 秒); 该模型也比SSD MobileNet更精确,可以捕获SSD MobileNet模型遗漏的一个狗物体。 最后一个模型,更快的 RCNN Resnet 101,在 iOS 模拟器中花费了将近 20 秒,但由于其大小而在 iPhone 6 上崩溃。 图 3.8 总结了运行结果:

图 3.8:使用不同的模型运行应用并显示检测结果
返回步骤 7 中的函数,FilePathForResourceName函数是用于返回资源文件路径的助手函数:mscoco_label_map.pbtxt文件,该文件定义了要检测的 90 个对象类的 ID,内部名称和显示名称。 ,模型图文件和测试图像。 它的实现与我们在上一章的HelloTensorFlow应用中看到的实现相同。
LoadLablesFile和GetDisplayName函数使用 Google Protobuf API 加载和解析mscoco_label_map.pbtxt文件,并返回显示名称以显示检测到的对象的 ID。
LoadGraph 尝试加载三个用户选择的模型文件之一,并返回加载状态。
这两个关键函数是RunInferenceOnImage和DrawTopDetections。 正如我们在“设置 TensorFlow 对象检测 API”部分中所看到的那样,summary_graph工具显示了我们在应用中使用的三种预训练对象检测模型的以下信息(请注意uint8类型):
Found 1 possible inputs: (name=image_tensor, type=uint8(4), shape=[?,?,?,3])
这就是为什么我们需要使用uint8创建一个图像张量,而不是float类型来加载到我们的模型,否则在运行模型时会出现错误。 另请注意,当我们使用 TensorFlow C++ API 的Session的Run方法将image_data转换为Tensor类型的image_data时,我们不使用input_mean和 input_std就像我们在使用图像分类模型时所做的(有关详细比较,请参见第 2 章,“通过迁移学习对图像进行分类”的 HelloTensorFlow 应用的RunInferenceOnImage实现)。 我们知道有四个名为detection_boxes,detection_scores,detection_classes和num_detections的输出,因此RunInferenceOnImage具有以下代码来为模型输入图像输入并获得四个输出:
tensorflow::Tensor image_tensor(tensorflow::DT_UINT8, tensorflow::TensorShape({1, image_height, image_width, wanted_channels}));
auto image_tensor_mapped = image_tensor.tensor<uint8, 4>();
tensorflow::uint8* in = image_data.data();
uint8* c_out = image_tensor_mapped.data();
for (int y = 0; y < image_height; ++y) {
tensorflow::uint8* in_row = in + (y * image_width * image_channels);
uint8* out_row = c_out + (y * image_width * wanted_channels);
for (int x = 0; x < image_width; ++x) {
tensorflow::uint8* in_pixel = in_row + (x * image_channels);
uint8* out_pixel = out_row + (x * wanted_channels);
for (int c = 0; c < wanted_channels; ++c) {
out_pixel[c] = in_pixel[c];
}
}
}
std::vector<Tensor> outputs;
Status run_status = session->Run({{"image_tensor", image_tensor}},
{"detection_boxes", "detection_scores", "detection_classes", "num_detections"}, {}, &outputs);
要在检测到的对象上绘制边界框,我们将outputs张量向量传递给DrawTopDetections,后者使用以下代码解析outputs向量以获取四个输出的值,并循环遍历每次检测以获得边界框值(左,上,右,下)以及检测到的对象 ID 的显示名称,因此您可以编写代码以使用以下名称绘制边界框:
auto detection_boxes = outputs[0].flat<float>();
auto detection_scores = outputs[1].flat<float>();
auto detection_classes = outputs[2].flat<float>();
auto num_detections = outputs[3].flat<float>()(0);
LOG(INFO) << "num_detections: " << num_detections << ", detection_scores size: " << detection_scores.size() << ", detection_classes size: " << detection_classes.size() << ", detection_boxes size: " << detection_boxes.size();
for (int i = 0; i < num_detections; i++) {
float left = detection_boxes(i * 4 + 1) * image_width;
float top = detection_boxes(i * 4 + 0) * image_height;
float right = detection_boxes(i * 4 + 3) * image_width;
float bottom = detection_boxes((i * 4 + 2)) * image_height;
string displayName = GetDisplayName(&imageLabels, detection_classes(i));
LOG(INFO) << "Detected " << i << ": " << displayName << ", " << score << ", (" << left << ", " << top << ", " << right << ", " << bottom << ")";
...
}
当前面的LOG(INFO)行与图 3.1 中的第二个测试图像一起运行时,以及 TensorFlow Object Detection API 网站上显示的演示图像时,将输出以下信息:
num_detections: 100, detection_scores size: 100, detection_classes size: 100, detection_boxes size: 400
Detected 0: person, 0.916851, (533.138, 498.37, 553.206, 533.727)
Detected 1: kite, 0.828284, (467.467, 344.695, 485.3, 362.049)
Detected 2: person, 0.779872, (78.2835, 516.831, 101.287, 560.955)
Detected 3: kite, 0.769913, (591.238, 72.0729, 676.863, 149.322)
这就是在 iOS 应用中使用现有的经过预训练的对象检测模型所需要的。 如何在 iOS 中使用我们的经过训练的对象检测模型? 事实证明,这与使用预训练模型几乎相同,在处理再训练图像分类模型时,无需像上一章一样修改input_size, input_mean, input_std和input_name。 您只需要执行以下操作:
- 将您的训练后的模型(例如,在上一节中创建的
output_inference_graph_ssd_mobilenet.pb文件,用于模型的训练的标签映射文件,例如pet_label_map.pbtxt)添加到TFObjectDetectionAPI项目 - 在
ViewController.mm中,使用重新训练的模型调用RunInferenceOnImage - 仍在
ViewController.mm中,在DrawTopDetections函数内调用LoadLablesFile([FilePathForResourceName(@"pet_label_map", @"pbtxt") UTF8String], &imageLabels);
而已。 运行该应用,您可以看到针对重新训练的模型对检测到的结果进行了更精细的调整。 例如,使用通过使用牛津宠物数据集进行重新训练而生成的前面的重新训练模型,我们希望看到边界框围绕头部区域而不是整个身体,而这正是我们在图 3.9 中所示的测试图像所看到的:

图 3.9:比较预训练和再训练模型的检测结果
使用 YOLO2 – 另一种物体检测模型
正如我们在第一部分中提到的,YOLO2 是另一个很酷的对象检测模型,它使用了与 RCNN 系列不同的方法。 它使用单个神经网络将输入图像划分为固定大小的区域(但不像 RCNN 系列方法那样提供区域建议),并预测每个区域的边界框,类别和概率。
TensorFlow Android 示例应用具有使用预训练的 YOLO 模型的示例代码,但没有 iOS 示例。 由于 YOLO2 是最快的对象检测模型之一,而且非常准确(请在其网站上查看其与 SSD 模型的 mAP 比较),因此有必要了解一下如何在 iOS 应用中使用它。
YOLO 使用称为 Darknet 的独特开源神经网络框架来训练其模型。 还有另一个名为 darkflow 的库,该库可以将使用 Darknet 训练的 YOLO 模型的神经网络权重转换为 TensorFlow 图格式,并重新训练预训练的模型。
要以 TensorFlow 格式构建 YOLO2 模型,请首先从这里获取 darkflow。因为它需要 Python3 和 TensorFlow 1.0(Python 2.7 和 TensorFlow 1.4 或更高版本也可能工作),所以我们将使用 Anaconda 来设置一个新的具有 Python3 支持的 TensorFlow 1.0 环境:
conda create --name tf1.0_p35 python=3.5
source activate tf1.0_p35
conda install -c derickl tensorflow
同时运行conda install -c menpo opencv3以安装 OpenCV 3,这是 darkflow 的另一个依赖项。 现在,将cd移至 darkflow 目录,然后运行pip install .安装 darkflow。
接下来,我们需要下载经过预训练的 YOLO 模型的权重-我们将尝试两个 Tiny-YOLO 模型,它们超级快,但不如完整的 YOLO 模型准确。 同时运行 Tiny-YOLO 模型和 YOLO 模型的 iOS 代码几乎相同,因此我们仅向您展示如何运行 Tiny-YOLO 模型。
您可以在 YOLO2 官方网站上下载 tiny-yolo-voc(受 20 个对象类的 PASCAL VOC 数据集训练)和 tiny-yolo(受 80 个对象类的 MS COCO 数据集训练)的权重和配置文件。 或 darkflow 仓库。 现在,运行以下命令将权重转换为 TensorFlow 图文件:
flow --model cfg/tiny-yolo-voc.cfg --load bin/tiny-yolo-voc.weights --savepb
flow --model cfg/tiny-yolo.cfg --load bin/tiny-yolo.weights --savepb
生成的两个文件tiny-yolo-voc.pb和tiny-yolo.pb将位于built_graph目录中。 现在,转到 TensorFlow 源根目录,并像上一章一样运行以下命令来创建量化模型:
python tensorflow/tools/quantization/quantize_graph.py --input=darkflow/built_graph/tiny-yolo.pb --output_node_names=output --output=quantized_tiny-yolo.pb --mode=weights
python tensorflow/tools/quantization/quantize_graph.py --input=darkflow/built_graph/tiny-yolo-voc.pb --output_node_names=output --output=quantized_tiny-yolo-voc.pb --mode=weights
现在,请按照以下步骤查看如何在我们的 iOS 应用中使用两个 YOLO 模型:
- 将
quantized_tiny-yolo-voc.pb和quantized_tiny-yolo.pb都拖到TFObjectDetectionAPI项目中 - 在
ViewController.mm中添加两个新的警报操作,因此在运行该应用时,您将看到可用于运行的模型,如图 3.10 所示:

图 3.10:将两个 YOLO 模型添加到 iOS 应用
- 添加以下代码以将输入图像处理到张量中以馈送到输入节点,并在加载了 YOLO 模型图的情况下运行 TensorFlow 会话以生成检测输出:
tensorflow::Tensor image_tensor(tensorflow::DT_FLOAT,tensorflow::TensorShape({1, wanted_height, wanted_width, wanted_channels}));
auto image_tensor_mapped = image_tensor.tensor<float, 4>();
tensorflow::uint8* in = image_data.data();
float* out = image_tensor_mapped.data();
for (int y = 0; y < wanted_height; ++y) {
...
out_pixel[c] = in_pixel[c] / 255.0f;
}
std::vector<tensorflow::Tensor> outputs;
tensorflow::Status run_status = session->Run({{"input", image_tensor}}, {"output"}, {}, &outputs);
请注意,此处的for-loop和session->Run与上一章中用于图像分类的代码以及使用本章前面所示的其他模型进行对象检测的代码中存在细微但重要的区别(我们未显示...中的代码段,因为与这两个示例中的相同)。 为了使图像数据转换正确,您需要了解模型的详细信息,或者从 Python,Android 或 iOS 的有效示例中学习,当然还要进行必要的调试。 为了正确设置输入和输出节点名称,可以使用summarize_graph工具或我们多次显示的 Python 代码段。
- 将输出结果传递给名为
YoloPostProcess的函数,该函数类似于tensorflow/examples/android/src/org/tensorflow/demo/TensorFlowYoloDetector.javaAndroid 示例文件中的后处理代码:
tensorflow::Tensor* output = &outputs[0];
std::vector<std::pair<float, int> > top_results;
YoloPostProcess(model, output->flat<float>(), &top_results);
我们不会在此处显示其余代码。 您可以在源代码存储库的ch3/ios中检出完整的 iOS 应用。
- 运行该应用,然后选择 YOLO2 Tiny VOC 或 YOLO2 Tiny COCO,与使用 SSD MobileNet v1 模型相比,您会看到类似的速度,但检测结果的准确率较差。
尽管基于 MobileNet 的 TensorFlow 模型和 Tiny YOLO2 模型的准确率较低,但 TensorFlow 对象检测模型和 YOLO2 模型在移动设备上的运行速度都非常快。 较大的 Faster RNN 模型和完整的 YOLO2 模型更准确,但是它们花费的时间更长,甚至无法在移动设备上运行。 因此,向移动应用添加快速对象检测的最佳方法是使用 SSD MobileNet 或 Tiny-YOLO2 模型,或经过重新训练和微调的模型。 模型的未来版本很可能会具有更好的表现和准确率。 凭借本章介绍的知识,您应该能够在 iOS 应用中快速启用对象检测。
总结
在本章中,我们首先简要概述了各种不同的基于深度学习的对象检测方法。 然后,我们详细介绍了如何使用 TensorFlow 对象检测 API 通过预训练的模型进行现成的推理,以及如何在 Python 中重新训练预训练的 TensorFlow 对象检测模型。 我们还提供了有关如何手动构建 TensorFlow iOS 库,使用该库创建新的 iOS 应用以及如何在 iOS 中使用预先存在和经过重新训练的 SSD MobileNet 和 Faster RCNN 模型的详细教程。 最后,我们展示了在您的 iOS 应用中使用另一种强大的对象检测模型 YOLO2 所需要的内容。
在下一章中,这是我们与计算机视觉相关的第三项任务,我们将仔细研究如何在 Python 和 TensorFlow 中训练和构建有趣的深度学习模型,以及如何在 iOS 和 Android 应用中使用它来添加令人赞叹的图像艺术风格。
四、以惊人的艺术风格变换图片
自从 2012 年深层神经网络在 AlexNet 赢得 ImageNet 挑战后开始起飞以来,人工智能研究人员一直在将深度学习技术(包括经过预训练的深度 CNN 模型)应用于越来越多的问题领域。 有什么能比创造艺术更有创造力? 一种想法已经提出并实现了,称为神经样式传递,它使您可以利用预训练的深度神经网络模型并传递图像或任何梵高的样式或莫奈的杰作),例如另一张图片(例如个人资料图片或您喜欢的狗的图片),从而创建将图片内容与杰作风格融合在一起的图片。 实际上,有一个名为 Prisma 的 iOS 应用在 2016 年获得了年度最佳应用奖。 在短短几秒钟内,它将以您选择的任何样式迁移您的图片。
在本章中,我们将首先概述三种神经样式迁移方法,其中一种是原始方法,一种是经过改进的方法,另一种是进一步改进的方法。 然后,我们将详细研究如何使用第二种方法来训练快速神经样式迁移模型,该模型可在您的 iOS 和 Android 智能手机中使用,以实现 Prisma 的功能。 接下来,我们将实际在 iOS 应用和 Android 应用中使用该模型,引导您完成从头开始创建此类应用的整个过程。 最后,我们将向您简要介绍 TensorFlow Magenta 开源项目,您可以将其用于基于深度学习构建更多的音乐和艺术生成应用,并向您展示如何使用单个预训练的样式迁移模型, 是基于神经样式迁移的最新研究进展而创建的,其中包括 26 种很酷的艺术样式,可在您的 iOS 和 Android 应用中获得更快的性能和结果。 总之,本章将涵盖以下主题:
-
神经样式迁移 -- 快速概述
-
训练快速的神经样式迁移模型
-
在 iOS 中使用快速的神经样式迁移模型
-
在 Android 中使用快速的神经样式迁移模型
-
在 iOS 中使用 TensorFlow Magenta 多样式模型
-
在 Android 中使用 TensorFlow Magenta 多样式模型
神经样式迁移 -- 快速概述
使用深度神经网络将图像内容与另一种图像的样式合并的原始思想和算法于 2015 年夏季发表在题为《艺术风格的神经算法》的论文中。它是 2014 年 ImageNet 图像识别挑战赛的获胜者,该挑战赛具有 16 个卷积层或特征映射,分别代表不同级别的图像内容。 在这种原始方法中,首先将最终迁移的图像初始化为与内容图像合并的白噪声图像。 内容损失函数定义为内容图像和结果图像的卷积层conv4_2上都被馈入 VGG-19 网络后,特定的一组特征表示形式的平方误差损失。 样式损失函数计算样式图像和所得图像在五个不同卷积层上的总误差差。 然后,将总损失定义为内容损失和样式损失的总和。 在训练期间,损失会降到最低,并生成将一个图像的内容与另一个图像的样式混合在一起的结果图像。
尽管原始神经样式迁移算法的结果令人惊叹,但其性能却很差-训练是样式迁移图像生成过程的一部分,通常在 GPU 上花费几分钟,在 CPU 上花费约一个小时才能生成良好的图像。 结果。
如果您对原始算法的细节感兴趣,可以在以下位置阅读该论文以及文档齐全的 Python 实现。我们不会讨论这种原始算法,因为在手机上运行该算法是不可行的,但是尝试该算法很有趣且有用,可以更好地了解如何针对不同的计算机视觉任务使用预训练的深度 CNN 模型。
自然地,在 2016 年,论文中发布了一种“快三个数量级”的新算法,即《实时样式传递和超分辨率的感知损失》,作者是 Justin Johnson 等。 它使用单独的训练过程,并定义了更好的损失函数,这些函数本身就是深度神经网络。 训练后(在下一节中我们将看到,在 GPU 上可能要花费几个小时),使用训练后的模型来生成样式迁移的图像在计算机上几乎是实时的,而在智能手机上只需几秒钟。
使用这种快速神经迁移算法仍然有一个缺点:只能针对特定样式训练模型,因此,要在您的应用中使用不同的样式,必须逐一训练这些样式以为每种样式生成一个模型 。 2017 年发表了一篇名为《学习风格的艺术表现形式》的新论文,它发现一个单一的深度神经网络模型可以概括许多不同的样式。 TensorFlow Magenta 项目包括具有多种样式的预训练模型,我们将在本章的最后两个部分中看到,在 iOS 和 Android 应用中使用这种模型来产生强大而神奇的艺术效果是多么容易。
训练快速的神经样式迁移模型
在本部分中,我们将向您展示如何使用带有 TensorFlow 的快速神经样式迁移算法训练模型。 执行以下步骤来训练这样的模型:
- 在 Mac 的终端上,或者最好在 GPU 驱动的 Ubuntu 上,运行
git clone https://github.com/jeffxtang/fast-style-transfer,这是 Johnson 的快速样式迁移的 TensorFlow 实现的一个很好的分支,已修改为允许在 iOS 或 Android 应用中使用经过训练的模型。 cd到快速样式迁移目录,然后运行setup.sh脚本下载预训练的 VGG-19 模型文件以及 MS COCO 训练数据集,我们在上一章中提到过,注意下载大文件可能需要几个小时。- 运行以下命令,使用名为
starry_night.jpg的样式图像和名为ww1.jpg的内容图像进行训练,以创建检查点文件:
mkdir checkpoints
mkdir test_dir
python style.py --style images/starry_night.jpg --test images/ww1.jpg --test-dir test_dir --content-weight 1.5e1 --checkpoint-dir checkpoints --checkpoint-iterations 1000 --batch-size 10
images目录中还有一些其他样式的图像,可用于创建不同的检查点文件。 此处使用的starry_night.jpg样式图片是梵高的一幅著名画作,如图 4.1 所示:

图 4.1:用梵高的绘画作为风格图像
在第 1 章, “移动 TensorFlow 入门”中设置的 NVIDIA GTX 1070 GPU 驱动的 Ubuntu 上,整个训练大约需要 5 个小时,并且在 CPU 上肯定要花更长的时间 。
该脚本最初是为 TensorFlow 0.12 编写的,但后来为 TensorFlow 1.1 进行了修改,并且已被验证为可以在 TensorFlow 1.4 的 Python 2.7 环境中正常运行。
- 在文本编辑器中打开
evaluate.py文件,然后取消注释以下两行代码(在 158 和 159 行):
# saver = tf.train.Saver()
# saver.save(sess, "checkpoints_ios/fns.ckpt")
- 运行以下命令,以输入图像
img_placeholder和迁移的图像preds创建新的检查点:
python evaluate.py --checkpoint checkpoints \
--in-path examples/content/dog.jpg \
--out-path examples/content/dog-output.jpg
- 运行以下命令以构建一个 TensorFlow 图文件,该文件将图定义和检查点中的权重结合在一起。 这将创建一个大约 6.7MB 的
.pb文件:
python freeze.py --model_folder=checkpoints_ios --output_graph fst_frozen.pb
- 假设您具有
/tf_files目录,将生成的fst_frozen.pb文件复制到/tf_files,cd直接复制到 TensorFlow 源根目录(可能是~/tensorflow-1.4.0),然后运行以下命令以生成量化模型的.pb文件(我们在第 2 章,“通过迁移学习对图像进行分类”中介绍了量化):
bazel-bin/tensorflow/tools/quantization/quantize_graph \
--input=/tf_files/fst_frozen.pb \
--output_node_names=preds \
--output=/tf_files/fst_frozen_quantized.pb \
--mode=weights
这会将冻结的图文件大小从 6.7MB 减小到 1.7MB,这意味着,如果在您的应用中放置 50 种不同风格的 50 个模型,则增加的大小将约为 85MB。 苹果于 2017 年 9 月宣布,蜂窝无线应用下载限制已增加至 150MB,因此用户仍应能够通过蜂窝网络下载具有 50 多种不同样式的应用。
这就是使用样式图像和输入图像来训练和量化快速神经迁移模型的全部步骤。 您可以在步骤 3 中生成的 test_dir 目录中签出生成的图像,以查看样式迁移的效果。 如果需要,您可以使用中记录的超参数 https://github.com/jeffxtang/fast-style-transfer/blob/master/docs.md#style 进行查看,以及希望样式迁移效果更好。
在了解如何在 iOS 和 Android 应用中使用这些模型之前,重要的一点是,您需要记下在第 5 步中使用的,指定为--in-path值的图像的确切图像宽度和高度参数,并在 iOS 或 Android 代码中使用图像的宽度和高度值(您会看到多久了),否则在应用中运行模型时,会出现 Conv2DCustomBackpropInput: Size of out_backprop doesn't match computed错误 。
在 iOS 中使用快速的神经样式迁移模型
事实证明,在由 TensorFlow 实验性容器构建的 iOS 应用中,使用在步骤 7 中生成的fst_frozen_quantized.pb模型文件没有问题,如第 2 章,“通过迁移学习对图像分类”,但 TensorFlow Magenta 项目中的预训练多样式模型文件(我们将在本章的后续部分中使用)将不会随 TensorFlow Pod 一起加载(截至 2018 年 1 月)—尝试加载多样式模型文件时将引发以下错误:
Could not create TensorFlow Graph: Invalid argument: No OpKernel was registered to support Op 'Mul' with these attrs. Registered devices: [CPU], Registered kernels:
device='CPU'; T in [DT_FLOAT]
[[Node: transformer/expand/conv1/mul_1 = Mul[T=DT_INT32](transformer/expand/conv1/mul_1/x, transformer/expand/conv1/strided_slice_1)]]
在第 3 章,“检测对象及其位置”中,我们讨论了原因以及如何使用手动构建的 TensorFlow 库修复此错误。 由于我们将在同一 iOS 应用中使用这两种模型,因此我们将使用功能更强大的手动 TensorFlow 库创建一个新的 iOS 应用。
对快速神经迁移模型进行添加和测试
如果您尚未手动构建 TensorFlow 库,则需要先回到上一章。 然后执行以下步骤以将 TensorFlow 支持和快速的神经样式迁移模型文件添加到您的 iOS 应用并测试运行该应用:
- 如果您已经具有添加了 TensorFlow 手动库的 iOS 应用,则可以跳过此步骤。 否则,类似于我们在上一章中所做的,创建一个新的基于 Objective-C 的 iOS 应用,例如
NeuralStyleTransfer,或者在现有应用中,在PROJECT下创建一个新的用户定义设置,命名为TENSORFLOW_ROOT,值为$HOME/tensorflow-1.4.0,假定在那儿已安装 TensorFlow 1.4.0,然后在TARGET的构建设置中,将其他链接器标志设置为:
-force_load $(TENSORFLOW_ROOT)/tensorflow/contrib/makefile/gen/lib/libtensorflow-core.a $(TENSORFLOW_ROOT)/tensorflow/contrib/makefile/gen/protobuf_ios/lib/libprotobuf.a $(TENSORFLOW_ROOT)/tensorflow/contrib/makefile/gen/protobuf_ios/lib/libprotobuf-lite.a $(TENSORFLOW_ROOT)/tensorflow/contrib/makefile/downloads/nsync/builds/lipo.ios.c++11/nsync.a
然后将标题搜索路径设置为:
$(TENSORFLOW_ROOT) $(TENSORFLOW_ROOT)/tensorflow/contrib/makefile/downloads/protobuf/src $(TENSORFLOW_ROOT)/tensorflow/contrib/makefile/downloads $(TENSORFLOW_ROOT)/tensorflow/contrib/makefile/downloads/eigen $(TENSORFLOW_ROOT)/tensorflow/contrib/makefile/gen/proto
-
将
fst_frozen_quantized.pb文件和一些测试图像拖放到项目的文件夹中。 从以前的 iOS 应用中,或从本书源代码仓库中Ch4/ios下的NeuralStyleTransfer应用文件夹中复制我们在前几章中使用过的相同ios_image_load.mm和.h文件到项目中。 -
将
ViewController.m重命名为ViewController.mm并将其替换为Ch4/ios/NeuralStyleTransfer中的ViewController.h和.mm文件。 在测试运行该应用后,我们将详细介绍核心代码段。 -
在 iOS 模拟器或 iOS 设备上运行该应用,您将看到一张狗图片,如图 4.2 所示:

图 4.2:应用样式之前的原始狗图片
- 点击以选择快速样式迁移,几秒钟后,您将在图 4.3 中看到一张新图片,其中已迁移了繁星点点的夜色:

图 4.3:就像让梵高画出您喜欢的狗一样
您只需选择喜欢的图片作为样式图像,然后按照上一节中的步骤操作,即可轻松构建具有不同样式的其他模型。 然后,您可以按照本节中的步骤在 iOS 应用中使用模型。 如果您想了解模型的训练方法,则应在上一节的 GitHub 存储库中查看代码。 让我们详细看一下使用该模型完成魔术的 iOS 代码。
回顾快速神经迁移模型的 iOS 代码
ViewController.mm中有几个关键代码段,它们在输入图像的预处理和迁移图像的后处理中是唯一的:
- 在步骤 5 中,将两个常量
wanted_width和wanted_height定义为与存储库图像examples/content/dog.jpg的图像宽度和高度相同的值:
const int wanted_width = 300;
const int wanted_height = 400;
- iOS 的分派队列用于在非 UI 线程中加载和运行我们的快速神经迁移模型,并在生成样式迁移的图像后,将图像发送到 UI 线程进行显示:
dispatch_async(dispatch_get_global_queue(0, 0), ^{
UIImage *img = imageStyleTransfer(@"fst_frozen_quantized");
dispatch_async(dispatch_get_main_queue(), ^{
_lbl.text = @"Tap Anywhere";
_iv.image = img;
});
});
- 定义了一个浮点数的 3 维张量,该张量用于将输入图像数据转换为:
tensorflow::Tensor image_tensor(tensorflow::DT_FLOAT, tensorflow::TensorShape({wanted_height, wanted_width, wanted_channels}));
auto image_tensor_mapped = image_tensor.tensor<float, 3>();
- 发送到 TensorFlow
Session->Run方法的输入节点名称和输出节点名称定义为与训练模型时相同:
std::string input_layer = "img_placeholder";
std::string output_layer = "preds";
std::vector<tensorflow::Tensor> outputs;
tensorflow::Status run_status = session->Run({{input_layer, image_tensor}} {output_layer}, {}, &outputs);
- 模型完成运行并发送回输出张量(其中包含 0 到 255 范围内的 RGB 值)后,我们需要调用一个名为
tensorToUIImage的实用函数,以将张量数据首先转换为 RGB 缓冲区:
UIImage *imgScaled = tensorToUIImage(model, output->flat<float>(), image_width, image_height);
static UIImage* tensorToUIImage(NSString *model, const Eigen::TensorMap<Eigen::Tensor<float, 1, Eigen::RowMajor>, Eigen::Aligned>& outputTensor, int image_width, int image_height) {
const int count = outputTensor.size();
unsigned char* buffer = (unsigned char*)malloc(count);
for (int i = 0; i < count; ++i) {
const float value = outputTensor(i);
int n;
if (value < 0) n = 0;
else if (value > 255) n = 255;
else n = (int)value;
buffer[i] = n;
}
- 然后,我们将缓冲区转换为
UIImage实例,然后再调整其大小并返回以供显示:
UIImage *img = [ViewController convertRGBBufferToUIImage:buffer withWidth:wanted_width withHeight:wanted_height];
UIImage *imgScaled = [img scaleToSize:CGSizeMake(image_width, image_height)];
return imgScaled;
完整的代码和应用程序位于Ch4/ios/NeuralStyleTransfer文件夹中。
在 Android 中使用快速的神经样式迁移模型
在第 2 章,“通过迁移学习对图像进行分类”中,我们描述了如何将 TensorFlow 添加到您自己的 Android 应用中,但没有任何 UI。 让我们创建一个新的 Android 应用,以使用我们之前训练并在 iOS 中使用的快速样式迁移模型。
由于此 Android 应用提供了一个很好的机会来使用最少的 TensorFlow 相关代码,Android UI 和线程化代码来运行完整的 TensorFlow 模型驱动的应用,因此,我们将从头开始添加每行代码,以帮助您进一步了解从头开始开发 Android TensorFlow 应用需要什么:
-
在 Android Studio 中,选择“文件 | 新增 | 新项目...”,然后输入
FastNeuralTransfer作为应用名称; 在单击“完成”之前,接受所有默认设置。 -
创建一个新的
assets文件夹,如图 2.13 所示,然后将您训练过的快速神经迁移模型从 iOS 应用中拖动(如果您在上一节中尝试过),或者从文件夹/tf_files中拖动,如“训练快速神经样式迁移模型”部分步骤 7 所示,以及一些测试图像到assets文件夹。 -
在应用的
build.gradle文件中,在dependencies的末尾添加一行compile 'org.tensorflow:tensorflow-android:+'和。 -
打开
res/layout/activity_main.xml文件,在其中删除默认的TextView,然后首先添加一个ImageView以显示样式迁移前后的图像:
<ImageView
android:id="@+id/imageview"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
- 添加一个按钮以启动样式迁移操作:
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Style Transfer"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0.502"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.965" />
- 在应用的
MainActivity.java文件中,首先输入我们最重要的导入:
import org.tensorflow.contrib.android.TensorFlowInferenceInterface;
TensorFlowInferenceInterface提供 JAVA 接口来访问本机 TensorFlow 推理 API。 然后确保MainActivity类实现了Runnable 接口,因为我们需要保持我们的应用响应速度,并在工作线程上加载并运行 TensorFlow 模型。
- 在类的开头,定义六个常量,如下所示:
private static final String MODEL_FILE = "file:///android_asset/fst_frozen_quantized.pb";
private static final String INPUT_NODE = "img_placeholder";
private static final String OUTPUT_NODE = "preds";
private static final String IMAGE_NAME = "pug1.jpg";
private static final int WANTED_WIDTH = 300;
private static final int WANTED_HEIGHT = 400;
您可以将任何训练有素的模型文件用于MODEL_FILE。 INPUT_NODE和OUTPUT_NODE的值与我们在 Python 训练脚本中设置并在 iOS 应用中使用的值相同。 同样,WANTED_WIDTH和WANTED_HEIGHT与我们在“训练快速神经样式迁移模型”部分的第 5 步中使用的--in-path图像的宽度和高度相同。
- 声明四个实例变量:
private ImageView mImageView;
private Button mButton;
private Bitmap mTransferredBitmap;
private TensorFlowInferenceInterface mInferenceInterface;
mImageView和mButton将使用onCreate方法中的简单findViewById方法进行设置。 mTransferredBitmap将保留已迁移图像的位图,以便mImageView可以显示它。 mInferenceInterface用于加载我们的 TensorFlow 模型,将输入图像输入模型,运行模型,并返回推理结果。
- 在我们的 TensorFlow 推断线程向
Handler实例发送消息之后,创建一个Handler实例来处理在主线程中显示最终迁移的图像的任务,我们还创建一个方便的Toast消息:
Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
mButton.setText("Style Transfer");
String text = (String)msg.obj;
Toast.makeText(MainActivity.this, text,
Toast.LENGTH_SHORT).show();
mImageView.setImageBitmap(mTransferredBitmap);
} };
- 在
onCreate方法内部,我们将使用mImageView实例变量绑定布局 xml 文件中的ImageView,将测试图像的位图加载到assets文件夹中,并在ImageView中显示 :
mImageView = findViewById(R.id.imageview);
try {
AssetManager am = getAssets();
InputStream is = am.open(IMAGE_NAME);
Bitmap bitmap = BitmapFactory.decodeStream(is);
mImageView.setImageBitmap(bitmap);
} catch (IOException e) {
e.printStackTrace();
}
- 类似地设置
mButton并设置一个点击监听器,以便在点击按钮时,创建并启动一个新线程,并调用run方法:
mButton = findViewById(R.id.button);
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mButton.setText("Processing...");
Thread thread = new Thread(MainActivity.this);
thread.start();
}
});
- 在线程的
run方法中,我们首先声明三个数组,并为其分配适当的内存:intValues数组保存测试图像的像素值,每个像素值代表 32 位 ARGB(Alpha,红,绿,蓝色)值;floatValues数组如模型所预期的那样分别保存每个像素的红色,绿色和蓝色值,因此其大小是intValues的三倍,并且outputValues的大小与floatValues相同 ],但保留模型的输出值:
public void run() {
int[] intValues = new int[WANTED_WIDTH * WANTED_HEIGHT];
float[] floatValues = new float[WANTED_WIDTH * WANTED_HEIGHT * 3];
float[] outputValues = new float[WANTED_WIDTH * WANTED_HEIGHT * 3];
然后,我们获得测试图像的位图数据,对其进行缩放以匹配训练中使用的图像的大小,然后将缩放后的位图的像素加载到intValues数组并将其转换为floatValues:
Bitmap bitmap = BitmapFactory.decodeStream(getAssets().open(IMAGE_NAME));
Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, WANTED_WIDTH, WANTED_HEIGHT, true);
scaledBitmap.getPixels(intValues, 0, scaledBitmap.getWidth(), 0, 0, scaledBitmap.getWidth(), scaledBitmap.getHeight());
for (int i = 0; i < intValues.length; i++) {
final int val = intValues[i];
floatValues[i*3] = ((val >> 16) & 0xFF);
floatValues[i*3+1] = ((val >> 8) & 0xFF);
floatValues[i*3+2] = (val & 0xFF);
}
注意,val或intValues像素数组的每个元素是一个 32 位整数,在其每个 8 位区域中均保留 ARGB。 我们使用向右移位(用于红色和绿色)和按位与运算来提取每个像素的红色,绿色和蓝色值,而忽略intValues元素中最左边的 8 位的 Alpha 值。 因此floatValues[i*3],floatValues[i*3+1]和floatValues[i*3+2]分别保持像素的红色,绿色和蓝色值。
现在,我们创建一个新的TensorFlowInferenceInterface实例,并在其中将AssetManager实例和模型文件名传递到assets文件夹中,然后使用 TensorFlowInferenceInterface实例将转换后的[ floatValues数组。 如果模型需要多个输入节点,则可以调用多个feed方法。 然后,我们通过传递输出节点名称的字符串数组来运行模型。 在这里,对于我们的快速样式迁移模型,我们只有一个输入节点和一个输出节点。 最后,我们通过传递输出节点名称来获取模型的输出值。 如果希望接收多个输出节点,则可以调用多个访存:
AssetManager assetManager = getAssets();
mInferenceInterface = new TensorFlowInferenceInterface(assetManager, MODEL_FILE);
mInferenceInterface.feed(INPUT_NODE, floatValues, WANTED_HEIGHT, WANTED_WIDTH, 3);
mInferenceInterface.run(new String[] {OUTPUT_NODE}, false);
mInferenceInterface.fetch(OUTPUT_NODE, outputValues);
模型生成的outputValues在每个元素中都保留 0 到 255 之间的 8 位红色,绿色和蓝色值之一,我们首先对红色和绿色值使用左移操作,但是具有不同的移位大小(16 和 8),然后使用按位或运算将 8 位 Alpha 值(0xFF)与 8 位 RGB 值组合,将结果保存在intValues数组中:
for (int i=0; i < intValues.length; ++i) {
intValues[i] = 0xFF000000
| (((int) outputValues[i*3]) << 16)
| (((int) outputValues[i*3+1]) << 8)
| ((int) outputValues[i*3+2]);
然后,我们创建一个新的Bitmap实例,并使用intValues数组设置其像素值,将位图缩放到测试图像的原始大小,并将缩放后的位图保存到mTransferredBitmap:
Bitmap outputBitmap = scaledBitmap.copy( scaledBitmap.getConfig() , true);
outputBitmap.setPixels(intValues, 0, outputBitmap.getWidth(), 0, 0, outputBitmap.getWidth(), outputBitmap.getHeight());
mTransferredBitmap = Bitmap.createScaledBitmap(outputBitmap, bitmap.getWidth(), bitmap.getHeight(), true);
最后,我们向主线程的处理器发送一条消息,以使其知道显示样式迁移图像的时间:
Message msg = new Message();
msg.obj = "Tranfer Processing Done";
mHandler.sendMessage(msg);
因此,总共不到 100 行代码,您就有了一个完整的 Android 应用,可以对图像进行惊人的样式迁移。 在 Android 设备或虚拟设备上运行该应用,首先将看到一个带有按钮的测试图像,点击该按钮,几秒钟后,您将看到样式迁移的图像,如图 4.4 所示:

图 4.4:Android 上原始图像和样式迁移的图像
快速神经样式模型存在的一个问题是,即使量化后每个模型只有 1.7MB,我们仍然需要针对每种样式分别进行训练,并且每个训练的模型只能支持一种样式迁移。 幸运的是,这个问题有很好的解决方案。
在 iOS 中使用 TensorFlow Magenta 多样式模型
TensorFlow Magenta 项目允许您使用 10 多种经过预训练的模型来生成新的音乐和图像。 在本节和下一节中,我们将重点介绍使用 Magenta 的图像样式化模型。 您可以单击链接在计算机上安装 Magenta,尽管要在移动应用中使用其炫酷的图像样式迁移模型,也不必安装 Magenta。 基于论文《艺术风格的习得表示》实现的 Magenta 预训练样式迁移模型,消除了一个模型只能具有一种风格的限制,并允许多种风格包含在单个模型文件中,您可以选择使用这些样式的任意组合。 您可以在这个页面上快速浏览该演示,但可以在此处下载两个预训练的检查点模型。 由于检查点文件中保存了某些NaN(不是数字)错误,因此无法直接在您的移动应用中使用。 我们不会详细说明如何删除这些数字并生成可在您的应用中使用的.pb模型文件(如果感兴趣,您可以查看这里),我们仅使用 TensorFlow Android 示例tensorflow/examples/android/assets中包含的经过预训练的stylize_quantized.pb模型文件来查看其工作原理。
如果您确实想训练自己的模型,则可以按照前面的image_stylization链接中的训练模型下的步骤进行。 但是请注意,您至少需要 500GB 的可用磁盘空间才能下载 ImageNet 数据集,并需要强大的 GPU 来完成训练。 在本节或下一节中看到代码和结果之后,您更有可能对预训练的stylize_quantized.pb模型启用的炫酷样式迁移效果感到满意。
在本章前面创建的 iOS 应用中,执行以下步骤来使用和运行多样式模型:
- 将
stylize_quantized.pb文件从tensorflow/examples/android/assets拖放到 Xcode 中的 iOSapps文件夹中。 - 使用用于加载和处理快速迁移样式模型的相同
dispatch_async,向抽头处理器中添加新的UIAlertAction:
UIAlertAction* multi_style_transfer = [UIAlertAction actionWithTitle:@"Multistyle Transfer" style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
_lbl.text = @"Processing...";
_iv.image = [UIImage imageNamed:image_name];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
UIImage *img = imageStyleTransfer(@"stylize_quantized");
dispatch_async(dispatch_get_main_queue(), ^{
_lbl.text = @"Tap Anywhere";
_iv.image = img;
});
});
}];
- 将
input_layer和output_layer值替换为新模型的正确值,并添加一个名为style_num的新输入节点名称(这些值来自StylizeActivity.java中的示例 Android 代码,但您也可以使用summarize_graph工具,TensorBoard 或我们在前几章中显示的代码段中找到它们):
std::string input_layer = "input";
std::string style_layer = "style_num";
std::string output_layer = "transformer/expand/conv3/conv/Sigmoid";
- 与快速样式迁移模型不同,此处的多样式模型期望使用 4 维浮点张量作为图像输入:
tensorflow::Tensor image_tensor(tensorflow::DT_FLOAT, tensorflow::TensorShape({1, wanted_height, wanted_width, wanted_channels}));
auto image_tensor_mapped = image_tensor.tensor<float, 4>();
- 我们还需要将
style_tensor定义为形状为[NUM_STYLES * 1]的另一个张量,其中NUM_STYLES在ViewController.mm的开头定义为const int NUM_STYLES = 26;。 数字 26 是stylize_quantized.pb模型文件中内置的样式数,您可以在其中运行 Android TF 风格化应用并查看 26 种结果,如图 4.5 所示。 请注意,第 20 张图片(左下角的图片)是梵高熟悉的繁星点点的夜晚:

图 4.5:多样式模型中的 26 种样式图像
tensorflow::Tensor style_tensor(tensorflow::DT_FLOAT, tensorflow::TensorShape({ NUM_STYLES, 1}));
auto style_tensor_mapped = style_tensor.tensor<float, 2>();
float* out_style = style_tensor_mapped.data();
for (int i = 0; i < NUM_STYLES; i++) {
out_style[i] = 0.0 / NUM_STYLES;
}
out_style[19] = 1.0;
out_style数组中所有值的总和必须为 1,最终的样式迁移图像将是由out_style数组中指定的值加权的样式的混合。 例如,前面的代码将仅使用繁星点点的夜晚样式(数组索引 19 对应于图 4.5 中的样式图像列表中的第 20 个图像)。
如果希望将繁星点点的夜景图像和右上角图像均匀混合,则需要用以下代码替换前面代码块中的最后一行:
out_style[4] = 0.5;
out_style[19] = 0.5;
如果您希望所有 26 种样式均等地混合使用,请将前面的for循环更改为以下样式,并且不要将其他值设置为任何特定的out_style元素:
for (int i = 0; i < NUM_STYLES; i++) {
out_style[i] = 1.0 / NUM_STYLES;
}
稍后,您将在图 4.8 和 4.9 中看到这三种设置的样式迁移效果。
- 将
session->Run调用更改为以下行,以将图像张量和样式张量都发送到模型:
tensorflow::Status run_status = session->Run({{input_layer, image_tensor}, {style_layer, style_tensor}}, {output_layer}, {}, &outputs);
这些就是使用多样式模型运行 iOS 应用所需的全部更改。 现在运行您的应用,您将首先看到如图 4.6 所示的内容:

图 4.6:显示原始内容图像
点击任意位置,您将看到两个样式选择,如图 4.7 所示:

图 4.7:显示两种样式模型的选择
两张已迁移图像的结果,out_style[19] = 1.0; 如图 4.8 所示:

图 4.8:两种不同模型的样式迁移结果(左侧是快速样式迁移,右侧是多样式)
图 4.9 显示了在图 4.5 中使用星夜图像和右上角图像的均等混合以及所有 26 种样式的均等混合的结果,如图 4.9 所示:

图 4.9:多种样式的不同混合的结果(左边:一半星空一半其它,右边:所有 26 种样式的混合)
多样式模型在 iPhone 6 上运行大约需要 5 秒钟,比快速样式迁移模型运行快大约 2-3 倍。
在 Android 中使用 TensorFlow Magenta 多样式模型
尽管 TensorFlow Android 示例应用已经具有使用多种样式模型的代码(实际上我们在上一节的 iOS 应用中使用了 Android 示例应用中的模型),但示例应用中与 TensorFlow 相关的代码却与超过 600 行的StylizeActivity.java文件中的很多 UI 代码混在一起。 您还可以通过 TensorFlow Android 样式迁移的 Codelab 进行操作,但是代码与 TensorFlow Android 示例应用大致相同。 由于我们已经使用 TensorFlow 快速样式迁移模型实现了 Android 应用的简约实现,因此很有趣的是,我们如何仅需更改几行代码就可以拥有一个强大的多样式样式迁移应用。 这也应该是一种更直观的方法,以了解如何将出色的 TensorFlow 模型添加到现有的 Android 应用中。
因此,这就是在我们之前构建的 Android 应用中使用多样式迁移模型所需要的:
-
将
stylize_quantized.pb文件从tensorflow/examples/android/assets拖放到我们 Android 应用的assets文件夹中。 -
在 Android Studio 中,打开
MainActivity.java,找到以下三行代码:
private static final String MODEL_FILE = "file:///android_asset/fst_frozen_quantized.pb";
private static final String INPUT_NODE = "img_placeholder";
private static final String OUTPUT_NODE = "preds";
然后将它们替换为以下四行:
private static final int NUM_STYLES = 26;
private static final String MODEL_FILE = "file:///android_asset/stylize_quantized.pb";
private static final String INPUT_NODE = "input";
private static final String OUTPUT_NODE = "transformer/expand/conv3/conv/Sigmoid";
这些值与我们在上一节中构建的 iOS 应用相同。 如果您仅进行 Android 应用开发并跳过了上一个 iOS 部分,请快速阅读上一个 iOS 部分中对步骤 3 的解释。
- 替换以下代码片段,该片段将输入图像馈送到快速样式迁移模型并处理输出图像:
mInferenceInterface.feed(INPUT_NODE, floatValues, WANTED_HEIGHT, WANTED_WIDTH, 3);
mInferenceInterface.run(new String[] {OUTPUT_NODE}, false);
mInferenceInterface.fetch(OUTPUT_NODE, outputValues);
for (int i = 0; i < intValues.length; ++i) {
intValues[i] = 0xFF000000
| (((int) outputValues[i * 3]) << 16)
| (((int) outputValues[i * 3 + 1]) << 8)
| ((int) outputValues[i * 3 + 2]);
}
使用首先设置styleVals数组的代码段(如果对styleVals和如何设置数组的值感到困惑,请查看上一节第 5 步中的注解):
final float[] styleVals = new float[NUM_STYLES];
for (int i = 0; i < NUM_STYLES; ++i) {
styleVals[i] = 0.0f / NUM_STYLES;
}
styleVals[19] = 0.5f;
styleVals[4] = 0.5f;
然后将输入图像张量和样式值张量同时馈送到模型,并运行模型以获取迁移的图像:
mInferenceInterface.feed(INPUT_NODE, floatValues, 1, WANTED_HEIGHT, WANTED_WIDTH, 3);
mInferenceInterface.feed("style_num", styleVals, NUM_STYLES);
mInferenceInterface.run(new String[] {OUTPUT_NODE}, false);
mInferenceInterface.fetch(OUTPUT_NODE, outputValues);
最后,它处理输出:
for (int i=0; i < intValues.length; ++i) {
intValues[i] = 0xFF000000
| (((int) (outputValues[i*3] * 255)) << 16)
| (((int) (outputValues[i*3+1] * 255)) << 8)
| ((int) (outputValues[i*3+2] * 255));
}
注意,多样式模型返回浮点数数组到outputValues,它们的范围都在 0.0 到 1.0 之间,因此我们需要将它们相乘。在应用左位移操作以获取红色和绿色值之前,先进行 255 乘以 255,然后对 intValues 数组的每个元素应用按位“或”设置最终 ARGB 值。
这就是将酷炫的多样式模型添加到独立的 Android 应用所需的全部工作。 现在,让我们运行该应用,并使用不同的测试图像,但使用与 iOS 应用中相同的三种样式值组合。
将第 20 和第 5 种样式图像按步骤 3 中的代码片段进行均等混合后,原始图像和迁移的图像如图 4.10 所示:

图 4.10:原始内容图像和样式迁移的图像,结合了第五个图像和繁星点点的夜晚图像
如果替换以下两行代码:
styleVals[19] = 0.5f;
styleVals[4] = 0.5f;
用单行代码styleVals[19] = 1.5f;或替换以下代码段:
for (int i = 0; i < NUM_STYLES; ++i) {
styleVals[i] = 0.0f / NUM_STYLES;
}
styleVals[19] = 0.5f;
styleVals[4] = 0.5f;
带有以下代码段:
for (int i = 0; i < NUM_STYLES; ++i) {
styleVals[i] = 1.0f / NUM_STYLES;
}
然后,您将在图 4.11 中看到效果:

图 4.11:仅以星空风格来风格化的图像,以及将所有 26 种风格均等混合的图像
在一些强大的 TensorFlow 模型的帮助下以及我们如何在移动应用中使用它们的知识,看起来我们的移动开发人员也可以成为伟大的艺术家。
总结
在本章中,我们首先概述了自 2015 年以来开发的各种神经样式迁移方法。然后,我们展示了如何训练第二代样式迁移模型,该模型足够快,可以在几秒钟内在移动设备上运行。 之后,我们介绍了如何在 iOS 应用和 Android 应用中使用该模型,这些方法是从头开始构建的,采用极简方法,总共不到 100 行代码。 最后,我们讨论了如何在 iOS 和 Android 应用中使用 TensorFlow Magenta 多样式神经迁移模型,该模型在单个小模型中包含 26 种惊人的艺术样式。
在下一章中,我们将探讨另一个在人类或我们最好的朋友演示时被视为智能的任务:能够识别语音命令。 谁不想让我们的狗狗理解“坐下”,“来”,“不”等命令或我们的婴儿对“是”,“停止”或“走”做出回应? 让我们看看我们如何开发与它们一样的移动应用。
五、了解简单的语音命令
如今,语音服务(例如 Apple Siri,Amazon Alexa,Google Assistant 和 Google Translate)已变得越来越流行,因为语音是我们在某些情况下查找信息或完成任务的最自然和有效的方法。 这些语音服务中的许多服务都是基于云的,因为用户语音可能会很长而且很自由,并且自动语音识别(ASR)非常复杂,并且需要大量的计算能力。 实际上,得益于深度学习的突破,仅在最近几年,在自然和嘈杂的环境中 ASR 才变得可行。
但是在某些情况下,能够离线识别设备上的简单语音命令是有意义的。 例如,要控制 Raspberry-Pi 驱动的机器人的运动,您不需要复杂的语音命令,不仅设备上的 ASR 比基于云的解决方案还快,而且即使在没有网络访问的环境。 设备上的简单语音命令识别还可以通过仅在发出某些明确的用户命令时才向服务器发送复杂的用户语音来节省网络带宽。
在本章中,我们将首先概述 ASR 技术,涵盖基于最新的深度学习系统和顶级开源项目。 然后,我们将讨论如何训练和重新训练 TensorFlow 模型,以识别简单的语音命令,例如"left", "right", "up", "down", "stop", "go"。 接下来,我们将使用训练有素的模型来构建一个简单的 Android 应用,然后再构建两个完整的 iOS 应用,一个由 Objective-C 实现,另一个由 Swift 实现。 在前两章中我们没有介绍使用 TensorFlow 模型的基于 Swift 的 iOS 应用,而本章是回顾和加强我们对构建基于 Swift 的 TensorFlow iOS 应用的理解的好地方。
总之,本章将涵盖以下主题:
- 语音识别 -- 快速概述
- 训练简单的命令识别模型
- 在 Android 中使用简单的语音识别模型
- 在带有 Objective-C 的 iOS 中使用简单的语音识别模型
- 在带有 Swift 的 iOS 中使用简单的语音识别模型
语音识别 -- 快速概述
1990 年代出现了第一个实用的独立于说话者的大词汇量和连续语音识别系统。 在 2000 年代初期,领先的初创公司 Nuance 和 SpeechWorks 提供的语音识别引擎为许多第一代基于 Web 的语音服务提供了支持,例如 TellMe,Phone 的 AOL 和 BeVocal。 当时构建的语音识别系统主要基于传统的隐马尔可夫模型(HMM),并且需要手动编写语法和安静环境以帮助识别引擎更准确地工作。
现代语音识别引擎几乎可以理解嘈杂环境下人们的任何说话,并且基于端到端深度学习,尤其是另一种更适合自然语言处理的深度神经网络,称为循环神经网络(RNN)。 与传统的基于 HMM 的语音识别不同,传统的基于 HMM 的语音识别需要人的专业知识来构建和微调手工设计的特征以及声学和语言模型,而基于 RNN 的端到端语音识别系统则将音频输入直接转换为文本,而无需将音频输入转换为语音表示以进行进一步处理。
RNN 允许我们处理输入和/或输出的序列,因为根据设计,网络可以存储输入序列中的先前项目或可以生成输出序列。 这使 RNN 更适用于语音识别(输入是用户说出的单词序列),图像标题(输出是由一系列单词组成的自然语言句子),文本生成和时间序列预测 。 如果您不熟悉 RNN,则一定要查看 Andrey Karpathy 的博客,循环神经网络的不合理有效性。 在本书的后面,我们还将介绍一些详细的 RNN 模型。
关于 RNN 端到端语音识别的第一篇研究论文发表于 2014 年,使用的是连接主义的时间分类(CTC)层。 2014 年下半年,百度发布了 Deep Speech,这是第一个使用基于 CTC 的端到端 RNN 构建但拥有庞大数据集的商业系统之一 ,并在嘈杂的环境中实现了比传统 ASR 系统更低的错误率。 如果您有兴趣,可以查看深度语音的 TensorFlow 实现,但是由于此类基于 CTC 的系统存在问题,生成的模型需要太多的资源才能在手机上运行。 在部署期间,它需要一个大型语言模型来纠正部分由 RNN 的性质引起的生成的文本错误(如果您想知道为什么,请阅读前面链接的 RNN 博客以获取一些见识)。
在 2015 年和 2016 年,较新的语音识别系统使用了类似的端到端 RNN 方法,但将 CTC 层替换为基于注意力的模型,因此运行模型时不需要大型语言模型,因此可以在内存有限的移动设备上进行部署。 在本书的此版本中,我们将不会探讨这种可能性,而将介绍如何在移动应用中使用最新的高级 ASR 模型。 相反,我们将从一个更简单的语音识别模型开始,我们知道该模型肯定会在移动设备上很好地工作。
要将离线语音识别功能添加到移动应用,您还可以使用以下两个领先的开源语音识别项目之一:
- CMU Sphinx 大约 20 年前开始,但仍在积极开发中。 要构建具有语音识别功能的 Android 应用,您可以使用其为 Android 构建的 PocketSphinx。 要构建具有语音识别功能的 iOS 应用,您可以使用 OpenEars 框架,这是一个免费的 SDK,在 iOS 应用中使用 CMU PocketSphinx 构建离线语音识别和文本转换。
- Kaldi,成立于 2009 年,最近非常活跃,截至 2018 年 1 月,已有 165 个参与者。要在 Android 上进行尝试,您可以查看此博客文章。 对于 iOS,请查看在 iOS 上使用 Kaldi 的原型。
由于这是一本关于在移动设备上使用 TensorFlow 的书,因此 TensorFlow 可用于为图像处理,语音处理和文本处理以及其他智能任务(本章其余部分的)构建强大的模型。 我们将重点介绍如何使用 TensorFlow 训练简单的语音识别模型并将其在移动应用中使用。
训练简单的命令识别模型
在本节中,我们将总结编写良好的 TensorFlow 简单音频识别教程中使用的步骤。 一些在训练模型时可能对您有帮助的提示。
我们将建立的简单语音命令识别模型将能够识别 10 个单词:"yes", "no", "up", "down", "left", "right", "on", "off", "stop", "go"; 它也可以检测沉默。 如果没有发现沉默,并且没有发现 10 个单词,它将生成“未知”。 稍后运行tensorflow/example/speech_commands/train.py脚本时,我们将下载语音命令数据集并用于训练模型,实际上除了这 10 个单词外,还包含 20 个单词:"zero", "two", "three", ..., "ten"(到目前为止,您已经看到的 20 个词称为核心词)和 10 个辅助词:"bed", "bird", "cat", "dog", "happy", "house", "marvin", "sheila", "tree", "wow"。 核心词比辅助词(约 1750)具有更多的.wav文件记录(约 2350)。
语音命令数据集是从开放语音记录站点收集的。您应该尝试一下,也许自己花些时间来录制自己的录音,以帮助改善录音效果,并在需要时了解如何收集自己的语音命令数据集。 关于使用数据集构建模型,还有一个 Kaggle 竞赛,您可以在此处了解有关语音模型和提示的更多信息。
在移动应用中要训练和使用的模型基于纸质卷积神经网络,用于小大小关键词发现,这与大多数其他基于 RNN 的大规模语音识别模型不同。 基于 CNN 的语音识别模型是可能的,但很有趣,因为对于简单的语音命令识别,我们可以在短时间内将音频信号转换为图像,或更准确地说,将频谱图转换为频率窗口期间音频信号的分布(有关使用wav_to_spectrogram脚本生成的示例频谱图图像,请参见本节开头的 TensorFlow 教程链接)。 换句话说,我们可以将音频信号从其原始时域表示转换为频域表示。 进行此转换的最佳算法是离散傅立叶变换(DFT),快速傅立叶变换(FFT)只是一种有效的选择 DFT 实现的算法。
作为移动开发人员,您可能不需要了解 DFT 和 FFT。 但是,您最好了解所有这些模型训练在移动应用中使用时是如何工作的,因为我们知道我们将要介绍的 TensorFlow 简单语音命令模型训练的幕后花絮,这是 FFT 的使用,前十大模型之一。当然,除其他事项外,20 世纪的算法使基于 CNN 的语音命令识别模型训练成为可能。 有关 DFT 的有趣且直观的教程,您可以阅读以下文章。
现在,让我们执行以下步骤来训练简单语音命令识别模型:
- 在终端上,
cd到您的 TensorFlow 源根,可能是~/tensorflow-1.4.0。 - 只需运行以下命令即可下载我们之前讨论的语音命令数据集:
python tensorflow/examples/speech_commands/train.py
您可以使用许多参数:--wanted_words默认为以yes开头的 10 个核心词; 您可以使用此参数添加更多可以被模型识别的单词。 要训练自己的语音命令数据集,请使用--data_url --data_dir=<path_to_your_dataset>禁用语音命令数据集的下载并访问您自己的数据集,其中每个命令应命名为自己的文件夹,其中应包含 1000-2000 个音频剪辑,大约需要 1 秒钟的长度; 如果音频片段更长,则可以相应地更改--clip_duration_ms参数值。 有关更多详细信息,请参见train.py源代码和 TensorFlow 简单音频识别教程。
- 如果您接受
train.py的所有默认参数,则在下载 1.48GB 语音命令数据集之后,在 GTX-1070 GPU 驱动的 Ubuntu 上,完成 18,000 个步骤的整个训练大约需要 90 分钟。 训练完成后,您应该在/tmp/speech_commands_train文件夹内看到检查点文件的列表,以及conv.pbtxt图定义文件和名为conv_labels.txt的标签文件,其中包含命令列表(与命令列表相同)。--wanted_words参数是默认值或设置为,在文件的开头加上两个附加词_silence和_unknown):
-rw-rw-r-- 1 jeff jeff 75437 Dec 9 21:08 conv.ckpt-18000.meta
-rw-rw-r-- 1 jeff jeff 433 Dec 9 21:08 checkpoint
-rw-rw-r-- 1 jeff jeff 3707448 Dec 9 21:08 conv.ckpt-18000.data-00000-of-00001
-rw-rw-r-- 1 jeff jeff 315 Dec 9 21:08 conv.ckpt-18000.index
-rw-rw-r-- 1 jeff jeff 75437 Dec 9 21:08 conv.ckpt-17900.meta
-rw-rw-r-- 1 jeff jeff 3707448 Dec 9 21:08 conv.ckpt-17900.data-00000-of-00001
-rw-rw-r-- 1 jeff jeff 315 Dec 9 21:08 conv.ckpt-17900.index
-rw-rw-r-- 1 jeff jeff 75437 Dec 9 21:07 conv.ckpt-17800.meta
-rw-rw-r-- 1 jeff jeff 3707448 Dec 9 21:07 conv.ckpt-17800.data-00000-of-00001
-rw-rw-r-- 1 jeff jeff 315 Dec 9 21:07 conv.ckpt-17800.index
-rw-rw-r-- 1 jeff jeff 75437 Dec 9 21:07 conv.ckpt-17700.meta
-rw-rw-r-- 1 jeff jeff 3707448 Dec 9 21:07 conv.ckpt-17700.data-00000-of-00001
-rw-rw-r-- 1 jeff jeff 315 Dec 9 21:07 conv.ckpt-17700.index
-rw-rw-r-- 1 jeff jeff 75437 Dec 9 21:06 conv.ckpt-17600.meta
-rw-rw-r-- 1 jeff jeff 3707448 Dec 9 21:06 conv.ckpt-17600.data-00000-of-00001
-rw-rw-r-- 1 jeff jeff 315 Dec 9 21:06 conv.ckpt-17600.index
-rw-rw-r-- 1 jeff jeff 60 Dec 9 19:41 conv_labels.txt
-rw-rw-r-- 1 jeff jeff 121649 Dec 9 19:41 conv.pbtxt
conv_labels.txt包含以下命令:
_silence_
_unknown_
yes
no
up
down
left
right
on
off
stop
go
现在运行以下命令,将图定义文件和检查点文件组合成一个我们可以在移动应用中使用的模型文件:
python tensorflow/examples/speech_commands/freeze.py \
--start_checkpoint=/tmp/speech_commands_train/conv.ckpt-18000 \
--output_file=/tmp/speech_commands_graph.pb
- (可选)在移动应用中部署
speech_commands_graph.pb模型文件之前,可以使用以下命令对其进行快速测试:
python tensorflow/examples/speech_commands/label_wav.py \
--graph=/tmp/speech_commands_graph.pb \
--labels=/tmp/speech_commands_train/conv_labels.txt \
--wav=/tmp/speech_dataset/go/9d171fee_nohash_1.wav
您将看到类似以下的输出:
go (score = 0.48427)
no (score = 0.17657)
_unknown_ (score = 0.08560)
- 使用
summarize_graph工具查找输入节点和输出节点的名称:
bazel-bin/tensorflow/tools/graph_transforms/summarize_graph --in_graph=/tmp/speech_commands_graph.pb
输出应如下所示:
Found 1 possible inputs: (name=wav_data, type=string(7), shape=[])
No variables spotted.
Found 1 possible outputs: (name=labels_softmax, op=Softmax)
不幸的是,它仅对于输出名称是正确的,并且不显示其他可能的输入。 使用tensorboard --logdir /tmp/retrain_logs,然后在浏览器中打开http://localhost:6006与图进行交互也无济于事。 但是,前面各章中显示的小代码段可以帮助您了解输入和输出名称,以下内容与 iPython 进行了交互:
In [1]: import tensorflow as tf
In [2]: g=tf.GraphDef()
In [3]: g.ParseFromString(open("/tmp/speech_commands_graph.pb","rb").read())
In [4]: x=[n.name for n in g.node]
In [5]: x
Out[5]:
[u'wav_data',
u'decoded_sample_data',
u'AudioSpectrogram',
...
u'MatMul',
u'add_2',
u'labels_softmax']
因此,我们看到wav_data和decoded_sample_data都是可能的输入。 如果在freeze.py文件中看不到注释,我们就必须深入研究模型训练代码,以准确找出应该使用的输入名称:“结果图包含一个名为 WAV 的编码数据输入 wav_data,用于原始 PCM 数据(在 -1.0 到 1.0 范围内浮动)的一种称为decoded_sample_data,输出称为labels_softmax。” 实际上,在该模型的情况下,有一个 TensorFlow Android 示例应用,这是我们在第 1 章,“移动 TensorFlow 入门”中看到的一部分,称为 TF 语音,专门定义了那些输入名称和输出名称。 在本书后面的几章中,您将看到如何在需要时借助或不借助我们的三种方法来查找模型训练的源代码,以找出关键的输入和输出节点名称。 或者希望,当您阅读本书时,TensorFlow summarize_graph工具将得到改进,以为我们提供准确的输入和输出节点名称。
现在是时候在移动应用中使用我们的热门新模型了。
在 Android 中使用简单的语音识别模型
位于tensorflow/example/android的用于简单语音命令识别的 TensorFlow Android 示例应用具有在SpeechActivity.java文件中进行音频记录和识别的代码,假定该应用需要始终准备好接受新的音频命令。 尽管在某些情况下这确实是合理的,但它导致的代码比仅在用户按下按钮后才进行记录和识别的代码要复杂得多,例如 Apple 的 Siri 的工作方式。 在本部分中,我们将向您展示如何创建新的 Android 应用并添加尽可能少的代码来记录用户的语音命令并显示识别结果。 这应该可以帮助您更轻松地将模型集成到自己的 Android 应用中。 但是,如果您需要处理语音命令应始终自动记录和识别的情况,则应查看 TensorFlow 示例 Android 应用。
使用模型构建新应用
执行以下步骤来构建一个完整的新 Android 应用,该应用使用我们在上一节中构建的speech_commands_graph.pb模型:
- 通过接受前面几章中的所有默认设置,创建一个名为
AudioRecognition的新 Android 应用,然后将compile 'org.tensorflow:tensorflow-android:+'行添加到应用build.gradle文件依赖项的末尾。 - 将
<uses-permission android:name="android.permission.RECORD_AUDIO" />添加到应用的AndroidManifest.xml文件中,以便可以允许该应用记录音频。 - 创建一个新的资产文件夹,然后将在上一节的步骤 2 和 3 中生成的
speech_commands_graph.pb和conv_actions_labels.txt文件拖放到assets文件夹中。 - 更改
activity_main.xml文件以容纳三个 UI 元素。 第一个是用于识别结果显示的TextView:
<TextView
android:id="@+id/textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
第二个TextView将显示上一节第 2 步中使用train.py Python 程序训练的 10 个默认命令:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="yes no up down left right on off stop go"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0.50"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.25" />
最后一个 UI 元素是一个按钮,在点击该按钮时,它会开始录音一秒钟,然后将录音发送到我们的模型以进行识别:
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Start"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0.50"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.8" />
- 打开
MainActivity.java,首先创建MainActivityimplements Runnable类。 然后添加以下常量,以定义模型名称,标签名称,输入名称和输出名称:
private static final String MODEL_FILENAME = "file:///android_asset/speech_commands_graph.pb";
private static final String LABEL_FILENAME = "file:///android_asset/conv_actions_labels.txt";
private static final String INPUT_DATA_NAME = "decoded_sample_data:0";
private static final String INPUT_SAMPLE_RATE_NAME = "decoded_sample_data:1";
private static final String OUTPUT_NODE_NAME = "labels_softmax";
- 声明四个实例变量:
private TensorFlowInferenceInterface mInferenceInterface;
private List<String> mLabels = new ArrayList<String>();
private Button mButton;
private TextView mTextView;
- 在
onCreate方法中,我们首先实例化mButton和mTextView,然后设置按钮单击事件处理器,该事件处理器首先更改按钮标题,然后启动线程进行记录和识别:
mButton = findViewById(R.id.button);
mTextView = findViewById(R.id.textview);
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mButton.setText("Listening...");
Thread thread = new Thread(MainActivity.this);
thread.start();
}
});
在onCreate方法的末尾,我们逐行读取标签文件的内容,并将每一行保存在mLabels数组列表中。
- 在
public void run()方法的开头(单击“开始”按钮时开始),添加代码,该代码首先获得用于创建 AndroidAudioRecord对象的最小缓冲区大小,然后使用buffersize创建新的AudioRecord实例具有 16,000SAMPLE_RATE和 16 位单声道格式,这是我们模型所期望的原始音频的类型,并最终从AudioRecord实例开始记录:
int bufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
AudioRecord record = new AudioRecord(MediaRecorder.AudioSource.DEFAULT, SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize);
if (record.getState() != AudioRecord.STATE_INITIALIZED) return;
record.startRecording();
Android 中有两个用于记录音频的类:MediaRecorder和AudioRecord。 MediaRecorder比AudioRecord更易于使用,但是它会保存压缩的音频文件,直到 Android API Level 24(Android 7.0)为止,该 API 支持录制未经处理的原始音频。 根据这里,截至 2018 年 1 月,市场上有 70% 以上的 Android 设备仍在运行 7.0 或更早的 Android 版本。 您可能不希望将应用定位到 Android 7.0 或更高版本。 另外,要解码由MediaRecorder录制的压缩音频,您必须使用MediaCodec,使用起来非常复杂。 AudioRecord尽管是一个低级的 API,但实际上非常适合记录未处理的原始数据,然后将其发送到语音命令识别模型进行处理。
- 创建两个由 16 位短整数组成的数组
audioBuffer和recordingBuffer,对于 1 秒记录,每次AudioRecord对象读取并填充audioBuffer数组后,实际读取的数据都会附加到recordingBuffer:
long shortsRead = 0;
int recordingOffset = 0;
short[] audioBuffer = new short[bufferSize / 2];
short[] recordingBuffer = new short[RECORDING_LENGTH];
while (shortsRead < RECORDING_LENGTH) { // 1 second of recording
int numberOfShort = record.read(audioBuffer, 0, audioBuffer.length);
shortsRead += numberOfShort;
System.arraycopy(audioBuffer, 0, recordingBuffer, recordingOffset, numberOfShort);
recordingOffset += numberOfShort;
}
record.stop();
record.release();
- 录制完成后,我们首先将按钮标题更改为
Recognizing:
runOnUiThread(new Runnable() {
@Override
public void run() {
mButton.setText("Recognizing...");
}
});
然后将recordingBuffer短数组转换为float数组,同时使float数组的每个元素都在 -1.0 和 1.0 的范围内,因为我们的模型期望在-之间浮动 1.0 和 1.0:
float[] floatInputBuffer = new float[RECORDING_LENGTH];
for (int i = 0; i < RECORDING_LENGTH; ++i) {
floatInputBuffer[i] = recordingBuffer[i] / 32767.0f;
}
- 如前几章所述,创建一个新的
TensorFlowInferenceInterface,然后使用两个输入节点的名称和值调用其feed方法,其中一个是采样率,另一个是存储在floatInputBuffer中的原始音频数据 ]数组:
AssetManager assetManager = getAssets();
mInferenceInterface = new TensorFlowInferenceInterface(assetManager, MODEL_FILENAME);
int[] sampleRate = new int[] {SAMPLE_RATE};
mInferenceInterface.feed(INPUT_SAMPLE_RATE_NAME, sampleRate);
mInferenceInterface.feed(INPUT_DATA_NAME, floatInputBuffer, RECORDING_LENGTH, 1);
之后,我们调用run方法在模型上运行识别推理,然后fetch输出 10 个语音命令中每个命令的输出分数以及“未知”和“沉默”输出:
String[] outputScoresNames = new String[] {OUTPUT_NODE_NAME};
mInferenceInterface.run(outputScoresNames);
float[] outputScores = new float[mLabels.size()];
mInferenceInterface.fetch(OUTPUT_NODE_NAME, outputScores);
outputScores数组与mLabels列表匹配,因此我们可以轻松找到最高得分并获取其命令名称:
float max = outputScores[0];
int idx = 0;
for (int i=1; i<outputScores.length; i++) {
if (outputScores[i] > max) {
max = outputScores[i];
idx = i;
}
}
final String result = mLabels.get(idx);
最后,我们在TextView中显示结果,并将按钮标题更改回"Start",以便用户可以再次开始记录和识别语音命令:
runOnUiThread(new Runnable() {
@Override
public void run() {
mButton.setText("Start");
mTextView.setText(result);
}
});
显示模型驱动的识别结果
现在,在您的 Android 设备上运行该应用。 您将看到如图 5.1 所示的初始屏幕:

图 5.1:应用启动后显示初始屏幕
点击START按钮,然后开始说上面显示的 10 个命令之一。 您将看到按钮标题更改为“监听...”,然后是“识别...”,如图 5.2 所示:

图 5.2:监听录制的音频并识别录制的音频
识别结果几乎实时显示在屏幕中间,如图 5.3 所示:

图 5.3:显示识别的语音命令
整个识别过程几乎立即完成,用于识别的speech_commands_graph.pb模型仅为 3.7MB。 当然,它仅支持 10 条语音命令,但是即使使用train.py脚本的 --wanted_words参数或您自己的数据集支持数十个命令,大小也不会发生太大变化,正如我们在训练部分中讨论的那样。
诚然,此处的应用屏幕截图并不像上一章中那样生动有趣(一张图片价值一千个单词),但是语音识别当然可以做艺术家不能做的事情,例如发出语音命令来控制机器人的运动。
该应用的完整源代码位于 Github 上该书的源代码存储库的Ch5/android文件夹中。 现在让我们看看如何使用该模型构建 iOS 应用,其中涉及一些复杂的 TensorFlow iOS 库构建和音频数据准备步骤,以使模型正确运行。
通过 Objective-C 在 iOS 中使用简单的语音识别模型
如果您已经阅读了前三章中的 iOS 应用,那么您可能更喜欢使用手动构建的 TensorFlow iOS 库而不是 TensorFlow 实验窗格,就像使用手动库方法一样,您可以更好地控制可以添加哪些 TensorFlow 操作来使您的模型满意,这也是我们决定专注于 TensorFlow Mobile 而不是第 1 章,“移动 TensorFlow”的 TensorFlow Lite 的原因之一。
因此,尽管您可以在阅读本书时尝试使用 TensorFlow Pod,以查看 Pod 是否已更新以支持模型中使用的所有操作,但从现在开始,我们将始终使用手动构建的 TensorFlow 库( 请参见 iOS 应用中第 3 章,“检测对象及其位置”的“在 iOS 中使用对象检测模型的”部分的步骤 1 和 2)。
使用模型构建新应用
现在执行以下步骤来创建一个新的 iOS 应用以使用语音命令识别模型:
-
在 Xcode 中创建一个名为 AudioRecognition 的新 Objective-C 应用,并将项目设置为使用 TensorFlow 手动构建的库,如“以惊人的艺术样式迁移图片”的步骤 1 中所述。 还将
AudioToolbox.framework,AVFoundation.framework和Accelerate.framework添加到目标的带库的链接二进制文件。 -
将
speech_commands_graph.pb模型文件拖放到项目中。 -
将
ViewController.m的扩展名更改为mm,然后添加音频记录和处理所使用的以下标头:
#import <AVFoundation/AVAudioRecorder.h>
#import <AVFoundation/AVAudioSettings.h>
#import <AVFoundation/AVAudioSession.h>
#import <AudioToolbox/AudioToolbox.h>
还添加 TensorFlow 的标头:
#include <fstream>
#include "tensorflow/core/framework/op_kernel.h"
#include "tensorflow/core/framework/tensor.h"
#include "tensorflow/core/public/session.h"
现在,定义一个音频SAMPLE_RATE常量,一个指向浮点数组的 C 指针,该数组保存将要发送到模型的音频数据,我们的关键audioRecognition函数签名以及两个属性,其中包含记录的文件路径和一个 iOS AVAudioRecorder实例。 我们还需要让ViewController实现AudioRecorderDelegate,以便它知道录制何时结束:
const int SAMPLE_RATE = 16000;
float *floatInputBuffer;
std::string audioRecognition(float* floatInputBuffer, int length);
@interface ViewController () <AVAudioRecorderDelegate>
@property (nonatomic, strong) NSString *recorderFilePath;
@property (nonatomic, strong) AVAudioRecorder *recorder;
@end
在此,我们不会显示以编程方式创建两个 UI 元素的代码段:一个按钮,当您点击该按钮时,它将开始录制 1 秒钟的音频,然后将音频发送到我们的模型以进行识别,以及一个显示识别结果的标签。 但是,我们将在下一部分中的 Swift 中展示一些 UI 代码以供复习。
- 在按钮的
UIControlEventTouchUpInside处理器内,我们首先创建一个AVAudioSession实例,并将其类别设置为记录并将其激活:
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
NSError *err = nil;
[audioSession setCategory:AVAudioSessionCategoryPlayAndRecord error:&err];
if(err){
NSLog(@"audioSession: %@", [[err userInfo] description]);
return;
}
[audioSession setActive:YES error:&err];
if(err){
NSLog(@"audioSession: %@", [[err userInfo] description]);
return;
}
然后创建一个记录设置字典:
NSMutableDictionary *recordSetting = [[NSMutableDictionary alloc] init];
[recordSetting setValue:[NSNumber numberWithInt:kAudioFormatLinearPCM] forKey:AVFormatIDKey];
[recordSetting setValue:[NSNumber numberWithFloat:SAMPLE_RATE] forKey:AVSampleRateKey];
[recordSetting setValue:[NSNumber numberWithInt: 1] forKey:AVNumberOfChannelsKey];
[recordSetting setValue :[NSNumber numberWithInt:16] forKey:AVLinearPCMBitDepthKey];
[recordSetting setValue :[NSNumber numberWithBool:NO] forKey:AVLinearPCMIsBigEndianKey];
[recordSetting setValue :[NSNumber numberWithBool:NO] forKey:AVLinearPCMIsFloatKey];
[recordSetting setValue:[NSNumber numberWithInt:AVAudioQualityMax] forKey:AVEncoderAudioQualityKey];
最后,在按钮点击处理器中,我们定义保存录制的音频的位置,创建AVAudioRecorder实例,设置其委托并开始录制 1 秒钟:
self.recorderFilePath = [NSString stringWithFormat:@"%@/recorded_file.wav", [NSHomeDirectory() stringByAppendingPathComponent:@"tmp"]];
NSURL *url = [NSURL fileURLWithPath:_recorderFilePath];
err = nil;
_recorder = [[ AVAudioRecorder alloc] initWithURL:url settings:recordSetting error:&err];
if(!_recorder){
NSLog(@"recorder: %@", [[err userInfo] description]);
return;
}
[_recorder setDelegate:self];
[_recorder prepareToRecord];
[_recorder recordForDuration:1];
- 在
AVAudioRecorderDelegate,audioRecorderDidFinishRecording的委托方法中,我们使用 Apple 的扩展音频文件服务,该服务用于读写压缩和线性 PCM 音频文件,以加载记录的音频,并将其转换为模型所需的格式, 并将音频数据读入存储器。 我们在这里不会显示这部分代码,它主要基于此博客。 在此处理之后,floatInputBuffer指向原始音频样本。 现在,我们可以将数据传递到工作线程中的audioRecognition方法中,并在 UI 线程中显示结果:
dispatch_async(dispatch_get_global_queue(0, 0), ^{
std::string command = audioRecognition(floatInputBuffer, totalRead);
delete [] floatInputBuffer;
dispatch_async(dispatch_get_main_queue(), ^{
NSString *cmd = [NSString stringWithCString:command.c_str() encoding:[NSString defaultCStringEncoding]];
[_lbl setText:cmd];
[_btn setTitle:@"Start" forState:UIControlStateNormal];
});
});
- 在
audioRecognition方法内部,我们首先定义一个 C++string数组,其中包含要识别的 10 个命令以及两个特殊值"_silence_"和"_unknown_":
std::string commands[] = {"_silence_", "_unknown_", "yes", "no", "up", "down", "left", "right", "on", "off", "stop", "go"};
在完成标准 TensorFlow Session,Status和GraphDef设置后(如我们在前几章的 iOS 应用中所做的那样),我们读出了模型文件,并尝试使用它创建 TensorFlow Session:
NSString* network_path = FilePathForResourceName(@"speech_commands_graph", @"pb");
PortableReadFileToProto([network_path UTF8String], &tensorflow_graph);
tensorflow::Status s = session->Create(tensorflow_graph);
if (!s.ok()) {
LOG(ERROR) << "Could not create TensorFlow Graph: " << s;
return "";
}
如果成功创建了会话,则为模型定义两个输入节点名称和一个输出节点名称:
std::string input_name1 = "decoded_sample_data:0";
std::string input_name2 = "decoded_sample_data:1";
std::string output_name = "labels_softmax";
- 对于
"decoded_sample_data:0",我们需要将采样率值作为标量发送(否则在调用 TensorFlowSession的run方法时会出错),并且在 TensorFlow C++ API 中定义了张量,如下所示:
tensorflow::Tensor samplerate_tensor(tensorflow::DT_INT32, tensorflow::TensorShape());
samplerate_tensor.scalar<int>()() = SAMPLE_RATE;
对于 "decoded_sample_data:1",需要将浮点数中的音频数据从floatInputBuffer数组转换为 TensorFlow audio_tensor张量,其方式类似于前几章的image_tensor的定义和设置方式:
tensorflow::Tensor audio_tensor(tensorflow::DT_FLOAT, tensorflow::TensorShape({length, 1}));
auto audio_tensor_mapped = audio_tensor.tensor<float, 2>();
float* out = audio_tensor_mapped.data();
for (int i = 0; i < length; i++) {
out[i] = floatInputBuffer[i];
}
现在我们可以像以前一样使用输入来运行模型并获取输出:
std::vector<tensorflow::Tensor> outputScores;
tensorflow::Status run_status = session->Run({{input_name1, audio_tensor}, {input_name2, samplerate_tensor}},{output_name}, {}, &outputScores);
if (!run_status.ok()) {
LOG(ERROR) << "Running model failed: " << run_status;
return "";
}
- 我们对模型的
outputScores输出进行简单的解析,然后返回最高分。outputScores是 TensorFlow 张量的向量,其第一个元素包含 12 个可能的识别结果的 12 个得分值。 可以通过flat方法访问这 12 个得分值,并检查最大得分:
tensorflow::Tensor* output = &outputScores[0];
const Eigen::TensorMap<Eigen::Tensor<float, 1, Eigen::RowMajor>, Eigen::Aligned>& prediction = output->flat<float>();
const long count = prediction.size();
int idx = 0;
float max = prediction(0);
for (int i = 1; i < count; i++) {
const float value = prediction(i);
printf("%d: %f", i, value);
if (value > max) {
max = value;
idx = i;
}
}
return commands[idx];
在应用可以录制任何音频之前,您需要做的另一件事是在应用的Info.plist文件中创建一个新的隐私-麦克风使用说明属性,并将该属性的值设置为诸如“听到并识别” 您的语音命令”。
现在,在 iOS 模拟器上运行该应用(如果您的 Xcode 版本早于 9.2,而 iOS 模拟器版本早于 10.0,则您可能必须在实际的 iOS 设备上运行该应用,因为您可能无法在 iOS 或 iPhone 模拟器(10.0 之前的版本)中录制音频,您将首先看到带有 Start 按钮位于中间的初始屏幕,然后点击该按钮并说出 10 个命令之一,识别结果应出现在顶部 ,如图 5.4 所示:

图 5.4:显示初始画面和识别结果
是的,应该会出现识别结果,但实际上不会出现,因为在 Xcode 输出窗格中会出现错误:
Could not create TensorFlow Graph: Not found: Op type not registered 'DecodeWav' in binary running on XXX's-MacBook-Pro.local. Make sure the Op and Kernel are registered in the binary running in this process.
使用tf_op_files.txt修复模型加载错误
我们已经在前面的章节中看到了这种臭名昭著的错误,除非您知道它的真正含义,否则弄清楚该修复程序可能要花很多时间。 TensorFlow 操作由两部分组成:位于 tensorflow/core/ops文件夹中的称为ops的定义(这有点令人困惑,因为操作既可以表示其定义,其实现,也可以表示其定义)。 和位于 tensorflow/core/kernels文件夹中的实现(称为内核)。 tensorflow/contrib/makefile文件夹中有一个名为tf_op_files.txt的文件,其中列出了在手动构建库时需要内置到 TensorFlow iOS 库中的操作的定义和实现。 tf_op_files.txt文件应该包含所有操作定义文件,如为 TensorFlow 移动部署准备模型,因为它们占用的空间很小。 但从 TensorFlow 1.4 或 1.5 开始,tf_op_files.txt文件中并未包含所有操作的操作定义。 因此,当我们看到“未注册操作类型”错误时,我们需要找出哪个操作定义和实现文件负责该操作。 在我们的情况下,操作类型名为DecodeWav。 我们可以运行以下两个 Shell 命令来获取信息:
$ grep 'REGISTER.*"DecodeWav"' tensorflow/core/ops/*.cc
tensorflow/core/ops/audio_ops.cc:REGISTER_OP("DecodeWav")
$ grep 'REGISTER.*"DecodeWav"' tensorflow/core/kernels/*.cc
tensorflow/core/kernels/decode_wav_op.cc:REGISTER_KERNEL_BUILDER(Name("DecodeWav").Device(DEVICE_CPU), DecodeWavOp);
在 TensorFlow 1.4 的 tf_op_files.txt文件中,已经有一行文本tensorflow/core/kernels/decode_wav_op.cc,但可以肯定的是tensorflow/core/ops/audio_ops.cc丢失了。 我们需要做的就是在tf_op_files.txt文件中的任意位置添加一行tensorflow/core/ops/audio_ops.cc,并像在第 3 章,“检测对象及其位置”中一样运行tensorflow/contrib/makefile/build_all_ios.sh,以重建 TensorFlow iOS 库。 然后再次运行 iOS 应用,并继续轻按启动按钮,然后说出语音命令以识别或误解,直到您无聊为止。
本章将重点介绍如何解决Not found: Op type not registered错误的过程,因为将来在其他 TensorFlow 模型上工作时,可以节省大量时间。
但是,在继续学习下一章中将介绍和使用另一种新的 TensorFlow AI 模型之前,让我们给其他喜欢使用更新的且至少对他们更凉快的 Swift 语言的 iOS 开发人员一些考虑。
通过 Swift 在 iOS 中使用简单的语音识别模型
我们在第 2 章中使用 TensorFlow 窗格创建了一个基于 Swift 的 iOS 应用。 现在让我们创建一个新的 Swift 应用,该应用使用我们在上一节中手动构建的 TensorFlow iOS 库,并在我们的 Swift 应用中使用语音命令模型:
- 通过 Xcode 创建一个新的“Single View iOS”项目,并按照与上一节中的步骤 1 和 2 相同的方式设置该项目,除了将语言设置为 Swift。
- 选择 Xcode “文件 | 新增 | 文件 ...”,然后选择 Objective-C 文件。 输入名称
RunInference。 您将看到一个消息框,询问您“您是否要配置一个 Objective-C 桥接头?” 单击创建桥接标题。 将文件RunInference.m重命名为RunInfence.mm,因为我们将混合使用 C,C++ 和 Objective-C 代码来进行后期录音音频处理和识别。 我们仍在 Swift 应用中使用 Objective-C,因为要从 Swift 调用 TensorFlow C++ 代码,我们需要一个 Objective-C 类作为 C++ 代码的包装。 - 创建一个名为
RunInference.h的头文件,并添加以下代码:
@interface RunInference_Wrapper : NSObject
- (NSString *)run_inference_wrapper:(NSString*)recorderFilePath;
@end
现在,您在 Xcode 中的应用应类似于图 5.5:

图 5.5:基于 Swift 的 iOS 应用项目
- 打开
ViewController.swift。 在import UIKit之后的顶部添加以下代码:
import AVFoundation
let _lbl = UILabel()
let _btn = UIButton(type: .system)
var _recorderFilePath: String!
然后使ViewController看起来像这样(未显示为_btn和_lbl定义NSLayoutConstraint并调用addConstraint的代码段):
class ViewController: UIViewController, AVAudioRecorderDelegate {
var audioRecorder: AVAudioRecorder!
override func viewDidLoad() {
super.viewDidLoad()
_btn.translatesAutoresizingMaskIntoConstraints = false
_btn.titleLabel?.font = UIFont.systemFont(ofSize:32)
_btn.setTitle("Start", for: .normal)
self.view.addSubview(_btn)
_btn.addTarget(self, action:#selector(btnTapped), for: .touchUpInside)
_lbl.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(_lbl)
- 添加一个按钮点击处理器,并在其内部,首先请求用户的录制许可:
@objc func btnTapped() {
_lbl.text = "..."
_btn.setTitle("Listening...", for: .normal)
AVAudioSession.sharedInstance().requestRecordPermission () {
[unowned self] allowed in
if allowed {
print("mic allowed")
} else {
print("denied by user")
return
}
}
然后创建一个AudioSession实例,并将其类别设置为记录,并将状态设置为活动,就像我们在 Objective-C 版本中所做的一样:
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setCategory(AVAudioSessionCategoryRecord)
try audioSession.setActive(true)
} catch {
print("recording exception")
return
}
现在定义AVAudioRecorder要使用的设置:
let settings = [
AVFormatIDKey: Int(kAudioFormatLinearPCM),
AVSampleRateKey: 16000,
AVNumberOfChannelsKey: 1,
AVLinearPCMBitDepthKey: 16,
AVLinearPCMIsBigEndianKey: false,
AVLinearPCMIsFloatKey: false,
AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
] as [String : Any]
设置文件路径以保存录制的音频,创建AVAudioRecorder实例,设置其委托并开始录制 1 秒钟:
do {
_recorderFilePath = NSHomeDirectory().stringByAppendingPathComponent(path: "tmp").stringByAppendingPathComponent(path: "recorded_file.wav")
audioRecorder = try AVAudioRecorder(url: NSURL.fileURL(withPath: _recorderFilePath), settings: settings)
audioRecorder.delegate = self
audioRecorder.record(forDuration: 1)
} catch let error {
print("error:" + error.localizedDescription)
}
- 在
ViewController.swift的末尾,添加具有以下实现的AVAudioRecorderDelegate方法audioRecorderDidFinishRecording,该实现主要调用run_inference_wrapper进行音频后处理和识别:
func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool) {
_btn.setTitle("Recognizing...", for: .normal)
if flag {
let result = RunInference_Wrapper().run_inference_wrapper(_recorderFilePath)
_lbl.text = result
}
else {
_lbl.text = "Recording error"
}
_btn.setTitle("Start", for: .normal)
}
在AudioRecognition_Swift-Bridging-Header.h文件中,添加#include "RunInference.h",以便前面的 Swift 代码RunInference_Wrapper().run_inference_wrapper(_recorderFilePath)起作用。
- 在
run_inference_wrapper方法内的RunInference.mm中,从 Objective-CAudioRecognition应用中的ViewController.mm复制代码,如上一节的步骤 5-8 所述,该代码将保存的录制音频转换为格式 TensorFlow 模型接受模型,然后将其与采样率一起发送给模型以获取识别结果:
@implementation RunInference_Wrapper
- (NSString *)run_inference_wrapper:(NSString*)recorderFilePath {
...
}
如果您确实想将尽可能多的代码移植到 Swift,则可以用 Swift 替换 C 中的音频文件转换代码。 还有一些非官方的开源项目提供了官方 TensorFlow C++ API 的 Swift 包装器。 但是为了简单起见和达到适当的平衡,我们将保持 TensorFlow 模型的推论,在本示例中,还将保持音频文件的读取和转换,以及在 C++ 和 Objective-C 中与控制 UI 和录音,并启动调用来进行音频处理和识别。
这就是构建使用语音命令识别模型的 Swift iOS 应用所需的全部内容。 现在,您可以在 iOS 模拟器或实际设备上运行它,并看到与 Objective-C 版本完全相同的结果。
总结
在本章中,我们首先快速概述了语音识别以及如何使用端到端深度学习方法构建现代 ASR 系统。 然后,我们介绍了如何训练 TensorFlow 模型以识别简单的语音命令,并介绍了如何在 Android 应用以及基于 Objective-C 和 Swift 的 iOS 应用中使用该模型的分步教程。 我们还讨论了如何通过找出丢失的 TensorFlow 操作或内核文件,添加它并重建 TensorFlow iOS 库来修复 iOS 中常见的模型加载错误。
ASR 用于将语音转换为文本。 在下一章中,我们将探讨另一个将文本作为输出的模型,并且文本中将包含完整的自然语言句子,而不是本章中的简单命令。 我们将介绍如何构建模型以将图像,我们的老朋友转换为文本,以及如何在移动应用中使用该模型。 观察和描述您在自然语言中看到的内容需要真正的人类智慧。 福尔摩斯是完成这项任务的最佳人选之一。 我们当然还不如福尔摩斯,但是让我们看看如何开始。
六、用自然语言描述图像
如果图像分类和物体检测是明智的任务,那么用自然语言描述图像绝对是一项更具挑战性的任务,需要更多的智能-请片刻考虑一下每个人如何从新生儿成长(他们学会了识别物体并检测它们的位置)到三岁的孩子(他们学会讲述图片故事)。 用自然语言描述图像的任务的正式术语是图像标题。 与具有长期研究和发展历史的语音识别不同,图像字幕(具有完整的自然语言,而不仅仅是关键词输出)由于其复杂性和 2012 年的深度学习突破而仅经历了短暂而令人兴奋的研究历史。
在本章中,我们将首先回顾基于深度学习的图像字幕模型如何赢得 2015 年 Microsoft COCO(大规模对象检测,分割和字幕数据集),我们在第 3 章,“检测对象及其位置”中简要介绍了该有效模型。 然后,我们将总结在 TensorFlow 中训练模型的步骤,并详细介绍如何准备和优化要在移动设备上部署的复杂模型。 之后,我们将向您展示有关如何构建 iOS 和 Android 应用以使用该模型生成描述图像的自然语言语句的分步教程。 由于该模型同时涉及计算机视觉和自然语言处理,因此您将首次看到两种主要的深度神经网络架构 CNN 和 RNN 如何协同工作,以及如何编写 iOS 和 Android 代码以访问经过训练的网络并进行多个推理。 总而言之,我们将在本章介绍以下主题:
- 图像字幕 -- 工作原理
- 训练和冻结图像字幕模型
- 转换和优化图像字幕模型
- 在 iOS 中使用图像字幕模型
- 在 Android 中使用图像字幕模型
图像字幕 -- 工作原理
Show and Tell:从 2015 年 MSCOCO 图像字幕挑战赛中汲取的经验教训。 在讨论训练过程之前,TensorFlow 的 im2txt 模型文档网站中也对此进行了很好的介绍,让我们首先基本了解一下解模型的工作原理。 这也将帮助您了解 Python 中的训练和推理代码,以及本章稍后将介绍的 iOS 和 Android 中的推理代码。
获奖的 Show and Tell 模型是使用端到端方法进行训练的,类似于我们在上一章中简要介绍的最新的基于深度学习的语音识别模型。 它使用 MSCOCO 图像字幕 2014 数据集,可从这里下载,该数据集包含超过 82,000 个训练图像,并以描述它们的自然语言句子为目标。 训练模型以使为每个输入图像输出目标自然语言句子的可能性最大化。 与使用多个子系统的其他更复杂的训练方法不同,端到端方法优雅,简单,并且可以实现最新的结果。
为了处理和表示输入图像,Show and Tell 模型使用预训练的 Inception v3 模型,该模型与我们在第 2 章,“通过迁移学习对图像进行分类”所使用的相同。 Inception v3 CNN 网络的最后一个隐藏层用作输入图像的表示。 由于 CNN 模型的性质,较早的层捕获更多的基本图像信息,而较后的层捕获更高级的图像概念。 因此,通过使用输入图像的最后一个隐藏层来表示图像,我们可以更好地准备具有高级概念的自然语言输出。 毕竟,我们通常会开始用诸如“人”或“火车”之类的词来描述图片,而不是“带有尖锐边缘的东西”。
为了表示目标自然语言输出中的每个单词,使用了单词嵌入方法。 词嵌入只是词的向量表示。 TensorFlow 网站上有一个不错的教程,介绍如何构建模型来获取单词的向量表示。
现在,在既表示输入图像又表示输出单词的情况下(每个这样的单词对构成一个训练示例),给定的最佳训练模型可用于最大化在目标输出中生成每个单词w的概率,给定输入图像和该单词w之前的先前单词,它是 RNN 序列模型,或更具体地说,是长短期记忆(LSTM)的 RNN 模型类型。 LSTM 以解决常规 RNN 模型固有的消失和爆炸梯度问题而闻名。 为了更好地了解 LSTM,您应该查看这个热门博客。
梯度概念在反向传播过程中用于更新网络权重,因此它可以学习生成更好的输出。 如果您不熟悉反向传播过程,它是神经网络中最基本,功能最强大的算法之一,那么您绝对应该花些时间来理解它-只是 Google 的“反向传播”,排名前五的结果都不会令人失望。 消失的梯度意味着,在深度神经网络反向传播学习过程中,早期层中的网络权重几乎没有更新,因此网络永不收敛。 梯度爆炸意味着这些权重更新得过分疯狂,从而导致网络差异很大。 因此,如果某人头脑封闭,从不学习,或者某人对新事物疯狂而又失去兴趣就快,那么您就会知道他们似乎遇到了什么样的梯度问题。
训练后,可以将 CNN 和 LSTM 模型一起用于推理:给定输入图像,该模型可以估计每个单词的概率,从而预测最有可能为输出语句生成哪n个最佳单词; 然后,给定输入图像和n个最佳单词,可以生成n个最佳的下一个单词,然后继续进行,直到模型返回句子的特定结尾单词,或达到了生成的句子的指定单词长度(以防止模型过于冗长)时,我们得到一个完整的句子。
在每次生成单词时使用n个最佳单词(意味着在末尾具有n个最佳句子)被称为集束搜索。 当n(即集束大小)为 1 时,它仅基于模型返回的所有可能单词中的最高概率值,就成为贪婪搜索或最佳搜索。 TensorFlow im2txt 官方模型的下一部分中的训练和推理过程使用以 Python 实现的集束大小设置为 3 的集束搜索; 为了进行比较,我们将开发的 iOS 和 Android 应用使用更简单的贪婪或最佳搜索。 您将看到哪种方法可以生成更好的字幕。
训练和冻结图像字幕模型
在本部分中,我们将首先总结训练训练名为 im2txt 的 Show and Tell 模型的过程,该模型记录在这个页面中, 一些提示,以帮助您更好地了解该过程。 然后,我们将展示 im2txt 模型项目随附的 Python 代码的一些关键更改,以便冻结该模型以准备在移动设备上使用。
训练和测试字幕生成
如果您已按照第 3 章“检测对象及其位置”中的“设置 TensorFlow 对象检测 API”部分进行操作,那么您已经安装im2txt文件夹; 否则,只需将cd移至您的 TensorFlow 源根目录,然后运行:
git clone https://github.com/tensorflow/models
您可能尚未安装的一个 Python 库是 自然语言工具包(NLTK),这是最流行的用于自然语言处理的 Python 库之一。 只需访问其网站以获得安装说明。
现在,请按照以下步骤来训练模型:
- 通过打开终端并运行以下命令来设置保存 2014 MSCOCO 图像字幕训练和验证数据集的位置:
MSCOCO_DIR="${HOME}/im2txt/data/mscoco"
请注意,尽管 2014 年要下载和保存的原始数据集约为 20GB,但该数据集将转换为 TFRecord 格式(我们还在第 3 章 “检测对象及其位置”来转换对象检测数据集,这是运行以下训练脚本所需的,并添加了大约 100GB 数据。 因此,使用 TensorFlow im2txt 项目总共需要约 140GB 的训练自己的图像字幕模型。
- 转到您的 im2txt 源代码所在的位置,然后下载并处理 MSCOCO 数据集:
cd <your_tensorflow_root>/models/research/im2txt
bazel build //im2txt:download_and_preprocess_mscoco
bazel-bin/im2txt/download_and_preprocess_mscoco "${MSCOCO_DIR}"
download_and_preprocess_mscoco脚本完成后,您将在$MSCOCO_DIR文件夹中看到所有 TFRecord 格式的训练,验证和测试数据文件。
在$MSCOCO_DIR文件夹中还生成了一个名为word_counts.txt 的文件。 它总共有 11,518 个单词,每行包含一个单词,一个空格以及该单词出现在数据集中的次数。 文件中仅保存计数等于或大于 4 的单词。 还保存特殊词,例如句子的开头和结尾(分别表示为<S>和 </S> )。 稍后,您将看到我们如何在 iOS 和 Android 应用中专门使用和解析文件来生成字幕。
- 通过运行以下命令来获取 Inception v3 检查点文件:
INCEPTION_DIR="${HOME}/im2txt/data"
mkdir -p ${INCEPTION_DIR}
cd ${INCEPTION_DIR}
wget "http://download.tensorflow.org/models/inception_v3_2016_08_28.tar.gz"
tar -xvf inception_v3_2016_08_28.tar.gz -C ${INCEPTION_DIR}
rm inception_v3_2016_08_28.tar.gz
之后,您将在${HOME}/im2txt/data文件夹中看到一个名为inception_v3.ckpt的文件,如下所示:
jeff@AiLabby:~/im2txt/data$ ls -lt inception_v3.ckpt
-rw-r----- 1 jeff jeff 108816380 Aug 28 2016 inception_v3.ckpt
- 现在,我们准备使用以下命令来训练我们的模型:
INCEPTION_CHECKPOINT="${HOME}/im2txt/data/inception_v3.ckpt"
MODEL_DIR="${HOME}/im2txt/model"
cd <your_tensorflow_root>/models/research/im2txt
bazel build -c opt //im2txt/...
bazel-bin/im2txt/train \
--input_file_pattern="${MSCOCO_DIR}/train-?????-of-00256" \
--inception_checkpoint_file="${INCEPTION_CHECKPOINT}" \
--train_dir="${MODEL_DIR}/train" \
--train_inception=false \
--number_of_steps=1000000
即使在 GPU 上(例如第 1 章, “移动 TensorFlow 入门”中设置的 Nvidia GTX 1070),整个步骤(在前面的--number_of_steps参数中指定)也会超过 5 个昼夜,因为运行 5 万步大约需要 6.5 个小时。 幸运的是,您很快就会看到,即使以大约 50K 的步长,图像字幕的结果也已经相当不错了。 另请注意,您可以随时取消train脚本,然后稍后重新运行它,该脚本将从最后保存的检查点开始; 默认情况下,检查点会每 10 分钟保存一次,因此在最坏的情况下,您只会损失 10 分钟的训练时间。
经过几个小时的训练,取消前面的train脚本,然后查看--train_dir指向的位置。 您将看到类似这样的内容(默认情况下,将保存五组检查点文件,但此处仅显示三组):
ls -lt $MODEL_DIR/train
-rw-rw-r-- 1 jeff jeff 2171543 Feb 6 22:17 model.ckpt-109587.meta
-rw-rw-r-- 1 jeff jeff 463 Feb 6 22:17 checkpoint
-rw-rw-r-- 1 jeff jeff 149002244 Feb 6 22:17 model.ckpt-109587.data-00000-of-00001
-rw-rw-r-- 1 jeff jeff 16873 Feb 6 22:17 model.ckpt-109587.index
-rw-rw-r-- 1 jeff jeff 2171543 Feb 6 22:07 model.ckpt-109332.meta
-rw-rw-r-- 1 jeff jeff 16873 Feb 6 22:07 model.ckpt-109332.index
-rw-rw-r-- 1 jeff jeff 149002244 Feb 6 22:07 model.ckpt-109332.data-00000-of-00001
-rw-rw-r-- 1 jeff jeff 2171543 Feb 6 21:57 model.ckpt-109068.meta
-rw-rw-r-- 1 jeff jeff 149002244 Feb 6 21:57 model.ckpt-109068.data-00000-of-00001
-rw-rw-r-- 1 jeff jeff 16873 Feb 6 21:57 model.ckpt-109068.index
-rw-rw-r-- 1 jeff jeff 4812699 Feb 6 14:27 graph.pbtxt
您可以告诉每 10 分钟生成一组检查点文件(model.ckpt-109068.*和model.ckpt-109332.*和model.ckpt-109587.*)。 graph.pbtxt是模型的图定义文件(以文本格式),model.ckpt-??????.meta文件还包含模型的图定义,以及特定检查点的其他一些元数据,例如model.ckpt-109587.data-00000-of-00001(请注意, 大小几乎为 150MB,因为所有网络参数都保存在此处)。
- 测试字幕生成,如下所示:
CHECKPOINT_PATH="${HOME}/im2txt/model/train"
VOCAB_FILE="${HOME}/im2txt/data/mscoco/word_counts.txt"
IMAGE_FILE="${HOME}/im2txt/data/mscoco/raw-data/val2014/COCO_val2014_000000224477.jpg"
bazel build -c opt //im2txt:run_inference
bazel-bin/im2txt/run_inference \
--checkpoint_path=${CHECKPOINT_PATH} \
--vocab_file=${VOCAB_FILE} \
--input_files=${IMAGE_FILE}
CHECKPOINT_PATH被设置为与--train_dir被设置为相同的路径。 run_inference脚本将生成类似以下内容(不完全相同,具体取决于已执行了多少训练步骤):
Captions for image COCO_val2014_000000224477.jpg:
0) a man on a surfboard riding a wave . (p=0.015135)
1) a person on a surfboard riding a wave . (p=0.011918)
2) a man riding a surfboard on top of a wave . (p=0.009856)
这很酷。 如果我们可以在智能手机上运行此模型,会不会更酷? 但是在此之前,由于模型的相对复杂性以及 Python 中train和run_inference脚本的编写方式,我们还需要采取一些额外的步骤。
冻结图像字幕模型
在第 4 章,“转换具有惊人艺术风格的图片”,和第 5 章,“了解简单语音命令”中,我们使用了一个名为freeze.py的脚本的两个略有不同的版本,将受过训练的网络权重与网络图定义合并到一个自足的模型文件中,这是我们可以在移动设备上使用的好处。 TensorFlow 还带有freeze脚本的更通用版本,称为freeze_graph.py,位于tensorflow/python/tools文件夹中,可用于构建模型文件。 要使其正常运行,您需要为其提供至少四个参数(要查看所有可用参数,请查看 tensorflow/python/tools/freeze_graph.py):
-
--input_graph或--input_meta_graph:模型的图定义文件。 例如,在上一节的第 4 步的命令ls -lt $MODEL_DIR/train的输出中,model.ckpt-109587.meta是一个元图文件,其中包含模型的图定义和其他与检查点相关的元数据,而graph.pbtxt只是模型的图定义。 -
--input_checkpoint:特定的检查点文件,例如model.ckpt-109587。 注意,您没有指定大型检查点文件model.ckpt-109587.data-00000-of-00001的完整文件名。 -
--output_graph:冻结模型文件的路径–这是在移动设备上使用的路径。 -
--output_node_names:输出节点名称列表,以逗号分隔,告诉freeze_graph工具冻结模型中应包括模型的哪一部分和权重,因此生成特定输出不需要的节点名称和权重将保留。
因此,对于该模型,我们如何找出必备的输出节点名称以及输入节点名称,这些对推理也至关重要,正如我们在上一章的 iOS 和 Android 应用中所见到的那样? 因为我们已经使用run_inference脚本来生成测试图像的标题,所以我们可以看到它是如何进行推理的。
转到您的 im2txt 源代码文件夹models/research/im2txt/im2txt:您可能想在一个不错的编辑器(例如 Atom 或 Sublime Text)中打开它,或者在 Python IDE(例如 PyCharm)中打开它。 在run_inference.py中,对inference_utils/inference_wrapper_base.py中的build_graph_from_config进行了调用,在inference_wrapper.py中调用了build_model,在show_and_tell_model.py中进一步调用了build方法。 最后,build方法将调用build_input方法,该方法具有以下代码:
if self.mode == "inference":
image_feed = tf.placeholder(dtype=tf.string, shape=[], name="image_feed")
input_feed = tf.placeholder(dtype=tf.int64,
shape=[None], # batch_size
name="input_feed")
还有build_model方法,它具有:
if self.mode == "inference":
tf.concat(axis=1, values=initial_state, name="initial_state")
state_feed = tf.placeholder(dtype=tf.float32,
shape=[None, sum(lstm_cell.state_size)],
name="state_feed")
...
tf.concat(axis=1, values=state_tuple, name="state")
...
tf.nn.softmax(logits, name="softmax")
因此,名为image_feed,input_feed和state_feed的三个占位符应该是输入节点名称,而initial_state,state和softmax应当是输出节点名称。 此外,inference_wrapper.py中定义的两种方法证实了我们的侦探工作–第一种是:
def feed_image(self, sess, encoded_image):
initial_state = sess.run(fetches="lstm/initial_state:0",
feed_dict={"image_feed:0": encoded_image})
return initial_state
因此,我们提供image_feed并返回initial_state(lstm/前缀仅表示该节点在lstm范围内)。 第二种方法是:
def inference_step(self, sess, input_feed, state_feed):
softmax_output, state_output = sess.run(
fetches=["softmax:0", "lstm/state:0"],
feed_dict={
"input_feed:0": input_feed,
"lstm/state_feed:0": state_feed,
})
return softmax_output, state_output, None
我们输入input_feed和state_feed,然后返回softmax和state。 总共三个输入节点名称和三个输出名称。
注意,仅当mode为“推断”时才创建这些节点,因为train.py和run_inference.py都使用了 show_and_tell_model.py。 这意味着在运行run_inference.py脚本后,将修改在步骤 5 中使用train生成的--checkpoint_path中模型的图定义文件和权重。 那么,我们如何保存更新的图定义和检查点文件?
事实证明,在run_inference.py中,在创建 TensorFlow 会话后,还有一个调用restore_fn(sess)来加载检查点文件,并且该调用在inference_utils/inference_wrapper_base.py中定义:
def _restore_fn(sess):
saver.restore(sess, checkpoint_path)
在启动run_inference.py之后到达saver.restore调用时,已进行了更新的图定义,因此我们可以在此处保存新的检查点和图文件,从而使_restore_fn函数如下:
def _restore_fn(sess):
saver.restore(sess, checkpoint_path)
saver.save(sess, "model/image2text")
tf.train.write_graph(sess.graph_def, "model", 'im2txt4.pbtxt')
tf.summary.FileWriter("logdir", sess.graph_def)
tf.train.write_graph(sess.graph_def, "model", 'im2txt4.pbtxt')行是可选的,因为当通过调用saver.save保存新的检查点文件时,也会生成一个元文件,freeze_graph.py可以将其与检查点文件一起使用。 但是对于那些希望以纯文本格式查看所有内容,或者在冻结模型时更喜欢使用带有--in_graph参数的图定义文件的人来说,它是在这里生成的。 最后一行tf.summary.FileWriter("logdir", sess.graph_def)也是可选的,但它会生成一个可由 TensorBoard 可视化的事件文件。 因此,有了这些更改,在再次运行run_inference.py之后(除非首先直接使用 Python 运行run_inference.py,否则请记住首先运行bazel build -c opt //im2txt:run_inference),您将在model目录中看到以下新的检查点文件和新的图定义文件:
jeff@AiLabby:~/tensorflow-1.5.0/models/research/im2txt$ ls -lt model
-rw-rw-r-- 1 jeff jeff 2076964 Feb 7 12:33 image2text.pbtxt
-rw-rw-r-- 1 jeff jeff 1343049 Feb 7 12:33 image2text.meta
-rw-rw-r-- 1 jeff jeff 77 Feb 7 12:33 checkpoint
-rw-rw-r-- 1 jeff jeff 149002244 Feb 7 12:33 image2text.data-00000-of-00001
-rw-rw-r-- 1 jeff jeff 16873 Feb 7 12:33 image2text.index
在logdir目录中:
jeff@AiLabby:~/tensorflow-1.5.0/models/research/im2txt$ ls -lt logdir
total 2124
-rw-rw-r-- 1 jeff jeff 2171623 Feb 7 12:33 events.out.tfevents.1518035604.AiLabby
Running the bazel build command to build a TensorFlow Python script is optional. You can just run the Python script directly. For example, we can run python tensorflow/python/tools/freeze_graph.py without building it first with bazel build tensorflow/python/tools:freeze_graph then running bazel-bin/tensorflow/python/tools/freeze_graph. But be aware that running the Python script directly will use the version of TensorFlow you’ve installed via pip, which may be different from the version you’ve downloaded as source and built by the bazel build command. This can be the cause of some confusing errors so be sure you know the TensorFlow version used to run a script. In addition, for a C++ based tool, you have to build it first with bazel before you can run it. For example, the transform_graph tool, which we'll see soon, is implemented in transform_graph.cc located at tensorflow/tools/graph_transforms; another important tool called convert_graphdef_memmapped_format, which we'll use for our iOS app later, is also implemented in C++ located at tensorflow/contrib/util.
现在我们到了,让我们快速使用 TensorBoard 看一下我们的图–只需运行tensorboard --logdir logdir,然后从浏览器中打开http://localhost:6006。 图 6.1 显示了三个输出节点名称(顶部为softmax,以及lstm/initial_state和红色矩形顶部的突出显示的lstm/state)和一个输入节点名称(底部的state_feed):

图 6.1:该图显示了三个输出节点名称和一个输入节点名称
图 6.2 显示了另一个输入节点名称image_feed:

图 6.2:该图显示了一个附加的输入节点名称image_feed
最后,图 6.3 显示了最后一个输入节点名称input_feed:

图 6.3:该图显示了最后一个输入节点名称input_feed
当然,这里有很多我们不能也不会涵盖的细节。 但是,您将了解大局,同样重要的是,有足够的细节可以继续前进。 现在运行freeze_graph.py应该像轻风(双关语):
python tensorflow/python/tools/freeze_graph.py --input_meta_graph=/home/jeff/tensorflow-1.5.0/models/research/im2txt/model/image2text.meta --input_checkpoint=/home/jeff/tensorflow-1.5.0/models/research/im2txt/model/image2text --output_graph=/tmp/image2text_frozen.pb --output_node_names="softmax,lstm/initial_state,lstm/state" --input_binary=true
请注意,我们在这里使用元图文件以及将--input_binary参数设置为true,因为默认情况下它为false,这意味着freeze_graph工具期望输入图或元图文件为文本格式。
您可以使用文本格式的图文件作为输入,在这种情况下,无需提供--input_binary参数:
python tensorflow/python/tools/freeze_graph.py --input_graph=/home/jeff/tensorflow-1.5.0/models/research/im2txt/model/image2text.pbtxt --input_checkpoint=/home/jeff/tensorflow-1.5.0/models/research/im2txt/model/image2text --output_graph=/tmp/image2text_frozen2.pb --output_node_names="softmax,lstm/initial_state,lstm/state"
两个输出图文件image2text_frozen.pb和image2text_frozen2.pb的大小会稍有不同,但是在经过转换和可能的优化后,它们在移动设备上使用时,它们的行为完全相同。
转换和优化图像字幕模型
如果您真的等不及了,现在决定尝试在 iOS 或 Android 应用上尝试新近冻结的热模型,则可以,但是您会看到一个致命错误No OpKernel was registered to support Op 'DecodeJpeg' with these attrs,迫使你重新考虑你的决定。
使用转换的模型修复错误
通常,您可以使用strip_unused.py, 工具,与 tensorflow/python/tools,中的 freeze_graph.py位于相同位置,来删除不包含在 TensorFlow 核心库中的DecodeJpeg操作。但是由于输入节点image_feed需要进行解码操作(图 6.2), strip_unused之类的工具不会将DecodeJpeg视为未使用,因此不会被剥夺。 您可以先运行strip_unused命令,如下所示进行验证:
bazel-bin/tensorflow/python/tools/strip_unused --input_graph=/tmp/image2text_frozen.pb --output_graph=/tmp/image2text_frozen_stripped.pb --input_node_names="image_feed,input_feed,lstm/state_feed" --output_node_names="softmax,lstm/initial_state,lstm/state" --input_binary=True
然后在 iPython 中加载输出图并列出前几个节点,如下所示:
import tensorflow as tf
g=tf.GraphDef()
g.ParseFromString(open("/tmp/image2text_frozen_stripped", "rb").read())
x=[n.name for n in g.node]
x[:6]
输出如下:
[u'image_feed',
u'input_feed',
u'decode/DecodeJpeg',
u'convert_image/Cast',
u'convert_image/y',
u'convert_image']
解决您的 iOS 应用错误的第二种可能解决方案,像第 5 章, “了解简单语音命令”一样,是在 tf_op_files文件中添加未注册的操作实现,并重建 TensorFlow iOS 库。 坏消息是,由于 TensorFlow 中没有DecodeJpeg函数的实现,因此无法将DecodeJpeg的 TensorFlow 实现添加到tf_op_files中。
实际上,在图 6.2 中也暗示了对此烦恼的解决方法,其中convert_image节点用作image_feed输入的解码版本。 为了更准确,单击 TensorBoard 图中的转换和解码节点,如图 6.4 所示,您将从右侧的 TensorBoard 信息卡中看到输入转换(名为convert_image/Cast)的输出为decode/DecodeJpeg和convert_image,解码的输入和输出为image_feed和convert_image/Cast:

图 6.4:调查解码和conver_image节点
实际上,在im2txt/ops/image_processing.py中有一行image = tf.image.convert_image_dtype(image, dtype=tf.float32)将解码的图像转换为浮点数。 让我们用convert_image/Cast代替 TensorBoard 中显示的名称image_feed,以及前面代码片段的输出,然后再次运行strip_unused:
bazel-bin/tensorflow/python/tools/strip_unused --input_graph=/tmp/image2text_frozen.pb --output_graph=/tmp/image2text_frozen_stripped.pb --input_node_names="convert_image/Cast,input_feed,lstm/state_feed" --output_node_names="softmax,lstm/initial_state,lstm/state" --input_binary=True
现在,重新运行代码片段,如下所示:
g.ParseFromString(open("/tmp/image2text_frozen_stripped", "rb").read())
x=[n.name for n in g.node]
x[:6]
并且输出不再具有decode / DecodeJpeg节点:
[u'input_feed',
u'convert_image/Cast',
u'convert_image/y',
u'convert_image',
u'ExpandDims_1/dim',
u'ExpandDims_1']
如果我们在 iOS 或 Android 应用中使用新的模型文件image2text_frozen_stripped.pb,则No OpKernel was registered to support Op 'DecodeJpeg' with these attrs. 肯定会消失。 但是发生另一个错误, Not a valid TensorFlow Graph serialization: Input 0 of node ExpandDims_6 was passed float from input_feed:0 incompatible with expected int64。 如果您通过名为 TensorFlow for Poets 2 的不错的 Google TensorFlow 代码实验室,可能会想起来,还有另一个名为optimize_for_inference的工具,其功能类似于strip_unused,并且可以很好地用于代码实验室中的图像分类任务。 您可以像这样运行它:
bazel build tensorflow/python/tools:optimize_for_inference
bazel-bin/tensorflow/python/tools/optimize_for_inference \
--input=/tmp/image2text_frozen.pb \
--output=/tmp/image2text_frozen_optimized.pb \
--input_names="convert_image/Cast,input_feed,lstm/state_feed" \
--output_names="softmax,lstm/initial_state,lstm/state"
但是在 iOS 或 Android 应用上加载输出模型文件 image2text_frozen_optimized.pb会导致相同的Input 0 of node ExpandDims_6 was passed float from input_feed:0 incompatible with expected int64 错误。 看起来,尽管我们试图至少在某种程度上实现福尔摩斯在本章中可以做的事情,但有人希望我们首先成为福尔摩斯。
如果您在其他模型(例如我们在前几章中看到的模型)上尝试过strip_unused或optimize_for_inference工具,则它们可以正常工作。 事实证明,尽管官方 TensorFlow 1.4 和 1.5 发行版中包含了两个基于 Python 的工具,但在优化一些更复杂的模型时却存在一些错误。 更新和正确的工具是基于 C++ 的transform_graph工具,现在是 TensorFlow Mobile 网站推荐的官方工具。 运行以下命令以消除在移动设备上部署时的int64不兼容float的错误:
bazel build tensorflow/tools/graph_transforms:transform_graph
bazel-bin/tensorflow/tools/graph_transforms/transform_graph \
--in_graph=/tmp/image2text_frozen.pb \
--out_graph=/tmp/image2text_frozen_transformed.pb \
--inputs="convert_image/Cast,input_feed,lstm/state_feed" \
--outputs="softmax,lstm/initial_state,lstm/state" \
--transforms='
strip_unused_nodes(type=float, shape="299,299,3")
fold_constants(ignore_errors=true, clear_output_shapes=true)
fold_batch_norms
fold_old_batch_norms'
我们将不讨论所有--transforms选项的详细信息,这些选项在这里有完整记录。 基本上,--transforms设置可以正确消除模型中未使用的节点,例如DecodeJpeg,并且还可以进行其他一些优化。
现在,如果您在 iOS 和 Android 应用中加载image2text_frozen_transformed.pb文件,则不兼容的错误将消失。 当然,我们还没有编写任何真实的 iOS 和 Android 代码,但是我们知道该模型很好,可以随时使用。 很好,但是可以更好。
优化转换后的模型
真正的最后一步,也是至关重要的一步,尤其是在运行复杂的冻结和转换模型(例如我们在较旧的 iOS 设备上训练过的模型)时,是使用位于 tensorflow/contrib/util的另一个工具convert_graphdef_memmapped_format ,将冻结和转换后的模型转换为映射格式。 映射文件允许现代操作系统(例如 iOS 和 Android)将文件直接映射到主内存,因此无需为文件分配内存,也无需写回磁盘,因为文件数据是只读的,这非常重要。 性能提高。
更重要的是,iOS 不会将已映射文件视为内存使用量,因此,当内存压力过大时,即使文件很大,使用已映射文件的应用也不会由于内存使用太大而被 iOS 杀死和崩溃。 实际上,正如我们将在下一节中很快看到的那样,如果模型文件的转换版本未转换为 memmapped 格式,则将在较旧的移动设备(如 iPhone 6)上崩溃,在这种情况下,转换是必须的, 有。
构建和运行该工具的命令非常简单:
bazel build tensorflow/contrib/util:convert_graphdef_memmapped_format
bazel-bin/tensorflow/contrib/util/convert_graphdef_memmapped_format \
--in_graph=/tmp/image2text_frozen_transformed.pb \
--out_graph=/tmp/image2text_frozen_transformed_memmapped.pb
下一节将向您展示如何在 iOS 应用中使用image2text_frozen_transformed_memmapped.pb模型文件。 它也可以在使用本机代码的 Android 中使用,但是由于时间限制,我们将无法在本章中介绍它。
我们花了很多功夫才能最终为移动应用准备好复杂的图像字幕模型。 是时候欣赏使用模型的简单性了。 实际上,使用模型不仅仅是 iOS 中的单个 session->Run 调用,还是 Android 中的 mInferenceInterface.run 调用,就像我们在前面所有章节中所做的那样; 从输入图像到自然语言输出的推论(如您在上一节中研究run_inference.py的工作原理时所见)涉及到对模型的run方法的多次调用。 LSTM 模型就是这样工作的:“继续向我发送新的输入(基于我以前的状态和输出),我将向您发送下一个状态和输出。” 简单来说,我们的意思是向您展示如何使用尽可能少的简洁代码来构建 iOS 和 Android 应用,这些应用使用该模型以自然语言描述图像。 这样,如果需要,您可以轻松地在自己的应用中集成模型及其推理代码。
在 iOS 中使用图像字幕模型
由于该模型的 CNN 部分基于 Inception v3,因此我们在第 2 章,“通过迁移学习对图像进行分类”时使用的模型相同,因此我们可以并且将使用更简单的 TensorFlow Pod 进行以下操作: 创建我们的 Objective-C iOS 应用。 请按照此处的步骤查看如何在新的 iOS 应用中同时使用image2text_frozen_transformed.pb和image2text_frozen_transformed_memmapped.pb模型文件:
- 类似于第 2 章,“通过迁移学习对图像进行分类”,“将 TensorFlow 添加到 Objective-C iOS 应用”部分中的前四个步骤, 名为
Image2Text的 iOS 项目,添加具有以下内容的名为Podfile的新文件:
target 'Image2Text'
pod 'TensorFlow-experimental'
然后在终端上运行pod install并打开Image2Text.xcworkspace文件。 将ios_image_load.h, ios_image_load.mm,tensorflow_utils.h和tensorflow_utils.mm文件从位于tensorflow/examples/ios/camera的 TensorFlow iOS 示例相机应用拖放到 Xcode 的Image2Text项目中。 之前我们已经重用了ios_image_load.*文件,此处tensorflow_utils.*文件主要用于加载映射的模型文件。 tensorflow_utils.mm中有两种方法LoadModel和 LoadMemoryMappedModel :一种以我们以前的方式加载非映射模型,另一种加载了映射模型 。 如果有兴趣,请看一下LoadMemoryMappedModel的实现方式,并且这个页面上的文档也可能会有用。
- 添加在上一节末尾生成的两个模型文件,在“训练和测试字幕生成”小节第 2 步中生成的
word_counts.txt文件,以及一些测试图像–我们保存并使用 TensorFlow im2txt 模型页面顶部的四个图像,以便我们比较我们的模型的字幕结果,以及那些由使用更多步骤训练的模型所生成的结果。 还将ViewController.m重命名为.mm,从现在开始,我们将只处理ViewController.mm文件即可完成应用。 现在,您的 XcodeImage2Text项目应类似于图 6.5:

图 6.5:设置Image2Text iOS 应用,还显示如何实现LoadMemoryMappedModel
- 打开
ViewController.mm并添加一堆 Objective-C 和 C++ 常量,如下所示:
static NSString* MODEL_FILE = @"image2text_frozen_transformed";
static NSString* MODEL_FILE_MEMMAPPED = @"image2text_frozen_transformed_memmapped";
static NSString* MODEL_FILE_TYPE = @"pb";
static NSString* VOCAB_FILE = @"word_counts";
static NSString* VOCAB_FILE_TYPE = @"txt";
static NSString *image_name = @"im2txt4.png";
const string INPUT_NODE1 = "convert_image/Cast";
const string OUTPUT_NODE1 = "lstm/initial_state";
const string INPUT_NODE2 = "input_feed";
const string INPUT_NODE3 = "lstm/state_feed";
const string OUTPUT_NODE2 = "softmax";
const string OUTPUT_NODE3 = "lstm/state";
const int wanted_width = 299;
const int wanted_height = 299;
const int wanted_channels = 3;
const int CAPTION_LEN = 20;
const int START_ID = 2;
const int END_ID = 3;
const int WORD_COUNT = 12000;
const int STATE_COUNT = 1024;
它们都是自我解释的,如果您通读了本章,则应该看起来都很熟悉,除了最后五个常量:CAPTION_LEN是我们要在标题中生成的最大单词数,START_ID是句子起始词<S>的 ID,定义为word_counts.txt文件中的行号; 所以 2 在第二行表示,在第三行表示 3。 word_counts.txt文件的前几行是这样的:
a 969108
<S> 586368
</S> 586368
. 440479
on 213612
of 202290
WORD_COUNT是模型假设的总单词数,对于您很快就会看到的每个推理调用,模型将返回总计 12,000 的概率得分以及 LSTM 模型的 1,024 个状态值。
- 添加一些全局变量和一个函数签名:
unique_ptr<tensorflow::Session> session;
unique_ptr<tensorflow::MemmappedEnv> tf_memmapped_env;
std::vector<std::string> words;
UIImageView *_iv;
UILabel *_lbl;
NSString* generateCaption(bool memmapped);
此简单的与 UI 相关的代码类似于第 2 章,“通过迁移学习对图像进行分类”的 iOS 应用的代码。 基本上,您可以在应用启动后点击任意位置,然后选择两个模型之一,图像描述结果将显示在顶部。 当用户在alert操作中选择了映射模型时,将运行以下代码:
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSString *caption = generateCaption(true);
dispatch_async(dispatch_get_main_queue(), ^{
_lbl.text = caption;
});
});
如果选择了非映射模型,则使用generateCaption(false)。
- 在
viewDidLoad方法的末尾,添加代码以加载word_counts.txt并将这些单词逐行保存在 Objective-C 和 C++ 中:
NSString* voc_file_path = FilePathForResourceName(VOCAB_FILE, VOCAB_FILE_TYPE);
if (!voc_file_path) {
LOG(FATAL) << "Couldn't load vocabuary file: " << voc_file_path;
}
ifstream t;
t.open([voc_file_path UTF8String]);
string line;
while(t){
getline(t, line);
size_t pos = line.find(" ");
words.push_back(line.substr(0, pos));
}
t.close();
- 剩下的我们要做的就是实现
generateCaption函数。 在其中,首先加载正确的模型:
tensorflow::Status load_status;
if (memmapped)
load_status = LoadMemoryMappedModel(MODEL_FILE_MEMMAPPED, MODEL_FILE_TYPE, &session, &tf_memmapped_env);
else
load_status = LoadModel(MODEL_FILE, MODEL_FILE_TYPE, &session);
if (!load_status.ok()) {
return @"Couldn't load model";
}
- 然后,使用类似的图像处理代码来准备要输入到模型中的图像张量:
int image_width;
int image_height;
int image_channels;
NSArray *name_ext = [image_name componentsSeparatedByString:@"."];
NSString* image_path = FilePathForResourceName(name_ext[0], name_ext[1]);
std::vector<tensorflow::uint8> image_data = LoadImageFromFile([image_path UTF8String], &image_width, &image_height, &image_channels);
tensorflow::Tensor image_tensor(tensorflow::DT_FLOAT, tensorflow::TensorShape({wanted_height, wanted_width, wanted_channels}));
auto image_tensor_mapped = image_tensor.tensor<float, 3>();
tensorflow::uint8* in = image_data.data();
float* out = image_tensor_mapped.data();
for (int y = 0; y < wanted_height; ++y) {
const int in_y = (y * image_height) / wanted_height;
tensorflow::uint8* in_row = in + (in_y * image_width * image_channels);
float* out_row = out + (y * wanted_width * wanted_channels);
for (int x = 0; x < wanted_width; ++x) {
const int in_x = (x * image_width) / wanted_width;
tensorflow::uint8* in_pixel = in_row + (in_x * image_channels);
float* out_pixel = out_row + (x * wanted_channels);
for (int c = 0; c < wanted_channels; ++c) {
out_pixel[c] = in_pixel[c];
}
}
}
- 现在,我们可以将图像发送到模型,并获取返回的
initial_state张量向量,该向量包含 1,200(STATE_COUNT)个值:
vector<tensorflow::Tensor> initial_state;
if (session.get()) {
tensorflow::Status run_status = session->Run({{INPUT_NODE1, image_tensor}}, {OUTPUT_NODE1}, {}, &initial_state);
if (!run_status.ok()) {
return @"Getting initial state failed";
}
}
- 定义
input_feed和state_feed张量,并将它们的值分别设置为起始字的 ID 和返回的initial_state值:
tensorflow::Tensor input_feed(tensorflow::DT_INT64, tensorflow::TensorShape({1,}));
tensorflow::Tensor state_feed(tensorflow::DT_FLOAT, tensorflow::TensorShape({1, STATE_COUNT}));
auto input_feed_map = input_feed.tensor<int64_t, 1>();
auto state_feed_map = state_feed.tensor<float, 2>();
input_feed_map(0) = START_ID;
auto initial_state_map = initial_state[0].tensor<float, 2>();
for (int i = 0; i < STATE_COUNT; i++){
state_feed_map(0,i) = initial_state_map(0,i);
}
- 在
CAPTION_LEN上创建一个for循环,然后在该循环内,首先创建output_feed和output_states张量向量,然后馈入我们先前设置的input_feed和state_feed,并运行模型以返回由softmax张量和new_state张量组成的output张量向量:
vector<int> captions;
for (int i=0; i<CAPTION_LEN; i++) {
vector<tensorflow::Tensor> output;
tensorflow::Status run_status = session->Run({{INPUT_NODE2, input_feed}, {INPUT_NODE3, state_feed}}, {OUTPUT_NODE2, OUTPUT_NODE3}, {}, &output);
if (!run_status.ok()) {
return @"Getting LSTM state failed";
}
else {
tensorflow::Tensor softmax = output[0];
tensorflow::Tensor state = output[1];
auto softmax_map = softmax.tensor<float, 2>();
auto state_map = state.tensor<float, 2>();
- 现在,找到可能性最大(softmax 值)的单词 ID。 如果是结束字的 ID,则结束
for循环;否则,结束循环。 否则,将具有最大 softmax 值的单词id添加到向量captions中。 请注意,此处我们使用贪婪搜索,始终选择概率最大的单词,而不是像run_inference.py脚本中那样将大小设置为 3 的集束搜索。 在for循环的末尾,用最大字数id更新input_feed值,并用先前返回的state值更新state_feed值,然后再将两个输入,所有下一个单词的 softmax 值和下一个状态值,馈送到模型:
float max_prob = 0.0f;
int max_word_id = 0;
for (int j = 0; j < WORD_COUNT; j++){
if (softmax_map(0,j) > max_prob) {
max_prob = softmax_map(0,j);
max_word_id = j;
}
}
if (max_word_id == END_ID) break;
captions.push_back(max_word_id);
input_feed_map(0) = max_word_id;
for (int j = 0; j < STATE_COUNT; j++){
state_feed_map(0,j) = state_map(0,j);
}
}
}
我们可能从未详细解释过如何在 C++ 中获取和设置 TensorFlow 张量值。 但是,如果您到目前为止已经阅读了本书中的代码,那么您应该已经学会了。 这就像 RNN 学习:如果您接受了足够的代码示例训练,就可以编写有意义的代码。 总而言之,首先使用Tensor类型定义变量,并使用该变量的数据类型和形状指定,然后调用Tensor类的tensor方法,传入数据类型的 C++ 版本和形状,以创建张量的贴图变量。 之后,您可以简单地使用映射来获取或设置张量的值。
- 最后,只需遍历
captions向量并将向量中存储的每个词 ID 转换为一个词,然后将该词添加到sentence字符串中,而忽略起始 ID 和结束 ID,然后返回该句子,希望是可读的自然语言:
NSString *sentence = @"";
for (int i=0; i<captions.size(); i++) {
if (captions[i] == START_ID) continue;
if (captions[i] == END_ID) break;
sentence = [NSString stringWithFormat:@"%@ %s", sentence, words[captions[i]].c_str()];
}
return sentence;
这就是在 iOS 应用中运行模型所需的一切。 现在,在 iOS 模拟器或设备中运行该应用,点击并选择一个模型,如图 6.6 所示:

图 6.6:运行Image2Text iOS 应用并选择模型
在 iOS 模拟器上,运行非映射模型需要 10 秒钟以上,运行映射模型则需要 5 秒钟以上。 在 iPhone 6 上,运行贴图模型还需要大约 5 秒钟,但由于模型文件和内存压力较大,运行非贴图模型时会崩溃。
至于结果,图 6.7 显示了四个测试图像结果:

图 6.7:显示图像字幕结果
图 6.8 显示了 TensorFlow im2txt 网站上的结果,您可以看到我们更简单的贪婪搜索结果看起来也不错。 但是对于长颈鹿图片,看来我们的模型或推理代码不够好。 完成本章中的工作后,希望您会在改进训练或模型推断方面有所收获:

图 6.8:字幕示例显示在 TensorFlow im2txt 模型网站上
在我们进行下一个智能任务之前,是时候给 Android 开发人员一个不错的选择了。
在 Android 中使用图像字幕模型
遵循相同的简单性考虑,我们将开发具有最小 UI 的新 Android 应用,并着重于如何在 Android 中使用该模型:
- 创建一个名为
Image2Text的新 Android 应用,在应用build.gradle文件的依存关系的末尾添加compile 'org.tensorflow:tensorflow-android:+',创建一个assets文件夹,然后将image2text_frozen_transformed.pb模型文件word_counts.txt文件和一些测试图像文件拖放到其中。 - 在
activity_main.xml文件中添加一个ImageView和一个按钮:
<ImageView
android:id="@+id/imageview"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="1.0"/>
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="DESCRIBE ME"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="1.0"/>
- 打开
MainActivity.java,使其实现Runnable接口,然后添加以下常量,在前一节中说明了其中的最后五个,而其他则是自解释的:
private static final String MODEL_FILE = "file:///android_asset/image2text_frozen_transformed.pb";
private static final String VOCAB_FILE = "file:///android_asset/word_counts.txt";
private static final String IMAGE_NAME = "im2txt1.png";
private static final String INPUT_NODE1 = "convert_image/Cast";
private static final String OUTPUT_NODE1 = "lstm/initial_state";
private static final String INPUT_NODE2 = "input_feed";
private static final String INPUT_NODE3 = "lstm/state_feed";
private static final String OUTPUT_NODE2 = "softmax";
private static final String OUTPUT_NODE3 = "lstm/state";
private static final int IMAGE_WIDTH = 299;
private static final int IMAGE_HEIGHT = 299;
private static final int IMAGE_CHANNEL = 3;
private static final int CAPTION_LEN = 20;
private static final int WORD_COUNT = 12000;
private static final int STATE_COUNT = 1024;
private static final int START_ID = 2;
private static final int END_ID = 3;
以及以下实例变量和处理器实现:
private ImageView mImageView;
private Button mButton;
private TensorFlowInferenceInterface mInferenceInterface;
private String[] mWords = new String[WORD_COUNT];
private int[] intValues;
private float[] floatValues;
Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
mButton.setText("DESCRIBE ME");
String text = (String)msg.obj;
Toast.makeText(MainActivity.this, text, Toast.LENGTH_LONG).show();
mButton.setEnabled(true);
} };
- 在
onCreate方法中,首先在ImageView中添加显示测试图像并处理按钮单击事件的代码:
mImageView = findViewById(R.id.imageview);
try {
AssetManager am = getAssets();
InputStream is = am.open(IMAGE_NAME);
Bitmap bitmap = BitmapFactory.decodeStream(is);
mImageView.setImageBitmap(bitmap);
} catch (IOException e) {
e.printStackTrace();
}
mButton = findViewById(R.id.button);
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mButton.setEnabled(false);
mButton.setText("Processing...");
Thread thread = new Thread(MainActivity.this);
thread.start();
}
});
然后添加读取word_counts.txt每行的代码,并将每个单词保存在mWords数组中:
String filename = VOCAB_FILE.split("file:///android_asset/")[1];
BufferedReader br = null;
int linenum = 0;
try {
br = new BufferedReader(new InputStreamReader(getAssets().open(filename)));
String line;
while ((line = br.readLine()) != null) {
String word = line.split(" ")[0];
mWords[linenum++] = word;
}
br.close();
} catch (IOException e) {
throw new RuntimeException("Problem reading vocab file!" , e);
}
- 现在,在
public void run()方法中,在DESCRIBE ME按钮发生onClick事件时启动,添加代码以调整测试图像的大小,从调整后的位图中读取像素值,然后将它们转换为浮点数-我们已经在前三章中看到了这样的代码:
intValues = new int[IMAGE_WIDTH * IMAGE_HEIGHT];
floatValues = new float[IMAGE_WIDTH * IMAGE_HEIGHT * IMAGE_CHANNEL];
Bitmap bitmap = BitmapFactory.decodeStream(getAssets().open(IMAGE_NAME));
Bitmap croppedBitmap = Bitmap.createScaledBitmap(bitmap, IMAGE_WIDTH, IMAGE_HEIGHT, true);
croppedBitmap.getPixels(intValues, 0, IMAGE_WIDTH, 0, 0, IMAGE_WIDTH, IMAGE_HEIGHT);
for (int i = 0; i < intValues.length; ++i) {
final int val = intValues[i];
floatValues[i * IMAGE_CHANNEL + 0] = ((val >> 16) & 0xFF);
floatValues[i * IMAGE_CHANNEL + 1] = ((val >> 8) & 0xFF);
floatValues[i * IMAGE_CHANNEL + 2] = (val & 0xFF);
}
- 创建一个
TensorFlowInferenceInterface实例,该实例加载模型文件,并通过向其提供图像值,然后在initialState中获取返回结果来使用该模型进行第一个推断:
AssetManager assetManager = getAssets();
mInferenceInterface = new TensorFlowInferenceInterface(assetManager, MODEL_FILE);
float[] initialState = new float[STATE_COUNT];
mInferenceInterface.feed(INPUT_NODE1, floatValues, IMAGE_WIDTH, IMAGE_HEIGHT, 3);
mInferenceInterface.run(new String[] {OUTPUT_NODE1}, false);
mInferenceInterface.fetch(OUTPUT_NODE1, initialState);
- 将第一个
input_feed值设置为起始 ID,并将第一个state_feed值设置为返回的initialState值:
long[] inputFeed = new long[] {START_ID};
float[] stateFeed = new float[STATE_COUNT * inputFeed.length];
for (int i=0; i < STATE_COUNT; i++) {
stateFeed[i] = initialState[i];
}
如您所见,得益于 Android 中的TensorFlowInferenceInterface实现,在 Android 中获取和设置张量值并进行推理比在 iOS 中更简单。 在我们开始重复使用inputFeed和stateFeed进行模型推断之前,我们创建了一个captions列表,该列表包含一对整数和浮点数,其中整数作为单词 ID,具有最大 softmax 值(在模型为每个推理调用返回的所有 softmax 值中)和float作为单词的 softmax 值。 我们可以使用一个简单的向量来保存每个推论返回中具有最大 softmax 值的单词,但是使用对的列表可以使以后我们从贪婪搜索方法切换到集束搜索时更加容易:
List<Pair<Integer, Float>> captions = new ArrayList<Pair<Integer, Float>>();
- 在字幕长度的
for循环中,我们将上面设置的值提供给input_feed和state_feed,然后获取返回的softmax和newstate值:
for (int i=0; i<CAPTION_LEN; i++) {
float[] softmax = new float[WORD_COUNT * inputFeed.length];
float[] newstate = new float[STATE_COUNT * inputFeed.length];
mInferenceInterface.feed(INPUT_NODE2, inputFeed, 1);
mInferenceInterface.feed(INPUT_NODE3, stateFeed, 1, STATE_COUNT);
mInferenceInterface.run(new String[]{OUTPUT_NODE2, OUTPUT_NODE3}, false);
mInferenceInterface.fetch(OUTPUT_NODE2, softmax);
mInferenceInterface.fetch(OUTPUT_NODE3, newstate);
- 现在,创建另一个由整数和浮点对组成的列表,将每个单词的 ID 和 softmax 值添加到列表中,并以降序对列表进行排序:
List<Pair<Integer, Float>> prob_id = new ArrayList<Pair<Integer, Float>>();
for (int j = 0; j < WORD_COUNT; j++) {
prob_id.add(new Pair(j, softmax[j]));
}
Collections.sort(prob_id, new Comparator<Pair<Integer, Float>>() {
@Override
public int compare(final Pair<Integer, Float> o1, final Pair<Integer, Float> o2) {
return o1.second > o2.second ? -1 : (o1.second == o2.second ? 0 : 1);
}
});
- 如果最大概率的单词是结束单词,则结束循环。 否则,将该对添加到
captions列表,并使用最大 softmax 值的单词 ID 更新input_feed并使用返回的状态值更新state_feed,以继续进行下一个推断:
if (prob_id.get(0).first == END_ID) break;
captions.add(new Pair(prob_id.get(0).first, prob_id.get(0).first));
inputFeed = new long[] {prob_id.get(0).first};
for (int j=0; j < STATE_COUNT; j++) {
stateFeed[j] = newstate[j];
}
}
- 最后,遍历
captions列表中的每一对,并将每个单词(如果不是开头和结尾的话)添加到sentence字符串,该字符串通过处理器返回,以向用户显示自然语言输出:
String sentence = "";
for (int i=0; i<captions.size(); i++) {
if (captions.get(i).first == START_ID) continue;
if (captions.get(i).first == END_ID) break;
sentence = sentence + " " + mWords[captions.get(i).first];
}
Message msg = new Message();
msg.obj = sentence;
mHandler.sendMessage(msg);
在您的虚拟或真实 Android 设备上运行该应用。 大约需要 10 秒钟才能看到结果。 您可以使用上一节中显示的四个不同的测试图像,并在图 6.9 中查看结果:

图 6.9:在 Android 中显示图像字幕结果
一些结果与 iOS 结果以及 TensorFlow im2txt 网站上的结果略有不同。 但是它们看起来都不错。 另外,在相对较旧的 Android 设备(例如 Nexus 5)上运行该模型的非映射版本也可以。 但是最好在 Android 中加载映射模型,以查看性能的显着提高,我们可能会在本书后面的章节中介绍。
因此,这将使用功能强大的图像字幕模型完成分步的 Android 应用构建过程。 无论您使用的是 iOS 还是 Android 应用,您都应该能够轻松地将我们训练有素的模型和推理代码集成到自己的应用中,或者返回到训练过程以微调模型,然后准备并优化更好的模型。 在您的移动应用中使用的模型。
总结
在本章中,我们首先讨论了由现代端到端深度学习支持的图像字幕如何工作,然后总结了如何使用 TensorFlow im2txt 模型项目训练这种模型。 我们详细讨论了如何找到正确的输入节点名称和输出节点名称,以及如何冻结模型,然后使用最新的图转换工具和映射转换工具修复在将模型加载到手机上时出现的一些讨厌的错误。 之后,我们展示了有关如何使用模型构建 iOS 和 Android 应用以及如何使用模型的 LSTM RNN 组件进行新的序列推断的详细教程。
令人惊讶的是,经过训练了成千上万个图像字幕示例,并在现代 CNN 和 LSTM 模型的支持下,我们可以构建和使用一个模型,该模型可以在移动设备上生成合理的自然语言描述。 不难想象可以在此基础上构建什么样的有用应用。 我们喜欢福尔摩斯吗? 当然不。 我们已经在路上了吗? 我们希望如此。 AI 的世界既令人着迷又充满挑战,但是只要我们不断取得稳步进步并改善自己的学习过程,同时又避免了梯度问题的消失和爆炸,我们就有很大机会建立一个类似于 Holmes 的模型,并可以随时随地在一天中在移动应用中使用它。
漫长的篇章讨论了基于 CNN 和 LSTM 的网络模型的实际使用,我们值得一试。 在下一章中,您将看到如何使用另一个基于 CNN 和 LSTM 的模型来开发有趣的 iOS 和 Android 应用,这些应用使您可以绘制对象然后识别它们是什么。 要快速获得游戏在线版本的乐趣,请访问这里。
七、使用 CNN 和 LSTM 识别绘画
在上一章中,我们看到了使用深度学习模型的强大功能,该模型将 CNN 与 LSTM RNN 集成在一起以生成图像的自然语言描述。 如果深度学习驱动的 AI 就像新的电力一样,我们当然希望看到这种混合神经网络模型在许多不同领域中的应用。 诸如图像字幕之类的严肃应用与之相反? 一个有趣的绘画应用,例如 Quick Draw(请参见这里了解有趣的示例数据),使用经过训练并基于 345 个类别中的 5000 万张绘画的模型,并将新绘画分类到这些类别中,听起来不错。 还有一个正式的 TensorFlow 教程,该教程介绍了如何构建这样的模型来帮助我们快速入门。
事实证明,在 iOS 和 Android 应用上使用本教程构建的模型的任务提供了一个绝佳的机会:
- 加深我们对找出模型的正确输入和输出节点名称的理解,因此我们可以为移动应用适当地准备模型
- 使用其他方法来修复 iOS 中的新模型加载和推断错误
- 首次为 Android 构建自定义的 TensorFlow 本机库,以修复 Android 中的新模型加载和预测错误
- 查看有关如何使用预期格式的输入来输入 TensorFlow 模型以及如何在 iOS 和 Android 中获取和处理其输出的更多示例
此外,在处理所有繁琐而重要的细节的过程中,以便模型可以像魔术一样工作,以进行漂亮的绘画分类,您将在 iOS 和 Android 设备上享受有趣的涂鸦。
因此,在本章中,我们将介绍以下主题:
- 绘画分类 -- 工作原理
- 训练并准备绘画分类模型
- 在 iOS 中使用绘画分类模型
- 在 Android 中使用绘画分类模型
绘画分类 – 工作原理
TensorFlow 教程中内置的绘画分类模型,首先接受表示为点列表的用户绘画输入,并将规范化输入转换为连续点的增量的张量,以及有关每个点是否是新笔画的开始的信息。 然后将张量穿过几个卷积层和 LSTM 层,最后穿过 softmax 层,如图 7.1 所示,以对用户绘画进行分类:

图 7.1:绘画分类模式
与接受 2D 图像输入的 2D 卷积 API tf.layers.conv2d 不同,此处将 1D 卷积 API tf.layers.conv1d用于时间卷积(例如绘画)。 默认情况下,在绘画分类模型中,使用三个 1D 卷积层,每个层具有 48、64 和 96 个过滤器,其长度分别为 5、5 和 3。 卷积层之后,将创建 3 个 LSTM 层,每层具有 128 个正向BasicLSTMCell节点和 128 个反向BasicLSTMCell节点,然后将其用于创建动态双向循环神经网络,该网络的输出将发送到最终的完全连接层以计算logits(非标准化的对数概率)。
If you don't have a good understanding of all these details, don't worry; to develop powerful mobile apps using a model built by others, you don't have to understand all the details, but in the next chapter we'll also discuss in greater detail how you can build a RNN model from scratch for stock prediction, and with that, you'll have a better understanding of all the RNN stuff.
在前面提到的有趣的教程中详细描述了简单而优雅的模型以及构建模型的 Python 实现,其源代码位于仓库中。 在继续进行下一部分之前,我们只想说一件事:模型的构建,训练,评估和预测的代码与上一章中看到的代码不同,它使用了称为Estimator的 TensorFlow API,或更准确地说,是自定义Estimator。 如果您对模型实现的详细信息感兴趣,则应该阅读有关创建和使用自定义Estimator的指南。 这个页面的models/samples/core/get_started/custom_estimator.py上的指南的有用源代码。 基本上,首先要实现一个函数,该函数定义模型,指定损失和准确率度量,设置优化器和training操作,然后创建tf.estimator.Estimator类的实例并调用其train,evaluate 和predict方法。 就像您将很快看到的那样,使用Estimator可以简化如何构建,训练和推断神经网络模型,但是由于它是高级 API,因此它还会执行一些更加困难的低级任务,例如找出输入和输出节点名称来推断移动设备。
训练,预测和准备绘画分类模型
训练模型非常简单,但为移动部署准备模型则有些棘手。 在我们开始训练之前,请首先确保您已经在 TensorFlow 根目录中克隆了 TensorFlow 模型库,就像我们在前两章中所做的一样。 然后从这里下载绘画分类训练数据集,大约 1.1GB,创建一个名为rnn_tutorial_data的新文件夹, 并解压缩dataset tar.gz文件。 您将看到 10 个训练 TFRecord 文件和 10 个评估 TFRecord 文件,以及两个带有.classes扩展名的文件,它们具有相同的内容,并且只是该数据集可用于分类的 345 个类别的纯文本,例如"sheep", "skull", "donut", "apple"。
训练绘画分类模型
要训练模型,只需打开终端cd到tensorflow/models/tutorials/rnn/quickdraw ,然后运行以下脚本:
python train_model.py \
--training_data=rnn_tutorial_data/training.tfrecord-?????-of-????? \
--eval_data=rnn_tutorial_data/eval.tfrecord-?????-of-????? \
--model_dir quickdraw_model/ \
--classes_file=rnn_tutorial_data/training.tfrecord.classes
默认情况下,训练步骤为 100k,在我们的 GTX 1070 GPU 上大约需要 6 个小时才能完成训练。 训练完成后,您将在模型目录中看到一个熟悉的文件列表(省略了其他四组model.ckpt*文件):
ls -lt quickdraw_model/
-rw-rw-r-- 1 jeff jeff 164419871 Feb 12 05:56 events.out.tfevents.1518422507.AiLabby
-rw-rw-r-- 1 jeff jeff 1365548 Feb 12 05:56 model.ckpt-100000.meta
-rw-rw-r-- 1 jeff jeff 279 Feb 12 05:56 checkpoint
-rw-rw-r-- 1 jeff jeff 13707200 Feb 12 05:56 model.ckpt-100000.data-00000-of-00001
-rw-rw-r-- 1 jeff jeff 2825 Feb 12 05:56 model.ckpt-100000.index
-rw-rw-r-- 1 jeff jeff 2493402 Feb 12 05:47 graph.pbtxt
drwxr-xr-x 2 jeff jeff 4096 Feb 12 00:11 eval
如果您运行tensorboard --logdir quickdraw_model,然后从浏览器在http://localhost:6006上启动 TensorBoard,您会看到精度达到约 0.55,损失到约 2.0。 如果继续进行约 200k 的训练,则精度将提高到约 0.65,损失将下降到 1.3,如图 7.2 所示:

图 7.2:300k 训练步骤后模型的准确率和损失
现在,我们可以像上一章一样运行freeze_graph.py工具,以生成用于移动设备的模型文件。 但是在执行此操作之前,我们首先来看一下如何在 Python 中使用该模型进行推断,例如上一章中的run_inference.py脚本。
使用绘画分类模型进行预测
看一下models/tutorial/rnn/quickdraw文件夹中的train_model.py文件。 当它开始运行时,将在create_estimator_and_specs函数中创建一个Estimator实例:
estimator = tf.estimator.Estimator(
model_fn=model_fn,
config=run_config,
params=model_params)
传递给Estimator类的关键参数是名为model_fn的模型函数,该函数定义:
- 获取输入张量并创建卷积,RNN 和最终层的函数
- 调用这些函数来构建模型的代码
- 损失,优化器和预测
在返回tf.estimator.EstimatorSpec实例之前,model_fn函数还具有一个名为mode的参数,该参数可以具有以下三个值之一:
tf.estimator.ModeKeys.TRAINtf.estimator.ModeKeys.EVALtf.estimator.ModeKeys.PREDICT
实现train_model.py的方式支持训练和求值模式,但是您不能直接使用它来通过特定的绘画输入进行推理(对绘画进行分类)。 要使用特定输入来测试预测,请按照以下步骤操作:
- 复制
train_model.py,然后将新文件重命名为predict.py-这样您就可以更自由地进行预测了。 - 在
predict.py中,定义[预测]的输入函数,并将features设置为模型期望的绘画输入(连续点的增量,其中第三个数字表示该点是否为笔划的起点) :
def predict_input_fn():
def _input_fn():
features = {'shape': [[16, 3]], 'ink': [[
-0.23137257, 0.31067961, 0\. ,
-0.05490196, 0.1116505 , 0\. ,
0.00784314, 0.09223297, 0\. ,
0.19215687, 0.07766992, 0\. ,
...
0.12156862, 0.05825245, 0\. ,
0\. , -0.06310678, 1\. ,
0\. , 0., 0\. ,
...
0\. , 0., 0\. ,
]]}
features['shape'].append( features['shape'][0])
features['ink'].append( features['ink'][0])
features=dict(features)
dataset = tf.data.Dataset.from_tensor_slices(features)
dataset = dataset.batch(FLAGS.batch_size)
return dataset.make_one_shot_iterator().get_next()
return _input_fn
我们并没有显示所有的点值,但它们是使用 TensorFlow RNN 用于绘画分类的教程中显示的示例猫示例数据创建的,并应用了parse_line函数(请参见教程或models/tutorials/rnn/quickdraw文件夹中的create_dataset.py 细节)。
还要注意,我们使用tf.data.Dataset的make_one_shot_iterator方法创建了一个迭代器,该迭代器从数据集中返回一个示例(在这种情况下,我们在数据集中只有一个示例),与模型在处理大型数据集时,在训练和评估过程中获取数据的方式相同–这就是为什么稍后在模型的图中看到OneShotIterator操作的原因。
- 在主函数中,调用估计器的
predict方法,该方法将生成给定特征的预测,然后打印下一个预测:
predictions = estimator.predict(input_fn=predict_input_fn())
print(next(predictions)['argmax'])
- 在
model_fn函数中,在logits = _add_fc_layers(final_state)之后,添加以下代码:
argmax = tf.argmax(logits, axis=1)
if mode == tf.estimator.ModeKeys.PREDICT:
predictions = {
'argmax': argmax,
'softmax': tf.nn.softmax(logits),
'logits': logits,
}
return tf.estimator.EstimatorSpec(mode, predictions=predictions)
现在,如果您运行predict.py,您将在步骤 2 中获得具有输入数据返回最大值的类 ID。
基本了解如何使用Estimator高级 API 构建的模型进行预测后,我们现在就可以冻结该模型,以便可以在移动设备上使用该模型,这需要我们首先弄清楚输出节点名称应该是什么。
准备绘画分类模型
让我们使用 TensorBoard 看看我们能找到什么。 在我们模型的 TensorBoard 视图的 GRAPHS 部分中,您可以看到,如图 7.3 所示,以红色突出显示的BiasAdd节点是ArgMax操作的输入,用于计算精度,以及 softmax 操作的输入。 我们可以使用SparseSoftmaxCrossEntropyWithLogits(图 7.3 仅显示为SparseSiftnaxCr ...)操作,也可以仅使用Dense/BiasAdd作为输出节点名称,但我们将ArgMax和Dense/BiasAdd用作freeze_graph工具的两个输出节点名称,因此我们可以更轻松地查看最终密集层的输出以及ArgMax结果:

图 7.3:显示模型的可能输出节点名称
用您的graph.pbtxt文件的路径和最新的模型检查点前缀替换--input_graph和--input_checkpoint值后,在 TensorFlow 根目录中运行以下脚本以获取冻结的图:
python tensorflow/python/tools/freeze_graph.py --input_graph=/tmp/graph.pbtxt --input_checkpoint=/tmp/model.ckpt-314576 --output_graph=/tmp/quickdraw_frozen_dense_biasadd_argmax.pb --output_node_names="dense/BiasAdd,ArgMax"
您会看到quickdraw_frozen_dense_biasadd_argmax.pb成功创建。 但是,如果您尝试在 iOS 或 Android 应用中加载模型,则会收到一条错误消息,内容为Could not create TensorFlow Graph: Not found: Op type not registered 'OneShotIterator' in binary. Make sure the Op and Kernel are registered in the binary running in this process.
我们在前面的小节中讨论了OneShotIterator的含义。 回到 TensorBoard GRAPHS部分,我们可以看到OneShotIterator(如图 7.4 所示),该区域以红色突出显示,并且还显示在右下方的信息面板中,在图表的底部,以及上方的几个层次中,有一个 Reshape操作用作第一卷积层的输入:

图 7.4:查找可能的输入节点名称
您可能想知道为什么我们不能使用我们之前使用的技术来解决Not found: Op type not registered 'OneShotIterator'错误,即先使用命令grep 'REGISTER.*"OneShotIterator"' tensorflow/core/ops/*.cc(您将看到输出为tensorflow/core/ops/dataset_ops.cc:REGISTER_OP("OneShotIterator")),然后将tensorflow/core/ops/dataset_ops.cc添加到tf_op_files.txt并重建 TensorFlow 库。 即使这可行,也会使解决方案复杂化,因为现在我们需要向模型提供一些与OneShotIterator相关的数据,而不是以点为单位的直接用户绘画。
此外,在右侧上方一层(图 7.5),还有另一种操作 Squeeze ,它是 rnn_classification 子图的输入:

图 7.5:找出输入节点名称的进一步研究
我们不必担心Reshape右侧的Shape运算,因为它实际上是rnn_classification子图的输出。 因此,所有这些研究背后的直觉是,我们可以使用Reshape和Squeeze作为两个输入节点,然后使用在上一章中看到的transform_graph 工具,我们应该能够删除 Reshape和Squeeze以下的节点,包括OneShotIterator。
现在在 TensorFlow 根目录中运行以下命令:
bazel-bin/tensorflow/tools/graph_transforms/transform_graph --in_graph=/tmp/quickdraw_frozen_dense_biasadd_argmax.pb --out_graph=/tmp/quickdraw_frozen_strip_transformed.pb --inputs="Reshape,Squeeze" --outputs="dense/BiasAdd,ArgMax" --transforms='
strip_unused_nodes(name=Squeeze,type_for_name=int64,shape_for_name="8",name=Reshape,type_for_name=float,shape_for_name="8,16,3")'
在这里,我们为strip_unused_nodes使用了更高级的格式:对于每个输入节点名称(Squeeze和Reshape),我们指定其特定的类型和形状,以避免以后出现模型加载错误。 有关transform_graph工具的strip_unused_nodes的更多详细信息,请参见其上的文档 https://github.com/tensorflow/tensorflow/tree/master/tensorflow/tools/graph_transforms 。
现在在 iOS 或 Android 中加载模型,OneShotIterator错误将消失。 但是,您可能已经学会了预期,但是会出现一个新错误:Could not create TensorFlow Graph: Invalid argument: Input 0 of node IsVariableInitialized was passed int64 from global_step:0 incompatible with expected int64_ref.
我们首先需要了解有关IsVariableInitialized的更多信息。 如果我们回到 TensorBoard GRAPHS标签,我们会在左侧看到一个IsVariableInitialized操作,该操作以红色突出显示并在右侧的信息面板中以global_step作为其输入(图 7.6)。
即使我们不确切知道它的用途,我们也可以确保它与模型推断无关,该模型推断只需要一些输入(图 7.4 和图 7.5)并生成绘画分类作为输出(图 7.3)。 :

图 7.6:查找导致模型加载错误但与模型推断无关的节点
那么,如何摆脱global_step以及其他相关的cond节点,由于它们的隔离性,它们不会被变换图工具剥离掉? 幸运的是,freeze_graph脚本支持这一点 – 仅在其源代码中记录。 我们可以为脚本使用variable_names_blacklist参数来指定应在冻结模型中删除的节点:
python tensorflow/python/tools/freeze_graph.py --input_graph=/tmp/graph.pbtxt --input_checkpoint=/tmp/model.ckpt-314576 --output_graph=/tmp/quickdraw_frozen_long_blacklist.pb --output_node_names="dense/BiasAdd,ArgMax" --variable_names_blacklist="IsVariableInitialized,global_step,global_step/Initializer/zeros,cond/pred_id,cond/read/Switch,cond/read,cond/Switch_1,cond/Merge"
在这里,我们只列出global_step和cond范围内的节点。 现在再次运行transform_graph工具:
bazel-bin/tensorflow/tools/graph_transforms/transform_graph --in_graph=/tmp/quickdraw_frozen_long_blacklist.pb --out_graph=/tmp/quickdraw_frozen_long_blacklist_strip_transformed.pb --inputs="Reshape,Squeeze" --outputs="dense/BiasAdd,ArgMax" --transforms='
strip_unused_nodes(name=Squeeze,type_for_name=int64,shape_for_name="8",name=Reshape,type_for_name=float,shape_for_name="8,16,3")'
在 iOS 或 Android 中加载生成的模型文件quickdraw_frozen_long_blacklist_strip_transformed.pb,您将不再看到 IsVariableInitialized错误。 当然,在 iOS 和 Android 上,您还会看到另一个错误。 加载先前的模型将导致此错误:
Couldn't load model: Invalid argument: No OpKernel was registered to support Op 'RefSwitch' 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]
device='CPU'; T in [DT_BOOL]
[[Node: cond/read/Switch = RefSwitch[T=DT_INT64, _class=["loc:@global_step"], _output_shapes=[[], []]](global_step, cond/pred_id)]]
要解决此错误,我们必须以不同的方式为 iOS 和 Android 构建自定义的 TensorFlow 库。 在下面的 iOS 和 Android 部分中讨论如何执行此操作之前,让我们首先做一件事:将模型转换为映射版本,以便在 iOS 中更快地加载并使用更少的内存:
bazel-bin/tensorflow/contrib/util/convert_graphdef_memmapped_format \
--in_graph=/tmp/quickdraw_frozen_long_blacklist_strip_transformed.pb \
--out_graph=/tmp/quickdraw_frozen_long_blacklist_strip_transformed_memmapped.pb
在 iOS 中使用绘画分类模型
要解决以前的 RefSwitch 错误,无论您是否像在第 2 章,“通过迁移学习对图像分类”和第 6 章,“用自然语言描述图像”或手动构建的 TensorFlow 库,就像在其他章节中一样,我们必须使用一些新技巧。 发生错误的原因是RefSwitch操作需要INT64数据类型,但它不是 TensorFlow 库中内置的已注册数据类型之一,因为默认情况下,要使该库尽可能小,仅包括每个操作的共同数据类型。 我们可能会从 Python 的模型构建端修复此问题,但是在这里,我们仅向您展示如何从 iOS 端修复此问题,当您无权访问源代码来构建模型时,这很有用。
为 iOS 构建自定义的 TensorFlow 库
从tensorflow/contrib/makefile/Makefile打开 Makefile,然后,如果您使用 TensorFlow 1.4,则搜索IOS_ARCH。 对于每种架构(总共 5 种:ARMV7,ARMV7S,ARM64,I386,X86_64),将-D__ANDROID_TYPES_SLIM__ 更改为
-D__ANDROID_TYPES_FULL__。 TensorFlow 1.5(或 1.6/1.7)中的Makefile稍有不同,尽管它仍位于同一文件夹中。 对于 1.5/1.6/1.7,搜索ANDROID_TYPES_SLIM并将其更改为 ANDROID_TYPES_FULL。 现在,通过运行tensorflow/contrib/makefile/build_all_ios.sh重建 TensorFlow 库。 此后,在加载模型文件时,RefSwitch错误将消失。 使用 TensorFlow 库构建并具有完整数据类型支持的应用大小约为 70MB,而使用默认的细长数据类型构建的应用大小为 37MB。
好像还不够,仍然发生另一个模型加载错误:
Could not create TensorFlow Graph: Invalid argument: No OpKernel was registered to support Op 'RandomUniform' with these attrs. Registered devices: [CPU], Registered kernels: <no registered kernels>.
幸运的是,如果您已经阅读了前面的章节,那么您应该非常熟悉如何解决这种错误。 快速回顾一下:首先找出哪些操作和内核文件定义并实现了该操作,然后检查tf_op_files.txt文件中是否包含操作或内核文件,并且应该至少缺少一个文件,从而导致错误 ; 现在只需将操作或内核文件添加到tf_op_files.txt并重建库。 在我们的情况下,运行以下命令:
grep RandomUniform tensorflow/core/ops/*.cc
grep RandomUniform tensorflow/core/kernels/*.cc
您将看到这些文件作为输出:
tensorflow/core/ops/random_grad.cc
tensorflow/core/ops/random_ops.cc:
tensorflow/core/kernels/random_op.cc
tensorflow/contrib/makefile/tf_op_files.txt文件只有前两个文件,因此只需将最后一个tensorflow/core/kernels/random_op.cc添加到 tf_op_files.txt的末尾,然后再次运行tensorflow/contrib/makefile/build_all_ios.sh。
最终,在加载模型时所有错误都消失了,我们可以通过实现应用逻辑来处理用户绘画,将点转换为模型期望的格式并返回分类结果,从而开始获得一些真正的乐趣。
开发 iOS 应用来使用模型
让我们使用 Objective-C 创建一个新的 Xcode 项目,然后从上一章中创建的Image2Text iOS 项目中拖放tensorflow_util.h和tensorflow_util.mm文件。 另外,将两个模型文件quickdraw_frozen_long_blacklist_strip_transformed.pb和quickdraw_frozen_long_blacklist_strip_transformed_memmapped.pb以及training.tfrecord.classes文件从 models/tutorials/rnn/quickdraw/rnn_tutorial_data拖放到QuickDraw项目,然后将training.tfrecord.classes重命名为classes.txt。
还将ViewController.m重命名为ViewController.mm,并在tensorflow_util.h中注释GetTopN函数定义,并在tensorflow_util.mm中注释其实现,因为我们将在ViewController.mm中实现修改后的版本。 您的项目现在应如图 7.7 所示:

图 7.7:显示带有ViewController初始内容的QuickDraw Xcode 项目。
我们现在准备单独处理ViewController.mm,以完成我们的任务。
- 在按图 7.6 设置基本常量和变量以及两个函数原型之后,在
ViewController的viewDidLoad中实例化UIButton,UILabel和UIImageView。 每个 UI 控件都设置有多个NSLayoutConstraint(有关完整的代码列表,请参见源代码仓库)。UIImageView的相关代码如下:
_iv = [[UIImageView alloc] init];
_iv.contentMode = UIViewContentModeScaleAspectFit;
[_iv setTranslatesAutoresizingMaskIntoConstraints:NO];
[self.view addSubview:_iv];
UIImageView将用于显示通过UIBezierPath实现的用户绘画。 同样,初始化两个用于保存每个连续点和用户绘制的所有点的数组:
_allPoints = [NSMutableArray array];
_consecutivePoints = [NSMutableArray array];
- 点击具有初始标题“开始”的按钮后,用户可以开始绘画; 按钮标题更改为“重新启动”,并进行了其他一些重置:
- (IBAction)btnTapped:(id)sender {
_canDraw = YES;
[_btn setTitle:@"Restart" forState:UIControlStateNormal];
[_lbl setText:@""];
_iv.image = [UIImage imageNamed:@""];
[_allPoints removeAllObjects];
}
- 为了处理用户绘画,我们首先实现
touchesBegan方法:
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
if (!_canDraw) return;
[_consecutivePoints removeAllObjects];
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView:self.view];
[_consecutivePoints addObject:[NSValue valueWithCGPoint:point]];
_iv.image = [self createDrawingImageInRect:_iv.frame];
}
然后是touchesMoved方法:
- (void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
if (!_canDraw) return;
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView:self.view];
[_consecutivePoints addObject:[NSValue valueWithCGPoint:point]];
_iv.image = [self createDrawingImageInRect:_iv.frame];
}
最后是touchesEnd方法:
- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
if (!_canDraw) return;
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView:self.view];
[_consecutivePoints addObject:[NSValue valueWithCGPoint:point]];
[_allPoints addObject:[NSArray arrayWithArray:_consecutivePoints]];
[_consecutivePoints removeAllObjects];
_iv.image = [self createDrawingImageInRect:_iv.frame];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
std::string classes = getDrawingClassification(_allPoints);
dispatch_async(dispatch_get_main_queue(), ^{
NSString *c = [NSString stringWithCString:classes.c_str() encoding:[NSString defaultCStringEncoding]];
[_lbl setText:c];
});
});
}
这里的代码很容易解释,除了createDrawingImageInRect和getDrawingClassification这两种方法外,我们将在后面介绍。
- 方法
createDrawingImageInRect使用UIBezierPath'smoveToPoint和addLineToPoint方法显示用户绘画。 它首先通过触摸事件准备所有完成的笔划,并将所有点存储在_allPoints数组中:
- (UIImage *)createDrawingImageInRect:(CGRect)rect
{
UIGraphicsBeginImageContextWithOptions(CGSizeMake(rect.size.width, rect.size.height), NO, 0.0);
UIBezierPath *path = [UIBezierPath bezierPath];
for (NSArray *cp in _allPoints) {
bool firstPoint = TRUE;
for (NSValue *pointVal in cp) {
CGPoint point = pointVal.CGPointValue;
if (firstPoint) {
[path moveToPoint:point];
firstPoint = FALSE;
}
else
[path addLineToPoint:point];
}
}
然后,它准备当前正在进行的笔划中的所有点,并存储在_consecutivePoints中:
bool firstPoint = TRUE;
for (NSValue *pointVal in _consecutivePoints) {
CGPoint point = pointVal.CGPointValue;
if (firstPoint) {
[path moveToPoint:point];
firstPoint = FALSE;
}
else
[path addLineToPoint:point];
}
最后,它执行实际绘画,并将绘画作为UIImage返回,以显示在UIImageView中:
path.lineWidth = 6.0;
[[UIColor blackColor] setStroke];
[path stroke];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
getDrawingClassification首先使用与上一章相同的代码来加载模型或其映射版本:
std::string getDrawingClassification(NSMutableArray *allPoints) {
if (!_modelLoaded) {
tensorflow::Status load_status;
if (USEMEMMAPPED) {
load_status = LoadMemoryMappedModel(MODEL_FILE_MEMMAPPED, MODEL_FILE_TYPE, &tf_session, &tf_memmapped_env);
}
else {
load_status = LoadModel(MODEL_FILE, MODEL_FILE_TYPE, &tf_session);
}
if (!load_status.ok()) {
LOG(FATAL) << "Couldn't load model: " << load_status;
return "";
}
_modelLoaded = YES;
}
然后,它获得总点数并分配一个浮点数数组,然后调用另一个函数normalizeScreenCoordinates(稍后将介绍)将点转换为模型期望的格式:
if ([allPoints count] == 0) return "";
int total_points = 0;
for (NSArray *cp in allPoints) {
total_points += cp.count;
}
float *normalized_points = new float[total_points * 3];
normalizeScreenCoordinates(allPoints, normalized_points);
接下来,我们定义输入和输出节点名称,并创建一个包含总点数的张量:
std::string input_name1 = "Reshape";
std::string input_name2 = "Squeeze";
std::string output_name1 = "dense/BiasAdd";
std::string output_name2 = "ArgMax"
const int BATCH_SIZE = 8;
tensorflow::Tensor seqlen_tensor(tensorflow::DT_INT64, tensorflow::TensorShape({BATCH_SIZE}));
auto seqlen_mapped = seqlen_tensor.tensor<int64_t, 1>();
int64_t* seqlen_mapped_data = seqlen_mapped.data();
for (int i=0; i<BATCH_SIZE; i++) {
seqlen_mapped_data[i] = total_points;
}
请注意,在运行train_model.py来训练模型时,我们必须使用与BATCH_SIZE相同的BATCH_SIZE,默认情况下为 8。
保存所有转换点值的另一个张量在这里创建:
tensorflow::Tensor points_tensor(tensorflow::DT_FLOAT, tensorflow::TensorShape({8, total_points, 3}));
auto points_tensor_mapped = points_tensor.tensor<float, 3>();
float* out = points_tensor_mapped.data();
for (int i=0; i<BATCH_SIZE; i++) {
for (int j=0; j<total_points*3; j++)
out[i*total_points*3+j] = normalized_points[j];
}
- 现在,我们运行模型并获得预期的输出:
std::vector<tensorflow::Tensor> outputs;
tensorflow::Status run_status = tf_session->Run({{input_name1, points_tensor}, {input_name2, seqlen_tensor}}, {output_name1, output_name2}, {}, &outputs);
if (!run_status.ok()) {
LOG(ERROR) << "Getting model failed:" << run_status;
return "";
}
tensorflow::string status_string = run_status.ToString();
tensorflow::Tensor* logits_tensor = &outputs[0];
- 使用修改后的
GetTopN版本并解析logits获得最佳结果:
const int kNumResults = 5;
const float kThreshold = 0.1f;
std::vector<std::pair<float, int> > top_results;
const Eigen::TensorMap<Eigen::Tensor<float, 1, Eigen::RowMajor>, Eigen::Aligned>& logits = logits_tensor->flat<float>();
GetTopN(logits, kNumResults, kThreshold, &top_results);
string result = "";
for (int i=0; i<top_results.size(); i++) {
std::pair<float, int> r = top_results[i];
if (result == "")
result = classes[r.second];
else result += ", " + classes[r.second];
}
- 通过将
logits值转换为 softmax 值来更改GetTopN,然后返回顶部 softmax 值及其位置:
float sum = 0.0;
for (int i = 0; i < CLASS_COUNT; ++i) {
sum += expf(prediction(i));
}
for (int i = 0; i < CLASS_COUNT; ++i) {
const float value = expf(prediction(i)) / sum;
if (value < threshold) {
continue;
}
top_result_pq.push(std::pair<float, int>(value, i));
if (top_result_pq.size() > num_results) {
top_result_pq.pop();
}
}
- 最后,
normalizeScreenCoordinates函数将其在触摸事件中捕获的屏幕坐标中的所有点转换为增量差异 – 这几乎是这个页面中的 Python 方法parse_line的一部分:
void normalizeScreenCoordinates(NSMutableArray *allPoints, float *normalized) {
float lowerx=MAXFLOAT, lowery=MAXFLOAT, upperx=-MAXFLOAT, uppery=-MAXFLOAT;
for (NSArray *cp in allPoints) {
for (NSValue *pointVal in cp) {
CGPoint point = pointVal.CGPointValue;
if (point.x < lowerx) lowerx = point.x;
if (point.y < lowery) lowery = point.y;
if (point.x > upperx) upperx = point.x;
if (point.y > uppery) uppery = point.y;
}
}
float scalex = upperx - lowerx;
float scaley = uppery - lowery;
int n = 0;
for (NSArray *cp in allPoints) {
int m=0;
for (NSValue *pointVal in cp) {
CGPoint point = pointVal.CGPointValue;
normalized[n*3] = (point.x - lowerx) / scalex;
normalized[n*3+1] = (point.y - lowery) / scaley;
normalized[n*3+2] = (m ==cp.count-1 ? 1 : 0);
n++; m++;
}
}
for (int i=0; i<n-1; i++) {
normalized[i*3] = normalized[(i+1)*3] - normalized[i*3];
normalized[i*3+1] = normalized[(i+1)*3+1] - normalized[i*3+1];
normalized[i*3+2] = normalized[(i+1)*3+2];
}
}
现在,您可以在 iOS 模拟器或设备中运行该应用,开始绘画,并查看模型认为您正在绘画的内容。 图 7.8 显示了一些绘画和分类结果–不是最佳绘画,而是整个过程!

图 7.8:在 iOS 上显示绘画和分类结果
在 Android 中使用绘画分类模型
现在该看看我们如何在 Android 中加载和使用该模型。 在之前的章节中,我们通过使用 Android 应用的build.gradle文件并添加了一行 compile 'org.tensorflow:tensorflow-android:+'仅添加了 TensorFlow 支持。 与 iOS 相比,我们必须构建一个自定义的 TensorFlow 库来修复不同的模型加载或运行错误(例如,在第 3 章,“检测对象及其位置”中,第四章,“变换具有惊人艺术风格的图片”和第五章,“了解简单的语音命令”),Android 的默认 TensorFlow 库对注册的操作和数据类型有更好的支持,这可能是因为 Android 是 Google 的一等公民,而 iOS 是第二名,甚至是第二名。
事实是,当我们处理各种惊人的模型时,我们不得不面对不可避免的问题只是时间问题:我们必须手动为 Android 构建 TensorFlow 库,以修复默认 TensorFlow 库中的一些根本无法应对的错误。 No OpKernel was registered to support Op 'RefSwitch' with these attrs.错误就是这样的错误之一。 对于乐观的开发人员来说,这仅意味着另一种向您的技能组合中添加新技巧的机会。
为 Android 构建自定义 TensorFlow 库
请按照以下步骤手动为 Android 构建自定义的 TensorFlow 库:
- 在您的 TensorFlow 根目录中,有一个名为
WORKSPACE的文件。 编辑它,并使android_sdk_repository和android_ndk_repository看起来像以下设置(用您自己的设置替换build_tools_version以及 SDK 和 NDK 路径):
android_sdk_repository(
name = "androidsdk",
api_level = 23,
build_tools_version = "26.0.1",
path = "$HOME/Library/Android/sdk",
)
android_ndk_repository(
name="androidndk",
path="$HOME/Downloads/android-ndk-r15c",
api_level=14)
- 如果您还使用过本书中的 iOS 应用,并且已将
tensorflow/core/platform/default/mutex.h从#include "nsync_cv.h"和#include "nsync_mu.h"更改为#include "nsync/public/nsync_cv.h"和#include "nsync/public/nsync_mu.h",请参见第 3 章, “检测对象及其位置” 时,您需要将其更改回以成功构建 TensorFlow Android 库(此后,当您使用手动构建的 TensorFlow 库在 Xcode 和 iOS 应用上工作时,需要先添加nsync/public这两个标头。
Changing tensorflow/core/platform/default/mutex.h back and forth certainly is not an ideal solution. It's supposed to be just as a workaround. As it only needs to be changed when you start using a manually built TensorFlow iOS library or when you build a custom TensorFlow library, we can live with it for now.
- 如果您具有支持 x86 CPU 的虚拟模拟器或 Android 设备,请运行以下命令来构建本机 TensorFlow 库:
bazel build -c opt --copt="-D__ANDROID_TYPES_FULL__" //tensorflow/contrib/android:libtensorflow_inference.so \
--crosstool_top=//external:android/crosstool \
--host_crosstool_top=@bazel_tools//tools/cpp:toolchain \
--cpu=x86_64
如果您的 Android 设备像大多数 Android 设备一样支持 armeabi-v7a,请运行以下命令:
bazel build -c opt --copt="-D__ANDROID_TYPES_FULL__" //tensorflow/contrib/android:libtensorflow_inference.so \
--crosstool_top=//external:android/crosstool \
--host_crosstool_top=@bazel_tools//tools/cpp:toolchain \
--cpu=armeabi-v7a
在 Android 应用中使用手动构建的本机库时,您需要让该应用知道该库是针对哪个 CPU 指令集(也称为应用二进制接口(ABI))构建的。 Android 支持两种主要的 ABI:ARM 和 X86,而armeabi-v7a是 Android 上最受欢迎的 ABI。 要找出您的设备或仿真器使用的是哪个 ABI,请运行adb -s <device_id> shell getprop ro.product.cpu.abi。 例如,此命令为我的 Nexus 7 平板电脑返回armeabi-v7a,为我的模拟器返回x86_64。
如果您具有支持 x86_64 的虚拟仿真器以在开发过程中进行快速测试,并且在设备上进行最终性能测试,则可能要同时构建两者。
构建完成后,您将在bazel-bin/tensorflow/contrib/android文件夹中看到 TensorFlow 本机库文件libtensorflow_inference.so。 将其拖到android/app/src/main/jniLibs/armeabi-v7a或 android/app/src/main/jniLibs/x86_64的app文件夹中,如图 7.9 所示:

图 7.9:显示 TensorFlow 本机库文件
- 通过运行以下命令构建 TensorFlow 本机库的 Java 接口:
bazel build //tensorflow/contrib/android:android_tensorflow_inference_java
这将在bazel-bin/tensorflow/contrib/android处生成文件libandroid_tensorflow_inference_java.jar。 将文件移动到 android/app/lib文件夹,如图 7.10 所示:

图 7.10:将 Java 接口文件显示到 TensorFlow 库
现在,我们准备在 Android 中编码和测试模型。
开发一个 Android 应用来使用该模型
请按照以下步骤使用 TensorFlow 库和我们先前构建的模型创建一个新的 Android 应用:
- 在 Android Studio 中,创建一个名为 QuickDraw 的新 Android 应用,接受所有默认设置。 然后在应用的
build.gradle中,将compile files('libs/libandroid_tensorflow_inference_java.jar')添加到依赖项的末尾。 像以前一样创建一个新的assets文件夹,并将quickdraw_frozen_long_blacklist_strip_transformed.pb和classes.txt拖放到该文件夹中。 - 创建一个名为
QuickDrawView的新 Java 类,该类扩展了View,并如下设置字段及其构造器:
public class QuickDrawView extends View {
private Path mPath;
private Paint mPaint, mCanvasPaint;
private Canvas mCanvas;
private Bitmap mBitmap;
private MainActivity mActivity;
private List<List<Pair<Float, Float>>> mAllPoints = new ArrayList<List<Pair<Float, Float>>>();
private List<Pair<Float, Float>> mConsecutivePoints = new ArrayList<Pair<Float, Float>>();
public QuickDrawView(Context context, AttributeSet attrs) {
super(context, attrs);
mActivity = (MainActivity) context;
setPathPaint();
}
mAllPoints用于保存mConsecutivePoints的列表。 QuickDrawView用于主要活动的布局中,以显示用户的绘画。
- 如下定义
setPathPaint方法:
private void setPathPaint() {
mPath = new Path();
mPaint = new Paint();
mPaint.setColor(0xFF000000);
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(18);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeJoin(Paint.Join.ROUND);
mCanvasPaint = new Paint(Paint.DITHER_FLAG);
}
添加两个实例化Bitmap和Canvas对象并向用户显示在画布上绘画的重写方法:
@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
mCanvas = new Canvas(mBitmap);
}
@Override protected void onDraw(Canvas canvas) {
canvas.drawBitmap(mBitmap, 0, 0, mCanvasPaint);
canvas.drawPath(mPath, mPaint);
}
- 覆盖方法
onTouchEvent用于填充mConsecutivePoints和mAllPoints,调用画布的drawPath方法,使图无效(以调用onDraw方法),以及(每次使用MotionEvent.ACTION_UP完成笔划线),以启动一个新线程以使用模型对绘画进行分类:
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!mActivity.canDraw()) return true;
float x = event.getX();
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mConsecutivePoints.clear();
mConsecutivePoints.add(new Pair(x, y));
mPath.moveTo(x, y);
break;
case MotionEvent.ACTION_MOVE:
mConsecutivePoints.add(new Pair(x, y));
mPath.lineTo(x, y);
break;
case MotionEvent.ACTION_UP:
mConsecutivePoints.add(new Pair(x, y));
mAllPoints.add(new ArrayList<Pair<Float, Float>>
(mConsecutivePoints));
mCanvas.drawPath(mPath, mPaint);
mPath.reset();
Thread thread = new Thread(mActivity);
thread.start();
break;
default:
return false;
}
invalidate();
return true;
}
- 定义两个将由
MainActivity调用的公共方法,以获取所有点并在用户点击重新启动按钮后重置绘画:
public List<List<Pair<Float, Float>>> getAllPoints() {
return mAllPoints;
}
public void clearAllPointsAndRedraw() {
mBitmap = Bitmap.createBitmap(mBitmap.getWidth(),
mBitmap.getHeight(), Bitmap.Config.ARGB_8888);
mCanvas = new Canvas(mBitmap);
mCanvasPaint = new Paint(Paint.DITHER_FLAG);
mCanvas.drawBitmap(mBitmap, 0, 0, mCanvasPaint);
setPathPaint();
invalidate();
mAllPoints.clear();
}
- 现在打开
MainActivity,并使其实现Runnable及其字段,如下所示:
public class MainActivity extends AppCompatActivity implements Runnable {
private static final String MODEL_FILE = "file:///android_asset/quickdraw_frozen_long_blacklist_strip_transformed.pb";
private static final String CLASSES_FILE = "file:///android_asset/classes.txt";
private static final String INPUT_NODE1 = "Reshape";
private static final String INPUT_NODE2 = "Squeeze";
private static final String OUTPUT_NODE1 = "dense/BiasAdd";
private static final String OUTPUT_NODE2 = "ArgMax";
private static final int CLASSES_COUNT = 345;
private static final int BATCH_SIZE = 8;
private String[] mClasses = new String[CLASSES_COUNT];
private QuickDrawView mDrawView;
private Button mButton;
private TextView mTextView;
private String mResult = "";
private boolean mCanDraw = false;
private TensorFlowInferenceInterface mInferenceInterface;
- 在主布局文件
activity_main.xml中,除了我们之前所做的TextView和Button之外,还创建一个QuickDrawView元素:
<com.ailabby.quickdraw.QuickDrawView
android:id="@+id/drawview"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
- 返回
MainActivity; 在其onCreate方法中,将 UI 元素 ID 与字段绑定,为启动/重启按钮设置点击监听器。 然后将classes.txt文件读入字符串数组:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mDrawView = findViewById(R.id.drawview);
mButton = findViewById(R.id.button);
mTextView = findViewById(R.id.textview);
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mCanDraw = true;
mButton.setText("Restart");
mTextView.setText("");
mDrawView.clearAllPointsAndRedraw();
}
});
String classesFilename = CLASSES_FILE.split("file:///android_asset/")[1];
BufferedReader br = null;
int linenum = 0;
try {
br = new BufferedReader(new InputStreamReader(getAssets().open(classesFilename)));
String line;
while ((line = br.readLine()) != null) {
mClasses[linenum++] = line;
}
br.close();
} catch (IOException e) {
throw new RuntimeException("Problem reading classes file!" , e);
}
}
- 然后从线程的
run方法中调用同步方法classifyDrawing:
public void run() {
classifyDrawing();
}
private synchronized void classifyDrawing() {
try {
double normalized_points[] = normalizeScreenCoordinates();
long total_points = normalized_points.length / 3;
float[] floatValues = new float[normalized_points.length*BATCH_SIZE];
for (int i=0; i<normalized_points.length; i++) {
for (int j=0; j<BATCH_SIZE; j++)
floatValues[j*normalized_points.length + i] = (float)normalized_points[i];
}
long[] seqlen = new long[BATCH_SIZE];
for (int i=0; i<BATCH_SIZE; i++)
seqlen[i] = total_points;
即将实现的normalizeScreenCoordinates方法将用户绘画点转换为模型期望的格式。 floatValues和seqlen将被输入模型。 请注意,由于模型需要这些确切的数据类型(float和int64),因此我们必须在floatValues中使用float在seqlen中使用long,否则在使用模型时会发生运行时错误。
- 创建一个与 TensorFlow 库的 Java 接口以加载模型,向模型提供输入并获取输出:
AssetManager assetManager = getAssets();
mInferenceInterface = new TensorFlowInferenceInterface(assetManager, MODEL_FILE);
mInferenceInterface.feed(INPUT_NODE1, floatValues, BATCH_SIZE, total_points, 3);
mInferenceInterface.feed(INPUT_NODE2, seqlen, BATCH_SIZE);
float[] logits = new float[CLASSES_COUNT * BATCH_SIZE];
float[] argmax = new float[CLASSES_COUNT * BATCH_SIZE];
mInferenceInterface.run(new String[] {OUTPUT_NODE1, OUTPUT_NODE2}, false);
mInferenceInterface.fetch(OUTPUT_NODE1, logits);
mInferenceInterface.fetch(OUTPUT_NODE1, argmax);
- 归一化所提取的
logits概率并以降序对其进行排序:
double sum = 0.0;
for (int i=0; i<CLASSES_COUNT; i++)
sum += Math.exp(logits[i]);
List<Pair<Integer, Float>> prob_idx = new ArrayList<Pair<Integer, Float>>();
for (int j = 0; j < CLASSES_COUNT; j++) {
prob_idx.add(new Pair(j, (float)(Math.exp(logits[j]) / sum) ));
}
Collections.sort(prob_idx, new Comparator<Pair<Integer, Float>>() {
@Override
public int compare(final Pair<Integer, Float> o1, final Pair<Integer, Float> o2) {
return o1.second > o2.second ? -1 : (o1.second == o2.second ? 0 : 1);
}
});
获取前五个结果并将其显示在TextView中:
mResult = "";
for (int i=0; i<5; i++) {
if (prob_idx.get(i).second > 0.1) {
if (mResult == "") mResult = "" + mClasses[prob_idx.get(i).first];
else mResult = mResult + ", " + mClasses[prob_idx.get(i).first];
}
}
runOnUiThread(
new Runnable() {
@Override
public void run() {
mTextView.setText(mResult);
}
});
- 最后,实现
normalizeScreenCoordinates方法,它是 iOS 实现的便捷端口:
private double[] normalizeScreenCoordinates() {
List<List<Pair<Float, Float>>> allPoints = mDrawView.getAllPoints();
int total_points = 0;
for (List<Pair<Float, Float>> cp : allPoints) {
total_points += cp.size();
}
double[] normalized = new double[total_points * 3];
float lowerx=Float.MAX_VALUE, lowery=Float.MAX_VALUE, upperx=-Float.MAX_VALUE, uppery=-Float.MAX_VALUE;
for (List<Pair<Float, Float>> cp : allPoints) {
for (Pair<Float, Float> p : cp) {
if (p.first < lowerx) lowerx = p.first;
if (p.second < lowery) lowery = p.second;
if (p.first > upperx) upperx = p.first;
if (p.second > uppery) uppery = p.second;
}
}
float scalex = upperx - lowerx;
float scaley = uppery - lowery;
int n = 0;
for (List<Pair<Float, Float>> cp : allPoints) {
int m = 0;
for (Pair<Float, Float> p : cp) {
normalized[n*3] = (p.first - lowerx) / scalex;
normalized[n*3+1] = (p.second - lowery) / scaley;
normalized[n*3+2] = (m ==cp.size()-1 ? 1 : 0);
n++; m++;
}
}
for (int i=0; i<n-1; i++) {
normalized[i*3] = normalized[(i+1)*3] - normalized[i*3];
normalized[i*3+1] = normalized[(i+1)*3+1] -
normalized[i*3+1];
normalized[i*3+2] = normalized[(i+1)*3+2];
}
return normalized;
}
在您的 Android 模拟器或设备上运行该应用,并享受分类结果的乐趣。 您应该看到类似图 7.11 的内容:

图 7.11:在 Android 上显示绘画和分类结果
既然您已经了解了训练 Quick Draw 模型的全过程,并在 iOS 和 Android 应用中使用了它,那么您当然可以微调训练方法,使其更加准确,并改善移动应用的乐趣。
在本章我们不得不结束有趣旅程之前的最后一个提示是,如果您使用错误的 ABI 构建适用于 Android 的 TensorFlow 本机库,您仍然可以从 Android Studio 构建和运行该应用,但将出现运行时错误java.lang.RuntimeException: Native TF methods not found; check that the correct native libraries are present in the APK.,这意味着您的应用的jniLibs文件夹中没有正确的 TensorFlow 本机库(图 7.9)。 要找出jniLibs内特定 ABI 文件夹中是否缺少该文件,可以从Android Studio | View | Tool Windows中打开Device File Explorer,然后选择设备的data | app | package | lib来查看,如图 7.12 所示。 如果您更喜欢命令行,则也可以使用adb工具找出来。

图 7.12:使用设备文件资源管理器检出 TensorFlow 本机库文件
总结
在本章中,我们首先描述了绘画分类模型的工作原理,然后介绍了如何使用高级 TensorFlow Estimator API 训练这种模型。 我们研究了如何编写 Python 代码以使用经过训练的模型进行预测,然后详细讨论了如何找到正确的输入和输出节点名称以及如何以正确的方式冻结和转换模型以使移动应用可以使用它。 我们还提供了一种新方法来构建新的 TensorFlow 自定义 iOS 库,并提供了一个逐步教程,以构建适用于 Android 的 TensorFlow 自定义库,以修复使用模型时的运行时错误。 最后,我们展示了 iOS 和 Android 代码,这些代码捕获并显示用户绘画,将其转换为模型所需的数据,并处理和呈现模型返回的分类结果。 希望您在漫长的旅途中学到了很多东西。
到目前为止,除了来自其他开放源代码项目的几个模型以外,所有由我们自己进行预训练或训练的模型,我们在 iOS 和 Android 应用中使用的都是 TensorFlow 开放源代码项目,当然,该项目提供了大量强大的模型,其中一些模型在强大的 GPU 上进行了数周的训练。 但是,如果您有兴趣从头开始构建自己的模型,并且还对本章中使用和应用的强大 RNN 模型以及概念感到困惑,那么下一章就是您所需要的:我们将讨论如何从头开始构建自己的 RNN 模型并在移动应用中使用它,从而带来另一种乐趣-从股市中赚钱-至少我们会尽力做到这一点。 当然,没有人能保证您每次都能从每次股票交易中获利,但是至少让我们看看我们的 RNN 模型如何帮助我们提高这样做的机会。
八、用 RNN 预测股价
如果在上一章中在移动设备上玩过涂鸦和构建(并运行模型以识别涂鸦),当您在股市上赚钱时会感到很开心,而如果您不认真的话会变得很认真。 一方面,股价是时间序列数据,一系列离散时间数据,而处理时间序列数据的最佳深度学习方法是 RNN,这是我们在前两章中使用的方法。 AurélienGéron 在他的畅销书《Scikit-Learn 和 TensorFlow 机器学习实战》中,建议使用 RNN“分析时间序列数据,例如股票价格,并告诉您何时买卖”。 另一方面,其他人则认为股票的过去表现无法预测其未来收益,因此,随机选择的投资组合的表现与专家精心挑选的股票一样好。 实际上,Keras(在 TensorFlow 和其他几个库之上运行的非常受欢迎的高级深度学习库)的作者 FrançoisChollet 在他的畅销书《Python 深度学习》中表示,使用 RNN。 仅用公开数据来击败市场是“一项非常困难的努力,您可能会浪费时间和资源,而无所作为。”
因此,冒着“可能”浪费我们时间和资源的风险,但是可以肯定的是,我们至少将了解更多有关 RNN 的知识,以及为什么有可能比随机 50% 的策略更好地预测股价,我们将首先概述如何使用 RNN 进行股票价格预测,然后讨论如何使用 TensorFlow API 构建 RNN 模型来预测股票价格,以及如何使用易于使用的 Keras API 来为价格预测构建 RNN LSTM 模型。 我们将测试这些模型是否可以击败随机的买入或卖出策略。 如果我们对我们的模型感到满意,以提高我们在市场上的领先优势,或者只是出于专有技术的目的,我们将了解如何冻结并准备 TensorFlow 和 Keras 模型以在 iOS 和 Android 应用上运行。 如果该模型可以提高我们的机会,那么我们支持该模型的移动应用可以在任何时候,无论何时何地做出买或卖决定。 感觉有点不确定和兴奋? 欢迎来到市场。
总之,本章将涵盖以下主题:
- RNN 和股价预测:什么以及如何
- 使用 TensorFlow RNN API 进行股价预测
- 使用 Keras RNN LSTM API 进行股价预测
- 在 iOS 上运行 TensorFlow 和 Keras 模型
- 在 Android 上运行 TensorFlow 和 Keras 模型
RNN 和股价预测 – 什么以及如何
前馈网络(例如密集连接的网络)没有内存,无法将每个输入视为一个整体。 例如,表示为像素向量的图像输入在单个步骤中由前馈网络处理。 但是,使用具有内存的网络可以更好地处理时间序列数据,例如最近 10 或 20 天的股价。 假设过去 10 天的价格为X1, X2, ..., X10,其中X1为最早的和X10为最晚,然后将所有 10 天价格视为一个序列输入,并且当 RNN 处理此类输入时,将发生以下步骤:
- 按顺序连接到第一个元素
X1的特定 RNN 单元处理X1并获取其输出y1 - 在序列输入中,连接到下一个元素
X2的另一个 RNN 单元使用X2以及先前的输出y1, 获得下一个输出y2 - 重复该过程:在时间步长使用 RNN 单元处理输入序列中的
Xi元素时,先前的输出y[i-1],在时间步i-1与Xi一起使用,以在时间步i生成新的输出yi。
因此,在时间步长i的每个yi输出,都具有有关输入序列中直到时间步长i以及包括时间步长i的所有元素的信息:X1, X2, ..., X[i-1]和Xi。 在 RNN 训练期间,预测价格y1, y2, ..., y9和y10的每个时间步长与每个时间步长的真实目标价格进行比较,即X2, X3, ..., X10和X11和损失函数因此被定义并用于优化以更新网络参数。 训练完成后,在预测期间,将X11用作输入序列的预测,X1, X2, ..., X10。
这就是为什么我们说 RNN 有内存。 RNN 对于处理股票价格数据似乎很有意义,因为直觉是,今天(以及明天和后天等等)的股票价格可能会受其前N天的价格影响。
LSTM 只是解决 RNN 已知梯度消失问题的一种 RNN,我们在第 6 章,“用自然语言描述图像”中引入了 LSTM。 基本上,在训练 RNN 模型的过程中,,如果到 RNN 的输入序列的时间步太长,则使用反向传播更新较早时间步的网络权重可能会得到 0 的梯度值, 导致没有学习发生。 例如,当我们使用 50 天的价格作为输入,并且如果使用 50 天甚至 40 天的时间步长变得太长,则常规 RNN 将是不可训练的。 LSTM 通过添加一个长期状态来解决此问题,该状态决定可以丢弃哪些信息以及需要在许多时间步骤中存储和携带哪些信息。
可以很好地解决梯度消失问题的另一种 RNN 被称为门控循环单元(GRU),它稍微简化了标准 LSTM 模型,并且越来越受欢迎。 TensorFlow 和 Keras API 均支持基本的 RNN 和 LSTM/GRU 模型。 在接下来的两部分中,您将看到使用 RNN 和标准 LSTM 的具体 TensorFlow 和 Keras API,并且可以在代码中简单地将LSTM替换为GRU,以将使用 GRU 模型的结果与 RNN 和标准 LSTM 模型比较。
三种常用技术可以使 LSTM 模型表现更好:
- 堆叠 LSTM 层并增加层中神经元的数量:如果不产生过拟合,通常这将导致功能更强大,更准确的网络模型。 如果还没有,那么您绝对应该玩 TensorFlow Playground来体验一下。
- 使用丢弃处理过拟合。 删除意味着随机删除层中的隐藏单元和输入单元。
- 使用双向 RNN 在两个方向(常规方向和反向方向)处理每个输入序列,希望检测出可能被常规单向 RNN 忽略的模式。
所有这些技术已经实现,并且可以在 TensorFlow 和 Keras API 中轻松访问。
那么,我们如何使用 RNN 和 LSTM 测试股价预测? 我们将在这个页面上使用免费的 API 收集特定股票代码的每日股票价格数据,将其解析为训练集和测试集,并每次向 RNN/LSTM 模型提供一批训练输入(每个训练输入有 20 个时间步长,即,连续 20 天的价格),对模型进行训练,然后进行测试以查看模型在测试数据集中的准确率。 我们将同时使用 TensorFlow 和 Keras API 进行测试,并比较常规 RNN 和 LSTM 模型之间的差异。 我们还将测试三个略有不同的序列输入和输出,看看哪个是最好的:
- 根据过去
N天预测一天的价格 - 根据过去
N天预测M天的价格 - 基于将过去
N天移动 1 并使用预测序列的最后输出作为第二天的预测价格进行预测
现在让我们深入研究 TensorFlow RNN API 并进行编码以训练模型来预测股票价格,以查看其准确率如何。
将 TensorFlow RNN API 用于股价预测
首先,您需要在这里索取免费的 API 密钥,以便获取任何股票代码的股价数据。 取得 API 密钥后,打开终端并运行以下命令(将<your_api_key>替换为您自己的密钥后)以获取 Amazon(amzn)和 Google(goog)的每日股票数据,或将它们替换为你感兴趣的任何符号:
curl -o daily_amzn.csv "https://www.alphavantage.co/query?function=TIME_SERIES_DAILY&symbol=amzn&apikey=<your_api_key>&datatype=csv&outputsize=full"
curl -o daily_goog.csv "https://www.alphavantage.co/query?function=TIME_SERIES_DAILY&symbol=goog&apikey=<your_api_key>&datatype=csv&outputsize=full"
这将生成一个daily_amzn.csv或daily_goog.csv csv 文件 ,其顶行为“时间戳,开盘,高位,低位,收盘,交易量”,这些行的其余部分作为每日股票信息。 我们只关心收盘价,因此运行以下命令以获取所有收盘价:
cut -d ',' -f 5 daily_amzn.csv | tail -n +2 > amzn.txt
cut -d ',' -f 5 daily_goog.csv | tail -n +2 > goog.txt
截至 2018 年 2 月 26 日,amzn.txt或goog.txt中的行数为 4,566 或 987,这是亚马逊或 Google 的交易天数。 现在,让我们看一下使用 TensorFlow RNN API 训练和预测模型的完整 Python 代码。
在 TensorFlow 中训练 RNN 模型
- 导入所需的 Python 包并定义一些常量:
import numpy as np
import tensorflow as tf
from tensorflow.contrib.rnn import *
import matplotlib.pyplot as plt
num_neurons = 100
num_inputs = 1
num_outputs = 1
symbol = 'goog' # amzn
epochs = 500
seq_len = 20
learning_rate = 0.001
NumPy 是用于 N 维数组操作的最受欢迎的 Python 库,而 Matplotlib 是领先的 Python 2D 绘图库。 我们将使用 numpy 处理数据集,并使用 Matplotlib 可视化股票价格和预测。 num_neurons是 RNN(或更准确地说是 RNN 单元)在每个时间步长上的神经元数量-每个神经元在该时间步长上都接收输入序列的输入元素,并从前一个时间步长上接收输出。 num_inputs和num_outputs指定每个时间步长的输入和输出数量-我们将从每个时间步长的 20 天输入序列中将一个股票价格提供给带有num_neurons神经元的 RNN 单元,并在每个步骤期望一个预测的股票输出。 seq_len是时间步数。 因此,我们将使用 Google 的 20 天股票价格作为输入序列,并将这些输入发送给具有 100 个神经元的 RNN 单元。
- 打开并读取包含所有价格的文本文件,将价格解析为
float数字列表,颠倒列表顺序,以便最早的价格首先开始,然后每次添加seq_len+1值(第一个seq_len值将是 RNN 的输入序列,最后的seq_len值将是目标输出序列),从列表中的第一个开始,每次移动 1 直到列表的末尾,直到一个 numpyresult数组:
f = open(symbol + '.txt', 'r').read()
data = f.split('\n')[:-1] # get rid of the last '' so float(n) works
data.reverse()
d = [float(n) for n in data]
result = []
for i in range(len(d) - seq_len - 1):
result.append(d[i: i + seq_len + 1])
result = np.array(result)
result数组现在包含我们模型的整个数据集,但是我们需要将其进一步处理为 RNN API 期望的格式。 首先,将其分为训练集(占整个数据集的 90%)和测试集(占 10%):
row = int(round(0.9 * result.shape[0]))
train = result[:row, :]
test = result[row:, :]
然后随机地随机排列训练集,作为机器学习模型训练中的标准做法:
np.random.shuffle(train)
制定训练集和测试集X_train和X_test的输入序列,以及训练集和测试集y_train和y_test的目标输出序列。 请注意,大写字母X和小写字母y是机器学习中常用的命名约定,分别代表输入和目标输出:
X_train = train[:, :-1] # all rows with all columns except the last one
X_test = test[:, :-1] # each row contains seq_len + 1 columns
y_train = train[:, 1:]
y_test = test[:, 1:]
最后,将四个数组重塑为 3-D(批大小,时间步数以及输入或输出数),以完成训练和测试数据集的准备:
X_train = np.reshape(X_train, (X_train.shape[0], X_train.shape[1], num_inputs))
X_test = np.reshape(X_test, (X_test.shape[0], X_test.shape[1], num_inputs))
y_train = np.reshape(y_train, (y_train.shape[0], y_train.shape[1], num_outputs))
y_test = np.reshape(y_test, (y_test.shape[0], y_test.shape[1], num_outputs))
注意,X_train.shape[1],X_test.shape[1],y_train.shape[1]和y_test.shape[1]与seq_len相同。
- 我们已经准备好构建模型。 创建两个占位符,以便在训练期间和
X_test一起喂入X_train和y_train:
X = tf.placeholder(tf.float32, [None, seq_len, num_inputs])
y = tf.placeholder(tf.float32, [None, seq_len, num_outputs])
使用BasicRNNCell创建一个 RNN 单元,每个时间步分别具有 num_neurons 神经元,:
cell = tf.contrib.rnn.OutputProjectionWrapper(
tf.contrib.rnn.BasicRNNCell(num_units=num_neurons, activation=tf.nn.relu), output_size=num_outputs)
outputs, _ = tf.nn.dynamic_rnn(cell, X, dtype=tf.float32)
OutputProjectionWrapper用于在每个单元的输出之上添加一个完全连接的层,因此,在每个时间步长处,RNN 单元的输出(将是num_neurons值的序列)都会减小为单个值。 这就是 RNN 在每个时间步为输入序列中的每个值输出一个值,或为每个实例的seq_len个数的值的每个输入序列输出总计seq_len个数的值的方式。
dynamic_rnn用于循环所有时间步长的 RNN 信元,总和为seq_len(在X形状中定义),它返回两个值:每个时间步长的输出列表,以及网络的最终状态。 接下来,我们将使用第一个outputs返回的整形值来定义损失函数。
- 通过以标准方式指定预测张量,损失,优化器和训练操作来完成模型定义:
preds = tf.reshape(outputs, [1, seq_len], name="preds")
loss = tf.reduce_mean(tf.square(outputs - y))
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate)
training_op = optimizer.minimize(loss)
请注意,当我们使用freeze_graph工具准备要在移动设备上部署的模型时,"preds"将用作输出节点名称,它也将在 iOS 和 Android 中用于运行模型进行预测。 如您所见,在我们甚至开始训练模型之前一定要知道那条信息,这绝对是一件很高兴的事情,而这是我们从头开始构建的模型的好处。
- 开始训练过程。 对于每个周期,我们将
X_train和y_train数据输入以运行training_op以最小化loss,然后保存模型检查点文件,并每 10 个周期打印损失值:
init = tf.global_variables_initializer()
saver = tf.train.Saver()
with tf.Session() as sess:
init.run()
count = 0
for _ in range(epochs):
n=0
sess.run(training_op, feed_dict={X: X_train, y: y_train})
count += 1
if count % 10 == 0:
saver.save(sess, "/tmp/" + symbol + "_model.ckpt")
loss_val = loss.eval(feed_dict={X: X_train, y: y_train})
print(count, "loss:", loss_val)
如果您运行上面的代码,您将看到如下输出:
(10, 'loss:', 243802.61)
(20, 'loss:', 80629.57)
(30, 'loss:', 40018.996)
(40, 'loss:', 28197.496)
(50, 'loss:', 24306.758)
...
(460, 'loss:', 93.095985)
(470, 'loss:', 92.864082)
(480, 'loss:', 92.33461)
(490, 'loss:', 92.09893)
(500, 'loss:', 91.966286)
您可以在第 4 步中用BasicLSTMCell替换BasicRNNCell并运行训练代码,但是使用BasicLSTMCell进行训练要慢得多,并且在 500 个周期之后损失值仍然很大。 在本节中,我们将不再对BasicLSTMCell进行实验,但是为了进行比较,在使用 Keras 的下一部分中,您将看到堆叠 LSTM 层,丢弃法和双向 RNN 的详细用法。
测试 TensorFlow RNN 模型
要查看 500 个周期后的损失值是否足够好,让我们使用测试数据集添加以下代码,以计算总测试示例中正确预测的数量(正确的意思是,预测价格在目标价格的同一个方向上上下波动,相对于前一天的价格):
correct = 0
y_pred = sess.run(outputs, feed_dict={X: X_test})
targets = []
predictions = []
for i in range(y_pred.shape[0]):
input = X_test[i]
target = y_test[i]
prediction = y_pred[i]
targets.append(target[-1][0])
predictions.append(prediction[-1][0])
if target[-1][0] >= input[-1][0] and prediction[-1][0] >=
input[-1][0]:
correct += 1
elif target[-1][0] < input[-1][0] and prediction[-1][0] <
input[-1][0]:
correct += 1
现在我们可以使用plot方法可视化预测正确率:
total = len(X_test)
xs = [i for i, _ in enumerate(y_test)]
plt.plot(xs, predictions, 'r-', label='prediction')
plt.plot(xs, targets, 'b-', label='true')
plt.legend(loc=0)
plt.title("%s - %d/%d=%.2f%%" %(symbol, correct, total,
100*float(correct)/total))
plt.show()
现在运行代码将显示如图 8.1 所示,正确预测的比率为 56.25% :

图 8.1:显示使用 TensorFlow RNN 训练的股价预测正确性
注意,每次运行此训练和测试代码时,您获得的比率可能都会有所不同。 通过微调模型的超参数,您可能会获得超过 60% 的比率,这似乎比随机预测要好。 如果您乐观的话,您可能会认为至少有 50% (56.25%)的东西要显示出来,并且可能希望看到该模型在移动设备上运行。 但首先让我们看看是否可以使用酷的 Keras 库来构建更好的模型-在执行此操作之前,让我们通过简单地运行来冻结经过训练的 TensorFlow 模型:
python tensorflow/python/tools/freeze_graph.py --input_meta_graph=/tmp/amzn_model.ckpt.meta --input_checkpoint=/tmp/amzn_model.ckpt --output_graph=/tmp/amzn_tf_frozen.pb --output_node_names="preds" --input_binary=true
将 Keras RNN LSTM API 用于股价预测
Keras 是一个非常易于使用的高级深度学习 Python 库,它运行在 TensorFlow,Theano 和 CNTK 等其他流行的深度学习库之上。 您很快就会看到,Keras 使构建和使用模型变得更加容易。 要安装和使用 Keras 以及 TensorFlow 作为 Keras 的后端,最好首先设置一个 VirtualEnv:
sudo pip install virtualenv
如果您的机器和 iOS 和 Android 应用上都有 TensorFlow 1.4 源,请运行以下命令;否则,请运行以下命令。 使用 TensorFlow 1.4 自定义库:
cd
mkdir ~/tf14_keras
virtualenv --system-site-packages ~/tf14_keras/
cd ~/tf14_keras/
source ./bin/activate
easy_install -U pip
pip install --upgrade https://storage.googleapis.com/tensorflow/mac/cpu/tensorflow-1.4.0-py2-none-any.whl
pip install keras
如果您的机器上装有 TensorFlow 1.5 源,则应在 Keras 上安装 TensorFlow 1.5,因为使用 Keras 创建的模型需要具有与 TensorFlow 移动应用所使用的模型相同的 TensorFlow 版本,或者在尝试加载模型时发生错误:
cd
mkdir ~/tf15_keras
virtualenv --system-site-packages ~/tf15_keras/
cd ~/tf15_keras/
source ./bin/activate
easy_install -U pip
pip install --upgrade https://storage.googleapis.com/tensorflow/mac/cpu/tensorflow-1.5.0-py2-none-any.whl
pip install keras
如果您的操作系统不是 Mac 或计算机具有 GPU,则您需要用正确的 URL 替换 TensorFlow Python 包 URL,您可以在这个页面上找到它。
在 Keras 中训练 RNN 模型
现在,让我们看看在 Keras 中建立和训练 LSTM 模型以预测股价的过程。 首先,一些导入和常量设置:
import keras
from keras import backend as K
from keras.layers.core import Dense, Activation, Dropout
from keras.layers.recurrent import LSTM
from keras.layers import Bidirectional
from keras.models import Sequential
import matplotlib.pyplot as plt
import tensorflow as tf
import numpy as np
symbol = 'amzn'
epochs = 10
num_neurons = 100
seq_len = 20
pred_len = 1
shift_pred = False
shift_pred用于指示我们是否要预测价格的输出序列而不是单个输出价格。 如果是True,我们将根据输入X1, X2, ..., Xn来预测X2, X3, ..., X[n+1],就像我们在使用 TensorFlow API 的最后一部分中所做的那样。 如果shift_pred为False,我们将基于输入X1, X2, ..., Xn来预测输出的pred_len。 例如,如果pred_len为 1,我们将预测X[n+1],如果pred_len为 3,我们将预测X[n+1], X[n+2], X[n+3],这很有意义,因为我们很想知道价格是连续连续 3 天上涨还是仅上涨 1 天然后下降 2 天。
现在,让我们创建一个根据上一节中的数据加载代码进行修改的方法,该方法根据pred_len和shift_pred设置准备适当的训练和测试数据集:
def load_data(filename, seq_len, pred_len, shift_pred):
f = open(filename, 'r').read()
data = f.split('\n')[:-1] # get rid of the last '' so float(n) works
data.reverse()
d = [float(n) for n in data]
lower = np.min(d)
upper = np.max(d)
scale = upper-lower
normalized_d = [(x-lower)/scale for x in d]
result = []
if shift_pred:
pred_len = 1
for i in range((len(normalized_d) - seq_len - pred_len)/pred_len):
result.append(normalized_d[i*pred_len: i*pred_len + seq_len + pred_len])
result = np.array(result)
row = int(round(0.9 * result.shape[0]))
train = result[:row, :]
test = result[row:, :]
np.random.shuffle(train)
X_train = train[:, :-pred_len]
X_test = test[:, :-pred_len]
if shift_pred:
y_train = train[:, 1:]
y_test = test[:, 1:]
else:
y_train = train[:, -pred_len:]
y_test = test[:, -pred_len:]
X_train = np.reshape(X_train, (X_train.shape[0], X_train.shape[1],
1))
X_test = np.reshape(X_test, (X_test.shape[0], X_test.shape[1], 1))
return [X_train, y_train, X_test, y_test, lower, scale]
注意,在这里我们也使用归一化,使用与上一章相同的归一化方法,以查看它是否可以改善我们的模型。 当使用训练模型进行预测时,我们还返回lower和scale值,这是非规范化所需的值。
现在我们可以调用load_data来获取训练和测试数据集,以及lower和scale值:
X_train, y_train, X_test, y_test, lower, scale = load_data(symbol + '.txt', seq_len, pred_len, shift_pred)
完整的模型构建代码如下:
model = Sequential()
model.add(Bidirectional(LSTM(num_neurons, return_sequences=True, input_shape=(None, 1)), input_shape=(seq_len, 1)))
model.add(Dropout(0.2))
model.add(LSTM(num_neurons, return_sequences=True))
model.add(Dropout(0.2))
model.add(LSTM(num_neurons, return_sequences=False))
model.add(Dropout(0.2))
if shift_pred:
model.add(Dense(units=seq_len))
else:
model.add(Dense(units=pred_len))
model.add(Activation('linear'))
model.compile(loss='mse', optimizer='rmsprop')
model.fit(
X_train,
y_train,
batch_size=512,
epochs=epochs,
validation_split=0.05)
print(model.output.op.name)
print(model.input.op.name)
即使使用新添加的Bidirectional,Dropout,validation_split和堆叠 LSTM 层,该代码也比 TensorFlow 中的模型构建代码更容易解释和简化。 请注意,LSTM 调用中的return_sequences 参数i必须为True,因此 LSTM 单元的输出将是完整的输出序列,而不仅仅是输出序列中的最后一个输出, 除非它是最后的堆叠层。 最后两个 print 语句将打印输入节点名称( bidirectional_1_input)和输出节点名称(activation_1/Identity),当我们冻结模型并在移动设备上运行模型时需要。
现在,如果您运行前面的代码,您将看到如下输出:
824/824 [==============================] - 7s 9ms/step - loss: 0.0833 - val_loss: 0.3831
Epoch 2/10
824/824 [==============================] - 2s 3ms/step - loss: 0.2546 - val_loss: 0.0308
Epoch 3/10
824/824 [==============================] - 2s 2ms/step - loss: 0.0258 - val_loss: 0.0098
Epoch 4/10
824/824 [==============================] - 2s 2ms/step - loss: 0.0085 - val_loss: 0.0035
Epoch 5/10
824/824 [==============================] - 2s 2ms/step - loss: 0.0044 - val_loss: 0.0026
Epoch 6/10
824/824 [==============================] - 2s 2ms/step - loss: 0.0038 - val_loss: 0.0022
Epoch 7/10
824/824 [==============================] - 2s 2ms/step - loss: 0.0033 - val_loss: 0.0019
Epoch 8/10
824/824 [==============================] - 2s 2ms/step - loss: 0.0030 - val_loss: 0.0019
Epoch 9/10
824/824 [==============================] - 2s 2ms/step - loss: 0.0028 - val_loss: 0.0017
Epoch 10/10
824/824 [==============================] - 2s 3ms/step - loss: 0.0027 - val_loss: 0.0019
训练损失和验证损失都可以通过简单调用model.fit进行打印。
测试 Keras RNN 模型
现在该保存模型检查点并使用测试数据集来计算正确预测的数量,正如我们在上一节中所解释的那样:
saver = tf.train.Saver()
saver.save(K.get_session(), '/tmp/keras_' + symbol + '.ckpt')
predictions = []
correct = 0
total = pred_len*len(X_test)
for i in range(len(X_test)):
input = X_test[i]
y_pred = model.predict(input.reshape(1, seq_len, 1))
predictions.append(scale * y_pred[0][-1] + lower)
if shift_pred:
if y_test[i][-1] >= input[-1][0] and y_pred[0][-1] >= input[-1]
[0]:
correct += 1
elif y_test[i][-1] < input[-1][0] and y_pred[0][-1] < input[-1][0]:
correct += 1
else:
for j in range(len(y_test[i])):
if y_test[i][j] >= input[-1][0] and y_pred[0][j] >= input[-1][0]:
correct += 1
elif y_test[i][j] < input[-1][0] and y_pred[0][j] < input[-1][0]:
correct += 1
我们主要调用model.predict来获取X_test中每个实例的预测,并将其与真实值和前一天的价格一起使用,以查看在方向方面是否为正确的预测。 最后,让我们根据测试数据集和预测来绘制真实价格:
y_test = scale * y_test + lower
y_test = y_test[:, -1]
xs = [i for i, _ in enumerate(y_test)]
plt.plot(xs, y_test, 'g-', label='true')
plt.plot(xs, predictions, 'r-', label='prediction')
plt.legend(loc=0)
if shift_pred:
plt.title("%s - epochs=%d, shift_pred=True, seq_len=%d: %d/%d=%.2f%%" %(symbol, epochs, seq_len, correct, total, 100*float(correct)/total))
else:
plt.title("%s - epochs=%d, lens=%d,%d: %d/%d=%.2f%%" %(symbol, epochs, seq_len, pred_len, correct, total, 100*float(correct)/total))
plt.show()
您会看到类似图 8.2 的内容:

图 8.2:使用 Keras 双向和堆叠 LSTM 层进行股价预测
很容易在栈中添加更多 LSTM 层,或者使用诸如学习率和丢弃率以及许多恒定设置之类的超参数。 但是,对于使用pred_len和shift_pred的不同设置,正确率的差异还没有发现。 也许我们现在应该对接近 60% 的正确率感到满意,并看看如何在 iOS 和 Android 上使用 TensorFlow 和 Keras 训练的模型-我们可以在以后继续尝试改进模型,但是,了解使用 TensorFlow 和 Keras 训练的 RNN 模型是否会遇到任何问题将非常有价值。
正如 FrançoisChollet 指出的那样,“深度学习更多的是艺术而不是科学……每个问题都是独特的,您将不得不尝试并经验地评估不同的策略。目前尚无理论可以提前准确地告诉您应该做什么。 以最佳方式解决问题。您必须尝试并进行迭代。” 希望我们为您使用 TensorFlow 和 Keras API 改善股票价格预测模型提供了一个很好的起点。
本节中最后要做的就是从检查点冻结 Keras 模型-因为我们在虚拟环境中安装了 TensorFlow 和 Keras,而 TensorFlow 是 VirtualEnv 中唯一安装并受支持的深度学习库,Keras 使用 TensorFlow 后端,并通过saver.save(K.get_session(), '/tmp/keras_' + symbol + '.ckpt')调用以 TensorFlow 格式生成检查点。 现在运行以下命令冻结检查点(回想我们在训练期间从print(model.input.op.name)获得output_node_name):
python tensorflow/python/tools/freeze_graph.py --input_meta_graph=/tmp/keras_amzn.ckpt.meta --input_checkpoint=/tmp/keras_amzn.ckpt --output_graph=/tmp/amzn_keras_frozen.pb --output_node_names="activation_1/Identity" --input_binary=true
因为我们的模型非常简单明了,所以我们将直接在移动设备上尝试这两个冻结的模型,而无需像前两章中那样使用transform_graph工具。
在 iOS 上运行 TensorFlow 和 Keras 模型
我们不会通过重复项目设置步骤来烦您-只需按照我们之前的操作即可创建一个名为 StockPrice 的新 Objective-C 项目,该项目将使用手动构建的 TensorFlow 库(请参阅第 7 章,“使用 CNN 和 LSTM 识别绘画”的 iOS 部分(如果需要详细信息)。 然后将两个模型文件amzn_tf_frozen.pb和amzn_keras_frozen.pb添加到项目中,您应该在 Xcode 中拥有 StockPrice 项目,如图 8.3 所示:

图 8.3:在 Xcode 中使用 TensorFlow 和 Keras 训练的模型的 iOS 应用
在ViewController.mm中,我们将首先声明一些变量和一个常量:
unique_ptr<tensorflow::Session> tf_session;
UITextView *_tv;
UIButton *_btn;
NSMutableArray *_closeprices;
const int SEQ_LEN = 20;
然后创建一个按钮点击处理器,以使用户可以选择 TensorFlow 或 Keras 模型(该按钮在viewDidLoad方法中像以前一样创建):
- (IBAction)btnTapped:(id)sender {
UIAlertAction* tf = [UIAlertAction actionWithTitle:@"Use TensorFlow Model" style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
[self getLatestData:NO];
}];
UIAlertAction* keras = [UIAlertAction actionWithTitle:@"Use Keras Model" style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
[self getLatestData:YES];
}];
UIAlertAction* none = [UIAlertAction actionWithTitle:@"None" style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {}];
UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"RNN Model Pick" message:nil preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:tf];
[alert addAction:keras];
[alert addAction:none];
[self presentViewController:alert animated:YES completion:nil];
}
getLatestData方法首先发出 URL 请求以获取紧凑型版本的 Alpha Vantage API,该 API 返回 Amazon 每日股票数据的最后 100 个数据点,然后解析结果并将最后 20 个收盘价保存在_closeprices数组中:
-(void)getLatestData:(BOOL)useKerasModel {
NSURLSession *session = [NSURLSession sharedSession];
[[session dataTaskWithURL:[NSURL URLWithString:@"https://www.alphavantage.co/query?function=TIME_SERIES_DAILY&symbol=amzn&apikey=<your_api_key>&datatype=csv&outputsize=compact"]
completionHandler:^(NSData *data,
NSURLResponse *response,
NSError *error) {
NSString *stockinfo = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];
NSArray *lines = [stockinfo componentsSeparatedByString:@"\n"];
_closeprices = [NSMutableArray array];
for (int i=0; i<SEQ_LEN; i++) {
NSArray *items = [lines[i+1] componentsSeparatedByString:@","];
[_closeprices addObject:items[4]];
}
if (useKerasModel)
[self runKerasModel];
else
[self runTFModel];
}] resume];
}
runTFModel方法定义如下:
- (void) runTFModel {
tensorflow::Status load_status;
load_status = LoadModel(@"amzn_tf_frozen", @"pb", &tf_session);
tensorflow::Tensor prices(tensorflow::DT_FLOAT,
tensorflow::TensorShape({1, SEQ_LEN, 1}));
auto prices_map = prices.tensor<float, 3>();
NSString *txt = @"Last 20 Days:\n";
for (int i = 0; i < SEQ_LEN; i++){
prices_map(0,i,0) = [_closeprices[SEQ_LEN-i-1] floatValue];
txt = [NSString stringWithFormat:@"%@%@\n", txt,
_closeprices[SEQ_LEN-i-1]];
}
std::vector<tensorflow::Tensor> output;
tensorflow::Status run_status = tf_session->Run({{"Placeholder",
prices}}, {"preds"}, {}, &output);
if (!run_status.ok()) {
LOG(ERROR) << "Running model failed:" << run_status;
}
else {
tensorflow::Tensor preds = output[0];
auto preds_map = preds.tensor<float, 2>();
txt = [NSString stringWithFormat:@"%@\nPrediction with TF RNN
model:\n%f", txt, preds_map(0,SEQ_LEN-1)];
dispatch_async(dispatch_get_main_queue(), ^{
[_tv setText:txt];
[_tv sizeToFit];
});
}
}
preds_map(0,SEQ_LEN-1) 是基于最近 20 天的第二天的预测价格; Placeholder 是“在 TensorFlow 中训练 RNN 模型”小节的第四步的X = tf.placeholder(tf.float32, [None, seq_len, num_inputs])中定义的输入节点名称。 在模型生成预测后,我们将其与最近 20 天的价格一起显示在TextView中。
runKeras方法的定义与此类似,但具有反规范化以及不同的输入和输出节点名称。 由于我们的 Keras 模型经过训练只能输出一个预测价格,而不是一系列seq_len价格,因此我们使用preds_map(0,0)来获得预测:
- (void) runKerasModel {
tensorflow::Status load_status;
load_status = LoadModel(@"amzn_keras_frozen", @"pb", &tf_session);
if (!load_status.ok()) return;
tensorflow::Tensor prices(tensorflow::DT_FLOAT,
tensorflow::TensorShape({1, SEQ_LEN, 1}));
auto prices_map = prices.tensor<float, 3>();
float lower = 5.97;
float scale = 1479.37;
NSString *txt = @"Last 20 Days:\n";
for (int i = 0; i < SEQ_LEN; i++){
prices_map(0,i,0) = ([_closeprices[SEQ_LEN-i-1] floatValue] -
lower)/scale;
txt = [NSString stringWithFormat:@"%@%@\n", txt,
_closeprices[SEQ_LEN-i-1]];
}
std::vector<tensorflow::Tensor> output;
tensorflow::Status run_status = tf_session->Run({{"bidirectional_1_input", prices}}, {"activation_1/Identity"},
{}, &output);
if (!run_status.ok()) {
LOG(ERROR) << "Running model failed:" << run_status;
}
else {
tensorflow::Tensor preds = output[0];
auto preds_map = preds.tensor<float, 2>();
txt = [NSString stringWithFormat:@"%@\nPrediction with Keras
RNN model:\n%f", txt, scale * preds_map(0,0) + lower];
dispatch_async(dispatch_get_main_queue(), ^{
[_tv setText:txt];
[_tv sizeToFit];
});
}
}
如果您现在运行该应用并点击Predict按钮,您将看到模型选择消息(图 8.4):

图 8.4:选择 TensorFlow 或 Keras RNN 模型
如果选择 TensorFlow 模型,则可能会出现错误:
Could not create TensorFlow Graph: Invalid argument: No OpKernel was registered to support Op 'Less' with these attrs. Registered devices: [CPU], Registered kernels:
device='CPU'; T in [DT_FLOAT]
[[Node: rnn/while/Less = Less[T=DT_INT32, _output_shapes=[[]]](rnn/while/Merge, rnn/while/Less/Enter)]]
如果选择 Keras 模型,则可能会出现稍微不同的错误:
Could not create TensorFlow Graph: Invalid argument: No OpKernel was registered to support Op 'Less' with these attrs. Registered devices: [CPU], Registered kernels:
device='CPU'; T in [DT_FLOAT]
[[Node: bidirectional_1/while_1/Less = Less[T=DT_INT32, _output_shapes=[[]]](bidirectional_1/while_1/Merge, bidirectional_1/while_1/Less/Enter)]]
我们在上一章中已经看到RefSwitch操作出现类似的错误,并且知道针对此类错误的解决方法是在启用 -D__ANDROID_TYPES_FULL__的情况下构建 TensorFlow 库。 如果没有看到这些错误,则意味着您在上一章的 iOS 应用中已建立了这样的库; 否则,请按照“为 iOS 构建自定义 TensorFlow 库”的开头的说明。 上一章的内容构建新的 TensorFlow 库,然后再次运行该应用。
现在选择 TensorFlow 模型,您将看到如图 8.5 所示的结果:

图 8.5:使用 TensorFlow RNN 模型进行预测
使用 Keras 模型输出不同的预测,如图 8.6 所示:

图 8.6:使用 Keras RNN 模型进行预测
我们无法确定哪个模型能在没有进一步研究的情况下更好地工作,但是我们可以确定的是,我们的两个 RNN 模型都使用 TensorFlow 和 Keras API 从头开始训练了,其准确率接近 60%, 在 iOS 上运行良好,这很值得我们付出努力,因为我们正在尝试建立一个许多专家认为将达到与随机选择相同的表现的模型,并且在此过程中,我们学到了一些新奇的东西-使用 TensorFlow 和 Keras 构建 RNN 模型并在 iOS 上运行它们。 在下一章中,我们只剩下一件事了:如何在 Android 上使用模型? 我们会遇到新的障碍吗?
在 Android 上运行 TensorFlow 和 Keras 模型
事实证明,这就像使用 Android 上的模型在沙滩上散步一样-尽管我们必须使用自定义的 TensorFlow 库(而不是 TensorFlow pod),我们甚至不需要像上一章那样使用自定义的 TensorFlow Android 库。 截至 2018 年 2 月)。 与用于 iOS 的 TensorFlow Pod 相比,在build.gradle文件中使用compile 'org.tensorflow:tensorflow-android:+'构建的 TensorFlow Android 库必须对Less操作具有更完整的数据类型支持。
要在 Android 中测试模型,请创建一个新的 Android 应用 StockPrice,并将两个模型文件添加到其assets文件夹中。 然后在布局中添加几个按钮和一个TextView并在MainActivity.java中定义一些字段和常量:
private static final String TF_MODEL_FILENAME = "file:///android_asset/amzn_tf_frozen.pb";
private static final String KERAS_MODEL_FILENAME = "file:///android_asset/amzn_keras_frozen.pb";
private static final String INPUT_NODE_NAME_TF = "Placeholder";
private static final String OUTPUT_NODE_NAME_TF = "preds";
private static final String INPUT_NODE_NAME_KERAS = "bidirectional_1_input";
private static final String OUTPUT_NODE_NAME_KERAS = "activation_1/Identity";
private static final int SEQ_LEN = 20;
private static final float LOWER = 5.97f;
private static final float SCALE = 1479.37f;
private TensorFlowInferenceInterface mInferenceInterface;
private Button mButtonTF;
private Button mButtonKeras;
private TextView mTextView;
private boolean mUseTFModel;
private String mResult;
制作onCreate如下:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mButtonTF = findViewById(R.id.tfbutton);
mButtonKeras = findViewById(R.id.kerasbutton);
mTextView = findViewById(R.id.textview);
mTextView.setMovementMethod(new ScrollingMovementMethod());
mButtonTF.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mUseTFModel = true;
Thread thread = new Thread(MainActivity.this);
thread.start();
}
});
mButtonKeras.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mUseTFModel = false;
Thread thread = new Thread(MainActivity.this);
thread.start();
}
});
}
其余代码全部在run方法中,在点击TF PREDICTION或KERAS PREDICTION按钮时在工作线程中启动,需要一些解释,使用 Keras 模型需要在运行模型之前和之后规范化和非规范化:
public void run() {
runOnUiThread(
new Runnable() {
@Override
public void run() {
mTextView.setText("Getting data...");
}
});
float[] floatValues = new float[SEQ_LEN];
try {
URL url = new URL("https://www.alphavantage.co/query?function=TIME_SERIES_DAILY&symbol=amzn&apikey=4SOSJM2XCRIB5IUS&datatype=csv&outputsize=compact");
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
InputStream in = new BufferedInputStream(urlConnection.getInputStream());
Scanner s = new Scanner(in).useDelimiter("\\n");
mResult = "Last 20 Days:\n";
if (s.hasNext()) s.next(); // get rid of the first title line
List<String> priceList = new ArrayList<>();
while (s.hasNext()) {
String line = s.next();
String[] items = line.split(",");
priceList.add(items[4]);
}
for (int i=0; i<SEQ_LEN; i++)
mResult += priceList.get(SEQ_LEN-i-1) + "\n";
for (int i=0; i<SEQ_LEN; i++) {
if (mUseTFModel)
floatValues[i] = Float.parseFloat(priceList.get(SEQ_LEN-i-1));
else
floatValues[i] = (Float.parseFloat(priceList.get(SEQ_LEN-i-1)) - LOWER) / SCALE;
}
AssetManager assetManager = getAssets();
mInferenceInterface = new TensorFlowInferenceInterface(assetManager, mUseTFModel ? TF_MODEL_FILENAME : KERAS_MODEL_FILENAME);
mInferenceInterface.feed(mUseTFModel ? INPUT_NODE_NAME_TF : INPUT_NODE_NAME_KERAS, floatValues, 1, SEQ_LEN, 1);
float[] predictions = new float[mUseTFModel ? SEQ_LEN : 1];
mInferenceInterface.run(new String[] {mUseTFModel ? OUTPUT_NODE_NAME_TF : OUTPUT_NODE_NAME_KERAS}, false);
mInferenceInterface.fetch(mUseTFModel ? OUTPUT_NODE_NAME_TF : OUTPUT_NODE_NAME_KERAS, predictions);
if (mUseTFModel) {
mResult += "\nPrediction with TF RNN model:\n" + predictions[SEQ_LEN - 1];
}
else {
mResult += "\nPrediction with Keras RNN model:\n" + (predictions[0] * SCALE + LOWER);
}
runOnUiThread(
new Runnable() {
@Override
public void run() {
mTextView.setText(mResult);
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
现在运行该应用,然后点击TF PREDICTION按钮,您将在图 8.7 中看到结果:

图 8.7:使用 TensorFlow 模型在亚马逊上进行股价预测
选择 KERAS 预测将为您提供如图 8.8 所示的结果:

图 8.8:使用 Keras 模型在亚马逊上进行股价预测
总结
在本章中,我们首先对表示不屑一顾,试图通过使用 TensorFlow 和 Keras RNN API 预测股价来击败市场。 我们首先讨论了 RNN 和 LSTM 模型是什么以及如何使用它们进行股价预测。 然后,我们使用 TensorFlow 和 Keras 从零开始构建了两个 RNN 模型,接近测试正确率的 60%。 最后,我们介绍了如何冻结模型并在 iOS 和 Android 上使用它们,并使用自定义 TensorFlow 库修复了 iOS 上可能出现的运行时错误。
如果您对我们尚未建立预测正确率为 80% 或 90% 的模型感到有些失望,则可能需要继续进行“尝试并迭代”过程,以查看是否可以以该正确率预测股票价格。 但是,您肯定会从使用 TensorFlow 和 Keras API 的 RNN 模型构建,训练和测试中学到的技能以及在 iOS 和 Android 上运行的技能而受益。
如果您对使用深度学习技术打败市场感兴趣并感到兴奋,让我们在 GAN(生成对抗网络)上的下一章中进行研究,该模型试图击败能够分辨真实数据与虚假数据之间差异的对手, 并且越来越擅长生成看起来像真实数据的数据,欺骗对手。 GAN 实际上被深度学习的一些顶级研究人员誉为是过去十年中深度学习中最有趣和令人兴奋的想法。
九、使用 GAN 生成和增强图像
自 2012 年深度学习起步以来,有人认为 Ian Goodfellow 在 2014 年提出的生成对抗网络(GAN)比这更有趣或更有前途。 实际上, Facebook AI 研究主管和之一,深度学习研究人员之一的 Yann LeCun 将 GAN 和对抗训练称为,“这是近十年来机器学习中最有趣的想法。” 因此,我们如何在这里不介绍它,以了解 GAN 为什么如此令人兴奋,以及如何构建 GAN 模型并在 iOS 和 Android 上运行它们?
在本章中,我们将首先概述 GAN 是什么,它如何工作以及为什么它具有如此巨大的潜力。 然后,我们将研究两个 GAN 模型:一个基本的 GAN 模型可用于生成类似人的手写数字,另一个更高级的 GAN 模型可将低分辨率的图像增强为高分辨率的图像。 我们将向您展示如何在 Python 和 TensorFlow 中构建和训练此类模型,以及如何为移动部署准备模型。 然后,我们将提供带有完整源代码的 iOS 和 Android 应用,它们使用这些模型来生成手写数字并增强图像。 在本章的最后,您应该准备好进一步探索各种基于 GAN 的模型,或者开始构建自己的模型,并了解如何在移动应用中运行它们。
总之,本章将涵盖以下主题:
- GAN – 什么以及为什么
- 使用 TensorFlow 构建和训练 GAN 模型
- 在 iOS 中使用 GAN 模型
- 在 Android 中使用 GAN 模型
GAN – 什么以及为什么
GAN 是学习生成类似于真实数据或训练集中数据的神经网络。 GAN 的关键思想是让生成器网络和判别器网络相互竞争:生成器试图生成看起来像真实数据的数据,而判别器试图分辨生成的数据是否真实(从已知真实数据)或伪造(由生成器生成)。 生成器和判别器是一起训练的,在训练过程中,生成器学会生成看起来越来越像真实数据的数据,而判别器则学会将真实数据与伪数据区分开。 生成器通过尝试使判别器的输出概率为真实数据来学习,当将生成器的输出作为判别器的输入时,生成器的输出概率尽可能接近 1.0,而判别器通过尝试实现两个目标来学习:
- 当以生成器的输出作为输入时,使其输出的可能性为实,尽可能接近 0.0,这恰好是生成器的相反目标
- 当输入真实数据作为输入时,使其输出的可能性为实数,尽可能接近 1.0
在下一节中,您将看到与生成器和判别器网络及其训练过程的给定描述相匹配的详细代码片段。 如果您想了解更多关于 GAN 的知识,除了这里的摘要概述之外,您还可以在 YouTube 上搜索“GAN 简介”,并观看 2016 年 NIPS(神经信息处理系统)和 ICCV(国际计算机视觉会议)2017 大会上的 Ian Goodfellow 的 GAN 入门和教程视频。 事实上,YouTube 上有 7 个 NIPS 2016 对抗训练训练班视频和 12 个 ICCV 2017 GAN 指导视频,您可以自己投入其中。
在生成器和判别器两个参与者的竞争目标下,GAN 是一个寻求两个对手之间保持平衡的系统。 如果两个玩家都具有无限的能力并且可以进行最佳训练,那么纳什均衡(继 1994 年诺贝尔经济学奖得主约翰·纳什和电影主题《美丽心灵》之后) 一种状态,在这种状态下,任何玩家都无法通过仅更改其自己的策略来获利,这对应于生成器生成数据的状态,该数据看起来像真实数据,而判别器无法从假数据中分辨真实数据。
如果您有兴趣了解有关纳什均衡的更多信息,请访问 Google “可汗学院纳什均衡”,并观看 Sal Khan 撰写的两个有趣的视频。 《经济学家》解释经济学的“纳什均衡”维基百科页面和文章“纳什均衡是什么,为什么重要?”也是不错的读物。 了解 GAN 的基本直觉和想法将有助于您进一步了解 GAN 具有巨大潜力的原因。
生成器能够生成看起来像真实数据的数据的潜力意味着可以使用 GAN 开发各种出色的应用,例如:
- 从劣质图像生成高质量图像
- 图像修复(修复丢失或损坏的图像)
- 翻译图像(例如,从边缘草图到照片,或者在人脸上添加或移除诸如眼镜之类的对象)
- 从文本生成图像(和第 6 章,“使用自然语言描述图像”的 Text2Image 相反)
- 撰写看起来像真实新闻的新闻文章
- 生成与训练集中的音频相似的音频波形
基本上,GAN 可以从随机输入生成逼真的图像,文本或音频数据; 如果您具有一组源数据和目标数据的训练集,则 GAN 还可从类似于源数据的输入中生成类似于目标数据的数据。 GAN 模型中的生成器和判别器以动态方式工作的这一通用特性,使 GAN 可以生成任何种类的现实输出,这使 GAN 十分令人兴奋。
但是,由于生成器和判别器的动态或竞争目标,训练 GAN 达到纳什均衡状态是一个棘手且困难的问题。 实际上,这仍然是一个开放的研究问题 – Ian Goodfellow 在 2017 年 8 月对 Andrew Ng 进行的“深度学习英雄”采访中(YouTube 上的搜索ian goodfellow andrew ng)说,如果我们可以使 GAN 变得像深度学习一样可靠,我们将看到 GAN 取得更大的成功,否则我们最终将用其他形式的生成模型代替它们。
尽管在 GAN 的训练方面存在挑战,但是在训练期间您已经可以应用许多有效的已知技巧 – 我们在这里不会介绍它们,但是如果您有兴趣调整我们将在本章中描述的模型或许多其他 GAN 模型 ),或构建自己的 GAN 模型。
使用 TensorFlow 构建和训练 GAN 模型
通常,GAN 模型具有两个神经网络:G用于生成器,D用于判别器。 x是来自训练集的一些实际数据输入,z是随机输入噪声。 在训练过程中,D(x)是x为真实的概率,D尝试使D(x)接近 1;G(z)是具有随机输入z的生成的输出,并且D试图使D(G(z))接近 0,但同时G试图使D(G(z))接近 1。 现在,让我们首先来看一下如何在 TensorFlow 和 Python 中构建基本的 GAN 模型,该模型可以编写或生成手写数字。
生成手写数字的基本 GAN 模型
手写数字的训练模型基于仓库,这是这个页面的分支,并添加了显示生成的数字并使用输入占位符保存 TensorFlow 训练模型的脚本,因此我们的 iOS 和 Android 应用可以使用该模型。 是的您应该查看原始仓库的博客。在继续之前,需要对具有代码的 GAN 模型有基本的了解。
在研究定义生成器和判别器网络并进行 GAN 训练的核心代码片段之前,让我们先运行脚本以在克隆存储库并转到仓库目录之后训练和测试模型:
git clone https://github.com/jeffxtang/generative-adversarial-networks
cd generative-adversarial-networks
该派生向gan-script-fast.py脚本添加了检查点保存代码,还添加了新脚本gan-script-test.py以使用随机输入的占位符测试和保存新的检查点–因此,使用新检查点冻结的模型可以在 iOS 和 Android 应用中使用。
运行命令python gan-script-fast.py训练模型,在 Ubuntu 上的 GTX-1070 GPU 上花费不到一小时。 训练完成后,检查点文件将保存在模型目录中。 现在运行python gan-script-test.py来查看一些生成的手写数字。 该脚本还从模型目录读取检查点文件,并在运行gan-script-fast.py时保存该文件,然后将更新的检查点文件以及随机输入占位符重新保存在newmodel目录中:
ls -lt newmodel
-rw-r--r-- 1 jeffmbair staff 266311 Mar 5 16:43 ckpt.meta
-rw-r--r-- 1 jeffmbair staff 65 Mar 5 16:42 checkpoint
-rw-r--r-- 1 jeffmbair staff 69252168 Mar 5 16:42 ckpt.data-00000-of-00001
-rw-r--r-- 1 jeffmbair staff 2660 Mar 5 16:42 ckpt.index
gan-script-test.py中的下一个代码片段显示了输入节点名称(z_placeholder)和输出节点名称(Sigmoid_1),如print(generated_images)所示:
z_placeholder = tf.placeholder(tf.float32, [None, z_dimensions], name='z_placeholder')
...
saver.restore(sess, 'model/ckpt')
generated_images = generator(z_placeholder, 5, z_dimensions)
print(generated_images)
images = sess.run(generated_images, {z_placeholder: z_batch})
saver.save(sess, "newmodel/ckpt")
在gan-script-fast.py脚本中,方法def discriminator(images, reuse_variables=None)定义了一个判别器网络,该网络使用一个真实的手写图像输入或由生成器生成的一个手写输入,经过一个典型的小型 CNN 网络,该网络具有两层conv2d层,每一层都带有relu激活和平均池化层以及两个完全连接的层来输出一个标量值,该标量值将保持输入图像为真或假的概率。 另一种方法def generator(batch_size, z_dim)定义了生成器网络,该网络采用随机输入的图像向量并将其转换为具有 3 个conv2d层的28 x 28图像。
现在可以使用这两种方法来定义三个输出:
Gz,即随机图像输入的生成器输出:Gz = generator(batch_size, z_dimensions)Dx,是真实图像输入的判别器输出:Dx = discriminator(x_placeholder)Dg,Gz的判别器输出:Dg = discriminator(Gz, reuse_variables=True)
和三个损失函数:
d_loss_real,Dx和 1 之差:d_loss_real = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits = Dx, labels = tf.ones_like(Dx)))d_loss_fake,Dg和 0 之差:d_loss_fake = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits = Dg, labels = tf.zeros_like(Dg)))g_loss,Dg和 1 之差:g_loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits = Dg, labels = tf.ones_like(Dg)))
请注意,判别器尝试使 d_loss_fake最小化,而生成器尝试使g_loss最小化,两种情况下Dg之间的差分别为 0 和 1。
最后,现在可以为三个损失函数设置三个优化器:d_trainer_fake,d_trainer_real和g_trainer,它们全部是通过tf.train.AdamOptimizer的minimize方法定义的。
现在,脚本仅创建一个 TensorFlow 会话,通过运行三个优化器将生成器和判别器进行 100,000 步训练,将随机图像输入馈入生成器,将真实和伪图像输入均馈入判别器。
在运行 gan-script-fast.py和gan-script-test.py之后,将检查点文件从newmodel目录运至/tmp,然后转到 TensorFlow 源根目录并运行:
python tensorflow/python/tools/freeze_graph.py \
--input_meta_graph=/tmp/ckpt.meta \
--input_checkpoint=/tmp/ckpt \
--output_graph=/tmp/gan_mnist.pb \
--output_node_names="Sigmoid_1" \
--input_binary=true
这将创建可用于移动应用的冻结模型gan_mnist.pb。 但是在此之前,让我们看一下可以增强低分辨率图像的更高级的 GAN 模型。
增强图像分辨率的高级 GAN 模型
我们将用于增强低分辨率模糊图像的模型,基于论文《使用条件对抗网络的图像到图像转换》及其 TensorFlow 实现 pix2pix。 在仓库的分支中,我们添加了两个脚本:
tools/convert.py从普通图像创建模糊图像pix2pix_runinference.py添加了一个用于低分辨率图像输入的占位符和一个用于返回增强图像的操作,并保存了新的检查点文件,我们将冻结这些文件以生成在移动设备上使用的模型文件。
基本上,pix2pix 使用 GAN 将输入图像映射到输出图像。 您可以使用不同类型的输入图像和输出图像来创建许多有趣的图像转换:
- 地图到航拍
- 白天到黑夜
- 边界到照片
- 黑白图像到彩色图像
- 损坏的图像到原始图像
- 从低分辨率图像到高分辨率图像
在所有情况下,生成器都将输入图像转换为输出图像,试图使输出看起来像真实的目标图像,判别器将训练集中的样本或生成器的输出作为输入,并尝试告诉它是真实图像还是生成器生成的图像。 自然,与模型相比,pix2pix 中的生成器和判别器网络以更复杂的方式构建以生成手写数字,并且训练还应用了一些技巧来使过程稳定-有关详细信息,您可以阅读本文或较早提供的 TensorFlow 实现链接。 我们在这里仅向您展示如何设置训练集和训练 pix2pix 模型以增强低分辨率图像。
- 通过在终端上运行来克隆仓库:
git clone https://github.com/jeffxtang/pix2pix-tensorflow
cd pix2pix-tensorflow
- 创建一个新目录
photos/original并复制一些图像文件-例如,我们将所有拉布拉多犬的图片从斯坦福狗数据集(在第 2 章,“使用迁移学习的图像分类”中使用)复制到photos/original目录 - 运行脚本
python tools/process.py --input_dir photos/original --operation resize --output_dir photos/resized调整photo/original目录中图像的大小并将调整后的图像保存到photos/resized目录中 - 运行
mkdir photos/blurry,然后运行python tools/convert.py,以使用流行的 ImageMagick 的convert命令将调整大小的图像转换为模糊的图像。convert.py的代码如下:
import os
file_names = os.listdir("photos/resized/")
for f in file_names:
if f.find(".png") != -1:
os.system("convert photos/resized/" + f + " -blur 0x3 photos/blurry/" + f)
- 将
photos/resized和photos/blurry中的每个文件合并为一个对,并将所有配对的图像(一个调整大小的图像,另一个模糊的版本)保存到photos/resized_blurry目录:
python tools/process.py --input_dir photos/resized --b_dir photos/blurry --operation combine --output_dir photos/resized_blurry
- 运行拆分工具
python tools/split.py --dir photos/resized_blurry,将文件转换为train目录和val目录 - 通过运行以下命令训练
pix2pix模型:
python pix2pix.py \
--mode train \
--output_dir photos/resized_blurry/ckpt_1000 \
--max_epochs 1000 \
--input_dir photos/resized_blurry/train \
--which_direction BtoA
方向BtoA表示从模糊图像转换为原始图像。 在 GTX-1070 GPU 上进行的训练大约需要四个小时,并且photos/resized_blurry/ckpt_1000目录中生成的检查点文件如下所示:
-rw-rw-r-- 1 jeff jeff 1721531 Mar 2 18:37 model-136000.meta
-rw-rw-r-- 1 jeff jeff 81 Mar 2 18:37 checkpoint
-rw-rw-r-- 1 jeff jeff 686331732 Mar 2 18:37 model-136000.data-00000-of-00001
-rw-rw-r-- 1 jeff jeff 10424 Mar 2 18:37 model-136000.index
-rw-rw-r-- 1 jeff jeff 3807975 Mar 2 14:19 graph.pbtxt
-rw-rw-r-- 1 jeff jeff 682 Mar 2 14:19 options.json
- (可选)您可以在测试模式下运行脚本,然后在
--output_dir指定的目录中检查图像翻译结果:
python pix2pix.py \
--mode test \
--output_dir photos/resized_blurry/output_1000 \
--input_dir photos/resized_blurry/val \
--checkpoint photos/resized_blurry/ckpt_1000
- 运行
pix2pix_runinference.py脚本以恢复在步骤 7 中保存的检查点,为图像输入创建一个新的占位符,为它提供测试图像ww.png,将翻译输出为result.png,最后将新的检查点文件保存在newckpt目录:
python pix2pix_runinference.py \
--mode test \
--output_dir photos/blurry_output \
--input_dir photos/blurry_test \
--checkpoint photos/resized_blurry/ckpt_1000
以下pix2pix_runinference.py中的代码段设置并打印输入和输出节点:
image_feed = tf.placeholder(dtype=tf.float32, shape=(1, 256, 256, 3), name="image_feed")
print(image_feed) # Tensor("image_feed:0", shape=(1, 256, 256, 3), dtype=float32)
with tf.variable_scope("generator", reuse=True):
output_image = deprocess(create_generator(image_feed, 3))
print(output_image) #Tensor("generator_1/deprocess/truediv:0", shape=(1, 256, 256, 3), dtype=float32)
具有tf.variable_scope("generator", reuse=True):的行非常重要,因为需要共享generator变量,以便可以使用所有训练后的参数值。 否则,您会看到奇怪的翻译结果。
以下代码显示了如何在newckpt目录中填充占位符,运行 GAN 模型并保存生成器的输出以及检查点文件:
if a.mode == "test":
from scipy import misc
image = misc.imread("ww.png").reshape(1, 256, 256, 3)
image = (image / 255.0) * 2 - 1
result = sess.run(output_image, feed_dict={image_feed:image})
misc.imsave("result.png", result.reshape(256, 256, 3))
saver.save(sess, "newckpt/pix2pix")
图 9.1 显示了原始测试图像,其模糊版本以及经过训练的 GAN 模型的生成器输出。 结果并不理想,但是 GAN 模型确实具有更好的分辨率而没有模糊效果:

图 9.1:原始的,模糊的和生成的
- 现在,将
newckpt目录复制到/tmp,我们可以如下冻结模型:
python tensorflow/python/tools/freeze_graph.py \
--input_meta_graph=/tmp/newckpt/pix2pix.meta \
--input_checkpoint=/tmp/newckpt/pix2pix \
--output_graph=/tmp/newckpt/pix2pix.pb \
--output_node_names="generator_1/deprocess/truediv" \
--input_binary=true
- 生成的
pix2pix.pb模型文件很大,约为 217MB,将其加载到 iOS 或 Android 设备上时会崩溃或导致内存不足(OOM)错误。 我们必须像在第 6 章,“使用自然语言描述图像”的复杂 im2txt 模型中所做的那样,将其转换为 iOS 的映射格式。
bazel-bin/tensorflow/tools/graph_transforms/transform_graph \
--in_graph=/tmp/newckpt/pix2pix.pb \
--out_graph=/tmp/newckpt/pix2pix_transformed.pb \
--inputs="image_feed" \
--outputs="generator_1/deprocess/truediv" \
--transforms='strip_unused_nodes(type=float, shape="1,256,256,3")
fold_constants(ignore_errors=true, clear_output_shapes=true)
fold_batch_norms
fold_old_batch_norms'
bazel-bin/tensorflow/contrib/util/convert_graphdef_memmapped_format \
--in_graph=/tmp/newckpt/pix2pix_transformed.pb \
--out_graph=/tmp/newckpt/pix2pix_transformed_memmapped.pb
pix2pix_transformed_memmapped.pb模型文件现在可以在 iOS 中使用。
- 要为 Android 构建模型,我们需要量化冻结的模型,以将模型大小从 217MB 减少到约 54MB:
bazel-bin/tensorflow/tools/graph_transforms/transform_graph \
--in_graph=/tmp/newckpt/pix2pix.pb \
--out_graph=/tmp/newckpt/pix2pix_transformed_quantized.pb --inputs="image_feed" \
--outputs="generator_1/deprocess/truediv" \
--transforms='quantize_weights'
现在,让我们看看如何在移动应用中使用两个 GAN 模型。
在 iOS 中使用 GAN 模型
如果您尝试在 iOS 应用中使用 TensorFlow 窗格并加载gan_mnist.pb文件,则会收到错误消息:
Could not create TensorFlow Graph: Invalid argument: No OpKernel was registered to support Op 'RandomStandardNormal' with these attrs. Registered devices: [CPU], Registered kernels:
<no registered kernels>
[[Node: z_1/RandomStandardNormal = RandomStandardNormal[T=DT_INT32, _output_shapes=[[50,100]], dtype=DT_FLOAT, seed=0, seed2=0](z_1/shape)]]
将行添加到tf_op_files.txt之后,请确保tensorflow/contrib/makefile/tf_op_files.txt文件具有tensorflow/core/kernels/random_op.cc,该文件实现了RandomStandardNormal操作,并且libtensorflow-core.a是由 tensorflow/contrib/makefile/build_all_ios.sh 构建的。
此外,如果即使在使用 TensorFlow 1.4 构建的自定义 TensorFlow 库中尝试加载pix2pix_transformed_memmapped.pb,也会出现以下错误:
No OpKernel was registered to support Op 'FIFOQueueV2' with these attrs. Registered devices: [CPU], Registered kernels:
<no registered kernels>
[[Node: batch/fifo_queue = FIFOQueueV2[_output_shapes=[[]], capacity=32, component_types=[DT_STRING, DT_FLOAT, DT_FLOAT], container="", shapes=[[], [256,256,1], [256,256,2]], shared_name=""]()]]
您需要将tensorflow/core/kernels/fifo_queue_op.cc添加到tf_op_files.txt并重建 iOS 库。 但是,如果您使用 TensorFlow 1.5 或 1.6,则tensorflow/core/kernels/fifo_queue_op.cc文件已经添加到tf_op_files.txt文件中。 在每个新版本的 TensorFlow 中,默认情况下,越来越多的内核被添加到tf_op_files.txt。
借助为模型构建的 TensorFlow iOS 库,让我们在 Xcode 中创建一个名为 GAN 的新项目,并像在第 8 章,“使用 RNN 预测股价”一样在该项目中设置 TensorFlow。 以及其他不使用 TensorFlow 窗格的章节。 然后将两个模型文件gan_mnist.pb和pix2pix_transformed_memmapped.pb以及一个测试图像拖放到项目中。 另外,将第 6 章,“使用自然语言描述图像”的 iOS 项目中的tensorflow_utils.h, tensorflow_utils.mm,ios_image_load.h和 ios_image_load.mm文件复制到 GAN 项目。 将ViewController.m重命名为ViewController.mm。
现在,您的 Xcode 应该类似于图 9.2:

图 9.2:在 Xcode 中显示 GAN 应用
我们将创建一个按钮,在点击该按钮时,提示用户选择一个模型以生成数字或增强图像:
- (IBAction)btnTapped:(id)sender {
UIAlertAction* mnist = [UIAlertAction actionWithTitle:@"Generate Digits" style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
_iv.image = NULL;
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSArray *arrayGreyscaleValues = [self runMNISTModel];
dispatch_async(dispatch_get_main_queue(), ^{
UIImage *imgDigit = [self createMNISTImageInRect:_iv.frame values:arrayGreyscaleValues];
_iv.image = imgDigit;
});
});
}];
UIAlertAction* pix2pix = [UIAlertAction actionWithTitle:@"Enhance Image" style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
_iv.image = [UIImage imageNamed:image_name];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSArray *arrayRGBValues = [self runPix2PixBlurryModel];
dispatch_async(dispatch_get_main_queue(), ^{
UIImage *imgTranslated = [self createTranslatedImageInRect:_iv.frame values:arrayRGBValues];
_iv.image = imgTranslated;
});
});
}];
UIAlertAction* none = [UIAlertAction actionWithTitle:@"None" style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {}];
UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"Use GAN to" message:nil preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:mnist];
[alert addAction:pix2pix];
[alert addAction:none];
[self presentViewController:alert animated:YES completion:nil];
}
这里的代码非常简单。 应用的主要功能通过以下四种方法实现: runMNISTModel , runPix2PixBlurryModel , createMNISTImageInRect 和 createTranslatedImageInRect 。
使用基本 GAN 模型
在runMNISTModel中,我们调用辅助方法LoadModel来加载 GAN 模型,然后将输入张量设置为具有正态分布(均值 0.0 和 std 1.0)的 100 个随机数的 6 批。 该模型期望具有正态分布的随机输入。 您可以将 6 更改为任何其他数字,然后取回该数字的生成位数:
- (NSArray*) runMNISTModel {
tensorflow::Status load_status;
load_status = LoadModel(@"gan_mnist", @"pb", &tf_session);
if (!load_status.ok()) return NULL;
std::string input_layer = "z_placeholder";
std::string output_layer = "Sigmoid_1";
tensorflow::Tensor input_tensor(tensorflow::DT_FLOAT, tensorflow::TensorShape({6, 100}));
auto input_map = input_tensor.tensor<float, 2>();
unsigned seed = (unsigned)std::chrono::system_clock::now().time_since_epoch().count();
std::default_random_engine generator (seed);
std::normal_distribution<double> distribution(0.0, 1.0);
for (int i = 0; i < 6; i++){
for (int j = 0; j < 100; j++){
double number = distribution(generator);
input_map(i,j) = number;
}
}
runMNISTModel方法中的其余代码运行模型,获得6 * 28 * 28浮点数的输出,表示每批像素大小为28 * 28的图像在每个像素处的灰度值,并调用方法createMNISTImageInRect,以便在将图像上下文转换为UIImage之前,先使用 UIBezierPath在图像上下文中呈现数字,然后将其返回并显示在UIImageView中:
std::vector<tensorflow::Tensor> outputs;
tensorflow::Status run_status = tf_session->Run({{input_layer, input_tensor}},
{output_layer}, {}, &outputs);
if (!run_status.ok()) {
LOG(ERROR) << "Running model failed: " << run_status;
return NULL;
}
tensorflow::string status_string = run_status.ToString();
tensorflow::Tensor* output_tensor = &outputs[0];
const Eigen::TensorMap<Eigen::Tensor<float, 1, Eigen::RowMajor>, Eigen::Aligned>& output = output_tensor->flat<float>();
const long count = output.size();
NSMutableArray *arrayGreyscaleValues = [NSMutableArray array];
for (int i = 0; i < count; ++i) {
const float value = output(i);
[arrayGreyscaleValues addObject:[NSNumber numberWithFloat:value]];
}
return arrayGreyscaleValues;
}
createMNISTImageInRect的定义如下-我们在第 7 章,“使用 CNN 和 LSTM 识别绘画”中使用了类似的技术:
- (UIImage *)createMNISTImageInRect:(CGRect)rect values:(NSArray*)greyscaleValues
{
UIGraphicsBeginImageContextWithOptions(CGSizeMake(rect.size.width, rect.size.height), NO, 0.0);
int i=0;
const int size = 3;
for (NSNumber *val in greyscaleValues) {
float c = [val floatValue];
int x = i%28;
int y = i/28;
i++;
CGRect rect = CGRectMake(145+size*x, 50+y*size, size, size);
UIBezierPath *path = [UIBezierPath bezierPathWithRect:rect];
UIColor *color = [UIColor colorWithRed:c green:c blue:c alpha:1.0];
[color setFill];
[path fill];
}
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
对于每个像素,我们绘制一个宽度和高度均为 3 的小矩形,并为该像素返回灰度值。
使用高级 GAN 模型
在runPix2PixBlurryModel方法中,我们使用LoadMemoryMappedModel方法加载pix2pix_transformed_memmapped.pb模型文件,并加载测试图像并设置输入张量,其方式与第 4 章,“以惊人的艺术样式迁移图片”相同:
- (NSArray*) runPix2PixBlurryModel {
tensorflow::Status load_status;
load_status = LoadMemoryMappedModel(@"pix2pix_transformed_memmapped", @"pb", &tf_session, &tf_memmapped_env);
if (!load_status.ok()) return NULL;
std::string input_layer = "image_feed";
std::string output_layer = "generator_1/deprocess/truediv";
NSString* image_path = FilePathForResourceName(@"ww", @"png");
int image_width;
int image_height;
int image_channels;
std::vector<tensorflow::uint8> image_data = LoadImageFromFile([image_path UTF8String], &image_width, &image_height, &image_channels);
然后我们运行模型,获得256 * 256 * 3(图像大小为256 * 256,RGB 具有 3 个值)浮点数的输出,并调用createTranslatedImageInRect将数字转换为UIImage:
std::vector<tensorflow::Tensor> outputs;
tensorflow::Status run_status = tf_session->Run({{input_layer, image_tensor}},
{output_layer}, {}, &outputs);
if (!run_status.ok()) {
LOG(ERROR) << "Running model failed: " << run_status;
return NULL;
}
tensorflow::string status_string = run_status.ToString();
tensorflow::Tensor* output_tensor = &outputs[0];
const Eigen::TensorMap<Eigen::Tensor<float, 1, Eigen::RowMajor>, Eigen::Aligned>& output = output_tensor->flat<float>();
const long count = output.size(); // 256*256*3
NSMutableArray *arrayRGBValues = [NSMutableArray array];
for (int i = 0; i < count; ++i) {
const float value = output(i);
[arrayRGBValues addObject:[NSNumber numberWithFloat:value]];
}
return arrayRGBValues;
最终方法createTranslatedImageInRect定义如下,所有这些都很容易解释:
- (UIImage *)createTranslatedImageInRect:(CGRect)rect values:(NSArray*)rgbValues
{
UIGraphicsBeginImageContextWithOptions(CGSizeMake(wanted_width, wanted_height), NO, 0.0);
for (int i=0; i<256*256; i++) {
float R = [rgbValues[i*3] floatValue];
float G = [rgbValues[i*3+1] floatValue];
float B = [rgbValues[i*3+2] floatValue];
const int size = 1;
int x = i%256;
int y = i/256;
CGRect rect = CGRectMake(size*x, y*size, size, size);
UIBezierPath *path = [UIBezierPath bezierPathWithRect:rect];
UIColor *color = [UIColor colorWithRed:R green:G blue:B alpha:1.0];
[color setFill];
[path fill];
}
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
现在,在 iOS 模拟器或设备中运行该应用,点击 GAN 按钮,然后选择生成数字,您将看到 GAN 生成的手写数字的结果,如图 9.3 所示:

图 9.3:显示 GAN 模型选择和生成的手写数字结果
这些数字看起来很像真实的人类手写数字,都是在训练了基本 GAN 模型之后完成的。 如果您返回并查看进行训练的代码,并且停下来思考一下 GAN 的工作原理,一般来说,则生成器和判别器如何相互竞争,以及尝试达到稳定的纳什均衡状态,在这种状态下,生成器可以生成判别器无法分辨出真实还是伪造的真实假数据,您可能会更欣赏 GAN 的魅力。
现在,让我们选择Enhance Image选项,您将在图 9.4 中看到结果,该结果与图 9.1 中的 Python 测试代码生成的结果相同:

图 9.4:iOS 上原始的模糊和增强图像
你知道该怎么做。 是时候将我们的爱献给 Android 了。
在 Android 中使用 GAN 模型
事实证明,我们不需要使用自定义的 TensorFlow Android 库,就像我们在第 7 章,“通过 CNN 和 LSTM 识别绘画”中所做的那样,即可在 Android 中运行 GAN 模型。 只需创建一个具有所有默认设置的名为 GAN 的新 Android Studio 应用,将compile 'org.tensorflow:tensorflow-android:+'添加到应用的build.gradle文件,创建一个新的素材文件夹,然后复制两个 GAN 模型文件和一个测试模糊图像。
现在,您在 Android Studio 中的项目应如图 9.5 所示:

图 9.5:Android Studio GAN 应用概述,显示常量定义
请注意,为简单起见,我们将BATCH_SIZE设置为 1。您可以轻松地将其设置为任何数字,并像在 iOS 中一样获得很多输出。 除了图 9.5 中定义的常量之外,我们还将创建一些实例变量:
private Button mButtonMNIST;
private Button mButtonPix2Pix;
private ImageView mImageView;
private Bitmap mGeneratedBitmap;
private boolean mMNISTModel;
private TensorFlowInferenceInterface mInferenceInterface;
应用布局由一个ImageView和两个按钮组成,就像我们之前所做的那样,它们在onCreate方法中实例化:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mButtonMNIST = findViewById(R.id.mnistbutton);
mButtonPix2Pix = findViewById(R.id.pix2pixbutton);
mImageView = findViewById(R.id.imageview);
try {
AssetManager am = getAssets();
InputStream is = am.open(IMAGE_NAME);
Bitmap bitmap = BitmapFactory.decodeStream(is);
mImageView.setImageBitmap(bitmap);
} catch (IOException e) {
e.printStackTrace();
}
然后,为两个按钮设置两个单击监听器:
mButtonMNIST.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mMNISTModel = true;
Thread thread = new Thread(MainActivity.this);
thread.start();
}
});
mButtonPix2Pix.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try {
AssetManager am = getAssets();
InputStream is = am.open(IMAGE_NAME);
Bitmap bitmap = BitmapFactory.decodeStream(is);
mImageView.setImageBitmap(bitmap);
mMNISTModel = false;
Thread thread = new Thread(MainActivity.this);
thread.start();
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
轻按按钮后,run方法在辅助线程中运行:
public void run() {
if (mMNISTModel)
runMNISTModel();
else
runPix2PixBlurryModel();
}
使用基本 GAN 模型
在runMNISTModel方法中,我们首先为模型准备一个随机输入:
void runMNISTModel() {
float[] floatValues = new float[BATCH_SIZE*100];
Random r = new Random();
for (int i=0; i<BATCH_SIZE; i++) {
for (int j=0; i<100; i++) {
double sample = r.nextGaussian();
floatValues[i] = (float)sample;
}
}
然后将输入提供给模型,运行模型并获得输出值,它们是介于 0.0 到 1.0 之间的缩放灰度值,并将它们转换为 0 到 255 范围内的整数:
float[] outputValues = new float[BATCH_SIZE * 28 * 28];
AssetManager assetManager = getAssets();
mInferenceInterface = new TensorFlowInferenceInterface(assetManager, MODEL_FILE1);
mInferenceInterface.feed(INPUT_NODE1, floatValues, BATCH_SIZE, 100);
mInferenceInterface.run(new String[] {OUTPUT_NODE1}, false);
mInferenceInterface.fetch(OUTPUT_NODE1, outputValues);
int[] intValues = new int[BATCH_SIZE * 28 * 28];
for (int i = 0; i < intValues.length; i++) {
intValues[i] = (int) (outputValues[i] * 255);
}
之后,对于创建位图时设置的每个像素,我们使用返回和转换的灰度值:
try {
Bitmap bitmap = Bitmap.createBitmap(28, 28, Bitmap.Config.ARGB_8888);
for (int y=0; y<28; y++) {
for (int x=0; x<28; x++) {
int c = intValues[y*28 + x];
int color = (255 & 0xff) << 24 | (c & 0xff) << 16 | (c & 0xff) << 8 | (c & 0xff);
bitmap.setPixel(x, y, color);
}
}
mGeneratedBitmap = Bitmap.createBitmap(bitmap);
}
catch (Exception e) {
e.printStackTrace();
}
最后,我们在主 UI 线程的 ImageView 中显示位图:
runOnUiThread(
new Runnable() {
@Override
public void run() {
mImageView.setImageBitmap(mGeneratedBitmap);
}
});
}
如果现在运行该应用,并使用void runPix2PixBlurryModel() {}的空白实现来避免生成错误,则在单击GENERATE DIGITS后会看到初始屏幕和结果,如图 9.6 所示:

图 9.6:显示生成的数字
使用高级 GAN 模型
runPix2PixBlurryModel方法类似于前面几章中的代码,在前几章中,我们使用图像输入来馈入模型。 我们首先从图像位图中获取 RGB 值,然后将它们保存到float数组中:
void runPix2PixBlurryModel() {
int[] intValues = new int[WANTED_WIDTH * WANTED_HEIGHT];
float[] floatValues = new float[WANTED_WIDTH * WANTED_HEIGHT * 3];
float[] outputValues = new float[WANTED_WIDTH * WANTED_HEIGHT * 3];
try {
Bitmap bitmap = BitmapFactory.decodeStream(getAssets().open(IMAGE_NAME));
Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, WANTED_WIDTH, WANTED_HEIGHT, true);
scaledBitmap.getPixels(intValues, 0, scaledBitmap.getWidth(), 0, 0, scaledBitmap.getWidth(), scaledBitmap.getHeight());
for (int i = 0; i < intValues.length; ++i) {
final int val = intValues[i];
floatValues[i * 3 + 0] = (((val >> 16) & 0xFF) - IMAGE_MEAN) / IMAGE_STD;
floatValues[i * 3 + 1] = (((val >> 8) & 0xFF) - IMAGE_MEAN) / IMAGE_STD;
floatValues[i * 3 + 2] = ((val & 0xFF) - IMAGE_MEAN) / IMAGE_STD;
}
然后,我们使用输入来运行模型,并获取并将输出值转换为整数数组,该整数数组随后用于设置新位图的像素:
AssetManager assetManager = getAssets();
mInferenceInterface = new TensorFlowInferenceInterface(assetManager, MODEL_FILE2);
mInferenceInterface.feed(INPUT_NODE2, floatValues, 1, WANTED_HEIGHT, WANTED_WIDTH, 3);
mInferenceInterface.run(new String[] {OUTPUT_NODE2}, false);
mInferenceInterface.fetch(OUTPUT_NODE2, outputValues);
for (int i = 0; i < intValues.length; ++i) {
intValues[i] = 0xFF000000
| (((int) (outputValues[i * 3] * 255)) << 16)
| (((int) (outputValues[i * 3 + 1] * 255)) << 8)
| ((int) (outputValues[i * 3 + 2] * 255));
}
Bitmap outputBitmap = scaledBitmap.copy( scaledBitmap.getConfig() , true);
outputBitmap.setPixels(intValues, 0, outputBitmap.getWidth(), 0, 0, outputBitmap.getWidth(), outputBitmap.getHeight());
mGeneratedBitmap = Bitmap.createScaledBitmap(outputBitmap, bitmap.getWidth(), bitmap.getHeight(), true);
}
catch (Exception e) {
e.printStackTrace();
}
最后,我们在主 UI 的ImageView中显示位图:
runOnUiThread(
new Runnable() {
@Override
public void run() {
mImageView.setImageBitmap(mGeneratedBitmap);
}
});
}
再次运行该应用,然后立即点击增强图像按钮,您将在几秒钟内看到图 9.7 中的增强图像:

图 9.7:Android 上的模糊和增强图像
这使用两个 GAN 模型完成了我们的 Android 应用。
总结
在本章中,我们快速浏览了 GAN 的美好世界。 我们介绍了 GAN 的含义以及它们为何如此有趣的原因-生成器和判别器相互竞争并尝试击败的方式听起来对大多数人来说很有吸引力。 然后,我们详细介绍了如何训练基本 GAN 模型和更高级的图像分辨率增强模型以及如何为移动设备准备它们的详细步骤。 最后,我们向您展示了如何使用这些模型构建 iOS 和 Android 应用。 如果您对整个过程和结果感到兴奋,那么您肯定会想进一步探索 GAN,这是一个快速发展的领域,在该领域中,新型 GAN 已经迅速开发出来,以克服先前模型的缺点; 例如,正如我们在“增强图像分辨率”小节的 GAN 高级模型中看到的那样,开发了需要配对图像进行训练的 pix2pix 模型的同一位研究人员提出了一种称为 CycleGAN 的模型,删除了图像配对的要求。 如果您对我们生成的数字或增强的图像的质量不满意,则可能还应该进一步探索 GAN,以了解如何改进 GAN 模型。 正如我们之前提到的,GAN 仍很年轻,研究人员仍在努力稳定训练,如果可以稳定的话,将会取得更大的成功。 至少到目前为止,您已经获得了如何在移动应用中快速部署 GAN 模型的经验。 由您决定是关注最新,最出色的 GAN 并在移动设备上使用它们,还是暂时搁置您的移动开发人员的帽子,会全力以赴来构建新的或改进现有的 GAN 模型。
如果 GAN 在深度学习社区中引起了极大的兴奋,那么 AlphaGo 在 2016 年和 2017 年击败最优秀的人类 GO 玩家的成就无疑令所有人都感到惊讶。 此外,在 2017 年 10 月,AlphaGo Zero(一种完全基于自学强化学习而无需任何人类知识的新算法)被推举为击败 AlphaGo 100-0,令人难以置信。 2017 年 12 月,与仅在 GO 游戏中定位的 AlphaGo 和 AlphaGo Zero 不同,AlphaZero(一种可在许多具有挑战性的领域实现“超人表现”的算法)被发布。 在下一章中,我们将看到如何使用最新最酷的 AlphaZero 来构建和训练用于玩简单游戏的模型,以及如何在移动设备上运行该模型。
十、构建类似 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 在游戏上发表了另一篇论文《围棋:在没有人类知识的情况下掌握围棋游戏》,它描述了一种称为 AlphaGo Zero 的改进算法,该算法仅通过自我强化学习来学习如何玩围棋,而无需依赖任何人类专家知识,例如大量玩过的专业的围棋游戏,AlphaGo 用它来训练其模型。 令人惊讶的是,AlphaGo Zero 完全击败了 AlphaGo,后者在几个月前以 100-0 击败了世界上最好的人类 GO 玩家!
事实证明,这只是朝着 Google 更雄心勃勃的目标迈出的一步,该目标是将 AlphaGo 背后的 AI 技术应用和改进到其他领域。 2017 年 12 月,DeepMind 发表了另一篇论文,即使用通用强化学习算法通过自学掌握国际象棋和将棋,对 AlphaGo 进行了概括。 将零程序归类为一个称为 AlphaZero 的算法,并使用该算法从头开始快速学习如何玩象棋和将棋的游戏,从除了游戏规则之外没有任何领域知识的随机游戏开始,并在 24 小时内实现了超人级别并击败世界冠军。
在本章中,我们将带您浏览 AlphaZero 的最新最酷的部分,向您展示如何构建和训练类似 AlphaZero 的模型来玩一个简单而有趣的游戏,称为 Connect4,在 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 的经典著作《强化学习:简介》,该书可在以下网站上公开获得。 您还可以在 YouTube 上观看 DeepMind 的 AlphaGo 的技术负责人 David Silver 的强化学习课程视频(搜索“强化学习 David Silver”)。 一个有趣且有用的强化学习工具包是 OpenAI Gym。 在本书的最后一章中,我们将更深入地学习强化学习和 OpenAI Gym。 对于 MCTS,请查看其维基页面,以及此博客。
在下一节中,我们将研究以 TensorFlow 为后端的 Keras 实现 AlphaZero 算法,其目标是使用该算法构建和训练模型以玩 Connect4。您将看到模型架构是什么样,以及构建模型的 Keras 关键代码。
训练和测试适用于 Connect4 的类似 AlphaZero 的模型
如果您从未玩过 Connect4,则可以在这个页面上免费玩它。 这是一个快速有趣的游戏。 基本上,两个玩家轮流从一列的顶部将不同颜色的光盘放入六行乘七列的网格中。 如果尚未在该列中放入任何光盘,则新放置的光盘将位于该列的底部,或者位于该列中最后放置的光盘的顶部。 谁先在三个可能的方向(水平,垂直,对角线)中的任何一个方向上拥有自己颜色的四个连续光盘赢得比赛。
Connect4 的 AlphaZero 模型基于存储库,这是这个页面的分支, 有一个不错的博客,如何使用 Python 和 Keras 构建自己的 AlphaZero AI,您可能应该在继续之前阅读它,因此以下步骤更有意义。
训练模型
在我们看一些核心代码片段之前,让我们首先看一下如何训练模型。 首先,通过在终端上运行以下命令来获取存储库:
git clone https://github.com/jeffxtang/DeepReinforcementLearning
然后,如果尚未在第 8 章,“使用 RNN 预测股价”中设置,则设置 Keras 和 TensorFlow 虚拟环境:
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):
(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文件以供参考):

图 10.1:深度残差网络的第一层
值得注意的是,神经网络称为残差网络(ResNet),由 Microsoft 于 2015 年在 ImageNet 和 COCO 2015 竞赛的获奖作品中引入。 在 ResNet 中,使用身份映射(图 10.1 右侧的箭头)可避免在网络越深时出现更高的训练误差。 有关 ResNet 的更多信息,您可以查看原始论文《用于图像识别的深度残差学习》, 以及博客《了解深度残差网络》 - 一个简单的模块化学习框架,它重新定义了构成最新技术的内容。
深度网络的最后一层如图 10.2 所示,您可以看到,在最后的残差块和具有批量归一化和 ReLU 层的卷积层之后,将应用密集的全连接层以输出value_head and policy_head值:

图 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更改为:
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脚本按照指定的方式玩游戏后,可以使用以下命令找出结果:
grep WINS run/logs/logger_tourney.log |tail -10
对于与版本 1 对抗的版本 4,您可能会看到与以下内容相似的结果,这意味着它们处于大致相同的水平:
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更改为:
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命令,您将看到这样打印的电路板网格:
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)将移动,显示新的棋盘格,如下所示:
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 移至游戏结束后继续输入新动作,直到可能的新游戏开始:
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)评分来选择要做出的举动,并且仓库中的公式是这样的(请参见博客以及 AlphaZero 官方论文以获取更多详细信息):
edge.stats['P'] * np.sqrt(Nb) / (1 + edge.stats['N'])
如果我们将其更改为更类似于 DeepMind 的用法:
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 的导入如下:
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
四种主要的模型构建方法是:
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方法中,定义了模型输入和输出:
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方法中还定义了深度神经网络以及模型损失和优化器:
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将输出以下两行:
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:
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检查点文件:
-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脚本:
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 模型会导致以下错误:
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 所示:

图 10.3:在 Xcode 中显示 AlphaZero iOS 应用
我们只需要使用三个 UI 组件:
- 一个
UIImageView,显示棋盘和演奏的棋子。 - 显示游戏结果并提示用户采取措施的
UILabel。 - 一个
UIButton可以玩或重玩游戏。 和以前一样,我们以编程方式在viewDidLoad方法中创建和定位它们。
轻按游玩或重放按钮时,随机决定谁先走,重置表示为整数数组的棋盘,清除存储我们的移动和 AI 的移动的两个向量,以及重新绘制原始板格:
int n = rand() % 2;
aiFirst = (n==0);
if (aiFirst) aiTurn = true;
else aiTurn = false;
for (int i=0; i<PIECES_NUM; i++)
board[i] = 0;
aiMoves.clear();
humanMoves.clear();
_iv.image = [self createBoardImageInRect:_iv.frame];
然后在辅助线程上开始游戏:
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];
});
});
在playGame方法中,首先检查是否已经加载了我们的模型,如果没有加载,则进行加载:
string playGame(bool withMCTS) {
if (!_modelLoaded) {
tensorflow::Status load_status;
load_status = LoadModel(MODEL_FILE, MODEL_FILE_TYPE, &tf_session);
if (!load_status.ok()) {
LOG(FATAL) << "Couldn't load model: " << load_status;
return "";
}
_modelLoaded = YES;
}
如果轮到我们了,请返回并告诉我们。 否则,按照模型的期望将板状态转换为二进制格式的输入:
if (!aiTurn) return "Tap the column for your move";
int binary[PIECES_NUM*2];
for (int i=0; i<PIECES_NUM; i++)
if (board[i] == 1) binary[i] = 1;
else binary[i] = 0;
for (int i=0; i<PIECES_NUM; i++)
if (board[i] == -1) binary[42+i] = 1;
else binary[PIECES_NUM+i] = 0;
例如,如果板数组为[0 1 1 -1 1 -1 0 0 1 -1 -1 -1 -1 1 0 0 1 -1 1 -1 1 0 0 -1 -1 -1 1 -1 0 1 1 1 -1 -1 -1 -1 1 1 1 -1 1 1 -1],代表以下板状态(X表示 1,O表示 -1,-表示 0):
['-', 'X', 'X', 'O', 'X', 'O', '-']
['-', 'X', 'O', 'O', 'O', 'X', '-']
['-', 'X', 'O', 'X', 'O', 'X', '-']
['-', 'O', 'O', 'O', 'X', 'O', '-']
['X', 'X', 'X', 'O', 'O', 'O', 'O']
['X', 'X', 'X', 'O', 'X', 'X', 'O']
然后,使用前面的代码段构建的二进制数组将为[0 1 1 0 1 0 0 0 0 0 0 0 1 0 0 1 0 1 0 1 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 1 1 1 0 1 1 0 0 0 0 1 0 1 0 0 0 1 1 1 0 0 0 0 1 0 1 0 0 0 1 1 1 0 1 0 0 0 0 1 1 1 1 0 0 0 1 0 0 1],它在板上编码两个玩家的棋子。
仍然在playGame方法中,调用getProbs方法,该方法使用binary输入运行冻结的模型,并在probs中返回概率策略,并在策略中找到最大概率值:
float *probs = new float[PIECES_NUM];
for (int i=0; i<PIECES_NUM; i++)
probs[i] = -100.0;
if (getProbs(binary, probs)) {
int action = -1;
float max = 0.0;
for (int i=0; i<PIECES_NUM; i++) {
if (probs[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 ,则触摸处理器将忽略所有触摸手势:
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辅助方法如下:
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 输出面板中,它将打印出如下内容:
- - - - - - -
- - - - - - -
- - O - - - -
X - O - - - O
O O O X X - X
X X O O X - X
在getProbs键方法中,首先定义输入和输出节点名称,然后使用binary中的值准备输入张量:
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<float, 4>();
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];
}
}
}
现在使用输入运行模型并获取输出:
std::vector<tensorflow::Tensor> 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::Tensor<float, 1, Eigen::RowMajor>, Eigen::Aligned>& value = value_tensor->flat<float>();
const Eigen::TensorMap<Eigen::Tensor<float, 1, Eigen::RowMajor>, Eigen::Aligned>& policy = policy_tensor->flat<float>();
仅设置允许动作的概率值,然后调用softmax以使允许动作的probs值之和为 1:
vector<int> actions;
getAllowedActions(board, actions);
for (int action : actions) {
probs[action] = policy(action);
}
softmax(probs, PIECES_NUM);
return true;
}
getAllowedActions函数定义如下:
void getAllowedActions(int bd[], vector<int> &actions) {
for (int i=0; i<PIECES_NUM; i++) {
if (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函数,它们都很简单:
void softmax(float vals[], int count) {
float max = -FLT_MAX;
for (int i=0; i<count; i++) {
max = fmax(max, vals[i]);
}
float sum = 0.0;
for (int i=0; i<count; i++) {
vals[i] = exp(vals[i] - max);
sum += vals[i];
}
for (int i=0; i<count; i++) {
vals[i] /= sum;
}
}
定义了其他一些辅助函数来测试游戏结束状态:
bool aiWon(int bd[]) {
for (int i=0; i<69; i++) {
int sum = 0;
for (int j=0; j<4; j++)
sum += bd[winners[i][j]];
if (sum == 4*AI_PIECE ) return true;
}
return false;
}
bool aiLost(int bd[]) {
for (int i=0; i<69; i++) {
int sum = 0;
for (int j=0; j<4; j++)
sum += bd[winners[i][j]];
if (sum == 4*HUMAN_PIECE ) return true;
}
return false;
}
bool aiDraw(int bd[]) {
bool hasZero = false;
for (int i=0; i<PIECES_NUM; i++) {
if (bd[i] == 0) {
hasZero = true;
break;
}
}
if (!hasZero) return true;
return false;
}
bool gameEnded(int bd[]) {
if (aiWon(bd) || aiLost(bd) || aiDraw(bd)) return true;
return false;
}
aiWon和aiLost函数都使用一个常量数组,该数组定义了所有 69 个可能的获胜位置:
int winners[69][4] = {
{0,1,2,3},
{1,2,3,4},
{2,3,4,5},
{3,4,5,6},
{7,8,9,10},
{8,9,10,11},
{9,10,11,12},
{10,11,12,13},
......
{3,11,19,27},
{2,10,18,26},
{10,18,26,34},
{1,9,17,25},
{9,17,25,33},
{17,25,33,41},
{0,8,16,24},
{8,16,24,32},
{16,24,32,40},
{7,15,23,31},
{15,23,31,39},
{14,22,30,38}};
在触摸事件处理器中,首先确保轮到人了。 然后检查触摸点值是否在面板区域内,根据触摸位置获取点击的列,并更新board数组和humanMoves向量:
- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
if (aiTurn) return;
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView:self.view];
if (point.y < startY || point.y > 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绘制或重绘棋盘和所有已玩过的棋子,检查游戏状态并在游戏结束时返回结果,或者继续玩游戏,如果没有:
_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方法绘制面板:
- (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 和人工移动的所有碎片–根据谁先采取行动,它开始交替绘制碎片,但顺序不同:
int columnPieces[] = {0,0,0,0,0,0,0};
if (aiFirst) {
for (int i=0; i<aiMoves.size(); i++) {
int action = aiMoves[i];
int column = action % 7;
CGRect r = CGRectMake(startX + column * BOARD_COLUMN_WIDTH, endY - BOARD_COLUMN_WIDTH - BOARD_COLUMN_WIDTH * columnPieces[column], BOARD_COLUMN_WIDTH, BOARD_COLUMN_WIDTH);
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:r];
UIColor *color = [UIColor redColor];
[color setFill];
[path fill];
columnPieces[column]++;
if (i<humanMoves.size()) {
int action = humanMoves[i];
int column = action % 7;
CGRect r = CGRectMake(startX + column * BOARD_COLUMN_WIDTH, endY - BOARD_COLUMN_WIDTH - BOARD_COLUMN_WIDTH * columnPieces[column], BOARD_COLUMN_WIDTH, BOARD_COLUMN_WIDTH);
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:r];
UIColor *color = [UIColor yellowColor];
[color setFill];
[path fill];
columnPieces[column]++;
}
}
}
else {
for (int i=0; i<humanMoves.size(); i++) {
int action = humanMoves[i];
int column = action % 7;
CGRect r = CGRectMake(startX + column * BOARD_COLUMN_WIDTH, endY - BOARD_COLUMN_WIDTH - BOARD_COLUMN_WIDTH * columnPieces[column], BOARD_COLUMN_WIDTH, BOARD_COLUMN_WIDTH);
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:r];
UIColor *color = [UIColor yellowColor];
[color setFill];
[path fill];
columnPieces[column]++;
if (i<aiMoves.size()) {
int action = aiMoves[i];
int column = action % 7;
CGRect r = CGRectMake(startX + column * BOARD_COLUMN_WIDTH, endY - BOARD_COLUMN_WIDTH - BOARD_COLUMN_WIDTH * columnPieces[column], BOARD_COLUMN_WIDTH, BOARD_COLUMN_WIDTH);
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:r];
UIColor *color = [UIColor redColor];
[color setFill];
[path fill];
columnPieces[column]++;
}
}
}
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
现在运行该应用,您将看到类似于图 10.4 的屏幕:

图 10.4:在 iOS 上玩 Connect4
使用 AI 玩一些游戏,图 10.5 显示了一些可能的最终游戏:

图 10.5:iOS 上 Connect4 的一些游戏结果
在我们暂停之前,让我们快速看一下使用该模型并玩游戏的 Android 代码。
在 Android 中使用模型玩 Connect4
毫不奇怪,我们不需要像第 7 章,“使用 CNN 和 LSTM 识别绘画”那样使用自定义 Android 库来加载模型。 只需创建一个名称为 AlphaZero 的新 Android Studio 应用,将alphazero19.pb模型文件复制到新创建的素材资源文件夹,然后将 compile 'org.tensorflow:tensorflow-android:+' 行添加到应用的build.gradle文件。
我们首先创建一个新类BoardView,该类扩展了View并负责绘制游戏板以及 AI 和用户制作的棋子:
public class BoardView extends View {
private Path mPathBoard, mPathAIPieces, mPathHumanPieces;
private Paint mPaint, mCanvasPaint;
private Canvas mCanvas;
private Bitmap mBitmap;
private MainActivity mActivity;
private static final float MARGINX = 20.0f;
private static final float MARGINY = 210.0f;
private float endY;
private float columnWidth;
public BoardView(Context context, AttributeSet attrs) {
super(context, attrs);
mActivity = (MainActivity) context;
setPathPaint();
}
我们使用了mPathBoard,mPathAIPieces和mPathHumanPieces这三个Path实例分别绘制了板子,AI 做出的动作和人类做出的不同颜色的。 。 BoardView的绘制功能是通过Path的moveTo和lineTo方法以及Canvas的drawPath方法在onDraw方法中实现的:
protected void onDraw(Canvas canvas) {
canvas.drawBitmap(mBitmap, 0, 0, mCanvasPaint);
columnWidth = (canvas.getWidth() - 2*MARGINX) / 7.0f;
for (int i=0; i<8; i++) {
float x = MARGINX + i * columnWidth;
mPathBoard.moveTo(x, MARGINY);
mPathBoard.lineTo(x, canvas.getHeight()-MARGINY);
}
mPathBoard.moveTo(MARGINX, canvas.getHeight()-MARGINY);
mPathBoard.lineTo(MARGINX + 7*columnWidth, canvas.getHeight()-
MARGINY);
mPaint.setColor(0xFF0000FF);
canvas.drawPath(mPathBoard, mPaint);
如果 AI 首先移动,我们开始绘制第一个 AI 移动,然后绘制第一个人类移动(如果有的话),并交替绘制 AI 和人类移动的图形:
endY = canvas.getHeight()-MARGINY;
int columnPieces[] = {0,0,0,0,0,0,0};
for (int i=0; i<mActivity.getAIMoves().size(); i++) {
int action = mActivity.getAIMoves().get(i);
int column = action % 7;
float x = MARGINX + column * columnWidth + columnWidth /
2.0f;
float y = canvas.getHeight()-MARGINY-
columnWidth*columnPieces[column]-columnWidth/2.0f;
mPathAIPieces.addCircle(x,y, columnWidth/2,
Path.Direction.CW);
mPaint.setColor(0xFFFF0000);
canvas.drawPath(mPathAIPieces, mPaint);
columnPieces[column]++;
if (i<mActivity.getHumanMoves().size()) {
action = mActivity.getHumanMoves().get(i);
column = action % 7;
x = MARGINX + column * columnWidth + columnWidth /
2.0f;
y = canvas.getHeight()-MARGINY-
columnWidth*columnPieces[column]-columnWidth/2.0f;
mPathHumanPieces.addCircle(x,y, columnWidth/2,
Path.Direction.CW);
mPaint.setColor(0xFFFFFF00);
canvas.drawPath(mPathHumanPieces, mPaint);
columnPieces[column]++;
}
}
如果人先移动,则将应用类似的绘图代码,如 iOS 代码中一样。 在BoardView的public boolean onTouchEvent(MotionEvent event)内部,如果轮到 AI 了,则返回它,我们检查哪一列已被挖掘,并且如果该列还没有被全部六个可能的片断填满,则将新的人工移动添加到humanMoves MainActivity的向量,然后重绘视图:
public boolean onTouchEvent(MotionEvent event) {
if (mActivity.getAITurn()) return true;
float x = event.getX();
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
if (y < MARGINY || y > 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 根据模型的策略返回进行下一步动作,以启动新线程继续玩游戏:
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:
<TextView
android:id="@+id/textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:textAlignment="center"
android:textColor="@color/colorPrimary"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.06"/>
<com.ailabby.alphazero.BoardView
android:id="@+id/boardview"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Play"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.94" />
在MainActivity.java中,首先定义一些常量和字段:
public class MainActivity extends AppCompatActivity implements Runnable {
private static final String MODEL_FILE =
"file:///android_asset/alphazero19.pb";
private static final String INPUT_NODE = "main_input";
private static final String OUTPUT_NODE1 = "value_head/Tanh";
private static final String OUTPUT_NODE2 = "policy_head/MatMul";
private Button mButton;
private BoardView mBoardView;
private TextView mTextView;
public static final int AI_PIECE = -1;
public static final int HUMAN_PIECE = 1;
private static final int PIECES_NUM = 42;
private Boolean aiFirst = false;
private Boolean aiTurn = false;
private Vector<Integer> aiMoves = new Vector<>();
private Vector<Integer> humanMoves = new Vector<>();
private int board[] = new int[PIECES_NUM];
private static final HashMap<Integer, String> PIECE_SYMBOL;
static
{
PIECE_SYMBOL = new HashMap<Integer, String>();
PIECE_SYMBOL.put(AI_PIECE, "X");
PIECE_SYMBOL.put(HUMAN_PIECE, "O");
PIECE_SYMBOL.put(0, "-");
}
private TensorFlowInferenceInterface mInferenceInterface;
然后像在 iOS 版本的应用中一样定义所有获胜职位:
private final int winners[][] = {
{0,1,2,3},
{1,2,3,4},
{2,3,4,5},
{3,4,5,6},
{7,8,9,10},
{8,9,10,11},
{9,10,11,12},
{10,11,12,13},
...
{0,8,16,24},
{8,16,24,32},
{16,24,32,40},
{7,15,23,31},
{15,23,31,39},
{14,22,30,38}};
BoardView类使用的一些获取器和设置器:
public boolean getAITurn() {
return aiTurn;
}
public boolean getAIFirst() {
return aiFirst;
}
public Vector<Integer> getAIMoves() {
return aiMoves;
}
public Vector<Integer> getHumanMoves() {
return humanMoves;
}
public int[] getBoard() {
return board;
}
public void setAiTurn() {
aiTurn = true;
}
还有一些助手,它们是 iOS 代码的直接端口,用于检查游戏状态:
public boolean aiWon(int bd[]) {
for (int i=0; i<69; i++) {
int sum = 0;
for (int j=0; j<4; j++)
sum += bd[winners[i][j]];
if (sum == 4*AI_PIECE ) return true;
}
return false;
}
public boolean aiLost(int bd[]) {
for (int i=0; i<69; i++) {
int sum = 0;
for (int j=0; j<4; j++)
sum += bd[winners[i][j]];
if (sum == 4*HUMAN_PIECE ) return true;
}
return false;
}
public boolean aiDraw(int bd[]) {
boolean hasZero = false;
for (int i=0; i<PIECES_NUM; i++) {
if (bd[i] == 0) {
hasZero = true;
break;
}
}
if (!hasZero) return true;
return false;
}
public boolean gameEnded(int[] bd) {
if (aiWon(bd) || aiLost(bd) || aiDraw(bd)) return true;
return false;
}
getAllowedActions方法(也是 iOS 代码的直接端口)将给定板位置的所有允许的动作设置为actions向量:
void getAllowedActions(int bd[], Vector<Integer> actions) {
for (int i=0; i<PIECES_NUM; i++) {
if (i>=PIECES_NUM-7) {
if (bd[i] == 0)
actions.add(i);
}
else {
if (bd[i] == 0 && bd[i+7] != 0)
actions.add(i);
}
}
}
在onCreate方法中,实例化三个 UI 元素,并设置按钮单击监听器,以便它随机决定谁先采取行动。 当用户想要重玩游戏时,也会点击该按钮,因此我们需要在绘制面板和启动线程进行游戏之前重置aiMoves和humanMoves向量:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mButton = findViewById(R.id.button);
mTextView = findViewById(R.id.textview);
mBoardView = findViewById(R.id.boardview);
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mButton.setText("Replay");
mTextView.setText("");
Random rand = new Random();
int n = rand.nextInt(2);
aiFirst = (n==0);
if (aiFirst) aiTurn = true;
else aiTurn = false;
if (aiTurn)
mTextView.setText("Waiting for AI's move");
else
mTextView.setText("Tap the column for your move");
for (int i=0; i<PIECES_NUM; i++)
board[i] = 0;
aiMoves.clear();
humanMoves.clear();
mBoardView.drawBoard();
Thread thread = new Thread(MainActivity.this);
thread.start();
}
});
}
线程启动run方法,该方法进一步调用playGame方法,首先将板的位置转换为binary整数数组,以用作模型的输入:
public void run() {
final String result = playGame();
runOnUiThread(
new Runnable() {
@Override
public void run() {
mBoardView.invalidate();
mTextView.setText(result);
}
});
}
String playGame() {
if (!aiTurn) return "Tap the column for your move";
int binary[] = new int[PIECES_NUM*2];
for (int i=0; i<PIECES_NUM; i++)
if (board[i] == 1) binary[i] = 1;
else binary[i] = 0;
for (int i=0; i<PIECES_NUM; i++)
if (board[i] == -1) binary[42+i] = 1;
else binary[PIECES_NUM+i] = 0;
playGame方法的其余部分也几乎是 iOS 代码的直接端口,它调用getProbs方法以使用为所有操作返回的概率值来获取所有允许的操作中的最大概率值, 该模型的策略输出中总共包括 42 个法律和非法的:
float probs[] = new float[PIECES_NUM];
for (int i=0; i<PIECES_NUM; i++)
probs[i] = -100.0f;
getProbs(binary, probs);
int action = -1;
float max = 0.0f;
for (int i=0; i<PIECES_NUM; i++) {
if (probs[i] > max) {
max = probs[i];
action = i;
}
}
board[action] = AI_PIECE;
printBoard(board);
aiMoves.add(action);
if (aiWon(board)) return "AI Won!";
else if (aiLost(board)) return "You Won!";
else if (aiDraw(board)) return "Draw";
aiTurn = false;
return "Tap the column for your move";
}
如果尚未加载getProbs方法,则加载模型;使用当前板状态作为输入运行模型;并在调用softmax以获得真实概率值之前获取输出策略,该值之和对于允许的动作为 1:
void getProbs(int binary[], float probs[]) {
if (mInferenceInterface == null) {
AssetManager assetManager = getAssets();
mInferenceInterface = new
TensorFlowInferenceInterface(assetManager, MODEL_FILE);
}
float[] floatValues = new float[2`6`7];
for (int i=0; i<2`6`7; i++) {
floatValues[i] = binary[i];
}
float[] value = new float[1];
float[] policy = new float[42];
mInferenceInterface.feed(INPUT_NODE, floatValues, 1, 2, 6, 7);
mInferenceInterface.run(new String[] {OUTPUT_NODE1, OUTPUT_NODE2},
false);
mInferenceInterface.fetch(OUTPUT_NODE1, value);
mInferenceInterface.fetch(OUTPUT_NODE2, policy);
Vector<Integer> actions = new Vector<>();
getAllowedActions(board, actions);
for (int action : actions) {
probs[action] = policy[action];
}
softmax(probs, PIECES_NUM);
}
softmax方法的定义与 iOS 版本中的定义几乎相同:
void softmax(float vals[], int count) {
float maxval = -Float.MAX_VALUE;
for (int i=0; i<count; i++) {
maxval = max(maxval, vals[i]);
}
float sum = 0.0f;
for (int i=0; i<count; i++) {
vals[i] = (float)exp(vals[i] - maxval);
sum += vals[i];
}
for (int i=0; i<count; i++) {
vals[i] /= sum;
}
}
现在,在 Android 虚拟或真实设备上运行该应用并使用该应用进行游戏,您将看到初始屏幕和一些游戏结果:

图 10.6:在 Android 上显示游戏板和一些结果
当您使用前面的代码在 iOS 和 Android 上玩游戏时,很快就会发现该模型返回的策略并不强大-主要原因是 MCTS 没有出现在这里,由于范围限制,不会与深度神经网络模型一起使用。 强烈建议您自己研究和实现 MCTS,或者在源代码存储库中使用我们的实现作为参考。 您还应该将网络模型和 MCTS 应用于您感兴趣的其他游戏-毕竟,AlphaZero 使用了通用 MCTS 和无领域知识的自我强化学习,从而使超人学习轻松移植到其他问题领域。 通过将 MCTS 与深度神经网络模型结合,您可以实现 AlphaZero 所做的事情。
总结
在本章中,我们介绍了 AlphaZero 的惊人世界,这是 DeepMind 截至 2017 年 12 月的最新和最大成就。我们向您展示了如何使用功能强大的 Keras API 和 TensorFlow 后端为 Connect4 训练类似 AlphaZero 的模型,以及如何测试并可能改善这种模型。 然后,我们冻结了该模型,并详细介绍了如何构建 iOS 和 Android 应用以使用该模型,以及如何使用基于模型的 AI 玩 Connect4。 尚不能完全击败人类象棋或 GO 冠军的确切 AlphaZero 模型,但我们希望本章为您提供扎实的基础,并激发您继续进行工作,以复制 AlphaZero 最初所做的工作并将其进一步扩展到其他问题领域。 这将需要很多努力,但完全值得。
如果最新的 AI 进展(例如 AlphaZero)使您兴奋不已,那么您还可能会发现由 TensorFlow 驱动的最新移动平台解决方案或工具包令人兴奋。 如我们在第 1 章“移动 TensorFlow 入门”中提到的,TensorFlow Lite 是 TensorFlow Mobile 的替代解决方案,我们在前面的所有章节中都有介绍。 根据 Google 的说法,TensorFlow Lite 将成为 TensorFlow 在移动设备上的未来,尽管在此时和可预见的将来,TensorFlow Mobile 仍应用于生产场合。
虽然 TensorFlow Lite 在 iOS 和 Android 上均可使用,但在 Android 设备上运行时,它也可以利用 Android Neural Networks API 进行硬件加速。 另一方面,iOS 开发人员可以利用 Core ML, Apple 针对 iOS 11 或更高版本的最新机器学习框架,该框架支持运行许多强大的预训练深度学习模型,以及使用经典的机器学习算法和 Keras,以优化的方式在设备上以最小的应用二进制文件大小运行。 在下一章中,我们将介绍如何在 iOS 和 Android 应用中使用 TensorFlow Lite 和 Core ML。
十一、在移动设备上使用 TensorFlow Lite 和 Core ML
在前九章中,我们使用 TensorFlow Mobile 在移动设备上运行各种由 TensorFlow 和 Keras 构建的强大的深度学习模型。 正如我们在第 1 章,“移动 TensorFlow 入门”中提到的那样,Google 还提供了 TensorFlow Lite(可替代 TensorFlow Mobile 的版本)在移动设备上运行模型。 尽管自 Google I/O 2018 起它仍在开发人员预览中,但 Google 打算“大大简化开发人员针对小型设备的模型定位的体验。” 因此,值得详细研究 TensorFlow Lite 并为未来做好准备。
如果您是 iOS 开发人员,或者同时使用 iOS 和 Android,则 Apple 一年一度的全球开发人员大会(WWDC)是您不容错过的活动。 在 WWDC 2017 中,Apple 宣布了新的 Core ML 框架,以支持 iOS(以及所有其他 Apple OS 平台:macOS,tvOS 和 watchOS)上的深度学习模型和标准机器学习模型的运行。 自 iOS 11 起,Core ML 就可用了,截至 2018 年 5 月,Core ML 已占到 80% 的标记份额。至少了解您可以在 iOS 应用中使用 Core ML 的基本知识绝对有意义。
因此,我们将在本章涵盖 TensorFlow Lite 和 Core ML,并通过以下主题展示两者的优势和局限性:
- TensorFlow Lite - 概述
- 在 iOS 中使用 TensorFlow Lite
- 在 Android 中使用 TensorFlow Lite
- 适用于 iOS 的 CoreML - 概述
- 结合使用 CoreML 和 Scikit-Learn 机器学习
- 将 CoreML 与 Keras 和 TensorFlow 结合使用
TensorFlow Lite – 概述
TensorFlow Lite 是一种轻量级解决方案,可在移动和嵌入式设备上运行深度学习模型。 如果可以将 TensorFlow 或 Keras 内置的模型成功转换为 TensorFlow Lite 格式,请基于 FlatBuffers,与 ProtoBuffers ProtoBuffers 类似,但速度更快,并且大小要小得多。 “检测对象及其位置” 和,然后可以期望模型以低延迟和较小的二进制大小运行。 在您的移动应用中使用 TensorFlow Lite 的基本工作流程如下:
- 使用 TensorFlow 或 Keras 以 TensorFlow 作为后端来构建和训练(或重新训练)TensorFlow 模型,例如我们在前几章中训练的模型。
您还可以选择一个预先构建的 TensorFlow Lite 模型,例如可从以下位置获得的 MobileNet 模型,我们在第 2 章,《使用迁移学习对图像进行分类》中将其用于再训练。 您可以在此处下载的每个 MobileNet 模型tgz文件都包含转换后的 TensorFlow Lite 模型。 例如,MobileNet_v1_1.0_224.tgz文件包含一个mobilenet_v1_1.0_224.tflite文件,您可以直接在移动设备上使用它。 如果使用这样的预构建 TensorFlow Lite 模型,则可以跳过步骤 2 和 3。
-
构建 TensorFlow Lite 转换器工具。 如果您从这里下载 TensorFlow 1.5 或 1.6 版本,则可以从 TensorFlow 源根目录在终端上运行
bazel build tensorflow/contrib/lite/toco:toco。 如果您使用更高版本或获取最新的 TensorFlow 仓库,您应该可以使用此build命令来执行此操作,但如果没有,请查看该新版本的文档。 -
使用 TensorFlow Lite 转换器工具将 TensorFlow 模型转换为 TensorFlow Lite 模型。 在下一节中,您将看到一个详细的示例。
-
在 iOS 或 Android 上部署 TensorFlow Lite 模型-对于 iOS,使用 C++ API 加载和运行模型; 对于 Android,请使用 Java API(围绕 C++ API 的包装器)加载和运行模型。 与我们之前在 TensorFlow Mobile 项目中使用的
Session类不同,C++ 和 Java API 均使用 TensorFlow-lite 特定的Interpreter类来推断模型。 在接下来的两个部分中,我们将向您展示 iOS C++ 代码和 Android Java 代码以使用Interpreter。
如果您在 Android 上运行 TensorFlow Lite 模型,并且 Android 设备为 Android 8.1(API 级别 27)或更高版本,并且通过专用的神经网络硬件,GPU 或某些其他数字信号处理器支持硬件加速,则Interpreter将使用 Android 神经网络 API来加快模型运行。 例如,谷歌的 Pixel 2 手机具有针对图像处理进行了优化的自定义芯片,可以通过 Android 8.1 开启该芯片,并支持硬件加速。
现在让我们看看如何在 iOS 中使用 TensorFlow Lite。
在 iOS 中使用 TensorFlow Lite
在向您展示如何创建新的 iOS 应用并向其添加 TensorFlow Lite 支持之前,让我们首先看一下使用 TensorFlow Lite 的几个示例 TensorFlow iOS 应用。
运行示例 TensorFlow Lite iOS 应用
有两个用于 iOS 的 TensorFlow Lite 示例应用,名为simple and camera,类似于 TensorFlow Mobile iOS 应用 simple 和 camera,但在 TensorFlow 1.5-1.8 的官方版本中的 TensorFlow Lite API 中实现,并且可能也在最新的 TensorFlow 仓库中。 您可以运行以下命令来准备和运行这两个应用,类似地在“iOS 演示应用”下进行了记录:
cd tensorflow/contrib/lite/examples/ios
./download_models.sh
sudo gem install cocoapods
cd camera
pod install
open tflite_camera_example.xcworkspace
cd ../simple
pod install
open simple.xcworkspace
现在,您将有两个 Xcode iOS 项目,分别是 simple 和 camera(在 Xcode 中分别命名为 tflite_simple_example和 tflite_camera_example),并启动了,您可以在您的 iOS 设备中安装和运行它们(简单的应用也可以在您的 iOS 模拟器上运行)。
download_models.sh will download a zip file that contains the mobilenet_quant_v1_224.tflite model file and labels.txt label file, then copy them to the simple/data and camera/data directories. Notice that somehow this script is not included in the official TensorFlow 1.5.0 and 1.6.0 releases. You'll need to do git clone https://github.com/tensorflow/tensorflow and clone the latest source (as of March 2018) to get it.
您可以查看 Xcode tflite_camera_example项目的CameraExampleViewController.mm文件和tflite_simple_example RunModelViewController.mm文件中的源代码,以了解如何使用 TensorFlow Lite API 加载和运行 TensorFlow Lite 模型。 在逐步指导您如何创建新的 iOS 应用并向其添加 TensorFlow Lite 支持以运行预先构建的 TensorFlow Lite 模型的逐步教程之前,我们将快速以具体数字向您展示使用 TensorFlow Lite-应用的好处之一,如前所述,二进制大小:
位于tensorflow/examples/ios/camera文件夹中的 TensorFlow Mobile 示例应用tf_camera_example 中使用的tensorflow_inception.graph.pb模型文件为 95.7MB,而位于tensorflow/contrib/lite/examples/ios/camera文件夹中的 tflite_camera_example TensorFlow Lite 示例应用中使用的模型文件mobilenet_quant_v1_224.tflite仅 4.3MB。 TensorFlow Mobile 重新训练的 Inception 3 模型文件的量化版本,如我们在第 2 章,“通过迁移学习对图像进行分类”的 HelloTensorFlow 应用中所见,约为 22.4MB,并且重新训练的 MobileNet TensorFlow Mobile 模型文件为 17.6MB。 总之,以下列出了四种不同类型的模型的大小:
- TensorFlow Mobile Inception 3 模型:95.7MB
- 量化和重新训练的 TensorFlow Mobile Inception 3 模型:22.4MB
- 训练有素的 TensorFlow Mobile MobileNet 1.0 224 模型:17.6MB
- TensorFlow Lite MobileNet 1.0 224 模型:4.3MB
如果在 iPhone 上安装并运行这两个应用,则从 iPhone 的设置中将看到 tflite_camera_example的应用大小约为 18.7MB,tf_camera_example的大小约为 44.2MB。
的确,Inception 3 模型的准确率比 MobileNet 模型要高,但是在许多使用情况下,可以忽略很小的准确率差异。 另外,不可否认,如今的移动应用很容易占用数十 MB 的空间,在某些用例中,应用大小相差 20 或 30MB 听起来并不大,但是在较小的嵌入式设备中,大小会更加敏感,如果我们可以以更快的速度和更小的大小获得几乎相同的精度,而不会遇到太多麻烦,对于用户而言,这永远是一件好事。
在 iOS 中使用预构建的 TensorFlow Lite 模型
使用预构建的 TensorFlow Lite 模型进行图像分类,执行以下步骤来创建新的 iOS 应用并向其添加 TensorFlow Lite 支持:
- 使用 Single View 创建一个名为 HelloTFLite 的新 Xcode iOS 项目,将 Objective-C 设置为语言,然后将
tensorflow/contrib/lite/examples/ios文件夹中的ios_image_load.mm和ios_image_load.h文件添加到项目中。
If you prefer Swift as the programming language, you can refer to Chapter 2, Classifying Images with Transfer Learning, or Chapter 5, Understanding Simple Speech Commands, after following the steps here, to see how to convert the Objective-C app to a Swift app. But be aware that the TensorFlow Lite inference code still needs to be in C++ so you'll end up with a mix of Swift, Objective-C, and C++ code, with your Swift code mainly responsible for the UI and pre- and post-processing of the TensorFlow Lite inference.
-
将使用
tensorflow/contrib/lite/examples/ios/simple/data文件夹中的前面的download_models.sh脚本生成的模型文件和标签文件,以及第二章源代码文件夹中的测试图像(例如lab1.jpg)添加到项目中。 -
关闭项目并创建一个名为
Podfile的新文件,其内容如下:
platform :ios, '8.0'
target 'HelloTFLite'
pod 'TensorFlowLite'
运行pod install。 然后在 Xcode 中打开 HelloTFLite.xcworkspace,将ViewController.m重命名为ViewController.mm,并添加必要的 C++ 头文件和 TensorFlow Lite 头文件。 您的 Xcode 项目应类似于以下屏幕截图:

图 11.1:使用 TensorFlow Lite Pod 的新 Xcode iOS 项目
我们仅向您展示如何在 iOS 应用中使用 TensorFlow Lite Pod。 还有另一种将 TensorFlow Lite 添加到 iOS 的方法,类似于构建自定义 TensorFlow Mobile iOS 库的过程,我们在前几章中已经做过很多次了。 有关如何构建自己的自定义 TensorFlow Lite iOS 库的更多信息,请参阅以下位置的文档。
- 将第 2 章,“通过迁移学习对图像进行分类”的 iOS 应用中的类似 UI 代码复制到
ViewController.mm,后者使用UITapGestureRecognizer捕获屏幕上的用户手势,然后调用RunInferenceOnImage方法,该方法将加载 TensorFlow Lite 模型文件:
NSString* RunInferenceOnImage() {
NSString* graph = @"mobilenet_v1_1.0_224";
std::string input_layer_type = "float";
std::vector<int> sizes = {1, 224, 224, 3};
const NSString* graph_path = FilePathForResourceName(graph, @"tflite");
std::unique_ptr<tflite::FlatBufferModel> model(tflite::FlatBufferModel::BuildFromFile([graph_path UTF8String]));
if (!model) {
NSLog(@"Failed to mmap model %@.", graph);
exit(-1);
}
- 创建
Interpreter类的实例并设置其输入:
tflite::ops::builtin::BuiltinOpResolver resolver;
std::unique_ptr<tflite::Interpreter> interpreter;
tflite::InterpreterBuilder(*model, resolver)(&interpreter);
if (!interpreter) {
NSLog(@"Failed to construct interpreter.");
exit(-1);
}
interpreter->SetNumThreads(1);
int input = interpreter->inputs()[0];
interpreter->ResizeInputTensor(input, sizes);
if (interpreter->AllocateTensors() != kTfLiteOk) {
NSLog(@"Failed to allocate tensors.");
exit(-1);
}
与 TensorFlow Mobile 不同,TensorFlow Lite 在馈入 TensorFlow Lite 模型进行推理时使用interpreter->inputs()[0]而不是特定的输入节点名称。
- 在以与在
HelloTensorFlow应用中相同的方式加载labels.txt文件后,也以相同的方式加载要分类的图像,但是使用 TensorFlow Lite 的Interpreter的typed_tensor方法而不是 TensorFlow Mobile 的Tensor类及其tensor方法。 图 11.2 比较了用于加载和处理图像文件数据的 TensorFlow Mobile 和 Lite 代码:

图 11.2:TensorFlow Mobile(左)和 Lite 代码,用于加载和处理图像输入
- 在调用
GetTopN辅助方法以获取前N个分类结果之前,调用Interpreter上的Invoke方法运行模型,并调用typed_out_tensor方法以获取模型的输出。 TensorFlow Mobile 和 Lite 之间的代码差异如图 11.3 所示:

图 11.3:运行模型并获取输出的 TensorFlow Mobile(左)和 Lite 代码
- 以类似于 HelloTensorFlow 中方法的方式实现
GetTopN方法,对于 TensorFlow Lite 使用const float* prediction类型而不是对于 TensorFlow Mobile 使用const Eigen::TensorMap<Eigen::Tensor<float, 1, Eigen::RowMajor>, Eigen::Aligned>& prediction。 TensorFlow Mobile 和 Lite 中GetTopN方法的比较如图 11.4 所示:

图 11.4:处理模型输出以返回最高结果的 TensorFlow Mobile(左)和 Lite 模型代码
- 如果值大于阈值(设置为
0.1f),则使用简单的UIAlertController显示带有 TensorFlow Lite 模型返回的置信度值的最佳结果:
-(void) showResult:(NSString *)result {
UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"TFLite Model Result" message:result preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction* action = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil];
[alert addAction:action];
[self presentViewController:alert animated:YES completion:nil];
}
-(void)tapped:(UITapGestureRecognizer *)tapGestureRecognizer {
NSString *result = RunInferenceOnImage();
[self showResult:result];
}
立即运行 iOS 应用,然后点击屏幕以运行模型。 对于lab1.jpg测试图像,您将在图 11.5 中看到模型的结果:

图 11.5:测试图像和模型推断结果
这就是您可以在新的 iOS 应用中使用预构建的 MobileNet TensorFlow Lite 模型的方式。 现在让我们看看如何使用经过重新训练的 TensorFlow 模型。
在 iOS 中为 TensorFlow Lite 使用经过重新训练的 TensorFlow 模型
在第 2 章,”通过迁移学习对图像进行分类“中,我们重新训练了 MobileNet TensorFlow 模型来进行狗的品种识别任务,并且要在 TensorFlow Lite 中使用这种模型,我们首先需要使用 TensorFlow Lite 转换工具将其转换为 TensorFlow Lite 格式:
bazel build tensorflow/contrib/lite/toco:toco
bazel-bin/tensorflow/contrib/lite/toco/toco \
--input_file=/tmp/dog_retrained_mobilenet10_224_not_quantized.pb \
--input_format=TENSORFLOW_GRAPHDEF --output_format=TFLITE \
--output_file=/tmp/dog_retrained_mobilenet10_224_not_quantized.tflite --inference_type=FLOAT \
--input_type=FLOAT --input_array=input \
--output_array=final_result --input_shape=1,224,224,3
我们必须使用--input_array和--output_array指定输入节点名称和输出节点名称。 有关转换器工具的详细命令行参数,请参阅这里。
添加转换后的dog_retrained_mobilenet10_224_not_quantized.tflite TensorFlow Lite 模型文件,以及相同的dog_retrained_labels.txt 标签文件 HelloTensorFlow到 Xcode 项目,只需将步骤 4 中的行从 NSString* graph = @"mobilenet_v1_1.0_224"; 更改为 NSString* graph = @"dog_retrained_mobilenet10_224_not_quantized"; 和 const int output_size = 1000; 更改为 const int output_size = 121; (回想一下 MobileNet 模型对 1,000 个对象进行分类,而我们的训练后的狗模型则对 121 个犬种进行了分类),然后使用 TensorFlow Lite 格式的训练后的模型再次运行该应用。 结果将大致相同。
因此,在我们成功将其转换为 TensorFlow Lite 模型之后,使用经过重新训练的 MobileNet TensorFlow 模型非常简单。 那本书和其他地方介绍的所有那些定制模型呢?
在 iOS 中使用自定义 TensorFlow Lite 模型
在前面的章节中,我们已经训练了许多定制的 TensorFlow 模型,并将其冻结以供移动使用。 不幸的是,如果您尝试使用上一节中内置的bazel-bin/tensorflow/contrib/lite/toco/toco TensorFlow Lite 转换工具,将模型从 TensorFlow 格式转换为 TensorFlow Lite 格式,则它们都会失败,除了第 2 章, “通过迁移学习对图像进行分类”的再训练模型; 大多数错误属于“转换不受支持的操作”类型。 例如,以下命令尝试将第 3 章, “检测对象及其位置” 中的 TensorFlow 对象检测模型转换为 TensorFlow Lite 格式:
bazel-bin/tensorflow/contrib/lite/toco/toco \
--input_file=/tmp/ssd_mobilenet_v1_frozen_inference_graph.pb \
--input_format=TENSORFLOW_GRAPHDEF --output_format=TFLITE \
--output_file=/tmp/ssd_mobilenet_v1_frozen_inference_graph.tflite --inference_type=FLOAT \
--input_type=FLOAT --input_arrays=image_tensor \
--output_arrays=detection_boxes,detection_scores,detection_classes,num_detections \
--input_shapes=1,224,224,3
但是 TensorFlow 1.6 中会出现很多错误,包括:
Converting unsupported operation: TensorArrayV3
Converting unsupported operation: Enter
Converting unsupported operation: Equal
Converting unsupported operation: NonMaxSuppressionV2
Converting unsupported operation: ZerosLike
以下命令尝试将第 4 章的神经风格迁移模型转换为 TensorFlow Lite 格式:
bazel-bin/tensorflow/contrib/lite/toco/toco \
--input_file=/tmp/stylize_quantized.pb \
--input_format=TENSORFLOW_GRAPHDEF --output_format=TFLITE \
--output_file=/tmp/stylize_quantized.tflite --inference_type=FLOAT \
--inference_type=QUANTIZED_UINT8 \
--input_arrays=input,style_num \
--output_array=transformer/expand/conv3/conv/Sigmoid \
--input_shapes=1,224,224,3:26
以下命令尝试转换第 10 章中的模型:
bazel-bin/tensorflow/contrib/lite/toco/toco \
--input_file=/tmp/alphazero19.pb \
--input_format=TENSORFLOW_GRAPHDEF --output_format=TFLITE \
--output_file=/tmp/alphazero19.tflite --inference_type=FLOAT \
--input_type=FLOAT --input_arrays=main_input \
--output_arrays=value_head/Tanh,policy_head/MatMul \
--input_shapes=1,2,6,7
但是,您还将收到许多“转换不受支持的操作”错误。
截至 2018 年 3 月以及 TensorFlow 1.6 中,TensorFlow Lite 仍在开发人员预览版中,但将来的发行版将支持更多操作,因此如果您想在 TensorFlow 1.6 中尝试 TensorFlow Lite,则应该将自己限制于预训练和重新训练的 Inception 和 MobileNet 模型,同时关注将来的 TensorFlow Lite 版本。 本书前面和其他章节中介绍的更多 TensorFlow 模型有可能会在 TensorFlow 1.7 或阅读本书时成功转换为 TensorFlow Lite 格式。
但是至少到目前为止,对于使用 TensorFlow 或 Keras 构建的自定义复杂模型,很可能您将无法成功进行 TensorFlow Lite 转换,因此您应该继续使用 TensorFlow Mobile,如前几章所述。现在,除非您致力于使它们与 TensorFlow Lite 一起使用,并且不介意帮助添加更多由 TensorFlow Lite 支持的操作-毕竟 TensorFlow 是一个开源项目。
在完成 TensorFlow Lite 的介绍之前,我们将看一下如何在 Android 中使用 TensorFlow Lite。
在 Android 中使用 TensorFlow Lite
为简单起见,我们将仅演示如何在新的 Android 应用中将 TensorFlow Lite 与预构建的 TensorFlow Lite MobileNet 模型一起添加,并在此过程中发现一些有用的技巧。 有一个使用 TensorFlow Lite 的示例 Android 应用,您可能希望首先在具有 API 级别的 Android 设备上与 Android Studio 一起运行。在执行以下步骤在新的 Android 应用中使用 TensorFlow Lite 之前,至少需要 15 个(版本至少为 4.0.3)。 如果您成功构建并运行了演示应用,则在 Android 设备上移动时,应该能够通过设备摄像头和 TensorFlow Lite MobileNet 模型看到识别出的对象。
现在执行以下步骤来创建一个新的 Android 应用,并添加 TensorFlow Lite 支持以对图像进行分类,就像我们在第 2 章,“通过迁移学习对图像进行分类”中的 HelloTensorFlow Android 应用一样:
-
创建一个新的 Android Studio 项目,并将应用命名为
HelloTFLite。 将最低 SDK 设置为 API 15:Android 4.0.3,并接受所有其他默认设置。 -
创建一个新的
assets文件夹,从演示应用tensorflow/contrib/lite/java/demo/app/src/main/assets文件夹中拖放mobilenet_quant_v1_224.tfliteTensorFlow Lite 文件和labels.txt文件,以及测试图像到 HelloTFLite 应用的assets文件夹中。 -
将
ImageClassifier.java文件从tensorflow/contrib/lite/java/demo/app/src/main/java/com/example/android/tflitecamerademo文件夹拖放到 Android Studio 中的 HelloTFLite 应用。ImageClassifier.java包含使用 TensorFlow Lite Java API 加载和运行 TensorFlow Lite 模型的所有代码,我们将在稍后详细介绍。 -
打开应用的
build.gradle文件,在dependencies部分的末尾添加compile 'org.tensorflow:tensorflow-lite:0.1',然后在buildTypes部分的以下三行中添加[android:
aaptOptions {
noCompress "tflite"
}
这是必需的,以避免在运行应用时出现以下错误:
10185-10185/com.ailabby.hellotflite W/System.err: java.io.FileNotFoundException: This file can not be opened as a file descriptor; it is probably compressed
03-20 00:32:28.805 10185-10185/com.ailabby.hellotflite W/System.err: at android.content.res.AssetManager.openAssetFd(Native Method)
03-20 00:32:28.806 10185-10185/com.ailabby.hellotflite W/System.err: at android.content.res.AssetManager.openFd(AssetManager.java:390)
03-20 00:32:28.806 10185-10185/com.ailabby.hellotflite W/System.err: at com.ailabby.hellotflite.ImageClassifier.loadModelFile(ImageClassifier.java:173)
现在,Android Studio 中的 HelloTFLite 应用应类似于图 11.6:

图 11.6:使用 TensorFlow Lite 和预构建的 MobileNet 图像分类模型的新 Android 应用
- 像以前一样,在
activity_main.xml中添加ImageView和Button,然后在MainActivity.java的onCreate方法中,将ImageView设置为测试图像的内容,然后单击Button的监听器以启动新线程,并实例化名为classifier的ImageClassifier实例:
private ImageClassifier classifier;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
try {
classifier = new ImageClassifier(this);
} catch (IOException e) {
Log.e(TAG, "Failed to initialize an image classifier.");
}
- 线程的
run方法将测试图像数据读入Bitmap,调用ImageClassifier的classifyFrame方法,并将结果显示为Toast:
Bitmap bitmap = BitmapFactory.decodeStream(getAssets().open(IMG_FILE));
Bitmap croppedBitmap = Bitmap.createScaledBitmap(bitmap, INPUT_SIZE, INPUT_SIZE, true);
if (classifier == null ) {
Log.e(TAG, "Uninitialized Classifier or invalid context.");
return;
}
final String result = classifier.classifyFrame(croppedBitmap);
runOnUiThread(
new Runnable() {
@Override
public void run() {
mButton.setText("TF Lite Classify");
Toast.makeText(getApplicationContext(), result, Toast.LENGTH_LONG).show();
}
});
如果立即运行该应用,您将看到测试图像和一个标题为“TF Lite 分类”的按钮。 轻按它,您将看到分类结果,例如“拉布拉多犬:0.86 哈巴狗:0.05 达尔马提亚狗:0.04”。
ImageClassifier中与 TensorFlow Lite 相关的代码使用核心org.tensorflow.lite.Interpreter类及其run方法来运行模型,如下所示:
import org.tensorflow.lite.Interpreter;
public class ImageClassifier {
private Interpreter tflite;
private byte[][] labelProbArray = null;
ImageClassifier(Activity activity) throws IOException {
tflite = new Interpreter(loadModelFile(activity));
...
}
String classifyFrame(Bitmap bitmap) {
if (tflite == null) {
Log.e(TAG, "Image classifier has not been initialized;
Skipped.");
return "Uninitialized Classifier.";
}
convertBitmapToByteBuffer(bitmap);
tflite.run(imgData, labelProbArray);
...
}
并定义了loadModelFile方法:
private MappedByteBuffer loadModelFile(Activity activity) throws IOException {
AssetFileDescriptor fileDescriptor = activity.getAssets().openFd(MODEL_PATH);
FileInputStream inputStream = new FileInputStream(fileDescriptor.getFileDescriptor());
FileChannel fileChannel = inputStream.getChannel();
long startOffset = fileDescriptor.getStartOffset();
long declaredLength = fileDescriptor.getDeclaredLength();
return fileChannel.map(FileChannel.MapMode.READ_ONLY, startOffset, declaredLength);
}
回想一下,在步骤 4 中,我们必须在build.gradle文件中添加noCompress "tflite",否则openFd方法将导致错误。 该方法返回模型的映射版本,我们在第 6 章,“使用自然语言描述图像”时使用convert_graphdef_memmapped_format工具将 TensorFlow Mobile 模型转换为映射格式。 和第 9 章,“使用 GAN 生成和增强图像”。
这就是在新的 Android 应用中加载并运行预构建的 TensorFlow Lite 模型所需的一切。 如果您有兴趣使用经过重新训练和转换的 TensorFlow Lite 模型(如我们在 iOS 应用,Android 应用中所做的那样),或者自定义 TensorFlow Lite 模型(如果您成功获得了转换后的模型),则可以在 HelloTFLite 应用。 我们将暂时保留最先进的 TensorFlow Lite,并继续为 iOS 开发人员介绍另一个非常酷的 WWDC 重量级主题。
适用于 iOS 的 Core ML – 概述
苹果的 Core ML 框架使 iOS 开发人员可以轻松地在运行 iOS 11 或更高版本的 iOS 应用中使用经过训练的机器学习模型,并构建 Xcode 9 或更高版本。 您可以下载并使用 Apple 已在这里提供的 Core ML 格式的经过预训练的模型,也可以使用称为 coremltools 的 Python 工具,Core ML 社区工具来将其他机器学习和深度学习模型转换为 Core ML 格式。
Core ML 格式的预训练模型包括流行的 MobileNet 和 Inception V3 模型,以及更新的 ResNet50 模型(我们在第 10 章中简要讨论了残差网络)。 可以转换为 Core ML 格式的模型包括使用 Caffe 或 Keras 构建的深度学习模型,以及传统的机器学习模型,例如线性回归,支持向量机和通过 Scikit Learn 构建的决策树,这是一个非常流行的 Python 机器学习库。
因此,如果您想在 iOS 中使用传统的机器学习模型,那么 Scikit Learn 和 Core ML 绝对是必经之路。 尽管这是一本有关移动 TensorFlow 的书,但构建智能应用有时不需要深度学习。 在某些用例中,经典机器学习完全有意义。 此外,Core ML 对 Scikit Learn 模型的支持是如此流畅,以至于我们不能拒绝快速浏览,因此您将在必要时知道何时短暂使用移动 TensorFlow 技能。
如果要使用 Apple 预训练的 MobileNet Core ML 模型,请在这个页面上查看 Apple 不错的示例代码项目,它使用 Vision 和 Core ML 对图像进行分类,还观看这个页面上列出的有关核心 ML 的 WWDC 2017 视频。
在接下来的两个部分中,我们将向您展示两个教程,该教程以 TensorFlow 为后端,在 Keras 中如何转换和使用 Scikit Learn 模型和股票预测 RNN 模型,它们是在第 8 章, “使用 RNN 预测股票价格”。 您将在 Objective-C 和 Swift 中看到使用源代码从头开始构建的完整 iOS 应用,以使用转换后的 Core ML 模型。 如果短语“从头开始”使您兴奋并使您想起 AlphaZero,则您可能喜欢上一章第 10 章,“构建类似 AlphaZero 的移动游戏应用”。
将 Core ML 与 Scikit-Learn 机器学习结合使用
线性回归和支持向量机是 Scikit Learn 当然支持的两种最常见的经典机器学习算法。 我们将研究如何使用这两种算法为房价预测建立模型。
建立和转换 Scikit Learn 模型
首先,让我们获取房价数据集,该数据集可从这里下载。 下载的 RealEstate.csv文件如下所示:
MLS,Location,Price,Bedrooms,Bathrooms,Size,Price/SQ.Ft,Status
132842,Arroyo Grande,795000.00,3,3,2371,335.30,Short Sale
134364,Paso Robles,399000.00,4,3,2818,141.59,Short Sale
135141,Paso Robles,545000.00,4,3,3032,179.75,Short Sale
...
我们将使用流行的开源 Python 数据分析库 Pandas 来解析 csv 文件。 要安装 Scikit Learn 和 Pandas,只需运行以下命令,最好从您之前创建的 TensorFlow 和 Keras 虚拟环境中运行以下命令:
pip install scikit-learn
pip install pandas
现在,输入以下代码以读取并解析RealEstate.csv文件,将第 4 到第 6 列(卧室,浴室和大小)下的所有行用作输入数据,并使用第 3 列(价格)的所有行作为目标输出:
from sklearn.linear_model import LinearRegression
from sklearn.svm import LinearSVR
import pandas as pd
import sklearn.model_selection as ms
data = pd.read_csv('RealEstate.csv')
X, y = data.iloc[:, 3:6], data.iloc[:, 2]
将数据集分为训练集和测试集,并使用标准fit方法使用 Scikit Learn 的线性回归模型训练数据集:
X_train, X_test, y_train, y_test = ms.train_test_split(X, y, test_size=0.25)
lr = LinearRegression()
lr.fit(X_train, y_train)
使用predict方法,使用经过训练的模型测试三个新输入(3 个卧室,2 个浴室,1,560 平方英尺,等等):
X_new = [[ 3, 2, 1560.0],
[3, 2, 1680],
[5, 3, 2120]]
print(lr.predict(X_new))
这将输出三个值作为预测房价:[319289.9552276 352603.45104977 343770.57498118]。
要训练支持向量机模型并使用X_new输入对其进行测试,类似地添加以下代码:
svm = LinearSVR(random_state=42)
svm.fit(X_train, y_train)
print(svm.predict(X_new))
这将使用支持向量机模型作为[298014.41462535 320991.94354092 404822.78465954]输出预测的房价。 我们不会讨论哪种模型更好,如何使线性回归或支持向量机模型更好地工作,或者如何在 Scikit Learn 支持的所有算法中选择更好的模型-有很多不错的书籍和在线资源介绍了这些内容。 话题。
要将两个 Scikit Learn 模型lr和svm转换为可在您的 iOS 应用中使用的 Core ML 格式,您需要首先安装 Core ML 工具。 我们建议您在我们在第 8 章,“用 RNN 预测股价”和第 10 章“构建支持 AlphaZero 的手机游戏应用”中创建的 TensorFlow 和 Keras 虚拟环境中使用pip install -U coremltools安装这些软件,因为我们还将在下一部分中使用它来转换 Keras 模型。
现在,只需运行以下代码即可将两个 Scikit Learn 模型转换为 Core ML 格式:
import coremltools
coreml_model = coremltools.converters.sklearn.convert(lr, ["Bedrooms", "Bathrooms", "Size"], "Price")
coreml_model.save("HouseLR.mlmodel")
coreml_model = coremltools.converters.sklearn.convert(svm, ["Bedrooms", "Bathrooms", "Size"], "Price")
coreml_model.save("HouseSVM.mlmodel")
有关转换器工具的更多详细信息,请参见其在线文档。 现在,我们可以将这两个模型添加到 Objective-C 或 Swift iOS 应用中,但是我们仅在此处显示 Swift 示例。 您将在下一节中看到使用从 Keras 和 TensorFlow 模型转换而来的股票预测 Core ML 模型得到的 Objective-C 和 Swift 示例。
在 iOS 中使用转换后的 Core ML 模型
在将两个 Core ML 模型文件HouseLR.mlmodel和HouseSVM.mlmodel添加到新的基于 Swift 的 Xcode iOS 项目中之后, HouseLR.mlmodel如图 11.7 所示:

图 11.7:显示 Swift iOS 项目和线性回归 Core ML 模型
其他HouseSVM.mlmodel模型看起来完全一样,只是机器学习模型名称和模型类从HouseLR更改为HouseSVM。
将以下代码添加到ViewController.swift中的class ViewController中:
private let lr = HouseLR()
private let svm = HouseSVM()
override func viewDidLoad() {
super.viewDidLoad()
let lr_input = HouseLRInput(Bedrooms: 3, Bathrooms: 2, Size: 1560)
let svm_input = HouseSVMInput(Bedrooms: 3, Bathrooms: 2, Size: 1560)
guard let lr_output = try? lr.prediction(input: lr_input) else {
return
}
print(lr_output.Price)
guard let svm_output = try? svm.prediction(input: svm_input) else {
return
}
print(svm_output.Price)
}
这应该非常简单。 运行该应用将打印:
319289.955227601 298014.414625352
它们与最后一部分中 Python 脚本输出的两个数组中的前两个数字相同,因为我们将 PythonLR 代码的X_new值中的第一个输入用于 HouseLR 和 HouseSVM 的预测输入。
将 Core ML 与 Keras 和 TensorFlow 结合使用
coremltools 工具还正式支持转换使用 Keras 构建的模型(请参见keras.convert链接)。 截至 2018 年 3 月,最新版本的 coremltools 0.8 的版本可与 TensorFlow 1.4 和 Keras 2.1.5 配合使用,我们在第 8 章,“使用 RNN 预测股票价格”中使用了 Keras 股票预测模型。 您可以使用两种方法使用 coremltools 生成模型的 Core ML 格式。 首先是在训练模型后,直接在 Python Keras 代码中调用 coremltools 的convert和save方法。 例如,将下面的最后三行代码添加到model.fit之后的ch8/python/keras/train.py文件中:
model.fit(
X_train,
y_train,
batch_size=512,
epochs=epochs,
validation_split=0.05)
import coremltools
coreml_model = coremltools.converters.keras.convert(model)
coreml_model.save("Stock.mlmodel")
对于我们的模型转换,您可以在运行新脚本时忽略以下警告:
WARNING:root:Keras version 2.1.5 detected. Last version known to be fully compatible of Keras is 2.1.3。
将生成的Stock.mlmodel文件拖放到 Xcode 9.2 iOS 项目中时,它将使用默认的输入名称input1和默认的输出名称output1,如图 11.8 所示。 基于 Objective-C 和 Swift 的 iOS 应用:

图 11.8:在 Objective-C 应用中显示从 Keras 和 TensorFlow 转换而来的股票预测 Core ML 模型
使用 coremltools 生成模型的 Core ML 格式的另一种方法是,首先将 Keras 构建的模型保存为 Keras HDF5 模型格式,这是我们在第 10 章,“构建类似 AlphaZero 的移动游戏应用”中,在转换为 AlphaZero TensorFlow 检查点文件之前使用的格式。 为此,只需运行model.save('stock.h5')。
然后,您可以使用以下代码片段将 Keras .h5模型转换为 Core ML 模型:
import coremltools
coreml_model = coremltools.converters.keras.convert('stock.h5',
input_names = ['bidirectional_1_input'],
output_names = ['activation_1/Identity'])
coreml_model.save('Stock.mlmodel')
请注意,此处使用与冻结 TensorFlow 检查点文件相同的输入和输出名称。 如果将Stock.mlmodel拖放到 Objective-C 项目,则自动生成的Stock.h中将出现错误,因为 Xcode 9.2 中的错误无法正确处理代码中的/字符activation_1/Identity输出名称。 如果它是 Swift iOS 对象,则自动生成的Stock.swift文件会正确地将/字符更改为_,从而避免了编译器错误,如图 11.9 所示。

图 11.9:在 Swift 应用中显示从 Keras 和 TensorFlow 转换而来的股票预测 Core ML 模型
要在 Objective-C 中使用该模型,请创建具有指定数据类型和形状的Stock对象和MLMultiArray对象,然后使用一些输入数据填充数组对象,并使用StockInput调用predictionFromFeatures方法用MLMultiArray数据初始化的实例:
#import "ViewController.h"
#import "Stock.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Stock *stock = [[Stock alloc] init];
double input[] = {
0.40294855,
0.39574954,
0.39789235,
0.39879138,
0.40368535,
0.41156033,
0.41556879,
0.41904324,
0.42543786,
0.42040193,
0.42384258,
0.42249741,
0.4153998 ,
0.41925279,
0.41295281,
0.40598363,
0.40289448,
0.44182321,
0.45822208,
0.44975226};
NSError *error = nil;
NSArray *shape = @[@20, @1, @1];
MLMultiArray *mlMultiArray = [[MLMultiArray alloc] initWithShape:(NSArray*)shape dataType:MLMultiArrayDataTypeDouble error:&error] ;
for (int i = 0; i < 20; i++) {
[mlMultiArray setObject:[NSNumber numberWithDouble:input[i]] atIndexedSubscript:(NSInteger)i];
}
StockOutput *output = [stock predictionFromFeatures:[[StockInput alloc] initWithInput1:mlMultiArray] error:&error];
NSLog(@"output = %@", output.output1 );
}
我们在这里使用了硬编码的规范化输入和 NSLog 只是为了演示如何使用 Core ML 模型。 如果立即运行该应用,您将看到 0.4486984312534332 的输出值,该值在非规范化后显示了预测的第二天股价。
前面代码的 Swift 版本如下:
import UIKit
import CoreML
class ViewController: UIViewController {
private let stock = Stock()
override func viewDidLoad() {
super.viewDidLoad()
let input = [
0.40294855,
0.39574954,
...
0.45822208,
0.44975226]
guard let mlMultiArray = try? MLMultiArray(shape:[20,1,1], dataType:MLMultiArrayDataType.double) else {
fatalError("Unexpected runtime error. MLMultiArray")
}
for (index, element) in input.enumerated() {
mlMultiArray[index] = NSNumber(floatLiteral: element)
}
guard let output = try? stock.prediction(input: StockInput(bidirectional_1_input:mlMultiArray)) else {
return
}
print(output.activation_1_Identity)
}
}
请注意,就像使用 TensorFlow Mobile iOS 应用一样,我们使用bidirectional_1_input和activation_1_Identity来设置输入并获取输出。
如果尝试转换在第 10 章,“构建类似 AlphaZero 的移动游戏应用”中在 Keras 中构建和训练的 AlphaZero 模型,则会收到错误消息ValueError: Unknown loss function:softmax_cross_entropy_with_logits。 如果您尝试转换我们在本书中构建的其他 TensorFlow 模型,则可以使用的最佳非官方工具是https://github.com/tf-coreml/tf-coreml上的 TensorFlow 到核心 ML 转换器。 不幸的是,类似于 TensorFlow Lite,它仅支持有限的 TensorFlow 操作集,其中一些原因是 Core ML 的限制,另一些原因是 tf-coreml 转换器的限制。 我们不会详细介绍将 TensorFlow 模型转换为 Core ML 模型的细节。 但是至少您已经了解了如何转换和使用 Scikit Learn 构建的传统机器学习模型以及基于 Keras 的 RNN 模型,该模型有望为您提供构建和使用 Core ML 模型的良好基础。 当然,如果您喜欢 Core ML,则应留意其将来的改进版本,以及 coremltools 和 tf-coreml 转换器的将来版本。 关于 Core ML,我们还没有涉及很多内容-要了解其确切功能,请参阅完整的 API 文档。
总结
在本章中,我们介绍了在移动和嵌入式设备上使用机器学习和深度学习模型的两个前沿工具:TensorFlow Lite 和 Core ML。 尽管 TensorFlow Lite 仍在开发人员预览版中,但对 TensorFlow 操作的支持有限,但其未来版本将支持越来越多的 TensorFlow 功能,同时保持较低的延迟和较小的应用大小。 我们提供了有关如何开发 TensorFlow Lite iOS 和 Android 应用以从头开始对图像进行分类的分步教程。 Core ML 是 Apple 为移动开发人员提供的将机器学习集成到 iOS 应用中的框架,它对转换和使用 Scikit Learn 构建的经典机器学习模型提供了强大的支持,并为基于 Keras 的模型提供了良好的支持。 我们还展示了如何将 Scikit Learn 和 Keras 模型转换为 Core ML 模型,以及如何在 Objective-C 和 Swift 应用中使用它们。 TensorFlow Lite 和 Core ML 现在都有一些严重的限制,导致它们无法转换我们在书中构建的复杂的 TensorFlow 和 Keras 模型。 但是他们今天已经有了用例,他们的未来会变得更好。 我们能做的最好的事情就是了解它们的用途,局限性和潜力,因此我们可以为现在或将来选择最合适的工具来完成不同的任务。 毕竟,我们不仅拥有锤子,而且并非所有东西都看起来像钉子。
在本书的下一章,最后一章中,我们将选择一些以前构建的模型,并添加强化学习的力量-强化学习的成功背后的关键技术,以及 2017 年 10 项突破性技术之一。 麻省理工学院评论–很酷的 Raspberry Pi 平台,一个小巧,价格合理但功能强大的计算机–谁不喜欢这三个的组合? 我们将看到有多少智能-,聆听,步行,平衡,当然还有学习,我们可以在一章中添加到小型 Raspberry-Pi 驱动的机器人中。 如果自动驾驶汽车是当今最热门的 AI 技术之一,那么自动行走机器人可能是我们家里最酷的玩具之一。
十二、在 Raspberry Pi 上开发 TensorFlow 应用
根据 Wikipedia 的说法,“ Raspberry Pi 是 Raspberry Pi 基金会在英国开发的一系列小型单板计算机,旨在促进学校和发展中国家的基础计算机科学教学。” Raspberry Pi 的官方网站将其描述为“一种小型且价格合理的计算机,可以用来学习编程。” 如果您以前从未听说过或使用过 Raspberry Pi,请访问其网站,然后您很快就会爱上这个很棒的小东西。 几乎没有什么功能-实际上,TensorFlow 的开发人员从 2016 年中期开始在早期版本的 Raspberry Pi 上提供了 TensorFlow,因此我们可以在微型计算机上运行复杂的 TensorFlow 模型,您只需花费 35 美元即可购买到。 这可能超出了“基础计算机科学的教学”或“学习编程”的范围,但另一方面,如果我们考虑过去几年中移动设备的所有飞速发展,那么看到如何在越来越小的设备中实现越来越多的功能,我们就不会感到惊讶。
在本章中,我们将进入 Raspberry Pi 的有趣世界,Raspberry Pi 是 TensorFlow 正式支持的最小设备。 我们将首先介绍如何获取和设置新的 Raspberry Pi 3B 板,包括本章中使用的所有必要配件,以使其能够看,听和说。 然后,我们将介绍如何使用 GoPiGo 机器人基础套件,将 Raspberry Pi 板变成一个可以移动的机器人。 之后,我们将提供最简单的工作步骤,以便在 Raspberry Pi 上设置 TensorFlow 1.6 并构建其示例 Raspberry Pi 应用。 我们还将讨论如何集成图像分类,这是我们在第 2 章,“通过迁移学习对图像进行分类”时使用的模型,并通过文字转语音功能使机器人告诉我们它可以识别的内容,以及如何集成音频识别,这是我们在第 5 章,“了解简单语音命令”中使用的模型以及 GoPiGo API,可让您使用语音命令来控制机器人的运动 。
最后,我们将向您展示如何使用 TensorFlow 和 OpenAI Gym,这是一个用于开发和比较强化学习算法的 Python 工具包,如何在模拟环境中实现强大的强化学习算法,以使我们的机器人能够在真实环境中移动和平衡身体。
在 Google I/O 2016 中,有一个名为“如何使用 Cloud Vision 和 Speech API 构建智能的 RasPi Bot”的会话(您可以在 YouTube 上观看视频)。 它使用 Google 的 Cloud API 执行图像分类以及语音识别和合成。 在本章中,我们将了解如何在设备上离线实现演示中的任务以及增强学习,从而展示 TensorFlow 在 Raspberry Pi 上的强大功能。
总而言之,我们将在本章中涵盖以下主题,以构建一个可以移动,看到,聆听,说话和学习的机器人:
- 设置 Raspberry Pi 并使其移动
- 在 Raspberry Pi 上设置 TensorFlow
- 图像识别和文字转语音
- 音频识别和机器人运动
- 在 Raspberry Pi 上进行强化学习
设置 Raspberry Pi 并使其移动
小型单板 Raspberry Pi 计算机系列包括 Raspberry Pi 3B+,3B,2B,1B+,1A+,0 和 0W(有关详细信息,请参见这里)。 我们将在此处使用 Pi 3B 主板,您可以从前面的链接或在 Amazon 上以 35 美元的价格购买。 我们在主板上使用并测试过的配件及其价格如下:
-
CanaKit 5V 2.5A Raspberry Pi 电源约 10 美元,可在开发期间使用。
-
Kinobo - 大约 4 美元的可以记录您的语音命令的 USB 2.0 微型麦克风。
-
USHONK USB 微型扬声器约合 12 美元,可以播放合成声音。
-
Arducam 5 Megapixels 1080p 传感器 OV5647 微型相机约合 14 美元,以支持图像分类。
-
16GB MicroSD 和适配器,价格约为 10 美元,用于存储 Raspbian(Raspberry Pi 的官方操作系统)的安装文件,并用作安装后的硬盘驱动器。
-
一个 USB 磁盘,例如 SanDisk 32GB USB Drive,售价 9 美元,将用作交换分区(有关详细信息,请参阅下一节) 因此我们可以手动构建 TensorFlow 库,这是构建和运行 TensorFlow C++ 代码所必需的。
-
售价 110 美元的 GoPiGo 机器人基础套件或官方网站,将 Raspberry Pi 板变成可以移动的机器人。
您还需要 HDMI 电缆将 Raspberry Pi 板连接到计算机显示器,USB 键盘和 USB 鼠标。 总共要花 200 美元,包括 110 美元的 GoPiGo,来构建一个可以移动,看,听,说的 Raspberry Pi 机器人。 尽管与功能强大的 Raspberry Pi 计算机相比,GoPiGo 套件似乎有点昂贵,但是如果没有它,那么一动不动的 Raspberry Pi 可能会失去很多吸引力。
有一个较旧的博客,“如何用 100 美元和 TensorFlow 构建“可视”的机器人”,由 Lukas Biewald 于 2016 年 9 月撰写,内容涵盖了如何使用 TensorFlow 和 Raspberry Pi 3 以及一些其他部件来构建能够说话和说话的机器人。 这很有趣。 我们这里介绍的内容除了提供语音命令识别和强化学习外,还提供了更详细的步骤来设置带有 GoPiGo 的 Raspberry Pi 3,GoPiGo(易于使用且受 Google 推荐的工具包,可将 Pi 变成机器人)以及更新版本的 TensorFlow 1.6。
现在,让我们首先看看如何设置 Raspbian,Raspberry Pi 开发板的操作系统。
设置 Raspberry Pi
最简单的方法是遵循 Raspbian 软件安装指南,总而言之,这是一个简单的三步过程:
-
为 Windows 或 Mac 下载并安装 SD 格式化程序。
-
使用 SD 格式化程序格式化 MicroSD 卡。
-
在这个页面上下载 Raspbian 的官方简易安装程序 New Out Of Box Software(NOOBS)的离线 ZIP 版本,将其解压缩,然后将提取的
NOOBS文件夹中的所有文件拖放到格式化的 MicroSD 卡中。
现在弹出 MicroSD 卡并将其插入 Raspberry Pi 板上。 将显示器的 HDMI 电缆以及 USB 键盘和鼠标连接到开发板上。 用电源为开发板供电,然后按照屏幕上的步骤完成 Raspbian 的安装,包括设置 Wifi 网络。 整个安装过程不到一个小时即可完成。 完成后,您可以打开一个终端并输入ifconfig来查找电路板的 IP 地址,然后从您的计算中使用ssh pi@<board_ip_address>来访问它,正如我们稍后将要看到的,这确实很方便并且需要在移动中测试控制 Raspberry Pi 机器人 -- 当移动时,您不想或不能将键盘,鼠标和显示器与板子一起使用。
但是默认情况下未启用 SSH,因此,当您首次尝试 SSH 到 Pi 板上时,会出现“SSH 连接被拒绝”错误。 启用它的最快方法是运行以下两个命令:
sudo systemctl enable ssh
sudo systemctl start ssh
之后,您可以使用pi登录的默认密码ssh进行登录。 当然,您可以使用passwd命令将默认密码更改为新密码。
现在我们已经安装了 Raspbian,让我们将 USB 迷你麦克风,USB 迷你扬声器和迷你相机插入 Pi 板上。 USB 麦克风和扬声器均可即插即用。 插入它们后,您可以使用aplay -l命令找出支持的音频播放设备:
aplay -l
**** List of PLAYBACK Hardware Devices ****
card 0: Device_1 [USB2.0 Device], device 0: USB Audio [USB Audio]
Subdevices: 1/1
Subdevice #0: subdevice #0
card 2: ALSA [bcm2835 ALSA], device 0: bcm2835 ALSA [bcm2835 ALSA]
Subdevices: 8/8
Subdevice #0: subdevice #0
Subdevice #1: subdevice #1
Subdevice #2: subdevice #2
Subdevice #3: subdevice #3
Subdevice #4: subdevice #4
Subdevice #5: subdevice #5
Subdevice #6: subdevice #6
Subdevice #7: subdevice #7
card 2: ALSA [bcm2835 ALSA], device 1: bcm2835 ALSA [bcm2835 IEC958/HDMI]
Subdevices: 1/1
Subdevice #0: subdevice #0
Pi 板上还有一个音频插孔,可用于在开发过程中获得音频输出。 但是 USB 扬声器肯定更方便。
要查找支持的记录设备,请使用arecord -l命令:
arecord -l
**** List of CAPTURE Hardware Devices ****
card 1: Device [USB PnP Sound Device], device 0: USB Audio [USB Audio]
Subdevices: 1/1
Subdevice #0: subdevice #0
现在,您可以使用以下命令测试音频记录:
arecord -D plughw:1,0 -d 3 test.wav
-D指定音频输入设备,这意味着它是具有卡 1,设备 0 的即插即用设备,如arecord -l命令的输出所示。 -d以秒为单位指定记录的持续时间。
要在 USB 扬声器上播放录制的音频,首先需要在主目录中创建一个名为 .asoundrc的文件,其内容如下:
pcm.!default {
type plug
slave {
pcm "hw:0,0"
}
}
ctl.!default {
type hw
card 0
}
请注意,"hw:0,0"与aplay -l为 USB 扬声器设备返回的卡 0,设备 0 信息匹配。 现在,您可以使用aplay test.wav命令在扬声器上测试录制的音频播放。
有时,Pi 板重新启动后,系统会自动更改 USB 扬声器的卡号,并且在运行aplay test.wav时您听不到声音。 在这种情况下,您可以再次运行aplay -l来找到为 USB 扬声器设置的新卡号,并相应地更新~/.asoundrc文件。
如果要调节扬声器的音量,请使用amixer set PCM -- 100%命令,其中 100% 将音量设置为最大。
要加载相机的驱动程序,请运行sudo modprobe bcm2835-v4l2命令。 之后,要验证是否已检测到摄像机,请运行vcgencmd get_camera命令,该命令应返回supported=1 detected=1 。 要在每次主板启动时加载摄像机驱动程序(这是我们需要的),请运行sudo vi /etc/modules并在/etc/modules的末尾添加一行 bcm2835-v4l2(或者您可以运行sudo bash -c "echo 'bcm2835-v4l2' >> /etc/modules")。 当我们运行 TensorFlow 图像分类示例时,我们将在后面的部分中测试相机。
这就是为我们的任务设置 Raspberry Pi 的全部内容。 现在,让我们看看如何使其移动。
使树莓派移动
GoPiGo 是一个流行的工具包,可将您的 Raspberry Pi 板变成移动的机器人。 购买并收到我们之前提到的 GoPiGo 机器人基础套件后,请按照这里将其与您的 Pi 板组装在一起。 根据您是同时观看 March March Madness 还是 NBA 季后赛比赛,这大约需要一两个小时。
完成后,您的 Raspberry Pi 机器人以及我们之前列出的所有附件应如下所示:

图 12.1:具有 GoPiGo 套件和相机,USB 扬声器和 USB 麦克风的 Raspberry Pi 机器人
现在,使用 Raspberry Pi 电源打开 Pi 机器人,并在启动后使用ssh pi@<your_pi_board_ip>连接到它。 要安装 GoPiGo Python 库,以便我们可以使用 GoPiGo 的 Python API 控制机器人,请运行以下命令 ,它将执行一个 shell 脚本,该脚本创建一个新的/home/pi/Dexter目录并在其中安装所有库和固件文件:
sudo sh -c "curl -kL dexterindustries.com/update_gopigo3 | bash"
您还应该转到~/Dexter目录并运行以下命令来更新 GoPiGo 板的固件:
bash GoPiGo3/Firmware/gopigo3_flash_firmware.sh
现在运行sudo reboot重新启动板,以使更改生效。 Pi 板重新启动后,您可以从 iPython 测试 GoPiGo 和 Raspberry Pi 的运动,可以使用sudo pip install ipython进行安装。
要测试基本的 GoPiGo Python API,请先运行 iPython,然后逐行输入以下代码:
当 GoPiGo Raspberry Pi 机器人开始移动时,请确保将其放在安全的表面上。 在最终测试期间,应使用 GoPiGo 电池组为机器人供电,使其可以自由移动。 但是在开发和初始测试中,除非您使用充电电池,否则绝对应该使用电源适配器来节省电池。 如果将机器人放在桌子上,请务必小心,因为如果发出命令导致机器人动作不佳,机器人可能会跌落。
import easygopigo3 as easy
gpg3_obj = easy.EasyGoPiGo3()
gpg3_obj.drive_cm(5)
gpg3_obj.drive_cm(-5)
gpg3_obj.turn_degrees(30)
gpg3_obj.turn_degrees(-30)
gpg3_obj.stop()
drive_cm根据其参数值是正值还是负值来向前或向后移动机器人。 turn_degrees顺时针或逆时针旋转机器人,取决于其参数值是正值还是负值。 因此,前面的示例代码将机器人向前移动 5 厘米,然后向后移动 5 厘米,顺时针旋转 30 度,然后逆时针旋转 30 度。 默认情况下,这些调用是阻塞调用,因此直到机器人完成移动后它们才返回。 要进行非阻塞调用,请添加False参数,如下所示:
gpg3_obj.drive_cm(5, False)
gpg3_obj.turn_degrees(30, False)
您还可以使用forward,backward和许多其他 API 调用来控制机器人的运动,但是在本章中,我们仅使用drive_cm和turn_degrees。
我们现在准备使用 TensorFlow 向机器人添加更多智能。
在 Raspberry Pi 上设置 TensorFlow
要在 Python 中使用 TensorFlow,就像我们稍后在“音频识别”和“强化学习”部分中所做的那样,我们可以在 TensorFlow Jenkins 持续集成站点上为 Pi 安装每晚 TensorFlow 1.6 版本:
sudo pip install http://ci.tensorflow.org/view/Nightly/job/nightly-pi/lastSuccessfulBuild/artifact/output-artifacts/tensorflow-1.6.0-cp27-none-any.whl
此方法更为常见,并在一个不错的博客条目中描述,《为 Raspberry Pi 交叉编译 TensorFlow》,作者是 Pete Warden。
一种更复杂的方法是使用makefile,这在您需要构建和使用 TensorFlow 库时是必需的。 TensorFlow 官方 makefile 文档的 Raspberry Pi 部分包含构建 TensorFlow 库的详细步骤,但是它可能不适用于每个版本的 TensorFlow。 此处的步骤与 TensorFlow 的早期版本(0.10)完美配合,但是在 TensorFlow 1.6 中会导致许多“未定义对google::protobuf的引用”错误。
TensorFlow 1.6 版本已经测试了以下步骤,可从下载 https://github.com/tensorflow/tensorflow/releases/tag/v1.6.0 ; 您当然可以在 TensorFlow 发行页面中尝试使用较新的版本,或者通过git clone https://github.com/tensorflow/tensorflow克隆最新的 TensorFlow 源,并修复所有可能的问题。
在cd之后到 TensorFlow 源根目录,然后运行以下命令:
tensorflow/contrib/makefile/download_dependencies.sh
sudo apt-get install -y autoconf automake libtool gcc-4.8 g++-4.8
cd tensorflow/contrib/makefile/downloads/protobuf/
./autogen.sh
./configure
make CXX=g++-4.8
sudo make install
sudo ldconfig # refresh shared library cache
cd ../../../../..
export HOST_NSYNC_LIB=`tensorflow/contrib/makefile/compile_nsync.sh`
export TARGET_NSYNC_LIB="$HOST_NSYNC_LIB"
确保您运行的是make CXX=g++-4.8,而不是运行在正式 TensorFlow Makefile 文档中的make,因为 Protobuf 必须使用与用于构建以下 TensorFlow 的版本相同的gcc版本来编译库,以修复那些“未定义对google::protobuf的引用”错误。 现在尝试使用以下命令构建 TensorFlow 库:
make -f tensorflow/contrib/makefile/Makefile HOST_OS=PI TARGET=PI \
OPTFLAGS="-Os -mfpu=neon-vfpv4 -funsafe-math-optimizations -ftree-vectorize" CXX=g++-4.8
经过几个小时的构建,您可能会收到诸如“虚拟内存耗尽:无法分配内存”之类的错误,否则 Pi 板将由于内存不足而冻结。 要解决此问题,我们需要设置一个交换,因为没有交换,当应用用尽内存时,由于内核崩溃,该应用将被杀死。 设置交换的方法有两种:交换文件和交换分区。 Raspbian 在 SD 卡上使用默认的 100MB 交换文件,如下所示,使用free命令:
pi@raspberrypi:~/tensorflow-1.6.0 $ free -h
total used free shared buff/cache available
Mem: 927M 45M 843M 660K 38M 838M
Swap: 99M 74M 25M
要将交换文件大小提高到 1GB,请通过 sudo vi /etc/dphys-swapfile修改 /etc/dphys-swapfile文件,将CONF_SWAPSIZE=100更改为CONF_SWAPSIZE=1024,然后重新启动交换文件服务:
sudo /etc/init.d/dphys-swapfile stop
sudo /etc/init.d/dphys-swapfile start
此后,free -h将显示交换总量为 1.0GB。
交换分区是在单独的 USB 磁盘上创建的,因此首选交换分区,因为交换分区不会碎片化,但 SD 卡上的交换文件很容易碎片化,从而导致访问速度变慢。 要设置交换分区,请将没有所需数据的 USB 闪存盘插入 Pi 板上,然后运行sudo blkid,您将看到类似以下内容:
/dev/sda1: LABEL="EFI" UUID="67E3-17ED" TYPE="vfat" PARTLABEL="EFI System Partition" PARTUUID="622fddad-da3c-4a09-b6b3-11233a2ca1f6"
/dev/sda2: UUID="E67F-6EAB" TYPE="vfat" PARTLABEL="NO NAME" PARTUUID="a045107a-9e7f-47c7-9a4b-7400d8d40f8c"
/dev/sda2是我们将用作交换分区的分区。 现在卸载并将其格式化为交换分区:
sudo umount /dev/sda2
sudo mkswap /dev/sda2
mkswap: /dev/sda2: warning: wiping old swap signature.
Setting up swapspace version 1, size = 29.5 GiB (31671701504 bytes)
no label, UUID=23443cde-9483-4ed7-b151-0e6899eba9de
您将在mkswap命令中看到一个 UUID 输出。 运行sudo vi /etc/fstab,将以下行添加到具有 UUID 值的fstab文件中:
UUID=<UUID value> none swap sw,pri=5 0 0
保存并退出 fstab 文件,然后运行sudo swapon -a。 现在,如果再次运行free -h,将会看到“交换总数”接近 USB 存储设备的大小。 我们绝对不需要所有大小的交换空间—实际上,具有 1GB 内存的 Raspberry Pi 3 板的建议最大交换大小为 2GB,但是我们将其保留原样,因为我们只是想成功地构建内存。 TensorFlow 库。
更改任一交换设置后,我们可以重新运行make命令:
make -f tensorflow/contrib/makefile/Makefile HOST_OS=PI TARGET=PI \
OPTFLAGS="-Os -mfpu=neon-vfpv4 -funsafe-math-optimizations -ftree-vectorize" CXX=g++-4.8
完成此操作后,TensorFlow 库将以tensorflow/contrib/makefile/gen/lib/libtensorflow-core.a的形式生成,如果您已经阅读了我们手动构建 TensorFlow 库的前几章,则应该看起来很熟悉。 现在,我们可以使用该库构建图像分类示例。
图像识别和文字转语音
在tensorflow/contrib/pi_examples: label_image和相机中有两个 TensorFlow Raspberry Pi 示例应用。 我们将修改相机示例应用,以将文本集成到语音中,以便该应用在四处走动时可以说出其识别出的图像。 在构建和测试这两个应用之前,我们需要安装一些库并下载预构建的 TensorFlow Inception 模型文件:
sudo apt-get install -y libjpeg-dev
sudo apt-get install libv4l-dev
curl https://storage.googleapis.com/download.tensorflow.org/models/inception_dec_2015_stripped.zip -o /tmp/inception_dec_2015_stripped.zip
cd ~/tensorflow-1.6.0
unzip /tmp/inception_dec_2015_stripped.zip -d tensorflow/contrib/pi_examples/label_image/data/
要构建label_image和相机应用,请运行:
make -f tensorflow/contrib/pi_examples/label_image/Makefile
make -f tensorflow/contrib/pi_examples/camera/Makefile
构建应用时,您可能会遇到以下错误:
./tensorflow/core/platform/default/mutex.h:25:22: fatal error: nsync_cv.h: No such file or directory
#include "nsync_cv.h"
^
compilation terminated.
要解决此问题,请运行sudo cp tensorflow/contrib/makefile/downloads/nsync/public/nsync*.h /usr/include。
然后编辑tensorflow/contrib/pi_examples/label_image/Makefile或 tensorflow/contrib/pi_examples/camera/Makefile文件,添加以下库,并在再次运行make命令之前包含路径:
-L$(DOWNLOADSDIR)/nsync/builds/default.linux.c++11 \
-lnsync \
要测试运行这两个应用,请直接运行这些应用:
tensorflow/contrib/pi_examples/label_image/gen/bin/label_image
tensorflow/contrib/pi_examples/camera/gen/bin/camera
看看 C++ 源代码tensorflow/contrib/pi_examples/label_image/label_image.cc和tensorflow/contrib/pi_examples/camera/camera.cc,您会看到它们使用与前几章中的 iOS 应用类似的 C++ 代码来加载模型图文件,准备输入张量,运行模型,并获得输出张量。
默认情况下,摄像机示例还使用label_image/data文件夹中解压缩的预构建 Inception 模型。 但是对于您自己的特定图像分类任务,提供通过迁移学习重新训练的模型。您可以像第 2 章,“通过迁移学习对图像进行分类”一样,在运行两个示例应用时使用--graph参数。
通常,语音是 Raspberry Pi 机器人与我们互动的主要 UI。 理想情况下,我们应该运行 TensorFlow 支持的自然声音文本到语音(TTS)模型,例如 WaveNet 或 Tacotron,但运行和部署不在本章范围之内。 这样的模型。 事实证明,我们可以使用称为 CMU Flite 的简单得多的 TTS 库,它提供了相当不错的 TTS,并且只需一个简单的命令即可安装它:sudo apt-get install flite。 如果要安装最新版本的 Flite 以期希望获得更好的 TTS 质量,只需从链接下载最新的 Flite 源并进行构建。
要使用我们的 USB 扬声器测试 Flite,请使用-t参数运行 flite,然后使用双引号引起来的文本字符串,例如 flite -t "i recommend the ATM machine"。 如果您不喜欢默认语音,则可以通过运行 flite -lv找到其他受支持的语音,它们应返回 Voices available: kal awb_time kal16 awb rms slt 。 然后,您可以指定用于 TTS 的语音: flite -voice rms -t "i recommend the ATM machine"。
要使相机应用说出识别出的对象,这是当 Raspberry Pi 机器人四处移动时所期望的行为,可以使用以下简单的pipe命令:
tensorflow/contrib/pi_examples/camera/gen/bin/camera | xargs -n 1 flite -t
您可能会听到太多声音。 要微调图像分类的 TTS 结果,还可以在使用make -f tensorflow/contrib/pi_examples/camera/Makefile重建示例之前,修改camera.cc文件并将以下代码添加到PrintTopLabels函数中:
std::string cmd = "flite -voice rms -t \"";
cmd.append(labels[label_index]);
cmd.append("\"");
system(cmd.c_str());
现在,我们已经完成了“如何使用 Cloud Vision 和 Speech API 来构建智能 RasPi Bot 演示”的图像分类和语音合成任务,而不使用任何 Cloud API。 使用我们在第 5 章,“了解简单语音命令”中使用的相同模型,在 Raspberry Pi 上进行音频识别。
音频识别和机器人运动
要使用 TensorFlow 教程中的预训练音频识别模型或我们之前描述的重新训练模型,我们将重用来自这个页面的 Python 脚本listen.py,并在识别四个基本音频命令后添加 GoPiGo API 调用以控制机器人的运动:"left", "right", "go", "stop"。 预训练模型支持的其他六个命令-"yes", "no", "up", "down", "on", "off",在我们的示例中不太适用,如果需要 ,您可以使用第 5 章 , “了解简单语音命令”中所示的重新训练模型,以支持针对特定任务的其他语音命令。
要运行脚本,请先从这里下载预训练的音频识别模型,然后将其解压缩到/tmp,或者对我们在第 5 章“了解简单语音命令”中使用的模型使用scp到 Pi 板的/tmp目录,然后运行:
python listen.py --graph /tmp/conv_actions_frozen.pb --labels /tmp/conv_actions_labels.txt -I plughw:1,0
或者您可以运行:
python listen.py --graph /tmp/speech_commands_graph.pb --labels /tmp/conv_actions_labels.txt -I plughw:1,0
请注意,plughw value 1,0应该与您的 USB 麦克风的卡号和设备号匹配,可以使用我们之前显示的arecord -l命令找到。
listen.py脚本还支持许多其他参数。 例如,我们可以使用--detection_threshold 0.5代替默认的检测阈值 0.8。
现在,让我们快速了解listen.py的工作原理,然后再添加 GoPiGo API 调用以使机器人移动。 listen.py使用 Python 的subprocess模块及其Popen类产生带有适当参数的运行arecord命令的新过程。 Popen类具有stdout属性,该属性指定arecord执行的命令的标准输出文件句柄,可用于读取记录的音频字节。
加载训练后的模型图的 Python 代码如下:
with tf.gfile.FastGFile(filename, 'rb') as f:
graph_def = tf.GraphDef()
graph_def.ParseFromString(f.read())
tf.import_graph_def(graph_def, name='')
使用tf.Session()创建 TensorFlow 会话,并在加载图并创建会话之后,将记录的音频缓冲区以及采样率作为输入数据发送到 TensorFlow 会话的run方法,该方法返回识别的预测:
run(softmax_tensor, {
self.input_samples_name_: input_data,
self.input_rate_name_: self.sample_rate_
})
在这里,将softmax_tensor定义为 TensorFlow 图的get_tensor_by_name(self.output_name_),将output_name_,input_samples_name_和input_rate_name_分别定义为labels_softmax,decoded_sample_data:0和decoded_sample_data:1,我们在第 5 章,“了解简单语音命令”中的 iOS 和 Android 应用中使用过它。
在之前的章节中,我们主要使用 Python 训练和测试 TensorFlow 模型,然后再使用本机 TensorFlow C++ 库的 Java 接口代码在使用 C++ 或 Android 的 iOS 中运行模型。 在 Raspberry Pi 上,您可以选择直接使用 TensorFlow Python API 或 C++ API 在 Pi 上运行 TensorFlow 模型,尽管通常仍会在功能更强大的电脑上训练模型。 有关完整的 TensorFlow Python API 文档,请参见这里。
要使用 GoPiGo Python API 使机器人根据您的语音命令移动,请首先在listen.py中添加以下两行:
import easygopigo3 as gpg
gpg3_obj = gpg.EasyGoPiGo3()
然后将以下代码添加到def add_data方法的末尾:
if current_top_score > self.detection_threshold_ and time_since_last_top > self.suppression_ms_:
self.previous_top_label_ = current_top_label
self.previous_top_label_time_ = current_time_ms
is_new_command = True
logger.info(current_top_label)
if current_top_label=="go":
gpg3_obj.drive_cm(10, False)
elif current_top_label=="left":
gpg3_obj.turn_degrees(-30, False)
elif current_top_label=="right":
gpg3_obj.turn_degrees(30, False)
elif current_top_label=="stop":
gpg3_obj.stop()
现在将您的 Raspberry Pi 机器人放在地面上,从计算机上用ssh连接到它,然后运行以下脚本:
python listen.py --graph /tmp/conv_actions_frozen.pb --labels /tmp/conv_actions_labels.txt -I plughw:1,0 --detection_threshold 0.5
您将看到以下输出:
INFO:audio:started recording
INFO:audio:_silence_
INFO:audio:_silence_
然后,您可以说左,右,停止,前进和停止,以查看命令被识别并且机器人相应地移动:
INFO:audio:left
INFO:audio:_silence_
INFO:audio:_silence_
INFO:audio:right
INFO:audio:_silence_
INFO:audio:stop
INFO:audio:_silence_
INFO:audio:go
INFO:audio:stop
您可以在单独的终端中运行相机应用,因此,当机器人根据您的语音命令走动时,它会识别出所看到的新图像并说出结果。 这就是构建一个基本的 Raspberry Pi 机器人所需的全部内容,该机器人可以听,动,看和说-Google I/O 2016 演示所做的事情,却不使用任何 Cloud API。 它远不是一个能听懂自然人的语音,进行有趣的对话或执行有用且不重要的任务的幻想机器人。 但是,借助预训练,再训练或其他强大的 TensorFlow 模型,并使用各种传感器,您当然可以为我们构建的 Pi 机器人增加越来越多的智能和物理动力。
在下一节中,您将看到如何在 Pi 上运行经过预训练和再训练的 TensorFlow 模型,我们将向您展示如何向使用 TensorFlow 构建和训练的机器人添加强大的强化学习模型。 毕竟,强化学习的反复试验方式及其与环境交互以获取最大回报的本质,使得强化学习成为机器人非常合适的机器学习方法。
在 Raspberry Pi 上进行强化学习
OpenAI Gym是一个开源 Python 工具包,提供了许多模拟环境来帮助您开发,比较和训练强化学习算法,因此您无需购买所有传感器并在实际环境中训练您的机器人,这在时间和金钱上都是昂贵的。 在本部分中,我们将向您展示如何在 TenAI 健身房的称为 CartPole。
要安装 OpenAI Gym,请运行以下命令:
git clone https://github.com/openai/gym.git
cd gym
sudo pip install -e .
您可以通过运行pip list来验证是否已安装 TensorFlow 1.6 和 Gym(“在 Raspberry Pi 上设置 TensorFlow”的最后部分介绍了如何安装 TensorFlow 1.6):
pi@raspberrypi:~ $ pip list
gym (0.10.4, /home/pi/gym)
tensorflow (1.6.0)
或者您可以启动 iPython,然后导入 TensorFlow 和 Gym:
pi@raspberrypi:~ $ ipython
Python 2.7.9 (default, Sep 17 2016, 20:26:04)
IPython 5.5.0 -- An enhanced Interactive Python.
In [1]: import tensorflow as tf
In [2]: import gym
In [3]: tf.__version__
Out[3]: '1.6.0'
In [4]: gym.__version__
Out[4]: '0.10.4'
现在,我们都准备使用 TensorFlow 和 Gym 来构建一些有趣的在 Raspberry Pi 上运行的强化学习模型。
了解 CartPole 模拟环境
CartPole 是一种可用于训练机器人以保持平衡的环境-如果它携带某些东西并希望在移动时保持其放置状态。 由于本章的范围,我们将仅构建在模拟 CartPole 环境中工作的模型,但是可以肯定地将模型以及模型的构建和训练方式应用于类似于 CartPole 的实际物理环境。
在 CartPole 环境中,将杆连接到推车,该推车沿轨道水平移动。 您可以对购物车执行 1(向右加速)或 0(向左加速)操作。 杆子开始直立,目的是防止其跌落。 杆保持直立的每个时间步长都奖励 1。 当极点与垂直方向的夹角超过 15 度,或者手推车从中心移出 2.4 个单位以上时,剧集就会结束。
现在让我们使用 CartPole 环境。 首先,创建一个新环境并找出智能体可以在该环境中采取的措施:
env = gym.make("CartPole-v0")
env.action_space
# Discrete(2)
env.action_space.sample()
# 0 or 1
每个观察(状态)都由关于购物车的四个值组成:其水平位置,速度,极角和角速度:
obs=env.reset()
obs
# array([ 0.04052535, 0.00829587, -0.03525301, -0.00400378])
环境中的每个步骤(动作)将导致新的观察,动作的奖励,剧集是否完成(如果是,则您无法采取任何进一步的步骤)以及一些其他信息:
obs, reward, done, info = env.step(1)
obs
# array([ 0.04069127, 0.2039052 , -0.03533309, -0.30759772])
记住动作(或步骤)1 表示向右移动,0 表示向左移动。 要查看当您继续向右移动购物车时剧集可以持续多长时间,请运行:
while not done:
obs, reward, done, info = env.step(1)
print(obs)
#[ 0.08048328 0.98696604 -0.09655727 -1.54009127]
#[ 0.1002226 1.18310769 -0.12735909 -1.86127705]
#[ 0.12388476 1.37937549 -0.16458463 -2.19063676]
#[ 0.15147227 1.5756628 -0.20839737 -2.52925864]
#[ 0.18298552 1.77178219 -0.25898254 -2.87789912]
现在,让我们手动执行从头到尾的一系列操作,并打印出观测值的第一个值(水平位置)和第三个值(极点与垂直方向的度数),因为这两个值确定一个剧集是否为完成。
首先,重置环境并正确加速购物车几次:
import numpy as np
obs=env.reset()
obs[0], obs[2]*360/np.pi
# (0.008710582898326602, 1.4858315848689436)
obs, reward, done, info = env.step(1)
obs[0], obs[2]*360/np.pi
# (0.009525842685697472, 1.5936049816642313)
obs, reward, done, info = env.step(1)
obs[0], obs[2]*360/np.pi
# (0.014239775393474322, 1.040038643681757)
obs, reward, done, info = env.step(1)
obs[0], obs[2]*360/np.pi
# (0.0228521194217381, -0.17418034908781568)
您会看到,推车向右移动时,其位置值会越来越大,杆的垂直度会越来越小,最后一步显示的是负度,这意味着杆位于中心的左侧。 所有这一切都是有道理的,在您心目中,您最喜欢的狗用杆子推着推车的画面有些生动。 现在,更改动作以使购物车向左(0)加速几次:
obs, reward, done, info = env.step(0)
obs[0], obs[2]*360/np.pi
# (0.03536432554326476, -2.0525933052704954)
obs, reward, done, info = env.step(0)
obs[0], obs[2]*360/np.pi
# (0.04397450935915654, -3.261322987287562)
obs, reward, done, info = env.step(0)
obs[0], obs[2]*360/np.pi
# (0.04868738508385764, -3.812330822419413)
obs, reward, done, info = env.step(0)
obs[0], obs[2]*360/np.pi
# (0.04950617929263011, -3.7134404042580687)
obs, reward, done, info = env.step(0)
obs[0], obs[2]*360/np.pi
# (0.04643238384389254, -2.968245724428785)
obs, reward, done, info = env.step(0)
obs[0], obs[2]*360/np.pi
# (0.039465670006712444, -1.5760901885345346)
首先,您可能会惊讶于 0 动作导致位置(obs[0])连续变大几次,但请记住,手推车以一定速度运动,并且一个或多个动作将手推车移至另一个方向不会立即降低位置值。 但是,如果您继续将购物车向左移动,如前两个步骤所示,您会看到购物车的位置开始变小(向左)。 现在继续执行 0 动作,您会看到位置越来越小,负值表示推车进入中心的左侧,而杆的角度越来越大:
obs, reward, done, info = env.step(0)
obs[0], obs[2]*360/np.pi
# (0.028603948219811447, 0.46789197320636305)
obs, reward, done, info = env.step(0)
obs[0], obs[2]*360/np.pi
# (0.013843572459953138, 3.1726728882727504)
obs, reward, done, info = env.step(0)
obs[0], obs[2]*360/np.pi
# (-0.00482029774222077, 6.551160678086707)
obs, reward, done, info = env.step(0)
obs[0], obs[2]*360/np.pi
# (-0.02739315127299434, 10.619948631208114)
如我们前面所述,定义 CartPole 环境的方式是剧集“当极距与垂直方向成 15 度以上时结束”,因此让我们再做一些动作并打印出done值, 时间:
obs, reward, done, info = env.step(0)
obs[0], obs[2]*360/np.pi, done
# (-0.053880356973985064, 15.39896478042983, False)
obs, reward, done, info = env.step(0)
obs[0], obs[2]*360/np.pi, done
# (-0.08428612474261402, 20.9109976051126, False)
obs, reward, done, info = env.step(0)
obs[0], obs[2]*360/np.pi, done
# (-0.11861214326416822, 27.181070460526062, True)
obs, reward, done, info = env.step(0)
# WARN: You are calling 'step()' even though this environment has already returned done = True. You should always call 'reset()' once you receive 'done = True' -- any further steps are undefined behavior.
环境决定何时将done返回True时会有一些延迟-尽管前两个步骤已经返回了大于 15 度的度数(当极点与垂直线成 15 度以上时,剧集结束了) ,您仍然可以对环境执行 0 操作。 第三步将done返回为True,并且环境中的另一步骤(最后一步)将导致警告,因为环境已经完成了该剧集。
对于 CartPole 环境,每个step调用返回的reward值始终为 1,信息始终为{}。 这就是关于 CartPole 模拟环境的全部知识。 现在我们了解了 CartPole 的工作原理,让我们看看可以在每种状态(观察)下制定什么样的策略,我们可以让该策略告诉我们要采取的操作(步骤),以便我们可以保持杆直立。换句话说,就是尽可能长的时间,这样我们才能最大化我们的回报。 请记住,强化学习中的一项策略只是一个函数,该函数以智能体所处的状态为输入,并输出智能体接下来应采取的行动,以实现值最大化或长期回报。
从基本的直观策略开始
显然,每次都执行相同的动作(全 0 或 1s)不会使杆保持太直的状态。 为了进行基线比较,请运行以下代码,以查看在每个剧集中应用相同操作时在 1,000 个剧集中获得的平均奖励:
# single_minded_policy.py
import gym
import numpy as np
env = gym.make("CartPole-v0")
total_rewards = []
for _ in range(1000):
rewards = 0
obs = env.reset()
action = env.action_space.sample()
while True:
obs, reward, done, info = env.step(action)
rewards += reward
if done:
break
total_rewards.append(rewards)
print(np.mean(total_rewards))
# 9.36
因此,所有 1,000 集的平均奖励约为 10。请注意, env.action_space.sample()对 0 或 1 动作进行采样,与随机输出 0 或 1 相同。您可以通过求值np.sum([env.action_space.sample() for _ in range(10000)]),应该接近 5,000。
要查看其他策略如何更好地工作,让我们使用一个简单直观的策略,当极数度数为正(在垂直方向的右侧)时执行 1 动作(向右移动购物车),然后执行 0(将购物车向右移动) 当极数度数为负时(在垂直方向的左侧)。 这项策略是有道理的,因为我们可能会采取尽可能长时间保持平衡的措施:
# simple_policy.py
import gym
import numpy as np
env = gym.make("CartPole-v0")
total_rewards = []
for _ in range(1000):
rewards = 0
obs = env.reset()
while True:
action = 1 if obs[2] > 0 else 0
obs, reward, done, info = env.step(action)
rewards += reward
if done:
break
total_rewards.append(rewards)
print(np.mean(total_rewards))
# 42.19
现在,每 1,000 集的平均奖励为 42,与 9.36 相比有很大提高。
现在让我们看看我们是否可以制定出更好,更复杂的策略。 回想一下,策略只是从状态到操作的映射或函数。 在过去的几年中,我们在神经网络的兴起中了解到的一件事是,如果不清楚如何定义复杂的函数(例如强化学习中的策略),请考虑一下神经网络,毕竟这是通用函数近似器 (有关详细信息,请参见神经网络可以计算任何函数的可视化证明,Michael Nelson)。
在上一章中我们介绍了 AlphaGo 和 AlphaZero,Jim Fleming 撰写了一篇有趣的博客文章,标题为“在 AlphaGo 之前的 TD-Gammon”,这是第一个强化学习应用,它使用神经网络作为评估函数来训练自己,以击败五子棋冠军。 博客条目和 Sutton 和 Barto 的《强化学习:入门》一书都对 TD-Gammon 进行了深入的描述。 如果您想了解有关使用神经网络作为强大的通用函数的更多信息,还可以使用 Google 搜索“时差学习和 TD-Gammon”的原始论文。
使用神经网络建立更好的策略
首先,让我们看看如何使用简单的完全连接的(密集)神经网络构建随机策略,该网络将观察中的 4 个值作为输入,使用 4 个神经元的隐藏层,并输出 0 动作的概率,基于 ,智能体可以采样 0 到 1 之间的下一个动作:
# nn_random_policy.py
import tensorflow as tf
import numpy as np
import gym
env = gym.make("CartPole-v0")
num_inputs = env.observation_space.shape[0]
inputs = tf.placeholder(tf.float32, shape=[None, num_inputs])
hidden = tf.layers.dense(inputs, 4, activation=tf.nn.relu)
outputs = tf.layers.dense(hidden, 1, activation=tf.nn.sigmoid)
action = tf.multinomial(tf.log(tf.concat([outputs, 1-outputs], 1)), 1)
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
total_rewards = []
for _ in range(1000):
rewards = 0
obs = env.reset()
while True:
a = sess.run(action, feed_dict={inputs: obs.reshape(1, num_inputs)})
obs, reward, done, info = env.step(a[0][0])
rewards += reward
if done:
break
total_rewards.append(rewards)
print(np.mean(total_rewards))
请注意,我们使用 tf.multinomial 函数根据动作 0 和 1 的概率分布,分别定义为outputs和1-outputs(两个概率之和为 1)对动作进行采样。 总奖励的平均值约为 20 左右,比一心一意的策略好,但比上一节中的简单直观的策略差。 这是一个神经网络,它在没有任何训练的情况下生成随机策略。
为了训练网络,我们使用tf.nn.sigmoid_cross_entropy_with_logits定义了网络输出与所需y_target作用之间的损失函数,该损失函数是使用前面小节中的基本简单策略定义的,因此我们希望该神经网络策略能够实现与基本的非神经网络策略大致相同的奖励:
# nn_simple_policy.py
import tensorflow as tf
import numpy as np
import gym
env = gym.make("CartPole-v0")
num_inputs = env.observation_space.shape[0]
inputs = tf.placeholder(tf.float32, shape=[None, num_inputs])
y = tf.placeholder(tf.float32, shape=[None, 1])
hidden = tf.layers.dense(inputs, 4, activation=tf.nn.relu)
logits = tf.layers.dense(hidden, 1)
outputs = tf.nn.sigmoid(logits)
action = tf.multinomial(tf.log(tf.concat([outputs, 1-outputs], 1)), 1)
cross_entropy = tf.nn.sigmoid_cross_entropy_with_logits(labels=y, logits=logits)
optimizer = tf.train.AdamOptimizer(0.01)
training_op = optimizer.minimize(cross_entropy)
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
for _ in range(1000):
obs = env.reset()
while True:
y_target = np.array([[1\. if obs[2] < 0 else 0.]])
a, _ = sess.run([action, training_op], feed_dict={inputs: obs.reshape(1, num_inputs), y: y_target})
obs, reward, done, info = env.step(a[0][0])
if done:
break
print("training done")
我们将outputs定义为logits净输出的sigmoid函数,即动作概率 0,然后使用tf.multinomial采样动作。 请注意,我们使用标准的tf.train.AdamOptimizer及其minimize方法来训练网络。 要测试并查看该策略的表现,请运行以下代码:
total_rewards = []
for _ in range(1000):
rewards = 0
obs = env.reset()
while True:
y_target = np.array([1\. if obs[2] < 0 else 0.])
a = sess.run(action, feed_dict={inputs: obs.reshape(1, num_inputs)})
obs, reward, done, info = env.step(a[0][0])
rewards += reward
if done:
break
total_rewards.append(rewards)
print(np.mean(total_rewards))
总奖励的平均值约为 40 左右,这与使用无神经网络的简单策略的收益大致相同,这正是我们在训练阶段将y: y_target专门用于简单策略时的期望值 ,以训练网络。
现在,我们都准备探索如何在此基础上实现策略梯度方法,以使我们的神经网络表现更好,获得的奖励要大几倍。
策略梯度的基本思想是,为了训练神经工作以生成更好的策略,当所有智能体从环境中知道的都是从任何给定状态采取行动时所能获得的奖励(这意味着我们不能使用监督学习进行训练),我们可以采用两种新机制:
-
折扣奖励:每个动作的值都需要考虑其未来动作的奖励。 例如,一个动作获得立即奖励 1,但是在两个动作(步骤)之后结束剧集的长期奖励应该比获得立即奖励 1 但在 10 个步骤之后结束剧集的动作具有较少的长期奖励。 动作的折现奖励的典型公式是其立即奖励加上其每个未来奖励的倍数和由未来步骤提供动力的折现率的总和。 因此,如果一个动作序列在剧集结束前有 1、1、1、1、1 个奖励,则第一个动作的折扣奖励为
1+(1*discount_rate)+(1*discount_rate**2)+(1*discount_rate**3)+(1*discount_rate**4)。 -
测试运行当前策略,查看哪些操作导致较高的折扣奖励,然后使用折扣奖励更新当前策略的梯度(权重损失),以使具有较高折扣奖励的操作在网络更新后, 下次被选中的可能性更高。 重复这样的测试运行并多次更新该过程,以训练神经网络以获得更好的策略。
有关更详细的讨论和策略梯度的演练,请参阅 Andrej Karpathy 的博客条目,《深度强化学习:来自像素的乒乓》。 现在,让我们看看如何为 TensorFlow 中的 CartPole 问题实现策略梯度。
首先,导入 tensorflow,numpy 和 gym,并定义一个用于计算标准化和折价奖励的助手方法:
import tensorflow as tf
import numpy as np
import gym
def normalized_discounted_rewards(rewards):
dr = np.zeros(len(rewards))
dr[-1] = rewards[-1]
for n in range(2, len(rewards)+1):
dr[-n] = rewards[-n] + dr[-n+1] * discount_rate
return (dr - dr.mean()) / dr.std()
例如,如果discount_rate为 0.95,则奖励列表[1,1,1]中第一个动作的折扣奖励为1 + 1 * 0.95 + 1 * 0.95 ** 2 = 2.8525,并且第二和最后一个折扣奖励是 1.95 和 1; 奖励列表[1,1,1,1,1]中第一个动作的折扣奖励为1 + 1 * 0.95 + 1 * 0.95 ** 2 + 1 * 0.95 ** 3 + 1 * 0.95 ** 4 = 4.5244,其余动作为 3.7099、2.8525、1.95 和 1 。 [1,1,1]和[1,1,1,1,1]的归一化折扣奖励为[1.2141, 0.0209, -1.2350]和[1.3777, 0.7242, 0.0362, -0.6879, -1.4502]。 每个规范化的折扣清单按降序排列,这意味着动作持续的时间越长(在剧集结束之前),其奖励就越大。
接下来,创建 CartPole 体育馆环境,定义learning_rate和discount_rate超参数,并像以前一样使用四个输入神经元,四个隐藏神经元和一个输出神经元构建网络:
env = gym.make("CartPole-v0")
learning_rate = 0.05
discount_rate = 0.95
num_inputs = env.observation_space.shape[0]
inputs = tf.placeholder(tf.float32, shape=[None, num_inputs])
hidden = tf.layers.dense(inputs, 4, activation=tf.nn.relu)
logits = tf.layers.dense(hidden, 1)
outputs = tf.nn.sigmoid(logits)
action = tf.multinomial(tf.log(tf.concat([outputs, 1-outputs], 1)), 1)
prob_action_0 = tf.to_float(1-action)
cross_entropy = tf.nn.sigmoid_cross_entropy_with_logits(logits=logits, labels=prob_action_0)
optimizer = tf.train.AdamOptimizer(learning_rate)
请注意,此处不再像以前的简单神经网络策略示例那样使用minimize函数,因为我们需要手动微调梯度以考虑每个动作的折价奖励。 这就要求我们首先使用compute_gradients方法,然后以所需的方式更新梯度,最后调用apply_gradients方法(我们大多数时候应该使用的minimize方法实际上是在幕后调用compute_gradients和apply_gradients)。
因此,让我们现在为网络参数(权重和偏差)计算交叉熵损失的梯度,并设置梯度占位符,稍后将使用考虑了计算梯度和动作折现奖励的值来填充它,动作在测试运行期间使用当前策略选取:
gvs = optimizer.compute_gradients(cross_entropy)
gvs = [(g, v) for g, v in gvs if g != None]
gs = [g for g, _ in gvs]
gps = []
gvs_feed = []
for g, v in gvs:
gp = tf.placeholder(tf.float32, shape=g.get_shape())
gps.append(gp)
gvs_feed.append((gp, v))
training_op = optimizer.apply_gradients(gvs_feed)
从optimizer.compute_gradients(cross_entropy)返回的gvs是一个元组列表,每个元组都由(可训练变量的cross_entropy的)梯度和可训练变量组成。 例如,如果您在整个程序运行后查看gvs,您将看到类似以下内容:
[(<tf.Tensor 'gradients/dense/MatMul_grad/tuple/control_dependency_1:0' shape=(4, 4) dtype=float32>,
<tf.Variable 'dense/kernel:0' shape=(4, 4) dtype=float32_ref>),
(<tf.Tensor 'gradients/dense/BiasAdd_grad/tuple/control_dependency_1:0' shape=(4,) dtype=float32>,
<tf.Variable 'dense/bias:0' shape=(4,) dtype=float32_ref>),
(<tf.Tensor 'gradients/dense_2/MatMul_grad/tuple/control_dependency_1:0' shape=(4, 1) dtype=float32>,
<tf.Variable 'dense_1/kernel:0' shape=(4, 1) dtype=float32_ref>),
(<tf.Tensor 'gradients/dense_2/BiasAdd_grad/tuple/control_dependency_1:0' shape=(1,) dtype=float32>,
<tf.Variable 'dense_1/bias:0' shape=(1,) dtype=float32_ref>)]
请注意,kernel只是权重的另一个名称,(4, 4),(4, ),(4, 1)和(1, )是权重的形状和对第一个(输入到隐藏)和第二层(隐藏到输出)。 如果您从 iPython 多次运行脚本,则tf对象的默认图将包含先前运行的可训练变量,因此,除非调用tf.reset_default_graph(),否则需要使用gvs = [(g, v) for g, v in gvs if g != None]删除那些过时的训练变量, 将返回 None 梯度(有关computer_gradients的更多信息,请参见这里)。
现在,玩一些游戏并保存奖励和梯度值:
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
for _ in range(1000):
rewards, grads = [], []
obs = env.reset()
# using current policy to test play a game
while True:
a, gs_val = sess.run([action, gs], feed_dict={inputs:
obs.reshape(1, num_inputs)})
obs, reward, done, info = env.step(a[0][0])
rewards.append(reward)
grads.append(gs_val)
if done:
break
在测试游戏之后,使用折价奖励更新梯度并训练网络(请记住training_op被定义为optimizer.apply_gradients(gvs_feed)):
# update gradients and do the training
nd_rewards = normalized_discounted_rewards(rewards)
gp_val = {}
for i, gp in enumerate(gps):
gp_val[gp] = np.mean([grads[k][i] * reward for k, reward in
enumerate(nd_rewards)], axis=0)
sess.run(training_op, feed_dict=gp_val)
最终,经过 1000 次迭代的测试和更新,我们可以测试经过训练的模型:
total_rewards = []
for _ in range(100):
rewards = 0
obs = env.reset()
while True:
a = sess.run(action, feed_dict={inputs: obs.reshape(1,
num_inputs)})
obs, reward, done, info = env.step(a[0][0])
rewards += reward
if done:
break
total_rewards.append(rewards)
print(np.mean(total_rewards))
请注意,我们现在使用经过训练的策略网络和sess.run以当前观察为输入来执行下一步操作。 总奖励的输出平均值约为 200,这与我们是否使用神经网络的简单直观策略相比有很大改进。
您也可以在使用tf.train.Saver进行训练后保存训练后的模型,就像我们在前几章中曾经做过的多次:
saver = tf.train.Saver()
saver.save(sess, "./nnpg.ckpt")
然后,您可以使用以下命令在单独的测试程序中重新加载它:
with tf.Session() as sess:
saver.restore(sess, "./nnpg.ckpt")
之前的所有策略实现都在 Raspberry Pi 上运行,甚至使用 TensorFlow 训练强化学习策略梯度模型的模型实现也需要大约 15 分钟才能完成。 这是我们涵盖的每项策略,在 Pi 上运行后返回的总奖励:
pi@raspberrypi:~/mobiletf/ch12 $ python single_minded_policy.py
9.362
pi@raspberrypi:~/mobiletf/ch12 $ python simple_policy.py
42.535
pi@raspberrypi:~/mobiletf/ch12 $ python nn_random_policy.py
21.182
pi@raspberrypi:~/mobiletf/ch12 $ python nn_simple_policy.py
41.852
pi@raspberrypi:~/mobiletf/ch12 $ python nn_pg.py
199.116
现在,您已经拥有了一个强大的基于神经网络的策略模型,可以帮助您的机器人保持平衡,并在模拟环境中进行了全面测试,在将模拟环境的 API 返回值替换为真实环境数据后,您可以将其部署在真实的物理环境中,当然,但是用于构建和训练神经网络强化学习模型的代码当然可以轻松地重用。
总结
在本章中,我们首先详细介绍了使用所有必需的附件和操作系统以及将 Raspberry Pi 板变成移动机器人的 GoPiGo 工具包来设置 Raspberry Pi 的详细步骤。 然后,我们介绍了如何在 Raspberry Pi 上安装 TensorFlow 并构建 TensorFlow 库,以及如何将 TTS 与图像分类集成以及如何使用 GoPiGO API 进行音频命令识别,从而使 Raspberry Pi 机器人可以移动,看到,听到和说出所有内容,而无需使用 Cloud API。 最后,我们介绍了用于强化学习的 OpenAI Gym 工具包,并向您展示了如何使用 TensorFlow 构建和训练功能强大的强化学习神经网络模型,以使您的机器人在模拟环境中保持平衡。
最后的话
因此,该说再见了。 在本书中,我们从三个经过预训练的 TensorFlow 模型开始,这些模型分别是图像分类,对象检测和神经样式迁移,并详细讨论了如何重新训练模型并在 iOS 和 Android 应用中使用它们。 然后,我们介绍了使用 Python 构建的 TensorFlow 教程中的三个有趣的模型(音频识别,图像字幕和快速绘制),并展示了如何在移动设备上重新训练和运行这些模型。
之后,我们从零开始开发了用于预测 TensorFlow 和 Keras 中的股价的 RNN 模型,两个用于数字识别和像素转换的 GAN 模型以及一个用于 Connect4 的类似于 AlphaZero 的模型,以及使用所有这些 TensorFlow 模型的完整 iOS 和 Android 应用 。 然后,我们介绍了如何将 TensorFlow Lite 以及 Apple 的 Core ML 与标准机器学习模型和转换后的 TensorFlow 模型一起使用,展示了它们的潜力和局限性。 最后,我们探索了如何使用 TensorFlow 构建 Raspberry Pi 机器人,该机器人可以使用强大的强化学习算法来移动,观看,聆听,讲话和学习。
我们还展示了同时使用 TensorFlow Pod 和手动构建的 TensorFlow 库的 Objective-C 和 Swift iOS 应用,以及使用即用型 TensorFlow 库和手动构建库的 Android 应用,以修复您在移动设备上部署和运行 TensorFlow 模型时可能遇到的各种问题。
我们已经介绍了很多,但是还有很多要讲的。 TensorFlow 的新版本已经快速发布。 已经构建并实现了采用最新研究论文的新 TensorFlow 模型。 本书的主要目的是向您展示使用各种智能 TensorFlow 模型的足够的 iOS 和 Android 应用,以及所有实用的故障排除和调试技巧,以便您可以在移动设备上为你的下一个杀手级移动 AI 应用快速部署和运行自己喜欢的 TensorFlow 模型。
如果您想使用 TensorFlow 或 Keras 构建自己的出色模型,实现最令您兴奋的算法和网络,则需要在本书结束后继续学习,因为我们没有详细介绍如何做到这一点, 但希望我们能激发您足够的动力来开始这一旅程,并从书中获得保证,一旦您构建并训练了模型,便知道如何快速,随时随地在移动设备上部署和运行它们。
关于走哪条路和要解决哪些 AI 任务,Ian Goodfellow 在接受 Andrew Ng 采访时的建议可能是最好的:“问问自己,下一步做什么是最好的,选取哪条路是最适合的:强化学习,无监督学习或生成对抗网络”。 无论如何,这将是一条充满兴奋的绝妙之路,当然还要有艰苦的工作,而您从本书中学到的技能就像您的智能手机一样,随时可以为您服务,并准备好将使您的甜蜜而聪明的小设备变得更加甜蜜和智能。

浙公网安备 33010602011771号