词嵌入简单入门教程
本文谈论了“词嵌入”概念,嵌入主要分词语嵌入和句子嵌入两种。
前文阐述了什么是嵌入以及如何使用嵌入,那么就让我们对嵌入进行更深入的研究吧!
词语嵌入和句子嵌入是自然语言处理(NLP)领域中两个相关但不同的概念。
- 词语嵌入(Word Embedding): 这是指将单个词语映射到向量空间的过程。通过词嵌入,每个单词都被表示为一个多维向量,使得语义相近的词在向量空间中的距离也相近。Word2Vec、GloVe和FastText是常见的词嵌入模型,它们通过大量文本数据学习词语之间的语义关系。
- 句子嵌入(Sentence Embedding): 与词语嵌入类似,句子嵌入是将整个句子映射到向量空间的过程。目标是捕捉整个句子的语义信息。不同于简单地将单词向量进行加和或平均,句子嵌入方法通常考虑了句子内单词的顺序和上下文关系。有一些模型专门设计用于生成句子嵌入,如InferSent、Universal Sentence Encoder等。
虽然词语嵌入和句子嵌入都涉及将自然语言映射到向量空间,但它们的应用场景和建模目标是不同的。词语嵌入更关注单词级别的语义关系,而句子嵌入更关注整个句子的语义信息。在某些任务中,可以将单词嵌入组合成句子嵌入,但并不意味着它们是相同的概念。
Word2Vec 和 GloVe
神经网络,例如 BERT,不能直接处理单词;他们需要数字。而提供单词的方式就是将它们表示为向量,也称为词嵌入。
在传统设置中,您定义一个词汇表(允许哪些单词),然后该词汇表中的每个单词都有一个指定的嵌入。不在词汇表中的单词被映射到一个特殊的标记,通常称为(训练期间未找到的单词的标准占位符)。
在实践中,嵌入是学习的。这是Word2Vec和GloVe等方法的主要思想。他们学习语料库中单词的嵌入,使得出现在相似上下文中的单词具有相似的嵌入。例如,“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 |
到目前为止,我们还没有谈到标记化。到目前为止,我们一直假设我们将数据分割成单词。
在使用转换器时,我们将文本划分为标记。例如,"banking"(银行)一词可以分成 "bank "和 "ing "两个标记。标记化器负责将数据分割成标记,它分割数据的方式是针对特定模型的,是一个确定性的学习过程,这意味着同一个单词将始终被分割成相同的标记。让我们看看代码中是什么样子的:
text = "The king and the queen are happy." |
输出:['[CLS]', 'the', 'king', 'and', 'the', 'queen', 'are', 'happy', '.', '[SEP]']
在这个例子中,每个单词都是一个标记!(情况并非总是如此,我们很快就会看到)。
但是,我们还看到了两个可能出乎意料的东西:[CLS] 和 [SEP]:[CLS] 和 [SEP]。这是添加到句子开头和结尾的特殊标记。之所以使用这些标记,是因为 BERT 在训练时使用了这种格式。BERT 的训练目标之一是预测下一个句子,这意味着它被训练来预测两个句子是否连续。CLS]标记代表整个句子,而[SEP]标记则分隔句子。这一点在我们以后讨论句子嵌入时会很有趣。
现在我们来获取每个标记的嵌入。
encoded_input = tokenizer(text, return_tensors="pt") |
输出:torch.Size([1, 10, 768])
太好了!BERT 为每个标记提供了 768 个嵌入值。每个标记都有语义信息--它们捕捉了单词在句子上下文中的含义。让我们看看 "king "一词在这种语境中的嵌入值是否与 "queen "中的嵌入值相似。
king_embedding = output["last_hidden_state"][0][2] # 2 is the position of king |
输出:
Shape of embedding torch.Size([768])
Similarity between king and queen embedding 0.7920711040496826
好吧,在这种情况下,它们似乎很相似!现在让我们来看看 "happy"这个词。
happy_embedding = output.last_hidden_state[0][7] # happy |
tensor([[0.5239]], grad_fn=<MmBackward0>)
这是有道理的;queen 嵌入比happy嵌入更类似于king 。
同一个词在不同的语境中会有不同的值
现在,让我们来看看同一个词在不同的语境中会有不同的值:
text = "The angry and unhappy king" |
输出: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] |
输出:tensor([[0.5740]], grad_fn=<MmBackward0>)
虽然这两种嵌入似乎都对应于 "国王king "嵌入,但它们在向量空间中却大相径庭。这是怎么回事呢?请记住,这些都是上下文嵌入。第一句话的上下文相当积极,而第二句话(The angry and unhappy king)则相当消极。因此,嵌入是不同的。
前面,我们讨论了标记符如何将一个词拆分成多个标记符。一个合理的问题是,在这种情况下我们如何获得词嵌入。让我们来看一个长词 "tokenization "的例子。
tokenizer.tokenize("tokenization")
输出:
['token', '#ization']
"标记化 "一词被拆分为两个标记,但我们关心的是 "标记化 "的嵌入!怎么办呢?我们可以采用汇集策略,获取每个标记的嵌入,然后求平均值,得到单词的嵌入。让我们试试看!
和之前一样,我们首先对测试进行标记化,然后通过模型运行标记 ID。
text = "this is about tokenization" |
让我们来看看句子的标记化:
tokenizer.tokenize(text, add_special_tokens=True) |
输出:['[CLS]', 'this', 'is', 'about', 'token', '#ization', '[SEP]']
因此,我们希望通过求平均值的方法,将标记 4 和标记 5 的嵌入信息集合起来。首先,我们来获取标记的嵌入。
word_token_indices = [4, 5] |
输出:torch.Size([2, 768])
现在我们用 torch.mean 求平均值。
import torch |
输出: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( |
输出:tensor([[0.8564]])
例 2.国王和王后嵌入式在国王高兴和王后生气时的相似性。请注意它们的相似度比上一个例子要低。
util.pytorch_cos_sim( |
输出:tensor([[0.8273]])
例 3.两种截然不同的语境下国王嵌入词的相似性。即使是同一个单词,不同的上下文语境也会使嵌入结果大相径庭。
# This is same as before |
输出:tensor([[0.5740]])
例 4.具有两种不同含义的词之间的相似性。银行 "一词含义模糊,可以是河岸,也可以是储蓄银行。根据语境的不同,嵌入效果也不同。
util.pytorch_cos_sim( |
tensor([[0.7587]])
我希望这能让大家了解什么是词嵌入。既然我们已经了解了单词嵌入,那么让我们来看看句子嵌入!