目录
4. 对比:微调(Fine-tuning) vs 特征提取(Feature Extraction
一、加载本地数据集
from datasets import load_from_disk
data_set = load_from_disk("../dataset/ChnSentiCorp")
print(data_set)
#取出训练集
train_dataset = data_set["train"]
print(train_dataset)
#查看数据
for data in train_dataset:
print(data)
打印出来的数据集:
数据集如下:
可以看到,上述dataset中的内容包含两部分:label和text。
Hugging Face 的 datasets 库支持多种数据集格式,如 CSV、JSON、TFRecord 等。加载数据集后,可以查看数据集的基本信息,如数据集大小、字段名称等。这有助于我们了解数据的分布情况,并在后续步骤中进行适当的处理。
二、制作DataSet
加载数据集后,需要对其进行处理以适应模型的输入格式。这包括数据清洗、格式转换等操作。
from datasets import load_from_disk
from torch.utils.data import Dataset
class MyDataset(Dataset):
def __init__(self, split):
# self.dataset = load_dataset(path="seamew/ChnSentiCorp",split="train")
# 从磁盘加载数据
self.dataset = load_from_disk("../dataset/ChnSentiCorp")
if split == "train":
self.dataset = self.dataset["train"]
elif split == "validation":
self.dataset = self.dataset["validation"]
elif split == "test":
self.dataset = self.dataset["test"]
def __len__(self):
return len(self.dataset)
def __getitem__(self, item):
text = self.dataset[item]["text"]
label = self.dataset[item]["label"]
return text, label
if __name__ == '__main__':
dataset = MyDataset("validation")
for data in dataset:
print(data)
在制作 Dataset 时,需定义数据集的字段。在本案例中,定义了两个字段: text (文本)和 label (情感标签)。每个字段都需要与模型的输入和输出匹配。
三、下游任务模型设计
在微调 BERT 模型之前,需要设计一个适应情感分析任务的下游模型结构。通常包括一个或多个全连接层,用于将 BERT 输出的特征向量转换为分类结果。
import torch
from transformers import BertModel
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
pretrained = BertModel.from_pretrained(r"../model/bert-base-chinese/models--bert-base-chinese/snapshots/c30a6ed22ab4564dc1e3b2ecbf6e766b0611a33f").to(DEVICE)
print(pretrained)
# 定义下游任务(将主干网络所提取的特征进行分类)
class Model(torch.nn.Module):
def __init__(self):
super().__init__()
self.fc = torch.nn.Linear(768, 2)
def forward(self, input_ids, attention_mask, token_type_ids):
# 冻结主干网络权重
with torch.no_grad():
out = pretrained(input_ids=input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids)
out = self.fc(out.last_hidden_state[:, 0])
out = out.softmax(dim=1)
return out
3.1 forward方法
forward
方法定义了数据如何通过网络流动:- 使用
with torch.no_grad()
上下文管理器冻结BERT模型的权重(不计算梯度,不更新参数) - 将输入数据(input_ids, attention_mask, token_type_ids)传递给预训练的BERT模型
- 从BERT输出中提取
last_hidden_state
(所有token的隐藏状态)的第一个token([CLS]标记)的状态 - 将这个状态通过全连接层(self.fc)进行分类
- 使用softmax函数将输出转换为概率分布(dim=1表示在类别维度上计算)
- 使用
3.2 with torch.no_grad()
使用 with torch.no_grad()
上下文管理器冻结 BERT 模型的权重,主要有以下几个原因:
1. 防止预训练权重被更新(冻结参数
- BERT 是一个在大规模语料上预训练好的模型,其权重已经包含了丰富的语言知识。
- 如果直接微调(fine-tuning)所有参数,可能会导致:
- 灾难性遗忘(Catastrophic Forgetting):BERT 学到的通用语言特征可能被覆盖,影响模型泛化能力。
- 过拟合风险:如果下游任务的数据较少,微调所有参数容易过拟合。
torch.no_grad()
确保在计算 BERT 的输出时不计算梯度,从而阻止优化器更新 BERT 的权重。
2. 节省计算资源
- BERT 参数量巨大(bert-base 有约 1.1 亿参数),计算梯度会占用大量显存和计算资源。
torch.no_grad()
会:- 禁用自动求导,减少 GPU 内存占用。
- 加速前向传播,因为不需要为反向传播存储中间变量。
3. 仅训练顶层分类器(特征提取模式)
- 这段代码的设计是仅训练顶层的全连接层(
self.fc
),而保持 BERT 的权重固定。 - 这是一种特征提取(Feature Extraction)策略,适用于:
- 下游任务数据量较少时(避免过拟合)。
- 希望快速实验或部署轻量级模型时。
4. 对比:微调(Fine-tuning) vs 特征提取(Feature Extraction
5. 何时应该去掉 torch.no_grad()
?
如果满足以下条件,可以解冻 BERT 并微调所有层:
- 下游任务数据量足够大(至少数千标注样本)。
- 任务与 BERT 预训练目标差异较大(如专业领域文本)。
- 有足够的 GPU 资源支持全参数微调。
6. 总结
with torch.no_grad()
的作用是:
- 保护预训练知识:防止 BERT 的通用语言表征被破坏。
- 节省资源:减少显存占用和计算时间。
- 适配小数据:更适合数据稀缺的下游任务。
四、自定义模型训练
4.1 collate_fn
函数
collate_fn
是 PyTorch DataLoader
的一个参数,用于自定义如何将多个样本合并成一个批次(batch)。它的输入 data
是一个列表,其中每个元素是 MyDataset
返回的一个样本(通常是 (文本, 标签)
的元组)。
collate_fn
对这些样本进行:
- 分词 + 编码(
token.batch_encode_plus
) - 填充/截断(
padding="max_length"
,truncation=True
) - 转为张量(
return_tensors="pt"
)
def collate_fn(data):
# 1. 提取文本和标签
sentes = [i[0] for i in data] # 所有句子(文本)
label = [i[1] for i in data] # 所有标签
# 2. 使用 tokenizer 批量编码文本
data = token.batch_encode_plus(
batch_text_or_text_pairs=sentes, # 输入文本列表
truncation=True, # 超过 max_length 时截断
padding="max_length", # 填充到 max_length
max_length=500, # 最大长度(token 数)
return_tensors="pt", # 返回 PyTorch 张量
return_length=True # 返回实际长度(可选)
)
# 3. 提取编码后的数据
input_ids = data["input_ids"] # token ID 序列(形状:[batch_size, max_length])
attention_mask = data["attention_mask"] # 注意力掩码(1=有效token,0=padding)
token_type_ids = data["token_type_ids"] # 句子类型(BERT 用于区分句子A/B)
# 4. 将标签转为 LongTensor
labels = torch.LongTensor(label) # 标签张量(形状:[batch_size])
return input_ids, attention_mask, token_type_ids, labels
关键点:
-
batch_encode_plus
的作用:- 对一批文本进行 tokenization(分词)、padding(填充)、truncation(截断)。
- 返回
input_ids
(token ID 序列)、attention_mask
(区分有效 token 和 padding)、token_type_ids
(区分句子A/B)。 max_length=500
表示每条句子最多保留 500 个 token(超出截断,不足填充)。- 当前使用
padding="max_length"
固定长度,可能浪费计算资源。可以改用padding="longest"
,按 batch 内最长句子填充。
-
return_tensors="pt"
:- 返回 PyTorch 张量(
torch.Tensor
),而不是 Python 列表或 NumPy 数组。
- 返回 PyTorch 张量(
-
attention_mask
的作用:- 告诉模型哪些 token 是真实的(1),哪些是填充的(0),避免模型计算 padding 部分。
-
token_type_ids
(可选):- 在 BERT 中用于区分两个句子(如 QA 任务),单句分类任务通常可以忽略。
-
labels
的处理:- 将 Python 列表的标签转为
torch.LongTensor
,用于计算损失函数(如交叉熵)。
- 将 Python 列表的标签转为
4.2 DataLoader
的创建
train_dataset = MyDataset("train") # 自定义数据集(假设返回 (文本, 标签))
train_loader = DataLoader(
dataset=train_dataset, # 数据集对象
batch_size=100, # 每批 100 个样本
shuffle=True, # 打乱数据顺序(训练时推荐)
drop_last=True, # 丢弃最后不足 batch_size 的样本
collate_fn=collate_fn # 使用自定义的 collate_fn 处理数据
)
关键参数解析:
batch_size=100
:- 每批加载 100 个样本,适合 GPU 并行计算。
-
shuffle=True
:- 每个 epoch 开始时打乱数据顺序,防止模型学习到数据顺序的偏差。
-
drop_last=True
:- 如果数据集总样本数不是
batch_size
的整数倍,丢弃最后不足一个 batch 的数据。 - 避免最后一个 batch 太小,影响批量归一化(BatchNorm)等操作。
- 如果数据集总样本数不是
-
collate_fn=collate_fn
:- 使用自定义的
collate_fn
处理数据,而不是默认的拼接方式。
- 使用自定义的
4.3 开始训练
#开始训练
model = Model().to(DEVICE)
optimizer = AdamW(model.parameters(), lr=5e-4)
loss_func = torch.nn.CrossEntropyLoss()
model.train()
for epoch in range(EPOCH):
for i,(input_ids,attention_mask,token_type_ids,labels) in enumerate(train_loader):
input_ids, attention_mask, token_type_ids, labels = input_ids.to(DEVICE), attention_mask.to(
DEVICE), token_type_ids.to(DEVICE), labels.to(DEVICE)
out = model(input_ids,attention_mask,token_type_ids)
loss = loss_func(out,labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
if i%5==0:
out = out.argmax(dim=1)
acc = (out==labels).sum().item()/len(labels)
print(epoch,i,loss.item(),acc)
torch.save(model.state_dict(),f"params/{epoch}bert01.pth")
print(epoch,"参数保存成功!")
1. 初始化模型、优化器和损失函数
model = Model().to(DEVICE) # 实例化模型并移动到 GPU(如果可用)
optimizer = AdamW(model.parameters(), lr=5e-4) # 使用 AdamW 优化器,学习率 5e-4
loss_func = torch.nn.CrossEntropyLoss() # 交叉熵损失函数(适用于分类任务)
-
Model()
:- 这里
Model
是之前定义的 BERT 下游任务模型(包含 BERT + 分类层)。 .to(DEVICE)
将模型移动到 GPU(如果DEVICE="cuda"
)。
- 这里
-
AdamW
:- 一种改进的 Adam 优化器,适用于 Transformer 模型(BERT 的原版优化器)。
lr=5e-4
是学习率,控制参数更新的步长。
-
CrossEntropyLoss
:- 用于多分类任务的损失函数,计算预测概率分布与真实标签的差异。
2. 训练循环
model.train() # 设置模型为训练模式(启用 Dropout/BatchNorm 等)
for epoch in range(EPOCH): # 遍历所有 epoch
for i, (input_ids, attention_mask, token_type_ids, labels) in enumerate(train_loader):
# 1. 数据移动到 GPU
input_ids, attention_mask, token_type_ids, labels = input_ids.to(DEVICE), attention_mask.to(DEVICE), token_type_ids.to(DEVICE), labels.to(DEVICE)
# 2. 前向传播
out = model(input_ids, attention_mask, token_type_ids) # 模型预测
# 3. 计算损失
loss = loss_func(out, labels) # 计算交叉熵损失
# 4. 反向传播 + 优化
optimizer.zero_grad() # 清空梯度(避免累积)
loss.backward() # 反向传播计算梯度
optimizer.step() # 更新模型参数
# 5. 打印训练日志(每 5 个 batch)
if i % 5 == 0:
out = out.argmax(dim=1) # 取预测类别(概率最大的索引)
acc = (out == labels).sum().item() / len(labels) # 计算准确率
print(epoch, i, loss.item(), acc) # 打印 epoch, batch, 损失, 准确率
# 6. 保存模型参数(每个 epoch 结束时)
torch.save(model.state_dict(), f"params/{epoch}bert01.pth")
print(epoch, "参数保存成功!")
-
optimizer.zero_grad()
是必须的:- PyTorch 默认会累积梯度,如果不手动清零,会导致梯度爆炸。
-
model.train()
和model.eval()
的区别:- 训练时用
model.train()
(启用 Dropout/BatchNorm)。 - 测试时用
model.eval()
(关闭 Dropout/BatchNorm)。
- 训练时用
- 学习率 (
lr=5e-4
) 的选择:- BERT 通常使用较小的学习率(
2e-5
到5e-5
),这里5e-4
可能偏大,可尝试调整。
- BERT 通常使用较小的学习率(
-
argmax(dim=1)
的作用:- 将模型输出的概率分布转为类别预测(如
[0.2, 0.8]
→1
)。
- 将模型输出的概率分布转为类别预测(如