微调BERT检测假新闻

在当今信息过载的时代,区分真实和虚假新闻比以往任何时候都更加重要。在这篇指南中,我将带领你完成一个基于BERT的文本分类器的端到端实现,使用的是WELFake数据集,全部在Google Colab中进行,设置最少。这旨在帮助新手对Hugging Face生态系统和NLP管道感到自信。

1、挂载Google Drive

from google.colab import drive  
drive.mount('/content/drive')

这让你可以直接将文件加载/保存到Google Drive。

2、安装所需库

由于你在使用Google Colab,许多重要的库如transformersdatasetstqdm已经预先安装好了。但如果它们版本过旧或缺失,你可能仍然需要安装或升级特定的库。

!pip install --upgrade tqdm  
!pip install evaluate

tqdm用于进度条,evaluate用于计算准确率和AUC等指标。

3、导入必要的库

import numpy as np              # 用于数值操作,如softmax或AUC  
import pandas as pd            # 用于读取和处理CSV或表格数据  
import re                      # 用于使用正则表达式清理文本  
import torch                   # 用于使用PyTorch并启用GPU支持  
from datasets import DatasetDict, Dataset  # 用于组织和分割数据集  

from transformers import (       
    AutoTokenizer,                        # 加载适合模型的分词器  
    AutoModelForSequenceClassification,  # 加载BERT用于分类任务  
    TrainingArguments,                   # 定义训练设置(批量大小、学习率等)  
    Trainer,                             # 处理训练、评估和保存  
    DataCollatorWithPadding,            # 自动为批次添加填充  
    EarlyStoppingCallback               # 如果模型停止改进,则停止训练  
)  

import evaluate                # 用于计算准确率、AUC、F1和其他指标

4、加载和清理数据集

# 📥 从Google Drive加载数据集  
DF = pd.read_csv("/content/drive/MyDrive/WELFake_Dataset.csv")  

# 🧹 删除不必要的'Unnamed: 0'列(自动生成的索引)  
DF.drop(["Unnamed: 0"], axis=1, inplace=True)  

# 🧼 删除任何包含缺失值(空值)的行  
DF.dropna(inplace=True)  

# 🧩 将'title'和'text'列合并成单个'Content'列  
DF["Content"] = DF["title"] + " " + DF["text"]  

# 🗑️ 删除原始'title'和'text'列——我们只需要'Content'  
DF.drop(["title", "text"], axis=1, inplace=True)

在这个阶段,我们通过删除不必要的列并将'title'和'text'合并到单个'Content'列中完成了初步的数据清理。但是,数据集尚未完全准备好。在接下来的步骤中,我们将删除重复项、检查类别平衡,并执行任何其他必要的清理工作以准备数据进行分词和微调。适当的预处理对于确保模型从高质量、无噪声的输入中学习至关重要。

5、清理和去重文本

# 🧼 定义一个简单的文本清理函数  
def clean(text):  
    if not isinstance(text, str):  
        return ""  # 如果输入不是字符串,则返回空字符串  
    text = text.lower()  # 将所有文本转换为小写  
    text = re.sub(r'[^a-z0-9\s]', '', text)  # 删除标点符号和特殊字符  
    text = re.sub(r'\s+', ' ', text).strip()  # 替换多余的空格为单个空格并去除首尾空白  
    return text  

# 🧹 将清理函数应用于'Content'列  
DF["Content"] = DF["Content"].apply(clean)  

# 🚫 删除清理过程中可能生成的任何空行  
DF = DF[DF["Content"].str.strip() != ""]  

# 🧭 基于'Content'列删除重复的文章  
# 这有助于确保模型不会多次学习相同的信息  
DF = DF.drop_duplicates(subset="Content").reset_index(drop=True)

为了进一步清理数据,我们创建了一个简单的函数来将文本转换为小写、删除特殊字符并减少多余空格。在将其应用于'Content'列后,我们删除了任何变成空的行,并删除了重复项以避免模型多次训练相同的新闻文章。这些步骤有助于确保我们的模型从干净、唯一且有意义的文本中学习。

6、转换为Hugging Face数据集

DF_dataset = Dataset.from_pandas(DF)

在清理完DataFrame后,我们将其转换为Hugging Face的Dataset对象。这种格式针对Hugging Face的Trainer、分词器和数据管道进行了优化。它使得应用转换、拆分数据和高效训练模型变得更加容易——特别是在处理大型数据集时。可以将其视为将数据转换为Hugging Face工具理解的最佳格式。

7、定义评估指标

# 📏 从Hugging Face加载内置的度量计算器  
accuracy = evaluate.load("accuracy")  
auc_score = evaluate.load("roc_auc")  

# 📊 定义一个函数在评估期间计算指标  
def compute_metrics(eval_pred):  
    predictions, labels = eval_pred  # 解包模型预测和真实标签  

    # 🔁 使用softmax将logits转换为概率  
    probabilities = np.exp(predictions) / np.exp(predictions).sum(-1, keepdims=True)  
    positive_class_probs = probabilities[:, 1]  # 保留正类的概率  

    # 🧮 计算AUC(模型分离类别的程度)  
    auc = auc_score.compute(prediction_scores=positive_class_probs, references=labels)['roc_auc']  

    # ✅ 计算准确率(预测与标签匹配的频率)  
    acc = accuracy.compute(predictions=np.argmax(predictions, axis=1), references=labels)['accuracy']  

    # 返回四舍五入后的分数以便易于阅读  
    return {"accuracy": round(acc, 3), "auc": round(auc, 3)}

为了评估模型的表现,我们定义了一个compute_metrics函数。它计算两个关键指标:

准确率:预测正确的百分比。

AUC(接收者操作特征曲线下面积):一个分数,显示模型分离两类的能力。AUC越高,分离能力越强。

首先使用softmax函数将预测转换为概率,然后提取正类的概率。此函数被传递给Hugging Face的Trainer,后者会在每次评估步骤后自动调用它以记录模型的性能。

8、加载BERT模型和分词器

model_path = "google-bert/bert-base-uncased"  
tokenizer = AutoTokenizer.from_pretrained(model_path)  
model = AutoModelForSequenceClassification.from_pretrained(  
    model_path, num_labels=2,  
    id2label={0: "Fake", 1: "Real"},  
    label2id={"Fake": 0, "Real": 1}  
)

model_path = "google-bert/bert-base-uncased是您要使用的预训练模型的名称。

  • "bert-base-uncased"是一个标准的BERT模型,有1.1亿个参数。
  • 它被称为uncased,因为它忽略大小写(例如,“News”和“news”被视为相同)。
  • google-bert/前缀表示它是Google上传到Hugging Face Hub的。

tokenizer = AutoTokenizer.from_pretrained(model_path)

这加载与所选BERT模型匹配的分词器

为什么这很重要:

  • 分词器将您的文本分成标记(如单词或子词)。
  • 然后,它们将这些标记转换为数字(输入ID),模型可以理解。
  • 使用AutoTokenizer确保它选择了正确的类型(BERT使用WordPiece)。

提示:始终使用与预训练模型匹配的分词器——否则,您可能会给模型提供它未受过训练的内容。

model = AutoModelForSequenceClassification.from_pretrained(...)

这加载预训练的BERT模型,并在顶部添加一个分类层——适合任务如假新闻检测。

num_labels=2:

告诉模型这是一个二元分类

{"Fake" : 0 , "Real" : 1}

id2label={0: "Fake", 1: "Real"}

这提供了从类ID(数字)到标签(文本)的映射
它在打印预测时使模型输出更易读。

label2id={"Fake": 0, "Real": 1}

这是反向映射——它告诉模型如何在训练期间将文本标签转换回数字。

9、冻结大部分BERT

#冻结所有层  
for param in model.bert.parameters():  
    param.requires_grad = False  
#解冻最后两层  
for param in model.bert.encoder.layer[-2:].parameters():  
    param.requires_grad = True

为什么要冻结BERT层?

当使用像BERT这样的大型预训练模型时,早期层已经从像维基百科这样的大型数据集中学习了通用语言模式。这些层已经擅长理解语法、意义和结构。

因此,与其从头开始重新训练整个模型(这既慢又需要更多数据),我们可以冻结这些层,只专注于微调最后几层以适应我们的特定任务——在这种情况下是假新闻检测。

这有助于:

  • 加快训练速度
  • 防止在小数据集上过拟合
  • 使用更少的计算资源

为什么解冻最后两层?

BERT的较深层(接近末尾)捕获更多的任务特定信息

通过仅解冻最后两层:

  • 您让模型学习与您的数据集相关的特征
  • 但仍然保持早期层稳定,以保留通用语言知识

这是一种常见的技术称为部分微调——它在性能效率之间取得平衡。

什么时候解冻更多层?

如果您有更大的数据集,任务与BERT的预训练非常不同(例如法律/医学领域),或者您看不到仅使用1-2层的好验证准确性,则可以逐渐解冻更多层。

但要小心:过早解冻太多会导致训练时间更长过拟合

10、对数据进行分词

def preprocess_function(examples):  
    return tokenizer([str(x) for x in examples["Content"]], truncation=True, padding=True, max_length=512)  

tokenized_data = DF_dataset.map(preprocess_function, batched=True)  
data_collator = DataCollatorWithPadding(tokenizer=tokenizer)

在将文本馈送给BERT之前,需要对其进行分词——这意味着将原始文本转换为模型能够理解的数值输入。

  • preprocess_function()接受每个示例的"Content"字段,并使用BERT分词器应用:
  • truncation=True:将长文章截断以适应BERT的512令牌限制。
  • padding=True:为较短的示例添加填充,使其大小一致。
  • max_length=512:确保所有序列都被限制在512个令牌以内,这是BERT能处理的最大长度。

我们使用.map()将此函数应用于整个数据集——这在批处理中高效运行。

然后,我们使用DataCollatorWithPadding确保在形成训练批次时,它们被正确填充——节省内存并确保GPU性能顺畅。

11、训练/验证/测试分割

train_test = tokenized_data.train_test_split(test_size=0.2, seed=42)  
val_test = train_test["test"].train_test_split(test_size=0.5, seed=42)  

final_dataset = DatasetDict({  
    "train": train_test["train"],          # 80%  
    "validation": val_test["train"],       # 10%  
    "test": val_test["test"]               # 10%  
})

一旦数据被分词,我们需要将其分为三部分:

  • 训练集:用于教模型
  • 验证集:用于在训练期间检查性能
  • 测试集:用于在训练结束后评估性能(最终得分)

12、定义训练参数

training_args = TrainingArguments(  
    output_dir="/content/drive/MyDrive/my_bert_model",  
    evaluation_strategy="epoch",  
    save_strategy="epoch",  
    learning_rate=2e-5,  
    per_device_train_batch_size=8,  
    per_device_eval_batch_size=8,  
    num_train_epochs=5,  
    weight_decay=0.01,  
    warmup_ratio=0.1,  
    load_best_model_at_end=True,  
    metric_for_best_model="eval_accuracy",  
    greater_is_better=True,  
    logging_steps=50,  
    fp16=True  
)

想想TrainingArguments就像模型的训练“设置面板”:

以下是完整分解:

13、训练模型

trainer = Trainer(  
    model=model,  
    args=training_args,  
    train_dataset=final_dataset["train"],  
    eval_dataset=final_dataset["validation"],  
    tokenizer=tokenizer,  
    data_collator=data_collator,  
    compute_metrics=compute_metrics,  
    callbacks=[EarlyStoppingCallback(early_stopping_patience=2)],  
)  

trainer.train()

Trainer类通过将所有组件——模型、数据、分词器、度量和配置——组合成单一、易于使用的对象,简化了训练过程。只需调用.train(),它就会处理其余部分!”

trainer.train(resume_from_checkpoint=True)

这一行代码将在你的output_dir中查找最新的保存检查点,并自动恢复训练。

14、在测试集上评估

eval_results = trainer.evaluate(eval_dataset=final_dataset["test"])  
print(eval_results)

15、绘制AUC曲线和混淆矩阵

from sklearn.metrics import roc_curve, auc, confusion_matrix, ConfusionMatrixDisplay  
import matplotlib.pyplot as plt  

y_true = final_dataset["test"]["label"]  
predictions = trainer.predict(final_dataset["test"])  
probs = np.exp(predictions.predictions) / np.exp(predictions.predictions).sum(-1, keepdims=True)  
y_scores = probs[:, 1]  
y_pred = np.argmax(predictions.predictions, axis=1)  

# ROC曲线  
fpr, tpr, _ = roc_curve(y_true, y_scores)  
plt.plot(fpr, tpr, label=f"AUC = {auc(fpr, tpr):.2f}")  
plt.plot([0, 1], [0, 1], linestyle='--')  
plt.title("ROC Curve")  
plt.xlabel("FPR")  
plt.ylabel("TPR")  
plt.legend()  
plt.grid()  
plt.show()  

# 混淆矩阵  
cm = confusion_matrix(y_true, y_pred)  
ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=["Fake", "Real"]).plot(cmap="Blues")  
plt.title("Confusion Matrix")  
plt.grid(False)  
plt.show()

16、学到的经验

  1. 仅微调BERT的最后两层可以在清洁、平衡良好的数据集上获得强大的结果
  2. 简单的文本清理(如小写化、删除特殊字符和去重)在提升模型性能方面大有裨益。
  3. 不要仅仅依赖准确率——使用AUC和混淆矩阵等指标可以给出更清晰、更可信的评估。
  4. Hugging Face的Trainer API非常初学者友好。它处理分词、训练、评估和保存时几乎没有样板代码。
  5. 然而,模型在未见过的不同来源数据上可能无法很好地泛化——即使经过彻底的预处理和类别平衡。

17、为什么在新数据集上准确率下降?

当我测试模型在一个完全不同来源的假新闻数据集上时,即使没有数据泄露、没有重复、类别平衡良好且预处理得当,准确率也明显下降。

这并不罕见。通常发生的原因是:

  1. 不同的写作风格或模型在训练期间未见过的来源
  2. 模型学会了特定于原始数据集的模式,而不是普遍真理
  3. 即使像BERT这样强大的模型也需要更多的数据多样性才能很好地泛化

18、最终想法

这个项目展示了,通过最小的设置,您可以从原始新闻文章到一个稳健的假新闻分类器,使用Hugging Face的强大工具和一些简单的预处理。

即使仅微调BERT的最后几层,我们在一个清理良好、平衡的数据集上也取得了令人印象深刻的结果——展示了迁移学习在NLP中的力量。

然而,在测试相同模型时,在一个完全不同来源的数据集上,准确率显著下降。这是一个常见且重要的观察:

这突显了一个关键教训:高验证准确率并不总是意味着强现实世界表现,尤其是在NLP任务中,数据在不同来源中差异很大。

你可以点击这里探索完整的笔记本并在Google Colab中自行运行它。


原文链接:Fake News Detection Using Fine-Tuned BERT : WELFake Dataset Analysis and Results

汇智网翻译整理,转载请标明出处