AWS-人工智能实用指南-全-

AWS 人工智能实用指南(全)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

《Amazon Web Services 上的人工智能实战》 将教你关于 AWS 上各种人工智能和机器学习服务的知识。通过实际的动手练习,你将学习如何使用这些服务来生成令人印象深刻的结果。你将能够有效地设计、开发、监控和维护 AWS 上的机器学习和深度学习模型。

本书从介绍 AI 及其在不同领域的应用开始,同时概述了 AWS 在 AI/机器学习服务和平台方面的内容。它将教你如何使用 Amazon Rekognition 和 Amazon Translate 进行文本的检测和翻译。你还将学习如何借助 Amazon Transcribe 和 Amazon Polly 进行语音转文本。

本书涵盖了使用 Amazon Comprehend 从文本中提取信息和使用 Amazon Lex 构建语音聊天机器人的内容。你将了解 Amazon SageMaker 的核心功能——处理大数据、发现文本集合中的主题以及图像分类。最后,本书探讨了使用深度学习和自回归进行销售预测以及模型准确性衰退的问题。

在本书结束时,你将掌握通过沉浸式的动手练习,涵盖模型生命周期各个方面的知识,从而在 AWS 中实现并应用人工智能。

本书适合的人群

本书非常适合数据科学家、机器学习开发者、深度学习研究人员以及希望利用 AWS 服务实现强大 AI 解决方案的 AI 爱好者。读者应具备机器学习概念的基础知识。

本书涵盖的内容

第一章,《Amazon Web Services 上的人工智能简介》,介绍了“人工智能”这一广义术语,包括机器学习和深度学习。我们将讨论一些人工智能领域最热门的话题,包括图像识别、自然语言处理和语音识别。我们将提供关于 AWS 人工智能和机器学习服务与平台的高层概述。AWS 既提供用于即插即用的人工智能/机器学习能力的托管服务,也提供用于训练定制机器学习模型的托管基础设施。我们将指导你何时利用托管服务,何时训练自定义机器学习模型。你将学习如何安装和配置你的开发环境。我们将引导你完成 Python、AWS SDK 和你将在后续章节的动手项目中需要的 Web 开发工具的设置过程。我们还将帮助你通过可以与 AWS 平台编程交互的工作代码来验证环境设置是否正确。

第二章,现代 AI 应用的架构,深入探讨了现代 AI 应用的架构和组件。我们开始介绍构建良好架构应用的模式和概念,这些将帮助你设计生产级智能解决方案。这些概念不仅帮助你快速实验和原型化解决方案,还将帮助你开发灵活、可扩展且可维护的解决方案,贯穿整个应用生命周期。你将构建一个目标架构的骨架,并在后续章节中填充详细内容。

第三章,使用 Amazon Rekognition 和 Translate 检测与翻译文本,演示了如何构建你的第一个 AI 应用程序,该应用程序可以将出现在图片中的外语文本翻译成其本国语言。你将获得使用 Amazon Rekognition 和 Amazon Translate 的实践经验。你将首先构建一个可重用的框架,包含来自 AWS 的 AI 和机器学习功能,然后在此框架的基础上构建应用程序。我们将展示能力与应用逻辑的分离如何带来灵活性和可重用性,这一概念将在后续章节的实践项目中变得越来越清晰。

第四章,使用 Amazon Transcribe 和 Polly 进行语音与文本转换,向你展示如何构建一个可以将语音对话翻译成不同语言的应用程序。你将获得使用 Amazon Transcribe 和 Amazon Polly 的实践经验。你不仅会继续构建 AI 能力的可重用框架,还将重用在上一章中构建的翻译能力。这将加深对构建良好架构生产就绪 AI 解决方案的概念和好处的理解,帮助你加速实验和上市进程。

第五章,使用 Amazon Comprehend 从文本中提取信息,展示了如何构建一个能够从名片照片中提取并组织信息的应用程序。你将获得使用 Amazon Comprehend 的实践经验,并重用之前章节中的文本检测功能。此外,我们将介绍人工环节(human-in-the-loop)的概念。你将构建一个人工环节图形用户界面,使用户能够验证甚至纠正 Amazon Comprehend 提取的信息。

第六章,使用 AWS Lex 构建语音聊天机器人,让你继续动手实践,通过构建一个语音聊天机器人来查询在前一个项目中提取并存储的名片联系信息。你将亲自体验如何使用 Amazon Lex 构建聊天机器人,并将聊天机器人界面集成到应用程序中作为数字助手。

第七章,使用 Amazon SageMaker,探索 Amazon SageMaker 的关键功能——从整理大数据到训练和部署内置模型(Object2Vec),再到识别最佳表现的模型,并将你自己的模型和容器引入 SageMaker 生态系统。我们通过图书评分数据集来说明每个组件。首先,我们预测用户对一本书的评分;也就是说,预测用户从未评分过的书籍。其次,我们利用 SageMaker 的 HPO 能力自动化超参数优化,同时通过 SageMaker 搜索发现最佳表现的模型及其相应的训练集和测试集。第三,我们展示如何将自己的模型和容器无缝地带入 SageMaker,避免在 SageMaker 中重建同一模型的麻烦。在本章结束时,你将掌握如何利用 Amazon SageMaker 的所有关键功能。

第八章,创建机器学习推理管道,带领你了解如何使用 SageMaker 和其他 AWS 服务来创建机器学习管道,这些管道能够处理大数据、训练算法、部署训练好的模型并进行推理——同时在模型训练和推理过程中使用相同的数据处理逻辑。

第九章,发现文本集合中的主题,引入了一个新话题。在前面所有的 NLP 章节中,你学习了如何使用 Amazon 提供的多个 NLP 服务。为了对模型训练和部署进行精细控制,并构建大规模的模型,我们将使用 Amazon SageMaker 中的算法。

第十章,使用 Amazon SageMaker 进行图像分类,在你学习完 Amazon Rekognition 之后继续展开。在这一章中,你将学习如何超越 Rekognition API 预定的图像分类,分类你自己的图像数据。特别地,我们将重点讲解如何标注自己的图像数据集,并使用 SageMaker 的图像分类算法来检测自定义图像。我们将学习如何从 ResNet50 进行迁移学习,这是一个在 ImageNet(一个由斯坦福大学和普林斯顿大学支持的、按名词组织的图像数据库)上训练的预训练深度残差学习模型。

第十一章,深度学习与自回归销售预测,解释了如何使用深度学习与自回归DeepAR)进行销售预测。特别是,通过对长短期记忆网络LSTM)的深入理解,您将掌握一种递归神经网络RNNs)形式的知识。RNN 是具有循环结构的网络,允许信息在网络中持续存在,将以前的信息与当前任务联系起来。自回归利用前一个时间步的数据作为回归方程的输入,预测下一个时间步的值。在本章结束时,您将使用 Amazon SageMaker 构建一个强大的销售预测模型。

第十二章,模型精度衰退与反馈循环,解释了为什么模型在生产中会衰退。为了说明这一点,我们讨论了如何预测移动应用的广告点击转化率。随着新数据的不断出现,重新训练模型以实现最佳生产性能变得尤为重要。

第十三章,接下来是什么?,总结了到目前为止我们所学的概念。此外,我们还将简要讨论 AWS 提供的 AI 框架和基础设施。

要充分利用本书

需要具备基础的机器学习和 AWS 概念知识。

下载示例代码文件

您可以从 www.packt.com 账户中下载本书的示例代码文件。如果您是从其他地方购买的本书,可以访问 www.packtpub.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/Hands-On-Artificial-Intelligence-on-Amazon-Web-Services。如果代码有更新,将会在现有的 GitHub 仓库中更新。

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

下载彩色图片

我们还提供了一份包含本书中使用的截图/图表的彩色图片的 PDF 文件。您可以在此下载:static.packt-cdn.com/downloads/9781789534146_ColorImages.pdf

使用的约定

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

CodeInText:表示文本中的代码词、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 用户名。举个例子:“让我们为这个项目创建一个目录,并命名为 ObjectDetectionDemo。”

代码块如下所示:

{
   "Image": {
     "Bytes”: “...”
    }
}

任何命令行输入或输出如下所示:

$ brew install python3
$ brew install pip3

粗体:表示新术语、重要词汇,或者屏幕上出现的文字。例如,菜单或对话框中的文字会像这样出现在文本中。举个例子:“该功能是利用深度学习技术如自动语音识别ASR)和自然语言理解NLU)构建的,目的是将语音转化为文本,并识别文本中的意图。”

警告或重要的注意事项会像这样显示。

提示和技巧会像这样显示。

联系我们

我们始终欢迎读者的反馈。

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

勘误表:尽管我们已经尽力确保内容的准确性,但错误是难免的。如果您在本书中发现错误,我们非常感激您能将其报告给我们。请访问 www.packtpub.com/support/errata,选择您的书籍,点击勘误提交表格链接,并填写详细信息。

盗版:如果您在互联网上发现任何我们作品的非法复制版本,我们将非常感激您提供该材料的地址或网站名称。请通过 copyright@packt.com 联系我们,并附上相关链接。

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

评论

请留下评论。读完并使用本书后,您为何不在您购买书籍的网站上留下评论呢?潜在读者可以看到并根据您的客观意见做出购买决策,我们也能了解您对我们产品的看法,作者也可以看到您对他们书籍的反馈。感谢您!

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

第一部分:现代 AI 应用程序的简介和结构

本节旨在介绍人工智能AI),并概述亚马逊 Web 服务AWS)提供的 AI 能力。它将提供在 AWS 上进行 AI 开发的逐步设置,包括 AWS 软件开发工具包SDK)和 Python 开发工具集。此外,还将介绍现代 AI 应用程序的组件和架构。

本节包括以下章节:

  • 第一章,亚马逊 Web 服务上的人工智能简介

  • 第二章,现代 AI 应用程序的结构

第一章:亚马逊网络服务上的人工智能简介

在本章中,我们将首先从一个高层次概述人工智能AI)开始,包括其历史和广泛使用的方法。然后,我们将看看一些人工智能的应用,它们有潜力深刻改变我们的世界。随着对人工智能兴趣的增加,许多公司,包括亚马逊,都提供了丰富的工具和服务,帮助开发者创建智能化应用。我们将提供亚马逊网络服务(AWS)人工智能产品的高层次概述,并为如何最佳利用这些工具提供指导。作为一本实践性强的书籍,我们将迅速深入亚马逊网络服务的智能化应用开发。

我们将涵盖以下主题:

  • 人工智能及其应用概览。

  • 了解亚马逊网络服务在人工智能方面的不同产品。

  • 如何设置亚马逊网络服务账户以及智能化应用开发环境。

  • 获取亚马逊 Rekognition 及其他支持服务的实践经验。

  • 开发我们的第一个智能化应用。

技术要求

本书的 GitHub 仓库,包含本章的源代码,可以在github.com/PacktPublishing/Hands-On-Artificial-Intelligence-on-Amazon-Web-Services找到。

什么是人工智能?

人工智能(AI)是一个总括性术语,描述了计算机科学的一个分支,旨在创建智能代理。人工智能领域高度技术化且专业化;其中包含了一整套理论、方法和技术,使计算机能够看(计算机视觉)、听(语音识别)、理解(自然语言处理)、说(文本转语音)以及思考(知识推理与规划)。

也许人工智能看起来是当下的流行词,但它自 1950 年代就已存在,当时模仿人脑的人工神经网络的早期工作激发了对思考机器的兴奋。尽管今天它在媒体中受到极大关注,但很难相信这个领域曾经历过两次“人工智能寒冬”,即对人工智能研究和发展的兴趣逐渐消退。如今,人工智能再次流行,得益于数据量的增加、存储成本的降低、算法的进步以及计算能力的提升。

人工智能最重要的子领域之一是机器学习ML)。机器学习是人工智能中如此重要的一部分,以至于这两个术语如今经常被互换使用。机器学习是实现人工智能的最有前景的一组技术。这些技术为我们提供了一种通过自学习算法编程计算机的新方法,能够从数据中获取知识。我们可以训练机器学习模型,寻找模式并像人类一样得出结论。通过这些自学习算法,数据本身已经成为最有价值的资产。数据已成为各行业的竞争优势;它是新的知识产权。在类似的机器学习技术中(即便是较差的机器学习技术),最佳的数据将获胜。

旧的东西再次变得新颖。人工神经网络再次成为机器学习研究和开发的焦点。更多的数据、更强的计算能力以及新的算法,如反向传播,使得神经网络可以拥有许多隐藏层,这也被称为深度神经网络或深度学习。深度神经网络模型的准确度提高,在几年前几乎是不可能的。今天,深度学习是推动现代人工智能繁荣的重大突破。数据、软件和硬件的结合创造了一种新型智能代理,它们在获得与世界相关的丰富信息后,往往可以像人类一样看、听、理解、说话,甚至思考。

人工智能已成为技术领域的重要组成部分。无论大小企业,都在通过利用人工智能解决问题。人工智能的能力正在渗透到我们生活的各个方面,提升我们的记忆力、视力、认知能力等。大多数情况下,人工智能不会单独作为产品出售。相反,您已经使用的产品将通过人工智能得到改善,并成为智能化解决方案。对我们来说,最令人兴奋的是人工智能和机器学习技术与服务的普及。这些技术和服务的丰富性意味着从业者可以轻松利用人工智能,为影响我们生活、工作和娱乐方式的产品增添智能。

本书将帮助您成为一名人工智能从业者。我们将通过实践项目教您如何将人工智能能力嵌入软件解决方案所需的工具和技术。成功的智能化解决方案需要架构设计、软件工程和数据科学的结合。您将学习如何设计、开发、部署和维护具有人工智能能力的生产级软件解决方案。作为一名人工智能从业者,重要的是通过商业能力的视角看待人工智能,而不仅仅是技术视角。本书旨在将各种技能结合起来,帮助您培养直觉,以便设计出能够解决实际问题的优秀智能化解决方案。

人工智能的应用

我们的生活已经被 AI 应用深刻影响,包括我们如何搜索信息、购买商品、与他人交流等等。然而,我们仍处在这个智能软件文艺复兴的初期阶段。虽然许多惊人的 AI 应用已经存在,但我们不妨看看其中几个例子。

自动驾驶车辆

一个受到媒体广泛关注的 AI 应用是自动驾驶车辆,也称为自驾车。这些车辆能够感知周围的世界,并在几乎不需要人类干预的情况下驾驶。

这些自动驾驶车辆是传感器与 AI 技术的完美融合,二者结合创造了自动驾驶能力。为了开发自动驾驶能力,这些车辆已经在高速公路和地方道路上行驶了数百万英里,此外还在模拟中完成了更多的训练。来自各种传感器的数据,包括摄像头、雷达、激光雷达、声纳、GPS 等,被用于训练众多机器学习模型,以执行确保车辆在现实条件下安全行驶所需的感知与执行任务。最终的 AI 能力,如计算机视觉、物体检测、预测建模和障碍物避免算法,可以创建环境的复杂模型,使车载计算机能够理解,从而控制、规划路径和进行导航。

自动驾驶技术比人类驾驶员更不容易出错,并且有潜力拯救数十万人的生命免受车祸和事故的伤害。这项技术也可以成为无法自驾的人的出行方式,例如老年人或残疾人。撰写本文时,世界上并没有大规模部署的真正完全自动驾驶的车辆。我们甚至无法想象,这项技术将在未来几十年如何重新塑造我们的世界。

医疗保健中的 AI

AI 和机器学习正开始改变医疗行业。这些技术被用来改善诊断能力和临床决策,以加速许多疾病的检测与治疗。这些 AI 程序不仅仅是遵循预设的诊断流程来判断疾病。相反,AI 被教会识别特定医疗状况的症状,例如心律失常、糖尿病引起的视力丧失,甚至癌症。

医学影像是关于病人健康的丰富数据源。通过来自 X 光、MRI 和 CT 扫描的高分辨率图像,机器学习模型可以利用成千上万张带有特定医疗条件标签的示例图像进行训练。通过足够的示例,生成的机器学习模型能够以接近甚至超过人类医生的精确度诊断疾病。借助 AI 程序不知疲倦地分析宝贵的医学见解,它们可以帮助医生更快速、准确地做出诊断,从而让患者尽早接受治疗。

更深刻的是,最终的人工智能能力融合了帮助开发这些能力的最优秀医生和临床专家的知识和经验。一旦开发完成,这些能力可以被大规模复制并分发到初级保健办公室和门诊诊所,这些地方以前无法获得这种水平的医学专业知识。这可以通过早期发现和治疗疾病拯救成千上万的生命。它将对人们的生活产生深远的影响,尤其是在那些专业医生稀缺的地区。

个性化预测键盘

尽管在突破性的、革命性的甚至是登月级的人工智能应用中充满了兴奋,这些应用无疑将改变我们的世界,但你并不总是需要追求这些根本上困难的问题,就能用人工智能为我们的世界带来价值。

一个很好的例子是智能启用解决方案在最近现实世界问题中的应用——移动设备上的预测键盘。当触摸屏移动设备变得流行时,我们不得不学会在小型虚拟键盘上打字,通常是在移动中、用更少的手指并且面临更多的干扰。这些预测键盘通过建议我们可能想输入的单词和标点符号,帮助我们更快速地打字,从而减少了移动通信中的摩擦。

这些键盘的预测功能通常是通过机器学习和自然语言处理NLP)技术构建的,结合了语言模型、定制词典和学习到的偏好,构成了它们的预测引擎。最好的预测引擎很可能是使用一种叫做长短期记忆LSTM)的递归神经网络RNN)来构建的。这些神经网络试图根据一段之前输入的文字预测下一个单词。成功预测的关键在于其速度和个性化。每次按键都会产生一个预测,因此预测引擎必须在移动硬件上运行得很快。这些预测引擎的设计目标是随着我们使用它们变得越来越智能;它们是人类参与的在线学习系统的良好例子。

虽然它们并没有拯救成千上万的生命,但这些预测键盘的用户却节省了万亿次的按键输入。我们就是喜欢这些智能启用的软件解决方案的优雅,它们应用了正确的人工智能技术来解决正确的问题。我们希望,通过本书中你将获得的技能和见解,你也能够找到优雅的人工智能应用来改善我们的生活。

为什么使用亚马逊云服务进行人工智能?

亚马逊网络服务AWS)迄今为止是最大的、最全面的云计算平台。AWS 提供了一套广泛的按需云服务,包括计算、存储、数据库、网络、分析等多种服务。多年来,开发者利用这些服务以无法匹敌的规模和速度构建企业级软件解决方案,这种规模和速度是其他任何云计算平台都无法比拟的。

令人兴奋的是,AWS 还提供了大量的 AI 服务,提供预训练的 AI 能力,包括图像识别、自然语言处理(NLP)、语音识别和生成,以及对话代理。AWS 还提供 ML 服务,简化了通过 ML 和深度学习模型构建、训练和部署定制 AI 能力的过程。公司和开发者可以利用这些 AI 和 ML 服务,就像使用 AWS 的其他云计算服务一样轻松地为他们的软件解决方案增加智能。

然而,在 AWS 上开发智能解决方案的真正力量,在于开发者将 AWS 的 AI 和 ML 服务与其他 AWS 云计算生态系统结合使用。通过结合各种 AWS 服务,你将立即获得一个企业级云计算平台,具备高度可靠、可扩展和安全的基础设施。这使得你,作为 AI 从业者,能够轻松收集和处理大规模数据集,从而集成各种 AI 能力,快速原型化创意,并持续实验和迭代解决方案。

正如本书标题所示,这本书是一本实践指南。我们的目标是汇集设计和构建端到端 AI 解决方案所需的各种技能。这里的关键词是技能。我们不仅涵盖 AI 的重要概念,还帮助你通过众多实践项目将这些概念付诸实践。只有通过这些实际操作经验,你才能培养出设计良好的、智能化解决方案的直觉。本书中的项目可以部署到 AWS 云平台;你可以从中学习,可以增强它们,甚至可以向他人展示。

在庞大的 AWS 生态系统中工作将需要一个陡峭的学习曲线。新用户很容易被 AWS 提供的众多服务所压倒。在本书中,我们将教授你开发智能解决方案所需的模式和实践,以及 AWS 平台提供的众多服务。你将深入了解许多 AWS 服务及其应用程序接口APIs)。你不仅将构建可工作的应用程序,还会理解使用这些服务和模式的选择。过程中,我们还将向你展示一些在 AWS 平台上工作的技巧和窍门。

AWS 由大量服务组成,并且仍在不断增长。关于这些服务的各种子集,已经有无数书籍和在线资源进行了深入探讨。在本书中,我们将重点介绍一些 AWS 服务,这些服务能够很好地协同工作,帮助您构建智能化应用程序。我们将涵盖大部分 ML 服务,以及计算、存储、网络和数据库等各种服务。请记住,本书无法涵盖这些服务的所有方面,更不用说涵盖每一项 AWS 服务了。

AWS AI 产品概述

为了更好地理解 AWS AI 产品,我们可以将服务分为两个主要组。

下图展示了本书将涉及的 AWS AI 功能和 AWS ML 平台的子集,并按两个组进行组织:

AWS ML 服务的列表每年都在增加。例如,Amazon Personalize、Forecast、Textract 和 DeepRacer 在 AWS re:Invent 2018 大会上发布,并且当时是有限预览。这些服务在 2019 年中期左右开始对公众开放。

上图中的第一个组是 AWS AI 功能。这些服务是建立在 AWS 预训练的 AI 技术之上的。它们直接 开箱即用,为您的应用程序提供现成的智能。您无需理解这些技术背后的 AI 原理,也不需要维护托管它们的基础设施。AWS 已经为您完成了所有艰难的工作,并通过 API 提供这些 AI 功能。随着 AWS 不断改进这些功能,您的应用程序将自动变得更加智能,而您无需做任何额外的努力。这些托管服务能够快速提升您的应用程序,使得智能解决方案得以快速且经济地构建。

以下是这些 AWS AI 功能:

  • Amazon Comprehend: 一种使用机器学习(ML)技术分析文本中的洞察和关系的自然语言处理(NLP)服务。此技术使您的应用程序能够筛选大量非结构化文本,并挖掘出有价值的信息。这项服务可以执行各种任务,包括自动分类文档;识别实体,如公司名称、人物和地址;以及提取文本中的主题、关键短语和情感。

  • Amazon Lex: 一项用于通过语音或文本将对话界面集成到应用程序中的服务。这项功能使用深度学习技术,如 自动语音识别ASR)和 自然语言理解NLU),将语音转换为文本,并识别文本中的意图。这项技术与 Amazon Alexa 语音助手背后的技术相同,您也可以将这项技术嵌入到自己的应用程序中。

  • Amazon Polly: 一种将文本转换为逼真语音的服务,可以为您的应用程序增加人类语音。支持这项服务的文本转语音技术使用先进的深度学习技术,可以合成具有不同语言、性别和口音的语音。

  • Amazon Rekognition: 一种可以分析图像和视频以识别对象、人物、文本、场景和活动的服务。此服务还可以为各种应用程序提供精确的面部分析和识别。支持这项服务背后的深度学习技术已在数十亿张图像和视频上进行了训练,以在多种分析任务上实现高精度。

  • Amazon Transcribe: 一个提供语音到文本能力的 ASR 服务,可以为您的应用程序提供此技术。此技术允许您的应用程序分析存储的音频文件或实时音频流,并实时获取转录文本。

  • AWS Translate: 一种提供自然流畅语言翻译的神经机器翻译服务。这项服务由能够提供准确和自然音质翻译的深度学习模型支持。您甚至可以配置此服务以包括品牌名称、产品名称和其他自定义术语的自定义语言模型。

在前述图表中的第二组是 AWS ML 平台。这些服务是完全托管的基础架构和工具集,帮助开发人员通过 ML 构建和运行其自定义 AI 能力。AWS 提供开发构造并处理 ML 训练计算资源,以便更轻松地开发自定义 AI 能力。AI 从业者负责设计这些 AI 能力的内部工作原理。这可能包括:收集和清洗训练数据;选择 ML 库和算法;调整和优化 ML 模型;设计和开发接入 AI 能力的界面。利用 AWS ML 平台构建自定义 AI 能力肯定比使用托管 AI 服务更为复杂,但这组服务为您创建创新解决方案提供了最大的灵活性。

本书将涵盖的 AWS ML 平台是:Amazon SageMaker—一个完全托管的服务,涵盖整个 ML 工作流程。使用 SageMaker,您可以收集和处理训练数据;可以选择 ML 算法和 ML 库,包括 TensorFlow、PyTorch、MXNet、Scikit-learn 等;可以在 ML 优化的计算资源上训练 ML 模型;并可以调整和部署生成的模型,为您的应用程序提供专门创建的 AI 能力。

我们强烈建议您尽可能首先利用 AWS 托管的 AI 服务。只有在需要自定义 AI 能力时,才应使用 AWS AI ML 平台构建它们。

与 AWS 服务一起实践

不再废话,让我们动手操作一些 AWS 服务。本节中我们将使用的服务和执行的任务将为你在 AWS 上进行智能应用开发打下基础。

创建你的 AWS 账户

如果你还没有 AWS 账户,可以在aws.amazon.com/注册一个,然后点击注册按钮。你将看到以下屏幕:

你的 AWS 账户让你可以按需访问 AWS 上的所有服务。但不用担心——通过 AWS 的按使用付费定价模式,你只需为实际使用的服务付费,而且价格具有行业领先的亲民水平。如果这是你首次注册 AWS 账户,你的账户会自动获得 12 个月的免费套餐访问权限。免费套餐提供一定数量的 AWS 服务免费使用,包括计算、存储、数据库和 API 调用。免费套餐期满后,还有一些永不过期的免费资源。详情请访问aws.amazon.com/free

在 AWS 管理控制台中导航

现在,让我们熟悉一下 AWS 管理控制台。第一次登录 AWS 账户时,你将看到 AWS 管理控制台。它可能看起来像下面的截图:

AWS 管理控制台是一个网页界面,你可以在其中管理 AWS 云平台。通过这个控制台,你可以启动、监控和停止各种资源,例如云计算和云存储;你可以管理你的 AWS 账户设置,包括月度账单和细粒度访问控制;你甚至可以访问教育资源,帮助你入门使用 AWS 提供的各种服务。

AWS 管理控制台是你与 AWS 云平台交互的三种方式之一。其他两种方式分别是 AWS 命令行界面(CLI)和 AWS 软件开发工具包(SDK)。我们将在本章稍后讨论这两种方式。

查找 AWS 服务

在 AWS 管理控制台上,你可以点击控制台左上角的服务选项卡。在这里,你将看到按照组别组织的各种 AWS 服务。你也可以通过名称搜索服务;搜索通常是快速找到你所需服务的方式。

你的服务选项卡应该看起来像这样:

选择 AWS 区域

不是每个 AWS 服务都可以在每个 AWS 区域使用。AWS 是一个全球云基础设施,基于 AWS 区域的概念构建。AWS 区域是你可以运行云应用程序的世界上一个物理位置。根据你选择操作的区域,某些服务可能不可用。例如,在我们创建账户时,默认区域是 US East(Ohio)。在写作时,俄亥俄区域没有 Amazon Lex 服务。

如果某个服务在某个区域不可用,你会看到区域不支持的消息,类似于这样:

对于本书,我们建议你将区域更改为US EastN. Virginia),也就是us-east-1区域。这个北弗吉尼亚区域提供了所有 AWS 服务,而且是首批获得最新 AWS 服务的区域。

点击 AWS 账户名旁边的区域名称,选择北弗吉尼亚地区。这一步很重要,以确保本书中的示例和项目保持一致。US East (N. Virginia)区域是以下截图中的首选:

测试 Amazon Rekognition 服务

让我们尝试一下 AWS 托管的 AI 服务——Amazon Rekognition,感受一下 AWS 的 AI 能力:

  1. 机器学习下的服务列表中点击Rekognition,进入其主页。

  2. Rekognition 提供了一套视觉分析功能,用于分析图像和视频。通过 Rekognition,你可以快速为图像和视频中的物体、面孔和文字检测添加强大的功能。你无需理解这些功能背后的深度学习技术,就可以将它们添加到你的应用中。在本书中的实践项目中,我们将创建多个这样的应用,但现在,让我们通过其中一个演示,看看 Rekognition 的能力如何发挥作用。

  3. 从 Amazon Rekognition 主页的左侧窗格中,点击演示部分下的物体和场景检测

  4. AWS 已经提供了几张示例图片来展示 Rekognition 的强大功能。在其中一张图片中,你和我都可以轻松看到,一个滑板运动员正在一条路上做特技,路两侧停满了车。这对于计算机来说是一个相当繁忙的图像,进行分析并不容易。

  5. 那么,Rekognition 的表现如何呢?Rekognition 已经为它检测到的物体画上了框,你可以将鼠标悬停在这些框上,以查看 Rekognition 认为每个物体是什么。

这是 Rekognition 演示页面,展示了滑板运动员图像的检测结果:

  1. 在图像的右侧,在结果下,Rekognition 还提供了它检测到的所有物体的置信度水平。

在置信度等级下方还有请求响应。实际上,这个演示页面实际上是在代表你调用 Rekognition 的对象和场景检测 API。如果你展开请求,它会显示有关 API 调用的一些细节:

{
 "Image": {
   "S3Object": {
        "Bucket": "console-sample-images",
        "Name": "skateboard.jpg"
    }
   }
}

请求采用JavaScript 对象表示法JSON)格式。请求指定要通过 Rekognition API 分析的图像。更具体地说,这是一个作为对象存储在 Amazon 简单存储服务S3)中的图像。从请求中可以看出,图像存储在 console-sample-images 桶中,名称为 skateboard.jpg

这个演示应用程序使用 Amazon S3 服务来存储示例图像,Rekognition 可以直接分析存储在 S3 中的图像。在后续章节的许多项目中,我们也将利用这种模式。正如我们之前提到的,AWS 生态系统的强大之处在于其众多服务之间的互操作性。

响应也是 JSON 格式。响应包含了有关在示例图像中检测到的对象的许多信息。此信息包括对象的名称、检测的置信度,甚至是每个对象在图像中所在边界框的坐标。在我们的项目中,我们将学习如何处理这种 JSON 响应,以便在智能化应用程序中使用结果。

在这个演示中,你还可以上传自己的图像来测试 Rekognition。找一张图片试试看。当你将图片上传到演示页面时,你会注意到发送到 API 的请求稍有不同。在请求中,你会看到以下内容:

{
   "Image": {
     "Bytes”: “...”
    }
}

这一次,上传的图像的原始字节直接发送到 Rekognition API,而不是指定 S3 中的图像。这个 Rekognition API 有多个变体:一个是引用 S3 对象,另一个是接受图像的原始字节。你选择哪个变体取决于你应用程序的性质。

使用 S3

Amazon S3 服务是 AWS 提供的首批服务之一。S3 提供安全、耐用、可扩展的对象存储,并且成本非常低。对象存储意味着你存储在 S3 中的内容是按文件级别进行访问的,而不是按块或字节级别进行访问。S3 是一个非常灵活的服务,具有多种使用模式。你可以在 aws.amazon.com/s3 上查看更多有关 Amazon S3 的信息。

让我们开始使用 Amazon S3 创建一个桶。你可以将桶视为一个文件夹,它可以存储无限数量的文件(对象)。

从 Amazon 管理控制台的左上角点击服务标签,然后在存储下点击或搜索S3,即可导航到 Amazon S3 的主页。如果这是你第一次使用 S3,你将看到一个类似于这个的页面:

在本书中,我们将在实践项目中大量使用 S3。我们将把 S3 用于三个主要目的。第一个目的是存储媒体文件和其他内容,以便其他 AWS 服务访问。许多 AWS AI 服务与 S3 紧密集成;例如,我们在 Rekognition 演示中看到了这种模式。第二个目的是使用 S3 托管整个静态网站,包括 HTML 文件、图像、视频和客户端 JavaScript。这使我们能够托管互动式 web 应用,而无需传统的 Web 服务器。第三个目的是将 S3 用作数据存储,用于收集、处理和分析任务,尤其是在训练我们自定义的机器学习模型时。

S3 存储类有多种,这些存储类已经针对不同的使用场景和成本层级进行了重新设计。对于您的企业级应用,您可能需要利用不同的存储类来平衡性能和成本。在本书中,我们将使用 Amazon S3 标准存储类进行通用存储。这是默认的存储类,足以满足本书中的项目需求。

点击 创建桶 按钮以创建一个新桶:

模型的第一屏要求提供三项信息:桶名称区域从现有桶复制设置。由于这是您的第一个桶,因此我们可以忽略第三项信息。

S3 桶名称必须是全球唯一的。这意味着您和其他人创建的每个桶名称都必须是唯一的。创建一个全球唯一的桶名称可能会很具挑战性;您不能指望像 contentswebsitedata 这样的名称仍然可用。S3 桶名称必须符合 DNS 标准,以便您可以遵循类似域名的命名模式。例如,如果我们选择 aws.ai 作为根域名,则可以创建诸如 contents.aws.aiwebsite.aws.aidata.aws.ai 之类的桶名称以避免冲突。请考虑一下您希望使用的根域名。

您不需要拥有域名就能使用指定的根域名命名桶;不过,如果您拥有域名,最好将其作为根域名使用。

您还必须指定桶的区域。这将决定您的对象将在全球的哪个物理区域存储。AWS 各区域之间是完全隔离的设计。存储在一个区域的对象不能被运行在另一个区域的服务和应用访问。如果您的业务线有高性能要求,需要将应用和数据部署得离客户更近,这一点就非常重要。如果您的业务线必须遵守行业和政府规定,要求应用和数据必须位于某个特定地理位置,这一点同样重要。

对于本书中的项目,我们不需要担心这两个问题。因此,为了保持一致性,我们再次选择美国东部(弗吉尼亚北部)区域。

这是你创建后的S3 存储桶页面的样子,当然,存储桶名称会不同:

一旦创建了 S3 存储桶,点击contents.aws.ai存储桶。你将看到类似这样的界面:

在这个界面,你将能够上传文件到存储桶,配置存储桶属性,设置访问权限,并执行一些高级设置,比如生命周期规则和跨区域复制。我们稍后会回到这些设置,但现在请上传一些你想用 Rekognition 服务分析的照片。你可以点击上传按钮,或将照片直接拖放到页面上进行上传。我们现在可以将所有文件设置保持为默认。

恭喜你,刚刚将文件存储到 AWS 云平台,具有 99.999999999%的持久性和 99.99%的可用性!换句话说,如果你在 S3 中存储 10,000 个文件,按统计学来说,你将每 1000 万年丢失一个文件,而且所有文件在每年 525,600 分钟中的 525,547.4 分钟里都可以被你的应用程序访问。

身份与访问管理

我们接下来要看的 AWS 服务是身份与访问管理IAM)。IAM 允许你安全地管理对其他 AWS 服务和资源的访问。AWS 提供企业级的安全性和访问控制,非常适合在云中构建生产级应用程序。然而,如果你是 AWS 新手,初次使用 IAM 可能会有些挑战。如果没有授予必要的访问权限,服务将直接拒绝执行所需的操作。我们将在本书中的项目中频繁使用 IAM,你将逐渐熟悉诸如用户、组和角色等概念,从而为你的应用程序提供所需的服务访问权限。

为了访问 IAM 主页,点击安全、身份与合规性下的IAM服务列表中的IAM,你将导航到其主页。IAM 主页应该类似这样:

IAM 仪表板为你提供 IAM 资源及其安全状态的概览。目前我们没有任何用户或组,但 AWS 默认创建了两个角色。

我们一直在使用根账户访问 AWS 管理控制台。默认情况下,这个账户只能访问 AWS 管理控制台;它无法通过程序化方式与 AWS 服务交互。我们现在创建一个具有程序化访问权限的新用户,供动手项目使用。

在左侧面板点击用户,然后点击添加用户按钮:

在输入所需的用户名后,确保只选择Programmatic access。编程访问将启用访问密钥 ID 和秘密访问密钥对。这个密钥对可以被 AWS API、CLI 和 SDK 使用。通常的做法是将每个用户限制为编程访问或 AWS 管理控制台访问。

在这里,我们创建了一个只有编程访问权限的aws_ai用户:

在下一屏幕上,我们还需要创建一个组来管理权限。我们建议将用户添加到一个或多个具有必要权限的组中,而不是直接将单独的权限和策略附加到用户。这种方式更容易管理权限,尤其是当你的组织中有很多用户需要不同权限时。

点击Create group按钮,位于Add user to group下,如下所示:

对于我们的组,命名为Developer,然后将AdministratorAccess策略附加到该组:

我们无需创建任何标签,因此只需审核并创建该用户。

为了简便起见,我们附加了一个功能强大的策略,允许完全访问 AWS 服务。对于你的生产环境,你需要更细致地管理权限和策略。在系统安全性方面,始终遵循最小权限原则。

用户创建完成后,你将看到Success屏幕:

在此屏幕上,你可以选择查看或下载我们之前提到的访问密钥对。这是唯一一次可以显示或下载密钥对,因此请将 CSV 文件下载到你的计算机上。稍后在本章节中,我们需要在使用 AWS CLI 和 AWS SDK 时用到这个密钥对。

访问密钥对相当于你的用户名和密码组合。务必不要与他人共享你的密钥对,因为它将授予他人访问你的 AWS 资源的权限,但账单却会由你来承担。另外,切勿在源代码中硬编码密钥对,并将其提交到公共源代码仓库。现在有一些自动化机器人会扫描代码库,寻找 AWS 密钥对,以窃取资源进行黑客攻击或加密货币挖掘。

恭喜你,你刚刚使用 IAM 服务创建了一个用户并将其添加到具有管理员权限的组!如果你返回到仪表板,你会看到我们已经将其添加到 IAM 资源中,并且在安全状态方面取得了进展:

我们强烈建议你完成安全状态部分剩余的两个步骤。第一个剩余步骤是激活 多因素认证MFA)以保护你的根账户。激活 MFA 会增加根账户的安全性,要求任何人登录时必须同时提供根账户密码(你知道的东西)和来自身份验证设备的动态令牌(你拥有的东西),例如智能手机。第二个剩余步骤是设置 IAM 密码策略,以确保遵循安全的密码实践。你可以定义密码的长度、复杂度、过期期限等。

熟悉 AWS CLI

AWS CLI 是一个允许你通过在终端 shell 中发出命令与 AWS 服务交互的工具。在本章之前,我们通过基于 Web 的 AWS 管理控制台与一些 AWS 服务进行了交互。虽然 Web 控制台是新用户熟悉 AWS 的最简单界面,但在软件开发过程中使用起来可能会显得繁琐。通过 AWS CLI 工具,你可以获得与 Web 控制台相同的所有功能,但这些功能直接在终端 shell 中触手可得,而终端是你大多数开发工具所在的地方。这样,你的开发过程将更加流畅,无需频繁切换到浏览器。

AWS CLI 主要通过 Python 包管理器分发;因此,你需要先在开发机器上安装 Python。请注意,我们将在开发项目中使用 Python。按照说明安装 Python 非常重要,这样可以确保你以后开发环境的兼容性。

安装 Python

Python 可用于所有三大主流操作系统:macOS、Linux 和 Microsoft Windows。你可以在 www.python.org 上找到安装程序和文档。本书是针对 Python 3.7.x 或更高版本编写的(除非另有说明),建议你使用最新版本。

在 macOS 上安装 Python

虽然 Python 已经预安装在 macOS 上,但该版本的 Python 是 2.7。要安装更新版本的 Python,我们建议使用一种名为 Homebrew 的 macOS 包管理器。Homebrew 被誉为 macOS 缺失的包管理器;它简化了许多 macOS 软件包的安装,包括 Python。要安装 Homebrew,请按照其网站上的说明操作:brew.sh。在写作时,在终端中安装 Homebrew 的命令如下:

$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

安装完 Homebrew 后,我们可以使用以下终端命令安装最新版本的 Python 和 pip,后者是 Python 包管理系统:

$ brew install python3
$ brew install pip3

使用终端命令检查最新的 Python 和 pip 版本是否已经正确链接到你的系统:

$ python --version
$ pip --version

这些命令的输出应分别显示类似于 3.7+ 和 18.0+ 的版本号。

在 Linux 上安装 Python

有许多不同的 Linux 发行版可供选择。安装 Python 的指令可能会有所不同,具体取决于你的 Linux 发行版。一般来说,你应该首先检查系统中是否已安装 Python,可以在终端中运行以下命令:

$ python --version
$ pip --version

如果没有安装 Python 或 pip,或安装了不同版本的 Python,可以通过 Linux 发行版的包管理器进行安装:

  • 对于 Debian 衍生版本,如 Ubuntu,使用apt
$ sudo apt-get install python3 python3-pip
  • 对于 Red Hat 衍生版本,如 Fedora,使用yum
$ sudo yum install python python-pip
  • 对于 SUSE 衍生版本,使用zypper
$ sudo zypper install python3 python3-pip

在 Microsoft Windows 上安装 Python

设置 Python 环境有多种选择,具体取决于你使用的是 Microsoft Windows 10 还是较早版本的 Windows。

Windows 10

如果你正在使用 Windows 10,我们强烈建议你安装Windows Subsystem for LinuxWSL)。WSL 允许你在 Windows 操作系统上运行你选择的 Linux 发行版。

首先,你需要启用 WSL,这是 Windows 10 中的一个可选功能。为此,请以管理员身份打开 PowerShell,并运行以下命令:

> Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux

如果系统提示,请重新启动计算机。

接下来,你可以从 Windows 商店下载并安装你喜欢的 Linux 发行版。截至本文撰写时,WSL 可用的 Linux 发行版有五个:Ubuntu、OpenSUSE、SUSE Linux Enterprise Server、Debian GNU/Linux 和 Kali Linux。

一旦你安装了所需的 Linux 发行版,你就可以按照该发行版的 Python 安装说明进行操作。

早期版本的 Windows

如果你使用的是较早版本的 Windows,建议使用 Anaconda Python 发行版和包管理器。你可以在 www.anaconda.com/download 找到 Anaconda 安装程序和相关文档。

安装 AWS CLI

一旦 Python 在你的开发机器上成功安装,我们就可以继续安装 AWS CLI。AWS CLI 主要通过 Python 包管理器pip进行分发,而我们刚刚安装了它。你可以使用以下命令来安装并验证 AWS CLI:

$ pip install awscli
$ aws --version

请注意,AWS CLI 的命令是aws,尽管我们安装的软件包是awscli

配置 AWS CLI

在使用 AWS CLI 之前,我们需要执行一些配置步骤。配置 AWS CLI 的最快方法是使用以下命令:

$ aws configure
AWS Access Key ID [None]: <your access key>
AWS Secret Access Key [None]: <your secret key>
Default region name [None]: us-east-1
Default output format [None]: json

上述代码的解释如下:

  • 我们需要输入的前两项是安全凭证,以便 CLI 可以代表你执行操作。这是我们在使用 IAM 服务创建新用户时下载的 CSV 文件中包含的密钥对。打开 CSV 文件并将访问密钥 ID 和密钥访问密钥复制并粘贴到配置命令提示符中。

  • 接下来,对于默认区域名称,我们将在本书中保持一致,继续使用us-east-1

  • 最后,对于默认输出格式,输入 json。这将把 AWS CLI 的输出设置为 JSON 格式。

AWS 配置命令会在您的用户主目录中创建一个隐藏目录 .aws,例如在 macOS 和 Linux 上是~/.aws。在该目录中,会创建两个文件。一个是 .aws/credentials,其中包含以下代码:

[default]
aws_access_key_id = YOUR_ACCESS_KEY
aws_secret_access_key = YOUR_SECRET_KEY

另一个文件是 .aws/config,其中包含以下代码:

[default]
region = us-east-1
output = json

在您的系统上找到这些文件,并验证其内容。

如果您没有记录或下载访问密钥对,可以在 AWS 管理控制台中获取新的密钥对:

  1. 安全、身份与合规部分下,导航到IAM服务。

  2. IAM 管理控制台中,点击右侧窗格中的用户,然后点击您的用户名。

  3. 在用户总结页面,点击安全凭证标签。

  4. 访问密钥部分,点击创建访问密钥按钮,系统将为您创建一个新的访问密钥。

  5. 请记得在创建新密钥对后删除旧的密钥对。

这就是您的屏幕显示效果:

记得每次更改访问密钥时,都需要使用以下命令重新配置您的 AWS CLI:

$ aws configure

输入您的安全凭证、默认区域和默认输出格式,如我们在初始 AWS CLI 配置中所述。

要测试 AWS CLI 是否配置正确,请执行以下命令:

$ aws s3 ls
2018-12-01 18:01:20 contents.aws.ai
2018-12-01 18:01:49 data.aws.ai
2018-12-01 18:01:35 website.aws.ai

此命令将打印出您 AWS 账户中的所有 S3 桶。更具体地说,此命令列出了与访问密钥相关联的用户有权限查看的 S3 桶。记得我们之前配置 CLI 时使用的密钥对吗?它属于一个我们授予了管理员权限的用户。管理员策略中的一个权限允许该用户访问 S3。无论如何,您应该能够通过 AWS 管理控制台看到我们在本章前面部分创建的 S3 桶。

使用 AWS CLI 调用 Rekognition 服务

现在,让我们通过 AWS CLI 调用 Amazon Rekognition 的物体检测功能。这次,我们将在存储在 S3 桶中的图像上执行物体检测。我们将使用 Pexels 网站上的一张示例图片,Pexels 是一个拥有成千上万张免版税图片的网站。请下载 www.pexels.com/photo/animal-beagle-canine-close-up-460823/ 上的图像,并将其上传到 contents S3 桶。

在这里,我们可以看到一只可爱的比格犬小狗躺在看起来像是碎石床上的样子:

当您列出 contents 桶中的对象时,应该看到如下输出:

$ aws s3 ls s3://<YOUR BUCKET>
2018-12-02 13:31:32     362844 animal-beagle-canine-460823.jpg

现在我们有了图像,可以通过以下 CLI 命令调用 Rekognition 的物体检测功能。请注意,我们必须用 \ 转义 {} 字符,并且在命令行中指定 S3 对象时,不能包含任何空格:

$ aws rekognition detect-labels --image S3Object=\{Bucket=<YOUR BUCKET>,Name=animal-beagle-canine-460823.jpg\}

结果几乎瞬间返回:

{
 "Labels": [
 {
 "Name": "Mammal",
 "Confidence": 98.9777603149414
 },
 {
 "Name": "Pet",
 "Confidence": 98.9777603149414
 },
 {
 "Name": "Hound",
 "Confidence": 98.9777603149414
 },
 {
 "Name": "Dog",
 "Confidence": 98.9777603149414
 },
 {
 "Name": "Canine",
 "Confidence": 98.9777603149414
 },
 {
 "Name": "Animal",
 "Confidence": 98.9777603149414
 },
 {
 "Name": "Beagle",
 "Confidence": 98.0347900390625
 },
 {
 "Name": "Road",
 "Confidence": 82.47952270507812
 },
 {
 "Name": "Gravel",
 "Confidence": 74.52912902832031
 },
 {
 "Name": "Dirt Road",
 "Confidence": 74.52912902832031
 }
 ]
}

输出为 JSON 格式,就像我们配置 CLI 输出一样。从输出中可以看到,Rekognition 服务检测到多个物体或标签。Rekognition 确信它检测到了狗,甚至识别出了这只狗的品种是比格犬!Rekognition 还检测到了图像中的碎石,这可能是泥土道路的一部分。当我们尝试 AWS 服务并查看输出结构时,AWS CLI 非常有用,它帮助我们了解开发应用程序时的输出结构。

使用 Python 进行 AI 应用程序开发

Python 是最受欢迎的编程语言之一。由于它在数据科学和机器学习社区中的广泛使用,Python 也是增长最快的编程语言之一。开发者和开源社区贡献了大量的附加库。这些库使 Python 开发者能够做几乎所有事情,从数据分析到深度神经网络,从简单脚本到 Web 应用程序开发。

对于 AI 和机器学习,Python 是事实上的标准语言。流行的 scikit-learn 库为开发者提供了访问许多有用机器学习算法的途径。还有许多深度神经网络的库,例如 MXNet 和 TensorFlow。

在本书中的每个动手项目中,我们都将使用 Python:

  • 在本书的前半部分,我们将使用 AWS AI 服务创建智能解决方案。在这些项目中,我们将使用 Python 创建后端组件、API 和 Web 应用程序,让我们的智能创作得以实现。AWS 提供了一个名为 Boto 的 Python SDK。通过 Boto,我们可以从应用程序与所有 AWS 服务进行交互,包括托管的 AI 能力。

  • 在本书的后半部分,我们将使用 AWS ML 服务训练自定义机器学习模型。在这些项目中,我们将使用 Python 处理数据、训练机器学习模型并部署智能功能。除了 Boto SDK,我们还将使用 AWS 的 SageMaker、弹性 MapReduceEMR)等许多库。

设置 Python 开发环境

让我们从设置本地开发环境开始。由于我们在项目中构建的是端到端的解决方案,因此我们需要安装许多包和依赖项。我们所需要的包并不总是标准库的一部分。有时我们的项目需要特定版本的库才能使各个组件正常协作。因此,按照本章中描述的步骤安装这些包是非常重要的。

使用 Pipenv 设置 Python 虚拟环境

我们将使用 Python 虚拟环境来管理本书中的项目包。虚拟环境是一种 Python 化的方式,用于创建一个自包含的项目目录树,包含特定版本的 Python 和特定于项目的包组合。

使用 Python 虚拟环境有很多好处:

  • 由于项目的所有包和依赖项都在配置文件中指定,因此其他开发人员可以轻松复制项目的开发环境;这在团队协作时非常有用。

  • 即使你是独自工作,拥有一个虚拟环境也会帮助你在一台或多台计算机上为开发、测试和部署创建(并重建)环境。

  • 虚拟环境还允许你创建独立的 Python 环境,在其中可以安装 Python 依赖项的并行副本。这样,我们就可以在同一台计算机上为不同项目保留不同的 Python 版本和包,而不会发生冲突。

Pipenv 是 Python 虚拟环境中的新兴工具,但它已被推崇为python.org官方推荐的 Python 打包工具。要安装pipenv,我们将使用 Python 的包管理工具 Pip:

$ pip install pipenv
$ pipenv --version

该命令将帮助你安装并验证pipenv

创建你的第一个 Python 虚拟环境

现在我们已经安装了 Python 工具集,让我们通过创建一个可以与 AWS 云平台交互的 Python 项目来测试它。首先,让我们为项目创建一个名为ObjectDetectionDemo的目录。在该目录中,我们使用pipenv初始化一个 Python 3 虚拟环境,如下所示:

$ mkdir ObjectDetectionDemo
$ cd ObjectDetectionDemo
$ pipenv --three

执行这些命令后,ObjectDetectionDemo目录将包含一个PipfilePipfile是一个pipenv配置文件,指定了该项目的 Python 包及其依赖项。

接下来,我们为ObjectDetectionDemo项目指定并安装 AWS Python SDK,Boto:

$ pipenv install boto3

可能需要几分钟的时间让 Pipenv 与 Python 包索引同步并安装boto3包及其依赖项。安装完成后,你的 Pipfile 应包含以下内容:

[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[packages]
"boto3" = "*"

[requires]
python_version = "3.7"

如你所见,boto3在包部分下有一个条目。目前,版本号列为*,这意味着使用最新版本。如果需要,你可以将*替换为任何包的特定版本。

第一个使用 AWS SDK 的项目

现在,让我们编写第一个 Python 应用程序,检测存储在 S3 存储桶中的图像中的物体。为此,我们将利用boto3与 Amazon S3 服务和 Amazon Rekognition 服务进行交互:

你可以使用任何文本编辑器,或你喜欢的 Python 集成开发环境 (IDE) 来创建 Python 源文件。如果你没有偏好,建议你查看 JetBrains PyCharm,www.jetbrains.com/pycharm/,这是一款跨平台的 Python IDE,提供代码编辑、代码分析、图形调试器、集成单元测试和版本控制系统的集成。

  1. 我们将创建的第一个源文件是 storage_service.py。在 ObjectDetectionDemo 目录中创建此源文件。以下是 storage_service.py 的 Python 代码:
import boto3

class StorageService:
    def __init__(self):
        self.s3 = boto3.resource('s3')

    def get_all_files(self, storage_location):
        return self.s3.Bucket(storage_location).objects.all()

在这段代码中,请注意以下信息:

    • storage_service.py 包含一个 Python 类 StorageService,它封装了与 Amazon S3 交互的业务逻辑。

    • 该类仅实现了一个方法 get_all_files(),该方法返回由 storage_location 参数指定的存储桶中的所有对象。

    • 与 Amazon S3 相关的其他功能也可以在此文件中实现,例如列出存储桶、向存储桶上传文件等。

  1. 接下来,我们将创建的源文件是 recognition_service.py。也在 ObjectDetectionDemo 目录中创建此源文件。以下是 recognition_service.py 的 Python 代码:
import boto3

class RecognitionService:
    def __init__(self):
        self.client = boto3.client('rekognition')

    def detect_objects(self, storage_location, image_file):
        response = self.client.detect_labels(
            Image = {
                'S3Object': {
                    'Bucket': storage_location,
                    'Name': image_file
                }
            }
        )

        return response['Labels']

在这段代码中,请注意以下信息:

    • recognition_service.py 包含一个 Python 类 RecognitionService,它封装了与 Amazon Rekognition 服务交互的业务逻辑。

    • 该类仅实现了一个方法 detect_objects(),该方法调用 Rekognition 的检测标签 API,并返回响应中的标签。

    • 调用此方法的用户可以分别通过 storage_locationimage_file 参数指定 S3 存储桶和文件名。

    • 与 Amazon Rekognition 相关的其他功能也可以在此文件中实现,例如文本检测、面部分析等。

  1. 最后一个要创建的文件是 object_detection_demo.py。在 ObjectDetectionDemo 目录中创建此源文件。以下是 object_detection_demo.py 的 Python 代码:
from storage_service import StorageService
from recognition_service import RecognitionService

storage_service = StorageService()
recognition_service = RecognitionService()

bucket_name = 'contents.aws.ai'

for file in storage_service.get_all_files(bucket_name):
    if file.key.endswith('.jpg'):
        print('Objects detected in image ' + file.key + ':')
        labels = recognition_service.detect_objects(file.bucket_name, file.key)

        for label in labels:
            print('-- ' + label['Name'] + ': ' + str(label['Confidence']))

在这段代码中,object_detection_demo.py 是一个 Python 脚本,它将我们两个服务的实现整合在一起,以便对存储在 S3 存储桶中的图像进行目标检测。

下面是描述演示应用程序流程的交互图:

请注意以下信息,所有这些内容都在前面的图中展示:

  • 这个脚本调用 StorageService 来获取存储在 contents.aws.ai 存储桶中的所有 JPG 图像文件(你应该将其替换为自己的存储桶)。

  • 在这里,为了简化,我们硬编码了存储桶名称,但你可以将存储桶名称作为参数传入,以使脚本更加通用。

  • 然后,对于指定存储桶中的每个图像,脚本会调用我们的 RecognitionService 来执行物体检测,并返回检测到的标签。

  • 脚本还会格式化并打印出这些标签,以及与检测到的物体相关的置信度分数。

请注意,我们在 StorageServiceRecognitionService 中都使用了 boto3boto3 对象管理我们项目代码与 AWS 服务之间的会话。这些会话是使用运行时环境中的可用凭证创建的。如果你在本地开发机器上运行脚本,那么 AWS 访问密钥对将从 ~/.aws/credentials 文件中获取。我们将在后续章节中讨论如何在其他运行时环境中使用凭证。

为了简单起见,我们保持了项目代码的简洁和简单。我们将在后续的动手项目中增强这些 Python 类。

即使这只是一个演示项目,组织代码并实现关注点分离仍然是一个很好的实践。在本项目中,所有与 Amazon S3 服务交互的业务逻辑都封装在 StorageService 类中;与 Amazon Rekognition 服务交互的所有逻辑则封装在 RecogntionService 类中。随着项目规模和复杂度的增加,我们将看到这种设计实践的更多好处。

  1. 现在,让我们通过进入虚拟环境的 shell,运行以下脚本:
$ pipenv shell

在此命令中,请注意以下信息:

    • 此命令在你的普通终端 shell 中启动一个带有 Python 虚拟环境的 shell。

    • 在虚拟环境 shell 中,我们指定并通过 pipenv 安装的 Python 版本和包都可供脚本使用。

  1. 在虚拟环境中,通过以下命令调用 object_detection_demo.py 脚本:
$ python object_detection_demo.py

该命令的输出应显示在指定的 S3 存储桶中检测到的物体:

Objects detected in image animal-beagle-canine-460823.jpg:
-- Pet: 98.9777603149414
-- Hound: 98.9777603149414
-- Canine: 98.9777603149414
-- Animal: 98.9777603149414
-- Dog: 98.9777603149414
-- Mammal: 98.9777603149414
-- Beagle: 98.0347900390625
-- Road: 82.47952270507812
-- Dirt Road: 74.52912902832031
-- Gravel: 74.52912902832031
  1. 记得通过 exit 命令退出虚拟环境并返回到正常的终端 shell:
$ exit

恭喜你,你刚刚创建了第一个利用 AI 技术进行图像分析的智能应用程序,并且它运行在 AWS 平台上!坐下来好好想一想;只用几行代码,你就能够创建一个可以检测并识别我们世界中无数物体的软件。这就是你通过利用 AWS AI 服务所能获得的快速提升。

总结

在这一章中,我们了解到 AI 已经存在了很长时间,但由于机器学习(ML)的重新兴起,特别是人工神经网络的出现,它现在再次变得流行。我们查看了 AI 和 ML 的几个现实应用案例。我们概述了 AWS 提供的两大类 AI 产品;AWS AI 服务可以在开发智能应用时提供快速支持,而 AWS ML 平台则允许你构建针对特定应用的定制 AI 能力。我们建议你首先尝试利用 AWS 提供的 AI 服务,只有在你有特定需求并且具备数据竞争优势的情况下,才考虑开发定制的 AI 能力。

我们通过实践经验熟悉了 AWS 上的多个服务,包括 AI 服务和其他配套的云服务。我们还为 AI 应用设置了本地开发环境,包括 Python、AWS CLI 和 Python 虚拟环境。随后,我们使用 Amazon S3 和 Amazon Rekognition 服务创建了第一个智能化应用。

在下一章中,我们将深入探讨 AI 应用的组件和架构。我们将为许多即将进行的实践项目设立架构模板,更重要的是,我们将讨论该架构模板所基于的设计原则和决策。

参考资料

你可以访问以下链接获取更多关于 AWS 上 AI 的信息:

第二章:现代 AI 应用的构成

在这一章中,我们将讨论人工智能AI)应用架构设计的重要性。首先,我们将介绍架构设计原则,然后为我们的动手项目创建一个参考架构。在本章中,我们将使用我们的参考架构及其组成部分重新创建 Amazon Rekognition 演示。我们将学习如何使用多个 AWS 工具和服务,以无服务器的方式构建我们的动手项目,然后将其部署到 AWS 云中。

本章将涵盖以下主题:

  • 理解人工智能应用的成功因素

  • 理解 AI 应用的架构设计原则

  • 理解现代 AI 应用的架构

  • 创建定制的 AI 能力

  • 使用 AWS Chalice 在本地开发 AI 应用

  • 开发演示应用的网页用户界面

技术要求

本书的 GitHub 仓库,包含本章节的源代码,可以在 github.com/PacktPublishing/Hands-On-Artificial-Intelligence-on-Amazon-Web-Services 找到。

理解人工智能应用的成功因素

让我们来讨论一下是什么使得 AI 应用成功,实际上,任何软件应用成功的关键也类似。有两个主要因素决定应用的成功:

  • 第一个因素是应用是否能够真正解决某个特定问题。

  • 第二个因素是应用实现的质量,即如何有效地将解决方案交付给问题。

基本上,我们讨论的是构建什么如何构建这两个问题。要做到这两点都非常困难,在大多数情况下,成功的应用都需要这两个因素。

事实上,决定构建什么才是两者中更为重要的因素。如果我们在这一点上做错了,最终产品将是有缺陷的,无法提供问题的可行解决方案。不管架构有多么优雅,代码基础有多干净——一个有缺陷的产品最终都会失败。然而,决定构建什么往往不是一次性就能搞定的。这种信念是错误的——认为完美的解决方案可以提前设计出来。在许多情况下,你的目标客户甚至不知道他们需要什么或想要什么。成功的解决方案需要经过反复的产品开发迭代、客户反馈,并且需要大量的努力来完善产品需求。

这种需要反复迭代和实验的特性使得如何构建成为找到构建什么的一个重要因素。让一个应用程序工作并不需要大量技能。你总是可以通过坚定的决心和蛮力实现第一次迭代、第一次版本或第一次转型,应用程序就能工作。它可能不优雅,但至少能运行。然而,当第一次迭代不是问题的正确解决方案时,一个更优雅的架构和更干净的代码库将支持更快的迭代和转型,从而为你提供更多机会去找出构建什么

理解 AI 应用程序的架构设计原则

构建优雅的应用程序并非易事,但构建优雅的 AI 应用程序可能更加困难。作为在快速变化的技术环境中工作的 AI 从业者,理解良好的架构设计原则并且对软件工艺充满热情是至关重要的,因为构建和维护能够适应快速发展的 AI 技术的应用程序需要不断的纪律性。良好的架构设计能够轻松适应变化。然而,不可能预测所有未来的变化。因此,我们需要依赖一套广泛接受的设计原则来指导我们构建良好的应用程序架构。现在我们一起来看看这些原则:

  • 一个设计良好的应用程序应该建立在具有专注业务能力的小型服务之上。这里的“小型”并不一定意味着代码量少,而是小型服务应该遵循单一职责原则;即做好一件或非常少的事情。

  • 这些小型服务更容易实现、测试和部署。它们也更容易复用,并且能够组合出更多的业务能力。

  • 一个好的应用程序架构应该有明确的边界,以强制执行关注点分离。

  • 应用程序的服务和组件应该通过隐藏内部实现细节来保持这种分离。

  • 这种分离允许服务和组件在对应用程序其余部分的影响最小的情况下被替换,从而支持解决方案的更容易演化和改进。

如果你是软件架构设计的新手,好的设计和差的设计之间的区别可能看起来微妙。你需要大量的经验才能获得真正理解好设计所需的知识和技能。在本书中,我们将为你提供一些优雅设计的例子,它们是 AI 应用程序的良好起点。

理解现代 AI 应用程序的架构

定义清晰的架构设计是开发成功 AI 应用程序的必要步骤,我们推荐四个基本组成部分来构建它。

这四个组成部分如下:

  • 用户界面:这是面向用户的组件,向最终用户提供应用程序的业务能力:

    • 它们也被称为前端。用户界面的例子包括网站、移动应用、可穿戴设备和语音助手界面。

    • 相同的应用程序可以通过选择不同的设备形态、交互方式和用户界面,提供不同的定制化用户体验。

    • 如何在网页上提供智能能力,与在可穿戴设备上提供智能能力的方式会有很大的不同。

作为一名 AI 从业者,一个重要的技能是设计用户体验,将你的智能能力传递给用户。做好这一部分是你的 AI 应用成功的最重要因素之一。

  • 协调层:这些是公共 API,将被你的用户界面调用以提供业务能力:

    • 通常,这些 API 是通向后台的入口点。

    • 公共 API 应根据特定的界面和交互方式进行定制,以便为用户提供最佳体验。

    • 公共 API 将通过私有 API 调用一个或多个小型服务,以提供业务能力。

它们在组合多个底层能力以形成用户界面所需的更高层次能力中起到了协调作用。这些公共 API 还有其他名称,你可能听说过;比如,前端的后端(或BFFs)和体验 API。

  • 私有 API:私有 API 定义了交互契约,公共 API 使用这些契约来访问更底层的服务:

    • 私有 API 封装了服务实现,这些实现提供特定的能力,以隐藏其详细信息。

    • 这些 API 在软件系统的可组合性和可替换性特性中发挥着关键作用。

    • 私有 API 是可以被多个应用组合和重用的通用能力接口。

    • 这些私有 API 遵循面向服务设计模式。你可能对这种模式比较熟悉,比如微服务架构和面向服务架构(或SOA)。

    • 它们应该以单一职责原则为设计基础。

一组设计良好的私有 API 对任何组织来说都是宝贵的资产和竞争优势。组织将能够迅速创新、改进并将解决方案推向市场。

  • 供应商/自定义服务:这些是业务能力的实现,无论是 AI 还是其他:

    • 这些实现可以由供应商作为 Web 服务提供,或者托管在你的基础设施中。它们也可以是你所在组织构建的定制解决方案。

    • 这些服务有自己的 API,如 RESTful 端点或 SDK,私有 API 将通过它们来封装这些实现。

在本书中,我们将利用 Amazon 作为供应商,通过 boto3 SDK 提供许多 Web 服务。稍后在本书中,我们还将教你如何使用 AWS 的机器学习服务构建自定义的 AI 能力,并将其作为 ML 模型通过 RESTful 接口进行部署。

下图展示了这些基本架构组件和层次的组织结构:

干净架构的关键是通过每一层之间定义明确的交互契约,保持这些组件的分离:

  • 用户界面应该只知道编排层中的公共 API。

  • 公共 API 应该只知道它们依赖的私有 API。

  • 私有 API 应该只知道它们所封装的服务实现。

  • 这是信息隐藏的原则,它在架构层面得到应用。

在架构层面强制执行这些逻辑边界有许多好处,例如,如果我们想切换到更好的供应商服务,我们只需创建一组新的私有 API 来封装新的供应商服务,同时保持与公共 API 相同的私有 API 契约(然后淘汰旧的私有 API)。这样,公共 API 和用户界面就不会受到这一变化的影响。这将把变化的影响限制在应用程序的特定部分。

我们今天使用的大多数应用程序由前端和后端组成。前端通常运行在浏览器或移动设备上,而后端则运行在云端或私有数据中心的服务器基础设施上。这里推荐的架构是这些类型应用程序的良好起点。还有一些更为专业化的应用程序,如嵌入式系统,可能需要不同的架构设计。我们在本书中不会深入讨论这些更专业化应用程序的架构需求。

创建自定义的 AI 能力

作为 AI 从业者,我们可以参与的有两个不同的开发生命周期:

  • AI 应用开发生命周期

  • AI 能力开发生命周期

通常,特别是在角色更加专业化的大型组织中,AI 从业者仅参与其中一个生命周期。即使你不参与其中一个生命周期,了解两个生命周期的基本内容对所有 AI 从业者都是有益的。

AI 应用开发生命周期涉及解决方案的迭代、用户体验的设计、应用架构的定义,以及各种业务能力的集成。这类似于传统的软件开发生命周期,但目的是将智能嵌入到解决方案中。

AI 能力开发生命周期涉及使用数据和机器学习技术开发智能能力。在 AI 能力开发生命周期中创建的数据产品可以集成到应用程序中作为 AI 能力或 AI 服务。换句话说,AI 能力开发生命周期产生了定制的 AI 能力,而 AI 应用开发生命周期则消耗这些能力。

这两个生命周期需要不同的技术和问题解决技能。以下图表提供了创建 AI 能力所需步骤的概述:

AI 能力是 AI 应用的核心。正如我们在第一章中提到的,人工智能与亚马逊 Web 服务简介,数据是新的知识产权。任何成功的组织都应该拥有明确的数据战略,用于收集、存储、处理和传播数据。原始和处理过的数据集应该安全地存放并在数据存储系统中提供,如数据库、数据湖和数据仓库。从这些数据存储中,数据科学家可以访问数据以支持他们正在处理的特定业务问题或问题的分析。部分分析结果将产生有价值的商业洞察,具有执行预测分析的潜力。通过这些洞察,数据科学家可以选择各种机器学习算法来训练机器学习模型,以执行自动化预测和决策,包括分类和回归分析。

一旦经过训练和调整,机器学习模型就可以作为 AI 服务进行部署,并提供接口供应用程序访问其智能。例如,Amazon SageMaker 允许我们训练机器学习模型,并将其作为具有 RESTful 端点的 Web 服务进行部署。最后,作为数据战略的一部分,应该收集从已部署 AI 服务中获取的反馈数据,以改进未来版本的 AI 服务。

正如我们在前一章中提到的,我们强烈建议您尽可能多地利用 AWS 等供应商现有的 AI 服务来为您的智能应用提供支持。每个 AWS AI 能力都经历了多个 AI 能力开发生命周期的迭代,并且使用了大量大多数组织无法访问的数据。如果您拥有真正的数据知识产权或需求尚未被供应商解决,构建您自己的 AI 能力才是有意义的。训练一个可以投入生产的机器学习模型需要大量的努力、技能和时间。

本书的第二部分将重点讨论 AI 应用开发生命周期,而第三部分将重点讨论 AI 能力开发生命周期。

与实践中的 AI 应用架构合作

在前一节中,我们推荐了现代 AI 应用程序的架构设计。在本节中,我们将定义用于实现推荐架构设计的具体技术和技术栈。在决定本书最佳选择时,我们评估了多个因素,包括简易性、学习曲线、行业趋势等。请记住,推荐架构设计可能有许多有效的技术选择和实施方法。

对于动手实施的 AI 应用开发项目,我们将采用以下架构和技术栈:

正如前面的图表所示,AI 应用项目将由我们前面讨论过的四个基本架构组件组成:

  • 用户界面:我们将使用网页作为用户界面。我们将使用 HTML、CSS 和 JavaScript 开发相对简单的用户界面。HTML 和 CSS 将显示 UI 组件并处理用户输入。JavaScript 将通过编排层的公共 API 与服务器后端进行通信。项目网页将部署在 AWS S3 上作为静态网站,无需传统的 Web 服务器。这被称为无服务器,因为我们不需要管理和维护任何服务器基础设施。

我们使用普通的 HTML 和 JavaScript 来限制本书的范围。完成本书中的实际项目后,您应考虑使用 Angular、React 或 Vue 等单页 Web 应用程序框架来开发您的 Web 用户界面。

此外,AI 应用程序并不仅限于 Web 应用作为唯一选择。其他用户界面和模式,如移动设备或语音助手设备,有时可以提供更好的用户体验。我们建议您考虑如何修改应用设计以支持这些其他用户界面和模式。这些思想实验将帮助您为 AI 从业者构建设计能力。

  • 编排层:我们将使用 AWS Chalice,这是一个用于 AWS 的 Python 无服务器微框架。Chalice 允许我们在其本地开发环境中快速开发和测试 Python 应用程序,然后轻松地将 Python 应用程序部署到 Amazon API Gateway 和 AWS Lambda 作为高可用性和可扩展性的无服务器后端。Amazon API Gateway 是一个完全托管的服务,将托管我们的公共 API 作为 RESTful 端点。API Gateway 将向 AWS Lambda 函数转发发往我们的公共 API 的 RESTful 请求,在那里我们的编排逻辑将被部署。AWS Lambda 是一种无服务器技术,允许我们在不预配或管理服务器的情况下运行代码。例如,当从 API Gateway 调用 Lambda 函数时,代码将自动触发并在 AWS 基础设施上运行。您只需支付消耗的计算资源。

  • 私有 API:我们将把私有 API 打包为 Chalice 框架中的 Python 库。Chalice 允许我们通过将一些服务结构化为Chalicelib目录中的库,以模块化的方式编写代码。在我们的实际项目中,私有 API 是简单的 Python 类,具有明确定义的方法签名,以提供对服务实现的访问。在我们的项目中,公共 API 与私有 API 之间的边界是逻辑上的,而非物理上的;因此,必须注意确保架构层次的清晰性。

我们将在多个项目中重用一些私有 API。我们的重用机制类似于共享库。在较大的组织中,私有 API 通常作为 RESTful 端点进行部署,以便不同的应用程序可以轻松共享它们。

  • 供应商服务:我们将利用 AWS 提供的各种能力。例如,我们需要开发这些智能应用,包括 AI 能力等。私有 API 将通过boto3 SDK 访问云中的 AWS 服务。清晰的设计要求私有 API 将boto3和 AWS 实现细节完全封装并隐藏;公共 API 不应知道私有 API 使用了哪些供应商服务或自定义解决方案来提供这些能力。

对象检测器架构

我们将用我们自己的网页前端和 Python 后端重新创建Amazon Rekognition演示。首先,让我们理解一下我们即将开发的对象检测器应用程序的架构。

使用我们之前讨论的参考架构设计和技术栈,以下是对象检测器应用程序的架构:

用户将通过网页用户界面与对象检测器进行交互:

  • 我们将提供一个网页用户界面供用户使用,以便他们查看对象检测演示。

  • 网页用户界面将与包含唯一 RESTful 端点的协调层进行交互:Demo 对象检测端点。

  • 该端点与存储服务和识别服务进行交互,以执行对象检测演示。

  • 存储服务和识别服务分别通过Boto3 SDK 调用 Amazon S3 和 Amazon Rekognition 服务。

对象检测器的组件交互

让我们理解一下对象检测器应用程序中各个组件之间的交互:

从用户的角度看,应用程序加载一张随机图片,并显示已检测到的物体(或标签)。演示工作流如下:

  1. 对象检测器应用程序的网页界面调用 Demo 对象检测端点以启动演示。

  2. 该端点调用存储服务以获取存储在指定 S3 存储桶中的文件列表。

  3. 接收到文件列表后,端点会随机选择一个图像文件进行演示。

  4. 然后,端点调用识别服务对选定的图像文件执行对象检测。

  5. 接收对象标签后,端点将结果打包成 JSON 格式。

  6. 最终,网页界面会显示随机选择的图像及其检测结果。

创建基础项目结构

接下来,让我们创建实际操作的项目结构。按照以下步骤创建架构和技术栈:

  1. 在终端中,我们将创建根项目目录并使用以下命令进入该目录:
$ mkdir ObjectDetector
$ cd ObjectDetector
  1. 我们将通过创建一个名为Website的目录来为 Web 前端创建占位符。在此目录中,我们将有两个文件,index.htmlscripts.js,如下所示:
$ mkdir Website
$ touch Website/index.html
$ touch Website/scripts.js
  1. 我们将在项目的根目录中使用pipenv创建一个 Python 3 虚拟环境。我们项目的 Python 部分需要两个包,boto3chalice。我们可以使用以下命令安装它们:
$ pipenv --three
$ pipenv install boto3
$ pipenv install chalice
  1. 请记住,通过pipenv安装的 Python 包仅在我们激活虚拟环境时才可用。一种激活方法是使用以下命令:
$ pipenv shell
  1. 接下来,在仍处于虚拟环境中的情况下,我们将使用以下命令创建一个名为Capabilities的 AWS Chalice 项目作为编排层:
$ chalice new-project Capabilities

该命令将在ObjectDetector目录下创建一个 Chalice 项目结构。Chalice 项目结构应该类似于以下内容:

├── ObjectDetector/
 ├── Capabilities/
 ├── .chalice/
 ├── config.json
 ├── app.py
 ├── requirements.txt
...

在这个项目结构中,我们有以下内容:

  • config.json文件包含了将我们的 Chalice 应用程序部署到 AWS 的配置选项。

  • app.py文件是我们的主要 Python 文件,其中定义并实现了公共编排 API。

  • requirements.txt文件指定了应用程序部署到 AWS 时所需的 Python 包。这些包不同于我们使用 Pipenv 安装的包。Pipenv 安装的包是在本地开发环境中需要的;而requirements.txt文件中的包是应用程序在 AWS 云中运行时所需的。例如,AWS Chalice 在开发应用程序时是必需的,但一旦应用程序部署到 AWS 后,就不再需要它。

当我们在 AWS 云中运行项目时,boto3是必需的;不过,它已经在 AWS Lambda 运行时环境中提供,因此我们不需要在requirements.txt文件中显式指定它。但请记得在该文件中包含应用程序所需的其他 Python 包。

  1. 现在,我们需要在Capabilities目录下的 Chalice 项目结构中创建一个chalicelib Python 包。Chalice 会自动将chalicelib中的任何 Python 文件包含在部署包中。我们将使用chalicelib来存放实现私有 API 的 Python 类。

要创建chalicelib包,请执行以下命令:

cd Capabilities
mkdir chalicelib
touch chalicelib/__init__.py
cd ..

请注意,__init__.py使chalicelib成为一个合适的 Python 包。

我们应该有以下的项目目录结构:

Project Structure
------------
├── ObjectDetector/
 ├── Capabilities/
 ├── .chalice/
 ├── config.json
 ├── chalicelib/
 ├── __init__.py
 ├── app.py
 ├── requirements.txt
 ├── Website/
 ├── index.html
 ├── script.js
 ├── Pipfile
 ├── Pipfile.lock

这是ObjectDetector应用程序的项目结构。它包含了我们之前定义的 AI 应用架构的所有层次。这个项目结构也是本书第二部分中所有实践项目的基础结构。

使用 AWS Chalice 在本地开发 AI 应用

首先,让我们实现提供通用功能的私有 API 和服务。我们将有两个服务,它们都应在chalicelib目录中创建:

  1. StorageServiceStorageService类在storage_service.py文件中实现,通过boto3连接到 AWS S3,以便对应用程序所需的文件执行操作。

让我们实现StorageService,如下所示:

import boto3

class StorageService:
    def __init__(self, storage_location):
        self.client = boto3.client('s3')
        self.bucket_name = storage_location

    def get_storage_location(self):
        return self.bucket_name

    def list_files(self):
        response = self.client.list_objects_v2(Bucket = self.bucket_name)

        files = []
        for content in response['Contents']:
            files.append({
                'location': self.bucket_name,
                'file_name': content['Key'],
                'url': "http://" + self.bucket_name + ".s3.amazonaws.com/" + content['Key']
            })
        return files

在这个类中,目前有一个构造函数和两个方法:

    • __init__()构造函数接受一个参数,storage_location。在StorageService的这个实现中,storage_location表示文件存储的 S3 桶。然而,我们故意给这个参数起了一个通用的名称,这样不同的StorageService实现可以使用 AWS S3 以外的其他存储服务。

    • 第一个方法,get_storage_location(),只是返回 S3 桶的名称作为storage_location。其他服务实现将使用此方法来获取通用的存储位置。

    • 第二个方法,list_files(),从指定的storage_location的 S3 桶中检索文件列表。然后,这些文件作为 Python 对象的列表返回。每个对象描述一个文件,包括其位置、文件名和 URL。

请注意,我们也在使用更通用的术语来描述文件,如位置、文件名和 URL,而不是桶、键和 s3 URL。此外,我们返回的是一个包含自定义 JSON 格式的新 Python 列表,而不是返回来自boto3的可用响应。这可以防止 AWS 的实现细节泄露到这个私有 API 的实现中。

StorageService中的设计决策是为了隐藏实现细节,避免暴露给其客户端。因为我们隐藏了boto3和 S3 的细节,我们可以自由地修改StorageService,使其能够使用其他 SDK 或服务来实现文件存储功能。

  1. RecognitionServiceRecognitionService类在recognition_service.py文件中实现,通过boto3调用 Amazon Rekognition 服务,以执行图像和视频分析任务。

让我们实现RecognitionService,如下所示:

import boto3

class RecognitionService:
    def __init__(self, storage_service):
        self.client = boto3.client('rekognition')
        self.bucket_name = storage_service.get_storage_location()

    def detect_objects(self, file_name):
        response = self.client.detect_labels(
            Image = {
                'S3Object': {
                    'Bucket': self.bucket_name,
                    'Name': file_name
                }
            }
        )

        objects = []
        for label in response["Labels"]:
            objects.append({
                'label': label['Name'],
                'confidence': label['Confidence']
            })
        return objects

在这个类中,目前有一个构造函数和一个方法:

    • __init__() 构造函数接受 StorageService 作为依赖项来获取必要的文件。这允许将新的 StorageService 实现注入并供 RecognitionService 使用;即,只要新的 StorageService 实现遵循相同的 API 契约。这被称为依赖注入设计模式,它使软件组件更具模块化、可重用性和可读性。

    • detect_objects() 方法接受一个图像文件名,包括路径和名称部分,然后对指定的图像执行对象检测。此方法实现假设图像文件存储在 S3 桶中,并从 boto3 SDK 调用 Rekognition 的 detect_labels() 函数。当 boto3 返回标签时,该方法会构造一个新的 Python 列表,列表中的每一项描述了检测到的对象及其检测的置信度。

注意,从方法的签名(参数和返回值)来看,它并没有暴露出使用 S3 和 Rekognition 服务的事实。这与我们在 StorageService 中使用的信息隐藏实践相同。

RecognitionService 中,我们本可以使用构造函数中传递的 StorageService 来获取实际的图像文件并对其进行检测。但我们直接通过 detect_labels() 函数传递图像文件的桶和名称。这种后者的实现方式利用了 AWS S3 和 Amazon Rekognition 的良好集成。关键点是私有 API 的契约支持这两种实现,而我们的设计决策选择了后者。

  1. app.py:接下来,让我们实现专为图像识别 Web 应用程序量身定制的公共 API。我们只需要一个公共 API 来演示应用程序。它应该在 Chalice 项目结构中的 app.py 文件中实现。

用以下代码块替换 app.py 中的现有内容。让我们理解该类的组件:

    • demo_object_detection() 函数使用 StorageServiceRecognitionService 来执行其任务;因此,我们需要从 chalicelib 导入这些服务并创建这些服务的新实例。

    • storage_location 被初始化为 contents.aws.ai,该位置包含我们在上一章上传的图像文件。你应将 contents.aws.ai 替换为你自己的 S3 桶。

    • 该函数使用 @app.route('/demo-object-detection', cors = True) 注解。这是 Chalice 用于定义一个名为 /demo-object-detection 的 RESTful 端点的特殊结构:

      • Chalice 将此端点映射到 demo_object_detection() Python 函数。

      • 注解还将cors设置为 true,这通过在此端点的响应中添加特定的 HTTP 头部,启用了跨源资源共享CORS)。这些额外的 HTTP 头部会告诉浏览器允许在一个源(域名)下运行的网页应用程序访问不同源(包括不同的域名、协议或端口号)上的资源。让我们看一下下面类中的实现:

Chalice 的注解语法可能对 Flask 开发者来说很熟悉。AWS Chalice 从 Flask 框架中借鉴了很多设计和语法。

from chalice import Chalice
from chalicelib import storage_service
from chalicelib import recognition_service

import random

#####
# chalice app configuration
#####
app = Chalice(app_name='Capabilities')
app.debug = True

#####
# services initialization
#####
storage_location = 'contents.aws.ai'
storage_service = storage_service.StorageService(storage_location)
recognition_service = recognition_service.RecognitionService(storage_service)

@app.route('/demo-object-detection', cors = True)
def demo_object_detection():
    """randomly selects one image to demo object detection"""
    files = storage_service.list_files()
    images = [file for file in files if file['file_name'].endswith(".jpg")]
    image = random.choice(images)

    objects = recognition_service.detect_objects(image['file_name'])

    return {
        'imageName': image['file_name'],
        'imageUrl': image['url'],
        'objects': objects
    }

让我们详细讨论一下前面的代码:

    • demo_object_detection()函数从StorageService获取一个包含图像文件(扩展名为.jpg的文件)列表,并随机选择其中一个进行目标检测演示。

    • 随机选择在这里实现,以简化我们的演示应用程序,使其仅显示一张图像及其检测结果。

    • 一旦图像被随机选择,函数会调用RecognitionService中的detect_objects(),然后生成一个JavaScript 对象表示法JSON)格式的 HTTP 响应。

    • Chalice 会自动将响应对象包装在适当的 HTTP 头部、响应代码和 JSON 负载中。JSON 格式是我们前端与这个公共 API 之间的合同的一部分。

我们准备好在本地运行并测试应用程序的后端了。Chalice 提供了一个本地模式,其中包括一个本地 HTTP 服务器,您可以使用它来测试端点。

  1. pipenv虚拟环境中,通过以下命令启动chalice local模式:
$ cd Capabilities
$ chalice local
Restarting local dev server.
Found credentials in shared credentials file: ~/.aws/credentials
Serving on http://127.0.0.1:8000

现在,本地 HTTP 服务器已在终端输出的地址和端口号上运行,也就是http://127.0.0.1:8000。请记住,尽管我们在本地运行端点,但端点调用的服务通过boto3 SDK 向 AWS 发出请求。

Chalice 的本地模式自动检测到了~/.aws/credentials文件中的 AWS 凭证。我们的服务实现会使用boto3中找到的密钥对,并使用相应用户的权限发出请求。如果该用户没有 S3 或 Rekognition 的权限,向端点发出的请求将会失败。

  1. 现在,我们可以向本地服务器发出 HTTP 请求,以测试/demo-object-detection端点。例如,您可以使用 Unix 的curl命令,如下所示:
$ curl http://127.0.0.1:8000/demo-object-detection
{"imageName":"beagle_on_gravel.jpg","imageUrl":"https://contents.aws.ai.s3.amazonaws.com/beagle_on_gravel.jpg","objects":[{"label":"Pet","confidence":98.9777603149414},{"label":"Hound","confidence":98.9777603149414},{"label":"Canine","confidence":98.9777603149414},{"label":"Animal","confidence":98.9777603149414},{"label":"Dog","confidence":98.9777603149414},{"label":"Mammal","confidence":98.9777603149414},{"label":"Beagle","confidence":98.0347900390625},{"label":"Road","confidence":82.47952270507812},{"label":"Dirt Road","confidence":74.52912902832031},{"label":"Gravel","confidence":74.52912902832031}]}

请注意,在这段代码中,我们只是将端点的 URL 路径附加到本地 HTTP 服务器运行的基本地址和端口号上。请求应该返回来自本地端点的 JSON 输出。

这是我们的网页用户界面将接收并用于向用户展示检测结果的 JSON 格式数据。

开发演示应用程序的网页用户界面

接下来,我们将在网站目录中的index.htmlscript.js文件中使用 HTML 和 JavaScript 创建一个简单的网页用户界面。

请参阅 index.html 文件中的代码,如下所示:

<!doctype html>
<html lang="en"/>

<head>
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>

    <title>Object Detector</title>

    <link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
    <link rel="stylesheet" href="https://www.w3schools.com/lib/w3-theme-blue-grey.css">
</head>

<body class="w3-theme-14" onload="runDemo()">
    <div style="min-width:400px">
        <div class="w3-bar w3-large w3-theme-d4">
            <span class="w3-bar-item">Object Detector</span>
        </div>

        <div class="w3-container w3-content">
            <p class="w3-opacity"><b>Randomly Selected Demo Image</b></p>
            <div class="w3-panel w3-white w3-card w3-display-container"
                style="overflow: hidden">
                <div style="float: left;">
                    <img id="image" width="600"/>
                </div>
                <div id="objects" style="float: right;">
                    <h5>Objects Detected:</h5>
                </div>
            </div>
        </div>
    </div>

    <script src="img/scripts.js"></script>
</body>

</html>

我们在这里使用标准的 HTML 标签,因此,任何熟悉 HTML 的人都应该能轻松跟随网页的代码。以下是几个值得注意的事项:

  • 我们从 www.w3schools.com 引入了几种 层叠样式表 (CSS) 来使我们的网页界面比简单的 HTML 更加美观。HTML 标签中的大多数类都是在这些样式表中定义的。

  • 带有 image ID 的 <img> 标签将用于显示随机选择的演示图片。JavaScript 会使用这个 ID 来动态添加图片。

  • 带有 objects ID 的 <div> 标签将用于显示在演示图片中检测到的对象。这个 ID 也将被 JavaScript 用来动态添加对象标签和置信度。

  • scripts.js 文件位于 HTML 文件的底部。这为该 HTML 页面添加了通过 JavaScript 实现的动态行为。

  • 当 HTML 页面在浏览器中加载时,scripts.js 中的 runDemo() 函数会被调用。这是在 index.html 页面 <body> 标签中的 onload 属性实现的。

请参阅以下 scripts.js 文件的代码:

"use strict";

const serverUrl = "http://127.0.0.1:8000";

function runDemo() {
    fetch(serverUrl + "/demo-object-detection", {
        method: "GET"
    }).then(response => {
        if (!response.ok) {
            throw response;
        }
        return response.json();
    }).then(data => {
        let imageElem = document.getElementById("image");
        imageElem.src = data.imageUrl;
        imageElem.alt = data.imageName;

        let objectsElem = document.getElementById("objects");
        let objects = data.objects;
        for (let i = 0; i < objects.length; i++) {
            let labelElem = document.createElement("h6");
            labelElem.appendChild(document.createTextNode(
                objects[i].label + ": " + objects[i].confidence + "%")
            );
            objectsElem.appendChild(document.createElement("hr"));
            objectsElem.appendChild(labelElem);
        }
    }).catch(error => {
        alert("Error: " + error);
    });
}

让我们详细讨论一下之前的代码:

  • 脚本只有一个函数,runDemo()。这个函数通过 JavaScript 中可用的 Fetch API,向本地 HTTP 服务器上运行的 /demo-object-detection 端点发送 HTTP GET 请求。

  • 如果本地端点的响应为 ok,则它会将负载转换为 JSON 对象,并将其传递到下一个处理块。

  • 然后,runDemo() 函数会查找具有 image ID 的 HTML 元素,也就是 HTML 中的 <img> 标签,并将 src 属性指定为端点返回的 imageUrl。记住,这个 imageUrl 是指向存储在 S3 中的图片文件的 URL。<img> 标签的 alt 属性设置为 imageName。如果图片因某种原因无法加载,imageName 将显示给用户。

  • 请注意,S3 中的图片必须设置为公共可读,网站才能显示该图片。如果你只看到 alt 文本,请重新检查图片是否对公众可读。

  • 然后,runDemo() 函数会查找具有 objects ID 的 HTML 元素,也就是一个 <div> 标签,并为本地端点返回的每个对象附加一个 <h6> 标题元素,其中包括每个对象的标签和检测置信度。

现在,我们准备好查看这个网站的实际效果了。要在本地运行网站,只需在浏览器中打开 index.html 文件即可。你应该会看到一个类似以下截图的网页:

上传几个 JPEG 图像文件并刷新页面几次,看看对象检测演示如何运行;每次运行时,演示将选择存储在 S3 桶中的不同图像。ObjectDetector 应用程序虽然不如 Amazon Rekognition 演示那样花哨,但为自己创造了一个架构良好的 AI 应用程序而自豪吧!

本地 HTTP 服务器将持续运行,除非你明确停止它。要停止本地 HTTP 服务器,请进入正在运行 chalice local 的终端窗口,并按下 Ctrl + C

ObjectDetector 应用程序的最终项目结构应如下所示:

Project Organization
------------
├── ObjectDetector/
    ├── Capabilities/
        ├── .chalice/
            ├── config.json
        ├── chalicelib/
            ├── __init__.py
            ├── recognition_service.py
            ├── storage_service.py
        ├── app.py
        ├── requirements.txt
    ├── Website/
        ├── index.html
        ├── script.js
    ├── Pipfile
    ├── Pipfile.lock

现在是时候将我们的 AI 应用程序公开并部署到 AWS 云上了。

将 AI 应用程序后端通过 Chalice 部署到 AWS

使用 Chalice 部署到 AWS 非常简单而强大。Chalice 会自动将 app.py 中的端点注解转换为 HTTP 端点,并将它们作为公共 API 部署到 Amazon API Gateway。Chalice 还将 app.pychalicelib 中的 Python 代码部署为 AWS Lambda 函数,然后将 API 网关端点作为触发器连接到这些 Lambda 函数。这种简易性就是我们选择 AWS Chalice 作为无服务器框架来开发实际项目的原因。

当我们在本地运行后端时,Chalice 会自动检测到我们开发环境中的 AWS 凭证,并将其提供给应用程序。那么,当应用程序在 AWS 中运行时,它将使用哪些凭证呢?Chalice 在部署过程中会自动为应用程序创建一个 AWS IAM 角色。然后,应用程序将以已授予该角色的权限运行。Chalice 可以自动检测所需的权限,但此功能在本文撰写时仍被视为实验性,并且与我们项目的结构不兼容。对于我们的项目,我们需要告诉 Chalice 不要 为我们执行此分析,通过在项目结构的 .chalice 目录中的 config.json 文件中将 autogen_policy 设置为 false 来实现。以下是 config.json 文件:

{
    "version": "2.0",
    "app_name": "Capabilities",
    "stages": {
        "dev": {
            "autogen_policy": false,
            "api_gateway_stage": "api"
        }
    }
}

请注意,在此配置中,config.json 中有一个 dev 阶段。Chalice 提供了将应用程序部署到多个环境的能力。不同的环境被成熟的软件组织用来以隔离的方式执行各种软件生命周期任务,如测试和维护。例如,我们有用于快速实验的开发(dev)环境,质量保证(qa)用于集成测试,用户验收测试(uat)用于业务需求验证,性能(prof)用于压力测试,产品(prod)用于来自最终用户的实时流量。

接下来,我们需要在 .chalice 目录中创建一个新文件 policy-dev.json,手动指定项目所需的 AWS 服务:

{
    "Version": "2012-10-17",
        "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents",
                "s3:*",
                "rekognition:*"
            ],
            "Resource": "*"
        }
    ]
}

在这里,我们指定了 S3 和 Rekognition,此外还包括一些权限,以允许项目将日志推送到 CloudWatch。

现在,我们准备好在 AWS Chalice 框架上部署后端了:

  1. Capabilities 目录中运行以下命令:
$ chalice deploy
Creating deployment package.
Creating IAM role: Capabilities-dev
Creating lambda function: Capabilities-dev
Creating Rest API
Resources deployed:
 - Lambda ARN: arn:aws:lambda:us-east-1:<UID>:function:Capabilities-dev
 - Rest API URL: https://<UID>.execute-api.us-east-1.amazonaws.com/api/

部署完成后,Chalice 会在输出中显示一个类似 https://<UID>.execute-api.us-east-1.amazonaws.com/api/ 的 RESTful API URL,其中 <UID> 是一个唯一标识符字符串。这是您的前端应用程序应该访问的服务器 URL,以连接到在 AWS 上运行的应用程序后端。

现在,您可以在 AWS 管理控制台的三个服务中验证 Chalice 部署的结果:

  • Amazon API Gateway

  • AWS Lambda

  • 身份和访问管理

查看这些服务的控制台页面,看看 AWS Chalice 为我们的应用程序设置了什么。

  1. 使用 curl 命令测试远程端点,命令如下。您应该会得到类似于我们在测试本地端点时的输出:
$ curl https://<UID>.execute-api.us-east-1.amazonaws.com/api/demo-object-detection
{"imageName":"beagle_on_gravel.jpg","imageUrl":"https://contents.aws.ai.s3.amazonaws.com/beagle_on_gravel.jpg","objects":[{"label":"Pet","confidence":98.9777603149414},{"label":"Hound","confidence":98.9777603149414},{"label":"Canine","confidence":98.9777603149414},{"label":"Animal","confidence":98.9777603149414},{"label":"Dog","confidence":98.9777603149414},{"label":"Mammal","confidence":98.9777603149414},{"label":"Beagle","confidence":98.0347900390625},{"label":"Road","confidence":82.47952270507812},{"label":"Dirt Road","confidence":74.52912902832031},{"label":"Gravel","confidence":74.52912902832031}]}

恭喜!您刚刚部署了一个无服务器的 AI 应用后端,它具有高可用性和可扩展性,并运行在云端。

通过 AWS S3 部署静态网站

接下来,让我们将前端网页用户界面部署到 AWS S3。

我们在上一章节中创建的一个桶是用于网站托管的。让我们通过 AWS 管理控制台为其配置静态网站托管:

  1. 导航到管理控制台中的 Amazon S3 服务并点击您的桶。

  2. Properties 选项卡中,如下图所示,点击 Static website hosting 卡片:

  1. 当您点击 Static website hosting 卡片时,会弹出一个配置卡片。

  2. 选择 Use this bucket to host a website,并分别在 Index documentError document 字段中输入 index.htmlerror.html

  3. 复制配置页面上的 Endpoint URL,然后点击 Save。这个端点 URL 将是您的静态网站的公共地址:

  1. 接下来,我们可以将 index.htmlscripts.js 文件上传到这个 S3 桶。在上传之前,我们需要对 scripts.js 进行更改。请记住,网站现在将运行在云中,无法访问我们的本地 HTTP 服务器。

  2. scripts.js 文件中的本地服务器 URL 替换为我们后端部署的 URL,如下所示:

"use strict";

const serverUrl = "https://<UID>.execute-api.us-east-1.amazonaws.com/api";
...
  1. 最后,将 index.htmlscripts.js 文件的权限设置为公开可读。为此,我们需要在 Permissions 选项卡下修改 S3 桶的权限。

  2. 点击 Public access settings 按钮,取消选中所有四个复选框,然后输入 confirm 以确认这些更改。这将允许将此 S3 桶的内容公开访问,如下所示:

  1. 现在,我们可以通过选择这两个文件,点击 Actions,然后点击 Make public,将文件设置为公开,如下图所示:

在浏览器中打开 S3 端点 URL。该 URL 应该类似于 http://<BUCKET>.s3-website-us-east-1.amazonaws.com/

你的 ObjectDetector 网站现在已经在浏览器中运行,并且它正在与运行在 AWS 上的后端进行通信,以展示你的智能应用功能。前端和后端都是无服务器架构,且都运行在 AWS 云基础设施上,能够根据需求自动扩展。

恭喜你!你刚刚将一个端到端的 AI 应用开发并部署到 AWS 上!现在,你可以与任何拥有浏览器的人分享这个 AI 应用。

即使你新的 AWS 账户可能拥有免费层服务,你仍然应该限制与你共享网站 URL 和 API 端点的人员数量。如果所消耗的 AWS 资源超出免费层计划覆盖的额度,你将会被收费。

总结

在本章中,我们讨论了良好的架构设计对人工智能应用的重要性。我们为 Web 应用创建了一个参考架构设计,该设计将作为本书第二部分所有实践项目的模板。利用这个参考架构,我们通过 AWS 的多个工具和服务,以无服务器架构的方式重建了 Amazon Rekognition 演示应用。我们使用 AWS Chalice 和 boto3 构建了演示应用的后端,并利用 AWS S3 和 Amazon Rekognition 提供了业务功能。通过这个实践项目,我们向你展示了架构边界和良好设计如何促进灵活的应用开发与演化。我们还用 HTML、CSS 和 JavaScript 为演示应用构建了一个简单的 Web 用户界面。最后,我们将演示应用作为无服务器应用部署到 AWS 云上。

现在我们已经有了构建一个简单而优雅的智能应用的经验,准备好在本书第二部分使用相同的架构模板和工具集来构建更多的人工智能应用。

深入阅读

你可以参考以下链接获取有关现代 AI 应用架构的更多信息:

第二部分:使用 AWS AI 服务构建应用程序

在本节中,AWS 提供了大量的托管 AI 服务,应用程序可以利用这些服务,而无需花费精力收集和清理训练数据、训练和调整自定义模型,以及托管和维护训练好的模型。AWS 已经在这些 AI 服务中完成了繁重的工作。你将能够快速设计、开发和部署生产就绪的 AI 解决方案,并以快速的速度将它们推向市场。我们不仅会为你提供这些服务的深入理解,还会教你如何将它们最佳地集成到 AI 解决方案中,以实现实验速度、架构灵活性和长期维护的最佳效果。

本节包括以下章节:

  • 第三章,使用 Amazon Rekognition 和 Amazon Translate 进行文本检测和翻译

  • 第四章,使用 Amazon Transcribe 和 Amazon Polly 进行语音转文本及反向操作

  • 第五章,使用 Amazon Comprehend 从文本中提取信息

  • 第六章,使用 AWS Lex 构建语音聊天机器人

第三章:使用 Amazon Rekognition 和 Translate 检测与翻译文本

在本章中,我们将构建第一个人工智能AI)应用程序,它解决一个现实问题,而不是一个理论展示。我们将构建一个可以翻译出现在图片中的外文文本的应用程序。我们将通过结合两个 AWS AI 服务——Amazon Rekognition 和 Amazon Translate 来实现这一目标。该应用程序将使用上一章介绍的参考架构。在这个实际项目中,我们不仅会为当前应用程序构建智能功能,还会将它们设计为可重用的组件,以便在未来的实际项目中利用。

我们将涵盖以下主题:

  • 使用 Amazon Rekognition 检测图像中的文本

  • 使用 Amazon Translate 进行文本翻译

  • 将智能功能嵌入应用程序

  • 使用 AWS 服务、RESTful API 和 Web 用户界面构建无服务器 AI 应用程序

  • 讨论良好的设计实践,并将智能功能构建为可重用组件

让世界更小

在本书的这一部分,我们将通过实际项目开始构建智能化解决方案。这些项目不仅能让你熟悉亚马逊的 AI 服务,还将帮助你加强如何将智能功能嵌入应用程序,解决实际问题的直觉。我们将从一个可以让世界更小的应用程序开始。

当谷歌在其 Google Lens 移动应用中发布新功能时,用户只需将手机指向周围的物体,就能获取更多信息。Google Lens 本质上将搜索功能带入了现实世界。这个应用的一个特别用例是实时语言翻译文本。用户可以将相机对准街头标牌或餐厅菜单,并将翻译结果以增强现实摄像头的形式呈现在手机屏幕上。仅凭这一功能,就能让世界变得更加可接近。

我们将在实际项目中实现这一图像翻译功能,使用的是 AWS AI 服务。我们的应用程序,暂时称之为图像翻译器,将提供类似的翻译功能,尽管其用户界面比谷歌镜头简洁得多。

理解图像翻译器的架构

根据第二章《现代 AI 应用程序的结构》定义的架构模板,以下是图像翻译器的架构设计:

我们将为用户提供一个 Web 用户界面,用户可以上传包含外文文本的照片,然后查看该外文文本的翻译。Web 用户界面将与包含两个 RESTful 端点的编排层进行交互,用于处理图片上传和翻译:

  • 上传图片端点将把图片上传委托给我们的存储服务

    • 存储服务AWS S3提供了一个抽象层,上传的照片将存储在其中,进行处理并从中显示。
  • 图像文本翻译端点将把照片中文本的检测委托给我们的识别服务,并将检测到的文本翻译交给我们的翻译服务

    • 识别服务为亚马逊 Rekognition 服务提供了一个抽象层,更具体地说,是 Rekognition 的文本检测功能。我们将我们的服务命名为识别,它更为通用,并且不会直接与AWS Rekognition绑定。

    • 翻译服务为亚马逊 Translate 服务提供了一个抽象层,用于执行语言翻译。

服务实现对一些读者来说可能显得有些多余。为什么不直接让端点与 AWS 服务通信,而是通过另一个抽象层进行通信呢?这种架构方式有许多好处。以下是一些示例:

  • 在开发阶段,我们可以更轻松地构建和测试应用,而无需依赖 AWS 服务。在开发过程中,任何这些服务的存根或模拟实现都可以用于提高速度、降低成本和进行实验。这使我们能够更快地开发和迭代应用。

  • 当其他提供更好存储、识别或翻译能力的服务出现时,我们的应用可以通过切换到具有相同抽象接口的新服务实现来使用这些能力。用户界面和端点无需修改即可利用这些更好的功能。这为我们的应用提供了更多的灵活性,以适应变化。

  • 这使我们的代码库更具可组合性和可重用性。这些 AWS 服务提供的能力可以被其他应用重用。这些服务是模块化的包,比起协调端点,它们更容易重用。协调端点通常包含特定于应用的业务逻辑,限制了重用性。

《图像翻译器组件交互》

在我们深入实现之前,思考应用各个组件如何相互交互,以及我们的设计选择如何影响用户体验是非常重要的:

从用户的角度来看,应用提供了一个顺序的体验,包括上传图像、查看已上传图像和查看翻译文本。我们做出了设计决策,确保用户等待每张照片上传并处理完毕(而不是一次性批量上传多张照片)。考虑到我们的使用案例假设用户在物理位置等待翻译结果,以便做出决策或采取行动,这个设计决策对于我们的应用来说是合理的。

我们的 Pictorial Translator 应用与上传图像端点和StorageService的交互是直接的。用户的请求本质上会通过链条传递到 AWS S3 并返回。当然,存储能力由 AWS S3 提供,这一点在端点和应用程序之间通过抽象层进行了屏蔽。照片将存储在 S3 桶中,文本检测和翻译将在 S3 桶中执行。

翻译图像文本的端点简化了 Pictorial Translator 应用程序中的一些业务逻辑。Pictorial Translator 只是将图像 ID 发送到翻译图像文本的端点,然后接收图像中每一行文本的翻译。该端点在后台执行了几项操作。这个端点调用了RecognitionService中的detect_text(),对整个图像进行处理,然后多次调用翻译服务中的translate_text(),对检测到的每一行文本进行翻译。只有当检测到的文本行达到最低置信度阈值时,端点才会调用翻译服务。

在这里,我们做出了两个设计决策:

  • 首先,我们在行级别进行文本翻译。这个想法是,现实世界中的文本并不总是处于相同的上下文中(例如,同一张照片中的多个街道标志),甚至可能不在同一种语言中。这个设计决策在现实世界中的结果需要被密切监控,以验证其用户体验。

  • 其次,我们只翻译RecognitionService非常有信心的文本行。现实世界是复杂的,用户可能会上传包含与翻译任务无关的文本的照片(例如,远处的街道标志),或者上传不适合进行高质量文本检测的照片(例如,光线较差或焦距不清晰的照片)。我们不希望给用户带来不准确的翻译,因此我们的应用程序采取了仅翻译照片中高质量文本的做法。

这些是 AI 从业人员在开发智能应用程序时应该评估和验证的设计决策示例。拥有灵活的架构可以让你更快地进行迭代。

设置项目结构

我们将创建一个类似的基础项目结构,按照第二章中概述的步骤进行,现代 AI 应用的结构,包括pipenvchalice和网页文件:

  1. 在终端中,我们将创建root项目目录并输入以下命令:
$ mkdir PictorialTranslator
$ cd PictorialTranslator
  1. 我们将通过创建一个名为Website的目录来为网页前端创建占位符,并在该目录下创建两个文件index.htmlscripts.js
$ mkdir Website
$ touch Website/index.html
$ touch Website/scripts.js
  1. 我们将在项目的 root 目录下使用 pipenv 创建一个 Python 3 虚拟环境。项目的 Python 部分需要两个包,boto3chalice。我们可以通过以下命令安装它们:
$ pipenv --three
$ pipenv install boto3
$ pipenv install chalice
  1. 请记住,通过 pipenv 安装的 Python 包只有在激活虚拟环境时才能使用。激活虚拟环境的一种方法是使用以下命令:
$ pipenv shell
  1. 接下来,在仍处于虚拟环境中时,我们将创建一个名为 Capabilities 的 AWS chalice 项目,并使用以下命令:
$ chalice new-project Capabilities
  1. 要创建 chalicelib Python 包,请执行以下命令:
cd Capabilities
mkdir chalicelib
touch chalicelib/__init__.py
cd ..

Pictorial Translator 项目的结构应如下所示:

Project Structure
------------
├── PictorialTranslator/
    ├── Capabilities/
        ├── .chalice/
            ├── config.json
        ├── chalicelib/
            ├── __init__.py
        ├── app.py
        ├── requirements.txt
    ├── Website/
        ├── index.html
        ├── script.js
    ├── Pipfile
    ├── Pipfile.lock

这是 Pictorial Translator 项目的结构。它包含了我们在 第二章 中定义的 AI 应用架构的用户界面、编排和服务实现层,现代 AI 应用的结构

实现服务

现在我们知道要构建什么,让我们逐层实现这个应用程序,从服务实现开始。

识别服务 – 文本检测

我们将利用 Amazon Rekognition 服务提供图像文本检测的功能。首先,让我们使用 AWS CLI 进行此功能的测试。我们将使用一张德国街道标志的照片:

上述照片的来源是 www.freeimages.com/photo/german-one-way-street-sign-3-1446112

由于我们将使用 S3 存储照片,首先让我们将这张照片上传到 第一章 中创建的 S3 桶中,Amazon Web Services 上的人工智能简介。例如,我们将图片上传到 contents.aws.ai 桶。一旦上传,要使用 AWS CLI 对名为 german_street_sign.jpg 的照片执行文本检测,执行以下命令:

$ aws rekognition detect-text --image S3Object=\{Bucket=contents.aws.ai,Name=german_street_sign.jpg\}
{
    "TextDetections": [
        {
            "DetectedText": "Einbahnstrabe",
            "Type": "LINE",
            "Id": 0,
            "Confidence": 99.16583251953125,
            "Geometry": {
                "BoundingBox": {
                    "Width": 0.495918333530426,
                    "Height": 0.06301824748516083,
                    "Left": 0.3853428065776825,
                    "Top": 0.4955403208732605
                },
                "Polygon": [
                    ...
                ]
            }
        },
        {
            "DetectedText": "Einbahnstrabe",
            "Type": "WORD",
            "Id": 1,
            "ParentId": 0,
            ...
        }
    ]
}

AWS CLI 是一个便捷的工具,用于检查 AWS 服务的输出格式:

  • 在这里,我们看到了来自文本检测的 JSON 输出,部分输出在此为了简洁而被截断。

  • 在最顶层,我们有一个由花括号 {} 包围的对象。在这个顶层对象中,我们有一个名称-值对,名称为 TextDetections,值是一个由方括号 [] 包围的数组。

  • 在这个数组中,有零个或多个描述检测到的文本的对象。查看数组中的检测文本对象时,我们可以看到诸如 DetectedTextTypeIdConfidenceGeometry 等信息。

在我们的照片中,只有一个单词。然而,Rekognition 在TextDetections数组中返回了两个对象。这是因为 Rekognition 返回了两种类型的DetectedText对象,分别是文本的LINE(行)和该行中的所有WORD(单词)对象。我们返回的两个对象分别表示LINE和该行中的单个WORD。注意这两个对象的类型不同,第二个对象(WORD)的 ParentId 引用了第一个对象(LINE)的 Id,显示了行与单词之间的父子关系。

我们还可以看到文本检测的Confidence级别,稍后我们将使用它来筛选哪些文本行需要翻译。Rekognition 对单词Einbahnstrabe非常有信心,其Confidence得分为99.16583251953125,最高分为 100。

Geometry名称/值对包含两个系统,用于描述检测到的文本在图像中的位置:

前面的图表解释了以下内容:

  • BoundingBox描述了一个粗略的矩形,表示文本所在的位置。该系统通过矩形左上角的坐标以及矩形的宽度和高度来描述BoundingBox

  • 这些坐标和测量值都以图像的比例表示。例如,如果图像的尺寸是 700 x 200 像素,并且服务返回了 left == 0.5 和 top == 0.25,那么矩形的左上角坐标为像素(350,50);700 x 0.5 = 350,200 x 0.25 = 50。

  • Polygon描述了BoundingBox内的一组点,形成一个精细的多边形,围绕检测到的文本。每个点的x和 y 坐标也使用与BoundingBox坐标相同的比例系统。

Geometry中提供的信息对于执行如在图像中突出显示文本或在图像上叠加其他信息等任务非常有用。

Rekognition 文本检测在基于字母的语言(如英语、德语和法语)中表现良好,但在基于字符的语言(如中文、韩文和日文)中效果较差。这无疑限制了应用程序的使用场景。

通过这些对文本检测输出的洞察,让我们实现我们的RecognitionService。让我们创建一个名为RecognitionService的 Python 类,如以下位于chalicelib目录中的recognition_service.py文件所示:

import boto3

class RecognitionService:
    def __init__(self, storage_service):
        self.client = boto3.client('rekognition')
        self.bucket_name = storage_service.get_storage_location()

    def detect_text(self, file_name):
        response = self.client.detect_text(
            Image = {
                'S3Object': {
                    'Bucket': self.bucket_name,
                    'Name': file_name
                }
            }
        )
        lines = []
        for detection in response['TextDetections']:
            if detection['Type'] == 'LINE':
                lines.append({
                    'text': detection['DetectedText'],
                    'confidence': detection['Confidence'],
                    'boundingBox': detection['Geometry']['BoundingBox']
                })

        return lines

在前面的代码中,以下内容适用:

  • 构造函数__init__()为 Rekognition 服务创建一个boto3客户端。构造函数还接受一个storage_location参数,作为我们实现中的 S3 桶名称。

  • detect_text()方法调用boto3 Rekognition 客户端的detect_text()函数,并传入 S3 桶名称和图像的文件键。然后,detect_text()方法处理TextDetections数组中的输出:

    • 在这里,我们只保留了 LINE 检测到的文本类型,对于每一行,我们存储 DetectedText、Confidence 对象以及 BoundingBox 坐标。

    • 任何使用 detect_text() 方法的 RecognitionService 客户端都将期望返回这些信息,作为一个包含字典(键值对映射)的 Python 列表,其中包含 textconfidenceboundingBox

在这里,我们将 AWS SDK 的输入和输出格式适配为我们自己的 RecognitionService 接口契约。我们应用程序的其余部分将期望我们的 RecognitionService 方法参数和返回类型。我们本质上实现了适配器设计模式。即使我们将 AWS Rekognition 服务替换为其他服务,只要我们将新服务适配到我们的接口契约,我们的应用程序仍然可以与新服务交互,无需进一步修改。

有两种方式可以指定用于文本检测的图像:

  • 一种方式是提供带有存储桶名称和对象键的 S3Object

  • 另一种方式是提供图像的原始位数据。

对于我们的应用程序,S3Object 方式效果更好。

翻译服务 – 翻译文本

我们将利用 Amazon Translate 服务提供语言翻译功能。再一次,让我们先通过 AWS CLI 进行一次测试体验。为了快速翻译,让我们复制上一节中检测到的文本 Einbahnstrabe,并执行以下命令:

$ aws translate translate-text --text "Einbahnstrabe" --source-language-code auto --target-language-code en
{
    "TranslatedText": "One way",
    "SourceLanguageCode": "de",
    "TargetLanguageCode": "en"
}

我们使用了 auto 作为源语言,这意味着 Amazon Translate 会自动检测文本的语言。对于目标语言,我们选择了 en 表示英语。

Amazon Translate 服务的输出非常简单,它只是一个包含三个名称/值对的 JSON 对象。如我们所见,Amazon Translate 正确地判断出 Einbahnstrabe 是一个德语单词,它的英文翻译是 One way。这一定是一个 One Way 交通标志的照片。

对于源语言,auto 值非常方便。然而,存在一些情况,源语言无法以很高的置信度被确定。在这种情况下,AWS 会抛出 DetectedLanguageLowConfidenceException 异常。这个异常会包含最可能的源语言。如果您的应用程序可以容忍这种低置信度,您可以再次发起翻译请求,并在异常中指定源语言。

Amazon Translate 支持多种语言之间的翻译,并且语言对的数量还在不断增加。然而,在撰写本书时,仍然存在一些不支持的语言对。请查看 AWS 文档中支持的语言对列表(docs.aws.amazon.com/translate/latest/dg/pairs.html)以获取最新信息。如果发出翻译一个不受支持的语言对的请求,AWS 将抛出 UnsupportedLanguagePairException 异常。

让我们创建一个名为TranslationService的 Python 类,如下所示,文件位于chalicelib目录中的translation_service.py文件中:

import boto3

class TranslationService:
    def __init__(self):
        self.client = boto3.client('translate')

    def translate_text(self, text, source_language = 'auto', target_language = 'en'):
        response = self.client.translate_text(
            Text = text,
            SourceLanguageCode = source_language,
            TargetLanguageCode = target_language
        )

        translation = {
            'translatedText': response['TranslatedText'],
            'sourceLanguage': response['SourceLanguageCode'],
            'targetLanguage': response['TargetLanguageCode']
        }

        return translation

在前面的代码中,以下内容适用:

  • 构造函数__init__()创建一个boto3客户端,或者被发送到翻译服务。

  • translate_text()方法调用boto3翻译客户端的translate_text()函数,并传入文本、源语言和目标语言。此方法的source_languagetarget_language参数默认值分别为autoen

  • translate_text()函数随后处理来自 AWS SDK 的输出,并将其返回为一个包含translatedTextsourceLanguagetargetLanguage键的 Python 字典。我们再次调整了 AWS SDK 的输入输出格式,以适应我们自己的X接口契约。

亚马逊翻译服务支持自定义术语的概念。此功能允许开发人员在翻译过程中设置自定义术语。这对于源文本中包含非标准语言的单词和短语的使用场景非常有用,比如公司名称、品牌和产品。例如,“Packt”不会被正确翻译。为纠正翻译,我们可以通过上传一个逗号分隔值CSV)文件,在 AWS 账户中创建一个自定义术语映射,将“Packt”映射为在不同语言中的正确翻译,具体如下所示:

en,fr,de,es
Packt, Packt, Packt, Packt

在翻译过程中,我们可以使用 TerminologyNames 参数指定一个或多个自定义术语。请参阅 AWS 文档,docs.aws.amazon.com/translate/latest/dg/how-custom-terminology.html了解更多详情。

存储服务 – 上传文件

让我们创建一个名为StorageService的 Python 类,如下所示,文件位于chalicelib目录中的storage_service.py文件中:

import boto3

class StorageService:
    def __init__(self, storage_location):
        self.client = boto3.client('s3')
        self.bucket_name = storage_location

    def get_storage_location(self):
        return self.bucket_name

    def upload_file(self, file_bytes, file_name):
        self.client.put_object(Bucket = self.bucket_name,
                               Body = file_bytes,
                               Key = file_name,
                               ACL = 'public-read')

        return {'fileId': file_name,
                'fileUrl': "http://" + self.bucket_name + ".s3.amazonaws.com/" + file_name}

在前面的代码中,以下内容适用:

  • 构造函数__init__()创建一个boto3客户端,或者被发送到 S3 服务。构造函数还接收一个storage_location参数,作为我们实现中的 S3 桶名称。

  • get_storage_location()方法返回 S3 桶的名称作为storage_location

  • upload_file()方法接收待上传文件的原始字节和文件名。该方法随后调用boto3 S3 客户端的put_object()函数,传入桶名称、原始字节、密钥和访问控制列表ACL)参数。

  • upload_file()的前三个参数不言自明。ACL 参数指定文件上传到 S3 桶后将是公开可读的。由于 S3 桶可以提供静态资源,例如图像和文件,因此我们将使用 S3 在 Web 用户界面中提供该图像。

  • 我们的upload_file()方法返回文件名以及上传到 S3 的文件的 URL。由于 ACL 设置为public-read,任何拥有该 URL 的人都可以在互联网上查看此文件。

这个类及其前两个方法与我们在第二章《现代 AI 应用程序解剖》中实现的StorageService完全相同。我们在这里复制它们,是为了让每个动手项目都能独立运行,但本质上,我们只是向第二章《现代 AI 应用程序解剖》的StorageService实现中添加了upload_file()方法。

关于单元测试的建议

尽管单元测试超出了本书的范围,我们还是强烈建议,在开发智能化或其他类型的应用程序时,将编写单元测试作为一种习惯。每一层的应用程序都应该编写单元测试。单元测试应该经常运行,以执行功能并捕捉错误。逐层测试应用程序将通过缩小错误的搜索范围来减少调试的时间和精力。在本书的所有动手项目的开发过程中,我们编写了单元测试。作为示例,以下是我们为TranslationService编写的单元测试:

return files
import os, sys
import unittest

from chalicelib import translation_service

class TranslationServiceTest(unittest.TestCase):
    def setUp(self):
        self.service = translation_service.TranslationService()

    def test_translate_text(self):
        translation = self.service.translate_text('Einbahnstrabe')
        self.assertTrue(translation)
        self.assertEqual('de', translation['sourceLanguage'])
        self.assertEqual('One way', translation['translatedText'])

if __name__ == "__main__":
    unittest.main()

这是一个简单的单元测试,但它确保了文本翻译在进入下一层之前能够正常工作。如果应用程序中的某些功能不正常,我们有理由相信,这不是由该服务实现引起的。

实现 RESTful 端点

现在服务已经实现,让我们进入编排层,使用 RESTful 端点。

用以下代码替换Chalice项目中的app.py文件内容:

from chalice import Chalice
from chalicelib import storage_service
from chalicelib import recognition_service
from chalicelib import translation_service

#####
# chalice app configuration
#####
app = Chalice(app_name='Capabilities')
app.debug = True

#####
# services initialization
#####
storage_location = 'contents.aws.ai'
storage_service = storage_service.StorageService(storage_location)
recognition_service = recognition_service.RecognitionService(storage_service)
translation_service = translation_service.TranslationService()

#####
# RESTful endpoints
#####
...

在前面的代码中,以下内容适用:

  • 前四行代码处理了chalice以及我们三个服务的导入。

  • 接下来的两行代码声明了名称为Capabilitieschalice应用,并开启了调试标志。debug标志告诉 chalice 输出更多有用的信息,这在开发过程中很有帮助。当将应用程序部署到生产环境时,你可以将此标志设置为False

  • 接下来的四行代码定义了storage_location参数,指定为我们的 S3 桶,然后实例化我们的存储、识别和翻译服务。storage_location参数应替换为你的 S3 桶名称。

请记住,storage_location 参数比 S3 存储桶名称更具通用性。对于 StorageServiceRecognitionService,该参数可以表示除 S3 存储桶之外的其他存储位置,例如 NFS 路径或资源 URI,具体取决于服务实现。这使得 StorageServiceRecognitionService 可以更换底层的存储技术。然而,在此设计中,StorageServiceRecognitionService 被耦合使用相同的存储技术。假设 RecognitionService 在执行文本检测任务时可以访问通过 StorageService 上传的文件。我们本可以设计 StorageService 返回图像的原始字节,然后将其传递给 RecognitionService。这种设计可以移除相同存储技术的限制,但它增加了复杂性和性能开销。设计时总是有取舍:作为 AI 从业者,你必须为你的具体应用做出取舍决策。

翻译图像文本端点

我们将从翻译图像文本端点开始。以下代码将继续 app.py 中的 Python 代码:

...
import json

...
#####
# RESTful endpoints
####
@app.route('/images/{image_id}/translated-text', methods = ['POST'], cors = True)
def translate_image_text(image_id):
    """detects then translates text in the specified image"""
    request_data = json.loads(app.current_request.raw_body)
    from_lang = request_data['fromLang']
    to_lang = request_data['toLang']

    MIN_CONFIDENCE = 80.0

    text_lines = recognition_service.detect_text(image_id)

    translated_lines = []
    for line in text_lines:
        # check confidence
        if float(line['confidence']) >= MIN_CONFIDENCE:
            translated_line = translation_service.translate_text(line['text'], from_lang, to_lang)
            translated_lines.append({
                'text': line['text'],
                'translation': translated_line,
                'boundingBox': line['boundingBox']
            })

    return translated_lines

在前面的代码中,以下内容适用:

  • translate_image_text() 函数实现了 RESTful 端点。

  • 在该函数上方的注释描述了可以访问此端点的 HTTP 请求。

  • translate_image_text() 函数中,我们首先获取包含源语言 fromLang 和目标语言 toLang 的请求数据,以进行翻译。

  • 接下来,我们调用 RecognitionService 来检测图像中的文本,并将检测到的文本行存储在 text_lines 中。

  • 然后,对于 text_lines 中的每一行文本,我们检查检测的置信度。如果置信度高于 MIN_CONFIDENCE(设置为 80.0),我们就会对该文本行进行翻译。

  • 然后,我们将 texttranslationboundingBox 作为 JSON 返回给调用者(chalice 会自动将 translated_line 中的内容格式化为 JSON)。

以下是一个针对该 RESTful 端点的 HTTP 请求。按照 RESTful 约定,/images 路径被视为集合资源,而 image_id 指定了该集合中的某个特定图像:

POST <server url>/images/{image_id}/translate-text
{
    fromLang: "auto",
    toLang: "en"
}

为了对 /images/{image_id} URL 中指定的特定图像执行操作,我们使用 POST HTTP 请求发起一个自定义的 translate-text 操作。请求体中作为 JSON 载荷的额外参数 fromLangtoLang 用来指定翻译的语言代码。为了读取这个 RESTful HTTP 请求,我们在 <server url> 上为 images 集合中的图像执行 translate-text 操作,并指定了 image_id

让我们通过在 Python 虚拟环境中运行 chalice local 来测试此端点,如下所示,然后发出以下 curl 命令,并指定一个已经上传到我们 S3 存储桶中的图像:

$ curl --header "Content-Type: application/json" --request POST --data '{"fromLang":"auto","toLang":"en"}' http://127.0.0.1:8000/images/german_street_sign.jpg/translate-text
[
 {
 "text": "Einbahnstrabe",
 "translation": {
 "translatedText": "One way",
 "sourceLanguage": "de",
 "targetLanguage": "en"
 },
 "boundingBox": {
 "Width": 0.495918333530426,
 "Height": 0.06301824748516083,
 "Left": 0.3853428065776825,
 "Top": 0.4955403208732605
 }
 }
]

这是我们的 Web 用户界面将接收的 JSON,并用于向用户显示翻译结果。

上传图像端点

我们将允许此端点的客户端使用 Base64 编码上传图像。通过 Base64 编码,我们可以将二进制数据(如图像和音频)转换为 ASCII 字符串格式,并进行反向转换。这种方法允许我们的应用程序使用 HTTP 请求中的 JSON 负载上传图像。别担心,你不需要了解 Base64 就能继续进行项目实现。

让我们看一下端点函数的代码:

import base64
import json
...

@app.route('/images', methods = ['POST'], cors = True)
def upload_image():
    """processes file upload and saves file to storage service"""
    request_data = json.loads(app.current_request.raw_body)
    file_name = request_data['filename']
    file_bytes = base64.b64decode(request_data['filebytes'])

    image_info = storage_service.upload_file(file_bytes, file_name)

    return image_info

在上述代码中,以下内容适用:

  • upload_image() 函数实现了 RESTful 端点。它上面的注解描述了可以访问此端点的 HTTP 请求。

  • upload_image() 函数中,我们使用 Base64 解码 HTTP 请求中的 JSON 负载中的上传文件,然后通过我们的 StorageService 进行上传。

  • 在这个函数中,我们将 StorageService.upload_file() 的输出以 JSON 格式返回给调用者。

以下是对该 RESTful 端点的 HTTP 请求。再次如下面的代码块所示,/images 在 RESTful 规范中被视为一个集合资源:

POST <server url>/images

要在该集合中创建一个新资源,RESTful 规范使用 POST 方法向 /images 集合资源发送请求。

chalice local 运行时,使用以下 curl 命令来测试上传端点。我们通过 echo 命令将包括 Base64 编码的 JSON 负载发送到我们的端点。命令中指定的文件必须位于本地文件系统中:

$ (echo -n '{"filename": "german_street_sign.jpg", "filebytes": "'; base64 /<file path>/german_street_sign.jpg; echo '"}') | curl --header "Content-Type: application/json" -d @- http://127.0.0.1:8000/images
{
   "fileId":"germany_street_sign.jpg",
   "fileUrl":"https://contents.aws.ai.s3.amazonaws.com/german_street_sign.jpg"
}

在上述代码中,以下内容适用:

  • 这是我们的 Web 用户界面将接收到的 JSON。我们会收到一个 fileId;此 ID 可以用来指定 /images 集合资源中的上传图像。

  • 我们还会得到一个 fileUrl,当前的 StorageService 实现返回文件的 S3 URL,但这个 fileUrl 是通用的,并未绑定到特定的服务。

  • 我们将使用这个 fileUrl 在 Web 用户界面中显示图像。

此时,你可以去 S3 存储桶查看文件是否已经成功上传。

实现 Web 用户界面

接下来,让我们在 Website 目录中的 index.htmlscript.js 文件里创建一个简单的 Web 用户界面,使用 HTML 和 JavaScript。

这就是最终的 Web 界面样式:

index.html

让我们创建一个 Web 用户界面,使用 index.html 文件,如下面的代码块所示:

<!doctype html>
<html lang="en"/>

<head>
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>

    <title>Pictorial Translator</title>

    <link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
    <link rel="stylesheet" href="https://www.w3schools.com/lib/w3-theme-blue-grey.css">
</head>

<body class="w3-theme-14">
    <div style="min-width:400px">
        <div class="w3-bar w3-large w3-theme-d4">
            <span class="w3-bar-item">Pictorial Translator</span>
        </div>

        <div class="w3-container w3-content">
            <p class="w3-opacity"><b>Upload</b></p>
            <input id="file" type="file" name="file" accept="image/*"/>
            <input class="w3-button w3-blue-grey" type="submit" value="Upload"
                   onclick="uploadAndTranslate()"/>

            <p class="w3-opacity"><b>Image</b></p>
            <div id="view" class="w3-panel w3-white w3-card w3-display-container"
                 style="display:none;">
                <div style="float: left;">
                    <img id="image" width="600"/>
                </div>
                <div style="float: right;">
                    <h5>Translated Text:</h5>
                    <div id="translations"/>
                </div>
            </div>
        </div>
    </div>

    <script src="img/scripts.js"></script>
</body>

</html>

我们在这里使用了标准的 HTML 标签,因此网页代码应该很容易理解。这里有一些要点:

  • 我们使用了两个 <input> 标签,一个用于选择文件,另一个用于上传按钮。通常,<input> 标签用于 HTML 表单中,但我们在这里使用 JavaScript 函数 uploadAndTranslate(),当点击上传按钮时触发。

  • 具有image ID 的<img>标签将用于显示上传的图像。JavaScript 将使用此 ID 动态添加图像。

  • 具有translations ID 的<div>标签将用于显示检测到的文本行及其翻译。JavaScript 将使用此id动态添加文本和翻译。

scripts.js

让我们按照以下示例创建scripts.js。JavaScript 函数与端点进行交互,构建了 Pictorial Translator 的整体用户体验。让我们看一下以下代码:

  1. 首先,将serverUrl定义为chalice local的地址。

  2. 我们还将定义一个新的HttpError来处理在 HTTP 请求过程中可能发生的异常。

  3. 将此 JavaScript 类添加到scripts.js文件的末尾:

"use strict";
const serverUrl = "http://127.0.0.1:8000";
...
class HttpError extends Error {
 constructor(response) {
        super(`${response.status} for ${response.url}`);
        this.name = "HttpError";
        this.response = response;
    }
}

  1. 接下来,我们将在scripts.js中定义四个函数:
  • uploadImage():该函数通过 Base64 编码上传图像到我们的UploadImage()端点。

  • updateImage():该函数更新用户界面,使用 S3 URL 显示上传的图像。

  • translateImage():该函数调用翻译图片文本的端点,将图像中检测到的文本进行翻译。

  • updateTranslations():该函数更新用户界面,显示翻译后的文本。

这些是用户体验的顺序步骤。我们将它们拆分为单独的函数,以使 JavaScript 代码更加模块化和易于阅读。每个函数执行特定任务。

让我们看一下uploadImage()函数,如下代码块所示:

async function uploadImage() {
    // encode input file as base64 string for upload
    let file = document.getElementById("file").files[0];
    let converter = new Promise(function(resolve, reject) {
        const reader = new FileReader();
        reader.readAsDataURL(file);
        reader.onload = () => resolve(reader.result
            .toString().replace(/^data:(.*,)?/, ''));
        reader.onerror = (error) => reject(error);
    });
    let encodedString = await converter;

    // clear file upload input field
    document.getElementById("file").value = "";

    // make server call to upload image
    // and return the server upload promise
    return fetch(serverUrl + "/images", {
        method: "POST",
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({filename: file.name, filebytes: encodedString})
    }).then(response => {
        if (response.ok) {
            return response.json();
        } else {
            throw new HttpError(response);
        }
    })
}

在前面的代码中,以下内容适用:

  • uploadImage()函数正在从index.html中的文件输入字段创建一个 Base64 编码的字符串。

    • 这个函数被声明为异步函数,因为我们需要等待文件被读取并编码。

    • 这个函数创建了一个 JavaScript Promise函数,使用FileReader来读取文件,然后通过readAsDataURL()函数将文件内容转换为 Base64。

  • 这个函数在每次上传后清空文件输入字段,以便用户可以更轻松地上传另一张图片。

  • 这个函数然后发送带有 JSON 负载的 POST HTTP 请求到我们的上传图片端点 URL,并返回response.json

让我们看一下updateImage()函数,如下代码块所示:

function updateImage(image) {
    document.getElementById("view").style.display = "block";

    let imageElem = document.getElementById("image");
    imageElem.src = image["fileUrl"];
    imageElem.alt = image["fileId"];

    return image;
}

在前面的代码中,以下内容适用:

  • updateImage()函数使得具有view ID 的<div>标签可见,以显示图片。

  • 这个函数查找具有image ID 的<img>标签,并将src属性设置为存储在 S3 中的图像文件的 URL。

  • <img>标签的alt属性设置为文件名,以防图像因某些原因无法加载。

alt属性使网页对更多用户更具可访问性,包括视力障碍者。有关网页可访问性的更多信息,请搜索508 合规性

让我们看一下translateImage()函数,如下代码块所示:

function translateImage(image) {
    // make server call to translate image
    // and return the server upload promise
    return fetch(serverUrl + "/images/" + image["fileId"] + "/translate-text", {
        method: "POST",
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({fromLang: "auto", toLang: "en"})
    }).then(response => {
        if (response.ok) {
            return response.json();
        } else {
            throw new HttpError(response);
        }
    })
}

在前面的代码中,以下内容适用:

  • translateImage() 函数向我们的 Translate Image Text Endpoint URL 发送 HTTP POST 请求,并附带 JSON 请求体。

  • 然后,函数返回包含翻译文本的响应 JSON。

让我们来看一下 annotateImage() 函数,如下所示的代码块:

function annotateImage(translations) {
    let translationsElem = document.getElementById("translations");
    while (translationsElem.firstChild) {
        translationsElem.removeChild(translationsElem.firstChild);
    }
    translationsElem.clear

    for (let i = 0; i < translations.length; i++) {
        let translationElem = document.createElement("h6");
        translationElem.appendChild(document.createTextNode(
            translations[i]["text"] + " -> " + translations[i]["translation"]["translatedText"]
        ));
        translationsElem.appendChild(document.createElement("hr"));
        translationsElem.appendChild(translationElem);
    }
}

在前面的代码中,以下内容适用:

  • updateTranslations() 函数找到 translations ID 的 <div> 标签,并删除先前图像中的任何现有翻译。

  • 然后,为每一行文本的 <div> 标签添加一个新的 <h6> 标签,用于显示检测到的文本及其翻译。

这四个函数通过以下的 uploadAndTranslate() 函数组合在一起:

function uploadAndTranslate() {
    uploadImage()
        .then(image => updateImage(image))
        .then(image => translateImage(image))
        .then(translations => annotateImage(translations))
        .catch(error => {
            alert("Error: " + error);
        })
}

注意观察 uploadAndTranslate() 函数中事件的执行顺序:

  1. 如果 updateImage() 函数成功,则使用图像信息运行 updateImage()

  2. 然后,使用图像信息运行 translateImage() 函数。如果 translateImage() 函数成功,则运行 updateTranslations()

  3. 捕获链中的任何错误并在弹出框中显示。

Pictorial Translator 应用程序的最终项目结构应如下所示:

├── Capabilities
│   ├── app.py
│   ├── chalicelib
│   │ ├── __init__.py
│   │ ├── recognition_service.py
│   │ ├── storage_service.py
│   │ └── translation_service.py
│   └── requirements.txt
├── Pipfile
├── Pipfile.lock
└── Website
    ├── index.html
    └── scripts.js

现在,我们已经完成了 Pictorial Translator 应用程序的实现。

将 Pictorial Translator 部署到 AWS

Pictorial Translator 应用程序的部署步骤与 第二章 中 Rekognition 演示的部署步骤相同,现代 AI 应用程序的结构;我们已经在此处包含了这些步骤以供完成:

  1. 首先,让我们告诉 chalice 执行策略分析,通过在项目结构的 .chalice 目录中的 config.json 文件中将 autogen_policy 设置为 false
{
  "version": "2.0",
  "app_name": "Capabilities",
  "stages": {
    "dev": {
      "autogen_policy": false,
      "api_gateway_stage": "api"
    }
  }
}
  1. 接下来,我们在 .chalice 目录中创建一个新的 policy-dev.json 文件,手动指定项目所需的 AWS 服务:
{
 "Version": "2012-10-17",
 "Statement": [
 {
 "Effect": "Allow",
 "Action": [
 "logs:CreateLogGroup",
 "logs:CreateLogStream",
 "logs:PutLogEvents",
 "s3:*",
 "rekognition:*",
 "translate:*"
 ],
 "Resource": "*"
 }
 ]
}
  1. 接下来,我们通过在 Capabilities 目录下运行以下命令,将 chalice 后端部署到 AWS:
$ chalice deploy
Creating deployment package.
Creating IAM role: Capabilities-dev
Creating lambda function: Capabilities-dev
Creating Rest API
Resources deployed:
  - Lambda ARN: arn:aws:lambda:us-east-1:<UID>:function:Capabilities-dev
  - Rest API URL: https://<UID>.execute-api.us-east-1.amazonaws.com/api/

部署完成后,chalice 将输出一个类似 https://<UID>.execute-api.us-east-1.amazonaws.com/api/ 的 RESTful API URL,其中 <UID> 标签是一个唯一标识符字符串。这是前端应用程序应访问的服务器 URL,以便连接运行在 AWS 上的应用程序后端。

  1. 接下来,我们将把 index.htmlscripts.js 文件上传到这个 S3 存储桶,并将权限设置为公开可读。在此之前,我们需要在 scripts.js 中进行以下更改。记住,网站现在将在云端运行,无法访问我们的本地 HTTP 服务器。将本地服务器的 URL 替换为来自后端部署的 URL:
"use strict";

const serverUrl = "https://<UID>.execute-api.us-east-1.amazonaws.com/api";

...

现在,Pictorial Translator 应用程序已经可以让互联网中的所有人访问,缩小了我们的世界!

讨论项目增强的想法

在第二部分的每个动手项目结束时,我们提供了一些扩展智能应用的想法。以下是几个增强“图像翻译器”的想法:

  • 为原文和翻译后的文本添加语音朗读功能。对原文进行语音朗读有助于用户学习外语;对翻译文本进行语音朗读可以帮助视障用户。AWS 提供了通过 Amazon Polly 服务生成语音的能力。

  • 创建一个原生移动应用以提供更好的用户体验。例如,提供连续的相机扫描以实现实时的图像翻译。移动应用可以利用我们创建的相同两个端点。这个移动应用只是“图像翻译器”应用程序的另一个前端。

总结

在这一章中,我们构建了一个“图像翻译器”应用,用于翻译出现在图片中的文本。我们利用 Amazon Rekognition 首先检测图片中的文本行,然后利用 Amazon Translate 进行翻译。这是我们第一个智能应用解决方案,它解决了一个实际问题。通过动手项目构建这些解决方案有助于培养你用 AI 能力解决问题的直觉。在这个过程中,我们还讨论了必须根据应用的实际使用情况进行验证的解决方案设计决策和权衡。从架构的角度来看,我们不仅构建了一个可运行的应用,还以一种允许未来复用的方式构建了它,可以在以后的动手项目中加以利用。

在下一章中,我们将使用更多的 AWS AI 服务构建更多的智能应用。在积累了更多动手项目的经验之后,要特别注意我们架构设计决策所创造的复用机会。

进一步阅读

要了解更多有关使用 Amazon Rekognition 和 Amazon Translate 检测和翻译文本的信息,请参考以下链接:

第四章:使用 Amazon Transcribe 和 Polly 执行语音转文本及反向操作

在本章中,我们将继续发展实现现实世界人工智能应用所需的技能和直觉。我们将构建一个能够将口语从一种语言翻译成另一种语言的应用程序。我们将利用 Amazon Transcribe 和 Amazon Polly 来执行语音转文本和文本转语音任务。我们还将展示我们的参考架构如何让我们重用在前一章项目中实现的服务。

我们将讨论以下主题:

  • 使用 Amazon Transcribe 执行语音转文本

  • 使用 Amazon Polly 进行文本转语音

  • 使用 AWS 服务、RESTful API 和网页用户界面构建无服务器 AI 应用程序

  • 在参考架构中重用现有的 AI 服务实现

  • 讨论用户体验和产品设计决策

技术要求

本书的 GitHub 仓库,包含本章的源代码,可以在 github.com/PacktPublishing/Hands-On-Artificial-Intelligence-on-Amazon-Web-Services 找到。

科幻中的技术

谷歌最近进入耳机市场,推出了 Pixel Buds,这款耳机有一个独特的功能让评论员们惊艳。这些耳机能够实时翻译多种语言的对话。这听起来像科幻小说中的情节。让人想到的是《星际迷航》中的通用翻译器,它可以让星际舰队成员与几乎所有外星种族交流。尽管 Pixel Buds 没有科幻作品中的那种强大功能,但它们集成了一些令人惊叹的 人工智能 (AI) 技术。这款产品展示了我们可以期待的 AI 能力,它能够帮助我们与更多地方的更多人沟通。

我们将使用 AWS AI 服务实现类似的对话翻译功能。我们的应用程序,谦虚地命名为通用翻译器,将提供多种语言之间的语音对语音翻译。然而,我们的通用翻译器并不是实时的,它仅支持几十种人类语言。

理解通用翻译器的架构

我们的通用翻译器应用程序将为用户提供一个网页用户界面,让他们可以录制一句话,并将该句子翻译成另一种语言。以下是我们应用程序架构设计,突出了各个层级和服务。我们之前项目中的层级和组件组织应该对你来说并不陌生:

在这个应用程序中,网页用户界面将与编排层中的三个 RESTful 端点进行交互:

  • 上传录音端点 将把音频录制上传委托给我们的存储服务,该服务为 AWS S3 提供了一个抽象层。

  • 录音翻译端点将使用 Amazon 转录服务和 Amazon 翻译服务。它首先获取音频录音的转录内容,然后将转录文本翻译成目标语言。转录服务和翻译服务分别抽象了 Amazon Transcribe 和 Amazon Translate 服务。

  • 语音合成端点将把翻译后的文本的语音合成任务委托给语音服务,而该服务由 Amazon Polly 支持。

正如我们将在项目实现中看到的,翻译服务是从图像翻译项目中复用的,未做任何修改。此外,存储服务实现中的文件上传方法也复用了先前项目中的方法。分离编排层和服务实现的一个好处应该很明显:我们可以在编排层中将不同的服务实现拼接在一起,复用并重新组合,而无需修改。每个应用程序特有的业务逻辑在编排层中实现,而功能实现则不依赖于特定应用程序的业务逻辑。

通用翻译器的组件交互

下图展示了不同组件如何相互作用,以形成通用翻译应用的业务逻辑工作流:

以下内容是从用户的角度来看:

  • 用户首先在网页用户界面中选择语音翻译的源语言和目标语言。

  • 然后,用户使用屏幕上的控制录制一段简短的音频语音。

  • 该录音可以从网页用户界面播放。用户可以使用播放功能检查语音录音的质量。

  • 当用户对录音满意时,可以上传以进行翻译。

  • 一段时间后,网页用户界面将同时显示转录文本和翻译文本。

  • 最后,翻译文本的合成语音将通过网页用户界面提供音频播放。

我们决定将端到端的翻译过程分为三个主要步骤:

  1. 上传音频录音。

  2. 获取翻译文本。

  3. 合成语音。

这一设计决策使得通用翻译器能够在翻译音频合成时,在网页用户界面中显示翻译文本。这样,应用不仅显得更为响应用户需求,而且用户在某些情况下也可以利用翻译文本,而无需等待音频合成完成。

设置项目结构

让我们按照第二章《现代 AI 应用的结构》中的步骤创建一个类似的基础项目结构,包括pipenvchalice和网页文件:

  1. 在终端中,我们将创建根项目目录,并使用以下命令进入该目录:
$ mkdir UniversalTranslator
$ cd UniversalTranslator
  1. 我们将通过创建一个名为 Website 的目录来为 Web 前端创建占位符,在该目录内创建 index.htmlscripts.js 文件,如下所示:
$ mkdir Website
$ touch Website/index.html
$ touch Website/scripts.js
  1. 我们将在项目的 root 目录中使用 pipenv 创建一个 Python 3 虚拟环境。项目的 Python 部分需要两个包,boto3chalice。我们可以使用以下命令安装它们:
$ pipenv --three
$ pipenv install boto3
$ pipenv install chalice
  1. 请记住,通过 pipenv 安装的 Python 包仅在我们激活虚拟环境时可用。我们可以通过以下命令来激活虚拟环境:
$ pipenv shell
  1. 接下来,在仍处于虚拟环境中时,我们将创建一个名为 Capabilities 的 AWS chalice 项目作为编排层,使用以下命令:
$ chalice new-project Capabilities
  1. 要创建 chalicelib Python 包,请执行以下命令:
cd Capabilities
mkdir chalicelib
touch chalicelib/__init__.py
cd ..

通用翻译器的初始项目结构应如下所示:

Project Structure
------------
├── UniversalTranslator/
    ├── Capabilities/
        ├── .chalice/
            ├── config.json
        ├── chalicelib/
            ├── __init__.py
        ├── app.py
        ├── requirements.txt
    ├── Website/
        ├── index.html
        ├── script.js
    ├── Pipfile
    ├── Pipfile.lock

这是通用翻译器的项目结构;它包含了用户界面、编排和服务实现层,这是我们在第二章《现代 AI 应用程序的结构》中定义的 AI 应用程序架构的一部分,现代 AI 应用程序的解剖

实现服务

让我们分层实现这个应用程序,从包含通用翻译器核心 AI 功能的服务实现开始。

转录服务 – 语音转文本

在通用翻译器中,我们将把一种语言的口语翻译成另一种语言。翻译过程的第一步是知道哪些单词被说了。为此,我们将使用 Amazon Transcribe 服务。Amazon Transcribe 使用基于深度学习的自动语音识别ASR)算法将语音转换为文本。

让我们使用 AWS CLI 来理解 Transcribe 服务是如何工作的。输入以下命令以启动转录:

$ aws transcribe start-transcription-job
 --transcription-job-name <jobname>
 --language-code en-US
 --media-format wav
 --media MediaFileUri=https://s3.amazonaws.com/contents.aws.a/<audio file>.wav
 --output-bucket-name contents.aws.a
{
 "TranscriptionJob": {
 "TranscriptionJobName": "<jobname>",
 "TranscriptionJobStatus": "IN_PROGRESS",
 "LanguageCode": "en-US",
 "MediaFormat": "wav",
 "Media": {
 "MediaFileUri": "https://s3.amazonaws.com/<input bucket>/<audio file>.wav"
 },
 "CreationTime": 1552752370.771
 }
}

让我们了解前面命令中传递的参数:

  • 作业名称必须是每个转录作业的唯一标识符。

  • 语言代码告诉服务音频语言是什么。撰写本文时,支持的语言包括"en-US""es-US""en-AU""fr-CA""en-GB""de-DE""pt-BR""fr-FR""it-IT"

  • 媒体格式指定了语音的音频格式;可能的值为 "mp3""mp4""wav""flac"

  • 媒体参数接受音频录音的 URI,例如,S3 URL。

  • 输出存储桶名称指定了转录输出应存储在哪个 S3 存储桶中。

你需要将音频录音上传到 S3 存储桶中,以便该命令能够工作。你可以使用任何可用的软件工具录制音频片段,或者跳到本章的语音服务部分,了解如何使用 Amazon Polly 服务生成语音音频。

有趣的是,我们在此命令的输出中不会得到转录文本。实际上,我们可以看到我们刚启动的转录任务尚未完成。在输出中,TranscriptionJobStatus仍然是IN_PROGRESS。Amazon Transcribe 服务遵循异步模式,这通常用于长时间运行的任务。

那么,如何知道任务是否完成呢?有另一个命令,如下所示。我们可以执行此命令来检查刚刚启动的任务的状态:

$ aws transcribe get-transcription-job --transcription-job-name <jobname>
{
 "TranscriptionJob": {
 "TranscriptionJobName": "<jobname>",
 "TranscriptionJobStatus": "COMPLETED",
 "LanguageCode": "en-US",
 "MediaSampleRateHertz": 96000,
 "MediaFormat": "wav",
 "Media": {
 "MediaFileUri": "https://s3.amazonaws.com/<input bucket>/<audio file>.wav"
 },
 "Transcript": {
 "TranscriptFileUri": "https://s3.amazonaws.com/<output bucket>/jobname.json"
 },
 "CreationTime": 1552752370.771,
 "CompletionTime": 1552752432.731,
 "Settings": {
 "ChannelIdentification": false
 }
 }
}

在上面的命令中:

  • get-transcription-job命令在这里接受一个参数,即我们启动任务时指定的唯一作业名称。

  • 当任务状态变为"COMPLETED"时,"TranscriptFileUri"将指向我们之前指定的输出桶中的 JSON 输出文件。

这个 JSON 文件包含了实际的转录内容;下面是一个摘录:

{
   "jobName":"jobname",
   "accountId":"...",
   "results":{
      "transcripts":[
         {
            "transcript":"Testing, testing one two three"
         }
      ],
      "items":[
         ...
      ]
   },
   "status":"COMPLETED"
}

这是我们在服务实现中需要解析的 JSON 输出,用于提取转录文本。

转录服务被实现为一个 Python 类:

  • 构造函数__init__()创建了一个boto3客户端,用于连接转录服务。构造函数还接受存储服务作为依赖,以便后续使用。

  • transcribe_audio()方法包含了与 Amazon Transcribe 服务交互的逻辑。

  • extract_transcript()方法是一个辅助方法,包含了解析来自转录服务的转录 JSON 输出的逻辑。

import boto3
import datetime
import time
import json

class TranscriptionService:
    def __init__(self, storage_service):
        self.client = boto3.client('transcribe')
        self.bucket_name = storage_service.get_storage_location()
        self.storage_service = storage_service

    def transcribe_audio(self, file_name, language):
        ...

    @staticmethod
    def extract_transcript(transcription_output):
        ...

在我们深入探讨"transcribe_audio()""extract_transcript()"方法的实现之前,首先让我们来看一下boto3 SDK 中与转录相关的 API。

与 AWS CLI 命令一样,Amazon Transcribe 的 API 遵循相同的异步模式。我们可以通过"TranscriptionJobName"调用"start_transcription_job()",这是转录任务的唯一标识符。此方法会启动转录过程;然而,当 API 调用完成时,它不会返回转录文本。"start_transcription_job()" API 调用的响应中还会返回一个"TranscriptionJobStatus"字段,其值可以是"IN_PROGRESS""COMPLETED""FAILED"中的一个。我们可以通过 API 调用"get_transcritpion_job()"并指定先前的"TranscriptionJobName"来检查任务的状态。

当任务完成时,转录文本会被放置在 S3 桶中。我们可以在调用"start_transcription_job()"时通过"OutputBucketName"指定一个 S3 桶,或者 Amazon Transcribe 会将转录输出放入一个默认的 S3 桶,并提供一个预签名的 URL 来访问转录文本。如果任务失败,"start_transcription_job()""get_transcription_job()"的响应中会有另一个"FailureReason"字段,提供任务失败的原因。

这种异步模式通常用于较长时间运行的过程。它允许调用者在等待过程完成的同时进行其他任务,之后再检查进度。可以类比在 Amazon.com 上订购商品的客户体验。客户不需要一直在网站上等待商品包装、发货和配送完成,而是会立即看到订单已成功下单的确认信息,并且可以稍后通过唯一的订单 ID 查询订单状态。

TranscriptionService 类中的 "transcribe_audio()" 方法解决了 Amazon Transcribe API 的异步模式:

def transcribe_audio(self, file_name, language):
    POLL_DELAY = 5

    language_map = {
        'en': 'en-US',
        'es': 'es-US',
        'fr': 'fr-CA'
    }

    job_name = file_name + '-trans-' + datetime.datetime.now().strftime("%Y%m%d%H%M%S")

    response = self.client.start_transcription_job(
        TranscriptionJobName = job_name,
        LanguageCode = language_map[language],
        MediaFormat = 'wav',
        Media = {
            'MediaFileUri': "http://" + self.bucket_name + ".s3.amazonaws.com/" + file_name
        },
        OutputBucketName = self.bucket_name
    )

    transcription_job = {
        'jobName': response['TranscriptionJob']['TranscriptionJobName'],
        'jobStatus': 'IN_PROGRESS'
    }
    while transcription_job['jobStatus'] == 'IN_PROGRESS':
        time.sleep(POLL_DELAY)
        response = self.client.get_transcription_job(
            TranscriptionJobName = transcription_job['jobName']
        )
        transcription_job['jobStatus'] = response['TranscriptionJob']
                                                ['TranscriptionJobStatus']

    transcription_output = self.storage_service.get_file(job_name + '.json')
    return self.extract_transcript(transcription_output)

以下是对前面 "transcribe_audio()" 实现的概述:

  • 我们使用一个简单的 Python 字典来存储三对语言代码。"en"、"es" 和 "fr" 是我们通用翻译器使用的语言代码,它们分别映射到 Amazon Transcribe 使用的语言代码 "en-US"、"es-US" 和 "fr-CA"。

  • 这种映射仅限于三种语言,以简化我们的项目。然而,它展示了抽象语言代码的技术,这是我们的应用程序中第三方服务的实现细节。通过这种方式,无论底层的第三方服务如何,我们都可以始终标准化应用程序使用的语言代码。

  • 我们为每个转录任务生成了一个独特的名称,叫做"job_name"。这个名称结合了音频文件名和当前时间的字符串表示。通过这种方式,即使在同一个文件上多次调用该方法,任务名称仍然保持唯一。

  • 然后,方法调用 "start_transcription_job()",传入唯一的 "job_name"、语言代码、媒体格式、录音所在的 S3 URI,最后是输出桶的名称。

  • 即便 Transcribe API 是异步的,我们仍然设计了 "transcribe_audio()" 方法为同步方式。为了让我们的同步方法能够与 Transcribe 的异步 API 配合使用,我们添加了一个等待循环。该循环等待 POLL_DELAY 秒(设置为 5 秒),然后重复调用 "get_transcription_job()" 方法检查任务状态。当任务状态仍为 "IN_PROGRESS" 时,循环会继续运行。

  • 最后,当任务完成或失败时,我们通过存储服务从指定的 S3 存储桶中获取 JSON 输出文件的内容。这也是我们在构造函数中需要将存储服务作为依赖的原因。接着,我们使用 "extract_transcript()" 辅助方法解析转录输出。

接下来,我们实现了之前由 "transcribe_audio()" 方法使用的 "extract_transcript()" 辅助方法来解析 Amazon Transcribe 的输出:

@staticmethod
def extract_transcript(transcription_output):
    transcription = json.loads(transcription_output)

    if transcription['status'] != 'COMPLETED':
        return 'Transcription not available.'

    transcript = transcription['results']['transcripts'][0]['transcript']
    return transcript

我们之前已经见过使用 AWS CLI 发出的转录任务的 JSON 输出格式。这个辅助方法封装了解析该 JSON 输出的逻辑。它首先检查任务是否成功完成,如果没有,则方法返回一个错误信息作为转录文本;如果成功完成,则返回实际的转录文本。

翻译服务 – 翻译文本

就像在第三章,使用 Amazon Rekognition 和 Translate 检测和翻译文本 中的图像翻译应用程序一样,我们将利用 Amazon Translate 服务提供语言翻译功能。如前所述,我们可以重复使用在图像翻译项目中使用的相同翻译服务实现:

import boto3

class TranslationService:
    def __init__(self):
        self.client = boto3.client('translate')

    def translate_text(self, text, source_language = 'auto', target_language = 'en'):
        response = self.client.translate_text(
            Text = text,
            SourceLanguageCode = source_language,
            TargetLanguageCode = target_language
        )

        translation = {
            'translatedText': response['TranslatedText'],
            'sourceLanguage': response['SourceLanguageCode'],
            'targetLanguage': response['TargetLanguageCode']
        }

        return translation

上面的代码与之前项目中的 TranslationService 实现完全相同。为了完整起见,我们在此包括此代码。如需更多关于其实现和设计选择的详细信息,请参考第三章使用 Amazon Rekognition 和 Translate 检测和翻译文本

语音服务 – 文本到语音

一旦我们获得翻译文本,就将利用 Amazon Polly 服务生成翻译的语音版本。

在我们深入实现之前,让我们使用以下 AWS CLI 命令生成一个简短的音频语音:

$ aws polly start-speech-synthesis-task
 --output-format mp3
 --output-s3-bucket-name <bucket>
 --text "testing testing 1 2 3"
 --voice-id Ivy
{
 "SynthesisTask": {
 "TaskId": "e68d1b6a-4b7f-4c79-9483-2b5a5932e3d1",
 "TaskStatus": "scheduled",
 "OutputUri": "https://s3.us-east-1.amazonaws.com/<bucket>/<task id>.mp3",
 "CreationTime": 1552754991.114,
 "RequestCharacters": 21,
 "OutputFormat": "mp3",
 "TextType": "text",
 "VoiceId": "Ivy"
 }
}

该命令有四个必填参数:

  • 输出格式是音频格式:

    • 对于音频流,可以是 "mp3""ogg_vorbis""pcm"

    • 对于语音标记,这将是 "json"

  • 输出 S3 存储桶名称是生成的音频文件将被放置的地方。

  • 该文本是用于文本到语音合成的文本。

  • 语音 ID 指定了 Amazon Polly 提供的多种语音之一。语音 ID 间接指定了语言以及男女声。我们使用了 Ivy,这是美国英语的女性语音之一。

Amazon Polly 服务遵循与之前 Amazon Transcribe 相似的异步模式。要检查我们刚刚启动的任务状态,我们发出以下 AWS CLI 命令:

$ aws polly get-speech-synthesis-task --task-id e68d1b6a-4b7f-4c79-9483-2b5a5932e3d1
{
 "SynthesisTask": {
 "TaskId": "e68d1b6a-4b7f-4c79-9483-2b5a5932e3d1",
 "TaskStatus": "completed",
 "OutputUri": "https://s3.us-east-1.amazonaws.com/<bucket>/<task id>.mp3",
 "CreationTime": 1552754991.114,
 "RequestCharacters": 21,
 "OutputFormat": "mp3",
 "TextType": "text",
 "VoiceId": "Ivy"
 }
}

在上述命令中,我们有以下内容:

  • "get-speech-synthesis-task" 命令只接受一个参数,即在 "start-speech-synthesis-task" 命令输出中返回的任务 ID。

  • 当任务状态变为 "completed" 时,"OutputUri" 将指向我们之前指定的 S3 存储桶中生成的音频文件。

  • 音频文件名是任务 ID 和指定音频格式文件扩展名的组合,例如,"e68d1b6a-4b7f-4c79-9483-2b5a5932e3d1.mp3" 表示 MP3 格式。

我们的语音服务实现是一个 Python 类,包含一个构造函数 "__init__()" 和一个名为 synthesize_speech() 的方法。其实现如下:

import boto3
import time

class SpeechService:
    def __init__(self, storage_service):
        self.client = boto3.client('polly')
        self.bucket_name = storage_service.get_storage_location()
        self.storage_service = storage_service

    def synthesize_speech(self, text, target_language):
        POLL_DELAY = 5
        voice_map = {
            'en': 'Ivy',
            'de': 'Marlene',
            'fr': 'Celine',
            'it': 'Carla',
            'es': 'Conchita'
        }

        response = self.client.start_speech_synthesis_task(
            Text = text,
            VoiceId = voice_map[target_language],
            OutputFormat = 'mp3',
            OutputS3BucketName = self.bucket_name
        )

        synthesis_task = {
            'taskId': response['SynthesisTask']['TaskId'],
            'taskStatus': 'inProgress'
        }

        while synthesis_task['taskStatus'] == 'inProgress'\
                or synthesis_task['taskStatus'] == 'scheduled':
            time.sleep(POLL_DELAY)

            response = self.client.get_speech_synthesis_task(
                TaskId = synthesis_task['taskId']
            )

            synthesis_task['taskStatus'] = response['SynthesisTask']['TaskStatus']
            if synthesis_task['taskStatus'] == 'completed':
                synthesis_task['speechUri'] = response['SynthesisTask']['OutputUri']
                self.storage_service.make_file_public(synthesis_task['speechUri'])
                return synthesis_task['speechUri']

        return ''

构造函数创建了一个 Amazon Polly 服务的 boto3 客户端,并将 StorageService 作为依赖传入,以便后续使用。

synthesize_speech()方法中,我们使用了 Python 的 voice_map 字典来存储五对语言代码。我们的通用翻译器应用支持的语言代码有"en""de""fr""it""es"。与语言代码不同,Amazon Polly 使用的是与语言及男女语音相关联的语音 ID。以下是 Polly 语音映射的一个摘录:

语言 女性 ID(s) 男性 ID(s)
英语(英国)(en-GB) Amy, Emma Brian
德语 (de-DE) Marlene, Vicki Hans
法语 (fr-FR) Celine, Lea Mathieu
意大利语 (it-IT) Carla, Bianca Giorgio
西班牙语(欧洲)(es-ES) Conchita, Lucia Enrique

该方法中的 voice_map 字典存储了通用翻译器所支持的每种语言的第一个女性语音 ID。这个设计选择是为了简化我们的项目实现。对于更精致的语音对语音翻译应用,开发者可以选择支持更多语言,并提供对不同语音的自定义。再次强调,"voice_map" 抽象了第三方服务的实现细节,即 Amazon Polly 的语音 ID,隔离了应用层的实现。

我们选择支持的语言并非完全随意。在这里,我们特意选择了美国英语、美国西班牙语和加拿大法语作为 Amazon Transcribe 的输入语音,并选择了 Amazon Polly 的欧洲变种作为输出语音。我们面向的客户群体是来自北美的游客,他们计划前往欧洲使用我们的通用翻译器,至少对于这个最小可行产品MVP)版本而言。

Amazon Polly 服务的 API 遵循与其 AWS CLI 命令相同的异步模式,分别为 "start_speech_synthesis_task()""get_speech_synthesis_task()" API 调用。语音合成的实现与转录实现非常相似。我们再次调用 "start_speech_synthesis_task()" 方法来启动长时间运行的过程,然后使用 while 循环使我们的实现变得同步。这个循环等待 POLL_DELAY 秒(设置为 5 秒),然后调用 "get_speech_synthesis_task()" 方法来检查作业状态,状态可能为 "scheduled""inProgress""completed""failed"。当状态为 "scheduled""inProgress" 时,循环会继续运行。

请注意,即使在 AWS API 中,状态值在不同服务之间也不一致。我们的语音和转录服务将所有这些实现细节屏蔽在服务层之外。如果我们想要更换不同的语音或转录服务实现,相关更改将仅局限于服务实现层。

最后,当任务状态为 "completed" 时,我们会获取合成音频翻译的 S3 URI。默认情况下,S3 桶中的文件不会公开访问,我们的 Web 用户界面也无法播放音频翻译。因此,在返回 S3 URI 之前,我们使用了我们的存储服务方法 "make_file_public()" 将音频翻译设为公开。接下来,我们将查看在存储服务实现中是如何完成这一操作的。

存储服务 – 上传和检索文件

存储服务的实现大部分应与上一章中相似。__init__() 构造函数、"get_storage_location()" 方法和 "upload_file()" 方法与我们之前的实现完全相同。我们添加了两个新方法来扩展 StorageService 的功能。

这是完整的实现:

import boto3

class StorageService:
    def __init__(self, storage_location):
        self.client = boto3.client('s3')
        self.bucket_name = storage_location

    def get_storage_location(self):
        return self.bucket_name

    def upload_file(self, file_bytes, file_name):
        self.client.put_object(Bucket = self.bucket_name,
                               Body = file_bytes,
                               Key = file_name,
                               ACL = 'public-read')

        return {'fileId': file_name,
                'fileUrl': "http://" + self.bucket_name + ".s3.amazonaws.com/" + file_name}

    def get_file(self, file_name):
        response = self.client.get_object(Bucket = self.bucket_name, Key = file_name)

        return response['Body'].read().decode('utf-8')

    def make_file_public(self, uri):
        parts = uri.split('/')
        key = parts[-1]
        bucket_name = parts[-2]

        self.client.put_object_acl(Bucket = bucket_name,
                                   Key = key,
                                   ACL = 'public-read')

让我们来看看这两个新类方法:

  • get_file() 方法接受文件名并返回该文件的内容作为字符串。我们通过使用 boto3 S3 客户端,通过密钥(文件名)从桶名称(存储服务的存储位置)获取对象,并将文件内容解码为 UTF-8 字符串来实现这一点。

  • make_file_public() 方法接受一个文件 URI,并更改目标文件的访问控制列表ACL),以允许公开访问。由于我们的存储服务是由 AWS S3 支持的,该方法假设 URI 是 S3 URI,并相应地解析它以提取桶名称和密钥。通过桶名称和密钥,它将对象的 ACL 更改为 'public-read'

存储服务中的所有方法都设计得足够通用,以便它们更容易被不同的应用程序重用。

实现 RESTful 端点

现在服务已经实现,让我们继续探讨具有 RESTful 接口的编排层。由于所有实际工作都由服务实现完成,因此这些端点用于将功能拼接在一起,并为用户界面层提供 HTTP 访问以使用这些功能。因此,实施代码简洁且易于理解。

app.py 文件包含了 RESTful 端点的实现。以下是 app.py 中的一个代码片段,包含了导入、配置和初始化代码:

from chalice import Chalice
from chalicelib import storage_service
from chalicelib import transcription_service
from chalicelib import translation_service
from chalicelib import speech_service

import base64
import json

#####
# chalice app configuration
#####
app = Chalice(app_name='Capabilities')
app.debug = True

#####
# services initialization
#####
storage_location = 'contents.aws.ai'
storage_service = storage_service.StorageService(storage_location)
transcription_service = transcription_service.TranscriptionService(storage_service)
translation_service = translation_service.TranslationService()
speech_service = speech_service.SpeechService(storage_service)

#####
# RESTful endpoints
#####
...

我们将在接下来的几节中详细讨论 app.py 中每个端点的实现。

翻译录音端点

翻译录音端点是一个 HTTP POST 端点,接收请求正文中的 JSON 参数。此端点接受录音 ID 作为参数,并通过 JSON 正文传递翻译的源语言和目标语言:

@app.route('/recordings/{recording_id}/translate-text', methods = ['POST'], cors = True)
def translate_recording(recording_id):
    """transcribes the specified audio then translates the transcription text"""
    request_data = json.loads(app.current_request.raw_body)
    from_lang = request_data['fromLang']
    to_lang = request_data['toLang']

    transcription_text = transcription_service.transcribe_audio(recording_id, from_lang)

    translation_text = translation_service.translate_text(transcription_text,
                                                        target_language = to_lang)

    return {
        'text': transcription_text,
        'translation': translation_text
    }

该函数之上的注释描述了可以访问该端点的 HTTP 请求:

POST <server url>/recordings/{recording_id}/translate-text
{
    "fromLang": <SOURCE LANGUAGE>,
    "toLang": <TARGET LANGUAGE>
}

让我们来看一下之前的代码:

  • "transcribe_recording()" 之上的注释描述了可以访问该端点的 HTTP POST 请求。

  • 该函数首先获取包含源语言("fromLang")和目标语言("toLang")的请求数据,用于翻译。

  • "transcribe_recording()"函数调用转录服务转录音频录音。

  • 接下来,该函数调用翻译服务翻译转录文本。

  • 最后,这个函数返回一个包含转录文本和翻译信息的 JSON 对象。

让我们通过在 Python 虚拟环境中运行chalice local来测试这个端点,然后发出以下curl命令,指定已经上传到我们的 S3 桶中的音频片段:

$ curl --header "Content-Type: application/json" --request POST --data '{"fromLang":"en","toLang":"de"}' http://127.0.0.1:8000/recordings/<recording id>/translate-text
[
 {
 "text": "<transcription>",
 "translation": {
 "translatedText": "<translation>",
 "sourceLanguage": "en",
 "targetLanguage": "de"
 }
 }
]

<recording id>标识我们 S3 桶中音频文件的文件名。

这是我们的网页用户界面将接收并用来显示翻译结果的 JSON 数据。

合成语音端点

合成语音端点是一个 HTTP POST 端点,接受请求体中的 JSON 参数。这个端点使用 JSON 传入目标语言和要转换为语音的文本。尽管 Universal Translator 设计用于翻译简短的短语,但用于执行文本到语音的文本可能会很长,这取决于应用程序。我们在这里使用 JSON 负载,而不是 URL 参数,因为 URL 的长度是有限制的。这个设计决策使得该端点将来可以更方便地用于其他应用程序。保持应用程序的 URL 简短清晰也是一个好习惯:

@app.route('/synthesize_speech', methods = ['POST'], cors = True)
def synthesize_speech():
    """performs text-to-speech on the specified text / language"""
    request_data = json.loads(app.current_request.raw_body)
    text = request_data['text']
    language = request_data['language']

    translation_audio_url = speech_service.synthesize_speech(text, language)

    return {
        'audioUrl': translation_audio_url
    }

该函数上方的注释描述了可以访问此端点的 HTTP 请求:

POST <server url>/synthesize_speech
{
    "text": <TEXT>,
    "language": <LANGUAGE>
}

在前面的代码中,我们有以下内容:

  • synthesize_speech()函数将请求体解析为 JSON 数据,以获取语音合成的文本和语言。

  • 该函数随后调用语音服务的synthesize_speech()方法。

  • 然后该函数返回音频文件的 URL。请记住,在 synthesize_speech()返回之前,我们已经使这个音频文件对外公开可访问。

让我们通过在 Python 虚拟环境中运行chalice local来测试这个端点,然后发出以下curl命令来传递 JSON 负载:

$ curl --header "Content-Type: application/json" --request POST --data '{"text":"Dies ist ein Test des Amazons Polly Service.","language":"de"}' http://127.0.0.1:8000/synthesize_speech
{
 "audioUrl": "https://s3.us-east-1.amazonaws.com/<bucket>/<task id>.mp3"
}

这是我们的网页用户界面将接收并用来更新翻译语音的音频播放器的 JSON 数据。

上传录音端点

这个端点本质上与第三章中的上传图像端点相同,使用 Amazon Rekognition 和翻译检测和翻译文本,*图像翻译器应用程序。它使用我们在项目中实现的两个相同的函数,没有修改。唯一的变化是@app.route注释,在那里我们创建了一个不同的 HTTP POST 端点/recordings,通过 Base64 编码进行上传:

@app.route('/recordings', methods = ['POST'], cors = True)
def upload_recording():
    """processes file upload and saves file to storage service"""
    request_data = json.loads(app.current_request.raw_body)
    file_name = request_data['filename']
    file_bytes = base64.b64decode(request_data['filebytes'])

    file_info = storage_service.upload_file(file_bytes, file_name)

    return file_info

为了完整性,我们在这里包含了端点及其辅助函数的代码。有关它们实现的更多细节,请参考第三章,使用 Amazon Rekognition 和 Translate 检测和翻译文本

实现网页用户界面

接下来,让我们在Website目录下的index.htmlscripts.js文件中创建一个简单的网页用户界面。

这就是最终的网页界面样式:

在此应用程序中,用户首先在选择语言部分选择翻译的源语言和目标语言。然后,用户在录音部分录制简短的语音。在此部分,用户还可以播放录音以检查其质量。然后,翻译过程开始。当翻译文本可用时,它将在翻译文本部分显示给用户。接着,开始文本转语音生成过程。当生成的音频翻译可用时,音频播放器控制被启用,以允许播放翻译内容。

我们做出的设计决策是将每次翻译的步骤视为顺序执行,意味着一次只能执行一个翻译。每次端到端的翻译都会有一定的等待时间,主要是由于 Amazon Transcribe 和 Amazon Polly 服务的速度。我们有一些技术可以改善用户在等待期间的体验:

  • 一个最重要的技术实际上是让用户知道应用程序正在处理。我们在翻译文本和翻译音频部分使用了加载指示器,表示应用程序正在处理。我们展示加载指示器的事实向用户暗示这些步骤不是即时完成的。

  • 我们采用的另一种技术是将文本翻译和音频翻译步骤分开。尽管总的处理时间大致相同,用户可以看到进度和中间结果。从心理学角度看,这大大减少了用户对等待时间的感知。

  • 我们还可以减少在转录服务和语音服务实现中的POLL_DELAY。目前,POLL_DELAY在两者中都设置为 5 秒。这会导致在处理完成后存在一些延迟,平均每步延迟 2.5 秒,总共约 5 秒的延迟。我们当然可以减少延迟。然而,这里存在一个权衡:较短的POLL_DELAY将导致更多的 AWS API 调用,分别是"get_transcription_job()""get_speech_synthesis_task()"函数。

  • 最后,如果有可用的实时服务,我们可以使用它们来加速处理。例如,亚马逊的 Transcribe 现在支持名为流式转录(Streaming Transcription)的实时转录功能。此功能使得应用程序能够传入实时音频流并实时接收文本转录。不幸的是,在本文撰写时,该功能尚未在 Python AWS SDK 中提供。一个灵活的架构将使未来的服务实现(无论是基于 AWS 还是其他)能够更轻松地替换,从而推动应用程序的长期发展。

index.html

以下是index.html文件。我们在这里使用了标准的 HTML 标签,因此网页的代码应该很容易理解:

<!doctype html>
<html lang="en"/>

<head>
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>

    <title>Universal Translator</title>

    <link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
    <link rel="stylesheet" href="https://www.w3schools.com/lib/w3-theme-blue-grey.css">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
</head>

<body class="w3-theme-14">
    <div style="min-width:400px">
        <div class="w3-bar w3-large w3-theme-d4">
            <span class="w3-bar-item">Universal Translator</span>
        </div>

        ...
    </div>

    <script src="img/MediaStreamRecorder.js"></script>
   <script src="img/scripts.js"></script>
</body>

</html>

这个代码片段显示了网页用户界面的框架和标题:

  • 除了在之前的项目中使用的 W3 样式表外,我们还包含了用于旋转图标的 Font-Awesome CSS。

  • index.html的底部,我们包含了MediaStreamRecorder.js,用于实现网页用户界面的音频录制功能。

  • 其余的index.html代码片段位于<body>内的顶级<div>标签中。

    ...
        <div class="w3-container w3-content">
            <p class="w3-opacity"><b>Select Languages</b></p>
            <div class="w3-panel w3-white w3-card w3-display-container  
            w3-center">
                <div>
                    <b class="w3-opacity">From:</b>
                    <select id="fromLang">
                        <option value="en">English</option>
                        <option value="es">Spanish</option>
                        <option value="fr">French</option>
                    </select>
                    <hr>
                    <b class="w3-opacity">To:</b>
                    <select id="toLang">
                        <option value="de">German</option>
                        <option value="fr">French</option>
                        <option value="it">Italian</option>
                        <option value="es">Spanish</option>
                    </select>
                </div>
            </div>
        </div>

        <div class="w3-container w3-content">
            <p class="w3-opacity"><b>Record Audio</b></p>
            <div class="w3-panel w3-white w3-card w3-display-container 
            w3-center">
                <div>
                    <audio id="recording-player" controls>
                        Your browser does not support the audio  
                        element...
                    </audio>
                </div>
                <div>
                    <input type="button" id="record-toggle" 
                    value="Record"
                           onclick="toggleRecording()"/>
                    <input type="button" id="translate" 
                     value="Translate"
                           onclick="uploadAndTranslate()" disabled/>
                </div>
            </div>
        </div>
     ...

在这个代码片段中,我们有以下内容:

  • 我们创建了网页用户界面的“选择语言”和“录制音频”部分。

  • 在“选择语言”部分,我们将支持的fromLangtoLang硬编码在dropdown列表中。

  • 在“录制音频”部分,我们使用了<audio>标签来创建一个音频播放器,并添加了几个输入按钮以控制录音和翻译功能。

  • 大多数动态行为是在scripts.js中实现的。

要继续编辑index.html代码,请执行以下命令:

...
        <div class="w3-container w3-content">
            <p class="w3-opacity"><b>Translation Text</b></p>
            <div class="w3-panel w3-white w3-card w3-display-container 
            w3-center">
                <p id="text-spinner" hidden>
                    <i class="fa fa-spinner w3-spin" style="font-
                    size:64px"></i>
                </p>
                <p class="w3-opacity"><b>Transcription:</b></p>
                <div id="transcription"></div>
                <hr>
                <p class="w3-opacity"><b>Translation:</b></p>
                <div id="translation"></div>
            </div>
        </div>

        <div class="w3-container w3-content">
            <p class="w3-opacity"><b>Translation Audio</b></p>
            <div class="w3-panel w3-white w3-card w3-display-container 
            w3-center">
                <p id="audio-spinner" hidden>
                    <i class="fa fa-spinner w3-spin" style="font-
                     size:64px"></i>
                </p>
                <audio id="translation-player" controls>
                    Your browser does not support the audio element...
                </audio>
            </div>
        </div>
...

在这个代码片段中,我们有以下内容:

  • 我们创建了网页用户界面的“翻译文本”和“翻译音频”部分。

  • 在“翻译文本”部分,我们放置了一个初始时隐藏的旋转图标,并且有几个<div>将用来显示翻译结果。

  • 在“翻译音频”部分,我们放置了另一个初始时隐藏的旋转图标,以及一个音频播放器,用于播放翻译后的音频。

scripts.js

以下是scripts.js文件。许多“通用翻译器”的动态行为是通过 JavaScript 实现的。scripts.js与端点进行交互,构建应用程序的整体用户体验:

"use strict";

const serverUrl = "http://127.0.0.1:8000";

...

class HttpError extends Error {
    constructor(response) {
        super(`${response.status} for ${response.url}`);
        this.name = "HttpError";
        this.response = response;
    }
}

这个片段将serverUrl定义为chalice local的地址,并定义了HttpError来处理可能在 HTTP 请求中发生的异常:

let audioRecorder;
let recordedAudio;

const maxAudioLength = 30000;
let audioFile = {};

const mediaConstraints = {
 audio: true
};

navigator.getUserMedia(mediaConstraints, onMediaSuccess, onMediaError);

function onMediaSuccess(audioStream) {
 audioRecorder = new MediaStreamRecorder(audioStream);
 audioRecorder.mimeType = "audio/wav";
 audioRecorder.ondataavailable = handleAudioData;
}

function onMediaError(error) {
 alert("audio recording not available: " + error.message);
}

function startRecording() {
 recordedAudio = [];
 audioRecorder.start(maxAudioLength);
}

function stopRecording() {
 audioRecorder.stop();
}

function handleAudioData(audioRecording) {
 audioRecorder.stop();
 audioFile = new File([audioRecording], "recorded_audio.wav", {type: "audio/wav"});

 let audioElem = document.getElementById("recording-player");
 audioElem.src = window.URL.createObjectURL(audioRecording);
}

这个代码片段遵循了github.com/intercom/MediaStreamRecorder中推荐的音频录制实现,我们将不在此详细介绍。这里有一些细节需要注意:

  • Universal Translator 支持最多 30 秒的音频录制,长度由maxAudioLength常量定义。这个时长足以翻译简短的短语。

  • 音频录制格式被设置为audio/wav,这是 Amazon Transcribe 支持的格式之一。

  • 当音频录制完成后,我们执行两个任务:

    • 我们将录制的音频数据放入一个 JavaScript 文件对象中,文件名为recorded_audio.wav;这将是上传到 S3 的录音文件名。由于所有录音的文件名相同,因此上传新的录音时会替换掉之前上传的录音。

    • 我们在录音音频部分更新音频播放器,使用录音音频的 Object URL 进行播放。

let isRecording = false;

function toggleRecording() {
    let toggleBtn = document.getElementById("record-toggle");
    let translateBtn = document.getElementById("translate");

    if (isRecording) {
        toggleBtn.value = 'Record';
        translateBtn.disabled = false;
        stopRecording();
    } else {
        toggleBtn.value = 'Stop';
        translateBtn.disabled = true;
        startRecording();
    }

    isRecording = !isRecording;
}

scripts.js中的toggleRecording函数使音频播放器下方的第一个输入按钮成为一个切换按钮。此切换按钮通过前面的MediaStreamRecorder实现开始或停止音频录制。

接下来,我们定义五个函数:

  • uploadRecording():通过 Base64 编码将音频录音上传到我们的上传录音端点。

  • translateRecording():调用我们的翻译录音端点来翻译音频录音。

  • updateTranslation():更新翻译文本部分,显示返回的转录和翻译文本。

  • synthesizeTranslation():调用我们的语音合成端点生成翻译文本的音频语音。

  • updateTranslationAudio():更新翻译音频部分的音频播放器,将音频语音的 URL 加载进去以启用播放。

这些函数对应于翻译用户体验的顺序步骤。我们将它们拆分为单独的函数,使 JavaScript 代码更加模块化和可读;每个函数执行一个特定的任务。让我们逐个了解这些函数的实现细节。

让我们来看一下uploadRecording()函数,如下所示的代码块所示:

async function uploadRecording() {
    // encode recording file as base64 string for upload
    let converter = new Promise(function(resolve, reject) {
        const reader = new FileReader();
        reader.readAsDataURL(audioFile);
        reader.onload = () => resolve(reader.result
            .toString().replace(/^data:(.*,)?/, ''));
        reader.onerror = (error) => reject(error);
    });
    let encodedString = await converter;

    // make server call to upload image
    // and return the server upload promise
    return fetch(serverUrl + "/recordings", {
        method: "POST",
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({filename: audioFile.name, filebytes: encodedString})
    }).then(response => {
        if (response.ok) {
            return response.json();
        } else {
            throw new HttpError(response);
        }
    })
}

在前面的代码中,我们有以下内容:

  • uploadRecording()函数通过将文件对象中的音频录音转换为 Base64 编码的字符串来创建上传数据。

  • 该函数格式化 JSON 负载,以包括我们端点所期望的filenamefilebytes

  • 然后,它向我们的上传录音端点 URL 发送 HTTP POST 请求,并返回 JSON 响应。

  • 这与 Pictorial Translator 应用中的uploadImage()函数几乎相同,唯一的区别是文件来自音频录制器。

让我们来看一下translateRecording()函数,如下所示的代码块所示:

let fromLang;
let toLang;

function translateRecording(audio) {
    let fromLangElem = document.getElementById("fromLang");
    fromLang = fromLangElem[fromLangElem.selectedIndex].value;
    let toLangElem = document.getElementById("toLang");
    toLang = toLangElem[toLangElem.selectedIndex].value;

    // start translation text spinner
    let textSpinner = document.getElementById("text-spinner");
    textSpinner.hidden = false;

    // make server call to transcribe recorded audio
    return fetch(serverUrl + "/recordings/" + audio["fileId"] + "/translate-text", {
        method: "POST",
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({fromLang: fromLang, toLang: toLang})
    }).then(response => {
        if (response.ok) {
            return response.json();
        } else {
            throw new HttpError(response);
        }
    })
}

在前面的代码中,我们有以下内容:

  • translateRecording()函数首先从网页用户界面的fromLangtoLang下拉菜单中获取语言的值。

  • 该函数启动旋转器,向用户发出开始翻译过程的信号。然后它调用我们的翻译录音端点,并等待响应。

让我们来看一下updateTranslation()函数,如下代码块所示:

function updateTranslation(translation) {
    // stop translation text spinner
    let textSpinner = document.getElementById("text-spinner");
    textSpinner.hidden = true;

    let transcriptionElem = document.getElementById("transcription");
    transcriptionElem.appendChild(document.createTextNode(translation["text"]));

    let translationElem = document.getElementById("translation");
    translationElem.appendChild(document.createTextNode(translation["translation"]
                                                        ["translatedText"]));

    return translation
}

在前面的代码中,以下内容适用:

  • 当翻译录音端点响应时,updateTranslation()函数会隐藏旋转指示器。

  • 该函数随后会更新Translation Text部分,包含转录和翻译文本。

让我们来看一下synthesizeTranslation()函数,如下代码块所示:

function synthesizeTranslation(translation) {
    // start translation audio spinner
    let audioSpinner = document.getElementById("audio-spinner");
    audioSpinner.hidden = false;

    // make server call to synthesize translation audio
    return fetch(serverUrl + "/synthesize_speech", {
        method: "POST",
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({text: translation["translation"]["translatedText"], language: toLang})
    }).then(response => {
        if (response.ok) {
            return response.json();
        } else {
            throw new HttpError(response);
        }
    })
}

在前面的代码中,我们有如下内容:

  • synthesizeTranslation()函数启动旋转指示器,以通知用户开始进行语音合成过程。

  • 该函数随后会调用语音合成端点并等待响应。请记住,该端点期望的是 JSON 参数,而这些参数在fetch()调用中设置。

让我们来看一下updateTranslationAudio()函数,如下代码块所示:

function updateTranslationAudio(audio) {
    // stop translation audio spinner
    let audioSpinner = document.getElementById("audio-spinner");
    audioSpinner.hidden = true;

    let audioElem = document.getElementById("translation-player");
    audioElem.src = audio["audioUrl"];
}

在前面的代码中,我们有如下内容:

  • 当语音合成端点响应时,updateTranslationAudio()函数会停止语音合成的旋转指示器。

  • 该函数随后会使用合成的翻译音频的 URL 更新音频播放器。

所有前面提到的五个函数都通过uploadAndTranslate()函数将其串联在一起,如下所示:

function uploadAndTranslate() {
    let toggleBtn = document.getElementById("record-toggle");
    toggleBtn.disabled = true;
    let translateBtn = document.getElementById("translate");
    translateBtn.disabled = true;

    uploadRecording()
        .then(audio => translateRecording(audio))
        .then(translation => updateTranslation(translation))
        .then(translation => synthesizeTranslation(translation))
        .then(audio => updateTranslationAudio(audio))
        .catch(error => {
            alert("Error: " + error);
        })

    toggleBtn.disabled = false;
}

注意uploadAndTranslate()函数中的事件顺序是多么清晰。作为该函数的最终步骤,我们启用了记录切换按钮,以便用户可以开始下一次翻译。

通用翻译器应用程序的最终项目结构应如下所示:

├── Capabilities
│ ├── app.py
│ ├── chalicelib
│ │ ├── __init__.py
│ │ ├── speech_service.py
│ │ ├── storage_service.py
│ │ ├── transcription_service.py
│ │ └── translation_service.py
│ └── requirements.txt
├── Pipfile
├── Pipfile.lock
└── Website
    ├── index.html
    └── scripts.js

现在,我们已经完成了通用翻译器应用程序的实现。

将通用翻译器部署到 AWS

通用翻译器应用程序的部署步骤与前几章项目的部署步骤相同。我们在此包含它们以便完整性。

  1. 首先,让我们通过在项目结构的.chalice目录中的config.json文件中将"autogen_policy"设置为false来告诉 Chalice 为我们执行策略分析:
{
  "version": "2.0",
  "app_name": "Capabilities",
  "stages": {
    "dev": {
      "autogen_policy": false,
      "api_gateway_stage": "api"
    }
  }
}
  1. 接下来,我们在chalice目录中创建一个新的文件policy-dev.json,手动指定项目需要的 AWS 服务:
{
 "Version": "2012-10-17",
 "Statement": [
 {
 "Effect": "Allow",
 "Action": [
 "logs:CreateLogGroup",
 "logs:CreateLogStream",
 "logs:PutLogEvents",
 "s3:*",
 "translate:*",
 "transcribe:*",
 "polly:*"
 ],
 "Resource": "*"
 }
 ]
}
  1. 接下来,我们通过在Capabilities目录中运行以下命令将 Chalice 后端部署到 AWS:
$ chalice deploy
Creating deployment package.
Creating IAM role: Capabilities-dev
Creating lambda function: Capabilities-dev
Creating Rest API
Resources deployed:
  - Lambda ARN: arn:aws:lambda:us-east-1:<UID>:function:Capabilities-dev
  - Rest API URL: https://<UID>.execute-api.us-east-1.amazonaws.com/api/

部署完成后,Chalice 会输出一个类似于https://<UID>.execute-api.us-east-1.amazonaws.com/api/的 RESTful API URL,其中<UID>是一个唯一的标识符字符串。这就是前端应用程序应该访问的服务器 URL,用于连接在 AWS 上运行的应用程序后端。

  1. 接下来,我们将index.htmlscripts.js文件上传到这个 S3 桶中,并设置权限为公开可读。在此之前,我们需要在scripts.js中做如下更改。请记住,网站现在将在云端运行,并且无法访问我们的本地 HTTP 服务器。将本地服务器的 URL 替换为来自后端部署的 URL:
"use strict";
const serverUrl = "https://<UID>.execute-api.us-east-1.amazonaws.com/api";
...

现在,通用翻译器应用程序可以通过互联网让每个人都能访问!

讨论项目增强想法

在第二部分的每个动手项目结束时,我们都会提供一些想法,以扩展智能启用应用程序。以下是增强通用翻译器的一些想法:

  • 允许用户在应用程序中保存默认的源语言和输出语音偏好设置。用户可能会将母语作为源语言,并可能希望翻译后的语音与其性别和声音相匹配。

  • 使用 Amazon Transcribe 的流式转录功能添加实时转录。此功能可以大大减少用户等待语音翻译的时间。在撰写本文时,Python SDK 尚不支持此功能,因此您的实现将需要使用其他 SDK。我们的架构确实支持多语言系统,即用多种语言编写的系统。

  • 通用翻译器和图像翻译器都提供翻译功能。这两种翻译功能可以结合成一个单一的应用程序,供旅行者和学生使用,尤其是一个在现实世界中始终伴随用户的移动应用。

总结

在本章中,我们构建了一个通用翻译器应用程序,用于将口语从一种语言翻译成另一种语言。我们结合了来自 AWS AI 服务的语音转文本、语言翻译和文本转语音能力,包括 Amazon Transcribe、Amazon Translate 和 Amazon Polly。这个动手项目继续了我们作为 AI 从业者的旅程,培养了我们开发实际 AI 应用程序所需的技能和直觉。在这个过程中,我们还讨论了通用翻译器应用程序的用户体验和产品设计决策。此外,我们展示了在 第二章《现代 AI 应用程序的构成》定义的参考架构中,翻译服务和存储服务的干净代码重用,现代 AI 应用程序的架构

在下一章,我们将利用更多的 AWS AI 服务来创建能够简化我们生活的解决方案。成为一名 AI 从业者不仅仅是了解使用哪些服务或 API,还需要具备将优秀的产品和架构设计与 AI 能力融合的技能。

参考文献

关于使用 Amazon Transcribe 和 Amazon Polly 进行语音转文本及反向操作的更多信息,请参考以下链接:

第五章:使用 Amazon Comprehend 提取文本中的信息

本章中,我们将构建一个应用程序,能够自动从名片照片中提取联系信息。通过这个应用程序,我们旨在通过自动化减少繁琐的手工工作。我们将使用 Amazon Rekognition 来检测名片照片中的文本,然后使用 Amazon Comprehend 提取结构化信息,如姓名、地址和电话号码。我们将展示自动化的目标并不总是完全的自主化;在解决方案中保持人的参与也具有一定的价值。

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

  • 了解人工智能在我们工作场所中的作用

  • 使用 Amazon Comprehend 和 Amazon Comprehend Medical 执行信息提取

  • 在 AWS DynamoDB 中存储和检索数据

  • 使用 AWS 服务、RESTful API 和网页用户界面构建无服务器 AI 应用程序

  • 在参考架构中重用现有的 AI 服务实现

  • 讨论自动化解决方案中的人机协同界面设计

技术要求

本书的 GitHub 仓库,包含本章的源代码,可以在 github.com/PacktPublishing/Hands-On-Artificial-Intelligence-on-Amazon-Web-Services 找到。

与你的人工智能同事合作

人工智能 (AI) 正在推动我们生活中自动化的进步。当大多数人想到智能自动化时,他们通常会想到智能温控器、吸尘机器人或自动驾驶汽车,这些都帮助我们过上更好的生活。我们也有巨大的机会利用智能自动化来帮助我们更高效地工作。人工智能可以在工作场所中补充人类劳动,为企业创造价值,促进经济增长,并将人类劳动转向创造性工作。一个迫切需要自动化进步的领域是手工的后勤处理过程。当我们存入支票、注册服务或在线购物时,仍然有许多任务是由人类在幕后完成的。

关于工作被自动化取代的担忧是存在的;然而,我们也观察到,当琐碎的工作被自动化时,工人的士气得到了改善。大多数手动后勤工作都是乏味且重复的。例如,有些人的工作是阅读多份文档,识别其中的某些信息,然后手动将信息输入计算机系统。这些后勤文档处理任务也被称为“旋转椅”过程,因为工人们不断在文件和计算机屏幕之间旋转椅子。我们可以利用 AI 来自动化文档处理过程,通过光学字符识别OCR)读取文档,再利用自然语言处理NLP)提取信息。

然而,自动化文档处理并非易事。纸质文档必须先进行扫描。根据文档图像的质量、文档结构的复杂性,甚至是文档中的手写文本,可能很难保证处理的准确性。对于某些业务环境和使用案例,任何低于 100%的准确性都是不可接受的。在这种情况下,自动化开发人员必须设计可支持人工干预的容错机制,以便在出现问题时,人工可以介入并接管处理。例如,自动化解决方案可以在银行存款过程中提取支票上的金额。如果该过程中的数据不准确,可能会对银行客户造成重大后果。为了确保正确的存款金额,自动化解决方案可以先提取金额,然后在存款完成之前将提取的金额展示给人工操作员进行确认。这个解决方案利用了 AI 技术来自动化任务,但也允许人在出错时进行干预。

在本章中,我们将实现一个名为“联系人管理器”的应用程序,用于自动化文档处理。更具体地说,该应用程序帮助我们从扫描的名片中提取联系人信息。为了确保准确性,我们的应用程序将提供一个“人机交互”用户界面,以便用户在保存信息之前,能够审核并更正自动提取的内容。这种“人机交互”用户界面是一种流行的方式,因为它通过人的判断来提高自动化的准确性。

了解联系人管理器架构

联系人管理器应用程序将为用户提供一个 Web 用户界面,用户可以通过该界面上传名片图像。应用程序将提取并分类联系人信息。然后,自动提取的联系人信息将在 Web 用户界面中显示给用户。用户可以在将其保存到永久联系人库之前,审查并更正这些信息。

以下图示展示了架构设计,重点突出联系组织器应用的各个层次和服务。以下架构设计现在应该很熟悉了;这些层次和组件遵循我们在第二章中定义的相同参考架构模板,现代 AI 应用的构成

在这个应用中,Web 用户界面将与协调层中的三个 RESTful 端点进行交互:

  • 上传录音端点将把图像上传委托给我们的存储服务

  • 提取信息端点将使用识别服务提取服务

    • 识别服务是从第三章中重用的,使用 Amazon Rekognition 和 Translate 检测和翻译文本,当时我们在讲解图像翻译器项目时使用过。

    • 提取服务将同时使用 Amazon Comprehend 和 Amazon Comprehend Medical 来提取和分类各种联系信息,如姓名、地址和电话号码。

  • 保存/获取联系人端点将写入/读取联系人存储,其后端是 AWS DynamoDB NoSQL 数据库。

在联系组织器中,我们有几个机会可以重用我们在之前项目中已经实现的组件。在协调层中,我们可以重用上传录音端点。在服务实现层中,我们可以重用存储和识别服务。

联系组织器中的组件交互

以下交互图展示了联系组织器中应用组件之间的业务逻辑工作流程:

从用户的角度来看,我们有以下内容:

  1. 当联系组织器 Web 用户界面首次加载时,它将获取并显示所有现有的联系人。

  2. 用户随后可以通过 Web 用户界面上传名片照片。

  3. 上传完成后,开始两个步骤:

    1. 上传的名片图像会显示在用户界面中。

    2. 自动化的联系信息提取过程已开始。

  4. 当信息提取完成后,提取的信息会展示给用户,以供审核和修正。

  5. 用户在审核并修正信息后,点击保存按钮后,联系信息可以被持久化。

我们设计了联系组织器,使其具有一个人机交互的用户界面,如下所示:

  1. 上传的名片图像会重新显示给用户,以便他们查看原始联系信息。

  2. 自动提取的联系信息也会与名片图像一起展示给用户。

  3. 用户可以选择从用户界面中更改或修正任何提取的信息。

  4. 用户必须明确点击“保存”按钮,作为确认联系信息正确的人的确认。

这个人机协作的用户界面也不应该仅仅是智能解决方案中的附加部分。我们评估此类界面设计的经验法则是,即使没有与用户界面一起存在的 AI 功能,解决方案也应该是完全可用的。

设置项目结构

创建一个类似于我们在第二章中概述的基础项目结构,现代人工智能应用程序的结构,包括pipenvchalice和网页文件:

  1. 在终端中,我们将创建root项目目录并进入该目录,使用以下命令:
$ mkdir ContactOrganizer
$ cd ContactOrganizer
  1. 我们将通过创建一个名为Website的目录来为 Web 前端创建占位符。在此目录中,我们将创建两个文件,index.htmlscripts.js,如下代码所示:
$ mkdir Website
$ touch Website/index.html
$ touch Website/scripts.js
  1. 我们将在项目的根目录中创建一个 Python 3 虚拟环境,并使用pipenv。项目中的 Python 部分需要两个包,boto3chalice。我们可以使用以下命令安装它们:
$ pipenv --three
$ pipenv install boto3
$ pipenv install chalice
  1. 请记住,通过pipenv安装的 Python 包仅在我们激活虚拟环境时才可用。实现这一点的一种方法是使用以下命令:
$ pipenv shell
  1. 接下来,仍在虚拟环境中,我们将创建一个名为Capabilities的 AWS Chalice 项目,作为协调层,使用以下命令:
$ chalice new-project Capabilities
  1. 要创建chalicelib Python 包,请执行以下命令:
cd Capabilities
mkdir chalicelib
touch chalicelib/__init__.py
cd ..

“联系人组织器”的初始项目结构应如下所示:

Project Structure
------------
├── ContactOrganizer/
    ├── Capabilities/
        ├── .chalice/
            ├── config.json
        ├── chalicelib/
            ├── __init__.py
        ├── app.py
        ├── requirements.txt
    ├── Website/
        ├── index.html
        ├── script.js
    ├── Pipfile
    ├── Pipfile.lock

这个“联系人组织器”项目结构包含了我们在第二章中定义的 AI 应用程序架构的用户界面、协调和服务实现层,现代人工智能应用程序的结构

实现服务

让我们逐层实现联系人组织器,从包含关键 AI 功能的服务实现开始。我们在这个项目中需要的许多功能,如检测图像中的文本和处理文件上传,都在之前的项目中实现过。具有真正新功能的服务是提取服务和联系人存储。

识别服务 - 文本检测

再次,我们将利用 Amazon Rekognition 服务提供检测图像中文本的功能。我们可以重用我们在第三章的图像翻译项目中实现的相同 Recognition 服务,使用 Amazon Rekognition 和 Translate 检测与翻译文本,如下代码所示:

import boto3

class RecognitionService:
    def __init__(self, storage_location):
        self.client = boto3.client('rekognition')
        self.bucket_name = storage_location

    def detect_text(self, file_name):
        response = self.client.detect_text(
            Image = {
                'S3Object': {
                    'Bucket': self.bucket_name,
                    'Name': file_name
                }
            }
        )

        lines = []
        for detection in response['TextDetections']:
            if detection['Type'] == 'LINE':
                lines.append({
                    'text': detection['DetectedText'],
                    'confidence': detection['Confidence'],
                    'boundingBox': detection['Geometry']['BoundingBox']
                })

        return lines

有关其实现和识别服务设计选择的更多细节,请参阅第三章,使用 Amazon Rekognition 和 Translate 检测和翻译文本

有一个新的 Amazon Textract 服务,可以自动从扫描文档中提取文本和数据。Amazon Textract 可能在提取名片文本方面同样有效,但有一些需要考虑的事项。尽管名片类似于文档,但我们的应用程序处理的是名片的照片,而非扫描图像。

Amazon Textract 现在已全面推出;由于其文本提取能力,取代 AWS Rekognition 将是一个很好的功能增强练习,适合用于此动手项目。想一想,我们架构中哪些组件和交互会受到这种变化的影响。

提取服务 – 联系信息提取

我们将利用 Amazon Comprehend 来提取从名片上检测到的文本中的联系信息。首先,让我们使用 AWS CLI 探索该服务。

联系人组织者需要从我们的名片中提取信息。通常,名片上的文本包含个人的姓名、职位、组织、地址、电话号码、电子邮件等信息。

以下是一个虚构联系人的示例文本:

AI Enterprise Inc.
John Smith
Senior Software Engineer
123 Main Street Washington D.C. 20001
john.smith@aienterprise.com
(202) 123-4567

让我们看看 Amazon Comprehend 能从这个示例文本中提取什么。执行以下 AWS CLI 命令:

$ aws comprehend detect-entities --language-code en --text "AI Enterprise Inc. John Smith Senior Software Engineer 123 Main Street Washington D.C. 20001 john.smith@aienterprise.com (202) 123-4567"
{
    "Entities": [
        {
            "Score": 0.8652380108833313,
            "Type": "ORGANIZATION",
            "Text": "AI Enterprise Inc",
            ...
        },
        {
            "Score": 0.9714182019233704,
            "Type": "PERSON",
            "Text": "John Smith",
            ...
        },
        {
            "Score": 0.9006084203720093,
            "Type": "LOCATION",
            "Text": "123 Main Street Washington D.C.",
            ...
        },
        {
            "Score": 0.48333245515823364,
            "Type": "DATE",
            "Text": "20001",
            ...
        },
        {
            "Score": 0.998563826084137,
            "Type": "OTHER",
            "Text": "john.smith@aienterprise.com",
            ...
        },
        {
            "Score": 0.9999305009841919,
            "Type": "OTHER",
            "Text": "(202) 123-4567",
            ...
        }
    ]
}

Amazon Comprehend 提取了一些信息,包括组织(ORGANIZATION)、个人姓名(PERSON)和地址(LOCATION)。然而,AWS Comprehend 将电子邮件和电话号码提取为OTHER,错误地将邮政编码提取为DATE,并且未能提取职位信息。

尽管提取结果并不完美,但我们的联系人组织者应用程序仍然可以利用这些结果,减少用户的手动工作量。

有一种方法可以改进这些信息提取结果。亚马逊提供了 Comprehend 服务的另一个变种,称为 AWS Comprehend Medical。该变种专门用于从各种医学文档中提取信息。

其特点之一是提取受保护的健康信息PHI),例如姓名、年龄、地址、电话号码和电子邮件。我们可以利用此功能来完成名片信息提取任务。

让我们看看这个功能在我们之前查看的相同示例文本上表现如何。执行以下 AWS CLI 命令:

aws comprehendmedical detect-phi --text "AI Enterprise Inc. John Smith Software Engineer 123 Main Street Washington D.C. 20001 john.smith@aienterprise.com (202) 123-4567"
{
    "Entities": [
        {
            "Text": "AI Enterprise Inc",
            "Category": "PROTECTED_HEALTH_INFORMATION",
            "Type": "ADDRESS",
            ...
        },
        {
            "Text": "John Smith",
            "Category": "PROTECTED_HEALTH_INFORMATION",
            "Type": "NAME",
            ...
        },
        {
            "Text": "Software Engineer",
            "Category": "PROTECTED_HEALTH_INFORMATION",
            "Type": "PROFESSION",
            ...
        },
        {
            "Text": "123 Main Street Washington D.C. 20001",
            "Category": "PROTECTED_HEALTH_INFORMATION",
            "Type": "ADDRESS",
            ...
        },
        {
            "Text": "john.smith@aienterprise.com",
            "Category": "PROTECTED_HEALTH_INFORMATION",
            "Type": "EMAIL",
            ...
        },
        {
            "Text": "(202) 123-4567",
            "Category": "PROTECTED_HEALTH_INFORMATION",
            "Type": "PHONE_OR_FAX",
            ...
        }
    ]
}

Amazon Comprehend Medical 提取了与其非医学对应物大部分相同的信息。此外,它还提取了职位(PROFESSION)、电话号码(PHONE_OR_FAX)和电子邮件(EMAIL)。所提取的地址(ADDRESS)似乎比非医学变体更准确。当我们结合 Comprehend 服务的两个变体的结果时,我们能够提取典型商务卡上的联系信息。

有了这些见解,让我们来实现我们的抽取服务。我们将在chalicelib目录下的extraction_service.py文件中创建一个名为ExtractionService的 Python 类:

import boto3
from collections import defaultdict
import usaddress

class ExtractionService:
    def __init__(self):
        self.comprehend = boto3.client('comprehend')
        self.comprehend_med = boto3.client('comprehendmedical')

    def extract_contact_info(self, contact_string):
        ...

此代码摘录显示了服务所需的导入以及构造函数方法,该方法为 Amazon Comprehend 和 Amazon Comprehend Medical 服务分别实例化了两个boto3客户端。

现在让我们看看如何使用这两个服务来实现extract_contact_info()方法:

    def extract_contact_info(self, contact_string):
        contact_info = defaultdict(list)

        # extract info with comprehend
        response = self.comprehend.detect_entities(
            Text = contact_string,
            LanguageCode = 'en'
        )

        for entity in response['Entities']:
            if entity['Type'] == 'PERSON':
                contact_info['name'].append(entity['Text'])
            elif entity['Type'] == 'ORGANIZATION':
                contact_info['organization'].append(entity['Text'])

        # extract info with comprehend medical
        response = self.comprehend_med.detect_phi(
            Text = contact_string
        )

        for entity in response['Entities']:
            if entity['Type'] == 'EMAIL':
                contact_info['email'].append(entity['Text'])
            elif entity['Type'] == 'PHONE_OR_FAX':
                contact_info['phone'].append(entity['Text'])
            elif entity['Type'] == 'PROFESSION':
                contact_info['title'].append(entity['Text'])
            elif entity['Type'] == 'ADDRESS':
                contact_info['address'].append(entity['Text'])

        # additional processing for address
        address_string = ' '.join(contact_info['address'])
        address_parts = usaddress.parse(address_string)

        for part in address_parts:
            if part[1] == 'PlaceName':
                contact_info['city'].append(part[0])
            elif part[1] == 'StateName':
                contact_info['state'].append(part[0])
            elif part[1] == 'ZipCode':
                contact_info['zip'].append(part[0])

        return dict(contact_info)

在上述代码中,我们可以看到以下内容:

  • extract_contact_info()方法通过boto3调用了两个 Amazon Comprehend 变体。来自两个调用的结果被处理并存储在contact_info字典中。

  • contact_info被声明为defaultdict(list),这是一种字典数据结构,其值默认为空列表。

实际上,对于给定类型,可能会提取多个结果。例如,单个商务卡可能提取两个电话号码。这可能出现三种情况,正如我们在使用案例中观察到的那样:

  • 第一个原因适用于给定类型实际上存在多个信息片段的情况。例如,在商务卡上可能有电话号码和传真号码。

  • 第二个原因是信息是较简单信息的组合。例如,许多职位实际上包括角色名称、职位级别和特长。

  • 第三个原因是 Amazon Comprehend 服务在提取过程中可能出现的不准确性。例如,地址中的邮政编码可能被错误地分类为电话号码。

对两个 AWS Comprehend 变体的两次调用如下:

  • 首次调用是对 Amazon Comprehend 客户端的detect_entities()函数。从响应中,我们将名称和组织存储在contact_info中。

  • 第二次调用是对 Amazon Comprehend Medical 客户端的detect_phi()函数。从响应中,我们将电子邮件、电话号码、职位和地址存储在contact_info中。

如果每种类型存在多个结果,则将它们附加到defaultdict(list)数据结构中相应列表中。

AWS Comprehend 将地址提取为一个整体信息。然而,将地址的不同部分(如城市、州和邮政编码)分别存储会更加有用。这将使得联系人信息的组织、搜索和展示更加方便。在 extract_contact_info() 方法中,我们还使用了一个名为 usaddress 的 Python 包来尝试解析地址的各个子组件,并将它们单独存储在 contact_info 数据结构中。

最后,extract_contact_info() 方法将 contact_info 返回为一个标准的 Python 字典。

在联系人管理器应用中,用户上传名片照片。然后,应用使用 AWS Rekognition 尝试检测文本,并将检测到的文本传递给 AWS Comprehend 以尝试提取信息。地址还会经过一个后处理步骤,解析出城市、州和邮政编码。

我们可以将此过程视为一个由多个顺序步骤组成的管道;前一步的输出作为输入传递到下一步。就像一场传话游戏,最终的结果可能会因为任何步骤的输出质量而受到影响。提取的准确性取决于照片的质量、照片中文字检测的准确性、信息提取的准确性以及后处理的解析准确性。

联系人存储 – 保存和检索联系人

用户在联系人管理器中保存联系人信息后,应该能够检索到这些信息。检索数据需要数据持久性。

在联系人管理器中,我们将使用 AWS DynamoDB,这是一个高度可扩展的云端 NoSQL 数据库。DynamoDB 非常适合我们的无服务器架构,因为开发者不需要管理数据库服务器。相反,开发者可以创建会自动根据需求扩展的表格。我们将使用 DynamoDB 表格来存储和检索联系人信息。

让我们使用 AWS Web 控制台创建一个联系人表格:

  1. 进入 DynamoDB 仪表盘页面并点击“创建表”按钮:

  1. 在创建 DynamoDB 表格页面上,将表名设置为 Contacts,并将主键设置为 name。由于 DynamoDB 是一个 NoSQL 或文档数据库,我们不需要预先指定整个数据库表的模式:

  1. 完成表格设置,选择“使用默认设置”选项并点击“创建”。

就这样!你刚刚创建了一个可以处理超过每天 10 万亿次请求的 Web 规模数据库。最棒的是,你无需管理它!

对于这个简单的项目,我们使用 AWS Web 控制台创建了一个数据库表。对于企业级应用程序,我们建议遵循符合基础设施即代码IaC)的最佳实践,其中基础设施应通过代码或配置自动配置和管理,而不是通过 AWS Web 控制台等手动设置。

其中的好处包括从灾难性事件中快速恢复、快速实验新功能、以及记录系统环境设置等。Boto3 允许您编写 Python 代码来创建和配置 DynamoDB 表。AWS CloudFormation 还允许自动创建和配置 DynamoDB 以及更多 AWS 服务。

现在Contacts表已创建,让我们实现我们的ContactStore服务。在chalicelib目录下的contact_store.py文件中创建一个名为ContactStore的 Python 类:

import boto3

class ContactStore:
    def __init__(self, store_location):
        self.table = boto3.resource('dynamodb').Table(store_location)

    def save_contact(self, contact_info):
        response = self.table.put_item(
            Item = contact_info
        )
        # should return values from dynamodb however,
        # dynamodb does not support ReturnValues = ALL_NEW
        return contact_info

    def get_all_contacts(self):
        response = self.table.scan()

        contact_info_list = []
        for item in response['Items']:
            contact_info_list.append(item)

        return contact_info_list

在前面的代码中,我们有以下内容:

  • 构造函数__init__()为 DynamoDB 创建了一个boto3源,以便获取我们的Contacts表。构造函数接受一个名为store_location的参数,它在我们的实现中作为表名。

  • save_contact()方法接受一个包含联系信息的 Python 字典数据结构,并使用put_item()函数存储联系人,该函数接受要插入表中的项。

  • save_contact()中,我们将contact_info数据对象返回给调用者。我们尝试遵循 RESTful API 约定:当 API 创建一个新资源(联系人)时,应返回更新后的资源(联系人):

    • RESTful 约定建议在创建资源后返回资源状态的新表示。例如,可能为资源创建了一个新的 ID。然而,boto3中的put_item()函数当前不会返回资源的新值。对于联系人管理器来说,这没有问题,因为我们选择使用“名称”作为联系人键或 ID。
  • get_all_contacts()方法通过scan()函数检索所有已保存到 DynamoDB 中的联系人。只提供表名的情况下,scan()函数将返回表中的所有项目。

存储服务——上传和检索文件

我们可以重用之前项目中StorageService的相同实现。我们仅提供当前项目所需的方法,如下所示:

import boto3

class StorageService:
    def __init__(self, storage_location):
        self.client = boto3.client('s3')
        self.bucket_name = storage_location
    def upload_file(self, file_bytes, file_name):
        self.client.put_object(Bucket = self.bucket_name,
                               Body = file_bytes,
                               Key = file_name,
                               ACL = 'public-read')

        return {'fileId': file_name,
                'fileUrl': "http://" + self.bucket_name + ".s3.amazonaws.com/" + file_name}

有关实现和设计选择的更多细节,请参见第三章,使用 Amazon Rekognition 和 Translate 检测和翻译文本

实现 RESTful 端点

让我们进入编排层,以便将我们在服务中实现的各种功能整合在一起。RESTful 端点为用户界面层提供了 HTTP 访问,以便访问业务功能。

正如我们之前所说,编排层应该简洁且易于理解。RESTful 端点应只关注编排服务,以形成更高层次的业务逻辑并处理 HTTP 协议细节。

评估编排层或 RESTful 端点在关注点分离方面是否设计良好的一种方法是检查包的导入。编排层是否需要导入来自服务的包?

例如,在我们的项目中,RESTful 端点是否导入了与 AWS 交互的 boto3?它们不应该。

通常,RESTful 端点会导入服务实现(storage_servicerecognition_service)、与编程框架相关的包(chalice)以及与协议相关的包(JSONCGI)。

用以下代码替换 Chalice 项目中的 app.py 内容:

from chalice import Chalice
from chalicelib import storage_service
from chalicelib import recognition_service
from chalicelib import extraction_service
from chalicelib import contact_store

import base64
import json

#####
# chalice app configuration
#####
app = Chalice(app_name='Capabilities')
app.debug = True

#####
# services initialization
#####
storage_location = 'contents.aws.ai'
storage_service = storage_service.StorageService(storage_location)
recognition_service = recognition_service.RecognitionService(storage_location)
extraction_service = extraction_service.ExtractionService()
store_location = 'Contacts'
contact_store = contact_store.ContactStore(store_location)

#####
# RESTful endpoints
#####
...

前面的代码片段处理了所有包的导入、Chalice 应用配置以及我们四个服务的实例化。

提取图像信息端点

extract_image_info()函数实现了 RESTful 端点。使用以下代码继续在app.py中的 Python 代码:

@app.route('/images/{image_id}/extract-info', methods = ['POST'], cors = True)
def extract_image_info(image_id):
    """detects text in the specified image then extracts contact information from the text"""
    MIN_CONFIDENCE = 70.0

    text_lines = recognition_service.detect_text(image_id)

    contact_lines = []
    for line in text_lines:
        # check confidence
        if float(line['confidence']) >= MIN_CONFIDENCE:
            contact_lines.append(line['text'])

    contact_string = '   '.join(contact_lines)
    contact_info = extraction_service.extract_contact_info(contact_string)

    return contact_info

该注释位于此函数上方,描述了可以访问此端点的 HTTP 请求:

POST <server url>/images/{image_id}/extracted-info

在前面的代码中,我们有以下内容:

  • extract_image_info() 函数中,我们调用 RecognitionService 来检测图像中的文本,并将检测到的文本行存储在 text_lines 中。

  • 然后,我们构建一个字符串 contact_string,其中包含所有检测到的文本行,且其置信度高于MIN_CONFIDENCE,该值被设置为70.0

    • 这个 contact_string 是通过将检测到的文本行用三个空格连接起来构建的。我们选择三个空格作为分隔符,因为检测到的行更有可能是相关信息,我们通过额外的空格向提取服务提示这种关系。
  • 然后,我们调用提取服务的extract_contact_info()方法并返回联系信息。请记住,extract_contact_info()不仅调用了 Amazon Comprehend 服务的两个变体,它还使用了usaddress Python 包来解析地址的各个部分。

让我们通过在 Python 虚拟环境中运行 chalice local 来测试这个端点,然后发出以下 curl 命令。接下来,我们将指定一个已经上传到 S3 存储桶中的图像:

curl -X POST http://127.0.0.1:8000/images/<uploaded image>/extract-info
{
    "organization":[
        "<organization>"
    ],
    "name":[
        "<name>"
    ],
    "title":[
        "<title>"
    ],
    "address":[
        "<address>"
    ],
    "phone":[
        "<phone>"
    ],
    "email":[
        "<email>"
    ]
}

这是我们的 Web 用户界面将接收的 JSON,并用于向用户显示翻译内容。

保存联系人和获取所有联系人端点

保存联系人和获取所有联系人端点通过联系存储服务处理联系人信息的保存和检索:

@app.route('/contacts', methods = ['POST'], cors = True)
def save_contact():
    """saves contact information to the contact store service"""
    request_data = json.loads(app.current_request.raw_body)

    contact = contact_store.save_contact(request_data)

    return contact

@app.route('/contacts', methods = ['GET'], cors = True)
def get_all_contacts():
    """gets all saved contacts in the contact store service"""
    contacts = contact_store.get_all_contacts()

    return contacts

它们的实现非常简单:

  • save_contact() 函数从请求体中的 JSON 参数获取联系人信息。此方法随后通过联系人存储保存该联系信息。以下代码是可以访问此端点的 HTTP 请求:
POST <server url>/contacts
{
    "name": <NAME>,
    "organization": <ORGANIZATION>,
    "title": <TITLE>,
    "address": <ADDRESS>,
    "city": <CITY>,
    "state": <STATE>,
    "zip": <ZIP>,
    "phone": <PHONE>,
    "email": <EMAIL>
}
  • get_all_contacts()方法通过联系人存储检索所有已保存的联系人。以下代码是可以访问该端点的 HTTP 请求:
GET <server url>/contacts

让我们通过一对curl命令来一起测试这些端点:

$ curl --header "Content-Type: application/json" --request POST --data '{"name": "John Smith", "organization": "AI Enterprise Inc.", "title": "Senior Software Engineer", "address": "123 Main Street", "city": "Washington D.C.", "zip": "20001", "phone": "(202) 123-4567", "email": "john.smith@aienterprise.com"}’ http://127.0.0.1:8000/contacts
{
 "name":"John Smith",
 "Organization":
 ...

$ curl http://127.0.0.1:8000/contacts
[
 {
 "city":"Washington D.C.",
 "zip":"20001",
 "organization":"AI Enterprise Inc.",
 "address":"123 Main Street",
 "email":"john.smith@aienterprise.com",
 "phone":"(202) 123-4567",
 "name":"John Smith",
 "title":"Senior Software Engineer"
 }
]

我们可以看到以下内容:

  • 第一个POST命令将联系人的表示作为响应返回,以符合 RESTful 规范。

  • 第二个GET命令获取一个包含我们刚刚保存的联系人列表的联系人信息。

这些是用于与网页用户界面交互的 JSON 格式。

上传图像端点

我们正在重新使用来自 Pictorial Translator 项目的上传图像端点的相同实现。有关此代码片段的更多实现细节和设计选择,请参阅第三章,使用 Amazon Rekognition 和 Translate 检测和翻译文本

@app.route('/images', methods = ['POST'], cors = True)
def upload_image():
    """processes file upload and saves file to storage service"""
    request_data = json.loads(app.current_request.raw_body)
    file_name = request_data['filename']
    file_bytes = base64.b64decode(request_data['filebytes'])

    image_info = storage_service.upload_file(file_bytes, file_name)

    return image_info

现在,联系信息管理器的编排层已经完成。

实现网页用户界面

接下来,我们将在index.htmlscripts.js文件中使用 HTML 和 JavaScript 创建一个简单的网页用户界面,并将它们放置在Website目录中。

以下截图显示了最终的网页用户界面:

在联系信息管理器中,用户上传名片的照片,应用程序会尽力检测卡片上的文字并提取其中的信息。然后,应用程序会用提取的信息填充输入框,供用户查看和修改。

如果针对某种类型提取了多项信息,联系信息管理器将用所有可用的信息填充该类型的输入框。例如,如果提取了多个电话号码,则电话号码输入框将填充所有可用的电话号码。

这个设计决策假设用户删除多余信息比输入缺失信息更容易。这个假设听起来是合理的;然而,它应该通过调查或用户研究来与应用程序的目标受众进行验证。为了使人机协作用户界面在竞争中稍占优势,需要微调这些设计决策。

尽管我们希望像联系信息管理器这样的应用程序能够自动提取并保存名片上的所有信息,但联系信息管理器的目的是尽可能减少繁琐的工作,同时仍然让用户参与,以确保信息的准确性。

人机协作用户界面还有另一个重要的好处。因为人类参与了纠正智能自动化所犯的错误,这是一个收集训练数据的机会,可以用于未来改进自动化技术。用户实际上是在为机器学习算法提供训练示例。记住,更好的数据总是能获胜!

Index.html

我们这里使用的是标准的 HTML 标签,因此网页的代码应该容易理解:

<!doctype html>
<html lang="en"/>

<head>
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>

    <title>Contact Organizer</title>

    <link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
    <link rel="stylesheet" href="https://www.w3schools.com/lib/w3-theme-blue-grey.css">
</head>

<body class="w3-theme-14" onload="retrieveAndDisplayContacts()">
    <div style="min-width:400px">
        <div class="w3-bar w3-large w3-theme-d4">
            <span class="w3-bar-item">Contact Organizer</span>
        </div>

        ...

        <div class="w3-container w3-content">
            <p>
                <b class="w3-opacity">My Contacts</b>
                <input class="w3-button w3-blue-grey" type="submit"
                       value="Refresh" onclick="retrieveAndDisplayContacts()"/>
            </p>
            <div class="w3-panel w3-white w3-card w3-display-container w3-center">
                <div id="contacts"/>
            </div>
        </div>

    </div>

    <script src="img/scripts.js"></script>
</body>

</html>

该 HTML 代码片段包含了网页用户界面的顶部和底部部分:

  • 当网页初次加载时,它会调用一个 JavaScript 函数,retrieveAndDisplayContacts(),从服务器加载现有联系人。此操作在<body>标签的 onload 属性中完成。

  • 当联系人从服务器返回后,retrieveAndDisplayContacts() 函数会更新 <div id="contacts"/> 以向用户显示现有联系人。

  • 该应用还为用户提供了一个刷新按钮,以便他们随时重新从服务器加载联系人:

...
<div class="w3-container w3-content">
    <p class="w3-opacity"><b>Add Contact</b></p>
    <div class="w3-panel w3-white w3-card w3-display-container w3-center">
        <div>
            <input id="file" type="file" name="file" accept="image/*"/>
            <input class="w3-button w3-blue-grey" type="submit"
                   value="Extract Info" onclick="uploadAndExtract()"/>
            <hr>
        </div>
        <div id="view" style="display: none;">
            <img id="image" width="400"/>
            <hr>
        </div>
        <div class="w3-display-container w3-left" style="width:45%">
            <fieldset>
                <legend>Information</legend>
                <p>
                    <label for="name">Name</label>
                    <input id="name" type="text" name="name"/>
                </p>
                <p>
                    <label for="title">Title</label>
                    <input id="title" type="text" name="title"/>
                </p>
                <p>
                    <label for="email">Email</label>
                    <input id="email" type="email" name="email"/>
                </p>
                <p>
                    <label for="phone">Phone</label>
                    <input id="phone" type="tel" name="phone"/>
                </p>
            </fieldset>
        </div>
        <div class="w3-display-container w3-right" style="width:50%">
            <fieldset>
                <legend>Address</legend>
                <p>
                    <label for="organization">Organization</label>
                    <input id="organization" type="text" 
                     name="organization"/>
                </p>
                <p>
                    <label for="address">Address</label>
                    <input id="address" type="text" name="address" 
                     size="30"/>
                </p>
                <p>
                    <label for="city">City</label>
                    <input id="city" type="text" name="city"/>
                </p>
                <p>
                    <label for="state">State</label>
                    <input id="state" type="text" name="state" size="3"/>
                    <label for="zip">Zip</label>
                    <input id="zip" type="text" name="zip" size="6"/>
                </p>
            </fieldset>
            <br>
            <input class="w3-button w3-blue-grey" type="submit" id="save"
                   value="Save Contact" onclick="saveContact()" disabled/>
        </div>
    </div>
</div>
... 

该代码片段包含了联系人管理器的人工干预界面,以便添加新的联系人。

有几件事情需要指出,具体如下:

  • 我们提供了一个类似于以往项目的图片上传界面。我们会向用户展示上传的名片图片。这样,用户可以在审查和修改联系人信息时查看名片。

  • 我们为各种联系人信息类型提供了两个输入字段列。

  • 我们为用户提供了一个保存联系人按钮,以便他们可以显式地保存联系人信息。保存联系人按钮在应用程序从服务器接收到提取的信息之前默认是禁用的。

scripts.js

联系人管理器的 scripts.js 文件的第一部分是来自《图像翻译器》项目中图像上传的相同实现:

"use strict";

const serverUrl = "http://127.0.0.1:8000";

class HttpError extends Error {
    constructor(response) {
        super(`${response.status} for ${response.url}`);
        this.name = "HttpError";
        this.response = response;
    }
}

async function uploadImage() {
    // encode input file as base64 string for upload
    let file = document.getElementById("file").files[0];
    let converter = new Promise(function(resolve, reject) {
        const reader = new FileReader();
        reader.readAsDataURL(file);
        reader.onload = () => resolve(reader.result
            .toString().replace(/^data:(.*,)?/, ''));
        reader.onerror = (error) => reject(error);
    });
    let encodedString = await converter;

    // clear file upload input field
    document.getElementById("file").value = "";

    // make server call to upload image
    // and return the server upload promise
    return fetch(serverUrl + "/images", {
        method: "POST",
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({filename: file.name, filebytes: encodedString})
    }).then(response => {
        if (response.ok) {
            return response.json();
        } else {
            throw new HttpError(response);
        }
    })
}

function updateImage(image) {
    document.getElementById("view").style.display = "block";

    let imageElem = document.getElementById("image");
    imageElem.src = image["fileUrl"];
    imageElem.alt = image["fileId"];

    return image;
}

在前面的代码中,我们实现了 uploadImage()updateImage() 函数,我们将在后续使用到它们:

function extractInformation(image) {
    // make server call to extract information
    // and return the server upload promise
    return fetch(serverUrl + "/images/" + image["fileId"] + "/extract-info", {
        method: "POST"
    }).then(response => {
        if (response.ok) {
            return response.json();
        } else {
            throw new HttpError(response);
        }
    })
}

function populateFields(extractions) {
    let fields = ["name", "title", "email", "phone", "organization", "address", "city", "state", "zip"];
    fields.map(function(field) {
        if (field in extractions) {
            let element = document.getElementById(field);
            element.value = extractions[field].join(" ");
        }
        return field;
    });
    let saveBtn = document.getElementById("save");
    saveBtn.disabled = false;
}

function uploadAndExtract() {
    uploadImage()
        .then(image => updateImage(image))
        .then(image => extractInformation(image))
        .then(translations => populateFields(translations))
        .catch(error => {
            alert("Error: " + error);
        })
}

在前面的代码片段中,我们实现了以下内容:

  • extractInformation() 函数,调用提取信息端点

  • populateFields() 函数,用于填充输入字段并提取联系人信息

  • uploadAndExtract() 函数与 uploadImage()updateImage()extractInformation()populateFields() 函数连接在一起,组成了用户点击提取信息按钮时的业务逻辑流程:

function saveContact() {
    let contactInfo = {};

    let fields = ["name", "title", "email", "phone", "organization", "address", "city", "state", "zip"];
    fields.map(function(field) {
        let element = document.getElementById(field);
        if (element && element.value) {
            contactInfo[field] = element.value;
        }
        return field;
    });
    let imageElem = document.getElementById("image");
    contactInfo["image"] = imageElem.src;

    // make server call to save contact
    return fetch(serverUrl + "/contacts", {
        method: "POST",
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json'
        },
        body: JSON.stringify(contactInfo)
    }).then(response => {
        if (response.ok) {
            clearContact();
            return response.json();
        } else {
            throw new HttpError(response);
        }
    })
}

在前面的代码片段中,发生了以下事情:

  1. saveContact() 函数从每个输入字段获取一个值,然后创建 contactInfo 数据结构。该函数随后将 contactInfo 数据发送到服务器以进行持久化存储。

  2. 如果服务器的响应是 ok,则意味着联系人已被保存。

  3. 然后,这个函数调用 clearContact() 函数来清空输入字段的值和图片显示。

以下是 clearContact() 辅助函数的代码:

function clearContact() {
    let fields = ["name", "title", "email", "phone", "organization", "address", "city", "state", "zip"];
    fields.map(function(field) {
        let element = document.getElementById(field);
        element.value = "";
        return field;
    });

    let imageElem = document.getElementById("image");
    imageElem.src = "";
    imageElem.alt = "";

    let saveBtn = document.getElementById("save");
    saveBtn.disabled = true;
}

上述代码中的 clearContact() 辅助函数准备用户界面以处理另一张名片。让我们看一下以下代码:

function retrieveContacts() {
    // make server call to get all contacts
    return fetch(serverUrl + "/contacts", {
        method: "GET"
    }).then(response => {
        if (response.ok) {
            return response.json();
        } else {
            throw new HttpError(response);
        }
    })
}

function displayContacts(contacts) {
    ...
}

function retrieveAndDisplayContacts() {
    retrieveContacts()
        .then(contacts => displayContacts(contacts))
        .catch(error => {
            alert("Error: " + error);
        })
}

在上述代码片段中,发生了以下情况:

  1. retrieveContacts() 函数调用服务器以获取所有现有联系人。

  2. displayContacts()函数获取联系人并在联系人管理器用户界面的底部显示它们。

  3. retrieveAndDisplayContacts()函数在网页界面初次加载或用户点击刷新按钮时,串联起业务逻辑流:

function displayContacts(contacts) {
    let contactsElem = document.getElementById("contacts")
    while (contactsElem.firstChild) {
        contactsElem.removeChild(contactsElem.firstChild);
    }

    for (let i = 0; i < contacts.length; i++) {
        let contactElem = document.createElement("div");
        contactElem.style = "float: left; width: 50%";
        contactElem.appendChild(document.createTextNode(contacts[i]["name"]));
        contactElem.appendChild(document.createElement("br"));
        contactElem.appendChild(document.createTextNode(contacts[i]["title"]));
        contactElem.appendChild(document.createElement("br"));
        contactElem.appendChild(document.createTextNode(contacts[i]["organization"]));
        contactElem.appendChild(document.createElement("br"));
        contactElem.appendChild(document.createTextNode(contacts[i]["address"]));
        contactElem.appendChild(document.createElement("br"));
        contactElem.appendChild(document.createTextNode(
             contacts[i]["city"] + ", " + contacts[i]["state"] + " " + contacts[i]["zip"]
        ));
        contactElem.appendChild(document.createElement("br"));
        contactElem.appendChild(document.createTextNode("phone: " + contacts[i]["phone"]));
        contactElem.appendChild(document.createElement("br"));
        contactElem.appendChild(document.createTextNode("email: " + contacts[i]["email"]));

        let cardElem = document.createElement("div");
        cardElem.style = "float: right; width: 50%";
        let imageElem = document.createElement("img");
        imageElem.src = contacts[i]["image"];
        imageElem.height = "150";
        cardElem.appendChild(imageElem);

        contactsElem.appendChild(document.createElement("hr"));
        contactsElem.appendChild(contactElem);
        contactsElem.appendChild(imageElem);
        contactsElem.appendChild(document.createElement("hr"));
    }
}

这段代码展示了生成 HTML 以显示联系人列表、联系人信息和名片图像的详细过程。

如你在displayContacts()函数中所见,使用了大量的 JavaScript 代码来生成 HTML。将业务逻辑和显示逻辑混合使用是不推荐的做法。

我们强烈建议使用如 Angular、React 或 Vue 等 JavaScript 框架,更好地实现模型视图控制MVC)设计模式来构建用户界面。为了限制本书的范围,我们别无选择,只能在实践项目中应对代码的丑陋。

将联系人管理器部署到 AWS

联系人管理器应用的部署步骤与我们之前涉及的其他项目的部署步骤相似,但略有不同。让我们开始吧:

  1. 对于联系人管理器,我们需要向 AWS Lambda 环境添加额外的 Python 包。我们通过向requirements.txt文件中添加两个包来实现:

    • usaddress包用于解析地址的各个部分,如城市、州、邮政编码等。

    • 这里指定了boto3包,因为我们需要特定的版本。在撰写时,AWS Lambda 环境中的boto3版本不支持comprehendmedical服务;我们需要为这个项目使用更新的版本:

usaddress==0.5.10
boto3==1.9.224
  1. 接下来,让我们通过将"autogen_policy"设置为false,在项目结构中的.chalice目录下的config.json文件中告诉 Chalice 为我们执行策略分析:
{
  "version": "2.0",
  "app_name": "Capabilities",
  "stages": {
    "dev": {
      "autogen_policy": false,
      "api_gateway_stage": "api"
    }
  }
}
  1. 接下来,我们在.chalice目录下创建一个新文件policy-dev.json,手动指定项目所需的 AWS 服务:
{
 "Version": "2012-10-17",
 "Statement": [
 {
 "Effect": "Allow",
 "Action": [
 "logs:CreateLogGroup",
 "logs:CreateLogStream",
 "logs:PutLogEvents",
 "s3:*",
 "rekognition:*",
 "comprehend:*",
 "comprehendmedical:*",
 "dynamodb:*"
 ],
 "Resource": "*"
 }
 ]
}
  1. 接下来,我们通过在Capabilities目录下运行以下命令将 Chalice 后端部署到 AWS:
$ chalice deploy
Creating deployment package.
Creating IAM role: Capabilities-dev
Creating lambda function: Capabilities-dev
Creating Rest API
Resources deployed:
  - Lambda ARN: arn:aws:lambda:us-east-1:<UID>:function:Capabilities-dev
  - Rest API URL: https://<UID>.execute-api.us-east-1.amazonaws.com/api/

部署完成后,Chalice 会输出一个类似于https://<UID>.execute-api.us-east-1.amazonaws.com/api/的 RESTful API URL,其中<UID>是唯一的标识符字符串。这是你的前端应用应该访问的服务器 URL,后端应用则运行在 AWS 上。

  1. 接下来,我们将把index.htmlscripts.js文件上传到该 S3 存储桶,然后设置权限为公开可读。在此之前,我们需要对scripts.js进行如下修改。记住,网站现在将运行在云端,无法访问我们本地的 HTTP 服务器。请将本地服务器 URL 替换为我们后端部署的 URL:
"use strict";

const serverUrl = "https://<UID>.execute-api.us-east-1.amazonaws.com/api";

...

现在,联系人管理器应用对所有人公开可访问。

如本章所述,联系人组织器向任何拥有该应用程序 URL 的人展示所有保存的联系人信息。我们不建议在互联网上公开任何个人可识别的信息。

保护这些信息的一种方法是为联系人组织器添加身份验证和授权功能。这些功能超出了本书的范围,但它们是该项目的有趣增强功能。

讨论项目增强的思路

在第二部分每个实践项目的结尾,我们为您提供了一些扩展我们智能应用程序的思路。以下是一些增强联系人组织器的想法:

  • 使用 Amazon Textract 服务创建识别服务的另一种实现。Textract 提供了光学字符识别OCR)功能,更适用于具有大量文本的文档。根据名片的外观、环境光照和照片质量,Textract 可能提供更好的文本检测性能。

  • 我们为联系人组织器创建的智能功能和用户界面也可以应用于其他用例,例如从商业文档中提取数据、总结学校笔记和分类客户请求。原始文本甚至不需要来自图像;其他来源可以包括电子邮件、电话甚至社交媒体。想一想您可能在什么用例中使用类似的人工介入用户界面和智能功能。

总结

在本章中,我们构建了一个联系人组织器应用程序,可以从上传的名片照片中提取联系信息。我们使用了亚马逊 Comprehend 服务的两个变体——Amazon Comprehend 和 Amazon Comprehend Medical,来提取不同类型的联系信息。联系人组织器具有一个人工介入的用户界面,用户可以在将信息保存到联系人存储库之前,查看并修正自动提取的信息。我们注意到,人工介入的用户界面应该提供商业价值,即使解决方案中没有 AI 能力。作为 AI 从业者,我们并不总是需要提供完全自动化的解决方案——提供智能辅助解决方案也有其价值,只要设计得当,并且考虑到人的因素,它们通常更容易构建和维护。

在下一章中,我们将构建一个 AI 解决方案,能够通过自然对话界面与我们进行交流。我们将使用流行的 Alexa 智能音响中所使用的核心 AI 技术。

深入阅读

欲了解有关使用 Amazon Comprehend 从文本中提取信息的更多信息,请参考以下链接:

第六章:使用 Amazon Lex 构建语音聊天机器人

在本章中,我们将构建一个聊天机器人,允许用户通过语音或文本对话搜索信息并执行操作。这个聊天机器人为人类与计算机的交互提供了更直观的界面。我们将使用 Amazon Lex 构建一个自定义的 AI 能力,理解自然语言中的请求,询问缺失的输入,并完成任务。我们将提供有关 Amazon Lex 开发范式的指导,包括其惯例和规范。

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

  • 使用 Amazon Lex 构建对话式界面

  • 使用 AWS Lambda 实现任务完成逻辑

  • 在 Amazon Lex 自定义 AI 能力前添加 RESTful API

  • 讨论对话式界面的设计问题

理解友好的人与计算机界面

智能个人助手,有时被称为聊天机器人,正在迅速出现在我们互动的越来越多的产品中。其中最显著的产品是智能音响,如 Amazon Echo 和 Google Home。用声音与机器互动曾经是科幻小说中的情节,如今,只需要对着 AlexaHey Google 说几句话,就能获得趣味事实和笑话。我们可以让这些智能助手执行的任务包括媒体控制、信息搜索、家庭自动化以及行政任务,如电子邮件、待办事项和提醒。

智能个人助手的能力可以集成到比智能音响更多种类的设备和平台中。这些平台包括移动操作系统,如 Android 和 iOS;即时通讯应用,如 Facebook Messenger;以及公司网站,如餐厅(接收订单)和银行(查询账户余额)。有两种主要的互动方式:通过文本或语音。这种智能助手能力是多种 AI 技术的结合。对于两种互动方式,都需要自然语言处理NLP)来解释并匹配文本与支持的问题或命令。对于语音交互,则需要语音转文本和文本转语音技术,分别由 Amazon Transcribe 和 Amazon Polly 提供,这也是我们已经亲自体验过的技术。

这些智能助手可能看起来像是在执行诸如回答问题、下订单和自动化家庭的任务。但在幕后,这些任务几乎总是由传统的 API 和服务完成的。我们通过智能助手能力获得的,实际上是一个更灵活的人机接口。利用这一新能力并不仅仅是将一个华丽的语音界面加在现有应用程序上。在设计智能助手时,理解智能助手可以提供更好用户体验的使用场景和操作环境非常重要。并非所有应用程序都应该具备这样的界面;例如,要求精确输入或密集输出、噪音环境和长时间复杂的工作流程等使用场景。

在本章中,我们将实现一个智能助手,称为“联系助手”,用于搜索联系信息。该联系助手将与我们在第五章中为联系组织者项目创建的相同联系数据存储一起工作,通过 Amazon Comprehend 提取文本中的信息。我们还将在联系助手前端添加一个 RESTful API,使我们能够通过多个应用程序利用其功能。这个智能助手的设计方式使其在实际场景中非常有用,例如,当一位外地销售人员正在开车前往客户,并需要口头获取客户的联系信息时。与通过浏览器运行的 Web 应用程序进行搜索不同,这种使用场景更适合开发一个具有驾驶员友好用户界面的移动应用程序。虽然本书的范围不涉及该移动应用程序及其用户界面,但它可能会成为一些人有趣的动手项目。

联系助手架构

联系助手项目的架构包括以下内容:

  • 编排层

  • 服务实现层

以下架构不包括用户界面层,因为我们不会实现连接到联系助手的移动或 Web 应用程序。相反,我们将重点开发一个自定义 AI 能力,一个智能助手机器人,使用 Amazon Lex 平台。让我们来看一下以下架构的截图:

联系助手架构包括以下内容:

  • 在编排层中,我们将构建一个联系助手端点,为访问我们联系助手的功能提供 RESTful 接口。

  • 在服务实现层,我们将构建一个名为智能助手服务的服务,来屏蔽我们自定义 AI 能力的实现细节,包括其 Amazon Lex 实现细节。这样,当我们想用不同的聊天机器人技术重新实现联系人助手机器人时,只需要修改智能助手服务。

在前面的章节中,我们构建了自己的服务,例如识别服务和语音服务,分别连接到 AWS AI 能力,如 Rekognition 和 Polly。就像这些服务屏蔽了 AWS AI 服务的实现细节一样,智能助手服务也屏蔽了我们基于 Amazon Lex 构建的自定义 AI 能力的实现细节。

  • 联系人助手机器人将能够执行两个任务,LookupPhoneNumberByNameMakePhoneCallByName。这个机器人利用 Amazon Lex 的底层 AI 能力来解释用户的口头命令,然后使用 AWS Lambda 函数执行任务,查找电话号码并拨打电话。

  • 联系人助手将查找存储在与第五章使用 Amazon Comprehend 提取文本信息》中相同的 DynamoDB 表中的联系人信息。在复用的精神下,我们将重新使用连接到 DynamoDB 表的联系人存储实现。更具体地说,Lambda 函数将把联系人搜索委托给联系人存储。联系人信息存储在 DynamoDB 表中的事实对联系人助手是透明的。

理解 Amazon Lex 开发范式

Amazon Lex 是一个用于构建智能助手或聊天机器人的开发平台。通过 Amazon Lex,我们正在构建自己的自定义智能助手能力。Lex 本身提供了许多 AI 能力,包括自动语音识别ASR)和自然语言理解NLU),这些能力对于构建对话界面非常有用。然而,开发人员必须遵循 Lex 的开发构造、约定和规范,才能利用这些底层的 AI 能力。

这些 Amazon Lex 对话界面是由 Lex 特定的构建模块构建的:

  • 机器人:Lex 机器人可以通过自定义的对话界面执行一组相关任务。一个机器人将相关任务组织成一个单元,以便于开发、部署和执行。

    • 例如,为了让任务对应用程序可用,任务会被部署或发布为一个机器人,应用程序必须指定机器人名称才能访问可用的任务。
  • 意图:意图表示用户希望执行的自动化任务。意图属于特定的 AWS 账户,而不是特定的机器人,可以被同一 AWS 账户中的不同机器人使用。这个设计决策使得意图更具可重用性。

  • 示例话语:话语是用户可能用来触发自动化任务的自然语言输入,无论是键入还是说出。Amazon Lex 鼓励开发者提供多个话语,以使对话界面对用户更加灵活。

    • 例如,用户可能会说今天的天气怎么样?,或者告诉我今天的天气?来查询天气报告。Amazon Lex 使用先进的自然语言理解(NLU)来理解用户的意图。

    • 根据前面两个示例话语,Amazon Lex 还利用 NLU 功能处理话语的变化。即使没有提供完全相同的表达方式,Lex 也能理解告诉我今天的天气怎么样?

  • 槽位:自动化任务可能需要零个或多个槽位(参数)来完成。例如,日期和地点是获取用户感兴趣的天气报告时使用的参数。在对话界面中,Lex 会要求用户提供所有必需的槽位。

    • 例如,如果未指定位置,可以默认使用用户的家庭地址。
  • 槽位类型:每个槽位都有一个类型。类似于编程语言中的参数类型,槽位类型限制了输入空间并简化了验证,从而使对话界面更加用户友好。特别是在口语交流中,了解槽位类型可以帮助 AI 技术更准确地判断输入的文本或语音。

    • 有许多内置的槽位类型,例如数字、城市、机场、语言和音乐家,仅举几例。开发者还可以创建特定于其应用程序的自定义槽位类型。
  • 提示和响应:提示是 Lex 向用户提问,要求用户提供某个槽位的输入或确认已提供的输入。响应是向用户告知任务结果的消息,例如天气报告。

    • 对话界面的提示和响应设计应考虑使用场景、通信方式(文本或语音)和操作环境。设计应在不让用户感到负担的情况下获取用户确认。
  • 会话属性:Amazon Lex 提供机制来保持上下文数据,这些数据可以在同一会话中的不同意图之间共享。

    • 例如,如果用户只请求了某个城市的天气报告,然后又提出问题那里的交通怎么样?,会话上下文应该能够推断出那里是指先前意图中提到的城市。此类上下文信息可以存储在 Lex 的会话属性中,供开发者构建更智能的机器人。

Amazon Lex 平台专注于构建对话接口;自动任务的执行则委托给 AWS Lambda。开发者可以使用两种内置的钩子类型来集成 Lambda 函数:

  • Lambda 初始化和验证:此钩子允许开发者编写 AWS Lambda 函数来验证用户输入。例如,Lambda 函数可以验证用户的输入,检查数据源并执行更复杂的业务逻辑。

  • 履行 Lambda 函数:此钩子允许开发者编写 AWS Lambda 代码来执行任务。通过这个 Lambda 钩子,开发者可以利用 AWS 服务、API 端点等,编写用于检查天气、订购披萨、发送消息等任务的业务逻辑。

设置联系助手机器人

现在我们已经理解了 Amazon Lex 的开发范式和术语,接下来我们将通过构建一个具有对话界面和业务逻辑实现的机器人来应用它们。我们将使用 AWS 控制台构建联系助手。请按照以下步骤操作:

  1. 导航至 Amazon Lex 页面并点击“创建”按钮。

  2. 在“创建你的机器人”页面,选择“自定义机器人”以创建我们自己的机器人,而不是从示例机器人开始。

  3. 在机器人名称字段中,输入 ContactAssistant

  4. 在输出语音(Output voice)中,选择 Joanna。目前,Lex 仅支持美式英语。

  5. 在会话超时(Session timeout)字段中,输入 5 分钟。这是联系助手在关闭会话之前的最大空闲时间。

  6. 对于 IAM 角色,保持默认设置为 AWSServiceRoleForLexBots。

  7. 对 COPPA 选择“否”;这个联系助手是为一位旅行推销员设计的,而不是为儿童设计的。

  8. 点击“创建”按钮。

在完成上述步骤后,“创建你的机器人”页面应具有以下设置:

  1. 一旦联系助手被创建,你将进入 Lex 的开发控制台,界面类似于下图所示:

让我们先熟悉一下 Lex 开发控制台:

  1. 机器人的名称可以在左上角找到,命名为 Contact_Assistant。

  2. 在右上角有一对禁用的“构建”和“发布”按钮。

  3. 在机器人名称和按钮下方是编辑器(Editor)、设置(Settings)、渠道(Channels)和监控(Monitoring)屏幕的标签。我们将在编辑器标签中进行大部分的机器人开发工作。

  4. 选中编辑器标签后,我们看到联系助手中尚未添加任何意图(Intent)或槽类型(Slot types)。

  5. 在屏幕的右上角,有一个可展开的“测试机器人”侧边栏(如图所示),展开后会显示一个聊天界面。该聊天界面用于向正在开发的机器人发出语音命令。目前,该聊天界面是禁用的,机器人需要先构建,并且至少创建一个意图(Intent)。

  6. 最后,点击“创建意图”按钮来构建我们的第一个意图。

LookupPhoneNumberByName 意图

我们的第一个意图允许用户通过说出联系人的名字和姓氏来查找该联系人的电话号码。这个意图本质上是一个构建在联系人存储之上的搜索功能,但它采用了对话式界面。

我们建议将每个意图设计为专注于一个狭窄的使用场景,并通过构建多个意图来扩展机器人的使用场景。

LookupPhoneNumberByName 意图有非常专注的输入和输出,但我们可以构建许多相关的意图,例如 LookupAddressByNameLookupContactNamesByState。即使我们可以将 LookupPhoneNumberByName 意图视为对数据源的搜索功能,它仍然需要不同的设计思维。

让我们在比较这个意图和更传统的网页应用搜索功能时,突出一些设计上的差异:

  • 在网页界面中,我们会提供多个搜索参数,例如姓名、组织和地点。在对话式界面中,我们希望每个意图的搜索参数或输入尽量少。尤其是在语音聊天机器人中,提示和确认所有输入可能会显得繁琐。

  • 在网页界面中,我们会返回关于联系人的许多信息并显示在屏幕上。在对话式界面中,我们需要考虑交互方式。如果这是一个文本聊天机器人,我们或许可以展示多条信息。但如果这是一个语音聊天机器人,那么向用户朗读大量信息可能会造成认知负担。

LookupPhoneNumberByName 的示例话语和槽位

在设计新的意图时,所有相关方——不仅仅是开发人员——必须仔细思考用户与机器人之间的对话流程。我们先从示例话语开始。

智能助手很可能会替代现有的用户沟通渠道,例如打电话给客户代表、发送产品问题咨询邮件以及与技术支持人员进行文本聊天。通常做法是使用来自这些现有渠道的用户对话录音来设计智能助手的对话流程。这些录音能最准确地反映用户与产品的互动,它们是设计话语和提示的良好起点。

示例话语是调用意图以执行自动任务的短语。以下是我们 LookupPhoneNumberByName 意图的一些示例话语:

如我们在前面的截图中所见,两个示例话语自然地在对话流程中包含了槽位或输入参数 {FirstName} 和 {LastName}。这样,用户可以在触发任务时提供完成任务所需的部分或全部输入。

对于LookupPhoneNumberByName,我们需要同时提供{FirstName}和{LastName}来查找电话号码,因为这两个都是必填项。我们来看一下以下的插槽截图:

如前面的截图所示,对于插槽类型,有内置的 AMAZON.US_FIRST_NAME 和 AMAZON.US_LAST_NAME 类型。如前所述,为输入指定最相关和最具体的类型,可以大大简化自然语言理解和底层 AI 技术的值验证。

如果用户没有提供插槽的输入怎么办?例如,如果用户说了第一个样本话语,我想查找一个电话号码。每个插槽如果没有在调用话语中提供输入值,都必须有一个或多个提示来请求用户输入。对于{FirstName}{LastName},我们分别使用了What's the contact's first name?What's the {FirstName}'s last name?。注意,{LastName}的提示中包含了{FirstName}的值。这可以让对话流程更加自然和人性化。

若要为插槽添加多个提示,请点击齿轮图标编辑插槽的设置。在这里,您可以添加其他提示,设置最大重试次数以获取此输入,并设置相应的表达式,如下所示:

机器人将从这些提示中选择一个来请求用户输入插槽值。机器人会尝试这些提示最多两次,然后放弃。

LookupPhoneNumberByName 的确认提示和响应

为了完成对话流程设计,让我们继续设计确认提示和响应。虽然这两者是可选的,但它们能极大地改善智能助手的行为和用户体验。

以下是一个确认提示的截图。确认提示是一个通知用户即将执行的操作的机会。此时,所有必填插槽和可能的可选插槽的值都已获取:

我们可以在确认消息中使用{FirstName}{LastName}。在确认消息中回显{FirstName}{LastName}的值是一个很好的设计,它能够确认机器人正确理解了用户输入。自然语言对话有时会产生歧义。让我们来看一个示例对话:

你发现问题了吗?我们其中一个示例话语是 What's {FirstName} {LastName} phone number。然而,用户在调用意图时没有提供{LastName}。我们的机器人将what's解释为{FirstName},将John解释为{LastName}。通过在确认提示中回显输入值,用户可以注意到并纠正输入错误,然后再执行操作。

我们现在跳过任务的 Fulfillment 部分,直接进入响应部分。在以下截图中,LookupPhoneNumberByName 意图通过显示或朗读联系人的电话号码来完成任务:

[Phone] 是一个会话属性,用于保存联系人的电话号码。它将在 Fulfillment lambda 函数中设置。我们将在本章后续部分介绍其实现方式。

该意图用于查询信息。提供响应中的信息将使用户感到自然。也有一些意图会执行任务,而无需向用户提供信息。在这种情况下,仍然建议向用户反馈任务的结果。

现在,我们已经完成了第一个意图的对话接口。接下来,我们将实现 AWS Lambda 函数,来执行智能助手所要求的任务。

使用 AWS Lambda 执行 LookupPhoneNumberByName 的 Fulfillment

要执行与智能助手相关的任何完成操作,开发者需要调用 AWS Lambda 函数。Fulfillment 部分提供了一个钩子,用于现有的 lambda 函数。让我们实现一个名为 LookupPhoneNumberByName 的 lambda 函数,通过联系人的名字和姓氏来查找其电话号码。

与之前使用 AWS Chalice 开发和部署 lambda 代码以及 AWS 权限的项目不同,我们将使用 AWS Lambda 控制台页面来创建 LookupPhoneNumberByName 函数。以下是步骤:

  1. 从 AWS 控制台导航到 AWS Lambda 服务,然后点击“创建函数”按钮。

  2. 选择“从头开始创建”。我们将不使用任何蓝图或示例应用来实现 lambda 函数。

  3. 将函数命名为 LookupPhoneNumberByName

  4. 选择 Python 3.7 运行时,以匹配我们其他动手项目的语言版本。

  5. 选择“创建具有基本 Lambda 权限的新角色”来创建一个角色。稍后我们需要添加更多策略来连接其他 AWS 服务。

  6. 点击“创建函数”按钮。

创建函数页面上的设置应该类似于以下截图:

创建 lambda 函数及其执行角色后,您将看到一个类似以下的开发控制台:

上述截图演示了以下内容:

  • 在设计器部分,我们可以添加触发器来调用这个 lambda 函数。对于 Lex 机器人,我们无需选择触发器。

  • 我们还看到,LookupPhoneNumberByName 函数有权限访问 CloudWatch 日志。该函数执行过程中产生的任何输出或错误消息将写入 CloudWatch,我们可以通过 CloudWatch 控制台页面查看这些日志。在开发和调试该函数时,这将非常有用。

  • 在函数代码部分,我们可以选择“编辑代码内联”,修改函数运行时并更改 Handler 函数名称。Handler 函数指定了构成 Lambda 函数入口点的 Python 文件和函数名。

  • 在三个 Lambda 配置字段下方,我们有内联代码编辑器。在这里,我们可以创建额外的源文件并编辑每个源文件的代码。

我们的 Lambda 函数需要与存储来自联系人管理应用的联系人信息的相同 DynamoDB 进行交互。我们可以利用现有的联系人存储,然后添加一个新函数来查询联系人信息,步骤如下:

  1. 在内联编辑器的左侧面板中右键点击,然后选择“新建文件”。

  2. 将文件命名为 contact_store.py

  3. contact_store.py 的内容替换为 第五章 中的联系人存储实现,使用 Amazon Comprehend 从文本中提取信息

  4. 在现有函数实现后添加 get_contact_by_name()

import boto3

class ContactStore:
    def __init__(self, store_location):
        self.table = boto3.resource('dynamodb').Table(store_location)

    ...

    def get_contact_by_name(self, name):
        response = self.table.get_item(
            Key = {'name': name}
        )

        if 'Item' in response:
            contact_info = response['Item']
        else:
            contact_info = {}

        return contact_info

上述代码包含以下元素:

  • get_contact_by_name() 方法通过唯一标识符(即姓名)检索单个联系人。在该方法中,我们调用 DynamoDB 的 get_item() 函数。get_item() 的响应包含一个字典。如果项键存在,则我们会得到一个包含联系人信息的返回值。

  • 在这里,我们通过键从 DynamoDB 表中获取一项。键是联系人姓名、名和姓,用空格分隔。此代码将在 Python 3.7 的 Lambda 运行时环境中执行。在该环境中,boto3 包已经安装。

用于 LookupPhoneNumberByName 的 DynamoDB IAM 角色

由于这段代码需要连接到 DynamoDB,我们需要为我们的 Lambda 函数的执行角色添加一个策略:

  1. 从 AWS 控制台导航到 IAM 页面。

  2. 点击左侧面板中的“角色”。

  3. 在角色列表中,找到并点击用于我们 Lambda 函数的 LookupPhoneNumberByName-role- 角色。

  4. 点击“附加策略”按钮。

  5. 找到并选择 AmazonDynamoDBFullAccess 策略,然后点击“附加策略”按钮。

现在,让我们看一下以下截图:

现在,我们的 LookupPhoneNumberByName Lambda 函数可以访问 DynamoDB。AmazonDynamoDBFullAccess 策略适用于我们的实操项目,但对于真实的生产应用,您应当调整该策略,以限制授予的权限数量。

用于 LookupPhoneNumberByName 的履行 Lambda 函数

在 Lambda 编辑器窗口中,打开现有的 lambda_function.py 文件,并将其内容替换为以下实现:

import contact_store

store_location = 'Contacts'
contact_store = contact_store.ContactStore(store_location)

def lex_lambda_handler(event, context):
    intent_name = event['currentIntent']['name']
    parameters = event['currentIntent']['slots']
    attributes = event['sessionAttributes'] if event['sessionAttributes'] is not None else {}

    response = lookup_phone(intent_name, parameters, attributes)

    return response

def lookup_phone(intent_name, parameters, attributes):
    first_name = parameters['FirstName']
    last_name = parameters['LastName']

    # get phone number from dynamodb
    name = (first_name + ' ' + last_name).title()
    contact_info = contact_store.get_contact_by_name(name)

    if 'phone' in contact_info:
        attributes['Phone'] = contact_info['phone']
        attributes['FirstName'] = first_name
        attributes['LastName'] = last_name
        response = intent_success(intent_name, parameters, attributes)
    else:
        response = intent_failure(intent_name, parameters, attributes, 'Could not find contact information.')

    return response

# Amazon lex helper functions
...

在上述代码中,发生了以下情况:

  • 我们首先用 DynamoDB 表 contacts 初始化联系人存储。

  • lambda_handler() 函数中,我们从传入的事件对象中提取意图名称、槽位和属性。当履行钩子被触发时,事件对象会传入我们的亚马逊 Lex 机器人。所有槽位输入值以及会话属性都会包含在此事件对象中。

  • lambda_handler() 然后调用 lookup_phone() 函数,该函数使用联系人存储来检索联系信息。

  • lookup_phone() 函数中,我们根据 FirstNameLastName 槽位值构建项目键。项目键必须是由空格分隔的 FirstNameLastName,并且需要正确的大写。

    • 例如,名字 john 和姓氏 smith 会生成项目键 John Smith;名字的每个部分的首字母都大写。

    • 我们使用 title() 函数确保正确的大写形式,无论用户如何输入名字。

如果我们能够通过这些名字查找到联系人,我们将把联系人的电话号码、名字和姓氏保存在会话属性中。这就是电话号码如何传回并在该意图的响应中显示或朗读的方式。稍后我们将解释为什么名字和姓氏会保存在会话属性中。

如果我们成功完成查找,我们会响应 intent_success(),否则我们会响应 intent_failure() 并附带解释信息。这些是封装了亚马逊 Lex 特定响应格式的助手函数。

亚马逊 Lex 助手函数

亚马逊 Lex 助手函数将响应格式化为 Lex 所期望的格式。这里有四个助手函数:

  • intent_success() 表示意图已经成功实现,并且任何会话属性都会作为 sessionAttributes 返回给 Lex。

  • intent_failure() 表示意图未能成功实现。此响应还包括一条解释信息。

  • intent_elicitation() 请求 Lex 机器人引导获取指定参数名称的值。此引导可能是由于缺少槽位值或槽位值无效。这一助手函数在我们创建自定义 Lambda 初始化和验证 逻辑时非常有用。

  • intent_delegation() 表示 Lambda 函数已经完成了它的任务,并指示 Lex 根据机器人配置选择下一步行动。

我们只使用了前两个助手函数来实现 LookupPhoneNumberByName 意图。以下是代码实现:

# Amazon lex helper functions
def intent_success(intent_name, parameters, attributes):
    return {
        'sessionAttributes': attributes,
        'dialogAction': {
            'type': 'Close',
            'fulfillmentState': 'Fulfilled'
        }
    }

def intent_failure(intent_name, parameters, attributes, message):
    return {
        'dialogAction': {
            'type': 'Close',
            'fulfillmentState': 'Failed',
            'message': {
                'contentType': 'PlainText',
                'content': message
            }
        }
    }

def intent_delegation(intent_name, parameters, attributes):
    return {
        'sessionAttributes': attributes,
        'dialogAction': {
            'type': 'Delegate',
            'slots': parameters,

        }
    }

def intent_elicitation(intent_name, parameters, attributes, parameter_name):
    return {
        'sessionAttributes': attributes,
        'dialogAction': {
            'type': 'ElicitSlot',
            'intentName': intent_name,
            'slots': parameters,
            'slotToElicit': parameter_name
        }
    }

即使 lambda_function.py 文件相对较短,我们仍然应用了一些清晰代码的实践。我们将所有与 AWS Lambda 和亚马逊 Lex 相关的实现细节都组织到了 lambda_handler() 函数和亚马逊 Lex 助手函数中。

例如,如何从 Lambda 事件对象中获取槽位和响应格式以供 Amazon Lex 使用?这样,lookup_phone() 函数就不受这些平台特定细节的影响,因此更可能在其他平台上重用。lookup_phone() 函数只需要 intent_name 为字符串类型,参数和属性为字典类型。

通过点击 Lambda 开发控制台右上角的 Save 按钮保存 Lambda 函数实现。

LookupPhoneNumberByName 的意图 Fulfillment

现在,让我们将此 Lambda 函数添加到 Fulfillment hook 中:

  1. 转到 Amazon Lex 开发控制台,在 Fulfillment 部分,从 Lambda 函数列表中选择 LookupPhoneNumberByName,如下图所示:

  1. 如下图所示,Amazon Lex 会请求调用此 Lambda 函数的权限。点击 OK 以授予权限:

  1. 在 Lex 开发控制台中,点击页面底部的 Save Intent 按钮,然后点击页面右上角的 Build 按钮。构建我们的第一个 Lex 机器人需要几秒钟时间。

测试 LookupPhoneNumberByName 的对话

现在,我们准备好构建并测试我们的第一个意图。在页面右侧的 Test bot 面板中,发出一些示例话语的变化,并与联系人助手进行对话。以下是一个示例对话:

在前面的对话中,发生了以下情况:

  • 话语中没有包含槽位,我们的联系人助手提示输入名字和姓氏

  • 助手在继续执行 Fulfillment 之前确认了对 John Smith 的查询

  • 响应中包含了联系人的名字和电话号码

现在,想想这个对话是如何进行的,无论是文本聊天还是语音对话。

这是另一个示例对话:

在前面的对话中,发生了以下情况:

  • 话语中包含了两个必需的槽位

  • 这次,我们的联系人助手只需要在继续执行 Fulfillment 和响应之前确认查询

  • 用户也可以通过回答no来取消 Fulfillment

恭喜!你刚刚完成了第一个具有对话界面和 AWS Lambda Fulfillment 实现的智能助手。

测试机器人面板的聊天界面也支持语音输入。你可以使用麦克风图标通过语音发出话语和响应。

在测试机器人聊天界面中,Lex 的响应将始终以文本形式显示。

MakePhoneCallByName 意图

接下来,我们将为我们的联系人助手创建第二个意图,命名为MakePhoneCallByName。从名字就可以看出,这个意图执行的任务是拨打电话给联系人。然而,在这个项目中我们不会实现电话拨打功能。

实现第二个意图的目标是演示智能助手的多个意图如何互相交互和协作。我们希望设计MakePhoneCallByName的对话界面,使其能够独立运行,同时也能够与LookupPhoneNumberByName意图协同工作。

为了使这种意图协作更具实用性,可以设想用户刚刚查找了某个联系人的电话号码,然后决定拨打该联系人的电话。第二个意图是否应该从头开始,要求提供姓氏和名字的插槽?还是说,考虑到之前的对话,知道用户想打电话给刚刚查找过的联系人,会更流畅自然呢?当然是后者。在LookupPhoneNumberByName成功执行后,用户如果说Call himCall herMakePhoneCallByName应该能够根据先前的对话上下文,知道himher指的是谁。这时,会话属性就可以帮助保持上下文。

MakePhoneCallByName创建的示例发声和 Lambda 初始化/验证

我们将通过点击左侧面板“Intents”旁边的蓝色加号按钮,从 Lex 开发控制台添加一个新的意图,具体步骤如以下截图所示:

  1. 选择创建意图,命名为MakePhoneCallByName,然后点击添加。

  2. 让我们为此意图创建一些示例发声。第一个发声Call {FirstName} {LastName}为两个必需插槽提供了值。对于其他发声,意图应该尽量从对话上下文中获取插槽值:

为了实现这一点,我们将使用来自 Amazon Lex 的第二种类型的 AWS Lambda 钩子——Lambda 初始化和验证。以下步骤将创建该钩子:

  1. 在 Lambda 初始化和验证部分,勾选初始化和验证代码钩子的复选框。

  2. 从 AWS 控制台进入 AWS Lambda 页面,创建一个名为InitContact的 Lambda 函数,选择 Python 3.7 环境。

  3. 创建一个新的默认 Lambda 执行角色。我们不需要为此 Lambda 函数添加 AmazonDynamoDBFullAccess 策略。

  4. 在内联函数代码编辑器中,用以下实现替换lambda_function.py文件的内容:

def lex_lambda_handler(event, context):
    intent_name = event['currentIntent']['name']
    parameters = event['currentIntent']['slots']
    attributes = event['sessionAttributes'] if event['sessionAttributes'] is not None else {}

    response = init_contact(intent_name, parameters, attributes)

    return response

def init_contact(intent_name, parameters, attributes):
    first_name = parameters.get('FirstName')
    last_name = parameters.get('LastName')

    prev_first_name = attributes.get('FirstName')
    prev_last_name = attributes.get('LastName')

    if first_name is None and prev_first_name is not None:
        parameters['FirstName'] = prev_first_name

    if last_name is None and prev_last_name is not None:
        parameters['LastName'] = prev_last_name

    if parameters['FirstName'] is not None and parameters['LastName'] is not None:
        response = intent_delegation(intent_name, parameters, attributes)
    elif parameters['FirstName'] is None:
        response = intent_elicitation(intent_name, parameters, attributes, 'FirstName')
    elif parameters['LastName'] is None:
        response = intent_elicitation(intent_name, parameters, attributes, 'LastName')

    return response

# lex response helper functions
...

在上述代码中,发生了以下操作:

  • init_contact()函数中,我们检查来自发声的插槽是否缺少FirstNameLastName。如果缺少,我们接着检查FirstNameLastName是否存在于会话属性中。

    • 你还记得我们在LookupPhoneNumberByName意图的 Fulfillment 实现中将FirstNameLastName保存到会话属性吗?我们在这里提取了那些保存的值。
  • 如果FirstNameLastName都已设置,那么我们将返回一个委托响应给 Lex。

    • 委托响应告诉 Lex,初始化和验证已完成,机器人应根据其配置继续执行,包括 Fulfillment。
  • 如果FirstNameLastName的值仍然缺失,那么我们将以诱导响应的方式进行回应。

    • 诱导响应将触发配置给机器人用于缺失插槽的提示。

保存 lambda 函数,然后返回到 Amazon Lex 开发控制台:

选择 InitContact 作为 Lambda 初始化和验证函数。

MakePhoneCallByName 的插槽和确认提示

MakePhoneCallByName意图的插槽配置可以与LookupPhoneNumberByName的配置完全相同。请参见以下截图中的详细信息:

两个插槽都是必需的,并且被设置为内置的AMAZON.US_FIRST_NAMEAMAZON.US_LAST_NAME类型。

确认提示可以根据拨打电话的需求进行定制,如下图所示:

确认和取消消息都定制为MakePhoneCallByName意图。

MakePhoneCallByName 的 Fulfillment 和响应

我们可以实现一个新的 lambda 函数来完成联系人查找和拨打电话的功能。但由于在这个项目中我们并不会真正拨打电话,所以 Fulfillment lambda 函数的业务逻辑与我们已经实现的联系人查找功能相同。

实际上,对于这个项目,Fulfillment 可以通过 LookupPhoneNumberByName lambda 函数来处理,如下图所示:

最后,响应配置也可以定制为进行电话拨打,如下所示:

现在,点击 Lex 开发控制台底部的保存意图按钮,然后点击开发控制台右上角的构建按钮。

MakePhoneCallByName 的测试对话

在页面右侧的测试机器人面板中,发布几个示例话语的不同版本,并与联系人助手进行对话。以下是一个示例对话:

上述对话展示了MakePhoneCallByName意图可以独立运行,而无需先执行LookupPhoneNumberByName意图。

这是另一个示例对话:

上述对话展示了上下文的强大作用:

  • 用户首先通过LookupPhoneNumberByName意图请求了 John Smith 的电话号码。

  • 然后,用户请求call him

  • 此时,我们的InitContact lambda 函数从会话属性中获取了FirstNameLastName,并确认 John Smith 是否是要联系的人。

    • 确认提示在这里非常重要,因为联系人助手正在推断联系人。我们不希望自动给错误的联系人打电话,最好先与用户确认再采取行动。

在发出下一个话语之前,点击“清除聊天历史记录”。这将清除会话及其存储的属性。继续以下示例对话:

在这次对话中,发生了以下情况:

  • 用户开始时没有提供任何槽位。然而,这次会话中并没有保存任何之前的对话上下文。

  • InitContact lambda 函数无法获取名字和姓氏,因此它通过意图激发请求进行响应。

  • 测试我们的智能助手以处理所有可能的意图和话语组合非常重要。随着更多意图共享会话属性,这种质量保证变得更加困难。

恭喜!我们的联系人助手现在通过上下文感知变得更智能了。

部署联系人助手机器人

我们现在可以将联系人助手发布为自定义智能助手能力。

点击 Lex 开发控制台右上角的“发布”按钮,并将别名设置为“生产”:

上述截图显示了联系人助手已经发布。一旦联系人助手发布,应用程序可以通过各种集成方式开始使用它,包括 boto3 SDK。

将联系人助手集成到应用程序中

接下来,我们将创建层来将联系人助手能力集成到应用程序中。如本章开始时所述,我们不会实现任何应用程序,我们只会实现服务和 RESTful 端点层。

与以前的动手项目一样,我们将使用 Python、Pipenv、Chalice 和 boto3 作为技术栈的一部分。让我们先创建项目结构。

  1. 在终端中,我们将创建root项目目录并进入,使用以下命令:
$ mkdir ContactAssistant
$ cd ContactAssistant
  1. 我们将在项目的root目录下使用Pipenv创建一个 Python 3 虚拟环境。我们的 Python 项目部分需要两个包,boto3chalice。我们可以通过以下命令安装它们:
$ pipenv --three
$ pipenv install boto3
$ pipenv install chalice
  1. 记住,通过pipenv安装的 Python 包只有在我们激活虚拟环境后才可用。一种方法是使用以下命令:
$ pipenv shell
  1. 接下来,在虚拟环境中,我们将创建一个名为 Capabilities 的 AWS Chalice 项目的编排层,使用以下命令:
$ chalice new-project Capabilities
  1. 要创建 chalicelib Python 包,发出以下命令:
cd Capabilities
mkdir chalicelib
touch chalicelib/__init__.py
cd ..

初始项目结构应如下所示:

Project Structure
------------
├── ContactAssistant/
 ├── Capabilities/
 ├── .chalice/
 ├── config.json
 ├── chalicelib/
 ├── __init__.py
 ├── app.py
 ├── requirements.txt
 ├── Pipfile
 ├── Pipfile.lock

项目结构与前几章中创建的结构略有不同。该项目结构包含编排层和服务实现层,但不包括 Web 用户界面。

智能助手服务实现

在当前实现中,联系人助手由 Lex 机器人支持,但良好的架构设计应该具有灵活性,能够轻松更换实现。该服务实现旨在将 Lex 实现的细节与客户端应用程序隔离开来。

chalicelib 中创建一个名为 intelligent_assistant_service.py 的 Python 文件,如下所示:

import boto3

class IntelligentAssistantService:
    def __init__(self, assistant_name):
        self.client = boto3.client('lex-runtime')
        self.assistant_name = assistant_name

    def send_user_text(self, user_id, input_text):
        response = self.client.post_text(
            botName = self.assistant_name,
            botAlias = 'Production',
            userId = user_id,
            inputText = input_text
        )

 return response['message']

在前面的代码中,发生了以下操作:

  • IntelligentAssistantService 是一个通用实现,可以配置为与不同的智能助手一起使用,而不仅仅是联系人助手。

  • __init__() 构造函数接收助手名称,以便在创建时为特定的智能助手配置自身。构造函数为 lex-runtime 创建一个 boto3 客户端,用于与已发布的 Lex 机器人进行通信。

  • IntelligentAssistantService 实现了 send_user_text() 方法,用于向助手发送文本聊天消息。此方法接收来自应用程序的 user_idinput_text,并使用 lex-runtimepost_text() 函数发送输入文本。

    • user_id 是由客户端应用程序创建的 ID。一个 Lex 机器人可以与多个不同的用户同时进行多个对话。该 user_id 标识一个用户;换句话说,它标识一个聊天会话。

还有一个来自 lex-runtimepost_content() 函数,用于发送文本和语音输入。除了 botNamebotAliasuserId 外,post_content() 函数还需要设置 contentTypeinputStream 参数。contentType 可以是音频或文本,支持几种音频格式。inputStream 包含音频或文本内容的字节流。

如果应用程序希望从 Lex 机器人接收音频响应,则应将 accept 参数设置为支持的音频输出格式之一。音频输入和输出格式是 Lex 的实现细节。任何音频输入和输出的格式转换应在此服务实现中执行,以隐藏这些细节给客户端应用程序。

联系人助手 RESTful 端点

让我们看看接下来的步骤:

  1. 现在,让我们在 app.py 中快速构建一个 RESTful 端点来连接联系人助手。通过这种方式,我们可以使用 curl 命令测试我们的 IntelligentAssistantService
from chalice import Chalice
from chalicelib import intelligent_assistant_service

import json

#####
# chalice app configuration
#####
app = Chalice(app_name='Capabilities')
app.debug = True

#####
# services initialization
#####
assistant_name = 'ContactAssistant'
assistant_service = intelligent_assistant_service.IntelligentAssistantService(assistant_name)

#####
# RESTful endpoints
#####
@app.route('/contact-assistant/user-id/{user_id}/send-text', methods = ['POST'], cors = True)
def send_user_text(user_id):
    request_data = json.loads(app.current_request.raw_body)

    message = assistant_service.send_user_text(user_id, request_data['text'])

    return message

RESTful 端点的实现简短且简单:

  • 初始化代码将我们的通用IntelligentAssistantService实现绑定到联系人助手

  • RESTful 端点本身通过 URL 接收user_id,并通过请求体中的 JSON 格式接收输入文本

  1. 在终端中使用以下命令启动chalice local环境:
$ chalice local
Restarting local dev server.
Found credentials in shared credentials file: ~/.aws/credentials
Serving on http://127.0.0.1:8000
  1. 现在,我们可以使用curl命令与联系人助手进行对话:
$ curl --header "Content-Type: application/json" --request POST --data '{"text": "Call John Smith"}' http://127.0.0.1:8000/contact-assistant/user-id/me/send-text
> Would you like me to call John Smith?

$ curl --header "Content-Type: application/json" --request POST --data '{"text": "Yes"}' http://127.0.0.1:8000/contact-assistant/user-id/me/send-text
> Calling John Smith at (202) 123-4567

在前述对话中,发生了以下情况:

  • 第一个curl命令发出了意图Call John Smith,其中包括联系人名字和姓氏所需的两个槽位。

  • 响应是来自联系人助手的确认,您希望我拨打 John Smith 的电话吗?

  • 第二个curl命令通过回复来继续对话。

  • 然后,联系人助手回应,正在拨打 John Smith 的电话,电话号码是(202) 123-4567

将利用联系人助手功能的应用程序将提供适当的用户界面,以最好地促进对话,例如,为旅行推销员设计的移动应用。该应用程序将通过 RESTful 端点传递用户与联系人助手之间的口头交流。

总结

在这一章中,我们构建了联系人助手,一个允许用户通过语音或文本对话界面搜索联系人信息的聊天机器人。我们使用 Amazon Lex 构建了联系人助手的对话界面。我们学习了 Amazon Lex 的开发范式,以构建定制的 AI 能力,涉及的概念包括意图、语句、提示和确认。联系人助手支持两个意图,LookupPhoneNumberByNameMakePhoneCallByName。这些意图的任务履行通过 AWS Lambda 实现。我们还通过使用 Amazon Lex 的会话属性设计了这两个意图,使其具有上下文感知能力;上下文感知减少了用户的认知负担,使得聊天机器人更加智能。

Amazon Lex 是我们在本书中介绍的最后一个 AWS AI 服务。在本书的下一部分,我们将介绍 AWS ML 服务,使用机器学习训练客户 AI 能力。

进一步阅读

有关使用 Amazon Lex 构建语音聊天机器人的更多信息,可以参考以下链接:

restechtoday.com/smart-speaker-industry/

www.lifewire.com/amazon-alexa-voice-assistant-4152107

www.nngroup.com/articles/intelligent-assistants-poor-usability-high-adoption/

第三部分:使用 Amazon SageMaker 训练机器学习模型

在本节中,您将学习如何在 AWS 上设计、开发和部署企业级机器学习解决方案。同时,本节将深入探讨处理大数据、数据并行化和模型部署过程中所面临的挑战。我们将通过讨论一些真实世界的案例来说明这些概念。

本节包括以下章节:

  • 第七章,使用 Amazon SageMaker

  • 第八章,创建机器学习推理管道

  • 第九章,文本集合中的主题发现

  • 第十章,使用 Amazon SageMaker 进行图像分类

  • 第十一章,通过深度学习和自动回归进行销售预测

第七章:使用 Amazon SageMaker

在过去几章中,你已经了解了可用的机器学习ML)API,它们解决了商业挑战。本章中,我们将深入探讨 AWS SageMaker——当机器学习 API 无法完全满足你的需求时,SageMaker 用于无缝地构建、训练和部署模型。SageMaker 通过抽象化计算和存储资源的复杂性,提升了数据科学家和机器学习工程师的生产力。

本章将涵盖的内容:

  • 通过 Spark EMR 处理大数据

  • 在 Amazon SageMaker 中进行训练

  • 部署训练好的模型并运行推断

  • 运行超参数优化

  • 理解 SageMaker 实验服务

  • 自带模型 – SageMaker、MXNet 和 Gluon

  • 自带容器 – R 模型

技术要求

在接下来的章节中,我们将使用名为goodbooks-10k的书籍评分数据集来说明之前提到的所有主题。该数据集包含来自 53,424 个用户对 10,000 本书籍的 600 万条评分。有关 goodbooks-10k 数据集的更多详情,请访问www.kaggle.com/zygmunt/goodbooks-10k#books.csv

在与本章相关的文件夹中,你将找到两个 CSV 文件:

  • ratings.csv:包含书籍评分、用户 ID、书籍 ID 和评分信息

  • books.csv:包含书籍属性,包括标题

现在是时候整理大数据以创建建模数据集了。

通过 Spark EMR 进行大数据预处理

在 SageMaker 中执行模型的设计模式是读取存储在 S3 中的数据。大多数情况下,这些数据可能并不容易直接使用。如果所需数据集较大,在 Jupyter 笔记本中整理数据可能不切实际。在这种情况下,可以使用 Spark EMR 集群对大数据进行操作。

在 Jupyter 笔记本中清理大数据集时会出现内存溢出错误。我们的解决方案是使用 AWS EMR(弹性 MapReduce)集群进行分布式数据处理。Hadoop 将作为底层分布式文件系统,而 Spark 将作为分布式计算框架使用。

现在,为了对 EMR 集群执行命令来处理大数据,AWS 提供了 EMR 笔记本。EMR 笔记本提供了一个基于 Jupyter Notebook 的托管笔记本环境。用户可以通过这些笔记本交互地处理大数据,进行可视化并准备适合分析的数据集。数据工程师和数据科学家可以使用多种语言(如 Python、SQL、R 和 Scala)来处理大量数据。这些 EMR 笔记本也可以定期保存到持久数据存储(如 S3),以便稍后检索已保存的工作。Amazon EMR 架构的关键组件之一是 Livy 服务。Livy 是一个开源的 REST 接口,用于与 Spark 集群交互,无需 Spark 客户端。Livy 服务使 EMR 笔记本与安装了该服务的 EMR 集群之间能够进行通信。

以下架构图详细说明了 EMR 笔记本如何与 Spark EMR 集群通信以处理大数据:

现在我们已经了解了 EMR 集群如何与 EMR 笔记本交互处理大数据,接下来我们将开始创建 EMR 笔记本和集群,如下所示:

  1. 在“服务”中导航到 Amazon EMR 并点击笔记本。

  2. 在创建笔记本页面,输入笔记本名称和描述,如下所示截图:

  1. 接下来,选择“创建集群”选项,输入集群名称,并选择实例类型和数量。如前面的截图所示,EMR 集群自带 Hadoop、Spark、Livy 和 Hive 应用程序。

  2. 现在,让我们回顾一下 EMR 角色和 EC2 实例配置文件的策略,并输入 EMR 笔记本将保存的 S3 位置,如下所示:

从前面的可视化图中,我们可以看到以下内容:

  • EMR 角色用于授予 EMR 服务访问其他 AWS 服务(例如,EC2)的权限。

  • EMR EC2 实例配置文件进一步使得由 EMR 启动的 EC2 实例可以访问其他 AWS 服务(例如,S3)。

  • 我们为 EMR 集群配置了适当的安全组,以允许 EMR 笔记本与 EMR 集群的主节点进行通信。

  • 我们还为 EMR 集群分配了一个服务角色,以便它能够与其他 AWS 服务进行交互。

  • 此外,EMR 笔记本在点击“保存”时会被保存到指定的 S3 位置。

  1. 现在,点击“创建笔记本”以启动一个新的 EMR 笔记本。笔记本和集群将开始创建,如下所示:

  1. 一旦 EMR 笔记本和集群创建完成,点击“打开”以打开笔记本。我们将使用 EMR 笔记本创建一个数据集,该数据集将通过 object2vec 算法向用户推荐书籍。object2vec 是一个内置的 SageMaker 算法,用于预测用户对书籍的亲和力。

在 EMR 笔记本中,我们做了以下五个步骤:

  1. 读取评分和书籍的 CSV 文件。

  2. 分析评分数据集,以了解每个用户和每本书的评分数量。

  3. 过滤原始评分数据集,只包含评分,其中包括评分超过 1%的用户和至少有 2%用户评分的书籍。

  4. 在评分数据集中为用户和书籍创建索引(从零开始)——这是训练object2vec算法所必需的。

  5. 将处理后的评分数据集(包括书名)写入相关的 S3 存储桶,并以parquet格式存储。评分数据集将包含丰富的用户偏好历史,以及书籍的受欢迎程度。

在下面的代码块中,我们将把 600 万条评分数据减少到约 100 万条:

# Filter ratings by selecting books that have been rated by at least 1200 users and users who have rated at least 130 books
fil_users = users.filter(F.col("count") >= 130)
fil_books = books.filter(F.col("count") >= 1200)

在前面的代码中,我们过滤了评分数据,保留了至少评分了 130 本书的用户和至少有 1200 个用户评分的书籍。

  1. 一旦准备好评分数据集,我们将把它持久化到 S3 存储桶中,如下所示:

从前面的截图中可以理解以下内容:

  • 由于数据在 EMR 集群上并行处理,输出包含多个parquet文件。

  • Apache Parquet 是 Apache Hadoop 生态系统中的一种开源压缩列式存储格式。

  • 与传统的行式数据存储方式相比,Parquet 允许我们在存储和性能方面更加高效。

  • 在将处理后的数据集存储到 S3 后,停止笔记本并终止集群,以避免不必要的费用。

现在,我们已经准备好理解内置的object2vec算法并开始训练模型。

在 Amazon SageMaker 中进行训练

让我们花几分钟时间理解一下object2vec算法是如何工作的。它是一种多用途算法,可以创建高维对象的低维嵌入。这一过程被称为维度约简,通常通过一种叫做主成分分析PCA)的统计方法来实现。然而,Object2Vec 使用神经网络来学习这些嵌入。

这些嵌入的常见应用包括客户细分和产品搜索。在客户细分的情况下,相似的客户在低维空间中会彼此接近。客户可以通过多个属性进行定义,如姓名、年龄、家庭地址和电子邮件地址。关于产品搜索,由于产品嵌入捕捉到基础数据的语义,任何搜索词的组合都可以用来检索目标产品。这些搜索词的嵌入(语义)应与产品的嵌入相匹配。

让我们看看 Object2Vec 是如何工作的。

学习 Object2Vec 是如何工作的

Object2vec 可以学习对象对的嵌入。在我们的案例中,书籍的评分越高,用户与书籍之间的关系就越强。这个想法是,具有相似品味的用户往往会给相似的书籍更高的评分。Object2vec 通过使用用户和书籍的嵌入来近似书籍评分。用户与某些书籍的接近程度越高,用户对这些书籍的评分也会越高。我们为算法提供 (user_indbook_ind) 对;对于每一对这样的组合,我们还提供一个 标签,告诉算法用户与书籍是否相似。在我们的案例中,标签就是书籍评分。因此,训练好的模型可以用来预测某个给定用户的书籍评分,例如用户从未评分过的书籍。

以下是 object2vec 工作原理的概念图:

从前面的图示中,我们可以看到以下内容:

  • 我们可以看到,用户和项目或书籍的嵌入被拼接在一起,然后传递给 多层感知机MLP)。

  • 用户和书籍的嵌入是通过用户和书籍索引的独热编码表示生成的。

  • 通过监督学习,MLP 可以学习网络的权重,这些权重可以用来预测用户-书籍对的评分或分数。

为了更好地理解 object2vec 的内部工作原理,请参见以下截图:

从前面的图示中,我们可以看到以下内容:

  • Object2vec 从使用独热编码(one-hot encoding)表示用户和书籍开始。具体来说,在我们的案例中,用户可以用大小为 12,347 的数组表示,这意味着数据集中总共有 12,347 个独特的用户。

  • 用户 #1 可以通过在位置 1 上表示 1,而数组中的所有其他位置为 0 来表示。

  • 书籍也可以以类似的方式表示。

  • 现在是时候减少这些表示的维度了。因此,算法使用了一个嵌入层,包含 1,024 个神经元,每个神经元对应一个用户和一本书。

  • Object2vec 通过对 1,024 个用户嵌入神经元和 1,024 个项目嵌入神经元进行逐元素的乘法和减法操作,进一步提取额外的特征。

换句话说,用户和书籍的嵌入是以不同的方式进行比较的。总的来说,当所有前一层的神经元被合并时,我们将有 4,096 个神经元。然后,算法使用一个包含 256 个神经元的感知机层。这个感知机层与输出层的一个神经元完全连接。这个神经元将预测用户给书籍的评分。

现在是时候训练 Object2Vec 算法了

训练 Object2Vec 算法

现在我们已经理解了算法的工作方式,让我们深入了解训练过程:

  1. 数据处理:以 JSON 行的形式输入数据;对数据进行随机打乱,以实现最佳性能。如您稍后将看到的,我们以用户索引书籍索引标签=评分的格式发送数据。

  2. 模型训练:我们将训练数据和验证数据传递给算法。我们可以配置多个超参数来微调模型的性能。我们将在接下来的部分中回顾它们。我们的目标函数是最小化均方误差MSE)。误差是标签(实际值)和预测评分之间的差异。

一旦模型训练完成,我们将其部署为推理端点。

在数据处理过程中,我们将执行以下操作:

  1. 首先,我们将读取存储在 S3 存储桶中的 parquet 格式评分数据集,如下代码所示:
s3 = s3fs.S3FileSystem()

s3_bucket = 's3://ai-in-aws1/'
input_prefix = 'Chapter7/object2vec/bookratings.parquet'
dataset_name = s3_bucket + input_prefix

df_bkRatngs = pq.ParquetDataset(dataset_name, filesystem=s3).read_pandas().to_pandas()

在前面的代码中,我们可以看到以下内容:

  • s3fs 是一个基于 boto3 的 Python 库,boto3 是 AWS 的 Python SDK。s3fs 提供了一个 S3 文件系统接口。

  • 我们使用 pyarrow Python 库从指定的 S3 存储桶读取分区的 parquet 文件。

  • 具体来说,我们通过传入数据集名称和文件系统,调用了 ParquetDataset() 函数。

  1. 在读取数据集之后,我们会显示它,以确保数据被正确读取,如下图所示:

然后,我们以 Object2Vec 算法所需的格式加载数据框。对于每个用户-书籍对和评分标签,我们通过调用 load_df_data() 函数在数据列表中创建一个条目。有关详细信息,请参考本章附带的源代码。

在模型训练中,我们首先将数据集划分为训练集、验证集和测试集。对于每个数据集,我们调用 write_data_list_to_jsonl() 函数来创建 .jsonl(JSON 行)文件,这是 object2vec 所需的格式。以下截图展示了一个 jsonl 文件的示例:

  1. 然后,我们将准备好的数据集上传到指定的 S3 存储桶。

  2. 我们获取了 Object2Vec 算法的 Docker 镜像,如下所示:

container = get_image_uri(boto3.Session().region_name, 'object2vec') 

在前面的代码中,我们可以看到以下内容:

  • 为了获取 object2vec Docker 镜像的统一资源标识符URI),我们通过传入本地 SageMaker 会话的区域名称和算法名称作为输入,调用了 get_image_uri() 函数。

  • get_image_uri() 函数是 SageMaker Python SDK 的一部分。

获取到 object2vec 算法的 uri 后,我们定义超参数,如下所示:

  • 编码器网络:包括以下内容:

    • enc0_layers:这是编码器网络中的层数。

    • enc0_max_seq_len:这是发送到编码器网络的最大序列数(在本例中,仅发送一个用户序列到网络)。

    • enc0_network: 这定义了如何处理嵌入。在这种情况下,由于我们一次处理一个用户嵌入,因此不需要聚合。

    • enc0_vocab_size: 这定义了第一个编码器的词汇表大小。它表示数据集中用户的数量。

由于网络中有两个编码器,因此相同的超参数适用于编码器 1。对于编码器 1,词汇表大小需要适当定义,即数据集中书籍的数量——enc1_vocab_size: 985

  • MLP: 这包括以下内容:

    • mlp_dim: 这是 MLP 层中的神经元数量。在我们的实验中,我们将其设置为 256。

    • mlp_layers: 这是 MLP 网络中的层数。在我们的实验中,我们使用的是单层。

    • mlp_activation: 这是 MLP 层的激活函数。在我们的实验中,我们使用修正线性单元ReLU)激活函数,以加速收敛并避免梯度消失问题。请注意,ReLU 激活函数由以下公式给出:

  • 以下实例控制 object2vec 的训练过程:

    • epochs: 这是向后和向前传播的次数。在我们的实验中使用的是 10。

    • mini_batch_size: 这是在更新权重之前处理的训练样本数量。我们使用 64。

    • early_stopping_patience: 这是在停止之前执行的最大坏的 epoch 数(即损失没有改善的 epoch)。我们使用的是 2。

    • early_stopping_tolerance: 这是在连续两个 epoch 之间,损失函数所需的改善量,只有在耐心 epoch 数结束后,训练才会继续。我们为该参数使用 0.01。

  • 其他 包括以下内容:

    • optimizer: 这是优化算法,用于找到最优的网络参数。在本实验中,我们使用自适应矩估计(也称为 Adam)。它为每个参数计算个别的学习率。与密集数据的特征或输入相比,稀疏数据的参数会进行较大的更新。此外,Adam 还会为每个参数计算个别的动量变化。请记住,在反向传播时,导航至正确的方向对加速收敛至关重要。动量变化有助于朝正确的方向导航。

    • output_layer: 这定义了网络是分类器还是回归器。在这种情况下,由于网络试图学习评分,因此我们将输出层定义为均方误差(线性)。

在定义了超参数后,我们将 object2vec 估算器拟合到准备好的数据集(训练集和验证集),如下所示的代码:

# create object2vec estimator
regressor = sagemaker.estimator.Estimator(container, role, train_instance_count=1, 
 train_instance_type='ml.m5.4xlarge', output_path=output_path, sagemaker_session=sess)

# set hyperparameters
regressor.set_hyperparameters(**static_hyperparameters)

# train and tune the model
regressor.fit(input_paths)

在前面的代码中,我们正在做以下操作:

  1. 我们首先通过传递 Docker 镜像、当前执行角色、训练实例的数量和类型,以及当前的 sagemaker 会话来创建一个 object2vec 估算器。

  2. 然后,使用 set_hyperparameters() 函数为新创建的 object2vec 评估器设置超参数。

  3. 然后,我们使用 Estimator 对象的 fit() 函数将模型拟合到训练和验证数据集。

  4. 训练持续时间取决于训练实例类型和实例数量。对于一个 m5.4xlarge 机器学习实例,完成 10 个周期花费了 2 小时。

要监视正在进行的训练作业,请导航到 SageMaker 服务左侧的 Training 部分。单击 Training Jobs,然后单击当前作业的作业名称。然后,导航到监控部分以查看训练作业的进度,如下截图所示:

正如在前面的截图中所示,随着训练 MSE 的减少,验证 MSE 也在减少,尽管在验证数据集中,误差的减少不及训练数据集的减少。通过此仪表板还可以监控训练吞吐量。

现在训练完成了,让我们将训练好的模型作为端点进行部署。

部署训练好的 Object2Vec 并进行推断

现在,让我们部署训练好的 object2vec 模型。SageMaker SDK 提供了方法,使我们可以无缝部署训练好的模型:

  1. 首先,我们将使用 SageMaker Estimator 对象的 create_model() 方法从训练作业创建模型,如下所示的代码:
from sagemaker.predictor import json_serializer, json_deserializer

# create a model using the trained algorithm
regression_model = regressor.create_model(serializer=json_serializer,
 deserializer=json_deserializer,content_type='application/json')

将序列化器和反序列化器类型传递给 create_model() 方法,在推断时用于负载。

  1. 一旦模型创建完成,可以通过 SageMaker Model 对象的 deploy() 方法将其部署为端点,如下所示的代码:
# deploy the model
predictor = regression_model.deploy(initial_instance_count=1, instance_type='ml.m4.xlarge')

deploy() 方法中,我们已指定启动托管端点所需的实例数量和类型。

  1. 一旦 object2vec 模型部署为端点,我们可以导航到左侧导航菜单下的 Inference 分组下的 Endpoints 部分。可以在此查看部署端点的状态,如下截图所示:

现在 object2vec 端点已准备就绪,让我们进行推断。

  1. 通过传递端点名称以及输入和输出的序列化和反序列化类型,我们将创建 RealTimePredictor 对象(SageMaker Python SDK)。请参阅以下代码,了解如何初始化 RealTimePredictor 对象:
from sagemaker.predictor import RealTimePredictor, json_serializer, json_deserializer

predictor = RealTimePredictor(endpoint='object2vec-2019-08-23-21-59-03-344', sagemaker_session=sess, serializer=json_serializer, deserializer=json_deserializer, content_type='application/json')

您可以更改端点名称以反映您当前的端点(RealTimePredictor 对象的第一个参数)。

  1. 然后我们调用 RealTimePredictorpredict() 方法,如下所示的代码:
# Send data to the endpoint to get predictions

prediction = predictor.predict(test_data)
print("The mean squared error on test set is %.3f" %get_mse_loss(prediction, test_label))

在前面的代码中,请记住,test_data应该是一个object2vec可以处理的格式。我们使用data_list_to_inference_format()函数将测试数据转换为两个组件:实例和标签。有关此函数的详细信息,请参阅本章相关的源代码。请查看以下截图,了解测试数据的结构:

如上图所示,in0(输入 0)和in1(输入 1)的索引分别应为用户和书籍的索引。至于测试标签,我们为每个关联的用户-书籍对生成一个评分数据列表,如下图所示:

如上图所示,我们将测试数据集中的前 100 个用户-书籍对传递给RealTimePredictorpredict()方法。结果是 MSE 为 0.110。

  1. 现在,让我们将这个 MSE 与通过简单方法计算书籍评分的 MSE 进行比较:
  • 基准 1:对于测试数据集中的每个用户-书籍对,计算评分,即所有用户的平均书籍评分,如下代码所示:
train_label = [row['label'] for row in copy.deepcopy(train_list)]
bs1_prediction = round(np.mean(train_label), 2)
print("The validation mse loss of the Baseline 1 is {}".format(get_mse_loss(len(test_label)*[bs1_prediction], test_label)))

为了计算所有用户的平均评分,我们执行以下操作:

    • 我们遍历训练数据集中的所有评分,以创建标签列表train_label

    • train_label接着被用来计算均值。为了计算 MSE,在get_mse_loss()函数中,从test_label中的每个评分中减去所有用户的平均评分。

    • 然后将误差平方,并在所有测试用户中求平均。有关详细信息,请参阅附带的源代码。这个选项的 MSE 为 1.13。

  • 基准 2:对于测试数据集中的每个用户-书籍对,我们计算评分,即该用户的平均书籍评分(即该用户所有评分书籍的平均评分),如下代码所示:

def bs2_predictor(test_data, user_dict):
  test_data = copy.deepcopy(test_data['instances'])
  predictions = list()
  for row in test_data:
    userID = int(row["in0"][0])

bs2_predictor()函数中,我们将测试数据和来自训练数据集的用户字典作为输入。对于测试数据中的每个用户,如果该用户存在于训练数据集中,我们计算该用户评分的所有书籍的平均评分。如果该用户不存在于训练数据集中,我们就获取所有用户的平均评分,如下代码所示:

    if userID in user_dict:
      local_books, local_ratings = zip(*user_dict[userID])
      local_ratings = [float(score) for score in local_ratings]
      predictions.append(np.mean(local_ratings))
    else:
      predictions.append(bs1_prediction)

   return predictions

在前面的bs2_predictor()函数中,zip(*)函数用于返回每个用户的书籍和评分列表。bs1_prediction是训练数据集中所有用户的平均评分。这个选项的 MSE 为 0.82。

如我们所见,object2vec的 MSE 为 0.110,优于基准模型:

  • 基准 1 MSE:1.13,其中预测的书籍评分是所有用户的全局平均书籍评分

  • 基准 2 MSE:0.82,其中预测的书籍评分是用户的平均书籍评分

现在我们已经训练并评估了内置的 SageMaker 算法 object2vec,是时候了解 SageMaker 提供的功能,以便我们能够自动化超参数调优。

运行超参数优化(HPO)

数据科学家通常需要花费大量时间和实验才能找到适合最佳模型性能的超参数集。这个过程大多依赖于试错法。

尽管 GridSearch 是数据科学家传统上使用的技术之一,但它面临维度灾难问题。例如,如果我们有两个超参数,每个超参数有五个可能的值,那么我们需要计算目标函数 25 次(5 x 5)。随着超参数数量的增加,计算目标函数的次数呈指数级增长。

随机搜索通过随机选择超参数的值来解决这个问题,而不是对每一个超参数组合进行穷举搜索。Bergstra 等人发表的 论文 认为,超参数空间的随机搜索比网格搜索更有效。

这个理念是,一些参数对目标函数的影响远小于其他参数。这一点通过网格搜索中每个参数选择的值的数量得到体现。随机搜索使得在多次试验下可以探索每个参数的更多值。以下是一个示意图,展示了网格搜索和随机搜索之间的区别:

如前面的截图所示,在随机搜索中,我们可以测试更多重要参数的值,从而通过训练模型提升性能。

这两种技术都没有自动化超参数优化过程。超参数优化HPO),来自 SageMaker,自动化了选择最佳超参数组合的过程。以下是该工具的工作原理:

请看以下几点:

  • HPO 使用贝叶斯技术,逐步选择超参数组合来训练算法。

  • HPO 根据模型的性能和所有历史步骤中超参数的配置,选择下一组超参数。

  • 此外,它还使用一个 采集函数 来确定下一个最佳机会,从而降低成本函数。

  • 在指定的迭代次数后,您将获得一个最佳的超参数配置,从而生成最佳模型。

对于 object2vec 算法,让我们选择我们想要调整的超参数:

  • learning_rate:控制神经网络中权重优化的速度

  • dropout:在前向和反向传播中,忽略的神经元百分比

  • enc_dim:用于生成用户/项目嵌入的神经元数量

  • mlp_dim:MLP 层中神经元的数量

  • weight_decay:防止过拟合的因子(L2 正则化——使权重按指定的因子衰减)

我们将使用sagemaker Python SDK 中的HyperparameterTuner类来创建调优作业。调优作业的目标是减少验证数据集的 MSE。根据您的预算和时间,您可以选择运行的训练作业数量。在这种情况下,我选择运行 10 个作业,并且每次只运行一个作业。您也可以选择并行运行多个作业。

要实例化超参数调优作业,我们需要执行以下操作:

  • 定义要调优的超参数并指定目标函数,如下代码所示:
tuning_job_name = "object2vec-job-{}".format(strftime("%d-%H-%M-%S", gmtime())) 

hyperparameters_ranges = { 
"learning_rate": ContinuousParameter(0.0004, 0.02),
"dropout": ContinuousParameter(0.0, 0.4),
"enc_dim": IntegerParameter(1000, 2000),
"mlp_dim": IntegerParameter(256, 500), 
"weight_decay": ContinuousParameter(0, 300) }

objective_metric_name = 'validation:mean_squared_error'

如前面的代码所示,我们定义了每个超参数的范围。对于目标函数,我们指定其为验证数据集中的均方误差:

  • 定义一个估算器来训练object2vec模型。

  • 通过传递估算器、目标函数及其类型,以及要运行的最大作业数,来定义HyperparameterTuner作业,如下所示:

tuner = HyperparameterTuner(regressor, objective_metric_name, hyperparameters_ranges, objective_type='Minimize', max_jobs=5, max_parallel_jobs=1)

HyperparameterTuner对象以估算器(名为regressor)作为输入之一。该估算器应初始化超参数,以及要启动的实例数量和类型。请参见本章的相关源代码。

  • 将调优器拟合到训练集和验证集数据集,如以下代码所示:
tuner.fit({'train': input_paths['train'], 'validation': input_paths['validation']}, job_name=tuning_job_name, include_cls_metadata=False)
tuner.wait()

对于hyperparameterTunerfit方法,我们传递了训练和验证数据集的位置。我们等待调优器完成所有作业的运行。

下图显示了由HyperparameterTuner执行的一些训练作业,它们使用了不同的超参数集:

对于每个作业,您可以查看所使用的超参数以及目标函数的值。

要查看具有最低 MSE 的最佳作业,请导航到最佳作业标签,如下图所示:

作业执行后,您可以对超参数优化的结果进行分析,以回答一些问题,例如,随着调优作业的执行,MSE 如何变化?您还可以查看 MSE 与正在调优的超参数(例如学习率、丢弃率、权重衰减、编码器和mlp的维度数量)之间是否存在相关性。

在以下代码中,我们绘制了随着训练作业的执行,MSE 如何变化:

objTunerAnltcs = tuner.analytics()
dfTuning = objTunerAnltcs.dataframe(force_refresh=False)
p = figure(plot_width=500, plot_height=500, x_axis_type = 'datetime') 
p.circle(source=dfTuning, x='TrainingStartTime', y='FinalObjectiveValue')
show(p)

在前面的代码中,我们从HyperparameterTuner创建了一个分析对象,该对象是我们之前创建的。然后,我们从分析对象中获取一个 DataFrame——该 DataFrame 包含了所有由调优器执行的训练作业的元数据。接着,我们将均方误差(MSE)与时间进行绘图。

在下图中,我们跟踪了 MSE 如何随训练时间变化:

如你所见,图表波动较大。如果你增加训练任务的数量,也许超参数调优任务会收敛。

现在是时候了解 SageMaker 的另一个重要特性了,那就是实验服务或搜索功能。

理解 SageMaker 实验服务

使用 SageMaker Search 进行实验管理的目标是加速模型的开发和实验阶段,提高数据科学家和开发人员的生产力,同时缩短机器学习解决方案的整体上市时间。

机器学习生命周期(持续实验与调优)表明,当你启动新学习算法的训练以提升模型性能时,你会进行超参数调优。在每次调优迭代中,你都需要检查模型性能如何提升。

这导致了成百上千个实验和模型版本。整个过程减缓了最终优化模型的选择。此外,监控生产模型的性能至关重要。如果模型的预测性能下降,了解真实数据与训练和验证过程中使用的数据有何不同非常重要。

SageMaker 的 Search 通过提供以下功能,解决了我们之前提到的所有挑战:

  • 组织、跟踪和评估模型训练实验:创建获胜模型的排行榜,记录模型训练运行,并通过训练损失、验证准确率等性能指标比较模型。

  • 无缝搜索并检索最相关的训练运行:可以按关键属性搜索的运行,这些属性可以是训练任务名称、状态、开始时间、最后修改时间、失败原因等。

  • 跟踪已部署模型在实时环境中的来源:跟踪使用的训练数据、指定的超参数值、模型的表现以及已部署模型的版本。

让我们来说明 SageMaker Search 的特点:

  1. 在 Amazon SageMaker 服务的左侧导航栏中,导航到 Search。

  2. 搜索使用object2vec算法进行的实验:

    1. 在 Search 面板中,选择 Property 下的 AlgorithmSpecification.TrainingImage。

    2. 在 Operator 下,选择 Contains。

    3. 在 Value 下,选择 object2vec,如下代码所示:

你也可以通过编程方式使用boto3(AWS 的 Python SDK)搜索实验,如下所示:

sgmclient = boto3.client(service_name='sagemaker')
results = sgmclient.search(**search_params)

在前面的代码中,我们通过传递服务名称实例化了sagemaker客户端。

  1. 然后,我们将通过传递搜索参数来调用 SageMaker 客户端的搜索功能,如下代码所示:
search_params={ "MaxResults": 10, "Resource": "TrainingJob",
"SearchExpression": {
 "Filters": [{"Name": "AlgorithmSpecification.TrainingImage","Operator": "Equals","Value": "Object2Vec"}]},
 "SortBy": "Metrics.validation:mean_squared_error",
 "SortOrder": "Descending"}

在前面的代码块中,我们定义了搜索参数,如要搜索的资源类型、显示的最大结果数、搜索表达式、排序方式和顺序。我们将已定义的搜索参数传递给 SageMaker 客户端的搜索函数,以检索结果:

要找到获胜的训练作业,请执行以下操作:

  1. 搜索实验,如前所述。我们可以根据多个属性进行搜索,例如与TrainingJobTuningJobAlgorithmSpecificationInputDataConfigurationResourceConfiguration相关的字段。

  2. 检索相关实验后,我们可以根据目标指标对其进行排序,以找到获胜的训练作业。

部署最佳模型,请按照以下步骤操作:

  1. 单击获胜的训练作业,然后点击顶部的“创建模型”按钮。

  2. 指定模型工件的位置和推理镜像的注册路径等详细信息,以创建一个模型。模型创建后,导航到 SageMaker 服务的推理部分(左侧导航菜单)下的“模型”。

  3. 您将看到两个选项:创建批处理转换作业和创建端点。对于实时推理,点击“创建端点”并提供配置详细信息。

要跟踪已部署模型的 lineage(继承关系),请执行以下操作:

  1. 在左侧导航窗格中选择端点,并选择获胜模型的端点。

  2. 向下滚动到端点配置设置,以找到用于创建端点的训练作业的超链接。

  3. 单击超链接后,您应该能看到有关模型和训练作业的详细信息,如下图所示:

您还可以通过编程方式跟踪已部署模型的 lineage:

  1. 使用 boto3 通过调用 SageMaker 客户端的 describe_endpoint_config() 函数来获取端点配置。

  2. 从配置中选择模型名称以检索模型数据 URL。

  3. 从模型数据 URL 检索训练作业。通过这样做,我们可以从已部署的端点追溯到训练作业。

现在,让我们关注 SageMaker 如何让数据科学家将自己的机器学习和深度学习库引入 AWS。

带上您自己的模型 – SageMaker、MXNet 和 Gluon

本节重点介绍 SageMaker 如何允许您将自己的深度学习库引入 Amazon Cloud,并仍然利用 SageMaker 的生产力特性,自动化大规模的训练和部署。

我们将引入的深度学习库是 Gluon:

  • Gluon 是由 AWS 和 Microsoft 共同创建的开源深度学习库。

  • 该库的主要目标是允许开发者在云端构建、训练和部署机器学习模型。

过去,关于推荐系统进行了大量研究。特别是,深度结构化语义模型试图捕捉来自属性的信息,例如产品图片、标题和描述。从这些附加特征中提取语义信息将解决推荐系统中的冷启动问题。换句话说,当给定用户的消费历史较少时,推荐系统可以建议与用户购买的最少产品相似的产品。

让我们看看如何通过 gluonnlp 库提供的预训练词嵌入,在 SageMaker 中找到与用户喜欢的书籍相似的书籍,即推荐与用户喜欢的书籍标题在语义上相似的书籍。

为此,我们将查看本章前面部分中使用的相同书籍评分数据集:

  1. 让我们先安装必要的依赖:

    • mxnet:这是一个深度学习框架。

    • gluonnlp:这是一个建立在 MXNet 之上的开源深度学习库,用于自然语言处理NLP)。

    • nltk:这是一个 Python 自然语言工具包。

  2. 接下来,我们将读取在 Amazon SageMaker 中进行训练 部分创建的过滤后的书籍评分数据集。然后,我们将从数据集中获取唯一的书名。

  3. 从每个书名中删除带有标点符号、数字和其他特殊字符的单词,只保留包含字母的单词,如下代码所示:

words = []

for i in df_bktitles['BookTitle']:
    tokens = word_tokenize(i)
    words.append([word.lower() for word in tokens if word.isalpha()])

在前面的代码块中,我们可以看到以下内容:

  • 我们遍历每个书名,并通过调用 nltk.tokenize 中的 word_tokenize() 函数来创建词元。

  • 对于每个书名,我们通过调用 isapha() 方法检查单词字符串,确保只保留包含字母的单词。最终,我们得到了一个名为 words 的词元列表的列表。

  1. 接下来,我们将计算所有书名中词元的频率,如下所示:
counter = nlp.data.count_tokens(itertools.chain.from_iterable(words))

在前面的代码中,我们可以看到以下内容:

  • 为了计算词元的频率,我们通过传递单词列表调用了 gluonnlp.data 中的 count_tokens() 函数。

  • counter 是一个字典,包含词元(键)和相关的频率(值)。

  1. 加载通过 fastText 训练的预训练词嵌入向量——fastText 是 Facebook AI 研究实验室开发的一个库,用于学习词嵌入。然后,将词嵌入与书名中的每个单词关联,示例如下:
vocab = nlp.Vocab(counter)
fasttext_simple = nlp.embedding.create('fasttext', source='wiki.simple')
vocab.set_embedding(fasttext_simple)

在前面的代码块中,我们可以看到以下内容:

  • 我们通过实例化 Vocab 类,创建了可以与词元嵌入相关联的词元索引。

  • 然后,我们通过将嵌入类型设置为 fasttext 来实例化词/词元嵌入。

  • 我们调用了 Vocab 对象的 set_embedding() 方法,将预训练的词嵌入附加到每个词元上。

  1. 现在,我们通过对单个词嵌入进行平均处理来创建书名的嵌入,示例如下:
for title in words:
title_arr = ndarray.mean(vocab.embedding[title], axis=0, keepdims=True)
title_arr_list = np.append(title_arr_list, title_arr.asnumpy(), axis=0)

在前面的代码中,我们可以看到以下内容:

  • 我们遍历了每个书名,并通过对书名中所有单词的嵌入进行平均来计算它的嵌入。这是通过调用 ndarray 对象的 mean() 方法实现的,ndarray 是一个 n 维数组。

  • 然后,我们通过使用 numpy 模块的 append() 方法创建了一个标题嵌入数组 title_arr_list

  1. 现在是时候绘制书名了——首先,我们将把嵌入的维度从 300 维减少到 2。请注意,title_arr_list 的形状是 978 x 300。这意味着数组包含 978 个独特的书名,每个书名由一个 300 维的向量表示。我们将使用 T-分布随机邻域嵌入TSNE)算法来减少维度,同时保留原始含义——即在高维空间中的书名之间的距离将与在低维空间中的距离相同。为了将书名投射到低维空间,我们实例化 sklearn 库中的 TSNE 类,如下代码所示:
tsne = TSNE(n_components=2, random_state=0)
Y = tsne.fit_transform(title_arr_list)

在前面的代码块中,我们调用了 TSNE 对象的 fit_transform() 方法,以返回转换后的嵌入版本。

在我们获取转换后的嵌入后,我们将做一个散点图,其中一个维度位于 x 轴,另一个维度位于 y 轴,如下图所示:

书名的接近性意味着它们在语义上是相似的。例如,像 RoomA Room with a View 这样的标题似乎都在讨论同一个主题——房间。这些标题在低维空间中彼此相近。

在本节中,您学习了如何通过 MXNet 深度学习库将预训练的 fastText 词嵌入引入 SageMaker。您还可以从头开始训练使用 MXNet 深度学习库构建的神经网络。SageMaker 的相同功能,如训练和部署,适用于内置算法和自定义算法。

现在我们已经介绍了如何将您的机器学习和/或深度学习库引入 SageMaker,接下来是如何带入您自己的容器。

带入您自己的容器——R 模型

在本节中,我们将展示如何将您自己的 Docker 容器引入 Amazon SageMaker。特别地,我们将重点讨论如何在 Amazon SageMaker 中无缝地训练和托管 R 模型。数据科学家和机器学习工程师可以在 SageMaker 中重用他们在 R 中已经完成的工作,而不是重新发明轮子来使用 SageMaker 的内置算法构建 ML 模型。

以下是有关 AWS 不同组件如何相互作用以训练和托管 R 模型的架构:

为了遵循前面的架构图,我们从 Amazon 弹性容器注册表ECR)开始:

  1. 我们创建了一个包含底层操作系统、训练推荐算法所需的先决条件和 R 代码(用于训练和评分基于用户的协同过滤UBCF)推荐算法)的 Docker 镜像。

  2. 创建的 Docker 镜像随后发布到 Amazon ECR。请记住,无论是 SageMaker 内建算法还是自定义算法的训练数据,都位于 S3 存储桶中。

  3. 要启动 SageMaker 中的训练任务,您需要指定训练数据的位置和训练镜像的 Docker 注册路径(在 ECR 中)。

  4. 在训练过程中,会触发适当的 R 函数来训练 UBCF 算法。训练发生在 SageMaker 的机器学习计算实例上。

  5. 结果训练好的模型称为模型工件,保存在 S3 存储桶的指定位置。

至于托管训练后的模型,SageMaker 需要两项内容:

  • 模型工件

  • 推断镜像的 Docker 注册路径

要创建推断端点,必须执行以下操作:

  1. SageMaker 将通过传递 R 模型的推断镜像 Docker 注册路径和模型工件来创建模型。

  2. 一旦创建了 SageMaker 模型,SageMaker 会通过实例化 Docker 推断镜像来启动机器学习计算实例。

  3. 计算实例将提供用于推断的 R 代码,作为一个 RESTful API。

在这一部分中,我们将查看在本章前面部分使用的相同书籍评分数据集——goodbooks-10k。我们的目标是向不在训练数据集中的用户推荐排名前五的书籍。我们将使用recommenderlab R 包来衡量用户之间的余弦距离(UBCF)。对于目标用户,我们将从训练集中根据余弦相似度选择 10 个用户/邻居。

为了估算目标用户的前五个书籍推荐,UBCF 算法使用两项内容:

  • 目标用户对某些书籍的偏好

  • 训练后的模型

在训练好的模型的帮助下,我们将为目标用户以前未评分的书籍计算评分。然后,向给定用户推荐评分最高的五本书(在数据集中的所有书籍中)。训练好的模型会为所有书籍和训练数据集中的所有用户填充评分,如下所示:

在训练过程中,UBCF 会计算缺失的评分。假设我们要填充用户 A 的缺失评分。用户 A 只对书籍#1BK1)和书籍#3BK3)进行了评分。为了计算书籍 2、4 和 5 的评分,UBCF 算法会执行以下操作:

  • 它计算用户 A 与训练数据集中其他用户之间的余弦相似度。为了计算用户 A 与 B 之间的相似度,我们执行以下操作:
  1. 如果用户 A 和 B 有共同评分的书籍,则通过书籍的评分进行乘法计算。

  2. 将这些评分添加到所有共享书籍中。

  3. 然后,将结果除以由用户 A 和 B 表示的向量的范数。

  • 给定用户 B 到 E 相对于 A 的相似度评分,通过计算用户 B 到 E 给定的该书评分的加权平均值,来计算用户 A 对某本新书的评分:
  1. 例如,要计算用户 A 对书籍 #2 的评分,我们将用户 B 对书籍 #2 给出的评分 3 乘以相似度评分 0.29,然后将用户 C 对书籍 #2 给出的评分 4 乘以相似度评分 0.73。最后,将这两个因子相加。

  2. 然后,我们将相似度评分 0.29 和 0.73 相加。

  3. 最后,我们用 1 除以 2 得到结果。

现在我们已经查看了 SageMaker 中自定义容器的训练和托管架构,并讨论了使用案例,让我们开始实现:

  1. 第一步是通过突出显示运行 R 代码所需的要求来定义 Dockerfile。要求包括操作系统、R 版本、R 包以及用于训练和推理的 R 逻辑的位置。创建并将 Docker 镜像发布到 EC2 容器注册表ECR)。

以下 Dockerfile 定义了训练和托管 R 模型的规格:

FROM ubuntu:16.04

RUN apt-get -y update --allow-unauthenticated && apt-get install -y --no-install-recommends \
 wget \
 r-base \
 r-base-dev \
 ca-certificates

在前面的代码块中,我们可以看到以下内容:

  • 我们定义了要安装的 Ubuntu 操作系统的版本。

  • 我们还指定了需要安装 R。

  • 此外,我们还指定了 Recommender 算法所需的 R 包,如以下代码所示:

RUN R -e "install.packages(c('reshape2', 'recommenderlab', 'plumber', 'dplyr', 'jsonlite'), quiet = TRUE)"

COPY Recommender.R /opt/ml/Recommender.R
COPY plumber.R /opt/ml/plumber.R

ENTRYPOINT ["/usr/bin/Rscript", "/opt/ml/Recommender.R", "--no-save"]

在前面的代码中,我们可以看到以下内容:

  • 我们将训练(Recommender.R)和推理(plumber.R)代码复制到适当的位置。

  • 后续我们指定了一个入口点(运行的代码),在 Docker 镜像实例化后执行。

既然 Dockerfile 已经编译完成,接下来是创建 Docker 镜像并将其推送到 ECR,如下所示:

Docker build -t ${algorithm_name}.
Docker tag ${algorithm_name} ${fullname}
Docker push ${fullname}

在前面的代码中,我们可以看到以下内容:

  • 为了在本地构建 Docker 镜像,我们通过将镜像名称传递给本地的 SageMaker 实例来运行 Docker build 命令。

  • 本地目录(".")中的 Dockerfile 被利用。

  • 在标记 Docker 镜像后,我们通过 Docker push 命令将其推送到 ECR。

  1. 下一步是创建一个 SageMaker 训练作业,列出训练数据集、最新的用于训练的 Docker 镜像和基础设施规格。训练作业的模型工件会存储在相关的 S3 存储桶中。这与在 SageMaker 上运行任何训练作业非常相似。

让我们了解一下训练过程中触发的 R 函数:

  • 请记住,Recommender.R 代码会在启动机器学习计算实例进行训练时执行。

  • 根据传递的命令行参数,train() 函数或 serve() 函数会被执行,如下所示:

args <- commandArgs()
if (any(grepl('train', args))) {
 train()}
if (any(grepl('serve', args))) {
 serve()}
  • 如果命令行参数包含 train 关键字,则执行 train() 函数。对于 serve 关键字,逻辑也是一样的。

在训练过程中,SageMaker 将训练数据集从 S3 存储桶复制到 ML 计算实例。我们准备好模型拟合的训练数据后,通过指定训练集中的用户数量、推荐算法类型以及输出类型(如前 N 本书推荐)来调用Recommender方法(recommenderlab R 包),如下所示:

rec_model = Recommender(ratings_mat[1:n_users], method = "UBCF", param=list(method=method, nn=nn))

在上述代码块中,我们可以看到以下内容:

  • 我们在 270 名用户和 973 本书上训练模型。

  • 整个数据集包含 275 名用户。

请参考本章附带的源代码。一旦 UBCF 算法训练完成,生成的模型将保存在 ML 计算实例的指定位置,然后推送到 S3 存储桶中的指定位置(模型输出路径)。

  1. 第三步是将训练好的模型作为端点(RESTful API)进行托管。SageMaker 在配置端点之前需要先创建模型。训练过程中的模型工件和 Docker 镜像是定义 SageMaker 模型所必需的。请注意,用于训练的 Docker 镜像也用于推理。SageMaker 端点需要输入 ML 计算实例的基础设施规格以及 SageMaker 模型。同样,为自定义容器创建 SageMaker 端点的过程与内置算法的过程相同。

让我们理解在推理过程中触发的 R 函数。

当 SageMaker 在推理时发送 serve 命令时,以下 R 函数将被执行,如下所示:

# Define scoring function
serve <- function() {
 app <- plumb(paste(prefix, 'plumber.R', sep='/'))
 app$run(host='0.0.0.0', port=8080)}

在上述代码中,我们可以看到以下内容:

  • 我们使用了 plumber R 包,将 R 函数转化为 REST 端点。

  • 需要转化为 REST API 的 R 函数会加上适当的注释。

  • 我们使用了plumb()方法将plumber.R代码作为一个端点托管。

对于每个发送到端点的 HTTP 请求,将调用相应的函数,如下所示:

load(paste(model_path, 'rec_model.RData', sep='/'), verbose = TRUE)
pred_bkratings <- predict(rec_model, ratings_mat[ind], n=5)

在上述代码中,我们可以看到以下内容:

    • 在推理时,我们通过调用 load() 方法并传递模型工件的路径来加载训练好的模型。

    • 然后,我们通过指定训练好的模型名称、新的用户向量或书籍偏好以及推荐书籍数量来调用predict()方法。

    • 请注意,评分矩阵 ratings_mat 包含所有 275 名用户及其对书籍的评分(若存在)。在本例中,我们关注的是用户 #272。请记住,在本节的数据集中,我们共有 275 名用户和 973 本书。

  1. 第四步是运行模型推理,如下所示:
payload = ratings.to_csv(index=False) 

response = runtime.invoke_endpoint(EndpointName='BYOC-r-endpoint-<timestamp>', ContentType='text/csv', Body=payload)

result = json.loads(response['Body'].read().decode())

在上述代码中,我们可以看到以下内容:

  • 我们将包含 275 名用户的完整数据集捕获在名为 payload 的 CSV 文件中。

  • 然后,我们将 payload 文件作为输入传递给 SageMaker 运行时的 invoke_endpoint() 方法,同时传递端点名称和内容类型。

端点将返回结果,如下所示:

通过这样做,我们看到了将自己的容器带到 SageMaker 来训练和托管模型的无缝体验,同时可以重用用其他语言编写的训练和评分(推理)逻辑。

摘要

在本章中,你已经学会了如何处理大数据,创建适合分析的数据集。你还看到 SageMaker 如何自动化机器学习生命周期的大部分步骤,使你能够无缝地构建、训练和部署模型。此外,我们还展示了一些生产力功能,如超参数优化和实验服务,这些功能使数据科学家能够进行多次实验并部署最终的优胜模型。最后,我们还讨论了如何将自己的模型和容器带入 SageMaker 生态系统。通过将基于开源机器学习库的模型带入,我们可以轻松地基于开源框架构建解决方案,同时仍能利用平台的所有功能。类似地,通过引入自己的容器,我们可以轻松地将用其他编程语言(除了 Python)编写的解决方案移植到 SageMaker。

学习上述所有关于 Amazon SageMaker 的内容使数据科学家和机器学习工程师能够减少将机器学习解决方案推向市场的时间。

在下一章,我们将介绍如何创建训练和推理流水线,以便能够训练和部署模型,以高效地运行推理(通过创建可重用的组件)。

进一步阅读

有关在 SageMaker 中工作时的扩展示例和详细信息,请参阅以下 AWS 博客:

第八章:创建机器学习推理管道

用于处理数据以进行模型训练的数据转换逻辑与用于准备数据进行推理的数据转换逻辑是相同的。重复相同的逻辑是冗余的。

本章的目标是带领您了解如何使用 SageMaker 和其他 AWS 服务创建 机器学习ML)管道,这些管道能够处理大数据、训练算法、部署训练好的模型并进行推理,同时在模型训练和推理过程中使用相同的数据处理逻辑。

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

  • 理解 SageMaker 中推理管道的架构

  • 使用 Amazon Glue 和 SparkML 创建特征

  • 通过在 SageMaker 中训练 NTM 来识别主题

  • 在 SageMaker 中进行在线推理,而不是批量推理

让我们来看看本章的技术要求。

技术要求

为了说明本章将要涵盖的概念,我们将使用 ABC Millions Headlines 数据集。该数据集包含大约一百万条新闻标题。在与本章相关的 github 仓库中,您应该能找到以下文件:

让我们从查看推理管道的架构开始。

理解 SageMaker 中推理管道的架构

我们正在构建的推理管道有三个主要组件:

  • 数据预处理

  • 模型训练

  • 数据预处理(来自 步骤 1)和推理

以下是架构图——我们将要演示的步骤适用于大数据:

在管道的第一步,我们通过 AWS Glue 在 Apache Spark 上执行数据处理逻辑。Glue 服务通过 SageMaker Notebook 实例进行调用。

Amazon Glue 是一个完全托管的无服务器 提取、转换和加载ETL)服务,用于处理大数据。ETL 作业运行在 Apache Spark 环境中,Glue 负责配置并按需扩展执行作业所需的资源。

数据处理逻辑,在我们的案例中,包括从每个新闻头条中创建标记/单词,去除停用词,并计算给定头条中每个单词的频率。ETL 逻辑被序列化为 MLeap 包,可以在推理时用于数据处理。序列化后的 SparkML 模型和处理过的输入数据都存储在 S3 桶中。

MLeap 是一个开源 Spark 包,旨在序列化 Spark 训练的转换器。序列化模型用于将数据转换为所需格式。

在第二步中,神经主题模型NTM)算法将在处理过的数据上进行训练,以发现主题。

第 3 步中,SparkML 和训练后的 NTM 模型一起用于创建管道模型,该模型用于按指定顺序执行模型。SparkML 提供一个 docker 容器,而 NTM docker 容器则作为实时模型预测的端点。相同的管道模型也可以用于批处理模式下运行推理,即一次处理多个新闻头条,为每个头条发现主题。

现在是时候全面深入了解第 1 步——如何从 SageMaker 笔记本实例调用 Amazon Glue 进行大数据处理。

使用 Amazon Glue 和 SparkML 创建特征

为了在大数据环境中创建特征,我们将使用 PySpark 编写数据预处理逻辑。该逻辑将作为 Python 文件abcheadlines_processing.py的一部分。在回顾逻辑之前,我们需要先走一遍一些前置条件。

走一遍前置条件

  1. 为 Amazon Glue 服务提供 SageMaker 执行角色访问权限,具体如下:

通过运行 SageMaker 会话对象的 get_execution_role()方法来获取 SageMaker 执行角色

  1. 在 IAM 仪表板上,点击左侧导航栏的“角色”并搜索该角色。点击目标角色,进入其摘要页面。点击信任关系标签,添加AWS Glue作为附加信任实体。点击“编辑信任关系”并将以下条目添加到"Service"键下:“glue.amazonaws.com”。

  2. 将 MLeap 二进制文件上传到 S3 桶的适当位置,具体如下。这些二进制文件可以在本章的源代码中找到:

python_dep_location = sess.upload_data(path='python.zip', bucket=default_bucket, key_prefix='sagemaker/inference-pipeline/dependencies/python')   

jar_dep_location = sess.upload_data(path='mleap_spark_assembly.jar', bucket=default_bucket, key_prefix='sagemaker/inference-pipeline/dependencies/jar') 
  1. 我们将使用 SageMaker 会话对象的upload_data()方法将 MLeap 二进制文件上传到 S3 桶的适当位置。我们将需要MLeap Java 包和 Python 包装器 MLeap 来序列化 SparkML 模型。同样,我们将把输入数据,即abcnews-date-text.zip,上传到 S3 桶中的相关位置。

现在,我们将回顾abcheadlines_processing.py中的数据预处理逻辑。

使用 PySpark 进行数据预处理

下面的数据预处理逻辑在一个 Spark 集群上执行。让我们逐步进行:

  1. 我们将首先收集由 SageMaker Notebook 实例发送的参数,如下所示:
args = getResolvedOptions(sys.argv, ['S3_INPUT_BUCKET',
 'S3_INPUT_KEY_PREFIX',
 'S3_INPUT_FILENAME',
 'S3_OUTPUT_BUCKET',
 'S3_OUTPUT_KEY_PREFIX',
 'S3_MODEL_BUCKET',
 'S3_MODEL_KEY_PREFIX'])

我们将使用 AWS Glue 库中的 getResolvedOptions() 实用函数来读取由 SageMaker Notebook 实例发送的所有参数。

  1. 接下来,我们将读取新闻标题,如下所示:
abcnewsdf = spark.read.option("header","true").csv(('s3://' + os.path.join(args['S3_INPUT_BUCKET'], args['S3_INPUT_KEY_PREFIX'], args['S3_INPUT_FILENAME'])))

我们使用 spark,即活动的 SparkSession,来读取包含相关新闻标题的 .csv 文件。

  1. 接下来,我们检索 10% 的标题,并定义数据转换。我们可以使用 Apache Spark 进行分布式计算处理所有 1,000,000 条新闻标题。然而,我们将通过使用数据集的样本从 SageMaker Notebook 实例中演示使用 AWS Glue 的背后概念:
abcnewsdf = abcnewsdf.limit(hdl_fil_cnt) 

tok = Tokenizer(inputCol="headline_text", outputCol="words") 
swr = StopWordsRemover(inputCol="words", outputCol="filtered")
ctv = CountVectorizer(inputCol="filtered", outputCol="tf", vocabSize=200, minDF=2)
idf = IDF(inputCol="tf", outputCol="features")

hdl_fil_cnt 是总新闻标题数的 10%。abcnewsdf 包含约 100,000 条新闻标题。我们使用 pyspark.ml.feature 中的 TokenizerStopWordsRemoverCountVectorizer,以及 逆文档频率 (IDF) 转换器和估计器对象来转换标题文本,如下所示:

  1. 首先,Tokenizer 将标题文本转换为单词列表。

  2. 第二,StopWordsTokenizerTokenizer 生成的单词列表中移除停用词。

  3. 第三,CountVectorizer 使用前一步骤的输出来计算词频。

  4. 最后,IDF,一个估计器,计算每个单词的逆文档频率因子(IDF 由 给出,其中 是标题 j 中术语 i 的词频,N 是总标题数,以及

    是包含术语 i 的标题数)。标题中独特的单词比那些在其他标题中频繁出现的单词更重要。

关于在 Spark ML 中的 EstimatorTransformer 对象的更多信息,请参阅 Spark 的文档 spark.apache.org/docs/latest/ml-pipeline.html

  1. 接下来,我们将所有转换器和估计器阶段连接成一个流水线,并将标题转换为特征向量。特征向量的宽度为 200,由 CountVectorizer 定义:
news_pl = Pipeline(stages=[tok, swr, ctv, idf])
news_pl_fit = news_pl.fit(abcnewsdf)
news_ftrs_df = news_pl_fit.transform(abcnewsdf)

在前面的代码中,我们使用 pyspark.ml 中的 Pipeline 对象将数据转换过程连接起来。我们还在 Pipeline 对象 news_pl 上调用了 fit() 方法,以创建 PipelineModelnews_pl_fit 将学习每个新闻标题中单词的 IDF 因子。当在 news_pl_fit 上调用 transform() 方法时,输入的标题将被转换为特征向量。每个标题将由一个长度为 200 的向量表示。CountVectorizer 按照所有标题中单词的频率排序,选择频率最高的前 200 个单词。请注意,处理后的标题将存储在 features 列中,这是 IDF 估算器阶段 outputCol 参数所指示的。

  1. 现在我们将保存结果特征向量为 .csv 格式,如下所示:
news_save = news_formatted.select("result")
news_save.write.option("delimiter", "\t").mode("append").csv('s3://' + os.path.join(args['S3_OUTPUT_BUCKET'], args['S3_OUTPUT_KEY_PREFIX']))

为了将处理后的标题保存为 .csv 格式,features 列需要以简单的字符串格式存储。CSV 文件格式不支持在列中存储数组或列表。我们将定义一个用户自定义函数 get_str,将特征向量转换为逗号分隔的 tf-idf 数字字符串。请参阅本章附带的源代码以获取更多细节。最终的 news_save DataFrame 将作为 .csv 文件保存到 S3 存储桶的指定位置。以下截图显示了 .csv 文件的格式:

  1. 同样,我们还将把词汇表保存到一个单独的文本文件中。

  2. 现在是时候序列化 news_pl_fit 并将其推送到 S3 存储桶,如下所示:

 SimpleSparkSerializer().serializeToBundle(news_pl_fit, "jar:file:/tmp/model.zip", news_ftrs_df)
s3.Bucket(args['S3_MODEL_BUCKET']).upload_file('/tmp/model.tar.gz', file_name)

在前面的代码块中,我们使用 MLeap pyspark 库中的 SimpleSparkSerializer 对象的 serializetoBundle() 方法来序列化 news_pl_fit。我们将在上传到 S3 存储桶之前,将序列化后的模型格式从 .zip 转换为 tar.gz 格式。

现在让我们通过 AWS Glue 作业来执行 abcheadlines_processing.py 脚本。

创建 AWS Glue 作业

现在我们将使用 Boto3 创建一个 Glue 作业,Boto3 是 AWS 的 Python SDK。该 SDK 允许 Python 开发人员创建、配置和管理 AWS 服务。

让我们创建一个 Glue 作业,并提供以下规格:

response = glue_client.create_job(
    Name=job_name,
    Description='PySpark job to featurize the ABC News Headlines dataset',
    Role=role,
    ExecutionProperty={
        'MaxConcurrentRuns': 1
    }, 

在前面的代码块中,我们通过传入作业名称、描述和角色,调用 AWS Glue 客户端的 create_job() 方法。我们还指定了要执行的并发数。

现在让我们看一下 Glue 发送到 Spark 集群的命令:

Command={
'Name': 'glueetl',
'ScriptLocation': script_location
},

在前面的代码中,我们定义了命令名称和包含数据预处理逻辑的 Python 脚本的位置,即 abcheadlines_processing.py

现在我们来看看需要配置哪些二进制文件,以便序列化 SparkML 模型:

DefaultArguments={
'--job-language': 'python',
'--extra-jars' : jar_dep_location,
'--extra-py-files': python_dep_location
},)

在前面的代码中,我们定义了默认语言,以便我们可以预处理大数据,指定了 MLeap .jar 文件的存放位置以及 MLeap 的 Python 包装器。

既然我们已经创建了 Glue 作业,让我们来执行它:

job_run_id = glue_client.start_job_run(JobName=job_name,
                                       Arguments = {
                                        '--S3_INPUT_BUCKET': s3_input_bucket,
                                        '--S3_INPUT_KEY_PREFIX': s3_input_key_prefix,
                                        '--S3_INPUT_FILENAME': s3_input_fn, 
                                        '--S3_OUTPUT_BUCKET': s3_output_bucket,
                                        '--S3_OUTPUT_KEY_PREFIX': s3_output_key_prefix,
                                        '--S3_MODEL_BUCKET': s3_model_bucket,
                                        '--S3_MODEL_KEY_PREFIX': s3_model_key_prefix
                                       })

我们通过传递之前创建的 Glue 作业的名称,以及定义输入和位置的参数,调用 AWS Glue 客户端的start_job_run()方法。

我们可以通过以下方式获取 Glue 作业的状态:

job_run_status = glue_client.get_job_run(JobName=job_name,RunId=job_run_id)['JobRun']['JobRunState']

我们将收到以下输出:

我们调用 AWS Glue 客户端的get_job_run()方法,并传入我们想检查其状态的 Glue 作业的名称。

要检查 AWS Glue 作业的状态,您还可以通过“服务”菜单导航到 AWS Glue 服务。在左侧导航菜单中的 ETL 部分,点击作业。选择一个作业名称以查看该 Glue 作业的详细信息:

现在,我们将通过拟合 NTM 到 ABC 新闻头条数据集来揭示其中的主题。

通过在 SageMaker 中训练 NTM 来识别主题

执行以下步骤来训练 NTM 模型:

  1. 从指定的 S3 桶的输出文件夹中读取处理后的 ABC 新闻头条数据集,如下所示:
abcnews_df = pd.read_csv(os.path.join('s3://', s3_output_bucket, f.key))

我们使用来自 pandas 库的read_csv()函数将处理后的新闻头条读取到 DataFrame 中。DataFrame 包含 110,365 条头条和 200 个单词。

  1. 然后,我们将数据集分成三部分——训练集、验证集和测试集,如下所示:
vol_train = int(0.8 * abcnews_csr.shape[0])

train_data = abcnews_csr[:vol_train, :] 
test_data = abcnews_csr[vol_train:, :]

vol_test = test_data.shape[0]
val_data = test_data[:vol_test//2, :]
test_data = test_data[vol_test//2:, :]

在前面的代码块中,我们将 80%的数据用于训练,10%用于验证,剩余 10%用于测试。

  1. 将训练集、验证集和测试集数据集上传到 S3 桶上的适当位置。我们还需要将 AWS Glue 作业创建的词汇文本文件上传到辅助路径。SageMaker 的内置算法使用辅助路径提供训练时的额外信息。在本例中,我们的词汇包含 200 个单词。然而,前一部分的特征向量不知道单词的名称;它确实知道单词的索引。因此,在 NTM 训练完成后,为了使 SageMaker 能够输出与主题对应的重要单词,它需要一个词汇文本文件。

  2. 下一步是通过将计算实例的数量和类型以及 Docker NTM 镜像传递给 SageMaker 会话,定义 SageMaker 中的 NTM Estimator 对象。Estimators 是适合数据的学习模型。

  3. 现在我们准备好训练 NTM 算法,如下所示:

ntm_estmtr_abc.fit({'train': s3_train, 'validation': s3_val, 'auxiliary': s3_aux})

为了训练 NTM 算法,我们使用ntm Estimator 对象的fit()方法,并传入训练集、测试集和辅助数据集的位置。由于我们有一整章内容,第九章,发现文本集合中的主题,专门讲解如何理解 NTM 算法的工作原理,因此我们将保存模型训练的详细信息,稍后再处理。

  1. 以下是模型的输出——我们已经配置了模型,使其能够检索五个主题:
International Politics and Conflict
[0.40, 0.94] defends decision denies war anti pm warns un bush report iraq calls public australia minister backs wins tas plans chief

Sports and Crime
[0.52, 0.77] clash top win world tour test pakistan back record cup killed title final talks england set australia us still pm

Natural Disasters and Funding
[0.45, 0.90] urged indigenous water power take call lead boost final residents get wa act funds england centre fire help plan funding

Protest and Law Enforcement
[0.51, 0.72] new record says found strike set win cup south police fire us go pay court plan rise australia bid deal

Crime
[0.54, 0.93] charged dies murder man charges crash death dead car two woman accident face charge found attack police injured court sydney

每个主题的开头有两个数字——kld 和 recons。我们将在下一章详细讨论这两个损失。但是目前,理解第一个比例反映了创建嵌入式新闻标题的损失,而第二个比例反映了重建损失(即从嵌入中创建标题)。损失越小,主题聚类的效果越好。

对于我们发现的每个主题,我们根据词语分组手动标注这些主题。

现在我们准备好查看推理模式。推理可以在实时模式和批量模式下获得。

在 SageMaker 中运行在线推理与批量推理

在现实世界的生产场景中,我们通常会遇到两种情况:

  • 在实时或在线模式下运行推理

  • 批量或离线模式下运行推理

为了说明这一点,假设使用推荐系统作为 Web 或移动应用的一部分,当你想根据应用内活动来个性化商品推荐时,可以使用实时推理。应用内活动,如浏览过的商品、购物车中但未结账的商品等,可以作为输入发送到在线推荐系统。

另一方面,如果你希望在客户与 Web 或移动应用进行交互之前就向他们展示商品推荐,那么你可以将与其历史消费行为相关的数据发送到离线推荐系统,从而一次性为整个客户群体获得商品推荐。

让我们看看如何运行实时预测。

通过推理管道创建实时预测

在本节中,我们将构建一个管道,重用序列化的 SparkML 模型进行数据预处理,并使用训练好的 NTM 模型从预处理的标题中提取主题。SageMaker 的 Python SDK 提供了ModelSparkMLModelPipelineModel等类,用于创建推理管道,进行特征处理,并使用训练好的算法对处理后的数据进行评分。

让我们逐步讲解如何创建一个可以用于实时预测的端点:

  1. 从 NTM 训练作业(我们在上一节中创建的那个)创建Model,如下所示:
ntm_model = Model(model_data=modeldataurl, image=container)

在这里,我们创建了Model对象,该对象位于sagemaker.model模块中。我们传入训练好的 NTM 模型的位置以及 NTM 推理镜像的 Docker 注册路径。

  1. 创建一个表示学习到的数据预处理逻辑的 SparkML Model,如下所示:
sparkml_data = 's3://{}/{}/{}'.format(s3_model_bucket, s3_model_key_prefix, 'model.tar.gz')
sparkml_model = SparkMLModel(model_data=sparkml_data, env={'SAGEMAKER_SPARKML_SCHEMA' : schema_json})

我们将sparkml_data定义为来自pyspark.ml包的序列化PipelineModel的位置。请记住,PipelineModel包含三个变换器(TokenizerStopWordsRemoverCountVectorizer)和一个估算器(IDF),这些都是我们在上一节中进行的数据预处理中用到的。然后,我们通过传入训练好的 Spark PipelineModel的位置和推理输入数据的模式,创建一个SparkMLModel对象sparkml_model

  1. 创建一个PipelineModel,将SparkMLModel(数据预处理)和ntm_model按顺序组合如下:
sm_model = PipelineModel(name=model_name, role=role, models=[sparkml_model, ntm_model])

我们通过传递模型名称、sagemaker执行角色和我们要执行的模型序列,来从sagemaker.pipeline模块创建一个PipelineModel对象。

  1. 现在是时候部署PipelineModel了,如下所示:
sm_model.deploy(initial_instance_count=1, instance_type='ml.c4.xlarge', endpoint_name=endpoint_name)

我们将调用sm_model上的deploy()方法,将模型部署为一个端点。我们需要将托管端点所需的实例数量和类型,以及端点名称,传递给部署的模型。

现在是时候将测试数据集中的一个样本头条新闻传递给新创建的端点了。让我们一步一步地了解这些步骤:

  1. 首先,我们从sagemaker.predictor模块创建一个RealTimePredictor对象,如下所示:
predictor = RealTimePredictor(endpoint=endpoint_name, sagemaker_session=sess, serializer=json_serializer,
 content_type=CONTENT_TYPE_JSON, accept=CONTENT_TYPE_CSV)

我们通过传递先前创建的端点名称、当前的 SageMaker 会话、序列化器(定义如何在将数据传输到端点时对输入数据进行编码)以及请求和响应的内容类型,来定义RealTimePredictor对象。

  1. 然后我们调用RealTimePredictor对象predictorpredict()方法,如下所示:
predictor.predict(payload)

我们调用predictorpredict()方法,该方法已初始化为RealTimePredictor对象,通过将测试数据集中的一个样本头条新闻作为json负载的一部分传递,如下所示:

payload = {
    "schema": {
        "input": [
        {
            "name": "headline_text",
            "type": "string"
        }, 
    ],
    "output": 
        {
            "name": "features",
            "type": "double",
            "struct": "vector"
        }
    },
    "data": [
        ["lisa scaffidi public hearing possible over expenses scandal"]
            ]

}

payload变量包含两个键:schemadataschema键包含SparkMLModel的输入和输出结构,而data键包含我们希望发现其主题的一个样本头条新闻。如果我们选择覆盖在初始化SparkMLModel时指定的 SageMaker sparkml架构,我们可以传递新的架构。以下是评分新闻头条后的输出:

{"predictions":[{"topic_weights":[0.5172129869,0.0405323133,0.2246916145,0.1741439849,0.0434190407]}]}

我们可以看到,头条新闻有三个显著的主题:国际政治与冲突,其次是与资金/支出相关的挑战,最后是执法。

一点背景——Lisa Scaffidi 曾是西澳大利亚珀斯市市长。她因不当使用职权而被控—未能申报价值数万美元的礼品和旅行。因此,这条头条新闻恰如其分地涵盖了多个主题:国际政治与冲突(51%),随后是与资金/支出相关的挑战(22%),然后是执法(17%)。

现在让我们来看一下如何为一批头条新闻推断主题。

通过推理管道创建批量预测

在本节中,我们将焦点从实时预测转向批量预测。为了满足将训练好的模型部署到离线模式的需求,SageMaker 提供了 Batch Transform。Batch Transform 是一个新发布的高性能和高吞吐量特性,能够对整个数据集进行推理。输入和输出数据都存储在 S3 桶中。Batch Transform服务负责管理必要的计算资源,以便使用训练好的模型对输入数据进行评分。

以下图示显示了 批量转换 服务是如何工作的:

在前面的图示中,我们可以看到以下步骤:

  1. 批量转换服务通过代理摄取大量输入数据(来自 S3 存储桶)。

  2. 批量转换代理的角色是协调训练模型与 S3 存储桶之间的通信,在那里存储输入和输出数据。

  3. 一旦请求数据对代理可用,它就会将其发送到训练好的模型,模型会转换新闻标题并生成主题。

  4. 由中介代理产生的推理或主题将被返回存储到指定的 S3 存储桶中。

让我们逐步了解如何运行一个批量转换任务:

  1. 定义存储输入和输出数据的 S3 存储桶路径,以及我们在上一节中创建的 PipelineModel 的名称。PipelineModel 的名称可以通过编程方式或通过 AWS 控制台获取(导航到左侧导航面板中的 SageMaker 服务;然后,在推理部分,点击模型)。

  2. sagemaker.transformer 模块创建一个 Transformer 对象,如下所示:

transformer = sagemaker.transformer.Transformer(
 model_name = model_name,
 instance_count = 1,
 instance_type = 'ml.m4.xlarge',
 strategy = 'SingleRecord',
 assemble_with = 'Line',
 output_path = output_data_path,
 base_transform_job_name='serial-inference-batch',
 sagemaker_session=sess,
 accept = CONTENT_TYPE_CSV
)

在这里,我们定义运行管道模型所需的计算资源,例如 EC2 实例类型和数量。然后,我们定义并组装策略,即如何批量处理记录(单条或多条记录)以及如何组装输出。还提供了当前 SageMaker 会话和由 accept 定义的输出内容类型。

  1. 调用我们在前一步创建的转换器对象的 transform() 方法,如下所示:
transformer.transform(data = input_data_path,
                      job_name = job_name,
                      content_type = CONTENT_TYPE_CSV, 
                      split_type = 'Line')

我们定义输入数据的路径、批量转换任务的名称、输入内容类型,以及如何分隔输入记录(在这种情况下,新闻标题按行分隔)。接下来,我们等待对所有输入数据进行批量推理。以下是产生的输出的摘录:

记住,我们已经涉及了五个主题:国际政治与冲突、体育与犯罪、自然灾害与资金、抗议与执法、以及犯罪。对于每个新闻标题,NTM 算法会预测该标题包含主题 1 至 5 的概率。因此,每个标题将由五个主题的混合组成。

例如,在 印尼警方称枪击事件导致阿扎哈里死亡 的标题中,犯罪相关的主题占主导地位。这些主题非常相关,因为该标题涉及的是 2002 年巴厘岛爆炸事件的幕后策划者阿扎哈里的谋杀。

通过完成本节,我们已经成功查看了两种在 SageMaker 中运行推理的不同模式。

总结

在本章中,我们学习了如何重用数据预处理逻辑进行训练和推理,以及如何执行在线推理与离线推理的区别。我们首先了解了机器学习推理流水线的架构。然后,我们使用 ABC 新闻头条数据集,通过 AWS Glue 和 SparkML 演示大数据处理。接着,我们通过将 NTM 算法应用于处理后的新闻头条,发现了新闻的主题。最后,我们通过利用相同的数据预处理逻辑进行推理,讲解了实时推理与批处理推理的区别。通过推理流水线,数据科学家和机器学习工程师可以提高机器学习解决方案的上市速度。

在下一章,我们将深入探讨神经主题模型NTMs)。

深入阅读

以下阅读材料旨在增强你对本章所涵盖内容的理解:

第九章:在文本集合中发现主题

理解文本的一个最有用的方法是通过主题。学习、识别和提取这些主题的过程叫做主题建模。理解文本中的广泛主题有许多应用。它可以在法律行业中用于从合同中提取主题。(与其人工审核大量合同中的某些条款,不如通过无监督学习自动提取主题或模式)。此外,它还可以在零售行业中用于识别社交媒体对话中的广泛趋势。这些广泛的趋势可以用于产品创新——如在线上和实体店推出新商品、告知其他人商品种类等。

在本章中,我们将学习如何从长文本中合成主题(即文本长度超过 140 个字符)。我们将回顾主题建模的技术,并理解神经主题模型NTM)的工作原理。接着,我们将探讨在 SageMaker 中训练和部署 NTM 的方法。

在本章中,我们将讨论以下主题:

  • 回顾主题建模技术

  • 了解神经主题模型的工作原理

  • 在 SageMaker 中训练 NTM

  • 部署 NTM 并运行推理

技术要求

为了说明本章的概念,我们将使用词袋模型archive.ics.uci.edu/ml/datasets/bag+of+words)数据集,该数据集来自UCI 机器学习库archive.ics.uci.edu/ml)。该数据集包含有关 Enron 电子邮件的信息,如电子邮件 ID、单词 ID 及其出现次数,即某个特定单词在给定电子邮件中出现的次数。

在与本章相关的 GitHub 仓库中(github.com/PacktPublishing/Hands-On-Artificial-Intelligence-on-Amazon-Web-Services/tree/master/Ch9_NTM),你应该能够找到以下文件:

我们先从回顾主题建模技术开始。

回顾主题建模技术

在这一部分,我们将讨论几种线性和非线性学习技术,涉及主题建模。线性技术包括潜在语义分析(两种方法——奇异向量分解和非负矩阵分解)、概率潜在语义分析和潜在狄利克雷分配。另一方面,非线性技术包括 LDA2Vec 和神经变分文档模型。

潜在语义分析LSA)的情况下,主题是通过将文档近似为较少数量的主题向量来发现的。一组文档通过文档-词矩阵表示:

  • 在其最简单的形式中,文档词矩阵由原始计数组成,即给定词在给定文档中出现的频率。由于这种方法没有考虑每个词在文档中的重要性,我们用tf-idf词频-逆文档频率)分数替代原始计数。

  • 通过 tf-idf,出现在特定文档中频繁出现但在其他文档中出现较少的词将具有较高的权重。由于文档-词矩阵是稀疏且嘈杂的,必须通过降维来获取文档和词之间通过主题形成的有意义关系。

  • 降维可以通过截断SVD奇异值分解)完成,其中文档-词矩阵被分解为三个不同的矩阵,即文档主题(U)、词-主题(V)和奇异值矩阵(S),其中奇异值表示主题的强度,如下图所示:

这种分解是唯一的。为了在低维空间中表示文档和词,只选择T个最大的奇异值(如前图所示的矩阵子集),并且只保留UV的前T列。T是一个超参数,可以调整以反映我们想要找到的主题数量。在线性代数中,任何m x n矩阵A都可以按如下方式分解:

  • ,其中U称为左奇异向量,V称为右奇异向量,S称为奇异值矩阵

有关如何计算奇异值以及给定矩阵的左奇异向量和右奇异向量的信息,请参考machinelearningmastery.com/singular-value-decomposition-for-machine-learning/ 直观解释——从 SVD 重构矩阵

  • 因此,我们得到了,

除了 SVD 外,你还可以通过(非负矩阵分解 (NMF))进行矩阵分解。NMF 属于线性代数算法,用于识别数据中的潜在结构。两个非负矩阵用于近似文档-词项矩阵,如下图所示(术语和单词可以互换使用):

让我们比较并对比 LSA 的不同线性技术,并查看一种提供更多灵活性的 LSA 变体:

  • NMF 与 SVD 的区别在于,使用 SVD 时,我们可能会得到负的组件(左侧和/或右侧)矩阵,这在解释文本表示时并不自然。而 NMF 则生成非负的表示,用于执行 LSA。

  • LSA 的缺点通常是它有较少可解释的主题和效率较低的表示。此外,它是一个线性模型,不能用于建模非线性依赖关系。潜在主题的数量受矩阵秩的限制。

概率 LSA (pLSA):pLSA 的整体思想是找到一个潜在主题的概率模型,该模型能够生成我们可以观察到的文档和单词。因此,联合概率,也就是文档和单词组合的概率,![],可以写成如下:

=

这里,D = 文档,W = 单词,Z = 主题。

让我们看看 pLSA 是如何工作的,并举例说明它在某些情况下并不充分:

  • 我们可以看到,pLSA 与 LSA 的相似之处在于,P(Z)对应于一个奇异值矩阵,P(D|Z)对应于一个左奇异向量,而P(W|Z)则对应于 SVD 中的一个右奇异向量。

  • 这种方法的最大缺点是,我们无法轻易地将其推广到新文档。LDA 解决了这个问题。

潜在狄利克雷分配 (LDA):虽然 LSA 和 pLSA 用于语义分析或信息检索,但 LDA 用于主题挖掘。简单来说,你基于文档集合中的词频来揭示主题:

  • 对于一组文档,你指定你想要揭示的主题数量。这个数字可以根据 LDA 在未见文档上的表现进行调整。

  • 然后,你对文档进行标记化,移除停用词,保留在语料库中出现一定次数的单词,并进行词干化处理。

  • 首先,对于每个单词,你分配一个随机主题。然后你通过文档计算主题混合,即每个主题在文档中出现的次数。你还要计算主题中每个单词在语料库中的混合,即每个单词在该主题中出现的次数。

  • 迭代/遍历 1:对于每个单词,在遍历整个语料库后,你将重新分配一个主题。主题的重新分配是根据每个文档的其他主题分配来进行的。假设一个文档的主题分布为:主题 1 - 40%,主题 2 - 20%,主题 3 - 40%,并且文档中的第一个单词被分配到主题 2。该单词在这些主题中(即整个语料库)出现的频率如下:主题 1 - 52%,主题 2 - 42%,主题 3 - 6%。在文档中,我们将该单词从主题 2 重新分配到主题 1,因为该单词代表主题 1(40%52%)的概率高于主题 2(20%42%)。这个过程会在所有文档中重复进行。经过遍历 1 后,你将覆盖语料库中的每个单词。

  • 我们对整个语料库进行几轮遍历或迭代,直到不再需要重新分配。

  • 最终,我们会得到一些指定的主题,每个主题由关键词表示。

到目前为止,我们已查看了用于主题建模的线性技术。现在,是时候将注意力转向非线性学习了。神经网络模型用于主题建模,具有更大的灵活性,允许增加新的功能(例如,为输入/目标单词创建上下文单词)。

Lda2vec 是 word2vec 和 LDA 模型的超集。它是 skip-gram word2vec 模型的一种变体。Lda2vec 可以用于多种应用,如预测给定单词的上下文单词(称为枢轴或目标单词),包括学习用于主题建模的主题向量。

Lda2vec 类似于 神经变分文档模型NVDM),用于生成主题嵌入或向量。然而,NVDM 采用了更为简洁和灵活的主题建模方法,通过神经网络创建文档向量,而完全忽视了单词之间的关系。

NVDM神经变分文档模型)是一种灵活的生成文档建模过程,我们通过主题学习多个文档表示(因此,NVDM 中的 变分——意味着多个——一词):

  • NVDM 基于 变分自编码器VAE)框架,该框架使用一个神经网络对文档集合进行编码(即编码器),另一个神经网络对文档的压缩表示进行解码(即解码器)。这个过程的目标是寻找在语料库中近似信息的最佳方式。自编码器通过最小化两种类型的损失进行优化:

    • 解码损失重构误差):通过主题嵌入重构原始文档。

    • 编码损失Kullback-Leibler 或 KL 散度):构建输入文档或主题嵌入的随机表示。KL 散度衡量在编码文档的词袋表示时丢失的信息。

现在,让我们深入了解神经主题模型(NTM),这是 AWS 实现的 NVDM。尽管 AWS 提供了一个现成可用的 API——AWS Comprehend,用于发现主题,但 NTM 算法提供了细粒度的控制和灵活性,可以从长文本中挖掘主题。

了解神经主题模型的工作原理

如前所述,神经主题模型NTM)是一个生成型文档模型,可以生成文档的多种表示。它生成两个输出:

  • 文档的主题混合

  • 一组解释主题的关键词,涵盖整个语料库中的所有主题

NTM 基于 变分自编码器(Variational Autoencoder)架构。下图展示了 NTM 的工作原理:

让我们逐步解释这个图示:

  • NTM 由两个组件组成——编码器和解码器。在编码器中,我们有一个 多层感知器MLP)网络,它接收文档的词袋表示,并创建两个向量,一个是均值向量 ,另一个是标准差向量 。直观地讲,均值向量控制编码输入的中心位置,而标准差控制中心周围的区域。由于从该区域生成的样本每次都会有所不同,解码器将学习如何重构输入的不同潜在编码。

MLP 是一种前馈 人工神经网络ANNs)类别。它由至少三层节点组成:输入层、输出层和隐藏层。除了输入节点外,每个节点都是一个使用非线性激活函数的神经元。

  • 第二个组件是解码器,它通过独立生成单词来重构文档。网络的输出层是一个 Softmax 层,通过重构主题词矩阵来定义每个单词按主题的概率。矩阵中的每一列表示一个主题,每一行表示一个单词。矩阵中某一列的值表示该主题下单词分布的概率。

Softmax 解码器使用多项式逻辑回归,我们考虑不同主题的条件概率。这个变换实际上是一个标准化的指数函数,用于突出最大值并抑制远低于最大值的值。

NTM 通过减少重构误差和 KL 散度,以及调整网络的学习权重和偏差来进行优化。因此,NTM 是一个灵活的神经网络模型,适用于主题挖掘和生成可解释的主题。现在,是时候在 SageMaker 中查看如何训练 NTM 了。

在 SageMaker 中训练 NTM

在这一部分,我们将使用 NTM(主题模型)对 Enron 邮件进行训练,以产生话题。这些邮件是在 Enron(一家因财务损失于 2007 年停止运营的美国能源公司)与其他与其有业务往来的各方之间交换的。

数据集包含 39,861 封邮件和 28,101 个独特的单词。我们将使用这些邮件的一个子集——3,986 封邮件和 17,524 个独特单词。另外,我们将创建一个文本文件vocab.txt,以便 NTM 模型可以报告某个话题的单词分布。

在开始之前,确保docword.enron.txt.gzvocab.enron.txt文件已经上传到本地 SageMaker 计算实例中的一个名为data的文件夹。请按照以下步骤操作:

  1. 创建一个邮件的词袋表示,步骤如下:
pvt_emails = pd.pivot_table(df_emails, values='count', index='email_ID', columns=['word_ID'], fill_value=0)

在前面的代码中,我们使用了 pandas 库中的pivot_table()函数来对邮件进行透视,使得邮件 ID 成为索引,单词 ID 成为列。透视表中的值表示单词的词频。该透视表包含 3,986 个邮件 ID 和 17,524 个单词 ID。

  1. 现在,让我们将词频乘以逆文档频率IDF)因子。我们的假设是,出现在一封邮件中并且在其他邮件中出现频率较低的单词,对于发现话题很重要,而那些在所有邮件中频繁出现的单词可能对发现话题不重要。

IDF 的计算公式如下:

dict_IDF = {name: np.log(float(no_emails) / (1+len(bag_of_words[bag_of_words[name] > 0]))) for name in bag_of_words.columns}

IDF 公式如下所示:,其中 N 是数据集中邮件的数量,是包含单词i的文档数量。

在这一步结束时,将创建一个新的 DataFrame,表示透视后的邮件及其 tf-idf 值。

  1. 现在,我们将从邮件的词袋表示中创建一个压缩稀疏行矩阵,步骤如下:
sparse_emails = csr_matrix(pvt_emails, dtype=np.float32)

在前面的代码中,我们使用了scipy.sparse模块中的csr_matrix()函数来高效表示邮件矩阵。使用压缩稀疏行矩阵,您可以仅对非零值进行操作,并且计算时占用更少的内存。压缩稀疏行矩阵使用行指针指向行号,列索引标识该行中的列,以及给定行指针和列索引的值。

  1. 按照如下方式将数据集分成训练集、验证集和测试集:
vol_train = int(0.8 * sparse_emails.shape[0])

# split train and test
train_data = sparse_emails[:vol_train, :] 
test_data = sparse_emails[vol_train:, :] 

vol_test = test_data.shape[0]
val_data = test_data[:vol_test//2, :]
test_data = test_data[vol_test//2:, :]

我们将 80%的邮件用于训练,10%用于验证,剩余的 10%用于测试。

  1. 将邮件从压缩稀疏行矩阵转换为 RecordIO 封装的 Protobuf 格式,步骤如下:
data_bytes = io.BytesIO()
smamzc.write_spmatrix_to_sparse_tensor(array=sprse_matrix[begin:finish], file=data_bytes, labels=None)
data_bytes.seek(0)

Protobuf 格式,也称为协议缓冲区,是来自 Google 的一种协议,用于序列化或编码结构化数据。虽然 JSON、XML 和 Protobuf 可以互换使用,但 Protobuf 比其他格式更为增强,并支持更多的数据类型。RecordIO 是一种文件格式,用于在磁盘上存储序列化数据。其目的是将数据存储为一系列记录,以便更快地读取。在底层,RecordIO 使用 Protobuf 来序列化结构化数据。

对于分布式训练,我们将训练数据集分成若干部分用于分布式训练。更多细节请参考本章附带的源代码。sagemaker.amazon.common中的write_spmatrix_to_sparse_tensor()函数用于将每个部分从稀疏行矩阵格式转换为稀疏张量格式。该函数以稀疏行矩阵作为输入,同时指定一个二进制流,将 RecordIO 记录写入其中。然后,我们通过调用seek()方法将流的位置重置到流的开始位置——这对于从文件开头读取数据至关重要。

  1. 将训练集和验证集上传到 S3 存储桶,方法如下:
 file_name = os.path.join(prefix, fname_template.format(i))
 boto3.resource('s3').Bucket(bucket).Object(file_name).upload_fileobj(data_bytes)
  1. 我们将文件名提供给二进制流,并指定 S3 存储桶的名称,该存储桶用于存储训练数据集。我们调用 S3 对象的upload_fileobj()方法,将二进制数据上传到指定位置。

  2. 现在,我们初始化 SageMaker 的Estimator对象,为训练做准备,方法如下:

ntm_estmtr = sagemaker.estimator.Estimator(container,
 role,
 train_instance_count=2,
 train_instance_type='ml.c4.xlarge',
 output_path=output_path,
 sagemaker_session=sess)
  1. 估算器对象ntm_estmtr是通过传入 NTM 镜像的 Docker 注册表路径、SageMaker 执行角色、训练实例的数量和类型以及输出位置来创建的。由于我们启动的计算实例数量为两个,因此我们将进行分布式训练。在分布式训练中,数据被划分,并在多个数据块上并行进行训练。

  2. 现在,让我们定义 NTM 算法的超参数,方法如下:

num_topics = 3
vocab_size = 17524 # from shape from pivoted emails DataFrame
ntm_estmtr.set_hyperparameters(num_topics=num_topics, 
 feature_dim=vocab_size, 
 mini_batch_size=30, 
 epochs=150, 
 num_patience_epochs=5, 
 tolerance=.001)

让我们来看看在前面的代码中指定的超参数:

    • feature_dim:表示特征向量的大小。它设置为词汇表的大小,即 17,524 个单词。

    • num_topics:表示要提取的主题数量。我们在这里选择了三个主题,但可以根据模型在测试集上的表现进行调整。

    • mini_batch_size:表示在更新权重之前要处理的训练样本数量。我们在这里指定了 30 个训练样本。

    • epochs:表示进行的前向和反向传递的次数。

    • num_patience_epochs:表示在停止之前执行的最大无效 epoch 数量(即损失没有改善的 epoch)。

    • optimizer:这表示用于优化网络权重的算法。我们使用的是 Adadelta 优化算法。自适应 Delta 梯度是Adagrad自适应梯度)的增强版本,其中学习率根据梯度更新的滚动窗口与所有过去的梯度更新进行比较而减小。

    • tolerance:这表示损失函数变化的阈值——如果在最后指定的耐心周期数内损失变化低于此阈值,则训练会提前停止。

  1. 将包含词汇或数据集单词的文本文件上传到辅助路径/频道。这是用于在训练过程中向 SageMaker 算法提供附加信息的通道。

  2. 将 NTM 算法拟合到训练集和验证集,如下所示:

ntm_estmtr.fit({'train': s3_train, 'validation': s3_val, 'auxiliary': s3_aux})
  1. 对于训练,我们通过传递初始化的S3_input对象(来自sagemaker.session模块)来调用ntm_estmtr对象的fit()方法。s3_trains3_vals3_aux对象提供训练、验证和辅助数据集的位置,以及它们的文件格式和分布类型。

现在,让我们来看看分布式训练的结果:

  • 查看第一个机器学习计算实例的训练输出,如下所示:

请记住,总共有 3,188 个训练示例。由于我们为训练启动了两个计算实例,在第一个实例上,我们使用了 2,126 个示例进行训练。

  • 查看第二个训练实例的结果,如下所示:

在第二个计算实例上,我们使用剩余的 1,062 个示例进行训练。

  • 接下来,我们将报告模型在验证数据集上的表现。关于训练数据集的度量标准,请参阅本章附带的源代码。

现在,让我们看看训练模型的验证结果。该模型在 390 个数据点上进行评估,这些数据点属于验证数据集。具体来说,我们将关注以下度量标准:

  • 词嵌入主题一致性度量WETC):这衡量每个主题中顶部单词的语义相似性。一个高质量的模型将在低维空间中将顶部单词聚集在一起。为了在低维空间中定位单词,使用了来自 GloVe(全球词向量)的预训练词嵌入。

  • 主题唯一性TU):这衡量生成主题的唯一性。该度量与一个单词在所有主题中出现的次数成反比。例如,如果一个单词只出现在一个主题中,则该主题的唯一性很高(即 1)。然而,如果一个单词出现在五个主题中,则唯一性度量为 0.2(1 除以 5)。为了计算所有主题的主题唯一性,我们将所有主题的 TU 度量取平均值。

  • 困惑度(logppx)是衡量概率模型预测样本(验证数据集)效果的统计指标。训练后,计算训练模型在验证数据集上的困惑度(训练模型在验证数据集上的表现)。困惑度越低越好,因为这最大化了验证数据集的准确性。

  • 总损失(total)是 Kullback-Leibler 散度损失和重建损失的组合。

记住,神经主题模型通过最小化损失,在多个轮次中进行优化,方法如下:

  • Kullback-Leibler 散度损失kld):构建了一个电子邮件(主题嵌入)的随机表示,涉及相对熵,这是一种度量概率分布之间差异的方法,也就是代理概率分布。

  • 重建损失recons):从主题嵌入中重建原始电子邮件。

以下截图显示了验证结果,并列出了所有定义的损失类型:

  • 损失为8.47,其中 0.19 定义为kld损失,8.28定义为重建损失。

  • 从前面的截图中,我们可以看到,在三个主题中,词嵌入主题一致性WETC)为.26主题唯一性TU)为0.73困惑度logppx)为8.47(与损失相同)。

  • 通过矩形框突出显示了三个主题及其定义的单词。

现在,到了将训练好的 NTM 模型部署为终端节点的时候了。

部署训练好的 NTM 模型并运行推理

在本节中,我们将部署 NTM 模型,运行推理,并解释结果。让我们开始吧:

  1. 首先,我们将训练好的 NTM 模型部署为终端节点,如下所示:
ntm_predctr = ntm_estmtr.deploy(initial_instance_count=1, instance_type='ml.m4.xlarge')

在前面的代码中,我们调用 SageMaker Estimator 对象ntm_estmtrdeploy()方法来创建终端节点。我们传递了部署模型所需的实例数量和类型。NTM Docker 镜像用于创建终端节点。SageMaker 需要几分钟时间来部署模型。以下截图显示了已配置的终端节点:

你可以通过导航到 SageMaker 服务,进入左侧导航窗格,在推理部分下找到并点击终端节点,查看你创建的端点。

  1. 指定测试数据的请求和响应内容类型,如下所示:
ntm_predctr.content_type = 'text/csv'
ntm_predctr.serializer = csv_serializer
ntm_predctr.deserializer = json_deserializer

在前面的代码中,ntm_estmtrdeploy()方法返回一个RealTimePredictor对象(来自sagemaker.predictor模块)。我们将测试数据的输入内容类型和反序列化器(响应的内容类型)分配给我们创建的RealTimePredictor对象ntm_predctr

  1. 现在,我们准备测试数据集以进行推理,如下所示:
test_data = np.array(test_data.todense())

在前面的代码中,我们使用 numpy Python 库将测试数据格式从压缩稀疏行矩阵转换为稠密数组。

  1. 然后,我们调用ntm_predctrpredict()方法进行推理,如下所示:
results = ntm_predctr.predict(test_data[1:6])
topic_wts_res = np.array([prediction['topic_weights'] for prediction in results['predictions']])

在前面的代码中,我们传递了测试数据集中前五封邮件进行推理。然后,我们浏览预测结果,创建了一个多维的主题权重数组,其中行表示邮件,列表示主题。

  1. 现在,我们解释结果,如下所示:
df_tpcwts=pd.DataFrame(topic_wts_res.T)

在前面的代码中,我们转置了topic_wts_res,一个多维数组,创建了一个数据框df_tpcwts,使得每一行表示一个主题。然后我们绘制主题,如下所示:

df_tpcwts.plot(kind='bar', figsize=(16,4), fontsize=fnt_sz)
plt.ylabel('Topic % Across Emails', fontsize=fnt_sz)
plt.xlabel('Topic Number', fontsize=fnt_sz)

在 x 轴上,我们绘制了主题编号,在 y 轴上,我们绘制了每个主题所代表的邮件百分比,如下所示:

从前面的图表中可以明显看出,主题 0 在所有五封邮件中的占比不到 10%。然而,主题 1 和主题 2 在邮件中具有不同程度的主导地位——大约 70%的第 4 封邮件由主题 1 表示,而大约 60%的第 5 封邮件由主题 2 表示。

现在,让我们看一下每个主题的词云。了解每个主题的词汇混合非常重要,这样我们就能知道哪些词语在描述特定主题时占主导地位。让我们开始吧:

  1. 下载已训练的模型,如下所示:
boto3.resource('s3').Bucket(bucket).download_file(model_path, 'downloaded_model.tar.gz')

在前面的代码中,我们从由model_path变量指定的路径下载了一个已训练的 NTM 模型(该路径在创建估算器时指定,ntm_estmtr)。

  1. 现在,我们从已训练的模型中获取主题-词矩阵,如下所示:
model_dict = mx.ndarray.load('params')
# Retrieve word distribution for each of the latent topics
W = model_dict['arg:projection_weight'] 

在前面的代码中,我们提取了 NTM 模型downloaded_model.tar.gz,加载了学习到的参数params。请记住,模型输出层的大小与数据集中单词(词汇)的数量相同。然后,我们创建了一个多维的 mxnet 数组W,按主题加载单词权重。W 的形状为 17,524 x 3,其中 17,524 行表示单词,3 列表示主题。

  1. 对每个主题,运行 softmax 函数计算单词权重,如下所示:
pvals = mx.nd.softmax(mx.nd.array(W[:, ind])).asnumpy()

在前面的代码中,我们对每个主题的单词权重运行 softmax 函数,将其值映射到 0 和 1 之间。每个主题中单词的概率和应当等于 1。请记住,softmax 层是 NTM 网络的输出层,它强调最大的值,并抑制离最大值较远的值。

  1. 按主题绘制词云,如下所示:

如我们所见,Topic0主要由单词resourcepending定义,而Topic1主要由单词instructionandornotification定义。

基于每个主题中排名靠前的单词,我们可以确定邮件中讨论的主题:

  • Topic0访问 Enron IT 应用):资源待请求创建接受管理员本地类型永久 nahoutrdhoustonpwrcommonelectric nahoutrdhoustonpwrcommonpower2region 审批 kobra 点击应用日期目录读取风险尾部。

  • Topic1能源交易):Andor 指令通知储备购买责任来源禁止订单证券基于情报降级强大公司网站征询隐私点击覆盖。

  • Topic2包括对 IT 应用和能源交易的访问):请求资源待创建审批类型应用日期尾目录接受管理员翻转永久对手方头部点击交换 kobra 风险。

在本节中,我们学习了如何解读主题建模的结果。现在,让我们总结本章中我们所学到的所有概念。

总结

在本章中,我们回顾了主题建模技术,包括线性和非线性学习方法。我们通过讨论其架构和内部机制,解释了 SageMaker 中的 NTM 如何工作。我们还探讨了 NTM 模型的分布式训练,其中数据集被分成多个块进行并行训练。最后,我们将训练好的 NTM 模型部署为端点并运行推理,解读了 Enron 邮件中的主题。对于任何数据科学家来说,从大量非结构化数据中提取信息和主题至关重要。SageMaker 中的 NTM 提供了一种灵活的方法来实现这一点。

在下一章中,我们将讲解如何使用 SageMaker 进行图像分类。

进一步阅读

关于主题建模技术——LDA 的参考文献,请访问blog.echen.me/2011/08/22/introduction-to-latent-dirichlet-allocation/

如果你想直观地理解 VAE,可以查看以下链接:

关于神经变分推断在文本处理中的参考文献,请访问arxiv.org/pdf/1511.06038.pdf

第十章:使用 Amazon SageMaker 进行图像分类

图像分类是过去五年中最重要的研究领域之一。这并不令人惊讶,因为成功地分类图像解决了许多不同行业的业务问题。例如,整个自动驾驶汽车行业依赖于图像分类和物体检测模型的准确性。

在本章中,我们将探讨 Amazon SageMaker 如何大大简化图像分类问题。除了收集丰富的图像集进行训练外,我们还将学习如何指定超参数(算法内部的参数)、训练 Docker 镜像,并使用基础设施规范进行训练。

在本章中,我们将讨论以下内容:

  • 了解卷积神经网络和残差网络

  • 通过迁移学习进行图像分类

  • 通过批量变换对图像进行推断

技术要求

请使用以下链接查看本章的源代码:github.com/PacktPublishing/Hands-On-Artificial-Intelligence-on-Amazon-Web-Services

了解卷积神经网络和残差网络

SageMaker 图像分类算法是 残差网络ResNets)的实现。在深入了解算法的细节之前,让我们简要了解一下 卷积神经网络CNN)和 ResNet,以及它们是如何从图像中学习模式的。

像其他神经网络一样,CNN 由输入层、隐藏层和输出层组成。这些网络有可学习的参数,称为权重和偏差。通过适当的优化器(例如 随机梯度下降SGD))与反向传播,权重和偏差可以进行调整。然而,任何前馈人工神经网络与 CNN 之间的区别在于,CNN 中的隐藏层是卷积层。每个卷积层由一个或多个过滤器组成。这些过滤器的任务是识别输入图像中的模式。

这些过滤器可以具有不同的形状,范围从 1 x 1 到 3 x 3 等等,并且它们是以随机权重进行初始化的。当输入图像通过卷积层时,每个过滤器将滑过每个 3 x 3 像素块(以 3 x 3 过滤器为例),直到整个图像被覆盖。这种滑动被称为卷积。在卷积过程中,过滤器权重与 3 x 3 块中的像素值进行点积,从而学习图像的特征。CNN 的初始层学习基本的几何形状,如边缘和圆形,而后续层学习更复杂的物体,如眼睛、耳朵、羽毛、喙、猫和狗。

随着卷积神经网络的加深,随着我们堆叠更多层来学习复杂特征,消失梯度问题也随之而来。换句话说,在训练过程中,一些神经元会失效(不会激活),导致消失梯度问题。这种情况发生在激活函数接收到具有不同分布的输入时(例如,如果你将黑白猫的图像与彩色猫的图像传递给网络,输入的原始像素属于不同的分布,从而导致消失梯度问题)。如果我们将神经元输出限制在接近零的范围内,就能确保每一层都会将有效的梯度传递回前一层。

为了解决卷积神经网络带来的挑战,深度残差学习将前一层学到的内容与浅层模型学到的内容相结合:

这里, 是一个卷积层或浅层模型,而 是前一层。

残差网络在解决卷积神经网络的挑战时,是一种进行图像分类时的最佳方法。在下一部分,我们将探讨迁移学习作为逐步训练已训练图像分类模型的一种方法。

通过迁移学习在 Amazon SageMaker 中对图像进行分类

图像分类的关键挑战之一是大型训练数据集的可用性。例如,为了创建类似 Amazon Go 的体验,电子商务零售商可能已经在大量图像上训练了他们的机器学习算法。当我们没有涵盖所有现实世界场景的图像时——这些场景包括一天中的时间(亮度)、目标物品周围的环境以及物品角度——我们就无法训练出在现实环境中表现良好的图像分类算法。此外,构建一个对当前数据集最优的卷积神经网络架构需要付出大量的努力。这些考虑因素从卷积层的数量到批处理大小,再到优化器和丢弃率,都需要进行反复试验和调整,才能得到一个最优的模型迭代。

因为图像分类需要大量的图像来训练卷积网络,当训练数据集较小时,可以采用另一种方法来对图像进行分类。迁移学习允许你将已经训练好的模型的知识应用于不同但相关的问题。我们可以重用一个经过百万张图像训练的预训练深度学习模型的权重,并通过新的/自定义的数据集对网络进行微调,这些数据集是针对我们业务案例特有的。通过迁移学习,低级几何特征,如边缘,已经能够被一个预训练的 ResNet-18(18 层网络)识别。然而,对于中级到高级特征学习,顶部的全连接FC)层会被重新初始化为随机权重。然后,通过新数据对整个网络进行微调——随机权重通过将训练数据传入网络并使用优化技术(例如,带有反向传播的随机梯度下降)进行调整。

在本章中,我们将采用 SageMaker 的图像分类算法,以迁移学习模式对一些面包店和快餐项目进行分类。我们将使用 Amazon SageMaker 提供的预训练 ResNet-18。图像分类算法实现了 ResNet 来对图像进行分类。我们可以从头开始训练 ResNet,或者使用预训练的网络。由于我们有一个小的图像数据集来进行训练,我们将使用 Amazon SageMaker 提供的 18 层预训练 ResNet。我们还可以尝试使用 ResNet50,一个 50 层的残差网络,以确定哪个网络的性能更好。通常,深层网络比浅层网络表现更好,因为它们能够更好地表示图像。然而,考虑到输入图像的类型和复杂性,结果可能会有所不同。

我们的新数据集包含约 302 张图像,涵盖五个类别(热狗、莓果甜甜圈、糖霜扭结、松饼和花生酱饼干)。每个类别包含 40 到 90 张图像,涵盖不同的角度,以及亮度、对比度和尺寸。

图像分类器从一个预训练的 ResNet 中学习图像的低级特征,并通过使用新的数据集训练相同的 ResNet-18 来学习高级特征。以下是 SageMaker 的图像分类算法如何学习一只莓果甜甜圈的特征——低级、中级和高级——的示意图:

到目前为止,我们已经回顾了迁移学习是什么以及何时适用。我们还简要描述了将要提供给 SageMaker 图像分类算法的图像数据集。现在,来准备好图像数据集进行训练。

为图像分类创建输入

Amazon SageMaker 的图像分类算法通过两种内容类型接受文件模式下的图像,分别是:

  • RecordIO(application/x-recordio

  • 图像(image/.png、image/.jpeg 和 application/x-image

在本章中,我们将使用 RecordIO 格式。RecordIO 是一种用于高效表示图像并以紧凑格式存储它们的二进制格式。训练和验证图像以压缩格式提供,作为本章相关源代码的一部分。

为了为我们的训练和验证数据集创建 RecordIO 文件,我们将执行以下操作:

  • 提取 .zip 文件,包括训练和验证文件(通过 extract_zipfile 函数)

  • 为训练和验证创建列表文件(通过 create_listfile 函数)

  • 创建用于训练和验证的 Record IO 文件(通过 create_recordio 函数)

有关这些函数的定义,请参阅随附的源代码文件夹:

# Extract training and validation zipped folders to merch_data/<train/val>

extract_zipfile(bucket, train_key, rel_train_path)
extract_zipfile(bucket, val_key, rel_val_path)

# Create List files (./merch_data)
create_listfile(rel_train_path, listfile_train_prefix) #data path, prefix path
create_listfile(rel_val_path, listfile_val_prefix)

# # Create RecordIO file
# data path --> prefix path (location of list file)
# mxnet's im2rec.py uses ./merch_data folder to locate .lst files for train and val
# mxnet's im2rec.py uses ./merch_data/<train/val> as data path
# list files are used to create recordio files

create_recordio(rel_train_path, listfile_train_prefix)
create_recordio(rel_val_path, listfile_val_prefix)

为了为训练和验证数据集创建 RecordIO 格式,我们需要创建一个列表文件,列出图像索引,后面跟着图像分类(注意我们有五类图像)和图像本身的位置。我们需要为训练和验证数据集中的每张图像定义这些属性。为了创建图像的列表文件,我们将使用 MXNet 的 im2rec图像转 RecordIO)模块,MXNet 是一个用于训练和部署深度学习模型的开源深度学习库。

以下代码片段展示了如何使用 im2rec 模块创建列表文件。为了创建列表文件,im2rec 需要图像的位置:

# Create List file for all images present in a directory

def create_listfile(data_path, prefix_path):
    """
    input: location of data -- path and prefix
    """

    # Obtain the path of im2rec.py on the current ec2 instance
    im2rec_path = mx.test_utils.get_im2rec_path()

    with open(os.devnull, 'wb') as devnull:
        subprocess.check_call(['python', im2rec_path, '--list', '--recursive', prefix_path, data_path], stdout=devnull) 

create_listfile() 函数产生如下输出。以下是示例列表文件的摘录:

从我们创建的列表文件中,我们通过 RecordIO 格式生成图像的压缩表示——同样使用 MXNet 的 im2rec 模块。

我们将上传前述的训练和验证数据集(.rec 文件)到 S3 存储桶。此外,我们还将把测试图像单独上传到测试文件夹,而不是上传到训练和验证图像文件夹。请参阅随附的源代码文件夹。以下截图显示了 S3 存储桶及相关数据集:

现在我们已经准备好了所有用于训练和推理的数据集,接下来我们准备定义图像分类算法的参数。

定义用于图像分类的超参数

在将模型拟合到训练和验证数据集之前,我们需要指定两类参数:

  • 训练任务的参数

  • 特定于算法的超参数

训练任务的参数处理输入和输出配置,包括要提供的基础设施类型。

为了训练任务配置,我们需要执行以下步骤:

  1. 首先,我们需要定义图像分类的 Docker 镜像和训练输入模式(文件模式与管道模式。管道模式是 SageMaker 工具包中的新功能,数据输入会实时传送到算法容器中,无需在训练前下载)。

  2. 接下来,我们定义训练输出的位置(S3OutputPath),以及要配置的 EC2 实例的数量和类型,以及超参数。

  3. 接下来,我们指定训练验证通道,这些通道将作为训练和验证数据的位置。至于分布式训练,目前该算法只支持fullyreplicated模式,其中数据会被复制到每台机器上。

以下超参数是算法特定的:

  • num_layers:网络的层数。在此示例中,我们将使用默认的 18 层。

  • image_shape:图像维度(宽度 x 高度)。

  • num_training_samples:训练数据点的总数。在我们的案例中,这个值为302

  • num_classes:类别数。对于我们的数据集,这是 5。我们将对五个商品进行分类。

  • mini_batch_size:每个小批量使用的训练样本数。在单机多 GPU 设置中,每个 GPU 处理mini_batch_size/GPU 数量的样本。在分布式训练中,当涉及多个机器时,实际批量大小是machines * mini_batch_size

  • epochs:训练分类算法所需的迭代次数。

  • learning_rate:这定义了反向传播时应该采取多大的步长以减少损失。在迁移学习的情况下,我们将采取较小的步长,以便可以逐步训练预训练的网络。

在以下代码中,我们定义了每个超参数的值:

# The algorithm supports multiple network depth (number of layers). They are 18, 34, 50, 101, 152 and 200
# For this training, we will use 18 layers

num_layers = 18
image_shape = "3,224,224" # Number of channels for color image, Number of rows, and columns (blue, green and red)
num_training_samples = 302 # number of training samples in the training set
num_classes = 5 # specify the number of output classes
mini_batch_size = 60 # batch size for training
epochs = 4  # number of epochs
learning_rate = 0.01 #learning rate
top_k=2
# Since we are using transfer learning, we set use_pretrained_model to 1 so that weights can be initialized with pre-trained weights
use_pretrained_model = 1

现在是训练时间:我们将提供作为输入定义的训练参数,并将其传递给 SageMaker 的create_training_job方法。SageMaker 服务通过boto3调用,boto3是一个 Amazon Web Services 的 Python SDK。创建训练任务后,我们可以检查其状态。

使用以下代码在 SageMaker 中创建训练任务:

# create the Amazon SageMaker training job
sagemaker = boto3.client(service_name='sagemaker')
sagemaker.create_training_job(**training_params)

# confirm that the training job has started
status = sagemaker.describe_training_job(TrainingJobName=job_name)['TrainingJobStatus']
print('Training job current status: {}'.format(status))

Output:
Training job current status: InProgress
Training job ended with status: Completed

现在,我们将绘制结果图来评估 ResNet-18 的训练和验证准确度。我们希望确保没有过拟合网络——即当训练准确度增加时,验证准确度下降的情况。让我们看一下以下的图表:

训练结果可以在 CloudWatch 日志中查看。上面的表示是训练期间,训练集和验证集准确度变化的可视化图。以下代码解释了上面图中蓝色和橙色线条的含义:

Training: Blue Line -- trn_acc[0.366667, 0.86, 0.966667, 0.986667]

Validation: Orange Line -- val_acc[0.45, 0.583333, 0.583333, 0.716667] 

如我们所见,训练后的 ResNet 模型已从快餐和面包店图像中学习到了足够的模式。我们已将训练好的模型部署用于推理。

通过 Batch Transform 执行推理

在本节中,我们将以批量模式分类一些测试数据集中的图像。由于我们希望一次分类多张图片,因此我们将创建一个 Batch Transform 作业。请参阅第八章,创建机器学习推理管道,以了解何时以及如何使用 Batch Transform 作业。

在创建 Batch Transform 作业之前,我们需要配置训练好的模型。

在以下代码片段中,我们将执行以下操作:

  1. 我们将通过调用 SageMaker 服务的 create_model() 函数来创建一个训练好的模型(boto3,即 AWS Python SDK,用于配置与 SageMaker 服务的低级接口)。

  2. 我们将把图像分类算法的 Docker 镜像和训练好的模型路径传递给这个函数:

info = sage.describe_training_job(TrainingJobName=job_name)
# Get S3 location of the model artifacts
model_data = info['ModelArtifacts']['S3ModelArtifacts']
print(model_data)
# Get the docker image of image classification algorithm
hosting_image = get_image_uri(boto3.Session().region_name, 'image-classification')
primary_container = {
    'Image': hosting_image,
    'ModelDataUrl': model_data,
}
# Create model 
create_model_response = sage.create_model(
    ModelName = model_name,
    ExecutionRoleArn = role,
    PrimaryContainer = primary_container)
print(create_model_response['ModelArn'])
  1. 现在训练好的模型已配置完成,我们需要创建一个 Batch Transform 作业。

我们将指定转换输入、输出和资源来配置 Batch Transform 作业。以下是定义:

    • 转换输入定义了图像的位置和格式。

    • 转换输出定义了推理结果的位置。

    • 转换资源定义了要配置的实例数量和类型。

在以下代码片段中,我们通过将作业规格作为 request JSON 文件的一部分传递,调用 SageMaker 服务的 create_transform_job 函数:

sagemaker = boto3.client('sagemaker')
sagemaker.create_transform_job(**request)

print("Created Transform job with name: ", batch_job_name)

while(True):
    response = sagemaker.describe_transform_job(TransformJobName=batch_job_name)
    status = response['TransformJobStatus']
    if status == 'Completed':
        print("Transform job ended with status: " + status)
        break
    if status == 'Failed':
        message = response['FailureReason']
        print('Transform failed with the following error: {}'.format(message))
        raise Exception('Transform job failed') 
    time.sleep(30) 
  1. 在之前的代码中,我们使用了 SageMaker 服务的 describe_transform_job() 函数来获取 Batch Transform 作业的状态。前面的代码将返回以下消息:
Created Transform job with name: merch-classification-model-2019-03-13-11-59-13
Transform job ended with status: Completed

现在是回顾结果的时候了。让我们导航到 S3 存储桶中的 Batch Transform 输出和测试数据集文件夹,以查看结果。对于测试数据集中的每张图片,我们将打印其最高分类概率,也就是训练好的模型将输入图像分类为:

  1. 测试数据集中的第一张图片是一只热狗,如下图所示。训练好的模型以 92% 的概率识别这只热狗。

以下是预测结果,即标签:Hot_Dog_1,概率:0.92

  1. 第二张图片是一只浆果甜甜圈,如下图所示。训练好的模型以 99% 的概率识别这张截图为浆果甜甜圈:

  1. 第三张图片是一个松饼,如下图所示。训练好的模型以 66% 的概率将以下截图识别为松饼:

  1. 然而,在第四张图片的情况下,训练好的模型并未正确识别图像。虽然实际图像是花生酱饼干,但模型将其误识别为松饼。这里有一个有趣的地方是,这个饼干看起来像松饼:

正如我们所见,在四张图像中,有三张被正确分类。为了提高模型的准确性,我们可以考虑调整超参数并收集大量的快餐和烘焙食品图像。因此,迁移学习被用来通过使用特定应用场景的图像来逐步训练预训练的图像分类模型。

总结

在本章中,我们回顾了卷积神经网络和残差网络的概述。此外,我们还展示了如何使用 SageMaker 的图像分类算法来识别快餐和烘焙食品图像。具体来说,我们回顾了训练图像分类算法的过程,包括为其提供基础设施;创建用于训练和验证数据集的压缩图像格式(RecordIO);以及提供格式化数据集以进行模型拟合。在推理方面,我们使用了 SageMaker 的批量转换功能,一次性对多个图像进行分类。

最重要的是,我们学会了如何将迁移学习应用于图像分类。这种技术在你没有大量训练数据的情况下尤为强大。

在下一章,你将学习如何使用 SageMaker 的 DeepAR 算法预测零售销售——这是深度学习应用于解决真实商业挑战的另一个案例。

进一步阅读

第十一章:使用深度学习和自回归进行销售预测

需求预测是许多行业的关键,如航空公司、零售、电信和医疗保健。不准确和不精确的需求预测会导致错失销售和客户,显著影响组织的底线。零售商面临的一个主要挑战是如何有效地管理基于多种内部和外部因素的库存。库存管理是一个复杂的商业问题——产品需求会根据地点、天气、促销活动、节假日、星期几、特殊事件以及其他外部因素(如门店人口统计数据、消费者信心和失业率)发生变化。

在本章中,我们将研究传统的时间序列预测技术(如 ARIMA 和指数平滑)与基于神经网络的技术有何不同。我们还将讨论 DeepAR 如何工作,并探讨其模型架构。

以下是本章将要涵盖的主题:

  • 理解传统时间序列预测

  • 理解 DeepAR 模型的工作原理

  • 通过 DeepAR 理解模型销售

  • 销售预测与评估

技术要求

在接下来的部分中,我们将使用零售数据集,其中包含约 45 家门店的销售数据,来说明 DeepAR 如何根据假期、促销和宏观经济指标(如失业率)等多个因素预测未来销售。

在与本章相关的文件夹中,您将找到三个 CSV 文件:

  • 特征数据集:此数据集包含与门店相关的区域活动数据。

  • 销售数据集:此数据集包含 2010 年至 2012 年间的历史销售数据,覆盖了 143 周。

  • 门店数据集:此数据集包含关于 45 家门店的匿名化信息,包括门店类型和大小。

请参考以下 GitHub 链接获取本章的源代码:

github.com/PacktPublishing/Hands-On-Artificial-Intelligence-on-Amazon-Web-Services

现在是时候理解传统的时间序列预测技术了。

理解传统时间序列预测

让我们从研究传统的时间序列预测技术开始,具体来说是 ARIMA 和指数平滑方法,以便在简单的使用案例中建模需求。我们将研究 ARIMA 如何利用历史销售数据和预测误差来估算销售。此外,我们还将回顾指数平滑如何处理历史销售数据中的不规则性,并捕捉趋势和季节性变化来预测销售。

自回归积分滑动平均(ARIMA)

ARIMA 是一种时间序列分析技术,用于捕捉单变量数据中的不同时间结构。为了对时间序列数据建模,对数据进行差分处理,使其平稳。差分技术是指对每个数据点(不包括第一个数据点),用当前数据点减去前一个数据点。此技术使得时间序列的概率分布的均值和方差在时间上保持不变,从而使得未来值的预测更加可靠。使用特定数量的滞后预测和预测误差来建模时间序列。该数量会通过迭代调整,直到残差与目标(销售预测)不相关,或者数据中的所有信号都已被捕捉。

让我们解析 ARIMA,看看其基本组成部分——自回归、差分和移动平均:

  • 自回归项的数量:这些项建立了特定数量历史数据点与当前数据点之间的关系,也就是说,它利用历史需求来估算当前需求。

  • 非季节性差分的数量:这些通过差分使时间序列数据变得平稳。我们假设,如果过去几次时间步长中的需求差异非常小,未来的需求将与历史需求相似。

  • 移动平均项的数量(滞后预测误差):这些项考虑了预测误差——实际需求与预测需求之间的差异——或者特定数量的历史数据点。

让我们看一下 ARIMA 方程,既有文字描述也有数学表达形式:

需求预测 = 常数项 + 自回归项 + 滚动平均项

这里是 ARIMA 工作原理的可视化表示:

在 ARIMA 模型中,AR 项为正,而 MA 项为负;换句话说,自回归项对需求产生正面影响,而滞后误差的移动平均则对需求产生负面影响。

指数平滑

ARIMA 的另一种替代方法是指数平滑技术,这也是一种用于单变量数据的时间序列预测方法,其中忽略了随机噪声,揭示了潜在的时间结构。尽管它与 ARIMA 类似,都是将过去的观测值加权求和来进行需求预测,但加权应用的方法不同——它并不是对过去的观测值赋予相等的权重,而是对滞后的观测值应用指数衰减的权重。换句话说,最近的观测值比历史数据更为相关。指数平滑用于进行短期预测,我们假设未来的模式和趋势将与当前的模式和趋势相似。

以下是三种类型的指数平滑方法:

  • 单一指数平滑:顾名思义,这种技术不考虑季节性或趋势。它需要一个单一参数,alpha (),来控制平滑程度。低 alpha 表示数据中没有不规则性,意味着最新的观察值被赋予较低的权重。

  • 双重指数平滑:这种技术支持单变量序列中的趋势。除了控制最近观察值与历史观察值之间的重要性外,还使用一个额外的因子来控制趋势对需求预测的影响。趋势可以是乘法型的也可以是加法型的,并通过平滑因子 来控制。

  • 三重指数平滑:此方法支持季节性。另一个新参数,gamma (),用于控制季节性因素对需求预测的影响。

下图展示了不同类型的指数平滑方法之间的区别:

在上面的图示中,我们可以看到以下内容:

  • 通过单一指数平滑预测在时刻 t 的需求,基于时刻 t-1 处的估计需求和预测误差(实际需求—估计需求)。

  • 在双重指数平滑的情况下,需求通过捕捉趋势和历史数据来进行预测。我们在这里使用两个平滑因子,数据和平滑趋势(以下是双重指数平滑如何捕捉趋势的可视化图示):

  • 对于三重指数平滑,我们还通过一个名为季节性平滑因子的第三个平滑因子来考虑季节性。请参见下图,它展示了季节性高峰和低谷,以及趋势:

这种方法的问题在于它将过去的销售数据视为未来销售的指标。此外,这些方法都是针对单一时间序列的预测技术。正如前面所详细说明的那样,可能会有其他因素影响当前和未来的销售情况,如天气、促销、星期几、节假日和特殊事件。

让我们来看一下如何利用 SageMaker 的 DeepAR 模型来建模多变量时间序列,定义输出变量(需求)与输入变量(包括历史销售数据、促销、天气和一天中的时间)之间的非线性关系。

DeepAR 模型是如何工作的

Sagemaker 提供的 DeepAR 算法是一个通用的深度学习模型,能够学习多个相关时间序列中的需求。与传统的预测方法不同,传统方法是针对单个时间序列进行建模,而 DeepAR 则可以建模成千上万甚至百万个相关时间序列。

例如,在数据中心预测服务器负载,或预测零售商提供的所有产品的需求,或预测各个家庭的能源消耗。该方法的独特之处在于,能够利用大量关于类似或相关时间序列的过去行为数据来预测单个时间序列。这种方法解决了传统技术所面临的过拟合问题,以及所需的时间和劳动密集型的手动特征工程和模型选择步骤。

DeepAR 是一种基于自回归神经网络的预测方法,它通过数据集中所有时间序列的历史数据学习全局模型。DeepAR 使用长短期记忆LSTM),一种递归神经网络RNN),来建模时间序列。RNN 的主要思想是捕捉顺序信息。与普通神经网络不同,输入(和输出)是相互依赖的。因此,RNN 具有记忆功能,能够捕捉到迄今为止所估算的信息。以下是展开的 RNN 示意图——为了记住已学习的信息,在每一步中,隐藏状态的计算不仅基于当前输入,还基于先前的隐藏状态:

递归神经网络和随着时间步展开的顺序学习示意图。来源:Nature;图片来源 WildML.

让我们更详细地解释一下:

  • 在时间t时输入。

  • ![]是在时间t时的隐藏状态。这个状态是基于之前的隐藏状态和当前输入计算得出的,在![]中,f是激活函数。

  • ![]是在时间t时的输出,![]。激活函数,f,根据具体应用可能有所不同。例如,当我们需要预测输入属于哪个类别时,会使用 softmax 激活函数——换句话说,是否检测到的图像是一只猫、一只狗还是一只长颈鹿。

  • 网络权重,UV,和W,在所有时间步中保持不变。

RNN 在不同领域有着有趣的应用,举例如下:

  • 自然语言处理:从生成图像描述到生成文本,再到机器翻译,RNN 可以作为生成模型。

  • 自动驾驶汽车:它们用于进行动态面部分析。

  • 时间序列:RNN 广泛应用于计量经济学(金融和趋势监测)和需求预测。

然而,普通的 RNN 由于最近数据和较老数据之间的间隙,无法学习长期依赖关系。而 LSTM 则可以解决这个问题:LSTM 的内层单元通过特殊的结构——门控(输入、遗忘和输出)——能够不变地携带信息。通过这些单元,LSTM 可以控制保留或删除的信息。

现在让我们看看 DeepAR 的模型架构,即 LSTM 网络。

模型架构

DeepAR 算法采用 LSTM 网络和概率模型来识别时间序列数据中的非线性结构,并提供预测的概率估计。

该模型是自回归的,它将上一个时间步的观察值作为输入。它也是递归的,因为它在下一个时间步使用网络的先前输出作为输入。在训练阶段,网络在每个时间步的隐藏状态或编码状态是基于当前协变量、先前的观察值和先前的网络输出计算的。然后,隐藏状态用于计算概率模型的参数,以描述时间序列的行为(例如,产品需求)。

换句话说,我们假设需求是一个随机变量,服从特定的概率分布。一旦我们有了可以通过一组参数(如均值和方差)定义的概率模型,就可以用来估计预测值。DeepAR 使用 Adam 优化器——一种随机梯度下降算法——来优化给定高斯模型参数的训练数据的最大对数似然性。通过这种方法,我们可以推导(优化)概率模型参数和 LSTM 参数,以准确估计预测。

以下图示展示了 DeepAR 算法的工作原理:

如上图所示,最大似然估计MLE)用于估计两组参数,前提是输入数据集中的所有时间序列都已给出:

  • RNN 的参数:这些参数或 RNN 网络的隐藏状态用于计算高斯参数。

  • 高斯模型的参数:高斯参数用于提供预测的概率估计。

MLE 是通过利用所有时间序列的数据计算的,i,其中i从 1 到N,也就是说,可能有N个不同的产品,其需求是你试图估计的。T表示时间序列的长度。

有关最大似然估计(MLE)的更多信息,请参阅这篇文章

到达最佳网络权重

时间序列或观测数据作为训练的一部分输入 DeepAR。在每个时间步,当前的协变量、先前的观测值以及先前的网络输出都会被使用。该模型使用时间反向传播BPTT)在每次迭代后计算梯度下降。特别地,Adam 优化器被用来进行 BPTT。通过随机梯度下降算法 Adam,我们通过反向传播得到了最优的网络权重。

在每个时间步t,网络的输入包括协变量,;上一个时间步的目标,;以及上一个网络输出,。然后,网络输出,,用于计算高斯参数,以最大化观察输入数据集的概率。

以下视觉图示说明了序列到序列的学习过程,其中编码器将历史时间序列中的需求模式进行封装,并将相同的数据()作为输入传递给解码器。解码器的功能是根据来自编码器的输入来预测需求:

来源:使用自回归递归网络的概率预测(link

对于预测,将时间序列的历史数据,,输入到预测范围中,之后在预测范围内,采样并反馈至下一点,直到预测范围结束,

DeepAR 通过联合学习所有时间序列的历史行为,产生精确的预测分布。此外,概率预测在不确定性下提供最优决策,而非点估计。

通过 DeepAR 理解模型销售

如本章介绍所述,零售商的库存管理是一项复杂的活动。节假日、特殊事件和降价促销会对商店的表现产生重大影响,进而影响商店内部各个部门的表现。

Kaggle 数据集包含 45 家商店的历史销售数据,每个商店属于特定类型(位置和表现)和规模。零售商全年进行几次促销降价。这些降价活动通常发生在节假日之前,如超级碗、劳动节、感恩节和圣诞节。

数据集简要描述

让我们简要考虑一下我们即将建模的数据集:

  • 特征数据:这是与商店相关的区域活动数据:

    • 商店:每个商店的数字化商店 ID。

    • Date: 商店的重要日期。

    • Fuel price: 当前燃油价格。

    • Markdowns: 在零售商店中,你从原始标价中获得的商品折扣。

    • CPI消费者物价指数):衡量一篮子消费品和服务(如交通、食品和医疗)的加权平均价格的指数。

    • Unemployment: 当前失业率。

    • IsHoliday: 特定日期是否为假期。

  • Sales data: 这是覆盖 2010 年到 2012 年三年的历史销售数据,共 143 周的销售数据:

    • Store: 每个商店的数字化商店 ID。

    • Dept: 每个部门的数字化部门 ID。

    • Date: 商店的重要日期。

    • Weekly sales: 每周销售额,用于衡量每个商店的销售表现。

    • IsHoliday: 特定日期是否为假期。

  • Store data: 这是 45 个商店的匿名信息,包括商店的类型和规模:

    • Store: 每个商店的数字化商店 ID。

    • Type: 商店的类型。

    • Size: 商店的规模。

  • Model Input and Output: 现在让我们来看一下输入和输出格式,包括 SageMaker DeepAR 算法的超参数。

算法有两个输入通道,并通过这两个通道输入训练和测试 JSON。训练 JSON 仅包含 134 周的销售数据,而测试 JSON 包含全部 143 周的销售数据。

以下是训练 JSON 的结构:

Training JSON {
Start: The starting date of weekly sales
Target: Weekly sales
Cat: Category or Department used to group sales
Dynamic_feat: Dynamic features used to explain variation in sales. Beyond holidays, these features can include price, promotion and other covariates.
}
{"start":"2010-01-01 00:00:00","target":[19145.49, 17743.27, 14700.85, 20092.86, 17884.43, 19269.09, 22988.12, 17679.72, 16876.61, 14539.77, 16026.23, 14249.85, 15474.07, 22464.57, 19075.56, 20999.38, 18139.89, 13496.23, 15361.65, 16164.48, 15039.44, 14077.75, 16733.58, 16552.23, 17393.2, 16608.36, 21183.71, 16089.01, 18076.54, 19378.51, 15001.62, 14691.15, 19127.39, 17968.37, 20380.96, 29874.28, 19240.27, 17462.27, 17327.15, 16313.51, 20978.94, 28561.95, 19232.34, 20396.46, 21052.61, 30278.47, 47913.44, 17054.1, 15355.95, 15704.19, 15193.36, 14040.86, 13720.49, 17758.99, 24013.25, 24157.54, 22574.19, 12911.72, 20266.06, 18102.13, 21749.04, 22252.73, 21672.82, 15231.31, 16781.35, 14919.64, 15948.11, 17263.32, 16859.26, 13326.75, 17929.47, 15888.17, 13827.35, 16180.46, 22720.76, 15347.18, 15089.43, 14016.56, 17147.61, 14301.9, 16951.62, 16623.8, 19349.35, 24535.59, 18402.46, 19320.64, 20048.28, 14622.65, 19402.27, 19657.79, 18587.11, 20878.24, 19686.7, 23664.29, 20825.85, 27059.08, 15693.12, 29177.6, 45362.67, 20011.27, 13499.62, 15187.32, 16988.52, 14707.59, 20127.86, 23249.25, 20804.15, 19921.62, 16096.04, 18055.34, 17727.24, 16478.45, 16117.33, 15082.89, 15050.07, 17302.59, 20399.83, 17484.31, 14056.35, 16979.18, 17279.4, 14494.48, 14661.37, 13979.33, 13476.7, 18898.57, 13740.2, 15684.97, 15266.29, 16321.69, 15728.07, 17429.51, 17514.05, 20629.24], 
"cat":[15], "dynamic_feat":[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0]]}

在上面的结构中,我们可以看到以下内容:

  • start: 每周销售的开始日期。

  • target: 用于排序后的每周销售数据。

  • cat: 用于分组时间序列的类别。

  • Dynamic_feat: 包括动态特征,用于考虑影响销售的因素,如假期。

测试 JSON 的格式与训练 JSON 相同。我们来看看以下代码:

Test JSON {"start":"2010-01-01 00:00:00","target":[19145.49, 17743.27, 14700.85, 20092.86, 17884.43, 19269.09, 22988.12, 17679.72, 16876.61, 14539.77, 16026.23, 14249.85, 15474.07, 22464.57, 19075.56, 20999.38, 18139.89, 13496.23, 15361.65, 16164.48, 15039.44, 14077.75, 16733.58, 16552.23, 17393.2, 16608.36, 21183.71, 16089.01, 18076.54, 19378.51, 15001.62, 14691.15, 19127.39, 17968.37, 20380.96, 29874.28, 19240.27, 17462.27, 17327.15, 16313.51, 20978.94, 28561.95, 19232.34, 20396.46, 21052.61, 30278.47, 47913.44, 17054.1, 15355.95, 15704.19, 15193.36, 14040.86, 13720.49, 17758.99, 24013.25, 24157.54, 22574.19, 12911.72, 20266.06, 18102.13, 21749.04, 22252.73, 21672.82, 15231.31, 16781.35, 14919.64, 15948.11, 17263.32, 16859.26, 13326.75, 17929.47, 15888.17, 13827.35, 16180.46, 22720.76, 15347.18, 15089.43, 14016.56, 17147.61, 14301.9, 16951.62, 16623.8, 19349.35, 24535.59, 18402.46, 19320.64, 20048.28, 14622.65, 19402.27, 19657.79, 18587.11, 20878.24, 19686.7, 23664.29, 20825.85, 27059.08, 15693.12, 29177.6, 45362.67, 20011.27, 13499.62, 15187.32, 16988.52, 14707.59, 20127.86, 23249.25, 20804.15, 19921.62, 16096.04, 18055.34, 17727.24, 16478.45, 16117.33, 15082.89, 15050.07, 17302.59, 20399.83, 17484.31, 14056.35, 16979.18, 17279.4, 14494.48, 14661.37, 13979.33, 13476.7, 18898.57, 13740.2, 15684.97, 15266.29, 16321.69, 15728.07, 17429.51, 17514.05, 20629.24, 17730.73, 18966.48, 20781.46, 22979.73, 16402.34, 20037.44, 18535.65, 16809.01, 19275.43], "cat":[15], "dynamic_feat":[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0]]

DeepAR 支持一系列超参数。以下是一些关键超参数的列表。详细列表请参阅亚马逊文档这里

  • Time frequency: 表示时间序列是按小时、按周、按月还是按年进行的。

  • Context length: 算法在训练时需要查看的过去时间步长数量。

  • Prediction length: 需要预测的数据点数量。

  • Number of cells: 每个隐藏层中使用的神经元数量。

  • Number of layers: 隐藏层的数量。

  • Likelihood function: 由于每周销售额是实际值,我们将选择高斯模型。

  • epochs: 训练数据的最大遍历次数。

  • Mini batch size: 训练过程中使用的小批量的大小。

  • Learning rate: 损失优化的速度。

  • Dropout rate: 每次迭代中未更新的隐藏神经元的百分比。

  • 提前停止耐心值:当指定数量的训练周期(那些损失未改进的周期)没有取得改进时,训练将停止。

  • 推理:对于给定的部门,我们发送了 134 周的历史销售数据,以及该部门类别和假期标志,涵盖了所有的周。

以下是来自模型端点的示例 JSON 输出。由于 DeepAR 生成概率预测,输出包含来自高斯分布的多个销售样本。这些样本的均值和分位数(50%和 90%)也会被报告,如下所示:

{
   "predictions": [
       {
           "quantiles": { 
               "0.9": [...],
               "0.5": [...]
           },
           "samples": [...],
           "mean": [...]
       }
   ]
}

我们刚刚回顾了 DeepAR 算法对于具有历史每周销售数据的商品的输入和输出示例。

DeepAR 还提供了独特的功能,能够处理现实世界时间序列问题中的复杂性。对于新商品或产品,时间序列的长度将短于具有完整销售历史的常规商品。DeepAR 能够捕捉新商品或产品首次观察的距离。由于该算法在多个时间序列中学习商品需求,它甚至可以估计新推出商品的需求——所有时间序列中的每周销售长度不需要保持一致。此外,算法还能够处理缺失值,缺失值将被替换为“Nan”。

以下截图是 DeepAR 输入和输出多样性的可视化表示:

如前所示,通过对所有新商品(年龄)和常规商品进行建模,可以生成每周销售的概率预测(销售时间序列),同时输入商品类别(商品嵌入)和其他特征(价格和促销)。

探索性数据分析

尽管有 45 家商店,但我们将选择其中一家商店,即商店编号 20,来分析三年来不同部门的业绩。这里的主要思想是,使用 DeepAR,我们可以学习不同部门商品的销售情况。

在 SageMaker 中,通过生命周期配置,我们可以在笔记本实例启动之前自定义安装 Python 包。这避免了在执行笔记本之前手动追踪所需的包。

为了探索零售销售数据,我们需要安装最新版本 0.9.0 的seaborn

在 SageMaker 中,点击笔记本下的生命周期配置:

  1. 在开始笔记本下,输入命令以升级seaborn Python 包,如下所示:

  1. 通过点击笔记本实例,选择操作,然后选择更新设置,编辑笔记本设置。

  2. 在“更新设置”生命周期配置部分下,选择新创建的生命周期配置的名称。

该选项使 SageMaker 能够在笔记本实例可用之前管理所有 Python 的前置条件,如下所示:

让我们合并销售、商店和特征的 CSV 文件数据:

  1. 我们将导入关键的 Python 库,如下所示:
import numpy #library to compute linear algebraic equations
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
  1. 让我们将.csv文件读取到 Python 数据框中,如下所示:
features = pd.read_csv('Features data set.csv')
sales = pd.read_csv('sales data-set.csv')
stores = pd.read_csv('stores data-set.csv')
  1. 让我们看一下每个创建的数据框的形状,如下所示:
features.shape #There are 8,190 store, date and holiday combinations
sales.shape #There are 421,570 sales transactions
stores.shape #There are 45 stores in question
  1. 现在,合并features数据框与salesstores,创建一个包含所有所需信息的数据框,如下所示:
merged_df = features.merge(sales, on=['Store', 'Date', 'IsHoliday']).merge(stores, on=['Store'])
merged_df.head()
  1. IsHoliday转换为数值型,并将Date字段转换为pandas日期格式,如下所示:
merged_df = features.merge(sales, on=['Store', 'Date', 'IsHoliday']).merge(stores, on=['Store'])
merged_df.head()
  1. 使用以下代码将合并后的数据集写入.csv文件:
merged_df.to_csv('retailsales.csv')
  1. 现在,让我们看看可能影响销售额的每个关键因素(TemperatureFuel_PriceUnemploymentCPI)的分布情况,如下所示:
#Create a figure and a set of subplots
f, ax = plt.subplots(4, figsize=(15, 15)) #f=figure; ax=axes
sns.distplot(merged_df.Temperature, ax=ax[0])
sns.distplot(merged_df.Fuel_Price, ax=ax[1])
sns.distplot(merged_df.Unemployment, ax=ax[2])
sns.distplot(merged_df.CPI, ax=ax[3])

我们使用seaborn Python 库绘制数据集中TemperatureFuel_PriceUnemploymentCPI的分布情况。让我们看看以下输出:

从前面的分布可以看出,销售发生时的温度大多在 60 到 80 度之间。同时,在大多数销售活动期间,燃油价格大约在$2.75 到$3.75 之间:

从前面的可视化中可以看出,在大多数销售活动期间,失业率在 6%到 9%之间。至于 CPI,销售活动发生在低 CPI 和高 CPI 水平下。

现在我们已经查看了每个关键特征的分布,接下来让我们看看它们与每周销售额的相关性:

  1. 首先,让我们看一下销售额(目标)与每个解释变量之间的散点图——HolidaysTemperatureCPIUnemploymentStore Type
f, ax = plt.subplots(6, figsize=(20,20))
sns.scatterplot(x="Fuel_Price", y="Weekly_Sales", data=merged_df, ax=ax[0])
sns.scatterplot(x="Temperature", y="Weekly_Sales", data=merged_df, ax=ax[1])

在前面的代码中,我们绘制了销售额与燃油价格和销售额与温度之间的散点图。让我们分析一下燃油价格和温度如何与销售额相关:

  1. 从前面的可视化中可以明显看出,燃油价格在$3.25 到$3.75 之间时,每周销售额较高。而温度在 50 到 65 度之间时,每周销售额较高。

现在,让我们将假期与否和 CPI 与销售额进行绘图,如以下代码所示:

sns.scatterplot(x="IsHoliday", y="Weekly_Sales", data=merged_df, ax=ax[2])
sns.scatterplot(x="CPI", y="Weekly_Sales", data=merged_df, ax=ax[3])

让我们看一下假期与否以及 CPI 如何影响销售额,如下截图所示:

  1. 看起来假期销售额高于非假期销售额。同时,CPI 对每周销售额似乎没有明显的影响。

现在,让我们将UnemploymentStore Type与销售额进行绘图,如以下代码所示:

sns.scatterplot(x="Unemployment", y="Weekly_Sales", data=merged_df, ax=ax[4])
sns.scatterplot(x="Type", y="Weekly_Sales", data=merged_df, ax=ax[5])

让我们看看销售额如何随着失业率和商店类型的变化而变化,如下截图所示:

从前面的可视化中可以看出,当失业率较低(7 到 8.5 之间)时,每周销售额较高,而 B 类型商店的每周销售额似乎较高。

  1. 其次,让我们查看所有特征的热图,找出哪些特征会影响销售额。我们将绘制一个热图,查看销售额与多个销售预测因子之间的相关性。

以下截图是数据集中数值属性的热图——我们从数据集中删除了商店和部门字段,因为它们是分类变量:

从散点图和热图中,可以明显看出以下几点:

  • Markdown 格式的文本会出现在节假日期间。

  • 节假日期间销售额较高。

  • B 类商店产生更高的销售额。

  • 更低的燃油价格(在$3 到$3.75 之间)会带来更高的销售额。

  • 理想的温度(在 50 到 65 度之间)会带来更高的销售额。

在我们进一步建模的过程中,我们将选择表现最好的商店——商店 20,来建模不同部门和不同年份的销售情况。对于时间序列中的每个时间步,我们还将传递该日期是否为假期的标识。

数据预处理

让我们从准备建模所需的数据集开始:

  • 创建一个名为 retailsales.py 的模块,用于创建 DeepAR 可以用于训练和验证的 JSON 文件。

  • 创建一个名为 salesinference.py 的模块,用于构建推理数据并获取和绘制预测结果。

有关模块的详细信息,请参阅本章相关的源代码。

为了将代码模块化以测试 DeepAR,我们将把 retailsalessalesinference 两个模块打包。为了打包这些模块,我们将创建 __init__.py 文件来导入这些模块,然后创建 setup.py 文件,详细说明需要安装的先决条件包。

接下来是 DeepAR 项目的文件夹结构:

DeepAR project structure.
Project Organization
------------
    ├── notebooks/            <- All notebooks are residing here.
    ├── data/                 <- Input data is residing here
    ├── deepar/               <- Python package with source code of this project.
      ├──retailsales.py       <- Creating training and testing datasets for DeepAR.
      ├──salesinference.py    <- Preparing data for predictions, obtaining and plotting predictions from DeepAR
    ├── README.md             <- The top-level README for developers using this project.
    ├── setup.py              <- Defines pre-requisite packages to install and distribute package.

让我们看看接下来的步骤:

  1. setup.py 文件中,我们将定义需要安装的先决条件包:
import os
from setuptools import setup, find_packages

def read(fname):
    return open(os.path.join(os.path.dirname(__file__), fname)).read()

setup(
    name="deepar",
    description="DeepAR project structure.",
    author="<your-name>",
    packages=find_packages(exclude=['data', 'figures', 'output', 'notebooks']),\
    long_description=read('README.md'),
)
  1. _init_.py 文件中,我们将导入之前定义的 retailsalessalesinference 模块:
from . import retailsales
from . import salesinference
  1. 我们现在将安装这个包,以便在训练 DeepAR 时使用这些模块:
#Navidate to deep-ar directory to install the deepar package containing commonly used functions
path = ".."
os.chdir(path)

#install predefined functions
!pip install .

#Navigate to the parent directory to train the DeepAR model
# org_path = ".."
# os.chdir(org_path)

现在我们已经准备好了所有需要的包来预处理每周的销售数据。预处理不仅包括将分类数据转换为数值数据,还包括按照 DeepAR 算法要求的 JSON 格式创建训练和测试数据。

训练 DeepAR 模型

在本节中,我们将为每周销售数据拟合 DeepAR 模型。让我们从准备训练和测试数据集(JSON 格式)开始。

让我们来看一下以下代码,展示了如何创建 json 行:

import deepar as da

train_key      = 'deepar_sales_training.json'
test_key       = 'deepar_sales_test.json'
#Prediction and context length for training the DeepAR model
prediction_length = 9

salesfn = 'data/store20_sales.csv'
salesdf = da.retailsales.prepareSalesData(salesfn)
testSet = da.retailsales.getTestSales(salesdf, test_key)
trainingSet = da.retailsales.getTrainSales(salesdf, train_key, prediction_length)

在上面的代码块中,我们已经创建了用于训练和测试的数据集的 JSON 行:

  • prepareSalesData() 函数用于选择在所有 143 周内都有销售的部门。此步骤确保数据中没有缺失值。虽然 DeepAR 能够处理缺失值,但我们尽量简化问题,只考虑几乎每周都有销售的部门。

  • 我们使用部门编号来分组或分类时间序列,以供 DeepAR 算法使用。此分组将由 DeepAR 用来按部门进行需求预测。

  • getTestSales() 函数用于为测试数据集创建 JSON 行。

  • getTrainSales() 函数则用于为训练数据集创建 JSON 行,该数据集是测试数据集的一个子集。对于每个部门,我们将截取由预测长度决定的最后九周的销售数据。

现在,我们将查看将 json 文件上传到 S3 存储桶的代码,如下所示:

bucket         = 'ai-in-aws'
prefix         = 'sagemaker/deepar-weekly-sales'

train_prefix   = '{}/{}'.format(prefix, 'train')
test_prefix    = '{}/{}'.format(prefix, 'test')
output_prefix  = '{}/{}'.format(prefix, 'output')

sagemaker_session = sagemaker.Session()

train_path = sagemaker_session.upload_data(train_key, bucket=bucket, key_prefix=train_prefix)
test_path = sagemaker_session.upload_data(test_key, bucket=bucket, key_prefix=test_prefix)

在前面的代码中,通过 Sagemaker 会话对象(Sagemaker Python SDK)中的 upload_data() 函数将新创建的 json 文件上传到指定的 S3 存储桶。

我们将通过以下代码获得 DeepAR Docker 镜像的 URI:

role = get_execution_role()
output_path = r's3://{0}/{1}'.format(bucket, output_prefix)

container = get_image_uri(boto3.Session().region_name, 'forecasting-deepar')

deepAR = sagemaker.estimator.Estimator(container,
                                   role,
                                   train_instance_count=1,
                                   train_instance_type='ml.c4.xlarge',
                                   output_path=output_path,
                                   sagemaker_session=sagemaker_session)

在前面的代码块中,我们可以看到以下内容:

  • get_image_uri() 函数来自 SageMaker 估算器对象,用于获取 DeepAR Docker 镜像的 uri

  • 一旦获得 uri,便会创建 DeepAR 估算器。

  • 构造函数参数包括 Docker 镜像 uri、执行角色、训练实例类型和数量,以及 outpath 保存训练后的算法和 SageMaker 会话的路径。

超参数用于配置学习或训练过程。让我们来看一下以下代码中使用的 hyperparameters

hyperparameters = {
    "time_freq": 'W',
    "context_length": prediction_length, 
    "prediction_length": prediction_length,
    "num_cells": "40", 
    "num_layers": "2", 
    "likelihood": "gaussian",
    "epochs": "300", 
    "mini_batch_size": "32", 
    "learning_rate": "0.00001",
    "dropout_rate": "0.05", 
    "early_stopping_patience": "10" 
}
deepAR.set_hyperparameters(**hyperparameters) 

在前面的代码中,我们遇到了以下超参数:

  • learning_rate:定义训练过程中权重更新的速度。

  • dropout_rate:为了避免过拟合,在每次迭代中,会随机选择一部分隐藏神经元不进行更新。

  • num_cells:定义每个隐藏层中使用的单元格数量。

  • num_layers:定义 RNN 中的隐藏层数。

  • time_freq:定义时间序列的频率。

  • epochs:定义训练数据的最大遍历次数。

  • context_length:定义回溯期——我们在预测之前会查看多少个数据点。

  • prediction_length:定义要预测的数据点数量。

  • mini_batch_size:定义权重更新的频率——即在处理指定数量的数据点后更新权重。

在下面的代码中,我们将 deepAR 拟合到训练数据集:

data_channels = {"train": train_path, "test": test_path}\
deepAR.fit(inputs=data_channels)

在前面的代码中,我们可以看到以下内容:

  • 我们将训练和测试 JSON 文件的路径传递到 S3 存储桶中。

  • 测试数据集用于评估模型的性能。

  • 对于训练,我们在 DeepAR 估算器上调用了 fit() 函数。

以下是训练 DeepAR 的输出:

#test_score (algo-1, RMSE): 7307.12501604
#test_score (algo-1, mean_wQuantileLoss): 0.198078
#test_score (algo-1, wQuantileLoss[0.1]): 0.172473
#test_score (algo-1, wQuantileLoss[0.2]): 0.236177
#test_score (algo-1, wQuantileLoss[0.3]): 0.236742
#test_score (algo-1, wQuantileLoss[0.4]): 0.190065
#test_score (algo-1, wQuantileLoss[0.5]): 0.1485
#test_score (algo-1, wQuantileLoss[0.6]): 0.178847
#test_score (algo-1, wQuantileLoss[0.7]): 0.223082
#test_score (algo-1, wQuantileLoss[0.8]): 0.226312
#test_score (algo-1, wQuantileLoss[0.9]): 0.170508

如前面的输出所示,均方根误差 (RMSE) 被用作评估最佳表现模型的指标。

我们已经成功地在我们的训练数据集上训练了 DeepAR 模型,该数据集包含 134 周的每周销售数据。为了将训练数据拟合到模型,我们已经定义了 S3 存储桶中训练和测试 JSON 文件的位置。此外,我们还定义了超参数来控制学习或拟合过程。然后,将表现最好的模型(基于最低 RMSE——即预测销售额尽可能接近实际销售额)进行保存。

销售预测与评估

在这一部分,训练好的模型将被部署,以便我们可以预测给定部门未来九周的每周销售额。

让我们来看一下下面的代码:

deepAR_predictor = deepAR.deploy(initial_instance_count=1, instance_type='ml.m4.xlarge')

在前面的代码中,deepAR 估算器的 deploy 函数用于将模型托管为一个端点。托管实例的数量和类型应通过以下参数指定:

  • initial_instance_count

  • instance_type

为了评估模型性能,我们使用了部门编号 90,如下面的代码所示:

#Predict last 9 weeks of a department and compare to ground truth

deepAR_predictor.content_type = 'application/json'
dept = 90

prediction_data = da.salesinference.buildInferenceData(dept, trainingSet, testSet)
#print(prediction_data)
result = deepAR_predictor.predict(prediction_data)

y_mean, y_q1, y_q2, y_sample = da.salesinference.getInferenceSeries(result)
print("Predicted Sales: ", y_mean)
print("Actual Sales: ", list(testSet[dept]['Weekly_Sales'][134:]))

da.salesinference.plotResults(prediction_length, result, truth=True, truth_data=testSet[dept]['Weekly_Sales'][134:], truth_label='truth')

在前面的代码中,我们可以看到以下内容:

  • buildInferencedata() 函数用于准备 JSON 格式的时间序列数据。我们通过给定部门,列出整个 143 周的假期、134 周的每周销售数据以及对应的商品类别,来构建推断数据。这里的目标是估计最后九周的销售额,其中 9 是预测的长度。

以下是 buildInferenceData 函数生成的 JSON 示例:

  • SageMaker 预测器对象用于推断。

  • getInferenceSeries() 函数用于解析来自 DeepAR 算法的 JSON 结果,识别平均销售额、10 百分位销售额和 90 百分位销售额。请注意,使用高斯分布,DeepAR 生成了未来九周的每周销售的 100 个样本。因此,10 百分位和 90 百分位的销售额表示预测期内每周销售的下限和上限。

  • 从端点返回的结果将通过 plotResults() 函数与实际销售进行对比并绘制。对于九周中的每一周,我们将查看平均销售额、实际销售额、样本销售额、10 百分位销售额和 90 百分位销售额。

如下所示,平均估计销售额接近实际销售额,这表明 DeepAR 算法已经充分捕捉到了不同部门的销售需求。更改部门编号,以评估所有部门的模型性能。因此,概率性销售估计使我们能够比点估计更准确地估算需求。以下是前面代码的输出:

Predicted Sales:  [92707.65625, 101316.90625, 86202.3984375, 87715.5625, 95967.359375, 101363.71875, 106354.90625, 94017.921875, 103476.71875]

Actual Sales:  [100422.86, 94987.08, 90889.75, 115695.71, 100372.02, 96616.19, 93460.57, 99398.64, 105059.88]

在下图中,我们可以看到以下内容:

  • 蓝线表示未来九周预测的平均销售额。

  • 紫线则反映了实际销售情况。

  • 这两条线足够接近,表明模型在捕捉销售模式(考虑了假期和历史每周销售)方面做得相当不错:

我们只看了 20 个门店的销售数据。然而,你可以通过在类别列表中加入门店编号来训练所有门店的销售数据——对于训练集和测试集中的每个时间序列,包含以下代码:

"cat": [department number, store number] 

通过覆盖不同产品和门店的大量时间序列,我们本可以实现更好的性能。

总结

在本章中,我们简要介绍了单变量时间序列预测技术,如 ARIMA 和指数平滑。然而,由于需求受多个变量影响,建模多变量序列变得至关重要。DeepAR 使得建模多变量序列成为可能,并提供了概率预测。虽然在某些情况下点估计可以起作用,但概率估计能够提供更好的数据,帮助做出更优决策。该算法通过生成一个全局模型,跨多个时间序列进行训练。每个项目或产品在多个门店和部门中都会有自己的每周销售数据。训练后的模型会考虑新推出的项目、每个项目的缺失销售数据以及多个能够解释销售的预测变量。借助 LSTM 网络和高斯似然函数,SageMaker 中的 DeepAR 提供了灵活的需求预测方法。此外,我们还通过 SageMaker Python SDK,介绍了模型的训练、选择、托管和推断过程。

现在,既然我们已经体验了 SageMaker 在大规模需求预测中的能力,在下一章中,我们将介绍模型监控与治理,并了解为何模型在生产中会退化。

进一步阅读

各种单变量时间序列预测方法概览:

DeepAR 算法如何工作的详细信息:

DeepAR 推断格式的详细信息:

第四部分:机器学习模型监控与治理

在本节中,我们将重点理解一旦模型投入生产后,必须设置的护栏。具体来说,我们将理解什么是模型退化以及如何应对它,以便对业务决策产生最佳的正面影响。你还将回顾 AWS AI 框架和 AI 基础设施,以便了解 AWS 在大规模构建 AI 解决方案方面提供的无限可能。

本节包括以下章节:

  • 第十二章,模型准确性退化与反馈循环

  • 第十三章,接下来是什么?

第十二章:模型准确度退化与反馈循环

本章将通过广告点击转化的示例来介绍模型性能退化的概念。我们的目标是识别那些导致移动应用下载的广告点击。在这种情况下,广告是用于推广移动应用的。

为了应对模型性能的退化,我们将学习关于反馈循环的内容,即当新数据可用时,我们重新训练模型并评估模型性能的管道。因此,训练后的模型会不断更新,以适应输入数据或训练数据中的变化模式。反馈循环在基于模型输出做出明智的商业决策时非常重要。如果训练后的模型无法充分捕捉动态数据中的模式,它可能会产生次优的结果。

本章将涵盖以下主题:

  • 监控退化性能的模型

  • 开发一个用于训练数据演变的案例——广告点击转化

  • 创建机器学习反馈循环

技术要求

本书的 GitHub 仓库包含本章的源代码,可以在 github.com/PacktPublishing/Hands-On-Artificial-Intelligence-on-Amazon-Web-Services 找到。

监控退化性能的模型

在实际场景中,部署的机器学习模型会随着时间的推移而退化性能。以欺诈检测为例,模型可能无法捕捉到不断变化的欺诈行为。由于欺诈者会随着时间的推移调整他们的方法和流程来规避系统,因此对于欺诈检测引擎来说,使用最新的反映异常行为的数据重新训练是非常重要的。请看下图:

上图展示了模型在生产环境中部署后,如何在预测性能上发生退化。作为另一个例子,在推荐系统中,客户的偏好会根据多种上下文和环境因素不断变化。因此,个性化引擎需要捕捉这种变化的偏好,并向客户呈现最相关的建议。

开发一个用于训练数据演变的案例——广告点击转化

欺诈风险几乎存在于每个行业,例如航空公司、零售、金融服务等。在在线广告中,欺诈风险尤其高。对于投资数字营销的公司来说,控制广告点击中的欺诈性点击非常重要。如果在线广告渠道中充斥着欺诈行为,广告成本可能变得不可承受。本章将通过移动应用的广告点击数据,预测哪些点击可能带来应用下载。通过这一预测,移动应用开发者能够更有效地分配在线营销预算。

广告点击行为是非常动态的。这种行为随时间、地点和广告渠道的变化而变化。欺诈者可以开发软件来自动点击移动应用广告,并隐藏点击的身份——点击可能来自多个 IP 地址、设备、操作系统和渠道。为了捕捉这种动态行为,重新训练分类模型以涵盖新的和新兴的模式变得非常重要。如果我们希望准确确定哪些点击将导致应用下载,实现反馈回路至关重要。例如,如果某些点击发生在最后一刻,来自相同的 IP 地址,并且间隔几分钟,则这些点击可能不会导致应用下载。然而,如果这些点击发生在工作时间,来自不同的 IP 地址,并且分布在整天,那么它们将会导致应用下载。

下图描述了广告点击行为,以及二元结果——是否下载了移动应用:

根据用户如何点击广告——使用了哪种设备、操作系统或渠道、点击时间以及所点击的应用,点击可能会或可能不会转化为移动应用下载。我们将利用这种动态点击行为来说明机器学习中反馈回路的重要性。

创建机器学习反馈回路

在本节中,我们将演示随着新数据的到来,如何通过重新训练分类模型来提升模型性能;即预测哪些广告点击将导致移动应用下载。

我们已经创建了一个合成/人工数据集,模拟了在四天(周一至周四;2018 年 7 月 2 日至 7 月 5 日)内发生的 240 万个点击。数据集可以在此找到:github.com/PacktPublishing/Hands-On-Artificial-Intelligence-on-Amazon-Web-Services/tree/master/Ch12_ModelPerformanceDegradation/Data

数据集包含以下元素:

  • ip: 点击的 IP 地址

  • app: 移动应用类型

  • device: 点击来源的设备类型(例如,iPhone 6 Plus,iPhone 7)

  • os: 点击来源的操作系统类型

  • channel: 点击来源的渠道类型

  • click_time: 点击的时间戳(UTC)

  • is_downloaded: 需要预测的目标,表示应用程序是否已下载

获取最新和最完整的数据是一个挑战。数据湖和数据仓库环境通常会滞后一天(24 小时)。当预测周四接近结束时发生的点击是否会导致应用下载时,确保拥有截至周四的最新数据是至关重要的,且要排除我们正在评分的点击数据,以便进行模型训练。

为了理解反馈循环的意义,我们将训练一个基于树的模型(XGBoost)来预测导致应用下载的广告点击的概率。我们将进行三个不同的实验:

  • 实验 1:使用周一的点击数据进行训练,并预测/评分部分周四的点击数据(来自当天晚些时候的点击)。

  • 实验 2:假设我们在数据湖环境中有更多数据可供使用,以重新训练分类模型。我们将使用周一、周二和周三的点击数据进行训练,并预测/评分部分周四的点击数据。

  • 实验 3:类似地,我们将使用周一、周二、周三和部分周四的点击数据进行训练,并预测/评分部分周四的点击数据。

在每次迭代或实验中,你将看到以下内容:

  • 分类模型的表现通过曲线下面积AUC)来衡量,AUC 通过绘制真正率与假正率来计算。

  • 随机分类器的 AUC 为 0.5。

  • 对于一个最佳模型,AUC 应该接近 1。

  • 换句话说,真正率(你正确识别的应用下载比例)应当高于假正率(那些未导致应用下载的点击,但被错误识别为会导致应用下载的比例)。

现在,我们需要加载和探索数据,以确定预测应用下载的最佳指标。

探索数据

亚马逊 SageMaker 提供了内置工具和能力来创建包含反馈循环的机器学习管道。由于机器学习管道在第八章中已经介绍过,创建机器学习推理管道,在这里我们将重点关注反馈循环的重要性。我们开始吧:

  1. 安装相关的 Python 包,并设置 S3 存储桶中训练、验证和模型输出的路径,如下所示:
!pip install pyarrow
!pip install joblib
!pip install xgboost
#Read the dataset from S3 bucket
s3_bucket = 'ai-in-aws'
s3_prefix = 'Click-Fraud'

s3_train_prefix = os.path.join(s3_prefix, 'train')
s3_val_prefix = os.path.join(s3_prefix, 'val')
s3_output_prefix = os.path.join(s3_prefix, 'output')

s3_train_fn = 'train_sample.csv.zip'
  1. 从本地 SageMaker 实例读取准备好的合成数据集,如以下代码所示:
file_name = 'ad_track_day' fn_ext = '.csv'
num_days = 4
dict_of_ad_trk_df = {}

for i in range(1, num_days+1):
dict_of_ad_trk_df[file_name+str(i)] = pd.read_csv(file_name+str(i)+fn_ext) 
  1. 我们将现在探索数据,以便准备以下特征:

    • 广告点击的来源,即 ipdeviceos

    • 它们何时到达,即 dayhr

    • 它们是如何到达的,即 channel

    • 结合何时、何地和如何

  2. 为每个实验创建数据块。我们将使用pandas库按天汇总广告点击数据,如以下代码所示:

df_ckFraud_exp1 = pd.concat([dict_of_ad_trk_df[key] for key in ["ad_track_day1"]], ignore_index=True)

df_ckFraud_exp2 = pd.concat([dict_of_ad_trk_df[key] for key in ["ad_track_day1", "ad_track_day2", "ad_track_day3"]], ignore_index=True)

df_ckFraud_exp3 = pd.concat([dict_of_ad_trk_df[key] for key in ["ad_track_day1", "ad_track_day2", "ad_track_day3", "ad_track_day4"]], ignore_index=True)

让我们了解最常见的因素,如应用类型、设备、渠道、操作系统和点击来源的 IP 地址,是否能导致应用下载。

流行的应用(由相关广告点击数定义)在未下载与已下载时并不相同。换句话说,虽然某些移动应用广告经常被点击,但它们不一定是那些最终被下载的应用。

周一的热门应用:让我们绘制应用下载与未下载时,广告点击数的分布,如下方代码所示:

%matplotlib inline
plot_clickcnt_ftr(df_ckFraud_exp1, 'app', '1') 

关于此代码中plot_clickcnt_ftr()函数的定义,请参见本章相关的源代码。第一个条形图显示了应用未下载时的情况,而第二个条形图则反映了应用已下载时的情况:

正如我们之前所见,应用 12、3、9 和 15 是在未下载时最受欢迎的前四个应用。另一方面,应用 19、34、29 和 9 是在广告点击导致下载时最受欢迎的应用。

周一的热门设备:现在让我们绘制设备在应用下载与未下载时的广告点击数分布,如下方代码所示:

%matplotlib inline
plot_clickcnt_ftr(df_ckFraud_exp1, 'device', '1') 

同样的主题依然成立;在点击未导致应用下载和点击导致应用下载时,流行设备的差异,如以下输出所示:

即使在操作系统和渠道方面,这一主题依然存在。因此,值得注意的是,某些设备、操作系统和渠道来源的广告点击可能会提示应用下载。也有可能的是,来自受欢迎渠道、操作系统或设备的点击,针对热门应用的下载转化率较高。受欢迎就意味着点击量大。

创建特征

现在我们已经探索了数据,接下来是创建一些特征。让我们从查看数据中的分类变量开始。

每个类别列的唯一 ID,即appdeviceoschannel,本身并不有用。例如,对于基于树的模型而言,较低的应用 ID 并不优于较高的应用 ID,反之亦然。因此,我们将计算这些分类变量的频率,如下代码所示:

def encode_cat_ftrs(df_ckFraud):
cat_ftrs = ['app','device','os','channel']

for c in cat_ftrs:
df_ckFraud[c+'_freq'] = df_ckFraud[c].map(df_ckFraud.groupby(c).size() / df_ckFraud.shape[0])
return df_ckFraud
  1. 首先,我们创建一个名为cat_ftrs. 的分类变量列表。我们对每一个分类变量都这样做。

  2. 我们通过将来自某一变量的点击数除以数据集中总的点击数来计算频率。

对于这些实验,我们调用encode_cat_ftrs()函数为所有分类变量创建与频率相关的特征,如下所示:

df_ckFraud_exp1 = encode_cat_ftrs(df_ckFraud_exp1)
df_ckFraud_exp2 = encode_cat_ftrs(df_ckFraud_exp2)
df_ckFraud_exp3 = encode_cat_ftrs(df_ckFraud_exp3)
  1. 现在让我们来看一下与时间相关的特征。我们将从click_time列创建各种与时间相关的特征,即dayhourminutesecond。这些特征可能有助于根据星期几和一天中的小时来揭示点击模式。

datetime列中,我们提取dayhourminutesecond,如下所示:

def create_date_ftrs(df_ckFraud, col_name):
"""
create day, hour, minute, second features
"""
df_ckFraud = df_ckFraud.copy()

df_ckFraud['day'] = df_ckFraud[col_name].dt.day.astype('uint8') ## dt is accessor object for date like properties
df_ckFraud['hour'] = df_ckFraud[col_name].dt.hour.astype('uint8')
df_ckFraud['minute'] = df_ckFraud[col_name].dt.minute.astype('uint8')
df_ckFraud['second'] = df_ckFraud[col_name].dt.second.astype('uint8')

return df_ckFraud
  1. 我们使用datetime列的dt访问器对象来获取与时间相关的特征。就像在每个与实验相关的数据集上调用encode_cat_ftrs一样,我们将在每个数据集上调用create_date_ftrs

  2. 最后,让我们创建反映点击来自何时何地的特征。因此,我们将通过以下方式统计点击次数:

    • IP 地址、日期和小时

    • IP 地址、渠道和小时

    • IP 地址、操作系统和小时

    • IP 地址、应用程序和小时

    • IP 地址、设备和小时

关于用于按每种组合统计点击次数的函数count_clicks的详细信息,请参阅与本章相关的源代码。count_clicks在每个与实验相关的数据集上被调用。

现在让我们看看经过特征工程处理后的准备数据集:

如你所见,我们已经拥有所有工程特征:

在前面的截图中,我们有:

    • 每次广告点击的dayhourminutesecond

    • appdevice、操作系统(os)和渠道频率

    • 时间time)、地点osdeviceip地址)以及方式channel)统计的点击次数

  1. 现在让我们看看这些特征之间是如何相互关联的。我们将使用相关矩阵来查看所有属性之间的关系,如下所示的代码所示:
# Correlation
df_ckFraud_exp1.corr()

以下是通过pandas DataFrame 的corr函数生成的相关矩阵的一部分:

如我们所见,应用类型、来自设备和渠道的点击比例,以及某个应用的点击比例是预测应用下载的关键指标。为每个实验绘制热图也表明这些观察结果是有效的。有关更多信息,请参阅与本章相关的源代码。

使用亚马逊的 SageMaker XGBoost 算法对广告点击数据进行分类

为了理解反馈循环的意义,我们将训练一个基于树的模型(XGBoost),以预测广告点击是否会导致应用下载的概率。

对于所有这些实验,我们有一个测试数据集。它包含广告点击数据,以及在星期四晚些时候下载的应用程序——当天最后的 120,000 次点击。让我们开始吧:

  1. 我们将从第三个数据集中选择 5% 的点击数据,该数据集包含了周一、周二、周三和周四的点击数据。第三个数据集按时间排序,因此我们选择了周四生成的最后 120,000 个点击,具体代码如下所示:
# Sort by hour, minute and second --> pick the last 5% of records
test_data = df_ckFraud_exp3.sort_values(['day', 'hour', 'minute', 'second'], ascending=False).head(n=120000)

  1. 我们还需要重新排列所有实验的数据集,使得is_downloaded,我们的目标变量,成为数据集中的第一列。SageMaker XGBoost 算法要求这种格式。

  2. 现在我们需要重新排列测试数据集,如下所示:

# Rearrange test data so that is_downloaded is the first column
test_data = pd.concat([test_data['is_downloaded'], test_data.drop(['is_downloaded'], axis=1)], axis=1)
  1. 对于每个实验,我们将首先创建训练集和验证集。

  2. 我们将把当前实验数据拆分为训练集和验证集,具体代码如下:

train_data, validation_data = np.split(current_experiment.sample(frac=1, random_state=4567), [int(0.7 * len(current_experiment))])
  1. 我们使用 NumPy 的 split 函数来完成此操作。70% 的数据用于训练,30% 的数据用于验证。

  2. 一旦我们准备好训练集、验证集和测试集,我们将它们上传到 S3。有关详细信息,请参阅本章相关的源代码。

现在是准备模型训练的时候了。为了训练 XGBoost 模型,定义了以下超参数(这里只报告了一部分)。详细信息请参阅 AWS 文档(docs.aws.amazon.com/sagemaker/latest/dg/xgboost_hyperparameters.html):

    • max_depth:树的根节点与叶节点之间的最大层数。

    • eta:学习率。

    • gamma:只有当分裂后能显著减少损失函数时,节点才会被分裂。Gamma 指定了进行分裂所需的最小损失减少值。

    • min_child_weight:用于控制树的复杂性和每个子节点所需的最小实例权重总和。如果未达到此阈值,则树的分裂将停止。

    • subsample:每棵树随机采样的观测值比例。

    • colsample_bytree:每棵树随机采样的列的比例。

    • scale_pos_weight:数据集高度不平衡,其中有大量点击(> 90%)没有导致应用下载。为了解决这个问题,使用 scale_pos_weight 超参数来给那些导致应用下载的点击赋予更大的权重。这些点击在数据集中被严重低估。

    • alpha:正则化参数,用于防止过拟合。Alpha 用于实现 L1 正则化,其中叶节点权重的总和是正则化项(目标函数的一部分)。

    • lambda:用于控制 L2 正则化,其中权重的平方和是正则化项的一部分。

  1. 然后,我们定义了一些 XGBoost 算法的超参数,如下所示:
xgb.set_hyperparameters(max_depth=4,
 eta=0.3,
 gamma=0,
 min_child_weight=6, 
 colsample_bylevel = 0.8,
 colsample_bytree = 0.8,
 subsample=0.8,
 silent=0,
 scale_pos_weight=scale_pos_weight,
 objective='binary:logistic',
 num_round=100)

尽管大多数超参数的默认值被接受,但有些在这里被显式定义。例如,min_child_weight被设置为6,而默认值是1。这意味着一个叶节点在进一步拆分之前,应该包含足够数量的实例或数据点。这些值可以根据特定数据进行调整。超参数优化HPO)可以使用 SageMaker 来自动化寻找最优超参数值的过程。

  1. 现在,我们将拟合 XGBoost 算法到实验数据(训练数据和验证数据),如下所示的代码:
xgb.fit({'train': s3_input_train, 'validation': s3_input_validation})

调用 XGBoost 估算器模块(SageMaker Python SDK)的fit()函数进行模型训练。训练和验证数据集的位置作为输入传递给模型训练。

一旦训练完成,训练好的模型将保存到指定的位置(S3 桶中)。我们需要为每个实验重复相同的训练步骤。最终,我们将得到三个训练好的模型。

评估模型性能

在本节中,我们将评估三个训练模型的性能。我们的假设是,第一个模型在星期一和星期二的点击数据上训练,但对于星期四后期的应用下载预测能力较弱,相比之下第二和第三个模型的表现会更好。类似地,第二个模型(基于星期一到星期三的点击数据训练)的表现将不如第三个模型(基于星期一到大部分星期四的点击数据训练)。

我们将首先分析每个模型认为重要的特征,如下所示的代码:

exp_lst = ['exp1', 'exp2', 'exp3']
for exp in exp_lst:
   model_file = os.path.join(sm_output_loc, exp, s3_output_fn)
    plot_ftr_imp(model_file)

上述代码的解释如下:

  1. 首先,我们检索每个实验的训练模型位置。

  2. 然后,我们将位置传递给plot_ftr_imp()函数,以创建一个显示特征重要性的图表。为了绘制特征重要性,函数执行以下操作:

    • .tar文件中提取训练模型

    • 加载 XGBoost 模型

    • 对加载的模型调用plot_importance()函数

下图显示了三个训练模型的特征重要性,从左侧的第一个模型开始:

如我们所见,随着更多数据的加入,大多数关键预测因子的相对重要性保持不变,但它们的重要性顺序发生了变化。要查看映射后的特征,请查看以下图表:

XGBoost 会对输入数据集中的特征进行编号,其中第一列是目标变量,而特征从第二列开始排序。

  1. 现在我们将评估三个实验的性能。让我们按照以下代码将所有三个训练好的模型作为端点进行部署:
model_loc = os.path.join(data_loc, s3_output_fn)
xgb_model = Model(model_data=model_loc, image=container, role=role)
xgb_model.deploy(initial_instance_count=1, instance_type='ml.m4.xlarge')

在上面的代码中,对于每个实验,要将训练后的模型部署为端点,我们将执行以下操作:

    1. 首先,我们将从存储位置(S3 桶)中检索训练好的模型。

    2. 然后,我们将通过传递训练好的模型、XGBoost 算法的 Docker 镜像以及 SageMaker 的执行角色来创建一个 SageMaker 模型。

    3. 最后,我们将调用新创建的 XGBoost 模型对象的deploy方法。我们将传递 EC2 实例的数量以及实例类型给 deploy 函数。

以下截图显示了训练后的模型部署后创建的端点:

  1. 要查看已部署的模型,请导航到 SageMaker 服务并展开推理部分。在该部分下,点击端点(Endpoints)以查看端点名称、创建时间、状态和最后更新时间。

现在是时候预测星期四最后 120,000 次点击的应用下载量了。

我们将为此创建一个RealTimePredictor对象,如下代码所示:

 xgb_predictor = sagemaker.predictor.RealTimePredictor(endpoint, sagemaker_session=sess, serializer=csv_serializer, deserializer=None, content_type='text/csv', accept=None)

RealTimePredictor对象通过传递endpoint的名称、当前的sagemaker会话和content类型来创建。

  1. 收集测试数据的predictions,如下所示:
predictions[exp_lst[ind]] = xgb_predictor.predict(test_data.as_matrix()[:10000, 1:]).decode('utf-8')
  1. 如我们所见,我们通过传递前 10,000 个数据点击,调用RealTimePredictor(SageMaker Python SDK)的预测方法。

现在,我们准备将预测结果与实际的应用下载量进行比较。我们使用sklearn库中的confusion_matrix模块来获取真正例率和假正例率。我们还使用sklearn中的roc_auc_scoreaccuracy_score模块分别计算曲线下面积和准确度。

以下是每个实验的输出:

以下是 AUC,展示了所有实验的性能:

如我们所见,Experiment2的表现优于Experiment1,而Experiment3的表现最好,因为它具有最高的AUC。在Experiment3中,真正例率相对于Experiment1Experiment2更高,假正例率较低。准确度在所有实验中保持不变。由于 AUC 不依赖于测试数据集的类分布,它是衡量模型区分能力的重要指标。另一方面,准确率、召回率和精度等指标可能会随着测试集的变化而变化。

因此,在训练好的模型部署到生产环境后,在模型运行期间寻求反馈非常重要。随着数据模式的变化和新数据的出现,重新训练和调整模型以达到最佳性能变得尤为关键。

总结

在本章中,我们了解了为何监控模型的性能下降至关重要。为说明这一观点,我们使用了一个合成数据集,该数据集捕捉了移动应用下载的广告点击行为。首先,我们探索了数据,以理解应用下载和广告点击之间的关系。然后,我们通过多维度聚合现有点击特征来创建特征。接下来,我们创建了三个不同的数据集,并在这些数据集上进行三次实验,以说明随着新数据的到来,模型性能的恶化问题。随后,我们为每个实验拟合了 XGBoost 模型。最后,我们评估了所有实验的性能,得出结论,表现最佳的模型是那个考虑了最新点击行为的模型。

因此,在机器学习生命周期中实施反馈循环对于保持和提升模型性能,以及充分实现业务目标至关重要,无论是用于欺诈检测还是捕捉用户偏好以供推荐使用。

在下一章,也就是最后一章,我们将总结本书中学习的所有概念,并重点介绍一些来自亚马逊网络服务的机器学习和深度学习服务,值得进一步探索。

进一步阅读

欲了解更多有关模型准确性下降和反馈循环的信息,请参考以下链接:docs.aws.amazon.com/sagemaker/latest/dg/xgboost_hyperparameters.html

第十三章:接下来是什么?

在前面的章节中,正如本书标题所示,我们采用了实践操作的方式,帮助你成为更好的 AI 实践者。通过本书中的实践项目,你掌握了将 AWS AI 能力嵌入应用程序以及使用 AWS ML 平台创建定制 AI 能力的技能。更重要的是,你培养了设计良好的、智能增强的解决方案的直觉,这些解决方案能够帮助解决现实世界中的问题。这些项目不仅让你了解了多种 AI 技术,还展示了 AI 可以应用于的各种问题领域和商业背景。作为 AI 从业者,重要的是要从业务能力的角度看待 AI,而不仅仅是技术。

在本章中,我们将讨论以下主题:

  • 总结第一部分学习的概念

  • 总结第二部分学习的概念

  • 总结第三部分学习的概念

  • 总结第四部分学习的概念

  • 接下来是什么?

总结第一部分学习的概念

在第一部分,我们向你介绍了 AWS 提供的各种 AI 服务,并将它们分为两类:

  • AI 服务

  • ML 平台

我们的建议是,首先在你的解决方案开发中利用 AWS 托管的 AI 服务,如 Rekognition、Translate 和 Comprehend。只有在需要定制 AI 能力时,才应通过 AWS ML 平台,如 SageMaker,来构建它们。这种方法将提高你的市场速度和智能应用的投资回报率。我们还解释了,在 AWS 上开发智能解决方案的真正优势是将 AWS 的 AI 服务与 AWS 云计算生态系统中的其他服务(如 S3、DynamoDB 和 EMR)结合起来。

我们还讨论了 AI 应用程序的架构设计,以及如何通过良好的架构设计实现快速迭代和适应市场变化。我们为第二部分的实践项目制定了架构设计模板,并向你展示了我们在第三部分中构建的定制 AI 能力如何轻松集成到该架构中。这个架构模板可以被采纳并修改,用于构建你下一个基于 AWS AI 服务或定制 AI 能力的智能解决方案。

总结第二部分学习的概念

在第二部分,我们通过以下方式集中讨论了将 AI 能力嵌入应用程序:

  • 我们使用了许多 AWS 托管的 AI 服务,构建了多个端到端的智能解决方案。

  • 我们向你介绍了 AWS SDK,boto3,用于与云服务及其基础设施进行交互。

  • 我们使用了 AWS Chalice 框架来开发和部署无服务器应用程序到 API Gateway 和 AWS Lambda。

  • 我们使用了 HTML、CSS 和 JavaScript 来构建这些解决方案的用户界面。

  • 在此过程中,我们分享了在 AWS 上开发、测试、维护和演化 AI 应用程序的一些技巧和窍门。

在第三章,使用 Amazon Rekognition 和 Translate 检测和翻译文本中,我们构建了一个图像翻译器,既能检测图像中的文本,还能将其翻译成任何语言(在我们的项目中是英语)。这个应用程序可以被前往外国的旅行者或希望与现实世界互动的视障人士使用。

在第四章,使用 Amazon Transcribe 和 Polly 进行语音转文本及反向操作中,我们构建了一个名为“Universal Translator”的简单应用,能够促进说不同语言的人的口头交流。这个应用程序可以被旅行者、学生等使用。

在第五章,使用 Amazon Comprehend 从文本中提取信息中,我们构建了一个联系人组织器,帮助自动化从名片图片中提取联系信息。我们引入了人工干预的概念,以提高端到端解决方案的准确性。这种类型的应用程序有助于减少许多后台任务中的人工工作,让员工能够专注于更具创意的任务。

在第六章,使用 Amazon Lex 构建语音聊天机器人中,我们构建了一个智能助手——联系人助手,能够通过对话界面搜索联系人信息。这个智能助手不仅通过自然语言理解我们,它还记住对话的上下文,使得界面更加流畅。这些类型的智能助手界面改善了我们许多日常任务,例如信息搜索、沟通、提醒等。

总结我们在第三部分中学到的概念。

在第三部分中,我们重点介绍了如何利用 SageMaker 训练和部署机器学习模型,包括内置模型和自定义模型,以解决那些无法通过 AWS AI 服务轻松解决的商业问题。

我们从第七章,使用 Amazon SageMaker 工作开始,学习了如何处理大型数据集、进行训练以及在 SageMaker 中优化超参数。

  • 此外,我们还探讨了 SageMaker 如何使得运行多个实验并部署最佳表现模型进行推理变得无缝。

  • 我们还展示了如何将自己的模型和容器带到 SageMaker,这样就能轻松利用诸如模型训练、部署和大规模推理等功能。

在第八章,创建机器学习推理管道中,我们学习了如何通过 Glue 进行数据预处理,Glue 是一个无服务器 ETL AWS 服务。构建了一个机器学习管道,能够重用数据预处理逻辑进行训练和推理。我们还学习了如何将机器学习管道应用于实时和批量预测。

在第九章,发现文本集合中的主题,我们回顾了各种方法——线性和非线性——以便在文本集合中发现主题。然后,我们深入探讨了如何通过内置的 NTM 算法(变分自编码器)处理主题建模。我们通过恩龙邮件的示例数据集,解释了 SageMaker 中的模型训练、部署和推理步骤。

在第十章,深度学习与自动回归的销售预测,我们探讨了传统时间序列预测方法(如指数平滑法和 ARIMA)与更灵活、更可扩展的方法(如自回归递归网络)之间的差异。然后,我们研究了 SageMaker 中的 DeepAR 算法如何利用多种因素(如假期、促销和失业率)来建模零售销售。

在第十一章,使用 Amazon SageMaker 进行图像分类,我们回顾了卷积神经网络和残差网络的目的。接着,我们介绍了通过迁移学习增量学习的概念。我们还解释了如何通过迁移学习对烘焙物品进行分类,即使只有少量图像数据集。

总结我们在第四部分学到的概念

在第四部分,也就是第十二章,模型精度下降与反馈循环,我们通过广告点击转化数据集定义了模型性能退化的概念。我们阐述了反馈循环的概念,以及为什么它在建模动态广告点击行为时变得如此重要。然后,我们演示了通过反馈循环,模型性能在预测广告点击是否会导致应用下载时的提升。

接下来是什么?

我们已经涵盖了许多人工智能的概念和技术,但通过这本书,我们仅仅触及了这一广阔而深刻领域的表面。掌握了必要的人工智能技能和直觉之后,AI 从业者接下来该做什么呢?以下是我们的一些建议,帮助你更全面地探索这一不断发展的领域。

物理世界中的人工智能

作为 AI 从业者,提升自己的一个方法是拓宽自己在不同人工智能应用领域的经验。一个不断增长的 AI 应用领域旨在将人工智能能力与物理世界中的传感器和执行器相结合。这类物理世界应用的例子包括家庭自动化、智能工厂、自动驾驶汽车和机器人。对于一些 AI 从业者来说,构建物理机器、车辆和机器人可能会让人感到畏惧。幸运的是,AWS 提供了多个产品,可以让你更轻松地开始这一类 AI 应用。

AWS DeepLens

AWS DeepLens 是一款物理设备,内置摄像头、计算、存储和互联网连接,并将其封装成一个小型设备。结合其他 AWS AI 服务和工具,当你想要获取深度学习应用的实践经验时,DeepLens 成为一个强大的平台。请看下面的屏幕截图,展示了 AWS DeepLens:

让我们来谈谈 DeepLens 的一些显著特点:

  • DeepLens 可以拍摄高清HD)图像和视频,并且具备足够的处理能力来实时处理高清晰度视频。

  • AI 从业者可以通过 AWS AI 服务快速启动 DeepLens 项目。例如,它与 Amazon Rekognition 集成,能够分析摄像头拍摄的图像和视频。

  • DeepLens 完全可以通过 AWS Lambda 编程,调用广泛的功能和连接到互联网的执行器。

  • DeepLens 还支持使用 Amazon SageMaker 训练的定制机器学习模型。

  • AI 从业者可以选择包括 TensorFlow 和 Caffe 在内的广泛深度学习框架来训练机器学习模型,并在 DeepLens 的车载推理引擎上运行它们。

  • 这些定制的机器学习模型可以通过几次点击或 API 调用轻松部署到 DeepLens 上。

通过阅读本书,你已经熟悉了我们刚才提到的许多工具,并且已经掌握了开始使用 AWS DeepLens 所需的许多技能。凭借这个强大的平台,AI 从业者可以构建一系列广泛的应用。几个示例应用包括家庭安全、鸟类观察、交通监控、送货通知、家庭自动化等等。结合其他传感器和执行器,可能性是无限的。

AWS DeepRacer

AWS DeepRacer 是一款 1/18^(th) 比例的赛车,集成了摄像头、加速度计和陀螺仪;它还配备了计算、存储和互联网连接功能。DeepRacer 旨在帮助 AI 从业者通过自动驾驶赛车获取强化学习的实践经验。强化学习是机器学习(ML)的一个分支,旨在创建智能体,通过优化奖励函数来学习,而不是通过示例学习(监督学习)或数据的固有结构(无监督学习)。这种 AI 技术已被用于训练智能体进行跑步、驾驶和玩游戏。例如,Google 的 AlphaGo 程序使用这种机器学习技术,并击败了世界顶级围棋选手。以下是 AWS DeepRacer 的展示:

DeepRacer 将以下 AI 能力带入现实世界:

  • 通过使用摄像头和其他车载传感器,AI 从业者可以开发强化学习模型来控制 DeepRacer 的油门和转向。

  • DeepRacer 配备了一个 3D 赛车模拟器,便于开发和测试其 AI 赛车能力。

  • 与 DeepLens 类似,DeepRacer 也与 AWS AI 服务和云基础设施集成。

  • 你可以使用 Amazon SageMaker 来训练强化学习模型,并轻松将它们部署到你的赛车上。

  • 甚至还有一个 DeepRacer 联赛,用于测试你为赛车开发的任何 AI 赛车能力,并有机会赢得奖品和荣耀。

基于此平台构建的应用程序也不必仅限于赛车。通过安装在轮子上的相机,有许多可选的应用场景,例如家庭监控、宠物训练和物品配送。我们敢打赌,DeepDrone 曾经在 AWS 中被提出过。

物联网与 AWS IoT Greengrass

为了让 AI 应用在物理世界中正常运作,需要在边缘端具备 AI 能力。

AWS IoT Greengrass 让连接的设备能够与其他设备和云应用无缝且安全地互动。Greengrass 为 AI 应用带来了许多好处,包括以下几点:

  • 它在你希望基于本地事件和数据做出反应时,带来了更快的响应时间,而无需访问云端。

  • 它通过减少边缘与云之间数据传输的带宽需求,提升了运行 IoT 的成本效益。

  • 通过在本地处理和匿名化敏感信息,简化了某些行业(如医疗保健)中的数据安全和隐私保护。

AWS IoT Greengrass 还可以将 AI 能力扩展到边缘设备,从而创建智能边缘。以下架构图展示了边缘设备如何在本地运行机器学习推理,然后通过 Greengrass Core 连接到 AWS IoT Core,将消息或推理发送到云端进行进一步分析:

在这种架构中,我们可以看到以下内容:

  • 在 Amazon SageMaker 中训练的模型会保存在 S3 存储桶中。

  • 在边缘生成的数据通过本地 Lambda 函数由这些训练过的模型进行评分。

  • AWS Greengrass 的核心控制了边缘和云之间的通信,包括安全性、消息传递和离线计算操作。

  • AWS IoT Core,另一方面,协调与其他 AWS 服务的连接,即持久存储或分析。

通过利用边缘智能架构,可以解决多个商业问题。以自动驾驶车辆为例,需要一个智能边缘来引导汽车绕过本地环境,而不受网络延迟的影响。如果你是制造业企业,通过在边缘运行机器学习推理,可以在本地预测机械设备的使用寿命,并采取即时措施以提高安全性。在医疗健康应用中,敏感的医疗信息可以在本地使用,以低延迟进行智能诊断,而不需要将患者的隐私风险暴露在云端。

你自己领域中的人工智能

作为 AI 从业者的另一种成长方式是深入探讨特定领域或业务领域。一个好的方法是将 AI 应用于你有专业知识或兴趣的领域。你比任何人都更清楚该选择哪个领域。

在这个选定的领域,你会发现一组特定的商业问题,然后发展相关的 AI 技能,以解决这些问题。你所在领域的问题可能需要计算机视觉、自然语言处理、语音识别、知识推理,或者这些技术的组合。作为 AI 从业者,你将进一步发展这些 AI 技术中的更专业技能。

在这个选定的领域,你可能已经知道一些你希望解决的现有问题,或者你可能需要发现新的问题来解决。发现并定义这些问题是获得相较于其他 AI 通才的竞争优势的关键。然而,开始这条道路时,我们建议你从与该领域相关的小型实践项目开始。就像本书的建议一样,你应该通过实际操作来发展你的直觉,即使这只是复制你所在领域的现有解决方案。随着时间的推移,你将逐渐积累解决相关问题的更好见解。

总结

本书涵盖了 AWS 中各种 AI 基础知识,重点介绍了 AI 应用开发、预定义的 AI API、模型构建、训练、部署、管理和通过 ML 管道进行实验。我们建议了两种方法,帮助你继续在 AWS 上作为 AI 从业者发展,即广度发展或深度发展。希望你在阅读(并完成)本书的过程中获得了乐趣,并且已经准备好通过 AWS 与人工智能相关的服务解决具有挑战性的商业问题。

posted @ 2025-07-17 15:21  绝不原创的飞龙  阅读(156)  评论(0)    收藏  举报