到目前为止,我们已经使用 transformers 库解决了分类和生成问题。文本表示是现代自然语言处理(NLP)中的另一个关键任务,尤其适用于无监督任务,如聚类、语义搜索和主题建模。本文将介绍如何使用各种模型(如 Universal Sentence Encoder (USE) 和 Sentence-BERT (SBERT))以及附加框架(如 SentenceTransformers)来表示句子。还将解释如何利用 BART 进行零样本学习。你将学习到如何利用它。我们还将描述几样本学习方法和无监督用例,如语义文本聚类和主题建模。最后,还会涵盖一次性学习用例,如语义搜索。尽管嵌入和文本表示模型大多基于语义范式或特定主题,我们还将讨论如何通过指令微调的嵌入模型来解决多样化的任务。
本章将涵盖以下主题:
- 介绍句子嵌入
- 句子相似性模型的基准测试
- 使用 BART 进行零样本学习
- 使用 FLAIR 进行语义相似性实验
- 使用 Sentence-BERT 进行文本聚类
- 使用 Sentence-BERT 进行语义搜索
- 指令微调的嵌入模型
技术要求
我们将使用 Jupyter Notebook 来运行我们的编码练习。为此,你需要 Python 3.6+ 和以下软件包:
- sklearn
- transformers >=4.00
- datasets
- sentence-transformers
- tensorflow-hub
- flair
- umap-learn
- bertopic
本章所有编码练习的笔记本将可以通过以下 GitHub 链接获得:github.com/PacktPublis…
句子嵌入简介
预训练的 BERT 模型并不会产生高效且独立的句子嵌入,因为它们总是需要在端到端的有监督设置中进行微调。这是因为我们可以将预训练的 BERT 模型视为一个不可分割的整体,语义分布在所有层中,而不仅仅是最后一层。没有微调的情况下,独立使用其内部表示可能效果不佳。处理无监督任务(如聚类、主题建模、信息检索或语义搜索)也较为困难。以聚类任务为例,我们需要评估许多句子对,这会导致巨大的计算开销。
幸运的是,许多修改已经应用于原始 BERT 模型,如 SBERT,以衍生出语义上有意义且独立的句子嵌入。SBERT 与 BERT 使用了不同的目标函数。在这种方法中,句子可以通过双网络配对,并通过相似性进行评估。我们将稍后讨论这些方法。在 NLP 文献中,已经提出了许多神经句子嵌入方法,这些方法将单个句子映射到一个共同的特征空间(向量空间模型),其中通常使用余弦函数(或点积)来测量相似性,使用欧几里得距离来测量不相似性。
以下是一些可以通过句子嵌入高效解决的应用:
- 句子对任务
- 信息检索
- 问题回答
- 重复问题检测
- 释义检测
- 文档聚类
- 主题建模
最简单但最有效的神经句子嵌入方式是平均池化操作,它是在句子中的词嵌入上进行的。为了获得更好的表示,一些早期的神经方法以无监督的方式学习句子嵌入,如 Doc2Vec、Skip-Thought、FastSent 和 Sent2Vec。Doc2Vec 利用词级分布理论和目标函数来预测相邻词,类似于 word2Vec。该方法在每个句子中注入了一个额外的记忆标记(称为 Paragraph-ID),类似于 transformers 库中的 CLS 或 SEP 标记。这个额外的标记作为表示上下文或文档嵌入的记忆。SkipThought 和 FastSent 被认为是句子级方法,其中目标函数用于预测相邻句子。这些模型提取句子的含义,以从相邻句子及其上下文中获取必要信息。
一些其他方法,如 InferSent,利用了有监督学习和多任务迁移学习来学习通用的句子嵌入。InferSent 通过训练各种有监督任务来获得更高效的嵌入。基于递归神经网络(RNN)的有监督模型,如 GRU 或长短期记忆(LSTM),使用最后的隐藏状态(或堆叠的所有隐藏状态)来获得有监督设置中的句子嵌入。我们在第 1 章中已经提到过 RNN 方法。
交叉编码器与双编码器
到目前为止,我们已经讨论了如何训练基于 transformer 的语言模型,并分别在半监督和有监督设置中进行微调。正如我们在前面的章节中所学,我们成功的结果得益于 transformer 架构。一旦在预训练模型的顶部添加了任务特定的薄线性层,网络的所有权重(不仅仅是最后的任务特定薄层)都会通过任务特定的标记数据进行微调。我们还体验了如何在不需要任何架构修改的情况下,对 BERT 架构进行微调以适应两组不同的任务(单句子或句子对)。唯一的区别是,对于句子对任务,句子会被拼接在一起,并用 SEP 标记标记。因此,自注意力会应用于拼接句子的所有标记。这是 BERT 模型的一个巨大优势,在每一层中,两个输入句子可以相互获取必要的信息。最终,它们会被同时编码。这被称为交叉编码。
然而,关于交叉编码器有两个缺点,这些缺点已由 SBERT 作者和 Humeau 等人在 2019 年提出:
- 交叉编码器设置对于许多句子对任务并不方便,因为需要处理的组合过多。例如,要从 1,000 个句子中找出两个最接近的句子,交叉编码器模型(BERT)需要大约 500,000 次(n * (n-1) /2)推理计算。因此,相比于 SBERT 或 Universal Sentence Encoder (USE) 等替代解决方案,它会非常慢,并且在现代架构上可以高效地执行相似性函数。此外,通过优化的索引结构,我们可以将计算复杂度从几个小时降低到几分钟,用于比较或聚类许多文档。
- 由于其有监督的特性,BERT 模型无法衍生出独立有意义的句子嵌入。很难直接利用预训练的 BERT 模型进行无监督任务,如聚类、语义搜索或主题建模。BERT 模型为文档中的每个标记产生一个固定大小的向量。在无监督设置中,文档级表示可能通过对标记向量、SEP 和 CLS 标记进行平均或池化来获得。稍后我们将看到,BERT 的这种表示产生的句子嵌入低于平均水平,并且其性能分数通常比 word2Vec、FastText 或 GloVe 等词嵌入池化技术要差。
另外,双编码器(如 SBERT)独立地将句子对映射到语义向量空间,如下图所示。由于表示是分开的,双编码器可以缓存每个输入的编码表示,从而实现快速的推理时间。SBERT 是 BERT 的一种成功的双编码器修改版。基于 Siamese 和 Triplet 网络结构,SBERT 微调了 BERT 模型,以生成语义上有意义且独立的句子嵌入。
下图展示了双编码器架构:
你可以在 public.ukp.informatik.tu-darmstadt.de/reimers/sen… 找到数百个已经训练过的 SBERT 模型,这些模型使用了不同的目标。我们将在下一节中使用其中的一些模型。
句子相似性模型的基准测试
有很多语义文本相似性模型可用,但强烈建议你使用指标对它们进行基准测试,以理解它们的能力和差异。Papers With Code 提供了这些数据集的列表,链接为 paperswithcode.com/task/semant…。
此外,每个数据集中都有许多模型输出,这些输出按结果排名。这些结果来源于上述文章。
General Language Understanding Evaluation (GLUE) 提供了大多数这些数据集和测试,但它不仅仅用于语义文本相似性。GLUE 是一个通用的基准,用于评估具有不同 NLP 特征的模型。第 2 章提供了有关 GLUE 数据集及其使用的更多详细信息。在我们继续之前,让我们先看一下:
要加载指标和一些数据集,我们导入 datasets 函数,如下所示:
from datasets import load_metric, load_dataset
假设模型产生了值,并且这些值存储在一个名为 predictions 的数组中。你可以轻松地使用这些指标与预测值来查看 F1 和准确性值:
labels = [i['label'] for i in dataset['test']]
metric.compute(predictions=predictions, references=labels)
一些语义文本相似性数据集,如 Semantic Textual Similarity Benchmark (STSB),具有不同的指标。例如,这个基准使用了斯皮尔曼和皮尔逊相关性,因为输出和预测值在 0 到 5 之间,并且是浮点数,而不是多个 0 和 1 值,这属于回归问题。以下代码展示了这个基准的一个示例:
metric = load_metric('glue', 'stsb')
metric.compute(predictions=[1,2,3],references=[5,2,2])
预测值是模型的输出,而参考值是数据集的标签。
为了获得两个模型之间的比较结果,我们将使用 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。这些小修改只是为了替换嵌入函数,该函数在 TensorFlow Hub 模型和 transformers 中是不同的。以下是修改后的函数:
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'])
对两个结果使用指标会生成斯皮尔曼和皮尔逊相关性值:
results = {
"USE": stsb_metric.compute(
predictions=use_results,
references=references),
"DistilRoberta": stsb_metric.compute(
predictions=distilroberta_results,
references=references)
}
你可以简单地使用 pandas 以比较的方式查看结果表:
import pandas as pd
pd.DataFrame(results)
输出结果如下:
在本节中,你了解了语义文本相似性的关键基准。无论模型是什么,你都学会了如何使用这些指标来量化模型的性能。在下一节中,你将学习少样本学习模型。
使用 BART 进行零样本学习
在机器学习领域,零样本学习指的是一种模型可以在没有明确训练的情况下执行某个任务。在 NLP 的情况下,假设有一个模型可以预测一些文本被分配到模型给定的类中的概率。然而,这种学习的有趣之处在于模型并没有在这些类别上进行训练。
随着许多先进语言模型的崛起,这些模型能够进行迁移学习,零样本学习应运而生。在 NLP 的情况下,这种学习是在测试时由 NLP 模型执行的,此时模型会看到属于新类别的样本,而这些样本之前没有出现过。
这种学习通常用于分类任务,其中类别和文本都被表示,并比较两者的语义相似性。这两者的表示形式是嵌入向量,而相似性度量(如余弦相似性或预训练分类器,如密集层)输出句子/文本被分类为某一类别的概率。
我们可以使用许多方法和方案来训练这样的模型,但最早的方法之一是使用网络上包含关键字标签的爬取页面。有关更多信息,请阅读以下文章和博客帖子:Zero-Shot Text Classification。
除了使用如此庞大的数据外,还有语言模型,如 BART,使用 Multi-Genre Natural Language Inference (MultiNLI) 数据集进行微调,以检测两个不同句子之间的关系。此外,Hugging Face 模型库中包含了许多实现了零样本学习的模型,它们还提供了易于使用的零样本学习管道。
例如,Meta 的 Fundamental AI Research (FAIR) 的 BART 被用来执行零样本文本分类,示例如下:
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)
结果如下:
正如你所看到的,“旅行”和“探索”标签的概率最高,但最可能的标签是“旅行”。
然而,有时一个样本可以属于多个类别(多标签)。Hugging Face 提供了一个名为 multi_label 的参数来处理这种情况。以下示例使用了这个参数:
result = classifier(sequence_to_classify,
candidate_labels,
multi_label=True)
pd.DataFrame(result)
因此,结果会被更改为如下:
你可以进一步测试结果,看看如果使用与“旅行”标签非常相似的标签,模型的表现如何。例如,你可以看看当将“移动”和“前往”添加到标签列表时,它的表现如何。还有其他模型也利用标签与上下文之间的语义相似性来执行零样本分类。在少样本学习的情况下,会给模型提供一些样本,但这些样本不足以单独训练模型。模型可以使用这些样本执行诸如语义文本聚类之类的任务,这将在稍后解释。
现在你已经学会了如何使用BART进行零样本学习,接下来你应该了解它是如何工作的。BART经过自然语言推理(NLI)数据集(如MultiNLI)的微调训练。这些数据集包含成对的句子,并为每对句子提供三个类别标签。这些类别标签分别是中立(Neutral)、蕴含(Entailment)和矛盾(Contradiction)。在这些数据集上训练过的模型可以捕捉到两句话的语义,并通过分配一个独热格式的标签来对其进行分类。如果你只使用“蕴含”和“矛盾”作为输出标签,去掉“中立”标签,那么如果两句话能够连在一起,这就意味着它们是密切相关的。换句话说,你可以将第一句话改为标签(例如“旅行”),将第二句话改为内容(例如“一天我会环游世界”)。根据这种逻辑,如果这两句话能够接在一起,就意味着标签和内容在语义上是相关的。以下代码示例展示了如何根据上述描述直接使用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
为了进行定性评估,我们定义了一组相似的句子对和一组不相似的句子对(每组五对)。我们期望嵌入模型能分别测量出高分和低分。
这些句子对是从语义文本相似性(STS)基准数据集中提取的,我们在第六章的句子对回归部分中已经熟悉了它们。对于相似的句子对,两句话是完全等价的,它们具有相同的含义。在STS-B数据集中,我们随机选择了相似度得分约为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"])
输出如下所示:
以下是从STS基准数据集(链接)中提取的相似度得分约为0的不相似句子列表:
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"])
输出如下所示:
现在,让我们准备必要的函数来评估嵌入模型。以下 sim() 函数计算两个句子之间的余弦相似度,即 s1 和 s2:
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)
在本次实验中使用的文档嵌入模型都是预训练模型。我们将把文档嵌入模型对象和句子对列表(相似或不相似)传递给以下 evaluate() 函数,其中,一旦模型对句子进行编码嵌入,它将计算列表中每对句子的相似度得分,并计算出列表的平均值。函数定义如下:
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)
# Output: ([0.97, 0.99, 0.97, 0.99, 0.98], 0.98)
结果看起来不错,因为这些结果值非常高,这是我们所期望的。然而,该模型在不相似句子列表上的平均得分也高达0.94。我们的期望值应低于0.4。关于为什么会出现这种情况,我们将在本章后面讨论。执行如下:
evaluate(glove_pool_embeddings, dissimilar)
# Output: ([0.94, 0.97, 0.94, 0.92, 0.93], 0.94)
接下来,让我们在同样的问题上评估一些基于RNN的嵌入。
基于RNN的文档嵌入
让我们基于GloVe嵌入实例化一个GRU模型,其中 DocumentRNNEmbeddings 的默认模型是GRU:
from flair.embeddings import (
WordEmbeddings, DocumentRNNEmbeddings)
gru_embeddings = DocumentRNNEmbeddings([glove_embedding])
运行评估方法:
evaluate(gru_embeddings, similar)
# Output: ([0.99, 1.0, 0.94, 1.0, 0.92], 0.97)
evaluate(gru_embeddings, dissimilar)
# Output: ([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)
# Output: ([0.85, 0.9, 0.96, 0.91, 0.89], 0.9)
evaluate(bert_embeddings, dissimilar)
# Output: ([0.93, 0.94, 0.86, 0.93, 0.92], 0.92)
这更糟糕!不相似句子列表的得分比相似句子列表还高。
SBERT嵌入
现在,让我们将SBERT应用于区分相似句子对和不相似句子对的问题,如下所示:
首先,一个提示:我们需要确保已经安装了 sentence-transformers 包:
!pip install sentence-transformers
如前所述,SBERT提供了多种预训练模型。我们将选择 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)
# Output: ([0.98, 0.95, 0.96, 0.99, 0.98], 0.97)
evaluate(sbert_embeddings, dissimilar)
# Output: ([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)
# Output: ([1.0, 1.0], 1.0)
evaluate(gru_embeddings, tricky_pairs)
# Output: ([0.87, 0.65], 0.76)
evaluate(bert_embeddings, tricky_pairs)
# Output: ([1.0, 0.98], 0.99)
evaluate(sbert_embeddings, tricky_pairs)
# Output: ([0.93, 0.97], 0.95)
有趣的是!得分非常高,因为句子相似度模型的工作方式类似于主题检测,并衡量内容的相似性。尽管这些句子彼此矛盾,但它们共享相同的内容。当我们查看这些句子时,它们涉及相同的内容,例如狮子和大象或猫和垫子。因此,模型产生了高相似度得分。由于GloVe嵌入方法在不考虑词序的情况下对词进行平均池化,因此它测量两句话是相同的。另一方面,GRU模型产生了较低的值,因为它关心词序。令人惊讶的是,即使是SBERT模型也没有产生有效的分数。这可能是由于SBERT模型中使用了基于内容相似性的监督。
要正确检测具有三个类别的句子对的语义,即“中立”(Neutral)、“矛盾”(Contradiction)和“蕴含”(Entailment),我们必须使用在MultiNLI上微调的模型。以下代码块展示了如何使用在XNLI上微调的XLM-RoBERTa,并使用相同的例子:
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 premise, hypothesis in tricky_pairs:
x = tokenizer.encode(premise,
hypothesis,
return_tensors='pt',
truncation_strategy='only_first')
logits = nli_model(x)[0]
print(f"Premise: {premise}")
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)
输出将显示每个例子的正确标签:
Premise: 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
====================
Premise: 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
====================
模型在第一个例子中表现出色,但在第二个例子中结果不确定。现在,我们将使用SBERT对文本应用聚类算法作为无监督学习模型。
使用Sentence-BERT进行文本聚类
对于聚类算法,我们需要一个适合文本相似度的模型。这里我们将使用 paraphrase-distilroberta-base-v1 模型进行尝试。我们将从加载Amazon Polarity数据集开始我们的聚类实验。该数据集包括截至2013年3月的18年间的Amazon网页评论。原始数据集包含超过3500万条评论。这些评论包括产品信息、用户信息、用户评分和用户评论。让我们开始吧:
首先,随机选择1万条评论,代码如下:
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
# Output: (10000, 768)
在这里,向量大小为768,这是BERT基本模型的默认嵌入大小。接下来,我们将继续使用传统的聚类方法。我们将选择k-means聚类算法,因为它是一种快速且广泛使用的聚类算法。我们只需要将聚类数量(k)设置为5。实际上,这个数字可能不是最佳的。可以使用许多技术来确定最佳的聚类数量,例如肘部法或轮廓法。然而,在这里我们暂时不考虑这些问题。以下是代码执行:
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
# Output:
# 3 2772
# 4 2089
# 0 1911
# 2 1883
# 1 1345
在这里,我们获得了五个评论聚类。从输出可以看出,我们的聚类分布相对均匀。聚类的另一个问题是我们需要理解这些聚类的含义。作为一种建议,我们可以对每个聚类进行主题分析或检查基于聚类的TF-IDF(词频-逆文档频率)以了解内容。现在,让我们看看另一种基于聚类中心的方法。k-means算法计算出聚类中心,称为质心,保存在 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")
输出如下:
从这些具有代表性的句子中,我们可以推测出聚类的类别。看起来k-means将评论聚类为五个不同的类别:电子产品(Electronics)、音频CD/音乐(Audio CD/Music)、DVD/电影(DVD/Film)、书籍(Books)、家具和家居(Furniture and Home)。现在,让我们在二维空间中可视化句子点和聚类质心。我们将使用统一流形近似与投影(UMAP)库来降低维度。在自然语言处理(NLP)中,你还可以使用其他广泛应用的降维技术,例如t-SNE(t分布随机邻居嵌入)和主成分分析(PCA)。
我们需要安装umap库,命令如下:
!pip install umap-learn
以下代码执行将所有嵌入降维并将其映射到二维空间:
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()
输出结果如下所示:
在前面的输出中,各点已根据其所属的聚类和质心进行了着色。看起来我们选择了正确的聚类数量。为了捕捉主题并解释这些聚类,我们找到了接近聚类质心的句子(每个聚类一个句子)。现在,让我们看看如何通过主题建模更准确地捕捉主题。
使用BERTopic进行主题建模
你可能熟悉许多用于从文档中提取主题的无监督主题建模技术;潜在狄利克雷分配(LDA)主题建模和非负矩阵分解(NMF)是文献中广泛应用的传统技术。BERTopic和top2Vec是两个重要的基于Transformer的主题建模项目。在本节中,我们将把BERTopic模型应用于我们的Amazon语料库。该模型利用BERT嵌入和基于类别的TF-IDF方法来获取易于解释的主题。
首先,BERTopic模型通过使用句子转换器或任何句子嵌入模型对句子进行编码。接下来是聚类步骤,这一过程包括两个阶段:首先使用UMAP对嵌入进行降维,然后使用带噪声的分层密度空间聚类(HDBSCAN)对降维后的向量进行聚类。这一过程将相似的文档分组在一起。在最后阶段,模型使用基于聚类的TF-IDF捕捉主题。与逐个文档提取最重要的词不同,该模型按聚类提取最重要的词,为每个聚类提供主题描述。让我们开始吧:
首先,安装必要的库,如下所示:
!pip install bertopic
重要提示
你可能需要重启运行环境,因为此安装会更新一些已经加载的包。因此,在Jupyter Notebook中,前往 Runtime | Restart Runtime。
如果你想使用自己的嵌入模型,需要实例化并将其传递给BERTopic模型。我们将实例化一个SentenceTransformer模型并将其传递给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]
输出如下所示:
请注意,由于UMAP模型是随机的,因此使用相同参数运行BERTopic可能会产生不同的结果。现在,让我们看看第五个主题的词分布,如下所示:
topic_model.get_topic(5)
输出如下所示:
主题词是那些在语义空间中与主题向量接近的词语。在这个实验中,我们并没有对语料库进行聚类,而是将技术应用于整个语料库。在前面的示例中,我们通过最接近的句子来分析聚类。现在,我们可以通过将主题模型分别应用于每个聚类来找到主题。这相当简单,你可以自行运行。
有关更多详细信息和有趣的主题建模应用,请参阅Top2Vec项目。
在接下来的子章节中,我们将使用SBERT进行语义搜索。
使用SBERT进行语义搜索
我们可能已经熟悉基于关键字的搜索(布尔模型),在这种搜索中,对于给定的关键字或模式,我们可以检索与该模式匹配的结果。或者,我们可以使用正则表达式,在其中可以定义高级模式,例如词汇-句法模式。然而,这些传统方法无法处理同义词问题(例如,car与automobile相同)或词义问题(例如,bank可以指河岸或银行)。第一个同义词问题会导致漏掉不应漏掉的文档,从而导致召回率低,而第二个问题会导致捕捉到不相关的文档,从而导致精度低。
让我们为常见问题解答(FAQs)设置一个案例研究,这些问题在网站上通常处于空闲状态。我们将在语义搜索问题中利用FAQ资源。我们将使用世界自然基金会(WWF)的FAQ,这是一个自然保护非政府组织(www.wwf.org.uk/)。
考虑到这些描述,很容易理解,使用语义模型进行语义搜索与一次性学习问题非常相似,在一次性学习问题中,我们只有一个类的单个样本,并希望根据它重新排序其余的数据(句子)。你可以将问题重新定义为寻找与给定样本语义上接近的样本,或者根据样本进行二元分类。你的模型可以提供一个相似性度量,并且其他所有样本的结果将根据该度量重新排序。最终的有序列表就是根据语义表示和相似性度量重新排序的搜索结果。
WWF在其网页上有18个问题及其答案。我们将它们定义为一个名为 wf_faq 的Python列表对象,以用于此实验:
1. I haven’t received my adoption pack. What should I do?
2. How quickly will I receive my adoption pack?
3. How can I renew my adoption?
4. How do I change my address or other contact details?
5. Can I adopt an animal if I don’t live in the UK?
6. If I adopt an animal, will I be the only person who adopts that animal?
7. My pack doesn’t contain a certificate?
8. My adoption is a gift but won’t arrive on time. What can I do?
9. Can I pay for an adoption with a one-off payment?
10. Can I change the delivery address for my adoption pack after I’ve placed my order?
11. How long will my adoption last for?
12. How often will I receive updates about my adopted animal?
13. What animals do you have for adoption?
14. How can I find out more information about my adopted animal?
15. How is my adoption money spent?
16. What is your refund policy?
17. An error has been made with my Direct Debit payment; can I receive a refund?
18. How do I change how you contact me?
用户可以自由地问任何他们想问的问题。我们需要评估FAQ中哪个问题与用户的问题最相似,这是 quora-distilbert-base 模型的目标。SBERT Hub中有两个选项——一个用于英文,另一个用于多语言,具体如下:
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")
输出如下所示:
在这里,我们可以按顺序看到索引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)
输出如下所示:
如果作为输入的问题与FAQ中的任何一个问题都不相似,会发生什么呢?下面是这样一个问题:
get_best("How do I get my plane ticket if I bought it online?")
输出如下所示:
最佳的不相似度分数是0.35。因此,我们需要定义一个阈值,例如0.3,这样模型就会忽略那些高于此阈值的问题,并提示未找到相似答案。
除了问题-问题的对称性搜索相似性之外,我们还可以利用SBERT的基于问题-答案的非对称性搜索模型,例如 msmarco-distilbert-base-v3,该模型在大约50万条Bing搜索查询数据集上进行了训练。这种方法被称为段落排名(Passage Ranking)。此模型帮助我们衡量问题与上下文的相关性,并检查答案是否在段落中。
现在,我们将讨论指令微调的嵌入。这些嵌入不仅仅关注文本的嵌入,还允许我们使用指令来处理任务。
指令微调嵌入模型
单一的文本表示模型可以针对特定任务进行训练。例如,一个训练用于给定文本语义表示的模型,可能并不适合表示科学文本,因为这种范围内的相似性与语义相似性不同。指令微调是近年来提出的一种方法,许多新方法都采用了这种方法来在单一模型中解决多种问题。Instructor模型就是这些模型之一。在本节中,我们将展示一些该模型的工作示例。
要导入模型,你可以使用Hugging Face transformers或InstructEmbedding库。两者提供相同的结果,但后者使用起来更加方便。要安装它,只需使用以下命令:
pip install InstructorEmbedding
请注意,sentence-transformers库也必须安装:
pip install sentence-transformers
要加载模型,你可以简单地使用以下代码:
from InstructorEmbedding import INSTRUCTOR
model = INSTRUCTOR('hkunlp/instructor-large')
这将自动加载模型,类似于Hugging Face的用法,因为它们使用的是相同的底层技术。
他们对模型进行了训练,使其遵循以下指令格式:
Represent the domain text_type for task_objective:
在此格式中,text_type 指的是文本的类型(例如金融、科学等),而 objective 用于各种任务,例如聚类、语义相似性等。
例如,你可以使用以下格式来表示用于聚类的医学句子:
import sklearn.cluster
sentences = [['Represent the Medical sentence for clustering: ','COVID has been striking all over globe from 2019.'],
['Represent the Medical sentence for clustering: ','The coronavirus that causes COVID-19 is officially called SARS-CoV-2, which stands for severe acute respiratory syndrome coronavirus 2.'],
['Represent the Medical sentence for clustering: ','The name of the illness caused by the coronavirus SARS-CoV-2. COVID-19 stands for "coronavirus disease 2019."'],
['Represent the Medical sentence for clustering: ',"HIV (human immunodeficiency virus) is a virus that attacks the body's immune system."],
['Represent the Medical sentence for clustering: ','If HIV is not treated, it can lead to AIDS (acquired immunodeficiency syndrome).']]
embeddings = model.encode(sentences)
clustering_model = sklearn.cluster.MiniBatchKMeans(n_clusters=2)
clustering_model.fit(embeddings)
cluster_assignment = clustering_model.labels_
print(cluster_assignment)
结果应该如下所示:
[0 0 0 1 1]
同样,你也可以将其用于非常不同的任务,例如从Wikipedia页面中匹配问题和答案。这个任务是一个基于信息检索的任务,需要检索与给定问题最接近的答案。作为示例,你可以使用以下代码:
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
query = [['Represent the Wikipedia question for retrieving supporting documents: ','What is sea sheep?']]
corpus = [['Represent the Wikipedia document for retrieval: ','Costasiella kuroshimae—also known as a "leaf slug", "leaf sheep", or "salty ocean caterpillar"—is a species of sacoglossan sea slug.'],
['Represent the Wikipedia document for retrieval: ',"Artificial intelligence (AI) is intelligence demonstrated by machines, as opposed to intelligence of humans and other animals."],
['Represent the Wikipedia document for retrieval: ','In the fields of medicine, biotechnology and pharmacology, drug discovery is the process by which new candidate medications are discovered.']]
query_embeddings = model.encode(query)
corpus_embeddings = model.encode(corpus)
similarities = cosine_similarity(query_embeddings, corpus_embeddings)
retrieved_doc_id = np.argmax(similarities)
print(retrieved_doc_id)
结果应该是:
0
这意味着第一个答案是语料库中的第一个项目。正如你从这个示例中看到的,问题的嵌入与答案相比使用了略有不同的指令。
指令微调为用单一模型解决多样化问题开辟了一条新途径。这个单一模型有能力理解并通过给定的指令解决不同的问题,而不是仅作为一个静态的单一用途问题求解器。
总结
在本章中,我们学习了文本表示方法。我们了解到如何使用不同且多样化的语义模型来执行零样本、少样本和单样本学习任务。我们还了解了NLI及其在捕捉文本语义中的重要性。此外,我们还研究了一些有用的应用场景,如使用基于Transformer的语义模型进行语义搜索、语义聚类和主题建模。我们学习了如何可视化聚类结果,并理解了质心在此类问题中的重要性。我们还描述了可以根据给定指令创建表示的指令微调多任务模型。
在下一章中,你将学习高效的Transformer模型。你将学习如何对基于Transformer的模型进行蒸馏、剪枝和量化。你还将了解不同的高效Transformer架构,它们在计算和内存效率上有所改进,以及如何将它们应用于NLP问题。