自然语言处理(NLP)领域中的一个重要任务是理解和处理高维空间中的文本数据。通常,NLP任务会将文本转换为向量或数组形式,然后进行特定的变换。这种处理方式涉及到高维空间的魅力。在高层次上,这个过程并不复杂,但需要深入理解其细节,并在Python中实现它。
句子相似度是展示高维空间魔法的一个明显例子。基本思想是:将一个句子转换成向量,将其他句子也转换成向量,然后找出这些向量之间最短的距离(欧几里得距离)或最小的角度(余弦相似度)。这样就能获得句子之间语义相似度的标准。
BERT模型是NLP领域的一个特殊MVP(最有价值球员),其核心能力之一就是将词汇的本质嵌入到密集的向量中。称这些向量为密集向量,因为向量中的每个值都有其存在的意义,这与稀疏向量形成对比。BERT擅长生成这些密集向量,每个编码层都会输出一系列密集向量。
对于BERT模型的支持,这将是一个包含768个数字的向量。这些768个值构成了特定词汇的数学表示,可以将其视为上下文消息嵌入。每个词汇由编码器生成的单位向量实际上是一个张量(768乘以词汇的数量)。可以使用这些张量并将其转换成输入序列的语义设计。然后,可以使用相似度度量来衡量不同行之间的相似性。
为了将last_hidden_states张量转换成想要的向量,使用平均池化方法。这些512个词汇每个都有768个值。这个池化操作将取所有词汇嵌入的平均值,并将它们合并到一个独特的768向量空间中,产生一个“句子向量”。
已经讨论了理论基础和逻辑,但如何在实际中应用呢?将介绍两种方法——简单方法和稍微复杂一些的方法。
可以使用sentence-transformers库来执行刚刚提到的所有操作,这个库将大部分规则封装在几行代码中。首先,使用pip安装sentence-transformers库。这个库使用HuggingFace的transformers作为后端,因此可以在这里找到sentence-transformers模型。将使用best-base-no-mean-tokens模型,它执行之前讨论的逻辑(它也使用128个输入词汇,而不是512个)。
# 安装sentence-transformers库
pip install sentence-transformers
# 导入SentenceTransformer类
from sentence_transformers import SentenceTransformer
# 初始化模型
model = SentenceTransformer('bert-base-nli-mean-tokens')
# 编码句子
sen_embeddings = model.encode(sen)
# 查看编码结果的形状
sen_embeddings.shape
现在,拥有了四个句子的嵌入,每个嵌入包含768个值。接下来,可以使用这些嵌入来发现每对句子之间的余弦相似度。
# 计算句子0的余弦相似度
from sklearn.metrics.pairwise import cosine_similarity
# 计算余弦相似度
cosine_similarity(
[sentence_embeddings[0]],
sentence_embeddings[1:]
)
通过上述代码,可以得到句子之间的相似度指数。
在介绍第二种方法之前,值得注意的是,它与上述方法做相同的事情,但在更低的层次上。希望将last_hidden_state转换为产生句子嵌入的计划。为此,执行平均池化操作。在执行平均池化操作之前,需要设计last_hidden_state;以下是代码:
# 导入AutoTokenizer和AutoModel类
from transformers import AutoTokenizer, AutoModel
import torch
# 初始化模型和分词器
tokenizer = AutoTokenizer.from_pretrained('sentence-transformers/bert-base-nli-mean-tokens')
model = AutoModel.from_pretrained('sentence-transformers/bert-base-nli-mean-tokens')
# 分词句子
sent = [
"Three years later, the coffin was still full of Jello.",
"The fish dreamed of escaping the fishbowl and into the toilet where he saw his friend go.",
"The person box was packed with jelly many dozens of months later.",
"He found a leprechaun in his walnut shell."
]
# 初始化字典:存储分词后的句子
token = {'input_ids': [], 'attention_mask': []}
for sentence in sent:
# 对每个句子进行编码,添加到字典
new_token = tokenizer.encode_plus(sentence, max_length=128,
truncation=True, padding='max_length',
return_tensors='pt')
token['input_ids'].append(new_token['input_ids'][0])
token['attention_mask'].append(new_token['attention_mask'][0])
# 将列表的张量重新格式化为单个张量
token['input_ids'] = torch.stack(token['input_ids'])
token['attention_mask'] = torch.stack(token['attention_mask'])
# 处理tokens通过模型
output = model(**token)
# 从输出中获取密集向量表示
embeddings = outputs.last_hidden_state
# 执行平均池化操作
att_mask = tokens['attention_mask']
mask = att_mask.unsqueeze(-1).expand(embeddings.size()).float()
mask_embeddings = embeddings * mask
summed = torch.sum(mask_embeddings, 1)
summed_mask = torch.clamp(mask.sum(1), min=1e-9)
mean_pooled = summed / summed_mask
# 计算句子0的余弦相似度
mean_pooled = mean_pooled.detach().numpy()
cosine_similarity(
[mean_pooled[0]],
mean_pooled[1:]
)