在第3章中,我们通过查看Perceptron(最简单的神经网络)来介绍神经网络的基础。感知器的最大的问题是它无法学习数据中存在的适度非线性模式。例如,看一下图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
图层和非线性的组合将最终隐藏矢量映射到输出矢量。
MLP的力量来自添加第二Linear层并允许模型学习可线性分离的中间表示 - 表示的属性,其中单个直线(或更一般地,超平面)可用于区分数据点它们落在哪一侧(或超平面)。学习具有特定属性的中间表示,如对于分类任务可线性分离,是使用神经网络的最深刻的后果之一,并且是其建模能力的典型。在下一节中,我们将更加深入和深入地了解这意味着什么。
1.1 一个简单的例子
让我们看看前面描述的XOR示例,看看Perceptron与MLP会发生什么。在这个例子中,我们在二进制分类任务中训练Perceptron和MLP:星形和圆形。每个数据点都是2D坐标。如果不深入了解实现细节,最终的模型预测如图4-3所示。在该图中,错误分类的数据点用黑色填充,而正确分类的数据点未填充。在左侧面板中,如填充的形状所证明的,感知器难以学习可以分离的决策边界。星星和圆圈。然而,MLP(右图)学习了一个决策边界,可以更准确地对星星和圆圈进行分类。
虽然在图中看来MLP有两个决策边界,这就是它的优势,但它实际上只是一个决策边界!决策边界就是这样出现的,因为中间表示已经变形了空间以允许一个超平面出现在这两个位置中。在图4-4中,我们可以看到MLP计算的中间值。点的形状表示类(星形或圆形)。我们看到的是神经网络(在这种情况下是一个MLP)已经学会“扭曲”数据所在的空间,以便它可以在通过最后一层时将它们分成一条线。
相反,如图4-没有额外的层,可以按下数据的形状,直到它变为线性可分。
1.2 在PyTorch中实现
在上一节中,我们概述了MLP的核心思想。在本节中,我们将介绍PyTorch中的实现。如上所述,MLP除了第3章中更简单的Perceptron之外还有一层额外的计算。在我们在例4-1中给出的实现中,我们用两个PyTorch Linear
模块实例化了这个想法。这些Linear
对象被命名fc1
并fc2
遵循一个共同的约定,即将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
要使用它的字符姓分类,我们使用Vocabulary
,Vectorizer
以及DataLoader
对姓字符串转换成矢量minibatches。这些是示例:分类餐厅评论的情感”中使用的相同数据结构,例证了以与Yelp评论的单词标记相同的方式处理姓氏的字符标记的多态性。不是通过将单词标记映射到整数来进行向量化,而是通过将字符映射到整数来对数据进行向量化。
2.2.1 词汇
在Vocabulary
示例中使用的类和Vocabulary
完全一样中的示例:餐厅点评的判断情”,在Yelp的评论词语映射到它们相应的整数。作为简要概述,它Vocabulary
是两个Python字典的协调,它们在令牌(本例中为字符)和整数之间形成双射; 也就是说,第一个字典将字符映射到整数索引,第二个字典将整数索引映射到字符。add_token
方法用于向其中添加新标记Vocabulary
,lookup_token
方法用于检索索引,lookup_index
方法用于检索给定索引的标记。与Yelp评论Vocabulary
相反,我们使用独热表示,但不计算字符的频率,这主要是因为数据集很小并且大多数字符都足够频繁。
2.2.2 SURNAMEVECTORIZER
Vocabulary
把单独的字符转换成整数,则SurnameVectorizer
是负责用Vocabulary
转换加载到内存中。在实例化和使用过程都非常相似,ReviewVectorizer
在“实例:判断餐厅点评的情绪”,有一个区别:该字符串不是空格分开。姓氏是字符序列,每个字符都是我们的标记Vocabulary
。卷积神经网络中将忽略序列信息,并通过迭代字符串输入中的每个字符来创建输入的独热矢量表示。我们为以前没有遇到的字符指定一个特殊标记UNK。UNK符号仍然在角色中使用,Vocabulary
因为我们Vocabulary
仅从训练数据中实例化,并且验证或测试数据中可能存在唯一字符。
您应该注意,尽管我们在本示例中使用了独热编码,在后面的章节中使用其他矢量化方法,这些方法可以替代独热编码,有时甚至比独热编码更好。具体来说,在“示例:使用CNN对姓氏进行分类”中,您将使用一个独热矩阵,其中每个字符都是矩阵中的一个位置,并且具有自己的独热编码。然后,在第5章中,您将了解Embedding
图层,返回整数向量的向量化,以及如何使用这些向量来创建密集向量矩阵。但现在,让我们来看看代码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 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
)
训练中最显着的差异与模型中的输出类型和使用的损失函数有关。在此示例中,输出是可以转换为概率的多类预测向量。如模型描述中所述,此输出的损失类型仅限于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)
2.4.1 训练循环
与“示例:餐厅评论的情感分类”中的训练循环相比,此示例的训练循环几乎相同,但变量名称除外。具体来说,例4-10显示了使用不同的参数来获取数据batch_dict
。除了格式上的差异,训练循环的功能保持不变。使用训练数据,计算模型输出,损失和梯度。然后,使用渐变来更新模型。
# the training routine is these 5 steps:
# --------------------------------------
# step 1. 清零梯度
optimizer.zero_grad()
# step 2. 计算前向输出
y_pred = classifier(batch_dict['x_surname'])
# step 3. 计算损失
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. 用损失计算梯度
loss.backward()
# step 5. 用梯度更新步进
optimizer.step()
2.5. 模型评估与预测
2.5.1 模型评估与预测
要了解模型的性能,您应该使用定量和定性方法分析模型。定量地,测量保持的测试数据上的误差确定分类器是否可以应用到看不见的示例。定性地,您可以通过查看分类器的top-k预测来为新示例开发直观的模型所学习的内容。
2.5.1 评估测试数据集
为了评估SurnameClassifier
测试数据,我们执行一个程序,这个程序与“示例:餐厅评论的情感分类”中的餐厅评论文本分类示例相同,我们设置数据集以迭代测试数据,调用classifier.eval()
方法并迭代测试数据与我们对其他数据的处理方式相同。在此示例中,classifier.eval()
当使用测试/评估数据时,调用会暂停PyTorch更新模型参数。该模型在测试数据上实现了约50%的准确度。如果您在随附的笔记本中运行训练程序,您会注意到训练数据的性能更高。这是因为模型总是更适合它所训练的数据,因此训练数据的性能并不表示新数据的性能。如果调试代码,我们建议您尝试不同大小的隐藏维度。您应该注意到性能的提高。然而,增加幅度不大。主要原因是独热矢量化方法是弱表示。虽然它确实将每个姓氏紧凑地表示为单个向量,但它会丢弃字符之间的关联信息,这些都是识别中很重要的信息。
2.5.2 对新姓进行分类
例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}
2.5.3 检索新姓氏的top-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的结构正则化方法变得非常重要。
2.6.1 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
)中相乘的位置。还有一些超参数可以控制输入数据张量用padding
填充多少,以及当应用于输入数据张量时乘法应该相隔多远(称为dilation
)。在以下小节中,我们将更详细地描述这些超参数的含义。
3.1.1 卷积运算的维数
要理解的第一个概念是卷积运算的维数。我们使用二维卷积进行说明,但是根据数据的性质,存在更适合的其他维度的卷积。在PyTorch,卷积可以是一维的,二维的,或三维和由被实现Conv1d
,Conv2d
和Conv3d
模块。一维卷积对于时间序列是有用的,其中每个时间步长具有特征向量。在这种情况下,我们可以学习序列维度上的模式。NLP中的大多数卷积运算都是一维卷积。另一方面,二维卷积试图沿数据中的两个方向捕获时空模式; 例如,在沿高度和宽度尺寸的图像中 - 这是为什么二维卷积在图像处理中很受欢迎。类似地,在三维卷积中,沿着数据中的三维捕获图案。例如,在视频数据中,信息位于三维中 - 表示图像帧的两个维度,以及表示帧序列的时间维度。就本书而言,我们使用Conv1d
为主。
3.1.2 通道
通道是指沿输入中每个点的特征尺寸。例如,在图像中,对应于RGB分量的图像中的每个像素有三个通道。当使用卷积时,类似的概念可以被转移到文本数据。从概念上讲,如果文本文档中的“像素”是单词,则通道的数量是词汇表的大小。如果我们更细粒度并考虑对字符进行卷积,则通道数是字符集的大小(在这种情况下恰好是词汇表)。在PyTorch卷积实现中,输入中的通道数是in_channels
参数。卷积运算可以在输出中产生多个通道(out_channels
)。您可以将此视为卷积运算符将输入要素维度“映射”到输出要素维度。图4-7和图4-8说明了这个概念。
3.1.3 内核大小
内核矩阵的宽度称为内核大小(kernel_size
在PyTorch中)。在图4-6中,内核大小为2,相比之下,我们在图4-9中]示了一个大小为3的内核。您应该开发的经验是,卷积在输入中组合空间(或时间)本地信息,并且每个卷积的本地信息量由内核大小控制。但是,通过增加内核的大小,您还可以减小输出的大小(Dumoulin和Visin,2016)。这就是为什么输出矩阵是2x2
在图4-9当内核大小是3,但3x3
在基质图4-6当内核大小为2。
3.1.4 Stride
Stride控制卷积之间的步长。如果步幅与内核的大小相同,则内核计算不会重叠。另一方面,如果步幅是1,则内核最大程度地重叠。
3.1.5 填充
即使stride
并且kernel_size
允许控制每个计算的特征值具有多少范围,同时缩小特征图的总大小(卷积的输出)的副作用。为了抵消这种情况,在每个相应的维度上附加和预先添加0
来人为地使输入数据张量的长度(如果是1d,2d或3d),高度(如果是2d或3d)和深度(如果是3d)更大。这因此意味着CNN将执行更多的卷积,但是可以控制输出形状而不损害期望的内核尺寸,步幅或扩张。
3.1.6 扩张
扩张控制卷积内核如何应用于输入矩阵。我们表明,将扩展从1(默认值)增加到2意味着当应用于输入矩阵时,内核的元素彼此相距两个空格。考虑这个问题的另一种方法是跨越内核本身 - 内核中的元素或内核应用程序之间存在一个步长,其中包含“漏洞”。这对于汇总输入空间的较大区域而不增加参数数量。当堆叠卷积层时,膨胀卷积已证明非常有用。连续扩张的卷积以指数方式增加“感受野”的大小; 也就是说,在进行预测之前网络所看到的输入空间的大小。