随着大型语言模型(LLMs)在解决问题方面的应用进入新时代,只有少数问题仍然存在不尽如人意的解决方案。大多数分类问题(在概念验证层面)可以通过良好的提示工程技术和自适应的上下文学习(ICL)示例,利用LLMs以70-90%的精确度/F1分数来解决。
当您希望持续实现高于此水平的性能时——当提示工程不再足够时,会发生什么?
分类难题文本分类是监督学习中最古老且最易理解的示例之一。鉴于这一前提,构建能够处理大量输入类别的稳健且性能良好的分类器应该并不难,对吧?
嗯。实际上很难。
这实际上与算法通常需要在其下工作的“约束”有很大关系:
每个类别的训练数据量较少
高分类准确率(随着添加更多类别而急剧下降)
可能向现有类别子集中添加新类别
快速训练/推理
成本效益
(潜在的)大量训练类别
(潜在的)由于数据漂移等原因,某些类别需要不断重新训练
在这些条件下,您是否尝试过构建超过几十个类别的分类器?(我的意思是,即使是GPT,可能也能在只有几个样本的情况下,处理多达约30个文本类别。)
如果您选择GPT路线——如果您有超过几十个类别或大量数据需要分类,您将不得不深入挖掘您的钱包,用于系统提示、用户提示、少样本示例令牌等,以分类一个样本。即使您运行异步查询,您也需要与API的吞吐量达成妥协。
在应用机器学习中,这些问题通常很难解决,因为它们不完全满足监督学习的要求,或者通过LLM运行不够便宜/快速。R.E.D.算法正是针对这一痛点:半监督学习,当每个类别的训练数据不足以构建(准)传统分类器时。
R.E.D.算法R.E.D:递归专家委托是一种新颖的框架,改变了我们处理文本分类的方式。这是一种应用机器学习范式——即,与现有架构没有根本不同,但它是一系列最有效构建实用且可扩展解决方案的想法的集锦。
在本文中,我们将通过一个具体示例进行探讨,其中我们有大量文本类别(100-1000个),每个类别只有少量样本(30-100个),并且有大量样本需要分类(10,000-100,000个)。我们通过R.E.D.将其视为半监督学习问题。
让我们深入探讨。
工作原理R.E.D.的简单表示 与让单个分类器在大量类别之间进行分类不同,R.E.D.智能地:
分而治之——将标签空间(大量输入标签)划分为多个标签子集。这是一种贪婪的标签子集形成方法。
高效学习——为每个子集训练专门分类器。此步骤侧重于构建一个对噪声进行过采样的分类器,其中噪声被智能地建模为来自其他子集的数据。
委托给专家——使用LLMs作为专家预言机,仅用于特定标签验证和校正,类似于拥有一组领域专家。使用LLM作为代理,它从经验上“模仿”人类专家如何验证输出。
递归重新训练——使用从专家添加的新样本持续重新训练,直到没有更多样本可添加/信息增益达到饱和
其背后的直觉并不难理解:主动学习使用人类作为领域专家,持续“纠正”或“验证”ML模型的输出,并进行持续训练。当模型达到可接受的性能时,这一过程停止。我们直觉地重新品牌化这一过程,并进行了一些巧妙的创新,这些创新将在稍后的研究预印本中详细介绍。
让我们更深入地看看……
贪婪子集选择与最不相似元素当输入标签(类别)数量较多时,学习类别之间线性决策边界的复杂性增加。因此,随着类别数量的增加,分类器的质量下降。当分类器没有足够的样本进行学习时尤其如此——即每个训练类别只有少量样本。
这非常反映现实世界的情况,也是创建R.E.D.的主要动机。
在这些约束下提高分类器性能的一些方法:
限制分类器需要在其中分类的类别数量
使类别之间的决策边界更清晰,即训练分类器处理高度不相似的类别
贪婪子集选择正是这样做的——由于问题的范围是文本分类,我们形成训练标签的嵌入,通过UMAP降低其维度,然后从中形成S个子集。每个S子集都有n个训练标签作为元素。我们贪婪地选择训练标签,确保我们为子集选择的每个标签与子集中存在的其他标签最不相似:
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
def avg_embedding(candidate_embeddings):
return np.mean(candidate_embeddings, axis=0)
def get_least_similar_embedding(target_embedding, candidate_embeddings):
similarities = cosine_similarity(target_embedding, candidate_embeddings)
least_similar_index = np.argmin(similarities) # 使用argmin找到最小值的索引
least_similar_element = candidate_embeddings[least_similar_index]
return least_similar_element
def get_embedding_class(embedding, embedding_map):
reverse_embedding_map = {value: key for key, value in embedding_map.items()}
return reverse_embedding_map.get(embedding) # 使用.get()优雅地处理缺失键
def select_subsets(embeddings, n):
visited = {cls: False for cls in embeddings.keys()}
subsets = []
current_subset = []
while any(not visited[cls] for cls in visited):
for cls, average_embedding in embeddings.items():
if not current_subset:
current_subset.append(average_embedding)
visited[cls] = True
elif len(current_subset) >= n:
subsets.append(current_subset.copy())
current_subset = []
else:
subset_average = avg_embedding(current_subset)
remaining_embeddings = [emb for cls_, emb in embeddings.items() if not visited[cls_]]
if not remaining_embeddings:
break # 处理边缘情况
least_similar = get_least_similar_embedding(target_embedding=subset_average, candidate_embeddings=remaining_embeddings)
visited_class = get_embedding_class(least_similar, embeddings)
if visited_class is not None:
visited[visited_class] = True
current_subset.append(least_similar)
if current_subset: # 添加current_subset中的任何剩余元素
subsets.append(current_subset)
return subsets
这种贪婪子集采样的结果是将所有训练标签清晰地划分为子集,其中每个子集最多只有n个类别。与原本需要分类的S个类别相比,这本质上使得分类器的工作变得更容易!
噪声过采样的半监督分类在初始标签子集形成之后进行级联——即,此分类器仅在给定子集的类别之间进行分类。
想象一下:当您有少量训练数据时,您绝对无法创建一个有意义的评估集。您应该这样做吗?您如何知道您的分类器是否工作良好?
我们以稍微不同的方式处理这个问题——我们将半监督分类器的基本工作定义为样本的预防性分类。这意味着无论样本被分类为什么,它都将在后期“验证”和“纠正”:此分类器只需要识别需要验证的内容。
因此,我们创建了其数据处理方式的设计:
n+1个类别,其中最后一个类别是噪声
噪声:不属于当前分类器范围内的类别数据。噪声类别被过采样为此分类器标签数据平均大小的2倍
在噪声上进行过采样是一种伪安全措施,以确保属于另一个类别的相邻数据最有可能被预测为噪声,而不是滑过验证。
您如何检查此分类器是否工作良好——在我们的实验中,我们将其定义为分类器预测中“不确定”样本的数量。使用不确定性采样和信息增益原则,我们有效地能够判断分类器是否在“学习”,这作为分类性能的指针。除非预测的不确定样本数量出现拐点,或者新样本迭代添加的信息增量仅为delta,否则此分类器将不断重新训练。
通过LLM代理进行代理主动学习这是该方法的精髓——使用LLM作为人类验证者的代理。我们所说的人类验证者方法是主动标记。
让我们直观地理解主动标记:
使用ML模型在样本输入数据集上学习,对大量数据点进行预测
对于在数据点上给出的预测,主题专家(SME)评估预测的“有效性”
递归地,新的“纠正”样本作为训练数据添加到ML模型
ML模型持续学习/重新训练,并做出预测,直到SME对预测质量感到满意
为了使主动标记工作,对SME有一些期望:
当我们期望人类专家“验证”输出样本时,专家理解任务是什么
人类专家在决定新样本是否应属于标签L时,将使用判断来评估“其他”肯定属于标签L的内容
鉴于这些期望和直觉,我们可以使用LLM“模仿”这些:
让LLM“理解”每个标签的含义。这可以通过使用更大的模型来批判性地评估所有标签的{标签:映射到标签的数据}之间的关系来实现。在我们的实验中,这是使用自托管的32B变体DeepSeek完成的。
赋予LLM理解“为什么、什么和如何”的能力
不是预测正确的标签,而是利用LLM仅识别预测是“有效”还是“无效”(即,LLM只需回答二元查询)。
强化其他有效样本对于标签的看法,即,对于每个预防性预测的样本标签,在提示验证时动态获取其训练(保证有效)集中c个最接近的样本。
结果?一个成本效益高的框架,依赖快速、便宜的分类器进行预防性分类,并使用LLM通过(标签的含义+动态获取的与当前分类相似的训练样本)验证这些分类:
import math
def calculate_uncertainty(clf, sample):
predicted_probabilities = clf.predict_proba(sample.reshape(1, -1))[0] # 为predict_proba重塑样本
uncertainty = -sum(p * math.log(p, 2) for p in predicted_probabilities)
return uncertainty
def select_informative_samples(clf, data, k):
informative_samples = []
uncertainties = [calculate_uncertainty(clf, sample) for sample in data]
# 按不确定性降序排序数据
sorted_data = sorted(zip(data, uncertainties), key=lambda x: x[1], reverse=True)
# 获取前k个不确定性最高的样本
for sample, uncertainty in sorted_data[:k]:
informative_samples.append(sample)
return informative_samples
def proxy_label(clf, llm_judge, k, testing_data):
#llm_judge - 任何LLM,其系统提示调整为验证样本是否属于某个类别。预期输出是一个布尔值:True或False。True验证原始分类,False反驳它
predicted_classes = clf.predict(testing_data)
# 使用不确定性采样选择k个信息量最大的样本
informative_samples = select_informative_samples(clf, testing_data, k)
# 存储正确样本的列表
voted_data = []
# 使用LLM判断评估信息量最大的样本
for sample in informative_samples:
sample_index = testing_data.tolist().index(sample.tolist()) # 从testing_data.index(sample)更改,因为numpy数组类型问题
predicted_class = predicted_classes[sample_index]
# 检查LLM判断是否同意预测
if llm_judge(sample, predicted_class):
# 如果正确,将样本添加到投票数据中
voted_data.append(sample)
# 返回带有代理标签的正确样本列表
return voted_data
通过在受控参数下将有效样本(voted_data)提供给我们的分类器,我们实现了算法的“递归”部分:
递归专家委托:R.E.D.通过这样做,我们能够在受控的多类别数据集上实现接近人类专家的验证数字。实验证明,R.E.D.扩展到1,000个类别,同时保持几乎与人类专家相当的准确度(90%以上的同意率)。
我相信这是应用机器学习中的一项重大成就,并且在成本、速度、规模和适应性方面的生产级期望中具有实际用途。技术报告将在今年晚些时候发布,重点介绍相关代码示例以及用于实现给定结果的实验设置。