文章目录
前言
这篇论文提出了一个名为FreeAL(Free Active Learning)的框架,旨在利用大型语言模型(LLMs)的知识,通过协作学习的方式,在无需人工标注的情况下提升小语言模型(SLMs)的零样本(zero-shot)学习性能。FreeAL的核心思想是让LLM充当主动标注者,提供粗略的知识,而SLM则作为学生,通过过滤高质量的上下文样本来反馈给LLM,以便进行后续的标签精细化。这种方法在多个基准数据集上的实验表明,FreeAL能够在不依赖人工监督的情况下,显著提高SLM和LLM的性能,甚至在某些情况下接近监督学习的性能。
一、核心内容
-
背景:传统的机器学习模型训练需要大量精确标注的数据,这在某些领域(如医疗诊断和工业应用)中可能非常耗时且需要专业知识。为了减轻标注负担,研究者们提出了半监督学习、带标签噪声学习等方法。然而,这些方法仍然需要人工参与。大型语言模型(LLMs)虽然在零样本(zero-shot)学习方面表现出色,但在特定任务上的泛化能力有限。
-
FreeAL框架:FreeAL框架通过LLM和SLM之间的交互来实现知识蒸馏和过滤。LLM作为主动标注器,提供粗粒度的知识,而SLM作为学生,从LLM的标注中筛选出高质量的样本,反馈给LLM进行后续的标签精炼。
-
协作训练过程:
- LLM主动标注:LLM利用其强大的上下文学习能力,为未标注的训练数据集分配弱标签。在初始轮次,LLM通过自生成的示例进行标注;在后续轮次,LLM使用SLM筛选出的高质量示例进行标注。
- SLM知识蒸馏:SLM接收LLM提供的弱标签进行训练,并使用稳健的自训练策略来最大化从噪声标注中提取任务特定知识。SLM还负责构建高质量的示例池(Ddemo),用于LLM的后续标注。
-
实验结果:在多个基准数据集上的广泛实验表明,FreeAL显著提高了SLM和LLM的零样本性能,甚至在某些情况下接近监督学习的性能,同时完全消除了人工标注成本。
-
主要贡献:
- 提出了FreeAL框架,这是在LLM时代首次尝试无需人工监督的主动学习。
- 设计了一个协作学习框架,使LLM作为主动标注器,SLM作为弱过滤器,从LLM中交互式地提取任务相关知识。
- FreeAL在无监督学习性能上取得了显著提升,证明了在LLM时代实现无需人工主动标注的可行性。
-
局限性:尽管FreeAL在无人工努力的情况下有效,但仍有改进空间。例如,对于一些极具挑战性的领域,当前的LLM可能无法提供合格的初始标注。此外,FreeAL完全放弃了人工努力,而在实际场景中可能存在不同程度的人工支持。
-
伦理声明: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