Deep Learning for NLP with PyTorch

本文翻译自官方教程Deep Learning for NLP with Pytorch

Introduction to PyTorch

Introduction to Torch’s tensor library

所有深度学习都是张量上的计算,这是对可以在2维以上进行索引的矩阵的推广。 我们将在后面深入地看到这究竟意味着什么。 首先,让我们看看我们可以用张量做什么。

1
2
3
4
5
6
7
import torch
import torch.autograd as autograd
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

torch.manual_seed(1)

Creating Tensors

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# torch.tensor(data) creates a torch.Tensor object with the given data.
V_data = [1., 2., 3.]
V = torch.tensor(V_data)
print(V) # tensor([ 1., 2., 3.])

# Creates a matrix
M_data = [[1., 2., 3.], [4., 5., 6]]
M = torch.tensor(M_data)
print(M)

"""
tensor([[ 1., 2., 3.],
[ 4., 5., 6.]])
"""

# Create a 3D tensor of size 2x2x2.
T_data = [[[1., 2.], [3., 4.]],
[[5., 6.], [7., 8.]]]
T = torch.tensor(T_data)
print(T)

"""
tensor([[[ 1., 2.],
[ 3., 4.]],

[[ 5., 6.],
[ 7., 8.]]])
"""

什么是3D张量呢? 像这样想想, 如果你有一个向量,索引到向量中给你一个标量;如果你有一个矩阵,索引到矩阵给你一个向量;如果你有一个3D张量,那么索引到张量就会给你一个矩阵!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Index into V and get a scalar (0 dimensional tensor)
print(V[0]) # tensor(1.)
# Get a Python number from it
print(V[0].item()) # 1.0

# Index into M and get a vector
print(M[0]) # tensor([ 1., 2., 3.])

# Index into T and get a matrix
print(T[0])

"""
tensor([[ 1., 2.],
[ 3., 4.]])
"""

你也可以创建其他数据类型的张量。 正如你所看到的,默认是Float。 要创建整数类型的张量,请尝试使用torch.LongTensor()。 查看文档以获取更多数据类型,但Float和Long将是最常见的。

您可以使用随机数据创建张量,并使用torch.randn() 提供的维度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
x = torch.randn((3, 4, 5))
print(x)

"""
tensor([[[-1.5256, -0.7502, -0.6540, -1.6095, -0.1002],
[-0.6092, -0.9798, -1.6091, -0.7121, 0.3037],
[-0.7773, -0.2515, -0.2223, 1.6871, 0.2284],
[ 0.4676, -0.6970, -1.1608, 0.6995, 0.1991]],

[[ 0.8657, 0.2444, -0.6629, 0.8073, 1.1017],
[-0.1759, -2.2456, -1.4465, 0.0612, -0.6177],
[-0.7981, -0.1316, 1.8793, -0.0721, 0.1578],
[-0.7735, 0.1991, 0.0457, 0.1530, -0.4757]],

[[-0.1110, 0.2927, -0.1578, -0.0288, 0.4533],
[ 1.1422, 0.2486, -1.7754, -0.0255, -1.0233],
[-0.5962, -1.0055, 0.4285, 1.4761, -1.7869],
[ 1.6103, -0.7040, -0.1853, -0.9962, -0.8313]]])
"""

Operations with Tensors

你可以按照你期望的方式对张量进行操作。

1
2
3
4
x = torch.tensor([1., 2., 3.])
y = torch.tensor([4., 5., 6.])
z = x + y
print(z) # tensor([ 5., 7., 9.])

请参阅文档以获取大量可用操作的完整列表。不止是数学运算。

稍后我们将使用的一个有用的操作是连接操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# By default, it concatenates along the first axis (concatenates rows)
x_1 = torch.randn(2, 5)
y_1 = torch.randn(3, 5)
z_1 = torch.cat([x_1, y_1])
print(z_1)

"""
tensor([[-0.8029, 0.2366, 0.2857, 0.6898, -0.6331],
[ 0.8795, -0.6842, 0.4533, 0.2912, -0.8317],
[-0.5525, 0.6355, -0.3968, -0.6571, -1.6428],
[ 0.9803, -0.0421, -0.8206, 0.3133, -1.1352],
[ 0.3773, -0.2824, -2.5667, -1.4303, 0.5009]])
"""

# Concatenate columns:
x_2 = torch.randn(2, 3)
y_2 = torch.randn(2, 5)
# second arg specifies which axis to concat along
z_2 = torch.cat([x_2, y_2], 1)
print(z_2)

"""
tensor([[ 0.5438, -0.4057, 1.1341, -0.1473, 0.6272, 1.0935, 0.0939,
1.2381],
[-1.1115, 0.3501, -0.7703, -1.3459, 0.5119, -0.6933, -0.1668,
-0.9999]])
"""

Reshaping Tensors

使用.view()方法reshape张量。这种方法使用广泛,因为许多神经网络组件希望它们的输入具有一定的形状。在将数据传递给组件之前,通常需要对张量进行reshape操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
x = torch.randn(2, 3, 4)
print(x)

"""
tensor([[[ 2.6415, -0.9624, -0.2076, -1.3889],
[ 0.0127, -1.8734, 1.7997, 0.2824],
[-1.2560, 0.8956, 0.1675, 0.7514]],

[[ 2.4142, 1.0206, -0.4405, -1.7342],
[-1.2362, 1.5786, -1.1161, 0.7678],
[-0.5882, 2.1189, -0.5422, -2.4593]]])
"""

print(x.view(2, 12)) # Reshape to 2 rows, 12 columns

"""
tensor([[ 2.6415, -0.9624, -0.2076, -1.3889, 0.0127, -1.8734, 1.7997,
0.2824, -1.2560, 0.8956, 0.1675, 0.7514],
[ 2.4142, 1.0206, -0.4405, -1.7342, -1.2362, 1.5786, -1.1161,
0.7678, -0.5882, 2.1189, -0.5422, -2.4593]])
"""

# Same as above. If one of the dimensions is -1, its size can be inferred
print(x.view(2, -1))

"""
tensor([[ 2.6415, -0.9624, -0.2076, -1.3889, 0.0127, -1.8734, 1.7997,
0.2824, -1.2560, 0.8956, 0.1675, 0.7514],
[ 2.4142, 1.0206, -0.4405, -1.7342, -1.2362, 1.5786, -1.1161,
0.7678, -0.5882, 2.1189, -0.5422, -2.4593]])
"""

Computation Graphs and Automatic Differentiation

计算图的概念对于高效的深度学习编程非常重要,因为它可以让您不必自己编写反向传播梯度算法。 计算图只是一个规范,说明如何将数据组合起来为您提供输出。 由于该图形完全指定了哪些参数与哪些操作有关,因此它包含足够的信息来计算导数。这可能听起来很模糊,所以我们来看看使用基本标志 require_grad 会发生什么。

首先,从程序员的角度思考。 存储在我们上面创建的torch.Tensor对象中的东西是什么。显然是数据和它的形状,也许还有其他一些东西。但是当我们将两个张量加在一起时,我们得到了一个输出张量。 这个输出张量知道它的数据和形状。但它不知道它是其他两个张量的总和(它可以从文件中读入,也可能是其他操作的结果等)。

如果 requires_grad = True,则Tensor对象会跟踪它是如何创建的。让我们具体来看看。

1
2
3
4
5
6
7
8
9
10
11
# Tensor factory methods have a ``requires_grad`` flag
x = torch.tensor([1., 2., 3], requires_grad=True)

# With requires_grad=True, you can still do all the operations you previously
# could
y = torch.tensor([4., 5., 6], requires_grad=True)
z = x + y
print(z) # tensor([ 5., 7., 9.])

# BUT z knows something extra.
print(z.grad_fn) # <AddBackward1 object at 0x0000027426E9C978>

所以张量知道是什么创造了它们。z知道它不是从文件中读入的,它也不是乘法或指数等操作的结果。如果你继续关注z.grad_fn,你会得到x和y的位置。

但是,这如何帮助我们计算梯度呢?

1
2
3
4
5
6
7
8
9
# Lets sum up all the entries in z
s = z.sum()
print(s)
print(s.grad_fn)

"""
tensor(21.)
<SumBackward0 object at 0x000002742471DDA0>
"""

那么现在,这个和的相对于x的第一个分量的导数是多少? 在数学中,我们想要:

那么,s知道它是作为张量z的和创建的。z知道它是x + y的总和。所以:

因此s包含足够的信息来确定我们想要的导数是1!

当然,这掩盖了如何实际计算该导数的挑战。这里要指出的是,有足够的信息可以计算出来。实际上,Pytorch的开发人员编程 sum() 和 + 操作来知道如何计算其梯度,并运行反向传播算法。 对该算法的深入讨论超出了本教程的范围。

让Pytorch计算梯度,来验证我们的观点是对的:(注意,如果多次运行该块,梯度将增加,这是因为Pytorch将梯度累积到.grad属性中,因为对于许多模型而言,这是非常方便的。)

1
2
3
# calling .backward() on any variable will run backprop, starting from it.
s.backward()
print(x.grad) # tensor([ 1., 1., 1.])

了解在下面的块中发生的事情对于成为深度学习中的成功程序员来说至关重要。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
x = torch.randn(2, 2)
y = torch.randn(2, 2)
# By default, user created Tensors have ``requires_grad=False``
print(x.requires_grad, y.requires_grad) # False False
z = x + y
# So you can't backprop through z
print(z.grad_fn) # None

# ``.requires_grad_( ... )`` changes an existing Tensor's ``requires_grad``
# flag in-place. The input flag defaults to ``True`` if not given.
x = x.requires_grad_()
y = y.requires_grad_()
# z contains enough information to compute gradients, as we saw above
z = x + y
print(z.grad_fn) # <AddBackward1 object at 0x0000027426E9C4E0>
# If any input to an operation has ``requires_grad=True``, so will the output
print(z.requires_grad) # True

# Now z has the computation history that relates itself to x and y
# Can we just take its values, and **detach** it from its history?
new_z = z.detach()

# ... does new_z have information to backprop to x and y?
# NO!
print(new_z.grad_fn) # None
# And how could it? ``z.detach()`` returns a tensor that shares the same storage
# as ``z``, but with the computation history forgotten. It doesn't know anything
# about how it was computed.
# In essence, we have broken the Tensor away from its past history

您还可以通过使用torch.no_grad()包装代码块来停止autograd跟踪在require_grad = True 的张量上的历史记录:

1
2
3
4
5
print(x.requires_grad) # True
print((x ** 2).requires_grad) # True

with torch.no_grad():
print((x ** 2).requires_grad) # False

Deep Learning with PyTorch

Deep Learning Building Blocks: Affine maps, non-linearities and objectives

深度学习以巧妙的方式将非线性映射和线性映射组合起来。非线性的引入可以造就强大的模型。 在本节中,我们将使用这些核心组件,构建一个目标函数,并且看看模型是如何训练的。

Afine Maps

深度学习的核心工作之一是仿射图,这是一个f(X)函数, 其中:

对于矩阵A和向量x,b。这里学习的参数是A和b。 通常,b被称为偏差项。

PyTorch和大多数其他深度学习框架与传统的线性代数有些许不同。它映射输入的行而不是列。也就是说,下面的输出的第i行是输入的第i行乘上A加上偏置项的映射。看下面的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

torch.manual_seed(1)

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]])
"""

Non-Linearities

首先,注意以下事实,这将解释为什么我们首先需要非线性映射。假设我们有两个仿射图 $f(x) = Ax + b$ 和 $g(x) = Cx + d$。那么 $f(g(x))$ 会是怎样呢?

$AC$ 是一个矩阵,$Ad + b$ 是一个向量,因此我们可以看到,仿射变换的组合还将是一个仿射变换。

由此你可以看到,如果你想让你的神经网络只是一连串仿射函数的组合,那么这不会为你的模型添加新的力量,而只是做了一个仿射变换。

如果我们在仿射层之间引入非线性函数,则不再是这种情况,我们可以构建更强大的模型。

有几个核心的非线性函数。$tanh(x)$,$σ(x)$,$ReLU(x)$是最常见的。你可能想知道:“为什么是这些函数? 我可以考虑其他许多非线性函数呀。” 其原因在于它们具有易于计算的梯度,并且计算梯度对于学习至关重要。例如:

一个简单的提示:虽然你可能已经在AI的介绍中学习到了一些神经网络,其中$σ(x)$是默认的非线性函数,通常人们会在实践中回避它。 这是因为随着参数绝对值的增长梯度消失得非常快。 小梯度意味着很难学习。大多数人默认使用$tanh$或$ReLU$。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 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)

"""
tensor([[-0.5404, -2.2102],
[ 2.1130, -0.0040]])
"""
print(F.relu(data))

"""
tensor([[ 0.0000, 0.0000],
[ 2.1130, 0.0000]])
"""

Softmax and Probabilities

函数$Softmax(x)$也是一种非线性映射,但它的特殊之处在于它通常是网络最后一步完成的操作。 这是因为它接受了一个实数向量并返回一个概率分布。其定义如下, 令x是实数向量(正数,负数,whatever,没有约束),那么$Softmax(x)$的第i个元素是:

应该清楚的是,输出是一个概率分布:每个元素都是非负的,并且所有元素的总和是1。

你也可以把它看作只是对输入做了一个element-wise的指数操作(使得所有内容非负),然后除以归一化常数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Softmax is also in torch.nn.functional
data = torch.randn(5)
print(data)

"""
tensor([ 1.3800, -1.3505, 0.3455, 0.5046, 1.8213])
"""
print(F.softmax(data, dim=0))

"""
tensor([ 0.2948, 0.0192, 0.1048, 0.1228, 0.4584])
"""
print(F.softmax(data, dim=0).sum()) # Sums to 1 because it is a distribution!

"""
tensor(1.)
"""
print(F.log_softmax(data, dim=0)) # theres also log_softmax

"""
tensor([-1.2214, -3.9519, -2.2560, -2.0969, -0.7801])
"""

Objective Functions

目标函数是网络训练时需要最小化的函数(在这种情况下,它通常称为损失函数或成本函数)。 首先选择一个训练实例,通过神经网络运行它,然后计算输出的损失。然后通过采用损失函数的导数来更新模型的参数。直观地说,如果你的模型对答案完全有信心,而且答案是错误的,你的损失就会很高;如果它的答案非常有信心,而且答案是正确的,那么损失就会很低。

将训练样例的损失函数最小化的想法是,你的网络能够很好地泛化,并且在开发集,测试集或产品上有小的损失。 例如损失函数是负对数似然损失,这是多类分类的一个非常普遍的目标函数。对于有监督的多类别分类,这意味着训练网络以最小化正确输出的负对数概率(或等同地,最大化正确输出的对数概率)。

Optimization and Training

那么我们可以计算一个实例的损失函数吗?我们该怎么做? 我们之前看到,张量知道如何计算用来计算它的变量的梯度。 那么,因为我们的损失是一个张量,所以我们可以根据用于计算它的所有参数计算梯度! 然后我们可以执行标准梯度更新。设$θ$是我们的参数,$L(θ)$是损失函数,$η$是一个正的学习率。那么:

有很多算法和积极活跃的研究,试图做更多的而不仅仅是这个普通的(没有新意的)梯度更新。 许多人试图根据训练时发生的情况改变学习率。除非你真的感兴趣,否则你不必担心这些算法具体做什么。Torch在torch.optim包中提供了许多工具,它们都是完全透明的。使用最简单的梯度更新与更复杂的算法相同。尝试不同的更新算法和更新算法的不同参数(如不同的初始学习率)对于优化网络性能非常重要。通常,只需用Adam或RMSProp等优化器替换普通的SGD即可显着提升性能。

Creating Network Components in PyTorch

在我们开始关注NLP之前,让我们做一个注释示例,在PyTorch中只使用仿射图和非线性映射构建网络。 我们还将看到如何使用PyTorch内置的负对数似然函数来计算损失函数,并通过反向传播更新参数。

所有网络组件都应该从nn.Module继承并重写forward()方法。这就是一个样板。 继承nn.Module为你的网络提供了很多功能。例如,它可以跟踪其可训练的参数,你可以使用.to(device)方法在CPU和GPU之间转换,其中设备可以是CPU设备torch.device(“cpu”)或CUDA设备torch.device(“cuda:0”)

我们来编写一个带有注释的网络示例,该网络采用稀疏的词袋表示法,并在两个标签上输出概率分布:“英语”和“西班牙语”。这个模型只是一个简单的逻辑回归。

Example: Logistic Regression Bag-of-Words classifier

我们的模型会将稀疏的BoW表示映射到标签上的对数概率。 我们为词汇表中的每个单词分配一个索引。 例如,假设我们的所有词汇只有两个词“hello”和“world”,索引分别为0和1。“hello hello hello hello”这个句子的BoW向量是:

对于句子“hello world world hello”,BoW向量是:

等等。总的来说,一个句子的BoW向量为:

将这个BoW向量表示为$x$。我们的网络输出是:

也就是说,我们通过仿射变换传递输入数据,然后做一个log softmax映射。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
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)

"""
{'me': 0, 'on': 25, 'la': 4, 'is': 16, 'una': 13, 'Yo': 23, 'sea': 12, 'si': 24, 'en': 3, 'que': 11, 'idea': 15, 'get': 20, 'buena': 14, 'cafeteria': 5, 'a': 18, 'lost': 21, 'to': 8, 'good': 19, 'gusta': 1, 'not': 17, 'comer': 2, 'creo': 10, 'at': 22, 'it': 7, 'Give': 6, 'No': 9}
"""

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)

"""
Parameter containing:
tensor([[ 0.1953, 0.1572, -0.0092, -0.1309, 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]])
Parameter containing:
tensor([ 0.1882, 0.0102])
"""

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

"""
tensor([[-0.3480, -1.2245]])
"""

上述哪一个值对应于英语的对数概率,哪一个值是西班牙语的对数概率? 我们从来没有定义过它,但如果我们想要训练这个模型,我们需要定义它们。

1
label_to_ix = {"SPANISH": 0, "ENGLISH": 1}

所以,让我们训练模型吧!要做到这一点,我们通过传递实例来获取对数概率,计算损失函数,计算损失函数的梯度,然后用梯度更新参数。Torch在nn软件包中提供了损失函数。 nn.NLLLoss()是我们想要的负对数似然损失。它还定义了torch.optim中的优化函数。 在这里,我们只使用SGD

请注意,NLLLoss的输入是一个对数概率向量和一个目标标签。它不会为我们计算对数概率。 这就是为什么我们网络的最后一层是log softmax。 损失函数nn.CrossEntropyLoss()与NLLLoss()相同,只是它还会进行log softmax 操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# 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)

"""
tensor([[-0.6553, -0.7325]])
tensor([[-0.3468, -1.2274]])
"""

# Print the matrix column corresponding to "creo"
print(next(model.parameters())[:, word_to_ix["creo"]])

"""
tensor([-0.1099, 0.0630])
"""

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)

"""
tensor([[-0.1469, -1.9905]])
tensor([[-2.3387, -0.1014]])
"""

# Index corresponding to Spanish goes up, English goes down!
print(next(model.parameters())[:, word_to_ix["creo"]])

"""
tensor([ 0.3332, -0.3801])

我们得到了正确的答案!你可以看到,第一个示例中西班牙语的对数概率要高得多,并且测试数据的第二个样本中英语的对数概率也高得多,而实际情况正因如此。

现在,你已经看到如何实现PyTorch组件,通过它传递一些数据并进行梯度更新。 接下来我们准备深入挖掘NLP的知识。

Word Embeddings: Encoding Lexical Semantics

词嵌入是一个稠密向量,向量元素都是实数。在词汇表中每个单词都可以表示为词嵌入。 在NLP中,你的特征几乎总是是单词!但是,如何在电脑中表达一个词?你可以存储它的ascii字符表示,但是它只能告诉你这个单词是什么,它没有过多地表达它的含义(你可以从它的词缀或大写字母中得到它的词性,但是不多)。更何况,在何种意义上可以将这些表示结合在一起? 我们经常需要来自神经网络的稠密输出,输入是|V| 维度,其中V是我们的词汇量,但通常输出只有少量的维度(例如,如果我们只预测少数几个标签)。 我们如何从庞大的维度空间到更小的维度空间?

如果不用ascii表示,而使用一个one-hot编码呢?也就是说,我们将单词w表示为:

1表示对应位置的单词出现了,而0则表示该位置对应的单词没有出现。

除了它所占空间巨大之外,这种表示还有一个很大的缺点。它基本上将所有单词作为彼此无关的独立实体对待。我们真正想要的是一些词语之间的相似概念。为什么?我们来看一个例子。

假设我们正在建立一个语言模型。假设我们看过这些句子:

  • The mathematician ran to the store.
  • The physicist ran to the store.
  • The mathematician solved the open problem.

以上的数据出现在我们的训练数据中。现在假设我们得到了一个我们训练数据中前所未见的新句子:

  • The physicist solved the open problem.

我们的语言模型在这句话上可能会行得通,但是如果我们能够使用以下两个事实会好很多:

  • We have seen mathematician and physicist in the same role in a sentence. Somehow they have a semantic relation.
  • We have seen mathematician in the same role in this new unseen sentence as we are now seeing physicist.

然后推断物理学家实际上很拟合新的看不见的句子?这就是我们所说的相似概念:我们的意思是语义相似性,而不仅仅是具有类似的正交表示。这是一种通过连接我们所看到的和我们所没有看到的之间的点来防止或者减轻语言数据的稀疏性的技术。这个例子当然依赖于一个基本的语言学假设:出现在相似语境中的词在语义上彼此相关。这被称为 Distributional semantics

Getting Dense Word Embeddings

我们如何解决这个问题?也就是说,我们如何才能编码单词之间的语义相似性呢? 也许我们想出了一些语义属性。例如,我们看到数学家和物理学家都可以奔跑,所以也许我们会给这些词赋予“能够奔跑”语义属性的高分。

如果每个属性都是一个维度,那么我们可能会给每个单词一个向量,如下所示:

然后我们可以通过这样做来获得这些词的相似度:

img

通过长度进行归一化更为常见:

img

其中$φ$是两个矩阵之间的角度。这样,极其相似的单词(词嵌入指向相同方向的单词)将具有相似性1,极不相似的单词应该具有相似性-1。

你可以将本节开头提到的稀疏one-hot矩阵看成是我们定义的新矩阵的特例,每个单词基本上具有相似性0,我们给每个单词一些独特的语义属性。这些新的矩阵是稠密的,也就是说它们的值(通常)是非零的。

但是这些新的矩阵也有它的痛点:你可以想象成千上万个可能与确定相似性有关的不同语义属性,以及如何设置不同属性的值?深度学习理念的核心是神经网络学习特征的表示,而不是要求程序员亲自设计它们。那么为什么不让这个词嵌入在我们的模型中作为参数,然后在训练中更新?这正是我们要做的。我们将会有一些神经网络原则上可以学习的潜在语义属性。请注意,词嵌入可能无法解释它的具体含义。也就是说,尽管使用上面的手工设计的矩阵,我们可以看到数学家和物理学家的相似之处在于,他们都喜欢咖啡,如果我们允许神经网络学习词嵌入,我们可能看到数学家和物理学家在第二个维度都有很大的值,目前尚不清楚这意味着什么。它们在一些潜在的语义维度上是相似的,但是这可能对我们没有任何解释性。

总而言之,词嵌入是单词的语义的表示,有效地编码可能与当前任务相关的语义信息。你也可以嵌入其他的东西:词性标签,解析树,任何东西!特征嵌入的想法是该领域的核心。

Word Embeddings in Pytorch

在我们开始一个例子和练习之前,先简单介绍一下如何在Pytorch中使用词嵌入。类似于我们在制作一个one-hot向量时为每个单词定义唯一索引的方式,我们在使用词嵌入时同样需要为每个单词定义一个索引。这些将成为查找表的关键。也就是说,词嵌入被存储为|V|×D矩阵,其中D是词嵌入的维数,被分配到索引i的单词将其词嵌入存储在矩阵的第i行。 在我的所有代码中,从单词到索引的映射是一个名为word_to_ix的字典。

使用词嵌入需要用到的模块是torch.nn.Embedding,它有两个参数:词汇大小(vocabulary size)和词嵌入的维度。

要索引到此表中,您必须使用torch.LongTensor(因为索引是整数,而不是浮点数)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

torch.manual_seed(1)

word_to_ix = {"hello": 0, "world": 1}
embeds = nn.Embedding(2, 5) # 2 words in vocab, 5 dimensional embeddings
lookup_tensor = torch.tensor([word_to_ix["hello"]], dtype=torch.long)
hello_embed = embeds(lookup_tensor)
print(hello_embed)

"""
tensor([[ 0.6614, 0.2669, 0.0617, 0.6213, -0.4519]])
"""

An Example: N-Gram Language Modeling

回想一下,在一个n-gram语言模型中,给定一个单词序列w,我们要计算:

img

$w_i$是序列的第i个单词。

在这个例子中,我们将计算一些训练样例的损失函数,并用反向传播更新参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
CONTEXT_SIZE = 2
EMBEDDING_DIM = 10
# We will use Shakespeare Sonnet 2
test_sentence = """When forty winters shall besiege thy brow,
And dig deep trenches in thy beauty's field,
Thy youth's proud livery so gazed on now,
Will be a totter'd weed of small worth held:
Then being asked, where all thy beauty lies,
Where all the treasure of thy lusty days;
To say, within thine own deep sunken eyes,
Were an all-eating shame, and thriftless praise.
How much more praise deserv'd thy beauty's use,
If thou couldst answer 'This fair child of mine
Shall sum my count, and make my old excuse,'
Proving his beauty by succession thine!
This were to be new made when thou art old,
And see thy blood warm when thou feel'st it cold.""".split()
# we should tokenize the input, but we will ignore that for now
# build a list of tuples. Each tuple is ([ word_i-2, word_i-1 ], target word)
trigrams = [([test_sentence[i], test_sentence[i + 1]], test_sentence[i + 2])
for i in range(len(test_sentence) - 2)]
# print the first 3, just so you can see what they look like
print(trigrams[:3])

"""
[(['When', 'forty'], 'winters'), (['forty', 'winters'], 'shall'), (['winters', 'shall'], 'besiege')]
"""

vocab = set(test_sentence)
word_to_ix = {word: i for i, word in enumerate(vocab)}


class NGramLanguageModeler(nn.Module):

def __init__(self, vocab_size, embedding_dim, context_size):
super(NGramLanguageModeler, self).__init__()
self.embeddings = nn.Embedding(vocab_size, embedding_dim)
self.linear1 = nn.Linear(context_size * embedding_dim, 128)
self.linear2 = nn.Linear(128, vocab_size)

def forward(self, inputs):
embeds = self.embeddings(inputs).view((1, -1))
out = F.relu(self.linear1(embeds))
out = self.linear2(out)
log_probs = F.log_softmax(out, dim=1)
return log_probs


losses = []
loss_function = nn.NLLLoss()
model = NGramLanguageModeler(len(vocab), EMBEDDING_DIM, CONTEXT_SIZE)
optimizer = optim.SGD(model.parameters(), lr=0.001)

for epoch in range(10):
total_loss = torch.Tensor([0])
for context, target in trigrams:

# Step 1. Prepare the inputs to be passed to the model (i.e, turn the words
# into integer indices and wrap them in variables)
context_idxs = torch.tensor([word_to_ix[w] for w in context], dtype=torch.long)

# Step 2. Recall that torch *accumulates* gradients. Before passing in a
# new instance, you need to zero out the gradients from the old
# instance
model.zero_grad()

# Step 3. Run the forward pass, getting log probabilities over next
# words
log_probs = model(context_idxs)

# Step 4. Compute your loss function. (Again, Torch wants the target
# word wrapped in a variable)
loss = loss_function(log_probs, torch.tensor([word_to_ix[target]], dtype=torch.long))

# Step 5. Do the backward pass and update the gradient
loss.backward()
optimizer.step()

# Get the Python number from a 1-element Tensor by calling tensor.item()
total_loss += loss.item()
losses.append(total_loss)
print(losses) # The loss decreased every iteration over the training data!

"""
[tensor([ 521.5344]), tensor([ 518.7239]), tensor([ 515.9378]), tensor([ 513.1744]), tensor([ 510.4317]), tensor([ 507.7059]), tensor([ 504.9966]), tensor([ 502.3058]), tensor([ 499.6322]), tensor([ 496.9749])]
"""

Exercise: Computing Word Embeddings: Continuous Bag-of-Words

连续词袋模型(CBOW)常用于NLP深度学习。 这是一种试图根据给定目标词之前和之后几个单词的来预测目标单词的模型。 这与语言建模不同,因为CBOW不是顺序化的,也不一定是概率性的。 一般地,CBOW用于快速训练单词嵌入,并且这些词嵌入用于初始化一些更复杂模型的词嵌入。 通常,这被称为预训练词嵌入。它几乎总是有助于提升模型几个百分点的表现。

CBOW模型如下。给定一个目标词 $w_i$ 以及在每一侧上的$N$个上下文窗口$w_{i-1}$,…,$w_{i-N}$和$w_{i+1}$,…,$w_{i+N}$,将所有上下文单词统称为$C$,CBOW尝试最小化:

img

其中$q_w$是单词$w$的词嵌入。

通过完善下面的类来在Pytorch中实现此模型。一些技巧:

  • 想想你需要定义哪些参数。
  • 确保你知道每个操作该有的shape。如果需要reshape,请使用.view()。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
CONTEXT_SIZE = 2  # 2 words to the left, 2 to the right
raw_text = """We are about to study the idea of a computational process.
Computational processes are abstract beings that inhabit computers.
As they evolve, processes manipulate other abstract things called data.
The evolution of a process is directed by a pattern of rules
called a program. People create programs to direct processes. In effect,
we conjure the spirits of the computer with our spells.""".split()

# By deriving a set from `raw_text`, we deduplicate the array
vocab = set(raw_text)
vocab_size = len(vocab)

word_to_ix = {word: i for i, word in enumerate(vocab)}
data = []
for i in range(2, len(raw_text) - 2):
context = [raw_text[i - 2], raw_text[i - 1],
raw_text[i + 1], raw_text[i + 2]]
target = raw_text[i]
data.append((context, target))
print(data[:5])

"""
[(['We', 'are', 'to', 'study'], 'about'), (['are', 'about', 'study', 'the'], 'to'), (['about', 'to', 'the', 'idea'], 'study'), (['to', 'study', 'idea', 'of'], 'the'), (['study', 'the', 'of', 'a'], 'idea')]
"""

class CBOW(nn.Module):

def __init__(self, vocab_size, embedding_dim, context_size):
super(CBOW, self).__init__()
self.embeddings = nn.Embedding(vocab_size, embedding_dim)
self.linear1 = nn.Linear(2 * context_size * embedding_dim, 128)
self.linear2 = nn.Linear(128, vocab_size)


def forward(self, inputs):
embeds = self.embeddings(inputs).view((1, -1))
out = F.relu(self.linear1(embeds))
out = self.linear2(out)
log_probs = F.log_softmax(out)
return log_probs

# create your model and train. here are some functions to help you make
# the data ready for use by your module


def make_context_vector(context, word_to_ix):
idxs = [word_to_ix[w] for w in context]
return torch.tensor(idxs, dtype=torch.long)


make_context_vector(data[0][0], word_to_ix) # example

losses = []
loss_function = nn.NLLLoss()
model = CBOW(vocab_size, 10, CONTEXT_SIZE)
optimizer = optim.SGD(model.parameters(), lr=0.001)

for epoch in range(10):
total_loss = torch.Tensor([0])
for context, target in data:


# Step 1. Prepare the inputs to be passed to the model (i.e, turn the words
# into integer indices and wrap them in variables)
context_idxs = torch.tensor([word_to_ix[w] for w in context], dtype=torch.long)

# Step 2. Recall that torch *accumulates* gradients. Before passing in a
# new instance, you need to zero out the gradients from the old
# instance
model.zero_grad()

# Step 3. Run the forward pass, getting log probabilities over next
# words
log_probs = model(context_idxs)

# Step 4. Compute your loss function. (Again, Torch wants the target
# word wrapped in a variable)
loss = loss_function(log_probs, torch.tensor([word_to_ix[target]], dtype=torch.long))

# Step 5. Do the backward pass and update the gradient
loss.backward()
optimizer.step()

# Get the Python number from a 1-element Tensor by calling tensor.item()
total_loss += loss.item()
losses.append(total_loss)
print(losses) # The loss decreased every iteration over the training data!

"""
[tensor([ 226.7636]), tensor([ 225.2879]), tensor([ 223.8248]), tensor([ 222.3729]), tensor([ 220.9305]), tensor([ 219.4968]), tensor([ 218.0720]), tensor([ 216.6528]), tensor([ 215.2410]), tensor([ 213.8338])]
"""

Sequence Models and Long-Short Term Memory Networks

至此,我们已经看到了各种前馈网络。也就是说,网络根本没有保持任何状态。 这可能不是我们想要的。序列模型是NLP的核心:它们是输入之间存在某种时间依赖性的模型。 序列模型的典型例子是用于词性标注的隐马尔可夫模型。另一个例子是条件随机场。

循环神经网络是可以维持某种状态的网络。 例如,它的输出可以用作下一个输入的一部分,以便在网络通过序列时信息可以传播。 在LSTM中,对于序列中的每个元素,存在相应的隐藏状态$h_t$,原则上可以包含序列中较早任意点的信息。 我们可以使用隐藏状态来预测语言模型中的单词,词性标签以及其他各种各样的内容。

LSTM’s in Pytorch

在开始示例之前,请注意一些事情。Pytorch的LSTM的所有输入都应该是3D张量。 这些张量的轴的语义是很重要的。 第一个轴是序列本身,第二个轴以mini-batch来索引实例,第三个轴索引输入的元素。我们还没有讨论过mini-batch,所以让我们先忽略这一点,并假设我们在第二轴上总是只有一个维度。如果我们想要在“The cow jumped”这个句子上运行序列模型,我们的输入应该是:

img

记住,第二维在这里的size是1(暂不讨论mini-batch)。

另外,您可以逐个检查序列,在这种情况下,第一个轴的size也是1。

我们来看一个简单的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

torch.manual_seed(1)

lstm = nn.LSTM(3, 3) # Input dim is 3, output dim is 3
inputs = [torch.randn(1, 3) for _ in range(5)] # make a sequence of length 5

# initialize the hidden state.
hidden = (torch.randn(1, 1, 3),
torch.randn(1, 1, 3))
for i in inputs:
# Step through the sequence one element at a time.
# after each step, hidden contains the hidden state.
out, hidden = lstm(i.view(1, 1, -1), hidden)

# alternatively, we can do the entire sequence all at once.
# the first value returned by LSTM is all of the hidden states throughout
# the sequence. the second is just the most recent hidden state
# (compare the last slice of "out" with "hidden" below, they are the same)
# The reason for this is that:
# "out" will give you access to all hidden states in the sequence
# "hidden" will allow you to continue the sequence and backpropagate,
# by passing it as an argument to the lstm at a later time
# Add the extra 2nd dimension
inputs = torch.cat(inputs).view(len(inputs), 1, -1)
hidden = (torch.randn(1, 1, 3), torch.randn(1, 1, 3)) # clean out hidden state
out, hidden = lstm(inputs, hidden)
print(out)

"""
tensor([[[-0.0187, 0.1713, -0.2944]],

[[-0.3521, 0.1026, -0.2971]],

[[-0.3191, 0.0781, -0.1957]],

[[-0.1634, 0.0941, -0.1637]],

[[-0.3368, 0.0959, -0.0538]]])
"""
print(hidden)

"""
(tensor([[[-0.3368, 0.0959, -0.0538]]]), tensor([[[-0.9825, 0.4715, -0.0633]]]))
"""

Example: An LSTM for Part-of-Speech Tagging

在本节中,我们将使用LSTM来获取词性标签。我们不会使用Viterbi或Forward-Backward或类似的东西,但作为一个(具有挑战性的)练习给读者,请在这之后考虑如何使用Viterbi。

模型如下:让我们的输入句子为$w_1$, …, $w_M$,其中$w_i∈V$,$V$是我们的词汇表。另外,$T$是我们的标签集,并且$y_i$是词$w_i$的标签。用$\hat{y_i}$表示我们对词$w_i$标签的预测。

这是一个结构预测模型,我们的输出是序列$\hat{y_1}$, …, $\hat{y_M}$,其中$\hat{y_i} ∈ T$。

要进行预测,我们会将序列通过一个LSTM。用$h_i$表示在时间步$i$时刻的隐状态。并且,我们也将会给每个标签分配一个唯一的索引(就像我们在词嵌入部分做的word_to_ix操作一样)。那么我们预测出$\hat{y_i}$的rule就是:

img

也就是说,取隐藏状态的仿射变换的log softmax,预测标签是在该向量中的最大值。 注意这暗示了$A$的目标空间的维度是$|T|$。

准备数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def prepare_sequence(seq, to_ix):
idxs = [to_ix[w] for w in seq]
return torch.tensor(idxs, dtype=torch.long)


training_data = [
("The dog ate the apple".split(), ["DET", "NN", "V", "DET", "NN"]),
("Everybody read that book".split(), ["NN", "V", "DET", "NN"])
]
word_to_ix = {}
for sent, tags in training_data:
for word in sent:
if word not in word_to_ix:
word_to_ix[word] = len(word_to_ix)
print(word_to_ix)

"""
{'read': 6, 'The': 0, 'book': 8, 'ate': 2, 'dog': 1, 'apple': 4, 'Everybody': 5, 'that': 7, 'the': 3}
"""
tag_to_ix = {"DET": 0, "NN": 1, "V": 2}

# These will usually be more like 32 or 64 dimensional.
# We will keep them small, so we can see how the weights change as we train.
EMBEDDING_DIM = 6
HIDDEN_DIM = 6

创建模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class LSTMTagger(nn.Module):

def __init__(self, embedding_dim, hidden_dim, vocab_size, tagset_size):
super(LSTMTagger, self).__init__()
self.hidden_dim = hidden_dim

self.word_embeddings = nn.Embedding(vocab_size, embedding_dim)

# The LSTM takes word embeddings as inputs, and outputs hidden states
# with dimensionality hidden_dim.
self.lstm = nn.LSTM(embedding_dim, hidden_dim)

# The linear layer that maps from hidden state space to tag space
self.hidden2tag = nn.Linear(hidden_dim, tagset_size)
self.hidden = self.init_hidden()

def init_hidden(self):
# Before we've done anything, we dont have any hidden state.
# Refer to the Pytorch documentation to see exactly
# why they have this dimensionality.
# The axes semantics are (num_layers, minibatch_size, hidden_dim)
return (torch.zeros(1, 1, self.hidden_dim),
torch.zeros(1, 1, self.hidden_dim))

def forward(self, sentence):
embeds = self.word_embeddings(sentence)
lstm_out, self.hidden = self.lstm(
embeds.view(len(sentence), 1, -1), self.hidden)
tag_space = self.hidden2tag(lstm_out.view(len(sentence), -1))
tag_scores = F.log_softmax(tag_space, dim=1)
return tag_scores

训练模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
model = LSTMTagger(EMBEDDING_DIM, HIDDEN_DIM, len(word_to_ix), len(tag_to_ix))
loss_function = nn.NLLLoss()
optimizer = optim.SGD(model.parameters(), lr=0.1)

# See what the scores are before training
# Note that element i,j of the output is the score for tag j for word i.
# Here we don't need to train, so the code is wrapped in torch.no_grad()
with torch.no_grad():
inputs = prepare_sequence(training_data[0][0], word_to_ix)
tag_scores = model(inputs)
print(tag_scores)


"""
tensor([[-1.1389, -1.2024, -0.9693],
[-1.1065, -1.2200, -0.9834],
[-1.1286, -1.2093, -0.9726],
[-1.1190, -1.1960, -0.9916],
[-1.0137, -1.2642, -1.0366]])
"""

for epoch in range(300): # again, normally you would NOT do 300 epochs, it is toy data
for sentence, tags in training_data:
# Step 1. Remember that Pytorch accumulates gradients.
# We need to clear them out before each instance
model.zero_grad()

# Also, we need to clear out the hidden state of the LSTM,
# detaching it from its history on the last instance.
model.hidden = model.init_hidden()

# Step 2. Get our inputs ready for the network, that is, turn them into
# Tensors of word indices.
sentence_in = prepare_sequence(sentence, word_to_ix)
targets = prepare_sequence(tags, tag_to_ix)

# Step 3. Run our forward pass.
tag_scores = model(sentence_in)

# Step 4. Compute the loss, gradients, and update the parameters by
# calling optimizer.step()
loss = loss_function(tag_scores, targets)
loss.backward()
optimizer.step()

# See what the scores are after training
with torch.no_grad():
inputs = prepare_sequence(training_data[0][0], word_to_ix)
tag_scores = model(inputs)

# The sentence is "the dog ate the apple". i,j corresponds to score for tag j
# for word i. The predicted tag is the maximum scoring tag.
# Here, we can see the predicted sequence below is 0 1 2 0 1
# since 0 is index of the maximum value of row 1,
# 1 is the index of maximum value of row 2, etc.
# Which is DET NOUN VERB DET NOUN, the correct sequence!
print(tag_scores)

"""
tensor([[-0.0858, -2.9355, -3.5374],
[-5.2313, -0.0234, -4.0314],
[-3.9098, -4.1279, -0.0368],
[-0.0187, -4.7809, -4.5960],
[-5.8170, -0.0183, -4.1879]])
"""

Exercise: Augmenting the LSTM part-of-speech tagger with character-level features

在上面的例子中,每个单词都有一个词嵌入,作为序列模型的输入。让我们使用单词里的字符的表征来扩充词嵌入。我们期望这应该会有很大的帮助,因为字符级别的信息如词缀对词性的影响很大。例如,带有-ly词缀的词几乎总是被标记为副词。

注意一下一些标记的含义,$c_w$是单词$w$的字符级表示。$x_w$是词嵌入。那么,我们序列模型的输入是$x_w$和$c_w$的concatenation。所以如果$x_w$有5维,$c_w$有3维,那么我们的LSTM的输入应该是$3 + 5 = 8$维。

为了获得字符级别表示,对单词的字符做一个LSTM操作,并让$c_w$成为这个LSTM的最终隐藏状态。提示:

  • 新模型中将会有两个LSTM。第一个输出POS标签分数,第二个输出每个单词的字符级别表示。
  • 要完成字符的序列模型,你必须进行字嵌入。字嵌入将成为字符LSTM的输入。
-------------本文结束感谢您的阅读-------------

本文标题:Deep Learning for NLP with PyTorch

文章作者:丁鹏

发布时间:2018年05月12日 - 16:05

最后更新:2018年05月16日 - 14:05

原始链接:http://deepon.me/2018/05/12/Deep-Learning-for-NLP-with-PyTorch/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

给博主投币,共同实现开源世界
0%