自然语言处理的深度学习-全-

自然语言处理的深度学习(全)

原文:annas-archive.org/md5/79edb699aa9642b682a3d7113b128c41

译者:飞龙

协议:CC BY-NC-SA 4.0

第一章:前言

关于

本节简要介绍了作者、本书的覆盖范围、入门所需的技术技能,以及完成书中所有活动和练习所需的硬件和软件要求。

关于本书

将深度学习方法应用于各种 NLP 任务,可以使你的计算算法在速度和准确性方面达到全新的水平。《自然语言处理中的深度学习》一书首先介绍了自然语言处理领域的基本构建块。接着,本书介绍了你可以用最先进的神经网络模型解决的各种问题。深入探讨不同的神经网络架构及其具体应用领域,将帮助你理解如何选择最适合你需求的模型。在你逐步深入本书的过程中,你将学习卷积神经网络、循环神经网络和递归神经网络,同时还将涉及长短期记忆网络(LSTM)。在后续章节中,你将能够使用 NLP 技术(如注意力模型和束搜索)开发应用程序。

在本书结束时,你不仅将掌握自然语言处理的基本知识,还能选择最佳的文本预处理和神经网络模型,解决一系列 NLP 问题。

关于作者

Karthiek Reddy Bokka 是一位语音和音频机器学习工程师,毕业于南加州大学,目前在波特兰的 Bi-amp Systems 工作。他的兴趣包括深度学习、数字信号和音频处理、自然语言处理和计算机视觉。他有设计、构建和部署人工智能应用程序的经验,致力于利用人工智能解决现实世界中与各种实用数据相关的问题,包括图像、语音、音乐、非结构化原始数据等。

Shubhangi Hora 是一位 Python 开发者、人工智能爱好者和作家。她拥有计算机科学和心理学的背景,尤其对与心理健康相关的 AI 感兴趣。她目前生活在印度浦那,热衷于通过机器学习和深度学习推动自然语言处理的发展。除此之外,她还喜欢表演艺术,并是一名受过训练的音乐家。

Tanuj Jain 是一位数据科学家,目前在一家总部位于德国的公司工作。他一直在开发深度学习模型,并将其投入到商业生产中。自然语言处理是他特别感兴趣的领域,他已将自己的专业知识应用于分类和情感评分任务。他拥有电气工程硕士学位,专注于统计模式识别。

Monicah Wambugu 是一家金融科技公司的首席数据科学家,该公司通过利用数据、机器学习和分析技术执行替代性信用评分,提供小额贷款。她是加州大学伯克利分校信息学院信息管理与系统硕士项目的研究生。Monicah 特别感兴趣的是数据科学和机器学习如何被用来设计能够响应目标受众行为和社会经济需求的产品和应用。

描述

本书将从自然语言处理领域的基本构建模块开始。它将介绍可以通过最先进的神经网络模型解决的问题。它将深入讨论文本处理任务中所需的必要预处理。本书将涵盖 NLP 领域的一些热门话题,包括卷积神经网络、递归神经网络和长短期记忆网络。读者将理解文本预处理和超参数调优的重要性。

学习目标

  • 学习自然语言处理的基本原理。

  • 理解深度学习问题的各种预处理技术。

  • 使用 word2vec 和 Glove 开发文本的向量表示。

  • 理解命名实体识别。

  • 使用机器学习进行词性标注。

  • 训练和部署可扩展的模型。

  • 理解几种神经网络架构。

读者对象

有志成为数据科学家和工程师的人,希望在自然语言处理领域入门深度学习。

读者将从自然语言处理概念的基础开始,逐渐深入理解神经网络的概念及其在文本处理问题中的应用。他们将学习不同的神经网络架构及其应用领域。要求具备扎实的 Python 和线性代数知识。

方法

面向自然语言处理的深度学习将从自然语言处理的基本概念开始。一旦基本概念介绍完毕,听众将逐步了解 NLP 技术在现实世界中可应用的领域和问题。一旦用户理解了问题领域,解决方案的开发方法将会被介绍。作为基于解决方案的方法的一部分,讨论神经网络的基本构建模块。最终,将详细阐述各种神经网络的现代架构及其对应的应用领域,并给出实例。

硬件要求

为了获得最佳体验,我们推荐以下硬件配置:

  • 处理器:Intel Core i5 或同等处理器

  • 内存:4 GB RAM

  • 存储:5 GB 可用空间

软件要求

我们还建议您提前安装以下软件:

规范

文本中的代码字、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄如下所示:

代码块设置如下:

from sklearn.datasets import make_blobs
import matplotlib.pyplot as plt
import numpy as np
%matplotlib inline

新术语和重要单词以粗体显示。屏幕上看到的词语,例如菜单或对话框中的内容,文本中会像这样显示:“接下来,点击生成文件,然后点击立即下载,并将下载的文件命名为model.h5。”

安装与设置

每一段伟大的旅程都始于谦逊的第一步,我们即将展开的数据处理之旅也不例外。在我们用数据做出令人惊叹的事情之前,我们需要准备好最具生产力的环境。在这篇简短的说明中,我们将展示如何做到这一点。

在 Windows 上安装 Python

  1. 在官方安装页面www.python.org/downloads/windows/找到所需的 Python 版本。

  2. 请确保安装正确的“-bit”版本,取决于您的计算机系统,是 32 位还是 64 位。您可以在操作系统的系统属性窗口中找到此信息。

    下载安装程序后,只需双击文件并按照屏幕上的用户友好提示进行操作。

在 Linux 上安装 Python

在 Linux 上安装 Python,请执行以下操作:

  1. 打开命令提示符,并通过运行 python3 --version 来确认是否已安装 Python 3。

  2. 要安装 Python 3,请运行以下命令:

    sudo apt-get update
    sudo apt-get install python3.6
    
  3. 如果遇到问题,网上有许多资源可以帮助您排查并解决问题。

在 macOS X 上安装 Python

要在 macOS X 上安装 Python,请执行以下操作:

  1. 通过按住命令和空格键(CMD + Space),在打开的搜索框中输入 terminal,按下回车键打开终端。

  2. 通过运行命令 xcode-select --install,在命令行中安装 Xcode。

  3. 安装 Python 3 的最简单方法是使用 homebrew,可以通过命令行运行 ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" 安装。

  4. 将 homebrew 添加到你的 PATH 环境变量中。通过运行sudo nano ~/.profile打开命令行中的配置文件,并在底部插入export PATH="/usr/local/opt/python/libexec/bin:$PATH"

  5. 最后一步是安装 Python。在命令行中运行brew install python

  6. 请注意,如果你安装了 Anaconda,最新版本的 Python 将会自动安装。

安装 Keras

安装 Keras,请执行以下步骤:

  1. 由于Keras需要另一个深度学习框架作为后端,因此你需要先下载另一个框架,推荐使用TensorFlow

    要为你的平台安装TensorFlow,请点击www.tensorflow.org/install/

  2. 一旦后端安装完成,你可以运行sudo pip install keras进行安装。

    另外,你也可以从 GitHub 源代码安装,使用以下命令克隆Keras

    git clone https://github.com/keras-team/keras.git
    
  3. 安装cd keras sudo python setup.py install

    现在需要配置后端。有关更多信息,请参考以下链接:(keras.io/backend/)

额外资源

本书的代码包也托管在 GitHub 上,网址为:github.com/TrainingByPackt/Deep-Learning-for-Natural-Language-Processing。我们还有来自丰富书籍和视频目录的其他代码包,地址是github.com/PacktPublishing/。快来看看吧!

你可以从这里下载本书的图形包:

www.packtpub.com/sites/default/files/downloads/9781838558024_ColorImages.pdf

第二章:第一章

自然语言处理简介

学习目标

本章结束时,你将能够:

  • 描述自然语言处理及其应用

  • 解释不同的文本预处理技术

  • 对文本语料进行文本预处理

  • 解释 Word2Vec 和 GloVe 词向量的工作原理

  • 使用 Word2Vec 和 GloVe 生成词向量

  • 使用 NLTK、Gensim 和 Glove-Python 库进行文本预处理和生成词向量

本章旨在让你掌握自然语言处理的基础知识,并体验在深度学习中使用的各种文本预处理技术。

介绍

欢迎来到自然语言处理的深度学习之旅。本书将引导你理解并优化深度学习技术,用于自然语言处理,推动通用人工智能的现实化。你将深入了解自然语言处理的概念——它的应用和实现——并学习深度神经网络的工作方式,同时利用它们使机器理解自然语言。

自然语言处理基础

为了理解什么是自然语言处理,我们将这个术语分解成两个部分:

  • 自然语言是一种书面和口头沟通形式,它是自然和有机发展的。

  • 处理是指用计算机分析和理解输入数据。

图 1.1:自然语言处理

图 1.1:自然语言处理

因此,自然语言处理是基于机器的人类沟通处理。它旨在教会机器如何处理和理解人类的语言,从而建立起人与机器之间的轻松沟通渠道。

例如,我们手机和智能音响中的个人语音助手,如 Alexa 和 Siri,就是自然语言处理的产物。它们的设计使得它们不仅能理解我们对它们说的话,还能根据我们的指示做出反应并提供反馈。自然语言处理算法帮助这些技术与人类进行交流。

在自然语言处理的上述定义中,需要考虑的关键点是沟通必须发生在人类的自然语言中。几十年来,我们通过编写程序来与机器沟通,执行特定的任务。然而,这些程序使用的语言并非自然语言,因为它们不是口语交流形式,也没有自然或有机发展。这些语言,如 Java、Python、C 和 C++,是为机器设计的,且始终以“机器能够理解和轻松处理什么”为出发点。

虽然 Python 是一种更易于使用的语言,因此对于人类来说更容易学习并编写代码,但基本的观点保持不变——为了与机器沟通,人类必须学习一种机器能够理解的语言。

图 1.2:自然语言处理的维恩图

图 1.2:自然语言处理的维恩图

自然语言处理的目的正好与此相反。与其让人类适应机器的方式,学习如何与机器有效沟通,不如让机器适应人类并学习人类的沟通方式。这更加合理,因为技术的目的是让我们的生活更轻松。

为了通过一个例子来说明这一点,你第一次编写的程序可能是一段要求计算机打印“hello world”的代码。这是你在遵循机器的规则,并要求它执行你所理解的语言中的任务。当你对语音助手发出命令说“hello world”,并且它返回“hello world”时,这是自然语言处理的应用示例,因为你正在使用自然语言(在此情况下是英语)与计算机进行交流。计算机则遵循你的交流方式,理解你说的话,处理你要求它执行的任务,然后执行该任务。

自然语言处理的重要性

下图展示了人工智能领域的各个部分:

图 1.3:人工智能及其部分子领域

图 1.3:人工智能及其部分子领域

自然语言处理与机器学习和深度学习一起,是人工智能的一个子领域,并且由于它处理的是自然语言,实际上它处于人工智能和语言学的交汇点。

如前所述,自然语言处理使得机器能够理解人类的语言,从而在两者之间建立一个高效的沟通渠道。然而,自然语言处理之所以必要,还有另一个原因,那就是,像机器一样,机器学习和深度学习模型在处理数值数据时效果最佳。数值数据对于人类来说很难自然产生;想象一下我们用数字而不是单词交流。因此,自然语言处理处理文本数据并将其转换为数值数据,使得机器学习和深度学习模型能够在其上进行训练。因此,它的存在是为了弥合人类与机器之间的沟通鸿沟,将人类的口语和书面语言转化为机器能够理解的数据。多亏了自然语言处理,机器能够理解、回答基于数据的问题、使用数据解决问题,并用自然语言进行沟通等。

自然语言处理的能力

自然语言处理在现实生活中有许多有益的应用,这些应用属于自然语言处理的三大主要能力:

  • 语音识别

    机器能够识别口语形式的自然语言并将其转化为文本形式。一个例子是智能手机上的语音输入——你可以启用语音输入,向手机说话,手机会将你说的内容转化为文本。

  • 自然语言理解

    机器能够理解自然语言的书面和口语形式。如果给出命令,机器能够理解并执行。一个例子是对着 iPhone 上的 Siri 说“嘿,Siri,打电话回家”,Siri 会自动为你拨打“家”的电话。

  • 自然语言生成

    机器能够自主生成自然语言。一个例子是对 iPhone 上的 Siri 说“现在几点了?”,Siri 会回答时间——“现在是下午 2:08”。

这三种能力被用来完成和自动化许多任务。让我们来看看自然语言处理为哪些方面做出了贡献,以及如何做到的。

文本数据被称为语料(复数),单个语料称为语料库。

自然语言处理的应用

下图展示了自然语言处理的一般应用领域:

图 1.4:自然语言处理的应用领域

图 1.4:自然语言处理的应用领域
  • 自动文本摘要

    这涉及处理语料以提供总结。

  • 翻译

    这包括翻译工具,能够将文本从一种语言翻译成另一种语言,例如谷歌翻译。

  • 情感分析

    这也被称为情感人工智能或意见挖掘,是从书面和口头的语料中识别、提取和量化情感与情绪状态的过程。情感分析工具用于处理如客户评论和社交媒体帖子等内容,旨在理解人们对特定事物(如新餐厅的食品质量)的情绪反应和意见。

  • 信息提取

    这是从语料中识别和提取重要术语的过程,称为实体。命名实体识别属于这一类别,这一过程将在下一章中进行解释。

  • 关系提取

    关系抽取涉及从语料库中提取语义关系。语义关系存在于两个或更多实体(如人、组织和事物)之间,并且属于多种语义类别中的一种。例如,如果一个关系抽取工具收到一段关于 Sundar Pichai 及其作为谷歌首席执行官的描述,该工具能够输出“Sundar Pichai 为谷歌工作”,其中 Sundar Pichai 和谷歌是两个实体,而“为……工作”则是定义它们关系的语义类别。

  • 聊天机器人

    聊天机器人是一种人工智能形式,旨在通过语音和文本与人类对话。它们大多数模仿人类,使你感觉像是在与另一个人交谈。聊天机器人在健康行业中被用于帮助那些遭受抑郁和焦虑的人。

  • 社交媒体分析

    像 Twitter 和 Facebook 这样的社交媒体应用有话题标签和趋势,这些话题和趋势通过自然语言处理技术被跟踪和监控,以便了解全球范围内正在讨论的内容。此外,自然语言处理还协助内容审查过程,通过过滤负面、冒犯性和不当的评论和帖子,帮助维护社交平台的健康环境。

  • 个人语音助手

    Siri、Alexa、Google Assistant 和 Cortana 都是个人语音助手,利用自然语言处理技术来理解并回应我们说的话。

  • 语法检查

    语法检查软件会自动检查和修正你的语法、标点符号和打字错误。

文本预处理

在回答理解性阅读题时,问题是针对文章的不同部分的,因此,某些单词和句子对你来说很重要,而其他的则无关紧要。技巧在于从问题中识别关键词,并将其与文章进行匹配,从而找到正确的答案。

文本预处理的工作方式类似——机器不需要语料库中的无关部分,它只需要执行任务所需的重要单词和短语。因此,文本预处理技术涉及为语料库准备适当的分析,并为机器学习和深度学习模型做好准备。文本预处理基本上是告诉机器它需要考虑什么,以及可以忽略什么。

每个语料库根据需要执行的任务不同,要求采用不同的文本预处理技术,一旦你掌握了不同的预处理技术,你就会理解在何时、为何使用特定的技术。技术解释的顺序通常也是它们执行的顺序。

在接下来的练习中,我们将使用NLTK Python 库,但在进行活动时也可以自由使用其他库。NLTK代表自然语言工具包,它是最简单且最流行的 Python 自然语言处理库之一,这也是我们使用它来理解自然语言处理基本概念的原因。

注意

如需了解更多关于 NLTK 的信息,请访问 https://www.nltk.org/。

文本预处理技术

以下是自然语言处理中的最流行的文本预处理技术:

  • 小写/大写

  • 噪声去除

  • 文本标准化

  • 词干提取

  • 词形还原

  • 分词

  • 去除停用词

让我们逐一查看每种技术。

小写/大写

这是最简单且最有效的预处理技术之一,但人们常常忘记使用它。它可以将所有现有的大写字母转换为小写字母,使整个语料库都是小写的,或者将语料库中所有的小写字母转换为大写字母,使整个语料库都是大写的。

这种方法尤其适用于当语料库的大小不太大且任务涉及识别由于字符大小写不同而可能被识别为不同的术语或输出时,因为机器本身会将大写字母和小写字母视为独立的实体——'A'与'a'是不同的。输入中的这种大小写变体可能导致输出不正确或完全没有输出。

一个例子是,一个语料库中包含了'India'和'india'。如果没有进行小写转换,机器会将它们识别为两个不同的术语,而实际上它们只是同一个单词的不同形式,表示的是同一个国家。经过小写转换后,语料库中只会存在一个"India"的实例,即'india',这简化了查找语料库中所有提到印度的地方的任务。

注意

所有练习和活动将主要在 Jupyter Notebook 上开发。你需要在系统上安装 Python 3.6 和 NLTK。

练习 1 – 6 可以在同一个 Jupyter 笔记本中完成。

练习 1:对句子进行小写转换

在本次练习中,我们将输入一个包含大写和小写字母的句子,并将其全部转换为小写字母。以下步骤将帮助你完成该解决方案:

  1. 打开cmd或根据你的操作系统打开其他终端。

  2. 导航到所需路径并使用以下命令启动Jupyter笔记本:

    jupyter notebook
    
  3. 将输入句子存储在's = "The cities I like most in India are Mumbai, Bangalore, Dharamsala and Allahabad."

  4. 应用lower()函数将大写字母转换为小写字母,然后打印出新的字符串,如下所示:

    s = s.lower()
    print(s)
    

    预期输出:

    图 1.5:包含混合大小写的句子的小写处理输出
  5. 创建一个包含大写字符的单词数组,如下所示:

    words = ['indiA', 'India', 'india', 'iNDia']
    
  6. 使用列表推导,对words数组中的每个元素应用lower()函数,然后打印新数组,如下所示:

    words = [word.lower() for word in words]
    print(words)
    

    预期输出:

图 1.6:带混合大小写单词的小写化输出

图 1.6:带混合大小写单词的小写化输出

噪声移除

噪声是一个非常通用的术语,在不同的语料库和任务中可能意味着不同的东西。对于一个任务来说被视为噪声的内容,可能在另一个任务中被认为是重要的,因此这是一个非常领域特定的预处理技术。例如,在分析推文时,标签可能对识别趋势和理解全球讨论的内容非常重要,但在分析新闻文章时,标签可能并不重要,因此在后者的情况下,标签就被视为噪声。

噪声不仅仅包括单词,还可以包括符号、标点符号、HTML 标记(<,>,, ?,.)、数字、空格、停用词、特定术语、特定的正则表达式、非 ASCII 字符(\W|\d+*)以及解析术语。

移除噪声至关重要,这样只有语料库中重要的部分才会被输入到模型中,从而确保准确的结果。它还通过将单词转化为根形态或标准形式来帮助分析。考虑以下示例:

图 1.7:噪声移除输出

图 1.7:噪声移除输出

删除所有符号和标点符号后,所有“sleepy”的实例都对应于单一的单词形式,从而提高了语料库的预测和分析效率。

练习 2:移除单词中的噪声

在这个练习中,我们将输入一个包含噪声的单词数组(如标点符号和 HTML 标记),并将这些单词转换为清洁、无噪声的形式。为此,我们需要使用 Python 的正则表达式库。该库提供了多个函数,允许我们筛选输入数据并移除不必要的部分,这正是噪声移除过程的目标。

注意

若要了解更多关于‘re’的信息,请点击 https://docs.python.org/3/library/re.html。

  1. 在相同的Jupyter笔记本中,导入正则表达式库,如下所示:

    import re
    
  2. 创建一个名为'clean_words'的函数,该函数包含移除不同类型噪声的方法,如下所示:

    def clean_words(text):
    
      #remove html markup
      text = re.sub("(<.*?>)","",text)
      #remove non-ascii and digits
      text=re.sub("(\W|\d+)"," ",text)
      #remove whitespace
      text=text.strip()
      return text 
    
  3. 创建一个包含噪声的原始单词数组,如下所示:

    raw = ['..sleepy', 'sleepy!!', '#sleepy', '>>>>>sleepy>>>>', '<a>sleepy</a>']
    
  4. 对原始数组中的单词应用clean_words()函数,然后打印清理后的单词数组,如下所示:

    clean = [clean_words(r) for r in raw]
    print(clean)
    

    预期输出:

图 1.8:噪声移除输出

文本归一化

这是将原始语料库转换为规范和标准形式的过程,基本上是确保文本输入在分析、处理和操作之前保持一致。

文本标准化的示例包括将缩写映射到其完整形式,将同一单词的不同拼写转换为单一拼写形式,等等。

以下是一些不正确拼写和缩写的标准形式示例:

图 1.9:不正确拼写的标准形式

图 1.9:不正确拼写的标准形式

图 1.10:缩写的标准形式

图 1.10:缩写的标准形式

由于标准化的方法非常依赖于语料库和具体任务,因此没有统一的标准方式。最常见的做法是使用字典映射法,即手动创建一个字典,将一个单词的各种形式映射到该单词的标准形式,然后将这些单词替换为该单词的标准形式。

词干提取

词干提取是在语料库上进行的,以将单词简化为它们的词干或词根形式。之所以说“词干或词根形式”,是因为词干提取的过程并不总是将单词还原为词根形式,有时只是简化为它的标准形式。

经历词干提取的单词称为屈折词。这些单词与词根形式不同,表示某种属性,例如数目或性别。例如,“journalists”是“journalist”的复数形式。因此,词干提取会去掉“s”,将“journalists”还原为它的词根形式:

图 1.11:词干提取的输出

图 1.11:词干提取的输出

词干提取在构建搜索应用时非常有用,因为在搜索特定内容时,您可能还希望找到拼写不同的该内容的实例。例如,如果您在搜索本书中的练习,您可能还希望搜索到“Exercise”。

然而,词干提取并不总是提供所需的词干,因为它是通过去掉单词末尾的部分来工作的。词干提取器有可能将“troubling”还原为“troubl”而不是“trouble”,这对解决问题没有太大帮助,因此词干提取并不是一种常用的方法。当使用时,Porter 的词干提取算法是最常见的算法。

练习 3:对单词进行词干提取

在本练习中,我们将处理一个包含同一单词不同形式的输入数组,并将这些单词转换为它们的词干形式。

  1. 在同一个Jupyter笔记本中,导入nltkpandas库以及Porter Stemmer,如示例所示:

    import nltk
    import pandas as pd
    from nltk.stem import PorterStemmer as ps
    
  2. 创建一个stemmer实例,如下所示:

    stemmer = ps()
    
  3. 创建一个包含同一单词不同形式的数组,如下所示:

    words=['annoying', 'annoys', 'annoyed', 'annoy']
    
  4. 将词干提取器应用于words数组中的每个单词,并将它们存储在一个新数组中,如所示:

    stems =[stemmer.stem(word = word) for word in words]
    
  5. 将原始单词及其词干以数据框的形式打印,如下所示:

    sdf = pd.DataFrame({'raw word': words,'stem': stems})
    sdf
    

    预期输出:

图 1.12:词干提取的输出

图 1.12:词干提取的输出

词形还原

词形还原是一个类似于词干提取的过程——它的目的是将单词还原为其根本形式。与词干提取不同的是,词形还原不仅仅是通过剪切单词的结尾来获得根本形式,而是遵循一个过程,遵循规则,并且通常使用 WordNet 进行映射,将单词还原为其根本形式。(WordNet 是一个英语语言数据库,包含单词及其定义,此外还包含同义词和反义词。它被认为是词典和同义词词典的融合。)例如,词形还原能够将单词“better”转换为其根本形式“good”,因为“better”只是“good”的比较级形式。

尽管词形还原的这一特性使得它在与词干提取相比时更具吸引力和效率,但缺点是,由于词形还原遵循如此有序的程序,因此它比词干提取需要更多时间。因此,当你处理大规模语料库时,不推荐使用词形还原。

练习 4:对单词进行词形还原

在本练习中,我们将接收一个包含单词不同形式的输入数组,并将这些单词转换为其根本形式。

  1. 在与前一个练习相同的Jupyter笔记本中,导入WordNetLemmatizer并下载WordNet,如所示:

    from nltk.stem import WordNetLemmatizer as wnl
    nltk.download('wordnet')
    
  2. 创建一个lemmatizer实例,如下所示:

    lemmatizer = wnl()
    
  3. 创建一个包含同一单词不同形式的数组,如示范所示:

    words = ['troubling', 'troubled', 'troubles', 'trouble']
    
  4. words数组中的每个单词应用lemmatizer,并将结果存储在一个新数组中,如下所示。word参数向词形还原函数提供它应还原的单词。pos参数是你希望词形还原为的词性。'v'代表动词,因此词形还原器会将单词还原为其最接近的动词形式:

    # v denotes verb in "pos"
    lemmatized = [lemmatizer.lemmatize(word = word, pos = 'v') for word in words]
    
  5. 打印原始单词及其根本形式,结果以数据框形式展示,如所示:

    ldf = pd.DataFrame({'raw word': words,'lemmatized': lemmatized})
    ldf = ldf[['raw word','lemmatized']]
    ldf
    

    预期输出:

图 1.13:词形还原的输出

图 1.13:词形还原的输出

分词

分词是将语料库拆解为单个词元的过程。词元是最常用的单词——因此,这个过程将语料库拆解为单个单词——但也可以包括标点符号和空格等其他元素。

这一技术是最重要的技术之一,因为它是很多自然语言处理应用的前提,我们将在下一章学习这些应用,比如词性标注PoS)。这些算法将词元作为输入,不能使用字符串或段落文本作为输入。

可以通过分词将文本分解成单个单词或单个句子作为词元。让我们在接下来的练习中尝试这两种方法。

练习 5:词元化单词

在本练习中,我们将接收一个输入句子,并从中生成单个单词作为词元。

  1. 在同一个Jupyter笔记本中,导入nltk

    import nltk
    
  2. nltk导入word_tokenizepunkt,如所示:

    nltk.download('punkt')
    from nltk import word_tokenize
    
  3. 将单词存储在一个变量中,并对其应用 word_tokenize(),然后打印结果,如下所示:

    s = "hi! my name is john."
    tokens = word_tokenize(s)
    tokens
    

    预期输出:

图 1.14:单词分词的输出

图 1.14:单词分词的输出

如你所见,甚至标点符号也被分词并视为独立的标记。

现在让我们看看如何分词句子。

练习 6:句子分词

在本练习中,我们将从输入句子中生成单独的单词作为标记。

  1. 在同一个 Jupyter 笔记本中,按如下方式导入 sent_tokenize

    from nltk import sent_tokenize
    
  2. 将两个句子存储在一个变量中(我们之前的句子实际上是两个句子,所以可以使用同一个句子来查看单词分词和句子分词之间的区别),然后对其应用 sent_tokenize(),接着打印结果,如下所示:

    s = "hi! my name is shubhangi."
    tokens = sent_tokenize(s)
    tokens
    

    预期输出:

图 1.15:句子分词的输出

图 1.15:句子分词的输出

如你所见,两个句子已经形成了两个独立的标记。

其他技术

有多种方式可以进行文本预处理,包括使用各种 Python 库,如 BeautifulSoup 来剥离 HTML 标记。前面的练习是为了向你介绍一些技术。根据手头的任务,你可能只需要使用其中的一两个,或者全部使用它们,包括对它们所做的修改。例如,在噪声移除阶段,你可能觉得有必要移除像 'the'、'and'、'this' 和 'it' 这样的词。因此,你需要创建一个包含这些词的数组,并通过 for 循环将语料库中的词汇传递,只保留那些不在该数组中的词,去除噪声词。另一种方法将在本章后面介绍,且是在完成分词后执行的。

练习 7:移除停用词

在本练习中,我们将从输入句子中移除停用词。

  1. 打开一个 Jupyter 笔记本,并使用以下代码行下载 'stopwords':

    nltk.download('stopwords')
    
  2. 将一个句子存储在一个变量中,如下所示:

    s = "the weather is really hot and i want to go for a swim"
    
  3. 导入 stopwords 并创建一组英语停用词,如下所示:

    from nltk.corpus import stopwords
    stop_words = set(stopwords.words('english'))
    
  4. 使用 word_tokenize 对句子进行分词,然后将那些在 stop_words 中没有出现的标记存储在一个数组中。接着,打印该数组:

    tokens = word_tokenize(s)
    tokens = [word for word in tokens if not word in stop_words]
    print(tokens)
    

    预期输出:

图 1.16:移除停用词后的输出

图 1.16:移除停用词后的输出

此外,你可能需要将数字转换为其词语形式。这也是可以添加到噪声移除功能中的一种方法。此外,你可能需要使用缩写词库,它的作用是扩展文本中已有的缩写词。例如,缩写词库会将 'you're' 转换为 'you are',如果这对你的任务是必要的,那么建议安装并使用该库。

文本预处理技术超出了本章所讨论的范围,可以包括任务或语料库中任何所需的内容和技术。在某些情况下,一些词语可能很重要,而在其他情况下则不重要。

词嵌入

如本章前面的部分所提到的,自然语言处理为机器学习和深度学习模型准备文本数据。当提供数值数据作为输入时,模型表现最为高效,因此自然语言处理的一个关键角色是将预处理后的文本数据转化为数值数据,这是文本数据的数值表示。

这就是词嵌入:它们是文本的数值表示,采用实值向量的形式。具有相似含义的词映射到相似的向量,因此具有相似的表示形式。这帮助机器学习不同单词的意义和上下文。由于词嵌入是映射到单个单词的向量,词嵌入只能在对语料库进行分词之后生成。

图 1.17:词嵌入示例

图 1.17:词嵌入示例

词嵌入包括用于创建学习到的数值表示的各种技术,是表示文档词汇最流行的方式。词嵌入的优点在于,它们能够捕捉上下文、语义和句法的相似性,以及一个单词与其他单词的关系,从而有效地训练机器理解自然语言。这就是词嵌入的主要目的——形成类似向量的簇,这些簇对应于具有相似意义的词。

使用词嵌入的原因是为了让机器像我们一样理解同义词。以在线餐厅评论为例——这些评论由形容词描述食物、环境和整体体验。它们要么是积极的,要么是消极的,理解哪些评论属于这两类中的哪一类非常重要。自动分类这些评论可以为餐厅提供快速的见解,了解需要改进的地方,人们喜欢餐厅的哪些方面,等等。

存在多种可以归类为积极的形容词,消极形容词也是如此。因此,机器不仅需要能够区分负面和正面,还需要学习和理解多个单词可以归属于同一类别,因为它们最终意味着相同的东西。这就是词嵌入发挥作用的地方。

以餐饮服务应用上的餐厅评论为例。以下两句话来自两条不同的餐厅评论:

  • 句子 A – 这里的食物很好。

  • 句子 B – 这里的食物很好。

机器需要能够理解这两条评论都是积极的,并且它们意味着类似的内容,尽管这两句话中的形容词不同。这是通过创建词嵌入来实现的,因为两个词“good”和“great”分别映射到两个不同但相似的实际值向量,因此可以将它们聚集在一起。

词嵌入的生成

我们已经了解了什么是词嵌入以及它们的重要性;现在我们需要理解它们是如何生成的。将词语转换为其实际值向量的过程被称为向量化,这是通过词嵌入技术完成的。市面上有许多词嵌入技术,但在本章中,我们将讨论两种主要技术——Word2Vec 和 GloVe。一旦词嵌入(向量)被创建,它们会结合成一个向量空间,这是一个代数模型,由遵循向量加法和标量乘法规则的向量组成。如果你不记得线性代数的内容,现在可能是快速复习的好时机。

Word2Vec

如前所述,Word2Vec 是一种词嵌入技术,用于从词语中生成向量——这从其名字上就能大致理解。

Word2Vec 是一个浅层神经网络——它只有两层——因此不算是深度学习模型。输入是一个文本语料库,机器使用它来生成向量作为输出。这些向量被称为输入语料库中单词的特征向量。它将语料库转换为深度神经网络能够理解的数值数据。

Word2Vec 的目标是理解两个或多个词语共同出现的概率,从而将具有相似意义的词语聚集在一起,形成向量空间中的一个簇。像其他机器学习或深度学习模型一样,Word2Vec 通过从过去的数据和单词的出现中学习,变得越来越高效。因此,如果提供足够的数据和上下文,它可以根据过去的出现和上下文准确地猜测一个词的含义,类似于我们如何理解语言。

例如,一旦我们听说并阅读过“boy”和“man”,“girl”和“woman”这些词,并理解它们的含义,我们就能建立起这些词之间的联系。同样,Word2Vec 也可以建立这种联系,并为这些词生成向量,这些向量彼此靠近,位于同一个簇中,以确保机器能够意识到这些词意味着相似的东西。

一旦 Word2Vec 接收到一个语料库,它会生成一个词汇表,其中每个词都有一个与之关联的向量,这被称为其神经词嵌入,简单来说,这个神经词嵌入就是用数字表示的词语。

Word2Vec 的工作原理

Word2Vec 训练一个词与其在输入语料库中邻近的词进行对比,有两种方法可以实现这一点:

  • 连续词袋模型(CBOW)

    该方法根据上下文预测当前单词。因此,它将单词的周围单词作为输入,输出该单词,并根据该单词是否确实是句子的一部分来选择该单词。

    例如,如果算法提供了“the food was”这些单词并需要预测后面的形容词,它最有可能输出“good”而不是“delightful”,因为“good”出现的次数更多,因此它学到“good”的出现概率高于“delightful”。CBOW 被认为比跳字法更快,并且在频繁出现的单词上准确性更高。

图 1.18: CBOW 算法

图 1.18: CBOW 算法
  • 跳字法

    该方法通过将一个单词作为输入,理解该单词的含义并将其与上下文关联,来预测周围的单词。例如,如果算法给定单词“delightful”,它需要理解该词的含义,并从过去的上下文中学习,预测出周围的单词是“the food was”的概率最大。跳字法通常认为在小型语料库中表现最好。

图 1.19: 跳字法算法

图 1.19: 跳字法算法

虽然这两种方法看起来是相反的方式,但它们本质上是根据局部(附近)单词的上下文来预测单词;它们使用一个上下文窗口来预测接下来会出现哪个单词。这个窗口是一个可配置的参数。

选择使用哪种算法的决定取决于手头的语料库。CBOW 基于概率工作,因此会选择在特定上下文中出现概率最高的单词。这意味着它通常只会预测常见和频繁出现的单词,因为这些单词的概率最高,而罕见和不常见的单词则永远不会由 CBOW 生成。另一方面,跳字法预测上下文,因此,当给定一个单词时,它会将其视为一个新的观察,而不是将其与具有相似意义的现有单词进行比较。因此,罕见单词不会被跳过或忽视。然而,这也意味着跳字法需要大量的训练数据才能高效地工作。因此,根据训练数据和语料库的不同,应该决定使用哪种算法。

本质上,这两种算法以及整个模型,都需要一个强度很高的学习阶段,在这个阶段它们会经过数千、数百万个单词的训练,以便更好地理解上下文和含义。基于此,它们能够为单词分配向量,从而帮助机器学习和预测自然语言。为了更好地理解 Word2Vec,让我们通过 Gensim 的 Word2Vec 模型做一个练习。

Gensim 是一个开源库,用于使用统计机器学习进行无监督主题建模和自然语言处理。Gensim 的 Word2Vec 算法接收由单独的单词(令牌)组成的句子序列作为输入。

此外,我们还可以使用min_count参数。它的作用是问你一个单词在语料库中应该出现多少次才对你有意义,然后在生成词嵌入时考虑这一点。在实际场景中,当处理数百万个单词时,只出现一次或两次的单词可能完全不重要,因此可以忽略它们。然而,现在我们仅在三句话上训练模型,每句只有 5 到 6 个单词。因此,min_count设置为 1,因为即使一个单词只出现一次,对我们来说它也是重要的。

练习 8:使用 Word2Vec 生成词嵌入

在本练习中,我们将使用 Gensim 的 Word2Vec 算法,在分词后生成词嵌入。

注意

你需要在系统上安装gensim才能进行以下练习。如果尚未安装,你可以使用以下命令进行安装:

pip install --upgrade gensim

如需更多信息,请点击 https://radimrehurek.com/gensim/models/word2vec.html。

以下步骤将帮助你完成该解答:

  1. 打开一个新的Jupyter笔记本。

  2. gensim导入 Word2Vec 模型,并从nltk导入word_tokenize,如所示:

    from gensim.models import Word2Vec as wtv
    from nltk import word_tokenize
    
  3. 将三个包含常见单词的字符串存入三个独立的变量中,然后对每个句子进行分词,并将所有令牌存储在一个数组中,如所示:

    s1 = "Ariana Grande is a singer"
    s2 = "She has been a singer for many years"
    s3 = "Ariana is a great singer"
    sentences = [word_tokenize(s1), word_tokenize(s2), word_tokenize(s3)]
    

    你可以打印句子数组来查看令牌。

  4. 如下所示,训练模型:

    model = wtv(sentences, min_count = 1)
    

    Word2Vec 的min_count默认值为 5\。

  5. 如所示,总结模型:

    print('this is the summary of the model: ')
    print(model)
    

    你的输出将会是如下所示:

    图 1.20:模型摘要的输出

    图 1.20:模型摘要的输出

    Vocab = 12 表示输入模型的句子中有 12 个不同的单词。

  6. 让我们通过总结模型来找出词汇表中存在哪些单词,如下所示:

    words = list(model.wv.vocab)
    print('this is the vocabulary for our corpus: ')
    print(words)
    

    你的输出将会是如下所示:

图 1.21:语料库词汇表的输出

图 1.21:语料库词汇表的输出

让我们来看一下单词‘singer’的向量(词嵌入):

print("the vector for the word singer: ")
print(model['singer'])

期望输出:

图 1.22:单词‘singer’的向量

图 1.22:单词‘singer’的向量

我们的 Word2Vec 模型已经在这三句话上进行了训练,因此它的词汇表仅包含这些句子中出现的单词。如果我们试图从 Word2Vec 模型中找到与某个特定输入单词相似的单词,由于词汇表非常小,我们不会得到任何实际有意义的单词。考虑以下例子:

#lookup top 6 similar words to great
w1 = ["great"]
model.wv.most_similar (positive=w1, topn=6)

“positive”指的是在输出中仅展示正向的向量值。

与‘great’最相似的六个单词是:

图 1.23:与“great”类似的词向量

图 1.23:与“great”类似的词向量

类似地,对于“singer”这个词,它可能是如下所示:

#lookup top 6 similar words to singer
w1 = ["singer"]
model.wv.most_similar (positive=w1, topn=6)

图 1.24:与“singer”类似的词向量

图 1.24:与“singer”类似的词向量

我们知道这些词与我们的输入词在意义上并不相似,这也体现在它们旁边的相关性值中。然而,它们会出现在这里,因为它们是我们词汇中唯一存在的词。

Gensim Word2Vec 模型的另一个重要参数是大小参数。它的默认值是 100,表示用于训练模型的神经网络层的大小。这对应于训练算法的自由度。较大的大小需要更多的数据,但也会提高准确性。

注意

有关 Gensim Word2Vec 模型的更多信息,请点击

https://rare-technologies.com/word2vec-tutorial/.

GloVe

GloVe,"global vectors" 的缩写,是一种由斯坦福大学开发的词嵌入技术。它是一种无监督学习算法,基于 Word2Vec。虽然 Word2Vec 在生成词嵌入方面非常成功,但它的问题在于它有一个较小的窗口,通过这个窗口它集中关注局部词汇和局部上下文来预测词汇。这意味着它无法从全局词频中学习,也就是说,无法从整个语料库中学习。正如其名称所示,GloVe 会查看语料库中出现的所有词汇。

虽然 Word2Vec 是一种预测模型,它通过学习向量来提高预测能力,但 GloVe 是一种基于计数的模型。这意味着 GloVe 通过对共现计数矩阵进行降维来学习向量。GloVe 能够建立的连接如下所示:

king – man + woman = queen

这意味着它能够理解“king”和“queen”之间的关系与“man”和“woman”之间的关系相似。

这些是复杂的术语,所以我们一个个来理解。所有这些概念来自于统计学和线性代数,如果你已经了解这些内容,可以跳过活动部分!

在处理语料库时,存在根据词频构建矩阵的算法。基本上,这些矩阵包含文档中出现的词作为行,列则是段落或单独的文档。矩阵的元素表示词在文档中出现的频率。自然地,对于一个大型语料库,这个矩阵会非常庞大。处理这样一个大型矩阵将需要大量的时间和内存,因此我们进行降维处理。这是减少矩阵大小的过程,使得可以对其进行进一步操作。

对于 GloVe 来说,矩阵被称为共现计数矩阵,包含单词在语料库中特定上下文中出现的次数信息。行是单词,列是上下文。然后,这个矩阵会被分解以减少维度,新的矩阵为每个单词提供了一个向量表示。

GloVe 还具有预训练的单词,并附有向量,可以在语义匹配语料库和任务时使用。以下活动将引导您完成在 Python 中实现 GloVe 的过程,不过代码不会直接给出,所以您需要动脑筋,甚至可能需要一些谷歌搜索。试试看!

练习 9:使用 GloVe 生成词嵌入

在本练习中,我们将使用Glove-Python生成词嵌入。

注意

要在您的平台上安装 Glove-Python,请访问 https://pypi.org/project/glove/#files。

http://mattmahoney.net/dc/text8.zip 下载 Text8Corpus。

提取文件并将其与您的 Jupyter 笔记本一起存储。

  1. 导入itertools

    import itertools
    
  2. 我们需要一个语料库来生成词嵌入,而幸运的是,gensim.models.word2vec库中有一个名为Text8Corpus的语料库。导入它和Glove-Python库中的两个模块:

    from gensim.models.word2vec import Text8Corpus
    from glove import Corpus, Glove
    
  3. 使用itertools将语料库转换为列表形式的句子:

    sentences = list(itertools.islice(Text8Corpus('text8'),None))
    
  4. 启动Corpus()模型并将其拟合到句子上:

    corpus = Corpus()
    corpus.fit(sentences, window=10)
    

    window参数控制考虑多少个相邻单词。

  5. 现在我们已经准备好了语料库,需要训练嵌入。启动Glove()模型:

    glove = Glove(no_components=100, learning_rate=0.05)
    
  6. 基于语料库生成共现矩阵,并将glove模型拟合到此矩阵上:

    glove.fit(corpus.matrix, epochs=30, no_threads=4, verbose=True)
    

    模型已训练完成!

  7. 添加语料库的字典:

    glove.add_dictionary(corpus.dictionary)
    
  8. 使用以下命令查看根据生成的词嵌入,哪些单词与您选择的单词相似:

    glove.most_similar('man')
    

    预期输出:

图 1.25:'man'的词嵌入输出

图 1.25:'man'的词嵌入输出

您可以尝试对多个不同的单词进行此操作,看看哪些单词与它们相邻并且最相似:

glove.most_similar('queen', number = 10)

预期输出:

图 1.26:'queen'的词嵌入输出

图 1.26:'queen'的词嵌入输出

注意

若要了解更多关于 GloVe 的信息,请访问 https://nlp.stanford.edu/projects/glove/。

活动 1:使用 Word2Vec 从语料库生成词嵌入。

您的任务是基于特定语料库(此处为 Text8Corpus)训练 Word2Vec 模型,确定哪些单词彼此相似。以下步骤将帮助您解决问题。

注意

您可以在 http://mattmahoney.net/dc/text8.zip 找到文本语料库文件。

  1. 从先前给出的链接上传文本语料库。

  2. gensim模型中导入word2vec

  3. 将语料库存储在一个变量中。

  4. 在语料库上拟合 word2vec 模型。

  5. 找到与“man”最相似的单词。

  6. 'Father' 对 'girl','x' 对 'boy'。 找到 x 的前 3 个单词。

    注意

    活动的解决方案可以在第 296 页找到。

    预期输出:

图 1.27:相似词嵌入的输出

图 1.27:相似词嵌入的输出

'x'的前三个词可能是:

图 1.28:'x'的前三个词的输出

图 1.28:'x'的前三个词的输出

摘要

在本章中,我们了解了自然语言处理如何使人类和机器能够使用自然语言进行交流。自然语言处理有三大应用领域,分别是语音识别、自然语言理解和自然语言生成。

语言是复杂的,因此文本需要经过多个阶段,才能让机器理解。这个过滤过程称为文本预处理,包含了多种技术,服务于不同的目的。它们都是任务和语料库依赖的,并为将文本输入到机器学习和深度学习模型中做好准备。

由于机器学习和深度学习模型最适合处理数值数据,因此有必要将预处理过的语料库转换为数值形式。此时,词嵌入便登场了;它们是单词的实值向量表示,帮助模型预测和理解单词。生成词嵌入的两种主要算法是 Word2Vec 和 GloVe。

在下一章中,我们将基于自然语言处理算法进行深入探讨。我们将介绍和解释词性标注和命名实体识别的过程。

第三章:第二章

自然语言处理的应用

学习目标

在本章结束时,你将能够:

  • 描述词性标注及其应用

  • 区分基于规则的词性标注器和基于随机的词性标注器

  • 对文本数据执行词性标注、词块分析和分块

  • 执行命名实体识别进行信息提取

  • 开发并训练你自己的词性标注器和命名实体识别器

  • 使用 NLTK 和 spaCy 执行词性标注(POS tagging)、词块分析(chunking)、分块(chinking)和命名实体识别(Named Entity Recognition)

本章旨在向你介绍自然语言处理的众多应用及其涉及的各种技术。

引言

本章从快速回顾自然语言处理是什么以及它可以提供哪些服务开始。接着,讨论自然语言处理的两个应用:词性标注(POS tagging)命名实体识别(Named Entity Recognition)。解释了这两种算法的功能、必要性和目的。此外,还有一些练习和活动,用于执行词性标注和命名实体识别,并构建和开发这些算法。

自然语言处理旨在帮助机器理解人类的自然语言,以便有效地与人类沟通并自动化大量任务。上一章讨论了自然语言处理的应用以及这些技术如何简化人类生活的实际案例。本章将特别探讨其中的两种算法及其现实应用。

自然语言处理的各个方面都可以类比为语言教学。在上一章中,我们看到机器需要被告知关注语料库的哪些部分,哪些部分是无关紧要的。它们需要被训练去去除停用词和噪声元素,专注于关键词,将同一词的不同形式归约为词根,这样就能更容易地进行搜索和解读。以类似的方式,本章讨论的两种算法也教会机器一些关于语言的特定知识,正如我们人类在学习语言时所经历的。

词性标注

在我们深入了解算法之前,先来了解什么是词性。词性是我们在学习英语的早期阶段被教授的内容。它们是根据单词的句法或语法功能对单词进行分类的方式。这些功能是不同单词之间存在的功能性关系。

词性

英语有九大主要词性:

  • 名词:事物或人

  • 示例:table(桌子)、dog(狗)、piano(钢琴)、London(伦敦)、towel(毛巾)

  • 代词:替代名词的词

  • 示例:I(我)、you(你)、he(他)、she(她)、it(它)

  • 动词:表示动作的词

  • 示例:to be(是)、to have(有)、to study(学习)、to learn(学习)、to play(玩)

  • 形容词:描述名词的词

  • 示例:intelligent(聪明的)、small(小的)、silly(傻的)、intriguing(有趣的)、blue(蓝色的)

  • 限定词:限制名词的词

  • 示例:a few(一些)、many(许多)、some(一些)、three(三个)

    注意

    想了解更多限定词的例子,请访问 www.ef.com/in/english-resources/english-grammar/determiners/

  • 副词:描述动词、形容词或副词本身的词

  • 示例:quickly(快速地)、shortly(很快)、very(非常)、really(真的)、drastically(急剧地)

  • 介词:将名词与其他词连接的词

  • 示例:to(到)、on(在……上)、in(在……里面)、under(在……下)、beside(在……旁边)

  • 连词:连接两个句子或词语的词

  • 示例:and(和)、but(但是)、yet(然而)

  • 感叹词:表示惊叹的词语

  • 示例:ouch!(哎呀!)、Ow!(啊!)、Wow!(哇!)

如你所见,每个词都被分配了一个特定的词性标签,这帮助我们理解词的意义和用途,使我们更好地理解其使用的上下文。

词性标注器

词性标注是为一个词分配标签的过程。这是通过一种叫做词性标注器的算法完成的。该算法的目标其实就是这么简单。

大多数词性标注器是监督式学习算法。如果你不记得什么是监督式学习算法,它是基于先前标注的数据学习执行任务的机器学习算法。算法将数据行作为输入。这些数据包含特征列——用于预测某些事物的数据——通常还包括一个标签列——需要预测的事物。模型在这些输入数据上进行训练,学习和理解哪些特征对应于哪些标签,从而学会如何执行预测标签的任务。最终,它们会接收到未标注的数据(仅包含特征列的数据),并根据这些数据预测标签。

以下图示是监督式学习模型的一般示意图:

图 2.1:监督式学习

图 2.1:监督式学习

注意

想了解更多关于监督式学习的信息,请访问 www.packtpub.com/big-data-and-business-intelligence/applied-supervised-learning-python

因此,词性标注器通过学习先前标注过的数据集来提升其预测能力。在这种情况下,数据集可以包含各种特征,比如词本身(显然)、词的定义、词与其前后及其他相关词语(在同一句话、短语或段落中出现的)之间的关系。这些特征共同帮助标注器预测应该为一个词分配什么词性标签。用于训练监督式词性标注器的语料库被称为预标注语料库。这样的语料库作为构建系统的基础,帮助词性标注器为未标注的词进行标注。这些系统/类型的词性标注器将在下一节中讨论。

然而,预标注的语料库并不总是现成可用的,而且为了准确训练标注器,语料库必须足够大。因此,最近有一些词性标注器的迭代版本可以被视为无监督学习算法。这些算法以仅包含特征的数据为输入。这些特征不与标签相关联,因此,算法不是预测标签,而是根据输入数据形成分组或聚类。

在词性标注的过程中,模型使用计算方法自动生成一组词性标签。而预标注语料库则在监督式词性标注器的情况下帮助创建标注器的系统,而在无监督词性标注器的情况下,这些计算方法则作为创建此类系统的基础。无监督学习方法的缺点在于,自动生成的词性标签集可能并不像用于训练监督方法的预标注语料库中的标签那么准确。

总结来说,监督学习和无监督学习方法的关键区别如下:

  • 监督式词性标注器以预标注的语料库作为输入进行训练,而无监督词性标注器则以未标注的语料库作为输入,创建一组词性标签。

  • 监督式词性标注器根据标注语料库创建包含相应词性标签的单词词典,而无监督式词性标注器则使用自创的词性标签集生成这些词典。

几个 Python 库(如 NLTK 和 spaCy)都有自己训练的词性标注器。你将在接下来的章节中学习如何使用其中一个,但现在先让我们通过一个示例来理解词性标注器的输入和输出。需要记住的一个重要点是,由于词性标注器会为给定语料库中的每个单词分配一个词性标签,因此输入需要以单词标记的形式提供。因此,在进行词性标注之前,需要对语料库进行标记化处理。假设我们给训练过的词性标注器输入以下标记:

['I', 'enjoy', 'playing', 'the', 'piano']

在进行词性标注后,输出将类似于以下内容:

['I_PRO', 'enjoy_V', 'playing_V', 'the_DT', piano_N']

这里,PRO = 代词,V = 动词,DT = 限定词,N = 名词。

训练过的监督式和无监督式词性标注器的输入和输出是相同的:分别是标记和带词性标签的标记。

注意

这不是输出的确切语法;你将在进行练习时看到正确的输出。这里仅仅是为了让你了解词性标注器的作用。

上述的词性只是非常基础的标签,为了简化自然语言理解的过程,词性算法创建了更为复杂的标签,这些标签是这些基础标签的变体。以下是完整的带描述的词性标签列表:

图 2.2:带描述的词性标签

图 2.2:带描述的词性标签

这些标签来自宾夕法尼亚树库标签集(www.ling.upenn.edu/courses/Fall_2003/ling001/penn_treebank_pos.html),它是最流行的标签集之一。大多数英语语言的预训练标注器都使用这个标签集进行训练,包括 NLTK 的词性标注器。

词性标注的应用

就像文本预处理技术通过促使机器专注于重要细节,帮助机器更好地理解自然语言一样,词性标注帮助机器解读文本的上下文,从而理解它。虽然文本预处理更像是清理阶段,但词性标注实际上是机器开始独立输出关于语料库有价值信息的部分。

理解哪些词对应哪些词性,对于机器处理自然语言有多种好处:

  • 词性标注对于区分同形异义词非常有用——这些词虽然拼写相同,但意思却不同。例如,单词“play”可以作为动词,表示参与某项活动,也可以作为名词,表示一种在舞台上表演的戏剧作品。词性标注器可以帮助机器通过确定“play”的词性标签,理解该词在上下文中的使用情况。

  • 词性标注建立在句子和单词分割的需求之上——这是自然语言处理的基本任务之一。

  • 词性标签被用于其他算法执行更高层次的任务,其中之一就是我们将在本章中讨论的命名实体识别。

  • 词性标签也有助于情感分析和问答的过程。例如,在句子“Tim Cook 是这家科技公司的 CEO”中,你希望机器能够将“这家科技公司”替换为公司的名称。词性标注可以帮助机器识别出“这家科技公司”是一个限定词((this)+名词短语(technology company))。它可以利用这些信息,例如,搜索网上的文章并检查“Tim Cook 是 Apple 的 CEO”出现的次数,进而判断 Apple 是否是正确答案。

因此,词性标注是理解自然语言过程中的一个重要步骤,因为它有助于其他任务的完成。

词性标注器的类型

正如我们在前一节中所看到的,词性标注器可以是监督学习型或无监督学习型。这一差异在很大程度上影响了标注器的训练方式。还有另一个区分点影响着标注器如何为未标注的词分配标签,这就是用于训练标注器的方法。

词性标注器有两种类型:基于规则的和随机的。我们来看看这两者。

基于规则的词性标注器

这些词性标注器的工作方式几乎完全如其名称所示——通过规则。为标注器提供规则集的目的是确保它们在大多数情况下准确标注歧义/未知单词,因此大多数规则仅在标注器遇到歧义/未知单词时应用。

这些规则通常被称为上下文框架规则,向标注器提供上下文信息,以帮助它们理解给歧义单词分配哪个标签。一个规则的示例是:如果一个歧义/未知单词 x 前面跟着一个限定词,后面跟着一个名词,则为其分配形容词标签。一个例子是“one small girl”,其中“one”是限定词,“girl”是名词,因此标注器会给“small”分配形容词标签。

规则取决于你对语法的理论。此外,规则通常还包括诸如大写和标点符号的规则。这可以帮助你识别代词,并将其与句子开头(跟随句号)出现的单词区分开来。

大多数基于规则的词性标注器是监督学习算法,旨在学习正确的规则并将其应用于正确标注歧义词。然而,最近也有实验尝试使用无监督的方式训练这些标注器。未标记的文本提供给标注器进行标注,然后人类检查输出的标签,纠正任何不准确的标签。这些正确标注的文本随后会提供给标注器,以便它可以在两个不同的标签集之间发展出修正规则,并学习如何准确标注单词。

这种基于修正规则的词性标注器的一个例子是布里尔的标注器,它遵循之前提到的过程。它的功能可以与绘画艺术进行比较——在给房子上色时,首先涂上房子的背景色(例如,一个棕色的方形),然后在背景上使用更细的画笔绘制细节,如门和窗户。类似地,布里尔的基于规则的词性标注器的目标是首先一般性地标注一个未标注的语料库,即使某些标签可能是错误的,然后重新检查这些标签,理解哪些标签是错误的并从中学习。

注意

练习 10-16 可以在同一个 Jupyter Notebook 中进行。

练习 10:执行基于规则的词性标注

NLTK 有一个基于规则的词性标注器。在本练习中,我们将使用 NLTK 的词性标注器进行词性标注。以下步骤将帮助你解决这个问题:

  1. 打开 cmd 或终端,具体取决于你的操作系统。

  2. 导航到所需路径,并使用以下命令启动Jupyter Notebook:

    jupyter notebook
    
  3. 导入nltkpunkt,如所示:

    import nltk
    nltk.download('punkt')
    nltk.download('averaged_perceptron_tagger')
    nltk.download('tagsets')
    
  4. 将输入字符串存储在一个名为s的变量中,如下所示:

    s = 'i enjoy playing the piano'
    
  5. 如示例所示,对句子进行分词:

    tokens = nltk.word_tokenize(s)
    
  6. 对词元应用词性标注器,然后打印标签集,如下所示:

    tags = nltk.pos_tag(tokens)
    tags
    

    你的输出将如下所示:

    图 2.3:标注输出

    图 2.3:标注输出
  7. 要理解 "NN" 词性标签的含义,你可以使用以下代码行:

    nltk.help.upenn_tagset("NN")
    

    输出结果将如下所示:

    图 2.4:名词详情

    图 2.4:名词详情

    你可以通过将“NN”替换为每个词性标签来对每个词性标签进行处理。

    让我们通过一个包含同义词的句子来尝试一下。

  8. 将包含同义词的输入字符串存储在一个名为 sent 的变量中:

    sent = 'and so i said im going to play the piano for the play tonight'
    
  9. 对这个句子进行分词处理,并像示例中一样应用词性标注器:

    tagset = nltk.pos_tag(nltk.word_tokenize(sent))
    tagset
    

    预期输出:

图 2.5:标注输出

图 2.5:标注输出

如你所见,第一个“play”实例被标注为“VB”,代表动词,基本形式,第二个“play”实例被标注为“NN”,代表名词。因此,词性标注器能够区分同义词和同一单词的不同实例。这有助于机器更好地理解自然语言。

随机词性标注器

随机词性标注器是使用除基于规则的方法之外的任何方法为单词分配标签的标注器。因此,有很多方法属于随机类别。所有在为单词确定词性标签时,采用统计方法(如概率和频率)模型的标注器都是随机模型。

我们将讨论三种模型:

  • 单字法或词频方法

  • n-gram 方法

  • 隐马尔可夫模型

单字法或词频方法

最简单的随机词性标注器仅根据一个词与标签出现的概率来为模糊的单词分配词性标签。这基本上意味着,标注器在训练集中最常与某个单词关联的标签,就是它为该模糊单词实例分配的标签。例如,假设训练集中,“beautiful”一词大多数情况下被标注为形容词。当词性标注器遇到“beaut”时,它不能直接标注该词,因为它不是一个有效的单词。这个词将是一个模糊词,因此它将计算它可能属于每个词性标签的概率,基于该词在不同实例中被标注为每个词性标签的频率。“beaut”可以看作是“beautiful”的模糊形式,并且由于“beautiful”大多数时候被标注为形容词,词性标注器也会将“beaut”标注为形容词。这被称为词频方法,因为标注器正在检查与单词关联的词性标签的频率。

n-gram 方法

这一方法建立在之前的基础上。名称中的 n 代表在确定一个单词属于某一词性标签的概率时考虑的单词数量。在单字法标注器中,n = 1,因此只考虑单词本身。增加 n 的值会导致标注器计算特定的 n 个词性标签序列共同出现的概率,并根据该概率为单词分配标签。

在为单词分配标签时,这些 POS 标注器通过考虑它的类型和前面 n 个单词的 POS 标签来创建单词的上下文。根据上下文,标注器选择最可能与前面单词标签序列一致的标签,并将其分配给所讨论的单词。最流行的 n-gram 标注器称为维特比算法。

隐马尔可夫模型

隐马尔可夫模型结合了词频方法和 n-gram 方法。马尔可夫模型描述了事件或状态的序列。每个状态发生的概率仅依赖于前一个事件的状态。这些事件基于观察。隐马尔可夫模型的“隐藏”方面在于,事件可能是一组隐藏状态。

在 POS 标注的情况下,观察结果是单词标记,而隐藏的状态集合是 POS 标签。工作方式是模型根据前一个词的标签计算当前词具有特定标签的概率。例如,P(V | NN)是当前单词是动词的概率,假设前一个单词是名词。

注意

这是对隐马尔可夫模型的非常基本的解释。要了解更多,请访问 https://medium.freecodecamp.org/an-introduction-to-part-of-speech-tagging-and-the-hidden-markov-model-953d45338f24。

要了解更多有关随机模型的信息,请访问 http://ccl.pku.edu.cn/doubtfire/NLP/Lexical_Analysis/Word_Segmentation_Tagging/POS_Tagging_Overview/POS Tagging Overview.htm。

之前提到的三种方法按顺序解释了每个模型是如何在前一个模型的基础上构建并提高精度的。然而,每个建立在前一个模型基础上的模型涉及更多的概率计算,因此将根据训练语料库的大小来进行更多的计算时间。因此,选择使用哪种方法取决于语料库的大小。

练习 11:执行随机 POS 标注

spaCy 的 POS 标注器是一种随机标注器。在本练习中,我们将使用 spaCy 的 POS 标注器对一些句子进行标记,以查看基于规则和随机标注之间的结果差异。以下步骤将帮助您解决问题:

注意

要安装 spaCy,请点击以下链接并按照说明操作:https://spacy.io/usage

  1. 导入spaCy

    import spacy
    
  2. 加载 spaCy 的'en_core_web_sm'模型:

    nlp = spacy.load('en_core_web_sm')
    

    spaCy 有针对不同语言的特定模型。'en_core_web_sm'模型是英语语言模型,已经在书面网络文本(如博客和新闻文章)上进行了训练,包括词汇、句法和实体。

    注意

    要了解更多有关 spaCy 模型的信息,请访问 https://spacy.io/models。

  3. 在您要为其分配 POS 标签的句子上拟合模型。让我们使用我们给 NLTK 的 POS 标注器的句子:

    doc = nlp(u"and so i said i'm going to play the piano for the play tonight")
    
  4. 现在,让我们对这个句子进行标记化,分配 POS 标签,并打印它们:

    for token in doc:
        print(token.text, token.pos_, token.tag_)
    

    预期输出:

图 2.6:词性标注输出

图 2.6:词性标注输出

若要理解词性标注的含义,可以使用以下代码:

spacy.explain("VBZ")

用你想了解的词性标注替换"VBZ"。在这种情况下,你的输出将是:

'verb, 3rd person singular present'

正如你所见,结果与 NLTK 词性标注器的输出基本相同。这是因为我们的输入非常简单。

分块处理

词性标注器处理单独的词元进行标注。然而,单独对每个词进行标注并不是理解语料库的最佳方式。例如,'United'和'Kingdom'分开时意义不大,但'United Kingdom'作为一个整体告诉机器这代表一个国家,从而提供更多的上下文和信息。正是在这个过程中,分块处理发挥了作用。

分块处理是一种算法,它以单词及其词性标注作为输入。它处理这些单独的词元及其标注,查看它们是否可以组合。一个或多个单独的词元的组合被称为一个分块,分配给该分块的词性标注称为分块标记。

分块标记是基本词性标记的组合。通过这些标记更容易定义短语,而且比简单的词性标记更高效。这些短语即为分块。有时单个词也会被视为一个分块并分配分块标记。常见的五种主要分块标记如下:

  • 名词短语 (NP):这些是以名词为核心词的短语。它们作为动词或动词短语的主语或宾语。

  • 动词短语 (VP):这些是以动词为核心词的短语。

  • 形容词短语 (ADJP):这些是以形容词为核心词的短语。形容词短语的主要功能是描述和修饰名词或代词。它们通常位于名词或代词之前或之后。

  • 副词短语 (ADVP): 这些是以副词为核心词的短语。它们通过提供描述和修饰名词或动词的细节,用来作为这些词的修饰语。

  • 介词短语 (PP):这些是以介词为核心词的短语。它们用来定位动作或实体在时间或空间中的位置。

例如,在句子“the yellow bird is slow and is flying into the brown house”中,以下短语将被分配相应的分块标记:

'the yellow bird' – NP

'is' – VP

'slow' – ADJP

'is flying' – VP

'into' – PP

'the brown house' – NP

因此,分块处理是在对语料库应用词性标注(POS tagging)之后进行的。这使得文本能够被分解为最简单的形式(单词的词元),对其结构进行分析,然后再重新组合成有意义的更高级别的分块。分块处理还有助于命名实体识别的过程。我们将在接下来的部分看到具体如何进行。

NLTK 库内的块解析器是基于规则的,因此需要提供一个正则表达式作为规则来输出具有其块标签的块。spaCy可以在没有规则的情况下执行分块。让我们看看这两种方法。

练习 12:使用 NLTK 执行分块

在这个练习中,我们将生成块和块标签。nltk有一个正则表达式解析器。这需要一个短语的正则表达式和相应的块标签作为输入。然后它在语料库中搜索这个表达式并分配标签。

由于分块与词性标记一起工作,我们可以在词性标记练习的基础上扩展代码。我们在 'tagset' 中保存了具有各自词性标记的标记。让我们使用它。以下步骤将帮助您解决问题:

  1. 创建一个正则表达式,将搜索一个名词短语,如下所示:

    rule = r"""Noun Phrase: {<DT>?<JJ>*<NN>}"""
    

    此正则表达式搜索一个限定词(可选)、一个或多个形容词,然后是一个名词。这将形成一个称为名词短语的块。

    注意

    如果您不知道如何编写正则表达式,请查看这些快速教程:https://www.w3schools.com/python/python_regex.asp https://pythonprogramming.net/regular-expressions-regex-tutorial-python-3/

  2. 创建一个RegexpParser的实例,并将规则传递给它:

    chunkParser = nltk.RegexpParser(rule)
    
  3. tagset 包含具有各自词性标记的标记传递给 chunkParser,以便它可以执行分块,然后绘制块:

    chunked = chunkParser.parse(tagset)
    chunked.draw()
    

    注意

    对于 .draw() 函数能正常工作,matplotlib 需要在您的机器上安装。

    您的输出将类似于这样:

    图 2.7:解析树。

    图 2.7:解析树。

    这是一个解析树。如您所见,分块过程已识别出名词短语并加标签,剩余的标记显示它们的词性标签。

  4. 让我们用另一句话试试同样的事情。将输入句子存储在另一个变量中:

    a = "the beautiful butterfly flew away into the night sky"
    
  5. 使用 NLTK 的 POS 标记器对句子进行标记化和词性标注:

    tagged = nltk.pos_tag(nltk.word_tokenize(a))
    
  6. 重复第 3 步:

    chunked2 = chunkParser.parse(tagged)
    chunked2.draw()              
    

    期望的输出:

图 2.8:分块的输出。

图 2.8:分块的输出。

练习 13:使用 spaCy 执行分块

在这个练习中,我们将使用 spaCy 实现分块。spaCy不需要我们制定规则来识别块;它自动识别块并告诉我们头词是什么,从而告诉我们块标签是什么。让我们使用与练习 12 相同的句子识别一些名词块。以下步骤将帮助您解决问题:

  1. 在这个句子上,使用 spaCy 的英文模型进行适配:

    doc = nlp(u"the beautiful butterfly flew away into the night sky")
    
  2. 在这个模型上应用 noun_chunks,对于每个块,打印块的文本、块的根词和连接根词与其头部的依赖关系:

    for chunk in doc.noun_chunks:
        print(chunk.text, chunk.root.text, chunk.root.dep_)
    

    期望的输出:

图 2.9:使用 spaCy 进行分块的输出

图 2.9:使用 spaCy 进行分块的输出

如您所见,与 NLTK 相比,spaCy 的分块要简单得多。

Chinking

切除操作是分块的扩展,正如你从它的名字中可能已经猜到的那样。它不是处理自然语言中的必选步骤,但它可以是有益的。

切除操作发生在分块之后。分块后,你会得到带有分块标签的块,以及带有词性标签的单个单词。通常,这些多余的单词是没有必要的。它们对最终结果或理解自然语言的整个过程没有贡献,因此会造成干扰。切除操作帮助我们通过提取块及其块标签来处理这个问题,形成标记语料库,从而去除不必要的部分。这些有用的块被称为切除块,一旦它们从标记语料库中提取出来。

例如,如果你只需要从语料库中提取名词或名词短语来回答诸如“这个语料库在讲什么?”这样的问题,你应该应用切除操作,因为它只会提取你需要的内容,并将其呈现在你眼前。让我们通过一个练习来验证这一点。

练习 14:执行切除操作

切除操作基本上是在改变你在语料库中寻找的东西。因此,应用切除操作涉及改变提供给 chinkParser 的规则(正则表达式)。以下步骤将帮助你完成解决方案:

  1. 创建一个规则,将整个语料库进行分块,并只从标记为名词或名词短语的单词或短语中创建切除块:

    rule = r"""Chink: {<.*>+}
                        }<VB.?|CC|RB|JJ|IN|DT|TO>+{"""
    

    这个规则的形式是正则表达式。基本上,这个正则表达式告诉机器忽略所有不是名词或名词短语的单词。当遇到名词或名词短语时,这个规则将确保它被提取为一个切除块。

  2. 创建一个 RegexpParser 实例并传入规则:

    chinkParser = nltk.RegexpParser(rule)
    
  3. chinkParser 提供包含带有相应 POS 标签的标记的 tagset,以便它可以执行切除操作,然后绘制切除块:

    chinked = chinkParser.parse(tagset)
    chinked.draw()
    

    预期输出:

图 2.10:切除操作的输出

图 2.10:切除操作的输出

如你所见,切除块已被高亮显示,并且只包含名词。

活动 2:构建和训练你自己的 POS 标注器

我们已经看过了如何使用现有的和预训练的 POS 标注器对单词进行词性标注。在这个活动中,我们将训练我们自己的 POS 标注器。这就像训练任何其他机器学习算法一样。以下步骤将帮助你完成解决方案:

  1. 选择一个语料库来训练标注器。你可以使用 nltk treebank 来进行操作。以下代码应该能帮助你导入 treebank 语料库:

    nltk.download('treebank')
    tagged = nltk.corpus.treebank.tagged_sents()
    
  2. 确定标注器在为单词分配标签时将考虑的特征。

  3. 创建一个函数,去除带标签的单词的标签,以便我们可以将它们传递给我们的标注器。

  4. 构建数据集并将数据分为训练集和测试集。将特征赋给 'X',并将 POS 标签附加到 'Y'。在训练集上应用这个函数。

  5. 使用决策树分类器训练标注器。

  6. 导入分类器,初始化它,在训练数据上拟合模型,并打印准确性分数。

    注意

    输出中的准确性分数可能会有所不同,这取决于使用的语料库。

    期望输出:

图 2.11:期望的准确性分数。

图 2.11:期望的准确性分数。

注意

该活动的解决方案可以在第 297 页找到。

命名实体识别

这是信息提取过程中的第一步。信息提取是机器从非结构化或半结构化文本中提取结构化信息的任务。这有助于机器理解自然语言。

在文本预处理和词性标注后,我们的语料库变得半结构化且机器可读。因此,信息提取是在我们准备好语料库后进行的。

以下图示为命名实体识别的示例:

图 2.12:命名实体识别示例

图 2.12:命名实体识别示例

命名实体

命名实体是可以归类为不同类别的现实世界对象,如人、地点和物品。基本上,它们是可以通过专有名称表示的词汇。命名实体还可以包括数量、组织、货币金额等。

一些命名实体及其所属类别示例如下:

  • 唐纳德·特朗普,人物

  • 意大利,地点

  • 瓶子,物体

  • 500 美元,货币

命名实体可以视为实体的实例。在之前的示例中,类别基本上是实体本身,命名实体是这些实体的实例。例如,伦敦是“城市”的一个实例,而“城市”是一个实体。

最常见的命名实体类别如下所示:

  • 组织(ORGANIZATION)

  • 人物(PERSON)

  • 地点(LOCATION)

  • 日期(DATE)

  • 时间(TIME)

  • 货币(MONEY)

  • 百分比(PERCENT)

  • 设施(FACILITY)

  • GPE(地理政治实体)

命名实体识别器

命名实体识别器是识别和提取语料库中的命名实体并为其分配类别的算法。提供给经过训练的命名实体识别器的输入是标记化的词语及其相应的词性标注。命名实体识别的输出是命名实体及其类别,并与其他标记化的词语及其词性标注一起给出。

命名实体识别问题分为两个阶段:

  1. 识别和识别命名实体(例如,“伦敦”)

  2. 对这些命名实体进行分类(例如,“伦敦”是一个“地点”)

第一阶段的命名实体识别与分块过程相似,因为其目标是识别由专有名词表示的事物。命名实体识别器需要关注连续的词元序列,以便正确地识别命名实体。例如,“美国银行”应该被识别为一个单一的命名实体,尽管该短语包含了“美国”这个词,而“美国”本身就是一个命名实体。

与词性标注器类似,大多数命名实体识别器都是监督学习算法。它们在包含命名实体及其所属类别的输入数据上进行训练,从而使算法能够学习如何在未来对未知命名实体进行分类。

这种包含命名实体及其相应类别的输入数据通常被称为知识库。一旦命名实体识别器经过训练并面对未识别的语料库,它会参考这个知识库,寻找最准确的分类,以分配给命名实体。

然而,由于监督学习需要大量标注数据,未监督学习版本的命名实体识别器也正在进行研究。这些模型在未标注的语料库上进行训练——这些文本中没有被分类的命名实体。与词性标注器类似,命名实体识别器会对命名实体进行分类,然后不正确的分类会由人工进行修正。修正后的数据会反馈给命名实体识别器,从而使它们能够从错误中学习。

命名实体识别的应用

如前所述,命名实体识别是信息提取的第一步,因此在使机器理解自然语言并执行各种基于此的任务中起着重要作用。命名实体识别可以并且已经在多个行业和场景中被使用,以简化和自动化过程。让我们来看几个应用案例:

  • 在线内容,包括文章、报告和博客文章,通常会被打上标签,以便用户更轻松地进行搜索,并快速了解内容的主要信息。命名实体识别器可以用来扫描这些内容并提取命名实体,以自动生成这些标签。这些标签也有助于将文章归类到预定义的层级中。

  • 搜索算法同样也受益于这些标签。如果用户向搜索算法输入一个关键词,算法不需要遍历每篇文章中的所有单词(这会耗费大量时间),它只需要参考命名实体识别所生成的标签,就可以快速提取出包含或与输入关键词相关的文章。这大大减少了计算时间和操作量。

  • 这些标签的另一个用途是创建高效的推荐系统。如果你阅读了一篇关于印度当前政治局势的文章,文章可能被标记为“印度政治”(这只是一个例子),那么新闻网站可以利用这个标签来推荐具有相同或相似标签的不同文章。在视觉娱乐领域,如电影和电视节目也是如此。在线视频平台会使用分配给内容的标签(例如“动作”、“冒险”、“惊悚”等类型),以更好地了解你的口味,从而向你推荐相似的内容。

  • 客户反馈 对于任何提供服务或产品的公司都至关重要。通过命名实体识别器处理客户投诉和评价,可以生成标签,帮助基于地点、产品类型和反馈类型(正面或负面)对它们进行分类。这些评价和投诉随后可以发送给负责该产品或该领域的人,并根据反馈是正面还是负面进行处理。对推文、Instagram 标题、Facebook 帖子等也可以进行类似操作。

如你所见,命名实体识别有许多应用。因此,理解它是如何工作的,以及如何实现它,非常重要。

命名实体识别器的类型

与 POS 标注器一样,设计命名实体识别器有两种主要方法:通过定义规则来识别实体的语言学方法,或者使用统计模型的随机方法来准确确定命名实体属于哪一类别。

基于规则的 NER

基于规则的 NER 的工作方式与基于规则的 POS 标注器相同。

随机 NER

这些包括所有使用统计学命名和识别实体的模型。对于随机命名实体识别,有几种方法。让我们来看一下其中的两种:

练习 15:使用 NLTK 执行命名实体识别

在本练习中,我们将使用 NLTKne_chunk 算法对一个句子进行命名实体识别。与前几次练习中使用的句子不同,创建一个包含可以分类的专有名词的新句子,这样你就能实际看到结果:

  1. 将输入句子存储在一个变量中,如下所示:

    ex = "Shubhangi visited the Taj Mahal after taking a SpiceJet flight from Pune."
    
  2. 对句子进行分词,并为标记分配 POS 标签

    tags = nltk.pos_tag(nltk.word_tokenize(ex))
    
  3. 对标记过的词语应用 ne_chunk() 算法,并打印或绘制结果:

    ne = nltk.ne_chunk(tags, binary = True)
    ne.draw()
    

    True 的值赋给 binary 参数,告诉算法仅识别命名实体,而不对其进行分类。因此,你的结果将类似于以下内容:

    图 2.13:带有 POS 标签的命名实体识别输出

    图 2.13:带有词性标注的命名实体识别输出

    正如你所看到的,命名实体被标记为 'NE'。

  4. 要知道算法为这些命名实体分配了哪些类别,只需将 'binary' 参数的值设置为 'False':

    ner = nltk.ne_chunk(tags, binary = False)
    ner.draw()
    

    预期输出:

图 2.14:带有命名实体的输出

图 2.14:带有命名实体的输出

该算法准确地将 'Shubhangi' 和 'SpiceJet' 分类。'Taj Mahal' 这一项不应该是组织(ORGANIZATION),它应该是设施(FACILITY)。因此,NLTK 的 ne_chunk() 算法并不是最佳选择。

练习 16:使用 spaCy 进行命名实体识别

在这个练习中,我们将实现 spaCy 的命名实体识别器,处理前一个练习中的句子并比较结果。spaCy 有多个命名实体识别模型,这些模型在不同的语料库上进行训练。每个模型有不同的类别集;以下是 spaCy 可以识别的所有类别的列表:

图 2.15:spaCy 的类别

图 2.15:spaCy 的类别

以下步骤将帮助你解决问题:

  1. 在前一个练习中使用的句子上适配 spaCy 的英语模型:

    doc = nlp(u"Shubhangi visited the Taj Mahal after taking a SpiceJet flight from Pune.")
    
  2. 对于该句中的每个实体,打印实体的文本和标签:

    for ent in doc.ents:
        print(ent.text, ent.label_)
    

    你的输出将像这样:

    图 2.16:命名实体输出

    图 2.16:命名实体输出

    它只识别了 'SpiceJet' 和 'Pune' 作为命名实体,而没有识别 'Shubhangi' 和 'Taj Mahal'。让我们试着给 'Shubhangi' 加上一个姓氏,看看是否有所不同。

  3. 在新句子上适配模型:

    doc1 = nlp(u"Shubhangi Hora visited the Taj Mahal after taking a SpiceJet flight from Pune.")
    
  4. 重复步骤 2:

    for ent in doc1.ents:
        print(ent.text, ent.label_)
    

    预期输出:

图 2.17:使用 spaCy 进行命名实体识别的输出。

图 2.17:使用 spaCy 进行命名实体识别的输出。

现在我们已经加上了姓氏,“Shubhangi Hora” 被识别为一个 PERSON,“Taj Mahal” 被识别为 WORK_OF_ART。后者是不正确的,因为如果你查看类别表,WORK_OF_ART 用来描述歌曲和书籍。

因此,命名实体的识别和分类强烈依赖于识别器所训练的数据。这一点在实现命名实体识别时需要记住;通常来说,为特定的使用案例训练和开发自己的识别器会更好。

活动 3:在标注语料库上执行 NER

现在我们已经看到了如何在句子上执行命名实体识别,在这个活动中,我们将在一个经过词性标注的语料库上执行命名实体识别。假设你有一个语料库,已经为其标注了词性标签,现在你的任务是从中提取实体,以便你能够提供一个关于该语料库讨论内容的总体总结。以下步骤将帮助你解决问题:

  1. 导入 NLTK 和其他必要的包。

  2. 打印 nltk.corpus.treebank.tagged_sents() 来查看你需要提取命名实体的标注语料库。

  3. 将标注句子的第一句存储到一个变量中。

  4. 使用nltk.ne_chunk对句子进行命名实体识别(NER)。将binary设置为True并打印命名实体。

  5. 对任意数量的句子重复步骤 3 和步骤 4,查看语料库中存在的不同实体。将binary参数设置为False,查看命名实体的分类。

    预期输出:

图 2.18:对标注语料进行 NER 的预期输出

图 2.18:对标注语料进行 NER 的预期输出

注意

该活动的解决方案可以在第 300 页找到。

总结

自然语言处理使机器能够理解人类的语言,就像我们学习如何理解和处理语言一样,机器也在被教导。两种能让机器更好理解语言并为现实世界做出贡献的方法是词性标注和命名实体识别。

前者是将词性标签(POS)分配给单个单词,以便机器能够学习上下文,后者则是识别并分类命名实体,从语料库中提取有价值的信息。

这些过程的执行方式有所不同:算法可以是有监督的或无监督的,方法可以是基于规则的或随机的。无论哪种方式,目标都是一样的,即理解并与人类进行自然语言交流。

在下一章中,我们将讨论神经网络,它们如何工作,以及如何在自然语言处理中使用它们。

第四章:第三章

神经网络简介

学习目标

到本章结束时,你将能够:

  • 描述深度学习及其应用

  • 区分深度学习和机器学习

  • 探索神经网络及其应用

  • 了解神经网络的训练与工作原理

  • 使用 Keras 创建神经网络

本章旨在向你介绍神经网络、它们在深度学习中的应用及其普遍的缺点。

介绍

在前两章中,你了解了自然语言处理的基础知识、它的重要性、准备文本进行处理的步骤以及帮助机器理解并执行基于自然语言任务的两种算法。然而,为了应对更高层次、更复杂的自然语言处理问题,如创建类似SiriAlexa的个人语音助手,还需要额外的技术。深度学习系统,如神经网络,常用于自然语言处理,因此我们将在本章中讨论它们。在接下来的章节中,你将学习如何使用神经网络进行自然语言处理。

本章首先解释深度学习及其与机器学习的不同之处。然后,讨论神经网络,它是深度学习技术的核心部分,以及它们的基本功能和实际应用。此外,本章还介绍了Keras,一个 Python 深度学习库。

深度学习简介

人工智能是指拥有类似人类自然智能的智能体。这种自然智能包括计划、理解人类语言、学习、做决策、解决问题以及识别单词、图像和物体的能力。在构建这些智能体时,这种智能被称为人工智能,因为它是人为制造的。这些智能体并不指代物理对象。实际上,它们是指能展示人工智能的软件。

人工智能有两种类型——窄域人工智能和广域人工智能。窄域人工智能是我们目前所接触到的人工智能类型;它是任何拥有自然智能若干能力之一的单一智能体。本书第一章中你了解的自然语言处理应用领域就是窄域人工智能的例子,因为它们是能够执行单一任务的智能体,例如,机器能够自动总结文章。确实存在能够执行多项任务的技术,如自动驾驶汽车,但这些技术仍被认为是多个窄域人工智能的组合。

广义人工智能是指在一个智能体中拥有所有人类能力及更多能力,而不是一个智能体中仅有一到两个能力。AI 专家声称,一旦人工智能超越了广义人工智能的目标,在所有领域中比人类更聪明、更熟练,它将成为超级人工智能。

如前几章所述,自然语言处理是一种实现人工智能的方法,通过使机器能够理解并与人类以自然语言进行沟通。自然语言处理准备文本数据并将其转换为机器能够处理的形式——即数值形式。这就是深度学习的应用领域。

像自然语言处理和机器学习一样,深度学习也是一种技术和算法类别。它是机器学习的一个子领域,因为这两种方法共享相同的主要原理——无论是机器学习还是深度学习算法,都从输入中获取信息并使用它来预测输出。

图 3.1:深度学习作为机器学习的子领域

图 3.1:深度学习作为机器学习的一个子领域

当在训练数据集上训练时,两种类型的算法(机器学习和深度学习)都旨在最小化实际结果和预测结果之间的差异。这帮助它们在输入和输出之间建立关联,从而提高准确性。

比较机器学习和深度学习

虽然这两种方法都基于相同的原理——从输入预测输出——但它们通过不同的方式实现这一点,这也是深度学习被归类为一种独立方法的原因。此外,深度学习出现的一个主要原因是这些模型在预测过程中的准确性得到了提高。

虽然机器学习模型相当自足,但它们仍然需要人工干预来判断预测是否错误,因此需要在执行特定任务时变得更好。而深度学习模型则能够自己判断预测是否错误。因此,深度学习模型是自足的;它们能够在没有人工干预的情况下做出决策并提高效率。

为了更好地理解这一点,让我们以一个可以通过语音命令控制温度设置的空调为例。假设当空调听到“热”这个词时,它会降低温度,而当它听到“冷”这个词时,它会升高温度。如果这是一个机器学习模型,那么空调会随着时间的推移学会在不同的句子中识别这两个词。然而,如果这是一个深度学习模型,它可以根据与“热”和“冷”类似的词语和句子(如“有点热”或“我快冻死了!”等)来学习调整温度。

这是一个直接与自然语言处理相关的例子,因为该模型能够理解人类的自然语言,并根据其理解做出反应。在本书中,我们将专注于使用深度学习模型进行自然语言处理,尽管实际上它们几乎可以应用于每个领域。目前,它们在自动化驾驶任务中也有所应用,使得车辆能够识别停车标志、读取交通信号,并在行人面前停车。医疗领域也在利用深度学习方法检测早期的疾病——如癌细胞。然而,由于本书的重点是让机器理解人类的自然语言,我们还是回到这个主题。

深度学习技术通常用于监督学习方式,即它们会接受标记数据进行学习。然而,机器学习方法与深度学习方法之间的关键区别在于,后者需要极为庞大的数据量,这是之前不存在的。因此,深度学习直到最近才变得具有优势。它还需要相当大的计算能力,因为它需要在如此庞大的数据集上进行训练。

然而,主要的区别在于算法本身。如果你以前学习过机器学习,那么你应该知道解决分类和回归问题的各种算法,以及无监督学习的问题。深度学习系统与这些算法的不同之处在于,它们使用的是人工神经网络。

神经网络

神经网络和深度学习通常是互换使用的术语。它们并不意味着相同的东西,因此让我们来了解它们之间的区别。

如前所述,深度学习是一种遵循与机器学习相同原则的方法,但它具备更高的准确性和效率。深度学习系统利用人工神经网络,这些神经网络本身就是计算模型。因此,基本上,神经网络是深度学习方法的一部分,但并不是深度学习方法的全部。它们是被深度学习方法所整合的框架。

图 3.2: 神经网络作为深度学习方法的一部分

图 3.2: 神经网络作为深度学习方法的一部分

人工神经网络基于一个受人脑中生物神经网络启发的框架。这些神经网络由节点组成,使得网络能够从图像、文本、现实物体等中学习,从而能够执行任务并进行准确预测。

神经网络由多个层组成,我们将在接下来的部分中深入了解。这些层的数量可以从三层到数百层不等。由三层或四层构成的神经网络称为浅层神经网络,而层数更多的网络则被称为深度神经网络。因此,深度学习方法使用的神经网络是深度神经网络,它们包含多个层。由于这一点,深度学习模型非常适合处理复杂任务,如人脸识别、文本翻译等。

这些层将输入分解为多个抽象级别。因此,深度学习模型能够更好地从输入中学习并理解,无论是图像、文本还是其他形式的输入,这有助于它做出决策并像人类大脑一样进行预测。

让我们通过一个例子来理解这些层。假设你正在卧室里做些工作,突然注意到自己在出汗。这就是你的输入数据——你感到很热,于是脑海中浮现出一个声音:“我觉得很热!”接着,你可能会想为什么自己会感到这么热:“为什么我会这么热?”这是一个思考。然后你会尝试解决这个问题,或许通过洗个澡来缓解:“让我快速洗个澡。”这是你做出的决策。但随后你记得自己很快就要出门上班:“但是,我得很快离开家。”这是一个记忆。你可能会尝试说服自己:“其实,还是有足够时间快速洗个澡吧?”这是推理的过程。最后,你可能会根据自己的想法做出行动,或者心里想着:“我要去洗澡了”,或者“没时间洗澡了,算了。”这就是决策过程,如果你真的去洗了澡,那就是一种行动。

深度神经网络中的多层结构使得模型能够像大脑一样经历不同的处理层级,从而建立起生物神经网络的原理。这些层正是深度学习模型能够高精度完成任务和预测输出的原因。

神经网络架构

神经网络架构指的是构成神经网络的基本元素。尽管有多种不同类型的神经网络,但基本架构和基础结构保持不变。该架构包括:

  • 节点

  • 边缘

  • 偏差

  • 激活函数

如前所述,神经网络由多个层组成。虽然这些层的数量因模型而异,并且依赖于当前的任务,但只有三种类型的层。每一层由多个节点组成,节点的数量取决于该层以及整个神经网络的需求。一个节点可以看作是一个神经元。

神经网络中的层如下所示:

  • 输入层

    顾名思义,这一层由进入神经网络的输入数据组成。它是一个必需的层,因为每个神经网络都需要输入数据进行学习和执行操作,从而生成输出。此层在神经网络中只能出现一次。每个输入节点与后续层中的每个节点相连。

    输入数据的变量或特征被称为特征。目标输出依赖于这些特征。例如,以鸢尾花数据集为例。(鸢尾花数据集是机器学习初学者中最流行的数据集之一。它包含三种不同类型花卉的数据。每个实例有四个特征和一个目标类别。)花卉的分类标签取决于四个特征——花瓣的长度和宽度,以及萼片的长度和宽度。特征,因此输入层,被表示为X,每个单独的特征被表示为X1X2、...、Xn

  • 隐藏层

    这是进行实际计算的层。它位于输入层之后,因为它作用于由输入层提供的输入,并且位于输出层之前,因为它生成由输出层提供的输出。

    隐藏层由称为“激活节点”的节点组成。每个节点拥有一个激活函数,这是一个对激活节点接收到的输入执行的数学函数,用于生成输出。本章后续会讨论激活函数。

    这是唯一可以多次出现的层,因此在深度神经网络中,可能存在最多上百个隐藏层。隐藏层的数量取决于具体任务。

    一个隐藏层的节点生成的输出将作为输入传递到下一个隐藏层。每个隐藏层的激活节点生成的输出被发送到下一层的每个激活节点。

  • 输出层

    这是神经网络的最后一层,包含提供所有处理和计算结果的节点。这也是一个必需的层,因为神经网络必须根据输入数据生成输出。

    以鸢尾花数据集为例,某一花卉实例的输出将是该花卉的类别——鸢尾花 Setosa、鸢尾花 Virginica 或鸢尾花 Versicolor。

    输出,通常称为目标,表示为y

图 3.3:具有 2 个隐藏层的神经网络

图 3.3:具有 2 个隐藏层的神经网络

节点

每个激活节点或神经元具有以下组件:

  • 激活

    这是节点的当前状态——它是否处于激活状态。

  • 阈值(可选)

    如果存在,该值决定了一个神经元是否被激活,具体取决于加权和是否高于或低于此阈值。

  • 激活函数

    这是根据输入和加权和计算激活节点的新激活值的函数。

  • 输出函数

    这会根据激活函数生成特定激活节点的输出。

    输入神经元没有像这样的组件,因为它们不进行计算,也没有前置神经元。类似地,输出神经元也没有这些组件,因为它们不进行计算,也没有后续神经元。

边缘

 图 3.4:神经网络的加权连接

图 3.4:神经网络的加权连接

在前面的图示中,每一条箭头代表两个不同层的节点之间的连接。这样的连接被称为边缘。每个指向激活节点的边缘都有自己的权重,可以视为一个节点对另一个节点的影响程度。权重可以是正的,也可以是负的。

看看之前的图示。在值到达激活函数之前,它们的值会先与分配给各自连接的权重相乘。然后这些乘积的值会相加,得到一个加权和。这个加权和本质上是衡量该节点对输出的影响程度。如果值较低,意味着它对输出的影响不大,因此也不那么重要。如果值较高,则意味着它与目标输出有强烈的相关性,因此在确定输出时起着重要作用。

偏置

偏置是一个节点,神经网络的每一层都有自己的偏置节点,输出层除外。因此,每一层都有自己的偏置节点。偏置节点保存一个值,称为偏置。这个值会在计算加权和的过程中被加入,因此它在确定节点生成的输出中也起到了作用。

偏置是神经网络中的一个重要方面,因为它允许激活函数向右或向左平移。这有助于模型更好地拟合数据,从而生成准确的输出。

激活函数

激活函数是神经网络隐含层中激活节点的一部分。它们的作用是为神经网络引入非线性,这是非常重要的,因为没有它们,神经网络将只有线性函数,这样就和线性回归模型没有区别了。这就违背了神经网络的初衷,因为没有非线性,神经网络就无法学习数据中的复杂函数关系。激活函数还需要是可微的,以便进行反向传播。这个内容将在本章的后续部分讨论。

基本上,一个激活节点计算它接收到的输入的加权和,添加偏置值,然后对这个值应用激活函数。这会为该特定激活节点生成一个输出,该输出随后作为输入传递给下一层。这个输出被称为激活值。因此,下一层的激活节点将接收到来自前一层激活节点的多个激活值,并计算一个新的加权和。它会对这个值应用激活函数,生成它自己的激活值。这就是数据在神经网络中流动的方式。因此,激活函数帮助将输入信号转换为输出信号。

计算加权和、应用激活函数并产生激活值的过程称为前向传播。

有几种激活函数(如 Logistic、TanH、ReLU 等)。Sigmoid 函数是其中最流行且最简单的激活函数之一。当用数学形式表示时,这个函数看起来像

图 3.5:Sigmoid 函数的表达式

图 3.5:Sigmoid 函数的表达式

如你所见,这个函数是非线性的。

训练神经网络

到目前为止,我们知道,一旦输入提供给神经网络,它会进入输入层,这是一个用于将输入传递到下一层的接口。如果存在隐藏层,则输入会通过加权连接发送到隐藏层的激活节点。激活节点接收到的所有输入的加权和是通过将输入与各自的权重相乘,然后将这些值加上偏置值来计算的。激活函数从加权和中生成激活值,并将其传递到下一层的节点。如果下一层是另一个隐藏层,则它将使用来自前一隐藏层的激活值作为输入,并重复激活过程。然而,如果下一层是输出层,则神经网络会提供输出。

从这些信息中,我们可以得出结论,深度学习模型中有三个部分会影响模型生成的输出——输入、连接权重和偏置、以及激活函数。

图 3.6:影响输出的深度学习模型方面

图 3.6:影响输出的深度学习模型方面

虽然输入来自数据集,但前两个部分不是。那么,接下来就会有两个问题:谁或什么决定连接的权重是多少?我们怎么知道该使用哪些激活函数?让我们逐一解决这些问题。

计算权重

权重在多层神经网络中起着非常重要的作用,因为改变单一连接的权重会完全改变分配给进一步连接的权重,从而影响后续层生成的输出。因此,拥有最优的权重对于创建一个准确的深度学习模型是必要的。听起来好像压力很大,但幸运的是,深度学习模型能够自主找到最优的权重。为了更好地理解这一点,让我们以线性回归为例。

线性回归是一种监督式机器学习算法,顾名思义,它适用于解决回归问题(输出为连续数值的数据集,例如房屋的售价)。该算法假设输入(特征)与输出(目标)之间存在线性关系。基本上,它认为存在一条最佳拟合线,可以准确描述输入和输出变量之间的关系。它使用这个关系来预测未来的数值。在只有一个输入特征的情况下,这条线的方程式为:

图 3.7:线性回归的表达式

图 3.7:线性回归的表达式

其中,

y 是目标输出

c 是 y 轴截距

m 是模型系数

x 是输入特征

类似于神经网络中的连接,输入特征也附带了数值——它们被称为模型系数。在某种程度上,这些模型系数决定了特征在确定输出中的重要性,这类似于神经网络中的权重作用。确保这些模型系数的值正确是非常重要的,以便获得准确的预测。

假设我们想预测房屋的售价,依据是它有多少个卧室。所以,房屋的售价是我们的目标输出,卧室的数量是我们的输入特征。由于这是一个监督学习方法,我们的模型将被提供一个数据集,其中包含输入特征与正确的目标输出的匹配实例。

图 3.8:线性回归的样本数据集

图 3.8:线性回归的样本数据集

现在,我们的线性回归模型需要找到一个模型系数,用来描述卧室数量对房屋售价的影响。它通过使用两种算法——损失函数和梯度下降算法——来实现这一目标。

损失函数

损失函数有时也被称为成本函数。

对于分类问题,损失函数计算特定类别的预测概率与该类别本身之间的差异。例如,假设你有一个二分类问题,需要预测一座房子是否会售出。只有两个输出——“是”和“否”。在拟合这个数据的分类模型时,模型会预测数据实例属于“是”类别或“否”类别的概率。假设“是”类别的值为 1,“否”类别的值为 0。因此,如果输出概率更接近 1,则它会落入“是”类别。该模型的损失函数将衡量这种差异。

对于回归问题,损失函数计算实际值与预测值之间的误差。上一节中的房价例子是一个回归问题,因此损失函数计算的是房子的实际价格与模型预测的价格之间的误差。因此,从某种意义上说,损失函数帮助模型自我评估其性能。显然,模型的目标是预测一个与实际价格完全相同,或者至少最接近的价格。为了做到这一点,它需要尽可能地最小化损失函数。

唯一直接影响模型预测价格的因素是模型系数。为了得到最适合当前问题的模型系数,模型需要不断改进模型系数的值。我们将每个不同的值称为模型系数的更新。因此,随着每次模型系数的更新,模型必须计算实际价格与使用该模型系数更新后的预测价格之间的误差。

一旦该函数达到了最小值,模型系数在此最小点的值被选为最终的模型系数。该值被存储,并在上述线性回归算法的线性方程中使用。从此之后,每当模型接收到房子的卧室数量等输入数据而没有目标输出时,它会使用带有适当模型系数的线性方程来计算并预测这座房子将以多少价格售出。

有许多不同种类的损失函数——例如 MSE(用于回归问题)和 Log Loss(用于分类问题)。让我们来看看它们是如何工作的。

均方误差函数计算实际值与预测值之间的差异,将其平方后,再对整个数据集取平均。该函数用数学表达式表示如下:

图 3.9:均方误差函数的表达式

图 3.9:均方误差函数的表达式

其中,

n 是数据点的总数

yi 是第 i 个实际值

xi 是输入

f() 是对输入执行的函数,用来生成输出,因此

f(xi) 是预测值

对数损失用于输出为 0 到 1 之间概率值的分类模型。预测概率与实际类别之间的差异越大,对数损失越高。对数损失函数的数学表示为:

图 3.10:对数损失函数的表达式

图 3.10:对数损失函数的表达式

其中,

N 是数据点的总数

yi 是第 i 个实际标签

p 是预测概率

梯度下降算法

通过损失函数评估模型性能的过程是由模型独立执行的,更新并最终选择模型系数的过程也是如此。

假设你在一座山上,想要下山并到达真正的底部。天空多云,山峰众多,你无法确切知道底部在哪儿,也不知道应该朝哪个方向走,你只知道你需要到达那里。你从海拔 5000 米的地方开始,决定迈大步。你迈出一步,然后检查手机,看看自己距离海平面有多少米。手机显示你距离海平面 5003 米,说明你走错了方向。现在,你朝另一个方向迈大步,手机显示你距离海平面 4998 米。这意味着你离底部更近了,但你怎么知道这一步是下降最快的那一步呢?如果你朝另一个方向走,发现自己降到了 4996 米呢?因此,你会检查每个可能方向上的位置,选择那个最接近底部的方向。

你不断重复这个过程,直到你的手机显示你位于海拔 100 米的地方。当你再迈出一步时,手机的读数仍然保持不变——海拔 100 米。最终,你到达了一个看起来像是底部的地方,因为从这个点出发的任何方向,都会导致你依然处于海拔 100 米的位置。

图 3.11:更新参数

图 3.11:更新参数

这就是梯度下降算法的工作原理。该算法沿着损失函数与模型系数和截距的可能值的图形下降,就像你在下山一样。它从给定的模型系数值开始——这就像你站在海平面上方 5000 米的某个点。它会计算该点处图形的梯度。这个梯度告诉模型应该朝哪个方向移动,以更新系数,进而接近全局最小值,这也是最终目标。因此,它采取一步,来到了一个新点,拥有了新的模型系数。它重复计算梯度、获取移动方向、更新系数并采取一步的过程。它检查是否这一步提供了最陡的下降。每次它迈出一步,都到达一个新的模型系数,并计算该点的梯度。这个过程会重复,直到梯度的值在多次试验中不再变化。这意味着算法已经达到了全局最小值并且收敛。此时的模型系数会作为线性方程中的最终模型系数。

在神经网络中,梯度下降算法和损失函数共同作用,找到分配给连接的权重和偏置的值。这些值通过最小化损失函数来更新,使用的是梯度下降算法,就像线性回归模型中一样。此外,在线性回归的情况下,由于损失函数是碗形的,因此总是只有一个最小值。这使得梯度下降算法很容易找到它,并且可以确定这是最低点。然而,在神经网络的情况下,情况并不如此简单。神经网络使用的激活函数目的在于引入非线性因素。

因此,神经网络的损失函数图像并不是碗形曲线,并且它不只有一个最小点。相反,它有多个最小值,其中只有一个是全局最小值,其余的被称为局部最小值。这听起来像是一个重大问题,但实际上,梯度下降算法能够达到一个局部最小值并选择该点的权重值是没问题的,因为大多数局部最小值通常离全局最小值非常近。为了设计神经网络,也有一些修改版的梯度下降算法被使用。随机梯度下降和批量梯度下降就是其中的两种。

假设我们的损失函数是均方误差(MSE),并且我们需要梯度下降算法更新一个权重(w)和一个偏置(b)。

图 3.12:损失函数梯度的表达式

图 3.12:损失函数梯度的表达式

梯度是损失函数对权重和偏置的偏导数。其数学表示如下:

图 3.13:损失函数偏导数的梯度表示

图 3.13:损失函数偏导数的梯度表示

这样得到的结果是当前点的损失函数的梯度。这也告诉我们应该朝哪个方向移动,以继续更新权重和偏置。

每一步的步长大小是由一个称为学习率的参数来调整的,它是梯度下降算法中一个非常敏感的参数。它被称为 alpha,并用符号 α 表示。如果学习率太小,算法会采取过多的微小步骤,从而需要很长时间才能达到最小值。然而,如果学习率过大,算法可能会完全错过最小值。因此,调整并测试不同的学习率以确保选择正确的学习率非常重要。

学习率与每一步计算出的梯度相乘,以修改步长的大小,因此每一步的步长不一定相同。数学上,这可以表示为:

图 3.14:学习率与梯度相乘的表达式

图 3.14:学习率与梯度相乘的表达式

以及,

图 3.15:每一步学习率与梯度相乘的表达式

图 3.15:每一步学习率与梯度相乘的表达式

这些值是从之前的权重和偏置值中减去的,因为偏导数指向的是最陡上升的方向,而我们的目标是下降。

图 3.16:学习率

图 3.16:学习率

反向传播

线性回归本质上是一个神经网络,只不过没有隐藏层,并且激活函数是恒等函数(即线性函数,因此是线性的)。因此,学习过程与前面几节描述的相同——损失函数的目标是通过让梯度下降算法不断更新权重,直到达到全局最小值,从而最小化误差。

然而,在处理更大、更复杂的非线性神经网络时,计算出的损失会通过网络反向传播到每一层,然后开始更新权重的过程。损失被反向传播,因此这一过程被称为反向传播(Backpropagation)。

反向传播是通过使用损失函数的偏导数来执行的。它涉及到通过在神经网络中反向传播来计算每个层中每个节点的损失。了解每个节点的损失可以让网络理解哪些权重对输出和损失产生了剧烈的负面影响。因此,梯度下降算法可以减少这些连接的权重,这些连接的错误率较高,从而减少该节点对网络输出的影响。

当处理神经网络中的多层时,许多激活函数作用于输入。这个过程可以表示为如下:

图 3.17:反向传播函数的表达式

图 3.17:反向传播函数的表达式

这里XYZ是激活函数。正如我们所看到的,f(x)是一个复合函数,因此,反向传播可以视为链式法则的应用。链式法则是用于计算复合函数偏导数的公式,这正是我们在反向传播过程中所做的。通过将链式法则应用于前述的函数(通常称为前向传播函数,因为数值朝着前向方向流动以生成输出),并计算相对于每个权重的偏导数,我们将能够精确地确定每个节点对最终输出的影响程度。

输出层中最后一个节点的损失是整个神经网络的总损失,因为它位于输出层,因此所有前面节点的损失都会累积到这一层。输入层中的输入节点没有损失,因为它们对神经网络没有影响。输入层仅仅是一个接口,将输入发送到隐藏层中的激活节点。

因此,反向传播过程就是通过梯度下降算法和损失函数来更新权重的过程。

注意

如需了解更多关于反向传播的数学原理,请点击此链接:https://ml-cheatsheet.readthedocs.io/en/latest/backpropagation.html

设计神经网络及其应用

训练和设计神经网络时,通常使用一些常见的机器学习技术。神经网络可以被分类为:

  • 有监督神经网络

  • 无监督神经网络

有监督神经网络

这些就像前一节中使用的例子(根据房间数量预测房屋价格)。有监督神经网络是在由样本输入和其对应输出组成的数据集上进行训练的。这些方法适用于噪声分类和预测任务。

有两种类型的监督学习方法:

  • 分类

    这是针对那些目标输出为离散类别或类的问题,例如鸢尾花数据集。神经网络从样本输入和输出中学习如何正确分类新数据。

  • 回归

    这是针对那些目标输出为一系列连续数值的问题,比如房价的例子。神经网络描述了输入与输出之间的因果关系。

无监督神经网络

这些神经网络是在没有任何目标输出的数据上进行训练的,因此能够识别并提取数据中的模式和推断。这使得它们非常适合执行如识别类别关系和发现数据中自然分布等任务。

  • 聚类

聚类分析是将相似的输入分组在一起。这些神经网络可以用于基因序列分析和物体识别等任务。

能够进行模式识别的神经网络可以通过监督学习或无监督学习方法进行训练。它们在文本分类和语音识别中发挥着关键作用。

练习 17:创建神经网络

在这个练习中,我们将实现一个简单的经典神经网络,通过遵循之前概述的工作流程,来预测评论是正面还是负面。

这是一个自然语言处理问题,因为神经网络将接收一行行的句子,这些句子实际上是评论。每个评论在训练集中都有一个标签——0 表示负面,1 表示正面。这个标签依赖于评论中出现的单词,因此我们的神经网络需要理解评论的含义并据此进行标注。最终,我们的神经网络需要能够预测评论是正面还是负面。

注意

从链接下载数据集:

https://github.com/TrainingByPackt/Deep-Learning-for-Natural-Language-Processing/tree/master/Lesson 03

以下步骤将帮助你解决这个问题。

  1. 在你想要编写代码的目录中,输入以下命令来打开一个新的 Jupyter 笔记本:

    jupyter notebook
    
  2. 接下来,导入pandas,以便你可以将数据存储在数据框中:

    import pandas as pd
    df = pd.read_csv('train_comment_small_50.csv', sep=',')
    
  3. 导入正则表达式包

    import re
    
  4. 创建一个函数来预处理评论,去除HTML标签、转义引号和普通引号:

    def clean_comment(text):
        # Strip HTML tags
        text = re.sub('<[^<]+?>', ' ', text)
    
        # Strip escaped quotes
        text = text.replace('\\"', '')
    
        # Strip quotes
        text = text.replace('"', '')
    
        return text
    
  5. 将这个函数应用于当前存储在数据框中的评论:

    df['cleaned_comment'] = df['comment_text'].apply(clean_comment)
    
  6. scikit-learn中导入train_test_split,以便将这些数据分为训练集和验证集:

    from sklearn.model_selection import train_test_split
    X_train, X_test, y_train, y_test = train_test_split(df['cleaned_comment'], df['toxic'], test_size=0.2)
    
  7. nltk库中导入nltkstopwords

    import nltk
    nltk.download('stopwords')
    
  8. 现在,机器学习和深度学习模型要求输入数据为数值型数据,而我们当前的数据是文本形式。因此,我们将使用一种名为 CountVectorizer 的算法,将评论中的单词转换为词频向量。

    from sklearn.feature_extraction.text import CountVectorizer
    from nltk.corpus import stopwords
    
    vectorizer = CountVectorizer(binary=True, stop_words = stopwords.words('english'), lowercase=True, min_df=3, max_df=0.9, max_features=5000)
    X_train_onehot = vectorizer.fit_transform(X_train)
    

    我们的数据现在已经清理并准备好了!

  9. 我们将创建一个两层的神经网络。在定义神经网络时,层数不包括输入层,因为输入层是默认存在的,而且输入层不参与计算过程。因此,一个两层的神经网络包括一个输入层,一个隐藏层和一个输出层。

  10. 从 Keras 导入模型和层:

    from keras.models import Sequential
    from keras.layers import Dense
    
  11. 初始化神经网络:

    nn = Sequential()
    
  12. 添加隐藏层。指定该层的节点数量、节点的激活函数以及该层的输入:

    nn.add(Dense(units=500, activation='relu', input_dim=len(vectorizer.get_feature_names())))
    
  13. 添加输出层。同样,指定节点数量和激活函数。由于这是一个二分类问题(预测评论是正面还是负面),我们将在这里使用sigmoid函数。我们只会有一个输出节点,因为输出只是一个值——要么是 1,要么是 0\。

    nn.add(Dense(units=1, activation='sigmoid'))
    
  14. 现在我们要编译神经网络,并决定使用哪个损失函数、优化算法和性能指标。由于这是一个二分类问题,我们将使用binary_crossentropy作为我们的loss函数。优化算法基本上是梯度下降算法。梯度下降有不同的版本和变体。在这种情况下,我们将使用Adam算法,这是随机梯度下降的扩展:

    nn.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
    
  15. 现在,让我们总结一下我们的模型,看看发生了什么:

    nn.summary()
    

    你将得到的输出将类似于这样:

    图 3.18: 模型摘要

    图 3.18: 模型摘要
  16. 现在,是时候训练模型了。使用我们之前划分的X_trainy_train数据来拟合神经网络:

    nn.fit(X_train_onehot[:-20], y_train[:-20], 
              epochs=5, batch_size=128, verbose=1, 
              validation_data=(X_train_onehot[-100:], y_train[-20:]))
    

    就这样!我们的神经网络现在准备好进行测试了。

  17. 将输入验证数据转换为词频向量并评估神经网络。打印准确率分数,看看网络的表现如何:

    scores = nn.evaluate(vectorizer.transform(X_test), y_test, verbose=1)
    print("Accuracy:", scores[1])
    

    你的分数可能会稍有不同,但应该接近 0.875\。

    这是一个相当不错的分数。所以,就是这样。你刚刚创建了你的第一个神经网络,训练了它,并验证了它。

    预期输出:

    图 3.19: 预期准确率分数

    ](https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-nlp/img/C13783_03_19.jpg)

    图 3.19: 预期准确率分数
  18. 保存你的模型:

    model.save('nn.hd5')
    

部署模型作为服务的基础

部署模型作为服务的目的是让其他人能够轻松查看和访问它,而不仅仅是通过查看你在 GitHub 上的代码。根据你创建模型的初衷,模型的部署方式有不同类型。可以说有三种类型——流式模型(一个不断学习的模型,随着不断输入数据并做出预测),分析即服务模型(AaaS——一个供任何人互动的模型)和在线模型(一个只允许公司内部人员访问的模型)。

展示你工作的最常见方式是通过 Web 应用程序。有多个部署平台可以帮助你通过它们部署你的模型,如 Deep Cognition、MLflow 等。

Flask 是最容易使用的微型 Web 框架,可用于在不使用现有平台的情况下部署你自己的模型。它是用 Python 编写的。使用这个框架,你可以为你的模型构建一个 Python API,该 API 将轻松生成预测并为你显示结果。

流程如下:

  1. 为 API 创建一个目录

  2. 将你的预训练神经网络模型复制到这个目录中。

  3. 编写一个程序,加载这个模型,预处理输入数据,使其与模型的训练输入匹配,使用该模型进行预测并准备、发送、显示这个预测结果。

测试和运行 API 时,你只需键入应用程序名称,并加上.run()

在我们创建的神经网络的情况下,我们会保存该模型,并将其加载到一个新的 Jupyter 笔记本中。我们会将输入数据(清洗后的评论)转换为词频向量,以确保 API 的输入数据与训练数据相同。然后,我们会使用模型生成预测并显示它们。

活动 4:评论情感分析

在这个活动中,我们将审查一个数据集中的评论,并将其分类为正面或负面。以下步骤将帮助你完成解决方案。

注意

你可以在以下链接找到数据集:

https://github.com/TrainingByPackt/Deep-Learning-for-Natural-Language-Processing/tree/master/Lesson 04

  1. 打开一个新的Jupyter笔记本。导入数据集。

  2. 导入必要的 Python 包和类。将数据集加载到数据框中。

  3. 导入必要的库来清洗和准备数据。创建一个数组来存储清洗后的文本。使用for循环,遍历每个实例(每条评论)。

  4. 导入 CountVectorizer 并将单词转换为词频向量。创建一个数组来存储每个独特单词作为单独的列,从而使它们成为独立变量。

  5. 导入必要的标签编码实体。

  6. 将数据集划分为训练集和测试集。

  7. 创建神经网络模型。

  8. 训练模型并验证它。

  9. 评估神经网络并打印准确率评分,查看它的表现如何。

    预期输出:

图 3.20:准确率评分

图 3.20:准确率评分

注意

活动的解决方案可以在第 302 页找到。

总结

在这一章中,我们介绍了机器学习的一个子集——深度学习。你了解了这两种技术类别之间的异同,并理解了深度学习的需求及其应用。

神经网络是对人脑中生物神经网络的人工表示。人工神经网络是深度学习模型中所采用的框架,已经证明它们在效率和准确性上不断提高。它们被应用于多个领域,从训练自动驾驶汽车到在非常早期阶段检测癌细胞。

我们研究了神经网络的不同组件,并了解了在损失函数、梯度下降算法和反向传播的帮助下,网络如何进行自我训练和修正。你还学会了如何对文本输入进行情感分析!此外,你还学习了将模型部署为服务的基本知识。

在接下来的章节中,你将了解更多关于神经网络及其不同类型的内容,并学习在不同情况下使用哪种神经网络。

第五章:第四章

卷积神经网络的基础

学习目标

到本章结束时,你将能够:

  • 描述 CNN 在神经科学中的灵感来源

  • 描述卷积操作

  • 描述一个基本的 CNN 架构用于分类任务

  • 实现一个简单的 CNN 用于图像和文本分类任务

  • 实现一个用于文本情感分析的 CNN

本章中,我们旨在涵盖卷积神经网络(CNN)的架构,并通过其在图像数据上的应用来获得对 CNN 的直觉,随后再深入探讨它们在自然语言处理中的应用。

引言

神经网络作为一个广泛的领域,从生物系统,特别是大脑中汲取了很多灵感。神经科学的进展直接影响了神经网络的研究。

CNN 的灵感来源于两位神经科学家的研究,D.H. Hubel 和 T.N. Wiesel。他们的研究集中在哺乳动物的视觉皮层,这是大脑中负责视觉的部分。在上世纪六十年代的研究中,他们发现视觉皮层由多层神经元组成。此外,这些层次是以一种层级结构排列的。这个层级从简单的神经元到超复杂的神经元都有。他们还提出了“感受野”的概念,即某些刺激能够激活或触发一个神经元的空间范围,具有一定的空间不变性。空间或位移不变性使得动物能够识别物体,无论它们是旋转、缩放、变换,还是部分遮挡。

图 4.1:空间变化的示例

图 4.1:空间变化的示例

受到动物视觉神经概念的启发,计算机视觉科学家们构建了遵循局部性、层次性和空间不变性相同原则的神经网络。我们将在下一节深入探讨 CNN 的架构。

CNN 是神经网络的一个子集,包含一个或多个“卷积”层。典型的神经网络是全连接的,意味着每个神经元都与下一层中的每个神经元连接。当处理高维数据(如图像、声音等)时,典型的神经网络运行较慢,并且容易过拟合,因为学习的权重太多。卷积层通过将神经元与低层输入的一个区域连接来解决这个问题。我们将在下一节中更详细地讨论卷积层。

为了理解 CNN 的一般架构,我们将首先将其应用于图像分类任务,然后再应用于自然语言处理。首先,我们将做一个小练习来理解计算机是如何看待图像的。

练习 18:了解计算机如何看待图像

图像和文本有一个重要的相似性。图像中一个像素的位置,或文本中的一个单词位置,都很重要。这种空间上的意义使得卷积神经网络可以同时应用于文本和图像。

在本练习中,我们希望确定计算机如何解读图像。我们将使用 MNIST 数据集,它包含手写数字,非常适合演示 CNN。

注意

MNIST 是一个内置的 Keras 数据集。

你需要安装 Python 和 Keras。为了更方便地可视化,你可以在 Jupyter notebook 中运行代码:

  1. 首先导入必要的类:

    %matplotlib inline
    import keras
    import matplotlib.pyplot as plt
    
  2. 由于我们将在整个章节中使用该数据集,因此我们将按如下方式导入训练集和测试集:

    (X_train, y_train), (X_test, y_test) = keras.datasets.mnist.load_data()
    
  3. 可视化数据集中的第一张图像:

    sample_image = X_train[0]
    plt.imshow(sample_image)
    

    运行前面的代码应该会显示出图像,如下所示:

    图 4.2: 图像的可视化

    图 4.2: 图像的可视化

    这些图像为 28x28 像素,每个像素的值在 0 到 255 之间。尝试修改不同的索引来显示它们的值,如下所示。你可以通过将任意数字在 0255 之间作为 xy 来实现:

    print(sample_image[x][y]) 
    
  4. 当你运行以下打印代码时,应该会看到 0 到 255 之间的数字:

    print(sample_image[22][11])
    print(sample_image[6][12])
    print(sample_image[5][23])
    print(sample_image[10][11])
    

    预期输出:

图 4.3: 图像的数字表示

图 4.3: 图像的数字表示

本练习旨在帮助你理解图像数据如何被处理,其中每个像素作为一个在 0255 之间的数字。这一理解至关重要,因为我们将在下一部分将这些图像输入到 CNN 中作为输入。

理解 CNN 的架构

假设我们有一个任务,将每个 MNIST 图像分类为 0 到 9 之间的数字。前面示例中的输入是一个图像矩阵。对于彩色图像,每个像素是一个包含三个值的数组,分别对应 RGB 颜色模式。对于灰度图像,每个像素仅是一个数字,就像我们之前看到的那样。

要理解 CNN 的架构,最好将其分为两个部分,如下图所示。

CNN 的前向传播涉及在两个部分中进行一系列操作。

图 4.4: 卷积和 ReLU 操作的应用

图 4.4: 卷积和 ReLU 操作的应用

该图在以下部分中解释:

  • 特征提取

  • 神经网络

特征提取

CNN 的第一部分是特征提取。从概念上讲,可以理解为模型尝试学习哪些特征可以区分不同的类别。在图像分类任务中,这些特征可能包括独特的形状和颜色。

CNN 学习这些特征的层次结构。CNN 的低层抽象特征如边缘,而高层则学习更明确的特征,如形状。

特征学习通过重复一系列三个操作进行,如下所示:

  1. 卷积

  2. 激活函数(应用 ReLU 激活函数以实现非线性)

  3. 池化

卷积

卷积是将 CNN(卷积神经网络)与其他神经网络区分开来的操作。卷积操作不仅仅是机器学习中的特有操作,它还广泛应用于其他领域,如电气工程和信号处理。

卷积可以理解为通过一个小窗口查看,当我们将窗口向右和向下移动时。卷积在这个上下文中意味着反复滑动一个“滤波器”跨越图像,同时在移动时应用点积操作。

这个窗口被称为“滤波器”或“卷积核”。在实际操作中,滤波器或卷积核是一个矩阵,通常比输入的尺寸小。为了更好地理解滤波器如何应用于图像,考虑以下示例。在计算滤波器覆盖区域的点积后,我们向右移动一步,再次计算点积:

图 4.5:滤波器应用于图像

图 4.5:滤波器应用于图像

卷积的结果称为特征图或激活图。

滤波器的大小需要定义为超参数。这个大小也可以看作是神经元能够“看到”输入的区域。这个区域被称为神经元的感受野。此外,我们需要定义步幅大小,即在应用滤波器之前需要进行的步数。位于中心的像素相比位于边缘的像素,滤波器会经过多次。为了避免在角落处丢失信息,建议添加一层零填充。

ReLU 激活函数

激活函数在整个机器学习中都被广泛使用。它们有助于引入非线性,使得模型能够学习非线性函数。在这个特定的上下文中,我们应用了修正线性单元ReLU)。它的基本原理是将所有负值替换为零。

以下图像展示了应用 ReLU 后图像的变化。

图 4.6:应用 ReLU 函数后的图像

图 4.6:应用 ReLU 函数后的图像

练习 19:可视化 ReLU

在本练习中,我们将可视化修正线性单元(ReLU)函数。ReLU 函数将在 X-Y 坐标轴上绘制,其中 X 轴是从 -15 到 15 范围内的数字,Y 轴是应用 ReLU 函数后的输出值。此练习的目标是可视化 ReLU。

  1. 导入所需的 Python 包:

    from matplotlib import pyplot
    
  2. 定义 ReLU 函数:

    def relu(x):
        return max(0.0, x)
    
  3. 指定输入和输出参考:

    inputs = [x for x in range(-15, 15)]
    outputs = [relu(x) for x in inputs]
    
  4. 绘制输入与输出的关系:

    pyplot.plot(inputs, outputs) #Plot the input against the output
    pyplot.show()
    

    预期输出:

图 4.7:ReLU 的图形绘制

图 4.7:ReLU 的图形绘制

池化

池化是一个降采样过程,它涉及将数据从更高维度空间减少到更低维度空间。在机器学习中,池化通常用于减少层的空间复杂性。这可以让我们学习更少的权重,从而加快训练速度。

历史上,曾使用不同的技术来执行池化操作,比如平均池化和 L2 范数池化。最常用的池化技术是最大池化。最大池化涉及在定义的窗口大小内取最大的元素。下面是一个对矩阵进行最大池化的例子:

图 4.8:最大池化

](https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-nlp/img/C13783_04_08.jpg)

图 4.8:最大池化

如果我们对前面的例子应用最大池化,那么包含 2、6、3 和 7 的区域将被缩减为 7。同样,包含 1、0、9 和 2 的区域将被缩减为 9。通过最大池化,我们选择一个区域中的最大值。

Dropout

机器学习中一个常见的问题是过拟合。过拟合发生在模型“记住”了训练数据,并且在测试时面对不同的示例时无法进行泛化。避免过拟合有几种方法,特别是通过正则化:

图 4.9:正则化

图 4.9:正则化

正则化是约束系数趋近于零的过程。正则化可以总结为一种技术,用于惩罚已学习的系数,使它们趋向于零。Dropout 是一种常见的正则化技术,在前向和反向传播过程中,通过随机“丢弃”一些神经元来实现。为了实现 dropout,我们将神经元被丢弃的概率指定为一个参数。通过随机丢弃神经元,我们确保模型能够更好地泛化,因此更加灵活。

卷积神经网络中的分类

CNN 的第二部分更具任务特定性。对于分类任务,这一部分基本上是一个全连接的神经网络。当神经网络中的每个神经元都与下一层的所有神经元连接时,神经网络被认为是全连接的。全连接层的输入是展平后的向量,这个向量是第一部分的输出。展平操作将矩阵转换为 1D 向量。

全连接层中隐藏层的数量是一个超参数,可以优化和微调。

练习 20:创建一个简单的 CNN 架构

在这个练习中,你将使用 Keras 构建一个简单的 CNN 模型。这个练习将包括创建一个包含到目前为止讨论的层的模型。在模型的第一部分,我们将有两个卷积层,使用 ReLU 激活函数,一个池化层和一个 dropout 层。在第二部分,我们将有一个展平层和一个全连接层。

  1. 首先,我们导入必要的类:

    from keras.models import Sequential #For stacking layers
    from keras.layers import Dense, Conv2D, Flatten, MaxPooling2D, Dropout
    from keras.utils import plot_model
    
  2. 接下来,定义所使用的变量:

    num_classes = 10
    
  3. 现在我们来定义模型。Keras 的 Sequential 模型允许你按顺序堆叠层:

    model = Sequential()
    
  4. 现在我们可以添加第一节的层。卷积层和 ReLU 层一起定义。我们有两个卷积层。每个层的卷积核大小都定义为 3。模型的第一层接收输入。我们需要定义它期望输入的结构方式。在我们的案例中,输入是 28x28 的图像形式。我们还需要指定每一层的神经元数量。在我们的案例中,我们为第一层定义了 64 个神经元,为第二层定义了 32 个神经元。请注意,这些是可以优化的超参数:

    model.add(Conv2D(64, kernel_size=3, activation='relu', input_shape=(28,28,1)))
    model.add(Conv2D(32, kernel_size=3, activation='relu'))
    
  5. 然后我们添加一个池化层,接着是一个丢弃层,丢弃层以 25%的概率“丢弃”神经元:

    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Dropout(0.25))
    

    第一节的层已经完成。请注意,层的数量也是一个可以优化的超参数。

  6. 对于第二节,我们首先将输入展平。然后我们添加一个完全连接层或密集层。使用 softmax 激活函数,我们可以计算 10 个类别的每个类别的概率:

    model.add(Flatten())
    model.add(Dense(num_classes, activation='softmax'))
    
  7. 为了可视化到目前为止的模型架构,我们可以按照以下方式打印出模型:

    model.summary()
    

    预期输出

    图 4.10:模型摘要

    图 4.10:模型摘要
  8. 你也可以运行以下代码将图像导出到文件:

    plot_model(model, to_file='model.png')
    

图 4.11:简单 CNN 的可视化架构

图 4.11:简单 CNN 的可视化架构

在前面的练习中,我们创建了一个简单的卷积神经网络(CNN),包含两个卷积层,用于分类任务。在前面的输出图像中,你会注意到这些层是如何堆叠的——从输入层开始,然后是两个卷积层、池化层、丢弃层和展平层,最后是完全连接层。

训练 CNN

在训练 CNN 时,模型尝试学习特征提取中的滤波器权重以及神经网络中完全连接层的权重。为了理解模型是如何训练的,我们将讨论如何计算每个输出类别的概率,如何计算误差或损失,最后,如何优化或最小化该损失,并在更新权重时进行调整:

  1. 概率

    回想一下,在神经网络的最后一层,我们使用了 softmax 函数来计算每个输出类别的概率。这个概率是通过将该类别分数的指数除以所有分数的指数总和来计算的:

    图 4.12:计算概率的表达式

    图 4.12:计算概率的表达式
  2. 损失

    我们需要能够量化计算出的概率如何预测实际类别。这是通过计算损失来实现的,在分类概率的情况下,最好通过类别交叉熵损失函数来完成。类别交叉熵损失函数接受两个向量,预测的类别(我们称之为 y')和实际的类别(称之为 y),并输出整体损失。交叉熵损失是类别概率的负对数似然之和。它可以用 H 函数表示:

    图 4.13:计算损失的表达式
  3. 优化

    考虑以下交叉熵损失的示意图。通过最小化损失,我们可以以更高的概率预测正确的类别:

图 4.14:交叉熵损失与预测概率

图 4.14:交叉熵损失与预测概率

梯度下降是一种优化算法,用于寻找函数的最小值,例如前面描述的损失函数。虽然计算了整体误差,但我们需要回过头来计算每个节点对损失的贡献。因此,我们可以更新权重,以最小化整体误差。反向传播应用了微积分中的链式法则来计算每个权重的更新。这是通过求误差或损失相对于权重的偏导数来完成的。

为了更好地可视化这些步骤,考虑以下图示,概括了这三个步骤。在分类任务中,第一步涉及计算每个输出类别的概率。然后,我们应用损失函数来量化概率预测实际类别的效果。为了在未来做出更好的预测,我们通过梯度下降进行反向传播,更新权重:

图 4.15:分类任务的步骤

图 4.15:分类任务的步骤

练习 21:训练 CNN

在本练习中,我们将训练在练习 20 中创建的模型。以下步骤将帮助您解决这个问题。请记住,这适用于整个分类任务。

  1. 我们首先定义训练的轮数。一个轮次是深度神经网络中的常见超参数。一个轮次表示整个数据集经过完整的前向传播和反向传播。当训练数据量很大时,数据可以分成多个批次:

    epochs=12
    
  2. 回想一下,我们通过运行以下命令导入了 MNIST 数据集:

    (X_train, y_train), (X_test, y_test) = keras.datasets.mnist.load_data()
    
  3. 我们首先重新调整数据以适应模型:

    X_train = X_train.reshape(60000,28,28,1) #60,000 is the number of training examples
    X_test = X_test.reshape(10000,28,28,1)
    
  4. to_categorical 函数将整数向量转换为一热编码的矩阵。给定以下示例,函数返回如下数组:

    #Demonstrating the to_categorical method
    Import numpy as np
    from keras.utils import to_categorical
    example = [1,0,3,2]
    to_categorical(example)
    

    数组将如下所示:

    图 4.16:数组输出

    图 4.16:数组输出
  5. 我们将其应用于目标列,如下所示:

    from keras.utils import to_categorical
    y_train = to_categorical(y_train)
    y_test = to_categorical(y_test)
    
  6. 我们随后将损失函数定义为分类交叉熵损失函数。此外,我们定义优化器和度量标准。Adam(自适应矩估计)优化器是一种经常用于替代随机梯度下降的优化算法。它为模型的每个参数定义了自适应学习率:

    model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])
    
  7. 要训练模型,请运行.fit方法:

    model.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=epochs)
    

    输出应如下所示:

    图 4.17:训练模型

    图 4.17:训练模型
  8. 要评估模型的性能,您可以运行以下命令:

    score = model.evaluate(X_test, y_test, verbose=0)
    print('Test loss:', score[0])
    print('Test accuracy:', score[1])
    
  9. 对于这个任务,我们预计在若干个周期后会有相当高的准确率:

图 4.18:准确率和损失输出

图 4.18:准确率和损失输出

应用 CNN 到文本

现在我们已经对 CNN 如何在图像中使用有了一般直觉,让我们看看它们如何应用在自然语言处理中。与图像类似,文本具有使其在 CNN 使用中成为理想选择的空间特性。但是,当我们处理文本时,架构上有一个主要变化。文本不再具有二维卷积层,而是一维的,如下所示。

图 4.19:一维卷积

图 4.19:一维卷积

需要注意的是,前述输入序列可以是字符序列或单词序列。在字符级别上应用 CNNs 到文本的应用可以如下图所示。CNN 具有 6 个卷积层和 3 个全连接层,如下所示。

图 4.20:CNN 具有 6 个卷积和 3 个全连接层

图 4.20:CNN 具有 6 个卷积和 3 个全连接层

当应用于大型嘈杂数据时,字符级 CNN 表现良好。它们与单词级应用相比较简单,因为它们不需要预处理(如词干处理),并且字符被表示为一热编码表示。

在以下示例中,我们将演示如何将 CNN 应用于单词级别的文本。因此,在将数据输入 CNN 架构之前,我们需要执行一些向量化和填充操作。

练习 22:将简单的 CNN 应用于 Reuters 新闻主题分类

在这个练习中,我们将应用 CNN 模型到内置的 Keras Reuters 数据集中。

注意

如果您使用 Google Colab,需要通过运行以下命令将您的numpy版本降级到 1.16.2:

!pip install numpy==1.16.1

import numpy as np

由于此版本的numpyallow_pickle的默认值设置为True,因此需要进行此降级。

  1. 首先导入必要的类:

    import keras
    from keras.datasets import reuters
    from keras.preprocessing.text import Tokenizer
    from keras.models import Sequential
    from keras import layers
    
  2. 定义变量:

    batch_size = 32
    epochs = 12
    maxlen = 10000
    batch_size = 32
    embedding_dim = 128
    num_filters = 64
    kernel_size = 5
    
  3. 加载 Reuters 数据集:

    (x_train, y_train), (x_test, y_test) = reuters.load_data(num_words=None, test_split=0.2)
    
  4. 准备数据:

    word_index = reuters.get_word_index(path="reuters_word_index.json")
    num_classes = max(y_train) + 1 
    index_to_word = {}
    for key, value in word_index.items():
        index_to_word[value] = key
    
  5. 对输入数据进行标记化:

    tokenizer = Tokenizer(num_words=maxlen)
    x_train = tokenizer.sequences_to_matrix(x_train, mode='binary')
    x_test = tokenizer.sequences_to_matrix(x_test, mode='binary')
    y_train = keras.utils.to_categorical(y_train, num_classes)
    y_test = keras.utils.to_categorical(y_test, num_classes)
    
  6. 定义模型:

    model = Sequential()
    model.add(layers.Embedding(512, embedding_dim, input_length=maxlen))
    model.add(layers.Conv1D(num_filters, kernel_size, activation='relu'))
    model.add(layers.GlobalMaxPooling1D())
    model.add(layers.Dense(10, activation='relu'))
    model.add(layers.Dense(num_classes, activation='softmax'))
    model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
    
  7. 训练和评估模型。打印准确率分数:

    history = model.fit(x_train, y_train, batch_size=batch_size, epochs=epochs, verbose=1, validation_split=0.1)
    score = model.evaluate(x_test, y_test, batch_size=batch_size, verbose=1)
    print('Test loss:', score[0])
    print('Test accuracy:', score[1])
    

    预期输出

图 4.21:准确率分数

图 4.21:准确率分数

因此,我们已经创建了一个模型,并在数据集上进行了训练。

CNN 的应用领域

现在我们已经了解了 CNN 的架构,让我们来看看一些应用。通常,CNN 非常适合具有空间结构的数据。具有空间结构的数据类型示例包括声音、图像、视频和文本。

在自然语言处理领域,CNNs 被用于各种任务,如句子分类。一个例子是情感分类任务,其中一个句子被分类为属于预定的类别之一。

如前所述,CNNs 被应用于字符级别的分类任务,如情感分类,尤其是在社交媒体帖子等嘈杂数据集上。

CNNs 更常应用于计算机视觉。以下是该领域的一些应用:

  • 面部识别

    大多数社交网络网站使用 CNN 来检测面部并随后执行诸如标签标注等任务。

图 4.22:面部识别

图 4.22:面部识别
  • 物体检测

    类似地,CNN 能够在图像中检测物体。有几种基于 CNN 的架构用于检测物体,其中最流行的之一是 R-CNN。(R-CNN 代表区域卷积神经网络。)R-CNN 的工作原理是应用选择性搜索来生成区域,然后使用 CNN 进行分类,每次处理一个区域。

图 4.23:物体检测

](https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-nlp/img/C13783_04_23.jpg)

图 4.23:物体检测
  • 图像标注

    该任务涉及为图像创建文本描述。执行图像标注的一种方式是将第二部分的全连接层替换为递归神经网络(RNN)。

图 4.24:图像标注

](https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-nlp/img/C13783_04_24.jpg)

图 4.24:图像标注
  • 语义分割

    语义分割是将图像划分为更有意义部分的任务。图像中的每个像素都会被分类为属于某个类别。

图 4.25:语义分割

图 4.25:语义分割

一种可以用于执行语义分割的架构是全卷积网络FCN)。FCN 的架构与之前的架构在两方面略有不同:它没有全连接层,并且具有上采样。上采样是将输出图像放大到与输入图像大小相同的过程。

这是一个示例架构:

图 4.26:语义分割的示例架构

图 4.26:语义分割的示例架构

注意

想了解更多关于 FCN 的信息,请参考 Jonathan Long、Evan Shelhamer 和 Trevor Darrell 的论文《Fully Convolutional Networks for Semantic Segmentation》。

活动 5:在实际数据集上进行情感分析

假设你被要求创建一个模型来分类数据集中的评论。在本活动中,我们将构建一个执行情感分析二分类任务的 CNN。我们将使用来自 UCI 数据集库的实际数据集。

注意

该数据集可从 https://archive.ics.uci.edu/ml/datasets/Sentiment+Labelled+Sentences 下载

从组标签到个体标签,基于深度特征,Kotziaa 等,KDD 2015 UCI 机器学习库 [http://archive.ics.uci.edu.ml]。加利福尼亚州欧文市:加利福尼亚大学信息与计算机科学学院

你也可以通过我们的 GitHub 仓库链接下载它:

https://github.com/TrainingByPackt/Deep-Learning-for-Natural-Language-Processing/tree/master/Lesson 04

以下步骤将帮助你解决问题。

  1. 下载 Sentiment Labelled Sentences 数据集。

  2. 在你的工作目录中创建一个名为 'data' 的文件夹,并将下载的文件夹解压到该目录中。

  3. 在 Jupyter Notebook 上创建并运行你的工作脚本(例如,sentiment.ipynb)。

  4. 使用 pandas 的 read_csv 方法导入数据。可以随意使用数据集中的一个或所有文件。

  5. 使用 scikit-learn 的 train_test_split 将数据拆分为训练集和测试集。

  6. 使用 Keras 的 tokenizer 进行分词。

  7. 使用 texts_to_sequences 方法将文本转换为序列。

  8. 通过填充确保所有序列具有相同的长度。你可以使用 Keras 的 pad_sequences 函数。

  9. 定义模型,至少包含一层卷积层和一层全连接层。由于这是二分类问题,我们使用 sigmoid 激活函数,并通过二元交叉熵损失函数计算损失。

  10. 训练和测试模型。

    注意

    该活动的解决方案可以在第 305 页找到。

    预期输出:

图 4.27:准确度分数

图 4.27:准确度分数

摘要

在本章中,我们学习了卷积神经网络(CNN)的架构和应用。CNN 不仅应用于文本和图像,还应用于具有某种空间结构的数据集。在接下来的章节中,你将探索如何将其他形式的神经网络应用于各种自然语言任务。

第六章:第五章

循环神经网络

学习目标

在本章结束时,你将能够:

  • 描述经典的前馈神经网络

  • 区分前馈神经网络和循环神经网络

  • 评估通过时间反向传播应用于循环神经网络的效果

  • 描述循环神经网络的缺点

  • 使用 Keras 中的循环神经网络解决作者归属问题

本章旨在介绍循环神经网络及其应用,并讨论它们的缺点。

介绍

我们在日常生活中会遇到不同种类的数据,其中一些数据具有时间依赖性(随时间变化的依赖关系),而另一些则没有。例如,一张图片本身就包含了它想要传达的信息。然而,音频和视频等数据形式则具有时间依赖性。如果只考虑一个固定的时间点,它们无法传递信息。根据问题的描述,解决问题所需的输入可能会有所不同。如果我们有一个模型来检测某个特定人物在一帧中的出现,那么可以使用单张图片作为输入。然而,如果我们需要检测他们的动作,我们需要一系列连续的图像作为输入。我们可以通过分析这些图像来理解一个人的动作,但不能仅仅通过单独的图像来分析。

在观看电影时,某个特定场景之所以能够理解,是因为已知其上下文,并且我们记住了电影中之前收集的所有信息,来理解当前场景。这一点非常重要,而我们作为人类之所以能够做到这一点,是因为我们的脑袋能够存储记忆,分析过去的数据,并提取有用信息以理解当前的场景。

像多层感知机和卷积神经网络这样的网络缺乏这种能力。它们对每个输入都视为独立处理,并且不会存储任何来自过去输入的信息来分析当前输入,因为它们在架构上缺乏记忆功能。在这种情况下,也许我们可以让神经网络具有记忆功能。我们可以尝试让它们存储过去的有用信息,并从过去获取有助于分析当前输入的信息。这是完全可能的,其架构被称为循环神经网络RNN)。

在深入了解 RNN 的理论之前,我们先来看一下它们的应用。目前,RNN 被广泛应用。以下是一些应用:

  • 语音识别:无论是亚马逊的 Alexa,苹果的 Siri,谷歌的语音助手,还是微软的 Cortana,它们的所有语音识别系统都使用 RNN。

  • 时间序列预测:任何具有时间序列数据的应用程序,如股市数据、网站流量、呼叫中心流量、电影推荐、Google Maps 路线等等,都使用 RNN 来预测未来数据、最佳路径、最佳资源分配等。

  • 自然语言处理:机器翻译(例如 Google Translate)、聊天机器人(如 Slack 和 Google 的聊天机器人)以及问答系统等应用都使用 RNN 来建模依赖关系。

神经网络的早期版本

大约 40 年前,人们发现前馈神经网络FFNNs)无法捕捉时间变化的依赖关系,而这对于捕捉信号的时间变化特性至关重要。建模时间变化的依赖关系在许多涉及现实世界数据的应用中非常重要,例如语音和视频,这些数据具有时间变化的特性。此外,人类生物神经网络具有递归关系,因此这是最明显的发展方向。如何将这种递归关系添加到现有的前馈网络中呢?

实现这一目标的首次尝试之一是通过添加延迟元素,网络被称为时延神经网络,简称TDNN

在这个网络中,正如下图所示,延迟元素被添加到网络中,过去的输入与当前时刻一起作为网络的输入。与传统的前馈网络相比,这种方法无疑具有优势,但也有一个缺点,即只能接收来自过去的有限输入,这取决于窗口的大小。如果窗口太大,网络随着参数的增加而增长,计算复杂度也随之增加。

图 5.1:TDNN 结构

图 5.1:TDNN 结构

随后出现了 Elman 网络,或称为简单 RNN(Simple RNN)。Elman 网络与前馈网络非常相似,不同之处在于其输出的隐藏层会被存储并用于下一个输入。这样,前一个时刻的信息可以在这些隐藏状态中被捕获。

观察 Elman 网络的一种方式是,在每个输入时,我们将前一个隐藏层的输出与当前输入一起附加,并将它们作为网络的输入。因此,如果输入大小是m,隐藏层大小是n,则有效的输入层大小变为m+n

下图显示了一个简单的三层网络,其中之前的状态被反馈到网络中以存储上下文,因此称之为SimpleRNN。这种架构有其他变种,例如 Jordan 网络,我们在本章中不会学习这些变种。对于那些对 RNN 早期历史感兴趣的人来说,阅读更多关于 Elman 网络和 Jordan 网络的资料可能是一个很好的起点。

图 5.2:SimpleRNN 结构

图 5.2:SimpleRNN 结构

然后就出现了 RNN,这是本章的主题。我们将在接下来的章节中详细探讨 RNN。需要注意的是,在递归网络中,由于存在与这些单位相关的记忆单元和权重,这些内容需要在反向传播中学习。由于这些梯度也通过时间进行反向传播,因此我们称之为 通过时间的反向传播BPTT)。我们将在后续章节中详细讨论 BPTT。然而,由于 BPTT,TDNN、Elman 网络和 RNN 存在一个主要缺陷,这个问题被称为梯度消失。梯度消失是指梯度在反向传播时越来越小,在这些网络中,随着时间步长的增加,反向传播的梯度越来越小,最终导致梯度消失。捕捉超过 20 个时间步长的时间依赖性几乎是不可能的。

为了解决这个问题,引入了一种名为 长短期记忆LSTM)的架构。这里的关键思想是保持一些细胞状态不变,并根据需要将其引入到未来的时间步长中。这些决策由门控机制完成,包括遗忘门和输出门。LSTM 的另一种常见变体叫做 门控递归单元GRU),简称 GRU。如果你还没有完全理解这些概念,不用太担心。接下来有两章内容专门讲解这些概念。

RNN

"递归"通常意味着反复发生。RNN 的递归部分简单来说就是在输入序列中的所有输入上执行相同的任务(对于 RNN,我们将时间步长序列作为输入序列)。前馈网络和 RNN 之间的一个主要区别是,RNN 拥有称为状态的记忆单元,用于捕获来自前一个输入的信息。因此,在这个架构中,当前的输出不仅依赖于当前输入,还依赖于当前的状态,而该状态则考虑了过去的输入。

RNN 通过输入序列而不是单一输入进行训练;同样,我们也可以将 RNN 的每个输入视为时间步长的序列。RNN 中的状态单元包含有关过去输入的信息,以处理当前的输入序列。

图 5.3:RNN 结构

图 5.3:RNN 结构

对于输入序列中的每个输入,RNN 获取一个状态,计算其输出,并将其状态传递给序列中的下一个输入。对于序列中的所有元素,都会重复执行相同的任务。

通过将 RNN 与前馈网络进行比较,我们可以更容易理解 RNN 及其运作方式。现在就让我们来进行这样的比较。

到目前为止,很明显,在前馈神经网络中,输入彼此之间是独立的,因此我们通过随机抽取输入和输出的配对来训练网络。序列本身没有任何重要性。在任何给定的时刻,输出只是输入和权重的函数。

图 5.4:RNN 输出的表达式

图 5.4:RNN 的输出表达式

在 RNN 中,时间 t 的输出不仅依赖于当前输入和权重,还依赖于之前的输入。在这种情况下,时间 t 的输出定义如下:

图 5.5:RNN 在时间 t 的输出表达式

图 5.5:RNN 在时间 t 的输出表达式

让我们看看一个简单的 RNN 结构,称为折叠模型。在下图中,St 状态向量从前一个时间步反馈到网络。这个表示法的一个重要启示是,RNN 在各个时间步之间共享相同的权重矩阵。通过增加时间步,我们并不是在学习更多的参数,而是在查看更大的序列。

图 5.6:RNN 的折叠模型

图 5.6:RNN 的折叠模型

这是 RNN 的折叠模型:

Xt:输入序列中的当前输入向量

Yt:输出序列中的当前输出向量

St:当前状态向量

Wx:连接输入向量到状态向量的权重矩阵

Wy:连接状态向量到输出向量的权重矩阵

Ws:连接前一时间步的状态向量到下一时间步的权重矩阵

由于输入 x 是一个时间步序列,并且我们对该序列中的每个元素执行相同的任务,因此我们可以展开该模型。

图 5.7:RNN 的展开

图 5.7:RNN 的展开

例如,时间 t+1 的输出,yt+1,依赖于时间 t+1 的输入、权重矩阵以及之前的所有输入。

图 5.8:展开的 RNN

图 5.8:展开的 RNN

由于 RNN 是 FFNN 的扩展,理解这两种架构之间的差异非常重要。

图 5.9:FFNN 和 RNN 的差异

图 5.9:FFNN 和 RNN 的差异

FFNN 和 RNN 的输出表达式如下:

图 5.10:FFNN 和 RNN 的输出表达式

图 5.10:FFNN 和 RNN 的输出表达式

从前面的图和公式可以明显看出,这两种架构之间有很多相似之处。事实上,如果 Ws=0,它们是相同的。显然是这种情况,因为 Ws 是与反馈到网络的状态相关的权重。没有 Ws 就没有反馈,这是 RNN 的基础。

在 FFNN(前馈神经网络)中,输出依赖于t时刻的输入和权重矩阵。在 RNN 中,输出不仅依赖于t时刻的输入,还依赖于t-1t-2等时刻的输入,以及权重矩阵。这可以通过进一步计算隐藏向量h(对于 FFNN)和s(对于 RNN)来解释。乍一看,似乎t时刻的状态依赖于t时刻的输入、t-1时刻的状态和权重矩阵;而t-1时刻的状态依赖于t-1时刻的输入、t-2时刻的状态,依此类推,形成一个从第一时刻开始回溯的链条。不过,FFNN 和 RNN 的输出计算是相同的。

RNN 架构

RNN(循环神经网络)可以有多种形式,具体使用哪种架构需要根据我们要解决的问题来选择。

图 5.11 不同架构的 RNN

图 5.11 不同架构的 RNN

一对多:在这种架构中,给定一个单一的输入,输出是一个序列。一个例子是图像描述,其中输入是单一的图像,输出是一系列描述图像的单词。

多对一:在这种架构中,给定一个输入序列,但期望一个单一的输出。一个例子是时间序列预测,其中需要预测下一个时刻的值,基于之前的时刻。

多对多:在这种架构中,输入序列被提供给网络,网络输出一个序列。在这种情况下,序列可以是同步的,也可以是不同步的。例如,在机器翻译中,整个句子需要先输入网络,然后网络才开始进行翻译。有时,输入和输出不是同步的;例如,在语音增强中,输入是一个音频帧,而输出是该音频帧的清晰版本。在这种情况下,输入和输出是同步的。

RNN 也可以堆叠在一起。需要注意的是,每个堆叠中的 RNN 都有自己的权重矩阵。因此,权重矩阵在横向(时间轴)上是共享的,而不是在纵向(RNN 数量轴)上共享的。

图 5.12: 堆叠的 RNN

图 5.12: 堆叠的 RNN

BPTT

RNN 可以处理不同长度的序列,能以不同形式使用,且可以堆叠在一起。之前,你已经遇到过反向传播技术,用于反向传播损失值以调整权重。对于 RNN,也可以进行类似的操作,不过稍有不同,那就是通过时间传递的门控损失。它被称为BPTT(反向传播通过时间)。

根据反向传播的基本理论,我们知道以下内容:

图 5.13: 权重更新的表达式

图 5.13: 权重更新的表达式

更新值是通过链式法则的梯度计算得出的:

图 5.14 权重的误差偏导数

图 5.14 误差相对于权重的偏导数

这里,α 是学习率。误差(损失)相对于权重矩阵的偏导数是主要的计算。获得新的矩阵后,调整权重矩阵就是将这个新矩阵按学习因子缩放后加到原矩阵上。

在计算 RNN 的更新值时,我们将使用 BPTT。

让我们通过一个例子来更好地理解这一点。考虑一个损失函数,例如均方误差(常用于回归问题):

图 5.15:损失函数

图 5.15:损失函数

在时间步 t = 3 时,计算得到的损失如图所示:

图 5.16 时间 t=3 时的损失

图 5.16 时间 t=3 时的损失

这个损失需要进行反向传播,WyWxWs 权重需要更新。

如前所述,我们需要计算更新值来调整这些权重,这个更新值可以通过偏导数和链式法则来计算。

完成此操作有三个部分:

  • 通过计算误差相对于 Wy 的偏导数来更新权重 Wy

  • 通过计算误差相对于 Ws 的偏导数来更新权重 Ws

  • 通过计算误差相对于 Wx 的偏导数来更新权重 Wx

在我们查看这些更新之前,先将模型展开,并保留对我们计算有实际意义的网络部分。

图 5.17 展开后的 RNN,时间 t=3 时的损失

图 5.17 展开后的 RNN,时间 t=3 时的损失

由于我们关注的是时间 t=3 时的损失如何影响权重矩阵,时间 t=2 及之前的损失值不再相关。现在,我们需要理解如何将损失反向传播通过网络。

让我们来逐个查看这些更新,并展示前图中每个更新的梯度流动。

更新与梯度流

更新可以列出如下:

  • 调整权重矩阵 Wy

  • 调整权重矩阵 Ws

  • 更新 Wx 的过程

调整权重矩阵 Wy

该模型可以通过如下方式进行可视化:

图 5.18:通过权重矩阵 **Wy** 对损失进行反向传播

图 5.18:通过权重矩阵 Wy 对损失进行反向传播

对于 Wy,更新非常简单,因为 Wy 和误差之间没有其他路径或变量。该矩阵可以按以下方式表示:

图 5.19:权重矩阵 **Wy** 的表达式

图 5.19:权重矩阵 Wy 的表达式

调整权重矩阵 Ws

图 5.20:通过权重矩阵 **Ws** 对损失进行反向传播,关于 S3

图 5.20:通过权重矩阵 Ws 对损失进行反向传播,关于 S3

我们可以使用链式法则计算误差关于Ws的偏导数,如前面的图所示。看起来这就是所需的,但重要的是要记住,S****t依赖于S****t-1,因此S****3依赖于S****2,所以我们还需要考虑S****2,如图所示:

图 5.21:通过权重矩阵 Ws 对 S2 进行损失的反向传播

图 5.21:通过权重矩阵 Ws 对 S2 进行损失的反向传播

同样,S****2依赖于S****1,因此也需要考虑S****1,如图所示:

图 5.22:通过权重矩阵 Ws 对 S1 进行损失的反向传播

图 5.22:通过权重矩阵 Ws 对 S1 进行损失的反向传播

t=3时,我们必须考虑状态S****3对误差的贡献,状态S****2对误差的贡献,以及状态S****1对误差的贡献,E****3。最终的值如下所示:

图 5.23:t=3 时关于 Ws 的所有误差导数之和

图 5.23:t=3 时关于 Ws 的所有误差导数之和

一般来说,对于时间步N,需要考虑之前时间步的所有贡献。因此,一般公式如下所示:

图 5.24:关于 Ws 的误差导数的一般表达式

图 5.24:关于 Ws 的误差导数的一般表达式

用于更新Wx

我们可以使用链式法则计算误差关于Wx的偏导数,如接下来的几张图所示。基于S****t依赖于S****t-1的相同推理,误差关于Wx的偏导数计算可以分为三个阶段,在t=3时进行。

图 5.25:通过权重矩阵 Wx 对 S2 进行损失的反向传播

图 5.25:通过权重矩阵 Wx 对 S2 进行损失的反向传播

通过权重矩阵 Wx 对 S2 进行损失的反向传播:

图 5.26:通过权重矩阵 Wx 对 S2 进行损失的反向传播

图 5.26:通过权重矩阵 Wx 对 S2 进行损失的反向传播

通过权重矩阵 Wx 对 S1 进行损失的反向传播:

图 5.27:通过权重矩阵 Wx 对 S1 进行损失的反向传播

图 5.27:通过权重矩阵 Wx 对 S1 进行损失的反向传播

类似于前面的讨论,在t=3时,我们必须考虑状态S****3对误差的贡献,状态S****2对误差的贡献,以及状态S****1对误差的贡献,E****3。最终的值如下所示:

图 5.28:t=3 时关于 Wx 的所有误差导数之和

图 5.28: 在 t=3 时关于 Wx 的所有误差导数之和

一般来说,对于时间步 N,需要考虑前面所有时间步的贡献。因此,通用公式如下所示:

图 5.29: 关于 Wx 的误差导数的通用表达式

图 5.29: 关于 Wx 的误差导数的通用表达式

由于链式导数在t=3时已经有 5 个相乘项,到第 20 时间步时,这个数量增长到了 22 个相乘项。每一个导数可能大于 0 或小于 0。由于连续乘法和更长的时间步,总导数会变得更小或更大。这个问题即为消失梯度或爆炸梯度。

梯度

已识别的两种梯度类型是:

  • 爆炸梯度

  • 消失梯度

爆炸梯度

正如名称所示,当梯度爆炸到更大的值时,就会发生这种情况。这可能是 RNN 架构在较大时间步时遇到的问题之一。当每个偏导数大于 1 时,乘法会导致一个更大的值。这些更大的梯度值每次通过反向传播调整权重时,会导致权重发生剧烈变化,从而使网络无法很好地学习。

有一些技术可以缓解这个问题,比如梯度裁剪,当梯度超过设定的阈值时会进行归一化处理。

消失梯度

无论是 RNN 还是 CNN,如果计算出的损失需要反向传播很长时间,消失梯度可能会成为问题。在 CNN 中,当有很多层并且激活函数是 sigmoid 或 tanh 时,这个问题可能会出现。损失需要反向传播到最初的层,而这些激活函数通常会在损失到达最初的层时将其稀释,意味着初始层几乎没有权重更新,导致欠拟合。即使是在 RNN 中也很常见,因为即使网络只有一个 RNN 层但时间步长较多,损失也需要通过时间反向传播穿越所有的时间步。由于梯度是相乘的,如前面所见的广义导数表达式,这些值往往变得较小,且在某个时间步后权重不会被更新。这意味着即使给网络显示更多的时间步,网络也无法受益,因为梯度无法完全反向传播。这种 RNN 的限制是由消失梯度引起的。

如其名称所示,当梯度变得过小时,就会发生这种情况。当每个偏导数小于 1 时,这种情况可能发生,并且这些偏导数的乘积会导致一个更小的值。由于信息的几何衰减,网络无法正确学习。权重值几乎没有变化,这会导致欠拟合。

必须有一种更好的机制来知道应记住前面时刻的哪些部分,哪些部分该忘记,等等。为了解决这个问题,像 LSTM 网络和 GRU 这样的架构应运而生。

使用 Keras 构建 RNN

到目前为止,我们已经讨论了 RNN 的理论背景,但有许多可用的框架可以抽象出实现的细节。只要我们知道如何使用这些框架,我们就能成功地让项目运行。TensorFlowTheanoKerasPyTorchCNTK 都是这些框架中的一部分。在这一章中,让我们更详细地了解最常用的框架——Keras。它使用 Tensorflow 或 Theano 作为后端,这意味着它创建了比其他框架更高的抽象级别。它是最适合初学者的工具。一旦熟悉了 Keras,像 TensorFlow 这样的工具可以在实现自定义函数时提供更大的能力。

你将在接下来的几章中学习到许多 RNN 的变种,但它们都使用相同的基类,称为 RNN:

keras.layers.RNN(cell, return_sequences=False, return_state=False, go_backwards=False, stateful=False, unroll=False)

在这一章中,我们讨论了 RNN 的简单形式,它在 Keras 中称为 SimpleRNN

keras.layers.SimpleRNN(units, activation='tanh', use_bias=True, kernel_initializer='glorot_uniform', recurrent_initializer='orthogonal', bias_initializer='zeros', kernel_regularizer=None, recurrent_regularizer=None, bias_regularizer=None, activity_regularizer=None, kernel_constraint=None, recurrent_constraint=None, bias_constraint=None, dropout=0.0, recurrent_dropout=0.0, return_sequences=False, return_state=False, go_backwards=False, stateful=False, unroll=False)

正如你从这里的参数所看到的,有两种类型:一种是常规的卷积核,用于计算层的输出,另一种是用于计算状态的循环卷积核。不要太担心约束、正则化器、初始化器和丢弃法。你可以在 keras.io/layers/recurrent/ 找到更多信息。它们主要用于避免过拟合。激活函数在这里的作用与任何其他层的激活函数作用相同。

单元数是指特定层中递归单元的数量。单元数量越多,需要学习的参数就越多。

return_sequences 是指定 RNN 层是否应返回整个序列还是仅返回最后一个时刻的参数。如果 return_sequences 为 False,则 RNN 层的输出仅为最后一个时刻,因此无法将其与另一个 RNN 层堆叠。换句话说,如果一个 RNN 层需要堆叠到另一个 RNN 层,则 return_sequences 需要为 True。如果 RNN 层连接到 Dense 层,这个参数可以是 True 或 False,具体取决于应用需求。

return_state 参数指定是否需要返回 RNN 的最后状态以及输出结果。根据应用需求,可以将其设置为 True 或 False。

go_backwards 可以用来处理输入序列的反向处理。如果由于某种原因需要反向处理输入序列,设置为 True 即可。请注意,如果将其设置为 True,返回的序列也会被反转。

stateful 是一个参数,如果需要在批次之间传递状态,可以将其设置为 true。如果将此参数设置为 true,数据需要谨慎处理;我们有一个话题会详细讲解这个问题。

unroll 是一个参数,若设置为 true,会使网络展开,这样可以加速操作,但根据时间步长的不同,可能会非常消耗内存。通常,对于短序列,这个参数会设置为 true。

时间步长不是某一层的参数,因为它对整个网络保持一致,这在输入形状中有所表示。这引出了使用 RNN 时网络形状的一个重要点:

Input_shape 
3D tensor with shape (batch_size, timesteps, input_dim)
Output_shape
If return_sequences is true, 3D tensor with shape (batch_size, timesteps, units)
If return_sequences is false, 2D tensor with shape (batch_size, units)
If return_state is True, a list of 2 tensors, 1 is output tensor same as above depending on return_sequences, the other is state tensor of shape (batch_size, units)

如果你开始构建一个包含 RNN 层的网络,必须指定input_shape

构建模型后,可以使用model.summary()查看每一层的形状以及总参数数量。

练习 23:构建一个 RNN 模型,以展示参数随时间的稳定性

让我们构建一个简单的 RNN 模型,展示参数在时间步长上保持不变。注意,提到input_shape参数时,除非需要,否则无需提及batch_size。在状态保持网络中需要batch_size,我们接下来将讨论这个问题。训练模型时,batch_size会在使用 fit()或fit_generator()函数时提及。

以下步骤将帮助你解决这个问题:

  1. 导入必要的 Python 包。我们将使用 Sequential、SimpleRNN 和 Dense。

    from keras.models import Sequential
    from keras.layers import SimpleRNN, Dense
    
  2. 接下来,我们定义模型及其各层:

    model = Sequential()
    # Recurrent layer
    model.add(SimpleRNN(64, input_shape=(10,100), return_sequences=False))
    # Fully connected layer
    model.add(Dense(64, activation='relu'))
    # Output layer
    model.add(Dense(100, activation='softmax'))
    
  3. 你可以查看模型的摘要:

    model.summary()
    

    model.summary() 会给出以下输出:

    图 5.30:模型层的模型摘要

    在这种情况下,batch_size 参数将由fit()函数提供。由于不返回序列,RNN 层的输出形状是 (None, 64)

  4. 让我们看一下返回序列的模型:

    model = Sequential()
    # Recurrent layer
    model.add(SimpleRNN(64, input_shape=(10,100), return_sequences=True))
    # Fully connected layer
    model.add(Dense(64, activation='relu'))
    # Output layer
    model.add(Dense(100, activation='softmax'))
    model.summary()
    

    返回序列的模型摘要如下所示:

    图 5.31:返回序列模型的模型摘要

    现在,RNN 层返回的是一个序列,因此它的输出形状是 3D,而不是之前看到的 2D。此外,请注意,Dense 层会自动适应其输入的变化。当前版本的 Keras 中,Dense 层能够自动适应来自之前 RNN 层的时间步长。在之前的 Keras 版本中,TimeDistributed(Dense) 用于实现这一点。

  5. 我们之前讨论过 RNN 如何在时间步长上共享参数。让我们实际看一下,并将之前模型的时间步长从 10 改为 1,000:

    model = Sequential()
    # Recurrent layer
    model.add(SimpleRNN(64, input_shape=(1000,100), return_sequences=True))
    # Fully connected layer
    model.add(Dense(64, activation='relu'))
    # Output layer
    model.add(Dense(100, activation='softmax'))
    model.summary()
    

图 5.32:时间步长的模型摘要

图 5.32:时间步长的模型摘要

很明显,网络的输出形状已经改变为这个新的 time_steps。然而,两个模型之间的参数没有变化。

这表明参数是随时间共享的,并且不会受到时间步数变化的影响。请注意,当在多个时间步上操作时,同样适用于Dense层。

有状态与无状态

RNN 有两种考虑状态的操作模式:无状态模式和有状态模式。如果参数 stateful=True,则您正在使用有状态模式,False表示无状态模式。

无状态模式基本上是指批次中的一个示例与下一个批次中的任何示例无关;也就是说,每个示例在给定情况下是独立的。每个示例后的状态会被重置。每个示例具有根据模型架构确定的时间步数。例如,我们看到的上一个模型有 1,000 个时间步,在这 1,000 个时间步之间,状态向量会被计算并从一个时间步传递到下一个时间步。然而,在示例的结尾或下一个示例的开始时,没有状态被传递。每个示例是独立的,因此无需考虑数据如何洗牌。

在有状态模式下,批次 1的示例i的状态会传递到批次 2i+1示例。这意味着状态会在批次之间的示例中传递。因此,示例必须在批次之间是连续的,而不能是随机的。下图解释了这一情况。示例ii+1i+2等是连续的,jj+1j+2等也是连续的,kk+1k+2等也是如此。

图 5.33 状态 RNN 的批量格式

图 5.33 状态 RNN 的批量格式

练习 24:通过仅更改参数将无状态网络转变为有状态网络

为了通过更改参数将网络从无状态转变为有状态,应该采取以下步骤。

  1. 首先,我们需要导入所需的 Python 包:

    from keras.models import Sequential
    from keras.layers import SimpleRNN, Dense
    
  2. 接下来,使用Sequential构建模型并定义层:

    model = Sequential()
    # Recurrent layer
    model.add(SimpleRNN(64, input_shape=(1000,100), return_sequences=True, stateful=False))
    # Fully connected layer
    model.add(Dense(64, activation='relu'))
    # Output layer
    model.add(Dense(100, activation='softmax'))
    model.summary()
    
  3. 将优化器设置为Adam,将categorical crossentropy设置为损失函数参数,并设置指标来拟合模型。编译模型并在 100 个周期内拟合模型:

    model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])
    model.fit(X, Y, batch_size=32, epochs=100, shuffle=True)
    
  4. 假设XY是作为连续示例的训练数据。将此模型转为有状态模型:

    model = Sequential()
    # Recurrent layer
    model.add(SimpleRNN(64, input_shape=(1000,100), return_sequences=True, stateful=True))
    # Fully connected layer
    model.add(Dense(64, activation='relu'))
    # Output layer
    model.add(Dense(100, activation='softmax'))
    
  5. 将优化器设置为Adam,将categorical crossentropy设置为损失函数参数,并设置指标来拟合模型。编译模型并在 100 个周期内拟合模型:

    model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])
    model.fit(X, Y, batch_size=1, epochs=100, shuffle=False)
    
  6. 您可以使用箱型图来可视化输出。

    results.boxplot()
    pyplot.show()
    

    预期输出:

图 5.34:有状态与无状态的箱型图

图 5.34:有状态与无状态的箱型图

注意

输出可能会根据使用的数据而有所不同。

从有状态模型的概念中,我们了解到,按批次输入的数据需要是连续的,因此需要关闭随机化OFF。然而,即使batch_size >1,批次之间的数据也不会是连续的,因此需要设置batch_size=1。通过将网络设置为stateful=True并使用上述参数进行拟合,实际上我们是在以有状态的方式正确地训练模型。

然而,我们没有使用小批量梯度下降的概念,也没有对数据进行打乱。因此,需要实现一个生成器,以便小心地训练有状态的网络,但这超出了本章的范围。

model.compile是一个函数,用于为网络分配优化器、损失函数和我们关心的评估指标。

model.fit()是一个用于训练模型的函数,通过指定训练数据、验证数据、训练轮数、批次大小、打乱模式等来进行训练。

活动 6:使用 RNN 解决问题——作者身份归属。

作者身份归属是一个经典的文本分类问题,属于自然语言处理(NLP)范畴。作者身份归属是一个研究深入的问题,它催生了风格计量学领域。

在这个问题中,我们给定了一组来自特定作者的文档。我们需要训练一个模型来理解作者的写作风格,并使用该模型来识别未知文档的作者。与许多其他自然语言处理(NLP)问题一样,这个问题得益于计算能力、数据和先进机器学习技术的提升。这使得作者身份归属成为使用深度学习(DL)的一个自然选择。特别是,我们可以利用深度学习自动提取与特定问题相关的特征的能力。

在本次活动中,我们将专注于以下内容:

  1. 从每个作者的文本中提取字符级别的特征(以获取每个作者的写作风格)。

  2. 使用这些特征构建一个分类模型来进行作者身份归属。

  3. 将模型应用于识别一组未知文档的作者。

    注意

    你可以在 https://github.com/TrainingByPackt/Deep-Learning-for-Natural-Language-Processing/tree/master/Lesson 05 找到本次活动所需的数据。

以下步骤将帮助你解决问题。

  1. 导入必要的 Python 包。

  2. 上传要使用的文本文件。然后,通过将所有文本转换为小写、将所有换行符和多个空格转换为单个空格,并去除任何提到作者姓名的部分来预处理文本文件,否则我们可能会导致数据泄露。

  3. 为了将长文本分割成较小的序列,我们使用 Keras 框架中的Tokenizer类。

  4. 接下来创建训练集和验证集。

  5. 我们构建模型图并执行训练过程。

  6. 将模型应用于未知的文档。对Unknown文件夹中的所有文档进行此操作。

    预期输出:

图 5.35:作者身份归属输出

图 5.35:作者归属的输出

注意

活动的解答可以在第 309 页找到。

总结

在本章中,我们介绍了循环神经网络(RNN),并讨论了 RNN 与前馈神经网络(FFNN)架构之间的主要区别。我们学习了反向传播通过时间(BPTT)以及权重矩阵的更新方法。我们通过 Keras 学习了如何使用 RNN,并通过 Keras 解决了一个作者归属的问题。我们通过观察梯度消失和梯度爆炸问题,分析了 RNN 的不足之处。在接下来的章节中,我们将深入研究解决这些问题的架构。

第七章:第六章

门控递归单元(GRU)

学习目标

本章结束时,你将能够:

  • 评估简单递归神经网络(RNNs)的缺点

  • 描述门控递归单元(GRU)的架构

  • 使用 GRU 进行情感分析

  • 应用 GRU 进行文本生成

本章旨在为现有 RNN 架构的缺点提供解决方案。

介绍

在前几章中,我们学习了文本处理技术,如词嵌入、分词和词频-逆文档频率(TFIDF)。我们还了解了一种特定的网络架构——递归神经网络(RNN),其存在消失梯度的问题。

在本章中,我们将研究一种通过在网络中添加记忆的有序方法来处理消失梯度的机制。本质上,GRU 中使用的门控是决定哪些信息应该传递到网络下一个阶段的向量。反过来,这有助于网络相应地生成输出。

一个基本的 RNN 通常由输入层、输出层和几个相互连接的隐藏层组成。下图展示了 RNN 的基本架构:

图 6.1:一个基本的 RNN

图 6.1:一个基本的 RNN

RNN 在其最简单的形式中存在一个缺点,即无法保留序列中的长期关系。为了纠正这个缺陷,需要向简单的 RNN 网络中添加一个特殊的层,叫做门控递归单元(GRU)。

在本章中,我们将首先探讨简单 RNN 无法保持长期依赖关系的原因,然后介绍 GRU 层及其如何尝试解决这个特定问题。接着,我们将构建一个包含 GRU 层的网络。

简单 RNN 的缺点

让我们通过一个简单的例子来回顾一下消失梯度的概念。

本质上,你希望使用 RNN 生成一首英文诗歌。在这里,你设置了一个简单的 RNN 来完成任务,结果它生成了以下句子:

"这些花,尽管已是秋天,却像星星一样盛开。"

很容易发现这里的语法错误。单词'blooms'应该是'bloom',因为句子开头的单词'flowers'表示应该使用'bloom'的复数形式,以使主谓一致。简单的 RNN 无法完成这一任务,因为它无法保留句子开头出现的单词'flowers'和后面出现的单词'blooms'之间的依赖关系(理论上,它应该能够做到!)。

GRU(门控循环单元)通过消除“消失梯度”问题来帮助解决这一问题,该问题妨碍了网络的学习能力,在长时间的文本关系中,网络未能保留这些关系。在接下来的部分中,我们将专注于理解消失梯度问题,并详细讨论 GRU 如何解决这个问题。

现在让我们回顾一下神经网络是如何学习的。在训练阶段,输入逐层传播,直到输出层。由于我们知道在训练过程中,对于给定输入,输出应该产生的确切值,我们计算预期输出和实际输出之间的误差。然后,这个误差被输入到一个成本函数中(这个成本函数会根据问题和网络开发者的创意而有所不同)。接下来的步骤是计算该成本函数相对于网络每个参数的梯度,从离输出层最近的层开始,一直到最底层的输入层:

图 6.2:一个简单的神经网络

图 6.2:一个简单的神经网络

考虑一个非常简单的神经网络,只有四层,并且每一层之间只有一个连接,并且只有一个单一的输出,如前图所示。请注意,实际应用中你不会使用这样的网络;它这里只是用来演示消失梯度问题的概念。

现在,为了计算成本函数相对于第一隐藏层偏置项 b[1] 的梯度,需要进行以下计算:

图 6.3:使用链式法则计算梯度

图 6.3:使用链式法则计算梯度

这里,每个元素的解释如下:

grad(x, y) = x 关于 y 的梯度

d(var) = 'var' 变量的 'sigmoid' 导数

w[i] = 第 'i' 层的权重

b[i] = 第 'i' 层的偏置项

a[i] = 第 'i' 层的激活函数

z[j] = w[j]*a[j-1] + b[j]

上述表达式可以归因于微分链式法则。

上述方程涉及多个项的乘法。如果这些项中的大多数值是在 -1 和 1 之间的分数,那么这些分数的乘积最终会得到一个非常小的值。在上述例子中,grad(C,b[1]) 的值将是一个非常小的分数。问题在于,这个梯度是将在下一次迭代中用来更新 b[1] 值的项:

![图 6.4:使用梯度更新 b[1] 的值

](https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-nlp/img/C13783_06_04.jpg)

图 6.4:使用梯度更新 b[1] 的值

注意

使用不同优化器执行更新可能有几种方式,但概念本质上是相同的。

这种情况的结果是,b[1]的值几乎没有从上一次迭代中改变,这导致了非常缓慢的学习进展。在一个现实世界的网络中,可能有很多层,这种更新会变得更加微小。因此,网络越深,梯度问题就越严重。这里还观察到,靠近输出层的层学习得比靠近输入层的层更快,因为前者的乘法项更少。这也导致了学习的不对称性,进而引发了梯度的不稳定性。

那么,这个问题对简单 RNN 有什么影响呢?回顾一下 RNN 的结构;它本质上是一个随着时间展开的层次结构,层数与单词数量相同(对于建模问题)。学习过程通过时间反向传播(BPTT)进行,这与之前描述的机制完全相同。唯一的区别是,每一层的相同参数都会被更新。后面的层对应的是句子中后出现的单词,而前面的层对应的是句子中先出现的单词。由于梯度消失,前面的层与初始值的变化很小,因此它们对后续层的影响也很小。距离当前时间点't'越远的层,对该时间点层输出的影响就越小。因此,在我们的示例句子中,网络很难学习到“flowers”是复数形式,这导致了错误的“bloom”形式被使用。

梯度爆炸问题

事实证明,梯度不仅会消失,还可能会爆炸——也就是说,早期层可能学习得过快,训练迭代之间的值偏差较大,而后期层的梯度变化则不那么迅速。这是如何发生的呢?好吧,回顾一下我们的方程式,如果单个项的值远大于 1 的数量级,乘法效应就会导致梯度变得非常大。这会导致梯度的不稳定并引发学习问题。

最终,这个问题是梯度不稳定的问题。实际上,梯度消失问题比梯度爆炸问题更常见,也更难解决。

幸运的是,梯度爆炸问题有一个强有力的解决方案:裁剪(clipping)。裁剪仅仅是指停止梯度值的增长,防止其超过预设的值。如果梯度值没有被裁剪,你将开始看到梯度和网络权重出现 NaN(不是一个数字),这是由于计算机的表示溢出。为梯度值设定上限可以帮助避免这个问题。请注意,裁剪只限制梯度的大小,而不限制其方向。因此,学习过程仍然沿着正确的方向进行。梯度裁剪效果的简单可视化可以在下图中看到:

图 6.5:裁剪梯度以应对梯度爆炸

图 6.5:裁剪梯度以应对梯度爆炸

门控循环单元(GRUs)

GRUs 帮助网络以显式的方式记住长期依赖关系。这是通过在简单的 RNN 结构中引入更多的变量实现的。

那么,什么可以帮助我们解决梯度消失问题呢?直观地说,如果我们允许网络从前一个时间步的激活函数中转移大部分知识,那么误差可以比简单的 RNN 情况更忠实地反向传播。如果你熟悉用于图像分类的残差网络,你会发现这个函数与跳跃连接(skip connection)非常相似。允许梯度反向传播而不消失,能够使网络在各层之间更加均匀地学习,从而消除了梯度不稳定的问题:

图 6.6:完整的 GRU 结构

图 6.6:完整的 GRU 结构

上图中不同符号的含义如下:

图 6.7:GRU 图中不同符号的含义

图 6.7:GRU 图中不同符号的含义

注意

哈达玛积运算是逐元素矩阵乘法。

上图展示了 GRU 所利用的所有组件。你可以观察到不同时间步(h[t]h[t-1])下的激活函数 h。r[t]项表示重置门,z[t]项表示更新门。h'[t]项表示候选函数,为了明确表达,我们将在方程中用h_candidate[t]变量表示它。GRU 层使用更新门来决定可以传递给下一个激活的先前信息量,同时使用重置门来决定需要忘记的先前信息量。在本节中,我们将详细检查这些术语,并探讨它们如何帮助网络记住文本结构中的长期关系。

下一层的激活函数(隐藏层)的表达式如下:

图 6.8:下一层激活函数的表达式,以候选激活函数为基础

图 6.8:下一层激活函数的表达式,以候选激活函数为基础

因此,激活函数是上一时间步的激活与当前时间步的候选激活函数的加权和。z[t] 函数是一个 sigmoid 函数,因此其取值范围在 0 和 1 之间。在大多数实际情况下,值更接近 0 或 1。在深入探讨前述表达式之前,让我们稍微观察一下引入加权求和方案更新激活函数的效果。如果z[t] 在多个时间步中保持为 1,则表示非常早期时间步的激活函数的值仍然可以传递到更晚的时间步。这反过来为网络提供了记忆能力。

此外,请观察它与简单 RNN 的不同之处,在简单 RNN 中,激活函数的值在每个时间步都会被覆盖,而不会显式地加权前一时间步的激活(在简单 RNN 中,前一激活的贡献是通过非线性操作隐含存在的)。

门类型

现在让我们在接下来的部分中展开讨论前述的激活更新方程。

更新门

更新门由下图表示。正如你从完整的 GRU 图中看到的,只有相关部分被突出显示。更新门的目的是确定从前一时间步传递到下一步激活的必要信息量。为了理解该图和更新门的功能,请考虑以下计算更新门的表达式:

图 6.9:计算更新门的表达式

图 6.9:计算更新门的表达式

以下图展示了更新门的图形表示:

图 6.10:完整 GRU 图中的更新门

图 6.10:完整 GRU 图中的更新门

隐藏状态的数量是n_hh的维度),而输入维度的数量是 n_x。时间步 t 的输入(x[t])将与一组新权重 W_z 相乘,维度为(n_h, n_x)。上一时间步的激活函数(h[t-1])将与另一组新权重 U_z 相乘,维度为(n_h, n_h)。

请注意,这里的乘法是矩阵乘法。然后将这两个项相加,并通过 sigmoid 函数将输出 z[t] 压缩到 [0,1] 范围内。z[t] 输出具有与激活函数相同的维度,即(n_h, 1)。W_zU_z 参数也需要通过 BPTT 进行学习。让我们编写一个简单的 Python 代码片段来帮助我们理解更新门:

import numpy as np
# Write a sigmoid function to be used later in the program
def sigmoid(x):
    return 1 / (1 + np.exp(-x))
n_x = 5 # Dimensionality of input vector
n_h = 3 # Number of hidden units
# Define an input at time 't' having a dimensionality of n_x
x_t = np.random.randn(n_x, 1)
# Define W_z, U_z and h_prev (last time step activation)
W_z = np.random.randn(n_h,  n_x) # n_h = 3, n_x=5
U_z = np.random.randn(n_h, n_h) # n_h = 3
h_prev = np.random.randn(n_h, 1)

图 6.11:显示权重和激活函数的截图

以下是更新门表达式的代码片段:

# Calculate expression for update gate
z_t = sigmoid(np.matmul(W_z, x_t) + np.matmul(U_z, h_prev))

在前面的代码片段中,我们初始化了 x[t]W_zU_zh_prev 的随机值,以演示 z[t] 的计算。在真实的网络中,这些变量将具有更相关的值。

重置门

重置门由以下图示表示。从完整的 GRU 图示中可以看到,只有相关部分被突出显示。重置门的目的是确定在计算下一个步骤的激活时,应该遗忘多少来自前一个时间步的信息。为了理解图示和重置门的功能,考虑以下用于计算重置门的表达式:

图 6.12:计算重置门的表达式

图 6.12:计算重置门的表达式

以下图示展示了重置门的图形表示:

图 6.13:重置门

图 6.13:重置门

在时间步 t 处的输入与权重 W_r 相乘,使用维度(n_h, n_x)。然后,将前一个时间步的激活函数(h[t-1])与另一组新权重 U_r 相乘,使用维度(n_h, n_h)。请注意,这里的乘法是矩阵乘法。然后将这两个项相加,并通过 sigmoid 函数将 r[t] 输出压缩到 [0,1] 范围内。r[t] 输出具有与激活函数相同的维度,即(n_h, 1)。

W_rU_r 参数也需要通过 BPTT 进行学习。让我们来看一下如何在 Python 中计算重置门的表达式:

# Define W_r, U_r
W_r = np.random.randn(n_h,  n_x) # n_h = 3, n_x=5
U_r = np.random.randn(n_h, n_h) # n_h = 3
# Calculate expression for update gate
r_t = sigmoid(np.matmul(W_r, x_t) + np.matmul(U_r, h_prev))

在前面的代码片段中,已经使用了来自更新门代码片段的 x_th_prevn_hn_x 变量的值。请注意,r_t 的值可能不会特别接近 0 或 1,因为这只是一个示例。在经过良好训练的网络中,值应该接近 0 或 1:

图 6.14:显示权重值的截图

图 6.14:显示权重值的截图

图 6.15:显示  输出的截图

图 6.15:显示 r_t 输出的截图

候选激活函数

在每个时间步长还计算了一个候选激活函数,用于替换前一个时间步长的激活函数。顾名思义,候选激活函数表示下一个时间步长激活函数应该采取的替代值。看看以下计算候选激活函数的表达式:

图 6.16: 计算候选激活函数表达式

图 6.16: 计算候选激活函数表达式

以下图显示了候选激活函数的图形表示:

图 6.17: 候选激活函数

图 6.17: 候选激活函数

在时间步 t 处的输入与权重 W 进行了维度 (n_h, n_x) 的乘法。W 矩阵的作用与简单 RNN 中使用的矩阵相同。然后,重置门与上一个时间步长的激活函数 (h[t-1]) 进行逐元素乘法。这个操作被称为 'Hadamard 乘法'。乘法的结果通过维度为 (n_h, n_h) 的 U 矩阵进行矩阵乘法。U 矩阵与简单 RNN 中使用的矩阵相同。然后将这两项加在一起,并通过双曲正切函数进行处理,以使输出 h_candidate[t] 被压缩到 [-1,1] 的范围内。h_candidate[t] 输出的维度与激活函数相同,即 (n_h, 1):

# Define W, U
W = np.random.randn(n_h,  n_x) # n_h = 3, n_x=5
U = np.random.randn(n_h, n_h) # n_h = 3
# Calculate h_candidate
h_candidate = np.tanh(np.matmul(W, x_t) + np.matmul(U,np.multiply(r_t, h_prev)))

再次使用与更新门和重置门计算中相同的变量值。请注意,Hadamard 矩阵乘法是使用 NumPy 函数 'multiply' 实现的:

图 6.18: 展示了 W 和 U 权重如何定义的截图

图 6.18: 展示了 W 和 U 权重如何定义的截图

以下图显示了 h_candidate 函数的图形表示:

图 6.19: 展示了 h_candidate 值的截图

图 6.19: 展示了 h_candidate 值的截图

现在,由于已经计算出更新门、重置门和候选激活函数的值,我们可以编写用于传递到下一层的当前激活函数表达式:

# Calculate h_new
h_new = np.multiply(z_t, h_prev) + np.multiply((1-z_t), h_candidate)

图 6.20: 展示了当前激活函数的值的截图

图 6.20: 展示了当前激活函数的值的截图

从数学角度讲,更新门的作用是选择前一个激活函数和候选激活函数之间的加权。因此,它负责当前时间步的激活函数的最终更新,并决定多少前一个激活函数和候选激活函数将传递到下一层。复位门的作用是选择或取消选择前一个激活函数的部分。这就是为什么要对前一个激活函数和复位门向量进行逐元素乘法的原因。考虑我们之前提到的诗歌生成句子的例子:

“尽管是秋天,花朵像星星一样绽放。”

一个复位门将用于记住单词“flowers”对单词“bloom”复数形式的影响,这种影响发生在句子的后半部分。因此,复位门向量中特定的值负责记住单词的复数或单数形式,该值将接近 0 或 1。如果 0 表示单数,那么在我们的例子中,复位门将保持值为 1,以记住单词“bloom”应采用复数形式。复位门向量中的不同值将记住句子复杂结构中的不同关系。

作为另一个例子,请考虑以下句子:

“来自法国的食物很美味,但法国人也非常热情好客。”

在分析句子结构时,我们可以看到有几个复杂的关系需要记住:

  • 单词“food”与单词“delicious”相关(这里,“delicious”只能在“food”的语境中使用)。

  • 单词“France”与“French”人相关。

  • 单词“people”和“were”是相互关联的;也就是说,单词“people”的使用决定了“was”的正确形式。

在一个训练良好的网络中,复位门的向量中会包含所有这些关系的条目。这些条目的值将根据需要记住哪些来自先前激活的关系,哪些需要遗忘,适当地被设置为“关闭”或“开启”。在实践中,很难将复位门或隐藏状态的条目归因于某个特定功能。因此,深度学习网络的可解释性仍然是一个热门的研究话题。

GRU 的变种

上面描述的 GRU 的形式是完整的 GRU。许多独立的研究者已使用不同形式的 GRU,比如完全移除复位门或使用激活函数。然而,完整的 GRU 仍然是最常用的方法。

基于 GRU 的情感分析

情感分析是应用自然语言处理技术的一个热门用例。情感分析的目的是判断给定的文本是否可以被视为传达“正面”情绪或“负面”情绪。例如,考虑以下文本对一本书的评论:

"这本书有过辉煌的时刻,但似乎经常偏离了要点。这样水准的作者,肯定比这本书所呈现的要更多。"

对于人类读者来说,显然提到的这本书评传达了一个负面情绪。那么,如何构建一个机器学习模型来进行情感分类呢?和往常一样,使用监督学习方法时,需要一个包含多个样本的文本语料库。语料库中的每一篇文本应该有一个标签,指示该文本是可以映射到正面情绪还是负面情绪。下一步是使用这些数据构建一个机器学习模型。

观察这个例子,你已经可以看到这个任务对于机器学习模型来说可能是具有挑战性的。如果使用简单的分词或 TFIDF 方法,像‘辉煌’和‘水准’这样的词语可能会被分类器误认为是传达正面情绪。更糟糕的是,文本中没有任何词语可以直接解读为负面情绪。这一观察也揭示了需要连接文本中不同部分结构的必要性,以便从句子中提取出真正的意义。例如,第一句话可以被分解为两部分:

  1. "这本书有过辉煌的时刻"

  2. ",但似乎经常偏离了要点。"

仅仅看句子的第一部分,可能会让你得出评论是积极的结论。只有在考虑到第二句话时,句子的含义才真正能被理解为表达负面情感。因此,这里需要保留长期依赖关系。简单的 RNN 模型显然无法胜任这项任务。那么我们来尝试在情感分类任务中应用 GRU,并看看它的表现。

练习 25:计算情感分类模型的验证准确率和损失

在本练习中,我们将使用 imdb 数据集编写一个简单的情感分类系统。imdb 数据集包含 25,000 个训练文本序列和 25,000 个测试文本序列——每个序列都包含一篇电影评论。输出变量是一个二元变量,如果评论为负面,则值为 0;如果评论为正面,则值为 1:

注意

所有练习和活动都应该在 Jupyter notebook 中运行。用于创建 Python 环境以运行此 notebook 的 requirements.txt 文件内容如下:h5py2.9.0,keras2.2.4,numpy1.16.1,tensorflow1.12.0。

解决方案:

我们首先加载数据集,如下所示:

from keras.datasets import imdb
  1. 我们还将定义生成训练序列时要考虑的最大频率最高的单词数为 10,000。我们还将限制序列长度为 500:

    max_features = 10000
    maxlen = 500
    
  2. 现在让我们按如下方式加载数据:

    (train_data, y_train), (test_data, y_test) = imdb.load_data(num_words=max_features)
    print('Number of train sequences: ', len(train_data))
    print('Number of test sequences: ', len(test_data))
    

    图 6.21:显示训练和测试序列的截图

    图 6.21:显示训练和测试序列的截图
  3. 可能存在长度小于 500 的序列,因此我们需要对其进行填充,使其长度恰好为 500。我们可以使用 Keras 函数来实现这一目的:

    from keras.preprocessing import sequence
    train_data = sequence.pad_sequences(train_data, maxlen=maxlen)
    test_data = sequence.pad_sequences(test_data, maxlen=maxlen)
    
  4. 让我们检查训练和测试数据的形状,如下所示:

    print('train_data shape:', train_data.shape)
    print('test_data shape:', test_data.shape)
    

    验证两个数组的形状是否为(25,000, 500)。

  5. 现在让我们构建一个带有 GRU 单元的 RNN。首先,我们需要导入必要的包,如下所示:

    from keras.models import Sequential
    from keras.layers import Embedding
    from keras.layers import Dense
    from keras.layers import GRU
    
  6. 由于我们将使用 Keras 的顺序 API 来构建模型,因此需要从 Keras 模型中导入顺序模型 API。嵌入层本质上将输入向量转换为固定大小,然后可以将其馈送到网络的下一层。如果使用,它必须作为网络的第一层添加。我们还导入了一个 Dense 层,因为最终正是这个层提供了目标变量(0 或 1)的分布。

    最后,我们导入 GRU 单元;让我们初始化顺序模型 API 并添加嵌入层,如下所示:

    model = Sequential()
    model.add(Embedding(max_features, 32))
    

    嵌入层接受 max_features 作为输入,我们定义它为 10,000。32 的值在此处设置,因为下一个 GRU 层期望从嵌入层获取 32 个输入。

  7. 接下来,我们将添加 GRU 和 Dense 层,如下所示:

    model.add(GRU(32))
    model.add(Dense(1, activation='sigmoid'))
    
  8. 32 的值是随意选择的,并可以作为设计网络时调整的超参数之一。它表示激活函数的维度。Dense 层只输出 1 个值,即评论(即我们的目标变量)为 1 的概率。我们选择 sigmoid 作为激活函数。

    接下来,我们使用二元交叉熵损失和 rmsprop 优化器来编译模型:

    model.compile(optimizer='rmsprop',
                  loss='binary_crossentropy',
                  metrics=['acc'])
    
  9. 我们选择将准确率(训练和验证)作为度量指标。接下来,我们将模型拟合到我们的序列数据上。请注意,我们还将从训练数据中分配 20%的样本作为验证数据集。我们还设置了 10 个周期和 128 的批次大小——即在一次前向反向传递中,我们选择在一个批次中传递 128 个序列:

    history = model.fit(train_data, y_train,
                        epochs=10,
                        batch_size=128,
                        validation_split=0.2
    

    图 6.22:显示训练模型的变量历史输出的截图

    图 6.22:显示训练模型的变量历史输出的截图

    变量历史可以用来跟踪训练进度。上一个函数将触发一个训练会话,在本地 CPU 上训练需要几分钟时间。

  10. 接下来,让我们通过绘制损失和准确率来查看训练进展的具体情况。为此,我们将定义一个绘图函数,如下所示:

    import matplotlib.pyplot as plt
    def plot_results(history):
        acc = history.history['acc']
        val_acc = history.history['val_acc']
        loss = history.history['loss']
        val_loss = history.history['val_loss']
    
        epochs = range(1, len(acc) + 1)
        plt.plot(epochs, acc, 'bo', label='Training Accuracy')
        plt.plot(epochs, val_acc, 'b', label='Validation Accuracy')
        plt.title('Training and validation Accuracy')
        plt.legend()
        plt.figure()
        plt.plot(epochs, loss, 'bo', label='Training Loss')
        plt.plot(epochs, val_loss, 'b', label='Validation Loss')
        plt.title('Training and validation Loss')
        plt.legend()
        plt.show()
    
  11. 让我们在作为 'fit' 函数输出的 history 变量上调用我们的函数:

    plot_results(history)
    
  12. 当作者运行时,前面的代码输出如下面的图示所示:

    期望输出:

图 6.23:情感分类任务的训练和验证准确率

](https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-nlp/img/C13783_06_23.jpg)

图 6.23:情感分类任务的训练和验证准确率

以下图示展示了训练和验证的损失:

图 6.24:情感分类任务的训练和验证损失

](https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-nlp/img/C13783_06_24.jpg)

图 6.24:情感分类任务的训练和验证损失

注意

在最佳时期,验证准确率相当高(约 87%)。

活动 7:使用简单 RNN 开发情感分类模型

在这个活动中,我们的目标是使用一个简单的 RNN 生成一个情感分类模型。这样做是为了判断 GRU 相较于简单 RNN 的效果。

  1. 加载数据集。

  2. 填充序列,使每个序列具有相同数量的字符。

  3. 定义并编译模型,使用带有 32 个隐藏单元的简单 RNN。

  4. 绘制验证和训练准确率与损失。

    注意

    该活动的解决方案可以在第 317 页找到。

使用 GRU 进行文本生成

文本生成问题需要一个算法来根据训练语料库生成新文本。例如,如果你将莎士比亚的诗歌输入学习算法,那么该算法应该能够以莎士比亚的风格生成新文本(逐字符或逐词生成)。接下来,我们将展示如何使用本章所学的内容来处理这个问题。

练习 26:使用 GRU 生成文本

那么,让我们回顾一下本章前面部分提出的问题。也就是说,你希望使用深度学习方法生成一首诗。我们将使用 GRU 来解决这个问题。我们将使用莎士比亚的十四行诗来训练我们的模型,以便我们的输出诗歌呈现莎士比亚风格:

  1. 让我们先导入所需的 Python 包,如下所示:

    import io
    import sys
    import random
    import string
    import numpy as np
    from keras.models import Sequential
    from keras.layers import Dense
    from keras.layers import GRU
    from keras.optimizers import RMSprop
    

    每个包的使用将在接下来的代码片段中变得清晰。

  2. 接下来,我们定义一个函数,从包含莎士比亚十四行诗的文件中读取内容并打印出前 200 个字符:

    def load_text(filename):
        with open(filename, 'r') as f:
            text = f.read()
        return text
    file_poem = 'shakespeare_poems.txt' # Path of the file
    text = load_text(file_poem)
    print(text[:200])
    

    图 6.25:莎士比亚十四行诗截图

    图 6.25:莎士比亚十四行诗截图
  3. 接下来,我们将进行一些数据准备步骤。首先,我们将从读取的文件中获取所有不同字符的列表。然后,我们将创建一个字典,将每个字符映射到一个整数索引。最后,我们将创建另一个字典,将整数索引映射到字符:

    chars = sorted(list(set(text)))
    print('Number of distinct characters:', len(chars))
    char_indices = dict((c, i) for i, c in enumerate(chars))
    indices_char = dict((i, c) for i, c in enumerate(chars))
    
  4. 现在,我们将从文本中生成训练数据的序列。我们会为模型输入每个固定长度为 40 个字符的序列。这些序列将按滑动窗口的方式生成,每个序列滑动三步。考虑诗歌中的以下部分:

"从最美的生物中,我们渴望增长,

这样美丽的玫瑰就永远不会凋谢,"

我们的目标是从前面的文本片段中得到以下结果:

图 6.26:训练序列的屏幕截图

图 6.26:训练序列的屏幕截图

这些序列的长度均为 40 个字符。每个后续字符串都比前一个字符串向右滑动了三步。这样安排是为了确保我们得到足够的序列(而不会像步长为 1 时那样得到太多序列)。通常,我们可以有更多的序列,但由于这个例子是演示用的,因此将运行在本地 CPU 上,输入过多的序列会使训练过程变得比预期要长得多。

此外,对于这些序列中的每一个,我们需要有一个输出字符,作为文本中的下一个字符。本质上,我们是在教模型观察 40 个字符,然后学习下一个最可能的字符是什么。为了理解输出字符是什么,可以考虑以下序列:

这样美丽的玫瑰就永远不会凋谢

该序列的输出字符将是 i 字符。这是因为在文本中,i 是下一个字符。以下代码片段实现了相同的功能:

max_len_chars = 40
step = 3
sentences = []
next_chars = []
for i in range(0, len(text) - max_len_chars, step):
    sentences.append(text[i: i + max_len_chars])
    next_chars.append(text[i + max_len_chars])
print('nb sequences:', len(sentences))

现在我们有了希望训练的序列及其对应的字符输出。接下来,我们需要为样本获得一个训练矩阵,并为输出字符获得另一个矩阵,这些矩阵将被输入到模型中进行训练:

x = np.zeros((len(sentences), max_len_chars, 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

这里,x 是存储输入训练样本的矩阵。x 数组的形状是序列的数量、最大字符数和不同字符的数量。因此,x 是一个三维矩阵。所以,对于每个序列,也就是对于每个时间步(=最大字符数),我们都有一个与文本中不同字符数量相同长度的独热编码向量。这个向量在给定时间步的字符位置上为 1,其他所有位置为 0。y 是一个二维矩阵,形状为序列数和不同字符数。因此,对于每个序列,我们都有一个与不同字符数量相同长度的独热编码向量。除了与当前输出字符对应的位置为 1,其他所有位置都是 0。独热编码是通过我们在之前步骤中创建的字典映射完成的。

  1. 我们现在准备好定义我们的模型,如下所示:

    model = Sequential()
    model.add(GRU(128, input_shape=(max_len_chars, len(chars))))
    model.add(Dense(len(chars), activation='softmax'))
    optimizer = RMSprop(lr=0.01)
    model.compile(loss='categorical_crossentropy', optimizer=optimizer)
    
  2. 我们使用顺序 API,添加一个具有 128 个隐藏参数的 GRU 层,然后添加一个全连接层。

    注意

    Dense 层的输出数量与不同字符的数量相同。这是因为我们本质上是在学习我们词汇表中可能字符的分布。从这个意义上讲,这本质上是一个多类分类问题,这也解释了我们为何选择分类交叉熵作为代价函数。

  3. 我们现在继续将模型拟合到数据上,如下所示:

    model.fit(x, y,batch_size=128,epochs=10)
    model.save("poem_gen_model.h5")
    

    在这里,我们选择了 128 个序列的批量大小,并训练了 10 个周期。我们还将模型保存在 hdf5 格式文件中,以便以后使用:

    图 6.27:显示训练周期的截图

    图 6.27:显示训练周期的截图

    注意

    你应该增加 GRU 的数量和训练周期。它们的值越高,训练模型所需的时间就越长,但可以期待更好的结果。

  4. 接下来,我们需要能够使用模型实际生成一些文本,如下所示:

    from keras.models import load_model
    model_loaded = load_model('poem_gen_model.h5')
    
  5. 我们还定义了一个采样函数,根据字符的概率分布选择一个候选字符:

    def sample(preds, temperature=1.0):
        # helper function to sample an index from a probability array
        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)
    
  6. 我们使用多项分布进行采样;温度参数有助于为概率分布添加偏差,使得较不可能的词可以有更多或更少的表示。你也可以简单地尝试对 preds 变量使用 argmax 参数,但这可能会导致单词的重复:

    def generate_poem(model, num_chars_to_generate=400):
        start_index = random.randint(0, len(text) - max_len_chars - 1)
        generated = ''
        sentence = text[start_index: start_index + max_len_chars]
        generated += sentence
        print("Seed sentence: {}".format(generated))
        for i in range(num_chars_to_generate):
            x_pred = np.zeros((1, max_len_chars, len(chars)))
            for t, char in enumerate(sentence):
                x_pred[0, t, char_indices[char]] = 1.
    
            preds = model.predict(x_pred, verbose=0)[0]
            next_index = sample(preds, 1)
            next_char = indices_char[next_index]
            generated += next_char
            sentence = sentence[1:] + next_char
        return generated
    
  7. 我们传入加载的模型和希望生成的字符数。然后,我们传入一个种子文本供模型作为输入(记住,我们教模型在给定 40 个字符的序列长度的情况下预测下一个字符)。这一过程发生在 for 循环开始之前。在循环的第一次迭代中,我们将种子文本传入模型,生成输出字符,并将输出字符附加到‘generated’变量中。在下一次迭代中,我们将刚更新的序列(在第一次迭代后有 41 个字符)右移一个字符,这样模型就可以将这个包含 40 个字符的新输入作为输入,其中最后一个字符是我们刚刚生成的新字符。现在,可以按如下方式调用函数:

    generate_poem(model_loaded, 100)
    

    看!你已经写出了一首莎士比亚风格的诗歌。一个示例输出如下所示:

图 6.28:显示生成的诗歌序列输出的截图

你会立刻注意到诗歌并没有真正有意义。这可以归因于两个原因:

  • 上述输出是在使用非常少量的数据或序列时生成的。因此,模型无法学到太多。在实践中,你会使用一个更大的数据集,从中生成更多的序列,并使用 GPU 进行训练以确保合理的训练时间(我们将在最后一章 9-‘组织中的实际 NLP 项目工作流’中学习如何在云 GPU 上训练)。

  • 即使使用大量数据进行训练,也总会有一些错误,因为模型的学习能力是有限的。

然而,我们仍然可以看到,即使在这个基本设置下,模型是一个字符生成模型,仍然有些词语是有意义的。像“我有喜好”这样的短语,作为独立短语是有效的。

注意

空白字符、换行符等也被模型学习。

活动 8:使用你选择的数据集训练你自己的字符生成模型

我们刚刚使用了一些莎士比亚的作品来生成我们自己的诗歌。你不必局限于诗歌生成,也可以使用任何一段文本来开始生成自己的写作。基本的步骤和设置与之前的示例相同。

注意

使用 requirements 文件创建一个 Conda 环境并激活它。然后,在 Jupyter notebook 中运行代码。别忘了输入一个包含你希望生成新文本的作者风格的文本文件。

  1. 加载文本文件。

  2. 创建字典,将字符映射到索引,反之亦然。

  3. 从文本中创建序列。

  4. 创建输入和输出数组以供模型使用。

  5. 使用 GRU 构建并训练模型。

  6. 保存模型。

  7. 定义采样和生成函数。

  8. 生成文本。

    注意

    活动的解决方案可以在第 320 页找到。

摘要

GRU 是简单 RNN 的扩展,它通过让模型学习文本结构中的长期依赖关系来帮助解决梯度消失问题。各种用例可以从这个架构单元中受益。我们讨论了情感分类问题,并了解了 GRU 如何优于简单 RNN。接着我们看到如何使用 GRU 来生成文本。

在下一章中,我们将讨论简单 RNN 的另一项进展——长短期记忆(LSTM)网络,并探讨它们通过新架构带来的优势。

第八章:第七章

长短期记忆(LSTM)

学习目标

本章结束时,您将能够:

  • 描述 LSTM 的目的

  • 详细评估 LSTM 的架构

  • 使用 LSTM 开发一个简单的二分类模型

  • 实现神经语言翻译,并开发一个英德翻译模型

本章简要介绍了 LSTM 架构及其在自然语言处理领域的应用。

介绍

在前面的章节中,我们学习了递归神经网络(RNN)以及一种专门的架构——门控递归单元(GRU),它有助于解决梯度消失问题。LSTM 提供了另一种解决梯度消失问题的方法。在本章中,我们将仔细研究 LSTM 的架构,看看它们如何使神经网络以忠实的方式传播梯度。

此外,我们还将看到 LSTM 在神经语言翻译中的有趣应用,这将使我们能够构建一个模型,用于将一种语言的文本翻译成另一种语言。

LSTM

梯度消失问题使得梯度很难从网络的后层传播到前层,导致网络的初始权重不会从初始值发生太大变化。因此,模型无法很好地学习,导致性能不佳。LSTM 通过引入“记忆”到网络中解决了这个问题,这使得文本结构中的长期依赖得以保持。然而,LSTM 的记忆添加方式与 GRU 的方式不同。在接下来的章节中,我们将看到 LSTM 是如何完成这一任务的。

LSTM 帮助网络以明确的方式记住长期依赖性。与 GRU 相似,这是通过在简单 RNN 的结构中引入更多变量来实现的。

使用 LSTM,我们允许网络从先前时间步的激活中转移大部分知识,这是简单 RNN 难以实现的壮举。

回忆一下简单 RNN 的结构,它本质上是相同单元的展开,可以通过以下图示表示:

图 7.1:标准 RNN 中的重复模块

图 7.1:标准 RNN 中的重复模块

图中块"A"的递归表示它是随着时间重复的相同结构。每个单元的输入是来自先前时间步的激活(由字母"h"表示)。另一个输入是时间"t"时的序列值(由字母"x"表示)。

与简单 RNN 的情况类似,LSTM 也具有固定的、时间展开的重复结构,但重复的单元本身具有不同的结构。每个 LSTM 单元有几种不同类型的模块,它们共同作用,为模型提供记忆。LSTM 的结构可以通过以下图示表示:

图 7.2:LSTM 单元

图 7.2:LSTM 单元

让我们也来熟悉一下在图示中使用的符号:

图 7.3:模型中使用的符号

图 7.3:模型中使用的符号

LSTM 的最核心组件是单元状态,以下简称为字母“C”。单元状态可以通过下图中方框上端的粗线表示。通常,我们可以将这条线视为一条传送带,贯穿不同的时间步,并传递一些信息。尽管有多个操作可以影响传播通过单元状态的值,但实际上,来自前一个单元状态的信息很容易到达下一个单元状态。

图 7.4:单元状态

图 7.4:单元状态

从修改单元状态的角度理解 LSTM 会非常有用。与 GRU 一样,LSTM 中允许修改单元状态的组件被称为“”。

LSTM 在多个步骤中操作,具体步骤将在接下来的章节中描述。

遗忘门

遗忘门负责确定应该从前一个时间步中忘记的单元状态内容。遗忘门的表达式如下:

图 7.5:遗忘门的表达式

图 7.5:遗忘门的表达式

在时间步 t,输入与一组新的权重 W_f 相乘,维度为(n_hn_x)。来自前一个时间步的激活值(h[t-1])与另一组新的权重 U_f 相乘,维度为(n_hn_h)。请注意,这些乘法是矩阵乘法。然后这两个项相加,并通过 Sigmoid 函数进行压缩,使输出 f[t] 的值保持在 [0,1] 范围内。输出的维度与单元状态向量 C 的维度相同(n_h1)。遗忘门为每个维度输出 '1' 或 '0'。值为 '1' 表示该维度的前一个单元状态的所有信息应通过并保留,而值为 '0' 表示该维度的前一个单元状态的所有信息应被忘记。图示如下:

图 7.6:遗忘门

图 7.6:遗忘门

那么,遗忘门的输出如何影响句子的构建呢?让我们来看一下生成的句子:

"Jack goes for a walk when his daughter goes to bed"。

句子中的第一个主语是 'Jack',表示男性性别。表示主语性别的单元状态值对应于 'Male'(这可以是 0 或 1)。直到句子中的 'his',主语没有改变,主语性别的单元状态继续保持 'male' 值。然而,接下来的单词 'daughter' 是新的主语,因此需要遗忘表示性别的旧值。注意,即使旧的性别状态是女性,仍然需要遗忘该值,以便使用与新主语对应的值。

遗忘门通过将主语性别值设置为 0 来完成“遗忘”操作(也就是说,f[t] 在该维度上输出 0)。

在 Python 中,可以使用以下代码片段计算遗忘门:

# Importing packages and setting the random seed to have a fixed output
import numpy as np
np.random.seed(0)
# A sigmoid needs to be defined to be used later
def sigmoid(x):
    return 1 / (1 + np.exp(-x))
# Simulating dummy values for the previous state and current input
h_prev = np.random.randn(3, 1)
x = np.random.randn(5, 1)

这段代码为 h_prevx 生成以下输出:

图 7.7:先前状态 'h_prev' 和当前输入 'x' 的输出

图 7.7:先前状态 'h_prev' 和当前输入 'x' 的输出

我们可以为 W_fU_f 初始化一些虚拟值:

# Initialize W_f and U_f with dummy values
W_f = np.random.randn(3, 5) # n_h = 3, n_x=5
U_f = np.random.randn(3, 3) # n_h = 3

这将生成以下值:

图 7.8:矩阵值的输出

图 7.8:矩阵值的输出

现在可以计算遗忘门:

f = sigmoid(np.matmul(W_f, x) + np.matmul(U_f, h_prev)

这会生成 f[t] 的以下值:

![图 7.9:遗忘门的输出,f[t]

](https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-nlp/img/C13783_07_09.jpg)

图 7.9:遗忘门的输出,f[t]

输入门和候选单元状态

在每个时间步,新的候选单元状态也通过以下表达式计算:

图 7.10:候选单元状态的表达式

图 7.10:候选单元状态的表达式

时间步 t 的输入与一组新的权重 W_c 相乘,维度为 (n_h, n_x)。来自前一时间步的激活值 (h[t-1]) 与另一组新的权重 U_c 相乘,维度为 (n_h, n_h)。注意,这些乘法是矩阵乘法。然后,这两个项相加并通过双曲正切函数进行压缩,输出 f[t] 在 [-1,1] 范围内。输出 C_candidate 的维度为 (n_h,1)。在随后的图中,候选单元状态由 C 波浪线表示:

图 7.11:输入门和候选状态

图 7.11:输入门和候选状态

候选值旨在计算它从当前时间步推断的单元状态。在我们的示例句子中,这对应于计算新的主语性别值。这个候选单元状态不会直接传递给下一个单元状态进行更新,而是通过输入门进行调节。

输入门决定了候选单元状态中的哪些值将传递到下一个单元状态。可以使用以下表达式来计算输入门的值:

图 7.12:输入门值的表达式

图 7.12:输入门值的表达式

时间步 t 的输入被乘以一组新的权重,W_i,其维度为 (n_h, n_x)。上一时间步的激活值 (h[t-1]) 被乘以另一组新的权重,U_i,其维度为 (n_h, n_h)。请注意,这些乘法是矩阵乘法。然后,这两个项相加并通过一个 sigmoid 函数来压缩输出,i[t],使其范围在 [0,1] 之间。输出的维度与单元状态向量 C 的维度相同 (n_h, 1)。在我们的示例句子中,在到达词语“daughter”后,需要更新单元状态中与主语性别相关的值。通过候选单元状态计算出新的主语性别候选值后,仅将与主语性别对应的维度设置为 1,输入门向量中的其他维度不变。

用于候选单元状态和输入门的 Python 代码片段如下:

# Initialize W_i and U_i with dummy values
W_i = np.random.randn(3, 5) # n_h = 3, n_x=5
U_i = np.random.randn(3, 3) # n_h = 3

这会为矩阵产生以下值:

图 7.13:候选单元状态和输入门的矩阵值截图

图 7.13:候选单元状态和输入门的矩阵值截图

输入门可以按如下方式计算:

i = sigmoid(np.matmul(W_i, x) + np.matmul(U_i, h_prev))

这会输出以下的i值:

图 7.14:输入门输出的截图

图 7.14:输入门输出的截图

为了计算候选单元状态,我们首先初始化W_cU_c矩阵:

# Initialize W_c and U_c with dummy values
W_c = np.random.randn(3, 5) # n_h = 3, n_x=5
U_c = np.random.randn(3, 3) # n_h = 3

这些矩阵所产生的值如下所示:

图 7.15:矩阵 W_c 和 U_c 值的截图

图 7.15:矩阵 W_c 和 U_c 值的截图

我们现在可以使用候选单元状态的更新方程:

c_candidate = np.tanh(np.matmul(W_c, x) + np.matmul(U_c, h_prev))

候选单元状态产生以下值:

图 7.16:候选单元状态的截图

图 7.16:候选单元状态的截图

单元状态更新

在这一点,我们已经知道应该忘记旧单元状态中的哪些内容(遗忘门),应该允许哪些内容影响新的单元状态(输入门),以及候选单元状态的变化应该是什么值(候选单元状态)。现在,可以按以下方式计算当前时间步的单元状态:

图 7.17:单元状态更新的表达式

图 7.17:单元状态更新的表达式

在前面的表达式中,'hadamard'表示逐元素相乘。因此,遗忘门与旧的单元状态逐元素相乘,允许它在我们的示例句子中忘记主语的性别。另一方面,输入门允许新的候选性别值影响新的单元状态。这两个项逐元素相加,使得当前单元状态现在具有与'female'对应的性别值。

下图展示了该操作

图 7.18:更新后的单元状态

图 7.18:更新后的单元状态

下面是生成当前单元状态的代码片段。

首先,为之前的单元状态初始化一个值:

# Initialize c_prev with dummy value
c_prev = np.random.randn(3,1)
c_new = np.multiply(f, c_prev) + np.multiply(i, c_candidate)

计算结果如下:

图 7.19:更新后的单元状态输出截图

图 7.19:更新后的单元状态输出截图

输出门和当前激活值

请注意,到目前为止,我们所做的只是更新单元状态。我们还需要为当前状态生成激活值;即(h[t])。这是通过输出门来实现的,输出门的计算方式如下:

图 7.20:输出门的表达式

图 7.20:输出门的表达式

时间步t的输入与一组新的权重W_o相乘,权重的维度为(n_hn_x)。上一时间步的激活值(h[t-1])与另一组新的权重U_o相乘,权重的维度为(n_hn_h)。请注意,这些乘法是矩阵乘法。然后,将这两个项相加,并通过 sigmoid 函数压缩输出值o[t],使其落在[0,1]范围内。输出的维度与单元状态向量h的维度相同(n_h1)。

输出门负责调节当前单元状态对时间步的激活值的影响程度。在我们的示例句子中,值得传播的信息是描述主语是单数还是复数,以便使用正确的动词形式。例如,如果“daughter”后面的单词是动词“goes”,那么使用正确的形式“go”就显得非常重要。因此,输出门允许相关信息传递给激活值,这个激活值随后作为输入传递到下一个时间步。在下图中,输出门表示为o_t

图 7.21:输出门和当前激活值

图 7.21:输出门和当前激活值

以下代码片段展示了如何计算输出门的值:

# Initialize dummy values for W_o and U_o
W_o = np.random.randn(3, 5) # n_h = 3, n_x=5
U_o = np.random.randn(3, 3) # n_h = 3

这将生成如下输出:

图 7.22:矩阵 W_o 和 U_o 输出的截图

图 7.22:矩阵 W_o 和 U_o 输出的截图

现在可以计算输出:

o = np.tanh(np.matmul(W_o, x) + np.matmul(U_o, h_prev))

输出门的值如下:

图 7.23:输出门值的截图

图 7.23:输出门值的截图

一旦输出门被评估,就可以计算下一个激活的值:

图 7.24:计算下一个激活值的表达式

图 7.24:计算下一个激活值的表达式

首先,应用一个双曲正切函数到当前单元状态。这将限制向量中的值在 -1 和 1 之间。然后,将此值与刚计算出的输出门值做逐元素乘积。

让我们来看一下计算当前时间步激活的代码片段:

h_new = np.multiply(o, np.tanh(c_new))

这最终会生成如下结果:

图 7.25:当前时间步激活的截图

图 7.25:当前时间步激活的截图

现在让我们构建一个非常简单的二元分类器来演示 LSTM 的使用。

练习 27:构建一个基于 LSTM 的模型来将电子邮件分类为垃圾邮件或非垃圾邮件(正常邮件)

在本练习中,我们将构建一个基于 LSTM 的模型,帮助我们将电子邮件分类为垃圾邮件或真实邮件:

  1. 我们将从导入所需的 Python 包开始:

    import pandas as pd
    import numpy as np
    from keras.models import Model, Sequential
    from keras.layers import LSTM, Dense,Embedding
    from keras.preprocessing.text import Tokenizer
    from keras.preprocessing import sequence
    

    注:

    LSTM 单元已经按照你为简单的 RNN 或 GRU 导入的方式导入。

  2. 我们现在可以读取包含文本列和另一列标签的输入文件,该标签指示文本是否为垃圾邮件。

    df = pd.read_csv("spam.csv", encoding="latin")
    df.head() 
    
  3. 数据看起来如下所示:图 7.26:垃圾邮件分类输出的截图

    图 7.26:垃圾邮件分类输出的截图
  4. 还有一些无关的列,但我们只需要包含文本数据和标签的列:

    df = df[["v1","v2"]]
    df.head()
    
  5. 输出应该如下所示:图 7.27:带有文本和标签的列的截图

    图 7.27:带有文本和标签的列的截图
  6. 我们可以检查标签分布:

    df["v1"].value_counts()
    

    标签分布看起来如下:

    图 7.28:标签分布的截图

    图 7.28:标签分布的截图
  7. 我们现在可以将标签分布映射为 0/1,以便它可以被馈送到分类器中。同时,还会创建一个数组来存储文本:

    lab_map = {"ham":0, "spam":1}
    Y = df["v1"].map(lab_map).values
    X = df["v2"].values
    
  8. 这将生成如下的输出 X 和 Y:图 7.29:输出 X 的截图

    图 7.29:输出 X 的截图

    图 7.30:输出 Y 的截图

    图 7.30:输出 Y 的截图
  9. 接下来,我们将限制为 100 个最常见单词生成的最大标记数。我们将初始化一个分词器,为文本语料库中使用的每个单词分配一个整数值:

    max_words = 100
    mytokenizer = Tokenizer(nb_words=max_words,lower=True, split=" ")
    mytokenizer.fit_on_texts(X)
    text_tokenized = mytokenizer.texts_to_sequences(X)
    
  10. 这将生成一个 text_tokenized 值:

    图 7.31:分词后值的输出截图

    请注意,由于我们限制了最大单词数为 100,因此只有文本中排名前 100 的最常见单词会被分配整数索引。其余的单词将被忽略。因此,即使 X 中的第一个序列有 20 个单词,在该句子的标记化表示中也只有 6 个索引。

  11. 接下来,我们将允许每个序列的最大长度为 50 个单词,并填充那些短于此长度的序列。而较长的序列则会被截断:

    max_len = 50
    sequences = sequence.pad_sequences(text_tokenized, maxlen=max_len)
    

    输出如下:

    图 7.32:填充序列的截图

    图 7.32:填充序列的截图

    请注意,填充是在“pre”模式下进行的,这意味着序列的初始部分会被填充,以使序列长度等于 max_len。

  12. 接下来,我们定义模型,LSTM 层具有 64 个隐藏单元,并将其拟合到我们的序列数据和相应的目标值:

    model = Sequential()
    model.add(Embedding(max_words, 20, input_length=max_len))
    model.add(LSTM(64))
    model.add(Dense(1, activation="sigmoid"))
    model.compile(loss='binary_crossentropy',
                  optimizer='adam',
                  metrics=['accuracy'])
    model.fit(sequences,Y,batch_size=128,epochs=10,
              validation_split=0.2)
    

    在这里,我们从一个嵌入层开始,确保输入到网络的固定大小(20)。我们有一个包含单个 sigmoid 输出的全连接层,该输出表示目标变量是 0 还是 1。然后,我们用二元交叉熵作为损失函数,并使用 Adam 作为优化策略来编译模型。之后,我们以批大小为 128 和 epoch 数为 10 的配置拟合模型。请注意,我们还保留了 20% 的训练数据作为验证数据。这开始了训练过程:

图 7.33:模型拟合到 10 个 epoch 的截图

图 7.33:模型拟合到 10 个 epoch 的截图

在 10 个 epoch 后,达到了 96% 的验证准确率。这是一个非常好的表现。

现在我们可以尝试一些测试序列并获得该序列是垃圾邮件的概率:

inp_test_seq = "WINNER! U win a 500 prize reward & free entry to FA cup final tickets! Text FA to 34212 to receive award"
test_sequences = mytokenizer.texts_to_sequences(np.array([inp_test_seq]))
test_sequences_matrix = sequence.pad_sequences(test_sequences,maxlen=max_len)
model.predict(test_sequences_matrix)

预期输出:

图 7.34:模型预测输出的截图

图 7.34:模型预测输出的截图

测试文本是垃圾邮件的概率非常高。

活动 9:使用简单 RNN 构建垃圾邮件或非垃圾邮件分类器

我们将使用与之前相同的超参数,基于简单的 RNN 构建一个垃圾邮件或非垃圾邮件分类器,并将其性能与基于 LSTM 的解决方案进行比较。对于像这样的简单数据集,简单的 RNN 性能非常接近 LSTM。然而,对于更复杂的模型,情况通常并非如此,正如我们将在下一节看到的那样。

注意

https://github.com/TrainingByPackt/Deep-Learning-for-Natural-Language-Processing/tree/master/Lesson 07/exercise 找到输入文件。

  1. 导入所需的 Python 包。

  2. 读取包含文本的列和包含文本标签的另一列的输入文件,该标签表示文本是否为垃圾邮件。

  3. 转换为序列。

  4. 填充序列。

  5. 训练序列。

  6. 构建模型。

  7. 对新测试数据进行邮件类别预测。

    预期输出:

图 7.35:邮件类别预测的输出

图 7.35:邮件类别预测输出

该活动的解决方案可以在第 324 页找到。

神经语言翻译

前一节描述的简单二分类器是自然语言处理(NLP)领域的基本用例,并不足以证明使用比简单 RNN 或更简单技术更复杂的技术的必要性。然而,确实存在许多复杂的用例,在这些用例中,必须使用更复杂的单元,例如 LSTM。神经语言翻译就是这样的一个应用。

神经语言翻译任务的目标是构建一个模型,能够将一段文本从源语言翻译成目标语言。在开始编写代码之前,让我们讨论一下该系统的架构。

神经语言翻译代表了一种多对多的自然语言处理(NLP)应用,这意味着系统有多个输入,并且系统也会产生多个输出。

此外,输入和输出的数量可能不同,因为相同的文本在源语言和目标语言中的单词数量可能不同。解决此类问题的 NLP 领域被称为序列到序列建模。该体系结构由编码器块和解码器块组成。以下图表示该体系结构:

图 7.36:神经翻译模型

图 7.36:神经翻译模型

体系结构的左侧是编码器块,右侧是解码器块。该图尝试将一个英文句子翻译成德文,如下所示:

英文:我想去游泳

德文:Ich möchte schwimmen gehen

为了演示的目的,前面的句子省略了句号。句号也被视为有效的标记。

编码器块在给定的时间步长内将英文(源语言)句子的每个单词作为输入。编码器块的每个单元是一个 LSTM。编码器块的唯一输出是最终的单元状态和激活值。它们合起来被称为思维向量。思维向量用于初始化解码器块的激活和单元状态,解码器块也是一个 LSTM 块。在训练阶段,在每个时间步长中,解码器的输出是句子中的下一个单词。它通过一个密集的 softmax 层表示,该层为下一个单词标记的值为 1,而对于向量中的其他所有条目,值为 0。

英语句子逐词输入到编码器中,生成最终的单元状态和激活。在训练阶段,解码器在每个时间步的真实输出是已知的,这实际上就是句子中的下一个德语单词。注意,句子开头插入了‘BEGIN_’标记,句子结尾插入了‘_END’标记。‘BEGIN_’标记的输出是德语句子的第一个单词。从最后一个图可以看到这一点。在训练过程中,网络被训练学习逐字翻译。

在推理阶段,英语输入句子被送入编码器模块,生成最终的单元状态和激活。解码器在第一个时间步输入‘BEGIN_’标记,以及单元状态和激活。利用这三个输入,会生成一个 softmax 输出。在一个训练良好的网络中,softmax 值对于正确单词的对应条目是最大的。然后将这个下一个单词作为输入送入下一个时间步。这个过程会一直继续,直到采样到‘_END’标记或达到最大句子长度。

现在让我们来逐步解析模型的代码。

我们首先读取包含句对的文件。为了演示目的,我们将句对数量限制为 20,000:

import os
import re
import numpy as np
with open("deu.txt", 'r', encoding='utf-8') as f:
    lines = f.read().split('\n')
num_samples = 20000 # Using only 20000 pairs for this example
lines_to_use = lines[: min(num_samples, len(lines) - 1)]
print(lines_to_use)

输出:

图 7.37:英德翻译句对的截图

图 7.37:英德翻译句对的截图

每一行首先是英语句子,后面跟着一个制表符,然后是该句子的德语翻译。接下来,我们将所有的数字映射到占位符词‘NUMBER_PRESENT’,并将‘BEGIN_’和‘_END’标记附加到每个德语句子中,正如之前讨论的那样:

for l in range(len(lines_to_use)):
    lines_to_use[l] = re.sub("\d", " NUMBER_PRESENT ",lines_to_use[l])
input_texts = []
target_texts = []
input_words = set()
target_words = set()
for line in lines_to_use:
    input_text, target_text = line.split('\t')
    target_text = 'BEGIN_ ' + target_text + ' _END'
    input_texts.append(input_text)
    target_texts.append(target_text)
    for word in input_text.split():
        if word not in input_words:
            input_words.add(word)
    for word in target_text.split():
        if word not in target_words:
            target_words.add(word)

在前面的代码片段中,我们获得了输入和输出文本。它们如下所示:

图 7.38:映射后的输入和输出文本截图

图 7.38:映射后的输入和输出文本截图

接下来,我们获取输入和输出序列的最大长度,并获得输入和输出语料库中的所有单词列表:

max_input_seq_length = max([len(i.split()) for i in input_texts])
max_target_seq_length = max([len(i.split()) for i in target_texts])
input_words = sorted(list(input_words))
target_words = sorted(list(target_words))
num_encoder_tokens = len(input_words)
num_decoder_tokens = len(target_words)

input_words 和 target_words 如下图所示:

图 7.39:输入文本和目标词汇的截图

图 7.39:输入文本和目标词汇的截图

接下来,我们为输入和输出单词的每个标记生成一个整数索引:

input_token_index = dict(
    [(word, i) for i, word in enumerate(input_words)])
target_token_index = dict([(word, i) for i, word in enumerate(target_words)])

这些变量的值如下所示:

图 7.40:每个标记的整数索引输出截图

图 7.40:每个标记的整数索引输出截图

我们现在定义编码器输入数据的数组,这是一个二维矩阵,行数等于句子对的数量,列数等于最大输入序列长度。同样,解码器输入数据也是一个二维矩阵,行数等于句子对的数量,列数等于目标语料库中最大序列长度。我们还需要目标输出数据,这是训练阶段所必需的。它是一个三维矩阵,第一维的大小与句子对的数量相同。第二维的大小与最大目标序列长度相同。第三维表示解码器令牌的数量(即目标语料库中不同单词的数量)。我们将这些变量初始化为零:

encoder_input_data = np.zeros(
    (len(input_texts), max_input_seq_length),
    dtype='float32')
decoder_input_data = np.zeros(
    (len(target_texts), max_target_seq_length),
    dtype='float32')
decoder_target_data = np.zeros(
    (len(target_texts), max_target_seq_length, num_decoder_tokens),
    dtype='float32')

现在我们填充这些矩阵:

for i, (input_text, target_text) in enumerate(zip(input_texts, target_texts)):
    for t, word in enumerate(input_text.split()):
        encoder_input_data[i, t] = input_token_index[word]
    for t, word in enumerate(target_text.split()):
        decoder_input_data[i, t] = target_token_index[word]
        if t > 0:
            # decoder_target_data is ahead of decoder_input_data by one timestep
            decoder_target_data[i, t - 1, target_token_index[word]] = 1.

这些值如下所示:

图 7.41:矩阵填充截图

](https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-nlp/img/C13783_07_41.jpg)

图 7.41:矩阵填充截图

我们现在定义一个模型。对于这个练习,我们将使用 Keras 的功能性 API:

from keras.layers import Input, LSTM, Embedding, Dense
from keras.models import Model
embedding_size = 50 # For embedding layer

让我们来看一下编码器模块:

encoder_inputs = Input(shape=(None,))
encoder_after_embedding =  Embedding(num_encoder_tokens, embedding_size)(encoder_inputs)
encoder_lstm = LSTM(50, return_state=True)
_, state_h, state_c = encoder_lstm(encoder_after_embedding)
encoder_states = [state_h, state_c]

首先,定义一个具有灵活输入数量的输入层(使用 None 属性)。然后,定义并应用一个嵌入层到编码器输入。接下来,定义一个具有 50 个隐藏单元的 LSTM 单元并应用于嵌入层。注意,LSTM 定义中的 return_state 参数设置为 True,因为我们希望获取最终的编码器状态,用于初始化解码器的细胞状态和激活值。然后将编码器 LSTM 应用于嵌入层,并将状态收集到变量中。

现在让我们定义解码器模块:

decoder_inputs = Input(shape=(None,))
decoder_after_embedding = Embedding(num_decoder_tokens, embedding_size)(decoder_inputs)
decoder_lstm = LSTM(50, return_sequences=True, return_state=True)
decoder_outputs, _, _ = decoder_lstm(decoder_after_embedding,
                                     initial_state=encoder_states)
decoder_dense = Dense(num_decoder_tokens, activation='softmax')
decoder_outputs = decoder_dense(decoder_outputs)

解码器接收输入,并以类似于编码器的方式定义嵌入层。然后定义一个 LSTM 模块,并将 return_sequences 和 return_state 参数设置为 True。这样做是因为我们希望使用序列和状态来进行解码。接着定义一个具有 softmax 激活函数的全连接层,输出数量等于目标语料库中不同令牌的数量。我们现在可以定义一个模型,它以编码器和解码器的输入为输入,生成解码器输出作为最终输出:

model = Model([encoder_inputs, decoder_inputs], decoder_outputs)
model.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['acc'])
model.summary()

以下是模型总结的显示:

图 7.42:模型总结截图

](https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-nlp/img/C13783_07_42.jpg)

图 7.42:模型总结截图

我们现在可以为输入和输出拟合模型:

model.fit([encoder_input_data, decoder_input_data], decoder_target_data,
          batch_size=128,
          epochs=20,
          validation_split=0.05)

我们设置了一个批次大小为 128,训练了 20 个 epochs:

图 7.43:模型拟合截图,训练了 20 个 epochs

图 7.43:模型拟合截图,训练了 20 个 epochs

模型现在已经训练完成。正如我们在神经语言翻译部分所描述的那样,推理阶段的架构与训练阶段使用的架构略有不同。我们首先定义编码器模型,它以编码器输入(包含嵌入层)作为输入,并生成编码器状态作为输出。这是有意义的,因为编码器模块的输出是细胞状态和激活值:

encoder_model = Model(encoder_inputs, encoder_states)

接下来,定义解码器推理模型:

decoder_state_input_h = Input(shape=(50,))
decoder_state_input_c = Input(shape=(50,))
decoder_states_inputs = [decoder_state_input_h, decoder_state_input_c]
decoder_outputs_inf, state_h_inf, state_c_inf = decoder_lstm(decoder_after_embedding, initial_state=decoder_states_inputs)

先前训练过的decoder_lstm的初始状态被设置为decoder_states_inputs变量,稍后将被设置为编码器的状态输出。然后,我们将解码器的输出通过密集的 softmax 层,以获取预测单词的索引,并定义解码器推理模型:

decoder_states_inf = [state_h_inf, state_c_inf]
decoder_outputs_inf = decoder_dense(decoder_outputs_inf)
# Multiple input, multiple output
decoder_model = Model(
    [decoder_inputs] + decoder_states_inputs,
    [decoder_outputs_inf] + decoder_states_inf)

解码器模型接收多个输入,形式包括带嵌入的decoder_input和解码器状态。输出也是一个多变量,其中包含密集层的输出和解码器状态返回的内容。这里需要状态,因为它们需要作为输入状态传递,以便在下一个时间步采样单词。

由于密集层的输出将返回一个向量,我们需要一个反向查找字典来将生成的单词的索引映射到实际单词:

# Reverse-lookup token index to decode sequences
reverse_input_word_index = dict(
    (i, word) for word, i in input_token_index.items())
reverse_target_word_index = dict(
    (i, word) for word, i in target_token_index.items())

字典中的值如下:

图 7.44:字典值的截图

](https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-nlp/img/C13783_07_44.jpg)

图 7.44:字典值的截图

我们现在需要开发一个采样逻辑。给定输入句子中每个单词的标记表示,我们首先使用这些单词标记作为编码器的输入,从encoder_model获取输出。我们还将解码器的第一个输入单词初始化为'BEGIN_'标记。然后,我们使用这些值来采样一个新的单词标记。下一个时间步的解码器输入就是这个新生成的标记。我们以这种方式继续,直到我们采样到'_END'标记或达到最大允许的输出序列长度。

第一步是将输入编码为状态向量:

def decode_sequence(input_seq):
states_value = encoder_model.predict(input_seq)

然后,我们生成一个长度为 1 的空目标序列:

    target_seq = np.zeros((1,1))

接下来,我们将目标序列的第一个字符填充为开始字符:

    target_seq[0, 0] = target_token_index['BEGIN_']

然后,我们为一批序列创建一个采样循环:

    stop_condition = False
    decoded_sentence = ''

    while not stop_condition:
        output_tokens, h, c = decoder_model.predict(
            [target_seq] + states_value)

接下来,我们采样一个标记:

        sampled_token_index = np.argmax(output_tokens)
        sampled_word = reverse_target_word_index[sampled_token_index]
        decoded_sentence += ' ' + sampled_word

然后,我们声明退出条件“either hit max length”(达到最大长度):

        # or find stop character.
        if (sampled_word == '_END' or
           len(decoded_sentence) > 60):
            stop_condition = True

        # Update the target sequence (of length 1).
        target_seq = np.zeros((1,1))
        target_seq[0, 0] = sampled_token_index

然后,我们更新状态:

        states_value = [h, c]

    return decoded_sentence

在这个实例中,您可以通过将用户定义的英语句子翻译为德语来测试模型:

text_to_translate = "Where is my car?"
encoder_input_to_translate = np.zeros(
    (1, max_input_seq_length),
    dtype='float32')
for t, word in enumerate(text_to_translate.split()):
    encoder_input_to_translate[0, t] = input_token_index[word]
decode_sequence(encoder_input_to_translate)

输出如图所示:

图 7.45:英语到德语翻译器的截图

图 7.45:英语到德语翻译器的截图

这是正确的翻译。

因此,即使是一个仅在 20,000 个序列上训练了 20 轮的模型,也能产生良好的翻译。在当前设置下,训练时长约为 90 分钟。

活动 10:创建法语到英语的翻译模型

在这个活动中,我们的目标是生成一个语言翻译模型,将法语文本转换为英语。

注意

您可以在 github.com/TrainingByPackt/Deep-Learning-for-Natural-Language-Processing/tree/master/Lesson%2007/activity 找到与此活动相关的文件。

  1. 读取句子对(请查看 GitHub 仓库中的文件)。

  2. 使用带有'BEGIN_'和'_END'单词的输出句子生成输入和输出文本。

  3. 将输入和输出文本转换为输入和输出序列矩阵。

  4. 定义编码器和解码器训练模型,并训练网络。

  5. 定义用于推理的编码器和解码器架构。

  6. 创建用户输入文本(法语:' Où est ma voiture?')。英文的示例输出文本应该是 'Where is my car?'。请参考 GitHub 仓库中的'French.txt'文件,获取一些示例法语单词。

    预期输出:

图 7.46:法语到英语翻译器模型的输出

图 7.46:法语到英语翻译器模型的输出

注意

该活动的解决方案可以在第 327 页找到。

总结

我们介绍了 LSTM 单元,作为解决梯度消失问题的一个可能方法。接着,我们详细讨论了 LSTM 架构,并使用它构建了一个简单的二元分类器。随后,我们深入研究了一个使用 LSTM 单元的神经机器翻译应用,并使用我们探讨的技术构建了一个法语到英语的翻译器模型。在下一章中,我们将讨论 NLP 领域的最新进展。

第九章:第八章

最先进的自然语言处理

学习目标

在本章结束时,你将能够:

  • 评估长句中的梯度消失问题

  • 描述作为最先进自然语言处理领域的注意力机制模型

  • 评估一种特定的注意力机制架构

  • 使用注意力机制开发神经机器翻译模型

  • 使用注意力机制开发文本摘要模型

本章旨在让你了解当前自然语言处理领域的实践和技术。

引言

在上一章中,我们学习了长短期记忆单元(LSTMs),它们有助于解决梯度消失问题。我们还详细研究了 GRU,它有自己处理梯度消失问题的方式。尽管与简单的循环神经网络相比,LSTM 和 GRU 减少了这个问题,但梯度消失问题在许多实际案例中仍然存在。问题本质上还是一样:长句子和复杂的结构依赖性使得深度学习算法难以进行封装。因此,当前最普遍的研究领域之一就是社区尝试减轻梯度消失问题的影响。

在过去几年中,注意力机制尝试为梯度消失问题提供解决方案。注意力机制的基本概念依赖于在得到输出时能够访问输入句子的所有部分。这使得模型能够对句子的不同部分赋予不同的权重(注意力),从而推断出依赖关系。由于其在学习这种依赖关系方面的非凡能力,基于注意力机制的架构代表了自然语言处理领域的最先进技术。

在本章中,我们将学习注意力机制,并使用基于注意力机制的特定架构解决神经机器翻译任务。我们还将提及一些当前业界正在使用的其他相关架构。

注意力机制

在上一章中,我们解决了一个神经语言翻译任务。我们采用的翻译模型架构由两部分组成:编码器和解码器。请参阅以下图示了解架构:

图 8.1:神经语言翻译模型

图 8.1:神经语言翻译模型

对于神经机器翻译任务,句子逐字输入到编码器中,生成一个单一的思想向量(在前面的图中表示为'S'),它将整个句子的意义嵌入到一个单一的表示中。解码器随后使用该向量初始化隐藏状态,并逐字生成翻译。

在简单的编码器-解码器模式下,只有一个向量(思维向量)包含整个句子的表示。句子越长,单个思维向量保持长期依赖关系的难度越大。使用 LSTM 单元只能在某种程度上减轻这个问题。为了进一步缓解梯度消失问题,提出了一个新概念,这个概念就是 注意力机制

注意力机制旨在模仿人类学习依赖关系的方式。我们用以下示例句子来说明这一点:

"最近我们社区发生了许多盗窃事件,这迫使我考虑雇佣一家安保公司在我家安装防盗系统,以便我能保护自己和家人安全。"

请注意单词“my”,“I”,“me”,“myself”和“our”的使用。它们出现在句子中的不同位置,但彼此紧密相连,共同表达句子的含义。

在尝试翻译前述句子时,传统的编码器-解码器功能如下:

  1. 将句子逐词传递给编码器。

  2. 编码器生成一个单一的思维向量,表示整个句子的编码。对于像前面的长句子,即使使用 LSTM,也很难让编码器嵌入所有依赖关系。因此,句子的前半部分编码不如后半部分强,这意味着后半部分对编码的影响占主导地位。

  3. 解码器使用思维向量来初始化隐藏状态向量,以生成输出翻译。

更直观的翻译方法是,在确定目标语言中特定单词时,注意输入句子中单词的正确位置。举个例子,考虑以下句子:

'那只动物无法走在街上,因为它受伤严重。'

在这个句子中,'it' 这个词指的是谁?是指动物还是街道?如果将整个句子一起考虑,并对句子的不同部分赋予不同的权重,就能够回答这个问题。注意力机制就完成了这一点,如下所示:

图 8.2:注意力机制的示例

图 8.2:注意力机制的示例

图表显示了在理解句子中每个单词时,每个单词所获得的权重。如图所示,单词 'it_' 从 'animal_' 获得了非常强的权重,而从 'street_' 获得了相对较弱的权重。因此,模型现在可以回答“it”指代句子中的哪个实体的问题。

对于翻译的编码器-解码器模型,在生成逐词输出时,在某个特定的时刻,并不是输入句子中的所有单词对输出词的确定都很重要。注意力机制实现了一个正是做这件事的方案:在每个输出词的确定时,对输入句子的不同部分进行加权,考虑到所有的输入词。一个经过良好训练的带有注意力机制的网络,会学会对句子的不同部分施加适当的加权。这个机制使得输入句子的全部内容在每次确定输出时都能随时使用。因此,解码器不再仅仅依赖一个思维向量,而是可以访问到每个输出词对应的“思维”向量。这种能力与传统的 LSTM/GRU/RNN 基础的编码器-解码器模型形成鲜明对比。

注意力机制是一个通用概念。它可以通过几种不同的架构实现,这些架构将在本章后面部分讨论。

注意力机制模型

让我们看看带有注意力机制的编码器-解码器架构可能是怎样的:

图 8.3:注意力机制模型

图 8.3:注意力机制模型

前面的图展示了带有注意力机制的语言翻译模型的训练阶段。与基本的编码器-解码器机制相比,我们可以注意到几个不同之处,如下所示:

  • 解码器的初始状态会被初始化为最后一个编码器单元的编码器输出状态。使用一个初始的NULL词开始翻译,生成的第一个词是‘Er’。这与之前的编码器-解码器模型相同。

  • 对于第二个词,除了来自前一个词的输入和前一个解码器时间步的隐藏状态外,还会有另一个向量作为输入传递给单元。这个向量通常被认为是‘上下文向量’,它是所有编码器隐藏状态的一个函数。从前面的图中来看,它是编码器在所有时间步上的隐藏状态的加权求和。

  • 在训练阶段,由于每个解码器时间步的输出是已知的,我们可以学习网络的所有参数。除了与所使用的 RNN 类型相关的常规参数外,还会学习与注意力函数相关的特定参数。如果注意力函数只是对隐藏状态编码器向量的简单求和,则可以学习每个编码器时间步的隐藏状态的权重。

  • 在推理阶段,在每个时间步,解码器单元可以将上一时间步的预测词、前一个解码器单元的隐藏状态以及上下文向量作为输入。

让我们看一下神经机器翻译中注意力机制的一个具体实现。在上一章中,我们构建了一个神经语言翻译模型,这是一个更广泛的自然语言处理(NLP)领域中的子问题,叫做神经机器翻译。在接下来的部分中,我们将尝试解决一个日期规范化问题。

使用注意力机制的数据规范化

假设你正在维护一个数据库,其中有一张表格包含了一个日期列。日期的输入由你的客户提供,他们填写表单并在日期字段中输入日期。前端工程师不小心忘记了对该字段进行验证,使得只有符合“YYYY-MM-DD”格式的日期才会被接受。现在,你的任务是规范化数据库表中的日期列,将用户以多种格式输入的日期转换为标准的“YYYY-MM-DD”格式。

作为示例,用户输入的日期及其对应的正确规范化如下所示:

图 8.4:日期规范化表格

图 8.4:日期规范化表格

你可以看到,用户输入日期的方式有很大的变化。除了表格中的示例外,还有许多其他方式可以指定日期。

这个问题非常适合通过神经机器翻译模型来解决,因为输入具有顺序结构,其中输入的不同组成部分的意义需要被学习。该模型将包含以下组件:

  • 编码器

  • 解码器

  • 注意力机制

编码器

这是一个双向 LSTM,它将日期的每个字符作为输入。因此,在每个时间步,编码器的输入是日期输入的单个字符。除此之外,隐藏状态和记忆状态也作为输入从上一个编码器单元传递过来。由于这是一个双向结构,因此与 LSTM 相关的有两组参数:一组用于正向,另一组用于反向。

解码器

这是一个单向 LSTM。它的输入是当前时间步的上下文向量。由于在日期规范化的情况下,每个输出字符不严格依赖于上一个输出字符,因此我们不需要将前一个时间步的输出作为当前时间步的输入。此外,由于它是一个 LSTM 单元,上一时间步解码器的隐藏状态和记忆状态也会作为输入传递给当前时间步单元,用于确定当前时间步的解码器输出。

注意力机制

本节将解释注意力机制。在给定时间步确定解码器输入时,计算一个上下文向量。上下文向量是所有时间步的编码器隐藏状态的加权总和。其计算方式如下:

图 8.5:上下文向量的表达式

图 8.5:上下文向量的表达式

点积操作是一个点积运算,它将权重(由alpha表示)与每个时间步的相应隐藏状态向量相乘并求和。alpha 向量的值是为每个解码器输出时间步单独计算的。alpha 值 encapsulates 了注意力机制的本质,即确定在当前时间步的输出中,应该给予输入的哪一部分“关注”。这可以通过一个图示来实现,如下所示:

图 8.6:输入的注意力权重的确定

图 8.6:输入的注意力权重的确定

举个例子,假设编码器输入有一个固定长度 30 个字符,而解码器输出有一个固定的输出长度 10 个字符。对于日期规范化问题,这意味着用户输入的最大长度固定为 30 个字符,而模型输出的长度固定为 10 个字符(即 YYYY-MM-DD 格式的字符数,包括中划线)。

假设我们希望确定在输出时间步=4 时的解码器输出(这是为了说明概念而选择的一个任意数字;它只需要小于等于 10,即输出时间步的数量)。在这一步骤中,会计算出权重向量 alpha。这个向量的维度等于编码器输入的时间步数(因为需要为每个编码器输入时间步计算一个权重)。因此,在我们的例子中,alpha 的维度为 30。

现在,我们已经得到了来自每个编码器时间步的隐藏状态向量,因此一共有 30 个隐藏状态向量可用。隐藏状态向量的维度同时考虑了双向编码器 LSTM 的前向和反向组件。对于给定的时间步,我们将前向隐藏状态和反向隐藏状态合并成一个单一的向量。因此,如果前向和反向隐藏状态的维度都是 32,我们将它们合并成一个 64 维的向量,如[h_forward, h_backward]。这只是一个简单的拼接函数。我们称之为编码器隐藏状态向量。

现在我们有一个单一的 30 维权重向量 alpha,以及 30 个 64 维的隐藏状态向量。因此,我们可以将这 30 个隐藏状态向量分别与 alpha 向量中的对应项相乘。此外,我们还可以将这些加权后的隐藏状态表示求和,得到一个单一的 64 维上下文向量。这本质上是点积操作所执行的运算。

Alpha 的计算

权重可以通过多层感知器(MLP)进行建模,它是一个由多个隐藏层组成的简单神经网络。我们选择使用两个密集层和 softmax 输出。密集层和单元的数量可以作为超参数处理。该 MLP 的输入由两个部分组成:这些部分是编码器双向 LSTM 所有时间步的隐藏状态向量,如上一步所解释的,以及来自解码器前一时间步的隐藏状态。这些向量被连接形成一个单一向量。因此,MLP 的输入是:[编码器隐藏状态向量来自解码器的前一状态向量]。这是一种张量的连接操作:[HS_prev]。S_prev 指的是解码器从前一时间步输出的隐藏状态。如果 S_prev 的维度是 64(表示解码器 LSTM 的隐藏状态维度为 64),且编码器的隐藏状态向量的维度也是 64(如上一点所述),那么这两个向量的连接将生成一个维度为 128 的向量。

因此,MLP 接收来自单个编码器时间步的 128 维输入。由于我们已将编码器的输入长度固定为 30 个字符,我们将得到一个大小为 [30,128] 的矩阵(更准确地说,是一个张量)。该 MLP 的参数使用与学习模型其他参数相同的反向传播通过时间(BPTT)机制来学习。因此,整个模型(编码器 + 解码器 + 注意力函数 MLP)的所有参数是一起学习的。可以通过以下图示查看:

图 8.7:Alpha 的计算

图 8.7:Alpha 的计算

在前一步中,我们学习了用于确定解码器输出的权重(alpha 向量)(我们假设这一时间步是 4)。因此,单步解码器输出的确定需要输入:S_prev 和编码器隐藏状态,用于计算上下文向量、解码器隐藏状态以及解码器前一时间步的记忆,这些将作为输入传递给解码器单向 LSTM。进入下一个解码器时间步时,需要计算一个新的 alpha 向量,因为对于这个下一步,输入序列的不同部分与上一个时间步相比,可能会被赋予不同的权重。

由于模型的架构,训练和推理步骤是相同的。唯一的区别是,在训练过程中,我们知道每个解码器时间步的输出,并利用这些输出来训练模型参数(这种技术称为“教师强制”)。

相比之下,在推理阶段,我们预测输出字符。请注意,在训练和推理期间,我们都不会将上一个时间步的解码器输出字符作为输入传递给当前时间步的解码器单元。需要注意的是,这里提出的架构是针对这个问题特定的。实际上有很多架构和定义注意力函数的方法。我们将在本章的后续部分简要了解其中一些。

练习 28: 为数据库列构建日期规范化模型

一个数据库列接受来自多个用户的日期输入,格式多样。在本练习中,我们旨在规范化数据库表的日期列,使得用户以不同格式输入的日期能够转换为标准的“YYYY-MM-DD”格式:

注意

运行代码所需的 Python 依赖项如下:

Babel==2.6.0

Faker==1.0.2

Keras==2.2.4

numpy==1.16.1

pandas==0.24.1

scipy==1.2.1

tensorflow==1.12.0

tqdm==4.31.1

Faker==1.0.2

  1. 我们导入所有必要的模块:

    from keras.layers import Bidirectional, Concatenate, Permute, Dot, Input, LSTM, Multiply
    from keras.layers import RepeatVector, Dense, Activation, Lambda
    from keras.optimizers import Adam
    from keras.utils import to_categorical
    from keras.models import load_model, Model
    import keras.backend as K
    import numpy as np
    from babel.dates import format_date
    from faker import Faker
    import random
    from tqdm import tqdm
    
  2. 接下来,我们定义一些辅助函数。我们首先使用'faker'babel模块生成用于训练的数据。babel中的format_date函数生成特定格式的日期(使用FORMATS)。此外,日期也以人类可读的格式返回,模拟我们希望规范化的非正式用户输入日期:

    fake = Faker()
    fake.seed(12345)
    random.seed(12345)
    
  3. 定义我们希望生成的数据格式:

    FORMATS = ['short',
               'medium',
               'long',
               'full',
               'full',
               'full',
               'full',
               'full',
               'full',
               'full',
               'full',
               'full',
               'full',
               'd MMM YYY',
               'd MMMM YYY',
               'dd MMM YYY',
               'd MMM, YYY',
               'd MMMM, YYY',
               'dd, MMM YYY',
               'd MM YY',
               'd MMMM YYY',
               'MMMM d YYY',
               'MMMM d, YYY',
               'dd.MM.YY']
    # change this if you want it to work with another language
    LOCALES = ['en_US']
    def load_date():
        """
            Loads some fake dates
            :returns: tuple containing human readable string, machine readable string, and date object
        """
        dt = fake.date_object()
        human_readable = format_date(dt, format=random.choice(FORMATS),  locale='en_US') # locale=random.choice(LOCALES))
            human_readable = human_readable.lower()
            human_readable = human_readable.replace(',','')
            machine_readable = dt.isoformat()
        return human_readable, machine_readable, dt
    
  4. 接下来,我们生成并编写一个函数来加载数据集。在此函数中,使用之前定义的load_date()函数创建示例。除了数据集外,函数还返回用于映射人类可读和机器可读标记的字典,以及逆向机器词汇表:

    def load_dataset(m):
        """
            Loads a dataset with m examples and vocabularies
            :m: the number of examples to generate
        """
        human_vocab = set()
        machine_vocab = set()
        dataset = []
        Tx = 30
        for i in tqdm(range(m)):
            h, m, _ = load_date()
            if h is not None:
                dataset.append((h, m))
                human_vocab.update(tuple(h))
                machine_vocab.update(tuple(m))
        human = dict(zip(sorted(human_vocab) + ['<unk>', '<pad>'],
                         list(range(len(human_vocab) + 2))))
        inv_machine = dict(enumerate(sorted(machine_vocab)))
        machine = {v:k for k,v in inv_machine.items()}
        return dataset, human, machine, inv_machine
    

    上述辅助函数用于生成一个数据集,使用babel Python 包。此外,它还返回输入和输出的词汇字典,就像我们在以前的练习中所做的那样。

  5. 接下来,我们使用这些辅助函数生成一个包含 10,000 个样本的数据集:

    m = 10000
    dataset, human_vocab, machine_vocab, inv_machine_vocab = load_dataset(m)
    

    变量存储值,如下所示:

    图 8.8: 显示变量值的截图

    图 8.8: 显示变量值的截图

    human_vocab是一个字典,将输入字符映射到整数。以下是human_vocab的值映射:

    图 8.9: 显示 human_vocab 字典的截图

    图 8.9: 显示 human_vocab 字典的截图

    machine_vocab字典包含输出字符到整数的映射。

    图 8.10: 显示 machine_vocab 字典的截图

    图 8.10: 显示 machine_vocab 字典的截图

    inv_machine_vocabmachine_vocab的逆映射,用于将预测的整数映射回字符:

    图 8.11: 显示 inv_machine_vocab 字典的截图

    图 8.11: 显示 inv_machine_vocab 字典的截图
  6. 接下来,我们预处理数据,使得输入序列的形状为(1000030len(human_vocab))。因此,矩阵中的每一行代表 30 个时间步和对应于给定时间步的字符的独热编码向量。同样,Y 输出的形状为(1000010len(machine_vocab))。这对应于 10 个输出时间步和相应的独热编码输出向量。我们首先定义一个名为'string_to_int'的函数,它接收一个用户日期作为输入,并返回一个可以输入到模型中的整数序列:

    def string_to_int(string, length, vocab):
        """
        Converts all strings in the vocabulary into a list of integers representing the positions of the
        input string's characters in the "vocab"
        Arguments:
        string -- input string, e.g. 'Wed 10 Jul 2007'
        length -- the number of timesteps you'd like, determines if the output will be padded or cut
        vocab -- vocabulary, dictionary used to index every character of your "string"
        Returns:
        rep -- list of integers (or '<unk>') (size = length) representing the position of the string's character in the vocabulary
        """
    
  7. 将大小写转换为小写,以标准化文本

        string = string.lower()
        string = string.replace(',','')
        if len(string) > length:
            string = string[:length]
        rep = list(map(lambda x: vocab.get(x, '<unk>'), string))
        if len(string) < length:
            rep += [vocab['<pad>']] * (length - len(string))
        return rep
    
  8. 现在我们可以利用这个辅助函数来生成输入和输出的整数序列,正如之前所解释的那样:

    def preprocess_data(dataset, human_vocab, machine_vocab, Tx, Ty):
        X, Y = zip(*dataset)
        print("X shape before preprocess: {}".format(X))
        X = np.array([string_to_int(i, Tx, human_vocab) for i in X])
        Y = [string_to_int(t, Ty, machine_vocab) for t in Y]
        print("X shape from preprocess: {}".format(X.shape))
        print("Y shape from preprocess: {}".format(Y))
        Xoh = np.array(list(map(lambda x: to_categorical(x, num_classes=len(human_vocab)), X)))
        Yoh = np.array(list(map(lambda x: to_categorical(x, num_classes=len(machine_vocab)), Y)))
        return X, np.array(Y), Xoh, Yoh
    Tx = 30
    Ty = 10
    X, Y, Xoh, Yoh = preprocess_data(dataset, human_vocab, machine_vocab, Tx, Ty)
    
  9. 打印矩阵的形状。

    print("X.shape:", X.shape)
    print("Y.shape:", Y.shape)
    print("Xoh.shape:", Xoh.shape)
    print("Yoh.shape:", Yoh.shape)
    

    这一阶段的输出如下所示:

    图 8.12:矩阵形状的截图
  10. 我们可以进一步检查XYXohYoh向量的形状:

    index = 0
    print("Source date:", dataset[index][0])
    print("Target date:", dataset[index][1])
    print()
    print("Source after preprocessing (indices):", X[index].shape)
    print("Target after preprocessing (indices):", Y[index].shape)
    print()
    print("Source after preprocessing (one-hot):", Xoh[index].shape)
    print("Target after preprocessing (one-hot):", Yoh[index].shape)
    

    输出应如下所示:

    图 8.13:处理后矩阵形状的截图
  11. 我们现在开始定义一些构建模型所需的函数。首先,我们定义一个计算 softmax 值的函数,给定张量作为输入:

    def softmax(x, axis=1):
        """Softmax activation function.
        # Arguments
            x : Tensor.
            axis: Integer, axis along which the softmax normalization is applied.
        # Returns
            Tensor, output of softmax transformation.
        # Raises
            ValueError: In case 'dim(x) == 1'.
        """
        ndim = K.ndim(x)
        if ndim == 2:
            return K.softmax(x)
        elif ndim > 2:
            e = K.exp(x - K.max(x, axis=axis, keepdims=True))
            s = K.sum(e, axis=axis, keepdims=True)
            return e / s
        else:
            raise ValueError('Cannot apply softmax to a tensor that is 1D')
    
  12. 接下来,我们可以开始组装模型:

    # Defined shared layers as global variables
    repeator = RepeatVector(Tx)
    concatenator = Concatenate(axis=-1)
    densor1 = Dense(10, activation = "tanh")
    densor2 = Dense(1, activation = "relu")
    activator = Activation(softmax, name='attention_weights')
    dotor = Dot(axes = 1)
    
  13. RepeatVector的作用是重复给定的张量多次。在我们的例子中,这是重复Tx次,也就是 30 个输入时间步。重复器用于将S_prev重复 30 次。回想一下,为了计算上下文向量以确定单个时间步的解码器输出,需要将S_prev与每个输入编码器时间步进行连接。Concatenatekeras函数完成了下一步,即将重复的S_prev和每个时间步的编码器隐藏状态向量连接在一起。我们还定义了 MLP 层,它包括两个全连接层(densor1densor2)。接下来,MLP 的输出通过一个softmax层。这个softmax分布就是一个 alpha 向量,其中每个条目对应于每个连接向量的权重。最后,定义了一个dotor函数,它负责计算上下文向量。整个流程对应于一个步骤的注意力机制(因为它是针对单个解码器输出时间步的):

    def one_step_attention(h, s_prev):
        """
        Performs one step of attention: Outputs a context vector computed as a dot product of the attention weights
        "alphas" and the hidden states "h" of the Bi-LSTM.
    
        Arguments:
        h -- hidden state output of the Bi-LSTM, numpy-array of shape (m, Tx, 2*n_h)
        s_prev -- previous hidden state of the (post-attention) LSTM, numpy-array of shape (m, n_s)
    
        Returns:
        context -- context vector, input of the next (post-attetion) LSTM cell
        """
    
  14. 使用repeators_prev重复至形状(mTxn_s),以便与所有隐藏状态'h'进行连接:

        s_prev = repeator(s_prev)
    
  15. 使用concatenator在最后一个轴上将as_prev连接起来:

        concat = concatenator([h, s_prev])
    
  16. 使用densor1通过一个小型全连接神经网络传播concat,以计算中间能量变量e

        e = densor1(concat)
    
  17. 使用densor2通过一个小型全连接神经网络传播e,以计算能量变量:

        energies = densor2(e)
    
  18. 使用activator作用于energies,以计算注意力权重alphas

        alphas = activator(energies)
    
  19. 使用dotor结合alphasa来计算上下文向量,以供下一个(注意力后)LSTM 单元使用:

        context = dotor([alphas, h])
    
        return context
    
  20. 到目前为止,我们仍然没有定义编码器和解码器 LSTM 的隐藏状态单元数量。我们还需要定义解码器 LSTM,这是一个单向 LSTM:

    n_h = 32
    n_s = 64
    post_activation_LSTM_cell = LSTM(n_s, return_state = True)
    output_layer = Dense(len(machine_vocab), activation=softmax)
    
  21. 现在我们定义编码器和解码器模型:

    def model(Tx, Ty, n_h, n_s, human_vocab_size, machine_vocab_size):
        """
        Arguments:
        Tx -- length of the input sequence
        Ty -- length of the output sequence
        n_h -- hidden state size of the Bi-LSTM
        n_s -- hidden state size of the post-attention LSTM
        human_vocab_size -- size of the python dictionary "human_vocab"
        machine_vocab_size -- size of the python dictionary "machine_vocab"
        Returns:
        model -- Keras model instance
        """
    
  22. 定义模型的输入形状(Tx,)。定义s0c0,以及解码器 LSTM 的初始隐藏状态,形状为(n_s,):

        X = Input(shape=(Tx, human_vocab_size), name="input_first")
        s0 = Input(shape=(n_s,), name='s0')
        c0 = Input(shape=(n_s,), name='c0')
        s = s0
        c = c0
    
  23. 初始化一个空的outputs列表:

        outputs = []
    
  24. 定义你的预注意力 Bi-LSTM。记得使用return_sequences=True

        h = Bidirectional(LSTM(n_h, return_sequences=True))(X)
    
  25. 迭代Ty步:

        for t in range(Ty):
    
  26. 执行一次注意力机制步骤,以获得步骤t的上下文向量:

            context = one_step_attention(h, s)
    
  27. 将后注意力 LSTM 单元应用于context向量。同时,传递initial_state = [隐藏状态,细胞状态]

            s, _, c = post_activation_LSTM_cell(context, initial_state = [s,c])
    
  28. Dense层应用于后注意力 LSTM 的隐藏状态输出:

            out = output_layer(s)
    
            # Append "out" to the "outputs" list
            outputs.append(out)
    
    
  29. 通过接收三个输入并返回输出列表来创建模型实例:

        model = Model(inputs=[X, s0, c0], outputs=outputs)
    
        return model
    model = model(Tx, Ty, n_h, n_s, len(human_vocab), len(machine_vocab))
    model.summary()
    

    输出可能如下面的图所示:

    图 8.14:模型总结的截图

    图 8.14:模型总结的截图
  30. 现在我们将使用categorical_crossentropy作为损失函数,Adam优化器作为优化策略来编译模型:

    opt = Adam(lr = 0.005, beta_1=0.9, beta_2=0.999, decay = 0.01)
    model.compile(loss='categorical_crossentropy', optimizer=opt, metrics=['accuracy'])
    
  31. 在拟合模型之前,我们需要初始化解码器 LSTM 的隐藏状态向量和记忆状态:

    s0 = np.zeros((m, n_s))
    c0 = np.zeros((m, n_s))
    outputs = list(Yoh.swapaxes(0,1))
    model.fit([Xoh, s0, c0], outputs, epochs=1, batch_size=100)
    

    这开始了训练:

    图 8.15:训练周期的截图

    图 8.15:训练周期的截图
  32. 模型现在已训练完成,可以进行推理调用:

    EXAMPLES = ['3 May 1979', '5 April 09', '21th of August 2016', 'Tue 10 Jul 2007', 'Saturday May 9 2018', 'March 3 2001', 'March 3rd 2001', '1 March 2001']
    for example in EXAMPLES:
    
        source = string_to_int(example, Tx, human_vocab)
        source = np.array(list(map(lambda x: to_categorical(x, num_classes=len(human_vocab)), source)))#.swapaxes(0,1)
        source = source[np.newaxis, :]
        prediction = model.predict([source, s0, c0])
        prediction = np.argmax(prediction, axis = -1)
        output = [inv_machine_vocab[int(i)] for i in prediction]
    
        print("source:", example)
        print("output:", ''.join(output))
    

    预期输出:

图 8.16:标准化日期输出的截图

图 8.16:标准化日期输出的截图

其他架构与发展

上一节描述的注意力机制架构只是构建注意力机制的一种方式。近年来,提出了几种其他架构,它们在深度学习 NLP 领域构成了最先进技术。在这一节中,我们将简要提及其中一些架构。

变换器

2017 年末,谷歌在其开创性论文《Attention is all you need》中提出了一种注意力机制架构。该架构被认为是自然语言处理(NLP)社区中的最先进技术。变换器架构利用一种特殊的多头注意力机制,在不同的层次生成注意力。此外,它还采用残差连接,进一步确保梯度消失问题对学习的影响最小。变换器的特殊架构还允许在训练阶段大幅加速,同时提供更高质量的结果。

最常用的带有变换器架构的包是tensor2tensor。Keras 的变换器代码通常很笨重且难以维护,而tensor2tensor允许使用 Python 包和简单的命令行工具来训练变换器模型。

注意

欲了解更多关于 tensor2tensor 的信息,请参考 github.com/tensorflow/tensor2tensor/#t2t-overview

有兴趣了解架构的读者可以阅读提到的论文以及相关的 Google 博客:Transformer: A Novel Neural Network

BERT

2018 年底,Google 再次开源了一个突破性的架构,名为BERTBidirectional Encoder Representations from Transformers)。深度学习社区在自然语言处理(NLP)领域已经很久没有看到适合的转移学习机制了。转移学习方法在深度学习中一直是图像相关任务(如图像分类)的最前沿技术。图像在基本结构上是通用的,无论地理位置如何,图像的结构都是一致的。这使得可以在通用图像上训练深度学习模型。这些预训练的模型可以进一步微调以应对特定任务。这节省了训练时间,也减少了对大量数据的需求,能够在较短的时间内达到令人满意的模型表现。

不幸的是,语言因地理位置的不同而差异很大,且往往没有共同的基本结构。因此,转移学习在自然语言处理(NLP)任务中并不是一个可行的选项。BERT 通过其新的注意力机制架构,基于基础的 Transformer 架构,使这一切成为可能。

注意

关于 BERT 的更多信息,请参考:BERT GitHub

有兴趣了解 BERT 的读者应该查看 Google 关于 BERT 的博客:Open Sourcing BERT

OpenAI GPT-2

OpenAI 还开源了一个名为GPT-2的架构,它在他们之前的架构 GPT 基础上进行了改进。GPT-2 架构的核心特点是它在文本生成任务中表现出色。GPT-2 模型同样基于 Transformer 架构,包含约 15 亿个参数。

注意

有兴趣了解更多的读者可以参考 OpenAI 的博客:Better Language Models

活动 11:构建文本摘要模型

我们将使用我们为神经机器翻译构建的注意力机制模型架构来构建文本摘要模型。文本摘要的目标是编写给定大规模文本语料的摘要。你可以想象使用文本摘要工具来总结书籍内容或为新闻文章生成标题。

作为一个示例,使用给定的输入文本:

“梅赛德斯-奔驰印度在庆祝其 25 周年之际,将通过发布全新 V-Class 重新定义印度汽车行业的豪华空间。V-Class 搭载 2.1 升 BS VI 柴油发动机,产生 120kW 的功率和 380Nm 的扭矩,0-100km/h 加速仅需 10.9 秒。它配备了 LED 前大灯、多功能方向盘和 17 英寸铝合金轮毂。”

一个好的文本摘要模型应该能够生成有意义的摘要,例如:

“梅赛德斯-奔驰印度发布全新 V-Class”

从架构的角度来看,文本摘要模型与翻译模型完全相同。模型的输入是文本,它会按字符(或按词)逐个输入到编码器中,而解码器则输出与源文本相同语言的字符。

注意

输入文本可以在此链接找到:github.com/TrainingByPackt/Deep-Learning-for-Natural-Language-Processing/tree/master/Lesson%2008

以下步骤将帮助你解决问题:

  1. 导入所需的 Python 包,并制作人类与机器的词汇字典。

  2. 定义输入和输出字符的长度以及模型功能(RepeatorConcatenateDensorsDotor)。

  3. 定义一个一步注意力函数,并为解码器和编码器定义隐状态的数量。

  4. 定义模型架构并运行它以获得模型。

  5. 定义模型的损失函数和其他超参数。同时,初始化解码器的状态向量。

  6. 将模型拟合到我们的数据上。

  7. 运行新文本的推理步骤。

    预期输出:

图 8.17:文本摘要输出

图 8.17:文本摘要输出

注意

活动的解决方案可以在第 333 页找到。

总结

在本章中,我们学习了注意力机制的概念。基于注意力机制,已经提出了几种架构,这些架构构成了 NLP 领域的最新技术。我们学习了一种特定的模型架构,用于执行神经机器翻译任务。我们还简要提到了其他先进的架构,如 Transformer 和 BERT。

到目前为止,我们已经看到了许多不同的自然语言处理(NLP)模型。在下一章,我们将讨论一个实际 NLP 项目在组织中的流程以及相关技术。

第十章:第九章

组织中的实用 NLP 项目工作流

学习目标

本章结束时,你将能够:

  • 确定自然语言处理项目的需求

  • 了解组织中不同团队的参与方式

  • 使用 Google Colab 笔记本,通过 GPU 训练深度学习模型

  • 在 AWS 上部署一个模型,作为软件即服务(SaaS)使用

  • 熟悉一个简单的技术栈用于部署

本章我们将关注一个实时 NLP 项目及其在组织中的流程,一直到最终阶段,贯穿整章。

引言

到目前为止,我们已经学习了几种深度学习技术,这些技术可以应用于解决 NLP 领域的特定问题。掌握这些技术使我们能够构建出优秀的模型并提供高质量的性能。然而,当涉及到在组织中交付一个可用的机器学习产品时,还有许多其他方面需要考虑。

本章将介绍在组织中交付一个可工作的深度学习系统时的实际项目工作流。具体来说,你将了解组织内不同团队的角色,构建深度学习管道,并最终将产品交付为 SaaS 形式。

机器学习产品开发的一般工作流

如今,在组织中有多种与数据科学合作的方式。大多数组织都有一个特定于其环境的工作流。以下是一些示例工作流:

图 9.1:机器学习产品开发的一般工作流

图 9.1:机器学习产品开发的一般工作流

展示工作流:

图 9.2:一般的展示工作流

展示工作流可以如下详细说明:

  1. 数据科学团队收到使用机器学习解决问题的请求。请求者可以是组织内的其他团队,或者是雇佣你作为顾问的其他公司。

  2. 获取相关数据并应用特定的机器学习技术。

  3. 你以报告/演示的形式向相关方展示结果和见解。这也可能是项目概念验证PoC)阶段的一种潜在方式。

研究工作流:

图 9.3:研究工作流

这种方法的主要重点是进行研究以解决一个特定的问题,且该问题迎合了一个实际应用场景。该解决方案既可以被组织使用,也可以为整个社区所利用。其他将这种工作流与展示工作流区分开来的因素如下:

  • 这类项目的时间线通常比呈现工作流的时间线要长。

  • 可交付成果是以研究论文和/或工具包的形式呈现的。

工作流可以分解如下:

  1. 你的组织有一个研究部门,希望增强社区内现有的机器学习状态,同时允许你的公司利用结果。

  2. 你的团队研究现有研究,以解决提出的问题。这包括详细阅读研究论文,并实施它们以在研究论文中建议的某些数据集上建立基准性能。

  3. 然后你要么尝试调整现有研究以解决你的问题,要么自己提出新的解决方案。

  4. 最终产品可以是研究论文和/或工具箱。

面向生产的工作流程

图 9.4:面向生产的工作流程

图 9.4:面向生产的工作流程

该工作流程可以详细阐述如下:

  1. 数据科学团队接到了使用机器学习解决问题的请求。请求者可能是组织内的其他团队,也可能是雇佣你们作为顾问的另一家公司。也可能是数据科学团队希望构建他们认为将为组织带来价值的产品。

  2. 你获取数据,进行必要的研究,并构建机器学习模型。数据可以来自组织内部,也可以是开源数据集(例如:语言翻译)。因此构建的模型可以作为PoC展示给利益相关者。

  3. 你定义了一个最小可行产品(MVP):例如,以 SaaS 形式的机器学习模型。

一旦达到 MVP,你可以逐步添加其他方面,例如数据获取管道持续集成监控等。

你会注意到,即使示例工作流程也共享组件。在本章中,我们的重点将放在生产工作流程的一部分。我们将为一个特定问题建立一个最小可行产品。

问题定义

假设你在一个电子商务平台工作,通过这个平台,客户可以购买各种产品。你公司的商品部门提出在网站上添加一个功能的请求 – '增加一个滑块,显示在特定日历周内获得最多正面评价的 5 个商品'。

首先将此请求提交给网页开发部门,因为最终他们负责显示网站内容。网页开发部门意识到,为了获得评价等级,需要数据科学团队参与。数据科学团队从网页开发团队接到请求 – '我们需要一个 Web 服务,接受文本字符串作为输入,并返回表示文本表达积极情绪程度的评分'。

然后数据科学团队细化需求,并与网页开发团队达成关于最小可行产品(MVP)定义的协议:

  1. 交付物将是部署在 AWS EC2 实例上的 Web 服务。

  2. Web 服务的输入将是一个包含四条评论的 POST 请求(即单个 POST 请求包含四条评论)。

  3. Web 服务的输出将是与每个输入文本对应的四个评分。

  4. 输出评分将以 1 到 5 的尺度表示,1 表示最不积极的评论,5 表示最积极的评论。

数据采集

任何机器学习模型性能的一个重要决定因素是数据的质量和数量。

通常,数据仓库团队/基础设施团队(DWH)负责公司内与数据相关的基础设施维护。该团队确保数据不会丢失,底层基础设施稳定,并且数据始终可供任何可能需要使用的团队使用。数据科学团队作为数据的消费者之一,会联系 DWH 团队,由其授予访问权限,访问包含公司产品目录中各种商品评论的数据库。

通常,数据库中有多个数据字段/表格,其中一些可能对机器学习模型的开发并不重要。

数据工程师(DWH 团队的一部分、其他团队成员或你团队的成员)随后连接到数据库,将数据处理为表格格式,并生成csv格式的平面文件。在这一过程中,数据科学家与数据工程师的讨论最终决定只保留数据库表中的三列:

  • 'Rating':1 到 5 的评分,表示积极情感的程度

  • 'Review Title':评论的简单标题

  • 'Review':实际的评论文本

请注意,这三个字段都是来自客户(即你电商平台的用户)的输入。此外,像‘item id’这样的字段并未保留,因为它们不需要用于构建这个情感分类的机器学习模型。这些信息的保留与删除也是数据科学团队、数据工程师和 DWH 团队之间讨论的结果。

目前的数据可能没有情感评分。在这种情况下,一种常见的解决方案是手动浏览每个评论并为其分配情感分数,以便为模型获得训练数据。然而,正如你所想的那样,为数百万条评论做这项工作是一个令人望而却步的任务。因此,可以利用众包服务,如Amazon Mechanical Turk,来标注数据并为其获取训练标签。

注意

欲了解更多关于 Amazon Mechanical Turk 的信息,请参见 https://www.mturk.com/。

Google Colab

你一定熟悉深度学习模型的强大计算需求。在 CPU 上,训练一个含有大量训练数据的深度学习模型会花费非常长的时间。因此,为了让训练时间保持在可接受范围内,通常会使用提供图形处理单元(GPU)加速计算的云服务。与在 CPU 上运行训练过程相比,你可以期待加速 10 到 30 倍。当然,具体的加速效果取决于 GPU 的性能、数据量和处理步骤。

有许多供应商提供此类云服务,如亚马逊 Web 服务AWS)、微软 Azure等。Google 提供了一个名为Google Colab的环境/IDE,任何想训练深度学习模型的人都可以每天免费使用最多 12 小时的 GPU。此外,代码是在类似Jupyter的笔记本上运行的。在本章中,我们将利用 Google Colab 的强大功能来开发我们的深度学习情感分类器。

为了熟悉 Google Colab,建议你完成一个 Google Colab 教程。

注意

在继续之前,请参考 https://colab.research.google.com/notebooks/welcome.ipynb#recent=true 上的教程

以下步骤将帮助你更好地了解 Google Colab:

  1. 若要打开一个新的空白Colab笔记本,请访问 https://colab.research.google.com/notebooks/welcome.ipynb,选择菜单中的“文件”,然后选择“新建 Python 3 笔记本”选项,如截图所示:

    图 9.5:Google Colab 上的新 Python 笔记本
  2. 接下来,给笔记本重命名为你选择的名称。然后,为了使用GPU进行训练,我们需要选择GPU作为运行时环境。为此,从菜单中选择“编辑”选项,然后选择“笔记本设置”。图 9.6:Google Colab 中的编辑下拉菜单

    图 9.6:Google Colab 中的编辑下拉菜单
  3. 一个菜单将弹出,其中有一个“硬件加速器”字段,默认设置为“”:图 9.7:Google Colab 的笔记本设置

    图 9.7:Google Colab 的笔记本设置
  4. 此时,可以使用下拉菜单选择“GPU”作为选项:图 9.8:GPU 硬件加速器

    图 9.8:GPU 硬件加速器
  5. 为了检查 GPU 是否已经分配给你的笔记本,请运行以下代码:

    # Check if GPU is detected
    import tensorflow as tf
    tf.test.gpu_device_name()
    

    运行此代码后的输出应显示 GPU 的可用性:

    图 9.9:GPU 设备名称的截图

    图 9.9:GPU 设备名称的截图

    输出结果是 GPU 的设备名称。

  6. 接下来,数据需要在笔记本中可访问。有多种方式可以做到这一点。一种方法是将数据移动到个人的 Google Drive 位置。为了避免占用过多的空间,最好将数据以压缩格式移动。首先,在 Google Drive 上创建一个新文件夹,并将压缩的 CSV 数据文件移入该文件夹。然后,我们将 Google Drive 挂载到 Colab 笔记本机器上,使得驱动器中的数据可以在 Colab 笔记本中使用:

    from google.colab import drive
    drive.mount('/content/gdrive')
    

    我们刚才提到的代码片段会返回一个用于授权的网页链接。点击该链接后,会打开一个新的浏览器标签页,显示一个授权码,复制并粘贴到笔记本提示框中:

    图 9.10:从 Google Drive 导入数据的截图

    图 9.10:从 Google Drive 导入数据的截图

    此时,Google Drive 中的所有数据都可以在 Colab 笔记本中使用了。

  7. 接下来,导航到包含压缩数据的文件夹位置:

    cd "/content/gdrive/My Drive/Lesson-9/"
    
  8. 通过在笔记本单元中输入 'pwd' 命令来确认你已经导航到所需位置:图 9.11:从 Google Drive 导入的数据截图

    图 9.11:从 Google Drive 导入的数据截图
  9. 接下来,使用 unzip 命令解压缩数据文件:

    !unzip data.csv.zip
    

    这将产生以下输出:

    图 9.12:在 Colab 笔记本上解压数据文件

    图 9.12:在 Colab 笔记本上解压数据文件

    'MACOSX' 输出行是操作系统特定的,可能并不适用于每个人。无论如何,一个解压后的数据文件 'data.csv' 现在可以在 Colab 笔记本中使用了。

  10. 现在我们已经有了数据,并且设置好了可以使用 GPU 的环境,我们可以开始编写模型代码了。首先,我们将导入所需的包:

    import os
    import re
    import pandas as pd
    from keras.preprocessing.text import Tokenizer
    from keras.preprocessing.sequence import pad_sequences
    from keras.models import Sequential
    from keras.layers import Dense, Embedding, LSTM
    
  11. 接下来,我们将编写一个预处理函数,将所有文本转为小写并移除任何数字:

    def preprocess_data(data_file_path):
        data = pd.read_csv(data_file_path, header=None) # read the csv
        data.columns = ['rating', 'title', 'review'] # add column names
        data['review'] = data['review'].apply(lambda x: x.lower()) # change all text to lower
        data['review'] = data['review'].apply((lambda x: re.sub('[^a-zA-z0-9\s]','',x))) # remove all numbers
        return data
    
  12. 请注意,我们使用 pandas 来读取和处理文本。让我们运行这个函数并提供 CSV 文件的路径:

    df = preprocess_data('data.csv')
    
  13. 现在,我们可以检查数据框的内容:图 9.13:数据框内容截图

    图 9.13:数据框内容截图
  14. 正如预期的那样,我们有三个字段。此外,我们可以看到 'review' 列的文本比 'title' 列多得多。因此,我们选择仅使用 'review' 列来开发模型。接下来,我们将进行文本分词:

    # initialize tokenization
    max_features = 2000
    maxlength = 250
    tokenizer = Tokenizer(num_words=max_features, split=' ')
    # fit tokenizer
    tokenizer.fit_on_texts(df['review'].values)
    X = tokenizer.texts_to_sequences(df['review'].values)
    # pad sequences
    X = pad_sequences(X, maxlen=maxlength)
    

    在这里,我们将特征数限制为 2,000 个词。然后,我们使用最大特征数的分词器应用于数据的 'review' 列。我们还将序列长度填充到 250 个词。

    X 变量如下所示:

    图 9.14:X 变量数组的截图

    图 9.14:X 变量数组的截图

    X 变量是一个 NumPy 数组,包含 3,000,000 行和 250 列。因为有 3,000,000 条评论,每条评论在填充后都有固定长度 250 个词。

  15. 我们现在准备目标变量以进行训练。我们将问题定义为一个五分类问题,每个类别对应一个评分。由于评分(情感分数)是 1 到 5 的范围,因此分类器有 5 个输出。(你也可以将其建模为回归问题)。我们使用 pandas 的 get_dummies 函数来获得这五个输出:

    # get target variable
    y_train = pd.get_dummies(df.rating).values
    

    y_train 变量是一个 NumPy 数组,包含 3,000,000 行和 5 列,值如下所示:

    图 9.15:y_train 输出

    ](https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-nlp/img/C13783_9_15.jpg)

    图 9.15:y_train 输出
  16. 我们现在已经预处理了文本并准备好了目标变量。接下来,我们定义模型:

    embed_dim = 128
    hidden_units = 100
    n_classes = 5
    model = Sequential()
    model.add(Embedding(max_features, embed_dim, input_length = X.shape[1]))
    model.add(LSTM(hidden_units))
    model.add(Dense(n_classes, activation='softmax'))
    model.compile(loss = 'categorical_crossentropy', optimizer='adam',metrics = ['accuracy'])
    print(model.summary())
    

    我们选择 128 的嵌入维度作为输入。我们还选择了 LSTM 作为 RNN 单元,隐藏维度为 100。模型摘要如下所示:

    图 9.16:模型摘要的截图

    ](https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-nlp/img/C13783_9_16.jpg)

    图 9.16:模型摘要的截图
  17. 我们现在可以拟合模型:

    # fit the model
    model.fit(X[:100000, :], y_train[:100000, :], batch_size = 128, epochs=15, validation_split=0.2)
    

    注意,我们训练了 100,000 条评论而不是 3,000,000 条。使用这个配置运行训练会话大约需要 90 分钟。如果使用完整的数据集,将需要更长时间:

    图 9.17:训练会话的截图

    ](https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-nlp/img/C13783_9_17.jpg)

    图 9.17:训练会话的截图

    这个五分类问题的验证准确率为 48%。这个结果不是很好,但为了演示的目的,我们可以继续进行部署。

  18. 我们现在拥有了想要部署的模型。接下来,我们需要保存模型文件和分词器,以便在生产环境中用于获取新的评论预测:

    # save model and tokenizer
    model.save('trained_model.h5')  # creates a HDF5 file 'trained_model.h5'
    with open('trained_tokenizer.pkl', 'wb') as f: # creates a pickle file 'trained_tokenizer.pkl'
        pickle.dump(tokenizer, f)
    
  19. 现在这些文件需要从 Google Colab 环境下载到本地硬盘:

    from google.colab import files
    files.download('trained_model.h5')
    files.download('trained_tokenizer.pkl')
    

    这个代码片段将下载分词器和模型文件到本地计算机。现在我们已经准备好使用模型进行预测。

Flask

在本节中,我们将使用 Python 提供的 Flask 微服务框架,制作一个提供预测的 Web 应用程序。我们将获得一个可以查询的 RESTful API 以获取结果。在开始之前,我们需要安装 Flask(使用 pip):

  1. 让我们开始导入必要的包:

    import re
    import pickle
    import numpy as np
    from flask import Flask, request, jsonify
    from keras.models import load_model
    from keras.preprocessing.sequence import pad_sequences
    
  2. 现在,让我们编写一个加载训练好的模型和 tokenizer 的函数:

    def load_variables():
        global model, tokenizer
        model = load_model('trained_model.h5')
        model._make_predict_function()  #https://github.com/keras-team/keras/issues/6462
        with open('trained_tokenizer.pkl',  'rb') as f:
            tokenizer = pickle.load(f)
    

    make_predict_function() 是一个技巧,使得 Flask 可以使用 keras 模型。

  3. 现在,我们将定义类似于训练代码的预处理函数:

    def do_preprocessing(reviews):
        processed_reviews = []
        for review in reviews:
            review = review.lower()
            processed_reviews.append(re.sub('[^a-zA-z0-9\s]', '', review))
        processed_reviews = tokenizer.texts_to_sequences(np.array(processed_reviews))
        processed_reviews = pad_sequences(processed_reviews, maxlen=250)
        return processed_reviews
    

    与训练阶段类似,评论首先会被转换为小写。然后,数字会被空白替换。接着,加载的分词器将被应用,并且序列会被填充为固定长度 250,以便与训练输入一致。

  4. 我们现在定义一个 Flask 应用实例:

    app = Flask(__name__)
    
  5. 现在我们定义一个端点,显示固定的消息:

    @app.route('/')
    def home_routine():
        return 'Hello World!'
    

    良好的实践是在根端点处检查 Web 服务是否可用。

  6. 接下来,我们将设置一个预测端点,向该端点发送我们的评论字符串。我们将使用的 HTTP 请求类型是‘POST’请求:

    @app.route('/prediction', methods=['POST'])
    def get_prediction():
      # get incoming text
      # run the model
        if request.method == 'POST':
            data = request.get_json()
        data = do_preprocessing(data)
        predicted_sentiment_prob = model.predict(data)
        predicted_sentiment = np.argmax(predicted_sentiment_prob, axis=-1)
        return str(predicted_sentiment)
    
  7. 现在我们可以启动网页服务器:

    if __name__ == '__main__':
      # load model
      load_variables()
      app.run(debug=True)
    
  8. 我们可以将此文件保存为 app.py(可以使用任何名称)。从终端运行此代码,使用 app.py

    python app.py
    

    在终端窗口中将产生如下所示的输出:

    图 9.18:Flask 输出

    图 9.18:Flask 输出
  9. 这时,请打开浏览器并输入 http://127.0.0.1:5000/ 地址。屏幕上将显示“Hello World!”消息。输出内容对应于我们在代码中设置的根端点。现在,我们将评论文本发送到 Flask 网络服务的“预测”端点。让我们发送以下四条评论:

  10. “这本书非常差”

  11. “非常好!”

  12. “作者本可以做得更多”

  13. “令人惊叹的产品!”

  14. 我们可以使用 curl 请求向 Web 服务发送 POST 请求。对于提到的四条评论,可以通过终端发送 curl 请求,方法如下:

    curl -X POST \
    127.0.0.1:5000/prediction \
    -H 'Content-Type: application/json' \
    -d '["The book was very poor", "Very nice!", "The author could have done more", "Amazing product!"]'
    

    四条评论将被发布到网络服务的预测端点。

    Web 服务返回四个评分的列表:

    [0 4 2 4]
    

    因此,情感评分如下:

  15. “这本书非常差”- 0

  16. “非常好!”- 4

  17. “作者本可以做得更多” - 2

  18. “令人惊叹的产品!” - 4

    评分实际上很有意义!

部署

到目前为止,数据科学团队已经拥有了一个在本地系统上运行的 Flask Web 服务。然而,网页开发团队仍然无法使用该服务,因为它只在本地系统上运行。因此,我们需要将这个 Web 服务托管在某个云平台上,以便网页开发团队也能使用。本节提供了一个基本的部署管道,分为以下几个步骤:

  1. 对 Flask 网页应用进行更改,以便可以部署。

  2. 使用 Docker 将 Flask 网页应用打包成容器。

  3. 将容器托管在亚马逊 Web 服务(AWS)EC2 实例上。

让我们详细查看每一步。

对 Flask 网页应用进行更改

在 FLASK 部分编码的 Flask 应用程序运行在本地 Web 地址:http://127.0.0.1:5000。由于我们的目标是将其托管在互联网上,因此该地址需要更改为:0.0.0.0。 此外,由于默认的 HTTP 端口是 80,因此端口也需要从 5000 更改为 80。所以,现在需要查询的地址变成了:0.0.0.0:80。

在代码片段中,可以通过修改对 app.run 函数的调用来简单地完成此更改,如下所示:

app.run(host=0.0.0.0, port=80)

请注意,‘debug’标志也消失了(‘debug’标志的默认值是‘False’)。这是因为应用程序已经过了调试阶段,准备部署到生产环境。

注意

其余的代码与之前完全相同。

应用程序应使用与之前相同的命令再次运行,并验证是否收到了与之前相同的响应。curl 请求中的地址需要更改为反映更新后的网址:

curl -X POST \
0.0.0.0:80/prediction \
-H 'Content-Type: application/json' \
-d '["The book was very poor", "Very nice!", "The author could have done more", "Amazing product!"]' 

注意

如果此时收到权限错误,请在 app.py 中的 app.run() 命令中将端口号更改为 5000。(端口 80 是特权端口,因此将其更改为非特权端口,例如 5000)。但是,请确保在验证代码正常工作后将端口改回 80。

使用 Docker 将 Flask Web 应用程序打包成容器

DS 团队打算在云平台(即 AWS EC2)上托管的虚拟机上运行 Web 服务。为了将 EC2 操作系统与代码环境隔离,Docker 提供了容器化作为解决方案。我们将在这里使用它。

注意

要了解 Docker 的基础知识以及如何安装和使用它,可以参考 https://docker-curriculum.com/。

按照以下步骤将应用程序部署到容器中:

  1. 我们首先需要一个 requirements.txt 文件,列出运行 Python 代码所需的特定包:

    Flask==1.0.2
    numpy==1.14.1
    keras==2.2.4
    tensorflow==1.10.0
    
  2. 我们需要一个包含指令的 Dockerfile,以便 Docker 守护进程可以构建 Docker 镜像:

    FROM python:3.6-slim
    COPY ./app.py /deploy/
    COPY ./requirements.txt /deploy/
    COPY ./trained_model.h5 /deploy/
    COPY ./trained_tokenizer.pkl /deploy/
    WORKDIR /deploy/
    RUN pip install -r requirements.txt
    EXPOSE 80
    ENTRYPOINT ["python", "app.py"]
    

    Docker 镜像是从 Python dockerhub 仓库拉取的。在这里,执行了 Dockerfile。app.pyrequirements.txttokenizer pickle 文件和 trained model 被使用 COPY 命令复制到 Docker 镜像中。为了将工作目录更改为“deploy”目录(文件已复制到该目录中),使用 WORKDIR 命令。接着,RUN 命令安装了 Dockerfile 中提到的 Python 包。由于端口 80 需要在容器外部进行访问,因此使用了 EXPOSE 命令。

    注意

    Docker Hub 链接可以在 https://hub.docker.com/_/python 找到。

  3. 接下来,应使用 docker build 命令构建 Docker 镜像:

    docker build -f Dockerfile -t app-packt .
    

    别忘了命令中的句点。命令的输出如下:

    图 9.19:docker build 的输出截图

    图 9.19:docker build 的输出截图

    'app-packt' 是生成的 Docker 镜像的名称。

  4. 现在,可以通过发出 docker run 命令来将 Docker 镜像作为容器运行:

    docker run -p 80:80 app-packt
    

    p 标志 用于在本地系统的端口 80 和 Docker 容器的端口 80 之间进行端口映射。(如果本地使用 5000 端口,请将命令中的端口映射部分改为 5000:80。验证 Docker 容器正常工作后,请按照说明将映射更改回 80:80。)

    以下截图展示了 docker run 命令的输出:

图 9.20:docker run 命令的输出截图

图 9.20:docker run 命令的输出截图

现在可以发出与上一部分完全相同的 curl 请求来验证应用程序是否正常工作。

现在,应用程序代码已经准备好部署到 AWS EC2 上。

在亚马逊 Web 服务(AWS)EC2 实例上托管容器

DS 团队现在拥有一个在本地系统上运行的容器化应用程序。由于该应用程序仍在本地,Web 开发团队仍然无法使用它。根据最初的 MVP 定义,DS 团队现在开始使用 AWS EC2 实例来部署该应用程序。部署将确保 Web 服务可供 Web 开发团队使用。

作为前提条件,您需要有一个 AWS 账户才能使用 EC2 实例。为了演示,我们将使用一个't2.small' EC2 实例类型。此实例在撰写时每小时约收费 2 美分(USD)。请注意,此实例不属于免费套餐。默认情况下,您的 AWS 区域内将无法使用此实例,您需要提出请求将此实例添加到您的账户中。通常需要几个小时。或者,检查您 AWS 区域的实例限制,并选择另一个至少有 2GB 内存的实例。一个简单的't2.micro'实例无法满足我们的需求,因为它只有 1GB 内存。

注意

AWS 账户的链接可以在 https://aws.amazon.com/premiumsupport/knowledge-center/create-and-activate-aws-account/找到。

要添加实例并检查实例限制,请参阅 https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-resource-limits.html。

让我们从部署过程开始:

  1. 登录到 AWS 管理控制台后,在搜索栏中搜索'ec2'。这将带您进入 EC2 仪表板,如下图所示:图 9.21:AWS 管理控制台中的 AWS 服务

    图 9.21:AWS 管理控制台中的 AWS 服务
  2. 需要创建一个密钥对来访问 AWS 资源。要创建一个,请查找以下面板并选择'密钥对'。这将允许您创建一个新的密钥对:图 9.22:AWS 控制台上的网络与安全

    图 9.22:AWS 控制台上的网络与安全
  3. 下载了一个'.pem'文件,这是密钥文件。请确保安全保存pem文件,并使用以下命令更改其模式:

    chmod 400 key-file-name.pem
    

    需要更改文件权限为私有。

  4. 要配置实例,请在 EC2 仪表板上选择'启动实例':图 9.23:AWS 控制台上的资源

    图 9.23:AWS 控制台上的资源
  5. 接下来,选择亚马逊机器实例AMI),它会选择 EC2 实例运行的操作系统。我们将使用'Amazon Linux 2 AMI':

    注意

    要了解有关 Amazon Linux 2 AMI 的更多信息,请参阅 https://aws.amazon.com/amazon-linux-2/。

    图 9.24:亚马逊机器实例(AMI)

    图 9.24:亚马逊机器实例(AMI)
  6. 现在,我们选择 EC2 的硬件部分,即't2.small'实例:图 9.25:选择 AMI 上的实例类型

    图 9.25:选择 AMI 上的实例类型
  7. 点击 'Review and Launch' 将进入第 7 步——审查实例启动屏幕:图 9.26:审查实例启动屏幕

    图 9.26:审查实例启动屏幕
  8. 现在,为了使 Web 服务可达,需要修改安全组。为此,需要创建一条规则。最后,你应该看到以下屏幕:图 9.27:配置安全组

    图 9.27:配置安全组

    注意

    可以通过 AWS 文档进一步了解安全组和配置:https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-network-security.html。

  9. 接下来,点击 'Launch' 图标将触发重定向到 Launch 屏幕:图 9.28:AWS 实例上的启动状态

    图 9.28:AWS 实例上的启动状态

    'View Instance' 按钮用于导航到显示正在启动的 EC2 实例的屏幕,当实例状态变为“running”时,表示实例已准备好使用。

  10. 接下来,通过以下命令从本地系统终端访问 EC2,替换 'public-dns-name' 字段为你的 EC2 实例名称(格式为:ec2–x–x–x–x.compute-1.amazonaws.com)和之前保存的密钥对 pem 文件的路径:

    ssh -i /path/my-key-pair.pem ec2-user@public-dns-name
    

    此命令将带你进入 EC2 实例的提示符,首先需要在 EC2 实例中安装 Docker。由于 Docker 镜像将在 EC2 实例中构建,因此安装 Docker 是工作流所必需的。

  11. 对于 Amazon Linux 2 AMI,应使用以下命令来完成此操作:

    sudo amazon-linux-extras install docker
    sudo yum install docker
    sudo service docker start
    sudo usermod -a -G docker ec2-user
    

    注意

    有关命令的解释,请查阅文档 https://docs.aws.amazon.com/AmazonECS/latest/developerguide/docker-basics.html。

  12. 应该使用 'exit' 命令退出实例。接下来,使用之前使用的 ssh 命令重新登录。通过执行 'docker info' 命令验证 Docker 是否正常工作。为接下来的步骤打开另一个本地终端窗口。

  13. 现在,将构建 Docker 镜像所需的文件复制到 EC2 实例内。从本地终端(不是 EC2 内部!)发出以下命令:

    scp -i /path/my-key-pair.pem file-to-copy ec2-user@public-dns-name:/home/ec2-user
    
  14. 应复制以下文件以构建 Docker 镜像,如之前所做:requirements.txtapp.pytrained_model.h5trained_tokenizer.pklDockerfile

  15. 接下来,登录到 EC2 实例,执行 'ls' 命令查看复制的文件是否存在,然后使用与本地系统相同的命令构建并运行 Docker 镜像(确保在代码/命令中的所有位置使用端口 80)。

  16. 从本地浏览器通过公共 DNS 名称进入首页端点,查看 'Hello World!' 消息:图 9.29:首页端点截图

    图 9.29:首页端点截图
  17. 现在,你可以从本地终端发送 curl 请求到 Web 服务,测试示例数据,并将 public-dns-name 替换为你的值:

    curl -X POST \
    public-dns-name:80/predict \
    -H 'Content-Type: application/json' \
    -d '["The book was very poor", "Very nice!", "The author could have done more", "Amazing product!"]'
    
  18. 这应该返回与本地获得的相同的评论评分。

这就结束了简单的部署过程。

DS 团队现在将此 curl 请求与 Web 开发团队共享,后者可以使用他们的测试样本来调用该 Web 服务。

注意

当不再需要 Web 服务时,停止或终止 EC2 实例,以避免产生费用。

图 9.30:停止 AWS EC2 实例

图 9.30:停止 AWS EC2 实例

从 MVP 的角度来看,交付物现在已经完成!

改进

本章描述的工作流程仅仅是为了介绍一个使用特定工具(Flask、Colab、Docker 和 AWS EC2)的基本工作流程,并为组织中的深度学习项目提供一个示范计划。然而,这仅仅是一个 MVP(最小可行产品),未来的迭代可以在许多方面进行改进。

总结

在本章中,我们看到了深度学习项目在组织中的发展历程。我们还学习了如何使用 Google Colab 笔记本来利用 GPU 加速训练。此外,我们开发了一个基于 Flask 的 Web 服务,使用 Docker 部署到云环境,从而使利益相关者能够为给定输入获取预测结果。

本章总结了我们如何利用深度学习技术解决自然语言处理领域问题的学习过程。本章以及前几章讨论的几乎每个方面都是研究课题,并且正在不断改进。保持信息更新的唯一方法是不断学习新的、令人兴奋的解决问题的方式。一些常见的做法是通过社交媒体跟踪讨论,关注顶尖研究人员/深度学习从业者的工作,并时刻留意那些在该领域进行前沿研究的组织。

第十一章:附录

关于

本节内容是为了帮助学习者完成书中的活动,内容包括学习者需要执行的详细步骤,以完成并实现书本中的目标。

第一章:自然语言处理简介

活动 1:使用 Word2Vec 从语料库生成词嵌入。

解决方案:

  1. 从上述链接上传文本语料。

  2. 从 gensim 模型中导入 word2vec

    from gensim.models import word2vec
    
  3. 将语料库存储在一个变量中。

    sentences = word2vec.Text8Corpus('text8')
    
  4. 在语料库上拟合 word2vec 模型。

    model = word2vec.Word2Vec(sentences, size = 200)
    
  5. 找到与‘man’最相似的词。

    model.most_similar(['man'])
    

    输出如下:

    图 1.29:相似词嵌入的输出

    图 1.29:相似词嵌入的输出
  6. ‘Father’对应‘girl’,‘x’对应 boy。找出 x 的前三个词。

    model.most_similar(['girl', 'father'], ['boy'], topn=3)
    

    输出如下:

图 1.30:‘x’的前三个词的输出

](https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-nlp/img/C13783_01_30.jpg)

图 1.30:‘x’的前三个词的输出

第二章:自然语言处理的应用

活动 2:构建并训练你自己的词性标注器

解决方案:

  1. 第一件事是选择我们想要训练标注器的语料。导入必要的 Python 包。在这里,我们使用nltk treebank语料库进行操作:

    import nltk
    nltk.download('treebank')
    tagged_sentences = nltk.corpus.treebank.tagged_sents()
    print(tagged_sentences[0])
    print("Tagged sentences: ", len(tagged_sentences))
    print ("Tagged words:", len(nltk.corpus.treebank.tagged_words()))
    
  2. 接下来,我们需要确定我们的标注器在为一个词分配标签时会考虑哪些特征。这些特征可以包括该词是否全大写、是否小写或是否有一个大写字母:

    def features(sentence, index):
        """ sentence: [w1, w2, ...], index: the index of the word """
        return {
            'word': sentence[index],
            'is_first': index == 0,
            'is_last': index == len(sentence) - 1,
            'is_capitalized': sentence[index][0].upper() == sentence[index][0],
            'is_all_caps': sentence[index].upper() == sentence[index],
            'is_all_lower': sentence[index].lower() == sentence[index],
            'prefix-1': sentence[index][0],
            'prefix-2': sentence[index][:2],
            'prefix-3': sentence[index][:3],
            'suffix-1': sentence[index][-1],
            'suffix-2': sentence[index][-2:],
            'suffix-3': sentence[index][-3:],
            'prev_word': '' if index == 0 else sentence[index - 1],
            'next_word': '' if index == len(sentence) - 1 else sentence[index + 1],
            'has_hyphen': '-' in sentence[index],
            'is_numeric': sentence[index].isdigit(),
            'capitals_inside': sentence[index][1:].lower() != sentence[index][1:]
        }
    import pprint 
    pprint.pprint(features(['This', 'is', 'a', 'sentence'], 2))
    
    {'capitals_inside': False,
     'has_hyphen': False,
     'is_all_caps': False,
     'is_all_lower': True,
     'is_capitalized': False,
     'is_first': False,
     'is_last': False,
     'is_numeric': False,
     'next_word': 'sentence',
     'prefix-1': 'a',
     'prefix-2': 'a',
     'prefix-3': 'a',
     'prev_word': 'is',
     'suffix-1': 'a',
     'suffix-2': 'a',
     'suffix-3': 'a',
     'word': 'a'}
    
  3. 创建一个函数来剥离标注词的标签,以便我们可以将它们输入到标注器中:

    def untag(tagged_sentence):
        return [w for w, t in tagged_sentence]
    
  4. 现在我们需要构建我们的训练集。我们的标注器需要为每个词单独提取特征,但我们的语料库实际上是句子的形式,所以我们需要做一些转换。将数据拆分为训练集和测试集。对训练集应用此函数。

    # Split the dataset for training and testing
    cutoff = int(.75 * len(tagged_sentences))
    training_sentences = tagged_sentences[:cutoff]
    test_sentences = tagged_sentences[cutoff:]
    
    print(len(training_sentences))   # 2935
    print(len(test_sentences))      # 979
     and create a function to assign the features to 'X' and append the POS tags to 'Y'.
    def transform_to_dataset(tagged_sentences):
        X, y = [], []
    
        for tagged in tagged_sentences:
            for index in range(len(tagged)):
                X.append(features(untag(tagged), index))
                y.append(tagged[index][1])
    
        return X, y
    
    X, y = transform_to_dataset(training_sentences)
    from sklearn.tree import DecisionTreeClassifier
    from sklearn.feature_extraction import DictVectorizer
    from sklearn.pipeline import Pipeline
    
  5. 在训练集上应用此函数。现在我们可以训练我们的标注器。它本质上是一个分类器,因为它将词分类到不同的类别中,所以我们可以使用分类算法。你可以使用任何你喜欢的算法,或者尝试多个看看哪个效果最好。这里,我们将使用决策树分类器。导入分类器,初始化它,并将模型拟合到训练数据上。然后打印准确率分数。

    clf = Pipeline([
        ('vectorizer', DictVectorizer(sparse=False)),
        ('classifier', DecisionTreeClassifier(criterion='entropy'))
    ])
    
    clf.fit(X[:10000], y[:10000])   # Use only the first 10K samples if you're running it multiple times. It takes a fair bit :)
    
    print('Training completed')
    
    X_test, y_test = transform_to_dataset(test_sentences)
    
    print("Accuracy:", clf.score(X_test, y_test))
    

    输出如下:

图 2.19:准确率分数

活动 3:在标注语料上执行命名实体识别(NER)

解决方案:

  1. 导入必要的 Python 包和类。

    import nltk
    nltk.download('treebank')
    nltk.download('maxent_ne_chunker')
    nltk.download('words')
    
  2. 打印nltk.corpus.treebank.tagged_sents()查看你需要从中提取命名实体的标注语料。

    nltk.corpus.treebank.tagged_sents()
    sent = nltk.corpus.treebank.tagged_sents()[0]
    print(nltk.ne_chunk(sent, binary=True))
    
  3. 将标注句子的第一句存储在一个变量中。

    sent = nltk.corpus.treebank.tagged_sents()[1]
    
  4. 使用nltk.ne_chunk对句子执行命名实体识别(NER)。将 binary 设置为 True 并打印命名实体。

    print(nltk.ne_chunk(sent, binary=False))
    sent = nltk.corpus.treebank.tagged_sents()[2]
    rint(nltk.ne_chunk(sent))
    

    输出如下:

图 2.20:在标注语料上的命名实体识别(NER)

第三章:神经网络简介

活动 4:评论的情感分析

解决方案:

  1. 打开一个新的Jupyter笔记本。导入numpypandasmatplotlib.pyplot。将数据集加载到数据框中。

    import numpy as np
    import matplotlib.pyplot as plt
    import pandas as pd
    dataset = pd.read_csv('train_comment_small_100.csv', sep=',')
    
  2. 下一步是清理和准备数据。导入renltk。从nltk.corpus导入stopwords。从nltk.stem.porter导入PorterStemmer。创建一个数组来存储清理后的文本。

    import re
    import nltk
    nltk.download('stopwords')
    from nltk.corpus import stopwords
    from nltk.stem.porter import PorterStemmer
    corpus = []
    
  3. 使用 for 循环,遍历每个实例(每个评论)。将所有非字母字符替换为' '(空格)。将所有字母转换为小写。将每条评论拆分成单个单词。初始化PorterStemmer。如果该单词不是停用词,则对该单词进行词干提取。将所有单个单词重新组合成一条清理后的评论。将这条清理后的评论附加到你创建的数组中。

    for i in range(0, dataset.shape[0]-1):
        review = re.sub('[^a-zA-Z]', ' ', dataset['comment_text'][i])
        review = review.lower()
        review = review.split()
    ps = PorterStemmer()
        review = [ps.stem(word) for word in review if not word in set(stopwords.words('english'))]
        review = ' '.join(review)
        corpus.append(review)
    
  4. 导入CountVectorizer。使用CountVectorizer将评论转换为词频向量。

    from sklearn.feature_extraction.text import CountVectorizer
    cv = CountVectorizer(max_features = 20)
    
  5. 创建一个数组来存储每个唯一的单词作为独立的列,从而使它们成为独立变量。

    X = cv.fit_transform(corpus).toarray()
    y = dataset.iloc[:,0]
    y1 = y[:99]
    y1
    
  6. sklearn.preprocessing导入LabelEncoder。对目标输出(y)使用LabelEncoder

    from sklearn import preprocessing
    labelencoder_y = preprocessing.LabelEncoder()
    y = labelencoder_y.fit_transform(y1)
    
  7. 导入train_test_split。将数据集划分为训练集和验证集。

    from sklearn.model_selection import train_test_split
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.20, random_state = 0)
    
  8. sklearn.preprocessing导入StandardScaler。对训练集和验证集(X)的特征使用StandardScaler

    from sklearn.preprocessing import StandardScaler
    sc = StandardScaler()
    X_train = sc.fit_transform(X_train)
    X_test = sc.transform(X_test)
    
  9. 现在下一个任务是创建神经网络。导入keras。从keras.models导入Sequential,从 Keras 层导入Dense

    import tensorflow
    import keras
    from keras.models import Sequential
    from keras.layers import Dense
    
  10. 初始化神经网络。添加第一个隐藏层,激活函数使用'relu'。对第二个隐藏层重复此步骤。添加输出层,激活函数使用'softmax'。编译神经网络,使用'adam'作为优化器,'binary_crossentropy'作为损失函数,'accuracy'作为性能评估标准。

    classifier = Sequential()
    classifier.add(Dense(output_dim = 20, init = 'uniform', activation = 'relu', input_dim = 20))
    classifier.add(Dense(output_dim =20, init = 'uniform', activation = 'relu'))
    classifier.add(Dense(output_dim = 1, init = 'uniform', activation = 'softmax'))
    classifier.compile(optimizer = 'adam', loss = 'binary_crossentropy', metrics = ['accuracy'])
    
  11. 现在我们需要训练模型。使用batch_size为 3 和nb_epoch为 5,在训练数据集上拟合神经网络。

    classifier.fit(X_train, y_train, batch_size = 3, nb_epoch = 5)
    X_test
    
  12. 验证模型。评估神经网络并打印准确度得分,以查看其表现如何。

    y_pred = classifier.predict(X_test)
    scores = classifier.evaluate(X_test, y_pred, verbose=1)
    print("Accuracy:", scores[1])
    
  13. (可选)通过从sklearn.metrics导入confusion_matrix打印混淆矩阵。

    from sklearn.metrics import confusion_matrix
    cm = confusion_matrix(y_test, y_pred)
    scores
    

    你的输出应类似于此:

图 3.21:情感分析的准确度得分

第四章:卷积网络介绍

活动 5:对真实数据集进行情感分析

解决方案:

  1. 导入必要的类

    from keras.preprocessing.text import Tokenizer
    from keras.models import Sequential
    from keras import layers
    from keras.preprocessing.sequence import pad_sequences
    import numpy as np
    import pandas as pd
    
  2. 定义你的变量和参数。

    epochs = 20
    maxlen = 100
    embedding_dim = 50
    num_filters = 64
    kernel_size = 5
    batch_size = 32
    
  3. 导入数据。

    data = pd.read_csv('data/sentiment labelled sentences/yelp_labelled.txt',names=['sentence', 'label'], sep='\t')
    data.head()
    

    Jupyter笔记本中打印此内容应显示:

    ![图 4.27:带标签的数据集

    图 4.27:带标签的数据集
  4. 选择'sentence''label'

    sentences=data['sentence'].values
    labels=data['label'].values
    
  5. 将数据拆分为训练集和测试集

    from sklearn.model_selection import train_test_split
    X_train, X_test, y_train, y_test = train_test_split(
        sentences, labels, test_size=0.30, random_state=1000)
    
  6. 分词

    tokenizer = Tokenizer(num_words=5000)
    tokenizer.fit_on_texts(X_train)
    X_train = tokenizer.texts_to_sequences(X_train)
    X_test = tokenizer.texts_to_sequences(X_test)
    vocab_size = len(tokenizer.word_index) + 1 #The vocabulary size has an additional 1 due to the 0 reserved index
    
  7. 填充数据以确保所有序列的长度相同

    X_train = pad_sequences(X_train, padding='post', maxlen=maxlen)
    X_test = pad_sequences(X_test, padding='post', maxlen=maxlen)
    
  8. 创建模型。注意,我们在最后一层使用 sigmoid 激活函数,并使用二元交叉熵来计算损失。这是因为我们正在进行二分类。

    model = Sequential()
    model.add(layers.Embedding(vocab_size, embedding_dim, input_length=maxlen))
    model.add(layers.Conv1D(num_filters, kernel_size, activation='relu'))
    model.add(layers.GlobalMaxPooling1D())
    model.add(layers.Dense(10, activation='relu'))
    model.add(layers.Dense(1, activation='sigmoid'))
    model.compile(optimizer='adam',
                  loss='binary_crossentropy',
                  metrics=['accuracy'])
    model.summary()
    

    上述代码应该输出

    图 4.28:模型总结

    模型也可以如下所示进行可视化:

    图 4.29:模型可视化

    图 4.29:模型可视化
  9. 训练并测试模型。

    model.fit(X_train, y_train,
                        epochs=epochs,
                        verbose=False,
                        validation_data=(X_test, y_test),
                        batch_size=batch_size)
    loss, accuracy = model.evaluate(X_train, y_train, verbose=False)
    print("Training Accuracy: {:.4f}".format(accuracy))
    loss, accuracy = model.evaluate(X_test, y_test, verbose=False)
    print("Testing Accuracy:  {:.4f}".format(accuracy))
    

    准确度输出应如下所示:

图 4.30:准确度评分

图 4.30:准确度评分

第五章:循环神经网络基础

活动 6:用 RNN 解决问题——作者归属

解决方案:

准备数据

我们首先设置数据预处理管道。对于每个作者,我们将所有已知的论文聚合为一篇长文。我们假设风格在不同论文中没有变化,因此一篇长文等同于多篇短文,而且从程序角度处理起来要更容易。

对于每个作者的每篇论文,我们执行以下步骤:

  1. 将所有文本转换为小写字母(忽略大小写可能是风格属性这一事实)

  2. 将所有换行符和多个空格转换为单个空格

  3. 删除任何涉及作者姓名的内容,否则我们可能会面临数据泄漏的风险(作者的名字是 hamiltonmadison

  4. 将上述步骤封装成一个函数,因为它在预测未知论文时是必需的。

    import numpy as np
    import os
    from sklearn.model_selection import train_test_split
    # Classes for A/B/Unknown
    A = 0
    B = 1
    UNKNOWN = -1
    def preprocess_text(file_path):
        with open(file_path, 'r') as f:
            lines = f.readlines()
            text = ' '.join(lines[1:]).replace("\n", ' ').replace('  ',' ').lower().replace('hamilton','').replace('madison', '')
            text = ' '.join(text.split())
            return text
    # Concatenate all the papers known to be written by A/B into a single long text
    all_authorA, all_authorB = '',''
    for x in os.listdir('./papers/A/'):
        all_authorA += preprocess_text('./papers/A/' + x)
    for x in os.listdir('./papers/B/'):
        all_authorB += preprocess_text('./papers/B/' + x)
    
    # Print lengths of the large texts
    print("AuthorA text length: {}".format(len(all_authorA)))
    print("AuthorB text length: {}".format(len(all_authorB)))
    

    该输出应如下所示:

    图 5.34:文本长度计数

    图 5.34:文本长度计数

    下一步是将每个作者的长文本拆分为多个小序列。如上所述,我们经验性地选择一个序列长度,并在模型生命周期内始终使用该长度。我们通过为每个序列标注其作者来获取完整的数据集。

    为了将长文本拆分为较小的序列,我们使用 keras 框架中的 Tokenizer 类。特别地,注意我们将其设置为按字符而非按词进行标记。

  5. 选择 SEQ_LEN 超参数,如果模型与训练数据不匹配,可能需要更改此参数。

  6. 编写一个函数 make_subsequences,将每个文档转化为长度为 SEQ_LEN 的序列,并赋予正确的标签。

  7. 使用 Keras Tokenizer 并设置 char_level=True

  8. 对所有文本进行分词器拟合

  9. 使用这个分词器将所有文本转换为序列,方法是使用 texts_to_sequences()

  10. 使用 make_subsequences() 将这些序列转化为合适的形状和长度

    from keras.preprocessing.text import Tokenizer
    # Hyperparameter - sequence length to use for the model
    SEQ_LEN = 30
    def make_subsequences(long_sequence, label, sequence_length=SEQ_LEN):
        len_sequences = len(long_sequence)
        X = np.zeros(((len_sequences - sequence_length)+1, sequence_length))
        y = np.zeros((X.shape[0], 1))
        for i in range(X.shape[0]):
            X[i] = long_sequence[i:i+sequence_length]
            y[i] = label
        return X,y
    
    # We use the Tokenizer class from Keras to convert the long texts into a sequence of characters (not words)
    tokenizer = Tokenizer(char_level=True)
    # Make sure to fit all characters in texts from both authors
    tokenizer.fit_on_texts(all_authorA + all_authorB)
    authorA_long_sequence = tokenizer.texts_to_sequences([all_authorA])[0]
    authorB_long_sequence = tokenizer.texts_to_sequences([all_authorB])[0]
    # Convert the long sequences into sequence and label pairs
    X_authorA, y_authorA = make_subsequences(authorA_long_sequence, A)
    X_authorB, y_authorB = make_subsequences(authorB_long_sequence, B)
    # Print sizes of available data
    print("Number of characters: {}".format(len(tokenizer.word_index)))
    print('author A sequences: {}'.format(X_authorA.shape))
    print('author B sequences: {}'.format(X_authorB.shape))
    

    输出应如下所示:

    图 5.35:序列的字符计数

    图 5.35:序列的字符计数
  11. 比较每个作者的原始字符数与标注序列数。深度学习需要大量的每种输入的示例。以下代码计算文本中的总词数和唯一词数。

    # Calculate the number of unique words in the text
    word_tokenizer = Tokenizer()
    word_tokenizer.fit_on_texts([all_authorA, all_authorB])
    print("Total word count: ", len((all_authorA + ' ' + all_authorB).split(' ')))
    print("Total number of unique words: ", len(word_tokenizer.word_index))
    

    输出应如下所示:

    图 5.36:总词数和唯一词数

    图 5.36:总词数和唯一词数

    我们现在开始创建我们的训练集和验证集。

  12. x 数据堆叠在一起,将 y 数据堆叠在一起。

  13. 使用 train_test_split 将数据集分割为 80% 的训练集和 20% 的验证集。

  14. 重塑数据,确保它们是正确长度的序列。

    # Take equal amounts of sequences from both authors
    X = np.vstack((X_authorA, X_authorB))
    y = np.vstack((y_authorA, y_authorB))
    # Break data into train and test sets
    X_train, X_val, y_train, y_val = train_test_split(X,y, train_size=0.8)
    # Data is to be fed into RNN - ensure that the actual data is of size [batch size, sequence length]
    X_train = X_train.reshape(-1, SEQ_LEN)
    X_val =  X_val.reshape(-1, SEQ_LEN) 
    # Print the shapes of the train, validation and test sets
    print("X_train shape: {}".format(X_train.shape))
    print("y_train shape: {}".format(y_train.shape))
    print("X_validate shape: {}".format(X_val.shape))
    print("y_validate shape: {}".format(y_val.shape))
    

    输出如下:

    图 5.37: 测试集和训练集

    图 5.37: 测试集和训练集

    最后,我们构建模型图并执行训练过程。

  15. 使用 RNNDense 层创建模型。

  16. 由于这是一个二分类问题,输出层应为 Dense,并使用 sigmoid 激活函数。

  17. 使用 optimizer,适当的损失函数和指标编译模型。

  18. 打印模型摘要。

    from keras.layers import SimpleRNN, Embedding, Dense
    from keras.models import Sequential
    from keras.optimizers import SGD, Adadelta, Adam
    Embedding_size = 100
    RNN_size = 256
    model = Sequential()
    model.add(Embedding(len(tokenizer.word_index)+1, Embedding_size, input_length=30))
    model.add(SimpleRNN(RNN_size, return_sequences=False))
    model.add(Dense(1, activation='sigmoid'))
    model.compile(optimizer='adam', loss='binary_crossentropy', metrics = ['accuracy'])
    model.summary()
    

    输出如下:

    图 5.38: 模型摘要
  19. 确定批量大小、训练周期,并使用训练数据训练模型,使用验证数据进行验证。

  20. 根据结果,返回上述模型,必要时进行更改(使用更多层,使用正则化,dropout 等,使用不同的优化器,或不同的学习率等)。

  21. 如有需要,修改 Batch_sizeepochs

    Batch_size = 4096
    Epochs = 20
    model.fit(X_train, y_train, batch_size=Batch_size, epochs=Epochs, validation_data=(X_val, y_val))
    

    输出如下:

图 5.39: 训练周期

将模型应用于未知论文

对“Unknown”文件夹中的所有论文进行此操作。

  1. 以与训练集相同的方式预处理它们(小写,去除空白行等)。

  2. 使用 tokenizer 和上述 make_subsequences 函数将它们转换为所需大小的序列。

  3. 使用模型对这些序列进行预测。

  4. 计算分配给作者 A 和作者 B 的序列数。

  5. 根据计数,选择具有最高票数/计数的作者。

    for x in os.listdir('./papers/Unknown/'):
        unknown = preprocess_text('./papers/Unknown/' + x)
        unknown_long_sequences = tokenizer.texts_to_sequences([unknown])[0]
        X_sequences, _ = make_subsequences(unknown_long_sequences, UNKNOWN)
        X_sequences = X_sequences.reshape((-1,SEQ_LEN))
    
        votes_for_authorA = 0
        votes_for_authorB = 0
    
        y = model.predict(X_sequences)
        y = y>0.5
        votes_for_authorA = np.sum(y==0)
        votes_for_authorB = np.sum(y==1)
    
        print("Paper {} is predicted to have been written by {}, {} to {}".format(
                    x.replace('paper_','').replace('.txt',''), 
                    ("Author A" if votes_for_authorA > votes_for_authorB else "Author B"),
                    max(votes_for_authorA, votes_for_authorB), min(votes_for_authorA, votes_for_authorB)))
    

    输出如下:

图 5.40: 作者归属输出

图 5.40: 作者归属输出

第六章:GRU 基础

活动 7:使用简单 RNN 开发情感分类模型

解决方案:

  1. 加载数据集。

    from keras.datasets import imdb
    max_features = 10000
    maxlen = 500
    
    (train_data, y_train), (test_data, y_test) = imdb.load_data(num_words=max_features)
    print('Number of train sequences: ', len(train_data))
    print('Number of test sequences: ', len(test_data))
    
  2. 填充序列,以确保每个序列具有相同数量的字符。

    from keras.preprocessing import sequence
    train_data = sequence.pad_sequences(train_data, maxlen=maxlen)
    test_data = sequence.pad_sequences(test_data, maxlen=maxlen)
    
  3. 使用 32 个隐藏单元的 SimpleRNN 定义并编译模型。

    from keras.models import Sequential
    from keras.layers import Embedding
    from keras.layers import Dense
    from keras.layers import GRU
    from keras.layers import SimpleRNN
    model = Sequential()
    model.add(Embedding(max_features, 32))
    model.add(SimpleRNN(32))
    model.add(Dense(1, activation='sigmoid'))
    
    model.compile(optimizer='rmsprop',
                  loss='binary_crossentropy',
                  metrics=['acc'])
    
    history = model.fit(train_data, y_train,
                        epochs=10,
                        batch_size=128,
                        validation_split=0.2)
    
  4. 绘制验证和训练准确度及损失。

    import matplotlib.pyplot as plt
    
    def plot_results(history):
        acc = history.history['acc']
        val_acc = history.history['val_acc']
        loss = history.history['loss']
        val_loss = history.history['val_loss']
    
        epochs = range(1, len(acc) + 1)
        plt.plot(epochs, acc, 'bo', label='Training Accuracy')
        plt.plot(epochs, val_acc, 'b', label='Validation Accuracy')
    
        plt.title('Training and validation Accuracy')
        plt.legend()
        plt.figure()
        plt.plot(epochs, loss, 'bo', label='Training Loss')
        plt.plot(epochs, val_loss, 'b', label='Validation Loss')
        plt.title('Training and validation Loss')
        plt.legend()
        plt.show()
    
  5. 绘制模型图。

    plot_results(history)
    

    输出如下:

图 6.29: 训练和验证准确度损失

](https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-nlp/img/C13783_06_29.jpg)

图 6.29: 训练和验证准确度损失

活动 8:使用您选择的数据集训练自己的字符生成模型。

解决方案:

  1. 加载文本文件,并导入必要的 Python 包和类。

    import sys
    import random
    import string
    import numpy as np
    from keras.models import Sequential
    from keras.layers import Dense
    from keras.layers import LSTM, GRU
    from keras.optimizers import RMSprop
    from keras.models import load_model
    # load text
    def load_text(filename):
        with open(filename, 'r') as f:
            text = f.read()
        return text
    in_filename = 'drive/shakespeare_poems.txt' # Add your own text file here
    text = load_text(in_filename)
    print(text[:200])
    

    输出如下:

    图 6.30: 莎士比亚的十四行诗

    图 6.30: 莎士比亚的十四行诗
  2. 创建字典,将字符映射到索引,并反向映射。

    chars = sorted(list(set(text)))
    print('Number of distinct characters:', len(chars))
    char_indices = dict((c, i) for i, c in enumerate(chars))
    indices_char = dict((i, c) for i, c in enumerate(chars))
    

    输出如下:

    图 6.31: 不同字符计数

    图 6.31: 不同字符计数
  3. 从文本中创建序列。

    max_len_chars = 40
    step = 3
    sentences = []
    next_chars = []
    for i in range(0, len(text) - max_len_chars, step):
        sentences.append(text[i: i + max_len_chars])
        next_chars.append(text[i + max_len_chars])
    print('nb sequences:', len(sentences))
    

    输出如下:

    图 6.32: 序列计数

    图 6.32: 序列计数
  4. 创建输入和输出数组,以供模型使用。

    x = np.zeros((len(sentences), max_len_chars, 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
    
  5. 使用 GRU 构建并训练模型,并保存该模型。

    print('Build model...')
    model = Sequential()
    model.add(GRU(128, input_shape=(max_len_chars, len(chars))))
    model.add(Dense(len(chars), activation='softmax'))
    optimizer = RMSprop(lr=0.01)
    model.compile(loss='categorical_crossentropy', optimizer=optimizer)
    model.fit(x, y,batch_size=128,epochs=10)
    model.save("poem_gen_model.h5")
    
  6. 定义采样和生成函数。

    def sample(preds, temperature=1.0):
        # helper function to sample an index from a probability array
        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)
    
  7. 生成文本。

    from keras.models import load_model
    model_loaded = load_model('poem_gen_model.h5')
    def generate_poem(model, num_chars_to_generate=400):
        start_index = random.randint(0, len(text) - max_len_chars - 1)
        generated = ''
        sentence = text[start_index: start_index + max_len_chars]
        generated += sentence
        print("Seed sentence: {}".format(generated))
        for i in range(num_chars_to_generate):
            x_pred = np.zeros((1, max_len_chars, len(chars)))
            for t, char in enumerate(sentence):
                x_pred[0, t, char_indices[char]] = 1.
    
            preds = model.predict(x_pred, verbose=0)[0]
            next_index = sample(preds, 1)
            next_char = indices_char[next_index]
            generated += next_char
            sentence = sentence[1:] + next_char
        return generated
    generate_poem(model_loaded, 100)
    

    输出如下:

图 6.33: 生成的文本输出

](https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-nlp/img/C13783_06_33.jpg)

图 6.33:生成的文本输出

第七章:LSTM 基础

活动 9:使用简单的 RNN 构建垃圾邮件或正常邮件分类器。

解决方案:

  1. 导入所需的 Python 包。

    import pandas as pd
    import numpy as np
    from keras.models import Model, Sequential
    from keras.layers import SimpleRNN, Dense,Embedding
    from keras.preprocessing.text import Tokenizer
    from keras.preprocessing import sequence
    
  2. 读取输入文件,文件中包含一列文本和另一列标签,标签表示该文本是否为垃圾邮件。

    df = pd.read_csv("drive/spam.csv", encoding="latin")
    df.head()
    

    输出如下:

    图 7.35:输入数据文件

    图 7.35:输入数据文件
  3. 标注输入数据中的列。

    df = df[["v1","v2"]]
    df.head()
    

    输出如下:

    图 7.36:标注的输入数据

    图 7.36:标注的输入数据
  4. 计算v1列中垃圾邮件和正常邮件字符的数量。

    df["v1"].value_counts()
    

    输出如下:

    图 7.37:垃圾邮件或正常邮件的值计数

    图 7.37:垃圾邮件或正常邮件的值计数
  5. 获取X作为特征,Y作为目标。

    lab_map = {"ham":0, "spam":1}
    X = df["v2"].values
    Y = df["v1"].map(lab_map).values
    
  6. 转换为序列并填充序列。

    max_words = 100
    mytokenizer = Tokenizer(nb_words=max_words,lower=True, split=" ")
    mytokenizer.fit_on_texts(X)
    text_tokenized = mytokenizer.texts_to_sequences(X)
    text_tokenized
    

    输出如下:

    图 7.38:分词数据

    图 7.38:分词数据
  7. 训练序列。

    max_len = 50
    sequences = sequence.pad_sequences(text_tokenized,maxlen=max_len)
    sequences
    
  8. 构建模型。

    model = Sequential()
    model.add(Embedding(max_words, 20, input_length=max_len))
    model.add(SimpleRNN(64))
    model.add(Dense(1, activation="sigmoid"))
    model.compile(loss='binary_crossentropy',
                  optimizer='adam',
                  metrics=['accuracy'])
    model.fit(sequences,Y,batch_size=128,epochs=10,
              validation_split=0.2)
    
  9. 对新测试数据预测邮件类别。

    inp_test_seq = "WINNER! U win a 500 prize reward & free entry to FA cup final tickets! Text FA to 34212 to receive award"
    test_sequences = mytokenizer.texts_to_sequences(np.array([inp_test_seq]))
    test_sequences_matrix = sequence.pad_sequences(test_sequences,maxlen=max_len)
    model.predict(test_sequences_matrix)
    

    输出如下:

图 7.39:新测试数据的输出

图 7.39:新测试数据的输出

活动 10:创建法语到英语的翻译模型

解决方案:

  1. 导入必要的 Python 包和类。

    import os
    import re
    import numpy as np
    
  2. 以句子对的形式读取文件。

    with open("fra.txt", 'r', encoding='utf-8') as f:
        lines = f.read().split('\n')
    num_samples = 20000 # Using only 20000 pairs for this example
    lines_to_use = lines[: min(num_samples, len(lines) - 1)]
    
  3. 移除\u202f字符。

    for l in range(len(lines_to_use)):
        lines_to_use[l] = re.sub("\u202f", "", lines_to_use[l])
    for l in range(len(lines_to_use)):
        lines_to_use[l] = re.sub("\d", " NUMBER_PRESENT ", lines_to_use[l])
    
  4. 在目标序列中附加**BEGIN_** **_END**词,将词映射到整数。

    input_texts = []
    target_texts = []
    input_words = set()
    target_words = set()
    for line in lines_to_use:
        target_text, input_text = line.split('\t')
        target_text = 'BEGIN_ ' + target_text + ' _END'
        input_texts.append(input_text)
        target_texts.append(target_text)
        for word in input_text.split():
            if word not in input_words:
                input_words.add(word)
        for word in target_text.split():
            if word not in target_words:
                target_words.add(word)
    max_input_seq_length = max([len(i.split()) for i in input_texts])
    max_target_seq_length = max([len(i.split()) for i in target_texts])
    input_words = sorted(list(input_words))
    target_words = sorted(list(target_words))
    num_encoder_tokens = len(input_words)
    num_decoder_tokens = len(target_words)
    
  5. 定义编码器-解码器输入。

    input_token_index = dict(
        [(word, i) for i, word in enumerate(input_words)])
    target_token_index = dict(
        [(word, i) for i, word in enumerate(target_words)])
    encoder_input_data = np.zeros(
        (len(input_texts), max_input_seq_length),
        dtype='float32')
    decoder_input_data = np.zeros(
        (len(target_texts), max_target_seq_length),
        dtype='float32')
    decoder_target_data = np.zeros(
        (len(target_texts), max_target_seq_length, num_decoder_tokens),
        dtype='float32')
    for i, (input_text, target_text) in enumerate(zip(input_texts, target_texts)):
        for t, word in enumerate(input_text.split()):
            encoder_input_data[i, t] = input_token_index[word]
        for t, word in enumerate(target_text.split()):
            decoder_input_data[i, t] = target_token_index[word]
            if t > 0:
                # decoder_target_data is ahead of decoder_input_data #by one timestep
                decoder_target_data[i, t - 1, target_token_index[word]] = 1.
    
  6. 构建模型。

    from keras.layers import Input, LSTM, Embedding, Dense
    from keras.models import Model
    embedding_size = 50
    
  7. 初始化编码器训练。

    encoder_inputs = Input(shape=(None,))
    encoder_after_embedding =  Embedding(num_encoder_tokens, embedding_size)(encoder_inputs)
    encoder_lstm = LSTM(50, return_state=True)_, 
    state_h, state_c = encoder_lstm(encoder_after_embedding)
    encoder_states = [state_h, state_c]
    
  8. 初始化解码器训练。

    decoder_inputs = Input(shape=(None,))
    decoder_after_embedding = Embedding(num_decoder_tokens, embedding_size)(decoder_inputs)
    decoder_lstm = LSTM(50, return_sequences=True, return_state=True)
    decoder_outputs, _, _ = decoder_lstm(decoder_after_embedding,
                                         initial_state=encoder_states)
    decoder_dense = Dense(num_decoder_tokens, activation='softmax')
    decoder_outputs = decoder_dense(decoder_outputs)
    
  9. 定义最终模型。

    model = Model([encoder_inputs, decoder_inputs], decoder_outputs)
    model.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['acc'])
    model.fit([encoder_input_data, decoder_input_data], 
              decoder_target_data,
              batch_size=128,
              epochs=20,
              validation_split=0.05)
    
  10. 将推理提供给编码器和解码器。

    # encoder part
    encoder_model = Model(encoder_inputs, encoder_states)
    # decoder part
    decoder_state_input_h = Input(shape=(50,))
    decoder_state_input_c = Input(shape=(50,))
    decoder_states_inputs = [decoder_state_input_h, decoder_state_input_c]
    decoder_outputs_inf, state_h_inf, state_c_inf = decoder_lstm(decoder_after_embedding, initial_state=decoder_states_inputs)
    decoder_states_inf = [state_h_inf, state_c_inf]
    decoder_outputs_inf = decoder_dense(decoder_outputs_inf)
    decoder_model = Model(
        [decoder_inputs] + decoder_states_inputs,
        [decoder_outputs_inf] + decoder_states_inf)
    
  11. 使用反向查找标记索引解码序列。

    reverse_input_word_index = dict(
        (i, word) for word, i in input_token_index.items())
    reverse_target_word_index = dict(
        (i, word) for word, i in target_token_index.items())
    def decode_sequence(input_seq):
    
  12. 将输入编码为状态向量。

        states_value = encoder_model.predict(input_seq)
    
  13. 生成长度为 1 的空目标序列。

        target_seq = np.zeros((1,1))
    
  14. 用开始字符填充目标序列的第一个字符。

        target_seq[0, 0] = target_token_index['BEGIN_']
    
  15. 为一批序列进行采样循环。

    stop_condition = False
        decoded_sentence = ''
    
        while not stop_condition:
            output_tokens, h, c = decoder_model.predict(
                [target_seq] + states_value)
    
  16. 采样一个标记。

            sampled_token_index = np.argmax(output_tokens)
            sampled_word = reverse_target_word_index[sampled_token_index]
            decoded_sentence += ' ' + sampled_word
    
  17. 退出条件:达到最大长度或找到停止字符。

            if (sampled_word == '_END' or
               len(decoded_sentence) > 60):
                stop_condition = True
    
  18. 更新目标序列(长度为 1)。

            target_seq = np.zeros((1,1))
            target_seq[0, 0] = sampled_token_index
    
  19. 更新状态。

            states_value = [h, c]
    
        return decoded_sentence
    
  20. 用户输入推理:接受一个词序列,逐个词地将序列转换为编码。

    text_to_translate = "Où est ma voiture??"
    encoder_input_to_translate = np.zeros(
        (1, max_input_seq_length),
        dtype='float32')
    for t, word in enumerate(text_to_translate.split()):
        encoder_input_to_translate[0, t] = input_token_index[word]
    decode_sequence(encoder_input_to_translate)
    

    输出如下:

图 7.47:法语到英语翻译器

图 7.47:法语到英语翻译器

第八章:自然语言处理的最新进展

活动 11:构建文本摘要模型

解决方案:

  1. 导入必要的 Python 包和类。

    import os
    import re
    import pdb
    import string
    import numpy as np
    import pandas as pd
    from keras.utils import to_categorical
    import matplotlib.pyplot as plt
    %matplotlib inline
    
  2. 加载数据集并读取文件。

    path_data = "news_summary_small.csv"
    df_text_file = pd.read_csv(path_data)
    df_text_file.headlines = df_text_file.headlines.str.lower()
    df_text_file.text = df_text_file.text.str.lower()
    lengths_text = df_text_file.text.apply(len)
    dataset = list(zip(df_text_file.text.values, df_text_file.headlines.values))
    
  3. 创建词汇字典。

    input_texts = []
    target_texts = []
    input_chars = set()
    target_chars = set()
    for line in dataset:
        input_text, target_text = list(line[0]), list(line[1])
        target_text = ['BEGIN_'] + target_text + ['_END']
        input_texts.append(input_text)
        target_texts.append(target_text)
    
        for character in input_text:
            if character not in input_chars:
                input_chars.add(character)
        for character in target_text:
            if character not in target_chars:
                target_chars.add(character)
    input_chars.add("<unk>")
    input_chars.add("<pad>")
    target_chars.add("<pad>")
    input_chars = sorted(input_chars)
    target_chars = sorted(target_chars)
    human_vocab = dict(zip(input_chars, range(len(input_chars))))
    machine_vocab = dict(zip(target_chars, range(len(target_chars))))
    inv_machine_vocab = dict(enumerate(sorted(machine_vocab)))
    def string_to_int(string_in, length, vocab):
        """
        Converts all strings in the vocabulary into a list of integers representing the positions of the
        input string's characters in the "vocab"
        Arguments:
        string -- input string
        length -- the number of time steps you'd like, determines if the output will be padded or cut
        vocab -- vocabulary, dictionary used to index every character of your "string"
        Returns:
        rep -- list of integers (or '<unk>') (size = length) representing the position of the string's character in the vocabulary
        """
    
  4. 转换为小写字母以标准化。

        string_in = string_in.lower()
        string_in = string_in.replace(',','')
        if len(string_in) > length:
            string_in = string_in[:length]
        rep = list(map(lambda x: vocab.get(x, '<unk>'), string_in))
        if len(string_in) < length:
            rep += [vocab['<pad>']] * (length - len(string_in))
    
        return rep
    def preprocess_data(dataset, human_vocab, machine_vocab, Tx, Ty):
        X, Y = zip(*dataset)
        X = np.array([string_to_int(i, Tx, human_vocab) for i in X])
        Y = [string_to_int(t, Ty, machine_vocab) for t in Y]
        print("X shape from preprocess: {}".format(X.shape))
        Xoh = np.array(list(map(lambda x: to_categorical(x, num_classes=len(human_vocab)), X)))
        Yoh = np.array(list(map(lambda x: to_categorical(x, num_classes=len(machine_vocab)), Y)))
        return X, np.array(Y), Xoh, Yoh
    def softmax(x, axis=1):
        """Softmax activation function.
        # Arguments
            x : Tensor.
            axis: Integer, axis along which the softmax normalization is applied.
        # Returns
            Tensor, output of softmax transformation.
        # Raises
            ValueError: In case 'dim(x) == 1'.
        """
        ndim = K.ndim(x)
        if ndim == 2:
            return K.softmax(x)
        elif ndim > 2:
            e = K.exp(x - K.max(x, axis=axis, keepdims=True))
            s = K.sum(e, axis=axis, keepdims=True)
            return e / s
        else:
            raise ValueError('Cannot apply softmax to a tensor that is 1D')
    
  5. 运行之前的代码片段以加载数据,获取词汇字典并定义一些稍后使用的工具函数。定义输入字符和输出字符的长度。

    Tx = 460
    Ty = 75
    X, Y, Xoh, Yoh = preprocess_data(dataset, human_vocab, machine_vocab, Tx, Ty)
    Define the model functions (Repeator, Concatenate, Densors, Dotor)
    # Defined shared layers as global variables
    repeator = RepeatVector(Tx)
    concatenator = Concatenate(axis=-1)
    densor1 = Dense(10, activation = "tanh")
    densor2 = Dense(1, activation = "relu")
    activator = Activation(softmax, name='attention_weights')
    dotor = Dot(axes = 1)
    Define one-step-attention function:
    def one_step_attention(h, s_prev):
        """
        Performs one step of attention: Outputs a context vector computed as a dot product of the attention weights
        "alphas" and the hidden states "h" of the Bi-LSTM.
    
        Arguments:
        h -- hidden state output of the Bi-LSTM, numpy-array of shape (m, Tx, 2*n_h)
        s_prev -- previous hidden state of the (post-attention) LSTM, numpy-array of shape (m, n_s)
    
        Returns:
        context -- context vector, input of the next (post-attetion) LSTM cell
        """  
    
  6. 使用repeators_prev重复为形状(mTxn_s),以便可以将其与所有隐藏状态“a”连接。

        s_prev = repeator(s_prev)
    
  7. 使用连接器在最后一个轴上连接as_prev(≈ 1 行)

        concat = concatenator([h, s_prev])
    
  8. 使用densor1通过一个小型全连接神经网络传播concat,以计算“中间能量”变量 e。

        e = densor1(concat)
    
  9. 使用densor2通过一个小型全连接神经网络传播 e,计算“能量”变量 energies。

        energies = densor2(e)  
    
  10. 使用“activator”对“能量”计算注意力权重“alphas

        alphas = activator(energies)
    
  11. 使用dotor与“alphas”和“a”一起计算要传递给下一个(后注意力)LSTM 单元的上下文向量

        context = dotor([alphas, h])
    
        return context
    Define the number of hidden states for decoder and encoder.
    n_h = 32
    n_s = 64
    post_activation_LSTM_cell = LSTM(n_s, return_state = True)
    output_layer = Dense(len(machine_vocab), activation=softmax)
    Define the model architecture and run it to obtain a model.
    def model(Tx, Ty, n_h, n_s, human_vocab_size, machine_vocab_size):
        """
        Arguments:
        Tx -- length of the input sequence
        Ty -- length of the output sequence
        n_h -- hidden state size of the Bi-LSTM
        n_s -- hidden state size of the post-attention LSTM
        human_vocab_size -- size of the python dictionary "human_vocab"
        machine_vocab_size -- size of the python dictionary "machine_vocab"
        Returns:
        model -- Keras model instance
        """
    
  12. 定义模型的输入,形状为(Tx,)

  13. 定义s0c0,解码器 LSTM 的初始隐藏状态,形状为(n_s,)

        X = Input(shape=(Tx, human_vocab_size), name="input_first")
        s0 = Input(shape=(n_s,), name='s0')
        c0 = Input(shape=(n_s,), name='c0')
        s = s0
        c = c0
    
  14. 初始化空的输出列表

        outputs = []
    
  15. 定义你的前注意力 Bi-LSTM。记得使用 return_sequences=True。

        a = Bidirectional(LSTM(n_h, return_sequences=True))(X)
    
        # Iterate for Ty steps
        for t in range(Ty):
    
            # Perform one step of the attention mechanism to get back the context vector at step t
            context = one_step_attention(h, s)      
    
  16. 将后注意力 LSTM 单元应用于“上下文”向量。

            # Pass: initial_state = [hidden state, cell state]
            s, _, c = post_activation_LSTM_cell(context, initial_state = [s,c])  
    
  17. Dense层应用于后注意力 LSTM 的隐藏状态输出

            out = output_layer(s)    
    
  18. 将“out”附加到“outputs”列表中

            outputs.append(out)
    
  19. 创建模型实例,接受三个输入并返回输出列表。

        model = Model(inputs=[X, s0, c0], outputs=outputs)
    
        return model
    model = model(Tx, Ty, n_h, n_s, len(human_vocab), len(machine_vocab))
    #Define model loss functions and other hyperparameters. Also #initialize decoder state vectors.
    opt = Adam(lr = 0.005, beta_1=0.9, beta_2=0.999, decay = 0.01)
    model.compile(loss='categorical_crossentropy', optimizer=opt, metrics=['accuracy'])
    s0 = np.zeros((10000, n_s))
    c0 = np.zeros((10000, n_s))
    outputs = list(Yoh.swapaxes(0,1))
    Fit the model to our data:
    model.fit([Xoh, s0, c0], outputs, epochs=1, batch_size=100)
    #Run inference step for the new text.
    EXAMPLES = ["Last night a meteorite was seen flying near the earth's moon."]
    for example in EXAMPLES:
    
        source = string_to_int(example, Tx, human_vocab)
        source = np.array(list(map(lambda x: to_categorical(x, num_classes=len(human_vocab)), source)))
        source = source[np.newaxis, :]
        prediction = model.predict([source, s0, c0])
        prediction = np.argmax(prediction, axis = -1)
        output = [inv_machine_vocab[int(i)] for i in prediction]
    
        print("source:", example)
        print("output:", ''.join(output))
    

    输出如下:

图 8.18:文本摘要模型输出

](https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-nlp/img/C13783_08_18.jpg)

图 8.18:文本摘要模型输出

第九章:组织中的实际 NLP 项目工作流程

LSTM 模型的代码

  1. 检查是否检测到 GPU

    import tensorflow as tf
    tf.test.gpu_device_name()
    
  2. 设置领口笔记本

    from google.colab import drive
    drive.mount('/content/gdrive')
    # Run the below command in a new cell
    cd /content/gdrive/My Drive/Lesson-9/
    # Run the below command in a new cell
    !unzip data.csv.zip
    
  3. 导入必要的 Python 包和类。

    import os
    import re
    import pickle
    import pandas as pd
    from keras.preprocessing.text import Tokenizer
    from keras.preprocessing.sequence import pad_sequences
    from keras.models import Sequential
    from keras.layers import Dense, Embedding, LSTM
    
  4. 加载数据文件。

    def preprocess_data(data_file_path):
        data = pd.read_csv(data_file_path, header=None) # read the csv
        data.columns = ['rating', 'title', 'review'] # add column names
        data['review'] = data['review'].apply(lambda x: x.lower()) # change all text to lower
        data['review'] = data['review'].apply((lambda x: re.sub('[^a-zA-z0-9\s]','',x))) # remove all numbers
        return data
    df = preprocess_data('data.csv')
    
  5. 初始化分词。

    max_features = 2000
    maxlength = 250
    tokenizer = Tokenizer(num_words=max_features, split=' ')
    
  6. 拟合分词器。

    tokenizer.fit_on_texts(df['review'].values)
    X = tokenizer.texts_to_sequences(df['review'].values)
    
  7. 填充序列。

    X = pad_sequences(X, maxlen=maxlength)
    
  8. 获取目标变量

    y_train = pd.get_dummies(df.rating).values
    embed_dim = 128
    hidden_units = 100
    n_classes = 5
    model = Sequential()
    model.add(Embedding(max_features, embed_dim, input_length = X.shape[1]))
    model.add(LSTM(hidden_units))
    model.add(Dense(n_classes, activation='softmax'))
    model.compile(loss = 'categorical_crossentropy', optimizer='adam',metrics = ['accuracy'])
    print(model.summary())
    
  9. 拟合模型。

    model.fit(X[:100000, :], y_train[:100000, :], batch_size = 128, epochs=15, validation_split=0.2)
    
  10. 保存模型和分词器。

    model.save('trained_model.h5')  # creates a HDF5 file 'trained_model.h5'
    with open('trained_tokenizer.pkl', 'wb') as f: # creates a pickle file 'trained_tokenizer.pkl'
        pickle.dump(tokenizer, f)
    from google.colab import files
    files.download('trained_model.h5')
    files.download('trained_tokenizer.pkl')
    

Flask 的代码

  1. 导入必要的 Python 包和类。

    import re
    import pickle
    import numpy as np
    from flask import Flask, request, jsonify
    from keras.models import load_model
    from keras.preprocessing.sequence import pad_sequences
    
  2. 定义输入文件并加载到数据框中

    def load_variables():
        global model, tokenizer
        model = load_model('trained_model.h5')
        model._make_predict_function()  # https://github.com/keras-team/keras/issues/6462
        with open('trained_tokenizer.pkl',  'rb') as f:
            tokenizer = pickle.load(f)
    
  3. 定义类似于训练代码的预处理函数:

    def do_preprocessing(reviews):
        processed_reviews = []
        for review in reviews:
            review = review.lower()
            processed_reviews.append(re.sub('[^a-zA-z0-9\s]', '', review))
        processed_reviews = tokenizer.texts_to_sequences(np.array(processed_reviews))
        processed_reviews = pad_sequences(processed_reviews, maxlen=250)
        return processed_reviews
    
  4. 定义 Flask 应用实例:

    app = Flask(__name__)
    
  5. 定义一个显示固定消息的端点:

    @app.route('/')
    def home_routine():
        return 'Hello World!'
    
  6. 我们将拥有一个预测端点,通过它可以发送我们的评论字符串。我们将使用的 HTTP 请求类型是'POST'请求:

    @app.route('/prediction', methods=['POST'])
    def get_prediction():
      # get incoming text
      # run the model
        if request.method == 'POST':
            data = request.get_json()
        data = do_preprocessing(data)
        predicted_sentiment_prob = model.predict(data)
        predicted_sentiment = np.argmax(predicted_sentiment_prob, axis=-1)
        return str(predicted_sentiment)
    
  7. 启动 Web 服务器。

    if __name__ == '__main__':
      # load model
      load_variables()
      app.run(debug=True)
    
  8. 将此文件保存为app.py(可以使用任何名称)。通过终端使用app.py运行此代码:

    python app.py
    

    输出如下:

图 9.31:Flask 输出

图 9.31:Flask 输出
posted @ 2025-07-13 15:43  绝不原创的飞龙  阅读(89)  评论(0)    收藏  举报