PyTorch中文教程 | (15) 在深度学习和NLP中使用PyTorch

Github地址

目录

1. PyTorch介绍

2. 使用PyTorch进行深度学习

3. 词嵌入:编码形式的词汇语义

4. 序列模型和LSTM网络

 

1. PyTorch介绍

  • Torch张量库介绍

深度学习的所有计算都是在张量上进行的,其中张量是一个可以被超过二维索引的矩阵的一般表示形式。稍后我们将详细讨论这意味着什么。首先,我们先来看一下我们可以用张量来干什么。

# 作者: Robert Guthrie

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)

创建张量:

张量可以在Python list形式下通过torch.Tensor()函数创建。

# 利用给定数据创建一个torch.Tensor对象.这是一个一维向量
V_data = [1., 2., 3.]
V = torch.Tensor(V_data)
print(V)

# 创建一个矩阵
M_data = [[1., 2., 3.], [4., 5., 6]]
M = torch.Tensor(M_data)
print(M)

# 创建2x2x2形式的三维张量.
T_data = [[[1., 2.], [3., 4.]],
          [[5., 6.], [7., 8.]]]
T = torch.Tensor(T_data)
print(T)

PyTorch中文教程 | (15) 在深度学习和NLP中使用PyTorch_第1张图片

什么是三维张量?让我们这样想象。如果你有一个向量,那么对这个向量索引就会得到一个标量。如果你有一个矩阵,对这个矩阵索引那么就会得到一个向量。如果你有一个三维张量,那么对其索引就会得到一个矩阵!

针对术语的说明:当我在本教程内使用“tensor”,它针对的是所有torch.Tensor对象。矩阵和向量是特殊的torch.Tensors,他们的维度分别是2和1。当我说到三维张量,我会简洁的使用“3D tensor”。

# 索引V得到一个标量(0维张量)
print(V[0])

# 从向量V中获取一个数字  .item()可以将tensor标量转换为python数值
print(V[0].item())

# 索引M得到一个向量
print(M[0])

# 索引T得到一个矩阵
print(T[0])

PyTorch中文教程 | (15) 在深度学习和NLP中使用PyTorch_第2张图片

你也可以创建其他数据类型的tensors。默认的数据类型为浮点型。可以使用torch.LongTensor()来创建一个整数类型的张量。你可以在文件中寻找更多的数据类型,但是浮点型和长整形是最常用的。

你可以使用torch.randn()创建一个张量。这个张量拥有随机数据和需要指定的维度。

x = torch.randn((3, 4, 5))
print(x)

PyTorch中文教程 | (15) 在深度学习和NLP中使用PyTorch_第3张图片

张量操作:

你可以以你想要的方式操作张量。

x = torch.Tensor([1., 2., 3.])
y = torch.Tensor([4., 5., 6.])
z = x + y
print(z)

可以查阅 文档 获取大量可用操作的完整列表,这些操作不仅局限于数学操作范围。

接下来一个很有帮助的操作就是连接。

# 默认情况下, 沿y轴/0轴(竖直)方向拼接
x_1 = torch.randn(2, 5)
y_1 = torch.randn(3, 5)
z_1 = torch.cat([x_1, y_1])
print(z_1)

# 连接列:
x_2 = torch.randn(2, 3)
y_2 = torch.randn(2, 5)
# 第二个参数指定了沿着哪条轴连接 1表示沿x轴/1轴(水平方向)拼接
z_2 = torch.cat([x_2, y_2], 1)
print(z_2)

# 如果你的tensors是不兼容的,torch会报错。取消注释来查看错误。
torch.cat([x_1, x_2])

PyTorch中文教程 | (15) 在深度学习和NLP中使用PyTorch_第4张图片

重构张量:

使用.view()去重构张量。这是一个高频方法,因为许多神经网络的神经元对输入格式有明确的要求。你通常需要先将数据重构再输入到神经元中。

x = torch.randn(2, 3, 4)
print(x)
print(x.view(2, 12))  # 重构为2行12列
# 同上。如果维度为-1,那么它的大小可以根据数据推断出来
print(x.view(2, -1))

PyTorch中文教程 | (15) 在深度学习和NLP中使用PyTorch_第5张图片

  • 计算图和自动求导

计算图的思想对于有效率的深度学习编程是很重要的,因为它可以使你不必去自己写反向梯度传播。计算图只是简单地说明了如何将数据组合在一起以输出结果。因为图完全指定了操作所包含的参数,因此它包含了足够的信息去求导。这可能听起来很模糊,所以让我们看看使用Pytorch的基本类:requires_grad。

首先,从程序员的角度来思考。我们在上面刚刚创建的torch.Tensor对象中存储了什么?显然,是数据和结构,也很可能是其他的东西。但是当我们将两个张量相加,我们得到了一个输出张量。这个输出所能体现出的只有数据和结构,并不能体现出是由两个张量加和得到的(因为它可能是从一个文件中读取的, 也可能是其他操作的结果等)。

如果requires_grad=True,张量对象可以一直跟踪它是如何创建的。让我们在实际中来看。

# 张量对象带有“requires_grad”标记 默认为False
x =torch.tensor([1., 2., 3],requires_grad=True)

# 通过requires_grad=True,您也可以做之前所有的操作。
y = torch.tensor([4., 5., 6], requires_grad=True)
z = x + y
print(z)

# 但是z还有一些额外的东西.
print(z.grad_fn)

既然变量知道怎么创建的它们。z知道它并非是从文件读取的,也不是乘法或指数或其他运算的结果。如果你继续跟踪 z.grad_fn,你会从中找到x和y的痕迹

但是它如何帮助我们计算梯度?

# 我们来将z中所有项作和运算
s = z.sum()
print(s)
print(s.grad_fn)

那么这个计算和对x的第一个分量的导数等于多少? 在数学上,我们求

PyTorch中文教程 | (15) 在深度学习和NLP中使用PyTorch_第6张图片

并且s包含了足够的信息去决定我们需要的导数为1!

当然它掩盖了如何计算导数的挑战。这是因为s携带了足够多的信息所以导数可以被计算。现实中,Pytorch 程序的开发人员用程序指令sum()和 + 操作以知道如何计算它们的梯度并且运行反向传播算法。深入讨论此算法超出了本教程的范围.

让我们用Pytorch计算梯度,发现我们是对的:(注意如果你运行这个模块很多次,它的梯度会上升,这是因为Pytorch累积梯度渐变为.grad属性,而且对于很多模型它是很方便的.)

# 在任意变量上使用 .backward()将会运行反向,从该变量开始.
s.backward()
print(x.grad)
print(y.grad)

作为一个成功的深度学习程序员了解下面的模块如何运行是至关重要的。

x = torch.randn((2, 2))
y = torch.randn((2, 2))
#用户创建的张量在默认情况下“requires_grad=False”
print(x.requires_grad, y.requires_grad)
z = x + y  
# 你不能通过z反向传播。
print(z.grad_fn)


# “.requires_grad_( ... )”可以改变张量的“requires_grad”属性
# 如果没有指定,标记默认为True
x = x.requires_grad_()
y = y.requires_grad_()
#正如我们在上面看到的一样,z包含足够的信息计算梯度。
z = x + y
print(z.grad_fn)
# 如果任何操作的输入部分带有“requires_grad=True”那么输出就会变为:
print(z.requires_grad)

# 现在z有关于x,y的历史信息
# 我们可以获取它的值,将其从历史中分离出来吗?
new_z = z.detach()

# new_z有足够的信息反向传播至x和y吗?
# 答案是没有
print(new_z.grad_fn)
# 怎么会这样? “z.detach()”函数返回了一个与“z”相同存储的张量
#但是没有携带历史的计算信息。
# 它对于自己是如何计算得来的不知道任何事情。
# 从本质上讲,我们已经把这个变量从过去的历史中分离出来了。
print(z)
print(new_z)

PyTorch中文教程 | (15) 在深度学习和NLP中使用PyTorch_第7张图片

您也可以通过.requires_grad``=True by wrapping the code block in ``with torch.no_grad():停止跟踪张量的历史记录中的自动求导。

print(x.requires_grad)
print((x ** 2).requires_grad)

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

PyTorch中文教程 | (15) 在深度学习和NLP中使用PyTorch_第8张图片

 

2. 使用PyTorch进行深度学习

  • 深度学习构建模块:仿射变换, 非线性函数以及目标函数

深度学习表现为使用更巧妙的方法将线性函数和非线性函数进行组合。非线性函数的引入使得训练出来的模型更加强大。在本节中,我们将学习这些核心组件,建立目标函数,并理解模型是如何构建的。

仿射变换:

深度学习的核心组件之一是仿射变换,仿射变换是一个关于矩阵A和向量xbf(x)函数,如下所示:

需要训练的参数就是该公式中的Ab

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)

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

非线性函数:

首先,注意以下这个例子,它将解释为什么我们需要非线性函数。假设我们有两个仿射变换 f(x) = Ax + b 和 g(x) = Cx + d 。那么 f(g(x)) 又是什么呢?

PyTorch中文教程 | (15) 在深度学习和NLP中使用PyTorch_第9张图片

AC 是一个矩阵,Ad + b是一个向量,可以看出,两个仿射变换的组合还是一个仿射变换。

由此可以看出,使用以上方法将多个仿射变换组合成的长链式的神经网络,相对于单个仿射变换并没有性能上的提升。

但是如果我们在两个仿射变换之间引入非线性,那么结果就大不一样了,我们可以构建出一个高性能的模型。

最常用的核心的非线性函数有:tanh(x)σ(x)ReLU(x)。你可能会想:“为什么是这些函数?明明有其他更多的非线性函数。”这些函数常用的原因是它们拥有可以容易计算的梯度,而计算梯度是学习的本质。例如

PyTorch中文教程 | (15) 在深度学习和NLP中使用PyTorch_第10张图片

注意:尽管你可能在AI课程的介绍中学习了一些神经网络,在这些神经网络中σ(x)是默认非线性的,但是通常在实际使用的过程中都会避开它们。这是因为当参数的绝对值增长时,梯度会很快消失。小梯度意味着很难学习。因此大部分人默认选择tanh或者ReLU

# In pytorch, most non-linearities are in torch.nn.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))

PyTorch中文教程 | (15) 在深度学习和NLP中使用PyTorch_第11张图片

Softmax和概率:

Softmax(x)也是一个非线性函数,但它的特殊之处在于,它通常是神经网络的最后一个操作。这是因为它接受实数向量,并且返回一个概率分布。它的定义如下。设x为实数向量(正、负,无论什么,没有约束)。然后Softmax(x)的第i个分量是:

PyTorch中文教程 | (15) 在深度学习和NLP中使用PyTorch_第12张图片

很明显,输出的是一个概率分布:每一个元素都非负且和为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

PyTorch中文教程 | (15) 在深度学习和NLP中使用PyTorch_第13张图片

目标函数:

目标函数正是神经网络通过训练来最小化的函数(因此,它常常被称作损失函数或者成本函数)。这需要首先选择一个训练数据实例,通过神经网络运行它并计算输出的损失。然后通过损失函数的导数来更新模型的参数。因此直观来讲,如果它的结果是错误的,而模型完全信任他,那么损失将会很高。反之,当模型信任计算结果而结果正确时,损失会很低。

在你的训练实例中最小化损失函数的目的是使你的网络拥有很好的泛化能力,可以在开发数据集,测试数据集以及实际生产中拥有很小的损失。损失函数的一个例子是负对数似然损失函数,这个函数经常在多级分类中出现。在监督多级分类中,这意味着训练网络最小化正确输出的负对数概率(等效于最大化正确输出的对数概率)。

 

  • 优化和训练

那么,我们该怎么计算函数实例的损失函数呢?我们应该做什么呢?我们在之前了解到TensorFlow中的Tensor知道如何计算梯度以及计算梯度相关的东西。由于我们的损失正是一个Tensor,因此我们可以使用所有与梯度有关的参数来计算梯度。然后我们可以进行标准梯度更新。设 θ为我们的参数,L(θ)为损失函数,η一个正的学习率。然后:

目前,有大量的算法和积极的研究试图做一些除了这种普通的梯度更新以外的事情。许多人尝试去基于训练时发生的事情来改变学习率。但是,你不需要担心这些特殊的算法到底在干什么,除非你真的很感兴趣。Torch提供了大量的算法在torch.optim包中,且全部都是透明的。在语法上使用复杂的算法和使用最简单的梯度更新一样简单。但是尝试不同的更新算法和在更新算法中使用不同的参数(例如不同的初始学习率)对于优化你的网络的性能很重要。通常,仅仅将普通的SGD替换成一个例如Adam或者RMSProp优化器都可以显著的提升性能。

 

  • 使用PyTorch创建网络组件

在我们继续关注NLP之前,让我们先使用PyTorch构建一个只用仿射变换和非线性函数组成的网络示例。我们也将了解如何计算损失函数,使用PyTorch内置的负对数似然函数,以及通过反向传播更新参数。

所有的网络组件应该继承nn.Module并覆盖forward()方法。继承nn.Module提供给了一些方法给你的组件。例如,它可以跟踪可训练的参数,你可以通过.to(device)方法在CPU和GPU之间交换它们。.to(device)方法中的device可以是CPU设备torch.device("cpu")或者CUDA设备torch.device("cuda:0")

让我们写一个神经网络的示例,它接受一些稀疏的BOW(词袋模式)表示,然后输出分布在两个标签上的概率:“English”和“Spanish”。这个模型只是一个逻辑回归。

示例:基于逻辑回归与词袋模式的文本分类器

我们的模型将会把BOW表示映射成标签上的对数概率。我们为词汇中的每个词指定一个索引。例如,我们所有的词汇是两个单词“hello”和"world",用0和1表示。句子“hello hello hello hello”的表示是

[4,0]

对于“hello world world hello”, 则表示成:

[2,2]

通常表示成

[Count(hello),Count(world)]

用x来表示这个BOW向量。网络的输出是:

也就是说,我们数据传入一个仿射变换然后做对数归一化logsoftmax

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)

PyTorch中文教程 | (15) 在深度学习和NLP中使用PyTorch_第14张图片

上面的哪一个值对应的是ENGLISH的对数概率,哪一个是SPANISH的对数概率?我们还没有定义,但是如果我必须要定义我们想要训练的东西。

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

让我们来训练吧! 我们将实例传入来获取对数概率,计算损失函数,计算损失函数的梯度,然后使用一个梯度步长来更新参数。在PyTorch的nn包里提供了损失函数。nn.NLLLoss()是我们想要的负对数似然损失函数。torch.optim中也定义了优化方法。这里,我们只使用SGD

注意,因为NLLLoss的输入是一个对数概率的向量以及目标标签。它不会为我们计算对数概率。这也是为什么我们最后一层网络是log_softmax的原因。损失函数nn.CrossEntropyLoss()除了对结果额外计算了logsoftmax之外和NLLLoss()没什么区别。

# 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() #每次训练之前需要清0 不然会累加

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

PyTorch中文教程 | (15) 在深度学习和NLP中使用PyTorch_第15张图片

我们得到了正确的结果!你可以看到Spanish的对数概率比第一个例子中的高的多,English的对数概率在第二个测试数据中更高,结果也应该是这样。

现在你了解了如何创建一个PyTorch组件,将数据传入并进行梯度更新。现在我们已经可以开始进行深度学习上的自然语言处理了。

 

3. 词嵌入:编码形式的词汇语义

词嵌入是一种由真实数字组成的稠密向量,每个向量都代表了单词表里的一个单词。 在自然语言处理中,总会遇到这样的情况:特征全是单词!但是,如何在电脑上表述一个单词呢?你在电脑上存储的单词的ascii码,但是它仅仅代表单词怎么拼写,没有说明单词的内在含义(你也许能够从词缀中了解它的词性,或者从大小写中得到一些属性,但仅此而已)。 更重要的是,你能把这些ascii码字符组合成什么含义?当V代表词汇表、输入数据是|V|维的情况下,我们往往想从神经网络中得到数据密集的结果,但是结果只有很少的几个维度(例如,预测的数据只有几个标签时)。我们如何从大的数据维度空间中得到稍小一点的维度空间?

放弃使用ascii码字符的形式表示单词,换用one-hot encoding会怎么样了?好吧,w这个单词就能这样表示:

PyTorch中文教程 | (15) 在深度学习和NLP中使用PyTorch_第16张图片

其中,1 表示的独有位置,其他位置全是0。其他的词都类似,在另外不一样的位置有一个1代表它,其他位置也都是0。 这种表达除了占用巨大的空间外,还有个很大的缺陷。 它只是简单的把词看做一个单独个体,认为它们之间毫无联系。 我们真正想要的是能够表达单词之间一些相似的含义。为什么要这样做呢?来看下面的例子:

假如我们正在搭建一个语言模型,训练数据有下面一些句子:

PyTorch中文教程 | (15) 在深度学习和NLP中使用PyTorch_第17张图片

现在又得到一个没见过的新句子:

我们的模型可能在这个句子上表现的还不错,但是,如果利用了下面两个事实,模型会表现更佳:

1)我们发现数学家和物理学家在句子里有相同的作用,所以在某种程度上,他们有语义的联系。

2)当看见物理学家在新句子中的作用时,我们发现数学家也有起着相同的作用。

然后我们就推测,物理学家在上面的句子里也类似于数学家吗? 这就是我们所指的相似性理念: 指的是语义相似,而不是简单的拼写相似。 这就是一种通过连接我们发现的和没发现的一些内容相似点、用于解决语言数据稀疏性的技术。 这个例子依赖于一个基本的语言假设: 那些在相似语句中出现的单词,在语义上也是相互关联的。 这就叫做 distributional hypothesis(分布式假设)。

 

  • 密集词嵌入

我们如何解决这个问题呢?也就是,怎么编码单词中的语义相似性? 也许我们会想到一些语义属性。 举个例子,我们发现数学家和物理学家都能跑, 所以也许可以给含有“能跑”语义属性的单词打高分,考虑一下其他的属性,想象一下你可能会在这些属性上给普通的单词打什么分。

如果每个属性都表示一个维度,那我们也许可以用一个向量表示一个单词,就像这样:

PyTorch中文教程 | (15) 在深度学习和NLP中使用PyTorch_第18张图片

那么,我们就这可以通过下面的方法得到这些单词之间的相似性:

尽管通常情况下需要进行长度归一化(余弦相似度):

是两个向量的夹角。 这就意味着,完全相似的单词相似度为1。完全不相似的单词相似度为-1。

你可以把本章开头介绍的one-hot稀疏向量看做是我们新定义向量的一种特殊形式,那里的单词相似度为0, 现在我们给每个单词一些独特的语义属性。 这些向量数据密集,也就是说它们数字通常都非零。

但是新的这些向量存在一个严重的问题: 你可以想到数千种不同的语义属性,它们可能都与决定相似性有关,而且,到底如何设置不同属性的值呢?深度学习的中心思想是用神经网络来学习特征的表示,而不是程序员去设计它们。 所以为什么不把词嵌入只当做模型参数,通过训练来更新呢? 这就才是我们要确切做的事。我们将用神经网络做一些潜在语义属性,但是原则上,学习才是关键。 注意,词嵌入可能无法解释。就是说,尽管使用我们上面手动制作的向量,能够发现数学家和物理学家都喜欢喝咖啡的相似性, 如果我们允许神经网络来学习词嵌入,那么就会发现数学家和物理学家在第二维度有个较大的值,它所代表的含义很不清晰。 它们在一些潜在语义上是相似的,但是对我们来说无法解释。

总结一下,词嵌入是单词语义的表示,有效地编码语义信息可能与手头的任务有关。你也可以嵌入其他的东西:语音标签,解析树,其他任何东西!特征嵌入是这个领域的核心思想。

 

  • PyTorch中的词嵌入

在我们举例或练习之前,这里有一份关于如何在Pytorch和常见的深度学习中使用词嵌入的简要介绍。 与制作one-hot向量时对每个单词定义一个特殊的索引类似,当我们使用词向量时也需要为每个单词定义一个索引。这些索引将是查询表的关键点。意思就是,词嵌入被存储在一个的向量中,其中是词嵌入的维度,|V|是我们构建的词典大小。词被被分配的索引i,表示在向量的第i行存储它的嵌入。在所有的代码中,从单词到索引的映射是一个叫word_to_ix的字典。

能使用词嵌入的模块是torch.nn.Embedding,这里面有两个参数:词汇表的大小和词嵌入的维度。

索引这张表时,你必须使用torch.LongTensor(因为索引是整数,不是浮点数)。

# 作者: Robert Guthrie

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)

 

  • 例子:N-Gram语言模型

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

是单词序列的第i个单词。 在本例中,我们将在训练样例上计算损失函数,并且用反向传播算法更新参数。

CONTEXT_SIZE = 2
EMBEDDING_DIM = 10
# 我们用莎士比亚的十四行诗 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()
# 应该对输入变量进行标记,但暂时忽略。
# 创建一系列的元组,每个元组都是([ word_i-2, word_i-1 ], target word)的形式。 三元组 3-gram
trigrams = [([test_sentence[i], test_sentence[i + 1]], test_sentence[i + 2])
            for i in range(len(test_sentence) - 2)]
# 输出前3行,先看下是什么样子。
print(trigrams[:3])

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 = 0
    for context, target in trigrams:

        # 步骤 1\. 准备好进入模型的数据 (例如将单词转换成整数索引,并将其封装在tensor中)
        context_idxs = torch.tensor([word_to_ix[w] for w in context], dtype=torch.long)

        # 步骤 2\. 回调torch累乘梯度
        # 在传入一个新实例之前,需要把旧实例的梯度置零。
        model.zero_grad()

        # 步骤 3\. 继续运行代码,得到单词的log概率值。
        log_probs = model(context_idxs)

        # 步骤 4\. 计算损失函数(再次注意,Torch需要将目标单词封装在tensor里)。
        loss = loss_function(log_probs, torch.tensor([word_to_ix[target]], dtype=torch.long))

        # 步骤 5\. 反向传播更新梯度
        loss.backward()
        optimizer.step()

        # 通过调tensor.item()得到单个Python数值。
        total_loss += loss.item()
    losses.append(total_loss)
print(losses)  # 用训练数据每次迭代,损失函数都会下降。

 

  • 练习:计算连续词袋模型的词向量

连续词袋模型(CBOW)在NLP深度学习中使用很频繁。它是一个模型,尝试通过目标词前后几个单词的文本,来预测目标词。这有别于语言模型,因为CBOW不是序列的,也不必是概率性的。 CBOW常用于快速地训练词向量,得到的嵌入用来初始化一些复杂模型的嵌入。通常情况下,这被称为预训练嵌入。 它几乎总能帮忙把模型性能提升几个百分点。

CBOW模型如下所示: 给定一个单词 ,代表两边的滑窗距,如,并将所有的上下文词统称为 ,CBOW试图最小化

其中是单词的嵌入。

Pytorch中,通过填充下面的类来实现这个模型,有两条需要注意:

1)考虑下你需要定义哪些参数。

2)确保你知道每步操作后的结构,如果想重构,请使用.view()

CONTEXT_SIZE = 2  # 左右各两个词
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()

# 通过对`raw_text`使用set()函数,我们进行去重操作
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])

class CBOW(nn.Module):

    def __init__(self):
        pass

    def forward(self, inputs):
        pass

# 创建模型并且训练。这里有些函数帮你在使用模块之前制作数据。

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

4. 序列模型和LSTM网络

之前我们已经学过了许多的前馈网络. 所谓前馈网络, 就是网络中不会保存状态. 然而有时 这并不是我们想要的效果. 在自然语言处理 (NLP, Natural Language Processing) 中, 序列模型是一个核心的概念. 所谓序列模型, 即输入依赖于时间信息的模型. 一个典型的序列模型是隐马尔科夫模型 (HMM, Hidden Markov Model). 另一个序列模型的例子是条件随机场 (CRF, Conditional Random Field).

循环神经网络是指可以保存某种状态的神经网络. 比如说, 神经网络中上个时刻的输出可以作为下个 时刻的输入的一部分, 以此信息就可以通过序列在网络中一直往后传递. 对于LSTM (Long-Short Term Memory) 来说, 序列中的每个元素都有一个相应的隐状态 $h_t$, 该隐状态 原则上可以包含序列当前结点之前的任一节点的信息. 我们可以使用隐藏状态来预测语言模型 中的单词, 词性标签以及其他各种各样的东西.

  • PyTorch中的LSTM

在正式学习之前,有几个点要说明一下,Pytorch中LSTM的输入形式是一个3D的Tensor,每一个维度都有重要的意义,第一个维度就是序列本身,第二个维度是mini-batch中实例的索引,第三个维度是输入元素的索引,我们之前没有接触过mini-batch,所以我们就先忽略它并假设第二维的维度是1。

如果要用"The cow jumped"这个句子来运行一个序列模型,那么就应该把它整理成如下的形式:

PyTorch中文教程 | (15) 在深度学习和NLP中使用PyTorch_第19张图片

除了有一个额外的大小为1的第二维度.

此外, 你还可以向网络逐个输入序列, 在这种情况下, 第一个轴的大小也是1.

来看一个简单的例子.

lstm = nn.LSTM(3, 3)  # 输入维度为3维,输出维度为3维
inputs = [torch.randn(1, 3) for _ in range(5)]  # 生成一个长度为5的序列 [5,1,3]

# 初始化隐藏状态.
hidden = (torch.randn(1, 1, 3),
          torch.randn(1, 1, 3))
for i in inputs:
    # 将序列中的元素逐个输入到LSTM.
    # 经过每步操作,hidden 的值包含了隐藏状态的信息.
    out, hidden = lstm(i.view(1, 1, -1), hidden)

# 另外我们可以对一整个序列进行训练.
# LSTM第一个返回的第一个值是所有时刻的隐藏状态
# 第二个返回值是最后一个时刻的隐藏状态
#(所以"out"的最后一个和"hidden"是一样的)
# 之所以这样设计:
# 通过"out"你能取得任何一个时刻的隐藏状态,而"hidden"的值是用来进行序列的反向传播运算, 具体方式就是将它作为参数传入后面的 LSTM 网络.

# 增加额外的第二个维度.
inputs = torch.cat(inputs).view(len(inputs), 1, -1) #5,3 -> 5,1,3
hidden = (torch.randn(1, 1, 3), torch.randn(1, 1, 3))  # 清空隐藏状态. 
out, hidden = lstm(inputs, hidden)
print(out)
print(hidden)

PyTorch中文教程 | (15) 在深度学习和NLP中使用PyTorch_第20张图片

  • 例子:用LSTM来进行词性标注

在这部分, 我们将会使用一个 LSTM 网络来进行词性标注. 在这里我们不会用到维特比算法, 前向-后向算法或者任何类似的算法,而是将这部分内容作为一个 (有挑战) 的练习留给读者, 希望读者在了解了这部分的内容后能够实现如何将维特比算法应用到 LSTM 网络中来.

该模型如下:输入的句子是w1,...,wM​对应的词性为y1,...,yM​ ,用y^i​表示对单词wi​词性的预测,标签的集合定义为T。

这是一个结构预测模型, 我们的输出是一个序列y^1,...,y^M, 其中y^i∈T.

在进行预测时, 需将句子每个词输入到一个 LSTM 网络中. 将时刻i​的隐藏状态标记为hi​,同样地, 对每个标签赋一个独一无二的索引 (类似 word embeddings 部分 word_to_ix 的设置). 然后就得到了y^i​的预测规则。

PyTorch中文教程 | (15) 在深度学习和NLP中使用PyTorch_第21张图片

即先对隐状态进行一个仿射变换, 然后计算一个对数 softmax, 最后得到的预测标签即为对数 softmax 中最大的值对应的标签. 注意, 这也意味着 A 空间的维度是 |T|​.

准备数据:

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)
tag_to_ix = {"DET": 0, "NN": 1, "V": 2}

# 实际中通常使用更大的维度如32维, 64维.
# 这里我们使用小的维度, 为了方便查看训练过程中权重的变化.
EMBEDDING_DIM = 6
HIDDEN_DIM = 6

创建模型:

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)

        # LSTM以word_embeddings作为输入, 输出维度为 hidden_dim 的隐藏状态值
        self.lstm = nn.LSTM(embedding_dim, hidden_dim)

        # 线性层将隐藏状态空间映射到标注空间
        self.hidden2tag = nn.Linear(hidden_dim, tagset_size)
        self.hidden = self.init_hidden()

    def init_hidden(self):
        # 一开始并没有隐藏状态所以我们要先初始化一个
        # 关于维度为什么这么设计请参考Pytoch相关文档
        # 各个维度的含义是 (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

训练模型:

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)

# 查看训练前的分数
# 注意: 输出的 i,j 元素的值表示单词 i 的 j 标签的得分
# 这里我们不需要训练不需要求导,所以使用torch.no_grad()
with torch.no_grad():
    inputs = prepare_sequence(training_data[0][0], word_to_ix)
    tag_scores = model(inputs)
    print(tag_scores)

for epoch in range(300):  # 实际情况下你不会训练300个周期, 此例中我们只是随便设了一个值
    for sentence, tags in training_data:
        # 第一步: 请记住Pytorch会累加梯度.
        # 我们需要在训练每个实例前清空梯度
        model.zero_grad()

        # 此外还需要清空 LSTM 的隐状态,
        # 将其从上个实例的历史中分离出来.
        model.hidden = model.init_hidden()

        # 准备网络输入, 将其变为词索引的 Tensor 类型数据
        sentence_in = prepare_sequence(sentence, word_to_ix)
        targets = prepare_sequence(tags, tag_to_ix)

        # 第三步: 前向传播.
        tag_scores = model(sentence_in)

        # 第四步: 计算损失和梯度值, 通过调用 optimizer.step() 来更新梯度
        loss = loss_function(tag_scores, targets)
        loss.backward()
        optimizer.step()

# 查看训练后的得分
with torch.no_grad():
    inputs = prepare_sequence(training_data[0][0], word_to_ix)
    tag_scores = model(inputs)

    # 句子是 "the dog ate the apple", i,j 表示对于单词 i, 标签 j 的得分.
    # 我们采用得分最高的标签作为预测的标签. 从下面的输出我们可以看到, 预测得
    # 到的结果是0 1 2 0 1. 因为 索引是从0开始的, 因此第一个值0表示第一行的
    # 最大值, 第二个值1表示第二行的最大值, 以此类推. 所以最后的结果是 DET
    # NOUN VERB DET NOUN, 整个序列都是正确的!
    print(tag_scores)

PyTorch中文教程 | (15) 在深度学习和NLP中使用PyTorch_第22张图片

 

  • 练习:使用字符级特征来增强LSTM词性标注器

在上面的例子中, 每个词都有一个词嵌入, 作为序列模型的输入. 接下来让我们使用每个的单词的 字符级别的表达来增强词嵌入。 我们期望这个操作对结果能有显著提升, 因为像词缀这样的字符级 信息对于词性有很大的影响。比如说, 像包含词缀 -ly 的单词基本上都是被标注为副词.

具体操作如下. 用cw​的字符级表达, 同之前一样, 我们使用xw​来表示词嵌入. 序列模型的输入就变成了xw和cw​的拼接. 因此, 如果 的维度xw​是5, 的维度cw​是3, 那么我们的 LSTM 网络的输入维度大小就是8.

为了得到字符级别的表达, 将单词的每个字符输入一个 LSTM 网络, 而cw则为这个 LSTM 网络最后的隐状态。 一些提示:

1)新模型中需要两个 LSTM, 一个跟之前一样, 用来输出词性标注的得分, 另外一个新增加的用来 获取每个单词的字符级别表达。

2)为了在字符级别上运行序列模型, 你需要用嵌入的字符来作为字符 LSTM 的输入。

 

5. 高级:制定动态决策和Bi-LSTM CRF

  • 动态与静态深度学习工具包

Pytorch是一种动态神经网络套件。另一个动态套件的例子是 Dynet (我之所以提到这一点,因为与Pytorch和Dynet一起使用是相似的。如果你在Dynet中看到一个例子,它可能会帮助你在Pytorch中实现它)。相反的是静态工具包,其中包括Theano,Keras,TensorFlow等。核心区别如下:

1)在静态工具箱中,您可以定义一次计算图,对其进行编译,然后将实例流式传输给它。

2)在动态工具箱中,为每个实例定义计算图。它永远不会被编译并且是即时执行的

没有很多经验,很难理解差异。一个例子是假设我们想要构建一个深层组成解析器。假设我们的模型大致涉及以下步骤:

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(PyTorch中文教程,PyTorch中文教程,NLP,序列模型)