AGI 之 【Hugging Face】 的【零样本和少样本学习】之一 [构建标记任务] / [ 基线模型 ] 的简单整理
目录
AGI 之 【Hugging Face】 的【零样本和少样本学习】之一 [构建标记任务] / [ 基线模型 ] 的简单整理
一、简单介绍
二、零样本学习 (Zero-shot Learning) 和少样本学习 (Few-shot Learning)
1、零样本学习 (Zero-shot Learning)
2、少样本学习 (Few-shot Learning)
三、构建GitHub issue标记任务
1、获取数据
2、准备数据
3、创建训练集
4、创建训练切片
四、基线模型
附录
一、当前案例环境 pacakge 的 版本如下
一、简单介绍
AGI,即通用人工智能(Artificial General Intelligence),是一种具备人类智能水平的人工智能系统。它不仅能够执行特定的任务,而且能够理解、学习和应用知识于广泛的问题解决中,具有较高的自主性和适应性。AGI的能力包括但不限于自我学习、自我改进、自我调整,并能在没有人为干预的情况下解决各种复杂问题。
- AGI能做的事情非常广泛:
跨领域任务执行:AGI能够处理多领域的任务,不受限于特定应用场景。
自主学习与适应:AGI能够从经验中学习,并适应新环境和新情境。
创造性思考:AGI能够进行创新思维,提出新的解决方案。
社会交互:AGI能够与人类进行复杂的社会交互,理解情感和社会信号。
- 关于AGI的未来发展前景,它被认为是人工智能研究的最终目标之一,具有巨大的变革潜力:
技术创新:随着机器学习、神经网络等技术的进步,AGI的实现可能会越来越接近。
跨学科整合:实现AGI需要整合计算机科学、神经科学、心理学等多个学科的知识。
伦理和社会考量:AGI的发展需要考虑隐私、安全和就业等伦理和社会问题。
增强学习和自适应能力:未来的AGI系统可能利用先进的算法,从环境中学习并优化行为。
多模态交互:AGI将具备多种感知和交互方式,与人类和其他系统交互。
Hugging Face作为当前全球最受欢迎的开源机器学习社区和平台之一,在AGI时代扮演着重要角色。它提供了丰富的预训练模型和数据集资源,推动了机器学习领域的发展。Hugging Face的特点在于易用性和开放性,通过其Transformers库,为用户提供了方便的模型处理文本的方式。随着AI技术的发展,Hugging Face社区将继续发挥重要作用,推动AI技术的发展和应用,尤其是在多模态AI技术发展方面,Hugging Face社区将扩展其模型和数据集的多样性,包括图像、音频和视频等多模态数据。
- 在AGI时代,Hugging Face可能会通过以下方式发挥作用:
模型共享:作为模型共享的平台,Hugging Face将继续促进先进的AGI模型的共享和协作。
开源生态:Hugging Face的开源生态将有助于加速AGI技术的发展和创新。
工具和服务:提供丰富的工具和服务,支持开发者和研究者在AGI领域的研究和应用。
伦理和社会责任:Hugging Face注重AI伦理,将推动负责任的AGI模型开发和应用,确保技术进步同时符合伦理标准。
AGI作为未来人工智能的高级形态,具有广泛的应用前景,而Hugging Face作为开源社区,将在推动AGI的发展和应用中扮演关键角色。
(注意:以下代码运行,可能需要科学上网)
二、零样本学习 (Zero-shot Learning) 和少样本学习 (Few-shot Learning)
1、零样本学习 (Zero-shot Learning)
定义: 零样本学习是一种让模型能够在没有见过目标类别数据的情况下进行预测的技术。它主要依赖于预训练的语言模型和自然语言描述,利用模型在预训练期间学到的广泛知识来理解和推断新的任务。
实现方式:
基于语言模型的零样本分类: 使用预训练的语言模型,如 BERT、GPT-3,通过自然语言提示 (prompt) 进行分类。Hugging Face 提供了
zero-shot-classification
pipeline,使这一过程非常简单。from transformers import pipeline # 加载零样本分类 pipeline zero_shot_classifier = pipeline("zero-shot-classification") # 定义待分类的文本 text = "Hugging Face's library is so easy to use!" # 定义候选标签 labels = ["education", "politics", "technology"] # 进行零样本分类 result = zero_shot_classifier(text, candidate_labels=labels) print(result)
利用嵌入向量和距离度量: 通过计算文本嵌入向量之间的相似度来实现零样本分类。模型在预训练期间学到的嵌入空间使得相似类别的文本在向量空间中更接近。
自然语言推理 (NLI): 使用 NLI 模型,如 RoBERTa,对于每个候选标签,模型判断该标签是否是输入文本的合理推断。Hugging Face 提供了类似的模型,可以通过 NLI 的方式实现零样本学习。
from transformers import pipeline # 加载 NLI 模型 nli_model = pipeline("zero-shot-classification", model="facebook/bart-large-mnli") # 定义待分类的文本 text = "Hugging Face's library is so easy to use!" # 定义候选标签 labels = ["education", "politics", "technology"] # 进行零样本分类 result = nli_model(text, candidate_labels=labels) print(result)
2、少样本学习 (Few-shot Learning)
定义: 少样本学习是一种让模型能够在只见过少量目标类别数据的情况下进行有效预测的技术。它通过对预训练模型进行微调,利用少量标注数据来学习新的任务。
实现方式:
基于预训练模型的微调: 使用预训练的 Transformer 模型(如 BERT、GPT-3),通过少量标注数据进行微调。Hugging Face 提供了
Trainer
API,可以方便地进行微调。from transformers import AutoTokenizer, AutoModelForSequenceClassification, Trainer, TrainingArguments from datasets import Dataset # 示例少样本数据 data = { "text": ["I love using Hugging Face!", "The library is very intuitive."], "label": [1, 1] } # 创建 Dataset 对象 dataset = Dataset.from_dict(data) # 加载预训练模型和分词器 model_name = "bert-base-uncased" tokenizer = AutoTokenizer.from_pretrained(model_name) model = AutoModelForSequenceClassification.from_pretrained(model_name, num_labels=2) # 数据预处理 def preprocess_data(examples): return tokenizer(examples["text"], truncation=True, padding=True) tokenized_dataset = dataset.map(preprocess_data, batched=True) # 设置训练参数 training_args = TrainingArguments( output_dir="./results", num_train_epochs=3, per_device_train_batch_size=2, logging_dir="./logs", ) # 创建 Trainer 实例 trainer = Trainer( model=model, args=training_args, train_dataset=tokenized_dataset ) # 开始训练 trainer.train()
Prompt-based 学习: 通过设计好的提示语(prompts)进行少样本学习,将少量数据转化为对模型的提示。这个方法在 GPT-3 等模型上表现出色。
元学习 (Meta-learning): 利用元学习算法,如 MAML (Model-Agnostic Meta-Learning),训练模型在少量新数据上快速适应。虽然 Hugging Face 目前没有直接的元学习 API,但可以结合 PyTorch 等库实现。
零样本学习和少样本学习是解决数据有限情况下进行有效机器学习的重要方法。通过使用 Hugging Face 提供的预训练模型和工具,可以轻松实现这两种技术:
- 零样本学习: 依赖于预训练语言模型,通过自然语言提示或嵌入向量实现分类和推理。
- 少样本学习: 通过微调预训练模型或使用提示语进行学习,以适应新的任务和数据。
在每一个算法专家的脑海中都有一个根深蒂固的观念,那就是在每个新项目开始的时候都会面临一个问题:有多少标注数据?大多数时候面临的情况是“没有”或者是“很少”,但是需求方可不关心这个,模型必须具备符合预期的效果。而现实是,在小型数据集上训练出的模型通常不会有很好的效果,最有成效的解决方案是标注更多的数据提供给模型进行训练。然而,数据标注的过程需要耗费很多的人力物力,尤其是那种需要具备专业知识才能进行标注的数据。
幸运的是,对于这种标注数据短缺的情况,业界已经有了一些解决方案。你可能已经对其中的某些方法有所耳闻,比如零样本学习(zero-shot learning)或少样本学习(few-shot learning),而GPT-3模型甚至可以仅用几十个样本就能处理各种不同的任务,让人感到非常惊奇。
一般来说,模型的最优效果取决于任务、可用数据,以及可用数据被标注的比例。下图所示的结构能一定程度上帮助我们选择最恰当的方法。
按步骤对图中结构做如下说明:
- 1.是否有标注数据?
我们需要明白,即使拥有少量的标注数据也可以为模型带来正向收益。如果根本没有标注数据,则可以用零样本学习方法,这通常会设置一个较高的基线指标。
- 2.有多少标注数据?
如果拥有标注数据,那么决定模型性能的因素就是标注数据所占的比例。如果有大量的标注数据用于模型训练,就可以使用第2章介绍的标准微调方法来进行处理。
- 3.是否有未标注的原始数据?
Q. Xie et al., “Unsupervised Data Augmentation for Consistency Training”(https://arxiv.org/abs/1904.12848),(2019); S. Mukherjee and A.H. Awadallah,“Uncertainty-Aware Self-Training for Few-Shot Text Classification”(https://arxiv.org/abs/2006.15315),(2020).
如果只有少量的标注数据,但是还有大量未标注的原始数据,则这对于模型训练也是很有帮助的。假如能获得这样的未标注数据,就能在训练分类器(Classfier)之前用它来微调模型,或者使用更复杂的方法,如无监督数据增强(Unsupervised Data Augmentation,UDA)或不确定性自我训练(Uncertainty-aware Self-Training,UST) 。如果没有这样的未标注数据,也就无法去标注更多的数据,在这种情况下,就需要使用少样本学习技术,或者使用预训练语言模型的嵌入,通过最近邻搜索(nearest neighbor search),来对目标进行分类。
接下来将基于上图的思路来帮助我们在使用Jira(https://oreil.ly/TVqZQ)或GitHub(https://oreil.ly/e0Bd1)的时候,根据issue的描述自动为其打上标注。这些标注包括issue类型、导致issue的组件名称,或者负责issue的团队。将这种打标注的任务进行自动化处理,会对生产力产生很大的影响,因为这样就可以让项目的维护团队能够专注于解决用户提出的问题,而不是将时间浪费在对问题的分类上面。本章以Hugging Face在GitHub上的Transformers代码仓库为例,带领大家分析此代码仓库中的issue,学习如何构建此类任务,以及如何获取数据。
接下来介绍的方法适用于文本分类场景,但在处理命名实体识别任务、问答任务或文本摘要生成任务这些更复杂的任务时,则可能需要其他技术的加持,比如数据增强。
三、构建GitHub issue标记任务
进入Transformers代码仓库的issue模块(https://oreil.ly/StdH3),点击其中一个issue,就会得到如下图所示的页面,该页面包含一个标题、一段描述和一组标注集合。因此,该任务可以看作给定标题和描述,预测一个或多个标注,是一个典型的多标注文本分类任务。这比在之前介绍中遇到的多分类问题更具有挑战性,因为在之前介绍中,每条推文只会被标注一种情感。
1、获取数据
为了获取代码仓库中的所有issue信息,我们将使用GitHub提供的REST API(https://oreil.ly/q605k)来轮询issues端点(https://oreil.ly/qXdWV)。这个端点会返回一个JSON对象列表,每个对象都包含大量关于当前issue的字段,包括该issue的状态(打开或关闭)、issue的发起者,以及在上图中可以看到的标题、正文和标注。
由于获取所有issue信息需要耗费一些时间,本书的GitHub代码仓库(https://oreil.ly/if2dm)中提供了一个github-issues-transformers.jsonl文件,以及一个fetch_issues()函数,你可以自行下载它们。
GitHub REST API会将pull请求也加入issue当中,因此我们的数据集里面混合了原始issue和pull请求issue。为了不让任务变得复杂,我们将为这两种类型的issue开发分类器。其实在实践中,我们也很可能会构建两个分类器,因为这样方便对模型的性能进行更精细的控制。
现在我们知道了如何获取目标数据,下面来看看如何处理这些数据。
2、准备数据
当我们下载好了所有的issue,就可以使用Pandas来加载它们:
# 导入 pandas 库,并使用缩写 pd 以便后续使用
import pandas as pd
# 定义数据集 URL,该 URL 指向一个 JSON 数据文件
dataset_url = "https://git.io/nlp-with-transformers"
# 使用 pandas 的 read_json 函数读取 JSON 数据,指定 lines=True 表示每行都是一个 JSON 对象
df_issues = pd.read_json(dataset_url, lines=True)
# 打印 DataFrame 的形状(行数和列数)
print(f"DataFrame shape: {df_issues.shape}")
# 该行代码输出 DataFrame 的形状,格式为 (行数, 列数)
运行结果:
DataFrame shape: (9930, 26)
结果显示,在数据集中有近10 000个issue,查看单行数据,我们可以看到其中包含的许多字段,如URL、ID、日期、用户、标题、正文以及标注:
# 定义要选择的列名称列表
cols = ["url", "id", "title", "user", "labels", "state", "created_at", "body"]
# 使用 loc 索引器选择 DataFrame 中的第 2 行,并仅选择指定的列
# 然后将该行转换为一个 DataFrame
df_issues.loc[2, cols].to_frame()
# 该行代码将第 2 行指定列的值转换为 DataFrame 并返回
# loc[2, cols] 选择 DataFrame 中的第 2 行和指定的列
# to_frame() 将结果转换为单列 DataFrame
运行结果:
其中的labels列就是标注数据,它包含了一个JSON对象列表,示例如下:
[{
'id': 2659267025,
'node_id': 'MDU6TGFiZWwyNjU5MjY3MDI1',
'url': 'https://api.github.com/repos/huggingface/transformers/labels/DeepSpeed',
'name': 'DeepSpeed',
'color': '4D34F7',
'default': False,
'description': ''
}]
每个JSON对象都包含一个标注的信息,这里我们需要的信息是标注的名称,也就是其中的name字段,下面将标注名称提取出来覆盖labels列的内容:
# 对 DataFrame 的 "labels" 列进行操作
# 使用 apply 函数对每个元素应用 lambda 函数
# lambda 函数提取每个元素(列表)中字典的 "name" 属性,并返回包含这些名称的新列表
df_issues["labels"] = (df_issues["labels"]
.apply(lambda x: [meta["name"] for meta in x]))
# 打印 DataFrame 的 "labels" 列的前几行
print(df_issues[["labels"]].head())
# 这段代码将 "labels" 列中的每个元素(列表)中的字典的 "name" 属性提取出来,形成新的列表
# 然后,打印 "labels" 列的前几行,查看转换后的结果
运行结果:
labels 0 [] 1 [] 2 [DeepSpeed] 3 [] 4 []
现在,labels列中的每行都是GitHub的标注名称列表,这样就能得出issue的标注数量分布情况:
# 对 DataFrame 的 "labels" 列进行操作
# 使用 apply 函数对每个元素应用 lambda 函数
# lambda 函数计算每个元素(列表)的长度
labels_length_counts = df_issues["labels"].apply(lambda x : len(x)).value_counts()
# 将结果转换为 DataFrame 并转置
labels_length_counts_df = labels_length_counts.to_frame().T
# 打印转置后的 DataFrame
print(labels_length_counts_df)
# 这段代码首先计算 "labels" 列中每个元素(列表)的长度
# 然后统计这些长度出现的频率,并将结果转换为 DataFrame 并转置,以便更直观地查看结果
运行结果:
labels 0 1 2 3 4 5 count 6440 3057 305 100 25 3
可以看出,大多数issue都有0或1个标注,有1个以上标注的issue则少得多。下面我们来看看数据集中最频繁出现的10个标注。在Pandas中,可以通过explode()函数来展开Labels列,这样列表中的每个标注会成为行,然后简单计算每个标注出现的次数:
# 对 DataFrame 的 "labels" 列进行操作
# 使用 explode 函数将列表展开,将每个列表元素分成单独的行
df_counts = df_issues["labels"].explode().value_counts()
# 打印标签的总数
print(f"Number of labels: {len(df_counts)}")
# 将结果转换为 DataFrame 并显示前 8 个标签类别的频次
top_8_labels = df_counts.to_frame().head(8).T
# 打印前 8 个标签类别的 DataFrame
print(top_8_labels)
# 这段代码首先将 "labels" 列中的列表展开成单独的行,然后统计每个标签的出现频率
# 打印标签的总数量,并将频次最高的前 8 个标签转换为 DataFrame 并打印
运行结果:
Number of labels: 65 labels wontfix model card Core: Tokenization New model Core: Modeling \ count 2284 649 106 98 64 labels Help wanted Good First Issue Usage count 52 50 46
从结果可以看出,数据集中有65种标注,这些标注数量差异很大,分布非常不均匀。其中“wontfix”和“model card”是最频繁出现的标注。有些标注很难通过标题来推测,比如“Good First Issue”或“Help Wanted”;有些标注可以基于规则来判断,比如“model card”,就可以根据仓库是否添加模型卡片来确定。所以,能够用于预测的标注只是标注集的一个子集,要把不需要预测的标注去除掉。
以下代码段对数据集进行过滤,以获得我们要处理的标注子集,同时对标注名称进行规范化处理,使其更易阅读:
# 定义标签映射字典,将原始标签映射到新标签
label_map = {"Core: Tokenization": "tokenization",
"New model": "new model",
"Core: Modeling": "model training",
"Usage": "usage",
"Core: Pipeline": "pipeline",
"TensorFlow": "tensorflow or tf",
"PyTorch": "pytorch",
"Examples": "examples",
"Documentation": "documentation"}
# 定义过滤标签的函数
# 该函数接收一个标签列表,返回映射后的标签列表
def filter_labels(x):
return [label_map[label] for label in x if label in label_map]
# 对 DataFrame 的 "labels" 列应用过滤标签的函数
df_issues["labels"] = df_issues["labels"].apply(filter_labels)
# 生成所有标签的列表
all_labels = list(label_map.values())
# 打印转换后的 "labels" 列和所有标签列表
print(df_issues["labels"].head()) # 查看转换后的 "labels" 列前几行
print(all_labels) # 打印所有标签列表
运行结果:
0 [] 1 [] 2 [] 3 [] 4 [] Name: labels, dtype: object ['tokenization', 'new model', 'model training', 'usage', 'pipeline', 'tensorflow or tf', 'pytorch', 'examples', 'documentation']
现在我们来看看新标注的分布情况:
# 对 DataFrame 的 "labels" 列进行操作
# 使用 explode 函数将列表展开,将每个列表元素分成单独的行
df_counts = df_issues["labels"].explode().value_counts()
# 将结果转换为 DataFrame 并转置
df_counts_df = df_counts.to_frame().T
# 打印转置后的 DataFrame
print(df_counts_df)
# 这段代码首先将 "labels" 列中的列表展开成单独的行,然后统计每个标签的出现频率
# 将频次结果转换为 DataFrame 并转置,以便更直观地查看结果
运行结果:
labels tokenization new model model training usage pipeline \ count 106 98 64 46 42 labels tensorflow or tf pytorch documentation examples count 41 37 28 24
在后面的内容中,我们会发现,未打标注的issue在训练过程中可以当作单独的分片来处理。所以这里我们创建一个新的列,来表示该issue是否打了标注:
# 新增一列 "split",初始值全部设置为 "unlabeled"
df_issues["split"] = "unlabeled"
# 创建一个布尔掩码,标识 "labels" 列中非空列表的行
mask = df_issues["labels"].apply(lambda x: len(x)) > 0
# 使用 loc 索引器和掩码,将 "labels" 列中非空列表的行的 "split" 列值设置为 "labeled"
df_issues.loc[mask, "split"] = "labeled"
# 统计 "split" 列中每个类别的数量,并将结果转换为 DataFrame
split_counts_df = df_issues["split"].value_counts().to_frame()
# 打印 "split" 列中每个类别的数量
print(split_counts_df)
# 这段代码首先新增一列 "split",初始值全部设置为 "unlabeled"
# 然后根据 "labels" 列的内容创建一个布尔掩码,将包含标签的行的 "split" 列值设置为 "labeled"
# 最后统计 "split" 列中每个类别的数量,并将结果转换为 DataFrame 以便查看
运行结果:
count split unlabeled 9489 labeled 441
下面来看一个例子:
# 对指定的列进行操作,打印每列第 26 行的前 500 个字符
# 使用一个 for 循环遍历列名称列表
for column in ["title", "body", "labels"]:
# 打印列名称
print(f"{column}: {df_issues[column].iloc[26][:500]}\n")
# 这段代码对 "title"、"body" 和 "labels" 列进行操作
# 对于每列,使用 iloc 索引器选择 DataFrame 中第 26 行
# 然后切片 [:500] 获取前 500 个字符,并打印结果
运行结果:
title: Add new CANINE model body: # 🌟 New model addition ## Model description Google recently proposed a new **C**haracter **A**rchitecture with **N**o tokenization **I**n **N**eural **E**ncoders architecture (CANINE). Not only the title is exciting: > Pipelined NLP systems have largely been superseded by end-to-end neural modeling, yet nearly all commonly-used models still require an explicit tokenization step. While recent tokenization approaches based on data-derived subword lexicons are less brittle than manually en labels: ['new model']
在该例子中,我们提出了一种新的模型架构,因此new model的添加对于该模型是有意义的。还可以看出,title包含了对分类器有用的信息,所以可以把它和body字段中的issue描述拼接起来:
# 使用 apply 函数将 "title" 和 "body" 列的内容合并到新列 "text" 中
df_issues["text"] = (df_issues
.apply(lambda x: x["title"] + "\n\n" + x["body"], axis=1))
# 这段代码使用 apply 函数,对 DataFrame 中的每一行应用 lambda 函数
# lambda 函数接收行对象 x,将其 "title" 和 "body" 列的内容合并成一个新的字符串
# 然后将合并后的字符串赋值给新列 "text"
在查看其他数据之前,先检查一下数据中是否有重复的地方,如果有,则使用drop_duplicates()方法将它们去重:
# 计算操作前 DataFrame 的行数
len_before = len(df_issues)
# 使用 drop_duplicates 函数基于 "text" 列删除重复的行
df_issues = df_issues.drop_duplicates(subset="text")
# 计算删除重复行的比例,并打印提示信息
removed_percentage = (len_before - len(df_issues)) / len_before
print(f"Removed {removed_percentage:.2%} duplicates.")
# 这段代码首先计算操作前 DataFrame 的行数
# 然后使用 drop_duplicates 函数基于 "text" 列删除重复的行
# 最后计算删除重复行的比例,并打印提示信息显示删除的比例
运行结果:
Removed 1.88% duplicates.
从去重结果中可以获知,在我们的数据集中有一些重复的issue,但只占很小的比例(1.88%)。下面我们先查看文本的单词数量,再按照模型的上下文大小将其截断,看看是否会丢失信息:
import numpy as np
import matplotlib.pyplot as plt
# 使用 str.split() 和 apply(len) 计算每个 "text" 列中的单词数量,并绘制直方图
(df_issues["text"].str.split().apply(len)
.hist(bins=np.linspace(0, 500, 50), grid=False, edgecolor="C0"))
# 添加标题和轴标签
plt.title("Words per issue")
plt.xlabel("Number of words")
plt.ylabel("Number of issues")
# 保存图表为文件,并设置自适应大小
plt.savefig('images/WordsPerIssue.png', bbox_inches='tight')
# 显示图形
plt.show()
# 这段代码首先使用 str.split() 将每个 "text" 列中的文本按单词分割成列表
# 然后使用 apply(len) 计算每个列表的长度(即单词数量),并绘制直方图
# 设置直方图的 bins、grid、edgecolor 参数
# 最后添加标题和轴标签,并显示绘制的直方图
运行结果:
以上分布结果符合大多数文本数据集的长尾特征,因为大多数issue都较短,很少有超过500个单词的issue。有超过500个单词的长issue也是很常见的,特别是当错误消息和代码片段合并到一起发布成为issue时。鉴于大多数Transformer模型的上下文大小为512个词元或更多,截断少数的长issue也不会对整体性能有什么大的影响。现在我们已经探索和清洗了我们的数据集,在这之后要做的是创建训练集和验证集,以便为我们的分类器设定基准指标。下面来看看如何实现。
3、创建训练集
对于多标注任务来说,创建训练集和验证集相对麻烦,因为无法保证所有标注在两个数据集分布情况一致。我们可以做一些近似操作,使用Scikit-multilearn库(http://scikit.ml)的MultiLabelBinarizer类将标注转换为独热编码形式。先传入一个标注名称列表,并创建一个向量,其中的0代表没有的标注,1代表有的标注。下面我们将all_labels传给MultiLabelBinarizer进行拟合,来看看标注名称与ID的映射关系,如下所示:
from sklearn.preprocessing import MultiLabelBinarizer
# 初始化 MultiLabelBinarizer 对象
mlb = MultiLabelBinarizer()
# 使用所有标签列表进行拟合
mlb.fit([all_labels])
# 对两个示例标签列表进行二值化处理
# 第一个标签列表包含 "tokenization" 和 "new model"
# 第二个标签列表包含 "pytorch"
binary_labels = mlb.transform([["tokenization", "new model"], ["pytorch"]])
# 打印二值化后的标签
print(binary_labels)
# 这段代码首先初始化一个 MultiLabelBinarizer 对象
# 然后使用所有标签列表对其进行拟合
# 接下来,对两个示例标签列表进行二值化处理,得到二值化后的标签矩阵
# 最后打印二值化后的标签矩阵
运行结果:
[[0 0 0 1 0 0 0 1 0] [0 0 0 0 0 1 0 0 0]]
在这个简单例子中,我们可以看到第一行有两个1分别对应于tokenization和new model的标注,而第二行只有一个1对应于pytorch。
然后使用Scikit-multilearn的iterative_train_test_split()函数将数据拆分为训练集和测试集。由于我们采用迭代式生成训练样本和测试样本,因此这个函数可以保证生成的训练集和测试集中的标注分布是一致的。我们把它封装成一个可以应用于DataFrames的函数,由于该函数期望有一个二维特征矩阵,我们需要在进行拆分之前给可能的索引增加一个维度:
# 导入 iterative_train_test_split 函数,用于进行分层多标签数据集划分
from skmultilearn.model_selection import iterative_train_test_split
# 定义一个函数,进行平衡的训练集和测试集划分
def balanced_split(df, test_size=0.5):
# 创建索引数组,并扩展维度以适应 split 函数的输入要求
ind = np.expand_dims(np.arange(len(df)), axis=1)
# 使用 MultiLabelBinarizer 将标签进行二值化
labels = mlb.transform(df["labels"])
# 使用 iterative_train_test_split 函数进行分层多标签数据集划分
ind_train, _, ind_test, _ = iterative_train_test_split(ind, labels, test_size)
# 根据划分的索引返回训练集和测试集
return df.iloc[ind_train[:, 0]], df.iloc[ind_test[:, 0]]
# 这段代码首先导入 iterative_train_test_split 函数,用于进行分层多标签数据集划分
# 定义了一个函数 balanced_split,该函数接收一个 DataFrame 和测试集比例 test_size
# 函数内部首先创建索引数组,并扩展维度以适应 split 函数的输入要求
# 使用 MultiLabelBinarizer 将标签进行二值化处理
# 然后使用 iterative_train_test_split 函数根据索引数组和二值化标签进行分层多标签数据集划分
# 最后根据划分的索引返回训练集和测试集
(如果没有安装skmultilearn,jupyter notebook 上使用命令 !pip install scikit-multilearn 安装)
定义好了balanced_split()函数,我们可以把数据拆分成监督和无监督部分组成的数据集,然后为监督部分创建标注分布一致的训练、验证和测试集:
from sklearn.model_selection import train_test_split
# 创建一个新的 DataFrame,仅包含 "text"、"labels" 和 "split" 列,并重置索引
df_clean = df_issues[["text", "labels", "split"]].reset_index(drop=True).copy()
# 提取未标注的数据集
df_unsup = df_clean.loc[df_clean["split"] == "unlabeled", ["text", "labels"]]
# 提取已标注的数据集
df_sup = df_clean.loc[df_clean["split"] == "labeled", ["text", "labels"]]
# 设置随机种子以确保结果可复现
np.random.seed(0)
# 使用 balanced_split 函数,将已标注的数据集划分为训练集和临时集,比例为 50%
df_train, df_tmp = balanced_split(df_sup, test_size=0.5)
# 再次使用 balanced_split 函数,将临时集划分为验证集和测试集,比例为 50%
df_valid, df_test = balanced_split(df_tmp, test_size=0.5)
# 打印数据集的大小
print(f"Train set size: {len(df_train)}, Validation set size: {len(df_valid)}, Test set size: {len(df_test)}")
# 这段代码首先创建一个仅包含 "text"、"labels" 和 "split" 列的 DataFrame,并重置索引
# 然后根据 "split" 列的值,提取未标注和已标注的数据集
# 设置随机种子以确保结果可复现
# 使用 balanced_split 函数,将已标注的数据集划分为训练集和临时集,比例为 50%
# 再次使用 balanced_split 函数,将临时集划分为验证集和测试集,比例为 50%
# 最后打印数据集的大小,确保划分的结果符合预期
运行结果:
Train set size: 223, Validation set size: 106, Test set size: 111
接着,将它们转为DatasetDict格式,这样就能轻松对数据集进行词元化,并与Trainer整合,方便训练。这里,我们将使用from_pandas()方法,直接从相应的Pandas DataFrame中加载每个拆分部分:
from datasets import Dataset, DatasetDict
# 将训练、验证、测试和未标注的数据集 DataFrame 转换为 Hugging Face 的 Dataset 对象
# 并将这些 Dataset 对象存储在 DatasetDict 中
ds = DatasetDict({
"train": Dataset.from_pandas(df_train.reset_index(drop=True)), # 将训练集 DataFrame 转换为 Dataset 对象
"valid": Dataset.from_pandas(df_valid.reset_index(drop=True)), # 将验证集 DataFrame 转换为 Dataset 对象
"test": Dataset.from_pandas(df_test.reset_index(drop=True)), # 将测试集 DataFrame 转换为 Dataset 对象
"unsup": Dataset.from_pandas(df_unsup.reset_index(drop=True)) # 将未标注的数据集 DataFrame 转换为 Dataset 对象
})
# 这段代码首先导入所需的 Dataset 和 DatasetDict 模块
# 然后将训练集、验证集、测试集和未标注的数据集 DataFrame 转换为 Hugging Face 的 Dataset 对象
# 使用 reset_index(drop=True) 来重置 DataFrame 的索引
# 最后,将这些 Dataset 对象存储在 DatasetDict 中,以便于后续的处理和使用
最后,创建一些训练切片,以便评估每个分类器的性能与训练集大小的关系。
4、创建训练切片
该数据集具有两个我们在本章要研究的特性:稀疏标注数据和多标注分类。训练集中只有220个可供训练的样本,即使是迁移学习,这也是一个挑战。为了深入研究模型在小标注数据量上的效果,我们还将会创建样本更少的训练数据切片。然后将样本的数量与性能做比较,研究模型在不同数据量下的效果。我们将从每个标记只有8个样本开始,使用iterative_train_test_split()函数创建有不同样本的数据集,直到覆盖全部训练集:
# 设置随机种子以确保结果可复现
np.random.seed(0)
# 获取训练集的所有索引,并扩展维度以适应 split 函数的输入要求
all_indices = np.expand_dims(list(range(len(ds["train"]))), axis=1)
# 初始化索引池为所有训练集的索引
indices_pool = all_indices
# 将训练集的标签进行二值化处理
labels = mlb.transform(ds["train"]["labels"])
# 定义需要创建的训练样本集的大小
train_samples = [8, 16, 32, 64, 128]
# 初始化存储不同大小训练样本集的列表和上一次划分的样本数
train_slices, last_k = [], 0
# 循环遍历每一个训练样本集的大小
for i, k in enumerate(train_samples):
# 使用 iterative_train_test_split 函数,从索引池中分离出新的样本集
indices_pool, labels, new_slice, _ = iterative_train_test_split(
indices_pool, labels, (k-last_k)/len(labels))
# 更新上一次划分的样本数
last_k = k
# 如果是第一个样本集,直接添加到 train_slices
if i == 0:
train_slices.append(new_slice)
# 否则,将新样本集与上一个样本集合并后添加到 train_slices
else:
train_slices.append(np.concatenate((train_slices[-1], new_slice)))
# 添加完整数据集作为最后一个样本集
train_slices.append(all_indices)
train_samples.append(len(ds["train"]))
# 将每个样本集的索引从二维数组压缩为一维数组
train_slices = [np.squeeze(train_slice) for train_slice in train_slices]
# 这段代码首先设置随机种子以确保结果可复现
# 获取训练集的所有索引,并扩展维度以适应 split 函数的输入要求
# 将训练集的标签进行二值化处理
# 定义需要创建的训练样本集的大小
# 初始化存储不同大小训练样本集的列表和上一次划分的样本数
# 循环遍历每一个训练样本集的大小,使用 iterative_train_test_split 函数,从索引池中分离出新的样本集
# 更新上一次划分的样本数,如果是第一个样本集,直接添加到 train_slices,否则将新样本集与上一个样本集合并后添加到 train_slices
# 添加完整数据集作为最后一个样本集,将每个样本集的索引从二维数组压缩为一维数组
注意,这种迭代方法只是将样本近似拆分成所需大小,因为如果严格指定大小,并不一定能找到标注分布一致的数据集分片:
# 打印目标训练样本集的大小
print("Target split sizes:")
print(train_samples)
# 打印实际训练样本集的大小
print("Actual split sizes:")
print([len(x) for x in train_slices])
# 这段代码首先打印目标训练样本集的大小,即我们希望创建的样本集大小列表
# 然后打印实际生成的每个训练样本集的大小,确保生成的样本集大小与目标大小一致
运行结果:
Target split sizes: [8, 16, 32, 64, 128, 223] Actual split sizes: [10, 19, 36, 68, 134, 223]
我们将使用特定的数据集分片大小作为后文图表的刻度。到这里,我们准备好了用于训练的数据集分片,接下来看看如何训练一个强大的基线模型。
四、基线模型
——朴素贝叶斯
每当开启一个新的NLP项目时,创建一个强大的基线模型是很有必要的,主要有两个原因:
- 1.如果能够使用正则表达式,或借助一个非常简单的模型就能解决实际问题,那么就没有必要使用更复杂的模型,因为其带来的提升也会非常有限,而且像Transformer这样的模型的部署和维护通常也比较复杂。
- 2.基线模型可以给复杂模型提供快速对比的参考依据。例如,假设你训练了一个BERT-large模型,并在验证集仅获得了80%的准确率,你很可能会认为这是验证集而不是模型的问题。在这之后,当你知道像逻辑回归这样的简单分类器也能得到95%的准确率时,你就会转变思路,认为验证集是没有问题的,而是模型出了问题,然后你就可能会去一遍遍地调试你的模型。
因此,训练一个不错的基线模型是很有必要的。对于文本分类这样的任务来说,朴素贝叶斯分类器(Naive Bayes classifier)是最基础的分类模型之一,它非常简单,能够快速训练,并有一定的稳健性。朴素贝叶斯的Scikit-learn实现并不具备开箱即用的多标注分类能力,不过我们可以使用Scikit-multilearn库将多标注分类转换为对每个标注的二分类问题,为L标注训练L个二进制分类器。首先使用MultiLabelBinarizer在我们的训练集中创建一个新的label_ids列,再使用map()函数来一次性解决所有处理步骤:
# 定义一个函数,用于准备标签
def prepare_labels(batch):
# 使用 MultiLabelBinarizer 将标签转换为二值化的标签 ID
batch["label_ids"] = mlb.transform(batch["labels"])
return batch
# 将 prepare_labels 函数映射到整个数据集上,应用于每个批次
ds = ds.map(prepare_labels, batched=True)
# 这段代码首先定义了一个函数 prepare_labels,用于将标签转换为二值化的标签 ID
# 然后使用 Dataset 对象的 map 方法,将 prepare_labels 函数应用于整个数据集的每个批次
# 通过 batched=True 参数,确保函数以批处理方式应用,能够高效地处理较大的数据集
运行结果:
为了评估分类器的性能,这里使用微观和宏观的F1分数,其中前者跟踪出现较频繁的标注上的性能,后者跟踪不考虑频率的所有标注上的性能。由于我们将在不同大小的训练分片中评估每个模型,因此我们将创建一个defaultdict,用一个列表来保存每个训练分片的分数:
from collections import defaultdict
# 初始化存储宏观评分和微观评分的字典
# 这两个字典将用于存储不同训练样本集大小下的评估结果
macro_scores, micro_scores = defaultdict(list), defaultdict(list)
# 这段代码首先导入 defaultdict 模块
# 然后分别初始化宏观评分(macro_scores)和微观评分(micro_scores)的字典
# 这些字典用于存储不同训练样本集大小下的评估结果,以便后续进行比较和分析
到这一步,训练基线模型的准备工作已经就绪,下面是训练基线模型的代码,在训练集规模持续增加的情况下评估分类器性能:
from sklearn.naive_bayes import MultinomialNB
from sklearn.metrics import classification_report
from skmultilearn.problem_transform import BinaryRelevance
from sklearn.feature_extraction.text import CountVectorizer
# 循环遍历每个训练样本集
for train_slice in train_slices:
# 获取当前训练样本集和测试数据
ds_train_sample = ds["train"].select(train_slice)
y_train = np.array(ds_train_sample["label_ids"])
y_test = np.array(ds["test"]["label_ids"])
# 使用简单的词频向量化器将文本编码为词频计数
count_vect = CountVectorizer()
X_train_counts = count_vect.fit_transform(ds_train_sample["text"])
X_test_counts = count_vect.transform(ds["test"]["text"])
# 创建并训练模型
classifier = BinaryRelevance(classifier=MultinomialNB())
classifier.fit(X_train_counts, y_train)
# 生成预测并进行评估
y_pred_test = classifier.predict(X_test_counts)
clf_report = classification_report(
y_test, y_pred_test, target_names=mlb.classes_, zero_division=0,
output_dict=True)
# 存储评估指标
macro_scores["Naive Bayes"].append(clf_report["macro avg"]["f1-score"])
micro_scores["Naive Bayes"].append(clf_report["micro avg"]["f1-score"])
# 这段代码首先导入必要的库,包括 MultinomialNB、classification_report、BinaryRelevance 和 CountVectorizer
# 然后在 for 循环中,遍历每个训练样本集,获取当前训练样本集和测试数据
# 使用 CountVectorizer 将文本编码为词频计数
# 创建并训练朴素贝叶斯模型,生成预测并进行评估,最后将评估指标存储在宏观评分和微观评分的字典中
上面是一段较长的代码片段,这其中进行了许多操作,我们来一探究竟。首先,我们获取训练切片并对标注进行编码。然后,我们使用CountVectorizer对文本进行编码,简单地创建一个与词表大小相关的向量,其中每个条目对应于文本中某一个词元出现的频率。这种方式被称为词袋(bag-of-words),因为所有词汇的顺序信息都会丢失。最后,我们训练分类器,并在测试集上使用预测结果,通过分类报告获得微观和宏观的F1分数。
使用下面的辅助函数,可以绘制这个实验的结果:
import matplotlib.pyplot as plt
def plot_metrics(micro_scores, macro_scores, sample_sizes, current_model, save_path=None):
"""
绘制微观和宏观 F1 分数随训练样本数量变化的曲线图,并可选择保存图片。
Args:
- micro_scores (dict): 包含每个模型微观 F1 分数的字典。
- macro_scores (dict): 包含每个模型宏观 F1 分数的字典。
- sample_sizes (list): 训练样本集大小的列表。
- current_model (str): 当前模型的名称,将用粗实线表示。
- save_path (str or None, optional): 图片保存路径。如果为 None,则不保存图片,默认为 None。
Returns:
- None: 直接显示绘制的图形或保存图片到指定路径。
"""
fig, (ax0, ax1) = plt.subplots(1, 2, figsize=(10, 4), sharey=True)
for run in micro_scores.keys():
if run == current_model:
ax0.plot(sample_sizes, micro_scores[run], label=run, linewidth=2)
ax1.plot(sample_sizes, macro_scores[run], label=run, linewidth=2)
else:
ax0.plot(sample_sizes, micro_scores[run], label=run,
linestyle="dashed")
ax1.plot(sample_sizes, macro_scores[run], label=run,
linestyle="dashed")
ax0.set_title("Micro F1 scores")
ax1.set_title("Macro F1 scores")
ax0.set_ylabel("Test set F1 score")
ax0.legend(loc="lower right")
for ax in [ax0, ax1]:
ax.set_xlabel("Number of training samples")
ax.set_xscale("log")
ax.set_xticks(sample_sizes)
ax.set_xticklabels(sample_sizes)
ax.minorticks_off()
plt.tight_layout()
# 判断是否保存图片
if save_path:
plt.savefig(save_path, bbox_inches='tight')
else:
plt.show()
# 绘制 Naive Bayes 模型的性能指标曲线图
plot_metrics(micro_scores, macro_scores, train_samples, "Naive Bayes","images/Naive_Bayes.png")
运行结果:
注意,这里将样本的数量绘制在对数坐标轴刻度之上。从结果图中可以看出,随着训练样本数量的增加,微观和宏观F1分数都有所升高。但可用于训练的样本太少,结果包含许多噪声,因为每个切片都可能有不同类别的分布。尽管如此,我们只需要得到这个结果的趋势即可。下面我们看看这些结果与基于Transformer的模型相比效果如何。
附录
一、当前案例环境 pacakge 的 版本如下
Package Version
------------------------- --------------
aiohttp 3.9.5
aiosignal 1.3.1
alembic 1.13.2
anyio 4.4.0
argon2-cffi 23.1.0
argon2-cffi-bindings 21.2.0
arrow 1.3.0
asttokens 2.4.1
async-lru 2.0.4
attrs 23.2.0
Babel 2.15.0
beautifulsoup4 4.12.3
bleach 6.1.0
certifi 2024.7.4
cffi 1.16.0
charset-normalizer 3.3.2
colorama 0.4.6
coloredlogs 15.0.1
colorlog 6.8.2
comm 0.2.2
contourpy 1.2.1
cycler 0.12.1
datasets 2.20.0
debugpy 1.8.2
decorator 5.1.1
defusedxml 0.7.1
dill 0.3.8
executing 2.0.1
fastjsonschema 2.20.0
filelock 3.15.4
flatbuffers 24.3.25
fonttools 4.53.1
fqdn 1.5.1
frozenlist 1.4.1
fsspec 2024.5.0
greenlet 3.0.3
h11 0.14.0
httpcore 1.0.5
httpx 0.27.0
huggingface-hub 0.23.4
humanfriendly 10.0
idna 3.7
ipykernel 6.29.5
ipython 8.26.0
ipywidgets 8.1.3
isoduration 20.11.0
jedi 0.19.1
Jinja2 3.1.4
joblib 1.4.2
json5 0.9.25
jsonpointer 3.0.0
jsonschema 4.23.0
jsonschema-specifications 2023.12.1
jupyter 1.0.0
jupyter_client 8.6.2
jupyter-console 6.6.3
jupyter_core 5.7.2
jupyter-events 0.10.0
jupyter-lsp 2.2.5
jupyter_server 2.14.2
jupyter_server_terminals 0.5.3
jupyterlab 4.2.3
jupyterlab_pygments 0.3.0
jupyterlab_server 2.27.2
jupyterlab_widgets 3.0.11
kiwisolver 1.4.5
Mako 1.3.5
MarkupSafe 2.1.5
matplotlib 3.9.1
matplotlib-inline 0.1.7
mistune 3.0.2
mpmath 1.3.0
multidict 6.0.5
multiprocess 0.70.16
nbclient 0.10.0
nbconvert 7.16.4
nbformat 5.10.4
nest-asyncio 1.6.0
networkx 3.3
notebook 7.2.1
notebook_shim 0.2.4
numpy 1.26.4
onnx 1.16.1
onnxruntime 1.18.1
optuna 3.6.1
overrides 7.7.0
packaging 24.1
pandas 2.2.2
pandocfilters 1.5.1
parso 0.8.4
pillow 10.4.0
pip 24.1.2
platformdirs 4.2.2
prometheus_client 0.20.0
prompt_toolkit 3.0.47
protobuf 5.27.2
psutil 6.0.0
pure-eval 0.2.2
pyarrow 16.1.0
pyarrow-hotfix 0.6
pycparser 2.22
Pygments 2.18.0
pyparsing 3.1.2
pyreadline3 3.4.1
python-dateutil 2.9.0.post0
python-json-logger 2.0.7
pytz 2024.1
pywin32 306
pywinpty 2.0.13
PyYAML 6.0.1
pyzmq 26.0.3
qtconsole 5.5.2
QtPy 2.4.1
referencing 0.35.1
regex 2024.5.15
requests 2.32.3
rfc3339-validator 0.1.4
rfc3986-validator 0.1.1
rpds-py 0.19.0
scikit-learn 1.5.1
scikit-multilearn 0.2.0
scipy 1.14.0
Send2Trash 1.8.3
sentencepiece 0.2.0
setuptools 70.0.0
six 1.16.0
sniffio 1.3.1
soupsieve 2.5
SQLAlchemy 2.0.31
stack-data 0.6.3
sympy 1.13.0
terminado 0.18.1
threadpoolctl 3.5.0
tinycss2 1.3.0
tokenizers 0.13.3
torch 2.2.1
tornado 6.4.1
tqdm 4.66.4
traitlets 5.14.3
transformers 4.24.0
types-python-dateutil 2.9.0.20240316
typing_extensions 4.12.2
tzdata 2024.1
uri-template 1.3.0
urllib3 2.2.2
wcwidth 0.2.13
webcolors 24.6.0
webencodings 0.5.1
websocket-client 1.8.0
wheel 0.43.0
widgetsnbextension 4.0.11
xxhash 3.4.1
yarl 1.9.4