「PyTorch自然语言处理系列」4. 自然语言处理的前馈网络(上)

来源 |  Natural Language Processing with PyTorch

作者 | Rao,McMahan

译者 | Liangchu

校对 | gongyouliu

编辑 | auroral-L

全文共10461字,预计阅读时间85分钟。

上下拉动翻看这个书签

4.1 多层感知机
  4.1.1 一个简单示例:XOR
  4.1.2 在 PyTorch 中实现多层感知机
 4.2 示例:使用多层感知机对姓氏进行分类
  4.2.1 姓氏数据集
  4.2.2 Vocabulary,Vectorizer和DataLoader
   4.2.2.1 Vocabulary类
   4.2.2.2 SurnameVectorizer
  4.2.3 SurnameClassifier模型
  4.2.4 训练例程
   4.2.4.1 训练循环(training loop)
  4.2.5 模型评估和预测
   4.2.5.1 在测试集上评估
   4.2.5.2 分类一个新姓氏
   4.2.5.3 获取新姓氏的前k个预测
  4.2.6 MLPs正则化:权重正则化和结构正则化(或Dropout)
 4.3 卷积神经网络
  4.3.1 CNN 超参数
   4.3.1.1 卷积操作的维度
   4.3.1.2 通道
   4.3.1.3 核大小
   4.3.1.4 Stride
   4.3.1.5 Padding
   4.3.1.6 Dilation
  4.3.2 在 PyTorch 实现 CNNs
 4.4 示例:使用 CNN 对姓氏进行分类
  4.4.1 SurnameDataset类
  4.4.2 Vocabulary,Vectorizer和DataLoader
  4.4.3 使用 CNN 重新实现SurnameClassifier
  4.4.4 训练例程
  4.4.5 模型评估和预测
   4.4.5.1 在测试集上评估
   4.4.5.2 为新的姓氏分类或获取最佳预测
 4.5 CNN 中的其他话题
  4.5.1 池化操作
  4.5.2 批量规范化(BatchNorm)
  4.5.3 网络中的网络连接(1x1卷积)
  4.5.4 残差连接/残差块
 4.6 总结

第三章中,我们通过感知机介绍了神经网络的基础知识,感知机是现存最简单的神经网络。感知机一个历史性的缺点是:它不能很好地学习数据中存在的一些特别的模式。例如,查看下图(4-1)中绘制的数据点,出现了一种非此即彼(XOR)的情况,在该情况下,决策边界不能是单条直线(也称线性可分(linearly separable))。在这个例子中,感知机分类失败。

「PyTorch自然语言处理系列」4. 自然语言处理的前馈网络(上)_第1张图片

本章中,我们将探索传统上称为前馈网络(feed-forward networks)的神经网络模型家族,我们重点关注两种前馈神经网络:多层感知机(multilayer perceptron,MLP))和卷积神经网络( convolutional neural network ,CNN)。我们在第三章中介绍过一种将多个感知机在一个单层聚合,并将多个层叠加在一起的感知机,相比之下,多层感知机在结构上是这种更简单的感知机的拓展。我们稍后将介绍多层感知机,并在“示例:使用多层感知机对姓氏进行分类”一节中展示它们在多层分类中的应用。

本章将介绍的第二种前馈神经网络是卷积神经网络,它在处理数字信号时深受窗口滤波器(windowed filter)的启发。通过这种窗口特性,卷积神经网络能够在输入中学习局部化模式,这不仅使其成为计算机视觉的主力,而且使其成为检测单词和句子等序列数据中的子结构的理想候选。我们将在“卷积神经网络”一节中介绍卷积神经网络,并在“示例:使用 CNN 对姓氏进行分类”一节中演示它们的用法。

在本章中,多层感知机和卷积神经网络被划分为一类网络,因为它们都是前馈神经网络,此外,我们还将之与另一类神经网络——循环神经网络(recurrent neural networks ,RNNs)进行对比,循环神经网络允许反馈(或循环),从而使得每次计算都可以从之前的计算中获得信息。在第六章和第七章中,我们会介绍循环神经网络以及为什么循环在神经网络结构中是有益的。

当我们介绍这些不同模型时,确保能理解事物如何工作的一个有用方法是:在计算数据张量(tensor)时注意它们的大小(size)和形状(shape)。每种神经网络层对它所计算的数据张量的大小和形状都有特定的影响,理解这种影响可以很好地帮助你理解这些模型。


4.1 多层感知机

多层感知机被认为是最基本的神经网络构建模块之一。最简单的多层感知机是对第三章中出现的感知机的扩展。感知机将数据向量作为输入,计算出单个输出值。在多层感知机中,许多感知机被分组,因此单个层的输出是一个新的向量而非单个输出值。在 PyTorch 中,正如你接下来将看到的,这仅仅通过设置线性层中的输出特征的数量就可以了。多层感知机的另一个方面是,它将多个层的每层之间非线性地结合到了一起。

最简单的多层感知机正如下图(4-2)所示,它由三个表示(representation)阶段和两个线性(Linear)层组成。第一阶段是输入向量(input vector),它是喂给模型的向量。在“示例:分类餐馆评论的情感”一节中,输入向量是 Yelp 评论的独热编码表示。给定输入向量,第一个线性层计算出隐藏向量(hidden vector),也即表示的第二阶段。隐藏向量之所以这样被调用,是因为它是位于输入和输出之间的层的输出。我们这里所说“层的输出”又是什么意思?可以这样理解:隐藏向量中的值是组成层的不同感知机的输出。使用这个隐藏的向量,第二个线性层计算一个输出向量(output vector)。在像 Yelp 评论情感分类这样的二元任务中,输出向量是1维的。在多元分类的情况下(将在本章后面“示例:使用多层感知机对姓氏进行分类”一节中介绍),输出向量等于类数量的大小。虽然在这个例子中,我们只展示了一个隐藏向量,但其实可以有很多中间阶段,每个阶段产生自己的隐藏向量。最终的隐藏向量总是通过线性层和非线性的组合映射到输出向量。

「PyTorch自然语言处理系列」4. 自然语言处理的前馈网络(上)_第2张图片

多层感知机的威力来自于:添加了第二个线性(Linear)层,以及允许模型学习一个线性可分(linearly separable)的中间表示——表示的一种性质,可以使用一条直线(或更广泛意义上的一个超平面)来区分数据点落在直线(或超平面)的哪一侧。学习具有特定属性的中间表示(intermediate representations),比如对分类任务来说的线性可分性,是使用神经网络的最有深远意义的影响之一,也是其建模能力的精髓。在下一节中,我们将更深入地研究它的意义。

4.1.1 一个简单示例:XOR

回顾一下前面描述的 XOR 示例,看看感知机与多层感知机相比会发生什么。在本例子中,我们在一个二元分类任务中训练感知机和多层感知机:给星(⭐)和圆(⚪)分类。每个数据点都是一个二维坐标。深入研究细节实现之前,我们先给出最终的模型预测,正如下图(4-3所示)。在这个图中,错误分类的数据点用黑色填充,而正确分类的数据点是空心的。在左侧面板中,根据填充结果可以发现,感知机要学习一个可以将星和圆分开的决策边界是比较困难的。然而右侧面板中的多层感知机更精确地习得了一个对星和圆进行分类的决策边界。

「PyTorch自然语言处理系列」4. 自然语言处理的前馈网络(上)_第3张图片

虽然图中显示多层感知机有两个决策边界,这正是它的优点,但它实际上只是一个决策边界!决策边界就是这样显示的,因为中间隐藏层的加入改变了空间,使得一个超平面同时出现在这两个位置上。在下图(4-4)中,我们可以看到多层感知机计算的中间值。这些点的形状表示类(星或圆)。我们能看到,神经网络(本例中为多层感知机)已经学会了“扭曲”数据所处的空间,以便在数据通过最后一层时,用单条线来分割它们。

「PyTorch自然语言处理系列」4. 自然语言处理的前馈网络(上)_第4张图片

对比之下,如下图(4-5)所示,感知机没有额外的层来处理数据形状直到数据变得线性可分:

「PyTorch自然语言处理系列」4. 自然语言处理的前馈网络(上)_第5张图片

4.1.2 在 PyTorch 中实现多层感知机

在上一节中,我们概述了多层感知机的核心思想。在本节中,我们将介绍 PyTorch 中的一个实现。如前所述,多层感知机与第三章中介绍的简单的感知机相比多出了一个计算层。在下例(4-1)给出的实现中,我们用 PyTorch 的两个线性模块实现了这个想法。线性对象(Linear objects)被命名为fc1和fc2,它们遵循一个通用约定:将线性模块称为“全连接层(fully connected layer)”,简称为“fc 层”。除了这两个线性层外,还有一个修正线性单元(Rectified Linear Unit,ReLU)的非线性层(在第三章“激活函数”一节中有介绍),它在被输入到第二个线性层之前应用于第一个线性层的输出。由于层的顺序性,你必须确保层中的输出数量等于下一层的输入数量。使用两个线性层之间的非线性是必要的,因为没有它,两个线性层在数学上等价于一个线性层,从而不能建模复杂的模式。多层感知机的实现只实现反向传播(backpropagation)的前向传递(forward pass),这是因为 PyTorch 根据模型的定义和向前传递的实现,自动计算出如何进行向后传递和梯度更新。

示例 4-1:使用PyTorch实现多层感知机

import torch.nn as nn
import torch.nn.functional as F


class MultilayerPerceptron(nn.Module):
    def __init__(self, input_dim, hidden_dim, output_dim):
        """
        Args:
            input_dim (int): the size of the input vectors
            hidden_dim (int): the output size of the first Linear layer
            output_dim (int): the output size of the second Linear layer
        """
        super(MultilayerPerceptron, self).__init__()
        self.fc1 = nn.Linear(input_dim, hidden_dim)
        self.fc2 = nn.Linear(hidden_dim, output_dim)


    def forward(self, x_in, apply_softmax=False):
        """The forward pass of the MLP


        Args:
            x_in (torch.Tensor): an input data tensor.
                x_in.shape should be (batch, input_dim)
            apply_softmax (bool): a flag for the softmax activation
                should be false if used with the Cross Entropy losses
        Returns:
            the resulting tensor. tensor.shape should be (batch, output_dim)
        """
        intermediate = F.relu(self.fc1(x_in))
        output = self.fc2(intermediate)


        if apply_softmax:
            output = F.softmax(output, dim=1)
        return output

在下例(4-2)中,我们实例化了多层感知机。由于多层感知机实现的通用性,我们可以建模任何大小的输入。为了演示这一点,我们使用大小为 3 的输入维度、大小为 4 的输出维度和大小为 100 的隐藏维度。请注意,在print语句的输出中,每个层中的单元数很好地排列在一起,以便为维度3的输入生成维度4的输出。

示例 4-2:NLP 的实例化示例

Input[0]
batch_size = 2 # number of samples input at once
input_dim = 3
hidden_dim = 100
output_dim = 4


# Initialize model
mlp = MultilayerPerceptron(input_dim, hidden_dim, output_dim)
print(mlp)


Output[0]
MultilayerPerceptron(
  (fc1): Linear(in_features=3, out_features=100, bias=True)
  (fc2): Linear(in_features=100, out_features=4, bias=True)
  (relu): ReLU()
)

我们可以通过传入一些随机输入来快速测试模型的“连接”,如下例(4-3)所示。由于模型还没经过训练,因此输出是随机的。在训练模型之前,这样做是一个有用的完整性检查。请注意 PyTorch 的交互性是如何让我们在开发过程中实时完成这些工作的,这与使用 NumPy 或 Pandas时差别不大:

示例 4-3:使用随机输入测试多层感知机

Input[0]
def describe(x):
    print("Type: {}".format(x.type()))
    print("Shape/size: {}".format(x.shape))
    print("Values: \n{}".format(x))


x_input = torch.rand(batch_size, input_dim)
describe(x_input)


Output[0]
Type: torch.FloatTensor
Shape/size: torch.Size([2, 3])
Values:
tensor([[ 0.8329,  0.4277,  0.4363],
        [ 0.9686,  0.6316,  0.8494]])


Input[1]
y_output = mlp(x_input, apply_softmax=False)
describe(y_output)


Output[1]
Type: torch.FloatTensor
Shape/size: torch.Size([2, 4])
Values:
tensor([[-0.2456,  0.0723,  0.1589, -0.3294],
        [-0.3497,  0.0828,  0.3391, -0.4271]])

学习如何读取 PyTorch 模型的输入和输出非常重要。在上述例子中,多层感知机模型的输出是一个有两行四列的张量。这个张量中的行与批次维数对应,批次维数是minibatch中数据点的数量。列是每个数据点最终的特征向量。在一些情况下,例如在分类任务中,特征向量是一个预测向量(prediction vector)。“预测向量”表示它对应于一个概率分布。预测向量会发生什么取决于我们当前是在进行训练还是推理。在训练期间,输出与损失函数和目标类标签的表示一起使用。我们将在“示例:使用多层感知机对姓氏进行分类”一节中对此进行深入介绍。

然而,如果你想将预测向量转换为概率,则还得采取额外步骤。具体而言,你需要使用softmax激活函数,它用于将一个值向量转换为概率。softmax 函数有很多其他名字:在物理学中,它被称为玻尔兹曼分布(Boltzmann distribution)或吉布斯分布(Gibbs distribution);在统计学中,它是多项式逻辑回归;在自然语言处理(NLP)中,它是最大熵(maximum entropy,MaxEnt)分类器。不管它叫什么,这个函数的思想是:大的正值会导致更高的概率,小的负值会导致更小的概率。在上例(4-3)中,apply_softmax参数应用了这个额外的步骤。在下例(4-4)中,你可以看到相同的输出,但是这次我们将apply_softmax标志设置为True:

示例 4-4:使用多层感知机产生概率输出(注意apply_softmax=True选项)

Input[0]
y_output = mlp(x_input, apply_softmax=True)
describe(y_output)


Output[0]
Type: torch.FloatTensor
Shape/size: torch.Size([2, 4])
Values:
tensor([[ 0.2087,  0.2868,  0.3127,  0.1919],
        [ 0.1832,  0.2824,  0.3649,  0.1696]])

综上所述,多层感知机是堆叠的线性层,这些线性层将张量映射到其他张量。在每一对线性层之间使用非线性来打破线性关系,并允许模型扭曲向量空间。在分类情况下,这种扭曲会产生类之间的线性可分性。此外,你可以使用 softmax 函数将多层感知机输出解释为概率,但你不应该将 softmax 与特定的损失函数一起使用,因为底层实现可以利用优越的数学/计算捷径。

4.2 示例:使用多层感知机对姓氏进行分类

在本节中,我们将使用多层感知机对姓氏进行分类,将之与其国籍对应。根据公开观察到的数据推断人口统计信息(如国籍),我们可以将之应用于产品推荐,以确保不同人口统计数据的用户获得公平的结果。然而,在建模和在产品中使用这些属性时,必须注意一点——人口统计和其他自定义信息统称为“受保护属性(protected attributes)”。我们首先对每个姓氏的字符进行拆分,并像对待“示例:分类餐馆评论的情感”一节中的单词一样对待这些字符。除了数据上的差异,字符层模型在结构和实现上与基于单词的模型基本相似.

你应该从这个例子中吸取的一个重要教训是:多层感知机的实现和训练是从我们在第三章中所学感知机的实现和训练直接发展来的。事实上,我们在本书的第三章中提到了这个例子,以便更全面地了解这些组件。此外,我们不会包括“示例:分类餐馆评论的情感”一节中看到的代码,倘若你想看到示例代码,我们强烈建议你参阅补充材料。

本节首先描述姓氏数据集及其预处理步骤,然后我们使用Vocabulary,Vectorizer和DataLoader类逐步完成从姓氏字符串到向量化minibatch的管道。若你读过第三章,那么你应该比较熟悉这些类了,这里只是做了一些小小的改动。

我们将通过surnameclassifier模型及其设计背后的过程来继续本节内容。多层感知机类似于我们在第三章中看到的感知机例子,但是除了模型的改变,我们在这个例子中引入了多类输出及其对应的损失函数。在描述了模型之后,我们会完成训练过程。训练程序与你在“示例:分类餐馆评论的情感”一节中看到的程序非常相似,因此,为了简洁起见,我们在这里不会像之前那样深入讲解程序。我们强烈建议你回顾一下该节内容,以更好理解本节内容。

我们会通过评估模型在数据集测试部分的表现以及描述一个新姓氏的推断过程来结束本例。多类预测的一个优秀属性是:我们可以看到的不仅仅是最高预测,还能知道如何推断新姓氏的前k个预测。

4.2.1 姓氏数据集

在这个例子中,我们介绍了一个姓氏数据集(surnames dataset),作者从互联网上收集了不同的姓名源的信息,这个数据集包括来自18个不同国家的10000个姓氏。该数据集会在本书中的几个示例中重复使用,它具备一些有趣的属性。第一个属性:它是相当不平衡的。排名前三的类占数据的 60% 以上:27% 是英语,21% 是俄语,14% 是阿拉伯语,剩下的 15 个国籍的频率也在下降——这也是语言特有的特性;第二个属性:在国籍和姓氏拼写体系(拼写)之间有种有效而直观的关系,有些拼写变体与原籍国的联系非常紧密(比如O'Neill、Antonopoulos、Nagasawa或Zhu)。

为了创建最终的数据集,我们从比本书补充材料中给出的版本处理更少的一版开始,并执行一些数据集更改操作。首先是要减少不平衡——原数据集中 70% 以上是俄文,这可能是由于抽样偏差或俄文姓氏的增多。为此,我们通过标记为俄语姓氏的随机子集来对这个占比过多类进行子采样。接下来,我们基于国籍对数据集进行分组,并将其分为三个部分:70% 分给训练集,15% 分给验证集,最后 15% 分给测试集,以便使跨这些数据集的类标签分布具有可比性。

SurnameDataset的实现与“示例:分类餐馆评论的情感”一节中的ReviewDataset几近相同,只是在__getitem__()方法的实现上略有差异。回想一下,本书中呈现的数据集类继承自 PyTorch 的Dataset类,因此,我们需要实现两个函数:__getitem__()方法以及__len__()方法,前者在给定索引时返回一个数据点,后者返回数据集的长度。第三章中的示例与本示例的区别在于__getitem__方法,正如下例(4-5)所示。相比“示例:分类餐馆评论的情感”中返回一个向量化的评论,本例返回的是一个向量化的姓氏和与其国籍相对应的索引:

示例 4-5:实现SurnameDataset.__getitem__()

class SurnameDataset(Dataset):
    # Implementation is nearly identical to Section 3.5


    def __getitem__(self, index):
        row = self._target_df.iloc[index]
        surname_vector = \
            self._vectorizer.vectorize(row.surname)
        nationality_index = \
            self._vectorizer.nationality_vocab.lookup_token(row.nationality)


        return {'x_surname': surname_vector,
                'y_nationality': nationality_index}
4.2.2 Vocabulary,Vectorizer和DataLoader

为了使用字符对姓氏进行分类,我们使用Vocabulary,Vectorizer和DataLoader将姓氏字符串转换为向量化的minibatch。这些数据结构与“示例:分类餐馆评论的情感”中使用的数据结构相同,它们举例说明了一种多态性,它以与 Yelp 评论的单词标记(word token)相同的方式处理姓氏的字符标记(character token)。数据是通过将字符映射到整数而非通过将单词标记映射到整数来向量化的。

4.2.2.1 Vocabulary类

本例中使用的Vocabulary类与示例3-16中将 Yelp 评论中的单词映射到对应的整数的类完全相同。简要介绍一下,Vocabulary是两个 Python 字典的组合,这两个字典在token(在本例中是字符character)和整数之间形成一个双向单射,也就是说,第一个字典将字符映射到整数索引,第二个字典将整数索引映射到字符。add_token()方法用于向Vocabulary中添加新的token,lookup_token()方法用于检索索引,并且可以用于根据给定索引检索token(在推理阶段很有用)。与 Yelp 评论的Vocabulary不同,我们使用的是一种独热表示,并不计算字符出现的频率,只对频繁出现的条目进行限制。这主要是因为数据集很小,且大多数字符出现的频率很高。

4.2.2.2 SurnameVectorizer

Vocabulary将单个token(字符)转换为整数,SurnameVectorizer负责应用Vocabulary并将姓氏转换为向量。本节它中的实例化和使用非常类似于之前"Vectorizer"一节中出现的ReviewVectorizer,但有一个关键区别:字符串不是基于空格分割的。姓氏是字符组成的序列,每个字符在我们的Vocabulary中都是一个独立的token。然而,在讲到“卷积神经网络”一节之前,我们将忽略序列信息,并通过迭代字符串输入中的每个字符来创建输入的折叠独热向量。我们为从未遇到过的字符指定一个特殊的标记——UNK,因为我们仅从训练数据实例化Vocabulary,然而验证集或测试集中可能有特殊字符,所以在字符Vocabulary中仍使用UNK符号。

请注意,尽管我们在本例中使用了折叠的的独热表示,但在后续章节中,你还会了解其他种向量化方法,可以用来替代独热向量,有时甚至会表现得更好。具体而言,在“示例:使用 CNN 对姓氏进行分类”中,你将看到一个独热矩阵(one-hot matrix),其中每个字符都是矩阵中的一个位置,并有属于自己的独热向量。然后在第五章中,你会学习嵌入层(embedding layer),这种向量化返回一个整数向量,你还会学习如何使用它们创建密集向量矩阵。但是现在,让我们看一下下例(4-6)中SurnameVectorizer的代码:

示例 4-6:实现SurnameVectorizer

class SurnameVectorizer(object):
    """ The Vectorizer which coordinates the Vocabularies and puts them to use"""
    def __init__(self, surname_vocab, nationality_vocab):
        self.surname_vocab = surname_vocab
        self.nationality_vocab = nationality_vocab


    def vectorize(self, surname):
        """Vectorize the provided surname


        Args:
            surname (str): the surname
        Returns:
            one_hot (np.ndarray): a collapsed onehot encoding
        """
        vocab = self.surname_vocab
        one_hot = np.zeros(len(vocab), dtype=np.float32)
        for token in surname:
            one_hot[vocab.lookup_token(token)] = 1
        return one_hot


    @classmethod
    def from_dataframe(cls, surname_df):
        """Instantiate the vectorizer from the dataset dataframe


        Args:
            surname_df (pandas.DataFrame): the surnames dataset
        Returns:
            an instance of the SurnameVectorizer
        """
        surname_vocab = Vocabulary(unk_token="@")
        nationality_vocab = Vocabulary(add_unk=False)


        for index, row in surname_df.iterrows():
            for letter in row.surname:
                surname_vocab.add_token(letter)
            nationality_vocab.add_token(row.nationality)


        return cls(surname_vocab, nationality_vocab)
4.2.3 SurnameClassifier模型

下例(4-7)中的SurnameClassifier是本章前面所介绍的多层感知机的实现。第一个线性(Linear)层将输入向量映射到中间向量,并对其应用非线性操作。第二个线性层将中间向量映射到预测向量。

在最后一步中,可以有选择性地应用softmax函数,以确保输出之和为 1,也就是所谓的”概率“。至于它为何是可选的,这就与我们所使用的交叉熵损失(见”损失函数“一节)的数学公式有关了。回想一下,交叉熵损失对于多元分类是最理想的,但在训练过程中softmax的计算不仅是一种浪费,而且在很多情况下并不稳定。

示例 4-7:使用多元感知机的SurnameClassifier

import torch.nn as nn
import torch.nn.functional as F


class SurnameClassifier(nn.Module):
    """ A 2-layer Multilayer Perceptron for classifying surnames """
    def __init__(self, input_dim, hidden_dim, output_dim):
        """
        Args:
            input_dim (int): the size of the input vectors
            hidden_dim (int): the output size of the first Linear layer
            output_dim (int): the output size of the second Linear layer
        """
        super(SurnameClassifier, self).__init__()
        self.fc1 = nn.Linear(input_dim, hidden_dim)
        self.fc2 = nn.Linear(hidden_dim, output_dim)


    def forward(self, x_in, apply_softmax=False):
        """The forward pass of the classifier


        Args:
            x_in (torch.Tensor): an input data tensor.
                x_in.shape should be (batch, input_dim)
            apply_softmax (bool): a flag for the softmax activation
                should be false if used with the Cross Entropy losses
        Returns:
            the resulting tensor. tensor.shape should be (batch, output_dim)
        """
        intermediate_vector = F.relu(self.fc1(x_in))
        prediction_vector = self.fc2(intermediate_vector)


        if apply_softmax:
            prediction_vector = F.softmax(prediction_vector, dim=1)


        return prediction_vector
4.2.4 训练例程

虽然在本例中,我们使用了不同的模型、数据集和损失函数,但是训练例程是相同的。因此,在下例(4-8)中,我们只展示args,以及本例中的训练例程与“示例:分类餐馆评论的情感”中训练例程之间的主要区别。

示例 4-8:超参数以及基于多层感知机的Yelp评论分类器的程序选项

args = Namespace(
    # Data and path information
    surname_csv="data/surnames/surnames_with_splits.csv",
    vectorizer_file="vectorizer.json",
    model_state_file="model.pth",
    save_dir="model_storage/ch4/surname_mlp",
    # Model hyper parameters
    hidden_dim=300
    # Training  hyper parameters
    seed=1337,
    num_epochs=100,
    early_stopping_criteria=5,
    learning_rate=0.001,
    batch_size=64,
    # Runtime options omitted for space
)

训练中最显著的差异与模型中输出的种类和使用的损失函数有关。在本例中,输出是一个多类预测向量,它可以转换为概率。可被用于此类输出的损失函数仅限于CrossEntropyLoss()和NLLLoss()。简洁起见,我们使用CrossEntropyLoss()。

在下例(4-9)中,我们展示了数据集、模型、损失函数和优化器的实例化。这些实例看起来应该与第三章中例子的实例几近相同。事实上,在后续章节中,这种模式会在每个例子中重复出现。

示例 4-9:实例化数据集,模型,损失和优化器

dataset = SurnameDataset.load_dataset_and_make_vectorizer(args.surname_csv) 
vectorizer = dataset.get_vectorizer() 
 
classifier = SurnameClassifier(input_dim=len(vectorizer.surname_vocab), 
                               hidden_dim=args.hidden_dim, 
                               output_dim=len(vectorizer.nationality_vocab)) 
 
classifier = classifier.to(args.device)     
 
loss_func = nn.CrossEntropyLoss(dataset.class_weights) 
optimizer = optim.Adam(classifier.parameters(), lr=args.learning_rate)
4.2.4.1 训练循环(training loop)

与第三章中出现的的训练循环相比,本例的训练循环除了变量名以外与之几近相同。具体而言,下例(4-10)展示使用不同的key从batch_dict中获取数据。除了外观上的差异,训练循环的功能保持不变。使用训练数据,计算模型输出、损失和梯度。然后,使用梯度来更新模型。

示例 4-10:训练循环的代码段

# the training routine is these 5 steps:


# --------------------------------------
# step 1\. zero the gradients
optimizer.zero_grad()


# step 2\. compute the output
y_pred = classifier(batch_dict['x_surname'])


# step 3\. compute the loss
loss = loss_func(y_pred, batch_dict['y_nationality'])
loss_batch = loss.to("cpu").item()
running_loss += (loss_batch - running_loss) / (batch_index + 1)


# step 4\. use loss to produce gradients
loss.backward()


# step 5\. use optimizer to take gradient step
optimizer.step()
4.2.5 模型评估和预测

要了解模型的性能,你应该使用定量(quantitative)和定性(qualitative)的方法来分析模型。定量,也就是测量测试数据的误差决定了分类器能否推广到不可见的例子;定性,你可以通过查看分类器对一个新示例的前k个预测,直观分析模型所学到的东西。

4.2.5.1 在测试集上评估

要在测试数据上评估SurnameClassifier,我们执行和”评估,推理和检查“一节中餐厅评论文本分类的例子一样的例程:将数据集设置为遍历测试数据,调用classifier.eval()方法,以与其他数据相同的方式迭代测试数据。本例中,调用classifier.eval()可以防止 PyTorch 在使用测试/评估数据时更新模型参数。

该模型在测试数据上的准确性达到 50% 左右。若你在附带的notebook中运行训练例程,会发现在训练数据上的表现更好。这是因为模型总是更适合它所训练的数据,所以模型对于训练数据的性能并不能代表它对新数据的性能。若你遵循代码,我们鼓励你尝试不同大小的隐藏维度,应该能得到性能上的提高。然而这种提高不会很大(尤其是与“示例:使用 CNN 对姓氏进行分类”中的模型相比)。主要原因是:折叠的独热向量是一种弱表示,虽然它简单地将每个姓氏表示为单个向量,但它丢弃了字符之间的顺序信息,而这对于识别姓氏来源非常重要。

4.2.5.2 分类一个新姓氏

下例(4-11)给出了为新姓氏分类的代码。给定一个姓氏作为字符串,该函数将首先应用向量化过程,然后获得模型预测。注意,我们包含了apply_softmax标志,所以结果包含概率。在多项式的情况下,模型预测是类概率组成的列表。我们使用 PyTorch 张量max()函数来得到由最大的预测概率表示的最优类。

示例 4-11:使用现存模型(分类器)进行推理:根据给定姓名预测国籍

def predict_nationality(name, classifier, vectorizer):
    vectorized_name = vectorizer.vectorize(name)
    vectorized_name = torch.tensor(vectorized_name).view(1, -1)
    result = classifier(vectorized_name, apply_softmax=True)


    probability_values, indices = result.max(dim=1)
    index = indices.item()


    predicted_nationality = vectorizer.nationality_vocab.lookup_index(index)
    probability_value = probability_values.item()


    return {'nationality': predicted_nationality,
            'probability': probability_value}
4.2.5.3 获取新姓氏的前k个预测

有时不仅要看最好的预测,还要看更多的预测。例如,NLP 中的标准实践是采用k个最佳预测并使用另一个模型对它们重新排序。PyTorch 提供了一个torch.topk()函数,它提供了一种方便的方法来获得这些预测,如下例(4-12)所示:

示例 4-12:预测前k个国籍

def predict_topk_nationality(name, classifier, vectorizer, k=5):
    vectorized_name = vectorizer.vectorize(name)
    vectorized_name = torch.tensor(vectorized_name).view(1, -1)
    prediction_vector = classifier(vectorized_name, apply_softmax=True)
    probability_values, indices = torch.topk(prediction_vector, k=k)


    # returned size is 1,k
    probability_values = probability_values.detach().numpy()[0]
    indices = indices.detach().numpy()[0]


    results = []
    for prob_value, index in zip(probability_values, indices):
        nationality = vectorizer.nationality_vocab.lookup_index(index)
        results.append({'nationality': nationality,
                        'probability': prob_value})


    return results
4.2.6 MLPs正则化:权重正则化和结构正则化(或Dropout)

在第三章中,我们解释了正则化是如何解决过拟合问题的,并研究了两类重要的权重正则化——L1和 L2。这些权重正则化方法也适用于多层感知机和卷积神经网络,我们将在本章后面介绍这些内容。除了权重正则化外,对于深度模型(也即有多个层的模型)比如本章中讨论的前馈网络,一种称为丢弃(dropout)的结构正则化方法变得非常重要。

简而言之,在训练过程中,dropout有一定概率使属于两个相邻层的单元之间的连接减弱。这有什么用呢?我们从Stephen Merity一段直观且幽默的解释领会到这一点:

dropout,简单地说,是指如果你在喝醉的时候还能反复学习如何做一件事,那么你在清醒的时候应能做得更好。这一见解产生了许多最先进的结果,并形成了一个致力于dropout在神经网络上使用的新兴领域。

神经网络——尤其是具有大量层次(layer)的深层网络——可以在单元之间创建有趣的共适应。“共适应”(coadaptation)是神经科学中的术语,但在这里它只是指一种情况,即两个单元之间的联系变得过于紧密,而牺牲了其他单元之间的联系,这通常会导致过拟合。通过有概率地丢弃单元之间的连接,我们可以确保没有一个单元总是依赖于另一个单元,从而得到一个健壮的模型。dropout不会向模型中添加额外的参数,但是需要一个超参数——“丢弃概率”(drop probability)。drop probability,你可能已经猜到了,它是单位之间的连接被丢弃的概率。通常将这个概率设置为 0.5。下例(4-13)给出了一个带dropout的多层感知机的重新实现:

示例 4-13:带有dropout的多层感知机

import torch.nn as nn
import torch.nn.functional as F


class MultilayerPerceptron(nn.Module):
    def __init__(self, input_dim, hidden_dim, output_dim):
        """
        Args:
            input_dim (int): the size of the input vectors
            hidden_dim (int): the output size of the first Linear layer
            output_dim (int): the output size of the second Linear layer
        """
        super(MultilayerPerceptron, self).__init__()
        self.fc1 = nn.Linear(input_dim, hidden_dim)
        self.fc2 = nn.Linear(hidden_dim, output_dim)


    def forward(self, x_in, apply_softmax=False):
        """The forward pass of the MLP


        Args:
            x_in (torch.Tensor): an input data tensor.
                x_in.shape should be (batch, input_dim)
            apply_softmax (bool): a flag for the softmax activation
                should be false if used with the Cross Entropy losses
        Returns:
            the resulting tensor. tensor.shape should be (batch, output_dim)
        """
        intermediate = F.relu(self.fc1(x_in))
        output = self.fc2(F.dropout(intermediate, p=0.5))


        if apply_softmax:
            output = F.softmax(output, dim=1)
        return output

请注意,dropout只适用于训练期间,不适用于评估期间。作为练习,我们鼓励你尝试使用dropout的SurnameClassifier模型,看看结果有什么变化。

「PyTorch自然语言处理系列」4. 自然语言处理的前馈网络(上)_第6张图片

你可能感兴趣的:(神经网络,python,机器学习,人工智能,深度学习)