Python 自然语言处理实用指南:第一、二部分

2023-04-27 15:19:13 浏览数 (3)

第一部分:用于 NLP 的 PyTorch 1.x 的要点

在本节中,您将在自然语言处理NLP)的背景下了解 PyTorch 1.x 的基本概念。 您还将学习如何在计算机上安装 PyTorch 1.x,以及如何使用 CUDA 加快处理速度。

本节包含以下章节:

  • “第 1 章”,“机器学习和深度学习基础知识”
  • “第 2 章”,“NLP 的 PyTorch 1.x 入门”

一、机器学习和深度学习的基础

我们的世界拥有丰富的自然语言数据。 在过去的几十年中,我们彼此之间的通信方式已经转变为数字领域,因此,这些数据可用于构建可改善我们在线体验的模型。 从在搜索引擎中返回相关结果,到自动完成您在电子邮件中输入的下一个单词,从自然语言中提取见解的好处显而易见。

尽管我们作为人类的语言理解方式与模型或人工智能理解语言的方式明显不同,但通过阐明机器学习及其用途,我们可以开始理解,这些深度学习模型如何理解语言,以及模型从数据中学习时发生的根本变化。

在本书中,我们将探讨人工智能和深度学习对自然语言的这种应用。 通过使用 PyTorch,我们将逐步学习如何构建模型,使我们能够执行情感分析,文本分类和序列翻译,从而使我们构建一个基本的聊天机器人。 通过介绍这些模型背后的理论,并演示如何实际实现它们,我们将使自然语言处理NLP)的领域神秘化,并为您提供足够的背景知识,让您开始构建自己的模型。

在第一章中,我们将探讨机器学习的一些基本概念。 然后,我们将通过研究深度学习,神经网络的基础知识以及深度学习方法相对于基本机器学习技术所具有的一些优势,将这一步骤进一步向前发展。 最后,我们将更深入地研究深度学习,特别是针对特定于 NLP 的任务,以及我们如何使用深度学习模型从自然语言中获得见解。 具体来说,我们将涵盖以下主题:

  • 机器学习概述
  • 神经网络概述
  • 用于机器学习的 NLP

机器学习概述

从根本上讲,机器学习是用于识别模式并从数据中提取趋势的算法过程。 通过在数据上训练特定的机器学习算法,机器学习模型可以学习对人眼不是立即显而易见的见解。 医学成像模型可能会学会从人体图像中检测出癌症,而情感分析模型可能会学习,与包含不好糟糕无聊的书评相比,包含良好优秀有意思的书评更可能是正面的。

广义上讲,机器学习算法分为两大类:监督学习和无监督学习。

监督学习

监督学习涵盖了所有我们希望使用输入来预测输出的任务。 假设我们希望训练一个模型来预测房价。 我们知道较大的房屋往往会卖出更多的钱,但我们不知道价格和面积之间的确切关系。 机器学习模型可以通过查看数据来学习这种关系:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gSOUOQUW-1681785734227)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_01_1.jpg)]

图 1.1 –显示住房数据的表格

在这里,我们已经得到的信息,包括最近出售的四栋房屋的大小,以及他们出售的的价格。 根据这四所房屋的数据,我们能否使用此信息对市场上的新房屋做出预测? 一个简单的称为回归的机器学习模型可以估计以下两个因素之间的关系:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6u1YTGKf-1681785734228)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_01_2.jpg)]

图 1.2 –外壳数据输出

给定此历史数据,我们可以使用此数据来估计大小X)和价格Y)之间的关系。 现在我们已经估计了大小和价格之间的关系,如果给了我们只知道房子大小的新房子,我们可以使用学习的函数使用它来预测价格:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l9h7W8HK-1681785734229)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_01_3.jpg)]

图 1.3 –预测房价

因此,给定许多输入与输出之间的关系的示例,所有监督学习任务旨在学习模型输入的某些函数以预测输出:

给定很多(X, y),请学习F(X) = y

您电话号码的输入可以包含任意数量的特征。 我们的简单房价模型仅包含一个特征(大小),但我们可能希望添加更多特征以提供更好的预测(例如,卧室数量,花园大小等) 上)。 因此,更具体地说,我们的监督模型学习了一个函数,以便将许多输入映射到输出。 由以下等式给出:

给定许多([X0, X1, X2, …, Xn], y),学习f(X0, X1, X2, …, Xn) = y

在前面的示例中,我们学习的函数如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a3dNJFNw-1681785734229)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/Formula_01_001.jpg)]

这里,θ[0]x轴截距,θ[1]是直线的斜率。

模型可以包含数百万甚至数十亿个输入特征的(尽管当特征空间太大时,您可能会遇到硬件限制)。 模型的输入类型也可能有所不同,模型可以从图像中学习:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-640SsfKx-1681785734229)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_01_4.jpg)]

图 1.4 –模型训练

正如我们稍后将更详细地研究一样,他们也可以从文本中学习:

我喜欢这部电影 -> 好评

这部电影很糟糕 -> 差评

我今年看过的最好的电影 -> ?

无监督学习

非监督学习 与监督学习的不同之处在于非监督学习不使用输入和输出对(X, y)进行学习。 相反,我们仅提供输入数据,模型将学习有关输入数据的结构或表示的知识。 无监督学习的最常见方法之一是聚类

例如,我们获取了来自四个不同国家/地区的温度和降雨测度读数的数据集,但没有关于这些读数取自何处的标签。 我们可以使用聚类算法来识别数据中存在的不同聚类(国家):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jFqafg43-1681785734229)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_01_5.jpg)]

图 1.5 –聚类算法的输出

群集在 NLP 领域中也有用途。 如果为我们提供了电子邮件的数据集,并想确定这些电子邮件中使用了多少种不同的语言,则一种集群形式可以帮助我们识别这一点。 如果英语单词在同一封电子邮件中与其他英语单词一起频繁出现,而西班牙语单词与其他西班牙语单词一起频繁出现,我们将使用聚类确定数据集有多少个不同的单词聚类,从而确定语言的数量。

模型如何学习?

为了学习模型,我们需要某种评估模型表现的方法。 为此,我们使用称为损失的概念。 损失是衡量如何根据其真实值接近模型预测的一种度量。 对于我们数据集中的给定房屋,损失的一种度量可能是真实价格(y)与我们的模型预测的价格(y_hat)之间的差。 我们可以通过对数据集中所有房屋的平均损失进行评估,从而评估系统中的总损失。 但是,从理论上讲,正损失可以抵消负损失,因此,更常见的损失度量是均方误差

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HpsIHBue-1681785734230)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/Formula_01_005.png)]

虽然其他模型可能使用不同的损失函数,但回归通常使用均方误差。 现在,我们可以计算整个数据集的损失量度,但是我们仍然需要方法,以算法的方式实现尽可能低的损失。 此过程称为梯度下降

梯度下降

在这里,我们绘制了损失函数,因为它与房价模型θ[1]中的单个学习参数有关。 我们注意到,当θ[1]设置得太高时,MSE 损失就很高;而当θ[1]设置得太低时,MSE 损失也就很高。 最佳点或损失最小的点位于中间位置。 为了计算该算法,我们使用梯度下降。 当我们开始训练自己的神经网络时,我们将更详细地看到这一点:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ygkTtr1H-1681785734230)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_01_6.jpg)]

图 1.6 –梯度下降

我们首先用随机值初始化θ[1]。 为了使损失最小化,我们需要从损失函数进一步下移,到达山谷的中部。 为此,我们首先需要知道向哪个方向移动。在我们的初始点,我们使用基本演算来计算坡度的初始坡度:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ws2FR3b9-1681785734230)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/Formula_01_010.png)]

在我们前面的示例中,初始点处的梯度为正。 这表明我们的θ[1]值大于最佳值,因此我们更新了θ[1]的值,使其低于我们先前的的值。 我们逐步迭代此过程,直到θ[1]越来越接近 MSE 最小化的值。 这发生在梯度等于零的点。

过拟合和欠拟合

考虑以下场景,其中基本线性模型无法很好地拟合到我们的数据。 我们可以看到,我们的模型(由方程y = θ[0] θ[1] x表示)似乎不是很好的预测指标:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iKZZCdmV-1681785734230)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_01_7.jpg)]

图 1.7 –欠拟合和过拟合的示例

当我们的模型由于缺乏特征,数据不足或模型规格不足而无法很好地拟合数据时,我们将其称为,欠拟合。 我们注意到我们数据的梯度越来越大,并怀疑如果使用多项式,则模型可能更合适。 例如y = θ[0] θ[1] x θ[2] x^2。 稍后我们将看到,由于神经网络的复杂结构,欠拟合很少成为问题:

考虑以下示例。 在这里,我们使用房价模型拟合的函数不仅适用于房屋的大小(X),而且适用于二阶和三阶多项式(X2, X3)。 在这里,我们可以看到我们的新模型非常适合我们的数据点。 但是,这不一定会产生良好的模型:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-83LoaLre-1681785734230)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_01_8.jpg)]

图 1.8 –过拟合的样本输出

我们现在有一个房子,其大小为 110 平方米,以预测价格。 根据我们的直觉,由于该房屋比 100 平方米房屋大,我们希望这所房屋的价格会更高,大约 340,000 美元。 使用我们的拟合多项式模型,我们可以看到预测的价格实际上低于小房子,约为 320,000 美元。 我们的模型适合我们训练过的数据,但不能很好地推广到一个新的,看不见的数据点。 这就是,称为过拟合。 由于过拟合,重要的是不要根据训练的数据评估模型的表现,因此我们需要生成单独的一组数据以评估我们的数据。

训练与测试

通常,在训练模型时,我们将数据分为两部分:训练数据集和较小的测试数据集。 我们使用训练数据集训练模型,并在测试数据集上对其进行评估。 这样做是为了在看不见的数据集上衡量模型的表现。 如前所述,要使模型成为良好的预测指标,必须将其很好地推广到该模型之前从未见过的一组新数据,而这恰恰是对一组测试数据进行评估所得出的结果。

评估模型

尽管我们试图将模型中的损失降到最低,但仅此一项并不能给我们太多有关模型在实际进行预测方面的优势的信息。 考虑一个反垃圾邮件模型,该模型可以预测收到的电子邮件是否为垃圾邮件,并自动将垃圾邮件发送到垃圾文件夹。 评估表现的一种简单方法是准确率

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dcZRikoa-1681785734231)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/Formula_01_016.png)]

为了计算准确率,我们只需将正确预测为垃圾邮件/非垃圾邮件的电子邮件数量除以我们做出的预测总数即可。 如果我们正确地预测了 1,000 封电子邮件中的 990 封电子邮件,则我们的准确率为 99%。 但是,高精度不一定意味着我们的模型是好的:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ei7ZKy7I-1681785734231)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_01_9.jpg)]

图 1.9 –该表显示了被预测为垃圾邮件/非垃圾邮件的数据

在这里,我们可以看到,尽管我们的模型正确地预测了 990 封电子邮件不是垃圾邮件(称为真实否定邮件),但它也预测了 10 封属于垃圾邮件的邮件被视为非垃圾邮件(称为错误负面邮件)。 我们的模型仅假设所有电子邮件都不是垃圾邮件,这根本不是一个很好的反垃圾邮件过滤器! 我们不仅应该使用准确率,还应该使用精度和召回评估模型。 在这种情况下,我们的模型的召回率为零(意味着未返回正结果)将立即成为危险信号:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h52WNhgg-1681785734231)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/Formula_01_017.jpg)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JHkoIMCP-1681785734231)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/Formula_01_018.jpg)]

神经网络

在前面的示例中,我们主要讨论了y = θ[0] θ[1] x形式的回归。 我们接触过使用多项式来拟合更复杂的方程式,例如y = θ[0] θ[1] x θ[2] x。 但是,随着我们向模型中添加更多特征,何时使用原始特征的转换成为反复试验的案例。 使用神经网络,我们可以将更复杂的函数y = f(x)拟合到我们的数据中,而无需设计或转换我们现有的特征。

神经网络的结构

当我们学习θ[1]的最优值时,该最优值将回归中的损失降到最低,这实际上与一层神经网络相同:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-It8hY8XN-1681785734232)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_01_10.jpg)]

图 1.10 –一层神经网络

在这里,我们将每个特征X[i]作为输入,在此通过节点进行说明。 我们希望学习参数θ[i],在此图中将其表示为连接。 我们对X[i]θ[i]之间所有乘积的最终总和为我们提供了最终预测y

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pRLhQQsN-1681785734232)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/Formula_01_026.png)]

一个简单的神经网络建立在这个初始概念的基础上,在计算中增加了额外的层,从而增加了复杂性和学习到的参数,使我们得到了类似的东西:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5VeeFuek-1681785734232)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_01_11.jpg)]

图 1.11 –全连接网络

每个输入节点都连接到另一层中的每个节点。 这被称为全连接层。 然后,将来自的全连接层的输出乘以其自身的附加权重,以便预测y。 因此,我们的预测不再只是X[i]θ[i]的函数,而是现在包括针对每个参数的多个学习权重。 特征X[1]不再仅受θ[1]影响。 现在,它也受到θ[1,1], θ[2,1], θ[2,2], θ[2,3]的影响。 参数。

由于全连接层中的每个节点都将X的所有值作为输入,因此神经网络能够学习输入特征之间的交互特征。 多个全连接层可以链接在一起,以学习更复杂的特征。 在本书中,我们将看到我们构建的所有神经网络都将使用该概念。 将不同品种的多层链接在一起,以构建更复杂的模型。 但是,在我们完全理解神经网络之前,还有另外一个关键元素要涵盖:激活函数。

激活函数

虽然将各种权重链接在一起可以使我们学习更复杂的参数,但最终,我们的最终预测仍将是权重和特征的线性乘积的组合。 如果希望神经网络学习一个真正复杂的非线性函数,则必须在模型中引入非线性元素。 这可以通过使用激活函数来完成:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PU19uCon-1681785734232)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_01_12.jpg)]

图 1.12 –神经网络中的激活函数

我们将激活函数应用于全连接层中的每个节点。 这意味着全连接层中的每个节点都将特征和权重之和作为输入,对结果值应用非线性函数,并输出转换后的结果。 尽管激活函数有许多不同,但最近使用最频繁的是 ReLU整流线性单元

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WGFSg4aP-1681785734232)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_01_13.jpg)]

图 1.13 – ReLU 输出的表示

ReLU 是非常简单的非线性函数,当x <= 0,返回y = 0;当x > 0时返回y = x。 在将这些激活函数引入我们的模型后,我们最终的学习函数将变为非线性,这意味着我们可以创建比单独使用传统回归和特征工程相结合的模型更多的模型。

神经网络如何学习?

使用神经网络从数据中学习的行为比使用基本回归学习时的行为稍微复杂一些。 尽管我们仍然像以前一样使用梯度下降,但是我们需要微分的实际损失函数变得更加复杂。 在没有激活函数的单层神经网络中,我们很容易计算损失函数的导数,因为很容易看到随着我们改变每个参数损失函数如何变化。 但是,在具有激活函数的多层神经网络中,这更加复杂。

我们必须首先执行正向传播,即,其中,使用模型的当前状态,我们计算y的预测值,并根据y的真实值来评估它,以便获得损失的度量。 利用这一损失,我们可以在网络中向后移动,计算网络中每个参数的梯度。 这使我们可以知道向哪个方向更新参数,以便使可以更接近损失最小的点。 这被称为反向传播。 我们可以使用链式规则计算相对于每个参数的损失函数的导数:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b7ot36o5-1681785734233)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/Formula_01_032.png)]

此处,o[j]是网络内每个给定节点的输出。 因此,总而言之,在神经网络上执行梯度下降时我们采取的四个主要步骤如下:

  1. 使用您的数据执行正向传播,计算网络的总损失。
  2. 使用反向传播,计算每个参数相对于网络中每个节点损失的梯度。
  3. 更新这些参数的值,朝着使损失最小化的方向发展。
  4. 重复直到收敛。

神经网络中的过拟合

我们看到,在回归的情况下,可以添加太多特征,从而有可能使网络过拟合。 这样一来,模型可以很好地拟合训练数据,但不能很好地推广到看不见的测试数据集。 这是神经网络中的一个普遍问题,因为模型复杂性的提高意味着通常有可能将函数拟合到不一定要泛化的数据训练集中。 以下是数据集每次向前和反向传播(称为周期)后训练和测试数据集上的总损失的图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DdvXxFtx-1681785734233)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_01_14.jpg)]

图 1.14 –测试和训练周期

在这里,我们可以看到随着我们继续训练网络,随着我们向总损失最小化的方向靠近,训练损失会随着时间的推移而减小。 虽然这可以很好地推广到测试数据集,但一段时间后,由于我们的函数过度适合训练集中的数据,测试数据集上的总损失开始增加。 一种解决方案是提前停止。 因为我们希望我们的模型对之前从未见过的数据做出良好的预测,所以我们可以在测试损失最小的时候停止训练我们的模型。 经过全面训练的 NLP 模型可能能够轻松地对以前见过的句子进行分类,但是,对真正了解到某些东西的模型的衡量标准是能够对看不见的数据进行预测。

用于机器学习的 NLP

与人类不同,计算机无法理解文本-至少不能以与我们相同的方式理解文本。 为了创建能够从数据中学习的机器学习模型,我们必须首先学习以计算机能够处理的方式来表示自然语言。

当我们讨论机器学习基础知识时,您可能已经注意到损失函数都处理数值数据,以便能够最大程度地减少损失。 因此,我们希望以数字格式表示文本,这可以构成我们向神经网络输入的基础。 在这里,我们将介绍几种数值表示数据的基本方法。

词袋

表示文本的第一种也是最简单的方法是使用词袋表示。 此方法只对给定句子或文档中的单词进行计数,然后对所有单词进行计数。 然后将这些计数转换为向量,其中向量的每个元素都是语料库中每个单词出现在句子中的次数计数。 语料库是,只是出现在所有要分析的句子/文档中的所有单词。 采取以下两个句子:

代码语言:javascript复制
The cat sat on the mat

The dog sat on the cat

我们可以将每个句子表示为单词数:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PdKmuct9-1681785734233)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_01_15.jpg)]

图 1.15 –字数表

然后,我们可以将它们转换为单个向量:

代码语言:javascript复制
The cat sat on the mat -> [2,1,0,1,1,1]

The dog sat on the cat -> [2,1,1,1,1,0]

然后,该数字表示形式可用作特征向量为X[0], X[1], ... ,X[n]的机器学习模型的输入特征。

序列表示

我们将在本书的后面看到,更复杂的神经网络模型,包括 RNN 和 LSTM,不仅将一个向量作为输入,而且可以采用矩阵形式的整个向量序列。 因此,为了更好地捕获单词的顺序,从而更好地捕获任何句子的含义,我们能够以单热编码的向量序列的形式来表示它:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jE4xJXJw-1681785734233)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_01_16.jpg)]

图 1.16 –单热编码向量

总结

在本章中,我们介绍了机器学习和神经网络的基础知识,以及对在这些模型中使用的文本转换的简要概述。 在下一章中,我们将简要概述 PyTorch 以及如何将其用于构建其中的一些模型。

二、用于 NLP 的 PyTorch 1.x 入门

PyTorch 是基于 Python 的机器学习库。 它包含两个主要功能:通过硬件加速(使用 GPU)有效执行张量操作的能力以及构建深度神经网络的能力。 PyTorch 还使用动态计算图代替静态计算图,这使其与 TensorFlow 等类似库区分开来。 通过演示如何使用张量表示语言以及如何使用神经网络向 NLP 学习,我们将显示这两个功能对于自然语言处理特别有用。

在本章中,我们将向您展示如何在计算机上启动和运行 PyTorch,以及演示其一些关键功能。 然后,在探索 PyTorch 的某些 NLP 功能(例如其执行张量运算的能力)之前,我们将把 PyTorch 与其他深度学习框架进行比较,最后演示如何构建简单的神经网络。 总之,本章将涵盖以下主题:

  • 安装 PyTorch
  • 将 PyTorch 与其他深度学习框架进行比较
  • PyTorch 的 NLP 功能

技术要求

在本章中,需要安装 Python。 建议使用最新版本的 Python(3.6 或更高版本)。 还建议使用 Anaconda 包管理器来安装 PyTorch。 需要 CUDA 兼容 GPU 才能在 GPU 上运行张量操作。 本章的所有代码都可以在这个页面中找到。

安装和使用 PyTorch 1.x

像大多数 Python 包一样,PyTorch 的安装非常简单。 这样做有两种主要方法。 首先,要使用命令行中的pip简单地安装。 只需键入以下命令:

代码语言:javascript复制
pip install torch torchvision

尽管此安装方法很快,但建议使用 Anaconda 进行安装,因为它包括运行 PyTorch 所需的所有依赖项和二进制文件。 此外,稍后将需要 Anaconda 使用 CUDA 在 GPU 上启用训练模型。 可以在 Anaconda 中通过在命令行中输入以下内容来安装 PyTorch:

代码语言:javascript复制
conda install torch torchvision -c pytorch

要检查 PyTorch 是否正常工作,我们可以打开 Jupyter 笔记本并运行一些简单的命令:

要在 PyTorch 中定义一个张量,我们可以执行以下操作。

代码语言:javascript复制
import torch
x = torch.tensor([1.,2.])
print(x)

结果为以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fjE5UvhE-1681785734233)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_02_1.png)]

图 2.1 –张量输出

这表明 PyTorch 中的张量被保存为它们自己的数据类型(与 NumPy 中的数组保存方式相同)。

我们可以使用标准的 Python 运算符来执行乘法等基本操作。

代码语言:javascript复制
x = torch.tensor([1., 2.])
y = torch.tensor([3., 4.])
print(x * y)

结果为以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PndfEnsv-1681785734234)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_02_2.jpg)]

图 2.2 –张量乘法输出

我们也可以从一个张量中选择单个元素,如下。

代码语言:javascript复制
x = torch.tensor([[1., 2.],[5., 3.],[0., 4.]])
print(x[0][1])

结果为以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1iHEbFWv-1681785734234)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_02_3.jpg)]

图 2.3 –张量选择输出

但是,请注意,与 NumPy 数组不同,从张量对象中选择单个元素会返回另一个张量。 为了从张量返回单个值,可以使用.item()函数:

代码语言:javascript复制
print(x[0][1].item())

结果为以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CfzUkZ7c-1681785734234)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_02_4.jpg)]

图 2.4 – .item()函数的输出

张量

在我们继续之前,重要的是您充分了解张量的属性。 张量具有属性,称为阶数,该属性实质上确定张量的维数。 一阶张量是一维张量,等效于向量或数字列表。 2 阶张量是具有二维的张量,等效于矩阵,而 3 阶张量则由三个维度组成。 PyTorch 中张量可以具有的最大阶数没有限制:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bP10Wul0-1681785734234)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_02_5.jpg)]

图 2.5 –张量矩阵

您可以通过键入以下命令来检查任何张量的大小:

代码语言:javascript复制
x.shape

结果为以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yhdqTvEs-1681785734234)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_02_6.jpg)]

图 2.6 –张量形状输出

这表明这是一个3x2张量(阶数 2)。

使用 CUDA 启用 PyTorch 加速

PyTorch 的主要优点之一是它能够通过使用图形处理单元GPU)来实现加速。 深度学习是一种易于并行化的计算任务,这意味着可以将计算分解为较小的任务,并可以在许多较小的处理器中进行计算。 意味着无需在单个 CPU 上执行任务,而是在 GPU 上执行计算更为有效。

GPU 最初是为了有效地渲染图形而创建的,但是由于深度学习在的普及中得到了发展,因此 GPU 被广泛用于同时执行多种计算的能力。 传统的 CPU 可能包含大约四个或八个内核,而 GPU 则包含数百个较小的内核。 由于可以同时在所有这些内核上执行计算,因此 GPU 可以快速减少执行深度学习任务所需的时间。

考虑神经网络内的一次通过。 我们可能会采集少量数据,将其通过我们的网络以获得损失,然后反向传播,根据梯度调整参数。 如果在传统的 CPU 上要处理大量数据,则必须等到批量 1 完成后才能计算批量 2:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Nup9Un1k-1681785734235)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_02_7.jpg)]

图 2.7 –神经网络中的一遍

但是,在 GPU 上,我们可以同时执行所有这些步骤,这意味着不需要批量即可在批量 2 开始之前完成。 我们可以同时计算所有批量的参数更新,然后一次执行所有参数更新(因为结果彼此独立)。 并行方法可以极大地加快机器学习过程:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4Hi5Onig-1681785734235)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_02_8.jpg)]

图 2.8 –并行执行通行证的方法

计算统一设备架构CUDA)是 Nvidia GPU 特有的技术,可在 PyTorch 上实现硬件加速。 为了启用 CUDA,我们首先必须确保我们系统上的图形卡兼容 CUDA。 可在此处找到 CUDA 兼容 GPU 的列表。 如果您具有兼容 CUDA 的 GPU,则可以从此链接安装 CUDA。 我们将使用以下步骤激活它:

首先,为了在 PyTorch 上实际启用 CUDA 支持,您将必须从源代码构建 PyTorch。 有关如何完成此操作的详细信息,请参见以下网址

然后,要在 PyTorch 代码中实际使用 CUDA,我们必须在 Python 代码中键入以下内容。

代码语言:javascript复制
cuda = torch.device('cuda')

这会将我们的默认 CUDA 设备的名称设置为'cuda'

然后,我们可以通过在任何张量操作中手动指定设备参数来执行对这个设备的操作。

代码语言:javascript复制
x = torch.tensor([5., 3.], device=cuda)

另外,我们可以通过调用cuda方法来做到这一点:

代码语言:javascript复制
y = torch.tensor([4., 2.]).cuda()

然后,我们可以运行一个简单的操作,以确保这是正确的工作。

代码语言:javascript复制
x * y

结果为以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Dv05UrIf-1681785734235)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_02_9.jpg)]

图 2.9 –使用 CUDA 的张量乘法输出

由于我们只是在创建张量,因此速度的变化在此阶段不会很明显,但是当以后开始大规模训练模型时,我们将看到使用 CUDA 并行化计算的速度优势。 通过并行训练我们的模型,我们将能够节省大量时间。

将 PyTorch 与其他深度学习框架进行比较

PyTorch 是当今深度学习中使用的主要框架之一。 还存在其他广泛使用的框架,例如 TensorFlow,Theano 和 Caffe 等。 尽管它们在很多方面都非常相似,但是它们的操作方式还是有一些关键的区别。 其中包括:

  • 如何计算模型
  • 计算图的编译方式
  • 创建具有可变层的动态计算图的能力
  • 语法差异

可以说,PyTorch 与其他框架之间的主要区别在于模型本身的计算方式。 PyTorch 使用称为 autograd 的自动微分方法,该方法允许动态定义和执行计算图。 这与其他框架(如 TensorFlow)相反,后者是静态框架。 在这些静态框架中,必须在最终执行之前定义和编译计算图。 尽管使用预编译的模型可以提高生产效率,但在研究和探索项目中却无法提供相同水平的灵活性。

在训练模型之前,诸如 PyTorch 之类的框架无需预先编译计算图。 PyTorch 使用的动态计算图意味着在执行图时对其进行编译,从而可以随时定义图。 动态模型构建方法在 NLP 领域特别有用。 让我们考虑两个我们希望对以下内容进行情感分析的句子:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HzDknbUs-1681785734235)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_02_10.jpg)]

图 2.10 – PyTorch 中的模型构造

我们可以将这些句子中的每一个表示为单个单词向量的序列,然后形成我们对神经网络的输入。 但是,正如我们所看到的,我们每个输入的大小都是。 在固定计算图中,这些变化的输入大小可能是个问题,但是对于 PyTorch 这样的框架,模型能够动态调整以解决输入结构的变化。 这就是为什么 PyTorch 经常被 NLP 相关的深度学习首选的原因之一。

PyTorch 与其他深度学习框架之间的另一个主要区别是语法。 PyTorch 通常是具有 Python 经验的开发人员首选,因为它本质上被认为是非常 Python 的。 PyTorch 与 Python 生态系统的其他方面很好地集成在一起,如果您具有 Python 的先验知识,则非常容易学习。 现在,我们将从头开始编写我们自己的神经网络,以演示 PyTorch 语法。

在 PyTorch 中构建简单的神经网络

现在,我们将逐步在 PyTorch 中逐步构建神经网络。 在这里,我们有一个小的.csv文件,其中包含来自 MNIST 数据集的图像的几个示例。 MNIST 数据集由我们想要尝试分类的 0 到 9 之间的手绘数字组成。 以下是来自 MNIST 数据集的示例,其中包括手绘数字 1:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RXr38M6c-1681785734235)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_02_11.jpg)]

图 2.11 –来自 MNIST 数据集的样本图像

这些图像的尺寸为28x28:总共 784 像素。 我们在train.csv中的数据集由 1,000 幅这些图像组成,每幅图像均由 784 个像素值以及正确的数字分类(在这种情况下为 1)组成。

加载数据

我们将从加载数据开始,如下所示:

首先,我们需要加载我们的训练数据集,如下。

代码语言:javascript复制
train = pd.read_csv("train.csv")
train_labels = train['label'].values
train = train.drop("label",axis=1).values.reshape(len(test),1,28,28)

请注意,我们将输入重塑为[1, 1, 28, 28],是 1,000 张图像的张量,每个图像由28x28像素组成。

接下来,我们将我们的训练数据和训练标签转换为 PyTorch 张量,以便它们可以被输入到神经网络中。

代码语言:javascript复制
X = torch.Tensor(train.astype(float))
y = torch.Tensor(train_labels).long()

注意这两个张量的数据类型。 浮点张量由 32 位浮点数组成,而长张量由 64 位整数组成。 为了使 PyTorch 能够计算梯度,我们的X函数必须为浮点数,而我们的标签必须为该分类模型中的整数(因为我们正在尝试预测 1、2、3 等等),因此 1.5 的预测就没有意义。

构建分类器

接下来,我们可以开始构造实际的神经网络分类器:

代码语言:javascript复制
class MNISTClassifier(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(784, 392)
        self.fc2 = nn.Linear(392, 196)
        self.fc3 = nn.Linear(196, 98)
        self.fc4 = nn.Linear(98, 10)

我们像从 Python PyTorch 中继承nn.Module一样,在 Python 中构建普通类,从而构建分类器。 在我们的__init__方法中,我们定义了神经网络的每一层。 在这里,我们定义了大小可变的全连接线性层。

我们的第一层接受784输入,因为这是我们要分类的每个图像的大小(28x28)。 然后,我们看到一层的输出必须与下一层的输入具有相同的值,这意味着我们的第一个全连接层输出392个单元,而我们的第二层则采用392单元作为输入。 对每一层都重复一次,每次它们具有一半的单元数量,直到我们到达最终的全连接层为止,该层输出10个单元。 这是我们分类层的长度。

我们的网络现在看起来像这样:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kh9VjBHo-1681785734235)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_02_12.jpg)]

图 2.12 –我们的神经网络

在这里,我们可以看到我们的最后一层输出了10个单元。 这是因为我们希望预测每个图像是否为 0 到 9 之间的数字,总共是 10 种不同的可能分类。 我们的输出是长度为10的向量,并且包含图像的 10 种可能值中的每一个的预测。 在进行最终分类时,我们将数值最高的数字分类作为模型的最终预测。 例如,对于给定的预测,我们的模型可能会预测图像类型为 1 的概率为 10%,类型 2 的概率为 10%,类型 3 的概率为 80%。 因此,我们将类型 3 作为预测,因为它以最高概率被预测。

实现丢弃

在我们的MNISTClassifier类的__init__方法中,我们还定义了一种丢弃方法,以帮助规范网络:

代码语言:javascript复制
self.dropout = nn.Dropout(p=0.2)

丢弃是一种规范化我们的神经网络以防止过拟合的方法。 在每个训练周期上,对于已应用丢包的层中的每个节点,都有可能(此处定义为p= 20%)该层内的每个节点将不用于训练/反向传播 。 这意味着,在训练时,我们的网络会针对过拟合变得健壮,因为在训练过程的每次迭代中都不会使用每个节点。 这可以防止我们的网络过于依赖网络中特定节点的预测。

定义正向传播

接下来,我们在分类器中定义正向传播:

代码语言:javascript复制
def forward(self, x):
        x = x.view(x.shape[0], -1)
        x = self.dropout(F.relu(self.fc1(x)))
        x = self.dropout(F.relu(self.fc2(x)))
        x = self.dropout(F.relu(self.fc3(x)))
        x = F.log_softmax(self.fc4(x), dim=1)

分类器中的forward()方法是我们在其中应用激活函数并定义在我们的网络中应用丢弃的位置的方法。 我们的forward()方法定义了输入将通过网络的路径。 首先,它获取我们的输入x,并将其整形以在网络中使用,并将其转换为一维向量。 然后,我们将其通过我们的第一个全连接层,并将其包装在 ReLU 激活函数中,以使其为非线性。 我们也将其包装在我们的丢弃中,如__init__方法中所定义。 我们对网络中的所有其他层重复此过程。

对于我们的最终预测层,我们将其包装在log_softmax层中。 我们将使用它来轻松计算我们的损失函数,如下所示。

设置模型参数

接下来,我们定义我们的模型参数:

代码语言:javascript复制
model = MNISTClassifier()
loss_function = nn.NLLLoss()
opt = optim.Adam(model.parameters(), lr=0.001)

我们将MNISTClassifier类的实例初始化为模型。 我们还将的损失定义为负对数似然损失

代码语言:javascript复制
Loss(y) = -log(y)

假设我们的图像为 7。如果我们以概率 1 预测类别 7,则损失为-log(1) = 0,但是如果我们仅以概率 0.7 预测类别 7,则损失将是-log(0.7) = 0.3。 这意味着我们与正确预测的距离越远,损失就越接近无穷大:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3hMFDrae-1681785734236)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_02_13.jpg)]

图 2.13 –我们网络的损失表示

然后将其汇总到数据集中所有正确的类中,以计算总损失。 请注意,我们在构建分类器时定义了对数 softmax,因为它已经应用了 softmax 函数(将预测输出限制在 0 到 1 之间)并获取了日志。 这意味着log(y)已经被计算出来,因此我们要计算网络上的总损失所需要做的就是计算输出的负和。

我们还将优化器定义为 Adam 优化器。 优化器控制模型中的学习率。 模型的学习率定义了每次训练期间参数更新的大小。 学习率的大小越大,梯度下降期间参数更新的大小越大。 优化器动态控制该学习率,以便在初始化模型时,参数更新很大。 但是,随着模型的学习和向损失最小化点的靠近,优化器将控制学习率,因此参数更新变得更小,可以更精确地定位局部最小值。

训练我们的网络

最后,我们实际上可以开始训练我们的网络了:

首先,创建一个循环,为我们训练的每一个周期运行一次。在这里,我们将在 50 个周期中运行我们的训练循环。我们首先将输入的图像张量和输出的标签张量转化为 PyTorch 变量。一个变量是一个 PyTorch 对象,它包含一个backward()方法,我们可以使用该方法通过我们的网络进行反向传播。

代码语言:javascript复制
for epoch in range(50):
    images = Variable(X)
    labels = Variable(y)

接下来,我们在优化器上调用zero_grad(),将计算出的梯度设为零。在 PyTorch 中,梯度是在每次反向传播时累计计算的。虽然这在某些模型中很有用,例如在训练 RNNs 时,但对于我们的示例,我们希望在每个周期后从头开始计算梯度,因此我们确保在每次通过后将梯度重置为零。

代码语言:javascript复制
opt.zero_grad()

接下来,我们使用模型的当前状态对我们的数据集进行预测。这实际上是我们的正向传播,因为我们然后使用这些预测来计算我们的损失。

代码语言:javascript复制
outputs = model(images)

使用输出和我们数据集的真实标签,我们使用定义的损失函数计算我们模型的总损失,在这种情况下,它是负对数似然。在计算出这个损失后,我们就可以调用backward(),通过网络反推我们的损失。然后,我们使用step()来使用我们的优化器,以便相应地更新我们的模型参数。

代码语言:javascript复制
loss = loss_function(outputs, labels)
loss.backward()
opt.step()

最后,在每个周期完成后,我们打印总损失。我们可以观察到这一点,以确保我们的模型正在学习。

代码语言:javascript复制
print ('Epoch [%d/%d] Loss: %.4f' %(epoch 1, 50, loss.data.item()))

一般而言,我们预计损失会在每个周期减少。 我们的输出将如下所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cjfngzeD-1681785734236)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_02_14.jpg)]

图 2.14 –训练周期

进行预测

现在我们的模型已经过训练,我们可以使用它对看不见的数据进行预测。 我们首先从读取我们的测试数据集(该数据未用于训练我们的模型):

代码语言:javascript复制
test = pd.read_csv("test.csv")
test_labels = test['label'].values
test = test.drop("label",axis=1).values.reshape(len(test),1,28,28)
X_test = torch.Tensor(test.astype(float))
y_test = torch.Tensor(test_labels).long()

在这里,我们执行与加载训练数据集时相同的步骤:重新整形测试数据并将其转换为 PyTorch 张量。 接下来,要使用我们训练过的模型进行预测,我们只需运行以下命令:

代码语言:javascript复制
preds = model(X_test)

与我们在模型中训练数据的正向传播上计算输出的方式相同,现在我们将测试数据传递通过模型并获得预测。 我们可以像这样查看其中一张图像的预测:

代码语言:javascript复制
print(preds[0])

结果为以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5V9Fvgb4-1681785734236)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_02_15.jpg)]

图 2.15 –预测输出

在这里,我们可以看到我们的预测是一个长度为 10 的向量,并且对每个可能的类别(0 到 9 之间的数字)进行了预测。 预测值最高的是我们的模型选择的预测值。 在这种情况下,它是向量的第十个单元,等于数字 9。请注意,由于我们较早使用对数 softmax,因此我们的预测是对数而非原始概率。 要将它们转换回概率,我们可以使用x对其进行转换。

现在,我们可以构造一个摘要DataFrame,其中包含我们的真实测试数据标签以及模型预测的标签:

代码语言:javascript复制
_, predictionlabel = torch.max(preds.data, 1)
predictionlabel = predictionlabel.tolist()
predictionlabel = pd.Series(predictionlabel)
test_labels = pd.Series(test_labels)
pred_table = pd.concat([predictionlabel, test_labels], axis=1)
pred_table.columns =['Predicted Value', 'True Value']
display(pred_table.head())

结果为以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3YJ4fZRs-1681785734236)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_02_16.jpg)]

图 2.16 –预测表

注意torch.max()函数如何自动选择具有最高值的预测。 在这里我们可以看到,基于的少量数据,我们的模型似乎做出了一些不错的预测!

评估我们的模型

既然我们已经从模型中获得了一些预测,我们就可以使用这些预测来评估模型的质量。 如上一章所述,评估模型表现的一种基本方法是准确率。 在这里,我们只是将正确的预测(预测的图像标签等于实际的图像标签)计算为模型做出的预测总数的百分比:

代码语言:javascript复制
preds = len(predictionlabel)
correct = len([1 for x,y in zip(predictionlabel, test_labels)               if x==y])
print((correct/preds)*100)

结果为以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Du2UE17R-1681785734236)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_02_17.jpg)]

图 2.17 –准确率得分

恭喜你! 您的第一个神经网络能够正确识别近 90% 的看不见的数字图像。 随着我们的前进,我们将看到可能会带来更先进的表现。 但是,到目前为止,我们已经证明使用 PyTorch 创建简单的深度神经网络非常简单。 只需几行代码就可以编写代码,从而获得超越基本机器学习模型(例如回归)所能达到的表现。

用于 PyTorch 的 NLP

现在我们已经学习了如何构建神经网络,我们将看到如何使用 PyTorch 为 NLP 构建模型。 在此示例中,我们将创建一个基本的词袋分类器,以对给定句子的语言进行分类。

设置分类器

在此示例中,我们将选择西班牙语和英语的句子:

首先,我们将每个句子拆分成一个单词列表,并将每个句子的语言作为标签。我们取一部分句子来训练我们的模型,并在一边保留一小部分作为我们的测试集。我们这样做是为了在训练完模型后,可以评估模型的表现。

代码语言:javascript复制
("This is my favourite chapter".lower().split(),
"English"),
("Estoy en la biblioteca".lower().split(), "Spanish")

请注意,我们还将每个单词转换为小写,这将阻止单词在单词袋中重复计算。 如果我们有单词book和单词Book,我们希望将它们视为相同的单词,因此将它们转换为小写。

接下来,我们建立我们的单词索引,它只是一个语料库中所有单词的字典,然后为每个单词创建一个唯一的索引值。这可以通过一个简短的for循环轻松完成。

代码语言:javascript复制
word_dict = {}
i = 0
for words, language in training_data   test_data:
    for word in words:
        if word not in word_dict:
            word_dict[word] = i
            i  = 1
print(word_dict)

结果为以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UAS1kGsa-1681785734237)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_02_18.jpg)]

图 2.18 –设置分类器

请注意,在这里,我们遍历了所有训练数据和测试数据。 如果我们只是在训练数据上创建单词索引,则在评估测试集时,我们将拥有在原始训练中看不到的新单词,因此我们将无法创建这些单词的真正的词袋表示形式。

现在,我们以类似于上一节中构建神经网络的方式来构建我们的分类器,即通过构建一个新的类,该类继承自nn.Module

在这里,我们定义分类器,以使其由单个线性层组成,该线性层具有对数 softmax 激活函数,近似于逻辑回归。 通过在此处添加额外的线性层,我们可以轻松地将其扩展到作为神经网络运行,但是单层参数将达到我们的目的。 请密切注意线性层的输入和输出大小:

代码语言:javascript复制
corpus_size = len(word_dict)
languages = 2
label_index = {"Spanish": 0, "English": 1}
class BagofWordsClassifier(nn.Module):  
    def __init__(self, languages, corpus_size):
        super(BagofWordsClassifier, self).__init__()
        self.linear = nn.Linear(corpus_size, languages)
    def forward(self, bow_vec):
        return F.log_softmax(self.linear(bow_vec), dim=1)

输入的长度为corpus_size,这只是我们的语料库中唯一词的总数。 这是因为对模型的每个输入都是一个词袋表示,由每个句子中的单词计数组成,如果给定单词​​未出现在我们的句子中,则计数为 0。 我们的输出大小为 2,这是我们可以预测的语言数量。 我们的最终预测将包括我们的句子是英语的概率与我们的句子是西班牙文的概率,而我们的最终预测是概率最高的那个。

接下来,我们定义一些实用函数。我们首先定义make_bow_vector,它将句子转化为一个词袋的表示。我们首先创建一个由所有零组成的向量。然后,我们对它们进行循环,对于句子中的每一个词,我们将该词在词袋向量中的索引数增加 1。最后,我们使用.view()对这个向量进行重塑,以便进入我们的分类器。

代码语言:javascript复制
def make_bow_vector(sentence, word_index):
    word_vec = torch.zeros(corpus_size)
    for word in sentence:
        word_vec[word_dict[word]]  = 1
    return word_vec.view(1, -1)

同样,我们定义了make_target,它只是简单地取句子(西班牙语或英语)的标签,并返回其相关索引(01)。

代码语言:javascript复制
def make_target(label, label_index):
    return torch.LongTensor([label_index[label]])

现在我们可以创建一个模型的实例,准备进行训练。我们还将我们的损失函数定义为负对数似然,因为我们使用的是对数 softmax 函数,然后定义我们的优化器,以便使用标准的随机梯度下降SGD)。

代码语言:javascript复制
model = BagofWordsClassifier(languages, corpus_size)
loss_function = nn.NLLLoss()
optimizer = optim.SGD(model.parameters(), lr=0.1)

现在,我们准备训练模型。

训练分类器

首先,我们建立一个循环,该循环由希望模型运行的周期数组成。 在这种情况下,我们将选择 100 个周期。

在此循环中,我们首先将梯度归零(否则,PyTorch 会累积计算梯度),然后对于每个句子/标签对,分别将其转换为词袋向量和目标。 然后,通过使数据向前通过模型的当前状态,我们计算出该特定句子对的预测输出。

然后使用此预测,获取我们的预测标签和实际标签,并在两者上调用定义的loss_function,以获取此句子的损失度量。 通过向后调用backward(),我们通过模型反向传播此损失,并在优化器上调用step(),从而更新模型参数。 最后,我们每 10 个训练步骤打印一次损失:

代码语言:javascript复制
for epoch in range(100):
    for sentence, label in training_data:
        model.zero_grad()
        bow_vec = make_bow_vector(sentence, word_dict)
        target = make_target(label, label_index)
        log_probs = model(bow_vec)
        loss = loss_function(log_probs, target)
        loss.backward()
        optimizer.step()
        
    if epoch % 10 == 0:
        print('Epoch: ',str(epoch 1),', Loss: '   str(loss.item()))

结果为以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-71qef2cI-1681785734237)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_02_19.jpg)]

图 2.19 –训练损失

在这里,我们可以看到,随着模型的学习,我们的损失随着时间而减少。 尽管本例中的训练集很小,但是我们仍然可以证明我们的模型学到了一些有用的东西,如下所示:

我们根据未接受模型训练的测试数据中的几句话来评估模型。 在这里,我们首先设置torch.no_grad(),这将停用 autograd 引擎,因为由于我们不再训练模型,因此不再需要计算梯度。 接下来,我们将测试句子转换为词袋向量,并将其输入模型以获取预测。

然后我们只需打印出句子、句子的真实标签,再打印出预测的概率。注意,我们将预测值从对数概率转化回概率。我们为每个预测得到两个概率,但如果我们参考标签索引,我们可以看到第一个概率(索引 0)对应的是西班牙语,而另一个概率对应的是英语。

代码语言:javascript复制
def make_predictions(data):
    with torch.no_grad():
        sentence = data[0]
        label = data[1]
        bow_vec = make_bow_vector(sentence, word_dict)
        log_probs = model(bow_vec)
        print(sentence)
        print(label   ':')
        print(np.exp(log_probs))
        
make_predictions(test_data[0])
make_predictions(test_data[1])

结果为以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fHqt2rDp-1681785734237)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_02_20.jpg)]

图 2.20 –预测的输出

在这里,我们可以看到,对于我们的两个预测,我们的模型都可以预测正确的答案,但是为什么会这样呢? 我们的模型究竟学到了什么? 我们可以看到我们的第一个测试句子包含单词estoy,该单词先前在我们的训练集中的西班牙语句子中出现过。 同样,我们可以看到book一词在我们的训练集中以英语句子出现。 由于我们的模型由单层组成,因此每个节点上的参数都易于解释。

在这里,我们定义了一个函数,它将一个词作为输入,并返回层内每个参数的权重。对于一个给定的词,我们从我们的字典中得到这个词的索引,然后从模型内的相同索引中选择这些参数。请注意,我们的模型会返回两个参数,因为我们是在做两个预测,即模型对西班牙语预测的贡献和模型对英语预测的贡献。

代码语言:javascript复制
def return_params(word):
    index = word_dict[word]
    for p in model.parameters():
        dims = len(p.size())
        if dims == 2:
            print(word   ':')
            print('Spanish Parameter = '   str(p[0][index].item()))
            print('English Parameter = '   str(p[1][index].item()))
            print('n')
            
return_params('estoy')
return_params('book')

结果为以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RFuyGCq6-1681785734237)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_02_21.jpg)]

图 2.21 –更新函数的预测输出

在这里,我们可以看到,对于单词estoy而言,此参数对西班牙语的预测为正,对英语的预测为负。 这意味着对于我们句子中的每个单词estoy,该句子更有可能是西班牙语句子。 类似地,对于book一词,我们可以看到这对句子为英语的预测有积极作用。

我们可以证明我们的模型仅基于对其进行了训练而学习。 如果我们尝试预测尚未训练过的单词,则可以看到它无法做出准确的决定。 在这种情况下,我们的模型认为英语单词not是西班牙语:

代码语言:javascript复制
new_sentence = (["not"],"English")
make_predictions(new_sentence)

结果为以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MbcBCKn7-1681785734238)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_02_22.jpg)]

图 2.22 – 最终输出

总结

在本章中,我们介绍了 PyTorch 及其一些关键功能。 希望您现在对 PyTorch 与其他深度学习框架有何不同以及如何用于构建基本神经网络有了更好的了解。 尽管这些简单的示例只是冰山一角,但我们已经说明了 PyTorch 是用于 NLP 分析和学习的强大工具。

在以后的章节中,我们将演示如何利用 PyTorch 的独特属性来构建高度复杂的模型,以解决非常复杂的机器学习任务。

第二部分:自然语言处理基础

在本节中,您将学习构建自然语言处理NLP)应用的基础知识。 您还将在本节中学习如何在 PyTorch 中使用各种 NLP 技术,例如单词嵌入,CBOW 和分词。

本节包含以下章节:

  • “第 3 章”,“NLP 和文本嵌入”
  • “第 4 章”,“词干提取和词形还原”

三、NLP 和文本嵌入

在深度学习中,有许多种表示文本的方式。 虽然我们已经介绍了基本的词袋BoW)表示形式,但不足为奇的是,还有一种更为复杂的表示文本数据的方式称为嵌入。 BoW 向量仅充当句子中单词的计数,而嵌入有助于从数字上定义某些单词的实际含义。

在本章中,我们将探讨文本嵌入,并学习如何使用连续 BoW 模型创建嵌入。 然后,我们将继续讨论 n 元语法以及如何在模型中使用它们。 我们还将介绍标记,分块和分词可用于将 NLP 分成其各个组成部分的各种方式。 最后,我们将研究 TF-IDF 语言模型,以及它们如何对不经常出现的单词加权我们的模型。

本章将涵盖以下主题:

  • 词嵌入
  • 探索 CBOW
  • 探索 N 元组
  • 分词
  • 对词性进行标记和分块
  • TF-IDF

技术要求

可以从这里下载 GLoVe 向量。 建议使用Gloves.6B.50d.txt文件,因为它比其他文件小得多,并且处理起来也快得多。 本章后面的部分将要求 NLTK。 本章的所有代码都可以在这个页面中找到。

NLP 的嵌入

单词没有表示其含义的自然方式。 在图像中,我们已经具有丰富的向量表示形式(包含图像中每个像素的值),因此显然具有单词的类似丰富的向量表示形式将是有益的。 当语言的部分以高维向量格式表示时,它们称为嵌入。 通过分析单词的语料库,并确定哪些单词经常出现在一起,我们可以获得每个单词的n长度向量,它可以更好地表示每个单词与所有其他单词的语义关系。 先前我们看到,我们可以轻松地将单词表示为单热编码的向量:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-djDFinZp-1681785734238)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_03_1.jpg)]

图 3.1 –单热编码向量

另一方面,嵌入是长度为n(在以下示例中为n = 3)的向量,可以采用任何值:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W68ASSJB-1681785734238)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_03_2.jpg)]

图 3.2 – n = 3的向量

这些嵌入表示n-维空间中的单词向量(其中n是嵌入向量的长度),并且在该空间中具有相似向量的单词被认为更相似。 含义。 尽管这些嵌入可以是任何大小,但它们的尺寸通常比 BoW 表示的尺寸低得多。 BOW 表示需要向量,该向量的长度为整个语料库的长度,当以一种整体语言查看时,它们可能很快变得非常大。 尽管嵌入的维数足够高以表示单个单词,但它们通常不超过几百个维。 此外,BOW 向量通常非常稀疏,主要由零组成,而嵌入则包含大量数据,并且每个维度都有助于单词的整体表示。 较低的维数和它们不稀疏的事实使得对嵌入执行深度学习比对 BOW 表示执行深度学习要有效得多。

GLoVe

我们可以下载一组预先计算的词嵌入,以演示它们如何工作。 为此,我们将使用用于词表示的全局向量GLoVe)嵌入,可以从此处下载。 这些嵌入是在非常大的 NLP 数据集上计算的,并且在词共现矩阵上训练了。 这是基于这样的概念,即在一起出现的单词更有可能具有相似的含义。 例如,单词sun与单词hot相对于单词cold更有可能出现,因此sunhot被认为更相似。

我们可以通过检查单个 GLoVe 向量来验证这是正确的:

我们首先创建一个简单的函数来从文本文件中加载我们的 GLoVe 向量。这只是建立一个字典,其中索引是语料库中的每个词,值是嵌入向量。

代码语言:javascript复制
def loadGlove(path):
    file = open(path,'r')
    model = {}
    for l in file:
        line = l.split()
        word = line[0]
        value = np.array([float(val) for val in line[1:]])
        model[word] = value
    return model
glove = loadGlove('glove.6B.50d.txt')

这意味着我们只需从字典中调用一个向量就可以访问它。

代码语言:javascript复制
glove['python']

这将产生以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NSJ1HxHC-1681785734238)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_03_3.jpg)]

图 3.3 –向量输出

我们可以看到,这将返回单词 Python 的 50 维向量嵌入。 现在,我们将引入余弦相似度的概念,以比较两个向量的相似度。 如果向量之间的n维空间中的角度为 0 度,则向量的相似度为 1。 余弦相似度高的值即使不相等也可以被认为是相似的。 可以使用以下公式进行计算,其中AB是要比较的两个嵌入向量:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nv6WZUpJ-1681785734238)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/Formula_03_001.jpg)]

我们可以使用 Sklearn 中的cosine_similarity()函数在 Python 中轻松计算。我们可以看到,有相似的向量,因为它们都是动物。

代码语言:javascript复制
cosine_similarity(glove['cat'].reshape(1, -1), glove['dog'].reshape(1, -1))

结果为以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cVGFopXA-1681785734238)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_03_4.jpg)]

图 3.4 –猫和狗的余弦相似度输出

然而,钢琴是完全不同的,因为它们是两个看似不相关的项目。

代码语言:javascript复制
cosine_similarity(glove['cat'].reshape(1, -1), glove['piano'].reshape(1, -1))

结果为以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qt4NdfFm-1681785734239)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_03_5.jpg)]

图 3.5 –猫和钢琴的余弦相似度输出

嵌入操作

由于嵌入是向量,因此我们可以对它们执行操作。 例如,假设我们将嵌入用于以下类别,然后计算出以下内容:

代码语言:javascript复制
queen - womam   man

这样,我们可以近似嵌入king的嵌入。 这基本上用Man向量替换了QueenWoman向量分量,从而得出了这种近似值。 我们可以通过图形方式对此进行说明,如下所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mBzZJIO6-1681785734239)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_03_6.jpg)]

图 3.6 –示例的图形表示

请注意,在此示例中,我们以二维方式对此进行了图形化说明。 就我们的嵌入而言,这是在 50 维空间中发生的。 虽然这并不确切,但我们可以验证我们的计算向量确实类似于King的 GLoVe 向量:

代码语言:javascript复制
predicted_king_embedding = glove['queen'] - glove['woman']   glove['man']
cosine_similarity(predicted_king_embedding.reshape(1, -1), glove['king'].reshape(1, -1))

结果为以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NmSDnE7z-1681785734239)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_03_7.jpg)]

图 3.7 – GLoVe 向量的输出

尽管 GLoVe 嵌入是非常有用的预先计算的嵌入,但实际上我们可以计算自己的嵌入。 当我们分析特别独特的语料库时,这可能很有用。 例如,Twitter 上使用的语言可能与维基百科上使用的语言不同,因此在一个语言上训练的嵌入可能对另一个语言没有用。 现在,我们将演示如何使用连续的词袋来计算自己的嵌入。

探索 CBOW

**连续词袋(CBOW)**模型构成 Word2Vec 的一部分–由 Google 创建的模型,用于获取单词的向量表示 。 通过在非常大的语料库上运行这些模型,我们能够获得单词的详细表示,这些单词表示它们的语义和上下文相似性。 Word2Vec 模型包含两个主要组件:

  • CBOW:给定周围的单词,该模型尝试预测文档中的目标单词。
  • SkipGram:这与 CBOW 相反; 该模型尝试在给定目标词的情况下预测周围的词。

由于这些模型执行类似的任务,因此我们现在仅关注一个,特别是 CBOW。 该模型旨在预测单词(目标单词),并为其指定其他单词(称为上下文单词)。 解决上下文单词的一种方法可能是,就像一样简单,使用句子中目标单词之前的单词来预测目标单词,而更复杂的模型可以在目标单词之前和之后使用多个单词。 考虑以下句子:

代码语言:javascript复制
PyTorch is a deep learning framework

假设我们要根据上下文词来预测单词deep

代码语言:javascript复制
PyTorch is a {target_word} learning framework

我们可以通过多种方式看待这一问题:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eLCmktJy-1681785734239)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_03_8.jpg)]

图 3.8 –上下文和表示表

对于我们的 CBOW 模型,我们将使用长度为 2 的窗口,这意味着对于模型的(X, y)输入/输出对,我们将使用[n-2, n-1, n 1, n 2, n],其中n是我们要预测的目标词。

使用这些作为模型输入,我们将训练一个包含嵌入层的模型。 此嵌入层自动形成我们语料库中单词的n维表示。 但是,首先,使用随机权重初始化该层。 这些参数是使用我们的模型学习的,因此,在我们的模型完成训练之后,可以使用此嵌入层来将我们的语料库编码为嵌入式向量表示形式。

CBOW 架构

现在,我们将设计模型的架构,以学习我们的嵌入。 在这里,我们的模型接受四个单词的输入(目标单词之前两个,之后两个单词),并针对输出(我们的目标单词)训练它。 下面的图示说明了它的外观:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DLbHA2AB-1681785734239)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_03_9.jpg)]

图 3.9 – CBOW 架构

输入的单词首先通过嵌入层,表示为大小(n, l)的张量,其中n是嵌入的指定长度,l是语料库中单词的数量。 这是因为语料库中的每个单词都有其自己独特的张量表示形式。

然后使用来自四个上下文词的组合(求和)嵌入,将其馈入一个全连接层中,以学习针对目标词与上下文词的嵌入表示形式进行最终分类。 请注意,我们的预测词/目标词被编码为向量,即我们的语料库的长度。 这是因为我们的模型可以有效地预测语料库中每个单词成为目标单词的概率,而最终分类是概率最高的一个。 然后,我们得到一个损失,通过我们的网络反向传播,并更新全连接层上的参数以及嵌入本身。

该方法之所以有效,是因为我们学习到的嵌入表示语义相似性。 假设我们在以下方面训练模型:

代码语言:javascript复制
X = ["is", "a", "learning", "framework"]; y = "deep"

我们的模型从本质上要学习的是,目标词的组合嵌入表示在语义上与目标词相似。 如果我们在足够大的单词语料库上重复此操作,我们会发现我们的单词嵌入开始类似于我们以前看到的 GLoVe 嵌入,在语义上相似的单词在嵌入空间中彼此出现。

构建 CBOW

现在,我们将贯穿,从头开始构建 CBOW 模型,从而说明如何学习嵌入向量:

我们首先定义一些文本,并进行一些基本的文本清理,删除基本的标点符号,并将其全部转换为小写。

代码语言:javascript复制
text = text.replace(',','').replace('.','').lower().split()

我们首先定义我们的语料库及其长度。

代码语言:javascript复制
corpus = set(text)
corpus_length = len(corpus)

请注意,我们使用的是集合而不是列表,因为我们只关注文本中的唯一词汇。然后我们建立我们的语料库索引和反语料库索引。我们的语料库索引将允许我们获得给定单词本身的索引,这将在编码单词进入我们的网络时非常有用。我们的反语料库索引允许我们获得一个词,给定的索引值,这将用于将我们的预测转换回单词。

代码语言:javascript复制
word_dict = {}
inverse_word_dict = {}
for i, word in enumerate(corpus):
    word_dict[word] = i
    inverse_word_dict[i] = word

接下来,我们对我们的数据进行编码。我们在语料库中循环,对于每个目标词,我们捕捉上下文词(前面的两个词和后面的两个词)。我们将此与目标词本身附加到我们的数据集中。请注意,我们如何从语料库中的第三个词开始(索引为2),并在语料库结束前两步停止这个过程。这是因为开头的两个词前面不会有两个词,同样,结尾的两个词后面也不会有两个词。

代码语言:javascript复制
data = []
for i in range(2, len(text) - 2):
    sentence = [text[i-2], text[i-1],
                text[i 1], text[i 2]]
    target = text[i]
    data.append((sentence, target))
    
print(data[3])

结果为以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XatV9xbu-1681785734240)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_03_10.jpg)]

图 3.10 –编码数据

然后,我们定义我们的嵌入的长度。虽然从技术上讲,这个长度可以是任何你想要的数字,但也要考虑一些权衡。虽然高维嵌入可以导致更详细的单词表示,但特征空间也会变得更稀疏,这意味着高维嵌入只适用于大型语料库。此外,更大的嵌入意味着更多的参数需要学习,所以增加嵌入大小会大大增加训练时间。我们只是在一个很小的数据集上进行训练,所以我们选择使用大小20的嵌入。

代码语言:javascript复制
embedding_length = 20

接下来,我们在 PyTorch 中定义 CBOW 模型。 我们定义嵌入层,以便它接受语料库长度的向量,并输出单个嵌入。 我们将线性层定义为一个全连接层,该层将嵌入并输出64的向量。 我们将最后一层定义为与文本语料库相同长度的分类层。

我们通过获取和求和所有输入语境词的嵌入来定义我们的前向过程,然后通过 ReLU 激活函数的全连接层,最后进入分类层。然后通过 ReLU 激活函数的全连接层,最后进入分类层,分类层预测语料库中哪个词与上下文词的求和嵌入对应最多。

代码语言:javascript复制
class CBOW(torch.nn.Module):
    def __init__(self, corpus_length, embedding_dim):
        super(CBOW, self).__init__()
        
        self.embeddings = nn.Embedding(corpus_length,                             embedding_dim)
        self.linear1 = nn.Linear(embedding_dim, 64)
        self.linear2 = nn.Linear(64, corpus_length)
        
        self.activation_function1 = nn.ReLU()
        self.activation_function2 = nn.LogSoftmax                                        (dim = -1)
    def forward(self, inputs):
        embeds = sum(self.embeddings(inputs)).view(1,-1)
        out = self.linear1(embeds)
        out = self.activation_function1(out)
        out = self.linear2(out)
        out = self.activation_function2(out)
        return out

我们还可以定义一个get_word_embedding()函数,它可以让我们在训练完我们的模型后提取给定单词的嵌入。

代码语言:javascript复制
def get_word_emdedding(self, word):
    word = torch.LongTensor([word_dict[word]])
    return self.embeddings(word).view(1,-1)

现在,我们已经准备好训练我们的模型。我们首先创建一个模型的实例,并定义损失函数和优化器。

代码语言:javascript复制
model = CBOW(corpus_length, embedding_length)
loss_function = nn.NLLLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)

然后,我们创建一个辅助函数,将我们的输入语境词,得到每一个语境词的词索引,并将其转换为一个长度为 4 的张量,形成我们神经网络的输入。

代码语言:javascript复制
def make_sentence_vector(sentence, word_dict):
    idxs = [word_dict[w] for w in sentence]
    return torch.tensor(idxs, dtype=torch.long)
print(make_sentence_vector(['stormy','nights','when','the'], word_dict))

结果为以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VjcW7LVy-1681785734240)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_03_11.jpg)]

图 3.11 –张量值

现在,我们训练我们的网络。我们循环通过 100 个周期,对于每一个通道,我们循环通过我们所有的上下文词,也就是目标词对。对于这些对,我们使用make_sentence_vector()加载上下文句子,并使用我们当前的模型状态来获得预测。我们根据我们的实际目标评估这些预测,以获得我们的损失。我们反推计算梯度,并通过我们的优化器来更新权重。最后,我们将该周期的所有损失相加并打印出来。在这里,我们可以看到,我们的损失正在减少,这表明我们的模型正在学习。

代码语言:javascript复制
for epoch in range(100):
    epoch_loss = 0
    for sentence, target in data:
        model.zero_grad()
        sentence_vector = make_sentence_vector(sentence, word_dict)  
        log_probs = model(sentence_vector)
        loss = loss_function(log_probs, torch.tensor(
        [word_dict[target]], dtype=torch.long))
        loss.backward()
        optimizer.step()
        epoch_loss  = loss.data
    print('Epoch: ' str(epoch) ', Loss: '   str(epoch_loss.item()))

这将产生以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QeS3enjd-1681785734240)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_03_12.jpg)]

图 3.12 –训练我们的网络

现在我们的模型已经训练完毕,我们可以进行预测了。 我们定义了几个函数来允许我们这样做。 get_predicted_result()从预测数组中返回预测的单词,而我们的predicted_sentence()函数则根据上下文单词进行预测。

我们将我们的句子分割成单个单词,并将它们转化为一个输入向量。然后我们将其输入到模型中,创建我们的预测数组,并使用get_predicted_result()函数获得最终的预测词。我们也会打印出预测目标词前后的两个词作为上下文。我们可以运行几个预测来验证我们的模型是否正常工作。

代码语言:javascript复制
def get_predicted_result(input, inverse_word_dict):
    index = np.argmax(input)
    return inverse_word_dict[index]
def predict_sentence(sentence):
    sentence_split = sentence.replace('.','').lower().split()
    sentence_vector = make_sentence_vector(sentence_split, word_dict)
    prediction_array = model(sentence_vector).data.numpy()
    print('Preceding Words: {}n'.format(sentence_split[:2]))
    print('Predicted Word: {}n'.format(get_predicted_result(prediction_array[0], inverse_word_dict)))
    print('Following Words: {}n'.format(sentence_split[2:]))
predict_sentence('to see leap and')

结果为以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LCTCQBSI-1681785734240)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_03_13.jpg)]

图 3.13 –预测值

现在我们已经有了一个训练好的模型,我们能够使用get_word_embedding()函数,以便返回我们语料库中任何单词的 20 维单词嵌入。如果我们在另一个 NLP 任务中需要我们的嵌入,我们实际上可以从整个嵌入层中提取权重,并将其用于我们的新模型中。

代码语言:javascript复制
print(model.get_word_emdedding('leap'))

结果为以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Diu4coIQ-1681785734240)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_03_14.jpg)]

图 3.14 –编辑模型后的张量值

在这里,我们演示了如何训练 CBOW 模型来创建单词嵌入。 实际上,要为语料库创建可靠的嵌入,我们将需要非常大的数据集,才能真正捕获所有单词之间的语义关系。 因此,对于您的模型,最好使用经过预先训练的嵌入,例如 GLoVe,它们已经在非常大的数据集上进行了训练,但是在某些情况下,最好对模型进行训练。 从头开始全新的嵌入集; 例如,当分析与正常 NLP 不同的数据语料库时(例如,Twitter 数据,用户可能会使用简短的缩写而不使用完整的句子)。

探索 N 元组

在我们的 CBOW 模型中,我们成功表明单词的含义与周围单词的上下文有关。 影响句子中单词含义的不仅是我们的上下文单词,还影响了这些单词的顺序。 考虑以下句子:

代码语言:javascript复制
The cat sat on the dog

The dog sat on the cat

如果将这两个句子转换成词袋表示法,我们将看到它们是相同的。 但是,通过阅读这些句子,我们知道它们的含义完全不同(实际上,它们是完全相反的!)。 这清楚地表明,句子的含义不仅是其包含的单词,还包括它们出现的顺序。 尝试捕获句子中单词顺序的一种简单方法是使用 N 元组。

如果我们对句子进行计数,而不是对单个单词进行计数,我们现在计算句子中出现的不同的两个单词对,这就是,即使用二元语法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6wlAyNH9-1681785734241)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_03_15.jpg)]

图 3.15 –二元语法的表格表示

我们可以这样表示:

代码语言:javascript复制
The cat sat on the dog -> [1,1,1,0,1,1]

The dog sat on the cat -> [1,1,0,1,1,1]

这些单词对试图捕捉单词在句子中出现的顺序,而不仅仅是它们的出现频率。 我们的第一句话包含两字组cat sat,而另一句话包含dog sat。 这些二元组显然可以帮助增加句子的上下文,而不仅仅是使用原始单词计数。

我们不仅限于单词。 我们还可以查看称为三元组或实际上是个不同数量的单词的不同单词三元组。 我们可以使用 N 元组作为深度学习模型的输入,而不仅仅是单个单词,但是,当使用 N 元组模型时,值得注意的是,您的特征空间会很快变得很大,并且可能使机器学习变得非常慢。 如果词典包含英语中的所有单词,则包含所有不同单词对的词典将大几个数量级!

N 元组语言建模

N 元组帮助我们做的一件事是了解自然语言是如何形成的。 如果我们认为一种语言是由较小的单词对(二元图)的一部分而不是单个单词代表的,则可以开始将语言建模为概率模型,其中单词出现在句子中的概率取决于它之前出现的单词。

一元模型中,我们假设基于单词在语料库或文档中的分布,所有单词都有出现的可能性。 我们来看一个包含一个句子的文档:

代码语言:javascript复制
My name is my name

基于此句子,我们可以生成单词的分布,其中每个单词根据其在文档中的出现频率具有给定的出现概率:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pbqll8nn-1681785734241)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_03_16.jpg)]

图 3.16 –字母组合的表格表示

然后,我们可以从该分布中随机抽取单词,以生成新的句子:

代码语言:javascript复制
Name is Name my my

但是正如我们所看到的,这句话毫无意义,说明了使用会标模型的问题。 因为每个单词出现的概率与句子中的所有其他单词无关,所以没有考虑单词出现的顺序或上下文。 这是 N 元组模型有用的地方。

现在,我们将考虑使用二元语言模型。 给定出现在单词前面的单词,此计算将考虑单词出现的概率:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P1Q0FBoP-1681785734241)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/Formula_03_002.png)]

这意味着,给定前一个单词,单词出现的概率是单词 N 元组出现的概率除以前一个单词出现的概率。 假设我们正在尝试预测以下句子中的下一个单词:

代码语言:javascript复制
My favourite language is ___

随之,我们得到以下 N 元组和单词概率:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JYmlg03p-1681785734241)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_03_17.jpg)]

图 3.17 –概率的表格表示

有了这个,我们可以计算出出现 Python 的概率,假设前一个单词出现的概率仅为 20%,而英语出现的概率仅为 10%。 我们可以进一步扩展此模型,以使用我们认为适当的来表示单词的三元组或任何 N 元组。 我们已经证明,可以使用 N 元组语言建模将关于单词之间的相互关系的更多信息引入我们的模型,而不必朴素地假设单词是独立分布的。

分词

接下来,我们将学习 NLP 的分词化,这是一种预处理文本的方式,可以输入到模型中。 分词将我们的句子分成较小的部分。 这可能涉及将一个句子拆分成单个单词,或者将整个文档分解成单个句子。 这是 NLP 必不可少的预处理步骤,可以在 Python 中相当简单地完成:

我们先接收一个基本的句子,用 NLTK 中的分词器把这个句子分割成各个词。

代码语言:javascript复制
text = 'This is a single sentence.'
tokens = word_tokenize(text)
print(tokens)

结果为以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5CTdW9ik-1681785734241)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_03_18.jpg)]

图 3.18 –拆分句子

请注意句号(.)是如何被认为是一个符号,因为它是自然语言的一部分。根据我们对文本的处理,我们可能希望保留或放弃标点符号。

代码语言:javascript复制
no_punctuation = [word.lower() for word in tokens if word.isalpha()]
print(no_punctuation)

打印(无标点符号)

结果为以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JRvurTx1-1681785734242)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_03_19.jpg)]

图 3.19 –删除标点符号

我们还可以使用句子分词器将文档标记为单个句子。

代码语言:javascript复制
text = "This is the first sentence. This is the second sentence. A document contains many sentences."
print(sent_tokenize(text))

结果为以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uSlMlho4-1681785734242)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_03_20.jpg)]

图 3.20 –将多个句子拆分为单个句子

另外,我们也可以将两者结合起来,拆成单独的词句。

代码语言:javascript复制
print([word_tokenize(sentence) for sentence in sent_tokenize(text)])

结果为以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NFCYIJDw-1681785734242)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_03_21.jpg)]

图 3.21 –将多个句子分解为单词

在分词的过程中,还有一个可选的步骤,那就是去除停顿词。歇后语是非常常见的词,对句子的整体意思没有帮助。这些词包括aIor等。我们可以使用下面的代码从 NLTK 中打印出一个完整的列表。

代码语言:javascript复制
stop_words = stopwords.words('english')
print(stop_words[:20])

结果为以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lP7tEyVu-1681785734242)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_03_22.jpg)]

图 3.22 –显示停用词

我们可以利用基本的列表理解,轻松地将这些停顿词从我们的单词中删除。

代码语言:javascript复制
text = 'This is a single sentence.'
tokens = [token for token in word_tokenize(text) if token not in stop_words]
print(tokens)

结果为以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kniDyetx-1681785734242)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_03_23.jpg)]

图 3.23 –删除停用词

尽管某些 NLP 任务(例如预测句子中的下一个单词)需要停用词,但其他任务(例如判断电影评论的情感)则不需要停用词,因为停用词对文档的整体含义没有多大帮助。 在这种情况下,最好删除停用词,因为这些常用词的出现频率意味着它们可能不必要地增加了我们的特征空间,从而增加了模型训练所需的时间。

对词性进行标记和分块

到目前为止,我们已经涵盖了几种表示单词和句子的方法,包括词袋,嵌入和 N 元组。 但是,这些表示无法捕获任何给定句子的结构。 在自然语言中,不同的单词在句子中可以具有不同的功能。 考虑以下:

代码语言:javascript复制
The big dog is sleeping on the bed

我们可以根据句子中每个单词的功能来“标记”此文本的各个单词。 因此,前面的句子变为:

代码语言:javascript复制
The -> big -> dog -> is -> sleeping -> on -> the -> bed

Determiner -> Adjective -> Noun -> Verb -> Verb -> Preposition -> Determiner-> Noun

这些词性包括但不限于以下内容:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-APTcM5AV-1681785734242)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_03_24.jpg)]

图 3.24 –词性

这些不同的语音部分可以用来更好地理解句子的结构。 例如,形容词通常在英语名词之前。 我们可以在模型中使用这些词性及其相互之间的关系。 例如,如果我们要预测句子中的下一个单词,并且上下文单词是形容词,则我们知道下一个单词为名词的可能性很高。

标记

词性标记是将这些词性标签分配给句子中各个单词的动作。 幸运的是,NTLK 具有内置的标记功能,因此我们不需要训练自己的分类器就能做到:

代码语言:javascript复制
sentence = "The big dog is sleeping on the bed"
token = nltk.word_tokenize(sentence)
nltk.pos_tag(token)

结果为以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JK6iwtqd-1681785734243)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_03_25.jpg)]

图 3.25 –分类词性

在这里,我们简单地标记我们的文本并调用pos_tag()函数以标记句子中的每个单词。 这将为句子中的每个单词返回一个标签。 我们可以通过在代码上调用upenn_tagset()来解码此标签的含义。 在这种情况下,我们可以看到VBG对应于一个动词:

代码语言:javascript复制
nltk.help.upenn_tagset("VBG")

结果为以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hqvw6fe7-1681785734243)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_03_26.jpg)]

图 3.26 – VBG 的说明

使用经过预先训练的语音标记器的部分是有好处的,因为它们不仅充当字典,可以查找句子中的各个单词。 他们还使用句子中单词的上下文来分配其含义。 考虑以下句子:

代码语言:javascript复制
He drinks the water

I will buy us some drinks

这些句子中的drinks代表了两个不同的语音部分。 在的第一句话中,drinks是指动词;drink动词的现在时。在第二句中,drinks是指名词; 单数drink的复数形式。 我们的训练过的标记器能够确定这些单个单词的上下文并执行语音标记的准确部分。

分块

分块扩展了语音标记的初始部分,旨在将我们的句子分成小块,其中这些大块中的每一个都代表一小部分语音。

我们可能希望将文本拆分为实体,其中每个实体都是单独的对象或事物。 例如,the red book不是指三个单独的实体,而是由三个单词描述的单个实体。 我们可以轻松地再次使用 NLTK 实现分块。 我们必须首先定义一个语法模式以使用正则表达式进行匹配。 问题中的模式查找名词短语NP),其中名词短语定义为确定词DT),然后是可选形容词JJ),然后是名词NN):

代码语言:javascript复制
expression = ('NP: {<DT>?<JJ>*<NN>}')

使用RegexpParser()函数,我们可以匹配此表达式的出现并将其标记为名词短语。 然后,我们可以打印结果树,显示标记的短语。 在我们的例句中,我们可以看到dogbed被标记为两个单独的名词短语。 我们可以根据需要使用正则表达式匹配定义的任何文本块:

代码语言:javascript复制
tagged = nltk.pos_tag(token)
REchunkParser = nltk.RegexpParser(expression)
tree = REchunkParser.parse(tagged)
print(tree)

结果为以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WD9dw7Z6-1681785734243)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_03_27.jpg)]

图 3.27 –树表示

TF-IDF

TF-IDF 是我们可以学习以更好地表示自然语言的另一种技术。 它通常用于文本挖掘和信息检索中,以基于搜索词匹配文档,但也可以与嵌入结合使用,以更好地以嵌入形式表示句子。 让我们用以下短语:

代码语言:javascript复制
This is a small giraffe

假设我们要用一个嵌入来表示这句话的意思。 我们可以做的一件事就是简单地对这句话中五个单词中每个单词的平均嵌入进行平均:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y2YijZ0k-1681785734243)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_03_28.jpg)]

图 3.28 –单词嵌入

但是,此方法为句子中的所有单词分配相等的权重。 您是否认为所有单词都对句子的含义有同等的贡献? Thisa是英语中非常常见的单词,但是giraffe很少见。 因此,我们可能希望为稀有词分配更多权重。 这种方法被称为词频-反向文档频率TD-IDF)。 现在,我们将演示如何计算文档的 TF-IDF 权重。

计算 TF-IDF

顾名思义,TF-IDF 由两个分开的部分组成:词频和文档反向频率。 词频是一种特定于文档的度量,用于计算要分析的文档中给定单词的频率:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z41SMcL9-1681785734243)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/Formula_03_003.png)]

请注意,由于较长的文档更可能包含任何给定的单词,因此我们将该度量除以文档中单词的总数。 如果单词在文档中出现多次,它将获得更高的词频。 但是,这与我们希望对 TF-IDF 进行加权相反,因为我们希望对文档中出现的稀有单词给予更高的加权。 这就是 IDF 发挥作用的地方。

文档频率测量要分析单词的整个文档库中文档的数量,逆文档频率计算总文档与文档频率的比率:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IkUdJuuO-1681785734244)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/Formula_03_004.jpg)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Yrut795D-1681785734244)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/Formula_03_005.jpg)]

如果我们有一个 100 个文档的语料库,并且单词在它们之间出现 5 次,则文档的倒数频率为 20。这意味着在所有文档中出现次数较少的单词的权重较高。 现在,考虑一个 100,000 个文档的语料库。 如果一个单词仅出现一次,则 IDF 为 100,000,而出现两次的单词的 IDF 为 50,000。 这些非常大且易失的 IDF 对于我们的计算而言并不理想,因此我们必须首先使用日志对其进行归一化。 请注意,如果我们为未出现在语料库中的单词计算 TF-IDF,我们如何在计算中加 1 以防止被 0 除:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JNU7CCS5-1681785734244)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/Formula_03_006.jpg)]

这使我们最终的 TF-IDF 方程如下所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ioZIuZQZ-1681785734244)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/Formula_03_007.jpg)]

现在,我们可以演示如何在 Python 中实现它并将 TF-IDF 权重应用于我们的嵌入。

实现 TF-IDF

在这里,我们将使用 NLTK 数据集中的 Emma 语料对数据集实现 TF-IDF。 该数据集由 Jane Austen 的书《Emma》中的句子组成,我们希望为这些句子中的每一个计算一个嵌入式向量表示:

我们首先导入我们的数据集,并循环处理每一个句子,删除所有标点符号和非字母数字字符(如星号)。我们选择在我们的数据集中留下停顿词,以展示 TF-IDF 如何处理这些词,因为这些词出现在许多文档中,因此具有非常低的 IDF。我们在语料库中创建了一个解析句子的列表和一组不同的词。

代码语言:javascript复制
emma = nltk.corpus.gutenberg.sents('austen-emma.txt')
emma_sentences = []
emma_word_set = []
for sentence in emma:
    emma_sentences.append([
        word.lower() for word in sentence 
        if word.isalpha()
    ])
    for word in sentence:
        if word.isalpha():
            emma_word_set.append(word.lower())
emma_word_set = set(emma_word_set)

接下来,我们创建一个函数,将返回给定文档中某个词的词频。我们以文档的长度来给出我们的词数,并计算这个词在文档中的出现次数,然后再返回比率。在这里,我们可以看到ago这个词在句子中出现了一次,而这个句子的长度是 41 个字,我们得到的词频是 0.024。

代码语言:javascript复制
def TermFreq(document, word):
    doc_length = len(document)
    occurances = len([w for w in document if w == word])
    return occurances / doc_length
TermFreq(emma_sentences[5], 'ago')

结果为以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OBO2uIAY-1681785734244)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_03_29.jpg)]

图 3.29 – TF-IDF 分数

接下来,我们计算我们的文档频率。为了有效地进行计算,我们首先需要预先计算一个文档频率词典。这将循环浏览所有数据,并统计语料库中每个词出现的文档数量。我们预先计算这个,这样我们就不必在每次计算某个词的文档频率时都要执行这个循环。

代码语言:javascript复制
def build_DF_dict():
    output = {}
    for word in emma_word_set:
        output[word] = 0
        for doc in emma_sentences:
            if word in doc:
                output[word]  = 1
    return output
        
df_dict = build_DF_dict()
df_dict['ago']

在这里,我们可以看到,ago这个词在我们的文档中出现了 32 次。使用这个词典,我们可以非常容易地计算出我们的反文档频率,方法是用文档频率除以文档总数,然后取这个值的对数。请注意,当这个词在语料库中没有出现时,我们如何在文档频率上加一,以避免除以零的错误。

代码语言:javascript复制
def InverseDocumentFrequency(word):
    N = len(emma_sentences)
    try:
        df = df_dict[word]   1
    except:
        df = 1
    return np.log(N/df)
InverseDocumentFrequency('ago')

最后,我们只需将词频和逆文档频率结合起来,就可以得到每个词/文档对的 TF-IDF 权重。

代码语言:javascript复制
def TFIDF(doc, word):
    tf = TF(doc, word)
    idf = InverseDocumentFrequency(word)
    return tf*idf
print('ago - '   str(TFIDF(emma_sentences[5],'ago')))
print('indistinct - '   str(TFIDF(emma_sentences[5],'indistinct')))

结果为以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WB1mg38E-1681785734245)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_03_30.jpg)]

图 3.30 – TF-IDF agoindistinct的分数

在这里,我们可以看到,尽管agoindistinct的单词在给定文档中仅出现一次,但indistinct出现在整个语料库中的频率较低, 表示它获得更高的 TF-IDF 权重。

计算 TF-IDF 加权嵌入

接下来,我们可以显示这些 TF-IDF 加权如何应用于嵌入:

我们首先加载我们预先计算的 GLoVe 嵌入,以提供我们语料库中单词的初始嵌入表示。

代码语言:javascript复制
def loadGlove(path):
    file = open(path,'r')
    model = {}
    for l in file:
        line = l.split()
        word = line[0]
        value = np.array([float(val) for val in line[1:]])
        model[word] = value
    return model
glove = loadGlove('glove.6B.50d.txt')

然后,我们计算文档中所有单个嵌入的非加权平均数,以获得句子整体的向量表示。我们简单地循环浏览文档中的所有单词,从 GLoVe 字典中提取嵌入物,然后计算所有这些向量的平均值。

代码语言:javascript复制
embeddings = []
for word in emma_sentences[5]:
    embeddings.append(glove[word])
mean_embedding = np.mean(embeddings, axis = 0).reshape      (1, -1)
print(mean_embedding)

结果为以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TyYxMTHb-1681785734245)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_03_31.jpg)]

图 3.31 –均值嵌入

我们重复这个过程来计算我们的 TF-IDF 加权文档向量,但这次,我们在求平均之前,先将我们的向量乘以它们的 TF-IDF 加权。

代码语言:javascript复制
embeddings = []
for word in emma_sentences[5]:
    tfidf = TFIDF(emma_sentences[5], word)
    embeddings.append(glove[word]* tfidf)
    
tfidf_weighted_embedding = np.mean(embeddings, axis =                               0).reshape(1, -1)
print(tfidf_weighted_embedding)

结果为以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BqdqIbGA-1681785734245)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_03_32.jpg)]

图 3.32 – TF-IDF 嵌入

然后,我们可以将 TF-IDF 加权嵌入与我们的平均嵌入进行比较,看看它们的相似度。我们可以使用余弦相似度来实现,如下。

代码语言:javascript复制
cosine_similarity(mean_embedding, tfidf_weighted_embedding)

结果为以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q1HX1G5w-1681785734245)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_03_33.png)]

图 3.33 – TF-IDF 和平均嵌入之间的余弦相似度

在这里,我们可以看到我们的两种不同表示形式非常相似。 因此,虽然使用 TF-IDF 可能不会显着改变我们对给定句子或文档的表示,但可能会偏重于感兴趣的单词,从而提供更有用的表示。

总结

在本章中,我们更深入地研究了词嵌入及其应用。 我们已经展示了如何使用连续词袋模型来训练它们,以及如何结合 N 元组语言模型来更好地理解句子中词之间的关系。 然后,我们研究了将文档拆分为单独的标记以进行轻松处理的方法,以及如何使用标记和分块来识别语音部分。 最后,我们展示了如何使用 TF-IDF 权重更好地以嵌入形式表示文档。

在下一章中,我们将看到如何使用 NLP 进行文本预处理,词干提取和词义化。

四、文本预处理,词干提取和词形还原

文本数据可以从许多不同的来源收集,并采用许多不同的形式。 文本可以整洁,可读或原始且混乱,也可以采用许多不同的样式和格式。 能够对这些数据进行预处理,以便可以在将其转换为 NLP 模型之前将其转换为标准格式,这就是我们将在本章中介绍的内容。

与分词相似,词干提取和词形还原是 NLP 预处理的其他形式。 但是,与将文档简化成单个单词的分词不同,词干提取和词形还原试图将这些单词进一步缩小到其词根。 例如,几乎所有英语动词都有许多不同的变体,具体取决于时态:

代码语言:javascript复制
He jumped

He is jumping

He jumps

尽管所有这些词都不同,但它们都与相同的根词相关–jump。 词干提取和词形还原都是我们可以用来减少单词的共同词根变化的技术。

在本章中,我们将解释如何对文本数据执行预处理,并探讨词干提取和词形还原,并展示如何在 Python 中实现这些。

在本章中,我们将介绍以下主题:

  • 文字预处理
  • 词干提取
  • 词形还原
  • 词干提取和词形还原的用途

技术要求

对于本章中的文本预处理,我们将主要使用内置的 Python 函数,但也将使用外部 BeautifulSoup 包。 对于词干提取和词形还原,我们将使用 NLTK Python 包。 本章中的所有代码都可以在这个页面中找到。

文本预处理

文本数据可以采用多种格式和样式。 文本可以是结构化的可读格式,也可以是更原始的非结构化格式。 我们的文本可能包含我们不希望包含在模型中的标点符号和符号,或者可能包含 HTML 和其他非文本格式。 从网上来源抓取文本时,这尤其令人担忧。 为了准备我们的文本以便可以将其输入到任何 NLP 模型中,我们必须执行预处理。 这将清除我们的数据,使其成为标准格式。 在本节中,我们将更详细地说明其中一些预处理步骤。

删除 HTML

从在线来源抓取文本时,您可能会发现您的文本包含 HTML 标记和其他非文本工件。 我们通常不希望在模型的 NLP 输入中包括这些,因此默认情况下应将其删除。 例如,在 HTML 中,<b>标签指示其后的文本应为粗体。 但是,它不包含有关句子内容的任何文本信息,因此我们应该删除它。 幸运的是,在 Python 中,有一个名为 BeautifulSoup 的包,它使我们可以在几行中删除所有 HTML:

代码语言:javascript复制
input_text = "<b> This text is in bold</br>, <i> This text is in italics </i>"
output_text =  BeautifulSoup(input_text, "html.parser").get_text()
print('Input: '   input_text)
print('Output: '   output_text)

这将返回以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PwacOPhG-1681785734245)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_04_01.jpg)]

图 4.1 –删除 HTML

上面的屏幕快照显示 HTML 已成功删除。 这在原始文本数据中可能存在 HTML 代码的任何情况下(例如在为数据抓取网页时)都可能有用。

将文本转换为小写

预处理文本以将所有内容转换为小写形式时,这是标准做法。 这是因为相同的任何两个单词都应被视为语义相同,而不管它们是否大写。 CatcatCAT都是相同的词,只是大小写不同。 我们的模型通常会将这三个词视为单独的实体,因为它们并不相同。 因此,通常的做法是将所有单词都转换为小写,以使这些单词在语义和结构上都相同。 使用以下代码行可以在 Python 中轻松完成此操作:

代码语言:javascript复制
input_text = ['Cat','cat','CAT']
output_text =  [x.lower() for x in input_text]
print('Input: '   str(input_text))
print('Output: '   str(output_text))

这将返回以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H1cqSZ6A-1681785734246)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_04_02.jpg)]

图 4.2 –将输入转换为小写

这表明输入已全部转换为相同的小写表示形式。 有一些示例中的大写字母实际上可以提供其他语义信息。 例如, May(五月)和may(意味着可能)在语义上有所不同, May(五月)将始终是大写。 但是,像这样的实例非常少见,将所有内容都转换为小写字母要比考虑这些少见的实例更为有效。

值得注意的是,大写可能在某些任务中很有用,例如语音标记的一部分(其中大写字母可能指示单词在句子中的作用)和命名实体识别(其中大写字母可能表明单词在句子中) 专有名词而不是非专有名词的替代; 例如Turkey(国家)和turkey(鸟类)。

删除标点符号

有时,根据所构建模型的类型,我们可能希望从输入文本中删除标点符号。 这在我们要汇总字数的模型中(例如在词袋表示中)特别有用。 句子中出现句号或逗号不会添加任何有关句子语义内容的有用信息。 但是,考虑到句子中标点符号位置的更复杂的模型实际上可能会使用标点符号的位置来推断不同的含义。 一个经典的例子如下:

代码语言:javascript复制
The panda eats shoots and leaves

The panda eats, shoots, and leaves

在这里,加上逗号会将描述 Pandas 的饮食习惯的句子转换为描述 Pandas 武装抢劫餐馆的句子! 尽管如此,为了保持一致性,能够从句子中删除标点符号仍然很重要。 我们可以使用re库在 Python 中执行此操作,以使用正则表达式匹配任何标点符号,并使用sub()方法将任何匹配的标点符号替换为空字符:

代码语言:javascript复制
input_text = "This ,sentence.'' contains-£ no:: punctuation?"
output_text = re.sub(r'[^ws]', '', input_text)
print('Input: '   input_text)
print('Output: '   output_text)

这将返回以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DMoBhzTK-1681785734246)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_04_03.jpg)]

图 4.3 –从输入中删除标点符号

这表明标点符号已从输入句子中删除。

在某些情况下,我们可能不希望直接删除标点符号。 一个很好的例子是的使用和号(&),在几乎每种情况下,它都与单词and互换使用。 因此,与其完全删除&号,不如选择直接用and一词代替。 我们可以使用.replace()函数在 Python 中轻松实现它:

代码语言:javascript复制
input_text = "Cats & dogs"
output_text = input_text.replace("&", "and")
print('Input: '   input_text)
print('Output: '   output_text)

这将返回以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0iY8BseZ-1681785734246)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_04_04.jpg)]

图 4.4 –删除和替换标点符号

同样值得考虑的特殊情况是标点符号对于句子的表示必不可少。 一个重要的例子是电子邮件地址。 从电子邮件地址中删除@不会使该地址更具可读性:

代码语言:javascript复制
name@gmail.com

删除标点符号将返回以下内容:

代码语言:javascript复制
namegmailcom

因此,在这种情况下,根据您的 NLP 模型的要求和目的,最好将整个项目全部删除。

替换数字

同样,对于数字,我们也想标准化我们的输出。 数字可以写为数字(9、8、7)或实际单词(九,八,七)。 可能值得将所有这些转换为一个标准化的表示形式,这样就不会将 1 和 1 视为单独的实体。 我们可以使用以下方法在 Python 中执行此操作:

代码语言:javascript复制
def to_digit(digit):
    i = inflect.engine()
    if digit.isdigit():
        output = i.number_to_words(digit)
    else:
        output = digit
    return output
input_text = ["1","two","3"]
output_text = [to_digit(x) for x in input_text]
print('Input: '   str(input_text))
print('Output: '   str(output_text))

这将返回以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-emxMvTjD-1681785734246)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_04_05.jpg)]

图 4.5 –用文字替换数字

这表明我们已成功将数字转换为文本。

但是,以类似于处理电子邮件地址的方式,处理电话号码可能不需要与常规电话号码相同的表示形式。 在以下示例中对此进行了说明:

代码语言:javascript复制
input_text = ["0800118118"]
output_text = [to_digit(x) for x in input_text]
print('Input: '   str(input_text))
print('Output: '   str(output_text))

这将返回以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Hbw4Ck6U-1681785734246)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_04_06.jpg)]

图 4.6 –将电话号码转换为文本

显然,前面示例中的输入是电话号码,因此全文表示不一定适合目的。 在这种情况下,最好从输入文本中删除任何长整数。

词干提取和词形还原

在语言中,变体是如何通过修改共同的词根来表达不同的语法类别(如时态,语气或性别)的。 这通常涉及更改单词的前缀或后缀,但也可能涉及修改整个单词的。 例如,我们可以对动词进行修改以更改其时态:

代码语言:javascript复制
Run -> Runs (Add "s" suffix to make it present tense)

Run -> Ran (Modify middle letter to "a" to make it past tense)

但是在某些情况下,整个词会发生变化:

代码语言:javascript复制
To be -> Is (Present tense)

To be -> Was (Past tense)

To be -> Will be (Future tense – addition of modal)

名词也可能有词汇上的变化:

代码语言:javascript复制
Cat -> Cats (Plural)

Cat -> Cat's (Possessive)

Cat -> Cats' (Plural possessive)

所有这些词都与根词cat相关。 我们可以计算句子中所有单词的词根,以将整个句子简化为词根:

代码语言:javascript复制
"His cats' fur are different colors" -> "He cat fur be different color"

词干提取和词形还原是我们得出这些词根的过程。 词干提取是一个算法过程,其中,切掉单词的末尾以到达一个共同的词根,而词形还原使用单词本身的真实词汇和结构分析来得出它们的真正词根,即词形。 在下面的部分中,我们将详细介绍这两种方法。

词干提取

词干提取是一个算法过程,通过该算法,我们将单词的末尾切掉以达到其词根或词干。 为此,我们可以使用不同的词干提取器,每个词干都遵循特定算法,以便返回单词的词干。 用英语,最常见的词干提取器之一是 Porter 词干提取器。

Porter 词干提取器是具有大量逻辑规则的算法,可用于返回单词的词干。 在继续讨论该算法之前,我们将首先展示如何使用 NLTK 在 Python 中实现 Porter 词干提取器。

首先,我们创建一个 Porter 词干提取器的实例。

代码语言:javascript复制
porter = PorterStemmer()

然后,我们只需在单个单词上调用这个词干提取器的实例,并打印结果。在这里,我们可以看到 Porter 词干提取器返回的词干的一个例子。

代码语言:javascript复制
word_list = ["see","saw","cat", "cats", "stem", "stemming","lemma","lemmatization","known","knowing","time", "timing","football", "footballers"]

for word in word_list: print(word ’ -> ’ porter.stem(word)) ```

代码语言:javascript复制
结果为以下输出:

![Figure 4.7 – Returning the stems of words ](https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_04_07.jpg)

图 4.7 –返回词干

我们也可以对整个句子应用词干提取,首先将句子符号化,然后对每个词单独进行词干提取。

代码语言:javascript复制
def SentenceStemmer(sentence):
    tokens=word_tokenize(sentence)
    stems=[porter.stem(word) for word in tokens]
    return " ".join(stems)
SentenceStemmer('The cats and dogs are running')

这将返回以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h1XkCfTZ-1681785734247)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_04_08.jpg)]

图 4.8 –对句子应用词干提取

在这里,我们可以看到如何使用 Porter 词干提取器提取不同的单词。 有些词,例如stemmingtiming,减少到stemtime的预期词干。 但是,某些单词,例如saw,并没有还原为它们的逻辑词干(see)。 这说明了 Porter 词干提取器的局限性。 由于词干提取器对单词应用了一系列逻辑规则,因此很难定义一组可以正确所有单词的词干的规则。 在英语单词中,根据时态(is/was/be)完全改变单词的情况下尤其如此。 这是因为没有通用规则可应用于这些单词,以将它们全部转换为相同的词根。

我们可以更详细地研究 Porter 词干提取器所应用的一些规则,以准确了解向茎的转化是如何发生的。 尽管实际的波特算法有许多详细的步骤,但是在这里,我们将简化一些规则以便于理解:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L35KAafQ-1681785734247)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_04_09.jpg)]

图 4.9 – Porter 词干提取器算法的规则

虽然不必了解 Porter 词干提取器中的每个规则,但关键是要了解其局限性。 虽然已经证明了 Porter 词干提取器在整个语料库中都能很好地工作,但总有人会说它不能正确地还原成它们的真实词干。 由于 Porter 词干提取器的规则集依赖于英语单词结构的约定,因此总会有一些单词不属于常规单词结构,并且不能被这些规则正确转换。 幸运的是,可以通过使用词形还原来克服这些限制中的某些限制。

词形还原

词形还原与词干提取的区别在于,它将单词减少为词形而不是词干。 虽然单词的词干可以被处理并简化为字符串,单词的词形是其真正的词根。 因此,虽然ran的词干只是ran,但它的词形是该词的真正词根,也就是run

词义化过程使用内置的预先计算的词法和关联词以及句子中词的上下文来确定给定词的正确词法。 在此示例中,我们将研究在 NLTK 中使用 WordNet 词形还原器。 WordNet 是一个庞大的英语单词及其词汇关系数据库。 它包含了英语中最强大,最全面的映射之一,特别是关于单词与其词形的关系。

我们将首先创建词形还原器的实例,并在一系列单词上调用它:

代码语言:javascript复制
wordnet_lemmatizer = WordNetLemmatizer()
print(wordnet_lemmatizer.lemmatize('horses'))
print(wordnet_lemmatizer.lemmatize('wolves'))
print(wordnet_lemmatizer.lemmatize('mice'))
print(wordnet_lemmatizer.lemmatize('cacti'))

结果为以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ywanOLnv-1681785734247)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_04_10.jpg)]

图 4.10 –最小化输出

在这里,我们已经可以开始看到使用词形还原胜于词干提取的优势。 由于 WordNet 词形还原器建立在英语所有单词的数据库上,因此知道micemouse的复数形式。 我们将无法使用词干提取达到相同的词根。 尽管词形还原在大多数情况下效果更好,但由于它依赖于内置的单词索引,因此无法将其推广到新单词或虚构单词:

代码语言:javascript复制
print(wordnet_lemmatizer.lemmatize('madeupwords'))
print(porter.stem('madeupwords'))

结果为以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pxuvJhOD-1681785734247)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_04_11.jpg)]

图 4.11 –虚词的词法输出

在这里,我们可以看到,在这种情况下,我们的词干提取器能够更好地推广到以前看不见的单词。 因此,如果我们要对来源进行词形还原,其语言不一定与真实英语匹配,则使用词形还原器可能会是一个问题,例如人们经常会缩写语言的社交媒体网站。

如果我们使用两个动词调用词形还原器,则会发现这不会将它们简化为预期的常见词形:

代码语言:javascript复制
print(wordnet_lemmatizer.lemmatize('run'))
print(wordnet_lemmatizer.lemmatize('ran'))

结果为以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9OvoMtJH-1681785734247)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_04_12.jpg)]

图 4.12 –在动词上进行词形还原

这是因为我们的词形还原器依靠单词的上下文来返回词形。 回想一下我们的 POS 分析,我们可以轻松地返回句子中单词的上下文并确定给定单词是名词,动词还是形容词。 现在,让我们手动指定我们的单词是动词。 我们可以看到现在可以正确返回词形:

代码语言:javascript复制
print(wordnet_lemmatizer.lemmatize('ran', pos='v'))
print(wordnet_lemmatizer.lemmatize('run', pos='v'))

结果为以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e8Rh1MKP-1681785734248)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_04_13.jpg)]

图 4.13 –在函数中实现 POS

这意味着为了返回任何给定句子的正确词形,我们必须首先执行 POS 标记以获得句子中词的上下文,然后将其传递给词形还原器以获取句子中每个词的词形。 我们首先创建一个函数,该函数将为句子中的每个单词返回 POS 标签:

代码语言:javascript复制
sentence = 'The cats and dogs are running'
def return_word_pos_tuples(sentence):
    return nltk.pos_tag(nltk.word_tokenize(sentence))
return_word_pos_tuples(sentence)

结果为以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BA1kJcI9-1681785734248)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_04_14.jpg)]

图 4.14 –句子上 POS 标签的输出

请注意,这如何为句子中的每个单词返回 NLTK POS 标签。 我们的 WordNet 词形还原器对 POS 的输入要求略有不同。 这意味着我们首先创建,该函数将 NLTK POS 标签映射到所需的 WordNet POS 标签:

代码语言:javascript复制
def get_pos_wordnet(pos_tag):
    pos_dict = {"N": wordnet.NOUN,
                "V": wordnet.VERB,
                "J": wordnet.ADJ,
                "R": wordnet.ADV}
    return pos_dict.get(pos_tag[0].upper(), wordnet.NOUN)
get_pos_wordnet('VBG')

结果为以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GzjXOkfT-1681785734248)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_04_15.jpg)]

图 4.15 –将 NTLK POS 标签映射到 WordNet POS 标签

最后,我们将这些函数组合为一个最终函数,该函数将对整个句子执行词形还原:

代码语言:javascript复制
def lemmatize_with_pos(sentence):
    new_sentence = []
    tuples = return_word_pos_tuples(sentence)
    for tup in tuples:
        pos = get_pos_wordnet(tup[1])
        lemma = wordnet_lemmatizer.lemmatize(tup[0], pos=pos)
        new_sentence.append(lemma)
    return new_sentence
lemmatize_with_pos(sentence)

结果为以下输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OGF42hQI-1681785734248)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-nlp-pt-1x/img/B12365_04_16.jpg)]

图 4.16 –最终的词形还原函数的输出

在这里,我们可以看到,与词干提取相比,词形通常可以更好地表示单词的真实词根,但有一些明显的例外。 当我们可能决定使用时,词干提取和词形还原取决于当前任务的要求,其中一些我们现在将讨论。

词干提取和词形还原的用途

词干提取和词形还原都是 NLP 的一种形式,可用于从文本中提取信息。 该被称为文本挖掘。 文本挖掘任务有多种类别,包括文本聚类,分类,文档汇总和情感分析。 可以将词干提取和词形还原与深度学习一起使用,以解决其中的一些任务,这将在本书的后面看到。

通过使用词干提取和词形还原进行预处理,再加上停用词的去除,我们可以更好地减少句子以了解其核心含义。 通过删除对句子的意义没有显着贡献的单词,并通过减少到单词的词根或词形,我们可以在深度学习框架内有效地分析句子。 如果能够将 10 个单词的句子减少为由多个核心词形而不是相似单词的多个变体组成的五个单词,则意味着我们需要通过神经网络提供的数据要少得多。 如果我们使用词袋表示法,则由于多个词都归结为相同的词形,我们的语料库会大大缩小,而如果我们计算嵌入表示法,则对于一个词,捕获我们单词的真实表示法所需的维数会更小。 减少语料库。

词干提取和词形还原的差异

现在我们已经看到了词干提取和词形还原,在的问题上,仍然存在问题,在什么情况下我们应该同时使用这两种技术。 我们看到,两种技术都试图将每个单词的根都减少。 在词干提取中,可能只是目标房间的简化形式,而在词形还原中,它会还原为真正的英语单词词根。

因为词义化需要在 WordNet 语料库中交叉引用目标词,并执行词性分析以确定词义的形式,所以如果必须使用大量词,这可能会花费大量的处理时间。 这与词干提取相反,词干提取使用详细但相对较快的算法来提取词干。 最终,与计算中的许多问题一样,这是在速度与细节之间进行权衡的问题。 在选择将这些方法中的哪一种纳入我们的深度学习流程时,要在速度和准确率之间进行权衡。 如果时间很重要,那么阻止可能是要走的路。 另一方面,如果您需要模型尽可能详细和准确,则限制词形还原可能会产生更好的模型。

总结

在本章中,我们通过探讨两种方法的功能,它们的用例以及如何实现它们,详细讨论了词干提取和词形还原。 既然我们已经涵盖了深度学习和 NLP 预处理的所有基础知识,我们就可以开始从头开始训练我们自己的深度学习模型。

在下一章中,我们将探讨 NLP 的基础知识,并演示如何在深度 NLP 领域中建立最广泛使用的模型:循环神经网络。

0 人点赞