总计划
使用BERT微调一个情感分析模型(电影评论分类)
🧠 理解BERT微调
BERT是一个强大的预训练语言模型。微调(Fine-tuning)是指在预训练好的BERT模型基础上,用我们自己的数据(如电影评论)进行额外训练,使其适应特定任务(如情感分析)。这个过程就像让一个已经学富五车的学者专门去学习电影评论这门课,效率远比从头培养一个新手要高。
1.环境准备
工具介绍到PyTorch或TensorFlow、Transformers库,这三样,之外还要确保NVIDIA显卡已安装CUDA:
| 工具 | 层级 | 类比说明 |
|---|---|---|
| PyTorch / TensorFlow | 底层框架 | 相当于“深度学习操作系统”,提供张量计算和自动微分 |
| Transformers | 领域库 | 基于框架的 NLP 专用工具包(依赖 PyTorch/TF) |
| Hugging Face | 生态系统 | 包含 Transformers 库,同时提供模型社区、部署平台等 |
CUDA:Compute Unified Device Architecture,是NVIDIA推出的并行计算平台和编程模型,它允许开发者使用NVIDIA GPU进行通用计算,大幅加速计算密集型任务,包括AI模型训练和推理。
##检查显卡驱动版本是否含有CUDA
nvidia-smi
# 安装 torch 对应 CUDA 12.1版本 NVIDIA显卡需提前安装CUDA驱动,高版本python不支持安装报错,3.9.x版本正常安装
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
# 安装 Transformers
pip install transformers torch pandas scikit-learn
验证以上安装脚本:
import torch
import transformers
import numpy as np
import pandas as pd
from sklearn import __version__ as sklearn_version
print(f"PyTorch版本: {torch.__version__}")
print(f"CUDA是否可用: {torch.cuda.is_available()}")
if torch.cuda.is_available():
print(f"GPU: {torch.cuda.get_device_name(0)}")
print(f"CUDA版本: {torch.version.cuda}")
print(f"GPU型号: {gpu_props.name}")
print(f"显存大小: {gpu_props.total_memory / 1024**3:.1f} GB")
print(f"CUDA核心数: {gpu_props.multi_processor_count}")
print(f"Transformers版本: {transformers.__version__}")
print(f"NumPy版本: {np.__version__}")
print(f"Pandas版本: {pd.__version__}")
print(f"Scikit-learn版本: {sklearn_version}")
# 测试BERT分词器
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
text = "Hello, BERT!"
tokens = tokenizer(text, return_tensors='pt')
print(f"\n测试分词: {text}")
print(f"Token IDs: {tokens['input_ids']}")
确保python版本受torch支持,否则无法成功下载torch的CUDA版本,比如我就是用了3.13版本python一直提示错误无法找到torch cu121即是gpu 12.1版本,降低python到13.9才解决,确定使用gpu版本torch无问题会输出如下:
2.数据准备
【token】:一个电影评论的token数量 = 评论长度(有几个词)* 系数(一般为2.5-3.5)
系数不是固定的,它取决于:分词器类型(BERT、GPT等不同);文本内容:(专业术语、缩写、符号);语言(英文系数低1.2-1.5,中文系数高2.0-2.5)
【欠拟合/过拟合】:好学生理解知识点,能解题(恰当拟合)、过拟合学生:只会死记硬背原题(过拟合)、欠拟合学生:根本没学会知识点,原题都不会做(欠拟合),欠拟合是"学不会",更别说泛化能力了,过拟合是"学过头了",泛化能力差,只认得训练数据,先解决欠拟合(让模型能学会),再防止过拟合(让模型不过度学习)
【分类标记】
[CLS] 分类标记:永远在句子的开头[SEP] 分隔标记:标记句子边界、在句子对任务中分割句子A和句子B[PAD] 填充标记:padding操作中填空的"占位符"
安装datasets库,获取训练数据:
pip install datasets
接下来要做的:
加载数据加载分词器分词函数应用分词创建数据加载器
# 极简数据准备
from datasets import load_dataset
from transformers import BertTokenizer, DataCollatorWithPadding
from torch.utils.data import DataLoader
# 1. 加载数据
dataset = load_dataset("imdb")
# 2. 加载分词器
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
# 3. 分词函数
def tokenize_function(examples):
return tokenizer(examples["text"], truncation=True, max_length=128)
# 4. 应用分词
tokenized_datasets = dataset.map(tokenize_function, batched=True)
# 5. 创建数据加载器
data_collator = DataCollatorWithPadding(tokenizer=tokenizer)
train_loader = DataLoader(
tokenized_datasets["train"],
shuffle=True,
batch_size=16,
collate_fn=data_collator
)
val_loader = DataLoader(
tokenized_datasets["test"],
batch_size=16,
collate_fn=data_collator
)
print("数据准备完成!")
print(f"训练集: {len(tokenized_datasets['train'])} 条")
print(f"测试集: {len(tokenized_datasets['test'])} 条")
2.1:加载数据
dataset = load_dataset("imdb")
做了什么:
- 从
Hugging Face Hub下载IMDb数据集 - 自动分割为
train(25000条)和test(25000条) - 每条数据包含:
{"text": "电影评论内容", "label": 0/1}
内存的数据结构:
DatasetDict({
train: Dataset({
features: ['text', 'label'],
num_rows: 25000
}),
test: Dataset({
features: ['text', 'label'],
num_rows: 25000
})
})
2.2:加载分词器
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
做了什么:
- 加载
BERT专用的"词典"和"分词规则" bert-base-uncased:小写版本(全部转小写)
2.3:分词函数
def tokenize_function(examples):
return tokenizer(
examples["text"],
truncation=True, # 太长就截断
max_length=128, # 最大128个token
# padding='max_length' # 注意:这里没有padding!
)
做了什么:
truncation=True:评论可能很长,只取前128个token- 没有设置
padding:因为不同评论长度不同,批处理时才padding - 返回的是字典,包含:
{
'input_ids': [101, 1045, 2293, ..., 102], # token IDs
'token_type_ids': [0, 0, 0, ..., 0], # 句子类型(单句全0)
'attention_mask': [1, 1, 1, ..., 1] # 哪些是真实token
}
2.4:应用分词
tokenized_datasets = dataset.map(tokenize_function, batched=True)
做了什么:
batched=True:一次处理多个样本,比循环快10-100倍- 自动并行处理
- 内存更高效
这一步主要将原始数据结构进行分词处理,生成包含input_ids、token_type_ids、attention_mask字段的数据结构:
input_ids- 文本的数字编码:如
[101, 1045, 2293, 102]= [CLS] I love [SEP]
token_type_ids- 句子类型标识(单句=0,句子对=0/1)
attention_mask- 注意力掩码:标识哪些是真实token(1),哪些是padding(0)
2.5:创建数据加载器
data_collator = DataCollatorWithPadding(tokenizer=tokenizer)
train_loader = DataLoader(
tokenized_datasets["train"],# 训练集数据集加载器
shuffle=True,# 打乱顺序,避免模型记住顺序
batch_size=16, # 16个样本一批
collate_fn=data_collator# 用collator处理padding
)
val_loader = DataLoader(# 测试集数据集加载器
tokenized_datasets["test"],
batch_size=16,
collate_fn=data_collator
)
DataLoader在训练中起到的作用:批处理(16个样本一组)、打乱顺序(避免过拟合)、并行加载(多进程读取数据)、自动padding(通过collate_fn)
# 假设一个batch有3个样本,长度不同:
batch = [
{'input_ids': [101, 1045, 102], ...}, # 长度3
{'input_ids': [101, 1045, 2293, 102], ...}, # 长度4
{'input_ids': [101, 1045, 2293, 2023, 102], ...} # 长度5
]
# DataCollator处理后:
# 所有都padding到最长长度5
batch_padded = {
'input_ids': [
[101, 1045, 102, 0, 0],
[101, 1045, 2293, 102, 0],
[101, 1045, 2293, 2023, 102]
],
'attention_mask': [
[1, 1, 1, 0, 0],
[1, 1, 1, 1, 0],
[1, 1, 1, 1, 1]
]
}
padding操作就是把token不同长度句子与批处理组内最长的句子对齐,并用注意力掩码进行标记补全的是哪个是假的token。
数据预处理
模型设置
模型训练
【模型的概率描述】 世界是模糊的、概率性的,一句话在AI用来判断的标准只能是量化的概率性数据
【模型天生偏向】 世界也是随机的、不确定的,AI初始化之后就一个向量就会随机偏向某个的位置,初始化后的不同AI对某件事有不同的偏向。只要没有经过训练,同一个的AI在不同时间段天生自带偏向会一直偏向某个方向,深度学习的本质就是从随机性中学习确定性。
在训练之前我需要对未训练的Bert进行一次评估,方便在训练后对照观察提升的效果:
# test_pretrained_bert.py
import torch
from transformers import BertForSequenceClassification, BertTokenizer
import numpy as np
def comprehensive_bert_test():
"""全面测试原始BERT模型"""
print("=" * 70)
print("BERT原始模型全面测试")
print("=" * 70)
# 1. 加载模型
print("\n1. 加载原始BERT模型...")
model = BertForSequenceClassification.from_pretrained(
'bert-base-uncased',
num_labels=2
)
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
# 2. 测试基础功能
print("\n2. 测试基础文本理解(非情感分析)...")
test_texts = [
"The cat sat on the mat.",
"Paris is the capital of France.",
"Apple is a fruit and also a company."
]
model.eval()
with torch.no_grad():
for text in test_texts:
inputs = tokenizer(text, return_tensors='pt', truncation=True, max_length=128)
outputs = model(**inputs)
logits = outputs.logits
# 计算"确定性"分数(两个类别概率的差异)
probs = torch.softmax(logits, dim=1)
confidence_diff = abs(probs[0][0] - probs[0][1])
print(f"\n 文本: '{text}'")
print(f" 输出logits: [{logits[0][0]:.3f}, {logits[0][1]:.3f}]")
print(f" 概率: [负面={probs[0][0]:.2%}, 正面={probs[0][1]:.2%}]")
print(f" 确定性差异: {confidence_diff:.2%}")
# 3. 情感分析特定测试
print("\n" + "=" * 70)
print("3. 情感分析能力测试(应该是随机的)")
print("=" * 70)
sentiment_pairs = [
("I love this amazing fantastic wonderful movie!", "极度正面"),
("This is horrible terrible awful disgusting trash.", "极度负面"),
("It's okay, not bad but not great either.", "中性"),
("The acting was good but the plot was bad.", "混合"),
]
print("\n 说明:原始BERT未经情感分析训练,预测应是随机的")
print(" (如果看起来合理,那只是巧合)")
results = []
with torch.no_grad():
for text, true_sentiment in sentiment_pairs:
inputs = tokenizer(text, return_tensors='pt', truncation=True, max_length=128)
outputs = model(**inputs)
probs = torch.softmax(outputs.logits, dim=1)
prediction = "正面" if probs[0][1] > 0.5 else "负面"
confidence = max(probs[0][0], probs[0][1]).item()
results.append({
"text": text[:50] + "..." if len(text) > 50 else text,
"true": true_sentiment,
"pred": prediction,
"conf": confidence,
"probs": [probs[0][0].item(), probs[0][1].item()]
})
# 打印结果表格
print("\n {:<50} {:<10} {:<10} {:<10} {:<20}".format(
"文本", "真实", "预测", "置信度", "概率分布"))
print(" " + "-" * 100)
for r in results:
print(" {:<50} {:<10} {:<10} {:<10.1%} 负面:{:.1%} 正面:{:.1%}".format(
r["text"], r["true"], r["pred"], r["conf"], r["probs"][0], r["probs"][1]
))
# 4. 测试随机性
print("\n" + "=" * 70)
print("4. 随机性测试")
print("=" * 70)
print("\n 多次预测同一文本:")
test_text = "This is a test sentence."
predictions = []
with torch.no_grad():
for i in range(5):
inputs = tokenizer(test_text, return_tensors='pt')
outputs = model(**inputs)
probs = torch.softmax(outputs.logits, dim=1)
pred = "正" if probs[0][1] > 0.5 else "负"
predictions.append(pred)
print(f" 第{i + 1}次: {pred} (负面={probs[0][0]:.2%}, 正面={probs[0][1]:.2%})")
print(f"\n 一致性: {'一致' if len(set(predictions)) == 1 else '不一致'}")
# 5. 分析模型输出分布
print("\n" + "=" * 70)
print("5. 输出分布分析")
print("=" * 70)
num_samples = 100
fake_inputs = torch.randint(0, tokenizer.vocab_size, (num_samples, 10)) # 随机输入
attention_mask = torch.ones_like(fake_inputs)
with torch.no_grad():
outputs = model(fake_inputs, attention_mask=attention_mask)
logits = outputs.logits
print(f"\n 对{num_samples}个随机输入的统计:")
print(f" logits均值: [{logits[:, 0].mean():.3f}, {logits[:, 1].mean():.3f}]")
print(f" logits标准差: [{logits[:, 0].std():.3f}, {logits[:, 1].std():.3f}]")
print(f" 预测正面比例: {(logits[:, 1] > logits[:, 0]).sum().item() / num_samples:.1%}")
# 6. 结论
print("\n" + "=" * 70)
print("结论")
print("=" * 70)
print("\n✅ 原始BERT模型状态:")
print(" 1. 有强大的语言理解能力(在预训练中学到)")
print(" 2. 但没有情感分析任务的知识")
print(" 3. 分类头是随机初始化的")
print(" 4. 对情感分析的表现应该是随机的(≈50%准确率)")
print("\n🎯 微调的目的:")
print(" 用IMDb数据训练分类头,让BERT学会情感分析")
print(" 保持BERT的语言理解能力,调整分类头权重")
if __name__ == "__main__":
comprehensive_bert_test()
测试结果:
测试基础文本理解(非情感分析)...
文本: 'The cat sat on the mat.'
输出logits: [0.488, -0.332]
概率: [负面=69.43%, 正面=30.57%]
确定性差异: 38.85%
文本: 'Paris is the capital of France.'
输出logits: [0.617, -0.494]
概率: [负面=75.22%, 正面=24.78%]
确定性差异: 50.45%
文本: 'Apple is a fruit and also a company.'
输出logits: [0.581, -0.269]
概率: [负面=70.04%, 正面=29.96%]
确定性差异: 40.09%
3. 情感分析能力测试(应该是随机的)
说明:原始BERT未经情感分析训练,预测应是随机的
(如果看起来合理,那只是巧合)
文本 真实 预测 置信度 概率分布
----------------------------------------------------------------------------------------------------
I love this amazing fantastic wonderful movie! 极度正面 负面 70.0% 负面:70.0% 正面:30.0%
This is horrible terrible awful disgusting trash. 极度负面 负面 66.9% 负面:66.9% 正面:33.1%
It's okay, not bad but not great either. 中性 负面 64.4% 负面:64.4% 正面:35.6%
The acting was good but the plot was bad. 混合 负面 69.7% 负面:69.7% 正面:30.3%
4. 随机性测试
多次预测同一文本:
第1次: 负 (负面=66.83%, 正面=33.17%)
第2次: 负 (负面=66.83%, 正面=33.17%)
第3次: 负 (负面=66.83%, 正面=33.17%)
第4次: 负 (负面=66.83%, 正面=33.17%)
第5次: 负 (负面=66.83%, 正面=33.17%)
一致性: 一致
5. 输出分布分析
对100个随机输入的统计:
logits均值: [0.186, -0.120]
logits标准差: [0.072, 0.068]
预测正面比例: 0.0%
怎么来分析初始模型无法判断样本情绪倾向呢?测试脚本提供了几个示例句子,通过模型给出的概率来计算确定性分数,这个偏向就是模型初始化之后天生自带的偏向:
文本: 'Paris is the capital of France.'
概率: [负面=75.22%, 正面=24.78%]
# 计算确定性:
certainty = abs(0.7522 - 0.2478) = 0.5044 = 50.44%
# 这就是你的"确定性差异: 50.45%"
# 情况1:非常确定
概率: [90%, 10%]
确定性 = |0.9 - 0.1| = 0.8 = 80%
→ 模型很自信
# 情况2:比较确定
概率: [70%, 30%]
确定性 = |0.7 - 0.3| = 0.4 = 40%
→ 模型比较自信
# 情况3:不确定
概率: [55%, 45%]
确定性 = |0.55 - 0.45| = 0.1 = 10%
→ 模型很犹豫
# 情况4:完全随机
概率: [50%, 50%]
确定性 = |0.5 - 0.5| = 0 = 0%
→ 模型完全不确定
无训练的模型问题:
概率分布:[70%, 30%](偏向负面)
确定性:40%(过度自信)
期望的随机模型:概率≈[50%, 50%],确定性≈0%
微调会改善:
校准概率:正面句子→高正面概率,负面句子→高负面概率
合理确定性:该确定时确定,该不确定时不确定
消除偏见:不再系统偏向负面
数据显示:原始BERT虽然随机,但过度自信且有偏见。这正是需要微调的原因!准确率从≈50%提升到>90%、确定性变得合理校准、偏见被消除。
记住:一个好的模型不仅要正确,还要知道什么时候可能出错,模型应知道自己不确定。这才是真正"智能"的体现。