1. setup enviroment
2. prepare dataset
ruby
class IMDbDataset(torch.utils.data.Dataset): def __init__(self, encodings, labels): self.encodings = encodings self.labels = labels def __getitem__(self, idx): item = {key: torch.tensor(val[idx]) for key, val in self.encodings.items()} item["labels"] = torch.tensor(self.labels[idx]) return item def __len__(self): return len(self.labels) train_dataset = IMDbDataset(train_encodings, train_labels)
3.Tokenize the data
ini
encodings = tokenizer(
list_of_questions, # 要处理的文本列表
truncation=True, # 超长截断
padding=True, # 不足补零
max_length=128, # 最大长度
return_tensors="pt" # 返回PyTorch张量
)
1. 核心作用
把原始文本(如["How does BERT work?", "什么是机器学习?"]
)转换成BERT能理解的数字格式,包含:
- input_ids :文本→数字编号(如
[101, 1731, 1452, 102]
) - attention_mask:标记哪些是真实词(1)、哪些是填充的0(0)
- token_type_ids(可选):区分句子A/B(对问答任务有用)
2. 参数详解(附比喻)
参数 | 作用 | 比喻 |
---|---|---|
list_of_questions |
要处理的文本列表(如["问题1", "问题2"] ) |
就像待翻译的外语句子列表 |
truncation=True |
超过max_length 时自动截断 |
像剪刀裁掉过长的句子(保留前128个词) |
padding=True |
短于max_length 时补零([PAD]) |
像用空白填空使所有句子长度相同 |
max_length=128 |
每条文本最大词数(BERT通常≤512) | 规定所有句子翻译后最多128个单词 |
return_tensors="pt" |
返回PyTorch张量(不是Python列表) | 要求返回"PyTorch专用格式"的数据 |
3. 输出示例
假设输入:["How old are you?", "你好"]
yaml
python
{
'input_ids': # 文本的数字编码
tensor([
[101, 1731, 1385, 1132, 1128, 102, 0, ..., 0], # "How old are you?"
[101, 6821, 102, 0, 0, ..., 0] # "你好"
]),
'attention_mask': # 标识哪些是有效词
tensor([
[1, 1, 1, 1, 1, 1, 0, ..., 0], # 前6个是真实词,后面是填充的0
[1, 1, 1, 0, 0, ..., 0]
])
}
4. 为什么需要这些处理?
- BERT的固定输入长度:模型需要统一尺寸的输入(像电梯限载10人,不足补空,超载要裁)
- 注意力机制依赖结构 :
attention_mask
告诉模型哪些位置需要计算注意力 - GPU计算优化 :张量格式(
return_tensors="pt"
)能加速GPU并行计算
5. 常见问题
Q1:max_length
设多大合适?
- 一般设为你数据中大多数文本的长度(可通过统计得出)
- BERT最大支持512,但越长消耗显存越多
Q2:padding
补零会影响模型吗?
- 不会!因为有
attention_mask
告诉模型忽略这些填充位置
Q3:截断会丢失信息吗?
-
可能丢失长文本尾部信息,可通过以下方法缓解:
inipython tokenizer(..., truncation="longest_first") # 优先截断长句子 # 或 tokenizer(..., stride=64, return_overflowing_tokens=True) # 滑动窗口处理超长文本
6. 完整流程图示
css
原始文本 → Tokenizer分词 → 添加[CLS][SEP] → 截断/填充 → 转数字ID → 输出张量
│ │ │ │
↓ ↓ ↓ ↓
"你好" → ["[CLS]", "你", "好", "[SEP]"] → [101, 6821, 102] → 补零到128长度
只要记住:Tokenizer就像翻译官+标准化工厂,把乱七八糟的文本变成整齐划一的数字矩阵!
4.Create a Custom Dataset
在PyTorch中创建自定义Dataset类来处理BERT问答(QA)或分类任务数据是一个关键步骤。下面我将详细解释如何创建一个QADataset
类,并解释每一部分的作用。
完整代码实现
python
python
from torch.utils.data import Dataset
class QADataset(Dataset):
def __init__(self, encodings, labels):
"""
初始化数据集
:param encodings: 包含input_ids, attention_mask等的字典
:param labels: 样本对应的标签
"""
self.encodings = encodings
self.labels = labels
def __len__(self):
"""
返回数据集中的样本数量
"""
return len(self.labels)
def __getitem__(self, idx):
"""
获取单个样本
:param idx: 样本索引
:return: 包含编码和标签的字典
"""
item = {key: val[idx] for key, val in self.encodings.items()}
item['labels'] = self.labels[idx]
return item
代码逐步解释
1. 继承Dataset基类
kotlin
python
from torch.utils.data import Dataset
class QADataset(Dataset):
Dataset
是PyTorch提供的抽象基类- 继承它后必须实现
__len__
和__getitem__
方法
2. __init__
方法
ruby
python
def __init__(self, encodings, labels):
self.encodings = encodings
self.labels = labels
-
接收两个参数:
-
encodings
: 包含BERT输入特征的字典,通常有:input_ids
: 分词后的token IDsattention_mask
: 注意力掩码token_type_ids
: 区分句子A/B的标记(对于QA任务)
-
labels
: 样本标签(分类任务)或答案位置(QA任务)
-
3. __len__
方法
python
python
def __len__(self):
return len(self.labels)
- 返回数据集中的样本总数
- PyTorch的DataLoader会调用这个方法确定batch数量
4. __getitem__
方法
ruby
python
def __getitem__(self, idx):
item = {key: val[idx] for key, val in self.encodings.items()}
item['labels'] = self.labels[idx]
return item
-
根据索引
idx
返回单个样本 -
将encodings中的每个特征在idx位置的值取出
-
添加对应的标签
-
返回格式示例:
csspython { 'input_ids': tensor([101, 1731, 1452, 102]), 'attention_mask': tensor([1, 1, 1, 1]), 'token_type_ids': tensor([0, 0, 0, 0]), 'labels': 1 # 或对于QA任务可能是[start_pos, end_pos] }
如何使用这个Dataset类
1. 准备数据
ini
python
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
# 示例数据
questions = ["What is BERT?", "How does attention work?"]
answers = ["A language model", "It weights input tokens"]
# 对问题和答案进行编码
encodings = tokenizer(questions, answers, truncation=True,
padding='max_length', max_length=128,
return_tensors='pt')
# 假设是分类任务,创建虚拟标签
labels = torch.tensor([1, 0]) # 1表示正例,0表示负例
2. 创建Dataset实例
ini
python
dataset = QADataset(encodings, labels)
3. 创建DataLoader
ini
python
from torch.utils.data import DataLoader
dataloader = DataLoader(dataset, batch_size=2, shuffle=True)
4. 在训练循环中使用
ini
python
for batch in dataloader:
input_ids = batch['input_ids']
attention_mask = batch['attention_mask']
labels = batch['labels']
# 将这些输入传递给BERT模型
outputs = model(input_ids, attention_mask=attention_mask, labels=labels)
loss = outputs.loss
loss.backward()
# ...训练步骤...
针对QA任务的调整
对于问答任务,通常需要修改__init__
和__getitem__
来处理答案位置:
python
python
class QADataset(Dataset):
def __init__(self, encodings):
"""QA任务可能不需要显式labels参数"""
self.encodings = encodings
def __getitem__(self, idx):
return {key: val[idx] for key, val in self.encodings.items()}
然后编码时包含答案位置信息:
ini
python
encodings = tokenizer(questions, answers, truncation=True, padding=True,
return_tensors='pt', return_offsets_mapping=True)
# 添加答案的start/end位置
encodings['start_positions'] = ...
encodings['end_positions'] = ...
总结
- 自定义Dataset类封装了数据加载逻辑
- 必须实现
__len__
和__getitem__
方法 - 与BERT Tokenizer的输出格式配合使用
- 可以通过调整来适应分类或QA等不同任务
这种模式使数据加载与模型训练代码分离,提高了代码的可维护性和可复用性。
5 Load and Configure the Modal
这个代码的作用是 加载一个预训练的BERT模型,并为其添加一个分类层,使其能够执行特定类别数量的文本分类任务。下面我会用通俗易懂的方式解释它的具体作用和实际应用场景:
🎯 核心作用
ini
python
model = BertForSequenceClassification.from_pretrained(
"bert-base-uncased",
num_labels=5
)
-
加载预训练知识
- 从Hugging Face模型库下载
bert-base-uncased
(一个通用的英语BERT模型),这个模型已经通过海量文本学习了语言理解能力。 - 相当于你拿到一个"读过千万本书的AI大脑"。
- 从Hugging Face模型库下载
-
适配分类任务
- 通过
num_labels=5
,在BERT的原始输出层上新增一个分类器,将BERT输出的768维向量转换为5个类别的概率(比如五星评价分类、情感五分类等)。 - 相当于给AI大脑接上一个"答题卡扫描仪",让它能回答你的具体问题。
- 通过
🛠️ 实际应用场景
场景示例 | num_labels 值 |
用途 |
---|---|---|
情感分析 | 2 (正面/负面) | 判断评论是好评还是差评 |
五星评分 | 5 | 预测用户会打1-5星中的哪一星 |
新闻分类 | 10 | 将新闻归类到体育/科技/政治等10个类别 |
意图识别 | 8 | 判断用户提问是咨询/投诉/售后等8种意图 |
🔧 代码细节拆解
1. 模型选择 "bert-base-uncased"
-
bert-base: 基础版模型(12层Transformer,768隐藏层维度)
-
uncased: 不区分大小写(会把"Apple"和"apple"视为相同)
-
其他常用选项:
bashpython "bert-large-uncased" # 更大模型(24层,1024隐藏层) "bert-base-chinese" # 中文BERT
2. 分类头改造 num_labels=5
-
自动添加的结构:
scssBERT原始输出 → Dropout层 → 线性层(768输入 → 5输出)
-
如果是二分类,输出形状就是
(batch_size, 2)
,每行两个数表示两类概率(如[0.2, 0.8])
3. 与普通BERT的区别
ini
python
# 普通BERT(仅特征提取)
from transformers import BertModel
base_model = BertModel.from_pretrained("bert-base-uncased") # 输出768维向量
# 分类专用BERT(带任务接口)
from transformers import BertForSequenceClassification
cls_model = BertForSequenceClassification.from_pretrained(...) # 直接输出类别概率
📌 关键注意事项
-
必须微调(fine-tune)
预训练BERT只是"知识库",需要用你的标注数据训练分类层(甚至全部参数),例如:
inipython from transformers import Trainer, TrainingArguments trainer = Trainer( model=model, args=TrainingArguments(output_dir="./results"), train_dataset=train_data ) trainer.train()
-
输入数据需要匹配
输入文本必须用相同的tokenizer处理:
inipython from transformers import BertTokenizer tokenizer = BertTokenizer.from_pretrained("bert-base-uncased") inputs = tokenizer("Your text here", return_tensors="pt")
-
GPU加速建议
如果可用GPU,运行:
bashpython model.to("cuda") # 将模型转移到GPU
🌰 举个栗子:餐饮评论分类
假设你想建立一个模型,自动判断食评属于以下哪类:
css
python
类别映射 = {0: "差评", 1: "一般", 2: "好评", 3: "强烈推荐", 4: "完美"}
只需:
- 准备标注数据(如5000条带标签的食评)
- 用上述代码加载
num_labels=5
的模型 - 在数据上微调后,模型就能预测新评论的类别!
总结来说,这行代码是将通用的BERT模型改造成适合你具体任务的分类工具的起点。就像给瑞士军刀安装一个专用刀头,让它能完美解决你的特定问题。
6。handle class imbalance
处理类别不平衡:在损失函数中使用类别权重
在文本分类任务中,经常会遇到类别不平衡的问题(某些类别的样本数量远多于其他类别)。这会导致模型偏向多数类,影响对少数类的识别能力。使用类别权重(class weights)是解决这个问题的有效方法之一。
代码解释
ini
python
from sklearn.utils.class_weight import compute_class_weight
# 1. 计算类别权重
class_weights = compute_class_weight(
"balanced",
classes=range(num_classes),
y=train_labels
)
# 2. 转换为PyTorch张量并移至设备(GPU/CPU)
weights_tensor = torch.tensor(class_weights, dtype=torch.float).to(device)
参数说明:
"balanced"
:自动计算权重,使各类别的权重与其频率成反比classes=range(num_classes)
:所有可能的类别标签(如0,1,2,3,4)y=train_labels
:训练集中的真实标签列表
计算原理:
对于每个类别c,其权重计算为:
ini
weight_c = 总样本数 / (类别数 * 类别c的样本数)
使用方法
1. 在损失函数中应用权重
ini
python
import torch.nn as nn
# 创建带权重的交叉熵损失函数
criterion = nn.CrossEntropyLoss(weight=weights_tensor)
# 在训练循环中使用
loss = criterion(outputs, labels)
2. 完整示例
ini
python
from transformers import BertForSequenceClassification, Trainer, TrainingArguments
from sklearn.utils.class_weight import compute_class_weight
import numpy as np
import torch
# 假设我们有5个类别和训练标签
num_classes = 5
train_labels = [0, 1, 1, 2, 2, 2, 3, 4] # 示例标签(明显不平衡)
# 计算类别权重
class_weights = compute_class_weight(
"balanced",
classes=np.unique(train_labels),
y=train_labels
)
weights_tensor = torch.tensor(class_weights, dtype=torch.float)
# 自定义带权重的Trainer
class WeightedTrainer(Trainer):
def compute_loss(self, model, inputs, return_outputs=False):
labels = inputs.pop("labels")
outputs = model(**inputs)
loss = torch.nn.functional.cross_entropy(
outputs.logits,
labels,
weight=weights_tensor.to(outputs.logits.device)
)
return (loss, outputs) if return_outputs else loss
# 初始化模型和训练参数
model = BertForSequenceClassification.from_pretrained("bert-base-uncased", num_labels=num_classes)
training_args = TrainingArguments(output_dir="./results")
# 创建并运行训练器
trainer = WeightedTrainer(
model=model,
args=training_args,
train_dataset=train_dataset,
eval_dataset=eval_dataset
)
trainer.train()
注意事项
-
权重计算时机:应在划分训练/验证集后,仅基于训练集标签计算权重
-
极端不平衡情况:对于极度不平衡的数据(如1:100),可能需要结合其他技术:
- 过采样少数类(如SMOTE)
- 欠采样多数类
- 分层抽样确保每批次的类别平衡
-
验证集评估:即使训练时使用权重,验证指标仍应关注原始分布或使用宏观平均
-
多标签任务 :对于多标签分类,需要使用
BCEWithLogitsLoss
并设置pos_weight
-
权重调整:可以手动调整权重,给特别重要的类别更高权重
通过这种方法,模型在训练时会更加关注样本较少的类别,从而改善整体分类性能,特别是在需要平衡召回率和精确度的场景中。
通俗解释:用"班级投票"理解类别权重
想象你是一个老师,班上要评选"最受欢迎班干部",有5个职位候选(班长、学习委员、文体委员、生活委员、纪律委员),但学生们投票时出现了问题:
问题情景
- 班长有40个支持者(全班80人里占一半)
- 学习委员有20个支持者
- 其他三个委员各自只有5-6个支持者
如果直接按票数决定,班长肯定每次都赢,其他委员永远没机会------这就是类别不平衡问题。
解决办法:给少数派"加分"
你作为老师,决定这样调整规则:
-
计算权重:给票数少的职位额外加分
- 班长票数多 → 每票算0.5分
- 学习委员 → 每票算1分
- 其他委员 → 每票算2分(因为支持者最少)
-
重新计票:
- 班长:40票 × 0.5 = 20分
- 学习委员:20票 × 1 = 20分
- 文体委员:6票 × 2 = 12分
- (其他委员同理)
这样虽然班长原始票数最多,但经过加权后和学习委员持平,小职位也有机会被看到。
对应到AI模型
-
BERT就像全班同学:默认情况下会倾向于选择数据量大的类别(就像同学都投熟人)
-
类别权重就是加分规则:
- 让模型在训练时更关注样本少的类别
- 防止模型"偷懒"只学多数类
-
效果:就像公平的选举结果,模型会对所有类别都保持敏感
就像老师调节投票规则保证公平性,我们调节损失函数的权重来保证模型对所有类别都"一视同仁"。
7.Defined Training Parameters
通俗解释:定义训练参数(Define Training Parameters)的作用
简单来说,这一步就是告诉计算机:
"你要用什么样的方式和节奏来训练这个AI模型?"
🔍 为什么需要定义训练参数?
想象你在教一个学生做数学题:
- 学多久? → 训练多少轮(epochs)
- 一次做几道题? → 每次喂多少数据(batch size)
- 学多快? → 学习率(learning rate)
- 要不要记笔记? → 是否保存检查点(save_steps)
- 在哪里考试? → 验证集评估(evaluation_strategy)
这些设置直接影响模型的学习效果 和训练速度。
📝 常见的训练参数(以Hugging Face的TrainingArguments
为例)
ini
python
from transformers import TrainingArguments
training_args = TrainingArguments(
output_dir="./results", # 训练结果保存路径(像作业本放哪)
num_train_epochs=3, # 训练3轮(把数据集学3遍)
per_device_train_batch_size=8, # 每次用8条数据训练(一次做8道题)
per_device_eval_batch_size=16, # 验证时每次用16条数据
learning_rate=2e-5, # 学习速率(小步慢走 vs 大步快跑)
evaluation_strategy="epoch", # 每轮结束后考一次试(验证)
save_strategy="epoch", # 每轮结束后保存一次模型
logging_steps=100, # 每100步打印一次日志
load_best_model_at_end=True, # 训练完后自动加载效果最好的模型
)
💡 关键参数的作用
参数名 | 作用类比 | 典型值 |
---|---|---|
num_train_epochs |
把教材反复学几遍 | 3-5(太多会死记硬背) |
batch_size |
一次做多少题(太大显存爆炸,太小速度慢) | 8/16/32 |
learning_rate |
调整参数时的步幅(太大错过答案,太小学得慢) | 2e-5 到 5e-5 |
evaluation_strategy |
什么时候检查学习效果(随时考 or 学完一章考) | "steps"或"epoch" |
save_steps |
隔多久保存一次进度(防崩溃丢失) | 500或"epoch" |
⚠️ 注意事项
-
学习率(LR) :
- BERT通常用很小的LR(如
2e-5
),因为预训练模型已经很接近目标,只需微调。 - 太大容易"学歪",太小训练慢。
- BERT通常用很小的LR(如
-
Batch Size:
- 取决于你的GPU显存(8GB显存建议≤16)。
-
Epochs:
- 太多会导致过拟合(像学生死记硬背,但不会举一反三)。
-
验证集监控:
- 用
load_best_model_at_end
可以自动保存效果最好的版本。
- 用
8 Evaluation and Tune
简单来说:
"考试 + 改错题 + 调整学习方法"
就像老师教学生做题:
- 考试(Evaluate) → 用测试集看看模型学得怎么样
- 改错题(Analyze) → 分析哪些题错得多(比如某个类别总错)
- 调整学习方法(Tune) → 改变学习策略(比如学慢点、多练错题)
🔍 详细拆解
1️⃣ 评估(Evaluate)------ "考完试批卷子"
目的 :检查模型在没见过的数据(测试集)上表现如何。
关键指标:
- 准确率(Accuracy) :整体对了多少题
- F1分数(类别不平衡时更重要)
- 混淆矩阵:哪些类别容易混淆(比如猫狗分不清)
代码示例:
ini
python
from sklearn.metrics import accuracy_score, f1_score
# 模型预测测试集
predictions = model.predict(test_encodings)
pred_labels = predictions.logits.argmax(dim=1)
# 计算准确率和F1
accuracy = accuracy_score(test_labels, pred_labels)
f1 = f1_score(test_labels, pred_labels, average="weighted")
print(f"准确率: {accuracy:.2f}, F1分数: {f1:.2f}")
2️⃣ 调优(Tune)------ "改学习方法"
如果考得不好,可能是以下问题:
问题 | 可能原因 | 调优方法 |
---|---|---|
准确率低 | 学习不够/学过头 | 增加epochs / 减小学习率 |
某些类别总错 | 数据不平衡 | 加类别权重 / 过采样少数类 |
训练集好但测试集差 | 过拟合(死记硬背) | 加Dropout / 早停 / 数据增强 |
训练速度太慢 | Batch Size太小 / 模型太大 | 增大Batch Size / 换轻量模型 |
常见调优操作:
A. 调整超参数
ini
python
training_args = TrainingArguments(
learning_rate=5e-5, # 原先是2e-5,试试加大
per_device_train_batch_size=32, # 原先16,试试翻倍
num_train_epochs=5, # 原先3轮,加2轮
)
B. 处理数据不平衡
ini
python
# 方法1:加类别权重(少数类错误惩罚更大)
loss_fct = torch.nn.CrossEntropyLoss(weight=class_weights)
# 方法2:过采样少数类(复制数据)
from imblearn.over_sampling import RandomOverSampler
ros = RandomOverSampler()
X_resampled, y_resampled = ros.fit_resample(train_texts, train_labels)
C. 防过拟合
ini
python
# 加Dropout(让模型别太依赖某些特征)
model = BertForSequenceClassification.from_pretrained(
"bert-base-uncased",
num_labels=5,
hidden_dropout_prob=0.2, # 默认0.1,调高
)
3️⃣ 自动化调优工具
如果手动调太麻烦,可以用工具自动搜索最佳参数:
Optuna(超参数搜索)
ini
python
import optuna
def objective(trial):
lr = trial.suggest_float("learning_rate", 1e-5, 5e-5)
batch_size = trial.suggest_categorical("batch_size", [16, 32, 64])
training_args = TrainingArguments(
learning_rate=lr,
per_device_train_batch_size=batch_size,
...
)
trainer = Trainer(model, args=training_args, ...)
trainer.train()
return trainer.evaluate()["eval_f1"] # 以F1分数为目标优化
study = optuna.create_study(direction="maximize")
study.optimize(objective, n_trials=10) # 尝试10组参数
print("最佳参数:", study.best_params)
🌰 举个实际例子
任务 :电商评论分类(好评/中评/差评)
问题 :模型总是把"中评"预测成"好评"
调优步骤:
-
评估发现:中评的F1只有0.5(好评0.9,差评0.8)
-
分析原因:中评数据只有好评的1/3
-
调优操作:
- 给中评加3倍权重
- 增加中评的过采样数据
- 降低学习率(从2e-5 → 1e-5)
-
结果:中评F1提升到0.7,整体更平衡
✅ 总结
-
评估(Evaluate) → 定期考试,避免闭门造车
-
调优(Tune) → 针对性改进,像老师因材施教
-
核心原则:
- 数据问题 → 加数据/改权重
- 模型问题 → 调参数/改结构
- 效率问题 → 改Batch Size/换硬件
最终目标:让模型像学霸一样,不偏科、不死记、高效学习! 🚀