Go-机器学习快速启动指南-全-

Go 机器学习快速启动指南(全)

原文:annas-archive.org/md5/da04a1381a1aabc00d45640c27f0ad74

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

机器学习ML)在现代数据驱动世界中扮演着至关重要的角色,并在金融预测、有效搜索、机器人技术、医疗保健中的数字成像等多个领域得到了广泛应用。这是一个快速发展的领域,每周都有新的算法和数据集被学术界和技术公司发布。本书将教会你如何在不同环境中使用 Go 执行各种机器学习任务。

你将了解开发 Go 机器学习应用程序并部署为生产系统所需的重要技术。最佳的学习方式是通过实践,所以请深入其中,开始将机器学习软件添加到自己的 Go 应用程序中。

本书面向对象

本书面向至少具备入门级 Go 知识以及机器学习旨在解决的问题类型模糊概念的开发商和数据科学家。不需要 Go 的高级知识,也不需要机器学习支撑的数学理论理解。

本书涵盖内容

第一章,《用 Go 介绍机器学习》,介绍了机器学习以及与机器学习相关的不同类型的问题。我们还将探讨机器学习开发的生命周期,以及创建和将机器学习应用程序投入生产的流程。

第二章,《设置开发环境》,解释了如何为机器学习应用程序和 Go 设置环境。我们还将了解如何安装交互式环境 Jupyter,以使用 Gota 和 gonum/plot 等库加速数据探索和可视化。

第三章,《监督学习》,介绍了监督学习算法,并演示了如何选择机器学习算法、训练它,并在之前未见过的数据上验证其预测能力。

第四章,《无监督学习》,重用了我们在本书中实现的数据加载和准备的相关技术,但将重点放在无监督机器学习上。

第五章,《使用预训练模型》,描述了如何加载预训练的 Go 机器学习模型并使用它进行预测。我们还将了解如何使用 HTTP 调用用其他语言编写的机器学习模型,这些模型可能位于不同的机器上,甚至位于互联网上。

第六章,《部署机器学习应用程序》,涵盖了机器学习开发生命周期的最后阶段:将用 Go 编写的机器学习应用程序投入生产。

第七章,《结论——成功的机器学习项目》,退后一步,从项目管理角度审视机器学习开发。

为了充分利用本书

代码示例,包括 bash 脚本和安装说明,在具有 8 GB RAM 和 500 GB SSD 硬盘的 Ubuntu 16.04 服务器上进行了测试。需要一台具有类似规格的机器。

下载示例代码文件

您可以从www.packt.com的账户下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packt.com/support并注册,以便将文件直接通过电子邮件发送给您。

您可以通过以下步骤下载代码文件:

  1. www.packt.com登录或注册。

  2. 选择支持选项卡。

  3. 点击代码下载与勘误。

  4. 在搜索框中输入书名,并遵循屏幕上的说明。

文件下载完成后,请确保使用最新版本的软件解压或提取文件夹:

  • WinRAR/7-Zip for Windows

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

本书代码包也托管在 GitHub 上,地址为github.com/PacktPublishing/Machine-Learning-with-Go-Quick-Start-Guide。如果代码有更新,它将在现有的 GitHub 仓库中更新。

我们还有其他来自我们丰富的书籍和视频目录的代码包,可在github.com/PacktPublishing/找到。查看它们吧!

下载彩色图像

我们还提供了一份包含本书中使用的截图/图表的彩色图像的 PDF 文件。您可以从这里下载:www.packtpub.com/sites/default/files/downloads/9781838550356_ColorImages.pdf

使用的约定

本书中使用了多种文本约定。

CodeInText: 表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“go-deep库让我们能够非常快速地构建这个架构。”

代码块设置如下:

categories := []string{"tshirt", "trouser", "pullover", "dress", "coat", "sandal", "shirt", "shoe", "bag", "boot"}

粗体: 表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。以下是一个示例:“通过点击新建 | 前往创建一个新的笔记本:”

警告或重要注意事项看起来像这样。

小贴士和技巧看起来像这样。

联系我们

我们欢迎读者的反馈。

一般反馈: 如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并通过customercare@packtpub.com给我们发邮件。

勘误: 尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,如果您能向我们报告,我们将不胜感激。请访问 www.packt.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。

盗版: 如果您在互联网上以任何形式发现了我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过 copyright@packt.com 联系我们,并附上材料的链接。

如果您有兴趣成为作者: 如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问 authors.packtpub.com

评论

请留下评论。一旦您阅读并使用了这本书,为何不在您购买它的网站上留下评论呢?潜在读者可以查看并使用您的客观意见来做出购买决定,Packt 公司可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!

如需了解更多关于 Packt 的信息,请访问 packt.com

第一章:使用 Go 介绍机器学习

在我们周围,自动化正在以细微的增量改变着我们的生活,这些增量处于数学和计算机科学的尖端。一个 Nest 恒温器、Netflix 的电影推荐和 Google 的图片搜索算法有什么共同之处?这些技术都是由当今软件行业中最聪明的大脑之一创造的,它们都依赖于机器学习ML)技术。

在 2019 年 2 月,Crunchbase 列出了超过 4,700 家将自己归类为人工智能AI)或机器学习的公司^([1])。其中大部分公司处于非常早期阶段,由天使投资者或风险投资家的早期轮次融资。然而,Crunchbase 在 2017 年和 2018 年的文章以及英国《金融时报》的文章都围绕着一个共同的认识,即机器学习越来越被依赖以实现持续增长([2]),并且其日益成熟将导致更广泛的应用([3]),尤其是如果能够解决机器学习算法决策不透明性的挑战([4])。甚至《纽约时报》还设有专门关于机器学习的专栏([5]),这是对其在日常生活中重要性的致敬。

本书将教会具有 Go 编程语言中级知识的软件工程师如何从概念到部署,以及更远地编写和制作一个机器学习应用程序。我们首先将分类适合机器学习技术的和机器学习应用程序的生命周期中的问题。然后,我们将解释如何使用 Go 语言设置一个专门适合数据科学开发的环境。接着,我们将提供主要机器学习算法、它们的实现及其陷阱的实用指南。我们还将提供一些关于使用其他编程语言产生的机器学习模型并在 Go 应用程序中集成的指导。最后,我们将考虑不同的部署模型以及 DevOps 和数据科学之间难以捉摸的交集。我们将结合我们自己的经验对管理机器学习项目进行一些评论。

机器学习理论是一个数学上高级的学科,但你可以在不完全理解它的情况下开发机器学习应用程序。本书将帮助你发展对使用哪些算法以及如何仅用基本数学知识来构建问题的直觉。

在我们的第一章中,我们将介绍 Go 机器学习应用程序的一些基本概念:

  • 什么是机器学习?

  • 机器学习问题类型

  • 为什么要在 Go 中编写机器学习应用程序?

  • 机器学习开发生命周期

什么是机器学习?

机器学习是统计学和计算机科学交叉的领域。这个领域的输出是一系列能够自主操作的算法,它们通过从数据集中推断出最佳决策或答案来解决问题。与传统的编程不同,程序员必须决定程序的规则,并费力地将这些规则编码在他们选择的编程语言的语法中,而机器学习算法只需要足够量的准备数据、从数据中学习的计算能力,以及通常需要一些知识来调整算法参数以改善最终结果。

结果系统非常灵活,并且能够很好地利用人类可能忽略的模式。想象一下从头开始编写一个电视剧推荐系统。你可能首先定义问题的输入和输出,然后找到一个包含电视剧发布日期、类型、演员和导演等详细信息的数据库。最后,你可能创建一个score函数,如果两对电视剧的发布日期接近、属于同一类型、共享演员或拥有相同的导演,则给予更高的评分。

推荐系统是一种预测算法,试图猜测用户会对一个输入样本赋予的评分。在在线零售中,广泛使用的一种应用是使用推荐系统根据用户的过去购买行为向用户推荐商品。

给定一部电视剧,你可以根据相似度评分递减对所有其他电视剧进行排名,并将前几部推荐给用户。在创建score函数时,你会在各种特征的相对重要性上进行判断,例如决定两个系列之间每对共享演员值一分。这种猜测工作,也称为启发式方法,是机器学习算法旨在为你做的事情,节省时间并提高最终结果的准确性,尤其是如果用户偏好发生变化,你必须定期更改评分函数以保持同步。

人工智能和机器学习这两个更广泛领域的区别是模糊的。虽然围绕机器学习的炒作可能相对较新^([6]),但这个领域的历史始于 1959 年,当时人工智能领域的领先专家 Arthur Samuel 首次使用了这些词^([7])。在 20 世纪 50 年代,像 Alan Turing^([8])和 Samuel 本人这样的发明家发明了诸如感知器、遗传算法等机器学习概念。在接下来的几十年里,实现通用人工智能的实践和理论困难导致了诸如基于规则的方法(如专家系统)等方法的产生,这些方法不是从数据中学习,而是从专家制定的规则中学习,这些规则是他们多年来学到的,并以 if-else 语句的形式编码。

机器学习的力量在于算法能够适应之前未见过的案例,这是 if-else 语句无法做到的。如果你不需要这种适应性,可能是因为所有案例事先都是已知的,那么坚持基本原理,使用传统的编程技术即可!

在 20 世纪 90 年代,意识到在现有技术下实现人工智能的可能性不大,人们越来越倾向于采用一种狭隘的方法来解决可以用统计和概率论相结合解决的问题。这导致了机器学习作为一个独立领域的发展。今天,机器学习和人工智能经常被互换使用,尤其是在市场营销文献中^([9])。

机器学习算法的类型

机器学习算法主要有两大类:监督学习和无监督学习。选择哪种类型的算法取决于你拥有的数据和项目目标。

监督学习问题

监督学习问题旨在根据提供的标记输入/输出对,推断输入和输出数据集之间最佳映射。标记数据集作为算法的反馈,允许算法评估其解决方案的优化程度。例如,给定 2010-2018 年每年的平均原油价格列表,你可能希望预测 2019 年的平均原油价格。算法在 2010-2018 年产生的误差将允许工程师估计其在目标预测年份 2019 年的误差。

标记对由一个包含独立变量的输入向量和一个包含依赖变量的输出向量组成。例如,用于面部识别的标记数据集可能包含带有面部图像数据的输入向量,以及编码照片中人物姓名的输出向量。标记集(或数据集)是标记对的集合。

给定一组标记的手写数字,你可能希望预测一个以前未见过的手写数字的标签。同样,给定一个标记为垃圾邮件或非垃圾邮件的电子邮件数据集,一个想要创建垃圾邮件过滤器的公司会希望预测一个以前未见过的消息是否为垃圾邮件。所有这些问题都是监督学习问题。

监督机器学习问题可以进一步分为预测和分类:

  • 分类试图用一个已知的输出值来标记一个未知的输入样本。例如,你可以训练一个算法来识别猫的品种。该算法会通过标记它已知的品种来对未知猫进行分类。

  • 相比之下,预测算法试图用一个已知或未知的输出值来标记一个未知的输入样本。这也被称为估计回归。一个典型的预测问题是时间序列预测,其中预测序列的输出值是在之前未见过的某个时间值。

分类算法将尝试将输入样本与给定输出类别列表中的一个项目关联起来:例如,决定一张照片是否代表猫、狗或都不是,这是一个分类问题。预测算法将输入样本映射到输出域中的一个成员,该域可以是连续的:例如,尝试根据一个人的体重和性别猜测其身高,这是一个预测问题。

我们将在第三章“监督学习”中更详细地介绍监督学习算法。

无监督学习问题

无监督学习问题旨在从未标记的数据中学习。例如,给定一个市场研究数据集,聚类算法可以将消费者划分为不同的细分市场,为市场营销专业人士节省时间。给定一个医学扫描数据集,无监督分类算法可以将图像划分为不同类型的组织,以便进行进一步分析。一种称为降维的无监督学习方法与其他算法协同工作,作为预处理步骤,以减少另一个算法在训练时需要处理的数据量,从而缩短训练时间。我们将在第四章“无监督学习”中更详细地介绍无监督学习算法。

大多数机器学习算法都可以在广泛的编程语言中高效实现。虽然 Python 以其易用性和丰富的开源库而受到数据科学家的青睐,但 Go 为创建商业机器学习应用的开发者提供了显著的优势。

为什么要在 Go 中编写机器学习应用?

对于其他语言,尤其是 Python,有更完整的库,这些库已经受益于数十年的世界顶尖大脑的研究。一些 Go 程序员为了寻求更好的性能而转向 Go,但由于机器学习库通常是用 C 编写的,并通过它们的绑定暴露给 Python,因此它们不会像解释型 Python 程序那样遇到相同的性能问题。深度学习框架如 TensorFlow 和 Caffe 对 Go 的绑定非常有限,甚至没有。即使考虑到这些问题,Go 仍然是一个优秀甚至可能是最好的语言,用于开发包含机器学习组件的应用程序。

Go 的优势

对于试图在学术环境中改进最先进算法的研究人员来说,Go 可能不是最佳选择。然而,对于拥有产品概念且现金储备快速减少的初创公司来说,在短时间内以可维护和可靠的方式完成产品的开发是至关重要的,这正是 Go 语言大放异彩的地方。

Go(或 Golang)起源于 Google,其设计始于 2007 年([10])。其声明的目标是创建一个高效、编译的编程语言,感觉轻便且令人愉悦([11])。Go 从众多旨在提高生产应用程序生产力和可靠性的特性中受益:

  • 易于学习和接纳新开发者

  • 快速构建时间

  • 运行时良好的性能

  • 极佳的并发支持

  • 优秀的标准库

  • 类型安全

  • 使用 gofmt 易于阅读、标准化的代码

  • 强制错误处理以最小化意外异常

  • 明确、清晰的依赖管理

  • 随着项目增长,易于适应的架构

所有这些原因使 Go 成为构建生产系统的优秀语言,尤其是网络应用程序。2018 年 Stack Overflow 开发者调查揭示,尽管只有 7% 的专业开发者将 Go 作为其主要语言,但它位列最受欢迎列表的第 5 位,并且与其他语言相比,Go 程序员的薪资非常高,这认可了 Go 程序员为企业带来的商业价值^([12])。

Go 成熟的生态系统

一些世界上最成功的技术公司将 Go 作为其生产系统的主要编程语言,并积极为其开发做出贡献,例如 Cloudflare([13])、Google、Uber([14])、Dailymotion^([15]) 和 Medium^([16])。这意味着现在有一个广泛的工具和库生态系统,可以帮助开发团队在 Go 中创建可靠、可维护的应用程序。甚至全球领先的容器技术 Docker 也是用 Go 编写的。

在撰写本文时,GitHub 上有 1,774 个用 Go 语言编写的仓库拥有超过 500 个星标,这通常被认为是质量和支持的优良指标。相比之下,Python 有 3,811 个,Java 有 3,943 个。考虑到 Go 相对较年轻,并且允许更快的生产就绪开发,用 Go 编写的得到良好支持的仓库数量相对较大,这构成了开源社区的高度认可。

Go 拥有众多稳定且得到良好支持的开放源代码机器学习库。按 GitHub 星标和贡献者数量,最受欢迎的 Go 机器学习库是 GoLearn^([17])。它也是最新更新的。其他 Go 机器学习库包括 GoML 和 Gorgonia,这是一个深度学习库,其 API 类似于 TensorFlow。

转移在其他语言中创建的知识和模型

数据科学家通常会探索不同的方法来解决机器学习问题,例如使用 Python,并创建一个可以在任何应用程序之外解决问题的模型。这些管道,如将数据输入和输出模型、向客户提供服务、持久化输出或输入、记录错误或监控延迟,并不属于这个交付成果,也不在数据科学家正常工作范围之内。因此,将模型从概念到 Go 生产应用程序需要多语言方法,如微服务。

本书中的大多数代码示例都使用了机器学习算法或绑定到库(如 OpenCV),这些库也在 Python 等语言中可用。这将使您能够快速将数据科学家的原型 Python 代码转换为生产 Go 应用程序。

然而,对于深度学习框架如 TensorFlow 和 Caffe,存在 Go 绑定。此外,对于更基本的算法,如决策树,相同的算法也已经在 Go 库中实现,并且如果以相同的方式配置,将产生相同的结果。综合考虑,这意味着可以在不牺牲准确性、速度或强迫数据科学家使用他们不习惯的工具的情况下,将数据科学产品完全集成到 Go 应用程序中。

机器学习开发生命周期

机器学习开发生命周期是一个创建并推向生产包含解决业务问题的机器学习模型的应用程序的过程。然后,该机器学习模型可以作为产品或服务提供的一部分通过应用程序提供给客户。

以下图表说明了机器学习开发生命周期过程:

定义问题和目标

在任何开发开始之前,必须定义要解决的问题以及理想结果的目标,以设定期望。问题的表述方式非常重要,因为这可能意味着无法解决的问题和简单解决方案之间的区别。这也可能涉及到关于任何算法的输入数据来源的讨论。

机器学习算法通常需要大量数据才能发挥最佳性能。在规划机器学习项目时,获取高质量数据是最重要的考虑因素。

机器学习问题的典型表述形式是“给定 X 数据集,预测 Y”。数据的可用性或缺乏可用性可能会影响问题的表述、解决方案及其可行性。例如,考虑以下问题:“给定一大组标注的手写数字图像”,预测一个之前未见过的图像的标签。深度学习算法已经证明,只要训练数据集足够大,工程师的工作量很小,就可以在这个特定问题上实现相对较高的准确性^([19])。如果训练集不大,问题立即变得更加困难,需要仔细选择要使用的算法。它还影响准确性,从而影响可达到的目标集。

Michael Nielsen 在 MNIST 手写数字数据集上进行的实验表明,对于大多数测试的算法,使用每个数字 1 个标注的输入/输出对进行训练与使用 5 个示例相比,准确率从大约 40%提高到大约 65%^([20])。通常,每个数字使用 10 个示例可以将准确率进一步提高 5%。

如果可用的数据不足以满足项目目标,有时可以通过对现有示例进行微小修改来人工扩大数据集以提高性能。在之前提到的实验中,Nielsen 观察到,向数据集中添加略微旋转或平移的图像可以将性能提高多达 15%。

获取和探索数据

我们之前已经论证,在指定项目目标之前理解输入数据集是至关重要的,尤其是与准确性相关的目标。一般来说,当有大量的训练数据集可用时,机器学习算法会产生最佳结果。用于训练它们的数据越多,它们的性能就越好。

因此,获取数据是机器学习开发生命周期中的一个关键步骤——这个步骤可能非常耗时且充满困难。在某些行业中,隐私法规可能导致个人数据不可用,这使得创建个性化产品变得困难,或者在使用之前需要对源数据进行匿名化。一些数据集可能可用,但可能需要如此广泛的准备甚至人工标记,这可能会给项目时间表或预算带来压力。

即使你没有专有数据集可以应用于你的问题,你也可能找到可用的公共数据集。通常,公共数据集已经引起了研究者的关注,因此你可能发现你试图解决的问题已经被解决,并且解决方案是开源的。以下是一些公共数据集的良好来源:

一旦获取了数据集,就应该对其进行探索,以获得对不同的特征(自变量)如何影响所需输出的基本理解。例如,当试图从自我报告的数据中预测正确的高度和体重时,研究人员在初步探索中确定,年龄较大的受试者更有可能低估肥胖,因此年龄在构建他们的模型时是一个相关特征。试图从所有可用数据中构建模型,即使是不相关的特征,在最坏的情况下可能会导致训练时间更长,并且通过引入噪声严重损害准确性。

花更多的时间来处理和转换数据集是值得的,因为这将提高最终结果的准确性,甚至可能缩短训练时间。本书中的所有代码示例都包括数据处理和转换。

在第二章《设置机器学习环境》中,我们将看到如何使用 Go 语言和一个名为Jupyter的基于浏览器的交互式工具来探索数据。

选择算法

算法的选取可以说是机器学习应用工程师需要做出的最重要的决定,也是需要投入最多研究的工作。有时,甚至需要将机器学习算法与传统计算机科学算法相结合,以便使问题更容易处理——这种例子就是我们后面将要讨论的推荐系统。

开始寻找解决特定问题的最佳算法的一个好方法是确定是否需要监督或无监督方法。我们在本章前面介绍了这两种方法。一般来说,当你拥有标记的数据集,并希望对之前未见过的样本进行分类或预测时,这将使用监督算法。当你希望通过将未标记的数据集聚类成不同的组来更好地理解它,可能为了随后对新样本进行分类,你将使用无监督学习算法。对每种算法的优点和缺点有更深入的了解,以及对你的数据进行彻底的探索,将提供足够的信息来选择算法。为了帮助你开始,我们在第三章《监督学习》中涵盖了各种监督学习算法,在第四章《无监督学习》中涵盖了无监督学习算法。

一些问题可以巧妙地应用机器学习技术和传统计算机科学。其中一个这样的问题是推荐系统,现在在像亚马逊和 Netflix 这样的在线零售商中非常普遍。这个问题要求,给定每个用户购买物品的数据集,预测用户最有可能购买的下 N 个物品。这在亚马逊的“购买 X 的人也购买 Y”系统中得到了体现。

解决方案的基本思想是,如果两个用户购买非常相似的商品,那么任何不在他们购买商品交集中的商品都是他们未来购买的好候选。首先,将数据集转换成将商品对映射到表示它们共现的分数。这可以通过计算相同客户购买两个商品的次数除以客户购买任一商品的次数来计算,得到一个介于 0 和 1 之间的数字。现在这提供了一个标记的数据集来训练一个监督算法,如二元分类器,以预测先前未见对对的分数。结合排序算法,给定一个单一的商品,可以生成一个按可购买性排序的商品列表。

准备数据

数据准备是指在训练算法之前对输入数据集所执行的过程。一个严谨的准备过程可以同时提高数据质量并减少算法达到所需精度所需的时间。数据准备的两个步骤是数据预处理和数据转换。我们将在第二章,设置开发环境,第三章,监督学习,和第四章,无监督学习中详细介绍数据准备。

数据预处理旨在将输入数据集转换为适合与所选算法一起工作的格式。预处理任务的典型示例是将日期列格式化为某种方式,或将 CSV 文件导入数据库,丢弃导致解析错误的任何行。输入数据文件中可能也存在需要填充(例如,使用平均值)或整个样本丢弃的缺失数据值。敏感信息,如个人信息,可能需要被移除。

数据转换是指对数据集进行采样、减少、增强或聚合的过程,使其更适合算法。如果输入数据集较小,可能需要通过人工创建更多示例来增强它,例如在图像识别数据集中旋转图像。如果输入数据集具有探索认为无关的特征,明智的做法是移除它们。如果数据集比问题所需的粒度更细,将其聚合到更粗的粒度可能有助于加快结果,例如,如果问题只需要对每个县进行预测,则将城市级数据聚合到县。

最后,如果输入数据集特别大,例如许多用于深度学习算法的图像数据集,那么从较小的样本开始,这将产生快速结果,以便在投资更多计算资源之前验证算法的可行性,这是一个好主意。

样本过程还将把输入数据集分成训练和验证子集。我们将在后面解释为什么这是必要的,以及应该使用多少数据比例。

训练

机器学习开发生命周期中最计算密集的部分是训练过程。在最简单的情况下,训练一个机器学习算法可能只需要几秒钟,而当输入数据集巨大且算法需要多次迭代才能收敛时,可能需要几天。后者通常与深度学习技术相关。例如,DeepMinds AlphaGo Zero 算法用了四十天时间才完全掌握围棋游戏,尽管它在仅仅三天后就已经很熟练了^([22])。在处理较小数据集和图像或声音识别以外的其他问题上的许多算法,可能不需要这么多的时间或计算资源。

基于云的计算资源正变得越来越便宜,因此,如果一个算法,尤其是深度学习算法,在您的 PC 上训练时间过长,您可以在云实例上部署和训练它,只需花费几美元。我们将在第六章中介绍部署模型,部署机器学习应用

当算法正在训练时,尤其是如果训练阶段将花费很长时间,那么有一些实时指标来衡量训练进展情况是有用的,这样就可以在不等待训练完成的情况下中断、重新配置和重新启动。这些指标通常被归类为损失指标,其中损失指的是算法在训练或验证子集上犯的假设性错误。

在预测问题中最常见的损失度量指标如下:

  • 均方误差MSE)衡量输出变量与预测值之间平方距离的总和。

  • 平均绝对误差MAE)衡量输出变量与预测值之间绝对距离的总和。

  • Huber 损失是 MSE 和 MAE 的组合,它对异常值更稳健,同时仍然是均值和中值损失的良好估计器。

在分类问题中最常见的损失度量指标如下:

  • 对数损失通过对错误分类进行惩罚来衡量分类器的准确性。它与交叉熵损失密切相关。

  • 焦点损失是一种新的损失函数,旨在防止当输入数据集稀疏时出现假阴性^([23])。

验证/测试

软件工程师熟悉测试和调试软件源代码,但如何测试机器学习模型呢?算法片段和数据输入/输出例程可以进行单元测试,但通常不清楚如何确保作为黑盒的机器学习模型本身是正确的。

确保机器学习模型正确性和足够准确的第一步是验证。这意味着将模型应用于预测或分类验证数据子集,并将结果准确性与项目目标进行比较。因为训练数据子集已经被算法看到,所以不能用来验证正确性,因为模型可能会遭受泛化能力差(也称为过拟合)的问题。为了举一个荒谬的例子,想象一个由哈希表组成的机器学习模型,该表记住每个输入样本并将其映射到相应的训练输出样本。该模型在之前记忆的训练数据子集上会有 100%的准确率,但在任何数据子集上都会有非常低的准确率,因此它将无法解决它打算解决的问题。验证测试针对这种现象。

此外,将模型输出与用户接受标准进行验证也是一个好主意。例如,如果你正在为电视剧构建推荐系统,你可能希望确保向儿童推荐的节目永远不会被评为 PG-13 或更高。与其试图将此编码到模型中,这将有一个非零的失败率,不如将此约束推入应用程序本身,因为不执行此约束的成本会太高。此类约束和业务规则应在项目开始时捕获。

集成和部署

机器学习模型与其他应用程序之间的边界必须定义。例如,算法是否会提供一个Predict方法来为给定的输入样本提供预测?是否需要调用者处理输入数据处理,还是算法实现会执行它?一旦这被定义,在测试或模拟机器学习模型以确保应用程序其余部分的正确性时,遵循最佳实践就会变得更容易。对于任何应用程序,关注点的分离都很重要,但对于那些一个组件表现得像黑盒的机器学习应用程序来说,这一点是至关重要的。

机器学习应用程序有几种可能的部署方法。对于 Go 应用程序来说,容器化特别简单,因为编译的二进制文件将没有依赖项(除非在某些非常特殊的情况下需要,例如需要绑定到深度学习库,如 TensorFlow)。不同的云服务提供商也接受无服务器部署,并提供不同的持续集成/持续部署CI/CD)服务。使用 Go 等语言的部分优势在于,应用程序可以非常灵活地部署,利用可用于传统系统应用程序的工具,而不必求助于混乱的多语言方法。

在第六章,“部署机器学习应用”中,我们将深入探讨诸如部署模型、平台即服务(PaaS)基础设施即服务(IaaS)的对比,以及针对机器学习应用的监控和警报等特定主题,利用为 Go 语言构建的工具。

重新验证

将模型投入生产而无需更新或重新训练的情况很少见。推荐系统可能需要定期重新训练,因为用户偏好会发生变化。用于汽车制造商和型号的图像识别模型可能需要随着市场上更多模型的推出而重新训练。为物联网群体中的每个设备生成一个模型的预测行为工具可能需要持续监控,以确保每个模型仍然满足所需的准确度标准,并对那些不满足标准的模型进行重新训练。

重新验证过程是一个持续的过程,其中测试模型的准确性,如果认为其准确性已降低,则触发自动或手动过程以重新训练它,确保结果始终是最优的。

摘要

在本章中,我们介绍了机器学习以及不同类型的机器学习问题。我们主张使用 Go 语言来开发机器学习应用。然后,我们概述了机器学习开发的生命周期,创建并部署机器学习应用的过程。

在下一章中,我们将解释如何为机器学习应用和 Go 设置开发环境。

进一步阅读

  1. www.crunchbase.com/hub/machine-learning-companies,于 2019 年 2 月 9 日检索。

  2. www.ft.com/content/133dc9c8-90ac-11e8-9609-3d3b945e78cf机器学习将成为全球增长的动力

  3. news.crunchbase.com/news/venture-funding-ai-machine-learning-levels-off-tech-matures/。于 2019 年 2 月 9 日检索。

  4. www.economist.com/science-and-technology/2018/02/15/for-artificial-intelligence-to-thrive-it-must-explain-itself。于 2019 年 2 月 9 日检索。

  5. www.nytimes.com/column/machine-learning。于 2019 年 2 月 9 日检索。

  6. 例如,请参阅Google Trends for Machine Learningtrends.google.com/trends/explore?date=all&geo=US&q=machine%20learning

  7. R. Kohavi 和 F. Provost,《机器学习术语表》,第 30 卷第 2-3 期,第 271-274 页,1998 年。30,第 2-3 期,第 271-274 页,1998 年。

  8. 图灵,艾伦(1950 年 10 月)。《计算机与智能》。Mind. 59(236):433–460。doi:10.1093/mind/LIX.236.433。于 2016 年 6 月 8 日检索。

  9. www.forbes.com/sites/bernardmarr/2016/12/06/what-is-the-difference-between-artificial-intelligence-and-machine-learning/。检索日期:2019 年 2 月 9 日。

  10. talks.golang.org/2012/splash.article。检索日期:2019 年 2 月 9 日。

  11. talks.golang.org/2012/splash.article。检索日期:2019 年 2 月 9 日。

  12. insights.stackoverflow.com/survey/2018/。检索日期:2019 年 2 月 9 日。

  13. github.com/cloudflare。检索日期:2019 年 2 月 9 日。

  14. github.com/uber。检索日期:2019 年 2 月 9 日。

  15. github.com/dailymotion。检索日期:2019 年 2 月 9 日。

  16. github.com/medium。检索日期:2019 年 2 月 9 日。

  17. github.com/sjwhitworth/golearn。检索日期:2019 年 2 月 10 日。

  18. 查看托管在yann.lecun.com/exdb/mnist/的 MNIST 数据集。检索日期:2019 年 2 月 10 日。

  19. 查看以下示例:machinelearningmastery.com/handwritten-digit-recognition-using-convolutional-neural-networks-python-keras/。检索日期:2019 年 2 月 10 日。

  20. cognitivemedium.com/rmnist。检索日期:2019 年 2 月 10 日。

  21. 从自我报告数据中预测校正体重、身高和肥胖患病率的回归模型:来自 BRFSS 1999-2007 的数据。Int J Obes (Lond)。2010 年 11 月;34(11):1655-64。doi:10.1038/ijo.2010.80。Epub 2010 年 4 月 13 日。

  22. deepmind.com/blog/alphago-zero-learning-scratch/。检索日期:2019 年 2 月 10 日。

  23. 密集目标检测中的焦点损失。Lin 等人。ICCV 2980-2988。预印本可在arxiv.org/pdf/1708.02002.pdf找到。

第二章:设置开发环境

就像传统的软件开发一样,机器学习应用开发需要掌握专业的样板代码和一个允许开发者以最低的摩擦和干扰速度进行工作的开发环境。软件开发者通常会在基本设置和数据整理任务上浪费大量时间。成为一个高效和专业的机器学习开发者需要能够快速原型化解决方案;这意味着在琐碎的任务上尽可能少地付出努力。

在上一章中,我们概述了主要的机器学习问题和你可以遵循以获得商业解决方案的开发流程。我们还解释了 Go 作为编程语言在创建机器学习应用时所提供的优势。

在本章中,我们将指导你完成设置 Go 开发环境的步骤,该环境针对机器学习应用进行了优化。具体来说,我们将涵盖以下主题:

  • 如何安装 Go

  • 使用 Jupyter 和 gophernotes 交互式运行 Go

  • 使用 Gota 进行数据处理

  • 使用 gonum/plot 和 gophernotes 进行数据可视化

  • 数据预处理(格式化、清洗和采样)

  • 数据转换(归一化和分类变量的编码)

本书附带的代码示例针对基于 Debian 的 Linux 发行版进行了优化。然而,它们可以被适应其他发行版(例如,将apt改为yum)和 Windows 的 Cygwin。

一旦你完成了这一章,你将能够快速探索、可视化和处理任何数据集,以便后续由机器学习算法使用。

安装 Go

开发环境是个人化的。大多数开发者会更倾向于选择一个代码编辑器或工具集,而不是另一个。虽然我们推荐使用 gophernotes 通过交互式工具如 Jupyter,但运行本书中的代码示例的唯一先决条件是 Go 1.10 或更高版本的正常安装。也就是说,go命令应该是可用的,并且GOPATH环境变量应该设置正确。

要安装 Go,从golang.org/dl/下载适用于你系统的二进制发布版。然后,参考以下与你的操作系统匹配的子节之一^([2])。

如果你只想使用 gophernotes 来运行 Go 代码,并且打算使用 Docker 作为安装方法,那么你可以跳过这一部分,直接进入使用 gophernotes 交互式运行 Go部分。

Linux、macOS 和 FreeBSD

二进制发布版被打包成 tar 包。提取二进制文件并将它们添加到你的PATH中。以下是一个示例:

tar -C /usr/local -xzf go$VERSION.$OS-$ARCH.tar.gz && \
 export PATH=$PATH:/usr/local/go/bin

要配置GOPATH环境变量,你需要决定你的 Go 文件(包括任何个人仓库)将存放在哪里。一个可能的位置是$HOME/go。一旦你决定了这一点,设置环境变量,例如如下所示:

export GOPATH=$HOME/go

要使此说明永久生效,您需要将此行添加到 .bashrc。如果您使用其他外壳(例如 .zsh),请参阅官方 Go 安装说明,网址为 github.com/golang/go/wiki/SettingGOPATH

确保您的 GOPATH 不与您的 Go 安装在同一目录中,否则这可能会引起问题。

Windows

二进制发布版本打包为 ZIP 文件或 MSI 安装程序,该安装程序会自动配置您的环境变量。我们建议使用 MSI 安装程序。但是,如果您不这样做,那么在将 ZIP 文件的内容提取到合适的位置(例如 C:\Program Files\Go)后,请确保您使用控制面板将 subdirectory bin 添加到您的 PATH 环境变量中。

一旦将二进制文件安装到合适的位置,您需要配置您的 GOPATH。首先,决定您想要您的 Go 文件(包括任何个人仓库)存放的位置。一个可能的位置是 C:\go。一旦您决定,将 GOPATH 环境变量设置为该目录的路径。

如果您不确定如何设置环境变量,请参阅官方 Go 安装说明,网址为 github.com/golang/go/wiki/SettingGOPATH

确保您的 GOPATH 不与您的 Go 安装在同一目录中,否则这可能会引起问题。

使用 gophernotes 运行 Go 的交互式操作

Project Jupyter 是一个非营利组织,旨在开发面向数据科学的语言无关交互式计算^([3])。结果是成熟、支持良好的环境,可以探索、可视化和处理数据,通过提供即时反馈和与绘图库(如 gonum/plot)的集成,可以显著加速开发。

虽然它的第一个迭代版本,称为 iPython,最初只支持基于 Python 的处理器(称为 kernels),但 Jupyter 的最新版本已超过 50 个内核,支持包括 Go 语言在内的数十种语言,其中包含三个 Go 语言的内核^([4])。GitHub 支持渲染 Jupyter 文件(称为 notebooks)^([5]),并且有各种专门的在线共享笔记本的枢纽,包括 Google Research Colabs^([6])、Jupyter 的社区枢纽 NBViewer^([7]) 和其企业产品 JupyterHub^([8])。用于演示目的的笔记本可以使用 nbconvert 工具转换为其他文件格式,如 HTML^([9])。

在这本书中,我们将使用 Jupyter 和 Go 的 gophernotes 内核。在 Linux 和 Windows 上开始使用 gophernotes 的最简单方法是使用其 Docker^([10]) 镜像。

对于其他安装方法,我们建议检查 gophernotes GitHub 存储库的 README 页面:

github.com/gopherdata/gophernotes

开始一个基于 gophernotes 的新项目步骤如下:

  1. 创建一个新目录来存放项目文件(这个目录不需要在您的GOPATH中)。

  2. (可选)在新目录中运行git init来初始化一个新的 git 仓库。

  3. 从新目录中运行以下命令(根据您如何安装 Docker,您可能需要在其前面加上sudo):

    docker run -it -p 8888:8888 -v $(pwd):/usr/share/notebooks gopherdata/gophernotes:latest-ds

  4. 在终端中,将有一个以?token=[一些字母和数字的组合]结尾的 URL。在现代网络浏览器中导航到这个 URL。您创建的新目录将被映射到/usr/share/notebooks,因此请导航到树形结构中显示的这个目录。

在 Windows 上,您可能需要修改前面的命令,将$(pwd)替换为%CD%

现在我们已经学习了如何安装 Go 以及如何使用 gophernotes 设置基本开发环境,现在是时候学习数据预处理了。

示例 - 正面和负面评论中最常见的短语

在我们的第一个代码示例中,我们将使用多领域情感数据集(版本 2.0)^([11])。这个数据集包含了来自四个不同产品类别的亚马逊评论。我们将下载它,预处理它,并将其加载到 Gota 数据整理库中,以找到正面和负面评论中最常见的短语,这些短语在两者中不会同时出现。这是一个不涉及 ML 算法的基本示例,但将作为 Go、gophernotes 和 Gota 的实战介绍。

您可以在本书的配套仓库中找到完整的代码示例,该仓库位于github.com/PacktPublishing/Machine-Learning-with-Go-Quick-Start-Guide

初始化示例目录和下载数据集

按照我们之前实施的过程,创建一个空目录来存放代码文件。在打开 gophernotes 之前,从www.cs.jhu.edu/~mdredze/datasets/sentiment/processed_acl.tar.gz下载数据集并将其解压到datasets/words目录下。在大多数 Linux 发行版中,您可以使用以下脚本完成此操作:

mkdir -p datasets/words && \
wget http://www.cs.jhu.edu/~mdredze/datasets/sentiment/processed_acl.tar.gz -O datasets/words-temp.tar.gz && \
tar xzvf datasets/words-temp.tar.gz -C datasets/words && \
rm datasets/words-temp.tar.gz

现在,启动 gophernotes 并导航到/usr/share/notebooks。通过点击New | Go创建一个新的 Notebook。您将看到一个空白的 Jupyter Notebook:

Jupyter 中的输入单元格带有In标签。当您在一个输入单元格中运行代码(Shift + Enter)时,将创建一个新的输出单元格,其中包含结果,并标记为Out。每个单元格都按其执行顺序编号。例如,In [1]单元格是在给定会话中运行的第一个单元格。

尝试运行一些 Go 语句,如下面的代码片段:

a := 1
import "fmt"
fmt.Println("Hello, world")
a

特别注意,即使没有调用fmt.Println()a变量也会在输出单元格中显示。

在一个会话中定义的所有导入、变量和函数都将保留在内存中,即使你删除了输入单元格。要清除当前作用域,请转到内核 | 重新启动。

加载数据集文件

数据处理的基本任务之一是读取输入文件并加载其内容。完成此任务的一种简单方法是使用 io/ioutil 工具函数 ReadFile。与 .go 文件不同,在 .go 文件中你需要将此代码放在你的 main 函数内部,使用 gophernotes,你可以运行以下代码而不需要声明任何函数:

import "io/ioutil"

const kitchenReviews = "../datasets/words/processed_acl/kitchen"

positives, err := ioutil.ReadFile(kitchenReviews + "/positive.review")
negatives, err2 := ioutil.ReadFile(kitchenReviews + "/negative.review")
if err != nil || err2 != nil {
 fmt.Println("Error(s)", err, err2)
}

上述代码将把具有积极情感的厨房产品评论内容加载到名为 positives 的字节切片中,将具有消极情感的评论内容加载到名为 negatives 的字节切片中。如果你已正确下载数据集并运行此代码,它不应该输出任何内容,因为没有错误。如果有任何错误出现,请检查数据集文件是否已提取到正确的文件夹。

如果你已经在文本编辑器中打开了 positive.reviewnegative.review 文件,你可能已经注意到它们是以空格或换行符分隔的对列表,即 phrase:frequency。例如,积极评论的开始如下:

them_it:1 hovering:1 and_occasional:1 cousin_the:2 fictional_baudelaire:1 their_struggles:1

在下一小节中,我们将解析这些对到 Go 结构体中。

将内容解析到结构体中

我们将使用 strings 包将数据文件的 内容解析成对数组的切片。字符串切片中的每个项目将包含一个对,例如 them_it:1。然后我们将进一步通过冒号符号分割这个对,并使用 strconv 包将整数频率解析为 int。每个 Pair 将是以下类型:

type Pair struct {
  Phrase string
  Frequency int
}

我们将按以下方式操作:

  1. 首先,观察这些对之间的分隔可以是换行符 (\n) 或空格。我们将使用字符串包中的 strings.Fields 函数,该函数将字符串按任何连续的空白字符分割:
pairsPositive := strings.Fields(string(positives))
pairsNegative := strings.Fields(string(negatives))
  1. 现在,我们将迭代每个对,通过冒号分隔符分割,并使用 strconv 包将频率解析为整数:
// pairsAndFilters returns a slice of Pair, split by : to obtain the phrase and frequency,
// as well as a map of the phrases that can be used as a lookup table later.
func pairsAndFilters(splitPairs []string) ([]Pair, map[string]bool) {
  var (
    pairs []Pair
    m map[string]bool
  )
  m = make(map[string]bool)
  for _, pair := range splitPairs {
    p := strings.Split(pair, ":")
    phrase := p[0]
    m[phrase] = true
    if len(p) < 2 {
      continue
    }
    freq, err := strconv.Atoi(p[1])
    if err != nil {
      continue
    }
    pairs = append(pairs, Pair{
      Phrase: phrase,
      Frequency: freq,
    })
  }
  return pairs, m
}
  1. 我们还将返回一个短语映射,以便我们可以在以后排除正负评论交集中的短语。这样做的原因是,正负评论中共同出现的单词不太可能是积极或消极情感的特征。这是通过以下函数完成的:
// exclude returns a slice of Pair that does not contain the phrases in the exclusion map
func exclude(pairs []Pair, exclusions map[string]bool) []Pair {
  var ret []Pair
  for i := range pairs {
    if !exclusions[pairs[i].Phrase] {
      ret = append(ret, pairs[i])
    }
  }
  return ret
}

  1. 最后,我们将此应用于我们的对数组切片:
parsedPositives, posPhrases := pairsAndFilters(pairsPositive)
parsedNegatives, negPhrases := pairsAndFilters(pairsNegative)
parsedPositives = exclude(parsedPositives, negPhrases)
parsedNegatives = exclude(parsedNegatives, posPhrases)

下一步是将解析好的对加载到 Gota 中,这是 Go 的数据处理库。

将数据加载到 Gota 数据框中

Gota 库包含数据框、系列和一些通用数据处理算法的实现^([12])。数据框的概念对于许多流行的数据科学库和语言(如 Python 的 pandas、R 和 Julia)至关重要。简而言之,数据框是一系列列表(称为系列),每个列表的长度都相同。每个列表都有一个名称——列名或系列名,具体取决于库所采用的命名法。这种抽象模仿了数据库表,并成为数学和统计工具的简单基本构建块。

Gota 库包含两个包:dataframeseries 包。series 包包含表示单个列表的函数和结构,而 dataframe 包处理整个数据框——即整个表格——作为一个整体。Go 开发者可能希望使用 Gota 来快速排序、过滤、聚合或执行关系操作,例如两个表之间的内连接,从而节省实现 sort 接口等样板代码^([13])。

使用 Gota 创建新的数据框有几种方法:

  • dataframe.New(se ...series.Series): 接受一个系列切片(可以通过 series.New 函数创建)。

  • dataframe.LoadRecords(records [][]string, options ...LoadOption): 接受一个字符串切片的切片。第一个切片将是一个表示列名的字符串切片。

  • dataframe.LoadStructs(i interface{}, options ...LoadOption): 接受一个结构体的切片。Gota 将使用反射根据结构体字段名称来确定列名。

  • dataframe.LoadMaps(maps []map[string][]interface{}): 接受一个列名到切片映射的切片。

  • dataframe.LoadMatrix(mat Matrix): 接受与 mat64 矩阵接口兼容的切片。

在我们的案例中,因为我们已经将数据解析到结构体中,我们将使用 LoadStructs 函数,为正面评论和负面评论创建一个数据框:

dfPos := dataframe.LoadStructs(parsedPositives)
dfNeg := dataframe.LoadStructs(parsedNegatives)

如果你想检查数据框的内容,即 df,只需使用 fmt.Println(df)。这将显示数据框的前 10 行,包括其列名和一些有用的元数据,例如总行数。

寻找最常见的短语

现在数据已经被解析,共现短语已经被过滤,结果短语/频率对已经被加载到数据框中,接下来要做的就是找到正面和负面评论中最常见的短语并显示它们。在不使用数据框的情况下,可以通过创建一个实现 sort 接口的 type ByFrequency []Pair 类型来完成这项工作,然后使用 sort.Reversesort.Sort 来按频率降序排列正面和负面配对。然而,通过使用 Gota,我们可以每个数据框一行代码就实现这个功能:

dfPos = dfPos.Arrange(dataframe.RevSort("Frequency"))
dfNeg = dfNeg.Arrange(dataframe.RevSort("Frequency"))

现在打印数据框会显示厨房用品正面和负面评论中最常见的 10 个短语。对于正面评论,我们有以下输出:

[46383x2] DataFrame

    Phrase       Frequency

 0: tic-tac-toe  10       

 1: wusthoff     7        

 2: emperor      7        

 3: shot_glasses 6        

 4: pulp         6        

 5: games        6        

 6: sentry       6        

 7: gravel       6        

 8: the_emperor  5        

 9: aebleskivers 5        

    ...          ...      

    <string>     <int>    

对于负面评论,我们有以下输出:

[45760x2] DataFrame

    Phrase          Frequency
 0: seeds           9        

 1: perculator      7        

 2: probes          7        

 3: cork            7        

 4: coffee_tank     5        

 5: brookstone      5        

 6: convection_oven 5        

 7: black_goo       5        

 8: waring_pro      5        

 9: packs           5        

    ...             ...      

    <string>        <int>    

这完成了本例。在下一节中,我们将更详细地介绍 Gota 的其他转换和处理功能。

示例 - 使用 gonum/plot 探索身体质量指数数据

在上一节中,我们介绍了 gophernotes 和 Gota。在本节中,我们将探索包含 500 个性别、身高和 BMI 指数样本的数据集。我们将使用 gonum/plot 库来完成这项工作。这个库最初是 2012 年 Plotinum 库的分支^([15]),它包含几个使 Go 中的数据可视化变得更容易的包^([16]):

  • plot 包包含布局和格式化接口。

  • plotter 包抽象了常见图表类型(如柱状图、散点图等)的布局和格式化。

  • plotutil 包包含常见图表类型的实用函数。

  • vg 包公开了一个用于矢量图形的 API,在将图表导出到其他软件时特别有用。我们不会介绍这个包。

安装 gonum 和 gonum/plot

无论你是按照之前建议使用 Docker 镜像运行 gophernotes,还是使用其他方法,你都需要使用 gonum/plot。为此,运行 go get gonum.org/v1/plot/... 命令。如果你没有安装 gonum 库,并且没有使用 gophernotes Docker 镜像,你需要使用 go get github.com/gonum/... 命令单独安装它。

要从 Jupyter 打开终端,打开树视图(默认视图)的 Web UI,然后点击 新建 | 终端

注意,尽管它们的名称相似,但 gonum 和 gonum/plot 并不属于同一个仓库,因此你需要分别安装它们。

加载数据

如果你已经克隆了项目仓库,它将已经包含在 datasets/bmi 文件夹中的 500 人 BMI 数据集。你也可以从 Kaggle^([14]) 下载数据集。数据集是一个包含以下几行数据的单个 CSV 文件:

Gender,Height,Weight,Index
Male,174,96,4
Male,189,87,2
Female,185,110,4
Female,195,104,3
Male,149,61,3
...

与上一节类似,我们将使用 io/ioutil 读取文件到字节切片,但这次,我们将利用 Gota 的 ReadCSV 方法(该方法接受一个 io.Reader 作为参数)直接将数据加载到数据框中,无需预处理:

b, err := ioutil.ReadFile(path)
if err != nil {
  fmt.Println("Error!", err)
}
df := dataframe.ReadCSV(bytes.NewReader(b))

检查数据框以确保数据已正确加载:

[500x4] DataFrame

    Gender   Height Weight Index
 0: Male     174    96     4    

 1: Male     189    87     2    

 2: Female   185    110    4    

 3: Female   195    104    3    

 4: Male     149    61     3    

 5: Male     189    104    3    

 6: Male     147    92     5    

 7: Male     154    111    5    

 8: Male     174    90     3    

 9: Female   169    103    4    

    ...      ...    ...    ...  

    <string> <int>  <int>  <int>

注意,序列的数据类型已被自动推断。

理解数据序列的分布

了解每个序列的一个好方法是绘制直方图。这将给你一个关于每个序列如何分布的印象。使用 gonum/plot,我们将为每个序列绘制直方图。然而,在我们绘制任何内容之前,我们可以通过 Gota 快速访问一些摘要统计信息,以获得对数据集的基本了解:

fmt.Println("Minimum", df.Col("Height").Min())
fmt.Println("Maximum", df.Col("Height").Max())
fmt.Println("Mean", df.Col("Height").Mean())
fmt.Println("Median", df.Col("Height").Quantile(0.5))

这告诉我们,样本个体的身高介于 140 厘米和 199 厘米之间,他们的平均身高和中位数分别为 169 厘米和 170 厘米,而平均数和中位数如此接近表明偏度较低——也就是说,分布是对称的。

要同时为所有列实现这一点的更快方法,请使用dataframe.Describe函数。这将生成另一个包含每列摘要统计数据的 dataframe:

[7x5] DataFrame

    column   Gender   Height     Weight     Index   
 0: mean     -        169.944000 106.000000 3.748000

 1: stddev   -        16.375261  32.382607  1.355053

 2: min      Female   140.000000 50.000000  0.000000

 3: 25%      -        156.000000 80.000000  3.000000

 4: 50%      -        170.000000 106.000000 4.000000

 5: 75%      -        184.000000 136.000000 5.000000
 6: max      Male     199.000000 160.000000 5.000000

    <string> <string> <float>    <float>    <float> 

现在,我们将使用直方图可视化分布。首先,我们需要将 Gota dataframe 的某一列转换为绘图友好的plotter.Values切片。这可以通过以下实用函数完成:

// SeriesToPlotValues takes a column of a Dataframe and converts it to a gonum/plot/plotter.Values slice.
// Panics if the column does not exist.
func SeriesToPlotValues(df dataframe.DataFrame, col string) plotter.Values {
  rows, _ := df.Dims()
  v := make(plotter.Values, rows)
  s := df.Col(col)
  for i := 0; i < rows; i++ {
    v[i] = s.Elem(i).Float()
  }
  return v
}

dataframe.Col函数从给定的 dataframe 中提取所需的列——在我们的例子中是一个单独的列。您还可以使用dataframe.Select,它接受字符串切片的列名,以返回只包含所需列的 dataframe。这可以用于丢弃不必要的数据。

现在,我们可以使用 gonum/plot 创建给定列的直方图的 JPEG 图像,并选择一个标题:

// HistogramData returns a byte slice of JPEG data for a histogram of the column with name col in the dataframe df.
func HistogramData(v plotter.Values, title string) []byte {
  // Make a plot and set its title.
  p, err := plot.New()
  if err != nil {
    panic(err)
  }
  p.Title.Text = title
  h, err := plotter.NewHist(v, 10)
  if err != nil {
    panic(err)
  }
  //h.Normalize(1) // Uncomment to normalize the area under the histogram to 1
  p.Add(h)
  w, err := p.WriterTo(5*vg.Inch, 4*vg.Inch, "jpg")
  if err != nil {
    panic(err)
  }
  var b bytes.Buffer
  writer := bufio.NewWriter(&b)
  w.WriteTo(writer)
  return b.Bytes()
}

要使用 gophernotes 显示结果绘图,请使用显示对象的适当方法。在这种情况下,我们生成一个 JPEG 图像,因此调用display.JPEG与前面代码生成的字节切片将显示输出单元格中的绘图。完整的代码输入单元格如下:

Display.JPEG(HistogramData(SeriesToPlotValues(df, "Age"), "Age Histogram"))

通常,从 gonum 的内置绘图器创建新绘图的步骤如下:

  1. 使用plot.New()创建一个新的绘图——这就像绘图将存在的画布。

  2. 设置任何绘图属性,例如其标题。

  3. 创建一个新的基于可用类型(BarChartBoxPlotColorBarContourHeatMapHistogramLineQuartPlotSankeyScatter)的绘图器。

  4. 设置任何绘图器属性,并通过调用其Add方法将绘图器添加到绘图中。

  5. 如果您想通过 gophernotes 显示绘图,请使用WriterTo方法和一个字节数组缓冲区将绘图数据输出为字节数组的切片,可以传递给内置的显示对象。否则,使用p.Save将图像保存到文件。

如果您想在 gophernotes 中显示图像而不是保存它,可以使用绘图器的Save方法。例如,p.Save(5*vg.Inch, 4*vg.Inch, title + ".png")将绘图保存为 5 英寸 x 4 英寸的 PNG 文件。

500 人体重/身高/BMI 数据集的结果直方图如下:

图片

在下面的例子中,我们不仅将加载数据并可视化,还将对其进行转换,使其更适合与机器学习算法一起使用。

示例 - 使用 Gota 预处理数据

机器学习算法训练过程的质量和速度取决于输入数据的质量。虽然许多算法对无关列和非规范化的数据具有鲁棒性,但有些则不是。例如,许多模型需要数据输入规范化,使其位于 0 到 1 之间。在本节中,我们将探讨使用 Gota 进行数据预处理的快速简单方法。对于这些示例,我们将使用包含 1,035 条记录的身高(英寸)和体重(磅)的主联赛棒球球员数据集^([17])。根据 UCLA 网站上的描述,数据集包含以下特征:

  • 姓名: 球员姓名

  • 队伍: 球员所属的棒球队

  • 位置: 球员的位置

  • 身高(英寸): 球员身高

  • 体重(磅): 球员体重,单位为磅

  • 年龄: 记录时的球员年龄

为了这个练习的目的,我们将以以下方式预处理数据:

  • 删除姓名和队伍列

  • 将身高和体重列转换为浮点类型

  • 过滤掉体重大于或等于 260 磅的球员

  • 标准化身高和体重列

  • 将数据分为训练集和验证集,其中训练集大约包含 70%的行,验证集包含 30%

将数据加载到 Gota 中

数据集以 HTML 表格的形式提供在 UCLA 网站上^([17])。在本书的配套仓库中,你可以找到一个 CSV 版本。要快速将 HTML 表格转换为 CSV 格式,而无需编写任何代码,首先选中表格,然后将其复制并粘贴到电子表格程序,如 Microsoft Excel 中。然后,将电子表格保存为 CSV 文件。在文本编辑器中打开此文件,以确保文件中没有碎片或多余的行。

使用dataframe.ReadCSV方法加载数据集。检查 dataframe 会产生以下输出:

[1034x6] DataFrame

    Name            Team     Position       Height(inches) Weight(pounds) ...

 0: Adam_Donachie   BAL      Catcher        74             180            ...

 1: Paul_Bako       BAL      Catcher        74             215            ...

 2: Ramon_Hernandez BAL      Catcher        72             210            ...

 3: Kevin_Millar    BAL      First_Baseman  72             210            ...

 4: Chris_Gomez     BAL      First_Baseman  73             188            ...

 5: Brian_Roberts   BAL      Second_Baseman 69             176            ...

 6: Miguel_Tejada   BAL      Shortstop      69             209            ...

 7: Melvin_Mora     BAL      Third_Baseman  71             200            ...

 8: Aubrey_Huff     BAL      Third_Baseman  76             231            ...

 9: Adam_Stern      BAL      Outfielder     71             180            ...

    ...             ...      ...            ...            ...            ...

    <string>        <string> <string>       <int>          <int>          ...

Not Showing: Age <float>

删除和重命名列

对于这个练习,我们决定我们不需要姓名队伍列。我们可以使用 dataframe 的Select方法来指定我们希望保留的列名字符串的切片:

df = df.Select([]string{"Position", "Height(inches)", "Weight(pounds)", "Age"})

在此同时,身高体重列应该重命名以去除单位。这可以通过Rename方法实现:

df = df.Rename("Height", "Height(inches)")
df = df.Rename("Weight", "Weight(pounds)")

得到的数据集如下:

[1034x4] DataFrame

    Position       Height Weight Age      

 0: Catcher        74     180    22.990000

 1: Catcher        74     215    34.690000

 2: Catcher        72     210    30.780000

 3: First_Baseman  72     210    35.430000

 4: First_Baseman  73     188    35.710000

 5: Second_Baseman 69     176    29.390000

 6: Shortstop      69     209    30.770000

 7: Third_Baseman  71     200    35.070000

 8: Third_Baseman  76     231    30.190000

 9: Outfielder     71     180    27.050000

    ...            ...    ...    ...      

    <string>       <int>  <int>  <float>  

将列转换为不同的类型

我们的数据框现在具有正确的列,且列名更简洁。然而,身高和体重列的类型为int,而我们需要它们为float类型,以便正确规范化它们的值。最容易的方法是在首次将数据加载到 dataframe 时添加此LoadOption。即func WithTypes(coltypes map[string]series.Type) LoadOption接受一个列名到系列类型的映射,我们可以使用它来在加载时执行转换。

然而,假设我们没有这样做。在这种情况下,我们通过用具有正确类型的新序列替换列来转换列类型。要生成此序列,我们可以使用 series.New 方法,以及 df.Col 来隔离感兴趣的列。例如,要从当前高度序列生成浮点数序列,我们可以使用以下代码:

heightFloat := series.New(df.Col("Height"), series.Float, "Height")

要替换列,我们可以使用 Mutate 方法:

df.Mutate(heightFloat)

现在对 HeightWeight 列都这样做会产生以下输出:

[1034x4] DataFrame

    Position       Height Weight Age      
 0: Catcher        74.00000     180.00000    22.990000

 1: Catcher        74.00000     215.00000    34.690000

 2: Catcher        72.00000     210.00000    30.780000

 3: First_Baseman  72.00000     210.00000    35.430000

 4: First_Baseman  73.00000     188.00000    35.710000

 5: Second_Baseman 69.00000     176.00000    29.390000

 6: Shortstop      69.00000     209.00000    30.770000

 7: Third_Baseman  71.00000     200.00000    35.070000

 8: Third_Baseman  76.00000     231.00000    30.190000

 9: Outfielder     71.00000     180.00000    27.050000

    ...            ...    ...    ...      

    <string>       <float>  <float>  <float> 

过滤掉不需要的数据

假设我们在探索数据后,不希望保留玩家体重大于或等于 260 磅的样本。这可能是因为没有足够重的玩家样本,因此任何分析都不会代表整个玩家群体。这样的玩家可以被称为当前数据集的异常值

你可以在 godoc.org/github.com/kniren/gota 找到 Gota 库的参考(Godocs)。

Gota 数据帧可以使用 Filter 函数进行过滤。该函数接受一个 dataframe.F 结构,它由目标列、比较器和值组成,例如 {"Column", series.Eq, 1},这将仅匹配 Column 等于 1 的行。可用的比较器如下:

  • series.Eq: 仅保留等于给定值的行

  • series.Neq: 仅保留不等于给定值的行

  • series.Greater: 仅保留大于给定值的行

  • series.GreaterEq: 仅保留大于或等于给定值的行

  • series.Less: 仅保留小于给定值的行

  • series.LessEq: 仅保留小于或等于给定值的行

series.Comparator 类型是字符串的一个别名。这些字符串与 Go 语言本身使用的字符串相同。例如,series.Neq 等同于 "!="

对于这个练习,我们将应用序列。我们将使用 less 过滤器来删除体重大于或等于 260 磅的行:

df = df.Filter(dataframe.F{"Weight", "<", 260})

归一化身高、体重和年龄列

数据归一化,也称为特征缩放,是将一组独立变量转换以映射到相同范围的过程。有几种方法可以实现这一点:

  • 缩放 (最小/最大归一化):这将线性地将变量范围映射到 [0,1] 范围,其中序列的最小值映射到 0,最大值映射到 1。这是通过应用以下公式实现的:

图片

  • 均值归一化:如果应用以下公式,这将映射变量范围:

图片

  • 标准化 (z 分数归一化):这是一种非常常见的用于机器学习应用的归一化方法,它使用均值和标准差将值序列转换为它们的 z 分数,即数据点相对于均值的多少个标准差。这是通过计算序列的均值和标准差,然后应用以下公式来完成的:

图片

注意,这并不保证将变量映射到封闭范围内。

可以使用以下实用函数实现缩放:

// rescale maps the given column values onto the range [0,1]
func rescale(df dataframe.DataFrame, col string) dataframe.DataFrame {
  s := df.Col(col)
  min := s.Min()
  max := s.Max()
  v := make([]float64, s.Len(), s.Len())
  for i := 0; i < s.Len(); i++ {
    v[i] = (s.Elem(i).Float() - min) / (max - min)
  }
  rs := series.Floats(v)
  rs.Name = col
  return df.Mutate(rs)
}

可以使用以下实用函数实现均值归一化:

// meanNormalise maps the given column values onto the range [-1,1] by subtracting mean and dividing by max - min
func meanNormalise(df dataframe.DataFrame, col string) dataframe.DataFrame {
  s := df.Col(col)
  min := s.Min()
  max := s.Max()
  mean := s.Mean()
  v := make([]float64, s.Len(), s.Len())
  for i := 0; i < s.Len(); i++ {
    v[i] = (s.Elem(i).Float() - mean) / (max - min)
  }
  rs := series.Floats(v)
  rs.Name = col
  return df.Mutate(rs)
}

可以使用以下实用函数实现标准化:

// meanNormalise maps the given column values onto the range [-1,1] by subtracting mean and dividing by max - min
func standardise(df dataframe.DataFrame, col string) dataframe.DataFrame {
  s := df.Col(col)
  std := s.StdDev()
  mean := s.Mean()
  v := make([]float64, s.Len(), s.Len())
  for i := 0; i < s.Len(); i++ {
    v[i] = (s.Elem(i).Float() - mean) / std
  }
  rs := series.Floats(v)
  rs.Name = col
  return df.Mutate(rs)
}

对于这个例子,我们将使用以下代码对HeightWeight列应用缩放:

df = rescale(df, "Height")
df = rescale(df, "Weight")

结果如下。请注意,HeightWeight列的值现在位于 0 到 1 之间,正如预期的那样:

[1034x4] DataFrame

    Position       Height   Weight   Age      

 0: Catcher        0.437500 0.214286 22.990000

 1: Catcher        0.437500 0.464286 34.690000

 2: Catcher        0.312500 0.428571 30.780000

 3: First_Baseman  0.312500 0.428571 35.430000

 4: First_Baseman  0.375000 0.271429 35.710000

 5: Second_Baseman 0.125000 0.185714 29.390000

 6: Shortstop      0.125000 0.421429 30.770000

 7: Third_Baseman  0.250000 0.357143 35.070000

 8: Third_Baseman  0.562500 0.578571 30.190000

 9: Outfielder     0.250000 0.214286 27.050000

    ...            ...      ...      ...      

    <string>       <float>  <float>  <float> 

用于获取训练/验证子集的采样

在训练机器学习算法时,保留数据集的一部分用于验证是有用的。这用于测试模型对先前未见数据的泛化能力,从而确保当面对不属于训练集的现实生活数据时,其有用性。没有验证步骤,就无法确定模型是否具有好的预测能力。

尽管没有关于为验证保留多少数据集的公认惯例,但通常保留 10%到 30%的比例。关于为验证保留多少数据集的研究表明,模型的可调整参数越多,需要保留的数据集比例就越小^([18])。在这个练习中,我们将把我们的 MLB 数据集分为两个子集:一个包含大约 70%样本的训练子集,一个包含 30%样本的验证子集。有两种方法可以做到这一点:

  • 选择前 70%的行以形成训练子集的一部分,剩下的 30%形成验证子集的一部分

  • 选择随机的 70%样本形成训练子集,并使用剩余的样本进行验证

通常,为了避免确定性采样以确保两个子集都能代表总体人口,最好是避免确定性采样。为了实现随机采样,我们将使用math/rand包生成随机索引,并将其与 Gota 的dataframe.Subset方法结合。第一步是生成数据框索引的随机排列:

rand.Perm(df.Nrow())

现在,我们将从这个切片的前 70%用于训练,剩余的元素用于验证,结果如下所示:

// split splits the dataframe into training and validation subsets. valFraction (0 <= valFraction <= 1) of the samples
// are reserved for validation and the rest are for training.
func Split(df dataframe.DataFrame, valFraction float64) (training dataframe.DataFrame, validation dataframe.DataFrame) {
  perm := rand.Perm(df.Nrow())
  cutoff := int(valFraction * float64(len(perm)))
  training = df.Subset(perm[:cutoff])
  validation = df.Subset(perm[cutoff:len(perm)])
  return training, validation
}

将此应用于我们的数据框split(df, 0.7)产生以下输出。第一个数据框是训练子集,第二个是验证子集:

[723x4] DataFrame

    Position         Height   Weight   Age      
 0: Relief_Pitcher   0.500000 0.285714 25.640000

 1: Starting_Pitcher 0.500000 0.500000 33.410000

 2: Second_Baseman   0.375000 0.235714 28.200000

 3: Relief_Pitcher   0.562500 0.392857 33.310000

 4: Outfielder       0.187500 0.250000 27.450000

 5: Relief_Pitcher   0.500000 0.042857 27.320000

 6: Relief_Pitcher   0.562500 0.428571 40.970000

 7: Second_Baseman   0.250000 0.357143 33.150000

 8: Outfielder       0.312500 0.071429 25.180000

 9: Relief_Pitcher   0.562500 0.321429 29.990000

    ...              ...      ...      ...      

    <string>         <float>  <float>  <float>  

 [310x4] DataFrame

    Position         Height   Weight   Age      

 0: Relief_Pitcher   0.375000 0.285714 25.080000

 1: Relief_Pitcher   0.437500 0.285714 28.310000

 2: Outfielder       0.437500 0.357143 34.140000

 3: Shortstop        0.187500 0.285714 25.080000

 4: Starting_Pitcher 0.500000 0.428571 32.550000

 5: Outfielder       0.250000 0.250000 30.550000

 6: Starting_Pitcher 0.500000 0.357143 28.480000

 7: Third_Baseman    0.250000 0.285714 30.960000

 8: Catcher          0.250000 0.421429 30.670000

 9: Third_Baseman    0.500000 0.428571 25.480000

    ...              ...      ...      ...      

    <string>         <float>  <float>  <float>  

使用分类变量编码数据

在前面的数据框中,Position列是字符串。假设我们希望 ML 算法使用这个输入,因为,比如说,我们正在尝试预测球员的体重,而处于某些位置的球员往往有不同的身体组成。在这种情况下,我们需要编码字符串到一个算法可以使用的数值。

一种简单的方法是确定所有球员位置集合,并为集合中的每个成员分配一个递增的整数。例如,我们可能会得到{Relief_Pitcher, Starting_Pitcher, Shortstop, Outfielder,...}集合,然后我们将0分配给Relief_Pitcher1分配给Starting_Pitcher2分配给Shortstop,依此类推。然而,这种方法的问题在于数字的分配方式,因为它赋予了不存在分类的类别顺序以重要性。假设 ML 算法的一个步骤是计算跨类别的平均值。因此,它可能会得出结论,Starting_PitcherRelief_PitcherShortstop的平均值!其他类型的算法可能会推断出不存在的相关性。

为了解决这个问题,我们可以使用独热编码。这种编码方式会将具有 N 个可能值的分类列拆分为 N 列。每一列,对应于一个分类,将具有值1,当输入属于该列时,否则为0。这也允许存在一个输入样本可能属于多个分类的情况。

使用 Gota 生成给定列的独热编码的步骤如下:

  1. 列出分类列的唯一值

  2. 为每个唯一值创建一个新的序列,如果行属于该类别则映射为1,否则为0

  3. 通过添加步骤 2 中创建的序列并删除原始列来修改原始数据框

使用映射可以轻松地枚举唯一值:

func UniqueValues(df dataframe.DataFrame, col string) []string {
  var ret []string
  m := make(map[string]bool)
  for _, val := range df.Col(col).Records() {
    m[val] = true
  }
  for key := range m {
    ret = append(ret, key)
  }
  return ret
}

注意,这是使用series.Records方法来返回给定列的值作为字符串的切片。同时,注意返回值的顺序不一定每次都相同。使用UniqueValues(df, "Position")在我们的数据框上运行此函数会得到以下唯一值:

[Shortstop Outfielder Starting_Pitcher Relief_Pitcher Second_Baseman First_Baseman Third_Baseman Designated_Hitter Catcher]

第二步是遍历数据框,在过程中创建新的序列:

func OneHotSeries(df dataframe.DataFrame, col string, vals []string) []series.Series {
  m := make(map[string]int)
  s := make([]series.Series, len(vals), len(vals))
  //cache the mapping for performance reasons
  for i := range vals {
    m[vals[i]] = i
  }
  for i := range s {
    vals := make([]int, df.Col(col).Len(), df.Col(col).Len())
    for j, val := range df.Col(col).Records() {
      if i == m[val] {
        vals[j] = 1
      }
    }
    s[i] = series.Ints(vals)
  }
  for i := range vals {
    s[i].Name = vals[i]
  }
  return s
}

此函数将为分类变量的每个唯一值返回一个序列。这些序列将具有类别的名称。在我们的例子中,我们可以使用OneHotSeries(df, "Position", UniqueValues(df, "Position"))来调用它。现在,我们将修改原始数据框并删除Position列:

ohSeries := OneHotSeries(df, "Position", UniqueValues(df, "Position"))
for i := range ohSeries {
  df = df.Mutate(ohSeries[i])
}

打印df会得到以下结果:

[1034x13] DataFrame

    Position       Height   Weight   Age       Shortstop Catcher ...

 0: Catcher        0.437500 0.214286 22.990000 0         1       ...

 1: Catcher        0.437500 0.464286 34.690000 0         1       ...

 2: Catcher        0.312500 0.428571 30.780000 0         1       ...

 3: First_Baseman  0.312500 0.428571 35.430000 0         0       ...

 4: First_Baseman  0.375000 0.271429 35.710000 0         0       ...

 5: Second_Baseman 0.125000 0.185714 29.390000 0         0       ...

 6: Shortstop      0.125000 0.421429 30.770000 1         0       ...

 7: Third_Baseman  0.250000 0.357143 35.070000 0         0       ...

 8: Third_Baseman  0.562500 0.578571 30.190000 0         0       ...

 9: Outfielder     0.250000 0.214286 27.050000 0         0       ...

    ...            ...      ...      ...       ...       ...     ...

    <string>       <float>  <float>  <float>   <int>     <int>   ...

Not Showing: Second_Baseman <int>, Outfielder <int>, Designated_Hitter <int>,

Starting_Pitcher <int>, Relief_Pitcher <int>, First_Baseman <int>, Third_Baseman <int>

总结来说,只需使用df = df.Drop("Position")删除Position列。

概述

在本章中,我们介绍了如何为 Go 设置一个针对机器学习应用优化的开发环境。我们解释了如何安装交互式环境 Jupyter,以使用 Gota 和 gonum/plot 等库加速数据探索和可视化。

我们还介绍了一些基本的数据处理步骤,例如过滤异常值、删除不必要的列和归一化。最后,我们讨论了采样。本章介绍了机器学习生命周期的前几个步骤:数据获取、探索和准备。现在你已经阅读了本章,你已经学会了如何将数据加载到 Gota 数据框中,如何使用数据框和序列包来处理和准备数据,使其符合所选算法的要求,以及如何使用 gonum 的 plot 包进行可视化。你还了解了不同的数据归一化方法,这是提高许多机器学习算法准确性和速度的重要步骤。

在下一章中,我们将介绍监督学习算法,并举例说明如何选择机器学习算法,训练它,并在未见过的数据上验证其预测能力。

进一步阅读

  1. 软件开发浪费。托德·塞达诺和保罗·拉尔夫。ICSE '17 第 39 届国际软件工程会议论文集。第 130-140 页。

  2. 请参阅官方 Go 安装说明golang.org/doc/install。获取日期:2019 年 2 月 19 日。

  3. jupyter.org/about. 获取日期:2019 年 2 月 19 日。

  4. github.com/jupyter/jupyter/wiki/Jupyter-kernels. 获取日期:2019 年 2 月 19 日。

  5. 查看更多说明,请参阅help.github.com/articles/working-with-jupyter-notebook-files-on-github/。获取日期:2019 年 2 月 19 日。

  6. colab.research.google.com. 获取日期:2019 年 2 月 19 日。

  7. nbviewer.jupyter.org/. 获取日期:2019 年 2 月 19 日。

  8. jupyter.org/hub. 获取日期:2019 年 2 月 19 日。

  9. github.com/jupyter/nbconvert. 获取日期:2019 年 2 月 19 日。

  10. 查看 Docker 安装说明,Linux 请参阅docs.docker.com/install/,Windows 请参阅docs.docker.com/docker-for-windows/install/。获取日期:2019 年 2 月 19 日。

  11. 约翰·布利策,马克·德雷兹,费尔南多·佩雷拉。传记,宝莱坞,音响盒和搅拌机:情感分类的领域自适应。计算语言学协会(ACL),2007 年。

  12. github.com/go-gota/gota. 获取日期:2019 年 2 月 19 日。

  13. godoc.org/sort#Interface. 获取日期:2019 年 2 月 19 日。

  14. www.kaggle.com/yersever/500-person-gender-height-weight-bodymassindex/version/2. 获取日期:2019 年 2 月 20 日。

  15. code.google.com/archive/p/plotinum/. 获取日期:2019 年 2 月 20 日。

  16. github.com/gonum/plot. 获取日期:2019 年 2 月 20 日。

  17. wiki.stat.ucla.edu/socr/index.php/SOCR_Data_MLB_HeightsWeights. 获取日期:2019 年 2 月 20 日。

  18. Guyon, Isabelle. 1996. A Scaling Law for the Validation-Set Training-Set Size Ratio. AT&T Bell Lab. 1.

第三章:监督学习

正如我们在第一章中学到的,监督学习是机器学习的两个主要分支之一。从某种意义上说,它与人类学习新技能的方式相似:有人向我们展示该怎么做,然后我们通过模仿他们的例子来学习。在监督学习算法的情况下,我们通常需要大量的例子,即大量的数据提供算法的输入以及预期输出应该是什么。算法将从这些数据中学习,然后能够根据它之前未见过的新输入预测输出。

使用监督学习可以解决大量问题。许多电子邮件系统会自动将新消息分类为重要或不重要,每当新消息到达收件箱时就会使用它。更复杂的例子包括图像识别系统,这些系统可以仅从输入像素值中识别图像内容([1])。这些系统最初是通过学习大量由人类手动标记的图像数据集来学习的,但随后能够自动对全新的图像进行分类。甚至可以使用监督学习来自动驾驶赛车:算法首先学习人类驾驶员如何控制车辆,最终能够复制这种行为([2])。

到本章结束时,您将能够使用 Go 实现两种类型的监督学习:

  • 分类,其中算法必须学习将输入分类到两个或多个离散类别中。我们将构建一个简单的图像识别系统来展示这是如何工作的。

  • 回归,其中算法必须学习预测一个连续变量,例如,在网站上出售的商品的价格。在我们的例子中,我们将根据输入预测房价,例如房屋的位置、大小和年龄。

在本章中,我们将涵盖以下主题:

  • 何时使用回归和分类

  • 如何使用 Go 机器学习库实现回归和分类

  • 如何衡量算法的性能

我们将涵盖构建监督学习系统涉及的两个阶段:

  • 训练,这是使用标记数据校准算法的学习阶段

  • 推理预测,即我们使用训练好的算法来实现其预期目的:从输入数据中进行预测

分类

在开始任何监督学习问题之前,第一步是加载数据并准备数据。我们将从加载MNIST Fashion 数据集^([3])开始,这是一个包含不同服装的小型、灰度图像集合。我们的任务是构建一个能够识别每张图像内容的系统;也就是说,它是否包含连衣裙、鞋子、外套等?

首先,我们需要通过在代码仓库中运行download-fashion-mnist.sh脚本来下载数据集。然后,我们将将其加载到 Go 中:

import (
    "fmt"
     mnist "github.com/petar/GoMNIST"
    "github.com/kniren/gota/dataframe"
    "github.com/kniren/gota/series"
    "math/rand"
    "github.com/cdipaolo/goml/linear"
    "github.com/cdipaolo/goml/base"
    "image"
    "bytes"
    "math"
    "github.com/gonum/stat"
    "github.com/gonum/integrate"
)
set, err := mnist.ReadSet("../datasets/mnist/images.gz", "../datasets/mnist/labels.gz")

让我们先看看图像的样本。每个图像都是 28 x 28 像素,每个像素的值在 0 到 255 之间。我们将使用这些像素值作为算法的输入:我们的系统将从图像中接受 784 个输入,并使用它们来根据包含的衣物项目对图像进行分类。在 Jupyter 中,您可以按以下方式查看图像:

set.Images[1]

这将显示数据集中 28 x 28 像素的图像之一,如下面的图像所示:

为了使这些数据适合机器学习算法,我们需要将其转换为我们在第二章,“设置开发环境”中学到的 dataframe 格式。首先,我们将从数据集中加载前 1000 张图像:

func MNISTSetToDataframe(st *mnist.Set, maxExamples int) dataframe.DataFrame {
 length := maxExamples
 if length > len(st.Images) {
 length = len(st.Images)
 }
 s := make([]string, length, length)
 l := make([]int, length, length)
 for i := 0; i < length; i++ {
 s[i] = string(st.Images[i])
 l[i] = int(st.Labels[i])
 }
 var df dataframe.DataFrame
 images := series.Strings(s)
 images.Name = "Image"
 labels := series.Ints(l)
 labels.Name = "Label"
 df = dataframe.New(images, labels)
 return df
}

df := MNISTSetToDataframe(set, 1000)

我们还需要一个包含每个图像可能标签的字符串数组:

categories := []string{"tshirt", "trouser", "pullover", "dress", "coat", "sandal", "shirt", "shoe", "bag", "boot"}

首先保留一小部分数据以测试最终算法非常重要。这使我们能够衡量算法在训练过程中未使用的新数据上的表现。如果您不这样做,您很可能会构建一个在训练期间表现良好但在面对新数据时表现不佳的系统。首先,我们将使用 75%的图像来训练我们的模型,25%的图像来测试它。

在使用监督学习时,将数据分成训练集测试集是一个关键步骤。通常,我们会保留 20-30%的数据用于测试,但如果您的数据集非常大,您可能可以使用更少的比例。

使用上一章中的Split(df dataframe.DataFrame, valFraction float64)函数来准备这两个数据集:

training, validation := Split(df, 0.75)

一个简单的模型——逻辑分类器

解决我们问题的最简单算法之一是逻辑分类器。这是数学家所说的线性模型,我们可以通过考虑一个简单的例子来理解它,在这个例子中,我们试图将以下两个图表上的点分类为圆圈或正方形。线性模型将尝试通过画一条直线来分隔这两种类型的点。这在左边的图表上效果很好,其中输入(图表轴上的)与输出(圆圈或正方形)之间的关系很简单。然而,它不适用于右边的图表,在右边的图表中,无法使用直线将点分成两个正确的组:

面对一个新的机器学习问题时,建议你从一个线性模型作为基线开始,然后将其与其他模型进行比较。尽管线性模型无法捕捉输入数据中的复杂关系,但它们易于理解,通常实现和训练速度也很快。你可能发现线性模型对于你正在解决的问题已经足够好,从而节省了时间,无需实现更复杂的模型。如果不是这样,你可以尝试不同的算法,并使用线性模型来了解它们的效果有多好。

基线是一个简单的模型,你可以将其用作比较不同机器学习算法时的参考点。

回到我们的图像数据集,我们将使用逻辑分类器来决定一张图片是否包含裤子。首先,让我们做一些最终的数据准备:将标签简化为“裤子”(true)或“非裤子”(false):

func EqualsInt(s series.Series, to int) (*series.Series, error) {
 eq := make([]int, s.Len(), s.Len())
 ints, err := s.Int()
 if err != nil {
 return nil, err
 }
 for i := range ints {
 if ints[i] == to {
 eq[i] = 1
 }
    }
    ret := series.Ints(eq)
    return &ret, nil
}

trainingIsTrouser, err1 := EqualsInt(training.Col("Label"), 1)
validationIsTrouser, err2 := EqualsInt(validation.Col("Label"), 1)
if err1 != nil || err2 != nil {
    fmt.Println("Error", err1, err2)
}

我们还将对像素数据进行归一化,使其不再是存储在 0 到 255 之间的整数,而是表示为 0 到 1 之间的浮点数:

许多监督式机器学习算法只有在数据归一化(即缩放,使其在 0 到 1 之间)的情况下才能正常工作。如果你在训练算法时遇到困难,请确保你已经正确归一化了数据。

func NormalizeBytes(bs []byte) []float64 {
    ret := make([]float64, len(bs), len(bs))
    for i := range bs {
        ret[i] = float64(bs[i])/255.
    }
    return ret
}

func ImageSeriesToFloats(df dataframe.DataFrame, col string) [][]float64 {
    s := df.Col(col)
    ret := make([][]float64, s.Len(), s.Len())
    for i := 0; i < s.Len(); i++ {
        b := []byte(s.Elem(i).String())
        ret[i] = NormalizeBytes(b)
    }
    return ret
}

trainingImages := ImageSeriesToFloats(training, "Image")
validationImages := ImageSeriesToFloats(validation, "Image")

在正确准备数据之后,现在终于到了创建逻辑分类器并对其进行训练的时候了:

model := linear.NewLogistic(base.BatchGA, 1e-4, 1, 150, trainingImages, trainingIsTrouser.Float())

//Train
err := model.Learn()
if err != nil {
  fmt.Println(err)
}

衡量性能

现在我们已经训练好了模型,我们需要通过将模型对每张图片的预测与真实情况(图片是否是一双裤子)进行比较来衡量其表现的好坏。一个简单的方法是测量准确率

准确率衡量算法能够正确分类输入数据的比例,例如,如果算法的 100 个预测中有 90 个是正确的,那么准确率为 90%。

在我们的 Go 代码示例中,我们可以通过遍历验证数据集并计算正确分类的图片数量来测试模型。这将输出模型准确率为 98.8%:

//Count correct classifications
var correct = 0.
for i := range validationImages {
  prediction, err := model.Predict(validationImages[i])
  if err != nil {
    panic(err)
  }

  if math.Round(prediction[0]) == validationIsTrouser.Elem(i).Float() {
    correct++
  }
}

//accuracy
correct / float64(len(validationImages))

精确率和召回率

测量准确率可能会非常误导。假设你正在构建一个系统来分类医疗患者是否会测试出罕见疾病,而在数据集中只有 0.1%的例子实际上是阳性的。一个非常差的算法可能会预测没有人会测试出阳性,然而它仍然有 99.9%的准确率,仅仅因为这种疾病很罕见。

一个分类比另一个分类有更多示例的数据集被称为不平衡。在衡量算法性能时,需要仔细处理不平衡数据集。

一种更好的衡量性能的方法是从将算法的每个预测放入以下四个类别之一开始:

图片

我们现在可以定义一些新的性能指标:

  • 精确度衡量的是模型真实预测中正确预测的比例。在下面的图中,这是从模型预测出的真实阳性(圆圈的左侧)除以模型所有阳性预测(圆圈中的所有内容)。

  • 召回率衡量模型在识别所有正例方面的好坏。换句话说,真实阳性(圆圈的左侧)除以所有实际为正的数据点(圆圈的整个左侧):

图片

上述图显示了模型在中心圆中预测为真实的数据点。实际上为真的点位于图的左侧一半。

精确度召回率是在处理不平衡数据集时更稳健的性能指标。它们的范围在 0 到 1 之间,其中 1 表示完美性能。

下面的代码是计算真实阳性和假阴性的总数:

//Count true positives and false negatives
var truePositives = 0.
var falsePositives = 0.
var falseNegatives = 0.
for i := range validationImages {
  prediction, err := model.Predict(validationImages[i])
  if err != nil {
    panic(err)
  }
  if validationIsTrouser.Elem(i).Float() == 1 {
    if math.Round(prediction[0]) == 0 {
      // Predicted false, but actually true
      falseNegatives++
    } else {
      // Predicted true, correctly
      truePositives++
    }
  } else {
    if math.Round(prediction[0]) == 1 {
      // Predicted true, but actually false
      falsePositives++
    }
  }
}

我们现在可以使用以下代码计算精确度和召回率:

//precision
truePositives / (truePositives + falsePositives)
//recall
truePositives / (truePositives + falseNegatives)

对于我们的线性模型,我们得到了 100%的精确度,这意味着没有假阳性,召回率为 90.3%。

ROC 曲线

另一种衡量性能的方法是更详细地观察分类器的工作方式。在我们的模型内部,发生两件事:

  • 首先,模型计算一个介于 0 到 1 之间的值,表示给定图像被分类为裤子对的可能性有多大。

  • 设置一个阈值,只有得分超过阈值的图像才被分类为裤子。设置不同的阈值可以在牺牲召回率的同时提高精确度,反之亦然。

如果我们查看模型输出在所有不同的阈值从 0 到 1 之间的情况,我们可以更了解它的有用性。我们使用称为接收者操作特征ROC)曲线的东西来做这件事,这是一个在不同阈值值下,数据集中真实阳性率与假阳性率的图表。以下三个示例显示了不良、中等和非常好的分类器的 ROC 曲线:

图片

通过测量这些 ROC 曲线下的阴影区域,我们得到一个衡量模型好坏的简单指标,这被称为曲线下面积AUC)。对于不良模型,这个值接近0.5,但对于非常好的模型,这个值接近1.0,表明模型可以同时实现高真实阳性率和低假阳性率。

gonum/stat包提供了一个用于计算 ROC 曲线的有用函数,一旦我们将模型扩展到处理数据集中的每个不同物品,我们就会使用它。

接收者操作特征,或ROC 曲线,是不同阈值值下真实阳性率与假阳性率的图表。它使我们能够可视化模型在分类方面的好坏。AUC 提供了一个简单的衡量分类器好坏的指标。

多分类模型

到目前为止,我们一直在使用二元分类;也就是说,如果图像显示一条裤子,则应输出true,否则输出false。对于某些问题,例如检测电子邮件是否重要,这可能就足够了。但在本例中,我们真正想要的是一个可以识别我们数据集中所有不同类型衣物的模型,即衬衫、靴子、连衣裙等。

对于某些算法实现,你可能需要首先对输出应用 one-hot 编码,如第二章中所示,设置开发环境。然而,在我们的例子中,我们将使用softmax 回归goml/linear中,这会自动完成这一步。我们可以通过简单地给它输入(像素值)和整数输出(0,1,2 等,代表 T 恤、裤子、开衫等)来训练模型:

model2 := linear.NewSoftmax(base.BatchGA, 1e-4, 1, 10, 100, trainingImages, training.Col("Label").Float())

//Train
err := model2.Learn()
if err != nil {
  fmt.Println(err)
}

当使用此模型进行推理时,它将为每个类别输出一个概率向量;也就是说,它告诉我们输入图像是 T 恤、裤子等的概率。这正是我们进行 ROC 分析所需要的,但如果我们要为每张图像提供一个单一的预测,我们可以使用以下函数来找到具有最高概率的类别:

func MaxIndex(f []float64) (i int) {
  var (
    curr float64
    ix int = -1
  )
  for i := range f {
    if f[i] > curr {
      curr = f[i]
      ix = i
    }
  }
  return ix
}

接下来,我们可以为每个单独的类别绘制 ROC 曲线和 AUC。以下代码将遍历验证数据集中的每个示例,并使用新模型为每个类别预测概率:

//create objects for ROC generation
//as per https://godoc.org/github.com/gonum/stat#ROC
y := make([][]float64, len(categories), len(categories))
classes := make([][]bool, len(categories), len(categories))
//Validate
for i := 0; i < validation.Col("Image").Len(); i++ {
  prediction, err := model2.Predict(validationImages[i])
  if err != nil {
    panic(err)
  }
  for j := range categories {
    y[j] = append(y[j], prediction[j])
    classes[j] = append(classes[j], validation.Col("Label").Elem(i).Float() != float64(j))
  }
}

//Calculate ROC
tprs := make([][]float64, len(categories), len(categories))
fprs := make([][]float64, len(categories), len(categories))

for i := range categories {
  stat.SortWeightedLabeled(y[i], classes[i], nil)
  tprs[i], fprs[i] = stat.ROC(0, y[i], classes[i], nil)
}

我们现在可以计算每个类别的 AUC 值,这表明我们的模型在某些类别上的表现优于其他类别:

for i := range categories {
  fmt.Println(categories[i])
  auc := integrate.Trapezoidal(fprs[i], tprs[i])
  fmt.Println(auc)
}

对于裤子,AUC 值为0.96,这表明即使是一个简单的线性模型在这种情况下也工作得非常好。然而,衬衫和开衫的得分都接近0.6。这从直观上是有道理的:衬衫和开衫看起来非常相似,因此模型正确识别它们要困难得多。我们可以通过为每个类别绘制 ROC 曲线作为单独的线条来更清楚地看到这一点:模型在衬衫和开衫上的表现最差,而在形状非常独特的衣物(如靴子、裤子、凉鞋等)上的表现最好。

以下代码加载 gonums 绘图库,创建 ROC 图,并将其保存为 JPEG 图像:

import (
  "gonum.org/v1/plot"
  "gonum.org/v1/plot/plotter"
  "gonum.org/v1/plot/plotutil"
  "gonum.org/v1/plot/vg"
  "bufio"
)

func plotROCBytes(fprs, tprs [][]float64, labels []string) []byte {
  p, err := plot.New()
  if err != nil {
    panic(err)
  }

  p.Title.Text = "ROC Curves"
  p.X.Label.Text = "False Positive Rate"
  p.Y.Label.Text = "True Positive Rate"

  for i := range labels {
    pts := make(plotter.XYs, len(fprs[i]))
    for j := range fprs[i] {
      pts[j].X = fprs[i][j]
      pts[j].Y = tprs[i][j]
    }
    lines, points, err := plotter.NewLinePoints(pts)
    if err != nil {
      panic(err)
    }
    lines.Color = plotutil.Color(i)
    lines.Width = 2
    points.Shape = nil

    p.Add(lines, points)
    p.Legend.Add(labels[i], lines, points)
  }

  w, err := p.WriterTo(5*vg.Inch, 4*vg.Inch, "jpg")
  if err != nil {
    panic(err)
  }
  if err := p.Save(5*vg.Inch, 4*vg.Inch, "Multi-class ROC.jpg"); err != nil {
    panic(err)
  }
  var b bytes.Buffer
  writer := bufio.NewWriter(&b)
  w.WriteTo(writer)
  return b.Bytes()
}

如果我们在 Jupyter 中查看图表,我们可以看到最差的类别紧贴着对角线,再次表明 AUC 接近0.5

图片

非线性模型——支持向量机

为了继续前进,我们需要使用不同的机器学习算法:一种能够对像素输入和输出类别之间的更复杂、非线性关系进行建模的算法。虽然一些主流的围棋机器学习库,如 Golearn,支持基本算法,如局部最小二乘法,但没有一个库支持像 Python 的 scikit-learn 或 R 的标准库那样广泛的算法集。因此,通常需要寻找实现绑定到广泛使用的 C 库的替代库,或者包含适用于特定问题的算法的可配置实现。对于这个例子,我们将使用一个称为支持向量机SVM)的算法。与线性模型相比,SVM 可能更难使用——它们有更多的参数需要调整——但它们的优势在于能够对数据中的更复杂模式进行建模。

SVM 是一种更高级的机器学习方法,可用于分类和回归。它们允许我们对输入数据应用,这意味着它们可以建模输入/输出之间的非线性关系。

SVM 模型的一个重要特性是它们能够使用核函数。简单来说,这意味着算法可以对输入数据进行变换,以便找到非线性模式。在我们的例子中,我们将使用LIBSVM库在图像数据上训练 SVM。LIBSVM 是一个开源库,具有多种语言的绑定,这意味着如果你想在 Python 的流行 scikit-learn 库中移植模型,它也非常有用。首先,我们需要做一些数据准备,使我们的输入/输出数据适合输入到 Go 库中:

trainingOutputs := make([]float64, len(trainingImages))
validationOutputs := make([]float64, len(validationImages))

ltCol:= training.Col("Label")
for i := range trainingImages {
    trainingOutputs[i] = ltCol.Elem(i).Float()
}

lvCol:= validation.Col("Label")
for i := range validationImages {
    validationOutputs[i] = lvCol.Elem(i).Float()
}

// FloatstoSVMNode converts a slice of float64 to SVMNode with sequential indices starting at 1
func FloatsToSVMNode(f []float64) []libsvm.SVMNode {
    ret := make([]libsvm.SVMNode, len(f), len(f))
    for i := range f {
        ret[i] = libsvm.SVMNode{
            Index: i+1,
            Value: f[i],
        }
    }
    //End of Vector
    ret = append(ret, libsvm.SVMNode{
        Index: -1,
        Value: 0,
    })
    return ret
}

接下来,我们可以设置 SVM 模型,并使用径向基函数RBF对其进行配置。RBF 核在 SVM 中是一个常见的选择,但训练时间比线性模型要长:

var (
  trainingProblem libsvm.SVMProblem
  validationProblem libsvm.SVMProblem
)

trainingProblem.L = len(trainingImages)
validationProblem.L = len(validationImages)
for i := range trainingImages {
  trainingProblem.X = append(trainingProblem.X, FloatsToSVMNode(trainingImages[i]))
}
trainingProblem.Y = trainingOutputs

for i := range validationImages {
  validationProblem.X = append(validationProblem.X, FloatsToSVMNode(validationImages[i]))
}
validationProblem.Y = validationOutputs

// configure SVM
svm := libsvm.NewSvm()
param := libsvm.SVMParameter{
  SvmType: libsvm.CSVC,
  KernelType: libsvm.RBF,
  C: 100,
  Gamma: 0.01,
  Coef0: 0,
  Degree: 3,
  Eps: 0.001,
  Probability: 1,
}

最后,我们可以将我们的模型拟合到 750 张图像的训练数据上,然后使用 svm.SVMPredictProbability 来预测概率,就像我们之前对线性多类模型所做的那样:

model := svm.SVMTrain(&trainingProblem, &param)

正如我们之前所做的那样,我们计算了 AUC 和 ROC 曲线,这表明该模型在各个方面的表现都更好,包括像衬衫和套头衫这样的困难类别:

图片

过度拟合和欠拟合

SVM 模型在我们的验证数据集上的表现比线性模型要好得多,但为了了解下一步该做什么,我们需要介绍机器学习中的两个重要概念:过度拟合欠拟合。这两个概念都指的是在训练模型时可能发生的问题。

如果一个模型欠拟合数据,它对输入数据中的模式解释得太简单,因此在评估训练数据集和验证数据集时表现不佳。这个问题还有另一个术语,即模型有高偏差。如果一个模型过拟合数据,它太复杂了,不能很好地推广到训练中没有包含的新数据点。这意味着当评估训练数据时,模型表现良好,但当评估验证数据集时表现不佳。这个问题还有另一个术语,即模型有高方差

理解过拟合和欠拟合之间的区别的一个简单方法是看看以下简单的例子:在构建模型时,我们的目标是构建适合数据集的东西。左边的例子欠拟合,因为直线模型无法准确地将圆和正方形分开。右边的模型太复杂了:它正确地分离了所有的圆和正方形,但不太可能在新的数据上工作得很好:

图片

我们的线性模型受到了欠拟合的影响:它太简单,无法模拟所有类别的差异。查看 SVM 的准确率,我们可以看到它在训练数据上得分为 100%,但在验证数据上只有 82%。这是一个明显的迹象表明它过拟合了:与训练数据相比,它在分类新图像方面表现得更差。

处理过拟合的一种方法是用更多的训练数据:即使是一个复杂的模型,如果训练数据集足够大,也不会过拟合。另一种方法是引入正则化:许多机器学习模型都有一个可以调整的参数,以减少过拟合。

深度学习

到目前为止,我们已经使用支持向量机(SVM)提高了我们模型的性能,但仍然面临两个问题:

  • 我们的 SVM 过度拟合了训练数据。

  • 也很难扩展到包含 60,000 张图像的全数据集:尝试用更多的图像训练最后一个示例,你会发现它变得慢得多。如果我们将数据点的数量加倍,SVM 算法所需的时间将超过加倍

在本节中,我们将使用深度神经网络来解决这个问题。这类模型已经在图像分类任务上实现了最先进的性能,以及许多其他机器学习问题。它们能够模拟复杂的非线性模式,并且在大数据集上扩展良好。

数据科学家通常会使用 Python 来开发和训练神经网络,因为它可以访问如TensorFlowKeras这样的深度学习框架,这些框架提供了极好的支持。这些框架使得构建复杂神经网络并在大型数据集上训练它们变得比以往任何时候都更容易。它们通常是构建复杂深度学习模型的最佳选择。在第五章,使用预训练模型中,我们将探讨如何从 Python 导出训练好的模型,然后从 Go 中进行推理。在本节中,我们将使用go-deep库从头开始构建一个更简单的神经网络,以演示关键概念。

神经网络

神经网络的基本构建块是一个神经元(也称为感知器)。这实际上与我们的简单线性模型相同:它将所有输入结合在一起,即 x[1],x[2],x[3]... 等等,根据以下公式生成一个单一的输出,即 y

图片

神经网络的魔力来自于当我们组合这些简单的神经元时会发生什么:

  1. 首先,我们创建一个包含许多神经元的,我们将输入数据馈送到这个层中。

  2. 在每个神经元的输出处,我们引入一个激活函数

  3. 然后,这个输入层的输出被馈送到另一个包含神经元和激活的层,称为隐藏层

  4. 这种过程会重复多次隐藏层——层的数量越多,网络就被说成是越

  5. 一个最终的输出层的神经元将网络的输出结果组合成最终的输出。

  6. 使用称为反向传播的技术,我们可以通过找到每个神经网络的权重,即 w[0],w[1],w[2]...,来训练网络,使整个网络能够适应训练数据。

下面的图显示了这种布局:箭头代表每个神经元的输出,这些输出被馈送到下一层的神经元的输入中:

图片

这个网络中的神经元被称为全连接密集层。计算能力和软件的最近进步使得研究人员能够构建和训练比以往任何时候都更复杂的神经网络架构。例如,一个最先进的图像识别系统可能包含数百万个单独的权重,并且需要多天的计算时间来训练所有这些参数以适应大量数据集。它们通常包含不同类型的神经元排列,例如在卷积层中,这些层在这些类型的系统中执行更专业的学习。

在实践中成功使用深度学习所需的大部分技能涉及对如何选择和调整网络以获得良好性能的广泛理解。有许多博客和在线资源提供了更多关于这些网络如何工作以及它们应用到的各种问题的细节。

神经网络中的一个全连接层是指每个神经元的输入都连接到前一层中所有神经元的输出。

一个简单的深度学习模型架构

在构建一个成功的深度学习模型中,大部分的技能在于选择正确的模型架构:层的数量/大小/类型,以及每个神经元的激活函数。在开始之前,值得研究一下是否有人已经使用深度学习解决了与你类似的问题,并发布了一个效果良好的架构。一如既往,最好从简单的东西开始,然后迭代地修改网络以提高其性能。

对于我们的例子,我们将从以下架构开始:

  • 输入层

  • 包含两个各含 128 个神经元的隐藏层

  • 一个包含 10 个神经元的输出层(每个输出类在数据集中都有一个)

  • 隐藏层中的每个神经元将使用线性整流单元ReLU)作为其输出函数

ReLUs 是神经网络中常用的激活函数。它们是向模型中引入非线性的一种非常简单的方式。其他常见的激活函数包括对数函数和双曲正切函数。

go-deep库让我们能够非常快速地构建这个架构:

import (
 "github.com/patrikeh/go-deep"
 "github.com/patrikeh/go-deep/training"
)

network := deep.NewNeural(&deep.Config{
 // Input size: 784 in our case (number of pixels in each image)
 Inputs: len(trainingImages[0]),
 // Two hidden layers of 128 neurons each, and an output layer 10 neurons (one for each class)
 Layout: []int{128, 128, len(categories)},
 // ReLU activation to introduce some additional non-linearity
 Activation: deep.ActivationReLU,
 // We need a multi-class model
 Mode: deep.ModeMultiClass,
 // Initialise the weights of each neuron using normally distributed random numbers
 Weight: deep.NewNormal(0.5, 0.1),
 Bias: true,
})

神经网络训练

训练神经网络是另一个需要巧妙调整以获得良好结果的地方。训练算法通过计算模型与一小批训练数据(称为损失)的拟合程度,然后对权重进行小幅度调整以改善拟合。这个过程在不同的训练数据批次上反复进行。学习率是一个重要的参数,它控制算法调整神经元权重速度的快慢。

在训练神经网络时,算法会反复将所有输入数据输入到网络中,并在过程中调整网络权重。每次完整的数据遍历被称为一个epoch

在训练神经网络时,监控每个 epoch 后网络的准确率损失(准确率应该提高,而损失应该降低)。如果准确率没有提高,尝试降低学习率。继续训练网络,直到准确率停止提高:此时,网络被认为是收敛了。

以下代码使用0.006的学习率对模型进行500次迭代训练,并在每个 epoch 后打印出准确率:

// Parameters: learning rate, momentum, alpha decay, nesterov
optimizer := training.NewSGD(0.006, 0.1, 1e-6, true)
trainer := training.NewTrainer(optimizer, 1)

trainer.Train(network, trainingExamples, validationExamples, 500) 
// training, validation, iterations

这个神经网络在训练集和验证集上都提供了 80%的准确率,这是一个好迹象,表明模型没有过拟合。看看你是否可以通过调整网络架构和重新训练来提高其性能。在第五章,“使用预训练模型”中,我们将通过在 Python 中构建一个更复杂的神经网络并导出到 Go 来重新审视这个例子。

回归

在掌握了分类部分中的许多关键机器学习概念之后,在本节中,我们将应用所学知识来解决回归问题。我们将使用包含加利福尼亚不同地区房屋群体信息的数据库^([4])。我们的目标将是使用如纬度/经度位置、中位数房屋大小、年龄等输入数据来预测每个群体的中位数房价。

使用download-housing.sh脚本下载数据集,然后将其加载到 Go 中:

import (
    "fmt"
    "github.com/kniren/gota/dataframe"
    "github.com/kniren/gota/series"
    "math/rand"
    "image"
    "bytes"
    "math"
    "github.com/gonum/stat"
    "github.com/gonum/integrate"
    "github.com/sajari/regression"
    "io/ioutil"
)

const path = "../datasets/housing/CaliforniaHousing/cal_housing.data"

columns := []string{"longitude", "latitude", "housingMedianAge", "totalRooms", "totalBedrooms", "population", "households", "medianIncome", "medianHouseValue"}
b, err := ioutil.ReadFile(path)
if err != nil {
    fmt.Println("Error!", err)
}
df := dataframe.ReadCSV(bytes.NewReader(b), dataframe.Names(columns...))

我们需要进行一些数据准备,在数据框中创建代表每个区域房屋平均房间数和卧室数的列,以及平均入住率。我们还将将中位数房价重新缩放为$100,000 为单位:

// Divide divides two series and returns a series with the given name. The series must have the same length.
func Divide(s1 series.Series, s2 series.Series, name string) series.Series {
    if s1.Len() != s2.Len() {
        panic("Series must have the same length!")
    }

    ret := make([]interface{}, s1.Len(), s1.Len())
    for i := 0; i < s1.Len(); i ++ {
        ret[i] = s1.Elem(i).Float()/s2.Elem(i).Float()
    }
    s := series.Floats(ret)
    s.Name = name
    return s
}

// MultiplyConst multiplies the series by a constant and returns another series with the same name.
func MultiplyConst(s series.Series, f float64) series.Series {
    ret := make([]interface{}, s.Len(), s.Len())
    for i := 0; i < s.Len(); i ++ {
        ret[i] = s.Elem(i).Float()*f
    }
    ss := series.Floats(ret)
    ss.Name = s.Name
    return ss
}

df = df.Mutate(Divide(df.Col("totalRooms"), df.Col("households"), "averageRooms"))
df = df.Mutate(Divide(df.Col("totalBedrooms"), df.Col("households"), "averageBedrooms"))
df = df.Mutate(Divide(df.Col("population"), df.Col("households"), "averageOccupancy"))
df = df.Mutate(MultiplyConst(df.Col("medianHouseValue"), 0.00001))
df = df.Select([]string{"medianIncome", "housingMedianAge", "averageRooms", "averageBedrooms", "population", "averageOccupancy", "latitude", "longitude", "medianHouseValue" })

如我们之前所做的那样,我们需要将此数据分为训练集和验证集:

func Split(df dataframe.DataFrame, valFraction float64) (training dataframe.DataFrame, validation dataframe.DataFrame){
    perm := rand.Perm(df.Nrow())
    cutoff := int(valFraction*float64(len(perm)))
    training = df.Subset(perm[:cutoff])
    validation = df.Subset(perm[cutoff:])
    return training, validation
}

training, validation := Split(df, 0.75)

// DataFrameToXYs converts a dataframe with float64 columns to a slice of independent variable columns as floats
// and the dependent variable (yCol). This can then be used with eg. goml's linear ML algorithms.
// yCol is optional - if it does not exist only the x (independent) variables will be returned.
func DataFrameToXYs(df dataframe.DataFrame, yCol string) ([][]float64, []float64){
    var (
        x [][]float64
        y []float64
        yColIx = -1
    )

    //find dependent variable column index
    for i, col := range df.Names() {
        if col == yCol {
            yColIx = i
            break
        }
    }
    if yColIx == -1 {
        fmt.Println("Warning - no dependent variable")
    }
    x = make([][]float64, df.Nrow(), df.Nrow()) 
    y = make([]float64, df.Nrow())
    for i := 0; i < df.Nrow(); i++ {
        var xx []float64
        for j := 0; j < df.Ncol(); j ++ {
            if j == yColIx {
                y[i] = df.Elem(i, j).Float()
                continue
            }
            xx = append(xx, df.Elem(i,j).Float())
        }
        x[i] = xx 
    }
    return x, y
}

trainingX, trainingY := DataFrameToXYs(training, "medianHouseValue")
validationX, validationY := DataFrameToXYs(validation, "medianHouseValue")

线性回归

与分类示例类似,我们将首先使用线性模型作为基线。不过,这次我们预测的是一个连续输出变量,因此我们需要一个不同的性能指标。回归中常用的指标是均方误差MSE),即模型预测值与真实值之间平方差的和。通过使用平方误差,我们确保当低估和超估真实值时,值会增加。

对于回归问题,MSE(均方误差)的一个常见替代方法是平均绝对误差MAE)。当你的输入数据包含异常值时,这可能很有用。

使用 Golang 回归库,我们可以按以下方式训练模型:

model := new(regression.Regression)

for i := range trainingX {
  model.Train(regression.DataPoint(trainingY[i], trainingX[i]))
}
if err := model.Run(); err != nil {
  fmt.Println(err)
}

最后,我们可以从验证集中计算出均方误差为0.51。这为我们提供了一个基准性能水平,我们可以将其作为比较其他模型的参考:

//On validation set
errors := make([]float64, len(validationX), len(validationX))
for i := range validationX {
  prediction, err := model.Predict(validationX[i])
  if err != nil {
    panic(fmt.Println("Prediction error", err))
  }
  errors[i] = (prediction - validationY[i]) * (prediction - validationY[i])
}

fmt.Printf("MSE: %5.2f\n", stat.Mean(errors, nil))

随机森林回归

我们知道房价会根据位置的不同而变化,通常以我们线性模型难以捕捉的复杂方式变化。因此,我们将引入随机森林回归作为替代模型。

随机森林回归集成模型的一个例子:它通过训练大量简单的基础模型,然后使用统计平均来输出最终预测。在随机森林中,基础模型是决策树,通过调整这些树和集成中模型的数量参数,你可以控制过拟合。

使用RF.go库,我们可以在房价数据上训练一个随机森林。首先,让我们对训练集和验证集进行一些数据准备:

func FloatsToInterfaces(f []float64) []interface{} {
    iif := make([]interface{}, len(f), len(f))
    for i := range f {
        iif[i] = f[i]
    }
    return iif
}

tx, trainingY := DataFrameToXYs(training, "medianHouseValue")
vx, validationY := DataFrameToXYs(validation, "medianHouseValue")

var (
    trainingX = make([][]interface{}, len(tx), len(tx))
    validationX = make([][]interface{}, len(vx), len(vx))
)

for i := range tx {
    trainingX[i] = FloatsToInterfaces(tx[i])
}
for i := range vx {
    validationX[i] = FloatsToInterfaces(vx[i])
}

现在,我们可以拟合一个包含 25 个底层决策树的随机森林:

model := Regression.BuildForest(trainingX, trainingY, 25, len(trainingX), 1)

这在验证集上给出了一个大幅改进的 MSE 为0.29,但在训练数据上仅显示0.05的错误,表明了过拟合的迹象。

其他回归模型

你还可以尝试在这个数据集上使用许多其他回归模型。实际上,我们在前一个示例中使用的 SVM 和深度学习模型也可以用于回归问题。看看你是否能通过使用不同的模型来提高随机森林的性能。记住,这些模型中的某些将需要数据归一化,以便正确训练。

摘要

在本章中,我们涵盖了大量的内容,并介绍了许多重要的机器学习概念。解决监督学习问题的第一步是收集和预处理数据,确保数据已归一化,并将其分为训练集和验证集。我们涵盖了用于分类和回归的多种不同算法。在每个示例中,都有两个阶段:训练算法,然后进行推理;也就是说,使用训练好的模型对新输入数据进行预测。每次你在数据上尝试新的机器学习技术时,跟踪其与训练集和验证集的性能对比都是非常重要的。这有两个主要目的:它帮助你诊断欠拟合/过拟合,同时也提供了你模型工作效果的指示。

通常,选择一个足够简单但能提供良好性能的模型是最佳选择。简单模型通常运行更快,更容易实现和使用。在每个示例中,我们从一个简单的线性模型开始,然后评估更复杂的技术与这个基线。

在线有许多针对围棋的机器学习模型的不同实现。正如我们在本章中所做的,通常更快的是找到并使用现有的库,而不是从头开始完全实现算法。通常,这些库在数据准备和调整参数方面有略微不同的要求,所以请务必仔细阅读每个案例的文档。

下一章将重用我们在本章中实现的数据加载和准备技术,但将专注于无监督机器学习。

进一步阅读

  1. yann.lecun.com/exdb/lenet/. 获取日期:2019 年 3 月 24 日。

  2. blogs.nvidia.com/blog/2016/05/06/self-driving-cars-3/. 获取日期:2019 年 3 月 24 日。

  3. github.com/zalandoresearch/fashion-mnist. 获取日期:2019 年 3 月 24 日。

  4. colah.github.io/. 获取日期:2019 年 5 月 15 日。

  5. karpathy.github.io/. 获取日期:2019 年 5 月 15 日。

  6. www.dcc.fc.up.pt/~ltorgo/Regression/cal_housing.html. 获取日期:2019 年 3 月 24 日。

第四章:无监督学习

尽管大多数机器学习问题涉及标记数据,正如我们在上一章所看到的,还有一个重要的分支称为无监督学习。这适用于你可能没有输入数据标签的情况,因此算法不能通过尝试从每个输入预测输出标签来工作。相反,无监督算法通过尝试在输入中找到模式或结构来工作。当对具有许多不同输入变量的大型数据集进行探索性分析时,这可能是一种有用的技术。在这种情况下,绘制所有不同变量的图表以尝试发现模式将非常耗时,因此,可以使用无监督学习来自动完成这项工作。

作为人类,我们非常熟悉这个概念:我们做的许多事情从未被其他人明确地教给我们。相反,我们探索周围的世界,寻找并发现模式。因此,无监督学习对试图开发通用智能系统的研究人员特别感兴趣:能够独立学习所需知识的计算机^([1])。

在本章中,我们将介绍两种流行的无监督算法,并在 Go 语言中实现它们。首先,我们将使用聚类算法将数据集分割成不同的组,而不需要任何关于要寻找什么的指导。然后,我们将使用一种称为主成分分析的技术,通过首先在数据集中找到隐藏的结构来压缩数据集。

这只是对无监督学习能够实现的内容的表面触及。一些前沿算法能够使计算机执行通常需要人类创造力的任务。一个值得关注的例子是 NVIDIA 从草图创建逼真图片的系统([2])。你还可以在网上找到可以改变图像外观的代码示例,例如,将马变成斑马,或将橙子变成苹果([3])。

本章将涵盖以下主题:

  • 聚类

  • 主成分分析

聚类

聚类算法旨在将数据集分割成组。一旦训练完成,任何新数据在到达时都可以分配到相应的组中。假设你正在处理一个电子商务商店客户信息的数据集。你可能使用聚类来识别客户群体,例如,商业/私人客户。然后,可以使用这些信息来做出关于如何最好地服务这些客户类型的决策。

你也可以在应用监督学习之前使用聚类作为预处理步骤。例如,图像数据集可能需要手动标记,这通常既耗时又昂贵。如果你可以使用聚类算法将数据集分割成组,那么你可能可以通过只标记部分图像来节省时间,并假设每个簇包含具有相同标签的图像。

聚类技术也被应用于自动驾驶汽车的计算机视觉应用中,它可以用来帮助车辆在未知路段上导航。通过聚类车辆摄像头的图像数据,可以识别出每个输入图像中包含车辆必须行驶的道路的区域^([4])。

对于我们的示例,我们将使用一个包含不同类型鸢尾花测量数据的数据集,你可以使用代码仓库中的./download-iris.sh脚本来下载这个数据集。这个数据集通常用于演示监督学习:你可以使用机器学习来根据鸢尾花种类的特征对数据进行分类。然而,在这种情况下,我们不会向聚类算法提供标签,这意味着它必须纯粹从测量数据中识别聚类:

  1. 首先,像之前示例中那样将数据加载到 Go 中:
import (
 "fmt"
 "github.com/kniren/gota/dataframe"
 "github.com/kniren/gota/series"
 "io/ioutil"
 "bytes"
 "math/rand"
)

const path = "../datasets/iris/iris.csv"

b, err := ioutil.ReadFile(path)
if err != nil {
    fmt.Println("Error!", err)
}
df := dataframe.ReadCSV(bytes.NewReader(b))
df.SetNames("petal length", "petal width", "sepal length", "sepal width", "species")
  1. 接下来,我们需要通过从数据中分割物种列来准备数据:这只是为了在聚类后对组进行最终评估。为此,使用之前示例中的DataFrameToXYs函数:
features, classification := DataFrameToXYs(df, "species")
  1. 现在,我们可以训练一个名为k-means的算法来尝试将数据集分为三个聚类。k-means 通过最初随机选择每个聚类的中间点(称为质心),并将训练集中的每个数据点分配到最近的质心来工作。然后它迭代地更新每个聚类的位置,在每一步重新分配数据点,直到达到收敛。

k-means 是一个简单的算法,训练速度快,因此在聚类数据时是一个好的起点。然而,它确实需要你指定要找到多少个聚类,这并不总是显而易见的。其他聚类算法,如 DBSCAN,没有这个限制。

使用 goml 中的 k-means 实现,我们可以在数据中尝试找到三个聚类。通常,你可能需要通过试错来找出要使用多少个聚类——K。如果你在运行 k-means 后有很多非常小的聚类,那么你可能需要减少 K:

import (
    "gonum.org/v1/plot"
    "gonum.org/v1/plot/plotter"
    "gonum.org/v1/plot/plotutil"
    "gonum.org/v1/plot/vg"
    "github.com/cdipaolo/goml/cluster"
    "github.com/cdipaolo/goml/base"
    "bufio"
    "strconv"
)

model := cluster.NewKMeans(3, 30, features)

if err := model.Learn(); err != nil {
    panic(err)
}

一旦我们将模型拟合到数据上,我们就可以从中生成预测;也就是说,找出每个数据点属于哪个聚类:

func PredictionsToScatterData(features [][]float64, model base.Model, featureForXAxis, featureForYAxis int) (map[int]plotter.XYs) {
    ret := make(map[int]plotter.XYs)
    if features == nil {
        panic("No features to plot")
    }

    for i := range features {
        var pt struct{X, Y float64}
        pt.X = features[i][featureForXAxis]
        pt.Y = features[i][featureForYAxis]
        p, _ := model.Predict(features[i])
        ret[int(p[0])] = append(ret[int(p[0])], pt)
    }
    return ret
}

scatterData := PredictionsToScatterData(features, model, 2, 3)

现在,我们可以使用以下代码绘制聚类图:

func PredictionsToScatterData(features [][]float64, model base.Model, featureForXAxis, featureForYAxis int) (map[int]plotter.XYs) {
    ret := make(map[int]plotter.XYs)
    if features == nil {
        panic("No features to plot")
    }

    for i := range features {
        var pt struct{X, Y float64}
        pt.X = features[i][featureForXAxis]
        pt.Y = features[i][featureForYAxis]
        p, _ := model.Predict(features[i])
        ret[int(p[0])] = append(ret[int(p[0])], pt)
    }
    return ret
}

scatterData := PredictionsToScatterData(features, model, 2, 3)

这所做的就是使用输入特征中的两个,花瓣宽度花瓣长度,来显示数据,如下面的图所示:

图片

每个点的形状是根据鸢尾花种类设置的,而颜色是由 k-means 的输出设置的,即算法将每个数据点分配到哪个聚类。我们现在可以看到,聚类几乎完全匹配每个鸢尾花的种类:k-means 已经能够将数据细分为三个对应不同种类的不同组。

虽然 k-means 在这个案例中效果很好,但你可能会发现需要在你的数据集上使用不同的算法。Python 的 scikit-learn 库提供了一个有用的演示,说明了哪些算法在不同类型的数据集上效果最佳^([5])。你也可能会发现,以某种方式准备你的数据是有帮助的;例如,对其进行归一化或对其应用非线性变换。

主成分分析

主成分分析 (PCA) 是一种降低数据集维度的方法。我们可以将其视为压缩数据集的一种方式。假设你的数据集中有 100 个不同的变量。可能的情况是,这些变量中的许多是相互关联的。如果是这样,那么通过组合变量来构建一个较小的数据集,就有可能解释数据中的大部分变化。PCA 执行这项任务:它试图找到输入变量的线性组合,并报告每个组合解释了多少变化。

PCA 是一种降低数据集维度的方法:实际上,通过总结它,你可以专注于最重要的特征,这些特征解释了数据集中大部分的变化。

PCA 在机器学习中有两种用途:

  • 在应用监督学习方法之前,它可能是一个有用的预处理步骤。在运行 PCA 后,你可能会发现,例如,95% 的变化仅由少数几个变量解释。你可以使用这些知识来减少输入数据中的变量数量,这意味着你的后续模型将训练得更快。

  • 在构建模型之前可视化数据集时,它也可能很有帮助。如果你的数据有超过三个变量,在图表上可视化它并理解它包含的图案可能非常困难。PCA 允许你转换数据,以便你只需绘制数据的最重要方面。

对于我们的示例,我们将使用 PCA 来可视化鸢尾花数据集。目前,这个数据集有四个输入特征:花瓣宽度、花瓣长度、萼片宽度和萼片长度。使用 PCA,我们可以将其减少到两个变量,然后我们可以轻松地在散点图上可视化它们。

  1. 首先像之前一样加载花瓣数据,并按以下方式对其进行归一化:
df = Standardise(df, "petal length")
df = Standardise(df, "petal width")
df = Standardise(df, "sepal length")
df = Standardise(df, "sepal width")
labels := df.Col("species").Float()
df = DropColumn(df, "species")
  1. 接下来,我们需要将数据转换为矩阵格式。gonum 库有一个 mat64 类型,我们可以用它来完成这个任务:
import (
    "github.com/gonum/matrix/mat64"
)

// DataFrameToMatrix converts the given dataframe to a gonum matrix
func DataFrameToMatrix(df dataframe.DataFrame) mat64.Matrix {
    var x []float64 //slice to hold matrix entries in row-major order

    for i := 0; i < df.Nrow(); i++ {
        for j := 0; j < df.Ncol(); j ++ {
            x = append(x, df.Elem(i,j).Float())
        } 
    }
    return mat64.NewDense(df.Nrow(), df.Ncol(), x)
}

features := DataFrameToMatrix(df)

PCA 通过寻找数据集的 特征向量特征值 来工作。因此,大多数软件库需要数据以矩阵结构存在,以便可以使用标准线性代数例程,如 blaslapack 来进行计算。

  1. 现在,我们可以利用 gonum 的 stat 包来实现 PCA:
model := stat.PC{}
if ok := model.PrincipalComponents(features, nil); !ok {
  fmt.Println("Error!")
}
variances := model.Vars(nil)
components := model.Vectors(nil)

这给我们提供了两个变量:components,这是一个矩阵,告诉我们如何将原始变量映射到新成分;以及variances,它告诉我们每个成分解释了多少方差。如果我们打印出每个成分的方差,我们可以看到前两个成分解释了整个数据集的 96%(成分 1 解释了 73%,成分 2 解释了 23%):

total_variance := 0.0
for i := range variances {
  total_variance += variances[i]
}

for i := range variances {
  fmt.Printf("Component %d: %5.3f\n", i+1, variances[i]/total_variance)
}
  1. 最后,我们可以将数据转换成新的成分,并保留前两个,以便我们可以用于可视化:
transform := mat64.NewDense(df.Nrow(), 4, nil)
transform.Mul(features, components)

func PCAToScatterData(m mat64.Matrix, labels []float64) map[int]plotter.XYs {
    ret := make(map[int]plotter.XYs)
    nrows, _ := m.Dims()
    for i := 0; i < nrows; i++ {
        var pt struct{X, Y float64}
        pt.X = m.At(i, 0)
        pt.Y = m.At(i, 1)
        ret[int(labels[i])] = append(ret[int(labels[i])], pt)
    }
    return ret
} 

scatterData := PCAToScatterData(transform, labels)

以下图表显示了每个数据点根据前两个主成分,而颜色表示每个数据点属于哪种鸢尾花物种。现在我们可以看到,三个组沿着第一个成分形成了明显的带状区域,这在将四个原始输入特征相互绘制时我们不容易看到:

你现在可以尝试训练一个监督学习模型,使用前两个 PCA 特征来预测鸢尾花物种:将其性能与在所有四个输入特征上训练的模型进行比较。

摘要

在本章中,我们介绍了无监督机器学习中的两种常见技术。这两种技术通常被数据科学家用于探索性分析,但也可以作为生产系统中数据处理管道的一部分。你学习了如何训练聚类算法自动将数据分组。这项技术可能被用于对电子商务网站上新注册的客户进行分类,以便他们能够获得个性化的信息。我们还介绍了主成分分析作为压缩数据的方法,换句话说,降低其维度。这可以用作在运行监督学习技术之前的数据预处理步骤,以减少数据集的大小。

在这两种情况下,都可以利用gonumgoml库在 Go 语言中以最少的代码构建高效的实现。

进一步阅读

  1. deepmind.com/blog/unsupervised-learning/. 2019 年 4 月 12 日检索。

  2. blogs.nvidia.com/blog/2019/03/18/gaugan-photorealistic-landscapes-nvidia-research/. 2019 年 4 月 12 日检索。

  3. github.com/junyanz/CycleGAN. 2019 年 4 月 12 日检索。

  4. robots.stanford.edu/papers/dahlkamp.adaptvision06.pdf. 2019 年 4 月 13 日检索。

  5. scikit-learn.org/stable/modules/clustering.html#overview-of-clustering-methods. 2019 年 4 月 12 日检索。

第五章:使用预训练模型

在前两章中,你学习了如何使用监督 ML 算法 (第三章,监督学习) 和无监督 ML 算法 (第四章,无监督学习) 解决广泛的问题。创建的解决方案从头开始创建模型,并且仅由 Go 代码组成。我们没有使用已经训练好的模型,也没有尝试从 Go 中调用 Matlab、Python 或 R 代码。然而,在某些情况下,这可能会很有益。在本章中,我们将介绍几种旨在使用预训练模型和创建多语言 ML 应用程序(即主要应用程序逻辑是用 Go 编写的,但专业技术和模型可能用其他语言编写)的策略。

在本章中,你将了解以下主题:

  • 如何加载预训练的 GoML 模型并使用它来生成预测

  • 何时考虑使用纯 Go 解决方案或多语言解决方案

  • 如何使用 os/exec 包调用用其他语言编写的 ML 模型

  • 如何使用 HTTP 调用用其他语言编写的 ML 模型,这些模型可能位于不同的机器上,甚至跨越互联网

  • 如何使用 Go 的 TensorFlow API 运行 TensorFlow 模型

如何恢复保存的 GoML 模型

一旦你投入了创建 ML 模型的辛勤工作,你可能需要关闭你的电脑。当电脑重启时,你的模型会发生什么?除非你已经将其持久化到磁盘,否则它将消失,你需要重新开始训练过程。即使你在 gophernotes 笔记本中保存了模型的超参数,模型本身也没有被保存。而且如果训练过程很长,你可能需要等待很长时间,模型才能再次使用。

在下面的示例中,我们将解释如何恢复我们在第三章,监督学习中创建的模型,并将其使用 GoML API 提供的 PersistToFile 方法持久化到本地的 model.dat 文件中。我们将使用其 RestoreFromFile 方法来恢复它。我们将假设我们在第三章,监督学习中创建的所有其他 func 都可用,例如将图像转换为浮点数切片:

// IsImageTrousers invokes the Saved model to predict if image at given index is, in fact, of trousers
// For simplicity, this loads the model from disk on every request, whereas loading it once and caching it
// would be preferable in a commercial application.

func IsImageTrousers(i int) (bool, error) {
  model := linear.Logistic{}
  if err := model.RestoreFromFile("model.dat"); err != nil {
    return false, err
  }
  prediction, err := model.Predict(testImages[i])
  return prediction > 0.5, err
}

我们现在可以使用此代码在 gophernotes 中生成预测,并将其与 Label 列中的真实值进行比较:

// Prediction

IsImageTrousers(16)

在 gophernotes 中运行前面的代码单元将产生以下输出:

true <nil>

让我们检查输出:

// Ground truth

df.Col("Label").Elem(16).Int() == 1

我们也可以使用在第三章中介绍的相同验证技术,即监督学习,来检查输出质量是否符合预期。当模型是用 Go 编写的并且被持久化以便稍后重用时,这种方法非常有效。然而,如果模型是用 Python 编写的且无法直接在 Go 中恢复(例如scikit-learn模型就是这样),使用该模型进行预测的唯一方法可能是设计一些 Python 模型和 Go 应用之间的通信。虽然这增加了应用程序的整体复杂性,但它具有显著的优势,我们将在接下来的章节中讨论。

决定何时采用多语言方法

如前几章所见,Go 生态系统提供了丰富的机会来原生地解决机器学习问题。然而,固执地要求解决方案保持纯 Go 可能会增加开发时间,甚至降低训练性能,因为其他更专业的 ML 库可以提供更高层次的 API 或性能优化,而这些优化尚未在相应的 Go 库中实现。

一个很好的例子是 Python ML 库 Keras。这个库的目的是提供一个高级 API,允许作者执行各种 ML 任务,如数据预处理、模型训练、模型验证和持久化。它的抽象在多个后端有具体的实现,例如 TensorFlow,这些后端都以其高性能而闻名。因此,Keras 是任何语言中最受欢迎的 ML 库之一:其 MIT 许可的 GitHub 仓库有超过 40,000 颗星,在 GitHub 上的搜索结果显示,超过 20,000 个仓库匹配搜索词 keras,这意味着仓库的名称包含这个词。对代码内容的搜索显示,GitHub 上超过一百万个文件包含搜索词 keras。

然而,仅仅为了使用一个库而将整个应用程序用 Python 编写,无法充分利用 Go 提供的优势,这些优势我们在第一章中已列举,即用 Go 介绍机器学习。如果这些因素对您应用程序的开发并不重要,那么当然可以创建一个 Python 应用程序,但在接下来的内容中,我们将假设您希望两者兼得。

因此,出现了两种选择:首先,完全用 Go 开发应用程序。其次,用 Python 开发 ML 模型,然后从您的 Go 代码中调用这个模型,其中将包含主要的应用程序和业务逻辑。在一个以生产就绪产品为目标的企业环境中,这两种选择的优点如下:

纯 Go 应用

  • 相比多语言解决方案更容易维护

  • 应用组件交互的复杂性降低,因为不需要管理外部 ML 组件的调用

  • 更容易吸纳团队成员

  • 更少的依赖需要更新

现有的库可能直接提供所需的功能并具有足够的性能,从而消除了使用其他语言中专用库所获得的优势。

多语言应用

  • 使用其他语言中专家库的高级抽象,可以大幅减少复杂机器学习问题所需的代码量

  • 在某些情况下,性能优势可能并不明显,因为一些 GoML 库并不是为了追求极致的速度而设计的(深度学习就是一个很好的例子)

  • 更适合多团队协作,因为数据科学团队对 Python 或 R 库更为熟悉

  • 利用现有模型——学术研究论文通常发布带有 Python 或 Lua 脚本的 Caffe 或 TensorFlow 模型,以便调用它们

总结来说,对于现有 Go 库能够直接提供所需功能或只需稍作修改的机器学习应用,原生 Go 解决方案可以降低应用复杂性并提高可维护性。然而,如果情况并非如此,尤其是对于深度学习、计算机视觉等非常复杂的问题,结合 Go 与其他语言的最新工具是值得增加的复杂性的。

在接下来的示例中,我们将从 Go 应用程序中调用各种 Python 机器学习模型。我们选择 Python 的原因是 Python 在大多数 Linux 发行版中都是预安装的,并且也是机器学习中最流行的语言[4][5]。我们将描述的解决方案可以应用于任何编程语言编写的模型。

示例 - 使用 os/exec 调用 Python 模型

要开始使用多语言机器学习应用,我们将回顾第三章中的逻辑回归示例,监督学习。我们将假设模型是用 Python 编写的,而不是 Go,并且我们希望从我们的 Go 应用程序中调用它。为此,我们将使用命令行参数将输入传递给模型,并从标准输出STDOUT)读取模型的预测。

为了在 Python 和 Go 之间交换数据,我们将使用JavaScript 对象表示法JSON)格式化的字符串。当然,这个选择是任意的,我们本可以选择 Go 和 Python 标准库支持的任何其他格式,例如 XML,或者发明我们自己的。JSON 的优势在于在两种语言中使用它都几乎不需要做任何努力。

我们将与 Python 子进程通信的过程如下。一般来说,有三个步骤:请求序列化、执行子进程和响应反序列化:

图片

图 1:我们用于与运行预训练逻辑回归模型的 Python 子进程通信的过程

我们将首先加载 MNIST 数据集并将其转换为数据框。您可以在 第三章,监督学习 中找到此代码。然而,这一次,我们将图像数据转换为整数的切片,每个整数介于 0 和 255(每个像素的值)之间,而不是浮点数的切片。这是为了确保与 Python 模型保持一致:

// ImageSeriesToInts converts the dataframe's column containing image data for multiple images to a slice of int slices, where each int is between 0 and 255, representing the value of the pixel.

func ImageSeriesToInts(df dataframe.DataFrame, col string) [][]int {

  s := df.Col(col)

  ret := make([][]int, s.Len(), s.Len())

  for i := 0; i < s.Len(); i++ {

    b := []byte(s.Elem(i).String())

    ret[i] = NormalizeBytes(b)

  }

  return ret

}

接下来,我们将介绍一个函数,它将允许我们启动 Python 子进程并等待其完成:

// InvokeAndWait invokes a Python 3 script with the given arguments, waits for it to finish, and returns the concatenated output of its STDOUT and STERRR.
func InvokeAndWait(args ...string) ([]byte, error) {
  var (
    output []byte
    errOutput []byte
    err error
  )
  cmd := exec.Command("python3", args...)
  stdout, err := cmd.StdoutPipe()
  if err != nil {
    return nil, err
  }
  stderr, err := cmd.StderrPipe()
  if err := cmd.Start(); err != nil {
    return nil, err
  }
  if output, err = ioutil.ReadAll(stdout); err != nil {
    return nil, err
  }
  if errOutput, err = ioutil.ReadAll(stderr); err != nil || len(errOutput) > 0 {

    return nil, fmt.Errorf("Error running model: %s", string(errOutput))
  }
  return output, nil
}

现在,我们已经准备好组装我们的预测函数,该函数将序列化图像数据,在启动时将其作为参数传递给子进程,等待子进程完成,并反序列化响应:

// IsImageTrousers invokes the Python model to predict if image at given index is, in fact, of trousers

func IsImageTrousers(i int) (bool, error){
    b, err := json.Marshal(testImages[i])
    if err != nil {
        panic(err)
    }
    b, err = InvokeAndWait("model.py", "predict", string(b))
    if err != nil {
        return false, err
    } else {
        var ret struct {
            IsTrousers bool `json:"is_trousers"`
        }
        err := json.Unmarshal(b, &ret)
        if err != nil {
            return false, err
        }
        return ret.IsTrousers, nil
    }
}

现在,我们可以在 gophernotes 中使用此代码生成预测,并将其与 Label 列中的真实值进行比较:

// Prediction
IsImageTrousers(16)

在 gophernotes 单元中运行此代码提供以下输出:

true <nil>

让我们检查输出:

// Ground truth
df.Col("Label").Elem(16).Int() == 1

如预期,这输出了 true。我们可以对几个不同的图像重复此操作,以获得一些信心,确保一切按预期工作。Go 和 Python 代码都使用 predict 参数来表示应该执行哪个操作 - 我们也可以有一个 test 操作,该操作检查 Python 代码从其参数重构的图像是否正确,这进一步增加了我们对子进程通信正确的信心。

子进程通信可能具有操作系统特定的特性,尤其是在涉及输出重定向时。Go 的一个优点是,我们在这里提出的管道方法在所有操作系统上都能同样很好地工作,无需额外修改,而其他语言如 Python 有时需要额外的工作。

虽然代码简洁且易于调试,但每次请求都需要启动一个新的 Python 进程来处理,这可能会影响具有较小、较快的模型的应用程序的性能。此外,它还在 Go 应用程序和其 Python 模型之间创建了一个相当紧密的耦合。在较大的团队中,这可能是一个问题,其中数据科学团队创建模型,而软件开发团队创建应用程序的其他部分。它也可能在模型应该暴露给多个应用程序,而不仅仅是单个应用程序的情况下造成问题 - 那时你应该怎么做?为每个应用程序保留模型的一个副本?这可能导致可维护性问题。在下面的示例中,我们将探讨一种将 Go 应用程序与其 Python 模型解耦的方法。

示例 - 使用 HTTP 调用 Python 模型

如果模型位于不同的机器上,我们需要解耦 Go 和模型逻辑,或者如果我们希望执行多个操作,例如根据用户数据训练用户特定的模型,然后使用此模型进行预测,会发生什么?在这些情况下,随着我们添加更多参数来区分操作和返回码,我们之前使用命令行参数的解决方案将变得更加复杂。这种调用通常被称为远程过程调用(RPC),像 SOAP 或 JSON-RPC 这样的解决方案已经为业界所熟知数十年^([7])。

在以下示例中,我们将使用一个更通用和通用的协议:HTTP。严格来说,HTTP 是一个数据传输协议,通常用作 RPC 协议的管道。然而,只需一点努力,我们就可以在 HTTP 之上创建自己的最小 RPC,通过暴露一个将接受 POST 请求的单个端点。这有一个优点,即不需要 Python 或 Go 标准库之外的任何依赖项,并且调试协议错误特别简单。缺点是处理序列化等问题需要更多的工作。

我们将遵循的请求/响应过程如下所示:

图片

图 2:GoML 应用程序使用 HTTP 与预训练的 Python 模型进行通信的请求/回复过程

与之前的例子不同,我们假设 Python HTTP 服务器已经运行。如果你正在跟随配套的存储库,你可以在使用install-python-dependencies.sh安装其依赖项后,使用python3 model_http.py命令启动 Python 服务器。这意味着 Go 代码特别简短:

// Predict returns whether the ith image represents trousers or not based on the logistic regression model

func Predict(i int) (bool, error){
    b, err := json.Marshal(testImages[i])
    if err != nil {
        return false, err
    }
    r := bytes.NewReader(b)
    resp, err := http.Post("http://127.0.0.1:8001", "application/json", r)
    if err != nil {
        return false, err
    }
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return false, err
    }
    resp.Body.Close()
    var resp struct {
        IsTrousers bool `json:"is_trousers"`
    }
    err := json.Unmarshal(body, &resp)
    return resp.IsTrousers, err 
}

正如我们之前所做的那样,我们可以生成一些预测以确保 Go 和 Python 进程之间的通信按预期工作:

// Expected: true <nil>

Predict(16)

如预期的那样,我们得到了以下结果:

true <nil>

我们可以继续这个过程,对其他几个图像进行测试,以确保响应与由df.Col("Label")系列定义的地面真实值相匹配。我们也可以在我们的 Python HTTP 服务器上创建多个 HTTP 端点,以允许进行各种测试,从而进一步增强我们对进程间通信的信心。

在你很可能需要调试与 HTTP 服务器通信的情况下,Postman 是一个伟大的工具,这是一个免费的图形用户界面工具,允许你创建 HTTP 请求并检查响应。你可以在以下地址获取 Postman:

www.getpostman.com/.

在之前的例子中,我们假设模型是在不同的编程语言(Python)中创建的,并且只能从该语言访问。然而,有一些流行的深度学习库一直在努力成为多语言库,因此提供了使用一种语言创建模型并使用另一种语言使用它的方法。在接下来的例子中,我们将查看这些库中的两个。

示例 - 使用 TensorFlow API 进行 Go 的深度学习

深度学习是机器学习的一个子领域,它使用通常具有许多层的神经网络来解决复杂问题,如图像或语音识别。在这个例子中,我们将探讨如何利用 TensorFlow,一个流行的深度学习框架,通过其 Go 绑定来实现这一点。

TensorFlow 是一个高度优化的库,由 Google 创建,用于对称为张量的对象进行计算^([8])。如果一个向量是一组标量条目(数字)的集合,而一个矩阵是一组向量的集合,那么张量可以被视为一个更高维度的矩阵,其中标量、向量和矩阵是特殊情况。虽然这可能看起来有些抽象,但张量是描述神经网络时的自然对象,这也是为什么 TensorFlow 成为最受欢迎的库之一——甚至有些评论家认为它是最受欢迎的,用于商业和学术深度学习开发^([9][10])。

2011 年,Google Brain 团队构建了一个专有的深度学习系统,名为 DistBelief^([11])。许多杰出的计算机科学家,如 Jeff Dean 和 Geoffrey Hinton,参与了其反向传播和其他神经网络相关算法的工作,这导致 Google 许多项目中框架的使用量增加。2017 年,这个框架的第二代,现在称为 TensorFlow,以开源许可证发布^([12])。

TensorFlow 在核心上是一个低级 API,也称为深度学习计算的底层后端。从实际的角度来看,一个处理商业问题的数据科学家通常不需要每天直接与 TensorFlow API 交互。相反,有多个前端,例如我们之前介绍过的 Keras,作为 TensorFlow 的高级抽象,提供了性能和易用性两方面的最佳选择。另一方面,在学术研究中,新类型的神经网络架构通常使用低级 API 进行,因为新的结构还没有抽象存在。你创建的 TensorFlow 中的对象,称为,可以持久化并在其他语言中重用,这得益于最近使框架更加多语言化的努力^([13])。

在这个例子中,我们将解释如何安装 TensorFlow 以及如何使用其 Go API 加载预训练的 TensorFlow 模型并使用它进行预测。

安装 TensorFlow

TensorFlow 的体验通常很流畅——也就是说,在你成功安装它之后。TensorFlow 团队认识到这是一个困难的步骤,从源代码构建 TensorFlow 在最佳情况下通常需要数小时,因此他们现在提供了几个简单的安装选项。值得注意的是,如果你系统中有兼容的 GPU,你应该安装 GPU 选项,因为这通常可以显著加速软件,这在训练阶段尤其明显:

  • 使用 pip 安装:TensorFlow 旨在为 Python 程序员提供支持,他们通常会使用pip来管理他们的包。在撰写本文时,这种方法已在 Ubuntu Linux 16.04 或更高版本、macOS 10.12.6(Sierra)或更高版本(尽管没有 GPU 支持)、Raspbian 9.0 或更高版本以及 Windows 7 或更高版本上进行了测试。

  • 使用 Docker 镜像:这将适用于支持 Docker 的广泛系统。有两个镜像可供选择:一个纯 TensorFlow 镜像和一个包含 Jupyter 的镜像,这允许你拥有与 gophernotes 相同但仅使用 Python 的体验。

  • 从源代码构建:如果你使用的是非标准配置或者想要对构建过程的部分进行特定控制(例如利用一些只适用于你特定配置的优化),这是最佳选择。

此外,还有一种第四种选择,即使用 Google Colaboratory 在 Google 的云中运行基于 TensorFlow 的代码,但我们将不会深入探讨这个选项,因为它目前仅支持 Python。

在这个例子中,我们将使用 Docker 镜像。Docker 可以被视为一种在相同机器上打包和运行多个应用程序(称为容器)的解决方案,同时确保它们不会相互干扰。如果你还不熟悉它,请访问docs.docker.com/get-started/获取五分钟教程。

我们将使用名为tensorflow/tensorflow的纯 TensorFlow-on-Ubuntu 镜像,它不包括 Jupyter。我们需要在这个镜像上安装 Go,以便我们可以运行我们的代码。由于我们的代码将依赖于 Go 的 TensorFlow 绑定,我们还将根据官方说明^([14])安装它们。这将要求我们安装 TensorFlow C 绑定。因此,我们的 Dockerfile 将如下所示。为了简洁起见,一些步骤已被省略——你可以在本书的配套仓库中找到完整的 Dockerfile:

FROM tensorflow/tensorflow

## Install gcc for cgo ##
RUN apt-get update && apt-get install -y --no-install-recommends \
 curl \
 git \
 wget \
 g++ \
 gcc \
 libc6-dev \
 make \
 pkg-config \
 && rm -rf /var/lib/apt/lists/*

## Install TensorFlow C library ##
RUN curl -L \
 "https://storage.googleapis.com/tensorflow/libtensorflow/libtensorflow-cpu-linux-x86_64-1.13.1.tar.gz" | \
 tar -C "/usr/local" -xz
RUN ldconfig

## Install Go ##
ENV GOLANG_VERSION 1.9.2

RUN wget -O go.tgz "https://golang.org/dl/go${GOLANG_VERSION}.${goRelArch}.tar.gz"; \
 echo "${goRelSha256} *go.tgz" | sha256sum -c -; \
 tar -C /usr/local -xzf go.tgz; \
 rm go.tgz; \
 \
 if [ "$goRelArch" = 'src' ]; then \
 echo >&2; \
 echo >&2 'error: UNIMPLEMENTED'; \
 echo >&2 'TODO install golang-any from jessie-backports for GOROOT_BOOTSTRAP (and uninstall after build)'; \
 echo >&2; \
 exit 1; \
 fi; \
 \
 export PATH="/usr/local/go/bin:$PATH"; \
 go version

ENV GOPATH /go
ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH

RUN mkdir -p "$GOPATH/src" "$GOPATH/bin" && chmod -R 777 "$GOPATH"

## Go get tensorflow go library ##
RUN \
 go get github.com/tensorflow/tensorflow/tensorflow/go \
 github.com/tensorflow/tensorflow/tensorflow/go/op

## Set up the environment so we can just run our code ##
RUN mkdir $GOPATH/src/model

WORKDIR $GOPATH/src/model

ADD . $GOPATH/src/model

CMD ["go", "run", "main.go"]

导入预训练的 TensorFlow 模型

在第三章《监督学习》中,我们解释了如何使用 go-deep 库在纯 Go 中创建深度学习模型。虽然这作为一个玩具示例是可行的,但它训练速度很慢,并且需要大量的冗余代码。使用行业领先的深度学习库之一会更容易,并且会产生性能更好的代码,但不幸的是,它们是用其他语言编写的。我们使用 Python 库 Keras 创建了一个深度学习模型,它将作为之前我们研究过的相同问题的分类器:给出的图像是裤子吗?现在我们将编写一些 Go 代码来导入我们的预训练模型。

如果只保存了模型的权重而不是更完整的 SavedModel 格式,会怎样呢?在这种情况下,你仍然可以使用graph.Import函数导入它,但随后需要做更多的工作来告诉 TensorFlow 所有操作和变量。TensorFlow API godocs 中有一个示例说明了这个过程^([15])。

以下假设模型是以SavedModel格式保存的,并且我们知道输入和输出Ops的名称。如果模型是由其他人使用 Keras 或另一个第三方库创建的,这有时可能很棘手。一个选项是使用SavedModel命令行界面工具来检查模型^([16])。

如果模型是在 Keras 中创建的,并且你有访问 Python 代码的权限,只需检查其inputoutput属性以查看相应张量的名称。它们可能附加了:0,你可以忽略它。

要在 Go 中恢复SavedModel,只需使用LoadSavedModel函数。这将返回一个 Graph 和 Session 对象,然后你可以对其进行操作,传递输入并检索输出:

savedModel, err := tf.LoadSavedModel("./saved_model", []string{"serve"}, nil)
if err != nil {
  log.Fatalf("failed to load model: %v", err)
}

注意,第二个参数,称为标签,通常按惯例设置。我们现在可以访问输入和输出操作:

input := savedModel.Graph.Operation("input_input_1")
output := savedModel.Graph.Operation("output_1/BiasAdd")

如果在这个阶段输入或输出是 nil,这意味着你没有正确的名称,因此你需要返回检查模型以找出它们应该是什么。查看savedModel.Graph.Operations也可能很有用,这是一个Operation切片,你可以通过在Name()中包含搜索字符串输入来过滤操作列表。

我们现在可以访问恢复的会话和图:

session := savedModel.Session
graph := savedModel.Graph
defer session.Close()
fmt.Println("Successfully imported model!")

现在,我们可以在 TensorFlow Docker 容器中运行此代码并查看结果。我们将从 Dockerfile 构建 Docker 镜像并运行它:

docker build -t tfgo . && \
docker run -it tfgo

如果一切顺利,在容器构建过程中应该会看到一些输出(这将在第二次运行时运行得更快),最后会有以下消息:

Successfully built 9658a6232ef8
Successfully tagged tfgo:latest
Successfully imported model!

前两行告诉我们我们的 Docker 镜像已成功构建,最后一行来自我们的 Go 代码,并告诉我们模型导入操作没有产生任何错误。

根据你如何安装 Docker,你可能需要超级用户权限来运行这些命令,所以如果需要,只需在它们前面加上sudo

创建 TensorFlow 模型的输入

现在我们能够从SavedModel重新创建 TensorFlow 图和会话,我们将创建一个程序,该程序将接受 MNIST 时尚数据集的图像作为字节数组,并使用这些字节填充我们之前加载的模型的输入。然后,我们将能够运行模型以获得输出预测。

我们必须创建一个程序,该程序将接受 MNIST 时尚数据集的图像并返回正确形状的张量。从第三章,监督学习,我们知道模型将期望一个包含 784 个浮点数的切片,并且检查模型(使用 Python 中的model.summarySavedModel CLI)将揭示输入应该是 1 x 784 个float32值的张量。

当通过将切片的切片作为参数传递给NewTensor函数来构造张量时,请确保它们的长度都相同。例如,你可以传递包含 7 个元素的 3 个切片,这将创建一个(3,7)的张量,但不能传递包含 5、6 和 7 个元素的 3 个切片——所有切片的第二维必须相同。

我们可以这样构造具有正确形状的空白(零)张量:

func makeBlankInputTensor() (*tf.Tensor, error) {
  t := make([][]float32, 1)
  t[0] = make([]float32, 784)
  tensor, err := tf.NewTensor(t)
  return tensor, err
}

虽然这本身并不很有用,但它说明了NewTensor函数的使用,该函数可以从传递给它的 Go interface{}中推断出正确的张量形状和值类型。使用我们在第三章,监督学习中引入的ImageSeriesToFloats函数,我们可以轻松地将图像转换为float32切片,从而创建输入张量。

我们可以运行模型以获取预测:

tensor, err := makeTensorFromImage("/path/to/fashion/MNIST", 12)
if err != nil {
  log.Fatal(err)
}
prediction, err := session.Run(
  map[tf.Output]*tf.Tensor{
    graph.Operation(input.Name()).Output(0): tensor,
  },
  []tf.Output{
    graph.Operation(output.Name()).Output(0),
  },
  nil)
if err != nil {
  log.Fatal(err)
}

probability := prediction[0].Value().([][]float32)[0][0]
if probability > 0.5 {
  fmt.Printf("It's a pair of trousers! Probability: %v\n", probability)
} else {
  fmt.Printf("It's NOT a pair of trousers! Probability: %v\n", probability)
}

例如,当使用空张量作为输入运行时,输出的最后几行如下:

Successfully built b7318b44f92d
Successfully tagged tfgo:latest
Successfully imported model!
It's NOT a pair of trousers! Probability: 0.04055497

在下一章中,我们将更详细地探讨使用 Docker 部署 ML 应用程序工作负载的模式。

摘要

在本章中,我们从实际的角度比较了 Go-only 和 polyglot ML 解决方案,对比了它们的优缺点。然后,我们提出了两个通用的解决方案来开发 polyglot ML 解决方案:os/exec 包和 JSON-RPC。最后,我们探讨了两个具有自己基于 RPC 的集成解决方案的高度专业化的库:TensorFlow 和 Caffe。您已经学习了如何决定在您的应用程序中是否使用 Go-only 或 polyglot 方法进行 ML,如何实现基于 RPC 的 polyglot ML 应用程序,以及如何从 Go 运行 TensorFlow 模型。

在下一章中,我们将介绍 ML 开发生命周期的最后一步:将用 Go 编写的 ML 应用程序投入生产。

进一步阅读

  1. Keras GitHub 仓库github.com/keras-team/keras。检索日期:2019 年 4 月 30 日。

  2. GitHub 搜索 kerasgithub.com/search?utf8=%E2%9C%93&q=keras&type=。检索日期:2019 年 4 月 30 日。

  3. GitHub 内容搜索 kerasgithub.com/search?q=keras&type=Code。检索日期:2019 年 4 月 30 日。

  4. Python 成为世界上最受欢迎的编程语言, 《经济学人》。2018 年 7 月 26 日: www.economist.com/graphic-detail/2018/07/26/python-is-becoming-the-worlds-most-popular-coding-language. 2019 年 4 月 30 日检索。

  5. 在 Unix 平台上使用 Python: docs.python.org/2/using/unix.html. 2019 年 4 月 30 日检索。

  6. JSON: www.json.org/. 2019 年 4 月 30 日检索。

  7. Cover Pages – SOAP: xml.coverpages.org/soap.html. 2019 年 4 月 30 日检索。

  8. TensorFlow Core: www.tensorflow.org/overview/. 2019 年 4 月 30 日检索。

  9. 深度学习框架功率评分: towardsdatascience.com/deep-learning-framework-power-scores-2018-23607ddf297a. 2019 年 4 月 30 日检索。

  10. 排名前列的深度学习框架: blog.thedataincubator.com/2017/10/ranking-popular-deep-learning-libraries-for-data-science/. 2019 年 4 月 30 日检索。

  11. Dean, Jeff et. al. 大规模异构分布式系统上的机器学习. 2015 年 11 月 9 日. download.tensorflow.org/paper/whitepaper2015.pdf. 2019 年 4 月 30 日检索。

  12. TensorFlow RELEASE.md: github.com/tensorflow/tensorflow/blob/07bb8ea2379bd459832b23951fb20ec47f3fdbd4/RELEASE.md. 2019 年 4 月 30 日检索。

  13. TensorFlow 在其他语言中的使用: www.tensorflow.org/guide/extend/bindings. 2019 年 4 月 30 日检索。

  14. 为 Go 安装 TensorFlow: www.tensorflow.org/install/lang_go . 2019 年 5 月 1 日检索。

  15. TensorFlow—godocs: godoc.org/github.com/tensorflow/tensorflow/tensorflow/go. 2019 年 5 月 3 日检索。

  16. 保存和恢复: www.tensorflow.org/guide/saved_model#install_the_savedmodel_cli. 2019 年 5 月 3 日检索。

  17. Tag constants: github.com/tensorflow/tensorflow/blob/master/tensorflow/python/saved_model/tag_constants.py. 2019 年 5 月 22 日检索。

第六章:部署机器学习应用

在前面的章节中,我们学习了如何创建一个应用,可以为监督学习(第二章,“设置开发环境”)或无监督学习(第四章,“无监督学习”)的机器学习算法准备数据。我们还学习了如何评估和测试这些算法的输出,增加了额外的复杂性,即我们对算法的内部状态和工作原理的了解不完整,因此必须将其视为黑盒。在第五章“使用预训练模型”中,我们探讨了模型持久性和 Go 应用如何利用其他语言编写的模型。你迄今为止学到的技能构成了成功原型化机器学习应用所需的基本技能。在本章中,我们将探讨如何为商业准备你的原型,重点关注特定于机器学习应用的方面。

在本章中,你将涵盖以下主题:

  • 持续交付反馈循环,包括如何测试、部署和监控机器学习应用

  • 机器学习应用的部署模型

持续交付反馈循环

持续交付CD)是在软件开发生命周期中使用短反馈循环的实践,以确保结果应用可以在任何时刻发布^([1])。虽然存在其他发布管理方法,但我们只考虑这一种,因为创建一个有意义的、短的——因此是自动化的——反馈循环,对于机器学习应用来说,提出了独特的挑战,这些挑战不是由可能不需要这种程度自动化的其他方法所引起的。

CD 反馈循环包括以下过程:

图片

图 1:持续交付反馈循环

开发

本书到目前为止所涵盖的反馈循环的开发部分。正如我们在第五章“使用预训练模型”中所论证的,在 Go 中开发机器学习模型既有优点也有缺点,有时将 Go 与其他语言(如 Python)结合使用,以利用如 Keras 等库,可以显著缩短开发周期的一部分。缺点是维护性降低,测试最终解决方案的工作量增加,因为它必然包含 Go-Python 接口(例如)。

测试

由于人类容易犯错,测试我们创建的源代码是开发周期中保证准确和可靠产品的关键要素。已经有许多书籍致力于这个主题,似乎软件测试的方法与软件工程师的数量一样多(如互联网搜索软件测试方法可以证实)。表面上看,机器学习应用尤其难以测试,因为它们看起来像一个黑盒,其输出取决于我们提供的训练集:我们提供数据,它们提供答案,但训练-测试集的微小变化或超参数的微小变化可能会为给定的输入向量产生不同的输出。我们如何确定他们提供的答案是否错误,是因为模型的超参数不正确,输入数据损坏,或者模型的算法有缺陷?或者这个特定的响应是隐藏在大量可接受响应中的异常值?

在前面的章节中,我们使用验证集对模型进行了统计测试,以衡量模型对有意义的输入样本的反应,并将这些反应与可用的预期输出值进行比较(监督学习)。可以说,这是测试机器学习模型准确度或精度的唯一方法,因为用数据集的不同样本(或改变的超参数)重新训练它们可能会为相同的输入产生不同的输出,但应该在具有相同准确度/精度指标的大型验证集上产生统计上较差的结果。换句话说,对模型进行小的修改,我们可能会看到它对单个输入向量的反应方式发生大的变化,但它的反应在测试足够大的输入向量样本(如验证集)时不应有太大差异。

这有两个后果。首先,单元测试通常的构建方式,即开发者选择输入值并断言输出结果,在模型发生最轻微的变化时可能会崩溃。因此,最好不要依赖于基于单个响应的断言。相反,最好使用跨更大集合并使用我们在第三章,“监督学习”,和第四章,“无监督学习”中介绍的技术,使用准确度或精度指标进行断言。

其次,可能存在边缘情况,我们希望保证模型的行为,或者我们希望保证某些响应永远不会发生(即使是作为异常行为)。如果我们不能确定一个黑盒模型能够实现这一点,将机器学习算法与传统的逻辑相结合是确保满足约束的唯一方法。例如,考虑谷歌最近禁止在谷歌图片搜索中使用“gorilla”作为搜索词,以防止一些意外出现的种族主义结果。使用 gorilla 图像对图像分类器进行统计测试将是困难的,并且只能覆盖这一个边缘情况;然而,知道什么是不被接受的反应,并添加约束逻辑来防止这种边缘情况是微不足道的,尽管可能有些尴尬。正如这个例子一样,传统的单元测试可以与统计测试相结合,传统的单元测试断言约束的输出,而统计测试直接断言模型输出。因此,机器学习测试的整体策略就出现了:

  1. 为模型定义准确度/精确度目标:这可能不仅仅是一个单一的准确度分数,因为减少误报或漏报可能更为重要。例如,一个旨在确定抵押贷款申请人是否应该获得贷款的分类器可能需要谨慎行事,容忍更多的漏报而不是误报,这取决于贷款人的风险概况。

  2. 定义边缘情况的行为并将其编码到单元测试中:这可能需要传统的逻辑来限制机器学习模型的输出,以确保满足这些约束,并使用传统的单元测试来断言机器学习模型的约束输出。

部署

一旦开发出机器学习应用,并且你已经测试过它,确保它按预期工作,CD 生命周期中的下一步就是部署软件——也就是说,采取步骤确保用户能够访问它。根据你打算在自己的硬件上运行应用程序还是打算使用基础设施即服务IaaS)或平台即服务PaaS)云等因素,存在不同的部署模型,我们将在下一节中讨论这些差异。在这里,我们假设你是在自己的服务器上运行应用程序,或者使用由 IaaS 提供商提供的虚拟基础设施。

机器学习应用在部署时可能面临一些独特的挑战,这些挑战在更简单的软件中是不存在的,例如一个连接到数据库的 HTTP 服务器:

  • 依赖于需要 LAPACK 或 BLAS 的科学库会导致复杂的安装过程,有许多步骤,出错的可能性也很大。

  • 依赖于深度学习库(如 TensorFlow)意味着需要动态链接到 C 库,这再次导致复杂的安装过程,包括许多与操作系统和架构相关的步骤,以及出错的机会。

  • 深度学习模型可能需要在专用硬件(例如,配备 GPU 的服务器)上运行,即使是测试阶段。

  • ML 模型应该保存在哪里?是否应该像源代码一样提交它们?如果是这样,我们如何确保我们部署的是正确的版本?

接下来,我们将介绍解决这些挑战的解决方案以及体现这些解决方案的示例应用程序。

依赖项

任何尝试从源代码构建 TensorFlow 或 NumPy 的人都会同情那句俗语:“任何可能出错的事情都会出错”。在 Google、Stack Overflow 或它们各自的 GitHub 问题页面上搜索将揭示许多与构建过程相关的隐晦潜在问题^([3][4][5])。这些并不是孤立发现,因为 ML 应用程序所依赖的科学计算库往往非常复杂,并且依赖于一个复杂且高度复杂的其他库集合。一个学术 ML 研究人员可能需要从源代码构建依赖项以利用某种优化,或者可能因为需要修改它们。相反,ML 应用程序开发者必须尽量避免这个过程,而是使用作为 Python wheels^([6])提供的预构建镜像、为所选包管理器(如 Ubuntu Linux 上的 apt 或 Windows 上的 Chocolatey^([7])提供的预构建包,或者 Docker 镜像。

我们将关注 Docker 作为开发与打包 Go ML 应用的解决方案,原因有以下几点:

  • 在广泛的操作系统之间的可移植性。

  • 来自主要云供应商(如 Microsoft Azure^([8])和 Amazon Web Services^([9])的出色支持。

  • 支持使用 Terraform([10])、Chef([11])和 Ansible^([12])等工具在流行的配置和基础设施配置中集成 Docker。

  • 通过预构建 Docker 镜像提供 ML 库。

  • Go 对 Docker 的特别适用性,因为它总是可以配置为生成静态二进制文件,这使我们能够大大减小生产 Docker 镜像的大小。

如果你已经尽可能减小了 Docker 镜像的大小(可能通过使用scratch镜像),但 Go 二进制文件的大小仍然使整体镜像对你来说太大,考虑使用strip命令或像upx这样的打包器。

在我们迄今为止查看的所有示例中,我们创建了一个包含我们应用程序所有依赖项以及应用程序文件的单一 Docker 镜像,通常使用 Dockerfile 中的ADDCOPY命令将这些文件添加到容器中。虽然这具有简单性的优势(开发和生产中只有一个 Dockerfile),但也意味着我们需要推送或拉取一个包含所有依赖项的过大 Docker 镜像来开发应用程序。

然而,可能不需要依赖关系来运行它,因为 Go 总是可以被配置为生成在精简的 Docker 镜像上运行的静态二进制文件。这意味着部署时间和测试时间会变慢,因为中间的 Docker 镜像可能不会在 CI 环境中缓存,更不用说一个更小的容器通常在其宿主服务器上使用更少的磁盘和内存。较小的镜像还有通过减少攻击面增加安全性的好处,因为它们将包含远少于攻击者可以利用的依赖项。例如,scratch镜像甚至不包含 shell,这使得攻击者即使容器中的应用程序本身被破坏,也很难对其进行破坏。

我们倡导的过程在以下图中展示:

图 2:使用两个独立的 Docker 镜像进行部署(一个用于开发,一个用于测试/生产)

在以下示例中,我们假设您已经有一个开发环境,其中所有依赖项都存在(这可能基于 Docker,或者不是——这并不重要)。您已经开发了自己的机器学习应用程序,它由一个main包和一些保存的模型权重model.data组成,并希望创建一个生产就绪的容器。为了创建这个容器,我们需要做两件事。

首先,我们需要将 Go 应用程序编译为静态二进制文件。如果您没有使用 CGO 并且链接到某些 C 库(如 TensorFlow C 库),那么使用不带任何附加标志的go build就足够了。但是,如果您的应用程序依赖于,比如说,TensorFlow C 库,那么您需要添加一些额外的命令行参数以确保生成的二进制文件是静态的——也就是说,它包含所有依赖的代码。在撰写本文时,有一个关于 Go 1.13 的提案,该提案为build命令添加了一个-static标志,这将无需进一步工作即可实现这一点。在此之前,Diogok 有一篇出色的博客文章解释了以下命令中的不同标志,以及如何在特定情况下调整它们:

CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -tags netgo -ldflags '-w -extldflags "-static"' -o mlapp *.go

这将生成一个包含所有必需依赖项的单个输出二进制文件mlapp。使用所有这些标志的目的是生成包含所有依赖项的静态二进制文件,这样我们只需将它们添加到“vanilla”Docker 镜像中,从而得到 Dockerfile:

FROM scratch
ADD . /usr/share/app
ENTRYPOINT ["/usr/share/app/mlapp"]

就这样!没有其他需要添加的内容,与之前我们使用的长 Dockerfile 不同,因为我们需要所有依赖项。在这种情况下,我们已经在 Go 二进制文件内部有了这些依赖项。这是 Go 的另一个优点;与某些其他编程语言不同,Go 使得这种类型的部署成为可能。

您还可以使用 Dockerfile 公开一个端口(例如,如果您打算通过 HTTP 服务器提供应用程序),使用EXPOSE命令。要公开监听端口 80 的 HTTP 服务器,使用EXPOSE 80/tcp命令。

在前面的例子中,我们假设包含训练模型权重/超参数的模型文件已持久化到磁盘,并保存与我们的二进制文件一起,以便添加到 Docker 容器中;然而,在某些情况下,这可能是不可行或不希望的。

模型持久化

大多数时候,你可以遵循上述模式,将模型文件与源代码一起提交,并在部署时将它们与二进制文件一起添加到 Docker 镜像中;然而,有时你可能需要重新考虑这一点:

  • 模型文件非常大,因此会导致非常大的 Docker 镜像,并减慢部署速度。

  • 你拥有的模型文件数量是动态的,每个模型都与你的应用程序对象相关联——也就是说,你为每个用户训练一个模型。

  • 模型的重新训练频率远高于代码可能发生的变化,导致非常频繁的部署。

在这些情况下,你可能希望从不同的来源提供模型,而不是将其提交到源控制。在基本层面上,模型文件只是一系列字节,因此它们可以存储在几乎任何地方:文件服务器、云文件存储或数据库。

例外情况是第二种情况:当你有一个与应用程序对象关联的动态数量的模型文件,例如用户。例如,如果你正在构建一个旨在预测家庭第二天将消耗多少电力的系统,你可能会为所有家庭或每个家庭创建一个模型。在后一种情况下,使用数据库来保存这些模型文件会更好:

  • 模型文件可能包含敏感数据,最好在数据库中安全存储和管理。

  • 大量的模型文件可以从数据库软件利用的高级压缩技术中受益,例如使用页面级压缩而不是行级压缩。这可以减少它们在磁盘上的总体大小。

  • 将与应用程序对象相关的数据都保存在同一个地方可能更容易,以限制执行与模型相关的操作所需的查询次数,例如。

由于这些原因,以及其他原因,我们建议在应用程序需要许多模型,每个模型都与一个应用程序对象(如用户)相关联的情况下,将模型保存到数据库中。

这带来了一点挑战,因为一些 Go ML 库,如 GoML,公开了持久化函数,例如linear包模型的PersistToFile函数,这些函数将模型持久化到文件;然而,它们并不直接提供对序列化模型的访问,如果我们想将其持久化到其他地方。

我们可以应用两种技术:

  • 查看 Godocs 以查看模型结构是否有未导出的字段。如果没有,我们可以简单地使用encoding/json来序列化模型。

  • 如果有未导出字段,我们可以将模型保存到临时文件中,将临时文件读入内存,然后再删除它。

在 Go 中,一个未导出字段是一个具有小写名称的结构体字段,它在其定义的包外部不可访问。这些字段在encoding/json的序列化中不存在。

在 GoML 的LeastSquares模型的情况下,没有未导出字段,对PersistToFile方法的初步检查会显示它正在使用 encoding/JSON 将模型序列化到一个字节数组。因此,我们可以直接使用serializedModel, err := json.Marshal(leastSquaresModel)来序列化它。生成的serializedModel然后可以被保存在我们希望的地方。

但是,如果我们为了辩论的目的不能这样做,因为模型结构体有未导出字段怎么办?例如,golearn 库的linear_models包有一个Export方法,它将模型持久化到文件中,但这依赖于对 C 函数的调用,并且模型有未导出字段。在这种情况下,我们别无选择,只能首先将模型持久化到一个临时文件中,然后恢复文件内容:

import (
  "io/ioutil"
  linear "github.com/sjwhitworth/golearn/linear_models"
)

func Marshal(model *linear.Model) ([]byte, error) {
  tmpfile, err := ioutil.TempFile("", "models")
  if err != nil {
    return nil, err
  }
  defer os.Remove(tmpfile.Name())
  if err := linear.Export(model, tmpfile.Name()); err != nil {
    return nil, err
  }
  return ioutil.ReadAll(tmpfile)
}

在前面的代码中,我们所做的一切只是提供一个临时的位置来存储模型文件在磁盘上,然后将其移回内存。虽然这不是存储模型的最高效方式,但由于一些 Go ML 库的接口限制,这是必要的,并且已经在 GoLearn 的 GitHub 页面上有一个开放的问题来改进这一点。

现在应用程序已经部署,我们希望有一些确定性,确保它正在正确运行,使用适当的资源量,并且没有潜在的问题可能会阻止它可用。在下一小节中,我们将探讨针对 ML 应用程序的特定监控技术。

监控

在他的书《规模架构》中,New Relic 的首席云架构师 Lee Atchison 主张使用风险矩阵,也称为风险登记册,来跟踪应用程序可能出错的情况以及如何缓解。虽然这可能看起来对于一个简单的应用程序来说有些过度,但它是在复杂环境中管理风险的优秀工具,尤其是在涉及 ML 模型的情况下。这是因为整个团队可以了解主要风险、它们的可能性和缓解措施,即使他们最初并没有参与创建应用程序的每一个部分。ML 模型有时可以由数据科学家创建,然后通过我们在第五章中概述的多语言集成方法之一,由软件开发团队接管,这使得了解与它们在生产中使用相关的任何风险都变得尤为重要。

虽然这看起来可能是一种相当主观的方法,但请记住,目标仅仅是让开发者思考什么可能导致他们的应用程序不可用。没有义务写下风险登记册或让你的团队使用一个(尽管两者都可能有益),但思考风险的做法总是通过照亮黑暗的角落,那里没有人想过要寻找那个难以捉摸的周五晚上导致整个应用程序在周一早上才恢复的虫子。

与生产应用程序相关的风险与测试失败不同,你希望在生产部署之前就捕捉到它。这是你在测试中假设为恒定的某些内容(例如可用内存或训练算法收敛)已经变为临界状态的风险。

与 ML 应用程序相关的风险可能包括但不限于以下内容:

  • 模型实例运行内存不足

  • 模型文件损坏,导致模型无法运行,尽管应用程序的其他部分可能仍然可用

  • 如果在生产中进行模型重新训练,则训练过程不收敛,导致模型无用

  • 恶意用户构建输入以试图欺骗模型产生期望的输出

  • 恶意用户构建格式错误的输入(模糊测试)以使模型崩溃

  • 上游服务,如存储 ML 模型的数据库不可用

  • 云数据中心在 GPU 可用性不足时运行模型,这意味着自动扩展功能失败,并且你的深度学习模型的可用性因此降低

这个列表显然不是详尽的,但希望它能给你一个可能出现的各种问题的概念,以便你在自己的应用程序中寻找这些问题。因为很难列出详尽的列表,所以一般性的监控原则适用:

  • 在可能的情况下,在应用程序中使用结构化日志并集中管理这些日志

  • 如果在生产中进行重新训练,确保为训练过程中的任何错误设置警报,因为这必然会导致模型无用(或回退到过时的一个)

  • 捕获可能用于检测任何风险在注册材料中显现的指标的重大变化(例如,内存空间的可用性)

Go 语言部分是为了服务 Web 应用而设计的^([17]),因此有许多第三方包可以帮助你执行这些任务,我们现在将探讨其中的一些。

结构化日志

Go 有很多日志库,例如标准库的log包^([18][19][20])。使用结构化日志库(例如记录到 JSON 这样的标准格式)而不是简单使用自由文本的结构化日志的一个显著优势是,一旦创建,处理日志数据就更容易。不仅通过特定字段进行搜索更容易(例如,使用jq^([21])处理 JSON 数据),而且结构化日志允许与现有的监控和分析工具进行更丰富的集成,例如 Splunk^([22])或 Datadog^([23])。

在以下示例中,我们将使用 Logrus 包来记录由训练过程返回的错误信息。请注意,使用这个特定的日志包是个人选择,任何其他结构化日志包也可以工作。

首先,我们配置记录器:

import "github.com/sirupsen/logrus"

logrus.SetFormatter(&logrus.JSONFormatter{})
logrus.SetReportCaller(true) // Add a field that reports the func name

可以通过使用JSONFormatter结构体的属性^([24])来配置输出格式:

  • TimestampFormat:使用兼容时间格式的格式字符串(例如,Mon Jan 2 15:04:05 -0700 MST 2006)来设置时间戳的格式。

  • DisableTimestamp:从输出中移除时间戳

  • DataKey:而不是扁平的 JSON 输出,这个选项将所有日志条目参数放入给定的键中

  • FieldMap:使用此选项重命名默认输出属性,例如时间戳

  • CallerPrettyfier:当ReportCaller被激活(如前一个代码片段所示)时,可以通过调用此函数来自定义输出——例如,从调用者的方法中去除包名

  • PrettyPrint:这决定了是否缩进 JSON 输出

这里有一个示例,其中我们将其用于实际操作:

import "github.com/sajari/regression"
model := new(regression.Regression)
 logrus.WithFields(logrus.Fields{ "model": "linear regression", }).Info("Starting training")
for i := range trainingX {
 model.Train(regression.DataPoint(trainingY[i], trainingX[i]))
}
if err := model.Run(); err != nil {

logrus.WithFields(log.Fields{
 "model": "linear regression",
 "error": err.Error(), }).Error("Training error")

}
 logrus.WithFields(logrus.Fields{ "model": "linear regression", }).Info("Finished training")

虽然这可能会产生比必要的更多输出,因为增加了两个 info 级别的消息,但我们可以使用logrus.SetLevel过滤掉这个级别的输出,如果不需要的话;然而,在生产中重新训练的情况下,训练时间很重要(确保训练过程完成也同样重要),所以记录日志中的过程记录永远不是一个坏主意,即使因此变得更加冗长。

当记录机器学习相关的信息时,有一个包含模型名称的字段(如果它们是由数据科学家创建的,可能对数据科学家有意义)是个好主意。当你在生产中同时运行多个模型时,有时很难判断哪个模型产生了错误!

训练算法所需的时间是我们建议定期计算并发送到专用指标系统的一个指标。我们将在下一小节中讨论捕获指标。

捕获指标

在先前的例子中,我们在日志中插入信息级消息以表示训练过程的开始和结束。虽然我们可以查看这两个消息的时间戳字段并将它们进行比较以确定训练过程花费了多长时间(例如,Splunk 可以通过正确的查询做到这一点),但实现相同结果的一种更直接、更简单的方法是显式地监控这个特定的数据点或指标。然后,如果训练过程变得过长,我们可以发出警报,或者有一个图表记录并显示常规模型训练过程所花费的时间。

我们可以使用两种方法:

  • 将指标存储在日志条目上的一个附加字段中,字段值为float64

  • 将指标存储在单独的分析系统中

最终,你采取的方法取决于你当前的分析系统、团队偏好和应用程序的大小。就机器学习应用而言,两种方法都同样有效,因此我们将假设第一种方法,因为它减少了所需的第三方应用程序代码量。

重复使用之前的例子,让我们设置如下:

import "github.com/sajari/regression"
model := new(regression.Regression)
 log.WithFields(log.Fields{ "model": "linear regression", }).Info("Starting training")
start := time.Now()

for i := range trainingX {
 model.Train(regression.DataPoint(trainingY[i], trainingX[i]))
}
if err := model.Run(); err != nil {
log.WithFields(log.Fields{ "model": "linear regression",
 "error": err.Error(), }).Error("Training error")

}
elapsed := time.Since(start)
 log.WithFields(log.Fields{ "model": "linear regression",
 "time_taken": elapsed.Seconds(), }).Info("Finished training")

注意,我们没有在计时块中包含任何日志调用。这是因为我们想要测量训练过程所花费的时间,而不是围绕它的任何日志。

如果你的公司使用分析系统,例如 Grafana 或 InfluxDB,你仍然可以使用之前描述的相同方法——只需确保为你的指标创建一个合理的名称,包括机器学习模型的名称。

在最后的子节中,我们将考虑准确性/精确度指标如何帮助在机器学习应用中创建反馈循环。

反馈

在任何系统中获取反馈的过程旨在改进系统。在机器学习应用的情况下,反馈可以帮助使应用在注册风险(或之前未缓解的新风险的添加)方面更加稳健,但这并不仅限于机器学习应用;所有生产应用都受益于反馈循环。然而,有一个特殊的反馈循环是特定于机器学习应用的。

机器学习模型是基于它满足某些准确性/精确度标准,使其在从数据中提取意义方面比简单的启发式方法更好或更通用。在第三章监督学习和第四章无监督学习中,我们概述了一些这些指标,例如房价回归的平均平方误差或二进制分类器在衣服图像上的测试/验证准确性。在我们目前的 CD 周期中,我们假设一旦创建了一个模型,其准确性将不会随着新输入而改变;然而,这很少是一个现实的假设。

考虑我们来自第三章的 MNIST 时尚分类器,监督学习,其目的是确定一张图片是否代表一条裤子。目前,这个数据库中不包含任何喇叭裤的图片。如果这些款式再次流行起来,而我们模型开始接收到的所有图片都是喇叭裤,会怎样呢?我们可能会注意到用户抱怨图片没有被正确分类。这样的考虑导致了许多依赖机器学习模型的网站添加了“对我的预测评分”模型,以期确保模型仍然输出相关的预测。

当然,这是一个有效的方法,尽管它依赖于客户告诉你产品何时工作不正常。因为客户更有可能在体验不满意时使用这些反馈功能^([26]),所以从这个练习中获得的数据,尽管仍然有用,但很可能偏向负面,因此不能自动用作代理准确度指标。

在客户提供图片并且你的模型对它们进行分类的情况下,这仍然可能是你的最佳选择,除非你能编写一个用于抓取新裤子图片的爬虫,并将它们持续输入到模型中,并测量其响应。这将是一项劳动密集型的工作,但显然会产生更好的结果,当然,前提是爬虫找到的裤子类型代表了客户提供的裤子图片的类型。在其他情况下,一些自动化的反馈循环可能是可能的,其中你可以直接监控模型的准确性,无论是在测试还是生产中,并据此决定何时重新训练模型。

考虑一个不同的场景,即你被要求根据住户数量和预测温度曲线等数据点预测大量家庭第二天个人的电力消耗。你决定为每个家庭使用一个回归模型,并在模型训练完成后将回归参数存储在数据库中。然后,每天你将运行数据库中的每个模型以生成预测。

在这种情况下,反馈循环非常简单,因为每天您还可以测量家庭的实际电力消耗,并将其与您的模型预测进行比较。然后,一个预定脚本可以比较在一定时期内两者的相对差异,可能使用移动平均来平滑任何异常,如果这种差异大于某个预定义的阈值,那么就可以假设模型的一些输入数据已更改,并且模型需要在新的数据集上重新训练。另一种选择是,如果模型的任何输入参数发生变化,则重新训练该模型,尽管这可能导致大量的不必要重新训练,从而增加额外的成本,因为预测温度曲线可能每天都会变化,因此每个模型可能每天都需要重新训练。

具有持续验证和重新训练的机器学习应用程序的反馈循环如下:

图 3:具有持续验证的机器学习应用程序的反馈循环

图 3:具有持续验证的机器学习应用程序的反馈循环

并非所有机器学习应用程序都可以应用反馈循环,但只要有一点创意,通常可以找到一种方法来找到既不在训练数据集也不在测试数据集中的输入样本,但这些样本具有更新的相关性。如果您可以自动化从这些样本生成预测并存储其与真实值的差异的过程,那么您仍然可以生成相同的反馈循环。

机器学习应用程序的部署模型

在前面的例子中,我们解释了如何使用 Docker 部署机器学习应用程序,以包含其依赖项。我们故意避免讨论将要运行这些容器的任何基础设施或任何可能促进开发或部署的平台即服务产品。在本节中,我们考虑了在假设应用程序将被部署到支持 IAAS 和平台即服务模型的云平台(如 Microsoft Azure 和 Amazon Web Services)的情况下,机器学习应用程序的不同部署模型。

本节专门编写,旨在帮助您在将机器学习应用程序部署到云端时决定使用哪种虚拟基础设施。

对于任何云应用程序,主要有两种部署模型:

  • 基础设施即服务:这是一种云服务,提供与虚拟化硬件(如虚拟机)的高级交互,而无需客户维护硬件或虚拟化层。

  • 平台即服务:这是一种云服务,提供软件即服务组件,您可以从这些组件构建应用程序,例如无服务器执行环境(例如,AWS Lambda)。

我们将考虑这两种选项,以及如何最好地利用它们来为机器学习应用服务。我们将根据截至 2018 年第四季度的市场份额比较和对比三个主要供应商:亚马逊网络服务、微软 Azure 和 Google Cloud^([30])。

基础设施即服务

在本章的早期部分,我们解释了如何使用 Docker 打包一个机器学习应用。在本小节中,我们将探讨使用 Docker 将机器学习应用部署到 AWS、Azure 或 Google Cloud 的简单方法。

在每种情况下,我们都会首先解释如何将你的本地 Docker 镜像推送到一个注册表(即一个将存储镜像并向你的基础设施的其他部分提供镜像的机器)。使用 Docker 注册表存储你的镜像有几种优点:

  • 更快的部署和构建时间:需要镜像的虚拟基础设施组件可以直接从注册表中拉取,而不是每次都从头开始构建。

  • 在应用中实现自动扩展的简便性:如果你每次需要扩展服务时都必须等待长时间的 Docker 构建——比如,对于 TensorFlow 来说,可能需要 20 分钟——那么你可能会遇到性能下降或不可用的情况。

  • 安全性:从单个可信源拉取镜像可以减少攻击面

亚马逊网络服务

AWS 虚拟化 IaaS 服务的核心是弹性计算EC2)。AWS 还提供弹性容器注册表ECR)作为注册表服务来提供镜像。要设置此服务,请按照以下步骤操作:

在你能够将镜像推送到或从 ECR 注册表拉取之前,你需要ecr:GetAuthorizationToken权限。

  1. 标记你的镜像,假设其 ID 为f8ab2d331c34
docker tag f8ab2d331c34 your_aws_account_id.dkr.ecr.region.amazonaws.com/my-ml-app
  1. 将镜像推送到 ECR:
docker push your_aws_account_id.dkr.ecr.region.amazonaws.com/my-ml-app

现在,这个镜像可以从 EC2 实例中使用。首先,按照第五章,使用预训练模型中的说明 SSH 到你的实例,然后运行以下命令来安装 Docker 并从镜像启动一个容器(修改docker run命令以添加公开的端口或卷):

docker pull your_aws_account_id.dkr.ecr.region.amazonaws.com/my-ml-app && \
docker run -d your_aws_account_id.dkr.ecr.region.amazonaws.com/my-ml-app

微软 Azure

与我们在前一小节中讨论的亚马逊的 ECR 类似,微软 Azure 提供了一个注册表,即 Azure Container Registry。我们可以通过遵循与 AWS ECR 相同的步骤来使用它,但有一个区别,即需要通过 Docker 命令行界面进行登录。一旦完成,你可以遵循前一小节的相同说明,但使用你的注册表和镜像详细信息:

docker login myregistry.azurecr.io

微软还允许将 Docker 作为 App Service Apps 的部署方法,这是一个基于微软的互联网信息服务IIS)的托管 Web 应用服务。如果你已经按照前面的步骤将 Docker 镜像部署到注册表,你可以使用az命令行工具从你的镜像创建一个 Web 应用:

az webapp create --resource-group myResourceGroup --plan myAppServicePlan --name <app name> --deployment-container-image-name myregistry.azurecr.io/my-ml-app

Google Cloud

与亚马逊和微软一样,谷歌也提供了一个名为 Container Registry 的注册表,它可以作为 Docker 注册表使用。使用它的步骤与 Amazon ECR 相同,只是增加了使用 gcloud 命令行工具进行初步认证步骤:

gcloud auth configure-docker

现在您可以推送镜像:

docker tag quickstart-image gcr.io/[PROJECT-ID]/quickstart-image:tag1

在 Google Cloud VM 上运行 Docker 容器的步骤与 EC2 VM 相同,只是增加了认证步骤。

平台即服务

随着机器学习组件在应用中的日益流行,云服务提供商纷纷推出平台即服务产品,以简化机器学习应用的部署,以期吸引客户。简要回顾一下截至 2018 年市场份额的三个主要云服务提供商是值得的^([30])。这并不是要推荐一个供应商而不是另一个,而是试图在保持对您可能已经做出的任何关于云供应商的决定的独立性的同时探索解决方案。换句话说,我们将讨论的部署模型将在所有三个云中(以及可能的其他云)工作——并且某些平台提供特定的服务,可能更适合某些应用或减少其开发工作量。

云服务提供商频繁更改其服务,因此在您阅读此内容时,可能会有比这里描述的更新、更好的服务。请查看 进一步阅读 部分中提供的 Google Cloud、AWS 和 Azure ML 服务的链接^([27][28][29])。

亚马逊网络服务

亚马逊网络服务AWS)在机器学习领域提供两种主要的服务类型:

  • AWS SageMaker:一个托管环境,用于运行机器学习笔记本和 SDK,以高效地执行各种与机器学习相关的任务,包括数据标注

  • AWS AI 服务:一组针对特定任务的预训练模型,例如图像识别

亚马逊 SageMaker

亚马逊 SageMaker 使用 Jupyter 作为机器学习模型的开发环境,正如我们在整本书中所做的那样。这些 Jupyter 笔记本运行的环境包含一些 Python 机器学习库。对于 Python 开发者来说,这项服务可以被视为另一个运行机器学习代码的环境,并具有一些通过 AWS 资源加速大规模学习的功能。使用 SageMaker 在自然语言处理任务上进行超参数调整的示例可以在 AWS GitHub^([31]) 上找到,更长的介绍可以在 YouTube^([33]) 上找到。不幸的是,目前还没有办法使用 SageMaker 与 Jupyter 的 Go 内核(如 gophernotes)一起使用,因此它不是在远程环境中交互式开发机器学习应用的纯 Go 解决方案。

对于需要与现有 Sagemaker 解决方案交互的 Go 开发者,有一个 SDK 具有与 Python SDK^([32]) 相当多的相同功能,因此可以使用 gophernotes 在本地创建 Sagemaker 任务。事实上,这个 SDK 非常强大,它允许 Go 开发者访问一个有用的数据预处理服务:Sagemaker 标签作业服务。该服务与 Mechanical Turk 集成,为训练数据提供地面实况标签,这些数据要么完全缺失,要么来自数据集的一部分。与手动设置 Mechanical Turk 作业相比,这节省了大量时间。暴露此功能的功能是CreateLabelingJob

如果您需要使用监督学习算法,但只有未标记的数据集,请考虑使用 Sagemaker 的 Mechanical Turk 接口来便宜地标记您的数据集。或者,您可以通过 Mechanical Turk UI 在www.mturk.com/创建标记任务。

Amazon AI 服务

如果已经有一个模型公开解决了您的 ML 问题,那么您就没有必要重新发明轮子并训练一个新的模型,尤其是考虑到 AWS 将投入大量资源来确保其模型的准确性和效率。在撰写本文时,以下类型的算法以按使用付费的方式提供:

  • Amazon Personalize: 建立在亚马逊在线零售店使用的相同推荐技术之上,这些技术允许您解决诸如显示与客户已购买的商品类似的项目等问题

  • Amazon Forecast: 时间序列预测模型

  • Amazon Rekognition: 图像和视频分析

  • Amazon Comprehend: 自然语言处理任务和文本分析

  • Amazon Textract: 大规模文档分析

  • Amazon Polly: 文本转语音

  • Amazon Lex: 在 UI 环境中构建聊天机器人

  • Amazon Translate: 自动翻译到和从多种语言

  • Amazon Transcribe: 语音转文本服务

虽然这些服务都不是 Go 特定的,但它们都提供了 Go SDK,您可以使用它来与之交互。这与我们在第五章中看到的示例非常相似,即使用预训练模型,其中模型通过 HTTP 公开,我们使用此协议发送数据并接收预测。

通常,这些方法是同步的——也就是说,您将在输出参数中获得结果,并且以后不需要进一步请求。它们还具有相同类型的签名,其中预测方法的名称可能不同,输入/输出的结构也会不同:

func (c *NameOfService) NameOfPredictionMethod(input
     *PredictionMethodInput) (*PredictionMethodOutput, error)

以 Rekognition 为例,与其他服务一样,它有一个 Go SDK^([34])。假设我们希望检测图像中的面部。为此,我们使用DetectFaces函数;它具有以下签名:

func (c *Rekognition) DetectFaces(input *DetectFacesInput
     (*DetectFacesOutput, error)

在这个例子中,输入包含我们希望返回的数组形式的面部特征,以及一张图片,可以是 base-64 编码的字节或 S3 对象。输出将包含一个FaceDetail结构体,其中将描述每个面部年龄范围、是否留胡须、其边界框的置信度、检测到的情绪、是否戴眼镜等等。这取决于我们在输入中请求了哪些面部特征,并且必然地,我们请求的特征越多,请求的成本就越高(因为亚马逊需要运行更多的模型来给出答案)。

通常,如果你可以通过组合通过 SDK 公开的预构建模型来构建你的 ML 应用程序,那么你可以节省大量时间,这将使你能够专注于添加特定于你业务的价值;然而,与供应商锁定相关的风险是存在的,并且在撰写本文时,没有其他云平台提供与亚马逊 AI 服务功能对等的选择。

Microsoft Azure

Azure 针对 ML 应用程序的主要产品如下:

  • Azure ML Studio:一个用于构建 ML 管道和训练模型的 UI 环境

  • Azure 认知服务:通过 HTTP 公开预训练模型

Azure ML Studio

Azure ML Studio 是一个基于云的 ML IDE。它允许用户从其他 Azure 服务(如 Blob 存储)导入数据,转换数据,并使用它来训练包含的 ML 算法之一。然后,可以通过 HTTP 公开该模型,或者与其他 Azure 服务(如 Azure 流分析)组合,以用于实时 ML 应用程序^([35])。

虽然在 Azure ML Studio UI 中可以运行自定义 Python 代码,但在撰写本文时,这并不包括 Go;然而,因为可以通过 HTTP 公开模型,你可以通过遵循我们在第五章“使用预训练模型”中讨论的相同模式来集成现有的 Azure ML Studio 模型,其中使用net/http客户端来发送请求。仅为了生成身份验证令牌而使用 Azure SDK 是值得的,而不是尝试自己实现,因为该过程可能会出错^([36])。与 AWS 相比,请求和响应的 JSON 结构非常简单,因此生成的代码可以干净且易于维护。

Azure 认知服务

Azure 认知服务通过 HTTP 公开了几个预训练的 ML 模型:

  • 计算机视觉:图像识别

  • 语音:语音识别和转录

  • LUIS:文本意图分析

  • Bing 图像搜索:检索与文本字符串匹配的图像

  • Bing 网络搜索:检索与文本字符串匹配的 URL

  • 文本分析:情感分析

在撰写本文时,没有 Go SDK 可以与认知服务交互,但可以通过使用 REST API 来调用模型,并且微软在快速入门文章中提供了一个示例^([37])。

Google Cloud

除了免费的 Google Colaboratory^([29])之外,Google Cloud 目前为机器学习应用程序开发者提供两项主要服务:

  • AI Platform:使用 Notebooks、VM 镜像或 Kubernetes 镜像的托管开发环境

  • AI Hub:托管即插即用 AI 组件的仓库

  • AI Building Blocks:通过 SDK 或 HTTP 暴露的预训练模型

因为 AI Hub 仅针对 Python 开发者,并且其部署模型与 AI 平台相同,所以我们不会进一步讨论它。

AI 平台

Google 的 AI Hub 是一个基于代码的环境,旨在促进机器学习应用程序开发生命周期的各个方面,从数据摄取到部署,通过 AI Platform Prediction(适用于作为SavedModel导出的 TensorFlow 模型,如我们第五章,使用预训练模型示例)或 Kubernetes。它与其他 Google Cloud 服务有松散的集成,但其核心仍然是一个托管笔记本环境。

因为没有高级 API 可以在 Go 中创建 TensorFlow 图,类似于 Python 中的 Keras,所以 Go 开发者不太可能发现端到端平台有用。然而,如果您正在与 TensorFlow 模型交互,使用 AI Platform Prediction 来管理模型资源并通过 HTTP^([40])调用它是一个很好的策略,特别是当模型可以在具有 Tensor Processing Unit 的 VM 上运行时,这可以是一个显著降低运行 TensorFlow 工作流程成本的方法^([39])。

AI Building Blocks

Google 的 AI Building Blocks 是一套预训练模型,通过 HTTP 或 Google Cloud 的 SDK 之一进行暴露:

  • Sight:包括视觉,用于图像识别,以及视频,用于内容发现

  • 语言:包括翻译和自然语言处理功能

  • 对话:包括语音转文本模型、文本转语音模型和聊天框构建器

  • 结构化数据

    • Recommendations AI:推荐引擎

    • AutoML Tables:生成预测模型的 UI

    • Cloud Inference AI:时间序列推理和相关性工具

Go SDK 非常易于使用,以下示例将展示这一点。该示例使用文本转语音 API 下载由机器模型说出的短语hello, world的录音:

package main
import (
     "context"
     "fmt"
     "io/ioutil"
     "log"
    texttospeech "cloud.google.com/go/texttospeech/apiv1"
     texttospeechpb "google.golang.org/genproto/googleapis/cloud/texttospeech/v1"
)
func main() {
     ctx := context.Background()
    c, err := texttospeech.NewClient(ctx)
     if err != nil {
         log.Fatal(err)
     }

    req := texttospeechpb.SynthesizeSpeechRequest{
         Input: &texttospeechpb.SynthesisInput{
     InputSource: &texttospeechpb.SynthesisInput_Text{Text: "Hello, World!"},
     },
     Voice: &texttospeechpb.VoiceSelectionParams{
         LanguageCode: "en-US",
         SsmlGender: texttospeechpb.SsmlVoiceGender_NEUTRAL,
     },
     AudioConfig: &texttospeechpb.AudioConfig{
         AudioEncoding: texttospeechpb.AudioEncoding_WAV,
     },
 }
    resp, err := c.SynthesizeSpeech(ctx, &req)
         if err != nil {
         log.Fatal(err)
     }
     filename := "prediction.wav"
     err = ioutil.WriteFile(filename, resp.AudioContent, 0666)
     if err != nil {
         log.Fatal(err)
     }
}

与其他通过 HTTP 类型的服务模型一样,如果您可以通过组合这些预制的模型来构建您的应用程序,那么您可以专注于工作在增值业务逻辑上;然而,始终要考虑供应商锁定带来的不利影响。

摘要

在本章中,我们讨论了如何将原型机器学习应用程序推向生产。在这个过程中,我们探讨了软件开发者或 DevOps 工程师通常会考虑的问题,但都是从机器学习应用程序开发者的角度出发。具体来说,我们学习了如何将持续开发生命周期应用于机器学习应用程序,以及云中部署机器学习应用程序的不同方式。

在下一章和最后一章中,我们将退后一步,从项目管理角度审视机器学习开发。

进一步阅读

  1. 持续软件工程及其超越:趋势和挑战,布赖恩·菲茨杰拉德,第 1 届快速持续软件工程国际研讨会。纽约,纽约:计算机协会出版社,第 1-9 页。

  2. 谷歌解决算法性种族歧视的方法:禁止大猩猩:www.theguardian.com/technology/2018/jan/12/google-racism-ban-gorilla-black-people. 2019 年 5 月 3 日检索。

  3. 从源代码构建 Numpyrobpatro.com/blog/?p=47. 2019 年 5 月 5 日检索。

  4. Python—使用 OpenBLAS 集成编译 Numpystackoverflow.com/questions/11443302/compiling-numpy-with-openblas-integration. 2019 年 5 月 5 日检索。

  5. TensorFlow 的问题github.com/tensorflow/tensorflow/issues. 2019 年 5 月 5 日检索。

  6. Python Wheelspythonwheels.com/. 2019 年 5 月 5 日检索。

  7. Chocolateay—Windows 的包管理器chocolatey.org/. 2019 年 5 月 5 日检索。

  8. 在 Azure 上部署 Dockerazure.microsoft.com/en-gb/services/kubernetes-service/docker/. 2019 年 5 月 5 日检索。

  9. 什么是 Docker?| AWSaws.amazon.com/docker/. 2019 年 5 月 5 日检索。

  10. Terraform 的 Docker 提供者www.terraform.io/docs/providers/docker/r/container.html. 2019 年 5 月 5 日检索。

  11. Docker 的 Chef 食谱github.com/chef-cookbooks/docker. 2019 年 5 月 5 日检索。

  12. Docker—管理 Docker 容器: https://docs.ansible.com/ansible/2.6/modules/docker_module.html. 2019 年 5 月 5 日检索。

  13. cmd/go: build: 添加静态标志:github.com/golang/go/issues/26492. 2019 年 5 月 5 日检索。

  14. 关于 Golang 静态二进制文件、交叉编译和插件medium.com/@diogok/on-golang-static-binaries-cross-compiling-and-plugins-1aed33499671. 2019 年 5 月 5 日检索。

  15. 在文件系统外保存模型github.com/sjwhitworth/golearn/issues/220. 2019 年 5 月 6 日检索。

  16. *《为规模而设计》,李·阿奇森,2016 年,奥莱利出版社。

  17. Server-side I/O: Node.js vs PHP vs Java vs Go: www.toptal.com/back-end/server-side-io-performance-node-php-java-go. 获取日期:2019 年 5 月 6 日。

  18. Zap: github.com/uber-go/zap. 获取日期:2019 年 5 月 6 日。

  19. Logrus: github.com/sirupsen/logrus. 获取日期:2019 年 5 月 6 日。

  20. Log: github.com/apex/log. 获取日期:2019 年 5 月 6 日。

  21. jq: stedolan.github.io/jq/. 获取日期:2019 年 5 月 6 日。

  22. Splunk: www.splunk.com/. 获取日期:2019 年 5 月 6 日。

  23. Datadog: www.datadoghq.com/. 获取日期:2019 年 5 月 6 日。

  24. logrus—GoDoc: godoc.org/github.com/sirupsen/logrus#JSONFormatter. 获取日期:2019 年 5 月 6 日。

  25. Grafana: grafana.com/. 获取日期:2019 年 5 月 6 日。

  26. Bias of bad customer service interactions: www.marketingcharts.com/digital-28628. 获取日期:2019 年 5 月 6 日。

  27. Machine Learning on AWS: aws.amazon.com/machine-learning/. 获取日期:2019 年 5 月 6 日。

  28. Azure Machine Learning Service: azure.microsoft.com/en-gb/services/machine-learning-service/. 获取日期:2019 年 5 月 6 日。

  29. Cloud AI: cloud.google.com/products/ai/. 获取日期:2019 年 5 月 6 日。

  30. Cloud Market Share Q4 2018 and Full Year 2018: www.canalys.com/newsroom/cloud-market-share-q4-2018-and-full-year-2018. 获取日期:2019 年 5 月 11 日。

  31. Amazon Sagemaker Example: github.com/awslabs/amazon-sagemaker-examples/blob/master/scientific_details_of_algorithms/ntm_topic_modeling/ntm_wikitext.ipynb. 获取日期:2019 年 5 月 11 日。

  32. Sagemaker SDK for Go: docs.aws.amazon.com/sdk-for-go/api/service/sagemaker/. 获取日期:2019 年 5 月 11 日。

  33. An overview of Sagemaker: www.youtube.com/watch?v=ym7NEYEx9x4. 获取日期:2019 年 5 月 11 日。

  34. Rekognition Go SDK: docs.aws.amazon.com/sdk-for-go/api/service/rekognition/. 获取日期:2019 年 5 月 11 日。

  35. Azure 流分析与 Azure 机器学习集成docs.microsoft.com/en-us/azure/stream-analytics/stream-analytics-machine-learning-integration-tutorial. 获取日期:2019 年 5 月 11 日。

  36. Azure Go SDKgithub.com/Azure/azure-sdk-for-go. 获取日期:2019 年 5 月 11 日。

  37. 消费 Web 服务docs.microsoft.com/en-us/azure/machine-learning/studio/consume-web-services. 获取日期:2019 年 5 月 11 日。

  38. 快速入门:使用 Go 调用文本分析 APIdocs.microsoft.com/en-us/azure/cognitive-services/text-analytics/quickstarts/go. 获取日期:2019 年 5 月 11 日。

  39. 深度学习硬件成本比较medium.com/bigdatarepublic/cost-comparison-of-deep-learning-hardware-google-tpuv2-vs-nvidia-tesla-v100-3c63fe56c20f. 获取日期:2019 年 5 月 11 日。

  40. 预测概述cloud.google.com/ml-engine/docs/tensorflow/prediction-overview. 获取日期:2019 年 5 月 11 日。

  41. Google AI Hubcloud.google.com/ai-hub/. 获取日期:2019 年 5 月 11 日。

  42. Amazon ECR 管理策略docs.aws.amazon.com/AmazonECR/latest/userguide/ecr_managed_policies.html. 获取日期:2019 年 5 月 11 日。

  43. App Service - 容器 Web 应用azure.microsoft.com/en-gb/services/app-service/containers/. 获取日期:2019 年 5 月 11 日。

  44. 将 Docker 镜像推送到私有注册库docs.microsoft.com/en-gb/azure/container-registry/container-registry-get-started-docker-cli. 获取日期:2019 年 5 月 11 日。

  45. 在 Linux 上创建 Docker/Go 应用docs.microsoft.com/en-gb/azure/app-service/containers/quickstart-docker-go. 获取日期:2019 年 5 月 11 日。

  46. 容器注册库cloud.google.com/container-registry/. 获取日期:2019 年 5 月 11 日。

  47. Docker 快速入门cloud.google.com/cloud-build/docs/quickstart-docker. 获取日期:2019 年 5 月 11 日。

  48. Mechanical Turkwww.mturk.com/. 获取日期:2019 年 5 月 15 日。

  49. 使用这个奇怪的小技巧缩小你的 Go 可执行文件blog.filippo.io/shrink-your-go-binaries-with-this-one-weird-trick/. 2019 年 5 月 16 日检索。

第七章:结论 - 成功的机器学习项目

到目前为止,在这本书中,我们主要关注如何在 Go 语言中准备和使用机器学习算法。这包括在第二章“设置开发环境”中准备数据,以及在第三章“监督学习”和第四章“无监督学习”中使用数据来构建模型。我们还探讨了如何在第五章“使用预训练模型”中将现有的机器学习模型集成到 Go 应用中。最后,我们在第六章“部署机器学习应用”中介绍了如何将机器学习集成到生产系统中。为了总结,我们将探讨典型项目中的不同阶段,以及如何管理开发和部署成功的机器学习系统的端到端流程。

人工智能专家安德烈·卡帕尔蒂(Andrej Karparthy)已经写了一些关于如何使用机器学习来简化以前非常复杂的系统。通常,让机器从数据中学习比在代码中表达所有需要的逻辑要简单得多。例如,谷歌的自动翻译应用通过使用神经网络系统,将传统的 500,000 行代码简化为 500 行机器学习代码。从传统代码转换为机器学习系统需要不同的技能集,以及软件开发的不同方法。

大多数关于机器学习(ML)的技术文献都集中在如何优化或选择模型以实现最佳性能,这种性能是通过测试数据集来衡量的。虽然这在推进最先进的机器学习方面很重要,但大多数现实世界的项目将根据非常不同的标准成功或失败。例如,理解如何将业务需求最佳地转化为机器学习任务,了解你的机器学习系统的局限性,以及如何最好地管理设计和维护机器学习应用的整体流程,这些都是至关重要的。

在本章中,我们将涵盖以下主题:

  • 何时使用机器学习

  • 机器学习项目的典型阶段

  • 何时将机器学习与传统代码结合使用

何时使用机器学习

在任何新项目的开始阶段,你需要确定机器学习是否是正确的方法。这取决于三个关键因素:

  • 首先,理解你的业务需求,以及它是否确实可以通过机器学习来解决,这是至关重要的。考虑你项目的目标是什么。例如,你是否希望降低目前需要大量人工工作和成本的过程的成本?你是否试图为最终客户提供更好的体验,例如,通过添加使用传统代码构建将花费太多时间的个性化功能?

  • 接下来,问问自己您是否拥有使您提出的机器学习系统运行所需的数据。如果没有,您将如何获取所需的数据,以及需要解决哪些潜在问题?例如,您可能需要将来自组织内部不同领域的数据集汇集在一起,或者您可能会发现隐私问题会影响您如何适当使用您的数据。

  • 最后,考虑机器学习的局限性以及这些局限性可能对您的最终产品产生的影响。例如,如果您打算使用包含男性比女性多得多的客户数据库中的信息,那么除非您采取措施纠正,否则您从该数据库构建的任何机器学习系统在其输出中可能会显示出偏见。通常,当机器学习系统面对与训练和开发中使用的非常不同的数据时,可以生成不可预测的输出。如果您设计一个用于交易金融证券的系统,考虑一下如果输入数据突然变化会发生什么,例如,在市场崩溃之后?您将如何确保您的系统安全运行,并且不会产生无意义或灾难性的输出?

在许多情况下,您在项目开始时可能无法知道所有这些问题的答案。在这种情况下,一个好的方法是首先识别和构建一个概念验证(PoC)。将其视为您可以构建的关于您的机器学习应用的简单且成本最低的演示。一个好的 PoC 应该能够做到以下几点:

  • 回答关于机器学习是否是正确的方法以及它是否满足您的业务需求的问题。

  • 揭示在构建完整系统时必须解决的问题。

  • 在您的组织内部为利益相关者创建一个演示,这样您可以获得反馈,了解您的系统是否适合用途,以及需要考虑哪些改进和变化。

PoC(概念验证)或最小可行产品(MVP)是机器学习产品的简单且成本低的演示。在您花费时间和金钱构建完整的生产系统之前,使用它来回答您对产品如何工作的疑问。

机器学习项目的典型阶段

正如我们在整本书中看到的那样,机器学习高度依赖于用于训练和测试的数据。因此,我们发现通过以下图表中的阶段来观察典型项目是有帮助的,该图表来自跨行业数据挖掘标准流程(CRISP-DM),这是一种流行的数据科学项目管理方法^([3]):

图片

与其他一些工程系统相比,机器学习通常永远不会产生完美的输出,因此,出于这个原因,项目通常是迭代的。对数据集和模型的改进使您能够产生越来越好的结果,前提是它们符合您的业务需求。

商业和数据理解

决定使用机器学习后,规划项目的一个关键步骤是将你的业务成功标准转换为模型的技术要求和目标。例如,你应该使用哪些性能指标来构建你的模型,无论是其准确性还是其他因素,如计算速度和成本?你的产品需要与哪些其他系统集成?你是否需要确保其预测没有偏见,如果是这样,你将如何测试这一点?

虽然对商业的理解有助于你设计一个有价值的产品,但数据理解有助于你确定从你拥有的数据中可能实现的内容。通过与你的数据科学家合作,你可以识别数据集中存在的问题,并开始识别可能成为你模型基础的潜在见解。

数据准备

正如我们在整本书中看到的那样,在构建机器学习应用时,能够访问正确准备的数据是至关重要的。在这个数据工程领域,挑战往往被忽视,导致进度缓慢,因为越来越多的时间被花在临时工作中,以整合数据源和修复质量问题。

因此,值得考虑你将如何构建你的数据管道:你的数据从哪里来,它需要什么样的预处理,你将把它存储在哪里?你应该在数据上运行哪些检查,以确保在数据被嵌入到训练模型之前,任何质量问题都能迅速被发现?

现在存在各种工具来帮助自动化和简化数据管道,例如 Apache Airflow 项目^([4]),以及像 Google 的 Cloud Composer^([5])和 Amazon 的 AWS 数据管道^([6])这样的托管服务。

数据管道是一个收集、转换和以通用格式存储数据的系统,使其能够作为你的机器学习应用的输入。

建模和评估

在这个阶段,你需要为你的数据开发、微调和评估模型。通常,有三种选择来决定如何进行:

  • 使用现成的机器学习解决方案并使用你自己的数据。例如,Google Vision^([7]) 提供了一个完全管理的图像分类系统的 API。通常,这些服务在 PoC 阶段快速获得结果是一个好方法,但在更大的项目中应谨慎对待。因为你没有自己训练模型,所以通常很难确保它捕捉到了你自己的数据的重要特征。

  • 采用现有的开源模型,并重新训练/定制它以适应你自己的目的。例如,如果你想构建一个检测图像中物体的系统,利用大型组织已经投入的大量研发努力是有意义的^([8])。你可以使用这些模型来给你一个先发优势,然后在你的数据集上重新训练它们。

  • 使用我们在第三章“监督学习”和第四章“无监督学习”中学到的技术从头开始开发和训练一个模型。虽然这可能是一种最耗时的方法,但如果你的问题仅限于你的组织,它通常会产生最佳结果。

无论你选择哪种选项,确保你的模型开发和测试是可复现的都很重要。确保它有文档记录,并且模型及其数据需求都包含在版本控制系统中。这样做将允许不同的团队成员在同一个模型上工作,并对其获得相同的结果有信心。

一个可复现模型是指拥有足够的代码和文档,以便能够轻松地在开发期间使用的相同数据集上重新训练。它还应包括它所依赖的所有软件库和框架的版本号。

部署

一个机器学习模型只有在其可以被部署到生产系统时才有用。在第六章“部署机器学习应用程序”中,我们探讨了如何在 Go 中实现这一目标的技巧。遵循这些技巧将允许你可靠地部署你的模型。当你开始迭代项目阶段以改进你的产品时,跟踪进入生产的不同模型版本也很重要。一个选择是将所有保存的模型检查到版本控制系统,如 Git,尽管如果模型包含大文件,这可能会出现问题。另一个选择是使用数据版本控制DVC),它能够跟踪代码、模型和它们所依赖的数据集。

何时将机器学习与传统代码结合使用

尽管这本书的大部分内容都集中在如何编写和使用机器学习代码,但你也会注意到需要大量的传统、非机器学习代码来支持我们所做的工作。其中大部分隐藏在我们使用的软件库中,但有些情况下你可能需要添加更多。

一个例子是当你需要对你的模型输出施加某些约束时,例如处理边缘情况或实施一些安全关键约束。假设你正在为自动驾驶汽车编写软件:你可能使用机器学习来处理来自汽车摄像头的图像数据,但当涉及到控制车辆的转向、发动机和制动控制时,你很可能会需要使用传统代码来确保汽车的安全控制。同样,除非你的机器学习系统被训练来处理意外的数据输入,例如来自失败的传感器,否则你将想要添加逻辑来处理这些情况。确保在部署之前用异常数据和边缘情况测试你的模型,以便了解其性能可能降低的情况。

在所有现实世界的系统中,你需要仔细思考你训练的机器学习模型要做什么,它的局限性是什么,以及如何确保你的端到端系统按预期运行。

摘要

在这本书中,你已经学习了在 Go 中开发机器学习应用所需的重要技术,并将它们作为生产系统部署。发展你的知识的最佳方式是通过实践操作,所以深入其中,开始将机器学习软件添加到你的 Go 应用中。在这里学到的技能将使你能够开始将前沿的机器学习功能添加到你正在工作的项目中。

机器学习是一个快速发展的领域,每周都有新的算法和数据集被学术界和技术公司发布。我们建议你阅读涵盖这一研究的技术博客、论文和代码库,其中许多我们在本书中都有引用。你可能会发现一个新的最先进的模型,它可以解决你一直在努力解决的问题,等待你在 Go 中实现它。

进一步阅读

  1. medium.com/@karpathy/software-2-0-a64152b37c35. 获取日期:2019 年 5 月 17 日。

  2. jack-clark.net/2017/10/09/import-ai-63-google-shrinks-language-translation-code-from-500000-to-500-lines-with-ai-only-25-of-surveyed-people-believe-automationbetter-jobs/. 获取日期:2019 年 5 月 17 日。

  3. pdfs.semanticscholar.org/48b9/293cfd4297f855867ca278f7069abc6a9c24.pdf. 获取日期:2019 年 5 月 18 日。

  4. airflow.apache.org/. 获取日期:2019 年 5 月 18 日。

  5. cloud.google.com/composer/. 获取日期:2019 年 5 月 18 日。

  6. aws.amazon.com/datapipeline/. 获取日期:2019 年 5 月 18 日。

  7. cloud.google.com/vision/. 获取日期:2019 年 5 月 18 日。

  8. github.com/tensorflow/models/tree/master/research/object_detection. 获取日期:2019 年 5 月 18 日。

  9. dvc.org/. 获取日期:2019 年 5 月 22 日。

  10. becominghuman.ai/how-to-version-control-your-machine-learning-task-ii-d37da60ef570. 获取日期:2019 年 5 月 22 日。

posted @ 2025-09-03 10:08  绝不原创的飞龙  阅读(41)  评论(0)    收藏  举报