第4章 自然语言处理的前馈网络

作者: readilen | 来源:发表于2018-12-22 15:48 被阅读3次

    在第3章中,我们通过查看Perceptron(最简单的神经网络)来介绍神经网络的基础。感知器的历史性挫折之一是它无法学习数据中存在的适度非线性模式。例如,看一下图4-1中绘制的数据点。是一个异或(XOR),使用一条线不能分开(也称为线性可分)。在这种情况下,感知器失败。

    图4-1 XOR数据集中的两个类绘制为圆和星 注意没有一行可以分隔这两个类。
    在本章中,我们探讨了一系列传统上称为前网络的神经网络模型,以及两种前馈神经网络:多层感知器和卷积神经网络。多层感知器通过在单个层中将多个感知器分组并将多个层堆叠在一起,在结构上扩展了我们在第3章中研究的更简单的感知器。我们在短时间内介绍了多层感知器,并在“示例:使用多层感知器进行姓氏分类”中显示了它们在多类分类中的用法。

    本章研究的第二种前馈神经网络,即卷积神经网络,深受数字信号处理中窗口滤波器的启发。通过这种窗口属性,卷积神经网络能够在其输入中学习本地化模式,这不仅使它们成为计算机视觉的主力,而且还是检测诸如单词和句子之类的顺序数据中的子结构的理想候选者。我们在“卷积神经网络”中概述了卷积神经网络,并在“示例:使用CNN对姓氏进行分类”中演示了它们的用法。

    在本章中,多层感知器和卷积神经网络被组合在一起,因为它们都是前馈神经网络,并且与不同的神经网络系列(递归神经网络(RNN))形成对比,后者允许反馈(或循环),例如每个计算都通过其先前的计算得到通知。在第6章和第7章中,我们介绍RNNs以及为什么它可以是有益的,以允许在网络结构周期。

    当我们浏览这些不同的模型时,确保理解工作原理的一个小技巧是在计算数据张量时注意数据张量的大小和形状。每种类型的神经网络层对其计算的数据张量的大小和形状具有特定的影响,并且理解该效果可以极大地有助于更深入地理解这些模型。

    1. 多层感知器

    多层感知器(MLP)被认为是最基本的神经网络构建块之一。最简单的MLP是第3章感知器的扩展。Perceptron将数据向量作为输入并计算单个输出值。在MLP中,许多感知器被分组,使得单个层的输出是新的矢量而不是单个输出值。在PyTorch中,您将在后面看到,只需设置Linear图层中的输出要素数即可完成此操作。MLP的另一个方面是它将多个层与每层之间的非线性组合在一起。

    最简单的MLP,如图4-2所示,由三个表示阶段和两个Linear层组成。第一阶段是输入向量。这是给模型的矢量。在“示例:对餐馆评论的情感进行分类”中,输入向量是Yelp评论的折叠单热表示。给定输入向量,第一Linear层计算隐藏向量 - 第二阶段的代表。隐藏向量就是这样调用的,因为它是输入和输出之间的层的输出。“层的输出”是什么意思?理解这一点的一种方法是隐藏矢量中的值是构成该层的不同感知器的输出。使用该隐藏向量,第二Linear层计算输出向量。在对Yelp评论的情绪进行分类的二进制任务中,输出向量仍然可以是大小1.在多类设置中,您将在本章后面的“示例:使用多层感知器进行姓氏分类”一节中看到,输出向量是类数的大小。虽然在这个例子中,我们只显示一个隐藏的矢量,但是可以有多个中间阶段,每个阶段产生自己的隐藏矢量。始终使用Linear图层和非线性的组合将最终隐藏矢量映射到输出矢量。

    图4-2 MLP的可视化表示,具有Linear两层和三个表示阶段 - 输入向量,隐藏向量和输出向量
    MLP的力量来自添加第二Linear层并允许模型学习可线性分离的中间表示 - 表示的属性,其中单个直线(或更一般地,超平面)可用于区分数据点它们落在哪一侧(或超平面)。学习具有特定属性的中间表示,如对于分类任务可线性分离,是使用神经网络的最深刻的后果之一,并且是其建模能力的典型。在下一节中,我们将更加深入和深入地了解这意味着什么。

    1.1 一个简单的例子

    让我们看看前面描述的XOR示例,看看Perceptron与MLP会发生什么。在这个例子中,我们在二进制分类任务中训练Perceptron和MLP:星形和圆形。每个数据点都是2D坐标。如果不深入了解实现细节,最终的模型预测如图4-3所示。在该图中,错误分类的数据点用黑色填充,而正确分类的数据点未填充。在左侧面板中,如填充的形状所证明的,感知器难以学习可以分离的决策边界。星星和圆圈。然而,MLP(右图)学习了一个决策边界,可以更准确地对星星和圆圈进行分类。


    图4-3 显示了来自Perceptron(左)和MLP(右)的XOR的学习解决方案。每个数据点的真实类是点的形状:星形或圆形。使用块填充表示不正确的分类。这些行是每个模型的决策边界。在左侧面板中,Perceptron学习的决策边界无法正确地将圆与星星分开。在右侧面板中,MLP已经学会将星星与圆圈分开。

    虽然在图中看来MLP有两个决策边界,这就是它的优势,但它实际上只是一个决策边界!决策边界就是这样出现的,因为中间表示已经变形了空间以允许一个超平面出现在这两个位置中。在图4-4中,我们可以看到MLP计算的中间值。点的形状表示类(星形或圆形)。我们看到的是神经网络(在这种情况下是一个MLP)已经学会“扭曲”数据所在的空间,以便它可以在通过最后一层时将它们分成一条线。


    图4-4 MLP的输入和中间表示是可视化的。从左到右:(1)网络输入,(2)第一Linear模块的输出,(3)第一非线性的输出,以及(4)第二Linear模块的输出。如您所见,第一个Linear模块的输出将圆和星组合在一起,而第二个Linear模块的输出则重新组织数据点以便线性分离
    相反,如图4-没有额外的层,可以按下数据的形状,直到它变为线性可分。
    图4-5 Perceptron的输入和输出表示。因为它没有像MLP那样分组和重组的中间表示,所以它不能分离圆和星。

    1.2 在PyTorch中实现

    在上一节中,我们概述了MLP的核心思想。在本节中,我们将介绍PyTorch中的实现。如上所述,MLP除了第3章中更简单的Perceptron之外还有一层额外的计算。在我们在例4-1中给出的实现中,我们用两个PyTorch Linear模块实例化了这个想法。这些Linear对象被命名fc1fc2遵循一个共同的约定,即将Linear模块称为“完全连接的层”或简称为“fc层”。除了这Linear两层外,还有一个整流线性单元(ReLU)非线性在第3章中介绍,在“激活功能”部分中,在Linear输入到第二Linear层之前应用于第一层的输出。由于层的顺序性,必须注意确保层中的输出数等于下一层的输入数。在Linear两层之间使用非线性是必不可少的,因为没有它,Linear顺序中的两个层在数学上等同于单个Linear层因此无法模拟复杂的模式。我们对MLP的实现只实现了反向传播的前向传递。这是因为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中,我们实例化了MLP。由于MLP实现的一般性,我们可以对任何大小的输入进行建模。为了演示,我们使用大小为3的输入维度,大小为4的输出维度和大小为100的隐藏维度。请注意,如果在print语句的输出中,每个层中的单元数很好地排列以产生一个尺寸为4的输入的尺寸为4的输出。

    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)
    
    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没有太大区别。

    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)
    
    Type: torch.FloatTensor
    Shape/size: torch.Size([2, 3])
    Values: 
    tensor([[ 0.8329,  0.4277,  0.4363],
            [ 0.9686,  0.6316,  0.8494]])
    
    y_output = mlp(x_input, apply_softmax=False)
    describe(y_output)
    
    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模型的输入和输出非常重要。在前面的示例中,MLP模型的输出是具有两行和四列的张量。此张量中的行对应于批量维度,即维基批次中的数据点数。列是每个数据点的最终特征向量。在一些情况下,例如在分类设置中,特征向量是预测向量。“预测矢量” 意味着它对应于概率分布。预测向量会发生什么取决于我们当前是在进行训练还是进行预测。在训练期间,输出按原样使用,具有损失函数和目标类标签的表示。我们在“示例:使用多层感知器的姓氏分类”中深入介绍了这一点。

    但是,如果要将预测向量转换为概率,则需要额外的步骤。具体来说,您需要softmax函数,该函数用于将值向量转换为概率。softmax有很多根源。在物理学中,它被称为玻尔兹曼或吉布斯分布; 在统计学中,它是多项Logistic回归; 在自然语言处理(NLP)社区中,它是最大熵(MaxEnt)分类器。无论名称如何,函数的都是大的正值会导致更高的概率,而较低的负值会导致较小的概率。在示例4-3中,需要指定apply_softmax参数应用。在例4-4中,你可以看到相同的输出,但这次将apply_softmax标志设置为True

    y_output = mlp(x_input,apply_softmax = True)
    describe(y_output)
    
    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]])
    
    y_output = mlp(x_input, apply_softmax=False)
    describe(y_output)
    
    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]])
    

    总而言之,MLP是Linear将张量映射到其他张量的堆叠层。在每对Linear层之间使用非线性来打破线性关系,并允许模型扭转周围的向量空间。在分类设置中,这种扭曲应该导致类之间的线性可分离性。此外,您可以使用softmax函数将MLP输出解释为概率,但不应将softmax与特定的损失函数一起使用,因为底层实现还有其他的快捷实现方式。

    2. 示例:使用多层感知器进行姓氏分类

    在本节中,我们将MLP应用于将姓氏分类到其原籍国的任务。从公开可观察数据推断人口统计信息(如国籍),确保不同人口统计数据的用户应用产品。人口统计信息和其他自我识别信息统称为“受保护属性”。您必须谨慎使用建模和产品中的此类属性。我们首先分割每个姓氏的字符,并按照“示例:餐厅评论的情感分类”处理单词的方式对待它们。除了数据差异外,字符级模型在结构和实现方面与基于字的模型大致相似。
    您应该从这个示例中获得的一个重要教训是,MLP的训练和预测是我们在第3章中对Perceptron所实现的训练和预测的直接进展。事实上,我们在本书的第3章中回顾了这个例子,作为对这些组件进行更全面概述的地方。此外,我们不会包含您在“示例:对餐厅评论的情感进行分类”中可以看到的代码。
    本节的其余部分首先介绍Surname数据集及其预处理步骤。然后,Vocabulary以及DataLoader类实现了姓字符串到矢量的分批次minibatch。如果您阅读第3章,你会很熟悉这些类,只是在上面做了一些小改动。
    我们通过描述Surname Classifier模型以及其设计背后的思维过程继续该部分。MLP类似于我们在第3章中看到的Perceptron示例,但除了模型更改之外,我们还在此示例中介绍了多类输出及其相应的损失函数。在描述模型之后,我们将完成训练程序。训练程序与您在“示例:餐厅评论的情感分类”中看到的非常相似,所以为了简洁起见,我们不像在该部分那样深入研究。我们强烈建议您返回该部分以获得进一步的说明。
    我们通过在数据集的测试部分上评估模型并描述新姓氏的推理过程来结束该示例。多类预测的一个很好的特性是我们不仅可以查看最高预测,还可以了解如何推断新姓氏的top- k预测。

    2.1. 姓氏数据集

    在这个例子中,我们引入了一个Surnames数据集,这是来自18个不同国籍的10,000个姓氏的集合,这些来自作者从互联网上的不同地方收集而来的。该数据集将在本书的几个示例中重复使用。这个数据集有几个有趣的属性。第一个属性是它相当不平衡。前三类占数据的60%以上:27%是英语,21%是俄语,14%是阿拉伯语。其余15个国家的频率正在下降 - 这是一种语言特有的分布。第二个属性是原籍国与姓氏拼写(拼写)之间存在有效且直观的关系。拼音变化与原籍国密切相关(例如“奥尼尔”,“安东诺普洛斯”,“长泽”或“朱”)。
    为了创建最终数据集,我们开始使用的比较低的版本,执行了几个数据集修改操作。第一个是减少不平衡 - 原始数据集超过70%是俄罗斯,可能是由于采样偏差或俄罗斯独特姓氏的扩散。为此,我们通过选择标记为俄语的随机化姓氏采样对这个过度代表的类进行采样。接下来,我们根据国籍对数据集进行分组,并将数据集分为三个部分:70%到训练数据集,15%到验证数据集,最后15%用于测试数据集。
    该实现SurnameDataset几乎与“示例:餐厅评论的情感分类”中ReviewDataset所见的相同,只是在方法的实现方式上存在细微差别。回想一下,本书中介绍的数据集类继承自PyTorch的类,因此,我们需要实现两个函数:getitem,在给定索引时返回数据点; 和len,返回数据集的长度。“示例:餐厅评论的情感分类”中的示例与此示例之间的差异在示例4-5中示出。而不是像以前那样返回矢量化评论__getitem__ Dataset __getitem__``__len__ `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}
    

    2.2. 词汇,Vectorizer和DataLoader

    要使用它的字符姓分类,我们使用VocabularyVectorizer以及DataLoader对姓字符串转换成矢量minibatches。这些是示例:分类餐厅评论的情感”中使用的相同数据结构,例证了以与Yelp评论的单词标记相同的方式处理姓氏的字符标记的多态性。不是通过将单词标记映射到整数来进行向量化,而是通过将字符映射到整数来对数据进行向量化。

    词汇课

    Vocabulary示例中使用的类是完全一样的Vocabulary中使用的“示例:餐厅点评的判断情”在Yelp的评论词语映射到它们相应的整数。作为简要概述,它Vocabulary是两个Python字典的协调,它们在令牌(本例中为字符)和整数之间形成双射; 也就是说,第一个字典将字符映射到整数索引,第二个字典将整数索引映射到字符。add_token方法用于向其中添加新标记Vocabularylookup_token方法用于检索索引,lookup_index方法用于检索给定索引的标记(在推理阶段很有用)。与之Yelp评论Vocabulary相反,我们使用独热表示并且不计算字符的频率,因此不仅仅限于频率项目。这主要是因为数据集很小并且大多数字符都足够频繁。

    SURNAMEVECTORIZER

    Vocabulary转换成单独的标记(字符)的整数,则SurnameVectorizer是负责应用Vocabulary和姓转换到载体中。实例化和使用都非常相似,ReviewVectorizer在“实例:判断餐厅点评的情绪”,但有一个关键的区别:该字符串不是空格分开。姓氏是字符序列,每个字符都是我们的个人标记Vocabulary。然而,直到“卷积神经网络”,我们将忽略序列信息,并通过迭代字符串输入中的每个字符来创建输入的折叠独热矢量表示。我们为以前没有遇到的字符指定一个特殊标记UNK。UNK符号仍然在角色中使用,Vocabulary因为我们Vocabulary仅从训练数据中实例化,并且验证或测试数据中可能存在唯一字符。

    您应该注意,尽管我们在本示例中使用了独热编码,但将在后面的章节中了解其他矢量化方法,这些方法可以替代独热编码,有时甚至比独热编码更好。具体来说,在“示例:使用CNN对姓氏进行分类”中,您将看到一个独热矩阵,其中每个字符都是矩阵中的一个位置,并且具有自己的独热编码。然后,在第5章中,您将了解Embedding图层,返回整数向量的向量化,以及如何使用这些向量来创建密集向量矩阵。但现在,让我们来看看为代码SurnameVectorizer在例4-6。

    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 one-hot 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)
    

    2.3. 姓氏分类器模型

    SurnameClassifier是本章前面介绍的MLP的实现(例4-7)。第一Linear层将输入矢量映射到中间矢量,并且将非线性应用于该矢量。第二Linear层将中间矢量映射到预测矢量。

    在最后一步中,softmax可选地应用操作以确保输出总和为1; 也就是说,解释为“概率”。我们研究了“损失函数”中的交叉熵损失。回想一下,交叉熵损失对于多类分类是最理想的,但是在训练期间计算softmax不仅浪费,而且在许多情况下也不是数值稳定的。

    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
    

    2.4. 训练程序

    虽然我们使用不同的模型,数据集和损失函数,但训练程序保持不变。因此,在示例4-8中,我们仅示出了args该示例中的训练例程与“示例:餐馆评论的分类情感”中的示例之间的主要差异。

    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
    )
    

    训练中最显着的差异与模型中的输出类型和使用的损失函数有关。在此示例中,输出是可以转换为概率的多类预测向量。如模型描述中所述,此输出的损失类型仅限于CrossEntropyLossNLLLoss。由于它的简化,我们使用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-10显示了使用不同的密钥来获取数据batch_dict。除了这种美容差异,训练循环的功能保持不变。使用训练数据,计算模型输出,损失和梯度。然后,使用渐变来更新模型。

    # 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()
    

    2.5. 模型评估与预测

    模型评估与预测

    要了解模型的性能,您应该使用定量和定性方法分析模型。定量地,测量保持的测试数据上的误差确定分类器是否可以概括为看不见的示例。定性地,您可以通过查看分类器的top-k预测来为新示例开发直观的模型所学习的内容。

    评估测试数据集

    为了评估SurnameClassifier测试数据,我们执行与“示例:餐厅评论的情感分类”中的餐厅评论文本分类示例相同的例程:我们设置数据集以迭代测试数据,调用classifier.eval()方法并迭代测试数据与我们对其他数据的处理方式相同。在此示例中,classifier.eval()当使用测试/评估数据时,调用会阻止PyTorch更新模型参数。
    该模型在测试数据上实现了约50%的准确度。如果您在随附的笔记本中运行培训例程,您会注意到培训数据的性能更高。这是因为模型总是更适合它所训练的数据,因此训练数据的性能并不表示新数据的性能。如果您跟随代码,我们建议您尝试不同大小的隐藏维度。您应该注意到性能的提高。然而,增加幅度不大(特别是与[“示例:使用CNN对姓氏进行分类”中的模型进行比较时。主要原因是折叠的单热矢量化方法是弱表示。虽然它确实将每个姓氏紧凑地表示为单个向量,但它会丢弃字符之间的订单信息,这对于识别来源至关重要。

    对新姓进行分类

    例4-11显示了对新姓氏进行分类的代码。给定姓氏作为字符串,该函数将首先应用矢量化过程,然后获得模型预测。请注意,我们包含apply_softmax标志以result包含概率。在多项式情况下,模型预测是类概率列表。我们使用PyTorch张量最大值函数来获得由最高预测概率表示的最佳类。

    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}
    

    检索新姓氏的前K预测

    查看不仅仅是最佳预测通常很有用。例如,NLP中的标准做法是采用最佳k -best预测并使用另一个模型重新排它们。PyTorch提供了一个torch.topk函数,它提供了一种方便的方法来获得这些预测,如例4-12所示

    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
    

    2.6. 规范MLP:权重正则化和结构正规化(或Dropout)

    在第3章中,我们解释了正则化如何成为过度拟合问题的解决方案,并研究了两种重要的权重正则化类型-L1和L2。这些权重正则化方法也适用于MLP以及卷积神经网络,我们将在本章后面介绍。除了权重正则化之外,对于深度模型(即具有多个层的模型),例如本章中讨论的前馈网络,称为Dropout的结构正则化方法变得非常重要。

    Dropout

    简单来说,在训练期间,丢失概率地丢弃属于两个相邻层的单元之间的连接。为什么要这有帮助?我们从Stephen Merity的直观和幽默)解释开始:

    Dropout,简单描述,是一个概念,如果你可以学习如何在醉酒时反复完成任务,你应该能够在清醒时更好地完成任务。这种见解产生了许多最先进的结果和一个致力于防止在神经网络上使用Dropout的新生领域。

    神经网络 - 尤其是具有大量层的深层网络 - 可以在单元之间创建有趣的共同适应。“共适应”是来自神经科学的术语,但在这里它仅仅指的是两个单元之间的连接变得过强而牺牲其他单元之间的连接的情况。这通常会导致模型过度拟合数据。通过概率地丢弃单元之间的连接,我们可以确保没有一个单元总是依赖于另一个单元,从而产生强大的模型。降不增加额外的参数模型,但需要一个超参数- “丢弃概率。” 掉落几率正如您可能已经猜到的那样,单位之间的连接被丢弃的概率。通常将丢弃概率设置为0.5。例4-13给出了具有丢失的MLP的重新实现。

    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仅在培训期间而不是在评估期间应用。作为练习,我们鼓励您尝试SurnameClassifier使用Dropout模型,看看它如何改变结果。

    3. 卷积神经网络

    在本章的第一部分中,我们深入研究了MLP,由一系列线性层和非线性函数构建的神经网络。MLP不是利用顺序模式的最佳工具。例如,在Surnames数据集中,姓氏可以有不同长度的片段,这可以揭示他们的起源国(例如“O'Neill”中的“O”,“Antonopoulos”中的“opoulos”) ,“”sawa“in”Nagasawa“,或”Zh“in”Zhu“)。这些段可以具有可变长度,并且挑战在于捕获它们而不明确地对它们进行编码。

    在本节中,我们将介绍卷积神经网络(CNN),这是一种非常适合检测空间子结构(并因此创建有意义的空间子结构)的神经网络。CNN通过使用少量权重来扫描输入数据张量来实现这一点。从这次扫描中,它们产生输出张量,代表子结构的检测(或不检测)。

    在本节的其余部分,我们首先描述CNN可以起作用的方式以及设计CNN时应该关注的问题。我们深入研究CNN超参数,目的是提供关于这些超参数对输出的行为和影响的直觉。最后,我们通过一些简单的例子来说明CNN的机制。在“示例:使用CNN对姓氏进行分类”中,我们将深入探讨更广泛的示例。

    历史背景
    CNN的名称和基本功能源于称为卷积的经典数学运算。几十年来,卷积已被用于各种工程学科,包括数字信号处理和计算机图形学。经典地,卷积使用了程序员指定的参数。指定参数以匹配某些功能设计,例如突出显示边缘或抑制高频声音。实际上,许多Photoshop滤镜都是应用于图像的固定卷积运算。然而,在深度学习和本章中,我们从数据中学习卷积滤波器的参数,因此它是解决手头任务的最佳选择。
    

    3.1. CNN超参数

    为了了解不同的设计决策对CNN的意义,我们在图4-6中给出了一个例子。在此示例中,单个“内核”应用于输入矩阵。卷积运算(线性算子)的精确数学表达式对于理解本节并不重要,但是你应该从这个图中得出的直觉是核是一个小方阵,应用于输入矩阵中的不同位置。一种系统的方式。
    通过指定内核的特定值来设计经典卷积,通过指定控制CNN行为的超参数然后使用梯度下降来找到给定数据集的最佳参数来设计 CNN。两个主要的超参数控制卷积的形状(称为kernel_size)和卷积将在输入数据张量(称为stride)中相乘的位置。还有一些超参数可以控制输入数据张量用0s(称为padding)填充多少,以及当应用于输入数据张量时乘法应该相隔多远(称为dilation)。在以下小节中,我们将更详细地描述这些超参数的直觉。

    卷积运算的维数

    要理解的第一个概念是卷积运算的维数。我们使用二维卷积进行说明,但是根据数据的性质,存在更适合的其他维度的卷积。在PyTorch,卷积可以是一维的,二维的,或三维和由被实现Conv1dConv2dConv3d模块。一维卷积对于时间序列是有用的,其中每个时间步长具有特征向量。在这种情况下,我们可以学习序列维度上的模式。NLP中的大多数卷积运算都是一维卷积。另一方面,二维卷积试图沿数据中的两个方向捕获时空模式; 例如,在沿高度和宽度尺寸的图像中 - 这是为什么二维卷积在图像处理中很受欢迎。类似地,在三维卷积中,沿着数据中的三维捕获图案。例如,在视频数据中,信息位于三维中 - 表示图像帧的两个维度,以及表示帧序列的时间维度。就本书而言,我们使用Conv1d 为主。

    通道

    通道是指沿输入中每个点的特征尺寸。例如,在图像中,对应于RGB分量的图像中的每个像素有三个通道。当使用卷积时,类似的概念可以被转移到文本数据。从概念上讲,如果文本文档中的“像素”是单词,则通道的数量是词汇表的大小。如果我们更细粒度并考虑对字符进行卷积,则通道数是字符集的大小(在这种情况下恰好是词汇表)。在PyTorch卷积实现中,输入中的通道数是in_channels参数。卷积运算可以在输出中产生多个通道(out_channels)。您可以将此视为卷积运算符将输入要素维度“映射”到输出要素维度。图4-7和图4-8说明了这个概念。

    image.png

    内核大小

    内核矩阵的宽度称为内核大小(kernel_size在PyTorch中)。在图4-6中,内核大小为2,相比之下,我们在图4-9中]示了一个大小为3的内核。您应该开发的直觉是,卷积在输入中组合空间(或时间)本地信息,并且每个卷积的本地信息量由内核大小控制。但是,通过增加内核的大小,您还可以减小输出的大小(Dumoulin和Visin,2016)。这就是为什么输出矩阵是2x2在图4-9当内核大小是3,但3x3在基质图4-6当内核大小为2。

    Stride

    Stride控制卷积之间的步长。如果步幅与内核的大小相同,则内核计算不会重叠。另一方面,如果步幅是1,则内核最大程度地重叠。

    填充

    即使stride并且kernel_size允许控制每个计算的特征值具有多少范围,它们也具有缩小特征图的总大小(卷积的输出)的有害且有时无意的副作用。为了抵消这种情况,通过0在每个相应的维度上附加和预先添加s来人为地使输入数据张量的长度(如果是1d,2d或3d),高度(如果是2d或3d)和深度(如果是3d)更大。这因此意味着CNN将执行更多的卷积,但是可以控制输出形状而不损害期望的内核尺寸,步幅或扩张。

    扩张

    扩张控制卷积内核如何应用于输入矩阵。我们表明,将扩展从1(默认值)增加到2意味着当应用于输入矩阵时,内核的元素彼此相距两个空格。考虑这个问题的另一种方法是跨越内核本身 - 内核中的元素或内核应用程序之间存在一个步长,其中包含“漏洞”。这对于汇总输入空间的较大区域而不增加参数数量。当堆叠卷积层时,膨胀卷积已证明非常有用。连续扩张的卷积以指数方式增加“感受野”的大小; 也就是说,在进行预测之前网络所看到的输入空间的大小。

    相关文章

      网友评论

        本文标题:第4章 自然语言处理的前馈网络

        本文链接:https://www.haomeiwen.com/subject/qubukqtx.html