赞
踩
来源 | Natural Language Processing with PyTorch
作者 | Rao,McMahan
译者 | Liangchu
校对 | gongyouliu
编辑 | auroral-L
全文共4790字,预计阅读时间30分钟。
上下拉动翻看这个书签
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 总结
为了证明 CNN 的有效性,让我们使用一个简单的CNN模型来分类姓氏。这项任务的许多细节与前面多层感知机的示例相同,但是模型的构造和向量化过程发生了变化。模型的输入将是一个独热矩阵,而非我们在上一个例子中看到的压缩的独热向量。这种设计使得 CNN 更好地“查看”字符的排列,并对在“示例:使用多层感知机对姓氏进行分类”一节中使用的压缩的独热编码中丢失的序列信息进行编码。
我们已经在“姓氏数据集”一节中介绍过姓氏数据集。本例中,我们会使用相同的数据集,然而在实现上有一个不同之处:数据集由独热向量矩阵而非一个压缩的独热向量组成。为了实现这一点,我们实现了一个数据集类,它跟踪最长的姓氏,并将其作为矩阵中包含的行数提供给向量化器。列的数量是独热向量的大小(Vocabulary的大小)。下例(4-17)展示对SurnameDataset.__getitem__()所做的更改,我们会在下一小节显示对SurnameVectorizer.vectorize()的更改。
示例 4-17:为传递最大姓氏长度而修改的SurnameDataset
- class SurnameDataset(Dataset):
- # ... existing implementation from Section 4.2
-
-
- def __getitem__(self, index):
- row = self._target_df.iloc[index]
-
-
- surname_matrix = \
- self._vectorizer.vectorize(row.surname, self._max_seq_length)
-
-
- nationality_index = \
- self._vectorizer.nationality_vocab.lookup_token(row.nationality)
-
-
- return {'x_surname': surname_matrix,
- 'y_nationality': nationality_index}
我们使用数据集中最长的姓氏来控制独热矩阵的大小,有两个原因:首先,将每个minibatch的姓氏矩阵组合成一个三维张量,要求它们的大小相同;其次,使用数据集中最长的姓氏意味着可以以相同的方式处理每个minibatch。
在本例中,尽管Vocabulary和DataLoader的实现方式与前面“ Vocabulary,Vectorizer和DataLoader”一节中的示例相同,但我们改变了Vectorizer的vectorize()方法,以适应 CNN 模型的需要。具体而言,正如我们在下例(4-18)中的代码中所示,该函数将字符串中的每个字符映射到一个整数,然后使用该整数构造一个由独热向量组成的矩阵。重要的是,矩阵中的每一列都是不同的独热向量,主要原因是因为我们将使用的Conv1d层要求数据张量在0维上具有batch,在第1维上具有channel,在第2维上具有feature。
除了更改为使用独热矩阵之外,我们还修改了Vectorizer,以便计算姓氏的最大长度并将其保存为max_surname_length。
示例 4-18:为 CNN 实现SurnameVectorizer
- class SurnameVectorizer(object):
- """ The Vectorizer which coordinates the Vocabularies and puts them to use"""
- def vectorize(self, surname):
- """
- Args:
- surname (str): the surname
- Returns:
- one_hot_matrix (np.ndarray): a matrix of onehot vectors
- """
-
-
- one_hot_matrix_size = (len(self.character_vocab), self.max_surname_length)
- one_hot_matrix = np.zeros(one_hot_matrix_size, dtype=np.float32)
-
-
- for position_index, character in enumerate(surname):
- character_index = self.character_vocab.lookup_token(character)
- one_hot_matrix[character_index][position_index] = 1
-
-
- return one_hot_matrix
-
-
- @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
- """
- character_vocab = Vocabulary(unk_token="@")
- nationality_vocab = Vocabulary(add_unk=False)
- max_surname_length = 0
-
-
- for index, row in surname_df.iterrows():
- max_surname_length = max(max_surname_length, len(row.surname))
- for letter in row.surname:
- character_vocab.add_token(letter)
- nationality_vocab.add_token(row.nationality)
-
-
- return cls(character_vocab, nationality_vocab, max_surname_length)
'运行
我们在本例中使用的模型是使用我们在“卷积神经网络”中介绍的方法构建的。事实上,我们创建的“人工”数据用于测试该部分中的卷积层,它与使用本例中的Vectorizer的姓氏数据集中的数据张量大小完全匹配。正如下例(4-19)所示,这与我们在前面介绍的Conv1d序列既有相似之处,也有需要解释的新内容。具体而言,该模型类似于前面的模型,它使用一系列一维卷积以增量方式计算更多的特征,从而生成单个特征向量。
然而本例中的新内容是Sequential和 ELU PyTorch 模块的使用。Sequential模块是封装线性操作序列的包装器,在这种情况下,我们使用它来封装Conv1d序列的应用。ELU类似于第三章中介绍的 ReLU 的非线性函数,但它不是将值裁剪到 0 以下,而是对它们求幂。ELU 已被证实是卷积层之间使用的一种很有前途的非线性操作(Clevert等人,2015)。
在本例中,我们将每个卷积的通道数与num_channels超参数绑定。我们可以选择不同数量的通道分别进行卷积运算,这样做需要优化更多的超参数。我们发现256已经足够大了,可以使模型达到合理的性能。
示例 4-19:基于 CNN 的SurnameClassifier
- import torch.nn as nn
- import torch.nn.functional as F
-
-
- class SurnameClassifier(nn.Module):
- def __init__(self, initial_num_channels, num_classes, num_channels):
- """
- Args:
- initial_num_channels (int): size of the incoming feature vector
- num_classes (int): size of the output prediction vector
- num_channels (int): constant channel size to use throughout network
- """
- super(SurnameClassifier, self).__init__()
-
-
- self.convnet = nn.Sequential(
- nn.Conv1d(in_channels=initial_num_channels,
- out_channels=num_channels, kernel_size=3),
- nn.ELU(),
- nn.Conv1d(in_channels=num_channels, out_channels=num_channels,
- kernel_size=3, stride=2),
- nn.ELU(),
- nn.Conv1d(in_channels=num_channels, out_channels=num_channels,
- kernel_size=3, stride=2),
- nn.ELU(),
- nn.Conv1d(in_channels=num_channels, out_channels=num_channels,
- kernel_size=3),
- nn.ELU()
- )
- self.fc = nn.Linear(num_channels, num_classes)
-
-
- def forward(self, x_surname, apply_softmax=False):
- """The forward pass of the classifier
- Args:
- x_surname (torch.Tensor): an input data tensor.
- x_surname.shape should be (batch, initial_num_channels,
- max_surname_length)
- 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, num_classes)
- """
- features = self.convnet(x_surname).squeeze(dim=2)
- prediction_vector = self.fc(features)
-
-
- if apply_softmax:
- prediction_vector = F.softmax(prediction_vector, dim=1)
-
-
- return prediction_vector
训练例程包括下面似曾相识的一系列操作:实例化数据集、实例化模型、实例化损失函数、实例化优化器、遍历数据集的训练集和更新模型参数、遍历数据集的验证集和测量性能、然后重复一定次数的数据集迭代。到目前为止,这是本书的第三个实现的训练例程,这一系列的操作应该内部化。对于这个例子,我们将不再详细描述具体的训练例程,因为它与前面“示例:使用多层感知机对姓氏进行分类”一节中的例程完全相同,然而输入参数是不同的,如下例(4-20)中所示:
示例 4-20:CNN 姓氏分类器的输入参数
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/cnn", # Model hyper parameters hidden_dim=100, num_channels=256, # Training hyper parameters seed=1337, learning_rate=0.001, batch_size=128, num_epochs=100, early_stopping_criteria=5, dropout_p=0.1, # Runtime omitted for space ... )
要理解模型的性能,需要对性能进行定量和定性的度量。下面将概述这两个度量的基本内容。我们鼓励你在这些基础上进一步探索该模型及其所学内容。
正如本示例与前面的的训练例程没变一样,执行评估的代码也没有变化。总之,调用分类器的eval()方法用于防止反向传播,并迭代测试集。与多层感知机大概50%的性能相比,该模型在测试集上性能的准确率约为 56%。尽管这些表示性能的数字绝非这些特定架构所能达到的上限,但是通过一个相对简单的 CNN 模型获得的改进应该足以激励你在文本数据上尝试 CNN了。
在本例中,predict_nationality()函数的一部分发生了更改,如下例(4-21)所示:不使用view()方法重塑新创建的数据张量以添加batch维度,而是使用PyTorch 的unsqueeze()函数在batch应处的位置添加size=1的维度。predict_topk_nationality()函数也有这种更改。
示例 4-21:使用训练过的模型进行预测
- def predict_nationality(surname, classifier, vectorizer):
- """Predict the nationality from a new surname
- Args:
- surname (str): the surname to classifier
- classifier (SurnameClassifer): an instance of the classifier
- vectorizer (SurnameVectorizer): the corresponding vectorizer
- Returns:
- a dictionary with the most likely nationality and its probability
- """
- vectorized_surname = vectorizer.vectorize(surname)
- vectorized_surname = torch.tensor(vectorized_surname).unsqueeze(0)
- result = classifier(vectorized_surname, 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}
'运行
为了结束本章的讨论,我们将在本节中概述几个对CNN至关重要而且也在平常使用中扮演主要角色的附加话题。准确的说,你会了解池化操作、批量规范化、网络中的网络连接和残差连接。
池化(pooling)是将高维特征映射总结为低维特征映射的操作。卷积的输出是一个特征映射,特征映射中的值总结了输入的一些部分。由于卷积计算的重叠性,许多计算出的特征可能是冗余的。池化是一种将高维(可能是冗余的)特征映射总结为低维特征映射的方法。在形式上,池化是一种像求和,均值或最大值这样的算术运算符,系统地应用于特征映射中的局部区域,得到的池操作分别称为求和池化(sum pooling)、平均池化(average pooling)和最大池化(max pooling)。池化还可以作为将较大但较弱的特征映射的统计强度改进为较小但较强的特征映射的方法。下图(4-13)阐述了池化:
批量标准化(Batch normalization或BatchNorm)是设计CNN时常用的一种工具。BatchNorm 通过将激活量缩放为零均值和单位方差来对 CNN 的输出进行转换。它用于Z转换(Z transform)的平均值和方差值每批(batch)更新一次,这样任何一个批次的波动都不会对其产生太大的影响。BatchNorm 使得模型对参数的初始化不那么敏感,并且简化了学习速率的调整(Ioffe and Szegedy, 2015)。在 PyTorch 中,BatchNorm是在nn模块中定义的。下例(4-22)展示了如何用卷积和线性层实例化和使用BatchNorm:
示例 4-22:使用Conv1D层和BatchNorm
- # ...
- self.conv1 = nn.Conv1d(in_channels=1, out_channels=10,
- kernel_size=5,
- stride=1)
- self.conv1_bn = nn.BatchNorm1d(num_features=10)
- # ...
-
-
- def forward(self, x):
- # ...
- x = F.relu(self.conv1(x))
- x = self.conv1_bn(x)
- # ...
网络中的网络(Network-in-network,NiN)连接是具有kernel_size=1的卷积核,并具有一些有趣的特性。具体来说,1x1卷积就像通道之间一个完全连通的线性层,这在从具有多个通道的特征映射映射到更浅的特征映射时非常有用。在下图(4-14)中,我们展示了一个用于输入矩阵的 NiN 连接。可以看到,它将两个通道缩减为一个通道。因此,NiN或1x1卷积提供了一种廉价的方法来在很少的参数下加入额外的非线性(Lin et al.,2013)。
CNN实现真正深度网络(超过100层)的最重要趋势之一是残差连接(residual connection),也称为跳跃连接(skip connection)。如果将卷积函数表示为conv,那么残差块的输出如下:
然而,这个操作有一个隐含的技巧,如下图(4-15)所示,对于要添加到卷积输出的输入,它们必须具有相同的形状。要实现这一点,标准做法是在卷积之前应用padding。在下图(4-15)中,对于大小为3的卷积,padding的大小为1。为了了解更多关于残差连接的细节,He等人(2016)的论文仍然是一个很好的参考。有关NLP中使用的残差网络的示例,请参见Huang和Wang(2017)。
在本章中,你学习了两个基本的前馈架构:多层感知机(MLP,也称“全连接”网络)和卷积神经网络(CNN)。我们了解了多层感知机在近似所有非线性函数方面的威力,也展示了它在NLP中根据姓氏对国籍进行分类的应用。我们知道了多层感知机的一个主要缺点/限制——缺乏参数共享——并介绍卷积网络架构作为一种可能的解决方案。卷积神经网络最初是为计算机视觉领域开发的网络,现已成为 NLP 的中流砥柱,主要是因为它们的高效实现和低内存需求。我们研究了卷积的不同变体——padding、dilated和strided——以及它们如何转换输入空间。本章还专门讨论了为卷积滤波器选择输入和输出大小的实际问题。我们通过扩展姓氏分类示例以使用convnet,展示了卷积运算如何帮助捕获语言中的子结构信息。最后,我们讨论了与卷积网络设计相关的一些主题:1)池化,2)批量规范化,3)1x1卷积,4)残差连接。在现代 CNN 设计中,经常能见到像Inception 架构(Szegedy等人,2015)那样同时使用许多这些技巧,其中谨慎地使用这些技巧可以实现数百层的卷积网络,这些网络不仅精确,而且训练速度很快。在第五章中,我们将探讨学习和使用离散单元表示的主题,比如单词、句子、文档和其他使用嵌入的特征类型。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。