R.E.D.算法:革新文本分类的半监督学习新范式

随着大型语言模型(LLMs)在解决问题方面的应用进入新时代,只有少数问题仍然存在不尽如人意的解决方案。大多数分类问题(在概念验证层面)可以通过良好的提示工程技术和自适应的上下文学习(ICL)示例,利用LLMs以70-90%的精确度/F1分数来解决。

当您希望持续实现高于此水平的性能时——当提示工程不再足够时,会发生什么?

R.E.D.算法:革新文本分类的半监督学习新范式_第1张图片

分类难题文本分类是监督学习中最古老且最易理解的示例之一。鉴于这一前提,构建能够处理大量输入类别的稳健且性能良好的分类器应该并不难,对吧?

嗯。实际上很难。

这实际上与算法通常需要在其下工作的“约束”有很大关系:

  • 每个类别的训练数据量较少

  • 高分类准确率(随着添加更多类别而急剧下降)

  • 可能向现有类别子集中添加新类别

  • 快速训练/推理

  • 成本效益

  • (潜在的)大量训练类别

  • (潜在的)由于数据漂移等原因,某些类别需要不断重新训练

在这些条件下,您是否尝试过构建超过几十个类别的分类器?(我的意思是,即使是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%以上的同意率)。

我相信这是应用机器学习中的一项重大成就,并且在成本、速度、规模和适应性方面的生产级期望中具有实际用途。技术报告将在今年晚些时候发布,重点介绍相关代码示例以及用于实现给定结果的实验设置。

你可能感兴趣的:(算法,r语言,分类,人工智能,学习)