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,用于一个简单的序列到序列的问答任务。
有了正确的训练数据,就有可能使用这个模型实现假设客户(餐饮连锁店)的目标,即构建一个强大的聊天机器人(结合我们探索的其他计算语言学技术),可以自动化电话订餐过程。
做得好!
更多推荐
所有评论(0)