评价此页

词嵌入:编码词汇语义#

创建于:2017 年 4 月 8 日 | 最后更新:2021 年 9 月 14 日 | 最后验证:2024 年 11 月 5 日

词嵌入是实数的密集向量,词汇表中的每个词都有一个。在自然语言处理中,你的特征几乎总是词!但是如何在计算机中表示一个词呢?你可以存储它的 ASCII 字符表示,但这只能告诉你这个词*是什么*,它并没有太多关于它*意味着什么*的信息(你可能能够从它的词缀中推断出它的词性,或者从它的大小写中推断出属性,但不多)。更重要的是,你如何结合这些表示呢?我们通常希望从神经网络中得到密集输出,其中输入是 \(|V|\) 维的,其中 \(V\) 是我们的词汇表,但通常输出只有几维(例如,如果我们只预测少数几个标签)。我们如何从一个巨大维度的空间到一个更小维度的空间呢?

那么,不用 ASCII 表示,我们使用独热编码如何?也就是说,我们用以下方式表示词 \(w\)

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

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

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

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

  • 数学家跑去商店。

  • 物理学家跑去商店。

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

在我们的训练数据中。现在假设我们得到一个训练数据中从未见过的新句子:

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

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

  • 我们已经看到数学家和物理学家在句子中扮演相同的角色。某种程度上它们具有语义关系。

  • 我们已经看到数学家在这个新未见的句子中扮演的角色与我们现在看到的物理学家相同。

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

获取密集词嵌入#

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

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

\[ q_\text{数学家} = \left[ \overbrace{2.3}^\text{能跑}, \overbrace{9.4}^\text{喜欢咖啡}, \overbrace{-5.5}^\text{主修物理}, \dots \right]\]
\[ q_\text{物理学家} = \left[ \overbrace{2.5}^\text{能跑}, \overbrace{9.1}^\text{喜欢咖啡}, \overbrace{6.4}^\text{主修物理}, \dots \right]\]

然后我们可以通过以下方式测量这些词之间的相似性:

\[\text{相似度}(\text{物理学家}, \text{数学家}) = q_\text{物理学家} \cdot q_\text{数学家} \]

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

\[ \text{相似度}(\text{物理学家}, \text{数学家}) = \frac{q_\text{物理学家} \cdot q_\text{数学家}} {\| q_\text{物理学家} \| \| q_\text{数学家} \|} = \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 0x7f04593586b0>
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')]
[525.254486322403, 522.6922554969788, 520.1497712135315, 517.6249442100525, 515.1160230636597, 512.6223750114441, 510.14174938201904, 507.6777584552765, 505.22602367401123, 502.78510999679565]
tensor([-0.7833, -0.5278,  0.8206,  0.8639,  0.4921,  0.4451, -0.4796,  0.7547,
        -1.7967,  0.9074], grad_fn=<SelectBackward0>)

练习:计算词嵌入:连续词袋模型#

连续词袋模型(CBOW)在自然语言处理深度学习中经常使用。它是一个尝试根据目标词前后几个词的上下文来预测词的模型。这与语言建模不同,因为 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([38, 30, 45, 46])

脚本总运行时间:(0 分 0.579 秒)