Python-自然语言处理应用指南-全-

Python 自然语言处理应用指南(全)

原文:Applied Natural Language Processing with Python

协议:CC BY-NC-SA 4.0

一、什么是自然语言处理?

深度学习和机器学习继续在各个行业中扩散,并彻底改变了我希望在本书中讨论的主题:自然语言处理(NLP)。NLP 是计算机科学的一个子领域,致力于让计算机像人类一样以“自然”的方式理解语言。通常,这将涉及诸如理解文本的情感、语音识别和生成对问题的响应之类的任务。

NLP 已经成为一个快速发展的领域,其应用代表了人工智能(AI)突破的很大一部分。使用深度学习实现的一些例子是处理客户服务请求的聊天机器人,手机上的自动拼写检查,以及智能手机上的人工智能助手,如 Cortana 和 Siri。对于那些有机器学习和深度学习经验的人来说,自然语言处理是个人应用技能最令人兴奋的领域之一。然而,为了给更广泛的讨论提供背景,让我们讨论自然语言处理作为一个领域的发展。

自然语言处理的历史

自然语言处理可以被归类为更广泛的语音和语言处理领域的子集。正因为如此,NLP 与计算语言学等平行学科有相似之处,计算语言学关注的是使用基于规则的模型对语言进行建模。NLP 的出现可以追溯到 20 世纪 40 年代计算机科学的发展,随着语言学的进步向前发展,导致了形式语言理论的构建。简而言之,形式语言理论在日益复杂的结构和这些结构的规则上模拟语言。例如,字母表是最简单的结构,因为它是可以形成称为单词的字符串的字母集合。正式语言是一种有规则的、上下文无关的和正式的语法的语言。除了整个计算机科学的发展,人工智能的进步也在我们对 NLP 的持续理解中发挥了作用。

在某种意义上,单层感知器(SLP)被认为是机器学习/人工智能的开端。图 1-1 显示了这款车型的照片。

img/463133_1_En_1_Fig1_HTML.jpg

图 1-1

单层感知器

SLP 是由神经生理学家沃伦·麦卡洛克和逻辑学家沃尔特·皮特设计的。它是今天大量使用的更高级的神经网络模型的基础,例如多层感知器。SLP 模型被认为部分源于艾伦·图灵在 20 世纪 30 年代末对计算的研究,该研究启发了其他科学家和研究人员发展不同的概念,如形式语言理论。

向前移动到二十世纪下半叶,NLP 开始分成两个不同的思想组:(1)支持语言建模的符号方法的人,和(2)支持随机方法的人。前一组主要由语言学家组成,他们使用简单的算法来解决 NLP 问题,通常利用模式识别。后者主要由统计学家和电气工程师组成。在第二组受欢迎的许多方法中,有贝叶斯统计。随着二十世纪的发展,NLP 作为一个领域扩大了,包括自然语言理解(NLU)到问题空间(允许计算机对命令做出准确的反应)。例如,如果有人对聊天机器人说话,并要求它“在我附近寻找食物”,聊天机器人会使用 NLU 将这句话转化为切实的行动,以产生理想的结果。

跳到现在,我们发现在过去的 20 年里,随着机器学习的使用激增,NLP 经历了兴趣的激增。除了计算能力的提高之外,这部分是由于标记数据集的大型存储库变得更加可用。这种计算能力的提升很大程度上归功于 GPU 的发展;尽管如此,它已经被证明对人工智能作为一个领域的发展至关重要。因此,对指导数据科学家和工程师如何利用各种人工智能算法的材料的需求增加了,这也是本书的部分原因。

现在你已经知道了 NLP 的历史,因为它与今天相关,我将给出一个你应该期望学习的简要概述。然而,重点主要是讨论深度学习如何影响 NLP,以及如何利用深度学习和机器学习技术来解决 NLP 问题。

机器学习和深度学习综述

你会对重要的机器学习概念感到耳目一新,特别是深度学习模型,如多层感知器(MLPs)循环神经网络(RNNs)长短期记忆 (LSTM)网络。在处理任何特定的 NLP 问题之前,您将会看到利用玩具示例的深度模型。

使用 Python 的 NLP、机器学习和深度学习包

与理解 NLP 理论同等重要的是在实际环境中应用它的能力。这本书利用了 Python 编程语言,以及用 Python 编写的包。Python 已经成为数据科学家的通用语言,对 NLP、机器学习和深度学习库的支持非常丰富。在解决示例问题和讨论一般概念时,我会参考许多这样的软件包。

假设本书的所有读者都对 Python 有一个大致的了解,这样你就有能力用这种语言编写软件。如果您不熟悉这种语言,但您熟悉其他语言,那么在给定相同数据集的情况下,本书中的概念对于用于解决问题的方法是可移植的。尽管如此,这本书并不打算指导用户学习 Python。现在,让我们讨论一些对理解深度学习最重要的技术。

TensorFlow

除了机器学习之外,开源软件的突破性版本之一无疑是谷歌的 TensorFlow。它是深度学习的开源库,是类似机器学习库 Theano 的继任者。两者都利用数据流图进行计算。具体来说,我们可以认为计算依赖于特定的单个操作。TensorFlow 在功能上通过用户首先定义图形/模型来操作,然后通过用户也创建的 TensorFlow 会话来操作。

使用数据流图而不是另一种计算格式计算背后的原因是多方面的,但是更简单的好处之一是能够将模型从一种语言移植到另一种语言。图 1-2 说明了一个数据流图。

img/463133_1_En_1_Fig2_HTML.png

图 1-2

数据流图表

例如,您可能正在从事一个项目,由于延迟原因(例如,高频交易),Java 是最适合生产软件的语言;但是,您希望利用神经网络在生产系统中进行预测。不用处理在 Java 中为 TensorFlow 图建立训练框架的耗时任务,可以相对快速地用 Python 编写一些东西,然后可以通过利用 Java 在生产系统中加载权重来恢复图/模型。TensorFlow 代码类似于 Theano 代码,如下所示。

    #Creating weights and biases dictionaries
    weights = {'input': tf.Variable(tf.random_normal([state_size+1, state_size])),
        'output': tf.Variable(tf.random_normal([state_size, n_classes]))}
    biases = {'input': tf.Variable(tf.random_normal([1, state_size])),
        'output': tf.Variable(tf.random_normal([1, n_classes]))}

    #Defining placeholders and variables
    X = tf.placeholder(tf.float32, [batch_size, bprop_len])

    Y = tf.placeholder(tf.int32, [batch_size, bprop_len])
    init_state = tf.placeholder(tf.float32, [batch_size, state_size])
    input_series = tf.unstack(X, axis=1)
    labels = tf.unstack(Y, axis=1)
    current_state = init_state
    hidden_states = []

    #Passing values from one hidden state to the next
    for input in input_series: #Evaluating each input within the series of inputs
        input = tf.reshape(input, [batch_size, 1]) #Reshaping input into MxN tensor
        input_state = tf.concat([input, current_state], axis=1) #Concatenating input and current state tensors
        _hidden_state = tf.tanh(tf.add(tf.matmul(input_state, weights['input']), biases['input'])) #Tanh transformation
        hidden_states.append(_hidden_state) #Appending the next state
        current_state = _hidden_state #Updating the current state

然而,TensorFlow 并不总是最容易使用的库,因为玩具示例的文档与真实世界的示例之间经常存在严重的差距,真实世界的示例合理地引导读者通过实现深度学习模型的复杂性。

在某些方面,TensorFlow 可以被认为是 Python 内部的一种语言,因为读者在无缝编写应用程序(如果有的话)之前必须了解语法上的细微差别。从某种意义上说,Keras 回答了这些问题。

由于 TensorFlow、Theano 和类似的深度学习框架中的应用程序开发过程缓慢,Keras 是为原型应用程序开发的,但它也用于生产工程中的各种问题。它是 TensorFlow、Theano、MXNet 和 DeepLearning4j 的包装器。与这些框架不同,定义计算图相对容易,如下面的 Keras 演示代码所示。

def create_model():
    model = Sequential()
    model.add(ConvLSTM2D(filters=40, kernel_size=(3, 3),
                       input_shape=(None, 40, 40, 1),
                       padding='same', return_sequences=True))
    model.add(BatchNormalization())

    model.add(ConvLSTM2D(filters=40, kernel_size=(3, 3),
                       padding='same', return_sequences=True))
    model.add(BatchNormalization())

    model.add(ConvLSTM2D(filters=40, kernel_size=(3, 3),
                       padding='same', return_sequences=True))
    model.add(BatchNormalization())

    model.add(ConvLSTM2D(filters=40, kernel_size=(3, 3),
                       padding='same', return_sequences=True))
    model.add(BatchNormalization())

    model.add(Conv3D(filters=1, kernel_size=(3, 3, 3),
                   activation='sigmoid',
                   padding='same', data_format="channels_last"))
    model.compile(loss='binary_crossentropy', optimizer="adadelta")
    return model

尽管在实现解决方案方面,Keras 具有易用性和速度方面的额外优势,但与 TensorFlow 相比,它也有相对的缺点。最广泛的解释是,Keras 用户对其计算图形的控制远不如 TensorFlow 用户。使用 Keras 时,您在沙盒的范围内工作。TensorFlow 更擅长支持更复杂的操作,并提供各种算法的最前沿实现。

提亚诺

虽然这本书没有涉及,但在深度学习的进展中讨论这个问题是很重要的。该库类似于 TensorFlow,为开发者提供了各种计算功能(加法、矩阵乘法、减法等。)在构建深度学习和机器学习模型时嵌入张量中。例如,下面是一个示例 Theano 代码。

(code redacted please see github)
X, Y = T.fmatrix(), T.vector(dtype=theano.config.floatX)
    weights = init_weights(weight_shape)
    biases = init_biases(bias_shape)
    predicted_y = T.argmax(model(X, weights, biases), axis=1)

    cost = T.mean(T.nnet.categorical_crossentropy(predicted_y, Y))
    gradient = T.grad(cost=cost, wrt=weights)
    update = [[weights, weights - gradient * 0.05]]

    train = theano.function(inputs=[X, Y], outputs=cost, updates=update, allow_input_downcast=True)
    predict = theano.function(inputs=[X], outputs=predicted_y, allow_input_downcast=True)

    for i in range(0, 10):
        print(predict(test_x_data[i:i+1]))

if __name__ == '__main__':

    model_predict()

当查看本示例中定义的函数时,请注意T是为张量定义的变量,这是一个您应该熟悉的重要概念。张量可以被认为是类似于向量的物体;然而,它们是不同的,因为它们通常由数字数组或函数来表示,这些数组或函数由它们自己独有的特定转换规则来控制。张量可以是时空中的一个点或点的集合(任何结合了 x、y、z 轴和一个时间维度的函数/模型),或者它们可以是一个连续统,这是一个张量场。当数据通过计算图传递时,Theano 和 TensorFlow 使用张量来执行大多数数学运算,俗称模型

一般建议,如果你不知道 Theano,你应该专注于掌握 TensorFlow 和 Keras。然而,那些熟悉 Theano 框架的人可以随意在 Theano 中重写现有的 TensorFlow 代码。

深度学习在自然语言处理中的应用

本节讨论深度学习在 NLP 中的应用。

自然语言处理技术和文档分类介绍

在第三章中,我们将介绍一些介绍性的技术,比如单词标记化、清理文本数据、术语频率、逆文档频率等等。当我们为第二章中回顾的一些算法准备数据集时,我们将在数据预处理过程中应用这些技术。具体来说,我们关注分类任务,并回顾不同特征提取技术应用于文档分类任务时的相对优势。

主题建模

在第四章中,我们讨论深度学习、机器学习和 NLP 的更高级用途。我们从主题建模以及如何通过潜在的狄利克雷分配和非负矩阵分解来实现主题建模开始。主题建模就是从文档中提取主题的过程。您可以通过数据可视化将这些主题用于探索目的,或者作为标注数据时的预处理步骤。

单词嵌入

单词嵌入是用于将单词(或短语)映射到向量空间的模型/技术的集合,使得它们出现在高维领域中。由此,您可以确定一个单词(或短语,或文档)与另一个单词(或短语,或文档)之间的相似度或相异度。当我们将单词向量投射到一个高维空间时,我们可以想象它看起来像图 1-3 所示的东西。

img/463133_1_En_1_Fig3_HTML.png

图 1-3

单词嵌入的可视化

最终,你如何利用单词嵌入取决于你自己的解释。它们可以针对拼写检查等应用进行修改,但也可以用于情感分析,特别是在评估较大的实体时,例如相互关联的句子或文档。我们只关注如何训练算法以及如何准备数据来训练嵌入本身。

涉及 RNNs 的语言建模任务

在第五章中,我们通过处理一些更高级的 NLP 应用程序来结束这本书,这是在你熟悉了预处理各种格式的文本数据和训练不同的算法之后。具体来说,我们专注于训练 rnn 来执行诸如名称实体识别、回答问题、语言生成以及将短语从一种语言翻译成另一种语言之类的任务。

摘要

这本书的目的是让你熟悉自然语言处理领域,然后学习应用这些知识的例子。这本书在必要的地方涵盖了机器学习,尽管它假设你之前已经在实际环境中使用过机器学习模型。

虽然这本书并不打算面面俱到,也不太学术,但我的意图是充分涵盖材料,以便读者能够比在阅读之前更容易地处理更高级的文本。对于那些对 NLP 的实际应用更感兴趣的人来说,这是我们在例子中讨论和展示的绝大多数内容。事不宜迟,让我们开始回顾机器学习,特别是它与本书中使用的模型的关系。

二、深度学习综述

你应该知道,我们在本章通篇都在使用深度学习和机器学习方法。虽然这一章没有提供 ML/DL 的全面回顾,但讨论一些神经网络模型是很重要的,因为我们将在后面应用它们。本章还简要地让你熟悉 TensorFlow,这是本书过程中使用的框架之一。本章中的所有示例都使用玩具数值数据集,因为很难同时复习神经网络和学习处理文本数据。

同样,这些玩具问题的目的是专注于学习如何创建一个 TensorFlow 模型,而不是创建一个可部署的解决方案。从本章开始,所有的例子都集中在这些带有文本数据的模型上。

多层感知器和循环神经网络

传统的神经网络模型,通常被称为多层感知器模型 (MLPs),接替单层感知器模型 (SLPs)。创建 MLP 是为了克服 SLP 模型的最大缺点,即不能有效地处理不可线性分离的数据。在实际案例中,我们经常观察到多元数据是非线性的,使得 SLP 无效。MLP 能够克服这一缺点,特别是因为 MLP 具有多层。我们将在遍历一些代码时更深入地检查这个细节,以使示例更加直观。然而,让我们从图 2-1 所示的 MLP 可视化开始。

img/463133_1_En_2_Fig1_HTML.jpg

图 2-1

多层感知器

MLP 模型的每一层都由权重连接,所有权重都是从标准正态分布随机初始化的。输入层有一定数量的节点,每个节点代表神经网络中的一个特征。隐藏层的数量可以变化,但是它们中的每一个通常具有相同的节点数量,由用户指定。在回归中,输出图层有一个节点。在分类中,它有 K 个节点,其中 K 是类的数量。

接下来,我们来深入探讨一下 MLP 是如何工作的,并在 TensorFlow 中完成一个例子。

玩具示例 1:用 MLP 模型模拟股票收益

让我们假设我们正试图根据同一天其他股票的收益来预测福特汽车公司(F)的股票收益。这是一个回归问题,假设我们试图预测一个连续的值。让我们从定义一个带有参数的mlp_model函数开始,这些参数稍后会用到,如下所示:

def mlp_model(train_data=train_data, learning_rate=0.01, iters=100, num_hidden1=256):

这个 Python 函数包含了构成神经网络主体的所有 TensorFlow 代码。除了定义图形之外,此函数还调用 TensorFlow 会话来训练网络并进行预测。我们将从一行一行地遍历函数开始,同时将代码与模型背后的理论联系起来。

首先,让我们解决函数中的参数:train_data是包含我们的训练数据的变量;在这个例子中;它是特定股票在给定时间内的收益。以下是我们数据集的标题:

0  0.002647 -0.001609  0.012800  0.000323  0.016132 -0.004664 -0.018598
1  0.000704  0.000664  0.023697 -0.006137 -0.004840  0.003555 -0.006664
2  0.004221  0.003600  0.002469 -0.010400 -0.008755 -0.002737  0.025367
3  0.003328  0.001605  0.050493  0.006897  0.010206  0.002260 -0.007156
4  0.001397  0.004052 -0.009965 -0.012720 -0.019235 -0.002255  0.017916
5 -0.009326 -0.003754 -0.014506 -0.006607 -0.034865  0.011463  0.003844
6  0.008446  0.005747  0.022830  0.009312  0.021757 -0.000319  0.023982
7  0.002705  0.002623  0.007636  0.020099 -0.007433 -0.008303 -0.004330
8 -0.011224 -0.009530 -0.008161 -0.003230 -0.015381 -0.003381 -0.010674
9  0.012496  0.010942  0.016750  0.007777  0.001233  0.008724  0.033367

每一列代表某一天股票的百分比回报,我们的训练集包含 1180 个观察值,测试集包含 582 个观察值。

接下来,我们来看学习率和激活函数。在机器学习文献中,学习率往往用符号 η (eta)来表示。学习率是一个标量值,它控制梯度更新到我们希望改变的参数的程度。当提到梯度下降更新方法时,我们可以举例说明这种技术。先看方程,然后可以迭代分解。

$$ {\theta}_{t+1}={\theta}_t-\eta \frac{1}{N}{\varSigma}_{i=1}^N{\left({h}_{\theta }{(x)}i-{y}i\right)}² $$

(2.1)

$$ {\theta}_{t+1}={\theta}_t-\eta \frac{1}{N}{\varSigma}_{i=1}2\left({h}_{\theta }{(x)}i-{y}i\right){\nabla}_{\theta }{h}_{\theta }{(x)}^i $$

在方程 2.1 中,我们在给定的时间步长 t 更新一些参数 θhθ(x)I等于假设的标签/值, y 是实际的标签/值,此外 N 等于我们正在训练的数据集中的观察总数。

θhθ(x)I是输出相对于模型参数的梯度。

神经网络中的每个单元(除了输入层)接收乘以权重的输入的加权和,所有这些加权和以偏差相加。数学上,这可以用等式 2.2 来描述。

$$ y=f\left(x,{w}^T\right)+b $$

(2.2)

在神经网络中,参数是权重和偏差。当参考图 2-1 时,权重是将层中的单元彼此连接起来的线,并且通常通过从正态分布中随机采样来初始化。下面是发生这种情况的代码:

    weights = {'input': tf.Variable(tf.random_normal([train_x.shape[1], num_hidden])),
            'hidden1': tf.Variable(tf.random_normal([num_hidden, num_hidden])),
            'output': tf.Variable(tf.random_normal([num_hidden, 1]))}

    biases = {'input': tf.Variable(tf.random_normal([num_hidden])),
            'hidden1': tf.Variable(tf.random_normal([num_hidden])),
            'output': tf.Variable(tf.random_normal([1]))}

因为它们是计算图形的一部分,TensorFlow 中的权重和偏差必须用tf.Variable()初始化为 TensorFlow 变量。幸运的是,TensorFlow 有一个从名为tf.random_normal()的正态分布中随机生成数字的函数,它将一个数组作为参数,指定您正在创建的矩阵的形状。对于创建神经网络的新手来说,为权重和偏差单元选择合适的维度是一个典型的挫折来源。以下是一些需要记住的快速提示:

  • 当引用权重时,给定层的列需要匹配下一层的行。

  • 每一层的权重列必须与偏差中每一层的单位数相匹配。

  • 权重字典的输出图层列(以及偏差字典的数组形状)应该代表您正在建模的问题。(如果回归,1;如果分类,N,其中 N =类的数量)。

你可能会好奇为什么我们要随机初始化权重和偏差。这让我们想到了神经网络成功的一个关键因素。最简单的解释是想象以下两种情景:

  • 所有权重 初始化为 1 。如果所有的权重都被初始化为 1,每个神经元都被传递相同的值,等于加权和加上偏差,然后被放入某个激活函数,不管这个值可能是什么。

  • 所有权重 初始化为 0 。与前面的场景类似,所有神经元都被传递了相同的值,只是这一次,该值肯定为零。

与在相同位置初始化的权重相关联的更普遍的问题是,它使得网络容易陷入局部最小值。让我们想象一个误差函数,如图 2-2 所示。

img/463133_1_En_2_Fig2_HTML.jpg

图 2-2

误差图

想象一下,当我们将神经网络的权重初始化为 0,随后当它计算误差时,它产生图 2-2 中 Y 变量的值。梯度下降算法总是从该算法的第一次迭代中给出相同的权重更新,并且它可能向前给出相同的值。正因为如此,我们没有利用神经网络从解决方案空间的任何一点开始的能力。这有效地消除了神经网络的随机性质,并大大降低了达到权重优化问题的最佳可能解决方案的概率。我们来讨论一下学习率。

学习率

学习率通常是一个静态浮点值,它决定了梯度或误差对您寻求优化的参数更新的影响程度。在示例问题中,常见的是学习率初始化在 0.01 到 1e–4 之间。学习率参数的初始化是另一个值得一提的地方,因为它会影响算法收敛到一个解的速度。简而言之,以下是两个有问题的场景:

  • 学习率过大。当学习率过大时,错误率以不稳定的方式移动。通常,我们观察到一次迭代中的算法似乎找到了比前一次更好的解决方案,但在下一次迭代中变得更差,并在这两个界限之间振荡。在最坏的情况下,我们最终开始接收错误率的 NaN 值,所有解决方案实际上都失效了。这就是爆炸梯度问题,我将在后面讨论。

  • 学习率太小。虽然随着时间的推移,这不会产生错误,但最终,我们会花费过多的时间等待解决方案收敛到最佳解决方案。

最佳解决方案是选择一个学习率,该学习率足够大以最小化收敛到最佳解决方案所需的迭代次数,同时不要太大以至于在其优化路径中超过该解决方案。一些解决方案,如自适应学习率算法,解决了必须网格搜索或迭代使用不同学习率的问题。mlp_model()函数使用 Adam ( ada 感受性 m 矩估计)优化算法,更新我们学习的学习率 aw。我简要地讨论了这个算法是如何工作的,以及为什么你应该使用它来加速学习率的优化。

迪德里克·金玛和吉米·巴雷在一篇论文中首次描述了亚当。Adam 特别寻求通过估计梯度的一阶和二阶矩来优化学习速率。对于那些不熟悉的人来说,被定义为一组点的形状的特定度量。由于与统计学相关,这些点通常是概率分布中的值。我们可以把零阶矩定义为总概率;第一时刻为均值;二阶矩作为方差。在本文中,除了一些初始假设之外,他们还描述了 Adam 的最佳参数,如下所示:

  • α =步长;【0.001】、= 10-】**

*** β 1β 2 =一阶和二阶矩估计的指数衰减率 β 1 = 0.9,β2= 0.999;β1,β2∈[0,1]

*   *f* ( *θ* ) =我们用参数 *θ* 优化的随机目标函数

*   *m* =第一力矩矢量, *v* =第二力矩矢量(均初始化为 0)** 

**考虑到这一点,尽管我们尚未找到最佳解决方案,但我们使用了以下算法:

  • g**=

***     $$ \widehat{m},\widehat{v}=\mathrm{Bias}-\mathrm{corrected}\ \mathrm{first}\ \mathrm{and}\ \mathrm{second}\ \mathrm{moment}\ \mathrm{estimates}\ \mathrm{respectively}; $$

*   ![$$ {m}_t:= {\beta}_1\ast {m}_{t-1}+\left(1-{\beta}_1\right)\ast {g}_t\kern0.5em {v}_t:= {\beta}_2\ast {v}_{t-1}+\left(1-{\beta}_2\right)\ast {g}_t² $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/app-nlp-py/img/463133_1_En_2_Chapter_TeX_IEq2.png)

*   ![$$ {\widehat{m}}_t:= \frac{m_t}{1-{\beta}_1^t},\kern1em \widehat{v_t}:= \frac{v_t}{1-{\beta}_2^t} $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/app-nlp-py/img/463133_1_En_2_Chapter_TeX_IEq3.png)

*   ![$$ {\theta}_t:= {\theta}_{t-1}-\alpha \ast \frac{{\widehat{m}}_t}{\sqrt{\widehat{\left({v}_t\right)\kern0.5em }}+\epsilon } $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/app-nlp-py/img/463133_1_En_2_Chapter_TeX_IEq4.png)** 

**虽然前面的公式描述了优化一个参数时的 Adam,但我们可以外推公式以调整多个参数(如多变量问题)。在论文中,Adam 优于其他标准优化技术,被视为默认的学习率优化算法。

至于最终参数,num_hidden是指隐藏层中的单元数。一个常用的经验法则是使这个数等于输入数加上输出数,然后乘以 2/3。

Epochs 指的是算法应该遍历整个训练集的次数。考虑到这是依赖于情况的,没有一个神经网络应该被训练的总的可暗示的时期数。然而,一个受暗示的方法是挑选一个任意大的数字(例如 1500),绘制训练误差,然后观察哪个历元数是足够的。如果需要,您可以扩大上限以允许模型进一步优化其解决方案。

既然我已经讨论完了参数,让我们来看看多层感知器的架构、代码和数学,如下所示:

#Creating training and test sets
train_x, train_y = train_data[0:int(len(train_data)*.67), 1:train_data.shape[1]], train_data[0:int(len(train_data)*.67), 0]
test_x, test_y = train_data[int(len(train_data)*.67):, 1:train_data.shape[1]], train_data[int(len(train_data)*.67):, 0]

请注意,我们正在创建一个训练集和一个测试集。训练集和测试集分别包含标记为train_data的原始数据集的 67%和 33%。建议机器学习问题至少有这两个数据集。创建一个验证集也是可选的,但是为了简洁起见,在这个例子中省略了这个步骤。

接下来,让我们讨论使用 TensorFlow 的以下重要方面:

#Creating placeholder values and instantiating weights and biases as dictionaries
X = tf.placeholder('float', shape = (None, 7))
Y = tf.placeholder('float', shape = (None, 1))

当在 TensorFlow 中工作时,将机器学习模型称为是很重要的,因为我们正在用不同的张量对象创建计算图。任何典型的深度学习或机器学习模型都需要一个解释变量和响应变量;然而,我们需要具体说明这些是什么。因为它们不是图形的一部分,而是我们传递数据的代表性对象,所以它们被定义为占位符变量,我们可以通过使用tf.placeholder()从 TensorFlow(导入为tf)访问这些变量。这个函数的三个参数是 dtype(数据类型)、shape 和 name。dtype 和 shape 是唯一必需的参数。以下是快速的经验法则:

  • 通常,X 和 Y 变量的形状应该初始化为一个元组。处理二维数据集时,X 变量的形状应为(无,特征数),Y 变量的形状应为(无,[1 表示回归,N 表示分类]),其中 N 表示类的数量。

  • 为这些占位符指定的数据类型应该反映您通过它们传递的值。在本例中,我们正在传递一个浮点值矩阵并预测一个浮点值,因此响应和解释变量的占位符都具有 float 数据类型。在这是一个分类问题的情况下,假设相同的数据通过解释变量传递,响应变量具有 int 数据类型,因为类的标签是整数。

因为我已经讨论了神经网络中的权重,所以让我们进入神经网络结构的核心:通过输出层的输入,如下面的代码所示(在mlp_model()函数内):

#Passing data through input, hidden, and output layers
input_layer = tf.add(tf.matmul(X, weights['input']), biases['input']) (1)
input_layer = tf.nn.sigmoid(input_layer) (2)
input_layer = tf.nn.dropout(input_layer, 0.20) (3)

hidden_layer = tf.add(tf.multiply(input_layer, weights['hidden1']), biases['hidden1'])
hidden_layer = tf.nn.relu(hidden_layer)
hidden_layer = tf.nn.dropout(hidden_layer, 0.20)

output_layer = tf.add(tf.multiply(hidden_layer, weights['output']),biases['output']) (4)

当查看突出显示的代码(1)的第一行时,我们看到了输入层操作。数学上,从一个神经网络层到下一个神经网络层的操作可以由下面的等式表示:

$$ laye{r}_k=f\left(\left({X}_k\ast {w}_k^T\right)+ bia{s}_k\right) $$

(2.2.1)

f ( x )等于某激活函数。此操作的输出被传递到下一层,在那里运行相同的操作,包括放置在层之间的任何操作。在 TensorFlow 中,有内置的数学运算来表示前面的等式:tf.add()tf.matmul()

在我们创建输出之后,在这个例子中是一个形状矩阵(1,256),我们把它传递给一个激活函数。在突出显示的代码(2)的第二行中,我们首先将输入和偏差的加权和传递给一个 sigmoid 激活函数,如公式 2.3 所示。

$$ \sigma =\left(\frac{1}{1+{e}^{-x}}\right) $$

(2.3)

e 是指数函数。激活函数是一种缩放来自方程 2.2 的输出的方式,有时与我们如何对输出进行分类直接相关。更重要的是,这是将非线性引入学习过程的神经网络的核心组件。简单地说,如果我们使用线性激活函数,其中 f ( x ) = x ,我们只是重复地将线性函数的输出从输入层传递到输出层。图 2-3 显示了该激活功能。

img/463133_1_En_2_Fig3_HTML.png

图 2-3

Sigmoid 激活函数

虽然这里的范围是从–6 到 6,但函数本质上看起来像∞到∞,因为当 X 分别变得无限大或无限小时,在 0 和 1 处有渐近线。这个函数是神经网络中使用的最常见的激活函数之一,我们在第一层中使用。

此外,我们定义了这个函数的导数,这在数学上解释消失梯度问题(在本章后面讨论)是很重要的。尽管遍历神经网络中的所有激活函数将是详尽的,但是值得讨论该神经网络利用的其他激活函数。隐藏层使用 ReLU 激活函数,该函数在等式 2.4 中进行了数学定义。

$$ ReLU(x)=\max \left(0,\kern0.5em x\right) $$

(2.4)

该功能如图 2-4 所示。

img/463133_1_En_2_Fig4_HTML.jpg

图 2-4

ReLU 激活功能

无论从数学上还是视觉上,ReLU 激活函数都很简单。ReLU 的输出是一个 0 的矩阵,带有一些正值。ReLU 激活函数的一个主要好处在于它产生一个稀疏矩阵作为输出。这个属性最终是我决定将它作为隐藏层的激活函数的原因,特别是当它涉及到渐变消失的问题时。

消失渐变和为什么 ReLU 有助于防止它们

消失梯度问题是神经网络训练所特有的,它是研究人员试图通过 LSTM 而不是 RNN 进行的改进的一部分(这两个问题都将在本章稍后讨论)。消失梯度问题是当梯度变得如此之小时观察到的现象,以至于从一次迭代到下一次迭代对权重的更新要么完全停止,要么可以忽略不计。

从逻辑上讲,接下来是神经网络有效停止训练的情况。在大多数情况下,这将导致较差的权重优化,并最终导致较差的训练和测试集性能。为什么会发生这种情况可以通过如何计算每个权重的更新来精确解释:

当我们看图 2-3 时,我们看到了 sigmoid 函数的导数。该函数的大部分导数落在一个狭窄的范围内,大部分值接近于 0。当考虑如何计算不同隐藏层的梯度时,随着我们的网络变得更深,这正是导致问题的原因。在数学上,这由以下等式表示:

$$ \frac{\partial {E}_3}{\partial W}=\sum \limits_{k=0}²\frac{\partial {E}_3}{\partial \widehat{y_3}}\frac{\partial {y}_3}{\partial {s}_3}\frac{\partial {s}_3}{\partial {s}_k}\frac{\partial {s}_k}{\partial W} $$

正如你所看到的,当我们将误差反向传播到层 k 时,在这个例子中是 0(输入层),我们将激活函数输出的几个导数相乘几次。这是链规则的简要解释,也是大多数神经网络反向传播训练算法的基础。链式法则是指定如何计算由两个或多个函数组成的导数的公式。假设我们有一个两层神经网络。我们也假设我们各自的梯度是 0.001 和 0.002。这产生 2e–6 作为输出层的相应梯度。我们对下一个梯度的更新可以忽略不计。

您应该知道,任何产生非稀疏输出的激活函数,特别是当连续用于多个层时,通常会导致渐变消失。我们能够通过使用稀疏和非稀疏输出激活函数的组合,或者专门利用非备用激活函数,来大大减轻这个问题。我们在mlp_model()函数中举例说明了这样一个神经网络。然而,现在,在我们完成对这个 MLP 的分析之前,让我们看看最后一个激活层。

注意,在每个激活层之后,我们使用由tf.nn.dropout()调用的丢弃层。脱落层与其前一层的尺寸相同;然而,他们任意将随机选择的权重值设置为 0,有效地“关闭”了与之相连的神经元。在每次迭代中,都会有一组不同的随机神经元关闭。使用 dropout 的好处是防止过度拟合,即模型在训练数据中表现良好,但在测试数据中表现不佳。

有许多因素会导致过度拟合,包括(但不限于)没有足够的训练数据或没有交叉验证数据(这导致模型记住数据集的给定方向的特性,而不是归纳到数据下面的分布)。尽管您应该首先解决这些问题,但是增加辍学并不是一个坏主意。当您在没有丢失的情况下执行函数时,您会注意到相对于包含丢失的模型的过度拟合。

让我们讨论一些最后的 MLP 主题,特别是导致模型学习的关键因素。

损失函数和反向传播

损失函数就是我们如何定义我们的模型不正确的程度。在回归中,最典型的选择是均方误差 (MSE)或均方根误差 (RMSE)。数学上,它们定义如下:

$$ MSE=\frac{1}{N}\sum \limits_{i=1}N{\left({h}_{\theta}\left({x}_i\right)-{y}i\right)}² $$

(2.5)

$$ RMSE=\sqrt{\frac{1}{N}\sum \limits_{i=1}N{\left({h}_{\theta}\left({x}_i\right)-{y}i\right)}²} $$

(2.6)

误差= TF . reduce _ mean(TF . pow(output _ layer–Y,2))(代码中的均方误差)

直观地说,MSE(见等式 2.5 )提供了一种评估给定时期内所有预测的平均误差的方法。RMSE(见方程 2.6 )提供了相同的统计数据,但取 MSE 值的平方根。RMSE 的好处在于它提供了与预测值相同单位的统计数据,允许用户更精确地评估模型的性能。MSE 没有这种好处,因此,它变得更难解释——除非从某种意义上说,一个时期到下一个时期的较低 MSE 是可取的。

作为一个例子,如果我们预测货币,这意味着我们的预测是 0.30 美元的平方不准确?虽然如果下一个时段产生 0.10 美元的 MSE,我们可以说我们有更好的解决方案,但是很难准确地说出在给定的预测中 0.10 美元的 MSE 意味着什么。我们在本章的最后一个玩具例子中比较了使用 RMSE 和 MSE 的结果。然而,在自然语言处理中,我们更经常处理为分类任务保留的错误函数。记住这一点,你应该习惯下面的公式。

二元交叉熵是

$$ \mathrm{\mathcal{L}}{\left(y,{h}_x\left(\theta \right)\right)}_i=-y\kern0.5em \log (p)+\left(1-y\right)\log \left(1-p\right)\Big) $$

(2.7)

多类交叉熵是

$$ \mathrm{\mathcal{L}}{\left(y,{h}_x\left(\theta \right)\right)}_i=\max \left(0,{s}_j-{s}_{y_i}+\varDelta \right) $$

(2.8)

交叉熵是识别从集合中抽取的事件所需的比特数。当使用基于交叉熵的损失函数时,执行相同的原理(关于使用 MSE 或 RMSE 损失函数的训练)。我们的目标是在尽可能减小误差的方向上优化权重。

至此,我们已经从参数的初始化、参数的含义、层如何从每一层移动、激活函数对其做了什么以及如何计算误差等方面了解了 MLP。接下来,我们来挖掘一下循环神经网络,长短期记忆,以及它们在自然语言处理领域的相对重要性。

循环神经网络和长短期记忆

尽管 MLP 相对稳健,但它们也有其局限性。该模型假设输入和输出之间的独立性,对于函数的输出在统计上依赖于前面的输入的问题,这是一个次优的选择。因为这与自然语言处理(NLP)有关,所以 MLP 可能对某些任务特别有用,例如情感分析。在这些问题中,一段文本被归类为负面的并不依赖于对另一段文本的情感评估。

举个例子,我不需要阅读多个餐馆评论来决定一个评论是正面的还是负面的。它可以由给定观察的属性来确定。然而,这并不总是我们遇到的 NLP 问题的类型。例如,假设我们正在尝试对以下句子进行拼写检查:

  • "我很高兴我们要去购物中心了!"

  • “我很乐意。那堂课棒极了。”

由于出现的上下文,这两个句子分别在单词 tooto 的用法上都是不正确的。我们必须使用前面的单词序列,甚至后面的单词,来确定什么是不正确的。另一个类似的问题是预测句子中的单词;比如,我们来看下面这句话。

  • “我出生在德国。我说 _ _ _ _ _ _ _ _ _。”

虽然不一定有一个答案来完成这个句子,因为出生在德国并不预先决定一个人只说德语,但很有可能遗漏的单词是德语。然而,我们只能说,因为单词周围的上下文,并且假设神经网络是在句子(或短语)上训练的,并且具有类似的结构。无论如何,这些类型的问题需要一个模型来适应与先前输入相关的某种记忆,这就把我们带到了循环神经网络。图 2-5 显示了 RNN 的结构。

img/463133_1_En_2_Fig5_HTML.png

图 2-5

循环神经网络

研究 RNN 的结构很重要,因为它关系到解决统计依赖问题。与前面的示例类似,让我们通过 TensorFlow 中的一些示例代码,使用一个玩具问题来说明模型结构。与 MLP 类似,我们将通过一个玩具问题来创建一个函数,为神经网络加载和预处理我们的数据,然后创建一个函数来构建我们的神经网络。下面是函数的开头:

def build_rnn(learning_rate=0.02, epochs=100, state_size=4):

前两个论点应该很熟悉。它们代表了与 MLP 例子中相同的概念。但是,我们有一个新的论点叫做state_size。在一个普通的 RNN 中,我们在这里建立的模型,我们从给定的时间向前一步,通过所谓的隐藏状态。隐藏状态类似于 MLP 的隐藏层,因为它是先前时间步长的隐藏状态的函数。下面将隐藏状态和输出定义为

$$ {h}_t=f\left({W}_{xh}{x}_t+{W}_{hh}{h}_{t-1}+{b}_h\right) $$

(2.9)

$$ {y}_t={W}_{ho}{h}_t+{b}_o $$

(2.10)

h t 是隐藏状态, W 是权重矩阵, b 是偏差数组, y 是函数的输出, f ( x )是我们选择的激活函数。

玩具示例 2:用 RNN 模型模拟股票收益

使用build_rnn()功能中的代码,观察以下内容。

#Loading data
    x, y = load_data(); scaler = MinMaxScaler(feature_range=(0, 1))
    x, y = scaler.fit_transform(x), scaler.fit_transform(y)
    train_x, train_y = x[0:int(math.floor(len(x)*.67)),  :], y[0:int(math.floor(len(y)*.67))]

    #Creating weights and biases dictionaries
    weights = {'input': tf.Variable(tf.random_normal([state_size+1, state_size])),
        'output': tf.Variable(tf.random_normal([state_size, train_y.shape[1]]))}
    biases = {'input': tf.Variable(tf.random_normal([1, state_size])),
        'output': tf.Variable(tf.random_normal([1, train_y.shape[1]]))}

我们首先加载训练和测试数据,在测试集中执行类似的拆分,使得完整数据集的前 67%成为训练集,剩余的 33%成为测试集。在这种情况下,我们区分两个类,0 或 1,表示价格是上涨还是下跌。然而,向前看,我们必须回头参考状态大小参数来理解我们为权重和偏差矩阵产生的矩阵的形状,同样作为 TensorFlow 变量。

为了明确你对状态大小参数的理解,参考图 2-5 ,其中神经网络的中心代表一个状态。我们将给定的输入以及先前的状态乘以一个权重矩阵,然后将所有这些与偏差相加。类似于 MLP,加权和值形成激活函数的输入。

激活函数的输出在时间步长 t 形成隐藏状态,其值成为等式 2.10 中加权和的一部分。该矩阵应用的值最终形成 RNN 的输出。我们对尽可能多的状态重复这些操作,这等于我们通过神经网络的输入数量。当回过头来看这幅图像时,这就是 RNN 被“展开”的含义我们例子中的state_size被设置为 4,这意味着我们在进行预测之前输入了四个输入序列。

现在让我们浏览一下与这些操作相关的 TensorFlow 代码。

#Defining placeholders and variables
    X = tf.placeholder(tf.float32, [batch_size, train_x.shape[1]])
    Y = tf.placeholder(tf.int32, [batch_size, train_y.shape[1]])
    init_state = tf.placeholder(tf.float32, [batch_size, state_size])
    input_series = tf.unstack(X, axis=1)
    labels = tf.unstack(Y, axis=1)
    current_state = init_state
    hidden_states = []

    #Passing values from one hidden state to the next
    for input in input_series: #Evaluating each input within the series of inputs
        input = tf.reshape(input, [batch_size, 1]) #Reshaping input into MxN tensor
        input_state = tf.concat([input, current_state], axis=1) #Concatenating input and current state tensors
        _hidden_state = tf.tanh(tf.add(tf.matmul(input_state, weights['input']), biases['input'])) #Tanh transformation
        hidden_states.append(_hidden_state) #Appending the next state
        current_state = _hidden_state #Updating the current state

与 MLP 模型类似,我们需要为数据将通过的 x 和 y 张量定义占位符变量。但是,这里会有一个新的占位符,它是init_state,代表初始状态矩阵。注意,当前状态是第一次迭代到下一次迭代的init_state占位符。它还拥有相同的维度,并期望相同的数据类型。

向前移动,我们迭代通过数据集中的每个input_sequence,其中_hidden_state是公式的 Python 定义(参见等式 2.9 )。最后,我们必须到达输出状态,由下式给出:

logits = [tf.add(tf.matmul(state, weights['output']), biases['output']) for state in hidden_states]

这里的代码代表等式 2.10 。然而,这只会给我们一个浮点小数,我们需要以某种方式转换成一个标签。这给我们带来了一个激活函数,这对于多类分类是很重要的,因此对于本文的剩余部分,softmax 激活函数。随后,我们将该激活函数定义如下:

$$ S\left({y}_i\right)=\left(\frac{e{yi}}{\sum_{i=1}N{e}{y^i}}\right) $$

(2.11)

当你看这个公式时,我们将所有可能的值相加。因此,我们将其定义为概率得分。当将其与分类联系起来时,特别是使用 RNN 时,我们输出的是一个观察值与另一个(或其他)观察值的相对概率。我们在这种情况下选择的标签是具有最高相对分数的标签,这意味着我们选择给定的标签 k,因为根据模型的预测,它最有可能是真的。等式 2.11 随后在代码中由以下行表示:

predicted_labels = [tf.nn.softmax(logit) for logit in logits] #predictions for each logit within the series

由于这是一个分类问题,我们使用基于交叉熵的损失函数,对于这个玩具示例,我们将使用梯度下降算法,这两种算法在前面的 MLPs 部分都有详细说明。调用 TensorFlow 会话的方式也与调用 MLP 图(以及所有 TensorFlow 计算图)的方式相同。稍微偏离 MLP,我们计算展开网络的每个时间步的误差,并将这些误差相加。这被称为通过时间 (BPTT)的反向传播,这是专门利用的,因为相同的权重矩阵用于每个时间步长。因此,除了输入之外,唯一变化的变量是隐藏状态矩阵。因此,我们可以计算每个时间步长对误差的影响。然后,我们将这些时间步长误差相加,得到误差。在数学上,这由以下等式表示:

$$ \frac{\partial {E}_3}{\partial W}=\sum \limits_{k=0}³\frac{\partial {E}_3}{\partial \widehat{y_3}}\frac{\partial {y}_3}{\partial {s}_3}\frac{\partial {s}_3}{\partial {s}_k}\frac{\partial {s}_k}{\partial W} $$

这是链式法则的一个应用,正如我们如何将误差从输出层反向传播回输入层以更新权重对总误差的贡献一节中简要描述的那样。BPPT 运用了同样的逻辑;相反,我们将时间步长视为层。然而,尽管 RNNs 解决了 MLPs 的许多问题,但它们有相对的局限性,这一点你应该知道。

RNNs 最大的缺点之一是消失梯度问题再次出现。然而,这并不是因为有非常深的神经网络层,而是因为试图评估任意长的序列。RNNs 中使用的激活函数通常是 tanh 激活函数。从数学上来说,我们定义如下:

$$ \tanh \left(\mathrm{x}\right)=\frac{ex-{e}{-x}}{ex+{e}{-x}} $$

图 2-6 说明了激活功能。

img/463133_1_En_2_Fig6_HTML.jpg

图 2-6

Tanh 激活和导数函数

类似于 sigmoid 激活函数的问题,双曲正切函数的导数可以为 0,从而当在大序列上反向传播时,导致等于 0 的梯度。类似于 MLP,这可能会导致学习问题。根据激活函数的选择,我们也可能会遇到与消失梯度问题相反的情况——爆炸梯度。简单地说,这是作为 NaN 值出现的梯度的结果。对于 RNNs 中的消失梯度函数有几个解。其中包括通过 L1 或 L2 范数尝试权重正则化,或者尝试不同的激活函数,就像我们在 MLP 所做的那样,利用诸如 ReLU。然而,更直接的解决方案之一是使用 Sepp Hochreiter 和 Jürgen Schmidhuber 在 20 世纪 90 年代设计的模型:长短期记忆单位,或 LSTM。先说这个模型长什么样,如图 2-7 。

img/463133_1_En_2_Fig7_HTML.png

图 2-7

LSTM 单位/街区

LSTMs 在结构上的区别在于,我们将它们视为块或单元,而不是神经网络通常显示的传统结构。也就是说,同样的原则通常也适用于此。然而,我们对普通 RNN 的隐藏状态进行了改进。我将遍历与 LSTM 相关的公式。

$$ {i}_t=\sigma \left({W}_{xi}{x}_t+{W}_{hi}{h}_{t-1}+{W}_{hc}{c}_{t-1}+{b}_i\right) $$

(2.12)

$$ {f}_t=\sigma \left({W}_{xf}{x}_t+{W}_{hf}{h}_{t-1}+{W}_{hf}{c}_{t-1}+{b}_f\right) $$

(2.13)

$$ {c}_t={f}_t\circ {c}_{t-1}+{i}_t\circ \tanh \left({W}_{xc}{x}_t+{W}_{hc}{h}_{t-1}+{b}_c\right) $$

(2.14)

$$ {o}_t=\sigma \left({W}_{xo}{x}_t+{W}_{ho}{h}_{t-1}+{W}_{co}{c}_t+{b}_o\right) $$

(2.15)

$$ {h}_t={o}_t\circ \tanh \left({c}_t\right) $$

(2.16)

i t 是输入门, f t 是遗忘门, c t 是细胞状态, o t 是输出门, h t 在算法初始化时,隐藏和单元状态都被初始化为 0。

来自 LSTM 的配方类似于香草 RNN,但是有一些轻微的复杂性。首先,让我们注意图表,特别是中间的 LSTM 单位,并理解与公式相关的方向流。初步来说,我们讨论一下记谱法。用img/463133_1_En_2_Figa_HTML.jpg表示的每个块代表一个神经网络层,我们通过它传递值。带箭头的水平线代表数据移动的向量和方向。在它通过神经网络层之后,数据通常被传递给逐点操作对象,用img/463133_1_En_2_Figb_HTML.jpg表示。

既然我已经讨论了如何阅读图表,让我们更深入地探讨一下。

LSTMs 的区别在于具有控制通过单个单元的信息以及什么信息传递到下一个单元的门。分别是输入门、输出门和遗忘门。除了这三个门,LSTM 还包含一个单元,这是该单元的一个重要方面。

在图上,单元格用水平线表示,数学上用方程 2.14 表示。单元格状态类似于隐藏状态,这里和 RNN 都有描述,除了我们可以自由决定从一个单元传递到下一个单元的信息量。当查看该图时,一个输入,xt通过输入门。这里,神经网络通过一个神经网络层,带有一个 sigmoid 激活函数,将输出传递给一个逐点乘法运算符。这个操作与遗忘门 f t 结合,就是方程 2.14 的全部。

最重要的是,你应该从这个操作中得到的是,它的输出是一个介于 0 和 1 之间的数。数字越接近 1,传递给后续单元的信息就越多。相反,数字越接近 0,传递给后续单元的信息就越少。

在等式 2.13 中,遗忘门是调节这种信息接受的东西,它由c??表示。

转到等式 2.15 并将其与图表关联起来,这是最右边的神经网络层,它通过另一个 sigmoid 层,以类似的方式进入输入层。然后,这个 sigmoid 激活的神经网络层的输出乘以 tanh 激活的细胞状态向量,在方程 2.16 中。最后,我们将细胞状态向量和输出向量传递到下一个 LSTM 单元。虽然我没有以与 RNN 相同的方式绘制 LSTM,但我使用了 LSTM 的 TensorFlow API 实现。

玩具示例 3:用 LSTM 模型模拟股票收益

正如我们之前的神经网络示例一样,我们仍然必须创建 TensorFlow 占位符和变量。对于这个例子,LSTM 需要数据序列,我们首先创建一个三维的 X 占位符变量。为了避免在使用不同数据集部署此 API 时出现调试问题,您应该仔细阅读以下说明

    X = tf.placeholder(tf.float32, (None, None, train_x.shape[1]))
    Y = tf.placeholder(tf.float32, (None, train_y.shape[1]))
    weights = {'output': tf.Variable(tf.random_normal([n_hidden, train_y.shape[1]]))}
    biases = {'output': tf.Variable(tf.random_normal([train_y.shape[1]]))}
    input_series = tf.reshape(X, [-1, train_x.shape[1]])
    input_series = tf.split(input_series, train_x.shape[1], 1)

    lstm = rnn.core_rnn_cell.BasicLSTMCell(num_units=n_hidden, forget_bias=1.0, reuse=None, state_is_tuple=True)
    _outputs, states = rnn.static_rnn(lstm, input_series, dtype=tf.float32)
    predictions = tf.add(tf.matmul(_outputs[-1], weights['output']), biases['output'])
    accuracy = tf.reduce_mean(tf.cast(tf.equal(tf.argmax(tf.nn.softmax(predictions), 1)tf.argmax(Y, 1)), dtype=tf.
    float32)),
    error = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=Y, logits=predictions))
    adam_optimizer = tf.train.AdamOptimizer(learning_rate).minimize(error)

当创建一个变量序列时,我们首先创建一个名为 X 的三维占位符,这是我们输入数据的地方。我们通过用tf.reshape()创建一个观察值的二维向量来转换这个变量。

接下来,我们用tf.split()函数为每个观察值创建一个张量对象,然后作为列表存储在input_series下。

然后,我们可以使用BasicLSTMCell()函数创建一个 LSTM 单元格。static_rnn()函数接受任何类型的 RNN 单元,因此您可以利用其他类型的 rnn,如 GRUs 或 vanilla RNNs,以及输入。其他一切都遵循与前面示例相同的模式,因为我们创建 TensorFlow 变量来计算准确性、错误率和 Adam 优化器。

摘要

在我们深入研究在文本数据上使用这些模型解决问题之前,我们已经结束了对机器学习的简短但必要的回顾。但是,我们有必要回顾一些关键概念:

  • 型号选择至关重要!了解你正在分析的数据。您预测的标签是否依赖于其他先前观察到的标签,或者这些输入和输出在统计上是否相互独立?如果没有事先检查数据的这些关键属性,将会浪费时间,并为您提供次优的结果。不要跳过这些步骤。

  • 参数选择至关重要!为问题选择正确的模型是第一步,但是您必须适当地调整这个模型以获得最佳结果。更改隐藏单位和纪元的数量时,检查模型性能。我建议在网络训练时利用 Adam 等算法来调整学习速率。在可能的情况下,网格搜索或使用类似的反应式搜索方法来找到更好的参数。

  • 激活功能至关重要!注意你的神经网络在消失梯度问题上的表现,特别是当你处理长序列或者有非常深的神经网络时。

记住这些概念后,还有一个概念我们在本章中没有涉及到:数据预处理。用我们面临的问题来讨论比较合适。

让我们离开这一章,进入自然语言处理的领域,看几个例子问题。在下一章中,我们将介绍几种预处理文本的方法,讨论它们的优缺点,并比较使用它们时的模型性能。****

三、处理原始文本

那些打算应用深度学习的人最有可能立即面临一个简单的问题:机器学习算法如何学习解释文本数据?类似于特征集可能具有分类特征的情况,我们必须执行一些预处理。虽然我们在 NLP 中执行的预处理通常比使用标签编码简单地转换分类特征更复杂,但原理是相同的。我们需要找到一种方法,将文本的单个观察结果表示为一行,并对所有这些观察结果中的静态数量的特征进行编码,表示为列。因此,特征提取成为文本预处理最重要的方面。

令人欣慰的是,已经有相当多的工作,包括正在进行的工作,来开发各种复杂的预处理算法。本章介绍了这些预处理方法,介绍了它们各自适用的情况,并将它们应用于侧重于文档分类的示例 NLP 问题。让我们从讨论在从文本中执行特征提取之前应该知道什么开始。

标记化和停用词

例如,当您处理原始文本数据时,尤其是当它使用 web crawler 从网站获取信息时,您必须假设并非所有文本都有助于从中提取特征。事实上,很可能会有更多的噪声被引入数据集,并使给定机器学习模型的训练变得不那么有效。因此,我建议您执行初步步骤。让我们使用下面的示例文本来完成这些步骤。

sample_text = "'I am a student from the University of Alabama. I
was born in Ontario, Canada and I am a huge fan of the United States. I am going to get a degree in Philosophy to improve
my chances of becoming a Philosophy professor. I have been
working towards this goal for 4 years. I am currently enrolled
in a PhD program. It is very difficult, but I am confident that
it will be a good decision"'

sample_text变量打印时,有如下输出:

'I am a student from the University of Alabama. I
was born in Ontario, Canada and I am a huge fan of the United
States. I am going to get a degree in Philosophy to improve my
chances of becoming a Philosophy professor. I have been working
towards this goal for 4 years. I am currently enrolled in a PhD program. It is very difficult, but I am confident that it will
be a good decision'

您应该观察到,计算机将文本(即使有标点符号)作为单个字符串对象读取。正因为如此,我们需要找到一种方法来分离这种单一的文本主体,以便计算机将每个单词作为一个单独的字符串对象进行评估。这让我们想到了单词标记化的概念,它只是将单个字符串对象(通常是不同长度的文本主体)分离成代表单词或字符的单个标记的过程,我们希望进一步评估这些单词或字符。尽管您可以找到从头实现它的方法,但为了简洁起见,我建议您使用自然语言工具包(NLTK)模块。

NLTK 允许您使用一些更基本的 NLP 功能,以及针对不同任务的预训练模型。我的目标是让你训练你自己的模型,所以我们不会在 NLTK 中使用任何预训练的模型。但是,您应该通读 NLTK 模块文档,熟悉某些加快文本预处理的函数和算法。回到我们的例子,让我们通过下面的代码来标记样本数据:

from nltk.tokenize import word_tokenize, sent_tokenize
sample_word_tokens = word_tokenize(sample_text)
sample_sent_tokens = sent_tokenize(sample_text)

当您打印sample_word_tokens变量时,您应该注意以下几点:

['I', 'am', 'a', 'student', 'from', 'the', 'University', 'of', 'Alabama', '.', 'I', 'was', 'born', 'in', 'Ontario', ',', 'Canada', 'and', 'I', 'am', 'a', 'huge', 'fan', 'of', 'the', 'United', 'States', '.', 'I', 'am', 'going', 'to', 'get', 'a', 'degree', 'in', 'Philosophy', 'to', 'improve', 'my', 'chances', 'of', 'becoming', 'a', 'Philosophy', 'professor', '.', 'I', 'have', 'been', 'working', 'towards', 'this', 'goal', 'for', '4', 'years', '.', 'I', 'am', 'currently', 'enrolled', 'in', 'a', 'PhD', 'program', '.', 'It', 'is', 'very', 'difficult', ',', 'but', 'I', 'am', 'confident', 'that', 'it', 'will', 'be', 'a', 'good', 'decision']

您还会发现我们已经定义了另一个标记化的对象,sample_sent_tokensword_tokenize()sent_tokenize()的区别仅仅在于后者通过句子分隔符来标记文本。这可以在以下输出中观察到:

 ['I am a student from the University of Alabama.', 'I \nwas born in Ontario, Canada and I am a huge fan of the United States.', 'I am going to get a degree in Philosophy to improve my chances of \nbecoming a Philosophy professor.', 'I have been working towards this goal\nfor 4 years.', 'I am currently enrolled in a PhD program.', 'It is very difficult, \nbut I am confident that it will be a good decision']

现在我们有了可以预处理的单个令牌!从这一步开始,我们可以清除一些我们不想从中提取特征的垃圾文本。通常,我们首先要去除的是停用词,它们通常被定义为给定语言中非常常见的词。最常见的是,我们在软件包中构建或利用的停用词列表包括虚词,这些虚词表达一种语法关系(而不是具有内在意义)。虚词的例子包括、的

在这个例子中,我们使用 NLTK 包中的停用词列表。

[u'i', u'me', u'my', u'myself', u'we', u'our', u'ours', u'ourselves', u'you', u"you're", u"you've", u"you'll", u"you'd", u'your', u'yours', u'yourself', u'yourselves', u'he', u'him', u'his', u'himself', u'she', u"she's", u'her', u'hers', u'herself', u'it', u"it's", u'its', u'itself', u'they', u'them', u'their', u'theirs', u'themselves', u'what', u'which', u'who', u'whom', u'this', u'that', u"that'll", u'these', u'those', u'am', u'is', u'are', u'was', u'were', u'be', u'been', u'being', u'have', u'has', u'had', u'having', u'do', u'does', u'did', u'doing', u'a', u'an', u'the', u'and', u'but', u'if', u'or', u'because', u'as', u'until', u'while', u'of', u'at', u'by', u'for', u'with', u'about', u'against', u'between', u'into', u'through', u'during', u'before', u'after', u'above', u'below', u'to', u'from', u'up', u'down', u'in', u'out', u'on', u'off', u'over', u'under', u'again', u'further', u'then', u'once', u'here', u'there', u'when', u'where', u'why', u'how', u'all', u'any', u'both', u'each', u'few', u'more', u'most', u'other', u'some', u'such', u'no', u'nor', u'not', u'only', u'own', u'same', u'so', u'than', u'too', u'very', u's', u't', u'can', u'will', u'just', u'don', u"don't", u'should', u"should've", u'now', u'd', u'll', u'm', u'o', u're', u've', u'y', u'ain', u'aren', u"aren't", u'couldn', u"couldn't", u'didn', u"didn't", u'doesn', u"doesn't", u'hadn', u"hadn't", u'hasn', u"hasn't", u'haven', u"haven't", u'isn', u"isn't", u'ma', u'mightn', u"mightn't", u'mustn', u"mustn't", u'needn', u"needn't", u'shan', u"shan't", u'shouldn', u"shouldn't", u'wasn', u"wasn't", u'weren', u"weren't", u'won', u"won't", u'wouldn', u"wouldn't"]

默认情况下,所有这些单词都是小写的。您应该知道,在比较两个单独的字符串时,字符串对象必须完全匹配才能返回真正的布尔变量。更直白地说,如果我们要执行代码“你”==“你”,Python 解释器返回 false。可以通过分别执行mistake()advised_preprocessing()函数来观察影响我们示例的具体实例。观察以下输出:

['I', 'student', 'University', 'Alabama', '.', 'I', 'born', 'Ontario', ',', 'Canada', 'I', 'huge', 'fan', 'United', 'States', '.', 'I', 'going', 'get', 'degree', 'Philosophy', 'improve', 'chances', 'becoming', 'Philosophy', 'professor', '.', 'I', 'working', 'towards', 'goal', '4', 'years', '.', 'I', 'currently', 'enrolled', 'PhD', 'program', '.', 'It', 'difficult', ',', 'I', 'confident', 'good', 'decision']

['student', 'University', 'Alabama', '.', 'born', 'Ontario', ',', 'Canada', 'huge', 'fan', 'United', 'States', '.', 'going', 'get', 'degree', 'Philosophy', 'improve', 'chances', 'becoming', 'Philosophy', 'professor', '.', 'working', 'towards', 'goal', '4', 'years', '.', 'currently', 'enrolled', 'PhD', 'program', '.', 'difficult', ',', 'confident', 'good', 'decision']

如您所见,mistake()函数没有捕捉大写的“I”字符,这意味着文本中还有几个停用词。这可以通过大写所有停用词,然后评估样本文本中的每个大写单词是否在stop_words列表中来解决。下面两行代码对此进行了举例说明:

stop_words = [word.upper() for word in stopwords.words('english')]
word_tokens = [word for word in sample_word_tokens if word.upper() not in stop_words]

虽然特征提取算法中的嵌入式方法可能会考虑到这种情况,但您应该知道字符串必须完全匹配,并且在手动预处理时必须考虑到这一点。

也就是说,有些垃圾数据是你应该注意的——特别是语法字符。听说word_tokenize()函数也将冒号和分号归类为单独的单词标记,您会松一口气,但您仍然必须去掉它们。幸运的是,NLTK 包含另一个值得了解的记号赋予器,在下面的代码中定义并使用了它:

from nltk.tokenize import RegexpTokenizer
tokenizer = RegexpTokenizer(r'\w+')
sample_word_tokens = tokenizer.tokenize(str(sample_word_tokens))
sample_word_tokens = [word.lower() for word in sample_word_tokens]

当我们打印sample_word_tokens变量时,我们得到以下输出:

['student', 'university', 'alabama', 'born', 'ontario', 'canada', 'huge', 'fan', 'united', 'states', 'going', 'get', 'degree', 'philosophy', 'improve', 'chances', 'becoming', 'philosophy', 'professor', 'working', 'towards', 'goal', '4', 'years', 'currently', 'enrolled', 'phd', 'program', 'difficult', 'confident', 'good', 'decision']

在这个例子的过程中,我们已经到了最后一步!我们已经删除了所有的标准停用词,以及所有的语法标记。这是一个准备进行特征提取的文档示例,在此基础上可能会进行一些额外的预处理。

接下来,我将讨论一些不同的特征提取算法。让我们在预处理过的示例段落旁边处理更密集的样本数据。

单词袋模型(BoW)

弓形模型是你会遇到的最简单的特征提取算法之一。“单词包”这个名字来源于这种算法,它只是简单地寻找一个给定单词在一段文本中出现的次数。这里不分析单词的顺序或上下文。类似地,如果我们有一个装满六支铅笔、八支钢笔和四个笔记本的包,算法只关心记录这些物体的数量,而不是它们被发现的顺序或它们的方向。

这里,我定义了一个示例单词包函数。

def bag_of_words(text):
    _bag_of_words = [collections.Counter(re.findall(r'\w+', word)) for word in text]
    bag_of_words = sum(_bag_of_words, collections.Counter())
    return bag_of_words

sample_word_tokens_bow = bag_of_words(text=sample_word_tokens)
print(sample_word_tokens_bow)

当我们执行上述代码时,会得到以下输出:

Counter({'philosophy': 2, 'program': 1, 'chances': 1, 'years': 1, 'states': 1, 'born': 1, 'towards': 1, 'canada': 1, 'huge': 1, 'united': 1, 'goal': 1, 'working': 1, 'decision': 1, 'currently': 1, 'confident': 1, 'going': 1, '4': 1, 'difficult': 1, 'good': 1, 'degree': 1, 'get': 1, 'becoming': 1, 'phd': 1, 'ontario': 1, 'fan': 1, 'student': 1, 'improve': 1, 'professor': 1, 'enrolled': 1, 'alabama': 1, 'university': 1})

这是一个作为字典呈现的蝴蝶结模型的例子。显然,这不是一种适合机器学习算法的输入格式。这让我讨论了 scikit-learn 库中可用的无数文本预处理函数,这是一个所有数据科学家和机器学习工程师都应该熟悉的 Python 库。对于新手来说,这个库提供了机器学习算法的实现,以及一些数据预处理算法。虽然我们不会详细介绍这个包,但是文本预处理函数非常有用。

数理器

让我们从 BoW 等价物开始,CountVectorizer 是一个单词包的实现,在这个实现中,我们将文本数据编码为特征/单词的表示。这些特征中的每一个的值代表单词在所有文档中的出现次数。如果您还记得,我们定义了一个sample_sent_tokens变量,我们将对其进行分析。我们在预处理数据的地方定义了一个bow_sklearn()函数。该函数定义如下:

from sklearn.feature_extraction.text import CountVectorizer
def bow_sklearn(text=sample_sent_tokens):
    c = CountVectorizer(stop_words='english', token_pattern=r'\w+')
    converted_data = c.fit_transform(text).todense()
    print(converted_data.shape)
    return converted_data, c.get_feature_names()

为了提供上下文,在本例中,我们假设每个句子都是一个单独的文档,并且我们正在创建一个特性集,其中每个特性都是一个单独的标记。当我们实例化CountVectorizer()时,我们设置了两个参数:stop_wordstoken_pattern。这两个参数是移除停用词和语法标记的特征提取中的嵌入方法。fit_transform()属性期望接收一个列表、一个数组或类似的可迭代字符串对象。我们将bow_datafeature_names变量分别赋给bow_sklearn()返回的数据。我们转换后的数据集是一个 6 × 50 的矩阵,也就是说我们有 6 个句子,每个句子都有 50 个特征。在下面的输出中,分别观察我们的数据集和特性名称:

[[0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 0 0]
 [0 0 1 1 0 0 0 0 0 0 0 1 0 0 0 1 0 1 0 0 0 0 1 0 1 0 0 0]
 [0 0 0 0 1 0 0 0 1 0 0 0 0 1 0 0 1 0 0 2 1 0 0 0 0 0 0 0]
 [1 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1]
 [0 0 0 0 0 0 1 0 0 0 1 0 0 0 0 0 0 0 1 0 0 1 0 0 0 0 0 0]
 [0 0 0 0 0 1 0 1 0 1 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0]]

[u'4', u'alabama', u'born', u'canada', u'chances', u'confident', u'currently', u'decision', u'degree', u'difficult', u'enrolled', u'fan', u'goal', u'going', u'good', u'huge', u'improve', u'ontario', u'phd', u'philosophy', u'professor', u'program', u'states', u'student', u'united', u'university', u'working', u'years']

将这个例子推广到更大数量的文档和表面上更大的词汇表,我们预处理文本数据的矩阵往往有大量的特征,有时超过 1000 个。如何有效地评估这些特征是我们寻求解决的机器学习挑战。您通常希望使用单词袋特征提取技术进行文档分类。为什么会这样呢?我们假设某些分类的文档包含某些单词。例如,我们期望一份引用政治学的文件可能会出现诸如辩证唯物主义自由市场资本主义这样的术语;而涉及古典音乐的文档会有诸如渐强渐弱等术语。在这些文档分类的例子中,单词本身的位置并不十分重要。了解一类文档和另一类文档中出现的词汇部分是很重要的。

接下来,让我们看看text_classifiction_demo.py文件中代码的第一个示例问题。

示例问题 1:垃圾邮件检测

垃圾邮件检测是一项相对常见的任务,因为大多数人都有一个被广告商或恶意行为者锁定的收件箱(电子邮件、社交媒体即时消息帐户或类似实体)。能够阻止不想要的广告或恶意文件是一项重要的任务。正因为如此,我们有兴趣寻求一种机器学习方法来检测垃圾邮件。在深入研究这个问题之前,让我们先描述一下数据集。

这个数据集是从 UCI 机器学习库,特别是文本数据部分下载的。我们的数据集由 5574 条观察数据组成,都是短信。我们从我们的数据集中观察到,大多数消息并不太长。图 3-1 是我们整个数据集的直方图。

img/463133_1_En_3_Fig1_HTML.jpg

图 3-1

SMS 消息长度直方图

我们应该注意的另一件事是类别标签之间的分布,这往往是严重倾斜的。在这个数据集中,4825 个观察被标记为“ham”(不是垃圾邮件),747 个被标记为“垃圾邮件”。在评估机器学习解决方案时,你必须保持警惕,以确保它们不会过度拟合训练数据,然后在测试数据上惨败。

在直接解决问题之前,让我们简单地做一些额外的数据集发现。当我们查看数据集的标题时,我们观察到以下情况:

0   ham  Go until jurong point, crazy.. Available only ...
1   ham                      Ok lar... Joking wif u oni...
2  spam  Free entry in 2 a wkly comp to win FA Cup fina...
3   ham  U dun say so early hor... U c already then say...
4   ham  Nah I don't think he goes to usf, he lives aro...

第一列是我们的分类标签/响应变量。第二列包括包含在每个 SMS 中的文本。我们将通过CountVectorizer()使用单词袋表示法。我们的整个数据集有 8477 个单词的词汇量。load_spam_data()函数显示预处理步骤模拟了本章开头的预热示例。

让我们拟合和训练我们的模型,并评估结果。开始分类任务时,我建议您评估逻辑回归的结果。这决定了你的数据是否是线性可分的。如果是这样的话,逻辑回归应该工作得很好,这样可以省去进一步的模型选择和耗时的超参数优化。如果失败了,你可以使用这些方法。

我们在text_classifiction_demo.py文件中使用 L1 和 L2 权重正则化来训练模型;然而,我们将在这里遍历 L1 范数正则化的例子,因为它产生了更好的测试结果:

#Fitting training algorithm
l = LogisticRegression(penalty='l1')
accuracy_scores, auc_scores = [], []

不熟悉逻辑回归的人应该去别处学习。然而,我将简要讨论 L1 正则化的逻辑回归。线性模型中的 L1 范数正则化是 LASSO(最小绝对收缩选择算子)的标准,其中在学习过程中,L1 范数理论上可以将一些回归系数强制为 0。相比之下,岭回归中常见的 L2 范数可以在学习过程中迫使一些回归系数接近于 0。这两者之间的区别在于,为 0 的系数通过消除它们来有效地对我们的特征集执行特征选择。数学上,我们通过方程 3.1 来表示这种正则化。

$$ \min \sum \limits_{i=1}^M-\log p\left({y}i|{x}I;\theta \right)+\beta {\left|\left|\theta \right|\right|}_1 $$

(3.1)

我们将评估几次试验的测试分数分布。scikit-learn algorithms 的fit()方法训练给定数据集的算法。这样,优化参数的所有迭代都被执行。要查看训练过程中的日志信息,请将verbose参数设置为 1。

让我们看一下将收集准确性和 AUC 分数分布的代码。

for i in range(trials):
   if i%10 == 0 and i > 0:
        print('Trial ' + str(i) + ' out of 100 completed')
   l.fit(train_x, train_y)
   predicted_y_values = l.predict(train_x)
   accuracy_scores.append(accuracy_score(train_y, predicted_y_values))
   fpr, tpr = roc_curve(train_y, predicted_y_values)[0], roc_curve(train_y, predicted_y_values)[1]
   auc_scores.append(auc(fpr, tpr))

scikit-learn 执行交叉验证,只要您使用np.random.seed()函数定义一个随机种子,我们在文件的开头附近执行。在每次试验中,我们将数据集与算法进行拟合,预测准确性和 AUC 得分,并将它们附加到我们定义的列表中。当我们评估培训结果时,我们观察到以下情况:

Summary Statistics (AUC):
        Min       Max      Mean      SDev    Range
0  0.965348  0.968378  0.967126  0.000882  0.00303

Summary Statistics (Accuracy Scores):
        Min      Max      Mean      SDev     Range
0  0.990356  0.99116  0.990828  0.000234  0.000804

Test Model Accuracy: 0.9744426318651441
Test True Positive Rate: 0.8412698412698413
Test False Positive Rate: 0.004410838059231254

[[1580    7]
 [  40  212]]

幸运的是,我们看到逻辑回归在这个问题上表现出色。我们有极好的准确性和 AUC 评分,每次试验之间的差异很小。我们来评估一下 AUC 评分,如图 3-2 所示。

img/463133_1_En_3_Fig2_HTML.jpg

图 3-2

测试集 ROC 曲线

我们的测试 AUC 分数是 0.92。该算法可部署在应用程序中,以测试垃圾邮件结果。在发现解决方案的过程中,我建议您使用这个模型,而不是其他模型。虽然鼓励你寻找其他方法,但我观察到梯度推进的分类树和随机森林的表现要差得多,AUC 分数大约为 0.72。让我们讨论一个更复杂的术语频率方案。

术语频率逆文档频率

术语频率-逆文档频率 (TFIDF)基于 BoW,但提供了比简单采用术语频率更详细的信息,如前面的示例。TFIDF 产生一个值,通过不仅查看术语频率,而且分析该词在所有文档中出现的次数,来显示给定词的重要性。第一部分,术语频率,相对简单。

让我们看一个例子,看看如何计算 TFIDF。我们定义一个新的正文,并使用本章开头定义的示例文本,如下所示:

  • text = " "我是宾夕法尼亚大学的学生,但现在在工作

  • 华尔街当律师。我已经在纽约生活了大约五年

  • 然而现在,我期待着一旦我有了就退休回德州

  • 存了足够的钱来这样做。”‘

document_list = list([sample_text, text])

现在我们有了一个文档列表,让我们看看 TFIDF 算法到底做了什么。第一部分,术语频率,有几个变量,但我们将重点放在标准的原始计数方案。我们简单地将所有文档中的术语相加。频率一词相当于方程式 3.2 。

$$ \frac{f_{t,d}}{\sum_{t^{\prime}\in d}{f}_{t^{\prime },d}} $$

(3.2)

f td 等于该术语在所有文档中的出现频率。$$ {f}_{t^{\prime },d} $$等于同一术语在每个单独文档中的出现频率。在我们的代码中,我们将这些步骤记录在tf_idf_example()函数中,如下所示:

def tf_idf_example(textblobs=[text, text2]):

def term_frequency(word, textblob): (1)
return textblob.words.count(word)/float(len(textblob.words))

def document_counter(word, text):
return sum(1 for blob in text if word in blob)

def idf(word, text): (2)
return np.log(len(text) /1 + float(document_counter(word, text)))

def tf_idf(word, blob, text):
return term_frequency(word, blob) * idf(word, text)

output = list()
for i, blob in enumerate(textblobs):
output.append({word: tf_idf(word, blob, textblobs) for word in blob.words})
print(output)

多亏了 TextBlob 包,我们能够相当快速地重新创建 TFIDF toy 实现。我将介绍tf_idf_example()函数中的每个函数。你知道术语频率,所以我可以讨论逆文档频率。我们将逆文档频率定义为一个词在整个语料库中出现频率的度量。在数学上,这种关系用方程式 3.3 表示。

$$ \mathrm{idf}\left(t,D\right)=\log \frac{N}{\left|\left{d\in D:t\in d\right}\right|} $$

(3.3)

这个等式计算我们的语料库中的文档总数的对数,除以我们正在评估的术语出现的所有文档。在我们的代码中,我们用函数(2)来计算。现在,我们准备进入算法的最后一步,将术语频率乘以逆文档频率,如前面的代码所示。然后,我们得到以下输出:

 [{'up': '0.027725887222397813', 'money': '0.021972245773362195', 'am': '0.027725887222397813', 'years': '0.027725887222397813', 'as': '0.027725887222397813', 'at': '0.027725887222397813', 'have': '0.055451774444795626', 'in': '0.027725887222397813', 'New': '0.021972245773362195', 'saved': '0.021972245773362195', 'Texas': '0.021972245773362195', 'living': '0.021972245773362195', 'for': '0.027725887222397813', 'to': '0.08317766166719343', 'retiring': '0.027725887222397813', 'been': '0.021972245773362195', 'looking': '0.021972245773362195', 'Pennsylvania': '0.021972245773362195', 'enough': '0.021972245773362195', 'York': '0.021972245773362195', 'forward': '0.027725887222397813', 'was': '0.027725887222397813', 'eventually': '0.021972245773362195', 'do': '0.027725887222397813', 'I': '0.11090354888959125', 'University': '0.027725887222397813', 'however': '0.027725887222397813', 'but': '0.021972245773362195', 'five': '0.021972245773362195', 'student': '0.021972245773362195', 'now': '0.04394449154672439', 'a': '0.055451774444795626', 'on': '0.027725887222397813', 'Wall': '0.021972245773362195', 'of': '0.027725887222397813', 'work': '0.021972245773362195', 'roughly': '0.021972245773362195', 'Street': '0.021972245773362195', 'so': '0.021972245773362195', 'Lawyer': '0.021972245773362195', 'the': '0.027725887222397813', 'once': '0.021972245773362195'}, {'and': '0.0207285337484549', 'is': '0.0207285337484549', 'each': '0.0207285337484549', 'am': '0.026156497379620575', 'years': '0.026156497379620575', 'have': '0.05231299475924115', 'in': '0.026156497379620575', 'children': '0.0414570674969098', 'considering': '0.0207285337484549', 'retirement': '0.0207285337484549', 'doctor': '0.0207285337484549', 'retiring': '0.026156497379620575', 'two': '0.0207285337484549', 'long': '0.0207285337484549', 'next': '0.0207285337484549', 'to': '0.05231299475924115', 'forward': '0.026156497379620575', 'was': '0.026156497379620575', 'couple': '0.0207285337484549', 'more': '0.0207285337484549', 'ago': '0.0207285337484549', 'them': '0.0207285337484549', 'that': '0.0207285337484549', 'I': '0.1046259895184823', 'University': '0.026156497379620575', 'who': '0.0414570674969098', 'however': '0.026156497379620575', 'quite': '0.0207285337484549', 'me': '0.0207285337484549', 'Yale': '0.0207285337484549', 'with': '0.0207285337484549', 'the': '0.05231299475924115', 'a': '0.07846949213886173', 'both': '0.0207285337484549', 'look': '0.026156497379620575', 'of': '0.026156497379620575', 'grandfather': '0.0207285337484549', 'spending': '0.0207285337484549', 'three': '0.0207285337484549', 'time': '0.0414570674969098', 'making': '0.0207285337484549', 'went': '0.0207285337484549'}]

这就把我们带到了使用 TFIDF 的玩具示例的结尾。在我们进入这个例子之前,让我们回顾一下我们将如何在 scikit-learn 中利用这个例子,以便我们可以将这些数据输入到机器学习算法中。与CountVectorizer()类似,scikit-learn 提供了一个很方便的TfidfVectorizer()方法。下面显示了它的应用。稍后我将深入探讨其预处理方法的使用。

def tf_idf_sklearn(document=document_list):
    t = TfidfVectorizer(stop_words='english', token_pattern=r'\w+')
    x = t.fit_transform(document_list).todense()
    print(x)

当我们执行该函数时,它会产生以下结果:

[[0\.         0\.         0\.         0\.         0\.         0.24235766
  0.17243947 0\.         0.24235766 0.24235766 0\.         0.
  0.24235766 0\.         0.24235766 0.24235766 0.24235766 0.
  0\.         0.17243947 0.24235766 0.24235766 0\.         0.24235766
  0.24235766 0.24235766 0\.         0.17243947 0.24235766 0.
  0.24235766 0\.         0.17243947 0.24235766]
 [0.20840129 0.41680258 0.20840129 0.20840129 0.20840129 0.
  0.14827924 0.20840129 0\.         0\.         0.20840129 0.20840129
  0\.         0.20840129 0\.         0\.         0\.         0.20840129
  0.20840129 0.14827924 0\.         0\.         0.20840129 0.
  0\.         0\.         0.41680258 0.14827924 0\.         0.20840129
  0\.         0.20840129 0.14827924 0\.        ]]

这个函数产生一个 2 × 44 的矩阵,并且它准备好输入到机器学习算法中进行评估。

现在,让我们使用 TFIDF 作为我们的特征提取器来解决另一个示例问题,同时利用另一个机器学习算法,就像我们对 BoW 特征提取所做的那样。

示例问题 2:对电影评论进行分类

我们从 http://www.cs.cornell.edu/people/pabo/movie-review-data/ 获得了以下 IMDB 电影评论数据集。

我们将直接处理原始文本,而不是使用通常通过各种机器学习包提供的预处理文本数据集。

让我们拍一张数据快照。

tristar / 1 : 30 / 1997 / r ( language , violence , dennis rodman ) cast : jean-claude van damme ; mickey rourke ; dennis rodman ; natacha lindinger ; paul freeman director : tsui hark screenplay : dan jakoby ; paul mones ripe with explosions , mass death and really weird hairdos , tsui hark's " double team " must be the result of a tipsy hollywood power lunch that decided jean-claude van damme needs another notch on his bad movie-bedpost and nba superstar dennis rodman should have an acting career . actually , in " double team , " neither's performance is all that bad . i've always been the one critic to defend van damme -- he possesses a high charisma level that some genre stars ( namely steven seagal ) never aim for ; it's just that he's never made a movie so exuberantly witty since 1994's " timecop . " and rodman . . . well , he's pretty much rodman . he's extremely colorful , and therefore he pretty much fits his role to a t , even if the role is that of an ex-cia

正如您所看到的,这些数据充满了我们需要消除的大量语法干扰,但也包含丰富的描述性文本。我们将选择对该数据使用TfidfVectorizer()方法。

首先,我想让您了解文件开头的两个函数:

def remove_non_ascii(text):
    return ".join([word for word in text if ord(word) < 128])

注意,我们正在使用本机 Python 函数ord()。该函数需要一个字符串,它返回 Unicode 对象的 Unicode 点或字节值。如果ord()函数返回一个小于 128 的整数,这对我们的预处理程序没有问题,因此我们保留这个字符串;否则,我们删除字符。我们通过使用".join()函数将所有剩余的单词连接在一起来结束这一步。在数据准备期间进行预处理的原因是,我们的文本预处理器在接收 Unicode 对象时需要 Unicode 对象。当我们捕获原始文本数据时,特别是当它来自 HTML 页面时,在预处理和删除停用词之前加载的许多字符串对象将不是 Unicode 兼容的。

让我们看看加载数据的函数。

def load_data():
    negative_review_strings = os.listdir('/Users/tawehbeysolow/Downloads/review_data/tokens/neg')
    positive_review_strings = os.listdir('/Users/tawehbeysolow/Downloads/review_data/tokens/pos')
    negative_reviews, positive_reviews = [], []

我们首先加载所有要处理的.txt文件的文件名。为此,我们使用了os.listdir()函数。我建议您在构建需要预处理大量文件的类似应用程序时使用该函数。

接下来,我们用open()函数加载文件,然后应用remove_non_ascii()函数,如下所示:

    for positive_review in positive_review_strings:
        with open('/Users/tawehbeysolow/Downloads/review_data/tokens/pos/'+str(positive_review), 'r') as positive_file:
            positive_reviews.append(remove_non_ascii(positive_file.read()))

    for negative_review in negative_review_strings:
        with open('/Users/tawehbeysolow/Downloads/review_data/tokens/neg/'+str(negative_review), 'r') as negative_file:
            negative_reviews.append(remove_non_ascii(negative_file.read()))

完成初始预处理后,我们通过连接正面和负面评论以及包含其标签的相应向量来结束。现在,我们可以进入这个机器学习问题的核心,从train_logistic_model()函数开始。以类似的方式,我们使用逻辑回归作为问题的基线。虽然下面的大部分函数在结构上与例题 1 相似,但是我们还是来看看这个函数的开头,分析一下我们做了哪些改变。

#Load and preprocess text data
x, y = load_data()
t = TfidfVectorizer(min_df=10, max_df=300, stop_words="english", token_pattern=r'\w+')
x = t.fit_transform(x).todense()

我们利用了两个新参数:min_df对应于保留一个单词的最小文档频率,而max_df指的是一个单词在从我们创建的稀疏矩阵中省略之前可以出现在文档中的最大数量。当增加最大和最小文档频率时,我注意到 L1 惩罚模型比 L2 惩罚模型表现得更好。我认为这很可能是因为当我们增加min_df参数时,我们正在创建一个比我们有一个更密集的矩阵要稀疏得多的矩阵。您应该记住这一点,以免在他们事先对矩阵执行任何特征选择时过度选择特征。

让我们评估逻辑回归的结果,如以下输出所示(另见图 3-3 和 3-4 )。

img/463133_1_En_3_Fig3_HTML.jpg

图 3-3

L1 逻辑回归测试集 ROC 曲线

Summary Statistics from Training Set (AUC):
       Mean       Max  Range      Mean  SDev
0  0.723874  0.723874    0.0  0.723874   0.0
Summary Statistics from Training Set (Accuracy):
       Mean       Max  Range      Mean  SDev
0  0.726788  0.726788    0.0  0.726788   0.0
Training Data Confusion Matrix:
[[272 186]
 [ 70 409]]

Summary Statistics from Test Set (AUC):
       Mean       Max  Range      Mean  SDev
0  0.723874  0.723874    0.0  0.723874   0.0
Summary Statistics from Test Set (Accuracy):
        Mean       Max  Range      Mean  SDev
0  0.726788  0.726788    0.0  0.726788   0.0
Test Data Confusion Matrix:
[[272 186]
 [ 70 409]]

Summary Statistics from Training Set (AUC):
       Mean       Max  Range      Mean  SDev
0  0.981824  0.981824    0.0  0.981824   0.0
Summary Statistics from Training Set (Accuracy):
       Mean       Max  Range      Mean  SDev
0  0.981857  0.981857    0.0  0.981857   0.0
Training Data Confusion Matrix:
[[449   9]
 [  8 471]]

Summary Statistics from Test Set (AUC):
       Mean       Max  Range      Mean  SDev
0  0.981824  0.981824    0.0  0.981824   0.0
Summary Statistics from Test Set (Accuracy):
        Mean       Max  Range      Mean  SDev
0  0.981857  0.981857    0.0  0.981857   0.0

Test Data Confusion Matrix:
[[449   9]
 [  8 471]]

考虑到我们用于TfidfVectorizer()特征提取算法的参数,当利用 L2 权重正则化方法时,逻辑回归在训练和性能上都表现得更好。

img/463133_1_En_3_Fig4_HTML.jpg

图 3-4

L2 逻辑回归测试集 ROC 曲线

我创建了多个解决方案来评估:随机森林分类器、朴素贝叶斯分类器和多层感知器。我们首先对我们所有的方法及其各自的方向进行概述。

mlp_movie_classification_model.py文件中的多层感知器开始,注意神经网络的大部分与第二章中的例子相同,除了一个额外的隐藏层。也就是说,我想请你注意第 92 到 94 行。

regularization = tf.contrib.layers.l2_regularizer(scale=0.0005, scope=None)
regularization_penalty = tf.contrib.layers.apply_regularization(regularization, weights.values())
cross_entropy = cross_entropy + regularization_penalty

在这些方面,我们正在执行权重正则化,正如本章前面讨论的逻辑回归 L2 和 L1 损失参数。那些希望在 TensorFlow 中应用这一点的人可以放心,这些是为你的神经网络增加权重惩罚所需的唯一修改。在开发这个解决方案的过程中,我尝试了利用 L1 和 L2 损失惩罚来调整体重,并且尝试了辍学。权重正则化是当利用不同的向量范数时限制权重可以增长的范围的过程。权重正则化的两个最常用的规范是 L1 规范和 L2 规范。以下是它们各自的方程,在图 3-5 中也有说明。

$$ {L}_1={\left|\left|\mathbf{v}\right|\right|}_{\mathbf{1}}=\sum \limits_{i=\mathbf{1}}N{\left|{v}_i\right|}{\mathbf{1}} $$

$$ {L}_2={\left|\left|\mathbf{v}\right|\right|}_{\mathbf{2}}=\sqrt{\sum \limits_{i=\mathbf{1}}N{\left|{v}_i\right|}{\mathbf{2}}} $$

img/463133_1_En_3_Fig5_HTML.jpg

图 3-5

L1 和 L2 规范可视化

当最初使用一个和两个隐藏层时,我注意到即使使用低至 0.05 的退出百分比,测试和训练性能也会因退出而明显变差。因此,我不建议你利用辍学来解决这个问题。至于权重正则化,额外的参数选择是不可取的;然而,我发现 L1 与 L2 正则化的差异可以忽略不计。混淆矩阵和 ROC 曲线如图 3-6 所示。

img/463133_1_En_3_Fig6_HTML.jpg

图 3-6

多层感知器摇摆曲线

Test Set Accuracy Score: 0.8285714285714286
Test Set Confusion Matrix:
[[122  26]
 [ 22 110]]

让我们分析随机森林和朴素贝叶斯分类器的参数选择。我们把我们的树保持在相对较短的 10 等分。至于朴素贝叶斯分类器,我们选择的唯一参数是 alpha,我们将其设置为 0.005。让我们评估图 3-6 和 3-7 的模型结果。

img/463133_1_En_3_Fig7_HTML.jpg

图 3-7

随机森林的 ROC 曲线

图 3-8 显示了朴素贝叶斯分类器的结果。

img/463133_1_En_3_Fig8_HTML.jpg

图 3-8

朴素贝叶斯分类器的 ROC 曲线

Summary Statistics from Training Set Random Forest (AUC):
       Mean       Max  Range      Mean  SDev
0  0.987991  0.987991    0.0  0.987991   0.0
Summary Statistics from Training Set Random Forest (Accuracy):
      Mean      Max  Range     Mean  SDev
0  0.98826  0.98826    0.0  0.98826   0.0
Training Data Confusion Matrix (Random Forest):
[[447  11]
 [  0 479]]
Summary Statistics from Training Set Naive Bayes (AUC):
       Mean       Max  Range      Mean          SDev
0  0.965362  0.965362    0.0  0.965362  2.220446e-16
Summary Statistics from Training Set Naive Bayes (Accuracy):
       Mean       Max  Range      Mean          SDev
0  0.964781  0.964781    0.0  0.964781  3.330669e-16
Training Data Confusion Matrix (Naive Bayes):
[[454   4]
 [ 29 450]]
Test Data Confusion Matrix:
[[189  27]
 [ 49 197]]
Test Data Confusion Matrix (Random Forest):
[[162  54]
 [ 19 227]]

在评估结果时,神经网络有过度拟合训练数据的趋势,但其测试性能非常类似于逻辑回归,尽管精确度略低。当评估朴素贝叶斯分类器和随机森林分类器的结果时,我们观察到大致相似的 AUC 分数,只有假阳性和真阳性的差异,这是我们必须接受的权衡。在这种情况下,重要的是考虑我们的目标。

如果我们使用这些算法来标记用户输入的评论,然后在这些评论的基础上进行分析,我们希望最大化准确率,或者寻求具有最高真正率和真负率的模型。在垃圾邮件检测的实例中,我们可能希望模型能够最好地将垃圾邮件从正常邮件中正确分类。

我在逻辑模型和朴素贝叶斯分类器中引入并应用了词袋方案。这就把我们带到了本节的最后一部分,在这一部分,我将讨论它们的相对优缺点。你应该意识到这一点,这样才不会浪费时间去修改不合格的解决方案。BoW 的主要优势在于,它是一种相对简单的算法,允许您快速将文本转换为机器学习算法可以解释的格式,并直接解决 NLP 问题。

BoW 最大的缺点就是相对简单。BoW 没有考虑单词的上下文,因此,对于更复杂的 NLP 任务,它不是理想的特征提取方法。例如,“4”和“4”被认为是语义上无法区分的,但是在 BoW 中,它们被认为是两个完全不同的单词。当我们将其扩展到短语“我上了四年大学”和“我上了四年大学”时,它们被视为正交向量。BoW 缺点的另一个例子是它不能区分单词的顺序。因此,“我是愚蠢的”和“我是愚蠢的”是同一个向量。

由于这些缺点,对我们来说,利用更高级的模型来解决这些难题是合适的,比如单词嵌入,这将在下一章中详细讨论。

摘要

这就把我们带到了第三章的结尾!本章讨论了在文档分类中处理文本数据的问题。您还熟悉了两种弓特征提取方法。

让我们花一点时间来回顾一下这一章中最重要的一些教训。就像传统的机器学习一样,你必须定义问题的类型并分析数据。这是单纯的文档分类吗?我们是在试图寻找同义词吗?在采取任何其他步骤之前,我们必须回答这些问题。

停用词、语法标记和常用词的删除提高了我们算法的准确性。并非文档中的每个单词都是信息丰富的,因此您应该知道如何去除干扰。也就是说,过度选择功能可能会对我们的模型的成功不利,所以你也应该意识到这一点!

每当你在处理一个机器学习问题时,无论是在 NLP 领域之内还是之外,你都必须建立一个基线解决方案,然后在必要时进行改进!我建议你总是通过查看解决方案如何出现来开始一个深度学习问题,比如用逻辑回归。虽然我的目标是教你如何将深度学习应用于基于 NLP 的问题,但没有理由使用过于复杂的方法,而不太复杂的方法会做得更好或同样好(除非你喜欢练习你的深度学习技能)。

最后,虽然预处理方法是有用的,但是基于 BoW 的模型最好与文档分类一起使用。对于更高级的 NLP 问题,如情感分析、理解语义和类似的抽象问题,BoW 可能不会产生最好的结果。

四、主题建模和单词嵌入

现在,您已经对处理文本数据有了一个介绍,让我们深入研究一种更高级的特征提取算法。为了解决一些更困难的问题,我有理由向您介绍处理 NLP 问题的其他技术。我们将浏览 Word2Vec、Doc2Vec 和 GloVe。

主题模型与潜在狄利克雷分配

主题模型是一种从文本主体中提取信息的方法,用来查看所有文档中出现的“主题”。直觉告诉我们,我们希望某些主题在相关文档中出现得更多,而在不相关的文档中出现得更少。当使用我们与文档相关联的主题作为更好、更直观的搜索的关键字时,或者当使用它进行速记摘要时,这可能是有用的。在应用这个模型之前,先说一下我们实际上是如何抽取话题的。

潜在狄利克雷分配(LDA)是 2003 年由戴维·布雷、吴恩达和迈克尔·乔丹提出的一个生成模型。他们在论文中强调了 TFIDF 的缺点。最值得注意的是,TFIDF 无法理解单词的语义,或者单词在文本中的位置。这导致了 LDA 的崛起。LDA 是一个生成模型,这意味着它输出给定现象的所有可能结果。在数学上,我们可以将这些假设描述如下:

  1. Choose N ~ 泊松 ( ξ )(文档内的一系列 N 单词具有泊松分布)

  2. 选择 θ ~ Dir ( α )(参数 θ 具有狄利克雷分布)

  3. 对于每个 N 字(wN):

    • 选择题目zn~多项式 ( θ )(每个题目 z n 都有多项式分布。)

    • 选择wnfromp(wn|znβ ,一个以题目为条件的多项概率 z n 。(每个主题被表示为单词的分布,其中概率是从第 n 个单词的概率中生成的,取决于主题以及 β 其中βij=p(wj= 1 |zI=))

      β =给定单词的概率, V =词汇中的单词数, k =狄利克雷分布的维数, θ =从概率单纯形中采样的随机变量。

让我们讨论一下这些假设中使用的一些分布。泊松分布表示在固定时间或空间以恒定速率发生的事件,与自上次事件以来的时间无关。这种分布的一个例子是在给定时间段内呼叫比萨饼店送货的人数的模型。多项式分布是二项分布的 k 结果推广;换句话说,与二项式分布相同的概念,但是扩展到有两个以上结果的情况。

最后,狄利克雷分布是贝塔分布的推广,但扩展到处理多元数据。贝塔分布是概率的分布。

LDA 假设(1)单词是从具有固定条件分布的主题生成的,以及(2)文档内的主题是无限可交换的,这意味着这些主题的联合概率分布不受它们被表示的顺序的影响。回顾陈述 1 和陈述 2,我们可以说主题中的单词不是无限可交换的。

让我们讨论参数 θ (从狄利克雷分布中提取),该参数利用了分布的维度 k 。我们假设 k 已知且固定,k 维狄利克雷随机变量 θ 可以取(k—1)概率单形中的任意值。这里,我们将概率单纯形定义为我们从中抽取随机变量的分布区域,用图形表示为具有 k + 1 个顶点的多维三角形。单纯形本身的概率分布可以表示如下:

$$ p\left(\theta |\alpha \right)=\left(\frac{\varGamma \left({\sum}_{i=1}k{\alpha}_i\right)}{\prod_{i=1}k\varGamma \left({\alpha}_i\right)}\right){\theta}_1^{\alpha_1-1},\dots \kern1em ,{\theta}_k^{\alpha_1-1} $$

(4.1)

α =正实数的 k 向量。γ(x)=γ函数。

随后,我们将混合主题的联合分布定义如下:

$$ p\left(\theta, \kern0.5em \mathbf{z},\kern0.5em \mathbf{w}\kern0.5em |\kern0.5em \alpha, \kern0.5em \beta \right)=p\left(\theta |\alpha \right)\kern0.5em \prod \limits_{n=1}^Np\left({z}_n|\theta \right)p\left({w}_n|{z}_n,\beta \right) $$

(4.2)

因此,给定的单词和主题序列必须具有以下形式:

$$ p\left(\mathbf{w},\kern0.5em \mathbf{z}\right)=\int p\left(\theta \right)\left(\kern0.5em \prod \limits_{n=1}^Np\Big({z}_n|\theta \right)p\left({w}_n\kern0.5em |{z}_n\right) $$

(4.3)

在 LDA 论文中,作者为方程 4.1 、 4.2 和 4.3 提供了一个有用的图解,如图 4-1 所示。

img/463133_1_En_4_Fig1_HTML.jpg

图 4-1

主题和单词简单

LDA 论文中给出的示例将图 4-1 描述为由三个单词组成的单词单形中的主题单形的图示。每个单纯形点分别代表一个给定的单词和主题。

在我们结束对 LDA 背后的理论的讨论之前,让我们用 Python 重新创建所有的工作。幸运的是,scikit-learn 提供了 LDA 的一个实现,我们将在接下来的例子中使用它。

基于 LDA 的电影评论数据主题建模

接下来,我们看看在我们的文档分类示例中使用的相同的电影评论数据。下面是我们将首先用来创建主题模型的一些代码的例子。我们将从 sklearn 中的一个实现开始。

def create_topic_model(model, n_topics=10, max_iter=5, min_df=10, max_df=300, stop_words="english", token_pattern=r'\w+'):
    print(model + ' topic model: \n')
    data = load_data()[0]
    if model == 'tf':
        feature_extractor = CountVectorizer(min_df=min_df, max_df=max_df, stop_words=stop_words, token_pattern=r'\w+')
    else:
        feature_extractor = TfidfVectorizer(min_df=min_df, max_df=max_df, stop_words=stop_words, token_pattern=r'\w+')
    processed_data = feature_extractor.fit_transform(data)

我们加载我们在第三章中使用的电影评论来解决分类问题。在这个例子中,我们将假设我们想要为给定数量的电影评论创建一个主题模型。

注意

我们正在从以前的文件导入load_data()函数。要执行lda_demo.py file,使用从code_applied_nlp_python目录的相对导入,并执行以下命令:'python –m chapter4.topic_modeling'

我们用函数加载数据,然后准备好输入 LDA fit_transform()方法。像其他 NLP 问题一样,我们不能将原始文本放入任何算法中;我们必须以某种形式对其进行预处理。然而,为了产生主题模型,我们将利用术语频率和 TFIDF 算法,但主要是为了比较结果。

让我们看看函数的其余部分。

lda_model = LatentDirichletAllocation(n_topics=n_topics, learning_method="online", learning_offset=50., max_iter=max_iter, verbose=1)
lda_model.fit(processed_data)
tf_features = feature_extractor.get_feature_names()
print_topics(model=lda_model, feature_names=tf_features, n_top_words=n_top_words)

当我们执行下面的函数时,我们得到这个输出:

tf topic model:
Topic #0: libby fugitive douglas sarah jones lee detective double innocent talk
Topic #1: beatty joe hanks ryan crystal niro fox mail kathleen shop
Topic #2: wars phantom lucas effects menace neeson jedi anakin special computer
Topic #3: willis mercury simon rising jackal bruce ray lynch baseball hughes
Topic #4: godzilla broderick redman bvoice kim michael bloomington mission space york
Topic #5: planet apes joe sci fi space ape alien gorilla newman
Topic #6: d american fun guy family woman day ll james bit
Topic #7: bond brosnan bottle message blake theresa pierce tomorrow dies crown
Topic #8: van spielberg amistad gibson en home american kevin ending sense
Topic #9: scream 2 wild williamson horror smith kevin arquette sidney finn

既然这是电影数据,我们可以看到主题指的是电影和围绕它的上下文。例如,主题#4 列出了“哥斯拉”(表面上是一个角色)和“布罗德里克”(表面上是一个演员)。我们还可以利用其他特征提取方法生成主题模型。

现在让我们看看使用 TFIDF 特征提取器时主题模型的结果。

tfidf topic model:
Topic #0: libby driver jacket attending terrorists tends finn doom tough parodies
Topic #1: godzilla beatty douglas arthur christ technology burns jesus york cases
Topic #2: wars lucas episode menace jar niro jedi darth anakin phantom
Topic #3: sub theron genre keaton cooper victor rita irene dating rules
Topic #4: midnight kim stiller mulan spice newman disney junkie troopers strange
Topic #5: clooney palma kevin pacino snake home toy woody pfeiffer space
Topic #6: anna disney jude carpenter men wrong siege lee king family
Topic #7: scream got mail bond hanks book performances summer cute dewey
Topic #8: en van z n er met reese die fallen lou
Topic #9: family american effects home guy woman michael original 10 james

有相似的结果,尽管我们在某些主题上得到的结果略有不同。在某些方面,TFIDF 模型比术语-频率模型更难解释。

在我们继续之前,让我们讨论一下如何在新的包中利用 LDA 模型。Gensim 是一个机器学习库,主要致力于将机器学习和深度学习应用于 NLP 任务。下面是在gensim_topic_model()函数中使用这个包的代码:

def gensim_topic_model():

    def remove_stop_words(text): (1)
        word_tokens = word_tokenize(text.lower())
        word_tokens = [word for word in word_tokens if word not in stop_words and re.match('[a-zA-Z\-][a-zA-Z\-]{2,}', word)]
        return word_tokens

    data = load_data()[0]
    cleaned_data = [remove_stop_words(data[i]) for i in range(0, len(data))]

当使用这个包时,Gensim LDA 实现期望不同于 Gensim 实现的输入,尽管它仍然需要预处理。当看函数时,我们必须使用一个专有函数来删除停用词,就像我们在第三章中所做的那样。除此之外,我们应该注意去掉那些出现得太频繁和不够频繁的单词。幸运的是,Gensim 在corpora.Dictionary()函数中提供了一个方法来实现这一点,如下所示:

dictionary = gensim.corpora.Dictionary(cleaned_data)
dictionary.filter_extremes(no_below=100, no_above=300)
corpus = [dictionary.doc2bow(text) for text in cleaned_data]
lda_model = models.LdaModel(corpus=corpus, num_topics=n_topics, id2word=dictionary, verbose=1)

类似于 scikit-learn 方法,我们可以基于文档频率过滤对象。我们在这里采取的预处理步骤与sklearn_topic_model()函数中的略有不同,这将成为我们在本节末尾讨论的中心。与您之前看到的类似,预处理步骤中看似微小的变化可能会导致截然不同的结果。

我们执行gensim_topic_model()函数,得到以下结果:

Gensim LDA implemenation:
Topic #0: 0.116*"movie" + 0.057*"people" + 0.051*"like" + 0.049*"good" + 0.041*"well" + 0.038*"film" + 0.037*"one" + 0.037*"story" + 0.033*"great" + 0.028*"new"
Topic #1: 0.106*"one" + 0.063*"movie" + 0.044*"like" + 0.043*"see" + 0.041*"much" + 0.038*"story" + 0.033*"little" + 0.032*"good" + 0.032*"way" + 0.032*"get"
Topic #2: 0.154*"film" + 0.060*"one" + 0.047*"like" + 0.039*"movie" + 0.037*"time" + 0.032*"characters" + 0.031*"scene" + 0.028*"good" + 0.028*"make" + 0.027*"little"
Topic #3: 0.096*"film" + 0.076*"one" + 0.060*"even" + 0.053*"like" + 0.051*"movie" + 0.040*"good" + 0.036*"time" + 0.033*"get" + 0.030*"would" + 0.028*"way"
Topic #4: 0.079*"film" + 0.068*"plot" + 0.058*"one" + 0.057*"would" + 0.049*"like" + 0.039*"two" + 0.038*"movie" + 0.036*"story" + 0.035*"scenes" + 0.033*"much"
Topic #5: 0.136*"film" + 0.067*"movie" + 0.064*"one" + 0.039*"first" + 0.037*"even" + 0.037*"would" + 0.036*"time" + 0.035*"also" + 0.029*"good" + 0.027*"like"
Topic #6: 0.082*"movie" + 0.072*"get" + 0.068*"film" + 0.059*"one" + 0.046*"like" + 0.036*"even" + 0.035*"know" + 0.027*"much" + 0.027*"way" + 0.026*"story"

Topic #7: 0.131*"movie" + 0.097*"film" + 0.061*"like" + 0.045*"one" + 0.032*"good" + 0.029*"films" + 0.027*"see" + 0.027*"bad" + 0.025*"would" + 0.025*"even"
Topic #8: 0.139*"film" + 0.060*"movie" + 0.052*"like" + 0.044*"story" + 0.043*"life" + 0.043*"could" + 0.041*"much" + 0.032*"well" + 0.031*"also" + 0.030*"time"
Topic #9: 0.116*"film" + 0.091*"one" + 0.059*"movie" + 0.035*"two" + 0.029*"character" + 0.029*"great" + 0.027*"like" + 0.026*"also" + 0.026*"story" + 0.026*"life"

到目前为止,使用术语频率作为我们的特征提取器的 LDA 的 scikit-learn 实现的结果给出了最可解释的结果。大多数结果是同质的,这可能不会导致太多的差异,从而使结果不太有用。

使用相同的数据集,让我们利用另一个主题提取模型。

非负矩阵分解(NMF)

非负矩阵分解(NMF)是一种算法,它获取一个矩阵并返回两个没有非负元素的矩阵。NMF 与矩阵分解密切相关,只是 NMF 只接收非负值(0 和任何大于 0 的值)。

我们希望利用 NMF,而不是另一种类型的矩阵分解,因为我们需要正系数,就像使用 LDA 时的情况一样。我们可以用下面的数学公式来描述这个过程:

$$ \mathrm{V}=\mathrm{WH} $$

矩阵 V 是我们输入数据的原始矩阵。我们输出的两个矩阵是 W 和 h。在本例中,假设矩阵 V 有 1000 行和 200 列。每行代表一个单词,每列代表一个文档。因此,我们有一个跨越 200 个文档的 1000 个单词的词汇表。由于与前面的等式有关,V 是一个 m × n 矩阵,W 是一个 m × p 矩阵,H 是一个 p × n 矩阵。w 是一个特征矩阵。

假设我们想要找到五个特征,从而生成具有 1000 行和 5 列的矩阵 W。矩阵 H 随后具有相当于 5 行 200 列的形状。当我们对 W 和 H 执行矩阵乘法时,我们得到具有 1000 行和 200 列的矩阵 V,相当于前面描述的维数。我们认为每个文件都是由许多隐藏的特征构成的,因此 NMF 会生成这些特征。下面是我们将在本例中使用的 NMF 的 scikit-learn 实现:

def nmf_topic_model():

    def create_topic_model(model, n_topics=10, max_iter=5, min_df=10,
                           max_df=300, stop_words="english", token_pattern=r'\w+'):
        print(model + ' NMF topic model: ')
        data = load_data()[0]
        if model == 'tf':
            feature_extractor = CountVectorizer(min_df=min_df, max_df=max_df,
                                 stop_words=stop_words, token_pattern=token_pattern)
        else:
            feature_extractor = TfidfVectorizer(min_df=min_df, max_df=max_df,
                                 stop_words=stop_words, token_pattern=token_pattern)

        processed_data = feature_extractor.fit_transform(data)
        nmf_model = NMF(n_components=n_components, max_iter=max_iter)
        nmf_model.fit(processed_data)
        tf_features = feature_extractor.get_feature_names()
        print_topics(model=nmf_model, feature_names=tf_features, n_top_words=n_topics)

    create_topic_model(model='tf')

我们以与调用 LDA 主题提取模型几乎相同的方式调用 NMF 主题提取。让我们看看术语频率预处理数据和 TFIDF 预处理数据的输出。

tf NMF topic model:
Topic #0: family guy original michael sex wife woman r men play
Topic #1: jackie tarantino brown ordell robert grier fiction pulp jackson michael
Topic #2: jackie hong drunken master fu kung chan arts martial ii
Topic #3: scream 2 williamson horror sequel mr killer sidney kevin slasher
Topic #4: webb jack girl gives woman ll male killed sir talking
Topic #5: musical musicals jesus death parker singing woman nation rise alan
Topic #6: bulworth beatty jack political stanton black warren primary instead american
Topic #7: godzilla effects special emmerich star york computer monster city nick
Topic #8: rock kiss city detroit got music tickets band soundtrack trying
Topic #9: frank chicken run shannon ca mun html sullivan particularly history

以下是 TFIDF NMF 主题模型:

Topic #0: 10 woman sense james sex wife guy school day ending
Topic #1: scream horror williamson 2 sidney craven stab killer arquette 3
Topic #2: wars phantom jedi lucas menace anakin jar effects darth gon
Topic #3: space deep alien ship armageddon harry effects godzilla impact aliens
Topic #4: disney mulan animated joe voice toy animation apes mermaid gorilla
Topic #5: van amistad spielberg beatty cinque political slavery en slave hopkins
Topic #6: carpenter ott ejohnsonott nuvo subscribe reviews johnson net mail e
Topic #7: hanks joe ryan kathleen mail shop online fox tom meg
Topic #8: simon sandler mercury adam rising willis wedding vincent kevin julian
Topic #9: murphy lawrence martin eddie ricky kit robbins miles claude police

在我们评估结果并对这两种方法进行更彻底的讨论之前,让我们先来关注结果的可视化。在前面的例子中,我们合理地降低了复杂性,以便用户可以评估所分析文档中的不同主题。然而,当我们想要查看大量数据并相对快速地从这个主题模型中做出推断时,这就没有什么帮助了。

我们将从我认为是一个有用的情节开始,由 pyLDAvis 提供。这个软件非常有用,当与 Jupyter 笔记本一起使用时,工作起来相对容易,这对于代码可视化和结果展示来说是非常好的。当使用来自亚马逊网络服务(AWS)或谷歌云的虚拟机实例时,通常使用 Jupyter 笔记本。

注意

对于没有使用过 Google Cloud 或者 AWS 的人,我推荐这些教程:Google Compute Engine:www.youtube.com/watch?v=zzMCKv1g5z0AWS:www.youtube.com/watch?v=q1vVedHbkAY

设置一个实例并启动一个 Jupyter 笔记本。我们将做一些小的调整,让它在你的本地机器上运行,然后在云中运行。在这个例子中,scikit-learn 实现——给定提供的预处理算法——使得收集可解释的主题比 Gensim 模型容易得多。尽管 Gensim 提供了更多的灵活性和很多特性,但它要求您从头开始微调预处理步骤。如果你有时间从头开始构建结果,这不是问题;然而,在构建自己的应用程序时要记住这一点,并考虑在 Gensim 中使用这种方法的困难。

在这个演示中,NMF 和 LDA 通常给出相似的结果;然而,选择一种模型还是另一种模型通常与我们理解数据的方式有关。LDA 假设主题是无限可交换的,但是主题中的单词不是。因此,如果我们不担心每个文档的主题概率保持不变(假设不会,因为不是所有的文档在大型语料库中都包含相同的主题),LDA 是更好的选择。如果我们对固定话题概率有很高的把握,并且数据集相当小,那么 NMF 可能是一个更好的选择。同样,在评估各个主题模型的结果时,应该考虑这些陈述,就像所有机器学习问题一样。

让我们讨论一种在情感分析中起作用的更高级的建模技术(除了更高级的 NLP 任务之外):单词嵌入。我们首先讨论一组算法:Word2Vec。

Word2Vec

2014 年,托马斯·米科洛夫、伊利亚·苏茨基弗、程凯、格雷格·科拉多和杰弗里·迪恩在谷歌工作时创造了 Word2Vec。Word2Vec 代表了 NLP 相关任务向前迈出的重要一步,因为它提供了一种寻找单词和短语的向量表示的方法,并且可以像文档一样进行扩展。

首先,让我们检查 Skip-Gram 模型,这是一个浅层神经网络,其目标是根据单词周围的单词来预测序列中的单词。让我们以下面的训练词序列为例:

$$ w=\left({w}_1,\kern0.5em {w}_2,\dots, \kern0.5em {w}_T\right) $$

目标函数是平均对数概率,表示如下:

$$ \frac{1}{T}\sum \limits_{t=1}^T\sum \limits_{-c\le j\le c,j\ne 0}\log p\left({w}_{t+j}|{w}_t\right) $$

$$ p\left({w}_{t+j}|{w}_t\right)=p\left({w}_O|{w}_I\right)=\frac{\exp \left({v_{w_O}{\prime}}T{v}_{wi}\right)}{\sum_{w=1}^W\exp \left({v_w{\prime}}T{v}_{wi}\right)\kern0.5em } $$

c =训练上下文的大小, T =训练单词的总数, t =当前单词的索引位置, j =确定我们正在查找的序列中的哪个单词的窗口, w t =序列的中心单词, W =词汇表中的单词数。

在我们继续之前,理解这个公式以及它如何解释这个模型的作用是很重要的。n-gram 是 n 个单词的连续组合。跳格是 n-gram 的推广,这样我们就有了单词的分组,但它们不再需要连续;也就是说,我们可以跳过单词来创建跳格。数学上,我们通常将 k-skip-n-grams 定义如下:

$$ \left{{w}_{i_1},\kern0.5em {w}_{i_2},\dots, \kern0.5em {w}_{i_n}|\sum \limits_{j=1}^n{i}_j-{i}_{j-1}<k\right} $$

让我们假设以下是 k-skip-n-gram 模型的输入:

《街上的猫》

让我们也假设我们正在寻求创建一个 2-skip-bi-gram 模型。因此,培训示例如下:

  • “那只猫”、“那只猫”、“那只猫”、“那只猫”、“街”

  • “猫,那个”,“猫,下来”,“猫,那个”,“猫,街”

  • “下来,该”,“下来,猫”,“下来,该”,“下来,街”

  • “the,the”,“the,cat”,“the,down”,“The,street”

  • “街,那个”,“街,猫”,“街,下来”,“街,那个”

现在你明白了输入数据是如何用单词表示的了。

让我们讨论一下我们如何用神经网络来表示这些单词。跳格模型的输入层是具有 W 个分量的独热编码矢量。换句话说,向量的每个元素都代表词汇表中的一个单词。图 4-2 用图形表示了跳过程序架构。

img/463133_1_En_4_Fig2_HTML.jpg

图 4-2

跳格模型体系结构

神经网络的目标是预测在输入序列中最有可能出现的单词。这正是我们为什么要使用 softmax,以及最终你如何直观地理解公式。给定单词的输入,我们想要预测最有可能的单词,并且我们正在基于由神经网络观察到的输入和输出序列的整体来计算这个概率。

尽管如此,我们还有一个小问题。Softmax 计算与输入大小成比例,这对于该问题来说是个坏兆头,因为准确的结果可能需要大量的词汇用于训练数据。因此,经常有人建议我们使用另一种方法。经常引用的方法之一是负抽样。负采样在以下等式中定义:

$$ \log \sigma \left({v_{wo}{\prime}}T{v}_{wi}\right)+\sum \limits_{i=1}^k{\mathbb{E}}_{wi}\sim {P}_n(w)\kern0.5em \left[\log \sigma \left(-{v}_{wi}^T\kern0.5em {v}_{wi}\right)\right] $$

负采样通过近似其输出来实现比 softmax 激活函数更便宜的计算。更准确地说,我们将只改变单词嵌入中的 K 个权重,而不是计算它们。Word2Vec 论文建议在较小的数据集中使用 5 到 20 个单词进行采样,但在较大的数据集中使用 2 到 5 个单词可以获得积极的结果。

除了训练单词嵌入,我们实际上要用它做什么?与许多神经网络不同,主要目的不一定是为了预测而使用它,而是为了获得经过训练的隐藏层权重矩阵。隐层权重矩阵就是我们训练好的单词嵌入。一旦这个隐藏层被训练,某些单词就聚集在向量空间的区域中,在那里它们共享相似的上下文。

例题 4.2:训练单词嵌入(跳格)

让我们在 Gensim 和 TensorFlow 中通过一个演示示例来展示 Word2Vec 的强大功能。下面是开始实现 TensorFlow Word2Vec 跳格模型的一些代码:

def remove_non_ascii(text):
    return ".join([word for word in text if ord(word) < 128])

def load_data(max_pages=100):
    return_string = StringIO()
    device = TextConverter(PDFResourceManager(), return_string, codec='utf-8', laparams=LAParams())
    interpreter = PDFPageInterpreter(PDFResourceManager(), device=device)
    filepath = file('/Users/tawehbeysolow/Desktop/applied_nlp_python/datasets/economics_textbook.pdf', 'rb')
    for page in PDFPage.get_pages(filepath, set(), maxpages=max_pages, caching=True, check_extractable=True):
        interpreter.process_page(page)
    text_data = return_string.getvalue()
    filepath.close(), device.close(), return_string.close()
    return remove_non_ascii(text_data)

对于我们的示例问题,我们将利用 PDFMiner Python 模块。对于那些经常以不同形式解析数据的人,强烈推荐使用这个包。PDF 数据在解析方面臭名昭著,因为它通常充满了图像和元数据,这使得预处理数据变得很麻烦。幸运的是,PDFMiner 处理了大部分繁重的工作,使我们主要关心的只是清除停用词、语法字符和其他预处理步骤,这些相对简单。对于这个问题,我们会从一本经济学教科书上读到数据。

def gensim_preprocess_data():
    data = load_data()
    sentences = sent_tokenize(data)
    tokenized_sentences = list([word_tokenize(sentence) for sentence in sentences])
    for i in range(0, len(tokenized_sentences)):
        tokenized_sentences[i] = [word for word in tokenized_sentences[i] if word not in punctuation]
    return tokenized_sentences

我们现在开始基于句子对数据进行标记。在此步骤之前不要删除标点符号。NLTK 句子标记器依靠标点符号来决定在哪里根据句子分割数据。如果删除了这一点,可能会导致您调试一些无关紧要的东西。无论如何,数据应该采用的下一种格式是列表格式,其中每个条目都是一个句子,其单词被标记化,单词如下所示:

 [['This', 'text', 'adapted', 'The', 'Saylor', 'Foundation', 'Creative', 'Commons', 'Attribution-NonCommercial-ShareAlike', '3.0', 'License', 'without', 'attribution', 'requested', 'works', 'original', 'creator', 'licensee'], ['Saylor', 'URL', 'http', '//www.saylor.org/books', 'Saylor.org', '1', 'Preface', 'We', 'written', 'fundamentally', 'different', 'text', 'principles', 'economics', 'based', 'two', 'premises', '1'], ['Students', 'motivated', 'study', 'economics', 'see', 'relates', 'lives'], ['2'], ['Students', 'learn', 'best', 'inductive', 'approach', 'first', 'confronted', 'question', 'led', 'process', 'answer', 'question'], ['The', 'intended', 'audience', 'textbook', 'first-year', 'undergraduates', 'taking', 'courses', 'principles', 'macroeconomics', 'microeconomics'], ['Many', 'may', 'never', 'take', 'another', 'economics', 'course'], ['We', 'aim', 'increase', 'economic', 'literacy', 'developing', 'aptitude', 'economic', 'thinking', 'presenting', 'key', 'insights', 'economics', 'every', 'educated', 'individual', 'know'], ['Applications', 'ahead', 'Theory', 'We', 'present', 'theory', 'standard', 'books', 'principles', 'economics'], ['But', 'beginning', 'applications', 'also', 'show', 'students', 'theory', 'needed'], ['We', 'take', 'kind', 'material', 'authors', 'put', 'applications', 'boxes', 'place', 'heart', 'book'], ['Each', 'chapter', 'built', 'around', 'particular', 'business', 'policy', 'application', 'microeconomics', 'minimum', 'wages', 'stock', 'exchanges', 'auctions', 'macroeconomics', 'social', 'security', 'globalization', 'wealth', 'poverty', 'nations']

现在我们已经完成了数据的预处理,我们可以使用 Skip-Gram 模型的 Gensim 实现了。

def gensim_skip_gram():
    sentences = gensim_preprocess_data()
    skip_gram = Word2Vec(sentences=sentences, window=1, min_count=10, sg=1)
    word_embedding = skip_gram[skip_gram.wv.vocab] (1)

调用 Skip-Gram 模型相对简单,模型的训练也由我们负责。Skip-Gram 模型的训练过程模拟了所有神经网络的训练过程,其中我们通过所有层传递输入,然后通过每层中的每个相应权重反向传播误差,更新它们,直到我们达到损失容限阈值或达到最大次数。一旦单词嵌入已经被训练,我们通过用模型本身的wv.vocab属性索引模型来获得权重矩阵。

现在,让我们讨论将单词可视化为向量。

    pca = PCA(n_components=2)
    word_embedding = pca.fit_transform(word_embedding)

    #Plotting results from trained word embedding
    plt.scatter(word_embedding[:, 0], word_embedding[:, 1])
    word_list = list(skip_gram.wv.vocab)
    for i, word in enumerate(word_list):
        plt.annotate(word, xy=(word_embedding[i, 0], word_embedding[i, 1]))

单词嵌入以难以以其原始格式可视化的维度输出。因此,我们需要找到一种方法来降低这个矩阵的维数,同时还保留原始数据集的所有方差和属性。实现这一点的预处理方法是主成分分析(PCA)。简而言之,PCA 变换矩阵,使得除了特征值之外,它还返回称为特征向量的特征分解。为了显示二维图,我们想要创建一个产生两个主成分的变换。重要的是要记住,这些主成分是而不是与原始矩阵完全相同,而是与之相关的单词嵌入的正交变换。图 4-3 说明了矩阵。

img/463133_1_En_4_Fig3_HTML.jpg

图 4-3

通过 Gensim 生成的跳格词嵌入

在向量空间中,彼此靠近的单词出现在相似的上下文中,而彼此远离的单词就它们出现的上下文而言更不相似。余弦相似性是衡量这一点的常用方法。数学上,余弦距离描述如下:

$$ \cos \left(\theta \right)=\frac{\mathbf{A}\ast \mathbf{B}}{\left|\left|\mathbf{A}\right|\right|\left|\left|\mathbf{B}\right|\right|} $$

我们直观地将余弦相似性描述为两个给定向量的所有相应元素的乘积之和除以它们的欧几里德范数的乘积。具有 0 度差的两个向量产生余弦相似度 1;而相差 90 度的两个向量产生的余弦相似度为 0。下面是不同单词向量之间的一些余弦距离的例子:

Cosine distance for people  and Saylor
 -0.10727774727479297
Cosine distance for URL  and people
 -0.137377917173043
Cosine distance for money  and URL
 -0.03124461706797222
Cosine distance for What  and money
 -0.007384979727807199
Cosine distance for one  and What
 0.022940581092187863
Cosine distance for see  and one
 0.05983065381073224
Cosine distance for economic  and see
 -0.0530102968258333

Gensim 负责预处理数据的一些不太好的方面。然而,了解如何从头开始执行这些事情是很有用的,所以让我们尝试利用相同的数据实现一个单词嵌入,只是这次我们将在 TensorFlow 中完成。

让我们通过一个玩具实现来确保您知道模型在做什么,然后通过一个更容易部署的实现。

def tf_preprocess_data(window_size=window_size):

    def one_hot_encoder(index, vocab_size):
        vector = np.zeros(vocab_size)
        vector[index] = 1
        return vector

    text_data = load_data()
    vocab_size = len(word_tokenize(text_data))
    word_dictionary = {}
    for index, word in enumerate(word_tokenize(text_data)):
        word_dictionary[word] = index

    sentences = sent_tokenize(text_data)
    tokenized_sentences = list([word_tokenize(sentence) for sentence in sentences])
    n_gram_data = []

我们必须为 TensorFlow 准备与 Gensim 略有不同的数据。Gensim Word2Vec 方法为我们处理了大部分后端的事情,但是从头实现一个简单的概念证明并遍历算法是值得的。

我们首先制作一个字典,将一个单词与一个索引号相匹配。这个索引号形成了我们的独热编码输入和输出向量中的位置。

让我们继续预处理数据。

#Creating word pairs for word2vec model
    for sentence in tokenized_sentences:
        for index, word in enumerate(sentence):
            if word not in punctuation:
                for _word in sentence[max(index - window_size, 0):
                                      min(index + window_size, len(sentence)) + 1]:
                    if _word != word:
                        n_gram_data.append([word, _word])

前面的代码部分有效地创建了我们的 n-Gram,并最终模拟了 Skip-Gram 模型如何在句子中进行卷积,从而以最高的概率预测接下来的单词。然后我们创建一个 m × n 矩阵,其中 m 是我们输入序列中的单词数, n 是词汇表中的单词数。

#One-hot encoding data and creating dataset intrepretable by skip-gram model
x, y = np.zeros([len(n_gram_data), vocab_size]), np.zeros([len(n_gram_data), vocab_size])

for i in range(0, len(n_gram_data)):
    x[i, :] = one_hot_encoder(word_dictionary[n_gram_data[i][0]], vocab_size=vocab_size)
    y[i, :] = one_hot_encoder(word_dictionary[n_gram_data[i][1]], vocab_size=vocab_size)

return x, y, vocab_size, word_dictionary

前进到我们将用来构建我们的跳格模型的函数,我们从加载数据、词汇大小和单词字典开始。与其他神经网络模型一样,我们实例化占位符、变量和权重。根据图 4-2 所示的跳格模型图,我们只需要包含一个隐藏和一个输出权重矩阵。

def tensorflow_word_embedding(learning_rate=learning_rate, embedding_dim=embedding_dim):
    x, y, vocab_size, word_dictionary = tf_preprocess_data()

    #Defining tensorflow variables and placeholder
    X = tf.placeholder(tf.float32, shape=(None, vocab_size))
    Y = tf.placeholder(tf.float32, shape=(None, vocab_size))

   weights = {'hidden': tf.Variable(tf.random_normal([vocab_size, embedding_dim])),
               'output': tf.Variable(tf.random_normal([embedding_dim, vocab_size]))}

    biases = {'hidden': tf.Variable(tf.random_normal([embedding_dim])),
              'output': tf.Variable(tf.random_normal([vocab_size]))}

    input_layer = tf.add(tf.matmul(X, weights['hidden']), biases['hidden'])
    output_layer = tf.add(tf.matmul(input_layer, weights['output']), biases['output'])

在第五章中,我们将介绍如何实现负采样。然而,因为我们在这里使用的例子数量相对较少,所以我们可以利用 TensorFlow 提供的 softmax 的常规实现。最后,我们执行我们的图表,与其他 TensorFlow 模型一样,并观察图 4-4 所示的结果。

img/463133_1_En_4_Fig4_HTML.jpg

图 4-4

跳跃式语法的玩具实现中的单词向量

Cosine distance for dynamic  and limited
 0.4128825113896724
Cosine distance for four  and dynamic
 0.2833843609582811
Cosine distance for controversial  and four
 0.3266445485300576
Cosine distance for hanging  and controversial
 0.37105348488163503
Cosine distance for worked  and hanging
 0.44684699747383416
Cosine distance for Foundation  and worked
 0.3751656692569623

同样,这里提供的实现是而不是训练有素的单词嵌入的最终示例。我们将在第五章中更具体地处理这项任务,因为数据收集是我们必须更详细讨论的主要问题。然而,跳格模型只是我们可能遇到的单词嵌入的一种。

我们现在将通过处理连续单词袋模型来继续我们的讨论。

连续词袋

类似于跳格模型,连续词袋模型(CBoW)以预测单词为目标进行训练。然而,与跳格模型不同,我们并不试图预测给定序列中的下一个单词。相反,我们试图根据目标标签周围的上下文来预测一些中心词。让我们想象下面的输入数据句子:

"男孩走向红房子"

在 CBoW 模型的上下文中,我们可以想象我们会有一个如下所示的输入向量:

那个,男孩,那个,红色的房子

在这里,“行走”是我们试图预测的目标。从视觉上看,CBoW 模型如图 4-5 所示。

img/463133_1_En_4_Fig5_HTML.jpg

图 4-5

CBoW 模型表示

输入中的每个单词都用一个独热编码向量来表示。类似于 Skip-Gram 模型,输入向量的长度等于词汇表中的单词数。当评估我们的输入数据时,值“1”表示存在的单词,值“0”表示不存在的单词。在图 4-5 中,我们基于单词 w_t-2、w_ t-1、w_ t+1 和 w_t+2 来预测目标单词 w_t。

然后,我们使用权重和偏差矩阵对此输入向量执行加权求和操作,将这些值传递给投影层,这与 Skip-Gram 模型中的投影层类似。最后,除了利用 softmax 分类器之外,我们还利用输出权重和偏差矩阵的另一个加权和操作来预测类别标签。训练方法与跳格模型中使用的方法相同。

接下来,让我们使用 Gensim 来做一个简短的例子。

例题 4.2:训练单词嵌入(CBoW)

CBoW 的 Gensim 实现要求只更改一个参数,如下所示:

cbow = Word2Vec(sentences=sentences, window=skip_gram_window_size, min_count=10, sg=0)

我们调用这个方法并观察结果,其方式与我们对 Skip-Gram 模型所做的方式相同。图 4-6 显示了结果。

img/463133_1_En_4_Fig6_HTML.jpg

图 4-6

CBoW 单词嵌入可视化

单词表示的全局向量(手套)

GloVe 是一种现代和先进的单词矢量表示方法。2014 年,Jeffrey Pennington、Richard Socher 和 Christopher Manning 写了一篇论文,描述了 GloVe。这种类型的单词嵌入是对基于矩阵分解的单词表示以及 Skip-gram 模型的改进。基于矩阵分解的单词表示方法并不特别擅长根据单词的相似性来表示单词。然而,Skip-Gram 和 CBoW 在孤立的文本窗口上训练,并且不利用与基于矩阵的因式分解方法相同的信息。具体来说,当我们使用 LDA 来创建主题模型时,我们必须以这样的方式对文本进行预处理,即使用在整个文本的上下文中表示单词的统计信息对每个单词进行编码。有了 Skip-Gram 和 CBoW,独热编码向量就不能捕捉相同类型的复杂性。

GloVe 专门训练“全局单词到单词共现计数”共现是指两个单词以特定的顺序并排出现。所谓全局,我指的是我们正在分析的语料库中所有文档的同现计数。从这个意义上说,GloVe 利用了两种模型背后的一点直觉,试图克服上述替代方案各自的缺点。

让我们首先定义一个共现矩阵 x,矩阵中的每一项代表两个特定单词的共现次数。更具体地说,X i,j 表示单词 j 在单词 I 的上下文中出现的次数。

$$ {\mathbf{X}}_{\mathbf{i}}=\sum \limits_k;{X}_{i,k} $$

(4.4)

$$ {P}_{i,j}=P\left(j|i\right)=\frac{X_{i,j}}{X_i} $$

(4.5)

等式 4.4 被定义为任何单词在单词 I 的上下文中出现的次数。等式 4.5 是给定单词 I 的单词 j 的概率。我们将该概率定义为单词 j 在单词“I”的上下文中出现的同现次数与单词 I 的总同现次数

我建议模型应该评估同现概率的比率,我们定义如下:

$$ F\left({w}_i,\kern0.5em {w}_j,\kern0.5em {\tilde{w}}_k\right)=\frac{P_{ik}}{P_{jk}} $$

(4.6)

w ∈ ℝ d =单词向量和$$ {\tilde{w}}_k\in {\mathrm{\mathbb{R}}}^d $$ =上下文向量,f= exp(x)

你应该注意到我们对 F 的定义上面有一个星号,特别是表示 F 的值可以是多种值;然而,我们经常把它推导为前面的定义。 F 的目的是将从同现概率产生的值编码到单词嵌入中。

以下函数导出我们用来训练手套单词嵌入的目标标签和误差函数:

$$ F\left({w}_i^T{\tilde{w}}_k\right)={P}_{i,k} $$

(4.7)

$$ {w}_i^T{\tilde{w}}_j+{b}_i+{\tilde{b}}_j-\log {X}_{i,j} $$

(4.8)

$$ J=\sum \limits_{i,j=1}Vf\left({X}_{i,j}\right){\left({w}_iT{\tilde{w}}_j+{b}_i+{\tilde{b}}_j-\log {X}_{i,j}\right)}² $$

(4.9)

其中f(Xij)=加权函数

正如 GloVe 论文中所详述的,加权函数应该遵守一些规则。最重要的是,如果 f 是一个连续函数,它应该消失为 x → 0, f ( x )应该是非递减的,对于 x 的大值, f ( x )应该相对较小。这些规则是为了确保在单词嵌入的训练中不过度加权罕见或频繁的同现值。尽管加权函数可以改变,但 GloVe 的论文提出了以下等式:

$$ f(x)=\left{\begin{array}{c}{\left(\frac{x}{x_m}\right)}^{\alpha}\kern0.5em if\kern0.5em x<{x}_m\kern0.5em \ {}1\kern1em otherwise\end{array}\right. $$

xm=最大值 x ,固定为 100。相对于 x 值,加权函数产生图 4-7 所示的值。

img/463133_1_En_4_Fig7_HTML.jpg

图 4-7

手套的加权函数

既然我们已经回顾了模型,那么了解如何使用预训练的单词嵌入对您来说是很有用的,特别是由于获取所有这些数据的困难性,不是每个人都有时间或能力从头开始训练这些嵌入。尽管不一定有一个预先确定的位置来获取单词嵌入,但是您应该注意下面的 GitHub 存储库,它包含了大量单词嵌入的文件: https://github.com/3Top/word2vec-api#where-to-get-a-pretrained-models 。您可以自由地试验和部署这些单词嵌入来完成不同的任务。

对于这个例子,我们将使用包含 60 亿个单词和 50 个特征的手套单词嵌入。这个单词嵌入是从维基百科的数据中训练出来的,拥有 40 万个单词的词汇量。现在,让我们从代码开始,如下所示:

def load_embedding(embedding_path='/path/to/glove.6B.50D.txt'):
    vocabulary, embedding = [], []
    for line in open(embedding_path, 'rb').readlines():
        row = line.strip().split(' ')
        vocabulary.append(row[0]), embedding.append(row[1:])
    vocabulary_length, embedding_dim = len(vocabulary), len(embedding[0])
    return vocabulary, np.asmatrix(embedding), vocabulary_length, embedding_dim

我们通过使用本机的open()函数加载单词嵌入来开始这个问题,以逐行读取文件。文件中的每一行都以词汇表中的一个单词开始,该行中的后续条目代表该单词的每个向量中的值。我们遍历文件中的所有行,将单词和单词向量附加到它们各自的数组中。因此,我们能够在一个词汇表中创建一个单词列表,并从一个.txt文件中重建单词嵌入。这个训练好的嵌入应该看起来像图 4-8 。

img/463133_1_En_4_Fig8_HTML.jpg

图 4-8

手套预训练包埋

图 4-8 显示了词汇表中前 50 个单词的表示,我们来看看从单词嵌入的转换中产生的两个主要成分。似乎出现在类似语境中的词的例子有,此外还有他的他的。当比较词汇表中其他单词的余弦相似性时,我们观察到以下情况。

Cosine Similarity Between so and u.s.: 0.5606769548631282
Cosine Similarity Between them and so: 0.8815159254335486
Cosine Similarity Between what and them: 0.8077565084355354
Cosine Similarity Between him and what: 0.7972281857691554
Cosine Similarity Between united and him: 0.5374600664967559
Cosine Similarity Between during and united: 0.6205250403136882
Cosine Similarity Between before and during: 0.8565694276984954
Cosine Similarity Between may and before: 0.7855322363492923
Cosine Similarity Between since and may: 0.7821437532357596

示例问题 4.4:在 LSTMs 中使用经过训练的单词嵌入

既然我们已经直观地检查了单词嵌入,那么让我们关注如何将经过训练的嵌入与深度学习算法一起使用。让我们想象一下,我们想要包括下面的段落作为我们单词嵌入的附加训练数据。

sample_text = "'Living in different places has been the
greatest experience that I have had in my life. It has allowed
me to understand people from different walks of life, as well as to question some of my own biases I have had with respect
to people who did not grow up as I did. If possible, everyone
should take an opportunity to travel somewhere separate from where they grew up."'.replace('\n', ")

将样本数据赋给变量后,让我们开始执行一些我们已经熟悉的预处理步骤,例如下面的代码:

def sample_text_dictionary(data=_sample_text):
    count, dictionary = collections.Counter(data).most_common(), {} #creates list of word/count pairs;
    for word, _ in count:
        dictionary[word] = len(dictionary) #len(dictionary) increases each iteration
        reverse_dictionary = dict(zip(dictionary.values(), dictionary.keys()))
    dictionary_list = sorted(dictionary.items(), key = lambda x : x[1])
    return dictionary, reverse_dictionary, dictionary_list

我们从使用一个remove_stop_words()函数开始,这个函数是在第三章中定义的样本预处理文本算法的重新定义,它从相对简单的样本数据中删除停用词。当您使用的数据不如样本数据清晰时,我建议您以类似于使用经济学教科书或战争与和平的方式对数据进行预处理。

转到sample_text_dictionary()函数,我们创建一个词频字典,然后返回这些变量。理解这个过程对你来说很重要,因为这是一个我们如何处理不在经过训练的单词嵌入的词汇表中的单词的例子:

for i in range(len(dictionary)):
    word = dictionary_list[i][0]
    if word in vocabulary:
        _embedding_array.append(embedding_dictionary[word])
    else:
        _embedding_array.append(np.random.uniform(low=-0.2, high=0.2, size=embedding_dim))

我们首先创建一个变量标题:_embedding_array。这个变量实际上包含了示例文本的单词嵌入表示。为了处理不在词汇表中的单词,我们将创建一个随机分布的数字来模拟单词嵌入,然后将它作为输入输入到神经网络中。

接下来,在创建计算图之前,我们对嵌入数据进行最后的转换。

embedding_array = np.asarray(_embedding_array)
decision_tree = spatial.KDTree(embedding_array, leafsize=100)

我们将使用 k-最近邻树来寻找最接近我们的神经网络输出的阵列的嵌入。由此,我们使用reverse_dictionary来寻找与预测嵌入相匹配的单词。

让我们构建我们的计算图,如下所示:

#Initializing placeholders and other variables
X = tf.placeholder(tf.int32, shape=(None, None, n_input))
Y = tf.placeholder(tf.float32, shape=(None, embedding_dim))
weights = {'output': tf.Variable(tf.random_normal([n_hidden, embedding_dim]))}
biases = {'output': tf.Variable(tf.random_normal([embedding_dim]))}

_weights = tf.Variable(tf.constant(0.0, shape=[vocabulary_length, embedding_dim]), trainable=True)
_embedding = tf.placeholder(tf.float32, [vocabulary_length, embedding_dim])
embedding_initializer = _weights.assign(_embedding)
embedding_characters = tf.nn.embedding_lookup(_weights, X)
input_series = tf.reshape(embedding_characters, [-1, n_input])
input_series = tf.split(input_series, n_input, 1)

你会发现这与 LSTM 教程第二章中的大部分内容相似,但是请注意第二组代码,特别是我们创建_weights_embedding变量的地方。当我们加载一个经过训练的单词嵌入,或者在我们的计算图中有一个嵌入层时,数据必须通过这一层才能到达神经网络。网络的维数是词汇中的单词数乘以特征数。虽然训练自己的嵌入时特征的数量可以改变,但这是我们加载单词嵌入时预先确定的值。

我们将 weights 变量分配给占位符_embedding,它最终是我们的优化器正在调整的权重,因此我们创建了一个嵌入的字符变量。tf.nn.embedding_lookup()函数专门检索_weights变量的索引号。最后,我们将embedding_characters变量转换为input_series变量,它实际上是直接输入到 LSTM 层的。

从这一点开始,从 LSTM 层通过图表的其余部分的数据通道应该是熟悉的教程。执行代码时,您应该会看到如下所示的输出:

Input Sequence: ['me', 'to', 'understand', 'people']
Actual Label: from
Predicted Label: an
Epoch: 210
Error: 45.62042

Input Sequence: ['different', 'walks', 'of', 'life,']
Actual Label: as
Predicted Label: with
Epoch: 220
Error: 64.55679

Input Sequence: ['well', 'as', 'to', 'question']
Actual Label: some
Predicted Label: has
Epoch: 230
Error: 75.29771

提高错误率的一个直接建议是加载不同的样本文本,也许是从实际的数据语料库中进行训练,因为有限的数据量不允许准确度有很大的提高。

另一个建议是使用被注释掉的load_data()函数来加载您自己的 PDF 文件,并从该点开始进行试验。

既然我们已经回顾了将单词表示为向量的方法,让我们来讨论其他文本表示。值得庆幸的是,由于大多数都是 Word2Vec 的抽象,这次不需要太多的解释。

段落向量的分布式存储

Paragraph2Vec 是一种算法,它允许我们表示不同长度的对象,从句子到整个文档,目的与我们在前面的示例中将单词表示为向量相同。这项技术是由 Quoc Le 和 Tomas Mikolov 开发的,主要基于 Word2Vec 算法。

在 Paragraph2Vec 中,我们将每个段落表示为矩阵中的唯一向量, D 。每个单词也被映射到一个唯一的向量,由矩阵 W 中的一列表示。我们随后构造一个矩阵, h,,它是通过连接矩阵 WD 而形成的。我们认为这个段落标记是来自 LSTM 的单元格状态的模拟,因为它以段落主题的形式为当前上下文提供内存。直观地说,这意味着矩阵 W 在所有段落中都是相同的,因此我们观察到给定单词的相同表示。训练发生在 Word2Vec 中,在这种情况下,可以通过从随机段落中的固定长度上下文中进行采样来进行负采样。

为了确保你理解这是如何工作的,让我们看看本章的最后一个例子。

示例问题 4.5:第 2 段使用电影评论数据的示例

幸运的是,Gensim 有一个 Doc2Vec 方法,使得这个算法的实现相对简单。在这个例子中,我们将保持事情相对简单,并在向量空间中表示句子,而不是创建或近似段落标记器,我们可能希望它比相对快速制定的试探法更精确(即,每个段落由四个句子组成)。在doc2vec_example.py文件中,Doc2Vec 模型和 Word2Vec 模型只有细微的区别,具体来说就是预处理。

def gensim_preprocess_data(max_pages):
    sentences = namedtuple('sentence', 'words tags')
    _sentences = sent_tokenize(load_data(max_pages=max_pages))
    documents = []
    for i, text in enumerate(_sentences):
        words, tags = text.lower().split(), [i]
        documents.append(sentences(words, tags))
    return documents

Doc2Vec 实现期望所谓的命名的元组对象。这个元组包含句子中包含的标记化单词的列表,以及索引该文档的整数。在在线文档中,有些人利用一个名为LabledLineSentence()的类对象;然而,这以同样的方式执行必要的预处理。当我们运行脚本时,我们遍历所有正在分析的句子,并查看它们相关的余弦相似性。以下是其中一些例子:

Document sentence(words=['this', 'text', 'adapted', 'the', 'saylor', 'foundation', 'creative', 'commons', 'attribution-noncommercial-sharealike', '3.0', 'license', 'without', 'attribution', 'requested', 'works', 'original', 'creator', 'licensee', '.'], tags=[0])

Document sentence(words=['saylor', 'url', ':', 'http', ':', '//www.saylor.org/books', 'saylor.org', '1', 'preface', 'we', 'written', 'fundamentally', 'different', 'text', 'principles', 'economics', ',', 'based', 'two', 'premises', ':', '1', '.'], tags=[1])

Cosine Similarity Between Documents: -0.025641936104727547
Document sentence(words=['saylor', 'url', ':', 'http', ':', '//www.saylor.org/books', 'saylor.org', '1', 'preface', 'we', 'written', 'fundamentally', 'different', 'text', 'principles', 'economics', ',', 'based', 'two', 'premises', ':', '1', '.'], tags=[1])

Document sentence(words=['students', 'motivated', 'study', 'economics', 'see', 'relates', 'lives', '.'], tags=[2])

Cosine Similarity Between Documents:
0.06511943195883922

除此之外,Gensim 还允许我们推断向量,而不必在这些向量上重新训练我们的模型。这在第五章中尤为重要,我们在实际环境中应用了单词嵌入。当我们执行将training_example参数设置为 False 的代码时,您可以看到这个功能。我们有两个示例文档,我们在文件的开头定义了它们:

sample_text1 = "'I love italian food. My favorite items are
pizza and pasta, especially garlic bread. The best italian food
I have had has been in New York. Little Italy was very fun"'

sample_text2 = "'My favorite time of italian food is pasta with
alfredo sauce. It is very creamy but the cheese is the best
part. Whenevr I go to an italian restaurant, I am always certain to get a plate."'

这两个例子非常相似。当我们训练我们的模型时——来自一本经济学教科书的超过 300 页的数据,我们得到以下结果:

 Cosine Similarity Between Sample Texts:
0.9911814256706748

同样,您应该意识到,他们可能需要大量的数据才能在看不见的数据中获得合理的结果。这些例子向他们展示了如何使用各种框架来训练和推断向量。对于那些致力于训练自己的单词嵌入的人来说,前进的道路应该是相当清晰的。

摘要

在我们继续学习自然语言处理任务之前,让我们回顾一下本章中学到的一些最重要的东西。正如你在第三章中看到的,在将深度学习应用于自然语言处理时,正确地预处理数据是我们需要执行的大部分工作。除了清除停用词、标点符号和统计噪声,您还应该准备好争论数据,并以神经网络可解释的格式组织数据。训练有素的单词嵌入通常需要收集数十亿个标记。

确保聚集正确的数据是极其重要的,因为来自完全不同的数据源的几十亿个标记可能会给你留下嵌入,而不会产生多少有用的东西。尽管我们的一些示例产生了积极的结果,但这并不意味着这些应用程序可以在生产环境中工作。你必须(负责任地)从来源收集大量文本数据,同时保持词汇和上下文的同质性。

在下一章,我们通过研究循环神经网络的应用来结束这本书。

五、文本生成、机器翻译和其他重复性语言建模任务

在第四章,我向你介绍了一些更先进的深度学习和 NLP 技术,我讨论了如何在一些基本问题中实现这些模型,比如映射词向量。在我们结束这本书之前,我将讨论一些其他的 NLP 任务,这些任务更特定于领域,但是仍然很有用。

至此,您应该对预处理各种格式的文本数据比较熟悉了,并且应该理解一些 NLP 任务,比如文档分类,并足以执行它们。因此,这一章通过解决几个问题,将重点放在结合我们所学的许多技能上。本章提供的所有解决方案都是可行的。我们非常欢迎您提出或完成超越他们的新解决方案。

使用 LSTMs 生成文本

文本生成在基于人工智能的工具中越来越重要。特别是在处理大量数据时,系统能够与用户进行通信以提供更加身临其境和信息丰富的体验是非常有用的。对于文本生成,主要目标是创建一个生成模型,提供某种关于数据的洞察力。您应该知道,文本生成不一定要创建文档的摘要,而是生成描述输入文本的输出。让我们从检查问题开始。

最初,对于这样的任务,我们需要一个数据源。由此,我们的数据源改变了结果。对于这个任务,我们从《哈利·波特与魔法石》开始。我选择了这本书,因为上下文应该提供一些关于包含在生成的文本中的主题的相当显著的结果。

让我们来回顾一下我们已经习惯的步骤。我们将利用我们在word_embeddings.py中使用的load_data()预处理函数;然而,我们唯一要做的改变是加载harry_potter.pdf而不是economics_textbook.pdf

也就是说,只要目录和其他参数发生变化,这个函数就可以让您轻松地将预处理函数用于任何目的。因为这是一个文本生成示例,所以除了删除非 ASCII 字符之外,我们不应该清理数据。

以下是数据显示方式的示例:

“Harry Potter Sorcerer's Stone CHAPTER ONE THE BOY WHO LIVED Mr. Mrs. Dursley, number four, Privet Drive, proud say perfectly normal, thank much. They last people 'd expect involved anything strange mysterious, n't hold nonsense. Mr. Dursley director firm called Grunnings, made drills. He big, beefy man hardly neck, although large mustache. Mrs. Dursley thin blonde nearly twice usual amount neck, came useful spent much time craning garden fences, spying neighbors. The Dursleys small son called Dudley opinion finer boy anywhere. The Dursleys everything wanted, also secret, greatest fear somebody would discover. They think could bear anyone found Potters. Mrs. Potter Mrs. Dursley's sister, n't met several years; fact, Mrs. Dursley pretended n't sister, sister good-for-nothing husband unDursleyish possible. The Dursleys shuddered think neighbors would say Potters arrived street. The Dursleys knew Potters small son,, never even seen. This boy another good reason keeping Potters away; n't want Dudley mixing child like. When Mr. Mrs. Dursley woke dull, gray Tuesday story starts, nothing cloudy sky outside suggest strange mysterious things would soon happening country. Mr. Dursley hummed picked boring tie work, Mrs. Dursley gossiped away happily wrestled screaming Dudley high chair. None noticed large, tawny owl flutter past window. At half past eight, Mr. Dursley picked briefcase, pecked Mrs. Dursley cheek, tried kiss Dudley good-bye missed, 1 Dudley tantrum throwing cereal walls. `` Little tyke, "chortled Mr. Dursley left house. He got car backed number four's drive. It corner street noticed first sign something peculiar -- cat reading map. For second, Mr. Dursley n't realize seen -- jerked head around look. There tabby cat standing corner Privet Drive, n't map sight. What could thinking ? It must trick light. Mr. Dursley blinked stared cat. It stared back. As Mr. Dursley drove around corner road, watched cat mirror. It reading sign said Privet Drive --, looking sign; cats...”

让我们检查一下我们的预处理函数。

def preprocess_data(sequence_length=sequence_length, max_pages=max_pages, pdf_file=pdf_file):
    text_data = load_data(max_pages=max_pages, pdf_file=pdf_file)
    characters = list(set(text_data.lower()))
    character_dict = dict((character, i) for i, character in enumerate(characters))
    int_dictionary = dict((i, character) for i, character in enumerate(characters))
    num_chars, vocab_size = len(text_data), len(characters)
    x, y = [], []

    for i in range(0, num_chars - sequence_length, 1):
        input_sequence = text_data[i: i+sequence_length]
        output_sequence = text_data[i+sequence_length]
        x.append([character_dict[character.lower()] for character in input_sequence])
        y.append(character_dict[output_sequence.lower()])

    for k in range(0, len(x)): x[i] = [_x for _x in x[i]]
    x = np.reshape(x, (len(x), sequence_length, 1))
    x, y = x/float(vocab_size), np_utils.to_categorical(y)
    return x, y, num_chars, vocab_size, character_dict, int_dictionary

在检查函数时,我们使用了类似于 Skip-Gram 模型的玩具示例中的tf_preprocess_data()函数的方法。我们的输入和输出序列是固定长度的,我们将把 y 变量转换成一个一次性编码的向量,向量中的每个条目代表一个可能的字符。我们将字符序列表示为一个矩阵,其中每行代表整个观察值,每列代表一个字符。

让我们看看书中使用的 Keras 代码的第一个例子。

    def create_rnn(num_units=num_units, activation=activation):
        model = Sequential()
        model.add(LSTM(num_units, activation=activation, input_shape=(None, x.shape[1])))
        model.add(Dense(y.shape[1], activation="softmax"))
        model.compile(loss='categorical_crossentropy', optimizer="adam")
        model.summary()
        return model

Keras 不像 TensorFlow 那样冗长。因此,这使得改变模型的架构变得相对容易。我们通过给一个变量赋值来实例化一个模型,然后简单地用Sequential().add()函数添加层类型。

在用 200 个历元运行网络之后,我们得到以下结果:

 driv, proud say perfecdly normal, thanp much. they last people 'd expect involved anytsing strange mysterious, s't hold donsense. mr. dursley director firm called grunnings, made drills. he big, berfy man, ardly neck, althougl larte mustache. mrs. dursley thic -londe. early twece uiual amount necd, came ueeful spent much time craning geddon fences, spying neighbors. the dursleys small son called dudley opinion finer boy anyw  rd. the dursleys everything wanted, slso secret, greatest fear somebody would discover. they thinn could bear antone found potters. mrs. potterimrs. dursley's sister, n't met several years; fact, mrs. dursley pretended n't sister, sister good-sur-notding husband undursleyir  pousible. the dursleys suuddered think auigybors would say potters arrived strett. the dursleys knew potters small. on,  ever even seen. thit boy another good reason keeping potters away; n'e want dudley mixing child like. wten mr. mrs. dursley woke dull, gray tuesday story startss, nothing cloudy skycoutside suggest strange mytter ous taings would soon darpening codntry. mr. dursley tummed picked boring tie work, mrs. dursley gosudaed away happily wrestled screaming dudley aigh cuair. noneoloticed large, tawny owl flutter past wincow. at ialf past, ight, mr. dursley picked briefcase, pecked mrs. dursley cheek, tried kiss dudley good-bye missed, 1 dudley tantrum,hrowigg cereal walls. `` lwttle tykp, "chortled mr. dursley left house. he got car backel number four's drive. it corner street noticed fir t sign somathing pcculilr -- cat feading,ap. for sicond, mr. dursley r't realize scen -- jerked head around look. thereytab y cat standing corneraprivet drive, n'tamap sight. what sould thinking ? it muse trick light. mr. dursley blinked stared cat. it stayed back. as mr. dursley drove around corner road, watched catcmirror. it reading sign saidsprivet druve --, lookingtsign; cats could n't read maps signs. mr. durs

注意

有些文本是可以理解的,但显然不是所有的都尽如人意。在这种情况下,我建议您允许神经网络训练更长时间,并添加更多数据。还要考虑使用不同的模型和模型架构。除了这个例子之外,提出一个对语音建模也有用的 LSTM 的更高级版本将是有用的。

双向 RNNs (BRNN)

BRNNs 是由 Mike Schuster 和 Kukdip Paliwal 在 1997 年创建的,他们将这项技术介绍给了一家信号处理学术期刊。该模型的目的是利用在“正向和负向时间方向”上移动的信息具体来说,他们希望利用向预测方向移动的信息,以及向相反方向移动的相同输入流。图 5-1 展示了 BRNN 的架构。

img/463133_1_En_5_Fig1_HTML.png

图 5-1

双向 RNN

让我们想象一下,我们有一个单词序列,如下所示:这个人走在木板路上。

在常规的 RNN 中,假设我们想要预测单词 boardwalk ,输入数据将是的步行向下。如果我们输入二元模型,它将是行走等等。我们继续遍历输入数据,预测在每个时间步长下最有可能出现的单词,最终得到我们的最终目标标签,这是一个概率,对应于在给定输入数据的情况下最有可能出现的独热编码向量。BRNN 的唯一区别是,当我们从左到右预测序列时,我们也从右到左预测序列。

*BRNNs 对于 NLP 任务特别有用。以下是构建 BRNN 的代码:

def create_lstm(input_shape=(1, x.shape[1])):
        model = Sequential()
        model.add(Bidirectional(LSTM(unites=n_units,
                                     activation=activation),
                                     input_shape=input_shape))

        model.add(Dense(train_y.shape[1]), activation=out_act)
        model.compile(loss='categorical_crossentropy', metrics=['accuracy'])
        return model

双向 RNN 的结构几乎是相同的,我们只是在我们的层上添加了一个Bidirectional()角色。这通常会增加训练神经网络所需的时间,但总的来说,它在许多任务中优于传统的 RNN 体系结构。记住这一点,让我们应用我们的模型。

创建名称实体识别标记器

使用 NLTK 或类似软件包的人可能会遇到名称实体识别 (NER)标记者。NER 标签通常输出在较大类别(个人、组织、位置等)中识别实体的标签。).创建一个 NER 标记者需要大量带注释的数据。

对于这个任务,我们将使用来自 Kaggle 的数据集。当我们解压缩数据时,我们看到它采用以下格式:

 played    on    Monday    (    home    team  in    CAPS )    :
VBD        IN    NNP       (    NN      NN    IN    NNP  )    :
O          O     O         O    O       O     O     O    0    O
American   League
NNP        NNP
B-MISC     I-MISC
Cleveland  2     DETROIT   1
NNP        CD    NNP       CD
B-ORG      O     B-ORG     O
BALTIMORE  12    Oakland   11   (       10    innings         )
VB         CD    NNP       CD   (       CD    NN         )
B-ORG      O     B-ORG     O    O       O     O     O
TORONTO    5     Minnesota 3
TO         CD    NNP       CD
B-ORG      O     B-ORG     O
Milwaukee  3     CHICAGO   2
NNP        CD    NNP       CD
B-ORG      O     B-ORG     O
Boston     4     CALIFORNIA 1

数据是制表符分隔的,但也是.txt格式。在我们开始训练 BRNN 之前,这需要一些数据争论。

让我们从将文本数据转换成可解释的格式开始,如下所示:

def load_data():

    text_data = open('/Users/tawehbeysolow/Downloads/train.txt', 'rb').readlines()
    text_data = [text_data[k].replace('\t', ' ').split() for k in range(0, len(text_data))]
    index = range(0, len(text_data), 3)

    #Transforming data to matrix format for neural network
    input_data =  list()
    for i in range(1, len(index)-1):
        rows = text_data[index[i-1]:index[i]]
        sentence_no = np.array([i for i in np.repeat(i, len(rows[0]))], dtype=str)
        rows.append(np.array(sentence_no))
        rows = np.array(rows).T
        input_data.append(rows)

我们必须首先遍历.txt文件的每一行。请注意,数据被组织成三个一组。典型的分组如下所示:

text_data[0]
['played', 'on', 'Monday', '(', 'home', 'team', 'in', 'CAPS', ')', ':']
 text_data[1]
['VBD', 'IN', 'NNP', '(', 'NN', 'NN', 'IN', 'NNP', ')', ':']
text_data[2]
['O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O']

第一组观察包含文本本身,第二组观察包含名称实体标记,最后一组包含特定标记。回到预处理函数,我们对句子进行分组,并添加一个包含句子编号标签的数组,稍后我将讨论它的重要性。

当查看input_data变量的快照时,我们会看到以下内容:

input_data[0:1]
[array([['played', 'VBD', 'O', '1'],
       ['on', 'IN', 'O', '1'],
       ['Monday', 'NNP', 'O', '1'],
       ['(', '(', 'O', '1'],
       ['home', 'NN', 'O', '1'],
       ['team', 'NN', 'O', '1'],
       ['in', 'IN', 'O', '1'],
       ['CAPS', 'NNP', 'O', '1'],
       [')', ')', 'O', '1'],
       [':', ':', 'O', '1']], dtype='|S6')]

我们需要删除句子标签,同时以这样一种方式观察数据,即神经网络隐含地理解这些句子是如何分组的。我们想要去除这个标签的原因是神经网络以这样一种方式读取分类标签(句子编号是其模拟),即编号较高的句子明显比编号较低的句子具有更大的重要性。对于这项任务,我想你们大多数人都明白,我们而不是想把它融入培训过程。因此,我们转到以下代码体:

   input_data = pan.DataFrame(np.concatenate([input_data[j] for j in range(0,len(input_data))]),
                       columns=['word', 'pos', 'tag', 'sent_no'])

   labels, vocabulary = list(set(input_data['tag'].values)), list(set(input_data['word'].values))
   vocabulary.append('endpad'); vocab_size = len(vocabulary); label_size = len(labels)

 aggregate_function = lambda input: [(word, pos, label) for word, pos, label in zip(input['word'].values.tolist(),
   input['pos'].values.tolist(),
   input['tag'].values.tolist())]

我们将input_data组织到一个数据帧中,然后创建几个其他变量,我们将在后面的函数中使用,以及train_brnn_keras()函数。其中一些变量与前一章脚本中的其他变量很相似(例如,vocab_size代表词汇表中的单词数)。但重要的部分主要是后两个变量,这才是你解决这个问题要重点关注的。

lambda 函数aggregate_function将一个数据帧作为输入,然后为一个分组中的每个观察值返回一个三元组。这正是我们将如何把所有的观察组合在一句话里。转换后的数据快照如下:

 sentences[0]
[('played', 'VBD', 'O'), ('on', 'IN', 'O'), ('Monday', 'NNP', 'O'), ('(', '(', 'O'), ('home', 'NN', 'O'), ('team', 'NN', 'O'), ('in', 'IN', 'O'), ('CAPS', 'NNP', 'O'), (')', ')', 'O'), (':', ':', 'O')]

我们几乎已经完成了所有必要的预处理;然而,有一个关键步骤你应该知道。

    x = [[word_dictionary[word[0]] for word in sent] for sent in sentences]
    x = pad_sequences(maxlen=input_shape, sequences=x, padding="post", value=0)
    y = [[label_dictionary[word[2]] for word in sent] for sent in sentences]
    y = pad_sequences(maxlen=input_shape, sequences=y, padding="post", value=0)
     = [np_utils.to_categorical(label, num_classes=label_size) for label in y]

在前面的代码行中,我们像在许多其他示例中一样,将单词转换为它们的整数标签,并创建一个独热编码矩阵。这与上一章类似,但是,我们应该明确地不使用pad_sequences()函数。

当处理句子数据时,我们并不总是得到等长的句子;然而,神经网络的输入矩阵必须在所有观测值中具有相同数量的特征。零填充用于添加额外的特征,使所有观察值的大小标准化。

完成这一步后,我们现在准备开始训练我们的神经网络。我们的模型如下:

def create_brnn():
        model = Sequential()
        model.add(Embedding(input_dim=vocab_size+1, output_dim=output_dim,
                            input_length=input_shape, mask_zero=True))
        model.add(Bidirectional(LSTM(units=n_units, activation=activation,
                                     return_sequences=True)))
        model.add(TimeDistributed(Dense(label_size, activation=out_act)))
        model.compile(optimizer='adam', loss="categorical_crossentropy", metrics=['accuracy'])
        model.summary()
        return model

我们的大部分模型类似于本章中建立的先前的 Keras 模型;然而,我们有一个嵌入层(类似于单词嵌入)堆叠在双向 LSTM 的顶部,该层随后堆叠在完全连接的输出层的顶部。

我们用大约 90%的数据训练我们的网络,然后评估结果。我们发现,我们在训练数据上的标记器产生了 90%甚至更高的准确率,这取决于我们训练它的时期数。

既然我们已经处理了这个分类任务,并且充分地使用了 BRNNs,那么让我们转到另一个神经网络模型,并且讨论它如何能够有效地应用于另一个 NLP 任务。

序列间模型(Seq2Seq)

序列到序列模型(seq2seq)值得注意,因为它们接受输入序列并返回输出序列,两者的长度都是可变的。这使得该模型特别强大,并且它倾向于在语言建模任务中表现良好。Sutskever 等人在一篇论文中对我们将利用的特定模型进行了最佳总结。图 5-2 展示了该模型。

img/463133_1_En_5_Fig2_HTML.png

图 5-2

编码器-解码器模型

该模型通常由两部分组成:编码器和解码器。编码器和解码器都是 rnn。编码器读取输入序列,并从 LSTM 单元输出除隐藏和单元状态之外的固定长度向量。随后,除了输出隐藏和单元状态之外,解码器还获取这个固定长度的向量,并将它们用作其第一个 LSTM 单元的输入。解码器输出一个固定长度的向量,我们将其作为目标标签。我们将一次一个字符地执行预测,这使我们能够很容易地评估从一个观察到下一个观察的不同长度的序列。接下来,您将看到这个模型的运行。

神经网络模型问答

深度学习在 NLP 中的一个流行应用是聊天机器人。许多公司使用聊天机器人来处理一般的客户服务请求,这需要他们灵活地将问题转化为答案。虽然我们看到的测试案例是问题和答案的缩影,但它是我们如何训练神经网络正确回答问题的一个例子。我们将使用斯坦福问答数据集。虽然它更能代表一般的知识,但你最好能认识到这些问题的结构方式。

让我们从研究如何利用以下函数预处理数据开始:

    dataset = json.load(open('/Users/tawehbeysolow/Downloads/qadataset.json', 'rb'))['data']
    questions, answers = [], []
    for j in range(0, len(dataset)):
        for k in range(0, len(dataset[j])):
            for i in range(0, len(dataset[j]['paragraphs'][k]['qas'])):
                questions.append(remove_non_ascii(dataset[j]['paragraphs'][k]['qas'][i]['question']))              answers.append(remove_non_ascii(dataset[j]['paragraphs'][k]['qas'][i]['answers'][0]['text']))

当我们查看数据快照时,我们观察到以下结构:

[{u'paragraphs': [{u'qas': [{u'question': u'To whom did the Virgin Mary allegedly appear in 1858 in Lourdes France?', u'id': u'5733be284776f41900661182', u'answers': [{u'text': u'Saint Bernadette Soubirous', u'answer_start': 515}]}, {u'question': u'What is in front of the Notre Dame Main Building?', u'id': u'5733be284776f4190066117f', u'answers': [{u'text': u'a copper statue of Christ', u'answer_start': 188}]}, {u'question': u'The Basilica of the Sacred heart at Notre Dame is beside to which structure?', u'id': u'5733be284776f41900661180', u'answers': [{u'text': u'the Main Building', u'answer_start': 279}]}, {u'question': u'What is the Grotto at Notre Dame?', u'id': u'5733be284776f41900661181', u'answers': [{u'text': u'a Marian place of prayer and reflection', u'answer_start': 381}]}, {u'question': u'What sits on top of the Main Building at Notre Dame?', u'id': u'5733be284776f4190066117e', u'answers': [{u'text': u'a golden statue of the Virgin Mary', u'answer_start': 92}]}], u'context': u'Architecturally, the school has a Catholic character. Atop the Main Building\'s gold dome is a golden statue of the Virgin Mary. Immediately in front of the Main Building and facing it, is a copper statue of Christ with arms upraised with the legend "Venite Ad Me Omnes". Next to the Main Building is the Basilica of the Sacred Heart. Immediately behind the basilica is the Grotto, a Marian place of prayer and reflection. It is a replica of the grotto at Lourdes, France where the Virgin Mary reputedly appeared to Saint Bernadette Soubirous in 1858\. At the end of the main drive (and in a direct line that connects through 3 statues and the Gold Dome), is a simple, modern stone statue of Mary.'}, {u'qas': [{u'question': u'When did the Scholastic Magazine of Notre dame begin publishing?', u'id': u'5733bf84d058e614000b61be', u'answers'

我们有一个 JSON 文件,里面有问题和答案。与名称实体识别任务类似,我们需要将数据预处理成矩阵格式,以便输入到神经网络中。我们必须首先收集与正确答案相对应的问题。然后我们遍历 JSON 文件,将每个问题和答案附加到相应的数组中。

现在让我们讨论一下,我们实际上是如何为神经网络构建问题的。我们不是让神经网络预测每个单词,而是让神经网络预测给定字符输入序列的每个字符。由于这是一个多标签分类问题,我们将为输出向量的每个元素输出一个 softmax 概率,然后选择概率最高的向量。这表示在给定先前输入序列的情况下最有可能继续的字符。

在我们对整个输出序列做了这些之后,我们将连接这个输出字符的数组,这样我们就得到一个人类可读的消息。因此,我们转到代码的以下部分:

    input_chars, output_chars = set(), set()

    for i in range(0, len(questions)):
        for char in questions[i]:
            if char not in input_chars: input_chars.add(char.lower())

    for i in range(0, len(answers)):
        for char in answers[i]:
            if char not in output_chars: output_chars.add(char.lower())

    input_chars, output_chars = sorted(list(input_chars)), sorted(list(output_chars))
    n_encoder_tokens, n_decoder_tokens = len(input_chars), len(output_chars)

我们遍历了每个问题和答案,并收集了输出和输入序列中所有独特的单个字符。这产生了下面的集合,它们分别代表输入和输出字符。

input_chars; output_chars
[u' ', u'"', u'#', u'%', u'&', u"'", u'(', u')', u',', u'-', u'.', u'/', u'0', u'1', u'2', u'3', u'4', u'5', u'6', u'7', u'8', u'9', u':', u';', u'>', u'?', u'_', u'a', u'b', u'c', u'd', u'e', u'f', u'g', u'h', u'i', u'j', u'k', u'l', u'm', u'n', u'o', u'p', u'q', u'r', u's', u't', u'u', u'v', u'w', u'x', u'y', u'z']
[u' ', u'!', u'"', u'$', u'%', u'&', u"'", u'(', u')', u'+', u',', u'-', u'.', u'/', u'0', u'1', u'2', u'3', u'4', u'5', u'6', u'7', u'8', u'9', u':', u';', u'?', u'[', u']', u'a', u'b', u'c', u'd', u'e', u'f', u'g', u'h', u'i', u'j', u'k', u'l', u'm', u'n', u'o', u'p', u'q', u'r', u's', u't', u'u', u'v', u'w', u'x', u'y', u'z']

这两个列表分别包含 53 和 55 个字符;然而,它们实际上是同质的,包含字母表中的所有字母,加上一些语法和数字字符。

我们转到预处理的最重要部分,其中我们将输入序列转换为神经网络可以解释的独热编码向量。

(code redacted, please see github)

    x_encoder = np.zeros((len(questions), max_encoder_len, n_encoder_tokens))
    x_decoder = np.zeros((len(questions), max_decoder_len, n_decoder_tokens))
    y_decoder = np.zeros((len(questions), max_decoder_len, n_decoder_tokens))

    for i, (input, output) in enumerate(zip(questions, answers)):
        for _character, character in enumerate(input):
            x_encoder[i, _character, input_dictionary[character.lower()]] = 1.

        for _character, character in enumerate(output):
            x_decoder[i, _character, output_dictionary[character.lower()]] = 1.

            if i > 0: y_decoder[i, _character, output_dictionary[character.lower()]] = 1.

我们首先实例化两个输入向量和一个输出向量,用x_encoderx_decodery_encoder表示。接下来,这表示数据通过神经网络并对照目标标签进行验证的顺序。虽然我们在这里选择创建的一次性编码是相似的,但是我们通过创建一个三维数组来评估每个问题和答案,做了一点小小的改变。每行代表一个问题,每个时间步长代表一个字符,每列代表我们的字符集中的字符类型。我们对每个问答对重复这个过程,直到我们有一个包含整个数据集的数组,它产生 4980 个数据观察值。

最后一步定义模型,由encoder_decoder()函数给出。

def encoder_decoder(n_encoder_tokens, n_decoder_tokens):

    encoder_input = Input(shape=(None, n_encoder_tokens))
    encoder = LSTM(n_units, return_state=True)
    encoder_output, hidden_state, cell_state = encoder(encoder_input)
    encoder_states = [hidden_state, cell_state]

    decoder_input = Input(shape=(None, n_decoder_tokens))
    decoder = LSTM(n_units, return_state=True, return_sequences=True)
    decoder_output, _, _ = decoder(decoder_input, initial_state=encoder_states)

    decoder = Dense(n_decoder_tokens, activation="softmax")(decoder_output)
    model = Model([encoder_input, decoder_input], decoder)
    model.compile(optimizer='adam', loss="categorical_crossentropy",  metrics=['accuracy'])
    model.summary()
    return model

我们实例化的模型与其他 Keras 模型略有不同。这种创建模型的方法是通过使用函数式 API 来完成的,而不是像我们经常做的那样依赖于顺序模型。具体来说,这种方法在创建更复杂的模型时非常有用,比如 seq2seq 模型,而且一旦您学会了如何使用顺序模型,这种方法就相对简单了。我们没有向顺序模型添加层,而是将不同的层实例化为变量,然后通过调用我们创建的张量来传递数据。当我们通过调用 encoder(encoder_input)实例化变量encoder_output时,我们看到了这一点。我们在编码器-解码器阶段一直这样做,直到我们得到一个输出向量,我们将其定义为具有 softmax 激活功能的密集/全连接层

最后,我们转向培训,观察以下结果:

Model Prediction: saint bernadette soubiroust
Actual Output: saint bernadette soubirous
Model Prediction: a copper statue of christ
Actual Output: a copper statue of christ
Model Prediction: the main building
Actual Output: the main building
Model Prediction: a marian place of prayer and reflection
Actual Output: a marian place of prayer and reflection
Model Prediction: a golden statue of the virgin mary
Actual Output: a golden statue of the virgin mary
Model Prediction: september 18760
Actual Output: september 1876
Model Prediction: twice
Actual Output: twice
Model Prediction: the observer
Actual Output: the observer
Model Prediction: three
Actual Output: three
Model Prediction: 19877
Actual Output: 1987

如您所见,这个模型表现相当好,只有三个纪元。尽管由于添加了额外的字符,在拼写上有一些问题,但在大多数情况下,消息本身是正确的。您可以继续尝试这个问题,特别是通过改变模型架构来看看是否有一个能产生更好的准确性。

摘要

随着本章接近尾声,我们应该回顾一下在帮助我们成功训练算法方面最重要的概念。首先,您应该注意适用于不同问题的模型类型。编码器-解码器模型体系结构引入了“多对多”输入-输出方案,并显示了它适用的场合。

其次,您应该注意预处理技术在哪里可以应用于看似不同但相关的问题。从一种语言到另一种语言的数据翻译使用与创建基于不同响应回答问题的神经网络相同的预处理步骤。注意这些建模步骤以及它们如何与数据的底层结构相关联,可以在看似无关紧要的任务上节省时间。

结论和最后陈述

我们已经读完了这本书。我们解决了各种复杂程度和领域的 NLP 问题。有许多概念在所有问题类型中都是不变的,尤其是数据预处理。让机器学习变得困难的绝大多数是预处理数据。您看到了相似的问题类型共享预处理步骤,因为当我们转向更高级的问题时,我们经常重用部分解决方案。

从现在开始,有一些最后的原则值得记住。深度学习的 NLP 可能需要大量的文本数据。认真负责地收集数据,并在处理大型数据集时考虑选择优化运行时的语言(C/C++还是 Python 等)。).

总的来说,神经网络是相当简单的工作模型。困难在于找到具有预测能力的好数据,此外还要以一种我们的神经网络可以找到模式来利用的方式来构建数据。

仔细研究文档分类的预处理步骤,例如,创建一个单词嵌入,或者创建一个 NER 标签。其中的每一个都代表了可以应用于不同问题的特征提取方案,并为您的研究指明了前进的道路。

虽然在机器学习社区中经常谈到数据的智能预处理,但这对于深度学习和数据科学的 NLP 范式来说尤其如此。我们训练的模型为您提供了如何在专业或学术环境中处理类似数据集的路线图。然而,这并不意味着我们已经部署的模型可以在生产中使用并且工作良好。

有相当多的变量我没有讨论,因为它们是维护生产系统的问题,而不是模型背后的理论。例子包括词汇表中随着时间的推移出现的未知单词,何时重新训练模型,如何同时评估多个模型的输出,等等。

根据我的经验,通过收集大量实时性能数据,可以很好地解决何时重新训练模型的问题。观察信号何时不被接受,如果它们确实不被接受的话,并跟踪再训练的效果,以及模型再训练的持久性。即使你的模型是准确的,也不意味着它在实践中会很容易使用。

仔细考虑如何处理错误分类,特别是如果错误分类的惩罚可能导致金钱和/或其他资源的损失。不要害怕对多种问题类型使用多种模型。实验时,从简单开始,根据需要逐渐增加复杂性。这比一开始试图设计非常复杂的东西,然后试图调试一个你不理解的系统要容易得多。

除了利用我的 GitHub 页面上的代码以自己独特的方式解决问题之外,我们鼓励您在闲暇时重读这本书,并作为参考。虽然阅读这本书提供了一个开始,但精通数据科学的唯一方法是自己实践这些问题。

我希望你喜欢学习自然语言处理和深度学习,就像我喜欢解释它一样。*

posted @ 2024-10-01 21:03  绝不原创的飞龙  阅读(8)  评论(0编辑  收藏  举报