从 Sentence-BERT 谈句子表征

53e8a71beb03c0df4a3af934203791a4.png

作者 | 太子长琴 

整理 | NewBeeNLP

在之前那篇 NLP 表征的历史与未来[1] 里,我们几乎从头到尾都在提及句子表征,也提出过一个很重要的概念:“句子” 才是语义理解的最小单位。不过当时并没有太过深入细节,直到做到文本相似度任务时才发现早已经有人将其 BERT 化了。

这就是本文要提到的一篇很重要但又很顺其自然的一篇论文

  • Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks

其实说到相似度,大家多少都会想到大名鼎鼎的 Siamese Recurrent Networks,他们当时(2016 年)用的是 LSTM 对句子表征,那是因为那时候 LSTM 效果是最好的。Sentence-BERT 其实就是将 LSTM 替换为 BERT。

背景问题

  • 针对句子对任务性能太差。这是因为原生 BERT 是通过将两个句子拼接后输出 Label 的,给定一组句子,要想找到相似度最高的句子对,需要二次方的复杂度。

  • 使用 CLS Token 作为句子表征效果太差,甚至不如 Glove。

作为一个求知欲满满的好奇之人,自然很想知道为神马。先按捺下自己躁动的心,看看本篇论文是怎么做的。对于第一个问题,其实就是 Siamese Network 的改版,专门用来做相似度计算。对于第二个问题,则尝试了三种不同的 Pooling 方法,分别是 CLSMAXAVERAGE。它之后的 Bert-Flow[2] 又增加了 AVERAGE 后两层(Bert 后两个 block)的方法。

句子表征

先看看文章 Related Work 提到哪些关于句子表征的研究:

  • Skip-Thought 通过预测上下文句子来做句子表征。这和 Skip-Gram 一样,只是把 Token 从词替换为句子。

  • Siamese Bi-LSTM 的结果做 MAX Pooling。

  • Transformer

  • Siamese DANSiamese Transformer。这个 DAN 就是对 Word Embedding 平均后再接一个 DNN。

这些方法基本都是我们熟悉的套路,可以说已经开发的淋漓尽致了。不过 BERT 的 CLS 却是另辟蹊径,突破了已有的范式,当然啦,这个想法其实是 Doc2Vec 用的方法,毕竟是同一个团队出品的。

关于未来还有哪些可能的方向,开头提到的文章里有比较深入的思考。短期来看,知识图谱(长时记忆)和充分的上下文(短时记忆)依然是可以进一步优化的。不过这可能只适用于对话领域,对于长文本的理解,可能还需从段落和文章结构上提出新的表征方法。

这里还提到一个比较有趣的点,SNLI 数据集适合用来训练句子表征,猜想可能是句子对能给任务带来一些 “指示”,比直接用单个句子学到的表征更加 “深刻”。之前只知道 “领域适配” 效果有提升,现在进一步证实,对文本的深入理解(相似句)同样可以提升效果。

模型算法

三种 Pooling 方法之前已经提到过,不再赘述。代码如下:

# From https://github.com/UKPLab/sentence-transformers/

# cls_token 直接使用 bert 的输出

# max pooling
input_mask_expanded = attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float()
# Set padding tokens to large negative value
token_embeddings[input_mask_expanded == 0] = -1e9  
max_over_time = torch.max(token_embeddings, 1)[0]

# average pooling
input_mask_expanded = attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float()
sum_embeddings = torch.sum(token_embeddings * input_mask_expanded, 1)
sum_mask = input_mask_expanded.sum(1)
sum_mask = torch.clamp(sum_mask, min=1e-9)
sum_embeddings / sum_mask

模型架构有两种,分别是:Siamese 和 Triplet,这主要是为了适配不同的任务,对句子对向量的输出做了不同处理。具体如下图(图片来自原论文)所示:

c3c9dcf45a3607699951b7ff3225a2c2.png

三类目标损失函数:

  • 分类(u v 分别是两个句子的向量):

  • 回归:使用均方误差损失(MSE Mean-Squared-Error Loss)

  • Triplet(给定句子 a,p 和 n 分别表示正向和反向句子):

    距离评估采用 Euclidean distance,ε 表示 sp 至少比 sn 更接近 sa ε 那么多,文中设为 1。

代码如下(做了部分简化处理):

# Code From https://github.com/UKPLab/sentence-transformers/
class SoftmaxLoss(nn.Module):
    def __init__(self, model: SentenceTransformer, 
                 sentence_embedding_dimension: int, num_labels: int):
        super(SoftmaxLoss, self).__init__()
        self.model = model
        self.num_labels = num_labels
        self.classifier = nn.Linear(3 * sentence_embedding_dimension, num_labels)
        self.loss_fct = nn.CrossEntropyLoss()

    def forward(self, sentence_features: Iterable[Dict[str, Tensor]], labels: Tensor):
        reps = [self.model(sentence_feature)['sentence_embedding'] 
                for sentence_feature in sentence_features]
        reps.append(torch.abs(rep_a - rep_b))
        features = torch.cat(reps, 1)
        output = self.classifier(features)
        loss = self.loss_fct(output, labels.view(-1))
        return loss

class MSELoss(nn.Module):
    def __init__(self, model):
        super(MSELoss, self).__init__()
        self.model = model
        self.loss_fct = nn.MSELoss()

    def forward(self, sentence_features: Iterable[Dict[str, Tensor]], labels: Tensor):
        rep = self.model(sentence_features[0])['sentence_embedding']
        return self.loss_fct(rep, labels)

class TripletLoss(nn.Module):
    def __init__(self, model: SentenceTransformer, triplet_margin: float = 1):
        super(TripletLoss, self).__init__()
        self.model = model
        self.distance_metric = lambda x, y: F.pairwise_distance(x, y, p=2)
        self.triplet_margin = triplet_margin

    def forward(self, sentence_features: Iterable[Dict[str, Tensor]], labels: Tensor):
        reps = [self.model(sentence_feature)['sentence_embedding'] 
                for sentence_feature in sentence_features]
        rep_anchor, rep_pos, rep_neg = reps
        distance_pos = self.distance_metric(rep_anchor, rep_pos)
        distance_neg = self.distance_metric(rep_anchor, rep_neg)
        losses = F.relu(distance_pos - distance_neg + self.triplet_margin)
        return losses.mean()

这些都比较直观,没啥需要特别说明的,稍微注意下,Triplet 损失其实就是 Relu 函数。

训练的一些配置:

  • BatchSize 16

  • Adam

  • lr 2e-5

  • lr warm-up 10% 训练数据

效果评估

首先看无监督任务,SBERT 训练数据:Wikipedia,NLI。注意,这里 NLI 数据是用来训练 SBERT 的,因为它是个相似度输出的模型(这点可能对其他模型略不公平,因为引入了新数据)。本任务评估的其实是预训练模型输出的句子向量表征的效果。结果如下图所示:

5d367a63c91d0381b724d7efa6f257b4.png

可以看出 CLS 和直接对 BERT 的隐向量取平均效果都不行。后面的几个实验也得出了类似的结论。不过这里有个现象还是值得注意:直接用 BERT 取平均的结果居然能比 SBERT 差那么多。这充分说明:「不同任务使用的不同方法对预训练结果影响比较明显」

但这并不能说明 BERT 的句子表征能力弱。果然,接下来的有监督任务(相似度)就证明了这一点,如下图所示:

6e3416ed45fafa799d063c3b152b6d22.png

同样的配置下,BERT 表现比 SBERT 还要更好。这说明:「下游任务精调效果显著」

而且后面在 SentEval 数据集上的实验(下游分类任务)同样也证明了这点,如下图所示:

86bd51fa78598ac1386356febb4d31e2.png

对此,文章中也做了解释:这主要是因为不同任务的配置不同。STS 任务使用 Cosine-Similarity 对句子向量进行评估,Cosine-Similarity 对所有维度平等处理;而 SentEval 使用逻辑回归分类器对句子向量分类,这就允许某些维度对分类结果有更高或更低的影响。同时,也可以发现 「SBERT 对句子表征本身也有一定的提升作用」,这应该算是个额外收获。

结论就是:「BERT 的 CLS 或输出向量无法和 “距离” 类的指标一起使用」。这点也可以说是本文最大的价值所在,正如文章在 Related Work 中所言:还没有针对这些方法(CLS 和平均输出向量)是否能够带来有用句子表征的评估。

另外,还有一个有意思的发现:「在交叉主题场景下,BERT 比 SBERT 表现的好很多」。论文的解释是:BERT 能够使用 Attention 直接对比句子,而 SBERT 必须将单个句子从一个没见过的主题映射到一个向量空间,以使具有相似主张和原因的论点接近。其实,即便是单主题下,BERT 也要好于 SBERT。具体如下图所示:

9bfcecd80301e7eee1caf9bb3ca051ca.png

这几个实验个人感觉还挺有价值,整理一下能带给我们的启发:

  • BERT 是个 “预训练” 的结果,直接使用一般不会有好效果,最好能在具体场景业务上精调一下。说到这里,其实我是不太赞同重新训练领域的 BERT 的,已经有很多实验证明提升有限。而且,预训练模型最主要的就是一个 “泛”,太 “专” 未必就好。不过倒是可以在领域数据上做增量训练。

  • 不同任务使用的训练方法不同效果可能差异很大。这里指的主要是 “预训练方法”,原因自然是不同目标函数的 “导向” 不同,所以我们才会常常看到 BERT 会有个句子对的预训练模型。

  • 不同数据集更适用的模型和任务不同。比如 SNLI 可能更适合训练句子表征(见下面两篇参考论文)。

使用指南

具体使用和 BERT 并无两样,因为模型架构本身其实还是 BERT,Similarity 无非是 BERT 输出之后的应用而已。作者已经将其发布为 pip 包,英文版直接安装后即可使用:

# From https://www.sbert.net/index.html
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('paraphrase-distilroberta-base-v1')
sentences = [
    'This framework generates embeddings for each input sentence',
    'The quick brown fox jumps over the lazy dog.']
embeddings = model.encode(sentences)

另外,这里面的功能也不止句子表征,还包括:相似度计算、文本聚类、语义搜索、信息检索、文本摘要、相似句挖掘、翻译句子挖掘、模型训练、模型蒸馏等。具体可参考文档:

  • SentenceTransformers Documentation — Sentence-Transformers documentation[3]

一起交流

想和你一起学习进步!『NewBeeNLP』目前已经建立了多个不同方向交流群(机器学习 / 深度学习 / 自然语言处理 / 搜索推荐 / 图网络 / 面试交流 / 等),名额有限,赶紧添加下方微信加入一起讨论交流吧!(注意一定o要备注信息才能通过)

7930620717af52666c0fe8757ac55123.png

本文参考资料

[1]

NLP 表征的历史与未来: https://yam.gift/2020/12/12/2020-12-12-NLP-Representation-History-Future/

[2]

Bert-Flow: https://yam.gift/2020/12/13/Paper/2020-12-13-Bert-Flow/

[3]

SentenceTransformers Documentation — Sentence-Transformers documentation: https://www.sbert.net/index.html

[4]

Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks: https://arxiv.org/abs/1908.10084

[5]

UKPLab/sentence-transformers: Sentence Embeddings with BERT & XLNet: https://github.com/UKPLab/sentence-transformers

END -

b520ad3815f11e2592a65b690946fd6b.png

c6eaac99a22cd6bd8b66b389966f63da.png

NLP 语义匹配:业务场景、数据集及比赛

2021-10-29

2fc753e818d0f2ceeb3791a83ee0e57c.png

GPT Plus Money!B O O M

2021-10-09

f45c695059791ae691fb821fd054a6e6.png

Batch Size对神经网络训练的影响

2021-10-04

76233e09bb6dbefa2244352f0ada3b0b.png

算法黑话大赏,我直呼好家伙!

2021-09-19

f46e8744625fe0d1e2bc6f9d0440b6e7.png

8449d45eca59167e7268d4f90eab9c77.gif

### 使用 BERT 进行小规模文本表示 对于小规模文本的表征BERT 提供了一种强大的方法来捕捉文本中的语义信息。由于 BERT 是一个预训练模型[^2],其已经具备了丰富的语言理解和表达能力。 为了利用 BERT 表征少量文本,首先需要准备数据集,并将其转换成适合 BERT 输入的形式。这涉及到将原始文本分割为标记(tokens),并在开头添加特殊标记 `[CLS]` 来指示这是分类任务的一部分[^1]。下面展示了一个具体的 Python 实现例子: #### 数据预处理 ```python from transformers import BertTokenizer tokenizer = BertTokenizer.from_pretrained('bert-base-uncased') texts = ["This is a sample sentence.", "Another example of text."] tokenized_texts = [] for text in texts: marked_text = "[CLS] " + text + " [SEP]" tokenized_text = tokenizer.tokenize(marked_text) indexed_tokens = tokenizer.convert_tokens_to_ids(tokenized_text) tokenized_texts.append(indexed_tokens) print(f'Tokenized Texts:\n{tokenized_texts}') ``` 这段代码展示了如何加载 `BertTokenizer` 并对给定的小批量文本进行分词操作。通过调用 `convert_tokens_to_ids()` 方法可以得到对应的 ID 序列,这些 ID 将作为后续输入到 BERT 模型中使用的特征向量。 #### 加载预训练模型并获取嵌入层输出 ```python import torch from transformers import BertModel model = BertModel.from_pretrained('bert-base-uncased', output_hidden_states=True) with torch.no_grad(): hidden_states, _ = model(torch.tensor([indexed_tokens])) embeddings = hidden_states[-2].numpy() # 取倒数第二层隐藏状态作为最终embedding print(embeddings.shape) # 输出形状应类似于 (batch_size, sequence_length, embedding_dim) ``` 这里选择了倒数第二个隐含层的状态作为最终的嵌入向量,因为研究表明这样的选择能够更好地保留上下文信息。当然也可以根据具体需求调整这一设置。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值