来自转换器的 BERT 句子嵌入

Mit*_*ops 5 bert-language-model huggingface-transformers

我正在尝试从 BERT 模型中的隐藏状态中获取句子向量。查看这里的 Huggingface BertModel 说明,其中说:

from transformers import BertTokenizer, BertModel
tokenizer = BertTokenizer.from_pretrained('bert-base-multilingual-cased')
model = BertModel.from_pretrained("bert-base-multilingual-cased")
text = "Replace me by any text you'd like."
encoded_input = tokenizer(text, return_tensors='pt') 
output = model(**encoded_input)
Run Code Online (Sandbox Code Playgroud)

所以首先要注意,因为它在网站上,它 /not/ 运行。你得到:

>>> Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'BertTokenizer' object is not callable
Run Code Online (Sandbox Code Playgroud)

但它看起来像是一个小改动修复了它,因为您不直接调用标记器,而是要求它对输入进行编码:

encoded_input = tokenizer.encode(text, return_tensors="pt")
output = model(encoded_input)
Run Code Online (Sandbox Code Playgroud)

好的,除此之外,我得到的张量的形状与我预期的不同:

>>> output[0].shape
torch.Size([1,11,768])
Run Code Online (Sandbox Code Playgroud)

这是很多层。哪个是用于句子嵌入的正确层? [0]? [-1]? 平均几个?我的目标是能够与这些进行余弦相似度,所以我需要一个适当的 1xN 向量而不是 NxK 张量。

我看到流行的bert-as-a-service 项目似乎使用[0]

这样对吗?是否有关于每一层是什么的文档?

Chi*_*cha 8

正如其他答案中提到的,BERT 并不是为了产生句子级嵌入。现在,让我们研究如何利用 BERT 的强大功能来计算上下文相关的句子级嵌入。

BERT 确实在单词级别携带上下文,下面是一个示例:

这是一根木棍坚持你的工作。

上面两个句子带有“stick”这个词,BERT 在根据句子(或者说上下文)计算 Stick 的嵌入方面做得很好。

现在,让我们来看另一个例子:

- 你几岁?

- 你今年多大?

上面两个句子在上下文上非常相似,因此,我们需要一个可以接受句子或文本块或段落并共同生成正确嵌入的模型。这是实现它的方法。

方法一:

使用预先训练的sentence_transformers,这里是huggingface hub的链接。

from sentence_transformers import SentenceTransformer
from sentence_transformers.util import cos_sim


model = SentenceTransformer(r"sentence-transformers/paraphrase-MiniLM-L6-v2")


embd_a = model.encode("What is your age?")
embd_b = model.encode("How old are you?")


sim_score = cos_sim(embd_a, embd_b)

print(sim_score)

output: tensor([[0.8648]])
Run Code Online (Sandbox Code Playgroud)

现在,可能存在一个问题:我们如何针对特定领域训练我们的句子转换器。开始了,

  1. 监督方法:

数据科学家或机器学习工程师面临的一个常见挑战是获取正确注释的数据,大多数情况下很难获得大量数据,但是如果你有的话,我们可以如何在句子转换器上训练我们的数据(别担心,有也是一种无监督的方法)。

model = SentenceTransformer('distilbert-base-nli-mean-tokens') 

train_examples = [InputExample(texts=['My first sentence', 'My second sentence'], label=0.8),
InputExample(texts=['Another pair', 'Unrelated sentence'], label=0.3)]

train_dataloader = DataLoader(train_examples, shuffle=True, batch_size=16)

train_dataloader = DataLoader(train_examples, shuffle=True, batch_size=16)
train_loss = losses.CosineSimilarityLoss(model)

#Tune the model
model.fit(train_objectives=[(train_dataloader, train_loss)], epochs=1, warmup_steps=100)
Run Code Online (Sandbox Code Playgroud)

更多详细信息请参见此处

提示:如果您有一组彼此相似的句子,比如说,您有一个 CSV,其中 A 列和 B 列包含彼此相似的句子(我的意思是每一行都有一对彼此相似的句子)其他),只需加载 csv 并分配 0.85 到 0.95 之间的随机值作为相似度分数并继续。

  1. 无监督方法

假设您没有大量带注释的数据,但您想训练特定于领域的句子转换器,以下是我们的做法。即使对于无监督训练,也需要数据,即句子/段落列表,但不需要注释。假设您根本没有任何数据,但仍然有一个解决方案(请访问答案的最后部分)。

有多种方法可用于无监督训练,这里列出了两种最突出的方法。要查看所有可用方法的列表,请访问此处

TSDAE 研究论文链接。

from sentence_transformers import SentenceTransformer, LoggingHandler
from sentence_transformers import models, util, datasets, evaluation, losses
from torch.utils.data import DataLoader

# Define your sentence transformer model using CLS pooling
model_name = 'bert-base-uncased'
word_embedding_model = models.Transformer(model_name)
pooling_model = models.Pooling(word_embedding_model.get_word_embedding_dimension(), 'cls')
model = SentenceTransformer(modules=[word_embedding_model, pooling_model])

# Define a list with sentences (1k - 100k sentences)
train_sentences = ["Your set of sentences",
                   "Model will automatically add the noise", 
                   "And re-construct it",
                   "You should provide at least 1k sentences"]

# Create the special denoising dataset that adds noise on-the-fly
train_dataset = datasets.DenoisingAutoEncoderDataset(train_sentences)

# DataLoader to batch your data
train_dataloader = DataLoader(train_dataset, batch_size=8, shuffle=True)

# Use the denoising auto-encoder loss
train_loss = losses.DenoisingAutoEncoderLoss(model, decoder_name_or_path=model_name, tie_encoder_decoder=True)

# Call the fit method
model.fit(
    train_objectives=[(train_dataloader, train_loss)],
    epochs=1,
    weight_decay=0,
    scheduler='constantlr',
    optimizer_params={'lr': 3e-5},
    show_progress_bar=True
)

model.save('output/tsdae-model')
Run Code Online (Sandbox Code Playgroud)

SimCSE 研究论文链接

from sentence_transformers import SentenceTransformer, InputExample
from sentence_transformers import models, losses
from torch.utils.data import DataLoader

# Define your sentence transformer model using CLS pooling
model_name = 'distilroberta-base'
word_embedding_model = models.Transformer(model_name, max_seq_length=32)
pooling_model = models.Pooling(word_embedding_model.get_word_embedding_dimension())
model = SentenceTransformer(modules=[word_embedding_model, pooling_model])

# Define a list with sentences (1k - 100k sentences)
train_sentences = ["Your set of sentences",
                   "Model will automatically add the noise",
                   "And re-construct it",
                   "You should provide at least 1k sentences"]

# Convert train sentences to sentence pairs
train_data = [InputExample(texts=[s, s]) for s in train_sentences]

# DataLoader to batch your data
train_dataloader = DataLoader(train_data, batch_size=128, shuffle=True)

# Use the denoising auto-encoder loss
train_loss = losses.MultipleNegativesRankingLoss(model)

# Call the fit method
model.fit(
    train_objectives=[(train_dataloader, train_loss)],
    epochs=1,
    show_progress_bar=True
)

model.save('output/simcse-model')
Run Code Online (Sandbox Code Playgroud)

提示:如果你仔细观察,主要区别在于使用的损失函数。要查看适用于此类训练场景的所有损失函数的列表,请访问此处。另外,通过我所做的所有实验,我发现当您想要良好的精度和良好的召回率时,TSDAE 更有用。但是,当您需要非常高的精度和低召回率时,可以使用 SimCSE。

现在,如果您没有足够的数据来微调模型,但您找到了在您的领域训练的 BERT 模型,您可以通过添加池化层和密集层来直接利用它。请研究一下什么是“池化”,以便更好地理解您正在做的事情。

from sentence_transformers import SentenceTransformer, models
from torch import nn

word_embedding_model = models.Transformer('bert-base-uncased', max_seq_length=256)
pooling_model = models.Pooling(word_embedding_model.get_word_embedding_dimension())
dense_model = models.Dense(in_features=pooling_model.get_sentence_embedding_dimension(), out_features=256, activation_function=nn.Tanh())

model = SentenceTransformer(modules=[word_embedding_model, pooling_model, dense_model])
Run Code Online (Sandbox Code Playgroud)

提示:通过上述方法,如果您开始获得极高的余弦分数,则需要进行负面测试。有时,简单地添加池化层可能没有帮助,您必须采取一些示例并检查不相似的输入的相似度分数(即使对于不相似的句子,这也可能显示出良好的相似性,这就是您应该停止的时候并尝试收集一些数据并进行无监督训练)

对于有兴趣深入了解的人,这里列出的主题可能会对您有所帮助。

  1. 池化
  2. 连体网络
  3. 对比损失

:) :)


Jin*_*ich 7

我认为没有单一的权威文档说明使用什么以及何时使用。您需要试验和衡量什么最适合您的任务。这篇论文很好地总结了最近关于 BERT 的观察:https : //arxiv.org/pdf/2002.12327.pdf

我认为经验法则是:

  • 如果您要为特定任务微调模型,请使用最后一层。并尽可能进行微调,几百甚至几十个训练示例就足够了。

  • 如果您无法对模型进行微调,请使用一些中间层(第 7 层或第 8 层)。其背后的直觉是,层首先开发出越来越抽象和通用的输入表示。在某些时候,表示开始更多地针对预训练任务。

Bert-as-services 默认使用最后一层(但它是可配置的)。在这里,它将是[:, -1]。但是,它始终返回所有输入标记的向量列表。对应于第一个特殊(所谓的[CLS])标记的向量被认为是句子嵌入。这[0]来自您所指的狙击手。


cro*_*oik 7

虽然Jindrich的现有答案大体上是正确的,但它并没有完全解决这个问题。OP 询问他应该使用哪一层来计算句子嵌入之间的余弦相似度,而这个问题的简短答案是none。像余弦相似度这样的度量要求向量的维度贡献相等且有意义,但原始作者发布的 BERT 权重并非如此。Jacob Devlin(BERT 论文的作者之一)写道

我不确定这些向量是什么,因为 BERT 不会生成有意义的句子向量。似乎这是对单词标记进行平均池化以获得句子向量,但我们从未建议这会生成有意义的句子表示。即使当它们被输入到为下游任务训练的 DNN 中时它们是不错的表示,这并不意味着它们在余弦距离方面是有意义的。(因为余弦距离是一个线性空间,其中所有维度的权重相等)。

但是,这并不意味着您不能将 BERT 用于此类任务。这只是意味着您不能开箱即用地使用预先训练的权重。您可以在 BERT 之上训练一个分类器,该分类器学习哪些句子相似(使用[CLS]令牌),或者您可以使用可以在无监督场景中使用的句子转换器,因为它们经过训练可以产生有意义的句子表示。