移动人工智能项目-全-
移动人工智能项目(全)
原文:
annas-archive.org/md5/24329c3bf9f3c672f8b5b2fa4cd1802e
译者:飞龙
前言
我们正见证着人工智能(AI)的革命,这要归功于深度学习的突破。移动人工智能项目使你能够通过应用 AI 技术来设计自然语言处理(NLP)、机器人技术和计算机视觉的应用,参与这一革命。
本书将教你如何在移动应用中利用 AI 的力量,并学习 NLP、神经网络、深度学习和移动视觉 API 的核心功能。本书包含一系列项目,涵盖自动推理、人脸识别、数字助手、自动文本生成和个性化新闻与故事等任务。你将学习如何利用 NLP 和机器学习算法使应用更具预测性、主动性,并能在较少的人为干预下做出自主决策。在最后几章中,你将使用如 TensorFlow Lite、CoreML 和 Snapdragon 神经处理引擎(NPE)等流行库,跨 Android 和 iOS 平台进行实践。
到本书结束时,你将能够开发出令人兴奋且直观的移动应用程序,为用户提供定制化和个性化的体验。
本书适合的人群
移动人工智能项目适合机器学习专家、深度学习工程师、AI 工程师以及希望将 AI 技术集成到移动平台和应用中的软件工程师。
本书内容简介
第一章,人工智能概念与基础,涵盖了构建移动端或网页端 AI 应用所需的主要概念和高级理论。我们将讨论人工神经网络(ANNs)和深度学习的基础知识,它们构成了当前 AI 研究和趋势的核心。我们将理解构建 AI 应用所需的所有基本术语,开启我们的 AI 应用构建之旅。
第二章,创建房地产价格预测移动应用程序,是本书的实用入门部分。我们将介绍全书中使用的所有必要工具和库,并考虑如何搭建深度学习环境。我们将通过展示如何使用 TensorFlow 构建一个房地产价格预测应用,并将其部署到移动端和网页端,来介绍这些内容。为此,我们将使用人工神经网络(ANNs)和 TensorFlow。
第三章,实现深度网络架构识别手写数字,聚焦于机器视觉相关的基本理论、术语和概念。我们将通过实践应用机器视觉,直观理解卷积神经网络(CNNs)的工作原理。我们将学习如何构建实际应用于机器视觉的应用程序。本章将注重直观理解和应用,而非理论分析。
第四章,构建机器视觉移动应用分类花卉物种,让我们能够将前几章的学习转化为构建一个物体识别应用,并进一步定制该应用,用来分类超过 100 种花卉物种,并展示它们的维基页面。我们将学习如何重新训练现有的深度网络架构,以适应物体和图像分类的定制用例。
第五章,使用 TensorFlow 构建 ML 模型预测汽车损伤,专注于图像修复的无监督任务。它讨论了用于这些任务的深度网络和库。我们将探讨深度学习中用于解决这些任务的技术,并逐一执行这些任务,之后将设置并运行来自安卓和 iOS 应用的图像修复。
第六章,基于 PyTorch 的 NLP 与 RNN 实验,重点介绍了递归神经网络(RNNs)的工作原理及人工智能(AI)和自然语言处理(NLP)的应用。我们将深入探讨如何实际解决 AI 中的 NLP 用例。
第七章,基于 TensorFlow 的移动端语音转文本与 WaveNet 模型,在这一章中,我们将学习如何使用 WaveNet 模型将音频转换为文本。然后,我们将构建一个模型,利用安卓应用将音频转化为文本。
第八章,使用 GAN 实现手写数字识别,在这一章中,我们将构建一个安卓应用,检测手写数字并通过对抗学习推断数字。我们将使用修改版国家标准技术研究院(MNIST)数据集进行数字分类。我们还将探讨生成对抗网络(GANs)的基础知识。
第九章,使用 LinearSVC 进行文本情感分析,在这一章中,我们将构建一个 iOS 应用,利用用户输入对文本和图像进行情感分析。我们将使用为相同目的而构建的现有数据模型,利用 LinearSVC 将这些模型转换为核心机器学习(ML)模型,方便我们在应用中使用。
第十章,下一步是什么?,讨论了流行的基于机器学习的云服务,以及在构建第一个基于机器学习的移动应用时应从哪里开始,还提供了一些进一步阅读的参考资料。
为了充分利用本书的内容
你只需要具备扎实的机器学习知识和任何编程语言的经验,就能开始本书的学习。
下载示例代码文件
您可以从您的账户在www.packt.com下载本书的示例代码文件。如果您是在其他地方购买的此书,您可以访问www.packt.com/support并注册,以便将文件直接通过电子邮件发送给您。
您可以通过以下步骤下载代码文件:
-
请在www.packt.com登录或注册。
-
选择“支持”选项卡。
-
点击“代码下载和勘误表”。
-
在搜索框中输入书名,并按照屏幕上的指示操作。
下载文件后,请确保使用最新版本的以下工具解压或提取文件夹:
-
WinRAR/7-Zip for Windows
-
Zipeg/iZip/UnRarX for Mac
-
7-Zip/PeaZip for Linux
本书的代码包也托管在 GitHub 上:github.com/PacktPublishing/Mobile-Artificial-Intelligence-Projects
。如果代码有更新,它将在现有的 GitHub 存储库中进行更新。
我们还提供了来自我们丰富书籍和视频目录的其他代码包,您可以在github.com/PacktPublishing/
上找到它们。赶快去看看吧!
下载彩色图片
我们还提供了一个 PDF 文件,包含本书中使用的截图/图表的彩色图片。您可以在这里下载:www.packtpub.com/sites/default/files/downloads/9781789344073_ColorImages.pdf
。
使用的约定
本书中使用了若干文本约定。
CodeInText
:表示文本中的代码字、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 用户名。以下是一个例子:“首先,让我们导入math
库,以便我们可以使用exponential
函数。”
代码块的格式如下:
def sigmoid ( x ):
return 1 / ( 1 + e **- x )
任何命令行输入或输出都如下所示:
pip install tensorflow
粗体:表示新术语、重要词汇或屏幕上看到的词汇。例如,菜单或对话框中的单词会以这种方式出现在文本中。以下是一个例子:“使用右上角的New下拉菜单来创建一个新的Python 3笔记本。”
警告或重要提示如下所示。
小贴士和技巧会这样显示。
联系我们
我们始终欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问,请在邮件主题中提到书名,并通过[email protected]
与我们联系。
勘误表:尽管我们已尽最大努力确保内容的准确性,但错误仍可能发生。如果您在本书中发现错误,请报告给我们。请访问www.packt.com/submit-errata,选择您的书籍,点击“勘误提交表单”链接,并输入相关细节。
盗版:如果你在互联网上遇到任何我们作品的非法复制品,无论形式如何,我们将不胜感激,如果你能提供该地址或网站名称。请通过[email protected]
与我们联系,并附上该材料的链接。
如果你有兴趣成为作者:如果你在某个领域有专长,并且有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com。
评价
请留下评价。阅读并使用本书后,不妨在你购买的站点上留下评价吗?潜在读者可以看到并根据你的公正意见做出购买决策,我们在 Packt 也能了解你对我们产品的看法,而我们的作者可以看到你对他们书籍的反馈。谢谢!
欲了解更多关于 Packt 的信息,请访问packt.com。
第一章:人工智能的概念和基础
本章作为整本书的序章以及书中相关概念的引导。我们将在足够高的层次上理解这些概念,以便我们能够理解本书中的构建内容。
我们将通过比较人工智能、机器学习和深度学习来理解人工智能(AI)的整体结构及其构建块,因为这些术语可以互换使用。接着,我们将浏览人工神经网络(ANNs)的历史、演变和原理。然后,我们将深入了解 ANNs 和深度学习的基本概念和术语,这些将在全书中使用。之后,我们将简要了解 TensorFlow Playground,以巩固我们对 ANNs 的理解。最后,我们将以关于在哪里获取人工智能和 ANN 原理的更深理论参考的思考来结束本章,具体如下:
-
人工智能与机器学习与深度学习
-
人工智能的演变
-
人工神经网络(ANNs)的机制
-
生物神经元
-
人工神经元的工作原理
-
激活函数与成本函数
-
梯度下降、反向传播和 softmax
-
TensorFlow Playground
人工智能与机器学习与深度学习
人工智能(AI)并不是一个新术语,因为我们在线阅读了大量相关文章,并且有许多基于该主题的电影。所以,在我们进一步讨论之前,先退一步,从实践者的角度理解人工智能及其常见的术语。我们将清晰地区分机器学习、深度学习和人工智能,因为这些术语经常被交替使用:
人工智能是可以嵌入到机器中的能力,使得机器能够执行具有人的智能特征的任务。这些任务包括看见和识别物体、听见并区分声音、理解和领会语言以及其他类似的任务。
机器学习(ML)是人工智能(AI)的一个子集,涵盖了使这些类人任务成为可能的技术。因此,从某种意义上讲,机器学习是实现人工智能的手段。
本质上,如果我们不使用机器学习来实现这些任务,那么我们实际上是在试图编写数百万行复杂的代码,包括循环、规则和决策树。
机器学习赋予了机器在没有明确编程的情况下进行学习的能力。因此,机器学习不再是为每个可能的场景硬编码规则,而是通过提供任务执行方式和不应执行方式的示例来进行学习。然后,机器学习系统根据这些数据进行训练,以便它能自我学习。
ML 是一种人工智能方法,通过它我们可以完成诸如分组或聚类、分类、推荐、预测和数据预测等任务。常见的例子包括垃圾邮件分类、股市预测、天气预报等。
深度学习 是机器学习中的一种特殊技术,它模仿了人类大脑的生物结构,并通过神经网络来完成类似人类的任务。这是通过使用人工神经网络(ANNs)——一种通过算法堆叠解决问题的技术——来构建像大脑一样的神经网络,从而以类人甚至更高效的能力解决问题。
这些层通常被称为深度网络(深层架构),每一层都有一个可以训练解决的特定问题。目前,深度学习领域正处于前沿技术,应用包括自动驾驶、Alexa 和 Siri、机器视觉等。
在本书中,我们将执行使用这些深度网络(deepnets)构建的任务和应用,并通过构建我们自己的深度网络架构来解决使用案例。
人工智能的发展
要理解我们当前在人工智能领域所能做到的事情,我们需要对模仿人类大脑的概念的起源有一个基本的了解,并且了解这个概念是如何发展到如今,通过机器我们能够轻松解决视觉和语言任务,且具有类似人类的能力。
一切始于 1959 年,当时哈佛的几位科学家 Hubel 和 Wiesel 通过监测猫的大脑初级视觉皮层,进行猫视觉系统的实验。
初级视觉皮层 是大脑中位于后脑勺的一群神经元,负责处理视觉信息。它是大脑接收来自眼睛输入信号的第一部分,类似于人类大脑如何处理视觉信息。
科学家们从向猫展示复杂的图像(例如鱼、狗和人类)开始,并观察猫的初级视觉皮层。令他们失望的是,初期没有从初级视觉皮层得到任何反应。因此,令他们惊讶的是,在某次实验中,当他们移除幻灯片时,黑暗的边缘形成,导致一些神经元在初级视觉皮层中激活。
他们偶然发现,初级视觉皮层中的这些个体神经元或脑细胞会对不同特定方向的条形或黑暗边缘做出反应。这导致了一个理解:哺乳动物的大脑每个神经元处理的信息量非常小,随着信息从一个神经元传递到另一个神经元,越复杂的形状、边缘、曲线和阴影逐渐被理解。因此,这些持有非常基础信息的独立神经元需要一同激活,才能理解一个完整而复杂的图像。
之后,关于如何模仿哺乳动物大脑的进展曾一度停滞,直到 1980 年,福岛提出了神经认知网络(neocognitron)。神经认知网络 的灵感来自于一个想法:我们应该能够通过许多非常简单的表现形式,创造出越来越复杂的表现形式——就像哺乳动物的大脑一样!
以下是福岛(Fukushima)提出的神经认知网络(Neocognitron)工作原理的示意图:
他提出,为了识别你的祖母,初级视觉皮层中有许多神经元被激活,每个细胞或神经元理解你祖母最终图像的一个抽象部分。这些神经元以顺序、并行和协同的方式工作,最后会激活一个“祖母细胞”或神经元,只有在看到你的祖母时它才会被激活。
快进到今天(2010-2018),在 Yoshua Bengio、Yann LeCun 和 Geoffrey Hinton 的贡献下,他们被广泛称为深度学习之父。他们为我们今天所工作的 AI 领域做出了巨大的贡献。他们催生了机器学习的一种全新方法,其中特征工程被自动化。
不直接告诉算法它应该寻找什么,而是通过给它提供大量的例子让它自己搞清楚这一点,是最新的发展。这个原理的类比就像是教一个孩子区分苹果和橙子。我们会向孩子展示苹果和橙子的图片,而不仅仅是描述这两种水果的特征,比如形状、颜色、大小等。
以下图示展示了机器学习和深度学习的区别:
这是传统机器学习与使用神经网络(深度学习)进行机器学习的主要区别。在传统机器学习中,我们提供特征和标签,而在使用人工神经网络时,我们让算法自己解码特征。
我们生活在一个激动人心的时代,这个时代我们与深度学习之父共同度过,甚至在 Stack Exchange 等地方的在线交流中,我们还能看到 Yann LeCun 和 Geoffrey Hinton 的贡献。这就像是生活在尼古拉斯·奥托(内燃机的发明者)时代,并且向他写信,正是他启动了我们至今仍在演变的汽车革命。而未来 AI 的潜力将使汽车革命相形见绌。确实是一个令人兴奋的时代!
人工神经网络的机制
在本节中,我们将了解构建我们自己 AI 项目所需的基本要素。我们将掌握深度学习技术中常用的术语。
本节旨在提供高层次的基本理论,帮助你获得足够的洞察力,从而能够构建自己的深度神经网络,调整它们,并理解构建最先进的神经网络所需的条件。
生物神经元
我们之前讨论过生物大脑如何成为人工神经网络(ANN)的灵感来源。大脑由数百亿个独立的单元或细胞组成,这些单元被称为神经元。
以下图示展示了一个神经元,它有多个输入信号进入,称为树突。还有一个从细胞体输出的信号,称为轴突:
树突将信息带入神经元,而轴突则允许处理后的信息从神经元流出。但实际上,有成千上万的树突将输入以微小电荷的形式传递给神经元。如果这些由树突携带的微小电荷对神经元主体的总电荷产生影响,或超过某个阈值,那么轴突将会触发。
现在我们了解了生物神经元的工作原理,接下来我们将理解人工神经元是如何工作的。
人工神经元的工作原理
就像生物大脑一样,人工神经网络(ANN)由独立的单元组成,这些单元被称为神经元。像生物神经元一样,人工神经元也有一个进行计算的主体,并且有许多输入馈送到细胞体或神经元:
例如,假设我们有三个输入到神经元。每个输入携带一个 0 或 1 的二进制值。我们有一个从主体流出的输出,它也携带一个 0 或 1 的二进制值。对于这个示例,神经元决定我今天是否应该吃蛋糕。也就是说,如果我应该吃蛋糕,神经元应输出 1;如果不该吃蛋糕,则输出 0:
在我们的示例中,三个输入代表了决定我是否应该吃蛋糕的三个因素。每个因素都有一个重要性权重;例如,第一个因素是 昨天我做了有氧运动,权重为 2。第二个因素是 昨天我去了健身房,权重为 3。第三个因素是 这是一个吃蛋糕的场合,权重为 6。
神经元的主体对输入进行一些计算,比如将所有这些输入相加并检查它们是否超过某个阈值:
所以,对于这个示例,我们设定阈值为 4。如果输入权重的总和超过阈值,则神经元输出 1,表示我可以吃蛋糕。
这可以表示为一个方程式:
** **
这里适用以下内容:
-
Xi 是第一个输入因素,昨天我做了有氧运动。
-
Wi 是第一个输入因素 Xi 的权重。在我们的示例中,Wi = 2。
-
Xii 是第二个输入因素,昨天我去了健身房。
-
Wii 是第二个输入因素 Xii 的权重。在我们的示例中,Wii = 3。
-
Xiii 是第三个输入因素,这是一个吃蛋糕的场合。
-
Wiii 是第三个输入因素 Xiii 的权重。在我们的示例中,Wiii= 6。
-
阈值 为 4。
现在,让我们用这个神经元来决定在三种不同的情况下我是否可以吃蛋糕。
情境 1
我想吃蛋糕,昨天我去了健身房,但我没有做有氧运动,也不是吃蛋糕的场合:
这里适用以下内容:
-
Xi 是第一个输入因素,昨天我做了有氧运动。现在,Xi = 0,因为这是假的。
-
Wi 是第一个输入因素 Xi 的权重。在我们的例子中,Wi = 2。
-
Xii 是第二个输入因素 我昨天去了健身房。现在,Xii = 1,因为这是正确的。
-
Wii 是第二个输入因素 Xii 的权重。在我们的例子中,Wii = 3。
-
Xiii 是第三个输入因素,这时是吃蛋糕的时机。现在,Xiii = 0,因为这个条件是错误的。
-
Wiii 是第三个输入因素 Xiii 的权重。在我们的例子中,Wiii = 6。
-
threshold 是 4。
我们知道神经元计算以下方程:
对于场景 1,方程将转化为以下形式:
这等于这个:
3 ≥ 4 是假的,所以它输出 0,这意味着我不应该吃蛋糕。
场景 2
我想吃蛋糕,而且今天是我的生日,但我没有做有氧运动,也没有去健身房:
这里,以下条件成立:
-
Xi 是第一个输入因素,我昨天做了有氧运动。现在,Xi = 0,因为这个因素是错误的。
-
Wi 是第一个输入因素 Xi 的权重。在我们的例子中,Wi = 2。
-
Xii 是第二个输入因素 我昨天去了健身房。现在,Xii = 0,因为这个因素是错误的。
-
Wii 是第二个输入因素 Xii 的权重。在我们的例子中,Wii = 3。
-
Xiii 是第三个输入因素,这时是吃蛋糕的时机。现在,Xiii = 1,这个因素为真。
-
Wiii 是第三个输入因素 Xiii 的权重。在我们的例子中,Wiii = 6。
-
threshold 是 4。
我们知道神经元计算以下方程:
对于场景 2,方程将转化为以下形式:
这将给我们以下输出:
6 ≥ 4 为真,所以它输出 1,这意味着我可以吃蛋糕。
场景 3
我想吃蛋糕,我昨天做了有氧运动并去了健身房,但这也不是吃蛋糕的时机:
这里,以下条件成立:
-
Xi 是第一个输入因素 我昨天做了有氧运动。现在,Xi = 1,因为这个因素为真。
-
Wi 是第一个输入因素 Xi 的权重。在我们的例子中,Wi = 2。
-
Xii 是第二个输入因素,我昨天去了健身房。现在,Xii = 1,因为这个因素为真。
-
Wii 是第二个输入因素 Xii 的权重。在我们的例子中,Wii = 3。
-
Xiii 是第三个输入因素,这时是吃蛋糕的时机。现在,Xiii = 0,因为这个因素是错误的。
-
Wiii 是第三个输入因素 Xiii 的权重。在我们的例子中,Wiii = 6。
-
threshold 是 4。
我们知道神经元计算以下方程:
对于场景 3,方程将转化为以下形式:
这给我们以下方程:
5 ≥ 4 为真,所以它触发 1,这意味着我可以吃蛋糕。
从之前的三个场景中,我们看到一个单一的人工神经元是如何工作的。这个单元也叫做感知机。感知机本质上处理二进制输入,计算总和,然后与阈值进行比较,最终给出二进制输出。
为了更好地理解感知机(perceptron)是如何工作的,我们可以将前面的方程式转化为一个更通用的形式,以便解释。
假设为了简化起见,只有一个输入因素:
假设阈值 = b。我们的方程式如下:
它现在变成了这样:
这里适用以下规则:
-
w是输入的权重。
-
b是阈值,称为偏置。
这条规则总结了感知机神经元的工作原理。
就像哺乳动物的大脑一样,人工神经网络由许多这样的感知机组成,它们被堆叠并分层在一起。在下一部分,我们将了解这些神经元如何在人工神经网络中协同工作。
人工神经网络(ANN)
就像生物神经元一样,人工神经元也并不是孤立存在的。它们存在于一个由其他神经元组成的网络中。基本上,神经元通过互相传递信息来存在;一些神经元的输出是其他神经元的输入。
在任何人工神经网络(ANN)中,第一个层被称为输入层。这些输入是真实值,比如我们之前示例中的带权重的因素(w.x)。从输入层传递的总和值会传播到下一层的每个神经元。该层的神经元进行计算并将它们的输出传递到下一层,以此类推:
接收来自所有前一层神经元的输入并将其输出传递给下一层所有神经元的层叫做密集层。由于这一层连接着前一层和下一层的所有神经元,它也常被称为全连接层。
输入和计算从一层流向下一层,最终结束于输出层,它给出了整个人工神经网络的最终估计值。
输入层和输出层之间的层被称为隐藏层,因为这些隐藏层内神经元的值对从业者来说是未知的,完全是一个黑箱。
随着层数的增加,你会增加网络的抽象程度,进而提高网络解决更复杂问题的能力。当隐藏层超过三层时,就被称为深度网络(deepnet)。
所以,如果这是一个机器视觉任务,那么第一层隐藏层将寻找边缘,下一层会寻找角落,再下一层寻找曲线和简单的形状,以此类推:
因此,问题的复杂性可以决定所需的层数;更多的层会导致更多的抽象。这些层可以非常深,达到 1,000 层或更多,也可以非常浅,只有大约六层。增加隐藏层的数量不一定能得到更好的结果,因为抽象可能是多余的。
到目前为止,我们已经看到如何将人工神经元堆叠在一起形成神经网络。但我们已经看到,感知机神经元只能接受二进制输入,并且只给出二进制输出。但在实践中,基于感知机思想做事情会出现问题。这个问题就是通过激活函数来解决的。
激活函数
我们现在知道,人工神经网络(ANN)是通过堆叠单个计算单元(称为感知机)来创建的。我们也已经看到感知机是如何工作的,并将其总结为输出 1,如果!**。
也就是说,它根据权重w和偏置b的值,输出1或0。
让我们看一下下面的图表,以了解为什么仅仅输出1或0会出现问题。以下是一个简单感知机的图表,只有一个输入,x:
-
w 是输入的权重,x,b 是偏置。
-
a 是输出,可以是1或0。
在这里,当z的值发生变化时,输出a会在某个点从0变为1。正如你所看到的,输出a的变化是突然和剧烈的:
这意味着,对于某些小的变化,,我们会得到输出a的剧烈变化。如果每个感知机都有如此剧烈的变化,那么它会导致网络不稳定,因此网络无法学习。
因此,为了使网络更高效、更稳定,我们需要减缓每个感知机学习的方式。换句话说,我们需要消除从0到1的突变,转而进行更渐进的变化:
这是通过激活函数来实现的。激活函数是应用于感知机的函数,使其不再输出0或1,而是输出介于0和1之间的任何值。
这意味着每个神经元可以通过使用更小的变化来更慢地学习,并且能够以更大的细节进行学习,。激活函数可以被看作是转换函数,用于将二进制值转换为介于给定最小值和最大值之间的更小的值序列。
有多种方法可以将二进制输出转化为一系列值,分别是 Sigmoid 函数、tanh 函数和 ReLU 函数。我们现在将快速浏览这些激活函数。
Sigmoid 函数
Sigmoid 函数是一个数学函数,对于任何输入,它都会输出一个介于 0 和 1 之间的值:
这里, 和
。
让我们通过一些简单的代码更好地理解 Sigmoid 函数。如果你没有安装 Python,没关系:我们现在将使用一个在线替代方案,网址是 www.jdoodle.com/python-programming-online
。我们将在 第二章 创建一个房地产价格预测移动应用 中从零开始进行完整的设置。目前,让我们继续使用这个在线替代方案。
一旦我们加载了页面 www.jdoodle.com/python-programming-online
,我们可以逐步浏览代码并理解 Sigmoid 函数:
- 首先,让我们导入
math
库,这样我们就可以使用指数函数:
from math import e
- 接下来,让我们定义一个名为
sigmoid
的函数,基于之前的公式:
def sigmoid ( x ):
return 1 / ( 1 + e **- x )
- 假设我们的 z 非常小,比如
-10
,因此该函数将输出一个非常小且接近 0 的数字:
sigmoid(-10)
4.539786870243442e-05
- 如果 z 非常大,比如
10000
,那么该函数将输出最大可能值 1:
sigmoid(10000)
1.0
因此,Sigmoid 函数将任何值 z 转换为介于 0 和 1 之间的值。当 Sigmoid 激活函数在神经元上使用时,而不是传统的感知机算法,我们得到的就是所谓的 Sigmoid 神经元:
Tanh 函数
与 Sigmoid 神经元类似,我们可以应用一个名为 tanh(z) 的激活函数,它将任何值转换为介于 -1 和 1 之间的值。
使用这个激活函数的神经元被称为tanh 神经元:
ReLU 函数
然后有一个激活函数,称为 修正线性单元,ReLU(z),它将任何值 z 转换为 0 或大于 0 的值。换句话说,它将小于 0 的任何值输出为 0,而将大于 0 的任何值输出为其本身:
简要总结我们目前的理解,感知机是传统的、过时的神经元,在实际应用中很少使用。它们非常适合帮助我们简单理解底层原理;然而,由于输出值的剧烈变化,它们存在快速学习的问题。
我们使用激活函数来减少学习速度,并确定 z 或 的细微变化。让我们总结一下这些激活函数:
-
sigmoid 神经元 是使用 sigmoid 激活函数的神经元,它将输出转化为介于 0 和 1 之间的值。
-
tanh 神经元 是使用 tanh 激活函数的神经元,它将输出转化为介于 -1 和 1 之间的值。
-
ReLU 神经元 是使用 ReLU 激活函数的神经元,它将输出转化为 0 或任何大于 0 的值。
Sigmoid 函数在实际中被使用,但与 tanh 和 ReLU 函数相比较慢。tanh 和 ReLU 函数是常用的激活函数。ReLU 函数也被认为是最先进的,通常是构建人工神经网络(ANN)时首选的激活函数。
以下是常用的激活函数列表:
在本书中的项目中,我们将主要使用 sigmoid、tanh 或 ReLU 神经元来构建人工神经网络(ANN)。
代价函数
快速回顾一下,我们已经了解了基本的感知机工作原理及其局限性。然后,我们看到激活函数如何克服了感知机的缺陷,从而诞生了今天使用的其他神经元类型。
现在,我们将探讨如何判断神经元的输出是否错误。任何类型的神经元要学习,都需要知道何时输出了错误的值以及错误的幅度。衡量神经网络错误的最常见方法是使用代价函数。
代价函数 定量化了神经元输出与我们需要的输出之间的差异。常用的两种代价函数是均方误差和交叉熵。
均方误差
均方误差(MSE)也被称为二次代价函数,因为它使用平方差来衡量误差的大小:
这里,适用以下公式:
-
a 是来自人工神经网络(ANN)的输出
-
y 是期望的输出
-
n 是使用的样本数
代价函数相当直接。例如,考虑一个只有一个样本的单神经元(n=1)。如果期望的输出是 2 (y=2),而神经元的输出是 3 (a=3),则 MSE 如下所示:
类似地,如果期望的输出是 3 (y=3),而神经元输出是 2 (a=2),那么 MSE 如下所示:
因此,均方误差(MSE)量化了神经元所犯错误的大小。MSE 的问题之一是,当网络中的数值变得很大时,学习会变得很慢。换句话说,当权重(w)和偏差(b)或 z 很大时,学习会变得非常慢。请记住,我们讨论的是人工神经网络(ANN)中的成千上万个神经元,这就是为什么当学习变得缓慢并最终停滞时,不再有任何进一步的学习。
交叉熵
交叉熵 是一种基于导数的函数,因为它使用了一个特别设计的方程的导数,该方程如下所示:
交叉熵使得当预期输出和实际输出之间的差距较大时,网络可以更快地学习。换句话说,错误越大,它越能帮助网络加速学习。我们将通过一些简单的代码来理解这一点。
和之前一样,如果你没有安装 Python,可以使用在线替代工具,网址为 www.jdoodle.com/python-programming-online
。我们将在第二章《创建一个房地产价格预测 移动应用》中讲解安装和设置。请按照以下步骤,了解网络如何通过交叉熵学习:
- 首先,让我们导入
math
库,以便使用log
函数:
from numpy import log
- 接下来,让我们定义一个名为
cross_entropy
的函数,基于前面的公式:
def cross_entropy(y,a):
return -1 *(y*log(a)+(1-y)*log (1-a))
- 例如,考虑一个只有一个样本的单个神经元(n=1)。假设预期输出是
0
(y=0),而神经元的输出是0.01
(a=0.01):
cross_entropy(0, 0.01)
输出结果如下:
0.010050335853501451
由于预期输出和实际输出值非常小,结果的成本也非常小。
同样地,如果预期输出和实际输出值非常大,那么结果的成本仍然很小:
cross_entropy(1000,999.99)
输出结果如下:
0.010050335853501451
同样地,如果预期输出与实际输出之间的差距很大,那么结果的成本也会很大:
cross_entropy(0,0.9)
输出结果如下:
2.3025850929940459
因此,预期输出与实际输出之间的差异越大,学习速度就越快。通过使用交叉熵,我们可以获得网络的误差,同时,权重和偏差的大小并不重要,有助于网络更快地学习。
梯度下降
到目前为止,我们已经涵盖了基于激活函数使用的不同类型的神经元。我们已经讨论了如何使用成本函数来量化神经元输出的误差。现在,我们需要一个机制来解决这个误差。
网络能够学习输出接近预期或期望输出值的机制被称为 梯度下降。梯度下降是机器学习中常用的一种方法,用于寻找最低成本。
为了理解梯度下降,让我们使用我们迄今为止一直在使用的单个神经元方程:
这里,以下公式适用:
-
x是输入
-
w是输入的权重
-
b是输入的偏置
梯度下降可以表示如下:
最初,神经元通过为w和b分配随机值来开始。从那时起,神经元需要调整w和b的值,以便降低或减少误差或成本(交叉熵)。
对交叉熵(成本函数)求导,会导致w和b朝着最低成本的方向逐步变化。换句话说,梯度下降尝试找到网络输出与预期输出之间的最佳线。
权重会根据一个叫做学习率的参数进行调整。学习率是调整神经元权重的值,以使输出更接近预期输出。
请记住,这里我们只用了一个单一的参数;这是为了让事情更容易理解。实际上,考虑到降低成本,会有成千上万的参数需要考虑。
反向传播——一种让神经网络学习的方法
太好了!我们已经走了很长一段路,从观察生物学神经元,到研究神经元类型,再到确定准确性并纠正神经元的学习。只剩下一个问题:整个神经网络如何一起学习?
反向传播是一种非常聪明的方式,使梯度下降能够遍及整个网络的所有层。反向传播利用微积分中的链式法则,使得信息可以在网络中前后传递:
原则上,输入参数和权重的信息会通过网络传播,猜测预期输出,然后整体的不准确性通过网络的各层进行反向传播,以便调整权重并重新猜测输出。
这个学习的单一循环叫做训练步骤或迭代。每次迭代都是在输入训练样本的一批数据上进行的。每批样本中的样本数叫做批量大小。当所有的输入样本都经历了一次迭代或训练步骤,那么这就叫做一个周期。
举个例子,假设有 100 个训练样本,每次迭代或训练步骤中,网络使用 10 个样本来学习。那么,我们可以说批量大小是 10,并且它需要 10 次迭代才能完成一个周期。如果每个批次的样本是独特的,也就是说每个样本至少被网络使用一次,那么这就是一个周期。
这种预测输出和成本在网络中前后传播的方式就是网络如何学习的过程。
在我们的实践环节中,我们将重新讨论训练步骤、周期、学习率、交叉熵、批量大小等内容。
Softmax
我们已经到达本章的最后一个概念性主题。我们已经讨论了神经元类型、成本函数、梯度下降,最后是应用梯度下降跨越网络的机制,使得在多次迭代中学习成为可能。
之前,我们看到了 ANN 的输入层和密集层或隐藏层:
Softmax是一种特殊的神经元,用于输出层中描述各输出的概率:
为了理解 softmax 方程及其概念,我们将使用一些代码。像以前一样,现在你可以使用任何在线 Python 编辑器来跟随代码。
首先,从math
库中导入指数方法:
from math import exp
为了这个例子,假设这个网络被设计为分类三个可能的标签:A
、B
和C
。假设从前面的层有三个信号输入到 softmax(-1,1,5):
a=[-1.0,1.0,5.0]
解释如下:
-
第一个信号表明输出应该是
A
,但信号较弱,其值为-1。 -
第二个信号表明输出应该是
B
,并且略强,值为 1。 -
第三个信号最强,表明输出应该是
C
,其值为 5。
这些表示的值是对预期输出的置信度度量。
现在,我们来查看 softmax 中第一个信号的分子,猜测输出是A
:
在这里,M是表示输出应该是A
的输出信号强度:
exp(a[0]) # taking the first element of a[-1,1,5] which represents A
0.36787944117144233
接下来,这是 softmax 中第二个信号的分子,猜测输出是B
:
在这里,M
是表示输出应该是B
的输出信号强度:
exp(a[0]) # taking the second element of a[-1,1,5] which represents B
2.718281828459045
最后,这是 softmax 中第二个信号的分子,猜测输出是C
:
在这里,M
是表示输出应该是C
的输出信号强度:
exp(a[2])
# taking the third element of a[-1,1,5] which represents C
148.4131591025766
我们可以观察到,表示的置信度值总是大于 0,并且结果会被指数级放大。
现在,让我们解释 softmax 函数的分母,它是每个信号值的指数和:
让我们为 softmax 函数写一些代码:
sigma = exp ( a [ 0 ]) + exp ( a [ 1 ]) + exp ( a [ 2 ])
sigma
151.49932037220708
因此,第一个信号正确的概率如下:
exp(a[0])/sigma
0.0024282580295913376
这意味着A
的概率不到 1%。
同样,第三个信号正确的概率如下:
exp(a[2])/sigma
0.9796292071670795
这意味着预期输出是C
的概率超过 97%。
本质上,softmax 接受一个加权信号,表示某个类别预测的置信度,并输出一个介于 0 到 1 之间的概率分数,针对所有这些类别。
很棒!我们已经掌握了实现项目所需的基本高层理论。接下来,我们将通过探索 TensorFlow Playground 来总结我们对这些概念的理解。
TensorFlow Playground
在我们开始使用 TensorFlow Playground 之前,让我们快速回顾一下基本概念。这将帮助我们更好地理解 TensorFlow Playground。
神经网络的灵感来源于生物大脑,而大脑中的最小单元是 神经元。
感知器(Perceptron) 是一种基于生物神经元概念的神经元。感知器基本上处理二进制输入和输出,这使得它在实际应用中不太可行。此外,由于其二进制性质,它对输入的微小变化会产生剧烈的输出变化,因此学习速度过快,无法提供细节。
激活函数 用于解决感知器的问题。这催生了其他类型的神经元,这些神经元处理介于 0 到 1、-1 到 1 等区间之间的值,而不仅仅是 0 或 1。
人工神经网络(ANNs) 由这些堆叠在各层中的神经元组成。它包括输入层、密集层或全连接层,以及输出层。
代价函数,如均方误差(MSE)和交叉熵(cross entropy),是衡量神经元输出误差大小的方式。
梯度下降 是一种机制,通过它,神经元可以学习输出更接近期望或目标输出的值。
反向传播 是一种非常聪明的方法,它使梯度下降在网络中的所有层中得以实现。
每次反向传播或预测输出与成本在网络中的迭代过程称为 训练步骤。
学习率 是在每次训练步骤中调整神经元权重的值,以使输出更接近期望的输出。
Softmax 是一种特殊的神经元,它接受一个加权信号,表示某个类别预测的置信度,并为所有这些类别输出一个介于 0 到 1 之间的概率值。
现在,我们可以访问 TensorFlow Playground:Playground.tensorflow.org
。TensorFlow Playground 是一个在线工具,用于可视化人工神经网络(ANN)或深度网络的运行,是一个很好的地方,可以直观地重复我们在概念上学到的内容。
现在,废话不多说,让我们开始使用 TensorFlow Playground。页面加载完成后,您将看到一个仪表板,可以创建自己的神经网络来解决预定义的分类问题。以下是默认页面及其各个部分的截图:
让我们来看一下这个截图中的各个部分:
-
第一部分:数据部分显示了选择预构建问题的选项,用于构建和可视化网络。第一个问题是选择的,基本上是区分蓝点和橙点。在下面,还有将数据分为训练集和测试集的控制项。还有一个参数用于设置批次大小。批次大小是每次训练步骤中输入到网络进行学习的样本数量。
-
第二部分:特征部分表示输入参数的数量。在这种情况下,选择了两个特征作为输入特征。
-
第三部分:隐藏层部分是我们可以创建隐藏层以增加复杂度的地方。这里还有控制项可以增加或减少每个隐藏层或全连接层中的神经元数量。在这个示例中,有两个隐藏层,分别包含 4 个和 2 个神经元。
-
第四部分:输出部分是我们可以看到损失或代价图表的地方,同时还可视化网络分离红点和蓝点的学习效果。
-
第五部分:这是调整网络调优参数的控制面板。它有一个小部件可以启动、暂停和刷新网络的训练。在它旁边是一个计数器,显示经过的训练轮数。接下来是学习率,表示权重调整的常数。然后是选择在神经元中使用的激活函数。最后,有一个选项可以指示可视化的问题类型,即分类或回归。在这个示例中,我们正在可视化一个分类任务。
-
目前我们将忽略正则化和正则化率,因为我们尚未以概念性的方式讲解这些术语。我们将在书中的后续部分介绍这些术语,当时会更容易理解它们的目的。
现在我们准备开始使用 TensorFlow Playground。我们将从第一个数据集开始,调整以下调优参数:
-
学习率 = 0.01
-
激活函数 = Tanh
-
正则化 = 无
-
正则化率 = 0
-
问题类型 = 分类
-
数据 = Circle
-
训练数据与测试数据的比例 = 50%
-
批次大小 = 10
-
特征 = X[1] 和 X[2]
-
两个隐藏/全连接层;第一个层有 4 个神经元,第二个层有 2 个神经元
现在通过点击仪表盘左上角的播放按钮开始训练。从播放/暂停按钮向右移动,我们可以看到已经经过的训练轮数。在大约 200 轮时,暂停训练并观察输出部分:
仪表盘的关键观察点如下:
-
我们可以在仪表盘的右侧看到网络的性能图。测试和训练损失分别是网络在测试和训练过程中的代价。如前所述,目标是最小化代价。
-
在下方,您将看到一个可视化图,展示了网络如何将蓝色点从橙色点中分离或分类。
-
如果我们将鼠标指针悬停在任何神经元上,我们可以看到该神经元已学会如何将蓝色点和橙色点分开。话虽如此,我们来仔细看看第二层的两个神经元,看看它们在这个任务中学到了什么。
-
当我们将鼠标悬停在第二层的第一个神经元上时,我们可以看到这个神经元已经很好地学会了当前的任务。相比之下,第二层的第二个神经元学到的任务知识较少。
-
这引出了从神经元中出来的虚线:它们是神经元的对应权重。蓝色虚线表示正权重,而橙色虚线表示负权重。它们通常被称为张量。
另一个关键观察是,第二层中的第一个神经元输出的张量信号比第二个神经元强。这表明该神经元在将蓝色和橙色点分离的整体任务中的影响力,而且当我们看到它与最终结果的可视化对比时,这一点非常明显。
现在,牢记我们在本章中学到的所有术语,我们可以通过改变参数来进行试验,看看这如何影响整个网络。甚至可以添加新的层和神经元。
TensorFlow Playground 是一个很好的地方,可以重温 ANNs 的基本原理和核心概念。
总结
到目前为止,我们已经在高层次上涵盖了基本概念,这足以让我们欣赏本书中将要做的实际操作。拥有概念性的理解已经足够让我们开始构建 AI 模型,但更深入的理解也是非常有用的。
在下一章中,我们将设置构建 AI 应用程序的环境,并创建一个小型的 Android 和 iOS 移动应用,能够使用基于 Keras 和 TensorFlow 构建的模型来预测房价。
进一步阅读
这里有一份资源列表,可以作为参考,以帮助我们更好地理解并深入探索 AI 和深度学习的概念:
-
Neural Networks and deep learning,
neuralnetworksanddeeplearning.com/
-
Michael Taylor 的Make Your Own Neural Network: An In-depth Visual Introduction For Beginners,
www.amazon.in/Machine-Learning-Neural-Networks-depth-ebook/dp/B075882XCP
-
Tariq Rashid 的Make Your Own Neural Network,
www.amazon.in/Make-Your-Own-Neural-Network-ebook/dp/B01EER4Z4G
-
Nick Bostrom 的Superintelligence,
en.wikipedia.org/wiki/Superintelligence:_Paths,_Dangers,_Strategies
-
Pedro Domingos 的《大师算法》,
en.wikipedia.org/wiki/The_Master_Algorithm
-
《深度学习》,
www.deeplearningbook.org/
第二章:创建一个房地产价格预测移动应用
在上一章,我们讲解了理论基础;而本章将会介绍所有工具和库的设置。
首先,我们将设置环境,构建一个 Keras 模型,用于通过房地产数据预测房价。接着,我们将使用 Flask 构建一个 RESTful API 来提供此模型。然后,我们将为 Android 设置环境,并创建一个应用,该应用将调用此 RESTful API,根据房地产的特征预测房价。最后,我们将在 iOS 上重复相同的操作。
本章的重点是设置、工具、库,并应用在第一章中学到的概念,人工智能概念与基础。此用例设计简洁,但足够灵活,可以适应类似的用例。通过本章的学习,你将能轻松创建用于预测或分类任务的移动应用。
本章将涵盖以下内容:
-
设置人工智能环境
-
使用 Keras 和 TensorFlow 构建 ANN 模型进行预测
-
将模型作为 API 提供
-
创建一个 Android 应用来预测房价
-
创建一个 iOS 应用来预测房价
设置人工智能 环境
首先需要做的事情是安装 Python。我们将在本书中使用 Python 进行所有的人工智能(AI)任务。有两种方式可以安装 Python,一种是通过www.python.org/downloads/
提供的可执行文件下载安装,另一种是通过 Anaconda 安装。我们将采用后一种方式,也就是使用 Anaconda。
下载并安装 Anaconda
现在,让我们访问 Anaconda 的官方安装页面(conda.io/docs/user-guide/install/index.html#regular-installation
),并根据你的操作系统选择合适的安装选项:
按照文档中的说明操作,安装过程需要一些时间。
安装完成后,让我们测试一下安装情况。打开命令提示符,输入conda list
命令。你应该能看到一个包含所有已安装库和包的列表,这些库和包是通过 Anaconda 安装的:
如果没有得到这个输出,请参考我们之前查看的官方文档页面,并重试。
Anaconda 的优点
让我们讨论使用包管理工具的一些优点:
-
Anaconda 允许我们创建环境以安装库和包。这个环境完全独立于操作系统或管理员库。这意味着我们可以为特定项目创建自定义版本的库的用户级环境,从而帮助我们以最小的努力在不同操作系统之间迁移项目。
-
Anaconda 可以拥有多个环境,每个环境都有不同版本的 Python 和支持库。这样可以避免版本不匹配,并且不受操作系统中现有包和库的影响。
-
Anaconda 预装了大多数数据科学相关任务所需的包和库,包括一个非常流行的交互式 Python 编辑器——Jupyter Notebook。在本书中,我们将大量使用 Jupyter Notebook,特别是在需要交互式编码任务时。
创建 Anaconda 环境
我们将创建一个名为ai-projects
的环境,使用 Python 版本 3.6。所有的依赖项都将安装在这个环境中:
conda create -n ai-projects python=3.6 anaconda
现在,继续并接受你看到的提示,你应该看到如下的输出:
在我们开始安装依赖项之前,我们需要使用activate ai-projects
命令激活我们刚刚创建的环境,如果你使用的是 bash shell,可以输入source activate ai-projects
。提示符会发生变化,表明环境已被激活:
安装依赖项
首先,让我们安装 TensorFlow。它是一个开源框架,用于构建人工神经网络(ANN):
pip install tensorflow
你应该看到以下输出,表示安装成功:
我们还可以手动检查安装情况。在命令行输入python
打开 Python 提示符。进入 Python 提示符后,输入import tensorflow
并按Enter。你应该看到以下输出:
输入exit()
返回到默认命令行,记住我们仍然在ai-projects
conda 环境中。
接下来,我们将安装 Keras,它是 TensorFlow 的一个封装器,使得设计深度神经网络更加直观。我们继续使用pip
命令:
pip install keras
安装成功后,我们应该看到以下输出:
要手动检查安装情况,在命令行输入python
打开 Python 提示符。进入 Python 提示符后,输入import keras
并按Enter。你应该看到以下输出,没有错误。请注意,输出中提到 Keras 正在使用 TensorFlow 作为其后端:
太好了!我们现在已经安装了创建我们自己神经网络所需的主要依赖项。接下来,让我们构建一个 ANN 来预测房地产价格。
使用 Keras 和 TensorFlow 构建用于预测的 ANN 模型
现在我们已经安装了必要的库,让我们创建一个名为aibook
的文件夹,在其中创建另一个名为chapter2
的文件夹。将本章的所有代码移动到chapter2
文件夹中。确保 conda 环境仍然处于激活状态(提示符将以环境名称开头):
一旦进入chapter2
文件夹,输入jupyter notebook
。这将在浏览器中打开一个交互式 Python 编辑器。
在右上角使用“New”下拉菜单创建一个新的 Python 3 笔记本:
我们现在准备使用 Keras 和 TensorFlow 构建第一个 ANN 模型,用于预测房地产价格:
- 导入我们为此练习所需的所有库。使用第一个单元格导入所有库并运行它。这里是我们将使用的四个主要库:
-
-
pandas
:我们用它来读取数据并将其存储在数据框中 -
sklearn
:我们用它来标准化数据和进行 k 折交叉验证 -
keras
:我们用它来构建顺序神经网络 -
numpy
:我们使用numpy
进行所有数学和数组操作
-
让我们导入这些库:
import numpy
import pandas as pd
from keras.models import Sequential
from keras.layers import Dense
from keras import optimizers
from keras.wrappers.scikit_learn import KerasRegressor
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import KFold
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
- 使用
pandas
加载房地产数据:
dataframe = pd.read_csv("housing.csv", sep=',', header=0)
dataset = dataframe.values
- 要查看特征变量、目标变量以及数据的几行,请输入以下内容:
dataframe.head()
这个输出将是几行dataframe
,如以下截图所示:
数据集有八列,每列的详细信息如下:
-
BIZPROP:每个城镇的非零售商业用地比例
-
ROOMS:每个住宅的平均房间数
-
AGE:建于 1940 年之前的自有住房单位的比例
-
HIGHWAYS:通往放射状高速公路的可达性指数
-
TAX:每$10,000 的全额财产税率
-
PTRATIO:每个城镇的师生比
-
LSTAT:低社会阶层人口的百分比
-
VALUE:拥有者自住住房的中位数价值,单位为千美元(目标变量)
在我们的应用场景中,我们需要预测 VALUE 列,因此我们需要将数据框分为特征和目标值。我们将使用 70/30 的分割比例,即 70%的数据用于训练,30%的数据用于测试:
features = dataset[:,0:7]
target = dataset[:,7]
此外,为了确保我们能重现结果,我们为随机生成设置一个种子。这个随机函数在交叉验证时用于随机抽样数据:
# fix random seed for reproducibility
seed = 9
numpy.random.seed(seed)
现在我们准备构建我们的人工神经网络(ANN):
-
创建一个具有简单且浅层架构的顺序神经网络。
-
创建一个名为
simple_shallow_seq_net()
的函数,定义神经网络的架构:
def simple_shallow_seq_net():
# create a sequential ANN
model = Sequential()
model.add(Dense(7, input_dim=7, kernel_initializer='normal', activation='sigmoid'))
model.add(Dense(1, kernel_initializer='normal'))
sgd = optimizers.SGD(lr=0.01)
model.compile(loss='mean_squared_error', optimizer=sgd)
return model
- 该函数执行以下操作:
model = Sequential()
- 创建一个顺序模型——顺序模型是一个通过线性堆叠的层构建的 ANN 模型:
model.add(Dense(7, input_dim=7, kernel_initializer='normal', activation='sigmoid'))
- 在这里,我们向这个顺序网络添加了一个具有七个神经元的稠密层或全连接层。此层接受具有
7
个特征的输入(因为有七个输入或特征用于预测房价),这由input_dim
参数指示。此层所有神经元的权重都使用随机正态分布初始化,这由kernel_initializer
参数指示。同样,此层所有神经元使用 sigmoid 激活函数,这由activation
参数指示:
model.add(Dense(1, kernel_initializer='normal'))
- 添加一个使用随机正态分布初始化的单神经元层:
sgd = optimizers.SGD(lr=0.01)
- 设置网络使用标量梯度下降(SGD)进行学习,通常作为
optimizers
指定。我们还表明网络将在每一步学习中使用学习率(lr
)为0.01
:
model.compile(loss='mean_squared_error', optimizer=sgd)
- 指示网络需要使用均方误差(MSE)代价函数来衡量模型的误差幅度,并使用 SGD 优化器从模型的错误率或损失中学习:
return model
最后,该函数返回一个具有定义规范的模型。
下一步是设置一个用于可重现性的随机种子;此随机函数用于将数据分成训练和验证集。使用的方法是 k-fold 验证,其中数据随机分为 10 个子集进行训练和验证:
seed = 9
kfold = KFold(n_splits=10, random_state=seed)
现在,我们需要适应这个模型以预测数值(在这种情况下是房价),因此我们使用KerasRegressor
。KerasRegressor
是一个 Keras 包装器,用于访问sklearn
中的回归估计器模型:
estimator = KerasRegressor(build_fn=simple_shallow_seq_netl, epochs=100, batch_size=50, verbose=0)
注意以下事项:
-
我们将
simple_shallow_seq_net
作为参数传递,以指示返回模型的函数。 -
epochs
参数表示每个样本需要至少经过网络100
次。 -
batch_size
参数表示在每次学习周期中,网络同时使用50
个训练样本。
下一步是训练和跨验证数据子集,并打印 MSE,这是评估模型表现的度量:
results = cross_val_score(estimator, features, target, cv=kfold)
print("simple_shallow_seq_model:(%.2f) MSE" % (results.std()))
这将输出 MSE - 如您所见,这个值相当高,我们需要尽可能地降低它:
simple_shallow_seq_net:(163.41) MSE
保存这个模型以备后用:
estimator.fit(features, target)
estimator.model.save('simple_shallow_seq_net.h5')
很好,我们已经建立并保存了第一个用于预测房地产价格的神经网络。我们接下来的努力是改进这个神经网络。在调整网络参数之前,首先尝试的是在标准化数据并使用它时提高其性能(降低 MSE):
estimators = []
estimators.append(('standardize', StandardScaler()))
estimators.append(('estimator', KerasRegressor(build_fn=simple_shallow_seq_net, epochs=100, batch_size=50, verbose=0)))
pipeline = Pipeline(estimators)
在上述代码中,我们创建了一个流水线来标准化数据,然后在每个学习周期中使用它。在以下代码块中,我们训练和交叉评估神经网络:
results = cross_val_score(pipeline, features, target, cv=kfold)
print("simple_std_shallow_seq_net:(%.2f) MSE" % (results.std()))
这将输出比以前好得多的 MSE,因此标准化和使用数据确实产生了差异:
simple_std_shallow_seq_net:(65.55) MSE
保存这个模型与之前略有不同,因为我们使用了pipeline
来拟合模型:
pipeline.fit(features, target)
pipeline.named_steps['estimator'].model.save('standardised_shallow_seq_net.h5')
现在,让我们调整一下我们的网络,看看能否获得更好的结果。我们可以从创建一个更深的网络开始。我们将增加隐藏层或全连接层的数量,并在交替的层中使用sigmoid
和tanh
激活函数:
def deep_seq_net():
# create a deep sequential model
model = Sequential()
model.add(Dense(7, input_dim=7, kernel_initializer='normal', activation='sigmoid'))
model.add(Dense(7,activation='tanh'))
model.add(Dense(7,activation='sigmoid'))
model.add(Dense(7,activation='tanh'))
model.add(Dense(1, kernel_initializer='normal'))
sgd = optimizers.SGD(lr=0.01)
model.compile(loss='mean_squared_error', optimizer=sgd)
return model
下一个代码块用于标准化训练数据中的变量,然后将浅层神经网络模型拟合到训练数据中。创建管道并使用标准化数据拟合模型:
estimators = []
estimators.append(('standardize', StandardScaler())) estimators.append(('estimator', KerasRegressor(build_fn=deep_seq_net, epochs=100, batch_size=50, verbose=0)))
pipeline = Pipeline(estimators)
现在,我们需要在数据的各个子集上交叉验证拟合模型并打印均方误差(MSE):
results = cross_val_score(pipeline, features, target, cv=kfold)
print("simple_std_shallow_seq_net:(%.2f) MSE" % (results.std()))
这将输出一个比我们之前创建的浅层网络更好的 MSE:
deep_seq_net:(58.79) MSE
保存模型以便后续使用:
pipeline.fit(features, target)
pipeline.named_steps['estimator'].model.save('deep_seq_net.h5')
因此,当我们增加网络的深度(层数)时,结果会更好。现在,让我们看看当我们加宽网络时会发生什么,也就是说,增加每一层中神经元(节点)的数量。我们定义一个深而宽的网络来解决这个问题,将每一层的神经元数增加到21
。此外,这次我们将在隐藏层中使用relu
和sigmoid
激活函数:
def deep_and_wide_net():
# create a sequential model
model = Sequential()
model.add(Dense(21, input_dim=7, kernel_initializer='normal', activation='relu'))
model.add(Dense(21,activation='relu'))
model.add(Dense(21,activation='relu'))
model.add(Dense(21,activation='sigmoid'))
model.add(Dense(1, kernel_initializer='normal'))
sgd = optimizers.SGD(lr=0.01)
model.compile(loss='mean_squared_error', optimizer=sgd)
return model
下一个代码块用于标准化训练数据中的变量,然后将深而宽的神经网络模型拟合到训练数据中:
estimators = []
estimators.append(('standardize', StandardScaler()))
estimators.append(('estimator', KerasRegressor(build_fn=deep_and_wide_net, epochs=100, batch_size=50, verbose=0)))
pipeline = Pipeline(estimators)
现在,我们需要在数据的各个子集上交叉验证拟合模型并打印均方误差(MSE):
results = cross_val_score(pipeline, features, target, cv=kfold)
print("deep_and_wide_model:(%.2f) MSE" % (results.std()))
这次,MSE 再次优于我们之前创建的网络。这是一个很好的例子,展示了更深的网络和更多的神经元如何更好地抽象问题:
deep_and_wide_net:(34.43) MSE
最后,保存网络以便后续使用。保存的网络模型将在下一节中使用,并通过 REST API 提供服务:
pipeline.fit(features, target)
pipeline.named_steps['estimator'].model.save('deep_and_wide_net.h5')
到目前为止,我们已经能够利用各种网络架构构建一个用于预测的序列神经网络。作为练习,尝试以下内容:
-
尝试调整网络的形状;玩玩网络的深度和宽度,看看它如何影响输出结果
-
尝试不同的激活函数(
keras.io/activations/
) -
尝试不同的初始化器,这里我们只使用了随机正态初始化器(
keras.io/initializers/
) -
我们在这里使用的数据是为了演示该技术,因此可以尝试在其他数据集上使用上述技术进行预测的不同用例(
data.world/datasets/prediction
)
我们将在第四章,构建一个用于分类花卉物种的机器视觉移动应用程序中了解更多关于优化器和正则化器的知识,这些是你可以用来调整网络的其他参数。我们创建 ANN 模型的完整代码作为 Python 笔记本文件,名为sequence_networks_for_prediction.ipynb
。
将模型作为 API 提供服务
现在我们已经创建了一个预测模型,接下来需要通过 RESTful API 来服务这个模型。为此,我们将使用一个轻量级的 Python 框架 Flask:flask.pocoo.org/
。
如果我们的 conda 环境中尚未安装 Flask
库,首先安装它:
pip install Flask
构建一个简单的 API 来添加两个数字
现在我们将构建一个非常简单的 API,以掌握 Flask
库和框架。这个 API 将接受一个包含两个数字的 JSON 对象,并返回这两个数字的和作为响应。
从 Jupyter 主页面打开一个新的 notebook:
- 导入我们需要的所有库,并创建一个应用实例:
from flask import Flask, request
app = Flask(__name__)
- 使用
route()
装饰器创建 RESTful API 的首页:
@app.route('/')
def hello_world():
return 'This is the Index page'
- 使用
route()
装饰器创建一个POST
API 来添加两个数字。这个 API 接受一个包含要添加的数字的 JSON 对象:
@app.route('/add', methods=['POST'])
def add():
req_data = request.get_json()
number_1 = req_data['number_1']
number_2 = req_data['number_2']
return str(int(number_1)+int(number_2))
保存 Python notebook,并使用文件菜单将其下载为 Python 文件。将 Python 文件放置在与模型文件相同的目录中。
启动一个新的命令终端,进入包含此 Python 文件和模型的文件夹。确保激活 conda 环境,并运行以下命令启动一个服务器来运行简单的 API:
- 如果你在使用 Windows,输入以下命令:
set FLASK_APP=simple_api
- 如果你不是在使用 Windows,输入以下命令:
export FLASK_APP=simple_api
然后输入以下命令:
flask run
当服务器启动时,你应该看到以下输出:
打开浏览器,将此地址粘贴到 URL 栏中以访问首页:http://127.0.0.1:5000/
。以下是输出:
接下来,我们将使用 curl
来访问添加两个数字的 POST
API。打开一个新的终端,并输入以下 curl 命令来测试 /add
API。此示例中要添加的数字是 1
和 2
,并作为 JSON 对象传递:
curl -i -X POST -H "Content-Type: application/json" -d "{\"number_1\":\"1\",\"number_2\":\"2\"}" http://127.0.0.1:5000/add
如果没有错误,我们将收到一个包含数字和的响应:
简单 API 的完整代码可以在名为 simple_api.ipynb
的 Python notebook 文件中找到,也可以在名为 simple_api.py
的 Python 文件中找到。
构建一个 API 来预测使用保存的模型进行房地产价格预测
现在我们已经了解了 Flask
的工作原理,我们需要实现一个 API 来服务我们之前构建的模型。启动一个新的 Jupyter Notebook 并按照以下步骤操作:
- 导入所需的 Python 模块并创建一个 Flask 应用实例:
from flask import Flask, request
from keras.models import load_model
from keras import backend as K
import numpy
app = Flask(__name__)
- 使用
route()
装饰器为 RESTful API 创建Index page
:
@app.route('/')
def hello_world():
return 'Index page'
- 创建一个
POST
API 来预测房价,使用route()
装饰器。该 API 接受一个包含预测房价所需所有特征的 JSON 对象:
@app.route('/predict', methods=['POST'])
def add():
req_data = request.get_json()
bizprop = req_data['bizprop']
rooms = req_data['rooms']
age = req_data['age']
highways = req_data['highways']
tax = req_data['tax']
ptratio = req_data['ptratio']
lstat = req_data['lstat']
# This is where we load the actual saved model into new variable.
deep_and_wide_net = load_model('deep_and_wide_net.h5')
# Now we can use this to predict on new data
value = deep_and_wide_net.predict_on_batch(numpy.array([[bizprop, rooms, age , highways , tax , ptratio , lstat]], dtype=float))
K.clear_session()
return str(value)
保存 Python notebook,并使用文件菜单将其下载为 Python 文件。将 Python 文件放置在与模型文件相同的目录中。
启动新的命令终端并转到包含此 Python 文件和模型的文件夹。确保激活 conda 环境并运行以下内容以启动运行简单 API 的服务器:
- 如果您使用的是 Windows,请输入以下内容:
set FLASK_APP=predict_api
- 如果您不使用 Windows,请使用以下内容:
export FLASK_APP= predict_api
然后输入以下内容:
flask run
接下来,我们将使用 curl
访问预测房价的 POST
API。打开新的终端并输入以下 curl
命令以测试 /predict
API。我们可以将要用作模型输入的特征作为 JSON 对象传递:
curl -i -X POST -H "Content-Type: application/json" -d "{\"bizprop\":\"1\",\"rooms\":\"2\",\"age\":\"1\",\"highways\":\"1\",\"tax\":\"1\",\"ptratio\":\"1\",\"lstat\":\"1\"}" http://127.0.0.1:5000/predict
这将输出根据提供的特征预测的房价:
就这样!我们刚刚构建了一个 API 来提供我们的预测模型,并使用 curl
进行了测试。预测 API 的完整代码以 Python 笔记本 predict_api.ipynb
和 Python 文件 simple_api.py
的形式提供。
接下来,我们将看到如何制作一个移动应用程序,该应用程序将使用托管我们模型的 API。我们将首先创建一个使用预测 API 的 Android 应用程序,然后在 iOS 应用程序上重复相同的任务。
创建一个 Android 应用程序来预测房价
在本节中,我们将通过 Android 应用程序的 RESTful API 消耗模型。本节的目的是演示模型如何被 Android 应用程序消耗和使用。在这里,我们假设您熟悉 Java 编程的基础知识。相同的方法也可以用于任何类似的用例,甚至是 Web 应用程序。本节涵盖以下步骤:
-
下载并安装 Android Studio
-
创建一个具有单个屏幕的新 Android 项目
-
设计屏幕布局
-
添加接受输入功能
-
添加消耗模型提供的 RESTful API 功能
-
附加说明
下载并安装 Android Studio
Android Studio 是用于 Android 应用开发的开发环境和沙盒。所有我们的 Android 项目都将使用 Android Studio 制作。我们可以使用 Android Studio 创建、设计和测试应用程序,然后再发布它们。
前往官方 Android Studio 下载页面,developer.android.com/studio/
,并选择与您操作系统匹配的版本。在本例中,我们使用的是 Windows 可执行文件:
下载可执行文件后,运行以开始安装过程。您将看到逐步安装菜单选项。选择“下一步”并继续安装过程。在安装步骤中,大多数选项都将选择默认设置。
创建一个具有单个屏幕的新 Android 项目
现在我们已经安装了 Android Studio,我们将创建一个简单的应用程序来根据某些输入估算房地产的价格。
一旦启动 Android Studio,它将提供一个菜单来开始创建项目。点击“开始一个新的 Android Studio 项目”选项:
下一个对话框是选择应用名称和项目位置。选择你想要的,并点击下一步:
接下来,选择应用程序要运行的目标版本:
然后选择一个应用屏幕;在这种情况下,选择一个空白活动:
选择屏幕或活动名称以及相应的布局或活动屏幕的设计名称:
在构建完成后,项目应该会在几秒钟内加载。在项目结构中,有三个主要文件夹:
-
manifests:这个文件夹包含了用于权限和应用版本管理的 manifest 文件。
-
java:这个文件夹包含了所有的 Java 代码文件(java|app|chapter2|realestateprediction|MainActivity.java)。
-
res:这个文件夹包含了应用程序中使用的所有布局文件和媒体文件(res|layout|activity_main.xml):
设计屏幕的布局
让我们设计一个屏幕,接受我们创建的模型的输入因素。屏幕将有七个输入框来接受这些因素,一个按钮和一个输出文本框来显示预测结果:
浏览到 res 文件夹中的 layout 文件夹,并选择 activity_layout.xml 文件在编辑面板中打开。选择底部的 Text 选项以查看现有的布局 XML:
现在,替换现有的 XML 代码,使用新的设计模板。请参考 Android 文件夹中的 activity_layout.xml 代码文件以查看完整的设计模板。以下仅是 XML 代码模板的骨架参考:
*<?*xml version="1.0" encoding="utf-8"*?>* <ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<TextView
android:id="@+id/bizprop"/>
<EditText
android:id="@+id/bizprop-edit"/>
<TextView
android:id="@+id/rooms"/>
<EditText
android:id="@+id/rooms-edit"/>
<TextView
android:id="@+id/age"/>
<EditText
android:id="@+id/age-edit"/>
<TextView
android:id="@+id/highways"/>
<EditText
android:id="@+id/highways-edit"/>
<TextView
android:id="@+id/tax"/>
<EditText
android:id="@+id/tax-edit"/>
<TextView
android:id="@+id/ptratio"/>
<EditText
android:id="@+id/ptratio-edit"/>
<TextView
android:id="@+id/lstat"/>
<EditText
android:id="@+id/lstat-edit"/>
<Button
android:id="@+id/button"/>
<TextView
android:id="@+id/value"/>
</RelativeLayout>
</ScrollView>
在这里,我们设计了一个布局来接受七个因素作为输入,具体如下:
-
BIZPROP:每个城镇非零售商业用地的比例
-
ROOMS:每个住宅的平均房间数
-
A****GE:1940 年之前建成的业主自住单元的比例
-
HIGHWAYS:通往辐射状高速公路的可达性指数
-
TAX:每$10,000 的全值财产税率
-
PTRATIO:每个城镇的学生与教师比例
-
LSTAT:低社会阶层人口的比例
还有一个按钮和一个文本框用于显示输出。当点击按钮时,预测值将显示出来。
要查看活动的设计,可以在顶部菜单栏的run菜单中选择运行应用程序选项。第一次运行时,环境会提示你创建一个虚拟设备来测试你的应用程序。你可以创建一个Android 虚拟设备(AVD)或使用传统的方法,即使用 USB 线将你的 Android 手机连接到 PC,这样你就可以直接在设备上运行输出:
当应用在设备或 AVD 模拟器上启动时,你应该能看到滚动布局的设计:
添加一个功能来接受输入
现在,我们需要接受输入并创建一个映射来保存这些值。然后,我们将把这个映射转换为一个 JSON 对象,以便它可以作为数据传递给POST
API 请求。
浏览到MainActivity.java
文件并在 Android Studio 的编辑面板中打开它。声明以下类变量:
private EditText bizprop, rooms, age, highways, tax, ptratio, lstat;
private Button estimate;
private TextView value;
你会看到一个名为onCreate()
的函数已经创建。将以下代码添加到onCreate()
函数中,以初始化布局元素:
bizprop = (EditText) findViewById(R.id.*bizprop_edit*);
rooms = (EditText) findViewById(R.id.*rooms_edit*);
age = (EditText) findViewById(R.id.*age_edit*);
highways = (EditText) findViewById(R.id.*highways_edit*);
tax = (EditText) findViewById(R.id.*tax_edit*);
ptratio = (EditText) findViewById(R.id.*ptratio_edit*);
lstat = (EditText) findViewById(R.id.*lstat_edit*);
value = (TextView) findViewById(R.id.*value*);
estimate = (Button) findViewById(R.id.*button*);
现在,向 Java 类中添加另一个名为makeJSON()
的函数。这个函数接受来自编辑框的值,并返回我们需要传递的 JSON 对象,以供 API 调用使用:
public JSONObject makeJSON() {
JSONObject jObj = new JSONObject();
try {
jObj.put("bizprop", bizprop.getText().toString());
jObj.put("rooms", rooms.getText().toString());
jObj.put("age", age.getText().toString());
jObj.put("tax", tax.getText().toString() );
jObj.put("highways", highways.getText().toString());
jObj.put("ptratio", ptratio.getText().toString());
jObj.put("lstat", lstat.getText().toString());
} catch (Exception e) {
System.*out*.println("Error:" + e);
}
Log.*i*("", jObj.toString());
return jObj;
}
添加一个功能来调用提供模型的 RESTful API
现在,我们需要在按钮点击时提交数据给 API。为此,我们需要以下辅助函数:
ByPostMethod
: 接受一个 URL 作为String
并返回一个InputStream
作为响应。这个函数接受我们使用 Flask 框架创建的服务器 URL 字符串,并返回来自服务器的响应流:
InputStream ByPostMethod(String ServerURL) {
InputStream DataInputStream = null;
try {
URL url = new URL(ServerURL);
HttpURLConnection connection = (HttpURLConnection)
url.openConnection();
connection.setDoOutput(true);
connection.setDoInput(true);
connection.setInstanceFollowRedirects(false);
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("charset", "utf-8");
connection.setUseCaches (false);
DataOutputStream dos = new DataOutputStream(connection.getOutputStream());
dos.writeBytes(makeJSON().toString());
*//flushes data output stream.* dos.flush();
dos.close();
*//Getting HTTP response code* int response = connection.getResponseCode();
*//if response code is 200 / OK then read Inputstream
//HttpURLConnection.HTTP_OK is equal to 200* if(response == HttpURLConnection.*HTTP_OK*) {
DataInputStream = connection.getInputStream();
}
} catch (Exception e) {
Log.*e*("ERROR CAUGHT", "Error in GetData", e);
}
return DataInputStream;
}
ConvertStreamToString
: 这个函数接受InputStream
并返回响应的String
。前一个函数返回的输入流将被此函数处理为字符串对象:
String ConvertStreamToString(InputStream stream) {
InputStreamReader isr = new InputStreamReader(stream);
BufferedReader reader = new BufferedReader(isr);
StringBuilder response = new StringBuilder();
String line = null;
try {
while ((line = reader.readLine()) != null) {
response.append(line);
}
} catch (IOException e) {
Log.*e*("ERROR CAUGHT", "Error in ConvertStreamToString", e);
} catch (Exception e) {
Log.*e*("ERROR CAUGHT", "Error in ConvertStreamToString", e);
} finally {
try {
stream.close();
} catch (IOException e) {
Log.*e*("ERROR CAUGHT", "Error in ConvertStreamToString", e);
} catch (Exception e) {
Log.*e*("ERROR CAUGHT", "Error in ConvertStreamToString", e);
}
}
return response.toString();
}
DisplayMessage
: 这个函数更新文本框内容,显示响应,即预测值:
public void DisplayMessage(String a)
{
value.setText(a);
}
需要注意的是,在 Android 上进行网络调用时,最佳实践是将其放在单独的线程中,以避免阻塞主用户界面(UI)线程。因此,我们将编写一个名为MakeNetworkCall
的内部类来实现这一点:
private class MakeNetworkCall extends AsyncTask<String, Void, String> {
@Override
protected void onPreExecute() {
super.onPreExecute();
DisplayMessage("Please Wait ...");
}
@Override
protected String doInBackground(String... arg) {
InputStream is = null;
String URL = "http://10.0.2.2:5000/predict";
Log.*d*("ERROR CAUGHT", "URL: " + URL);
String res = "";
is = ByPostMethod(URL);
if (is != null) {
res = ConvertStreamToString(is);
} else {
res = "Something went wrong";
}
return res;
}
protected void onPostExecute(String result) {
super.onPostExecute(result);
DisplayMessage(result);
Log.*d*("COMPLETED", "Result: " + result);
}
}
请注意,我们使用了http://10.0.2.2:5000/predict
而不是http://127.0.0.1:5000/predict
。这是因为在 Android 中,当我们使用模拟器时,它通过10.0.2.2
访问本地主机,而不是127.0.0.1
。由于示例是在模拟器中运行的,因此我们使用了10.0.2.2
。
最后,我们需要添加一个功能,当按钮被点击时调用 API。因此,在oncreate()
方法中,在按钮初始化后插入以下代码。这将启动一个后台线程,在按钮点击时访问 API:
estimate.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.*i*("CHECK", "CLICKED");
new MakeNetworkCall().execute("http://10.0.2.2:5000/predict", "Post");
}
});
我们需要在AndroidManifest.xml
文件中添加使用互联网的权限。将以下代码放入<manifest>
标签内:
<uses-permission android:name="android.permission.INTERNET"></uses-permission>
别忘了运行你的 Flask 应用。如果你还没有运行,请确保在激活的 conda 环境中运行它:
set FLASK_APP=predict_api
flask run
这就是在 Android 上运行和测试应用所需的所有代码。现在,在模拟器中运行应用,输入屏幕上的信息,点击 ESTIMATE VALUE 按钮,你将立即获得结果:
额外说明
这是一个演示,展示了如何在 Android 设备上使用构建的 AI 模型。话虽如此,仍有许多其他任务可以添加到现有的应用程序中:
-
改进 UI 设计
-
添加输入检查以验证输入的数据
-
托管 Flask 应用程序(Heroku、AWS 等)
-
发布应用程序
所有这些任务都与我们的核心 AI 主题无关,因此可以作为读者的练习来处理。
创建一个用于预测房价的 iOS 应用程序
在本节中,我们将通过 iOS 应用程序通过 RESTful API 来使用模型。本节的目的是展示如何通过 iOS 应用程序使用和消费模型。这里假设您已经熟悉 Swift 编程。相同的方法可以用于任何类似的用例。以下是本节所涵盖的步骤:
-
下载并安装 Xcode
-
创建一个单屏幕的 iOS 项目
-
设计屏幕的布局
-
添加接受输入的功能
-
添加功能以消费提供模型的 RESTful API
-
附加说明
下载并安装 Xcode
您需要一台 Mac(macOS 10.11.5 或更高版本)来开发本书中实现的 iOS 应用程序。此外,需要安装 Xcode 的最新版本才能运行这些代码,因为它包含了设计、开发和调试任何应用所必需的所有功能。
要下载最新版本的 Xcode,请按以下步骤操作:
-
打开 Mac 上的 App Store(默认在 Dock 中)。
-
在搜索框中输入
Xcode
,它位于右上角。然后按下回车键。 -
搜索结果中的第一个就是 Xcode 应用程序。
-
点击“获取”,然后点击“安装应用程序”。
-
输入您的 Apple ID 和密码。
-
Xcode 将被下载到您的
/Applications
目录中。
创建一个单屏幕的 iOS 项目
Xcode 包含多个内置应用程序模板。我们将从一个基本模板开始:单视图应用程序。从/Applications
目录中打开 Xcode 以创建一个新项目。
如果您第一次启动 Xcode,它可能会要求您同意所有用户协议。按照这些提示操作,直到 Xcode 安装并准备好在您的系统上启动。
启动 Xcode 后,以下窗口将会出现:
点击“创建一个新的 Xcode 项目”。将会打开一个新窗口,显示一个对话框,允许我们选择所需的模板:
选择模板后,会弹出一个对话框。在这里,您需要为您的应用程序命名,您可以使用以下值。您还可以选择一些附加选项来配置您的项目:
-
产品名称:Xcode 将使用您输入的产品名称来命名项目和应用程序。
-
团队:如果没有填写值,请将团队设置为“无”。
-
组织名称:这是一个可选字段。您可以输入您的组织名称或您的名字。您也可以选择将此选项留空。
-
组织标识符:如果您有组织标识符,请使用该值。如果没有,请使用
com.example
。 -
捆绑标识符:此值是根据您的产品名称和组织标识符自动生成的。
-
语言:Swift。
-
设备:通用。一个在 iPhone 和 iPad 上都能运行的应用程序被认为是通用应用程序。
-
使用核心数据:我们不需要核心数据。因此,它保持未选中。
-
包含单元测试:我们需要包含单元测试。因此,这个选项将被选中。
-
包含 UI 测试:我们不需要包含任何 UI 测试。因此,这个选项保持未选中。
现在,点击下一步。一个对话框将出现,您需要选择一个位置来保存您的项目。保存项目后,点击创建。您的新项目将由 Xcode 在工作区窗口中打开。
设计屏幕的布局
让我们设计一个屏幕,用于接收我们创建的模型因子的输入。该屏幕将有七个输入框用于接收因子,一个按钮,以及一个输出文本框来显示预测结果:
让我们来处理应用所需的故事板。什么是故事板?故事板展示了内容的屏幕及其之间的过渡。它为我们提供了应用程序 UI 的视觉表现。我们可以使用所见即所得(WYSIWYG)编辑器,在这里我们可以实时看到更改。
要打开故事板,请在项目导航器中选择Main.storyboard
选项。这将打开一个画布,我们可以在其中设计屏幕。现在我们可以添加元素并设计画布:
也可以使用编码来代替拖放方法。为此,从定义作为输入使用的文本字段开始,放在ViewController
类中:
@interface ViewController ()<UITextFieldDelegate>
{
UITextField* bizropfeild,*roomsfeild,*agefeild,*highwaysfeild,*taxfeild,*ptratiofeild,*lstatfeild;
}
@end
然后,在CreateView
方法中,我们实现每个文本字段的设计。以下是前两个文本字段的示例;其余文本字段可以采用相同的方法。完成的项目代码可以在chapter2_ios_prediction
文件夹中找到。
首先,创建一个标题文本字段,估算房地产价值
:
UILabel *headerLabel = [[UILabel alloc]initWithFrame:CGRectMake(10, 20, self.view.frame.size.width-20, 25)];
headerLabel.font = [UIFont fontWithName:@"SnellRoundhand-Black" size:20]; //custom font
headerLabel.backgroundColor = [UIColor clearColor];
headerLabel.textColor = [UIColor blackColor];
headerLabel.textAlignment = NSTextAlignmentCenter;
headerLabel.text=@"Estimate the value of real estate";
[self.view addSubview:headerLabel];
UIView *sepratorLine =[[UIView alloc]initWithFrame:CGRectMake(0, 50, self.view.frame.size.width, 5)];
sepratorLine.backgroundColor=[UIColor blackColor];
[self.view addSubview:sepratorLine];
接下来,创建另一个文本字段,输入房地产详细信息
:
UILabel *detailLabel = [[UILabel alloc]initWithFrame:CGRectMake(10, 55, self.view.frame.size.width-20, 25)];
detailLabel.font = [UIFont fontWithName:@"SnellRoundhand-Black" size:18]; //custom font
detailLabel.backgroundColor = [UIColor clearColor];
detailLabel.textColor = [UIColor blackColor];
detailLabel.textAlignment = NSTextAlignmentLeft;
detailLabel.text=@"Enter real estate details";
[self.view addSubview:detailLabel];
接下来,创建一个字段,用于输入非零售业务面积比例:
UILabel *bizropLabel = [[UILabel alloc]initWithFrame:CGRectMake(5, 85, self.view.frame.size.width-150, 35)];
bizropLabel.font = [UIFont fontWithName:@"TimesNewRomanPSMT" size:12]; //custom font
bizropLabel.backgroundColor = [UIColor clearColor];
bizropLabel.numberOfLines=2;
bizropLabel.textColor = [UIColor blackColor];
bizropLabel.textAlignment = NSTextAlignmentLeft;
bizropLabel.text=@"Bizrope, The proportion of non-retail business acres per town";
[self.view addSubview:bizropLabel];
bizropfeild = [[UITextField alloc] initWithFrame:CGRectMake(self.view.frame.size.width-140, 85, 130, 35)];
bizropfeild.delegate=self;
bizropfeild.layer.borderColor=[UIColor blackColor].CGColor;
bizropfeild.layer.borderWidth=1.0;
[self.view addSubview:bizropfeild];
现在创建一个字段,用于输入每个住宅的平均房间数:
UILabel *roomsLabel = [[UILabel alloc]initWithFrame:CGRectMake(5, 125, self.view.frame.size.width-150, 35)];
roomsLabel.font = [UIFont fontWithName:@"TimesNewRomanPSMT" size:12]; //custom font
roomsLabel.backgroundColor = [UIColor clearColor];
roomsLabel.numberOfLines=2;
roomsLabel.textColor = [UIColor blackColor];
roomsLabel.textAlignment = NSTextAlignmentLeft;
roomsLabel.text=@"ROOMS, the average number of rooms per dwelling";
[self.view addSubview:roomsLabel];
roomsfeild = [[UITextField alloc] initWithFrame:CGRectMake(self.view.frame.size.width-140, 125, 130, 35)];
roomsfeild.delegate=self;
roomsfeild.layer.borderColor=[UIColor blackColor].CGColor;
roomsfeild.layer.borderWidth=1.0;
[self.view addSubview:roomsfeild];
然后,创建一个按钮来调用 API:
UIButton *estimateButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[estimateButton addTarget:self action:@selector(estimateAction)
forControlEvents:UIControlEventTouchUpInside];
estimateButton.layer.borderColor=[UIColor blackColor].CGColor;
estimateButton.layer.borderWidth=1.0;
[estimateButton setTitle:@"Estimate" forState:UIControlStateNormal];
[estimateButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
estimateButton.frame = CGRectMake(self.view.frame.size.width/2-80, 375, 160.0, 40.0);
[self.view addSubview:estimateButton];
添加接受输入的功能
在这里,所有来自文本字段的输入都被打包成一个NSString
对象,并用于请求的POST
正文中:
NSString *userUpdate =[NSString stringWithFormat:@"bizprop=%@&rooms=%@&age=%@&highways=%@&tax=%@&ptratio=%@&lstat=%@",bizropfeild.text,roomsfeild.text,agefeild.text,highwaysfeild.text,taxfeild.text,ptratiofeild.text,lstatfeild.text];
添加一个功能来调用提供模型的 RESTful API
现在我们需要使用NSURLSession
对象,通过活动屏幕中的输入来调用 RESTful API:
//create the Method "GET" or "POST"
[urlRequest setHTTPMethod:@"POST"];
//Convert the String to Data
NSData *data1 = [userUpdate dataUsingEncoding:NSUTF8StringEncoding];
//Apply the data to the body
[urlRequest setHTTPBody:data1];
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:urlRequest completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { }
最后,显示从 API 接收到的响应中的输出:
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
if(httpResponse.statusCode == 200)
{
NSError *parseError = nil;
NSDictionary *responseDictionary = [NSJSONSerialization JSONObjectWithData:data options:0 error:&parseError];
NSLog(@"The response is - %@",responseDictionary);
UILabel *outputLabel = [[UILabel alloc]initWithFrame:CGRectMake(5, 325, self.view.frame.size.width-150, 35)];
outputLabel.font = [UIFont fontWithName:@"TimesNewRomanPSMT" size:12]; //custom font
outputLabel.backgroundColor = [UIColor clearColor];
outputLabel.numberOfLines=2;
outputLabel.textColor = [UIColor blackColor];
outputLabel.textAlignment = NSTextAlignmentLeft;
outputLabel.text = [responseDictionary valueForKey:@""];
[self.view addSubview:outputLabel];
}
该应用现在可以运行,并准备在模拟器上进行测试。
附加说明
这展示了我们如何在 iOS 设备上使用 AI 模型。话虽如此,现有应用中还有很多可以添加的任务:
-
改进 UI 设计
-
添加输入检查以验证输入的数据
-
托管 Flask 应用(Heroku、AWS 等)
-
发布应用
所有这些任务与我们的核心 AI 主题无关,因此可以作为读者的练习来处理。安卓和 iOS 应用的完整代码和项目文件分别命名为chapter2_android_prediction
和chapter2_ios_prediction
。
总结
在本章中,我们探索了基本的顺序网络,并在移动设备上使用了它。在下一章,我们将探讨一种特殊类型的网络——卷积神经网络(CNN)。CNN 是最常用于机器视觉的网络类型。下一章的目标是熟悉机器视觉,并构建我们自己的定制 CNN。
第三章:实现深度神经网络架构来识别手写数字
在前面的章节中,我们已经了解了必要的概念,并设置了启动我们人工智能(AI)之旅所需的工具。我们还构建了一个小型预测应用程序,以便熟悉我们将要使用的工具。
在本章中,我们将讨论 AI 的一个更有趣且更受欢迎的应用——计算机视觉,或称机器视觉。我们将从上一章继续,逐步过渡到构建卷积神经网络(CNN),这是计算机视觉中最流行的神经网络类型。本章还将涵盖第一章中承诺的人工智能概念和基础内容,但与之不同的是,本章将采取非常实践的方式。
本章将涵盖以下主题:
-
构建一个前馈神经网络来识别手写数字
-
神经网络的其余概念
-
构建一个更深层的神经网络
-
计算机视觉简介
构建一个前馈神经网络来识别手写数字,版本一
在这一节中,我们将运用前两章所学的知识来解决一个包含非结构化数据的问题——图像分类。我们的思路是,通过当前的设置和我们熟悉的神经网络基础,深入解决计算机视觉任务。我们已经看到,前馈神经网络可以用于使用结构化数据进行预测;接下来,我们就用它来分类手写数字图像。
为了解决这个任务,我们将利用MNSIT数据库,并使用手写数字数据集。MNSIT 代表的是修改后的国家标准与技术研究院(Modified National Institute of Standards and Technology)。这是一个大型数据库,通常用于训练、测试和基准化与计算机视觉相关的图像任务。
MNSIT 数字数据集包含 60,000 张手写数字图像,用于训练模型,还有 10,000 张手写数字图像,用于测试模型。
从现在开始,我们将使用 Jupyter Notebook 来理解和执行这项任务。所以,如果你还没有启动 Jupyter Notebook,请启动它并创建一个新的 Python Notebook。
一旦你的 Notebook 准备好,第一件要做的,和往常一样,是导入所有必需的模块:
- 导入
numpy
并设置seed
以确保结果可复现:
import numpy as np np.random.seed(42)
- 加载 Keras 依赖项和内置的 MNSIT 数字数据集:
import keras from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense
from keras.optimizers import SGD
- 将数据分别加载到训练集和测试集中:
(X_train, y_train), (X_test, y_test)= mnist.load_data()
- 检查训练图像的数量以及每张图像的大小。在这个案例中,每张图像的大小是 28 x 28 像素:
X_train.shape
(60000, 28, 28)
- 检查因变量,在这种情况下,包含 60,000 个带有正确标签的案例:
y_train.shape
(60000,)
- 检查前 100 个训练样本的标签:
y_train [0 :99]
array([5, 0, 4, 1, 9, 2, 1, 3, 1, 4, 3, 5, 3, 6, 1, 7, 2, 8, 6, 9, 4, 0, 9,
1, 1, 2, 4, 3, 2, 7, 3, 8, 6, 9, 0, 5, 6, 0, 7, 6, 1, 8, 7, 9, 3, 9,
8, 5, 9, 3, 3, 0, 7, 4, 9, 8, 0, 9, 4, 1, 4, 4, 6, 0, 4, 5, 6, 1, 0,
0, 1, 7, 1, 6, 3, 0, 2, 1, 1, 7, 9, 0, 2, 6, 7, 8, 3, 9, 0, 4, 6, 7,
4, 6, 8, 0, 7, 8, 3], dtype=uint8)
- 检查测试图像的数量以及每张图像的大小。在本例中,每张图像的大小是 28 x 28 像素:
X_test.shape
(10000, 28, 28)
- 检查测试数据中的样本,这些基本上是 28 x 28 大小的二维数组:
X_test[0]
array([[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
.
.
,
0, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 121, 254, 207,
18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0]], dtype=uint8)
- 检查因变量,在本例中是 10,000 个带有正确标签的案例:
y_test.shape
(10000,)
- 测试集中第一个样本的正确标签如下:
y_test[0]
7
- 现在,我们需要对数据进行预处理,将其从 28 x 28 的二维数组转换为归一化的 784 个元素的一维数组:
X_train = X_train.reshape(60000, 784).astype('float32')
X_test = X_test.reshape(10000, 784).astype('float32')
X_train/=255
X_test /=255
- 检查预处理数据集的第一个样本:
X_test[0]
array([ 0\. , 0\. , 0\. , 0\. , 0\. ,
0\. , 0\. , 0\. , 0\. , 0\. ,
.
.
.
0\. , 0\. , 0\. , 0\. , 0\. ,
0.47450981, 0.99607843, 0.99607843, 0.85882354, 0.15686275,
0\. , 0\. , 0\. , 0\. , 0\. ,
0\. , 0\. , 0\. , 0\. , 0\. ,
0\. , 0\. , 0\. , 0\. , 0\. ,
0\. , 0\. , 0\. , 0\. , 0\. ,
0\. , 0\. , 0\. , 0.47450981, 0.99607843,
0.81176472, 0.07058824, 0\. , 0\. , 0\. ,
0\. , 0\. , 0\. , 0\. , 0\. ,
0\. , 0\. , 0\. , 0\. , 0\. ,
0\. , 0\. , 0\. , 0\. , 0\. ,
0\. , 0\. , 0\. , 0\. , 0\. ,
0\. , 0\. , 0\. , 0\. ], dtype=float32)
- 下一步是对标签进行一热编码;换句话说,我们需要将标签(从零到九)的数据类型从数字转换为类别型:
n_classes=10
y_train= keras.utils.to_categorical(y_train ,n_classes)
y_test= keras.utils.to_categorical(y_test,n_classes)
- 查看已经进行过一热编码的标签的第一个样本。在这种情况下,数字是七:
y_test[0]
array([ 0., 0., 0., 0., 0., 0., 0., 1., 0., 0.])
- 现在,我们需要设计一个简单的前馈神经网络,输入层使用
sigmoid
激活函数和 64 个神经元。我们将在输出层添加一个softmax
函数,通过给出分类标签的概率来进行分类:
model=Sequential()
model.add(Dense(64,activation='sigmoid', input_shape=(784,)))
model.add(Dense(10,activation='softmax'))
- 我们可以通过
summary()
函数查看我们刚刚设计的神经网络的结构,这是一个简单的网络,具有 64 个神经元的输入层和 10 个神经元的输出层。输出层有 10 个神经元,我们有 10 个分类标签需要预测/分类(从零到九):
model.summary()
_______________________________________________________________
Layer (type) Output Shape Param #
=================================================================
dense_1 (Dense) (None, 64) 50240
_______________________________________________________________
dense_2 (Dense) (None, 10) 650
=================================================================
Total params: 50,890
Trainable params: 50,890
Non-trainable params: 0
_________________________________________________________________
- 接下来,我们需要配置模型,以便使用优化器、损失函数和度量标准来判断准确性。在这里,使用的优化器是标量梯度下降法(SGD),学习率为 0.01。使用的损失函数是代数均方误差(MSE),用于衡量模型正确性的度量标准是
accuracy
,即概率分数:
model.compile(loss='mean_squared_error', optimizer=SGD(lr=0.01),metrics['accuracy'])
- 现在,我们准备好训练模型了。我们希望它每次迭代使用 128 个样本,通过网络进行学习,这由
batch_size
指示。我们希望每个样本在整个网络中至少迭代 200 次,这由epochs
指示。同时,我们指定了用于训练和验证的数据集。Verbose
控制控制台上的输出打印:
model.fit(X_train,y_train,batch_size=128,epochs=200,
verbose=1,validation_data =(X_test,y_test))
- 在 60,000 个样本上进行训练,然后在 10,000 个样本上进行验证:
Epoch 1/200
60000/60000 [==============================] - 1s - loss: 0.0915 - acc: 0.0895 - val_loss: 0.0911 - val_acc: 0.0955
Epoch 2/200
.
.
.
60000/60000 [==============================] - 1s - loss: 0.0908 - acc:
0.8579 - val_loss: 0.0274 - val_acc: 0.8649
Epoch 199/200
60000/60000 [==============================] - 1s - loss: 0.0283 - acc: 0.8585 - val_loss: 0.0273 - val_acc: 0.8656
Epoch 200/200
60000/60000 [==============================] - 1s - loss: 0.0282 - acc: 0.8587 - val_loss: 0.0272 - val_acc: 0.8658
<keras.callbacks.History at 0x7f308e68be48>
- 最后,我们可以评估模型以及模型在测试数据集上的预测效果:
model.evaluate(X_test,y_test)
9472/10000 [===========================>..] - ETA: 0s
[0.027176343995332718, 0.86580000000000001]
这可以解释为错误率(MSE)为 0.027,准确率为 0.865,这意味着它在测试数据集上预测正确标签的次数占 86%。
构建一个前馈神经网络来识别手写数字,第二版
在上一部分中,我们构建了一个非常简单的神经网络,只有输入层和输出层。这个简单的神经网络给了我们 86%的准确率。让我们看看通过构建一个比之前版本更深的神经网络,是否能进一步提高这个准确率:
- 我们将在一个新的笔记本中进行这项工作。加载数据集和数据预处理将与上一部分相同:
import numpy as np np.random.seed(42)
import keras from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense
from keras.optimizers import SG
#loading and pre-processing data
(X_train,y_train), (X_test,y_test)= mnist.load_data()
X_train= X_train.reshape( 60000, 784). astype('float32')
X_test =X_test.reshape(10000,784).astype('float32')
X_train/=255
X_test/=255
- 神经网络的设计与之前版本稍有不同。我们将在网络中加入一个包含 64 个神经元的隐藏层,以及输入层和输出层:
model=Sequential()
model.add(Dense(64,activation='relu', input_shape=(784,)))
model.add(Dense(64,activation='relu'))
model.add(Dense(10,activation='softmax'))
-
同时,我们将为输入层和隐藏层使用
relu
激活函数,而不是之前使用的sigmoid
函数。 -
我们可以如下检查模型设计和架构:
model.summary()
_______________________________________________________________
Layer (type) Output Shape Param #
=================================================================
dense_1 (Dense) (None, 64) 50240
_______________________________________________________________
dense_2 (Dense) (None, 64) 4160
_______________________________________________________________
dense_3 (Dense) (None, 10) 650
=================================================================
Total params: 55,050
Trainable params: 55,050
Non-trainable params: 0
_________________________________________________________________
- 接下来,我们将配置模型,使用派生的
categorical_crossentropy
代价函数,而不是我们之前使用的 MSE。同时,将学习率从 0.01 提高到 0.1:
model.compile(loss='categorical_crossentropy',optimizer=SGD(lr=0.1),
metrics =['accuracy'])
- 现在,我们将像之前的例子一样训练模型:
model.fit(X_train,y_train,batch_size=128,epochs=200,verbose=1,validation_data =(X_test,y_test))
- 在 60,000 个样本上训练,并在 10,000 个样本上验证:
Epoch 1/200
60000/60000 [==============================] - 1s - loss: 0.4785 - acc: 0.8642 - val_loss: 0.2507 - val_acc: 0.9255
Epoch 2/200
60000/60000 [==============================] - 1s - loss: 0.2245 - acc: 0.9354 - val_loss: 0.1930 - val_acc: 0.9436
.
.
.
60000/60000 [==============================] - 1s - loss: 4.8932e-04 - acc: 1.0000 - val_loss: 0.1241 - val_acc: 0.9774
<keras.callbacks.History at 0x7f3096adadd8>
如你所见,和我们在第一版中构建的模型相比,准确率有所提高。
构建更深的神经网络
在本节中,我们将使用本章所学的概念,构建一个更深的神经网络来分类手写数字:
- 我们将从一个新的笔记本开始,然后加载所需的依赖:
import numpy as np np.random.seed(42)
import keras
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Dropout
*# new!*
from
keras.layers.normalization *# new!*
import
BatchNormalization
*# new!*
from keras import regularizers
*# new!*
from keras.optimizers
import SGD
- 我们现在将加载并预处理数据:
(X_train,y_train),(X_test,y_test)= mnist.load_data()
X_train= X_train.reshape(60000,784).
astype('float32')
X_test= X_test.reshape(10000,784).astype('float32')
X_train/=255
X_test/=255
n_classes=10
y_train=keras.utils.to_categorical(y_train,n_classes)
y_test =keras.utils.to_categorical(y_test,n_classes)
- 现在,我们将设计一个更深的神经网络架构,并采取措施防止过拟合,以提供更好的泛化能力:
model=Sequential()
model.add(Dense(64,activation='relu',input_shape=(784,)))
model.add(BatchNormalization())
model.add(Dropout(0.5))
model.add(Dense(64,activation='relu'))
model.add(BatchNormalization())
model.add(Dropout(0.5))
model.add(Dense(10,activation='softmax'))
model.summary()
_______________________________________________________________
Layer (type) Output Shape Param #
=================================================================
dense_1 (Dense) (None, 64) 50240
_______________________________________________________________
batch_normalization_1 (Batch (None, 64) 256
_______________________________________________________________
dropout_1 (Dropout) (None, 64) 0
_______________________________________________________________
dense_2 (Dense) (None, 64) 4160
_______________________________________________________________
batch_normalization_2 (Batch (None, 64) 256
_______________________________________________________________
dropout_2 (Dropout) (None, 64) 0
_______________________________________________________________
dense_3 (Dense) (None, 10) 650
=================================================================
Total params: 55,562
Trainable params: 55,306
Non-trainable params: 256_______________________________________________________________
- 这次,我们将使用
adam
优化器来配置模型:
model.compile(loss='categorical_crossentropy',optimizer='adam',metrics=['accuracy'])
- 现在,我们将设定训练模型
200
个周期,批量大小为128
:
model.fit(X_train, y_train, batch_size= 128, epochs= 200, verbose= 1, validation_data= (X_test,y_test))
- 在 60,000 个样本上训练,并在 10,000 个样本上验证:
Epoch 1/200
60000/60000 [==============================] - 3s - loss: 0.8586 - acc: 0.7308 - val_loss: 0.2594 - val_acc: 0.9230
Epoch 2/200
60000/60000 [==============================] - 2s - loss: 0.4370 - acc: 0.8721 - val_loss: 0.2086 - val_acc: 0.9363
.
.
.
Epoch 200/200
60000/60000 [==============================] - 2s - loss: 0.1323 - acc: 0.9589 - val_loss: 0.1136 - val_acc: 0.9690
<keras.callbacks.History at 0x7f321175a748>
计算机视觉简介
计算机视觉可以定义为人工智能的一个子集,我们可以教计算机“看”。我们不能仅仅给机器添加一个相机让它“看”。为了让机器像人类或动物一样真正感知世界,它依赖于计算机视觉和图像识别技术。阅读条形码和人脸识别就是计算机视觉的应用实例。计算机视觉可以描述为人类大脑中处理眼睛感知信息的部分,别无其他。
图像识别是计算机视觉在人工智能领域中一个有趣的应用。从机器通过计算机视觉接收的输入由图像识别系统解读,依据其所见,输出会被分类。
换句话说,我们用眼睛捕捉周围的物体,这些物体/图像在大脑中被处理,使我们能够直观地感知周围的世界。计算机视觉赋予机器这种能力。计算机视觉负责从输入的视频或图像中自动提取、分析并理解所需的信息。
计算机视觉有多种应用,主要用于以下场景:
-
增强现实
-
机器人技术
-
生物特征识别
-
污染监测
-
农业
-
医学图像分析
-
法医
-
地球科学
-
自动驾驶汽车
-
图像恢复
-
流程控制
-
字符识别
-
遥感
-
手势分析
-
安全与监控
-
人脸识别
-
交通
-
零售
-
工业质量检测
计算机视觉的机器学习
使用适当的机器学习理论和工具非常重要,这对于我们开发涉及图像分类、物体检测等各种应用将非常有帮助。利用这些理论创建计算机视觉应用需要理解一些基本的机器学习概念。
计算机视觉领域的会议
一些值得关注的会议,了解最新的研究成果和应用,如下所示:
-
计算机视觉与模式识别会议(CVPR)每年举行,是最受欢迎的会议之一,涵盖从理论到应用的研究论文,跨越广泛的领域。
-
国际计算机视觉大会(ICCV)是每两年举行一次的另一大会议,吸引着一些最优秀的研究论文。
-
计算机图形学特别兴趣小组(SIGGRAPH)和交互技术,虽然更多集中在计算机图形学领域,但也有几篇应用计算机视觉技术的论文。
其他值得注意的会议包括神经信息处理系统(NIPS)、国际机器学习大会(ICML)、亚洲计算机视觉大会(ACCV)、欧洲计算机视觉大会(ECCV)等。
总结
本章中,我们构建了一个前馈神经网络,识别手写数字,并分为两个版本。然后,我们构建了一个神经网络,用于分类手写数字,最后简要介绍了计算机视觉。
在下一章中,我们将构建一个机器视觉移动应用程序,用于分类花卉品种并检索相关信息。
深入阅读
若要深入了解计算机视觉,请参考以下 Packt 出版的书籍:
-
计算机视觉中的深度学习 由 Rajalingappaa Shanmugamani 编写
-
实用计算机视觉 由 Abhinav Dadhich 编写
第四章:创建一个机器视觉移动应用程序来分类花的种类
在这一章中,我们将利用我们在前几章中学到的理论知识来创建一个可以分类特定花种的移动应用程序。通过使用您的移动摄像头对着花拍照,该应用程序将分析图像并尽力猜测出那种花的种类。这是我们把对卷积神经网络(CNN)的理解付诸实践的地方。我们还将学习更多关于使用 TensorFlow 以及一些工具如 TensorBoard 的内容。但在我们深入研究之前,让我们先谈谈一些事情。
在本章中,我们使用一些可能不为所有人熟悉的术语,因此让我们确保我们对它们的含义有一致的理解。
在本章中,我们将涵盖以下主题:
-
CoreML 与 TensorFlow Lite 的对比
-
什么是 MobileNet
-
用于图像分类的数据集
-
创建您自己的图像数据集
-
使用 TensorFlow 构建模型
-
运行 TensorBoard
CoreML 与 TensorFlow Lite 的对比
在机器学习领域,有两个努力(截至撰写本文时)正在进行,旨在改善移动 AI 体验。而不是将 AI 或 ML 处理转移到云端和数据中心,更快的选择是在设备本身上处理数据。为了做到这一点,模型必须已经预先训练好,这意味着它可能并不完全训练用于您要使用的目的。
在这个领域,苹果的努力(iOS)称为Core ML,而谷歌的(Android)称为TensorFlow Lite。让我们简要讨论一下两者。
CoreML
Apple 的 CoreML 框架提供了大量的神经网络类型。这使得开发人员可以在开发应用程序时尝试不同的设计。摄像头和麦克风数据只是可以用于诸如图像识别、自然语言处理等领域的两个可以利用的区域。有几个预训练模型开发人员可以直接使用,并根据需要进行调整。
TensorFlow Lite
TensorFlow Lite 是 TensorFlow 的本地设备版本,意味着它设计用于在您的移动设备上运行。截至撰写本文时,它仍处于预发布状态,因此很难与 CoreML 进行直接比较。我们需要等待并看看最终提供的功能。目前,只需知道在移动设备上有两个选项可供选择的本地 AI 和机器学习。
什么是 MobileNet?
在深入之前,我们先来谈谈你在本章中会经常听到的一个术语——MobileNets。你可能会问,什么是 MobileNet?简而言之,它是一种专门为移动设备和嵌入式视觉应用设计的架构。在这些设备上,处理这类任务的计算能力有限,因此迫切需要一种比桌面环境中使用的解决方案更好的方法。
MobileNet架构由 Google 提出,简要来说:
-
使用深度可分离卷积。与使用普通卷积的神经网络相比,这显著减少了参数的数量,结果就是所谓的轻量级深度神经网络。
-
深度卷积,随后是点卷积,替代了正常的卷积过程。
为了简化问题,我们将本章分为以下两个部分:
-
图像分类数据集:在这一节中,我们将探索可用于图像分类的各种数据集(所有这些数据集都可以在线获得)。我们还将讨论如何在必要时创建我们自己的数据集。
-
使用 TensorFlow 构建模型:在这一节中,我们将使用 TensorFlow 来训练我们的分类模型。我们通过使用一个名为MobileNet的预训练模型来实现这一点。MobileNets 是一系列为 TensorFlow 设计的移动优先计算机视觉模型,旨在在考虑设备上有限资源的情况下最大化准确性。
-
此外,我们将研究如何将输出模型转换为
.tflite
格式,该格式可用于其他移动或嵌入式设备。TFLite 代表 TensorFlow Lite。你可以通过任何互联网搜索引擎了解更多关于 TensorFlow Lite 的信息。
图像分类数据集
对于我们的花卉分类示例,我们将使用牛津大学的视觉几何组(VGG)图像数据集。该数据集可以通过以下链接访问:www.robots.ox.ac.uk/~vgg/data/
。
VGG 是曾在以往的 ImageNet 竞赛中获胜的部门。像 VGG14 和 VGG16 这样的预训练模型是由该部门构建的,它们分别在 2014 年和 2016 年获得了胜利。这些数据集被 VGG 用于训练和评估他们所构建的模型。
花卉数据集可以在页面的精细识别数据集部分找到,此外还有纹理和宠物数据集。点击“Flower Category Datasets”,或使用以下链接访问 VGG 的花卉数据集,www.robots.ox.ac.uk/~vgg/data/flowers/
。
在这里,你可以找到两个数据集,一个包含 17 种不同的花卉,另一个包含 102 种不同的花卉。你可以根据它们在教程中的易用性,或者根据你所能使用的处理方法选择其中的一个。
使用更大的数据集意味着训练时间会更长,训练前的数据处理时间也会更长;因此,我们建议你谨慎选择。
这里是你将在此处找到的图像子集。正如你所看到的,文件夹名称与我们在本章稍后会用到的完全一致:
除了我们上面提到的图像外,下面是一些额外的链接,若你将来需要类似分类用途的图像数据,可以使用它们:
-
CVonline 数据集:
homepages.inf.ed.ac.uk/rbf/CVonline/Imagedbase.htm
-
CVpapers 数据集:
www.cvpapers.com/datasets.html
-
深度学习数据集:
deeplearning.net/datasets/
-
COCO 数据集:
cocodataset.org/#home
-
ImageNet 数据集:
www.image-net.org/
-
Kaggle 数据集:
www.kaggle.com/datasets?sortBy=relevance&group=featured&search=image
使用 Google 图片创建你自己的图像数据集
假设因为某种原因,我们需要确定一张图片是什么狗,但电脑上没有现成的图片。我们该怎么办呢?或许最简单的方法是打开 Google Chrome 并在线搜索图片。
以 Doberman 犬为例,假设我们对 Doberman 犬感兴趣。只需打开 Google Chrome 并搜索doberman的图片,如下所示:
- 搜索 Doberman 犬的图片: 搜索后,得到以下结果:
- 打开 JavaScript 控制台: 你可以在 Chrome 的右上角菜单中找到 JavaScript 控制台:
点击“更多工具”,然后选择“开发者工具”:
确保选择“控制台”标签页,如下所示:
- 使用 JavaScript:继续向下滚动,直到你认为已经有足够的图像用于你的用例。完成后,返回到开发者工具中的 Console 标签,然后复制并粘贴以下脚本:
//the jquery is pulled down in the JavaScript console
var script = document.createElement('script');
script.src = "https://ajax.googleapis.com/ajax/libs/jquery/2.2.0/jquery.min.js";
document.getElementsByTagName('head')[0].appendChild(script);
//Let us get the URLs
var urls = $('.rg_di .rg_meta').map(function() { return JSON.parse($(this).text()).ou; });
// Now, we will write the URls one per line to file
var textToSave = urls.toArray().join('\n');
var hiddenElement = document.createElement('a');
hiddenElement.href = 'data:attachment/text,' + encodeURI(textToSave);
hiddenElement.target = '_blank';
hiddenElement.download = 'urls.txt';
hiddenElement.click();
这段代码会收集所有图像的 URL,并将它们保存到名为urls.txt
的文件中,该文件位于你的默认Downloads
目录。
- 使用 Python 下载图像:现在,我们将使用 Python 从
urls.txt
读取图像的 URL,并将所有图像下载到一个文件夹中:
这可以通过以下步骤轻松完成:
- 打开 Python 笔记本,复制并粘贴以下代码以下载图像:
# We will start by importing the required pacages
from imutils import paths
import argparse
import requests
import cv2
import os
- 导入后,开始构造参数,并且构造后解析参数非常重要:
ap = argparse.ArgumentParser()
ap.add_argument("-u", "--urls", required=True,
help="path to file containing image URLs")
ap.add_argument("-o", "--output", required=True,
help="path to output directory of images")
args = vars(ap.parse_args())
- 下一步包括从输入文件中获取 URL 列表,并计算下载的图像总数:
rows = open(args["urls"]).read().strip().split("\n")
total = 0
# URLs are looped in
for url in rows:
try:
# Try downloading the image
r = requests.get(url, timeout=60)
#The image is then saved to the disk
p = os.path.sep.join([args["output"], "{}.jpg".format(
str(total).zfill(8))])
f = open(p, "wb")
f.write(r.content)
f.close()
#The counter is updated
print("[INFO] downloaded: {}".format(p))
total += 1
- 在下载过程中,需要处理抛出的异常:
print("[INFO] error downloading {}...skipping".format(p))
- 下载的图像路径需要循环遍历:
for imagePath in paths.list_images(args["output"])
- 现在,决定图像是否应该被删除,并据此初始化:
delete = False
- 需要加载图像。让我们尝试执行此操作:
image = cv2.imread(imagePath)
- 如果我们未能正确加载图像,由于图像为
None
,则应该将其从磁盘中删除:
if image is None:
delete = True
- 此外,如果 OpenCV 无法加载图像,这意味着图像已损坏,应当删除该图像:
except:
print("Except")
delete = True
- 最后进行检查,查看图像是否已被删除:
if delete:
print("[INFO] deleting {}".format(imagePath))
os.remove(imagePath)
-
完成后,让我们将此笔记本下载为 Python 文件并命名为
image_download.py
。确保将urls.txt
文件放置在与你刚刚创建的 Python 文件相同的文件夹中。这一点非常重要。 -
接下来,我们需要执行刚刚创建的 Python 文件。我们将通过使用命令行来执行,如下所示(确保
path
变量指向你的 Python 位置):
Image_download.py --urls urls.txt --output Doberman
执行此命令后,图像将被下载到名为 Doberman 的文件夹中。完成后,你应该能看到所有在 Google Chrome 中查看到的杜宾犬图像,类似于以下所示的图像:
选择所需的文件夹以保存图像,如下所示:
就这样,我们现在拥有了一个充满杜宾犬图像的文件夹。相同的方法可以应用于创建任何其他类型类别的文件夹。
可能会有一些来自 Google 图像结果的图像是不需要的。确保浏览图像并移除任何不想要的图像。
从视频创建自定义数据集的替代方法
有时我们通过互联网找到的图像可能无法满足我们的需求,或者我们根本找不到任何图像。这可能是由于数据的独特性、当前的用例、版权限制、所需分辨率等原因造成的。在这种情况下,另一种方法是记录需要的物体的视频,提取符合要求的视频帧,并将每一帧保存为单独的图像。我们该如何操作呢?
假设我们有一种皮肤病,无法在网上找到相关信息。我们需要对这种皮肤病进行分类。然而,为了做到这一点,我们需要一张该皮肤病的图像。因此,我们可以拍摄这张皮肤病的录像,并将视频文件保存为一个文件。为了讨论的方便,我们假设我们将视频保存为文件名myvideo.mp4
。
完成后,我们可以使用以下 Python 脚本将视频分解为图像,并将其保存到一个文件夹中。此函数将接受视频文件的路径,根据频率将视频分解为帧,并将相应的图像保存到指定的输出位置。以下是该函数的完整代码:
import sys
import argparse
import os
import cv2
import numpy as np
print(cv2.__version__)
这个函数接受视频文件的路径,根据频率将视频分解为帧,并将相应的图像保存到指定的输出位置:
def extractImages(pathIn, pathOut):
count = 0
vidcap = cv2.VideoCapture(pathIn)
success,image = vidcap.read()
success = True
while success:
vidcap.set(cv2.CAP_PROP_POS_MSEC,(count*10)) # Adjust frequency of frames here
success,image = vidcap.read()
print ('Read a new frame: ', success)
#Once we identify the last frame, stop there
image_last = cv2.imread("frame{}.png".format(count-1))
if np.array_equal(image,image_last):
break
cv2.imwrite( os.path.join("frames","frame{:d}.jpg".format(count)), image) # save frame as JPEG file
count = count + 1
pathIn = "myvideo.mp4"
pathOut = ""
extractImages(pathIn, pathOut)
如上所述,这将在当前文件夹中根据设置的频率保存视频的每一帧。运行此脚本后,您将创建好您的图像数据集,并可以使用所需的图像。
使用 TensorFlow 构建模型
现在,我们已经了解了获取所需图像的几种方法,或者在没有图像的情况下创建我们自己的图像,接下来我们将使用 TensorFlow 为我们的花卉用例创建分类模型:
-
创建文件夹结构:首先,让我们为我们的花卉分类用例创建所需的文件夹结构。首先,创建一个名为
image_classification
的主文件夹。在image_classification
文件夹内,创建两个文件夹:images
和tf_files
。images
文件夹将包含模型训练所需的图像,而tf_files
文件夹将在运行时保存所有生成的 TensorFlow 特定文件。 -
下载图像:接下来,我们需要下载适用于我们用例的特定图像。以花卉为例,我们的图像将来自我们之前讨论过的 VGG 数据集页面。
请随意使用您自己的数据集,但请确保每个类别都有单独的文件夹。将下载的图像数据集放在images
文件夹内。
例如,完整的文件夹结构将如下所示:
- 创建 Python 脚本:在这一步,我们将创建构建模型所需的 TensorFlow 代码。在主
image_classification
文件夹中创建一个名为retrain.py
的 Python 文件。
完成这些后,以下代码块应被复制并使用。我们将过程分解为几个步骤,以便描述发生了什么:
- 以下代码块是完整的脚本内容,应该放入
retrain.py
中:
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import argparse
import collections
from datetime import datetime
import hashlib
import os.path
import random
import re
import sys
import tarfile
import numpy as np
from six.moves import urllib
import tensorflow as tf
from tensorflow.python.framework import graph_util
from tensorflow.python.framework import tensor_shape
from tensorflow.python.platform import gfile
from tensorflow.python.util import compat
FLAGS = None
MAX_NUM_IMAGES_PER_CLASS = 2 ** 27 - 1 # ~134M
- 接下来,我们需要准备图像,以便它们可以进行训练、验证和测试:
result = collections.OrderedDict()
sub_dirs = [
os.path.join(image_dir,item)
for item in gfile.ListDirectory(image_dir)]
sub_dirs = sorted(item for item in sub_dirs
if gfile.IsDirectory(item))
for sub_dir in sub_dirs:
我们要做的第一件事是从存储图像的目录路径中检索图像。我们将使用这些图像,通过您之前下载并安装的模型来创建模型图。
下一步是通过创建所谓的瓶颈文件来初始化瓶颈。瓶颈是一个非正式术语,用来指代最终输出层之前的那一层,该层负责实际的分类。(TensorFlow Hub 将其称为图像特征向量。)这一层经过训练,输出的值足够让分类器使用,以便区分它被要求识别的所有类别。这意味着它必须是图像的有意义且紧凑的总结,因为它必须包含足够的信息,让分类器能够在一小组值中做出正确的选择。
每个图像都需要有瓶颈值,这是非常重要的。如果每个图像的瓶颈值不可用,我们将不得不手动创建它们,因为这些值在未来训练图像时会被需要。强烈建议缓存这些值,以便以后加快处理速度。因为每个图像在训练过程中都会被多次重复使用,并且计算每个瓶颈值会花费大量时间,所以将这些瓶颈值缓存到磁盘上可以避免重复计算,从而加速过程。默认情况下,瓶颈值会存储在/tmp/bottleneck
目录中(除非作为参数指定了新的目录)。
当我们检索瓶颈值时,我们将基于缓存中存储的图像文件名来检索它们。如果对图像进行了扭曲处理,可能会在检索瓶颈值时遇到困难。启用扭曲的最大缺点是瓶颈缓存不再有用,因为输入图像永远不会被完全重复使用。这直接导致了训练过程时间的延长,因此强烈建议在对模型基本满意时再启用扭曲处理。如果您遇到问题,我们已经在本书的 GitHub 仓库中提供了一种方法来获取带有扭曲的图像的瓶颈值。
请注意,我们首先将扭曲的图像数据转化为 NumPy 数组。
接下来,我们需要对图像进行推理。这需要一个训练好的目标检测模型,并通过使用两个内存副本来完成。
我们的下一步是对图像进行失真处理。失真处理如裁剪、缩放和亮度是以百分比的形式给出的,这些百分比值控制每种失真在每个图像上应用的程度。合理的做法是从每种失真值 5 或 10 开始,然后通过实验确定哪些对模型有帮助,哪些没有。
接下来,我们需要基于准确性和损失来总结我们的模型。我们将使用 TensorBoard 可视化工具进行分析。如果你还不知道,TensorFlow 提供了一套名为 TensorBoard 的可视化工具,它可以帮助你可视化 TensorFlow 图,绘制执行过程中的变量,并展示其他数据,如通过图的图像。以下是一个 TensorBoard 仪表盘的示例:
我们的下一步是将模型保存到文件中,并设置一个目录路径,用于写入 TensorBoard 的摘要。
在这一点上,我们需要指出create_model_info
函数,它将返回模型信息。在下面的示例中,我们处理的是 MobileNet 和 Inception_v3 架构。稍后你将看到我们如何处理这些架构之外的其他架构:
def create_model_info(architecture):
architecture = architecture.lower()
if architecture == 'inception_v3':
# pylint: disable=line-too-long
data_url = 'http://download.tensorflow.org/models/image/imagenet/inception-2015-12-05.tgz'
# pylint: enable=line-too-long
bottleneck_tensor_name = 'pool_3/_reshape:0'
bottleneck_tensor_size = 2048
input_width = 299
input_height = 299
input_depth = 3
resized_input_tensor_name = 'Mul:0'
model_file_name = 'classify_image_graph_def.pb'
input_mean = 128
input_std = 128
elif architecture.startswith('mobilenet_'):
parts = architecture.split('_')
if len(parts) != 3 and len(parts) != 4:
tf.logging.error("Couldn't understand architecture name '%s'",
architecture)
return None
version_string = parts[1]
if (version_string != '1.0' and version_string != '0.75' and
version_string != '0.50' and version_string != '0.25'):
tf.logging.error(
""""The Mobilenet version should be '1.0', '0.75', '0.50', or '0.25',
but found '%s' for architecture '%s'""",
version_string, architecture)
return None
size_string = parts[2]
if (size_string != '224' and size_string != '192' and
size_string != '160' and size_string != '128'):
tf.logging.error(
"""The Mobilenet input size should be '224', '192', '160', or '128',
but found '%s' for architecture '%s'""",
size_string, architecture)
return None
if len(parts) == 3:
is_quantized = False
如果上述参数返回为 false,意味着我们遇到了一个意外的架构。如果发生这种情况,我们需要执行以下代码块来获取结果。在此示例中,我们处理的既不是 MobileNet 也不是 Inception_V3,默认将使用 MobileNet 的版本 1:
else:
if parts[3] != 'quantized':
tf.logging.error(
"Couldn't understand architecture suffix '%s' for '%s'", parts[3],
architecture)
return None
is_quantized = True
data_url = 'http://download.tensorflow.org/models/mobilenet_v1_'
data_url += version_string + '_' + size_string + '_frozen.tgz'
bottleneck_tensor_name = 'MobilenetV1/Predictions/Reshape:0'
bottleneck_tensor_size = 1001
input_width = int(size_string)
input_height = int(size_string)
input_depth = 3
resized_input_tensor_name = 'input:0'
if is_quantized:
model_base_name = 'quantized_graph.pb'
else:
model_base_name = 'frozen_graph.pb'
model_dir_name = 'mobilenet_v1_' + version_string + '_' + size_string
model_file_name = os.path.join(model_dir_name, model_base_name)
input_mean = 127.5
input_std = 127.5
else:
tf.logging.error("Couldn't understand architecture name '%s'", architecture)
raise ValueError('Unknown architecture', architecture)
return {
'data_url': data_url,
'bottleneck_tensor_name': bottleneck_tensor_name,
'bottleneck_tensor_size': bottleneck_tensor_size,
'input_width': input_width,
'input_height': input_height,
'input_depth': input_depth,
'resized_input_tensor_name': resized_input_tensor_name,
'model_file_name': model_file_name,
'input_mean': input_mean,
'input_std': input_std,
}
==============================================================
另一个重要的事项是,我们需要在处理后解码图像的 JPEG 数据。下面的add_jpeg_decoding
函数是一个完整的代码片段,通过调用tf.image.decode_jpeg
函数来实现这一功能:
def add_jpeg_decoding(input_width, input_height, input_depth, input_mean,
input_std):
jpeg_data = tf.placeholder(tf.string, name='DecodeJPGInput')
decoded_image = tf.image.decode_jpeg(jpeg_data, channels=input_depth)
decoded_image_as_float = tf.cast(decoded_image, dtype=tf.float32)
decoded_image_4d = tf.expand_dims(decoded_image_as_float, 0)
resize_shape = tf.stack([input_height, input_width])
resize_shape_as_int = tf.cast(resize_shape, dtype=tf.int32)
resized_image = tf.image.resize_bilinear(decoded_image_4d,
resize_shape_as_int)
offset_image = tf.subtract(resized_image, input_mean)
mul_image = tf.multiply(offset_image, 1.0 / input_std)
return jpeg_data, mul_image
这里是我们的main
函数,展示了它的全部内容。基本上,我们做了以下操作:
-
设置我们的日志级别为
INFO
-
准备文件系统以供使用
-
创建我们的模型信息
-
下载并提取我们的数据
def main(_):
tf.logging.set_verbosity(tf.logging.INFO)
prepare_file_system()
model_info = create_model_info(FLAGS.architecture)
if not model_info:
tf.logging.error('Did not recognize architecture flag')
return -1
maybe_download_and_extract(model_info['data_url'])
graph, bottleneck_tensor, resized_image_tensor = (
create_model_graph(model_info))
image_lists = create_image_lists(FLAGS.image_dir, FLAGS.testing_percentage,
FLAGS.validation_percentage)
上述retrain.py
文件可以作为本书附带资源进行下载。
运行 TensorBoard
要运行 TensorBoard,请使用以下命令:
tensorboard --logdir=path/to/log-directory
其中logdir
指向存储序列化数据的目录。如果该目录包含子目录,并且这些子目录也包含序列化数据,TensorBoard 将可视化所有这些运行的数据显示。一旦 TensorBoard 开始运行,请在浏览器中访问localhost:6006
来查看 TensorBoard 及其相关数据。
对于那些想要深入了解 TensorBoard 的读者,请查看以下教程:www.tensorflow.org/tensorboard/r1/summaries
。
总结
在这一章中,我们在这个小章节中完成了很多内容。我们首先理解了可用于图像分类的各种数据集,以及如果我们找不到符合要求的图像时,如何获取或创建图像。接着,我们将章节分为两个不同的部分。在第一部分,我们学习了如何创建我们自己的图像数据集。在第二部分,我们学习了如何使用 TensorFlow 构建模型。
在下一章,我们将通过使用各种 TensorFlow 库来进一步扩展我们的 TensorFlow 知识,构建一个机器学习模型,该模型将预测汽车的车身损伤。
第五章:使用 TensorFlow 构建一个预测汽车损坏的 ML 模型
在本章中,我们将建立一个系统,通过分析照片使用迁移学习来检测车辆的损坏程度。这样的解决方案将有助于降低保险索赔成本,并简化车主的流程。如果系统被正确实施,在理想的情况下,用户将上传一组损坏车辆的照片,照片将经过损伤评估,保险索赔将自动处理。
在实施这种用例的完美解决方案中涉及许多风险和挑战。首先,存在多种未知条件可能导致车辆损坏。我们不了解室外环境、周围物体、区域内的光线以及事故前车辆的质量。通过所有这些障碍并找出问题的共同解决方案是具有挑战性的。这是任何基于计算机视觉的场景中的常见问题。
在本章中,我们将涵盖以下主题:
-
迁移学习基础知识
-
图像数据集收集
-
设置一个 Web 应用程序
-
训练我们自己的 TensorFlow 模型
-
搭建一个消费模型的 Web 应用程序
迁移学习基础知识
为了实现汽车损坏预测系统,我们将基于 TensorFlow 构建我们自己的机器学习(ML)模型,用于车辆数据集。现代识别模型需要数百万个参数。我们需要大量时间和数据来从头开始训练新模型,以及数百个图形处理单元(GPUs)或张量处理单元(TPUs)运行数小时。
通过使用已经训练好的现有模型和在其上重新训练我们自己的分类器,迁移学习使这项任务变得更加容易。在我们的示例中,我们将使用MobileNet模型的特征提取能力。即使我们不能达到 100%的准确率,这在许多情况下仍然有效,特别是在手机上,我们没有重型资源的情况下。我们甚至可以在典型的笔记本电脑上轻松训练这个模型数小时,即使没有 GPU。该模型是在配备 2.6 GHz 英特尔 i5 处理器和 8 GB 内存的 MacBook Pro 上构建的。
在深度学习中,迁移学习是最流行的方法之一,其中一个为一项任务开发的模型被重用于另一个不同任务的模型上。在基于计算机视觉的任务或基于自然语言处理(NLP)的任务中,我们可以利用预训练模型作为第一步,前提是我们拥有非常有限的计算资源和时间。
在典型的基于计算机视觉的问题中,神经网络尝试在其初始级别层中检测边缘,在中间级别层中检测形状,并在最终级别层中检测更具体的特征。通过迁移学习,我们将使用初始和中间级别的层,并仅重新训练最终级别的层。
举个例子,如果我们有一个训练用于识别苹果的模型,我们可以将其重用于检测水瓶。在初始层中,模型已被训练识别物体,因此我们只需重新训练最后几层。这样,我们的模型就能学到如何区分水瓶与其他物体。这个过程可以通过下图看到:
通常,我们需要大量数据来训练我们的模型,但大多数时候我们没有足够的相关数据。这时迁移学习就派上用场了,它允许你用很少的数据来训练模型。
如果你之前的分类器是使用 TensorFlow 开发并训练的,你可以重复使用相同的模型,并重新训练其中一些层以适应新的分类器。这是完全可行的,但前提是从旧任务中学到的特征具有更通用的性质。例如,你不能将为文本分类器开发的模型直接用于图像分类任务。此外,两个模型的输入数据大小必须一致。如果大小不匹配,我们需要添加一个额外的预处理步骤来调整输入数据的大小。
迁移学习方法
让我们深入探讨迁移学习的不同方法。可能有不同的名称用于描述这些方法,但概念保持一致:
-
使用预训练模型:目前有很多预训练模型可以满足你基本的深度学习研究需求。在本书中,我们使用了很多预训练模型,并从中得出我们的结果。
-
训练一个可重用的模型:假设你想解决问题 A,但你没有足够的数据来实现目标。为了解决这个问题,我们有另一个问题 B,其中有足够的数据。在这种情况下,我们可以为问题 B 开发一个模型,并将该模型作为问题 A 的起点。是否需要重用所有层或仅重用某些层,取决于我们所解决问题的类型。
-
特征提取:通过深度学习,我们可以提取数据集的特征。大多数时候,特征是由开发者手工设计的。神经网络有能力学习哪些特征需要传递,哪些特征不需要传递。例如,我们只会使用初始层来检测特征的正确表示,而不会使用输出层,因为它可能过于特定于某个特定任务。我们将简单地将数据输入网络,并使用其中一个中间层作为输出层。
有了这个,我们将开始使用 TensorFlow 构建我们的模型。
构建 TensorFlow 模型
构建你自己的自定义模型需要遵循一个逐步的过程。首先,我们将使用 TensorFlow Hub 来通过预训练模型输入图像。
要了解更多关于 TensorFlow Hub 的信息,请参考www.tensorflow.org/hub
。
安装 TensorFlow
在写这本书时,TensorFlow r1.13 版本已经发布。同时,2.0.0 版本也处于 Alpha 阶段,但我们将使用稳定版本。TensorFlow Hub 依赖于可以通过pip
安装的 TensorFlow 库,如下所示:
$ pip install tensorflow
$ pip install tensorflow-hub
当tensorflow
库安装完成后,我们需要在训练过程开始之前收集我们的图像数据集。在开始训练之前,我们需要考虑很多因素。
训练图像
在本节中,我们将收集图像并将其按类别整理在各自的文件夹中。
选择自己图像数据集的几个常见步骤如下:
-
首先,你需要为每个想要识别的图像类别至少准备 100 张照片。模型的准确性与数据集中的图像数量成正比。
-
你需要确保图像集中的图像更具相关性。例如,如果你拍摄了一组背景单一的图像,比如所有图像的背景都是白色且拍摄于室内,而用户需要识别具有干扰背景(例如拍摄于户外、背景五颜六色)的物体,那么这样做并不会提高准确性。
-
选择具有多样背景的图像。例如,如果你只选择了具有两种背景颜色的图像,那么你的预测将倾向于这两种颜色,而不是图像中的物体。
-
尝试将较大的类别拆分为更小的子类。例如,你可以使用“猫”,“狗”或“老虎”来代替“动物”。
-
确保选择所有包含你想识别的物体的输入图像。例如,如果你有一个识别狗的应用,我们就不会使用汽车、建筑物或山脉的图片作为输入图像。在这种情况下,最好为无法识别的图像设置一个独立的分类器。
-
确保正确标记图像。例如,将花朵标记为茉莉花时,图片中可能会包含整株植物或背景中有人物。当输入图像中有干扰物体时,我们算法的准确性会有所不同。假设你从 Google 图片搜索中获取了一些食物图片,这些图片具有可重复使用的许可,因此在收集图像用于训练模型时,务必确保这些图像符合许可要求。你可以通过在 Google 图片搜索中输入关键词,并根据可重复使用的使用权筛选图片来实现。点击搜索栏下方的工具按钮即可找到此选项。
我们在本章中收集了一些互联网图片用于教学目的。详细信息将在下一节中讨论。
构建我们自己的模型
在这里,我们将使用 TensorFlow 构建自己的机器学习模型,分析车辆的损坏程度。我们需要小心选择数据集,因为它在损伤评估阶段起着至关重要的作用。以下是我们将遵循的构建模型的步骤:
-
查找损坏车辆的图像数据集。
-
根据损坏程度对图像进行分类。首先,我们需要识别图中的物体实际上是一辆车。为此,我们需要有两类图像集,一类包含有车的图像,另一类不包含车。然后,我们还需要三个类别来确定车的损坏等级,分别是高、中、低三个等级。确保每个类别下至少有 1,000 张图像。数据集准备好后,我们就可以开始训练模型了。
-
我们将使用 TensorFlow 训练我们的模型。
-
我们将构建一个 Web 应用程序来分析车辆的损坏程度。
-
更新结果。
使用我们自己的图像重新训练
我们现在将使用retrain.py
脚本,该脚本位于我们的项目目录中。
使用curl
下载此脚本,如下所示:
mkdir -/Chapter5/images
cd -/Chapter5/images
curl -LO https://github.com/tensorflow/hub/raw/master/examples/image_retraining/ retrain.py
python retrain.py --image_dir ./images/
在训练开始之前,有几个参数必须传递给训练脚本并查看。
一旦数据集准备好,我们需要着手改善结果。我们可以通过调整学习过程中的步骤数来实现这一点。
最简单的方法是使用以下代码:
--how_many_training_steps = 4000
当步骤数增加时,准确率的提高速度会变慢,并且准确率在达到某个点之后将停止提高。你可以通过实验来决定什么对你来说最有效。
架构
MobileNet 是一个较小、低功耗、低延迟的模型,旨在满足移动设备的限制。在我们的应用中,我们从 MobileNet 数据集中选择了以下架构作为参数之一,如下代码所示,用于在构建模型时获得更好的准确性基准:
--architecture=" mobilenet_v2_1.4_224"
网络的功率和延迟随着乘法累加(MACs)的数量而增长,MACs 衡量的是融合的乘法和加法操作的数量,如下所示:
你可以从github.com/tensorflow/models/tree/master/research/slim/nets/mobilenet
下载模型。
扭曲
我们可以通过在训练过程中提供困难的输入图像来提高结果。训练图像可以通过随机裁剪、亮度调节和形变等方式生成。这将有助于生成一个有效的训练数据集。
然而,启用扭曲存在一个缺点,因为瓶颈缓存没有用处。因此,输入图像没有被重用,导致训练时间增加。这里有多种方式启用扭曲,如下所示:
--random_crop
--random_scale
--random_brightness
这在所有情况下并不一定有用。例如,在数字分类系统中,它没有什么帮助,因为翻转和扭曲图像在生成可能的输出时并不合理。
超参数
我们可以尝试更多的参数,看看额外的参数是否有助于提高结果。
按照以下项目符号中给出的形式指定它们。超参数的解释如下:
-
--learning_rate
:这个参数控制在训练过程中最终层的更新。如果这个值较小,训练将需要更多的时间。不过,这不一定总是能帮助提高准确性。 -
--train_batch_size
:这个参数帮助控制在训练过程中用于估算最终层更新的图像数量。一旦图像准备好,脚本会将它们分成三个不同的集合。最大的集合用于训练。这种划分主要有助于防止模型识别输入图像中不必要的模式。如果一个模型使用特定背景模式进行训练,当面对带有新背景的图像时,它就无法给出正确的结果,因为它记住了输入图像中的不必要信息。这就是过拟合。 -
--testing_percentage
和--validation_percentage
标志:为了避免过拟合,我们将 80%的数据保留在主训练集中。这些数据中的 10%用于在训练过程中进行验证,最后的 10%用于测试模型。 -
--validation_batch_size
:我们可以看到验证的准确性在每次迭代中有所波动。
如果你是新手,你可以在不修改这些参数的情况下运行默认值。让我们开始构建我们的模型。为此,我们需要训练图像数据。
图像数据集收集
对于我们的实验,我们需要汽车在良好状态和损坏状态下的数据集。如果你有符合隐私政策的数据源,那么这里是一个很好的起点。否则,我们需要找到一种方法来在数据集上构建我们的模型。现在有多个公开的数据集可供使用。如果没有类似数据模型的现有参考,我们需要开始构建自己的数据集,因为这可能是一个耗时且重要的步骤,能够帮助我们获得更好的结果。
我们将使用一个简单的 Python 脚本从 Google 下载图像。只要确保你筛选的是可重用的图像。我们不鼓励使用那些带有不可重用许可证的图片。
使用 Python 脚本,我们将从 Google 拉取并保存图像,然后使用一个库来完成相同的任务。这一步是构建任何机器学习模型的最基础步骤之一。
我们将使用一个叫做Beautiful Soup的 Python 库来从互联网抓取图像。
Beautiful Soup 简介
Beautiful Soup 是一个 Python 库,用于从 HTML 和 XML 文件中提取数据。它在涉及抓取的项目中非常有用。使用这个库,我们可以导航、搜索和修改 HTML 和 XML 文件。
这个库解析你提供的任何内容,并对数据进行树形遍历。你可以要求库找到所有 URL 匹配 google.com
的链接,找到所有类为 bold 的链接,或者找到所有包含粗体文本的表头。
有几个特性使它非常有用,具体如下:
-
Beautiful Soup 提供了一些简单的方法和 Pythonic 风格的习惯用法,用于遍历、搜索和修改解析树。解析树是一个工具包,用于解剖文档并提取所需内容。我们可以减少编写应用程序的代码量。
-
Beautiful Soup 自动将传入的文档转换为 Unicode,并将传出的文档转换为 UTF-8。除非文档没有指定编码且 Beautiful Soup 无法检测到任何编码,否则我们无需考虑编码问题。然后,我们只需要指定原始编码。
-
Beautiful Soup 可以与流行的 Python 解析器一起使用,如
lxml
(lxml.de/
) 和html5lib
(github.com/html5lib/
),并允许你尝试不同的解析策略,或者在灵活性和速度之间做出权衡。 -
Beautiful Soup 通过提取所需信息来节省你的时间,从而让你的工作更轻松。
这是代码的简单版本:
import argparse
import json
import itertools
import logging
import re
import os
import uuid
import sys
from urllib.request import urlopen, Request
from bs4 import BeautifulSoup
#logger will be useful for your debugging need
def configure_logging():
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
handler = logging.StreamHandler()
handler.setFormatter(
logging.Formatter('[%(asctime)s %(levelname)s %(module)s]: %(message)s'))
logger.addHandler(handler)
return logger
logger = configure_logging()
设置用户代理以避免 403 错误代码:
REQUEST_HEADER = {
'User-Agent': "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.134 Safari/537.36"}
def get_soup(url, header):
response = urlopen(Request(url, headers=header))
return BeautifulSoup(response, 'html.parser')
# initialize place for links
def get_query_url(query):
return "https://www.google.co.in/search?q=%s&source=lnms&tbm=isch" % query
# pull out specific data through navigating into source data tree
def extract_images_from_soup(soup):
image_elements = soup.find_all("div", {"class": "rg_meta"})
metadata_dicts = (json.loads(e.text) for e in image_elements)
link_type_records = ((d["ou"], d["ity"]) for d in metadata_dicts)
return link_type_records
传入你想提取的图像数量。默认情况下,Google 提供 100 张图像:
def extract_images(query, num_images):
url = get_query_url(query)
logger.info("Souping")
soup = get_soup(url, REQUEST_HEADER)
logger.info("Extracting image urls")
link_type_records = extract_images_from_soup(soup)
return itertools.islice(link_type_records, num_images)
def get_raw_image(url):
req = Request(url, headers=REQUEST_HEADER)
resp = urlopen(req)
return resp.read()
保存所有下载的图像及其扩展名,如以下代码块所示:
def save_image(raw_image, image_type, save_directory):
extension = image_type if image_type else 'jpg'
file_name = str(uuid.uuid4().hex) + "." + extension
save_path = os.path.join(save_directory, file_name)
with open(save_path, 'wb+') as image_file:
image_file.write(raw_image)
def download_images_to_dir(images, save_directory, num_images):
for i, (url, image_type) in enumerate(images):
try:
logger.info("Making request (%d/%d): %s", i, num_images, url)
raw_image = get_raw_image(url)
save_image(raw_image, image_type, save_directory)
except Exception as e:
logger.exception(e)
def run(query, save_directory, num_images=100):
query = '+'.join(query.split())
logger.info("Extracting image links")
images = extract_images(query, num_images)
logger.info("Downloading images")
download_images_to_dir(images, save_directory, num_images)
logger.info("Finished")
#main method to initiate the scrapper
def main():
parser = argparse.ArgumentParser(description='Scrape Google images')
#change the search term here
parser.add_argument('-s', '--search', default='apple', type=str, help='search term')
在这里更改图像数量参数。默认情况下它设置为 1,如以下代码所示:
parser.add_argument('-n', '--num_images', default=1, type=int, help='num images to save')
#change path according to your need
parser.add_argument('-d', '--directory', default='/Users/karthikeyan/Downloads/', type=str, help='save directory')
args = parser.parse_args()
run(args.search, args.directory, args.num_images)
if __name__ == '__main__':
main()
将脚本保存为 Python 文件,然后通过执行以下命令运行代码:
python imageScrapper.py --search "alien" --num_images 10 --directory "/Users/Karthikeyan/Downloads"
使用更好的库进行 Google 图像抓取,包括更多可配置的选项。我们将使用 github.com/hardikvasa/google-images-download
。
这是一个命令行 Python 程序,用于在 Google 图像上搜索关键词或关键短语,并可选择将图像下载到你的计算机。你也可以从另一个 Python 文件调用此脚本。
这是一个小型且可以立即运行的程序。如果你只想每个关键词下载最多 100 张图像,它不需要任何依赖。如果你想要每个关键词超过 100 张图像,那么你需要安装 Selenium
库以及 ChromeDriver。详细说明在 故障排除 部分提供。
你可以使用一个拥有更多实用选项的库。
如果你偏好基于命令行的安装方式,可以使用以下代码:
$ git clone https://github.com/hardikvasa/google-images-download.git
$ cd google-images-download && sudo python setup.py install
另外,你也可以通过 pip
安装这个库:
$ pip install google_images_download
如果通过 pip
安装或使用 命令行语言解释器 (CLI) 安装,可以使用以下命令:
$ googleimagesdownload [Arguments...]
如果是从 github.com
的 UI 下载的,请解压下载的文件,进入 google_images_download
目录,并使用以下命令之一:
$ python3 google_images_download.py [Arguments...]
$ python google_images_download.py [Arguments...]
如果您想从另一个 Python 文件中使用此库,请使用以下命令:
from google_images_download import google_images_download
response = google_images_download.googleimagesdownload()
absolute_image_paths = response.download({<Arguments...>})
您可以直接从命令行传递参数,如下所示,或者通过配置文件传递参数。
您可以通过配置文件传递多个记录。以下示例由两组记录组成。代码将遍历每一条记录,并根据传递的参数下载图片。
以下是配置文件的示例:
{
"Records": [
{
"keywords": "apple",
"limit": 55,
"color": "red",
"print_urls": true
},
{
"keywords": "oranges",
"limit": 105,
"size": "large",
"print_urls": true
}
]
}
示例
如果您从另一个 Python 文件调用此库,以下是来自 Google 的示例代码:
_images_download import google_images_download
#importing the library
response = google_images_download.googleimagesdownload()
#class instantiation
arguments = {"keywords":"apple, beach, cat","limit":30,"print_urls":True} #creating list of arguments
paths = response.download(arguments) #passing the arguments to the function
print(paths)
#printing absolute paths of the downloaded images
如果您是通过配置文件传递参数,只需传递 config_file
参数,并指定您的 JSON 文件名:
$ googleimagesdownload -cf example.json
以下是使用关键词和限制参数的简单示例:
$ googleimagesdownload --keywords "apple, beach, cat" --limit 20
使用后缀关键词可以指定主关键词后的词语。例如,如果关键词是 car
,而后缀关键词是 red
和 blue
,则会先搜索红色的汽车,再搜索蓝色的汽车:
$ googleimagesdownload --k "car" -sk 'yellow,blue,green' -l 10
要使用简化命令,请使用以下代码:
$ googleimagesdownload -k "apple, beach, cat" -l 20
要下载具有特定图像扩展名或格式的图片,请使用以下代码:
$ googleimagesdownload --keywords "logo" --format svg
要为图片使用颜色过滤器,请使用以下代码:
$ googleimagesdownload -k "playground" -l 20 -co red
要使用非英语关键词进行图片搜索,请使用以下代码:
$ googleimagesdownload -k "" -l 5
要从 Google 图片链接下载图片,请使用以下代码:
$ googleimagesdownload -k "sample" -u <google images page URL>
要将图片保存到特定的主目录(而不是 Downloads
)中,请使用以下代码:
$ googleimagesdownload -k "boat" -o "boat_new"
要下载图像 URL 中的单张图片,请使用以下代码:
$ googleimagesdownload --keywords "baloons" --single_image <URL of the images>
要下载具有大小和类型约束的图片,请使用以下代码:
$ googleimagesdownload --keywords "baloons" --size medium --type animated
要下载具有特定使用权的图片,请使用以下代码:
$ googleimagesdownload --keywords "universe" --usage_rights labeled-for-reuse
要下载具有特定颜色类型的图片,请使用以下代码:
$ googleimagesdownload --keywords "flowers" --color_type black-and-white
要下载具有特定纵横比的图片,请使用以下代码:
$ googleimagesdownload --keywords "universe" --aspect_ratio panoramic
要下载与您提供的图像 URL 中的图片相似的图像(即反向图片搜索),请使用以下代码:
$ googleimagesdownload -si <image url> -l 10
要根据给定关键词从特定网站或域名下载图片,请使用以下代码:
$ googleimagesdownload --keywords "universe" --specific_site google.com
图片将下载到它们各自的子目录中,位于您所在文件夹的主目录内(无论是您提供的目录,还是 Downloads
)。
现在,我们需要开始准备我们的数据集。
数据集准备
我们需要构建四个不同的数据集。对于汽车损坏检测,我们将考虑所有可能的输入。它可以是一辆状况良好的车,或一辆不同损坏程度的车,或者也可以是与车无关的图像。
我们将按照以下截图中所示的方式操作:
这是用于识别严重损坏汽车的数据集:
googleimagesdownload -k "heavily damaged car" -sk 'red,blue,white,black,green,brown,pink,yellow' -l 500
下面是一些为识别严重损坏的红色汽车所捕获的示例图片:
这是一些捕获到的有严重损坏的蓝色汽车的示例图片:
我们还拥有另一组轻微损坏的汽车图像:
googleimagesdownload -k "car dent" -sk 'red,blue,white,black,green,brown,pink,yellow' -l 500
这是一些捕获到的有凹痕的红色汽车的示例图片:
这是一些捕获到的有凹痕的蓝色汽车的示例图片:
以下命令可用于检索没有任何损坏的普通汽车数据集:
googleimagesdownload -k "car" -l 500
这是一些捕获到的红色汽车的示例图片:
这是一些捕获到的蓝色汽车的示例图片:
以下命令可用于检索不属于汽车的随机物体:
googleimagesdownload -k "bike,flight,home,road,tv" -l 500
这是一些捕获到的自行车的示例图片:
这是一些捕获到的航班的示例图片:
一旦每个数据集有了 500 张图片,就可以开始训练了。在理想条件下,每个数据集至少应有 1,000 张图片。
我们面临的主要问题是去除噪声数据。对于我们的示例,我们将手动进行这一操作。以下是我们列出的一些示例图像,它们可能是噪声数据,不提供有效输入,因此无法用于构建数据模型:
一旦我们准备好了所有的图像数据集,就可以开始处理我们的四大类了。目前,所有图像都按颜色和类别分开,如下面的截图所示:
我们将把它们分为损坏汽车
、有凹痕的汽车
、汽车
和非汽车
:
运行训练脚本
在讨论完所有与参数相关的细节后,我们可以开始使用下载的脚本进行训练:
python retrain.py \
--bottleneck_dir=./ \
--how_many_training_steps=4000 \
--model_dir=./ \
--output_graph=./retrained_graph.pb \
--output_labels=retrained_labels.txt \
--architecture=" mobilenet_v2_1.4_224" \
--image_dir=/Users/karthikeyan/Documents/ /book/Chapter5/images
根据我们的处理器性能以及图像数量,脚本训练可能会需要更长时间。对我来说,50 个不同类别的汽车,每个类别包含 10,000 张图片,训练花费了超过 10 小时。一旦脚本完成,我们将在输出中得到 TensorFlow 模型。
设置一个 Web 应用程序
我们将使用Flask框架来构建一个简单的应用程序,以检测汽车的损坏。
想了解更多关于 Flask 的信息,请参考www.fullstackpython.com/flask.html
。
我们在这里不会深入讲解 Flask 的基础知识。相反,我们只是将我们的模型与 Flask 中现有的文件上传示例结合起来。
文件的结构如下面的截图所示:
这里是app.py
中的内容列表:
import os
import glob
from classify import prediction
import tensorflow as tf
import thread
import time
from flask import Flask, render_template, request, redirect, url_for, send_from_directory,flash
from werkzeug import secure_filename
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = 'uploads/'
app.config['ALLOWED_EXTENSIONS'] = set(['jpg', 'jpeg'])
app.config['SECRET_KEY'] = '7d441f27d441f27567d441f2b6176a'
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1] in app.config['ALLOWED_EXTENSIONS']
@app.route('/')
def index():
return render_template('index.html')
@app.route('/upload', methods=['POST'])
def upload():
file = request.files['file']
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
filename = str(len(os.listdir(app.config['UPLOAD_FOLDER']))+1)+'.jpg'
file_name_full_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(file_name_full_path)
return render_template('upload_success.html')
@app.route('/uploads/<filename>')
def uploaded_file(filename):
return send_from_directory(app.config['UPLOAD_FOLDER'],
filename)
@app.route('/claim', methods=['POST'])
def predict():
list_of_files = glob.glob('/Users/karthikeyan/Documents/code/play/acko/cardamage/Car-Damage-Detector/uploads/*.jpg') # * means all if need specific format then *.csv
latest_file = max(list_of_files, key=os.path.getctime)
print(latest_file)
image_path = latest_file
下一段代码帮助我们打印输出:
#print(max(glob.glob(r'uploads\*.jpg'), key=os.path.getmtime))
with tf.Graph().as_default():
human_string, score= prediction(image_path)
print('model one value' + str(human_string))
print('model one value' + str(score))
if (human_string == 'car'):
label_text = 'This is not a damaged car with confidence ' + str(score) + '%. Please upload a damaged car image'
print(image_path)
return render_template('front.html', text = label_text, filename="http://localhost:5000/uploads/"+os.path.basename(image_path))
elif (human_string == 'low'):
label_text = 'This is a low damaged car with '+ str(score) + '% confidence.'
print(image_path)
打印图像路径后,继续执行以下代码:
return render_template('front.html', text = label_text, filename="http://localhost:5000/uploads/"+os.path.basename(image_path))
elif (human_string == 'high'):
label_text = 'This is a high damaged car with '+ str(score) + '% confidence.'
print(image_path)
return render_template('front.html', text = label_text, filename="http://localhost:5000/uploads/"+os.path.basename(image_path))
elif (human_string == 'not'):
label_text = 'This is not the image of a car with confidence ' + str(score) + '%. Please upload the car image.'
print(image_path)
return render_template('front.html', text = label_text, filename="http://localhost:5000/uploads/"+os.path.basename(image_path))
def cleanDirectory(threadName,delay):
while
循环从这里开始:
while True:
time.sleep(delay)
print ("Cleaning Up Directory")
filelist = [ f for f in (os.listdir(app.config['UPLOAD_FOLDER'])) ]
for f in filelist:
#os.remove("Uploads/"+f)
os.remove(os.path.join(app.config['UPLOAD_FOLDER'], f))
if __name__ == '__main__':
try:
_thread.start_new_thread( cleanDirectory, ("Cleaning Thread", 99999999, ) )
except:
print("Error: unable to start thread" )
app.run()
Classify.py does the model classification using TensorFlow.
import tensorflow as tf
import sys
import os
import urllib
禁用 TensorFlow 编译警告:
os.environ['TF_CPP_MIN_LOG_LEVEL']='2'
import tensorflow as tf
def prediction(image_path):
image_data = tf.gfile.FastGFile(image_path, 'rb').read()
print(image_path)
label_lines = [line.rstrip() for line
in tf.gfile.GFile(r"./models/tf_files/retrained_labels.txt")]
with tf.gfile.FastGFile(r"./models/tf_files/retrained_graph.pb", 'rb') as f:
graph_def = tf.GraphDef()
graph_def.ParseFromString(f.read())
_ = tf.import_graph_def(graph_def, name='')
with tf.Session() as sess:
一旦将 image_data
作为输入传递给图表,我们就会得到第一次预测:
softmax_tensor = sess.graph.get_tensor_by_name('final_result:0')
predictions = sess.run(softmax_tensor, \
{'DecodeJpeg/contents:0': image_data})
top_k = predictions[0].argsort()[-len(predictions[0]):][::-1]
for node_id in top_k:
count = 1
human_string = label_lines[node_id]
score = predictions[0][node_id]
print(count)
count += 1
print('%s (score = %.5f)' % (human_string, score))
score = (round((score * 100), 2))
return human_string,score
控制器 Python 文件与前端 HTML 文件排布在一起:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="Karthikeyan NG">
<title>Damage Estimator</title>
<!-- Bootstrap core CSS -->
<link href="{{ url_for('static', filename='vendor/bootstrap/css/bootstrap.min.css') }}" rel="stylesheet"/>
<!-- Custom fonts for this template -->
<link href="{{ url_for('static', filename='vendor/font-awesome/css/font-awesome.min.css') }}" rel="stylesheet" type="text/css"/>
<link href='https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800' rel='stylesheet' type='text/css'>
<link href='https://fonts.googleapis.com/css?family=Merriweather:400,300,300italic,400italic,700,700italic,900,900italic' rel='stylesheet' type='text/css'>
<!-- Plugin CSS -->
<link href="{{ url_for('static', filename='vendor/magnific-popup/magnific-popup.css') }}" rel="stylesheet" />
<!-- Custom styles for this template -->
<link href="{{ url_for('static', filename='css/creative.min.css') }}" rel="stylesheet" />
</head>
<body id="page-top">
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-light fixed-top" id="mainNav">
<a class="navbar-brand" href="#page-top">Damage Estimator</a>
<button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse" data-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarResponsive">
</div>
</nav>
<section class="bg-primary" id="about">
<div class="container">
<div class="row">
<div class="col-lg-8 mx-auto text-center">
<h2 class="section-heading text-white">Do you have a damaged vehicle?</h2>
<hr class="light">
<p class="text-faded">Machine Learning allows for a classification process that is automated and makes lesser error. Besides risk group classification, Deep Learning algorithms can be applied to images of vehicle damage, allowing for automated claim classification.</p>
<br/>
<div class="contr"><h4 class="section-heading text-white">Select the file (image) and Upload</h4></div>
<br/>
<form action="upload" method="post" enctype="multipart/form-data">
<div class="form-group">
<input type="file" name="file" class="file">
<div class="input-group col-xs-12">
<span class="input-group-addon"><i class="glyphicon glyphicon-picture"></i></span>
<input type="text" class="form-control input-lg" disabled placeholder="Upload Image">
<span class="input-group-btn">
<button class="browse btn btn-primary input-lg" type="button"><i class="glyphicon glyphicon-search"></i> Browse</button>
</span>
</div>
</div>
<input type="submit" class="btn btn-primary" value="Upload"><br /><br />
</form>
</div>
</div>
</section>
接着上一个脚本,让我们为核心 JavaScript 设置 Bootstrap:
<!-- Bootstrap core JavaScript -->
<script src="img/jquery.min.js') }}"></script>
<script src="img/popper.min.js') }}"></script>
<script src="img/bootstrap.min.js') }}"></script>
<!-- Plugin JavaScript -->
<script src="img/jquery.easing.min.js') }}"></script>
<script src="img/scrollreveal.min.js') }}"></script>
<script src="img/jquery.magnific-popup.min.js') }}"></script>
<!-- Custom scripts for this template -->
<script src="img/creative.min.js') }}"></script>
<script>
$(document).on('click', '.browse', function(){
var file = $(this).parent().parent().parent().find('.file');
file.trigger('click');
});
$(document).on('change', '.file', function(){
$(this).parent().find('.form-control').val($(this).val().replace(/C:\\fakepath\\/i, ''));
});
</script>
</body>
</html>
你可以直接从 GitHub 仓库拉取文件的其余内容。一旦文件结构准备好,你可以通过命令行运行应用程序,如下所示:
$ python app.py
现在,启动你的浏览器并访问 http://localhost:5000/
:
以下是应用程序中的一些截图。
这是运行应用程序后的主页:
这是上传图片后的屏幕:
这是显示一辆轻微受损汽车的截图:
由于我们的数据集规模非常小,上面的截图中的数据可能不准确。
以下是一个截图,显示了一个预测汽车的模型,但该模型没有显示出汽车:
总结
在本章节中,我们已经学习了如何从零开始构建一个模型,并使用 TensorFlow 进行训练。
拥有这些知识后,我们可以在接下来的章节中开始构建更多基于 Android 和 iOS 的应用程序。
第六章:PyTorch 在 NLP 和 RNN 上的实验
在本章中,我们将深入研究 PyTorch 库在 自然语言处理(NLP)和其他实验中的应用。然后,我们将把开发的模型转换为可以在 Android 或 iOS 应用中使用的格式,使用 TensorFlow 和 CoreML。
在本章中,我们将覆盖以下主题:
-
PyTorch 特性和安装简介
-
在 PyTorch 中使用变量
-
构建我们自己的模型网络
-
分类 递归神经网络(RNN)
-
自然语言处理
PyTorch
PyTorch 是一个基于 Python 的库,用于执行与 GPU 相关的科学计算操作。它通过加速实验来帮助运行生产级生态系统并分布式训练库。它还提供了两个高级特性:张量计算和基于磁带的自动求导系统构建神经网络。
PyTorch 的特性
PyTorch 提供了一个端到端的深度学习系统。它的特点如下:
-
Python 使用:PyTorch 不仅仅是 C++ 框架的 Python 绑定。它深度集成于 Python,因此可以与其他流行的库和框架一起使用。
-
工具和库:它在计算机视觉和强化学习领域拥有一个活跃的研究人员和开发者社区。
-
灵活的前端:包括易于使用的混合模式支持,在急切模式下加速速度并实现无缝切换到图模式,以及在 C++ 运行时的功能性和优化。
-
云支持:支持所有主要的云平台,允许使用预构建的镜像进行无缝开发和扩展,以便能够作为生产级应用运行。
-
分布式训练:包括通过原生支持异步执行操作和点对点(p2p)通信来优化性能,这样我们可以同时访问 C++ 和 Python。
-
原生支持 ONNX:我们可以将模型导出为标准的 Open Neural Network Exchange (ONNX) 格式,以便在其他平台、运行时和可视化工具中访问。
安装 PyTorch
在编写本书时,有一个稳定版本的 PyTorch 可用,即 1.0。如果你想亲自体验最新的代码库,还可以选择使用每日预览构建版。你需要根据你的包管理器安装相应的依赖项。Anaconda 是推荐的包管理器,它会自动安装所有依赖项。LibTorch 仅适用于 C++。以下是安装 PyTorch 时可用的安装选项网格:
上面的截图指定了在编写本书时使用的包网格。你可以根据硬件配置的可用性选择任何一个包网格。
要安装 PyTorch 并启动 Jupyter Notebook,请运行以下命令:
python --version
sudo brew install python3
brew install python3
pip3 install --upgrade pip
pip3 install jupyter
jupyter notebook
PyTorch 的安装过程如下图所示:
当你启动 Jupyter Notebook 时,一个新的浏览器会话会打开,显示一个空白的笔记本,如下所示:
让我们先了解一下 PyTorch 的基础。
PyTorch 基础
现在 PyTorch 已经安装完成,我们可以开始实验了。我们将从 torch
和 numpy
开始。
从顶部菜单创建一个新的笔记本,并包含以下代码:
# first basic understanding on PyTorch
# book: AI for Mobile application projects
import torch
import numpy as np
# convert numpy to tensor or vise versa
numpy_data = np.arange(8).reshape((2, 4))
torch_data = torch.from_numpy(numpy_data)
#convert tensor to array
tensor2array = torch_data.numpy()
#Print the results
print
(
'\nnumpy array:', numpy_data, # [[0 1 2 3], [4 5 6 7]]
'\ntorch tensor:', torch_data, # 0 1 2 3\n 4 5 6 7 [torch.LongTensor of size 2x3]
'\ntensor to array:', tensor2array, # [[0 1 2 3], [4 5 6 7]]
)
现在,让我们进行一些数学运算:
# abs method on numpy
numpy_data = [-1, -2, 1, 2]
tensor = torch.FloatTensor(numpy_data) # 32-bit floating point
#print the results
print
(
'\nabs',
'\nnumpy: ', np.abs(numpy_data), # [1 2 1 2]
'\ntorch: ', torch.abs(tensor) # [1 2 1 2]
)
# sin method on numpy
#print the results
print
(
'\nsin',
'\nnumpy: ', np.sin(numpy_data), # [-0.84147098 -0.90929743 0.84147098 0.90929743]
'\ntorch: ', torch.sin(tensor) # [-0.8415 -0.9093 0.8415 0.9093]
)
让我们计算均值方法并打印结果:
#print the results
print
(
'\nmean',
'\nnumpy: ', np.mean(data), # 0.0
'\ntorch: ', torch.mean(tensor) # 0.0
)
# matrix multiplication with numpy
numpy_data = [[1,2], [3,4]]
tensor = torch.FloatTensor(numpy_data) # 32-bit floating point
# correct method and print the results
print(
'\nmatrix multiplication (matmul)',
'\nnumpy: ', np.matmul(numpy_data, numpy_data), # [[7, 10], [15, 22]]
'\ntorch: ', torch.mm(tensor, tensor) # [[7, 10], [15, 22]]
)
以下代码展示了数学运算的输出:
numpy array: [[0 1 2 3]
[4 5 6 7]]
torch tensor: tensor([[0, 1, 2, 3],
[4, 5, 6, 7]])
tensor to array: [[0 1 2 3]
[4 5 6 7]]
abs
numpy: [1 2 1 2]
torch: tensor([1., 2., 1., 2.])
sin
numpy: [-0.84147098 -0.90929743 0.84147098 0.90929743]
torch: tensor([-0.8415, -0.9093, 0.8415, 0.9093])
mean
numpy: 0.0
torch: tensor(0.)
matrix multiplication (matmul)
numpy: [[ 7 10]
[15 22]]
torch: tensor([[ 7., 10.],
[15., 22.]])
现在,让我们来看看如何在 PyTorch 中使用不同的变量。
在 PyTorch 中使用变量
torch
中的变量用于构建计算图。每当一个变量被计算时,它都会构建一个计算图。这个计算图用于连接所有的计算步骤(节点),最终当误差反向传播时,会同时计算所有变量的修改范围(梯度)。相比之下,tensor
并不具备这种能力。我们将通过一个简单的例子来探讨这种差异:
import torch
from torch.autograd import Variable
# Variable in torch is to build a computational graph,
# So torch does not have placeholder, torch can just pass variable to the computational graph.
tensor = torch.FloatTensor([[1,2,3],[4,5,6]]) # build a tensor
variable = Variable(tensor, requires_grad=True) # build a variable, usually for compute gradients
print(tensor) # [torch.FloatTensor of size 2x3]
print(variable) # [torch.FloatTensor of size 2x3]
# till now the tensor and variable looks similar.
# However, the variable is a part of the graph, it's a part of the auto-gradient.
#Now we will calculate the mean value on tensor(X²)
t_out = torch.mean(tensor*tensor)
#Now we will calculate the mean value on variable(X²)
v_out = torch.mean(variable*variable)
现在,我们将打印所有参数的结果:
#print the results
print(t_out)
print(v_out)
#result will be 7.5
v_out.backward() # backpropagation from v_out
# v_out = 1/4 * sum(variable*variable)
# the gradients with respect to the variable,
#Let's print the variable gradient
print(variable.grad)
'''
0.5000 1.0000
1.5000 2.0000
'''
print("Resultant data in the variable: "+str(variable)) # this is data in variable
"""
Variable containing:
1 2
3 4
We will consider the variable as a FloatTensor
[torch.FloatTensor of size 2x2]
"""
print(variable.data) # this is data in tensor format
"""
1 2
3 4
We will consider the variable as FloatTensor
[torch.FloatTensor of size 2x2]
"""
#we will print the result in the numpy format
print(variable.data.numpy())
"""
[[ 1\. 2.]
[ 3\. 4.]]
"""
以下是前面代码块的输出:
tensor([[1., 2., 3.],
[4., 5., 6.]])
tensor([[1., 2., 3.],
[4., 5., 6.]], requires_grad=True)
tensor(15.1667)
tensor(15.1667, grad_fn=<MeanBackward1>)
tensor([[0.3333, 0.6667, 1.0000],
[1.3333, 1.6667, 2.0000]])
Data in the variabletensor([[1., 2., 3.],
[4., 5., 6.]], requires_grad=True)
tensor([[1., 2., 3.],
[4., 5., 6.]])
[[1\. 2\. 3.]
[4\. 5\. 6.]]
现在,让我们尝试使用 matplotlib
在图表上绘制数据。
在图表上绘制值
让我们做一个简单的程序,将值绘制在图表上。为此,使用以下代码:
#This line is necessary to print the output inside jupyter notebook
%matplotlib inline
import torch
import matplotlib.pyplot as plt
import torch.nn.functional as F
from torch.autograd import Variable
# dummy data for the example
#lets declare linspace
x = torch.linspace(-5, 5, 200) # x data (tensor), shape=(100, 1)
x = Variable(x)
#call numpy array to plot the results
x_np = x.data.numpy()
以下代码块列出了一些激活方法:
#RelU function
y_relu = torch.relu(x).data.numpy()
#sigmoid method
y_sigmoid = torch.sigmoid(x).data.numpy()
#tanh method
y_tanh = torch.tanh(x).data.numpy()
#softplus method
y_softplus = F.softplus(x).data.numpy() # there's no softplus in torch
# y_softmax = torch.softmax(x, dim=0).data.numpy() softmax is an activation function and it deals with probability
使用 matplotlib
激活函数:
#we will plot the activation function with matplotlib
plt.figure(1, figsize=(8, 6))
plt.subplot(221)
plt.plot(x_np, y_relu, c='red', label='relu')
plt.ylim((-1, 5))
plt.legend(loc='best')
#sigmoid activation function
plt.subplot(222)
plt.plot(x_np, y_sigmoid, c='red', label='sigmoid')
plt.ylim((-0.2, 1.2))
plt.legend(loc='best')
#tanh activation function
plt.subplot(223)
plt.plot(x_np, y_tanh, c='red', label='tanh')
plt.ylim((-1.2, 1.2))
plt.legend(loc='best')
#softplus activation function
plt.subplot(224)
plt.plot(x_np, y_softplus, c='red', label='softplus')
plt.ylim((-0.2, 6))
plt.legend(loc='best')
#call the show method to draw the graph on screen
plt.show()
让我们在图表上绘制这些值,如下所示:
请注意,前面代码的第一行是必须的,用于在 Jupyter Notebook 中绘制图表。如果你是直接从终端运行 Python 文件,可以省略代码的第一行。
构建我们自己的模型网络
在这一部分,我们将通过一步步的示例使用 PyTorch 构建我们自己的网络。
让我们从线性回归开始,作为起点。
线性回归
线性回归可能是任何人在学习机器学习时接触的第一个方法。线性回归的目标是找到一个或多个特征(自变量)与一个连续的目标变量(因变量)之间的关系,这可以在以下代码中看到。
导入所有必要的库并声明所有必要的变量:
%matplotlib inline
#Import all the necessary libraries
import torch
import torch.nn.functional as F
import matplotlib.pyplot as plt
#we will define data points for both x-axis and y-axis
# x data (tensor), shape=(100, 1)
x = torch.unsqueeze(torch.linspace(-1, 1, 100), dim=1)
# noisy y data (tensor), shape=(100, 1)
y = x.pow(2) + 0.2*torch.rand(x.size())
# torch can only train on Variable, so convert them to Variable
# x, y = Variable(x), Variable(y)
# plt.scatter(x.data.numpy(), y.data.numpy())
# plt.show()
我们将定义线性回归类,并运行一个简单的 nn
来解释回归:
class Net(torch.nn.Module):
def __init__(self, n_feature, n_hidden, n_output):
super(Net, self).__init__()
self.hidden = torch.nn.Linear(n_feature, n_hidden) # hidden layer
self.predict = torch.nn.Linear(n_hidden, n_output) # output layer
def forward(self, x):
x = F.relu(self.hidden(x)) # activation function for hidden layer
x = self.predict(x) # linear output
return x
net = Net(n_feature=1, n_hidden=10, n_output=1) # define the network
print(net) # net architecture
optimizer = torch.optim.SGD(net.parameters(), lr=0.2)
loss_func = torch.nn.MSELoss() # this is for regression mean squared loss
plt.ion() # something about plotting
for t in range(200):
prediction = net(x) # input x and predict based on x
loss = loss_func(prediction, y) # must be (1\. nn output, 2\. target)
optimizer.zero_grad() # clear gradients for next train
loss.backward() # backpropagation, compute gradients
optimizer.step() # apply gradients
if t % 50 == 0:
现在我们将看到如何绘制图表并展示学习过程:
plt.cla()
plt.scatter(x.data.numpy(), y.data.numpy())
plt.plot(x.data.numpy(), prediction.data.numpy(), 'r-', lw=5)
plt.text(0.5, 0, 'Loss=%.4f' % loss.data.numpy(), fontdict={'size': 20, 'color': 'black'})
plt.pause(0.1)
plt.ioff()
plt.show()
让我们将这段代码的输出绘制到图表上,如下所示:
最终的图表如下所示,其中损失(即预测输出与实际输出之间的偏差)为 0.01:
现在,我们将开始使用 PyTorch 进行更深入的应用案例。
分类
分类问题运行神经网络模型以对输入进行分类。例如,它将衣物的图像分类为裤子、上衣和衬衫。当我们向分类模型提供更多输入时,它将预测输出的结果值。
一个简单的示例是将电子邮件过滤为垃圾邮件或非垃圾邮件。分类要么根据训练集预测分类标签,要么在分类新数据时使用的分类属性来预测分类标签(类别标签)。有许多分类模型,如朴素贝叶斯、随机森林、决策树和逻辑回归。
在这里,我们将处理一个简单的分类问题。为此,使用以下代码:
%matplotlib inline
import torch
import torch.nn.functional as F
import matplotlib.pyplot as plt
# torch.manual_seed(1) # reproducible
# make fake data
n_data = torch.ones(100, 2)
x0 = torch.normal(2*n_data, 1) # class0 x data (tensor), shape=(100, 2)
y0 = torch.zeros(100) # class0 y data (tensor), shape=(100, 1)
x1 = torch.normal(-2*n_data, 1) # class1 x data (tensor), shape=(100, 2)
y1 = torch.ones(100) # class1 y data (tensor), shape=(100, 1)
x = torch.cat((x0, x1), 0).type(torch.FloatTensor) # shape (200, 2) FloatTensor = 32-bit floating
y = torch.cat((y0, y1), ).type(torch.LongTensor) # shape (200,) LongTensor = 64-bit integer
class Net(torch.nn.Module):
def __init__(self, n_feature, n_hidden, n_output):
super(Net, self).__init__()
self.hidden = torch.nn.Linear(n_feature, n_hidden) # hidden layer
self.out = torch.nn.Linear(n_hidden, n_output) # output layer
def forward(self, x):
x = F.relu(self.hidden(x)) # activation function for hidden layer
x = self.out(x)
return x
net = Net(n_feature=2, n_hidden=10, n_output=2) # define the network
print(net) # net architecture
optimizer = torch.optim.SGD(net.parameters(), lr=0.02)
loss_func = torch.nn.CrossEntropyLoss() # the target label is NOT an one-hotted
plt.ion() # something about plotting
for t in range(100):
out = net(x) # input x and predict based on x
loss = loss_func(out, y) # must be (1\. nn output, 2\. target), the target label is NOT one-hotted
optimizer.zero_grad() # clear gradients for next train
loss.backward() # backpropagation, compute gradients
optimizer.step() # apply gradients
if t % 10 == 0:
现在,让我们绘制图表并显示学习过程:
plt.cla()
prediction = torch.max(out, 1)[1]
pred_y = prediction.data.numpy()
target_y = y.data.numpy()
plt.scatter(x.data.numpy()[:, 0], x.data.numpy()[:, 1], c=pred_y, s=100, lw=0, cmap='RdYlGn')
accuracy = float((pred_y == target_y).astype(int).sum()) / float(target_y.size)
plt.text(1.5, -4, 'Accuracy=%.2f' % accuracy, fontdict={'size': 20, 'color': 'red'})
plt.pause(0.1)
plt.ioff()
plt.show()
上述代码的输出如下:
Net(
(hidden): Linear(in_features=2, out_features=10, bias=True)
(out): Linear(in_features=10, out_features=2, bias=True)
)
我们将只从输出中选取几个图形,如以下截图所示:
你可以看到随着迭代步骤数的增加,准确度水平也有所提升:
我们可以在执行的最后一步达到 1.00 的准确度水平:
使用 torch 构建简单神经网络
当需要启发式方法来解决问题时,神经网络是必不可少的。让我们通过以下示例来探索一个基本的神经网络:
import torch
import torch.nn.functional as F
# replace following class code with an easy sequential network
class Net(torch.nn.Module):
def __init__(self, n_feature, n_hidden, n_output):
super(Net, self).__init__()
self.hidden = torch.nn.Linear(n_feature, n_hidden) # hidden layer
self.predict = torch.nn.Linear(n_hidden, n_output) # output layer
def forward(self, x):
x = F.relu(self.hidden(x)) # activation function for hidden layer
x = self.predict(x) # linear output
return x
net1 = Net(1, 10, 1)
以下是构建网络的最简单且最快的方法:
net2 = torch.nn.Sequential(
torch.nn.Linear(1, 10),
torch.nn.ReLU(),
torch.nn.Linear(10, 1)
)
print(net1) # net1 architecture
"""
Net (
(hidden): Linear (1 -> 10)
(predict): Linear (10 -> 1)
)
"""
print(net2) # net2 architecture
"""
Sequential (
(0): Linear (1 -> 10)
(1): ReLU ()
(2): Linear (10 -> 1)
)
"""
上述代码的输出如下:
Net(
(hidden): Linear(in_features=1, out_features=10, bias=True)
(predict): Linear(in_features=10, out_features=1, bias=True)
)
Sequential(
(0): Linear(in_features=1, out_features=10, bias=True)
(1): ReLU()
(2): Linear(in_features=10, out_features=1, bias=True)
)
Out[1]:
'\nSequential (\n (0): Linear (1 -> 10)\n (1): ReLU ()\n (2): Linear (10 -> 1)\n)\n'
在网络上保存和重新加载数据
让我们看看一个保存网络数据然后恢复数据的示例:
%matplotlib inline
import torch
import matplotlib.pyplot as plt
# torch.manual_seed(1) # reproducible
# fake data
x = torch.unsqueeze(torch.linspace(-1, 1, 100), dim=1) # x data (tensor), shape=(100, 1)
y = x.pow(2) + 0.2*torch.rand(x.size()) # noisy y data (tensor), shape=(100, 1)
# The code below is deprecated in Pytorch 0.4\. Now, autograd directly supports tensors
# x, y = Variable(x, requires_grad=False), Variable(y, requires_grad=False)
def save():
# save net1
net1 = torch.nn.Sequential(
torch.nn.Linear(1, 10),
torch.nn.ReLU(),
torch.nn.Linear(10, 1)
)
optimizer = torch.optim.SGD(net1.parameters(), lr=0.5)
loss_func = torch.nn.MSELoss()
for t in range(100):
prediction = net1(x)
loss = loss_func(prediction, y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
# plot result
plt.figure(1, figsize=(10, 3))
plt.subplot(131)
plt.title('Net1')
plt.scatter(x.data.numpy(), y.data.numpy())
plt.plot(x.data.numpy(), prediction.data.numpy(), 'r-', lw=5)
保存网络的两种方式:
torch.save(net1, 'net.pkl') # save entire net
torch.save(net1.state_dict(), 'net_params.pkl') # save only the parameters
def restore_net():
# restore entire net1 to net2
net2 = torch.load('net.pkl')
prediction = net2(x)
# plot result
plt.subplot(132)
plt.title('Net2')
plt.scatter(x.data.numpy(), y.data.numpy())
plt.plot(x.data.numpy(), prediction.data.numpy(), 'r-', lw=5)
def restore_params():
# restore only the parameters in net1 to net3
net3 = torch.nn.Sequential(
torch.nn.Linear(1, 10),
torch.nn.ReLU(),
torch.nn.Linear(10, 1)
)
# copy net1's parameters into net3
net3.load_state_dict(torch.load('net_params.pkl'))
prediction = net3(x)
绘制结果:
# plot result
plt.subplot(133)
plt.title('Net3')
plt.scatter(x.data.numpy(), y.data.numpy())
plt.plot(x.data.numpy(), prediction.data.numpy(), 'r-', lw=5)
plt.show()
# save net1
save()
# restore entire net (may slow)
restore_net()
# restore only the net parameters
restore_params()
代码的输出将类似于以下图表所示的图形:
批量运行
Torch 帮助你通过DataLoader
来组织数据。我们可以使用它通过批量训练来打包数据。我们可以将自己的数据格式(例如 NumPy 数组或其他格式)加载到 Tensor 中,并进行包装。
以下是一个数据集的示例,其中随机数以批量的形式被引入数据集并进行训练:
import torch
import torch.utils.data as Data
torch.manual_seed(1) # reproducible
BATCH_SIZE = 5
x = torch.linspace(1, 10, 10) # this is x data (torch tensor)
y = torch.linspace(10, 1, 10) # this is y data (torch tensor)
torch_dataset = Data.TensorDataset(x, y)
loader = Data.DataLoader(
dataset=torch_dataset, # torch TensorDataset format
batch_size=BATCH_SIZE, # mini batch size
shuffle=True, # random shuffle for training
num_workers=2, # subprocesses for loading data
)
def show_batch():
for epoch in range(3): # train entire dataset 3 times
for step, (batch_x, batch_y) in enumerate(loader): # for each training step
# train your data...
print('Epoch: ', epoch, '| Step: ', step, '| batch x: ',
batch_x.numpy(), '| batch y: ', batch_y.numpy())
if __name__ == '__main__':
show_batch()
代码的输出如下:
Epoch: 0 | Step: 0 | batch x: [ 5\. 7\. 10\. 3\. 4.] | batch y: [6\. 4\. 1\. 8\. 7.]
Epoch: 0 | Step: 1 | batch x: [2\. 1\. 8\. 9\. 6.] | batch y: [ 9\. 10\. 3\. 2\. 5.]
Epoch: 1 | Step: 0 | batch x: [ 4\. 6\. 7\. 10\. 8.] | batch y: [7\. 5\. 4\. 1\. 3.]
Epoch: 1 | Step: 1 | batch x: [5\. 3\. 2\. 1\. 9.] | batch y: [ 6\. 8\. 9\. 10\. 2.]
Epoch: 2 | Step: 0 | batch x: [ 4\. 2\. 5\. 6\. 10.] | batch y: [7\. 9\. 6\. 5\. 1.]
Epoch: 2 | Step: 1 | batch x: [3\. 9\. 1\. 8\. 7.] | batch y: [ 8\. 2\. 10\. 3\. 4.]
优化算法
在我们实现神经网络时,总是存在关于应该使用哪种优化算法以获得更好输出的疑问。这是通过修改关键参数,如权重和偏差值来完成的。
这些算法用于最小化(或最大化)误差(E(x)),它依赖于内部参数。它们用于计算从模型中使用的预测变量(x)集得出的目标结果(Y)。
现在,让我们通过以下示例来看看不同类型的算法:
%matplotlib inline
import torch
import torch.utils.data as Data
import torch.nn.functional as F
import matplotlib.pyplot as plt
# torch.manual_seed(1) # reproducible
LR = 0.01
BATCH_SIZE = 32
EPOCH = 12
# dummy dataset
x = torch.unsqueeze(torch.linspace(-1, 1, 1000), dim=1)
y = x.pow(2) + 0.1*torch.normal(torch.zeros(*x.size()))
# plot dataset
plt.scatter(x.numpy(), y.numpy())
plt.show()
将数据集放入 torch 数据集:
torch_dataset = Data.TensorDataset(x, y)
loader = Data.DataLoader(dataset=torch_dataset, batch_size=BATCH_SIZE, shuffle=True, num_workers=2,)
# default network
class Net(torch.nn.Module):
def __init__(self):
super(Net, self).__init__()
self.hidden = torch.nn.Linear(1, 20) # hidden layer
self.predict = torch.nn.Linear(20, 1) # output layer
def forward(self, x):
x = F.relu(self.hidden(x)) # activation function for hidden layer
x = self.predict(x) # linear output
return x
if __name__ == '__main__':
# different nets
net_SGD = Net()
net_Momentum = Net()
net_RMSprop = Net()
net_Adam = Net()
nets = [net_SGD, net_Momentum, net_RMSprop, net_Adam]
# different optimizers
opt_SGD = torch.optim.SGD(net_SGD.parameters(), lr=LR)
opt_Momentum = torch.optim.SGD(net_Momentum.parameters(), lr=LR, momentum=0.8)
opt_RMSprop = torch.optim.RMSprop(net_RMSprop.parameters(), lr=LR, alpha=0.9)
opt_Adam = torch.optim.Adam(net_Adam.parameters(), lr=LR, betas=(0.9, 0.99))
optimizers = [opt_SGD, opt_Momentum, opt_RMSprop, opt_Adam]
loss_func = torch.nn.MSELoss()
losses_his = [[], [], [], []] # record loss
训练模型并进行多个周期:
for epoch in range(EPOCH):
print('Epoch: ', epoch)
for step, (b_x, b_y) in enumerate(loader): # for each training step
for net, opt, l_his in zip(nets, optimizers, losses_his):
output = net(b_x) # get output for every net
loss = loss_func(output, b_y) # compute loss for every net
opt.zero_grad() # clear gradients for next train
loss.backward() # backpropagation, compute gradients
opt.step() # apply gradients
l_his.append(loss.data.numpy()) # loss recoder
labels = ['SGD', 'Momentum', 'RMSprop', 'Adam']
for i, l_his in enumerate(losses_his):
plt.plot(l_his, label=labels[i])
plt.legend(loc='best')
plt.xlabel('Steps')
plt.ylabel('Loss')
plt.ylim((0, 0.2))
plt.show()
执行上述代码块的输出显示在以下图表中:
Epoch 计数的输出将如下所示:
Epoch: 0
Epoch: 1
Epoch: 2
Epoch: 3
Epoch: 4
Epoch: 5
Epoch: 6
Epoch: 7
Epoch: 8
Epoch: 9
Epoch: 10
Epoch: 11
我们将绘制所有优化器,并将它们表示在图表中,如下所示:
在下一部分,我们将讨论 RNN。
循环神经网络
使用 RNN 时,与前馈神经网络不同,我们可以利用内部记忆按顺序处理输入。在 RNN 中,节点之间的连接沿时间序列形成一个有向图。这有助于将任务分配给 RNN,处理大量未分割且互相关联的语音或字符识别。
MNIST 数据库
MNIST 数据库包含 60,000 个手写数字。此外,还有一个由 10,000 个数字组成的测试数据集。虽然它是 NIST 数据集的一个子集,但该数据集中的所有数字都进行了大小标准化,并且已经居中在一个 28 x 28 像素的图像中。这里,每个像素的值为 0-255,表示其灰度值。
MNIST 数据集可以在 yann.lecun.com/exdb/mnist/
找到。
NIST 数据集可以在 www.nist.gov/srd/nist-special-database-19
找到。
RNN 分类
在这里,我们将看一个例子,展示如何构建一个 RNN 来识别 MNIST 数据库中的手写数字:
import torch
from torch import nn
import torchvision.datasets as dsets
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
# torch.manual_seed(1) # reproducible
# Hyper Parameters
EPOCH = 1 # train the training data n times, to save time, we just train 1 epoch
BATCH_SIZE = 64
TIME_STEP = 28 # rnn time step / image height
INPUT_SIZE = 28 # rnn input size / image width
LR = 0.01 # learning rate
DOWNLOAD_MNIST = True # set to True if haven't download the data
# Mnist digital dataset
train_data = dsets.MNIST(
root='./mnist/',
train=True, # this is training data
transform=transforms.ToTensor(), # Converts a PIL.Image or numpy.ndarray to
# torch.FloatTensor of shape (C x H x W) and normalize in the range [0.0, 1.0]
download=DOWNLOAD_MNIST, # download it if you don't have it
)
绘制一个示例:
print(train_data.train_data.size()) # (60000, 28, 28)
print(train_data.train_labels.size()) # (60000)
plt.imshow(train_data.train_data[0].numpy(), cmap='gray')
plt.title('%i' % train_data.train_labels[0])
plt.show()
# Data Loader for easy mini-batch return in training
train_loader = torch.utils.data.DataLoader(dataset=train_data, batch_size=BATCH_SIZE, shuffle=True)
将测试数据转换为变量,选择 2000 个样本加速测试:
test_data = dsets.MNIST(root='./mnist/', train=False, transform=transforms.ToTensor())
test_x = test_data.test_data.type(torch.FloatTensor)[:2000]/255\. # shape (2000, 28, 28) value in range(0,1)
test_y = test_data.test_labels.numpy()[:2000] # covert to numpy array
class RNN(nn.Module):
def __init__(self):
super(RNN, self).__init__()
self.rnn = nn.LSTM( # if use nn.RNN(), it hardly learns
input_size=INPUT_SIZE,
hidden_size=64, # rnn hidden unit
num_layers=1, # number of rnn layer
batch_first=True, # input & output will has batch size as 1s dimension. e.g. (batch, time_step, input_size)
)
self.out = nn.Linear(64, 10)
def forward(self, x):
# x shape (batch, time_step, input_size)
# r_out shape (batch, time_step, output_size)
# h_n shape (n_layers, batch, hidden_size)
# h_c shape (n_layers, batch, hidden_size)
r_out, (h_n, h_c) = self.rnn(x, None) # None represents zero initial hidden state
# choose r_out at the last time step
out = self.out(r_out[:, -1, :])
return out
rnn = RNN()
print(rnn)
optimizer = torch.optim.Adam(rnn.parameters(), lr=LR) # optimize all cnn parameters
loss_func = nn.CrossEntropyLoss() # the target label is not one-hotted
训练和测试不同的 Epoch:
for epoch in range(EPOCH):
for step, (b_x, b_y) in enumerate(train_loader): # gives batch data
b_x = b_x.view(-1, 28, 28) # reshape x to (batch, time_step, input_size)
output = rnn(b_x) # rnn output
loss = loss_func(output, b_y) # cross entropy loss
optimizer.zero_grad() # clear gradients for this training step
loss.backward() # backpropagation, compute gradients
optimizer.step() # apply gradients
if step % 50 == 0:
test_output = rnn(test_x) # (samples, time_step, input_size)
pred_y = torch.max(test_output, 1)[1].data.numpy()
accuracy = float((pred_y == test_y).astype(int).sum()) / float(test_y.size)
print('Epoch: ', epoch, '| train loss: %.4f' % loss.data.numpy(), '| test accuracy: %.2f' % accuracy)
# print 10 predictions from test data
test_output = rnn(test_x[:10].view(-1, 28, 28))
pred_y = torch.max(test_output, 1)[1].data.numpy()
print(pred_y, 'prediction number')
print(test_y[:10], 'real number')
需要下载并解压以下文件以训练图像:
Downloading http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz to ./mnist/MNIST/raw/train-images-idx3-ubyte.gz
100.1%
Extracting ./mnist/MNIST/raw/train-images-idx3-ubyte.gz
Downloading http://yann.lecun.com/exdb/mnist/train-labels-idx1-ubyte.gz to ./mnist/MNIST/raw/train-labels-idx1-ubyte.gz
113.5%
Extracting ./mnist/MNIST/raw/train-labels-idx1-ubyte.gz
Downloading http://yann.lecun.com/exdb/mnist/t10k-images-idx3-ubyte.gz to ./mnist/MNIST/raw/t10k-images-idx3-ubyte.gz
100.4%
Extracting ./mnist/MNIST/raw/t10k-images-idx3-ubyte.gz
Downloading http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz to ./mnist/MNIST/raw/t10k-labels-idx1-ubyte.gz
180.4%
Extracting ./mnist/MNIST/raw/t10k-labels-idx1-ubyte.gz
Processing...
Done!
torch.Size([60000, 28, 28])
torch.Size([60000])
/usr/local/lib/python3.7/site-packages/torchvision/datasets/mnist.py:53: UserWarning: train_data has been renamed data
warnings.warn("train_data has been renamed data")
/usr/local/lib/python3.7/site-packages/torchvision/datasets/mnist.py:43: UserWarning: train_labels has been renamed targets
warnings.warn("train_labels has been renamed targets")
上述代码的输出结果如下:
让我们通过以下代码进一步处理:
/usr/local/lib/python3.7/site-packages/torchvision/datasets/mnist.py:58: UserWarning: test_data has been renamed data
warnings.warn("test_data has been renamed data")
/usr/local/lib/python3.7/site-packages/torchvision/datasets/mnist.py:48: UserWarning: test_labels has been renamed targets
warnings.warn("test_labels has been renamed targets")
RNN(
(rnn): LSTM(28, 64, batch_first=True)
(out): Linear(in_features=64, out_features=10, bias=True)
)
Epoch 输出结果如下:
Epoch: 0 | train loss: 2.3156 | test accuracy: 0.12
Epoch: 0 | train loss: 1.1875 | test accuracy: 0.57
Epoch: 0 | train loss: 0.7739 | test accuracy: 0.68
Epoch: 0 | train loss: 0.8689 | test accuracy: 0.73
Epoch: 0 | train loss: 0.5322 | test accuracy: 0.83
Epoch: 0 | train loss: 0.3657 | test accuracy: 0.83
Epoch: 0 | train loss: 0.2960 | test accuracy: 0.88
Epoch: 0 | train loss: 0.3869 | test accuracy: 0.90
Epoch: 0 | train loss: 0.1694 | test accuracy: 0.92
Epoch: 0 | train loss: 0.0869 | test accuracy: 0.93
Epoch: 0 | train loss: 0.2825 | test accuracy: 0.91
Epoch: 0 | train loss: 0.2392 | test accuracy: 0.94
Epoch: 0 | train loss: 0.0994 | test accuracy: 0.91
Epoch: 0 | train loss: 0.3731 | test accuracy: 0.94
Epoch: 0 | train loss: 0.0959 | test accuracy: 0.94
Epoch: 0 | train loss: 0.1991 | test accuracy: 0.95
Epoch: 0 | train loss: 0.0711 | test accuracy: 0.94
Epoch: 0 | train loss: 0.2882 | test accuracy: 0.96
Epoch: 0 | train loss: 0.4420 | test accuracy: 0.95
[7 2 1 0 4 1 4 9 5 9] prediction number
[7 2 1 0 4 1 4 9 5 9] real number
RNN 循环神经网络 – 回归
现在,我们将处理一个基于 RNN 的回归问题。循环神经网络为神经网络提供了记忆功能。对于序列数据,循环神经网络可以实现更好的效果。在这个例子中,我们将使用 RNN 来预测时间序列数据。
要了解更多关于循环神经网络的信息,请访问 iopscience.iop.org/article/10.1209/0295-5075/18/3/003/meta
。
以下代码用于逻辑回归:
%matplotlib inline
import torch
from torch import nn
import numpy as np
import matplotlib.pyplot as plt
# torch.manual_seed(1) # reproducible
# Hyper Parameters
TIME_STEP = 10 # rnn time step
INPUT_SIZE = 1 # rnn input size
LR = 0.02 # learning rate
# show data
steps = np.linspace(0, np.pi*2, 100, dtype=np.float32) # float32 for converting torch FloatTensor
x_np = np.sin(steps)
y_np = np.cos(steps)
plt.plot(steps, y_np, 'r-', label='target (cos)')
plt.plot(steps, x_np, 'b-', label='input (sin)')
plt.legend(loc='best')
plt.show()
RNN
类在以下代码中定义。我们将以线性方式使用 r_out
计算预测输出。我们也可以使用 for
循环与 torch.stack
来计算预测输出:
class RNN(nn.Module):
def __init__(self):
super(RNN, self).__init__()
self.rnn = nn.RNN(
input_size=INPUT_SIZE,
hidden_size=32, # rnn hidden unit
num_layers=1, # number of rnn layer
batch_first=True, # input & output will have batch size as 1s dimension. e.g. (batch, time_step, input_size)
)
self.out = nn.Linear(32, 1)
def forward(self, x, h_state):
# x (batch, time_step, input_size)
# h_state (n_layers, batch, hidden_size)
# r_out (batch, time_step, hidden_size)
r_out, h_state = self.rnn(x, h_state)
outs = [] # save all predictions
for time_step in range(r_out.size(1)): outs.append(self.out(r_out[:, time_step, :]))
return torch.stack(outs, dim=1), h_state
//instantiate RNN
rnn = RNN()
print(rnn)
输出结果如下:
"""
RNN (
(rnn): RNN(1, 32, batch_first=True)
(out): Linear (32 -> 1)
)
"""
我们现在需要优化 RNN 参数,如下代码所示,在运行 for
循环以进行预测之前:
optimizer = torch.optim.Adam(rnn.parameters(), lr=LR)
loss_func = nn.MSELoss()
h_state = None
plt.figure(1, figsize=(12, 5))
plt.ion()
以下代码块运行时会呈现动态效果,但在本书中无法展示。我们添加了一些截图帮助你理解这一效果。我们使用 x
作为输入的 sin
值,y
作为输出的拟合 cos
值。由于这两条曲线之间存在关系,我们将使用 sin
来预测 cos
:
for step in range(100):
start, end = step * np.pi, (step+1)*np.pi # time range
# use sin predicts cos
steps = np.linspace(start, end, TIME_STEP, dtype=np.float32, endpoint=False) # float32 for converting torch FloatTensor
x_np = np.sin(steps)
y_np = np.cos(steps)
x = torch.from_numpy(x_np[np.newaxis, :, np.newaxis]) # shape (batch, time_step, input_size)
y = torch.from_numpy(y_np[np.newaxis, :, np.newaxis])
prediction, h_state = rnn(x, h_state) # rnn output
h_state = h_state.data # repack the hidden state, break the connection from last iteration
loss = loss_func(prediction, y) # calculate loss
optimizer.zero_grad() # clear gradients for this training step
loss.backward() # backpropagation, compute gradients
optimizer.step() # apply gradients
绘制结果:
plt.plot(steps, y_np.flatten(), 'r-')
plt.plot(steps, prediction.data.numpy().flatten(), 'b-')
plt.draw(); plt.pause(0.05)
plt.ioff()
plt.show()
前述代码的输出如下:
以下是第 10 次迭代后生成的图形:
以下是第 25 次迭代后生成的图形:
我们不会在这里展示所有 100 次迭代的输出图像,而是直接跳到最终的输出,即第 100 次迭代,如下截图所示:
在接下来的部分,我们将探讨自然语言处理(NLP)。
自然语言处理
现在是时候利用 PyTorch 尝试一些自然语言处理技术了。这对那些之前没有在任何深度学习框架中编写代码的人特别有用,尤其是那些对 NLP 核心问题和算法有更好理解的人。
在这一章中,我们将通过简单的小维度示例来观察神经网络训练过程中层权重的变化。一旦你理解了网络的工作原理,就可以尝试自己的模型。
在处理任何基于 NLP 的问题之前,我们需要理解深度学习的基本构件,包括仿射映射、非线性和目标函数。
仿射映射
仿射映射 是深度学习的基本构件之一,如下所示:
在这种情况下,矩阵由 A 表示,向量由 x 和 b 表示。A 和 b 是需要学习的参数,而 b 是偏置。
解释这个的简单示例如下:
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
torch.manual_seed(1)
lin = nn.Linear(6, 3) # maps from R⁶ to R³, parameters A, b
# data is 2x5\. A maps from 6 to 3... can we map "data" under A?
data = torch.randn(2, 6)
print(lin(data)
之后,使用以下命令运行程序:
$ python3 torch_nlp.py
输出将如下所示:
tensor([[ 1.1105, -0.1102, -0.3235],
[ 0.4800, 0.1633, -0.2515]], grad_fn=<AddmmBackward>)
非线性
首先,我们需要明确为什么我们需要非线性。考虑我们有两个仿射映射:f(x)=Ax+b
和 g(x)=Cx+d
。f(g(x))
如下所示:
在这里,我们可以看到,当仿射映射组合在一起时,结果仍然是一个仿射映射,其中 Ad+b 是一个向量,AC 是一个矩阵。
我们可以将神经网络视为一系列仿射组合。以前,仿射层之间可能引入了非线性。但是幸运的是,现在已经不是这样了,这有助于构建更强大且高效的模型。
在使用最常见的函数(如 tanh (x)、σ(x) 和 ReLU (x))时,我们看到有一些核心的非线性,如下所示的代码块所示:
#let's see more about non-linearities
#Most of the non-linearities in PyTorch are present in torch.functional which we import as F)
# Please make a note that unlike affine maps, there are mostly no parameters in non-linearites
# That is, they don't have weights that are updated during training.
#This means that during training the weights are not updated.
data = torch.randn(2, 2)
print(data)
print(F.relu(data))
上述代码的输出如下:
tensor([[ 0.5848, 0.2149],
[-0.4090, -0.1663]])
tensor([[0.5848, 0.2149],
[0.0000, 0.0000]])
目标函数
目标函数(也称为损失函数或代价函数)将帮助您的网络进行最小化。它通过选择一个训练实例,将其传递通过神经网络,然后计算输出的损失来工作。
损失函数的导数会被更新,以找到模型的参数。例如,如果模型自信地预测了一个答案,而答案结果是错误的,那么计算出的损失将会很高。如果预测答案正确,那么损失较低。
网络如何最小化?
-
首先,函数将选择一个训练实例
-
然后,它通过我们的神经网络传递以获得输出
-
最后,输出的损失被计算出来
在我们的训练示例中,我们需要最小化损失函数,以减少使用实际数据集时错误结果的概率。
在 PyTorch 中构建网络组件
在将注意力转向 NLP 之前,在本节中我们将使用非线性激活函数和仿射映射在 PyTorch 中构建一个网络。在这个例子中,我们将学习如何使用 PyTorch 内置的负对数似然(negative log likelihood)来计算损失函数,并使用反向传播更新参数。
请注意,网络的所有组件需要继承自nn.Module
,并且还需要重写forward()
方法。考虑到样板代码,这些是我们应该记住的细节。当我们从nn.Module
继承这些组件时,网络组件会提供相应的功能。
现在,如前所述,我们将看一个例子,其中网络接收一个稀疏的词袋(BoW)表示,输出是一个概率分布到两个标签,即英语和西班牙语。同时,这个模型是逻辑回归的一个例子。
使用逻辑回归的 BoW 分类器
概率将被记录在我们的两个标签“英语”和“西班牙语”上,我们生成的模型将映射一个稀疏的 BoW 表示。在词汇表中,我们会为每个词分配一个索引。假设在我们的词汇表中有两个词,即 hello 和 world,它们的索引分别是零和一。例如,对于句子 hello hello hello hello hello, 其 BoW 向量是 [5,0]。类似地,hello world world hello world 的 BoW 向量是 [2,3],以此类推。
通常,它是 [Count(hello), Count(world)]。
让我们将 BOW 向量表示为 x.
网络的输出如下:
接下来,我们需要通过仿射映射传递输入,然后使用 log softmax:
data = [("El que lee mucho y anda mucho, ve mucho y sabe mucho".split(), "SPANISH"),
("The one who reads a lot and walks a lot, sees a lot and knows a lot.".split(), "ENGLISH"),
("Nunca es tarde si la dicha es buena".split(), "SPANISH"),
("It is never late if the joy is good".split(), "ENGLISH")]
test_data = [("Que cada palo aguante su vela".split(), "SPANISH"),
("May every mast hold its own sail".split(), "ENGLISH")]
#each word in the vocabulary is mapped to an unique integer using word_to_ix, and that will be considered as that word's index in BOW
word_to_ix = {}
for sent, _ in data + test_data:
for word in sent:
if word not in word_to_ix:
word_to_ix[word] = len(word_to_ix)
print(word_to_ix)
VOCAB_SIZE = len(word_to_ix)
NUM_LABELS = 2
class BoWClassifier(nn.Module): # inheriting from nn.Module!
def __init__(self, num_labels, vocab_size):
#This calls the init function of nn.Module. The syntax might confuse you, but don't be confused. Remember to do it in nn.module
super(BoWClassifier, self).__init__()
接下来,我们将定义所需的参数。在这里,这些参数是A
和B
,以下代码块解释了进一步所需的实现:
# let's look at the prarmeters required for affine mapping
# nn.Linear() is defined using Torch that gives us the affine maps.
#We need to ensure that we understand why the input dimension is vocab_size
# num_labels is the output
self.linear = nn.Linear(vocab_size, num_labels)
# Important thing to remember: parameters are not present in the non-linearity log softmax. So, let's now think about that.
def forward(self, bow_vec):
#first, the input is passed through the linear layer
#then it is passed through log_softmax
#torch.nn.functional contains other non-linearities and many other fuctions
return F.log_softmax(self.linear(bow_vec), dim=1)
def make_bow_vector(sentence, word_to_ix):
vec = torch.zeros(len(word_to_ix))
for word in sentence:
vec[word_to_ix[word]] += 1
return vec.view(1, -1)
def make_target(label, label_to_ix):
return torch.LongTensor([label_to_ix[label]])
model = BoWClassifier(NUM_LABELS, VOCAB_SIZE)
现在,模型知道了自己的参数。第一个输出是A
,第二个是B
,如下所示:
#A component is assigned to a class variable in the __init__ function
# of a module, which was done with the line
# self.linear = nn.Linear(...)
# Then from the PyTorch devs, knowledge of the nn.linear's parameters #is stored by the module (here-BoW Classifier)
for param in model.parameters():
print(param)
#Pass a BoW vector for running the model
# the code is wrapped since we don't need to train it
torch.no_grad()
with torch.no_grad():
sample = data[0]
bow_vector = make_bow_vector(sample[0], word_to_ix)
log_probs = model(bow_vector)
print(log_probs)
上述代码的输出如下:
{'El': 0, 'que': 1, 'lee': 2, 'mucho': 3, 'y': 4, 'anda': 5, 'mucho,': 6, 've': 7, 'sabe': 8, 'The': 9, 'one': 10, 'who': 11, 'reads': 12, 'a': 13, 'lot': 14, 'and': 15, 'walks': 16, 'lot,': 17, 'sees': 18, 'knows': 19, 'lot.': 20, 'Nunca': 21, 'es': 22, 'tarde': 23, 'si': 24, 'la': 25, 'dicha': 26, 'buena': 27, 'It': 28, 'is': 29, 'never': 30, 'late': 31, 'if': 32, 'the': 33, 'joy': 34, 'good': 35, 'Que': 36, 'cada': 37, 'palo': 38, 'aguante': 39, 'su': 40, 'vela': 41, 'May': 42, 'every': 43, 'mast': 44, 'hold': 45, 'its': 46, 'own': 47, 'sail': 48}
Parameter containing:
tensor([[-0.0347, 0.1423, 0.1145, -0.0067, -0.0954, 0.0870, 0.0443, -0.0923,
0.0928, 0.0867, 0.1267, -0.0801, -0.0235, -0.0028, 0.0209, -0.1084,
-0.1014, 0.0777, -0.0335, 0.0698, 0.0081, 0.0469, 0.0314, 0.0519,
0.0708, -0.1323, 0.0719, -0.1004, -0.1078, 0.0087, -0.0243, 0.0839,
-0.0827, -0.1270, 0.1040, -0.0212, 0.0804, 0.0459, -0.1071, 0.0287,
0.0343, -0.0957, -0.0678, 0.0487, 0.0256, -0.0608, -0.0432, 0.1308,
-0.0264],
[ 0.0805, 0.0619, -0.0923, -0.1215, 0.1371, 0.0075, 0.0979, 0.0296,
0.0459, 0.1067, 0.1355, -0.0948, 0.0179, 0.1066, 0.1035, 0.0887,
-0.1034, -0.1029, -0.0864, 0.0179, 0.1424, -0.0902, 0.0761, -0.0791,
-0.1343, -0.0304, 0.0823, 0.1326, -0.0887, 0.0310, 0.1233, 0.0947,
0.0890, 0.1015, 0.0904, 0.0369, -0.0977, -0.1200, -0.0655, -0.0166,
-0.0876, 0.0523, 0.0442, -0.0323, 0.0549, 0.0462, 0.0872, 0.0962,
-0.0484]], requires_grad=True)
Parameter containing:
tensor([ 0.1396, -0.0165], requires_grad=True)
tensor([[-0.6171, -0.7755]])
我们得到了张量输出值。但是,正如我们从前面的代码中看到的,这些值与对数概率并不对应,无论哪个是English
,哪个对应的是单词Spanish
。我们需要训练模型,为此将这些值映射到对数概率是很重要的。
label_to_ix = {"SPANISH": 0, "ENGLISH": 1}
那么我们开始训练我们的模型吧。我们首先通过模型传递实例,得到这些对数概率。然后计算损失函数,损失函数计算完成后,我们计算该损失函数的梯度。最后,使用梯度更新参数。PyTorch 中的nn
包提供了损失函数。我们需要使用 nn.NLLLoss()作为负对数似然损失。优化函数也在torch.optim
中定义。
在这里,我们将使用随机梯度下降法(SGD):
# Pass the BoW vector for running the model
# the code is wrapped since we don't need to train it
torch.no_grad()
with torch.no_grad():
sample = data[0]
bow_vector = make_bow_vector(sample[0], word_to_ix)
log_probs = model(bow_vector)
print(log_probs)
# We will run this on data that can be tested temporarily, before training, just to check the before and after difference using touch.no_grad():
with torch.no_grad():
for instance, label in test_data:
bow_vec = make_bow_vector(instance, word_to_ix)
log_probs = model(bow_vec)
print(log_probs)
#The matrix column corresponding to "creo" is printed
print(next(model.parameters())[:, word_to_ix["mucho"]])
loss_function = nn.NLLLoss()
optimizer = optim.SGD(model.parameters(), lr=0.1)
我们不想毫无理由地一次次传递训练数据。实际数据集有多个实例,而不仅仅是 2 个。合理的做法是将模型训练在 5 到 30 个 epoch 之间。
以下代码展示了我们示例的范围:
for epoch in range(100):
for instance, label in data:
# Firstly, remember that gradients are accumulated by PyTorch
# It's important that we clear those gradients before each instance
model.zero_grad()
#The next step is to prepare our BOW vector and the target should be #wrapped in also we must wrap the target in a tensor in the form of an #integer
# For example, as considered above, if the target word is SPANISH, #then, the integer wrapped should be 0
#The loss function is already trained to understand that when the 0th element among the log probabilities is the one that is in accordance to SPANISH label
bow_vec = make_bow_vector(instance, word_to_ix)
target = make_target(label, label_to_ix)
# Next step is to run the forward pass
log_probs = model(bow_vec)
在这里,我们将通过调用函数 optimizer.step()来计算各种因素,如损失、梯度和更新参数:
loss = loss_function(log_probs, target)
loss.backward()
optimizer.step()
with torch.no_grad():
for instance, label in test_data:
bow_vec = make_bow_vector(instance, word_to_ix)
log_probs = model(bow_vec)
print(log_probs)
# After computing and the results, we see that the index that corresponds to Spanish has gone up, and for English is has gone down!
print(next(model.parameters())[:, word_to_ix["mucho"]])
输出如下:
tensor([[-0.7653, -0.6258]])
tensor([[-1.0456, -0.4331]])
tensor([-0.0071, -0.0462], grad_fn=<SelectBackward>)
tensor([[-0.1546, -1.9433]])
tensor([[-0.9623, -0.4813]])
tensor([ 0.4421, -0.4954], grad_fn=<SelectBackward>)
总结
现在,我们已经基本了解了如何使用 PyTorch 进行基于文本的处理。我们也更清楚了 RNN 是如何工作的,以及如何使用 PyTorch 解决与 NLP 相关的问题。
在接下来的章节中,我们将利用所学的神经网络和 NLP 知识构建应用程序。祝编码愉快!
第七章:使用 WaveNet 模型进行语音转文本的 TensorFlow 移动端实现
在本章中,我们将学习如何使用 WaveNet 模型将音频转换为文本。然后,我们将构建一个模型,使用 Android 应用程序将音频转换为文本。
本章基于 Aaron van den Oord、Sander Dieleman、Heiga Zen、Karen Simonyan、Oriol Vinyals、Alex Graves、Nal Kalchbrenner、Andrew Senior 和 Koray Kavukcuoglu 的论文《WaveNet: A Generative Model for Raw Audio》。您可以在arxiv.org/abs/1609.03499
找到这篇论文。
本章将涵盖以下主题:
-
WaveNet 及其工作原理
-
WaveNet 架构
-
使用 WaveNet 构建模型
-
数据集预处理
-
训练 WaveNet 网络
-
将语音 WAV 文件转换为英文文本
-
构建 Android 应用程序
让我们深入探讨 WaveNet 究竟是什么。
WaveNet
WaveNet 是一个深度生成网络,用于生成原始音频波形。WaveNet 通过模拟人声来生成声音波形。与当前任何现有的语音合成系统相比,这种生成的声音更为自然,缩小了系统与人类表现之间的差距达 50%。
使用单一 WaveNet,我们可以以相同的保真度区分多个说话者。我们还可以根据说话者的身份在不同的说话者之间切换。该模型是自回归的和概率性的,可以在每秒处理成千上万的音频样本时高效训练。单一 WaveNet 能够以相同的保真度捕捉多个不同说话者的特点,并且可以通过条件化说话者身份来在它们之间切换。
如电影《她》中所示,人机交互的长期梦想就是让人类与机器对话。近年来,随着深度神经网络(例如,Google 助手、Siri、Alexa 和 Cortana)的发展,计算机理解语音的能力大幅提高。另一方面,计算机生成语音的过程被称为语音合成或文本转语音。在文本转语音方法中,一个说话者录制大量短音频片段,然后将这些片段拼接成所需的语句。这一过程非常困难,因为我们不能更换说话者。
这一难题促使了对其他生成语音方法的巨大需求,其中生成数据所需的所有信息都存储在模型的参数中。此外,使用输入数据,我们可以控制语音的内容和各种属性。当通过拼接音频片段生成语音时,会生成归属图。
以下是生成的1 秒语音归属图:
以下是生成的100 毫秒语音归属图:
以下是生成10 毫秒语音的归因图:
以下是生成1 毫秒语音的归因图:
谷歌的像素递归神经网络(PixelRNN)和像素卷积神经网络(PixelCNN)模型确保能够生成包含复杂形态的图像——不是一次生成一个像素,而是同时生成整个颜色通道。在任何时刻,一个颜色通道至少需要对每个图像进行一千次预测。通过这种方式,我们可以将二维的 PixelNet 转换为一维的 WaveNet;这一思路如图所示:
上图展示了 WaveNet 模型的结构。WaveNet 是一个完整的卷积神经网络(CNN),其中卷积层包含多种扩张因子。这些因子帮助 WaveNet 的感受野随着深度的增加呈指数增长,并且有助于覆盖成千上万个时间步长。
在训练过程中,人工语音记录输入序列以创建波形。训练完成后,我们通过从网络中采样生成合成语音。每次采样时,从网络计算出的概率分布中选取一个值。接收到的值作为下一步的输入,然后进行新的预测。在每一步生成这些样本是非常昂贵的;然而,为了生成复杂且逼真的音频,这是必需的。
有关 PixelRNN 的更多信息,请访问arxiv.org/pdf/1601.06759.pdf
,而关于条件图像生成与 PixelCNN 解码器的信息请访问arxiv.org/pdf/1606.05328.pdf
。
架构
WaveNet 神经网络的架构通过生成音频和语音合成翻译展示了惊人的效果,因为它直接生成原始音频波形。
当输入包括之前的样本和额外的参数时,网络通过条件概率生成下一个音频样本,输出为音频波形。
作为输入的波形被量化为固定范围的整数。这发生在音频预处理之后。通过对这些整数幅度进行独热编码,生成张量。因此,卷积层仅访问当前和先前的输入,从而减少了通道的维度。
以下是 WaveNet 架构的示意图:
使用一堆因果扩张卷积层来构建网络核心。每一层都是一个带孔的扩张卷积层,仅访问过去和当前的音频样本。
然后,所有层接收到的输出被合并,并通过一系列密集的后处理层,输入到原始通道中。之后,softmax 函数将输出转换为分类分布。
损失函数是通过计算每个时间步的输出与下一个时间步输入之间的交叉熵来得到的。
WaveNet 中的网络层
在这里,我们将重点介绍生成过滤器大小为 2 的扩张因果卷积网络层。注意,这些概念也适用于更大的过滤器大小。
在此生成过程中,用于计算单个输出值的计算图可以看作是一个二叉树:
图中底层的输入节点是树的叶子节点,而输出层是根节点。中间的计算由输入层上方的节点表示。图中的边对应于多个矩阵。由于计算是二叉树结构,因此图的总体计算时间为O(2^L)。当L较大时,计算时间会呈指数级增长。
然而,由于该模型是随着时间反复应用的,因此存在大量的冗余计算,我们可以缓存这些计算以提高生成单个样本的速度。
关键的洞察是这样的——给定图中的某些节点,我们已经拥有计算当前输出所需的所有信息。我们通过类比 RNNs,称这些节点为递归状态。这些节点已经被计算过,因此我们所需要做的就是将它们缓存到不同的层中,如下图所示:
注意,在下一个时间点,我们将需要不同子集的递归状态。因此,我们需要在每一层缓存多个递归状态。我们需要保留的数量等于该层的扩张值,如下图所示:
如前图所示,带有箭头标记的地方,递归状态的数量与层中的扩张值相同。
算法的组件
构建语音检测器的算法包含两个组件:
-
生成模型
-
卷积队列
这两个组件在下图中展示:
生成模型可以视为 RNN 的一个步骤。它以当前观测值和若干递归状态作为输入,然后计算输出预测和新的递归状态。
卷积队列存储了由下面的层计算得到的新递归状态。
让我们开始构建模型。
构建模型
我们将使用 DeepMind 的 WaveNet 实现句子级别的英语语音识别。但是,在构建模型之前,我们需要考虑一些数据点:
-
首先,虽然 WaveNet 论文(在本章开头提供)使用了 TIMIT 数据集进行语音识别实验,但我们将使用免费的 VCTK 数据集代替。
-
其次,论文在扩张卷积层后添加了一个均值池化层进行降采样。我们已经从
.wav
文件中提取了梅尔频率倒谱系数(MFCC),并删除了最后的均值池化层,因为原始设置在我们的 TitanX 图形处理单元(GPU)上无法运行。 -
第三,由于 TIMIT 数据集具有音素标签,论文使用了两个损失项来训练模型:音素分类和下一个音素预测。相反,我们将使用单一的连接时序分类(CTC)损失,因为 VCTK 提供了句子级标签。因此,我们只使用扩张的 Conv1D 层,而没有任何扩张的 Conv1D 层。
-
最后,由于时间限制,我们不会进行定量分析,如双语评估下游评分(BLEU)得分以及通过结合语言模型的后处理。
依赖项
下面是所有需要首先安装的依赖库列表:
-
tensorflow
-
sugartensor
-
pandas
-
librosa
-
scikits.audiolab
如果你遇到librosa
库的问题,可以尝试使用pip
安装ffmpeg
。
数据集
我们使用了 VCTK、LibriSpeech 和 TED-LIUM 发布 2 的数据集。训练集中的句子总数由这三个数据集组成,总计 240,612 个句子。验证集和测试集仅使用 LibriSpeech 和 TED-LIUM 语料库构建,因为 VCTK 语料库没有验证集和测试集。下载每个语料库后,将它们提取到asset/data/VCTK-Corpus
、asset/data/LibriSpeech
和asset/data/TEDLIUM_release2
目录中。
你可以在这里找到这些数据集的链接:CSTR VCTK 语料库: homepages.inf.ed.ac.uk/jyamagis/page3/page58/page58.html
LibriSpeech ASR 语料库: www.openslr.org/12
TED-LIUM: www-lium.univ-lemans.fr/en/content/ted-lium-corpus
数据集预处理
TED-LIUM 发布 2 数据集提供的是 SPH 格式的音频数据,因此我们应该将其转换为librosa
库能够处理的格式。为此,请在asset/data
目录下运行以下命令,将 SPH 格式转换为 WAV 格式:
find -type f -name '*.sph' | awk '{printf "sox -t sph %s -b 16 -t wav %s\n", $0, $0".wav" }' | bash
如果你没有安装sox
,请先安装它。
我们发现训练中的主要瓶颈是磁盘读取时间,因为音频文件的大小。在处理之前,最好将音频文件缩小,以便更快地执行。因此,我们决定将所有音频数据预处理为 MFCC 特征文件,这些文件要小得多。此外,我们强烈建议使用固态硬盘(SSD),而不是传统硬盘。
在控制台中运行以下命令以预处理整个数据集:
python preprocess.py
使用处理过的音频文件,我们现在可以开始训练网络。
训练网络
我们将通过执行以下命令开始训练网络:
python train.py ( <== Use all available GPUs )
如果你正在使用启用了 CUDA 的机器,请使用以下命令:
CUDA_VISIBLE_DEVICES=0,1 python train.py ( <== Use only GPU 0, 1 )
你可以在asset/train
目录中看到生成的.ckpt
文件和日志文件。启动tensorboard--logdir asset/train/log
来监控训练过程。
我们已经在 3 台 Nvidia 1080 Pascal GPU 上训练了这个模型 40 小时,直到达到 50 个纪元,然后选择了验证损失最小的纪元。在我们的例子中,这是第 40 个纪元。如果你看到内存溢出的错误,请在train.py
文件中将batch_size
从 16 减少到 4。
每个纪元的 CTC 损失如下:
纪元 | 训练集 | 验证集 | 测试集 |
---|---|---|---|
20 | 79.541500 | 73.645237 | 83.607269 |
30 | 72.884180 | 69.738348 | 80.145867 |
40 | 69.948266 | 66.834316 | 77.316114 |
50 | 69.127240 | 67.639895 | 77.866674 |
在这里,你可以看到训练数据集和测试数据集的值之间的差异。这个差异主要是由于训练数据集的体积更大。
测试网络
在训练网络之后,你可以使用以下命令检查验证集或测试集的 CTC 损失:
python test.py --set train|valid|test --frac 1.0(0.01~1.0)
如果你只想测试数据集的一部分以进行快速评估,frac
选项将非常有用。
将一个语音 WAV 文件转换成英文文本
接下来,你可以通过执行以下命令将语音 WAV 文件转换为英文文本:
python recognize.py --file
这将把语音 WAV 文件转换为英文句子。
结果将会打印在控制台上;尝试以下命令作为示例:
python recognize.py --file asset/data/LibriSpeech/test-clean/1089/134686/1089-134686-0000.flac
python recognize.py --file asset/data/LibriSpeech/test-clean/1089/134686/1089-134686-0001.flac
python recognize.py --file asset/data/LibriSpeech/test-clean/1089/134686/1089-134686-0002.flac
python recognize.py --file asset/data/LibriSpeech/test-clean/1089/134686/1089-134686-0003.flac
python recognize.py --file asset/data/LibriSpeech/test-clean/1089/134686/1089-134686-0004.flac
结果如下:
他希望晚餐有炖菜,胡萝卜和萝卜,还有破损的土豆和肥羊肉块,放入浓厚的胡椒面调料中,一勺勺地倒进你肚子里。他劝告自己,在早早的夜幕降临后,黄色的灯光会在这里和那里亮起,肮脏的妓院区,嘿,伯蒂,脑袋里有好主意吗?10 号新鲜的内莉在等你,晚安,丈夫。
真实情况如下:
他希望晚餐有炖菜,胡萝卜和萝卜,还有破损的土豆和肥羊肉块,放入浓厚的胡椒面调料中,一勺勺地倒进你肚子里。他劝告自己,在早早的夜幕降临后,黄色的灯光会在这里和那里亮起,肮脏的妓院区,嘿,伯蒂,脑袋里有好主意吗?10 号新鲜的内莉在等你,晚安,丈夫。
正如我们之前提到的,由于没有语言模型,因此在某些情况下会出现大写字母和标点符号误用,或者单词拼写错误。
获取模型
与图像问题不同,找到一个提供检查点的语音转文本预训练深度学习模型并不容易。幸运的是,我找到了以下的 WaveNet 语音转文本实现。为了将模型导出以进行压缩,我运行了 Docker 镜像,加载了检查点,并将其写入了协议缓冲文件。运行此操作时,请使用以下命令:
python export_wave_pb.py
我们将为推理构建图,加载检查点,并将其写入协议缓冲文件,如下所示:
batch_size = 1 # batch size
voca_size = data.voca_size
x = tf.placeholder(dtype=tf.sg_floatx, shape=(batch_size, None, 20))
# sequence length except zero-padding
seq_len = tf.not_equal(x.sg_sum(axis=2), 0.).sg_int().sg_sum(axis=1)
# encode audio feature
logit = get_logit(x, voca_size)
# ctc decoding
decoded, _ = tf.nn.ctc_beam_search_decoder(logit.sg_transpose(perm=[1, 0, 2]), seq_len, merge_repeated=False)
# to dense tensor
y = tf.add(tf.sparse_to_dense(decoded[0].indices, decoded[0].dense_shape, decoded[0].values), 1, name="output")
with tf.Session() as sess:
tf.sg_init(sess)
saver = tf.train.Saver()
saver.restore(sess, tf.train.latest_checkpoint('asset/train'))
graph = tf.get_default_graph()
input_graph_def = graph.as_graph_def()
with tf.Session() as sess:
tf.sg_init(sess)
saver = tf.train.Saver()
saver.restore(sess, tf.train.latest_checkpoint('asset/train'))
# Output model's graph details for reference.
tf.train.write_graph(sess.graph_def, '/root/speech-to-text-wavenet/asset/train', 'graph.txt', as_text=True)
# Freeze the output graph.
output_graph_def = graph_util.convert_variables_to_constants(sess,input_graph_def,"output".split(","))
# Write it into .pb file.
with tfw.gfile.GFile("/root/speech-to-text-wavenet/asset/train/wavenet_model.pb", "wb") as f:
f.write(output_graph_def.SerializeToString())
接下来,我们需要构建一个 TensorFlow 模型并对其进行量化,以便在移动应用中使用。
使用 Bazel 构建 TensorFlow 并量化模型
要使用 TensorFlow 对模型进行量化,你需要安装 Bazel 并克隆 TensorFlow 仓库。我建议创建一个新的虚拟环境,在其中安装和构建 TensorFlow。完成后,你可以运行以下命令:
bazel build tensorflow/tools/graph_transforms:transform_graph
bazel-bin/tensorflow/tools/graph_transforms/transform_graph \
--in_graph=/your/.pb/file \
--outputs="output_node_name" \
--out_graph=/the/quantized/.pb/file \
--transforms='quantize_weights'
你可以查看 TensorFlow 网站上的官方量化教程,了解变换中的其他选项。量化后,模型大小减小了 75%,从 15.5 MB 降到 4 MB,原因是进行了 8 位转换。由于时间限制,我还没有通过测试集计算字母错误率,以量化量化前后准确率的下降。
关于神经网络量化的详细讨论,Pete Warden 写了一篇很好的文章,名为 使用 TensorFlow 进行神经网络量化 (petewarden.com/2016/05/03/how-to-quantize-neural-networks-with-tensorflow/
)。
请注意,你也可以按照本节中的说明进行完整的 8 位计算图转换。
经过此转换后,模型的大小减少到 5.9 MB,推理时间加倍。这可能是由于 8 位计算没有针对 macOS 平台上的 Intel i5 处理器进行优化,而该平台用于编写应用程序。
现在我们已经有了一个压缩的预训练模型,让我们看看在 Android 上部署该模型还需要做些什么。
TensorFlow 操作注册
在这里,我们将使用 Bazel 构建 TensorFlow 模型,创建一个 .so
文件,该文件可以通过 Java 原生接口(JNI)调用,并包括我们在预训练 WaveNet 模型推理中需要的所有操作库。我们将在 Android 应用程序中使用构建好的模型。
如需了解更多关于 Bazel 的信息,可以参考以下链接:docs.bazel.build/versions/master/bazel-overview.html
。
首先,通过取消注释并更新软件 开发 工具包(SDK)和原生开发工具包(NDK)的路径,编辑克隆的 TensorFlow 仓库中的 WORKSPACE 文件。
接下来,我们需要找出在预训练模型中使用了哪些操作,并生成一个包含这些信息的 .so
文件。
首先,运行以下命令:
bazel build tensorflow/python/tools:print_selective_registration_header && \
bazel-bin/tensorflow/python/tools/print_selective_registration_header \
--graphs=path/to/graph.pb > ops_to_register.h
所有.pb
文件中的操作将列在ops_to_register.h
中。
接下来,移动op_to_register.h
到/tensorflow/tensorflow/core/framework/
,并运行以下命令:
bazel build -c opt --copt="-DSELECTIVE_REGISTRATION" \
--copt="-DSUPPORT_SELECTIVE_REGISTRATION" \
//tensorflow/contrib/android:libtensorflow_inference.so \
--host_crosstool_top=@bazel_tools//tools/cpp:toolchain \
--crosstool_top=//external:android/crosstool --cpu=armeabi-v7a
不幸的是,虽然我没有收到任何错误消息,但是.so
文件仍然没有包含在头文件中列出的所有操作:
Modify BUILD in /tensorflow/tensorflow/core/kernels/
如果您尚未尝试第一个选项并且已经获取了模型中操作的列表,则可以通过使用tf.train.write_graph
命令并在终端中键入以下内容来获取操作:
grep "op: " PATH/TO/mygraph.txt | sort | uniq | sed -E 's/^.+"(.+)".?$/\1/g'
接下来,在 Android 库部分的android_extended_ops_group1
或android_extended_ops_group2
中添加缺失的操作,并使.so
文件更小化,删除任何不必要的操作。现在,运行以下命令:
bazel build -c opt //tensorflow/contrib/android:libtensorflow_inference.so \
--crosstool_top=//external:android/crosstool \
--host_crosstool_top=@bazel_tools//tools/cpp:toolchain \
--cpu=armeabi-v7a
您将在以下位置找到libtensorflow_inference.so
文件:
bazel-bin/tensorflow/contrib/android/libtensorflow_inference.so
请注意,在 Android 上运行此命令时,我们遇到了与sparse_to_dense
操作相关的错误。如果您希望重复此工作,请在第 153 行将REGISTER_KERNELS_ALL(int64);
添加到sparse_to_dense_op.cc
中,并重新编译。
除了.so
文件外,我们还需要一个 JAR 文件。您可以简单地将其添加到build.gradle
文件中,如下所示:
allprojects {
repositories {
jcenter()
}
}
dependencies {
compile 'org.tensorflow:tensorflow-android:+'
}
或者,您可以运行以下命令:
bazel build //tensorflow/contrib/android:android_tensorflow_inference_java
您会在以下代码块中找到该文件:
bazel-bin/tensorflow/contrib/android/libandroid_tensorflow_inference_java.jar
现在,将这两个文件移动到您的 Android 项目中。
构建一个 Android 应用程序
在本节中,我们将构建一个 Android 应用程序,将用户的语音输入转换为文本。基本上,我们将构建一个语音到文本的转换器。我们已经修改了 TensorFlow 语音示例,放置在 TensorFlow Android 演示库中以供本练习使用。
您可以在github.com/tensorflow/tensorflow/tree/master/tensorflow/examples/android
找到 TensorFlow Android 演示应用程序。
在演示的build.gradle
文件实际上帮助您构建.so
和 JAR 文件。因此,如果您想要使用自己的模型开始演示示例,您只需获取您的操作列表,修改 BUILD 文件,并让build.gradle
文件处理其余部分。我们将在以下部分详细介绍设置 Android 应用程序的细节。
要求
构建 Android 应用程序所需的要求如下:
-
TensorFlow 1.13
-
Python 3.7
-
NumPy 1.15
-
python-speech-features
TensorFlow 链接:github.com/tensorflow/tensorflow/releases
Python 链接:pip.pypa.io/en/stable/installing/
NumPy 链接:docs.scipy.org/doc/numpy-1.13.0/user/install.html
Python-speech-features 链接:github.com/jameslyons/python_speech_features
现在,让我们从头开始构建 Android 应用程序。在此应用程序中,我们将录制音频然后将其转换为文本。
根据您的操作系统设置 Android Studio,方法是访问以下链接:developer.android.com/studio/install
。本项目中使用的代码库已从此处提供的 TensorFlow 示例进行修改:github.com/tensorflow/tensorflow/tree/master/tensorflow/examples/android.
我们将使用 TensorFlow 示例应用程序并根据我们的需求进行编辑。
添加应用程序名称和公司域名,如下图所示:
下一步,选择目标 Android 设备的版本。我们将选择最低版本为 API 15:
接下来,我们将添加 Empty Activity 或 No Activity:
现在,让我们开始添加活动并使用生成的 TensorFlow 模型来获取结果。我们需要启用两个权限,以便在应用程序中使用它们,如下代码块所示:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.mlmobileapps.speech">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-sdk
android:minSdkVersion="25"
android:targetSdkVersion="25" />
<application android:allowBackup="true"
android:debuggable="true"
android:label="@string/app_name"
android:icon="@drawable/ic_launcher"
android:theme="@style/MaterialTheme">
<activity android:name="org.tensorflow.demo.SpeechActivity"
android:screenOrientation="portrait"
android:label="@string/activity_name_speech">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
我们将为应用程序提供一个最小的 UI,包含几个TextView
组件和一个Button
:
以下 XML 布局模仿了上面截图中的 UI:
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#6200EE"
tools:context="org.tensorflow.demo.SpeechActivity">
<LinearLayout
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_height="wrap_content">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:textColor="#fff"
android:layout_marginLeft="10dp"
android:layout_marginTop="30dp"
android:text="Talk within 5 seconds"
android:textAlignment="center"
android:textSize="24dp" />
<TextView
android:id="@+id/output_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#fff"
android:layout_gravity="top"
android:layout_marginLeft="10dp"
android:layout_marginTop="10dp"
android:textAlignment="center"
android:textSize="24dp" />
</LinearLayout>
<Button
android:id="@+id/start"
android:background="#ff0266"
android:textColor="#fff"
android:layout_width="wrap_content"
android:padding="20dp"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:layout_marginBottom="50dp"
android:text="Record Voice" />
</FrameLayout>
让我们添加语音识别活动的步骤,如下:
@Override
protected void onCreate(Bundle savedInstanceState) {
// Set up the UI.
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_speech);
startButton = (Button) findViewById(R.id.start);
startButton.setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View view) {
startRecording();
}
});
outputText = (TextView) findViewById(R.id.output_text);
// Load the Pretrained WaveNet model.
inferenceInterface = new TensorFlowInferenceInterface(getAssets(), MODEL_FILENAME);
requestMicrophonePermission();
}
请注意,我们这里不会讨论 Android 的基础知识。
接下来,我们将启动录音机,具体步骤如下:
public synchronized void startRecording() {
if (recordingThread != null) {
return;
}
shouldContinue = true;
recordingThread =
new Thread(
new Runnable() {
@Override
public void run() {
record();
}
});
recordingThread.start();
}
以下代码展示了record()
方法的实现:
private void record() {
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_AUDIO);
// Estimate the buffer size we'll need for this device.
int bufferSize =
AudioRecord.getMinBufferSize(
SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
if (bufferSize == AudioRecord.ERROR || bufferSize == AudioRecord.ERROR_BAD_VALUE) {
bufferSize = SAMPLE_RATE * 2;
}
short[] audioBuffer = new short[bufferSize / 2];
AudioRecord record =
new AudioRecord(
MediaRecorder.AudioSource.DEFAULT,
SAMPLE_RATE,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT,
bufferSize);
if (record.getState() != AudioRecord.STATE_INITIALIZED) {
Log.e(LOG_TAG, "Audio Record can't initialize!");
return;
}
record.startRecording();
Log.v(LOG_TAG, "Start recording");
while (shouldContinue) {
int numberRead = record.read(audioBuffer, 0, audioBuffer.length);
Log.v(LOG_TAG, "read: " + numberRead);
int maxLength = recordingBuffer.length;
recordingBufferLock.lock();
try {
if (recordingOffset + numberRead < maxLength) {
System.arraycopy(audioBuffer, 0, recordingBuffer, recordingOffset, numberRead);
} else {
shouldContinue = false;
}
recordingOffset += numberRead;
} finally {
recordingBufferLock.unlock();
}
}
record.stop();
record.release();
startRecognition();
}
以下代码展示了音频识别方法的实现:
public synchronized void startRecognition() {
if (recognitionThread != null) {
return;
}
shouldContinueRecognition = true;
recognitionThread =
new Thread(
new Runnable() {
@Override
public void run() {
recognize();
}
});
recognitionThread.start();
}
private void recognize() {
Log.v(LOG_TAG, "Start recognition");
short[] inputBuffer = new short[RECORDING_LENGTH];
double[] doubleInputBuffer = new double[RECORDING_LENGTH];
long[] outputScores = new long[157];
String[] outputScoresNames = new String[]{OUTPUT_SCORES_NAME};
recordingBufferLock.lock();
try {
int maxLength = recordingBuffer.length;
System.arraycopy(recordingBuffer, 0, inputBuffer, 0, maxLength);
} finally {
recordingBufferLock.unlock();
}
// We need to feed in float values between -1.0 and 1.0, so divide the
// signed 16-bit inputs.
for (int i = 0; i < RECORDING_LENGTH; ++i) {
doubleInputBuffer[i] = inputBuffer[i] / 32767.0;
}
//MFCC java library.
MFCC mfccConvert = new MFCC();
float[] mfccInput = mfccConvert.process(doubleInputBuffer);
Log.v(LOG_TAG, "MFCC Input======> " + Arrays.toString(mfccInput));
// Run the model.
inferenceInterface.feed(INPUT_DATA_NAME, mfccInput, 1, 157, 20);
inferenceInterface.run(outputScoresNames);
inferenceInterface.fetch(OUTPUT_SCORES_NAME, outputScores);
Log.v(LOG_TAG, "OUTPUT======> " + Arrays.toString(outputScores));
//Output the result.
String result = "";
for (int i = 0;i<outputScores.length;i++) {
if (outputScores[i] == 0)
break;
result += map[(int) outputScores[i]];
}
final String r = result;
this.runOnUiThread(new Runnable() {
@Override
public void run() {
outputText.setText(r);
}
});
Log.v(LOG_TAG, "End recognition: " +result);
}
模型通过TensorFlowInferenceInterface
类运行,如上面的代码所示。
一旦我们完成代码并成功运行,启动应用程序。
在第一次运行时,您需要允许应用程序使用手机的内部麦克风,如下图所示:
一旦我们授权使用麦克风,点击“录音”按钮并在 5 秒内输入语音。在以下截图中显示了使用印度口音输入how are you
关键字的两次尝试。对于美国和英国口音效果更好。
第一次尝试如下:
第二次尝试如下:
您应该尝试使用您自己的口音来获取正确的输出。这是开始构建您自己语音检测器的一个非常简单的方法,您可以进一步改进。
总结
在本章中,你学习了如何独立构建一个完整的语音检测器。我们详细讨论了 WaveNet 模型的工作原理。有了这个应用,我们可以使一个简单的语音转文本转换器运行;然而,要获得完美的结果,还需要做很多改进和更新。你也可以通过将模型转换为 CoreML,在 iOS 平台上构建相同的应用。
在下一章中,我们将继续构建一个手写数字分类器,使用修改版国家标准与技术研究所(MNIST)模型。
第八章:使用 GAN 识别手写数字
在本章中,我们将构建一个安卓应用程序,该程序通过使用对抗学习来检测手写数字并识别数字是什么。我们将使用修改后的国家标准与技术研究院 (MNIST) 数据集进行数字分类。我们还将了解生成对抗网络 (GANs)的基本知识。
在本章中,我们将更详细地探讨以下主题:
-
GAN 简介
-
理解 MNIST 数据库
-
构建 TensorFlow 模型
-
构建安卓应用程序
本应用程序的代码可以在github.com/intrepidkarthi/AImobileapps
找到。
GAN 简介
GAN 是一类机器学习 (ML) 算法,用于无监督学习。它们由两个深度神经网络组成,这两个网络相互竞争(因此被称为对抗式)。GAN 是由 Ian Goodfellow 和其他研究人员(包括 Yoshua Bengio)于 2014 年在蒙特利尔大学提出的。
Ian Goodfellow 关于 GAN 的论文可以在arxiv.org/abs/1406.2661
找到。
GAN 具有模拟任何数据的潜力。这意味着 GAN 可以被训练生成任何数据的相似版本,例如图像、音频或文本。下图展示了 GAN 的简单工作流程:
GAN 的工作流程将在接下来的章节中解释。
生成算法与判别算法
为了理解 GAN,我们必须知道判别算法和生成算法是如何工作的。判别算法试图预测一个标签并对输入数据进行分类,或者将它们归类到数据所属的类别中。另一方面,生成算法则尝试预测特征,以给出某个特定标签。
例如,一个判别算法可以预测一封邮件是否是垃圾邮件。这里,垃圾邮件是标签之一,邮件中提取的文本被认为是输入数据。如果将标签看作y,将输入看作x,我们可以将其表示如下:
另一方面,生成算法则尝试预测这些输入特征(在前面的公式中是x)的可能性。生成模型关注的是如何获得x,而判别模型关注的是x与y之间的关系。
以 MNIST 数据库为例,生成器将生成图像并传递给判别器。如果图像确实来自 MNIST 数据集,判别器将验证该图像。生成器生成图像的目的是希望它能够通过判别器的验证,即使它是假的(如上图所示)。
GAN 如何工作
根据我们的示例,我们将假设输入的是数字:
-
生成器接受随机数作为输入,并返回图像作为输出
-
输出图像传递给判别器,同时,判别器也从数据集中接收输入
-
判别器接受真实和假输入图像,并返回一个 0 到 1 之间的概率(其中 1 表示真实性的预测,0 表示假图像的预测)
使用本章中讨论的示例应用程序,我们可以使用相同的步骤,将用户手绘的图像作为假图像之一,并尝试找出其正确性的概率值。
理解 MNIST 数据库
MNIST 数据集包含 60,000 个手写数字。它还包含一个由 10,000 个数字组成的测试数据集。虽然它是 NIST 数据集的一个子集,但该数据集中的所有数字都进行了大小归一化,并且已被居中在一个 28 x 28 像素的图像中。在这里,每个像素包含一个 0-255 的值,对应于其灰度值。
MNIST 数据集可以在yann.lecun.com/exdb/mnist/
找到。NIST 数据集可以在www.nist.gov/srd/nist-special-database-19
找到。
构建 TensorFlow 模型
在这个应用程序中,我们将构建一个基于 MNIST 数据集的 TensorFlow 模型,并将在我们的 Android 应用程序中使用它。一旦我们有了 TensorFlow 模型,我们将把它转换成 TensorFlow Lite 模型。下载模型并构建 TensorFlow 模型的步骤如下:
这是我们的模型如何工作的架构图。实现这一点的方式如下所示:
使用 TensorFlow,我们可以通过一行 Python 代码下载 MNIST 数据,如下所示:
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
#Reading data
mnist = input_data.read_data_sets("./data/", one_hot-True)
现在,我们已经下载了 MNIST 数据集。之后,我们将按照之前的代码读取数据。
现在,我们可以运行脚本来下载数据集。我们将从控制台运行该脚本,如下所示:
> python mnist.py
Successfully downloaded train-images-idx3-ubyte.gz 9912422 bytes.
Extracting MNIST_data/train-images-idx3-ubyte.gz Successfully downloaded train-labels-idxl-ubyte.gz 28881 bytes.
Extracting MNIST_data/train -labels -idxl -ubyte.gz
Successfully downloaded tlOk -images -idx3 -ubyte.gz 1648877 bytes. Extracting MNIST_data/t1Ok -images -idx3 -ubyte.gz
Successfully downloaded tlOk -labels -idxl -ubyte.gz 4542 bytes. Extracting MNIST_data/t1Ok -labels -idxl -ubyte.gz
一旦我们准备好数据集,我们将添加一些将在应用程序中使用的变量,如下所示:
image_size = 28
labels_size = 10
learning_rate = 0.05
steps_number = 1000
batch size = 100
我们需要定义这些变量,以控制构建模型时所需的参数,这是 TensorFlow 框架要求的。这个分类过程很简单。28 x 28 图像中存在的像素数量是 784。因此,我们有相应数量的输入层。设置好架构后,我们将训练网络并评估结果,以了解模型的有效性和准确性。
现在,让我们定义在前面的代码块中添加的变量。根据模型处于训练阶段还是测试阶段,不同的数据将传递到分类器中。训练过程需要标签,以便能够将其与当前预测进行匹配。这个变量如下所定义:
#Define placeholders
training_data = tf.placeholder(tf.float32, [None, image_size*image_size])
labels = tf.placeholder(tf.float32, [None, labels_size])
随着计算图的评估进行,占位符将被填充。在训练过程中,我们调整偏置和权重的值,以提高结果的准确性。为了实现这一目标,我们将定义权重和偏置参数,如下所示:
#Variables to be tuned
W = tf.Variable(tf.truncated_normal([image_size*image_size, labels_size], stddev=0.1))
b = tf.Variable(tf.constant(0.1, shape-[labels_size]))
一旦我们有了可以调节的变量,我们就可以一步完成输出层的构建:
#Build the network
output = tf.matmul(training_data, W) + b
我们已经成功地使用训练数据构建了网络的输出层。
训练神经网络
通过优化损失,我们可以使训练过程有效。我们需要减少实际标签值与网络预测值之间的差异。定义这种损失的术语是交叉熵。
在 TensorFlow 中,交叉熵通过以下方法提供:
tf.nn.softmax_cross_entropy_with_logits
该方法将 softmax 应用于模型的预测。Softmax 类似于逻辑回归,输出一个介于 0 和 1.0 之间的小数。例如,电子邮件分类器的逻辑回归输出 0.9 表示邮件为垃圾邮件的概率为 90%,不为垃圾邮件的概率为 10%。所有概率的总和为 1.0,如下表所示:
Softmax 通过神经网络层实现,就在输出层之前。Softmax 层必须与输出层具有相同数量的节点。
损失使用tf.reduce_mean
方法定义,并且在训练步骤中使用GradientDescentOptimizer()
方法来最小化损失。如下所示的代码演示了这一过程:
#Defining the loss
loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels-labels, logits-output))
# Training step with gradient descent
train_step = tf.train.GradientDescentOptimizer(learning_rate).minimize(loss)
GradientDescentOptimizer
方法会通过调整输出中的w和b(权重和偏置参数)的值,经过多步迭代,直到我们减小损失并更接近准确的预测,具体如下:
# Accuracy calculation
correct_prediction = tf.equal(tf.argmax(output, 1), tf.argmax(labels, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
我们通过初始化会话和变量来开始训练,如下所示:
# Run the training
sess = tf.InteractiveSession() sess.run(tf.global_variables_initializer())
基于之前定义的步数(steps_number
)参数,算法将循环运行。然后我们将运行优化器,如下所示:
for i in range(steps_number):
# Get the next batch input_batch,
labels_batch = mnist.train.next_batch(batch_size)
feed_dict = {training_data: input_batch, labels: labels_batch}
# Run the training step
train_step.run(feed_dict=feed_dict)
使用 TensorFlow,我们可以衡量算法的准确性并打印准确率值。只要准确度提高并找到停止的阈值,我们可以继续优化,如下所示:
# Print the accuracy progress on the batch every 100 steps
if i%100 == 0:
train_accuracy = accuracy.eval(feed_dict=feed_dict)
print("Step %d, batch accuracy %g %%"%(i, train_accuracy*100))
一旦训练完成,我们可以评估网络的性能。我们可以使用训练数据来衡量性能,如下所示:
# Evaluate on the test set
test_accuracy = accuracy.eval(feed_dict=ftraining_data: mnist.test.images, labels: mnist.test.labels})
print("Test accuracy: %g %%"%(test_accuracy*100))
当我们运行 Python 脚本时,控制台上的输出如下:
Step 0, training batch accuracy 13 %
Step 100, training batch accuracy 80 %
Step 200, training batch accuracy 87 %
Step 300, training batch accuracy 81 %
Step 400, training batch accuracy 86 %
Step 500, training batch accuracy 85 %
Step 600, training batch accuracy 89 %
Step 700, training batch accuracy 90 %
Step 800, training batch accuracy 94 %
Step 900, training batch accuracy: 89.49 %
Test accuracy 91 %
现在,我们的准确率已经达到了 89.2%。当我们尝试进一步优化结果时,准确度反而下降;这就是我们设置阈值停止训练的原因。
让我们为 MNIST 数据集构建 TensorFlow 模型。在 TensorFlow 框架中,提供的脚本将 MNIST 数据集保存为 TensorFlow(.pb
)模型。相同的脚本附在本应用程序的代码库中。
此应用程序的代码可以在 github.com/intrepidkarthi/AImobileapps
找到。
我们将从以下 Python 代码行开始训练模型:
$:python mnist.py
现在我们将运行脚本生成我们的模型。
以下脚本帮助我们通过添加一些额外参数来导出模型:
python mnist.py --export_dir /./mnist_model
可以在时间戳目录 /./mnist_mode1/
下找到保存的模型(例如,/./mnist_model/1536628294/
)。
获得的 TensorFlow 模型将使用 toco
转换为 TensorFlow Lite 模型,如下所示:
toco \
--input_format=TENSORFLOW_GRAPHDEF
--output_format=TFLITE \
--output_file=./mnist.tflite \
--inference_type=FLOAT \
--input_type=FLOAT
--input_arrays=x \
--output_arrays=output
--input_shapes=1,28,28,1 \
--graph_def_file=./mnist.pb
Toco 是一个命令行工具,用于运行 TensorFlow Lite 优化转换器(TOCO),将 TensorFlow 模型转换为 TensorFlow Lite 模型。上述 toco
命令会生成 mnist.tflite
作为输出,我们将在下一节中在我们的应用程序中使用它。
构建 Android 应用程序
让我们按照我们建立的模型逐步创建 Android 应用程序。我们将从在 Android Studio 中创建一个新项目开始:
- 在 Android Studio 中创建一个新应用程序:
- 将创建的 TensorFlow Lite 模型拖到
assets
文件夹中,以及labels.txt
文件。我们将从 assets 文件夹中读取模型和标签:
前面的屏幕截图显示了项目中的文件结构。如果需要,我们也可以将模型文件存储在辅助存储器中。
FreeHandView 的一个优点是我们可以创建一个简单的视图,用户可以在其中绘制任意数量的数字。除此之外,屏幕上的条形图将显示检测到的数字的分类。
我们将使用逐步过程来创建分类器。
这是我们将用来绘制数字的 FreeHandView
构造方法。我们使用必要的参数初始化 Paint
对象,如下所示:
public FreeHandView(Context context, AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setDither(true);
mPaint.setColor(DEFAULT_COLOR);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeJoin(Paint.Join.ROUND);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setXfermode(null); mPaint.setAlpha(Oxff);
mEmboss = new EmbossMaskFilter(new float[] I1, 1, 1}, 0.4f, 6, 3.5f);
mBlur = new BlurMaskFilter(5, BlurMaskFilter.Blur.NORMAL);
}
上述代码块中每个参数的功能如下所述:
-
mPaint.setAntiAlias(true)
:setFlags()
的辅助函数,用于设置或清除ANTI_ALIAS_FLAG
位。抗锯齿会使所绘制内容的边缘更加平滑,但不影响形状的内部。 -
mPaint.setDither(true)
:setFlags()
的辅助函数,用于设置或清除DITHER_FLAG
位。抖动会影响高于设备精度的颜色如何被降低采样。 -
mPaint.setColor(DEFAULT_COLOR)
: 设置画笔的颜色。 -
mPaint.setStyle(Paint.Style.STROKE)
: 设置画笔的样式,用于控制如何解释基元的几何图形(除了drawBitmap
,它总是假定Fill
)。 -
mPaint.setStrokeJoin(Paint.Join.ROUND)
: 设置画笔的Join
。 -
mPaint.setStrokeCap(Paint.Cap.ROUND)
: 设置画笔的Cap
。 -
mPaint.setXfermode(null)
: 设置或清除传输模式对象。 -
mPaint.setAlpha(Oxff)
:一个辅助方法,用于setColor()
,它仅分配颜色的alpha
值,保持其r
、g
和b
值不变。
在视图生命周期的 init()
方法内部,我们将初始化 ImageClassifier
,并传入 BarChart
对象:
public void init(DisplayMetrics metrics, ImageClassifier classifier, BarChart barChart) {
int height = 1000;
int width = 1000;
mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
mCanvas = new Canvas(mBitmap);
currentColor = DEFAULT_COLOR; strokeWidth = BRUSH_SIZE;
mClassifier = classifier;
this.predictionBar = predictionBar;
this.barChart = barChart; addValuesToBarEntryLabels();
}
我们将使用以下库中的图表:github.com/PhilJay/MPAndroidChart
。
我们将初始化 BarChart
视图,x 轴包含从零到九的数字,y 轴包含从 0 到 1.0 的概率值:
BarChart barChart = (BarChart) findViewByld(R.id.barChart);
barChart.animateY(3000);
barChart.getXAxis().setEnabled(true);
barChart.getAxisRight().setEnabled(false);
barChart.getAxisLeft().setAxisMinimum(0.0f); // start at zero
barChart.getAxisLeft().setAxisMaximum(1.0f); // the axis maximum is 100
barChart.getDescription().setEnabled(false);
barChart.getLegend().setEnabled(false);
// the labels that should be drawn on the X-Axis final String[] barLabels = new String[]{"0", "1", "2", "3", "4", "5", "6", n7,1, 118n, n9,1};
//To format the value as integers
IAxisValueFormatter formatter = new IAxisValueFormatter() {
@Override public String getFormattedValue(float value, AxisBase axis) {
return barLabels(int) value); }
};
barChart.getXAxis().setGranularity(0f); // minimum axis-step (interval) is 1
barChart.getXAxis().setValueFormatter(formatter);
barChart.getXAxis().setPosition(XAxis.XAxisPosition.BOTTOM);
barChart.getXAxis().setTextSize(5f);
一旦我们初始化了 BarChart
视图,我们将调用视图生命周期中的 OnDraw()
方法,按照用户手指的移动路径绘制笔画。OnDraw()
方法是视图生命周期方法的一部分,一旦 BarChart
视图初始化完成,就会被调用。
在 OnDraw
方法中,我们将跟踪用户的手指移动,并将相同的动作绘制到画布上,如下所示:
@Override protected void onDraw(Canvas canvas) {
canvas.save();
mCanvas.drawColor(backgroundColor);
for (FingerPath fp : paths) {
mPaint.setColor(fp.color);
mPaint.setStrokeWidth(fp.strokeWidth);
mPaint.setMaskFilter(null);
if (fp.emboss)
mPaint.setMaskFilter(mEmboss);
else if (fp.blur)
mPaint.setMaskFilter(mBlur);
mCanvas.drawPath(fp.path, mPaint);
}
canvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint); canvas. restore();
}
在 onTouchEvent()
方法中,我们可以通过移动、抬起和按下事件跟踪用户的手指位置,并基于此触发相应的动作。这是视图生命周期中的一个方法,用于跟踪事件。当你触摸手机时,会根据手指的移动触发三个事件。在 action_down
和 action_move
的情况下,我们将处理事件,在视图上绘制手指的移动轨迹,使用初始的画笔对象属性。当 action_up
事件被触发时,我们会将视图保存为文件,并将文件图像传递给分类器识别数字。之后,我们将使用 BarChart
视图表示概率值。这些步骤如下:
@Override public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
BarData exampleData;
switch(event.getAction()) {
case MotionEvent.ACTION_DOWN :
touchStart(x, y);
invalidate();
break;
case MotionEvent.ACTION_MOVE :
touchMove(x, y);
invalidate();
break;
case MotionEvent.ACTION_UP :
touchUp();
Bitmap scaledBitmap = Bitmap.createScaledBitmap(mBitmap, mClassifier.getImageSizeX(), mClassifier.getImageSizeY(), true);
Random rng = new Random();
try {
File mFile;
mFile = this.getContext().getExternalFilesDir(String.valueOf (rng.nextLong() + ".png"));
FileOutputStream pngFile = new FileOutputStream(mFile);
}
catch (Exception e){ }
//scaledBitmap.compress(Bitmap.CompressFormat.PNG, 90, pngFile);
Float prediction = mClassifier.classifyFrame(scaledBitmap);
exampleData = updateBarEntry();
barChart.animateY(1000, Easing.EasingOption.EaseOutQuad);
XAxis xAxis = barChart.getXAxis();
xAxis.setValueFormatter(new IAxisValueFormatter() {
@Override public String getFormattedValue(float value, AxisBase axis) {
return xAxisLabel.get((int) value);
});
barChart.setData(exampleData);
exampleData.notifyDataChanged(); // let the data know a // dataset changed
barChart.notifyDataSetChanged(); // let the chart know it's // data changed
break;
}
return true;
}
在 ACTION_UP
动作中,有一个 updateBarEntry()
方法调用。在这里,我们调用分类器来获取结果的概率值。这个方法还会根据分类器的结果更新 BarChart
视图,如下所示:
public BarData updateBarEntry() {
ArrayList<BarEntry> mBarEntry = new ArrayList<>();
for (int j = 0; j < 10; ++j) {
mBarEntry.add(new BarEntry(j, mClassifier.getProbability(j)));
}
BarDataSet mBarDataSet = new BarDataSet(mBarEntry, "Projects");
mBarDataSet.setColors(ColorTemplate.COLORFUL_COLORS);
BarData mBardData = new BarData(mBarDataSet);
return mBardData;
}
FreeHandView 看起来像这样,并附有一个空的柱状图:
![通过这个,我们将添加一个模块来识别手写数字并进行分类。# 数字分类器现在,让我们编写分类器。1. 首先,我们将加载模型文件。这个方法从 assets 文件夹读取模型并将其加载到内存中:py/** Memory-map the model file in Assets. */ private MappedByteBuffer loadModelFile(Activity activity) throws I0Exception { AssetFileDescriptor fileDescriptor = activity.getAssets().openFd(getModelPath()); FilelnputStream 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); }
1. 现在,让我们逐步编写 TensorFlow Lite 分类器。这是我们从数字分类器获得结果的地方。一旦我们接收到保存的文件图像作为用户输入,位图将被转换为字节缓冲区,以便在模型上运行推理。一旦接收到输出,所花费的时间将通过 SystemClock
时间来记录:py/** Classifies a frame from the preview stream. */ public float classifyFrame(Bitmap bitmap) { if (tflite == null){ Log.e(TAG, "classifier has not been initialized; Skipped."); return 0.5f; } convertBitmapToByteBuffer(bitmap); // Here's where the classification happens!!! long startTime = SystemClock.uptimeMillis(); runlnference(); long endTime = SystemClock.uptimeMillis(); Log.d(TAG, "Timecost to run model inference: " + Long.toString(endTime - startTime)); return getProbability(0); }
1. runlnference()
方法调用了tflite
中的 run
方法,如下所示:py@Override protected void runlnference() { tflite.run(imgData, labelProbArray); }
1. 接下来,让我们从 MainActivity
启动应用程序,在此处初始化 barChart
视图。初始化 barChart
视图时需要设置 x 和 y 轴,并使用以下值:pyBARENTRY = new ArrayList<>(); initializeBARENTRY();Bardataset = new BarDataSet(BARENTRY, "project"); BARDATA = new BarData(Bardataset); barChart.setData(BARDATA);
1. 在 MainActivity
的 OnCreate()
方法中初始化 FreeHandView 以开始分类:pypaintView.init(metrics, classifier, barChart);
1. 当你达到 1.00 的概率值时,算法能够以 100% 的准确度识别该数字。以下是一个示例:
- 在某些情况下,分类会因为部分匹配而降低概率,以下截图展示了这种情况:
- 还有其他情况,概率会出现多个部分匹配。以下截图展示了这种情况:
任何此类情况都需要对模型进行更严格的训练。
- 点击 RESET 按钮将清除视图,以便你重新绘制。我们将使用以下代码行来实现此功能:
resetButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
paintView.clear();
}
}) ;
一旦点击 RESET 按钮,前面的代码会清除 FreeHandView 区域,如下所示:
你还可以通过写入数字以外的字符并检查条形图上的输出性能,来验证应用程序是否正常工作。
在本节中,我们学习了应用程序如何对手绘的不同数字进行分类,并提供这些数字是否正确的概率。
摘要
使用这个 Android 应用程序,我们可以学习如何使用 TensorFlow Lite 编写一个手写分类器。随着更多手写字母数据集的加入,我们应该能够使用 GANs 识别任何语言的字母。
在下一章中,我们将构建一个情感分析模型,并在其基础上构建一个应用程序。
第九章:使用 LinearSVC 进行文本情感分析
在这一章中,我们将构建一个 iOS 应用程序,通过用户输入对文本和图像进行情感分析。我们将使用现有的数据模型,这些模型是通过使用 LinearSVC 为同一目的构建的,并将这些模型转换为核心机器学习(ML)模型,以便在我们的应用程序中更易于使用。
情感分析是识别任何给定数据(如文本、图像、音频或视频)所激发的情感或观点的过程。情感分析有很多应用场景。即使现在,政党也能轻松识别出选民的普遍心态,他们还有潜力改变这一心态。
让我们来看看如何从现有的数据集中构建我们自己的情感分析 ML 模型。在这一章中,我们将探讨以下主题:
-
使用 scikit-learn 构建 ML 模型
-
线性支持向量分类(LinearSVC)
-
构建一个 iOS 应用程序
使用 scikit-learn 构建 ML 模型
在这一部分中,我们将构建我们自己的模型。有现成的数据集可供使用,这些数据集与 Twitter 动态数据相关,主题是产品和电影评论。你可以选择适合你自己的数据集;在这一章中,我们将选择一个包含客户评论的数据集。
一个包含正面和负面客户评论的数据集可以在boston.lti.cs.cmu.edu/classes/95-865-K/HW/HW3/
找到。你可以通过以下链接下载该数据集:boston.lti.cs.cmu.edu/classes/95-865-K/HW/HW3/epinions3.zip
。
上述数据集包含有关产品的正面和负面反馈,如下图所示:
我们将使用 scikit-learn 管道和 LinearSVC 来训练数据集。让我们更详细地了解这两者。
Scikit-learn
这是一个基于 NumPy、SciPy 和 Matplotlib 构建的数据挖掘和数据分析 Python 库。它有助于解决与分类、回归、聚类和降维相关的 ML 问题。
scikit-learn 管道
scikit-learn 管道的主要目的是将 ML 步骤组合在一起。可以通过交叉验证来设置各种参数。scikit-learn 提供了一系列转换器,用于数据预处理(数据清理)、核近似(扩展)、无监督的降维(减少)和特征提取(生成)。该管道包含一系列转换器,并最终产生一个估算器。
管道按顺序应用一系列变换,然后是最终的估算器。在管道中,fit
和 transform
方法在中间步骤中实现。fit
方法仅在管道操作的最后由最终估算器实现。为了缓存管道中的变换器,使用了 memory
参数。
用于分类的估算器是一个 Python 对象,它实现了方法的 fit (x, y) 和 predict (T) 值。例如,class sklearn.svm.SVC
实现了 SVC。模型的参数作为估算器构造函数的参数传递。scikit-learn 中的 memory
类具有 class sklearn.utils.Memory(*args, **kwargs)
签名。它有缓存、清除、减少、评估和格式化内存对象的方法。cache
方法用于计算函数的返回值。返回的对象是一个 MemorizedFunc
对象,它的行为类似于一个函数,并提供额外的方法用于缓存查找和管理。cache
方法接受诸如 func=None, ignore=None, verbose=None, mmap_mode=False
等参数。
class signature
管道如下:
class sklearn.pipeline.Pipeline(steps, memory=None)
接下来,让我们看看下一个重要组件。
LinearSVC
scikit-learn 库中的一个类是 LinearSVC,它支持稀疏和密集类型的输入。使用一对多方案处理多类支持。LinearSVC 类似于 SVC,其中参数为 kernel = linear
,但在 LinearSVC 中使用的是 liblinear
来实现该参数,而不是 SVC 中使用的 libvsm
。这为我们提供了更多选择惩罚和损失函数的灵活性,并且有助于更好地对大量样本进行缩放。
class
签名如下:
class sklearn.svm.LinearSVC(penalty=’l2’, loss=’squared_hinge’, dual=True, tol=0.0001, C=1.0, multi_class=’ovr’, fit_intercept=True, intercept_scaling=1, class_weight=None, verbose=0, random_state=None, max_iter=1000)
现在是时候开始构建我们的模型了,具体如下:
- 我们将从导入所有必要的库开始,具体如下:
import re
import coremltools
import pandas as pd
import numpy as np
from nltk.corpus import stopwords
from nltk import word_tokenize
from string import punctuation
from sklearn.feature_extraction import DictVectorizer
from sklearn.pipeline import Pipeline
from sklearn.svm import LinearSVC
from sklearn.model_selection import GridSearchCV
re
库是提供匹配操作的正则表达式库,使得处理文本数据变得更加容易。nltk
库用于根据我们的需求格式化文本,而 sklearn
提供了所需的机器学习工具。coremltools
库帮助我们将 sklearn
模型转换为 Core ML 模型。
- 现在,让我们开始读取输入,具体如下:
# Read reviews from CSV
reviews = pd.read_csv('epinions.csv')
reviews = reviews.as_matrix()[:, :]
print "%d reviews in dataset" % len(reviews)
上述代码读取了 CSV 文件,并将其转换为一个包含所有行和列的 numpy
数组。现在,我们已经准备好数据集,可以开始从数据中提取特征。
- 现在,让我们进行特征选择,具体如下:
# Create features
def features(sentence):
stop_words = stopwords.words('english') + list(punctuation)
words = word_tokenize(sentence)
words = [w.lower() for w in words]
filtered = [w for w in words if w not in stop_words and not
w.isdigit()]
words = {}
for word in filtered:
if word in words:
words[word] += 1.0
else:
words[word] = 1.0
return words
- 我们将从向量化
features
函数开始。然后,我们将提取 DataFrame 中每个句子的特征,并将它们存储在X
变量中。之后,我们将设置目标变量。目标变量将是输出。在我们的案例中,我们将为每个句子获取一个标签,指示其中的情感:
# Vectorize the features function
features = np.vectorize(features)
# Extract the features for the whole dataset
X = features(reviews[:, 1])
# Set the targets
y = reviews[:, 0]
- 在我们的例子中,我们将创建一个包含
DictVectorizer
和LinearSVC
的管道。DictVectorizer
,顾名思义,将字典转换为向量。我们选择了GridSearchCV
来从一系列通过参数网格化的模型中选择最佳模型:
# Do grid search
clf = Pipeline([("dct", DictVectorizer()), ("svc", LinearSVC())])
params = {
"svc__C": [1e15, 1e13, 1e11, 1e9, 1e7, 1e5, 1e3, 1e1, 1e-1, 1e-3,
1e-5]
}
gs = GridSearchCV(clf, params, cv=10, verbose=2, n_jobs=-1)
gs.fit(X, y)
model = gs.best_estimator_
- 然后,我们将打印出结果,如下所示:
# Print results
print model.score(X, y)
print "Optimized parameters: ", model
print "Best CV score: ", gs.best*score*
- 现在,我们可以将 scikit-learn 模型转换为
mlmodel
,如下所示:
# Convert to CoreML model
coreml_model = coremltools.converters.sklearn.convert(model)
coreml_model.short_description = 'Sentiment analysis AI projects.'
coreml_model.input_description['input'] = 'Features extracted from
the text.'
coreml_model.output_description['classLabel'] = 'The most likely polarity (positive or negative or neutral), for the given input.'
coreml_model.output_description['classProbability'] = 'The probabilities for each class label, for the given input.'
coreml_model.save('Sentiment.mlmodel')
一旦我们有了模型,就可以开始构建应用程序了。
构建 iOS 应用程序
让我们开始构建 iOS 应用程序,使用在上一阶段构建的模型。该模型将根据输入文本的性质(积极、中性或消极)预测输出。
要构建此应用程序,应使用 Xcode 版本 10.1:
- 创建一个新的项目,选择单视图应用,如以下截图所示:
-
在下一屏幕上提到我们的应用程序名称。
-
在下一个向导屏幕上,为你的应用程序选择一个合适的名称。
-
填写其余的字段,包括组织名称以及组织标识符。
-
我们在此应用程序中不使用核心数据,因此跳过该选项。
-
让我们从在 Xcode 中创建一个新应用开始。以下截图展示了如何在 Xcode 中创建一个新项目:
- 接下来,创建一个故事板,如以下截图所示:
- 一旦你选择了保存应用程序的文件位置,你将能够看到带有新应用程序信息的“常规”选项卡,如以下截图所示:
- 我们将创建一个简单的 UI,在底部放置一个按钮来显示情感:
//initialize Ui components
private lazy var textView: UITextView = self.makeTextView() private lazy var accessoryView = UIView()
private lazy var resultLabel: UILabel = self.makeResultLabel() private lazy var clearButton: UIButton = self.makeClearButton()
private let padding = CGFloat(16)
private var textViewBottomConstraint: NSLayoutConstraint?
- 我们将定义
sentiments
作为枚举类型,如下所示:
enum Sentiment {
case neutral
case positive
case negative
var emoji: String {
switch self {
case .neutral:
return ""
case .positive:
return ""
case .negative:
return ""
}
}
var color: UIColor? {
switch self {
case .neutral:
return UIColor(named: "NeutralColor")
case .positive:
return UIColor(named: "PositiveColor")
case .negative:
return UIColor(named: "NegativeColor")
}
}
}
- 让我们编写
ClassificationService
来获取我们构建的模型的结果:
//variables initialization
private let options: NSLinguisticTagger.Options = [.omitWhitespace, .omitPunctuation, .omitOther]
private lazy var tagger: NSLinguisticTagger = .init(
tagSchemes: NSLinguisticTagger.availableTagSchemes(forLanguage: "en"),
options: Int(self.options.rawValue)
)
private extension ClassificationService {
func features(from text: String) -> [String: Double] {
var wordCounts = [String: Double]()
tagger.string = text
let range = NSRange(location: 0, length: text.utf16.count)
// let's tokenize the input text and count the sentence
tagger.enumerateTags(in: range, scheme: .nameType, options: options) { _, tokenRange, _, _ in
let token = (text as NSString).substring(with: tokenRange).lowercased()
// Skip small words
guard token.count >= 3 else {
return
}
if let value = wordCounts[token] {
wordCounts[token] = value + 1.0
} else {
wordCounts[token] = 1.0
}
}
return wordCounts
}
- 输入被传递到
prediction
方法,以将语句过滤为positive
、negative
或neutral
情感:
func predictSentiment(from text: String) -> Sentiment {
do {
let inputFeatures = features(from: text)
// Make prediction only with 2 or more words
guard inputFeatures.count > 1 else {
throw Error.featuresMissing
}
let output = try model.prediction(input: inputFeatures)
switch output.classLabel {
case "Positive":
return .positive
case "Negative":
return .negative
default:
return .neutral
}
} catch {
return .neutral
}
}
}
- 让我们通过初始化
view
组件来编写ViewController
,如下所示:
override func viewDidLoad() {
super.viewDidLoad()
title = "Sentiment Analysis".uppercased()
view.backgroundColor = UIColor(named: "BackgroundColor")
view.addSubview(textView)
accessoryView.frame = CGRect(x: 0, y: 0, width: view.frame.size.width, height: 60)
accessoryView.addSubview(resultLabel)
accessoryView.addSubview(clearButton)
textView.inputAccessoryView = accessoryView
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardDidShow(notification:)),
name: .UIKeyboardDidShow,
object: nil
)
setupConstraints()
//Show default sentiment as neutral
show(sentiment: .neutral)
}
- 按钮和标签上的初始
setupConstraints
定义如下:
func setupConstraints() {
//input textview
textView.translatesAutoresizingMaskIntoConstraints = false
textView.topAnchor.constraint(equalTo: view.topAnchor, constant: 80).isActive = true
textView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: padding).isActive = true
textView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -padding).isActive = true
textViewBottomConstraint = textView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
textViewBottomConstraint?.isActive = true
//result label at the bottom
resultLabel.translatesAutoresizingMaskIntoConstraints = false
resultLabel.centerXAnchor.constraint(equalTo: accessoryView.centerXAnchor).isActive = true
resultLabel.centerYAnchor.constraint(equalTo: accessoryView.centerYAnchor).isActive = true
//Clear button at the bottom right
clearButton.translatesAutoresizingMaskIntoConstraints = false
clearButton.trailingAnchor.constraint(
equalTo: accessoryView.trailingAnchor,
constant: -padding
).isActive = true
clearButton.centerYAnchor.constraint(equalTo: accessoryView.centerYAnchor).isActive = true
}
- 定义
Show()
方法,如下所示:
private func show(sentiment: Sentiment) { accessoryView.backgroundColor = sentiment.color
resultLabel.text = sentiment.emoji
}
- 让我们在模拟器上运行应用程序。你可以在以下截图中看到输出:
- 现在,让我们使用不同的输入来运行我们的应用程序并获取输出,如下所示:
- 使用相同输出的消极输入示例语句显示在以下截图中:
- 使用中性文本的示例输入显示在以下截图中:
在这里,我们能够从给定的文本输入中获取情感。现在,你可以通过改进现有模型,进一步提升模型的效果。
你可以在多个来源中深入探索如何在图像上识别情感。一个示例应用是 Fine-tuning CNNs for Visual Sentiment Prediction。你可以在 github.com/imatge-upc/sentiment-2017-imavis
阅读关于这个应用的详细信息。
总结
到此为止,你应该能够构建自己的 TensorFlow 模型,并将其转换为 Core ML 模型,以便在 iOS 应用中使用。相同的 TensorFlow 模型可以转换为 TensorFlow Lite 模型,然后可以在 Android 应用或 iOS 应用中使用。现在,你可以开始这个任务并尝试不同的结果。话虽如此,我们现在准备进入下一章。
在下一章,你将使用本书中学到的知识,继续探索如何构建自己的应用。
第十章:下一步是什么?
计算机日新月异,设备的外形和功能发生了巨大变化。过去,我们只会在办公室看到计算机;然而现在,我们在家庭办公桌上、膝上、口袋里、甚至手腕上都能看到它们。随着机器不断融入更多智能,市场也变得越来越多样化。
目前几乎每个成年人都会随身携带一部设备,据估计我们每天至少会查看智能手机 50 次,无论是否有需要。这些设备影响着我们的日常决策过程。如今,设备上都配备了像 Siri、Google Assistant、Alexa 或 Cortana 这样的应用程序——这些功能旨在模仿人类的智能。能够回答任何提问的能力使得这些技术显得比人类更为强大。在这一过程中,这些系统通过使用所有用户获得的集体智能不断提升。你与虚拟助手互动的次数越多,它们提供的结果就会越好。
尽管取得了这些进展,我们距离通过机器真正创建一个人脑还差多远?我们现在已经是 2019 年了;如果科学能够找到控制大脑神经元的方法,这在不久的将来可能会成为现实。模拟人类能力的机器正在帮助解决复杂的文本、视觉和音频问题。它们与人脑每天执行的任务类似;举个例子,平均而言,人脑每天大约做出 35,000 个决策。
虽然我们将来能够模拟人脑,但这会付出相应的代价。目前我们还没有更便宜的解决方案。人脑模拟的功耗级别限制了与实际人脑相比的开发进程。人脑的功耗大约是 20 瓦,而模拟程序的功耗约为 1 兆瓦或更多。人脑中的神经元以 200 赫兹的速度工作,而典型的微处理器速度为 2 吉赫,约为人脑神经元速度的 1000 万倍。
虽然我们距离克隆人脑仍然遥远,但我们可以实现一种基于先前数据以及来自类似设备的数据做出决策的算法。这时,人工智能(AI)的子集就派上用场了。通过预定义的算法识别我们所拥有的复杂数据中的模式,这些类型的智能能够为我们提供有用的信息。
当计算机开始在没有每次明确指令的情况下做出决策时,我们就实现了机器学习(ML)功能。现在,机器学习无处不在,包括通过诸如识别电子邮件垃圾邮件、推荐电子商务网站上最适合购买的产品、自动在社交媒体照片中标记你的面孔等功能。所有这些都是通过识别历史数据中的模式来完成的,同时也通过减少数据中不必要的噪声并生成优质输出的算法来实现的。随着数据的不断积累,计算机可以做出更好的决策。
手机已经成为大多数今天正在生产的数字产品的默认消费媒介。随着数据消费的增加,我们必须尽可能快地将结果呈现给用户。例如,当你滚动浏览 Facebook 动态时,很多内容将基于你的兴趣以及你的朋友们喜欢的内容。由于用户在这些应用上花费的时间有限,因此服务器端和客户端有许多算法在运行,用以根据你在 Facebook 动态中偏好的内容加载和组织内容。如果所有的算法都能在本地设备上运行,我们就不需要依赖互联网来更快地加载内容。这只有通过在客户端设备上执行这些过程,而不是在云端处理,才能实现。
随着移动设备处理能力的提高,我们将被鼓励在移动设备本身上运行所有 ML 模型。现在,已经有很多服务在客户端设备上处理,例如从照片中识别面孔(如苹果的 Face ID 功能),它使用本地设备上的机器学习。
虽然诸如人工智能(AI)、增强现实(AR)、虚拟现实(VR)、机器学习(ML)和区块链等多个话题正处于趋势之中,但机器学习的增长速度超过了其他技术,且在各个领域中已经有了明确的应用案例。机器学习算法正被应用于图像、文本、音频和视频,以获取我们所期望的输出。
如果你是初学者,那么有多种方式可以开始你的工作,利用所有正在构建的免费和开源框架。如果你担心自己构建模型,你可以从 Google 的 Firebase ML Kit 开始。或者,你可以使用 TensorFlow 构建自己的模型,然后将该模型转换为 TensorFlow Lite 模型(适用于 Android)和 Core ML 模型(适用于 iOS)。
本章将涵盖以下主题:
-
-
流行的基于 ML 的云服务
-
当你构建第一个基于机器学习的移动应用时,从哪里开始
-
-
进一步阅读的参考资料
流行的基于 ML 的云服务
要开始你的机器学习之旅,你可以使用现有的基于云的机器学习服务。机器学习即服务(MLaaS)在所有现代商业领域中得到了广泛应用。数据变得更加便宜,数据量呈指数级增长。因此,设备的处理能力正在以更快的速度增长。这一趋势促使了多种基于云的服务的出现,如软件即服务(SaaS)、平台即服务(PaaS)和基础设施即服务(IaaS),现在又加入了 MLaaS。
尽管我们可以在移动设备上运行机器学习模型,但仍然受到内存、CPU 和 图形处理单元(GPU)资源的限制。在这种情况下,云服务变得非常有用。
从云端的机器学习开始,提供多种服务,如面部识别、光学字符识别、图像识别、地标识别、数据可视化以及自然语言处理(NLP)。所有这些选项都得到了深度神经网络、卷积神经网络(CNNs)、概率模型等技术的支持。大多数云服务提供商采用商业模式,提供一些免费的限制额度供开发者探索,然后决定哪种服务最适合开发他们的应用程序。
以下部分将解释现在可用的四大服务,这些服务在开发者和企业中也非常流行。作为初学者,你可以探索每个提供商的功能,并选择适合自己应用程序的服务。
IBM Watson 服务
IBM Watson 通过多种产品提供深度学习服务。它提供一个名为 AI 助手的文本机器人服务,支持移动平台和聊天服务;还有一个名为 Watson Studio 的服务,帮助构建和分析模型。IBM Watson 还拥有另一个独立的 API 服务,处理文本、视觉和语音。
可以找到一个示例应用程序,用于使用 Core ML 开发视觉应用程序。可以在github.com/watson-developer-cloud/visual-recognition-coreml
查看。
微软 Azure 认知服务
微软提供了五个类别的即用型认知服务,如下所示:
-
视觉 API
-
语音 API
-
知识 API
-
搜索 API
-
语言 API
这些 API 的功能将在以下部分中介绍。
视觉 API
视觉 API 是用于图像处理的算法,能够智能地识别、生成图像描述并对图片进行审核。
语音 API
通过语音 API,语音音频可以转换为文本。这些 API 使用语音印刷进行验证,或为应用程序添加说话人识别功能。
知识 API
为了解决智能推荐和语义搜索等任务,我们使用知识 API,帮助我们将复杂的信息和数据映射出来。
搜索 API
搜索 API 提供 Bing 网络搜索 API 给您的应用。它还利用将数十亿个网页、图像、视频和新闻结合在一个 API 调用中的能力。
语言 API
语言 API 允许您的应用处理自然语言,使用预构建脚本,评估情感,并学习如何识别用户需求。
针对上述 API,提供了多个示例应用。这些示例可以在 azure.microsoft.com/en-in/resources/samples/?%20sort=0&sort=0
中找到。
亚马逊机器学习
亚马逊 Web 服务 (AWS) 提供了多个基于机器学习的服务。所有这些服务都紧密结合,以便在 AWS 云中高效运行。以下部分将重点介绍其中一些服务。
视觉服务
AWS 提供了 Amazon Rekognition,这是一种基于深度学习的服务,旨在处理图像和视频。我们还可以将该服务集成到移动设备上。
聊天服务
Amazon Lex 帮助构建聊天机器人。这个行业仍在增长,随着越来越多数据的涌入,服务将变得更加智能,能够更好地回答查询。
语言服务
一些语言服务的示例包括 Amazon Comprehend,它帮助揭示文本中的洞察和关系;Amazon Translate,它帮助进行流畅的文本翻译;Amazon Transcribe,它帮助进行自动语音识别;Amazon Polly,它帮助将自然听起来的文本转化为语音。
您可以在 github.com/aws-samples/machine-learning-samples
中查看一些示例应用。
Google Cloud 机器学习
如果您想在云中运行模型,Google Cloud ML Engine 提供了 TensorFlow、scikit-learn 和 XGBoost 在云中的强大功能和灵活性。如果这不适合您的需求,您可以选择最适合您场景的 API 服务。在 Google Cloud ML 中,提供了多个 API 供您选择。这些 API 分为以下四大类:
-
视觉:Cloud Vision API 帮助进行图像识别和分类;Cloud Video Intelligence API 帮助进行场景级别的视频标注;AutoML Vision 帮助进行自定义图像分类模型。
-
对话:Dialogflow 企业版构建对话界面;Cloud Text-to-Speech API 将文本转为语音;Cloud Speech-to-Text API 将语音转为文本。
-
语言:Cloud Translation API 用于语言检测和翻译;Cloud Natural Language API 用于文本解析和分析;AutoML Translation 用于自定义领域特定的翻译;AutoML Natural Language 帮助构建自定义文本分类模型。
-
知识:Cloud Inference API 从时间序列数据集中提取洞察。
你可以在github.com/GoogleCloudPlatform/cloud-vision
找到许多 Google Vision API。
还有一些开发者常用的其他服务,包括Dialogflow和Wit.ai。
构建你的第一个机器学习模型
通过这本书所获得的知识,你可以开始开发运行在手机上的自己的模型。你需要首先确定问题陈述。有许多使用场景不需要机器学习模型;我们不能把机器学习强行应用到所有事情中。因此,在构建自己的模型之前,你需要遵循一个循序渐进的步骤:
-
确定问题。
-
规划模型的有效性;决定数据是否能在预测未来类似案例的输出时发挥作用。例如,收集相似年龄、性别和位置的人的购买历史,将有助于预测新客户的购买偏好。然而,如果你要预测的是新客户的身高,那么这些数据将不起作用。
-
开发一个简单的模型(可以基于 SQL)。这将有助于减少构建实际模型时的工作量。
-
验证数据并丢弃不必要的数据。
-
构建实际的模型。
随着各类参数(来自多个传感器的数据)的数据量在本地设备以及云服务提供商处呈指数级增长,我们可以构建更多个性化内容的更好应用场景。已经有很多应用程序在使用机器学习,比如邮件服务(Gmail 和 Outlook)和出租车服务(Uber、Ola 和 Lyft)。
构建自己模型的限制
尽管机器学习(ML)越来越流行,但在移动平台上运行机器学习模型以便普及到大众尚不可行。当你为移动应用构建自己的模型时,也存在一些限制。虽然可以在本地设备上进行预测而无需依赖云服务,但不建议构建一个根据当前操作做出预测并在本地设备上积累数据的不断演化的模型。由于移动设备的内存和处理能力的限制,目前我们只能在移动设备上运行预先构建的模型并从中获得推理结果。一旦移动设备有了更强大的处理器,我们就可以在本地设备上训练和改进模型。
与此相关的使用案例有很多。Apple 的 Face ID 就是一个例子,它在本地设备上运行一个需要 CPU 或 GPU 计算的模型。当设备能力在未来得到提升时,将有可能在设备上构建一个全新的模型。
准确性是人们避免在移动设备上开发模型的另一个原因。由于我们目前无法在移动设备上进行繁重的运算,与基于云的服务相比,准确性看起来十分有限,原因在于内存和计算能力的限制。你可以运行 TensorFlow 和 Core ML 库中适用于移动设备的模型。
TensorFlow Lite 模型可以在www.tensorflow.org/lite/models
找到;Core ML 模型可以在github.com/likedan/Awesome-CoreML-Models
找到。
个性化用户体验
个性化的用户体验(UX)将是任何基于移动设备的消费类业务的基本用例,以为移动应用的用户提供更具针对性和个性化的体验。这可以通过分析以下数据点来实现:
-
谁是你的客户?
-
他们从哪里来?
-
他们的兴趣是什么?
-
他们怎么评价你?
-
他们在哪里找到你?
-
他们有什么痛点吗?
-
他们能承担得起你的产品吗?
-
他们有购买或搜索的历史记录吗?
例如,考虑零售公司或餐厅的客户。如果我们能回答上述问题,我们就能获得关于客户的丰富数据,从中我们可以构建数据模型,提供更个性化的体验(借助于机器学习)。我们可以分析并识别相似的客户,为所有用户提供更好的用户体验(UX),并精准地锁定未来的潜在客户。
让我们在接下来的部分中进一步详细探讨这些想法。
提供更好的搜索结果
提供更好的搜索结果是其中一个主要用例,尤其是在移动应用中,以提供更多上下文相关的结果,而不是基于文本的结果。这将有助于改善业务的客户基础。机器学习算法应从用户的搜索中学习并优化结果。甚至拼写错误也可以通过直观的方式进行修正。此外,收集用户的行为数据(关于他们如何使用你的应用)将有助于提供最佳的搜索结果,从而按个性化的方式对结果进行排名。
精准定位正确的用户
大多数应用在用户第一次安装时都会收集用户的年龄和性别数据。这将帮助你了解应用的常见用户群体。你还将获得用户的数据,这能让你了解用户使用应用的频率和时长,以及位置信息(如果用户允许的话)。这些数据将有助于你在未来预测客户目标。例如,你将能够看到你的用户群体是否来自 18 到 25 岁之间,并且以女性为主。在这种情况下,你可以制定吸引更多男性用户的策略,或者只专注于吸引女性用户。这个算法应该能够预测和分析所有这些数据,这将有助于你进行市场营销并扩大用户基础。
机器学习驱动的移动应用在许多小众的使用场景中能够发挥巨大作用,其中一些应用场景如下:
-
自动产品标签
-
用于计步器、Uber 和 Lyft 的时间估算
-
基于健康的推荐
-
运输成本估算
-
供应链预测
-
财务管理
-
物流优化
-
提高生产力
总结
有了本书中获得的所有基本概念,你可以开始构建具有机器学习能力的应用程序。此外,随着与设备如 Amazon Alexa、Google Home 或 Facebook Portal 的互动方式不断增加,你会发现更多的应用场景来构建机器学习应用。最终,我们正在朝着一个更加互联的世界迈进,这将使得连接和交流变得更紧密,并引领我们创造更好的机器学习体验。
进一步阅读
现在有很多机器学习课程可以在线学习。以下是一些课程的列举:
-
如果你是初学者,可以从 Andrew Ng 在 Coursera 上讲授的机器学习教程开始,链接地址是
www.coursera.org/learn/machine-learning
-
可以在
developers.google.com/machine-learning/crash-course/
找到谷歌提供的机器学习速成课程。 -
Adam Geitgey 写的最好的(也是最启发性的)基于机器学习的博客系列之一,可以在
medium.comi@ageitgey/machine-learning-is-fun-80ea3ec3c471
找到。 -
你可以在
codelabs.developers.google.com/codelabs/tensorflow-for-poets
开始学习 TensorFlow 的技能。 -
对 TensorFlow 的更深入了解可以参考
petewarden.com/2016/09/27/tensorflow-for-mobile-poets/