微调较小的Transformer模型:文本分类
使用微软的Phi-3生成合成数据

从更大的模型构建一个更小的模型以在特定用例上执行 | 作者插图
文本分类模型并不是新鲜事,但它们的构建速度和性能要求已经提高。
我在这里微调的基于Transformer的模型比GPT-3.5 Turbo小1000倍以上。由于它将专门针对这个用例进行训练,因此在这个用例中表现将更为一致。
这个想法是优化AI工作流程,在较小模型表现出色的地方,特别是在处理冗余任务时,较大模型则显得过于复杂。

简化的模型大小演示,纯属娱乐 | 图片由作者提供
我之前谈到过 这个,我使用序列到序列的转换器模型构建了一个稍大的关键词提取器,专注于技术内容。我还介绍了不同的 模型 及其擅长的领域。
对于这篇文章,我将深入探讨使用Transformer进行文本分类,在这种情况下,编码器模型表现良好。我将训练一个预训练的编码器模型,使用二元类别来识别诱饵标题与实际文章。不过,您可以将其训练用于其他用例。
您可以在这里找到完成的模型。
大多数组织使用开源 LLM,如 Mistral 和 Llama 来转换他们的训练数据集,但我在这里要做的是通过 Ollama 使用 Phi-3 创建训练数据。

理想情况下,您希望拥有更平衡的训练数据 | 图片由作者提供
在使用大语言模型的数据时,总是存在模型过拟合的风险,但在这种情况下,它表现良好,所以我正在加入人工数据的行列。不过,你需要小心,并在训练过程中查看指标。
关于构建一个文本分类器来识别诱饵标题,我想我们可以达成共识,有些诱饵标题是有好处的,由于它们能保持事情的趣味性。我在我编造的各种标题上尝试了完成的模型,发现只有实际内容可能有点乏味。

编写一些标题以测试模型 | 作者提供的图片
这些问题总是看起来一目了然,不过深入其中后,你会发现它们比你思考的要复杂得多。我脑海中出现的问题是,“什么是好的标题党内容,什么是坏的标题党内容?”一个平台可能需要两者的结合,以保持人们的阅读兴趣。
我在我所有的内容上使用了新的模型,没有我的标题被识别为标题党。我不确定这是否是好事。
如果你是像BERT这样的变换器编码器模型的新手,这是一个很好的学习体验。如果你对使用变换器构建文本分类模型并不陌生,你可能会发现查看合成数据的效果以及我的模型性能指标很有趣。
众所周知,使用虚假数据比访问真实数据更简单。
介绍
我从这件作品中得到了灵感
Fabian Ridder 在使用 ChatGPT 来识别吸引眼球的标题和实际文章,以训练一个使用 FastText 的模型。我认为这个案例超级适合一个较小的变换器模型。我们正在构建的模型将使用合成数据,而不是实际数据。这个过程会很快速,由于生成数据只需大约一个小时左右,训练只需几分钟。该模型将超级小,仅有 1100 万个参数。
由于我们使用的是二元类,即标题党或实际,我们将能够达到99%的准确性。该模型将比FastText更好地解释细微的文本。
训练的费用将为零,我已经准备好了我们将用来进行此项工作的数据集。不过,您可以为其他用例生成自己的数据。
如果你想深入训练模型,你可以跳过介绍部分,我会提供一些关于编码器模型及其擅长任务的信息。
编码器模型及其擅长的领域
尽管变换器在生成文本方面引入了惊人的能力,但它们在其他自然语言处理任务中也有所改善,例如文本分类和提取。
模型架构之间的区别有些模糊,但理解不同的 transformer 模型最初是为不同任务构建的,这一点是有用的。
一个解码器模型接收较小的输入并输出较大文本。GPT,在早期介绍了令人印象深刻的文本生成,是一个解码器模型。虽然更大的语言模型今天提供了更细致的能力,但解码器并不是为涉及提取和标记的任务而构建的。对于这些任务,我们可以使用编码器模型,它们接收更多输入并提供简化输出。
编码器擅长提取信息而不是生成信息。

更小的 Transformer 模型 — 编码器与解码器 | 作者图片
我不会再深入讲这个话题,但你应该可以找到许多关于这个主题的信息,尽管这可能有点技术性。
那么,编码器常用的任务有哪些呢?一些例子包括情感分析、分类、命名实体识别和关键词/主题提取等。
您可以尝试一个将文本分类为十二种不同情感的模型 这里。您还可以查看一个将仇恨言论分类为有毒的模型 这里。这两者都是使用仅编码器模型构建的,在这种情况下,使用的是RoBERTa。
有许多基础模型可以使用;RoBERTa是一个更新的模型,它使用了更多的数据进行训练,并通过优化训练技术改善了BERT。

更为知名的编码器变换器模型——它们有不同的大小 | 图片来自作者
BERT 是第一个仅编码的 Transformer 模型,这个模型通过比以前的模型更好地理解语言上下文而开启了一切。DistillBERT 是 BERT 的压缩版本。
ALBERT 使用了一些技巧来减少参数的数量,使其变得更小而不会显著损失性能。这是我在这个案例中将使用的,由于我认为它能表现良好。
DeBERTA是一个改善的模型,更好地理解单词关系和上下文。一般来说,更大的模型在复杂的自然语言处理任务中表现更好。不过,如果训练数据不够多样化,它们更容易出现过拟合。
对于这一部分,我专注于一个任务:文本分类。那么,构建一个文本分类模型有多难呢?这实际上取决于你希望它做什么。当处理二元类别时,在大多数情况下你可以获得较高的准确率。不过,这也取决于用例的复杂性。
您可以查看一些基准测试来了解 BERT 在不同开源数据集上的表现。我查阅了论文“如何为文本分类微调 BERT?”,以查看这些基准测试,并在下面绘制了它们的准确率与训练标签数量的关系图。

来自论文“如何微调BERT进行文本分类?”的基准数据集 | 图片来源于作者
我们看到只有两个标签的数据集表现得相当不错。这就是我们所说的二元标签。值得注意的是,DBpedia 数据集具有 14 个类别,但作为基准却达到了 98% 的准确性,而 Yelp Review Full 数据集仅有 5 个类别,准确率仅为 70%。
这里复杂性就出现了:Yelp评论很难进行标记,特别是在1到5星之间进行评分时。想想看,人类要将别人的文本分类到一个特定的星级有多困难;这实际上取决于个人如何对自己评论进行分类。
如果你想用Yelp评论数据集构建一个文本分类器,你会发现1星和5星评论大多数时候被正确标记,但模型在2星、3星和4星评论上会遇到困难。这是由于一个人可能将其归类为2星评论,而AI模型可能会将其解释为3星评论。
另一方面,DBpedia 数据聚焦的文本对于模型来说更易于解释。
当我们训练模型时,我们可以查看每个标签的指标,而不是整体,以了解哪些标签表现不佳。尽管如此,如果您正在处理复杂任务,面对不完美的指标也不要感到沮丧。
始终在新数据上尝试,以查看它在你的用例中是否工作得足够好,并继续对数据集进行调整,或切换基础模型。
较小模型的经济学
我总是会有一部分关于构建和运行模型的成本。在任何项目中,你都必须权衡资源和效率以获得结果。
如果你只是试验一下,那么使用一个更大模型的API接口是合理的,尽管它会是计算上不够高效。
我已经运行Claude Haiku进行自然语言处理项目一个月了,从文本中提取类别、主题和地点。这只是出于演示目的,但当你想为一个组织原型设计某些东西时,这是有意义的。
不过,使用这些更大的模型进行零样本学习,会导致许多不一致的情况,有些文本必须完全忽略。有时更大的模型会输出完全无意义的内容,但同时,对于这样一个小项目来说,运行它们的成本更低。
使用您自己的模型时,您还必须托管它们,这就是我们花费大量时间尝试使它们更小的缘由。您自然可以在本地运行它们,但您可能希望能够将它们用于开发项目,因此您需要思考托管成本。

我们比较每小时托管可以处理的标题数量与API调用 | 图片作者提供
看到上面的图片,我计算了每个实例可以处理的标题数量,并比较了 GPT-3.5 的一样成本。我意识到这可能看起来有点混乱,但可视化的确 很困难。
我们可以至少推断出,如果我们在一天中偶尔使用GPT-3.5进行一个小项目,即使托管较小模型的成本很低,使用它依旧是有意义的。
断点是指你持续处理的数据量超过了某个阈值。在这种情况下,当要处理的标题超过每天32,000个时,由于保持实例24/7运行的成本将等于一样的价格。

使用 1 vCPU 的托管成本与此案例中 GPT-3.5 的 API 调用成本比较 | 图片作者
这将计算为您全天保持实例运行的情况,如果您仅在一天中的特定时间处理数据,那么托管并在不使用时缩减到零是有意义的。由于它超级小,我们也可以将其容器化,然后在ECS上托管,甚至在Lambda上进行无服务器推理。
在使用闭源的 LLM 进行零-shot 推理时,我们还需要思考到模型并没有为了这个特定情况进行训练,因此我们可能会得到不一致的结果。所以对于需要一致性的冗余任务,构建你自己的模型是一个更好的选择。
还值得注意的是,有时你需要在更复杂任务上表现良好的模型。在这里,对于更大的大型语言模型(LLM),成本差异可能会更明显,由于你需要一个更好的模型和更长的提示模板。
使用合成数据
使用LLMs转换数据并不是新鲜事,如果你还没有这样做,应该尝试一下。这比手动转换数千个数据点要快得多。
我查看了电信巨头Orange通过他们的AI/NLP工作组——NEPAL——所做的事情,他们从各个地方抓取数据,并使用GPT-3.5和Mixtral将原始文本转换为类似指令的格式,以创建可以用于训练的数据。
如果你有兴趣了解更多,可以查看Nvidia的GTC提供的会议 这里.
但是人们已经走得更远,利用更大的语言模型构建整个数据集;这被称为合成数据。 这是一种机智的方法,可以用来自更大语言模型的数据构建更小的专业模型,而这些模型的托管成本更低,效率更高。
不过对此存在一些担忧,合成数据的质量可能会受到质疑。仅依靠生成的数据可能导致模型错过现实世界数据中固有的细微差别或偏见,从而在实际看到这些数据时出现故障。
不过,生成合成数据比获取真实数据要容易得多。
构建模型
我将在这里开始创建一个超级简单的模型,模型的目的是将标题识别为点击诱饵或实际。您可以构建一个具有更多标签的不同文本分类器。
这个过程很简单,我将逐步介绍整个过程,我们将使用的烹饪书是这个。
本教程将使用这个数据集,如果你想构建自己的数据集,请务必阅读第一部分。
数据集
要创建一个合成数据集,我们可以在本地启动 Ollama 并运行我们想要用来构建训练数据的模型。确保这是一个商业可用的模型。我选择了 Phi-3,由于它小且超级优秀。
我很喜爱Javascript,所以我使用了 Ollama JS 框架构建了一个可以在后台运行以生成CSV文件的脚本。
此脚本创建引人注目的标题并将其存储在您根文件夹中的新CSV中。您需要稍后更改提示模板以生成等量的实际性标题。
由于我正在使用生成文本模型Phi-3,一些输出可能无法使用,但这是可以预期的。这将需要一些时间来运行,所以你可以去做其他事情。

我的终端用于测试生成数据到CSV | 作者图片
完成后,您可以将带有点击诱饵和实际标题的完成的 CSV 文件存储在您的 Google Drive 中。记得将文本和标签设置为字段,其中文本是标题,标签是它是点击诱饵还是实际。

数据集应如何结构化 | 图片作者
由于我已经准备好了我们将使用的数据集,请查看这个脚本,将您的自定义数据集上传到HuggingFace。
在查看数据集时,你会发现大多数由Phi-3生成的点击诱饵文章在结尾处都有一个感叹号。这是你需要确保不发生的事情,所以检查生成数据的LLM的工作是很重大的。
请记住我提供给你的脚本将你的数据分为训练集、测试集和验证集。我提议至少有一个训练集和一个测试集来训练模型。
如果你已经准备好数据集,我们可以开始微调模型。
数据集 & 模型
如果您还没有打开食谱,可以 点击这里。第一部分是决定您的数据集,然后是您的预训练模型。
从datasets导入load_dataset,DatasetDict br brdataset = load_dataset("ilsilfverskiold/clickbait_titles_synthetic_data") brdataset
model_name = "albert/albert-base-v2" bryour_path = "classify-clickbait"
我浏览了引言部分的不同模型,其中 ALBERT 和 DistillBERT 是较小的模型,而 BERT 和 RoBERTa 是较大的模型。
对于这个案例,由于它并不特别复杂,我将选择 ALBERT。我确信 BERT 可以做得更好,但 ALBERT 小十倍。RoBERTa 太大,可能会在这个数据集上产生一些过拟合。
请记住,如果您正在使用不同的语言,请寻找一个基于至少类似语言的语料库训练的基础模型。
如果您正在处理北欧语言,我可以推荐
KB/bert-base-swedish-cased,我使用它来为 IPTC 新闻编码类别创建模型。
准备数据集
目前我们需要做几件事情才能使其正常工作。
我们第一将标签转换为训练器能够理解的标准化数字格式。
from sklearn.preprocessing import LabelEncoder br brlabel_encoder = LabelEncoder() br brlabel_encoder.fit(dataset['train']['label']) br brdef encode_labels(example): br return {'encoded_label': label_encoder.transform([example['label']])[0]} br brfor split in dataset: br dataset[split] = dataset[split].map(encode_labels, batched=False)
然后我们需要将数字表明映射回实际的标签名称。这是为了在我们使用模型进行推断时能够获得实际的标签名称,而不是数字表明。
from transformers import AutoConfig br brunique_labels = sorted(list(set(dataset['train']['label']))) brid2label = {i: label for i, label in enumerate(unique_labels)} brlabel2id = {label: i for i, label in enumerate(unique_labels)} br brconfig = AutoConfig.from_pretrained(model_name) brconfig.id2label = id2label brconfig.label2id = label2id br br# 验证标签是否正确 brprint("ID to Label Mapping:", config.id2label) brprint("Label to ID Mapping:", config.label2id)
在这之后,我们准备好获取预训练模型及其分词器。我们在导入模型时使用我们设置的带有标签的配置。
br
from transformers import AlbertForSequenceClassification, AlbertTokenizer br
br
tokenizer = AlbertTokenizer.from_pretrained(model_name) br
model = AlbertForSequenceClassification.from_pretrained(model_name, config=config)
如果你使用的是不同的模型,如 BERT 或 RoBERTa,你可以使用 AutoTokenizer 和
AutoModelForSequenceClassification,它将自动为你指定的模型选择正确的类。
此函数用于过滤无效内容,然后确保文本数据被正确标记和划分,为训练准备数据集。
br
def filter_invalid_content(example): br
return isinstance(example['text'], str) br
br
dataset = dataset.filter(filter_invalid_content, batched=False) br
br
def encode_data(batch): br
tokenized_inputs = tokenizer(batch["text"], padding=True, truncation=True, max_length=256) br
tokenized_inputs["labels"] = batch["encoded_label"] br
return tokenized_inputs br
br
dataset_encoded = dataset.map(encode_data, batched=True) br
dataset_encoded
dataset_encoded.set_format(type='torch', columns=['input_ids', 'attention_mask', 'labels'])
我们还需要获取一个数据收集器来处理输入的填充。
from transformers import DataCollatorWithPadding br brdata_collator = DataCollatorWithPadding(tokenizer)
评估指标
您不需要设置任何评估指标,例如准确率、准确率、召回率或F1。但您的确 需要至少有准确率,以理解模型的表现。
准确率 衡量模型在所有类别中正确预测的数量。 准确率 衡量特定类别的预测有多准确。 召回率 告知我们模型在识别特定类别中的所有实例时的表现如何。 F1 分数 是 准确率 和 召回率 的加权平均。
我不会详细讨论这些指标,但还有许多其他的 文章 关于这个。对于这个案例,我更关心它在新的真实数据上的表现,而不是合成数据。因此,我关注的是那些过于优秀的指标,这表明它已经过拟合。
我们的确 设置了一个函数,让我们查看每个标签的准确性,而不是作为一个平均值。当你有许多标签而不仅仅是两个时,这更为相关。
br
from sklearn.preprocessing import LabelEncoder br
from sklearn.metrics import accuracy_score, confusion_matrix br
import numpy as np br
br
label_encoder = LabelEncoder() br
label_encoder.fit(unique_labels) br
br
def per_label_accuracy(y_true, y_pred, labels): br
cm = confusion_matrix(y_true, y_pred, labels=labels) br
correct_predictions = cm.diagonal() br
label_totals = cm.sum(axis=1) br
per_label_acc = np.divide(correct_predictions, label_totals, out=np.zeros_like(correct_predictions, dtype=float), where=label_totals != 0) br
return dict(zip(labels, per_label_acc))
我们还设置了一般计算指标的函数。我在这里使用了所有这些指标,由于这是我对任何文本分类器的通用模板,但你可以决定你想要哪些。
br
from sklearn.metrics import accuracy_score, recall_score, precision_score, f1_score br
br
def compute_metrics(pred): br
labels = pred.label_ids br
preds = pred.predictions.argmax(-1) br
br
decoded_labels = label_encoder.inverse_transform(labels) br
decoded_preds = label_encoder.inverse_transform(preds) br
br
precision = precision_score(decoded_labels, decoded_preds, average='weighted') br
recall = recall_score(decoded_labels, decoded_preds, average='weighted') br
f1 = f1_score(decoded_labels, decoded_preds, average='weighted') br
acc = accuracy_score(decoded_labels, decoded_preds) br
br
labels_list = list(label_encoder.classes_) br
per_label_acc = per_label_accuracy(decoded_labels, decoded_preds, labels_list) br
br
per_label_acc_metrics = {} br
for label, accuracy in per_label_acc.items(): br
label_key = f"accuracy_label_{label}" br
per_label_acc_metrics[label_key] = accuracy br
br
return { br
'accuracy': acc, br
'f1': f1, br
'precision': precision, br
'recall': recall, br
**per_label_acc_metrics br
}
一旦你对结果感到满意,我们就可以开始设置训练参数和训练器。
训练模型
接下来我们设置训练参数。在这里您可以调整轮数、批量大小和学习率。
br
from transformers import Trainer, TrainingArguments br
br
training_args = TrainingArguments( br
output_dir=your_path, br
num_train_epochs=3, br
warmup_steps=500, br
per_device_train_batch_size=16, br
per_device_eval_batch_size=16, br
weight_decay=0.01, br
logging_steps=10, br
evaluation_strategy='steps', br
eval_steps=100, br
learning_rate=2e-5, br
save_steps=1000, br
gradient_accumulation_steps=2 br
)
我选择了基于论文“如何对 BERT 进行微调以进行文本分类?”的学习率和轮数,但减少了批量大小。
目前我们可以继续设置训练器,利用我们准备的所有内容,并运行它。
br
trainer = Trainer( br
model=model, br
args=training_args, br
train_dataset=dataset_encoded['train'], br
eval_dataset=dataset_encoded['test'], br
compute_metrics=compute_metrics, br
tokenizer=tokenizer, br
data_collator=data_collator, br
) br
br
trainer.train()
在训练时,您需要留意过拟合。由于训练集和评估集都是合成的,过拟合的典型迹象可能不明显。
关注训练和评估数据集的准确性和损失。也就是说,训练和验证损失超级低,以及过于优异的评估指标可能是过拟合的迹象。
但是请记住,二元类别对于较简单的任务一般表现良好。
你将看到我下面进行的一次运行的结果。

训练指标可能看起来很优秀,但要小心合成数据 | 图片来自作者
正如您从训练指标中看到的,它们有点过于理想。验证损失也在波动。这可能是一个超级糟糕的迹象,因此您必须确保在模型训练完成后在真实数据上测试模型。
如果您正在使用多个类别训练模型,甚至可能是使用偏斜的数据集,请不要担心平均评估指标不佳。查看每个标签的指标。
评估模型
一旦训练完成,您可以运行最终评估指标,保存模型,然后保存状态。这将为您推送到模型页面的指标构建。
br
trainer.evaluate() br
trainer.save_model(你的路径) br
trainer.save_state()
目前你可以在你的笔记本中运行HuggingFace管道进行测试。
br from transformers import pipeline brpipe = pipeline('文本分类', model=your_path)brbr
br
example_titles = [ br
"获取一个示例标题", br
"获取另一个示例标题", br
"再获取一个示例标题" br
] br
br
for title in example_titles: br
result = pipe(title) br
print(f"标题: {title}") br
print(f"输出: {result[0]['label']}")
我的模型在测试数据上表现良好,但它遗漏了一些我个人认为是诱饵文章的点击诱饵。对于生产用例,最好构建一个更加多样化的数据集(尤其是合成数据),这样它才能在新的真实数据上表现良好。
不过,如果你不满意,那么你就回到数据集,重新进行或者尝试不同的模型。
如果你在想,我的确 在某些运行中得到了超级出色的结果,而在其他运行中则得到了不那么出色的结果,使用的是一样的数据、一样的训练参数以及一样的种子。
测试模型
在推送模型之前,您还可以将模型与其他替代方案进行测试。
我请GPT-3.5告知我它认为哪些标题是吸引点击的,哪些是实际性内容,它表现得超级好,这是预期中的,由于它的规模比Albert大了1000倍以上。
我们还可以将一些标题与经过微调的FastText模型与经过微调的Transformer编码器模型进行比较。

使用微调后的Albert模型和FastText测试几个标题 | 图片来源于作者
使用FastText超级简单且计算效率高,但它孤立地处理单词,缺乏深层次的上下文理解。
因此,FastText没有像基于Transformer的模型那样很好地捕捉语言的上下文和细微差别。
推送到中心
如果您对您的模型满意,可以将其推送到 HuggingFace hub 以存储在那里。
您只需使用您在HuggingFace账户的设置中找到的写入令牌登录。
!huggingface-cli 登录
然后推送它。
tokenizer.push_to_hub("用户名/分类-点击诱饵")
trainer.push_to_hub("用户名/分类-点击诱饵")
以防万一,推送分词器,特别是如果你正在使用Albert的版本。
目前你可以直接从那里使用它,你会在这里找到。
优化技术
如果你想使用更大的模型,列如BERT,你可以应用不同的技术,这样你可以在微调后进一步提炼它。就这个案例而言,我发现它并没有比直接使用ALBERT更成功。
BERT 本身的表现总体上要好得多。虽然我在大多数情况下真的很喜爱 RoBERTa,但它在这个特定数据集上容易过拟合,由于数据集要么太小,要么不好,要么过于人工。
对于每个案例,你需要估计在追求效率的过程中可以牺牲多少性能,最终你会了解到哪些模型在什么情况下表现良好。
结束语
模型如果使用真实数据表现会更好吗?这有可能,但除非数据集经过仔细排序,否则准确性可能更低。
这是一项艰苦的工作。
使用合成数据可以超级快速地完成任务,这样您可以建立一个原型进行工作。合成数据更易于处理。
您也可以自由地使用更大的开源 LLM,因此这不违反无法在不违反协议的情况下访问高质量数据的人的任何规则。
我没有投入时间和精力来构建这个数据集,但在所有情况下,您应该确保拥有模型可以学习的多样化数据。
















暂无评论内容