• 教程 >
  • 词嵌入:编码词汇语义
快捷方式

词嵌入:编码词汇语义

词嵌入是实数的稠密向量,词汇表中的每个词对应一个向量。在 NLP 中,特征几乎总是词!但是,您应该如何在计算机中表示一个词呢?您可以存储其 ASCII 字符表示,但这只能告诉您这个词是什么,并不能说明其含义(您或许可以从其前缀推导出其词性,或从其大小写推导出属性,但信息有限)。更重要的是,在何种意义上可以组合这些表示?我们通常希望神经网络的输出是稠密的,其中输入是 \(|V|\) 维的,其中 \(V\) 是我们的词汇表,但输出通常只有几维(例如,如果我们只预测少数几个标签)。我们如何从一个巨大的维度空间转换到一个较小的维度空间?

除了使用 ASCII 表示之外,我们还可以使用独热编码?也就是说,我们用以下方式表示词 \(w\)

\[\overbrace{\left[ 0, 0, \dots, 1, \dots, 0, 0 \right]}^\text{|V| 个元素} \]

其中 1 位于 \(w\) 独有的位置。任何其他词都将在其他某个位置具有 1,而在其他所有位置都为 0。

除了表示规模巨大之外,这种表示还存在一个巨大的缺点。它基本上将所有词视为独立的实体,彼此之间没有任何关系。我们真正想要的是词之间的一些相似性的概念。为什么?让我们看一个例子。

假设我们正在构建一个语言模型。假设我们看到了以下句子:

  • 数学家跑到商店。

  • 物理学家跑到商店去了。

  • 数学家解决了那个开放性问题。

在我们的训练数据中。现在假设我们得到一个新的句子,这个句子以前从未在我们的训练数据中出现过

  • 物理学家解决了那个开放性问题。

我们的语言模型在这个句子上可能表现不错,但如果我们能够利用以下两个事实,会不会更好得多呢?

  • 我们曾在句子中看到过数学家和物理学家担任相同的角色。他们在某种程度上存在语义关系。

  • 我们在这个新的未见句子中看到过数学家担任与我们现在看到的物理学家相同的角色。

然后推断出物理学家实际上在新的未见句子中是一个很好的选择?这就是我们所说的相似性概念:我们指的是语义相似性,而不仅仅是具有相似的拼写表示。这是一种通过连接我们已经看到的内容和我们没有看到的内容来对抗语言数据稀疏性的技术。当然,这个例子依赖于一个基本的语言学假设:在相似上下文中出现的单词在语义上彼此相关。这被称为分布式假设

获取密集词嵌入

我们如何解决这个问题?也就是说,我们如何真正将语义相似性编码到单词中?也许我们会想出一些语义属性。例如,我们看到数学家和物理学家都能跑步,所以也许我们会给这些词在“能够跑步”的语义属性上打高分。想想其他一些属性,并想象一下你可能会在这些属性上给一些常用词打多少分。

如果每个属性都是一个维度,那么我们可能会给每个单词一个向量,像这样

\[ q_\text{mathematician} = \left[ \overbrace{2.3}^\text{能跑}, \overbrace{9.4}^\text{喜欢咖啡}, \overbrace{-5.5}^\text{物理专业}, \dots \right]\]
\[ q_\text{physicist} = \left[ \overbrace{2.5}^\text{能跑}, \overbrace{9.1}^\text{喜欢咖啡}, \overbrace{6.4}^\text{物理专业}, \dots \right]\]

然后我们可以通过以下方式获得这两个词之间的相似度度量

\[\text{Similarity}(\text{physicist}, \text{mathematician}) = q_\text{physicist} \cdot q_\text{mathematician} \]

尽管更常见的是按长度进行归一化

\[ \text{Similarity}(\text{physicist}, \text{mathematician}) = \frac{q_\text{physicist} \cdot q_\text{mathematician}} {\| q_\text{physicist} \| \| q_\text{mathematician} \|} = \cos (\phi)\]

其中\(\phi\)是两个向量之间的夹角。这样,非常相似的词(其嵌入指向相同方向的词)的相似度将为1。非常不相似的词的相似度应该为-1。

你可以将本节开头的一热向量视为这些我们新定义的向量的一个特例,其中每个单词基本上都具有相似度0,并且我们给每个单词赋予了一些独特的语义属性。这些新向量是密集的,也就是说它们的条目(通常)是非零的。

但是这些新向量很麻烦:你可以想到数千种不同的语义属性可能与确定相似性相关,那么你究竟如何设置不同属性的值呢?深度学习的核心思想是神经网络学习特征的表示,而不是要求程序员自己设计它们。所以为什么不让词嵌入成为我们模型中的参数,然后在训练期间进行更新呢?这正是我们将要做的。我们将有一些潜在语义属性,网络原则上可以学习这些属性。请注意,词嵌入可能无法解释。也就是说,虽然在我们上面手工制作的向量中,我们可以看到数学家和物理学家在他们都喜欢咖啡方面是相似的,但如果我们允许神经网络学习嵌入并看到数学家和物理学家在第二维上都具有较大的值,则不清楚这意味着什么。他们在某个潜在的语义维度上是相似的,但这可能对我们没有意义。

总之,词嵌入是单词*语义*的表示,有效地编码了可能与手头任务相关的语义信息。你也可以嵌入其他东西:词性标签、句法树,任何东西!特征嵌入的思想是该领域的中心。

Pytorch中的词嵌入

在我们进入一个示例和一个练习之前,关于如何在Pytorch以及在深度学习编程中一般使用嵌入的一些快速说明。类似于我们在制作一热向量时为每个单词定义了一个唯一的索引,我们也需要在使用嵌入时为每个单词定义一个索引。这些将是查找表中的键。也就是说,嵌入存储为一个\(|V| \times D\)矩阵,其中\(D\)是嵌入的维度,使得分配索引\(i\)的单词将其嵌入存储在矩阵的第\(i\)行中。在我的所有代码中,从单词到索引的映射是一个名为word_to_ix的字典。

允许你使用嵌入的模块是torch.nn.Embedding,它接受两个参数:词汇量大小和嵌入的维度。

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

# 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 0x7f1c952e3950>
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]],
       grad_fn=<EmbeddingBackward0>)

一个例子:N-Gram语言建模

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

\[P(w_i | w_{i-1}, w_{i-2}, \dots, w_{i-n+1} ) \]

其中\(w_i\)是序列的第i个单词。

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

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-CONTEXT_SIZE, ..., word_i-1 ], target word)
ngrams = [
    (
        [test_sentence[i - j - 1] for j in range(CONTEXT_SIZE)],
        test_sentence[i]
    )
    for i in range(CONTEXT_SIZE, len(test_sentence))
]
# Print the first 3, just so you can see what they look like.
print(ngrams[: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 ngrams:

        # Step 1. Prepare the inputs to be passed to the model (i.e, turn the words
        # into integer indices and wrap them in tensors)
        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 tensor)
        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!

# To get the embedding of a particular word, e.g. "beauty"
print(model.embeddings.weight[word_to_ix["beauty"]])
[(['forty', 'When'], 'winters'), (['winters', 'forty'], 'shall'), (['shall', 'winters'], 'besiege')]
[518.6696996688843, 516.121301651001, 513.5881462097168, 511.0691747665405, 508.56321573257446, 506.06897497177124, 503.5867495536804, 501.11545610427856, 498.6512076854706, 496.1948835849762]
tensor([ 0.7891,  1.3691, -0.8514,  0.5135,  0.3350,  1.1081,  0.1861, -0.2758,
        -0.6109,  0.8172], grad_fn=<SelectBackward0>)

练习:计算词嵌入:连续词袋

连续词袋模型(CBOW)在NLP深度学习中经常使用。它是一个试图根据目标词之前和之后的几个词的上下文来预测单词的模型。这与语言建模不同,因为CBOW不是顺序的,也不必是概率性的。通常,CBOW用于快速训练词嵌入,并且这些嵌入用于初始化更复杂模型的嵌入。通常,这被称为预训练嵌入。它几乎总是可以提高几个百分点的性能。

CBOW模型如下。给定一个目标词\(w_i\)和每侧一个\(N\)上下文窗口,\(w_{i-1}, \dots, w_{i-N}\)\(w_{i+1}, \dots, w_{i+N}\),将所有上下文词统称为\(C\),CBOW试图最小化

\[-\log p(w_i | C) = -\log \text{Softmax}\left(A(\sum_{w \in C} q_w) + b\right) \]

其中\(q_w\)是单词\(w\)的嵌入。

通过填写下面的类在Pytorch中实现此模型。一些提示

  • 考虑你需要定义哪些参数。

  • 确保你知道每个操作期望的形状。如果需要重塑,请使用.view()。

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(CONTEXT_SIZE, len(raw_text) - CONTEXT_SIZE):
    context = (
        [raw_text[i - j - 1] for j in range(CONTEXT_SIZE)]
        + [raw_text[i + j + 1] for j in range(CONTEXT_SIZE)]
    )
    target = raw_text[i]
    data.append((context, target))
print(data[:5])


class CBOW(nn.Module):

    def __init__(self):
        pass

    def forward(self, inputs):
        pass

# 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
[(['are', 'We', 'to', 'study'], 'about'), (['about', 'are', 'study', 'the'], 'to'), (['to', 'about', 'the', 'idea'], 'study'), (['study', 'to', 'idea', 'of'], 'the'), (['the', 'study', 'of', 'a'], 'idea')]

tensor([39, 14,  0, 47])

脚本的总运行时间:(0分钟0.861秒)

由Sphinx-Gallery生成的图库

文档

访问PyTorch的全面开发者文档

查看文档

教程

获取初学者和高级开发人员的深入教程

查看教程

资源

查找开发资源并获得问题的解答

查看资源