TensorFlow-Lite-ML-Kit-和-Flutter-移动深度学习-全-
TensorFlow Lite,ML Kit 和 Flutter 移动深度学习(全)
原文:Mobile Deep Learning with TensorFlow Lite, ML Kit and Flutter
零、前言
深度学习正迅速成为业界最受欢迎的话题。 本书以工业和应用为重点的方法介绍了流行的深度学习概念及其用例。 您将涵盖一系列项目,包括移动视觉,面部识别,智能 AI 助手和增强现实等任务。
在八个项目的帮助下,您将学习将深度学习过程集成到 iOS 和 Android 移动平台中。 这将帮助您有效地将深度学习功能转换为强大的移动应用。 这本书让您动手选择正确的深度学习架构并优化移动深度学习模型,同时遵循面向应用的方法在本机移动应用上进行深度学习。 稍后,我们将介绍各种经过预先训练和定制的基于深度学习模型的 API,例如通过 Google Firebase 的 ML Kit。 进一步,这本书将带您通过使用 TensorFlow Lite 借助 Python 创建自定义深度学习模型的示例。 从准备模型到部署,每个项目都会演示如何将深度学习库集成到您的移动应用中。
到本书结尾,您将具备在 iOS 和 Android 上构建和部署高级深度学习移动应用的技能。
这本书是给谁的
本书适合那些希望利用深度学习功能来提供更好的用户体验或希望将强大的智能功能引入其应用的应用开发人员。 同时,它也适合希望将其深度学习模型部署到跨平台移动应用的深度学习从业人员。
为了充分利用本书,需要对移动应用的工作方式有基本的了解,并对 Python 有很好的了解。 建议对中学数学有较高的了解。
本书涵盖的内容
“第 1 章”,“移动深度学习简介”讨论了移动设备上深度学习的新兴重要性。 它涵盖了机器学习和深度学习的基本概念,还向您介绍了可用于将深度学习与 Android 和 iOS 集成的各种选项。 本章还介绍了使用本机和基于云的学习方法进行深度学习项目的实现。
“第 2 章”,“移动视觉–使用设备上模型的人脸检测”向您介绍 ML Kit 中提供的移动视觉和移动视觉模型。 您将学习如何在 Keras 中创建人脸检测模型,并了解如何将其转换为可用于移动设备的模型。 该模型使用 Google Cloud Vision API 进行面部检测。
“第 3 章”,“Google 上的聊天机器人”使用操作,可以通过扩展 Google Assistant 的功能来创建自己的自定义聊天机器人。 该项目很好地理解了如何使用 Google Actions 和 Dialogflow 的 API 构建使用引人入胜的基于语音和文本的会话界面的产品。
“第 4 章”,“识别植物物种”提供了有关如何构建自定义 Tensorflow Lite 模型的深入讨论,该模型能够使用图像处理执行视觉识别任务。 开发的模型在移动设备上运行,主要用于识别不同的植物物种。 该模型使用深层卷积神经网络(CNN)进行视觉识别。
“第 5 章”,“从摄像机供稿生成实时字幕”提出了一种使用摄像机供稿实时生成自然语言字幕的方法。 在这个项目中,您将创建自己的相机应用,该应用使用由图像标题生成器生成的自定义预训练模型。 该模型使用 CNN 和长短期记忆(LSTM)生成字幕。
“第 6 章”,“构建人工智能认证系统”为您提供了验证用户身份并创建识别稀有和可疑用户交互的机制的方法。 在识别罕见事件(即与大多数数据不同的事件)后,不允许用户登录,并收到一条消息,指出检测到恶意用户。 当相关应用包含高度安全的数据(例如机密电子邮件或虚拟银行保险库)时,这可能会很有用。 该项目在网络请求标头上使用基于 LSTM 的模型来对异常登录进行分类。
“第 7 章”,“语音/多媒体处理-使用 AI 生成音乐”探索了使用 AI 生成音乐的方法。 将向您介绍多媒体处理。 本章演示了样本训练后用于生成音乐的方法。 该项目使用循环神经网络和基于 LSTM 的模型来生成 MIDI 音乐文件。
“第 8 章”,“基于增强型神经网络的国际象棋引擎”讨论了 Google 的 DeepMind,以及如何将增强型神经网络用于 Android 平台上的机器辅助游戏。 首先,您将创建一个 Connect4 引擎,以直观地构建自学习,玩游戏的 AI。 然后,您将开发基于深度强化学习的国际象棋引擎,并将其作为 API 托管在 Google Cloud Platform(GCP)上。 然后,您将使用国际象棋引擎的 API 在移动设备上执行游戏。
“第 9 章”,“构建图像超分辨率应用”提出了一种借助深度学习生成超分辨率图像的方法。 您将学习在 Android/iOS 上处理图像的第三种方法,以及如何创建可在 DigitalOcean 上托管并包含在 Android/iOS 应用中的 TensorFlow 模型。 由于此模型的资源非常密集,因此将指导您如何在云上托管该模型。 该项目使用生成对抗网络。
“第 10 章”,“未来之路”简要介绍了当今移动应用中最受欢迎的深度学习应用,当前趋势以及在该领域中有望发展的技术。
充分利用这本书
您需要在本地系统上有效的 Python 3.5+ 安装。 将 Python 作为 Anaconda 发行版的一部分进行安装是一个好主意。 要构建移动应用,您需要有效安装 Flutter 2.0+。 此外,整本书中通常会同时需要 TensorFlow 1.x 和 2.x。 因此,必须具有两个 Anaconda 环境:
| 书中涵盖的软件/硬件 | 操作系统要求 |
|---|---|
| Jupyter 笔记本 | 带有更新的网络浏览器(最好是 Google Chrome/Mozilla Firefox/Apple Safari)的任何操作系统。最低 RAM 要求:4 GB; 但是,建议使用 8 GB。 |
| Microsoft Visual Studio 代码 | 任何具有 4 GB 以上 RAM 的操作系统; 但是,建议使用 8 GB。 |
| 具有开发人员访问权限的智能手机 | 具有至少 2 GB RAM 的 Android/iOS; 但是,建议使用 3 GB。 |
您可以免费获得本书中所需的所有软件工具。 但是,您必须将您的信用卡/借记卡详细信息添加到您的帐户中才能激活 GCP 或 DigitalOcean 平台。
如果您使用的是本书的数字版本,建议您自己键入代码或通过 GitHub 存储库(下一节中提供的链接)访问代码。 这样做将帮助您避免与代码的复制和粘贴相关的任何潜在错误。
Flutter 移动应用上的深度学习尚处于开发的早期阶段。 阅读本书后,如果您撰写博客并制作有关如何在移动应用上执行机器学习或深度学习的视频,那么您将为不断增长的应用开发人员和机器学习从业者生态系统做出贡献。
下载示例代码文件
您可以从 www.packt.com 的帐户中下载本书的示例代码文件。 如果您在其他地方购买了此书,则可以访问 www.packtpub.com/support 并注册以将文件直接通过电子邮件发送给您。
您可以按照以下步骤下载代码文件:
- 登录或注册 www.packt.com 。
- 选择“支持”选项卡。
- 单击“代码下载”。
- 在搜索框中输入书籍的名称,然后按照屏幕上的说明进行操作。
下载文件后,请确保使用以下最新版本解压缩或解压缩文件夹:
- Windows 的 WinRAR/7-Zip
- Mac 版 Zipeg/iZip/UnRarX
- 适用于 Linux 的 7-Zip/PeaZip
本书的代码包也托管在 GitHub 上。 如果代码有更新,它将在现有的 GitHub 存储库中进行更新。
我们还从这里提供了丰富的书籍和视频目录中的其他代码包。 去看一下!
下载彩色图像
我们还提供了 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。 您可以从这里下载。
使用约定
本书中使用了许多文本约定。
CodeInText:指示文本,数据库表名称,文件夹名称,文件名,文件扩展名,路径名,虚拟 URL,用户输入和 Twitter 句柄中的代码字。 这是一个示例:“注意,这里的dialogflow变量是actions-on-google模块的对象。”
代码块设置如下:
dependencies:
flutter:
sdk: flutter
firebase_ml_vision: ^0.9.2+1
image_picker: ^0.6.1+4
粗体:表示新术语,重要单词或您在屏幕上看到的单词。 例如,菜单或对话框中的单词会出现在这样的文本中。 这是一个示例:“要进入控制台,请单击开始构建或转到操作控制台按钮。”
警告或重要提示如下所示。
提示和技巧如下所示。
一、移动深度学习简介
在本章中,我们将探索移动设备上深度学习的新兴途径。 我们将简要讨论机器学习和深度学习的基本概念,并将介绍可用于将深度学习与 Android 和 iOS 集成的各种选项。 本章还介绍了使用本机和基于云的学习方法进行深度学习项目的实现。
在本章中,我们将介绍以下主题:
- 基于人工智能(AI)的移动设备的发展
- 了解机器学习和深度学习
- 介绍一些常见的深度学习架构
- 强化学习和自然语言处理(NLP)简介
- 在 Android 和 iOS 上集成 AI 的方法
人工智能移动设备的增长
AI 变得比以前更加移动,因为更小的设备具有更多的计算能力。 移动设备原本仅用于拨打电话和发送短信,但随着 AI 的引入,如今已转变为智能手机。 这些设备现在能够利用 AI 不断增强的功能来学习用户的行为和喜好,增强照片,进行全面的对话等等。 人工智能驱动的智能手机的功能预计只会一天一天地增长。 根据 Gartner 的数据,到 2022 年,80% 的智能手机将支持 AI。
支持 AI 的硬件变化
为了应对 AI 的高计算能力,对手机的硬件支持进行了定期更改和增强,以使他们具有思考和行动的能力。 移动制造公司一直在不断升级移动设备上的硬件支持,以提供无缝和个性化的用户体验。
华为已经发布了麒麟 970 SoC,该芯片可以使用专门的神经网络处理单元来实现设备上的 AI 体验。 苹果设备装有称为神经引擎的 AI 芯片,该芯片是 A11 Bionic 芯片的一部分。 它专用于机器学习和深度学习任务,例如面部和语音识别,记录动画和拍摄照片时的对象检测。 高通公司和联发科已经发布了他们自己的芯片,这些芯片支持设备上的 AI 解决方案。 三星宣布的 Exynos 9810 是一种基于神经网络的芯片,例如高通的 Snapdragon 845。 2018 年的三星设备 Galaxy S9 和 S9+ 包括这些基于其销售国家/地区的芯片。 借助 Galaxy S9,该公司非常明显地表明它将集成 AI 以改善设备相机的功能和实时文本翻译。 最新的三星 Galaxy S10 系列由 Qualcomm Snapdragon 855 提供支持,以支持设备上的 AI 计算。
Google Translate Word Lens 和 Bixby 个人助理已用于开发该功能。 有了这些技术,该设备即可翻译多达 54 种语言。 这些电话足够智能,可以在 f/2.4 和 f/1.5 的传感器之间进行选择,非常适合在弱光条件下拍摄照片。 Google Pixel 2 利用其机器学习的强大功能,通过其协处理器 Pixel Visual Core 集成了八个图像处理单元。
为什么移动设备需要使用 AI 芯片?
集成 AI 芯片不仅有助于提高效率和计算能力,而且还保留了用户的数据和隐私。 在移动设备上包含 AI 芯片的优势可以列举如下:
- 性能:当前日期中移动设备的 CPU 不适合机器学习的需求。 尝试在这些设备上部署机器学习模型通常会导致服务速度缓慢和电池消耗更快,从而导致不良的用户体验。 这是因为 CPU 缺乏执行 AI 计算所需的大量小型计算的效率。 AI 芯片类似于负责处理设备上图形的图形处理器(GPU)芯片,提供了单独的空间来执行与机器学习和深度学习过程专门相关的计算 。 这使 CPU 可以将时间集中在其他重要任务上。 通过合并专用的 AI 硬件,设备的性能和电池寿命得到了改善。
- 用户隐私:硬件还确保提高用户隐私和安全性。 在传统的移动设备中,数据分析和机器学习过程将需要将用户数据的大块发送到云中,从而威胁到用户数据隐私和移动设备的安全性。 借助设备上的 AI 芯片,可以在设备本身上离线执行所有必需的分析和计算。 在移动设备中包含专用硬件的这种方式极大地降低了用户数据被黑或泄漏的风险。
- 效率:在现实世界中,通过集成 AI 芯片,诸如图像识别和处理之类的任务可能会快得多。 华为的神经网络处理单元就是一个很好的例子。 它能够以每秒 2,000 张图片的效率识别图像。 该公司声称这比标准 CPU 所花费的时间快 20 倍。 当使用 16 位浮点数时,它可以执行 1.92 teraflops 或每秒 1 万亿次浮点运算。 苹果公司的神经引擎每秒可处理约 6000 亿次操作。
- 经济性:设备上的 AI 芯片减少了将数据发送到云中的需求。 此功能使用户可以脱机访问服务并保存数据。 因此,可以避免使用应用的人为服务器付费。 这对用户和开发人员都是有利的。
让我们简要概述一下移动设备上的 AI 如何影响我们与智能手机交互的方式。
在移动设备上使用 AI 改善用户体验
人工智能的使用极大地增强了移动设备上的用户体验。 可以将其大致分为以下几类。
个性化
个性化主要是指修改服务或产品以适合特定个人的偏好,有时与个人集群有关。 在移动设备上,使用 AI 通过使设备和应用适应用户的习惯及其独特的个人资料(而不是面向通用个人资料的应用)来帮助改善用户体验。 移动设备上的 AI 算法利用可用的特定于用户的数据(例如位置,购买历史记录和行为模式)来预测和个性化当前和将来的交互,例如在一天的特定时间段内用户的首选活动或音乐。
例如,AI 收集有关用户购买历史的数据,并将其与从在线流量,移动设备,电子设备中嵌入的传感器和车辆中获得的其他数据进行编译。 然后,这些经过编译的数据将用于分析用户的行为,并允许品牌采取必要的措施来提高用户参与率。 因此,用户可以利用基于 AI 的应用的好处来获得个性化的结果,这将减少他们的滚动时间,并让他们探索更多的产品和服务。
最好的例子是通过购物平台(如沃尔玛,亚马逊)或媒体平台(如 YouTube 或 Netflix)运行的推荐系统。
2011 年,亚马逊报告的销售额增长了 29%,从 99 亿美元增至 128.3 亿美元。 凭借最成功的推荐率,亚马逊 35% 的销售额来自遵循其产品推荐引擎生成的推荐的客户。
虚拟助手
虚拟助手是一种可以理解语音命令并为用户完成任务的应用。 他们能够使用自然语言理解(NLU)来解释人的语音,并且通常会通过合成语音进行响应。 您可能会使用虚拟助手来完成真正的私人助手为您执行的几乎所有任务,即代表您打电话给他人,记下您指定的笔记,打开或关闭家中的电灯/ 在家庭自动化的帮助下办公,为您播放音乐,甚至只是与您讨论您想谈论的任何话题! 虚拟助手可能能够接受文本,音频或视觉手势形式的命令。 虚拟助手会随着时间的推移适应用户习惯并变得更聪明。
利用 NLP 的功能,虚拟助手可以识别口头语言的命令,并从您上传到助手或保存在他们可以访问的任何在线相册中的图像中识别人和宠物。
目前市场上最受欢迎的虚拟助手是亚马逊上运行的 Amazon Alexa,Google 助手,iPhone 的 Siri,微软的 Cortana 和在三星设备上运行的 Bixby。 一些虚拟助手是被动监听器,仅当他们收到特定的唤醒命令时才响应。 例如,可以使用“嘿谷歌”或“确定谷歌”激活 Google 助手,然后使用“关闭卧室灯”命令关闭谷歌助手,或者使用来从联系人列表中呼叫某人。 “打给”。 在 Google IO '18 中,Google 推出了双工电话预订 AI,这表明 Google Assistant 不仅能够拨打电话,而且还可以进行对话并有可能独自在美发沙龙中进行预订 。
虚拟助手的使用呈指数增长,预计到 2021 年将达到 18 亿用户。54% 的用户同意虚拟助手有助于简化日常任务,而 31% 的人已经在日常生活中使用助手。 此外,有 64% 的用户将虚拟助手用于多个目的。
面部识别
足以识别或验证面部或从数字图像和视频中识别面部表情的技术被称为面部识别。 该系统通常通过将给定图像中最常见和最显着的面部特征与数据库中存储的面部进行比较来工作。 面部识别还具有根据个人的面部纹理和形状来理解图案和变化的能力,以唯一地识别一个人,通常被称为基于 AI 的生物识别应用。
最初,面部识别是计算机应用的一种形式。 但是,近来它在移动平台上被广泛使用。 面部识别以及诸如指纹和虹膜识别之类的生物识别技术在移动设备的安全系统中得到了普遍的应用。 通常,人脸识别过程分两个步骤进行:特征提取和选择是第一步,对象分类是第二步。 后来的发展引入了其他几种方法,例如使用面部识别算法,三维识别,皮肤纹理分析和热像仪。
Apple 的 iPhone X 中引入的 Face ID 是生物识别认证的后继产品,是几种基于 Android 的智能手机中基于指纹的认证系统的继任者。 人脸 ID 的人脸识别传感器由两部分组成:Romeo模块和Juliet模块。 Romeo 模块负责将 30,000 多个红外点投射到用户的脸上。 该模块的对应部分Juliet 模块,读取用户面部上的点形成的图案。 然后将图案发送到设备 CPU 中的设备上Secure Enclave模块,以确认面部是否与所有者匹配。 苹果无法直接访问这些面部图案。 当用户闭上眼睛时,系统不允许授权工作,这是增加的安全性。
该技术从用户外观的变化中吸取教训,并可以用于化妆,胡须,眼镜,太阳镜和帽子。 它也可以在黑暗中工作。 泛光照明器是专用的红外闪光灯,可将不可见的红外光投射到用户的脸上,以正确读取面部表情,并帮助系统在弱光条件下甚至在完全黑暗的情况下运行。 与 iPhone 相反,三星设备主要依赖于二维面部识别,并带有虹膜扫描仪,该虹膜扫描仪可在 Galaxy Note 8 中用作生物识别。印度领先的高级智能手机销售商 OnePlus 也仅依赖于二维面部识别。
到 2023 年,利用面部识别的软件全球市场预计将从 2017 年的 38.5 亿美元增长到 97.8 亿美元。亚太地区是增长最快的地区,其市场份额约为 16%。
人工智能相机
相机中的 AI 集成使他们能够识别,理解和增强场景和照片。 AI 摄像机能够理解和控制摄像机的各种参数。 这些相机基于称为计算摄影的数字图像处理技术的原理工作。 它使用算法而不是光学过程来寻求使用机器视觉来识别和改善图片内容。 这些相机使用深度学习模型,这些模型在包含数百万个样本的巨大图像数据集上进行训练,可自动识别场景,光线的可用性以及所捕获场景的角度。
当相机指向正确的方向时,相机的 AI 算法将接管更改相机的设置,以产生最佳质量的图像。 在幕后,实现 AI 摄影的系统并不简单。 所使用的模型经过高度优化,可在检测到几乎实时捕获的场景特征时产生正确的相机设置。 它们还可以添加动态曝光,颜色调整以及图像的最佳效果。 有时,图像可能会由 AI 模型自动进行后处理,而不是在单击照片时进行处理,以减少设备的计算开销。
如今,移动设备通常配备双镜头相机。 这些相机使用两个镜头在照片上添加散景效果(日语中为“模糊”)。 背景虚化效果为主要拍摄对象周围的背景增添了模糊感,使其在美学上令人愉悦。 基于 AI 的算法有助于模拟识别对象的效果,并使剩余部分模糊,从而产生人像效果。
Google Pixel 3 相机可以在 Top Shot 和 Photobooth 两种拍摄模式下工作。 相机最初在用户尝试捕获之前和之后捕获几帧。 然后,设备中可用的 AI 模型就可以选择最佳帧。 通过为相机的图像识别系统提供大量训练,使之成为可能,然后,该系统便能够选择看上去最好的图片,几乎就像人在捡照片一样。 照相棚模式允许用户简单地将设备对准动作场景,并在相机预测为图像完美的时刻自动拍摄图像。
预测文本
预测文本是一种输入技术,通常在消息传递应用中使用,根据输入的单词和短语向用户建议单词。 每次按键后的预测都是唯一的,而不是以相同的恒定顺序产生重复的字母序列。 预测性文本可以通过一次按键即可输入整个单词,从而可以大大加快输入过程。 这使得输入书写任务(例如键入文本消息,编写电子邮件或使用较少的设备键来使地址簿中的条目高效输入)成为可能。 预测文本系统将用户的首选界面样式与他们学习预测文本软件的能力水平联系在一起。 通过分析和适应用户的语言,系统最终变得更智能。 T9 词典是此类文本预测器的一个很好的例子。 它分析使用的单词的频率,并生成多个最可能的单词。 它也能够考虑单词的组合。
快速类型是苹果公司在其 iOS 8 版本中宣布的一种预想性文本功能。 它使用机器学习和 NLP,这使软件可以根据用户的打字习惯来构建自定义词典。 这些词典随后用于预测。 这些预测系统还取决于对话的上下文,并且能够区分正式和非正式语言。 此外,它支持全球多种语言,包括美国英语,英国英语,加拿大英语,澳大利亚英语,法语,德语,意大利语,巴西葡萄牙语,西班牙语和泰语。
Google 还推出了一项新功能,该功能将帮助用户比以前更快地撰写和发送电子邮件。 名为 Smart Compose 的功能可以理解键入的文本,以便 AI 可以建议单词和短语来完成句子。 智能撰写功能可通过纠正拼写错误和语法错误以及建议用户最常用的单词,帮助用户节省编写电子邮件的时间。 智能回复是另一个功能,类似于 LinkedIn 消息中的回复建议,该建议根据用户接收到的电子邮件的上下文,建议单击一次即可发送的回复。 例如,如果用户收到一封祝贺他们接受的应用的电子邮件,则“智能回复”功能可能会提供以下选项来进行回复:“谢谢!”,“谢谢让我知道”和“谢谢您” 接受我的申请。” 然后,用户可以单击首选答复并发送快速答复。
在 1940 年代,林语堂创建了一种打字机,其中的启动键会根据所选字符提示字符。
使用 AI 的最受欢迎的移动应用
近年来,我们看到将 AI 集成到其功能中以增加用户参与度和定制服务交付的应用数量激增。 在本节中,我们将简要讨论移动应用领域中一些最大的参与者如何利用 AI 的优势来促进其业务发展。
Netflix
Netflix 是移动应用中机器学习的最佳和最受欢迎的例子。 该应用使用线性回归,逻辑回归和其他机器学习算法为用户提供完美的个性化推荐体验。 按演员,体裁,时长,评论,年等分类的内容用于训练机器学习算法。 所有这些机器学习算法都会学习并适应用户的动作,选择和偏好。 例如,约翰看了一个新电视连续剧的第一集,但并不十分喜欢,所以他不会看后续的几集。 Netflix 涉及的推荐系统了解他不喜欢这种电视节目,因此将其从推荐中删除。 同样,如果约翰从推荐列表中选择了第八条推荐,或者在看完电影预告片后写了一篇不好的评论,则所涉及的算法会尝试适应其行为和偏好,以提供极为个性化的内容。
Seeing AI
微软开发的 Seeing AI 是一款智能相机应用,它使用计算机视觉来听觉上帮助盲人和视障人士了解周围的环境。 它具有一些功能,例如为用户读取简短的文本和文档,提供有关人的描述,使用设备的相机识别其他应用中的货币,颜色,笔迹,光线甚至图像。 为了使该应用具有先进的实时响应能力,开发人员采用了使服务器与 Microsoft Cognitive Services 通信的想法。 OCR,条形码扫描仪,面部识别和场景识别是该应用整合在一起的最强大的技术,可为用户提供一系列出色的功能。
Allo
Allo 是 Google 开发的以 AI 为中心的消息传递应用。 自 2019 年 3 月起,Allo 已停产。 但是,这是 Google 推动 AI 应用发展的重要里程碑。 该应用允许用户通过语音在 Android 手机上执行操作。 它使用了智能回复功能,该功能可以通过分析对话的上下文来建议单词和短语。 该应用不仅限于文本。 实际上,它同样能够分析对话期间共享的图像并提出回复建议。 强大的图像识别算法使之成为可能。 后来,此智能回复功能也在 Google 收件箱中实现,现在已在 Gmail 应用中提供。
英语语音助手
英语语音助手(ELSA)被评为全球基于 AI 的应用排名前五的应用,它是世界上最智能的 AI 语音导师。 该移动应用可以帮助人们提高发音。 它被设计为冒险游戏,按级别进行区分。 每个级别呈现一组供用户发音的单词,将其作为输入。 仔细检查用户的响应以指出他们的错误并帮助他们改进。 当应用检测到错误的发音时,它会通过指示用户嘴唇和舌头的正确运动来教给用户正确的发音,以便正确地说出单词。
Socratic
Socratic 是一个导师应用,它允许用户拍摄数学问题,并给出答案以解释其背后的理论,并详细说明应如何解决。 该应用不仅限于数学。 当前,它可以为 23 个不同主题的用户提供帮助,包括英语,物理,化学,历史,心理学和微积分。 该应用使用 AI 的功能来分析所需的信息,并通过分步解决方案返回视频。 该应用的算法与计算机视觉技术相结合,能够读取图像中的问题。 此外,它使用针对数百万个示例问题训练的机器学习分类器,有助于准确预测解决问题所涉及的概念。
现在,让我们更深入地研究机器学习和深度学习。
了解机器学习和深度学习
在能够研究包含与 AI 领域相关的技术和算法的解决方案之前,了解一些机器学习和深度学习的关键概念很重要。 当我们谈论 AI 的当前状态时,我们通常指的是能够搅动大量数据以找到模式并根据这些模式进行预测的系统。
尽管“人工智能”一词可能会带来说话的类人机器人或自动驾驶到外行的图像,但对于研究该领域的人来说,它们可能是互连的计算模块图和网络的形式。
在下一节中,我们将首先介绍机器学习。
了解机器学习
1959 年,亚瑟·塞缪尔(Arthur Samuel)创造了术语机器学习。 在他对机器学习的定义的轻描淡写中,使机器能够从过去的经验中学习并在提供未知输入的情况下基于它们进行预测的计算机科学领域称为机器学习。
机器学习的更精确定义可以描述如下:
- 通过学习有关任务
T的经验E来提高其在任何任务T上的性能P的计算机程序,称为机器学习程序。 - 使用前面的定义,在目前类似的情况下,
T是与预测有关的任务,而P是计算机程序在执行任务T时所达到的准确率度量,基于程序能够学习的内容,该学习称为E。 随着E的增加,计算机程序会做出更好的预测,这意味着P得到改善,因为该程序以更高的精度执行任务T。 - 在现实世界中,您可能会遇到一位老师在教学生执行特定任务,然后通过让学生参加考试来评估学生执行任务的技能。 学生接受的训练越多,他们执行任务的能力就越好,并且他们的考试成绩也就越高。
在下一节中,让我们尝试了解深度学习。
了解深度学习
我们已经很长时间听到了学习一词,并且在某些情况下通常意味着获得执行任务的经验。 但是,以学习为前缀的深度是什么意思?
在计算机科学中,深度学习是指一种机器学习模型,其中涉及多个学习层。 这意味着计算机程序由多种算法组成,数据通过这些算法逐一传递,最终产生所需的输出。
深度学习系统是使用神经网络的概念创建的。 神经网络是连接在一起的神经元层的组成,因此数据从一层神经元传递到另一层,直到到达最终层或输出层。 神经元的每一层以与最初将数据作为输入提供给神经网络的形式相同或不同的形式获取数据输入。
考虑以下神经网络图:

前面的屏幕截图中介绍了一些术语。 让我们简要地讨论其中的每一个。
输入层
保留输入值的层称为输入层。 有人认为该层实际上不是一个层,而仅仅是一个保存数据的变量,因此是数据本身,而不是一个层。 但是,保持该层的矩阵的尺寸很重要,必须正确定义,以使神经网络与第一隐藏层进行通信。 因此,从概念上讲,它是保存数据的层。
隐藏层
输入层和输出层之间的任何中间层都称为隐藏层。 生产环境中使用的典型神经网络可能包含数百个输入层。 通常,隐藏层比输入或输出层包含更多的神经元。 但是,在某些特殊情况下,这可能不成立。 通常会在隐藏层中包含大量神经元,以处理输入以外的维度中的数据。 这使程序可以以用户将其馈入网络时所呈现的格式,以数据的形式获得洞察力或模式,这些数据可能不可见。
神经网络的复杂性直接取决于网络中神经元的层数。 尽管神经网络可以通过添加更多层来发现数据中更深的模式,但它也增加了网络的计算成本。 网络也有可能进入称为过拟合的错误状态。 相反,如果网络太简单,或者说网络不够深,它将到达另一个错误状态,称为欠拟合。
您可以在这个页面上了解有关过拟合和不足的更多信息。
输出层
产生并存储所需输出的最后一层称为输出层。 该层通常对应于所需输出类别的数量,或具有一个包含所需回归输出的单个神经元。
激活函数
神经网络中的每一层都接受称为激活函数的函数。 此函数的作用是将神经元内部包含的数据保持在正常范围内,否则该范围会变得太大或太小,并导致与计算机中大十进制系数或大数的处理有关的计算错误。 另外,激活函数使神经网络能够处理数据中模式的非线性。
一些常见的深度学习架构简介
在对关键术语进行简短修订之后,我们现在准备更深入地研究深度学习领域。 在本节中,我们将学习一些著名的深度学习算法及其工作原理。
卷积神经网络
从动物视觉皮层得到启发,卷积神经网络(CNN)主要用于图像处理,并且实际上已经成为图像处理的标准。 卷积层的核心概念是核(或过滤器)的存在,这些核学习区分图像的特征。 核通常比图像矩阵短得多,并且以滑动窗口的方式传递到整个图像上,从而产生核的点积与待处理图像的相应矩阵切片。 点积使程序可以识别图像中的特征。
考虑以下图像向量:
[[10, 10, 10, 0, 0, 0],
[10, 10, 10, 0, 0, 0],
[10, 10, 10, 0, 0, 0],
[0, 0, 0, 10, 10, 10],
[0, 0, 0, 10, 10, 10],
[0, 0, 0, 10, 10, 10]]
前面的矩阵对应于如下图像:

在应用过滤器检测水平边缘时,过滤器由以下矩阵定义:
[[1, 1, 1],
[0, 0, 0],
[-1, -1, -1]]
原始图像与过滤器卷积后产生的输出矩阵如下:
[[ 0, 0, 0, 0],
[ 30, 10, -10, -30],
[ 30, 10, -10, -30],
[ 0, 0, 0, 0]]
在图像的上半部或下半部没有检测到边缘。 从左边缘移到图像的垂直中间时,会发现清晰的水平边缘。 在向右移动时,在水平边缘的另一个清晰实例之前找到了两个水平边缘的不清楚实例。 但是,现在发现的清晰水平边缘的颜色与上一个相反。
因此,通过简单的卷积,可以发现图像文件中的图案。 CNN 还使用其他几个概念,例如池化。
可以从以下屏幕截图中了解池化:

用最简单的术语来说,合并是将多个图像像素合并为单个像素的方法。 前面的屏幕快照中使用的合并方法称为最大池化,其中只有来自所选滑动窗口核的最大值保留在所得矩阵中。 这极大地简化了图像,并有助于训练通用且不是单个图像专用的过滤器。
生成对抗网络
生成对抗网络(GAN)是 AI 领域中一个相当新的概念,并且最近已成为一项重大突破。 它们是由 Ian Goodfellow 在 2014 年的研究论文中介绍的。GAN 的核心思想是两个相互竞争的神经网络的并行运行。 第一个神经网络执行生成样本的任务,称为生成器。 另一个神经网络尝试根据先前提供的数据对样本进行分类,称为判别器。 可以通过以下屏幕截图了解 GAN 的功能:

在此,随机图像向量经过生成过程以生成伪造图像,然后由已经用真实图像训练的判别器对伪造图像进行分类。 具有较高分类置信度的伪图像将进一步用于生成,而具有较低置信度的伪图像将被丢弃。 随着时间的流逝,判别器学会正确识别伪造的图像,而生成器学会在每一代之后逐渐生成与真实图像相似的图像。
在学习结束时,我们将拥有一个可以产生近乎真实数据的系统,以及一个可以非常精确地对样本进行分类的系统。
在接下来的章节中,我们将了解有关 GAN 的更多信息。
对于 GAN 的深入研究,您可以阅读 Ian Goodfellow 在这个页面上发表的研究论文。
循环神经网络
并非世界上所有数据都独立于时间而存在。 股市价格和口语/书面文字只是与时间序列相关的数据的几个示例。 因此,数据序列具有时间维度,您可能会假设能够以适合数据的方式使用它(随时间的流逝而不是保持不变的数据块)会更直观,更直观。 会产生更好的预测精度。 在许多情况下,这是事实,并导致了神经网络架构的出现,在学习和预测时可能需要时间。
一种这样的架构是循环神经网络(RNN)。 这种网络的主要特征是它不仅以顺序的方式将数据从一层传递到另一层,而且还从任何先前的层获取数据。 回顾“了解机器学习和深度学习”部分的示意图,该图具有两个隐藏层,是一个简单的人工神经网络(ANN)。 数据仅由上一层馈入下一层。 在具有两个隐藏层的 RNN 中,并非强制要求仅由第一隐藏层提供对第二隐藏层的输入,就像在简单的 ANN 中那样。
以下屏幕截图中的虚线箭头表示了这一点:

与简单的 ANN 相比,RNN 使用一种称为时间上的反向传播(BPTT)的方法,而不是 ANN 中的经典反向传播。 BPTT 通过在与网络中必须重复出现的输入有关的函数中定义时间,从而确保在错误的反向传播中很好地表示时间。
长期记忆
在 RNN 中观察到消失和爆炸梯度非常普遍。 在深度 RNN 的实现中,这是一个严重的瓶颈,在深度 RNN 中,数据以特征之间的关系比线性函数更复杂的形式存在。 为了克服消失的梯度问题,德国研究人员 Sepp Hochreiter 和 Juergen Schmidhuber 于 1997 年提出了长短期记忆(LSTM)的概念。
LSTM 已被证明在 NLP,图像标题生成,语音识别和其他领域中非常有用,在引入后,LSTM 打破了先前建立的记录。 LSTM 将信息存储在网络外部,可以随时调用,就像计算机系统中的辅助存储设备一样。 这允许将延迟的奖励引入网络。 对 LSTM 进行了精神上的类比,称其为一个人因过去所采取的行动而获得的“业力”或报酬。
在本书的后续章节中,我们将更深入地研究 LSTM 和 CNN。
强化学习和自然语言处理简介
在本节中,我们将研究强化学习和自然语言处理的基本概念。 这些是 AI 领域中非常重要的主题。 他们可能会也可能不会使用深度学习网络来实现,但是通常会使用深度网络来实现。 因此,了解它们的功能至关重要。
强化学习
强化学习是机器学习的一个分支,用于创建 AI“智能体”,以在给定环境中执行一组可能的动作,以使报酬最大化。 机器学习的其他两个分支(有监督的和无监督的机器学习)通常以表的形式在数据集上执行学习,而强化学习智能体通常使用决策树进行学习,以在任何给定情况下进行决策,最终使决策树到达具有最大奖励的叶子。
例如,考虑一个希望学习走路的人形机器人。 它可以首先将其两条腿推到自己的前面,在这种情况下它会掉落,而奖励(在这种情况下,是人形机器人所覆盖的距离)将为 0。然后,它将学会在提出的前一条和提出的下一条之间增加一定的延迟。 由于存在一定程度的延迟,这可能是机器人能够在再次踩踏双脚同时向外并且摔倒之前采取x1的步骤。
强化学习采用探索的概念,这意味着寻求更好的解决方案,而利用的概念则意味着使用先前获得的知识。 继续我们的示例,由于x1大于 0,因此该算法学会了在跨步之间放置大约相同的特定延迟量。 随着时间的推移,在开发和探索的共同作用下,强化学习算法变得非常强大,在这种情况下,类人动物不仅能够学习如何走路,而且还能学习跑步。
自然语言处理
NLP 是 AI 的广阔领域,它通过使用计算机算法来处理和理解人类语言。 NLP 包含几种针对人类语言理解的不同部分的方法和技术,例如,基于两个文本摘录的相似性来理解含义,生成人类语言响应,理解人类语言中提出的问题或指令以及将文本从一种语言翻译到另一种语言。
NLP 已在当今的技术领域中得到了广泛的应用,几家顶尖的技术公司都在朝着该领域迈进。 有几种基于语音的用户助手,例如 Siri,Cortana 和 Google Assistant,它们严重依赖准确的 NLP 才能正确执行其功能。 NLP 还发现可以通过自动客户支持平台在客户支持中使用它们,这些平台可以回答最常见的查询,而无需人工代表来回答。 这些基于 NLP 的客户支持系统在与客户互动时,还可以从真实代表的响应中学习。 在新加坡开发银行创建的 DBS DigiBank 应用的“帮助”部分中,可以找到一种这样的主要部署系统。
目前正在这一领域进行广泛的研究,并有望在未来几天主导 AI 的所有其他领域。 在下一部分中,让我们看一下将深度学习与移动应用集成的当前可用方法。
在 Android 和 iOS 上集成 AI 的方法
随着 AI 的日益普及,移动应用用户希望应用能够适应提供给他们的信息。 使应用适应数据的唯一方法是部署经过微调的机器学习模型,以提供令人愉悦的用户体验。
Firebase ML 套件
Firebase ML Kit 是机器学习软件开发工具包(SDK),可在 Firebase 上为移动开发人员使用。 它促进了移动机器学习模型的托管和服务。 它减少了在移动设备上运行机器学习模型的繁重任务,从而减少了 API 调用,该 API 调用涵盖了常见的移动用例,例如面部检测,文本识别,条形码扫描,图像标记和地标识别。 它只是将输入作为参数,以便输出大量分析信息。 ML Kit 提供的 API 可以在设备上,在云上或在两者上运行。 设备上的 API 独立于网络连接,因此,与基于云的 API 相比,工作速度更快。 基于云的 API 托管在 Google Cloud Platform 上,并使用机器学习技术来提供更高的准确率。 如果可用的 API 无法满足所需的用例,则可以使用 Firebase 控制台构建,托管和提供自定义 TensorFlow Lite 模型。 ML Kit 充当自定义模型之间的 API 层,使其易于运行。 让我们看下面的截图:

在这里,您可以查看 Firebase ML Kit 的仪表板外观。
Core ML
Core ML 是 Apple 在 iOS 11 中发布的一种机器学习框架,用于使在 iOS 上运行的应用(例如 Siri,Camera 和 QuickType)更加智能。 通过提供有效的性能,Core ML 促进了 iOS 设备上机器学习模型的轻松集成,使应用能够根据可用数据进行分析和预测。 Core ML 支持标准的机器学习模型,例如树状集成体,SVM 和广义线性模型。 它包含具有 30 多种类型的神经元层的广泛的深度学习模型。
使用 Vision 框架,可以轻松地将面部跟踪,面部检测,文本检测和对象跟踪等功能与应用集成。 自然语言框架有助于分析自然文本并推导其特定于语言的元数据。 与 Create ML 一起使用时,该框架可用于部署自定义 NLP 模型。 对 GamePlayKit 的支持有助于评估学习的决策树。 Core ML 建立在诸如 Metal 和 Accelerate 之类的底层技术之上,因此非常高效。 这使其可以利用 CPU 和 GPU。 此外,Core ML 不需要活动的网络连接即可运行。 它具有很高的设备上优化能力。 这样可确保所有计算都在设备本身内部离线进行,从而最大程度地减少了内存占用和功耗。
Caffe2
Caffe2 建立在由加州大学伯克利分校开发的用于快速嵌入的原始卷积架构(Caffe)上,是一种轻量级,模块化,可扩展的深度学习框架,由 Facebook 开发。 它可以帮助开发人员和研究人员部署机器学习模型,并在 Android,iOS 和 Raspberry Pi 上提供 AI 驱动的性能。 此外,它支持在 Android Studio,Microsoft Visual Studio 和 Xcode 中进行集成。 Caffe2 带有可互换使用的本机 Python 和 C++ API,从而简化了原型设计和优化过程。 它足够有效地处理大量数据,并且有助于自动化,图像处理以及统计和数学运算。 Caffe2 是开源的,托管在 GitHub 上,它利用社区的贡献来开发新模型和算法。
TensorFlow
TensorFlow 是 Google Brain 开发的开源软件库,可促进高性能数值计算。 由于其灵活的架构,它允许在 CPU,GPU 和 TPU 之间轻松部署深度学习模型和神经网络。 Gmail 使用 TensorFlow 模型来了解邮件的上下文,并通过其广为人知的功能“智能回复”来预测回复。 TensorFlow Lite 是 TensorFlow 的轻量级版本,有助于在 Android 和 iOS 设备上部署机器学习模型。 它利用 Android 神经网络 API 的功能来支持硬件加速。
下图说明了可通过 TensorFlow Lite 用于移动设备的 TensorFlow 生态系统:

在上图中,您可以看到我们需要将 TensorFlow 模型转换为 TensorFlow Lite 模型,然后才能在移动设备上使用它。 这很重要,因为与优化运行在移动设备上的 Lite 模型相比,TensorFlow 模型体积更大且延迟更大。 转换是通过 TF Lite 转换器执行的,可以通过以下方式使用:
- 使用 Python API:可以使用 Python 和以下任何代码行将 TensorFlow 模型转换为 TensorFlow Lite 模型。
TFLiteConverter.from_saved_model(): Converts SavedModel directories.
TFLiteConverter.from_keras_model(): Converts tf.keras models.
TFLiteConverter.from_concrete_functions(): Converts concrete functions.
- 使用命令行工具:TensorFlow Lite 转换器也可以作为 CLI 工具使用,尽管它的功能与 Python API 版本相比有些不同:
tflite_convert \
--saved_model_dir=/tf_model \
--output_file=/tflite_model.tflite
在接下来的章节中,我们将演示将 TensorFlow 模型转换为 TensorFlow Lite 模型。
总结
在本章中,我们了解了移动设备中 AI 的增长,这使机器无需进行明确编程即可推理和做出决策。 我们还研究了机器学习和深度学习,其中包括与 AI 领域相关的技术和算法。 我们研究了各种深度学习架构,包括 CNN,GAN,RNN 和 LSTM。
我们介绍了强化学习和 NLP,以及在 Android 和 iOS 上集成 AI 的不同方法。 深度学习的基本知识以及如何将其与移动应用集成对于接下来的章节非常重要,在这些章节中,我们将广泛使用该知识来创建一些实际应用。
在下一章中,我们将学习使用设备上模型进行面部检测的知识。
二、移动视觉 - 使用设备上的模型的人脸检测
在本章中,我们将构建一个 Flutter 应用,该应用能够使用 ML Kit 的 Firebase Vision 人脸检测 API 从从设备图库上传的媒体中或直接从相机中检测人脸。 该 API 利用了 Firebase 上托管的预训练模型的功能,并为应用提供了识别面部关键特征,检测表情并获取检测到的面部轮廓的功能。 由于人脸检测是通过 API 实时执行的,因此它还可用于跟踪视频序列,视频聊天或响应用户表情的游戏中的人脸。 用 Dart 编码的应用将在 Android 和 iOS 设备上有效运行。
在本章中,我们将讨论以下主题:
- 图像处理简介
- 使用 Flutter 开发人脸检测应用
让我们先简单介绍一下图像识别的工作原理!
技术要求
您需要带有 Flutter 和 Dart 插件的 Visual Studio Code,并且需要设置 Firebase 控制台。 本章的 GitHub 存储库位于这里。
图像处理简介
在本章中,我们将检测图像中的人脸。 在人工智能的上下文中,为了提取有关该图像的视觉内容的信息而处理图像的动作称为图像处理。
得益于更好的人工智能驱动相机,基于医学图像的机器学习,自动驾驶汽车,人们从图像中分析人的情感以及许多其他应用的数量激增,图像处理是一个新兴领域。
考虑使用自动驾驶车辆进行图像处理。 车辆需要尽可能接近实时地做出决策,以确保最佳的无事故驾驶。 汽车驾驶 AI 模型的响应延迟可能会导致灾难性后果。 已经开发了几种技术和算法来进行快速和准确的图像处理。 图像处理领域中最著名的算法之一是卷积神经网络(CNN)。
我们不会在本章中开发完整的 CNN,但是,我们在 “第 1 章”,“移动深度学习简介”中简要讨论了 CNN。 稍后,我们将使用设备上存在的预训练模型构建面部检测 Flutter 应用。
了解图像
在深入研究图像处理之前,让我们从计算机软件的角度讨论图像的解剖结构。 考虑以下简单图像:

前面的图像是10 x 10像素的图像(放大); 前两行像素为紫色,后六行像素为红色,后两行像素为黄色。
但是,计算机看不到该图像中的颜色。 计算机以像素密度矩阵的形式看到此图像。 我们在这里处理 RGB 图像。 RGB 图像由三层颜色组成,即红色,绿色和蓝色。 这些层中的每一个都由图像中的矩阵表示。 每个矩阵的元素对应于图像的每个像素中该矩阵表示的颜色的强度。
让我们检查程序中的上一个图像。 紫色的两行像素之一由以下数组表示:
[[255, 0, 255],
[255, 0, 255],
[255, 0, 255],
[255, 0, 255],
[255, 0, 255],
[255, 0, 255],
[255, 0, 255],
[255, 0, 255],
[255, 0, 255],
[255, 0, 255]]
在前面的矩阵中,255的第一列表示红色。 第二列代表绿色,第三列代表蓝色。 因此,图像左上角的第一个像素是红色,绿色和蓝色的组合。 红色和蓝色都处于最大强度,而绿色则完全缺失。 因此,正如预期的那样,产生的组合颜色是紫色,基本上是红色和蓝色以相等的比例混合。 如果我们按预期观察到图像红色区域中的任何像素,则会得到以下数组:
[ 255, 0, 0 ]
同样,从黄色区域开始,由于黄色是红色和绿色的等比例组合,因此像素用以下形式表示:
[ 255, 255, 0 ]
现在,如果我们关闭图像的红色和绿色部分,仅打开蓝色通道,则会得到以下图像:

根据我们之前的观察,这非常多,只有前两行像素包含蓝色成分,而图像的其余部分没有蓝色成分,因此将其显示为黑色,这表示没有强度或0 蓝色强度。
处理图像
在本节中,我们将讨论如何对图像进行一些常见的操作以帮助图像处理。 通常,对图像进行一些简单的操作可以导致更快,更好的预测。
旋转
假设我们希望将示例中的图像旋转 90 度。 如果检查旋转后从顶部开始的第一行像素,则可以预期该行的前两个像素为紫色,中间的六个像素为红色,最后两个像素为黄色。 与矩阵旋转类似,这可以看作是转置操作,其中行转换为列,反之亦然。 图像如下所示:

而且,正如预期的那样,第一行像素由以下矩阵表示:
[[255, 0, 255],
[255, 0, 255],
[255, 0, 0],
[255, 0, 0],
[255, 0, 0],
[255, 0, 0],
[255, 0, 0],
[255, 0, 0],
[255, 255, 0],
[255, 255, 0]]
在此矩阵中,前两个元素代表紫色,然后是六个红色,最后两个是黄色。
灰度转换
在对其进行机器学习之前,从图像中完全删除颜色信息通常很有用。 原因是颜色有时不是所要求的预测的促成因素。 例如,在检测图像中数字的系统中,数字的形状很重要,而数字的颜色对解决方案无济于事。
简而言之,灰度图像是对图像区域中可见光的量度。 通常,最占主导地位的浅色元素会被完全去除,以显示可见度较低的区域的对比度。
将 RGB 转换为灰度的公式如下:

Y是要转换为灰度的像素将保留的最终值。R,G和B是该特定像素的红色,绿色和蓝色值。 产生的输出如下:

现在让我们开始研究面部检测应用!
使用 Flutter 开发人脸检测应用
通过“第 1 章”,“移动深度学习简介”以及如何在最基本的水平上完成图像处理,对 CNN 的工作原理有了基本的了解,我们准备继续使用 Firebase ML Kit 中的预训练模型来检测给定图像中的人脸。
我们将使用 Firebase ML Kit 人脸检测 API 来检测图像中的人脸。 Firebase Vision 人脸检测 API 的主要功能如下:
- 识别并返回检测到的每个脸部的面部特征的坐标,例如眼睛,耳朵,脸颊,鼻子和嘴巴。
- 获取检测到的面部和面部特征的轮廓。
- 检测面部表情,例如一个人在微笑还是闭着眼睛。
- 获取在视频帧中检测到的每个人脸的标识符。 该标识符在调用之间是一致的,可用于对视频流中的特定面孔执行图像处理。
让我们从第一步开始,添加所需的依赖项。
添加发布依赖
我们首先添加发布依赖项。 依赖项是特定功能正常运行所需的外部包。 在pubspec.yaml文件中指定了应用所需的所有依赖项。 对于每个依赖项,都应提及包的名称。 通常在其后跟随一个版本号,指定我们要使用的包的版本。 此外,还可以包括包的源代码,该资源告诉 pub 如何找到该包,以及源代码需要查找该包的任何描述。
要获取有关特定包的信息,请访问这里。
我们将用于此项目的依赖项如下:
-
firebase_ml_vision:一种 Flutter 插件,增加了对 Firebase ML Kit 功能的支持 -
image_picker:Flutter 插件,可使用相机拍照并从 Android 或 iOS 图像库中选择图像
包含依赖项后,pubspec.yaml文件的dependencies部分如下所示:
dependencies:
flutter:
sdk: flutter
firebase_ml_vision: ^0.9.2+1
image_picker: ^0.6.1+4
为了使用我们添加到pubspec.yaml文件的依赖项,我们需要安装它们。 只需在终端中运行flutter pub get或单击pubspec.yaml文件顶部操作区域右侧的“获取包”即可完成此操作。 一旦安装了所有依赖项,我们就可以简单地将它们导入我们的项目中。 现在,让我们看一下本章将要处理的应用的基本功能。
建立应用
现在我们构建应用。 名为人脸检测的应用将包含两个屏幕。 第一个带有两个按钮的文本标题,允许用户从设备的图片库中选择图像或使用相机拍摄新图像。 此后,用户被引导至第二屏幕,该屏幕显示高亮显示检测到的面部而选择用于面部检测的图像。 以下屏幕截图显示了该应用的流程:

该应用的小部件树如下所示:

现在让我们详细讨论每个小部件的创建和实现。
创建第一个屏幕
在这里,我们创建第一个屏幕。 第一个屏幕的用户界面将包含一个文本标题Pick Image和两个按钮Camera和Gallery。 可以将其视为包含文本标题的列和带有两个按钮的行,如以下屏幕截图所示:

在以下各节中,我们将构建称为小部件的每个元素,然后将它们放在支架下。
用英语讲,支架表示提供某种支持的结构或平台。 就 Flutter 而言,可以将支架视为设备屏幕上的主要结构,所有次要组件(在此情况下为小部件)都可以放置在其上。
在 Flutter 中,每个 UI 组件都是小部件。 它们是 Flutter 框架中的中心类层次结构。 如果您以前使用过 Android Studio,则可以将小部件视为TextView或Button或任何其他视图组件。
建立行标题
然后正在建立行标题。 我们首先在face_detection_home.dart file内创建一个有状态的小部件FaceDetectionHome。 FaceDetectionHomeState将包含构建应用第一个屏幕所需的所有方法。
让我们定义一个称为buildRowTitle()的方法来创建文本标题:
Widget buildRowTitle(BuildContext context, String title) {
return Center(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 16.0),
child: Text(
title,
style: Theme.of(context).textTheme.headline,
), //Text
) //Padding
); //Center
}
该方法用于使用title字符串中传递的值作为参数来创建带有标题的窗口小部件。 使用Center()将文本水平对齐到中心,并使用EdgeInsets.symmetric(horizontal: 8.0, vertical: 16.0)将文本水平8.0和16.0垂直填充。 它包含一个子级,用于创建带有标题的Text。 文本的印刷样式被修改为textTheme.headline,以更改文本的默认大小,粗细和间距。
Flutter 使用逻辑像素作为度量单位,与与设备无关的像素(dp)相同。 此外,每个逻辑像素中的设备像素的数量可以根据devicePixelRatio来表示。 为了简单起见,我们将仅使用数字项来谈论宽度,高度和其他可测量的属性。
使用按钮小部件构建行
接下来是使用按钮小部件构建行。 放置文本标题后,我们现在将创建一行两个按钮,使用户可以从图库中选择图像或从相机中获取新图像。 让我们按照以下步骤进行操作:
- 我们首先定义
createButton()以创建具有所有必需属性的按钮:
Widget createButton(String imgSource) {
return Expanded(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0),
child: RaisedButton(
color: Colors.blue,
textColor: Colors.white,
splashColor: Colors.blueGrey,
onPressed: () {
onPickImageSelected(imgSource);
},
child: new Text(imgSource)
),
)
);
}
在提供8.0的水平填充之后,该方法返回一个小部件,即RaisedButton。 按钮的颜色设置为blue,按钮文本的颜色设置为white。 splashColor设置为blueGrey表示通过产生波纹效果来单击按钮。
按下按钮时,将执行onPressed内部的代码段。 在这里,我们调用了onPickImageSelected(),它在本章的后面部分中定义。 按钮内显示的文本设置为imgSource,这里可以是图库或照相机。 此外,整个代码段都包装在Expanded()中,以确保所创建的按钮完全占据所有可用空间。
- 现在,我们使用
buildSelectImageRowWidget()方法来构建带有两个按钮的行,以列出两个图像源:
Widget buildSelectImageRowWidget(BuildContext context) {
return Row(
children: <Widget>[
createButton('Camera'),
createButton('Gallery')
],
);
}
在前面的代码片段中,我们调用先前定义的createButton()方法将Camera和Gallery添加为图像源按钮,并将它们添加到该行的children小部件列表中。
- 现在,让我们定义
onPickImageSelected()。 此方法使用image_picker库将用户定向到图库或照相机以获取图像:
void onPickImageSelected(String source) async {
var imageSource;
if (source == 'Camera') {
imageSource = ImageSource.camera;
} else {
imageSource = ImageSource.gallery;
}
final scaffold = _scaffoldKey.currentState;
try {
final file = await ImagePicker.pickImage(source: imageSource);
if (file == null) {
throw Exception('File is not available');
}
Navigator.push(
context,
new MaterialPageRoute(
builder: (context) => FaceDetectorDetail(file)),
);
} catch (e) {
scaffold.showSnackBar(SnackBar(
content: Text(e.toString()),
));
}
}
首先,使用if-else块将imageSource设置为摄像机或图库。 如果传递的值为Camera,则图像文件的源设置为ImageSource.camera; 否则,将其设置为ImageSource.gallery。
一旦确定了图像的来源,就使用pickImage()来选择正确的imageSource。 如果源是Camera,则将引导用户到相机拍摄图像; 否则,将指示他们从图库中选择图片。
如果pickImage()未成功返回图像,则为处理异常,对该方法的调用包含在try-catch块内。 如果发生异常,则通过调用showSnackBar()将执行定向到catch块和小吃店,并在屏幕上显示错误消息:

在成功选择图像并且file变量具有所需的uri之后,用户将迁移到下一个屏幕FaceDetectorDetail,这在“创建第二个屏幕”部分中进行了介绍,并使用Navigator.push()将当前上下文和所选文件传递到构造器中。 在FaceDetectorDetail屏幕上,我们用所选图像填充图像支架并显示有关检测到的面部的详细信息。
创建整个用户界面
现在,我们创建了整个用户界面,所有创建的小部件都放在了FaceDetectorHomeState类中被覆盖的build()方法中。
在以下代码片段中,我们为应用的第一个屏幕创建了最终的支架:
@override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
centerTitle: true,
title: Text('Face Detection'),
),
body: SingleChildScrollView(
child: Column(
children: <Widget>[
buildRowTitle(context, 'Pick Image'),
buildSelectImageRowWidget(context)
],
)
)
);
}
通过在appBar中设置标题,可以将工具栏的文本设置为Face Detection。 另外,通过将centerTitle设置为true,文本将居中对齐。 接下来,支架的主体是一列小部件。 第一个是文本标题,第二个是一行按钮。
可以在这个页面上查看FaceDetectorHome.dart中的整个代码。
创建第二个屏幕
接下来,我们创建第二个屏幕。 成功获取用户选择的图像后,我们迁移到应用的第二个屏幕,在其中显示选择的图像。 此外,我们使用 Firebase ML Kit 标记在图像中检测到的面部。 我们首先在新的 Dart 文件face_detection.dart中创建一个名为FaceDetection的有状态小部件。
获取图像文件
首先,需要将所选图像传递到第二个屏幕进行分析。 我们使用FaceDetection()构造器执行此操作。
构造器是用于初始化类变量的特殊方法。 它们与类具有相同的名称。 构造器没有返回类型,并且在创建类的对象时会自动调用它们。
我们声明一个file变量,并使用参数化的构造器对其进行初始化,如下所示:
File file;
FaceDetection(File file){
this.file = file;
}
现在让我们继续下一步。
分析图像来检测面部
现在,我们分析图像以检测面部。 我们将创建FirebaseVision人脸检测器的实例,以使用以下步骤检测人脸:
- 首先,我们在
FaceDetectionState类内创建一个全局faces变量,如以下代码所示:
List<Face> faces;
- 现在我们定义一个
detectFaces()方法,在其中实例化FaceDetector如下:
void detectFaces() async{
final FirebaseVisionImage visionImage = FirebaseVisionImage.fromFile(widget.file);
final FaceDetector faceDetector = FirebaseVision.instance.faceDetector(FaceDetectorOptions( mode: FaceDetectorMode.accurate, enableLandmarks: true, enableClassification: true));
List<Face> detectedFaces = await faceDetector.processImage(visionImage);
for (var i = 0; i < faces.length; i++) {
final double smileProbablity = detectedFaces[i].smilingProbability;
print("Smiling: $smileProb");
}
faces = detectedFaces;
}
我们首先创建一个使用FirebaseVisionImage.fromFile()方法选择的图像文件的FirebaseVisionImage实例,该实例称为visionImage。 接下来,我们使用FirebaseVision.instance.faceDetector()方法创建FaceDetector的实例,并将其存储在名为faceDetector的变量中。 现在我们使用先前创建的FaceDetector实例faceDetector调用processImage(),并将图像文件作为参数传递。 方法调用返回检测到的面部列表,该列表存储在名为detectedFaces的列表变量中。 请注意,processImage()返回类型为Face的列表。 Face是一个对象,其属性包含检测到的脸部的特征。 Face对象具有以下属性:
getLandmarkhashCodehasLeftEyeOpenProbabilityhasRightEyeOpenProbabilityheadEulerEyeAngleYheadEylerEyeAngleZleftEyeOpenProbabilityrightEyeOpenProbabilitysmilingProbability
现在,我们使用for循环遍历脸部列表。 我们可以使用detectedFaces[i].smilingProbability获得第i个smilingProbablity值。 我们将其存储在名为smileProbablity的变量中,然后使用print()将其值打印到控制台。 最后,我们将全局faces列表的值设置为detectedFaces。
添加到detectFaces()方法的async修饰符使该方法能够异步执行,这意味着将创建一个与执行主线程不同的单独线程。 async方法适用于回调机制,以在执行完成后返回由其计算的值。
为了确保在用户迁移到第二个屏幕后立即检测到面部,我们将覆盖initState()并从其中调用detectFaces():
@override
void initState() {
super.initState();
detectFaces();
}
initState()是在创建窗口小部件之后调用的第一个方法。
标记检测到的面部
接下来,标记检测到的面部。 检测到图像中存在的所有面部之后,我们将通过以下步骤在其周围绘制矩形框:
- 首先,我们需要将图像文件转换为原始字节。 为此,我们定义
loadImage方法如下:
void loadImage(File file) async {
final data = await file.readAsBytes();
await decodeImageFromList(data).then(
(value) => setState(() {
image = value;
}),
);
}
loadImage()方法将图像文件作为输入。 然后,我们使用file.readAsByte()将文件的内容转换为字节,并将结果存储在数据中。 接下来,我们调用decodeImageFromList(),它用于将单个图像帧从字节数组加载到Image对象中,并将最终结果值存储在图像中。 我们从先前定义的detectFaces()内部调用此方法。
- 现在,我们定义一个名为
FacePainter的CustomPainter类,以便在所有检测到的面部周围绘制矩形框。 我们开始如下:
class FacePainter extends CustomPainter {
Image image;
List<Face> faces;
List<Rect> rects = [];
FacePainter(ui.Image img, List<Face> faces) {
this.image = img;
this.faces = faces;
for(var i = 0; i < faces.length; i++) {
rects.add(faces[i].boundingBox);
}
}
}
}
我们首先定义三个全局变量image,faces和rects。 类型为Image的image用于获取图像文件的字节格式。 faces是检测到的Face对象的List。 image和faces都在FacePainter构造器中初始化。 现在我们遍历这些面,并使用faces[i].boundingBox获得每个面的边界矩形,并将其存储在rects列表中。
- 接下来,我们覆盖
paint(),以用矩形绘制Canvas,如下所示:
@override
void paint(Canvas canvas, Size size) {
final Paint paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 8.0
..color = Colors.red;
canvas.drawImage(image, Offset.zero, Paint());
for (var i = 0; i < faces.length; i++) {
canvas.drawRect(rects[i], paint);
}
}
我们从创建Paint类的实例开始,以描述绘制Canvas的样式,即我们一直在使用的图像。 由于我们需要绘制矩形边框,因此将style设置为PaintingStyle.stroke以仅绘制形状的边缘。 接下来,我们将strokeWidth,即矩形边框的宽度设置为8。 另外,我们将color设置为red。 最后,我们使用cavas.drawImage()绘制图像。 我们遍历rects列表内检测到的面部的每个矩形,并使用canvas.drawRect()绘制矩形。
在屏幕上显示最终图像
成功检测到面部并在其周围绘制矩形后,我们现在将在屏幕上显示最终图像。 我们首先为第二个屏幕构建最终的脚手架。 我们将覆盖FaceDetectionState中的build()方法,以返回支架,如下所示:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Face Detection"),
),
body: (image == null)
? Center(child: CircularProgressIndicator(),)
: Center(
child: FittedBox(
child: SizedBox(
width: image.width.toDouble(),
height: image.width.toDouble(),
child: CustomPaint(painter: FacePainter(image, faces))
),
),
)
);
}
我们首先为屏幕创建appBar,并提供标题Face Detection。 接下来,我们指定支架的body。 我们首先检查image的值,该值存储所选图像的字节数组。 直到时间为零,我们确信检测面部的过程正在进行中。 因此,我们使用CircularProgressIndicator()。 一旦检测到脸部的过程结束,用户界面就会更新,以显示具有与所选图像相同的宽度和高度的SizedBox。 SizedBox的child属性设置为CustomPaint,它使用我们之前创建的FacePainter类在检测到的脸部周围绘制矩形边框。
可以在这个页面上查看face_detection.dart中的整个代码。
创建最终的 MaterialApp
最后,我们创建最终的MaterialApp。 我们创建main.dart文件,该文件提供了整个代码的执行点。 我们创建一个名为FaceDetectorApp的无状态小部件,该小部件用于返回指定标题,主题和主屏幕的MaterialApp:
class FaceDetectorApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new FaceDetectorHome(),
);
}
}
现在,我们通过传入FaceDetectorApp()的实例,定义main()方法来执行整个应用,如下所示:
void main() => runApp(new FaceDetectorApp());
可以在这个页面中查看main.dart中的整个代码。
总结
在本章中,我们研究了图像处理背后的概念,以及如何将其与使用 Flutter 进行面部检测的基于 Android 或 iOS 的应用集成。 本章从添加相关的依赖关系开始,以支持 Firebase ML Kit 和image_picker库的功能。 添加了具有必要功能的必需 UI 组件。 该实现主要介绍了使用 Flutter 插件选择图像文件以及选择图像后如何对其进行处理。 给出了设备上人脸检测器模型用法的示例,并深入讨论了实现方法。
在下一章中,我们将讨论如何创建自己的 AI 驱动的聊天机器人,该聊天机器人可以使用 Google 平台上的 Actions 兼作虚拟助手。
三、使用 Google Action 的聊天机器人
在这个项目中,我们将介绍使用 Dialogflow API 实现对话聊天机器人的方法,以及如何借助 Google 的操作使对话聊天机器人在 Google Assistant 上执行不同的操作。 该项目将使您对如何构建使用引人入胜的基于语音和文本的对话界面的产品有很好的了解。
我们将实现一个聊天机器人,该机器人将询问用户名称,然后为该用户生成一个幸运数字。 我们还将研究如何使用 Google 的 Actions 在 Google Assistant 平台上提供聊天机器人。
本章将涵盖以下主题:
- 了解可用于创建聊天机器人的工具
- 创建一个 Dialogflow 帐户
- 创建一个 Dialogflow 智能体
- 了解 Dialogflow 控制台
- 在 Google 上创建您的第一个动作
- 在 Google 项目上创建操作
- 实现 Webhook
- 将 Webhook 部署到 Firebase 的 Cloud Functions
- 在 Google 版本上创建动作
- 为对话应用创建 UI
- 集成 Dialogflow 智能体
- 与助手添加音频交互
技术要求
对于移动应用,您将需要带有 Flutter 和 Dart 插件的 Visual Studio Code,以及 Firebase 控制台的设置和运行。
可以在本书的 GitHub 存储库中找到本章的代码文件。
了解可用于创建聊天机器人的工具
如果您希望使用聊天机器人为用户建立对话体验,那么您将有很多选择可以建立。 有几种平台具有不同的功能集,每种平台在其提供的服务方面都是独一无二的。
人工智能聊天机器人是近十年来一直在持续增长的聊天机器人类型,它已经成功地为聊天机器人更容易地进入专业网站和行业铺平了道路。 这些漫游器提供什么样的情报? 他们解决什么业务目标?
让我们尝试用一个场景回答这两个问题。
假设您拥有一家百货商店,并在商店中雇用了几名员工,以便他们可以将您的客户引导到正确的部门。 有一天,您意识到这些员工实际上正在加剧商店的拥挤。 为了替换它们,您想出了一个能够响应“在哪里可以找到一些谷物?”之类的问题的应用, 带有“谷物部分朝向商店的西北部,就在水果部分旁边!”之类的答案。
聊天机器人因此具有理解用户需求的能力,在这种情况下,该需求是找到谷物。 然后,聊天机器人能够确定谷物与杂货之间的关系。 根据对商店库存的了解,它可以将用户定向到正确的部门。 为了能够提出联想,甚至将单词从一种语言翻译成另一种语言,深度学习在聊天机器人的内部工作中起着至关重要的作用。
在以下各节中,我们将探讨各种支持人工智能的工具,这些工具可用于创建聊天机器人并将其部署在手机上。
Wit.ai
Wit.ai平台由 Facebook 制作,围绕自然语言处理(NLP)和语音转文本服务提供了一套 API。 Wit.ai平台是完全开源的,并在 NLP 领域提供一些最新服务。 它可以轻松地与移动应用和可穿戴设备集成,甚至可以用于家庭自动化。 该平台提供的语音文本服务使其非常适合创建使用语音接口的应用。
开发人员可以轻松设计完整的对话,甚至可以为聊天机器人添加个性。 Wit.ai支持超过 130 种语言的对话和语音到文本服务,这使其成为专注于全球语言可访问性的应用的绝佳选择。
要了解有关该平台的更多信息,请访问这里。
Dialogflow
从Api.ai重命名的 Dialogflow 提供了基于深度神经网络的自然语言处理,以创建可与多个平台(例如 Facebook Messenger,Slack,WhatsApp,Telegram 等)无缝集成的对话界面。
Dialogflow 项目在 Google Cloud 上运行,并且能够从与构建会话相关的所有 Google Cloud 产品中受益,例如获取用户的位置,在 Firebase 或 App Engine 上部署 Webhooks 以及在这两个平台上由 Google 开发的应用中启动操作 Android 和 iOS。 您可以通过这里了解有关该平台的更多信息。
现在,让我们更深入地研究 Dialogflow 及其功能,以了解如何为移动设备开发类似 Google Assistant 的应用。
Dialogflow 如何工作?
在上一节中,我们简要介绍了一些可用于根据需要使用文本和语音开发聊天机器人和对话界面的工具。 我们遇到了 Dialogflow,我们将在本节中对其进行深入讨论。 我们还将使用它来快速开发行业级的聊天解决方案。
在开始开发 Dialogflow 聊天机器人之前,我们需要了解 Dialogflow 的工作原理,并了解与 Dialogflow 相关的一些术语。
下图显示了使用 Dialogflow 的应用中的信息流:

让我们讨论上图中引入的术语:
- 用户:用户是使用聊天机器人/应用的人,并且负责发出用户请求。 用户请求只是由用户发出的口语或句子,必须由聊天机器人进行解释。 需要针对它生成适当的响应。
- 集成:集成是一个软件组件,负责将用户请求传递给聊天机器人逻辑,并将智能体响应传递给用户。 这种集成可以是您创建的应用或网站,也可以是现有服务(例如 Slack,Facebook Messenger),也可以是调用 Dialogflow 聊天机器人的脚本。
- 智能体:我们使用 Dialogflow 工具开发的聊天机器人称为智能体。 聊天机器人生成的响应称为智能体响应。
- 意图:这表示用户在其用户请求中尝试执行的操作。 用户输入的自然语言必须与意图相匹配,以确定针对任何特定请求要生成的响应类型。
- 实体:在用户请求中,用户有时可能会使用处理响应所需的单词或短语。 这些以实体的形式从用户请求中提取,然后按需使用。 例如,如果用户说“我在哪里可以买到芒果?” 聊天机器人应该提取芒果一词,以便搜索其可用的数据库或互联网以提出适当的响应。
- 上下文:要了解 Dialogflow 中的上下文,请考虑以下情形,在这种情况下,您无法与聊天机器人交谈来维护上下文:
您问您的聊天机器人“谁是主要角色? 印度大臣?” 并生成适当的响应。 接下来,您问您的聊天机器人“他几岁了?” 您的聊天机器人不知道“他的”是指谁。 因此,上下文是在聊天会话或会话的一部分上维护的会话状态,除非上下文被与聊天机器人的会话中的新事物所覆盖。 - 实现:实现是处理聊天机器人内业务逻辑的软件组件。 它是一个可以通过 Webhooks 访问的 API,可以接收有关传递给它的实体的输入,并生成响应,然后聊天机器人可以使用该响应来生成最终的智能体响应。
涵盖了 Dialogflow 的基本术语和工作流程之后,我们现在将构建一个基本的 Dialogflow 智能体,该智能体可以提供对用户请求的响应。
创建一个 Dialogflow 帐户
要开始使用 Dialogflow,您需要在 Dialogflow 网站上创建一个帐户。 为此,请按照下列步骤操作:
- 访问这里开始帐户创建过程。
您将需要一个 Google 帐户来创建 Dialogflow 帐户。 如果尚未创建一个,请访问这里。
- 在 Dialogflow 网站的主页上,单击“免费注册”以创建帐户,或单击“进入控制台”以打开 Dialogflow 控制台:

- 单击“使用 Google 登录”后,系统会要求您使用 Google 帐户登录。 您将被要求获得使用 Dialogflow 的帐户权限,然后接受条款和条件。
现在,我们可以开始创建 Dialogflow 智能体。
创建一个 Dialogflow 智能体
正如我们在“Dialogflow 如何工作”部分中讨论的那样,智能体是我们在 Dialogflow 平台中创建的聊天机器人。
成功创建帐户后,将显示 Dialogflow 控制台的登录屏幕,提示您创建智能体:
- 单击“创建智能体”提示。 您将被带到一个类似于以下内容的屏幕:

- 填写智能体的名称。 我们将其命名为
DemoBot。 - 将任何现有的 Google Project 链接到聊天机器人。 如果您还没有合格的 Google Project,则单击“创建”按钮时将创建一个新项目。
您需要在 Google Project 上启用结算功能才能创建 Dialogflow 聊天机器人。 要了解如何创建 Google Project,请访问这里。
了解 Dialogflow 控制台
Dialogflow 控制台是图形用户界面,用于管理聊天机器人,意图,实体以及 Dialogflow 提供的所有其他功能。
创建智能体后,您应该能够看到以下屏幕:

Dialogflow 控制台提示您创建一个新的意图。 让我们创建一个新的意图,该意图可以识别用户名并使用它为用户生成一个幸运数字。
创建一个意图并获取实体
现在,我们将创建一个意图,该意图从用户那里获取输入并确定用户名称。 然后,该意图提取名称的值并将其存储在一个实体中,该实体稍后将传递给 Webhook 进行处理。 请按照以下步骤操作:
-
单击屏幕右上方的“创建意图”按钮。 意向创建表单打开。
-
我们必须为该意图提供一个名称,例如
luckyNum。 然后,向下滚动到“训练短语”部分并添加一个训练短语:name is John。 -
抓住所需的实体,然后选择单词
John。 将出现一个下拉列表,将单词与任何预定义实体匹配。 我们将使用@sys.person实体获取名称并将其存储为userName参数,如以下屏幕截图所示:

- 向下滚动到“操作和参数”部分,并添加
userName参数,如以下屏幕快照所示:

- 现在,只要用户查询类似于名称的东西,就会将某些东西提取到
$userName变量中。 现在可以将其传递到 webhook 或 Firebase Cloud Function 以根据其值生成响应。
现在,让我们添加一个操作,以便可以通过 Google Assistant 访问 Dialogflow 智能体。
在 Google 上创建您的第一个动作
在 Google 上创建动作之前,让我们尝试了解什么是动作。 您可能听说过 Google 助手,它在本质上可以与 Siri 或 Cortana 媲美。 它围绕虚拟助手的概念构建,虚拟助手是一种软件,能够根据用户的指示以文本或语音形式为用户执行任务。
Google 助手可以执行的每个任务称为操作。 因此,当用户发出类似于“向我显示购物清单”或“打给 Sam 的请求”的请求时执行的任务是这样的动作,其中,函数showShoppingList()或makeCall(Sam)以适当的方式执行附加的参数。
Google 平台上的 Actions 使我们能够创建充当 Google Assistant 上的 Actions 的聊天机器人。 一旦调用,我们就可以进行对话,直到被用户结束为止。
调用操作是在 Google 助手中执行的,该助手将调用请求与其目录中的操作列表进行匹配,并启动适当的操作。 然后,用户接下来要做的几个动作就是与动作。 因此,Google 助手会充当多个此类操作的汇总器,并提供对其进行调用的方法。
您为什么是 Google Action?
Google 平台上的操作为有兴趣构建聊天机器人的开发人员提供了哪些商业利益? 考虑以下屏幕截图:

只需与 Google 助理交谈,用户便可以获取 Uber 选项。 这是因为“与 Uber 对话”调用与由 Uber 开发并通过 Google 平台上的“操作”提供的聊天机器人相匹配,该聊天机器人正在响应“与 Uber 对话”用户请求。
因此,Uber 通过提供无文本的界面(如果使用语音输入)来提高其可用性和交互性,并受益于 Google 助手中最先进的 NLP 算法,从而最终增强了其销量。
有效地将您创建的聊天机器人发布到 Google 的 Actions 上,可以为您的企业提供对话界面。 您可以使用 webhooks(我们将在本章稍后介绍)来管理业务逻辑。现在,让我们在 Google 上创建一个 Action 并将其链接到我们的聊天机器人。
在 Google 项目上创建操作
在本部分中,我们将在 Google Project 上创建一个 Actions,然后将其与 Google Assistant 应用集成。 这将使我们构建的聊天机器人可以通过 Google 助手应用访问,该助手在全球数十亿设备上都可用。
让我们从在 Google 项目上创建操作开始:
- 在浏览器中,打开这里,以打开 xGoogle 主页上的“操作”,您可以在其中阅读有关该平台的所有信息,并对其进行介绍。
- 要进入控制台,请单击开始构建或转到操作控制台按钮。 您将被带到 Google 控制台上的“操作”,系统将提示您创建一个项目。
- 在继续进行项目创建时,您将看到一个对话框,如以下屏幕截图所示:

您必须选择在其中创建 Dialogflow chatbot 智能体的同一 Google Project。
- 单击“导入项目”,将 Dialogflow 聊天机器人的操作添加到 Google 助手。 在加载的下一个屏幕上,选择“对话”模板以创建我们的操作。
- 然后,您将被带到 Google 控制台上的“操作”,如下所示:

在顶部栏上,您将看到内置 Action 的 Google Project 的项目 ID。在左侧垂直导航栏上,将列出所有不同步骤,您需要执行它们才能完成设置 Action。 在右侧的主要内容部分,提供了一个快速演练来设置您的第一个 Action。
- 单击“确定”如何调用操作。 您需要为您的操作提供唯一的调用字符串。 对于本章中的示例,我们使用了
Talk to Peter please调用。 您将需要选择稍微不同的调用。
成功设置调用后,演练将要求您添加一个动作。
- 单击“添加动作”链接以开始动作创建过程。
- 在出现的“创建操作”对话框中,在左侧列表中选择“自定义意图”,然后单击“构建”按钮。 这将带您回到 Dialogflow 界面。
现在,您需要在 Google 上启用“操作”才能访问您的聊天机器人的意图。
创建与 Google Assistant 的集成
默认情况下,您在 Dialogflow 控制台中构建的聊天机器人不允许 Google Actions 项目访问其中可用的意图。 通过执行以下步骤,我们可以启用对意图的访问:
- 在 Dialogflow 界面上,单击左侧导航窗格上的
Integrations按钮。 - 在加载的页面上,将为您提供 Dialogflow 支持的各种服务的集成选项,其中包括所有主要的社交聊天平台,以及 Amazon 的 Alexa 和 Microsoft 的 Cortana。
- 在屏幕上,您应该看到 Google 助手的“集成设置”按钮。 单击该按钮。 将打开一个对话框,如以下屏幕截图所示:

前面的屏幕快照中的对话框使您可以快速定义 Dialogflow 智能体与 Google 项目中的操作之间的集成设置。
- 在“默认调用”下,将“默认欢迎意图”设置为当用户开始通过 Google Assistant 与您的聊天机器人进行交互时将首先运行的意图。
- 在隐式调用中,指定我们之前创建的
luckyNum意图。 这将用于为用户生成幸运数字。 - 启用自动预览更改是个好主意,因为它使您可以将集成设置自动传播到 Google Console 上的“操作”和 Google Assistant 测试模拟器(我们将在稍后讨论),以便在为以下版本创建版本之前测试我们的应用。
现在,让我们为“默认欢迎意图”提供有意义的提示,以要求用户输入其名称,以便在用户做出响应时,其输入类似于luckyNum意图的训练短语,从而调用它:
- 单击“意图”按钮。 然后,单击“默认欢迎意图”。 向下滚动到“意图”编辑页面的“响应”部分,然后删除那里的所有响应。 由于
luckyNum意图希望用户说类似My name is XYZ的内容,因此合适的问题是What is your name?。 因此,我们将响应设置为Hi, what is your name?。
请注意,“响应”部分的选项卡式导航中有一个名为“Google 助手”的新导航栏。 这样,当我们从 Google Assistant 调用此意图时,我们可以为其指定其他响应。
- 单击选项卡,然后从默认选项卡中启用用户响应作为第一个响应。 我们这样做是因为我们不想在聊天机器人中指定特定于 Google 助手的其他响应。
- 向上滚动到“事件”部分,并检查它是否类似于以下屏幕截图:

- 如果缺少前面两个事件中的任何一个,则可以通过简单地键入它们并从出现的自动建议框中选择它们来包括它们。
- 单击 Dialogflow 控制台中间部分右上方的“保存”。
现在,我们准备创建我们的业务逻辑,以便为用户生成幸运数字。 首先,我们将为luckyNum意图创建一个 Webhook,然后将其部署到 Firebase 的 Cloud Functions 中。
实现 Webhook
在本节中,我们将为luckyNum意图启用 webhook,并为luckyNum意图的逻辑准备 webhook 代码。 请按照以下步骤操作:
- 打开
luckyNum意图的意图编辑页面,然后向下滚动到“实现”部分。 在这里,启用“为此意图启用 webhook 调用”选项。
现在,此意图将寻找从 webhook 生成的响应。
- 打开您选择的文本编辑器以创建用于 Webhook 的代码,使其使用 JavaScript 并在 Firebase 提供的 Node.js 平台上运行:
'use strict';
上一行确保我们使用 ECMAScript 5 中定义的一组编码标准,这些编码标准对 JavaScript 语言进行了一些有用的修改,从而使其更加安全并且减少了混乱。
- 使用
require函数将 JavaScript 中的模块导入到项目中。 包括actions-on-google模块和firebase-functions模块,因为脚本将部署到 Firebase:
// Import the Dialogflow module from the Actions on Google client library.
const {dialogflow} = require('actions-on-google');
// Import the firebase-functions package for deployment.
const functions = require('firebase-functions');
- 为我们构建的 Dialogflow 智能体实例化一个新的客户端对象:
// Instantiate the Dialogflow client.
const app = dialogflow({debug: true});
注意,这里的 Dialogflow 变量是actions-on-google模块的对象。
- 将 Webhook 响应的意图设置为
luckyNum,然后将其传递给conv变量:
app.intent('luckyNum', (conv, {userName}) => {
let name = userName.name;
conv.close('Your lucky number is: ' + name.length );
});
app变量保存正在处理的会话的状态信息以及我们从luckyNum意图中提取的userName参数。 然后,我们声明变量名称,并将其设置为userName变量的名称键。 这样做是因为userName变量是一个 JavaScript 对象。 您可以在右侧部分的“测试”控制台中通过为luckyNum意图(例如My name is Max)键入匹配的调用来查看此内容。
- 设置 Webhook,使其响应所有 HTTPS POST 请求,并通过 Firebase 将其导出为 Dialogflow 实现:
// Set the DialogflowApp object to handle the HTTPS POST request.
exports.dialogflowFirebaseFulfillment = functions.https.onRequest(app);
我们在本节中开发的脚本需要部署到服务器以使其响应。 我们将为 Firebase 使用 Cloud Functions 部署此脚本并将其用作聊天机器人的 webhook 端点。
将 Webhook 部署到 Firebase 的 Cloud Functions
既然我们已经完成了 Webhook 的逻辑创建,那么在 Firebase 上使用 Cloud Functions 部署它就非常简单。 请按照以下步骤操作:
- 单击 Dialogflow 控制台左侧导航上的
Fulfillment按钮。 使内联编辑器能够添加您的 Webhook 并将其直接部署到 Cloud Functions。
您必须清除内联编辑器中的默认样板代码才能执行此操作。
- 将上一部分中的编辑器中的代码粘贴到
index.js选项卡式导航丸中,然后单击Deploy。
请记住,用于部署的环境是 Node.js,因此index.js是包含所有业务逻辑的文件。 package.json文件管理您的项目所需的包。
使用 Cloud Functions 具有部署 Webhook 的简单性和最小化设置的优势。 另一方面,仅设置index.js的限制可防止您将 Webhook 逻辑拆分为多个文件,这通常是在大型 chatbot 应用中完成的。 现在,您准备为 Action 创建一个发行版。
在 Google 版本上创建动作
最后,我们处于可以在 Google chatbot 上为 Actions 创建发行版的阶段。 但在这样做之前,重要的是在 Google Assistant 测试模拟器中测试聊天机器人:
-
单击 Google 控制台上“操作”左侧导航窗格中的“模拟器”按钮,以进入模拟器。 在模拟器中,将显示一个类似于在手机上使用 Google Assistant 的界面。 建议的输入将包含您的操作的调用方法。
-
在模拟器中为您的操作输入调用,在本例中为
Talk to Peter Please。 这将产生来自默认欢迎意图要求您输入名称的输出。 输入您的姓名作为响应后,类似于My name is Sammy,您将看到您的幸运数字,如下所示:

现在我们知道我们的聊天机器人可以正常工作,并且可以与 Google 上的 Action 集成在一起,让我们为其创建一个发行版:
- 在 Google 控制台上的操作中单击“概述”,您将看到准备部署的提示。
- Actions 测试控制台要求您输入一些 Action 所需的信息。 这些通常是简短和长格式的说明,开发人员的详细信息,隐私策略,操作条款和条件以及徽标。 成功填写所有内容后,单击“保存”。
- 在“部署”类别下的左侧导航栏中单击“发布”,以打开“发布”页面。 在这里,选择
Alpha发布选项,然后单击Submit发布。
部署将需要几个小时才能完成。 部署完成后,您将能够在已登录到内置 Action 的 Google 帐户的任何设备上测试您的操作。成功创建并部署 Dialogflow 智能体后,我们现在将使用以下方法开发 Flutter 应用: 与智能体进行交互的能力。 单屏应用将具有与任何基本的移动聊天应用非常相似的用户界面,带有一个用于输入消息的文本框,这些消息是 Dialogflow 智能体的查询,还有一个将每个查询发送到智能体的发送按钮。 该屏幕还将包含一个列表视图,以显示来自用户的所有查询和来自智能体的响应。 另外,在“发送”按钮旁边将有一个麦克风选项,以便用户可以利用语音到文本功能将查询发送到智能体。
为对话应用创建 UI
我们将从使用一些硬编码文本为应用创建基本用户界面开始,以测试 UI 是否正确更新。 然后,我们将集成 Dialogflow 智能体,以便它可以回答查询并告诉用户他们的幸运数字,然后添加一个mic选项,以便我们可以利用语音转文本功能。
该应用的整体小部件树如下所示:

现在,让我们详细讨论每个小部件的实现。
创建文本控制器
首先,让我们在名为chat_screen.dart的新 dart 文件中创建一个名为ChatScreen的 StatefulWidget。 现在,请按照下列步骤操作:
- 创建一个文本框-用 Flutter 项
TextField-允许用户输入输入文本。 要创建TextField,我们需要定义createTextField():
Widget createTextField() {
return new Flexible(
child: new TextField(
decoration:
new InputDecoration.collapsed(hintText: "Enter your message"),
controller: _textController,
onSubmitted: _handleSubmitted,
),
);
}
当用户指示已完成将文本输入到文本字段中时,onSubmitted属性用作文本字段的回调,以处理文本输入。 当按下键盘上的Enter按钮时,将触发该属性。
在前面的TextField小部件中,当用户输入完文本后便会调用_handleSubmitted()。 稍后将详细描述_handleSubmitted()。
我们还将decoration属性指定为折叠状态,以删除可能出现在文本字段中的默认边框。 我们还将hintText属性指定为Enter your message。 要收听更改并更新TextField,我们还附加了TextEditingController的实例。 可以通过执行以下代码来创建实例:
final TextEditingController _textController = new TextEditingController();
与 Java 不同,Dart 没有诸如public,private或protected之类的关键字来定义变量的使用范围。 而是在标识符名称之前使用下划线_来指定该标识符是类专有的。
- 接下来,创建一个发送按钮,该按钮可用于向
createSendButton()函数内部的智能体发送查询:
Widget createSendButton() {
return new Container(
margin: const EdgeInsets.symmetric(horizontal: 4.0),
child: new IconButton(
icon: new Icon(Icons.send),
onPressed: () => _handleSubmitted(_textController.text),
),
);
}
在 Flutter 中,可以使用Icons类轻松添加类似于发送按钮的图形图标。 为此,我们创建一个新的Icon实例并指定Icons.send,以便将小部件用作发送按钮。 用作icon属性的参数。 我们还设置了onPressed属性,该属性在用户点击“发送”按钮时调用。 在这里,我们再次致电_handleSubmitted。
=>(有时称为箭头)是一种速记符号,用于定义包含一行的方法。 定义为fun() { return 10; }的方法可以写为fun() => return 10;。
- 文本字段和发送按钮应该并排显示,因此可以通过将它们作为子代添加到
Row小部件中来将它们包装在一行中。 包装好的Row小部件位于屏幕底部。 我们在_buildTextComposer()中创建此小部件:
Widget _buildTextComposer() {
return new IconTheme(
data: new IconThemeData(color: Colors.blue),
child: new Container(
margin: const EdgeInsets.symmetric(horizontal: 8.0),
child: new Row(
children: <Widget>[
createTextField(),
createSendButton(),
],
),
),
);
}
_buildTextComposer()函数返回一个以Container作为其子元素的IconTheme小部件。 容器包含由文本字段和我们在“步骤 1”和2中创建的发送按钮组成的Row小部件。
在下一节中,我们将构建ChatMessage小部件,该小部件用于显示用户与聊天机器人的交互。
创建ChatMessage
来自用户的查询和来自智能体的响应可以被视为单个组件的两个不同部分。 我们将为它们创建两个不同的容器,然后将它们添加到名为ChatMessage的单个单元中。 这样可以确保每个查询及其答案的显示顺序与用户输入的顺序相同。 我们将在一个名为chat_message.dart的新 dart 文件中创建一个名为ChatMessage的有状态小部件。 下图显示了ChatMesage的查询和响应划分:

要创建屏幕的 UI,请按照下列步骤操作:
- 创建一个包含一些文本的容器,该容器将在屏幕上显示用户输入的查询:
new Container(
margin: const EdgeInsets.only(top: 8.0),
child: new Text("Here is the query text",
style: TextStyle(
fontSize: 16.0,
color: Colors.black45,
),
),
)
我们从为容器提供8.0的上边距开始,该边距包含一个当用户输入查询时将显示的字符串。 当调用_handleSubmitted()时,我们会将这个硬编码的字符串修改为字符串参数。 我们还将fontSize属性的边距修改为16.0,并将颜色设置为black45(深灰色),以帮助用户区分查询和响应。
- 创建一个容器以显示响应字符串:
new Container(
margin: const EdgeInsets.only(top: 8.0),
child: new Text("This will be the response string",
style: TextStyle(
fontSize: 16.0
),
),
)
顶部边距属性为8.0的容器包含一个硬编码的响应字符串。 稍后将对其进行修改,使其可以适应用户的响应。
- 将两个容器包装在单个
Column中,然后将其作为有状态窗口小部件(即ChatMessage)中覆盖的build()方法的容器返回:
@override
Widget build(BuildContext context) {
return new Container(
margin: const EdgeInsets.symmetric(vertical: 10.0),
child: new Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
new Container(
margin: const EdgeInsets.only(top: 8.0),
child: new Text("Here is the query text",
style: TextStyle(
fontSize: 16.0,
color: Colors.black45,
),
),
),
new Container(
margin: const EdgeInsets.only(top: 8.0),
child: new Text("this will be the response text",
style: TextStyle(
fontSize: 16.0
),
),
)
]
)
);
在 Flutter 中,文本包装在Container中。 通常,当它们太长而无法水平放置在屏幕中时,它们往往会从屏幕上溢出。 这可以看成是屏幕角落的红色标记。 为避免文本溢出,请确保将Container和Text包裹在Flexible内,以便文本可以垂直占据可用空间并自行调整。
- 为了存储和显示所有字符串(查询和响应),我们将使用
ChatMessage类型的List:
final List<ChatMessage> _messages = <ChatMessage>[];
此列表应出现在我们先前创建的TextField上方,以接受用户输入。
- 为了确保字段以垂直顺序正确显示,我们需要将它们包装在列中,然后从
ChatScreen.dart的Widget build()方法返回它们。 该列的三个子级是一个灵活的列表视图,一个分隔符和一个带有文本字段的容器。 通过重写build()方法来创建 UI,如下所示:
@override
Widget build(BuildContext context) {
return new Column(
children: <Widget>[
new Flexible(
child: new ListView.builder(
padding: new EdgeInsets.all(8.0),
reverse: true,
itemBuilder: (_, int index) => _messages[index],
itemCount: _messages.length,
),
),
new Divider(
height: 1.0,
),
new Container(
decoration: new BoxDecoration(
color: Theme.of(context).cardColor,
),
child: _buildTextComposer(),
),
],
);
}
以ChatMessages作为其子元素的ListView被制作为Flexible,以便在放置分隔符和文本字段的容器之后,可以在垂直方向上占据屏幕上可用的整个空间。 在所有四个基本方向上都给8.0填充。 另外,将reverse属性设置为true可以使其在底部到顶部的方向上滚动。 itemBuilder属性被分配索引的当前值,以便它可以构建子项。 另外,为itemCount分配了一个值,该值可帮助列表视图正确估计最大可滚动内容。 列的第二个子级创建分隔符。 这是一条devicePixel粗水平线,标记了列表视图和文本字段的分隔。 在该列的最底部位置,我们将带有文本字段的容器作为其子容器。 这是通过对我们先前定义的_buildTextComposer()进行方法调用而构建的。
- 在
ChatScreen.dart方法内定义_handleSubmit(),以正确响应用户的“发送消息”操作:
void _handleSubmitted(String query) {
_textController.clear();
ChatMessage message = new ChatMessage(
query: query, response: "This is the response string",
);
setState(() {
_messages.insert(0, message);
});
}
方法的字符串参数包含用户输入的查询字符串的值。 该查询字符串以及一个硬编码的响应字符串用于创建ChatMessage的实例,并插入到_messages列表中。
- 在
ChatMessage中定义一个构造器,以便正确传递和初始化参数值,查询和响应:
final String query, response;
ChatMessage({this.query, this.response});
- 分别在
ChatMessages.dart中修改用于查询和响应的容器内Text属性的值,以使屏幕上显示的文本与用户和用户输入的文本相同。 从动作助手获得的回复:
//Modifying the query text
child: new Text(query,
style:.......
)
//Modify the response text
child: new Text(response,
style:.......
)
成功编译到目前为止我们编写的代码后,屏幕应如下所示:

在前面的屏幕截图中,您可以看到将由用户编写的虚拟查询文本以及来自聊天机器人的响应字符串。
整个chat_message.dart文件可以在 GitHub 上查看。
在下一节中,我们将集成 Dialogflow 智能体,以便我们对用户查询具有实时响应。
集成 Dialogflow 智能体
现在,我们已经为应用创建了一个非常基本的用户界面,我们将把 Dialogflow 智能体与应用集成在一起,以便该智能体实时响应用户输入的文本。 按着这些次序:
- 为了将 Dialogflow 集成到应用中,我们将使用名为
flutter_dialogflow的 Flutter 插件。
要浏览此插件,请转到这里。
将依赖项添加到pubspec.yaml文件中的插件:
dependencies:
flutter_dialogflow: ^0.1.0
- 接下来,我们需要安装依赖项。 可以使用
$ flutter pub get命令行参数,也可以通过单击屏幕上显示的选项来完成。 在这里,我们将使用dialogflow_v2,因此让我们将包导入chat_screen.dart文件中:
import 'package:flutter_dialogflow/dialogflow_v2.dart';
- 添加
.json文件,其中包含您在项目的控制台上创建 Dialogflow 智能体时下载的 GCP 凭据。 为此,创建一个assets文件夹并将文件放在其中:

- 将文件的路径添加到
pubspec.yaml文件的assets部分:
flutter:
uses-material-design: true
assets:
- assets/your_file_downloaded_google_cloud.json
- 修改
_handleSubmitted(),以便它可以与智能体进行通信并获得对用户输入的查询的响应:
Future _handleSubmitted(String query) async {
_textController.clear();
//Communicating with DailogFlow agent
AuthGoogle authGoogle = await AuthGoogle(fileJson: "assets/gcp-api.json").build();
Dialogflow dialogflow = Dialogflow(authGoogle: authGoogle,language: Language.english);
AIResponse response = await dialogflow.detectIntent(query);
String rsp = response.getMessage();
ChatMessage message = new ChatMessage(
query: query, response: rsp
);
setState(() {
_messages.insert(0, message);
});
}
首先,我们通过指定assets文件夹的路径来创建一个名为authGoogle的AuthGoogle实例。 接下来,我们创建Dialogflow智能体的实例,该实例指定 Google 认证实例以及用于与其通信的语言。 在这里,我们选择了英语。 然后使用response.getMessage()提取响应,并将其存储在rsp字符串变量中,然后在创建ChatMessage实例时传递该变量,以确保两个字符串(输入文本和响应)均在屏幕上正确更新。
以下屏幕快照显示了在进行上述修改以反映用户的实际查询和 Dialogflow 智能体的响应之后的应用:

在下一部分中,我们将向应用添加音频交互功能。
添加与助手的音频交互
现在,我们将语音识别添加到应用中,以便它可以监听用户的查询并采取相应的措施。
添加插件
我们将在此处使用speech_recognition插件。 让我们添加依赖项,如下所示:
- 将依赖项添加到
pubspec.yaml文件,如下所示:
dependencies:
speech_recognition: "^0.3.0"
- 通过运行以下命令行参数来获取包:
flutter packages get
- 现在,由于我们正在使用设备的麦克风,因此我们需要征得用户的许可。 为此,我们需要添加以下代码行:
在 iOS 上,权限是在infos.plist中指定的:
<key>NSMicrophoneUsageDescription</key>
<string>This application needs to access your microphone</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>This application needs the speech recognition permission</string>
在 Android 上,权限在AndroidManifest.xml文件中指定:
<uses-permission android:name="android.permission.RECORD_AUDIO" />
- 现在,我们准备将包导入到
chat_screen.dart文件中,以便可以使用它:
import 'package:speech_recognition/speech_recognition.dart';
在下一节中,我们将添加将利用speech_recognition插件来帮助进行音频交互的方法。
添加语音识别
添加speech_recognition插件并导入包后,我们都准备在我们的应用中使用它。 让我们从添加将在应用内部处理语音识别的方法开始,如下所示:
- 添加并初始化所需的变量:
SpeechRecognition _speechRecognition;
bool _isAvailable = false;
bool _isListening = false;
String transcription = '';
_speechRecognition是SpeechRecognition的实例。 _isAvailable很重要,因为它可以让平台(Android/iOS)知道我们正在与之交互,并且_isListening将用于检查应用当前是否正在监听麦克风。
最初,我们将两个boolean变量的值都设置为false。 transcription是一个字符串变量,将用于存储已监听的字符串。
- 定义
activateSpeechRecognizer()方法以设置音频操作:
void activateSpeechRecognizer() {
_speechRecognition = SpeechRecognition();
_speechRecognition.setAvailabilityHandler((bool result)
=> setState(() => _isAvailable = result));
_speechRecognition.setRecognitionStartedHandler(()
=> setState(() => _isListening = true));
_speechRecognition.setRecognitionResultHandler((String text)
=> setState(() => transcription = text));
_speechRecognition.setRecognitionCompleteHandler(()
=> setState(() => _isListening = false));
}
在前面的代码片段中,我们在_speechRecognition内部初始化了SpeechRecognition的实例。 然后,我们通过调用_speechRecognition.setAvailabilityHandler()回调函数来设置AvailabilityHandler,该回调函数需要传回可以分配给_isAvailable的boolean结果。 接下来,我们设置RecognitionStartedHandler,它在启动语音识别服务时执行,并将_isListening设置为true表示移动设备的麦克风当前处于活动状态并且正在监听。 然后,我们使用setRecognitionResultHandler设置RecognitionResultHandler,这将给我们返回生成的文本。 这存储在字符串转录中。 最后,我们设置RecognitionCompleteHandler,当麦克风停止收听时,将_isListening设置为false。
- 公开内部的
initState()函数调用activateSpeechRecognizer()来设置_speechRecognition实例,如下所示:
@override
void initState(){
super.initState();
activateSpeechRecognizer();
}
此时,该应用能够识别音频并将其转换为文本。 现在,我们将增强 UI,以便用户可以提供音频作为输入。
添加麦克风按钮
现在,我们已经激活了语音识别器,我们将在发送按钮旁边添加一个麦克风图标,以允许用户利用该选项进行语音识别。 请按照以下步骤操作:
- 首先,我们定义
createMicButton()函数,该函数作为第三个子项添加到_buildTextComposer()内部的Row小部件中:
Widget createMicButton() {
return new Container(
margin: const EdgeInsets.symmetric(horizontal: 4.0),
child: new IconButton(
icon: new Icon(Icons.mic),
onPressed: () {
if (_isAvailable && !_isListening) {
_speechRecognition.recognitionStartedHandler();
_speechRecognition .listen(locale: "en_US")
.then((transcription) => print('$transcription'));
} else if (_isListening) {
_isListening = false;
transcription = '';
_handleSubmitted(transcription);
_speechRecognition
.stop()
.then((result) => setState(() => _isListening = result));
}
}
),
);
}
在前面的代码片段中,我们返回带有子项IconButton的Container,其子项为Icons.mic。 我们为使用onPressed()的按钮提供了双重功能,以便它可以开始收听用户的声音,并且再次按下该按钮时,可以通过传递记录的字符串以与智能体进行交互来停止记录并调用_handleSubmitted()方法。
首先,我们使用_isAvailable和_isListening变量检查麦克风是否可用并且尚未在收听用户的声音。 如果if语句中的条件为true,则将_isListening的值设置为true。 然后,我们通过调用_speechRecognition上的.listen()方法开始监听。 locale参数指定语言,此处为en_US。 相应的字符串存储在transcription变量中。
当第二次按下麦克风停止录制时,由于_isListening的值设置为true,因此if条件将不满足。 现在,执行else块。 在这里,通过传递记录的字符串以使其可以与智能体进行交互来调用_handleSubmitted(),然后使用结果将_isListening的值设置为true:

成功编译所有代码并将ChatScreen包裹在main.dart文件中的MaterialApp实例中之后,该应用的外观将与前面的屏幕快照类似。
可以在这个页面和这个页面上查看chat_screen.dart文件。
总结
在本章中,我们研究了一些可用于创建聊天机器人的最常用工具,然后对 Dialogflow 进行了深入讨论,以了解所使用的基本术语。 我们了解了 Dialogflow 控制台的工作方式,以便我们可以创建自己的 Dialogflow 智能体。 为此,我们创建了一个意图,该意图可以提取用户的姓名并将其添加为与 Google Assistant 的集成,从而可以用幸运数字进行响应。
在将 Webhook 部署为 Firebase 的 Cloud Functions 并在 Google 版本上创建 Actions 之后,我们创建了一个对话式 Flutter 应用。 我们学习了如何创建对话应用界面,并集成了 Dialogflow 智能体以根据聊天机器人的响应促进深度学习模型。 最后,我们使用 Flutter 插件向应用添加语音识别,该应用再次使用基于深度学习的模型将语音转换为文本。
在下一章中,我们将研究定义和部署自己的自定义深度学习模型并将其集成到移动应用中。
四、认识植物种类
该项目将深入讨论如何构建自定义的 TensorFlow Lite 模型,该模型能够从图像中识别植物物种。 该模型将在移动设备上运行,并将主要用于识别不同的植物物种。 该模型使用在 TensorFlow 的 Keras API 上开发的深层卷积神经网络(CNN)进行图像处理。 本章还向您介绍了如何使用基于云的 API 来执行图像处理。 以 Google Cloud Platform(GCP)提供的 Cloud Vision API 为例。
在本章结束时,您将了解基于云的服务对于深度学习(DL)应用的重要性,设备模型对脱机执行的好处,以及移动设备上的即时深度学习任务。
在本章中,我们将介绍以下主题:
- 图像分类简介
- 了解项目架构
- Cloud Vision API 简介
- 配置 Cloud Vision API 进行图像识别
- 使用软件开发套件(SDK)/工具来建立模型
- 创建用于图像识别的自定义 TensorFlow Lite 模型
- 创建 Flutter 应用
- 运行图像识别
技术要求
本章的技术先决条件如下:
- 具有 Python 3.6 及更高版本的 Anaconda
- TensorFlow 2.0
- 启用了结算功能的 GCP 帐户
- Flutter
图像分类简介
图像分类是当今人工智能(AI)的主要应用领域。 我们可以在我们周围的许多地方找到图像分类的实例,例如手机的面部解锁,对象识别,光学字符识别,照片中人物的标记等等。 从人的角度来看,这些任务看起来很简单,但对于计算机而言却并不那么简单。 首先,系统必须从图像中识别出物体或人,并在其周围绘制一个边界框,然后进行分类。 这两个步骤都是计算密集型的,很难在计算机上执行。
研究人员每天都在努力克服图像处理中的若干挑战,例如戴眼镜或新留胡子的人的脸部识别,在拥挤的地方通过脸部识别和跟踪多个人,以及新样式的字符识别。 手写或全新的语言。 深度学习一直以来都是克服这些挑战的好工具,它能够学习图像中的几种不可见图案。
深度学习中用于图像处理的一种非常常见的方法是部署 CNN,我们已经在前面的章节中进行了介绍。 要查看其概念和基本工作,请参阅“第 2 章”,“移动视觉–使用设备上模型的人脸检测”。 在这个项目中,我们将介绍如何将这些模型转换为可以在移动设备上高效运行的压缩模型。
您可能想知道我们将如何构建这些模型。 为了简化语法,对 TensorFlow API 的强大支持以及广泛的技术支持社区,我们将使用 Python 构建这些模型。 很明显,您的开发计算机上将需要 Python 运行时,但对于该项目,我们将选择一种更快,更强大的选项-Google 的 Colaboratory 环境。 Colaboratory(或简称为 Colab)为即时可用的运行时提供了几个重要的机器学习(ML)以及与运行时预装的数据科学相关的模块。 另外,Colaboratory 还为启用图形处理器(GPU)和张量处理单元(TPU)的运行时提供支持。 训练深度学习模型可谓小菜一碟。 然后,我们将直接在设备上部署 TensorFlow Lite 模型,这是一种适合快速运行且不需要定期更新的模型的良好做法。
让我们开始了解项目架构。
了解项目架构
我们将在本章中构建的项目将包括以下技术:
- TensorFlow:使用 CNN 构建分类模型
- TensorFlow Lite:一种浓缩 TensorFlow 模型的格式,可以在移动设备上高效运行
- Flutter:跨平台应用的开发库
您可以通过访问前面的链接来了解这些技术。 以下屏幕快照给出了这些技术在该项目中发挥作用的框图:

首先,我们将在包含数百张图像的数据集上训练分类模型。 为此,我们将使用 Python 构建 TensorFlow 模型。 然后,必须以的格式保存模型。 tflite,是 TensorFlow Lite 模型的扩展。 后端到此结束,我们切换到前端。
在前端,我们首先使用 Flutter 构建一个应用,该应用可以从设备上存在的图库中加载图像。 Firebase 上的预测模型已下载并缓存到设备上。 从图库中选择的图像将传递到模型,该模型将预测包含图像中显示的植物物种名称的标签。 模型存储在移动设备上,即使离线也可以使用模型。
设备上模型是在移动应用上使用深度学习的强大且首选的方式。 如今,普通人的手机上有几种应用使用设备上的模型来为其应用带来智能。 设备上模型通常是在桌面上开发的模型的压缩形式,并且可能会或可能不会编译为字节码。 诸如 TensorFlow Lite 之类的框架在上执行特殊的优化。 tflite 模型,使其比非移动形式的体积更小,运行更快。
但是,在我们开始为任务构建定制模型之前,让我们全面了解一下我们可以使用哪些预先存在的工具或服务来执行此类任务。
介绍 Cloud Vision API
Cloud Vision API 是 GCP 套件中流行的 API。 它已成为使用计算机视觉构建应用的基准服务。 简而言之,计算机视觉是计算机识别图像中实体的能力,范围从人脸到道路和自动驾驶任务的车辆。 此外,计算机视觉可用于使人类视觉系统执行的任务自动化,例如计算道路上行驶中的车辆的数量,以及观察物理环境的变化。 计算机视觉已在以下领域得到广泛应用:
- 在社交媒体平台上标记公认的人脸
- 从图像中提取文本
- 从图像中识别物体
- 自动驾驶汽车
- 基于医学图像的预测
- 反向图像搜索
- 地标检测
- 名人识别
通过 Cloud Vision API,可以轻松访问前面的某些任务,并为每个识别的实体返回标签。 例如,我们可以看到在下面的屏幕截图中,正确识别了具有 200 年历史的著名工程学杰作 Howrah Bridge。 根据有关地标的信息,可以预测该图像属于加尔各答市:

至于前面截图的标签,最主要的标签是桥和悬索桥,它们都与桥有关。 如前面的屏幕截图所示,还可以通过单击“响应”部分中的“文本”选项卡来检查图像中是否有任何可识别的文本。 要检查图像是否适合安全搜索或单击其中是否有干扰内容的内容,请单击“安全搜索”选项卡。 例如,从著名名人那里接到电话的图像很可能是欺骗,如以下屏幕快照所示:

接下来,我们将从设置 GCP 帐户开始,然后继续创建用于使用 API的示例 Flutter 应用。
为图像识别配置 Cloud Vision API
在本节中,我们将准备通过 Flutter 应用使用 Cloud Vision API。 必须为此任务拥有一个 Google 帐户,我们假设您已经拥有该帐户。 否则,您可以通过以下链接注册免费创建 Google 帐户。
如果您目前拥有 Google 帐户,请继续进行下一部分。
启用 Cloud Vision API
要创建 GCP 帐户,请转到以下链接。 初始注册后,您将能够看到类似于以下屏幕截图的仪表板:

在左上角,您将能够看到三栏菜单,该菜单会列出 GCP 上所有可用的服务和产品的列表。 项目名称显示在搜索栏的左侧。 确保您为该项目创建并启用计费功能,以便本章进一步介绍。 在右侧,您可以看到用户个人资料信息,通知和 Google Cloud Shell 调用图标。 仪表板中心显示当前用户正在运行的服务的各种日志和统计信息。
为了访问 Cloud Vision API 并使用它,我们首先需要为项目启用它并为服务创建 API 密钥。 为此,请执行以下步骤:
- 点击左上方的汉堡菜单图标。 这将弹出一个菜单,类似于以下屏幕快照中所示的菜单:

- 单击“API 和服务”选项。 这将打开 API 仪表板,其中显示了与项目中启用的 API 相关的统计信息。
- 单击“启用 API 和服务”按钮。
- 在出现的搜索框中,键入
Cloud Vision API。 - 单击相关的搜索结果。 该 API 供应商将列为 Google。
- API 页面打开后,单击“启用”。 之后,应该显示一个图标,表明您已启用此 API,并且“启用”按钮变为“管理”。
为了能够使用 Cloud Vision API,您必须为此服务创建一个 API 密钥。 我们将在下一部分中进行此操作。
创建 Cloud Vision API 密钥
现在,您必须创建一个 API 密钥来访问 API 并从中获取响应。 为此,请执行以下步骤:
- 再次打开左侧的导航菜单,并将鼠标悬停在“API 和服务”菜单项上。 出现一个子菜单-单击“凭据”。
- 单击“创建凭据”按钮。 在显示的下拉菜单中,选择 API 密钥,如以下屏幕截图所示:

- API 密钥已创建。 在调用 Cloud Vision API 时,您将需要此 API 密钥。
API 密钥方法仅适用于 GCP 的部分选定 API 和服务,并非十分安全。 如果要完全访问所有 API 和服务以及细粒度的安全性,则需要对服务帐户使用该方法。 为此,您可以阅读 GCP 官方文档中的以下文章。
有了 API 密钥,您现在就可以通过 Flutter 应用进行 API 调用了。 在下一部分中,我们将在 Colab 上开发预测模型,并将其保存为.tflite模型。
使用 SDK /工具构建模型
我们介绍了针对现有任务使用预先存在的基于服务的深度学习模型的准备工作,以预测图片中存在的植物种类。 我们将在来自五种不同花的样本上训练图像分类器模型。 然后,模型将尝试确定花朵的任何图像可能所属的物种。 但是,此类模型通常在通常可用的数据集上进行训练,并且有时不具备特定的要求(例如,在科学实验室中)。 因此,您必须学习如何建立自己的模型来预测植物种类。
这可以通过完全从头训练模型或通过扩展先前存在的模型来实现。 从头开始完全训练模型的好处是,您可以完全控制输入到模型中的数据,以及训练过程中对模型所做的任何学习。 但是,如果以这种方式设计模型,则可能会出现缓慢或偏差。 TensorFlow 团队扩展了诸如 MobileNet 模型之类的预训练模型,其优点是速度超快。 该方法的缺点是它可能不如从头开始构建的模型那样准确,但是时间准确率的折衷使 MobileNet 模型更适合在移动设备上运行。
偏差是机器学习模型中非常关键的问题。 在统计术语中,这种偏差(或抽样偏差)是指数据集中的偏斜,即对于数据集中的每个分类类别,其样本数均相等。 这样的类别将获得较少的训练样本,因此很有可能被模型的输出预测所忽略。 偏见模型的一个很好的例子可能是仅在小孩脸上训练的面部识别模型。 该模型可能完全无法识别成年人或老年人的面孔。
您可以在汗学院(Khan Academy)的以下课程中了解有关识别样本偏差的更多信息。
因此,在接下来的部分中,我们将使用 MobileNet 模型来实现在移动设备上快速执行的功能。 为此,我们将使用 TensorFlow 的 Keras API。 用于该任务的语言是 Python,如前所述,它最能涵盖 TensorFlow 框架的功能。 我们假定您在接下来的部分中具有 Python 的基本工作知识。 但是,重要的是要了解 TensorFlow 和 Keras 在此项目中如何协同工作。
我们将在协作环境中工作。 让我们从了解该工具开始。
Google Colab 介绍
Google 提供的协作工具允许用户在公司提供的计算资源上运行类似笔记本的运行时,并可以选择免费使用 GPU 和 TPU,只要用户需要即可。 运行时预装了几个与 ML 和数据科学相关的 Python 模块。 Colaboratory 中的笔记本电脑都可以直接从代码内访问 GCP API(具有适当的配置)。 每个笔记本电脑都有自己的临时存储空间,当断开运行时时,该存储空间将被销毁。 同样,可以将 Colaboratory 笔记本与 GitHub 同步,从而实现最新的版本控制。 通常,协作笔记本位于用户的 Google 云端硬盘存储中。 它们可以与多个用户实时共享和一起工作。
要打开合作实验室,请转到以下链接。
您将获得一个样本,欢迎笔记本。 随意浏览欢迎笔记本,以基本了解 Colaboratory 的工作方式。 在笔记本电脑的左侧,您将能够看到导航选项卡药丸,如以下屏幕截图所示:

“目录”选项卡显示笔记本中创建的标题和子标题,并使用 Markdown 格式进行声明。 “代码片段”选项卡提供了快速单击并插入代码片段的功能,以用于 Colaboratory 上的某些常用功能。 如果您对协作实验室不是很熟悉,但希望执行特定任务,则可能需要在此处搜索任务。 第三个选项卡“文件”是分配给此笔记本的存储空间。 此处存储的文件是此笔记本的专用文件,不会在其他任何地方显示。 使用脚本下载或脚本创建的所有文件都存储在此处。 您可以使用此屏幕上的文件管理器来浏览笔记本的整个目录结构。
在右侧,主要内容部分是笔记本本身。 为了熟悉 Colaboratory 和 Notebooks 的使用,我们强烈建议您阅读以下文章。
创建用于图像识别的自定义 TensorFlow Lite 模型
一旦您在 Colaboratory 取得了不错的成绩,我们所有人都将建立自定义的 TensorFlow Lite 模型,用于识别植物物种的任务。 为此,我们将从新的协作笔记本开始并执行以下步骤:
- 导入项目所需的模块。 首先,我们导入 TensorFlow 和 NumPy。 NumPy 对于处理图像数组很有用,而 TensorFlow 将用于构建 CNN。 可以在以下片段中看到导入模块的代码:
!pip install tf-nightly-gpu-2.0-preview
import tensorflow as tf
import numpy as np
import os
注意第一行中使用的!pip install <package-name>命令。 这用于在正在运行的 Colaboratory 笔记本中安装包,在这种情况下,该笔记本将安装最新的 TensorFlow 版本,该版本内部实现了 Keras 库,该库将用于构建 CNN。
您可以在以下位置阅读有关使用!pip install命令以及其他将新库导入并安装到您的 Colaboratory 运行时的方法的更多信息。
- 要运行代码单元,请按住
Shift键并按Enter。 TensorFlow 版本的下载和安装进度显示在您执行代码的单元下方。这将需要几秒钟,之后您会收到类似于Successfully installed <package_name>, <package_name>, ...的消息。 - 最后,我们需要
os模块来处理文件系统上的文件。 - 下载数据集并提取图像。
现在,我们将从可用的统一资源定位器(URL)下载数据集,并将其提取到名为/content/flower_photos的文件夹中,如以下代码块所示:
_URL = "https://storage.googleapis.com/download.tensorflow.org/example_images/flower_photos.tgz"
zip_file = tf.keras.utils.get_file(origin=_URL,
fname="flower_photos.tgz",
extract=True, cache_subdir='/content',)
base_dir = os.path.join(os.path.dirname(zip_file), 'flower_photos')
您可以使用左侧面板上的“文件”选项卡浏览提取的文件夹的内容。 您会发现该文件夹还包含五个其他文件夹,其名称分别为:雏菊,蒲公英,玫瑰,向日葵和郁金香。 这些将是花朵的种类,我们将在其上训练我们的模型,此后称为标签。 下一步,我们将再次讨论这些文件夹名称。
- 下一步是设置生成器,以将数据传递到基于 TensorFlow 的 Keras 模型。
- 现在,我们将创建两个生成器函数,用于将数据输入 Keras 神经网络。 Keras 的
ImageDataGenerator类提供了两个工具函数,可通过使用flow_from_directory方法读取磁盘或通过使用flow_from_dataframe方法将图像转换为 NumPy 数组来将数据馈送到 Python 程序。 在这里,我们将使用flow_from_directory方法,因为我们已经有一个包含图像的文件夹。
但是,在此必须注意,包含图像的文件夹名称与图像所属的标签相同是故意的。 这是flow_from_directory方法要求其才能正常运行的文件夹结构的设计。 您可以在此处阅读有关此方法的更多信息。
可以使用以下屏幕快照中显示的目录树来对此进行总结:

- 然后,我们创建
ImageDataGenerator类的对象,并使用它为训练数据集创建生成器,如以下代码块所示:
IMAGE_SIZE = 224
BATCH_SIZE = 64
datagen = tf.keras.preprocessing.image.ImageDataGenerator(
rescale=1./255,
validation_split=0.2)
train_generator = datagen.flow_from_directory(
base_dir,
target_size=(IMAGE_SIZE, IMAGE_SIZE),
batch_size=BATCH_SIZE,
subset='training')
datagen对象采用两个参数-rescale和validation_split。 rescale参数告诉对象将所有黑白图像转换为0到255的范围,就像红色,绿色和蓝色(RGB)的规模,因为 MobileNet 模型已经在 RGB 图像上进行了训练。 validation_split参数从数据集中分配 20%(0.2 x 100)的图像作为验证集。 但是,我们也需要为验证集创建一个生成器,就像我们为训练集所做的那样。
训练集生成器train_generator接受target_size和batch_size参数以及其他参数。 target_size参数设置要生成的图像的尺寸。 这样做是为了与 MobileNet 模型中的图像尺寸匹配。 batch_size参数指示单个批量应生成多少个图像。
- 对于验证集,我们具有生成器,如以下代码块所示:
val_generator = datagen.flow_from_directory(
base_dir,
target_size=(IMAGE_SIZE, IMAGE_SIZE),
batch_size=BATCH_SIZE,
subset='validation')
- 让我们快速看一下这些生成器生成的数据的形状,如下所示:
for image_batch, label_batch in train_generator:
break
image_batch.shape, label_batch.shape
这将产生以下输出:((64, 224, 224, 3), (64, 5)),这意味着在第一批train_generator中,创建了尺寸为224 x 224 x 3的 64 个图像,以及 5 个单编码格式的 64 个标签。
- 可以通过运行以下代码来获取分配给每个标签的编码索引:
print(train_generator.class_indices)
这将产生以下输出:{'daisy': 0, 'dandelion': 1, 'roses': 2, 'sunflowers': 3, 'tulips': 4}。 请注意标签名称的字母顺序。
- 现在,我们将保存这些标签,以备将来在 Flutter 应用中部署模型时使用,如下所示:
labels = '\n'.join(sorted(train_generator.class_indices.keys()))
with open('labels.txt', 'w') as f:
f.write(labels)
- 接下来,我们将创建一个基本模型并冻结层。 在这一步中,我们将首先创建一个基础模型,然后冻结除最后一层之外的所有模型层,如下所示:
IMG_SHAPE = (IMAGE_SIZE, IMAGE_SIZE, 3)
base_model = tf.keras.applications.MobileNetV2(input_shape=IMG_SHAPE,
include_top=False,
weights='imagenet')
通过导入 TensorFlow 团队提供的MobileNetV2模型来创建基本模型。 输入形状设置为(64、64、3),然后导入 ImageNet 数据集中的权重。 该模型可能在您的系统上不存在,在这种情况下,将从外部资源下载该模型。
- 然后,我们冻结基本模型,以使
MobileNetV2模型中的权重不受未来训练的影响,如下所示:
base_model.trainable = False
- 现在,我们将创建一个扩展的 CNN,并扩展基础模型以在基础模型层之后添加另一个层,如下所示:
model = tf.keras.Sequential([
base_model,
tf.keras.layers.Conv2D(32, 3, activation='relu'),
tf.keras.layers.Dropout(0.2),
tf.keras.layers.GlobalAveragePooling2D(),
tf.keras.layers.Dense(5, activation='softmax')
])
我们创建了一个扩展基础模型的顺序模型,这实质上意味着数据是在单层连续地在连续层之间传递的,一次是一层。 我们还添加了具有relu激活函数的 2D 卷积层,然后是Dropout层,然后是Pooling层。 最后,添加带有softmax激活的输出层。
- 然后,必须对模型进行编译以对其进行训练,如下所示:
model.compile(optimizer=tf.keras.optimizers.Adam(),
loss='categorical_crossentropy',
metrics=['accuracy'])
我们将损失设置为分类交叉熵,将模型评估指标设置为预测的准确率。 已经发现Softmax在分类交叉熵作为损失函数时表现最佳,因此是首选。
- 训练并保存模型。 最终,我们处于 ML 最激动人心的步骤之一-训练。 运行以下代码:
epochs = 10
history = model.fit(train_generator,
epochs=epochs,
validation_data=val_generator)
该模型经过 10 个周期的训练,这意味着每个样本至少要在神经网络上抛出 10 次。 注意在此函数中使用了train_generator和val_generator。 即使有 12GB+ 的 RAM 和 TPU 加速可用,训练也需要花费相当长的时间(这在任何个人中端设备上都是过大的)。 您将能够观察到运行上述代码的单元下方的训练日志。
- 然后,我们可以保存模型,之后可以继续转换保存的模型文件,如下所示:
saved_model_dir = ''
tf.saved_model.save(model, saved_model_dir)
- 将模型文件转换并下载到 TensorFlow Lite。 现在,我们可以使用以下代码转换保存的模型文件。 这会将模型另存为
model.tflite文件,如下所示:
converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir)
tflite_model = converter.convert()
with open('model.tflite', 'wb') as f:
f.write(tflite_model)
- 现在,我们需要下载此文件,以将其嵌入到我们构建的 Flutter 应用中。 我们可以使用以下代码进行操作:
from google.colab import files
files.download('model.tflite')
files.download('labels.txt')
注意,我们使用了google.colab库中的files模块。 我们还下载了在“步骤 11”中创建的labels.txt文件。
现在,我们准备开始创建 Flutter 应用,以演示 Cloud Vision API 的用法以及嵌入式 TensorFlow Lite 模型的用法。
创建 Flutter 应用
成功创建可识别多种植物物种的 TensorFlow Lite 模型后,现在让我们创建一个 Flutter 应用,以在移动设备上运行 TensorFlow Lite 模型。 该应用将有两个屏幕。 第一个屏幕将包含两个按钮,供用户在两个不同的模型(Cloud Vision API 和 TensorFlow Lite 模型)之间进行选择,这些模型可用于对任何选定的图像进行预测。 第二个屏幕将包含一个浮动操作按钮(FAB),使用户可以从设备的库中选择图像,一个图像视图来显示用户选择的图像,以及一个文本来使用所选模型显示预测。
以下屏幕截图说明了应用的流程:

现在,让我们看一下构建应用的步骤。
在两个不同的模型之间进行选择
让我们从创建应用的第一个屏幕开始。 第一个屏幕将包含两个不同的按钮,使用户可以在 Cloud Vision API 和 TensorFlow Lite 模型之间进行选择。
首先,我们创建一个新的choose_a_model.dart文件,其中将包含ChooseModel有状态的小部件。 该文件将包含用于创建应用第一个屏幕的代码,其中包含带有一些文本和两个凸起按钮的列,如以下屏幕截图所示:

创建应用的第一个屏幕的步骤如下:
- 首先,我们将定义一些全局字符串变量,这些变量稍后将在创建用于选择模型的按钮以及保存用户选择的模型时使用,如下所示:
var str_cloud = 'Cloud Vision API';
var str_tensor = 'TensorFlow Lite';
- 现在,让我们定义一个方法来创建一个简单的
Text小部件,如下所示:
Widget buildRowTitle(BuildContext context, String title) {
return Center(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 16.0),
child: Text(
title,
style: Theme.of(context).textTheme.headline,
),
),
);
}
该方法返回一个小部件,该小部件与中心对齐,并包含一些带有title值的文本作为参数传递,并带有标题为主题的“选择模型”字符串。 使用EdgeInsets.symmetric()属性和EdgeInsets.symmetric()属性,还为文本提供了水平和垂直填充。
- 接下来,我们将定义用于创建按钮的
createButton()方法,如下所示:
Widget createButton(String chosenModel) {
return (RaisedButton(
color: Colors.blue,
textColor: Colors.white,
splashColor: Colors.blueGrey,
child: new Text(chosenModel),
onPressed: () {
var a = (chosenModel == str_cloud ? 0 : 1);
Navigator.push(
context,
new MaterialPageRoute(
builder: (context) => PlantSpeciesRecognition(a)
),
);
}
)
);
}
该方法返回RaisedButton方法,其颜色为blue,textColor值为white,splashColor值为blueGrey。 该按钮具有一个Text子元素,该子元素是使用chosenModel中传递的值构建的。 如果用户单击了“运行 Cloud Vision API”的按钮,则chosenModel的值将为 Cloud Vision API,并且如果单击TensorFlow Lite的按钮,则其值为 TensorFlow Lite。
当按下按钮时,我们首先检查chosenModel中的值。 如果与str_cloud相同(即 Cloud Vision API),则分配给变量a的值为0; 否则,分配给变量a的值为1。 该值与使用Navigator.push()迁移到PlantSpeciesRecognition一起传递,这将在后面的部分中进行介绍。
- 最后,我们创建第一个屏幕的
appBar和主体,并从build()方法返回Scaffold,如下所示:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text('Plant Species Recognition'),
),
body: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
buildRowTitle(context, 'Choose Model'),
createButton(str_cloud),
createButton(str_tensor),
],
)
)
);
}
appBar包含位于中间的Plant Species Recognition标题。 Scaffold的主体是一列,其中包含一些文本和两个按钮,其值分别为str_cloud和str_tensor,并与中心对齐。
创建第二个屏幕
当用户选择了模型时,应用将迁移到第二个屏幕,该屏幕将允许用户从设备的本地存储中选择一个图像,然后在该图像上运行所选模型以进行预测。 我们从创建一个新文件plant_species_recognition.dart开始,该文件包含PlantSpeciesRecognition有状态的小部件。
创建用户界面
我们将首先创建一个新文件PlantSpeciesRecognition.dart,其中包含一个名为PlantSpeciesRecognition的有状态小部件,然后将覆盖其build()方法以放置用户界面(UI)的应用组件:
- 让我们创建一个带有 FAB 的
Scaffold和一个带有build()方法返回的应用标题的AppBar。 FAB 允许用户从设备的图库中选择图像,以预测图像中包含的植物种类,如下所示:
return Scaffold(
appBar: AppBar(
title: const Text('Plant Species Recognition'),
),
floatingActionButton: FloatingActionButton(
onPressed: chooseImageGallery,
tooltip: 'Pick Image',
child: Icon(Icons.image),
),
);
在前面的代码片段中,AppBar将包含Plant Species Recognition文本。 这将作为应用的标题显示在屏幕顶部的应用栏上。
在 Flutter 中,const关键字有助于冻结对象的状态。 描述为const的对象的完整状态是在应用本身的编译期间确定的,并且保持不变。 同样,当与Text()之类的构造器一起使用时,该关键字对于小型内存优化也很有用。 在代码中添加第二个Text()构造器会重用为第一个Text()构造器分配的内存,从而重用内存空间并使应用更快。
接下来,我们通过指定FloatingActionButton类并传递所需的参数来添加floatingActionButton属性。
FloatingActionButtons是圆形按钮,它们悬停在屏幕内容的顶部。 通常,一个屏幕应该包含一个位于右下角的 FAB,并且不受内容滚动的影响。
onPressed被添加到chooseImageGallery,按下该按钮将被调用。 接下来,我们添加tooltip属性,其String值为'Pick Image',描述按钮将执行的操作。 最后,我们将Icon(Icons.image)添加为child,将材质图标图像放置在 FAB 的顶部。
添加功能
现在,让我们添加功能,以允许用户从设备的图库中选择图像。 我们将使用image_picker插件来执行此操作,并且整个代码将放置在chooseImageGallery方法内,如下所示:
- 首先,将依赖项添加到
pubspec.yaml文件,指定名称和版本号,如下所示:
dev_dependencies:
flutter_test:
sdk: flutter
image_picker: ^0.6.0
有关发布依赖关系的详细讨论,请参阅“第 2 章”,“移动视觉–使用设备上模型的面部检测”。 确保运行Flutter包以在项目中包含依赖项。 要了解有关image_picker插件的更多信息,请访问这里。
- 将库导入到
PlantSpeciesRecognition.dart中,如下所示:
import 'package:image_picker/image_picker.dart';
- 此时,我们在
plant_species_recognition.dart内声明以下两个全局变量:
File_image:存储从图库中选择的图像文件bool _busy(初始值为false):一个用于平滑处理 UI 操作的标志变量
- 现在,让我们定义按下
FloatingActionButton按钮时将调用的chooseImageGallery()方法,如下所示:
Future chooseImageGallery() async {
var image = await ImagePicker.pickImage(source: ImageSource.gallery);
if (image == null) return;
setState(() {
_busy = true;
});
}
在这里,我们使用ImagePicker.pickImage()方法通过将其作为来源来从图库中获取图像。 我们将返回的值存储在变量图像中。 如果从调用返回的值为null,则由于无法对null值执行进一步的操作,因此我们返回了该调用。 否则,请将_busy的值更改为true,以指示正在对该图像进行进一步的操作。
setState()是一个同步回调,用于通知框架对象的内部状态已更改。 此更改可能实际上会影响应用的 UI,因此,框架将需要安排State对象的构建。 请参阅以下链接以进行进一步讨论。
此时,该应用已成功编译,然后按 FAB 启动画廊,可以从中选择图像。 但是,所选的图像不会显示在屏幕上,因此,现在让我们开始吧。
在屏幕上显示所选图像
现在,让我们添加一个小部件以显示在上一节中选择的图像,如下所示:
- 我们将使用小部件列表,从图库中选择的图像以及彼此堆叠或重叠的预测结果显示在屏幕上。 因此,我们首先声明一个空的小部件列表,其中将包含栈的所有子级。 另外,我们声明一个
size实例,以使用MediaQuery类查询包含应用的窗口的大小,如下所示:
List<Widget> stackChildren = [];
Size size = MediaQuery.of(context).size;
- 现在,将图像添加为栈的第一个子项,如下所示:
stackChildren.add(Positioned(
top: 0.0,
left: 0.0,
width: size.width,
child: _image == null ?Text('No Image Selected') : Image.file(_image),
));
Positioned类用于控制栈的子代的位置。 在这里,通过指定top,left和width属性的值。 top和left值分别指定子项的顶部和左侧边缘与栈顶部和左侧边缘的距离,此处为 0,即设备屏幕的左上角 。 width值指定子项的宽度-此处是包含应用的窗口的宽度,这意味着图像将占据整个宽度。
- 接下来,我们将添加子项,该子项将是一个文本,如果
_image的值为null,则表示未选择任何图像; 否则,它包含用户选择的图像。
为了在屏幕上显示栈,我们将stackChildren列表添加为build()方法返回的Scaffold的主体,如下所示:
return Scaffold(
appBar: AppBar(
title: const Text('Plant Species Recognition'),
),
//Add stackChildren in body
body: Stack(
children: stackChildren,
),
floatingActionButton: FloatingActionButton(
onPressed: chooseImageGallery,
tooltip: 'Pick Image',
child: Icon(Icons.image),
),
);
在前面的代码中,我们在Stack()内部传递stackChildren,以创建包含在列表内的所有小部件的覆盖结构。
- 此时编译代码将产生以下结果:

此时,单击FAB将启动图库,并且所选图像将显示在屏幕上。
接下来,我们将在设备上加载 TensorFlow Lite 模型,并向 Cloud Vision API 发出 HTTP 请求,以在所选图像上获得识别结果。
运行图像识别
现在,从图库中选择的图像可用作 Cloud Vision API 和 TensorFlow Lite 模型的两种预测方法的输入。 接下来,让我们定义两种方法。
使用 Cloud Vision API
在本节中,我们简单地定义一个visionAPICall方法,该方法用于向 CloudVision API 发出http Post请求,传入编码为json的请求字符串,该字符串返回一个json响应,该响应被解析以获取所需标签中的值:
- 首先,我们在
pubspec.yaml文件中定义一个http插件依赖项,如下所示:
http: ^0.12.0+2
- 将插件导入
PlantSpeciesRecognition.dart,以帮助发出http请求,如下所示:
import 'package:http/http.dart' as http;
- 现在,我们定义创建请求 URL 并发出
httpPOST请求的方法,如下所示:
List<int> imageBytes = _image.readAsBytesSync();
String base64Image = base64Encode(imageBytes);
为了能够将图像文件与 HTTP 发布请求一起发送进行分析,我们需要将png文件转换为 Base64 格式,即,转换为仅包含美国信息交换标准码(ASCII)的字符串值。 首先,我们使用readAsByteSync()读取_image的内容作为字节列表并将其存储在imageBytes中。 然后,通过将imageBytes列表作为base64Encode的参数传递给我们,以 Base64 格式对该列表进行编码。
- 接下来,我们创建请求字符串,其格式如下:
var request_str = {
"requests":[
{
"image":{
"content": "$base64Image"
},
"features":[
{
"type":"LABEL_DETECTION",
"maxResults":1
}
]
}
]
};
虽然整个字符串将被硬编码,但是内容密钥的值将根据用户选择的图像及其 base64 编码格式而有所不同。
- 我们将需要调用的 URL 存储在
url变量中,如下所示:
var url = 'https://vision.googleapis.com/v1/images:annotate?key=API_KEY;
确保用您生成的密钥替换API_KEY。
- 使用
http.post()方法发出 HTTP 发布请求,并传入url和响应字符串,如下所示:
var response = await http.post(url, body: json.encode(request_str));
print('Response status: ${response.statusCode}');
print('Response body: ${response.body}');
我们还使用response.statusCode检查状态码,如果请求成功,则状态码应为200。
- 由于来自服务器的响应是 JSON 格式,因此我们使用
json.decode()对其进行解码,然后进一步解析它,以将所需的值存储在str变量中,如下所示:
var responseJson = json.decode(response.body);
str = '${responseJson["responses"][0]["labelAnnotations"][0]["description"]}: ${responseJson["responses"][0]["labelAnnotations"][0]["score"].toStringAsFixed(3)}';
- 将所有内容放在一起后,整个
visionAPICall()方法将如下所示:
Future visionAPICall() async {
List<int> imageBytes = _image.readAsBytesSync();
print(imageBytes);
String base64Image = base64Encode(imageBytes);
var request_str = {
"requests":[
{
"image":{
"content": "$base64Image"
},
"features":[
{
"type":"LABEL_DETECTION",
"maxResults":1
}
]
}
]
};
var url = 'https://vision.googleapis.com/v1/images:annotate?key=AIzaSyDJFPQO3N3h78CLOFTBdkPIN3aE9_ZYHy0';
var response = await http.post(url, body: json.encode(request_str));
print('Response status: ${response.statusCode}');
print('Response body: ${response.body}');
var responseJson = json.decode(response.body);
str = '${responseJson["responses"][0]["labelAnnotations"][0]["description"]}: ${responseJson["responses"][0]["labelAnnotations"][0]["score"].toStringAsFixed(3)}';
}
在下一节中,我们将介绍使用设备上 TensorFlow Lite 模型的步骤。
使用设备上的 TensorFlow Lite 模型
现在,让我们为用户的第二选择添加功能,即使用 TensorFlow Lite 模型分析所选图像。 在这里,我们将使用我们先前创建的 TensorFlow Lite 模型。 以下步骤详细讨论了如何使用设备上的 TensorFlow Lite 模型:
- 我们将从在
pubspec.yaml文件中添加tflite依赖关系开始,如下所示:
dev_dependencies:
flutter_test:
sdk: flutter
image_picker: ^0.6.0
//Adding tflite dependency
tflite: ^0.0.5
- 接下来,我们在 Android 中配置
aaptOptions。 将以下代码行添加到android块内的android/app/build.gradle文件中:
aaptOptions {
noCompress 'tflite'
noCompress 'lite'
}
前面的代码段确保tflite文件未以压缩形式存储在 Android 应用包(APK)中。
- 接下来,我们需要将已经保存的
model.tflite和labels.txt文件包括在assests文件夹中,如以下屏幕截图所示:

- 在
pubspec.yaml文件中指定文件的路径,如下所示:
flutter:
uses-material-design: true
//Specify the paths to the respective files
assets:
- assets/model.tflite
- assets/labels.txt
- 现在,我们都准备从在设备上加载并运行我们的第一个 TensorFlow Lite 模型开始。 首先,将
tflite.dart文件导入到PlantSpeciesRecognition.dart中,如下所示:
import 'package:tflite/tflite.dart';
- 为了执行所有相关任务,我们定义了
analyzeTFLite()方法。 在这里,我们从加载模型开始,将model.tflite文件和labels.txt文件作为输入传递给Tflite.loadModel()中的model和labels参数。
如果成功加载模型,我们将结果输出存储在res字符串变量中,该变量将包含success值,如下所示:
String res = await Tflite.loadModel(
model: "assets/model.tflite",
labels: "assets/labels.txt",
numThreads: 1 // defaults to 1
);
print('Model Loaded: $res');
- 现在,我们使用
Tflite.runModelOnImage()方法在图像上运行模型,并传递存储在设备内部的所选图像的路径。 我们将结果存储在recognitions变量中,如下所示:
var recognitions = await Tflite.runModelOnImage(
path: _image.path
);
setState(() {
_recognitions = recognitions;
});
-
一旦模型在图像上成功运行并将结果存储在
recognitions局部变量中,我们将创建_recognitions全局列表并将其状态设置为recognitions中存储的值,以便可以更新 UI 结果正确。将所有内容放在一起后,整个
analyzeTfLite()方法将如下所示:
Future analyzeTFLite() async {
String res = await Tflite.loadModel(
model: "assets/model.tflite",
labels: "assets/labels.txt",
numThreads: 1 // defaults to 1
);
print('Model Loaded: $res');
var recognitions = await Tflite.runModelOnImage(
path: _image.path
);
setState(() {
_recognitions = recognitions;
});
print('Recognition Result: $_recognitions');
}
在成功选择并存储图像后,取决于用户单击的按钮,这是由visionAPICall()和analyzeTFLite()这两个定义的方法从chooseImageGallery()调用的,这取决于传递给窗口的值。 PlantSpeciesRecognition构造器:Cloud Vision API 为 0,TensorFlow Lite 为 1。
修改后的chooseImagGallery()方法如下所示:
Future chooseImageGallery() async {
var image = await ImagePicker.pickImage(source: ImageSource.gallery);
if (image == null) return;
setState(() {
_busy = true;
_image = image;
});
//Deciding on which method should be chosen for image analysis
if(widget.modelType == 0)
await visionAPICall();
else if(widget.modelType == 1)
await analyzeTFLite();
setState(() {
_image = image;
_busy = false;
});
}
在方法调用之前提到await关键字,以确保所有操作都是异步进行的。 在这里,我们还将_image的值设置为image,将_busy的值设置为false,以指示所有处理已完成,并且现在可以更新 UI。
用结果更新用户界面
在上一节“创建用户界面”中,我们通过向stackChildren添加一个额外的子代来更新 UI,以显示用户选择的图像。 现在,我们将另一个子项添加到栈中以显示图像分析的结果,如下所示:
- 首先,我们将添加 Cloud Vision API 的结果,如下所示:
stackChildren.add( Center (
child: Column(
children: <Widget>[
str != null?
new Text(str,
style: TextStyle(
color: Colors.black,
fontSize: 20.0,
background: Paint()
..color = Colors.white,
)
): new Text('No Results')
],
)
)
);
回想一下,请求的 JSON 响应已被解析,格式化并存储在str变量中。 在这里,我们使用str的值创建具有指定颜色和背景的Text。 然后,我们将此Text作为子级添加到列中,并对齐Text以显示在屏幕中央。 最后,我们将整个格式包装在stackChildren.add()周围,以将其添加到 UI 元素栈中。
- 接下来,我们将添加 TensorFlow Lite 的结果,如下所示:
stackChildren.add(Center(
child: Column(
children: _recognitions != null
? _recognitions.map((res) {
return Text(
"${res["label"]}: ${res["confidence"].toStringAsFixed(3)}",
style: TextStyle(
color: Colors.black,
fontSize: 20.0,
background: Paint()
..color = Colors.white,
),
);
}).toList() : [],
),
));
_recognitions列表中存储的 TensorFlow Lite 模型的结果逐元素进行迭代,并映射到使用.map()指定的列表。 列表中的每个元素都将进一步转换为Text,并作为与屏幕中心对齐的列子元素添加。
此外,请注意,需要将 Cloud Vision API 的输出或 TensorFlow Lite 模型的输出添加到栈中。 为了确保这一点,我们将前面的代码包装在if-else块中,这样,如果在构造器中传递的值(即modelChosen)为 0,则添加前者的输出;如果该值是,则添加后者的输出。 是 1。
- 最后,在各种图像集上运行 Cloud Vision API 将提供不同的输出。 以下屏幕快照显示了一些示例:

当 TensorFlow Lite 模型使用相同的图像集时,识别方式会有所不同。 以下屏幕快照显示了一些示例:

在上一个屏幕截图中,我们可以看到已正确识别将图像加载到图库中的花朵的种类。
总结
在本章中,我们介绍了如何使用流行的基于深度学习的 API 服务来使用图像处理。 我们还讨论了如何通过扩展先前创建的基础模型,将其与定制训练模型一起应用。 虽然我们没有明确提及,但是基础模型的扩展是称为迁移学习(TL)的过程的一部分,其中将在特定数据集上训练的模型导入并用在完全不同的场景中,几乎没有或只有很少的微调。
此外,本章还介绍了 TensorFlow Lite 为什么以及何时适合用于构建模型,以及如何将 Flutter 应用于在离线运行且非常快速的设备模型上进行应用。 本章设定了里程碑,在项目中引入了 Python 和 TensorFlow,在接下来的章节中将广泛使用这两种方法。
在下一章中,我们将介绍计算机科学的一个非常激动人心的领域,即增强现实,并介绍深度学习在现实世界中的应用。
五、从摄像机源生成实时字幕
作为人类,我们每天在不同的场景中看到一百万个物体。 对于人类来说,描述场景通常是一件微不足道的任务:我们所做的事情甚至都不需要花费大量的时间去思考。 但是,机器要理解图像或视频等视觉媒体中呈现给它的元素和场景是一项艰巨的任务。 但是,对于人工智能(AI)的几种应用,具有在计算机系统中理解此类图像的功能很有用。 例如,如果我们能够设计出可以将周围环境实时转换为音频的机器,则对视障人士将大有帮助。 此外,研究人员一直在努力实时生成图像和视频的字幕,以提高网站和应用上呈现的内容的可访问性。
本章介绍了一种使用摄像机供稿实时生成自然语言字幕的方法。 在此项目中,您将创建一个使用存储在设备上的自定义预训练模型的相机应用。 该模型使用深层卷积神经网络(CNN)和长短期记忆(LSTM)生成字幕。
我们将在本章介绍以下主题:
- 设计项目架构
- 了解图像字幕生成器
- 了解相机插件
- 创建相机应用
- 从相机源生成图像字幕
- 创建材质应用
让我们从讨论此项目将要遵循的架构开始。
设计项目架构
在这个项目中,我们将构建一个移动应用,当指向任何风景时,它将能够创建描述该风景的标题。 这样的应用对于有视觉缺陷的人非常有用,因为它既可以用作网络上的辅助技术,又可以与 Alexa 或 Google Home 等语音界面搭配使用,用作日常应用。 该应用将调用一个托管 API,该 API 将为传递给它的任何给定图像生成标题。 API 返回该图像的三个最佳字幕,然后该应用将其显示在应用中相机视图的正下方。
从鸟瞰图可以通过下图说明项目架构:

输入将是在智能手机中获得的相机提要,然后将其发送到托管为网络 API 的图像标题生成模型。 该模型在 Red Hat OpenShift 上作为 Docker 容器托管。 图像标题生成模型返回图像的标题,然后将其显示给用户。
有了关于如何构建应用的清晰思路,让我们首先讨论图像字幕的问题以及如何解决它们。
了解图像字幕生成器
计算机科学的一个非常流行的领域是图像处理领域。 它涉及图像的操纵以及我们可以从中提取信息的各种方法。 另一个流行的领域是自然语言处理(NLP),涉及如何制造可以理解和产生有意义的自然语言的机器。 图像标题定义了两个主题的混合,试图首先提取出现在任何图像中的对象的信息,然后生成描述对象的标题。
标题应以有意义的字串形式生成,并以自然语言句子的形式表示。
考虑下图:

图像中可以检测到的物体如下:勺子,玻璃杯,咖啡和桌子。
但是,我们对以下问题有答案吗?
- 杯子里装着咖啡还是汤匙,还是空的?
- 桌子在玻璃上方还是下方?
- 汤匙在桌子上方还是下方?
我们意识到,为了回答上述问题,我们需要使用如下语句:
- 杯子里装着咖啡。
- 玻璃放在桌子上。
- 汤匙放在桌子上。
因此,如果我们试图在图像周围创建标题,而不是简单地识别图像中的项目,我们还需要在可见项目之间建立一些位置和特征关系。 这将帮助我们获得良好的图像标题,例如一杯咖啡在桌子上,旁边放着勺子。 在图像标题生成算法中,我们尝试从图像创建此类标题。
但是,一个字幕可能并不总是足以描述风景,我们可能必须在两个可能相同的字幕之间进行选择,如以下屏幕截图所示:

Allef Vinicius 在 Unsplash 上的照片
您如何在前面的屏幕快照中描述图像?
您可以提出以下任何标题:
- 背景中有两棵树和多云的天空。
- 一把椅子和一把吉他放在地上。
根据用户,这提出了在任何图像中重要的问题。 尽管最近有一些设计用于处理这种情况的方法,例如“注意机制”方法,但在本章中我们将不对其进行深入讨论。
您可以在 CaptionBot 的这个页面上查看由 Microsoft 创建的图像字幕系统的非常酷的演示。
现在让我们定义将用于创建图像字幕模型的数据集。
了解数据集
不出所料,我们需要大量的通用图像以及可能列出的标题。 我们已经在上一节“了解图像字幕生成器”中显示,单个图像可以具有多个字幕,而不必任何一个都错了。 因此,在这个项目中,我们将研究 Flickr8k 数据集。 除此之外,我们还需要由 Jeffrey Pennington,Richard Socher 和 Christopher D. Manning 创建的 GloVE 嵌入。 简而言之,GloVE 告诉我们在给定单词之后可能跟随哪些单词,从而帮助我们从一组不连续的单词中形成有意义的句子。
您可以在这个页面上阅读有关 GloVE 嵌入的更多信息,以及描述它们的论文。
Flickr8k 数据集包含 8,000 个图像样本,以及每个图像的五个可能的标题。 还有其他可用于该任务的数据集,例如具有 30,000 个样本的 Flickr30k 数据集,或具有 180,000 张图像的 Microsoft COCO 数据集。 虽然使用较大的数据库会产生更好的结果,但是为了能够在普通机器上训练模型,我们将不再使用它们。 但是,如果可以使用高级计算能力,则可以肯定地尝试围绕较大的数据集构建模型。
您可以通过伊利诺伊大学厄本那香槟分校提供的以下格式的请求来下载 Flickr8k 数据集。
下载数据集时,您将能够看到以下文件夹结构:
Flickr8k/
- dataset
- images
- 8091 images
- text
- Flickr8k.token.txt
- Flickr8k.lemma.txt
- Flickr_8k.trainImages.txt
- Flickr_8k.devImages.txt
- Flickr_8k.testImages.txt
- ExpertAnnotations.txt
- CrowdFlowerAnnotations.txt
在可用的文本文件中,我们感兴趣的是Flickr8k.token.txt,其中包含dataset目录下images文件夹中每个图像的原始标题。
字幕以以下格式显示:
1007129816_e794419615.jpg#0 A man in an orange hat staring at something .
1007129816_e794419615.jpg#1 A man wears an orange hat and glasses .
1007129816_e794419615.jpg#2 A man with gauges and glasses is wearing a Blitz hat .
1007129816_e794419615.jpg#3 A man with glasses is wearing a beer can crocheted hat .
1007129816_e794419615.jpg#4 The man with pierced ears is wearing glasses and an orange hat .
通过检查,我们可以观察到前面示例中的每一行都包含以下部分:
Image_Filename#Caption_Number Caption
因此,通过浏览dataset/images文件夹中存在的图像的文件中的每一行,我们可以将标题映射到每个图像。
现在开始处理图像标题生成器代码。
建立图像字幕生成模型
在本节中,我们将看一看代码,这些代码将帮助我们创建一个管道,以将抛出该图像的图像转换为字幕。 我们将本节分为四个部分,如下所示:
- 初始化字幕数据集
- 准备字幕数据集
- 训练
- 测试
让我们从项目初始化开始。
初始化字幕数据集
在本节介绍的步骤中,我们将导入项目所需的模块并将数据集加载到内存中。 让我们从导入所需的模块开始,如下所示:
- 导入所需的库,如下所示:
import numpy as np
import pandas as pd
import nltk
from nltk.corpus import stopwords
import re
import string
import pickle
import matplotlib.pyplot as plt
%matplotlib inline
您会看到在这个项目中将使用许多模块和子模块。 在模型的运行中,它们都非常重要,从本质上讲,帮助器模块也是如此。 下一步,我们将导入更多特定于构建模型的模块。
- 导入 Keras 和子模块,如下所示:
import keras
from keras.layers.merge import add
from keras.preprocessing import image
from keras.utils import to_categorical
from keras.models import Model, load_model
from keras.applications.vgg16 import VGG16
from keras.preprocessing.sequence import pad_sequences
from keras.layers import Input, Dense, Dropout, Embedding, LSTM
from keras.applications.resnet50 import ResNet50, preprocess_input, decode_predictions
我们导入了 Keras 模块以及其他几个子模块和方法,以帮助我们快速构建深度学习模型。 Keras 是可用的最受欢迎的深度学习库之一,除 TensorFlow 外,还可以与 Theano 和 PyTorch 等其他框架一起使用。
- 加载字幕-在这一步中,我们将
Flickr8k.token.txt文件中存在的所有字幕加载到单个captions列表中,如下所示:
caption_file = "./data/Flickr8k/text/Flickr8k.token.txt"
captions = []
with open(caption_file) as f:
captions = f.readlines()
captions = [x.strip() for x in captions]
从文件加载所有标题后,让我们看看它们包含的内容,如下所示:
captions[:5]
正如预期的那样,并在前面的“了解数据集”部分中提到,我们获得了数据集中的以下前五行:
['1000268201_693b08cb0e.jpg#0\tA child in a pink dress is climbing up a set of stairs in an entry way .',
'1000268201_693b08cb0e.jpg#1\tA girl going into a wooden building .',
'1000268201_693b08cb0e.jpg#2\tA little girl climbing into a wooden playhouse .',
'1000268201_693b08cb0e.jpg#3\tA little girl climbing the stairs to her playhouse .',
'1000268201_693b08cb0e.jpg#4\tA little girl in a pink dress going into a wooden cabin .']
既然我们已经看到了写入每一行的模式,那么我们就可以继续分割每一行,以便可以将数据放入数据结构中,这比一大串字符串有助于更快地访问和更新。
准备字幕数据集
在以下步骤中,我们将处理加载的字幕数据集,并将其转换为适合对其进行训练的形式:
- 在此步骤中,我们将图像描述拆分并以字典格式存储,以方便将来的代码中使用,如以下代码块所示:
descriptions = {}
for x in captions:
imgid, cap = x.split('\t')
imgid = imgid.split('.')[0]
if imgid not in descriptions.keys():
descriptions[imgid] = []
descriptions[imgid].append(cap)
在前面的代码行中,我们将文件中的每一行细分为图像 ID 和每个图像标题的部分。 我们用它创建了一个字典,其中图像 ID 是字典键,每个键值对都包含五个标题的列表。
- 接下来,我们开始进行基本的字符串预处理,以便继续在字幕上应用自然语言技术,如下所示:
for key, caps in descriptions.items():
for i in range(len(caps)):
caps[i] = caps[i].lower()
caps[i] = re.sub("[^a-z]+", " ", caps[i])
- 另外,为了帮助我们将来分配合适的内存空间大小并准备词汇表,让我们创建标题文本中所有单词的列表,如下所示:
allwords = []
for key in descriptions.keys():
_ = [allwords.append(i) for cap in descriptions[key] for i in cap.split()]
- 一旦创建了所有单词的列表,就可以创建单词的频率计数。 为此,我们使用
collections模块的Counter方法。 一些单词在数据集中很少出现。 删除这些单词是一个好主意,因为它们不太可能频繁出现在用户提供的输入中,因此不会为字幕生成算法增加太多价值。 我们使用以下代码进行操作:
from collections import Counter
freq = dict(Counter(allwords))
freq = sorted(freq.items(), reverse=True, key=lambda x:x[1])
threshold = 15
freq = [x for x in freq if x[1]>threshold]
print(len(freq))
allwords = [x[0] for x in freq]
让我们通过运行以下代码来尝试查看最常用的单词:
freq[:10]
我们看到以下输出:
[('a', 62995),
('in', 18987),
('the', 18420),
('on', 10746),
('is', 9345),
('and', 8863),
('dog', 8138),
('with', 7765),
('man', 7275),
('of', 6723)]
我们可以得出结论,停用词在字幕文本中占很大比例。 但是,由于我们在生成句子时需要它们,因此我们不会将其删除。
训练
在以下步骤中,我们加载训练并测试图像数据集并对其进行训练:
- 现在,将分离的训练和测试文件加载到数据集中。 它们包含图像文件名列表,它们实际上是带有文件扩展名的图像 ID,如以下代码块所示:
train_file = "./data/Flickr8k/text/Flickr_8k.trainImages.txt"
test_file = "./data/Flickr8k/text/Flickr_8k.testImages.txt"
现在,我们将处理训练图像列表文件以提取图像 ID,并省略文件扩展名,因为在所有情况下它都相同,如以下代码片段所示:
with open(train_file) as f:
cap_train = f.readlines()
cap_train = [x.strip() for x in cap_train]
我们对测试图像列表进行相同的操作,如下所示:
with open(test_file) as f:
cap_test = f.readlines()
cap_test = [x.strip() for x in cap_test]
train = [row.split(".")[0] for row in cap_train]
test = [row.split(".")[0] for row in cap_test]
- 现在,我们将创建一个字符串,其中合并每个图像的所有五个可能的标题,并将它们存储在
train_desc中。 字典。 我们使用#START#和#STOP#区分字幕,以便将来在字幕生成中使用它们,如以下代码块所示:
train_desc = {}
max_caption_len = -1
for imgid in train:
train_desc[imgid] = []
for caption in descriptions[imgid]:
train_desc[imgid].append("#START# " + caption + " #STOP#")
max_caption_len = max(max_caption_len, len(caption.split())+1)
- 我们将使用 Keras 模型资源库中的
ResNet50预训练模型。 我们将输入形状设置为224 x 224 x 3,其中224 x 244是将传递给模型的每个图像的尺寸,而 3 是颜色通道的数量。 请注意,与美国国家混合标准技术研究院(MNIST)数据集不同,在该数据集中每个图像的尺寸均相等,而 Flickr8k 数据集则并非如此。 该代码可以在以下代码段中看到:
model = ResNet50(weights="imagenet", input_shape=(224,224,3))
model.summary()
从高速缓存中下载或加载模型后,将为每个层显示模型摘要。 但是,我们需要根据需要重新训练模型,因此我们将删除并重新创建模型的最后两层。 为此,我们使用与加载的模型相同的输入来创建一个新模型,并且输出等效于倒数第二层的输出,如以下代码片段所示:
model_new = Model(model.input, model.layers[-2].output)
- 我们将需要一个函数来重复预处理图像,预测图像中包含的特征,并根据图像中识别出的对象或属性形成特征向量。 因此,我们创建一个
encode_image函数,该函数接受图像作为输入参数,并通过ResNet50重新训练的模型运行图像,从而返回图像的特征向量表示,如下所示:
def encode_img(img):
img = image.load_img(img, target_size=(224,224))
img = image.img_to_array(img)
img = np.expand_dims(img, axis=0)
img = preprocess_input(img)
feature_vector = model_new.predict(img)
feature_vector = feature_vector.reshape((-1,))
return feature_vector
- 现在,我们需要将数据集中的所有图像编码为特征向量。 为此,我们首先需要将数据集中的所有图像一张一张地加载到内存中,并对其应用
encode_img函数。 首先,设置images文件夹的路径,如以下代码片段所示:
img_data = "./data/Flickr8k/dataset/images/"
完成后,我们使用先前创建的训练图像列表遍历文件夹中的所有图像,并对每个图像应用encode_img函数。 然后,将特征向量存储在以图像 ID 为键的字典中,如下所示:
train_encoded = {}
for ix, imgid in enumerate(train):
img_path = img_data + "/" + imgid + ".jpg"
train_encoded[imgid] = encode_img(img_path)
if ix%100 == 0:
print(".", end="")
我们类似地使用以下代码对测试数据集中的所有图像进行编码:
test_encoded = {}
for i, imgid in enumerate(test):
img_path = img_data + "/" + imgid + ".jpg"
test_encoded[imgid] = encode_img(img_path)
if i%100 == 0:
print(".", end="")
- 在接下来的几个步骤中,我们需要将加载的 GloVe 嵌入与项目中包含的单词列表进行匹配。 为此,我们当然必须找到任何给定单词的索引或在任何给定索引处找到该单词。 为方便起见,我们将在字幕数据集中找到的所有单词创建两个字典,将它们映射到索引和索引之间,如以下代码片段所示:
word_index_map = {}
index_word_map = {}
for i,word in enumerate(allwords):
word_index_map[word] = i+1
index_word_map[i+1] = word
我们还将在两个字典中分别使用"#START#"和"#STOP#"字创建两个附加的键值对,如下所示:
index_word_map[len(index_word_map)] = "#START#"
word_index_map["#START#"] = len(index_word_map)
index_word_map[len(index_word_map)] = "#STOP#"
word_index_map["#STOP#"] = len(index_word_map)
- 现在,将 GloVe 嵌入内容加载到项目中,如下所示:
f = open("./data/glove/glove.6B.50d.txt", encoding='utf8')
使用发现open,我们将嵌入内容读入字典,其中每个词都是键,如下所示:
embeddings = {}
for line in f:
words = line.split()
word_embeddings = np.array(words[1:], dtype='float')
embeddings[words[0]] = word_embeddings
读取完embeddings文件后,我们将其关闭以实现更好的内存管理,如下所示:
f.close()
- 现在,让我们在数据集中的标题中的所有单词与 GloVe 嵌入之间创建嵌入矩阵,如以下代码块所示:
embedding_matrix = np.zeros((len(word_index_map) + 1, 50))
for word, index in word_index_map.items():
embedding_vector = embeddings.get(word)
if embedding_vector is not None:
embedding_matrix[index] = embedding_vector
请注意,我们存储的最大嵌入数量为 50,这对于生成长而有意义的字符串是足够的。
- 接下来,我们将创建另一个模型,该模型将在从之前的步骤中获取特征向量后,专门用于为看不见的图像生成标题。 为此,我们将特征向量的形状作为输入来创建
Input层,如以下代码块所示:
in_img_feats = Input(shape=(2048,))
in_img_1 = Dropout(0.3)(in_img_feats)
in_img_2 = Dense(256, activation='relu')(in_img_1)
完成后,我们还需要以 LSTM 的形式在整个训练数据集中的标题中输入单词,以便给定任何单词,我们都能够预测接下来的 50 个单词。 我们使用以下代码进行操作:
in_caps = Input(shape=(max_caption_len,))
in_cap_1 = Embedding(input_dim=len(word_index_map) + 1, output_dim=50, mask_zero=True)(in_caps)
in_cap_2 = Dropout(0.3)(in_cap_1)
in_cap_3 = LSTM(256)(in_cap_2)
最后,我们需要添加一个decoder层,该层以 LSTM 的形式接受图像特征和单词,并在字幕生成过程中输出下一个可能的单词,如下所示:
decoder_1 = add([in_img_2, in_cap_3])
decoder_2 = Dense(256, activation='relu')(decoder_1)
outputs = Dense(len(word_index_map) + 1, activation='softmax')(decoder_2)
现在,通过运行以下代码,在适当添加输入和输出层之后,让我们对该模型进行总结:
model = Model(inputs=[in_img_feats, in_caps], outputs=outputs)
model.summary()
我们得到以下输出,描述了模型层:

接下来,让我们在训练模型之前设置其权重。
- 我们将在 GloVe 嵌入中的单词和数据集的标题中的可用单词之间插入我们先前创建的
embedding_matrix,如以下代码块所示:
model.layers[2].set_weights([embedding_matrix])
model.layers[2].trainable = False
这样,我们就可以编译模型了,如下所示:
model.compile(loss='categorical_crossentropy', optimizer='adam')
- 由于数据集很大,因此我们不想在训练时将所有图像同时加载到数据集中。 为了促进模型的内存有效训练,我们使用生成器函数,如下所示:
def data_generator(train_descs, train_encoded, word_index_map, max_caption_len, batch_size):
X1, X2, y = [], [], []
n = 0
while True:
for key, desc_list in train_descs.items():
n += 1
photo = train_encoded[key]
for desc in desc_list:
seq = [word_index_map[word] for word in desc.split() if word in word_index_map]
for i in range(1, len(seq)):
xi = seq[0:i]
yi = seq[i]
xi = pad_sequences([xi], maxlen=max_caption_len, value=0, padding='post')[0]
yi = to_categorical([yi], num_classes=len(word_index_map) + 1)[0]
X1.append(photo)
X2.append(xi)
y.append(yi)
if n==batch_size:
yield [[np.array(X1), np.array(X2)], np.array(y)]
X1, X2, y = [], [], []
n = 0
- 我们现在准备训练模型。 在执行此操作之前,我们必须设置模型的一些超参数,如以下代码片段所示:
batch_size = 3
steps = len(train_desc)//batch_size
设置超参数后,我们可以使用以下代码行开始训练:
generator = data_generator(train_desc, train_encoded, word_index_map, max_caption_len, batch_size)
model.fit_generator(generator, epochs=1, steps_per_epoch=steps, verbose=1)
model.save('./model_weights/model.h5')
测试
现在,在以下步骤中,我们将基于前面步骤中训练的模型创建用于预测字幕的功能,并在示例图像上测试字幕:
- 我们终于到了可以使用模型生成图像标题的阶段。 我们创建了一个函数,该函数可以吸收图像并使用
model.predict方法在每个步骤中提出一个单词,直到在预测中遇到#STOP#。 它在那里停止并输出生成的字幕,如下所示:
def predict_caption(img):
in_text = "#START#"
for i in range(max_caption_len):
sequence = [word_index_map[w] for w in in_text.split() if w in word_index_map]
sequence = pad_sequences([sequence], maxlen=max_caption_len, padding='post')
pred = model.predict([img, sequence])
pred = pred.argmax()
word = index_word_map[pred]
in_text += (' ' + word)
if word == "#STOP#":
break
caption = in_text.split()[1:-1]
return ' '.join(caption)
- 让我们在测试数据集中的某些图像上测试生成模型,如下所示:
img_name = list(test_encoded.keys())[np.random.randint(0, 1000)]
img = test_encoded[img_name].reshape((1, 2048))
im = plt.imread(img_data + img_name + '.jpg')
caption = predict_caption(img)
print(caption)
plt.imshow(im)
plt.axis('off')
plt.show()
假设我们将以下屏幕截图中显示的图像输入了算法:

对于前面的屏幕快照中显示的图像,我们获得了以下生成的标题:一只棕色的狗正穿过草丛。 虽然标题不是很准确,但完全遗漏了图片中的第二只动物,但它的确足以确定一条棕色的狗在草地上奔跑。
但是,我们训练有素的模型非常不准确,因此不适合用于生产或实验以外的用途。 您可能已经注意到,我们将训练中的周期数设置为 1,这是一个非常低的值。 这样做是为了使该程序的训练在合理的时间内完成,以供您阅读本书!
在下一节中,我们将研究如何将图像字幕生成模型部署为 API 并使用它来生成实时的摄像机供稿字幕。
创建一个简单的可单击部署的图像标题生成模型
虽然我们在上一节“测试”中开发的图像标题生成模型看起来不错,但不是很好。 因此,在本节中,我们将向您展示一种方法,以单击方式将可直接用于生产环境的模型作为 Docker 映像部署在 Red Hat OpenShift 上,并由 IBM 出色的机器学习专家创建。
将微服务用于在任何网站上执行的此类微小且专用的操作是一种非常普遍的做法,因此,我们将把此图像标题服务视为微服务。
我们将使用的图像是 IBM 开发的 MAX 图像字幕生成器模型。 它基于im2txt模型的代码,作为 《Show and Tell: Lessons learned from the 2015 MSCOCO Image Captioning Challenge》论文的可公开使用的 TensorFlow 实现托管在 GitHub 上。
在更大的 Microsoft COCO 数据集上训练了图像中使用的模型,该数据集包含超过 200,000 个带标签图像的实例,以及总共超过 300,000 个图像实例。 该数据集包含包含超过 150 万个不同对象的图像,并且是用于构建对象检测和图像标记模型的最大,最受欢迎的数据集之一。 但是,由于其巨大的尺寸,很难在低端设备上训练模型。 因此,我们将使用已经可用的 Docker 映像,而不是尝试在其上训练我们的模型。 但是,项目章节前面各节中描述的方法与 Docker 映像中的代码所使用的方法非常相似,并且在有足够的可用资源的情况下,您绝对可以尝试训练并提高模型的准确率。
您可以在以下链接中查看有关此 Docker 映像项目的所有详细信息。
您可以在此 Docker 映像的项目页面上了解其他可用的方法来部署此映像,但我们将向您展示在 Red Hat OpenShift 上的部署,从而使您只需单击几下即可快速测试模型。 。
让我们看看如何部署此映像,如下所示:
- 创建一个 Red Hat OpenShift 帐户。 为此,请将浏览器指向这里,然后单击“免费试用”。
- 选择尝试 RedHat OpenShift Online,如以下屏幕截图所示:

- 在下一个屏幕中,选择“注册 Openshift Online”。 然后,单击页面右上方的“注册”以找到“注册”页面。
- 填写所有必要的详细信息,然后提交表格。 系统将要求您进行电子邮件验证,完成后将带您进入订阅确认页面,该页面将要求您确认平台免费订阅的详细信息,如以下屏幕快照所示:

请注意,前面的订阅详细信息随时可能更改,并且可能反映订阅的其他值,区域或持续时间。
- 确认订阅后,您将需要等待几分钟才能配置系统资源。 设置完成后,您应该能够看到将带您进入管理控制台的按钮,如以下屏幕截图所示:

在上一个屏幕快照中显示的管理控制台的左侧,您可以找到各种菜单选项,并且在当前页面的中心,将提示您创建一个新项目。
- 单击“创建项目”,然后在出现的对话框中填写项目名称。 确保您创建的项目具有唯一的名称。 创建项目后,将为您提供一个仪表板,其中显示了对所有可用资源及其使用情况的监视。
在左侧菜单上,选择“开发人员”以切换到控制台的“开发人员”视图,如以下屏幕截图所示:

- 现在,您应该能够看到控制台的 Developer 视图以及更新的左侧菜单。 在这里,单击“拓扑”以获取以下部署选项:

- 在显示有部署选项的屏幕中单击“容器映像”,以调出用于容器映像部署的表单。
在此处,将图像名称填写为codait/max-image-caption-generator,然后单击“搜索”图标。 其余字段将自动获取,并且将显示与图像有关的信息,如以下屏幕截图所示:

- 在显示部署详细信息的下一个屏幕中,单击屏幕中央的“部署的映像”选项,如以下屏幕截图所示:

- 然后,向下滚动显示在屏幕右侧的信息面板,找到“路由”信息,该信息类似于以下屏幕截图:

单击此路由,将为您提供以下已成功部署的 API 的 Swagger UI:

您可以通过将图像发布到/model/predict路由来快速检查模型的工作情况。 随意使用 Swagger UI 可以很好地了解其表现。 您也可以使用/model/metadata路由找到模型元数据。
我们准备在项目中使用此 API。 让我们在接下来的部分中了解如何构建相机应用以及如何将此 API 集成到应用中。 我们首先使用相机插件构建应用。
了解相机插件
通过camera依赖项提供的相机插件,使我们可以自由访问设备的摄像机。 它为 Android 和 iOS 设备提供支持。 该插件是开源的,并托管在 GitHub 上,因此任何人都可以自由访问代码,修复错误并提出对当前版本的增强建议。
该插件可用于在小部件上显示实时摄像机预览,捕获图像并将其本地存储在设备上。 它也可以用来录制视频。 此外,它具有访问图像流的功能。
可以通过以下三个简单步骤将相机插件添加到任何应用:
- 安装包
- 添加用于持久存储和正确执行的方法
- 编程
现在让我们详细讨论每个步骤。
安装相机插件
要在应用中使用相机插件,我们需要在pubspec.yaml文件中添加camera作为依赖项。 可以按照以下步骤进行:
camera: 0.5.7+3
最后,运行flutter pub get将依赖项添加到应用。
添加用于持久存储和正确执行的方法
对于 iOS 设备,我们还需要指定一个空间来存储系统可以轻松访问的配置数据。 iOS 设备借助Info.plist文件来确定要显示的图标,应用支持的文档类型以及其他行为。 您需要在此步骤中修改ios/Runner/Info.plist中存在的Info.plist文件。
这可以通过添加以下文本来完成:
<key>NSCameraUsageDescription</key>
<string>Can I use the camera please?</string>
<key>NSMicrophoneUsageDescription</key>
<string>Can I use the mic please?</string>
对于 Android 设备,插件正常运行所需的最低软件开发套件(SDK)版本是 21。因此,请将最低 Android SDK 版本更改为 21(或更高版本), 存储在android/app/build.gradle文件中,如下所示:
minSdkVersion 21
安装依赖项并进行必要的更改之后,现在让我们开始编写应用代码。
编码
安装插件并进行必要的修改后,现在就可以使用它来访问相机,单击图片并录制视频。
涉及的最重要步骤如下:
- 通过运行以下代码导入插件:
import 'package:camera/camera.dart';
- 通过运行以下代码来检测可用的摄像机:
List<CameraDescription> cameras = await availableCameras();
- 初始化相机控件实例,如下所示:
CameraController controller = CameraController(cameras[0], ResolutionPreset.medium);
controller.initialize().then((_) {
if (!mounted) {
return;
}
setState(() {});
});
- 通过运行以下代码来处理控制器实例:
controller?.dispose();
现在,我们已经具备了相机插件的基本知识,让我们为应用构建实时相机预览。
创建相机应用
现在,我们将开始构建移动应用,以为指向相机的对象生成标题。 它包括一个用于捕获图像的相机预览和一个用于显示模型返回的字幕的文本视图。
该应用可以大致分为两部分,如下所示:
- 建立相机预览
- 集成模型来获取标题
在以下部分中,我们将讨论构建基本的相机预览。
建立相机预览
现在,我们将为应用构建摄像机预览。 我们首先创建一个新文件generate_live_caption.dart和一个GenerateLiveCaption有状态小部件。
让我们看一下创建实时摄像机预览的以下步骤:
- 要添加实时摄像机预览,我们将使用
camera插件。 首先,将依存关系添加到pubspec.yaml文件中,如下所示:
camera: ^0.5.7
接下来,我们需要通过运行flutter pub get将依赖项添加到项目中。
-
现在,我们创建一个新文件
generate_live_captions.dart,其中包含GenerateLiveCaptions有状态的小部件。 进一步步骤中描述的所有代码将包含在_GenerateLiveCaptionState类中。 -
导入
camera库。 我们将其导入generate_live_captions.dart,如下所示:
import 'package:camera/camera.dart';
- 现在,我们需要检测设备上所有可用的摄像机。 为其定义
detectCameras()函数,如下所示:
Future<void> detectCameras() async{
cameras = await availableCameras();
}
cameras是包含所有可用摄像机的全局列表,并在GenerateLiveCaptionState中声明,如下所示:
List<CameraDescription> cameras;
- 现在,我们使用
initializeController()方法创建CameraController的实例,如下所示:
void initializeController() {
controller = CameraController(cameras[0], ResolutionPreset.medium);
controller.initialize().then((_) {
if (!mounted) {
return;
}
setState(() {});
});
}
在应用中,我们将使用设备的后置摄像头,因此我们使用camera[0]创建CameraController实例,并使用ResolutionPreset.medium将分辨率指定为中等。 接下来,我们使用controller.initialize()初始化控制器。
- 为了在应用的屏幕上显示摄像机源,我们定义了
buildCameraPreview()方法,如下所示:
Widget _buildCameraPreview() {
var size = MediaQuery.of(context).size.width;
return Container(
child: Column(
children: <Widget>[
Container(
width: size,
height: size,
child: CameraPreview(controller),
),
]
)
);
}
在前面的方法中,我们使用MediaQuery.of(context).size.width获取容器的宽度并将其存储在size变量中。 接下来,我们创建一列小部件,其中第一个元素是Container。 Container的子项只是CameraPreview,用于在应用的屏幕上显示摄像机的信息。
- 现在,我们覆盖
initState,以便在初始化GenerateLiveCaption后立即检测到所有摄像机,如下所示:
@override
void initState() {
super.initState();
detectCameras().then((_){
initializeController();
});
}
在前面的代码片段中,我们仅调用detectCameras()首先检测所有可用的摄像机,然后调用initializeController()用后置摄像机初始化CameraController。
- 要从相机供稿生成字幕,我们将从相机供稿中拍摄照片并将其存储在本地设备中。 这些单击的图片将稍后从图像文件中检索以生成标题。 因此,我们需要一种读取和写入文件的机制。 我们通过在
pubspec.yaml文件中添加以下依赖项来使用path_provider插件:
path_provider: ^1.4.5
接下来,我们通过在终端中运行flutter pub get来安装包。
- 要在应用中使用
path_provider插件,我们需要通过在文件顶部添加import语句将其导入generate_live_caption.dart中,如下所示:
import 'package:path_provider/path_provider.dart';
- 要将图像文件保存到磁盘,我们还需要导入
dart:io库,如下所示:
import 'dart:io';
- 现在,让我们定义一种方法
captureImages(),以从相机源中捕获图像并将其存储在设备中。 这些存储的图像文件将在以后用于生成字幕。 该方法定义如下:
capturePictures() async {
String timestamp = DateTime.now().millisecondsSinceEpoch.toString();
final Directory extDir = await getApplicationDocumentsDirectory();
final String dirPath = '${extDir.path}/Pictures/generate_caption_images';
await Directory(dirPath).create(recursive: true);
final String filePath = '$dirPath/${timestamp}.jpg';
controller.takePicture(filePath).then((_){
File imgFile = File(filePath);
});
}
在前面的代码片段中,我们首先使用DateTime.now().millisecondsSinceEpoch()找出当前时间(以毫秒为单位),然后将其转换为字符串并将其存储在变量timestamp中。 时间戳将用于为我们将进一步存储的图像文件提供唯一的名称。 接下来,我们使用getApplicationDocumentsDirectory()获取可用于存储图像的目录的路径,并将其存储在Directory类型的extDir中。 现在,我们通过在外部目录后附加'/Pictures/generate_caption_images'来创建适当的目录路径。 然后,我们通过将目录路径与当前时间戳组合并为其指定.jpg格式来创建最终的filePath。 由于时间戳始终具有不同的值,因此所有单击的图像的filePath将始终是唯一的。 最后,我们使用当前的相机控制器实例调用takePicture()并传入filePath来捕获图像。 我们存储在imgFile中创建的图像文件,稍后将用于生成适当的字幕。
- 如前所述,为了从实时摄像机的提要中生成字幕,我们会定期捕获图像。 为了使它起作用,我们修改
initializeController()并添加一个计时器,如下所示:
void initializeController() {
controller = CameraController(cameras[0], ResolutionPreset.medium);
controller.initialize().then((_) {
if (!mounted) {
return;
}
setState(() {});
const interval = const Duration(seconds:5);
new Timer.periodic(interval, (Timer t) => capturePictures());
});
}
在initializeController()内部,一旦正确初始化并安装了摄像机控制器,我们将使用Duration()类创建 5 秒的持续时间,并将其存储在间隔中。 现在,我们使用Timer.periodic创建一个定期计时器,并为其设置 5 秒的间隔。 此处指定的回调为capturePictures()。 将在指定间隔内重复调用它。
至此,我们创建了一个实时摄像机供稿,该供稿显示在屏幕上,并且能够以 5 秒的间隔捕获图像。 在下一部分中,我们将集成模型以为所有捕获的图像生成标题。
从相机源生成图像字幕
现在,我们对图像标题生成器有了一个清晰的想法,并有了一个带有摄像头提要的应用,我们准备为摄像头提要生成图像的标题。 要遵循的逻辑非常简单。 图像是在特定时间间隔从实时摄像机的提要中捕获的,并存储在设备的本地存储中。 接下来,检索存储的图片,并为托管模型创建HTTP POST请求,传入检索的图像以获取生成的字幕,解析响应并将其显示在屏幕上。
现在让我们看一下详细步骤,如下所示:
- 我们首先将
http依赖项添加到pubspec.yaml文件,以发出http请求,如下所示:
http: ^0.12.0
使用flutter pub get将依赖项安装到项目。
- 要在应用中使用
http包,我们需要将其导入generate_live_caption.dart中,如下所示:
import 'package:http/http.dart' as http;
- 现在,我们定义一个方法
fetchResponse(),它使用一个图像文件并使用该图像为托管模型创建一个帖子,如下所示:
Future<Map<String, dynamic>> fetchResponse(File image) async {
final mimeTypeData =
lookupMimeType(image.path, headerBytes: [0xFF, 0xD8]).split('/');
final imageUploadRequest = http.MultipartRequest(
'POST',
Uri.parse(
"http://max-image-caption-generator-mytest865.apps.us-east-2.starter.openshift-online.com/model/predict"));
final file = await http.MultipartFile.fromPath('image', image.path,
contentType: MediaType(mimeTypeData[0], mimeTypeData[1]));
imageUploadRequest.fields['ext'] = mimeTypeData[1];
imageUploadRequest.files.add(file);
try {
final streamedResponse = await imageUploadRequest.send();
final response = await http.Response.fromStream(streamedResponse);
final Map<String, dynamic> responseData = json.decode(response.body);
parseResponse(responseData);
return responseData;
} catch (e) {
print(e);
return null;
}
}
在上述方法中,我们首先通过查看文件的头字节来找到所选文件的 mime 类型。 然后,我们按照托管 API 的要求初始化一个多部分请求。 我们将传递给函数的文件附加为image POST 参数。 由于image_picker存在一些错误,因此错误地将图像扩展名与文件名(例如filenamejpeg)混合在一起,因此我们在请求正文中明确传递了图像扩展名,这会在服务器端管理或验证文件扩展名时产生问题。 响应采用 JSON 格式,因此,我们需要使用json.decode()对其进行解码,并使用res.body传入响应的主体。 现在,我们通过调用下一步定义的parseResponse()来解析响应。 此外,我们使用catchError()检测并打印执行POST请求时可能发生的任何错误。
- 成功执行
POST请求并从模型中获得带有传递的图像的标题的响应之后,我们在parseResponse()方法内部解析响应,如下所示:
void parseResponse(var response) {
String resString = "";
var predictions = response['predictions'];
for(var prediction in predictions) {
var caption = prediction['caption'];
var probability = prediction['probability'];
resString = resString + '${caption}: ${probability}\n\n';
}
setState(() {
resultText = resString;
});
}
在上述方法中,我们首先存储response['predictions']中存在的所有预测的列表,并将其存储在prediction变量中。 现在,我们使用prediction变量遍历for each循环内的每个预测。 对于每个预测,我们分别取出prediction['caption']和prediction['probability']中存储的标题和概率。 我们将它们附加到resString字符串变量,该变量将包含所有预测的字幕以及概率。 最后,我们将resultText的状态设置为resString中存储的值。 resultText是此处的全局字符串变量,将在接下来的步骤中使用它来显示预测的字幕。
- 现在,我们修改
capturePictures(),以便每次捕获新图像时都会发出 HTTP 发布请求,如下所示:
capturePictures() async {
. . . . .
controller.takePicture(filePath).then((_){
File imgFile = File(filePath);
fetchResponse(imgFile);
});
}
在前面的代码片段中,我们向fetchResponse()添加了一个调用,并传入了图像文件。
- 现在,让我们修改
buildCameraPreview()以显示所有预测,如下所示:
Widget buildCameraPreview() {
. . . . .
return Container(
child: Column(
children: <Widget>[
Container(
. . . . .
child: CameraPreview(controller),
),
Text(resultText),
]
)
);
}
在前面的代码片段中,我们简单地将Text与result.Text相加。 result.Text是一个全局字符串变量,它将包含“步骤 5”中所述的所有预测,并声明如下:
String resultText = "Fetching Response..";
- 最后,我们重写
build()方法以为应用创建最终的脚手架,如下所示:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Generate Image Caption'),),
body: (controller.value.isInitialized)?buildCameraPreview():new Container(),
);
}
在前面的代码片段中,我们返回了一个标题为Generate Image Caption的appBar支架。 主体最初设置为空容器。 初始化摄像机控制器后,将更新主体以显示摄像机供稿以及预测的字幕。
- 最后,我们按以下方式处置摄像头控制器:
@override
void dispose() {
controller?.dispose();
super.dispose();
}
现在,我们已经成功创建了一种在屏幕上显示实时摄像机供稿的机制。 实时摄像头的提要以 5 秒的间隔被捕获,并作为输入发送到模型。 然后,所有捕获图像的预测字幕将显示在屏幕上。
在下一节中,我们现在创建最终的材质应用以将所有内容整合在一起。
创建材质应用
在使所有段正常工作之后,让我们创建最终的材质应用。 在main.dart文件中,我们创建StatelessWidget并覆盖build()方法,如下所示:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: GenerateLiveCaption()
);
}
}
最后,我们执行以下代码:
void main() => runApp(MyApp());
您应该能够拥有一个应用屏幕,如以下屏幕截图所示:

请注意图像中显示的标题,如下所示:
- 放在桌子上的便携式计算机。
- 放在桌子上的一台打开的便携式计算机。
- 放在一张木桌上的一台打开的便携式计算机。
这些标题的描述非常准确。 但是,由于训练数据集中相关图片的不可用,它们有时可能表现不佳。
总结
在本章中,我们了解了如何创建一个应用,该应用使用深层的 CNN 和 LSTM 为摄像机的提要实时生成字幕。 我们还看到了如何快速将以 Docker 映像形式提供的某些机器学习/深度学习模型部署到 Red Hat OpenShift,并以可调用 API 的形式轻松获取它们。 从应用开发人员的角度来看,这是至关重要的,因为当与一组机器学习开发人员一起工作时,他们通常会为您提供要使用的模型的 Docker 映像,这样您就无需在其中执行任何代码或配置。 系统。 可以将这种应用用于多种用途,例如为盲人创建辅助技术,生成当时发生的事件的成绩单,或者(例如)为孩子提供现场指导,以帮助他们识别环境中的物体。 我们介绍了如何应用 Flutter 相机插件并在框架上进行深度学习。
在下一章中,我们将研究如何开发用于执行应用安全性的深度学习模型。
六、构建人工智能认证系统
认证是任何应用中最突出的功能之一,无论它是本机移动软件还是网站,并且自从保护数据的需求以及与机密有关的隐私需求开始以来,认证一直是一个活跃的领域。 在互联网上共享的数据。 在本章中,我们将从基于 Firebase 的简单登录到应用开始,然后逐步改进以包括基于人工智能(AI)的认证置信度指标和 Google 的 ReCaptcha。 所有这些认证方法均以深度学习为核心,并提供了一种在移动应用中实现安全性的最新方法。
在本章中,我们将介绍以下主题:
- 一个简单的登录应用
- 添加 Firebase 认证
- 了解用于认证的异常检测
- 用于认证用户的自定义模型
- 实现 ReCaptcha 来避免垃圾邮件
- 在 Flutter 中部署模型
技术要求
对于移动应用,需要具有 Flutter 的 Visual Studio Code 和 Dart 插件以及 Firebase Console
一个简单的登录应用
我们将首先创建一个简单的认证应用,该应用使用 Firebase 认证对用户进行认证,然后再允许他们进入主屏幕。 该应用将允许用户输入其电子邮件和密码来创建一个帐户,然后使他们随后可以使用此电子邮件和密码登录。
以下屏幕快照显示了应用的完整流程:

该应用的小部件树如下:

现在让我们详细讨论每个小部件的实现。
创建 UI
让我们从创建应用的登录屏幕开始。 用户界面(UI)将包含两个TextFormField来获取用户的电子邮件 ID 和密码,RaisedButton进行注册/登录,以及FlatButton进行注册和登录操作之间的切换。
以下屏幕快照标记了将用于应用的第一个屏幕的小部件:

现在让我们创建应用的 UI,如下所示:
- 我们首先创建一个名为
signup_signin_screen.dart的新 dart 文件。 该文件包含一个有状态的小部件–SignupSigninScreen。 - 第一个屏幕中最上面的窗口小部件是
TextField,用于获取用户的邮件 ID。_createUserMailInput()方法可帮助我们构建窗口小部件:
Widget _createUserMailInput() {
return Padding(
padding: const EdgeInsets.fromLTRB(0.0, 100.0, 0.0, 0.0),
child: new TextFormField(
maxLines: 1,
keyboardType: TextInputType.emailAddress,
autofocus: false,
decoration: new InputDecoration(
hintText: 'Email',
icon: new Icon(
Icons.mail,
color: Colors.grey,
)),
validator: (value) => value.isEmpty ? 'Email can\'t be empty' : null,
onSaved: (value) => _usermail = value.trim(),
),
);
}
首先,我们使用EdgeInsets.fromLTRB()为小部件提供了填充。 这有助于我们在四个基本方向的每个方向(即左,上,右和下)上创建具有不同值的偏移量。 接下来,我们使用maxLines(输入的最大行数)创建了TextFormField,其值为1作为子级,它接收用户的电子邮件地址。 另外,根据输入类型TextInputType.emailAddress,我们指定了将在属性keyboardType中使用的键盘类型。 然后,将autoFocus设置为false。 然后,我们在装饰属性中使用InputDecoration提供hintText "Email"和图标Icons.mail。 为了确保用户在没有输入电子邮件地址或密码的情况下不要尝试登录,我们添加了一个验证器。 当尝试使用空字段登录时,将显示警告“电子邮件不能为空”。 最后,我们通过使用trim()删除所有尾随空格来修剪输入的值,然后将输入的值存储在_usermail字符串变量中。
- 与“步骤 2”中的
TextField相似,我们定义了下一个方法_createPasswordInput(),以创建用于输入密码的TextFormField():
Widget _createPasswordInput() {
return Padding(
padding: const EdgeInsets.fromLTRB(0.0, 15.0, 0.0, 0.0),
child: new TextFormField(
maxLines: 1,
obscureText: true,
autofocus: false,
decoration: new InputDecoration(
hintText: 'Password',
icon: new Icon(
Icons.lock,
color: Colors.grey,
)),
validator: (value) => value.isEmpty ? 'Password can\'t be empty' : null,
onSaved: (value) => _userpassword = value.trim(),
),
);
}
我们首先使用EdgeInsets.fromLTRB()在所有四个基本方向上提供填充,以在顶部提供15.0的偏移量。 接下来,我们创建一个TextFormField,其中maxLines为1,并将obscureText设置为true,将autofocus设置为false。 obscureText用于隐藏正在键入的文本。 我们使用InputDecoration提供hintText密码和一个灰色图标Icons.lock。 为确保文本字段不为空,使用了一个验证器,当传递空值时,该警告器会发出警告Password can't be empty,即用户尝试在不输入密码的情况下登录/注册。 最后,trim()用于删除所有尾随空格,并将密码存储在_userpassword字符串变量中。
- 接下来,我们在
_SignupSigninScreenState外部声明FormMode枚举,该枚举在两种模式SIGNIN和SIGNUP之间运行,如以下代码片段所示:
enum FormMode { SIGNIN, SIGNUP }
我们将对该按钮使用此枚举,该按钮将使用户既可以登录又可以注册。 这将帮助我们轻松地在两种模式之间切换。 枚举是一组用于表示常量值的标识符。
使用enum关键字声明枚举类型。 在enum内部声明的每个标识符都代表一个整数值; 例如,第一标识符具有值0,第二标识符具有值1。 默认情况下,第一个标识符的值为0。
- 让我们定义一个
_createSigninButton()方法,该方法返回按钮小部件以使用户注册并登录:
Widget _createSigninButton() {
return new Padding(
padding: EdgeInsets.fromLTRB(0.0, 45.0, 0.0, 0.0),
child: SizedBox(
height: 40.0,
child: new RaisedButton(
elevation: 5.0,
shape: new RoundedRectangleBorder(borderRadius: new BorderRadius.circular(30.0)),
color: Colors.blue,
child: _formMode == FormMode.SIGNIN
? new Text('SignIn',
style: new TextStyle(fontSize: 20.0, color: Colors.white))
: new Text('Create account',
style: new TextStyle(fontSize: 20.0, color: Colors.white)),
onPressed: _signinSignup,
),
));
}
我们从Padding开始,将45.0的按钮offset置于顶部,然后将SizedBox和40.0的height作为子项,并将RaisedButton作为其子项。 使用RoundedRectangleBorder()为凸起的按钮赋予圆角矩形形状,其边框半径为30.0,颜色为blue。 作为子项添加的按钮的文本取决于_formMode的当前值。 如果_formMode的值(FormMode枚举的一个实例)为FormMode.SIGNIN,则按钮显示SignIn,否则创建帐户。 按下按钮时将调用_signinSignup方法,该方法将在后面的部分中介绍。
- 现在,我们将第四个按钮添加到屏幕上,以使用户在
SIGNIN和SIGNUP表单模式之间切换。 我们定义返回FlatButton的_createSigninSwitchButton()方法,如下所示:
Widget _createSigninSwitchButton() {
return new FlatButton(
child: _formMode == FormMode.SIGNIN
? new Text('Create an account',
style: new TextStyle(fontSize: 18.0, fontWeight: FontWeight.w300))
: new Text('Have an account? Sign in',
style:
new TextStyle(fontSize: 18.0, fontWeight: FontWeight.w300)),
onPressed: _formMode == FormMode.SIGNIN
? _switchFormToSignUp
: _switchFormToSignin,
);
}
如果_formMode的当前值为SIGNIN并按下按钮,则应更改为SIGNUP并显示Create an account。 否则,如果_formMode将SIGNUP作为其当前值,并且按下按钮,则该值应切换为由文本Have an account? Sign in表示的SIGNIN。 使用三元运算符创建RaisedButton的Text子级时,添加了在文本之间切换的逻辑。 onPressed属性使用非常相似的逻辑,该逻辑再次检查_formMode的值以在模式之间切换并使用_switchFormToSignUp和_switchFormToSignin方法更新_formMode的值。 我们将在“步骤 7”和 8 中定义_switchFormToSignUp和_switchFormToSignin方法。
- 现在,我们定义
_switchFormToSignUp()如下:
void _switchFormToSignUp() {
_formKey.currentState.reset();
setState(() {
_formMode = FormMode.SIGNUP;
});
}
此方法重置_formMode的值并将其更新为FormMode.SIGNUP。 更改setState()内部的值有助于通知框架该对象的内部状态已更改,并且 UI 可能需要更新。
- 我们以与
_switchFormToSignUp()非常相似的方式定义_switchFormToSignin():
void _switchFormToSignin() {
_formKey.currentState.reset();
setState(() {
_formMode = FormMode.SIGNIN;
});
}
此方法重置_formMode的值并将其更新为FormMode.SIGNIN。 更改setState()内部的值有助于通知框架该对象的内部状态已更改,并且 UI 可能需要更新。
- 现在,让我们将所有屏幕小部件
Email TextField,Password TextFied,SignIn Button和FlatButton切换为在单个容器中进行注册和登录。 为此,我们定义了一种方法createBody(),如下所示:
Widget _createBody(){
return new Container(
padding: EdgeInsets.all(16.0),
child: new Form(
key: _formKey,
child: new ListView(
shrinkWrap: true,
children: <Widget>[
_createUserMailInput(),
_createPasswordInput(),
_createSigninButton(),
_createSigninSwitchButton(),
_createErrorMessage(),
],
),
)
);
}
此方法返回一个以Form作为子元素的新Container并为其填充16.0。 表单使用_formKey作为其键,并添加ListView作为其子级。 ListView的元素是我们在前述方法中创建的用于添加TextFormFields和Buttons的小部件。 shrinkWrap设置为true,以确保ListView仅占用必要的空间,并且不会尝试扩展和填充整个屏幕
Form类用于将多个FormFields一起分组和验证。 在这里,我们使用Form将两个TextFormFields,一个RaisedButton和一个FlatButton包装在一起。
- 这里要注意的一件事是,由于进行认证,因此用户最终将成为网络操作,因此可能需要一些时间来发出网络请求。 在此处添加进度条可防止在进行网络操作时 UI 的死锁。 我们声明
boolean标志_loading,当网络操作开始时将其设置为true。 现在,我们定义一种_createCircularProgress()方法,如下所示:
Widget _createCircularProgress(){
if (_loading) {
return Center(child: CircularProgressIndicator());
} return Container(height: 0.0, width: 0.0,);
}
仅当_loading为true并且正在进行网络操作时,该方法才返回CircularProgressIndicator()。
- 最后,让我们在
build()方法内添加所有小部件:
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Firebase Authentication'),
),
body: Stack(
children: <Widget>[
_createBody(),
_createCircularProgress(),
],
));
}
从build()内部,添加包含应用标题的AppBar变量后,我们返回一个支架。 支架的主体包含一个带有子项的栈,这些子项是_createBody()和_createCircularProgress() 函数调用返回的小部件。
现在,我们已经准备好应用的主要 UI 结构。
可以在这个页面中找到SignupSigninScreen的完整代码。
在下一部分中,我们将介绍将 Firebase 认证添加到应用中涉及的步骤。
添加 Firebase 认证
如前所述,在“简单登录应用”部分中,我们将使用用户的电子邮件和密码通过 Firebase 集成认证。
要在 Firebase 控制台上创建和配置 Firebase 项目,请参考“附录”。
以下步骤详细讨论了如何在 Firebase Console 上设置项目:
- 我们首先在 Firebase 控制台上选择项目:

- 接下来,我们将在
Develop菜单中单击Authentication选项:

这将带我们进入认证屏幕。
- 迁移到登录标签并启用登录提供者下的“电子邮件/密码”选项:

这是设置 Firebase 控制台所需的全部。
接下来,我们将 Firebase 集成到代码中。 这样做如下:
- 迁移到 Flutter SDK 中的项目,然后将
firebase-auth添加到应用级别build.gradle文件中:
implementation 'com.google.firebase:firebase-auth:18.1.0'
- 为了使
FirebaseAuthentication在应用中正常工作,我们将在此处使用firebase_auth插件。 在pubspec.yaml文件的依赖项中添加插件依赖项:
firebase_auth: 0.14.0+4
确保运行flutter pub get以安装依赖项。
现在,让我们编写一些代码以在应用内部提供 Firebase 认证功能。
创建auth.dart
现在,我们将创建一个 Dart 文件auth.dart。 该文件将作为访问firebase_auth插件提供的认证方法的集中点:
- 首先,导入
firebase_auth插件:
import 'package:firebase_auth/firebase_auth.dart';
- 现在,创建一个抽象类
BaseAuth,该类列出了所有认证方法,并充当 UI 组件和认证方法之间的中间层:
abstract class BaseAuth {
Future<String> signIn(String email, String password);
Future<String> signUp(String email, String password);
Future<String> getCurrentUser();
Future<void> signOut();
}
顾名思义,这些方法将使用认证的四个主要函数:
signIn():使用电子邮件和密码登录已经存在的用户signUp():使用电子邮件和密码为新用户创建帐户getCurrentUser():获取当前登录的用户signOut():注销已登录的用户
这里要注意的重要一件事是,由于这是网络操作,因此所有方法都异步操作,并在执行完成后返回Future值。
- 创建一个实现
BaseAuth的Auth类:
class Auth implements BaseAuth {
//. . . . .
}
在接下来的步骤中,我们将定义BaseAuth中声明的所有方法。
- 创建
FirebaseAuth的实例:
final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;
signIn()方法实现如下:
Future<String> signIn(String email, String password) async {
AuthResult result = await _firebaseAuth.signInWithEmailAndPassword(email: email, password: password);
FirebaseUser user = result.user;
return user.uid;
}
此方法接收用户的电子邮件和密码,然后调用signInWithEmailAndPassword(),并传递电子邮件和密码以登录已经存在的用户。 登录操作完成后,将返回AuthResult实例。 我们将其存储在result中,还使用result.user,它返回FirebaseUser.。它可用于获取与用户有关的信息,例如他们的uid,phoneNumber和photoUrl。 在这里,我们返回user.uid,它是每个现有用户的唯一标识。 如前所述,由于这是网络操作,因此它异步运行,并在执行完成后返回Future。
- 接下来,我们将定义
signUp()方法以添加新用户:
Future<String> signUp(String email, String password) async {
AuthResult result = await _firebaseAuth.createUserWithEmailAndPassword(email: email, password: password);
FirebaseUser user = result.user;
return user.uid;
}
前面的方法接收在注册过程中使用的电子邮件和密码,并将其值传递给createUserWithEmailAndPassword。 类似于上一步中定义的对象,此调用还返回AuthResult对象,该对象还用于提取FirebaseUser。 最后,signUp方法返回新创建的用户的uid。
- 现在,我们将定义
getCurrentUser():
Future<String> getCurrentUser() async {
FirebaseUser user = await _firebaseAuth.currentUser();
return user.uid;
}
在先前定义的函数中,我们使用_firebaseAuth.currentUser()提取当前登录用户的信息。 此方法返回包装在FirebaseUser对象中的完整信息。 我们将其存储在user变量中。 最后,我们使用user.uid返回用户的uid。
- 接下来,我们执行
signOut():
Future<void> signOut() async {
return _firebaseAuth.signOut();
}
此函数仅在当前FirebaseAuth实例上调用signOut()并注销已登录的用户。
至此,我们已经完成了用于实现 Firebase 认证的所有基本编码。
可以在这个页面中查看auth.dart中的整个代码。
现在让我们看看如何在应用内部使认证生效。
在SignupSigninScreen中添加认证
在本节中,我们将在SignupSigninScreen中添加 Firebase 认证。
我们在signup_signin_screen.dart文件中定义了_signinSignup()方法。 当按下登录按钮时,将调用该方法。 该方法的主体如下所示:
void _signinSignup() async {
setState(() {
_loading = true;
});
String userId = "";
if (_formMode == FormMode.SIGNIN) {
userId = await widget.auth.signIn(_usermail, _userpassword);
} else {
userId = await widget.auth.signUp(_usermail, _userpassword);
}
setState(() {
_loading = false;
});
if (userId.length > 0 && userId != null && _formMode == FormMode.SIGNIN) {
widget.onSignedIn();
}
}
在上述方法中,我们首先将_loading的值设置为true,以便进度条显示在屏幕上,直到登录过程完成。 接下来,我们创建一个userId字符串,一旦登录/登录操作完成,该字符串将存储userId的值。 现在,我们检查_formMode的当前值。 如果等于FormMode.SIGNIN,则用户希望登录到现有帐户。 因此,我们使用传递到SignupSigninScreen构造器中的实例来调用Auth类内部定义的signIn()方法。
这将在后面的部分中详细讨论。 否则,如果_formMode的值等于FormMode.SIGNUP,则将调用Auth类的signUp()方法,并传递用户的邮件和密码以创建新帐户。 一旦成功完成登录/注册,userId变量将用于存储用户的 ID。 整个过程完成后,将_loading设置为false,以从屏幕上删除循环进度指示器。 另外,如果在用户登录到现有帐户时userId具有有效值,则将调用onSignedIn(),这会将用户定向到应用的主屏幕。
此方法也传递给SignupSigninScreen的构造器,并将在后面的部分中进行讨论。 最后,我们将整个主体包裹在try-catch块中,以便在登录过程中发生的任何异常都可以捕获而不会导致应用崩溃,并可以在屏幕上显示。
创建主屏幕
我们还需要确定认证状态,即用户在启动应用时是否已登录,如果已经登录,则将其定向到主屏幕。如果尚未登录,则应显示SignInSignupScreen 首先,在完成该过程之后,将启动主屏幕。 为了实现这一点,我们在新的 dart 文件main_screen.dart中创建一个有状态的小部件MainScreen,然后执行以下步骤:
- 我们将从定义枚举
AuthStatus开始,该枚举表示用户的当前认证状态,可以登录或不登录:
enum AuthStatus {
NOT_SIGNED_IN,
SIGNED_IN,
}
- 现在,我们创建
enum类型的变量来存储当前认证状态,其初始值设置为NOT_SIGNED_IN:
AuthStatus authStatus = AuthStatus.NOT_SIGNED_IN;
- 初始化小部件后,我们将通过覆盖
initState()方法来确定用户是否已登录:
@override
void initState() {
super.initState();
widget.auth.getCurrentUser().then((user) {
setState(() {
if (user != null) {
_userId = user;
}
authStatus =
user == null ? AuthStatus.NOT_SIGNED_IN : AuthStatus.SIGNED_IN;
});
});
}
使用在构造器中传递的类的实例调用Auth类的getCurrentUser()。 如果该方法返回的值不为null,则意味着用户已经登录。因此,_userId字符串变量的值设置为返回的值。 另外,将authStatus设置为AuthStatus.SIGNED_IN.,否则,如果返回的值为null,则意味着没有用户登录,因此authStatus的值设置为AuthStatus.NOT_SIGNED_IN。
- 现在,我们将定义另外两个方法
onSignIn()和onSignOut(),以确保将认证状态正确存储在变量中,并相应地更新用户界面:
void _onSignedIn() {
widget.auth.getCurrentUser().then((user){
setState(() {
_userId = user;
});
});
setState(() {
authStatus = AuthStatus.SIGNED_IN;
});
}
void _onSignedOut() {
setState(() {
authStatus = AuthStatus.NOT_SIGNED_IN;
_userId = "";
});
}
_onSignedIn()方法检查用户是否已经登录,并将authStatus设置为AuthStatus.SIGNED_IN.。 _onSignedOut()方法检查用户是否已注销,并将authStatus设置为AuthStatus.SIGNED_OUT。
- 最后,我们重写
build方法将用户定向到正确的屏幕:
@override
Widget build(BuildContext context) {
if(authStatus == AuthStatus.SIGNED_OUT) {
return new SignupSigninScreen(
auth: widget.auth,
onSignedIn: _onSignedIn,
);
} else {
return new HomeScreen(
userId: _userId,
auth: widget.auth,
onSignedOut: _onSignedOut,
);
}
}
如果authStatus为AuthStatus.SIGNED_OUT,则返回SignupSigninScreen,并传递auth实例和_onSignedIn()方法。 否则,将直接返回HomeScreen,并传递已登录用户的userId,Auth实例类和_onSignedOut()方法。
在下一部分中,我们将为应用添加一个非常简单的主屏幕。
创建主屏幕
由于我们对认证部分更感兴趣,因此主屏幕(即成功登录后指向用户的屏幕)应该非常简单。 它仅包含一些文本和一个注销选项。 正如我们对所有先前的屏幕和小部件所做的一样,我们首先创建一个home_screen.dart文件和一个有状态的HomeScreen小部件。
主屏幕将显示如下:

此处的完整代码位于重写的build()方法内部:
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Firebase Authentication'),
actions: <Widget>[
new FlatButton(
child: new Text('Logout',
style: new TextStyle(fontSize: 16.0, color: Colors.white)),
onPressed: _signOut
)
],
),
body: Center(child: new Text('Hello User',
style: new TextStyle(fontSize: 32.0))
),
);
}
我们在此处返回Scaffold,其中包含标题为Text Firebase Authentication的AppBar和actions属性的小部件列表。 actions用于在应用标题旁边添加小部件列表到应用栏中。 在这里,它仅包含FlatButton,Logout,在按下时将调用_signOut。
_signOut()方法显示如下:
_signOut() async {
try {
await widget.auth.signOut();
widget.onSignedOut();
} catch (e) {
print(e);
}
}
该方法主要是调用Auth类中定义的signOut()方法,以将用户从应用中注销。 回忆传入HomeScreen的MainScreen的_onSignedOut()方法。 当用户退出时,该方法在此处用作widget.onSignedOut()来将authStatus更改为SIGNED_OUT。 同样,它包装在try-catch块中,以捕获并打印此处可能发生的任何异常。
至此,应用的主要组件已经准备就绪,现在让我们创建最终的材质应用。
创建main.dart
在main.dart内部,我们创建Stateless Widget,App,并覆盖build()方法,如下所示:
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Firebase Authentication',
debugShowCheckedModeBanner: false,
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MainScreen(auth: new Auth()));
}
该方法从主屏幕返回MaterialApp,以提供标题,主题。
了解用于认证的异常检测
异常检测是机器学习的一个备受关注的分支。 该术语含义简单。 基本上,它是用于检测异常的方法的集合。 想象一袋苹果。 识别并挑选坏苹果将是异常检测的行为。
异常检测以几种方式执行:
- 通过使用列的最小最大范围来识别数据集中与其余样本非常不同的数据样本
- 通过将数据绘制为线形图并识别图中的突然尖峰
- 通过围绕高斯曲线绘制数据并将最末端的点标记为离群值(异常)
一些常用的方法是支持向量机,贝叶斯网络和 K 最近邻。 在本节中,我们将重点介绍与安全性相关的异常检测。
假设您通常在家中登录应用上的帐户。 如果您突然从数千英里外的位置登录帐户,或者在另一种情况下,您以前从未使用过公共计算机登录帐户,那将是非常可疑的,但是突然有一天您这样做。 另一个可疑的情况可能是您尝试 10-20 次密码,每次在成功成功登录之前每次都输入错误密码。 当您的帐户遭到盗用时,所有这些情况都是可能的行为。 因此,重要的是要合并一个能够确定您的常规行为并对异常行为进行分类的系统。 换句话说,即使黑客使用了正确的密码,企图破坏您的帐户的尝试也应标记为异常。
这带给我们一个有趣的观点,即确定用户的常规行为。 我们如何做到这一点? 什么是正常行为? 它是针对每个用户的还是一个通用概念? 问题的答案是它是非常特定于用户的。 但是,行为的某些方面对于所有用户而言都可以相同。 一个应用可能会在多个屏幕上启动登录。 单个用户可能更喜欢其中一种或两种方法。 这将导致特定于该用户的特定于用户的行为。 但是,如果尝试从未由开发人员标记为登录屏幕的屏幕进行登录,则无论是哪个用户尝试登录,都肯定是异常的。
在我们的应用中,我们将集成一个这样的系统。 为此,我们将记录一段时间内我们应用的许多用户进行的所有登录尝试。 我们将特别注意他们尝试登录的屏幕以及它们传递给系统的数据类型。 一旦收集了很多这些样本,就可以根据用户执行的任何操作来确定系统对认证的信心。 如果系统在任何时候认为用户表现出的行为与他们的惯常行为相差很大,则该用户将未经认证并被要求验证其帐户详细信息。
让我们从创建预测模型开始,以确定用户认证是常规的还是异常的。
用于认证用户的自定义模型
我们将本节分为两个主要子节:
- 构建用于认证有效性检查的模型
- 托管自定义认证验证模型
让我们从第一部分开始。
构建用于认证有效性检查的模型
在本部分中,我们将构建模型来确定是否有任何用户正在执行常规登录或异常登录:
- 我们首先导入必要的模块,如下所示:
import sys
import os
import json
import pandas
import numpy
from keras.models import Sequential
from keras.layers import LSTM, Dense, Dropout
from keras.layers.embeddings import Embedding
from keras.preprocessing import sequence
from keras.preprocessing.text import Tokenizer
from collections import OrderedDict
- 现在,我们将数据集导入到项目中。 可以在这里中找到该数据集:
csv_file = 'data.csv'
dataframe = pandas.read_csv(csv_file, engine='python', quotechar='|', header=None)
count_frame = dataframe.groupby([1]).count()
print(count_frame)
total_req = count_frame[0][0] + count_frame[0][1]
num_malicious = count_frame[0][1]
print("Malicious request logs in dataset: {:0.2f}%".format(float(num_malicious) / total_req * 100))
前面的代码块将 CSV 数据集加载到项目中。 它还会打印一些与数据有关的统计信息,如下所示:

- 我们在上一步中加载的数据目前尚无法使用,无法进行深度学习。 在此步骤中,我们将其分为特征列和标签列,如下所示:
X = dataset[:,0]
Y = dataset[:,1]
- 接下来,我们将删除数据集中包含的某些列,因为我们不需要所有这些列来构建简单的模型:
for index, item in enumerate(X):
reqJson = json.loads(item, object_pairs_hook=OrderedDict)
del reqJson['timestamp']
del reqJson['headers']
del reqJson['source']
del reqJson['route']
del reqJson['responsePayload']
X[index] = json.dumps(reqJson, separators=(',', ':'))
- 接下来,我们将在剩余的请求正文上执行分词。 分词是一种用于将大文本块分解为较小文本的方法,例如将段落分成句子,将句子分成单词。 我们这样做如下:
tokenizer = Tokenizer(filters='\t\n', char_level=True)
tokenizer.fit_on_texts(X)
- 分词之后,我们将请求正文中的文本转换为单词向量,如下一步所示。 我们将数据集和
DataFrame标签分为两部分,即 75%-25%,以进行训练和测试:
num_words = len(tokenizer.word_index)+1
X = tokenizer.texts_to_sequences(X)
max_log_length = 1024
train_size = int(len(dataset) * .75)
X_processed = sequence.pad_sequences(X, maxlen=max_log_length)
X_train, X_test = X_processed[0:train_size], X_processed[train_size:len(X_processed)]
Y_train, Y_test = Y[0:train_size], Y[train_size:len(Y)]
- 接下来,我们基于长短期记忆(LSTM)创建基于循环神经网络(RNN)的学习方法,来识别常规用户行为。 将单词嵌入添加到层中,以帮助维持单词向量和单词之间的关系:
model = Sequential()
model.add(Embedding(num_words, 32, input_length=max_log_length))
model.add(Dropout(0.5))
model.add(LSTM(64, recurrent_dropout=0.5))
model.add(Dropout(0.5))
model.add(Dense(1, activation='sigmoid'))
我们的输出是单个神经元,在正常登录的情况下,该神经元保存0;在登录异常的情况下,则保存1。
- 现在,我们以精度作为度量标准编译模型,而损失则作为二进制交叉熵来计算:
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
print(model.summary())
- 现在,我们准备进行模型的训练:
model.fit(X_train, Y_train, validation_split=0.25, epochs=3, batch_size=128)
- 我们将快速检查模型所达到的准确率。 当前模型的准确率超过 96%:
score, acc = model.evaluate(X_test, Y_test, verbose=1, batch_size=128)
print("Model Accuracy: {:0.2f}%".format(acc * 100))
下面的屏幕快照显示了前面代码块的输出:

- 现在,我们保存模型权重和模型定义。 我们稍后将它们加载到 API 脚本中,以验证用户的认证:
model.save_weights('lstm-weights.h5')
model.save('lstm-model.h5')
现在,我们可以将认证模型作为 API 进行托管,我们将在下一部分中进行演示。
托管自定义认证验证模型
在本节中,我们将创建一个 API,用于在用户向模型提交其登录请求时对其进行认证。 请求标头将被解析为字符串,并且模型将使用它来预测登录是否有效:
- 我们首先导入创建 API 服务器所需的模块:
from sklearn.externals import joblib
from flask import Flask, request, jsonify
from string import digits
import sys
import os
import json
import pandas
import numpy
import optparse
from keras.models import Sequential, load_model
from keras.preprocessing import sequence
from keras.preprocessing.text import Tokenizer
from collections import OrderedDict
- 现在,我们实例化一个
Flask应用对象。 我们还将从上一节“构建用于认证有效性检查的模型”中加载保存的模型定义和模型权重。然后,我们重新编译模型,并使用_make_predict_function( )方法创建其预测方法,如以下步骤所示:
app = Flask(__name__)
model = load_model('lstm-model.h5')
model.load_weights('lstm-weights.h5')
model.compile(loss = 'binary_crossentropy', optimizer = 'adam', metrics = ['accuracy'])
model._make_predict_function()
- 然后,我们创建一个
remove_digits()函数,该函数用于从提供给它的输入中去除所有数字。 这将用于在将请求正文文本放入模型之前清除它:
def remove_digits(s: str) -> str:
remove_digits = str.maketrans('', '', digits)
res = s.translate(remove_digits)
return res
- 接下来,我们将在 API 服务器中创建
/login路由。 该路由由login()方法处理,并响应GET和POST请求方法。 正如我们对训练输入所做的那样,我们删除了请求标头中的非必要部分。 这可以确保模型将对数据进行预测,类似于对其进行训练的数据:
@app.route('/login', methods=['GET, POST'])
def login():
req = dict(request.headers)
item = {}
item["method"] = str(request.method)
item["query"] = str(request.query_string)
item["path"] = str(request.path)
item["statusCode"] = 200
item["requestPayload"] = []
## MORE CODE BELOW THIS LINE
## MORE CODE ABOVE THIS LINE
response = {'result': float(prediction[0][0])}
return jsonify(response)
- 现在,我们将代码添加到
login()方法中,该方法将标记请求正文并将其传递给模型以执行有关登录请求有效性的预测,如下所示:
@app.route('/login', methods=['GET, POST'])
def login():
...
## MORE CODE BELOW THIS LINE
X = numpy.array([json.dumps(item)])
log_entry = "store"
tokenizer = Tokenizer(filters='\t\n', char_level=True)
tokenizer.fit_on_texts(X)
seq = tokenizer.texts_to_sequences([log_entry])
max_log_length = 1024
log_entry_processed = sequence.pad_sequences(seq, maxlen=max_log_length)
prediction = model.predict(log_entry_processed)
## MORE CODE ABOVE THIS LINE
...
最后,应用以 JSON 字符串的形式返回其对用户进行认证的信心。
- 最后,我们使用
app的run()方法启动服务器脚本:
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000)
- 将此文件另存为
main.py。 要开始执行服务器,请打开一个新终端并使用以下命令:
python main.py
服务器监听其运行系统的所有传入 IP。 通过在0.0.0.0 IP 上运行它,可以实现这一点。 如果我们希望稍后在基于云的服务器上部署脚本,则需要这样做。 如果不指定0.0.0.0主机,则默认情况下会使它监听127.0.0.1,这不适合在公共服务器上进行部署。 您可以在此处详细了解这些地址之间的区别。
在下一节中,我们将看到如何将 ReCaptcha 集成到迄今为止在该项目中构建的应用中。 之后,我们将把本节中构建的 API 集成到应用中。
实现 ReCaptcha 来保护垃圾邮件
为了为 Firebase 认证增加另一层安全性,我们将使用 ReCaptcha。 这是 Google 所支持的一项测试,可帮助我们保护数据免受垃圾邮件和滥用行为的自动 bot 攻击。 该测试很简单,很容易被人类解决,但是却阻碍了漫游器和恶意用户的使用。
要了解有关 ReCaptcha 及其用途的更多信息,请访问这里。
ReCAPTCHA v2
在本节中,我们将把 ReCaptcha 版本 2 集成到我们的应用中。 在此版本中,向用户显示一个简单的复选框。 如果刻度变为绿色,则表明用户已通过验证。
另外,还可以向用户提出挑战,以区分人和机器人。 这个挑战很容易被人类解决。 他们要做的就是根据说明选择一堆图像。 使用 ReCaptcha 进行认证的传统流程如下所示:

一旦用户能够验证其身份,他们就可以成功登录。
获取 API 密钥
要在我们的应用内部使用 ReCaptcha,我们需要在reCAPTCHA管理控制台中注册该应用,并获取站点密钥和秘密密钥。 为此,请访问这里并注册该应用。 您将需要导航到“注册新站点”部分,如以下屏幕截图所示:

我们可以通过以下两个简单步骤来获取 API 密钥:
- 首先提供一个域名。 在这里,我们将在 reCAPTCHA v2 下选择 reCAPTCHA Android。
- 选择 Android 版本后,添加项目的包名称。 正确填写所有信息后,单击“注册”。
这将引导您到显示站点密钥和秘密密钥的屏幕,如以下屏幕快照所示:

将站点密钥和秘密密钥复制并保存到安全位置。 我们将在编码应用时使用它们。
代码整合
为了在我们的应用中包含 ReCaptcha v2,我们将使用 Flutter 包flutter_recaptcha_v2。 将flutter_recaptcha_v2:0.1.0依赖项添加到pubspec.yaml文件中,然后在终端中运行flutter packages get以获取所需的依赖项。 以下步骤详细讨论了集成:
- 我们将代码添加到
signup_signin_screen.dart。 首先导入依赖项:
import 'package:flutter_recaptcha_v2/flutter_recaptcha_v2.dart';
- 接下来,创建一个
RecaptchaV2Controller实例:
RecaptchaV2Controller recaptchaV2Controller = RecaptchaV2Controller();
- reCAPTCHA 复选框将添加为小部件。 首先,让我们定义一个返回小部件的
_createRecaptcha()方法:
Widget _createRecaptcha() {
return RecaptchaV2(
apiKey: "Your Site Key here",
apiSecret: "Your API Key here",
controller: recaptchaV2Controller,
onVerifiedError: (err){
print(err);
},
onVerifiedSuccessfully: (success) {
setState(() {
if (success) {
_signinSignup();
} else {
print('Failed to verify');
}
});
},
);
}
在上述方法中,我们仅使用RecaptchaV2()构造器,即可为特定属性指定值。 添加您先前在apiKey和apiSecret属性中注册时保存的站点密钥和秘密密钥。 我们使用先前为属性控制器创建的recaptcha控制器recaptchaV2Controller的实例。 如果成功验证了用户,则将调用_signinSignup()方法以使用户登录。如果在验证期间发生错误,我们将打印错误。
- 现在,由于在用户尝试登录时应显示
reCaptcha,因此我们将createSigninButton()中的登录凸起按钮的onPressed属性修改为recaptchaV2Controller:
Widget _createSigninButton() {
. . . . . . .
return new Padding(
. . . . . . .
child: new RaisedButton(
. . . . . .
//Modify the onPressed property
onPressed: recaptchaV2Controller.show
)
)
}
- 最后,我们将
_createRecaptcha()添加到build()内部的主体栈中:
@override
Widget build(BuildContext context) {
. . . . . . .
return new Scaffold(
. . . . . . .
body: Stack(
children: <Widget>[
_createBody(),
_createCircularProgress(),
//Add reCAPTCHA Widget
_createRecaptcha()
],
));
}
这就是一切! 现在,我们具有比 Firebase 认证更高的安全级别,可以保护应用的数据免受自动机器人的攻击。 现在让我们看一下如何集成定制模型以检测恶意用户。
在 Flutter 中部署模型
至此,我们的 Firebase 认证应用与 ReCaptcha 保护一起运行。 现在,让我们添加最后的安全层,该层将不允许任何恶意用户进入应用。
我们已经知道该模型位于以下端点。 我们只需从应用内部进行 API 调用,传入用户提供的电子邮件和密码,并从模型中获取结果值。 该值将通过使用阈值结果值来帮助我们判断登录是否是恶意的。
如果该值小于 0.20,则认为该登录名是恶意的,并且屏幕上将显示以下消息:

现在,让我们看一下在 Flutter 应用中部署模型的步骤:
- 首先,由于我们正在获取数据并且将使用网络调用(即 HTTP 请求),因此我们需要向
pubspec.yaml文件添加http依赖项,并按以下方式导入:
import 'package:http/http.dart' as http;
- 首先在
auth.dart:内部定义的BaseAuth抽象类中添加以下函数声明
Future<double> isValidUser(String email, String password);
- 现在,让我们在
Auth类中定义isValidUser()函数:
Future<double> isValidUser(String email, String password) async{
final response = await http.Client()
.get('http://34.67.160.232:8000/login?user=$email&password=$password');
var jsonResponse = json.decode(response.body);
var val = '${jsonResponse["result"]}';
double result = double.parse(val);
return result;
}
此函数将用户的电子邮件和密码作为参数,并将它们附加到请求 URL,以便为特定用户生成输出。 get request响应存储在变量响应中。 由于响应为 JSON 格式,因此我们使用json.decode()对其进行解码,并将解码后的响应存储在另一个变量响应中。 现在,我们使用‘${jsonResponse["result"]}'访问jsonResponse中的结果值,使用double.parse()将其转换为双精度类型整数,并将其存储在结果中。 最后,我们返回结果的值。
- 为了激活代码内部的恶意检测,我们从
SigninSignupScreen调用了isValidUser()方法。 当具有现有帐户的用户选择从if-else块内部登录时,将调用此方法:
if (_formMode == FormMode.SIGNIN) {
var val = await widget.auth.isValidUser(_usermail, _userpassword);
. . . .
} else {
. . . .
}
isValidUser返回的值存储在val变量中。
- 如果该值小于 0.20,则表明登录活动是恶意的。 因此,我们将异常抛出并在 catch 块内抛出
catch并在屏幕上显示错误消息。 这可以通过创建自定义异常类MalicousUserException来完成,该类在实例化时返回一条错误消息:
class MaliciousUserException implements Exception {
String message() => 'Malicious login! Please try later.';
}
- 现在,我们将在调用
isValidUser()之后添加if块,以检查是否需要抛出异常:
var val = await widget.auth.isValidUser(_usermail, _userpassword);
//Add the if block
if(val < 0.20) {
throw new MaliciousUserException();
}
- 现在,该异常已捕获在
catch块内,并且不允许用户继续登录。此外,我们将_loading设置为false以表示不需要进一步的网络操作:
catch(MaliciousUserException) {
setState(() {
_loading = false;
_errorMessage = 'Malicious user detected. Please try again later.';
});
这就是一切! 我们之前基于 Firebase 认证创建的 Flutter 应用现在可以在后台运行智能模型的情况下找到恶意用户。
总结
在本章中,我们了解了如何使用 Flutter 和由 Firebase 支持的认证系统构建跨平台应用,同时结合了深度学习的优势。 然后,我们了解了如何将黑客攻击尝试归类为一般用户行为中的异常现象,并创建了一个模型来对这些异常现象进行分类以防止恶意用户登录。最后,我们使用了 Google 的 ReCaptcha 来消除对该应用的垃圾邮件使用,因此,使其在自动垃圾邮件或脚本化黑客攻击方面更具弹性。
在下一章中,我们将探索一个非常有趣的项目–使用移动应用上的深度学习生成音乐成绩单。
七、语音/多媒体处理 - 使用 AI 生成音乐
鉴于人工智能(AI)的应用越来越多,将 AI 与音乐结合使用的想法已经存在了很长时间,并且受到了广泛的研究。 由于音乐是一系列音符,因此它是时间序列数据集的经典示例。 最近证明时间序列数据集在许多预测领域中非常有用–股市,天气模式,销售模式以及其他基于时间的数据集。 循环神经网络(RNN)是处理时间序列数据集的最多模型之一。 对 RNN 进行的流行增强称为长短期记忆(LSTM)神经元。 在本章中,我们将使用 LSTM 处理音符。
多媒体处理也不是一个新话题。 在本项目系列的早期,我们在多章中详细介绍了图像处理。 在本章中,我们将讨论并超越图像处理,并提供一个带有音频的深度学习示例。 我们将训练 Keras 模型来生成音乐样本,每次都会生成一个新样本。 然后,我们将此模型与 Flutter 应用结合使用,以通过 Android 和 iOS 设备上的音频播放器进行部署。
在本章中,我们将介绍以下主题:
- 设计项目的架构
- 了解多媒体处理
- 开发基于 RNN 的音乐生成模型
- 在 Android 和 iOS 上部署音频生成 API
让我们首先概述该项目的架构。
设计项目的架构
该项目的架构与作为应用部署的常规深度学习项目略有不同。 我们将有两组不同的音乐样本。 第一组样本将用于训练可以生成音乐的 LSTM 模型。 另一组样本将用作 LSTM 模型的随机输入,该模型将输出生成的音乐样本。 我们稍后将开发和使用的基于 LSTM 的模型将部署在 Google Cloud Platform(GCP)上。 但是,您可以将其部署在 AWS 或您选择的任何其他主机上。
下图总结了将在本项目中使用的不同组件之间的交互:

移动应用要求部署在服务器上的模型生成新的音乐样本。 该模型使用随机音乐样本作为输入,以使其通过预先训练的模型来生成新的音乐样本。 然后,新的音乐样本由移动设备获取并播放给用户。
您可以将此架构与我们之前介绍的架构进行比较,在该架构中,将有一组用于训练的数据样本,然后将模型部署在云上或本地,并用于作出预测。
我们还可以更改此项目架构,以在存在为 Dart 语言编写的 midi 文件处理库的情况下在本地部署模型。 但是,在撰写本文时,还没有与我们在开发模型时使用的 Python midi 文件库的要求兼容的稳定库。
让我们从学习多媒体处理的含义以及如何使用 OpenCV 处理多媒体文件开始。
了解多媒体处理
多媒体是几乎所有形式的视觉,听觉或两者兼有的内容的总称。 术语多媒体处理本身非常模糊。 讨论该术语的更精确方法是将其分解为两个基本部分-视觉或听觉。 因此,我们将讨论多媒体处理的术语,即图像处理和音频处理。 这些术语的混合产生了视频处理,这只是多媒体的另一种形式。
在以下各节中,我们将以单独的形式讨论它们。
图像处理
图像处理或计算机视觉是迄今为止人工智能研究最多的分支之一。 在过去的几十年中,它发展迅速,并在以下几种技术的进步中发挥了重要作用:
- 图像过滤器和编辑器
- 面部识别
- 数字绘画
- 自动驾驶汽车
我们在较早的项目中讨论了图像处理的基础知识。 在这个项目中,我们将讨论一个非常流行的用于执行图像处理的库-OpenCV。 OpenCV 是开源计算机视觉的缩写。 它由 Intel 开发,并由 Willow Garage 和 Itseez(后来被 Intel 收购)推动。 毫无疑问,由于它与所有主要的机器学习框架(例如 TensorFlow,PyTorch 和 Caffe)兼容,因此它是执行图像处理的全球大多数开发人员的首要选择。 除此之外,OpenCV 还可以使用多种语言,例如 C++,Java 和 Python。
要在 Python 环境中安装 OpenCV,可以使用以下命令:
pip install opencv-contrib-python
前面的命令将同时安装主 OpenCV 模块和contrib模块。 您可以在此处找到更多模块供您选择。 有关更多安装说明,如果前面的链接不符合您的要求,则可以在此处遵循官方文档。
让我们为您介绍一个非常简单的示例,说明如何使用 OpenCV 执行图像处理。 创建一个新的 Jupyter 笔记本,并从以下步骤开始:
- 要将 OpenCV 导入笔记本,请使用以下代码行:
import cv2
- 我们还要将 matplotlib 导入笔记本,因为如果您尝试使用本机 OpenCV 图像显示功能,Jupyter 笔记本将会崩溃:
from matplotlib import pyplot as plt
%matplotlib inline
- 让我们使用 matplotlib 为 OpenCV 的本机图像显示功能创建一个替代函数,以方便在笔记本中显示图像:
def showim(image):
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
plt.imshow(image)
plt.show()
请注意,我们将图像的配色方案从蓝色绿色红色(BGR)转换为红色绿色蓝色(RGB)。 这是由于默认情况下 OpenCV 使用 BGR 配色方案。 但是,matplotlib 在显示图片时会使用 RGB 方案,并且如果不进行这种转换,我们的图像就会显得奇怪。
- 现在,让我们将图像读取到 Jupyter 笔记本中。 完成后,我们将能够看到加载的图像:
image = cv2.imread("Image.jpeg")
showim(image)
前面代码的输出取决于您选择加载到笔记本中的图像:

在我们的示例中,我们加载了柑橘类水果切片的图像,这是艾萨克·奎萨达(Isaac Quesada)在“Unsplash”上拍摄的惊人照片。
您可以在这里找到上一张图片。
- 让我们通过将之前的图像转换为灰度图像来进行简单的操作。 为此,我们就像在声明的
showim()函数中那样简单地使用转换方法:
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
showim(gray_image)
这将产生以下输出:

- 现在让我们执行另一种常见的操作,即图像模糊。 在图像处理中通常采用模糊处理,以消除图像中信息的不必要的细节(此时)。 我们使用高斯模糊过滤器,这是在图像上创建模糊的最常见算法之一:
blurred_image = cv2.GaussianBlur(image, (7, 7), 0)
showim(blurred_image)
这将产生以下输出:

请注意,前面的图像不如原始图像清晰。 但是,它很容易达到愿意计算此图像中对象数量的目的。
- 为了在图像中定位对象,我们首先需要标记图像中的边缘。 为此,我们可以使用
Canny()方法,该方法是 OpenCV 中可用的其他选项之一,用于查找图像的边缘:
canny = cv2.Canny(blurred_image, 10, 50)
showim(canny)
这将产生以下输出:

请注意,在上图中找到的边缘数量很高。 虽然这会显示图像的细节,但是如果我们尝试对边缘进行计数以尝试确定图像中的对象数量,这将无济于事。
- 让我们尝试计算上一步生成的图像中不同项目的数量:
contours, hierarchy= cv2.findContours(canny, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
print("Number of objects found = ", len(contours))
上面的代码将产生以下输出:
Number of objects found = 18
但是,我们知道前面的图像中没有 18 个对象。 只有 9。因此,在寻找边缘时,我们将在canny方法中处理阈值。
- 让我们在 canny 方法中增加边缘发现的阈值。 这使得更难检测到边缘,因此仅使最明显的边缘可见:
canny = cv2.Canny(blurred_image, 50, 150)
showim(canny)
这将产生以下输出:

请注意,在柑橘类水果体内发现的边缘急剧减少,仅清晰可见其轮廓。 我们希望这会在计数时产生较少的对象。
- 让我们再次运行以下代码块:
contours, hierarchy= cv2.findContours(canny, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
print("Number of objects found = ", len(contours))
这将产生以下输出:
Number of objects found = 9
这是期望值。 但是,只有在特殊情况下,该值才是准确的。
- 最后,让我们尝试概述检测到的对象。 为此,我们绘制了
findContours()方法的上一步中确定的轮廓:
_ = cv2.drawContours(image, contours, -1, (0,255,0), 10)
showim(image)
这将产生以下输出:

请注意,我们已经在拍摄的原始图像中非常准确地识别出了九片水果。 我们可以进一步扩展此示例,以在任何图像中找到某些类型的对象。
要了解有关 OpenCV 的更多信息并找到一些可供学习的示例,请访问以下存储库。
现在让我们学习如何处理音频文件。
音频处理
我们已经看到了如何处理图像以及可以从中提取信息。 在本节中,我们将介绍音频文件的处理。 音频或声音是吞没您周围环境的东西。 在许多情况下,您仅能从该区域的音频剪辑中正确预测该区域或环境,而无需实际看到任何视觉提示。 声音或语音是人与人之间交流的一种形式。 安排良好的节奏模式形式的音频称为音乐,可以使用乐器制作。
音频文件的一些流行格式如下:
- MP3:一种非常流行的格式,广泛用于共享音乐文件。
- AAC:是对 MP3 格式的改进,AAC 主要用于 Apple 设备。
- WAV:由 Microsoft 和 IBM 创建,这种格式是无损压缩,即使对于小的音频文件也可能很大。
- MIDI:乐器数字接口文件实际上不包含音频。 它们包含乐器音符,因此体积小且易于使用。
音频处理是以下技术的增长所必需的:
- 用于基于语音的界面或助手的语音处理
- 虚拟助手的语音生成
- 音乐生成
- 字幕生成
- 推荐类似音乐
TensorFlow 团队的 Magenta 是一种非常流行的音频处理工具。
您可以通过这里访问 Magenta 主页。 该工具允许快速生成音频和音频文件的转录。
让我们简要地探讨 Magenta。
Magenta
Magenta 是 Google Brain 团队参与研究的一部分,该团队也参与了 TensorFlow。 它被开发为一种工具,可允许艺术家借助深度学习和强化学习算法来增强其音乐或艺术创作渠道。 这是 Magenta 的徽标:

让我们从以下步骤开始:
- 要在系统上安装 Magenta,可以使用 Python 的 pip 存储库:
pip install magenta
- 如果缺少任何依赖项,则可以使用以下命令安装它们:
!apt-get update -qq && apt-get install -qq libfluidsynth1 fluid-soundfont-gm build-essential libasound2-dev libjack-dev
!pip install -qU pyfluidsynth pretty_midi
- 要将 Magenta 导入项目中,可以使用以下命令:
import magenta
或者,按照流行的惯例,仅加载 Magenta 的音乐部分,可以使用以下命令:
import magenta.music as mm
您可以使用前面的导入在线找到很多样本。
让我们快速创作一些音乐。 我们将创建一些鼓声,然后将其保存到 MIDI 文件:
- 我们首先需要创建一个
NoteSequence对象。 在 Magenta 中,所有音乐都以音符序列的格式存储,类似于 MIDI 存储音乐的方式:
from magenta.protobuf import music_pb2
drums = music_pb2.NoteSequence()
- 创建
NoteSequence对象后,该对象为空,因此我们需要向其添加一些注解:
drums.notes.add(pitch=36, start_time=0, end_time=0.125, is_drum=True, instrument=10, velocity=80)
drums.notes.add(pitch=38, start_time=0, end_time=0.125, is_drum=True, instrument=10, velocity=80)
drums.notes.add(pitch=42, start_time=0, end_time=0.125, is_drum=True, instrument=10, velocity=80)
drums.notes.add(pitch=46, start_time=0, end_time=0.125, is_drum=True, instrument=10, velocity=80)
.
.
.
drums.notes.add(pitch=42, start_time=0.75, end_time=0.875, is_drum=True, instrument=10, velocity=80)
drums.notes.add(pitch=45, start_time=0.75, end_time=0.875, is_drum=True, instrument=10, velocity=80)
请注意,在前面的代码中,每个音符都有音高和力度。 再次类似于 MIDI 文件。
- 现在让我们为音符添加节奏,并设置音乐播放的总时间:
drums.total_time = 1.375
drums.tempos.add(qpm=60)
完成此操作后,我们现在准备导出 MIDI 文件。
- 我们首先需要将 Magenta
NoteSequence对象转换为 MIDI 文件:
mm.sequence_proto_to_midi_file(drums, 'drums_sample_output.mid')
前面的代码首先将音符序列转换为 MIDI,然后将它们写入磁盘上的drums_sample_output.mid文件。 您现在可以使用任何合适的音乐播放器播放midi文件。
继续前进,让我们探索如何处理视频。
视频处理
视频处理是多媒体处理的另一个重要部分。 通常,我们需要弄清楚移动场景中发生的事情。 例如,如果我们要生产自动驾驶汽车,则它需要实时处理大量视频才能平稳行驶。 这种情况的另一个实例可以是将手语转换为文本以帮助与语音障碍者互动的设备。 此外,需要视频处理来创建电影和动作效果。
我们将在本节中再次探讨 OpenCV。 但是,我们将演示如何在 OpenCV 中使用实时摄像机供稿来检测面部。
创建一个新的 Python 脚本并执行以下步骤:
- 首先,我们需要对脚本进行必要的导入。 这将很简单,因为我们只需要 OpenCV 模块:
import cv2
- 现在,让我们将 Haar 级联模型加载到脚本中。 Haar 级联算法是一种用于检测任何给定图像中的对象的算法。 由于视频不过是图像流,因此我们将其分解为一系列帧并检测其中的人脸:
faceCascade = cv2.CascadeClassifier("haarcascade_frontalface_default.xml")
您将不得不从以下位置获取haarcascade_frontalface_default.xml文件。
Haar 级联是一类使用级联函数执行分类的分类器算法。 保罗·维奥拉(Paul Viola)和迈克尔·琼斯(Michael Jones)引入了它们,以试图建立一种对象检测算法,该算法足够快以在低端设备上运行。 级联函数池来自几个较小的分类器。
Haar 级联文件通常以可扩展标记语言(XML)的格式找到,并且通常执行一项特定功能,例如面部检测,身体姿势检测, 对象检测等。 您可以在此处阅读有关 Haar 级联的更多信息。
- 现在,我们必须实例化摄像机以进行视频捕获。 为此,我们可以使用默认的笔记本电脑摄像头:
video_capture = cv2.VideoCapture(0)
- 现在让我们从视频中捕获帧并显示它们:
while True:
# Capture frames
ret, frame = video_capture.read()
### We'll add code below in future steps
### We'll add code above in future steps
# Display the resulting frame
cv2.imshow('Webcam Capture', frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
这样您就可以在屏幕上显示实时视频供稿。 在运行此程序之前,我们需要释放相机并正确关闭窗户。
- 要正确关闭实时捕获,请使用以下命令:
video_capture.release()
cv2.destroyAllWindows()
现在,让我们对脚本进行测试运行。
您应该会看到一个窗口,其中包含您的脸部实时捕捉的图像(如果您不害羞的话)。
- 让我们向该视频提要添加面部检测。 由于用于面部检测的 Haar 级联在使用灰度图像时效果更好,因此我们将首先将每个帧转换为灰度,然后对其进行面部检测。 我们需要将此代码添加到
while循环中,如以下代码所示:
### We'll add code below in future steps
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
faces = faceCascade.detectMultiScale(
gray,
scaleFactor=1.1,
minNeighbors=5,
minSize=(30, 30),
flags=cv2.CASCADE_SCALE_IMAGE
)
### We'll add code above in future steps
这样,我们就可以检测到人脸了,因此让我们在视频供稿中对其进行标记!
- 我们将简单地使用 OpenCV 的矩形绘制函数在屏幕上标记面孔:
minNeighbors=5,
minSize=(30, 30),
flags=cv2.CASCADE_SCALE_IMAGE
)
for (x, y, w, h) in faces:
cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2)
### We'll add code above in future steps
现在让我们再次尝试运行脚本。
转到终端并使用以下命令运行脚本:
python filename.py
在这里,文件名是您保存脚本文件时的名称。
您应该获得类似于以下屏幕截图的输出:

要退出实时网络摄像头捕获,请使用键盘上的Q键(我们已在前面的代码中进行了设置)。
我们已经研究了多媒体处理的三种主要形式的概述。 现在,让我们继续前进,构建基于 LSTM 的模型以生成音频。
开发基于 RNN 的音乐生成模型
在本节中,我们将开发音乐生成模型。 我们将为此使用 RNN,并使用 LSTM 神经元模型。 RNN 与简单的人工神经网络(ANN)有很大的不同-允许在层之间重复使用输入。
虽然在 ANN 中,我们希望输入到神经网络的输入值向前移动,然后产生基于错误的反馈,并将其合并到网络权重中,但 RNN 使输入多次循环返回到先前的层。
下图表示 RNN 神经元:

从上图可以看到,通过神经元激活函数后的输入分为两部分。 一部分在网络中向前移动到下一层或输出,而另一部分则反馈到网络中。 在时间序列数据集中,可以相对于给定样本在t的时间标记每个样本,我们可以扩展前面的图,如下所示:

但是,由于通过激活函数反复暴露值,RNN 趋向于梯度消失,其中 RNN 的值逐梯度小到可以忽略不计(或在梯度爆炸的情况下变大)。 为避免这种情况,引入了 LSTM 单元,该单元通过将信息存储在单元中而允许将信息保留更长的时间。 每个 LSTM 单元由三个门和一个存储单元组成。 三个门(输入,输出和遗忘门)负责确定哪些值存储在存储单元中。
因此,LSTM 单元变得独立于 RNN 其余部分的更新频率,并且每个单元格都有自己的时间来记住它所拥有的值。 就我们而言,与其他信息相比,我们忘记了一些随机信息的时间要晚得多,这更自然地模仿了自然。
您可以在以下链接中找到有关 RNN 和 LSTM 的详细且易于理解的解释。
在开始为项目构建模型之前,我们需要设置项目目录,如以下代码所示:
├── app.py
├── MusicGenerate.ipynb
├── Output/
└── Samples/
├── 0.mid
├── 1.mid
├── 2.mid
└── 3.mid
请注意,我们已经在Samples文件夹中下载了四个 MIDI 文件样本。 然后,我们创建了要使用的MusicGenerate.ipynb Jupyter 笔记本。 在接下来的几个步骤中,我们将仅在此 Jupyter 笔记本上工作。 app.py脚本当前为空,将来,我们将使用它来托管模型。
现在让我们开始创建基于 LSTM 的用于生成音乐的模型。
创建基于 LSTM 的模型
在本节中,我们将在 Jupyter 笔记本环境中研究MusicGenerate.ipynb笔记本:
- 在此笔记本中,我们将需要导入许多模块。 使用以下代码导入它们:
import mido
from mido import MidiFile, MidiTrack, Message
from tensorflow.keras.layers import LSTM, Dense, Activation, Dropout, Flatten
from tensorflow.keras.preprocessing import sequence
from tensorflow.keras.models import Sequential
from tensorflow.keras.optimizers import Adam
from sklearn.preprocessing import MinMaxScaler
import numpy as np
我们使用了mido库。 如果您的系统上未安装它,则可以使用以下命令来安装它:
pip install mido
注意,在前面的代码中,我们还导入了 Keras 模块和子部件。 该项目中使用的 TensorFlow 版本为 2.0。 为了在您的系统上安装相同版本或升级当前的 TensorFlow 安装,可以使用以下命令:
pip install --upgrade pip
pip install --upgrade tensorflow
现在,我们将继续阅读示例文件。
- 要将 MIDI 文件读入项目笔记本,请使用以下代码:
notes = []
for msg in MidiFile('Samples/0.mid') :
try:
if not msg.is_meta and msg.channel in [0, 1, 2, 3] and msg.type == 'note_on':
data = msg.bytes()
notes.append(data[1])
except:
pass
这将在notes列表中加载通道0,1,2和3的所有开头音符。
要了解有关注解,消息和频道的更多信息,请使用以下文档。
- 由于音符处于大于 0–1 范围的可变范围内,因此我们将使用以下代码将其缩放以适合公共范围:
scaler = MinMaxScaler(feature_range=(0,1))
scaler.fit(np.array(notes).reshape(-1,1))
notes = list(scaler.transform(np.array(notes).reshape(-1,1)))
- 我们基本上拥有的是随时间变化的笔记列表。 我们需要将其转换为时间序列数据集格式。 为此,我们使用以下代码转换列表:
notes = [list(note) for note in notes]
X = []
y = []
n_prev = 20
for i in range(len(notes)-n_prev):
X.append(notes[i:i+n_prev])
y.append(notes[i+n_prev])
我们已将其转换为一个集合,其中每个样本都带有未来的 20 个音符,并且在数据集的末尾具有过去的 20 个音符。这可以通过以下方式进行:如果我们有 5 个样本,例如M₁,M₂,M₃,M₄和M₅,然后我们将它们安排在大小为 2 的配对中(类似于我们的 20),如下所示:
M[1] M[2]M[2] M[3]M[3] M[4],依此类推
- 现在,我们将使用 Keras 创建 LSTM 模型,如以下代码所示:
model = Sequential()
model.add(LSTM(256, input_shape=(n_prev, 1), return_sequences=True))
model.add(Dropout(0.3))
model.add(LSTM(128, input_shape=(n_prev, 1), return_sequences=True))
model.add(Dropout(0.3))
model.add(LSTM(256, input_shape=(n_prev, 1), return_sequences=False))
model.add(Dropout(0.3))
model.add(Dense(1))
model.add(Activation('linear'))
optimizer = Adam(lr=0.001)
model.compile(loss='mse', optimizer=optimizer)
随意使用此 LSTM 模型的超参数。
- 最后,我们将训练样本适合模型并保存模型文件:
model.fit(np.array(X), np.array(y), 32, 25, verbose=1)
model.save("model.h5")
这将在我们的项目目录中创建model.h5文件。 每当用户从应用发出生成请求时,我们都会将此文件与其他音乐样本一起使用,以随机生成新的乐曲。
现在,让我们使用 Flask 服务器部署此模型。
使用 Flask 部署模型
对于项目的这一部分,您可以使用本地系统,也可以在其他地方的app.py中部署脚本。 我们将编辑此文件以创建 Flask 服务器,该服务器生成音乐并允许下载生成的 MIDI 文件。
该文件中的某些代码与 Jupyter 笔记本类似,因为每次加载音频样本并将其与我们生成的模型一起使用时,音频样本始终需要进行类似的处理:
- 我们使用以下代码将所需的模块导入此脚本:
import mido
from mido import MidiFile, MidiTrack, Message
from tensorflow.keras.models import load_model
from sklearn.preprocessing import MinMaxScaler
import numpy as np
import random
import time
from flask import send_file
import os
from flask import Flask, jsonify
app = Flask(__name__)
请注意,我们进行的最后四次导入与之前在 Jupyter 笔记本中导入的内容不同。 同样,我们不需要将几个 Keras 组件导入此脚本,因为我们将从已经准备好的模型中加载。
在上一个代码块的最后一行代码中,我们实例化了一个名为app的 Flask 对象。
- 在此步骤中,我们将创建函数的第一部分,当在 API 上调用
/generate路由时,该函数将生成新的音乐样本:
@app.route('/generate', methods=['GET'])
def generate():
songnum = random.randint(0, 3)
### More code below this
- 一旦我们随机决定在音乐生成过程中使用哪个样本文件,我们就需要像 Jupyter 笔记本中的训练样本那样对它进行类似的转换:
def generate():
.
.
.
notes = []
for msg in MidiFile('Samples/%s.mid' % (songnum)):
try:
if not msg.is_meta and msg.channel in [0, 1, 2, 3] and msg.type == 'note_on':
data = msg.bytes()
notes.append(data[1])
except:
pass
scaler = MinMaxScaler(feature_range=(0, 1))
scaler.fit(np.array(notes).reshape(-1, 1))
notes = list(scaler.transform(np.array(notes).reshape(-1, 1)))
### More code below this
在前面的代码块中,我们加载了示例文件,并从训练过程中使用的相同通道中提取了其注解。
- 现在,我们将像在训练期间一样缩放音符:
def generate():
.
.
.
notes = [list(note) for note in notes]
X = []
y = []
n_prev = 20
for i in range(len(notes) - n_prev):
X.append(notes[i:i + n_prev])
y.append(notes[i + n_prev])
### More code below this
我们也将这些笔记列表转换为适合模型输入的形状,就像我们在训练过程中对输入所做的一样。
- 接下来,我们将使用以下代码来加载 Keras 模型并从该模型创建新的注解列表:
def generate():
.
.
.
model = load_model("model.h5")
xlen = len(X)
start = random.randint(0, 100)
stop = start + 200
prediction = model.predict(np.array(X[start:stop]))
prediction = np.squeeze(prediction)
prediction = np.squeeze(scaler.inverse_transform(prediction.reshape(-1, 1)))
prediction = [int(i) for i in prediction]
### More code below this
- 现在,我们可以使用以下代码将此音符列表转换为 MIDI 序列:
def generate():
.
.
.
mid = MidiFile()
track = MidiTrack()
t = 0
for note in prediction:
vol = random.randint(50, 70)
note = np.asarray([147, note, vol])
bytes = note.astype(int)
msg = Message.from_bytes(bytes[0:3])
t += 1
msg.time = t
track.append(msg)
mid.tracks.append(track)
### More code below this
- 现在,我们准备将文件保存到磁盘。 它包含从模型随机生成的音乐:
def generate():
.
.
.
epoch_time = int(time.time())
outputfile = 'output_%s.mid' % (epoch_time)
mid.save("Output/" + outputfile)
response = {'result': outputfile}
return jsonify(response)
因此,/generate API 以 JSON 格式返回生成的文件的名称。 然后,我们可以下载并播放此文件。
- 要将文件下载到客户端,我们需要使用以下代码:
@app.route('/download/<fname>', methods=['GET'])
def download(fname):
return send_file("Output/"+fname, mimetype="audio/midi", as_attachment=True)
请注意,前面的函数在/download/filename路由上起作用,在该路由上,客户端根据上一代 API 调用的输出提供文件名。 下载的文件的 MIME 类型为audio/midi,它告诉客户端它是 MIDI 文件。
- 最后,我们可以添加将执行此服务器的代码:
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000)
完成此操作后,我们可以在终端中使用以下命令来运行服务器:
python app.py
如果代码中产生任何警告,您将从控制台获得一些调试信息。 完成此操作后,我们准备在下一节中为我们的 API 构建 Flutter 应用客户端。
在 Android 和 iOS 上部署音频生成 API
成功创建和部署模型后,现在开始构建移动应用。 该应用将用于获取和播放由先前创建的模型生成的音乐。
它将具有三个按钮:
- 生成音乐:生成新的音频文件
- 播放:播放新生成的文件
- 停止:停止正在播放的音乐
另外,它的底部将显示一些文本,以显示应用的当前状态。
该应用将显示如下:

该应用的小部件树如下所示:

现在开始构建应用的 UI。
创建 UI
我们首先创建一个新的 Dart 文件play_music.dart和一个有状态的小部件PlayMusic。 如前所述,在该文件中,我们将创建三个按钮来执行基本功能。 以下步骤描述了如何创建 UI:
- 定义
buildGenerateButton()方法以创建RaisedButton变量,该变量将用于生成新的音乐文件:
Widget buildGenerateButton() {
return Padding(
padding: EdgeInsets.only(left: 16, right: 16, top: 16),
child: RaisedButton(
child: Text("Generate Music"),
color: Colors.blue,
textColor: Colors.white,
),
);
}
在前面定义的函数中,我们创建一个RaisedButton,并添加Generate Music文本作为子元素。 color属性的Colors.blue值用于为按钮赋予蓝色。 另外,我们将textColor修改为Colors.white,以使按钮内的文本为白色。 使用EdgeInsets.only()给按钮提供左,右和顶部填充。 在后面的部分中,我们将在按钮上添加onPressed属性,以便每次按下按钮时都可以从托管模型中获取新的音乐文件。
- 定义
buildPlayButton()方法以播放新生成的音频文件:
Widget buildPlayButton() {
return Padding(
padding: EdgeInsets.only(left: 16, right: 16, top: 16),
child: RaisedButton(
child: Text("Play"),
onPressed: () {
play();
},
color: Colors.blue,
textColor: Colors.white,
),
);
}
在前面定义的函数中,我们创建一个RaisedButton,并添加"Play"文本作为子元素。 color属性的Colors.blue值用于为按钮赋予蓝色。 另外,我们将textColor修改为Colors.white,以使按钮内的文本为白色。 使用EdgeInsets.only()给按钮提供左,右和顶部填充。 在后面的部分中,我们将在按钮上添加onPressed属性,以在每次按下按钮时播放新生成的音乐文件。
- 定义
buildStopButton()方法以停止当前正在播放的音频:
Widget buildStopButton() {
return Padding(
padding: EdgeInsets.only(left: 16, right: 16, top: 16),
child: RaisedButton(
child: Text("Stop"),
onPressed: (){
stop();
},
color: Colors.blue,
textColor: Colors.white,
)
);
}
在前面定义的函数中,我们创建一个RaisedButton,并添加"Stop"文本作为子元素。 color属性的Colors.blue值用于为按钮赋予蓝色。 另外,我们将textColor修改为Colors.white,以使按钮内的文本为白色。 使用EdgeInsets.only()给按钮提供左,右和顶部填充。 在下一节中,我们将向按钮添加onPressed属性,以在按下按钮时停止当前播放的音频。
- 覆盖
PlayMusicState中的build()方法,以创建先前创建的按钮的Column:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Generate Play Music"),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
buildGenerateButton(),
buildPlayButton(),
buildStopButton(),
],
)
);
}
在前面的代码片段中,我们返回Scaffold。 它包含一个AppBar,其中具有[Generate Play Music]作为title。 Scaffold的主体是Column。 列的子级是我们在上一步中创建的按钮。 通过调用相应方法将按钮添加到该列中。 此外,crossAxisAlignment属性设置为CrossAxisAlignment.stretch,以便按钮占据父容器(即列)的总宽度。
此时,该应用如下所示:

在下一节中,我们将添加一种在应用中播放音频文件的机制。
添加音频播放器
创建应用的用户界面后,我们现在将音频播放器添加到应用中以播放音频文件。 我们将使用audioplayer插件添加音频播放器,如下所示:
- 我们首先将依赖项添加到
pubspec.yaml文件中:
audioplayers: 0.13.2
现在,通过运行flutter pub get获得包。
- 接下来,我们将插件导入
play_music.dart。
import 'package:audioplayers/audioplayers.dart';
- 然后,在
PlayMusicState内创建AudioPlayer的实例:
AudioPlayer audioPlayer = AudioPlayer();
- 现在,让我们定义一个
play()方法来播放远程可用的音频文件,如下所示:
play() async {
var url = 'http://34.70.80.18:8000/download/output_1573917221.mid';
int result = await audioPlayer.play(url);
if (result == 1) {
print('Success');
}
}
最初,我们将使用存储在url变量中的样本音频文件。 通过传递url中的值,使用audioPlayer.play()播放音频文件。 另外,如果从url变量成功访问和播放了音频文件,则结果将存储在结果变量中,其值将为1。
- 现在,将
onPressed属性添加到buildPlayButton内置的播放按钮中,以便每当按下该按钮时就播放音频文件:
Widget buildPlayButton() {
return Padding(
padding: EdgeInsets.only(left: 16, right: 16, top: 16),
child: RaisedButton(
....
onPressed: () {
play();
},
....
),
);
}
在前面的代码片段中,我们添加onPressed属性并调用play()方法,以便每当按下按钮时就播放音频文件。
- 现在,我们将定义
stop()以停止正在播放的音乐:
void stop() {
audioPlayer.stop();
}
在stop()方法内部,我们只需调用audioPlayer.stop()即可停止正在播放的音乐。
- 最后,我们为
buildStopButton()中内置的停止按钮添加onPressed属性:
Widget buildStopButton() {
return Padding(
padding: EdgeInsets.only(left: 16, right: 16, top: 16),
child: RaisedButton(
....
onPressed: (){
stop();
},
....
)
);
}
在前面的代码片段中,我们向onPressed中的stop()添加了一个调用,以便一旦按下停止按钮就停止音频。
现在开始使用 Flutter 应用部署模型。
部署模型
在为应用成功添加基本的播放和停止功能之后,现在让我们访问托管模型以每次生成,获取和播放新的音频文件。 以下步骤详细讨论了如何在应用内部访问模型:
- 首先,我们定义
fetchResponse()方法来生成和获取新的音频文件:
void fetchResponse() async {
final response =
await http.get('http://35.225.134.65:8000/generate');
if (response.statusCode == 200) {
var v = json.decode(response.body);
fileName = v["result"] ;
} else {
throw Exception('Failed to load');
}
}
我们首先使用http.get()从 API 获取响应,然后传入托管模型的 URL。 get()方法的响应存储在response变量中。 get()操作完成后,我们使用response.statusCode检查状态码。 如果状态值为200,则获取成功。 接下来,我们使用json.decode()将响应的主体从原始 JSON 转换为Map<String,dynamic>,以便可以轻松访问响应主体中包含的键值对。 我们使用v["result"]访问新音频文件的值,并将其存储在全局fileName变量中。 如果responseCode不是200,我们只会抛出一个错误。
- 现在让我们定义
load()以对fetchResponse()进行适当的调用:
void load() {
fetchResponse();
}
在前面的代码行中,我们仅定义一个load()方法,该方法用于调用fetchResponse()来获取新生成的音频文件的值。
- 现在,我们将修改
buildGenerateButton()中的onPressed属性,以每次生成新的音频文件:
Widget buildGenerateButton() {
return Padding(
....
child: RaisedButton(
....
onPressed: () {
load();
},
....
),
);
}
根据应用的功能,每当按下生成按钮时,都应生成一个新的音频文件。 这直接意味着无论何时按下“生成”按钮,我们都需要调用 API 以获取新生成的音频文件的名称。 因此,我们修改buildGenerateButton()以添加onPressed属性,以便每当按下按钮时,它都会调用load(),该调用随后将调用fetchResponse()并将新音频文件的名称存储在输出中。
- 托管的音频文件有两个部分,
baseUrl和fileName。baseUrl对于所有调用均保持不变。 因此,我们声明一个存储baseUrl的全局字符串变量:
String baseUrl = 'http://34.70.80.18:8000/download/';
回想一下,我们已经在“步骤 1”中将新音频文件的名称存储在fileName中。
- 现在,让我们修改
play()以播放新生成的文件:
play() async {
var url = baseUrl + fileName;
AudioPlayer.logEnabled = true;
int result = await audioPlayer.play(url);
if (result == 1) {
print('Success');
}
}
在前面的代码片段中,我们修改了前面定义的play()方法。 我们通过附加baseUrl和fileName创建一个新的 URL,以便url中的值始终与新生成的音频文件相对应。 我们在调用audioPlayer.play()时传递 URL 的值。 这样可以确保每次按下播放按钮时,都会播放最新生成的音频文件。
- 此外,我们添加了
Text小部件以反映文件生成状态:
Widget buildLoadingText() {
return Center(
child: Padding(
padding: EdgeInsets.only(top: 16),
child: Text(loadText)
)
);
}
在前面定义的函数中,我们创建了一个简单的Text小部件,以反映提取操作正在运行以及何时完成的事实。 Text小部件具有顶部填充,并与Center对齐。 loadText值用于创建窗口小部件。
全局声明该变量,其初始值为'Generate Music':
String loadText = 'Generate Music';
- 更新
build()方法以添加新的Text小部件:
@override
Widget build(BuildContext context) {
return Scaffold(
....
body: Column(
....
children: <Widget>[
buildGenerateButton(),
....
buildLoadingText()
],
)
);
}
现在,我们更新build()方法以添加新创建的Text小部件。 该窗口小部件只是作为先前创建的Column的子级添加的。
- 当用户想要生成一个新的文本文件时,并且在进行提取操作时,我们需要更改文本:
void load() {
setState(() {
loadText = 'Generating...';
});
fetchResponse();
}
在前面的代码段中,loadText值设置为'Generating...',以反映正在进行get()操作的事实。
- 最后,获取完成后,我们将更新文本:
void fetchResponse() async {
final response =
await http.get('http://35.225.134.65:8000/generate').whenComplete((){
setState(() {
loadText = 'Generation Complete';
});
});
....
}
提取完成后,我们将loadText的值更新为'Generation Complete'。 这表示应用现在可以播放新生成的文件了。
在使应用的所有部分正常工作之后,现在让我们通过创建最终的材质应用将所有内容放在一起。
创建最终的材质应用
现在创建main.dart文件。 该文件包含无状态窗口小部件MyApp。 我们重写build()方法并将PlayMusic设置为其子级:
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: PlayMusic(),
);
}
在覆盖的build()方法中,我们简单地将home创建为PlayMusic()的MaterialApp。
总结
在本章中,我们通过将多媒体处理分解为图像,音频和视频处理的核心组件来进行研究,并讨论了一些最常用的处理工具。 我们看到了使用 OpenCV 执行图像或视频处理变得多么容易。 另外,我们看到了一个使用 Magenta 生成鼓音乐的简单示例。 在本章的下半部分,我们介绍了 LSTM 如何与时间序列数据一起使用,并构建了一个 API,该 API 可以从提供的样本文件生成器乐。 最后,我们将此 API 与 Flutter 应用结合使用,该应用是跨平台的,可以同时部署在 Android,iOS 和 Web 上。
在下一章中,我们将研究如何使用深度强化学习(DRL)来创建可以玩棋盘游戏(例如国际象棋)的智能体。
八、基于强化神经网络的国际象棋引擎
在几个在线应用商店以及几乎每个软件商店中,游戏都提供了自己的完整版块。 游戏的重要性和热情不容忽视,这就是为什么全世界的开发人员都在不断尝试开发出更好,更吸引人的游戏的原因。
在流行的棋盘游戏世界中,国际象棋是全世界最有竞争力和最复杂的游戏之一。 已经尝试了一些强大的自动化程序来下棋和与人类竞争。 本章将讨论 DeepMind 的开发人员所使用的方法,他们创建了 Alpha Zero,这是一种自学算法,可以自学下棋,从而能够以一个单打击败市场上当时最好的国际象棋 AI,Stockfish 8。 在短短 24 小时的训练中得分较高。
在本章中,我们将介绍您需要理解的概念,以便构建这种深度强化学习算法,然后构建示例项目。 请注意,该项目将要求您具有 Python 和机器学习的丰富知识。
我们将在本章介绍以下主题:
- 强化学习导论
- 手机游戏中的强化学习
- 探索 Google 的 DeepMind
- 适用于 Connect 4 的 Alpha 类零 AI
- 基础项目架构
- 为国际象棋引擎开发 GCP 托管的 REST API
- 在 Android 上创建简单的国际象棋 UI
- 将国际象棋引擎 API 与 UI 集成
让我们从讨论增强学习智能体在手机游戏中的用法和普及程度开始。
强化学习导论
在过去的几年中,强化学习已成为机器学习研究人员中一个重要的研究领域。 人们越来越多地使用它来构建能够在任何给定环境中表现更好的智能体,以寻求对他们所执行行为的更好回报。 简而言之,这为我们提供了强化学习的定义–在人工智能领域,这是一种算法,旨在创建虚拟的智能体,它可在任何给定条件下,在环境中执行动作,在执行一系列动作后,取得最佳的奖励。
让我们尝试通过定义与通用强化学习算法关联的变量来赋予此定义更多的结构:
- 智能体:执行动作的虚拟实体。 是替换游戏/软件的指定用户的实体。
- 操作(
a):智能体可以执行的可能操作。 - 环境(
e):在软件/游戏中可用的一组场景。 - 状态(
S):所有方案的集合,以及其中可用的配置。 - 奖励(
R):对于智能体执行的任何操作返回的值,然后智能体尝试将其最大化。 - 策略(
π):智能体用来确定接下来必须执行哪些操作的策略。 - 值(
V):R是短期每动作奖励,而值是在一组动作结束时预期的总奖励。V[π](s)通过遵循状态S下的策略π来定义预期的总回报。
下图显示了该算法的流程:

尽管我们在前面的定义列表中没有提到观察者,但必须有观察者或评估者才能产生奖励。 有时,观察者本身可能是一个复杂的软件,但是通常,这是一个简单的评估函数或指标。
要获得关于强化学习的更详细的想法,您可以阅读这个页面上的 Wikipedia 文章。 有关正在使用的强化学习智能体的快速样本,请阅读以下 DataCamp 文章。
在下一部分中,我们将学习强化学习在手机游戏中的地位。
手机游戏中的强化学习
出于各种原因而希望构建具有游戏性的 AI 的开发人员中,强化学习已变得越来越流行-只需检查 AI 的功能,建立可以帮助专业人士改善游戏水平的训练智能体等等。 从研究人员的角度来看,游戏为强化学习智能体提供了最佳的测试环境,可以根据经验做出决策并学习在任何给定环境中的生存/成就。 这是因为可以使用简单而精确的规则设计游戏,从而可以准确预测环境对特定动作的反应。 这使得更容易评估强化学习智能体的表现,从而为 AI 提供良好的训练基础。 考虑到在玩游戏的 AI 方面的突破,也有人表示,我们向通用 AI 的发展速度比预期的要快。 但是强化学习概念如何映射到游戏?
让我们考虑一个简单的游戏,例如井字棋。 另外,如果您觉得古怪,只需使用 Google 搜索井字棋,您就会在搜索结果中看到一个游戏!
考虑您正在用计算机玩井字棋。 这里的计算机是智能体。 在这种情况下,环境是什么? 您猜对了–井字棋板以及在环境中管理游戏的一组规则。 井字棋盘上已经放置的标记可以确定环境所在的状态。座席可以在棋盘上放置的X或O是他们可以执行的动作,即输掉,赢得比赛或平局。 或朝着损失,胜利或平局前进是他们执行任何行动后回馈给智能体的奖励。 智能体赢得比赛所遵循的策略是遵循的策略。
因此,从该示例可以得出结论,强化学习智能体非常适合构建学习玩任何游戏的 AI。 这导致许多开发人员想出了象围棋,跳棋,反恐精英等国际象棋以外的几种流行游戏的游戏 AI。 甚至 Chrome Dino 之类的游戏也发现开发人员试图使用 AI 进行游戏。
在下一部分中,我们将简要概述 Google 的 DeepMind,它是游戏 AI 制造商领域中最受欢迎的公司之一。
探索 Google 的 DeepMind
当您谈论自学习人工智能的发展时,DeepMind 可能是最著名的名称之一,这是由于它们在该领域的开创性研究和成就。 自 2015 年 Google 重组以来,DeepMind 在 2014 年被 Google 收购,目前是 Alphabet 的全资子公司。DeepMind 最著名的作品包括 AlphaGo 及其继任者 Alpha Zero。 让我们更深入地讨论这些项目,并尝试了解是什么使它们在当今如此重要。
AlphaGo
2015 年,AlphaGo 成为第一个在19x19棋盘上击败职业围棋选手 Lee Sedol 的计算机软件。 突破被记录下来并作为纪录片发行。 击败李·塞多尔的影响如此之大,以至于韩国 Baduk 协会授予了荣誉 9 丹证书,这实际上意味着围棋选手的游戏技能与神性息息相关。 这是围棋历史上第一次提供 9 荣誉荣誉证书,因此提供给 AlphaGo 的证书编号为 001。ELO 等级为 3,739。
AlphaGo Master 的继任者 AlphaGo Master 在三场比赛中击败了当时统治世界的游戏冠军 Ke Jie。 为了表彰这一壮举,它获得了中国围棋协会颁发的 9 丹证书。 该软件当时的 ELO 等级为 4,858。
但是,这两款软件都被其继任者 AlphaGo Zero 压倒了,后者在 3 天的自学式学习中,能够在 21 分之后以 100:0 的游戏得分击败 AlphaGo,在 89:11 的游戏得分下击败 AlphaGo Master。 天的训练。 40 天后,它的 ELO 评分达到了 5,185,超过了以前所有 Go AI 的技能。
AlphaGo 基于蒙特卡洛树搜索算法,并采用了对生成的和人类玩家游戏日志进行的深度学习。 该模型的初始训练是通过人类游戏进行的。 然后,计算机将与自己对战并尝试改善其游戏性。 树搜索将被设置为一定的深度,以避免巨大的计算开销,在这种开销下,计算机将尝试达到所有可能的动作,然后再进行任何动作。
总而言之,遵循以下过程:
- 最初,该模型将在人类游戏日志上进行训练。
- 一旦在基线上进行了训练,计算机将使用在先前步骤中训练过的模型与自己竞争,并使用有上限的蒙特卡洛树搜索来确保进行移动而不会长时间停滞该软件。 这些游戏的日志已生成。
- 然后对生成的游戏进行了训练,从而改善了整体模型。
现在,让我们讨论 Alpha Zero。
Alpha Zero
Alpha Zero 是 AlphaGo Zero 的后继产品,它是对算法进行泛化的尝试,以便也可以用于其他棋盘游戏。 Alpha Zero 经过训练可以下棋,将棋(类似于棋的日式游戏)和围棋,其表现与相应游戏的现有 AI 相当。 经过 34 小时的训练,Alpha Zero for Go 击败了经过 3 天训练的 AlphaGo Zero,得分为 60:40。 这导致 ELO 等级为 4,430。
经过约 9 个小时的训练,Alpha Zero 击败了 TCEC 竞赛 2016 年冠军的 Stockfish 8。 因此,它仍然是迄今为止最强大的国际象棋 AI,尽管有人声称最新版本的 Stockfish 将能够击败它。
AlphaGo Zero 和 Alpha Zero 变体之间的主要区别如下:
- 出现平局的可能性:在围棋中,保证有一名选手获胜,而对于象棋则不是这样。 因此,对 Alpha Zero 进行了修改,以允许并列游戏。
- 对称性:AlphaGo Zero 利用了电路板的对称性。 但是,由于国际象棋不是非对称游戏,因此必须对 Alpha Zero 进行修改以使其工作。
- 硬编码的超参数搜索:Alpha Zero 具有用于超参数搜索的硬编码规则。
- 在 Alpha Zero 的情况下,神经网络会不断更新。
此时,您可能会想,“什么是蒙特卡罗树搜索?”。 让我们尝试回答这个问题!
蒙特卡洛树搜索
当我们谈论象棋,围棋或井字棋等基于当前场景的战略游戏时,我们所谈论的是大量可能的场景和可以在任何情况下在其中的给定点执行的动作。 尽管对于井字棋等较小的游戏,可能的状态和动作的数量在现代计算机可以计算的范围内,但对于游戏可以生成的状态数量,更复杂的游戏(如国际象棋和围棋)呈指数增长。
蒙特卡洛树搜索尝试找到在给定环境下赢得任何游戏或获得更好奖励所需要的正确动作序列。 之所以将其称为树搜索是因为它创建了游戏中所有可能状态的树,并通过创建每个状态的分支来实现其中的所有可能动作。 表示为树中的节点。
让我们考虑以下简单的游戏示例。 假设您正在玩一个游戏,要求您猜一个三位数的数字,每个猜中都有一个相关的奖励。 可能的数字范围是 1 到 5,您可以猜测的次数是 3。 如果您做出准确的猜测,即正确猜测任意给定位置的数字,则将获得 5 分。但是,如果您做出错误的猜测,将得到正确数字两边的线性差值的分数。
例如,如果要猜测的数字是 2,则可能获得以下奖励分数:
- 如果您猜 1,则得分为 4
- 如果您猜 2,则得分为 5
- 如果您猜 3,则得分为 4
- 如果您猜 4,则得分为 3
- 如果您猜 5,则得分为 2
因此,游戏中的最佳总得分为 15,即每个正确的猜测为 5 分。 鉴于此,您可以在每个步骤中的五个选项中进行选择,游戏中可能的状态总数为5 * 5 * 5 = 125,只有一个状态会给出最佳分数。
让我们尝试在树上描绘前面的游戏。 假设您要猜测的数字是 413。在第一步中,您将具有以下树:

做出选择后,您将获得奖励,再次有五个选项可供选择-换句话说,每个节点中有五个分支可以遍历。 在最佳游戏玩法中,将获得以下树:

现在,让我们考虑以下事实:围棋游戏共有3^361个可能状态。 在 AI 采取行动之前尝试计算每种可能性变得不切实际。 这是蒙特卡罗树搜索与上限可信度算法相结合的地方,它比其他方法更具优势,因为它可以终止到任何搜索深度,并且可以产生趋向于最佳分数的结果。 因此,算法不需要遍历树的每个分支。 一旦树形搜索算法意识到任何特定分支的表现不佳,就可以停止沿该路径前进,而专注于表现更好的路径。 而且,它可以尽早终止任何路径并在该点返回预期的回报,从而可以调整 AI 采取任何行动所需的时间。
更确切地说,蒙特卡罗树搜索遵循以下步骤:
-
选择:从树的当前节点中选择最佳回报分支。 例如,在前面的游戏树中,选择除 4 以外的任何分支将产生较低的分数,因此选择了 4。
-
扩展:一旦选择了最佳回报节点,该节点下的树将进一步扩展,从而创建具有该节点可用的所有可能选项(分支)的节点。 这可以理解为从游戏的任何位置布局 AI 的未来动作。
-
模拟:现在,由于事先不知道在扩展阶段创建的哪个未来选项最有回报,因此我们使用强化学习逐个模拟游戏的每个选项。 请注意,与上限可信度上限算法结合使用时,直到结束游戏才算重要。 计算任何
n个步骤的奖励也是一种不错的方法。 -
更新:最后,更新节点和父节点的奖励分数。 尽管不可能回到游戏中,并且由于任何节点的值都已减小,但如果在以后的游戏中的那个阶段找到了更好的替代方案,那么 AI 将不会遵循这条路径,从而通过多次迭代来改善其游戏玩法。
接下来,我们将构建一个系统,该系统的工作原理类似于 Alpha Zero,并尝试学习玩 Connect 4 游戏,该游戏比 Tic-Tac-Toe 游戏要复杂得多,但对我们来说足够大,来解释如何构建类似的国际象棋引擎。
适用于 Connect 4 的类似 Alpha Zero 的 AI
在开始研究可玩 Connect4 的 AI 之前,让我们简要了解一下游戏及其动态。 Connect 4,有时也称为连续四人,连续四人,四人以上,等等,是全世界儿童中最受欢迎的棋盘游戏之一。 我们也可以将它理解为井字棋的更高级版本,在其中您必须水平,垂直或对角放置三个相同类型的标记。 棋盘通常是一个6x7的网格,两个玩家各自玩一个标记。
Connect 4 的规则可能会有所不同,因此让我们为 AI 将学习的规则版本制定一些具体规则:
- 该游戏被模拟为在具有七个空心列和六行的垂直板上玩。 每列在板的顶部都有一个开口,可以在其中插入片段。可以查看已放入板的片段。
- 两位玩家都有 21 个形状像不同颜色硬币的硬币。
- 将硬币放在板上构成一个动作。
- 碎片从顶部的开口下降到最后一行,或者堆积在该列的最后一块。
- 第一个以任意方向连接其任意四枚硬币的玩家,因此彼此之间不会存在任何间隙或其他玩家的硬币获胜。
现在,让我们分解将 Connect 4 播放式自学 AI 分解为子问题的问题:
- 首先,我们需要创建棋盘的虚拟表示。
- 接下来,我们必须创建允许根据游戏规则移动的函数。
- 然后,为了保存游戏状态,我们需要一个状态管理系统。
- 接下来,我们将简化游戏玩法,其中将提示用户进行移动并宣布游戏终止。
- 之后,我们必须创建一个脚本,该脚本可以生成示例游戏玩法,供系统学习。
- 然后,我们必须创建训练函数来训练系统。
- 接下来,我们需要蒙特卡洛树搜索(MCTS)实现。
- 最后,我们需要一个神经网络的实现。
- 除了前面的具体步骤之外,我们还需要为系统创建许多驱动脚本以使其更加可用。
让我们依次移至前面的要点,一次覆盖系统的每个部分。 但是,首先,我们将快速浏览该项目中存在的目录结构和文件,这在本书的 GitHub 存储库中也可以找到。 让我们来看看:
command/:__init__.py:此文件使我们可以将此文件夹用作模块。arena.py:此文件获取并解析用于运行游戏的命令。generate.py:此文件接受并分析自玩招式生成系统的命令。newmodel.py:此文件用于为智能体创建新的空白模型。train.py:此文件用于训练基于增强学习的神经网络如何玩游戏。util/:__init__.py:此文件使我们可以将此文件夹用作模块。arena.py:此文件创建并维护玩家之间进行的比赛的记录,并允许我们在轮到谁之间切换。compat.py:此文件是用于使程序与 Python 2 和 Python 3 兼容的便捷工具。如果您确定正在开发的版本并希望在其上运行,则可以跳过此文件。generate.py:此文件播放一些随机移动的游戏,再加上 MCTS 移动,以生成可用于训练目的的游戏日志。 该文件存储每个游戏的获胜者以及玩家做出的动作。internal.py:此文件创建棋盘的虚拟表示并定义与棋盘相关的函数,例如将棋子放置在棋盘上,寻找获胜者或只是创建新棋盘。keras_model.py:此文件定义充当智能体大脑的模型。 在本项目的后面,我们将更深入地讨论该文件。mcts.py:此文件提供 MCTS 类,该类实质上是蒙特卡罗树搜索的实现。nn.py:此文件提供 NN 类,它是神经网络的实现,以及与神经网络相关的函数,例如拟合,预测,保存等。player.py:此文件为两种类型的播放器提供了类-MCTS 播放器和人工播放器。 MCTS 玩家是我们将训练的智能体,以玩游戏。state.py:这是internal.py文件的包装,提供了用于访问电路板和与电路板相关的函数的类。trainer.py:这使我们可以训练模型。 这与nn.py中提供的内容不同,因为它更专注于涵盖游戏的训练过程,而nn.py中的内容主要是围绕此功能的包装。
接下来,我们将继续探索这些文件中每个文件的一些重要部分,同时遵循我们先前为构建 AI 制定的步骤。
创建棋盘的虚拟表示
您将如何代表 Connect 4 棋盘? 代表 Connect 4 棋盘的两种常用方法以及游戏状态。 让我们来看看:
- 人类可读的长格式:在这种形式中,木板的行和列分别显示在 x 和 y 轴上,并且两个玩家的标记都显示为
x和o, 分别(或任何其他合适的字符)。 可能如下所示:
|1 2 3 4 5 6 7
--+--------------
1|. . . . . . .
2|. . . . . . .
3|. . . . . . .
4|. . . . o x .
5|x o x . o o .
6|o x x o x x o
但是,这种形式有点冗长并且在计算上不是很友好。
- 计算有效的形式:在此形式中,我们将板存储为 2D NumPy 数组:
array([[1, 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, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 0]], dtype=int8)
以这种方式创建该数组,当将其展平为一维数组时,板位置按顺序排列,就好像该数组实际上是一维数组一样。 前两个位置分别编号为 0 和 1,而第 5 个位置位于第 5 行和第 5 列,编号为 32。通过将前一个代码块中的矩阵与给定的表进行映射,可以轻松理解此条件。 在下图中:

这种形式适合于进行计算,但不适合玩家在游戏过程中观看,因为对于玩家而言很难解密。
- 一旦决定了如何表示电路板及其部件,就可以开始在
util/internal.py文件中编写代码,如下所示:
BOARD_SIZE_W = 7
BOARD_SIZE_H = 6
KEY_SIZE = BOARD_SIZE_W * BOARD_SIZE_H
前几行设置了板子的常数,在这种情况下,是板子上的行数和列数。 我们还通过将它们相乘来计算板上的按键或位置的数量。
- 现在,让我们准备在板上生成获胜位置的代码,如下所示:
LIST4 = []
LIST4 += [[(y, x), (y + 1, x + 1), (y + 2, x + 2), (y + 3, x + 3)] for y in range(BOARD_SIZE_H - 3) for x in range(BOARD_SIZE_W - 3)]
LIST4 += [[(y, x + 3), (y + 1, x + 2), (y + 2, x + 1), (y + 3, x)] for y in range(BOARD_SIZE_H - 3) for x in range(BOARD_SIZE_W - 3)]
LIST4 += [[(y, x), (y, x + 1), (y, x + 2), (y, x + 3)] for y in range(BOARD_SIZE_H) for x in range(BOARD_SIZE_W - 3)]
NO_HORIZONTAL = len(LIST4)
LIST4 += [[(y, x), (y + 1, x), (y + 2, x), (y + 3, x)] for y in range(BOARD_SIZE_H - 3) for x in range(BOARD_SIZE_W)]
LIST4变量存储任何玩家赢得比赛时可以实现的可能组合。
我们不会在此文件中讨论整个代码。 但是,重要的是要了解以下函数及其作用:
get_start_board():此函数以 NumPy 数组的形式返回电路板的空白 2D 数组表示形式。clone_board(board):此函数用于按板级克隆整个 NumPy 数组。get_action(board):此函数返回播放器已修改的数组中的位置。action_to_string(action):此函数将玩家执行的动作的内部数字表示形式转换为可以以易于理解的形式显示给用户的字符串。 例如place_at(board, pos,。player):执行为任何给定玩家在板上放置一块棋子的动作。 它还会更新板。def get_winner(board):此函数确定棋盘当前状态下的游戏是否有赢家。 如果是,则返回获胜玩家的标识符,该标识符将为 1 或 -1。def to_string(board):此函数将板的 NumPy 数组表示形式转换为字符串,该字符串为人类可读的格式。
接下来,我们将研究如何对 AI 进行编程,使其根据游戏规则进行并仅接受有效的动作。
允许根据游戏规则移动
为了确定玩家(无论是人还是机器)做出的动作的有效性,我们需要建立一种机制,在机器的情况下,该机制连续不断地只生成有效的动作,或者不断验证任何人类玩家的输入。 让我们开始吧:
- 可以在
util/generator.py文件的_selfplay(self, state, args)函数中找到一个这样的实例,如以下代码所示:
turn = 0
hard_random_turn = args['hard_random'] if 'hard_random' in args else 0
soft_random_turn = (args['soft_random'] if 'soft_random' in args else 30) + hard_random_turn
history = []
首先,我们将移动切换设置为0,指示游戏开始时尚未进行任何移动。 我们还考虑了用户在其 AI 自行生成的游戏中想要的硬性和软性随机回合的数量。 然后,我们将移动的历史记录设置为空白。
- 现在,我们可以开始为 AI 生成动作,如下所示:
while state.getWinner() == None:
if turn < hard_random_turn:
# random action
action_list = state.getAction()
index = np.random.choice(len(action_list))
(action, key) = action_list[index]
前面的代码说,直到没有游戏的获胜者,都必须生成招式。 在前面的案例中,我们可以看到,只要进行一次随机随机转弯的可能性为真,AI 就会选择一个完全随机的位置来放置其棋子。
- 通过在前面的
if语句中添加else块,我们告诉 AI,只要它需要进行柔和转弯,它就可以检查是否有任何随机位置将其放置在其中,但只能在 MCTS 算法所建议的移动范围内,如下所示:
else:
action_list = self.mcts.getActionInfo(state, args['simulation'])
if turn < soft_random_turn:
# random action by visited count
visited = [1.0 * a.visited for a in action_list]
sum_visited = sum(visited)
assert(sum_visited > 0)
p = [v / sum_visited for v in visited]
index = np.random.choice(len(action_list), p = p)
else:
# select most visited count
index = np.argmax([a.visited for a in action_list])
请注意,如果既不进行硬转弯也不进行软转弯,则坐席会在游戏的那一刻进行最常用的动作,这有望使它朝着胜利迈进。
因此,在非人类玩家的情况下,智能体只能在任何给定阶段在一组填充的有效动作之间进行选择。 对于人类玩家而言,情况并非如此,根据他们的创造力,他有可能尝试做出无效的举动。 因此,当人类玩家做出动作时,需要对其进行验证。
- 可以在
util/player.py文件的getNextAction(self, state)函数中找到验证人类玩家移动的方法,如下所示:
action = state.getAction()
available_x = []
for i in range(len(action)):
a, k = action[i]
x = a % util.BOARD_SIZE_W + 1
y = a // util.BOARD_SIZE_W + 1
print('{} - {},{}'.format(x, x, y))
available_x.append(x)
- 首先,我们现在计算人类玩家可能采取的合法行动,并将其显示给用户。 然后,我们提示用户输入一个动作,直到他们做出有效的动作为止,如下所示:
while True:
try:
x = int(compat_input('enter x: '))
if x in available_x:
for i in range(len(action)):
if available_x[i] == x:
select = i
break
break
except ValueError:
pass
因此,我们根据填充的一组有效动作来验证用户所做的动作。 我们还可以选择向用户显示错误。
接下来,我们将研究程序的状态管理系统,您肯定已经注意到,到目前为止,我们一直在看该代码。
状态管理系统
游戏的状态管理系统是整个程序中最重要的部分之一,因为它控制着所有的游戏玩法,并在 AI 的自学习过程中促进了游戏玩法。 这样可以确保向玩家展示棋盘,并在进行有效的移动。 它还存储了几个与状态有关的变量,这些变量对于游戏进行很有用。 让我们来看看:
- 让我们讨论
util/state.py文件中提供的State类中最重要的特性和函数:
import .internal as util
此类使用util/internal.py文件中定义的名称为util的变量和函数。
__init__(self, prototype = None):此类在启动时,会继承现有状态或创建新状态。 该函数的定义如下:
def __init__(self, prototype = None):
if prototype == None:
self.board = util.get_start_board()
self.currentPlayer = 1
self.winner = None
else:
self.board = util.clone_board(prototype.board)
self.currentPlayer = prototype.currentPlayer
self.winner = prototype.winner
在这里,您可以看到该类可以使用游戏的现有状态启动,并作为参数传递给该类的构造器; 否则,该类将创建一个新的游戏状态。
getRepresentativeString(self):此函数返回可以由人类玩家读取的游戏状态的格式正确的字符串表示形式。 其定义如下:
def getRepresentativeString(self):
return ('x|' if self.currentPlayer > 0 else 'o|') + util.to_oneline(self.board)
状态类中的许多其他重要方法如下:
getCurrentPlayer(self):此方法返回游戏的当前玩家; 也就是说,应该采取行动的玩家。getWinner(self):如果游戏结束,则此方法返回游戏获胜者的标识符。getAction(self):此方法检查游戏是否结束。 如果没有,它将在任何给定状态下返回一组下一个可能的动作。getNextState(self, action):此方法返回游戏的下一个状态; 也就是说,在将当前正在移动的棋子放在棋盘上并评估游戏是否结束之后,它将执行从一种状态到另一种状态的切换。getNnInput(self):此方法返回玩家到目前为止在游戏中执行的动作,并为每个玩家的动作使用不同的标记。
现在,让我们看一下如何改善程序的游戏玩法。
实现游戏玩法
负责控制程序中游戏玩法的文件是util/arena.py文件。
它在Arena类中定义了以下两种方法:
def fight(self, state, p1, p2, count):
stats = [0, 0, 0]
for i in range(count):
print('==== EPS #{} ===='.format(i + 1))
winner = self._fight(state, p1, p2)
stats[winner + 1] += 1
print('stats', stats[::-1])
winner = self._fight(state, p2, p1)
stats[winner * -1 + 1] += 1
print('stats', stats[::-1])
前面的fight()函数管理玩家的胜利/损失或平局的状态。 它确保在每个回合中进行两场比赛,其中每位玩家只能先玩一次。
此类中定义的另一个_fight()函数如下:
def _fight(self, state, p1, p2):
while state.getWinner() == None:
print(state)
if state.getCurrentPlayer() > 0:
action = p1.getNextAction(state)
else:
action = p2.getNextAction(state)
state = state.getNextState(action)
print(state)
return state.getWinner()
此函数负责切换棋盘上的玩家,直到找到赢家为止。
现在,让我们看一下如何生成随机的游戏玩法以使智能体自学。
生成示例游戏
到目前为止,我们已经讨论了util/gameplay.py文件,以演示该文件中与移动规则相关的代码-特别是该文件的自播放函数。 现在,我们来看看这些自玩游戏如何在迭代中运行以生成完整的游戏玩法日志。 让我们开始吧:
- 请考虑此文件提供的
Generator类的generate()方法的代码:
def generate(self, state, nn, cb, args):
self.mcts = MCTS(nn)
iterator = range(args['selfplay'])
if args['progress']:
from tqdm import tqdm
iterator = tqdm(iterator, ncols = 50)
# self play
for pi in iterator:
result = self._selfplay(state, args)
if cb != None:
cb(result)
本质上,此函数负责运行该类的_selfplay()函数,并确定一旦完成自播放后必须执行的操作。 在大多数情况下,您会将输出保存到文件中,然后将其用于训练。
- 这已在
command/generate.py文件中定义。 该脚本可以作为具有以下签名的命令运行:
usage: run.py generate [-h]
[--model, default='latest.h5', help='model filename']
[--number, default=1000000, help='number of generated states']
[--simulation, default=100, help='number of simulations per move']
[--hard, default=0, help='number of random moves']
[--soft, default=1000, help='number of random moves that depends on visited node count']
[--progress, help='show progress bar']
[--gpu, help='gpu memory fraction']
[--file, help='save to a file']
[--network, help='save to remote server']
- 该命令的示例调用如下:
python run.py generate --model model.h5 --simulation 100 -n 5000 --file selfplay.txt --progress
现在,让我们看一下一旦生成自播放日志就可以训练模型的函数。
系统训练
要训练智能体,我们需要创建util/trainer.py文件,该文件提供train()函数。 让我们来看看:
- 签名如下:
train(state, nn, filename, args = {})
该函数接受State类,神经网络类和其他参数。 它还接受文件名,该文件名是包含生成的游戏玩法的文件的路径。 训练后,我们可以选择将输出保存到另一个模型文件中,如command/train.py文件的train()函数所提供的。
- 此命令具有以下签名:
usage: run.py train [-h]
[--progress, help='show progress bar']
[--epoch EPOCH, help='training epochs']
[--batch BATCH, help='batch size']
[--block BLOCK, help='block size']
[--gpu GPU, help='gpu memory fraction']
history, help='history file'
input, help='input model file name'
output, help='output model file name'
历史参数是存储生成的游戏玩法的文件。 输入文件是当前保存的模型文件,而输出文件是将新训练的模型保存到的文件。
- 该命令的示例调用如下:
python run.py train selfplay.txt model.h5 newmodel.h5 --epoch 3 --progress
现在我们已经有了一个训练系统,我们需要创建 MCTS 和神经网络实现。
实现蒙特卡罗树搜索
util/mcts.py文件中提供了完整的 MCTS 算法实现。 该文件提供了 MCTS 类,该类具有以下重要函数:
getMostVisitedAction:此函数返回将状态传递给访问次数最多的操作。getActionInfo:执行任何操作后,此函数返回状态信息。_simulation:此函数执行单个游戏模拟,并返回有关在模拟过程中玩过的游戏的信息。
最后,我们需要创建一个神经网络实现。
实现神经网络
在最后一节中,我们将了解为智能体进行训练而创建的神经网络。 我们将探索util/nn.py文件,该文件提供NN类以及以下重要方法:
__init__(self, filename):如果磁盘上不存在此函数,则使用util/keras_model.py函数创建新模型。 否则,它将模型文件加载到程序中。util/keras_model.py文件中定义的模型是残差 CNN,它与 MCTS 和 UCT 结合使用,表现得像深度强化学习神经网络。 形成的模型具有以下配置:
input_dim: (2, util.BOARD_SIZE_H, util.BOARD_SIZE_W),
policy_dim: util.KEY_SIZE,
res_layer_num: 5,
cnn_filter_num: 64,
cnn_filter_size: 5,
l2_reg: 1e-4,
learning_rate: 0.003,
momentum: 0.9
默认情况下,模型具有五个残差卷积层块。 我们先前在util/internal.py文件中定义了BOARD_SIZE_H,BOARD_SIZE_W和KEY_SIZE常量:
save(self, filename):此函数将模型保存到提供的文件名中。predict(self, x):提供了板状态以及已经进行的移动,此函数输出可以下一步进行的单个移动。fit(self, x, policy, value, batch_size = 256, epochs = 1):此函数用于将新样本拟合到模型并更新权重。
除了上述脚本之外,我们还需要一些驱动脚本。 您可以在该项目的存储库中查找它们,以了解它们的用法。
要运行已完成的项目,您需要执行以下步骤:
- 使用以下命令创建新模型:
python run.py newmodel model.h5
这将创建一个新模型并打印出其摘要。
- 生成示例游戏日志:
python run.py generate --model model.h5 --simulation 100 -n 5000 --file selfplay.txt --progress
在仿真过程中,上一行为 MCTS 生成了 5,000 个示例游戏,深度为 100。
- 训练模型:
python run.py train selfplay.txt model.h5 newmodel.h5 --epoch 3 --progress
前面的命令在游戏文件上训练模型三个时间,并将训练后的模型另存为newmodel.h5。
- 与 AI 对抗:
python run.py arena human mcts,newmodel.h5,100
前面的命令开始与 AI 进行游戏。 在这里,您将在终端中看到一个面板和游戏选项,如下所示:

现在,我们已经成功创建了一个基于 Alpha Zero 的程序来学习玩棋盘游戏,现在我们可以将其推论到国际象棋 AI 上了。 但是,在这样做之前,我们将简要地介绍项目架构。
基础项目架构
为了创建国际象棋引擎,将其作为 REST API 托管在 GCP 上,我们将遵循常规项目架构:

虽然上图提供了该项目的非常简化的概述,但它可以用于更复杂的系统,这些系统可以产生更好的自学习象棋引擎。
GCP 上托管的模型将放置在 EC2 VM 实例中,并将包装在基于 Flask 的 REST API 中。
为国际象棋引擎开发 GCP 托管的 REST API
现在我们已经看到了如何继续进行此项目,我们还需要讨论如何将 Connect 4 的游戏映射到国际象棋,以及如何将国际象棋 RL 引擎部署为 API。
您可以在这个页面上找到我们为该象棋引擎创建的文件。 在将这些文件与 Connect 4 项目中的文件映射之前,让我们快速了解一些最重要的文件:
src/chess_zero/agent/:player_chess.py:此文件描述ChessPlayer类,该类保存有关在任何时间点玩游戏的玩家的信息。 它为与使用蒙特卡洛树搜索来搜索新动作,更改玩家状态以及每个用户在玩游戏期间所需的其他功能的相关方法提供了包装。model_chess.py:此文件描述了此系统中使用的剩余 CNN。src/chess_zero/config/:mini.py:此文件定义国际象棋引擎学习或玩的配置。 您将需要在此处有时调整这些参数,以降低在低端计算机上进行训练期间的批量大小或虚拟 RAM 消耗。src/chess_zero/env/:chess_env.py:此文件描述棋盘的设置,游戏规则以及执行游戏操作所需的函数。 它还包含检查游戏状态和验证移动的方法。src/chess_zero/worker/:evaluate.py:此文件负责与当前最佳模型和下一代模型玩游戏。 如果下一代模型的表现优于 100 款游戏,则它将替代以前的模型。optimize.py:此文件加载当前最佳模型,并在其上执行更多监督的基于学习的训练。self.py:引擎与自己对战并学习新的游戏玩法。sl.py:监督学习的缩写,此文件将来自其他玩家的游戏的 PGN 文件作为输入,并对其进行监督学习。src/chess_zero/play_game/:uci.py:此文件提供了通用国际象棋界面(UCI)标准环境,可以与引擎进行交互。flask_server.py:该文件创建一个 Flask 服务器,该服务器使用国际象棋游戏的 UCI 表示法与引擎进行通信。
现在我们知道每个文件的作用,让我们建立这些文件与 Connect 4 游戏中文件的映射。
还记得我们在讨论 Connect 4 AI 时制定的步骤吗? 让我们看看国际象棋项目是否也遵循相同的步骤:
- 创建棋盘的虚拟代表。 这是在
src/chess_zero/env/chess_env.py文件中完成的。 - 创建允许根据游戏规则进行移动的函数。 这也可以在
src/chess_zero/env/chess_env.py文件中完成。 - 原地的状态管理系统:此功能在许多文件上维护,例如
src/chess_zero/agent/player_chess.py和src/chess_zero/env/chess_env.py。 - 简化游戏:这是通过
src/chess_zero/play_game/uci.py文件完成的。 - 创建一个可以生成示例游戏玩法的脚本,以供系统学习。 尽管此系统未将生成的游戏玩法明确地存储为磁盘上的文件,但该任务由
src/chess_zero/worker/self_play.py执行。 - 创建训练函数来训练系统。 这些训练函数位于
src/chess_zero/worker/sl.py和src/chess_zero/worker/self.py处。 - 现在,我们需要一个 MCTS 实现。 可以在
src/chess_zero/agent/player_chess.py的文件的移动搜索方法中找到该项目的 MCTS 实现。 - 神经网络的实现:
src/chess_zero/agent/model_chess.py中定义了项目的神经网络。
除了前面的映射之外,我们还需要讨论 Universal Chess Interface 和 Flask 服务器脚本,这两个都是游戏性和 API 部署所必需的。
了解通用国际象棋界面
/src/chess_zero/play_game/uci.py上的文件为引擎创建了通用国际象棋界面。 但是,UCI 到底是什么?
UCI 是 Rudolf Huber 和 Stefan Meyer-Kahlen 引入的一种通信标准,它允许在任何控制台环境中使用国际象棋引擎进行游戏。 该标准使用一小组命令来调用国际象棋引擎,以搜索并输出板子任何给定位置的最佳动作。
通过 UCI 进行的通信与标准输入/输出发生,并且与平台无关。 在我们程序的 UCI 脚本中可用的命令如下:
uci:打印正在运行的引擎的详细信息。isready:这查询引擎是否准备好进行对抗。ucinewgame:这将启动带有引擎的新游戏。position [fen | startpos] moves:此设置板的位置。 如果用户从非起始位置开始,则用户需要提供 FEN 字符串来设置板。go:这要求引擎进行搜索并提出最佳建议。quit:这将结束游戏并退出界面。
以下代码显示了带有 UCI 引擎的示例游戏玩法:
> uci
id name ChessZero
id author ChessZero
uciok
> isready
readyok
> ucinewgame
> position startpos moves e2e4
> go
bestmove e7e5
> position rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 1 moves g1f3
> go
bestmove b8c6
> quit
要快速生成任何板位置的 FEN 字符串,可以使用板编辑器。
现在,让我们讨论一下 Flask 服务器脚本以及如何在 GCP 实例上部署它。
在 GCP 上部署
该国际象棋引擎程序需要存在 GPU。 因此,我们必须遵循其他步骤,才能在 GCP 实例上部署脚本。
大致的工作流程如下:
- 请求增加帐户可用的 GPU 实例的配额。
- 创建基于 GPU 的计算引擎实例。
- 部署脚本。
我们将在以下各节中详细介绍这些步骤。
请求增加 GPU 实例的配额
第一步将是请求增加 GPU 实例的配额。 默认情况下,您的 GCP 帐户上可拥有的 GPU 实例数为 0。此限制由您的帐户的配额配置设置,您需要请求增加。 这样做,请按照下列步骤操作:
- 通过这里打开 Google Cloud Platform 控制台。
- 在左侧菜单上,单击“IAM&Admin | 配额”,如以下屏幕截图所示:

- 单击
Metrics过滤器,然后键入 GPU 以找到读取 GPU(所有区域)的条目,如以下屏幕截图所示:

- 选择条目,然后单击“编辑配额”。
- 系统将要求您提供身份证明,包括您的电话号码。 填写详细信息,然后单击“下一步”。
- 输入您想要将 GPU 配额设置为的限制(最好是
1,以避免滥用)。 另外,请提供您提出要求的理由,例如学术研究,机器学习探索或任何适合您的东西! - 单击“提交”。
提出要求后,大约需要 10 到 15 分钟才能将您的配额增加/设置为您指定的数量。 您将收到一封电子邮件,通知您有关此更新。 现在,您准备创建一个 GPU 实例。
创建一个 GPU 实例
下一步是创建 GPU 实例。 创建 GPU 实例的过程与创建非 GPU 实例的过程非常相似,但是需要额外的步骤。 让我们快速完成所有这些步骤:
- 在您的 Google Cloud Platform 仪表板上,单击左侧导航菜单中的“Compute Engine | VM 实例”。
- 单击“创建实例”。
- 单击“计算机类型选择”部分正下方的 CPU 平台和 GPU,如以下屏幕截图所示:

- 单击“添加 GPU”(大加号(
+)按钮)。 选择要附加到此 VM 的 GPU 类型和 GPU 数量。 - 将启动盘操作系统更改为 Ubuntu 版本 10.10。
- 在“防火墙”部分中,检查 HTTP 和 HTTPS 通信权限,如以下屏幕截图所示:

- 单击表单底部的“创建”。
几秒钟后,您的实例将成功创建。 如果遇到任何错误,例如超出了区域资源限制,请尝试更改要在其中创建实例的区域/区域。这通常是一个临时问题。
现在,我们可以部署 Flask 服务器脚本。
部署脚本
现在,我们将部署 Flask 服务器脚本。 但是在我们这样做之前,让我们先看一下该脚本的作用:
- 脚本的前几行导入了必要的模块,脚本才能正常工作:
from flask import Flask, request, jsonify
import os
import sys
import multiprocessing as mp
from logging import getLogger
from chess_zero.agent.player_chess import ChessPlayer
from chess_zero.config import Config, PlayWithHumanConfig
from chess_zero.env.chess_env import ChessEnv
from chess_zero.agent.model_chess import ChessModel
from chess_zero.lib.model_helper import load_best_model_weight
logger = getLogger(__name__)
- 其余代码放入
start()函数中,该函数由config对象实例化:
def start(config: Config):
## rest of the code
- 以下几行创建了引擎和人类玩家的实例,并在脚本开始运行时重置了游戏环境:
def start(config: Config):
...
PlayWithHumanConfig().update_play_config(config.play)
me_player = None
env = ChessEnv().reset()
...
- 将创建模型,并使用以下代码将模型的最佳权重加载到其中:
def start(config: Config):
...
model = ChessModel(config)
if not load_best_model_weight(model):
raise RuntimeError("Best model not found!")
player = ChessPlayer(config, model.get_pipes(config.play.search_threads))
...
- 前面代码中的最后一行创建具有指定配置和模型知识的国际象棋引擎玩家实例:
def start(config: Config):
...
app = Flask(__name__)
@app.route('/play', methods=["GET", "POST"])
def play():
data = request.get_json()
print(data["position"])
env.update(data["position"])
env.step(data["moves"], False)
bestmove = player.action(env, False)
return jsonify(bestmove)
...
前面的代码创建了 Flask 服务器应用的实例。 定义/play路由,使其可以接受位置并移动参数,这与我们先前在 UCI 游戏中使用的命令相同。
- 游戏状态将更新,并且要求象棋引擎计算下一个最佳移动。 这以 JSON 格式返回给用户:
def start(config: Config):
...
app.run(host="0.0.0.0", port="8080")
脚本的最后一行在主机0.0.0.0处启动 Flask 服务器,这意味着脚本将监听其运行所在设备的所有打开的 IP。 指定的端口为8080。
-
最后,我们将脚本部署到我们创建的 VM 实例。 为此,请执行以下步骤:
-
打开 GCP 控制台的 VM 实例页面。
-
输入在上一节中创建的 VM 后,单击
SSH按钮。 -
SSH 会话激活后,通过运行以下命令来更新系统上的存储库:
sudo apt update
- 接下来,使用以下命令克隆存储库:
git clone https://github.com/PacktPublishing/Mobile-Deep-Learning-Projects.git
- 将当前工作目录更改为
chess文件夹,如下所示:
cd Mobile-Deep-Learning-Projects/Chapter8/chess
- 为 Python3 安装 PIP:
sudo apt install python3-pip
- 安装项目所需的所有模块:
pip3 install -r requirements.txt
- 为最初的监督学习提供训练 PGN。 您可以从这里下载示例 PGN。
ficsgamesdb2017.pgn文件包含 5,000 个已存储的游戏。 您需要将此文件上传到data/play_data/文件夹。 - 运行监督学习命令:
python3 src/chess_zero/run.py sl
- 运行自学习命令:
python3 src/chess_zero/run.py self
当您对程序可以自行播放的时间感到满意时,请使用Ctrl + C/Z停止脚本。
- 运行以下命令以启动服务器:
python3 src/chess_zero/run.py server
现在,您应该能够将职位和移动发送到服务器并获得响应。 让我们快速测试一下。 使用 Postman 或其他任何用于 API 测试的工具,我们将使用 FEN 字符串向 API 发出请求,以设置位置和正在进行的移动。
假设您的 VM 实例正在公共 IP 地址上运行(在 VM 实例仪表板的实例条目上可见)1.2.3.4。 在这里,我们发送以下POST请求:
endpoint: http://1.2.3.4:8080/play
Content-type: JSON
Request body:
{
"position": "r1bqk2r/ppp2ppp/2np1n2/2b1p3/2B1P3/2N2N2/PPPPQPPP/R1B1K2R w KQkq - 0 1",
"moves": "f3g5"
}
先前代码的输出为"h7h6"。 让我们直观地了解这种交互。 FEN 中定义的板看起来如下:

我们告诉服务器这是怀特的举动,而怀特玩家的举动是f3g5,这意味着将怀特骑士移动到板上的 G5 位置。 我们传递给 API 的棋盘 FEN 字符串中的'w'表示白人玩家将进行下一回合。
引擎通过将 H7 处的棋子移动到 H6 进行响应,威胁到马的前进,如以下屏幕快照所示:

现在,我们可以将此 API 与 Flutter 应用集成!
在 Android 上创建简单的国际象棋 UI
现在,我们了解了强化学习以及如何使用它来开发可部署到 GCP 的国际象棋引擎,让我们为游戏创建 Flutter 应用。 该应用将具有两个播放器–用户和服务器。 用户是玩游戏的人,而服务器是我们在 GCP 上托管的国际象棋引擎。 首先,用户采取行动。 记录此移动并将其以 POST 请求的形式发送到国际象棋引擎。 然后,国际象棋引擎以自己的动作进行响应,然后在屏幕上进行更新。
我们将创建一个简单的单屏应用,将棋盘和棋子放置在中间。 该应用将显示如下:

该应用的小部件树如下所示:

让我们开始编写应用代码。
将依赖项添加到pubspec.yaml
首先,将chess_vectors_flutter包添加到pubspec.yaml文件中,以便在将要构建的棋盘上显示实际的棋子。 将以下行添加到pubspec.yaml的依赖项部分:
chess_vectors_flutter: ">=1.0.6 <2.0.0"
运行flutter pub get安装包。
将棋子放置在正确的位置可能会有些棘手。 让我们了解将所有片段放置在正确位置的约定。
了解映射结构
我们将首先创建一个名为chess_game.dart的新 dart 文件。 这将包含所有游戏逻辑。 在文件内部,我们声明一个名为ChessGame的有状态小部件:
- 要将棋子映射到棋盘的正方形,我们将使用与构建模型时相同的符号,以便每个正方形均由字母和数字表示。 我们将在
ChessGameState内创建一个列表squareList,以便我们可以存储所有索引的正方形,如下所示:
var squareList = [
["a8","b8","c8","d8","e8","f8","g8","h8"],
["a7","b7","c7","d7","e7","f7","g7","h7"],
["a6","b6","c6","d6","e6","f6","g6","h6"],
["a5","b5","c5","d5","e5","f5","g5","h5"],
["a4","b4","c4","d4","e4","f4","g4","h4"],
["a3","b3","c3","d3","e3","f3","g3","h3"],
["a2","b2","c2","d2","e2","f2","g2","h2"],
["a1","b1","c1","d1","e1","f1","g1","h1"],
];
- 为了将正确的棋子存储在正确的正方形中并根据玩家的移动来更新它们,我们将创建一个名为
board的HashMap:
HashMap board = new HashMap<String, String>();
HashMap的键将包含正方形的索引,而值将是正方形将保留的片段。 我们将使用一个字符串来表示一块特定的作品,该字符串将根据作品的名称包含一个字母。 例如,K代表王,B代表相。 我们通过使用大写和小写字母来区分白色和黑色部分。 大写字母代表白色,小写字母代表黑色。 例如,K代表白王,b代表黑相。 board['e7'] = "P"表示索引为'e7'的盒子当前有一个白色棋子。
- 现在,让我们将它们放置在初始位置。 为此,我们需要定义
initializeBoard()方法,如下所示:
void initializeBoard() {
setState(() {
for(int i = 8; i >= 1; i--) {
for(int j = 97; j <= 104; j++) {
String ch = String.fromCharCode(j)+'$i';
board[ch] = " ";
}
}
//Placing White Pieces
board['a1'] = board['h1']= "R";
board['b1'] = board['g1'] = "N";
board['c1'] = board['f1'] = "B";
board['d1'] = "Q";
board['e1'] = "K";
board['a2'] = board['b2'] = board['c2'] = board['d2'] =
board['e2'] = board['f2'] = board['g2'] = board['h2'] = "P";
//Placing Black Pieces
board['a8'] = board['h8']= "r";
board['b8'] = board['g8'] = "n";
board['c8'] = board['f8'] = "b";
board['d8'] = "q";
board['e8'] = "k";
board['a7'] = board['b7'] = board['c7'] = board['d7'] =
board['e7'] = board['f7'] = board['g7'] = board['h7'] = "p";
});
}
在前面的方法中,我们使用一个简单的嵌套循环通过从a到h的所有行以及从 1 到 8 的所有列进行遍历,使用空白字符串初始化哈希映射板的所有索引。 如“步骤 2”中所述,将其放置在其初始位置上。 为了确保在初始化棋盘时重新绘制 UI,我们将整个分配放在setState()中。
- 屏幕启动后,板将被初始化。 为了确保这一点,我们需要覆盖
initState()并从那里调用initializeBoard():
@override
void initState() {
super.initState();
initializeBoard();
}
现在我们对映射棋子有了更好的了解,让我们开始在屏幕上放置棋子的实际图像。
放置实际片段的图像
将片段映射到其初始位置后,我们可以开始放置实际的图像向量:
- 我们首先定义一个名为
mapImages()的函数,该函数采用正方形的索引(即哈希图板的键值)并返回图像:
Widget mapImages(String squareName) {
board.putIfAbsent(squareName, () => " ");
String p = board[squareName];
var size = 6.0;
Widget imageToDisplay = Container();
switch (p) {
case "P":
imageToDisplay = WhitePawn(size: size);
break;
case "R":
imageToDisplay = WhiteRook(size: size);
break;
case "N":
imageToDisplay = WhiteKnight(size: size);
break;
case "B":
imageToDisplay = WhiteBishop(size: size);
break;
case "Q":
imageToDisplay = WhiteQueen(size: size);
break;
case "K":
imageToDisplay = WhiteKing(size: size);
break;
case "p":
imageToDisplay = BlackPawn(size: size);
break;
case "r":
imageToDisplay = BlackRook(size: size);
break;
case "n":
imageToDisplay = BlackKnight(size: size);
break;
case "b":
imageToDisplay = BlackBishop(size: size);
break;
case "q":
imageToDisplay = BlackQueen(size: size);
break;
case "k":
imageToDisplay = BlackKing(size: size);
break;
case "p":
imageToDisplay = BlackPawn(size: size);
break;
}
return imageToDisplay;
}
在前面的函数中,我们构建一个与矩形中所含件名相对应的开关盒块。 我们使用哈希图在特定的正方形中找到片段,然后返回相应的图像。 例如,如果将a1的值传递到squareName中,并且哈希图板具有与键值a1对应的值P,则白兵的图像将存储在变量imageToDisplay中。
请注意,在 64 个棋盘格正方形中,只有 32 个包含棋子。 其余将为空白。 因此,在哈希表board中,将存在没有值的键。 如果squareName没有片段,则将其传递给imageToDisplay变量,该变量将只有一个空容器。
-
在上一步中,我们构建了对应于棋盘上每个正方形的小部件(图像或空容器)。 现在,让我们将所有小部件排列成行和列。
squareName中的特定元素(例如[a1,b1,....,g1])包含应并排放置的正方形。 因此,我们将它们包装成一行并将这些行中的每一个包装成列。 -
让我们从定义
buildRow()方法开始,该方法包含一个列表。 这本质上是sqaureName中的元素列表,并构建完整的行。 该方法如下所示:
Widget buildRow(List<String> children) {
return Expanded(
flex: 1,
child: Row(
children: children.map((squareName) => getImage(squareName)).toList()
),
);
}
在前面的代码片段中,我们迭代使用map()方法传递的列表的每个元素。 这会调用getImage()以获取对应于正方形的适当图像。 然后,我们将所有这些返回的图像添加为一行的子级。 该行将一个子代添加到展开的窗口小部件并返回。
getImage()方法定义如下:
Widget getImage(String squareName) {
return Expanded(
child: mapImages(squareName),
);
}
只需输入squareName的值,然后返回一个扩展的小部件,其中将包含我们先前定义的mapImages返回的图像。 我们稍后将修改此方法,以确保玩家可以拖动每个图像,以便它们可以在棋盘上移动。
- 现在,我们需要构建将包含已构建行的列。 为此,我们需要定义
buildChessBoard()方法,如下所示:
Widget buildChessBoard() {
return Container(
height: 350,
child: Column(
children: widget.squareList.map((row) {
return buildRow(row,);
}).toList()
)
);
}
在前面的代码中,我们迭代了squareList内部的每一行,这些行表示为一个列表。 我们通过调用buildRow()来构建行,并将它们作为子级添加到列中。 此列作为子级添加到容器中并返回。
- 现在,让我们将所有片段以及实际的棋盘图像放到屏幕上。 我们将覆盖
build()方法,以构建由棋盘图像及其碎片组成的小部件栈:
@override
Widget build(BuildContext context) {
return Container(
child: Stack(
children: <Widget>[
Container(
child: new Center(child: Image.asset("assets/chess_board.png", fit: BoxFit.cover,)),
),
Center(
child: Container(
child: buildChessBoard(),
),
)
],
)
);
}
前面的方法使用容器来构建栈,该容器添加存储在assets文件夹中的棋盘图像。 栈的下一个子项是居中对齐的容器,其中所有片段图像都通过对buildChessBoard()的调用以小部件的形式添加为行和列包装。 整个栈作为子级添加到容器中并返回,以便出现在屏幕上。
此时,应用显示棋盘,以及所有放置在其初始位置的棋子。 如下所示:

现在,让我们使这些棋子变得可移动,以便我们可以玩一个真实的游戏。
使片段移动
在本节中,我们将用可拖动的工具包装每块棋子,以便用户能够将棋子拖动到所需位置。 让我们详细看一下实现:
- 回想一下,我们声明了一个哈希图来存储片段的位置。 移动将包括从一个盒子中移出一块并将其放在另一个盒子中。 假设我们有两个变量
'from'和'to',它们存储用于移动片段的盒子的索引。 进行移动后,我们拿起'from'处的片段并将其放入'to'中。 因此,'from'的框变为空。 按照相同的逻辑,我们将定义refreshBoard()方法,该方法在每次移动时都会调用:
void refreshBoard(String from, String to) {
setState(() {
board[to] = board[from];
board[from] = " ";
});
}
from和to变量存储源和目标正方形的索引。 这些值在board HasMhap 中用作键。 进行移动时,from处的棋子会移至to.。此后,from处的方块应该变空。 它包含在setState()中,以确保每次移动后都更新 UI。
- 现在,让我们将其拖曳。 为此,我们将拖动项附加到
getPieceImage()方法返回的木板的每个图像小部件上。 我们通过修改方法来做到这一点:
Widget getImage(String squareName) {
return Expanded(
child: DragTarget<List>(builder: (context, accepted, rejected) {
return Draggable<List>(
child: mapImages(squareName),
feedback: mapImages(squareName),
onDragCompleted: () {},
data: [
squareName,
],
);
}, onWillAccept: (willAccept) {
return true;
}, onAccept: (List moveInfo) {
String from = moveInfo[0];
String to = squareName;
refreshBoard(from, to);
})
);
}
在前面的函数中,我们首先将特定正方形的图像包装在Draggable中。 此类用于感测和跟随屏幕上的拖动手势。 child属性用于指定要拖动的窗口小部件,而反馈内部的窗口小部件用于跟踪手指在屏幕上的移动。 当拖动完成并且用户抬起手指时,目标将有机会接受所携带的数据。 由于我们正在源和目标之间移动,因此我们将添加Draggable作为DragTarget的子代,以便可以在源和目标之间移动小部件。 onWillAccept设置为true,以便可以进行所有移动。
可以修改此属性,以使其具有可以区分合法象棋动作并且不允许拖动非法动作的功能。 放下片段并完成拖动后,将调用onAccept。 moveInfo列表保存有关拖动源的信息。 在这里,我们调用refreshBoard(),并传入from和to的值,以便屏幕可以反映运动。 至此,我们完成了向用户显示初始棋盘的操作,并使棋子可以在盒子之间移动。
在下一节中,我们将通过对托管的国际象棋服务器进行 API 调用来增加应用的交互性。 这些将使游戏栩栩如生。
将国际象棋引擎 API 与 UI 集成
托管的棋牌服务器将作为对手玩家添加到应用中。 用户将是白色的一面,而服务器将是黑色的一面。 这里要实现的游戏逻辑非常简单。 第一步是提供给应用用户。 用户进行移动时,他们将棋盘的状态从状态 X 更改为状态 Y。棋盘的状态由 FEN 字符串表示。 同样,他们将一块from移到一个特定的正方形to移到一个特定的正方形,这有助于他们的移动。 当用户完成移动时,状态 X 的 FEN 字符串及其当前移动(通过将from和to正方形连接在一起而获得)以POST请求的形式发送到服务器。 作为回报,服务器从其侧面进行下一步移动,然后将其反映在 UI 上。
让我们看一下此逻辑的代码:
- 首先,我们定义一个名为
getPositionString()的方法来为应用的特定状态生成 FEN 字符串:
String getPositionString(String move) {
String s = "";
for(int i = 8; i >= 1; i--) {
int count = 0;
for(int j = 97; j <= 104; j++) {
String ch = String.fromCharCode(j)+'$i';
if(board[ch] == " ") {
count += 1;
if(j == 104)
s = s + "$count";
} else {
if(count > 0)
s = s + "$count";
s = s + board[ch];count = 0;
}
}
s = s + "/";
}
String position = s.substring(0, s.length-1) + " w KQkq - 0 1";
var json = jsonEncode({"position": position, "moves": move});
}
在前面的方法中,我们将move作为参数,它是from和to变量的连接。 接下来,我们为棋盘的当前状态创建 FEN 字符串。 创建 FEN 字符串背后的逻辑是,我们遍历电路板的每一行并为该行创建一个字符串。 然后将生成的字符串连接到最终字符串。
让我们借助示例更好地理解这一点。 考虑一个rnbqkbnr/pp1ppppp/8/1p6/8/3P4/PPP1PPPP/RNBQKBNR w KQkq - 0 1的 FEN 字符串。 在此,每行可以用八个或更少的字符表示。 特定行的状态通过使用分隔符“/”与另一行分开。 对于特定的行,每件作品均以其指定的符号表示,其中P表示白兵,b表示黑相。 每个占用的正方形均由件符号明确表示。 例如,PpkB指示板上的前四个正方形被白色棋子,黑色棋子,黑色国王和白色主教占据。 对于空盒子,使用整数,该数字表示可传染的空盒子的数量。 注意示例 FEN 字符串中的8。 这表示该行的所有 8 个正方形均为空。 3P4表示前三个正方形为空,第四个方框被白色棋子占据,并且四个正方形为空。
在getPositionString()方法中,我们迭代从 8 到 1 的每一行,并为每行生成一个状态字符串。 对于每个非空框,我们只需在's'变量中添加一个表示该块的字符。 对于每个空框,当找到非空框或到达行末时,我们将count的值增加 1 并将其连接到's'字符串。 遍历每一行后,我们添加“/”以分隔两行。 最后,我们通过将生成的's'字符串与w KQkq - 0 1连接来生成位置字符串。 然后,我们通过将jsonEncode()与键值对结合使用来生成所需的 JSON 对象
- 我们使用“步骤 1”的“步骤 1”中的
from和to变量来保存用户的当前移动。 我们可以通过在refreshBoard()方法中添加两行来实现:
void refreshBoard(String from, String to) {
String move= from + to;
getPositionString(move);
.....
}
在前面的代码片段中,我们将from和to的值连接起来,并将它们存储在名为move的字符串变量中。 然后,我们调用getPositionString(),并将move的值传递给参数。
- 接下来,我们使用在上一步中
makePOSTRequest()方法中生成的JSON向服务器发出POST请求:
void makePOSTRequest(var json) async{
var url = 'http://35.200.253.0:8080/play';
var response = await http.post(url, headers: {"Content-Type": "application/json"} ,body: json);
String rsp = response.body;
String from = rsp.substring(0,3);
String to = rsp.substring(3);
}
首先,将国际象棋服务器的 IP 地址存储在url变量中。 然后,我们使用http.post()发出HTTP POST请求,并为 URL,标头和正文传递正确的值。 POST 请求的响应包含服务器端的下一个动作,并存储在变量响应中。 我们解析响应的主体并将其存储在名为rsp的字符串变量中。 响应基本上是一个字符串,是服务器端的源方和目标方的连接。 例如,响应字符串f4a3表示国际象棋引擎希望将棋子以f4正方形移动到a3正方形。 我们使用substring()分隔源和目标,并将值存储在from和to变量中。
- 现在,通过将调用添加到
makePOSTrequest()来从getPositionString()发出 POST 请求:
String getPositionString(String move) {
.....
makePOSTRequest(json);
}
在 FEN 字符串生成板的给定状态之后,对makePOSTrequest()的调用添加在函数的最后。
- 最后,我们使用
refreshBoardFromServer()方法刷新板以反映服务器在板上的移动:
void refreshBoardFromServer(String from, String to) {
setState(() {
board[to] = board[from];
board[from] = " ";
});
}
前述方法中的逻辑非常简单。 首先,我们将映射到from索引正方形的片段移动到to索引正方形,然后清空from索引正方形。
- 最后,我们调用适当的方法以用最新的动作更新 UI:
void makePOSTRequest(var json) async{
......
refreshBoardFromServer(from, to);
buildChessBoard();
}
发布请求成功完成后,我们收到了服务器的响应,我们将调用refreshBoardFromServer()以更新板上的映射。 最后,我们调用buildChessBoard()以在应用屏幕上反映国际象棋引擎所做的最新动作。
以下屏幕快照显示了国际象棋引擎进行移动后的更新的用户界面:

请注意,黑色的块在白色的块之后移动。 这就是代码的工作方式。 首先,用户采取行动。 它以板的初始状态发送到服务器。 然后,服务器以其移动进行响应,更新 UI。 作为练习,您可以尝试实现一些逻辑以区分有效动作和无效动作。
可以在这个页面中找到此代码。
现在,让我们通过创建材质应用来包装应用。
创建材质应用
现在,我们将在main.dart中创建最终的材质应用。 让我们从以下步骤开始:
- 首先,我们创建无状态窗口小部件
MyApp,并覆盖其build()方法,如下所示:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Chess',
theme: ThemeData(primarySwatch: Colors.blue,),
home: MyHomePage(title: 'Chess'),
);
}
}
- 我们创建一个单独的
StatefulWidget,称为MyHomePage,以便将 UI 放置在屏幕中央。MyHomePage的build()方法如下所示:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Chess'),),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ChessGame()
],
),
),
);
}
- 最后,我们通过在
main.dart中添加以下行来执行整个代码:
void main() => runApp(MyApp());
而已! 现在,我们有一个交互式的国际象棋游戏应用,您可以与聪明的对手一起玩。 希望你赢!
整个文件的代码可以在这个页面中找到。
总结
在此项目中,我们介绍了强化学习的概念以及为什么强化学习在创建游戏性 AI 的开发人员中很受欢迎。 我们讨论了 Google DeepMind 的 AlphaGo 及其兄弟项目,并深入研究了它们的工作算法。 接下来,我们创建了一个类似的程序来玩 Connect 4,然后下棋。 我们将基于 AI 的国际象棋引擎作为 API 部署到 GPU 实例的 GCP 上,并将其与基于 Flutter 的应用集成。 我们还了解了如何使用 UCI 促进国际象棋的无状态游戏。 完成此项目后,您将对如何将游戏转换为强化学习环境,如何以编程方式定义游戏规则以及如何创建用于玩这些游戏的自学智能体有很好的了解。
在下一章中,我们将创建一个应用,该应用可以使低分辨率图像变成非常高分辨率的图像。 我们将在 AI 的帮助下进行此操作。
九、构建图像超分辨率应用
还记得上次和亲人一起旅行并拍了一些漂亮的照片作为记忆,但是当您回到家并刷过它们时,您发现它们非常模糊且质量低下吗? 现在,您剩下的所有美好时光就是您自己的心理记忆和那些模糊的照片。 如果可以使您的照片清晰透明并且可以看到其中的每个细节,那不是很好吗?
超分辨率是基于像素信息的近似将低分辨率图像转换为高分辨率图像的过程。 虽然今天可能还不完全是神奇的,但当技术发展到足以成为通用 AI 应用时,它肯定会在将来挽救生命。
在此项目中,我们将构建一个应用,该应用使用托管在 DigitalOcean Droplet 上的深度学习模型,该模型可以同时比较低分辨率和高分辨率图像,从而使我们更好地了解今天的技术。 我们将使用生成对抗网络(GAN)生成超分辨率图像。
在本章中,我们将介绍以下主题:
- 基本项目架构
- 了解 GAN
- 了解图像超分辨率的工作原理
- 创建 TensorFlow 模型以实现超分辨率
- 构建应用的 UI
- 从设备的本地存储中获取图片
- 在 DigitalOcean 上托管 TensorFlow 模型
- 在 Flutter 上集成托管的自定义模型
- 创建材质应用
让我们从了解项目的架构开始。
基本项目架构
让我们从了解项目的架构开始。
我们将在本章中构建的项目主要分为两个部分:
- Jupyter 笔记本,它创建执行超分辨率的模型。
- 使用该模型的 Flutter 应用,在 Jupyter 笔记本上接受训练后,将托管在 DigitalOcean 中的 Droplet 中。
从鸟瞰图可以用下图描述该项目:

将低分辨率图像放入模型中,该模型是从 Firebase 上托管的 ML Kit 实例中获取的,并放入 Flutter 应用中。 生成输出并将其作为高分辨率图像显示给用户。 该模型缓存在设备上,并且仅在开发人员更新模型时才更新,因此可以通过减少网络延迟来加快预测速度。
现在,让我们尝试更深入地了解 GAN。
了解 GAN
Ian Goodfellow,Yoshua Bengio 和其他人在 NeurIPS 2014 中引入的 GAN 席卷全球。 可以应用于各种领域的 GAN 会根据模型对实际数据样本的学习近似,生成新的内容或序列。 GAN 已被大量用于生成音乐和艺术的新样本,例如下图所示的面孔,而训练数据集中不存在这些面孔:

经过 60 个周期的训练后,GAN 生成的面孔。 该图像取自这里。
前面面孔中呈现的大量真实感证明了 GAN 的力量–在为他们提供良好的训练样本量之后,他们几乎可以学习生成任何类型的模式。
GAN 的核心概念围绕两个玩家玩游戏的想法。 在这个游戏中,一个人说出一个随机句子,另一个人仅仅考虑第一人称使用的单词就指出它是事实还是假。 第二个人唯一可以使用的知识是假句子和实句中常用的单词(以及如何使用)。 这可以描述为由 minimax 算法玩的两人游戏,其中每个玩家都试图以其最大能力抵消另一位玩家所做的移动。 在 GAN 中,第一个玩家是生成器(G),第二个玩家是判别器(D)。 G和D都是常规 GAN 中的神经网络。 生成器从训练数据集中给出的样本中学习,并基于其认为当观察者查看时可以作为真实样本传播的样本来生成新样本。
判别器从训练样本(正样本)和生成器生成的样本(负样本)中学习,并尝试对哪些图像存在于数据集中以及哪些图像进行分类。 它从G获取生成的图像,并尝试将其分类为真实图像(存在于训练样本中)或生成图像(不存在于数据库中)。
通过反向传播,GAN 尝试不断减少判别器能够对生成器正确生成的图像进行分类的次数。 一段时间后,我们希望达到识别器在识别生成的图像时开始表现不佳的阶段。 这是 GAN 停止学习的地方,然后可以使用生成器生成所需数量的新样本。 因此,训练 GAN 意味着训练生成器以从随机输入产生输出,从而使判别器无法将其识别为生成的图像。
判别器将传递给它的所有图像分为两类:
- 真实图像:数据集中存在的图像或使用相机拍摄的图像
- 伪图像:使用某软件生成的图像
生成器欺骗判别器的能力越好,当向其提供任何随机输入序列时,生成的输出将越真实。
让我们以图表形式总结前面关于 GAN 进行的讨论:

GAN 具有许多不同的变体,所有变体都取决于它们正在执行的任务。 其中一些如下:
- 渐进式 GAN:在 ICLR 2018 上的一篇论文中介绍,渐进式 GAN 的生成器和判别器均以低分辨率图像开始,并随着图像层的增加而逐渐受到训练,从而使系统能够生成高分辨率图像。 例如,在第一次迭代中生成的图像为
10x10像素,在第二代中它变为20x20,依此类推,直到获得非常高分辨率的图像为止。 生成器和判别器都在深度上一起增长。 - 条件 GAN:假设您有一个 GAN 可以生成 10 个不同类别的样本,但是在某个时候,您希望它在给定类别或一组类别内生成样本。 这是有条件 GAN 起作用的时候。有条件 GAN 使我们可以生成 GAN 中经过训练可以生成的所有标签中任何给定标签的样本。 在图像到图像的翻译领域中,已经完成了条件 GAN 的一种非常流行的应用,其中将一个图像生成为相似或相同域的另一个更逼真的图像。 您可以通过这个页面上的演示来尝试涂鸦一些猫,并获得涂鸦的真实感版本。
- 栈式 GAN:栈式 GAN 的最流行的应用是基于文本描述生成图像。 在第一阶段,GAN 生成描述项的概述,在第二阶段,根据描述添加颜色。 然后,后续层中的 GAN 将更多细节添加到图像中,以生成图像的真实感版本,如描述中所述。 通过观察堆叠 GAN 的第一次迭代中的图像已经处于将要生成最终输出的尺寸,可以将栈式 GAN 与渐进式 GAN 区别开来。但是,与渐进式 GAN 相似,在第一次迭代中, 图像是最小的,并且需要进一步的层才能将其馈送到判别器。
在此项目中,我们将讨论 GAN 的另一种形式,称为超分辨率 GAN(SRGAN)。 我们将在下一部分中了解有关此变体的更多信息。
了解图像超分辨率的工作原理
几十年来,人们一直在追求并希望能够使低分辨率图像更加精细,以及使高分辨率图像化。 超分辨率是用于将低分辨率图像转换为超高分辨率图像的技术的集合,是图像处理工程师和研究人员最激动人心的工作领域之一。 已经建立了几种方法和方法来实现图像的超分辨率,并且它们都朝着自己的目标取得了不同程度的成功。 然而,近来,随着 SRGAN 的发展,关于使用任何低分辨率图像可以实现的超分辨率的量有了显着的改进。
但是在讨论 SRGAN 之前,让我们了解一些与图像超分辨率有关的概念。
了解图像分辨率
用质量术语来说,图像的分辨率取决于其清晰度。 分辨率可以归类为以下之一:
- 像素分辨率
- 空间分辨率
- 时间分辨率
- 光谱分辨率
- 辐射分辨率
让我们来看看每个。
像素分辨率
指定分辨率的最流行格式之一,像素分辨率最通常是指形成图像时涉及的像素数量。 单个像素是可以在任何给定查看设备上显示的最小单个单元。 可以将几个像素组合在一起以形成图像。 在本书的前面,我们讨论了图像处理,并将像素称为存储在矩阵中的颜色信息的单个单元,它代表图像。 像素分辨率定义了形成数字图像所需的像素元素总数,该总数可能与图像上可见的有效像素数不同。
标记图像像素分辨率的一种非常常见的表示法是以百万像素表示。 给定NxM像素分辨率的图像,其分辨率可以写为(NxM / 1000000)百万像素。 因此,尺寸为2,000x3,000的图像将具有 6,000,000 像素,其分辨率可以表示为 6 兆像素。
空间分辨率
这是观察图像的人可以分辨图像中紧密排列的线条的程度的度量。 在这里,严格说来,图像的像素越多,清晰度越好。 这是由于具有较高像素数量的图像的空间分辨率较低。 因此,需要良好的空间分辨率以及具有良好的像素分辨率以使图像以良好的质量呈现。
它也可以定义为像素一侧所代表的距离量。
时间分辨率
分辨率也可能取决于时间。 例如,卫星或使用无人飞行器(UAV)无人机拍摄的同一区域的图像可能会随时间变化。 重新捕获相同区域的图像所需的时间称为时间分辨率。
时间分辨率主要取决于捕获图像的设备。 如在图像捕捉的情况下,这可以是变型,例如当在路边的速度陷阱照相机中触发特定传感器时执行图像捕捉。 它也可以是常数。 例如,在配置为每x间隔拍照的相机中。
光谱分辨率
光谱分辨率是指图像捕获设备可以记录的波段数。 也可以将其定义为波段的宽度或每个波段的波长范围。 在数字成像方面,光谱分辨率类似于图像中的通道数。 理解光谱分辨率的另一种方法是在任何给定图像或频带记录中可区分的频带数。
黑白图像中的波段数为 1,而彩色(RGB)图像中的波段数为 3。可以捕获数百个波段的图像,其中其他波段可提供有关图像的不同种类的信息。 图片。
辐射分辨率
辐射分辨率是捕获设备表示在任何频带/通道上接收到的强度的能力。 辐射分辨率越高,设备可以更准确地捕获其通道上的强度,并且图像越真实。
辐射分辨率类似于图像每个像素的位数。 虽然 8 位图像像素可以表示 256 个不同的强度,但是 256 位图像像素可以表示2 ^ 256个不同的强度。 黑白图像的辐射分辨率为 1 位,这意味着每个像素只能有两个不同的值,即 0 和 1。
现在,让我们尝试了解 SRGAN。
了解 SRGAN
SRGAN 是一类 GAN,主要致力于从低分辨率图像创建超分辨率图像。
SRGAN 算法的功能描述如下:该算法从数据集中选取高分辨率图像,然后将其采样为低分辨率图像。 然后,生成器神经网络尝试从低分辨率图像生成高分辨率图像。 从现在开始,我们将其称为超分辨率图像。 将超分辨率图像发送到鉴别神经网络,该神经网络已经在高分辨率图像和一些基本的超分辨率图像的样本上进行了训练,以便可以对它们进行分类。
判别器将由生成器发送给它的超分辨率图像分类为有效的高分辨率图像,伪高分辨率图像或超分辨率图像。 如果将图像分类为超分辨率图像,则 GAN 损失会通过生成器网络反向传播,以便下次产生更好的伪造图像。 随着时间的流逝,生成器将学习如何创建更好的伪造品,并且判别器开始无法正确识别超分辨率图像。 GAN 在这里停止学习,被列为受过训练的人。
可以用下图来总结:

现在,让我们开始创建用于超分辨率的 SRGAN 模型。
创建 TensorFlow 模型来实现超分辨率
现在,我们将开始构建在图像上执行超分辨率的 GAN 模型。 在深入研究代码之前,我们需要了解如何组织项目目录。
项目目录结构
本章中包含以下文件和文件夹:
api/:model /:__init __.py:此文件指示此文件的父文件夹可以像模块一样导入。common.py:包含任何 GAN 模型所需的常用函数。srgan.py:其中包含开发 SRGAN 模型所需的函数。weights/:gan_generator.h5:模型的预训练权重文件。 随意使用它来快速运行并查看项目的工作方式。data.py:用于在 DIV2K 数据集中下载,提取和加载图像的工具函数。flask_app.py:我们将使用此文件来创建将在 DigitalOcean 上部署的服务器。train.py:模型训练文件。 我们将在本节中更深入地讨论该文件。
您可以在这个页面中找到项目此部分的源代码。
多样 2K(DIV2K)数据集由图像恢复和增强的新趋势(NTIRE)2017 单张图像超分辨率挑战赛引入,也用于挑战赛的 2018 版本中。
在下一节中,我们将构建 SRGAN 模型脚本。
创建用于超分辨率的 SRGAN 模型
首先,我们将从处理train.py文件开始:
- 让我们从将必要的模块导入项目开始:
import os
from data import DIV2K
from model.srgan import generator, discriminator
from train import SrganTrainer, SrganGeneratorTrainer
前面的导入引入了一些现成的类,例如SrganTrainer,SrganGeneratorTrainer等。 在完成此文件的工作后,我们将详细讨论它们。
- 现在,让我们为权重创建一个目录。 我们还将使用此目录来存储中间模型:
weights_dir = 'weights'
weights_file = lambda filename: os.path.join(weights_dir, filename)
os.makedirs(weights_dir, exist_ok=True)
- 接下来,我们将从 DIV2K 数据集中下载并加载图像。 我们将分别下载训练和验证图像。 对于这两组图像,可以分为两对:高分辨率和低分辨率。 但是,这些是单独下载的:
div2k_train = DIV2K(scale=4, subset='train', downgrade='bicubic')
div2k_valid = DIV2K(scale=4, subset='valid', downgrade='bicubic')
- 将数据集下载并加载到变量后,我们需要将训练图像和验证图像都转换为 TensorFlow 数据集对象。 此步骤还将两个数据集中的高分辨率和低分辨率图像结合在一起:
train_ds = div2k_train.dataset(batch_size=16, random_transform=True)
valid_ds = div2k_valid.dataset(batch_size=16, random_transform=True, repeat_count=1)
- 现在,回想一下我们在“了解 GAN”部分中提供的 GAN 的定义。 为了使生成器开始产生判别器可以评估的伪造品,它需要学习创建基本的伪造品。 为此,我们将快速训练神经网络,以便它可以生成基本的超分辨率图像。 我们将其命名为预训练器。 然后,我们将预训练器的权重迁移到实际的 SRGAN,以便它可以通过使用判别器来学习更多。 让我们构建并运行预训练器:
pre_trainer = SrganGeneratorTrainer(model=generator(), checkpoint_dir=f'.ckpt/pre_generator')
pre_trainer.train(train_ds,
valid_ds.take(10),
steps=1000000,
evaluate_every=1000,
save_best_only=False)
pre_trainer.model.save_weights(weights_file('pre_generator.h5'))
现在,我们已经训练了一个基本模型并保存了权重。 我们可以随时更改 SRGAN 并通过加载其权重从基础训练中重新开始。
- 现在,让我们将预训练器权重加载到 SRGAN 对象中,并执行训练迭代:
gan_generator = generator()
gan_generator.load_weights(weights_file('pre_generator.h5'))
gan_trainer = SrganTrainer(generator=gan_generator, discriminator=discriminator())
gan_trainer.train(train_ds, steps=200000)
请注意,在具有 8 GB RAM 和 Intel i7 处理器的普通计算机上,上述代码中的训练操作可能会花费大量时间。 建议在具有图形处理器(GPU)的基于云的虚拟机中执行此训练。
- 现在,让我们保存 GAN 生成器和判别器的权重:
gan_trainer.generator.save_weights(weights_file('gan_generator.h5'))
gan_trainer.discriminator.save_weights(weights_file('gan_discriminator.h5'))
现在,我们准备继续进行下一部分,在该部分中将构建将使用此模型的 Flutter 应用的 UI。
构建应用的 UI
现在,我们了解了图像超分辨率模型的基本功能并为其创建了一个模型,让我们深入研究构建 Flutter 应用。 在本节中,我们将构建应用的 UI。
该应用的用户界面非常简单:它将包含两个图像小部件和按钮小部件。 当用户单击按钮小部件时,他们将能够从设备的库中选择图像。 相同的图像将作为输入发送到托管模型的服务器。 服务器将返回增强的图像。 屏幕上将放置的两个图像小部件将用于显示服务器的输入和服务器的输出。
下图说明了应用的基本结构和最终流程:

该应用的三个主要小部件可以简单地排列在一列中。 该应用的小部件树如下所示:

现在,让我们编写代码以构建主屏幕。 以下步骤讨论了该应用小部件的创建和放置:
- 首先,我们创建一个名为
image_super_resolution.dart的新文件。 这将包含一个名为ImageSuperResolution的无状态窗口小部件。 该小部件将包含应用主屏幕的代码。 - 接下来,我们将定义一个名为
buildImageInput()的函数,该函数返回一个小部件,该小部件负责显示用户选择的图像:
Widget buildImage1() {
return Expanded(
child: Container(
width: 200,
height: 200,
child: img1
)
);
}
此函数返回带有Container作为其child.的Expanded小部件。Container的width和height为200。 Container的子元素最初是存储在资产文件夹中的占位符图像,可以通过img1变量进行访问,如下所示:
var img1 = Image.asset('assets/place_holder_image.png');
我们还将在pubspec.yaml文件中添加图像的路径,如下所示:
flutter:
assets:
- assets/place_holder_image.png
- 现在,我们将创建另一个函数
buildImageOutput(),该函数返回一个小部件,该小部件负责显示模型返回的增强图像:
Widget buildImageOutput() {
return Expanded(
child: Container(
width: 200,
height: 200,
child: imageOutput
)
);
}
此函数返回一个以其Container作为其子元素的Expanded小部件。 Container的宽度和高度设置为200。 Container的子级是名为imageOutput的小部件。 最初,imageOutput还将包含一个占位符图像,如下所示:
Widget imageOutput = Image.asset('assets/place_holder_image.png');
将模型集成到应用中后,我们将更新imageOutput。
- 现在,我们将定义第三个函数
buildPickImageButton(),该函数返回一个Widget,我们可以使用它从设备的图库中选择图像:
Widget buildPickImageButton() {
return Container(
margin: EdgeInsets.all(8),
child: FloatingActionButton(
elevation: 8,
child: Icon(Icons.camera_alt),
onPressed: () => {},
)
);
}
此函数返回以FloatingActionButton作为其子元素的Container。 按钮的elevation属性控制其下方阴影的大小,并设置为8。 为了反映该按钮用于选择图像,通过Icon类为它提供了摄像机的图标。 当前,我们已经将按钮的onPressed属性设置为空白。 我们将在下一部分中定义一个函数,使用户可以在按下按钮时从设备的图库中选择图像。
- 最后,我们将覆盖
build方法以返回应用的Scaffold:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Image Super Resolution')),
body: Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
buildImageInput(),
buildImageOutput(),
buildPickImageButton()
]
)
)
);
}
Scaffold包含一个appBar,其标题设置为“图像超分辨率”。 Scaffold的主体为Container,其子代为Column。 该列的子级是我们在先前步骤中构建的三个小部件。 另外,我们将Column的crossAxisAlignment属性设置为CrossAxisAlignment.center,以确保该列位于屏幕的中央。
至此,我们已经成功构建了应用的初始状态。 以下屏幕截图显示了该应用现在的外观:

尽管屏幕看起来很完美,但目前无法正常工作。 接下来,我们将向应用添加功能。 我们将添加让用户从图库中选择图像的功能。
从设备的本地存储中获取图片
在本节中,我们将添加FloatingActionButton的功能,以使用户可以从设备的图库中选择图像。 这最终将被发送到服务器,以便我们能够收到响应。
以下步骤描述了如何启动图库并让用户选择图像:
- 为了允许用户从设备的图库中选择图像,我们将使用
image_picker库。 这将启动图库并存储用户选择的图像文件。 我们将从在pubspec.yaml文件中添加依赖项开始:
image_picker: 0.4.12+1
另外,我们通过在终端上运行flutter pub get来获取库。
- 接下来,我们将库导入
image_super_resolution.dart文件中:
import 'package:image_picker/image_picker.dart';
- 现在,让我们定义
pickImage()函数,该函数使用户可以从图库中选择图像:
void pickImage() async {
File pickedImg = await ImagePicker.pickImage(source: ImageSource.gallery);
}
- 从函数内部,我们只需调用
ImagePicker.pickImage()并将source指定为ImageSource.gallery即可。 该库本身处理启动设备图库的复杂性。 用户选择的图像文件最终由该函数返回。 我们将函数返回的文件存储在File类型的pickedImg变量中。 - 接下来,我们定义
loadImage()函数,以便在屏幕上显示用户选择的图像:
void loadImage(File file) {
setState(() {
img1 = Image.file(file);
});
}
此函数将用户选择的图像文件作为输入。 在函数内部,我们将先前声明的img1变量的值设置为Image.file(file),这将返回从'file'构建的Image小部件。 回想一下,最初,img1被设置为占位符图像。 为了重新渲染屏幕并显示用户选择的图像,我们将img1的新分配放在setState()中。
- 现在,将
pickImage()添加到builtPickImageButton()内的FloatingActionButton的onPressed属性中:
Widget buildPickImageButton() {
return Container(
....
child: FloatingActionButton(
....
onPressed: () => pickImage(),
)
);
}
前面的补充内容确保单击按钮时,会启动图库,以便可以选择图像。
- 最后,我们将从
pickImage()向loadImage()添加一个调用:
void pickImage() async {
....
loadImage(pickedImg);
}
在loadImage()内部,我们传入用户选择的图像,该图像存储在pickedImage变量中,以便可以在应用的屏幕上查看该图像。
完成上述所有步骤后,该应用将如下所示:

至此,我们已经构建了应用的用户界面。 我们还添加了一些功能,使用户可以从设备的图库中选择图像并将其显示在屏幕上。
在下一部分中,我们将学习如何托管在“为超分辨率创建 TensorFlow 模型”中创建的模型作为 API,以便我们可以使用它执行超分辨率。
在 DigitalOcean 上托管 TensorFlow 模型
DigitalOcean 是一个了不起的低成本云解决方案平台,非常易于上手,并提供了应用开发人员为立即可用的应用后端提供动力所需的几乎所有功能。 该界面非常易于使用,并且 DigitalOcean 拥有一些最广泛的文档,这些文档围绕着如何在云上设置不同类型的应用服务器提供入门。
在这个项目中,我们将使用 DigitalOcean 的 Droplet 部署我们的超分辨率 API。 DigitalOcean 中的 Droplet 只是通常在共享硬件空间上运行的虚拟机。
首先,我们将在项目目录中创建flask_app.py文件,并添加服务器工作所需的代码。
创建一个 Flask 服务器脚本
在本节中,我们将处理flask_app.py文件,该文件将作为服务器在云虚拟机上运行。 让我们开始吧:
- 首先,我们将对文件进行必要的导入:
from flask import Flask, request, jsonify, send_file
import os
import time
from matplotlib.image import imsave
from model.srgan import generator
from model import resolve_single
- 现在,我们将定义
weights目录并将生成器权重加载到文件中:
weights_dir = 'weights'
weights_file = lambda filename: os.path.join(weights_dir, filename)
gan_generator = generator()
gan_generator.load_weights(weights_file('gan_generator.h5'))
- 接下来,我们将使用以下代码行实例化
Flask应用:
app = Flask(__name__)
- 现在,我们准备构建服务器将监听的路由。 首先,我们将创建
/generate路由,该路由将图像作为输入,生成其超分辨率版本,并将所生成的高分辨率图像的文件名返回给用户:
@app.route('/generate', methods=["GET", "POST"])
def generate():
global gan_generator
imgData = request.get_data()
with open("input.png", 'wb') as output:
output.write(imgData)
lr = load_image("input.png")
gan_sr = resolve_single(gan_generator, lr)
epoch_time = int(time.time())
outputfile = 'output_%s.png' % (epoch_time)
imsave(outputfile, gan_sr.numpy())
response = {'result': outputfile}
return jsonify(response)
让我们尝试了解前面的代码块中发生的情况。 /generate路由已设置为仅监听 HTTP 请求的 GET 和 POST 方法。 首先,该方法获取 API 请求中提供给它的图像,将其转换为 NumPy 数组,然后将其提供给 SRGAN 模型。 SRGAN 模型返回超分辨率图像,然后为其分配一个唯一的名称并存储在服务器上。 用户显示文件名,他们可以使用该文件名调用另一个端点来下载文件。 让我们现在构建此端点。
- 为了创建端点以便下载生成的文件,我们可以使用以下代码:
@app.route('/download/<fname>', methods=['GET'])
def download(fname):
return send_file(fname)
在这里,我们创建了一个名为/download的端点,该端点附加了文件名后,将其提取并发送回给用户。
- 最后,我们可以编写执行该脚本并设置服务器的代码:
app.run(host="0.0.0.0", port="8080")
保存此文件。 确保此时将您的存储库推送到 GitHub/GitLab 存储库。
现在,我们准备将该脚本部署到DigitalOcean Droplet。
将 Flask 脚本部署到 DigitalOcean Droplet
要将 Flask 脚本部署到 DigitalOcean Droplet,您必须创建一个 DigitalOcean 帐户并创建一个 Droplet。 请按照以下步骤操作:
- 在您喜欢的 Web 浏览器中转到 digitalocean.com 。
如果您希望在添加帐单详细信息时获得 100 美元的赠金,也可以转到这里。 我们稍后再做。
-
在 DigitalOcean 的注册表格中填写您的详细信息,然后提交表格继续进行下一步。
-
系统将要求您验证电子邮件并为 DigitalOcean 帐户添加结算方式。
-
在下一步中,系统将提示您创建第一个项目。 输入所需的详细信息并提交表单以创建您的项目:

- 创建项目后,您将被带到 DigitalOcean 仪表板。 您将能够看到创建 Droplet 的提示,如以下屏幕截图所示:

-
单击“提示”以弹出 Droplet 创建表单。 选择下表中描述的选项:
字段 说明 要使用的值 选择一张图片 Droplet 将在其上运行的操作系统。 Ubuntu 18.04(或最新可用版本) 选择一个计划 选择 Droplet 的配置。 4 GB RAM 或更高 添加块存储 Droplet 的其他持久性,可拆卸存储容量。 保留默认值 选择数据中心区域 投放 Droplet 的区域。 根据您的喜好选择任何一个 选择其他选项 选择将与您的 Droplet 一起使用的所有其他功能。 保留默认值 认证方式 选择虚拟机的认证方法。 一次性密码 完成并创建 Droplet 的一些其他设置和选项。 保留默认值 -
单击“创建 Droplet”,然后等待 DigitalOcean 设置您的 Droplet。
-
创建 Droplet 后,单击其名称以打开 Droplet 管理控制台,该控制台应如下所示:

-
现在,我们可以使用上一幅截图所示的 Droplet 控制台左侧导航菜单上的 Access 选项卡登录到 Droplet。 单击“访问”,然后启动控制台。
-
将打开一个新的浏览器窗口,显示您的 Droplet 的 VNC 视图。 系统将要求您输入 Droplet 的用户名和密码。 您必须在此处使用的用户名是
root。 可以在您已注册的电子邮件收件箱中找到该密码。 -
首次登录时,系统会要求您更改 Droplet 密码。 确保您选择一个强密码。
-
登录 Droplet 后,将在 VNC 终端上看到一些 Ubuntu 欢迎文本,如以下屏幕截图所示:

- 现在,按照本书的“附录”中的说明,执行在云 VM 上设置深度学习环境的步骤。
- 接下来,将项目存储库克隆到您的 Droplet,并使用以下命令将工作目录更改为存储库的
api文件夹:
git clone https://github.com/yourusername/yourrepo.git
cd yourrepo/api
- 使用以下命令运行服务器:
python3 flask_app.py
除了来自 TensorFlow 的一些警告消息之外,在终端输出的末尾,您还应该看到以下几行指示服务器已成功启动:

现在,如 Droplet 控制台所示,您的服务器已启动并在 Droplet 的 IP 上运行。
在下一部分中,我们将学习如何使用 Flutter 应用向服务器发出 POST 请求,并在屏幕上显示服务器的响应。
在 Flutter 上集成托管的自定义模型
在本节中,我们将向托管模型发出 POST 请求,并将其传递给用户选择的图像。 服务器将以 PNG 格式响应NetworkImage。 然后,我们将更新之前添加的图像小部件,以显示模型返回的增强图像。
让我们开始将托管模型集成到应用中:
- 首先,我们将需要两个以上的外部库来发出成功的 POST 请求。 因此,我们将以下库作为依赖项添加到
pubspec.yaml文件:
dependencies:
flutter:
http: 0.12.0+4
mime: 0.9.6+3
http依赖项包含一组类和函数,这些类和函数使使用 HTTP 资源非常方便。 mime依赖性用于处理 MIME 多部分媒体类型的流。
现在,我们需要运行flutter pub get以确保所有依赖项均已正确安装到我们的项目中。
- 接下来,我们将所有新添加的依赖项导入
image_super_resolution.dart文件:
import 'package:http/http.dart' as http;
import 'package:mime/mime.dart';
- 现在,我们需要定义
fetchResponse(),它接受所选的图像文件并向服务器创建 POST 请求:
void fetchResponse(File image) async {
final mimeTypeData =
lookupMimeType(image.path, headerBytes: [0xFF, 0xD8]).split('/');
final imageUploadRequest = http.MultipartRequest('POST', Uri.parse("http://x.x.x.x:8080/generate"));
final file = await http.MultipartFile.fromPath('image', image.path,
contentType: MediaType(mimeTypeData[0], mimeTypeData[1]));
imageUploadRequest.fields['ext'] = mimeTypeData[1];
imageUploadRequest.files.add(file);
try {
final streamedResponse = await imageUploadRequest.send();
final response = await http.Response.fromStream(streamedResponse);
final Map<String, dynamic> responseData = json.decode(response.body);
String outputFile = responseData['result'];
} catch (e) {
print(e);
return null;
}
}
在前面的方法中,我们通过使用lookupMimeType函数并使用文件的路径及其头来查找所选文件的 MIME 类型。 然后,按照托管模型的服务器的预期,初始化一个多部分请求。 我们使用 HTTP 执行此操作。 我们使用MultipartFile.fromPath并将image的值设置为作为POST参数附加的路径。 由于image_picker存在一些错误,因此我们将图片的扩展名明确传递给请求主体。 因此,它将图像扩展名与文件名(例如filenamejpeg)混合在一起,这在管理或验证文件扩展名时在服务器端造成了问题。 然后,来自服务器的响应将存储在response变量中。 响应为 JSON 格式,因此我们需要使用json.decode()对其进行解码。 该函数接收响应的主体,可以使用response.body进行访问。 我们将解码后的 JSON 存储在responseData变量中。 最后,使用responseDate['result']访问服务器的输出并将其存储在outputFile变量中。
- 接下来,我们定义
displayResponseImage()函数,该函数接受服务器在outputFile参数内返回的 PNG 文件的名称:
void displayResponseImage(String outputFile) {
print("Updating Image");
outputFile = 'http://x.x.x.x:8080/download/' + outputFile;
setState(() {
imageOutput = Image(image: NetworkImage(outputFile));
});
}
根据服务器的自定义,我们需要在文件名之前附加一个字符串以将其显示在屏幕上。 该字符串应包含服务器正在运行的端口地址,后跟'/download/<outputFile>'。 然后,我们将outputFile的最终值用作url值,将imageOutput小部件的值设置为NetworkImage。 另外,我们将其封装在[HTG5]中,以便在正确获取响应后可以刷新屏幕。
- 接下来,我们在
fetchResponse()的最后调用displayResponseImage(),并传入从托管模型收到的outputFile:
void fetchResponse(File image) async {
....
displayResponseImage(outputFile);
}
- 最后,通过传入用户最初选择的图像,将调用从
pickImage()添加到fetchResponse():
void pickImage() async {
....
fetchResponse(pickedImg);
}
在前面的步骤中,我们首先向托管模型的服务器发出 POST 请求。 然后,我们解码响应并添加代码以在屏幕上显示它。 在pickImage()末尾添加fetchResponse()可确保仅在用户选择图像后才发出 POST 请求。 另外,为了确保在成功解码来自服务器的输出之后已经尝试显示响应图像,在fetchResponse()的末尾调用displayImageResponse()。 以下屏幕快照显示了屏幕的最终预期状态:

因此,我们已经完成了应用的构建,以便可以显示模型的输出。 我们将两个图像保存在屏幕上,以便我们可以看到它们之间的差异。
可以在这个页面上访问image_super_resolution.dart文件的代码。
创建材质应用
现在,我们将添加main.dart以创建最终的 Material 应用。 我们将创建一个名为MyApp的无状态小部件,并覆盖build()方法:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: ImageSuperResolution(),
);
}
}
最后,我们执行代码,如下所示:
void main() => runApp(MyApp());
至此,我们完成了一个应用的创建,该应用允许用户选择图像并修改其分辨率。
总结
在本章中,我们研究了超分辨率图像以及如何使用 SRGAN 应用它们。 我们还研究了其他类型的 GAN 以及 GAN 的总体工作方式。 然后,我们讨论了如何创建一个 Flutter 应用,该应用可以与 DigitalOcean Droplet 上托管的 API 集成在一起,以便当从图库中拾取图像时可以执行图像超分辨率。 接下来,我们介绍了如何使用 DigitalOcean Droplet,以及由于其低成本和易于使用的界面而成为托管应用后端的理想选择。
在下一章中,我们将讨论一些流行的应用,这些应用通过将深度学习集成到其功能中而获得了很大的改进。 我们还将探索手机深度学习中的一些热门研究领域,并简要讨论已在其上进行的最新工作。
十、前方的路
旅程中最重要的部分是知道结束后要去哪里。 到目前为止,在本系列项目中,我们已经介绍了一些与 Flutter 应用相关的独特且功能强大的深度学习(DL)应用,但重要的是,您必须知道在哪里可以找到更多这样的项目,灵感和知识来构建自己的出色项目。 在本章中,我们将简要介绍当今在移动应用上使用 DL 的最流行的应用,当前趋势以及将来在该领域中将会出现的情况。
在本章中,我们将介绍以下主题:
- 了解移动应用中 DL 的最新趋势
- 探索移动设备上 DL 的最新发展
- 探索移动应用中 DL 的当前研究领域
让我们开始研究 DL 移动应用世界中的一些趋势。
了解移动应用中 DL 的最新趋势
特别是 DL,随着最新技术和硬件的发展,人工智能(AI)变得越来越移动。 组织一直在使用智能算法来提供个性化的用户体验并提高应用参与度。 借助人脸检测,图像处理,文本识别,对象识别和语言翻译等技术,移动应用已不仅仅是提供静态信息的媒介。 它们能够适应用户的个人偏好和选择以及当前和过去的环境状况,以提供无缝的用户体验。
让我们看一下一些流行的应用及其部署的方法,以提供良好的用户体验,同时增加应用的参与度。
数学求解器
数学求解器应用由微软于 2020 年 1 月 16 日启动,可通过简单地单击智能手机上有问题的图片来帮助学生完成数学作业。 该应用为基本和高级数学问题提供支持,涵盖了广泛的主题,包括基本算术,二次方程,微积分和统计。 以下屏幕截图显示了该应用的工作方式:

用户可以在其智能手机上单击手写或打印问题的图片,或直接在设备上涂鸦或键入图片。 该应用利用 AI 来识别问题并准确解决。 此外,它还可以提供分步说明,并提供其他学习资料,例如与问题有关的工作表和视频教程。
Netflix
Netflix 的推荐系统是在移动应用上使用 DL 的最大成功案例之一。 Netflix 利用多种算法来了解用户的偏好,并提供了他们可能感兴趣的推荐列表。所有内容都标记有标签,这些标签提供了可以从中学习算法的初始数据集。 此外,该系统监视着超过 1 亿个用户个人资料,以分析人们观看的内容,以后可能观看的内容,以前观看的内容,一年前观看的内容,等等。 将收集的所有数据汇总在一起,以了解用户可能感兴趣的内容类型。
然后,将使用标签和用户行为收集的数据汇总在一起,并输入到复杂的 ML 算法中。 这些数据有助于解释可能最重要的因素-例如,如果用户一年前观看的电影与上周观看的系列相比应被计数两次。 该算法还可以从用户行为中学习,例如用户喜欢还是不喜欢特定的内容,或者用户在 2 个晚上观看和观看的节目。 将所有因素汇总在一起并进行仔细分析,从而得出用户可能最感兴趣的建议列表。
谷歌地图
Google Maps 已帮助通勤者前往新地方,探索新城市并监控每日流量。 在 2019 年 6 月上旬,谷歌地图发布了一项新功能,使用户可以监控印度 10 个主要城市的巴士旅行时间,以及从印度铁路局获得实时更新。 该功能位于班加罗尔,钦奈,哥印拜陀,德里,海得拉巴,勒克瑙,孟买,浦那和苏拉特,它利用 Google 的实时交通数据和公交时刻表来计算准确的出行时间和延误。 支持该功能的算法可从总线位置随时间的顺序中学习。 该数据还与通勤时公交车上的汽车速度结合在一起。 数据还用于捕获特定街道的独特属性。 研究人员还模拟了围绕某个区域弹出查询的可能性,以使该模型更加健壮和准确。
Tinder
作为结识新朋友的全球最受欢迎的应用,Tinder 部署了许多学习模型,以增加喜欢特定个人资料的人数。 智能照片功能增加了用户找到正确匹配项的可能性。 该功能随机排序特定用户的图片并将其显示给其他人。 支持该功能的算法分析了向左或向右滑动图片的频率。 它使用该知识根据图片的受欢迎程度对其重新排序。 随着越来越多的数据收集,该算法的准确率一直在不断提高。
Snapchat
Snapchat 使用的过滤器是在图片和视频的顶部添加的设计叠加层,可以跟踪面部移动。 这些过滤器是通过计算机视觉实现的。 应用使用的算法的第一步是检测图像中存在的面部。 它输出包围检测到的面部的框。 然后,它为检测到的每个脸部标记面部标志(例如眼睛,鼻子和嘴唇)。 这里的输出通常是一个包含x-坐标和y-坐标的二维点。 正确检测到面部和面部特征后,它将使用图像处理功能在整个面部上正确放置或应用过滤器。 该算法使用 Active Shape Model 进一步分析了关键的面部特征。 在通过手动标记关键面部特征的边界进行训练后,该模型将创建与屏幕上出现的面部对齐的平均面部。 该模型将创建一个网格,以正确放置过滤器并跟踪其运动。
现在,我们来看看 DL 领域的研究领域。
探索移动设备上 DL 的最新发展
随着 DL 和 AI 的复杂性与移动应用的结合,正在不断进行软件和硬件优化,以在设备上高效运行模型。 让我们看看其中的一些。
谷歌的 MobileNet
Google 的 MobileNet 于 2017 年推出。它是基于 TensorFlow 的一组移动优先计算机视觉模型,经过精心优化以在受限的移动环境中高效运行。 它充当复杂神经网络结构的准确率与移动运行时性能约束之间的桥梁。 由于这些模型具有在设备本身上本地运行的能力,因此 MobileNet 具有安全性,隐私性和灵活的可访问性的优点。 MobileNet 的两个最重要的目标是在处理计算机视觉模型时减小尺寸并降低复杂性。 MobileNet 的第一个版本提供了低延迟模型,该模型能够在受限资源下正常工作。 它们可用于分类,检测,嵌入和分段,支持各种用例。
于 2018 年发布的 MobileNetV2 是对第一个版本的重大增强。 它可以用于语义分割,对象检测和分类。 作为 TensorFlow-Slim 图像分类库的一部分启动的 MobileNetV2,可以从 Colaboratory 直接访问。 也可以在本地下载,使用 Jupyter 进行浏览,也可以从 TF-Hub 和 GitHub 访问。 添加到架构中的两个最重要的功能是层之间的线性瓶颈和瓶颈之间的快捷连接。 瓶颈对中间的输入和输出进行编码,并且内层支持从较低级别的概念转换为较高级别的描述符的功能。 传统的剩余连接和快捷方式有助于减少训练时间并提高准确率。 与第一个版本相比,MobileNetV2 更快,更准确,并且所需的操作和参数更少。 它非常有效地用于对象检测和分割以提取特征。
阿里巴巴移动神经网络
阿里巴巴移动神经网络(MNN)是开源的轻量级 DL 推理引擎。 阿里巴巴工程副总裁贾阳清说:“与 TensorFlow 和 Caffe2 等通用框架相比,它既涵盖训练又包括推理,MNN 专注于推理的加速和优化,并解决了模型部署过程中的效率问题。 因此可以在移动端更高效地实现模型背后的服务,这实际上与 TensorRT 等服务器端推理引擎中的思想相符在大型机器学习应用中,推理的计算量通常是 10 倍以上,因此,进行推理的优化尤为重要。”
MNN 的主要关注领域是深度神经网络(DNN)模型的运行和推断。 它专注于模型的优化,转换和推断。 MNN 已被成功用于阿里巴巴公司的许多移动应用中,例如 Mobile Tmall,Mobile Taobao,Fliggy,UC,Qianuu 和 Juhuasuan。 它涵盖了搜索推荐,短视频捕获,直播,资产分配,安全风险控制,交互式营销,按图像搜索产品以及许多其他实际场景。 菜鸟呼叫机柜等物联网(IoT)设备也越来越多地使用技术。 MNN 具有很高的稳定性,每天可以运行超过 1 亿次。
MNN 具有高度的通用性,并为市场上大多数流行的框架提供支持,例如 TensorFlow,Caffe 和开放式神经网络交换(ONNX)。 它与卷积神经网络(CNN)和循环神经网络(RNN)等通用神经网络兼容。 MNN 轻巧且针对移动设备进行了高度优化,并且没有依赖关系。 它可以轻松部署到移动设备和各种嵌入式设备。 它还通过便携式操作系统接口(POSIX)支持主要的 Android 和 iOS 移动操作系统以及嵌入式设备。 MNN 不受任何外部库的影响,可提供非常高的性能。 它的核心操作通过大量的手写汇编代码来实现,以充分利用高级 RISC 机器(ARM)CPU 的优势。 借助高效的图像处理模块(IPM),无需 libyuv 或 OpenCV 即可加速仿射变换和色彩空间变换,MNN 易于使用。
在积极开发和研究这些产品的同时,现在让我们看一下将来有望变得越来越重要的一些领域。
探索移动应用中 DL 的当前研究领域
活跃的研究人员社区要投入时间和精力,对于任何研究领域的健康发展至关重要。 幸运的是,DL 在移动设备上的应用引起了全球开发人员和研究人员的强烈关注,许多手机制造商(例如三星,苹果,Realme 和 Xiaomi)将 DL 直接集成到了系统用户界面中 (UI)为所有设备生成。 这极大地提高了模型的运行速度,并且通过系统更新定期提高模型的准确率。
让我们看一下该领域中一些最受欢迎的研究领域,以及它们是如何发展的。
DeepFashion
在 2019 年,DeepFashion2 数据集由葛玉英,张瑞茂等提出。 该数据集是对 DeepFashion 数据集的改进,包括来自卖方和消费者的 491,000 张图像。 数据集可识别 801,000 件服装。 数据集中的每个项目都标有比例,遮挡,放大,视点,类别,样式,边界框,密集的界标和每个像素的蒙版。
数据集在训练集中有 391,000 张图像,在验证集中有 34,000 张图像,在测试集中有 67,000 张图像。 该数据集提供了提出更好的模型的可能性,该模型能够从图像中识别时装和不同的服装。 可以轻松想象此数据集可能会导致的应用范围-包括在线商店根据消费者经常穿的衣服推荐要购买的产品,以及首选品牌和产品的预期价格范围。 仅通过识别他们所穿的服装和品牌,也有可能识别任何人可能从事的职业及其财务,宗教和地理细节。
您可以在此处阅读有关 DeepFashion2 数据集的更多信息。
自我注意生成对抗网络
我们在“第 9 章”,“构建图像超分辨率应用”中讨论了生成对抗网络(GAN)的应用,其中我们从低分辨率图像中生成高分辨率图像。 GAN 在学习模仿艺术和图案方面做得相当不错。 但是,在需要记住更长的序列的情况下,以及在序列的多个部分对于生成生成的输出很重要的情况下,它们无法很好地执行。 因此,我们期待 Ian Goodfellow 及其团队推出的自我注意力 GAN(SAGAN),它们是对图像生成任务应用注意力驱动的远程依赖建模的 GAN 系统。 该系统在 ImageNet 数据集上具有更好的性能,并有望在将来被广泛采用。
Jason Antic 的 DeOldify 项目是使用 SAGANs 完成的工作的衍生产品。 该项目旨在将色彩带入旧的图像和视频中,从而使它们似乎从来没有缺少色彩。 以下屏幕快照显示了 DeOldify 项目的示例:

Dorothea Lange(1936)的《移民母亲》。 图像取自 DeOldify GitHub 存储库。 该项目可通过这里进行测试和演示。 您可以在这个页面上了解有关 SAGAN 的更多信息。
图片动画
Facebook 是一个流行的社交媒体平台,具有用于多个平台的专用应用,一直在致力于创建工具,使您可以使用普通的相机生成 3D 图像,否则这些相机只会生成 2D 图像。 图像动画是一项类似的技术,可让我们将动画带入静态图像。 可以想象这种技术非常令人兴奋的用法,人们拍摄自拍照,然后从运动库中进行选择以对其图像进行动画处理,就好像他们自己在进行这些运动一样。
图像动画虽然还处于起步阶段,但可以成为流行和有趣的应用,考虑到采用 Deepfake 技术的类似应用已成功地成为一项业务-例如,中国的 Zao 应用。
总结
在本章中,我们讨论了一些最流行的移动应用,这些应用因其在业务产品中最前沿地使用 DL 而著称,还讨论了 DL 影响其增长的方式。 我们还讨论了移动应用 DL 领域的最新发展。 最后,我们讨论了该领域的一些令人兴奋的研究领域,以及它们将来如何发展成潜在的流行应用。 我们相信,到目前为止,您将对如何在移动应用上部署 DL 以及如何使用 Flutter 来构建可在所有流行的移动平台上运行的跨平台移动应用有一个很好的了解。
我们在本章结束时希望,您将充分利用本项目系列中介绍的思想和知识,并构建出令人敬畏的东西,从而在此技术领域带来一场革命。
十一、附录
计算机科学领域令人兴奋的是,它允许多个软件组件组合在一起并致力于构建新的东西。 在这个简短的附录中,我们介绍了在移动设备上进行深度学习之前需要设置的工具,软件和在线服务。
在本章中,我们将介绍以下主题:
- 在 Cloud VM 上设置深度学习环境
- 安装 Dart SDK
- 安装 Flutter SDK
- 配置 Firebase
- 设置 Visual Studio(VS)代码
在 Cloud VM 上设置深度学习环境
在本节中,我们将提供有关如何在 Google Cloud Platform(GCP)计算引擎虚拟机(VM)实例以执行深度学习。 您也可以轻松地将此处描述的方法扩展到其他云平台。
我们将以快速指南开始,介绍如何创建您的 GCP 帐户并为其启用结算功能。
创建 GCP 帐户并启用结算
要创建 GCP 帐户,您需要一个 Google 帐户。 如果您有一个以@gmail.com结尾的电子邮件地址或 G Suite 上的帐户,则您已经有一个 Google 帐户。 否则,您可以通过访问这里创建一个 Google 帐户。 登录到 Google 帐户后,请执行以下步骤:
- 在浏览器上访问这里。
- 接受在弹出窗口中显示给您的所有条款。
- 您将能够查看 GCP 控制台信息中心。 您可以通过阅读这个页面上的支持文档来快速使用此仪表板。
- 在左侧导航菜单上,单击“计费”以打开计费管理仪表板。 系统将提示您添加一个计费帐户,如以下屏幕截图所示:

- 点击“添加结算帐户”。 如果有资格,您将被重定向到
GCP Free Trial注册页面。 您可以在这个页面上了解有关免费试用的更多信息。 您应该看到类似于以下屏幕截图的屏幕:

- 根据需要填写表格。 创建完帐单后,请返回 GCP 控制台信息中心。
您已成功创建 GCP 帐户并为其启用了结算功能。 接下来,您将能够在 GCP 控制台中创建一个项目并将资源分配给该项目。 我们将在接下来的部分中对此进行演示。
创建一个项目和 GCP Compute Engine 实例
在本部分中,您将在 GCP 帐户上创建一个项目。 GCP 中的所有资源都封装在项目下。 项目可能属于或不属于组织。 一个组织下可以有多个项目,而一个项目中可能有多个资源。 让我们开始创建项目,如以下步骤所示:
-
在屏幕的左上方,单击“选择项目”下拉菜单。
-
在出现的对话框中,单击对话框右上方的“新建项目”。
-
您将看到新的项目创建表单,如以下屏幕截图所示:

- 填写必要的详细信息后,单击
CREATE完成创建项目。 创建项目后,将带您到项目的仪表板。 在这里,您将能够查看与当前所选项目相关的一些基本日志记录和监视。 您可以在这个页面上了解有关 GCP 资源组织方式的更多信息。 - 在左侧导航窗格中,单击
Compute Engine。 系统将提示您创建一个 VM 实例。 - 点击“创建”以显示 Compute Engine 实例创建表单。 根据需要填写表格。 我们假设您在创建实例时选择了 Ubuntu 18.04 LTS 发行版。
- 确保在防火墙设置中启用对 VM 实例的 HTTP 和 HTTPS 连接的访问,如以下屏幕快照所示:

- 单击“创建”。 GCP 开始为您配置 VM 实例。 您将被带到 VM 实例管理页面。 您应该在此页面上看到您的 VM,如以下屏幕截图所示:

现在,您准备开始配置此 VM 实例以执行深度学习。 我们将在下一部分中对此进行介绍。
配置您的 VM 实例来执行深度学习
在本节中,我们将指导您如何安装包和模块,以在创建的 VM 实例上执行深度学习。 这些包和模块的安装说明在您选择的任何云服务提供商中都是相似的。
您还可以在本地系统上使用类似的命令,以设置本地深度学习环境。
首先调用 VM 的终端:
-
单击 VM 实例页面上的
SSH按钮,以启动到 VM 的终端会话。 -
您应该看到终端会话开始,其中包含一些与系统有关的常规信息以及上次登录的详细信息,如以下屏幕截图所示:

- 现在,让我们对该新创建的实例的包存储库执行更新:
sudo apt update
- 接下来,我们将在此 VM 上安装 Anaconda。 Anaconda 是一个受欢迎的包集合,用于使用 Python 执行深度学习和与数据科学相关的任务。 它带有
conda包管理器打包在一起,这使得管理系统上安装的 Python 包的不同版本非常容易。 要安装它,我们首先需要获取 Anaconda 安装程序下载链接。 前往这里。 您将转到一个页面,为您提供要安装的 Anaconda 版本的选择,如以下屏幕截图所示:

- 建议您选择 Python 3.7 版本。 右键单击“下载”按钮,然后在菜单中找到允许您复制链接地址的选项。
- 切换到您的 VM 实例的终端会话。 使用以下命令将占位符文本粘贴到命令中,从而将其替换为您复制的链接,如下所示:
curl -O <link_you_have_copied>
- 前面的命令会将 Anaconda 安装程序下载到当前用户的主目录中。 要对其进行验证,可以使用
ls命令。 现在,要将此文件设置为可执行文件,我们将使用以下命令:
chmod +x Anaconda*.sh
- 现在,安装程序文件可以由您的系统执行。 要开始执行,请使用以下命令:
./Anaconda*.sh
- 安装应开始。 应该显示一个提示,询问您是否接受 Anaconda 软件的许可协议,如下所示:

- 点击
Enter继续检查许可证。 您会看到许可证文件。 - 点击向下箭头键以阅读协议。 输入
yes接受许可证。 - 系统将要求您确认 Anaconda 安装的位置,如以下屏幕截图所示:

- 点击
Enter确认位置。 包提取和安装将开始。 完成此操作后,系统将询问您是否要初始化 Anaconda 环境。 在此处输入yes,如下所示:

- 现在,安装程序将完成其任务并退出。 要激活 Anaconda 环境,请使用以下命令:
source ~/.bashrc
- 您已经成功安装了 Anaconda 环境并激活了它。 要检查安装是否成功,请在终端中输入以下命令:
python3
如果以下命令的输出在第二行包含单词 Anaconda,Inc.,则表明安装成功。 您可以在以下屏幕截图中看到它:

现在,您可以在此环境上开始运行深度学习脚本。 但是,您将来可能希望向此环境添加更多工具库,例如 PyTorch 或 TensorFlow 或任何其他包。 由于本书假定读者熟悉 Python,因此我们不会详细讨论pip工具。
现在让我们看一下如何在 VM 上安装 TensorFlow。
在 VM 上安装 TensorFlow
TensorFlow 是执行深度学习的绝佳框架。
要安装它,可以使用以下命令:
# TensorFlow 1 with CPU only support
python3 -m pip install tensorflow==1.15
# TensorFlow 1 with GPU support
python3 -m pip install tensorflow-gpu==1.15
# TensorFlow 2 with CPU only support
python3 -m pip install tensorflow
# Tensorflow 2 with GPU support
python3 -m pip install tensorflow-gpu
Python 中另一个经常安装的流行库是自然语言工具包(NLTK)库。 我们将在接下来的部分中演示其安装过程。
在 VM 上安装 NLTK 并下载包
要在 VM 上安装 NLTK 并为其下载数据包,请执行以下步骤:
- 使用
pip安装 NLTK:
python3 -m pip install nltk
- NLTK 有几种不同的数据包。 在大多数情况下,您并不需要全部。 要列出 NLTK 的所有可用数据包,请使用以下命令:
python3 -m nltk.downloader
前面命令的输出将允许您交互式地查看所有可用的包,选择所需的包,然后下载它们。
- 但是,如果您只希望下载一个包,请使用以下命令:
python3 -m nltk.downloader stopwords
前面的命令将下载 NLTK 的stopwords数据包。 在极少数情况下,您可能会发现自己需要或使用 NLTK 中可用的所有数据包。
通过这种设置,您应该能够在云 VM 上运行大多数深度学习脚本。
在下一部分中,我们将研究如何在本地系统上安装 Dart。
安装 Dart SDK
Dart 是 Google 开发的一种面向对象的语言。 它用于移动和 Web 应用开发。 Flutter 是用 Dart 构建的。 Dart 具有即时(JIT)开发周期,该状态与有状态的热重载兼容,并且具有提前编译的功能,可以快速启动并提供可预测的性能,这使其成为了可能。 适用于 Flutter。
以下各节讨论如何在 Windows,macOS 和 Linux 上安装 Dart。
Windows
在 Windows 中安装 Dart 的最简单方法是使用 Chocolatey。 只需在终端中运行以下命令:
C:\> choco install dart-sdk
接下来,我们将研究如何在 Mac 系统上安装 Dart。
MacOS
要在 macOS 上安装 Dart,请执行以下步骤:
- 通过在终端中运行以下命令来安装 Homebrew:
$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
- 运行以下命令以安装 Dart:
$brew tap dart-lang/dart
$brew install dart
接下来,我们将研究如何在 Linux 系统上安装 Dart。
Linux
Dart SDK 可以如下安装在 Linux 中:
- 执行以下一次性设置:
$sudo apt-get update
$sudo apt-get install apt-transport-https
$sudo sh -c 'wget -qO- https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -'
$sudo sh -c 'wget -qO- https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list > /etc/apt/sources.list.d/dart_stable.list'
- 安装稳定版本:
$sudo apt-get update
$sudo apt-get install dart
接下来,我们将研究如何在本地计算机上安装 Flutter SDK。
安装 Flutter SDK
Flutter 是 Google 的一个工具包,用于使用单个代码库构建本地编译的 Android,iOS 和 Web 应用。 Flutter 具有热重载的快速开发,易于构建的表达性 UI 和本机性能等功能,这些都使 Flutter 成为应用开发人员的首选。
以下各节讨论如何在 Windows,macOS 和 Linux 上安装 Flutter SDK。
Windows
以下步骤详细概述了如何在 Windows 上安装 Flutter:
- 从这里下载最新的 Flutter SDK 稳定版本。
- 解压缩 ZIP 文件夹,并导航到要安装 Flutter SDK 的目录,以放置
flutter文件夹。
避免将flutter放在可能需要特殊特权的目录中,例如C:\Program Files\。
- 在“开始”搜索栏中输入
env,然后选择“编辑环境变量”。 - 使用
;作为分隔符,将flutter/bin的完整路径附加到用户变量下的路径。
如果缺少Path条目,只需创建一个新的Path变量并将path设置为flutter/bin作为其值。
- 在终端中运行
flutter doctor。
flutter doctor分析整个 Flutter 的安装,以检查是否需要更多工具才能在计算机上成功运行 Flutter。
接下来,我们将研究如何在 Mac 系统上安装 Flutter。
MacOS
Flutter 可以如下安装在 macOS 上:
- 从这里下载最新的稳定 SDK。
- 将下载的 ZIP 文件夹解压缩到合适的位置,如下所示:
$cd ~/
$unzip ~/Downloads/flutter_macos_v1.9.1+hotfix.6-stable.zip
- 将
flutter工具添加到路径变量:$ export PATH=pwd/flutter/bin:$PATH。 - 打开
bash_profile以永久更新PATH:
$cd ~
$nano .bash_profile
- 将以下行添加到
bash_profile:
$export PATH=$HOME/flutter/bin:$PATH
- 运行
flutter doctor。
Linux
以下步骤概述了如何在 Linux 上安装 Flutter:
- 从这里下载 SDK 的最新稳定版本。
- 将文件提取到合适的位置:
$cd ~/development
$tar xf ~/Downloads/flutter_linux_v1.9.1+hotfix.6-stable.tar.xz
- 将
flutter添加到path变量中:
$export PATH="$PATH:`pwd`/flutter/bin"
- 运行
flutter doctor。
接下来,我们将研究如何配置 Firebase 以提供 ML Kit 和自定义模型。
配置 Firebase
Firebase 提供了可促进应用开发并帮助支持大量用户的工具。 Firebase 可以轻松用于 Android,iOS 和 Web 应用。 Firebase 提供的产品(例如 Cloud Firestore,ML Kit,Cloud Functions,Authentication,Crashlytics,Performance Monitoring,Cloud Messaging 和 Dynamic Links)有助于构建应用,从而在不断发展的业务中提高应用质量。
要集成 Firebase 项目,您需要创建一个 Firebase 项目并将其集成到您的 Android 或 iOS 应用中。 以下各节讨论如何创建 Firebase 项目并将其集成到 Android 和 iOS 项目中。
创建 Firebase 项目
首先,我们需要创建一个 Firebase 项目并将其链接到我们的 Android 和 iOS 项目。 此链接有助于我们利用 Firebase 提供的功能。
要创建 Firebase 项目,请执行以下步骤:
- 通过这里访问 Firebase 控制台。
- 单击“添加项目”以添加新的 Firebase 项目:

- 为您的项目提供一个名称:

- 根据您的要求启用/禁用 Google Analytics(分析)。 通常建议您保持启用状态。
Google Analytics 是一种免费且不受限制的分析解决方案,可在 Firebase Crashlytics,Cloud Messaging,应用内消息传递,远程配置,A/B 测试,预测和 Cloud Functions 中实现目标定位,报告等功能。
- 如果您选择 Firebase Analytics,则还需要选择一个帐户:

在 Firebase 控制台上创建项目后,您将需要分别为 Android 和 iOS 平台进行配置。
配置 Android 项目
以下步骤讨论了如何配置 Android 项目以支持 Firebase:
- 导航到 Firebase 控制台上的应用。 在项目概述页面的中心,单击 Android 图标以启动工作流程设置:

- 添加包名称以在 Firebase 控制台上注册该应用。 此处填写的包名称应与您的应用的包名称匹配。 此处提供的包名称用作标识的唯一密钥:

此外,您可以提供昵称和调试签名证书 SHA-1。
- 下载
google-services.json文件并将其放在app文件夹中:

google-services.json文件存储开发人员凭据和配置设置,并充当 Firebase 项目和 Android 项目之间的桥梁。
- 用于 Gradle 的 Google 服务插件会加载您刚刚下载的
google-services.json文件。 项目级别的build.gradle(<project>/build.gradle)应该进行如下修改,以使用该插件:
buildscript {
repositories {
// Check that you have the following line (if not, add it):
google() // Google's Maven repository
}
dependencies {
...
// Add this line
classpath 'com.google.gms:google-services:4.3.3'
}
}
allprojects {
...
repositories {
// Check that you have the following line (if not, add it):
google() // Google's Maven repository
...
}
}
- 这是应用级别的
build.gradle(<project>/<app-module>build.gradle):
apply plugin: 'com.android.application'
// Add this line
apply plugin: 'com.google.gms.google-services'
dependencies {
// add SDKs for desired Firebase products
// https://firebase.google.com/docs/android/setup#available-libraries
}
现在,您都可以在 Android 项目中使用 Firebase。
配置 iOS 项目
以下步骤演示了如何配置 iOS 项目以支持 Firebase:
- 导航到 Firebase 控制台上的应用。 在项目概述页面的中心,单击 iOS 图标以启动工作流程设置:

- 添加 iOS 捆绑包 ID 名称,以在 Firebase 控制台上注册该应用。 您可以在“常规”选项卡中的捆绑包标识符中找到应用主要目标的 Xcode。 它用作标识的唯一密钥:

此外,您可以提供昵称和 App Store ID。
- 下载
GoogleService-Info.plist文件:

- 将刚刚下载的
GoogleService-Info.plist文件移到 Xcode 项目的根目录中,并将其添加到所有目标中。
Google 服务使用 CocoaPods 来安装和管理依赖项。
- 打开一个终端窗口,然后导航到您的应用的 Xcode 项目的位置。 如果没有,请在此文件夹中创建一个 Podfile:
pod init
- 打开您的 Podfile 并添加以下内容:
# add pods for desired Firebase products # https://firebase.google.com/docs/ios/setup#available-pods
- 保存文件并运行:
pod install
这将为您的应用创建一个.xcworkspace文件。 使用此文件进行应用的所有将来开发。
- 要在应用启动时连接到 Firebase,请将以下初始化代码添加到主
AppDelegate类中:
import UIKit
import Firebase
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions:
[UIApplicationLaunchOptionsKey: Any]?) -> Bool {
FirebaseApp.configure()
return true
}
}
现在,您都可以在 Android 项目中使用 Firebase。
设置 VS 代码
Visual Studio(VS)Code 是由 Microsoft 开发的轻型代码编辑器。 它的简单性和广泛的插件存储库使其成为开发人员的便捷工具。 凭借其 Dart 和 Flutter 插件,以及应用执行和调试支持,Flutter 应用非常易于开发。
在接下来的部分中,我们将演示如何设置 VS Code 以开发 Flutter 应用。 我们将从这里下载最新版本的 VS Code 开始。
安装 Flutter 和 Dart 插件
首先,我们需要在 VS Code 上安装 Flutter 和 Dart 插件。
可以按照以下步骤进行:
- 在计算机上加载 VS Code。
- 导航到“查看 | 命令面板”。
- 开始输入
install,然后选择扩展:安装扩展。 - 在扩展搜索字段中键入
flutter,从列表中选择 Flutter,然后单击安装。 这还将安装所需的 Dart 插件。 - 或者,您可以导航到侧栏来安装和搜索扩展:

成功安装 Flutter 和 Dart 扩展后,我们需要验证设置。 下一节将对此进行描述。
用 Flutter Doctor 验证设置
通常建议您验证设置以确保一切正常。
Flutter 安装可以通过以下方式验证:
- 导航到“查看 | 命令面板”。
- 输入
doctor,然后选择Flutter: Run Flutter Doctor。 - 查看“输出”窗格中的输出。 输出中列出了所有错误或缺少库。
- 另外,您可以在终端上运行
flutter doctor来检查一切是否正常:

上面的屏幕快照显示,尽管 Flutter 很好用,但其他一些相关的配置却丢失了。 在这种情况下,您可能需要安装所有支持软件并重新运行flutter doctor以分析设置。
在 VS Code 上成功设置 Flutter 之后,我们可以继续创建我们的第一个 Flutter 应用。
创建第一个 Flutter 应用
创建第一个 Flutter 应用非常简单。 执行以下步骤:
-
导航到“查看 | 命令面板”。
-
开始输入
flutter,然后选择Flutter: New Project。 -
输入项目名称,例如
my_sample_app。 -
点击
Enter。 -
创建或选择新项目文件夹的父目录。
-
等待项目创建完成,然后显示
main.dart文件。
有关更多详细信息,请参阅这个页面上的文档。
在下一节中,我们将讨论如何运行您的第一个 Flutter 应用。
运行应用
一个新的 Flutter 项目的创建带有一个模板代码,我们可以直接在移动设备上运行它。 创建第一个模板应用后,可以尝试如下运行它:
- 导航至“VS Code”状态栏(即窗口底部的蓝色栏):

- 从设备选择器区域中选择您喜欢的设备:
- 如果没有可用的设备,并且要使用设备模拟器,请单击“无设备”并启动模拟器:

- 您也可以尝试设置用于调试的真实设备。
- 单击设置按钮-位于右上角的齿轮图标齿轮(现已标记为红色或橙色指示器),位于
DEBUG文本框旁边,显示为No Configuration。 选择 Flutter,然后选择调试配置以创建仿真器(如果已关闭)或运行仿真器或已连接的设备。 - 导航到“调试 | 开始调试”或按
F5。 - 等待应用启动,进度会显示在
DEBUG CONSOLE视图中:

应用构建完成后,您应该在设备上看到已初始化的应用:

在下一节中,我们将介绍 Flutter 的热重载功能,该功能有助于快速开发。
尝试热重载
Flutter 提供的快速开发周期使其适合于时间优化的开发。 它支持有状态热重载,这意味着您可以重载正在运行的应用的代码,而不必重新启动或丢失应用状态。 热重装可以描述为一种方法,您可以通过该方法对应用源进行更改,告诉命令行工具您要热重装,并在几秒钟内在设备或仿真器上查看更改。
在 VS Code 中,可以按以下方式执行热重装:
-
打开
lib/main.dart。 -
将
You have pushed the button this many times:字符串更改为You have clicked the button this many times:。 不要停止您的应用。 让您的应用运行。 -
保存更改:调用全部保存,或单击
Hot Reload。

浙公网安备 33010602011771号