注意
转到 文末 下载完整的示例代码。
PyTorch 深度学习#
创建于: 2017年4月8日 | 最后更新: 2018年4月24日 | 最后验证: 2024年11月5日
深度学习构建块:仿射映射、非线性函数和目标函数#
深度学习是通过巧妙地组合线性和非线性函数来实现的。引入非线性函数能够构建强大的模型。在本节中,我们将一起探索这些核心组件,构建一个目标函数,并了解模型如何进行训练。
仿射映射#
深度学习的核心工作之一是仿射映射,它是一个函数 \(f(x)\),其中:
其中 \(A\) 是一个矩阵,\(x, b\) 是向量。在此过程中需要学习的参数是 \(A\) 和 \(b\)。通常,\(b\) 被称为偏置项。
PyTorch 和大多数其他深度学习框架与传统线性代数的操作方式略有不同。它映射的是输入的行,而不是列。也就是说,下面输出的第 \(i\) 行是通过 \(A\) 映射输入的第一行,再加上偏置项得到的。请看下面的例子。
# Author: Robert Guthrie
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
torch.manual_seed(1)
<torch._C.Generator object at 0x7f671a36c9b0>
lin = nn.Linear(5, 3) # maps from R^5 to R^3, parameters A, b
# data is 2x5. A maps from 5 to 3... can we map "data" under A?
data = torch.randn(2, 5)
print(lin(data)) # yes
tensor([[ 0.1755, -0.3268, -0.5069],
[-0.6602, 0.2260, 0.1089]], grad_fn=<AddmmBackward0>)
非线性函数#
首先,请注意以下事实,它将解释我们为什么一开始就需要非线性函数。假设我们有两个仿射映射 \(f(x) = Ax + b\) 和 \(g(x) = Cx + d\)。那么 \(f(g(x))\) 是什么?
\(AC\) 是一个矩阵,\(Ad + b\) 是一个向量,因此我们看到仿射映射的组合仍然是一个仿射映射。
由此可见,如果我们希望神经网络是一系列仿射映射的组合,那么这并不会比单个仿射映射增加模型的任何能力。
如果我们在线性映射层之间引入非线性函数,情况就不同了,这样我们就可以构建更强大的模型。
存在几种核心的非线性函数。\(\tanh(x), \sigma(x), \text{ReLU}(x)\) 是最常见的。你可能会想:“为什么是这些函数?我能想到很多其他的非线性函数。” 之所以选择它们,是因为它们的梯度易于计算,而计算梯度对于学习至关重要。例如:
简要说明:虽然你在人工智能入门课程中可能学过一些神经网络,其中 \(\sigma(x)\) 是默认的非线性函数,但实际上人们通常会避免使用它。这是因为当自变量的绝对值增大时,其梯度会很快消失。梯度很小意味着学习会变得困难。大多数人默认使用 tanh 或 ReLU。
# In pytorch, most non-linearities are in torch.functional (we have it imported as F)
# Note that non-linearites typically don't have parameters like affine maps do.
# That is, they don't have weights that are updated during training.
data = torch.randn(2, 2)
print(data)
print(F.relu(data))
tensor([[-0.5404, -2.2102],
[ 2.1130, -0.0040]])
tensor([[0.0000, 0.0000],
[2.1130, 0.0000]])
Softmax 和概率#
\(\text{Softmax}(x)\) 函数也是一个非线性函数,但它很特别,通常是网络中的最后一个操作。这是因为它接收一个实数向量并返回一个概率分布。其定义如下。令 \(\mathbf{x}\) 为一个实数向量(可以是正数、负数,没有任何限制)。那么 \(\text{Softmax}(\mathbf{x})\) 的第 \(i\) 个分量是:
可以清楚地看出,输出是一个概率分布:每个元素都是非负的,并且所有分量的和为 1。
你也可以将其视为对输入应用了一个逐元素的指数运算,使所有值都变为非负,然后除以归一化常数。
# Softmax is also in torch.nn.functional
data = torch.randn(5)
print(data)
print(F.softmax(data, dim=0))
print(F.softmax(data, dim=0).sum()) # Sums to 1 because it is a distribution!
print(F.log_softmax(data, dim=0)) # theres also log_softmax
tensor([ 1.3800, -1.3505, 0.3455, 0.5046, 1.8213])
tensor([0.2948, 0.0192, 0.1048, 0.1228, 0.4584])
tensor(1.)
tensor([-1.2214, -3.9519, -2.2560, -2.0969, -0.7801])
目标函数#
目标函数是你的网络试图最小化的函数(在这种情况下,通常称为损失函数或成本函数)。这个过程首先选择一个训练实例,将其通过神经网络,然后计算输出的损失。模型参数然后通过计算损失函数的导数进行更新。直观地说,如果你的模型对其答案非常自信,并且其答案是错误的,那么你的损失会很高。如果它对其答案非常自信,并且其答案是正确的,那么损失会很低。
在训练集上最小化损失函数的想法是,你的网络希望能够很好地泛化,并在未见过的开发集、测试集或生产环境中的样本上获得较低的损失。一个示例损失函数是负对数似然损失,这是多类别分类中一个非常常见的目标。对于有监督的多类别分类,这意味着训练网络以最小化正确输出的负对数概率(或者等效地,最大化正确输出的对数概率)。
优化和训练#
那么,如果我们能够计算一个实例的损失函数,我们该如何处理呢?我们之前看到张量(Tensors)知道如何计算相对于用于计算它们的元素的梯度。既然我们的损失是一个张量,我们就可以计算相对于用于计算它的所有参数的梯度!然后我们可以执行标准的梯度更新。设 \(\theta\) 为我们的参数,\(L(\theta)\) 为损失函数,\(\eta\) 为正的学习率。那么:
存在大量算法以及活跃的研究试图做一些比这个简单的梯度更新更高级的事情。许多算法试图根据训练时发生的情况来调整学习率。除非你非常感兴趣,否则你不需要担心这些算法具体在做什么。Torch 在 `torch.optim` 包中提供了许多这样的算法,并且它们都是完全透明的。使用最简单的梯度更新与使用更复杂的算法效果相同。尝试不同的更新算法和更新算法的不同参数(例如不同的初始学习率)对于优化网络的性能很重要。通常,仅将标准的 SGD 替换为 Adam 或 RMSProp 等优化器就能显著提升性能。
在 PyTorch 中创建网络组件#
在我们深入 NLP 之前,让我们通过一个带注释的示例来了解如何在 PyTorch 中仅使用仿射映射和非线性函数来构建网络。我们还将看到如何使用 PyTorch 内置的负对数似然来计算损失函数,并通过反向传播来更新参数。
所有网络组件都应继承自 `nn.Module` 并重写 `forward()` 方法。除此之外,就是一些样板代码。继承自 `nn.Module` 会为你的组件提供功能。例如,它会跟踪其可训练的参数,你可以使用 `.to(device)` 方法在 CPU 和 GPU 之间切换它,其中 `device` 可以是 CPU 设备 `torch.device("cpu")` 或 CUDA 设备 `torch.device("cuda:0")`。
下面我们来编写一个示例网络,该网络接收一个稀疏词袋(bag-of-words)表示,并输出两个标签:“英语”和“西班牙语”的概率分布。这个模型本质上就是逻辑回归。
示例:逻辑回归词袋模型分类器#
我们的模型将稀疏的词袋表示映射到标签的对数概率。我们为词汇表中的每个单词分配一个索引。例如,假设我们的整个词汇表包含两个单词“hello”和“world”,它们的索引分别为 0 和 1。句子“hello hello hello hello”的词袋向量是:
对于“hello world world hello”,它是:
等等。一般来说,它是:
设这个词袋向量为 \(x\)。我们网络的输出是:
也就是说,我们将输入通过一个仿射映射,然后进行对数 Softmax 操作。
data = [("me gusta comer en la cafeteria".split(), "SPANISH"),
("Give it to me".split(), "ENGLISH"),
("No creo que sea una buena idea".split(), "SPANISH"),
("No it is not a good idea to get lost at sea".split(), "ENGLISH")]
test_data = [("Yo creo que si".split(), "SPANISH"),
("it is lost on me".split(), "ENGLISH")]
# word_to_ix maps each word in the vocab to a unique integer, which will be its
# index into the Bag of words vector
word_to_ix = {}
for sent, _ in data + test_data:
for word in sent:
if word not in word_to_ix:
word_to_ix[word] = len(word_to_ix)
print(word_to_ix)
VOCAB_SIZE = len(word_to_ix)
NUM_LABELS = 2
class BoWClassifier(nn.Module): # inheriting from nn.Module!
def __init__(self, num_labels, vocab_size):
# calls the init function of nn.Module. Dont get confused by syntax,
# just always do it in an nn.Module
super(BoWClassifier, self).__init__()
# Define the parameters that you will need. In this case, we need A and b,
# the parameters of the affine mapping.
# Torch defines nn.Linear(), which provides the affine map.
# Make sure you understand why the input dimension is vocab_size
# and the output is num_labels!
self.linear = nn.Linear(vocab_size, num_labels)
# NOTE! The non-linearity log softmax does not have parameters! So we don't need
# to worry about that here
def forward(self, bow_vec):
# Pass the input through the linear layer,
# then pass that through log_softmax.
# Many non-linearities and other functions are in torch.nn.functional
return F.log_softmax(self.linear(bow_vec), dim=1)
def make_bow_vector(sentence, word_to_ix):
vec = torch.zeros(len(word_to_ix))
for word in sentence:
vec[word_to_ix[word]] += 1
return vec.view(1, -1)
def make_target(label, label_to_ix):
return torch.LongTensor([label_to_ix[label]])
model = BoWClassifier(NUM_LABELS, VOCAB_SIZE)
# the model knows its parameters. The first output below is A, the second is b.
# Whenever you assign a component to a class variable in the __init__ function
# of a module, which was done with the line
# self.linear = nn.Linear(...)
# Then through some Python magic from the PyTorch devs, your module
# (in this case, BoWClassifier) will store knowledge of the nn.Linear's parameters
for param in model.parameters():
print(param)
# To run the model, pass in a BoW vector
# Here we don't need to train, so the code is wrapped in torch.no_grad()
with torch.no_grad():
sample = data[0]
bow_vector = make_bow_vector(sample[0], word_to_ix)
log_probs = model(bow_vector)
print(log_probs)
{'me': 0, 'gusta': 1, 'comer': 2, 'en': 3, 'la': 4, 'cafeteria': 5, 'Give': 6, 'it': 7, 'to': 8, 'No': 9, 'creo': 10, 'que': 11, 'sea': 12, 'una': 13, 'buena': 14, 'idea': 15, 'is': 16, 'not': 17, 'a': 18, 'good': 19, 'get': 20, 'lost': 21, 'at': 22, 'Yo': 23, 'si': 24, 'on': 25}
Parameter containing:
tensor([[ 0.1194, 0.0609, -0.1268, 0.1274, 0.1191, 0.1739, -0.1099, -0.0323,
-0.0038, 0.0286, -0.1488, -0.1392, 0.1067, -0.0460, 0.0958, 0.0112,
0.0644, 0.0431, 0.0713, 0.0972, -0.1816, 0.0987, -0.1379, -0.1480,
0.0119, -0.0334],
[ 0.1152, -0.1136, -0.1743, 0.1427, -0.0291, 0.1103, 0.0630, -0.1471,
0.0394, 0.0471, -0.1313, -0.0931, 0.0669, 0.0351, -0.0834, -0.0594,
0.1796, -0.0363, 0.1106, 0.0849, -0.1268, -0.1668, 0.1882, 0.0102,
0.1344, 0.0406]], requires_grad=True)
Parameter containing:
tensor([0.0631, 0.1465], requires_grad=True)
tensor([[-0.5378, -0.8771]])
上面计算出的值中,哪个对应于 ENGLISH 的对数概率,哪个对应于 SPANISH 的对数概率?我们还没有定义它,但如果我们想训练这个模型,我们就必须定义它。
label_to_ix = {"SPANISH": 0, "ENGLISH": 1}
那么,我们开始训练吧!为此,我们输入实例以获得对数概率,计算损失函数,计算损失函数的梯度,然后通过梯度步长更新参数。损失函数由 Torch 在 `nn` 包中提供。`nn.NLLLoss()` 就是我们想要的负对数似然损失。它还在 `torch.optim` 中定义了优化函数。这里,我们只使用 SGD。
请注意,`NLLLoss` 的输入是对数概率向量和一个目标标签。它不会为我们计算对数概率。这就是为什么我们的网络最后一层是 log softmax。损失函数 `nn.CrossEntropyLoss()` 与 `NLLLoss()` 相同,只是它会自动为你完成 log softmax 操作。
# Run on test data before we train, just to see a before-and-after
with torch.no_grad():
for instance, label in test_data:
bow_vec = make_bow_vector(instance, word_to_ix)
log_probs = model(bow_vec)
print(log_probs)
# Print the matrix column corresponding to "creo"
print(next(model.parameters())[:, word_to_ix["creo"]])
loss_function = nn.NLLLoss()
optimizer = optim.SGD(model.parameters(), lr=0.1)
# Usually you want to pass over the training data several times.
# 100 is much bigger than on a real data set, but real datasets have more than
# two instances. Usually, somewhere between 5 and 30 epochs is reasonable.
for epoch in range(100):
for instance, label in data:
# Step 1. Remember that PyTorch accumulates gradients.
# We need to clear them out before each instance
model.zero_grad()
# Step 2. Make our BOW vector and also we must wrap the target in a
# Tensor as an integer. For example, if the target is SPANISH, then
# we wrap the integer 0. The loss function then knows that the 0th
# element of the log probabilities is the log probability
# corresponding to SPANISH
bow_vec = make_bow_vector(instance, word_to_ix)
target = make_target(label, label_to_ix)
# Step 3. Run our forward pass.
log_probs = model(bow_vec)
# Step 4. Compute the loss, gradients, and update the parameters by
# calling optimizer.step()
loss = loss_function(log_probs, target)
loss.backward()
optimizer.step()
with torch.no_grad():
for instance, label in test_data:
bow_vec = make_bow_vector(instance, word_to_ix)
log_probs = model(bow_vec)
print(log_probs)
# Index corresponding to Spanish goes up, English goes down!
print(next(model.parameters())[:, word_to_ix["creo"]])
tensor([[-0.9297, -0.5020]])
tensor([[-0.6388, -0.7506]])
tensor([-0.1488, -0.1313], grad_fn=<SelectBackward0>)
tensor([[-0.2093, -1.6669]])
tensor([[-2.5330, -0.0828]])
tensor([ 0.2803, -0.5605], grad_fn=<SelectBackward0>)
我们得到了正确答案!你可以看到,在第一个示例中,Spanish 的对数概率要高得多;在第二个示例中,对于测试数据,English 的对数概率要高得多,正如预期的那样。
现在你已经了解了如何在 PyTorch 中创建一个组件,如何通过它传递数据并进行梯度更新。我们已经准备好深入探索深度 NLP 的更多内容了。
脚本总运行时间: (0 分钟 0.273 秒)