🔎大家好,我是Sonhhxg_柒,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流🔎
📝个人主页-Sonhhxg_柒的博客_CSDN博客 📃
🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝
📣系列专栏 - 机器学习【ML】 自然语言处理【NLP】 深度学习【DL】
🖍foreword
✔说明⇢本人讲解主要包括Python、机器学习(ML)、深度学习(DL)、自然语言处理(NLP)等内容。
如果你对这个系列感兴趣的话,可以关注订阅哟👋
文章目录
到目前为止,我们已经解决了转换器库的分类和生成问题。文本表示是现代自然语言处理( NLP ) 中的另一项关键任务,尤其是对于聚类、语义搜索和主题建模等无监督任务。使用各种模型表示句子,例如Universal Sentence Encoder ( USE) 和 Siamese BERT (Sentence-BERT) 以及句子转换器等附加库将在此处进行说明。还将解释使用 BART 的零样本学习,您将学习如何使用它。还将描述少量学习方法和无监督用例,例如语义文本聚类和主题建模。最后,将介绍语义搜索等一次性学习用例。
本章将涵盖以下主题:
- 句子嵌入简介
- 基准句子相似性模型
- 使用 BART 进行零样本学习
- 使用 FLAIR 进行语义相似性实验
- 使用 Sentence-BERT 进行文本聚类
- 使用 Sentence-BERT 进行语义搜索
技术要求
我们将使用 Jupyter 笔记本来运行我们的编码练习。为此,您将需要 Python 3.6+ 和以下软件包:
- sklearn
- transformers >=4.00
- datasets
- sentence-transformers
- tensorflow-hub
- flair
- umap-learn
- bertopic
句子嵌入简介
预训练的 BERT 模型不会产生高效且独立的句子嵌入,因为它们始终需要在端到端的监督设置中进行微调。这是因为我们可以将预训练的 BERT 模型视为一个不可分割的整体,并且语义分布在所有层中,而不仅仅是最后一层。如果没有微调,独立使用其内部表示可能是无效的。处理聚类、主题建模、信息检索或语义搜索等无监督任务也很困难。例如,因为我们必须在聚类任务期间评估许多句子对,这会导致大量计算开销。
幸运的是,对原始 BERT 模型进行了许多修改,例如Sentence-BERT ( SBERT ),以导出语义上有意义和独立的句子嵌入。我们稍后将讨论这些方法。在 NLP 文献中,已经提出了许多用于将单个句子映射到公共特征空间(向量空间模型)的神经句子嵌入方法,其中通常使用余弦函数(或点积)来衡量相似度,而欧几里德距离则用来衡量不相似度.
以下是一些可以通过句子嵌入有效解决的应用:
- 句对任务
- 信息检索
- 问答
- 重复问题检测
- 释义检测
- 文档聚类
- 主题建模
最简单但最一种有效的神经句子嵌入是平均池化操作,它是对句子中单词的嵌入执行的。为了更好地表示这一点,一些早期的神经方法以无监督的方式学习句子嵌入,例如 Doc2Vec、Skip-Thought、FastSent 和 Sent2Vec。Doc2Vec 利用令牌级分布理论和目标函数来预测相邻单词,类似于 Word2Vec。该方法将额外的内存令牌(称为Paragraph-ID)注入每个句子都让人联想到转换器库中的 CLS 或 SEP 标记。这个额外的令牌充当表示上下文或文档嵌入的一块内存。SkipThought 和 FastSent 被认为是句子级别的方法,其中目标函数用于预测相邻的句子。这些模型提取句子的含义,以从相邻的句子及其上下文中获取必要的信息。
其他一些方法,例如 InferSent,利用监督学习和多任务迁移学习来学习通用句子嵌入。InferSent 训练了各种监督任务以获得更有效的嵌入。基于 RNN 的监督模型(例如 GRU 或 LSTM)利用最后一个隐藏状态(或堆叠的整个隐藏状态)在监督设置中获得句子嵌入。我们在第 1 章“从词袋到变形金刚”中谈到了 RNN 方法。
交叉编码器与双编码器
到目前为止,我们有讨论了如何训练基于 Transformer 的语言模型,并分别在半监督和监督设置中对其进行微调。正如我们在前几章中所了解的,由于变压器架构,我们获得了成功的结果。一旦将特定于任务的薄线性层置于预训练模型之上,网络的所有权重(不仅是最后一个特定于任务的薄层)都会使用特定于任务的标记数据进行微调。我们还体验了 BERT 架构如何针对两组不同的任务(单句或句子对)进行微调,而无需进行任何架构修改。唯一的区别是,对于句子对任务,句子被连接起来并用 SEP 标记进行标记。因此,自我注意应用于连接句子的所有标记。这是BERT模型的一大优势,其中两个输入句子可以在每一层从对方获取必要的信息。最后,它们同时被编码。这就是所谓的交叉编码。
但是,有两个缺点关于 SBERT 作者和Humeau 等人在 2019 年解决的交叉编码器,如下所示:
- 由于需要处理太多可能的组合,交叉编码器设置对于许多句子对任务来说并不方便。例如,要从 1,000 个句子的列表中获取最接近的两个句子,交叉编码器模型 (BERT) 需要大约 500,000 ( n * (n-1) /2) 推理计算。因此,与 SBERT 或 USE 等替代解决方案相比,它会非常慢。这是因为这些替代方案会产生独立的句子嵌入,其中可以轻松应用相似度函数(余弦相似度)或相异度函数(欧几里得或曼哈顿)。请注意,这些差异/相似性功能可以在现代架构上有效地执行。此外,借助优化的索引结构,我们可以在比较或聚类许多文档时将计算复杂度从数小时减少到几分钟。
- 由于其监督特性,BERT 模型无法导出独立的有意义的句子嵌入。像聚类、语义搜索或主题建模等无监督任务一样,很难利用预训练的 BERT 模型。BERT 模型为文档中的每个标记生成一个固定大小的向量。在无监督设置中,文档级表示可以通过平均或池化令牌向量以及 SEP 和 CLS 令牌来获得。稍后,我们将看到 BERT 的这种表示产生低于平均水平的句子嵌入,并且其性能得分通常比 Word2Vec、FastText 或 GloVe 等词嵌入池技术差。
或者,双编码器(如 SBERT)独立将句子对映射到语义向量空间,如下图所示。由于表示是分开的,双编码器可以缓存每个输入的编码输入表示,从而缩短推理时间。BERT 的成功双编码器修改之一是 SBERT。基于 Siamese 和 Triplet 网络结构,SBERT 微调 BERT 模型以产生语义上有意义且独立的句子嵌入。
下图显示了双编码器架构:
图 7.1 – 双编码器架构
您可以找到数百个预训练的 SBERT 模型在https://public.ukp.informatik.tu-darmstadt.de/reimers/sentence-transformers/v0.2/接受过不同目标的培训。
我们将在下一节中使用其中的一些。
基准句子相似性模型
那里有许多可用的语义文本相似性模型,但强烈建议您使用指标进行基准测试并了解它们的能力和差异。Papers With Code在Semantic Textual Similarity | Papers With Code提供了这些数据集的列表。
此外,每个数据集中有许多模型输出,它们按结果排序。这些结果取自上述文章。
GLUE 提供了大多数这些数据集和测试,但它不仅用于语义文本相似性。GLUE,即General Language Understanding Evaluation的缩写,是一个通用的用于评估具有不同 NLP 特征的模型的基准。有关 GLUE 数据集及其用法的更多详细信息,请参见第 2 章,主题的动手介绍。在我们继续之前,让我们看一下它:
- 加载指标和来自 GLUE 基准测试的 MRPC 数据集,您可以使用以下代码:
从数据集导入 load_metric, load_dataset
from datasets import load_metric, load_dataset metric = load_metric('glue', 'mrpc') mrpc = load_dataset('glue', 'mrpc')
该数据集中的样本被标记为1和0,分别表示它们是相似的还是不同的。无论架构如何,您都可以使用任何模型为两个给定句子生成值。换句话说,模型应该将两个句子分类为 0 和 1。
- 让我们假设模型产生值,并且这些值存储在一个名为predictions的数组中。您可以轻松地将此指标与预测一起使用,以查看 F1 和准确度值:
labels = [i['label'] for i in dataset['test']] metric.compute(predictions=predictions, references=labels)
- 一些Semantic Textual Similarity Benchmark ( STSB )等语义文本相似性数据集具有不同的指标。例如,该基准测试使用 Spearman 和 Pearson 相关性,因为输出和预测介于 0 和 5 之间,并且是浮点数而不是 0 和 1,这是一个回归问题。以下代码显示了此基准测试的示例:
metric = load_metric('glue', 'stsb') metric.compute(predictions=[1,2,3],references=[5,2,2])
预测和参考是与Microsoft Research Paraphrase Corpus ( MRPC ) 中的相同;预测是模型输出,而参考是数据集标签。
- 为了获得两个模型之间的比较结果,我们将使用精简版的 Roberta 并在 STSB 上测试这两个模型。首先,您必须加载两个模型。以下代码显示了如何在加载和使用模型之前安装所需的库:
pip install tensorflow-hub pip install sentence-transformers
- 正如我们之前提到的,下一步是加载数据集和指标:
from datasets import load_metric, load_dataset stsb_metric = load_metric('glue', 'stsb') stsb = load_dataset('glue', 'stsb')
- 之后,我们必须加载两个模型:
import tensorflow_hub as hub use_model = hub.load( "https://tfhub.dev/google/universal-sentence-encoder/4") from sentence_transformers import SentenceTransformer distilroberta = SentenceTransformer( 'stsb-distilroberta-base-v2')
- 这两个模型为给定的句子提供嵌入。为了比较两个句子之间的相似度,我们将使用余弦相似度。以下函数将句子作为一个批次,并利用 USE 为每对提供余弦相似度:
import tensorflow as tf import math def use_sts_benchmark(batch): sts_encode1 = \ tf.nn.l2_normalize(use_model(tf.constant(batch['sentence1'])), axis=1) sts_encode2 = \ tf.nn.l2_normalize(use_model(tf.constant(batch['sentence2'])), axis=1) cosine_similarities = \ tf.reduce_sum(tf.multiply(sts_encode1,sts_encode2),axis=1) clip_cosine_similarities = \ tf.clip_by_value(cosine_similarities,-1.0, 1.0) scores = 1.0 - \ tf.acos(clip_cosine_similarities) / math.pi return scores
- 稍作修改,同样的功能也可以用于 RoBERTa。这些小修改只是为了替换embedding函数,不同的是TensorFlow Hub 模型和转换器。以下是修改后的功能:
def roberta_sts_benchmark(batch): sts_encode1 = tf.nn.l2_normalize(distilroberta.encode(batch['sentence1']), axis=1) sts_encode2 = tf.nn.l2_normalize(distilroberta.encode(batch['sentence2']), axis=1) cosine_similarities = tf.reduce_sum(tf.multiply(sts_encode1, sts_encode2), axis=1) clip_cosine_similarities = tf.clip_by_value(cosine_similarities, -1.0, 1.0) scores = 1.0 - tf.acos(clip_cosine_similarities) / math.pi return scores
- 将这些函数应用于数据集将导致每个模型的相似度得分:
use_results = use_sts_benchmark(stsb['validation']) distilroberta_results = roberta_sts_benchmark( stsb['validation'])
- 对两个结果使用度量会产生 Spearman 和 Pearson 相关值:
results = { "USE":stsb_metric.compute( predictions=use_results, references=references), "DistillRoberta":stsb_metric.compute( predictions=distilroberta_results, references=references) }
- 你可以简单地使用 pandas 以比较方式查看结果:
import pandas as pd pd.DataFrame(results)
输出如下:
图 7.2 – DistilRoberta 和 USE 上的 STSB 验证结果
在本节中,您了解了语义文本相似度的重要基准。无论模型如何,您都学习了如何使用这些指标中的任何一个来量化模型性能。在下一节中,您将了解小样本学习模型。
使用 BART 进行零样本学习
在机器学习领域,零样本学习是被称为无需明确接受培训即可执行任务的模型。在 NLP 的情况下,假设有一个模型可以预测某些文本被分配给模型的类的概率。然而,关于这种学习类型的有趣部分是模型没有在这些类上进行训练。
随着许多可以执行迁移学习的高级语言模型的兴起,零样本学习应运而生。在 NLP 的情况下,这种学习是由 NLP 模型在测试时执行的,其中模型看到属于新类别的样本,而这些样本以前没有看到过。
这种学习通常用于分类任务,其中表示类和文本,并比较两者的语义相似性。这两者的表示形式是嵌入向量,而相似度度量(如余弦相似度或密集层等预训练分类器)输出句子/文本被分类为类的概率。
我们可以使用许多方法和方案来训练此类模型,但最早的方法之一是使用从互联网上抓取的页面,其中包含元部分中的关键字标签。有关更多信息,请阅读https://amitness.com/2020/05/zero-shot-text-classification/上的以下文章和博客文章。
BART 等语言模型没有使用如此庞大的数据,而是使用多流派自然语言推理( MNLI ) 数据集微调和检测两个不同句子之间的关系。此外,HuggingFace 模型库包含许多已实现零样本学习的模型。它们还提供了一个零样本学习管道,以方便使用。
例如,捷运来自Facebook AI Research ( FAIR ) 的代码在以下代码中用于执行零样本文本分类:
from transformers import pipeline
import pandas as pd
classifier = pipeline("zero-shot-classification",
model="facebook/bart-large-mnli")
sequence_to_classify = "one day I will see the world"
candidate_labels = ['travel',
'cooking',
'dancing',
'exploration']
result = classifier(sequence_to_classify, candidate_labels)
pd.DataFrame(result)
图 7.3 – 使用 BART 的零样本学习结果
如您所见,旅行和探索标签的概率最高,但最可能的标签是旅行。
但是,有时,一个样本可以属于多个类别(多标签)。HuggingFace 为此提供了一个名为multi_label的参数。以下示例使用此参数:
result = classifier(sequence_to_classify,
candidate_labels,
multi_label=True)
Pd.DataFrame(result)
因此,它更改为以下内容:
图 7.4 – 使用 BART 的零样本学习结果(multi_label = True)
你可以进一步测试结果,看看模型如何执行,如果使用与旅行非常相似的标签。例如,如果将移动和移动添加到标签列表中,您可以查看它的执行情况。
还有其他模型也利用标签和上下文之间的语义相似性来执行零样本分类。在few-shot learning的情况下,给模型一些样本,但是这些样本不足以单独训练一个模型。模型可以使用这些样本来执行语义文本聚类等任务,稍后将对此进行解释。
既然您已经了解了如何使用 BART 进行零样本学习,那么您应该了解它的工作原理。BART 经过微调自然语言推理( NLI ) 数据集,例如 MNLI。这些数据集包含句子对和每对的三个类;即Neutral、Entailment和Contradiction。在这些数据集上训练过的模型可以捕获两个句子的语义,并通过以 one-hot 格式分配标签来对它们进行分类。如果去掉 Neutral 标签,只使用 Entailment 和 Contradiction 作为输出标签,如果两个句子可以紧随其后,则说明这两个句子是密切相关的。换句话说,您可以将第一句更改为标签(例如travel),将第二句更改为内容(例如,有一天我会看到这个世界)。据此,如果这两者能够先后出现,这意味着标签和内容在语义上是相关的。以下代码示例展示了如何直接使用 BART 模型没有根据前面的描述进行零样本分类流水线:
from transformers import AutoModelForSequenceClassification,AutoTokenizer
nli_model = AutoModelForSequenceClassification\
.from_pretrained(
"facebook/bart-large-mnli")
tokenizer = AutoTokenizer.from_pretrained(
"facebook/bart-large-mnli")
premise = "one day I will see the world"
label = "travel"
hypothesis = f'This example is {label}.'
x = tokenizer.encode(
premise,
hypothesis,
return_tensors='pt',
truncation_strategy='only_first')
logits = nli_model(x)[0]
entail_contradiction_logits = logits[:,[0,2]]
probs = entail_contradiction_logits.softmax(dim=1)
prob_label_is_true = probs[:,1]
print(prob_label_is_true)
结果如下:
tensor([0.9945], grad_fn=<SelectBackward>)
你可以也称第一句为假设和句子包含标签的前提。根据结果,前提可以包含假设。这意味着假设被标记为前提。
到目前为止,您已经学会了如何通过利用 NLI 微调模型来使用零样本学习。接下来,您将学习如何使用语义文本聚类和语义搜索执行少量/一次性学习。
使用 FLAIR 进行语义相似性实验
在这个实验中,我们将对句子进行定性评估表示模型,这要归功于flair库,它确实为我们简化了获取文档嵌入的过程。
我们将在采用以下方法的同时进行实验:
- 记录平均池嵌入
- 基于 RNN 的嵌入
- BERT 嵌入
- SBERT 嵌入
在开始实验之前,我们需要安装这些库:
!pip install sentence-transformers
!pip install dataset
!pip install flair
对于定性评估,我们定义了一个相似句子对列表和一个不相似句子对列表(每对五对)。我们对嵌入模型的期望是它们应该分别测量高分和低分。
句子对是从 SBS Benchmark 数据集中提取的,我们已经从第 6 章的句对回归部分熟悉了,用于标记分类的微调语言模型。对于相似的配对,两个句子是完全等价的,并且它们具有相同的含义。
带有 a 的对STSB 数据集中的相似度得分约为 5随机抽取,如下:
import pandas as pd
similar=[("A black dog walking beside a pool.",
"A black dog is walking along the side of a pool."),
("A blonde woman looks for medical supplies for work in a suitcase. ",
" The blond woman is searching for medical supplies in a suitcase."),
("A doubly decker red bus driving down the road.",
"A red double decker bus driving down a street."),
("There is a black dog jumping into a swimming pool.",
"A black dog is leaping into a swimming pool."),
("The man used a sword to slice a plastic bottle.",
"A man sliced a plastic bottle with a sword.")]
pd.DataFrame(similar, columns=["sen1", "sen2"])
输出如下:
图 7.5 – 相似对列表
这是列表相似度得分的不同句子大约为 0,取自 STS-B 数据集:
import pandas as pd
dissimilar= [
("A little girl and boy are reading books. ",
"An older child is playing with a doll while gazing out the window."),
("Two horses standing in a field with trees in the background.",
"A black and white bird on a body of water with grass in the background."),
("Two people are walking by the ocean.",
"Two men in fleeces and hats looking at the camera."),
("A cat is pouncing on a trampoline.",
"A man is slicing a tomato."),
("A woman is riding on a horse.",
"A man is turning over tables in anger.")]
pd.DataFrame(dissimilar, columns=["sen1", "sen2"])
输出如下:
图 7.6 – 不同对列表
现在,让我们准备评估嵌入的必要函数楷模。下面的sim()函数计算两个句子之间的余弦相似度;也就是说,s1,s2:
导入火炬,numpy 作为 np
import torch, numpy as np
def sim(s1,s2):
s1=s1.embedding.unsqueeze(0)
s2=s2.embedding.unsqueeze(0)
sim=torch.cosine_similarity(s1,s2).item()
return np.round(sim,2)
本实验中使用的文档嵌入模型都是预训练模型。我们将文档嵌入模型对象和句子对列表(相似或不同)传递给下面的评估()函数,一旦模型对句子嵌入进行编码,它将计算列表中每一对的相似度得分,以及列表平均值。函数定义如下:
from flair.data import Sentence
def evaluate(embeddings, myPairList):
scores=[]
for s1, s2 in myPairList:
s1,s2=Sentence(s1), Sentence(s2)
embeddings.embed(s1)
embeddings.embed(s2)
score=sim(s1,s2)
scores.append(score)
return scores, np.round(np.mean(scores),2)
平均词嵌入
平均单词嵌入(或文档池)将平均池操作应用于句子中的所有单词,其中所有单词嵌入的平均值被认为是句子嵌入。以下执行实例化基于 GloVe 向量的文档池嵌入。请注意,虽然我们在这里只使用 GloVe 向量,但flair API 允许我们使用多个词嵌入。这是代码定义:
from flair.data import Sentence
from flair.embeddings import WordEmbeddings, DocumentPoolEmbeddings
glove_embedding = WordEmbeddings('glove')
glove_pool_embeddings = DocumentPoolEmbeddings(
[glove_embedding]
)
让我们在相似对上评估 GloVe 池模型,如下所示:
evaluate(glove_pool_embeddings, similar)
([0.97, 0.99, 0.97, 0.99, 0.98], 0.98)
结果似乎很好,因为这些结果值非常高,这是我们所期望的。但是,该模型也会为不同列表产生高分,例如平均 0.94。我们的期望值小于 0.4。我们将在本章后面讨论为什么会得到这个。这是执行:
evaluate(glove_pool_embeddings, dissimilar)
([0.94, 0.97, 0.94, 0.92, 0.93], 0.94)
接下来,让我们在同一问题上评估一些 RNN 嵌入。
基于 RNN 的文档嵌入
让我们实例化一个 GRU 模型基于 GloVe 嵌入,其中DocumentRNNEmbeddings的默认模型是 GRU:
from flair.embeddings import WordEmbeddings, DocumentRNNEmbeddings
gru_embeddings = DocumentRNNEmbeddings([glove_embedding])
运行评估方法:
evaluate(gru_embeddings, similar)
([0.99, 1.0, 0.94, 1.0, 0.92], 0.97)
evaluate(gru_embeddings, dissimilar)
([0.86, 1.0, 0.91, 0.85, 0.9], 0.9)
同样,我们在不同列表中获得高分。这不是我们想要的句子嵌入。
基于 Transformer 的 BERT 嵌入
以下执行实例化一个池化最后一层的基于bert-base-uncased 的模型:
from flair.embeddings import TransformerDocumentEmbeddings
from flair.data import Sentence
bert_embeddings = TransformerDocumentEmbeddings(
'bert-base-uncased')
evaluate(bert_embeddings, similar)
([0.85, 0.9, 0.96, 0.91, 0.89], 0.9)
evaluate(bert_embeddings, dissimilar)
([0.93, 0.94, 0.86, 0.93, 0.92], 0.92)
这更糟!不相似列表的得分高于相似列表的得分。
句子-BERT 嵌入
现在,让我们申请Sentence-BERT 用于区分相似对和不同对的问题,如下:
- 首先,警告:我们需要确保已经安装了sentence-transformers包:
!pip install sentence-transformers
- 正如我们之前提到的,Sentence-BERT 提供了多种预训练模型。我们将选择bert-base-nli-mean-tokens模型进行评估。这是代码:
from flair.data import Sentence from flair.embeddings import SentenceTransformerDocumentEmbeddings sbert_embeddings = SentenceTransformerDocumentEmbeddings( 'bert-base-nli-mean-tokens')
- 让我们评估模型:
evaluate(sbert_embeddings, similar)
([0.98, 0.95, 0.96, 0.99, 0.98], 0.97)
evaluate(sbert_embeddings, dissimilar)
([0.48, 0.41, 0.19, -0.05, 0.0], 0.21)
做得好!SBERT 模型产生了更好的结果。该模型为不同列表产生了较低的相似性分数,这是我们所期望的。
- 现在,我们将做一个更难的测试,我们将矛盾的句子传递给模型。我们将定义一些棘手的句子对,如下所示:
tricky_pairs=[ ("An elephant is bigger than a lion", "A lion is bigger than an elephant") , ("the cat sat on the mat", "the mat sat on the cat")] evaluate(glove_pool_embeddings, tricky_pairs)
([1.0, 1.0], 1.0)
evaluate(gru_embeddings, tricky_pairs)
([0.87, 0.65], 0.76)
evaluate(bert_embeddings, tricky_pairs)
([1.0, 0.98], 0.99)
evaluate(sbert_embeddings, tricky_pairs)
([0.93, 0.97], 0.95)
有趣的!分数非常高,因为句子相似度模型的工作原理类似于主题检测并测量内容相似度。当我们查看这些句子时,它们共享相同的内容,即使它们相互矛盾。内容是关于狮子和大象或猫和垫子。因此,这些模型产生了很高的相似性分数。由于 GloVe 嵌入方法在不关心词序的情况下汇集了词的平均值,因此它将两个句子测量为相同。另一方面,GRU 模型产生的值较低,因为它关心词序。令人惊讶的是,即使是 SBERT 模型也不能产生有效的分数。这可能是由于 SBERT 模型中使用的基于内容相似性的监督。
- 要正确检测具有三个类别的两个句子对的语义——即中性、矛盾和蕴涵——我们必须在 MNLI 上使用微调模型。以下代码块显示了使用 XLM-Roberta 的示例,在 XNLI 上使用相同的示例进行了微调:
从变压器\
from transformers import AutoModelForSequenceClassification, AutoTokenizer nli_model = AutoModelForSequenceClassification.from_pretrained( 'joeddav/xlm-roberta-large-xnli') tokenizer = AutoTokenizer.from_pretrained( 'joeddav/xlm-roberta-large-xnli') import numpy as np for permise, hypothesis in tricky_pairs: x = tokenizer.encode(premise, hypothesis, return_tensors='pt', truncation_strategy='only_first') logits = nli_model(x)[0] print(f"Permise: {permise}") print(f"Hypothesis: {hypothesis}") print("Top Class:") print(nli_model.config.id2label[np.argmax( logits[0].detach().numpy()). ]) print("Full softmax scores:") for i in range(3): print(nli_model.config.id2label[i], logits.softmax(dim=1)[0][i].detach().numpy()) print("="*20)
- 输出将为每个显示正确的标签:
Permise: An elephant is bigger than a lion Hypothesis: A lion is bigger than an elephant Top Class: contradiction Full softmax scores: contradiction 0.7731286 neutral 0.2203285 entailment 0.0065428796 ==================== Permise: the cat sat on the mat Hypothesis: the mat sat on the cat Top Class: entailment Full softmax scores: contradiction 0.49365467 neutral 0.007260764 entailment 0.49908453 ====================
在某些问题中,在某些问题中,NLI 比语义文本具有更高的优先级,因为它旨在找到矛盾或蕴涵,而不是原始相似度得分。对于下一个示例,同时使用两个句子表示蕴涵和矛盾。这有点主观,但对模型而言,第二句对似乎是蕴涵和矛盾之间的密切联系。
使用 Sentence-BERT 进行文本聚类
用于聚类算法,我们需要一个适合的模型文字相似度。让我们在这里使用paraphrase-distilroberta-base-v1模型进行更改。我们将首先为我们的集群实验加载 Amazon Polarity 数据集。该数据集包括截至 2013 年 3 月的 18 年期间的亚马逊网页评论。原始数据集包括超过 3500 万条评论。这些评论包括产品信息、用户信息、用户评分和用户评论。让我们开始吧:
- 首先,随机抽取10K条评论,如下:
import pandas as pd, numpy as np import torch, os, scipy from datasets import load_dataset dataset = load_dataset("amazon_polarity",split="train") corpus=dataset.shuffle(seed=42)[:10000]['content']
- 语料库现在已准备好进行聚类。以下代码使用预训练的paraphrase-distilroberta-base-v1模型实例化一个句子转换器对象:
from sentence_transformers import SentenceTransformer model_path="paraphrase-distilroberta-base-v1" model = SentenceTransformer(model_path)
- 整个语料库通过以下执行进行编码,其中模型将句子列表映射到嵌入向量列表:
corpus_embeddings = model.encode(corpus) corpus_embeddings.shape
(10000, 768)
- 在这里,向量大小为768,这是默认嵌入BERT-base 模型的大小。从现在开始,我们将继续使用传统的聚类方法。我们将在这里选择Kmeans,因为它是一种快速且广泛使用的聚类算法。我们只需要将簇号 ( K ) 设置为5。实际上,这个数字可能不是最优的。有许多技术可以确定最佳聚类数,例如 Elbow 或 Silhouette 方法。但是,让我们把这些问题放在一边。这是执行:
from sklearn.cluster import KMeans K=5 kmeans = KMeans( n_clusters=5, random_state=0).fit(corpus_embeddings) cls_dist=pd.Series(kmeans.labels_).value_counts() cls_dist
3 2772
4 2089
0 1911
2 1883
1 1345
在这里,我们获得了五组评论。正如我们从输出中看到的,我们有相当分布的集群。集群的另一个问题是我们需要了解这些集群的含义。作为建议,我们可以对每个集群应用主题分析或检查基于集群的 TF-IDF 以了解内容。现在,让我们看看另一种方法来做到这一点集群中心。Kmeans 算法计算聚类中心,称为质心,保存在kmeans.cluster_centers_属性中。质心只是每个集群中向量的平均值。因此,它们都是虚构的点,而不是现有的数据点。让我们假设最接近质心的句子将是相应集群的最具代表性的示例。
- 让我们尝试只找到一个最接近每个质心点的真实句子嵌入。如果你喜欢,你可以捕捉不止一个句子。这是代码:
distances = scipy.spatial.distance.cdist(kmeans.cluster_centers_, corpus_embeddings) centers={} print("Cluster", "Size", "Center-idx", "Center-Example", sep="\t\t") for i,d in enumerate(distances): ind = np.argsort(d, axis=0)[0] centers[i]=ind print(i,cls_dist[i], ind, corpus[ind] ,sep="\t\t")
输出如下:
图 7.7 – 集群的质心
从这些有代表性的句子,我们可以推理集群。似乎 Kmeans 将评论分为五个不同的类别:电子产品、音频 Cd/音乐、DVD 电影、书籍和家具与家居。现在,让我们在 2D 空间中可视化句子点和聚类质心。我们将使用统一流形逼近和投影( UMAP ) 库来减少维度。您可以使用的其他在 NLP 中广泛使用的降维技术包括 t-SNE 和 PCA(请参阅第 1 章,从词袋到变形金刚)。
- 我们需要安装umap库,如下:
!pip install umap-learn
- 以下执行减少了所有嵌入并将它们映射到 2D 空间:
import matplotlib.pyplot as plt import umap X = umap.UMAP( n_components=2, min_dist=0.0).fit_transform(corpus_embeddings) labels= kmeans.labels_fig, ax = plt.subplots(figsize=(12,8)) plt.scatter(X[:,0], X[:,1], c=labels, s=1, cmap='Paired') for c in centers: plt.text(X[centers[c],0], X[centers[c], 1],"CLS-"+ str(c), fontsize=18) plt.colorbar()
图 7.8 – 聚类点可视化
在里面之前的输出,点已根据到他们的集群成员和质心。看起来我们选择了正确数量的集群。
为了捕捉主题并解释集群,我们简单地将句子(每个集群一个句子)定位在靠近集群中心的位置。现在,让我们看一下通过主题建模更准确地捕捉主题的方法。
使用 BERTopic 进行主题建模
你可能熟悉许多无监督主题建模技术用于从文档中提取主题;潜在狄利克雷分配( LDA ) 主题建模和非负矩阵分解( NMF ) 是在文学。BERTopic 和 Top2Vec 是两个重要的基于 Transformer 的主题建模项目。在本节中,我们将 BERTopic 模型应用于我们的亚马逊语料库。它利用 BERT 嵌入和基于类的 TF-IDF 方法来获得易于解释的主题。
首先,BERTopic 模型首先使用句子转换器或任何句子嵌入对句子进行编码模型,然后是聚类步骤。聚类步骤有两个阶段:通过UMAP减少嵌入的维数,然后通过基于层次密度的带噪声应用空间聚类( HDBSCAN ) 对减少的向量进行聚类,这产生类似文档的组。在最后阶段,主题由集群方式的 TF-IDF 捕获,其中模型提取每个集群而不是每个文档的最重要的单词,并获得每个集群的主题描述。让我们开始吧:
- 首先,让我们安装必要的库,如下:
!pip install bertopic
重要的提示
您可能需要重新启动运行时,因为此安装将更新一些已加载的包。因此,从 Jupyter notebook 转到Runtime | 重新启动运行时。
- 如果你想使用自己的嵌入模型,你需要实例化并通过 BERTopic 模型传递它。我们将实例化一个 Sentence Transformer 模型并将其传递给 BERTopic 的构造函数,如下所示:
from bertopic import BERTopic sentence_model = SentenceTransformer( "paraphrase-distilroberta-base-v1") topic_model = BERTopic(embedding_model=sentence_model) topics, _ = topic_model.fit_transform(corpus) topic_model.get_topic_info()[:6]
输出如下:
图 7.9 – BERTopic 结果
请注意,使用相同参数的不同 BERTopic 运行可能会产生不同的结果,因为 UMAP 模型是随机的。现在,我们来看看话题五的词分布,如下:
topic_model.get_topic(5)
输出如下:
图 7.10 – 主题模型的第五个主题词
这主题词是那些向量接近的词语义空间中的主题向量。在这个实验中,我们没有对语料库进行聚类;相反,我们将该技术应用于整个语料库。在我们之前的示例中,我们分析了具有最接近句子的聚类。现在,我们可以通过将主题模型分别应用于每个集群来找到主题。这非常简单,您可以自己运行它。
请参阅 Top2Vec 项目有关更多详细信息和有趣的主题建模应用程序,请访问GitHub - ddangelov/Top2Vec: Top2Vec learns jointly embedded topic, document and word vectors.。
使用 Sentence-BERT 进行语义搜索
我们可能已经熟悉基于关键字的搜索(布尔模型),对于给定的关键字或模式,我们可以检索与模式匹配的结果。或者,我们可以使用正则表达式,我们可以在其中定义高级模式,例如 lexico-syntactic 模式。这些传统方法无法处理同义词(例如,汽车与汽车相同)或词义问题(例如,将银行作为河流或银行的一侧)作为金融机构)。第一个同义词案例由于错过了不应错过的文档而导致召回率低,而第二个同义词案例由于捕获了不被捕获的文档而导致精度低。基于向量或语义的搜索方法可以通过构建查询和文档的密集数字表示来克服这些缺点。
让我们为网站上闲置的常见问题( FAQ )建立一个案例研究。我们将在语义搜索问题中利用常见问题解答资源。常见问题解答包含常见问题。我们将使用自然非政府组织世界自然基金会( WWF ) 的常见问题解答 ( The World Wildlife Fund: for people and nature to thrive | WWF )。
鉴于这些描述,很容易理解使用语义模型执行语义搜索与一次性学习问题非常相似,其中我们只有一个类(单个样本),并且我们想要重新排序根据它的其余数据(句子)。您可以将问题重新定义为搜索语义上接近给定样本的样本,或者根据样本进行二元分类。您的模型可以提供相似度指标,所有其他样本的结果将使用该指标重新排序。最终的有序列表是搜索结果,它根据语义表示和相似度度量重新排序。
WWF 在其网页上有 18 个问题和答案。在这个实验中,我们将它们定义为一个名为wf_faq的 Python 列表对象:
- 我还没有收到我的收养包。我应该怎么办?
- 我多久能收到我的收养包?
- 我怎样才能更新我的收养?
- 如何更改我的地址或其他联系方式?
- 如果我不住在英国,我可以领养动物吗?
- 如果我收养一只动物,我会是唯一收养那只动物的人吗?
- 我的包裹没有证书?
- 我的收养是礼物,但不会准时到达。我能做些什么?
- 我可以一次性支付收养费用吗?
- 我可以在下单后更改领养包的收货地址吗?
- 我的收养能持续多久?
- 我多久会收到关于我收养的动物的更新?
- 你有什么动物可以收养?
- 我怎样才能找到关于我收养的动物的更多信息?
- 我的收养钱是怎么花的?
- 你们的退款政策是什么?
- 我的直接付款出现错误;我可以收到退款吗?
- 如何更改您与我的联系方式?
用户可以自由提出他们想要的任何问题。我们需要评估FAQ中哪个问题与用户的问题最相似,这是quora-distilbert-base模型的目标。SBERT 中心有两种选择——一种是英语,另一种是多语种,如下所示:
- quora-distilbert-base:这是针对 Quora 重复问题检测检索进行了微调。
- quora-distilbert-multilingual:这是quora-distilbert-base的多语言版本。它使用 50 多种语言的并行数据进行了微调。
- 以下是 SBERT 模型的实例化:
from sentence_transformers import SentenceTransformer model = SentenceTransformer('quora-distilbert-base')
- 让我们对 FAQ 进行编码,如下所示:
faq_embeddings = model.encode(wwf_faq)
- 让我们准备五个问题,使其分别与FAQ中的前五个问题相似;也就是我们的第一个测试问题应该和FAQ中的第一个问题相似,第二个问题应该和第二个问题相似,以此类推,这样我们就可以很容易地跟踪结果。让我们在test_questions列表对象中定义问题并对其进行编码,如下所示:
test_questions=["What should be done, if the adoption pack did not reach to me?", " How fast is my adoption pack delivered to me?", "What should I do to renew my adoption?", "What should be done to change address and contact details ?", "I live outside of the UK, Can I still adopt an animal?"] test_q_emb= model.encode(test_questions)
- 以下代码测量每个测试问题与 FAQ 中每个问题之间的相似度,然后对它们进行排名:
from scipy.spatial.distance import cdist for q, qe in zip(test_questions, test_q_emb): distances = cdist([qe], faq_embeddings, "cosine")[0] ind = np.argsort(distances, axis=0)[:3] print("\n Test Question: \n "+q) for i,(dis,text) in enumerate( zip(distances[ind],[wwf_faq[i] for i in ind])): print(dis,ind[i],text, sep="\t")
图 7.11 – 问题-问题相似度
在这里,我们可以依次看到索引0、1、2、3和4,这意味着模型成功地找到了预期的类似问题。
- 对于部署,我们可以设计如下getBest()函数,它接受一个问题并返回FAQ中最相似的K个问题:
def get_best(query, K=5): query_emb = model.encode([query]) distances = cdist(query_emb,faq_embeddings,"cosine")[0] ind = np.argsort(distances, axis=0) print("\n"+query) for c,i in list(zip(distances[ind], ind))[:K]: print(c,wwf_faq[i], sep="\t")
- 让我们问一个问题:
get_best("How do I change my contact info?",3)
输出如下:
图 7.12 – 相似问题相似度结果
- 如果用作输入的问题与常见问题解答中的问题不相似怎么办?这是一个这样的问题:
get_best("我如何获得我的机票\
get_best("How do I get my plane ticket if I bought it online?")
输出如下:
图 7.13 – 不同的问题相似度结果
最佳相异性得分为 0.35。因此,我们需要定义一个阈值,例如 0.3,以便模型忽略高于该阈值的问题,并且说没有找到类似的答案。
除了问题-问题对称搜索相似度之外,我们还可以利用 SBERT 的问答非对称搜索模型,例如msmarco-distilbert-base-v3,它是在大约 500K Bing 搜索查询的数据集上训练的。它被称为 MSMARCO Passage Ranking。这个模型帮助我们衡量问题和上下文的相关程度,并检查问题的答案是否在文章中。
概括
在本章中,我们学习了文本表示方法。我们了解了如何使用不同且多样化的语义模型来执行诸如零/少数/一次性学习之类的任务。我们还了解了 NLI 及其在捕获文本语义方面的重要性。此外,我们还研究了一些有用的用例,例如语义搜索、语义聚类和使用基于 Transformer 的语义模型的主题建模。我们学习了如何可视化聚类结果并了解质心在此类问题中的重要性。
在下一章中,您将了解高效的 Transformer 模型。您将了解基于 Transformer 的模型的蒸馏、修剪和量化。您还将了解不同且高效的 Transformer 架构,这些架构可以提高计算和内存效率,以及如何在 NLP 问题中使用它们。