原文:zh.annas-archive.org/md5/8ec582b7b8053e580e33bbe0a0c5a460

译者:飞龙

协议:CC BY-NC-SA 4.0

第一章: 文本分类

自然语言处理和机器学习中最常见的任务之一是分类。该任务的目标是训练一个模型,为一些输入文本分配标签或类别。文本分类在全球范围内用于广泛的应用,从情感分析和意图检测到实体提取和语言检测。

大型语言模型对分类的影响不容小觑。这些模型的加入迅速成为这类任务的默认选择。

在本章中,我们将讨论使用大型语言模型进行文本分类的多种方法。由于文本分类的广泛领域,将讨论多种技术和应用案例。本章还可以很好地引入 LLM,因为大多数模型都可以用于分类。

我们将重点利用预训练的 LLM,这些模型已经在大量数据上进行训练,可以用于文本分类。对这些模型进行文本分类和领域适应的微调将在第十章中详细讨论。

让我们从最基本的应用和技术开始,完全监督的文本分类。

监督文本分类

分类有多种形式,例如稍微学习(few-shot)和零学习(zero-shot)分类,我们将在本章后面讨论,但最常用的方法是完全监督的分类。这意味着在训练过程中,每个输入都有一个目标类别,模型可以从中学习。

对于使用文本数据作为输入的监督分类,通常遵循一个常见的程序。如图 1-1 所示,我们首先使用特征提取模型将文本输入转换为数值表示。传统上,这样的模型将文本表示为词袋,简单地计算一个单词在文档中出现的次数。然而,在本书中,我们将重点关注 LLM 作为我们的特征提取模型。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/categorizing_text_559117_01.png

图 1-1. 监督分类的示例。我们能否预测电影评论是正面还是负面?

然后,我们在数值表示上训练分类器,例如嵌入(还记得第 X 章吗?),以对文本数据进行分类。分类器可以是多种形式,例如神经网络或逻辑回归。它甚至可以是许多 Kaggle 竞赛中使用的分类器,即 XGBoost!

在这个流程中,我们总是需要训练分类器,但可以选择微调整个 LLM、其中某些部分或保持不变。如果我们选择不进行微调,我们称这一过程为冻结其层。这意味着在训练过程中这些层无法更新。然而,至少解冻一些层可能是有益的,这样大型语言模型就可以针对特定的分类任务进行微调。该过程在图 1-2 中有所说明。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/categorizing_text_559117_02.png

图 1-2. 监督文本分类的常见过程。我们通过特征提取将文本输入数据转换为数值表示。然后,训练分类器以预测标签。

模型选择

我们可以使用 LLM 来表示要输入到分类器中的文本。然而,这个模型的选择可能并不像您想象的那样简单。模型在可处理的语言、架构、大小、推理速度、架构、特定任务的准确性等方面存在差异,且还有许多其他差异。

BERT 是一个优秀的基础架构,可以针对多种任务进行微调,包括分类。尽管我们可以使用生成模型,比如知名的生成预训练变换器(GPT),例如 ChatGPT,但 BERT 模型通常在特定任务的微调上表现优越。相比之下,GPT 类模型通常在广泛的任务上表现突出。从某种意义上说,这是专业化与泛化的对比。

现在我们知道要为监督分类任务选择一个类似 BERT 的模型,我们将使用哪个呢?BERT 有多种变体,包括 BERT、RoBERTa、DistilBERT、ALBERT、DeBERTa,每种架构都以不同的形式进行了预训练,从特定领域的训练到多语言数据的训练。您可以在图 1-3 中找到一些著名大型语言模型的概述。

为工作选择合适的模型本身可以是一种艺术。尝试数千个可以在 HuggingFace Hub 上找到的预训练模型是不可行的,因此我们需要高效地选择模型。话虽如此,仍然有一些模型是很好的起点,并能让您了解这些模型的基础性能。将它们视为稳固的基线:

**https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/categorizing_text_559117_03.png

图 1-3. 常见大型语言模型发布的时间线。** 在本节中,我们将使用“bert-base-cased”进行一些示例。可以随意将“bert-base-cased”替换为上述任意模型。尝试不同的模型,以感受性能与训练速度之间的权衡。 **## 数据

在本章中,我们将演示许多文本分类技术。我们将用来训练和评估模型的数据集是“rotten_tomatoes”;pang2005seeing)数据集。它包含约 5000 条正面和 5000 条负面电影评论,来自Rotten Tomatoes

我们加载数据并将其转换为pandas dataframe以便于控制:

import pandas as pd
from datasets import load_dataset
tomatoes = load_dataset("rotten_tomatoes")

# Pandas for easier control
train_df = pd.DataFrame(tomatoes["train"])
eval_df = pd.DataFrame(tomatoes["test"])
提示

尽管本书专注于 LLMs,但强烈建议将这些示例与经典且强大的基准进行比较,例如使用 TF-IDF 表示文本并在其上训练 LogisticRegression 分类器。

分类头

使用 Rotten Tomatoes 数据集,我们可以从最简单的预测任务开始,即二分类。这通常应用于情感分析,检测某个文档是正面还是负面。这可以是带有指示该评论是正面还是负面的标签(0 或 1)的客户评论。在我们的案例中,我们将预测一条电影评论是负面(0)还是正面(1)。

使用基于变换器的模型训练分类器通常遵循两步法:

首先,如图 1-4 所示,我们采用现有的变换器模型,将文本数据转换为数值表示。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/categorizing_text_559117_04.png

图 1-4. 首先,我们使用通用的预训练 LLM(例如 BERT)将我们的文本数据转换为更数值化的表示。在训练过程中,我们将“冻结”模型,以便其权重不会被更新。这显著加快了训练速度,但通常精度较低。

其次,如图 1-5 所示,我们在预训练模型的顶部添加一个分类头。这个分类头通常是一个单一的线性层,我们可以对其进行微调。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/categorizing_text_559117_05.png

图 1-5. 在微调我们的 LLM 后,我们在数值表示和标签上训练分类器。通常,选择前馈神经网络作为分类器。

这两个步骤描述的是同一模型,因为分类头直接添加到 BERT 模型中。如图 1-6 所示,我们的分类器只不过是一个附加了线性层的预训练 LLM。它实现了特征提取和分类的结合。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/categorizing_text_559117_06.png

图 1-6. 我们采用 BERT 模型,其输出嵌入被输入到分类头中。该头通常由一个线性层组成,但可能会提前包含 dropout。
注意

在第十章中,我们将使用图 2-4 和 2-5 中显示的相同管道,但将微调大型语言模型。在那里,我们将更深入地探讨微调的工作原理以及为什么它能改善这里展示的管道。目前,重要的是要知道,微调这个模型和分类头一起提高了分类任务的准确性。这是因为它使大型语言模型能够更好地表示文本以进行分类,针对特定领域文本进行微调。

示例

为了训练我们的模型,我们将使用简单变换器包。它抽象了大部分技术难点,让我们可以专注于当前的分类任务。我们从初始化模型开始:

from simpletransformers.classification import ClassificationModel, ClassificationArgs

# Train only the classifier layers
model_args = ClassificationArgs()
model_args.train_custom_parameters_only = True
model_args.custom_parameter_groups = [
    {
        "params": ["classifier.weight"],
        "lr": 1e-3,
    },
    {
        "params": ["classifier.bias"],
        "lr": 1e-3,
        "weight_decay": 0.0,
    },
]

# Initializing pre-trained BERT model
model = ClassificationModel("bert", "bert-base-cased", args=model_args)

我们选择了流行的“bert-base-cased”,但如前所述,我们还有许多其他模型可以选择。可以随意尝试不同模型,以查看其对性能的影响。

接下来,我们可以在训练数据集上训练模型,并预测评估数据集的标签:

import numpy as np
from sklearn.metrics import f1_score

# Train the model
model.train_model(train_df)

# Predict unseen instances
result, model_outputs, wrong_predictions = model.eval_model(eval_df, f1=f1_score)
y_pred = np.argmax(model_outputs, axis=1)

现在我们已经训练了模型,剩下的就是评估:

>>> from sklearn.metrics import classification_report
>>> print(classification_report(eval_df.label, y_pred))
              precision    recall  f1-score   support

           0       0.84      0.86      0.85       533
           1       0.86      0.83      0.84       533

    accuracy                           0.85      1066
   macro avg       0.85      0.85      0.85      1066
weighted avg       0.85      0.85      0.85      1066

使用预训练的 BERT 模型进行分类使我们的 F-1 得分达到 0.85。我们可以将这个得分作为本节示例中的基准。

提示

simpletransformers包提供了许多易于使用的功能来处理不同任务。例如,你也可以用它创建一个自定义的命名实体识别模型,只需几行代码。

预训练嵌入

与之前展示的示例不同,我们可以以更经典的形式进行监督分类。我们可以完全将特征提取与分类训练分开,而不是在训练前冻结层并在其上使用前馈神经网络。

这种两步法完全将特征提取与分类分开:

首先,正如我们在图 1-7 中看到的,我们使用一个专门训练以创建嵌入的 LLM,SBERT(www.sbert.net/)。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/categorizing_text_559117_07.png

图 1-7. 首先,我们使用一个专门训练用于生成准确数值表示的 LLM。这些通常比我们从像 BERT 这样的一般 Transformer 模型中获得的更具代表性。

其次,如图 1-8 所示,我们使用嵌入作为逻辑回归模型的输入。我们完全将特征提取模型与分类模型分开。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/categorizing_text_559117_08.png

图 1-8. 使用嵌入作为特征,我们在训练数据上训练一个逻辑回归模型。

与我们之前的示例相比,这两个步骤分别描述了不同的模型。SBERT 用于生成特征,即嵌入,而逻辑回归则作为分类器。如图 2-9 所示,我们的分类器仅仅是一个附加了线性层的预训练 LLM。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/categorizing_text_559117_09.png

图 1-9. 分类器是一个单独的模型,它利用来自 SBERT 的嵌入进行学习。** **### 示例

使用句子转换器,我们可以在训练分类模型之前创建我们的特征:

from sentence_transformers import SentenceTransformer, util
model = SentenceTransformer('all-mpnet-base-v2')
train_embeddings = model.encode(train_df.text)
eval_embeddings = model.encode(eval_df.text)

我们为训练(train_df)和评估(eval_df)数据创建了嵌入。生成的每个嵌入实例由 768 个值表示。我们将这些值视为可以用于训练模型的特征。

选择模型可以很简单。我们可以回归基础,使用逻辑回归,而不是使用前馈神经网络:

from sklearn.linear_model import LogisticRegression
clf = LogisticRegression(random_state=42).fit(train_embeddings, train_df.label)

在实践中,你可以在我们生成的嵌入上使用任何分类器,例如决策树或神经网络。

接下来,让我们评估我们的模型:

>>> from sklearn.metrics import classification_report
>>> y_pred = clf.predict(eval_embeddings)
>>> print(classification_report(eval_df.label, y_pred))

              precision    recall  f1-score   support

           0       0.84      0.86      0.85       151
           1       0.86      0.83      0.84       149

    accuracy                           0.85       300
   macro avg       0.85      0.85      0.85       300
weighted avg       0.85      0.85      0.85       300

在不需要微调我们的 LLM 的情况下,我们成功地达到了 0.85 的 F1 得分。这一点尤其令人印象深刻,因为它相比于我们的前一个示例,模型要小得多。**** ****# 零-shot 分类

本章开始时的例子中,我们所有的训练数据都有标签。然而,在实践中,这可能并不总是如此。获取标记数据是一项资源密集型任务,可能需要大量人力。相反,我们可以使用零样本分类模型。这种方法是迁移学习的一个良好例子,训练用于一项任务的模型被用于与其最初训练的任务不同的任务。零样本分类的概述在图 2-11 中给出。请注意,这个流程还展示了如果多个标签的概率超过给定阈值,则执行多标签分类的能力。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/categorizing_text_559117_10.png

图 1-10. 图 2-11. 在零样本分类中,LLM 并未在任何候选标签上进行训练。它从不同的标签中学习,并将这些信息推广到候选标签上。

通常,零样本分类任务与使用自然语言描述我们希望模型执行的操作的预训练 LLM 一起使用。随着模型规模的增加,这通常被称为 LLM 的涌现特性(wei2022emergent)。正如我们将在本章后面关于生成模型分类时看到的,类似 GPT 的模型通常能够很好地完成这些任务。

预训练嵌入

正如我们在监督分类示例中所看到的,嵌入是一种出色且常常准确地表示文本数据的方法。在处理没有标记的文档时,我们需要在如何使用预训练嵌入方面稍微富有创造性。由于没有可用的标记数据,分类器无法进行训练。

幸运的是,我们可以使用一个技巧。我们可以根据标签应表示的内容来描述它们。例如,电影评论的负面标签可以描述为“这是一条负面的电影评论”。通过描述和嵌入标签和文档,我们有了可以使用的数据。这个过程如图 1-11 所示,使我们能够生成自己的目标标签,而无需实际拥有任何标记数据。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/categorizing_text_559117_11.png

图 1-11. 要嵌入标签,我们首先需要给它们一个描述。例如,负面标签的描述可以是“负面的电影评论”。这个描述可以通过句子变换器嵌入。最后,标签和所有文档都会被嵌入。

要为文档分配标签,我们可以对文档标签对应用余弦相似度。余弦相似度是检查两个向量彼此相似程度的相似性度量,整个书中会经常使用。

这是向量之间角度的余弦,通过嵌入的点积计算,并除以它们长度的乘积。听起来确实比实际复杂,希望图 1-12 中的插图能提供额外的直觉。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/categorizing_text_559117_12.png

图 1-12. 余弦相似度是两个向量或嵌入之间的角度。在这个例子中,我们计算文档与两个可能标签(正面和负面)之间的相似度。

对于每个文档,它的嵌入与每个标签的嵌入进行比较。选择与文档相似度最高的标签。图 1-13 很好地展示了文档如何被分配标签。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/categorizing_text_559117_13.png

图 1-13. 在嵌入标签描述和文档后,我们可以对每个标签文档对使用余弦相似度。对于每个文档,选择与该文档相似度最高的标签。

示例

我们首先为评估数据集生成嵌入。这些嵌入是使用句子转换器生成的,因为它们相当准确且计算速度较快。

from sentence_transformers import SentenceTransformer, util

# Create embeddings for the input documents
model = SentenceTransformer('all-mpnet-base-v2')
eval_embeddings = model.encode(eval_df.text)

接下来,需要生成标签的嵌入。然而,这些标签没有可以利用的文本表示,因此我们需要自己命名这些标签。

由于我们要处理正面和负面电影评论,我们将标签命名为“正面评论”和“负面评论”。这使我们能够嵌入这些标签:

# Create embeddings for our labels
label_embeddings = model.encode(["A negative review", "A positive review"])

现在我们有了评论和标签的嵌入,我们可以在它们之间应用余弦相似度,以查看哪个标签最适合哪个评论。这样只需要几行代码:

import numpy as np
from sklearn.metrics.pairwise import cosine_similarity

# Find the best matching label for each document
sim_matrix = cosine_similarity(eval_embeddings, label_embeddings)
y_pred = np.argmax(sim_matrix, axis=1)

就这样!我们只需为我们的标签想出名称,就可以执行分类任务。让我们看看这种方法效果如何:

>>> print(classification_report(eval_df.label, y_pred))

              precision    recall  f1-score   support

           0       0.83      0.77      0.80       151
           1       0.79      0.84      0.81       149

    accuracy                           0.81       300
   macro avg       0.81      0.81      0.81       300
weighted avg       0.81      0.81      0.81       300

考虑到我们根本没有使用任何标记数据,0.81 的 F-1 分数相当令人印象深刻!这显示了嵌入的多功能性和实用性,尤其是当你在使用方式上稍微有点创意时。

让我们来测试一下这个创造力。我们决定将“负面/积极评论”作为我们的标签名称,但可以进一步改进。相反,我们可以通过使用“非常负面/积极的电影评论”使其更具体,更贴合我们的数据。这样,嵌入将捕捉到这是一个电影评论,并将更加关注两个标签的极端情况。

我们使用之前的代码来查看这是否真的有效:

>>> # Create embeddings for our labels
>>> label_embeddings = model.encode(["A very negative movie review", "A very positive movie review"])
>>> 
>>> # Find the best matching label for each document
>>> sim_matrix = cosine_similarity(eval_embeddings, label_embeddings)
>>> y_pred = np.argmax(sim_matrix, axis=1)
>>> 
>>> # Report results
>>> print(classification_report(eval_df.label, y_pred))

              precision    recall  f1-score   support

           0       0.90      0.74      0.81       151
           1       0.78      0.91      0.84       149

    accuracy                           0.83       300
   macro avg       0.84      0.83      0.83       300
weighted avg       0.84      0.83      0.83       300

仅通过改变标签的措辞,我们大大提高了我们的 F-1 分数!

提示

在这个例子中,我们通过命名标签并嵌入它们来应用零样本分类。当我们有少量标记的示例时,嵌入它们并将其添加到管道中可以帮助提高性能。例如,我们可以将标记示例的嵌入与标签嵌入进行平均。我们甚至可以通过创建不同类型的表示(标签嵌入、文档嵌入、平均嵌入等)进行投票程序,看看哪个标签最常被找到。这将使我们的零样本分类示例成为少样本方法。

自然语言推理

零样本分类也可以使用自然语言推理(NLI)进行,这指的是调查给定前提时,假设是否为真(蕴含)或为假(矛盾)的任务。图 1-14 展示了它们之间的良好示例。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/categorizing_text_559117_14.png

图 1-14. 自然语言推理(NLI)的示例。假设与前提相矛盾,彼此之间没有关联。

NLI 可以通过稍微创造性地使用前提/假设对进行零样本分类,如在图 1-15 中所示。我们使用输入文档,即我们想要提取情感的评论,并将其作为我们的前提(yin2019benchmarking)。然后,我们创建一个假设,询问前提是否与我们的目标标签有关。在我们的电影评论示例中,假设可以是:“这个例子是一个积极的电影评论”。当模型发现这是一个蕴含关系时,我们可以将评论标记为正面,而当其为矛盾时则标记为负面。使用 NLI 进行零样本分类的示例在图 1-15 中进行了说明。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/categorizing_text_559117_15.png

图 1-15. 自然语言推理(NLI)中的零样本分类示例。假设得到了前提的支持,模型将返回该评论确实是积极的电影评论。

示例

使用变压器,加载和运行预训练的 NLI 模型非常简单。我们选择“facebook``/bart-large-mnli”作为我们的预训练模型。该模型在超过 40 万个前提/假设对上进行了训练,应该非常适合我们的用例。

注意

在过去的几年中,Hugging Face 努力成为机器学习的 Github,托管与机器学习相关的几乎所有内容。因此,他们的中心提供了大量预训练模型。对于零样本分类任务,您可以查看此链接:huggingface.co/models?pipeline_tag=zero-shot-classification

我们加载变压器管道,并在评估数据集上运行它:

from transformers import pipeline

# Pre-trained MNLI model
pipe = pipeline(model="facebook/bart-large-mnli")

# Candidate labels
candidate_labels_dict = {"negative movie review": 0, "positive movie review": 1}
candidate_labels = ["negative movie review", "positive movie review"]

# Create predictions
predictions = pipe(eval_df.text.values.tolist(), candidate_labels=candidate_labels)

由于这是一个零样本分类任务,因此我们无需进行训练即可获得我们感兴趣的预测。预测变量不仅包含预测结果,还包含一个分数,指示候选标签(假设)蕴含输入文档(前提)的概率。

>>> from sklearn.metrics import classification_report
>>> y_pred = [candidate_labels_dict[prediction["labels"][0]] for prediction in predictions]
>>> print(classification_report(eval_df.label, y_pred))

              precision    recall  f1-score   support

           0       0.77      0.89      0.83       151
           1       0.87      0.74      0.80       149

    accuracy                           0.81       300
   macro avg       0.82      0.81      0.81       300
weighted avg       0.82      0.81      0.81       300

完全没有进行微调,它的 F1 分数达到了 0.81. 根据我们措辞候选标签的方式,可能能够提高这个值。例如,如果候选标签简单为“消极”和“积极”,会发生什么情况?

提示

另一个优秀的零样本分类预训练模型是 sentence-transformers 的交叉编码器,即 ‘cross-encoder/``nli``-deberta-base’。由于训练 sentence-transformers 模型侧重于句子对,因此它自然而然地适用于利用前提/假设对的零样本分类任务。

使用生成模型进行分类

使用生成性大型语言模型(如 OpenAI 的 GPT 模型)进行分类,与我们之前所做的稍有不同。我们不是对模型进行微调以适应我们的数据,而是使用模型并尝试引导它朝向我们所寻找的答案类型。

这个引导过程主要通过您提供的提示来完成,例如模型。优化提示以使模型理解您所寻找的答案类型被称为 提示工程。本节将演示如何利用生成模型执行各种分类任务。

对于极大型语言模型,如 GPT-3,这一点尤其真实。一篇优秀的论文和相关阅读,“语言模型是少样本学习者”,描述了这些模型在下游任务上具有竞争力,同时需要更少的特定任务数据 (brown2020language)。

上下文学习

生成模型如此有趣的原因在于它们能够遵循给定的提示。生成模型甚至可以通过仅仅展示几个新任务的示例而做出完全新的事情。这一过程也称为上下文学习,指的是在不实际微调模型的情况下,让模型学习或做一些新的事情。

例如,如果我们要求生成模型写一首俳句(传统的日本诗歌形式),如果它之前没有见过俳句,可能无法做到。然而,如果提示中包含几条俳句的示例,那么模型就会“学习”并能够创作俳句。

我们故意将“学习”放在引号中,因为模型实际上并没有学习,而是遵循示例。在成功生成俳句后,我们仍需不断提供示例,因为内部模型并未更新。上下文学习的这些示例显示在图 1-16 中,展示了创作成功且高效提示所需的创造力。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/categorizing_text_559117_16.png

图 1-16. 通过提示工程与生成模型进行零-shot 和少-shot 分类。

上下文学习在少量示例的少-shot 分类任务中尤其有用,生成模型可以跟随这些少量示例。

不需要对内部模型进行微调是上下文学习的一个主要优势。这些生成模型通常体积庞大,难以在消费者硬件上运行,更不用说微调它们了。优化你的提示以引导生成模型相对容易,通常不需要精通生成 AI 的人。

示例

在我们进入上下文学习的示例之前,首先创建一个允许我们使用 OpenAI 的 GPT 模型进行预测的函数。

from tenacity import retry, stop_after_attempt, wait_random_exponential

@retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6))
def gpt_prediction(prompt, document, model="gpt-3.5-turbo-0301"):
  messages=[
    {"role": "system", "content": "You are a helpful assistant."},
    {"role": "user", "content":   prompt.replace("[DOCUMENT]", document)}
  ]
  response = openai.ChatCompletion.create(model=model, messages=messages, temperature=0)
  return response["choices"][0]["message"]["content"]

这个函数允许我们传递一个特定的promptdocument,用于我们想要创建预测的内容。你在这里看到的tenacity模块帮助我们处理速率限制错误,这种错误发生在你调用 API 过于频繁时。OpenAI 和其他外部 API 通常希望限制调用其 API 的速率,以免过载其服务器。

这个tenacity模块本质上是一个“重试模块”,允许我们以特定方式重试 API 调用。在这里,我们在gpt_prediction函数中实现了一种叫做指数退避的机制。当我们遇到速率限制错误时,指数退避会进行短暂休眠,然后重试未成功的请求。每当请求未成功时,休眠时间会增加,直到请求成功或达到最大重试次数。

避免速率限制错误的一种简单方法是自动重试请求并使用随机指数退避。当遇到速率限制错误时,重试时会进行短暂的休眠,然后重试失败的请求。如果请求仍然失败,休眠时间将增加,并重复此过程。直到请求成功或达到最大重试次数为止。

最后,我们需要使用从你的账户获取的 API 密钥登录 OpenAI 的 API:

import openai
openai.api_key = "sk-..."
警告

在使用外部 API 时,始终跟踪你的使用情况。如果你频繁请求外部 API,如 OpenAI 或 Cohere,费用会迅速增加。

零样本分类

使用生成模型进行零样本分类本质上是我们与这些模型交互时通常所做的,简单地询问它们是否能执行某项任务。在我们的示例中,我们询问模型特定文档是否为积极或消极的电影评论。

为此,我们创建一个零样本分类提示的基础模板,并询问模型是否能预测评论是积极还是消极:

# Define a zero-shot prompt as a base
zeroshot_prompt = """Predict whether the following document is a positive or negative movie review:

[DOCUMENT]

If it is positive say 1 and if it is negative say 0\. Do not give any other answers.
"""

你可能注意到我们明确要求不要提供其他答案。这些生成模型往往有自己的想法,会返回大量关于某事为何是或不是消极的解释。由于我们在评估其结果,我们希望返回的是 0 或 1。

接下来,让我们看看它是否能正确预测评论“谦逊、迷人、快速、原创”是积极的:

# Define a zero-shot prompt as a base
zeroshot_prompt = """Predict whether the following document is a positive or negative movie review:

[DOCUMENT]

If it is positive say 1 and if it is negative say 0\. Do not give any other answers.
"""

# Predict the target using GPT
document = "unpretentious , charming , quirky , original"
gpt_prediction(zeroshot_prompt, document)

输出确实显示该评论被 OpenAI 的模型标记为积极!使用此提示模板,我们可以在“[DOCUMENT]”标签中插入任何文档。这些模型有令牌限制,这意味着我们可能无法将整本书插入提示中。幸运的是,评论通常不会像书那样长,而是相对较短。

接下来,我们可以对评估数据集中的所有评论进行此操作,并观察其性能。不过请注意,这需要向 OpenAI 的 API 发送 300 个请求:

> from sklearn.metrics import classification_report
> from tqdm import tqdm
>
> y_pred = [int(gpt_prediction(zeroshot_prompt, doc)) for doc in tqdm(eval_df.text)]
> print(classification_report(eval_df.label, y_pred))

              precision    recall  f1-score   support

           0       0.86      0.96      0.91       151
           1       0.95      0.86      0.91       149

    accuracy                           0.91       300
   macro avg       0.91      0.91      0.91       300
weighted avg       0.91      0.91      0.91       300

F-1 分数为 0.91!这是我们迄今为止看到的最高分数,考虑到我们完全没有对模型进行微调,这实在令人印象深刻。

注意

尽管这种基于 GPT 的零样本分类表现出色,但需要注意的是,微调通常优于本节中所述的上下文学习。特别是在涉及特定领域数据时,模型在预训练期间不太可能见过这些数据。当模型的参数未针对当前任务进行更新时,其对任务特定细微差别的适应性可能有限。理想情况下,我们希望在这些数据上对 GPT 模型进行微调,以进一步提升其性能!

少样本分类

在上下文学习中,少样本分类效果尤其好。与零样本分类相比,我们只需添加一些电影评论示例,以引导生成模型。这样,它对我们想要完成的任务有了更好的理解。

我们首先更新我们的提示模板,以包含几个精心挑选的示例:

# Define a few-shot prompt as a base
fewshot_prompt = """Predict whether the following document is a positive or negative moview review:

[DOCUMENT]

Examples of negative reviews are:
- a film really has to be exceptional to justify a three hour running time , and this isn't .
- the film , like jimmy's routines , could use a few good laughs .

Examples of positive reviews are:
- very predictable but still entertaining
- a solid examination of the male midlife crisis .

If it is positive say 1 and if it is negative say 0\. Do not give any other answers.
"""

我们为每个类别选择了两个示例,以快速引导模型为电影评论分配情感。

注意

由于我们在提示中添加了一些示例,生成模型消耗了更多的标记,因此可能会增加请求 API 的成本。然而,相较于微调和更新整个模型,这相对较少。

预测与之前相同,但将零样本提示替换为少样本提示:

# Predict the target using GPT
document = "unpretentious , charming , quirky , original"
gpt_prediction(fewshot_prompt, document)

不出所料,它正确地为评论分配了情感。任务越困难或复杂,提供示例的效果就越显著,尤其是当示例质量较高时。

和以前一样,让我们对整个评估数据集运行改进的提示:

>>> predictions = [gpt_prediction(fewshot_prompt, doc) for doc in tqdm(eval_df.text)]

              precision    recall  f1-score   support

           0       0.88      0.97      0.92       151
           1       0.96      0.87      0.92       149

    accuracy                           0.92       300
   macro avg       0.92      0.92      0.92       300
weighted avg       0.92      0.92      0.92       300

现在的 F1 分数为 0.92,与之前相比略有提高。这并不意外,因为之前的分数已经相当高,而手头的任务也不是特别复杂。

注意

我们可以通过设计提示将上下文学习的示例扩展到多标签分类。例如,我们可以要求模型选择一个或多个标签,并将它们用逗号分隔返回。

命名实体识别

在之前的示例中,我们尝试对整个文本(如评论)进行分类。然而,有许多情况下,我们更关注这些文本中的具体信息。我们可能希望从文本电子健康记录中提取某些药物,或找出新闻帖子中提到的组织。

这些任务通常被称为标记分类或命名实体识别(NER),涉及在文本中检测这些实体。如图 1-17 所示,我们现在将对某些标记或标记集进行分类,而不是对整个文本进行分类。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/categorizing_text_559117_17.png

图 1-17. 一个识别“地点”和“时间”实体的命名实体识别示例。

当我们想到标记分类时,一个主要框架浮现在脑海中,即 SpaCy (spacy.io/)。它是执行许多工业级自然语言处理应用的绝佳工具,并且一直是命名实体识别(NER)任务的首选框架。所以,让我们来使用它吧!

示例

要在 SpaCy 中使用 OpenAI 的模型,我们首先需要将 API 密钥保存为环境变量。这使得 SpaCy 更容易访问,而无需在本地保存:

import os
os.environ['OPENAI_API_KEY'] = "sk-..."

接下来,我们需要配置我们的 SpaCy 管道。需要定义一个“任务”和一个“后端”。“任务”是我们希望 SpaCy 管道执行的内容,即命名实体识别。“后端”是用于执行该“任务”的基础 LLM,即 OpenAI 的 GPT-3.5-turbo 模型。在任务中,我们可以创建任何希望从文本中提取的标签。假设我们有关于患者的信息,我们希望提取一些个人信息,以及他们所患的疾病和症状。我们创建实体:日期、年龄、地点、疾病和症状:

import spacy

nlp = spacy.blank("en")

# Create a Named Entity Recognition Task and define labels
task = {"task": {
            "@llm_tasks": "spacy.NER.v1",
            "labels": "DATE,AGE,LOCATION, DISEASE, SYMPTOM"}}

# Choose which backend to use
backend = {"backend": {
            "@llm_backends": "spacy.REST.v1",
            "api": "OpenAI",
            "config": {"model": "gpt-3.5-turbo"}}}

# Combine configurations and create SpaCy pipeline
config = task | backend
nlp.add_pipe("llm", config=config)

接下来,我们只需要两行代码即可自动提取我们感兴趣的实体:

> doc = nlp("On February 11, 2020, a 73-year-old woman came to the hospital \n and was diagnosed with COVID-19 and has a cough.")
> print([(ent.text, ent.label_) for ent in doc.ents])

[('February 11', 'DATE'), ('2020', 'DATE'), ('73-year-old', 'AGE'), ('hospital', 'LOCATION'), ('COVID-19', ' DISEASE'), ('cough', ' SYMPTOM')]

它似乎能够正确提取实体,但很难立即看到一切是否顺利进行。幸运的是,SpaCy 有一个显示功能,可以让我们可视化文档中找到的实体(图 1-18):

from spacy import displacy
from IPython.core.display import display, HTML

# Display entities
html = displacy.render(doc, style="ent")
display(HTML(html))

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/categorizing_text_559117_18.png

图 1-18. 使用 OpenAI 的 GPT-3.5 模型的 SpaCy 输出。没有任何训练,它正确识别了我们的自定义实体。

这要好得多!图 2-X 显示我们可以清晰地看到模型正确识别了我们的自定义实体。在没有任何微调或模型训练的情况下,我们可以轻松检测到我们感兴趣的实体。

提示

从头开始使用 SpaCy 训练一个 NER 模型并不能仅仅通过几行代码实现,但这也绝对不是困难的事情!他们的文档和教程在我们看来是最先进的,并且在解释如何创建自定义模型方面做得非常出色。

总结

在本章中,我们看到了多种不同的技术来执行各种分类任务。从微调整个模型到完全不调优!对文本数据的分类并不像表面上看起来那么简单,而且有大量创造性的技术可以实现这一目标。

在下一章中,我们将继续讨论分类,但将重点放在无监督分类上。如果我们有没有任何标签的文本数据,我们该怎么办?我们可以提取什么信息?我们将重点关注对数据进行聚类,以及使用主题建模技术为聚类命名。

第二章: 语义搜索

搜索是第一个被广泛采用的“大语言模型”(LLM)应用之一。在开创性论文BERT:用于语言理解的深度双向变换器预训练发布几个月后,谷歌宣布它在使用此模型来增强谷歌搜索,并且它代表了“搜索历史上最大的飞跃之一”。微软必应也表示“从今年四月开始,我们使用大型变换器模型为我们的必应客户带来了过去一年最大的质量改进”。

这清楚地证明了这些模型的强大和实用性。它们的加入瞬间大幅提升了一些最成熟、维护良好的系统,这些系统是全球数十亿人依赖的。它们增加的能力称为语义搜索,使得根据意义进行搜索,而不仅仅是关键词匹配。

在本章中,我们将讨论使用语言模型增强搜索系统的三种主要方法。我们将介绍代码示例,您可以利用这些功能来增强自己的应用程序。请注意,这不仅对网页搜索有用,搜索还是大多数应用程序和产品的重要组成部分。因此,我们的重点不仅是构建一个网页搜索引擎,而是关注您自己的数据集。此功能为许多其他基于搜索的激动人心的 LLM 应用提供动力(例如,检索增强生成或文档问答)。让我们开始看看这三种使用 LLM 进行语义搜索的方法。

基于语言模型的搜索系统的三大类。

关于如何最好地使用 LLM 进行搜索的研究很多。这些模型的三大类是:

1- 密集检索

假设用户在搜索引擎中输入搜索查询。密集检索系统依赖于嵌入的概念,这是我们在前面的章节中遇到的相同概念,并将搜索问题转化为检索搜索查询的最近邻(在查询和文档都转换为嵌入后)。图 2-1 展示了密集检索如何获取搜索查询,查阅其文本档案,并输出一组相关结果。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/semantic_search_888356_01.png

图 2-1. 密集检索是语义搜索的关键类型之一,依赖文本嵌入的相似性来检索相关结果。

2- 重新排序

这些系统是多个步骤的管道。重排序 LLM 是这些步骤之一,负责对结果子集相对于查询的相关性进行评分,然后根据这些评分更改结果的顺序。图 2-2 显示了重排序器如何不同于密集检索,因为它们需要额外的输入:来自搜索管道前一步的搜索结果集。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/semantic_search_888356_02.png

图 2-2. 重排序器,第二种关键的语义搜索类型,接收搜索查询和结果集合,并根据相关性重新排序,通常会显著改善结果。

3- 生成搜索

不断增长的文本生成 LLM 能力导致了一批新的搜索系统,其中包括一个生成模型,它简单地对查询生成答案。图 2-3 显示了一个生成搜索的例子。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/semantic_search_888356_03.png

图 2-3. 生成搜索针对问题生成答案并引用其信息来源。

这三种概念都很强大,可以在同一流程中结合使用。本章其余部分将更详细地介绍这三种系统。虽然这些是主要类别,但它们并不是搜索领域中唯一的 LLM 应用。

密集检索

回想一下,嵌入将文本转换为数字表示。这些可以被视为空间中的点,如我们在图 2-4 中所见。接近的点意味着它们所代表的文本是相似的。因此在这个例子中,文本 1 和文本 2 彼此相似(因为它们靠近),而与文本 3 不同(因为它更远)。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/semantic_search_888356_04.png

图 2-4. 嵌入的直观理解:每段文本都是一个点,含义相似的文本彼此接近。

这是用于构建搜索系统的属性。在这种情况下,当用户输入搜索查询时,我们将查询嵌入,从而将其投影到与我们的文本档案相同的空间中。然后,我们只需在该空间中找到与查询最接近的文档,这些文档就是搜索结果。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/semantic_search_888356_05.png

图 2-5. 密集检索依赖于搜索查询与相关结果之间的接近性。

根据图 2-5 中的距离,“文本 2”是这个查询的最佳结果,其次是“文本 1”。但是,这里可能会出现两个问题:

文本 3 是否应该被返回作为结果?这是你作为系统设计者的决定。有时需要设置一个最大相似度分数的阈值,以过滤掉不相关的结果(以防语料库中没有与查询相关的结果)。

查询及其最佳结果在语义上相似吗?不一定。这就是为什么语言模型需要在问答对上进行训练,以便在检索方面变得更好的原因。这个过程在第十三章中有更详细的说明。

密集检索示例

让我们通过使用 Cohere 搜索维基百科关于电影星际穿越的页面来看一个密集检索示例。在这个示例中,我们将执行以下操作:

  1. 获取我们想要使其可搜索的文本,对其进行一些轻处理以将其拆分成句子。

  2. 嵌入句子

  3. 构建搜索索引

  4. 搜索并查看结果

首先,我们需要安装示例所需的库:

# Install Cohere for embeddings, Annoy for approximate nearest neighbor search
!pip install cohere tqdm Annoy

通过在 https://cohere.ai/注册获取你的 Cohere API 密钥。将其粘贴到下面的单元格中。你在运行这个示例时无需支付任何费用。

让我们导入所需的数据集:

import cohere
import numpy as np
import re
import pandas as pd
from tqdm import tqdm
from sklearn.metrics.pairwise import cosine_similarity
from annoy import AnnoyIndex

# Paste your API key here. Remember to not share publicly
api_key = ''

# Create and retrieve a Cohere API key from os.cohere.ai
co = cohere.Client(api_key)

  1. 获取文本档案

    让我们使用维基百科关于电影星际穿越的第一部分。https://en.wikipedia.org/wiki/Interstellar_(film)。我们将获取文本,然后将其拆分成句子。

    text = """
    Interstellar is a 2014 epic science fiction film co-written, directed, and produced by Christopher Nolan. 
    It stars Matthew McConaughey, Anne Hathaway, Jessica Chastain, Bill Irwin, Ellen Burstyn, Matt Damon, and Michael Caine. 
    Set in a dystopian future where humanity is struggling to survive, the film follows a group of astronauts who travel through a wormhole near Saturn in search of a new home for mankind.
    
    Brothers Christopher and Jonathan Nolan wrote the screenplay, which had its origins in a script Jonathan developed in 2007\. 
    Caltech theoretical physicist and 2017 Nobel laureate in Physics[4] Kip Thorne was an executive producer, acted as a scientific consultant, and wrote a tie-in book, The Science of Interstellar. 
    Cinematographer Hoyte van Hoytema shot it on 35 mm movie film in the Panavision anamorphic format and IMAX 70 mm. 
    Principal photography began in late 2013 and took place in Alberta, Iceland, and Los Angeles. 
    Interstellar uses extensive practical and miniature effects and the company Double Negative created additional digital effects.
    
    Interstellar premiered on October 26, 2014, in Los Angeles. 
    In the United States, it was first released on film stock, expanding to venues using digital projectors. 
    The film had a worldwide gross over $677 million (and $773 million with subsequent re-releases), making it the tenth-highest grossing film of 2014\. 
    It received acclaim for its performances, direction, screenplay, musical score, visual effects, ambition, themes, and emotional weight. 
    It has also received praise from many astronomers for its scientific accuracy and portrayal of theoretical astrophysics. Since its premiere, Interstellar gained a cult following,[5] and now is regarded by many sci-fi experts as one of the best science-fiction films of all time.
    Interstellar was nominated for five awards at the 87th Academy Awards, winning Best Visual Effects, and received numerous other accolades"""
    # Split into a list of sentences
    texts = text.split('.')
    
    # Clean up to remove empty spaces and new lines
    texts = np.array([t.strip(' \n') for t in texts])
    
  2. 嵌入文本

    让我们现在嵌入文本。我们将把它们发送到 Cohere API,并为每个文本返回一个向量。

    # Get the embeddings
    response = co.embed(
      texts=texts,
    ).embeddings
    
    embeds = np.array(response)
    print(embeds.shape)
    

    输出如下:

    (15, 4096)

    表明我们有 15 个向量,每个向量的大小为 4096。

  3. 构建搜索索引

    在我们可以搜索之前,我们需要构建一个搜索索引。索引存储嵌入,并被优化为快速检索最近邻,即使我们有非常大量的点。

    # Create the search index, pass the size of embedding
    search_index = AnnoyIndex(embeds.shape[1], 'angular')
    
    # Add all the vectors to the search index
    for index, embed in enumerate(embeds):
        search_index.add_item(index, embed)
    
    search_index.build(10) 
    search_index.save('test.ann')
    
  4. 搜索索引

    现在我们可以使用任何我们想要的查询来搜索数据集。我们只需嵌入查询,并将其嵌入呈现给索引,索引将检索出最相似的文本。

    让我们定义我们的搜索函数:

    def search(query):
    
      # 1\. Get the query's embedding
      query_embed = co.embed(texts=[query]).embeddings[0]
    
      # 2\. Retrieve the nearest neighbors
      similar_item_ids = search_index.get_nns_by_vector(query_embed, n=3,
                                                      include_distances=True)
      # 3\. Format the results
      results = pd.DataFrame(data={'texts': texts[similar_item_ids[0]], 
                                  'distance': similar_item_ids[1]})
    
      # 4\. Print and return the results
      print(f"Query:'{query}'\nNearest neighbors:")
      return results
    

    我们现在准备好写查询并搜索文本了!

    query = "How much did the film make?"
    search(query)
    

    这产生的输出为:

    Query:'How much did the film make?'
    Nearest neighbors:
    
    texts
    

    |

    distance
    

    |

    |

    0
    

    |

    The film had a worldwide gross over $677 million (and $773 million with subsequent re-releases), making it the tenth-highest grossing film of 2014
    

    |

    0.815905
    

    |

    |

    1
    

    |

    It stars Matthew McConaughey, Anne Hathaway, Jessica Chastain, Bill Irwin, Ellen Burstyn, Matt Damon, and Michael Caine
    

    |

    1.066861
    

    |

    |

    2
    

    |

    In the United States, it was first released on film stock, expanding to venues using digital projectors
    

    |

    1.086919
    

    |

第一个结果的距离最小,因此与查询最相似。查看它,它完美地回答了问题。请注意,如果我们仅进行关键字搜索,这是不可能的,因为最佳结果中不包含“much”或“make”这两个词。

为了进一步说明密集检索的能力,这里有一个查询列表及每个查询的最佳结果:

查询:“告诉我关于$$$的事情?”

最佳结果:这部电影在全球的总票房超过 6.77 亿美元(与后来的重映一起为 7.73 亿美元),成为 2014 年票房第十高的电影。

距离:1.244138

查询:“哪些演员参与了?”

顶部结果:它由马修·麦康纳、安妮·海瑟薇、杰西卡·查斯坦、比尔·欧文、艾伦·伯斯廷、马特·达蒙和迈克尔·凯恩主演。

距离:0.917728

查询:“这部电影是如何上映的?”

顶部结果:在美国,它最初是在胶卷上发布的,扩展到使用数字放映机的场所。

距离:0.871881

密集检索的注意事项

了解密集检索的一些缺点及其解决方法是有用的。例如,如果文本中不包含答案,会发生什么?我们仍然会得到结果及其距离。例如:

Query:'What is the mass of the moon?'
Nearest neighbors:
texts

|

distance

|

|

0

|

The film had a worldwide gross over $677 million (and $773 million with subsequent re-releases), making it the tenth-highest grossing film of 2014

|

1.298275

|

|

1

|

It has also received praise from many astronomers for its scientific accuracy and portrayal of theoretical astrophysics

|

1.324389

|

|

2

|

Cinematographer Hoyte van Hoytema shot it on 35 mm movie film in the Panavision anamorphic format and IMAX 70 mm

|

1.328375

|

在这种情况下,一个可能的启发式方法是设定一个阈值水平——例如,相关性的最大距离。许多搜索系统向用户提供他们能获取的最佳信息,并由用户决定其相关性。跟踪用户是否点击了结果(并对此感到满意)的信息,可以改善未来版本的搜索系统。

密集检索的另一个注意事项是用户希望找到与其正在寻找的文本完全匹配的情况。这种情况非常适合关键词匹配。这也是为什么同时包括语义搜索和关键词搜索的混合搜索被使用的原因之一。

密集检索系统在训练以外的领域中正常工作也面临挑战。例如,如果你在互联网和维基百科数据上训练检索模型,然后在法律文本上部署(而训练集中没有足够的法律数据),模型在法律领域的表现将不佳。

我们想指出的最后一点是,这是一个每个句子包含一条信息的情况,我们展示了具体询问这些信息的查询。那么,对于答案跨越多个句子的提问呢?这显示了密集检索系统的一个重要设计参数:分块长文本的最佳方法是什么?我们为什么要首先进行分块?

长文本分块

Transformer 语言模型的一个限制是它们的上下文大小有限。这意味着我们不能输入超过模型支持的某个字数或标记数量的非常长的文本。那么我们如何嵌入长文本呢?

有几种可能的方法,图 2-6 中展示的两种可能方法包括每个文档索引一个向量,以及每个文档索引多个向量。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/semantic_search_888356_06.png

图 2-6. 可以创建一个向量来表示整个文档,但对于较长的文档,将其分割成较小的块以获取各自的嵌入更好。

每个文档一个向量

在这种方法中,我们使用单个向量来表示整个文档。这里的可能性包括:

  • 仅嵌入文档的代表部分而忽略其余文本。这可能意味着仅嵌入标题或文档的开头。这对于快速开始构建演示非常有用,但会留下大量未索引的信息,因此不可搜索。作为一种方法,它可能更适合于那些开头捕获文档主要观点的文档(例如:维基百科文章)。但这并不是一个真正系统的最佳方法。

  • 将文档分块、嵌入这些块,然后将这些块聚合为单个向量。这里常用的聚合方法是对这些向量取平均。该方法的一个缺点是会产生一个高度压缩的向量,导致文档中大量信息丢失。

这种方法可以满足某些信息需求,但不能满足其他需求。很多时候,搜索的是包含在文章中的特定信息,如果该概念有自己的向量,捕获效果会更好。

每个文档多个向量

在这种方法中,我们将文档分块为更小的部分,并嵌入这些块。我们的搜索索引因此变为块嵌入,而不是整个文档的嵌入。

分块方法更好,因为它全面覆盖了文本,并且向量倾向于捕获文本中的单个概念。这导致了更具表现力的搜索索引。图 X-3 展示了一些可能的方法。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/semantic_search_888356_07.png

图 2-7. 多种用于嵌入文档的分块选项。

对长文本的最佳分块方式将取决于系统预期的文本类型和查询。方法包括:

  • 每个句子是一个块。这里的问题是这可能过于细化,向量无法捕获足够的上下文。

  • 每个段落是一个块。如果文本由短段落组成,这很棒。否则,可能每 4-8 句话是一个块。

  • 一些块的意义来自于周围的文本。因此,我们可以通过以下方式结合一些上下文:

    • 将文档的标题添加到块中。

    • 在块中添加一些前后的文本。这样,块可以重叠,从而包括一些周围文本。这就是我们在图 2-8 中看到的。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/semantic_search_888356_08.png

图 2-8. 将文本分块为重叠片段是一种保留不同片段周围更多上下文的策略。

随着该领域的发展,预计会出现更多的分块策略——其中一些甚至可能使用 LLM 动态地将文本分割成有意义的块。

最近邻搜索与向量数据库

找到最近邻的最简单方法是计算查询与档案之间的距离。这可以很容易地用 NumPy 实现,如果你的档案中有成千上万或几万个向量,这也是一种合理的方法。

当你扩展到数百万个向量时,优化检索的方式是依赖于近似最近邻搜索库,如 Annoy 或 FAISS。这些库允许你在毫秒内从巨大的索引中检索结果,有些可以扩展到 GPU 和机器集群,以服务非常大的索引。

另一类向量检索系统是像 Weaviate 或 Pinecone 这样的向量数据库。向量数据库允许你添加或删除向量,而无需重建索引。它们还提供了超越单纯向量距离的搜索过滤或自定义的方法。

为密集检索微调嵌入模型

就像我们在文本分类章节中看到的那样,我们可以通过微调提高大型语言模型在某项任务上的表现。和那种情况一样,检索需要优化文本嵌入,而不仅仅是令牌嵌入。这个微调过程的目标是获取由查询和相关结果组成的训练数据。

看一个来自我们数据集的例子,句子“《星际穿越》于 2014 年 10 月 26 日在洛杉矶首映。”。两个可能的相关查询是:

  • 相关查询 1:“《星际穿越》发布日期”

  • 相关查询 2:“《星际穿越》什么时候首映”

微调过程的目的是使这些查询的嵌入接近结果句子的嵌入。它还需要看到与句子不相关的查询的负示例。

  • 无关查询:“星际穿越演员表”

有了这些示例,我们现在有三对——两对正样本和一对负样本。假设,如我们在图 2-9 中看到的,微调之前,这三条查询与结果文档的距离相同。这并不牵强,因为它们都是在谈论《星际穿越》。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/semantic_search_888356_09.png

图 2-9。微调之前,相关和无关查询的嵌入可能接近某个特定文档。

微调步骤的目的是使相关查询更靠近文档,同时使无关查询远离文档。我们可以在图 2-10 中看到这一效果。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/semantic_search_888356_10.png

图 2-10. 经过微调过程后,文本嵌入模型通过结合我们使用相关和不相关文档的示例来定义数据集上的相关性,从而在这一搜索任务上变得更好。

重新排序

很多公司已经建立了搜索系统。对于这些公司,整合语言模型的更简单方法是作为其搜索管道中的最后一步。此步骤的任务是根据搜索查询的相关性改变搜索结果的顺序。这一步可以大大改善搜索结果,实际上这是微软必应为使用类似 BERT 模型改善搜索结果而添加的功能。

图 2-11 展示了作为两阶段搜索系统第二阶段的重新排序搜索系统的结构。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/semantic_search_888356_11.png

图 2-11. LLM 重新排序器作为搜索管道的一部分,旨在根据相关性重新排列一组筛选后的搜索结果

重新排序示例

重新排序模型接收搜索查询和多个搜索结果,并返回这些文档的最佳排序,使得与查询最相关的文档排名更高。

import cohere as co
API_KEY = ""
co = cohere.Client(API_KEY)
MODEL_NAME = "rerank-english-02" # another option is rerank-multilingual-02

query = "film gross"

Cohere 的Rerank端点是开始使用第一重新排序模型的简单方式。我们只需传入查询和文本,就能得到结果。我们无需对其进行训练或调整。

results = co.rerank(query=query, model=MODEL_NAME, documents=texts, top_n=3)

我们可以打印这些结果:

results = co.rerank(query=query, model=MODEL_NAME, documents=texts, top_n=3) # Change top_n to change the number of results returned. If top_n is not passed, all results will be returned.
for idx, r in enumerate(results):
  print(f"Document Rank: {idx + 1}, Document Index: {r.index}")
  print(f"Document: {r.document['text']}")
  print(f"Relevance Score: {r.relevance_score:.2f}")
  print("\n")

输出:

Document Rank: 1, Document Index: 10
Document: The film had a worldwide gross over $677 million (and $773 million with subsequent re-releases), making it the tenth-highest grossing film of 2014
Relevance Score: 0.92

Document Rank: 2, Document Index: 12
Document: It has also received praise from many astronomers for its scientific accuracy and portrayal of theoretical astrophysics
Relevance Score: 0.11

Document Rank: 3, Document Index: 2
Document: Set in a dystopian future where humanity is struggling to survive, the film follows a group of astronauts who travel through a wormhole near Saturn in search of a new home for mankind
Relevance Score: 0.03

这表明,重新排序模型对第一个结果的信心更高,为其分配了 0.92 的相关性评分,而其他结果的评分则明显较低。

然而,更常见的是,我们的索引会有数千或数百万个条目,我们需要筛选出,比如说一百或一千个结果,然后将这些结果呈现给重新排序模型。这个筛选过程被称为搜索管道的第一阶段

我们在上一节中看到的密集检索器示例是一个可能的第一阶段检索器。在实践中,第一阶段也可以是一个结合了关键词搜索和密集检索的搜索系统。

使用句子转换器的开源检索和重新排序

如果你想在自己的机器上本地设置检索和重新排序,那么你可以使用句子转换器库。请参考 https://www.sbert.net/ 中的文档进行设置。查看检索与重新排序部分以获取如何在库中进行这些步骤的说明和代码示例。

重新排序模型的工作原理

一种流行的构建 LLM 搜索重排序器的方法是将查询和每个结果同时呈现给作为交叉编码器工作的 LLM。这意味着查询和可能的结果同时呈现给模型,使其在分配相关性得分之前能够查看这两段文本的完整内容。该方法在一篇题为 多阶段文档排名与 BERT 的论文中有更详细的描述,有时被称为 monoBERT。

将搜索形式化为相关性评分基本上归结为分类问题。给定这些输入,模型输出一个从 0 到 1 的得分,其中 0 是不相关,1 是高度相关。这应该在查看分类章节时是熟悉的。

要了解更多关于使用 LLM 进行搜索的发展,可以参考 预训练变换器进行文本排名:BERT 及其后续,这是对这些模型直到 2021 年的发展进行的高度推荐的观察。

生成搜索

你可能注意到,密集检索和重排序都使用表示语言模型,而不是生成语言模型。这是因为它们在这些任务上比生成模型更优化。

然而,在某个规模上,生成 LLM 开始显得越来越能够进行有用的信息检索。人们开始向像 ChatGPT 这样的模型提问,有时得到了相关的答案。媒体开始将其描绘为对谷歌的威胁,这似乎引发了一场在搜索中使用语言模型的军备竞赛。微软 推出 了由生成模型驱动的 Bing AI。谷歌推出了 Bard,这是它在这个领域的回应。

什么是生成搜索?

生成搜索系统在搜索流程中包括文本生成步骤。然而,目前,生成 LLM 不是可靠的信息检索工具,容易生成连贯但通常不正确的文本来回应它们不知道答案的问题。

第一批生成搜索系统仅将搜索模型作为搜索流程末尾的总结步骤。我们可以在 图 2-12 中看到一个例子。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/semantic_search_888356_12.png

图 2-12. 生成搜索在搜索流程的末尾制定答案和摘要,同时引用其来源(由搜索系统之前的步骤返回)。

然而,在撰写本文时,语言模型在生成连贯文本方面表现出色,但在检索事实时并不可靠。它们尚未真正知道自己知道或不知道什么,往往用连贯的文本回答许多问题,但可能是错误的。这通常被称为幻觉。因此,由于搜索常常依赖于事实或引用现有文档,生成搜索模型被训练以引用其来源并在答案中包含链接。

生成搜索仍处于初期阶段,预计会随着时间的推移而改善。它源自一个叫做检索增强生成的机器学习研究领域。该领域的显著系统包括RAGRETROAtlas等。

搜索中其他 LLM 应用

除了这三类之外,还有很多其他方式可以使用 LLM 来推动或改善搜索系统。例子包括:

  • 生成合成数据以改进嵌入模型。这包括像GenQInPars-v2等方法,它们查看文档,生成关于这些文档的可能查询和问题,然后使用生成的数据微调检索系统。

  • 文本生成模型日益增长的推理能力使搜索系统能够通过将复杂问题和查询分解为多个子查询来逐步解决,最终得到原始问题的答案。该类别中的一种方法在Demonstrate-Search-Predict: 组合检索和语言模型以进行知识密集型 NLP中有所描述。

评估指标

语义搜索使用信息检索(IR)领域的指标进行评估。让我们讨论这两个流行指标:平均精确度(MAP)和标准化折扣累积增益(nDCG)。

评估搜索系统需要三个主要组件:文本档案、一组查询和相关性判断,指示哪些文档与每个查询相关。我们在图 3-13 中看到了这些组件。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/semantic_search_888356_13.png

图 2-13。要评估搜索系统,我们需要一个测试套件,包括查询和相关性判断,指示我们档案中的哪些文档与每个查询相关。

使用这个测试套件,我们可以开始探索评估搜索系统。让我们从一个简单的例子开始,假设我们将查询 1 传递给两个不同的搜索系统,并获得两个结果集。假设我们将结果数量限制为仅三个,如在图 2-14 中所示。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/semantic_search_888356_14.png

图 2-14。为了比较两个搜索系统,我们将测试套件中的同一查询传递给两个系统,并查看它们的顶部结果。

为了判断哪个系统更好,我们查看针对该查询的相关性判断。图 2-15 显示了哪些返回的结果是相关的。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/semantic_search_888356_15.png

图 2-15。从我们的测试套件查看相关性判断,我们可以看到系统 1 比系统 2 表现更好。

这向我们展示了一个清晰的案例,系统 1 优于系统 2。直观上,我们可能只计算每个系统检索到的相关结果数量。系统 A 正确获取了三个中的两个,而系统 2 仅正确获取了三个中的一个。

但是,对于像图 3-16 这样的情况,两个系统都只获得了三个中的一个相关结果,但它们的位置不同,该如何处理呢?

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/semantic_search_888356_16.png

图 2-16。我们需要一个评分系统,该系统奖励系统 1 为相关结果分配高位,即使两个系统在其前三个结果中仅检索到一个相关结果。

在这种情况下,我们可以直观地判断系统 1 比系统 2 表现更好,因为第一个位置(最重要的位置)的结果是正确的。但我们如何为该结果的优越性分配一个数字或评分呢?均值平均精度是一个能够量化这种区别的度量。

在这种情况下,分配数字评分的一个常见方法是平均精度,它评估系统 1 对该查询的结果为 0.6,而系统 2 的结果为 0.1。因此,让我们看看如何计算平均精度来评估一组结果,然后如何将其聚合以评估整个测试套件中的系统。

均值平均精度(MAP)

为了对系统 1 进行评分,我们需要首先计算多个分数。由于我们只关注三个结果,因此我们需要查看三个分数——与每个位置相关联的一个分数。

第一个很简单,只看第一个结果,我们计算精度分数:将正确结果的数量除以结果的总数(正确和不正确)。图 2-17 显示,在这种情况下,我们在第一个位置上有一个正确结果。因此,精度为 1/1 = 1。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/semantic_search_888356_17.png

图 2-17. 计算均值平均精度时,我们从计算每个位置的精度开始,从第 1 个位置开始。

我们需要继续计算其余位置的精度结果。第二个位置的计算考虑了第一个和第二个位置。这里的精度分数为 1(两个结果中有一个是正确的)除以 2(我们正在评估的两个结果)= 0.5。

图 2-18 继续计算第二和第三个位置的精度。接下来更进一步——在计算每个位置的精度后,我们将它们平均得到平均精度分数为 0.61。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/semantic_search_888356_18.png

图 2-18. 标题待补充

该计算显示了单个查询及其结果的平均精度。如果我们计算系统 1 在测试套件中所有查询的平均精度并得出它们的均值,我们可以得到均值平均精度分数,从而可以将系统 1 与测试套件中其他系统进行比较。

摘要

在本章中,我们探讨了使用语言模型来改善现有搜索系统的不同方法,甚至作为新型、更强大搜索系统的核心。这些包括:

  • 密集检索依赖于文本嵌入的相似性。这些系统将搜索查询嵌入并检索与查询嵌入最接近的文档。

  • 重新排序器(如 monoBERT),这些系统查看查询和候选结果,并对每个文档与该查询的相关性进行评分。这些相关性评分用于根据其与查询的相关性对入围结果进行排序,通常能产生改进的结果排名。

  • 生成式搜索,指的是在管道末端具有生成性 LLM 的搜索系统,基于检索到的文档来形成答案,同时引用其来源。

我们还探讨了一种可能的搜索系统评估方法。均值平均精度允许我们为搜索系统评分,以便在查询的测试套件及其已知相关性之间进行比较。

第三章: 文本聚类与主题建模

尽管监督技术,如分类,在过去几年中在行业中占据主导地位,但无监督技术(如文本聚类)的潜力不可低估。

文本聚类旨在根据其语义内容、意义和关系对相似文本进行分组,如图 3-1 所示。正如我们在第 XXX 章中使用文本嵌入之间的距离进行密集检索一样,聚类嵌入使我们能够根据相似性对档案中的文档进行分组。

语义相似文档的聚类结果不仅促进了对大量非结构化文本的高效分类,还允许快速的探索性数据分析。随着大型语言模型(LLMs)的出现,能够提供文本的上下文和语义表示,文本聚类的力量在过去几年中显著增强。语言不是一袋单词,大型语言模型已证明能够很好地捕捉这一概念。

文本聚类的一个被低估的方面是其创造性解决方案和实施的潜力。从某种意义上说,无监督意味着我们不受限于某个特定的任务或我们想要优化的事物。因此,文本聚类中有很大的自由度,使我们能够偏离常规路径。尽管文本聚类自然会用于文档的分组和分类,但它还可以用于算法性和视觉上发现不恰当的标签,进行主题建模,加速标记,以及许多其他有趣的用例。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_clustering_and_topic_modeling_664502_01.png

图 3-1. 聚类非结构化文本数据。

这种自由也带来了挑战。由于我们没有特定任务的指导,那么我们如何评估我们的无监督聚类输出?我们如何优化我们的算法?没有标签,我们在优化算法的目标是什么?我们什么时候知道我们的算法是正确的?算法“正确”意味着什么?尽管这些挑战可能相当复杂,但并非不可逾越,通常需要一些创造力和对用例的良好理解。

在文本聚类的自由与其带来的挑战之间取得平衡可能相当困难。如果我们进入主题建模的世界,这种平衡变得更加明显,因为主题建模开始采用“文本聚类”的思维方式。

通过主题建模,我们希望发现出现在大型文本数据集中的抽象主题。我们可以用多种方式描述主题,但它通常由一组关键字或短语描述。有关自然语言处理(NLP)的主题可以用“深度学习”、“变换器”和“自注意力”等术语来描述。传统上,我们期望关于特定主题的文档包含的术语出现频率高于其他术语。然而,这种期望忽略了文档可能包含的上下文信息。相反,我们可以利用大型语言模型结合文本聚类来建模上下文化的文本信息并提取语义相关的主题。图 3-2 展示了通过文本表示描述集群的想法。

*https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_clustering_and_topic_modeling_664502_02.png

图 3-2. 主题建模是一种赋予文本文件集群意义的方法。* 在本章中,我们将提供关于如何使用大型语言模型进行文本聚类的指南。然后,我们将转向一种受文本聚类启发的主题建模方法,即 BERTopic。

文本聚类

NLP 中探索性数据分析的一个主要组成部分是文本聚类。这种无监督技术旨在将相似的文本或文档分组,以便轻松发现大量文本数据中的模式。在深入分类任务之前,文本聚类可以帮助我们直观理解任务及其复杂性。

从文本聚类中发现的模式可以应用于多种商业用例。从识别重复的支持问题和发现新内容以推动 SEO 实践,到检测社交媒体中的主题趋势和发现重复内容,可能性多种多样,运用这样的技术,创造力成为关键要素。因此,文本聚类不仅仅是快速进行探索性数据分析的方法。

数据

在描述如何进行文本聚类之前,我们将首先介绍在本章中将使用的数据。为了保持本书的主题,我们将对机器学习和自然语言处理领域的各种 ArXiv 文章进行聚类。该数据集包含大约XXX篇文章,时间跨度为XXXXXX

我们首先使用HuggingFace 的数据集包导入我们的数据集,并提取稍后要使用的元数据,例如文章的摘要、年份和类别。

# Load data from huggingface
from datasets import load_dataset
dataset = load_dataset("maartengr/arxiv_nlp")["train"]

# Extract specific metadata
abstracts = dataset["Abstracts"]
years = dataset["Years"]
categories = dataset["Categories"]
titles = dataset["Titles"]

我们如何进行文本聚类?

现在我们有了数据,可以进行文本聚类。进行文本聚类时,可以采用多种技术,从基于图的神经网络到基于中心的聚类技术。在这一部分,我们将介绍一种著名的文本聚类流程,包括三个主要步骤:

  1. 嵌入文档

  2. 降维

  3. 聚类嵌入

1. 嵌入文档

聚类文本数据的第一步是将我们的文本数据转换为文本嵌入。回想前面的章节,嵌入是文本的数值表示,捕捉其含义。为语义相似性任务优化的嵌入对于聚类尤为重要。通过将每个文档映射到数值表示,使语义相似的文档彼此接近,聚类将变得更加强大。一组为这些任务优化的流行大型语言模型可以在著名的句子转换器框架中找到(reimers2019sentence)。图 3-3 展示了将文档转换为数值表示的第一步。

*https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_clustering_and_topic_modeling_664502_03.png

图 3-3. 第一步:我们将文档转换为数值表示,即嵌入。* *句子转换器具有清晰的 API,可以如下所示从文本片段生成嵌入:
from sentence_transformers import SentenceTransformer

# We load our model
embedding_model = SentenceTransformer('all-MiniLM-L6-v2')

# The abstracts are converted to vector representations
embeddings = model.encode(abstracts)

这些嵌入的大小因模型而异,但通常每个句子或段落至少包含 384 个值。嵌入所包含的值的数量称为嵌入的维度。* *### 2. 降维

在我们聚类从 ArXiv 摘要生成的嵌入之前,我们首先需要处理维度灾难。这个诅咒是在处理高维数据时出现的现象。随着维度的增加,每个维度内可能值的数量呈指数级增长。在每个维度内找到所有子空间变得越来越复杂。此外,随着维度的增加,点之间的距离概念变得越来越不精确。

因此,高维数据对于许多聚类技术来说可能是麻烦的,因为识别有意义的聚类变得更加困难。聚类变得更加分散和难以区分,使得准确识别和分离它们变得困难。

先前生成的嵌入具有较高的维度,通常会引发维度灾难。为了防止维度成为问题,我们聚类管道中的第二步是降维,如图 3-4 所示。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_clustering_and_topic_modeling_664502_04.png

图 3-4. 第 2 步:嵌入通过维度减少被降低到一个低维空间。

维度减少技术旨在通过寻找低维表示来保留高维数据的全局结构。著名的方法包括主成分分析(PCA)和均匀流形近似与投影(UMAP; mcinnes2018umap)。对于这个流程,我们选择 UMAP,因为它通常比 PCA 更好地处理非线性关系和结构。

注意

然而,维度减少技术并非完美无缺。它们无法完美地将高维数据捕捉到低维表示中。在这个过程中信息总会有所丢失。在减少维度和尽可能保留信息之间存在平衡。

为了进行维度减少,我们需要实例化我们的 UMAP 类并将生成的嵌入传递给它:

from umap import UMAP

# We instantiate our UMAP model
umap_model = UMAP(n_neighbors=15, n_components=5, min_dist=0.0, metric='cosine')

# We fit and transform our embeddings to reduce them
reduced_embeddings = umap_model.fit_transform(embeddings)

我们可以使用n_components参数来决定低维空间的形状。在这里,我们使用了n_components=5,因为我们希望尽可能保留信息而不陷入维度灾难。没有哪个值比另一个更好,因此请随意尝试!

3. 聚类嵌入

如图 3-5 所示,我们流程中的最后一步是对之前减少的嵌入进行聚类。许多算法能够很好地处理聚类任务,从基于质心的方法如 k-Means 到层次方法如凝聚聚类。选择取决于用户,并受到相应用例的高度影响。我们的数据可能包含一些噪声,因此更倾向于使用能检测异常值的聚类算法。如果我们的数据是每日产生的,我们可能希望寻找在线或增量的方法来建模是否创建了新的聚类。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_clustering_and_topic_modeling_664502_05.png

图 3-5. 第 3 步:我们使用减少维度的嵌入对文档进行聚类。

一个好的默认模型是基于密度的空间聚类算法(HDBSCAN; mcinnes2017hdbscan)。HDBSCAN 是名为 DBSCAN 的聚类算法的一种层次变体,允许找到密集(微)聚类,而无需明确指定聚类数量。作为一种基于密度的方法,它也可以检测数据中的异常值。数据点如果不属于任何聚类。这一点很重要,因为强行将数据归入聚类可能会产生噪声聚合。

与之前的包一样,使用 HDBSCAN 非常简单。我们只需实例化模型并将我们的减少嵌入传递给它:

from hdbscan import HDBSCAN

# We instantiate our HDBSCAN model
hdbscan_model = HDBSCAN(min_cluster_size=15, metric='euclidean', cluster_selection_method='eom')

# We fit our model and extract the cluster labels
hdbscan_model.fit(reduced_embeddings)
labels = hdbscan_model.labels_

然后,利用我们之前生成的 2D 嵌入,我们可以可视化 HDBSCAN 如何对数据进行聚类:

import seaborn as sns

# Reduce 384-dimensional embeddings to 2 dimensions for easier visualization
reduced_embeddings = UMAP(n_neighbors=15, n_components=2, 
min_dist=0.0, metric='cosine').fit_transform(embeddings)
df = pd.DataFrame(np.hstack([reduced_embeddings, clusters.reshape(-1, 1)]),
     columns=["x", "y", "cluster"]).sort_values("cluster")

# Visualize clusters
df.cluster = df.cluster.astype(int).astype(str)
sns.scatterplot(data=df, x='x', y='y', hue='cluster', 
   linewidth=0, legend=False, s=3, alpha=0.3)

如我们在图 3-6 中看到的,它能够很好地捕捉主要聚类。注意这些点的聚类被涂成相同的颜色,表明 HDBSCAN 将它们分为一组。由于我们有大量聚类,绘图库在聚类之间循环颜色,所以不要认为所有蓝色点都是一个聚类,例如。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_clustering_and_topic_modeling_664502_06.png

图 3-6. 生成的聚类(彩色)和离群点(灰色)作为 2D 可视化呈现。
注意

任何用于可视化目的的降维技术都会导致信息损失。这仅仅是对我们原始嵌入的近似。尽管这很有信息量,但它可能将聚类推得更近或更远于它们实际的位置。因此,人类评估,亲自检查聚类,是聚类分析的关键组成部分!

我们可以手动检查每个聚类,以查看哪些文档在语义上足够相似以被聚类在一起。例如,让我们从聚类XXX中随机取出几个文档:

>>> for index in np.where(labels==1)[0][:3]:
>>>    print(abstracts[index])
 Sarcasm is considered one of the most difficult problem in sentiment
analysis. In our ob-servation on Indonesian social media, for cer-tain topics,
people tend to criticize something using sarcasm. Here, we proposed two
additional features to detect sarcasm after a common sentiment analysis is
con...

  Automatic sarcasm detection is the task of predicting sarcasm in text. This
is a crucial step to sentiment analysis, considering prevalence and challenges
of sarcasm in sentiment-bearing text. Beginning with an approach that used
speech-based features, sarcasm detection has witnessed great interes...

  We introduce a deep neural network for automated sarcasm detection. Recent
work has emphasized the need for models to capitalize on contextual features,
beyond lexical and syntactic cues present in utterances. For example, different
speakers will tend to employ sarcasm regarding different subjects...

这些打印的文档告诉我们,该聚类可能包含关于XXX的文档。我们可以对每个创建的聚类执行此操作,但这可能会很繁琐,特别是如果我们想尝试调整超参数。相反,我们希望创建一种方法,能够自动从这些聚类中提取表示,而无需逐一检查所有文档。

这就是主题建模发挥作用的地方。它使我们能够对这些聚类进行建模,并赋予它们单一的意义。尽管有许多技术可供选择,我们选择了一种基于这种聚类理念的方法,因为它具有显著的灵活性。* *# 主题建模

传统上,主题建模是一种旨在在一组文本数据中寻找潜在主题或主题的技术。对于每个主题,会识别出一组最佳代表该主题含义的关键词或短语。这种技术非常适合在大语料库中寻找共同主题,因为它为相似内容集赋予意义。关于主题建模实践的图示概述可以在图 3-7 中找到。

潜在狄利克雷分配(LDA;blei2003latent)是一种经典且流行的主题建模方法,它假设每个主题由语料库词汇中的单词概率分布特征化。每个文档被视为主题的混合。例如,关于大型语言模型的文档可能高度概率地包含“BERT”、“自注意力”和“变换器”等词,而关于强化学习的文档可能高度概率地包含“PPO”、“奖励”和“rlhf”等词。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_clustering_and_topic_modeling_664502_07.png

图 3-7. 传统主题建模概述。

至今,这项技术仍然是许多主题建模应用中的基础,凭借其强大的理论背景和实际应用,它不太可能很快消失。然而,随着大型语言模型的似乎呈指数增长,我们开始想知道是否可以在主题建模领域利用这些大型语言模型。

已经有几种模型采用大型语言模型进行主题建模,例如嵌入式主题模型上下文化主题模型。然而,随着自然语言处理领域的快速发展,这些模型难以跟上。

解决此问题的方案是 BERTopic,这是一种利用高度灵活和模块化架构的主题建模技术。通过这种模块化,许多新发布的模型可以集成到其架构中。随着大型语言模型领域的发展,BERTopic 也在不断发展。这使得这些模型在主题建模中应用的方式变得有趣且意想不到。

BERTopic

BERTopic 是一种主题建模技术,它假设语义相似文档的集群是生成和描述集群的有效方式。每个集群中的文档预计描述一个主要主题,合在一起可能代表一个主题。

正如我们在文本聚类中看到的,集群中的文档集合可能代表一个共同主题,但主题本身尚未被描述。通过文本聚类,我们必须逐一查看集群中的每个文档,以了解该集群的内容。要使一个集群被称为主题,我们需要一种以简洁且易于理解的方式描述该集群的方法。

尽管有很多方法可以做到这一点,但 BERTopic 中有一个技巧,可以快速描述一个集群,从而将其定义为一个主题,同时生成一个高度模块化的管道。BERTopic 的基础算法大致包含两个主要步骤。

首先,正如我们在文本聚类示例中所做的那样,我们嵌入文档以创建数值表示,然后降低它们的维度,最后对降维后的嵌入进行聚类。结果是语义上相似文档的聚类。

图 3-8 描述了与之前相同的步骤,即使用句子变换器对文档进行嵌入,使用 UMAP 进行降维,以及使用 HDBSCAN 进行聚类。

*https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_clustering_and_topic_modeling_664502_08.png

图 3-8. BERTopic 流程的第一部分是对文本数据进行聚类。* *第二,我们为每个聚类找到最佳匹配的关键词或短语。通常,我们会取一个聚类的中心点,寻找可能最好地代表它的单词、短语或甚至句子。然而,这有一个缺点:我们必须持续跟踪我们的嵌入,如果我们有数百万个文档,存储和跟踪变得计算上困难。相反,BERTopic 使用经典的词袋方法来表示聚类。词袋正是名称所暗示的,对于每个文档,我们简单地计算某个单词出现的频率,并将其用作我们的文本表示。

然而,“the”、“and”和“I”等词在大多数英语文本中出现相当频繁,可能会被过度代表。为了给予这些词适当的权重,BERTopic 使用了一种称为 c-TF-IDF 的技术,意为基于类别的词频逆文档频率。c-TF-IDF 是经典 TF-IDF 过程的类别适应版本。与考虑文档内单词的重要性不同,c-TF-IDF 考虑的是文档聚类之间单词的重要性。

要使用 c-TF-IDF,我们首先将聚类中的每个文档连接成一个长文档。然后,我们提取类别c中术语f_x的频率,其中c指的是我们之前创建的聚类之一。现在我们可以知道每个聚类中包含多少个和哪些单词,仅仅是一个计数。

为了加权这个计数,我们取一个加一后的聚类中平均单词数A的对数,然后除以所有聚类中术语x的频率。加一是在对数内添加的,以确保得到正值,这在 TF-IDF 中也是常见的做法。

如图 3-9 所示,c-TF-IDF 计算使我们能够为每个聚类中的单词生成一个对应于该聚类的权重。因此,我们为每个主题生成一个主题-词矩阵,描述它们所包含的最重要的单词。它本质上是每个主题中语料库词汇的排名。

*https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_clustering_and_topic_modeling_664502_09.png

图 3-9. BERTopic 管道的第二部分是表示主题。计算术语x在类别c中的权重。*将这两个步骤结合起来,即聚类和表示主题,形成了 BERTopic 的完整管道,如图 3-10 所示。通过这个管道,我们可以对语义相似的文档进行聚类,并从这些聚类生成由多个关键词表示的主题。关键词对主题的权重越高,它就越能代表该主题。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_clustering_and_topic_modeling_664502_10.png

图 3-10. BERTopic 的完整管道大致由两个步骤组成:聚类和主题表示。
注意

有趣的是,c-TF-IDF 技巧不使用大型语言模型,因此不考虑单词的上下文和语义特性。然而,就像神经搜索一样,它提供了一个高效的起点,之后我们可以使用计算量较大的技术,例如类似 GPT 的模型。

该管道的一个主要优点是这两个步骤,聚类和主题表示,相对独立。当我们使用 c-TF-IDF 生成主题时,不使用聚类步骤的模型,例如,不需要跟踪每个文档的嵌入。因此,这为主题生成过程以及整个管道提供了显著的模块化。

注意

在聚类过程中,每个文档仅分配到一个单一的聚类或主题。在实践中,文档可能包含多个主题,将多主题文档分配到单一主题并不总是最准确的方法。我们稍后会深入讨论这一点,因为 BERTopic 有几种处理方法,但理解 BERTopic 的主题建模本质上是一项聚类任务是很重要的。

BERTopic 管道的模块化特性可以扩展到每个组件。尽管句子变换器作为默认嵌入模型用于将文档转换为数值表示,但我们并不受限于使用任何其他嵌入技术。维度减少、聚类和主题生成过程同样适用。无论用例是选择 k-Means 而不是 HDBSCAN,还是选择 PCA 而不是 UMAP,都是可能的。

你可以把这种模块化看作是用乐高积木构建,管道的每个部分都可以完全替换为另一个类似的算法。这种“乐高积木”思维方式在图 3-11 中得到了说明。该图还展示了我们可以使用的一个额外的算法乐高块。尽管我们使用 c-TF-IDF 来创建初始主题表示,但还有许多有趣的方法可以利用 LLMs 来微调这些表示。在下面的“表示模型”部分,我们将详细探讨这个算法乐高块的工作原理。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_clustering_and_topic_modeling_664502_11.png

图 3-11. BERTopic 的模块化是一个关键组件,允许你根据需要构建自己的主题模型。

代码概述

够了,开始动手吧!这是一本实践型书籍,现在是时候进行一些实际编码了。默认管道,如之前在图 3-10 中所示,只需要几行代码:

from bertopic import BERTopic

# Instantiate our topic model
topic_model = BERTopic()

# Fit our topic model on a list of documents
topic_model.fit(documents)

然而,BERTopic 的模块化特性以及我们迄今为止可视化的内容,也可以通过编码示例进行可视化。首先,让我们导入一些相关的包:

from umap import UMAP
from hdbscan import HDBSCAN
from sentence_transformers import SentenceTransformer
from sklearn.feature_extraction.text import CountVectorizer

from bertopic import BERTopic
from bertopic.representation import KeyBERTInspired
from bertopic.vectorizers import ClassTfidfTransformer

正如你可能注意到的,大多数导入的包,如 UMAP 和 HDBSCAN,是默认 BERTopic 管道的一部分。接下来,让我们更明确地构建 BERTopic 的默认管道,逐步进行每个个体步骤:

# Step 1 - Extract embeddings (blue block)
embedding_model = SentenceTransformer("all-MiniLM-L6-v2")

# Step 2 - Reduce dimensionality (red block)
umap_model = UMAP(n_neighbors=15, n_components=5, min_dist=0.0, metric='cosine')

# Step 3 - Cluster reduced embeddings (green block)
hdbscan_model = HDBSCAN(min_cluster_size=15, metric='euclidean', cluster_selection_method='eom', prediction_data=True)

# Step 4 - Tokenize topics (yellow block)
vectorizer_model = CountVectorizer(stop_words="english")

# Step 5 - Create topic representation (grey block)
ctfidf_model = ClassTfidfTransformer()

# Step 6 - (Optional) Fine-tune topic representations with 
# a `bertopic.representation` model (purple block)
representation_model = KeyBERTInspired()
# Combine the steps and build our own topic model
topic_model = BERTopic(
  embedding_model=embedding_model,          *# Step 1 - Extract embeddings*
  umap_model=umap_model,                    *# Step 2 - Reduce dimensionality*
  hdbscan_model=hdbscan_model,              *# Step 3 - Cluster reduced embeddings*
  vectorizer_model=vectorizer_model,        *# Step 4 - Tokenize topics*
  ctfidf_model=ctfidf_model,                *# Step 5 - Extract topic words*
  representation_model=representation_model *# Step 6 - Fine-tune topics*
)

这段代码使我们能够明确地经历算法的所有步骤,并且基本上让我们以任何我们想要的方式构建主题模型。所得到的主题模型,在变量topic_model中定义,现已代表 BERTopic 的基本管道,如之前在图 3-10 中所示。** **## 示例

在整个使用案例中,我们将继续使用 ArXiv 文章的摘要。为了回顾我们在文本聚类中所做的工作,我们开始使用 HuggingFace 的数据集包导入数据集,并提取我们稍后要使用的元数据,如摘要、年份和文章类别。

# Load data from huggingface
from datasets import load_dataset
dataset = load_dataset("maartengr/arxiv_nlp")

# Extract specific metadata
abstracts = dataset["Abstracts"]
years = dataset["Years"]
categories = dataset["Categories"]
titles = dataset["Titles"]

使用 BERTopic 非常简单,只需三行代码即可完成:

# Train our topic model in only three lines of code
from bertopic import BERTopic

topic_model = BERTopic()
topics, probs = topic_model.fit_transform(abstracts)

使用这个管道,你将获得 3 个返回变量,即topic_modeltopicsprobs

  • topic_model是我们刚刚训练的模型,包含有关模型和我们创建的主题的信息。

  • topics是每个摘要的主题。

  • probs是某个主题属于特定摘要的概率。

在我们开始探索主题模型之前,有一个变化需要使结果可复现。如前所述,BERTopic 的一个基础模型是 UMAP。这个模型具有随机性,这意味着每次运行 BERTopic 时,我们都会得到不同的结果。我们可以通过将random_state传递给 UMAP 模型来防止这种情况。

from umap import UMAP
from bertopic import BERTopic

# Using a custom UMAP model
umap_model = UMAP(n_neighbors=15, n_components=5, min_dist=0.0, metric='cosine', random_state=42)

# Train our model
topic_model = BERTopic(umap_model=umap_model)
topics, probs = topic_model.fit_transform(abstracts)

现在,让我们开始探索创建的主题。get_topic_info()方法可以快速描述我们找到的主题:

>>> topic_model.get_topic_info()
Topic    Count    Name
0    -1    11648    -1_of_the_and_to
1    0    1554    0_question_answer_questions_qa
2    1    620    1_hate_offensive_toxic_detection
3    2    578    2_summarization_summaries_summary_abstractive
4    3    568    3_parsing_parser_dependency_amr
...    ...    ...    ...
317    316    10    316_prf_search_conversational_spoke
318    317    10    317_crowdsourcing_workers_annotators_underline
319    318    10    318_curriculum_nmt_translation_dcl
320    319    10    319_botsim_menu_user_dialogue
321    320    10    320_color_colors_ib_naming

从我们的模型中生成了许多主题,XXX!每个主题由几个关键字表示,这些关键字在名称列中用“_”连接。这个名称列使我们能够快速了解主题内容,因为它显示了最能代表该主题的四个关键字。

注意

你可能也注意到第一个主题标记为-1。这个主题包含所有无法归入某个主题的文档,并被视为离群值。这是聚类算法 HDBSCAN 的结果,它并不强制所有点都被聚类。为了去除离群值,我们可以使用非离群算法,如 k-Means,或使用 BERTopic 的reduce_outliers()函数去除一些离群值并将它们分配给主题。

例如,主题 2 包含关键字“summarization”、“summaries”、“summary”和“abstractive”。根据这些关键字,似乎这个主题是关于总结任务的。为了获取每个主题的前 10 个关键字及其 c-TF-IDF 权重,我们可以使用 get_topic()函数:

>>> topic_model.get_topic(2)
[('summarization', 0.029974019692323675),
 ('summaries', 0.018938088406361412),
 ('summary', 0.018019112468622436),
 ('abstractive', 0.015758156442697138),
 ('document', 0.011038627359130419),
 ('extractive', 0.010607624721836042),
 ('rouge', 0.00936377058925341),
 ('factual', 0.005651676100789188),
 ('sentences', 0.005262910357048789),
 ('mds', 0.005050565343932314)]

这为我们提供了更多关于主题的背景,有助于我们理解主题的内容。例如,看到“rogue”这个词出现是很有趣的,因为这是评估摘要模型的一个常见指标。

我们可以使用find_topics()函数根据搜索词搜索特定主题。让我们搜索一个关于主题建模的主题:

>>> topic_model.find_topics("topic modeling")
([17, 128, 116, 6, 235],
 [0.6753638370140129,
  0.40951682679389345,
  0.3985390076544335,
  0.37922002441932795,
  0.3769700288091359])

它返回主题 17 与我们的搜索词具有相对较高的相似度(0.675)。如果我们检查该主题,可以看到它确实是关于主题建模的主题:

>>> topic_model.get_topic(17)
[('topic', 0.0503756681079549),
 ('topics', 0.02834246786579726),
 ('lda', 0.015441277604137684),
 ('latent', 0.011458141214781893),
 ('documents', 0.01013764950401255),
 ('document', 0.009854201885298964),
 ('dirichlet', 0.009521114618288628),
 ('modeling', 0.008775384549157435),
 ('allocation', 0.0077508974418589605),
 ('clustering', 0.005909325849593925)]

尽管我们知道这个主题是关于主题建模的,但让我们看看 BERTopic 的摘要是否也分配给了这个主题:

>>> topics[titles.index('BERTopic: Neural topic modeling with a class-based TF-IDF procedure')]
17

是的!看起来这个主题不仅涉及基于 LDA 的方法,还有基于聚类的技术,比如 BERTopic。

最后,我们之前提到许多主题建模技术假设一个文档甚至一句话中可能包含多个主题。尽管 BERTopic 利用聚类,这假设每个数据点只有一个分配,但它可以近似主题分布。

我们可以使用这种技术查看 BERTopic 论文第一句话的主题分布:

index = titles.index('BERTopic: Neural topic modeling with a class-based TF-IDF procedure')

# Calculate the topic distributions on a token-level
topic_distr, topic_token_distr = topic_model.approximate_distribution(abstracts[index][:90], calculate_tokens=True)
df = topic_model.visualize_approximate_distribution(abstracts[index][:90], topic_token_distr[0])
df

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_clustering_and_topic_modeling_664502_12.png

图 3-12. BERTopic 中提供了多种可视化选项。

如图 3-12 所示的输出表明,文档在一定程度上包含多个主题。此分配甚至是在令牌级别上完成的!

(互动)可视化

手动处理XXX主题可能是一项艰巨的任务。相反,多个有用的可视化功能让我们能够广泛了解生成的主题。其中许多使用 Plotly 可视化框架进行互动。

图 3-13 显示了 BERTopic 中所有可能的可视化选项,从二维文档表示和主题条形图到主题层次和相似性。虽然我们没有逐一介绍所有可视化,但有些值得关注。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_clustering_and_topic_modeling_664502_13.png

图 3-13。BERTopic 中可用多种可视化选项。

首先,我们可以通过使用 UMAP 来减少每个主题的 c-TF-IDF 表示,创建主题的二维表示。

topic_model.visualize_topics()

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_clustering_and_topic_modeling_664502_14.png

图 3-14。主题在二维空间中的主题间距离图。

如图 3-14 所示,这生成了一个互动可视化,当鼠标悬停在一个圆圈上时,我们可以看到主题、其关键词及其大小。主题的圆圈越大,包含的文档越多。通过与此可视化的交互,我们可以快速看到相似主题的组。

我们可以使用visualize_documents()函数将分析提升到另一个层次,即在文档层面分析主题。

# Visualize a selection of topics and documents
topic_model.visualize_documents(titles, 
      topics=[0, 1, 2, 3, 4, 6, 7, 10, 12, 
 13, 16, 33, 40, 45, 46, 65])

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_clustering_and_topic_modeling_664502_15.png

图 3-15。摘要及其主题在二维可视化中表示。

图 3-15 演示了 BERTopic 如何在二维空间中可视化文档。

注意

我们只可视化了部分主题,因为显示所有 300 个主题会导致可视化变得相当杂乱。此外,我们传递的是titles而不是abstracts,因为我们只想在鼠标悬停在文档上时查看每篇论文的标题,而不是整个摘要。

最后,我们可以使用 visualize_barchart()创建一个关键词的条形图,基于一部分主题:

topic_model.visualize_barchart(topics=list(range(50, 58, 1)))

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_clustering_and_topic_modeling_664502_16.png

图 3-16。前 8 个主题的前 5 个关键词。

图 3-16 中的柱状图很好地指示了哪些关键词对特定主题最重要。以主题 2 为例——似乎单词“总结”最能代表该主题,而其他单词在重要性上非常相似。

表示模型

借助 BERTopic 采用的神经搜索风格的模块化,它可以利用多种不同类型的大型语言模型,同时最小化计算。这使得各种主题微调方法得以实现,从词性标注到文本生成方法,例如 ChatGPT。图 3-17 展示了我们可以利用来微调主题表示的各种 LLM。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_clustering_and_topic_modeling_664502_17.png

图 3-17。在应用 c-TF-IDF 权重后,可以使用多种表示模型对主题进行微调。其中许多是大型语言模型。

使用 c-TF-IDF 生成的主题是与其主题相关的单词的良好初步排名。在本节中,这些单词的初步排名可以视为主题的候选关键词,因为我们可能会根据任何表示模型来改变它们的排名。我们将介绍几种可以在 BERTopic 中使用的表示模型,并且从大型语言模型的角度来看,这些模型也非常有趣。

在开始之前,我们首先需要做两件事。第一,我们将保存原始主题表示,这样与有无表示模型进行比较时将更容易:

# Save original representations
from copy import deepcopy
original_topics = deepcopy(topic_model.topic_representations_)

第二,让我们创建一个简短的包装,以便快速可视化主题词的差异,以便比较有无表示模型的情况:

def topic_differences(model, original_topics, max_length=75, nr_topics=10):
  """ For the first 10 topics, show the differences in 
  topic representations between two models """
  for topic in range(nr_topics):

    # Extract top 5 words per topic per model
    og_words = " | ".join(list(zip(*original_topics[topic]))[0][:5])
    new_words = " | ".join(list(zip(*model.get_topic(topic)))[0][:5])

    # Print a 'before' and 'after'
    whitespaces = " " * (max_length - len(og_words))
    print(f"Topic: {topic}    {og_words}{whitespaces}-->     {new_words}")

KeyBERTInspired

c-TF-IDF 生成的主题并未考虑主题中单词的语义性质,这可能导致生成包含停用词的主题。我们可以使用模块 bertopic.representation_model.KeyBERTInspired() 根据关键词与主题的语义相似性来微调主题关键词。

KeyBERTInspired 是一种方法,正如你可能猜到的,灵感来自于 关键词提取包 KeyBERT。在最基本的形式中,KeyBERT 通过余弦相似度比较文档中单词的嵌入与文档嵌入,以查看哪些单词与文档最相关。这些最相似的单词被视为关键词。

在 BERTopic 中,我们希望使用类似的方法,但在主题层面而不是文档层面。如图 3-18 所示,KeyBERTInspired 使用 c-TF-IDF 为每个主题创建一组代表性文档,方法是随机抽取每个主题的 500 个文档,计算它们的 c-TF-IDF 值,并找到最具代表性的文档。这些文档被嵌入并平均,用作更新后的主题嵌入。然后,计算我们的候选关键词与更新后的主题嵌入之间的相似度,以重新排序我们的候选关键词。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_clustering_and_topic_modeling_664502_18.png

图 3-18. KeyBERTInspired 表示模型的过程
# KeyBERTInspired
from bertopic.representation import KeyBERTInspired
representation_model = KeyBERTInspired()

# Update our topic representations
new_topic_model.update_topics(abstracts, representation_model=representation_model)

# Show topic differences
topic_differences(topic_model, new_topic_model)

主题: 0 问题 | qa | 问题 | 答案 | 回答 --> 问答 | 回答 | 问答 | 注意 | 检索

主题: 1 仇恨 | 攻击性 | 言论 | 检测 | 有毒 --> 仇恨的 | 仇恨 | 网络欺凌 | 言论 | 推特

主题: 2 摘要 | 总结 | 总结 | 抽象 | 提取 --> 摘要生成器 | 摘要生成 | 摘要 | 摘要 | 总结

主题: 3 解析 | 解析器 | 依赖 | amr | 解析器 --> 解析器 | 解析 | 树库 | 解析器 | 树库

主题: 4 词 | 嵌入 | 嵌入 | 相似性 | 向量 --> word2vec | 嵌入 | 嵌入 | 相似性 | 语义

主题: 5 性别 | 偏见 | 偏差 | 去偏见 | 公平 --> 偏见 | 偏差 | 性别 | 性别 | 性别化

主题: 6 关系 | 提取 | re | 关系 | 实体 --> 关系 | 关系 | 实体 | 实体 | 关系的

主题: 7 提示 | 少量实例 | 提示 | 上下文 | 调整 --> 提示调整 | 提示 | 提示 | 提示中 | 基于提示

主题: 8 方面 | 情感 | absa | 基于方面 | 意见 --> 情感 | 方面 | 方面 | 方面级别 | 情感

主题: 9 解释 | 解释 | 理由 | 理由 | 可解释性 --> 解释 | 解释者 | 可解释性 | 解释 | 注意

更新后的模型显示,与原始模型相比,主题的可读性大大提高。同时,它也显示了使用基于嵌入技术的缺点。原始模型中的词汇,例如“amr”和“qa”,都是合理的词汇。

词性

c-TF-IDF 并不区分其认为重要的词的类型。无论是名词、动词、形容词,甚至是介词,它们都可能成为重要关键词。当我们希望有易于人类理解的标签,简单直观时,我们可能希望主题仅由名词来描述。

这里就是著名的 SpaCy 包派上用场的地方。这是一个工业级的自然语言处理框架,提供多种管道、模型和部署选项。更具体地说,我们可以使用 SpaCy 加载一个能够检测词性(无论是名词、动词还是其他)的英语模型。

如图 3-19 所示,我们可以使用 SpaCy 确保只有名词进入我们的主题表示。与大多数表示模型一样,这种方法非常高效,因为名词仅从一个小而具有代表性的数据子集提取。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_clustering_and_topic_modeling_664502_19.png

图 3-19. 词性表示模型的过程
# Part-of-Speech tagging
from bertopic.representation import PartOfSpeech
representation_model = PartOfSpeech("en_core_web_sm")

# Use the representation model in BERTopic on top of the default pipeline
topic_model.update_topics(abstracts, representation_model=representation_model)

# Show topic differences
topic_differences(topic_model, original_topics)

主题:0 问题 | qa | 问题 | 答案 | 回答 --> 问题 | 问题 | 答案 | 回答 | 答案

主题:1 仇恨 | 冒犯 | 言论 | 检测 | 有毒 --> 仇恨 | 冒犯 | 言论 | 检测 | 有毒

主题:2 摘要 | 摘要 | 总结 | 抽象 | 提取 --> 摘要 | 摘要 | 总结 | 抽象 | 提取

主题:3 解析 | 解析器 | 依赖 | amr | 解析器 --> 解析 | 解析器 | 依赖 | 解析器 | 树库

主题:4 单词 | 嵌入 | 嵌入 | 相似性 | 向量 --> 单词 | 嵌入 | 相似性 | 向量 | 单词

主题:5 性别 | 偏见 | 偏见 | 去偏见 | 公平 --> 性别 | 偏见 | 偏见 | 去偏见 | 公平

主题:6 关系 | 提取 | re | 关系 | 实体 --> 关系 | 提取 | 关系 | 实体 | 远程

主题:7 提示 | 少样本 | 提示 | 上下文 | 调整 --> 提示 | 提示 | 调整 | 提示 | 任务

主题:8 方面 | 情感 | absa | 基于方面 | 意见 --> 方面 | 情感 | 意见 | 方面 | 极性

主题:9 解释 | 解释 | 理由 | 理由 | 可解释性 --> 解释 | 解释 | 理由 | 理由 | 可解释性

最大边际相关性

使用 c-TF-IDF,生成的关键词可能会有很多冗余,因为它不认为“车”和“汽车”本质上是相同的。换句话说,我们希望生成的主题具有足够的多样性,同时尽可能少重复。(图 3-20)

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_clustering_and_topic_modeling_664502_20.png

图 3-20. 最大边际相关性表示模型的过程。生成关键词的多样性由 λ(λ)表示。我们可以使用一种名为最大边际相关性(MMR)的算法来使我们的主题表示多样化。该算法从与主题最匹配的关键词开始,然后迭代计算下一个最佳关键词,同时考虑一定程度的多样性。换句话说,它会取一些候选主题关键词,例如 30 个,并尝试选择最佳代表主题的前 10 个关键词,同时确保它们彼此多样化。
# Maximal Marginal Relevance
from bertopic.representation import MaximalMarginalRelevance
representation_model = MaximalMarginalRelevance(diversity=0.5)

# Use the representation model in BERTopic on top of the default pipeline
topic_model.update_topics(abstracts, representation_model=representation_model)

# Show topic differences
topic_differences(topic_model, original_topics)

主题: 0 问题 | QA | 问题 | 回答 | 回答中 --> QA | 问题 | 回答 | 理解 | 检索

主题: 1 仇恨 | 冒犯 | 演讲 | 检测 | 有毒 --> 演讲 | 侮辱 | 毒性 | 平台 | 仇恨

主题: 2 总结 | 摘要 | 总结 | 抽象 | 提取 --> 总结 | 提取 | 多文档 | 文档 | 评估

主题: 3 解析 | 解析器 | 依赖 | AMR | 解析器 --> AMR | 解析器 | 语料库 | 句法 | 成分

主题: 4 词 | 嵌入 | 嵌入 | 相似性 | 向量 --> 嵌入 | 相似性 | 向量 | word2vec | glove

主题: 5 性别 | 偏见 | 偏见 | 去偏见 | 公平 --> 性别 | 偏见 | 公平 | 刻板印象 | 嵌入

主题: 6 关系 | 提取 | 关系 | 实体 --> 提取 | 关系 | 实体 | 文档级 | 文档提取

主题: 7 提示 | 少样本 | 提示 | 上下文 | 调整 --> 提示 | 零样本 | PLMs | 元学习 | 标签

主题: 8 方面 | 情感 | ABSA | 基于方面 | 观点 --> 情感 | ABSA | 方面 | 提取 | 极性

主题: 9 解释 | 解释 | 理由 | 理由 | 可解释性 --> 解释 | 可解释性 | 显著性 | 可信性 | 方法

生成的主题更加多样化!主题XXX原本使用了很多“总结”相关的词汇,而现在该主题只包含“总结”这个词。同时,像“embedding”和“embeddings”的重复词汇也被移除了。* *## 文本生成

文本生成模型在 2023 年显示出巨大的潜力。它们在广泛的任务中表现出色,并允许在提示中进行广泛的创造性。它们的能力不容小觑,而不在 BERTopic 中使用它们无疑是一种浪费。我们在XXX章中详细讨论了这些模型,但现在查看它们如何与主题建模过程结合是有益的。

如图 3-21 所示,我们可以通过专注于生成主题级输出而非文档级输出,来高效使用 BERTopic。这可以将 API 调用的数量从数百万(例如,数百万的摘要)减少到几百(例如,数百个主题)。这不仅显著加快了主题标签的生成速度,而且在使用外部 API(如 Cohere 或 OpenAI)时,也不需要大量的费用。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_clustering_and_topic_modeling_664502_21.png

图 3-21. 使用文本生成 LLMs 和提示工程从与每个主题相关的关键词和文档中创建主题标签。

提示

正如在图 3-21 中所示,文本生成的一个主要组成部分是提示。在 BERTopic 中,这同样重要,因为我们希望向模型提供足够的信息,以便它能决定主题内容。BERTopic 中的提示通常看起来像这样:

prompt = """
I have a topic that contains the following documents: \n[DOCUMENTS]
The topic is described by the following keywords: [KEYWORDS]

Based on the above information, give a short label of the topic.
"""

该提示包含三个组成部分。首先,它提到一些最能描述主题的文档。这些文档通过计算它们的 c-TF-IDF 表示并与主题的 c-TF-IDF 表示进行比较来选择。然后提取前四个最相似的文档,并使用“[文档]”标签进行引用。

I have a topic that contains the following documents: \n[DOCUMENTS]

其次,构成主题的关键词也会传递给提示,并使用“[关键词]”标签进行引用。这些关键词也可以通过 KeyBERTInspired、词性或任何表示模型进行优化。

The topic is described by the following keywords: [KEYWORDS]

第三,我们向大型语言模型提供具体指令。这与之前的步骤同样重要,因为这将决定模型如何生成标签。

Based on the above information, give a short label of the topic.

该提示将被呈现为主题 XXX:

"""
I have a topic that contains the following documents: 
- Our videos are also made possible by your support on patreon.co.
- If you want to help us make more videos, you can do so on patreon.com or get one of our posters from our shop.
- If you want to help us make more videos, you can do so there.
- And if you want to support us in our endeavor to survive in the world of online video, and make more videos, you can do so on patreon.com.

The topic is described by the following keywords: videos video you our support want this us channel patreon make on we if facebook to patreoncom can for and more watch 

Based on the above information, give a short label of the topic.
"""

HuggingFace

幸运的是,与大多数大型语言模型一样,我们可以通过HuggingFace 的 Modelhub使用大量开源模型。

最著名的开源大型语言模型之一是 Flan-T5 生成模型系列,它针对文本生成进行了优化。这些模型的有趣之处在于它们使用一种称为指令调优的方法进行训练。通过对许多以指令形式表达的任务进行微调,模型学会了遵循特定的指令和任务。

BERTopic 允许使用这样的模型生成主题标签。我们创建一个提示,请它根据每个主题的关键词生成主题,并标记为[关键词]

from transformers import pipeline
from bertopic.representation import TextGeneration

# Text2Text Generation with Flan-T5
generator = pipeline('text2text-generation', model='google/flan-t5-xl')
representation_model = TextGeneration(generator)

# Use the representation model in BERTopic on top of the default pipeline
topic_model.update_topics(abstracts, representation_model=representation_model)

# Show topic differences
topic_differences(topic_model, original_topics)

主题:0 演讲 | asr | 识别 | 声学 | 端到端 --> 音频语法识别

主题:1 临床 | 医疗 | 生物医学 | 笔记 | 健康 --> ehr

主题:2 摘要 | 总结 | 总结 | 抽象 | 抽取 --> mds

主题:3 解析 | 解析器 | 依赖关系 | amr | 解析器 --> 解析器

主题:4 仇恨 | 攻击性 | 演讲 | 检测 | 有毒 --> Twitter

主题:5 词 | 嵌入 | 嵌入向量 | 相似性 --> word2vec

主题:6 性别 | 偏见 | 偏差 | 去偏见 | 公平性 --> 性别偏见

主题:7 命名 | 实体 | 识别 | 嵌套 --> ner

主题:8 提示 | 少样本 | 提示 | 上下文 | 调优 --> gpt3

主题:9 关系 | 提取 | re | 关系 | 远程 --> docre

有趣的主题标签被创建,但我们也可以看到该模型并不是完美无缺的。

OpenAI

当我们谈论生成性 AI 时,不能忘记 ChatGPT 及其惊人的表现。尽管不是开源的,但它是一个有趣的模型,在短短几个月内改变了 AI 领域。我们可以从 OpenAI 的集合中选择任何文本生成模型在 BERTopic 中使用。

由于该模型是基于 RLHF 训练的,并且优化用于聊天目的,因此使用该模型进行提示非常令人满意。

from bertopic.representation import OpenAI

# OpenAI Representation Model
prompt = """
I have a topic that contains the following documents: \n[DOCUMENTS]
The topic is described by the following keywords: [KEYWORDS]

Based on the information above, extract a short topic label in the following format:
topic: <topic label>
"""
representation_model = OpenAI(model="gpt-3.5-turbo", delay_in_seconds=10, chat=True)

# Use the representation model in BERTopic on top of the default pipeline
topic_model.update_topics(abstracts, representation_model=representation_model)

# Show topic differences
topic_differences(topic_model, original_topics)

主题:0 演讲 | asr | 识别 | 声学 | 端到端 --> 音频语法识别

主题:1 临床 | 医疗 | 生物医学 | 记录 | 健康 --> ehr

主题:2 总结 | 摘要 | 总结 | 抽象 | 提取 --> mds

主题:3 解析 | 解析器 | 依赖 | amr | 解析器 --> parser

主题:4 仇恨 | 攻击性 | 言论 | 检测 | 有毒 --> Twitter

主题:5 词 | 嵌入 | 嵌入向量 | 相似性 --> word2vec

主题:6 性别 | 偏见 | 偏差 | 去偏见 | 公平性 --> 性别偏见

主题:7 命名 | 实体 | 识别 | 嵌套 --> ner

主题:8 提示 | 少样本 | 提示 | 上下文 | 调优 --> gpt3

主题:9 关系 | 提取 | re | 关系 | 远程 --> docre

由于我们期望 ChatGPT 以特定格式返回主题,即“主题:<主题标签>”,因此在创建自定义提示时,指示模型按此格式返回非常重要。请注意,我们还添加了delay_in_seconds参数,以便在 API 调用之间创建恒定的延迟,以防你使用的是免费账户。

Cohere

与 OpenAI 一样,我们可以在 BERTopic 的管道中使用 Cohere 的 API,进一步微调主题表示,结合生成文本模型。确保获取 API 密钥,这样你就可以开始生成主题表示。

import cohere
from bertopic.representation import Cohere

# Cohere Representation Model
co = cohere.Client(my_api_key)
representation_model = Cohere(co)

# Use the representation model in BERTopic on top of the default pipeline
topic_model.update_topics(abstracts, representation_model=representation_model)

# Show topic differences
topic_differences(topic_model, original_topics)

主题:0 演讲 | asr | 识别 | 声学 | 端到端 --> 音频语法识别

主题:1 临床 | 医疗 | 生物医学 | 记录 | 健康 --> ehr

主题:2 总结 | 摘要 | 总结 | 抽象 | 提取 --> mds

主题:3 解析 | 解析器 | 依赖 | amr | 解析器 --> parser

主题:4 仇恨 | 攻击性 | 言论 | 检测 | 有毒 --> Twitter

主题:5 词 | 嵌入 | 嵌入向量 | 相似性 --> word2vec

主题:6 性别 | 偏见 | 偏差 | 去偏见 | 公平性 --> 性别偏见

主题:7 命名 | 实体 | 识别 | 嵌套 --> ner

主题:8 提示 | 少样本 | 提示 | 上下文 | 调优 --> gpt3

主题:9 关系 | 提取 | re | 关系 | 远程 --> docre

LangChain

为了进一步提升大型语言模型的能力,我们可以利用 LangChain 框架。它允许任何先前的文本生成方法补充额外信息,甚至链式结合。特别是,LangChain 将语言模型连接到其他数据源,使它们能够与环境互动。

例如,我们可以使用它与 OpenAI 构建一个向量数据库,并在该数据库上应用 ChatGPT。由于我们希望尽量减少 LangChain 所需的信息量,因此将最具代表性的文档传递给该软件包。然后,我们可以使用任何 LangChain 支持的语言模型来提取主题。下面的示例演示了如何将 OpenAI 与 LangChain 结合使用。

from langchain.llms import OpenAI
from langchain.chains.question_answering import load_qa_chain
from bertopic.representation import LangChain

# Langchain representation model
chain = load_qa_chain(OpenAI(temperature=0, openai_api_key=MY_API_KEY), chain_type="stuff")
representation_model = LangChain(chain)

# Use the representation model in BERTopic on top of the default pipeline
topic_model.update_topics(abstracts, representation_model=representation_model)

# Show topic differences
topic_differences(topic_model, original_topics)

Topic: 0 speech | asr | recognition | acoustic | endtoend --> audio grammatical recognition

Topic: 1 clinical | medical | biomedical | notes | health --> ehr

Topic: 2 summarization | summaries | summary | abstractive | extractive --> mds

Topic: 3 parsing | parser | dependency | amr | parsers --> parser

Topic: 4 hate | offensive | speech | detection | toxic --> Twitter

Topic: 5 word | embeddings | embedding | vectors | similarity --> word2vec

Topic: 6 gender | bias | biases | debiasing | fairness --> gender bias

Topic: 7 ner | named | entity | recognition | nested --> ner

Topic: 8 prompt | fewshot | prompts | incontext | tuning --> gpt3

Topic: 9 relation | extraction | re | relations | distant --> docre

主题建模变体

主题建模的领域相当广泛,涵盖了许多不同的应用和同一模型的变体。BERTopic 也不例外,它为不同目的实现了多种变体,例如动态、(半)监督、在线、分层和引导的主题建模。图 3-22-X 展示了一些主题建模变体以及如何在 BERTopic 中实现它们。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_clustering_and_topic_modeling_664502_22.png

图 3-22. -X BERTopic 中的主题建模变体*** ***# 摘要

在本章中,我们讨论了一种基于聚类的主题建模方法,BERTopic。通过利用模块化结构,我们使用了多种大型语言模型来创建文档表示并微调主题表示。我们提取了 ArXiv 摘要中的主题,并观察了如何利用 BERTopic 的模块化结构来开发不同类型的主题表示。*****

第四章:使用 GPT 模型进行文本生成

在本书的前几章中,我们已迈出了进入大型语言模型(LLMs)世界的第一步。我们深入探讨了各种应用,如分类和语义搜索,采用了专注于文本表示的模型,例如 BERT 及其衍生模型。

随着我们的进展,我们使用了主要用于文本生成的模型,这些模型通常被称为生成式预训练变换器(GPT)。这些模型具有响应用户提示生成文本的卓越能力。通过提示工程,我们可以以增强生成文本质量的方式设计这些提示。

在本章中,我们将更详细地探讨这些生成模型,并深入探讨提示工程、与生成模型推理、验证甚至评估其输出的领域。

使用文本生成模型

在开始提示工程的基本知识之前,探索如何利用文本生成模型的基础知识是至关重要的。我们如何选择使用的模型?我们是使用专有模型还是开源模型?我们如何控制生成的输出?这些问题将作为我们使用文本生成模型的垫脚石。

选择文本生成模型

选择文本生成模型始于在专有模型和开源模型之间进行选择。尽管专有模型通常性能更优,但我们在本书中更多地关注开源模型,因为它们提供了更多灵活性并且可以免费使用。

图 4-1 展示了一小部分具有影响力的基础模型,这些大型语言模型(LLMs)在大量文本数据上进行过预训练,并通常为特定应用进行了微调。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_generation_with_gpt_models_479875_01.png

图 4-1:基础模型

从这些基础模型中,数百个甚至数千个模型已被微调,使得某些模型更适合特定任务。选择使用的模型可能是一项艰巨的任务!

我们通常建议从小型且最近发布的基础模型开始,例如图 4-1 中的 Llama 2 或 Mistral 7B。这允许快速迭代,从而深入理解该模型是否适合你的用例。此外,小型模型需要更少的 GPU 内存(VRAM),如果你的 GPU 不大,这使得运行更加容易和快速。放大通常比缩小更令人愉快。

在本章的示例中,我们将采用来自 Zephyr 系列的模型,即Zephyr 7B-beta。这些模型是在 Mistral 7B 上经过微调的,Mistral 7B 是一个相对较小但相当强大的开源大型语言模型(LLM)。

如果你在生成 AI 领域刚起步,重要的是从一个较小的模型开始。这为初学者提供了很好的介绍,并为进阶到更大的模型打下坚实的基础。

加载文本生成模型

“如何加载文本生成模型”实际上可以单独成为一个章节。市面上有几十个包,各自拥有不同的压缩和推理策略来提高性能。

最简单的方法是通过众所周知的 HuggingFace Transformers 库:

import torch
from transformers import pipeline
# Load our model
pipe = pipeline(
    "text-generation", 
    model="HuggingFaceH4/zephyr-7b-beta", 
    torch_dtype=torch.bfloat16, 
    device_map="auto"
)

要使用该模型,我们需要仔细查看它的提示模板。任何 LLM 都需要特定的模板,以便它能够区分最近和较旧的查询/响应对。

为了说明这一点,让我们请 LLM 讲个关于鸡的笑话:

`def` format_prompt(query="", messages=`False`):
    """Use the internal chat template to format our query"""
    # The system prompt (what the LLM should know before answering) and our query:
    `if` `not` messages:
        messages = [
            {
                "role": "system",
                "content": "You are a helpful assistant.",
            },
            {"role": "user", "content": query},
        ]
    # We apply the LLMs internal chat template to our input prompt
    prompt = pipe.tokenizer.apply_chat_template(
        messages,
        tokenize=`False`,
        add_generation_prompt=`True`
    )
    `return` prompt
prompt = format_prompt("Write a short joke about chickens.")

除了我们的主要提示外,我们还生成了一个系统提示,为 LLM 提供生成响应的上下文或指导。如图 4-2 所示,提示模板帮助 LLM 理解不同类型提示之间的区别,以及 LLM 生成的文本与用户文本之间的区别。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_generation_with_gpt_models_479875_02.png

图 4-2. Zephyr 在与模型互动时所期望的模板。

使用该提示,我们可以让 LLM 给出答案:

# Generate the output
outputs = pipe(
    prompt, 
    max_new_tokens=256, 
    do_sample=`True`, 
    temperature=0.1, 
    top_p=0.95
)
print(outputs[0]["generated_text"])

输出结果为:

"""
<|system|>
You are a friendly chatbot.</s>
<|user|>
Write a joke about chickens.</s>
<|assistant|>
Why did the chicken cross the Mobilo?
Because the Eggspressway was closed for pavement!
"""

既然我们知道如何使用聊天模板创建提示,让我们深入探讨如何控制模型的输出。

控制模型输出

除了提示工程,我们还可以通过调整模型参数来控制我们想要的输出类型。在之前的示例中,你可能注意到我们在pipe函数中使用了多个参数,包括temperaturetop_p

这些参数控制输出的随机性。使 LLM 成为令人兴奋技术的一部分在于它可以为完全相同的提示生成不同的响应。每当 LLM 需要生成一个标记时,它会为每个可能的标记分配一个可能性数字。

如图 4-3 所示,在句子“我正在开着一辆…”中,后面跟上“”或“卡车”等标记的可能性通常高于“大象”。然而,“大象”仍然有生成的可能性,但它的概率要低得多。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_generation_with_gpt_models_479875_03.png

图 4-3. 模型根据它们的可能性评分选择生成下一个标记。

温度

temperature 控制生成文本的随机性或创造性。它定义了选择不太可能的标记的可能性。基本思想是,温度为 0 时每次都会生成相同的响应,因为它总是选择最可能的单词。如 图 4-4 所示,较高的值允许生成不太可能的单词。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_generation_with_gpt_models_479875_04.png

图 4-4。较高的温度增加了生成不太可能的标记的可能性,反之亦然。

结果是,较高的温度(例如 0.8)通常会导致更具多样性的输出,而较低的温度(例如 0.2)则会产生更确定性的输出。

top_p

top_p,也称为核采样,是一种控制 LLM 可以考虑哪些标记(核)的采样技术。它会考虑标记直到达到其累积概率。如果我们将 top_p 设置为 0.1,它会考虑标记直到达到该值。如果我们将 top_p 设置为 1,它会考虑所有标记。

如 图 4-5 所示,通过降低该值,将考虑更少的标记,通常会产生较少的“创造性”输出,而增加该值则允许 LLM 从更多的标记中进行选择。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_generation_with_gpt_models_479875_05.png

图 4-5。较高的 top_p 增加了可以选择生成的标记数量,反之亦然。

同样,top_k 参数精确控制 LLM 可以考虑多少个标记。如果将其值更改为 100,LLM 将仅考虑前 100 个最可能的标记。

如表 5-1 所示,这些参数使用户在创造性(高 temperaturetop_p)与可预测性(低 temperaturetop_p)之间拥有一个滑动尺度。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_generation_with_gpt_models_479875_06.png

图 4-6。选择 temperaturetop_p 值时的使用案例示例。

提示工程简介

与文本生成 LLM 相关的重要部分是提示工程。通过仔细设计我们的提示,我们可以引导 LLM 生成所需的响应。无论提示是问题、陈述还是指令,提示工程的主要目标是从模型中引发有用的响应。

然而,提示工程不仅仅是设计有效的提示。它还可以作为评估模型输出、设计保护措施和安全缓解方法的工具。这是一个提示优化的迭代过程,需要实验。不存在,也不太可能有完美的提示设计。

在本节中,我们将讨论提示工程的常见方法,以及理解某些提示效果的小技巧。这些技能使我们能够理解 LLM 的能力,并构成与这些模型接口的基础。

我们首先回答问题:提示中应该包含什么?

提示的基本组成部分

LLM 是一种预测机器。根据特定输入(提示),它尝试预测可能跟随的词语。在其核心,正如图 4-7 所示,提示不需要超过几个词就能引出 LLM 的响应。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_generation_with_gpt_models_479875_07.png

图 4-7. 提示的基本示例。没有给出指令,因此 LLM 将简单地尝试完成句子。

然而,尽管这个插图作为基本示例有效,但它无法完成特定任务。相反,我们通常通过询问一个特定问题或任务来进行提示工程,以便大型语言模型(LLM)可以完成。为了引出期望的响应,我们需要一个更结构化的提示。

例如,如图 4-8 所示,我们可以要求 LLM 将一个句子分类为具有正面或负面情感。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_generation_with_gpt_models_479875_08.png

图 4-8. 基本指令提示的两个组成部分,即指令本身和它所指的数据。

这将最基本的提示扩展为由两个组成部分构成——指令本身和与指令相关的数据。

更复杂的使用案例可能需要提示中包含更多组成部分。例如,为确保模型只输出“负面”或“正面”,我们可以引入输出指示符来帮助指导模型。在图 4-9 中,我们在句子前加上“文本:”并添加“情感:”以防止模型生成完整句子。相反,这种结构表示我们期待“负面”或“正面”。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_generation_with_gpt_models_479875_09.png

图 4-9. 通过输出指示符扩展提示,以允许特定输出。

我们可以继续添加或更新提示的元素,直到引出我们所寻找的响应。我们可以添加额外的示例,更详细地描述使用案例,提供额外的上下文等。这些组成部分仅是示例,并不是一个有限的可能集。设计这些组件所带来的创造力是关键。

尽管提示是单一的文本,但将提示视为更大拼图的一部分是非常有帮助的。我是否描述了我的问题的上下文?提示中是否包含了输出的示例?

基于指令的提示

尽管提示有很多种形式,从与 LLM 讨论哲学到与自己喜欢的超级英雄角色扮演,提示通常用于让 LLM 回答特定问题或解决某项任务。这被称为基于指令的提示。

图 4-10 展示了多个在基于指令的提示中发挥重要作用的用例。我们在之前的示例中已经进行了其中一个,即监督分类。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_generation_with_gpt_models_479875_10.png

图 4-10. 使用基于指令的提示的用例示例。

这些任务每个都需要不同格式的提示,更具体地说,需要向 LLM 提出不同的问题。要求 LLM 总结一段文本并不会突然导致分类。为了说明,一些这些用例的提示示例可以在图 4-11 中找到。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_generation_with_gpt_models_479875_11.png

图 4-11. 常见用例的提示示例。注意在一个用例中,指令的结构和位置可以改变。

尽管这些任务需要不同的指令,但实际上在用于提高输出质量的提示技术上有很多重叠。这些技术的非详尽列表包括:

特异性

准确描述你想要实现的目标。与其问 LLM“写一个产品描述。”不如问它“用少于两句话写一个产品描述,并使用正式的语气。”。

幻觉

LLM 可能会自信地生成不正确的信息,这被称为幻觉。为了减少其影响,我们可以要求 LLM 仅在知道答案的情况下生成回答。如果它不知道答案,则回应“我不知道”。

顺序

要么在提示的开始或结束时给出指令。尤其是在长提示中,中间的信息往往被遗忘。LLM 倾向于关注提示开头(首因效应)或结尾(近因效应)中的信息。

在这里,特异性可以说是最重要的方面。一个大型语言模型(LLM)不知道你想要什么,除非你对自己想要实现的目标和原因非常具体。

高级提示工程

表面上,创建一个好的提示似乎很简单。问一个具体的问题,准确,添加一些示例,你就完成了!然而,提示很快就会变得复杂,因此常常被低估为利用 LLM 的一个组成部分。

在这里,我们将通过几种高级技术来构建你的提示,从构建复杂提示的迭代工作流程开始,一直到顺序使用 LLM 以获得更好的结果。最终,我们甚至将建立高级推理技术。

提示的潜在复杂性

正如我们在提示工程导言中探讨的,提示通常由多个组件组成。在我们的第一个示例中,提示由指令、数据和输出指标组成。正如我们之前提到的,没有提示仅限于这三种组件,你可以根据需要构建得越复杂越好。

这些高级组件可以快速使提示变得相当复杂。一些常见的组件包括:

角色

描述 LLM 应该扮演的角色。例如,如果你想问关于天体物理学的问题,可以使用*“你是天体物理学专家。”*

指令

任务本身。确保这一点尽可能具体。我们不想留太多解释的余地。

上下文

描述问题或任务上下文的额外信息。它回答类似*“指令的原因是什么?”*的问题。

格式

LLM 应使用的输出生成文本的格式。如果没有它,LLM 将自行生成格式,这在自动化系统中是麻烦的。

受众

生成文本应面向谁。这也描述了生成输出的级别。出于教育目的,使用 ELI5(“像我 5 岁时那样解释。”)往往很有帮助。

语气

LLM 在生成文本时应使用的语气。如果你正在给老板写正式邮件,可能不想使用非正式的语气。

数据

与任务本身相关的主要数据。

为了说明这一点,让我们扩展之前的分类提示,并使用上述所有组件。这在图 4-12 中展示。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_generation_with_gpt_models_479875_12.png

图 4-12. 一个包含多个组件的复杂提示示例。

这个复杂的提示展示了提示的模块化特性。我们可以自由添加和删除组件,并判断它们对输出的影响。如图 4-13 所示,我们可以逐步构建提示,并探索每次变化的影响。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_generation_with_gpt_models_479875_13.png

图 4-13. 迭代模块组件是提示工程的重要组成部分。** **这些变化不仅限于简单地引入或移除组件。正如我们之前看到的近期效应和首因效应,它们的顺序会影响 LLM 的输出质量。

换句话说,实验在找到适合你用例的最佳提示时至关重要。通过提示,我们实际上处于一个迭代实验的循环中。

尝试一下!使用复杂提示添加和/或移除部分,以观察其对生成提示的影响。当你发现拼图中值得保留的部分时,会很快注意到。你可以通过将自己的数据添加到data变量中来使用自己的数据:

# Prompt components
persona = "You are an expert in Large Language models. You excel at breaking down complex papers into digestible summaries.`\n`"
instruction = "Summarize the key findings of the paper provided.`\n`"
context = "Your summary should extract the most crucial points that can help researchers quickly understand the most vital information of the paper.`\n`"
data_format = "Create a bullet-point summary that outlines the method. Follow this up with a concise paragraph that encapsulates the main results.`\n`"
audience = "The summary is designed for busy researchers that quickly need to grasp the newest trends in Large Language Models.`\n`"
tone = "The tone should be professional and clear.`\n`"
data = "Text to summarize: PUT_THE_DATA_TO_SUMMARIZE_HERE"
# The full prompt - remove and add pieces to view its impact on the generated output
query = persona + instruction + context + data_format + audience + tone + data
prompt = format_prompt(query)
提示

几乎每周都有新的提示组件可能提高输出的准确性。我们可以添加各种各样的组件,每周都会发现使用情感刺激(例如,“这对我的职业非常重要。”)等创造性组件。

提示工程的一大乐趣在于你可以尽可能地富有创造力,以找出哪些提示组件的组合对你的用例有帮助。开发一个适合你的格式几乎没有约束。

然而,请注意,某些提示对特定模型的效果更好,因为它们的训练数据可能不同,或者它们的训练目的不同。** **## 上下文学习:提供示例

在之前的章节中,我们试图准确描述大型语言模型(LLM)应该做什么。尽管准确和具体的描述有助于 LLM 理解用例,但我们可以更进一步。

我们为什么不直接展示任务,而是描述它呢?

我们可以向 LLM 提供我们想要实现的事物的确切示例。这通常被称为上下文学习,我们向模型提供正确的示例。

如图 4-14 所示,展示给 LLM 的示例数量会影响形式。零-shot 提示不利用示例,one-shot 提示使用一个示例,而 few-shot 提示使用两个或更多示例。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_generation_with_gpt_models_479875_14.png

图 4-14. 复杂提示的示例,包含多个组件。

采用原始短语,我们认为“一个例子胜过千言万语”。这些示例提供了 LLM 应该如何实现的直接示例。

我们可以用一个简单的示例来说明这种方法,该示例取自描述此方法的原始论文。提示的目标是生成一个包含虚构单词的句子。为了提高生成句子的质量,我们可以向生成模型展示一个包含虚构单词的正确句子的示例。

为此,我们需要区分我们的提问(user)和模型提供的答案(assistant):

# Use a single example of using the made-up word in a sentence
one_shot_prompt = format_prompt(messages=[
    {"role": "user", "content": "Q: A 'Gigamuru' is a type of Japanese musical instrument. An example of a sentence that uses the word Gigamuru is:"},
    {"role": "assistant", "content": "A: I have a Gigamuru that my uncle gave me as a gift. I love to play it at home."},
    {"role": "user", "content": "Q: To 'screeg' something is to swing a sword at it. An example of a sentence that uses the word screeg is:"}
])
print(one_shot_prompt)

该提示说明了区分用户和助手的必要性。如果我们不这样做,就会似乎在自言自语:

"""
<|user|>
Q: A 'Gigamuru' is a type of Japanese musical instrument. An example of a sentence that uses the word Gigamuru is:</s>
<|assistant|>
A: I have a Gigamuru that my uncle gave me as a gift. I love to play it at home.</s>
<|user|>
Q: To 'screeg' something is to swing a sword at it. An example of a sentence that uses the word screeg is:</s>
<|assistant|>
"""

我们可以使用这个提示来运行我们的模型:

# Run generative model
outputs = pipe(one_shot_prompt, max_new_tokens=64, do_sample=`True`, return_full_text=`False`)
print(outputs[0]["generated_text"])

结果是一个使用虚构词“screeg”的正确句子:

"A: I screeged the dragon's tail with my sword, but it only seemed to make it angrier."

与所有提示组件一样,一次或几次提示并不是提示工程的终极解决方案。我们可以将其作为增强我们给出的描述的拼图的一部分。模型仍然可以通过随机抽样“选择”忽略指令。

链式提示:拆分问题

在之前的示例中,我们探索了将提示拆分为模块化组件,以提高 LLM 的性能。尽管这在许多用例中效果很好,但对于高度复杂的提示或用例,这可能不可行。

我们可以在提示之间拆分问题,而不是在提示内部。基本上,我们将一个提示的输出作为下一个提示的输入,从而创建一个连续的交互链,解决我们的难题。

为了说明,假设我们希望使用 LLM 为我们根据一系列产品特征创建产品名称、口号和销售推介。尽管我们可以要求 LLM 一次性完成,但我们可以将问题拆分为多个部分。

因此,如在图 4-16 中所示,我们得到一个顺序管道,首先创建产品名称,然后将其与产品特征作为输入创建口号,最后使用特征、产品名称和口号来创建销售推介。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_generation_with_gpt_models_479875_15.png

图 4-15. 使用产品特征的描述,链式提示创建合适的名称、口号和销售推介。

这种链式提示的技术使 LLM 能够在每个单独问题上花费更多时间,而不是处理整个问题。

让我们用一个小例子来说明。

# Create name and slogan for a product
product_prompt = format_prompt("Create a name and slogan for a chatbot that leverages LLMs.")
outputs = pipe(product_prompt, max_new_tokens=32, do_sample=`True`, return_full_text=`False`)
product_description = outputs[0]["generated_text"]
# Use name and slogan as input for a sales pitch
sales_prompt = format_prompt(f"What would be a good sales pitch for the following product: '`{`product_description`}`'?")
outputs = pipe(sales_prompt, max_new_tokens=128, do_sample=`True`, return_full_text=`False`)
sales_pitch = outputs[0]["generated_text"]
# Results
print(product_description)
print(sales_pitch)

在这个例子中,我们首先要求模型创建名称和口号。然后,我们可以使用输出请求基于产品特征的良好销售推介。

这给我们带来了以下输出:

"""
Name: LLM Assistant
Slogan: "Your go-to chatbot powered by cutting-edge language learning models."
Introducing LLM Assistant, the revolutionary chatbot that transforms the way you learn languages. Unlike traditional language learning methods, LLM Assistant utilizes the latest language learning models powered by Artificial Intelligence. With LLM Assistant, you can communicate with native speakers, practice real-life conversations, and receive instant feedback. Whether you're a novice or an advanced speaker, LLM Assistant caters to your unique learning needs, making language learning fun, interactive, and efficient. So, why wait? Say hello to LLM Assistant, your new language learning companion!
"""

虽然我们需要两次调用模型,但一个主要好处是我们可以给每次调用不同的参数。例如,名称和口号创建的令牌数量相对较少,而推介可以更长。

它可以用于多种用例,包括:

响应验证

请求 LLM 重新检查之前生成的输出

并行提示

并行创建多个提示,并进行最终合并。例如,请多个 LLM 并行生成多个食谱,并使用组合结果创建购物清单。

写故事

利用 LLM 撰写书籍或故事,可以将问题拆解为各个组成部分。例如,首先撰写摘要,再发展角色并构建故事情节,然后再深入创作对话。

在第六章中,我们将超越链接 LLM,连接其他技术组件,如内存、搜索等!在此之前,提示链的这一概念将在接下来的部分中进一步探讨,描述更复杂的提示链方法,如自一致性、链式思维和树状思维。** **# 与生成模型的推理

在之前的部分中,我们主要集中于提示的模块化组件,通过迭代构建它们。这些先进的提示工程技术,如提示链,证明了实现生成模型复杂推理的第一步。

为了允许这种复杂的推理,现在是退后一步探讨推理内容的好时机。简单来说,我们的推理方法可以分为系统 1 和系统 2 思维过程,如图 5-X 所示。

系统 1 思维代表自动、直觉和几乎瞬时的反应。它与生成模型有相似之处,生成模型自动生成令牌而没有任何自我反思行为。相反,系统 2 思维是一个有意识、缓慢和逻辑的过程,类似于头脑风暴和自我反思。

如果我们能够赋予生成模型自我反思的能力,我们实际上是在模拟系统 2 的思维方式,这通常比系统 1 思维产生更深思熟虑的响应。

在本节中,我们将探讨几种技术,试图模仿这些人类推理者的思维过程,旨在提高模型的输出。

链式思维:回答前思考

实现生成模型复杂推理的第一大步骤是通过一种称为链式思维(CoT)的方法。CoT 旨在让生成模型“思考”后再回答问题,而不是直接回答而不进行任何推理。

如图 4-16 所示,它在提示中提供了示例,展示了模型在生成响应之前应进行的推理。这些推理过程被称为“思考”。这对涉及更高复杂度的任务(如数学问题)帮助巨大。增加这个推理步骤使模型能够在推理过程中分配更多计算资源。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_generation_with_gpt_models_479875_16.png

图 4-16. 链式思维提示使用推理示例来说服生成模型在其回答中使用推理。

我们将使用他们在论文中使用的例子来演示这一现象。首先,让我们探讨一下没有 CoT 的标准提示的输出。我们在提供示例时,不仅提供单一查询,而是区分用户和助手:

# Answering without explicit reasoning
standard_prompt = format_prompt(messages=[
    {"role": "user", "content": "Q: Roger has 5 tennis balls. He buys 2 more cans of tennis balls. Each can has 3 tennis balls. How many tennis balls does he have now?"},
    {"role": "assistant", "content": "A: The answer is 11."},
    {"role": "user", "content": "Q: The cafeteria had 23 apples. If they used 20 to make lunch and bought 6 more, how many apples do they have?"}
])
# Run generative model
outputs = pipe(standard_prompt, max_new_tokens=64, do_sample=`True`, return_full_text=`False`)
print(outputs[0]["generated_text"])

这给出了错误的答案:

"A: The answer is 26."

相反,我们将使用 CoT 让模型在给出答案之前展示其推理过程:

# Answering with chain-of-thought
cot_prompt = format_prompt(messages=[
    {"role": "user", "content": "Q: Roger has 5 tennis balls. He buys 2 more cans of tennis balls. Each can has 3 tennis balls. How many tennis balls does he have now?"},
    {"role": "assistant", "content": "A: Roger started with 5 balls. 2 cans of 3 tennis balls each is 6 tennis balls. 5 + 6 = 11\. The answer is 11."},
    {"role": "user", "content": "Q: The cafeteria had 23 apples. If they used 20 to make lunch and bought 6 more, how many apples do they have?"}
])
# Run generative model
outputs = pipe(cot_prompt, max_new_tokens=256, do_sample=`True`, return_full_text=`False`)
print(outputs[0]["generated_text"])

这次,我们得到了正确的响应:

"A: Initially, there were 23 apples. They used 20 apples to make lunch, leaving 3 apples (23 - 20 = 3). Then they bought 6 more apples. So, in total, the cafeteria now has (3 + 6) apples, which is 9 apples in total. The answer is 9."

这个推理过程尤其有帮助,因为模型是在生成答案之前这样做的。通过这样做,它可以利用迄今为止生成的知识来计算正确的答案。

零-shot 思维链

尽管 CoT 是增强生成模型输出的好方法,但它确实需要一个或多个推理示例,而用户可能没有这些示例。

我们可以简单地要求生成模型提供推理,而不是提供示例。有许多不同的形式有效,但一个常见且有效的方法是使用短语“让我们一步一步思考”,如图 4-17 所示。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_generation_with_gpt_models_479875_17.png

图 4-17. 不使用示例的思维链提示。相反,它使用短语“让我们一步一步思考”来激发其回答中的推理。

使用我们之前的例子,我们可以简单地将该短语附加到提示中,以启用类似 CoT 的推理:

# Zero-shot Chain-of-Thought
zeroshot_cot = format_prompt(
    "The cafeteria had 23 apples. If they used 20 to make lunch and bought 6 more, how many apples do they have? Let's think step-by-step."
)
# Run generative model
outputs = pipe(zeroshot_cot, max_new_tokens=512, do_sample=`True`, return_full_text=`False`)
print(outputs[0]["generated_text"])

再次,我们得到了正确的响应,但现在不需要提供示例:

"""
1\. We start with the original number of apples in the cafeteria: 23
2\. We determine how many apples were used to make lunch: 20
3\. We subtract the number of apples used to make lunch from the total number of apples to find the number of apples left in the cafeteria: 23 - 20 = 3
4\. We purchase 6 more apples: 3 + 6 = 9
5\. So the total number of apples now in the cafeteria is 23 (original number) - 20 (apples used for lunch) + 6 (apples purchased) = 9
6\. We can confirm that the calculation is correct by comparing the result with the original number of apples. Our answer is indeed closer to the original 23 apples than it was after we used 20 apples for lunch.
"""

这就是在进行计算时“展示你的过程”如此重要的原因。通过关注推理过程,我们可以为答案提供依据,更加确定答案的正确性。

提示

尽管提示“让我们一步一步思考”可以改善输出,但你并不受限于这个确切的表述。还有替代方案,例如“深呼吸一下,逐步思考”和“让我们逐步解决这个问题”。作者证明了提出替代表述的实用性。

自我一致性:抽样输出

如果通过 temperaturetop_p 等参数允许一定程度的创造性,使用相同的提示多次可能会导致不同的结果。因此,输出的质量可能会因随机选择的词元而提高或降低。换句话说,这全看运气!

为了抵消这种随机性并提高生成模型的性能,引入了自我一致性。这种方法多次向生成模型提出相同的提示,并将多数结果作为最终答案。在这个过程中,每个答案可能会受到不同 temperaturetop_p 值的影响,以增加抽样的多样性。

如图 4-18 所示,通过添加链式思维提示,可以进一步改进此方法,以提高推理能力,同时仅使用答案进行投票程序。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_generation_with_gpt_models_479875_18.png

图 4-18。通过从多个推理路径中采样,我们可以使用多数投票提取最可能的答案。

尽管此方法在提高输出方面效果良好,但确实需要对同一个问题进行多次提问。因此,尽管该方法可以提高性能,但它变得* n * 倍慢,其中 * n * 是输出样本的数量。

思维树:探索中间步骤

链式思维和自洽性旨在实现更复杂的推理。通过从多个“思想”中进行采样并使其更具思考性,我们旨在提高生成模型的输出。

这些技术仅仅触及了当前为实现这种复杂推理所做工作的表面。思维树对这些方法的改进使得多个想法的深入探索成为可能。

该方法的工作原理如下。当面临需要多次推理步骤的问题时,通常有助于将其拆分成几个部分。在每一步,如图 4-19 所示,生成模型被提示探索当前问题的不同解决方案。然后它投票选出最佳解决方案,接着继续到下一步。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_generation_with_gpt_models_479875_19.png

图 4-19。通过利用基于树的结构,生成模型可以生成待评估的中间思想。最有前景的思想被保留,最差的被剪除。

当需要考虑多条路径时,这种方法非常有帮助,例如在编写故事或想出创意时。

这种方法的一个缺点是需要多次调用生成模型,这会显著减慢应用程序的运行速度。幸运的是,已经成功尝试将思维树框架转化为一种简单的提示技术。

与其多次调用生成模型,我们让模型通过模拟多个专家之间的对话来模仿这种行为。这些专家将相互质疑,直到达成共识。一个思维树提示的例子是:

# Zero-shot Chain-of-Thought
zeroshot_tot = format_prompt(
    "Imagine three different experts are answering this question. All experts will write down 1 step of their thinking, then share it with the group. Then all experts will go on to the next step, etc. If any expert realises they're wrong at any point then they leave. The question is 'The cafeteria had 23 apples. If they used 20 to make lunch and bought 6 more, how many apples do they have?'"
)

我们可以使用此提示来探索大型语言模型(LLM)如何回应复杂问题:

# Run generative model
outputs = pipe(zeroshot_tot, max_new_tokens=512, do_sample=`True`, return_full_text=`False`)
print(outputs[0]["generated_text"])

结果,它通过利用多个专家之间的讨论生成了正确答案:

"""
Expert 1: The cafeteria started with 23 apples.
Expert 2: They used 20 of those apples to make lunch.
Expert 3: After making lunch, they bought 6 more apples to replenish their supply.
Expert 2: Now, they have a total of (23-20, initial apples minus apples used for lunch) plus 6, the additional apples they bought. That's a total of 23-20 + 6 = 9 apples left.
Expert 1: Wait a minute... If they started with 23 apples, how did they only have 9 left after buying more? That can't be right.
[Expert 1 realizes they made a math mistake and leaves.]
Expert 2: I'm going to double-check my math... [Takes out a calculator and verifies that they do indeed have 9 apples left.]
Expert 3: I'm confident in my figures, it's clear we have 9 apples left.
"""

看到“专家”之间如此详细的讨论是有趣的,并展示了提示工程带来的创造力。

输出验证

使用生成模型构建的系统和应用程序最终可能会投入生产。当发生这种情况时,验证和控制模型的输出以防止破坏应用程序并创建稳健的生成 AI 应用程序是重要的。

验证输出的原因可能包括:

结构化输出

默认情况下,大多数生成模型创建自由格式文本,而不遵循自然语言以外的特定结构。某些用例要求它们的输出以特定格式结构化,如 JSON。

有效输出

即使我们允许模型生成结构化输出,它仍然能够自由生成其内容。例如,当模型被要求输出两个选择之一时,它不应提出第三个选择。

伦理

一些开源生成模型没有安全限制,会生成不考虑安全或伦理的输出。例如,某些用例可能要求输出不包含亵渎、个人可识别信息(PII)、偏见、文化刻板印象等。

准确性

许多用例要求输出遵循特定标准或性能。其目的是仔细检查生成的信息是否在事实准确性、一致性或是否无幻觉方面。

控制生成模型的输出,如我们通过top_ptemperature等参数探讨的,并不是一件容易的事情。这些模型需要帮助才能生成符合某些指导方针的一致输出。

通常,有三种方式控制生成模型的输出:

示例

提供预期输出的多个示例。

语法

控制标记选择过程。

微调

在包含预期输出的数据上调优模型

在本节中,我们将讨论前两种方法。第三种方法,即微调模型,将留到第十二章,我们将在其中深入探讨微调方法。

提供示例

修复输出的一个简单明了的方法是向生成模型提供输出应有的示例。如前所述,少量示例学习是一种有效的技术,可以指导生成模型的输出。此方法也可以推广以指导输出的结构。

例如,让我们考虑一个示例,我们希望生成模型为 RPG 游戏创建角色档案。我们开始时不使用示例:

# Zero-shot learning: Providing no examples
zero_shot = format_prompt("Create a character profile for an RPG game in JSON format.")
outputs = pipe(zero_shot, max_new_tokens=128, do_sample=`True`, return_full_text=`False`)
print(outputs[0]["generated_text"])

这给我们以下结构,我们截断了它以防止过长的描述:

{
  "name": "Aurelia",
  "race": "Human",
  "class": "Mage",
  "age": 22,
  "gender": "Female",
  "description": "Aurelia is a young woman with a striking golden mane and...",
  "stats": {
    "strength": 8
  }
}

虽然这是有效的 JSON,但我们可能不希望某些属性,如“强度”或“年龄”。相反,我们可以向模型提供一些示例,指示预期的格式:

# Providing an example of the output structure
one_shot_prompt = format_prompt("""Create a character profile for an RPG game. Make sure to only use this format:
{
  "description": "A SHORT DESCRIPTION",
  "name": "THE CHARACTER'S NAME",
  "armor": "ONE PIECE OF ARMOR",
  "weapon": "ONE OR MORE WEAPONS"
}
""")
outputs = pipe(one_shot_prompt, max_new_tokens=256, do_sample=`True`, return_full_text=`False`)
print(outputs[0]["generated_text"])

这给我们以下内容,我们再次截断了它以防止过长的描述:

{
  "description": "A human wizard with long, wild grey hair and...",
  "name": "Sybil Astrid",
  "armor": "None",
  "weapon": [
    "Crystal Staff",
    "Oak Wand"
  ]
}

该模型完美遵循了我们给出的示例,从而实现了更一致的行为。这也展示了利用少量样本学习来改善输出结构而不仅仅是内容的重要性。

这里的重要说明是,模型是否遵循你建议的格式仍然取决于模型本身。有些模型在遵循指令方面表现得比其他模型更好。

语法:约束采样

少量样本学习有一个重大缺点:我们无法明确防止生成某些输出。尽管我们引导模型并给出指令,但它可能仍然不会完全遵循。

相反,许多工具迅速被开发出来以限制和验证生成模型的输出,如 Guidance、Guardrails 和 LMQL。部分上,它们利用生成模型验证自身输出,如图 4-20 所示。生成模型将输出作为新提示检索,并尝试根据一系列预定义的规则进行验证。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_generation_with_gpt_models_479875_20.png

图 4-20. 使用 LLM 检查输出是否正确遵循我们的规则。

同样,如图 4-21 所示,它还可以通过生成我们已知应如何结构化的格式部分来控制输出格式。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_generation_with_gpt_models_479875_21.png

图 4-21. 使用 LLM 仅生成我们事先不知道的信息片段。

这个过程可以更进一步,而不是在输出验证后进行,我们可以在令牌采样过程中进行验证。在采样令牌时,我们可以定义一系列语法或规则,以便 LLM 在选择下一个令牌时遵循。例如,如果我们要求模型在执行情感分类时返回“正面”、“负面”或“中性”,它仍然可能返回其他内容。如图 4-22 所示,通过限制采样过程,我们可以让 LLM 只输出我们感兴趣的内容。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/hsn-llm/img/text_generation_with_gpt_models_479875_22.png

图 4-22. 将令牌选择限制为仅三个可能的令牌:“正面”、“中性”和“负面”。

请注意,这仍然受到top_ptemperature等参数的影响,所示内容非常受限。

让我们通过 llama-cpp-python 来说明这一现象,这是一种库,类似于 transformers,我们可以用它来加载我们的语言模型。它通常用于有效加载和使用压缩模型(通过量化;见第十三章)。

我们通过在终端中运行以下命令来下载模型的量化版本:

wget https://huggingface.co/TheBloke/zephyr-7B-beta-GGUF/resolve/main/zephyr-7b-beta.Q4_K_M.gguf

py`Then, we load the model using llama-cpp-python and choose a JSON grammar to use. This will ensure that the output of the model adheres to JSON: 从 llama_cpp.llama 导入 httpx、Llama 和 LlamaGrammar # 我们从官方的 llama.cpp 存储库加载 JSON 语法 grammar = httpx.get(     “https://raw.githubusercontent.com/ggerganov/llama.cpp/master/grammars/json_arr.gbnf” ) grammar = LlamaGrammar.from_string(grammar.text) # 加载一个预量化的 LLM llm = Llama(“zephyr-7b-beta.Q4_K_M.gguf”) py The rules are described in the grammar file we downloaded. Using the JSON grammar, we can ask the model for an RPG character in JSON format to be used in our Dungeons and Dragons session: 导入 json # 运行生成模型,并要求它以 JSON 格式创建一个角色 response = llm(     “为 RPG 创建一个 JSON 格式的战士。”,     max_tokens=-1,     grammar=grammar ) # 以格式良好的 JSON 打印输出 print(json.dumps(json.loads(response[‘choices’][0][‘text’]), indent=4)) py This gives us valid JSON: [     {         “name”: “剑术大师”,         “level”: 10,         “health”: 250,         “mana”: 100,         “strength”: 18,         “dexterity”: 16,         “intelligence”: 10,         “armor”: 75,         “weapon”: “双手剑”,         “specialty”: “单手剑”     } ] ```py This allows us to more confidently use generative models in applications where we expect the output to adhere to certain formats.  ###### Note Note that we set the number of tokens to be generated with max_tokens to be, in principle, unlimited. This means that the model will continue generating until is has completed its JSON output or until it reaches its context limit.`````# 概要 在这一章中,我们通过提示工程和输出验证探讨了生成模型的基础知识。我们关注了提示工程带来的创造力和潜在复杂性。我们发现提示的组件是生成符合我们用例输出的关键。因此,在进行提示工程时,实验至关重要。 在下一章中,我们将探索利用生成模型的高级技术。这些技术超越了提示工程,旨在增强这些模型的能力。从给模型提供外部记忆到使用外部工具,我们旨在赋予生成模型超级能力!`**

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐