文本分类:微调Transformer模型

2024年06月03日 由 alex 发表 158 0

我在这里要微调的基于转换器的模型比 GPT-3.5 Turbo 小 1000 多倍。它在这种使用情况下的表现会一直更好,因为它是经过专门训练的。


我们的想法是优化人工智能工作流程,让更小的模型发挥更大的优势,尤其是在处理冗余任务时。


6


在这篇文章中,我将深入探讨使用转换器进行文本分类的问题,编码器模型在这方面表现出色。我将用二元类训练一个预先训练好的编码器模型,以识别点击诱饵和事实性文章。不过,你也可以针对不同的用例进行训练。


大多数机构都使用 Mistral 和 Llama 等开源 LLM 来转换数据集以进行训练,但我在这里要做的是通过 Ollama 使用 Phi-3 创建训练数据。


7


在使用大型语言模型的数据时,模型总有过拟合的风险,但在这种情况下,它的表现很好,所以我开始进行人工数据训练。不过,一旦进入训练阶段,你就必须小心并查看各项指标。


8


这些问题似乎总是一目了然,但当你深入研究之后,就会发现它们比你想象的更加微妙。我突然想到的问题是:'什么是好的点击诱饵内容,什么是坏的点击诱饵内容?


编码器模型及其擅长领域

transformer为文本生成带来了惊人的能力,同时也改进了其他 NLP 任务,如文本分类和提取。


模型架构之间的区别有点模糊,但了解不同的transformer模型最初是为不同的任务而构建的,还是很有帮助的。解码器模型接收较小的输入,输出较大的文本。GPT 就是一种解码器模型,它在当年带来了令人印象深刻的文本生成功能。


虽然大型语言模型如今能提供更细致入微的功能,但解码器并不是为涉及提取和标记的任务而构建的。对于这些任务,我们可以使用编码器模型,它能接收更多输入,并提供精简的输出。


编码器擅长提取信息,而不是生成信息。


9


那么,编码器常见的任务有哪些呢?例如情感分析、分类、命名实体识别和关键词/主题提取等。


你可以使用的基础模型有很多;RoBERTa 是一个更新的模型,它使用了更多的数据进行训练,并通过优化训练技术对 BERT 进行了改进。


10


BERT 是第一个仅用于编码器的变换器模型,它比以前的模型更能理解语言上下文,从而开创了这一先河。DistillBERT 是 BERT 的压缩版本。


ALBERT 使用了一些技巧来减少参数的数量,从而在不明显降低性能的情况下缩小了体积。在本例中,我将使用它,因为我认为它的性能会很好。


DeBERTA 是一个改进的模型,它能更好地理解单词关系和上下文。一般来说,较大的模型在复杂的 NLP 任务中表现会更好。不过,如果训练数据不够多样化,它们会更容易过拟合。


在这篇文章中,我只关注一项任务:文本分类。那么,建立文本分类模型有多难?这实际上取决于你要求它做什么。在处理二元类时,大多数情况下都能获得较高的准确率。不过,这也取决于用例的复杂程度。


你可以参考某些基准,了解 BERT 在处理不同开源数据集时的表现。我查阅了论文《如何微调 BERT 进行文本分类?


11


我们看到只有两个标签的数据集表现相当不错。这就是我们所说的二元标签。比较突出的是 DBpedia 数据集,它有 14 个类别,但作为基准准确率却达到了 98%,而 Yelp Review Full 数据集只有 5 个类别,准确率却只有 70%。


这就是复杂性所在: Yelp 评论很难标注,尤其是在评定 1 到 5 星级时。想一想,人类将他人的文本划分为特定的星级有多难;这实际上取决于该人如何对自己的评论进行分类。


如果使用 Yelp 评论数据集建立一个文本分类器,你会发现 1 星和 5 星评论在大多数情况下都能被正确标注,但模型在处理 2 星、3 星和 4 星评论时就会遇到困难。这是因为一个人可能将其归类为 2 星评论,而人工智能模型可能将其解释为 3 星评论。


另一方面,DBpedia 数据集的文本对模型来说更容易解读。


在训练模型时,我们可以查看每个标签的指标,而不是整体指标,以了解哪些标签表现不佳。尽管如此,如果您正在处理一项复杂的任务,如果您的指标并不完美,也不要感到气馁。


小型模型的经济性

我总是有一部分内容是关于建造和运行模型的成本。在任何项目中,你都必须权衡资源和效率,才能取得成果。


如果你只是想尝试一下,那么一个带有 API 端点的更大的模型是有意义的,尽管它的计算效率会很低。


我在一个项目中使用 Claude Haiku 进行自然语言处理已有一个月,从文本中提取类别、主题和位置。这只是出于演示目的,但当你想为一个组织建立原型时,这样做是有意义的。


然而,使用这些较大的模型进行 “零样本 ”会导致很多不一致,有些文本不得不完全忽略。有时,大型模型输出的内容绝对是胡言乱语,但同时,对于这样一个小项目来说,使用大型模型的成本更低。


使用您自己的模型,您还必须托管它们,这就是为什么我们要花费大量时间来缩小它们。您自然可以在本地运行它们,但您可能希望将它们用于开发项目,因此需要考虑托管成本。


12


看上面的图片,我计算了每个实例可以处理的标题数量,并比较了 GPT-3.5 的相同成本。我知道这看起来有点乱,但这很难可视化。


我们至少可以推断出,如果我们在一个小项目中整天都在零星地使用 GPT-3.5,那么使用它是有意义的,尽管托管较小模型的成本相当低。


当你持续处理的数据量超过某个阈值时,就是断点。在本例中,当每天要处理的标题超过 32,000 个时,就会出现这种情况,因为保持实例全天候运行的成本与价格相同。


13


这种计算方式就像你要让实例全天运行一样,如果你只在一天中的某些时段处理数据,那么托管它是合理的,然后在不使用它时将其缩减为零。由于它非常小,我们也可以将其容器化,然后托管在 ECS 甚至 Lambda 上,用于无服务器推理。


在使用闭源 LLM 进行零次推理时,我们还需要考虑到模型并没有针对这种特定情况进行过训练,因此可能会得到不一致的结果。因此,对于需要一致性的冗余任务,建立自己的模型是更好的选择。


同样值得注意的是,有时您需要的模型可以执行更复杂的任务。在这种情况下,大型 LLM 的成本差异可能会更大,因为你需要更好的模型和更长的提示模板。


使用合成数据

使用 LLM 转换数据并不是什么新鲜事,如果您没有这样做,那就应该这样做。这比手动转换数千个数据点要快得多。


我查看了电信巨头 Orange 通过其人工智能/NLP 工作组(NEPAL)所做的工作,他们从不同地方获取数据,并使用 GPT-3.5 和 Mixtral 将原始文本转换为类似指令的格式,以创建可用于训练的数据。


建立模型

在此,我将着手创建一个非常简单的模型,该模型只需将标题识别为点击诱饵或事实即可。您可以使用更多标签建立不同的文本分类器。


数据集

要创建合成数据集,我们可以在本地启动 Ollama,然后运行一个模型来创建训练数据。请确保该模型是商用模型。我选择了 Phi-3,因为它体积小,性能好。


我很喜欢 Javascript,所以我使用 Ollama JS 框架创建了一个脚本,可以在后台运行,生成 CSV 文件。


import fs from 'fs';
import ollama from 'ollama';
const prompt1 = 'Give me 10 clickbait articles in an array as strings, create them as if they were the real thing, as if you were demonstrating what would be clickbait titles, ideally they should be newsarticles as well as medium article. Return only the array with the titles.';
const prompt2 = 'Give me 10 clickbait tech articles in an array as strings, create them as if they were the real thing, as if you were demonstrating what would be clickbait titles, ideally they should be newsarticles as well as medium articles.  Return only the array with the titles.';
async function runThis() {
  let allTitles = [];
  for (let i = 0; i < 300; i++) {
    const prompt = i % 2 === 0 ? prompt1 : prompt2;
    const response = await ollama.chat({
      model: 'phi3',
      messages: [{ role: 'user', content: prompt }],
    });
    let titles = [];
    const jsonArrayMatch = response.message.content.match(/\[[^\]]*\]/);
    if (jsonArrayMatch) {
      try {
        titles = JSON.parse(jsonArrayMatch[0]);
      } catch (error) {
        console.error('Failed to parse JSON array from response:', error);
      }
    }
    allTitles = allTitles.concat(titles);
    console.log(`Iteration ${i + 1}: Collected ${allTitles.length} titles so far, ${3000 - allTitles.length} to go`);
    if (allTitles.length >= 3000) break; 
  }
  saveToCSV(allTitles);
}
function saveToCSV(titles) {
  const csvContent = titles.join('\n');
  fs.writeFileSync('clickbait_titles_3.csv', csvContent);
  console.log('CSV file has been created with 1,000 titles');
}
runThis();


该脚本会创建点击诱饵标题,并将其存储到根文件夹中的新 CSV 中。稍后您需要更改提示模板,以生成等量的事实性标题。


14


完成后,您可以将包含点击诱饵和事实磁贴的 CSV 文件存储到 Google Drive 中。记住将文本和标签设置为字段,其中文本为标题,标签为点击诱饵还是事实。


15


由于我已经准备好了我们要使用的数据集,请将您的自定义数据集上传到 HuggingFace。


翻阅数据集,你会发现大多数由 Phi-3 生成的点击诱饵文章末尾都有一个感叹号。你要确保这种情况不会发生,因此检查生成数据的 LLM 的工作非常重要。


请记住,我提供的脚本会将数据分成训练集、测试集和验证集。我建议至少要有一个训练集和测试集来训练模型。


如果你已经整理好了数据集,我们就可以继续微调模型了。


数据集和模型

首先是确定数据集,然后是预训练模型。


from datasets import load_dataset, DatasetDict
dataset = load_dataset("ilsilfverskiold/clickbait_titles_synthetic_data")
dataset


model_name = "albert/albert-base-v2""albert/albert-base-v2"
your_path = "classify-clickbait"


我在介绍部分浏览了不同的模型,其中 ALBERT 和 DistillBERT 是较小的模型,BERT 和 RoBERTa 是较大的模型。


在这个案例中,由于不是太复杂,我选择了 ALBERT。我相信 BERT 可以做得更好,但 ALBERT 要小十倍。RoBERTa 太大了,可能会对这个数据集产生一些过度拟合。


请记住,如果您使用的是一种不同的语言,那么您需要一个至少在类似语言的语料库上训练过的基础模型。


准备数据集

现在,我们需要做几件事才能顺利完成。


首先,我们要将标签转换为培训师可以理解的标准化数字格式。


from sklearn.preprocessing import LabelEncoder
label_encoder = LabelEncoder()
label_encoder.fit(dataset['train']['label'])
def encode_labels(example):
    return {'encoded_label': label_encoder.transform([example['label']])[0]}
for split in dataset:
    dataset[split] = dataset[split].map(encode_labels, batched=False)


然后,我们需要将数字代表映射回实际标签名称。这样,当我们使用模型进行推理时,就能得到实际的标签名称,而不是数字代表。


from transformers import AutoConfig
unique_labels = sorted(list(set(dataset['train']['label'])))
id2label = {i: label for i, label in enumerate(unique_labels)}
label2id = {label: i for i, label in enumerate(unique_labels)}
config = AutoConfig.from_pretrained(model_name)
config.id2label = id2label
config.label2id = label2id
# Verify the correct labels
print("ID to Label Mapping:", config.id2label)
print("Label to ID Mapping:", config.label2id)


在此之后,我们就可以获取预训练模型和它的标记化器了。我们使用导入模型时与标签一起设置的配置。


from transformers import AlbertForSequenceClassification, AlbertTokenizer
tokenizer = AlbertTokenizer.from_pretrained(model_name)
model = AlbertForSequenceClassification.from_pretrained(model_name, config=config)


如果您使用的是 BERT 或 RoBERTa 等不同的模型,可以使用 AutoTokenizer 和 AutoModelForSequenceClassification,它们会自动为您指定的模型选择正确的类。


下一个函数将过滤无效内容,然后确保文本数据已正确标记和标注,为训练数据集做好准备。


def filter_invalid_content(example):
    return isinstance(example['text'], str)
dataset = dataset.filter(filter_invalid_content, batched=False)
def encode_data(batch):
    tokenized_inputs = tokenizer(batch["text"], padding=True, truncation=True, max_length=256)
    tokenized_inputs["labels"] = batch["encoded_label"]
    return tokenized_inputs
dataset_encoded = dataset.map(encode_data, batched=True)
dataset_encoded


dataset_encoded.set_format(type='torch', columns=['input_ids', 'attention_mask', 'labels'])type='torch', columns=['input_ids', 'attention_mask', 'labels'])


我们还需要获取一个数据整理器来处理输入的填充。


from transformers import DataCollatorWithPadding
data_collator = DataCollatorWithPadding(tokenizer)


评估指标

您不需要设置任何评估指标,如准确率、精确度、召回率或 f1。不过,您至少需要准确率来了解模型的表现。


准确度衡量模型在所有类别中正确预测的数量。精确度衡量的是对某一特定类别的预测正确率。召回率则告诉我们模型识别特定类别中所有实例的能力。F1 分数是精确度和召回率的加权平均值。


不过,我们确实设置了一个函数,让我们查看每个标签的准确率,而不是平均值。当你有很多标签,而不是只有两个标签时,这一点就更为重要。


from sklearn.preprocessing import LabelEncoder
from sklearn.metrics import accuracy_score, confusion_matrix
import numpy as np
label_encoder = LabelEncoder()
label_encoder.fit(unique_labels)
def per_label_accuracy(y_true, y_pred, labels):
    cm = confusion_matrix(y_true, y_pred, labels=labels)
    correct_predictions = cm.diagonal()
    label_totals = cm.sum(axis=1)
    per_label_acc = np.divide(correct_predictions, label_totals, out=np.zeros_like(correct_predictions, dtype=float), where=label_totals != 0)
    return dict(zip(labels, per_label_acc))


我们还设置了通用计算度量函数。我在这里使用了所有这些度量,因为这是我为任何文本分类器准备的通用模板,但你也可以决定自己需要哪些度量。


from sklearn.metrics import accuracy_score, recall_score, precision_score, f1_score
def compute_metrics(pred):
    labels = pred.label_ids
    preds = pred.predictions.argmax(-1)
    decoded_labels = label_encoder.inverse_transform(labels)
    decoded_preds = label_encoder.inverse_transform(preds)
    precision = precision_score(decoded_labels, decoded_preds, average='weighted')
    recall = recall_score(decoded_labels, decoded_preds, average='weighted')
    f1 = f1_score(decoded_labels, decoded_preds, average='weighted')
    acc = accuracy_score(decoded_labels, decoded_preds)
    labels_list = list(label_encoder.classes_)
    per_label_acc = per_label_accuracy(decoded_labels, decoded_preds, labels_list)
    per_label_acc_metrics = {}
    for label, accuracy in per_label_acc.items():
        label_key = f"accuracy_label_{label}"
        per_label_acc_metrics[label_key] = accuracy
    return {
        'accuracy': acc,
        'f1': f1,
        'precision': precision,
        'recall': recall,
        **per_label_acc_metrics
    }


一旦您感到非常满意,我们就可以开始设置训练参数和训练器了。


训练模型

接下来我们设置训练参数。在这里,你可以调整历时、批量大小和学习率。


from transformers import Trainer, TrainingArguments
training_args = TrainingArguments(
    output_dir=your_path,
    num_train_epochs=3,
    warmup_steps=500,
    per_device_train_batch_size=16,
    per_device_eval_batch_size=16,
    weight_decay=0.01,
    logging_steps=10,
    evaluation_strategy='steps',
    eval_steps=100,
    learning_rate=2e-5,
    save_steps=1000,
    gradient_accumulation_steps=2
)


现在,我们可以继续设置训练器,并运行我们准备好的一切。


trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=dataset_encoded['train'],'train'],
    eval_dataset=dataset_encoded['test'],
    compute_metrics=compute_metrics,
    tokenizer=tokenizer,
    data_collator=data_collator,
)
trainer.train()


在训练时,需要注意过度拟合。由于训练数据集和评估数据集都是合成的,过度拟合的典型迹象可能并不明显。


请密切关注训练数据集和评估数据集的准确率和损失。也就是说,如果训练和验证的损失非常低,同时评估指标过于突出,就可能是过度拟合的迹象。


下面是我的一次运行结果。


16


从训练指标中可以看出,它们有点太好了。验证损失也在波动。这可能是一个非常不好的信号,所以你必须确保模型完成训练后在真实数据上进行测试。


如果你正在训练一个包含多个类别的模型,甚至可能是一个倾斜的数据集,那么如果平均评估指标不是很好,也不用担心。请查看每个标签的指标。


评估模型

完成训练后,可以运行最终评估指标,保存模型,然后保存状态。这将为您将模型推送到模型页面的中心建立指标。


trainer.evaluate()
trainer.save_model(your_path)
trainer.save_state()


现在,您可以在笔记本中运行 HuggingFace 管道进行测试。


from transformers import pipeline
pipe = pipeline('text-classification', model=your_path)


example_titles = [
    "grab an example title","grab an example title",
    "grab another example title",
    "and another xample title"
]
for title in example_titles:
    result = pipe(title)
    print(f"Title: {title}")
    print(f"Output: {result[0]['label']}")


我的产品在测试数据上表现不错,但它漏掉了几篇我个人认为是点击诱饵的文章。对于生产用例,最好建立一个更多样化的数据集(尤其是合成数据),这样它才能在新的真实数据上表现良好。


尽管如此,如果你不满意,那就回到数据集,重做数据集或尝试使用不同的模型。


测试模型

在推送模型之前,还可以对照其他替代方案测试模型。


我让 GPT-3.5 告诉我它认为哪些标题是点击诱饵,哪些是事实,它做得非常好,这是意料之中的,因为它比 Albert 大 1000 多倍。


我们还可以将一些标题与微调 FastText 所说的内容和微调变压器编码器模型进行比较。


17


使用 FastText 非常简单,计算效率高,但它孤立地处理单词,缺乏对上下文的深入理解。


因此,FastText 无法像基于转换器的模型那样捕捉语言的上下文和细微差别。


推送到Hub

如果您对自己的模型感到满意,可以将其推送到 HuggingFace 中枢进行存储。


您只需使用写令牌登录即可,该令牌可在设置下的 HuggingFace 帐户中找到。


!huggingface-cli login


然后推动它。


tokenizer.push_to_hub("username/classify-clickbait")"username/classify-clickbait")
trainer.push_to_hub("username/classify-clickbait")


结论

如果我们使用真实数据,模型是否会表现得更好?有可能,但准确率可能会更低,除非数据集经过细致分类。

文章来源:https://medium.com/towards-data-science/fine-tune-smaller-transformer-models-text-classification-77cbbd3bf02b
欢迎关注ATYUN官方公众号
商务合作及内容投稿请联系邮箱:bd@atyun.com
评论 登录
写评论取消
回复取消