支持大型语言模型

大型语言模型(LLMs)随着时间的推移,其能力和可用性不断增强,因此它们在越来越多的不同应用中被发现。尽管skorch主要不是专注于与预训练模型一起工作,但它确实提供了一些与Hugging Face生态系统的集成,从而与Hugging Face上的预训练模型集成。本文档详细介绍了skorch支持的LLMs的一些用例。

使用语言模型作为零样本和少样本分类器

在本节中,我们将展示如何使用skorch执行零样本和少样本分类。

零样本分类入门

LLMs的一个应用是在零样本和少样本学习中。特别是,它们可以用于执行零样本和少样本分类。这意味着在不更新训练步骤中的模型权重的情况下,模型可以对输入数据最适合的类别做出有用的预测。

举个例子,假设我们有客户评论,并且想要了解他们的情感,即他们是积极的还是消极的。一个客户评论可能看起来像这样:

我对这款新智能手机非常满意。显示屏非常出色,电池续航时间长达数天。我唯一的抱怨是相机,它可以更好。总的来说,我强烈推荐这款产品。

得益于理解文本的能力,LLM应该完全能够将这篇评论分类为正面或负面。借助skorch和预训练的Hugging Face模型,我们可以以方便的方式进行这种类型的预测。让我们看看这在代码中是如何实现的:

from skorch.llm import ZeroShotClassifier

clf = ZeroShotClassifier('bigscience/bloomz-1b1')
clf.fit(X=None, y=['positive', 'negative'])

review = """I'm very happy with this new smartphone. The display is excellent
and the battery lasts for days. My only complaint is the camera, which could
be better. Overall, I can highly recommend this product."""

clf.predict([review])  # returns 'positive'
clf.predict_proba([review])  # returns array([[0.05547452, 0.94452548]])

让我们一步步来解析。首先,要运行这个,我们需要在我们的环境中安装Hugging Face transformers库。为此,请运行:

python -m pip install transformers

然后,我们导入了skorch类 ZeroShotClassifier。这个类负责所有繁重的工作,比如加载LLMs和生成预测。正如skorch所期望的那样,它与scikit-learn兼容,因此你可以将其用于网格搜索等(稍后会详细介绍)。

如你所见,我们通过传递参数'bigscience/bloomz-1b1'来初始化模型。这是什么意思呢?这个字符串是托管在Hugging Face上的大型语言模型的名称。它被称为“bloomz”,正如其名所示,它是10亿参数的变体(按照今天的标准,它是一个“小型”大型语言模型)。你可以浏览Hugging Face模型页面来找到更多可能适合任务的模型。

注意

可以通过直接初始化ZeroShotClassifier来使用模型和分词器,如下所示: ZeroShotClassifier(model=model, tokenizer=tokenizer)。在这种情况下,模型和分词器不会从Hugging Face下载。如果你想使用自己的模型,这可能会很有用,只要它们与Hugging Face的transformers兼容即可。特别是,它们必须实现.generate方法。

初始化模型后,我们将对其进行拟合。在零样本学习的背景下,“拟合”是什么意思?实际上,并没有拟合。幕后发生的唯一事情是skorch准备了一些东西,比如下载模型和分词器。这就是为什么我们传递X=None——我们实际上不需要任何训练数据来进行零样本学习。然而,我们需要传递y=['positive', 'negative'],因为这些信息将被模型用来确定它允许预测哪些标签。

然后,我们以上面的评论为例,并将其传递给.predict方法。正如预期的那样,模型预测为'positive',这确实是最符合评论的情感。

当调用.predict_proba时,我们得到0.055和0.945。第一个值 对应于“负面”,第二个对应于“正面”。这些结果的顺序 由标签的字母顺序决定,如果不确定,请检查 clf.classes_。所以这意味着模型预测标签为正面的概率为94.5%。

当然,情感分析并不是你唯一可以用LLMs做的事情。 也许你想知道评论是否提到了你在电子商务网站上的购物体验。 或者你想识别所有提到电池寿命的手机评论?这些信息可能是更大的机器学习管道的一部分(例如,根据评论帮助客户找到电池寿命好的手机)。 几乎没有什么是不可能的。

提示

我们尚未提及的一个重要方面是提示。如您所知,为了从LLM获得最佳结果,提示必须精心设计。网上有很多关于如何最好地提示LLM的信息,因此我们不会在这里重复。但使用ZeroShotClassifier的好处是,只需几行代码即可更改提示,并查看是否改进了结果。

以预测客户评论情感为例,假设我们有一个(小型)手动标记的数据集 Xy。现在假设我们想测试两个提示中哪一个能带来更好的准确性。这是我们可以实现的方法:

from sklearn.metrics import accuracy_score

X, y = ...  # your data

# first the default prompt
clf_default = ZeroShotClassifier('bigscience/bloomz-1b1')
clf_default.fit(X=None, y=['negative', 'positive'])
accuracy_default = accuracy_score(y, clf_default.predict(X))

# now a custom prompt
my_prompt = """Your job is to analyze the sentiment of customer reviews.

The available sentiments are: {labels}

The customer review is:

```
{text}
```

Your response:"""

clf_my_prompt = ZeroShotClassifier('bigscience/bloomz-1b1', prompt=my_prompt)
clf_my_prompt.fit(X=None, y=['negative', 'positive'])
accuracy_my_prompt = accuracy_score(y, clf_my_prompt.predict(X))

在这个例子中,我们检查skorch提供的默认提示,或者我们自定义的提示是否能够带来更好的准确性。对于我们自己的提示,我们必须注意包含两个占位符,{labels}{text}。Labels是放置可能标签的地方,在这个例子中是“negative”和“positive”。Text占位符用于从X中获取的输入。还要注意,我们使用“```”来分隔文本输入。虽然这不是绝对必要的,但通常是一个好主意,让LLM知道文本的哪一部分属于评论,哪一部分属于指令。尝试使用不同的分隔符进行实验。

网格搜索LLM参数

如果我们有很多提示和指标,以上述方式调查最佳提示可能会变得相当繁琐。此外,可能还有其他我们想要检查的超参数。这就是sklearn.model_selection.GridSearchCV登场的地方。由于ZeroShotClassifier与sklearn兼容,我们可以直接将其插入网格搜索中,让sklearn为我们完成繁琐的工作。让我们看看这在实践中是如何运作的:

from sklearn.model_selection import GridSearchCV
from skorch.llm import DEFAULT_PROMPT_ZERO_SHOT

params = {
    'model_name': ['bigscience/bloomz-1b1', 'gpt2', 'tiiuae/falcon-7b-instruct'],
    'prompt': [DEFAULT_PROMPT_ZERO_SHOT, my_prompt],
}
metrics = ['accuracy', 'neg_log_loss']
search = GridSearchCV(clf, param_grid=params, cv=2, scoring=metrics, refit=False)
search.fit(X, y)
print(search.cv_results_)

在这个例子中,我们对三种不同的LLM、两种不同的提示进行了网格搜索,并计算了两个分数。

请注意,使用LLMs进行预测可能会相当慢,这取决于您的可用硬件,因此这种网格搜索可能需要相当长的时间。如果您有一个具有足够内存的CUDA兼容GPU,建议将device='cuda'传递给ZeroShotClassifier

少样本分类

正如研究所显示的,向LLM提供一些关于如何执行其任务的示例可以显著提高性能。这种技术被称为少样本学习,我们在skorch中也支持这一点。它的工作原理几乎与零样本学习相同,但有一些显著的差异,我们稍后会解释。首先,让我们看一个代码示例:

from skorch.llm import FewShotClassifier

X_train, y_train, X_test, y_test = ...  # your data
clf = FewShotClassifier('bigscience/bloomz-1b1', max_samples=5)
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
accuracy_score(y_test, y_pred)

这个例子几乎与我们之前看到的相同。我们需要导入 FewShotClassifier 并提供一个LLM名称。我们 还在这里设置了 max_samples 参数。这表示我们 想要用于少样本学习的样本数量。这些样本稍后将被添加到提示中。

与零样本学习相比,在fit调用期间,我们传递一些实际数据。这是因为从传递的X_trainy_train中,将选择5个样本来通过示例增强提示。skorch将尝试选择示例,以便尽可能使所有标签至少被表示一次。

由于示例是从数据中选取的,并且它们的标签显示给LLM,因此在这种情况下,拆分训练和测试数据非常重要,这是我们在使用零样本学习时不需要担心的事情。当然,如果您稍后想运行网格搜索,sklearn会自动为您完成此操作。

用于增强提示的样本数量由max_samples参数控制。默认情况下,该值为5,即使用来自X_trainy_train的5个样本。未使用的样本实际上不会影响结果。因此,您应尽可能保持X_trainy_train的规模较小。这为您的验证/测试数据留下了更多的样本。

其余部分与我们之前看到的没有什么不同。我们可以调用 .predict.predict_proba 并根据这些预测计算分数。

在测试不同的提示时,方法与零样本分类几乎相同。一个区别是除了{labels}{text}的占位符外,还应该有一个{examples}的占位符。这是放置少量示例的地方。

少样本学习的一个大缺点是,由于提示被示例增强,它变得更长。这不仅会导致预测时间变慢,还使得提示长度更有可能超过LLM的上下文大小窗口。如果发生这种情况,我们将从Hugging Face transformers收到警告。如果有疑问,只需在max_samples参数上运行网格搜索,您将看到它如何影响分数和推理时间。

检测LLMs的问题

LLMs 有点像一个黑盒子,因此可能很难检测到潜在的问题。 skorch 提供了一些选项,可以在第一时间帮助注意到问题。

一般来说,如果模型的表现不如预期,你应该能在指标中看到这一点。在上面的例子中,如果提示选择不当或者LLM无法胜任任务,我们预计准确率会很低。这应该是出现问题的第一个迹象。

为了更深入地挖掘,了解模型分配给标签的概率是否较低可能是有用的。乍一看,这并不容易检测。如果我们调用predict_proba,每个标签的总概率总是会加起来等于1,这是概率的预期结果。然而,如果我们用参数probas_sum_to_1=False初始化分类器,我们将收到未归一化的概率。

例如,假设我们已经设置了此选项,让我们假设“负面”和“正面”的返回概率分别为0.1和0.2。这意味着LLM为这两个标签分配了0.7的概率。如果我们观察到这一点,这是一个强烈的指示,表明提示对于这个特定的LLM效果不佳,因为它试图将生成的文本引导到错误的方向。考虑调整提示,选择不同的LLM,或者如果尚未进行,可以进行少量学习。

skorch 提供了更多选项来检测此类问题。将参数 error_low_prob 设置为 "warn",当(未归一化的)概率过低时,您将收到警告。将参数设置为 raise 则会引发错误。

如果你发现模型只是偶尔给标签分配低概率,你可能想要设置error_low_prob='none'。在这种情况下,skorch不会对低概率发出警告,但对于每个低概率的样本,.predict实际上会返回None而不是最高概率的标签(.predict_proba不受影响)。你可以稍后决定如何处理这些特定的预测。

如果您使用了这些选项中的任何一个,您还应该更改 threshold_low_prob。这是一个浮点数,表示实际上被认为是“低”的概率。对于上面的情感分析示例,如果我们将此值设置为 0.5,这意味着如果“负面”和“正面”的概率之和小于0.5,则认为概率较低。

你可能会发现的一个常见问题是,模型总是返回一个非常低的概率。在这种情况下,我们如何取得一些进展呢?一个提示是检查模型对于给定提示实际会生成什么。这将有助于弄清楚LLM试图实现的目标,这可能会引导我们找到一个更好的提示。生成输出并不困难,因为skorch只是使用Hugging Face transformers模型,所以我们可以使用它们来生成没有任何约束的序列。以下是一个代码片段:

clf = ZeroShotClassifier(..., probas_sum_to_1=False)
clf.fit(X, y)
y_proba = clf.predict_proba(X)
# we notice that y_proba values are quite low, let's check what the LLM tries
# to do:
prompt = clf.get_prompt(X[0])  # get prompt for 1st sample from X
inputs = clf.tokenizer_(prompt, return_tensors='pt').to(clf.device_)
# adjust the min_new_tokens argument as needed
output = clf.model_.generate(**inputs, min_new_tokens=20)
generation = clf.tokenizer_.decode(output[0])
print(generation)

现在我们可以看到,如果我们不强制LLM预测其中一个标签,它实际上试图生成什么。这通常可以帮助我们更好地理解潜在的问题。

这有助于识别的一些问题示例:

  • 如果模型没有返回标签,而是生成了新的示例输入,它可能对预期响应的结构感到困惑;仔细重新措辞提示或使用少量样本学习可能会有所帮助。

  • 如果模型尝试在标签前插入一个多余的新行/空格,请将其添加到您的提示中。如果模型仍然坚持,请更改标签以包含该新行/空格,例如从“positive”更改为“ positive”。

  • 如果模型没有产生任何输出,请检查clf.model_.max_length是否设置得太低。希望这些提示能帮助解决最常见的问题。

优势

使用skorch进行零样本和小样本分类有哪些优势?

  • 处理少量甚至没有标记样本的情况:当没有足够的标记数据时,监督式机器学习方法不是一个选择。使用LLM可能为您的用例提供一个足够好的解决方案。

  • 强制LLM输出其中一个标签:通过使用 ZeroShotClassifierFewShotClassifier,你可以确保LLM 只会预测所需的标签之一。通常,在使用LLMs时,这可能相当棘手——无论提示语如何精心设计, 都无法保证LLM不会返回不期望的输出。有了skorch,你就不必担心这个问题了。

  • 返回概率:在使用LLMs生成文本时,通常只会得到生成的文本作为输出。但通常,我们想知道相关的概率:模型是99%确定标签是“正面”还是只有51%?skorch提供了.predict_proba方法,它将返回这些概率给你。

  • 缓存:skorch 在底层执行一些缓存。这可以加快预测时间,特别是当标签很长且共享一个共同的前缀时。

  • Scikit-learn 兼容性:感谢 skorch,分类器与 sklearn 兼容。你可以像往常一样调用 fitpredictpredict_proba。你可以运行网格搜索以确定最佳的 LLM 和提示。你可以将分类器作为现有 sklearn 模型的直接替代品使用。或者你可以从零/少样本分类器开始原型设计,然后在有更多标记数据可用时,将其替换为 sklearn 模型,而无需更改任何进一步的代码。

  • 一切都在本地运行:一旦模型和分词器被下载,所有操作都在您的机器上本地运行。不会向任何API提供商(如OpenAI)发送数据。如果您处理敏感数据或公司数据,您不必担心将其泄露到外部世界。(提示:如果您实际上更喜欢使用OpenAI API,请查看scikit-llm

也就是说,在某些情况下,你不应该使用零样本和小样本分类与skorch:当你有足够的标记数据时,监督机器学习方法(如skorch.NeuralNetClassifier)很可能会表现得更好,需要更少的内存和计算资源,因此推理速度更快。如果你需要更开放的文本生成,比如抽象摘要而不是预测固定的标签列表,这也不是一个好的用例。另一个问题可能是可解释性——大型语言模型大多是一个黑箱,但其他一些机器学习方法更适合解释。

技术细节

问:ZeroShotClassifierFewShotClassifier 如何确保只生成给定的标签,例如“正面”和“负面”?通常,语言模型可以生成任何标记。

A: 在底层,我们拦截模型的预测(logits)并强制它们成为其中一个标签。这样,我们可以确保模型永远不会预测我们不期望的内容。这是通过与Hugging Face transformers的集成实现的。

问:ZeroShotClassifierFewShotClassifier 是如何推导出概率的? 通常,语言模型只返回文本。

A: 我们不使用生成的文本,而是直接检查语言模型返回的logits。例如,假设标签“positive”表示为两个标记,[123, 456]。然后我们首先检查语言模型在给定输入提示的情况下分配给标记123的logit,然后强制模型预测123并检查分配给标记456的logit。然后将logits转换为概率并聚合。

更多示例

要查看使用 ZeroShotClassifierFewShotClassifier 的完整工作示例,请查看 这个关于使用LLMs进行分类的笔记本。 它包含了电影评论情感分析的用例,并将结果与其他方法的结果进行了比较。