Python-深度学习项目-全-
Python 深度学习项目(全)
原文:
annas-archive.org/md5/2f844d9ad75aa257caad1025fa00b786译者:飞龙
序言
你曾经尝试过让计算机做出一些新颖的事情吗?我可以让你编写一个故事,或者看一张图片并告诉我其中的内容。你如何让计算机程序像这样表现,而不是像过去 30 多年我们所使用的数字存储和传输单元呢?
如果你拥有完美的知识和无限的时间,你可以编写出计算机程序所需遵循的所有规则。当然,如果你有足够的知识来定义操作规则,那么你就不需要计算机来做任何事情了!那么,当你需要计算机以复杂的方式运作(进行预测、分类、优化过程、生成内容、响应交互、执行机器人控制)但又没有完全定义所有启发式规则时,你该怎么办呢?
你构建一个基于算法的应用程序,能够从相关领域的数据中学习规则、寻找模式或确定信号。你设定训练过程,使其能够以极快的速度进行迭代,且进行大量循环(我们称之为 epochs),以提供“经验”,从而在一个人类寿命内无法完成的过程中逐步训练模型。
当我们将这些算法架构分层构建时,我们创造了能够学习特征的深度学习模型(例如,狗有尾巴,车有轮子),而这些学习到的特征是非常强大的!我们在《Python 深度学习项目》中真正发现的是,我们能够提出之前无法回答的深刻问题。正是这些问题推动了深度学习技术解决从放射学中的医疗诊断到癌症筛查等问题。深度学习应用推动了聊天机器人体验、人脸识别、自动驾驶、推荐引擎和营销技术的发展。物理学、生物学和化学的硬科学也在将深度学习技能训练纳入其中,正如过去对统计学和显微镜的应用一样。
本书适合人群
如果你已经学习过至少一门机器学习课程,并且具备一定的 Python 使用能力(意味着在示例支持下,你可以用 Python 编写程序),那么本书非常适合你。我们的许多读者将是学习计算机科学、统计学、数学、物理学、生物学、化学、市场营销和商业等专业的本科生。深度学习技术正在应用于这些学位所为你准备的所有职业,而本书是学习这些技能的绝佳方式,这些技能将直接有助于你的成功。研究生们也会很喜欢这本书的教学水平,因为所选的项目直接适用于现代职场,从科技初创公司到企业应用。
Python 深度学习项目 聚焦于数据科学管道的核心——模型构建、训练、评估和验证。由于篇幅限制,数据管道中生产应用所需的额外前期和后期数据工程过程在此无法详细讨论,但我们计划在未来的出版物中进行探讨。
本书内容概览
第一章,构建深度学习环境,在本章中,我们将建立一个通用的工作空间,包含核心技术,如 Ubuntu、Anaconda、Python、TensorFlow、Keras 和 Google Cloud Platform (GCP)。
第二章,使用回归训练神经网络进行预测,在本章中,我们将在 TensorFlow 中构建一个 2 层(最小深度)神经网络,并使用经典的 MNIST 手写数字数据集对其进行训练,应用于餐厅顾客短信通知业务案例。
第三章,使用 word2vec 进行词表示,在本章中,我们将学习并使用 word2vec 将词转换为稠密向量(即张量),为语料库创建嵌入表示,然后构建 卷积神经网络 (CNN) 来为情感分析构建语言模型,应用于文本交换业务案例。
第四章,构建用于聊天机器人的 NLP 流水线,在本章中,我们将创建一个 NLP 流水线,对语料库进行分词、标注词性、通过依赖解析确定词之间的关系,并进行命名实体识别(NER)。使用 TF-IDF 对文档中的特征进行向量化,创建一个简单的 FAQ 类型聊天机器人。通过命名实体识别(NER)和 Rasa NLU 实现来增强此聊天机器人,使其能够理解上下文(意图),提供准确的响应。
第五章,用于构建聊天机器人的序列到序列模型,在本章中,我们将使用 第四章,构建用于聊天机器人的 NLP 流水线,聊天机器人来构建一个更为先进的聊天机器人,结合早期项目中的学习,使用多种技术使其更具上下文感知能力和鲁棒性。我们通过构建 递归神经网络 (RNN) 模型,并结合 长短期记忆 (LSTM) 单元,避免了 CNN 在聊天机器人中存在的一些局限性,LSTM 专门用于捕捉字符或单词序列中表示的信号。
第六章,内容创作的生成性语言模型,在本章中,我们实现一个生成性模型,使用长短期记忆网络(LSTM)、变分自编码器和生成对抗网络(GANs)生成内容。你将有效地实现文本和音乐的模型,能够为艺术家和各种创意企业生成歌曲歌词、剧本和音乐。
第七章,使用 DeepSpeech2 构建语音识别,在本章中,我们将构建并训练一个自动语音识别(ASR)系统,该系统接受音频通话并将其转换为文本,然后可以将文本作为输入用于基于文本的聊天机器人。通过处理语音和声谱图,构建一个端到端的语音识别系统,并使用连接时序分类(CTC)损失函数、批量归一化和 SortaGrad 进行 RNN 训练。本章是《Python 深度学习项目》书中自然语言处理部分的核心章节。
第八章,使用 ConvNets 进行手写数字分类,在本章中,我们将教授卷积神经网络(ConvNets)的基础知识,重点讨论卷积操作、池化和丢弃正则化。这些是你在职业生涯中调节模型时需要掌握的工具。与第二章中使用回归训练神经网络进行预测的早期Python 深度学习项目相比,你将看到部署更复杂、更深层模型在性能上的价值。
第九章,使用 OpenCV 和 TensorFlow 进行目标检测,在本章中,我们将学习如何掌握目标检测和分类,并使用比之前项目更为信息复杂的数据,来产生令人印象深刻的结果。学习使用深度学习包 YOLOv2,并获得该模型架构如何变得更加深入复杂并取得良好效果的经验。
第十章,使用 FaceNet 构建人脸识别,在本章中,我们将使用 FaceNet 构建一个模型,该模型查看一张图片并识别其中的所有可能面孔,然后执行人脸提取以了解图像中人脸部分的质量。对图像中已识别人脸部分进行特征提取,为与另一个数据点(该人的标注面孔图像)进行比较提供基础。这个Python 深度学习项目展示了该技术在从社交媒体到安全等应用中的激动人心的潜力。
第十一章,自动图像描述生成,在本章中,我们将结合到目前为止在Python 深度学习项目中学到的最前沿技术,涵盖计算机视觉和自然语言处理,构建一个完整的图像描述方法。实现这一目标的聪明方法是将编码器-解码器架构中的编码器(RNN 层)替换为一个经过训练的深度卷积神经网络(CNN),该网络能够对图像中的物体进行分类。这个模型能够生成任何提供图像的计算机生成自然语言描述。
第十二章,使用卷积神经网络在 3D 模型中进行姿势估计,在本章中,我们将在 Keras 中成功构建一个深度卷积神经网络/VGG16 模型,应用于电影标签图像(FLIC)。获得实际操作经验,了解如何准备图像以进行建模。成功实施迁移学习,并测试修改后的 VGG16 模型在未见数据上的表现,以确定是否成功。
第十三章,使用 GAN 进行图像翻译与风格迁移,在本章中,你将构建一个神经网络,填补手写数字的缺失部分。重点放在模型的创建——利用 GAN 进行神经修复(生成/重构)手写数字的缺失部分,随后你将重新构建(生成回)缺失的部分,使分类器能够接收到清晰的手写数字,以便转换成数字。
第十四章,使用深度强化学习开发自主智能体,在本章中,我们将构建一个深度强化学习模型,成功玩转 OpenAI Gym 中的 CartPole-v1 游戏。学习并展示在 Gym 工具包中的专业能力,掌握 Q 学习与 SARSA 学习,如何编码强化学习模型并定义超参数,构建训练循环并测试模型。
第十五章,总结与你深度学习职业的下一步,在本章中,你将回顾关键学习内容,总结深度学习项目的直觉,并展望你深度学习职业生涯的下一步。
充分利用本书的内容
我们从一个非常实际的角度来处理深度学习项目。在思考如何分享我们的知识、经验、所学策略和我们采用的战术时,将这本书格式化成这样似乎是自然而然的——你(读者)仿佛是我们“智能工厂”应用 AI 工程团队的一员。
为了从这些项目中获得最大的收获,你至少需要具备基本的 Python 工作知识,并且对深度学习概念有所了解。本书《Python 深度学习项目》主要是一本技术性指导书,内容涉及深度学习的直觉方面,以帮助你学习能产生实际工作的模型代码。本书不涉及深度探讨作为这些技术基础的微积分。
每一章就像是参与 AI 团队的每周站会。当你与这些内容互动时,你将会:
-
看清整体大局
-
这个项目的实际应用案例和目标是什么?
-
成功的影响是什么?
-
我们的战略是什么,如何实现目标?
-
-
集中精力,开始编写代码吧!
-
确定实现项目目标的具体策略
-
为什么这是正确的方法?
-
反复执行这些策略
-
输入或建立背景是什么?
-
代码示例
-
输出和成功标准
-
-
问答环节
-
我们有什么问题?
-
你可能会有哪些问题?
-
-
-
再次回到整体大局
-
让我们确认是否达成了目标
-
从这段经历中我们能获得什么直觉?
-
如何将这一成功经验推广到新的应用场景?
-
解释 Python 深度学习就像 1-2-3 一样简单!但是,讨论深度学习并不等同于实际操作,而这正是本书的主题。接下来将展示一些发人深思且令人兴奋的经历。我们将使用最先进的 Python 库和技术,赋能你(我们最新的应用 AI 工程团队成员),让你能通过本书中创建的项目为你的职业生涯做出贡献。我们很高兴你能参加我们每周的 AI 团队站会。
现在让我们一起学习,享受其中的乐趣,并在这些 Python 深度学习项目中做出出色的工作!
下载示例代码文件
你可以从你的账户在www.packt.com下载本书的示例代码文件。如果你在其他地方购买了本书,可以访问www.packt.com/support,并注册以便直接将文件通过电子邮件发送给你。
你可以按照以下步骤下载代码文件:
-
登录或注册 www.packt.com。
-
选择 SUPPORT 标签。
-
点击代码下载和勘误表。
-
在搜索框中输入书名,并按照屏幕上的指示操作。
一旦文件下载完成,请确保使用最新版本的以下工具解压或提取文件夹:
-
WinRAR/7-Zip for Windows
-
Zipeg/iZip/UnRarX for Mac
-
7-Zip/PeaZip for Linux
本书的代码包也托管在 GitHub 上,地址为https://github.com/PacktPublishing/Python-Deep-Learning-Projects。如果代码有更新,将会在现有的 GitHub 仓库中进行更新。
我们还在我们丰富的书籍和视频目录中提供了其他代码包,您可以在 github.com/PacktPublishing/ 查阅它们!快来看看吧!
下载彩色图像
我们还提供了一个 PDF 文件,其中包含本书中使用的截图/图表的彩色图像。您可以在此下载:www.packtpub.com/sites/default/files/downloads/9781788997096_ColorImages.pdf。
使用的约定
本书中使用了许多文本约定。
CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入以及 Twitter 账号。例如:“一旦您安装了 Docker,您应该能够在终端使用 docker 命令。”
代码块如下所示:
import sys
import dlib
from skimage import io
当我们希望特别引起您对代码块中特定部分的注意时,相关的行或项目会以粗体显示:
# Create a HOG face detector using the built-in dlib class
face_detector = dlib.get_frontal_face_detector()
任何命令行的输入或输出如下所示:
curl https://get.docker.com | sh
警告或重要提示如下所示。
提示和技巧如下所示。
联系我们
我们始终欢迎读者的反馈。
一般反馈:如果您对本书的任何内容有疑问,请在邮件的主题中提及书名,并通过电子邮件联系我们:customercare@packtpub.com。
勘误:虽然我们已经尽力确保内容的准确性,但错误仍然可能发生。如果您在本书中发现了错误,我们将非常感激您向我们报告。请访问 www.packt.com/submit-errata,选择您的书籍,点击“勘误提交表单”链接,并输入详细信息。
盗版:如果您在互联网上发现任何非法复制的我们作品的形式,我们将非常感激您提供该位置或网站名称。请通过 copyright@packt.com 联系我们,并提供该材料的链接。
如果您有兴趣成为作者:如果您在某个领域有专业知识,并且有兴趣撰写或贡献书籍,请访问 authors.packtpub.com。
评论
请留下评论。阅读并使用本书后,您不妨在购买网站上留下评论。潜在读者可以参考您的公正意见来做出购买决定,我们 Packt 可以了解您对我们产品的看法,作者也能看到您对其书籍的反馈。感谢您!
如需了解更多关于 Packt 的信息,请访问 packt.com。
第一章:构建深度学习环境
欢迎加入应用 AI 深度学习团队,欢迎来到我们的第一个项目——构建一个通用深度学习环境!我们对本书中所整理的项目感到兴奋。通用工作环境的基础将帮助我们一起工作,并学习非常酷且强大的深度学习(DL)技术,例如计算机视觉(CV)和自然语言处理(NLP),这些技术将在你作为数据科学家的职业生涯中大有裨益。
本章将涵盖以下主题:
-
构建通用 DL 环境的组件
-
设置本地 DL 环境
-
在云端设置 DL 环境
-
使用云端部署 DL 应用程序
-
自动化设置过程,以减少错误并快速启动
构建通用的 DL 环境
本章的主要目标是标准化工具集,使其能够协同工作,并获得一致的准确结果。
在使用 DL 算法构建可扩展生产应用的过程中,拥有正确的设置是非常重要的,无论是在本地还是在云端,都必须确保端到端的流程能够顺利进行。因此,在本章中,我们将学习如何设置 DL 环境,并将用其来运行所有实验,最终将 AI 模型投入生产。
首先,我们将讨论构建、开发和部署深度学习(DL)模型所需的主要组件,然后介绍各种实现方法,最后查看一些帮助自动化整个过程的代码片段。
以下是构建 DL 应用程序所需的组件列表:
-
Ubuntu 16.04 或更高版本
-
Anaconda 包
-
Python 2.x/3.x
-
TensorFlow/Keras DL 包
-
支持 GPU 的 CUDA
-
Gunicorn 用于大规模部署
集中精力,开始编码吧!
我们将从设置本地 DL 环境开始。你做的大部分工作都可以在本地机器上完成。但在处理大数据集和复杂模型架构时,处理时间会显著变慢。这就是为什么我们还会在云端设置 DL 环境,因为这些复杂且重复的计算处理时间过长,否则就无法高效完成工作。
我们将按照上面的列表逐步进行,到最后(在一点自动化脚本的帮助下),你将完成所有设置!
本地 DL 环境设置
在本书中,我们将使用 Ubuntu 操作系统来运行所有实验,因为 Linux 有很好的社区支持,几乎所有的深度学习应用程序都可以轻松在 Linux 上设置。如果需要关于 Ubuntu 安装和设置的帮助,请参考tutorials.ubuntu.com/上的教程。此外,本书将使用 Python 2.7+ 的 Anaconda 包来编写代码、训练和测试。Anaconda 自带大量预安装的 Python 包,如 numpy、pandas、sklearn 等,这些包在各种数据科学项目中都非常常用。
为什么我们需要 Anaconda?难道不能使用原生 Python 吗?
Anaconda 是一个通用的捆绑包,包含 iPython Notebook、编辑器以及许多预安装的 Python 库,可以节省大量设置时间。使用 Anaconda,我们可以快速开始解决数据科学问题,而不是花时间配置环境。
但是,当然可以使用默认的 Python——完全取决于读者的选择,我们将在本章末尾学习如何通过脚本配置 python env。
下载并安装 Anaconda
Anaconda 是一个非常流行的数据科学平台,供使用 Python 构建机器学习和深度学习模型以及可部署应用程序的人们使用。Anaconda 市场团队在他们的 什么是 Anaconda? 页面上总结得最好,页面链接:www.anaconda.com/what-is-anaconda/。要安装 Anaconda,请执行以下步骤:
-
点击菜单中的 Anaconda,然后点击 Downloads 进入下载页面:
www.anaconda.com/download/#linux -
选择适合你平台的下载版本(Linux、OS X 或 Windows):
-
选择 Python 3.6 版本*
-
选择图形化安装程序
-
-
按照向导中的说明操作,10 到 20 分钟后,你的 Anaconda 环境(Python)将设置完成。
安装过程完成后,你可以使用以下命令检查终端中的 Python 版本:
python -V
你应该能看到以下输出:
Python 3.6 :: Anaconda,Inc.
如果命令不起作用,或者返回错误,请查阅你平台的文档寻求帮助。
安装深度学习库
现在,让我们安装用于深度学习的 Python 库,具体来说是 TensorFlow 和 Keras。
什么是 TensorFlow?
TensorFlow 是由 Google 开发和维护的 Python 库。你可以使用 TensorFlow 在自定义模型和应用程序中实现许多强大的机器学习和深度学习架构。想了解更多,请访问:www.tensorflow.org/
通过输入以下命令安装 TensorFlow 深度学习库(适用于所有操作系统,除了 Windows):
conda install -c conda-forge tensorflow
另外,你也可以选择使用 pip 安装,并根据你的平台安装指定版本的 TensorFlow,使用以下命令:
pip install tensorflow==1.6
你可以在www.tensorflow.org/get_started/os_setup#anaconda_installation找到 TensorFlow 的安装说明。
现在我们将使用以下命令安装keras:
pip install keras
为了验证环境和包的版本,让我们编写以下脚本,该脚本将打印出每个库的版本号:
# Import the tensorflow library
import tensorflow
# Import the keras library
import keras
print('tensorflow: %s' % tensorflow.__version__)
print('keras: %s' % keras.__version__)
将脚本保存为dl_versions.py。通过输入以下命令来运行脚本:
python dl_version.py
你应该看到以下输出:
tensorflow: 1.6.0
Using TensorFlow backend.
keras: 2.1.5
看!现在我们的 Python 开发环境已经准备好,可以开始在本地编写一些超棒的深度学习应用程序了。
在云端设置深度学习环境
到目前为止我们所执行的所有步骤在云端也同样适用,但为了使你的深度学习应用程序可服务和可扩展,还需要一些额外的模块来配置云虚拟机。因此,在设置服务器之前,请按照前一节的指示操作。
要在云端部署深度学习应用程序,你需要一台足够强大的服务器,能够同时训练模型并提供服务。随着深度学习领域的巨大进展,对云服务器的需求也急剧增加,市场上的选择也随之增多。以下是一些最佳选择:
-
Paperspace (
www.paperspace.com/) -
FloydHub (
www.floydhub.com) -
Amazon Web Services (
aws.amazon.com/) -
Google Cloud Platform (
cloud.google.com/) -
DigitalOcean (
cloud.digitalocean.com/)
这些选项各有优缺点,最终的选择完全取决于你的使用场景和偏好,因此可以随意探索更多内容。本书中,我们将主要在Google Compute Engine(GCE)上构建和部署模型,而 GCE 是Google Cloud Platform(GCP)的一部分。按照本章中提到的步骤启动虚拟机服务器并开始操作。
Google 发布了一个内部笔记本平台,Google Colab (colab.research.google.com/),该平台预装了所有深度学习包和其他 Python 库。你可以在 Google Cloud 上编写所有机器学习/深度学习应用程序,并免费使用 GPU 进行 10 小时的计算。
云平台部署
本书的主要目的是帮助你构建和部署深度学习应用程序。在本节中,我们将讨论一些关键组件,这些组件是让你的应用程序可以服务成千上万的用户所必需的。
使应用程序可访问的最佳方式是将其作为 Web 服务暴露,使用 REST 或 SOAP API。为此,我们有许多 Python Web 框架可供选择,例如web.py、Flask、Bottle 等。这些框架使我们能够轻松构建 Web 服务并进行部署。
先决条件
你应该有一个 Google Cloud (cloud.google.com/) 账户。Google 目前正在推广其平台,并提供 300 美元的信用和 12 个月的免费使用期。
设置 GCP
按照以下步骤设置你的 GCP:
- 创建新项目:点击如下面截图所示的三个点,然后点击加号来创建一个新项目:

-
启动虚拟机实例:点击屏幕左上角的三条线,选择计算选项,然后点击计算引擎。接着选择创建新实例。为虚拟机实例命名,并选择你的区域为 us-west2b。选择机器类型的大小。
选择你的启动磁盘为 Ubuntu 16.04 LTS。在防火墙选项中,选择 HTTP 和 HTTPS 选项(确保它可以从外部访问)。若要选择 GPU 选项,可以点击自定义按钮,并找到 GPU 选项。你可以选择两种 NVIDIA GPU。勾选“允许 HTTP 流量”和“允许 HTTPS 流量”。
现在点击“创建”。砰!你的新虚拟机正在准备中。
-
修改防火墙设置:现在点击“网络”下的防火墙规则设置。在协议和端口下,我们需要选择一个端口来导出我们的 API。我们选择了
tcp:8080作为我们的端口号。点击保存按钮。这将在虚拟机的防火墙中分配一个规则,以便从外部世界访问应用程序。 -
启动虚拟机:现在启动你的虚拟机实例。当你看到绿色的对勾时,点击 SSH—这将打开一个命令窗口,你现在就进入了虚拟机。你也可以使用
gcloud cli登录并访问你的虚拟机。 -
然后按照我们设置本地环境时的相同步骤操作,或者继续阅读,了解如何创建一个自动化脚本来自动执行所有设置。
现在我们需要一个 Web 框架来将我们的 DL 应用程序写为 Web 服务——虽然有很多选择,但为了简化,我们将使用 web.py 和 Gunicorn 的组合。
如果你想知道如何根据内存消耗、CPU 使用率等因素选择适合的 Web 框架,可以查看 klen.github.io/py-frameworks-bench 上的综合基准列表。
让我们使用以下命令来安装它们:
pip install web.py
pip install gunicorn
现在我们准备将我们的 DL 解决方案部署为 Web 服务,并将其扩展到生产级别。
自动化设置过程
安装 Python 包和 DL 库可能是一个繁琐的过程,需要大量的时间和重复的工作。因此,为了简化这一过程,我们将创建一个 bash 脚本,使用单个命令即可安装所有内容。
以下是将安装和配置的组件列表:
-
Java 8
-
Bazel 用于构建
-
Python 及其相关依赖
-
TensorFlow
-
Keras
-
Git
-
解压
-
上述所有服务的依赖项(请查看脚本以获取详细信息)
你可以简单地将自动化脚本下载到服务器或本地,执行它,操作完成。以下是需要遵循的步骤:
- 通过从仓库中克隆代码,将脚本保存到你的主目录:
git clone https://github.com/PacktPublishing/Python-Deep-Learning-Projects
- 一旦你拥有完整仓库的副本,进入
Chapter01文件夹,其中将包含一个名为setupDeepLearning.sh的脚本文件。我们将执行这个脚本来启动设置过程,但在执行之前,我们需要使用chmod命令使其具有可执行权限:
cd Python-Deep-Learning-Projects/Chapter01/
chmod +x setupDeepLearning.sh
- 完成此操作后,我们准备按照以下步骤执行它:
./setupDeepLearning.sh
按照出现的任何指示进行操作(基本上,对所有内容选择yes并接受 Java 的许可)。安装所有内容大约需要 10 到 15 分钟。完成后,你将看到正在安装的 Python 包列表,如下截图所示:

列出的包包括 TensorFlow 和其他 Python 依赖项
还有其他几个选项,例如从 TensorFlow 和其他深度学习(DL)包获取 Docker 镜像,这可以为大规模和生产环境设置功能齐全的 DL 机器。你可以在www.docker.com/what-docker了解更多有关 Docker 的信息。此外,想要快速入门的用户,可以参考这个仓库中的指引,获取一个全功能的 DL Docker 镜像:github.com/floydhub/dl-docker。
总结
在本章中,我们的工作是让团队在一个共同的环境中设置好标准化的工具集。我们计划通过利用 Gunicorn 和 CUDA 来部署我们的项目应用。这些项目将依赖于高度先进且高效的深度学习库,例如在 Python 2.x/3.x 中运行的 TensorFlow 和 Keras。我们将使用 Anaconda 包中的资源来编写代码,所有这些都将在 Ubuntu 16.04 或更高版本上运行。
现在,我们已准备好执行实验并将我们的深度学习模型部署到生产环境中!
第二章:使用回归训练神经网络进行预测
欢迎来到我们的第一个 Python 深度学习项目!今天我们要做的是构建一个分类器,解决从图像数据集中识别特定手写样本的问题。在这个假设的使用案例中,一家餐饮连锁店要求我们做到这一点,他们需要准确地将手写数字分类成数字。他们让顾客在一个简单的 iPad 应用程序中写下自己的电话号码。当顾客可以入座时,他们会收到一条短信,提示他们前往餐厅接待员处。我们需要准确分类手写数字,以便应用程序的输出能够精确预测电话号码中的各个数字标签。然后,这些信息可以发送到他们(假设的)自动拨号服务中进行短信发送,从而将通知送到正确的饥饿顾客手中!
定义成功:一个好的做法是在项目开始时定义成功的标准。我们应该用什么指标来衡量这个项目的成功?我们将使用全球准确度测试作为一个百分比来衡量我们在这个项目中的表现。
数据科学在分类问题上的方法可以以多种方式进行配置。事实上,在本书后续部分,我们将研究如何通过卷积神经网络提高图像分类的准确度。
迁移学习:这意味着在一个不同(但非常相似)的数据集上预训练深度学习模型,以加快在另一个(通常较小)数据集上的学习速度和准确度。在这个项目和我们假设的使用案例中,通过在 MNIST 数据集上对深度学习多层感知器(MLP)进行预训练,可以使我们在不需要长时间收集数据样本的情况下,部署一个用于手写识别的生产系统,而不是在一个实际但不具备功能的系统中长时间进行数据收集。Python 深度学习项目非常酷!
让我们从基准深度神经网络模型架构开始。我们将牢固地建立我们的直觉和技能,为学习更复杂的架构做好准备,进而解决我们在本书项目中遇到的更广泛问题。
本章将学习以下内容:
-
什么是 MLP?
-
探索一个常见的开源手写数据集——MNIST 数据集
-
建立我们的直觉和为模型架构做准备
-
编写模型代码并定义超参数
-
构建训练循环
-
测试模型
使用 MLP 深度神经网络构建回归模型进行预测
在任何真实的人工智能团队工作中,其中一个主要目标是构建能够在非线性数据集上进行预测的回归模型。由于现实世界的复杂性以及你将处理的数据,简单的线性回归模型无法提供你所寻求的预测能力。这就是为什么在本章中,我们将讨论如何使用 MLP 构建世界级的预测模型。更多信息可以在www.deeplearningbook.org/contents/mlp.html中找到,下面展示了一个 MLP 架构的示例:

一个具有两层隐藏层的多层感知器(MLP)
我们将使用 TensorFlow 实现一个简单架构的神经网络,只有两层,该网络将在我们提供的 MNIST 数据集(yann.lecun.com/exdb/mnist/)上进行回归。我们可以(并且会)在后续的项目中深入探讨架构!我们假设你已经熟悉反向传播(如果没有,请阅读 Michal Nielsen 关于反向传播的文章,链接为neuralnetworksanddeeplearning.com/chap2.html)。我们不会花太多时间讲解 TensorFlow 的工作原理,但如果你有兴趣深入了解这项技术的底层实现,可以参考官方教程,网址为www.tensorflow.org/versions/r0.10/get_started/basic_usage.html。
探索 MNIST 数据集
在我们开始构建我们令人惊叹的神经网络之前,让我们先来看一下著名的 MNIST 数据集。所以让我们在本节中可视化 MNIST 数据集。
智慧之言:你必须了解你的数据以及它是如何预处理的,这样你才能知道为什么你所构建的模型会有那样的表现。本节回顾了数据集准备过程中所做的重要工作,这使得我们当前构建 MLP 的任务更加轻松。永远记住:数据科学始于数据!
因此,让我们通过以下命令开始下载数据:
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("/tmp/data/", one_hot=True)
如果我们检查mnist变量的内容,我们可以看到它以特定格式构建,包含三个主要部分——TRAIN(训练集)、TEST(测试集)和VALIDATION(验证集)。每个集合都有手写图像及其相应的标签。图像以扁平化的方式存储为单个向量:

MNIST 数据集的格式
让我们从数据集中提取一张图像并绘制它。由于单张图像矩阵的存储形状为[1,784],我们需要将这些向量重塑为[28,28]以可视化原始图像:
sample_image = mnist.train.images[0].reshape([28,28])
一旦我们拥有了图像矩阵,我们将使用matplotlib来绘制它,如下所示:
import matplotlib.pyplot as plt
plt.gray()
plt.imshow(sample_image)
输出将如下所示:

MNIST 数据集的一个示例
与这张图类似,共有 55,000 张类似的手写数字[0-9]图像。MNIST 数据集中的标签是图像中数字的真实值。因此,我们的目标是通过这组图像和标签训练一个模型,使其能够预测任何来自 MNIST 数据集的图像的标签。
成为深度学习探索者:如果你有兴趣操作数据集,你可以尝试使用 Colab Notebook,链接地址为 drive.google.com/file/d/1-GVlob72EyiJyQpk8EL2fg2mvzaEayJ_/view?usp=sharing。
直觉与准备
让我们围绕这个项目建立直觉。我们需要做的是构建一个深度学习技术,它能准确地为输入图像分配类别标签。我们使用的深度神经网络被称为 MLP。这个技术的核心是回归的数学原理。具体的微积分证明超出了本书的范围,但在这一部分,我们为你提供了一个理解的基础。我们还概述了项目的结构,以便你能轻松理解创建我们想要的结果所需的主要步骤。
定义回归
我们的第一个任务是定义一个将在提供的 MNIST 数据集上执行回归的模型。因此,我们将创建一个具有两个隐藏层的 TensorFlow 模型,作为一个完全连接的神经网络的一部分。你可能会听到它被称为 MLP。
该模型将执行以下方程的操作,其中 y 是标签,x 是图像,W 是模型将学习的权重,b 是偏置,模型也会学习它,以下是该模型的回归方程:

模型的回归方程
监督学习:当你有数据和准确的标签用于训练集(也就是说,你知道答案)时,你处于一个监督式深度学习的范式中。模型训练是一个数学过程,通过这个过程,数据的特征被学习并与正确的标签关联,以便当一个新的数据点(测试数据)被呈现时,能够输出准确的类别标签。换句话说,当你提供一个新的数据点并且没有标签(也就是说,你不知道答案)时,你的模型可以通过高度可靠的类别预测为你生成标签。
每次迭代将尝试概括权重和偏置的值,并减少误差率。同时,请记住,我们需要确保模型不会发生过拟合,这可能导致对未见数据集的错误预测。我们将向你展示如何编写代码并可视化进度,以帮助你理解模型性能。
定义项目结构
让我们按照以下模式组织我们的项目:
-
hy_param.py:所有的超参数和其他配置都在这里定义 -
model.py:模型的定义和架构在这里进行定义 -
train.py:用于训练模型的代码在这里编写 -
inference.py:执行训练好的模型并进行预测的代码在这里定义 -
/runs:此文件夹将存储在训练过程中创建的所有检查点
你可以从代码库克隆代码——代码可以在Chapter02文件夹中找到,链接地址是github.com/PacktPublishing/Python-Deep-Learning-Projects/。
让我们开始编写实现代码吧!
为了实现代码,我们首先会定义超参数,然后定义模型,接着构建并执行训练循环。最后,我们检查模型是否过拟合,并编写推理代码,加载最新的检查点,然后基于学习到的参数进行预测。
定义超参数
我们将在hy_param.py文件中定义所有需要的超参数,并在其他代码中导入它作为模块。这使得部署变得更加方便,并且将代码模块化是一个良好的实践。让我们看看在hy_param.py文件中定义的超参数配置:
#!/usr/bin/env python2
# Hyperparameters and all other kind of params
# Parameters
learning_rate = 0.01
num_steps = 100
batch_size = 128
display_step = 1
# Network Parameters
n_hidden_1 = 300 # 1st layer number of neurons
n_hidden_2 = 300 # 2nd layer number of neurons
num_input = 784 # MNIST data input (img shape: 28*28)
num_classes = 10 # MNIST total classes (0-9 digits)
#Training Parameters
checkpoint_every = 100
checkpoint_dir = './runs/'
我们将在整个代码中使用这些值,并且它们是完全可配置的。
作为一个 Python 深度学习项目的探索机会,我们邀请你,作为我们的项目团队成员和读者,尝试不同的学习率和隐藏层数量,进行实验并构建更好的模型!
由于前面展示的图像的平坦向量大小为[1 x 786],因此num_input=784在这种情况下是固定的。此外,MNIST 数据集中的类别数为10。我们有 0 到 9 的数字,因此显然我们有num_classes=10。
模型定义
首先,我们将加载 Python 模块;在这种情况下,加载 TensorFlow 包以及我们之前定义的超参数:
import tensorflow as tf
import hy_param
然后,我们定义将用于输入数据到模型中的占位符。tf.placeholder允许我们将输入数据传递给计算图。我们可以定义占位符的形状约束,以便仅接受特定形状的张量。请注意,通常会为第一个维度提供None,这允许我们在运行时动态指定批次大小。
精通你的技艺:批处理大小往往对深度学习模型的性能有很大影响。在这个项目中,尝试不同的批处理大小。结果会有什么变化?你的直觉是什么?批处理大小是你数据科学工具箱中的另一个工具!
我们还给占位符分配了名称,以便在构建推理代码时可以使用它们:
X = tf.placeholder("float", [None, hy_param.num_input],name="input_x")
Y = tf.placeholder("float", [None, hy_param.num_classes],name="input_y")
现在,我们将定义保存权重和偏置值的变量。tf.Variable允许我们在图中存储并更新张量。为了使用正态分布的随机值初始化变量,我们将使用tf.random_normal()(更多细节可以在www.tensorflow.org/api_docs/python/tf/random_normal查看)。这里需要注意的重点是层之间的映射变量大小:
weights = {
'h1': tf.Variable(tf.random_normal([hy_param.num_input, hy_param.n_hidden_1])),
'h2': tf.Variable(tf.random_normal([hy_param.n_hidden_1, hy_param.n_hidden_2])),
'out': tf.Variable(tf.random_normal([hy_param.n_hidden_2, hy_param.num_classes]))
}
biases = {
'b1': tf.Variable(tf.random_normal([hy_param.n_hidden_1])),
'b2': tf.Variable(tf.random_normal([hy_param.n_hidden_2])),
'out': tf.Variable(tf.random_normal([hy_param.num_classes]))
}
现在,让我们设置之前在本章中定义的操作。这是逻辑回归操作:
layer_1 = tf.add(tf.matmul(X, weights['h1']), biases['b1'])
layer_2 = tf.add(tf.matmul(layer_1, weights['h2']), biases['b2'])
logits = tf.matmul(layer_2, weights['out']) + biases['out']
使用tf.nn.softmax()将逻辑值转换为概率值。Softmax 激活将每个单元的输出值压缩到 0 和 1 之间:
prediction = tf.nn.softmax(logits, name='prediction')
接下来,我们使用tf.nn.softmax_cross_entropy_with_logits来定义我们的成本函数。我们将使用 Adam 优化器优化性能。最后,我们可以使用内置的minimize()函数来计算每个网络参数的随机梯度下降(SGD)更新规则:
loss_op = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=logits, labels=Y))
optimizer = tf.train.AdamOptimizer(learning_rate=hy_param.learning_rate)
train_op = optimizer.minimize(loss_op)
接下来,我们进行预测。为了计算并捕获批次中的准确度值,以下函数是必需的:
correct_pred = tf.equal(tf.argmax(prediction, 1), tf.argmax(Y, 1))
accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32) ,name='accuracy')
完整的代码如下:
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
import tensorflow as tf
import hy_param
## Defining Placeholders which will be used as inputs for the model
X = tf.placeholder("float", [None, hy_param.num_input],name="input_x")
Y = tf.placeholder("float", [None, hy_param.num_classes],name="input_y")
# Defining variables for weights & bias
weights = {
'h1': tf.Variable(tf.random_normal([hy_param.num_input, hy_param.n_hidden_1])),
'h2': tf.Variable(tf.random_normal([hy_param.n_hidden_1, hy_param.n_hidden_2])),
'out': tf.Variable(tf.random_normal([hy_param.n_hidden_2, hy_param.num_classes]))
}
biases = {
'b1': tf.Variable(tf.random_normal([hy_param.n_hidden_1])),
'b2': tf.Variable(tf.random_normal([hy_param.n_hidden_2])),
'out': tf.Variable(tf.random_normal([hy_param.num_classes]))
}
# Hidden fully connected layer 1 with 300 neurons
layer_1 = tf.add(tf.matmul(X, weights['h1']), biases['b1'])
# Hidden fully connected layer 2 with 300 neurons
layer_2 = tf.add(tf.matmul(layer_1, weights['h2']), biases['b2'])
# Output fully connected layer with a neuron for each class
logits = tf.matmul(layer_2, weights['out']) + biases['out']
# Performing softmax operation
prediction = tf.nn.softmax(logits, name='prediction')
# Define loss and optimizer
loss_op = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(
logits=logits, labels=Y))
optimizer = tf.train.AdamOptimizer(learning_rate=hy_param.learning_rate)
train_op = optimizer.minimize(loss_op)
# Evaluate model
correct_pred = tf.equal(tf.argmax(prediction, 1), tf.argmax(Y, 1))
accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32) ,name='accuracy')
太棒了!代码的繁重部分已经完成。我们将模型代码保存在model.py文件中。到目前为止,我们已经定义了一个简单的两层隐藏层模型架构,每层有 300 个神经元,使用 Adam 优化器来学习最佳权重分布,并预测十个类别的概率。以下是这些层的示意图:

我们创建的模型示意图
构建训练循环
下一步是利用模型进行训练,并记录已学习的模型参数,我们将在train.py中完成此任务。
让我们首先导入所需的依赖项:
import tensorflow as tf
import hy_param
# MLP Model which we defined in previous step
import model
然后,我们定义需要输入到 MLP 中的变量:
# This will feed the raw images
X = model.X
# This will feed the labels associated with the image
Y = model.Y
让我们创建一个文件夹,用于保存我们的检查点。检查点基本上是学习过程中捕获W和b值的中间步骤。然后,我们将使用tf.train.Saver()函数(更多详情请见www.tensorflow.org/api_docs/python/tf/train/Saver)来保存和恢复checkpoints:
checkpoint_dir = os.path.abspath(os.path.join(hy_param.checkpoint_dir, "checkpoints"))
checkpoint_prefix = os.path.join(checkpoint_dir, "model")
if not os.path.exists(checkpoint_dir):
os.makedirs(checkpoint_dir)
# We only keep the last 2 checkpoints to manage storage
saver = tf.train.Saver(tf.global_variables(), max_to_keep=2)
为了开始训练,我们需要在 TensorFlow 中创建一个新的会话。在此会话中,我们将初始化图中的变量,并将有效数据提供给模型操作:
# Initialize the variables
init = tf.global_variables_initializer()
# Start training
with tf.Session() as sess:
# Run the initializer
sess.run(init)
for step in range(1, hy_param.num_steps+1):
# Extracting
batch_x, batch_y = mnist.train.next_batch(hy_param.batch_size)
# Run optimization op (backprop)
sess.run(model.train_op, feed_dict={X: batch_x, Y: batch_y})
if step % hy_param.display_step == 0 or step == 1:
# Calculate batch loss and accuracy
loss, acc = sess.run([model.loss_op, model.accuracy], feed_dict={X: batch_x,
Y: batch_y})
print("Step " + str(step) + ", Minibatch Loss= " + \
"{:.4f}".format(loss) + ", Training Accuracy= " + \
"{:.3f}".format(acc))
if step % hy_param.checkpoint_every == 0:
path = saver.save(
sess, checkpoint_prefix, global_step=step)
print("Saved model checkpoint to {}\n".format(path))
print("Optimization Finished!")
我们将从 MNIST 数据集中提取 128 个训练图像-标签对,并将其输入到模型中。经过后续步骤或多个 epoch 后,我们将使用saver操作存储检查点:
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from __future__ import print_function
# Import MNIST data
import os
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("/tmp/data/", one_hot=True)
import tensorflow as tf
import model
import hy_param
## tf Graph input
X = model.X
Y = model.Y
checkpoint_dir = os.path.abspath(os.path.join(hy_param.checkpoint_dir, "checkpoints"))
checkpoint_prefix = os.path.join(checkpoint_dir, "model")
if not os.path.exists(checkpoint_dir):
os.makedirs(checkpoint_dir)
saver = tf.train.Saver(tf.global_variables(), max_to_keep=2)
#loss = tf.Variable(0.0)
# Initialize the variables
init = tf.global_variables_initializer()
all_loss = []
# Start training
with tf.Session() as sess:
writer_1 = tf.summary.FileWriter("./runs/summary/",sess.graph)
sum_var = tf.summary.scalar("loss", model.accuracy)
write_op = tf.summary.merge_all()
# Run the initializer
sess.run(init)
for step in range(1, hy_param.num_steps+1):
# Extracting
batch_x, batch_y = mnist.train.next_batch(hy_param.batch_size)
# Run optimization op (backprop)
sess.run(model.train_op, feed_dict={X: batch_x, Y: batch_y})
if step % hy_param.display_step == 0 or step == 1:
# Calculate batch loss and accuracy
loss, acc, summary = sess.run([model.loss_op, model.accuracy, write_op], feed_dict={X: batch_x,
Y: batch_y})
all_loss.append(loss)
writer_1.add_summary(summary, step)
print("Step " + str(step) + ", Minibatch Loss= " + \
"{:.4f}".format(loss) + ", Training Accuracy= " + \
"{:.3f}".format(acc))
if step % hy_param.checkpoint_every == 0:
path = saver.save(
sess, checkpoint_prefix, global_step=step)
# print("Saved model checkpoint to {}\n".format(path))
print("Optimization Finished!")
# Calculate accuracy for MNIST test images
print("Testing Accuracy:", \
sess.run(model.accuracy, feed_dict={X: mnist.test.images,
Y: mnist.test.labels}))
一旦我们执行了 train.py 文件,你将在控制台上看到进度,如前面的截图所示。这显示了每一步损失的减少,以及准确率的提升:

训练时期的输出,包括小批量损失和训练准确率参数
此外,你还可以在下面的图表中看到小批量损失的变化,随着每一步的推进,它逐渐接近最小值:

绘制每一步计算出的损失值
可视化模型的表现非常重要,这样你就可以分析并防止模型出现欠拟合或过拟合。过拟合是在处理深度模型时非常常见的情况。让我们花些时间详细了解它们,并学习一些克服它们的技巧。
过拟合和欠拟合
强大的能力带来巨大的责任,而更深的模型也带来了更深的问题。深度学习的一个基本挑战是找到泛化和优化之间的正确平衡。在深度学习过程中,我们正在调整超参数,并经常不断配置和调整模型,以便基于我们用于训练的数据生成最佳结果。这就是 优化。关键问题是,我们的模型在对未见过的数据进行预测时,泛化能力如何?
作为专业的深度学习工程师,我们的目标是构建具有良好实际世界泛化能力的模型。然而,泛化能力受到模型架构和训练数据集的影响。我们通过减少模型学习到与目标无关的模式或仅仅是数据中相似的简单模式的可能性来引导模型达到最大的实用性。如果没有做到这一点,它可能会影响泛化过程。一个好的解决方案是通过获取更多的数据来训练模型,并优化模型架构,提供模型可能具有更好(即更完整且通常更复杂)信号的更多信息,这有助于你真正想要建模的内容。以下是一些防止过拟合的快速技巧,可以提高你的模型:
-
获取更多用于训练的数据
-
通过调整层数或节点数来减少网络容量
-
使用 L2(并尝试 L1)权重正则化技术
-
在模型中添加丢弃层或池化层
L1 正则化,其中附加的代价与权重系数的绝对值成正比,也称为 L1 范数。L2 正则化,其中附加的代价与权重系数值的平方成正比,也称为 L2 范数 或 权重衰减。
当模型完全训练完毕后,其输出会作为检查点被存入 /runs 文件夹,其中包含 checkpoints 的二进制转储,如下面的截图所示:

训练过程完成后的检查点文件夹
构建推理
现在,我们将创建一个推理代码,加载最新的检查点,然后根据已学习的参数进行预测。为此,我们需要创建一个saver操作,提取最新的检查点并加载元数据。元数据包含有关我们在图表中创建的变量和节点的信息:
# Pointing the model checkpoint
checkpoint_file = tf.train.latest_checkpoint(os.path.join(hy_param.checkpoint_dir, 'checkpoints'))
saver = tf.train.import_meta_graph("{}.meta".format(checkpoint_file))
我们知道这一点的重要性,因为我们希望从存储的检查点加载类似的变量和操作。我们通过使用tf.get_default_graph().get_operation_by_name()将它们加载到内存中,通过传递在模型中定义的操作名称作为参数:
# Load the input variable from the model
input_x = tf.get_default_graph().get_operation_by_name("input_x").outputs[0]
# Load the Prediction operation
prediction = tf.get_default_graph().get_operation_by_name("prediction").outputs[0]
现在,我们需要初始化会话,并将测试图像的数据传递给执行预测操作的代码,如下所示:
# Load the test data
test_data = np.array([mnist.test.images[0]])
with tf.Session() as sess:
# Restore the model from the checkpoint
saver.restore(sess, checkpoint_file)
# Execute the model to make predictions
data = sess.run(prediction, feed_dict={input_x: test_data })
print("Predicted digit: ", data.argmax() )
以下是完整代码:
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from __future__ import print_function
import os
import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt
import hy_param
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("/tmp/data/", one_hot=True)
# Pointing the model checkpoint
checkpoint_file = tf.train.latest_checkpoint(os.path.join(hy_param.checkpoint_dir, 'checkpoints'))
saver = tf.train.import_meta_graph("{}.meta".format(checkpoint_file))
# Loading test data
test_data = np.array([mnist.test.images[6]])
# Loading input variable from the model
input_x = tf.get_default_graph().get_operation_by_name("input_x").outputs[0]
# Loading Prediction operation
prediction = tf.get_default_graph().get_operation_by_name("prediction").outputs[0]
with tf.Session() as sess:
# Restoring the model from the checkpoint
saver.restore(sess, checkpoint_file)
# Executing the model to make predictions
data = sess.run(prediction, feed_dict={input_x: test_data })
print("Predicted digit: ", data.argmax() )
# Display the feed image
print ("Input image:")
plt.gray()
plt.imshow(test_data.reshape([28,28]))
这样,我们就完成了第一个项目:预测手写图像中的数字!以下是模型在接收来自 MNIST 数据集的测试图像时所做的一些预测结果:

模型的输出,展示了模型的预测和输入图像
项目总结
今天的项目是构建一个分类器,解决从图像数据集中识别特定手写样本的问题。我们的假设使用场景是应用深度学习技术,让餐厅连锁的顾客可以在简单的 iPad 应用中写下他们的电话号码,从而收到通知,告知他们的桌位已准备好。我们的具体任务是构建能驱动这个应用的智能系统。
重新审视我们的成功标准:我们做得怎么样?我们成功了吗?我们的成功有什么影响?正如我们在项目开始时定义的那样,这些是我们作为深度学习数据科学家需要问的关键问题,尤其是当我们准备结束一个项目时。
我们的 MLP 模型准确度达到了 87.42%!考虑到模型的深度和我们在开始时选择的超参数,这个成绩还不错。看看你能否调整模型,获得更高的测试集准确度。
这个准确度意味着什么?我们来计算一下错误发生的概率,导致客户服务问题(也就是客户没有收到“桌位已准备好”的短信,并因为在餐厅等待时间过长而感到不满)。
每个顾客的电话号码是十位数。假设我们的假设餐厅在每个地点平均有 30 张桌子,并且这些桌子在高峰时段每晚翻台两次,而此时系统正处于高使用频率,最后,这个餐厅连锁有 35 个分店。这意味着每天运营中大约会捕捉到 21,000 个手写数字(30 张桌子 x 每天翻台 2 次 x 35 个分店 x 10 位电话号码)。
显然,所有数字必须被正确分类,才能将信息传递给等待的餐厅顾客。因此,任何单一数字的错误分类都会导致失败。在我们的示例中,87.42%的模型准确率将导致每天错误分类 2,642 个数字。假设的最坏情况是,每个电话号码中只有一个数字被错误分类。由于只有 2,100 个顾客和相应的电话号码,这意味着每个电话号码都会有一个错误分类(100%的失败率),没有一个顾客能收到他们的座位通知!在这种情况下,最佳情景是每个电话号码中的所有 10 个数字都被错误分类,这将导致 2,100 个号码中有 263 个错误电话号码(12.5%的失败率)。即使在这种情况下,餐厅连锁也不太可能满意这一性能水平。
箴言:模型表现不一定等同于系统或应用程序的表现。许多因素决定了系统在现实世界中的鲁棒性或脆弱性。模型表现是一个关键因素,但其他具有独立容错能力的因素同样发挥着作用。了解你的深度学习模型如何融入更大的项目中,这样你才能设定正确的期望!
摘要
在本章的项目中,我们成功构建了一个多层感知机(MLP),用来基于手写数字进行回归分类预测。我们通过使用 MNIST 数据集和深度神经网络模型架构积累了经验,同时也有机会定义一些关键的超参数。最后,我们对模型在测试中的表现进行了评估,判断是否达成了我们的目标。
第三章:使用 word2vec 进行词表示
我们的Python 深度学习项目团队做得很好,我们(假设的)业务用例已经扩展!在上一个项目中,我们被要求准确地对手写数字进行分类,以生成电话号码,以便向餐饮连锁的顾客发送可用桌位通知短信。我们从项目中学到的是,餐厅发送的短信内容既友好又容易被接受。餐厅实际上收到了顾客的短信回复!
通知文本是:我们很高兴你来了,你的桌子已经准备好了!请见迎宾员,我们现在为您安排座位。
回复文本内容各异且通常较短,但迎宾员和餐厅管理层注意到了这些回复,并开始考虑也许可以利用这个简单的系统来获取就餐体验的反馈。这些反馈可以提供有价值的商业智能,了解食物的味道、服务的质量以及整体就餐体验的好坏。
定义成功:本项目的目标是构建一个计算语言学模型,使用 word2vec,它可以接受文本回复(如我们本章假设的用例所示),并输出一个有意义的情感分类。
本章将介绍深度学习(DL)在计算语言学中的基础知识。
我们介绍了词的稠密向量表示在各种计算语言学任务中的作用,以及如何从无标签的单语语料库中构建这些表示。
然后,我们将展示语言模型在各种计算语言学任务中的作用,如文本分类,以及如何使用卷积神经网络(CNNs)从无标签的单语语料库中构建它们。我们还将探讨用于语言建模的 CNN 架构。
在进行机器学习/深度学习(DL)时,数据的结构非常重要。不幸的是,原始数据通常非常脏且无结构,特别是在自然语言处理(NLP)的实践中。处理文本数据时,我们不能将字符串直接输入大多数深度学习算法;因此,词嵌入方法应运而生。词嵌入用于将文本数据转换为稠密的向量(张量)形式,我们可以将其输入到学习算法中。
有几种方法可以进行词嵌入,如独热编码、GloVe、word2vec 等,每种方法都有其优缺点。我们目前最喜欢的是 word2vec,因为它已被证明是学习高质量特征时最有效的方法。
如果你曾经处理过输入数据为文本的案例,那么你就知道这是一件非常杂乱的事,因为你需要教计算机理解人类语言的种种不规则性。语言有很多歧义,你需要像处理层级结构一样教会计算机,语法是稀疏的。这些就是词向量解决的问题,通过消除歧义并使不同种类的概念变得相似。
在本章中,我们将学习如何构建 word2vec 模型,并分析我们可以从提供的语料库中学到哪些特征。同时,我们将学习如何构建一个利用卷积神经网络(CNN)和训练好的词向量的语言模型。
学习词向量
为了实现一个完全功能的词嵌入模型,我们将执行以下步骤:
-
加载所有依赖项
-
准备文本语料库
-
定义模型
-
训练模型
-
分析模型
-
使用 t-分布随机邻域嵌入(t-SNE)算法绘制词汇集群
-
在 TensorBoard 上绘制模型
让我们一起制作世界级的词嵌入模型!
本节的代码可以在 github.com/PacktPublishing/Python-Deep-Learning-Projects/blob/master/Chapter03/create_word2vec.ipynb 找到。
加载所有依赖项
在本章中,我们将使用 gensim 模块(github.com/RaRe-Technologies/gensim)来训练我们的 word2vec 模型。Gensim 为许多流行算法提供了大规模多核处理支持,包括 潜在狄利克雷分配(LDA)、层次狄利克雷过程(HDP)和 word2vec。我们还可以采取其他方法,例如使用 TensorFlow (github.com/tensorflow/models/blob/master/tutorials/embedding/word2vec_optimized.py) 定义我们自己的计算图并构建模型——这将是我们稍后要探讨的内容。
了解代码!Python 的依赖项相对容易管理。你可以在 packaging.python.org/tutorials/managing-dependencies/ 了解更多内容。
本教程将引导你通过使用 Pipenv 来管理应用程序的依赖项。它会展示如何安装和使用必要的工具,并给出关于最佳实践的强烈建议。请记住,Python 被广泛用于各种不同的目的,你管理依赖项的方式可能会根据你决定如何发布软件而发生变化。这里的指导最直接适用于网络服务(包括 Web 应用程序)的开发和部署,但也非常适合用于任何类型项目的开发和测试环境的管理。
我们将使用seaborn包来绘制词汇集群,使用sklearn来实现 t-SNE 算法,并使用tensorflow来构建 TensorBoard 图表:
import multiprocessing
import os , json , requests
import re
import nltk
import gensim.models.word2vec as w2v
import sklearn.manifold
import pandas as pd
import seaborn as sns
import tensorflow as tf
from tensorflow.contrib.tensorboard.plugins import projector
准备文本语料库
我们将使用之前训练过的自然语言工具包(NLTK)分词器(www.nltk.org/index.html)和英语的停用词,来清理我们的语料库,并从中提取相关的唯一单词。我们还将创建一个小模块来清理提供的集合,该集合包含未处理的句子列表,最终输出单词列表:
"""**Download NLTK tokenizer models (only the first time)**"""
nltk.download("punkt")
nltk.download("stopwords")
def sentence_to_wordlist(raw):
clean = re.sub("[^a-zA-Z]"," ", raw)
words = clean.split()
return map(lambda x:x.lower(),words)
由于我们还没有从假设的业务案例中的文本响应捕获数据,先让我们收集一个高质量的数据集,这些数据集可以从网络上获得。通过使用这个语料库来展示我们的理解和技能,将为我们准备处理假设的业务用例数据。你也可以使用自己的数据集,但重要的是拥有大量的单词,这样word2vec模型才能很好地进行泛化。因此,我们将从古腾堡项目网站加载我们的数据,网址是Gutenberg.org。
然后,我们将原始语料库分词成唯一且干净的单词列表,如下图所示:

该过程展示了数据转化的过程,从原始数据到将输入到 word2vec 模型中的单词列表。
在这里,我们将从 URL 下载文本数据并按照前面图示的方式处理它们:
# Article 0on earth from Gutenberg website
filepath = 'http://www.gutenberg.org/files/33224/33224-0.txt
corpus_raw = requests.get(filepath).text
tokenizer = nltk.data.load('tokenizers/punkt/english.pickle')
raw_sentences = tokenizer.tokenize(corpus_raw)
#sentence where each word is tokenized
sentences = []
for raw_sentence in raw_sentences:
if len(raw_sentence) > 0:
sentences.append(sentence_to_wordlist(raw_sentence))
定义我们的 word2vec 模型
现在让我们在定义word2vec模型时使用gensim。首先,我们定义模型的一些超参数,例如维度,它决定了我们希望学习多少低级特征。每个维度将学习一个独特的概念,比如性别、物体、年龄等。
计算语言学模型小贴士 #1:增加维度数量可以提高模型的泛化能力……但同时也会增加计算复杂度。正确的维度数是一个经验性问题,需要你作为应用人工智能深度学习工程师来决定!
计算语言学模型小贴士 #2:注意context_size。这很重要,因为它设定了当前单词与目标单词预测之间的最大距离限制。这个参数帮助模型学习单词与其他相邻单词之间的深层关系。
使用gensim实例,我们将定义我们的模型,包括所有超参数:
num_features = 300
# Minimum word count threshold.
min_word_count = 3
# Number of threads to run in parallel.
#more workers, faster we train
num_workers = multiprocessing.cpu_count()
# Context window length.
context_size = 7
# Downsample setting for frequent words. 0 - 1e-5 is good for this
downsampling = 1e-3
seed = 1
model2vec = w2v.Word2Vec(
sg=1,
seed=seed,
workers=num_workers,
size=num_features,
min_count=min_word_count,
window=context_size,
sample=downsampling
)
model2vec.build_vocab(sentences)
训练模型
一旦我们配置了gensim word2vec对象,我们需要给模型一些训练。请做好准备,因为根据数据量和计算能力的不同,这可能需要一些时间。在此过程中,我们需要定义训练的轮次(epoch),这可以根据数据的大小而有所不同。你可以调整这些值并评估word2vec模型的性能。
此外,我们将保存训练好的模型,以便在构建语言模型时能够稍后使用:
"""**Start training, this might take a minute or two...**"""
model2vec.train(sentences ,total_examples=model2vec.corpus_count , epochs=100)
"""**Save to file, can be useful later**"""
if not os.path.exists(os.path.join("trained",'sample')):
os.makedirs(os.path.join("trained",'sample'))
model2vec.save(os.path.join("trained",'sample', ".w2v"))
一旦训练过程完成,你可以看到一个二进制文件存储在/trained/sample.w2v中。你可以将sample.w2v文件分享给他人,他们可以在他们的 NLP 应用中使用这些词向量,并将其加载到任何其他 NLP 任务中。
分析模型
现在我们已经训练了word2vec模型,让我们来探索一下模型能够学到什么。我们将使用most_similar()来探索不同单词之间的关系。在下面的例子中,你会看到模型学到了earth这个词与crust、globe等词是相关的。很有趣的是,我们只提供了原始数据,模型就能够自动学习到这些关系和概念!以下是这个例子:
model2vec.most_similar("earth")
[(u'crust', 0.6946468353271484),
(u'globe', 0.6748907566070557),
(u'inequalities', 0.6181437969207764),
(u'planet', 0.6092090606689453),
(u'orbit', 0.6079996824264526),
(u'laboring', 0.6058655977249146),
(u'sun', 0.5901342630386353),
(u'reduce', 0.5893668532371521),
(u'moon', 0.5724939107894897),
(u'eccentricity', 0.5709577798843384)]
让我们试着找出与human相关的词,并看看模型学到了什么:
model2vec.most_similar("human")
[(u'art', 0.6744576692581177),
(u'race', 0.6348963975906372),
(u'industry', 0.6203593611717224),
(u'man', 0.6148483753204346),
(u'population', 0.6090731620788574),
(u'mummies', 0.5895125865936279),
(u'gods', 0.5859177112579346),
(u'domesticated', 0.5857442021369934),
(u'lives', 0.5848811864852905),
(u'figures', 0.5809590816497803)]
批判性思维提示:有趣的是,art、race和industry是最相似的输出。请记住,这些相似性是基于我们用来训练的语料库的,应该在这个上下文中加以理解。当过时或不相关的训练语料库的相似性被用于训练应用于新的语言数据或文化规范的模型时,泛化以及它的不受欢迎的伴侣偏见可能会产生影响。
即使我们试图通过使用两个正向向量earth和moon,以及一个负向向量orbit来推导类比,模型仍然预测出sun这个词,这是有道理的,因为月球绕地球公转,地球绕太阳公转之间存在语义关系:
model2vec.most_similar_cosmul(positive=['earth','moon'], negative=['orbit'])
(u'sun', 0.8161555624008179)
所以,我们了解到,通过使用word2vec模型,我们可以从原始未标记数据中提取有价值的信息。这个过程在学习语言的语法和词语之间的语义关联方面至关重要。
稍后,我们将学习如何将这些word2vec特征作为分类模型的输入,这有助于提高模型的准确性和性能。
使用 t-SNE 算法绘制词汇聚类
所以,在我们的分析之后,我们知道我们的word2vec模型从提供的语料库中学到了一些概念,但我们如何可视化它呢?因为我们已经创建了一个 300 维的空间来学习特征,实际上我们不可能直接进行可视化。为了使其成为可能,我们将使用一个降维算法,叫做 t-SNE,它在将高维空间降到更容易理解的二维或三维空间方面非常有名。
"t-分布随机邻居嵌入(t-SNE)(lvdmaaten.github.io/tsne/)是一种(获奖的)降维技术,特别适合于高维数据集的可视化。该技术可以通过 Barnes-Hut 近似实现,从而可以应用于大型真实世界数据集。我们将其应用于包含多达 3000 万个示例的数据集。"
– Laurens van der Maaten
为了实现这一点,我们将使用sklearn包,并定义n_components=2,这意味着我们希望输出的是二维空间。接下来,我们将通过将单词向量输入到 t-SNE 对象中来进行变换。
在这一步之后,我们现在有一组每个单词的值,可以分别作为x和y坐标,用于在二维平面上绘制它。让我们准备一个DataFrame来存储所有单词及其x和y坐标在同一个变量中,如下图所示,并从中获取数据来创建散点图:
tsne = sklearn.manifold.TSNE(n_components=2, random_state=0)
all_word_vectors_matrix = model2vec.wv.vectors
all_word_vectors_matrix_2d = tsne.fit_transform(all_word_vectors_matrix)
points = pd.DataFrame(
[
(word, coords[0], coords[1])
for word, coords in [
(word, all_word_vectors_matrix_2d[model2vec.wv.vocab[word].index])
for word in model2vec.wv.vocab
]
],
columns=["word", "x", "y"]
)
sns.set_context("poster")
ax = points.plot.scatter("x", "y", s=10, figsize=(20, 12))
fig = ax.get_figure()
这是我们的DataFrame,包含了单词及其x和y坐标:

我们的单词列表及使用 t-SNE 获得的坐标值。
这是在二维平面上绘制了 425,633 个标记后,整个集群的样子。每个点的位置是在学习了附近单词的特征和关联后确定的,如下所示:

在二维平面上绘制所有独特单词的散点图
通过在 TensorBoard 上绘制模型来可视化嵌入空间
如果不能利用可视化来理解模型的学习过程和内容,那么可视化是没有意义的。为了更好地理解模型学到了什么,我们将使用 TensorBoard。
TensorBoard 是一个强大的工具,可以用来构建各种类型的图表,以监控模型在训练过程中的状态,并且在构建深度学习架构和词嵌入时非常有用。让我们构建一个 TensorBoard 嵌入投影,并利用它进行各种分析。
为了在 TensorBoard 中构建嵌入图,我们需要执行以下步骤:
-
收集我们在之前步骤中学习到的单词及其相应的张量(300 维向量)。
-
在图中创建一个变量,用于存储张量。
-
初始化投影器。
-
包含一个合适命名的嵌入层。
-
将所有单词存储在一个
.tsv格式的元数据文件中。这些文件类型由 TensorBoard 使用,以加载和显示单词。 -
将
.tsv元数据文件链接到投影器对象。 -
定义一个函数,用于存储所有的总结检查点。
以下是完成前面七个步骤的代码:
vocab_list = points.word.values.tolist()
embeddings = all_word_vectors_matrix
embedding_var = tf.Variable(all_word_vectors_matrix, dtype='float32', name='embedding')
projector_config = projector.ProjectorConfig()
embedding = projector_config.embeddings.add()
embedding.tensor_name = embedding_var.name
LOG_DIR='./'
metadata_file = os.path.join("sample.tsv")
with open(os.path.join(LOG_DIR, metadata_file), 'wt') as metadata:
metadata.writelines("%s\n" % w.encode('utf-8') for w in vocab_list)
embedding.metadata_path = os.path.join(os.getcwd(), metadata_file)
# Use the same LOG_DIR where you stored your checkpoint.
summary_writer = tf.summary.FileWriter(LOG_DIR)
# The next line writes a projector_config.pbtxt in the LOG_DIR. TensorBoard will
# read this file during startup.
projector.visualize_embeddings(summary_writer, projector_config)
saver = tf.train.Saver([embedding_var])
with tf.Session() as sess:
# Initialize the model
sess.run(tf.global_variables_initializer())
saver.save(sess, os.path.join(LOG_DIR, metadata_file+'.ckpt'))
一旦执行了 TensorBoard 准备模块,二进制文件、元数据和检查点就会存储到磁盘中,如下图所示:

TensorBoard 创建的输出
要可视化 TensorBoard,请在终端中执行以下命令:
tensorboard --logdir=/path/of/the/checkpoint/
现在,在浏览器中打开http://localhost:6006/#projector,你应该能看到 TensorBoard,其中所有数据点都投影到 3D 空间中。你可以放大、缩小,查找特定的词语,以及使用 t-SNE 重新训练模型,查看数据集的聚类形成:

TensorBoard 嵌入投影
数据可视化帮助你讲述故事!TensorBoard 非常酷!你的业务案例相关方喜欢令人印象深刻的动态数据可视化。它们有助于提升你对模型的直觉,并生成新的假设以进行测试。
使用 CNN 和 word2vec 构建语言模型
现在我们已经学习了计算语言学的核心概念,并从提供的数据集中训练了关系,我们可以利用这一学习来实现一个可以执行任务的语言模型。
在本节中,我们将构建一个文本分类模型,用于情感分析。对于分类,我们将使用 CNN 和预训练的word2vec模型的结合,这部分我们在本章的前一节中已经学习过。
这个任务是我们假设的业务案例的模拟,目的是从餐厅顾客的文本反馈中提取信息,并将他们的回复分类为餐厅有意义的类别。
我们受到了 Denny Britz 的启发(twitter.com/dennybritz),在他关于在 TensorFlow 中实现 CNN 进行文本分类 (www.wildml.com/2015/12/implementing-a-cnn-for-text-classification-in-tensorflow/)的工作基础上,构建了我们自己的 CNN 和文本分类模型。我们邀请你查看他创作的博客,以便更全面地了解 CNN 在文本分类中的内部机制。
总体而言,这种架构从输入嵌入步骤开始,然后是使用多个滤波器的最大池化进行的 2D 卷积,最后是一个 softmax 激活层输出结果。
探索 CNN 模型
你可能会问,CNN 在图像处理领域最常用,如何将其用于文本分类呢?
文献中有很多讨论(在本提示底部提供了链接),证明了 CNN 是一个通用的特征提取函数,可以计算位置不变性和组合性。位置不变性帮助模型捕捉词语的上下文,无论它们在语料库中的出现位置如何。组合性有助于利用低级特征推导出更高级的表示:
-
用于句子分类的卷积神经网络 (
arxiv.org/abs/1408.5882) -
基于 CNN 的场景中文文本识别算法与合成数据引擎 (
arxiv.org/abs/1604.01891) -
文本注意卷积神经网络用于场景文本检测(
arxiv.org/pdf/1510.03283.pdf)
因此,模型接收到的不是图像的像素值,而是一个热编码的词向量或word2vec矩阵,代表一个词或一个字符(对于基于字符的模型)。Denny Britz 的实现中有两个滤波器,每个滤波器有两个、三个和四个不同的区域大小。卷积操作通过这些滤波器在句子矩阵上进行处理,生成特征图。通过对每个激活图执行最大池化操作来进行下采样。最后,所有输出都被连接并传递到 softmax 分类器中。
因为我们正在执行情感分析,所以会有正面和负面两个输出类别目标。softmax 分类器将输出每个类别的概率,如下所示:

该图来自 Denny Britz 的博客文章,描述了 CNN 语言模型的工作原理
让我们看看模型的实现。我们通过添加先前训练的word2vec模型组件的输入,修改了现有的实现。
这个项目的代码可以在github.com/PacktPublishing/Python-Deep-Learning-Projects/tree/master/Chapter03/sentiment_analysis找到。
模型位于text_cnn.py中。我们创建了一个名为TextCNN的类,该类接受一些超参数作为模型配置的输入。以下是超参数的列表:
-
sequence_length:固定的句子长度 -
num_classes:softmax 激活输出的类别数(正面和负面) -
vocab_size:我们词向量中唯一词的数量 -
embedding_size:我们创建的嵌入维度 -
filter_sizes:卷积滤波器将覆盖这么多的词 -
num_filters:每个滤波器大小将有这么多的滤波器 -
pre_trained:集成了先前训练过的word2vec表示
以下是TextCNN()类的声明,init()函数初始化了所有超参数值:
import tensorflow as tf
import numpy as np
class TextCNN(object):
"""
A CNN for text classification.
Uses an embedding layer, followed by a convolutional, max-pooling and softmax layer.
"""
def __init__(self,
sequence_length,
num_classes,
vocab_size,
embedding_size,
filter_sizes,
num_filters,
l2_reg_lambda=0.0,
pre_trained=False):
代码被分为六个主要部分:
- 输入的占位符:我们首先定义了所有需要包含模型输入值的占位符。在这种情况下,输入是句子向量和相关标签(正面或负面)。
input_x保存句子,input_y保存标签值,我们使用dropout_keep_prob来表示我们在 dropout 层中保留神经元的概率。以下代码展示了一个示例:
# Placeholders for input, output and dropout
self.input_x = tf.placeholder(
tf.int32, [
None,
sequence_length,
], name="input_x")
self.input_y = tf.placeholder(
tf.float32, [None, num_classes], name="input_y")
self.dropout_keep_prob = tf.placeholder(
tf.float32, name="dropout_keep_prob")
# Keeping track of l2 regularization loss (optional)
l2_loss = tf.constant(0.0)
- 嵌入层:我们模型的第一层,是我们将
word2vec模型训练过程中学习到的单词表示输入的嵌入层。我们将修改仓库中的基准代码,使用我们预训练的嵌入模型,而不是从头开始学习嵌入。这将提高模型的准确性。这也是一种迁移学习,我们将从通用的 Wikipedia 或社交媒体语料库中转移学到的一般知识。通过word2vec模型初始化的嵌入矩阵命名为W,如下所示:
# Embedding layer
with tf.device('/cpu:0'), tf.name_scope("embedding"):
if pre_trained:
W_ = tf.Variable(
tf.constant(0.0, shape=[vocab_size, embedding_size]),
trainable=False,
name='W')
self.embedding_placeholder = tf.placeholder(
tf.float32, [vocab_size, embedding_size],
name='pre_trained')
W = tf.assign(W_, self.embedding_placeholder)
else:
W = tf.Variable(
tf.random_uniform([vocab_size, embedding_size], -1.0, 1.0),
name="W")
self.embedded_chars = tf.nn.embedding_lookup(W, self.input_x)
self.embedded_chars_expanded = tf.expand_dims(
self.embedded_chars, -1)
- 卷积与最大池化:定义卷积层是通过
tf.nn.conv2d()完成的。它的输入是前一个嵌入层的权重(W—过滤矩阵),并应用一个非线性 ReLU 激活函数。然后,使用tf.nn.max_pool()对每个过滤器大小进行进一步的最大池化。结果会被串联起来,形成一个单一的向量,作为下一层模型的输入:
# Create a convolution + maxpool layer for each filter size
pooled_outputs = []
for i, filter_size in enumerate(filter_sizes):
with tf.name_scope("conv-maxpool-%s" % filter_size):
# Convolution Layer
filter_shape = [filter_size, embedding_size, 1, num_filters]
W = tf.Variable(
tf.truncated_normal(filter_shape, stddev=0.1), name="W")
b = tf.Variable(
tf.constant(0.1, shape=[num_filters]), name="b")
conv = tf.nn.conv2d(
self.embedded_chars_expanded,
W,
strides=[1, 1, 1, 1],
padding="VALID",
name="conv")
# Apply nonlinearity
h = tf.nn.relu(tf.nn.bias_add(conv, b), name="relu")
# Maxpooling over the outputs
pooled = tf.nn.max_pool(
h,
ksize=[1, sequence_length - filter_size + 1, 1, 1],
strides=[1, 1, 1, 1],
padding='VALID',
name="pool")
pooled_outputs.append(pooled)
# Combine all the pooled features
num_filters_total = num_filters * len(filter_sizes)
self.h_pool = tf.concat(pooled_outputs, 3)
self.h_pool_flat = tf.reshape(self.h_pool, [-1, num_filters_total])
- Dropout 层:为了正则化 CNN 并防止模型过拟合,少量来自神经元的信号会被阻断。这迫使模型学习更多独特或个别的特征:
# Add dropout
with tf.name_scope("dropout"):
self.h_drop = tf.nn.dropout(self.h_pool_flat,
self.dropout_keep_prob)
- 预测:一个 TensorFlow 包装器执行 W * x+b 度量乘法,其中
x是上一层的输出。这个计算将计算分数的值,预测结果将通过tf.argmax()产生:
# Final (unnormalized) scores and predictions
with tf.name_scope("output"):
W = tf.get_variable(
"W",
shape=[num_filters_total, num_classes],
initializer=tf.contrib.layers.xavier_initializer())
b = tf.Variable(tf.constant(0.1, shape=[num_classes]), name="b")
l2_loss += tf.nn.l2_loss(W)
l2_loss += tf.nn.l2_loss(b)
self.scores = tf.nn.xw_plus_b(self.h_drop, W, b, name="scores")
self.predictions = tf.argmax(self.scores, 1, name="predictions")
- 准确率:我们可以使用我们的分数定义
loss函数。记住,我们网络所犯的错误的衡量标准叫做 loss。作为优秀的深度学习工程师,我们希望最小化它,让我们的模型更加准确。对于分类问题,交叉熵损失 (cs231n.github.io/linear-classify/#softmax) 是标准的loss函数:
# CalculateMean cross-entropy loss
with tf.name_scope("loss"):
losses = tf.nn.softmax_cross_entropy_with_logits(
labels=self.input_y, logits=self.scores)
self.loss = tf.reduce_mean(losses) + l2_reg_lambda * l2_loss
# Accuracy
with tf.name_scope("accuracy"):
correct_predictions = tf.equal(self.predictions,
tf.argmax(self.input_y, 1))
self.accuracy = tf.reduce_mean(
tf.cast(correct_predictions, "float"), name="accuracy")
就这样,我们的模型完成了。让我们使用 TensorBoard 来可视化网络并提高我们的直觉,如下所示:

CNN 模型架构定义
理解数据格式
在本案例中,使用了一个有趣的数据集,来自 Rotten Tomatoes 的 电影评论数据 (www.cs.cornell.edu/people/pabo/movie-review-data/)。一半的评论是正面的,另一半是负面的,总共有大约 10,000 个句子。词汇表中大约有 20,000 个不同的单词。数据集存储在 data 文件夹中。
它包含两个文件:一个,rt-polarity.neg,包含所有负面句子;另一个,rt-polarity.pos,只包含正面句子。为了执行分类,我们需要将它们与标签关联。每个正面句子与一个独热编码标签 [0, 1] 关联,每个负面句子与 [1, 0] 关联,如下图所示:

一些正面句子的示例及与这些句子关联的标签
文本数据的预处理通过以下四个步骤完成:
-
加载:确保加载正面和负面句子数据文件
-
清理:使用正则表达式移除标点符号和其他特殊字符
-
填充:通过添加
<PAD>标记使每个句子大小相同 -
索引:将每个单词映射到一个整数索引,以便每个句子都能变成整数向量
现在我们已经将数据格式化为向量,我们可以将其输入到模型中。
将 word2vec 与 CNN 结合
所以,上次我们创建word2vec模型时,我们将该模型导出了一个二进制文件。现在是时候将该模型作为 CNN 模型的一部分来使用了。我们通过将嵌入层中的W权重初始化为这些值来实现这一点。
由于我们在之前的word2vec模型中使用了非常小的语料库,让我们选择一个已经在大规模语料库上预训练的word2vec模型。一种不错的策略是使用 fastText 嵌入,它是在互联网上可用的文档和 294 种语言上训练的(github.com/facebookresearch/fastText/blob/master/pretrained-vectors.md)。我们按以下步骤进行:
-
我们将下载英语 Embedding fastText 数据集(
s3-us-west-1.amazonaws.com/fasttext-vectors/wiki.en.zip) -
接下来,将词汇表和嵌入向量提取到一个单独的文件中
-
将它们加载到
train.py文件中
就是这样——通过引入这一步骤,我们现在可以将预训练的word2vec模型馈入嵌入层。信息的整合提供了足够的特征,来改善 CNN 模型的学习过程。
执行模型
现在是时候使用提供的数据集和预训练的嵌入模型来训练我们的模型了。需要对一些超参数进行微调,以达到良好的结果。但一旦我们使用合理的配置执行train.py文件,就可以证明该模型能够在分类时很好地区分正面和负面句子。
如下图所示,准确率的性能指标逐渐趋近于 1,而损失因子在每次迭代中逐渐减少至 0:

在训练过程中,CNN 模型的性能指标准确率和损失的图表
就这样!我们刚刚使用了预训练的嵌入模型来训练我们的 CNN 分类器,平均损失为 6.9,准确率为 72.6%。
一旦模型训练成功完成,模型的输出将包含以下内容:
-
检查点存储在
/runs/folder目录下。我们将使用这些检查点来进行预测。 -
一个包含所有损失、准确率、直方图和训练过程中捕获的梯度值分布的总结。我们可以使用 TensorBoard 可视化这些信息。
将模型部署到生产环境
现在,我们的模型二进制文件已存储在/runs/文件夹中,我们只需编写一个 RESTful API,可以使用 Flask,然后调用在model_inference.py代码中定义的sentiment_engine()。
始终确保使用最佳模型的检查点和正确的嵌入文件,定义如下:
checkpoint_dir = "./runs/1508847544/"
embedding = np.load('fasttext_embedding.npy')
总结
今天的项目是使用 word2vec 构建一个深度学习计算语言学模型,在情感分析范式中准确分类文本。我们的假设使用案例是将深度学习应用于帮助餐厅连锁管理层理解客户通过短信回答问题后的整体情感。我们的具体任务是构建一个自然语言处理模型,从这个简单(假设的)应用中获得数据,生成商业智能。
回顾我们的成功标准:我们做得怎么样?我们成功了吗?成功的影响是什么?正如我们在项目开始时定义的那样,这些是我们作为深度学习数据科学家在结束项目时会问的关键问题。
我们的 CNN 模型,基于本章前面创建的训练好的word2vec模型,达到了 72.6%的准确率!这意味着我们能够合理准确地将无结构的文本句子分类为正面或负面。
这种准确性的意义是什么?在我们的假设案例中,这意味着我们可以将一堆数据总结出来,而这些数据如果没有这个深度学习自然语言处理模型是很难总结的,并通过总结得出可操作的见解供餐厅管理层使用。通过总结对短信问题的正面或负面情感的数据点,餐厅连锁可以跟踪表现,进行调整,甚至可能奖励员工的改进。
在本章的项目中,我们学习了如何构建word2vec模型,并分析了我们可以从提供的语料库中学到哪些特征。我们还学习了如何使用训练好的词嵌入构建一个基于 CNN 的语言模型。
最后,我们查看了模型在测试中的表现,并确定是否成功达成了目标。在下一个章节的项目中,我们将利用更多的计算语言学技能,创建一个自然语言管道,驱动一个开放域问答的聊天机器人。这是一个令人兴奋的工作——让我们看看接下来会发生什么!
第四章:为构建聊天机器人建立 NLP 管道。
我们的项目再次扩展了,这要归功于我们所做的出色工作。最初,我们为一家餐饮连锁店工作,帮助他们对手写数字进行分类,用于一个文本通知系统,提醒等待的客人他们的餐桌已经准备好。基于这一成功,当店主意识到顾客实际上在回应这些短信时,我们被要求贡献一个深度学习解决方案,利用自然语言处理(NLP)来准确地将文本分类为有意义的情感类别,以便为店主提供顾客在用餐体验中的满意度反馈。
你知道做得好深度学习工程师会发生什么吗?他们会被要求做更多的事!
这个下一步的商业用例项目非常酷。我们被要求做的是创建一个自然语言处理管道,为开放域问答提供支持。这个(假设的)餐饮连锁店有一个网站,上面有菜单、历史、地点、营业时间和其他信息,他们希望添加一个功能,使网站访问者可以在查询框中提问,然后由我们的深度学习 NLP 聊天机器人找到相关信息并反馈给他们。他们认为,将正确的信息快速传达给网站访问者,有助于推动到店访问并改善整体客户体验。
命名实体识别(NER)是我们将使用的方法,它将赋予我们快速分类输入文本的能力,然后我们可以将其与相关内容匹配以回应。这是一种很好的方法,可以利用大量不断变化的非结构化数据,而无需使用硬编码的启发式方法。
在本章中,我们将学习 NLP 模型的构建模块,包括预处理、分词和词性标注。我们将利用这些理解来构建一个系统,能够读取非结构化的文本,以便为特定问题制定回答。我们还将描述如何将这个深度学习组件纳入经典的 NLP 管道中,以便检索信息,从而提供一个无需结构化知识库的开放域问答系统。
在本章中,我们将做以下几件事:
-
使用统计建模框架构建一个基于常见问题的聊天机器人,能够检测意图和实体,以回答开放域问题。
-
学习如何生成句子的密集表示。
-
构建一个文档阅读器,从非结构化文本中提取答案。
-
学习如何将深度学习模型集成到经典的 NLP 管道中。
定义目标:构建一个能够理解上下文(意图)并能够提取实体的聊天机器人。为了做到这一点,我们需要一个能够执行意图分类,并结合 NER 提取的 NLP 管道,以便提供准确的回应。
学习的技能:你将学习如何使用经典的自然语言处理管道构建一个开放领域的问答系统,文档阅读器组件使用深度学习技术来生成句子表示。
让我们开始吧!
自然语言处理管道基础
文本数据是一个非常庞大的信息来源,正确处理它对于成功至关重要。因此,为了处理这些文本数据,我们需要遵循一些基本的文本处理步骤。
本节中覆盖的大部分处理步骤在自然语言处理(NLP)中是常用的,并涉及将多个步骤组合成一个可执行流程。这就是我们所说的 NLP 管道。这个流程可以是分词、词干提取、词频统计、词性标注等多个元素的组合。
让我们详细了解如何实现 NLP 管道中的各个步骤,特别是每个处理阶段的功能。我们将使用 自然语言工具包(NLTK)——一个用 Python 编写的 NLP 工具包,你可以通过以下方式安装它:
import nltk
nltk.download('punkt')
nltk.download('averaged_perceptron_tagger')
该项目的代码可在 github.com/PacktPublishing/Python-Deep-Learning-Projects/blob/master/Chapter04/Basic%20NLP%20Pipeline.ipynb找到。
分词
分词将语料库分割成句子、单词或词项。分词是让我们的文本准备好进行进一步处理的必要步骤,也是构建 NLP 管道的第一步。一个词项的定义可以根据我们执行的任务或我们所工作的领域而有所不同,所以在定义词项时要保持开放的心态!
了解代码:NLTK 非常强大,因为库中已经完成了大量的硬编码工作。你可以在 www.nltk.org/api/nltk.tokenize.html#nltk.tokenize.api.TokenizerI.tokenize_sents 阅读更多关于 NLTK 分词的信息。
让我们尝试加载一个语料库,并使用 NLTK 分词器首先将原始语料库分割成句子,然后进一步将每个句子分割成单词:
text = u"""
Dealing with textual data is very crucial so to handle these text data we need some
basic text processing steps. Most of the processing steps covered in this section are
commonly used in NLP and involve the combination of several steps into a single
executable flow. This is usually referred to as the NLP pipeline. These flow
can be a combination of tokenization, stemming, word frequency, parts of
speech tagging, etc.
"""
# Sentence Tokenization
sentenses = nltk.sent_tokenize(text)
# Word Tokenization
words = [nltk.word_tokenize(s) for s in sentenses]
OUTPUT:
SENTENCES:
[u'\nDealing with textual data is very crucial so to handle these text data we need some \nbasic text processing steps.',
u'Most of the processing steps covered in this section are \ncommonly used in NLP and involve the combination of several steps into a single \nexecutable flow.',
u'This is usually referred to as the NLP pipeline.',
u'These flow \ncan be a combination of tokenization, stemming, word frequency, parts of \nspeech tagging, etc.']
WORDS:
[[u'Dealing', u'with', u'textual', u'data', u'is', u'very', u'crucial', u'so', u'to', u'handle', u'these', u'text', u'data', u'we', u'need', u'some', u'basic', u'text', u'processing', u'steps', u'.'], [u'Most', u'of', u'the', u'processing', u'steps', u'covered', u'in', u'this', u'section', u'are', u'commonly', u'used', u'in', u'NLP', u'and', u'involve', u'the', u'combination', u'of', u'several', u'steps', u'into', u'a', u'single', u'executable', u'flow', u'.'], [u'This', u'is', u'usually', u'referred', u'to', u'as', u'the', u'NLP', u'pipeline', u'.'], [u'These', u'flow', u'can', u'be', u'a', u'combination', u'of', u'tokenization', u',', u'stemming', u',', u'word', u'frequency', u',', u'parts', u'of', u'speech', u'tagging', u',', u'etc', u'.']]
词性标注
一些单词有多重含义,例如,charge 是一个名词,但也可以是动词,(to) charge。了解词性(POS)有助于消除歧义。句子中的每个词项都有多个属性,我们可以用来进行分析。词性的例子包括:名词表示人、地点或事物;动词表示动作或发生的事情;形容词是描述名词的词汇。利用这些属性,我们可以轻松创建文本摘要,统计最常见的名词、动词和形容词:
tagged_wt = [nltk.pos_tag(w)for w in words]
[[('One', 'CD'), ('way', 'NN'), ('to', 'TO'), ('extract', 'VB'), ('meaning', 'VBG'), ('from', 'IN'), ('text', 'NN'), ('is', 'VBZ'), ('to', 'TO'), ('analyze', 'VB'), ('individual', 'JJ'), ('words', 'NNS'), ('.', '.')], [('The', 'DT'), ('processes', 'NNS'), ('of', 'IN'), ('breaking', 'VBG'), ('up', 'RP'), ('a', 'DT'), ('text', 'NN'), ('into', 'IN'), ('words', 'NNS'), ('is', 'VBZ'), ('called', 'VBN'), ('tokenization', 'NN'), ('--', ':'), ('the', 'DT'), ('resulting', 'JJ'), ('words', 'NNS'), ('are', 'VBP'), ('referred', 'VBN'), ('to', 'TO'), ('as', 'IN'), ('tokens', 'NNS'), ('.', '.')], [('Punctuation', 'NN'), ('marks', 'NNS'), ('are', 'VBP'), ('also', 'RB'), ('tokens', 'NNS'), ('.', '.')], [('Each', 'DT'), ('token', 'NN'), ('in', 'IN'), ('a', 'DT'), ('sentence', 'NN'), ('has', 'VBZ'), ('several', 'JJ'), ('attributes', 'IN'), ('we', 'PRP'), ('can', 'MD'), ('use', 'VB'), ('for', 'IN'), ('analysis', 'NN'), ('.', '.')]]
patternPOS= []
for tag in tagged_wt:
patternPOS.append([v for k,v in tag])
[['CD', 'NN', 'TO', 'VB', 'VBG', 'IN', 'NN', 'VBZ', 'TO', 'VB', 'JJ', 'NNS', '.'], ['DT', 'NNS', 'IN', 'VBG', 'RP', 'DT', 'NN', 'IN', 'NNS', 'VBZ', 'VBN', 'NN', ':', 'DT', 'JJ', 'NNS', 'VBP', 'VBN', 'TO', 'IN', 'NNS', '.'], ['NN', 'NNS', 'VBP', 'RB', 'NNS', '.'], ['DT', 'NN', 'IN', 'DT', 'NN', 'VBZ', 'JJ', 'IN', 'PRP', 'MD', 'VB', 'IN', 'NN', '.'], ['DT', 'NN', 'IN', 'NN', 'IN', 'DT', 'NN', 'VBZ', 'CD', 'NN', ':', 'NNS', 'VBP', 'DT', 'NN', ',', 'NN', ',', 'CC', 'NN', ':', 'NNS', 'VBP', 'NNS', 'CC', 'NNS', ':', 'NNS', 'VBP', 'NNS', 'IN', 'NN', 'NNS', '.'], ['VBG', 'DT', 'NNS', ',', 'PRP', 'VBZ', 'JJ', 'TO', 'VB', 'DT', 'NN', 'IN', 'DT', 'NN', 'IN', 'NN', 'IN', 'VBG', 'DT', 'RBS', 'JJ', 'NNS', ',', 'NNS', ',', 'CC', 'NNS', '.']]
提取名词
让我们提取语料库中所有的名词。这在你需要提取特定内容时非常有用。我们使用 NN、NNS、NNP 和 NNPS 标签来提取名词:
nouns = []
for tag in tagged_wt:
nouns.append([k for k,v in tag if v in ['NN','NNS','NNP','NNPS']])
[['way', 'text', 'words'], ['processes', 'text', 'words', 'tokenization', 'words', 'tokens'], ['Punctuation', 'marks', 'tokens'], ['token', 'sentence', 'analysis'], ['part', 'speech', 'word', 'example', 'nouns', 'person', 'place', 'thing', 'verbs', 'actions', 'occurences', 'adjectives', 'words', 'describe', 'nouns'], ['attributes', 'summary', 'piece', 'text', 'nouns', 'verbs', 'adjectives']]
提取动词
让我们提取语料库中所有的动词。在这种情况下,我们使用VB、VBD、VBG、VBN、VBP和VBZ作为动词标签:
verbs = []
for tag in tagged_wt:
verbs.append([k for k,v in tag if v in ['VB','VBD','VBG','VBN','VBP','VBZ']])
[['extract', 'meaning', 'is', 'analyze'], ['breaking', 'is', 'called', 'are', 'referred'], ['are'], ['has', 'use'], ['is', 'are', 'are', 'are'], ['Using', "'s", 'create', 'counting']]
现在,让我们使用spacy对一段文本进行分词,并访问每个词语的词性(POS)属性。作为示例应用,我们将对前一段进行分词,并通过以下代码统计最常见的名词。我们还将对这些词语进行词形还原(lemmatization),将词语还原为其根形,以帮助我们在不同形式的词语之间进行标准化:
! pip install -q spacy
! pip install -q tabulate
! python -m spacy download en_core_web_lg
from collections import Counter
import spacy
from tabulate import tabulate
nlp = spacy.load('en_core_web_lg')
doc = nlp(text)
noun_counter = Counter(token.lemma_ for token in doc if token.pos_ == 'NOUN')
print(tabulate(noun_counter.most_common(5), headers=['Noun', 'Count']))
以下是输出结果:
Noun Count
----------- -------
step 3
combination 2
text 2
processing 2
datum 2
依存句法分析
依存句法分析是一种理解句子中词语之间关系的方法。依存关系是一种更细粒度的属性,可以帮助建立模型对单词在句子中的关系的理解:
doc = nlp(sentenses[2])
spacy.displacy.render(doc,style='dep', options={'distance' : 140}, jupyter=True)
这些词语之间的关系可能会变得复杂,取决于句子的结构。依存句法分析的结果是一个树形数据结构,其中动词是根节点,如下图所示:

句子的依存句法分析树结构,其中动词是根节点。
NER
最后,还有 NER。命名实体是句子中的专有名词。计算机已经能够相当准确地判断句子中是否存在这些实体,并对它们进行分类。spacy在文档级别处理 NER,因为实体的名称可能跨越多个词语:
doc = nlp(u"My name is Jack and I live in India.")
entity_types = ((ent.text, ent.label_) for ent in doc.ents)
print(tabulate(entity_types, headers=['Entity', 'Entity Type']))
Output:
Entity Entity Type
-------- -------------
Jack PERSON
India GPE
所以,我们刚刚看到了一些 NLP 管道的基本构建模块。这些管道在各种 NLP 项目中被一致地使用,无论是在机器学习领域还是在深度学习领域。
有什么看起来熟悉的吗?
在前一章中,我们使用了其中一些 NLP 管道构建块,第三章,使用 word2vec 的词表示,来构建我们的 word2vec 模型。对 NLP 管道构建块的更深入解释帮助我们在项目中迈出下一步,因为我们寻求部署越来越复杂的模型!
就像本书中关于Python 深度学习项目的其他内容一样,我们鼓励你也尝试将之前的处理流程与数据科学职业中所处理的用例相结合。现在,让我们使用这些管道实现一个聊天机器人!
构建对话机器人
在本节中,我们将学习一些基本的统计建模方法,以构建一个信息检索系统,使用词频-逆文档频率(TF-IDF),我们可以将其与 NLP 管道结合使用,构建功能齐全的聊天机器人。此外,稍后我们将学习构建一个更为高级的对话机器人,能够提取特定的信息,比如位置、捕获时间等,使用命名实体识别(NER)。
什么是 TF-IDF?
TF-IDF 是一种将文档表示为特征向量的方式。那么它们到底是什么呢?TF-IDF 可以理解为 词频 (TF) 和 逆文档频率 (IDF) 的修改版。TF 是特定单词在给定文档中出现的次数。TF-IDF 背后的概念是根据一个词汇出现在多少个文档中来减少它的权重。这里的核心思想是,出现在很多不同文档中的词汇很可能不重要,或者对 NLP 任务(如文档分类)没有什么有用的信息。
准备数据集
如果我们考虑使用 TF-IDF 方法构建一个聊天机器人,我们首先需要构建一个支持带标签训练数据的数据结构。现在,让我们以一个聊天机器人的例子为例,假设它是用来回答用户提问的。
在这种情况下,通过使用历史数据,我们可以形成一个数据集,其中包含两列,一列是问题,另一列是该问题的答案,如下表所示:
| 问题 | 答案 |
|---|---|
| 你们店什么时候开门? | 我们的营业时间是工作日早上 9:00 至晚上 9:00,周末是早上 11:00 到午夜 12:00。 |
| 今天的特价是什么? | 今天我们提供各种意大利面,配上特制酱料,还有更多其他面包店的选项。 |
| 美式咖啡多少钱? | 一杯单份美式咖啡的价格是 1.4 美元,双份是 2.3 美元。 |
| 你们卖冰淇淋吗? | 我们确实有甜点,比如冰淇淋、布朗尼和糕点。 |
让我们继续考虑前面的例子,把它看作一个样本数据集。它是一个非常小的例子,而在原始的假设场景中,我们会有一个更大的数据集来处理。典型的流程如下:用户将与机器人互动,并输入关于商店的随机查询。机器人会将查询发送给 NLP 引擎,使用 API,然后由 NLP 模型决定针对新查询(测试数据)返回什么内容。参考我们的数据集,所有问题都是训练数据,而答案是标签。在出现新的查询时,TF-IDF 算法会将其与数据集中某个问题进行匹配,并给出一个置信度分数,这个分数告诉我们用户提问的新问题与数据集中的某个特定问题相近,针对该问题的答案就是我们的机器人返回的答案。
让我们进一步考虑前面的例子。当用户查询:“我能买一杯美式咖啡吗?顺便问一下,多少钱?”时,我们可以看到像 I、an 和 it 这些词汇,在其他问题中也会有较高的出现频率。
现在,如果我们匹配其余的重要词汇,我们会发现这个问题最接近:"美式咖啡多少钱?" 所以,我们的机器人会回复这个问题的历史答案:“一杯单份美式咖啡的价格是 1.4 美元,双份是 2.3 美元。”
实现
在之前提到的以表格格式创建数据结构之后,我们将在每次用户查询我们的机器人时计算预测的答案。我们从数据集中加载所有问题-答案对。
让我们使用pandas加载我们的 CSV 文件,并对数据集进行一些预处理:
import pandas as pd
filepath = 'sample_data.csv'
csv_reader=pd.read_csv(filepath)
question_list = csv_reader[csv_reader.columns[0]].values.tolist()
answers_list = csv_reader[csv_reader.columns[1]].values.tolist()
query= 'Can I get an Americano, btw how much it will cost ?'
该项目的代码可以在github.com/PacktPublishing/Python-Deep-Learning-Projects/tree/master/Chapter04/tfidf_version找到。
创建向量化器
现在,让我们初始化 TF-IDF 向量化器并定义一些参数:
-
min_df:在构建词汇表时,忽略文档频率低于给定阈值的术语 -
ngram_range:配置我们的向量化器一次捕捉n个单词 -
norm:用于使用 L1 或 L2 范数对术语向量进行归一化 -
encoding:处理 Unicode 字符
还有许多其他参数,您可以查看、配置并进行实验:
from sklearn.feature_extraction.text import TfidfVectorizer
vectorizer = TfidfVectorizer(min_df=0, ngram_range=(2, 4), strip_accents='unicode',norm='l2' , encoding='ISO-8859-1')
现在,我们将在问题上训练模型:
# We create an array for our train data set (questions)
X_train = vectorizer.fit_transform(np.array([''.join(que) for que in question_list]))
# Next step is to transform the query sent by user to bot (test data)
X_query=vectorizer.transform(query)
处理查询
为了处理查询,我们需要找到它与其他问题的相似度。我们通过计算训练数据矩阵与查询数据的转置的点积来实现这一点:
XX_similarity=np.dot(X_train.todense(), X_query.transpose().todense())
现在,我们将查询与训练数据的相似度提取为一个列表:
XX_sim_scores= np.array(XX_similarity).flatten().tolist()
排名结果
我们为查询创建了一个相似度排序字典:
dict_sim= dict(enumerate(XX_sim_scores))
sorted_dict_sim = sorted(dict_sim.items(), key=operator.itemgetter(1), reverse =True)
最后,在排序的字典中,我们检查最相似问题的索引,以及该索引在答案列中的响应值。如果没有找到任何结果,我们可以返回默认答案:
if sorted_dict_sim[0][1]==0:
print("Sorry I have no answer, please try asking again in a nicer way :)")
elif sorted_dic_sim[0][1]>0:
print answer_list [sorted_dic_sim[0][0]]
使用 NER 的高级聊天机器人
我们刚刚创建了一个非常基本的聊天机器人,它能够理解用户的查询,并根据情况做出回应。但它还无法理解上下文,因为它无法提取诸如产品名称、地点或其他任何实体的信息。
为了构建一个理解上下文(意图)并能够提取实体的机器人,我们需要一个 NLP 管道,能够执行意图分类和 NER 提取,并提供准确的响应。
牢记目标!这就是我们开放领域问答机器人所追求的目标。
为此,我们将使用一个名为 Rasa NLU 的开源项目(github.com/RasaHQ/rasa_nlu)。
Rasa NLU 是一个自然语言理解工具,用于理解文本,尤其是短文本中所表达的内容。例如,假设系统接收到类似以下的简短消息:
"I'm looking for an Italian restaurant in the center of town"
在这种情况下,系统返回以下内容:
intent: search_restaurant
entities:
- cuisine : Italian
- location : center of town
因此,通过利用 RASA 的强大功能,我们可以构建一个能够进行意图分类和 NER 提取的聊天机器人。
太好了,我们开始吧!
该项目的代码可以在github.com/PacktPublishing/Python-Deep-Learning-Projects/tree/master/Chapter04/rasa_version找到。
安装 Rasa
使用以下命令在我们的本地环境或服务器中安装 Rasa:
pip install rasa_nlu
pip install coloredlogs sklearn_crfsuite spacy
python -m spacy download en
如果安装失败,可以通过查看nlu.rasa.com/installation.html中的详细方法来解决。
Rasa 使用多种 NLP 管道,例如spacy、sklearn或 MITIE。你可以选择其中任何一个,或者构建自己的自定义管道,其中可以包含任何深度模型,例如我们在前一章节中创建的带有 word2vec 的 CNN。在我们的案例中,我们将使用spacy和sklearn。
准备数据集
在我们之前的项目中,我们创建了一个 CSV 文件数据集,包含了问题和答案对的两列。我们需要再次执行此操作,但使用不同的格式。在这种情况下,我们需要将问题与其意图相关联,如下图所示,这样我们就有一个标注为greet的hello问题。同样,我们将为所有问题标注各自的意图。
一旦我们准备好了所有问题和意图的形式,就需要标注实体。在这种情况下,如下图所示,我们有一个location实体,值为centre,以及一个cuisine实体,值为mexican:

这张图展示了我们为聊天机器人准备的数据内容。最主要的是所有意图的列表,我们需要让我们的机器人理解这些意图。然后,我们为每个意图提供相应的示例语句。最右侧部分表示具体实体的标注,实体的标签是“location”和“cuisine”,在这个例子中就是这样。
要将数据输入到 Rasa 中,我们需要将这些信息存储为特定的 JSON 格式,格式如下所示:
# intent_list : Only intent part
[
{
"text": "hey",
"intent": "greet"
},
{
"text": "hello",
"intent": "greet"
}
]
# entity_list : Intent with entities
[{
"text": "show me indian restaurants",
"intent": "restaurant_search",
"entities": [
{
"start": 8,
"end": 15,
"value": "indian",
"entity": "cuisine"
}
]
},
]
JSON 的最终版本应该具有如下结构:
{
"rasa_nlu_data": {
"entity_examples": [entity_list],
"intent_examples": [intent_list]
}
}
为了简化操作,提供了一个在线工具,您可以将所有数据输入并标注,然后下载 JSON 版本。您可以按照github.com/RasaHQ/rasa-nlu-trainer上的说明在本地运行编辑器,或者直接使用其在线版本:rasahq.github.io/rasa-nlu-trainer/。
将此 JSON 文件保存为restaurant.json,并存放在当前工作目录中。
训练模型
现在我们将创建一个配置文件。这个配置文件将定义在训练和构建模型过程中使用的管道。
在您的工作目录中创建一个名为config_spacy.yml的文件,并将以下代码插入其中:
language: "en"
pipeline: "spacy_sklearn"
fine_tune_spacy_ner: true
了解代码:spaCy 配置自定义是有原因的。其他数据科学家已经发现能够在这里更改值有一定的用处,随着你对这项技术的熟悉,探索这一点是一个很好的实践。这里有一个配置的庞大列表,你可以在nlu.rasa.com/config.html查看。
这个配置表示我们将使用英语语言模型,并且后台运行的管道将是基于 spaCy 和 scikit-learn。现在,为了开始训练过程,执行以下命令:
python -m rasa_nlu.train \
--config config_spacy.yml \
--data restaurant.json \
--path projects
这将配置文件和训练数据文件作为输入。--path参数是存储训练模型的位置。
一旦模型训练过程完成,你将看到一个新文件夹,命名为projects/default/model_YYYYMMDD-HHMMSS格式,包含训练完成时的时间戳。完整的项目结构将如以下截图所示:

训练过程完成后的文件夹结构。模型文件夹将包含所有在训练过程中学到的二进制文件和元数据。
部署模型
现在是让你的机器人上线的时候了!使用 Rasa 时,你不需要编写任何 API 服务——一切都可以在包内完成。所以,为了将训练好的模型暴露为服务,你需要执行以下命令,该命令需要存储的训练模型路径:
python -m rasa_nlu.server --path projects
如果一切顺利,将会在5000端口暴露一个 RESTful API,你应该能在控制台屏幕上看到以下日志:
2018-05-23 21:34:23+0530 [-] Log opened.
2018-05-23 21:34:23+0530 [-] Site starting on 5000
2018-05-23 21:34:23+0530 [-] Starting factory <twisted.web.server.Site instance at 0x1062207e8>
要访问 API,你可以使用以下命令。我们正在查询模型,提出一个语句,比如"I am looking for Mexican food"(我在寻找墨西哥菜):
curl -X POST localhost:5000/parse -d '{"q":"I am looking for Mexican food"}' | python -m json.tool
Output:
{
"entities": [
{
"confidence": 0.5348393725109971,
"end": 24,
"entity": "cuisine",
"extractor": "ner_crf",
"start": 17,
"value": "mexican"
}
],
"intent": {
"confidence": 0.7584285478135262,
"name": "restaurant_search"
},
"intent_ranking": [
{
"confidence": 0.7584285478135262,
"name": "restaurant_search"
},
{
"confidence": 0.11009204166074991,
"name": "goodbye"
},
{
"confidence": 0.08219245368495268,
"name": "affirm"
},
{
"confidence": 0.049286956840770876,
"name": "greet"
}
],
"model": "model_20180523-213216",
"project": "default",
"text": "I am looking for Mexican food"
}
所以在这里,我们可以看到模型在意图分类和实体提取过程中表现得相当准确。它能够以 75.8%的准确率将意图分类为restaurant_search,并且能够检测到cuisine实体,值为mexican。
服务化聊天机器人
到目前为止,我们已经看到了如何使用TF-IDF和Rasa NLU两种方法来构建聊天机器人。接下来,我们将把它们暴露为 API。这个简单聊天机器人框架的架构将如下所示:

这个聊天机器人流程说明了我们可以将任何用户界面(如 Slack、Skype 等)与我们暴露的chatbot_api进行集成。在后台,我们可以设置任意数量的算法,如TFIDF和RASA。
请参考本章节的 Packt 仓库(可访问 github.com/PacktPublishing/Python-Deep-Learning-Projects/tree/master/Chapter04)获取 API 代码,并查看 chatbot_api.py 文件。在这里,我们实现了一个通用 API,可以加载两种版本的机器人,你现在可以在其基础上构建完整的框架。
执行 API 服务时,请按照以下步骤操作:
- 使用以下命令进入章节目录:
cd Chapter04/
- 这将使 Rasa 模块暴露在
localhost:5000。如果您尚未训练 Rasa 引擎,请使用以下命令:
python -m rasa_nlu.server --path ./rasa_version/projects
- 在一个单独的控制台中,执行以下命令。这将在
localhost:8080暴露一个 API:
python chatbot_api.py
- 现在,您的聊天机器人已准备好通过 API 进行访问。试试以下操作:
-
- 调用以下 API 执行 TFIDF 版本:
curl http://localhost:8080/version1?query=Can I get an Americano
-
- 调用以下 API 执行 Rasa 版本:
http://localhost:8080/version2?query=where is Indian cafe
总结
在这个项目中,我们被要求创建一个自然语言处理管道,为开放领域问答的聊天机器人提供支持。一个(假设的)餐饮连锁公司在其网站上拥有大量基于文本的数据,包括菜单、历史、位置、营业时间等信息,他们希望为网站访客提供一个查询框,允许他们提问。我们的深度学习 NLP 聊天机器人将根据这些信息找到相关内容,并返回给访客。
我们首先展示了如何构建一个简单的 FAQ 聊天机器人,该机器人接收随机查询,将其与预定义问题匹配,并返回一个响应,带有表示输入问题与数据库中问题相似度的置信度评分。但这仅仅是通向我们真正目标的第一步,我们的目标是创建一个能够捕捉问题意图并准备适当响应的聊天机器人。
我们探索了一种命名实体识别(NER)方法,赋予我们所需的能力,快速对输入文本进行分类,然后匹配到相关的响应内容。这种方法适合我们的目标,即支持开放领域问答,并且能够利用大量不断变化的非结构化数据,而无需使用硬编码的启发式方法(就像我们假设的餐厅例子中一样)。
我们学会了使用 NLP 模型的基本构建模块,包括预处理、分词和 POS 标注。我们利用这些理解,构建了一个能够读取非结构化文本的系统,以便理解针对特定问题的答案。
具体来说,我们在这个项目中获得了以下技能:
-
使用统计建模构建基于 FAQ 的基础聊天机器人框架,能够检测意图和实体以回答开放领域问题。
-
生成句子的密集表示
-
构建一个文档读取器,从非结构化文本中提取答案
-
学习了如何将深度学习模型集成到经典的 NLP 管道中。
这些技能在你的职业生涯中将非常有用,因为你将会遇到类似的商业应用场景,同时,随着对话式用户界面日益流行,它们也会变得更加重要。做得好——让我们来看看下一项目 Python 深度学习项目会带来什么!
第五章:用于构建聊天机器人的序列到序列模型
我们学到了很多东西,并且做了一些有价值的工作!在我们假设的商业用例的演进过程中,本章直接建立在 第四章 构建聊天机器人 NLP 管道 的基础上,我们在该章中创建了 自然语言处理(NLP)管道。我们到目前为止在计算语言学中学到的技能应该能够让我们有信心扩展到本书中的训练示例之外,并着手处理下一个项目。我们将为我们假设的餐饮连锁店构建一个更先进的聊天机器人,自动化接听电话订单的过程。
这个需求意味着我们需要结合我们迄今为止所学的一些技术。但在这个项目中,我们将学习如何制作一个更加上下文敏感且强大的聊天机器人,以便将其集成到这个假设的更大系统中。通过在这个训练示例中展示我们的掌握程度,我们将有信心在实际情况下执行这个任务。
在前几章中,我们学习了表示学习方法,如 word2vec,并了解了如何将其与一种名为 卷积神经网络(CNN)的深度学习算法结合使用。但是,使用 CNN 构建语言模型时存在一些限制,如下所示:
-
该模型将无法保持状态信息
-
句子的长度需要在输入值和输出值之间保持固定大小
-
CNN 有时无法充分处理复杂的序列上下文
-
循环神经网络(RNNs)在建模序列信息方面表现更好
因此,为了解决这些问题,我们有一个替代算法,它专门设计来处理以序列形式输入的数据(包括单词序列或字符序列)。这种类型的算法被称为 RNN。
在本章中,我们将做以下事情:
-
学习 RNN 及其各种形式
-
使用 RNN 创建一个语言模型实现
-
基于 长短期记忆(LSTM)模型构建我们的直觉
-
创建 LSTM 语言模型实现并与 RNN 模型进行比较
-
基于 LSTM 单元实现一个编码器-解码器 RNN,用于简单的问答任务序列
定义目标:构建一个具有记忆功能的更强大的聊天机器人,以提供更具上下文相关性的正确回答。
让我们开始吧!
介绍 RNN
RNN 是一种深度学习模型架构,专门为序列数据设计。该模型的目的是通过使用一个小窗口来遍历语料库,从而提取文本中单词和字符的相关特征。
RNN 对序列中的每个项应用非线性函数。这被称为 RNN 单元或步,在我们的例子中,这些项是序列中的单词或字符。RNN 中的输出是通过对序列中每个元素应用 RNN 单元的输出得到的。关于使用文本数据作为输入的自然语言处理和聊天机器人,模型的输出是连续的字符或单词。
每个 RNN 单元都包含一个内部记忆,用于总结它目前为止看到的序列的历史。
该图帮助我们可视化 RNN 模型架构:

RNN 模型架构的经典版本。
RNN 的核心目的在于引入一种反馈机制,通过使用固定权重的反馈结构来实现上下文建模。这样做的目的是建立当前映射到先前版本之间的连接。基本上,它使用序列的早期版本来指导后续版本。
这非常聪明;然而,它也并非没有挑战。梯度爆炸和梯度消失使得在处理复杂时间序列问题时,训练这些类型的模型变得极其令人沮丧。
有一个很好的参考资料深入讲解了梯度消失和梯度爆炸问题,并提供了可行解决方案的技术解释,可以参考 Sepp 1998 年的工作(dl.acm.org/citation.cfm?id=355233)。
另一个发现的问题是,RNN 只会捕捉到两种时间结构中的一种:短期结构或长期结构。然而,最佳模型的性能需要能够同时从两种类型的特征(短期和长期)中学习。解决方案是将基础的 RNN 单元更换为门控递归单元(GRU)或 LSTM 单元。
若要了解更多关于 GRU 的信息,请参考www.wildml.com/2015/10/recurrent-neural-network-tutorial-part-4-implementing-a-grulstm-rnn-with-python-and-theano/,或者,若要了解更多关于 LSTM 的内容,请参考colah.github.io/posts/2015-08-Understanding-LSTMs/。
我们将在本章后续部分详细探讨 LSTM 架构。让我们先直观地了解 LSTM 的价值,这将有助于我们实现目标。
RNN 架构
我们将主要使用 LSTM 单元,因为它在大多数自然语言处理任务中表现更好。LSTM 在 RNN 架构中的主要优点是,它能够在保持记忆的同时进行长序列的模型训练。为了解决梯度问题,LSTM 包括更多的门控机制,有效控制对单元状态的访问。
我们发现 Colah 的博客文章(colah.github.io/posts/2015-08-Understanding-LSTMs/)是一个很好的地方,可以帮助理解 LSTM 的工作原理。
这些 RNN 的小型 LSTM 单元可以以多种形式组合来解决各种类型的使用案例。RNN 在结合不同输入和输出模式方面非常灵活,具体如下:
-
多对一:该模型将完整的输入序列作为输入,做出单一的预测。这在情感分析模型中使用。
-
一对多:该模型将单一的输入(例如一个日期)转化为生成一个序列字符串,如“日”、“月”或“年”。
-
多对多:这是一个序列到序列(seq2seq)模型,它将整个序列作为输入,转换为第二个序列的形式,正如问答系统所做的那样。
这张图很好地展示了这些关系:

在本章中,我们将重点关注多对多关系,也称为 seq2seq 架构,以构建一个问答聊天机器人。解决 seq2seq 问题的标准 RNN 方法包括三个主要组成部分:
-
编码器:这些将输入句子转换为某种抽象的编码表示
-
隐藏层:在这里处理编码后的句子转换表示
-
解码器:这些输出解码后的目标序列
让我们来看看以下图表:

这是构建编码解码模型的示意图,该模型将输入文本(问题)传递到编码器,在中间步骤中进行转换,然后与解码器进行映射,解码器表示相应的文本(答案)。
让我们通过首先实现 RNN 模型的基本形式,来建立对 RNN 的直觉。
实现基本的 RNN
在本节中,我们将实现一个语言模型,使用基础的 RNN 进行情感分类。模型的代码文件可以在github.com/PacktPublishing/Python-Deep-Learning-Projects/blob/master/Chapter05/1.%20rnn.py找到。
导入所有依赖项
这段代码导入了 TensorFlow 和 RNN 的关键依赖项:
from utils import *
import tensorflow as tf
from sklearn.cross_validation import train_test_split
import time
准备数据集
在本项目中,我们将使用来自 Rotten Tomatoes 的电影评论数据(www.cs.cornell.edu/people/pabo/movie-review-data/)。该数据集包含 10,662 个示例评论句子,约一半是正面评价,一半是负面评价。数据集的词汇量约为 20,000 个单词。我们将使用sklearn包装器从原始文件加载数据集,然后使用separate_dataset()辅助函数清理数据集,并将其从原始形式转换为分离的列表结构:
#Helper function
def separate_dataset(trainset,ratio=0.5):
datastring = []
datatarget = []
for i in range(int(len(trainset.data)*ratio)):
data_ = trainset.data[i].split('\n')
data_ = list(filter(None, data_))
for n in range(len(data_)):
data_[n] = clearstring(data_[n])
datastring += data_
for n in range(len(data_)):
datatarget.append(trainset.target[i])
return datastring, datatarget
在这里,trainset是一个存储所有文本数据和情感标签数据的对象:
trainset = sklearn.datasets.load_files(container_path = './data', encoding = 'UTF-8')
trainset.data, trainset.target = separate_dataset(trainset,1.0)
print (trainset.target_names)
print ('No of training data' , len(trainset.data))
print ('No. of test data' , len(trainset.target))
# Output: ['negative', 'positive']
No of training data 10662
No of test data 10662
现在我们将标签转化为一热编码。
理解一热编码向量的维度很重要。由于我们有10662个独立的句子,且有两个情感类别,negative和positive,因此我们的“一热”向量的大小将是[10662, 2]。
我们将使用一个流行的train_test_split() sklearn 包装器来随机打乱数据,并将数据集分为两个部分:training集和test集。进一步地,借助另一个build_dataset()辅助函数,我们将使用基于词频的方式创建词汇表:
ONEHOT = np.zeros((len(trainset.data),len(trainset.target_names)))
ONEHOT[np.arange(len(trainset.data)),trainset.target] = 1.0
train_X, test_X, train_Y, test_Y, train_onehot, test_onehot = train_test_split(trainset.data, trainset.target,
ONEHOT, test_size = 0.2)
concat = ' '.join(trainset.data).split()
vocabulary_size = len(list(set(concat)))
data, count, dictionary, rev_dictionary = build_dataset(concat, vocabulary_size)
print('vocab from size: %d'%(vocabulary_size))
print('Most common words', count[4:10])
print('Sample data', data[:10], [rev_dictionary[i] for i in data[:10]])
# OUTPUT:vocab from size: 20465
'Most common words', [(u'the', 10129), (u'a', 7312), (u'and', 6199), (u'of', 6063), (u'to', 4233), (u'is', 3378)]
'Sample data':
[4, 662, 9, 2543, 8, 22, 4, 3558, 18064, 98] -->
[u'the', u'rock', u'is', u'destined', u'to', u'be', u'the', u'21st', u'centurys', u'new']
你也可以尝试将任何嵌入模型放入这里,以提高模型的准确性。
在为 RNN 模型准备数据集时,有一些重要的事项需要记住。我们需要在词汇表中明确添加特殊标签,以跟踪句子的开始、额外的填充、句子的结束以及任何未知的词汇。因此,我们在词汇字典中为特殊标签保留了以下位置:
# Tag to mark the beginning of the sentence
'GO' = 0th position
# Tag to add extra padding in the sentence
'PAD'= 1st position
# Tag to mark the end of the sentence
'EOS'= 2nd position
# Tag to mark the unknown word
'UNK'= 3rd position
超参数
我们将为模型定义一些超参数,如下所示:
size_layer = 128
num_layers = 2
embedded_size = 128
dimension_output = len(trainset.target_names)
learning_rate = 1e-3
maxlen = 50
batch_size = 128
定义一个基本的 RNN 单元模型
现在我们将创建 RNN 模型,它需要几个输入参数,包括以下内容:
-
size_layer:RNN 单元中的单元数 -
num_layers:隐藏层的数量 -
embedded_size:嵌入的大小 -
dict_size:词汇表大小 -
dimension_output:我们需要分类的类别数 -
learning_rate:优化算法的学习率
我们的 RNN 模型架构由以下部分组成:
-
两个占位符;一个用于将序列数据输入模型,另一个用于输出
-
用于存储从词典中查找嵌入的变量
-
然后,添加包含多个基本 RNN 单元的 RNN 层
-
创建权重和偏差变量
-
计算
logits -
计算损失
-
添加 Adam 优化器
-
计算预测和准确率
这个模型类似于前一章中创建的 CNN 模型,第四章,构建自然语言处理管道以创建聊天机器人,除了 RNN 单元部分:
class Model:
def __init__(self, size_layer, num_layers, embedded_size,
dict_size, dimension_output, learning_rate):
def cells(reuse=False):
return tf.nn.rnn_cell.BasicRNNCell(size_layer,reuse=reuse)
self.X = tf.placeholder(tf.int32, [None, None])
self.Y = tf.placeholder(tf.float32, [None, dimension_output])
encoder_embeddings = tf.Variable(tf.random_uniform([dict_size, embedded_size], -1, 1))
encoder_embedded = tf.nn.embedding_lookup(encoder_embeddings, self.X)
rnn_cells = tf.nn.rnn_cell.MultiRNNCell([cells() for _ in range(num_layers)])
outputs, _ = tf.nn.dynamic_rnn(rnn_cells, encoder_embedded, dtype = tf.float32)
W = tf.get_variable('w',shape=(size_layer, dimension_output),initializer=tf.orthogonal_initializer())
b = tf.get_variable('b',shape=(dimension_output),initializer=tf.zeros_initializer())
self.logits = tf.matmul(outputs[:, -1], W) + b
self.cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits = self.logits, labels = self.Y))
self.optimizer = tf.train.AdamOptimizer(learning_rate = learning_rate).minimize(self.cost)
correct_pred = tf.equal(tf.argmax(self.logits, 1), tf.argmax(self.Y, 1))
self.accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32))
在这个模型中,数据从我们在步骤 1中创建的变量流动。接着,它进入在步骤 2中定义的嵌入层,然后是我们的 RNN 层,它在两个隐藏层的 RNN 单元中执行计算。之后,logits通过进行权重与 RNN 层输出的矩阵乘法并加上偏差来计算。最后一步是我们定义cost函数;我们将使用softmax_cross_entropy函数。
这是计算后完整模型的样子:

TensorBoard 图形可视化 RNN 架构
下图表示前面截图中的 RNN 块结构。在这个架构中,我们有两个 RNN 单元被集成在隐藏层中:

TensorBoard 可视化 RNN 块,其中包含代码中定义的 2 个隐藏层
训练 RNN 模型
现在我们已经定义了模型架构,接下来让我们训练模型。我们从 TensorFlow 图初始化开始,并按以下步骤执行训练:
tf.reset_default_graph()
sess = tf.InteractiveSession()
model = Model(size_layer,num_layers,embedded_size,vocabulary_size+4,dimension_output,learning_rate)
sess.run(tf.global_variables_initializer())
EARLY_STOPPING, CURRENT_CHECKPOINT, CURRENT_ACC, EPOCH = 5, 0, 0, 0
while True:
lasttime = time.time()
if CURRENT_CHECKPOINT == EARLY_STOPPING:
print('break epoch:%d\n'%(EPOCH))
break
train_acc, train_loss, test_acc, test_loss = 0, 0, 0, 0
for i in range(0, (len(train_X) // batch_size) * batch_size, batch_size):
batch_x = str_idx(train_X[i:i+batch_size],dictionary,maxlen)
acc, loss, _ = sess.run([model.accuracy, model.cost, model.optimizer],
feed_dict = {model.X : batch_x, model.Y : train_onehot[i:i+batch_size]})
train_loss += loss
train_acc += acc
for i in range(0, (len(test_X) // batch_size) * batch_size, batch_size):
batch_x = str_idx(test_X[i:i+batch_size],dictionary,maxlen)
acc, loss = sess.run([model.accuracy, model.cost],
feed_dict = {model.X : batch_x, model.Y : train_onehot[i:i+batch_size]})
test_loss += loss
test_acc += acc
train_loss /= (len(train_X) // batch_size)
train_acc /= (len(train_X) // batch_size)
test_loss /= (len(test_X) // batch_size)
test_acc /= (len(test_X) // batch_size)
if test_acc > CURRENT_ACC:
print('epoch: %d, pass acc: %f, current acc: %f'%(EPOCH,CURRENT_ACC, test_acc))
CURRENT_ACC = test_acc
CURRENT_CHECKPOINT = 0
else:
CURRENT_CHECKPOINT += 1
print('time taken:', time.time()-lasttime)
print('epoch: %d, training loss: %f, training acc: %f, valid loss: %f, valid acc: %f\n'%(EPOCH,train_loss, train_acc,test_loss,test_acc))
EPOCH += 1
在训练 RNN 模型时,我们可以看到每个 epoch 的日志,如下所示:

RNN 模型评估
让我们来看看结果。一旦模型训练完成,我们就可以输入本章前面准备好的测试数据并评估预测结果。在这种情况下,我们将使用几种不同的指标来评估模型:精度、召回率和 F1 分数。
为了评估您的模型,选择合适的指标非常重要——与准确率分数相比,F1 分数被认为更为实用。
以下是一些帮助你简单理解这些概念的关键点:
-
准确率:正确预测的数量除以已评估的总样本数。
-
精度:高精度意味着你正确识别了几乎所有的正例;低精度意味着你经常错误地预测出一个正例,而实际上并没有。
-
召回率:高召回率意味着你正确预测了数据中几乎所有的真实正例;低召回率意味着你经常漏掉实际存在的正例。
-
F1 分数:召回率和精度的平衡调和均值,给予这两个指标相等的权重。F1 分数越高,表现越好。
现在我们将通过提供包含词汇表和文本最大长度的测试数据来执行模型。这将生成logits值,我们将利用这些值来生成评估指标:
logits = sess.run(model.logits, feed_dict={model.X:str_idx(test_X,dictionary,maxlen)})
print(metrics.classification_report(test_Y, np.argmax(logits,1), target_names = trainset.target_names))
输出结果如下:

在这里,我们可以看到使用基本 RNN 单元时,我们的平均f1-score是 66%。让我们看看是否通过使用其他 RNN 架构变体能有所改进。
LSTM 架构
为了更有效地建模序列数据,并且克服梯度问题的限制,研究人员创造了 LSTM 变体,这是在之前的 RNN 模型架构基础上发展出来的。LSTM 由于引入了控制细胞内存过程的门控机制,因此能够实现更好的性能。以下图示展示了一个 LSTM 单元:

LSTM 单元(来源:http://colah.github.io/posts/2015-08-Understanding-LSTMs)
LSTM 由三个主要部分组成,在上述图示中标记为1、2和3:
-
遗忘门 f(t):该门控机制在 LSTM 单元架构中提供了忘记不需要信息的能力。Sigmoid 激活函数接受输入X(t)和h(t-1),并有效地决定通过传递0来移除旧的输出信息。该门控的输出是f(t)c(t-1)*。
-
从新输入的 X(t) 中,需要保留的信息将在下一步中存储在单元状态中。此过程中使用了一个 sigmoid 激活函数来更新或忽略新信息的部分。接下来,通过 tanh 激活函数创建一个包含新输入所有可能值的向量。新单元状态是这两个值的乘积,然后将这个新记忆添加到旧记忆 c(t-1) 中,得出 c(t)。
-
LSTM 单元的最后一个过程是确定最终输出。一个 sigmoid 层决定输出单元状态的哪些部分。然后,我们将单元状态通过 tanh 激活生成所有可能的值,并将其与 sigmoid 门的输出相乘,以根据非线性函数产生所需的输出。
LSTM 单元过程中的这三步产生了显著的效果,即模型可以被训练去学习哪些信息需要保存在长期记忆中,哪些信息需要被遗忘。真是天才!
实现 LSTM 模型
我们之前执行的构建基本 RNN 模型的过程将保持不变,唯一的区别是模型定义部分。所以,让我们实现这个并检查新模型的性能。
模型的代码可以在 github.com/PacktPublishing/Python-Deep-Learning-Projects/blob/master/Chapter05/2.%20rnn_lstm.py 查看。
定义我们的 LSTM 模型
再次,大部分代码将保持不变——唯一的主要变化是使用 tf.nn.rnn_cell.LSTMCell(),而不是 tf.nn.rnn_cell.BasicRNNCell()。在初始化 LSTM 单元时,我们使用了一个正交初始化器,它会生成一个随机的正交矩阵,这是对抗梯度爆炸和消失的有效方法:
class Model:
def __init__(self, size_layer, num_layers, embedded_size,
dict_size, dimension_output, learning_rate):
def cells(reuse=False):
return tf.nn.rnn_cell.LSTMCell(size_layer,initializer=tf.orthogonal_initializer(),reuse=reuse)
self.X = tf.placeholder(tf.int32, [None, None])
self.Y = tf.placeholder(tf.float32, [None, dimension_output])
encoder_embeddings = tf.Variable(tf.random_uniform([dict_size, embedded_size], -1, 1))
encoder_embedded = tf.nn.embedding_lookup(encoder_embeddings, self.X)
rnn_cells = tf.nn.rnn_cell.MultiRNNCell([cells() for _ in range(num_layers)])
outputs, _ = tf.nn.dynamic_rnn(rnn_cells, encoder_embedded, dtype = tf.float32)
W = tf.get_variable('w',shape=(size_layer, dimension_output),initializer=tf.orthogonal_initializer())
b = tf.get_variable('b',shape=(dimension_output),initializer=tf.zeros_initializer())
self.logits = tf.matmul(outputs[:, -1], W) + b
self.cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits = self.logits, labels = self.Y))
self.optimizer = tf.train.AdamOptimizer(learning_rate = learning_rate).minimize(self.cost)
correct_pred = tf.equal(tf.argmax(self.logits, 1), tf.argmax(self.Y, 1))
self.accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32))
所以,这是 LSTM 模型的架构——与之前的基本模型几乎相同,唯一的不同是增加了 LSTM 单元在RNN 块中的位置:

训练 LSTM 模型
现在,我们已经建立了 LSTM 直觉并构建了模型,让我们按照以下方式训练它:
EARLY_STOPPING, CURRENT_CHECKPOINT, CURRENT_ACC, EPOCH = 5, 0, 0, 0
while True:
lasttime = time.time()
if CURRENT_CHECKPOINT == EARLY_STOPPING:
print('break epoch:%d\n'%(EPOCH))
break
train_acc, train_loss, test_acc, test_loss = 0, 0, 0, 0
for i in range(0, (len(train_X) // batch_size) * batch_size, batch_size):
batch_x = str_idx(train_X[i:i+batch_size],dictionary,maxlen)
acc, loss, _ = sess.run([model.accuracy, model.cost, model.optimizer],
feed_dict = {model.X : batch_x, model.Y : train_onehot[i:i+batch_size]})
train_loss += loss
train_acc += acc
for i in range(0, (len(test_X) // batch_size) * batch_size, batch_size):
batch_x = str_idx(test_X[i:i+batch_size],dictionary,maxlen)
acc, loss = sess.run([model.accuracy, model.cost],
feed_dict = {model.X : batch_x, model.Y : train_onehot[i:i+batch_size]})
test_loss += loss
test_acc += acc
train_loss /= (len(train_X) // batch_size)
train_acc /= (len(train_X) // batch_size)
test_loss /= (len(test_X) // batch_size)
test_acc /= (len(test_X) // batch_size)
if test_acc > CURRENT_ACC:
print('epoch: %d, pass acc: %f, current acc: %f'%(EPOCH,CURRENT_ACC, test_acc))
CURRENT_ACC = test_acc
CURRENT_CHECKPOINT = 0
else:
CURRENT_CHECKPOINT += 1
print('time taken:', time.time()-lasttime)
print('epoch: %d, training loss: %f, training acc: %f, valid loss: %f, valid acc: %f\n'%(EPOCH,train_loss,
train_acc,test_loss,
test_acc))
EPOCH += 1
在 LSTM 模型训练时,我们可以看到每个 epoch 的日志,如下截图所示:

以下是输出结果:
('time taken:', 18.061596155166626)
epoch: 10, training loss: 0.015714, training acc: 0.994910, valid loss: 4.252270, valid acc: 0.500000
('time taken:', 17.786305904388428)
epoch: 11, training loss: 0.011198, training acc: 0.995975, valid loss: 4.644272, valid acc: 0.502441
('time taken:', 19.031064987182617)
epoch: 12, training loss: 0.009245, training acc: 0.996686, valid loss: 4.575824, valid acc: 0.499512
('time taken:', 16.996762990951538)
epoch: 13, training loss: 0.006528, training acc: 0.997751, valid loss: 4.449901, valid acc: 0.501953
('time taken:', 17.008245944976807)
epoch: 14, training loss: 0.011770, training acc: 0.995739, valid loss: 4.282045, valid acc: 0.499023
break epoch:15
你会注意到,即使使用相同的模型配置,基于 LSTM 的模型所需的训练时间仍然比 RNN 模型要长。
LSTM 模型的评估
现在,让我们再次计算指标并比较性能:
logits = sess.run(model.logits, feed_dict={model.X:str_idx(test_X,dictionary,maxlen)})
print(metrics.classification_report(test_Y, np.argmax(logits,1), target_names = trainset.target_names))
计算的输出如下所示:

因此,我们可以清晰地看到模型性能的提升!现在,使用 LSTM 后,f1-score 提高到了 72%,而在我们之前的基本 RNN 模型中,它为 66%,这意味着提高了 7%,是一个相当不错的进步。
序列到序列模型
在本节中,我们将实现一个基于 LSTM 单元的 seq2seq 模型(编码器-解码器 RNN),用于一个简单的序列到序列的问答任务。这个模型可以训练将输入序列(问题)映射到输出序列(答案),这些答案的长度不一定与问题相同。
这种类型的 seq2seq 模型在其他许多任务中表现出色,如语音识别、机器翻译、问答、神经机器翻译(NMT)和图像描述生成。
以下图帮助我们可视化我们的 seq2seq 模型:

序列到序列(seq2seq)模型的示意图。每个矩形框表示一个 RNN 单元,其中蓝色的是编码器,红色的是解码器。
在编码器-解码器结构中,一个 RNN(蓝色)编码输入序列。编码器输出上下文C,通常是其最终隐藏状态的简单函数。第二个 RNN(红色)解码器计算目标值并生成输出序列。一个关键步骤是让编码器和解码器进行通信。在最简单的方法中,您使用编码器的最后一个隐藏状态来初始化解码器。其他方法则让解码器在解码过程中在不同的时间步访问编码输入的不同部分。
现在,让我们开始进行数据准备、模型构建、训练、调优和评估我们的 seq2seq 模型,看看它的表现如何。
数据准备
在这里,我们将构建我们的问答系统。对于该项目,我们需要一个包含问题和答案对的数据集,如下图所示。两列数据都包含词序列,这正是我们需要输入到 seq2seq 模型中的内容。此外,请注意我们的句子可以具有动态长度:

我们准备的包含问题和答案的数据集
让我们加载它们并使用build_dataset()执行相同的数据处理。最终,我们将得到一个以单词为键的字典,相关值是该单词在相应语料中的出现次数。此外,我们还会得到之前在本章中提到的四个额外的值:
import numpy as np
import tensorflow as tf
import collections
from utils import *
file_path = './conversation_data/'
with open(file_path+'from.txt', 'r') as fopen:
text_from = fopen.read().lower().split('\n')
with open(file_path+'to.txt', 'r') as fopen:
text_to = fopen.read().lower().split('\n')
print('len from: %d, len to: %d'%(len(text_from), len(text_to)))
concat_from = ' '.join(text_from).split()
vocabulary_size_from = len(list(set(concat_from)))
data_from, count_from, dictionary_from, rev_dictionary_from = build_dataset(concat_from, vocabulary_size_from)
concat_to = ' '.join(text_to).split()
vocabulary_size_to = len(list(set(concat_to)))
data_to, count_to, dictionary_to, rev_dictionary_to = build_dataset(concat_to, vocabulary_size_to)
GO = dictionary_from['GO']
PAD = dictionary_from['PAD']
EOS = dictionary_from['EOS']
UNK = dictionary_from['UNK']
定义一个 seq2seq 模型
在本节中,我们将概述 TensorFlow seq2seq 模型的定义。我们采用了一个嵌入层,将整数表示转化为输入的向量表示。这个 seq2seq 模型有四个主要组成部分:嵌入层、编码器、解码器和成本/优化器。
您可以在以下图表中查看模型的图形表示:

seq2seq 模型的 TensorBoard 可视化。该图显示了编码器与解码器之间的连接,以及其他相关组件如优化器。
以下是 TensorFlow seq2seq 模型定义的正式大纲:
class Chatbot:
def __init__(self, size_layer, num_layers, embedded_size,
from_dict_size, to_dict_size, learning_rate, batch_size):
def cells(reuse=False):
return tf.nn.rnn_cell.LSTMCell(size_layer,initializer=tf.orthogonal_initializer(),reuse=reuse)
self.X = tf.placeholder(tf.int32, [None, None])
self.Y = tf.placeholder(tf.int32, [None, None])
self.X_seq_len = tf.placeholder(tf.int32, [None])
self.Y_seq_len = tf.placeholder(tf.int32, [None])
with tf.variable_scope("encoder_embeddings"):
encoder_embeddings = tf.Variable(tf.random_uniform([from_dict_size, embedded_size], -1, 1))
encoder_embedded = tf.nn.embedding_lookup(encoder_embeddings, self.X)
main = tf.strided_slice(self.X, [0, 0], [batch_size, -1], [1, 1])
with tf.variable_scope("decoder_embeddings"):
decoder_input = tf.concat([tf.fill([batch_size, 1], GO), main], 1)
decoder_embeddings = tf.Variable(tf.random_uniform([to_dict_size, embedded_size], -1, 1))
decoder_embedded = tf.nn.embedding_lookup(encoder_embeddings, decoder_input)
with tf.variable_scope("encoder"):
rnn_cells = tf.nn.rnn_cell.MultiRNNCell([cells() for _ in range(num_layers)])
_, last_state = tf.nn.dynamic_rnn(rnn_cells, encoder_embedded,
dtype = tf.float32)
with tf.variable_scope("decoder"):
rnn_cells_dec = tf.nn.rnn_cell.MultiRNNCell([cells() for _ in range(num_layers)])
outputs, _ = tf.nn.dynamic_rnn(rnn_cells_dec, decoder_embedded,
initial_state = last_state,
dtype = tf.float32)
with tf.variable_scope("logits"):
self.logits = tf.layers.dense(outputs,to_dict_size)
print(self.logits)
masks = tf.sequence_mask(self.Y_seq_len, tf.reduce_max(self.Y_seq_len), dtype=tf.float32)
with tf.variable_scope("cost"):
self.cost = tf.contrib.seq2seq.sequence_loss(logits = self.logits,
targets = self.Y,
weights = masks)
with tf.variable_scope("optimizer"):
self.optimizer = tf.train.AdamOptimizer(learning_rate = learning_rate).minimize(self.cost)
超参数
现在我们已经准备好模型定义,我们将定义超参数。我们将保持大部分配置与之前相同:
size_layer = 128
num_layers = 2
embedded_size = 128
learning_rate = 0.001
batch_size = 32
epoch = 50
训练 seq2seq 模型
现在,让我们来训练模型。我们将需要一些辅助函数来填充句子并计算模型的准确率:
def pad_sentence_batch(sentence_batch, pad_int):
padded_seqs = []
seq_lens = []
max_sentence_len = 50
for sentence in sentence_batch:
padded_seqs.append(sentence + [pad_int] * (max_sentence_len - len(sentence)))
seq_lens.append(50)
return padded_seqs, seq_lens
def check_accuracy(logits, Y):
acc = 0
for i in range(logits.shape[0]):
internal_acc = 0
for k in range(len(Y[i])):
if Y[i][k] == logits[i][k]:
internal_acc += 1
acc += (internal_acc / len(Y[i]))
return acc / logits.shape[0]
我们初始化模型并迭代会话,训练指定的 epoch 次数:
tf.reset_default_graph()
sess = tf.InteractiveSession()
model = Chatbot(size_layer, num_layers, embedded_size, vocabulary_size_from + 4,
vocabulary_size_to + 4, learning_rate, batch_size)
sess.run(tf.global_variables_initializer())
for i in range(epoch):
total_loss, total_accuracy = 0, 0
for k in range(0, (len(text_from) // batch_size) * batch_size, batch_size):
batch_x, seq_x = pad_sentence_batch(X[k: k+batch_size], PAD)
batch_y, seq_y = pad_sentence_batch(Y[k: k+batch_size], PAD)
predicted, loss, _ = sess.run([tf.argmax(model.logits,2), model.cost, model.optimizer],
feed_dict={model.X:batch_x,
model.Y:batch_y,
model.X_seq_len:seq_x,
model.Y_seq_len:seq_y})
total_loss += loss
total_accuracy += check_accuracy(predicted,batch_y)
total_loss /= (len(text_from) // batch_size)
total_accuracy /= (len(text_from) // batch_size)
print('epoch: %d, avg loss: %f, avg accuracy: %f'%(i+1, total_loss, total_accuracy))
OUTPUT:
epoch: 47, avg loss: 0.682934, avg accuracy: 0.000000
epoch: 48, avg loss: 0.680367, avg accuracy: 0.000000
epoch: 49, avg loss: 0.677882, avg accuracy: 0.000000
epoch: 50, avg loss: 0.678484, avg accuracy: 0.000000
.
.
.
epoch: 1133, avg loss: 0.000464, avg accuracy: 1.000000
epoch: 1134, avg loss: 0.000462, avg accuracy: 1.000000
epoch: 1135, avg loss: 0.000460, avg accuracy: 1.000000
epoch: 1136, avg loss: 0.000457, avg accuracy: 1.000000
评估 seq2seq 模型。
所以,在 GPU 上运行训练过程几个小时后,你可以看到准确率已达到1.0,并且损失显著降低至0.00045。让我们看看当我们提出一些通用问题时,模型表现如何。
为了进行预测,我们将创建一个 predict() 函数,它将接受任意大小的原始文本作为输入,并返回我们提出问题的答案。我们对 Out Of Vocab(OOV)词汇进行了快速修复,通过将其替换为 PAD 来处理:
def predict(sentence):
X_in = []
for word in sentence.split():
try:
X_in.append(dictionary_from[word])
except:
X_in.append(PAD)
pass
test, seq_x = pad_sentence_batch([X_in], PAD)
input_batch = np.zeros([batch_size,seq_x[0]])
input_batch[0] =test[0]
log = sess.run(tf.argmax(model.logits,2),
feed_dict={
model.X:input_batch,
model.X_seq_len:seq_x,
model.Y_seq_len:seq_x
}
)
result=' '.join(rev_dictionary_to[i] for i in log[0])
return result
当模型经过前 50 次 epoch 训练后,我们得到了以下结果:
>> predict('where do you live')
>> i PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD
>> print predict('how are you ?')
>> i am PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD
当模型训练了 1,136 个 epoch 后:
>> predict('where do you live')
>> miami florida PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD
>> print predict('how are you ?')
>> i am fine thank you PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD PAD
哇!这很令人印象深刻,对吧?现在你的模型不仅能理解上下文,还能逐词生成回答。
总结
在本章中,我们涵盖了基本的 RNN 单元、LSTM 单元,以及 seq2seq 模型,构建了一个可以用于多种 NLP 任务的语言模型。我们从头开始实现了一个聊天机器人,通过从提供的数据集中生成词语序列来回答问题。
这次练习的经验展示了 LSTM 作为 RNN 的一个常见必要组件的价值。有了 LSTM,我们能够看到以下相较于过去的 CNN 模型的改进:
-
LSTM 能够保持状态信息。
-
输入和输出的句子长度可能是可变且不同的。
-
LSTM 能够有效地处理复杂的上下文。
具体来说,在本章中,我们做了以下工作:
-
获得了对 RNN 及其主要形式的直觉理解。
-
实现了一个基于 RNN 的语言模型。
-
学习了 LSTM 模型。
-
实现了 LSTM 语言模型并与 RNN 进行了比较。
-
实现了一个基于 LSTM 单元的编码器-解码器 RNN,用于一个简单的序列到序列的问答任务。
有了正确的训练数据,就有可能使用这个模型实现假设客户(餐饮连锁店)的目标,即构建一个强大的聊天机器人(结合我们探索的其他计算语言学技术),可以自动化电话订餐过程。
做得好!
第六章:内容创作的生成性语言模型
这项工作无疑令人兴奋,而且已经有消息传出,我们正在展示一套专业的深度学习能力,通过为各种商业用例提供解决方案!作为数据科学家,我们理解自己技能的可迁移性。我们知道,在处理我们知道结构上相似但乍看之下不同的问题时,通过运用核心技能,我们能够提供价值。这一点在下一个深度学习项目中尤为真实。接下来,我们(假设性地)将参与一个项目,创意团队请求我们帮助为电影剧本、歌曲歌词,甚至音乐创作一些原创内容!
我们如何将解决餐饮连锁店问题的经验,运用到如此不同的行业中呢?让我们探索一下我们所知道的和将要做的事情。在过去的项目中,我们展示了如何以图像为输入并输出类别标签(第二章,使用回归进行预测的神经网络训练);我们训练了一个模型,接受文本输入并输出情感分类(第三章,使用 word2vec 进行词汇表示);我们构建了一个开放领域问题解答聊天机器人的 NLP 管道,接受文本输入并从语料库中提取相关文本作为适当输出(第四章,为构建聊天机器人构建 NLP 管道);我们还扩展了该聊天机器人的功能,使其能够为餐厅提供自动化点餐系统服务(第五章,用于构建聊天机器人的序列到序列模型)。
定义目标:在这个项目中,我们将迈出计算语言学旅程的下一步,在 Python 深度学习项目 中为客户生成新的内容。我们需要通过提供一个深度学习解决方案来帮助他们,生成可用于电影剧本、歌曲歌词和音乐的新内容。
在本章中,我们将实现一个生成模型,使用 长短期记忆(LSTM)、变分自编码器和 生成对抗网络(GANs)来生成内容。我们将实现用于文本和图像的模型,生成图像和文本供艺术家和各种商业使用。
在本章中,我们将讨论以下主题:
-
使用 LSTM 进行文本生成
-
双向 LSTM 在文本生成中的额外优势
-
深度(多层)LSTM 生成歌曲歌词
-
深度(多层)LSTM 音乐生成用于歌曲创作
LSTM 在文本生成中的应用
在本节中,我们将探索一种流行的深度学习模型:循环神经网络(RNN),以及它如何用于生成序列数据。在深度学习中,创建序列数据的通用方法是训练一个模型(通常是 RNN 或 ConvNet)来预测序列中的下一个标记或下几个标记,基于前面的标记作为输入。例如,假设我们给定输入句子:I love to work in deep learning。我们将训练该网络以预测下一个字符作为目标。
在处理文本数据时,标记通常是单词或字符,任何能够基于前一个标记预测下一个标记的网络都称为语言模型,它可以捕捉语言的潜在空间。
训练语言模型后,我们可以开始输入一些初始文本,要求它生成下一个标记,然后将生成的标记重新输入语言模型,以预测更多的标记。对于我们的假设用例,我们的创意客户将使用此模型,并随后提供一些文本示例,我们将被要求在该风格下创建新的内容。
构建文本生成模型的第一步是导入所需的所有模块。该项目将使用 Keras API 来创建模型,Keras 工具将用于下载数据集。为了构建文本生成模块,我们需要大量简单的文本数据。
你可以在github.com/PacktPublishing/Python-Deep-Learning-Projects/blob/master/Chapter06/Basics/generative_text.py找到该代码文件:
import keras
import numpy as np
from keras import layers
# Gather data
path = keras.utils.get_file(
'sample.txt',
origin='https://s3.amazonaws.com/text-datasets/nietzsche.txt')
text = open(path).read().lower()
print('Number of words in corpus:', len(text))
数据预处理
让我们执行数据预处理,将原始数据转换为编码形式。我们将提取固定长度的句子,使用独热编码过程对它们进行编码,最后构建一个形状为(sequence,maxlen,unique_characters)的张量,如下图所示。同时,我们将准备目标向量y,用于包含每个提取序列后续的字符。
以下是我们将用于预处理数据的代码:
# Length of extracted character sequences
maxlen = 100
# We sample a new sequence every 5 characters
step = 5
# List to hold extracted sequences
sentences = []
# List to hold the target characters
next_chars = []
# Extracting sentences and the next characters.
for i in range(0, len(text) - maxlen, step):
sentences.append(text[i: i + maxlen])
next_chars.append(text[i + maxlen])
print('Number of sequences:', len(sentences))
# List of unique characters in the corpus
chars = sorted(list(set(text)))
# Dictionary mapping unique characters to their index in `chars`
char_indices = dict((char, chars.index(char)) for char in chars)
# Converting characters into one-hot encoding.
x = np.zeros((len(sentences), maxlen, len(chars)), dtype=np.bool)
y = np.zeros((len(sentences), len(chars)), dtype=np.bool)
for i, sentence in enumerate(sentences):
for t, char in enumerate(sentence):
x[i, t, char_indices[char]] = 1
y[i, char_indices[next_chars[i]]] = 1
以下是数据预处理的过程。我们已经将原始数据转换为张量,接下来将用于训练目的:

定义用于文本生成的 LSTM 模型
这个深度模型是一个由一个隐藏的 LSTM 层(具有128个内存单元)组成的网络,后面跟着一个Dense分类器层,并对所有可能的字符使用softmax激活函数。目标是独热编码,这意味着我们将使用categorical_crossentropy作为loss函数来训练模型。
以下代码块定义了模型的架构:
model = keras.models.Sequential()
model.add(layers.LSTM(128, input_shape=(maxlen, len(chars))))
model.add(layers.Dense(len(chars), activation='softmax'))
optimizer = keras.optimizers.RMSprop(lr=0.01)
model.compile(loss='categorical_crossentropy', optimizer=optimizer)
下图帮助我们可视化模型的架构:

训练模型
在文本生成中,选择后续字符的方式至关重要。最常见的方法(贪婪采样)会导致重复的字符,无法生成连贯的语言。这就是为什么我们使用一种不同的方法,称为随机采样。这种方法在预测概率分布中添加了一定程度的随机性。
使用以下代码重新加权预测概率分布并采样一个字符索引:
def sample(preds, temperature=1.0):
preds = np.asarray(preds).astype('float64')
preds = np.log(preds) / temperature
exp_preds = np.exp(preds)
preds = exp_preds / np.sum(exp_preds)
probas = np.random.multinomial(1, preds, 1)
return np.argmax(probas)
现在,我们开始迭代训练和文本生成,首先进行 30 轮训练,然后对模型进行 1 次迭代拟合。接着,随机选择一个种子文本,将其转换为独热编码格式,并预测 100 个字符。最后,在每次迭代中将新生成的字符附加到种子文本后。
每次迭代后,通过使用不同的温度值进行生成。这样可以查看并理解在模型收敛时生成文本的演变,以及温度在采样策略中的影响。
Temperature是 LSTM 的超参数,它通过在应用 softmax 之前进行 logit 缩放来影响预测的随机性。
我们需要执行以下代码,以便训练模型:
for epoch in range(1, 30):
print('epoch', epoch)
# Fit the model for 1 epoch
model.fit(x, y, batch_size=128, epochs=1, callbacks=callbacks_list)
# Select a text seed randomly
start_index = random.randint(0, len(text) - maxlen - 1)
generated_text = text[start_index: start_index + maxlen]
print('---Seeded text: "' + generated_text + '"')
for temperature in [0.2, 0.5, 1.0, 1.2]:
print('------ Selected temperature:', temperature)
sys.stdout.write(generated_text)
# We generate 100 characters
for i in range(100):
sampled = np.zeros((1, maxlen, len(chars)))
for t, char in enumerate(generated_text):
sampled[0, t, char_indices[char]] = 1.
preds = model.predict(sampled, verbose=0)[0]
next_index = sample(preds, temperature)
next_char = chars[next_index]
generated_text += next_char
generated_text = generated_text[1:]
sys.stdout.write(next_char)
sys.stdout.flush()
print()
推理与结果
这将引导我们进入生成语言模型的激动人心的部分——创建自定义内容!深度学习中的推理步骤是我们将训练好的模型暴露于新数据,并进行预测或分类。在本项目的当前背景下,我们寻找的是模型输出,也就是新的句子,这将是我们的新颖自定义内容。让我们看看我们的深度学习模型能做什么!
我们将使用以下代码将检查点存储到一个二进制文件中,该文件保存所有权重:
from keras.callbacks import ModelCheckpoint
filepath="weights-{epoch:02d}-{loss:.4f}.hdf5"
checkpoint = ModelCheckpoint(filepath, monitor='loss', verbose=1, save_best_only=True, mode='min')
callbacks_list = [checkpoint]
现在,我们将使用训练好的模型生成新的文本:
seed_text = 'i want to generate new text after this '
print (seed_text)
# load the network weights
filename = "weights-30-1.545.hdf5"
model.load_weights(filename)
model.compile(loss='categorical_crossentropy', optimizer='adam')
for temperature in [0.5]:
print('------ temperature:', temperature)
sys.stdout.write(seed_text)
# We generate 400 characters
for i in range(40):
sampled = np.zeros((1, maxlen, len(chars)))
for t, char in enumerate(seed_text):
sampled[0, t, char_indices[char]] = 1.
preds = model.predict(sampled, verbose=0)[0]
next_index = sample(preds, temperature)
next_char = chars[next_index]
seed_text += next_char
seed_text = seed_text[1:]
sys.stdout.write(next_char)
sys.stdout.flush()
print()
在成功训练模型后,我们将在第 30^(th)轮看到以下结果:
--- Generating with seed:
the "good old time" to which it belongs, and as an expressio"
------ temperature: 0.2
the "good old time" to which it belongs, and as an expression of the sense of the stronger and subli
------ temperature: 0.5 and as an expression of the sense of the stronger and sublication of possess and more spirit and in
------ temperature: 1.0 e stronger and sublication of possess and more spirit and instinge, and it: he ventlumentles, no dif
------ temperature: 1.2
d more spirit and instinge, and it: he ventlumentles, no differific and does amongly domen--whete ac
我们发现,当temperature超参数取较低值时,模型能够生成更实用和更真实的词语。当我们使用较高的温度时,生成的文本变得更加有趣和不寻常——有些人甚至会说它是具有创意的。有时,模型甚至会发明出一些听起来模糊可信的新词。因此,低温度的使用理念对于需要保持现实的商业用例更为合理,而较高温度值则适用于更加创意和艺术化的用例。
深度学习和生成语言模型的艺术在于平衡学习到的结构和随机性,这使得输出变得有趣。
使用深度(多层)LSTM 生成歌词
现在我们已经为文本生成构建了基本的 LSTM 模型并学习了它的价值,让我们再进一步,创建一个适用于生成音乐歌词任务的深层 LSTM 模型。我们现在有了一个新目标:构建并训练一个模型,输出完全新颖、原创性的歌词,符合任意数量艺术家的风格。
让我们开始吧。您可以参考位于 Lyrics-ai 文件夹中的代码文件 (github.com/PacktPublishing/Python-Deep-Learning-Projects/tree/master/Chapter06/Lyrics-ai) 进行此练习。
数据预处理
要构建一个能够生成歌词的模型,我们需要大量的歌词数据,可以从各种来源轻松提取。我们从大约 10,000 首歌曲中收集了歌词,并将它们存储在一个名为 lyrics_data.txt 的文本文件中。您可以在我们的 GitHub 仓库 (github.com/PacktPublishing/Python-Deep-Learning-Projects/blob/master/Chapter06/Lyrics-ai/lyrics_data.txt) 中找到数据文件。
现在我们有了数据,我们需要将这些原始文本转换为独热编码版本:
import numpy as np
import codecs
# Class to perform all preprocessing operations
class Preprocessing:
vocabulary = {}
binary_vocabulary = {}
char_lookup = {}
size = 0
separator = '->'
# This will take the data file and convert data into one hot encoding and dump the vocab into the file.
def generate(self, input_file_path):
input_file = codecs.open(input_file_path, 'r', 'utf_8')
index = 0
for line in input_file:
for char in line:
if char not in self.vocabulary:
self.vocabulary[char] = index
self.char_lookup[index] = char
index += 1
input_file.close()
self.set_vocabulary_size()
self.create_binary_representation()
# This method is to load the vocab into the memory
def retrieve(self, input_file_path):
input_file = codecs.open(input_file_path, 'r', 'utf_8')
buffer = ""
for line in input_file:
try:
separator_position = len(buffer) + line.index(self.separator)
buffer += line
key = buffer[:separator_position]
value = buffer[separator_position + len(self.separator):]
value = np.fromstring(value, sep=',')
self.binary_vocabulary[key] = value
self.vocabulary[key] = np.where(value == 1)[0][0]
self.char_lookup[np.where(value == 1)[0][0]] = key
buffer = ""
except ValueError:
buffer += line
input_file.close()
self.set_vocabulary_size()
# Below are some helper functions to perform pre-processing.
def create_binary_representation(self):
for key, value in self.vocabulary.iteritems():
binary = np.zeros(self.size)
binary[value] = 1
self.binary_vocabulary[key] = binary
def set_vocabulary_size(self):
self.size = len(self.vocabulary)
print "Vocabulary size: {}".format(self.size)
def get_serialized_binary_representation(self):
string = ""
np.set_printoptions(threshold='nan')
for key, value in self.binary_vocabulary.iteritems():
array_as_string = np.array2string(value, separator=',', max_line_width=self.size * self.size)
string += "{}{}{}\n".format(key.encode('utf-8'), self.separator, array_as_string[1:len(array_as_string) - 1])
return string
预处理模块的总体目标是将原始文本数据转换为独热编码,如下图所示:

该图表示数据预处理部分。原始歌词数据用于构建词汇映射,进而转换为独热编码。
成功执行预处理模块后,将会以 {dataset_filename}.vocab 的形式导出一个二进制文件。此 vocab 文件是在训练过程中必须提供给模型的文件之一,连同数据集一起。
定义模型
我们将使用此项目中早期使用的 Keras 模型方法来构建这个模型。为了构建一个更复杂的模型,我们将使用 TensorFlow 从头开始编写每一层。作为数据科学家和深度学习工程师,TensorFlow 为我们提供了对模型架构更精细的控制。
对于这个模型,我们将使用以下代码块中的代码创建两个占位符,用于存储输入和输出值:
import tensorflow as tf
import pickle
from tensorflow.contrib import rnn
def build(self, input_number, sequence_length, layers_number, units_number, output_number):
self.x = tf.placeholder("float", [None, sequence_length, input_number])
self.y = tf.placeholder("float", [None, output_number])
self.sequence_length = sequence_length
接下来,我们需要将权重和偏置存储到我们创建的变量中:
self.weights = {
'out': tf.Variable(tf.random_normal([units_number, output_number]))
}
self.biases = {
'out': tf.Variable(tf.random_normal([output_number]))
}
x = tf.transpose(self.x, [1, 0, 2])
x = tf.reshape(x, [-1, input_number])
x = tf.split(x, sequence_length, 0)
我们可以通过使用多个 LSTM 层来构建此模型,基本的 LSTM 单元为每个层分配指定数量的单元,如下图所示:

Tensorboard 可视化 LSTM 架构
以下是此过程的代码:
lstm_layers = []
for i in range(0, layers_number):
lstm_layer = rnn.BasicLSTMCell(units_number)
lstm_layers.append(lstm_layer)
deep_lstm = rnn.MultiRNNCell(lstm_layers)
self.outputs, states = rnn.static_rnn(deep_lstm, x, dtype=tf.float32)
print "Build model with input_number: {}, sequence_length: {}, layers_number: {}, " \
"units_number: {}, output_number: {}".format(input_number, sequence_length, layers_number,
units_number, output_number)
# This method is using to dump the model configurations
self.save(input_number, sequence_length, layers_number, units_number, output_number)
训练基于 TensorFlow 的深度 LSTM 模型
现在我们有了必要的输入,即数据集文件路径、vocab 文件路径和模型名称,我们将启动训练过程。让我们定义模型的所有超参数:
import os
import argparse
from modules.Model import *
from modules.Batch import *
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--training_file', type=str, required=True)
parser.add_argument('--vocabulary_file', type=str, required=True)
parser.add_argument('--model_name', type=str, required=True)
parser.add_argument('--epoch', type=int, default=200)
parser.add_argument('--batch_size', type=int, default=50)
parser.add_argument('--sequence_length', type=int, default=50)
parser.add_argument('--log_frequency', type=int, default=100)
parser.add_argument('--learning_rate', type=int, default=0.002)
parser.add_argument('--units_number', type=int, default=128)
parser.add_argument('--layers_number', type=int, default=2)
args = parser.parse_args()
由于我们是在进行批量训练,我们将使用Batch模块将数据集划分为定义好的batch_size批次:
batch = Batch(training_file, vocabulary_file, batch_size, sequence_length)
每个批次将返回两个数组。一个是输入序列的输入向量,形状为[batch_size, sequence_length, vocab_size],另一个数组将保存标签向量,形状为[batch_size, vocab_size]。
现在,我们初始化模型并创建优化器函数。在这个模型中,我们使用了Adam优化器。
Adam 优化器是一个强大的工具。你可以通过官方的 TensorFlow 文档了解更多内容
www.tensorflow.org/api_docs/python/tf/train/AdamOptimizer
接下来,我们将训练我们的模型,并对每一批次进行优化:
# Building model instance and classifier
model = Model(model_name)
model.build(input_number, sequence_length, layers_number, units_number, classes_number)
classifier = model.get_classifier()
# Building cost functions
cost = tf.reduce_mean(tf.square(classifier - model.y))
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(cost)
# Computing the accuracy metrics
expected_prediction = tf.equal(tf.argmax(classifier, 1), tf.argmax(model.y, 1))
accuracy = tf.reduce_mean(tf.cast(expected_prediction, tf.float32))
# Preparing logs for Tensorboard
loss_summary = tf.summary.scalar("loss", cost)
acc_summary = tf.summary.scalar("accuracy", accuracy)
train_summary_op = tf.summary.merge_all()
out_dir = "{}/{}".format(model_name, model_name)
train_summary_dir = os.path.join(out_dir, "summaries")
##
# Initializing the session and executing the training
init = tf.global_variables_initializer()
with tf.Session() as sess:
sess.run(init)
iteration = 0
while batch.dataset_full_passes < epoch:
iteration += 1
batch_x, batch_y = batch.get_next_batch()
batch_x = batch_x.reshape((batch_size, sequence_length, input_number))
sess.run(optimizer, feed_dict={model.x: batch_x, model.y: batch_y})
if iteration % log_frequency == 0:
acc = sess.run(accuracy, feed_dict={model.x: batch_x, model.y: batch_y})
loss = sess.run(cost, feed_dict={model.x: batch_x, model.y: batch_y})
print("Iteration {}, batch loss: {:.6f}, training accuracy: {:.5f}".format(iteration * batch_size,
loss, acc))
batch.clean()
一旦模型完成训练,检查点将被存储。我们可以稍后用于推理。以下是训练过程中准确率和损失的图示:

随时间变化的准确率(上)和损失(下)图。我们可以看到,准确率随着时间增加而提高,损失随着时间减少。
推理
现在模型已准备就绪,我们可以使用它来进行预测。我们将首先定义所有的参数。在构建推理时,我们需要提供一些种子文本,就像我们在前一个模型中做的那样。同时,我们还需要提供vocab文件的路径和我们将存储生成歌词的输出文件。我们还将提供生成文本的长度:
import argparse
import codecs
from modules.Model import *
from modules.Preprocessing import *
from collections import deque
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--model_name', type=str, required=True)
parser.add_argument('--vocabulary_file', type=str, required=True)
parser.add_argument('--output_file', type=str, required=True)
parser.add_argument('--seed', type=str, default="Yeah, oho ")
parser.add_argument('--sample_length', type=int, default=1500)
parser.add_argument('--log_frequency', type=int, default=100)
接下来,我们将通过提供我们在前面的代码训练步骤中使用的模型名称来加载模型,并从文件中恢复词汇:
model = Model(model_name)
model.restore()
classifier = model.get_classifier()
vocabulary = Preprocessing()
vocabulary.retrieve(vocabulary_file)
我们将使用堆栈方法来存储生成的字符,附加到堆栈上,然后用相同的堆栈以交互的方式将其输入到模型中:
# Preparing the raw input data
for char in seed:
if char not in vocabulary.vocabulary:
print char,"is not in vocabulary file"
char = u' '
stack.append(char)
sample_file.write(char)
# Restoring the models and making inferences
with tf.Session() as sess:
tf.global_variables_initializer().run()
saver = tf.train.Saver(tf.global_variables())
ckpt = tf.train.get_checkpoint_state(model_name)
if ckpt and ckpt.model_checkpoint_path:
saver.restore(sess, ckpt.model_checkpoint_path)
for i in range(0, sample_length):
vector = []
for char in stack:
vector.append(vocabulary.binary_vocabulary[char])
vector = np.array([vector])
prediction = sess.run(classifier, feed_dict={model.x: vector})
predicted_char = vocabulary.char_lookup[np.argmax(prediction)]
stack.popleft()
stack.append(predicted_char)
sample_file.write(predicted_char)
if i % log_frequency == 0:
print "Progress: {}%".format((i * 100) / sample_length)
sample_file.close()
print "Sample saved in {}".format(output_file)
输出
执行成功后,我们将获得自己新鲜出炉的、由 AI 生成的歌词,经过审核并发布。以下是其中一首歌词的示例。我们已修改部分拼写,以使句子更通顺:
Yeah, oho once upon a time, on ir intasd
I got monk that wear your good
So heard me down in my clipp
Cure me out brick
Coway got baby, I wanna sheart in faic
I could sink awlrook and heart your all feeling in the firing of to the still hild, gavelly mind, have before you, their lead
Oh, oh shor,s sheld be you und make
Oh, fseh where sufl gone for the runtome
Weaaabe the ligavus I feed themust of hear
在这里,我们可以看到模型已经学会了如何生成段落和句子,并且使用了适当的空格。它仍然不完美,并且有些地方不合逻辑。
看到成功的迹象:第一个任务是创建一个能够学习的模型,第二个任务是改进该模型。通过使用更大的训练数据集和更长的训练时间来训练模型,可以实现这一点。
使用多层 LSTM 生成音乐
我们的(假设的)创意代理客户非常喜欢我们在生成音乐歌词方面的成果。现在,他们希望我们能创作一些音乐。我们将使用多个 LSTM 层,如下图所示:

到目前为止,我们知道 RNNs 适合处理序列数据,我们也可以将一首音乐曲目表示为音符和和弦的序列。在这种范式中,音符变成包含八度、偏移量和音高信息的数据对象。和弦则变成包含同时演奏的音符组合信息的数据容器对象。
音高是音符的声音频率。音乐家使用字母表示音符[A, B, C, D, E, F, G],其中 G 是最低音,A 是最高音。
八度标识在演奏乐器时使用的音高集合。
偏移量标识音符在乐曲中的位置。
让我们探讨以下部分,建立如何通过首先处理音频文件、将其转换为序列映射数据,然后使用 RNN 训练模型来生成音乐的直觉。
我们开始吧。你可以参考此练习的 Music-ai 代码,代码可以在github.com/PacktPublishing/Python-Deep-Learning-Projects/tree/master/Chapter06/Music-ai找到。
数据预处理
为了生成音乐,我们需要一组足够大的音乐文件训练数据。这些数据将用于提取序列,并建立我们的训练数据集。为了简化此过程,在本章中,我们使用单一乐器的原声带。我们收集了一些旋律并将其存储在 MIDI 文件中。以下 MIDI 文件的示例展示了其内容:

该图片表示了一个示例 MIDI 文件的音高和音符分布
我们可以看到音符之间的间隔、每个音符的偏移量以及音高。
为了提取我们的数据集内容,我们将使用 music21。它还可以将模型的输出转化为音乐符号。Music21 (web.mit.edu/music21/) 是一个非常有用的 Python 工具包,用于计算机辅助音乐学。
为了开始,我们将加载每个文件,并使用converter.parse(file)函数来创建一个 music21 stream对象。稍后,我们将通过这个stream对象获取文件中的所有音符和和弦列表。由于音符的音高最显著的特征可以通过谱号重现,我们将附加每个音符的音高。为了处理和弦,我们将把和弦中每个音符的 ID 编码为一个单一字符串,音符之间用点分隔,并将其附加到和弦上。这个编码过程使我们能够轻松地解码模型生成的输出,得到正确的音符和和弦。
我们将把 MIDI 文件中的数据加载到一个数组中,如下代码片段所示:
from music21 import converter, instrument, note, chord
import glob
notes = []
for file in glob.glob("/data/*.mid"):
midi = converter.parse(file)
notes_to_parse = None
parts = instrument.partitionByInstrument(midi)
if parts: # file has instrument parts
notes_to_parse = parts.parts[0].recurse()
else: # file has notes in a flat structure
notes_to_parse = midi.flat.notes
for element in notes_to_parse:
if isinstance(element, note.Note):
notes.append(str(element.pitch))
elif isinstance(element, chord.Chord):
notes.append('.'.join(str(n) for n in element.normalOrder))
下一步是为模型创建输入序列和相应的输出,如下图所示:

数据处理部分的概述,其中我们从 MIDI 文件中提取音符和和弦,并将它们存储为数组。
模型对每个输入序列输出一个音符或和弦。我们使用输入序列中第一个音符或和弦,在我们的音符列表中继续。为了完成数据准备的最后一步,我们需要对输出进行独热编码。这将标准化下一次迭代的输入。
我们可以通过以下代码来实现:
sequence_length = 100
# get all pitch names
pitchnames = sorted(set(item for item in notes))
# create a dictionary to map pitches to integers
note_to_int = dict((note, number) for number, note in enumerate(pitchnames))
network_input = []
network_output = []
# create input sequences and the corresponding outputs
for i in range(0, len(notes) - sequence_length, 1):
sequence_in = notes[i:i + sequence_length]
sequence_out = notes[i + sequence_length]
network_input.append([note_to_int[char] for char in sequence_in])
network_output.append(note_to_int[sequence_out])
n_patterns = len(network_input)
# reshape the input into a format compatible with LSTM layers
network_input = numpy.reshape(network_input, (n_patterns, sequence_length, 1))
# normalize input
network_input = network_input / float(n_vocab)
network_output = np_utils.to_categorical(network_output)
现在我们已经提取了所有音符和和弦。我们将创建我们的训练数据 X 和 Y,如下图所示:

捕获的音符和和弦在数组中进一步转换为独热编码向量,通过映射词汇表中的值。所以我们将把 X 矩阵中的序列输入到模型中,并期望模型能够学习预测给定序列的 Y。
定义模型和训练
现在,我们进入了所有深度学习工程师喜爱的部分:设计模型架构!我们将在模型架构中使用四种不同类型的层:
-
LSTM:这是一种 RNN 层。
-
Dropout:一种正则化技术。它通过随机丢弃一些节点来帮助防止模型过拟合。
-
Dense:这是一个全连接层,其中每个输入节点都与每个输出节点相连。
-
Activation:这个决定了将用于生成节点输出的
activation函数。
我们将再次使用 Keras API 来快速实现:
model = Sequential()
model.add(LSTM(
256,
input_shape=(network_input.shape[1], network_input.shape[2]),
return_sequences=True
))
model.add(Dropout(0.5))
model.add(LSTM(512, return_sequences=True))
model.add(Dropout(0.3))
model.add(LSTM(256))
model.add(Dense(256))
model.add(Dropout(0.3))
model.add(Dense(n_vocab))
model.add(Activation('softmax'))
model.compile(loss='categorical_crossentropy',
optimizer='rmsprop',
metrics=['accuracy'])
我们设计的生成模型架构包含三层 LSTM、三层Dropout、两层Dense和一层Activation,如下面的图所示:

音乐生成的模型架构
将使用类别交叉熵来计算每次训练迭代的损失。我们将在此网络中再次使用 Adam 优化器。现在我们已经配置了深度学习模型架构,接下来是时候训练模型了。我们决定训练模型 200 个周期,每个周期有 25 个批次,使用model.fit()。我们还希望跟踪每个周期损失的减少,并将使用检查点来实现这个目的。
现在我们将执行训练操作,并将模型保存到以下代码中提到的文件:
filepath = "weights-{epoch:02d}-{loss:.4f}.hdf5"
checkpoint = ModelCheckpoint(
filepath,
monitor='loss',
verbose=0,
save_best_only=True,
mode='min'
)
callbacks_list = [checkpoint]
history = model.fit(network_input, network_output, epochs=200, batch_size=64, callbacks=callbacks_list)
模型的性能如下所示:

精度和损失在各个周期中的变化图
现在训练过程已完成,我们将加载训练好的模型并生成我们自己的音乐。
生成音乐
真正有趣的部分来了!让我们生成一些器乐音乐。我们将使用模型设置和训练中的代码,但不执行训练(因为我们的模型已经训练完成),而是插入我们在之前训练中获得的学习到的权重。
以下代码块执行这两个步骤:
model = Sequential()
model.add(LSTM(
512,
input_shape=(network_input.shape[1], network_input.shape[2]),
return_sequences=True
))
model.add(Dropout(0.5))
model.add(LSTM(512, return_sequences=True))
model.add(Dropout(0.3))
model.add(LSTM(512))
model.add(Dense(256))
model.add(Dropout(0.3))
model.add(Dense(n_vocab))
model.add(Activation('softmax'))
model.compile(loss='categorical_crossentropy', optimizer='adam')
# Load the weights to each node
model.load_weights('weights_file.hdf5')
通过这样做,我们创建了相同的模型,但这次是为了预测目的,并添加了一行额外的代码来将权重加载到内存中。
由于我们需要一个种子输入,以便模型可以开始生成音乐,我们选择使用从处理文件中获得的随机音符序列。只要你确保序列长度恰好为 100,你也可以发送你自己的节点:
# Randomly selected a note from our processed data
start = numpy.random.randint(0, len(network_input)-1)
pattern = network_input[start]
int_to_note = dict((number, note) for number, note in enumerate(pitchnames))
prediction_output = []
# Generate 1000 notes of music
for note_index in range(1000):
prediction_input = numpy.reshape(pattern, (1, len(pattern), 1))
prediction_input = prediction_input / float(n_vocab)
prediction = model.predict(prediction_input, verbose=0)
index = numpy.argmax(prediction)
result = int_to_note[index]
prediction_output.append(result)
pattern.append(index)
pattern = pattern[1:len(pattern)]
我们迭代了 1,000 次模型生成,这创建了 1,000 个音符,并通过网络生成了大约五分钟的音乐。我们用来选择每次迭代下一个序列的过程是:我们从第一个序列开始提交,因为它是起始索引位置的音符序列。对于后续的输入序列,我们去掉第一个音符,并将前一次迭代的输出附加到序列的末尾。这是一种非常粗糙的方法,称为滑动窗口方法。你可以尝试并为每个选择的序列添加一些随机性,这可能会为生成的音乐带来更多的创造力。
此时,我们有一个包含所有音符和和弦编码表示的数组。为了将这个数组转换回Note和Chord对象,我们需要对其进行解码。
当我们检测到模式是Chord对象时,我们将把字符串分解成音符数组。然后,我们将遍历字符串中每个音符的表示形式,为每个项目创建一个Note对象。接着,我们创建Chord对象,其中包含这些音符。
当模式是Note对象时,我们将使用音高模式的字符串表示形式来创建Note对象。在每次迭代结束时,我们将偏移量增加0.5,这可以再次更改,并且可以引入随机性。
以下函数负责确定输出是Note还是Chord对象。最后,我们可以使用 music21 输出的stream对象来创建 MIDI 文件。以下是一些生成的音乐样本:github.com/PacktPublishing/Python-Deep-Learning-Projects/tree/master/Chapter06/Music-ai/generated_music。
要执行这些步骤,你可以使用这个helper函数,如以下代码块所示:
def create_midi_file(prediction_output):
""" convert the output from the prediction to notes and create a midi file"""
offset = 0
output_notes = []
for pattern in prediction_output:
# pattern is a chord
if ('.' in pattern) or pattern.isdigit():
notes_in_chord = pattern.split('.')
notes = []
for current_note in notes_in_chord:
new_note = note.Note(int(current_note))
new_note.storedInstrument = instrument.Piano()
notes.append(new_note)
new_chord = chord.Chord(notes)
new_chord.offset = offset
output_notes.append(new_chord)
# pattern is a note
else:
new_note = note.Note(pattern)
new_note.offset = offset
new_note.storedInstrument = instrument.Piano()
output_notes.append(new_note)
# increase offset each iteration so that notes do not stack
offset += 0.5
midi_stream = stream.Stream(output_notes)
midi_stream.write('midi', fp='generated.mid')
总结
哇,这些都是使用深度学习项目在 Python 中构建解决方案的实用示例,真是太令人印象深刻了!让我们回顾一下我们为自己设定的目标。
定义目标:
在这个项目中,我们将继续在深度学习项目中进行计算语言学的下一步,并为我们的客户生成新的内容。我们需要为他们提供一个深度学习解决方案,生成可以用于电影剧本、歌曲歌词和音乐的新内容。
使用深度学习生成创意内容显然是非常棘手的。本章的现实目标是展示并训练你掌握启动此类项目所需的技能和架构。要产生可接受的结果,必须与数据、模型及其输出进行互动,并通过适当的受众进行测试。需要记住的关键点是,你的模型输出可以根据具体任务进行高度个性化,而且你可以拓展思维,思考哪些商业用例是你在职业生涯中应当自信从事的。
在本章中,我们实现了一个生成模型,通过使用 LSTM 来生成内容。我们实现了适用于文本和音频的模型,假设它们为艺术家和创意领域的各类企业(如:音乐和电影产业)生成内容。
本章中我们学到的内容如下:
-
使用 LSTM 进行文本生成
-
双向 LSTM 在文本生成中的附加功能
-
深度(多层)LSTM 用于生成歌曲歌词
-
深度(多层)LSTM 用于生成歌曲音乐
这是一些令人兴奋的关于深度学习的工作,并且它将在下一章继续展开。让我们看看接下来有什么内容!
第七章:使用 DeepSpeech2 构建语音识别
这是一段很棒的旅程,在 Python 中使用图像、文本和声音数据构建了出色的深度学习项目。
在之前的章节中,我们在构建聊天机器人中相当重视语言模型。聊天机器人是客户参与和各种业务流程自动化的强大工具,从客户服务到销售。聊天机器人实现了重复和/或多余交互的自动化,如常见问题或产品订购工作流程。这种自动化为企业节省了时间和金钱。如果我们作为深度学习工程师做得很好,这也意味着消费者因此而获得了更好的用户体验(UX)。
通过聊天机器人进行新的业务与客户之间的互动非常有效,每一方都能获得价值。让我们看看互动场景,并确定我们下一个项目应该关注的任何约束。到目前为止,我们所有的聊天互动都是通过文本进行的。让我们思考这对消费者意味着什么。文本互动通常(但不限于)通过移动设备发起。其次,聊天机器人开辟了一种新的用户界面(UI)—对话式 UI。对话式 UI 的强大之处在于它可以消除物理键盘的限制,并打开了现在可以进行此类互动的位置和设备范围。
通过流行设备(如您的智能手机与苹果的 Siri、亚马逊的 Echo 和谷歌 Home)进行的语音识别系统使得会话界面成为可能。这是非常酷的技术,消费者喜爱它,采用这项技术的企业在其行业中获得了优势。
在本章中,我们将构建一个使用DeepSpeech2(DS2)模型识别英语语音的系统。
您将学到以下内容:
-
为了处理语音和频谱图
-
构建一个端到端的语音识别系统
-
连接主义时序分类(CTC)损失函数
-
用于递归神经网络(RNNs)的批归一化和 SortaGrad
让我们开始深入研究语音数据,学习如何从中提取特征工程的语音数据,提取各种类型的特征,然后构建一个可以检测您或注册用户声音的语音识别系统。
定义目标:本项目的目标是构建和训练一个自动语音识别(ASR)系统,用于接收并转换音频呼叫为文本,然后可以作为文本聊天机器人的输入,能够理解并作出响应。
数据预处理
在本项目中,我们将使用LibriSpeech ASR corpus(www.openslr.org/12/),这是 1,000 小时的 16 kHz 英语语音数据。
让我们使用以下命令来下载语料库并解压缩 LibriSpeech 数据:
mkdir -p data/librispeech
cd data/librispeech
wget http://www.openslr.org/resources/12/train-clean-100.tar.gz
wget http://www.openslr.org/resources/12/dev-clean.tar.gz
wget http://www.openslr.org/resources/12/test-clean.tar.gz
mkdir audio
cd audio
tar xvzf ../train-clean-100.tar.gz LibriSpeech/train-clean-100 --strip-components=1
tar xvzf ../dev-clean.tar.gz LibriSpeech/dev-clean --strip-components=1
tar xvzf ../test-clean.tar.gz LibriSpeech/test-clean --strip-components=1
这个过程需要一些时间,完成后我们将得到如下截图所示的data文件夹结构:

我们现在有三个文件夹,分别名为train-clean-100、dev-clean和test-clean。每个文件夹都有一些子文件夹,这些子文件夹包含与成绩单和音频的小段落映射相关的 ID。所有音频文件都为.flac扩展名,每个文件夹中都会有一个.txt文件,这是音频文件的成绩单。
语料库探索
让我们详细探讨数据集。首先,我们通过从文件读取音频文件并绘制它来查看音频文件。为了读取音频文件,我们将使用pysoundfile包,命令如下:
pip install pysoundfile
接下来,我们将导入模块,读取音频文件,并通过以下代码块绘制它们:
import soundfile as sf
import matplotlib.pyplot as plt
def plot_audio(audio):
fig, axs = plt.subplots(4, 1, figsize=(20, 7))
axs[0].plot(audio[0]);
axs[0].set_title('Raw Audio Signals')
axs[1].plot(audio[1]);
axs[2].plot(audio[2]);
axs[3].plot(audio[3]);
audio_list =[]
for i in xrange(4):
file_path = 'data/128684/911-128684-000{}.flac'.format(i+1)
a, sample_rate = sf.read(file_path)
audio_list.append(a)
plot_audio(audio_list)
以下是每个语音段的频率表示:

来自音频 MIDI 文件的原始音频信号图
现在让我们看一下成绩单文本文件的内容。它是文本的清洁版本,开头是音频文件 ID,后面是相关的文本:


成绩单数据以特定格式存储。左边的数字是 midi 文件名,右边是实际的成绩单。这有助于建立 midi 文件与其相应成绩单之间的映射。
我们看到每个音频文件都是该文件中成绩单的叙述。我们的模型将尝试学习这个序列模式。但在我们处理模型之前,我们需要从音频文件中提取一些特征,并将文本转换为独热编码格式。
特征工程
所以,在将原始音频数据输入我们的模型之前,我们需要将数据转化为数值表示,即特征。在本节中,我们将探索从语音数据中提取特征的各种技术,这些特征可以用于输入到模型中。模型的准确性和性能取决于我们使用的特征类型。作为一个富有好奇心的深度学习工程师,这是你探索和学习特征的机会,并使用最适合当前用例的特征。
以下表格给出了技术及其属性的列表:
| 技术 | 属性 |
|---|---|
| 主成分分析 (PCA) |
-
基于特征向量的方法
-
非线性特征提取方法
-
支持线性映射
-
比其他技术更快
-
适合高斯数据
|
| 线性判别分析 (LDA) |
|---|
-
线性特征提取方法
-
支持监督式线性映射
-
比其他技术更快
-
对分类来说优于 PCA
|
| 独立成分分析 (ICA) |
|---|
-
盲源分离方法
-
支持线性映射
-
本质上是迭代的
-
适合非高斯数据
|
| 倒谱分析 |
|---|
-
静态特征提取方法
-
功率谱方法
-
用于表示谱包络
|
| 梅尔频率尺度分析 |
|---|
-
静态特征提取方法
-
谱分析方法
-
计算梅尔尺度
|
| 梅尔频率倒谱系数 (MFCCs) |
|---|
-
功率谱是通过进行傅里叶分析来计算的
-
语音特征提取的稳健且动态的方法
|
| 小波技术 |
|---|
-
比傅里叶变换更好的时间分辨率
-
实时因子最小
|
MFCC 技术是最有效的,通常用于提取语音识别的语音特征。MFCC 基于人耳的临界带宽频率的已知变化,低频部分的滤波器是线性间隔的。MFCC 的过程如下图所示:

MFCC 过程的框图
对于我们的实现目的,我们不打算执行每个步骤;相反,我们将使用一个名为python_speech_features的 Python 包,它提供了 ASR 常用的语音特征,包括 MFCC 和滤波器组能量。
让我们使用以下命令pip install安装该包:
pip install python_speech_features
所以,让我们定义一个函数来归一化音频时间序列数据并提取 MFCC 特征:
from python_speech_features import mfcc
def compute_mfcc(audio_data, sample_rate):
''' Computes the MFCCs.
Args:
audio_data: time series of the speech utterance.
sample_rate: sampling rate.
Returns:
mfcc_feat:[num_frames x F] matrix representing the mfcc.
'''
audio_data = audio_data - np.mean(audio_data)
audio_data = audio_data / np.max(audio_data)
mfcc_feat = mfcc(audio_data, sample_rate, winlen=0.025, winstep=0.01,
numcep=13, nfilt=26, nfft=512, lowfreq=0, highfreq=None,
preemph=0.97, ceplifter=22, appendEnergy=True)
return mfcc_feat
让我们绘制音频和 MFCC 特征并可视化它们:
audio, sample_rate = sf.read(file_path)
feats[audio_file] = compute_mfcc(audio, sample_rate)
plot_audio(audio,feats[audio_file])
以下是频谱图的输出:

数据转换
一旦我们获得了需要输入模型的所有特征,我们将把原始的 NumPy 张量转换为 TensorFlow 特定的格式——TFRecords。
在下面的代码片段中,我们正在创建文件夹以存储所有已处理的记录。make_example()函数根据序列长度、MFCC 特征和相应的转录创建单一发音的序列示例。然后,使用tf.python_io.TFRecordWriter()函数将多个序列记录写入 TFRecord 文件:
if os.path.basename(partition) == 'train-clean-100':
# Create multiple TFRecords based on utterance length for training
writer = {}
count = {}
print('Processing training files...')
for i in range(min_t, max_t+1):
filename = os.path.join(write_dir, 'train' + '_' + str(i) +
'.tfrecords')
writer[i] = tf.python_io.TFRecordWriter(filename)
count[i] = 0
for utt in tqdm(sorted_utts):
example = make_example(utt_len[utt], feats[utt].tolist(),
transcripts[utt])
index = int(utt_len[utt]/100)
writer[index].write(example)
count[index] += 1
for i in range(min_t, max_t+1):
writer[i].close()
print(count)
# Remove bins which have fewer than 20 utterances
for i in range(min_t, max_t+1):
if count[i] < 20:
os.remove(os.path.join(write_dir, 'train' +
'_' + str(i) + '.tfrecords'))
else:
# Create single TFRecord for dev and test partition
filename = os.path.join(write_dir, os.path.basename(write_dir) +
'.tfrecords')
print('Creating', filename)
record_writer = tf.python_io.TFRecordWriter(filename)
for utt in sorted_utts:
example = make_example(utt_len[utt], feats[utt].tolist(),
transcripts[utt])
record_writer.write(example)
record_writer.close()
print('Processed '+str(len(sorted_utts))+' audio files')
所有数据处理代码都写在preprocess_LibriSpeech.py文件中,该文件将执行所有先前提到的数据处理部分,操作完成后,处理后的数据将存储在data/librispeech/processed/位置。使用以下命令运行该文件:
python preprocess_LibriSpeech.py
DS2 模型描述与直觉
DS2 架构由多个递归连接层、卷积滤波器和非线性层组成,还包括批量归一化对 RNN 的特定实例的影响,如下所示:

为了从包含大量数据的数据集进行学习,DS2 模型通过增加更多深度来提升容量。架构由最多 11 层的双向递归层和卷积层组成。为了成功优化这些模型,使用了 RNN 的批量归一化和一种名为 SortaGrad 的新优化课程。
训练数据是输入序列x(i)和转录y(i)的组合,而 RNN 层的目标是学习x(i)和y(i)之间的特征:
training set X = {(x(1), y(1)), (x(2), y(2)), . . .}
utterance = x(i)
label = y(i)
用于系统特征的输入是功率归一化后的音频剪辑的谱图,网络的输出是每种语言的字形。为了增加非线性,使用了修正线性单元(ReLU)函数,σ(x) = min{max{x, 0}, 20}。在双向循环层之后,放置一个或多个全连接层,输出层L是 softmax 层,计算字符的概率分布。
现在让我们深入了解 DS2 架构的实现。你可以在这里找到完整代码:github.com/PacktPublishing/Python-Deep-Learning-Projects/tree/master/Chapter07。
以下是模型在 TensorBoard 中的显示:

对于卷积层,我们有一个大小为[11, input_seq_length, number_of_filter]的卷积核,接着对输入序列进行 2D 卷积操作,然后应用dropout以防止过拟合。
以下代码段执行这些步骤:
with tf.variable_scope('conv1') as scope:
kernel = _variable_with_weight_decay(
'weights',
shape=[11, feat_len, 1, params.num_filters],
wd_value=None, use_fp16=params.use_fp16)
feats = tf.expand_dims(feats, dim=-1)
conv = tf.nn.conv2d(feats, kernel,
[1, params.temporal_stride, 1, 1],
padding='SAME')
biases = _variable_on_cpu('biases', [params.num_filters],
tf.constant_initializer(-0.05),
params.use_fp16)
bias = tf.nn.bias_add(conv, biases)
conv1 = tf.nn.relu(bias, name=scope.name)
_activation_summary(conv1)
# dropout
conv1_drop = tf.nn.dropout(conv1, params.keep_prob)
接下来,我们进入循环层,在这里我们将卷积层的输出重塑,以便将数据适配到 RNN 层。然后,根据一个叫做rnn_type的超参数创建自定义的 RNN 单元,它可以是单向的或双向的,之后是 dropout 单元。
以下代码块创建了模型的 RNN 部分:
with tf.variable_scope('rnn') as scope:
# Reshape conv output to fit rnn input
rnn_input = tf.reshape(conv1_drop, [params.batch_size, -1,
feat_len*params.num_filters])
# Permute into time major order for rnn
rnn_input = tf.transpose(rnn_input, perm=[1, 0, 2])
# Make one instance of cell on a fixed device,
# and use copies of the weights on other devices.
cell = rnn_cell.CustomRNNCell(
params.num_hidden, activation=tf.nn.relu6,
use_fp16=params.use_fp16)
drop_cell = tf.contrib.rnn.DropoutWrapper(
cell, output_keep_prob=params.keep_prob)
multi_cell = tf.contrib.rnn.MultiRNNCell(
[drop_cell] * params.num_rnn_layers)
seq_lens = tf.div(seq_lens, params.temporal_stride)
if params.rnn_type == 'uni-dir':
rnn_outputs, _ = tf.nn.dynamic_rnn(multi_cell, rnn_input,
sequence_length=seq_lens,
dtype=dtype, time_major=True,
scope='rnn',
swap_memory=True)
else:
outputs, _ = tf.nn.bidirectional_dynamic_rnn(
multi_cell, multi_cell, rnn_input,
sequence_length=seq_lens, dtype=dtype,
time_major=True, scope='rnn',
swap_memory=True)
outputs_fw, outputs_bw = outputs
rnn_outputs = outputs_fw + outputs_bw
_activation_summary(rnn_outputs)
此外,创建了线性层来执行 CTC 损失函数,并从 softmax 层输出结果:
with tf.variable_scope('softmax_linear') as scope:
weights = _variable_with_weight_decay(
'weights', [params.num_hidden, NUM_CLASSES],
wd_value=None,
use_fp16=params.use_fp16)
biases = _variable_on_cpu('biases', [NUM_CLASSES],
tf.constant_initializer(0.0),
params.use_fp16)
logit_inputs = tf.reshape(rnn_outputs, [-1, cell.output_size])
logits = tf.add(tf.matmul(logit_inputs, weights),
biases, name=scope.name)
logits = tf.reshape(logits, [-1, params.batch_size, NUM_CLASSES])
_activation_summary(logits)
生产规模提示:在这些规模下训练单个模型需要数十个 exaFLOP,单 GPU 执行这些任务需要三到六周的时间。这使得模型探索变得非常耗时,因此 DeepSpeech 的开发者们构建了一个高度优化的训练系统,使用八个或十六个 GPU 来训练一个模型,并且使用同步随机梯度下降(SGD),这种方法在测试新想法时更容易调试,同时对于相同的数据并行度也能更快地收敛。
训练模型
现在我们已经理解了使用的数据和 DeepSpeech 模型架构,接下来让我们设置环境以训练模型。创建项目的虚拟环境有一些预备步骤,这些步骤是可选的,但强烈建议使用。另外,建议使用 GPU 来训练这些模型。
除了 Python 版本 3.5 和 TensorFlow 版本 1.7+,以下是一些先决条件:
-
python-Levenshtein:用来计算字符错误率(CER),基本上是计算距离 -
python_speech_features:用来从原始数据中提取 MFCC 特征 -
pysoundfile:用来读取 FLAC 文件 -
scipy:用于窗口化的辅助函数 -
tqdm: 用于显示进度条
让我们创建虚拟环境并安装所有依赖项:
conda create -n 'SpeechProject' python=3.5.0
source activate SpeechProject
安装以下依赖项:
(SpeechProject)$ pip install python-Levenshtein
(SpeechProject)$ pip install python_speech_features
(SpeechProject)$ pip install pysoundfile
(SpeechProject)$ pip install scipy
(SpeechProject)$ pip install tqdm
安装支持 GPU 的 TensorFlow:
(SpeechProject)$ conda install tensorflow-gpu
如果看到sndfile错误,请使用以下命令:
(SpeechProject)$ sudo apt-get install libsndfile1
现在,你需要克隆包含所有代码的仓库:
(SpeechRecog)$ git clone https://github.com/FordSpeech/deepSpeech.git
(SpeechRecog)$ cd deepSpeech
让我们移动在数据转换部分中创建的 TFRecord 文件。计算得到的 MFCC 特征存储在data/librispeech/processed/目录中:
cp -r ./data/librispeech/audio /home/deepSpeech/data/librispeech
cp -r ./data/librispeech/processed /home/deepSpeech/librispeech
一旦我们将所有数据文件准备好,就可以开始训练模型。我们定义了四个超参数:num_rnn_layers设置为3,rnn_type设置为bi-dir,max_steps设置为30000,initial_lr设置为3e-4:
(SpeechRecog)$python deepSpeech_train.py --num_rnn_layers 3 --rnn_type 'bi-dir' --initial_lr 3e-4 --max_steps 30000 --train_dir ./logs/
此外,如果你想使用drive.google.com/file/d/1E65g4HlQU666RhgY712Sn6FuU2wvZTnQ/view中的预训练模型继续训练,可以下载并解压到logs文件夹中:
(SpeechRecog)$python deepSpeech_train.py --checkpoint_dir ./logs/ --max_steps 40000
请注意,在第一个 epoch 期间,成本将增加,且随着后续步骤的训练,训练时间会变得更长,因为语音样本是按照排序顺序呈现给网络的。
以下是训练过程中涉及的步骤:
# Learning rate set up from the hyper-param.
learning_rate, global_step = set_learning_rate()
# Create an optimizer that performs gradient descent.
optimizer = tf.train.AdamOptimizer(learning_rate)
# Fetch a batch worth of data for each tower to train.
data = fetch_data()
# Construct loss and gradient ops.
loss_op, tower_grads, summaries = get_loss_grads(data, optimizer)
# Calculate the mean of each gradient. Note that this is the synchronization point across all towers.
grads = average_gradients(tower_grads)
# Apply the gradients to adjust the shared variables.
apply_gradient_op = optimizer.apply_gradients(grads,
global_step=global_step)
# Track the moving averages of all trainable variables.
variable_averages = tf.train.ExponentialMovingAverage(
ARGS.moving_avg_decay, global_step)
variables_averages_op = variable_averages.apply(
tf.trainable_variables())
# Group all updates to into a single train op.
train_op = tf.group(apply_gradient_op, variables_averages_op)
# Build summary op.
summary_op = add_summaries(summaries, learning_rate, grads)
# Create a saver.
saver = tf.train.Saver(tf.all_variables(), max_to_keep=100)
# Start running operations on the Graph with allow_soft_placement set to True
# to build towers on GPU.
sess = tf.Session(config=tf.ConfigProto(
allow_soft_placement=True,
log_device_placement=ARGS.log_device_placement))
# Initialize vars depending on the checkpoints.
if ARGS.checkpoint is not None:
global_step = initialize_from_checkpoint(sess, saver)
else:
sess.run(tf.initialize_all_variables())
# Start the queue runners.
tf.train.start_queue_runners(sess)
# Run training loop.
run_train_loop(sess, (train_op, loss_op, summary_op), saver)
在训练过程中,我们可以看到显著的改进,如下图所示。以下图表显示了 50k 步之后的准确率:

以下是 50k 步骤的损失图:

学习率随着时间的推移逐渐减小:

测试和评估模型
一旦模型训练完成,你可以执行以下命令,使用test数据集执行test步骤:
(SpeechRecog)$python deepSpeech_test.py --eval_data 'test' --checkpoint_dir ./logs/
我们通过在先前未见过的语音样本上测试模型来评估其性能。模型生成概率向量的序列作为输出,因此我们需要构建解码器,将模型的输出转换为单词序列。尽管在字符序列上训练,DS2 模型仍能学习到一个隐式的语言模型,并且已经相当擅长根据发音拼写单词,正如以下表格所示。模型的拼写性能通常通过计算基于 Levenshtein 距离(en.wikipedia.org/wiki/Levenshtein_distance)的字符级 CER 来衡量:
| 真实值 | 模型输出 |
|---|---|
| 这对他起到了安抚作用 | 这对他起到了安抚作用 |
| 他进去查看了信件,但没有来自快递员的信 | 他进去查看了信件,但没有来自快递员的信 |
| 设计不同,但东西显然是一样的 | 设计不同,但东西显然是一样的 |
尽管模型展示了优秀的 CER(字符错误率),但它们倾向于按音标拼写单词,导致相对较高的词错误率(WER)。你可以通过允许解码器结合外部词典和语言模型的约束来改善模型的词错误率(WER)。
我们观察到,模型预测中的许多错误发生在训练集中没有出现的单词上。因此,随着我们增加训练集的大小和训练步骤,整体 CER 有望持续改善。经过 30k 步训练后,它达到了 15%的 CER。
总结
我们直接进入了这个 Python 深度学习项目,创建并训练了一个能够理解语音数据的 ASR 模型。我们学习了如何对语音数据进行特征工程,从中提取各种特征,然后构建一个能够识别用户声音的语音识别系统。
我们很高兴达成了我们设定的目标!
在本章中,我们构建了一个识别英语语音的系统,使用了 DS2 模型。
你学习了以下内容:
-
使用语音和声谱图进行工作
-
构建一个端到端的语音识别系统
-
CTC 损失函数
-
批量归一化和用于 RNN 的 SortaGrad
这标志着本书中深度学习项目的一个主要部分的结束,这些项目涉及聊天机器人、自然语言处理(NLP)和使用 RNN(单向和双向,包括或不包括 LSTM 组件)以及 CNN 的语音识别。我们已经看到了这些技术为现有业务流程提供智能,以及创造全新智能系统的潜力。这是应用 AI 领域中令人兴奋的前沿工作,利用深度学习推动创新!在本书的下半部分,我们将探索通常归类为计算机视觉技术的 Python 深度学习项目。
让我们翻到下一页,开始吧!
第八章:使用 ConvNets 进行手写数字分类
欢迎来到本章,介绍如何使用卷积神经网络(ConvNets)对手写数字进行分类。在第二章,《使用回归训练神经网络进行预测》中,我们建立了一个简单的神经网络来分类手写数字。这个网络的准确率为 87%,但我们对其表现并不满意。在本章中,我们将了解卷积的概念,并构建一个 ConvNet 来对手写数字进行分类,帮助餐饮连锁公司更准确地将短信发送给正确的人。如果你还没有学习过第二章《使用回归训练神经网络进行预测》,请先学习它,这样你能更好地理解案例背景。
本章将涵盖以下主题:
-
卷积
-
池化
-
Dropout(丢弃法)
-
训练模型
-
测试模型
-
构建更深的模型
如果你在学习本章时边做边实现代码片段,最好使用 Jupyter Notebook 或任何源代码编辑器。这将帮助你更轻松地跟进并理解代码的不同部分是如何工作的。
本章所有 Python 文件和 Jupyter Notebook 文件可以在github.com/PacktPublishing/Python-Deep-Learning-Projects/tree/master/Chapter08找到。
代码实现
在本次练习中,我们将使用 Keras 深度学习库,它是一个高层次的神经网络 API,能够在 TensorFlow、Theano 和 CNTK 之上运行。
了解代码!我们不会花时间去理解 Keras 是如何工作的,但如果你感兴趣,可以参考 Keras 官方提供的这份简单易懂的文档:keras.io/。
导入所有依赖项
在本次练习中,我们将使用numpy、matplotlib、keras、scipy和tensorflow这些包。这里,TensorFlow 作为 Keras 的后端。你可以通过pip安装这些包。对于 MNIST 数据集,我们将使用keras模块中的数据集,并通过简单的import导入:
import numpy as np
设置seed以确保结果可复现非常重要:
# set seed for reproducibility
seed_val = 9000
np.random.seed(seed_val)
探索数据
让我们用以下代码导入keras中的mnist模块:
from keras.datasets import mnist
然后,使用以下代码解压mnist的训练和测试图片:
# unpack mnist data
(X_train, y_train), (X_test, y_test) = mnist.load_data()
现在数据已导入,让我们来探索这些数字:
print('Size of the training_set: ', X_train.shape)
print('Size of the test_set: ', X_test.shape)
print('Shape of each image: ', X_train[0].shape)
print('Total number of classes: ', len(np.unique(y_train)))
print('Unique class labels: ', np.unique(y_train))
以下是前述代码的输出:

图 8.1:数据的打印信息
从前面的截图中,我们可以看到,我们有60000张训练图片,10000张测试图片,每张图片的大小为28*28,总共有10个可预测类别。
现在,让我们绘制9个手写数字。在此之前,我们需要导入matplotlib库用于绘图:
import matplotlib.pyplot as plt
# Plot of 9 random images
for i in range(0, 9):
plt.subplot(331+i) # plot of 3 rows and 3 columns
plt.axis('off') # turn off axis
plt.imshow(X_train[i], cmap='gray') # gray scale
以下是前面代码的输出:

图 8.2:可视化 MNIST 数字
打印出training_set中像素的最大值和最小值:
# maximum and minimum pixel values
print('Maximum pixel value in the training_set: ', np.max(X_train))
print('Minimum pixel value in the training_set: ', np.min(X_train))
以下是前面代码的输出:

图 8.3:数据中最大和最小像素值的打印输出
我们可以看到,训练集中像素的最大值和最小值分别为255和0。
定义超参数
以下是我们将在代码中使用的一些超参数。这些参数都是可配置的:
# Number of epochs
epochs = 20
# Batchsize
batch_size = 128
# Optimizer for the generator from keras.optimizers import Adam
optimizer = Adam(lr=0.0001)
# Shape of the input image
input_shape = (28,28,1)
如果回顾一下第二章,使用回归训练神经网络进行预测,你会看到当时使用的optimizer是Adam。因此,我们将从keras模块导入Adam优化器,并设置其学习率,如前面的代码所示。在接下来的大多数情况中,我们将训练20个epochs,以便于比较。
要了解更多关于 Keras 中的optimizers及其 API,请访问 keras.io/optimizers/。
尝试不同的学习率、优化器和批量大小,看看这些因素如何影响模型的质量。如果你得到更好的结果,可以向深度学习社区展示。
构建和训练一个简单的深度神经网络
现在我们已经将数据加载到内存中,接下来需要构建一个简单的神经网络模型来预测 MNIST 数字。我们将使用与第二章中相同的架构,使用回归训练神经网络进行预测。
我们将构建一个Sequential模型。所以,让我们从 Keras 中导入它,并用以下代码初始化它:
from keras.models import Sequential
model = Sequential()
要了解更多关于 Keras 模型 API 的信息,请访问 keras.io/models/model/。
接下来,我们需要定义Dense/感知机层。在 Keras 中,可以通过导入Dense层来完成,如下所示:
from keras.layers import Dense
然后,我们需要将Dense层添加到Sequential模型中,如下所示:
model.add(Dense(300, input_shape=(784,), activation = 'relu'))
要了解更多关于 Keras Dense API 的信息,请访问 keras.io/layers/core/。
add命令用于将一个层追加到Sequential模型中,在这种情况下是Dense层。
在前面的代码中的Dense层中,我们定义了第一隐藏层的神经元数量,即300。我们还将input_shape参数定义为(784,),以便告诉模型它将接受形状为(784,)的输入数组。这意味着输入层将有784个神经元。
需要应用于结果的激活函数类型可以通过 activation 参数定义。在本例中,这是 relu。
使用以下代码添加另一个包含 300 个神经元的 Dense 层:
model.add(Dense(300, activation='relu'))
以及具有以下代码的最终 Dense 层:
model.add(Dense(10, activation='softmax'))
这里,最终层有 10 个神经元,因为我们需要它预测 10 个类别的分数。这里选择的 activation 函数是 softmax,以便我们将分数限制在 0 到 1 之间,并使所有分数之和为 1。
在 Keras 中编译模型非常简单,可以通过以下代码完成:
# compile the model
model.compile(loss = 'sparse_categorical_crossentropy', optimizer=optimizer , metrics = ['accuracy'])
编译模型时,所需要做的就是调用模型的 compile 方法,并指定 loss、optimizer 和 metrics 参数,在本例中分别为 sparse_categorical_crossentropy、Adam 和 ['accuracy']。
要了解更多关于 Keras Model 的 compile 方法,请访问 keras.io/models/model/。
在学习过程中需要监控的指标必须作为列表传递给 compile 方法的 metrics 参数。
使用以下代码打印出模型的摘要:
# print model summary
model.summary()
以下是前述代码的输出:

图 8.4:多层感知器模型的摘要
请注意,该模型有 328,810 个可训练参数,这是合理的。
现在,使用从 sklearn 导入的 train_test_split 函数,将训练数据拆分为训练和验证数据:
from sklearn.model_selection import train_test_split
# create train and validation data
X_train, X_val, y_train, y_val = train_test_split(X_train, y_train, stratify = y_train, test_size = 0.08333, random_state=42)
X_train = X_train.reshape(-1, 784)
X_val = X_val.reshape(-1, 784)
X_test = X_test.reshape(-1, 784)
print('Training Examples', X_train.shape[0])
print('Validation Examples', X_val.shape[0])
print('Test Examples', X_test.shape[0])
我们将数据分割成了 55,000 个训练示例和 5,000 个验证示例。
你还会看到我们已经重塑了数组,使得每个图像的形状为 (784,)。这是因为我们已经定义了模型接受形状为 (784,) 的图像/数组。
正如我们在 第二章 中所做的,使用回归进行预测训练神经网络,我们现在将在 55,000 个训练示例上训练模型,在 5,000 个示例上验证,并在 10,000 个示例上测试。
将拟合结果赋值给一个变量,会存储相关信息,如每个 epoch 的训练和验证损失及准确率,之后可以用来绘制学习过程。
拟合模型
在 Keras 中拟合模型时,结合训练数字和训练标签,调用模型的 fit 方法,并传入以下参数:
-
epochs:训练的轮数 -
batch_size:每个批次中的图像数量 -
validation_data:验证图像和验证标签的元组
查看本章的 定义超参数 部分,获取 epochs 和 batch_size 的定义值:
# fit the model
history = model.fit(X_train, y_train, epochs = epochs, batch_size=batch_size, validation_data=(X_val, y_val))
以下是前述代码的输出:

以下是代码执行结束时的输出:

图 8.5:在 MLP 训练过程中打印出的指标
评估模型
为了在测试数据上评估模型,你可以通过将测试图像和测试标签传入model的evaluate方法来进行评估:
# evaluate the model
loss, acc = model.evaluate(X_test, y_test)
print('Test loss:', loss)
print('Accuracy:', acc)
以下是前面代码的输出:

图 8.6:MLP 评估的打印输出
从验证和测试准确率来看,我们可以看到,在经过 20 个周期的训练后,我们达到了与第二章中使用回归训练神经网络进行预测相同的准确率,但代码量非常少。
现在,让我们定义一个函数,绘制我们存储在history变量中的训练和验证损失及准确率:
import matplotlib.pyplot as plt
def loss_plot(history):
train_acc = history.history['acc']
val_acc = history.history['val_acc']
plt.figure(figsize=(9,5))
plt.plot(np.arange(1,21),train_acc, marker = 'D', label = 'Training Accuracy')
plt.plot(np.arange(1,21),val_acc, marker = 'o', label = 'Validation Accuracy')
plt.xlabel('Epochs')
plt.ylabel('Accuracy')
plt.title('Train/Validation Accuracy')
plt.legend()
plt.margins(0.02)
plt.show()
train_loss = history.history['loss']
val_loss = history.history['val_loss']
plt.figure(figsize=(9,5))
plt.plot(np.arange(1,21),train_loss, marker = 'D', label = 'Training Loss')
plt.plot(np.arange(1,21),val_loss, marker = 'o', label = 'Validation Loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.title('Train/Validation Loss')
plt.legend()
plt.margins(0.02)
plt.show()
# plot training loss
loss_plot(history)
以下是前面代码的输出:

图 8.7:训练过程中 MLP 损失/准确率图
MLP – Python 文件
该模块实现了一个简单 MLP 的训练和评估:
"""This module implements a simple multi layer perceptron in keras."""
import numpy as np
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from loss_plot import loss_plot
# Number of epochs
epochs = 20
# Batchsize
batch_size = 128
# Optimizer for the generator
from keras.optimizers import Adam
optimizer = Adam(lr=0.0001)
# Shape of the input image
input_shape = (28,28,1)
(X_train, y_train), (X_test, y_test) = mnist.load_data()
X_train, X_val, y_train, y_val = train_test_split(X_train, y_train,
stratify = y_train,
test_size = 0.08333,
random_state=42)
X_train = X_train.reshape(-1, 784)
X_val = X_val.reshape(-1, 784)
X_test = X_test.reshape(-1, 784)
model = Sequential()
model.add(Dense(300, input_shape=(784,), activation = 'relu'))
model.add(Dense(300, activation='relu'))
model.add(Dense(10, activation='softmax'))
model.compile(loss = 'sparse_categorical_crossentropy', optimizer=optimizer,
metrics = ['accuracy'])
history = model.fit(X_train, y_train, epochs = epochs, batch_size=batch_size,
validation_data=(X_val, y_val))
loss,acc = model.evaluate(X_test, y_test)
print('Test loss:', loss)
print('Accuracy:', acc)
loss_plot(history)
卷积
卷积可以定义为将一个小的卷积核/滤波器/数组沿着目标数组滑动,并在每个位置计算卷积核与目标数组相同大小子集的逐元素相乘的和。
考虑以下示例:
array = np.array([0, 1, 0, 1, 0, 1, 0, 1, 0, 1])
kernel = np.array([-1, 1, 0])
这里,你有一个长度为 10 的目标array和一个长度为 3 的kernel。
当你开始卷积时,执行以下步骤:
-
kernel将与目标array中索引 0 到 2 的子集进行相乘。这将是[-1,1,0](卷积核)与[0,1,0](目标数组索引 0 到 2 的部分)。这个逐元素相乘的结果将被求和,得到所谓的卷积结果。 -
然后,
kernel将按 1 单位步幅移动,并与目标array中索引 1 到 3 的子集相乘,像步骤 1那样,得到结果。 -
步骤 2会重复执行,直到在新的步幅位置无法提取出与
kernel长度相等的子集。
每一步卷积的结果都存储在一个array中。这个存储卷积结果的array被称为特征图。1 维特征图的长度(步幅为 1 时)等于kernel和目标array长度的差值再加 1。
只有在这种情况下,我们需要考虑以下方程:
特征图的长度 = 目标数组的长度 - 卷积核的长度 + 1
这里有一个实现 1 维卷积的代码片段:
array = np.array([0, 1, 0, 1, 0, 1, 0, 1, 0, 1])
kernel = np.array([-1, 1, 0])
# empty feature map
conv_result = np.zeros(array.shape[0] - kernel.shape[0] +1).astype(int)
for i in range(array.shape[0] - kernel.shape[0] +1):
# convolving
conv_result[i] = (kernel * array[i:i+3]).sum()
print(kernel, '*', array[i:i+3], '=', conv_result[i])
print('Feature Map :', conv_result)
以下是前面代码的输出:

图 8.8:示例特征图的打印输出
Keras 中的卷积
现在你已经了解了卷积是如何工作的,让我们将其付诸实践,构建一个基于 MNIST 数字的 CNN 分类器。
为此,导入 Keras 的layers模块中的Conv2D API。你可以使用以下代码来实现:
from keras.layers import Conv2D
由于卷积会被定义为接受28281形状的图像,因此我们需要将所有图像重塑为28281:
# reshape data
X_train = X_train.reshape(-1,28,28,1)
X_val = X_val.reshape(-1,28,28,1)
X_test = X_test.reshape(-1,28,28,1)
print('Train data shape:', X_train.shape)
print('Val data shape:', X_val.shape)
print('Test data shape:', X_test.shape)
以下是前述代码的输出:

图 8.9:重塑后的数据形状
要构建 model,就像之前那样,我们需要将 model 初始化为 Sequential:
model = Sequential()
现在,使用以下代码将 Conv2D 层添加到 model 中:
model.add(Conv2D(32, kernel_size=(3,3), input_shape=input_shape, activation = 'relu'))
在 Conv2D API 中,我们定义了以下参数:
-
units:32(卷积核/滤波器的数量) -
kernel_size:(3,3)(每个卷积核的大小) -
input_shape:28*28*1(它将接收的输入数组的形状) -
activation:relu
有关 Conv2D API 的更多信息,请访问 keras.io/layers/convolutional/。
前述卷积操作的结果是 32 个 26*26 的特征图。现在,这些 2-D 特征图需要转换成 1-D 特征图。这可以通过以下 Keras 代码完成:
from keras.layers import Flatten
model.add(Flatten())
前述代码片段的结果就像一个简单神经网络中的神经元层。Flatten 函数将所有的 2-D 特征图转换为一个单一的 Dense 层。在此层中,我们将添加一个包含 128 个神经元的 Dense 层:
model.add(Dense(128, activation = 'relu'))
由于我们需要获取每个 10 个可能类别的分数,我们必须添加另一个包含 10 个神经元的 Dense 层,并使用 softmax 作为 activation 函数:
model.add(Dense(10, activation = 'softmax'))
现在,就像我们在前述代码中构建的简单全连接神经网络一样,我们将编译并拟合模型:
# compile model
model.compile(loss = 'sparse_categorical_crossentropy', optimizer=optimizer, metrics = ['accuracy'])
# print model summary
model.summary()
以下是前述代码的输出:

图 8.10:卷积分类器的总结
从模型的总结中,我们可以看到,这个卷积分类器有 2,770,634 个参数。与感知机模型相比,这是一个非常庞大的参数数量。让我们拟合这个模型并评估其性能。
拟合模型
使用以下代码将卷积神经网络模型拟合到数据:
# fit model
history = model.fit(X_train, y_train, epochs = epochs, batch_size=batch_size, validation_data=(X_val, y_val))
以下是前述代码的输出:

以下是代码执行结束时的输出:

图 8.11:卷积分类器训练过程中打印出的指标
我们可以看到,卷积分类器在验证数据上的准确率为 97.72%。
评估模型
您可以使用以下代码评估卷积模型在测试数据上的表现:
# evaluate model
loss,acc = model.evaluate(X_test, y_test)
print('Test loss:', loss)
print('Accuracy:', acc)
以下是前述代码的输出:

图 8.12:卷积分类器评估的打印输出
我们可以看到,模型在测试数据上的准确率为 97.92%,在验证数据上的准确率为 97.72%,在训练数据上的准确率为 99.71%。从损失情况来看,模型在训练数据上有些过拟合。我们稍后将讨论如何处理过拟合问题。
现在,让我们绘制训练和验证指标,以查看训练进展情况:
# plot training loss
loss_plot(history)
以下是前述代码的输出:

图 8.13:卷积分类器训练过程中的损失/准确率图
卷积 – Python 文件
该模块实现了卷积分类器的训练和评估:
"""This module implements a simple convolution classifier."""
import numpy as np
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense, Conv2D, Flatten
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from loss_plot import loss_plot
# Number of epochs
epochs = 20
# Batchsize
batch_size = 128
# Optimizer for the generator
from keras.optimizers import Adam
optimizer = Adam(lr=0.0001)
# Shape of the input image
input_shape = (28,28,1)
(X_train, y_train), (X_test, y_test) = mnist.load_data()
X_train, X_val, y_train, y_val = train_test_split(X_train, y_train,
stratify = y_train,
test_size = 0.08333,
random_state=42)
X_train = X_train.reshape(-1,28,28,1)
X_val = X_val.reshape(-1,28,28,1)
X_test = X_test.reshape(-1,28,28,1)
model = Sequential()
model.add(Conv2D(32, kernel_size=(3,3), input_shape=input_shape,
activation = 'relu'))
model.add(Flatten())
model.add(Dense(128, activation = 'relu'))
model.add(Dense(10, activation='softmax'))
model.compile(loss = 'sparse_categorical_crossentropy', optimizer=optimizer,
metrics = ['accuracy'])
history = model.fit(X_train, y_train, epochs = epochs, batch_size=batch_size,
validation_data=(X_val, y_val))
loss,acc = model.evaluate(X_test, y_test)
print('Test loss:', loss)
print('Accuracy:', acc)
loss_plot(history)
池化
最大池化可以定义为将一组值用该组中的最大值进行总结的过程。类似地,如果计算平均值,那么就是平均池化。池化操作通常在卷积后生成的特征图上执行,以减少参数的数量。
让我们考虑卷积时使用的示例数组:
array = np.array([0, 1, 0, 1, 0, 1, 0, 1, 0, 1])
现在,如果你在这个 array 上执行最大池化,池大小设置为 12,步幅为 2,结果将是数组 [1,1,1,1,1]。这个 110 的 array 由于最大池化,已经被缩小为 1*5。
在这里,由于池大小为 1*2,你将从索引 0 到索引 2 提取目标 array 的子集,即 [0,1],然后计算该子集的最大值为 1。对于从索引 2 到索引 4、从索引 4 到索引 6、从索引 6 到索引 8,最后从索引 8 到 10,你都会执行相同的操作。
类似地,平均池化可以通过计算池化部分的平均值来实现。在这种情况下,结果数组将是 [0.5, 0.5, 0.5, 0.5, 0.5]。
以下是几个实现最大池化和平均池化的代码片段:
# 1D Max Pooling
array = np.array([0, 1, 0, 1, 0, 1, 0, 1, 0, 1])
result = np.zeros(len(array)//2)
for i in range(len(array)//2):
result[i] = np.max(array[2*i:2*i+2])
result
以下是前面代码的输出:

图 8.14:最大池化操作在数组上的结果
以下是平均池化的代码片段:
# 1D Average Pooling
array = np.array([0, 1, 0, 1, 0, 1, 0, 1, 0, 1])
result = np.zeros(len(array)//2)
for i in range(len(array)//2):
result[i] = np.mean(array[2*i:2*i+2])
result
以下是前面代码的输出:

图 8.15:平均池化操作在数组上的结果
以下是解释最大池化操作的图示:

图 8.16:2*2 最大池化,步幅为 2(来源:https://en.wikipedia.org/wiki/Convolutional_neural_network)
请考虑以下关于数字的代码:
plt.imshow(X_train[0].reshape(28,28), cmap='gray')
以下是前面代码的输出:

图 8.17:随机 MNIST 数字
该图像的形状是 2828。现在,如果你对其执行 22 最大池化操作,结果图像的形状将变为 14*14。
现在,让我们编写一个函数来实现 MNIST 数字的 2*2 最大池化操作:
def square_max_pool(image, pool_size=2):
result = np.zeros((14,14))
for i in range(result.shape[0]):
for j in range(result.shape[1]):
result[i,j] = np.max(image[i*pool_size : i*pool_size+pool_size, j*pool_size : j*pool_size+pool_size])
return result
# plot a pooled image
plt.imshow(square_max_pool(X_train[0].reshape(28,28)), cmap='gray')
以下是前面代码的输出:

图 8.18:最大池化后的随机 MNIST 数字
你可能已经注意到,我们在上一节中构建的卷积分类器大约有 270 万个参数。已经证明,在许多情况下,拥有大量参数会导致过拟合。此时池化操作就显得至关重要。它帮助我们保留数据中的重要特征,并减少参数的数量。
现在,让我们实现一个带有最大池化的卷积分类器。
使用以下代码从 Keras 导入最大池化操作:
from keras.layers import MaxPool2D
然后,定义并编译模型:
# model
model = Sequential()
model.add(Conv2D(32, kernel_size=(3,3), input_shape=input_shape, activation = 'relu'))
model.add(MaxPool2D(2,2))
model.add(Dropout(0.2))
model.add(Flatten())
model.add(Dense(128, activation = 'relu'))
model.add(Dense(10, activation = 'softmax'))
# compile model
model.compile(loss = 'sparse_categorical_crossentropy', optimizer= optimizer, metrics = ['accuracy'])
# print model summary
model.summary()
以下是前述代码的输出:

图 8.19:带最大池化的卷积分类器概述
从摘要中,我们可以看到,使用步长为 2 的 2*2 池化滤波器后,参数数量已降至693,962,是卷积分类器参数数量的 1/4。
拟合模型
现在,让我们在数据上拟合模型:
# fit model
history = model.fit(X_train, y_train, epochs = epochs, batch_size=batch_size, validation_data=(X_val, y_val))
以下是前述代码的输出:

以下是代码执行完毕后的输出:

图 8.20:带最大池化的卷积分类器训练过程中打印出的指标
我们可以看到,带最大池化的卷积分类器在验证数据上的准确率为 97.72%。
评估模型
现在,在测试数据上评估带最大池化的卷积模型:
# evaluate model
loss, acc = model.evaluate(X_test, y_test)
print('Test loss:', loss)
print('Accuracy:', acc)
以下是前述代码的输出:

图 8.21:带最大池化的卷积分类器评估的打印输出
我们可以看到,模型在测试数据上的准确率为 97.88%,在验证数据上的准确率为 97.72%,在训练数据上的准确率为 99.74%。带池化的卷积模型与不带池化的卷积模型表现相同,但参数减少了四倍。
在这种情况下,我们可以从损失值中清楚地看到,模型在训练数据上略微过拟合。
就像我们之前做的那样,绘制训练和验证指标,查看训练的进展:
# plot training loss
loss_plot(history)
以下是前述代码的输出:

图 8.22:带最大池化的卷积分类器训练过程中的损失/准确率图
卷积与池化 – Python 文件
本模块实现了带池化操作的卷积分类器的训练与评估:
"""This module implements a convolution classifier with maxpool operation."""
import numpy as np
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense, Conv2D, Flatten, MaxPool2D
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from loss_plot import loss_plot
# Number of epochs
epochs = 20
# Batchsize
batch_size = 128
# Optimizer for the generator
from keras.optimizers import Adam
optimizer = Adam(lr=0.0001)
# Shape of the input image
input_shape = (28,28,1)
(X_train, y_train), (X_test, y_test) = mnist.load_data()
X_train, X_val, y_train, y_val = train_test_split(X_train, y_train,
stratify = y_train,
test_size = 0.08333,
random_state=42)
X_train = X_train.reshape(-1,28,28,1)
X_val = X_val.reshape(-1,28,28,1)
X_test = X_test.reshape(-1,28,28,1)
model = Sequential()
model.add(Conv2D(32, kernel_size=(3,3), input_shape=input_shape,
activation='relu'))
model.add(MaxPool2D(2,2))
model.add(Flatten())
model.add(Dense(128, activation = 'relu'))
model.add(Dense(10, activation='softmax'))
model.compile(loss = 'sparse_categorical_crossentropy', optimizer=optimizer,
metrics = ['accuracy'])
history = model.fit(X_train, y_train, epochs = epochs, batch_size=batch_size,
validation_data=(X_val, y_val))
loss,acc = model.evaluate(X_test, y_test)
print('Test loss:', loss)
print('Accuracy:', acc)
loss_plot(history)
Dropout
Dropout 是一种正则化技术,用于防止过拟合。在训练过程中,它通过在每次前向和反向传播时随机从原始神经网络中采样一个神经网络,然后在输入数据的批次上训练这个子集网络来实现。测试时不进行 dropout。测试结果作为所有采样网络的集合获得:

图 8.23:Dropout,如在《Dropout:防止神经网络过拟合的一种简单方法》所示
过拟合论文(来源:http://www.cs.toronto.edu/~rsalakhu/papers/srivastava14a.pdf)
在 Keras 中,实现Dropout非常简单。首先,从keras的layers模块中导入它:
from keras.layers import Dropout
然后,将该层放置在需要的地方。对于我们的 CNN,我们将在最大池化操作后放置一个层,在Dense层后放置另一个层,如以下代码所示:
# model
model = Sequential()
model.add(Conv2D(32, kernel_size=(3,3), input_shape=input_shape, activation = 'relu'))
model.add(MaxPool2D(2,2))
model.add(Dropout(0.2))
model.add(Flatten())
model.add(Dense(128, activation = 'relu'))
model.add(Dropout(0.2))
model.add(Dense(10, activation = 'softmax'))
# compile model
model.compile(loss = 'sparse_categorical_crossentropy', optimizer= optimizer, metrics = ['accuracy'])
# model summary
model.summary()
以下是前述代码的输出:

图 8.24:卷积分类器总结
由于Dropout是一种正则化技术,将其添加到模型中不会改变可训练参数的数量。
拟合模型
再次在标准的 20 个epochs上训练模型:
# fit model
history = model.fit(X_train, y_train, epochs = epochs, batch_size=batch_size, validation_data=(X_val, y_val))
以下是前述代码的输出:

以下是代码执行结束时的输出:

图 8.25:训练过程中打印出的卷积分类器的指标,使用最大池化和丢弃法
我们看到使用最大池化和丢弃法的卷积分类器在验证数据上的准确率为 98.52%。
评估模型
现在,让我们评估模型并捕捉损失和准确率:
# evaluate model
loss, acc = model.evaluate(X_test, y_test)
print('Test loss:', loss)
print('Accuracy:', acc)
以下是前述代码的输出:

图 8.26:使用最大池化和丢弃法评估卷积分类器的输出
我们可以看到,模型在测试数据上的准确率为 98.42%,在验证数据上为 98.52%,在训练数据上为 99.26%。带有池化和丢弃法的卷积模型提供了与没有池化的卷积模型相同的性能水平,但参数量减少了四倍。如果你也查看loss,你会发现这个模型比我们之前训练的其他模型达到了一个更好的最小值。
绘制指标图以了解训练进度:
# plot training loss
loss_plot(history)
以下是前述代码的输出:

图 8.27:卷积分类器训练过程中,使用最大池化和丢弃法的损失/准确度曲线
使用池化的卷积 – Python 文件
本模块实现了一个带有最大池化和Dropout操作的卷积分类器的训练与评估:
"""This module implements a deep conv classifier with max pool and dropout."""
import numpy as np
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense, Conv2D, Flatten, MaxPool2D, Dropout
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from loss_plot import loss_plot
# Number of epochs
epochs = 20
# Batchsize
batch_size = 128
# Optimizer for the generator
from keras.optimizers import Adam
optimizer = Adam(lr=0.0001)
# Shape of the input image
input_shape = (28,28,1)
(X_train, y_train), (X_test, y_test) = mnist.load_data()
X_train, X_val, y_train, y_val = train_test_split(X_train, y_train,
stratify = y_train,
test_size = 0.08333,
random_state=42)
X_train = X_train.reshape(-1,28,28,1)
X_val = X_val.reshape(-1,28,28,1)
X_test = X_test.reshape(-1,28,28,1)
model = Sequential()
model.add(Conv2D(32, kernel_size=(3,3), input_shape=input_shape,
activation = 'relu'))
model.add(MaxPool2D(2,2))
model.add(Dropout(0.2))
model.add(Conv2D(64, kernel_size=(3,3), activation = 'relu'))
model.add(MaxPool2D(2,2))
model.add(Dropout(0.2))
model.add(Conv2D(128, kernel_size=(3,3), activation = 'relu'))
model.add(MaxPool2D(2,2))
model.add(Dropout(0.2))
model.add(Flatten())
model.add(Dense(128, activation = 'relu'))
model.add(Dropout(0.2))
model.add(Dense(10, activation = 'softmax'))
model.compile(loss = 'sparse_categorical_crossentropy', optimizer= optimizer,
metrics = ['accuracy'])
history = model.fit(X_train, y_train, epochs = epochs, batch_size=batch_size,
validation_data=(X_val, y_val))
loss,acc = model.evaluate(X_test, y_test)
print('Test loss:', loss)
print('Accuracy:', acc)
loss_plot(history)
向更深的网络进发
使用最大池化和丢弃法的卷积分类器似乎是迄今为止最好的分类器。然而,我们也注意到训练数据上存在轻微的过拟合现象。
让我们构建一个更深的模型,看看能否创建一个比我们迄今训练的其他模型更准确的分类器,并看看能否让它达到一个更好的最小值。
我们将通过向目前最好的模型中添加两个卷积层来构建一个更深的模型:
-
第一层是一个卷积 2D 层,包含 32 个大小为 33 的滤波器,
activation使用relu,接着进行大小为 22 的最大池化下采样,最后使用Dropout作为正则化方法 -
第二层是一个卷积 2D 层,包含 64 个大小为 33 的滤波器,
activation使用relu,接着进行大小为 22 的最大池化下采样,最后使用Dropout作为正则化方法 -
第三层是一个卷积二维层,具有 128 个 33 大小的滤波器,
activation为relu,接着是 22 大小的最大池化进行下采样,最后是Dropout作为正则化器
编译模型
以下是更深层模型的代码:
# model
model = Sequential()
model.add(Conv2D(32, kernel_size=(3,3), input_shape=input_shape, activation = 'relu'))
model.add(MaxPool2D(2,2))
model.add(Dropout(0.2))
model.add(Conv2D(64, kernel_size=(3,3), activation = 'relu'))
model.add(MaxPool2D(2,2))
model.add(Dropout(0.2))
model.add(Conv2D(128, kernel_size=(3,3), activation = 'relu'))
model.add(MaxPool2D(2,2))
model.add(Dropout(0.2))
model.add(Flatten())
model.add(Dense(128, activation = 'relu'))
model.add(Dropout(0.2))
model.add(Dense(10, activation = 'softmax'))
# compile model
model.compile(loss = 'sparse_categorical_crossentropy', optimizer= optimizer, metrics = ['accuracy'])
# print model summary
model.summary()
以下是前述代码的输出:

图 8.28:深度卷积分类器的总结
从总结中可以看到,更深层的模型仅有110,474个参数。现在,让我们看看一个具有更少参数的更深层模型是否能比我们到目前为止做得更好。
拟合模型
就像之前一样,拟合模型,但将epochs设置为40,因为更深的模型需要更长时间来学习。首先尝试训练 20 个 epoch,看看会发生什么:
# fit model
history = model.fit(X_train, y_train, epochs = 40, batch_size=batch_size, validation_data=(X_val, y_val))
以下是前述代码的输出:

以下是代码执行结束时的输出:

图 8.29:深度卷积分类器训练过程中的度量输出
评估模型
现在,使用以下代码评估模型:
# evaluate model
loss,acc = model.evaluate(X_test, y_test)
print('Test loss:', loss)
print('Accuracy:', acc)
以下是前述代码的输出:

图 8.30:深度卷积分类器评估结果输出
我们可以看到,模型在测试数据上的准确率为 99.01%,在验证数据上的准确率为 98.84%,在训练数据上的准确率为 98.38%。具有池化和 Dropout 的更深层卷积模型仅有 110,000 个参数,却提供了更好的性能。如果你也查看loss,这个模型能够达到比我们之前训练的其他模型更好的最小值:
绘制度量图表,以了解训练进度:
# plot training loss
loss_plot(history)
以下是前述代码的输出:

图 8.31:深度卷积分类器训练过程中的损失/准确率图
这是你可以获得的最佳训练图之一。我们可以看到完全没有过拟合。
卷积与池化以及 Dropout – Python 文件
该模块实现了一个深度卷积分类器的训练与评估,包含最大池化和Dropout操作:
"""This module implements a deep conv classifier with max pool and dropout."""
import numpy as np
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense, Conv2D, Flatten, MaxPool2D, Dropout
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from loss_plot import loss_plot
# Number of epochs
epochs = 20
# Batchsize
batch_size = 128
# Optimizer for the generator
from keras.optimizers import Adam
optimizer = Adam(lr=0.0001)
# Shape of the input image
input_shape = (28,28,1)
(X_train, y_train), (X_test, y_test) = mnist.load_data()
X_train, X_val, y_train, y_val = train_test_split(X_train, y_train,
stratify = y_train,
test_size = 0.08333,
random_state=42)
X_train = X_train.reshape(-1,28,28,1)
X_val = X_val.reshape(-1,28,28,1)
X_test = X_test.reshape(-1,28,28,1)
model = Sequential()
model.add(Conv2D(32, kernel_size=(3,3), input_shape=input_shape,
activation = 'relu'))
model.add(MaxPool2D(2,2))
model.add(Dropout(0.2))
model.add(Conv2D(64, kernel_size=(3,3), activation = 'relu'))
model.add(MaxPool2D(2,2))
model.add(Dropout(0.2))
model.add(Conv2D(128, kernel_size=(3,3), activation = 'relu'))
model.add(MaxPool2D(2,2))
model.add(Dropout(0.2))
model.add(Flatten())
model.add(Dense(128, activation = 'relu'))
model.add(Dropout(0.2))
model.add(Dense(10, activation = 'softmax'))
model.compile(loss = 'sparse_categorical_crossentropy', optimizer= optimizer,
metrics = ['accuracy'])
history = model.fit(X_train, y_train, epochs = epochs, batch_size=batch_size,
validation_data=(X_val, y_val))
loss,acc = model.evaluate(X_test, y_test)
print('Test loss:', loss)
print('Accuracy:', acc)
loss_plot(history)
数据增强
想象一个场景,你可能希望在一小组图像上构建一个卷积分类器。这里的问题是,分类器很容易在这小部分数据上过拟合。分类器之所以过拟合,是因为相似的图像非常少。也就是说,模型在特定类别中捕捉到的变化不多,因此很难在新的数据上表现得既强大又精准。
Keras 提供了一个名为ImageDataGenerator的预处理工具,可以通过简单的配置来增强图像数据。
它的功能包括以下内容:
-
zoom_range:随机对图像进行缩放,至给定的缩放级别 -
horizontal_flip:随机水平翻转图像 -
vertical_flip:随机垂直翻转图像 -
rescale:用提供的因子乘以数据
它还包括随机旋转、随机剪切等多种功能。
访问官方 Keras 文档(keras.io/preprocessing/image/)以了解更多关于 image_data_generator API 的附加功能。
使用 ImageDataGenerator
image_data_generator API 会在进行过程中按批次转换和增强数据,而且使用起来非常简单。
首先,导入 ImageDataGenerator:
from keras.preprocessing.image import ImageDataGenerator
实现一个随机水平翻转增强器:
train_datagen = ImageDataGenerator(horizontal_flip=True)
在训练数据上拟合增强器:
# fit the augmenter
train_datagen.fit(X_train)
拟合后,我们通常使用 transform 命令。在这里,我们使用 flow 命令代替 transform。它接受图像及其对应标签,然后生成指定批次大小的转换数据。
让我们转换一批图像并查看结果:
# transform the data
for img, label in train_datagen.flow(X_train, y_train, batch_size=6):
for i in range(0, 6):
plt.subplot(2,3,i+1)
plt.title('Label {}'.format(label[i]))
plt.imshow(img[i].reshape(28, 28), cmap='gray')
break
plt.tight_layout()
plt.show()
以下是前述代码的输出:

图 8.32:水平翻转增强后的数字
类似地,我们可以实现一个随机缩放增强器,如下所示:
train_datagen = ImageDataGenerator(zoom_range=0.3)
#fit
train_datagen.fit(X_train)
#transform
for img, label in train_datagen.flow(X_train, y_train, batch_size=6):
for i in range(0, 6):
plt.subplot(2,3,i+1)
plt.title('Label {}'.format(label[i]))
plt.imshow(img[i].reshape(28, 28), cmap='gray')
break
plt.tight_layout()
plt.show()
以下是前述代码的输出:

图 8.33:缩放增强后的数字
拟合 ImageDataGenerator
现在,我们用与深度卷积模型相同的架构(带池化和 Dropout)构建一个分类器,但使用增强数据。
首先,定义 ImageDataGenerator 的特性,如下所示:
train_datagen = ImageDataGenerator(
rescale = 1./255,
zoom_range = 0.2,
horizontal_flip = True)
我们已经定义了 ImageDataGenerator 可以执行以下操作
-
重标定
-
随机缩放
-
随机水平翻转
重标定操作将像素值缩放到 0 到 1 的范围内。
下一步是将此生成器拟合到训练数据上:
train_datagen.fit(X_train)
编译模型
我们需要这样定义并编译深度卷积模型:
# define model
model = Sequential()
model.add(Conv2D(32, kernel_size=(3,3), input_shape=input_shape, activation = 'relu'))
model.add(MaxPool2D(2,2))
model.add(Dropout(0.2))
model.add(Conv2D(64, kernel_size=(3,3), activation = 'relu'))
model.add(MaxPool2D(2,2))
model.add(Dropout(0.2))
model.add(Conv2D(128, kernel_size=(3,3), activation = 'relu'))
model.add(MaxPool2D(2,2))
model.add(Dropout(0.2))
model.add(Flatten())
model.add(Dense(128, activation = 'relu'))
model.add(Dropout(0.2))
model.add(Dense(10, activation = 'softmax'))
# compile model
model.compile(loss = 'sparse_categorical_crossentropy', optimizer= optimizer, metrics = ['accuracy'])
拟合模型
最后,我们需要拟合模型:
# fit the model on batches with real-time data augmentation
history = model.fit_generator(train_datagen.flow(X_train, y_train, batch_size=128), steps_per_epoch=len(X_train) / 128, epochs=10, validation_data=(train_datagen.flow(X_val, y_val)))
以下是前述代码的输出:

代码执行结束时的输出如下:

图 8.34:深度卷积分类器在增强数据上训练过程中的打印指标
评估模型
现在,我们需要评估模型:
# transform/augment test data
for test_img, test_lab in train_datagen.flow(X_test, y_test, batch_size = X_test.shape[0]):
break
# evaluate model on test data
loss,acc = model.evaluate(test_img, test_lab)
print('Test loss:', loss)
print('Accuracy:', acc)
以下是前述代码的输出:

图 8.35:深度卷积分类器在增强数据上的评估打印输出
然后,我们需要绘制深度卷积分类器:
# plot the learning
loss_plot(history)
以下是前述代码的输出:

图 8.36:深度卷积分类器在增强数据上的训练损失/准确率图
增强 – Python 文件
本模块实现了深度卷积分类器在增强数据上的训练和评估:
"""This module implements a deep conv classifier on augmented data."""
import numpy as np
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense, Conv2D, Flatten, MaxPool2D, Dropout
import matplotlib.pyplot as plt
from keras.preprocessing.image import ImageDataGenerator
from sklearn.model_selection import train_test_split
from loss_plot import loss_plot
# Number of epochs
epochs = 10
# Batchsize
batch_size = 128
# Optimizer for the generator
from keras.optimizers import Adam
optimizer = Adam(lr=0.001)
# Shape of the input image
input_shape = (28,28,1)
(X_train, y_train), (X_test, y_test) = mnist.load_data()
X_train, X_val, y_train, y_val = train_test_split(X_train, y_train,
stratify = y_train,
test_size = 0.08333,
random_state=42)
X_train = X_train.reshape(-1,28,28,1)
X_val = X_val.reshape(-1,28,28,1)
X_test = X_test.reshape(-1,28,28,1)
train_datagen = ImageDataGenerator(
rescale=1./255,
zoom_range=0.2,
horizontal_flip=True)
train_datagen.fit(X_train)
model = Sequential()
model.add(Conv2D(32, kernel_size=(3,3), input_shape=input_shape,
activation = 'relu'))
model.add(MaxPool2D(2,2))
model.add(Dropout(0.2))
model.add(Conv2D(64, kernel_size=(3,3), activation = 'relu'))
model.add(MaxPool2D(2,2))
model.add(Dropout(0.2))
model.add(Conv2D(128, kernel_size=(3,3), activation = 'relu'))
model.add(MaxPool2D(2,2))
model.add(Dropout(0.2))
model.add(Flatten())
model.add(Dense(128, activation = 'relu'))
model.add(Dropout(0.2))
model.add(Dense(10, activation = 'softmax'))
model.compile(loss = 'sparse_categorical_crossentropy', optimizer= optimizer,
metrics = ['accuracy'])
# fits the model on batches with real-time data augmentation:
history = model.fit_generator(train_datagen.flow(X_train, y_train,
batch_size=128),
steps_per_epoch=len(X_train) / 128, epochs=epochs,
validation_data=(train_datagen.flow(X_val,
y_val)))
for test_img, test_lab in train_datagen.flow(X_test, y_test,
batch_size = X_test.shape[0]):
break
loss,acc = model.evaluate(test_img, test_lab)
print('Test loss:', loss)
print('Accuracy:', acc)
loss_plot(history)
附加主题 – 卷积自编码器
自编码器是由两部分组成的:编码器和解码器。简单自编码器的编码器和解码器通常由全连接层构成,而卷积自编码器则由卷积层构成:

图 8.37:自编码器的结构(图像来源:维基百科)
自编码器的编码器部分接受图像并通过池化操作压缩为更小的尺寸。在我们的例子中,这是最大池化。解码器接受编码器的输入,并通过使用卷积和上采样来学习扩展图像到我们想要的尺寸。
想象一下,你想从模糊的图像中构建高分辨率图像的情况:

图 8.38:左侧是低分辨率数字,右侧是高分辨率数字。
卷积自编码器能够非常好地完成这项任务。你看到的前述高分辨率数字实际上是使用卷积自编码器生成的。
到本节结束时,你将构建一个卷积自编码器,它接受低分辨率的 14141 MNIST 数字并生成高分辨率的 28281 数字。
导入依赖项
在开始本节之前,请考虑重新启动会话:
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
from keras.datasets import mnist
(X_train, y_train), (X_test, y_test) = mnist.load_data()
from keras.layers import Conv2D, MaxPooling2D, UpSampling2D
from keras.models import Model, Sequential
from keras.optimizers import Adam
from keras import backend as k
# for resizing images
from scipy.misc import imresize
生成低分辨率图像
为了生成低分辨率图像,定义一个名为reshape()的函数,该函数将输入的图像/数字调整为14*14的大小。定义完成后,我们将使用reshape()函数生成低分辨率的训练和测试图像:
def reshape(x):
"""Reshape images to 14*14"""
img = imresize(x.reshape(28,28), (14, 14))
return img
# create 14*14 low resolution train and test images
XX_train = np.array([*map(reshape, X_train.astype(float))])
XX_test = np.array([*map(reshape, X_test.astype(float))])
XX_train和XX_test将是我们输入到编码器中的图像,X_train和X_test将是目标。
缩放
将训练输入、测试输入和目标图像缩放到 0 到 1 之间,这样学习过程会更快:
# scale images to range between 0 and 1
# 14*14 train images
XX_train = XX_train/255
# 28*28 train label images
X_train = X_train/255
# 14*14 test images
XX_test = XX_test/255
# 28*28 test label images
X_test = X_test/255
定义自编码器
我们将要构建的卷积自编码器将接受 14141 的图像作为输入,28281 的图像作为目标,并将具有以下特点:
在编码器中:
-
第一层是一个卷积二维层,包含 64 个大小为 33 的滤波器,接着是批量归一化,
activation为relu,然后是使用大小为 22 的MaxPooling2D进行下采样。 -
第二层,即编码器部分的最后一层,再次是一个卷积二维层,包含 128 个大小为 3*3 的滤波器,进行批量归一化,
activation为relu。
在解码器中:
-
第一层是一个卷积二维层,包含 128 个大小为 3*3 的滤波器,
activation为relu,接着是通过UpSampling2D进行上采样。 -
第二层是一个卷积二维层,包含 64 个大小为 3*3 的滤波器,
activation为relu,接着是使用UpSampling2D进行上采样。 -
第三层,或解码器部分的最后一层,是一个卷积二维层,包含 1 个大小为 3*3 的滤波器,
activation为sigmoid。
以下是我们的自编码器代码:
batch_size = 128
epochs = 40
input_shape = (14,14,1)
# define autoencoder
def make_autoencoder(input_shape):
generator = Sequential()
generator.add(Conv2D(64, (3, 3), activation='relu', padding='same', input_shape=input_shape))
generator.add(MaxPooling2D(pool_size=(2, 2)))
generator.add(Conv2D(128, (3, 3), activation='relu', padding='same'))
generator.add(Conv2D(128, (3, 3), activation='relu', padding='same'))
generator.add(UpSampling2D((2, 2)))
generator.add(Conv2D(64, (3, 3), activation='relu', padding='same'))
generator.add(UpSampling2D((2, 2)))
generator.add(Conv2D(1, (3, 3), activation='sigmoid', padding='same'))
return generator
autoencoder = make_autoencoder(input_shape)
# compile auto encoder
autoencoder.compile(loss='mean_squared_error', optimizer = Adam(lr=0.0002, beta_1=0.5))
# auto encoder summary
autoencoder.summary()
以下是前述代码的输出:

图 8.39:自编码器摘要
我们使用mean_squared_error作为loss,因为我们希望模型预测像素值。
如果你查看摘要,输入的图像大小为 14141,在宽度和高度维度上被压缩到 77 的大小,但在通道维度上从 1 扩展到 128。这些小/压缩的特征图被输入到解码器中,学习生成高分辨率图像所需的映射,这些图像的定义尺寸在此为 2828*1。
如果你对 Keras API 的使用有任何疑问,请访问 Keras 官方文档:keras.io/。
拟合自编码器
像任何常规模型拟合一样,拟合自编码器:
# fit autoencoder
autoencoder_train = autoencoder.fit(XX_train.reshape(-1,14,14,1), X_train.reshape(-1,28,28,1), batch_size=batch_size,
epochs=epochs, verbose=1,
validation_split = 0.2)
以下是前述代码的输出:

以下是代码执行结束时的输出:

图 8.40:自编码器训练过程中的打印输出
你会注意到在拟合过程中,我们指定了一个叫做validation_split的参数,并将其设置为0.2。这会将训练数据分割为训练数据和验证数据,其中验证数据占原始训练数据的 20%。
损失图和测试结果
现在,让我们绘制训练过程中训练和验证损失的变化曲线。同时,我们还将通过将测试图像输入模型,绘制模型生成的高分辨率图像结果:
loss = autoencoder_train.history['loss']
val_loss = autoencoder_train.history['val_loss']
epochs_ = [x for x in range(epochs)]
plt.figure()
plt.plot(epochs_, loss, label='Training loss')
plt.plot(epochs_, val_loss, label='Validation loss')
plt.title('Training and validation loss')
plt.legend()
plt.show()
print('Input')
plt.figure(figsize=(5,5))
for i in range(9):
plt.subplot(331 + i)
plt.imshow(np.squeeze(XX_test.reshape(-1,14,14)[i]), cmap='gray')
plt.show()
# Test set results
print('GENERATED')
plt.figure(figsize=(5,5))
for i in range(9):
pred = autoencoder.predict(XX_test.reshape(-1,14,14,1)[i:i+1], verbose=0)
plt.subplot(331 + i)
plt.imshow(pred[0].reshape(28,28), cmap='gray')
plt.show()
以下是前述代码的输出:

图 8.41:训练/验证损失图
以下是从低分辨率图像生成的高分辨率图像的输出:

图 8.42:从低分辨率测试(1414)图像生成的高分辨率测试(2828)图像
自编码器 – Python 文件
本模块实现了在 MNIST 数据上训练自编码器:
"""This module implements a convolution autoencoder on MNIST data."""
import numpy as np
import matplotlib.pyplot as plt
from keras.datasets import mnist
(X_train, y_train), (X_test, y_test) = mnist.load_data()
from keras.layers import Conv2D, MaxPooling2D, UpSampling2D
from keras.models import Model, Sequential
from keras.optimizers import Adam
from keras import backend as k
# for resizing images
from scipy.misc import imresize
def reshape(x):
"""Reshape images to 14*14"""
img = imresize(x.reshape(28,28), (14, 14))
return img
# create 14*14 low resolution train and test images
XX_train = np.array([*map(reshape, X_train.astype(float))])
XX_test = np.array([*map(reshape, X_test.astype(float))])
# scale images to range between 0 and 1
#14*14 train images
XX_train = XX_train/255
#28*28 train label images
X_train = X_train/255
#14*14 test images
XX_test = XX_test/255
#28*28 test label images
X_test = X_test/255
batch_size = 128
epochs = 40
input_shape = (14,14,1)
def make_autoencoder(input_shape):
generator = Sequential()
generator.add(Conv2D(64, (3, 3), activation='relu', padding='same',
input_shape=input_shape))
generator.add(MaxPooling2D(pool_size=(2, 2)))
generator.add(Conv2D(128, (3, 3), activation='relu', padding='same'))
generator.add(Conv2D(128, (3, 3), activation='relu', padding='same'))
generator.add(UpSampling2D((2, 2)))
generator.add(Conv2D(64, (3, 3), activation='relu', padding='same'))
generator.add(UpSampling2D((2, 2)))
generator.add(Conv2D(1, (3, 3), activation='sigmoid', padding='same'))
return generator
autoencoder = make_autoencoder(input_shape)
autoencoder.compile(loss='mean_squared_error', optimizer = Adam(lr=0.0002,
beta_1=0.5))
autoencoder_train = autoencoder.fit(XX_train.reshape(-1,14,14,1),
X_train.reshape(-1,28,28,1),
batch_size=batch_size,
epochs=epochs, verbose=1,
validation_split = 0.2)
loss = autoencoder_train.history['loss']
val_loss = autoencoder_train.history['val_loss']
epochs_ = [x for x in range(epochs)]
plt.figure()
plt.plot(epochs_, loss, label='Training loss', marker = 'D')
plt.plot(epochs_, val_loss, label='Validation loss', marker = 'o')
plt.title('Training and validation loss')
plt.legend()
plt.show()
print('Input')
plt.figure(figsize=(5,5))
for i in range(9):
plt.subplot(331 + i)
plt.imshow(np.squeeze(XX_test.reshape(-1,14,14)[i]), cmap='gray')
plt.show()
# Test set results
print('GENERATED')
plt.figure(figsize=(5,5))
for i in range(9):
pred = autoencoder.predict(XX_test.reshape(-1,14,14,1)[i:i+1], verbose=0)
plt.subplot(331 + i)
plt.imshow(pred[0].reshape(28,28), cmap='gray')
plt.show()
结论
本项目的目标是构建一个 CNN 分类器,用于比我们在第二章中的使用回归训练神经网络进行预测章节中通过多层感知机分类手写数字更好的模型。
我们的深度卷积神经网络分类器,使用最大池化和丢弃法,在 10000 张图像/数字的测试集上达到了 99.01%的准确率。这是一个很好的成绩,比我们的多层感知机模型高出几乎 12%。
然而,这里有一些影响。这个准确率的含义是什么?理解这一点很重要。就像我们在第二章中的使用回归训练神经网络进行预测一章一样,让我们计算一下导致客户服务问题的错误发生率。
为了帮助我们回忆,在这个假设的用例中,我们假设每个餐厅地点平均有 30 张桌子,并且这些桌子在高峰时段(系统可能会被使用时)每晚翻台两次,最后,假设该餐厅连锁有 35 个地点。这意味着每天运营时,大约会捕捉到 21,000 个手写数字(30 张桌子 x 2 次翻台/天 x 35 个地点 x 10 位电话号码)。
最终目标是正确分类所有数字,因为即使是一个数字的误分类也会导致失败。使用我们构建的分类器,每天会错误分类 208 个数字。如果我们考虑最坏的情况,在 2,100 个顾客中,将会有 208 个电话号码被误分类。也就是说,即使在最坏的情况下,我们仍然有 90.09%的时间((2,100-208)/2,100)会将短信发送给正确的顾客。
最好的情况是,如果每个电话号码中的所有十个数字都被误分类,那么我们只会错误分类 21 个电话号码。这意味着我们的失败率为((2,100-21)/2,100) 1%。这已经是最理想的情况了。
除非你想减少那个 1%的错误率……
总结
在这一章中,我们了解了如何在 Keras 中实现卷积神经网络分类器。现在你对卷积、平均池化、最大池化和丢弃法有了简要的理解,并且你还构建了一个深度模型。你了解了如何减少过拟合,以及在数据量不足时,如何生成更多的训练/验证数据来构建一个可泛化的模型。最后,我们评估了模型在测试数据上的表现,并确认我们成功达到了目标。本章的结尾,我们介绍了自编码器。
第九章:使用 OpenCV 和 TensorFlow 进行物体检测
欢迎来到第二章,专注于计算机视觉的内容,出自 Python 深度学习项目(让我们用一个数据科学的双关语开始吧!)。让我们回顾一下在第八章中我们所取得的成就,使用卷积神经网络(ConvNets)进行手写数字分类,在这一章中,我们能够使用卷积神经网络(CNN)训练一个图像分类器,准确地对图像中的手写数字进行分类。原始数据的一个关键特征是什么?我们的业务目标又是什么?数据比可能的情况要简单一些,因为每张图片中只有一个手写数字,我们的目标是准确地为图像分配数字标签。
如果每张图片中包含多个手写数字会发生什么?如果我们有一段包含数字的视频呢?如果我们想要识别图片中数字的位置呢?这些问题代表了现实世界数据所体现的挑战,并推动我们的数据科学创新,朝着新的模型和能力发展。
让我们将问题和想象力扩展到下一个(假设的)业务用例,针对我们的 Python 深度学习项目,我们将构建、训练和测试一个物体检测和分类模型,供一家汽车制造商用于其新一代自动驾驶汽车。自动驾驶汽车需要具备基本的计算机视觉能力,而这种能力正是我们通过生理和经验性学习所自然具备的。我们人类可以检查我们的视野并报告是否存在特定物体,以及该物体与其他物体的位置关系(如果存在的话)。所以,如果我问你是否看到了一个鸡,你可能会说没有,除非你住在农场并正在望着窗外。但如果我问你是否看到了一个键盘,你可能会说是的,并且甚至能够说出键盘与其他物体的不同,且位于你面前的墙之前。
对于计算机来说,这不是一项简单的任务。作为深度学习工程师,你将学习到直觉和模型架构,这将使你能够构建一个强大的物体检测与分类引擎,我们可以设想它将被用于自动驾驶汽车的测试。在本章中,我们将处理的数据输入比以往的项目更为复杂,且当我们正确处理这些数据时,结果将更加令人印象深刻。
那么,让我们开始吧!
物体检测直觉
当你需要让应用程序在图像中找到并命名物体时,你需要构建一个用于目标检测的深度神经网络。视觉领域非常复杂,静态图像和视频的相机捕捉的帧中包含了许多物体。目标检测被用于制造业的生产线过程自动化;自动驾驶车辆感知行人、其他车辆、道路和标志等;当然,还有面部识别。基于机器学习和深度学习的计算机视觉解决方案需要你——数据科学家——构建、训练和评估能够区分不同物体并准确分类检测到的物体的模型。
正如你在我们处理的其他项目中看到的,CNN 是处理图像数据的非常强大的模型。我们需要查看在单张(静态)图像上表现非常好的基础架构的扩展,看看哪些方法在复杂图像和视频中最有效。
最近,以下网络取得了进展:Faster R-CNN、基于区域的全卷积网络 (R-FCN)、MultiBox、固态硬盘 (SSD) 和 你只看一次 (YOLO)。我们已经看到了这些模型在常见消费者应用中的价值,例如 Google Photos 和 Pinterest 视觉搜索。我们甚至看到其中一些模型足够轻量且快速,能够在移动设备上表现良好。
可以通过以下参考文献列表进行近期该领域的研究:
-
PVANET: 用于实时目标检测的深度轻量级神经网络, arXiv:1608.08021
-
R-CNN: 用于准确目标检测和语义分割的丰富特征层次结构, CVPR, 2014.
-
SPP: 用于视觉识别的深度卷积网络中的空间金字塔池化, ECCV, 2014.
-
Fast R-CNN, arXiv:1504.08083.
-
Faster R-CNN: 使用区域提议网络实现实时目标检测, arXiv:1506.01497.
-
R-CNN 减去 R, arXiv:1506.06981.
-
拥挤场景中的端到端人物检测, arXiv:1506.04878.
-
YOLO – 你只看一次:统一的实时目标检测, arXiv:1506.02640
-
Inside-Outside Net: 使用跳跃池化和递归神经网络在上下文中检测物体
-
深度残差网络:用于图像识别的深度残差学习
-
R-FCN: 基于区域的全卷积网络进行目标检测
-
SSD: 单次多框检测器, arXiv:1512.02325
另外,以下是从 1999 年到 2017 年目标检测发展的时间线:

图 9.1:1999 到 2017 年目标检测发展时间线
本章的文件可以在github.com/PacktPublishing/Python-Deep-Learning-Projects/tree/master/Chapter09找到。
目标检测模型的改进
物体检测和分类一直是研究的主题。使用的模型建立在前人研究的巨大成功基础上。简要回顾进展历史,从 2005 年 Navneet Dalal 和 Bill Triggs 开发的计算机视觉模型方向梯度直方图(HOG)特征开始。
HOG 特征速度快,表现良好。深度学习和 CNN 的巨大成功使其成为更精确的分类器,因为其深层网络。然而,当时 CNN 的速度相较之下过于缓慢。
解决方案是利用 CNN 的改进分类能力,并通过一种技术提高其速度,采用选择性搜索范式,形成了 R-CNN。减少边界框的数量确实在速度上有所提升,但不足以满足预期。
SPP-net 是一种提出的解决方案,其中计算整个图像的 CNN 表示,并驱动通过选择性搜索生成的每个子部分的 CNN 计算表示。选择性搜索通过观察像素强度、颜色、图像纹理和内部度量来生成所有可能的物体位置。然后,这些识别出的物体会被输入到 CNN 模型中进行分类。
这一改进催生了名为 Fast R-CNN 的模型,采用端到端训练,从而解决了 SPP-net 和 R-CNN 的主要问题。通过名为 Faster R-CNN 的模型进一步推进了这项技术,使用小型区域提议 CNN 代替选择性搜索表现得非常好。
这是 Faster R-CNN 物体检测管道的快速概述:

对之前讨论的 R-CNN 版本进行的快速基准对比显示如下:
| R-CNN | Fast R-CNN | Faster R-CNN | |
|---|---|---|---|
| 平均响应时间 | ~50 秒 | ~2 秒 | ~0.2 秒 |
| 速度提升 | 1 倍 | 25 倍 | 250 倍 |
性能提升令人印象深刻,Faster R-CNN 是目前在实时应用中最准确、最快的物体检测算法之一。其他近期强大的替代方法包括 YOLO 模型,我们将在本章后面详细探讨。
使用 OpenCV 进行物体检测
让我们从开源计算机视觉(OpenCV)的基本或传统实现开始我们的项目。该库主要面向需要计算机视觉能力的实时应用。
OpenCV 在 C、C++、Python 等多种语言中都有 API 封装,最佳的前进方式是使用 Python 封装器或任何你熟悉的语言来快速构建原型,一旦代码完成,可以在 C/C++中重写以用于生产。
在本章中,我们将使用 Python 封装器创建我们的初始物体检测模块。
所以,让我们开始吧。
一种手工制作的红色物体检测器
在本节中,我们将学习如何创建一个特征提取器,能够使用各种图像处理技术(如腐蚀、膨胀、模糊等)从提供的图像中检测任何红色物体。
安装依赖
首先,我们需要安装 OpenCV,我们通过这个简单的 pip 命令来完成:
pip install opencv-python
接着我们将导入它以及其他用于可视化和矩阵运算的模块:
import cv2
import matplotlib
from matplotlib import colors
from matplotlib import pyplot as plt
import numpy as np
from __future__ import division
此外,让我们定义一些帮助函数,帮助我们绘制图像和轮廓:
# Defining some helper function
def show(image):
# Figure size in inches
plt.figure(figsize=(15, 15))
# Show image, with nearest neighbour interpolation
plt.imshow(image, interpolation='nearest')
def show_hsv(hsv):
rgb = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)
show(rgb)
def show_mask(mask):
plt.figure(figsize=(10, 10))
plt.imshow(mask, cmap='gray')
def overlay_mask(mask, image):
rgb_mask = cv2.cvtColor(mask, cv2.COLOR_GRAY2RGB)
img = cv2.addWeighted(rgb_mask, 0.5, image, 0.5, 0)
show(img)
def find_biggest_contour(image):
image = image.copy()
im2,contours, hierarchy = cv2.findContours(image, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
contour_sizes = [(cv2.contourArea(contour), contour) for contour in contours]
biggest_contour = max(contour_sizes, key=lambda x: x[0])[1]
mask = np.zeros(image.shape, np.uint8)
cv2.drawContours(mask, [biggest_contour], -1, 255, -1)
return biggest_contour, mask
def circle_countour(image, countour):
image_with_ellipse = image.copy()
ellipse = cv2.fitEllipse(countour)
cv2.ellipse(image_with_ellipse, ellipse, (0,255,0), 2)
return image_with_ellipse
探索图像数据
在任何数据科学问题中,首先要做的就是探索和理解数据。这有助于我们明确目标。所以,让我们首先加载图像并检查图像的属性,比如色谱和尺寸:
# Loading image and display
image = cv2.imread('./ferrari.png')
show(image)
以下是输出结果:

由于图像在内存中存储的顺序是蓝绿红(BGR),我们需要将其转换为红绿蓝(RGB):
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
show(image)
以下是输出结果:

图 9.2:RGB 色彩格式中的原始输入图像。
对图像进行归一化处理
我们将缩小图像尺寸,为此我们将使用 cv2.resize() 函数:
max_dimension = max(image.shape)
scale = 700/max_dimension
image = cv2.resize(image, None, fx=scale,fy=scale)
现在我们将执行模糊操作,使像素更加规范化,为此我们将使用高斯核。高斯滤波器在研究领域非常流行,常用于各种操作,其中之一是模糊效果,能够减少噪声并平衡图像。以下代码执行了模糊操作:
image_blur = cv2.GaussianBlur(image, (7, 7), 0)
然后我们将把基于 RGB 的图像转换为 HSV 色谱,这有助于我们使用颜色强度、亮度和阴影来提取图像的其他特征:
image_blur_hsv = cv2.cvtColor(image_blur, cv2.COLOR_RGB2HSV)
以下是输出结果:

图:9.3:HSV 色彩格式中的原始输入图像。
准备掩膜
我们需要创建一个掩膜,可以检测特定的颜色谱;假设我们要检测红色。现在我们将创建两个掩膜,它们将使用颜色值和亮度因子进行特征提取:
# filter by color
min_red = np.array([0, 100, 80])
max_red = np.array([10, 256, 256])
mask1 = cv2.inRange(image_blur_hsv, min_red, max_red)
# filter by brightness
min_red = np.array([170, 100, 80])
max_red = np.array([180, 256, 256])
mask2 = cv2.inRange(image_blur_hsv, min_red, max_red)
# Concatenate both the mask for better feature extraction
mask = mask1 + mask2
以下是我们的掩膜效果:

掩膜的后处理
一旦我们成功创建了掩膜,我们需要执行一些形态学操作,这是用于几何结构分析和处理的基本图像处理操作。
首先,我们将创建一个内核,执行各种形态学操作,对输入图像进行处理:
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15, 15))
闭操作:膨胀后腐蚀 对于关闭前景物体内部的小碎片或物体上的小黑点非常有帮助。
现在让我们对掩膜执行闭操作:
mask_closed = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
开操作 腐蚀后膨胀 用于去除噪声。
然后我们执行开操作:
mask_clean = cv2.morphologyEx(mask_closed, cv2.MORPH_OPEN, kernel)
以下是输出结果:

图 9.4:此图展示了形态学闭运算和开运算的输出(左侧),我们将二者结合起来得到最终处理后的掩膜(右侧)。
在前面的截图中,你可以看到(截图的左侧)形态学操作如何改变掩膜的结构,并且当将两种操作结合时(截图的右侧),你会得到去噪后的更干净的结构。
应用掩膜
现在是时候使用我们创建的掩膜从图像中提取物体了。首先,我们将使用辅助函数找到最大的轮廓,这是我们需要提取的物体的最大区域。然后将掩膜应用于图像,并在提取的物体上绘制一个圆形边界框:
# Extract biggest bounding box
big_contour, red_mask = find_biggest_contour(mask_clean)
# Apply mask
overlay = overlay_mask(red_mask, image)
# Draw bounding box
circled = circle_countour(overlay, big_contour)
show(circled)
以下是输出:

图 9.5:此图展示了我们从图像中检测到红色区域(汽车车身),并在其周围绘制了一个椭圆。
啪!我们成功提取了图像,并使用简单的图像处理技术在物体周围绘制了边界框。
使用深度学习进行物体检测
在本节中,我们将学习如何构建一个世界级的物体检测模块,而不需要太多使用传统的手工技术。我们将使用深度学习方法,这种方法足够强大,可以自动从原始图像中提取特征,然后利用这些特征进行分类和检测。
首先,我们将使用一个预制的 Python 库构建一个物体检测器,该库可以使用大多数最先进的预训练模型,之后我们将学习如何使用 YOLO 架构实现一个既快速又准确的物体检测器。
快速实现物体检测
物体检测在 2012 年后由于行业趋势向深度学习的转变而得到了广泛应用。准确且越来越快的模型,如 R-CNN、Fast-RCNN、Faster-RCNN 和 RetinaNet,以及快速且高度准确的模型如 SSD 和 YOLO,现在都在生产中使用。在本节中,我们将使用 Python 库中功能齐全的预制特征提取器,只需几行代码即可使用。此外,我们还将讨论生产级设置。
那么,开始吧。
安装所有依赖项
这与我们在前几章中执行的步骤相同。首先,让我们安装所有依赖项。在这里,我们使用一个名为 ImageAI 的 Python 模块(github.com/OlafenwaMoses/ImageAI),它是一个有效的方法,可以帮助你快速构建自己的物体检测应用程序:
pip install tensorflow
pip install keras
pip install numpy
pip install scipy
pip install opencv-python
pip install pillow
pip install matplotlib
pip install h5py
# Here we are installing ImageAI
pip3 install https://github.com/OlafenwaMoses/ImageAI/releases/download/2.0.2/imageai-2.0.2-py3-none-any.whl
我们将使用 Python 3.x 环境来运行这个模块。
对于此实现,我们将使用一个在 COCO 数据集上训练的预训练 ResNet 模型(cocodataset.org/#home)(一个大规模的目标检测、分割和描述数据集)。你也可以使用其他预训练模型,如下所示:
-
DenseNet-BC-121-32.h5(github.com/OlafenwaMoses/ImageAI/releases/download/1.0/DenseNet-BC-121-32.h5) (31.7 MB) -
inception_v3_weights_tf_dim_ordering_tf_kernels.h5(github.com/OlafenwaMoses/ImageAI/releases/download/1.0/inception_v3_weights_tf_dim_ordering_tf_kernels.h5) (91.7 MB) -
resnet50_coco_best_v2.0.1.h5(github.com/OlafenwaMoses/ImageAI/releases/download/1.0/resnet50_coco_best_v2.0.1.h5) (146 MB) -
resnet50_weights_tf_dim_ordering_tf_kernels.h5(github.com/OlafenwaMoses/ImageAI/releases/download/1.0/resnet50_weights_tf_dim_ordering_tf_kernels.h5) (98.1 MB) -
squeezenet_weights_tf_dim_ordering_tf_kernels.h5(github.com/OlafenwaMoses/ImageAI/releases/download/1.0/squeezenet_weights_tf_dim_ordering_tf_kernels.h5) (4.83 MB) -
yolo-tiny.h5(github.com/OlafenwaMoses/ImageAI/releases/download/1.0/yolo-tiny.h5) (33.9 MB) -
yolo.h5(github.com/OlafenwaMoses/ImageAI/releases/download/1.0/yolo.h5): 237 MB
要获取数据集,请使用以下命令:
wget https://github.com/OlafenwaMoses/ImageAI/releases/download/1.0/resnet50_coco_best_v2.0.1.h5
实现
现在我们已经准备好所有的依赖项和预训练模型,我们将实现一个最先进的目标检测模型。我们将使用以下代码导入 ImageAI 的ObjectDetection类:
from imageai.Detection import ObjectDetection
import os
model_path = os.getcwd()
然后我们创建ObjectDetection对象的实例,并将模型类型设置为RetinaNet()。接下来,我们设置下载的 ResNet 模型部分,并调用loadModel()函数:
object_detector = ObjectDetection()
object_detector.setModelTypeAsRetinaNet()
object_detector.setModelPath( os.path.join(model_path , "resnet50_coco_best_v2.0.1.h5"))
object_detector.loadModel()
一旦模型被加载到内存中,我们就可以将新图像输入模型,图像可以是任何常见的图像格式,如 JPEG、PNG 等。此外,函数对图像的大小没有限制,因此你可以使用任何维度的数据,模型会在内部处理它。我们使用detectObjectsFromImage()来输入图像。此方法返回带有更多信息的图像,例如检测到的对象的边界框坐标、检测到的对象的标签以及置信度分数。
以下是一些用作输入模型并执行目标检测的图像:

图 9.6:由于在写这章时我正在去亚洲(马来西亚/兰卡威)旅行,我决定尝试使用一些我在旅行中拍摄的真实图像。
以下代码用于将图像输入到模型中:
object_detections = object_detector.detectObjectsFromImage(input_image=os.path.join(model_path , "image.jpg"), output_image_path=os.path.join(model_path , "imagenew.jpg"))
此外,我们迭代object_detection对象,以读取模型预测的所有物体及其相应的置信度分数:
for eachObject in object_detections:
print(eachObject["name"] , " : " , eachObject["percentage_probability"])
以下是结果的展示方式:



图 9.7:从目标检测模型中提取的结果,图中包含了检测到的物体周围的边界框。结果包含物体的名称和置信度分数。
所以,我们可以看到,预训练模型表现得非常好,只用了很少的代码行。
部署
现在我们已经准备好所有基本代码,让我们将ObjectDetection模块部署到生产环境中。在本节中,我们将编写一个 RESTful 服务,它将接受图像作为输入,并返回检测到的物体作为响应。
我们将定义一个POST函数,它接受带有 PNG、JPG、JPEG 和 GIF 扩展名的图像文件。上传的图像路径将传递给ObjectDetection模块,后者执行检测并返回以下 JSON 结果:
from flask import Flask, request, jsonify, redirect
import os , json
from imageai.Detection import ObjectDetection
model_path = os.getcwd()
PRE_TRAINED_MODELS = ["resnet50_coco_best_v2.0.1.h5"]
# Creating ImageAI objects and loading models
object_detector = ObjectDetection()
object_detector.setModelTypeAsRetinaNet()
object_detector.setModelPath( os.path.join(model_path , PRE_TRAINED_MODELS[0]))
object_detector.loadModel()
object_detections = object_detector.detectObjectsFromImage(input_image='sample.jpg')
# Define model paths and the allowed file extentions
UPLOAD_FOLDER = model_path
ALLOWED_EXTENSIONS = set(['png', 'jpg', 'jpeg', 'gif'])
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route('/predict', methods=['POST'])
def upload_file():
if request.method == 'POST':
# check if the post request has the file part
if 'file' not in request.files:
print('No file part')
return redirect(request.url)
file = request.files['file']
# if user does not select file, browser also
# submit a empty part without filename
if file.filename == '':
print('No selected file')
return redirect(request.url)
if file and allowed_file(file.filename):
filename = file.filename
file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(file_path)
try:
object_detections = object_detector.detectObjectsFromImage(input_image=file_path)
except Exception as ex:
return jsonify(str(ex))
resp = []
for eachObject in object_detections :
resp.append([eachObject["name"],
round(eachObject["percentage_probability"],3)
]
)
return json.dumps(dict(enumerate(resp)))
if __name__ == "__main__":
app.run(host='0.0.0.0', port=4445)
将文件保存为object_detection_ImageAI.py并执行以下命令来运行 Web 服务:
python object_detection_ImageAI.py
以下是输出结果:

图 9.8:成功执行 Web 服务后的终端屏幕输出。
在另一个终端中,你现在可以尝试调用 API,如下所示的命令:
curl -X POST \
http://0.0.0.0:4445/predict \
-H 'content-type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW' \
-F file=@/Users/rahulkumar/Downloads/IMG_1651.JPG
以下是响应输出:
{
"0": ["person",54.687],
"1": ["person",56.77],
"2": ["person",55.837],
"3": ["person",75.93],
"4": ["person",72.956],
"5": ["bird",81.139]
}
所以,这真是太棒了;仅用了几个小时的工作,你就准备好了一个接近最先进技术的生产级目标检测模块。
使用 YOLOv2 进行实时目标检测
目标检测和分类的重大进展得益于一个过程,即你只需对输入图像进行一次查看(You Only Look Once,简称 YOLO)。在这一单次处理过程中,目标是设置边界框的角坐标,以便绘制在检测到的物体周围,并使用回归模型对物体进行分类。这个过程能够避免误报,因为它考虑了整个图像的上下文信息,而不仅仅是像早期描述的区域提议方法那样的较小区域。如下所示的卷积神经网络(CNN)可以一次性扫描图像,因此足够快,能够在实时处理要求的应用中运行。
YOLOv2 在每个单独的网格中预测 N 个边界框,并为每个网格中的对象分类关联一个置信度级别,该网格是在前一步骤中建立的 S×S 网格。

图 9.9:YOLO 工作原理概述。输入图像被划分为网格,然后被送入检测过程,结果是大量的边界框,这些框通过应用一些阈值进一步过滤。
这个过程的结果是生成 S×S×N 个补充框。对于这些框的很大一部分,你会得到相当低的置信度分数,通过应用一个较低的阈值(在本例中为 30%),你可以消除大多数被错误分类的对象,如图所示。
在本节中,我们将使用一个预训练的 YOLOv2 模型进行目标检测和分类。
准备数据集
在这一部分,我们将探索如何使用现有的 COCO 数据集和自定义数据集进行数据准备。如果你想用很多类别来训练 YOLO 模型,你可以按照已有部分提供的指示操作,或者如果你想建立自己的自定义目标检测器,跟随自定义构建部分的说明。
使用预先存在的 COCO 数据集
在此实现中,我们将使用 COCO 数据集。这是一个用于训练 YOLOv2 以进行大规模图像检测、分割和标注的优质数据集资源。请从 cocodataset.org 下载数据集,并在终端中运行以下命令:
- 获取训练数据集:
wget http://images.cocodataset.org/zips/train2014.zip
- 获取验证数据集:
wget http://images.cocodataset.org/zips/val2014.zip
- 获取训练和验证的标注:
wget http://images.cocodataset.org/annotations/annotations_trainval2014.zip
现在,让我们将 COCO 格式的标注转换为 VOC 格式:
- 安装 Baker:
pip install baker
- 创建文件夹以存储图像和标注:
mkdir images annotations
- 在
images文件夹下解压train2014.zip和val2014.zip:
unzip train2014.zip -d ./images/
unzip val2014.zip -d ./images/
- 将
annotations_trainval2014.zip解压到annotations文件夹:
unzip annotations_trainval2014.zip -d ./annotations/
- 创建一个文件夹来存储转换后的数据:
mkdir output
mkdir output/train
mkdir output/val
python coco2voc.py create_annotations /TRAIN_DATA_PATH train /OUTPUT_FOLDER/train
python coco2voc.py create_annotations /TRAIN_DATA_PATH val /OUTPUT_FOLDER/val
最终转换后的文件夹结构如下所示:

图 9.10:COCO 数据提取和格式化过程示意图
这建立了图像和标注之间的完美对应关系。当验证集为空时,我们将使用 8:2 的比例自动拆分训练集和验证集。
结果是我们将有两个文件夹,./images 和 ./annotation,用于训练目的。
使用自定义数据集
现在,如果你想为你的特定应用场景构建一个目标检测器,那么你需要从网上抓取大约 100 到 200 张图像并进行标注。网上有很多标注工具可供使用,比如 LabelImg (github.com/tzutalin/labelImg) 或 Fast Image Data Annotation Tool (FIAT) (github.com/christopher5106/FastAnnotationTool)。
为了让你更好地使用自定义目标检测器,我们提供了一些带有相应标注的示例图像。请查看名为 Chapter09/yolo/new_class/ 的代码库文件夹。
每个图像都有相应的标注,如下图所示:

图 9.11:这里显示的是图像与标注之间的关系
此外,我们还需要从 pjreddie.com/darknet/yolo/ 下载预训练权重文件, 我们将用它来初始化模型,并在这些预训练权重的基础上训练自定义目标检测器:
wget https://pjreddie.com/media/files/yolo.weights
安装所有依赖:
我们将使用 Keras API 结合 TensorFlow 方法来创建 YOLOv2 架构。让我们导入所有依赖:
pip install keras tensorflow tqdm numpy cv2 imgaug
以下是相关的代码:
from keras.models import Sequential, Model
from keras.layers import Reshape, Activation, Conv2D, Input, MaxPooling2D, BatchNormalization, Flatten, Dense, Lambda
from keras.layers.advanced_activations import LeakyReLU
from keras.callbacks import EarlyStopping, ModelCheckpoint, TensorBoard
from keras.optimizers import SGD, Adam, RMSprop
from keras.layers.merge import concatenate
import matplotlib.pyplot as plt
import keras.backend as K
import tensorflow as tf
import imgaug as ia
from tqdm import tqdm
from imgaug import augmenters as iaa
import numpy as np
import pickle
import os, cv2
from preprocessing import parse_annotation, BatchGenerator
from utils import WeightReader, decode_netout, draw_boxes
#Setting GPU configs
os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID"
os.environ["CUDA_VISIBLE_DEVICES"] = ""
总是建议使用 GPU 来训练任何 YOLO 模型。
配置 YOLO 模型
YOLO 模型是通过一组超参数和其他配置来设计的。这个配置定义了构建模型的类型,以及模型的其他参数,如输入图像的大小和锚点列表。目前你有两个选择:tiny YOLO 和 full YOLO。以下代码定义了要构建的模型类型:
# List of object that YOLO model will learn to detect from COCO dataset
#LABELS = ['person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus', 'train', 'truck', 'boat', 'traffic light', 'fire hydrant', 'stop sign', 'parking meter', 'bench', 'bird', 'cat', 'dog', 'horse', 'sheep', 'cow', 'elephant', 'bear', 'zebra', 'giraffe', 'backpack', 'umbrella', 'handbag', 'tie', 'suitcase', 'frisbee', 'skis', 'snowboard', 'sports ball', 'kite', 'baseball bat', 'baseball glove', 'skateboard', 'surfboard', 'tennis racket', 'bottle', 'wine glass', 'cup', 'fork', 'knife', 'spoon', 'bowl', 'banana', 'apple', 'sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza', 'donut', 'cake', 'chair', 'couch', 'potted plant', 'bed', 'dining table', 'toilet', 'tv', 'laptop', 'mouse', 'remote', 'keyboard', 'cell phone', 'microwave', 'oven', 'toaster', 'sink', 'refrigerator', 'book', 'clock', 'vase', 'scissors', 'teddy bear', 'hair drier', 'toothbrush']
# Label for the custom curated dataset.
LABEL = ['kangaroo']
IMAGE_H, IMAGE_W = 416, 416
GRID_H, GRID_W = 13 , 13
BOX = 5
CLASS = len(LABELS)
CLASS_WEIGHTS = np.ones(CLASS, dtype='float32')
OBJ_THRESHOLD = 0.3
NMS_THRESHOLD = 0.3
ANCHORS = [0.57273, 0.677385, 1.87446, 2.06253, 3.33843, 5.47434, 7.88282, 3.52778, 9.77052, 9.16828]
NO_OBJECT_SCALE = 1.0
OBJECT_SCALE = 5.0
COORD_SCALE = 1.0
CLASS_SCALE = 1.0
BATCH_SIZE = 16
WARM_UP_BATCHES = 0
TRUE_BOX_BUFFER = 50
配置预训练模型和图像的路径,如以下代码所示:
wt_path = 'yolo.weights'
train_image_folder = '/new_class/images/'
train_annot_folder = '/new_class/anno/'
valid_image_folder = '/new_class/images/'
valid_annot_folder = '/new_class/anno/'
定义 YOLO v2 模型
现在,让我们来看看 YOLOv2 模型的架构:
# the function to implement the organization layer (thanks to github.com/allanzelener/YAD2K)
def space_to_depth_x2(x):
return tf.space_to_depth(x, block_size=2)
input_image = Input(shape=(IMAGE_H, IMAGE_W, 3))
true_boxes = Input(shape=(1, 1, 1, TRUE_BOX_BUFFER , 4))
# Layer 1
x = Conv2D(32, (3,3), strides=(1,1), padding='same', name='conv_1', use_bias=False)(input_image)
x = BatchNormalization(name='norm_1')(x)
x = LeakyReLU(alpha=0.1)(x)
x = MaxPooling2D(pool_size=(2, 2))(x)
# Layer 2
x = Conv2D(64, (3,3), strides=(1,1), padding='same', name='conv_2', use_bias=False)(x)
x = BatchNormalization(name='norm_2')(x)
x = LeakyReLU(alpha=0.1)(x)
x = MaxPooling2D(pool_size=(2, 2))(x)
# Layer 3
# Layer 4
# Layer 23
# For the entire architecture, please refer to the yolo/Yolo_v2_train.ipynb notebook here: https://github.com/PacktPublishing/Python-Deep-Learning-Projects/blob/master/Chapter09/yolo/Yolo_v2_train.ipynb
我们刚刚创建的网络架构可以在这里找到:github.com/PacktPublishing/Python-Deep-Learning-Projects/blob/master/Chapter09/Network_architecture/network_architecture.png
以下是输出结果:
Total params: 50,983,561
Trainable params: 50,962,889
Non-trainable params: 20,672
训练模型
以下是训练模型的步骤:
- 加载我们下载的权重并用它们初始化模型:
weight_reader = WeightReader(wt_path)
weight_reader.reset()
nb_conv = 23
for i in range(1, nb_conv+1):
conv_layer = model.get_layer('conv_' + str(i))
if i < nb_conv:
norm_layer = model.get_layer('norm_' + str(i))
size = np.prod(norm_layer.get_weights()[0].shape)
beta = weight_reader.read_bytes(size)
gamma = weight_reader.read_bytes(size)
mean = weight_reader.read_bytes(size)
var = weight_reader.read_bytes(size)
weights = norm_layer.set_weights([gamma, beta, mean, var])
if len(conv_layer.get_weights()) > 1:
bias = weight_reader.read_bytes(np.prod(conv_layer.get_weights()[1].shape))
kernel = weight_reader.read_bytes(np.prod(conv_layer.get_weights()[0].shape))
kernel = kernel.reshape(list(reversed(conv_layer.get_weights()[0].shape)))
kernel = kernel.transpose([2,3,1,0])
conv_layer.set_weights([kernel, bias])
else:
kernel = weight_reader.read_bytes(np.prod(conv_layer.get_weights()[0].shape))
kernel = kernel.reshape(list(reversed(conv_layer.get_weights()[0].shape)))
kernel = kernel.transpose([2,3,1,0])
conv_layer.set_weights([kernel])
- 随机化最后一层的权重:
layer = model.layers[-4] # the last convolutional layer
weights = layer.get_weights()
new_kernel = np.random.normal(size=weights[0].shape)/(GRID_H*GRID_W)
new_bias = np.random.normal(size=weights[1].shape)/(GRID_H*GRID_W)
layer.set_weights([new_kernel, new_bias])
- 生成如下代码中的配置:
generator_config = {
'IMAGE_H' : IMAGE_H,
'IMAGE_W' : IMAGE_W,
'GRID_H' : GRID_H,
'GRID_W' : GRID_W,
'BOX' : BOX,
'LABELS' : LABELS,
'CLASS' : len(LABELS),
'ANCHORS' : ANCHORS,
'BATCH_SIZE' : BATCH_SIZE,
'TRUE_BOX_BUFFER' : 50,
}
- 创建训练和验证批次:
# Training batch data
train_imgs, seen_train_labels = parse_annotation(train_annot_folder, train_image_folder, labels=LABELS)
train_batch = BatchGenerator(train_imgs, generator_config, norm=normalize)
# Validation batch data
valid_imgs, seen_valid_labels = parse_annotation(valid_annot_folder, valid_image_folder, labels=LABELS)
valid_batch = BatchGenerator(valid_imgs, generator_config, norm=normalize, jitter=False)
- 设置早停和检查点回调:
early_stop = EarlyStopping(monitor='val_loss',
min_delta=0.001,
patience=3,
mode='min',
verbose=1)
checkpoint = ModelCheckpoint('weights_coco.h5',
monitor='val_loss',
verbose=1,
save_best_only=True,
mode='min',
period=1)
- 使用以下代码来训练模型:
tb_counter = len([log for log in os.listdir(os.path.expanduser('~/logs/')) if 'coco_' in log]) + 1
tensorboard = TensorBoard(log_dir=os.path.expanduser('~/logs/') + 'coco_' + '_' + str(tb_counter),
histogram_freq=0,
write_graph=True,
write_images=False)
optimizer = Adam(lr=0.5e-4, beta_1=0.9, beta_2=0.999, epsilon=1e-08, decay=0.0)
#optimizer = SGD(lr=1e-4, decay=0.0005, momentum=0.9)
#optimizer = RMSprop(lr=1e-4, rho=0.9, epsilon=1e-08, decay=0.0)
model.compile(loss=custom_loss, optimizer=optimizer)
model.fit_generator(generator = train_batch,
steps_per_epoch = len(train_batch),
epochs = 100,
verbose = 1,
validation_data = valid_batch,
validation_steps = len(valid_batch),
callbacks = [early_stop, checkpoint, tensorboard],
max_queue_size = 3)
以下是输出结果:
Epoch 1/2
11/11 [==============================] - 315s 29s/step - loss: 3.6982 - val_loss: 1.5416
Epoch 00001: val_loss improved from inf to 1.54156, saving model to weights_coco.h5
Epoch 2/2
11/11 [==============================] - 307s 28s/step - loss: 1.4517 - val_loss: 1.0636
Epoch 00002: val_loss improved from 1.54156 to 1.06359, saving model to weights_coco.h5
以下是仅两个 epoch 的 TensorBoard 输出:

图 9.12:该图表示 2 个 epoch 的损失曲线
评估模型
一旦训练完成,让我们通过将输入图像馈送到模型中来执行预测:
- 首先,我们将模型加载到内存中:
model.load_weights("weights_coco.h5")
- 现在设置测试图像路径并读取它:
input_image_path = "my_test_image.jpg"
image = cv2.imread(input_image_path)
dummy_array = np.zeros((1,1,1,1,TRUE_BOX_BUFFER,4))
plt.figure(figsize=(10,10))
- 对图像进行归一化:
input_image = cv2.resize(image, (416, 416))
input_image = input_image / 255.
input_image = input_image[:,:,::-1]
input_image = np.expand_dims(input_image, 0)
- 做出预测:
netout = model.predict([input_image, dummy_array])
boxes = decode_netout(netout[0],
obj_threshold=OBJ_THRESHOLD,
nms_threshold=NMS_THRESHOLD,
anchors=ANCHORS,
nb_class=CLASS)
image = draw_boxes(image, boxes, labels=LABELS)
plt.imshow(image[:,:,::-1]); plt.show()
这是一些结果:




恭喜你,你已经开发出了一个非常快速且可靠的最先进物体检测器。
我们学习了如何使用 YOLO 架构构建一个世界级的物体检测模型,结果看起来非常有前景。现在,你也可以将相同的模型部署到其他移动设备或树莓派上。
图像分割
图像分割是将图像中的内容按像素级别进行分类的过程。例如,如果你给定一张包含人的图片,将人从图像中分离出来就是图像分割,并且是通过像素级别的信息来完成的。
我们将使用 COCO 数据集进行图像分割。
在执行任何 SegNet 脚本之前,你需要做以下工作:
cd SegNet
wget http://images.cocodataset.org/zips/train2014.zip
mkdir images
unzip train2014.zip -d images
在执行 SegNet 脚本时,确保你的当前工作目录是SegNet。
导入所有依赖项
在继续之前,确保重新启动会话。
我们将使用numpy、pandas、keras、pylab、skimage、matplotlib和pycocotools,如以下代码所示:
from __future__ import absolute_import
from __future__ import print_function
import pylab
import numpy as np
import pandas as pd
import skimage.io as io
import matplotlib.pyplot as plt
from pycocotools.coco import COCO
pylab.rcParams['figure.figsize'] = (8.0, 10.0)
import cv2
import keras.models as models, Sequential
from keras.layers import Layer, Dense, Dropout, Activation, Flatten, Reshape, Permute
from keras.layers import Conv2D, MaxPool2D, UpSampling2D, ZeroPadding2D
from keras.layers import BatchNormalization
from keras.callbacks import ModelCheckpoint, ReduceLROnPlateau
from keras.optimizers import Adam
import keras
keras.backend.set_image_dim_ordering('th')
from tqdm import tqdm
import itertools
%matplotlib inline
探索数据
我们将首先定义用于图像分割的注释文件的位置,然后初始化 COCO API:
# set the location of the annotation file associated with the train images
annFile='annotations/annotations/instances_train2014.json'
# initialize COCO api with
coco = COCO(annFile)
下面是应该得到的输出:
loading annotations into memory...
Done (t=12.84s)
creating index...
index created!
图像
由于我们正在构建一个二进制分割模型,让我们考虑从images/train2014文件夹中只标记为person标签的图像,以便将图像中的人分割出来。COCO API 为我们提供了易于使用的方法,其中两个常用的方法是getCatIds和getImgIds。以下代码片段将帮助我们提取所有带有person标签的图像 ID:
# extract the category ids using the label 'person'
catIds = coco.getCatIds(catNms=['person'])
# extract the image ids using the catIds
imgIds = coco.getImgIds(catIds=catIds )
# print number of images with the tag 'person'
print("Number of images with the tag 'person' :" ,len(imgIds))
这应该是输出结果:
Number of images with the tag 'person' : 45174
现在让我们使用以下代码片段来绘制图像:
# extract the details of image with the image id
img = coco.loadImgs(imgIds[2])[0]
print(img)
# load the image using the location of the file listed in the image variable
I = io.imread('images/train2014/'+img['file_name'])
# display the image
plt.imshow(I)
下面是应该得到的输出:
{'height': 426, 'coco_url': 'http://images.cocodataset.org/train2014/COCO_train2014_000000524291.jpg', 'date_captured': '2013-11-18 09:59:07', 'file_name': 'COCO_train2014_000000524291.jpg', 'flickr_url': 'http://farm2.staticflickr.com/1045/934293170_d1b2cc58ff_z.jpg', 'width': 640, 'id': 524291, 'license': 3}
我们得到如下输出图像:

图 9.13:数据集中样本图像的绘制表示。
在前面的代码片段中,我们将一个图像 ID 传入 COCO 的loadImgs方法,以提取与该图像对应的详细信息。如果你查看img变量的输出,列出的一项键是file_name键。这个键包含了位于images/train2014/文件夹中的图像名称。
然后,我们使用已导入的io模块的imread方法读取图像,并使用matplotlib.pyplot进行绘制。
注释
现在让我们加载与之前图片对应的标注,并在图片上绘制该标注。coco.getAnnIds()函数帮助我们通过图像 ID 加载标注信息。然后,借助coco.loadAnns()函数,我们加载标注并通过coco.showAnns()函数绘制出来。重要的是,你要先绘制图像,再进行标注操作,代码片段如下所示:
# display the image
plt.imshow(I)
# extract the annotation id
annIds = coco.getAnnIds(imgIds=img['id'], catIds=catIds, iscrowd=None)
# load the annotation
anns = coco.loadAnns(annIds)
# plot the annotation on top of the image
coco.showAnns(anns)
以下应为输出:

图 9.14:在图像上可视化标注
为了能够获取标注标签数组,使用coco.annToMask()函数,如以下代码片段所示。该数组将帮助我们形成分割目标:
# build the mask for display with matplotlib
mask = coco.annToMask(anns[0])
# display the mask
plt.imshow(mask)
以下应为输出:

图 9.15:仅可视化标注
准备数据
现在让我们定义一个data_list()函数,它将自动化加载图像及其分割数组到内存,并使用 OpenCV 将它们调整为 360*480 的形状。此函数返回两个列表,其中包含图像和分割数组:
def data_list(imgIds, count = 12127, ratio = 0.2):
"""Function to load image and its target into memory."""
img_lst = []
lab_lst = []
for x in tqdm(imgIds[0:count]):
# load image details
img = coco.loadImgs(x)[0]
# read image
I = io.imread('images/train2014/'+img['file_name'])
if len(I.shape)<3:
continue
# load annotation information
annIds = coco.getAnnIds(imgIds=img['id'], catIds=catIds, iscrowd=None)
# load annotation
anns = coco.loadAnns(annIds)
# prepare mask
mask = coco.annToMask(anns[0])
# This condition makes sure that we select images having only one person
if len(np.unique(mask)) == 2:
# Next condition selects images where ratio of area covered by the
# person to the entire image is greater than the ratio parameter
# This is done to not have large class imbalance
if (len(np.where(mask>0)[0])/len(np.where(mask>=0)[0])) > ratio :
# If you check, generated mask will have 2 classes i.e 0 and 2
# (0 - background/other, 1 - person).
# to avoid issues with cv2 during the resize operation
# set label 2 to 1, making label 1 as the person.
mask[mask==2] = 1
# resize image and mask to shape (480, 360)
I= cv2.resize(I, (480,360))
mask = cv2.resize(mask, (480,360))
# append mask and image to their lists
img_lst.append(I)
lab_lst.append(mask)
return (img_lst, lab_lst)
# get images and their labels
img_lst, lab_lst = data_list(imgIds)
print('Sum of images for training, validation and testing :', len(img_lst))
print('Unique values in the labels array :', np.unique(lab_lst[0]))
以下应为输出:
Sum of images for training, validation and testing : 1997
Unique values in the labels array : [0 1]
图像归一化
首先,让我们定义make_normalize()函数,它接受一张图像并对其进行直方图归一化操作。返回的对象是一个归一化后的数组:
def make_normalize(img):
"""Function to histogram normalize images."""
norm_img = np.zeros((img.shape[0], img.shape[1], 3),np.float32)
b=img[:,:,0]
g=img[:,:,1]
r=img[:,:,2]
norm_img[:,:,0]=cv2.equalizeHist(b)
norm_img[:,:,1]=cv2.equalizeHist(g)
norm_img[:,:,2]=cv2.equalizeHist(r)
return norm_img
plt.figure(figsize = (14,5))
plt.subplot(1,2,1)
plt.imshow(img_lst[9])
plt.title(' Original Image')
plt.subplot(1,2,2)
plt.imshow(make_normalize(img_lst[9]))
plt.title(' Histogram Normalized Image')
以下应为输出:

图 9.16:图像直方图归一化前后对比
在前面的截图中,我们看到左边是原始图片,非常清晰,而右边是归一化后的图片,几乎看不见。
编码
定义了make_normalize()函数后,我们现在定义一个make_target函数。该函数接受形状为(360, 480)的分割数组,然后返回形状为(360,480,2)的分割目标。在目标中,通道0表示背景,并且在图像中代表背景的位置为1,其他位置为零。通道1表示人物,并且在图像中代表人物的位置为1,其他位置为0。以下代码实现了该函数:
def make_target(labels):
"""Function to one hot encode targets."""
x = np.zeros([360,480,2])
for i in range(360):
for j in range(480):
x[i,j,labels[i][j]]=1
return x
plt.figure(figsize = (14,5))
plt.subplot(1,2,1)
plt.imshow(make_target(lab_lst[0])[:,:,0])
plt.title('Background')
plt.subplot(1,2,2)
plt.imshow(make_target(lab_lst[0])[:,:,1])
plt.title('Person')
以下应为输出:

图 9.17:可视化编码后的目标数组
模型数据
我们现在定义一个名为model_data()的函数,它接受图像列表和标签列表。该函数将对每个图像应用make_normalize()函数以进行归一化,并对每个标签/分割数组应用make_encode()函数以获得编码后的数组。
该函数返回两个列表,一个包含归一化后的图像,另一个包含对应的目标数组:
def model_data(images, labels):
"""Function to perform normalize and encode operation on each image."""
# empty label and image list
array_lst = []
label_lst=[]
# apply normalize function on each image and encoding function on each label
for x,y in tqdm(zip(images, labels)):
array_lst.append(np.rollaxis(normalized(x), 2))
label_lst.append(make_target(y))
return np.array(array_lst), np.array(label_lst)
# Get model data
train_data, train_lab = model_data(img_lst, lab_lst)
flat_image_shape = 360*480
# reshape target array
train_label = np.reshape(train_lab,(-1,flat_image_shape,2))
# test data
test_data = test_data[1900:]
# validation data
val_data = train_data[1500:1900]
# train data
train_data = train_data[:1500]
# test label
test_label = test_label[1900:]
# validation label
val_label = train_label[1500:1900]
# train label
train_label = train_label[:1500]
在前面的代码片段中,我们还将数据划分为训练集、测试集和验证集,其中训练集包含1500个数据点,验证集包含400个数据点,测试集包含97个数据点。
定义超参数
以下是一些我们将在整个代码中使用的定义超参数,它们是完全可配置的:
# define optimizer
optimizer = Adam(lr=0.002)
# input shape to the model
input_shape=(3, 360, 480)
# training batchsize
batch_size = 6
# number of training epochs
nb_epoch = 60
要了解更多关于optimizers及其在 Keras 中的 API,请访问keras.io/optimizers/。如果遇到关于 GPU 的资源耗尽错误,请减少batch_size。
尝试不同的学习率、optimizers和batch_size,看看这些因素如何影响模型的质量,如果得到更好的结果,可以与深度学习社区分享。
定义 SegNet
为了进行图像分割,我们将构建一个 SegNet 模型,它与我们在第八章中构建的自编码器非常相似:使用卷积神经网络进行手写数字分类,如图所示:

图 9.18:本章使用的 SegNet 架构
我们将定义的 SegNet 模型将接受(3,360, 480)的图像作为输入,目标是(172800, 2)的分割数组,并且在编码器中将具有以下特点:
-
第一层是一个具有 64 个 33 大小滤波器的二维卷积层,
activation为relu,接着是批量归一化,然后是使用大小为 22 的 MaxPooling2D 进行下采样。 -
第二层是一个具有 128 个 33 大小滤波器的二维卷积层,
activation为relu,接着是批量归一化,然后是使用大小为 22 的 MaxPooling2D 进行下采样。 -
第三层是一个具有 256 个 33 大小滤波器的二维卷积层,
activation为relu,接着是批量归一化,然后是使用大小为 22 的 MaxPooling2D 进行下采样。 -
第四层再次是一个具有 512 个 3*3 大小滤波器的二维卷积层,
activation为relu,接着是批量归一化。
模型在解码器中将具有以下特点:
-
第一层是一个具有 512 个 33 大小滤波器的二维卷积层,
activation为relu,接着是批量归一化,然后是使用大小为 22 的 UpSampling2D 进行下采样。 -
第二层是一个具有 256 个 33 大小滤波器的二维卷积层,
activation为relu,接着是批量归一化,然后是使用大小为 22 的 UpSampling2D 进行下采样。 -
第三层是一个具有 128 个 33 大小滤波器的二维卷积层,
activation为relu,接着是批量归一化,然后是使用大小为 22 的 UpSampling2D 进行下采样。 -
第四层是一个具有 64 个 3*3 大小滤波器的二维卷积层,
activation为relu,接着是批量归一化。 -
第五层是一个大小为 1*1 的 2 个卷积 2D 层,接着是 Reshape、Permute 和一个
softmax激活层,用于预测得分。
使用以下代码描述模型:
model = Sequential()
# Encoder
model.add(Layer(input_shape=input_shape))
model.add(ZeroPadding2D())
model.add(Conv2D(filters=64, kernel_size=(3,3), padding='valid', activation='relu'))
model.add(BatchNormalization())
model.add(MaxPool2D(pool_size=(2,2)))
model.add(ZeroPadding2D())
model.add(Conv2D(filters=128, kernel_size=(3,3), padding='valid', activation='relu'))
model.add(BatchNormalization())
model.add(MaxPool2D(pool_size=(2,2)))
model.add(ZeroPadding2D())
model.add(Conv2D(filters=256, kernel_size=(3,3), padding='valid', activation='relu'))
model.add(BatchNormalization())
model.add(MaxPool2D(pool_size=(2,2)))
model.add(ZeroPadding2D())
model.add(Conv2D(filters=512, kernel_size=(3,3), padding='valid', activation='relu'))
model.add(BatchNormalization())
# Decoder # For the remaining part of this section of the code refer to the segnet.ipynb file in the SegNet folder. Here is the github link: https://github.com/PacktPublishing/Python-Deep-Learning-Projects/tree/master/Chapter09
编译模型
在模型定义完成后,使用categorical_crossentropy作为loss,Adam作为optimizer来编译模型,这由超参数部分中的optimizer变量定义。我们还将定义ReduceLROnPlateau,以便在训练过程中根据需要减少学习率,如下所示:
# compile model
model.compile(loss="categorical_crossentropy", optimizer=Adam(lr=0.002), metrics=["accuracy"])
# use ReduceLROnPlateau to adjust the learning rate
reduceLROnPlat = ReduceLROnPlateau(monitor='val_acc', factor=0.75, patience=5,
min_delta=0.005, mode='max', cooldown=3, verbose=1)
callbacks_list = [reduceLROnPlat]
拟合模型
模型编译完成后,我们将使用模型的fit方法在数据上拟合模型。在这里,由于我们在一个小的数据集上进行训练,重要的是将参数 shuffle 设置为True,以便在每个 epoch 后对图像进行打乱:
# fit the model
history = model.fit(train_data, train_label, callbacks=callbacks_list,
batch_size=batch_size, epochs=nb_epoch,
verbose=1, shuffle = True, validation_data = (val_data, val_label))
这应为输出结果:

图 9.19:训练输出
以下展示了准确率和损失曲线:

图 9.20:展示训练进展的曲线
测试模型
训练好模型后,在测试数据上评估模型,如下所示:
loss,acc = model.evaluate(test_data, test_label)
print('Loss :', loss)
print('Accuracy :', acc)
这应为输出结果:
97/97 [==============================] - 7s 71ms/step
Loss : 0.5390811630131043
Accuracy : 0.7633129960482883
我们看到,我们构建的 SegNet 模型在测试图像上损失为 0.539,准确率为 76.33。
让我们绘制测试图像及其相应生成的分割结果,以便理解模型的学习情况:
for i in range(3):
plt.figure(figsize = (10,3))
plt.subplot(1,2,1)
plt.imshow(img_lst[1900+i])
plt.title('Input')
plt.subplot(1,2,2)
plt.imshow(model.predict_classes(test_data[i:(i+1)*1]).reshape(360,480))
plt.title('Segmentation')
以下应为输出结果:

图 9.21:在测试图像上生成的分割结果
从前面的图中,我们可以看到模型能够从图像中分割出人物。
结论
项目的第一部分是使用 Keras 中的 YOLO 架构构建一个目标检测分类器。
项目的第二部分是建立一个二进制图像分割模型,针对的是包含人物和背景的 COCO 图像。目标是建立一个足够好的模型,将图像中的人物从背景中分割出来。
我们通过在 1500 张形状为 3604803 的图像上进行训练构建的模型,在训练数据上的准确率为 79%,在验证和测试数据上的准确率为 78%。该模型能够成功地分割出图像中的人物,但分割的边缘略微偏离应有的位置。这是由于使用了一个较小的训练集。考虑到训练所用的图像数量,模型在分割上做得还是很不错的。
在这个数据集中还有更多可用于训练的图像,虽然使用 Nvidia Tesla K80 GPU 训练所有图像可能需要一天以上的时间,但这样做将能够获得非常好的分割效果。
总结
在本章的第一部分,我们学习了如何使用现有的分类器构建一个 RESTful 服务来进行目标检测,并且我们还学习了如何使用 YOLO 架构的目标检测分类器和 Keras 构建一个准确的目标检测器,同时实现了迁移学习。在本章的第二部分,我们了解了图像分割是什么,并在 COCO 数据集的图像上构建了一个图像分割模型。我们还在测试数据上测试了目标检测器和图像分割器的性能,并确定我们成功达成了目标。
第十章:使用 FaceNet 构建人脸识别
在上一章中,我们学习了如何在图像中检测物体。本章中,我们将探讨物体检测的一个具体应用——人脸识别。人脸识别结合了两项主要操作:人脸检测,接着是人脸分类。
在这个项目中提供我们业务用例的(假设)客户是一个高性能计算数据中心,属于 Tier III,并获得了可持续性认证。他们设计了这个设施,以满足对自然灾害的最高保护标准,并配备了许多冗余系统。
目前该设施已经实施了超高安全协议,以防止恶意的人为灾难,并且他们希望通过人脸识别技术增强其安全性,用于控制设施内的安全区域访问。
他们所容纳和维护的服务器处理着世界上最敏感、最有价值且最具影响力的数据,因此风险非常高:

这个人脸识别系统需要能够准确识别出他们自己员工的身份,同时也能识别出偶尔参观数据中心进行检查的客户员工。
他们要求我们提供一个基于智能的能力的 POC,供审查并随后在他们的数据中心中应用。
所以,在本章中,我们将学习如何构建一个世界级的人脸识别系统。我们将定义如下的流程:
-
人脸检测:首先,查看一张图像并找到其中所有可能的人脸
-
人脸提取:其次,聚焦于每一张人脸图像并理解它,例如它是否转向一侧或光线较暗
-
特征提取:第三,使用卷积神经网络(CNN)从人脸中提取独特特征
-
分类器训练:最后,将该人脸的独特特征与所有已知人员的特征进行比较,从而确定此人的姓名
你将学习每个步骤背后的主要思想,以及如何使用以下深度学习技术在 Python 中构建你自己的面部识别系统:
-
dlib (
dlib.net/):提供一个可以用于人脸检测和对齐的库。 -
OpenFace (
cmusatyalab.github.io/openface/):一个深度学习人脸识别模型,由 Brandon Amos 等人(bamos.github.io/)开发。它还能够在实时移动设备上运行。 -
FaceNet (
arxiv.org/abs/1503.03832):一种用于特征提取的 CNN 架构。FaceNet 使用三元组损失作为损失函数。三元组损失通过最小化正样本之间的距离,同时最大化负样本之间的距离来工作。
设置环境
由于设置过程可能非常复杂并且耗时,且本章不涉及这些内容,我们将构建一个包含所有依赖项(包括 dlib、OpenFace 和 FaceNet)的 Docker 镜像。
获取代码
从仓库中获取我们将用来构建人脸识别的代码:
git clone https://github.com/PacktPublishing/Python-Deep-Learning-Projects
cd Chapter10/
构建 Docker 镜像
Docker 是一个容器平台,简化了部署过程。它解决了在不同服务器环境中安装软件依赖的问题。如果你是 Docker 新手,可以在 www.docker.com/ 阅读更多内容。
要在 Linux 机器上安装 Docker,请运行以下命令:
curl https://get.docker.com | sh
对于 macOS 和 Windows 等其他系统,请访问 docs.docker.com/install/。如果你已经安装了 Docker,可以跳过此步骤。
安装 Docker 后,你应该能够在终端中使用 docker 命令,示例如下:

现在我们将创建一个 docker 文件,安装所有依赖项,包括 OpenCV、dlib 和 TensorFlow。该文件可以在 GitHub 仓库中找到,链接如下:github.com/PacktPublishing/Python-Deep-Learning-Projects/tree/master/Chapter10/Dockerfile:
#Dockerfile for our env setup
FROM tensorflow/tensorflow:latest
RUN apt-get update -y --fix-missing
RUN apt-get install -y ffmpeg
RUN apt-get install -y build-essential cmake pkg-config \
libjpeg8-dev libtiff5-dev libjasper-dev libpng12-dev \
libavcodec-dev libavformat-dev libswscale-dev libv4l-dev \
libxvidcore-dev libx264-dev \
libgtk-3-dev \
libatlas-base-dev gfortran \
libboost-all-dev \
python3 python3-dev python3-numpy
RUN apt-get install -y wget vim python3-tk python3-pip
WORKDIR /
RUN wget -O opencv.zip https://github.com/Itseez/opencv/archive/3.2.0.zip \
&& unzip opencv.zip \
&& wget -O opencv_contrib.zip https://github.com/Itseez/opencv_contrib/archive/3.2.0.zip \
&& unzip opencv_contrib.zip
# install opencv3.2
RUN cd /opencv-3.2.0/ \
&& mkdir build \
&& cd build \
&& cmake -D CMAKE_BUILD_TYPE=RELEASE \
-D INSTALL_C_EXAMPLES=OFF \
-D INSTALL_PYTHON_EXAMPLES=ON \
-D OPENCV_EXTRA_MODULES_PATH=/opencv_contrib-3.2.0/modules \
-D BUILD_EXAMPLES=OFF \
-D BUILD_opencv_python2=OFF \
-D BUILD_NEW_PYTHON_SUPPORT=ON \
-D CMAKE_INSTALL_PREFIX=$(python3 -c "import sys; print(sys.prefix)") \
-D PYTHON_EXECUTABLE=$(which python3) \
-D WITH_FFMPEG=1 \
-D WITH_CUDA=0 \
.. \
&& make -j8 \
&& make install \
&& ldconfig \
&& rm /opencv.zip \
&& rm /opencv_contrib.zip
# Install dlib 19.4
RUN wget -O dlib-19.4.tar.bz2 http://dlib.net/files/dlib-19.4.tar.bz2 \
&& tar -vxjf dlib-19.4.tar.bz2
RUN cd dlib-19.4 \
&& cd examples \
&& mkdir build \
&& cd build \
&& cmake .. \
&& cmake --build . --config Release \
&& cd /dlib-19.4 \
&& pip3 install setuptools \
&& python3 setup.py install \
&& cd $WORKDIR \
&& rm /dlib-19.4.tar.bz2
ADD $PWD/requirements.txt /requirements.txt
RUN pip3 install -r /requirements.txt
CMD ["/bin/bash"]
现在执行以下命令来构建镜像:
docker build -t hellorahulk/facerecognition -f Dockerfile
安装所有依赖项并构建 Docker 镜像大约需要 20-30 分钟:

下载预训练模型
我们将下载一些额外的文件,这些文件将在本章后面详细使用和讨论。
使用以下命令下载 dlib 的人脸关键点预测器:
curl -O http://dlib.net/
files/shape_predictor_68_face_landmarks.dat.bz2
bzip2 -d shape_predictor_68_face_landmarks.dat.bz2
cp shape_predictor_68_face_landmarks.dat facenet/
下载预训练的 Inception 模型:
curl -L -O https://www.dropbox.com/s/hb75vuur8olyrtw/Resnet-185253.pb
cp Resnet-185253.pb pre-model/
一旦我们准备好所有组件,文件夹结构应该大致如下所示:

代码的文件夹结构
确保你将要训练模型的人的图像保存在 /data 文件夹中,并将该文件夹命名为 /data/<class_name>/<class_name>_000<count>.jpg。
/output 文件夹将包含训练后的 SVM 分类器和所有预处理的图像,这些图像将保存在一个子文件夹 /intermediate 中,使用与 /data 文件夹相同的命名规则。
专业提示:为了提高准确度,始终确保每个类别有超过五张样本图像。这将有助于模型更快地收敛,并且能够更好地泛化。
构建管道
人脸识别是一种生物识别解决方案,它通过测量面部的独特特征来进行识别。为了执行人脸识别,你需要一种方式来唯一地表示一个面孔。
任何人脸识别系统的基本思想是将面部特征分解为独特的特征,然后使用这些特征来表示身份。
构建一个强大的特征提取流水线非常重要,因为它将直接影响我们系统的性能和准确性。1960 年,伍德罗·布莱德索(Woodrow Bledsoe)使用了一种技术,标记出面部显著特征的坐标。这些特征包括发际线、眼睛和鼻子的位置信息。
后来,在 2005 年,发明了一种更强大的技术——定向梯度直方图(HOG)。这种技术捕捉了图像中密集像素的朝向。
目前最先进的技术,超越了所有其他技术,使用的是卷积神经网络(CNN)。2015 年,谷歌的研究人员发布了一篇论文,描述了他们的系统 FaceNet (arxiv.org/abs/1503.03832),该系统使用 CNN 依赖图像像素来识别特征,而不是手动提取它们。
为了构建面部识别流水线,我们将设计以下流程(在图中用橙色方块表示):
-
预处理:找到所有的面部,修正面部的朝向。
-
特征提取:从处理过的面部图像中提取独特的特征。
-
分类器训练:使用 128 维特征训练 SVM 分类器。
图示如下:

这张图示例了面部识别流水线的端到端流程。
我们将详细查看每一个步骤,并构建我们世界级的面部识别系统。
图像的预处理
我们流水线的第一步是面部检测。然后我们将对面部进行对齐,提取特征,并最终在 Docker 上完成预处理。
面部检测
很显然,首先定位给定照片中的面部是非常重要的,这样它们才能被送入流水线的后续部分。检测面部有很多方法,例如检测皮肤纹理、椭圆/圆形形状检测以及其他统计方法。我们将使用一种叫做 HOG 的方法。
HOG是一种特征描述符,表示梯度方向(或定向梯度)的分布(直方图),这些梯度被用作特征。图像的梯度(x和y导数)是有用的,因为在边缘和角落(强度变化突然的区域)周围,梯度的幅值较大,而这些是图像中的优秀特征。
为了在图像中找到面部,我们将把图像转换为灰度图像。然后,我们会逐个查看图像中的每个像素,并尝试使用 HOG 检测器提取像素的朝向。我们将使用dlib.get_frontal_face_detector()来创建我们的面部检测器。
以下小示例展示了基于 HOG 的面部检测器在实现中的应用:
import sys
import dlib
from skimage import io
# Create a HOG face detector using the built-in dlib class
face_detector = dlib.get_frontal_face_detector()
# Load the image into an array
file_name = 'sample_face_image.jpeg'
image = io.imread(file_name)
# Run the HOG face detector on the image data.
# The result will be the bounding boxes of the faces in our image.
detected_faces = face_detector(image, 1)
print("Found {} faces.".format(len(detected_faces)))
# Loop through each face we found in the image
for i, face_rect in enumerate(detected_faces):
# Detected faces are returned as an object with the coordinates
# of the top, left, right and bottom edges
print("- Face #{} found at Left: {} Top: {} Right: {} Bottom: {}".format(i+1, face_rect.left(), face_rect.top(), face_rect.right(), face_rect.bottom()))
输出结果如下:
Found 1 faces.
-Face #1 found at Left: 365 Top: 365 Right: 588 Bottom: 588
对齐面部
一旦我们知道面部所在的区域,就可以执行各种隔离技术,从整体图像中提取出面部。
需要解决的一个挑战是,图像中的人脸可能会被旋转成不同的方向,使其在机器看来有些不同。
为了解决这个问题,我们将对每张图像进行扭曲,使得眼睛和嘴唇始终位于提供图像中的相同位置。这将使我们在接下来的步骤中更容易进行人脸比较。为此,我们将使用一种叫做人脸地标估计的算法。
基本思想是,我们将提出 68 个特定的关键点(称为地标),这些关键点存在于每张脸上——下巴顶部、每只眼睛的外缘、每条眉毛的内缘等等。然后,我们将训练一个机器学习算法,使其能够在任何脸部上找到这 68 个特定的关键点。
我们将在每张脸上定位的 68 个地标显示在下图中:

这张图像是由 Brandon Amos 创建的(bamos.github.io/),他在 OpenFace 项目中工作(github.com/cmusatyalab/openface)。
这里有一个小片段,演示了如何使用我们在环境设置部分下载的人脸地标:
import sys
import dlib
import cv2
import openface
predictor_model = "shape_predictor_68_face_landmarks.dat"
# Create a HOG face detector , Shape Predictor and Aligner
face_detector = dlib.get_frontal_face_detector()
face_pose_predictor = dlib.shape_predictor(predictor_model)
face_aligner = openface.AlignDlib(predictor_model)
# Take the image file name from the command line
file_name = 'sample_face_image.jpeg'
# Load the image
image = cv2.imread(file_name)
# Run the HOG face detector on the image data
detected_faces = face_detector(image, 1)
print("Found {} faces.".format(len(detected_faces))
# Loop through each face we found in the image
for i, face_rect in enumerate(detected_faces):
# Detected faces are returned as an object with the coordinates
# of the top, left, right and bottom edges
print("- Face #{} found at Left: {} Top: {} Right: {} Bottom: {}".format(i, face_rect.left(), face_rect.top(), face_rect.right(), face_rect.bottom()))
# Get the the face's pose
pose_landmarks = face_pose_predictor(image, face_rect)
# Use openface to calculate and perform the face alignment
alignedFace = face_aligner.align(534, image, face_rect, landmarkIndices=openface.AlignDlib.OUTER_EYES_AND_NOSE)
# Save the aligned image to a file
cv2.imwrite("aligned_face_{}.jpg".format(i), alignedFace)
使用这个方法,我们可以执行各种基本的图像变换,如旋转和缩放,同时保持平行线的特性。这些变换也被称为仿射变换(en.wikipedia.org/wiki/Affine_transformation)。
输出结果如下:

通过分割,我们解决了在图像中找到最大脸部的问题,而通过对齐,我们基于眼睛和下唇的位置,将输入图像标准化为居中。
这是我们数据集中的一个示例,展示了原始图像和处理后的图像:

特征提取
现在我们已经完成了数据的分割和对齐,我们将生成每个身份的向量嵌入。这些嵌入可以作为分类、回归或聚类任务的输入。
训练一个 CNN 输出人脸嵌入的过程需要大量的数据和计算能力。然而,一旦网络训练完成,它就可以为任何脸部生成测量结果,甚至是它从未见过的脸!因此,这一步只需要做一次。
为了方便起见,我们提供了一个已经在 Inception-Resnet-v1 上预训练的模型,您可以在任何人脸图像上运行它,以获取 128 维特征向量。我们在环境设置部分下载了此文件,它位于/pre-model/Resnet-185253.pb目录中。
如果您想自己尝试这个步骤,OpenFace 提供了一个 Lua 脚本(github.com/cmusatyalab/openface/blob/master/batch-represent/batch-represent.lua),该脚本会生成文件夹中所有图像的嵌入,并将它们写入 CSV 文件。
创建输入图像嵌入的代码可以在段落后找到。该代码可在仓库中找到:github.com/PacktPublishing/Python-Deep-Learning-Projects/blob/master/Chapter10/facenet/train_classifier.py。
在这个过程中,我们从 Resnet 模型加载了训练好的组件,如embedding_layer、images_placeholder和phase_train_placeholder,以及图像和标签:
def _create_embeddings(embedding_layer, images, labels, images_placeholder, phase_train_placeholder, sess):
"""
Uses model to generate embeddings from :param images.
:param embedding_layer:
:param images:
:param labels:
:param images_placeholder:
:param phase_train_placeholder:
:param sess:
:return: (tuple): image embeddings and labels
"""
emb_array = None
label_array = None
try:
i = 0
while True:
batch_images, batch_labels = sess.run([images, labels])
logger.info('Processing iteration {} batch of size: {}'.format(i, len(batch_labels)))
emb = sess.run(embedding_layer,
feed_dict={images_placeholder: batch_images, phase_train_placeholder: False})
emb_array = np.concatenate([emb_array, emb]) if emb_array is not None else emb
label_array = np.concatenate([label_array, batch_labels]) if label_array is not None else batch_labels
i += 1
except tf.errors.OutOfRangeError:
pass
return emb_array, label_array
这是嵌入创建过程的快速概览。我们将图像和标签数据以及来自预训练模型的几个组件一起输入:

该过程的输出将是一个 128 维的向量,表示人脸图像。
在 Docker 上执行
我们将在 Docker 镜像上实现预处理。我们将通过-v标志将project目录挂载为 Docker 容器中的一个卷,并在输入数据上运行预处理脚本。结果将写入通过命令行参数指定的目录。
align_dlib.py文件来自 CMU。它提供了检测图像中的人脸、查找面部特征点并对齐这些特征点的方法:
docker run -v $PWD:/facerecognition \
-e PYTHONPATH=$PYTHONPATH:/facerecognition \
-it hellorahulk/facerecognition python3 /facerecognition/facenet/preprocess.py \
--input-dir /facerecognition/data \
--output-dir /facerecognition/output/intermediate \
--crop-dim 180
在前面的命令中,我们通过--input-dir标志设置了输入数据路径。该目录应包含我们要处理的图像。
我们还使用--output-dir标志设置了输出路径,存储分割对齐的图像。我们将使用这些输出图像作为训练输入。
--crop-dim标志用于定义图像的输出尺寸。在这种情况下,所有图像将被存储为 180 × 180。
该过程的结果将在/output文件夹内创建一个/intermediate文件夹,其中包含所有预处理过的图像。
训练分类器
首先,我们将从input目录(--input-dir标志)加载已分割并对齐的图像。在训练过程中,我们将对图像进行预处理。此预处理将向图像添加随机变换,从而生成更多的图像用于训练。
这些图像将以 128 的批量大小输入到预训练模型中。该模型将为每张图像返回一个 128 维的嵌入,为每个批次返回一个 128 x 128 的矩阵。
在创建这些嵌入后,我们将使用它们作为特征输入到 scikit-learn 的 SVM 分类器中,进行每个身份的训练。
以下命令将启动该过程并训练分类器。分类器将作为pickle文件保存在--classifier-path参数定义的路径中:
docker run -v $PWD:/facerecognition \
-e PYTHONPATH=$PYTHONPATH:/facerecognition \
-it hellorahulk/facerecognition \
python3 /facerecognition/facenet/train_classifier.py \
--input-dir /facerecognition/output/intermediate \
--model-path /facerecognition/pre-model/Resnet-185253.pb \
--classifier-path /facerecognition/output/classifier.pkl \
--num-threads 16 \
--num-epochs 25 \
--min-num-images-per-class 10 \
--is-train
一些自定义参数可以调整:
-
--num-threads:根据 CPU/GPU 配置进行修改 -
--num-epochs:根据你的数据集进行更改 -
--min-num-images-per-class:根据你的数据集进行更改 -
--is-train:设置为True标志表示训练
这个过程可能需要一段时间,具体取决于你用于训练的图像数量。完成后,你会在/output文件夹内找到一个classifier.pkl文件。
现在,你可以使用classifier.pkl文件进行预测,并将其部署到生产环境中。
评估
我们将评估训练好的模型的性能。为此,我们将执行以下命令:
docker run -v $PWD:/facerecognition \
-e PYTHONPATH=$PYTHONPATH:/facerecognition \
-it hellorahulk/facerecognition \
python3 /facerecognition/facenet/train_classifier.py \
--input-dir /facerecognition/output/intermediate \
--model-path /facerecognition/pre-model/Resnet-185253.pb \
--classifier-path /facerecognition/output/classifier.pkl \
--num-threads 16 \
--num-epochs 2 \
--min-num-images-per-class 10 \
执行完成后,你将看到带有置信度分数的预测结果,如下图所示:

我们可以看到,模型能够以 99.5%的准确度进行预测,并且预测速度相对较快。
总结
我们成功完成了一个世界级的人脸识别概念验证(POC)项目,利用 OpenFace、dlib 和 FaceNet 的深度学习技术,服务于我们假设的高性能数据中心。
我们构建了一个包含以下内容的流水线:
-
人脸检测:检查图像并找到其中包含的所有人脸
-
人脸提取:集中关注每张人脸并了解其基本特征
-
特征提取:通过卷积神经网络(CNN)从人脸中提取独特特征
-
分类器训练:将这些独特特征与所有已知的人进行比较,并确定该人的名字
强大的面部识别系统为访问控制提供的安全等级,符合这一 Tier III 设施所要求的高标准。这个项目是深度学习强大能力的一个极佳例子,能够为我们客户的业务运营带来有意义的影响。
第十一章:自动图像描述
在上一章中,我们了解了如何构建物体检测和分类模型,这非常令人兴奋。但在这一章中,我们将做一些更令人印象深刻的事情,结合计算机视觉和自然语言处理的当前最先进技术,形成一个完整的图像描述方法(www.cs.cmu.edu/~afarhadi/papers/sentence.pdf)。这将负责构建任何提供图像的计算机生成的自然描述。
我们的团队被要求构建这个模型,以生成自然语言的图像描述,用作一家公司的核心智能,该公司希望帮助视障人士利用网络上照片分享的爆炸式增长。想到这项深度学习技术可能具备有效地为这一社区带来图像内容的能力,令人兴奋。可能会喜欢我们工作的成果的人群包括从出生时就视力受损的人到老年人群体。这些用户类型及更多人群可以使用基于本项目模型的图像描述机器人,从而了解发布的图像内容,举个例子,他们可以跟上家人的动态。
考虑到这一点,我们来看一下我们需要做的深度学习工程。这个想法是用一个经过训练的深度卷积神经网络(CNN)替换编码器-解码器架构中的编码器(RNN 层),该 CNN 用于分类图像中的物体。
通常,CNN 的最后一层是 softmax 层,它为每个物体分配该物体在图像中出现的概率。但如果我们将 CNN 中的 softmax 层去除,我们可以将 CNN 对图像的丰富编码传递给解码器(语言生成 RNN),该解码器设计用于生成短语。然后,我们可以直接在图像及其描述上训练整个系统,这样可以最大化生成的描述与每个图像的训练描述最佳匹配的可能性。
这是自动图像描述模型的小示意图。在左上角是编码器-解码器架构,用于序列到序列的模型,并与物体检测模型结合,如下图所示:

在此实现中,我们将使用预训练的 Inception-v3 模型作为特征提取器,在 ImageNet 数据集上训练一个编码器。
数据准备
让我们导入构建自动描述模型所需的所有依赖项。
本章的所有 Python 文件和 Jupyter Notebook 可以在github.com/PacktPublishing/Python-Deep-Learning-Projects/tree/master/Chapter11找到。
初始化
对于此实现,我们需要 TensorFlow 版本大于或等于 1.9,并且我们还将启用即时执行模式(www.tensorflow.org/guide/eager),这将帮助我们更有效地调试代码。以下是代码:
# Import TensorFlow and enable eager execution
import tensorflow as tf
tf.enable_eager_execution()
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.utils import shuffle
import re
import numpy as np
import os
import time
import json
from glob import glob
from PIL import Image
import pickle
下载并准备 MS-COCO 数据集
我们将使用 MS-COCO 数据集 (cocodataset.org/#home) 来训练我们的模型。该数据集包含超过 82,000 张图片,每张图片都至少有五个不同的标题注释。以下代码将自动下载并提取数据集:
annotation_zip = tf.keras.utils.get_file('captions.zip',
cache_subdir=os.path.abspath('.'),
origin = 'http://images.cocodataset.org/annotations/annotations_trainval2014.zip',
extract = True)
annotation_file = os.path.dirname(annotation_zip)+'/annotations/captions_train2014.json'
name_of_zip = 'train2014.zip'
if not os.path.exists(os.path.abspath('.') + '/' + name_of_zip):
image_zip = tf.keras.utils.get_file(name_of_zip,
cache_subdir=os.path.abspath('.'),
origin = 'http://images.cocodataset.org/zips/train2014.zip',
extract = True)
PATH = os.path.dirname(image_zip)+'/train2014/'
else:
PATH = os.path.abspath('.')+'/train2014/'
这将涉及一个大的下载过程。我们将使用训练集,它是一个 13 GB 的文件。
以下是输出结果:
Downloading data from http://images.cocodataset.org/annotations/annotations_trainval2014.zip
252878848/252872794 [==============================] - 6s 0us/step
Downloading data from http://images.cocodataset.org/zips/train2014.zip
13510574080/13510573713 [==============================] - 322s 0us/step
在这个示例中,我们将选择 40,000 个标题的子集,并使用这些标题及其对应的图片来训练模型。和往常一样,如果选择使用更多数据,标题质量会得到提升:
# read the json annotation file
with open(annotation_file, 'r') as f:
annotations = json.load(f)
# storing the captions and the image name in vectors
all_captions = []
all_img_name_vector = []
for annot in annotations['annotations']:
caption = '<start> ' + annot['caption'] + ' <end>'
image_id = annot['image_id']
full_coco_image_path = PATH + 'COCO_train2014_' + '%012d.jpg' % (image_id)
all_img_name_vector.append(full_coco_image_path)
all_captions.append(caption)
# shuffling the captions and image_names together
# setting a random state
train_captions, img_name_vector = shuffle(all_captions,
all_img_name_vector,
random_state=1)
# selecting the first 40000 captions from the shuffled set
num_examples = 40000
train_captions = train_captions[:num_examples]
img_name_vector = img_name_vector[:num_examples]
数据准备完成后,我们将把所有图片路径存储在 img_name_vector 列表变量中,相关的标题存储在 train_caption 中,如下图所示:

深度 CNN 编码器的数据准备
接下来,我们将使用预训练的 Inception-v3(在 ImageNet 上训练)来对每张图片进行分类。我们将从最后一个卷积层提取特征。我们将创建一个辅助函数,将输入图像转换为 Inception-v3 期望的格式:
#Resizing the image to (299, 299)
#Using the preprocess_input method to place the pixels in the range of -1 to 1.
def load_image(image_path):
img = tf.read_file(image_path)
img = tf.image.decode_jpeg(img, channels=3)
img = tf.image.resize_images(img, (299, 299))
img = tf.keras.applications.inception_v3.preprocess_input(img)
return img, image_path
现在让我们初始化 Inception-v3 模型,并加载预训练的 ImageNet 权重。为此,我们将创建一个 tf.keras 模型,其中输出层是 Inception-v3 架构中的最后一个卷积层。
在创建 keras 模型时,你可以看到一个名为 include_top=False 的参数,它表示是否包括网络顶部的全连接层:
image_model = tf.keras.applications.InceptionV3(include_top=False,
weights='imagenet')
new_input = image_model.input
hidden_layer = image_model.layers[-1].output
image_features_extract_model = tf.keras.Model(new_input, hidden_layer)
输出结果如下:
Downloading data from https://github.com/fchollet/deep-learning-models/releases/download/v0.5/inception_v3_weights_tf_dim_ordering_tf_kernels_notop.h5
87916544/87910968 [==============================] - 40s 0us/step
所以,image_features_extract_model 是我们的深度 CNN 编码器,它负责从给定的图像中学习特征。
执行特征提取
现在我们将使用深度 CNN 编码器对每张图片进行预处理,并将输出保存到磁盘:
-
我们将使用之前创建的
load_image()辅助函数按批加载图像 -
我们将把图像输入编码器以提取特征
-
将特征作为
numpy数组输出:
encode_train = sorted(set(img_name_vector))
#Load images
image_dataset = tf.data.Dataset.from_tensor_slices(
encode_train).map(load_image).batch(16)
# Extract features
for img, path in image_dataset:
batch_features = image_features_extract_model(img)
batch_features = tf.reshape(batch_features,
(batch_features.shape[0], -1, batch_features.shape[3]))
#Dump into disk
for bf, p in zip(batch_features, path):
path_of_feature = p.numpy().decode("utf-8")
np.save(path_of_feature, bf.numpy())
语言生成(RNN)解码器的数据准备
第一步是对标题进行预处理。
我们将对标题执行一些基本的预处理步骤,例如以下操作:
-
首先,我们将对标题进行分词(例如,通过空格拆分)。这将帮助我们构建一个包含数据中所有唯一单词的词汇表(例如,“playing”,“football”等)。
-
接下来,我们将把词汇表大小限制为前 5,000 个单词以节省内存。我们将用
unk(表示未知)替换所有其他单词。你显然可以根据使用场景进行优化。 -
最后,我们将创建一个单词到索引的映射以及反向映射。
-
然后,我们将对所有序列进行填充,使其长度与最长的序列相同。
这里是相关代码:
# Helper func to find the maximum length of any caption in our dataset
def calc_max_length(tensor):
return max(len(t) for t in tensor)
# Performing tokenization on the top 5000 words from the vocabulary
top_k = 5000
tokenizer = tf.keras.preprocessing.text.Tokenizer(num_words=top_k,
oov_token="<unk>",
filters='!"#$%&()*+.,-/:;=?@[\]^_`{|}~ ')
# Converting text into sequence of numbers
tokenizer.fit_on_texts(train_captions)
train_seqs = tokenizer.texts_to_sequences(train_captions)
tokenizer.word_index = {key:value for key, value in tokenizer.word_index.items() if value <= top_k}
# putting <unk> token in the word2idx dictionary
tokenizer.word_index[tokenizer.oov_token] = top_k + 1
tokenizer.word_index['<pad>'] = 0
# creating the tokenized vectors
train_seqs = tokenizer.texts_to_sequences(train_captions)
# creating a reverse mapping (index -> word)
index_word = {value:key for key, value in tokenizer.word_index.items()}
# padding each vector to the max_length of the captions
cap_vector = tf.keras.preprocessing.sequence.pad_sequences(train_seqs, padding='post')
# calculating the max_length
# used to store the attention weights
max_length = calc_max_length(train_seqs)
所以,最终的结果将是一个整数序列数组,如下图所示:

现在,我们将使用 80:20 的比例将数据分为训练样本和验证样本:
img_name_train, img_name_val, cap_train, cap_val = train_test_split(img_name_vector,cap_vector,test_size=0.2,random_state=0)
# Checking the sample counts
print ("No of Training Images:",len(img_name_train))
print ("No of Training Caption: ",len(cap_train) )
print ("No of Training Images",len(img_name_val))
print ("No of Training Caption:",len(cap_val) )
No of Training Images: 24000
No of Training Caption: 24000
No of Training Images 6000
No of Training Caption: 6000
设置数据管道
我们的图像和标题已准备好!接下来,让我们创建一个tf.data数据集(www.tensorflow.org/api_docs/python/tf/data/Dataset)用于训练我们的模型。现在,我们将通过对它们进行转换和批处理来为图像和文本模型准备管道:
# Defining parameters
BATCH_SIZE = 64
BUFFER_SIZE = 1000
embedding_dim = 256
units = 512
vocab_size = len(tokenizer.word_index)
# shape of the vector extracted from Inception-V3 is (64, 2048)
# these two variables represent that
features_shape = 2048
attention_features_shape = 64
# loading the numpy files
def map_func(img_name, cap):
img_tensor = np.load(img_name.decode('utf-8')+'.npy')
return img_tensor, cap
#We use the from_tensor_slices to load the raw data and transform them into the tensors
dataset = tf.data.Dataset.from_tensor_slices((img_name_train, cap_train))
# Using the map() to load the numpy files in parallel
# NOTE: Make sure to set num_parallel_calls to the number of CPU cores you have
# https://www.tensorflow.org/api_docs/python/tf/py_func
dataset = dataset.map(lambda item1, item2: tf.py_func(
map_func, [item1, item2], [tf.float32, tf.int32]), num_parallel_calls=8)
# shuffling and batching
dataset = dataset.shuffle(BUFFER_SIZE)
dataset = dataset.batch(BATCH_SIZE)
dataset = dataset.prefetch(1)
定义字幕生成模型
我们用来构建自动字幕的模型架构灵感来自于Show, Attend and Tell论文(arxiv.org/pdf/1502.03044.pdf)。我们从 Inception-v3 的低层卷积层提取的特征给我们一个形状为(8, 8, 2048)的向量。然后,我们将其压缩成(64, 2048)的形状。
然后,这个向量通过 CNN 编码器传递,该编码器由一个单一的全连接层组成。RNN(在我们的案例中是 GRU)会关注图像来预测下一个词:
def gru(units):
if tf.test.is_gpu_available():
return tf.keras.layers.CuDNNGRU(units,
return_sequences=True,
return_state=True,
recurrent_initializer='glorot_uniform')
else:
return tf.keras.layers.GRU(units,
return_sequences=True,
return_state=True,
recurrent_activation='sigmoid',
recurrent_initializer='glorot_uniform')
注意
现在,我们将定义广为人知的注意力机制——巴赫达诺注意力(Bahdanau Attention)(arxiv.org/pdf/1409.0473.pdf)。我们将需要来自 CNN 编码器的特征,形状为(batch_size,64,embedding_dim)。该注意力机制将返回上下文向量和时间轴上的注意力权重:
class BahdanauAttention(tf.keras.Model):
def __init__(self, units):
super(BahdanauAttention, self).__init__()
self.W1 = tf.keras.layers.Dense(units)
self.W2 = tf.keras.layers.Dense(units)
self.V = tf.keras.layers.Dense(1)
def call(self, features, hidden):
# hidden_with_time_axis shape == (batch_size, 1, hidden_size)
hidden_with_time_axis = tf.expand_dims(hidden, 1)
# score shape == (batch_size, 64, hidden_size)
score = tf.nn.tanh(self.W1(features) + self.W2(hidden_with_time_axis))
# attention_weights shape == (batch_size, 64, 1)
# we get 1 at the last axis because we are applying score to self.V
attention_weights = tf.nn.softmax(self.V(score), axis=1)
# context_vector shape after sum == (batch_size, hidden_size)
context_vector = attention_weights * features
context_vector = tf.reduce_sum(context_vector, axis=1)
return context_vector, attention_weights
CNN 编码器
现在让我们定义 CNN 编码器,它将是一个单一的全连接层,后面跟着 ReLU 激活函数:
class CNN_Encoder(tf.keras.Model):
# Since we have already extracted the features and dumped it using pickle
# This encoder passes those features through a Fully connected layer
def __init__(self, embedding_dim):
super(CNN_Encoder, self).__init__()
# shape after fc == (batch_size, 64, embedding_dim)
self.fc = tf.keras.layers.Dense(embedding_dim)
def call(self, x):
x = self.fc(x)
x = tf.nn.relu(x)
return x
RNN 解码器
在这里,我们将定义 RNN 解码器,它将接受来自编码器的编码特征。这些特征被输入到注意力层,与输入的嵌入向量连接。然后,连接后的向量被传递到 GRU 模块,进一步通过两个全连接层:
class RNN_Decoder(tf.keras.Model):
def __init__(self, embedding_dim, units, vocab_size):
super(RNN_Decoder, self).__init__()
self.units = units
self.embedding = tf.keras.layers.Embedding(vocab_size, embedding_dim)
self.gru = gru(self.units)
self.fc1 = tf.keras.layers.Dense(self.units)
self.fc2 = tf.keras.layers.Dense(vocab_size)
self.attention = BahdanauAttention(self.units)
def call(self, x, features, hidden):
# defining attention as a separate model
context_vector, attention_weights = self.attention(features, hidden)
# x shape after passing through embedding == (batch_size, 1, embedding_dim)
x = self.embedding(x)
# x shape after concatenation == (batch_size, 1, embedding_dim + hidden_size)
x = tf.concat([tf.expand_dims(context_vector, 1), x], axis=-1)
# passing the concatenated vector to the GRU
output, state = self.gru(x)
# shape == (batch_size, max_length, hidden_size)
x = self.fc1(output)
# x shape == (batch_size * max_length, hidden_size)
x = tf.reshape(x, (-1, x.shape[2]))
# output shape == (batch_size * max_length, vocab)
x = self.fc2(x)
return x, state, attention_weights
def reset_state(self, batch_size):
return tf.zeros((batch_size, self.units))
encoder = CNN_Encoder(embedding_dim)
decoder = RNN_Decoder(embedding_dim, units, vocab_size)
损失函数
我们正在使用Adam优化器来训练模型,并对为<PAD>键计算的损失进行屏蔽:
optimizer = tf.train.AdamOptimizer()
# We are masking the loss calculated for padding
def loss_function(real, pred):
mask = 1 - np.equal(real, 0)
loss_ = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=real, logits=pred) * mask
return tf.reduce_mean(loss_)
训练字幕生成模型
现在,让我们训练模型。我们需要做的第一件事是提取存储在相应.npy文件中的特征,然后将这些特征通过 CNN 编码器传递。
编码器的输出、隐藏状态(初始化为 0)以及解码器输入(即起始标记)将传递给解码器。解码器返回预测结果和解码器的隐藏状态。
然后,解码器的隐藏状态会被传回模型中,预测结果将用于计算损失。在训练过程中,我们使用教师强迫技术来决定解码器的下一个输入。
教师强迫是一种技术,将目标词作为下一个输入传递给解码器。这种技术有助于快速学习正确的序列或序列的正确统计特性。
最后一步是计算梯度并将其应用于优化器,然后进行反向传播:
EPOCHS = 20
loss_plot = []
for epoch in range(EPOCHS):
start = time.time()
total_loss = 0
for (batch, (img_tensor, target)) in enumerate(dataset):
loss = 0
# initializing the hidden state for each batch
# because the captions are not related from image to image
hidden = decoder.reset_state(batch_size=target.shape[0])
dec_input = tf.expand_dims([tokenizer.word_index['<start>']] * BATCH_SIZE, 1)
with tf.GradientTape() as tape:
features = encoder(img_tensor)
for i in range(1, target.shape[1]):
# passing the features through the decoder
predictions, hidden, _ = decoder(dec_input, features, hidden)
loss += loss_function(target[:, i], predictions)
# using teacher forcing
dec_input = tf.expand_dims(target[:, i], 1)
total_loss += (loss / int(target.shape[1]))
variables = encoder.variables + decoder.variables
gradients = tape.gradient(loss, variables)
optimizer.apply_gradients(zip(gradients, variables), tf.train.get_or_create_global_step())
if batch % 100 == 0:
print ('Epoch {} Batch {} Loss {:.4f}'.format(epoch + 1,
batch,
loss.numpy() / int(target.shape[1])))
# storing the epoch end loss value to plot later
loss_plot.append(total_loss / len(cap_vector))
print ('Epoch {} Loss {:.6f}'.format(epoch + 1,
total_loss/len(cap_vector)))
print ('Time taken for 1 epoch {} sec\n'.format(time.time() - start))
以下是输出:

在执行了几次训练迭代后,让我们绘制Epoch与Loss的图表:
plt.plot(loss_plot)
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.title('Loss Plot')
plt.show()
输出如下:

训练过程中的损失与迭代次数图
评估字幕生成模型
评估函数与训练循环类似,唯一的不同是我们这里不使用教师强迫。每次时间步解码器的输入是它之前的预测结果、隐藏状态以及编码器的输出。
做预测时需要记住的几个要点:
-
当模型预测结束标记时,停止预测
-
存储每个时间步的注意力权重
让我们定义evaluate()函数:
def evaluate(image):
attention_plot = np.zeros((max_length, attention_features_shape))
hidden = decoder.reset_state(batch_size=1)
temp_input = tf.expand_dims(load_image(image)[0], 0)
img_tensor_val = image_features_extract_model(temp_input)
img_tensor_val = tf.reshape(img_tensor_val, (img_tensor_val.shape[0], -1, img_tensor_val.shape[3]))
features = encoder(img_tensor_val)
dec_input = tf.expand_dims([tokenizer.word_index['<start>']], 0)
result = []
for i in range(max_length):
predictions, hidden, attention_weights = decoder(dec_input, features, hidden)
attention_plot[i] = tf.reshape(attention_weights, (-1, )).numpy()
predicted_id = tf.argmax(predictions[0]).numpy()
result.append(index_word[predicted_id])
if index_word[predicted_id] == '<end>':
return result, attention_plot
dec_input = tf.expand_dims([predicted_id], 0)
attention_plot = attention_plot[:len(result), :]
return result, attention_plot
此外,让我们创建一个helper函数来可视化预测单词的注意力点:
def plot_attention(image, result, attention_plot):
temp_image = np.array(Image.open(image))
fig = plt.figure(figsize=(10, 10))
len_result = len(result)
for l in range(len_result):
temp_att = np.resize(attention_plot[l], (8, 8))
ax = fig.add_subplot(len_result//2, len_result//2, l+1)
ax.set_title(result[l])
img = ax.imshow(temp_image)
ax.imshow(temp_att, cmap='gray', alpha=0.6, extent=img.get_extent())
plt.tight_layout()
plt.show()
# captions on the validation set
rid = np.random.randint(0, len(img_name_val))
image = img_name_val[rid]
real_caption = ' '.join([index_word[i] for i in cap_val[rid] if i not in [0]])
result, attention_plot = evaluate(image)
print ('Real Caption:', real_caption)
print ('Prediction Caption:', ' '.join(result))
plot_attention(image, result, attention_plot)
# opening the image
Image.open(img_name_val[rid])
输出如下:


部署字幕生成模型
现在,让我们将整个模块部署为 RESTful 服务。为此,我们将编写一个推理代码,加载最新的检查点,并对给定的图像进行预测。
查看仓库中的inference.py文件。所有代码与训练循环相似,唯一的不同是我们这里不使用教师强迫。每次时间步解码器的输入是它之前的预测结果、隐藏状态以及编码器的输出。
一个重要部分是将模型加载到内存中,我们使用tf.train.Checkpoint()方法来加载所有学习到的权重,包括optimizer、encoder、decoder,并将它们加载到内存中。以下是相应的代码:
checkpoint_dir = './my_model'
checkpoint_prefix = os.path.join(checkpoint_dir, "ckpt")
checkpoint = tf.train.Checkpoint(
optimizer=optimizer,
encoder=encoder,
decoder=decoder,
)
checkpoint.restore(tf.train.latest_checkpoint(checkpoint_dir))
因此,我们将创建一个evaluate()函数,该函数定义了预测循环。为了确保预测在某些词语后停止,我们将在模型预测结束标记<end>时停止预测:
def evaluate(image):
attention_plot = np.zeros((max_length, attention_features_shape))
hidden = decoder.reset_state(batch_size=1)
temp_input = tf.expand_dims(load_image(image)[0], 0)
# Extract features from the test image
img_tensor_val = image_features_extract_model(temp_input)
img_tensor_val = tf.reshape(img_tensor_val, (img_tensor_val.shape[0], -1, img_tensor_val.shape[3]))
# Feature is fed into the encoder
features = encoder(img_tensor_val)
dec_input = tf.expand_dims([tokenizer.word_index['<start>']], 0)
result = []
# Prediction loop
for i in range(max_length):
predictions, hidden, attention_weights = decoder(dec_input, features, hidden)
attention_plot[i] = tf.reshape(attention_weights, (-1, )).numpy()
predicted_id = tf.argmax(predictions[0]).numpy()
result.append(index_word[predicted_id])
# Hard stop when end token is predicted
if index_word[predicted_id] == '<end>':
return result, attention_plot
dec_input = tf.expand_dims([predicted_id], 0)
attention_plot = attention_plot[:len(result), :]
return result, attention_plot
现在让我们在 Web 应用程序代码中使用这个evaluate()函数:
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
"""
@author: rahulkumar
"""
from flask import Flask , request, jsonify
import time
from inference import evaluate
import tensorflow as tf
app = Flask(__name__)
@app.route("/wowme")
def AutoImageCaption():
image_url=request.args.get('image')
print('image_url')
image_extension = image_url[-4:]
image_path = tf.keras.utils.get_file(str(int(time.time()))+image_extension, origin=image_url)
result, attention_plot = evaluate(image_path)
data = {'Prediction Caption:': ' '.join(result)}
return jsonify(data)
if __name__ == "__main__":
app.run(host = '0.0.0.0',port=8081)
在终端执行以下命令来运行 Web 应用程序:
python caption_deploy_api.py
你应该会得到以下输出:
* Running on http://0.0.0.0:8081/ (Press CTRL+C to quit)
现在我们请求 API,如下所示:
curl 0.0.0.0:8081/wowme?image=https://www.beautifulpeopleibiza.com/images/BPI/img_bpi_destacada.jpg
我们应该能得到预测的字幕,如下图所示:

确保在大图像上训练模型,以获得更好的预测效果。
哇!我们刚刚部署了最先进的自动字幕生成模块。
总结
在这个实现中,我们使用了一个预训练的 Inception-v3 模型作为特征提取器,并将其作为编码器的一部分,在 ImageNet 数据集上进行训练,作为深度学习解决方案的一部分。该解决方案结合了目前最先进的计算机视觉和自然语言处理技术,形成了一个完整的图像描述方法(www.cs.cmu.edu/~afarhadi/papers/sentence.pdf),能够为任何提供的图像构建计算机生成的自然描述。通过这个训练好的模型,我们有效地打破了图像与语言之间的障碍,并提供了一项技术,可以作为应用程序的一部分,帮助视障人士享受照片分享这一大趋势带来的益处!伟大的工作!
第十二章:使用卷积神经网络(ConvNets)进行 3D 模型的姿势估计
欢迎来到我们关于人体姿势估计的章节。在本章中,我们将构建一个神经网络,该网络将使用 2D 图像预测 3D 人体姿势。我们将借助迁移学习,使用 VGG16 模型架构,并根据当前问题对其进行修改。到本章结束时,你将拥有一个深度学习(DL)模型,它能够很好地预测人体姿势。
电影中的视觉效果(VFX)成本高昂。它们涉及使用大量昂贵的传感器,这些传感器将在拍摄时安装在演员的身体上。然后,来自这些传感器的信息将用于构建视觉效果,所有这些都最终变得非常昂贵。在这个假设的案例中,我们被一家大型电影制片厂问是否可以通过构建一个人体姿势估算器来帮助他们的图形部门构建更便宜、更好的视觉效果,他们将使用该估算器来在编辑时更好地估计屏幕上的姿势。
对于本任务,我们将使用来自电影标签框架(FLIC)的图像。这些图像尚未准备好用于建模。所以,请准备好在本章花费更多时间来准备图像数据。此外,我们将只估算手臂、肩膀和头部的姿势。
在本章中,我们将学习以下主题:
-
为姿势估计处理/准备图像
-
VGG16 模型
-
迁移学习
-
构建并理解训练循环
-
测试模型
如果你在本章中逐步实现代码片段,最好使用 Jupyter Notebook 或任何源代码编辑器。这将使你更容易跟上进度,并理解代码的每一部分的作用。
本章的所有 Python 文件和 Jupyter 笔记本可以在github.com/PacktPublishing/Python-Deep-Learning-Projects/tree/master/Chapter12找到。
代码实现
在这个练习中,我们将使用 Keras 深度学习库,它是一个高级神经网络 API,能够在 TensorFlow、Theano 和 CNTK 之上运行。
如果你有任何关于 Keras 的问题,请参考这个易于理解的 Keras 文档:keras.io。
在继续本章内容之前,请从 GitHub 下载Chapter12文件夹。
本项目涉及从多个来源下载文件,这些文件将在脚本中调用。为了确保 Python 脚本或 Jupyter Notebook 能够正确定位下载的文件,请按照以下步骤操作:
-
打开终端并使用
cd命令切换到Chapter12文件夹。 -
使用以下命令下载
FLIC-full数据文件:
wget http://vision.grasp.upenn.edu/video/FLIC-full.zip
- 使用以下命令解压 ZIP 文件:
unzip FLIC-full.zip
- 使用以下命令删除 ZIP 文件:
rm -rf FLIC-full.zip
- 使用以下命令在
FLIC-full文件夹中切换目录:
cd FLIC-full
- 下载包含训练索引的文件:
wget http://cims.nyu.edu/~tompson/data/tr_plus_indices.mat
-
将目录切换回
Chapter12文件夹。 -
启动你的 Jupyter Notebook 或从
Chapter12目录运行 Python 脚本。
你可以在bensapp.github.io/flic-dataset.html找到关于FLIC-full数据文件夹的更多信息。
导入依赖包
本次练习中我们将使用numpy、matplotlib、keras、tensorflow和tqdm包。在这里,TensorFlow 作为 Keras 的后端。你可以通过pip安装这些包。对于 MNIST 数据,我们将使用keras模块中提供的数据集,使用简单的import即可:
import matplotlib.pyplot as plt
%matplotlib inline
import os
import random
import glob
import h5py
from scipy.io import loadmat
import numpy as np
import pandas as pd
import cv2 as cv
from __future__ import print_function
from sklearn.model_selection import train_test_split
from keras.models import Sequential, Model
from keras.layers.core import Flatten, Dense, Dropout
from keras.optimizers import Adam
from keras import backend as K
from keras import applications
K.clear_session()
为了可重复性,请务必设置seed:
# set seed for reproducibility
seed_val = 9000
np.random.seed(seed_val)
random.seed(seed_val)
探索和预处理数据
下载并解压完FLIC-full数据文件夹后,你应该能在FLIC-full文件夹中找到tr_plus_indices.mat和examples.mat MATLAB 文件,并且还有一个名为images的文件夹,里面是将用于本项目的图像。
你会发现,这些图像是从电影如速度与激情 2、与波莉同行、美国婚礼等中截取的。每张图像的大小为 480*720 像素。这些图像只是选定电影中演员场景的截图,我们将使用它们进行姿势估计。
让我们加载 MATLAB 文件examples.mat。我们将借助已经导入的loadmat模块来完成这项操作,并打印出文件中的一些信息:
# load the examples file
examples = loadmat('FLIC-full/examples.mat')
# print type of the loaded file
print('examples variable is of', type(examples))
# print keys in the dictionary examples
print('keys in the dictionary examples:\n', examples.keys())ut
以下是输出:

图 12.1:来自打印输出 1 的示例文件信息
从打印输出中,我们可以看到该 MATLAB 文件已作为字典加载,其中包含四个键,其中一个是我们需要的:examples键。我们来看一下这个键的内容:
# print type and shape of values in examples key
print('Shape of value in examples key: ',examples['examples'].shape)
# print examples
print('Type: ',type(examples['examples']))
# reshape the examples array
examples = examples['examples'].reshape(-1,)
# print shape of examples array
print("Shape of reshaped 'examples' array:", examples.shape)
以下是输出:

图 12.2:来自打印输出 1 的示例文件信息
这里需要注意的是,examples键的值是一个形状为(1, 20928)的 numpy 数组。你还会看到,这个数组已经被重塑为形状(20928,)。examples键包含的是图像(在images文件夹中)的 ID 及其对应的姿势坐标,可用于建模。
让我们打印出一个图像 ID 及其对应的坐标数组及其形状。我们需要的图像 ID 存储在索引3中,相关的坐标存储在索引2中。我们来打印这些出来:
print('Coordinates at location/index 3 of example 0:\n' ,examples[0][2].T)
print('\n Data type in which the coordinates are stored: ',type(examples[0][2]))
print('\n Shape of the coordinates:', examples[0][2].shape)
print('\n Name of the image file the above coordinates correspond to :\n ',examples[0][3][0])
以下是输出:

图 12.3:来自打印输出 2 的示例文件信息
从之前的截图中,我们可以看到坐标数组的形状是(2,29):
# each coordinate corresponds to the the below listed body joints/locations and in the same order
joint_labels = ['lsho', 'lelb', 'lwri', 'rsho', 'relb', 'rwri', 'lhip',
'lkne', 'lank', 'rhip', 'rkne', 'rank', 'leye', 'reye',
'lear', 'rear', 'nose', 'msho', 'mhip', 'mear', 'mtorso',
'mluarm', 'mruarm', 'mllarm', 'mrlarm', 'mluleg', 'mruleg',
'mllleg', 'mrlleg']
# print joint_labels
print(joint_labels)
以下是输出:

图 12.4:关节标签列表
但是,如果你回顾前面的截图中我们打印的坐标数组,在 29 个坐标中,我们只得到了 11 个身体关节/位置的信息。具体如下:
# print list of known joints
known_joints = [x for i,x in enumerate(joint_labels) if i in np.r_[0:7, 9, 12:14, 16]]
print(known_joints)
以下是输出结果:

图 12.5:带坐标的关节标签列表
对于本项目,我们只需要以下身体关节/位置的信息:
# print needed joints for the task
target_joints = ['lsho', 'lelb', 'lwri', 'rsho', 'relb',
'rwri', 'leye', 'reye', 'nose']
print('Joints necessary for the project:\n', target_joints)
# print the indices of the needed joints in the coordinates array
joints_loc_id = np.r_[0:6, 12:14, 16]
print('\nIndices of joints necessary for the project:\n',joints_loc_id)
以下是输出结果:

图 12.6:数组中所需的关节及其索引
lsho:左肩
lelb:左肘
lwri:左手腕
rsho:右肩
relb:右肘
rwri:右手腕
leye:左眼
reye:右眼
nose:鼻子
现在,让我们定义一个函数,该函数接受包含九个关节标签和坐标的字典,并返回一个包含七个坐标(7(x,y)对)的列表。七个坐标的原因是,当我们对leye、reye和nose坐标取平均时,它们会合并成一个头部坐标:
def joint_coordinates(joint):
"""Store necessary coordinates to a list"""
joint_coor = []
# Take mean of the leye, reye, nose to obtain coordinates for the head
joint['head'] = (joint['leye']+joint['reye']+joint['nose'])/3
joint_coor.extend(joint['lwri'].tolist())
joint_coor.extend(joint['lelb'].tolist())
joint_coor.extend(joint['lsho'].tolist())
joint_coor.extend(joint['head'].tolist())
joint_coor.extend(joint['rsho'].tolist())
joint_coor.extend(joint['relb'].tolist())
joint_coor.extend(joint['rwri'].tolist())
return joint_coor
现在让我们加载tr_plus_indices.mat MATLAB 文件,就像我们之前做的那样:
我们需要使用tr_plus_indices.mat文件的原因是,它包含了仅应用于训练的图像的索引,以及一些未列出的用于测试的图像。这样的划分目的是确保训练集和测试集来自完全不同的电影片段,从而避免过拟合。有关更多信息,请访问 bensapp.github.io/flic-dataset.html。
# load the indices matlab file
train_indices = loadmat('FLIC-full/tr_plus_indices.mat')
# print type of the loaded file
print('train_indices variable is of', type(train_indices))
# print keys in the dictionary training_indices
print('keys in the dictionary train_indices:\n', train_indices.keys())
以下是输出结果:

图 12.7:train_indices 文件信息输出 1
从之前的截图可以看到,MATLAB 文件已作为字典加载,包含四个键,其中之一是tr_plus_indices,这是我们需要的。让我们看一下这个键的内容:
# print type and shape of values in tr_plus_indices key
print('Shape of values in tr_plus_indices key: ',train_indices['tr_plus_indices'].shape)
# print tr_plus_indices
print('Type: ',type(train_indices['tr_plus_indices']))
# reshape the training_indices array
train_indices = train_indices['tr_plus_indices'].reshape(-1,)
# print shape of train_indices array
print("Shape of reshaped 'train_indices' array:", train_indices.shape)
以下是输出结果:

图 12.8:train_indices 文件信息输出 2
我们可以看到,tr_plus_indices键对应一个形状为(17380*1)的数组。为了方便起见,我们将其重塑为(17380, )。
tr_plus_indices包含了examples.mat文件中examples键的数据索引,这些数据仅应用于训练。使用这些信息,我们将数据划分为训练集和测试集:
# empty list to store train image ids
train_ids = []
# empty list to store train joints
train_jts = []
# empty list to store test image ids
test_ids = []
# empty list to store test joints
test_jts = []
for i, example in enumerate(examples):
# image id
file_name = example[3][0]
# joint coordinates
joint = example[2].T
# dictionary that goes into the joint_coordinates function
joints = dict(zip(target_joints, [x for k,x in enumerate(joint) if k in joints_loc_id]))
# obtain joints for the task
joints = joint_coordinates(joints)
# use train indices list to decide if an image is to be used for training or testing
if i in train_indices:
train_ids.append(file_name)
train_jts.append(joints)
else:
test_ids.append(file_name)
test_jts.append(joints)
# Concatenate image ids dataframe and the joints dataframe and save it as a csv
对于代码片段的其余部分,请参阅deeppose.ipynb文件: github.com/PacktPublishing/Python-Deep-Learning-Projects/blob/master/Chapter12/deeppose.ipynb
我们可以看到,训练数据有 17,380 个数据点,每个数据点有一个图像 ID 和7(x,y)的关节坐标。同样,测试数据有 3,548 个数据点。
在前面的代码片段中,我们首先初始化了四个空列表,两个用于保存训练和测试图像 ID,两个用于保存训练和测试关节。然后,对于 examples 键中的每个数据点,我们执行以下操作:
-
提取文件名。
-
提取关节坐标。
-
将目标关节(目标关节标签)与相应的关节坐标配对,并将其转换为字典。
-
将字典传递给
joint_coordinates函数,以获取此任务所需的关节。 -
使用
train_indices列表,将之前步骤中得到的图像 ID 和相应的关节添加到训练或测试列表中。
最后,将列表转换为训练和测试数据框,并将其保存为 CSV 文件。保存数据框时,请确保不将索引和标题参数设置为 False。
让我们加载在前一步保存的 train_joints.csv 和 test_joints.csv 文件,并打印出一些细节:
# load train_joints.csv
train_data = pd.read_csv('FLIC-full/train_joints.csv', header=None)
# load test_joints.csv
test_data = pd.read_csv('FLIC-full/test_joints.csv', header = None)
# train image ids
train_image_ids = train_data[0].values
print('train_image_ids shape', train_image_ids.shape)
# train joints
train_joints = train_data.iloc[:,1:].values
print('train_image_ids shape', train_joints.shape)
# test image ids
test_image_ids = test_data[0].values
print('train_image_ids shape', test_image_ids.shape)
# test joints
test_joints = test_data.iloc[:,1:].values
print('train_image_ids shape', test_joints.shape)
以下是输出:

图 12.9:图像 ID 和关节数组形状的打印输出
现在,让我们从 images 文件夹加载一些图像,并绘制它们,看看它们的样子:
import glob
image_list = glob.glob('FLIC-full/images/*.jpg')[0:8]
plt.figure(figsize=(12,5))
for i in range(8):
plt.subplot(2,4,(i+1))
img = plt.imread(image_list[i])
plt.imshow(img, aspect='auto')
plt.axis('off')
plt.title('Shape: '+str(img.shape))
plt.tight_layout()
plt.show()
以下是输出:

图 12.10:来自 FLIC_full 文件夹中的图像文件夹的八张图像的绘图
我们可以看到每张图像的形状为 (4807203)。接下来的任务是通过使用我们所拥有的关节坐标来裁剪原始图像并聚焦于感兴趣的对象。我们将图像调整为 2242243 的大小,以便将其输入到 VGG16 模型中。最后,我们还将构建一个 plotting 函数,用于在图像上绘制关节:

图 12.11:显示每张图像必须经过的变换的绘图
数据准备
现在让我们实现之前章节结尾时讨论的任务所需的函数。
裁剪
我们将首先从 image_cropping() 函数开始。此函数接受图像 ID 及其相应的关节坐标。它将图像加载到内存中,然后裁剪图像,只保留图像中由坐标框定的部分。裁剪后的图像将被填充,以便关节和肢体完全可见。对于添加的填充,关节坐标也会相应调整。完成此操作后,图像将被返回。这是转换中最重要的部分。花点时间分析这个函数,看看到底发生了什么(crop_pad_inf 和 crop_pad_sup 参数控制填充量):
def image_cropping(image_id, joints, crop_pad_inf = 1.4, crop_pad_sup=1.6, shift = 5, min_dim = 100):
"""Function to crop original images"""
## image cropping
# load the image
image = cv.imread('FLIC-full/images/%s' % (image_id))
# convert joint list to array
joints = np.asarray([int(float(p)) for p in joints])
# reshape joints to shape (7*2)
joints = joints.reshape((len(joints) // 2, 2))
# transform joints to list of (x,y) tuples
posi_joints = [(j[0], j[1]) for j in joints if j[0] > 0 and j[1] > 0]
# obtain the bounding rectangle using opencv boundingRect
x_loc, y_loc, width, height = cv.boundingRect(np.asarray([posi_joints]))
if width < min_dim:
width = min_dim
if height < min_dim:
height = min_dim
## bounding rect extending
inf, sup = crop_pad_inf, crop_pad_sup
r = sup - inf
# define width padding
pad_w_r = np.random.rand() * r + inf # inf~sup
# define height padding
pad_h_r = np.random.rand() * r + inf # inf~sup
# adjust x, y, w and h by the defined padding
x_loc -= (width * pad_w_r - width) / 2
y_loc -= (height * pad_h_r - height) / 2
width *= pad_w_r
height *= pad_h_r
## shifting
## clipping
## joint shifting
对于此代码片段的剩余部分,请参阅此处的文件deeppose.ipynb:github.com/PacktPublishing/Python-Deep-Learning-Projects/blob/master/Chapter12/deeppose.ipynb
让我们将图像 ID 及其关节传递给image_cropping()函数,并绘制输出图像:
# plot the original image
plt.figure(figsize = (15,5))
plt.subplot(1,2,1)
plt.title('Original')
plt.imshow(plt.imread('FLIC-full/images/'+train_image_ids[0]))
# plot the cropped image
image, joint = image_cropping(train_image_ids[0], train_joints[0])
plt.subplot(1,2,2)
plt.title('Cropped')
plt.imshow(image)
以下是输出:

图 12.12:裁剪后的图像与原始图像的绘图对比
调整大小
在Cropping部分中,我们看到形状为(4807203)的原始图像被裁剪为形状为(3932543)。然而,VGG16 架构接受形状为(2242243)的图像。因此,我们将定义一个名为image_resize()的函数来完成调整大小。它接受裁剪后的图像及其由image_cropping()函数产生的关节作为输入,并返回调整大小后的图像及其关节坐标:
def image_resize(image, joints, new_size = 224):
"""Function resize cropped images"""
orig_h, orig_w = image.shape[:2]
joints[0::2] = joints[0::2] / float(orig_w) * new_size
joints[1::2] = joints[1::2] / float(orig_h) * new_size
image = cv.resize(image, (new_size, new_size), interpolation=cv.INTER_NEAREST)
return image, joints
# plot resized image
image, joint = image_resize(image, joint)
plt.title('Cropped + Resized')
plt.imshow(image)
以下是输出:

图 12.13:调整大小后图像的绘图
将裁剪后的图像传递给image_resize()函数后,我们可以看到生成的图像形状为(2242243)。现在可以将此图像及其关节传递到模型进行训练。
绘制关节和四肢
让我们也定义绘制功能,它将在调整大小后的图像上绘制四肢。以下定义的plot_joints()函数接受调整大小后的图像及其关节,并返回相同形状的带有绘制四肢的图像:
def plot_limb(img, joints, i, j, color):
"""Function to plot the limbs"""
cv.line(img, joints[i], joints[j], (255, 255, 255), thickness=2, lineType=16)
cv.line(img, joints[i], joints[j], color, thickness=1, lineType=16)
return img
def plot_joints(img, joints, groundtruth=True, text_scale=0.5):
"""Function to draw the joints"""
h, w, c = img.shape
if groundtruth:
# left hand to left elbow
img = plot_limb(img, joints, 0, 1, (0, 255, 0))
# left elbow to left shoulder
img = plot_limb(img, joints, 1, 2, (0, 255, 0))
# left shoulder to right shoulder
img = plot_limb(img, joints, 2, 4, (0, 255, 0))
# right shoulder to right elbow
img = plot_limb(img, joints, 4, 5, (0, 255, 0))
# right elbow to right hand
img = plot_limb(img, joints, 5, 6, (0, 255, 0))
# neck coordinate
neck = tuple((np.array(joints[2]) + np.array(joints[4])) // 2)
joints.append(neck)
# neck to head
img = plot_limb(img, joints, 3, 7, (0, 255, 0))
joints.pop()
# joints
for j, joint in enumerate(joints):
# plot joints
cv.circle(img, joint, 5, (0, 255, 0), -1)
# plot joint number black
cv.putText(img, '%d' % j, joint, cv.FONT_HERSHEY_SIMPLEX, text_scale,
(0, 0, 0), thickness=2, lineType=16)
# plot joint number white
cv.putText(img, '%d' % j, joint, cv.FONT_HERSHEY_SIMPLEX, text_scale,
(255, 255, 255), thickness=1, lineType=16)
else:
对于此代码片段的剩余部分,请参阅此处的deeppose.ipynb文件:github.com/PacktPublishing/Python-Deep-Learning-Projects/blob/master/Chapter12/deeppose.ipynb
以下是输出:

图 12.14:显示真实关节坐标在图像顶部的绘图
转换图像
现在让我们使用我们之前定义的函数将图像及其对应的关节转换为所需形式。我们将借助定义如下的model_data()函数来实现这一点:
def model_data(image_ids, joints, train = True):
"""Function to generate train and test data."""
if train:
# empty list
train_img_joints = []
# create train directory inside FLIC-full
if not os.path.exists(os.path.join(os.getcwd(), 'FLIC-full/train')):
os.mkdir('FLIC-full/train')
for i, (image, joint) in enumerate(zip(image_ids, joints)):
# crop the image using the joint coordinates
image, joint = image_cropping(image, joint)
# resize the cropped image to shape (224*224*3)
image, joint = image_resize(image, joint)
# save the image in train folder
cv.imwrite('FLIC-full/train/train{}.jpg'.format(i), image)
# store joints and image id/file name of the saved image in the initialized list
train_img_joints.append(['train{}.jpg'.format(i)] + joint.tolist())
# convert to a dataframe and save as a csv
pd.DataFrame(train_img_joints).to_csv('FLIC-full/train/train_joints.csv', index=False, header=False)
else:
# empty list
test_img_joints = []
对于此代码片段的剩余部分,请参阅此处的deeppose.ipynb文件:github.com/PacktPublishing/Python-Deep-Learning-Projects/blob/master/Chapter12/deeppose.ipynb
前述定义的 model_data() 函数接受三个参数:image_ids(图像 ID 数组)、joints(关节点数组),以及一个布尔类型的参数 train。在转换训练图像和关节点时,将 train 参数设置为 True,在转换测试图像和关节点时,将其设置为 False。
当 train 参数设置为 True 时,执行以下步骤:
-
初始化一个空列表,用于存储转换后的图像 ID 及其关节点坐标。
-
如果
images文件夹中不存在train文件夹,则将在该文件夹内创建一个新的train文件夹。 -
首先将图像及其关节点坐标传递给我们之前定义的
image_cropping函数,该函数将返回裁剪后的图像和关节点坐标。 -
步骤 3 的结果被传递给
image_resize函数,然后该函数将图像调整为所需的形状。在我们的例子中,这是 2242243。 -
然后,调整大小后的图像通过 OpenCV 的
imwrite()函数写入train文件夹,并附上新的图像 ID(例如train0.jpg)。 -
新的图像 ID 和关节点将被附加到 步骤 1 中初始化的列表中。
-
步骤 3 到 步骤 6 会重复执行,直到所有训练图像都被转换。
-
在 步骤 1 中定义的列表现在包含了新的图像 ID 和关节点坐标,这些数据将被转换为数据框并保存为 CSV 文件,存放在
train文件夹中。
为了转换测试数据,前述过程会重复执行,将 train 参数设置为 False,并输入测试图像 ID 和关节点。
在 model_data() 函数中生成的训练和测试数据框将作为没有头部和索引列的 CSV 文件存储。加载这些文件时要注意这一点。
定义训练的超参数
以下是我们在代码中将使用的一些已定义的超参数,这些都可以进行配置:
# Number of epochs
epochs = 3
# Batchsize
batch_size = 128
# Optimizer for the model
optimizer = Adam(lr=0.0001, beta_1=0.5)
# Shape of the input image
input_shape = (224, 224, 3)
# Batch interval at which loss is to be stored
store = 40
尝试不同的学习率、优化器、批量大小,以及平滑值,看看这些因素如何影响模型的质量。如果获得了更好的结果,可以向深度学习社区展示。
构建 VGG16 模型
VGG16 模型是一种深度卷积神经网络图像分类器。该模型使用 Conv2D、MaxPooling2D 和 Dense 层的组合形成最终的架构,且使用 ReLU 激活函数。它接受形状为 2242243 的彩色图像,并能够预测 1,000 个类别。这意味着最终的 Dense 层有 1,000 个神经元,并使用 softmax 激活函数来获得每个类别的得分。
定义 VGG16 模型
在这个项目中,我们希望输入形状为 2242243 的图像,并能够预测图像中身体的关节点坐标。也就是说,我们希望能够预测 14 个数值(7(x,y)对)。因此,我们将最后的 Dense 层修改为 14 个神经元,并使用 ReLU 激活函数代替 sigmoid。
在本地机器上训练一个深度学习模型,如 VGG16,可能需要一周的时间。这是非常耗时的。在我们的案例中,替代方案是通过迁移学习使用已经训练好的 VGG16 模型的权重。
我们将借助于在开始时导入的 Keras 应用模块以及其他导入来完成此操作。
在以下代码中,我们将加载 VGG16 模型的一部分,直到但不包括 Flatten 层以及相应的权重。将include_top参数设置为False可以实现这一点:
以下代码的第一行也将从 Keras 服务器下载 VGG16 的权重,因此你无需担心从其他地方下载权重文件。
# load the VGG16 model
model = applications.VGG16(weights = "imagenet", include_top=False, input_shape = input_shape)
# print summary of VGG16 model
model.summary()
以下是输出:

图 12.15:VGG16 模型的总结(直到 Flatten)
从总结中,我们可以看到 VGG16 模型的所有层(直到但不包括 Flatten 层)已经加载了它们的权重。
要了解 Keras 的应用模块的附加功能,请查看官方文档:keras.io/applications/。
我们不希望这些层的任何权重被训练。因此,在以下代码中,我们需要将每个层的trainable参数设置为False:
# set layers as non trainable
for layer in model.layers:
layer.trainable = False
下一步,扁平化前面部分的模型输出,然后添加三个 Dense 层,其中两个层每个有 1,024 个神经元,并且它们之间有一个 dropout,最后添加一个 Dense 层,包含 14 个神经元,用来获取 14 个关节坐标。我们将仅训练以下代码片段中定义的层的权重:
# Adding custom Layers
x = model.output
x = Flatten()(x)
x = Dense(1024, activation="relu")(x)
x = Dropout(0.5)(x)
x = Dense(1024, activation="relu")(x)
# Dense layer with 14 neurons for predicting 14 numeric values
predictions = Dense(14, activation="relu")(x)
一旦所有层都被定义和配置完毕,我们将使用 Keras 中的Model函数将它们组合在一起,如下所示:
# creating the final model
model_final = Model(inputs = model.input, outputs = predictions)
# print summary
model_final.summary()
以下是输出:

图 12.16:定制化 VGG16 模型的总结
从总结中,我们可以看到26,755,086个参数是可训练的,而14,714,688个参数是不可训练的,因为我们已经将它们设置为不可训练。
然后,模型使用mean_squared_error作为loss进行编译。这里使用的optimizer是 Adam,其学习率为 0.0001,这是在超参数部分通过优化器变量定义的:
# compile the model
model_final.compile(loss = "mean_squared_error", optimizer = optimizer)
训练循环
现在 VGG16 模型已经准备好用于训练,接下来我们加载train文件夹中的train_joints.csv文件,该文件包含了裁剪和调整大小后的图像的 ID 以及它们的关节坐标。
然后,使用来自sklearn的train_test_split模块将数据拆分为 80:20 的训练集和验证集。我们在本章开头与其他导入一起导入了它。由于验证数据较少,将所有相应的图像加载到内存中:
请注意加载多少验证图像到内存,因为这可能会在内存较小的系统中成为问题。
# load the train data
train = pd.read_csv('FLIC-full/train/train_joints.csv', header = None)
# split train into train and validation
train_img_ids, val_img_ids, train_jts, val_jts = train_test_split(train.iloc[:,0], train.iloc[:,1:], test_size=0.2, random_state=42)
# load validation images
val_images = np.array([cv.imread('FLIC-full/train/{}'.format(x)) for x in val_img_ids.values])
# convert validation images to dtype float
val_images = val_images.astype(float)
使用 pandas 的head、tail和info函数探索数据。请注意,在使用 pandas 加载.csv文件时,要将header参数设置为False,这样 pandas 就知道文件没有头部。
我们现在将定义training()函数,该函数将在训练图像上训练 VGG16 模型。此函数接受 VGG16 模型、训练图像 ID、训练关节、验证图像和验证关节作为参数。以下步骤定义了training()函数的工作过程:
-
该函数通过使用
loss_lst定义空列表来存储训练损失,并使用val_loss_lst来存储验证损失。它还定义了一个计数器 count,用于跟踪批次的总数。 -
它接着创建了一个包含训练图像 ID 和其对应关节的批次。
-
使用批次图像 ID,它通过使用 OpenCV 的
imread()函数将相应的图像加载到内存中。 -
它接着将加载的训练图像转换为
float,并将其与联合 ID 一起输入到模型的train_on_batch()函数进行拟合。 -
每经过第 40 次批次,它会在验证数据上评估模型,并将训练和验证损失存储在定义的列表中。
-
它接着重复步骤 2 到 步骤 5,直到达到所需的训练轮次。
以下是代码:
def training(model, image_ids, joints ,val_images, val_jts, batch_size = 128, epochs=3, store = 40):
# empty train loss list
loss_lst = []
# empty validation loss list
val_loss_lst = []
# counter
count = 0
count_lst = []
# create shuffled batches
batches = np.arange(len(image_ids)//batch_size)
data_idx = np.arange(len(image_ids))
random.shuffle(data_idx)
print('......Training......')
for epoch in range(epochs):
for batch in (batches):
# batch of training image ids
imgs = image_ids[data_idx[batch*batch_size : (batch+1)*batch_size:]]
# corresponding joints for the above images
jts = joints[data_idx[batch*batch_size : (batch+1)*batch_size:]]
# load the training image batch
batch_imgs = np.array([cv.imread('FLIC-full/train/{}'.format(x)) for x in imgs])
# fit model on the batch
loss = model.train_on_batch(batch_imgs.astype(float), jts)
if batch%store==0:
关于此代码段的其余部分,请参阅deeppose.ipynb文件:github.com/PacktPublishing/Python-Deep-Learning-Projects/blob/master/Chapter12/deeppose.ipynb
输出如下:

以下是代码执行完毕后的输出:

图 12.17:训练模型时的损失输出
如果你使用的是小型 GPU 进行训练,请减少批次大小以避免 GPU 内存问题。还要记住,较小的批次大小可能不会得到本章节中所示的相同拟合结果。
绘制训练和验证损失图
通过loss_lst和val_loss_lst在每 40 个批次的间隔中包含训练和验证的 MSE 损失,让我们绘制图表并查看学习进展:
plt.style.use('ggplot')
plt.figure(figsize=(10, 6))
plt.plot(count_lst, loss_lst, marker='D', label = 'training_loss')
plt.plot(count_lst, val_loss_lst, marker='o', label = 'validation_loss')
plt.ylabel('Mean Squared Error')
plt.title('Plot of MSE over time')
plt.legend(loc = 'upper right')
plt.show()
以下是输出:

图 12.18:训练和验证损失图
通过减少存储超参数,可以获得平滑的训练验证损失图。较小的存储值将导致更长的训练时间。
预测
这是我们一直在等待的……
进行测试预测!
我们将定义一个函数,该函数以模型作为输入,测试我们已经预处理并保存在test文件夹中的测试数据。除了预测结果外,它还将通过使用我们在前一部分定义的plot_limb()和plot_joints()函数,将测试图像上的真实和预测关节绘制出来:
def test(model, nrows=200, batch_size=128):
# load the train data
test = pd.read_csv('FLIC-full/test/test_joints.csv', header = None, nrows=nrows)
test_img_ids = test.iloc[:,0].values
# load validation images
test_images = np.array([cv.imread('FLIC-full/test/{}'.format(x)) for x in test_img_ids])
# convert validation images to dtype float
test_images = test_images.astype(float)
# joints
test_joints = test.iloc[:,1:].values
# evaluate
test_loss = model.evaluate(test_images, test_joints, verbose = 0, batch_size=batch_size)
# predict
predictions = model.predict(test_images, verbose = 0, batch_size=batch_size)
# folder to save the results
if not os.path.exists(os.path.join(os.getcwd(), 'FLIC-full/test_plot')):
os.mkdir('FLIC-full/test_plot')
for i, (ids, image, joint, pred) in enumerate(zip(test_img_ids, test_images, test_joints, predictions)):
joints = joint.tolist()
joints = list(zip(joints[0::2], joints[1::2]))
# plot original joints
image = plot_joints(image.astype(np.uint8), joints, groundtruth=True, text_scale=0.5)
pred = pred.astype(np.uint8).tolist()
pred = list(zip(pred[0::2], pred[1::2]))
# plot predicted joints
image = plot_joints(image.astype(np.uint8), pred, groundtruth=False, text_scale=0.5)
# save resulting images with the same id
plt.imsave('FLIC-full/test_plot/'+ids, image)
return test_loss
# test and save results
test_loss = test(m, batch_size)
# print test loss
print('Test Loss:', test_loss)
以下是输出:

图 12.19:测试损失
在 200 张测试图像的测试集中,测试 MSE 损失为 454.80,与验证 MSE 损失 503.85 非常接近,这表明该模型没有在训练数据上过拟合。
如果可能的话,继续训练模型更多的周期,并检查是否能得到更好的拟合。注意你想加载多少测试图像到内存中进行评估,因为在内存有限的机器上,这可能会成为问题。
现在让我们绘制在测试期间保存的图像,以衡量真实关节点与预测关节点的对比:
image_list = glob.glob('FLIC-full/test_plot/*.jpg')[8:16]
plt.figure(figsize=(16,8))
for i in range(8):
plt.subplot(2,4,(i+1))
img = cv.imread(image_list[i])
plt.imshow(img, aspect='auto')
plt.axis('off')
plt.title('Green-True/Red-Predicted Joints')
plt.tight_layout()
plt.show()
以下是输出结果:

图 12.20:测试图像及其真实和预测关节点的重叠图
从前面的图像中,我们可以看到模型在预测未见图像中的七个关节点时表现得非常好。
模块化形式的脚本
整个脚本可以分为四个模块,分别是train.py、test.py、plotting.py和crop_resize_transform.py。你应该能够在Chapter12文件夹中找到这些脚本。按照本章代码实现部分的说明来运行这些脚本。在你最喜欢的源代码编辑器中将Chapter12设置为项目文件夹,然后运行train.py文件。
train.py Python 文件将在执行所需的位置导入其他模块中的函数。
现在让我们逐步查看每个文件的内容。
模块 1 – crop_resize_transform.py
该 Python 文件包含image_cropping()、image_resize()和model_data()函数,如下所示:
"""This module contains functions to crop and resize images."""
import os
import cv2 as cv
import numpy as np
import pandas as pd
def image_cropping(image_id, joints, crop_pad_inf=1.4, crop_pad_sup=1.6,
shift=5, min_dim=100):
"""Crop Function."""
# # image cropping
# load the image
image = cv.imread('FLIC-full/images/%s' % (image_id))
# convert joint list to array
joints = np.asarray([int(float(p)) for p in joints])
# reshape joints to shape (7*2)
joints = joints.reshape((len(joints) // 2, 2))
# transform joints to list of (x,y) tuples
posi_joints = [(j[0], j[1]) for j in joints if j[0] > 0 and j[1] > 0]
# obtain the bounding rectangle using opencv boundingRect
x_loc, y_loc, width, height = cv.boundingRect(np.asarray([posi_joints]))
if width < min_dim:
width = min_dim
if height < min_dim:
height = min_dim
# # bounding rect extending
inf, sup = crop_pad_inf, crop_pad_sup
r = sup - inf
# define width padding
pad_w_r = np.random.rand() * r + inf # inf~sup
# define height padding
pad_h_r = np.random.rand() * r + inf # inf~sup
# adjust x, y, w and h by the defined padding
x_loc -= (width * pad_w_r - width) / 2
y_loc -= (height * pad_h_r - height) / 2
width *= pad_w_r
height *= pad_h_r
# # shifting
x_loc += np.random.rand() * shift * 2 - shift
y_loc += np.random.rand() * shift * 2 - shift
# # clipping
x_loc, y_loc, width, height = [int(z) for z in [x_loc, y_loc,
width, height]]
x_loc = np.clip(x_loc, 0, image.shape[1] - 1)
y_loc = np.clip(y_loc, 0, image.shape[0] - 1)
width = np.clip(width, 1, image.shape[1] - (x_loc + 1))
height = np.clip(height, 1, image.shape[0] - (y_loc + 1))
image = image[y_loc: y_loc + height, x_loc: x_loc + width]
# # joint shifting
# adjust joint coordinates onto the padded image
joints = np.asarray([(j[0] - x_loc, j[1] - y_loc) for j in joints])
joints = joints.flatten()
return image, joints
def image_resize(image, joints, new_size=224):
"""Resize Function."""
orig_h, orig_w = image.shape[:2]
joints[0::2] = joints[0::2] / float(orig_w) * new_size
joints[1::2] = joints[1::2] / float(orig_h) * new_size
image = cv.resize(image, (new_size, new_size),
interpolation=cv.INTER_NEAREST)
return image, joints
def model_data(image_ids, joints, train=True):
"""Function to generate train and test data."""
if train:
# empty list
train_img_joints = []
# create train directory inside FLIC-full
if not os.path.exists(os.path.join(os.getcwd(), 'FLIC-full/train')):
os.mkdir('FLIC-full/train')
for i, (image, joint) in enumerate(zip(image_ids, joints)):
# crop the image using the joint coordinates
image, joint = image_cropping(image, joint)
# resize the cropped image to shape (224*224*3)
image, joint = image_resize(image, joint)
# save the image in train folder
cv.imwrite('FLIC-full/train/train{}.jpg'.format(i), image)
# store joints and image id/file name of the saved image in
# the initialized list
train_img_joints.append(['train{}.jpg'.format(i)] + joint.tolist())
# convert to a dataframe and save as a csv
pd.DataFrame(train_img_joints
).to_csv('FLIC-full/train/train_joints.csv',
index=False, header=False)
else:
# empty list
test_img_joints = []
# create test directory inside FLIC-full
if not os.path.exists(os.path.join(os.getcwd(), 'FLIC-full/test')):
os.mkdir('FLIC-full/test')
for i, (image, joint) in enumerate(zip(image_ids, joints)):
# crop the image using the joint coordinates
image, joint = image_cropping(image, joint)
# resize the cropped image to shape (224*224*3)
image, joint = image_resize(image, joint)
# save the image in test folder
cv.imwrite('FLIC-full/test/test{}.jpg'.format(i), image)
# store joints and image id/file name of the saved image
# in the initialized list
test_img_joints.append(['test{}.jpg'.format(i)] + joint.tolist())
# convert to a dataframe and save as a csv
pd.DataFrame(test_img_joints).to_csv('FLIC-full/test/test_joints.csv',
index=False, header=False)
模块 2 – plotting.py
该 Python 文件包含两个函数,分别是plot_limb()和plot_joints(),如下所示:
"""This module contains functions to plot the joints and the limbs."""
import cv2 as cv
import numpy as np
def plot_limb(img, joints, i, j, color):
"""Limb plot function."""
cv.line(img, joints[i], joints[j], (255, 255, 255), thickness=2,
lineType=16)
cv.line(img, joints[i], joints[j], color, thickness=1, lineType=16)
return img
def plot_joints(img, joints, groundtruth=True, text_scale=0.5):
"""Joint and Limb plot function."""
h, w, c = img.shape
if groundtruth:
# left hand to left elbow
img = plot_limb(img, joints, 0, 1, (0, 255, 0))
# left elbow to left shoulder
img = plot_limb(img, joints, 1, 2, (0, 255, 0))
# left shoulder to right shoulder
img = plot_limb(img, joints, 2, 4, (0, 255, 0))
# right shoulder to right elbow
img = plot_limb(img, joints, 4, 5, (0, 255, 0))
# right elbow to right hand
img = plot_limb(img, joints, 5, 6, (0, 255, 0))
# neck coordinate
neck = tuple((np.array(joints[2]) + np.array(joints[4])) // 2)
joints.append(neck)
# neck to head
img = plot_limb(img, joints, 3, 7, (0, 255, 0))
joints.pop()
# joints
for j, joint in enumerate(joints):
# plot joints
cv.circle(img, joint, 5, (0, 255, 0), -1)
# plot joint number black
cv.putText(img, '%d' % j, joint, cv.FONT_HERSHEY_SIMPLEX, text_scale,
(0, 0, 0), thickness=2, lineType=16)
# plot joint number white
cv.putText(img, '%d' % j, joint, cv.FONT_HERSHEY_SIMPLEX, text_scale,
(255, 255, 255), thickness=1, lineType=16)
else:
# left hand to left elbow
img = plot_limb(img, joints, 0, 1, (0, 0, 255))
# left elbow to left shoulder
img = plot_limb(img, joints, 1, 2, (0, 0, 255))
# left shoulder to right shoulder
img = plot_limb(img, joints, 2, 4, (0, 0, 255))
# right shoulder to right elbow
img = plot_limb(img, joints, 4, 5, (0, 0, 255))
# right elbow to right hand
img = plot_limb(img, joints, 5, 6, (0, 0, 255))
# neck coordinate
neck = tuple((np.array(joints[2]) + np.array(joints[4])) // 2)
joints.append(neck)
# neck to head
img = plot_limb(img, joints, 3, 7, (0, 0, 255))
joints.pop()
# joints
for j, joint in enumerate(joints):
# plot joints
cv.circle(img, joint, 5, (0, 0, 255), -1)
# plot joint number black
cv.putText(img, '%d' % j, joint, cv.FONT_HERSHEY_SIMPLEX, text_scale,
(0, 0, 0), thickness=3, lineType=16)
# plot joint number white
cv.putText(img, '%d' % j, joint, cv.FONT_HERSHEY_SIMPLEX, text_scale,
(255, 255, 255), thickness=1, lineType=16)
return img
模块 3 – test.py
该模块包含了test()函数,该函数将在train_dqn.py脚本中调用,用于测试训练模型的性能,如下所示:
"""This module contains the function to test the vgg16 model performance."""
from plotting import *
import os
import pandas as pd
import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt
def test(model, nrows=200, batch_size=128):
"""Test trained vgg16."""
# load the train data
test = pd.read_csv('FLIC-full/test/test_joints.csv', header=None,
nrows=nrows)
test_img_ids = test.iloc[:, 0].values
# load validation images
test_images = np.array(
[cv.imread('FLIC-full/test/{}'.format(x)) for x in test_img_ids])
# convert validation images to dtype float
test_images = test_images.astype(float)
# joints
test_joints = test.iloc[:, 1:].values
# evaluate
test_loss = model.evaluate(test_images, test_joints,
verbose=0, batch_size=batch_size)
# predict
predictions = model.predict(test_images, verbose=0, batch_size=batch_size)
# folder to save the results
if not os.path.exists(os.path.join(os.getcwd(), 'FLIC-full/test_plot')):
os.mkdir('FLIC-full/test_plot')
for i, (ids, image, joint, pred) in enumerate(zip(test_img_ids,
test_images,
test_joints,
predictions)):
joints = joint.tolist()
joints = list(zip(joints[0::2], joints[1::2]))
# plot original joints
image = plot_joints(image.astype(np.uint8), joints,
groundtruth=True, text_scale=0.5)
pred = pred.astype(np.uint8).tolist()
pred = list(zip(pred[0::2], pred[1::2]))
# plot predicted joints
image = plot_joints(image.astype(np.uint8), pred,
groundtruth=False, text_scale=0.5)
# save resulting images with the same id
plt.imsave('FLIC-full/test_plot/'+ids, image)
return test_loss
模块 4 – train.py
在这个模块中,我们有joint_coordinates()和training()函数,以及用于训练和测试 VGG16 模型的调用:
"""This module imports other modules to train the vgg16 model."""
from __future__ import print_function
from crop_resize_transform import model_data
from test import test
import matplotlib.pyplot as plt
import random
from scipy.io import loadmat
import numpy as np
import pandas as pd
import cv2 as cv
import glob
from sklearn.model_selection import train_test_split
from keras.models import Model
from keras.optimizers import Adam
from keras.layers import Flatten, Dense, Dropout
from keras import backend as K
from keras import applications
K.clear_session()
# set seed for reproducibility
seed_val = 9000
np.random.seed(seed_val)
random.seed(seed_val)
# load the examples file
examples = loadmat('FLIC-full/examples.mat')
# reshape the examples array
examples = examples['examples'].reshape(-1,)
# each coordinate corresponds to the the below listed body joints/locations
# in the same order
joint_labels = ['lsho', 'lelb', 'lwri', 'rsho', 'relb', 'rwri', 'lhip',
'lkne', 'lank', 'rhip', 'rkne', 'rank', 'leye', 'reye',
'lear', 'rear', 'nose', 'msho', 'mhip', 'mear', 'mtorso',
'mluarm', 'mruarm', 'mllarm', 'mrlarm', 'mluleg', 'mruleg',
'mllleg', 'mrlleg']
# print list of known joints
known_joints = [x for i, x in enumerate(joint_labels) if i in np.r_[0:7, 9,
12:14, 16]]
target_joints = ['lsho', 'lelb', 'lwri', 'rsho', 'relb',
'rwri', 'leye', 'reye', 'nose']
# indices of the needed joints in the coordinates array
joints_loc_id = np.r_[0:6, 12:14, 16]
def joint_coordinates(joint):
"""Store necessary coordinates to a list."""
joint_coor = []
# Take mean of the leye, reye, nose to obtain coordinates for the head
joint['head'] = (joint['leye']+joint['reye']+joint['nose'])/3
joint_coor.extend(joint['lwri'].tolist())
joint_coor.extend(joint['lelb'].tolist())
joint_coor.extend(joint['lsho'].tolist())
joint_coor.extend(joint['head'].tolist())
joint_coor.extend(joint['rsho'].tolist())
joint_coor.extend(joint['relb'].tolist())
joint_coor.extend(joint['rwri'].tolist())
return joint_coor
# load the indices matlab file
train_indices = loadmat('FLIC-full/tr_plus_indices.mat')
# reshape the training_indices array
train_indices = train_indices['tr_plus_indices'].reshape(-1,)
# empty list to store train image ids
train_ids = []
# empty list to store train joints
train_jts = []
# empty list to store test image ids
test_ids = []
# empty list to store test joints
test_jts = []
for i, example in enumerate(examples):
# image id
file_name = example[3][0]
# joint coordinates
joint = example[2].T
# dictionary that goes into the joint_coordinates function
joints = dict(zip(target_joints,
[x for k, x in enumerate(joint) if k in joints_loc_id]))
# obtain joints for the task
joints = joint_coordinates(joints)
# use train indices list to decide if an image is to be used for training
# or testing
if i in train_indices:
train_ids.append(file_name)
train_jts.append(joints)
else:
test_ids.append(file_name)
test_jts.append(joints)
# Concatenate image ids dataframe and the joints dataframe and save it as a csv
train_df = pd.concat([pd.DataFrame(train_ids), pd.DataFrame(train_jts)],
axis=1)
test_df = pd.concat([pd.DataFrame(test_ids), pd.DataFrame(test_jts)], axis=1)
train_df.to_csv('FLIC-full/train_joints.csv', index=False, header=False)
test_df.to_csv('FLIC-full/test_joints.csv', index=False, header=False)
# load train_joints.csv
train_data = pd.read_csv('FLIC-full/train_joints.csv', header=None)
# load test_joints.csv
test_data = pd.read_csv('FLIC-full/test_joints.csv', header=None)
# train image ids
train_image_ids = train_data[0].values
# train joints
train_joints = train_data.iloc[:, 1:].values
# test image ids
test_image_ids = test_data[0].values
# test joints
test_joints = test_data.iloc[:, 1:].values
model_data(train_image_ids, train_joints, train=True)
model_data(test_image_ids, test_joints, train=False)
# Number of epochs
epochs = 3
# Batchsize
batch_size = 128
# Optimizer for the model
optimizer = Adam(lr=0.0001, beta_1=0.5)
# Shape of the input image
input_shape = (224, 224, 3)
# Batch interval at which loss is to be stores
store = 40
# load the vgg16 model
model = applications.VGG16(weights="imagenet", include_top=False,
input_shape=input_shape)
# set layers as non trainable
for layer in model.layers:
layer.trainable = False
# Adding custom Layers
x = model.output
x = Flatten()(x)
x = Dense(1024, activation="relu")(x)
x = Dropout(0.5)(x)
x = Dense(1024, activation="relu")(x)
# Dense layer with 14 neurons for predicting 14 numeric values
predictions = Dense(14, activation="relu")(x)
# creating the final model
model_final = Model(inputs=model.input, outputs=predictions)
# compile the model
model_final.compile(loss="mean_squared_error", optimizer=optimizer)
# load the train data
train = pd.read_csv('FLIC-full/train/train_joints.csv', header=None)
# split train into train and validation
train_img_ids, val_img_ids, train_jts, val_jts = train_test_split(
train.iloc[:, 0], train.iloc[:, 1:], test_size=0.2, random_state=42)
# load validation images
val_images = np.array(
[cv.imread('FLIC-full/train/{}'.format(w)) for w in val_img_ids.values])
# convert validation images to dtype float
val_images = val_images.astype(float)
def training(model, image_ids, joints, val_images, val_jts,
batch_size=128, epochs=2):
"""Train vgg16."""
# empty train loss and validation loss list
loss_lst = []
val_loss_lst = []
count = 0 # counter
count_lst = []
# create shuffled batches
batches = np.arange(len(image_ids)//batch_size)
data_idx = np.arange(len(image_ids))
random.shuffle(data_idx)
print('......Training......')
for epoch in range(epochs):
for batch in (batches):
# batch of training image ids
imgs = image_ids[data_idx[batch*batch_size:(batch+1)*batch_size:]]
# corresponding joints for the above images
jts = joints[data_idx[batch*batch_size:(batch+1)*batch_size:]]
# load the training image batch
batch_imgs = np.array(
[cv.imread('FLIC-full/train/{}'.format(x)) for x in imgs])
# fit model on the batch
loss = model.train_on_batch(batch_imgs.astype(float), jts)
if batch % 40 == 0:
# evaluate model on validation set
val_loss = model.evaluate(val_images, val_jts, verbose=0,
batch_size=batch_size)
# store train and val loss
loss_lst.append(loss)
val_loss_lst.append(val_loss)
print('Epoch:{}, End of batch:{}, loss:{:.2f},val_loss:{:.2f}\
'.format(epoch+1, batch+1, loss, val_loss))
count_lst.append(count)
else:
print('Epoch:{}, End of batch:{}, loss:{:.2f}\
'.format(epoch+1, batch+1, loss))
count += 1
count_lst.append(count)
loss_lst.append(loss)
val_loss = model.evaluate(val_images, val_jts, verbose=0,
batch_size=batch_size)
val_loss_lst.append(val_loss)
print('Epoch:{}, End of batch:{}, VAL_LOSS:{:.2f}\
'.format(epoch+1, batch+1, val_loss))
return model, loss_lst, val_loss_lst, count_lst
m, loss_lst, val_loss_lst, count_lst = training(model_final,
train_img_ids.values,
train_jts.values,
val_images,
val_jts.values,
epochs=epochs,
batch_size=batch_size)
# plot the learning
plt.style.use('ggplot')
plt.figure(figsize=(10, 6))
plt.plot(count_lst, loss_lst, marker='D', label='training_loss')
plt.plot(count_lst, val_loss_lst, marker='o', label='validation_loss')
plt.xlabel('Batches')
plt.ylabel('Mean Squared Error')
plt.title('Plot of MSE over time')
plt.legend(loc='upper right')
plt.show()
# test and save results
test_loss = test(m)
# print test_loss
print('Test Loss:', test_loss)
image_list = glob.glob('FLIC-full/test_plot/*.jpg')[8:16]
plt.figure(figsize=(16, 8))
for i in range(8):
plt.subplot(2, 4, (i+1))
img = cv.imread(image_list[i])
plt.imshow(img, aspect='auto')
plt.axis('off')
plt.title('Green-True/Red-Predicted Joints')
plt.tight_layout()
plt.show()
结论
这个项目的目的是构建一个卷积神经网络 (CNN)分类器,来解决使用从电影中捕捉的帧估计 3D 人体姿势的问题。我们的假设用例是让视觉特效专家能够轻松估计演员的姿势(通过视频帧中的肩膀、脖子和头部)。我们的任务是为这个应用程序构建智能。
我们通过迁移学习构建的修改版 VGG16 架构,其在 200 个测试图像中对每个 14 个坐标(即7(x,y)对)的测试均方误差(MSE)为 454.81 平方单位。我们还可以说,对于每个 14 个坐标,200 个测试图像的测试均方根误差(RMSE)为 21.326 单位。这意味着什么呢?
均方根误差 (RMSE) 在此情况下是衡量预测的关节坐标/关节像素位置与实际关节坐标/关节像素位置之间差距的指标。
RMSE 损失值为 21.32 单位,相当于在形状为 2242243 的图像中,每个预测的坐标偏差为 21.32 像素。图 13.20 中的测试结果展示了这一度量。
每个坐标偏差 21.32 像素在一般层面上是可以接受的,但我们希望构建的产品将用于电影制作,其中误差的容忍度要低得多,偏差 21 像素是不可接受的。
为了改进模型,您可以采取以下措施:
-
尝试使用较低的学习率,增加训练的轮数
-
尝试使用不同的损失函数(例如,均值绝对误差 (MAE))
-
尝试使用更深的模型,例如 RESNET50 或 VGG19
-
尝试对数据进行中心化和标准化
-
获取更多的数据
如果您在完成本章后有兴趣成为该领域的专家,以下是一些您应该采取的额外步骤。
总结
在本章中,我们成功地在 Keras 中构建了一个深度卷积神经网络/VGG16 模型,应用于 FLIC 图像。我们亲手实践了如何准备这些图像以供建模使用。我们成功实现了迁移学习,并理解到这样做将节省大量时间。在某些地方,我们还定义了一些关键的超参数,并推理为什么使用这些参数。最后,我们测试了修改后的 VGG16 模型在未见数据上的表现,并确认我们成功达成了目标。
第十三章:使用 GAN 进行图像翻译以实现风格迁移
欢迎来到关于生成对抗网络(GANs)的章节。在本章中,我们将构建一个神经网络,填补手写数字缺失的部分。之前,我们为餐饮连锁店构建了一个数字分类器。但他们也注意到,有时当顾客写下电话号码时,数字的某些小部分缺失。这可能是由于顾客在 iPad 应用程序上写字时没有流畅的书写动作,也可能是因为 iPad 应用程序没有正确处理用户在屏幕上的完整手势。这使得手写数字分类器很难预测出与手写数字对应的正确数字。现在,他们希望我们重建(生成回)手写数字缺失的部分,以便分类器能够接收到清晰的手写数字并转化为数字。这样,分类器将能够更准确地分类手写数字,通知也能发送到正确的饥饿顾客!
我们将主要关注数字缺失部分的生成/重建,并且我们将借助 GAN 的神经修复来完成这一点;请参见以下流程图:

图 13.1:GAN 流程图
本章我们将学习以下内容:
-
什么是 GAN
-
什么是生成器和判别器
-
编写模型并定义超参数
-
构建并理解训练循环
-
测试模型
-
将模型扩展到新的数据集
在本章中,你将实现以下内容:
-
构建一个 MNIST 数字分类器
-
模拟一个手写数字数据集,其中部分手写数字缺失
-
使用 MNIST 分类器预测带噪声/掩码的 MNIST 数字数据集(模拟数据集)
-
实现 GAN 来恢复数字缺失的部分
-
使用 MNIST 分类器预测来自 GAN 生成的数字
-
比较掩码数据和生成数据的性能
最好在本章中边学边实现代码片段,可以在 Jupyter Notebook 或任何源代码编辑器中进行。这将帮助你更容易地跟随课程,并理解每一部分代码的作用。
本章的所有 Python 文件和 Jupyter Notebook 文件可以在这里找到:github.com/PacktPublishing/Python-Deep-Learning-Projects/tree/master/Chapter13。
让我们开始实现代码吧!
在这个练习中,我们将使用 Keras 深度学习库,它是一个高级神经网络 API,可以在 Tensorflow、Theano 或 Cognitive Toolkit (CNTK) 上运行。
了解代码!我们不会花时间去理解 Keras 的工作原理,但如果你感兴趣,可以参考这个通俗易懂的 Keras 官方文档:keras.io/。
导入所有依赖包
在这个练习中,我们将使用 numpy、matplotlib、keras、tensorflow 和 tqdm 包。这里,TensorFlow 被用作 Keras 的后端。你可以使用 pip 安装这些包。对于 MNIST 数据,我们将使用 keras 模块中的数据集,只需简单导入即可:
import numpy as np
import random
import matplotlib.pyplot as plt
%matplotlib inline
from tqdm import tqdm
from keras.layers import Input, Conv2D
from keras.layers import AveragePooling2D, BatchNormalization
from keras.layers import UpSampling2D, Flatten, Activation
from keras.models import Model, Sequential
from keras.layers.core import Dense, Dropout
from keras.layers.advanced_activations import LeakyReLU
from keras.optimizers import Adam
from keras import backend as k
from keras.datasets import mnist
设置 seed 以确保可重现性是很重要的:
# set seed for reproducibility
seed_val = 9000
np.random.seed(seed_val)
random.seed(seed_val)
探索数据
我们将从 keras 模块中使用 mnist.load_data() 加载 MNIST 数据到会话中。完成后,我们将打印数据集的形状和大小,以及数据集中类别的数量和唯一标签:
(X_train, y_train), (X_test, y_test) = mnist.load_data()
print('Size of the training_set: ', X_train.shape)
print('Size of the test_set: ', X_test.shape)
print('Shape of each image: ', X_train[0].shape)
print('Total number of classes: ', len(np.unique(y_train)))
print('Unique class labels: ', np.unique(y_train))
我们有一个包含 10 个不同类别、60,000 张图像的数据集,每张图像的形状是 28*28,每个类别有 6,000 张图像。
让我们绘制并看看这些手写图像是什么样子的:
# Plot of 9 random images
for i in range(0, 9):
plt.subplot(331+i) # plot of 3 rows and 3 columns
plt.axis('off') # turn off axis
plt.imshow(X_train[i], cmap='gray') # gray scale
输出结果如下:

图 13.2:来自训练集的九个 MNIST 数字图
让我们绘制来自每个类别的一个手写数字:
# plotting image from each class
fig=plt.figure(figsize=(8, 4))
columns = 5
rows = 2
for i in range(0, rows*columns):
fig.add_subplot(rows, columns, i+1)
plt.title(str(i)) # label
plt.axis('off') # turn off axis
plt.imshow(X_train[np.where(y_train==i)][0], cmap='gray') # gray scale
plt.show()
输出结果如下:

图 13.3:来自每个类别的一个 MNIST 数字图
查看数据集中最大和最小的像素值:
print('Maximum pixel value in the training_set: ', np.max(X_train))
print('Minimum pixel value in the training_set: ', np.min(X_train))
输出结果如下:

图 13.5:九个带噪声/掩蔽的 MNIST 数字图
我们看到数据集中的最大像素值为 255,最小值为 0。
准备数据
类型转换、居中、缩放和重塑是我们将在本章中实现的一些预处理操作。
类型转换、居中和缩放
将类型设置为 np.float32。
重要:这样做的主要原因之一是权重将全部为 float 类型,而浮动数值间的乘法运算比整数和浮动数值间的乘法运算要快得多。因此,将输入转换为 float 类型是更好的选择。
对于居中,我们通过 127.5 从数据集中减去。数据集中的值将现在介于 -127.5 到 127.5 之间。
对于缩放,我们通过数据集中最大像素值的一半,即 255/2,来除以居中的数据集。这将导致一个值范围介于 -1 和 1 之间的数据集:
# Converting integer values to float types
X_train = X_train.astype(np.float32)
X_test = X_test.astype(np.float32)
# Scaling and centering
X_train = (X_train - 127.5) / 127.5
X_test = (X_test - 127.5)/ 127.5
print('Maximum pixel value in the training_set after Centering and Scaling: ', np.max(X_train))
print('Minimum pixel value in the training_set after Centering and Scaling: ', np.min(X_train))
让我们定义一个函数,将缩放后的图像的像素值重新缩放到 0 到 255 之间:
# Rescale the pixel values (0 and 255)
def upscale(image):
return (image*127.5 + 127.5).astype(np.uint8)
# Lets see if this works
z = upscale(X_train[0])
print('Maximum pixel value after upscaling scaled image: ',np.max(z))
print('Maximum pixel value after upscaling scaled image: ',np.min(z))
Matplotlib 提示:需要进行缩放,以避免在使用未经放大的缩放图像时遇到 Matplotlib 错误。
放大后的 9 张居中和缩放后的图像:
for i in range(0, 9):
plt.subplot(331+i) # plot of 3 rows and 3 columns
plt.axis('off') # turn off axis
plt.imshow(upscale(X_train[i]), cmap='gray') # gray scale
输出结果如下:

图 13.4:放大后九个居中和缩放的 MNIST 数字图
掩蔽/插入噪声
根据本项目的需求,我们需要模拟一个不完整数字的数据集。因此,让我们编写一个函数,遮挡原始图像中的小区域,形成噪声数据集。
这里的想法是将图像的一个 8*8 区域进行遮挡,遮挡区域的左上角落在图像的第 9 到第 13 个像素之间(即在 x 和 y 轴上索引 8 到 12 之间)。目的是确保我们总是遮住图像的中心部分:
def noising(image):
array = np.array(image)
i = random.choice(range(8,12)) # x coordinate for the top left corner of the mask
j = random.choice(range(8,12)) # y coordinate for the top left corner of the mask
array[i:i+8, j:j+8]=-1.0 # setting the pixels in the masked region to -1
return array
noised_train_data = np.array([*map(noising, X_train)])
noised_test_data = np.array([*map(noising, X_test)])
print('Noised train data Shape/Dimension : ', noised_train_data.shape)
print('Noised test data Shape/Dimension : ', noised_train_data.shape)
蒙版的大小越大,MNIST 分类器预测正确数字的难度就越大。
随意尝试不同的遮挡区域大小,可以尝试更小或更大,也可以尝试不同的遮挡位置。
9 张经过放大处理的噪声图像的图:
# Plot of 9 scaled noised images after upscaling
for i in range(0, 9):
plt.subplot(331+i) # plot of 3 rows and 3 columns
plt.axis('off') # turn off axis
plt.imshow(upscale(noised_train_data[i]), cmap='gray') # gray scale
输出如下:

图 13.5:九个带噪声/遮挡的 MNIST 数字的图示
重塑
将原始数据集和噪声数据集重塑为 6000028281 的形状。这非常重要,因为 2D 卷积期望接收的图像形状是 2828*1:
# Reshaping the training data
X_train = X_train.reshape(X_train.shape[0], X_train.shape[1], X_train.shape[2], 1)
print('Size/Shape of the original training set: ', X_train.shape)
# Reshaping the noised training data
noised_train_data = noised_train_data.reshape(noised_train_data.shape[0],
noised_train_data.shape[1],
noised_train_data.shape[2], 1)
print('Size/Shape of the noised training set: ', noised_train_data.shape)
# Reshaping the testing data
X_test = X_test.reshape(X_test.shape[0], X_test.shape[1], X_test.shape[2], 1)
print('Size/Shape of the original test set: ', X_test.shape)
# Reshaping the noised testing data
noised_test_data = noised_test_data.reshape(noised_test_data.shape[0],
noised_test_data.shape[1],
noised_test_data.shape[2], 1)
print('Size/Shape of the noised test set: ', noised_test_data.shape)
如果你在 GPU 上进行多次训练,最好在每次训练后清理 GPU 空间,以确保下次训练能够高效执行,避免资源耗尽相关的错误,这在 GPU 上比较常见。可以使用以下代码来完成:
from keras import backend as k
k.clear_session()
MNIST 分类器
为了开始建模,让我们构建一个简单的卷积神经网络(CNN)。
数字分类器。
第一层是一个卷积层,包含 32 个 33 大小的滤波器,使用 relu 激活函数,并且使用 Dropout 作为正则化。第二层是一个卷积层,包含 64 个 33 大小的滤波器,使用 relu 激活函数,并且使用 Dropout 作为正则化。第三层是一个卷积层,包含 128 个 3*3 大小的滤波器,使用 relu 激活函数,并且使用 Dropout 作为正则化,最后进行展平处理。第四层是一个 Dense 层,包含 1024 个神经元,使用 relu 激活函数。最后一层是一个 Dense 层,包含 10 个神经元,对应于 MNIST 数据集中的 10 个类别,使用 softmax 激活函数,batch_size 设置为 128,使用的 optimizer 是 adam,validation_split 设置为 0.2。这意味着 20% 的训练集将作为验证集使用:
# input image shape
input_shape = (28,28,1)
def train_mnist(input_shape, X_train, y_train):
model = Sequential()
model.add(Conv2D(32, (3, 3), strides=2, padding='same',
input_shape=input_shape))
model.add(Activation('relu'))
model.add(Dropout(0.2))
model.add(Conv2D(64, (3, 3), strides=2, padding='same'))
model.add(Activation('relu'))
model.add(Dropout(0.2))
model.add(Conv2D(128, (3, 3), padding='same'))
model.add(Activation('relu'))
model.add(Dropout(0.2))
model.add(Flatten())
model.add(Dense(1024, activation = 'relu'))
model.add(Dense(10, activation='softmax'))
model.compile(loss = 'sparse_categorical_crossentropy',
optimizer = 'adam', metrics = ['accuracy'])
model.fit(X_train, y_train, batch_size = 128,
epochs = 3, validation_split=0.2, verbose = 1 )
return model
mnist_model = train_mnist(input_shape, X_train, y_train)
输出如下:

图 13.6:MNIST CNN 分类器训练三轮
使用已构建的 CNN 数字分类器在被遮挡的图像上进行测试,来评估其在缺少部分数字的图像上的表现:
# prediction on the masked images
pred_labels = mnist_model.predict_classes(noised_test_data)
print('The model model accuracy on the masked images is:',np.mean(pred_labels==y_test)*100)
在被遮挡的图像上,CNN 数字分类器的准确率为 74.9%。当你运行时,结果可能会略有不同,但应该会非常接近。
在前面的分类器中,我们没有使用最大池化(maxpooling)。尝试使用最大池化或其他池化选项构建相同的分类器。
定义 GAN 的超参数
以下是一些在代码中定义并将使用的超参数,完全可以配置:
# Smoothing value
smooth_real = 0.9
# Number of epochs
epochs = 5
# Batchsize
batch_size = 128
# Optimizer for the generator
optimizer_g = Adam(lr=0.0002, beta_1=0.5)
# Optimizer for the discriminator
optimizer_d = Adam(lr=0.0004, beta_1=0.5)
# Shape of the input image
input_shape = (28,28,1)
尝试不同的学习率、优化器、批大小和光滑值,观察这些因素如何影响模型的质量,如果得到更好的结果,展示给深度学习社区。
构建 GAN 模型组件
基于最终的 GAN 模型能够填充缺失(被遮蔽)图像部分的想法,让我们来定义生成器。
定义生成器
我们在这里使用的生成器是一个简单的卷积自编码器,它由两部分组成——编码器和解码器。
在编码器中,我们有以下内容:
-
第一层是一个卷积 2D 层,使用
32个 3x3 大小的滤波器,接着进行批归一化,激活函数为relu,然后是使用AveragePooling2D进行 2x2 大小的下采样。 -
第二层是一个卷积 2D 层,使用
64个 3x3 大小的滤波器,接着进行批归一化,激活函数为relu,然后是使用AveragePooling2D进行 2x2 大小的下采样。 -
第三层或该编码器部分的最终层仍然是一个卷积 2D 层,使用
128个 3x3 大小的滤波器,进行批归一化,激活函数为relu。
在解码器中,我们有以下内容:
-
第一层是一个卷积 2D 层,使用
128个 3x3 大小的滤波器,激活函数为relu,接着是使用UpSampling2D进行上采样。 -
第二层是一个卷积 2D 层,使用
64个 3x3 大小的滤波器,激活函数为relu,接着是使用UpSampling2D进行上采样。 -
第三层或该解码器部分的最终层仍然是一个卷积 2D 层,使用
1个 3x3 大小的滤波器,激活函数为tanh。
记住,在编码器中,如果你有32、64、128个滤波器,那么在解码器中应该跟随128、64、image_channels个滤波器。image_channels是输入图像的通道数,在 MNIST 数据集中为 1。如果编码器的第一、第二、第三和第四层有64、128、256、512个滤波器,那么解码器中的对应滤波器应该是256、128、64、image_channels。
def img_generator(input_shape):
generator = Sequential()
generator.add(Conv2D(32, (3, 3), padding='same', input_shape=input_shape)) # 32 filters
generator.add(BatchNormalization())
generator.add(Activation('relu'))
generator.add(AveragePooling2D(pool_size=(2, 2)))
generator.add(Conv2D(64, (3, 3), padding='same')) # 64 filters
generator.add(BatchNormalization())
generator.add(Activation('relu'))
generator.add(AveragePooling2D(pool_size=(2, 2)))
generator.add(Conv2D(128, (3, 3), padding='same')) # 128 filters
generator.add(BatchNormalization())
generator.add(Activation('relu'))
generator.add(Conv2D(128, (3, 3), padding='same')) # 128 filters
generator.add(Activation('relu'))
generator.add(UpSampling2D((2,2)))
generator.add(Conv2D(64, (3, 3), padding='same')) # 64 filters
generator.add(Activation('relu'))
generator.add(UpSampling2D((2,2)))
generator.add(Conv2D(1, (3, 3), activation='tanh', padding='same')) # 1 filter
return generator
关于生成器中最终卷积层,有两点需要记住。一是使用tanh作为激活函数,因为数据集的范围是-1 到 1,另一个是使用与输入图像通道数相同数量的滤波器。这是为了确保生成的图像与输入图像有相同数量的通道。
如果你决定像我们在本次练习中做的那样对数据进行居中和缩放,你需要在生成器中使用批归一化进行下采样,否则损失将无法收敛。你可以通过在没有批归一化层的情况下训练生成器,亲自见证不使用批归一化的效果。
在以下的生成器summary中,如果查看输出形状,您会看到网络前半部分是图像的降采样或压缩,后半部分是图像的放大:
# print generator summary
img_generator(input_shape).summary()
输出如下:

图 13.7:生成器概述(自动编码器)
在使用自动编码器时,如果没有得到好的结果,请考虑以下几点。首先使用AveragePooling2D,然后尝试MaxPooling2D进行降采样。先使用LeakyReLU,然后再尝试relu。除了最后一层的卷积层外,所有卷积层都使用LeakyReLU或relu激活函数。尝试使用更深的自动编码器。可以在卷积层中使用更多的滤波器,调整滤波器和池化层的大小。
定义判别器
判别器是一个简单的 CNN 二分类器,接收由生成器生成的图像,并尝试将其分类为原始图像或伪造图像。
第一层是一个卷积 2D 层,具有64个 3*3 大小的滤波器,激活函数为LeakyReLU,并使用Dropout作为正则化器。第二层和第三层与第一层相同,唯一不同的是第二层有128个滤波器,第三层有256个滤波器。最后一层是一个Dense层,使用sigmoid激活函数,因为我们正在进行二分类:
def img_discriminator(input_shape):
discriminator = Sequential()
discriminator.add(Conv2D(64, (3, 3), strides=2, padding='same', input_shape=input_shape, activation = 'linear'))
discriminator.add(LeakyReLU(0.2))
discriminator.add(Dropout(0.2))
discriminator.add(Conv2D(128, (3, 3), strides=2, padding='same', activation = 'linear'))
discriminator.add(LeakyReLU(0.2))
discriminator.add(Dropout(0.2))
discriminator.add(Conv2D(256, (3, 3), padding='same', activation = 'linear'))
discriminator.add(LeakyReLU(0.2))
discriminator.add(Dropout(0.2))
discriminator.add(Flatten())
discriminator.add(Dense(1, activation='sigmoid'))
return discriminator
# print summary of the discriminator
img_discriminator(input_shape).summary()
输出如下:

图 13.8:判别器概述
根据你需要解决的问题,调整判别器的参数。如果需要,可以在模型中加入MaxPooling层。
定义 DCGAN
以下函数将输入传递给生成器,然后是判别器,从而形成 DCGAN 架构:
def dcgan(discriminator, generator, input_shape):
# Set discriminator as non trainable before compiling GAN
discriminator.trainable = False
# Accepts the noised input
gan_input = Input(shape=input_shape)
# Generates image by passing the above received input to the generator
gen_img = generator(gan_input)
# Feeds the generated image to the discriminator
gan_output = discriminator(gen_img)
# Compile everything as a model with binary crossentropy loss
gan = Model(inputs=gan_input, outputs=gan_output)
return gan
如果你以前没有使用过Model函数 API,请访问 Keras 的详细文档,了解如何使用Model函数 API 并进行编译,链接:keras.io/models/model/。
训练 GAN
我们已经构建了 GAN 的各个组件。接下来,让我们开始训练模型吧!
绘制训练过程 - 第一部分
在每个 epoch 中,以下函数将绘制9张生成的图像。为了对比,它还将绘制对应的9张原始目标图像和9张带噪声的输入图像。绘制时我们需要使用已经定义的upscale函数,确保图像缩放到 0 到 255 之间,以避免绘图时出现问题:
def generated_images_plot(original, noised_data, generator):
print('NOISED')
for i in range(9):
plt.subplot(331 + i)
plt.axis('off')
plt.imshow(upscale(np.squeeze(noised_data[i])), cmap='gray') # upscale for plotting
plt.show()
print('GENERATED')
for i in range(9):
pred = generator.predict(noised_data[i:i+1], verbose=0)
plt.subplot(331 + i)
plt.axis('off')
plt.imshow(upscale(np.squeeze(pred[0])), cmap='gray') # upscale to avoid plotting errors
plt.show()
print('ORIGINAL')
for i in range(9):
plt.subplot(331 + i)
plt.axis('off')
plt.imshow(upscale(np.squeeze(original[i])), cmap='gray') # upscale for plotting
plt.show()
此函数的输出如下:

图 13.9:生成图像绘制函数的示例/预期输出
绘制训练过程 - 第二部分
让我们定义另一个函数,用于绘制每个 epoch 生成的图像。为了反映差异,我们还将在图中包含原始图像和带噪声/掩码的图像。
顶行是原始图像,中间行是遮罩图像,底行是生成的图像。
绘图有12行,顺序为:第 1 行 - 原始图像,第 2 行 - 遮罩图像,第 3 行 - 生成图像,第 4 行 - 原始图像,第 5 行 - 遮罩图像,...,第 12 行 - 生成图像。
让我们来看看相应的代码:
def plot_generated_images_combined(original, noised_data, generator):
rows, cols = 4, 12
num = rows * cols
image_size = 28
generated_images = generator.predict(noised_data[0:num])
imgs = np.concatenate([original[0:num], noised_data[0:num], generated_images])
imgs = imgs.reshape((rows * 3, cols, image_size, image_size))
imgs = np.vstack(np.split(imgs, rows, axis=1))
imgs = imgs.reshape((rows * 3, -1, image_size, image_size))
imgs = np.vstack([np.hstack(i) for i in imgs])
imgs = upscale(imgs)
plt.figure(figsize=(8,16))
plt.axis('off')
plt.title('Original Images: top rows, '
'Corrupted Input: middle rows, '
'Generated Images: bottom rows')
plt.imshow(imgs, cmap='gray')
plt.show()
输出如下:

图 13.10:来自plot_generated_images_combined函数的样本/预期输出
训练循环
现在我们来到了代码中最重要的部分;即之前定义的所有函数将会在这一部分使用。以下是步骤:
-
通过调用
img_generator()函数加载生成器。 -
通过调用
img_discriminator()函数加载判别器,并使用二元交叉熵损失和优化器optimizer_d进行编译,该优化器在超参数部分已定义。 -
将生成器和判别器输入到
dcgan()函数中,并使用二元交叉熵损失和优化器optimizer_g进行编译,该优化器在超参数部分已定义。 -
创建一批新的原始图像和遮罩图像。通过将这批遮罩图像输入生成器,生成新的假图像。
-
将原始图像和生成的图像拼接,使得前 128 张图像为原始图像,接下来的 128 张为假图像。重要的是,这里不要打乱数据,否则训练会变得困难。将生成的图像标记为
0,将原始图像标记为0.9,而不是 1。 这就是对原始图像进行的单边标签平滑。使用标签平滑的原因是使网络能够抵抗对抗样本。这是单边标签平滑,因为我们只对真实图像进行标签平滑。 -
设置
discriminator.trainable为True,以启用判别器的训练,并将这 256 张图像及其相应的标签输入判别器进行分类。 -
现在,将
discriminator.trainable设置为False,并将新的 128 张标记为 1 的遮罩图像输入 GAN(DCGAN)进行分类。将discriminator.trainable设置为False非常重要,以确保在训练生成器时,判别器不会参与训练。 -
重复步骤 4 至 7,直到达到期望的训练轮次。
这里使用的批量大小为 128。
我们已经将plot_generated_images_combined()函数和generated_images_plot()函数放置在一起,以便在第一轮的第一次迭代和每轮结束后,通过这两个函数生成一个绘图。
根据你需要显示图像的频率,随意放置这些绘图函数:
def train(X_train, noised_train_data,
input_shape, smooth_real,
epochs, batch_size,
optimizer_g, optimizer_d):
# define two empty lists to store the discriminator
# and the generator losses
discriminator_losses = []
generator_losses = []
# Number of iteration possible with batches of size 128
iterations = X_train.shape[0] // batch_size
# Load the generator and the discriminator
generator = img_generator(input_shape)
discriminator = img_discriminator(input_shape)
# Compile the discriminator with binary_crossentropy loss
discriminator.compile(loss='binary_crossentropy',optimizer=optimizer_d)
# Feed the generator and the discriminator to the function dcgan
# to form the DCGAN architecture
gan = dcgan(discriminator, generator, input_shape)
# Compile the DCGAN with binary_crossentropy loss
gan.compile(loss='binary_crossentropy', optimizer=optimizer_g)
for i in range(epochs):
print ('Epoch %d' % (i+1))
# Use tqdm to get an estimate of time remaining
for j in tqdm(range(1, iterations+1)):
# batch of original images (batch = batchsize)
original = X_train[np.random.randint(0, X_train.shape[0], size=batch_size)]
# batch of noised images (batch = batchsize)
noise = noised_train_data[np.random.randint(0, noised_train_data.shape[0], size=batch_size)]
# Generate fake images
generated_images = generator.predict(noise)
# Labels for generated data
dis_lab = np.zeros(2*batch_size)
# data for discriminator
dis_train = np.concatenate([original, generated_images])
# label smoothing for original images
dis_lab[:batch_size] = smooth_real
# Train discriminator on original images
discriminator.trainable = True
discriminator_loss = discriminator.train_on_batch(dis_train, dis_lab)
# save the losses
discriminator_losses.append(discriminator_loss)
# Train generator
gen_lab = np.ones(batch_size)
discriminator.trainable = False
sample_indices = np.random.randint(0, X_train.shape[0], size=batch_size)
original = X_train[sample_indices]
noise = noised_train_data[sample_indices]
generator_loss = gan.train_on_batch(noise, gen_lab)
# save the losses
generator_losses.append(generator_loss)
if i == 0 and j == 1:
print('Iteration - %d', j)
generated_images_plot(original, noise, generator)
plot_generated_images_combined(original, noise, generator)
print("Discriminator Loss: ", discriminator_loss,\
", Adversarial Loss: ", generator_loss)
# training plot 1
generated_images_plot(original, noise, generator)
# training plot 2
plot_generated_images_combined(original, noise, generator)
# plot the training losses
plt.figure()
plt.plot(range(len(discriminator_losses)), discriminator_losses,
color='red', label='Discriminator loss')
plt.plot(range(len(generator_losses)), generator_losses,
color='blue', label='Adversarial loss')
plt.title('Discriminator and Adversarial loss')
plt.xlabel('Iterations')
plt.ylabel('Loss (Adversarial/Discriminator)')
plt.legend()
plt.show()
return generator
generator = train(X_train, noised_train_data,
input_shape, smooth_real,
epochs, batch_size,
optimizer_g, optimizer_d)
输出如下:


图 13.11.1:在第 1 轮的第一次迭代结束时,绘制的生成图像与训练图像一起显示


图 13.11.2:第 2 轮训练结束时,生成的图像与训练图表的结合


图 13.11.3:第 5 轮训练结束时,生成的图像与训练图表的结合

图 13.12:训练过程中判别器与对抗损失的变化图
尝试调整生成器和判别器的学习率,找出最适合你使用场景的最优值。通常,在训练 GAN 时,你需要训练多个轮次,然后使用前述的损失与迭代次数图来找到你希望训练停止时的最小点。
预测
这就是我们一直在构建的目标:进行预测!
CNN 分类器对噪声和生成图像的预测
现在,我们将在遮蔽的 MNIST 测试数据上调用生成器生成图像,也就是填补数字缺失的部分:
# restore missing parts of the digit with the generator
gen_imgs_test = generator.predict(noised_test_data)
然后,我们将把生成的 MNIST 数字传递给已经建好的数字分类器:
# predict on the restored/generated digits
gen_pred_lab = mnist_model.predict_classes(gen_imgs_test)
print('The model model accuracy on the generated images is:',np.mean(gen_pred_lab==y_test)*100)
MNIST CNN 分类器在生成数据上的准确率为 87.82%。
以下是一个图表,展示了生成器生成的 10 张图像、生成图像的实际标签以及经过处理后由数字分类器预测的标签:
# plot of 10 generated images and their predicted label
fig=plt.figure(figsize=(8, 4))
plt.title('Generated Images')
plt.axis('off')
columns = 5
rows = 2
for i in range(0, rows*columns):
fig.add_subplot(rows, columns, i+1)
plt.title('Act: %d, Pred: %d'%(gen_pred_lab[i],y_test[i])) # label
plt.axis('off') # turn off axis
plt.imshow(upscale(np.squeeze(gen_imgs_test[i])), cmap='gray') # gray scale
plt.show()
输出结果如下:

图 13.13:MNIST 分类器对生成图像的预测图
模块化形式的脚本
整个脚本可以分为四个模块,分别为 train_mnist.py、training_plots.py、GAN.py 和 train_gan.py。将这些文件保存在你选择的文件夹中,例如 gan。将 gan 设为项目文件夹,然后在你喜欢的源代码编辑器中运行 train_gan.py 文件。
train_gan.py Python 文件将从其他模块导入函数,在需要执行的地方调用它们。
现在,让我们逐一浏览每个文件的内容。
模块 1 – train_mnist.py
这个 Python 文件包含了我们之前用来训练 MNIST 数字 CNN 分类器的 train_mnist() 函数:
"""This module is used to train a CNN on mnist."""
from keras.layers import Conv2D
from keras.layers import Flatten, Activation
from keras.models import Sequential
from keras.layers.core import Dense, Dropout
def train_mnist(input_shape, X_train, y_train):
"""Train CNN on mnist data."""
model = Sequential()
model.add(Conv2D(32, (3, 3), strides=2, padding='same',
input_shape=input_shape))
model.add(Activation('relu'))
model.add(Dropout(0.2))
model.add(Conv2D(64, (3, 3), strides=2, padding='same'))
model.add(Activation('relu'))
model.add(Dropout(0.2))
model.add(Conv2D(128, (3, 3), padding='same'))
model.add(Activation('relu'))
model.add(Dropout(0.2))
model.add(Flatten())
model.add(Dense(1024, activation='relu'))
model.add(Dense(10, activation='softmax'))
model.compile(loss='sparse_categorical_crossentropy',
optimizer='adam', metrics=['accuracy'])
model.fit(X_train, y_train, batch_size=128,
epochs=3, validation_split=0.2, verbose=1)
return model
模块 2 – training_plots.py
这个 Python 文件包含了四个函数:upscale()、generated_images_plot()、plot_generated_images_combined() 和 plot_training_loss():
"""This module contains functions to plot image generated when training GAN."""
import matplotlib.pyplot as plt
import numpy as np
def upscale(image):
"""Scale the image to 0-255 scale."""
return (image*127.5 + 127.5).astype(np.uint8)
def generated_images_plot(original, noised_data, generator):
"""Plot subplot of images during training."""
print('NOISED')
for i in range(9):
plt.subplot(331 + i)
plt.axis('off')
plt.imshow(upscale(np.squeeze(noised_data[i])), cmap='gray')
plt.show()
print('GENERATED')
for i in range(9):
pred = generator.predict(noised_data[i:i+1], verbose=0)
plt.subplot(331 + i)
plt.axis('off')
plt.imshow(upscale(np.squeeze(pred[0])), cmap='gray')
plt.show()
若要查看代码的其余部分,请访问:github.com/PacktPublishing/Python-Deep-Learning-Projects/blob/master/Chapter13/training_plots.py
模块 3 – GAN.py
该模块包含了 DCGAN 组件,即 img_generator()、img_discriminator() 和 dcgan():
"""This module contains the DCGAN components."""
from keras.layers import Input, Conv2D, AveragePooling2D
from keras.layers import UpSampling2D, Flatten, Activation, BatchNormalization
from keras.models import Model, Sequential
from keras.layers.core import Dense, Dropout
from keras.layers.advanced_activations import LeakyReLU
def img_generator(input_shape):
"""Generator."""
generator = Sequential()
generator.add(Conv2D(32, (3, 3), padding='same', input_shape=input_shape))
generator.add(BatchNormalization())
generator.add(Activation('relu'))
generator.add(AveragePooling2D(pool_size=(2, 2)))
generator.add(Conv2D(64, (3, 3), padding='same'))
generator.add(BatchNormalization())
generator.add(Activation('relu'))
generator.add(AveragePooling2D(pool_size=(2, 2)))
generator.add(Conv2D(128, (3, 3), padding='same'))
generator.add(BatchNormalization())
generator.add(Activation('relu'))
generator.add(Conv2D(128, (3, 3), padding='same'))
generator.add(Activation('relu'))
generator.add(UpSampling2D((2, 2)))
generator.add(Conv2D(64, (3, 3), padding='same'))
generator.add(Activation('relu'))
generator.add(UpSampling2D((2, 2)))
generator.add(Conv2D(1, (3, 3), activation='tanh', padding='same'))
return generator
对于此代码的其余部分,请访问:github.com/PacktPublishing/Python-Deep-Learning-Projects/blob/master/Chapter13/GAN.py
模块 4 – train_gan.py
在本模块中,我们将包括超参数,预处理数据,生成合成数据,训练 GAN,训练 CNN 分类器,并从其他模块导入所有必要的函数:
import numpy as np
from training_plots import upscale, generated_images_plot, plot_training_loss
from training_plots import plot_generated_images_combined
from keras.optimizers import Adam
from keras import backend as k
import matplotlib.pyplot as plt
from tqdm import tqdm
from GAN import img_generator, img_discriminator, dcgan
from keras.datasets import mnist
from train_mnist import train_mnist
%matplotlib inline
# Smoothing value
smooth_real = 0.9
# Number of epochs
epochs = 5
# Batchsize
batch_size = 128
# Optimizer for the generator
optimizer_g = Adam(lr=0.0002, beta_1=0.5)
# Optimizer for the discriminator
optimizer_d = Adam(lr=0.0004, beta_1=0.5)
# Shape of the input image
input_shape = (28, 28, 1)
对于本模块的其余部分,请访问:github.com/PacktPublishing/Python-Deep-Learning-Projects/blob/master/Chapter13/train_gan.py
你可以使用你创建的相同模块来训练时尚 MNIST 数据。你需要做的就是将train_gan.py文件中的第 11 行替换为(from keras.datasets import fashion_mnist),并将第 28 行替换为((X_train, y_train), (X_test, y_test) = fashion_mnist.load_data())。结果会很好,但不会非常出色,因为这里设置的参数在 MNIST 数字数据上表现最佳。这将是一个很好的练习,你可以在不费力的情况下获得令人难以置信的结果。
这里有一些关于训练 GAN 的技巧资源,你一定要查看:
前述 DCGAN MNIST 修复的 Jupyter Notebook 代码文件可以在github.com/PacktPublishing/Python-Deep-Learning-Projects/blob/master/Chapter%2014/DCGAN_MNIST.ipynb找到。DCGAN 时尚 MNIST 修复的 Jupyter Notebook 代码文件可以在github.com/PacktPublishing/Python-Deep-Learning-Projects/blob/master/Chapter%2014/DCGAN_Fashion_MNIST.ipynb找到。
项目的结论
本项目的目标是构建一个 GAN,解决手写数字缺失部分/区域重生的问题。在最初的章节中,我们应用深度学习技术,使餐饮连锁店的顾客可以通过一个简单的 iPad 应用写下他们的电话号码,以便收到通知,提示他们的座位已准备好。本章节的使用案例是应用深度学习技术生成电话号码中缺失的数字部分,从而能将文本通知发送给正确的人。
CNN 数字分类器在 MNIST 验证数据上的准确率达到了 98.84%。使用我们生成的数据来模拟数字缺失部分时,输入到 CNN 数字分类器中时,模型的准确率仅为 74.90%。
相同的缺失部分数字数据被传递给生成器,以恢复丢失的部分。然后,生成的数字被传递给 CNN 分类器,模型的准确率为 87.82%。看看你能否调整 CNN 分类器和 GAN,生成更清晰的数字,以及大幅提高这些生成图像的准确性。
让我们沿用之前章节中评估模型表现的相同方法,从餐饮连锁的角度进行分析。
这种准确性有什么意义呢?我们来计算一下错误发生的几率,错误导致客户服务问题的情况(也就是说,客户没有收到他们的桌子准备好的通知,反而因为餐厅等待时间过长而感到不满)。
每个客户的电话号码由十位数字组成。假设我们假设的餐厅在每个地点平均有 30 张桌子,这些桌子在高峰时段,每晚翻台两次,并且餐厅连锁有 35 个地点。这意味着每天的运营中大约会捕获 21,000 个手写数字(30 张桌子 × 每天 2 次翻台 × 35 个地点 × 10 位数字的电话号码)。
显然,所有数字必须正确分类,才能确保文本通知发送到正确的等待餐厅顾客。因此,任何一个数字的错误分类都会导致失败。在模拟数据上,模型准确率为 74.90%,意味着总共有 5,271 个数字被误分类。通过从训练好的 GAN 的生成器中恢复的数据(基于模拟数据),模型的准确率为 87.82%,这意味着在我们的例子中每天会错误分类 2,558 个数字。假设最坏的情况是每个电话号码中只发生一个错误分类数字。那么,考虑到只有 2,100 个顾客及相应的电话号码,这就意味着每个电话号码都会有一个分类错误(100%的失败率),没有一个顾客能收到通知,知道他们的聚会可以入座!最好的情况是每个电话号码中的 10 个数字都被误分类,这将导致 2,100 个电话号码中有 263 个错误(12.5%的失败率)。这仍然不是餐饮连锁可能满意的表现水平,因此你可以看到为什么我们需要继续微调模型,以获得可能的最大性能。
总结
在本章的项目中,我们成功地在 Keras 中构建了一个深度卷积 GAN,应用于手写的 MNIST 数字。我们了解了 GAN 中生成器和判别器组件的功能,定义了一些关键的超参数,并且在某些地方解释了我们为何使用这些参数。最后,我们在未见过的数据上测试了 GAN 的表现,并确定我们达成了预期目标。
第十四章:开发一个深度强化学习的自主代理
欢迎来到强化学习这一章节。在之前的章节中,我们已经解决了监督学习的问题。在本章中,我们将学习如何构建和训练一个能够玩游戏的深度强化学习模型。
强化学习通常是深度学习工程师接触到的一种新范式,这也是我们选择用游戏框架进行本次训练的原因。我们需要关注的业务应用场景通常涉及过程优化。强化学习在游戏中表现出色,但也适用于从无人机控制(arxiv.org/pdf/1707.05110.pdf)和导航到优化移动网络文件下载(anrg.usc.edu/www/papers/comsnets_2017.pdf)等各种应用场景。
我们将通过深度 Q 学习和深度状态-动作-奖励-状态-动作(SARSA)学习来实现这一点。我们的想法是构建一个深度学习模型,在强化学习术语中也称为代理(Agent),它与游戏环境互动,并在多次游戏尝试后学习如何玩游戏,同时最大化奖励。这里是一个强化学习的示意图:

图 14.1:强化学习
本章中,我们将使用 OpenAI Gym 的倒立摆游戏(CartPole)。
本章我们将学习以下内容:
-
如何与 Gym 工具包进行交互
-
什么是 Q 学习和 SARSA 学习
-
编写强化学习模型并定义超参数
-
构建和理解训练循环
-
测试模型
最好在本章进行时就实现代码片段,无论是在 Jupyter Notebook 还是任何源代码编辑器中。这将使你更容易跟上进度,并理解代码的每一部分所做的事情。
本章的所有 Python 和 Jupyter Notebook 文件可以在github.com/PacktPublishing/Python-Deep-Learning-Projects/tree/master/Chapter14找到。
让我们开始编码吧!
在这个练习中,我们将使用来自 OpenAI 的 Gym 工具包来开发强化学习模型。它支持像倒立摆(CartPole)和弹球(Pinball)这样的游戏教学。
要了解更多关于 OpenAI Gym 工具包及其支持的游戏,请访问gym.openai.com/。
我们还将使用 Keras 深度学习库,它是一个高级神经网络 API,能够运行在 TensorFlow、Theano 或认知工具包(CNTK)之上。
要了解更多关于 Keras 及其功能的信息,请访问keras.io/。
深度 Q 学习
在这一部分中,我们将实现深度 Q 学习,并使用 Keras 深度学习库构建的深度学习模型作为函数近似器。
我们将从如何使用 Gym 模块的简单介绍开始,然后继续了解什么是 Q 学习,最后实现深度 Q 学习。我们将使用 OpenAI Gym 中的 CartPole 环境。
为了跟进,参考 Jupyter Notebook 代码文件中的深度 Q 学习部分:github.com/PacktPublishing/Python-Deep-Learning-Projects/blob/master/Chapter%2015/DQN.ipynb。
导入所有依赖
在本节练习中,我们将使用 numpy、gym、matplotlib、keras 和 tensorflow 包。在这里,TensorFlow 将作为 Keras 的后端。你可以使用 pip 安装这些包:
import random
import numpy as np
import matplotlib.pyplot as plt
from keras.layers import Dense, Dropout, Activation
from keras.models import Sequential
from keras.optimizers import Adam
from keras import backend as k
from collections import deque
import gym
deque 是一个类似列表的容器,可以在两端快速追加和弹出元素。
探索 CartPole 游戏
在 CartPole 游戏中,你会看到一根通过未固定的关节连接到小车的杆,杆在无摩擦的轨道上移动。在每一局游戏开始时,杆会处于竖直位置,目标是尽可能长时间地保持杆在竖直位置,或者保持给定的时间步数。你可以通过施加 +1 和 -1 的力(分别使小车向右或向左移动)来控制 CartPole 系统,防止杆倒下。游戏/回合结束的条件是小车从中心位置移动超过 2.4 个单位,或者杆与竖直方向的夹角超过 45 度。
与 CartPole 游戏互动
OpenAI Gym 使得与游戏交互变得非常简单。在本节中,我们将介绍如何加载、重置并玩 CartPole 游戏。
加载游戏
让我们从 gym 模块加载 CartPole-v1 游戏。非常简单,你只需要将游戏名称传递给 gym.make() 函数。在我们的例子中,游戏是 CartPole-v1。然后 Gym 会将游戏加载到你的工作空间中:
env = gym.make('CartPole-v1')
设置 seed 以保证结果可复现非常重要:
# Set seed for reproducibility
seed_val = 456
np.random.seed(seed_val)
env.seed(seed_val)
random.seed(seed_val)
让我们探索一下在 CartPole 游戏中有哪些变量:
states = env.observation_space.shape[0]
print('Number of states/variables in the cartpole environment', states)
以下是输出:

我们可以看到 CartPole 游戏有 4 个变量,分别是位置(x)、速度(x_dot)、角度位置(theta)和角速度(theta_dot)。
让我们探索一下在这个游戏中我们有多少种可能的响应,使用以下代码:
actions = env.action_space.n
print('Number of responses/classes in the cartpole environment', actions)
以下是输出:

我们看到 CartPole 环境有 2 种可能的响应/按钮,即向左移动和向右移动。
重置游戏
你可以使用以下代码重置游戏:
state = env.reset() # reset the game
print('State of the Cart-Pole after reset', state)
print('Shape of state of the Cart-Pole after reset', state.shape)
上面的代码片段将重置游戏,并返回重置后 CartPole 的状态(x、x_dot、theta、theta_dot),该状态将是形状为 (4,) 的数组。
玩游戏
现在,一旦你重置了游戏,接下来就是玩游戏。你可以使用以下代码将你的动作/响应输入到游戏中:
action = 0
new_state, reward, done, info = env.step(action)
print((new_state, reward, done, info))
env.step 函数接受你的响应/动作(向左或向右移动),并生成 CartPole 系统的 new_state/方向(x, x_dot, theta, theta_dot)。随着新状态的生成,env.step 函数还返回 reward,即你刚才采取的 action 得到的分数;done,指示游戏是否结束;以及 info,包含系统相关信息。
当游戏开始时,done 被设置为 False。只有当 CartPole 的方向超出游戏规则时,done 才会被设置为 True,这表示要么小车已从中心位置移动了 2.4 单位,要么杆子与垂直方向的夹角超过了 45 度。
只要你所采取的每一步都在游戏规则范围内,那么该步骤的奖励为 1 单位,否则为零。
让我们通过进行随机动作来玩这个游戏:
def random_actions_game(episodes):
for episode in range(episodes):
state = env.reset() # reset environment
done = False # set done to False
score = 0
while not done:
#env.render() # Display cart pole game on the screen
action = random.choice([0,1]) # Choose between 0 or 1
new_state, reward, done, info = env.step(action) # perform the action
score+=1
print('Episode: {} Score: {}'.format(episode+1, score))
# play game
random_actions_game(10)
以下是终端输出:

图 14.2:随机动作游戏的得分
以下是 CartPole 游戏的输出:

图 14.3:渲染时显示的 CartPole 游戏快照
random.choice 从非空序列(如列表/数组)中返回随机选择的项。
Q-learning
Q-learning 是一种基于策略的强化学习技术,Q-learning 的目标是学习一种最优策略,帮助智能体在环境的不同状态下决定采取什么行动。
要实现 Q-learning,你需要了解什么是 Q 函数。
一个 Q 函数接受一个状态和相应的动作作为输入,并返回总期望奖励。它可以表示为 Q(s, a)。当处于 s 状态时,最优的 Q 函数会告诉智能体选择某个动作 a 的优劣。
对于单一状态 s 和动作 a,Q(s, a) 可以通过以下公式表示为下一个状态 s' 的 Q 值:

这就是著名的贝尔曼方程。它告诉我们,最大奖励是智能体进入当前状态 s 所得到的奖励和下一个状态 s' 的折扣后最大未来奖励之和。
以下是《强化学习:导论》一书中 Q-learning 算法的伪代码,作者是 Richard S. Sutton 和 Andrew G. Barto:

图 14.4:Q-learning 的伪代码
《强化学习:导论》一书,由 Richard S. Sutton 和 Andrew G. Barto 编著 ( incompleteideas.net/book/ebook/the-book.html)。
定义深度 Q 学习(DQN)的超参数
以下是我们在代码中使用的一些超参数,这些超参数完全可配置:
# Discount in Bellman Equation
gamma = 0.95
# Epsilon
epsilon = 1.0
# Minimum Epsilon
epsilon_min = 0.01
# Decay multiplier for epsilon
epsilon_decay = 0.99
# Size of deque container
deque_len = 20000
# Average score needed over 100 epochs
target_score = 200
# Number of games
episodes = 2000
# Data points per episode used to train the agent
batch_size = 64
# Optimizer for training the agent
optimizer = 'adam'
# Loss for training the agent
loss = 'mse'
以下是所使用的参数:
-
gamma:贝尔曼方程中的折扣参数 -
epsilon_decay:你希望在每一局/游戏后按比例折扣epsilon的值。 -
epsilon_min:epsilon的最小值,低于该值后不再衰减。 -
deque_len:deque容器的大小,用于存储训练示例(包括状态、奖励、完成标志和动作)。 -
target_score:你希望代理在 100 个训练周期内达到的平均得分,达到该分数后停止学习过程。 -
episodes:你希望代理玩的最大游戏次数。 -
batch_size:用于训练代理的批量数据大小(存储在deque容器中),每一局游戏后使用这些数据来训练代理。 -
optimizer:用于训练代理的优化器 -
loss:用于训练代理的损失函数
尝试不同的学习率、优化器、批次大小以及epsilon_decay值,看看这些因素如何影响模型的质量。如果得到更好的结果,分享给深度学习社区。
构建模型组件
在本节中,我们将定义所有用于训练强化学习代理的函数。这些函数如下:
-
代理
-
代理动作
-
记忆
-
性能图
-
回放
-
训练和测试,用于训练和测试代理
定义代理
让我们定义一个代理/函数近似器。
代理其实就是一个简单的深度神经网络,输入的是 CartPole 系统的状态(四个变量),输出的是每个动作的最大可能奖励。
第一、第二和第三层是简单的Dense层,具有 16 个神经元,激活函数为relu。
最后一层是一个Dense层,具有两个神经元,等于可能的actions数量:
def agent(states, actions):
"""Simple Deep Neural Network."""
model = Sequential()
model.add(Dense(16, input_dim=states))
model.add(Activation('relu'))
model.add(Dense(16))
model.add(Activation('relu'))
model.add(Dense(16))
model.add(Activation('relu'))
model.add(Dense(actions))
model.add(Activation('linear'))
return model
# print summary of the agent
print(agent(states, actions).summary())
以下是输出:

图 14.5:代理摘要
根据你要解决的问题调整代理的参数。必要时可以在模型中尝试使用泄漏relu。
定义代理动作
让我们定义一个函数,当被调用时,将返回该特定状态下需要采取的动作:
def agent_action(model, epsilon, state, actions):
"""Define action to be taken."""
if np.random.rand() <= epsilon:
act = random.randrange(actions)
else:
act = np.argmax(model.predict(state)[0])
return act
对于来自均匀分布(介于 0 和 1 之间)的任何值,如果小于或等于epsilon,返回的动作将是random。对于任何大于epsilon的值,选择的动作将是我们在前面的代码中定义的代理预测的动作。
numpy.random.rand函数从 0 到 1 的均匀分布中生成一个随机数。numpy.argmax返回序列中最大值的索引。random.randrange从range()中随机选择一个项目。
定义记忆
让我们定义一个deque对象,用来存储与每个相关步骤(如游戏过程中的state、action、reward和done)相关的信息。我们将使用存储在这个deque对象中的数据进行训练:
training_data = deque(maxlen=deque_len)
我们将deque对象的大小定义为20000。一旦该容器填满 20,000 个数据点,每当在一端添加新数据时,另一端的数据点会被弹出。然后,我们将只保留最新的信息。
我们将定义一个名为memory的函数,在游戏中调用时,它会在该时间步接受与action、state、reward和done相关的信息作为输入,然后将其存储在我们在前面代码中定义的训练数据deque容器中。你会看到,我们在每个时间步将这五个变量作为元组条目进行存储:
def memory(state, new_state, reward, done, action):
"""Function to store data points in the deque container."""
training_data.append((state, new_state, reward, done, action))
定义性能图
以下performance_plot函数绘制模型的性能随时间变化的图像。这个函数被放置在只有当我们达成 200 分的目标时才会绘制。你也可以将这个函数放置在每训练 100 个回合后绘制进度:
def performance_plot(scores, target_score):
"""Plot the game progress."""
scores_arr = np.array(scores) # convert list to array
scores_arr[np.where(scores_arr > target_score)] = target_score # scores
plt.figure(figsize=(20, 5)) # set figure size to 20 by 5
plt.title('Plot of Score v/s Episode') # title
plt.xlabel('Episodes') # xlabel
plt.ylabel('Scores') # ylabel
plt.plot(scores_arr)
plt.show()
以下是函数(在目标达成后)示例图输出的截图:

图 14.6:性能图函数的示例输出
定义 replay
以下replay函数会在游戏结束时,在train函数(在下一节定义)内部被调用,用于训练代理。在这个函数中,我们使用Q函数贝尔曼方程来定义每个状态的目标:
def replay(epsilon, gamma, epsilon_min, epsilon_decay, model, training_data, batch_size=64):
"""Train the agent on a batch of data."""
idx = random.sample(range(len(training_data)), min(len(training_data), batch_size))
train_batch = [training_data[j] for j in idx]
for state, new_state, reward, done, action in train_batch:
target = reward
if not done:
target = reward + gamma * np.amax(model.predict(new_state)[0])
#print('target', target)
target_f = model.predict(state)
#print('target_f', target_f)
target_f[0][action] = target
#print('target_f_r', target_f)
model.fit(state, target_f, epochs=1, verbose=0)
if epsilon > epsilon_min:
epsilon *= epsilon_decay
return epsilon
正是在这个函数中,我们训练代理使用均方误差损失来学习最大化奖励。我们这样做是因为我们在预测两个动作的奖励的数值。记住,代理将状态作为输入,状态的形状是 14。该代理的输出形状是 12,它基本上包含了两个可能动作的期望奖励。
所以,当一个回合结束时,我们使用存储在deque容器中的一批数据来训练代理。
在这批数据中,考虑第 1 个元组:
state = [[-0.07294358 -0.94589796 0.03188364 1.40490844]]
new_state = [[-0.09186154 -1.14140094 0.05998181 1.70738606]]
reward = 1
done = False
action = 0
对于state,我们知道需要采取的action以进入new_state,以及为此所获得的reward。我们还有done,它表示进入的new_state是否符合游戏规则。
只要进入的新状态,s',符合游戏规则,即done为False,根据贝尔曼方程,进入新状态s'通过采取action从状态s过渡的总reward可以在 Python 中写为如下:
target = reward + gamma * np.amax(model.predict(new_state)[0])
model.predict(new_state)[0]的输出为[-0.55639267, 0.37972435]。np.amax([-0.55639267, 0.37972435])的结果为0.37972435。
在折扣/gamma为 0.95 和reward为1时,得到如下值。reward + gamma * np.amax(model.predict(new_state)[0])的结果为1.36073813587427。
这是先前定义的目标值。
使用模型,我们预测当前状态下两种可能动作的奖励。target_f = model.predict(state) 将返回 [[-0.4597198 0.31523475]]。
由于我们已经知道需要采取的 action,即 0,以最大化下一个状态的奖励,我们将 target_f 中索引为零的 reward 设置为使用贝尔曼方程计算得到的 reward,即 target_f[0][action] = 1.3607381358742714。
最终,target_f 将等于 [[1.3607382 0.31523475]]。
我们将使用状态作为 input,target_f 作为目标奖励,并根据它来训练代理/模型。
这个过程将对训练数据批次中的所有数据点重复执行。此外,每次调用回放函数时,epsilon 的值会根据衰减因子减少。
random.sample 从一个集合中随机抽取 n 个元素。np.amax 返回数组中的最大值。
训练循环
现在,让我们将到目前为止形成的所有部分结合起来,使用我们在此定义的 train() 函数实现代理的训练:
-
通过调用
agent()函数加载代理,并将其与损失函数loss和优化器optimizer编译,这些内容我们已在 定义深度 Q 学习(DQN)超参数 章节中定义。 -
重置环境并调整初始状态的形状。
-
调用
agent_action函数,传入model、epsilon和state信息,获取需要采取的下一个动作。 -
使用
env.step函数获取在 Step 3 中获得的动作。通过调用memory函数并传递必要的参数,将结果信息存储在training_data双端队列容器中。 -
将在 Step 4 中获得的新状态分配给
state变量,并将时间步长增加 1 单位。 -
直到 Step 4 返回
True,重复 Step 3 到 Step 5。 -
在每次回合/游戏结束时,调用
replay函数,在一批训练数据上训练代理。 -
重复 Step 2 到 Step 7,直到达到目标分数:
以下代码展示了 train() 函数的实现:
def train(target_score, batch_size, episodes,
optimizer, loss, epsilon,
gamma, epsilon_min, epsilon_decay, actions, render=False):
"""Training the agent on games."""
print('----Training----')
k.clear_session()
# define empty list to store the score at the end of each episode
scores = []
# load the agent
model = agent(states, actions)
# compile the agent with mean squared error loss
model.compile(loss=loss, optimizer=optimizer)
for episode in range(1, (episodes+1)):
# reset environment at the end of each episode
state = env.reset()
# reshape state to shape 1*4
state = state.reshape(1, states)
# set done value to False
done = False
有关此代码片段的其余部分,请参阅此处的 DQN.ipynb 文件:github.com/PacktPublishing/Python-Deep-Learning-Projects/blob/master/Chapter14/DQN.ipynb
若要在训练时在屏幕上查看 CartPole 游戏,请将 train 函数中的 render 参数设置为 True。另外,游戏可视化会减慢训练速度。
以下两张图片是 DQN 训练过程中生成的输出:

图 14.7:训练代理时的分数输出

图 14.8:训练代理时分数与回合的关系图
我们可以看到,在训练智能体时,我们设定的 200 分目标在300场游戏结束时达到了,且这个分数是在最近100场游戏中平均计算得出的。
我们一直在使用ε-greedy 策略来训练智能体。一旦你掌握了 DQN 的训练过程,可以尝试使用github.com/keras-rl/keras-rl/blob/master/rl/policy.py中列出的其他策略。
训练智能体时,不一定每次都能在 300 场游戏内完成。有时可能需要超过 300 场游戏。你可以参考这个笔记本:github.com/PacktPublishing/Python-Deep-Learning-Projects/blob/master/Chapter%2015/DQN.ipynb,查看训练智能体的五次尝试以及每次训练所用的游戏回合数。
测试 DQN 模型
现在,让我们测试我们训练的 DQN 模型在新游戏中的表现。以下test函数使用训练好的 DQN 模型进行十场游戏,看看我们设定的 200 分的目标能否达成:
def test(env, model, states, episodes=100, render=False):
"""Test the performance of the DQN agent."""
scores_test = []
for episode in range(1, (episodes+1)):
state = env.reset()
state = state.reshape(1, states)
done = False
time_step = 0
while not done:
if render:
env.render()
action = np.argmax(model.predict(state)[0])
new_state, reward, done, info = env.step(action)
new_state = new_state.reshape(1, states)
state = new_state
time_step += 1
scores_test.append(time_step)
if episode % 10 == 0:
print('episode {}, score {} '.format(episode, time_step))
print('Average score over 100 test games: {}'.format(np.mean(scores_test)))
test(env, model, states, render=False)
要在测试时查看 CartPole 游戏画面,请在test函数内将render参数设置为true。
以下是输出结果:

图 14.9:使用训练好的 Q 智能体测试得分
当智能体在新的 100 场 CartPole 游戏中进行测试时,它的平均得分为277.88。
移除 200 分的门槛,目标是训练智能体始终保持平均得分为 450 分或更多。
深度 Q 学习脚本模块化形式
整个脚本可以分为四个模块,分别是train_dqn.py、agent_reply_dqn.py、test_dqn.py和hyperparameters_dqn.py。将它们存储在你选择的文件夹中,例如chapter_15。将chapter_15设置为你喜欢的源代码编辑器中的项目文件夹,然后运行train_dqn.py文件。
train_dqn.py Python 文件将在需要执行的地方从其他模块中导入函数。
现在让我们逐步讲解每个文件的内容。
模块 1 – hyperparameters_dqn.py
这个 Python 文件包含 DQN 模型的超参数:
"""This module contains hyperparameters for the DQN model."""
# Discount in Bellman Equation
gamma = 0.95
# Epsilon
epsilon = 1.0
# Minimum Epsilon
epsilon_min = 0.01
# Decay multiplier for epsilon
epsilon_decay = 0.99
# Size of deque container
deque_len = 20000
# Average score needed over 100 epochs
target_score = 200
# Number of games
episodes = 2000
# Data points per episode used to train the agent
batch_size = 64
# Optimizer for training the agent
optimizer = 'adam'
# Loss for training the agent
loss = 'mse'
模块 2 – agent_replay_dqn.py
这个 Python 文件包含四个函数,分别是agent()、agent_action()、performance_plot()和replay():
"""This module contains."""
import random
import numpy as np
import matplotlib.pyplot as plt
from keras.layers import Dense, Dropout, Activation
from keras.models import Sequential
from keras.optimizers import Adam
def agent(states, actions):
"""Simple Deep Neural Network."""
model = Sequential()
model.add(Dense(16, input_dim=states))
model.add(Activation('relu'))
model.add(Dense(16))
model.add(Activation('relu'))
model.add(Dense(16))
model.add(Activation('relu'))
model.add(Dense(actions))
model.add(Activation('linear'))
return model
有关此文件的其余部分,请访问这里:github.com/PacktPublishing/Python-Deep-Learning-Projects/blob/master/Chapter14/agent_replay_dqn.py
模块 3 – test_dqn.py
这个模块包含test()函数,它将在train_dqn.py脚本中调用,以测试 DQN 智能体的表现:
"""This module contains function to test the performance of the DQN model."""
import numpy as np
def test(env, model, states, episodes=100, render=False):
"""Test the performance of the DQN agent."""
scores_test = []
for episode in range(1, (episodes+1)):
state = env.reset()
state = state.reshape(1, states)
done = False
time_step = 0
while not done:
if render:
env.render()
action = np.argmax(model.predict(state)[0])
new_state, reward, done, info = env.step(action)
new_state = new_state.reshape(1, states)
state = new_state
time_step += 1
scores_test.append(time_step)
if episode % 10 == 0:
print('episode {}, score {} '.format(episode, time_step))
print('Average score over 100 test games: {}'.format(np.mean(scores_test)))
模块 4 – train_dqn.py
在这个模块中,我们包括了memory()和train()函数,并且调用了用于训练和测试强化学习模型的函数:
"""This module is used to train and test the DQN agent."""
import random
import numpy as np
from agent_replay_dqn import agent, agent_action, replay, performance_plot
from hyperparameters_dqn import *
from test_dqn import test
from keras import backend as k
from collections import deque
import gym
env = gym.make('CartPole-v1')
# Set seed for reproducibility
seed_val = 456
np.random.seed(seed_val)
env.seed(seed_val)
random.seed(seed_val)
states = env.observation_space.shape[0]
actions = env.action_space.n
training_data = deque(maxlen=deque_len)
def memory(state, new_state, reward, done, action):
"""Function to store data points in the deque container."""
training_data.append((state, new_state, reward, done, action))
def train(target_score, batch_size, episodes,
optimizer, loss, epsilon,
gamma, epsilon_min, epsilon_decay, actions, render=False):
"""Training the agent on games."""
print('----Training----')
k.clear_session()
对于此代码的其余部分,请访问:github.com/PacktPublishing/Python-Deep-Learning-Projects/blob/master/Chapter14/train_dqn.py
深度 SARSA 学习
在本部分中,我们将使用keras-rl库实现深度 SARSA 学习。keras-rl库是一个简单的神经网络 API,允许简单而易于实现强化学习模型(Q、SARSA 等)。要了解更多有关keras-rl库的信息,请访问文档:keras-rl.readthedocs.io/en/latest/。
我们将继续使用到现在为止在 OpenAI Gym 中使用的相同 CartPole 环境。
一个关于深度 SARSA 学习的 Jupyter Notebook 代码示例可以在github.com/PacktPublishing/Python-Deep-Learning-Projects/blob/master/Chapter14/Deep%20SARSA.ipynb找到。
SARSA 学习
SARSA 学习方法和 Q-learning 一样,都是基于策略的强化学习技术。它的目标是学习一个最优策略,帮助智能体在不同的可能情况下决定需要采取的行动。
SARSA 和 Q-learning 非常相似,除了 Q-learning 是一个脱离策略的算法,而 SARSA 是一个基于策略的算法。SARSA 学习的 Q 值不是像 Q-learning 那样基于贪心策略,而是基于当前策略下执行的动作。
对于单个状态,s,和一个动作,a,Q(s, a) 可以通过以下公式表示为下一个状态,s',和动作,a',的 Q 值:

以下是《强化学习:导论》一书中,理查德·S·萨顿和安德鲁·G·巴托编写的 SARSA 学习算法的伪代码:

图 14.10:SARSA 学习的伪代码
导入所有依赖项
在本部分练习中,我们将使用numpy、gym、matplotlib、keras、tensorflow和keras-rl包。这里,TensorFlow 将作为 Keras 的后端。您可以使用pip安装这些包:
import numpy as np
import gym
from keras.models import Sequential
from keras.layers import Dense, Activation, Flatten
from keras.optimizers import Adam
from rl.agents import SARSAAgent
from rl.policy import EpsGreedyQPolicy
加载游戏环境
就像在 DQN 部分加载游戏一样,我们将游戏加载到工作区并设置seed以确保结果可重复:
env = gym.make('CartPole-v1')
# set seed
seed_val = 456
env.seed(seed_val)
np.random.seed(seed_val)
states = env.observation_space.shape[0]
actions = env.action_space.n
定义智能体
对于深度 SARSA 学习,我们将使用在深度 Q-learning 部分中使用的相同智能体:
def agent(states, actions):
"""Simple Deep Neural Network."""
model = Sequential()
model.add(Flatten(input_shape=(1,states)))
model.add(Dense(16))
model.add(Activation('relu'))
model.add(Dense(16))
model.add(Activation('relu'))
model.add(Dense(16))
model.add(Activation('relu'))
model.add(Dense(actions))
model.add(Activation('linear'))
return model
model = agent(states, actions)
训练智能体
使用keras-rl库训练智能体非常简单:
-
定义您希望训练遵循的策略。我们将使用 epsilon-greedy 策略。与 DQN 部分相对应的是智能体的
action函数。要了解更多其他策略的信息,请访问github.com/keras-rl/keras-rl/blob/master/rl/policy.py。 -
加载您希望使用的智能体。在这种情况下,SARSA 智能体有许多参数,重要的参数包括
model、nb_actions和policy。model是您在前面代码中定义的深度学习智能体,nb_actions是系统中可能的操作数,而policy是您偏好的训练 SARSA 智能体的策略。 -
我们为 SARSA 智能体编译所选的损失函数和优化器。
-
我们通过将环境和训练步数作为参数传递给
.fit函数来训练 SARSA 智能体:
要获取keras-rl库中智能体的完整使用细节及其参数定义,请访问 Keras 的文档:keras-rl.readthedocs.io/en/latest/agents/sarsa/#sarsaagent。
# Define the policy
policy = EpsGreedyQPolicy()
# Loading SARSA agent by feeding it the policy and the model
sarsa = SARSAAgent(model=model, nb_actions=actions, policy=policy)
# compile sarsa with mean squared error loss
sarsa.compile('adam', metrics=['mse'])
# train the agent for 50000 steps
sarsa.fit(env, nb_steps=50000, visualize=False, verbose=1)
在训练时,要在.fit函数内将visualize参数设置为true,以便在屏幕上查看 CartPole 游戏。但可视化游戏会减慢训练速度。
这是训练 SARSA 智能体时的得分输出:

图 14.11:训练 SARSA 智能体时的得分输出
测试智能体
一旦智能体经过训练,我们将在 100 个新的回合上评估其表现。可以通过调用.test函数并提供测试环境和回合数作为参数来实现:
# Evaluate the agent on 100 new episodes
scores = sarsa.test(env, nb_episodes=100, visualize=False)
print('Average score over 100 test games: {}'.format(np.mean(scores.history['episode_reward'])))
在测试时,要在.test函数内将visualize参数设置为True,以便在屏幕上查看 CartPole 游戏。
以下是测试 100 轮后的输出:

以下是代码执行结束后的输出:

图 14.12:训练后的 SARSA 智能体测试得分
深度 SARSA 学习脚本(模块化形式)
对于 SARSA 学习,我们只有一个脚本,它实现了 SARSA 智能体的训练和测试:
"""This module implements training and testing of SARSA agent."""
import gym
import numpy as np
from keras.layers import Dense, Activation, Flatten
from keras.models import Sequential
from rl.agents import SARSAAgent
from rl.policy import EpsGreedyQPolicy
# load the environment
env = gym.make('CartPole-v1')
# set seed
seed_val = 456
env.seed(seed_val)
np.random.seed(seed_val)
states = env.observation_space.shape[0]
actions = env.action_space.n
def agent(states, actions):
"""Agent/Deep Neural Network."""
model = Sequential()
model.add(Flatten(input_shape=(1, states)))
model.add(Dense(16))
model.add(Activation('relu'))
model.add(Dense(16))
model.add(Activation('relu'))
model.add(Dense(16))
model.add(Activation('relu'))
model.add(Dense(actions))
model.add(Activation('linear'))
return model
model = agent(states, actions)
# Define the policy
policy = EpsGreedyQPolicy()
# Define SARSA agent by feeding it the policy and the model
sarsa = SARSAAgent(model=model, nb_actions=actions, nb_steps_warmup=10,
policy=policy)
# compile sarsa with mean squared error loss
sarsa.compile('adam', metrics=['mse'])
# train the agent for 50000 steps
sarsa.fit(env, nb_steps=50000, visualize=False, verbose=1)
# Evaluate the agent on 100 new episodes.
scores = sarsa.test(env, nb_episodes=100, visualize=False)
print('Average score over 100 test games: {}'
.format(np.mean(scores.history['episode_reward'])))
项目的结论
本项目的目标是构建一个深度强化学习模型,成功地玩 OpenAI Gym 中的 CartPole-v1 游戏。本章的用例是在一个简单的游戏环境中构建强化学习模型,然后将其扩展到其他复杂的游戏,如 Atari。
本章前半部分,我们构建了一个深度 Q 学习(DQN)模型来玩 CartPole 游戏。在测试过程中,DQN 模型在 100 场游戏中的平均得分为 277.88 分。
在本章的后半部分,我们建立了一个深度 SARSA 学习模型(使用与 Q-learning 相同的 epsilon-greedy 策略)来玩 CartPole 游戏。SARSA 模型在测试期间,100 局游戏的平均得分为 365.67 分。
现在,让我们遵循之前章节中用于评估模型表现的相同方法,从餐饮连锁的角度来看待模型的表现。
这个得分意味着什么?
Q-learning 平均得分为 277.88,这意味着我们已经成功解决了 OpenAI 网站定义的 CartPole 游戏。这也意味着我们的模型能够存活超过游戏时长的一半,且总游戏时长为 500 分钟。
至于 SARSA 学习,另一方面,Q-learning 的平均得分为 365.67,这意味着我们已经成功解决了 OpenAI 网站定义的 CartPole 游戏,并且我们的模型能够存活超过游戏时长的 70%,总游戏时长为 500 分钟。
这仍然不是你应该感到满意的表现水平,因为目标不仅仅是解决问题,而是训练一个能够在每局游戏中稳定得分 500 分的优秀模型,因此你可以理解为什么我们需要继续对模型进行微调,以获得最大的性能。
总结
在本章中,我们成功构建了一个深度强化学习模型,分别使用 Q-learning 和 SARSA 学习,基于 OpenAI Gym 中的 CartPole 游戏。我们理解了 Q-learning、SARSA 学习,如何与 Gym 中的游戏环境交互,以及代理(深度学习模型)的功能。我们定义了一些关键的超参数,并且在一些地方,我们也解释了为什么选择使用这些方法。最后,我们在新游戏上测试了我们强化学习模型的表现,并确定我们成功地实现了目标。
第十五章:深度学习职业生涯总结与下一步
这是一次精彩的旅程,作为团队的一员,你非常高效!我们希望你喜欢我们实践性的Python 深度学习项目教学方法。此外,我们的目标是为你提供激发思考、令人兴奋的体验,进一步增强你的直觉,并为你的深度学习工程师职业生涯奠定技术基础。
每一章的结构都类似于作为我们智能工厂团队的一员,随着学习材料的推进,我们达成了以下目标:
-
看到了实际应用场景的大局,并确定了成功的标准
-
集中精力进入代码,加载依赖项和数据,构建、训练并评估我们的模型
-
回到大局观,确认我们已达成目标
我们热衷于解决问题,构建智能的解决方案、洞察力和人才!让我们回顾一下关键学习,总结一些直觉,并展望你深度学习职业生涯的下一个阶段。
Python 深度学习 – 构建基础 – 两个项目
一个共同的工作环境基础使我们能够合作,并促使我们在计算机视觉(CV)和自然语言处理(NLP)领域学习酷炫且强大的深度学习技术。本书的前两章提供了建立经验,你将多次在数据科学家的职业生涯中使用这些经验。
第一章 – 构建深度学习环境
本章的主要目标是标准化我们的工具集,以便共同工作并取得一致准确的结果。我们希望建立一种使用深度学习算法构建可扩展生产应用的流程。在最后,我们识别了共同深度学习环境的组成部分,并最初设置了一个本地深度学习环境,随后扩展到基于云的环境。在随后的项目中,你获得了使用 Ubuntu、Anaconda、Python、TensorFlow、Keras 和Google Cloud Platform(GCP)等核心技术的经验。这些将继续对你在深度学习工程师的职业生涯中产生价值!
第二章 – 使用回归训练神经网络进行预测
在第二章《使用回归训练神经网络进行预测》中,我们确定了第一个商业用例——这将成为多个项目的主题:即一个餐饮连锁希望自动化其部分流程。具体来说,在本章中,商业用例是构建一个使用多层感知器(MLP)的深度学习分类器,MLP 是深度学习中的基本构建块,用来准确分类顾客电话号码的手写数字。如果你还记得,目标是准确地分类(数字化)iPad 上的手写电话号码,以便顾客能够收到一条短信,告知他们桌位已准备好。
我们在 TensorFlow 中构建了一个两层(最小深度)神经网络,并在经典的 MNIST 数据集上进行训练。这个项目为我们提供了一个机会,去解决过拟合、欠拟合、超参数调优以及激活函数等问题,以便探索模型的表现。我们发现尤其有趣的是商业用例对模型表现的实用性解读的影响。最初,使用这个简单的模型,我们的准确率看起来足够了,直到我们考虑到电话号码中的一个数字错误对准确传送短信给正确顾客的影响。在这个背景下,我们很快明白,我们需要做得更好。幸运的是,在书的后面,我们有机会再次尝试这个问题,在第八章《使用卷积神经网络进行手写数字分类》中,我们使用了一个更复杂的深度学习模型,效果更好!
Python 深度学习 – 自然语言处理 – 5 个项目
我们的Python 深度学习项目中,有三分之一集中在计算语言学领域。非结构化文本数据无处不在,而且生成的速度惊人。我们将所使用的技术和方法分为五个部分,以充分应对信息的广度。让我们回顾一下第三章《使用 word2vec 进行单词表示》到第八章《使用卷积神经网络进行手写数字分类》中的项目,看看它们是如何相互关联并逐步构建的。
第三章 – 使用 word2vec 进行单词表示
计算语言学的核心是对词语及其所体现特征的有效表示。word2vec 被用来将词语转化为密集向量(即张量),为语料库创建嵌入表示。我们接着创建了一个卷积神经网络(CNN)来构建情感分析的语言模型。为了帮助我们框定这个任务,我们设想了一个假设的使用案例:我们的餐厅连锁客户要求我们分析他们收到的顾客反馈文本,这些文本是顾客收到他们的桌位准备好通知时的回复。特别有趣的是意识到,CNN 不仅可以应用于图像数据!我们还利用这个项目作为探索数据可视化的机会,使用了t-分布随机邻居嵌入(t-SNE)和 TensorBoard。
第四章 – 构建自然语言处理管道用于构建聊天机器人
在这个项目中,我们通过探索语言模型的深度学习技术(构建块)深入研究了计算语言学。像我们在第三章中提到的 word2vec 模型,使用 word2vec 的词表示,是通过自然语言处理(NLP)管道实现的。我们的任务是创建一个自然语言处理管道,为开放域问答的聊天机器人提供支持。我们设想我们的(假设的)餐厅连锁有一个网站,包含菜单、历史、位置、营业时间和其他信息,并且他们希望在网站上增加一个查询框,访客可以提出问题,我们的深度学习 NLP 聊天机器人能够找到相关信息并返回答案。
自然语言处理管道对语料库进行了分词,标注了词性,使用依存句法分析确定了词与词之间的关系,并进行了命名实体识别(NER)。这使我们能够使用 TF-IDF 对文档中的特征进行向量化,从而创建一个简单的 FAQ 类型聊天机器人。我们通过 NER 和 Rasa NLU 的实现进一步增强了这一点。随后,我们能够构建一个能够理解文本意图(上下文)的机器人,并且还能够提取实体,因为我们创建了一个可以进行意图分类以及 NER 提取的 NLP 管道,使得它能够提供准确的回复。
第五章 – 序列到序列模型构建聊天机器人
本章直接在第四章,为建立聊天机器人构建 NLP 管道的基础上,为我们假设的餐厅连锁店构建了一个更先进的聊天机器人,以自动化电话点餐的过程。我们结合了多种技术的学习,制作了一个更具上下文意识和鲁棒性的聊天机器人。通过构建具有循环神经网络(RNN)模型和长短期记忆(LSTM)单元的模型,专门设计用于捕捉字符或单词序列中表示的信号,避免了聊天机器人中 CNN 的一些限制。
我们实现了一个语言模型,使用基于 LSTM 单元的编码器-解码器 RNN,用于简单的序列到序列问答任务。这个模型能够处理不同大小的输入和输出,保持信息状态,并足够处理复杂的上下文。我们的另一个学习是,获得足够数量和正确训练数据的重要性,因为模型的输出要符合非常高的语音可解释性标准。然而,通过正确的训练数据,可以使用这个模型实现假设的餐厅连锁店建立强大聊天机器人的目标(结合我们探索过的其他计算语言学技术)。
第六章 – 生成语言模型用于内容创作
在这个项目中,我们不仅在计算语言学的旅程中迈出了下一步,还跨越了一个深刻的鸿沟来生成新内容!我们定义了业务使用案例目标,提供一种深度学习解决方案,用于生成可以用于电影剧本、歌词和音乐的新内容。我们问自己:我们如何利用我们在解决餐厅连锁店问题方面的经验,并将其应用于不同的行业?反思过去项目中关于模型输入和输出的学习,我们相信新颖的内容只是另一种输出形式。我们证明了我们可以将图像作为输入,并输出一个类别标签(第二章,使用回归进行预测的 NN 训练)。我们训练了一个模型,接受文本输入并输出情感分类(第三章,使用 word2vec 进行词表示),并为开放域问答聊天机器人建立了一个 NLP 管道,其中我们将文本作为输入,并在语料库中识别文本以呈现适当的输出(第四章,为建立聊天机器人构建 NLP 管道)。然后,我们扩展了该聊天机器人功能,以便为餐厅提供自动点餐系统服务(第五章,用于构建聊天机器人的序列到序列模型)。
在本章中,我们实现了一个生成模型,使用长短期记忆网络(LSTM)、变分自编码器和生成对抗网络(GANs)来生成内容。我们成功地为文本和音乐领域实现了模型,这些模型能够为艺术家和各种创意行业生成歌曲歌词、剧本和音乐。
第七章 – 构建基于 DeepSpeech2 的语音识别系统
本项目是《Python 深度学习项目》一书中自然语言处理章节的一个集大成项目,旨在构建基于 DeepSpeech2 的语音识别系统。到目前为止,我们已经探讨了聊天机器人、自然语言处理和使用 RNN(包括单向和双向、含有和不含 LSTM 组件的 RNN)以及 CNN 的语音识别技术。我们见识了这些技术为现有商业流程提供智能支持的强大能力,并且能够创造出全新的智能系统。这是应用 AI 最前沿的令人兴奋的工作,运用了深度学习技术!
本项目的目标是构建并训练一个自动语音识别(ASR)系统,能够接收并将语音通话转换为文本,然后可以作为文本聊天机器人的输入,后者能够解析这些输入并做出适当回应。我们深入研究了语音数据,进行特征工程,以便从数据中提取各种特征,并构建一个能够识别用户语音的语音识别系统。最终,我们通过构建一个基于 DeepSpeech2 模型的系统,展示了我们对英语语音识别的掌握。我们结合语音和频谱图,使用连接时序分类(CTC)损失函数、批量归一化(Batch Normalization)和 SortaGrad 优化方法,搭建了一个端到端的语音识别系统,基于 RNN 技术。
深度学习 – 计算机视觉 – 6 个项目
以下六个 Python 深度学习项目,专注于计算机视觉(CV),是本书内容的主要部分。我们已经看到,部分深度学习技术,尤其是与计算机视觉相关的技术,如何能够应用于其他类型的数据,尤其是文本数据。很大程度上,这是因为 CNN 在特征提取和层次化表示上的巨大效用。没有哪种工具可以适用于所有工作——成为数据科学领域的深度学习工程师也不例外。但你不应低估你对 CNN 的熟悉程度,因为你会在许多不同的数据集和商业用例中反复使用它们。没有 CNN 技能的数据科学家,就像没有锤子的木匠。显然的警告是,并非数据科学中的所有任务都可以简单地比作钉子!
第八章 – 使用卷积神经网络(ConvNets)进行手写数字分类
本章提醒我们回顾在第二章《使用回归训练神经网络进行预测》中创建的第一个深度神经网络,以及它所应用的商业用例。本章的目的是为我们理解深度神经网络及其运行原理提供基础。在比较模型架构与更先进的技术时,我们强调了深度学习背后的数学复杂性,尤其是在我们构建更深、更强大的模型时所能获得的进步。复杂性并不是因为它复杂而“酷”;在这个情况下,它之所以“酷”是因为它带来了实际性能的提升。
我们花了大量时间研究卷积操作、池化和 Dropout 正则化。这些是你在职业生涯中调节模型时所需要调整的杠杆,所以尽早理解它们非常重要。回到商业用例,我们看到部署更复杂模型的价值,因为性能提升支持了母产品的实施。在第二章《使用回归训练神经网络进行预测》中获得的误差率,在最坏情况下,假设餐饮连锁店中的每个顾客都没有收到正确的文本信息(即使在最好情况下,效果依然很差,基本上无法使用)。而相同数据集上的 CNN 模型则产生了这样的结果:在新的最坏情况下,90%的顾客会收到文本通知,在最好情况下,99%的人会收到通知!
第九章 – 使用 OpenCV 和 TensorFlow 进行物体检测
让我们回顾一下在第八章《使用卷积神经网络进行手写数字分类》中所取得的成果,我们成功地训练了一个图像分类器,利用 CNN 准确地分类了图像中的手写数字。数据的复杂度比实际可能的要低,因为每个图像中只有一个手写数字,而我们的目标是为图像准确分配一个类别标签。如果每个图像中包含多个手写数字,或者不同类型的物体会怎样呢?如果我们有一个视频呢?如果我们想识别图像中数字的位置会怎样?这些问题代表了现实世界数据所面临的挑战,并推动我们的数据科学创新朝着新的模型和能力发展。
对计算机而言,目标检测和分类绝非一件简单的事情,尤其是在大规模和高速度要求下。在这个项目中,我们采用了比以往项目中更加信息复杂的数据输入,当我们成功处理这些数据时,结果也更加令人印象深刻。我们发现,深度学习包 YOLOv2 表现得非常出色,并且看到了我们的模型架构变得更加深度和复杂,同时也取得了不错的效果。
第十章 – 使用 OpenFace 构建人脸识别
在第九章,使用 OpenCV 和 TensorFlow 进行目标检测中,我们展示了构建深度学习目标检测和分类模型所需技能的掌握。在此基础上,我们的目标是对这一分类操作进行进一步细化:物体是否与另一个物体相同?在我们的案例中,我们希望构建一种类似间谍电影中看到的人脸识别系统,现在这种系统已经应用于高科技安防系统中。人脸识别是两项主要操作的结合:人脸检测和人脸分类。
在这个项目中,我们使用 OpenFace 构建了一个模型,该模型能够查看图片并识别其中的所有可能人脸,然后进行人脸提取,以了解包含人脸的图像部分的质量。接着,我们对人脸进行了特征提取,识别图像中可为与另一数据点(该人脸的标记图像)进行比较的部分。这个 Python 深度学习项目展示了这项技术的令人兴奋的潜力,也展现了擅长这些应用的工程师们的未来。
第十一章 – 自动化图像标题生成
在第九章,使用 OpenCV 和 TensorFlow 进行目标检测中,我们学习了如何检测和分类图像中的物体;在第十章,使用 FaceNet 构建人脸识别中,我们学习了如何检测、分类和识别物体是否为同一事物(例如,识别两张不同人脸图片中的同一人)。在这个项目中,我们做了更为复杂和酷炫的事情!我们结合了到目前为止在 Python 深度学习项目中学到的最先进技术,在计算机视觉(CV)和自然语言处理(NLP)领域,形成了一种完整的图像描述方法。这个模型能够为提供的任何图像生成计算机生成的自然语言描述。
使这一切成为可能的巧妙想法是,用一个深度卷积神经网络(CNN)替代了编码器-解码器架构中的编码器(RNN 层),并训练它来分类图像中的物体。通常,CNN 的最后一层是 softmax 层,用于为每个物体分配其出现在图像中的概率。但是,当我们去除 CNN 的 softmax 层时,我们可以将 CNN 对图像的丰富编码输入到解码器中(RNN 的语言生成组件),该解码器的设计目的是生成短语。然后,我们可以直接在图像及其描述上训练整个系统,最大化它生成的描述与每个图像的训练描述最匹配的可能性。这项深度学习技术是许多智能工厂解决方案的核心!
第十二章 – 使用卷积神经网络(ConvNets)在 3D 模型上进行姿态估计
我们应用于模型的数据是现实世界的表现。这是计算语言学和计算机视觉(CV)之间的基本联系。对于计算机视觉,我们需要记住,二维图像代表的是三维世界,就像视频代表的是四维世界,时间和运动是其中的额外维度。回忆这个显而易见的事实可以让我们提出越来越有趣的问题,并发展出具有更大实用性的深度学习技术。我们假设的应用场景是让视觉特效专家能够轻松估算演员在视频帧中的姿态(特别是肩膀、脖子和头部)。我们的任务是为这个应用程序构建智能系统。
我们成功地在 Keras 中构建了一个深度 CNN/VGG16 模型,使用的是电影标注帧(FLIC)图像。我们在为建模准备图像时获得了实际操作经验。我们成功实施了迁移学习,并明白了这样做将节省我们大量的时间。我们定义了一些关键的超参数,并理解了为什么我们要这么做。最后,我们在未见过的数据上测试了修改后的 VGG16 模型的性能,并确定我们成功达成了目标。
第十三章 – 使用生成对抗网络(GAN)进行风格迁移的图像翻译
生成对抗网络(GANs)简直太酷了。当我们回顾在这些项目中积累的技能和直觉时,我们有了一个有趣的想法。我们能否预测缺失的信息?换句话说:我们能否生成图像中应该有的,但实际上缺失的数据?如果我们能够接受文本输入并生成新的文本输出,或者能够接受 2D 图像并生成或预测 3D 位置输出,那么似乎是可能的:如果我们有一张缺少一些信息的 2D 图像,也许我们应该能够生成缺失的部分?因此,在这一章中,我们构建了一个神经网络,用来填补手写数字的缺失部分。我们之前为一个假设的餐厅连锁客户构建了一个数字分类器。误差率可能与数字未被准确捕捉、导致图像中的数字部分未完全绘制有关。我们将精力集中在模型创建的新增部分——通过使用生成对抗网络(GANs)进行神经修复,重建缺失的数字部分。然后我们重建了手写数字的缺失部分,以便分类器能够接收到清晰的手写数字,从而进行数字转换。通过这一过程,分类器能够更准确地分类手写数字(而我们假设的餐厅顾客也能及时收到通知并迅速入座)。
Python 深度学习 – 自主代理 – 1 项目
我们书中的最终项目与之前的任何项目都不同,因此值得单独处理。机器人过程自动化与优化,以及自主代理(如无人机和车辆)要求我们的深度学习模型能够在强化学习范式中从环境线索中学习。与之前专注于解决监督学习问题的项目不同,在这一章中,我们学习了如何构建并训练一个能够玩游戏的深度强化学习模型。
我们采用了深度 Q 学习和深度状态-动作-奖励-状态-动作(SARSA)学习模型。与通过定义启发式规则编程简单模型、在监督学习环境中映射 A-B 或在无监督学习中确定聚类分析的决策边界不同,强化学习中的反馈来自于游戏或环境的规则(通过强化传递)。该深度学习模型,也就是强化学习中的代理,与游戏环境进行互动,学习如何玩游戏,并在多次尝试后寻求最大化奖励。
第十四章 – 使用深度强化学习开发自主代理
在这个项目中,我们构建了一个深度强化学习模型,成功地玩转了 OpenAI Gym 中的 CartPole-v1 游戏。首先展示我们在此方面的掌握,我们然后可以将其扩展到其他复杂的游戏,如 Atari 的游戏。
我们学会了如何与 Gym 工具包、Q 学习和 SARSA 学习进行交互;如何编码强化学习模型并定义超参数;以及如何构建训练循环并测试模型。我们发现我们的 SARSA 模型比 Q 学习模型表现得要好得多。进一步的训练和超参数调整,以及我们自己捕获的强化单位(我们模型的更高分数),应当塑造我们的行为,构建出更好的模型,最终实现我们的代理几乎完美的表现!
下一步——AI 战略和平台
在本书中,你获得了构建深度学习项目技术基础的经验。然而,本书的范围使得我们的重点只能集中在整个生产规模数据科学管道的一部分。我们花时间以商业用例为背景,帮助我们在领域和成功标准上进行思考,但很快就深入到了深度学习模型的训练、评估和验证中。这些组成了我们项目中训练的主体,毫无疑问,它们是企业数据科学管道的核心,但不能孤立运作。进一步的 AI 战略和数据科学平台的学习和训练是你教育和职业发展的自然下一步。
AI 战略
AI 战略是通过从客户处获取知识,使你能够确定以下内容:
-
客户基于智能的竞争优势的宏大愿景
-
如何将这一愿景转化为一个有效的生产规模数据科学管道:
-
考虑客户当前和近期的数字化成熟度
-
数据摄取、分析和转换的过程
-
技术和工程资源与约束
-
分析团队目前的能力
-
模型选择、定制、训练、评估、验证和服务
-
-
达成与客户领导目标一致的 KPI 和 ROI
AI 战略咨询揭示了目标和期望,同时将结果与机器学习和深度学习技术对齐。构建 AI 解决方案架构必须考虑到所有这些因素,才能取得成功。你应该向行业导师学习,阅读可用的案例研究,并在你的职业发展过程中时刻牢记这一点,随着你越来越早地被召唤在解决方案构建过程中提供指导和意见。
深度学习平台——TensorFlow Extended (TFX)
为满足生产规模部署需求而设计的数据科学平台需要大量的工程支持。在智能工厂和 Skejul,我们构建了深度学习平台,能够接收不断更新的实时数据流,在毫秒内生成基于智能的输出,通过基于云的 Web 应用程序使用 API 网关交付。这是一个异常复杂且有回报的过程,一旦你把所有环节拼接起来,就能看到其巨大价值!
一项能够帮助您在深度学习和数据科学职业生涯中取得成功的技术是 TFX。这是 Google 基于 TensorFlow 的生产级机器学习平台。以下是它们文章摘要中的前几行
TFX: 基于 TensorFlow 的生产级机器学习平台 (ai.google/research/pubs/pub46484) 总结了 TFX 及类似平台的潜力:
"创建和维护一个可靠地生成和部署机器学习模型的平台,需要对许多组件进行精心的协调——一个基于训练数据生成模型的学习器,分析和验证数据以及模型的模块,最后是用于在生产环境中服务模型的基础设施。当数据随时间变化并且需要不断生成新的模型时,这变得尤为具有挑战性。"
基于精心策划的 AI 战略的数据科学平台工程是我们训练的下一步,我们也期待与您分享这些经验!
结论与感谢!
我们感谢您选择我们的书籍《Python 深度学习项目》作为您数据科学教育的一部分!我们希望您觉得这些项目和商业用例既引人入胜又富有信息性,并且比开始时更加专业地准备好了。我们期待有机会通过我们的博客、社交媒体,甚至在会议上与您互动,或者一起为全球客户提供基于 AI 的解决方案。
很高兴您参与了我们每周的 AI 团队会议,参与这些项目。现在我们已经学到了很多东西,并且享受了使用非常酷且强大的数据科学技术的乐趣,让我们基于这些 Python 深度学习项目去做伟大的工作吧!


浙公网安备 33010602011771号