如何使用BERT嵌入来比较句子相似性

26

我正在使用HuggingFace Transformers包来访问预训练模型。由于我的用例需要处理英语和阿拉伯语,因此我正在使用bert-base-multilingual-cased预训练模型。我需要能够比较句子之间的相似性,例如通过余弦相似度等方法。为了实现这一点,首先需要获取每个句子的嵌入向量,然后可以计算余弦相似度。

首先,从BERT模型中提取语义嵌入的最佳方式是什么?仅需在输入句子之后取出模型的最后一个隐藏状态是否就足够了呢?

import torch
from transformers import BertModel, BertTokenizer

model_class = BertModel
tokenizer_class = BertTokenizer
pretrained_weights = 'bert-base-multilingual-cased'

tokenizer = tokenizer_class.from_pretrained(pretrained_weights)
model = model_class.from_pretrained(pretrained_weights)

sentence = 'this is a test sentence'

input_ids = torch.tensor([tokenizer.encode(sentence, add_special_tokens=True)])
with torch.no_grad():
    output_tuple = model(input_ids)
    last_hidden_states = output_tuple[0]

print(last_hidden_states.size(), last_hidden_states)
其次,如果这是从我的句子中获取嵌入的足够方式,那么我现在有另一个问题,即嵌入向量的长度取决于原始句子的长度不同。输出形状为[1,n,vocab_size],其中n可以具有任何值。
为了计算两个向量的余弦相似度,它们需要具有相同的长度。在这里,我该如何做到这一点?是否像先前的例子一样“沿着轴(axis)=1求和”仍然有效?我还有哪些其他选择?
5个回答

23
除了已经很好的被接受的答案外,我想指向sentence-BERT,其中详细讨论了相似性方面和特定度量(如余弦相似度)的影响。他们在网上提供了非常便捷的实现方式。这里的主要优势是,与“朴素”的句子嵌入比较相比,它们似乎获得了更快的处理速度,但我对实现本身不太熟悉。
重要的是,还有一种更精细的区分需要关注的相似性类型。特别是为此,在来自SemEval 2014(SICK数据集)的一个任务论文中也有很好的讨论。该论文详细介绍了这个问题。从您的任务描述中,我假设您已经使用了后期SemEval任务之一的数据,这也将扩展到多语言相似性。

3
谢谢!是的 - 我通过三元组损失模型使用句子BERT得到了一个非常好的解决方案。 - user8291021

14

您可以使用[CLS]标记作为整个序列的表示。在预处理步骤中,通常将此令牌置于句子的开头。该令牌通常用于分类任务(请参见BERT论文第2图和第3.2段)。

它是嵌入的第一个标记。

或者,您可以对序列取平均向量(例如沿第一(? )轴),这可能会根据HuggingFace文档(第3条技巧)获得更好的结果。

请注意,BERT并不是为了使用余弦距离进行句子相似性而设计的,尽管我在经验中发现它确实能产生不错的结果。


2
好的,我明白了 - 非常有趣。所以,如果我按照你上面展示的方法将一个句子输入模型,并提取出的 last_hidden_states 的形状为 [1, 9, 768],那么我可以 (1) 使用 [CLS] 标记作为 last_hidden_states[0][0],得到一个长度为 768 的向量,或者 (2) 使用 last_hidden_states.mean(1) 沿中间轴取平均值,也会得到一个长度为 768 的向量? - KOB
是的,其中任意一种方法都可以给出一个有意义的向量来表示输入的句子。 - Swier
嗨@Swier,你认为如果我们计算平均向量,句子表示会失去信息或其依赖性(例如单词顺序)吗?如果句子相似性不起作用,有没有比较句子嵌入的方法? - Thang Pham
@ThangM.Pham 句子嵌入永远不会包含原始句子中的所有信息;它们包含对训练任务最有用的信息。如果一个嵌入对您的问题没有用处,您要么必须继续训练几次,要么找到适合您任务的嵌入。我建议您查看dennlinger在此问题的其他答案中提出的BERT实现。 - Swier

7

不应该使用BERT的输出作为语义相似度的句子嵌入。 BERT没有针对语义相似度进行预训练,这将导致较差的结果,甚至比简单的Glove嵌入更差。请看下面来自BERT论文第一作者Jacob Devlin的评论和来自Sentence-BERT论文的一篇文章,其中详细讨论了句子嵌入。

Jacob Devlin的评论:我不确定这些向量是什么,因为BERT不能生成有意义的句子向量。似乎这是对词条进行平均池化以获得句子向量,但我们从未建议这会生成有意义的句子表示。即使当它们在下游任务的DNN中被用作良好的表示时,也并不意味着它们在余弦距离方面具有意义。(由于余弦距离是一个线性空间,所有维度都是等权重的)。(https://github.com/google-research/bert/issues/164#issuecomment-441324222)

从Sentence-BERT的论文中可以看出,直接使用BERT的输出结果会导致表现相当糟糕。对BERT嵌入进行平均处理仅能实现54.81的平均相关性,而仅使用CLS令牌输出则仅能实现29.19的平均相关性。这两种方法都不如计算平均GloVe嵌入。(https://arxiv.org/pdf/1908.10084.pdf
相反,你应该使用专门针对句子相似度预训练的模型,例如Sentence-BERT。句子-BERT和其他几个用于句子相似度的预训练模型可以在sentence-transformers库中找到(https://www.sbert.net/docs/pretrained_models.html),该库与HuggingFace transformers库完全兼容。使用这些库,您只需要一行代码即可获取句子嵌入。

2
作为对dennlinger回答的补充,我会添加一个代码示例,来自https://www.sbert.net/docs/usage/semantic_textual_similarity.html,使用BERT的嵌入来比较句子相似性:
from sentence_transformers import SentenceTransformer, util
model = SentenceTransformer('paraphrase-MiniLM-L12-v2')

# Two lists of sentences
sentences1 = ['The cat sits outside',
             'A man is playing guitar',
             'The new movie is awesome']

sentences2 = ['The dog plays in the garden',
              'A woman watches TV',
              'The new movie is so great']

#Compute embedding for both lists
embeddings1 = model.encode(sentences1, convert_to_tensor=True)
embeddings2 = model.encode(sentences2, convert_to_tensor=True)

#Compute cosine-similarits
cosine_scores = util.pytorch_cos_sim(embeddings1, embeddings2)

#Output the pairs with their score
for i in range(len(sentences1)):
    print("{} \t\t {} \t\t Score: {:.4f}".format(sentences1[i], sentences2[i], cosine_scores[i][i]))

该图书馆包含最先进的句子嵌入模型。
请参见https://dev59.com/OFMI5IYBdhLWcg3wq9eL#68728666以执行句子聚类。

0

通过一些描述来说明如何使用Bert架构进行句子嵌入。

同时,Christian Arteaga的评论也阐述了选择正确模型以完成正确任务的重要性。

我使用Hugging Face提供的Bert模型和分词器,而不是使用sentence_transformer包装器,因为这将更好地向那些刚开始学习NLP的用户展示它们的工作原理。

Bert模型 - https://huggingface.co/transformers/v3.0.2/model_doc/bert.html

注意 - 这只是伪代码;还请参阅https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2

'''
 Adapted and extended from 
 https://github.com/huggingface/transformers/issues/1950#issuecomment-558679189

'''
import pandas as pd
from transformers import BertTokenizer, BertModel
from sklearn.metrics.pairwise import cosine_similarity
import torch

def get_sentence_similarity(tokenizer,model,s1,s2):

    s1 = tokenizer.encode(s1)  
    s2 = tokenizer.encode(s2)

    print("1 len(s1) s1",len(s1),s1) # prints length of tokens - input_ids 8 [101, 7592...
    print("1 len(s2) s2",len(s2),s2)
    s1 = torch.tensor(s1)
    #print("2",s1) # prints tensor([ 101, 7592, ...
    s1 = s1.unsqueeze(0) # add an extra dimension, why ? the model needs to be fed in batches, we give a dummy batch 1
    #print("3",s1) # prints tensor([[ 101, 7592, 
    s2 = torch.tensor(s2).unsqueeze(0)

    # Pass it to the model for inference
    with torch.no_grad():
        output_1 = model(s1)
        output_2 = model(s2)

    logits_s1 = output_1[0]  # The last hidden-state is the first element of the output tuple
    logits_s2 = output_2[0].detach()
    #print("logits_s1 before detach",logits_s1) # prints  tensor([[[-0.1162,  0.2388, ...-0.2128]]], grad_fn=<NativeLayerNormBackward0>)
    logits_s1 = logits_s1.detach() # to remove the last part we call detach

    print("logits_s1.shape",logits_s1.shape ) # prints ([1, <length of tokens>, 768]) - Each token is rep by a 768 row vector for the base Bert Model!
    print("logits_s2.shape",logits_s2.shape ) # 1 the dummy batch dimension we added to the model by un-squeeze
    logits_s1 = torch.squeeze(logits_s1) #lets remove the batch dimension by squeeze
    logits_s2 = torch.squeeze(logits_s2)
    print("logits_s1.shape",logits_s1.shape ) # prints ([<length of tokens>, 768]) say torch.Size([8, 768])
    print("logits_s2.shape",logits_s2.shape )
    a = logits_s1.reshape(1,logits_s1.numel()) # we lay the vector flat make it 1, **768 via reshape; numel is number of elements
    b = logits_s2.reshape(1,logits_s2.numel())
    print("a.shape",a.shape ) # torch.Size([1, 6144])
    print("b.shape",b.shape ) # the shape will be 1, 768* no of tokens in b sentence - need not be similar

    # we can  mean over the rows to give it better similarity - but that is giving poor output
    # a = sentence_vector_1.mean(axis=1) this is giving cosine similarity as 1
    # b = sentence_vector_2.mean(axis=1)
    #cos_sim = F.cosine_similarity(a.reshape(1,-1),b.reshape(1,-1), dim=1)

    # so we pad the tensors to be same shape
    if  a.shape[1] <  b.shape[1]:
        pad_size = (0, b.shape[1] - a.shape[1]) 
        a = torch.nn.functional.pad(a, pad_size, mode='constant', value=0)
    else:
        pad_size = (0, a.shape[1] - b.shape[1]) 
        b = torch.nn.functional.pad(b, pad_size, mode='constant', value=0)

    print("After padding")
    print("a.shape",a.shape ) # 1,N
    print("b.shape",b.shape ) # 1, N


    # Calculate the cosine similarity
    cos_sim = cosine_similarity(a,b)
    #print("got cosine similarity",cos_sim) # output [[0.80432487]]
    return cos_sim



if __name__ == "__main__":


    s1 = "John loves dogs" 
    s2 = "dogs love John"

    # Tokenize the text using BERT tokenizer
    tokenizer = BertTokenizer.from_pretrained("bert-base-uncased")
    model = BertModel.from_pretrained("bert-base-uncased") #Not good for sentence similarity
    model.eval()
    
    cos_sim = get_sentence_similarity(tokenizer,model,s1,s2)
    print("got cosine similarity",cos_sim) # output [[0.738616]]

    # Let's try the same with a better model - say for sentence embedding
    # From https://www.sbert.net/docs/pretrained_models.html
    # They have been extensively evaluated for their quality to embedded sentences 
    # (Performance Sentence Embeddings) and to embedded search queries & paragraphs 

    # better to use AutoTokenizer for other models see https://github.com/huggingface/transformers/issues/5587
    tokenizer = BertTokenizer.from_pretrained("sentence-transformers/all-MiniLM-L6-v2")
    model = BertModel.from_pretrained("sentence-transformers/all-MiniLM-L6-v2")
    model.eval()
    cos_sim = get_sentence_similarity(tokenizer,model,s1,s2)
    print("got cosine similarity",cos_sim) # output [[0.5646803]]

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接