注意
转到结尾下载完整的示例代码。
使用 PyTorch 进行深度学习#
创建日期:2017年4月8日 | 最后更新:2018年4月24日 | 最后验证:2024年11月5日
深度学习构件:仿射映射、非线性函数和目标函数#
深度学习通过巧妙地组合线性和非线性函数来构建。非线性函数的引入使得强大的模型成为可能。在本节中,我们将研究这些核心组件,构建一个目标函数,并观察模型如何被训练。
仿射映射#
深度学习的核心主力之一是仿射映射,它是一个函数 \(f(x)\),其中
对于矩阵 \(A\) 和向量 \(x, b\)。这里要学习的参数是 \(A\) 和 \(b\)。通常,\(b\) 被称为偏置项。
PyTorch 和大多数其他深度学习框架的做法与传统线性代数略有不同。它映射的是输入的行而不是列。也就是说,下面输出的第 \(i\) 行是输入的第 \(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 0x7fb3c97446b0>
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)\) 也只是一个非线性函数,但它的特殊之处在于它通常是网络中执行的最后一个操作。这是因为它接收一个实数向量并返回一个概率分布。其定义如下。设 \(x\) 是一个实数向量(可以是正数、负数或任何数,没有限制)。那么 \(\text{Softmax}(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 包中提供了许多算法,它们都是完全透明的。使用最简单的梯度更新与使用更复杂的算法是一样的。尝试不同的更新算法和更新算法的不同参数(比如不同的初始学习率)对于优化网络性能非常重要。通常,仅仅用像 Adam 或 RMSProp 这样的优化器替换普通的 SGD 就能显著提升性能。
在 PyTorch 中创建网络组件#
在我们转向关注 NLP 之前,让我们做一个带注释的例子,用 PyTorch 仅使用仿射映射和非线性函数来构建一个网络。我们还将看到如何计算损失函数,使用 PyTorch 内置的负对数似然,并通过反向传播更新参数。
所有网络组件都应继承自 nn.Module 并重写 forward() 方法。就样板代码而言,这差不多就是全部了。继承自 nn.Module 为你的组件提供了功能。例如,它使其能够跟踪其可训练的参数,你可以使用 .to(device)
方法在 CPU 和 GPU 之间切换它,其中 device 可以是 CPU 设备 torch.device("cpu")
或 CUDA 设备 torch.device("cuda:0")
。
让我们来写一个带注释的例子,构建一个网络,它接收一个稀疏的词袋表示,并输出两个标签“英语”和“西班牙语”的概率分布。这个模型只是逻辑回归。
示例:逻辑回归词袋分类器#
我们的模型将一个稀疏的词袋(BoW)表示映射到标签的对数概率。我们为词汇表中的每个词分配一个索引。例如,假设我们的整个词汇表只有两个词“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]])
上面的值中哪个对应英语的对数概率,哪个对应西班牙语的对数概率?我们从未定义过,但如果想训练模型,就必须定义它。
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>)
我们得到了正确的答案!你可以看到,对于测试数据,第一个例子中西班牙语的对数概率要高得多,而第二个例子中英语的对数概率要高得多,这正是我们所期望的。
现在你已经了解了如何创建一个 PyTorch 组件,将一些数据传入其中并进行梯度更新。我们准备好更深入地挖掘深度 NLP 所能提供的内容了。
脚本总运行时间: (0 分 0.257 秒)