From 0af0072ecf0efef1bfc030cebce10c9b8f04d935 Mon Sep 17 00:00:00 2001 From: wizardforcel <562826179@qq.com> Date: Sat, 16 Jan 2021 17:14:47 +0800 Subject: [PATCH] 2021-01-16 17:14:47 --- new/handson-nlp-pt-1x/5.md | 78 ++++++++++++++++++++------------------ 1 file changed, 42 insertions(+), 36 deletions(-) diff --git a/new/handson-nlp-pt-1x/5.md b/new/handson-nlp-pt-1x/5.md index e9f8a77b..094dd3b0 100644 --- a/new/handson-nlp-pt-1x/5.md +++ b/new/handson-nlp-pt-1x/5.md @@ -1,6 +1,6 @@ # “第 5 章”:循环神经网络和情感分析 -在本章中,我们将研究**循环神经网络**(**RNN**),这是 PyTorch 中基本前馈神经网络的变体,我们学习了如何在 [*中进行构建 ]第 1 章*](../Text/1.html#_idTextAnchor015) *,机器学习基础*。 通常,RNN 可用于任何可以将数据表示为序列的任务。 其中包括使用以序列表示的历史数据的时间序列进行股票价格预测之类的事情。 我们通常在 NLP 中使用 RNN,因为可以将文本视为单个单词的序列,并可以对其进行建模。 传统的神经网络将单个矢量作为模型的输入,而 RNN 可以采用整个矢量序列。 如果我们将文档中的每个单词表示为矢量嵌入,则可以将整个文档表示为矢量序列(或 3 阶张量)。 然后,我们可以使用 RNN(以及更复杂的 RNN 形式,称为**长短期记忆**(**LSTM**))从我们的数据中学习。 +在本章中,我们将研究**循环神经网络**(**RNN**),这是 PyTorch 中基本前馈神经网络的变体,我们在第 1 章“机器学习基础”中学习了如何构建它。 通常,RNN 可用于任何可以将数据表示为序列的任务。 其中包括使用以序列表示的历史数据的时间序列进行股票价格预测之类的事情。 我们通常在 NLP 中使用 RNN,因为可以将文本视为单个单词的序列,并可以对其进行建模。 传统的神经网络将单个矢量作为模型的输入,而 RNN 可以采用整个矢量序列。 如果我们将文档中的每个单词表示为矢量嵌入,则可以将整个文档表示为矢量序列(或 3 阶张量)。 然后,我们可以使用 RNN(以及更复杂的 RNN 形式,称为**长短期记忆**(**LSTM**))从我们的数据中学习。 在本章中,我们将介绍 RNN 的基础知识和更高级的 LSTM。 然后,我们将研究情绪分析,并通过一个实际的示例来研究如何使用 PyTorch 构建 LSTM 对文档进行分类。 最后,我们将在简单的云应用程序平台 Heroku 上托管我们的简单模型,这将使​​我们能够使用我们的模型进行预测。 @@ -17,7 +17,7 @@ # 构建 RNN -RNN 由循环层组成。 尽管它们在许多方面类似于标准前馈神经网络中的全连接层,但这些递归层由隐藏状态组成,该隐藏状态在顺序输入的每个步骤中进行更新。 这意味着对于任何给定的序列,模型都使用隐藏状态初始化,该状态通常表示为一维向量。 然后,将序列的第一步输入模型,并根据一些学习到的参数更新隐藏状态。 然后将第二个单词馈入网络,并根据其他一些学习到的参数再次更新隐藏状态。 重复这些步骤,直到处理完整个序列,并且使我们处于最终的隐藏状态。 该计算*循环*具有从先前的计算中继承并更新的隐藏状态,这就是为什么我们将这些网络称为循环的原因。 然后,将这个最终的隐藏状态连接到另一个全连接层,并预测最终的分类。 +RNN 由循环层组成。 尽管它们在许多方面类似于标准前馈神经网络中的全连接层,但这些递归层由隐藏状态组成,该隐藏状态在顺序输入的每个步骤中进行更新。 这意味着对于任何给定的序列,模型都使用隐藏状态初始化,该状态通常表示为一维向量。 然后,将序列的第一步输入模型,并根据一些学习到的参数更新隐藏状态。 然后将第二个单词馈入网络,并根据其他一些学习到的参数再次更新隐藏状态。 重复这些步骤,直到处理完整个序列,并且使我们处于最终的隐藏状态。 该计算使用从先前的计算中继承并更新的隐藏状态来循环执行,这就是为什么我们将这些网络称为循环的原因。 然后,将这个最终的隐藏状态连接到另一个全连接层,并预测最终的分类。 我们的循环层如下所示,其中`h`是隐藏状态,而`x`是我们在序列中各个时间步的输入。 对于每次迭代,我们都会在每个时间步`x`中更新隐藏状态: @@ -33,7 +33,7 @@ RNN 由循环层组成。 尽管它们在许多方面类似于标准前馈神经 该层用于`n`时间步长的输入。 我们的隐藏状态在状态`h`0 中初始化,然后使用我们的第一个输入`x`1 来计算下一个隐藏状态`h`1。 还学习了两组权重矩阵:矩阵`U`和矩阵`W`,矩阵`U`了解隐藏状态如何在时间步之间变化,矩阵`W`隐藏状态。 -我们还将 *tanh* 激活函数应用于所得产品,将隐藏状态的值保持在-1 和 1 之间。用于计算任何隐藏状态的公式`h`t 变为 以下: +我们还将 *tanh* 激活函数应用于所得产品,将隐藏状态的值保持在-1 和 1 之间。用于计算任何隐藏状态的公式`h[t]`变为 以下: ![](img/Formula_05_001.png) @@ -51,9 +51,9 @@ RNN 由循环层组成。 尽管它们在许多方面类似于标准前馈神经 现在,我们将重点介绍 RNN 的问题之一-爆炸和收缩梯度-以及我们如何使用梯度剪切来对此进行补救。 -## 爆炸和收缩梯度 +## 梯度爆炸和收缩 -RNN 中我们经常面临的一个问题是**爆炸或收缩梯度**。 我们可以将递归层视为一个非常深的网络。 在计算梯度时,我们在隐藏状态的每次迭代中都这样做。 如果在给定位置的损耗相对于权重的梯度变得很大,则在递归层的所有迭代中前馈时,这将产生乘法效果。 这会导致梯度爆炸,因为它们会很快变得非常大。 如果我们的梯度较大,则可能导致网络不稳定。 另一方面,如果隐藏状态下的梯度非常小,这将再次产生乘法效果,并且梯度将接近 0。这意味着梯度可能变得太小而无法通过梯度下降准确地更新参数, 表示我们的模型无法学习。 +RNN 中我们经常面临的一个问题是**梯度爆炸或收缩**。 我们可以将递归层视为一个非常深的网络。 在计算梯度时,我们在隐藏状态的每次迭代中都这样做。 如果在给定位置的损耗相对于权重的梯度变得很大,则在递归层的所有迭代中前馈时,这将产生乘法效果。 这会导致梯度爆炸,因为它们会很快变得非常大。 如果我们的梯度较大,则可能导致网络不稳定。 另一方面,如果隐藏状态下的梯度非常小,这将再次产生乘法效果,并且梯度将接近 0。这意味着梯度可能变得太小而无法通过梯度下降准确地更新参数, 表示我们的模型无法学习。 我们可以用来防止的梯度爆炸的一种技术是使用**梯度剪切**。 此技术限制了我们的渐变,以防止它们变得太大。 我们只需选择一个超参数`C`,就可以计算出裁剪的梯度,如下所示: @@ -73,11 +73,11 @@ RNN 中我们经常面临的一个问题是**爆炸或收缩梯度**。 我们 RNN 的基本结构意味着它们很难长期保留信息。 考虑一个 20 个字长的句子。 从影响初始隐藏状态的句子中的第一个单词到句子中的最后一个单词,我们的隐藏状态被更新 20 次。 从句子的开头到最终的隐藏状态,RNN 很难保留句子开头的单词信息。 这意味着 RNN 不太擅长捕获序列中的长期依赖关系。 这也与前面提到的消失的梯度问题有关,在梯度问题中,通过向量的稀疏序列反向传播非常无效。 -考虑一段较长的段落,我们试图预测下一个单词。 句子的开头是**我正在学习数学…**,而结尾处是**我的期末考试是在…**中。 直观地,我们希望下一个单词是**数学**或某些与数学相关的字段。 但是,在较长序列的 RNN 模型中,由于需要多个更新步骤,我们的隐藏状态可能难以在到达句子结尾时保留句子开头的信息。 +考虑一段较长的段落,我们试图预测下一个单词。 句子的开头是`I study math…`,而结尾处是`my final exam is in….`中。 直观地,我们希望下一个单词是`math`或某些与数学相关的字段。 但是,在较长序列的 RNN 模型中,由于需要多个更新步骤,我们的隐藏状态可能难以在到达句子结尾时保留句子开头的信息。 我们还应该注意,RNN 在捕获整个句子中单词的上下文方面很差。 在查看 n-gram 模型时,我们前面已经看到,一个单词在句子中的含义取决于它在句子中的上下文,上下文取决于它之前出现的单词和之后出现的单词。 在 RNN 中,我们的隐藏状态仅在一个方向上更新。 在单个前向传递中,我们的隐藏状态被初始化,并且序列中的第一个单词被传递到其中。 然后依次对句子中的所有后续单词重复此过程,直到我们处于最终的隐藏状态。 这意味着对于句子中的任何给定单词,我们只考虑了到该点之前在句子中出现的单词的累积效果。 我们不会考虑其后的任何单词,这意味着我们不会捕获句子中每个单词的完整上下文。 -在另一个示例中,我们再次希望预测句子中丢失的单词,但是现在它出现在开头而不是结尾。 我有一个长成的句子**,所以我能说流利的荷兰语**。 在这里,我们可以凭直觉猜测该人在荷兰长大,这是因为他们会说荷兰语。 但是,由于 RNN 会顺序分析此信息,因此它将仅使用我在……中长大的**进行预测,而缺少句子中的其他关键上下文。** +在另一个示例中,我们再次希望预测句子中丢失的单词,但是现在它出现在开头而不是结尾。 `I grew up in…so I can speak fluent Dutch.` 在这里,我们可以凭直觉猜测该人在荷兰长大,这是因为他们会说荷兰语。 但是,由于 RNN 会顺序分析此信息,因此它将仅使用`I grew up in…`进行预测,而缺少句子中的其他关键上下文。 使用 LSTM 可以部分解决这两个问题。 @@ -85,11 +85,15 @@ RNN 的基本结构意味着它们很难长期保留信息。 考虑一个 20 LSTM 是 RNN 的更高级版本,并包含两个额外的属性-**更新门**和**遗忘门**。 这两个附加项使易于网络学习长期依赖性。 考虑以下电影评论: -*这部电影很棒。 周二下午,我和妻子和女儿去看了它。 尽管我没想到它会带来很大的娱乐性,但结果却充满了乐趣。 如果有机会,我们一定会再去看看。* +```py +The film was amazing. I went to see it with my wife and my daughters on Tuesday afternoon. Although I didn't expect it to be very entertaining, it turned out to be loads of fun. We would definitely go back and see it again given the chance. +``` 在情感分析中,很明显,并不是句子中的所有单词在确定是正面评论还是负面评论时都相关。 我们将重复这句话,但是这次重点介绍与评估评论情绪相关的词: -*这部电影很棒。 周二下午,我和妻子和女儿去看了它。 尽管我没想到它会带来很大的娱乐性,但结果却充满了乐趣。 如果有机会,我们一定会再去看看。* +```py +The film was amazing. I went to see it with my wife and my daughters on Tuesday afternoon. Although I didn't expect it to be very entertaining, it turned out to be loads of fun. We would definitely go back and see it again given the chance. +``` LSTM 试图做到这一点—在忘记所有不相关信息的同时记住句子中的相关单词。 通过这样做,它可以阻止无关信息稀释相关信息,这意味着可以更好地了解长序列的长期依赖性。 @@ -113,7 +117,7 @@ LSTM 与 RNN 的结构非常相似。 虽然 LSTM 的各个步骤之间存在一 图 5.7 –遗忘门 -遗忘门本质上是学习要忘记序列中的哪些元素。 先前的隐藏状态`h`t-1 和最新的输入步骤`x`1 被串联在一起,并通过了在遗忘门和 S 形函数上获得的权重矩阵 将值压缩为 0 到 1 之间的值。将得到的矩阵 *ft* 逐点乘以上一步`c`t-1 的单元状态。 这有效地将掩码应用于先前的小区状态,使得仅来自先前的小区状态的相关信息被提出。 +遗忘门本质上是学习要忘记序列中的哪些元素。 先前的隐藏状态`h[t-1]`和最新的输入步骤`x`1 被串联在一起,并通过了在遗忘门和 S 形函数上获得的权重矩阵 将值压缩为 0 到 1 之间的值。将得到的矩阵`f[t]`逐点乘以上一步`c[t-1]`单元状态。 这有效地将掩码应用于先前的小区状态,使得仅来自先前的小区状态的相关信息被提出。 接下来,我们将查看**输入门**: @@ -137,7 +141,7 @@ LSTM 与 RNN 的结构非常相似。 虽然 LSTM 的各个步骤之间存在一 ## 双向 LSTM -前面我们曾提到,简单 RNN 的缺点是,由于它们只是向后看,它们无法捕获句子中单词的完整上下文。 在 RNN 的每个时间步骤中,仅考虑先前看到的单词,而不考虑句子中接下来出现的单词。 虽然基本 LSTM 类似地是向后的,但我们可以使用 LSTM 的改进版本,称为**双向 LSTM** ,它在序列中的每个时间步都考虑它之前和之后的词。 +前面我们曾提到,简单 RNN 的缺点是,由于它们只是向后看,它们无法捕获句子中单词的完整上下文。 在 RNN 的每个时间步骤中,仅考虑先前看到的单词,而不考虑句子中接下来出现的单词。 虽然基本 LSTM 类似地是向后的,但我们可以使用 LSTM 的改进版本,称为**双向 LSTM**,它在序列中的每个时间步都考虑它之前和之后的词。 双向 LSTM 同时处理规则顺序和反向顺序的序列,并保持两个隐藏状态。 我们将前向隐藏状态称为`f`t,并将`r`t 用作反向隐藏状态: @@ -145,7 +149,7 @@ LSTM 与 RNN 的结构非常相似。 虽然 LSTM 的各个步骤之间存在一 图 5.10 –双向 LSTM 过程 -在这里,我们可以看到我们在整个过程中都保持了这两个隐藏状态,并使用它们来计算最终的隐藏状态`h`t。 因此,如果我们希望计算时间步`t`的最终隐藏状态,则可以使用前向隐藏状态`f`t,该状态已看到所有单词,包括输入 *] x* t,以及反向隐藏状态`r`t,其中已经看到了`x`t 之后的所有单词。 因此,我们最终的隐藏状态`h`t 包含隐藏状态,这些状态已经看到了句子中的所有单词,而不仅仅是出现在时间步`t`之前的单词。 这意味着可以更好地捕获整个句子中任何给定单词的上下文。 事实证明,双向 LSTM 可以在多个 NLP 任务上提供比常规单向 LSTM 更高的性能。 +在这里,我们可以看到我们在整个过程中都保持了这两个隐藏状态,并使用它们来计算最终的隐藏状态`h`t。 因此,如果我们希望计算时间步`t`的最终隐藏状态,则可以使用前向隐藏状态`f[t]`,该状态已看到所有单词,包括输入`x[t]`,以及反向隐藏状态`r[t]`,其中已经看到了`x[t]`之后的所有单词。 因此,我们最终的隐藏状态`h[t]`包含隐藏状态,这些状态已经看到了句子中的所有单词,而不仅仅是出现在时间步`t`之前的单词。 这意味着可以更好地捕获整个句子中任何给定单词的上下文。 事实证明,双向 LSTM 可以在多个 NLP 任务上提供比常规单向 LSTM 更高的性能。 # 使用 LSTM 构建情感分析器 @@ -299,7 +303,7 @@ encode_sentences [0] 图 5.17 –模型架构 -现在,我们将演示如何使用 PyTorch 从头开始对该模型进行编码。 我们创建了一个名为 **SentimentLSTM** 的类,该类继承自 **nn.Module** 类。 我们将**初始**参数定义为 vocab 的大小,模型将具有的 LSTM 层数以及模型隐藏状态的大小: +现在,我们将演示如何使用 PyTorch 从头开始对该模型进行编码。 我们创建了一个名为`SentimentLSTM`的类,该类继承自`nn.Module`类。 我们将`__init__`参数定义为 vocab 的大小,模型将具有的 LSTM 层数以及模型隐藏状态的大小: SentimentLSTM(nn.Module)类: @@ -313,7 +317,7 @@ self.n_layers = n_layers self.n_hidden = n_hidden -然后,我们定义网络的每个层。 首先,我们定义嵌入层,该层的词汇量为字长,嵌入向量的大小为 **n_embed** 超参数。 我们的 LSTM 层是使用嵌入层的输出矢量大小,模型的隐藏状态的长度以及 LSTM 层将具有的层数来定义的。 我们还添加了一个参数来指定可以对 LSTM 进行批量数据训练,并添加一个参数以允许我们通过辍学实现网络正则化。 我们用概率 **drop_p** (将在模型创建时指定的超参数)定义另外一个辍学层,以及对最终全连接层和输出/预测节点的定义(具有 S 型激活函数) : +然后,我们定义网络的每个层。 首先,我们定义嵌入层,该层的词汇量为字长,嵌入向量的大小为`n_embed`超参数。 我们的 LSTM 层是使用嵌入层的输出矢量大小,模型的隐藏状态的长度以及 LSTM 层将具有的层数来定义的。 我们还添加了一个参数来指定可以对 LSTM 进行批量数据训练,并添加一个参数以允许我们通过辍学实现网络正则化。 我们用概率`drop_p`(将在模型创建时指定的超参数)定义另外一个辍学层,以及对最终全连接层和输出/预测节点的定义(具有 S 型激活函数) : self.embedding = nn.Embedding(n_vocab,n_embed) @@ -325,7 +329,7 @@ self.fc = nn.Linear(n_hidden,n_output) self.sigmoid = nn.Sigmoid() -接下来,我们需要在模型类中定义正向传递。 在此前向传递中,我们只是将一层的输出链接在一起,成为下一层的输入。 在这里,我们可以看到我们的嵌入层将 **input_words** 作为输入并输出了嵌入的单词。 然后,我们的 LSTM 层将嵌入的单词作为输入并输出 **lstm_out** 。 唯一的区别是,我们使用 **view()**将 LSTM 输出中的张量整形为正确的大小,以输入到全连接层中。 重塑隐藏层的输出以匹配输出节点的输出也是如此。 请注意,我们的输出将返回 **class = 0** 和 **class = 1** 的预测,因此我们将输出切为仅返回 **class = 1** 的预测— 也就是说,我们的句子为正的概率为: +接下来,我们需要在模型类中定义正向传递。 在此前向传递中,我们只是将一层的输出链接在一起,成为下一层的输入。 在这里,我们可以看到我们的嵌入层将`input_words`作为输入并输出了嵌入的单词。 然后,我们的 LSTM 层将嵌入的单词作为输入并输出`lstm_out`。 唯一的区别是,我们使用`view()`将 LSTM 输出中的张量整形为正确的大小,以输入到全连接层中。 重塑隐藏层的输出以匹配输出节点的输出也是如此。 请注意,我们的输出将返回`class = 0`和`class = 1`的预测,因此我们将输出切为仅返回`class = 1`的预测— 也就是说,我们的句子为正的概率为: def 转发(自己,input_words): @@ -347,7 +351,7 @@ sigmoid_last = sigmoid_out [:, -1] 返回 sigmoid_last,h -我们还定义了一个名为 **init_hidden()**的函数,该函数使用批量大小的尺寸初始化隐藏层。 如果我们愿意的话,这使我们的模型可以同时训练和预测许多句子,而不仅仅是一次训练一个句子。 请注意,此处将**设备**定义为**“ cpu”** ,以便在本地处理器上运行它。 但是,也可以将其设置为启用了 CUDA 的 GPU,以便在拥有 GPU 的 GPU 上进行训练: +我们还定义了一个名为`init_hidden()`的函数,该函数使用批量大小的尺寸初始化隐藏层。 如果我们愿意的话,这使我们的模型可以同时训练和预测许多句子,而不仅仅是一次训练一个句子。 请注意,此处将`device`定义为`"cpu"`,以便在本地处理器上运行它。 但是,也可以将其设置为启用了 CUDA 的 GPU,以便在拥有 GPU 的 GPU 上进行训练: def init_hidden(自己,batch_size): @@ -365,7 +369,7 @@ self.n_hidden).zero _()。to(device)) 返回 h -然后,我们通过创建 **SentimentLSTM** 类的新实例来初始化模型。 我们传递 vocab 的大小,嵌入的大小,隐藏状态的大小,输出大小以及 LSTM 中的层数: +然后,我们通过创建`SentimentLSTM`类的新实例来初始化模型。 我们传递 vocab 的大小,嵌入的大小,隐藏状态的大小,输出大小以及 LSTM 中的层数: n_vocab = len(word_to_int_dict) @@ -385,7 +389,7 @@ n_layers = 2 要训​​练我们的模型,我们必须首先定义我们的数据集。 我们将使用训练数据集来训练模型,在验证集的每一步上评估训练后的模型,然后最后使用看不见的测试数据集来测量模型的最终性能。 我们使用与验证训练分开的测试集的原因是,我们可能希望基于针对验证集的损失来微调模型超参数。 如果这样做,我们可能最终会选择对于该特定验证数据而言性能最佳的超参数。 我们根据看不见的测试集评估最终时间,以确保我们的模型能够很好地推广到训练循环任何部分之前从未见过的数据。 -我们已经将模型输入(`x`)定义为 **encode_sentences** ,但是我们还必须定义模型输出(`y`)。 我们这样做很简单,如下所示: +我们已经将模型输入(`x`)定义为`encode_sentences`,但是我们还必须定义模型输出(`y`)。 我们这样做很简单,如下所示: 标签= np.array([int(x)for data in data ['Sentiment']。values]) @@ -415,7 +419,7 @@ valid_data = TensorDataset(valid_x,valid_y) test_data = TensorDataset(test_x,test_y) -然后,我们使用这些数据集创建 PyTorch **DataLoader** 对象。 **DataLoader** 允许我们使用 **batch_size** 参数批量处理数据集,从而可以轻松地将不同的批量大小传递给我们的模型。 在这种情况下,我们将使其保持简单,并设置 **batch_size = 1** ,这意味着我们的模型将针对单个句子进行训练,而不是使用大量数据。 我们还选择随机调整 **DataLoader** 对象,以便数据以随机顺序(而不是每个时期相同)通过神经网络传递,从而有可能从训练顺序中消除任何有偏差的结果: +然后,我们使用这些数据集创建 PyTorch `DataLoader`对象。 `DataLoader`允许我们使用`batch_size`参数批量处理数据集,从而可以轻松地将不同的批量大小传递给我们的模型。 在这种情况下,我们将使其保持简单,并设置`batch_size = 1`,这意味着我们的模型将针对单个句子进行训练,而不是使用大量数据。 我们还选择随机调整`DataLoader`对象,以便数据以随机顺序(而不是每个时期相同)通过神经网络传递,从而有可能从训练顺序中消除任何有偏差的结果: batch_size = 1 @@ -425,7 +429,7 @@ valid_loader = DataLoader(valid_data,batch_size = batch_size,随机播放= test_loader = DataLoader(test_data,batch_size = batch_size,随机播放= True) -现在我们已经为我们的三个数据集定义了 **DataLoader** 对象,接下来我们定义训练循环。 我们首先定义许多超参数,这些超参数将在我们的训练循环中使用。 最重要的是,我们将损失函数定义为二进制交叉熵(因为我们正在处理单个二进制类别的预测),并且将优化器定义为 **Adam** ,学习率为`0.001`。 我们还定义了模型以运行较短的时间(以节省时间),并设置 **clip = 5** 来定义梯度裁剪: +现在我们已经为我们的三个数据集定义了`DataLoader`对象,接下来我们定义训练循环。 我们首先定义许多超参数,这些超参数将在我们的训练循环中使用。 最重要的是,我们将损失函数定义为二进制交叉熵(因为我们正在处理单个二进制类别的预测),并且将优化器定义为 **Adam**,学习率为`0.001`。 我们还定义了模型以运行较短的时间(以节省时间),并设置`clip = 5`来定义梯度裁剪: print_every = 2400 @@ -461,7 +465,7 @@ nn.utils.clip_grad_norm(net.parameters(),剪辑) Optimizer.step() -在这里,我们只训练了多个时期的模型,对于每个时期,我们首先使用批处理大小参数初始化隐藏层。 在这种情况下,我们设置 **batch_size = 1** ,因为我们一次只训练我们的模型一个句子。 对于火车装载机中的每批输入语句和标签,我们首先将梯度归零(以防止它们累积),并使用模型的当前状态使用数据的正向传递来计算模型输出。 然后使用此输出,使用模型的预测输出和正确的标签来计算损失。 然后,我们通过网络对该损失进行反向传递,以计算每个阶段的梯度。 接下来,我们使用 **grad_clip_norm()**函数裁剪渐变,因为这将阻止渐变爆炸,如本章前面所述。 我们定义了 **clip = 5** ,这意味着任何给定节点的最大梯度为`5`。 最后,我们通过调用 **optimizer.step()**,使用在向后传递中计算出的梯度来更新权重。 +在这里,我们只训练了多个时期的模型,对于每个时期,我们首先使用批处理大小参数初始化隐藏层。 在这种情况下,我们设置`batch_size = 1`,因为我们一次只训练我们的模型一个句子。 对于火车装载机中的每批输入语句和标签,我们首先将梯度归零(以防止它们累积),并使用模型的当前状态使用数据的正向传递来计算模型输出。 然后使用此输出,使用模型的预测输出和正确的标签来计算损失。 然后,我们通过网络对该损失进行反向传递,以计算每个阶段的梯度。 接下来,我们使用`grad_clip_norm()`函数裁剪渐变,因为这将阻止渐变爆炸,如本章前面所述。 我们定义了`clip = 5`,这意味着任何给定节点的最大梯度为`5`。 最后,我们通过调用`optimizer.step()`,使用在向后传递中计算出的梯度来更新权重。 如果我们自己运行此循环,我们将训练我们的模型。 但是,我们想在每个时期之后评估模型的性能,以便根据验证数据集确定模型的性能。 我们这样做如下: @@ -489,7 +493,7 @@ print(“ Epoch:{} / {}”。format((epoch + 1),n_epochs), net.train() -这意味着在每个时期结束时,我们的模型都会调用 **net.eval()**冻结模型的权重,并像以前一样使用我们的数据进行前向传递。 请注意,当我们处于评估模式时,也不会应用辍学。 但是,这次,我们使用验证加载程序,而不是使用训练数据加载程序。 通过这样做,我们可以在我们的验证数据集中计算模型当前状态的总损失。 最后,我们打印结果并调用 **net.train()**解冻模型的权重,以便我们可以在下一个纪元再次进行训练。 我们的输出看起来像,如下所示: +这意味着在每个时期结束时,我们的模型都会调用`net.eval()`冻结模型的权重,并像以前一样使用我们的数据进行前向传递。 请注意,当我们处于评估模式时,也不会应用辍学。 但是,这次,我们使用验证加载程序,而不是使用训练数据加载程序。 通过这样做,我们可以在我们的验证数据集中计算模型当前状态的总损失。 最后,我们打印结果并调用`net.train()`解冻模型的权重,以便我们可以在下一个纪元再次进行训练。 我们的输出看起来像,如下所示: ![Figure 5.18 – Training the model ](img/B12365_05_18.jpg) @@ -535,7 +539,7 @@ print(“测试精度:{:. 2f}”。format(num_correct / len(test_loader. 图 5.19 –输出值 -然后,我们将模型预测与真实标签进行比较,以获得 **correct_tensor** ,这是一个评估我们模型的每个预测是否正确的向量。 然后,我们对该向量求和,然后将其除以其长度,以获得模型的总精度。 在这里,我们获得了 76% 的准确率。 尽管我们的模型肯定还远非完美,但鉴于我们的训练量很小且训练时间有限,这一点也不差! 仅用于说明从 NLP 数据学习时 LSTM 的有用性。 接下来,我们将展示如何使用模型从新数据进行预测。 +然后,我们将模型预测与真实标签进行比较,以获得`correct_tensor`,这是一个评估我们模型的每个预测是否正确的向量。 然后,我们对该向量求和,然后将其除以其长度,以获得模型的总精度。 在这里,我们获得了 76% 的准确率。 尽管我们的模型肯定还远非完美,但鉴于我们的训练量很小且训练时间有限,这一点也不差! 仅用于说明从 NLP 数据学习时 LSTM 的有用性。 接下来,我们将展示如何使用模型从新数据进行预测。 ## 使用我们的模型进行预测 @@ -571,7 +575,7 @@ final.append(word_to_int_dict ['']) 我们删除标点符号和尾随空格,将字母转换为小写,并像以前一样对输入句子进行标记化。 我们将句子填充到长度为`50`的序列上,然后使用我们的预先计算的字典将的标记转换为数值。 请注意,我们的输入内容可能包含我们的网络从未见过的新词。 在这种情况下,我们的函数会将它们视为空令牌。 -接下来,我们创建实际的**预言()**函数。 我们预处理输入检查,将其转换为张量,然后将其传递给数据加载器。 然后,我们遍历该数据加载器(即使它仅包含一个句子),并通过我们的网络进行审查以获得预测。 最后,我们评估我们的预测并打印出正面还是负面的评价: +接下来,我们创建实际的`predict()`函数。 我们预处理输入检查,将其转换为张量,然后将其传递给数据加载器。 然后,我们遍历该数据加载器(即使它仅包含一个句子),并通过我们的网络进行审查以获得预测。 最后,我们评估我们的预测并打印出正面还是负面的评价: def 预测(评论): @@ -593,7 +597,7 @@ msg =“这是正面评价。” 如果输出> = 0.5,否则“这是负面评 print('Prediction ='+ str(输出)) -最后,我们在我们的评论中仅调用**预报()**进行预测: +最后,我们在我们的评论中仅调用`predict()`进行预测: 预言(“电影很好”) @@ -603,7 +607,7 @@ print('Prediction ='+ str(输出)) 图 5.20 –正值上的预测字符串 -我们还尝试对负值使用**预测()**: +我们还尝试对负值使用`predict()`: 预测(“不好”) @@ -631,7 +635,7 @@ heroku 登录 heroku 创建情感分析烧瓶 API -请注意,所有项目名称都必须是唯一的,因此您将需要选择一个非 **sentiment-analysis-flask-api** 的项目名称。 +请注意,所有项目名称都必须是唯一的,因此您将需要选择一个非`sentiment-analysis-flask-api`的项目名称。 我们的第一步是使用 Flask 构建基本 API。 @@ -649,7 +653,7 @@ cd flaskAPI python3 -m venv vir_env -在您的环境中,使用`pip`安装所需的所有软件包。 这包括您在模型程序中使用的所有软件包,例如 NLTK, **pandas** ,NumPy 和 PyTorch,以及运行 API 所需的软件包,例如 Flask 和 Gunicorn: +在您的环境中,使用`pip`安装所需的所有软件包。 这包括您在模型程序中使用的所有软件包,例如 NLTK, **Pandas**,NumPy 和 PyTorch,以及运行 API 所需的软件包,例如 Flask 和 Gunicorn: pip 安装 nltk 熊猫 numpy 火炬瓶 gunicorn @@ -657,9 +661,11 @@ pip 安装 nltk 熊猫 numpy 火炬瓶 gunicorn 点冻结> requirements.txt -我们需要进行的一项调整是,用以下内容替换 **requirements.txt** 文件中的**火炬**行: +我们需要进行的一项调整是,用以下内容替换`requirements.txt`文件中的`torch`行: -[**https://download.pytorch.org/whl/cpu/torch-1.3.1%2Bcpu-cp37-cp37m-linux_x86_64.whl**](https://download.pytorch.org/whl/cpu/torch-1.3.1%2Bcpu-cp37-cp37m-linux_x86_64.whl) +```py +https://download.pytorch.org/whl/cpu/torch-1.3.1%2Bcpu-cp37-cp37m-linux_x86_64.whl +``` 这是仅包含 CPU 实现的 PyTorch 版本的 wheel 文件的链接。 包括完整 GPU 支持的 PyTorch 完整版的大小超过 500 MB,因此它将无法在免费的 Heroku 群集上运行。 使用此更紧凑的 PyTorch 版本意味着您仍然可以在 Heroku 上使用 PyTorch 运行模型。 最后,我们在文件夹中创建另外三个文件,以及模型的最终目录: @@ -675,7 +681,7 @@ mkdir 模型 ## 使用 Flask 创建 API-API 文件 -在 **app.py** 文件中,我们可以开始构建我们的 API: +在`app.py`文件中,我们可以开始构建我们的 API: 1. We first carry out all of our imports and create a **predict** route. This allows us to call our API with the **predict** argument in order to run a **predict()** method within our API: @@ -711,7 +717,7 @@ mkdir 模型 word_to_int_dict = json.load(句柄) - 这将使用我们在主模型笔记本中计算的 **word_to_int** 字典并将其加载到我们的模型中。 这样我们的词索引与我们训练的模型一致。 然后,我们使用该词典将输入文本转换为编码序列。 确保从原始笔记本输出中提取 **word_to_int_dict.json** 文件,并将其放置在**模型**目录中。 + 这将使用我们在主模型笔记本中计算的`word_to_int`字典并将其加载到我们的模型中。 这样我们的词索引与我们训练的模型一致。 然后,我们使用该词典将输入文本转换为编码序列。 确保从原始笔记本输出中提取`word_to_int_dict.json`文件,并将其放置在`model`目录中。 3. Similarly, we must also load the weights from our trained model. We first define our **SentimentLSTM** class and the load our weights using **torch.load**. We will use the **.pkl** file from our original notebook, so be sure to place this in the **models** directory as well: @@ -759,11 +765,11 @@ curl -X GET http://0.0.0.0:8080/predict -H“ Content-Type:application / json ## 使用 Flask 创建 API-在 Heroku 上托管 -我们首先需要将文件提交到 Heroku,其方式类似于使用 GitHub 提交文件的方式。 我们只需运行以下命令,即可将工作中的 **flaskAPI** 目录定义为 **git** 文件夹: +我们首先需要将文件提交到 Heroku,其方式类似于使用 GitHub 提交文件的方式。 我们只需运行以下命令,即可将工作中的`flaskAPI`目录定义为`git`文件夹: git 初始化 -在此文件夹中,我们将以下代码添加到 **.gitignore** 文件中,这将阻止我们将不必要的文件添加到 Heroku 存储库中: +在此文件夹中,我们将以下代码添加到`.gitignore`文件中,这将阻止我们将不必要的文件添加到 Heroku 存储库中: vir_env @@ -771,7 +777,7 @@ __pycache __ / .DS_Store -最后,我们添加第一个**提交**函数,并将其推送到 **heroku** 项目中: +最后,我们添加第一个`commit`函数,并将其推送到 **heroku** 项目中: git 添加 -一种 @@ -781,7 +787,7 @@ git push heroku 主 编译可能会花费一些时间,因为系统不仅必须将所有文件从本地目录复制到 Heroku,而且 Heroku 将自动构建您定义的环境,安装所有必需的程序包并运行您的 API。 -现在,如果一切正常,您的 API 将自动在 Heroku 云上运行。 为了做出预测,您只需使用项目名称而不是 **sentiment-analysis-flask-api** 向 API 发出请求: +现在,如果一切正常,您的 API 将自动在 Heroku 云上运行。 为了做出预测,您只需使用项目名称而不是`sentiment-analysis-flask-api`向 API 发出请求: curl -X GET https://sentiment-analysis-flask-api.herokuapp.com/predict -H“内容类型:application / json” -d'{“输入”:“电影很好”}' -- GitLab