词嵌入简单入门教程

本文谈论了“词嵌入”概念,嵌入主要分词语嵌入和句子嵌入两种。

前文阐述了什么是嵌入以及如何使用嵌入,那么就让我们对嵌入进行更深入的研究吧!

词语嵌入和句子嵌入是自然语言处理(NLP)领域中两个相关但不同的概念。

  1. 词语嵌入(Word Embedding): 这是指将单个词语映射到向量空间的过程。通过词嵌入,每个单词都被表示为一个多维向量,使得语义相近的词在向量空间中的距离也相近。Word2Vec、GloVe和FastText是常见的词嵌入模型,它们通过大量文本数据学习词语之间的语义关系。
  2. 句子嵌入(Sentence Embedding): 与词语嵌入类似,句子嵌入是将整个句子映射到向量空间的过程。目标是捕捉整个句子的语义信息。不同于简单地将单词向量进行加和或平均,句子嵌入方法通常考虑了句子内单词的顺序和上下文关系。有一些模型专门设计用于生成句子嵌入,如InferSent、Universal Sentence Encoder等。

虽然词语嵌入和句子嵌入都涉及将自然语言映射到向量空间,但它们的应用场景和建模目标是不同的。词语嵌入更关注单词级别的语义关系,而句子嵌入更关注整个句子的语义信息。在某些任务中,可以将单词嵌入组合成句子嵌入,但并不意味着它们是相同的概念。

Word2Vec 和 GloVe
神经网络,例如 BERT,不能直接处理单词;他们需要数字。而提供单词的方式就是将它们表示为向量,也称为词嵌入。

在传统设置中,您定义一个词汇表(允许哪些单词),然后该词汇表中的每个单词都有一个指定的嵌入。不在词汇表中的单词被映射到一个特殊的标记,通常称为(训练期间未找到的单词的标准占位符)。

在实践中,嵌入是学习的。这是Word2VecGloVe等方法的主要思想。他们学习语料库中单词的嵌入,使得出现在相似上下文中的单词具有相似的嵌入。例如,“king”和“queen”的嵌入是相似的,因为它们出现在相似的上下文中。

词嵌入的图像:
一些开源库,例如 Gensim 和 fastText,允许您快速获得预先训练的 Word2Vec 和 GloVe 嵌入。在 NLP 的美好时光(2013 年),人们使用这些模型来计算词嵌入,这对于其他模型的输入很有帮助。例如,您可以计算句子中每个单词的单词嵌入,然后将其作为输入传递给 sci-kit 学习分类器,以对句子的情感进行分类。

Glove 和 Word2Vec 有固定的表示。一旦训练完成,每个单词都会被分配一个固定的向量表示,无论其上下文如何(因此“bank”和“river bank河岸”中的“bank银行”将具有相同的嵌入)。Word2vec 和 GloVe 将难以处理具有多重含义的单词。

使用 Transformer 进行词嵌入
最近,随着Transformer 的出现,我们有了计算嵌入的新方法。嵌入也是学习的,但是 Transformer 不是训练一个嵌入模型,然后针对特定任务训练另一个模型,而是在其任务的上下文中学习有用的嵌入。例如,流行的 Transformer 模型 BERT 在掩码语言模型(预测要填空的单词)和下一个句子预测(句子 B 是否在句子 A 之后)的背景下学习单词嵌入。

Transformer 在许多 NLP 任务中都是最先进的,并且能够捕获 word2vec 和 GloVe 无法捕获的上下文信息,这要归功于一种称为注意力的机制。

注意力使模型能够权衡其他单词的重要性并捕获上下文信息。例如,在“我去银行bank存钱”这句话中,“银行”一词是有歧义的。是河岸river bank还是储蓄银行bank?

该模型可以使用“存款deposit”一词来理解它是一家储蓄银行。这些是上下文嵌入——它们的词嵌入可以根据周围的词而有所不同。

代码
让我们使用一个预先训练好的转换器模型(基于 bert-base-uncased),获取一些单词嵌入。为此,我们将使用转换器库。首先,让我们加载模型及其标记器

from transformers import AutoModel, AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
model = AutoModel.from_pretrained(
"bert-base-uncased")

到目前为止,我们还没有谈到标记化。到目前为止,我们一直假设我们将数据分割成单词。

在使用转换器时,我们将文本划分为标记。例如,"banking"(银行)一词可以分成 "bank "和 "ing "两个标记。标记化器负责将数据分割成标记,它分割数据的方式是针对特定模型的,是一个确定性的学习过程,这意味着同一个单词将始终被分割成相同的标记。让我们看看代码中是什么样子的:

text = "The king and the queen are happy."
tokenizer.tokenize(text, add_special_tokens=True)

输出:['[CLS]', 'the', 'king', 'and', 'the', 'queen', 'are', 'happy', '.', '[SEP]']

在这个例子中,每个单词都是一个标记!(情况并非总是如此,我们很快就会看到)。

但是,我们还看到了两个可能出乎意料的东西:[CLS] 和 [SEP]:[CLS] 和 [SEP]。这是添加到句子开头和结尾的特殊标记。之所以使用这些标记,是因为 BERT 在训练时使用了这种格式。BERT 的训练目标之一是预测下一个句子,这意味着它被训练来预测两个句子是否连续。CLS]标记代表整个句子,而[SEP]标记则分隔句子。这一点在我们以后讨论句子嵌入时会很有趣。

现在我们来获取每个标记的嵌入。

encoded_input = tokenizer(text, return_tensors="pt")
output = model(**encoded_input)
output[
"last_hidden_state"].shape

输出:torch.Size([1, 10, 768])

太好了!BERT 为每个标记提供了 768 个嵌入值。每个标记都有语义信息--它们捕捉了单词在句子上下文中的含义。让我们看看 "king "一词在这种语境中的嵌入值是否与 "queen "中的嵌入值相似。

king_embedding = output["last_hidden_state"][0][2]  # 2 is the position of king
queen_embedding = output[
"last_hidden_state"][0][5]  # 5 is the position of queen
print(f
"Shape of embedding {king_embedding.shape}")
print(
    f
"Similarity between king and queen embedding {util.pytorch_cos_sim(king_embedding, queen_embedding)[0][0]}"
)

输出:
Shape of embedding torch.Size([768])
Similarity between king and queen embedding 0.7920711040496826

好吧,在这种情况下,它们似乎很相似!现在让我们来看看 "happy"这个词。

happy_embedding = output.last_hidden_state[0][7]  # happy
util.pytorch_cos_sim(king_embedding, happy_embedding)

tensor([[0.5239]], grad_fn=<MmBackward0>)

这是有道理的;queen 嵌入比happy嵌入更类似于king 。

同一个词在不同的语境中会有不同的值
现在,让我们来看看同一个词在不同的语境中会有不同的值:

text = "The angry and unhappy king"
encoded_input = tokenizer(text, return_tensors=
"pt")
output = model(**encoded_input)
output[
"last_hidden_state"].shape

 输出:torch.Size([1, 7, 768])

tokenizer.tokenize(text, add_special_tokens=True)

输出:['[CLS]', 'the', 'angry', 'and', 'unhappy', 'king', '[SEP]']

king_embedding_2 = output["last_hidden_state"][0][5]
util.pytorch_cos_sim(king_embedding, king_embedding_2)

输出:tensor([[0.5740]], grad_fn=<MmBackward0>)

虽然这两种嵌入似乎都对应于 "国王king "嵌入,但它们在向量空间中却大相径庭。这是怎么回事呢?请记住,这些都是上下文嵌入。第一句话的上下文相当积极,而第二句话(The angry and unhappy king)则相当消极。因此,嵌入是不同的。

前面,我们讨论了标记符如何将一个词拆分成多个标记符。一个合理的问题是,在这种情况下我们如何获得词嵌入。让我们来看一个长词 "tokenization "的例子。

tokenizer.tokenize("tokenization")
输出:
['token', '#ization']

"标记化 "一词被拆分为两个标记,但我们关心的是 "标记化 "的嵌入!怎么办呢?我们可以采用汇集策略,获取每个标记的嵌入,然后求平均值,得到单词的嵌入。让我们试试看!

和之前一样,我们首先对测试进行标记化,然后通过模型运行标记 ID。

text = "this is about tokenization"

encoded_input = tokenizer(text, return_tensors=
"pt")
output = model(**encoded_input)

让我们来看看句子的标记化:

tokenizer.tokenize(text, add_special_tokens=True)

输出:['[CLS]', 'this', 'is', 'about', 'token', '#ization', '[SEP]']

因此,我们希望通过求平均值的方法,将标记 4 和标记 5 的嵌入信息集合起来。首先,我们来获取标记的嵌入。

word_token_indices = [4, 5]
word_embeddings = output["last_hidden_state"][0, word_token_indices]
word_embeddings.shape

输出:torch.Size([2, 768])

现在我们用 torch.mean 求平均值。

import torch

torch.mean(word_embeddings, dim=0).shape

输出:torch.Size([768])

让我们用一个函数将其全部封装起来,以便以后使用。

def get_word_embedding(text, word):
        # 对文本进行编码,并通过模型进行前向传递,以获取隐藏状态
    encoded_input = tokenizer(text, return_tensors="pt")
    with torch.no_grad():  # We don't need gradients for embedding extraction
        output = model(**encoded_input)

    # 查找单词的索引
    word_ids = tokenizer.encode(
        word, add_special_tokens=False
    )   # 不再有特殊标记
    word_token_indices = [
        i
        for i, token_id in enumerate(encoded_input["input_ids"][0])
        if token_id in word_ids
    ]

      # 计算单词的内嵌值
    word_embeddings = output["last_hidden_state"][0, word_token_indices]
    return torch.mean(word_embeddings, dim=0)

例 1.在国王和王后都生气的情况下,国王和王后嵌入的相似性。

util.pytorch_cos_sim(
    get_word_embedding("The king is angry", "king"),
    get_word_embedding(
"The queen is angry", "queen"),
)

输出:tensor([[0.8564]])

例 2.国王和王后嵌入式在国王高兴和王后生气时的相似性。请注意它们的相似度比上一个例子要低。

util.pytorch_cos_sim(
    get_word_embedding("The king is happy", "king"),
    get_word_embedding(
"The queen is angry", "queen"),
)

输出:tensor([[0.8273]])

例 3.两种截然不同的语境下国王嵌入词的相似性。即使是同一个单词,不同的上下文语境也会使嵌入结果大相径庭。

# This is same as before
util.pytorch_cos_sim(
    get_word_embedding("The king and the queen are happy.", "king"),
    get_word_embedding(
"The angry and unhappy king", "king"),
)

输出:tensor([[0.5740]])

例 4.具有两种不同含义的词之间的相似性。银行 "一词含义模糊,可以是河岸,也可以是储蓄银行。根据语境的不同,嵌入效果也不同。

util.pytorch_cos_sim(
    get_word_embedding("The river bank", "bank"),
    get_word_embedding(
"The savings bank", "bank"),
)

tensor([[0.7587]])

我希望这能让大家了解什么是词嵌入。既然我们已经了解了单词嵌入,那么让我们来看看句子嵌入!