【Towards Human-Free Active Learning in the Era of Large Language Models--FreeAL】



前言

这篇论文提出了一个名为FreeAL(Free Active Learning)的框架,旨在利用大型语言模型(LLMs)的知识,通过协作学习的方式,在无需人工标注的情况下提升小语言模型(SLMs)的零样本(zero-shot)学习性能。FreeAL的核心思想是让LLM充当主动标注者,提供粗略的知识,而SLM则作为学生,通过过滤高质量的上下文样本来反馈给LLM,以便进行后续的标签精细化。这种方法在多个基准数据集上的实验表明,FreeAL能够在不依赖人工监督的情况下,显著提高SLM和LLM的性能,甚至在某些情况下接近监督学习的性能。


一、核心内容


  1. 背景:传统的机器学习模型训练需要大量精确标注的数据,这在某些领域(如医疗诊断和工业应用)中可能非常耗时且需要专业知识。为了减轻标注负担,研究者们提出了半监督学习、带标签噪声学习等方法。然而,这些方法仍然需要人工参与。大型语言模型(LLMs)虽然在零样本(zero-shot)学习方面表现出色,但在特定任务上的泛化能力有限。

  2. FreeAL框架:FreeAL框架通过LLM和SLM之间的交互来实现知识蒸馏和过滤。LLM作为主动标注器,提供粗粒度的知识,而SLM作为学生,从LLM的标注中筛选出高质量的样本,反馈给LLM进行后续的标签精炼。

  3. 协作训练过程

    • LLM主动标注:LLM利用其强大的上下文学习能力,为未标注的训练数据集分配弱标签。在初始轮次,LLM通过自生成的示例进行标注;在后续轮次,LLM使用SLM筛选出的高质量示例进行标注。
    • SLM知识蒸馏:SLM接收LLM提供的弱标签进行训练,并使用稳健的自训练策略来最大化从噪声标注中提取任务特定知识。SLM还负责构建高质量的示例池(Ddemo),用于LLM的后续标注。
  4. 实验结果:在多个基准数据集上的广泛实验表明,FreeAL显著提高了SLM和LLM的零样本性能,甚至在某些情况下接近监督学习的性能,同时完全消除了人工标注成本。

  5. 主要贡献

    • 提出了FreeAL框架,这是在LLM时代首次尝试无需人工监督的主动学习。
    • 设计了一个协作学习框架,使LLM作为主动标注器,SLM作为弱过滤器,从LLM中交互式地提取任务相关知识。
    • FreeAL在无监督学习性能上取得了显著提升,证明了在LLM时代实现无需人工主动标注的可行性。
  6. 局限性:尽管FreeAL在无人工努力的情况下有效,但仍有改进空间。例如,对于一些极具挑战性的领域,当前的LLM可能无法提供合格的初始标注。此外,FreeAL完全放弃了人工努力,而在实际场景中可能存在不同程度的人工支持。

  7. 伦理声明:FreeAL的预测和LLM的自生成示例可能包含偏见和不公平性。建议用户在使用FreeAL时,首先使用减少和纠正偏见的技术,以提高整体的公平性和伦理标准。

这篇论文的核心在于提出了一种新的协作学习框架,通过LLM和SLM之间的交互,实现了在无需人工标注的情况下提高模型的泛化性能。这种方法在多个NLP任务上展示了其有效性,并为未来的主动学习算法开发提供了新的方向。

二、代码解析

1.main 次要解析

在这里插入图片描述

  
all_response = [
{'id': 'chatcmpl-123', 'object': 'chat.completion', 'created': 1234567890, 'choices': [ {'message': {'role': 'assistant', 'content': '正面'}}]}, 
{'id': 'chatcmpl-456', 'object': 'chat.completion', 'created': 1234567891, 'choices': [ {'message': {'role': 'assistant', 'content': '负面'}}]}] 

all_sentences = [ "这部电影真的很棒,剧情紧凑,演员表演出色。",
									"电影的剧情有点平淡,不太吸引人。"] 

batch_messages = [ [ {"role": "system", "content": "你是一个助手,负责对电影评论进行情感分类..."}, {"role": "user", "content": "给定一条电影评论:'这部电影真的很棒,剧情紧凑...'"} # ... 可能包含更多上下文信息 ... 
										 {"role": "user", "content": "你觉得这条评论的情感倾向是正面还是负面?请直接回答。"} ], [ {"role": "system", "content": "你是一个助手,负责对电影评论进行情感分类..."}, {"role": "user", "content": "给定一条电影评论:'电影的剧情有点平淡,不太吸引人。'"} # ... 可能包含更多上下文信息 ... {"role": "user", "content": "你觉得这条评论的情感倾向是正面还是负面?请直接回答。"} ]] 

batch_sentences = [ "这部电影真的很棒,剧情紧凑,演员表演出色。", 
										"电影的剧情有点平淡,不太吸引人。"] 

batch_index = [0, 1] # 假设这些是原始数据集中的评论索引

2. mytrainer 主要解析

在这里插入图片描述

1. 导入必要的库和模块

from transformers import ...
from datasets import ...
...

这部分代码导入了训练过程中所需的所有外部库和模块,包括Hugging Face的Transformers库、自定义的数据处理和训练参数模块、以及PyTorch等。

2. 定义线性上升函数

def linear_rampup(current, rampup_length):
    ...

这个函数用于实现线性上升的学习率调整策略,通常在训练的前几个epoch中逐渐增加学习率。

3. 自定义训练器类

class MyTrainer(Trainer):
    ...

这个类继承自Trainer,用于自定义训练循环。它包含了训练过程中的所有步骤,如数据加载、模型训练、损失计算、优化器更新等。

4. 训练循环

这是训练循环的核心部分,它负责执行整个训练过程。包括设置训练参数、加载数据、执行训练步骤、保存模型和日志等
def _inner_training_loop(self, ...):
    ...

5. 损失计算

这个函数计算模型的损失。它包括交叉熵损失、混合策略损失和一致性正则化损失。这些损失在训练过程中被用来指导模型的学习。
def compute_loss(self, model, inputs, return_outputs=False):
    ...

6. 训练步骤

这个函数执行单个训练步骤,包括前向传播、损失计算、反向传播和梯度更新。它是训练循环中每次迭代的核心。
def training_step(self, model: nn.Module, inputs: Dict[str, Union[torch.Tensor, Any]]) -> torch.Tensor:
    ...

7. 其他辅助函数

代码中还包含了一些辅助函数,如_prepare_inputs用于准备输入数据,_load_rng_state用于加载随机数生成器的状态,以及一些用于日志记录和模型保存的函数。


# Start of sample selection and demonstration pool construction
self.temp_u = args.temp_u
length = len(train_dataloader.dataset)
chosen_list = torch.zeros((length)).cuda()
chosen_list_sel = torch.zeros((length)).cuda()
self.num_labels = args.num_labels

if epoch > 0:
    t_model = copy.deepcopy(model).to(args.device)
    t_model.eval()
    rho_sel = 0.2

    num_labels = args.num_labels
    targets_all = torch.zeros((length), dtype=torch.long).cuda()
    outputs_all = torch.zeros((length, num_labels)).cuda()
    loss_all = torch.zeros((length)).cuda()
    embeddings_all = torch.zeros((length, args.embedding_dim))
    new_dataloader = train_dataloader

    # Calculate the losses for each training sample
    for step, inputs in enumerate(new_dataloader):
        # ... (省略了前向传播和损失计算的代码) ...

    # GMM selection for robust self-training of SLM
    loss_all = ((loss_all - loss_all[valid_idx_all].min()) / (loss_all[valid_idx_all].max() - loss_all[valid_idx_all].min())).detach().cpu().reshape(-1, 1)
    gmm = GaussianMixture(n_components=2, max_iter=10, tol=1e-2, reg_covar=5e-4)
    gmm.fit(loss_all_tmp)
    prob = gmm.predict_proba(loss_all_tmp)
    prob = prob[:, gmm.means_.argmin()]
    chosen_idx_all_gmm = np.where(prob > 0.7)[0]
    chosen_list[torch.where(valid_idx_all)[0][chosen_idx_all_gmm]] = 1

    # Class-wise demonstration pool construction as feedback to LLM
    # ... (省略了示范池构建的代码) ...

    # demonstration retrieval for LLMs: similarity-based demonstration retrieval
    # ... (省略了示范池检索的代码) ...

    # representation and index of samples to be demonstrations
    embedding_all_representative = torch.cat([emb.reshape(1, -1) for emb in embedding_all_representative])
    idx_all_representative = torch.cat(idx_all_representative)

    # demonstration retrieval for LLMs: similarity-based demonstration retrieval
    # ... (省略了相似度计算和示范样本选择的代码) ...

    # saving the feedbacks
    if args.learning_setting == 'transductive' and epoch == 1:
        import pickle
        # clean sample index
        with open("./feedback/right_list_mr.pkl", "wb") as f:
            pickle.dump(chosen_idx_sel.tolist(), f)
        # demonstration retrieval
        with open("./feedback/demo_index_mr.pkl", "wb") as f:
            pickle.dump(chosen_top_indices.tolist(), f)
        # pseudo-labels for all samples
        with open("./feedback/pred_label_mr.pkl", "wb") as f:
            pickle.dump(pred_label_all, f)

if epoch > args.warmup:
    self.chosen_list = chosen_list.to(args.device)
else:
    # warm-up phase
    self.chosen_list = torch.ones_like(chosen_list).to(args.device)

8. 训练结束处理

在训练结束后,如果设置了`load_best_model_at_end`,脚本会加载在训练过程中表现最好的模型。
if args.load_best_model_at_end and self.state.best_model_checkpoint is not None:
    ...

9. 清理和资源释放

在训练完成后,如果使用了`_past`属性,脚本会清理相关资源。

if args.past_index and hasattr(self, "_past"):
    ...

三,问题解析


1,LLM和SLM 如何交互举例:

以下是处理初始标注和同步损失选取数据的步骤:

自生成示范:

  • 在FreeAL的初始轮次,LLM接收一些未标注样本和任务描述性指令,然后生成新的、带有标签的示例。这些示例模拟了人类在没有具体标签的情况下,如何根据文本内容和任务背景生成类似标签的样本。

  • 生成的示例(Dgen)构成了初始的示范池(Ddemo),用于后续的ICL过程。 ICL过程:

  • 使用自生成的示范池,LLM通过ICL为训练数据集提供初始标注。这些标注是基于LLM的预测,可能包含噪声。 SLM的稳健自训练:

  • SLM接收LLM提供的弱标注进行训练。SLM通过自训练策略(如一致性正则化和GMM选择)来筛选出高质量的样本,并构建一个更精确的示范池。
    重新经过LLM:

在后续的轮次中,SLM筛选出的高质量示范池(Ddemo)被反馈给LLM。LLM使用这些高质量的示范池进行重新标注,以改进其预测。这个过程类似于一个迭代过程,LLM和SLM相互协作,逐步提高标注的质量和模型的性能。

代码举例:

import torch
import numpy as np
from transformers import AutoModelForSequenceClassification, AutoTokenizer
from sklearn.mixture import GaussianMixture
from sklearn.cluster import KMeans

# 假设我们有一个未标注的数据集,每个样本包含文本和对应的标签
unlabeled_dataset = [
    {"text": "This is a positive review", "label": None},
    {"text": "This is a negative review", "label": None},
    # ... 更多样本
]

# 初始化SLM和LLM
slm = AutoModelForSequenceClassification.from_pretrained("roberta-base")
llm = AutoModelForSequenceClassification.from_pretrained("gpt3-large")

# 初始化分词器
tokenizer = AutoTokenizer.from_pretrained("roberta-base")

# 对数据集进行编码
encoded_dataset = tokenizer(unlabeled_dataset, padding=True, truncation=True, return_tensors="pt")

# SLM进行自训练,筛选出高质量的样本
# 这里我们简化为直接使用交叉熵损失来筛选
slm.train()
for step, batch in enumerate(encoded_dataset):
    with torch.no_grad():
        outputs = slm(**batch)
        logits = outputs.logits
        # 假设我们有一个简单的阈值来筛选样本
        high_confidence_indices = (logits.max(dim=1)[0] > 0.9).nonzero(as_tuple=False).squeeze(1)

# 使用高斯混合模型(GMM)进一步筛选样本
gmm = GaussianMixture(n_components=2)
gmm.fit(logits[high_confidence_indices, :].numpy())
selected_indices = gmm.predict_proba(logits[high_confidence_indices, :].numpy()).argmax(axis=1)

# 构建示范池(Ddemo)
selected_samples = [unlabeled_dataset[i] for i in selected_indices]

# 将示范池反馈给LLM进行重新标注
# 这里我们简化为直接使用LLM对示范池中的样本进行预测
llm.eval()
for sample in selected_samples:
    encoded_sample = tokenizer(sample["text"], padding=True, truncation=True, return_tensors="pt")
    with torch.no_grad():
        outputs = llm(**encoded_sample)
        logits = outputs.logits
        # 获取LLM的预测
        predicted_label = logits.argmax(dim=1).item()

    # 更新样本的标签
    sample["label"] = predicted_label

# 输出更新后的样本
for sample in selected_samples:
    print(f"Text: {sample['text']}, Predicted Label: {sample['label']}")

在没有人工标注的情况下,使用大型语言模型(LLM)的预测作为伪标签来训练SLM。在通过SLM 的原始文本+ 伪标签构成了示范池(Ddemo),用于后面的LLM 重新更新数据进行SLM的训练优化。

当然可能有人会问伪标签怎么处理

  • 设计提示模板:首先,你需要设计一个提示模板,这个模板能够将原始文本和SLM的预测结果结合起来,以便LLM能够理解它们之间的关系。提示模板通常包括任务描述、示例(如果可用)以及当前文本和伪标签。

  • 构建输入:对于每个待预测的文本,将原始文本和SLM的预测结果嵌入到提示模板中,形成一个完整的输入。这个输入将被用来引导LLM的预测。

  • LLM的ICL训练:在LLM的训练过程中,使用这些构建好的输入进行训练。LLM会尝试从提示中的信息学习,以改进其对当前文本的预测。

# 假设我们有一个函数来计算SLM预测的置信度分数
def calculate_confidence_score(prediction, text):
    # ... 实现计算置信度分数的逻辑 ...
    return confidence_score

# 在构建提示时,包含置信度分数
prompt = prompt_template.format(
    text=text_to_predict,
    examples=" ".join(slm_predictions),
    label=slm_predictions[0],
    confidence=calculate_confidence_score(slm_predictions[0], text_to_predict)
)

# 在LLM的预测过程中,考虑置信度分数
outputs = llm(**encoded_prompt)
logits = outputs.logits
predicted_label = logits.argmax(dim=1).item()
confidence = outputs.confidence_scores[predicted_label]

# 使用置信度阈值进行后处理
confidence_threshold = 0.8
if confidence < confidence_threshold:
    # 处理低置信度的预测,例如,选择最可能的标签或请求人工标注

四,相关链接

Github Source Code

https://github.com/Justherozen/FreeAL/

FreeAl papers Cool

https://aclanthology.org/2023.emnlp-main.896.pdf

  • 21
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值