模型微调
模型微调的基本概念与流程
微调是指在预训练模型的基础上,通过进一步的训练来适应特定的下游任务。BERT 模型通过预训练来学习语言的通用模式,然后通过微调来适应特定任务,如情感分析、命名实体识别等。微调过程中,通常冻结 BERT 的预训练层,只训练与下游任务相关的层。本课件将介绍如何使用 BERT 模型进行情感分析任务的微调训练。
加载数据集
情感分析任务的数据通常包括文本及其对应的情感标签。使用 Hugging Face 的 datasets 库可以轻松地加载和处理数据集。
数据集字段
在制作 Dataset 时,需定义数据集的字段。在本案例中,定义了两个字段: text (文本)和label (情感标签)。每个字段都需要与模型的输入和输出匹配。
数据集信息
制作 Dataset 后,可以通过 dataset.info 等方法查看其大小、字段名称等信息,以确保数据集的正确性和完整性。
使用PyTorch 原生的 Dataset 加载数据(重点,常用)
MyData.py:
from torch.utils.data import Dataset
from datasets import load_from_disk
class Mydataset(Dataset):
#初始化数据
def __init__(self,split):
#从磁盘加载数据
self.dataset = load_from_disk(r"D:\learn\learncode\huggingface\demo_4\data\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"]
else:
print("数据集名称错误!")
#获取数据集大小
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)
('位置尚可,但距离海边的位置比预期的要差的多,只能远远看大海,没有停车场', 0)
('整体来说,本书还是不错的。至少在书中描述了许多现实中存在的司法系统方面的问题,这是值得每个法律工作者去思考的。尤其是让那些涉世不深的想加入到律师队伍中的年青人,看到了社会特别是中国司法界真实的一面。缺点是:书中引用了大量的法律条文和司法解释,对于已经是律师或有一定工作经验的法律工作者来说有点多余,而且所占的篇幅不少,有凑字数的嫌疑。整体来说还是不错的。不要对一本书提太高的要求。', 1)
('5月8日付款成功,当当网显示5月10日发货,可是至今还没看到货物,也没收到任何通知,简不知怎么说好!!!', 0)
说明:huggingface的datasets和Pytorch的
Dataset的区别
下游任务模型设计
在微调 BERT 模型之前,需要设计一个适应情感分析任务的下游模型结构。通常包括一个或多个全连接层,用于将 BERT 输出的特征向量转换为分类结果。
模型创建
net.py:
# 1. 导入必要的库:BertModel是BERT的基础预训练模型,torch是PyTorch深度学习框架
from transformers import BertModel # 从transformers库导入BERT基础模型(负责提取文本特征)
import torch # 导入PyTorch,用于构建和训练神经网络
# 2. 定义模型训练使用的设备(CPU或GPU):优先用GPU,没有则用CPU
# 解释:GPU能大幅加速模型训练,尤其是深度学习模型;这里通过torch判断是否有可用GPU
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 补充:如果有GPU,DEVICE会是"cuda:0"(表示第1块GPU);没有则是"cpu"
# 3. 加载BERT预训练模型,并移动到指定设备(CPU/GPU)
# "bert-base-chinese":这是预训练模型的名称,是专门针对中文文本训练的基础BERT模型
# from_pretrained():自动下载(首次)或加载本地的预训练模型权重和配置
# .to(DEVICE):将模型的所有参数(如权重矩阵)移动到前面定义的设备上(确保计算在指定硬件上进行)
pretrained = BertModel.from_pretrained("bert-base-chinese").to(DEVICE)
# 4. 打印预训练模型的结构(可选,用于查看模型细节)
# 作用:可以看到BERT模型的层数、注意力头数、隐藏层维度等信息,确认模型加载正确
print(pretrained)
# 5. 定义下游任务模型(这里是文本分类模型)
# 解释:BERT基础模型(pretrained)只负责提取文本特征,不能直接做分类;
# 我们需要在它的基础上添加“分类头”(全连接层等),构建完整的分类模型
class Model(torch.nn.Module): # 继承PyTorch的Module类,这是自定义模型的标准写法
# 6. 模型初始化:定义模型的层结构(在__init__里声明所有要用到的层)
def __init__(self):
super().__init__() # 调用父类(Module)的初始化方法,必须写
# 定义dropout层:防止模型过拟合(随机让30%的神经元暂时“失效”)
# 0.3表示 dropout概率(即30%的神经元会被随机失活),值越大正则化越强
self.drop_out = torch.nn.Dropout(0.3)
# 定义全连接层(分类头):将BERT提取的特征转换为分类结果
# 输入维度768:BERT-base模型的隐藏层维度是768(每个token的特征是768维向量)
# 输出维度2:假设是二分类任务(如情感分析的“正面/负面”,输出2个类别的概率)
self.fc = torch.nn.Linear(768, 2)
# 7. 模型前向传播:定义数据在模型中的流动路径(核心逻辑)
# input_ids:文本分词后对应的索引序列(模型的核心输入)
# attention_mask:注意力掩码(标记哪些是有效token,哪些是填充的[PAD])
# token_type_ids:句子类型标记(单句任务中全为0,句子对任务中区分两个句子)
def forward(self, input_ids, attention_mask, token_type_ids):
# 第一步:用BERT预训练模型提取文本特征,且不参与训练(冻结预训练权重)
# with torch.no_grad():表示该代码块内的计算不计算梯度,权重不会更新
# 目的:固定BERT的预训练特征提取能力,只训练后面添加的分类头(适合小数据集,避免过拟合)
with torch.no_grad():
# 调用BERT模型,传入三个核心输入,得到模型输出
# out是BERT的输出对象,包含last_hidden_state(所有token的特征)、pooler_output(句子级特征)等
out = pretrained(input_ids=input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids)
# 第二步:从BERT输出中提取“句子级特征”(用于分类)
# out.last_hidden_state:形状是[batch_size, seq_length, 768](批次大小,序列长度,特征维度)
# [:, 0]:取每个序列的第0个token(即[CLS]标记)的特征,作为整个句子的特征(BERT的常用做法):详参文末解释《下游任务模型设计为什么out数据只取:[:, 0]》
# 此时特征形状变为[batch_size, 768](每个样本对应768维特征)
sentence_feature = out.last_hidden_state[:, 0]
# 第三步:通过全连接层将特征映射到分类结果
# self.fc(sentence_feature):将768维特征转换为2维(对应2个类别的原始分数)
class_score = self.fc(sentence_feature)
# 第四步:通过dropout层防止过拟合(对全连接层的输出做随机失活)
class_score_dropout = self.drop_out(class_score)
# 第五步:将原始分数转换为概率(用softmax函数,确保所有类别的概率和为1)
# dim=1:表示在“类别维度”上做softmax(每个样本的2个类别概率和为1)
class_prob = class_score_dropout.softmax(dim=1)
# 返回最终的类别概率(模型输出)
return class_prob
模型结构
下游任务模型通常包括以下几个部分:
BERT 模型:用于生成文本的上下文特征向量。
Dropout 层:用于防止过拟合,通过随机丢弃一部分神经元来提高模型的泛化能力。
全连接层:用于将 BERT 的输出特征向量映射到具体的分类任务上。
模型初始化
使用 BertModel.from_pretrained() 方法加载预训练的 BERT 模型,同时也可以初始化自定义的全连接层。初始化时,需要根据下游任务的需求,定义合适的输出维度。
模型训练
模型设计完成后,进入训练阶段。通过数据加载器(DataLoader)高效地批量处理数据,并使用优化器更新模型参数。
trainer.py:
import torch # 导入PyTorch深度学习框架
from torch.optim import AdamW # 导入PyTorch自带的AdamW优化器(用于更新模型参数)
from MyData import Mydataset # 导入自定义的数据集类(处理数据加载逻辑)
from torch.utils.data import DataLoader # 导入数据加载器(批量加载数据,支持多线程)
from net import Model # 导入自定义的模型类(基于BERT的分类模型)
from transformers import BertTokenizer # 导入BERT分词器(将文本转换为模型可识别的格式)
# 定义训练设备:优先使用GPU(cuda),如果没有则使用CPU
# GPU能大幅加速训练,尤其是深度学习模型;CPU训练会较慢
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 定义训练轮次:EPOCH=100表示整个数据集会被重复训练100次
# 轮次太少可能欠拟合(模型没学透),太多可能过拟合(模型只记住训练数据)
EPOCH = 100
# 加载BERT分词器:需要指定预训练模型的本地路径
# 分词器的作用是将中文文本拆分成BERT能理解的"token"(词或字),并转换为数字索引
token = BertTokenizer.from_pretrained(
r"D:\learn\learncode\huggingface\demo_4\trasnFormers_test\model\bert-base-chinese\models--bert-base-chinese\snapshots\c30a6ed22ab4564dc1e3b2ecbf6e766b0611a33f"
)
# 自定义数据处理函数:将原始数据转换为模型训练所需的格式
# DataLoader会将批量数据传入该函数,返回处理后的特征和标签
def collate_fn(data):
# 从传入的批量数据中提取文本和标签
# data是一个列表,每个元素是(文本, 标签)的元组
sentes = [i[0] for i in data] # 提取所有文本,组成列表
label = [i[1] for i in data] # 提取所有标签,组成列表
# 使用分词器对批量文本进行编码处理
data = token.batch_encode_plus(
batch_text_or_text_pairs=sentes, # 要处理的文本列表
truncation=True, # 当文本长度超过max_length时,截断多余部分
padding="max_length", # 将所有文本填充到max_length长度(短文本补[PAD])
max_length=350, # 统一文本长度为350(根据数据特点设置,不宜过长/过短)
return_tensors="pt", # 返回PyTorch张量(模型只能处理张量格式)
return_length=True, # 返回每个文本的实际长度(这里暂时用不到)
)
# 从编码结果中提取模型需要的输入特征
input_ids = data["input_ids"] # 文本token对应的数字索引(核心输入)
attention_mask = data["attention_mask"]# 注意力掩码(1表示有效token,0表示[PAD]填充)
token_type_ids = data["token_type_ids"]# 句子类型标记(单句任务全为0,这里保留格式)
labels = torch.LongTensor(label) # 将标签转换为长整型张量(分类任务标签格式)
# 返回处理后的特征和标签,供模型训练使用
return input_ids, attention_mask, token_type_ids, labels
# 创建训练数据集:使用自定义的Mydataset类加载"train"子集(训练集)
# Mydataset类内部实现了从本地加载数据、区分训练/验证/测试集的逻辑
train_dataset = Mydataset("train")
# 创建DataLoader:批量加载训练数据,是PyTorch训练的标准组件
train_loader = DataLoader(
dataset=train_dataset, # 要加载的数据集
batch_size=32, # 每次加载32条数据(批次大小,根据GPU内存调整)
shuffle=True, # 训练时打乱数据顺序(避免模型记住数据顺序)
drop_last=True, # 如果最后一批数据不足32条,就丢弃(保证批次大小一致)
collate_fn=collate_fn # 指定使用上面定义的collate_fn处理数据
)
# 主函数:当脚本直接运行时执行训练逻辑
if __name__ == '__main__':
# 打印当前使用的设备(确认是否用到GPU)
print("使用的设备:", DEVICE)
# 初始化模型,并移动到指定设备(CPU/GPU)
model = Model().to(DEVICE)
# 初始化优化器:用于更新模型参数,控制训练过程
# AdamW是常用的优化器,在Adam基础上改进了权重衰减(防止过拟合)
# lr=5e-4表示学习率(控制参数更新幅度,BERT通常用较小的学习率如2e-5,这里可根据效果调整)
optimizer = AdamW(model.parameters(), lr=5e-4)
# 定义损失函数:分类任务常用交叉熵损失(CrossEntropyLoss)
# 自动计算预测概率与真实标签之间的差距
loss_func = torch.nn.CrossEntropyLoss()
# 开启模型训练模式:会启用dropout等训练时特有的层
model.train()
# 开始训练:循环EPOCH轮次
for epoch in range(EPOCH):
# 遍历训练数据的每个批次(由DataLoader提供)
# enumerate用于获取批次索引i和批次数据
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的形状是[batch_size, num_labels],即每个样本的类别概率
out = model(input_ids, attention_mask, token_type_ids)
# 计算损失:预测结果与真实标签的差距
loss = loss_func(out, labels)
# 反向传播三步法:
# 1. 清空优化器的梯度(避免累积上一批的梯度)
optimizer.zero_grad()
# 2. 计算损失对所有可训练参数的梯度(自动求导)
loss.backward()
# 3. 根据梯度更新参数(优化器的核心作用)
optimizer.step()
# 每5个批次打印一次训练信息(监控训练进度)
if i % 5 == 0:
# 计算准确率:将概率最大的类别作为预测结果(argmax(dim=1))
out = out.argmax(dim=1)
# 统计预测正确的样本数,除以总样本数得到准确率
acc = (out == labels).sum().item() / len(labels)
# 打印轮次、批次索引、损失值、准确率
print(f"轮次: {epoch}, 批次: {i}, 损失: {loss.item():.4f}, 准确率: {acc:.4f}")
点击查看训练结果
BertModel(
(embeddings): BertEmbeddings(
(word_embeddings): Embedding(21128, 768, padding_idx=0)
(position_embeddings): Embedding(512, 768)
(token_type_embeddings): Embedding(2, 768)
(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
(dropout): Dropout(p=0.1, inplace=False)
)
(encoder): BertEncoder(
(layer): ModuleList(
(0-11): 12 x BertLayer(
(attention): BertAttention(
(self): BertSdpaSelfAttention(
(query): Linear(in_features=768, out_features=768, bias=True)
(key): Linear(in_features=768, out_features=768, bias=True)
(value): Linear(in_features=768, out_features=768, bias=True)
(dropout): Dropout(p=0.1, inplace=False)
)
(output): BertSelfOutput(
(dense): Linear(in_features=768, out_features=768, bias=True)
(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
(dropout): Dropout(p=0.1, inplace=False)
)
)
(intermediate): BertIntermediate(
(dense): Linear(in_features=768, out_features=3072, bias=True)
(intermediate_act_fn): GELUActivation()
)
(output): BertOutput(
(dense): Linear(in_features=3072, out_features=768, bias=True)
(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
(dropout): Dropout(p=0.1, inplace=False)
)
)
)
)
(pooler): BertPooler(
(dense): Linear(in_features=768, out_features=768, bias=True)
(activation): Tanh()
)
)
使用的设备: cpu
轮次: 0, 批次: 0, 损失: 0.7116, 准确率: 0.4688
轮次: 0, 批次: 5, 损失: 0.6405, 准确率: 0.6875
轮次: 0, 批次: 10, 损失: 0.5961, 准确率: 0.7500
轮次: 0, 批次: 15, 损失: 0.6326, 准确率: 0.6875
轮次: 0, 批次: 20, 损失: 0.5807, 准确率: 0.7188
轮次: 0, 批次: 25, 损失: 0.5562, 准确率: 0.7812
轮次: 0, 批次: 30, 损失: 0.6037, 准确率: 0.7188
轮次: 0, 批次: 35, 损失: 0.5383, 准确率: 0.8438
轮次: 0, 批次: 40, 损失: 0.6177, 准确率: 0.7812
轮次: 0, 批次: 45, 损失: 0.5110, 准确率: 0.9375
轮次: 0, 批次: 50, 损失: 0.5977, 准确率: 0.5938
数据加载
使用 DataLoader 实现批量数据加载。 DataLoader 自动处理数据的批处理和随机打乱,确保训练的高效性和数据的多样性。
6.2 优化器
AdamW 是一种适用于 BERT 模型的优化器,结合了 Adam 和权重衰减的特点,能够有效地防止过拟合。
训练循环
训练循环包含前向传播(forward pass)、损失计算(loss calculation)、反向传播(backwardpass)、参数更新(parameter update)等步骤。每个 epoch 都会对整个数据集进行一次遍历,更新
模型参数。通常训练过程中会跟踪损失值的变化,以判断模型的收敛情况。
模型评估与测试
在模型训练完成后,需要评估其在测试集上的性能。通常使用准确率、精确率、召回率和 F1 分数等指标来衡量模型的效果。
准确率(Accuracy)
准确率是衡量分类模型整体性能的基本指标,计算公式为正确分类的样本数量除以总样本数量
# -------------------------- 2. 模型评估核心逻辑 --------------------------
# 2.1 开启模型的"评估模式"(eval()):
# 作用:关闭训练时特有的层(如Dropout、BatchNorm),避免这些层影响评估结果(比如Dropout会随机失活神经元,评估时需要用完整模型)
model.eval()
# 2.2 初始化两个列表:分别存储所有测试样本的"预测标签"和"真实标签"
predictions = [] # 存模型预测的标签(比如0=负向,1=正向)
true_labels = [] # 存数据集中的真实标签
# 2.3 关闭梯度计算(with torch.no_grad()):
# 核心原因:评估阶段不需要更新模型参数,计算梯度会浪费内存和时间,所以直接禁用
with torch.no_grad():
# 2.4 遍历测试集的每个批次(batch)
for batch in test_loader:
# 2.5 把测试数据移动到和模型相同的设备(CPU/GPU)—— 必须和模型在同一设备,否则报错
# (修复原代码瑕疵:原代码没移设备,且直接用batch['xxx'],需先确保batch里的键和collate_fn输出一致)
# 注:根据你之前的collate_fn,返回的是(input_ids, attention_mask, token_type_ids, labels),不是字典,这里统一调整为元组取值
input_ids = batch[0].to(DEVICE) # 第0个元素是input_ids
attention_mask = batch[1].to(DEVICE) # 第1个元素是attention_mask
token_type_ids = batch[2].to(DEVICE) # 第2个元素是token_type_ids(你的模型需要这个输入,原代码漏了)
labels = batch[3].to(DEVICE) # 第3个元素是真实标签
# 2.6 模型前向计算:输入测试数据,得到预测结果
# (修复原代码瑕疵:原代码漏传token_type_ids,且输出变量名不统一)
outputs = model(input_ids=input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids)
# 2.7 从模型输出中提取"预测标签":
# outputs是模型输出的概率(如[0.1, 0.9]),torch.max(outputs, dim=1)取概率最大的索引(即预测标签)
# _ 表示忽略最大概率值(我们只需要标签索引),preds是当前批次的预测标签
_, preds = torch.max(outputs, dim=1)
# 2.8 把当前批次的预测标签和真实标签存入列表:
# 1. cpu():把GPU上的张量移到CPU(numpy不支持GPU张量)
# 2. numpy():把PyTorch张量转为numpy数组(方便后续计算准确率)
# 3. extend():把数组元素逐个加入列表(区别于append(),append()会加整个数组)
predictions.extend(preds.cpu().numpy())
true_labels.extend(labels.cpu().numpy())
# -------------------------- 3. 计算并打印评估指标(准确率) --------------------------
# 3.1 计算准确率:用sklearn的accuracy_score,比较"真实标签"和"预测标签"的匹配程度
# (需提前导入sklearn的accuracy_score:from sklearn.metrics import accuracy_score,原代码漏了)
from sklearn.metrics import accuracy_score
accuracy = accuracy_score(true_labels, predictions)
# 3.2 打印准确率:保留4位小数,直观查看模型在测试集上的泛化能力
print(f"测试集准确率(Accuracy): {accuracy:.4f}")
查看打印结果
轮次: 0, 批次: 0, 损失: 0.6811, 准确率: 0.5312
测试集准确率(Accuracy): 0.5084
第 0 轮训练结束:模型参数已保存到 0bert.pt
测试集准确率(Accuracy): 0.5076
第 0 轮训练结束:模型参数已保存到 0bert.pt
测试集准确率(Accuracy): 0.5296
第 0 轮训练结束:模型参数已保存到 0bert.pt
测试集准确率(Accuracy): 0.5524
第 0 轮训练结束:模型参数已保存到 0bert.pt
测试集准确率(Accuracy): 0.5954
第 0 轮训练结束:模型参数已保存到 0bert.pt
轮次: 0, 批次: 5, 损失: 0.6725, 准确率: 0.5312
测试集准确率(Accuracy): 0.6444
第 0 轮训练结束:模型参数已保存到 0bert.pt
测试集准确率(Accuracy): 0.6740
第 0 轮训练结束:模型参数已保存到 0bert.pt
测试集准确率(Accuracy): 0.7002
第 0 轮训练结束:模型参数已保存到 0bert.pt
测试集准确率(Accuracy): 0.7154
第 0 轮训练结束:模型参数已保存到 0bert.pt
测试集准确率(Accuracy): 0.7280
第 0 轮训练结束:模型参数已保存到 0bert.pt
轮次: 0, 批次: 10, 损失: 0.6572, 准确率: 0.5938
测试集准确率(Accuracy): 0.7500
第 0 轮训练结束:模型参数已保存到 0bert.pt
测试集准确率(Accuracy): 0.7593
第 0 轮训练结束:模型参数已保存到 0bert.pt
测试集准确率(Accuracy): 0.7720
第 0 轮训练结束:模型参数已保存到 0bert.pt
测试集准确率(Accuracy): 0.7796
第 0 轮训练结束:模型参数已保存到 0bert.pt
测试集准确率(Accuracy): 0.7863
第 0 轮训练结束:模型参数已保存到 0bert.pt
轮次: 0, 批次: 15, 损失: 0.6281, 准确率: 0.7188
测试集准确率(Accuracy): 0.7998
第 0 轮训练结束:模型参数已保存到 0bert.pt
测试集准确率(Accuracy): 0.8007
第 0 轮训练结束:模型参数已保存到 0bert.pt
测试集准确率(Accuracy): 0.8049
第 0 轮训练结束:模型参数已保存到 0bert.pt
测试集准确率(Accuracy): 0.8125
第 0 轮训练结束:模型参数已保存到 0bert.pt
测试集准确率(Accuracy): 0.8142
第 0 轮训练结束:模型参数已保存到 0bert.pt
轮次: 0, 批次: 20, 损失: 0.6241, 准确率: 0.6875
测试集准确率(Accuracy): 0.8243
第 0 轮训练结束:模型参数已保存到 0bert.pt
测试集准确率(Accuracy): 0.8285
第 0 轮训练结束:模型参数已保存到 0bert.pt
测试集准确率(Accuracy): 0.8311
第 0 轮训练结束:模型参数已保存到 0bert.pt
测试集准确率(Accuracy): 0.8328
第 0 轮训练结束:模型参数已保存到 0bert.pt
测试集准确率(Accuracy): 0.8353
第 0 轮训练结束:模型参数已保存到 0bert.pt
轮次: 0, 批次: 25, 损失: 0.5638, 准确率: 0.7812
测试集准确率(Accuracy): 0.8395
第 0 轮训练结束:模型参数已保存到 0bert.pt
测试集准确率(Accuracy): 0.8370
第 0 轮训练结束:模型参数已保存到 0bert.pt
关键概念拆解(新手必懂)
精确率、召回率和 F1 分数
精确率(Precision)和召回率(Recall)是分类模型的另两个重要指标,分别反映模型在正例预测上的精确性和召回能力。F1 分数是精确率和召回率的调和平均数,通常用于不均衡数据集的评估。
from sklearn.metrics import precision_score, recall_score, f1_score
precision = precision_score(true_labels, predictions, average='weighted')
recall = recall_score(true_labels, predictions, average='weighted')
f1 = f1_score(true_labels, predictions, average='weighted')
print(f"Precision: {precision:.4f}")
print(f"Recall: {recall:.4f}")
print(f"F1 Score: {f1:.4f}")
查看打印结果
使用的设备: cuda
轮次: 0, 批次: 0, 损失: 0.6933, 准确率: 0.5312
测试集准确率(Accuracy): 0.4932
Precision: 0.2439
Recall: 0.4932
F1 Score: 0.3264
第 0 轮训练结束:模型参数已保存到 0bert.pt
测试集准确率(Accuracy): 0.5008
Precision: 0.6433
Recall: 0.5008
F1 Score: 0.3459
第 0 轮训练结束:模型参数已保存到 0bert.pt
测试集准确率(Accuracy): 0.5270
Precision: 0.6989
Recall: 0.5270
F1 Score: 0.4031
第 0 轮训练结束:模型参数已保存到 0bert.pt
测试集准确率(Accuracy): 0.5963
Precision: 0.7093
Recall: 0.5963
F1 Score: 0.5369
第 0 轮训练结束:模型参数已保存到 0bert.pt
测试集准确率(Accuracy): 0.7002
Precision: 0.7480
Recall: 0.7002
F1 Score: 0.6862
第 0 轮训练结束:模型参数已保存到 0bert.pt
轮次: 0, 批次: 5, 损失: 0.6296, 准确率: 0.7500
测试集准确率(Accuracy): 0.7644
Precision: 0.7752
Recall: 0.7644
F1 Score: 0.7624
第 0 轮训练结束:模型参数已保存到 0bert.pt
测试集准确率(Accuracy): 0.7829
Precision: 0.7833
Recall: 0.7829
F1 Score: 0.7828
第 0 轮训练结束:模型参数已保存到 0bert.pt
测试集准确率(Accuracy): 0.7736
Precision: 0.7816
Recall: 0.7736
F1 Score: 0.7717
第 0 轮训练结束:模型参数已保存到 0bert.pt
结果分析与模型优化
通过分析测试集上的结果,可以发现模型的强项和弱项。例如,如果 F1 分数较低,可能是由于数据集不平衡,导致模型在某些类别上表现不佳。通过调整超参数、改进数据预处理步骤,或使用更复杂的模型结构,可以进一步提高模型性能。
保存与加载模型
为了在未来使用训练好的模型,可以将其保存为文件,之后再加载进行推理或进一步的微调。
保存
# -------------------------- 4. 保存当前轮次的模型参数 --------------------------
# 把模型的权重(state_dict())保存到指定路径:"params/轮次号bert.pt"
# 作用:1. 防止程序中断丢失进度;2. 后续可加载最优轮次的模型(比如测试集准确率最高的那轮)
torch.save(model.state_dict(), f"params/{epoch}bert.pt")
print(f"第 {epoch} 轮训练结束:模型参数已保存到 params/{epoch}bert.pt")

加载
# 1. 导入必要的包
import torch # 导入PyTorch核心库(加载模型、张量操作必备)
from demo_15.net import Model # 导入你的模型类(Model是你自定义的BERT分类模型)
# 注:demo_15.net 表示 Model 类在 demo_15 文件夹下的 net.py 文件中,路径要和你的实际文件一致
# 2. 初始化模型结构(先创建空的模型框架,参数是随机的)
# 这里的 Model() 必须和你训练时用的模型类完全一致(结构、层名不能变)
model = Model()
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
# 3. 加载保存的模型参数(关键修复:添加 map_location 解决GPU/CPU冲突)
# torch.load(参数文件路径, map_location=...):读取参数并指定加载到的设备
# map_location=torch.device('cpu'):强制把参数加载到CPU(无论保存时在哪个设备)
model.load_state_dict(
torch.load(
r'D:\learn\learncode\huggingface\demo_15\params\0bert.pt', # 你的参数文件绝对路径
map_location=device # 核心修复:解决GPU/CPU不匹配问题
)
)
# 4. 切换模型到评估模式(必须!)
# 作用:关闭训练时特有的层(如Dropout),确保预测结果稳定(不会随机失活神经元)
model.eval()
# (可选)如果之后有GPU可用,也可以加载到GPU(现在先适配CPU)
# device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# model = model.to(device)
print("模型加载完成!已切换到评估模式,可用于后续预测任务。")
预测
# 示例:用加载后的模型预测单条文本(需先分词,格式和训练时一致)
from transformers import BertTokenizer
# 加载和训练时相同的分词器
tokenizer = BertTokenizer.from_pretrained(r"D:\learn\learncode\huggingface\demo_4\trasnFormers_test\model\bert-base-chinese\models--bert-base-chinese\snapshots\c30a6ed22ab4564dc1e3b2ecbf6e766b0611a33f")
# 待预测的新文本
new_text = "这家酒店环境很好,下次还来!"
# 处理文本格式(和训练时的collate_fn逻辑一致)
encoded = tokenizer(
new_text,
truncation=True,
padding="max_length",
max_length=350,
return_tensors="pt" # 返回PyTorch张量
)
# 预测(关闭梯度计算,节省资源)
with torch.no_grad():
output = model(
input_ids=encoded["input_ids"],
attention_mask=encoded["attention_mask"],
token_type_ids=encoded["token_type_ids"]
)
pred_label = torch.argmax(output, dim=1).item() # 取概率最大的类别
# 输出结果(假设0=负面,1=正面)
print(f"新文本:{new_text}")
print(f"预测标签:{pred_label}({['负面评价', '正面评价'][pred_label]})")
新文本:这家酒店环境很好,下次还来!
预测标签:1(正面评价)
小结
在本课程中,我们详细介绍了如何使用 Hugging Face 的 BERT 模型进行中文评价情感分析的微调训练。我们从加载数据集、制作 Dataset、词汇表操作、模型设计、自定义训练,到最后的效果评估与测试,逐步讲解了整个微调过程。通过本课程,你需要掌握使用预训练语言模型进行下游任务微调的基本流程,并能应用到实际的 NLP 项目中。
完整代码可以直接复制到colab运行
点击查看完整代码
from torch.utils.data import Dataset
from datasets import load_from_disk, load_dataset
class Mydataset(Dataset):
# 初始化数据
def __init__(self, split):
# 从磁盘加载数据
self.dataset = load_dataset("lansinuote/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"]
else:
print("数据集名称错误!")
# 获取数据集大小
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)
# 1. 导入必要的库:BertModel是BERT的基础预训练模型,torch是PyTorch深度学习框架
from transformers import BertModel # 从transformers库导入BERT基础模型(负责提取文本特征)
import torch # 导入PyTorch,用于构建和训练神经网络
# 2. 定义模型训练使用的设备(CPU或GPU):优先用GPU,没有则用CPU
# 解释:GPU能大幅加速模型训练,尤其是深度学习模型;这里通过torch判断是否有可用GPU
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 补充:如果有GPU,DEVICE会是"cuda:0"(表示第1块GPU);没有则是"cpu"
# 3. 加载BERT预训练模型,并移动到指定设备(CPU/GPU)
# "bert-base-chinese":这是预训练模型的名称,是专门针对中文文本训练的基础BERT模型
# from_pretrained():自动下载(首次)或加载本地的预训练模型权重和配置
# .to(DEVICE):将模型的所有参数(如权重矩阵)移动到前面定义的设备上(确保计算在指定硬件上进行)
pretrained = BertModel.from_pretrained("bert-base-chinese").to(DEVICE)
# 4. 打印预训练模型的结构(可选,用于查看模型细节)
# 作用:可以看到BERT模型的层数、注意力头数、隐藏层维度等信息,确认模型加载正确
print(pretrained)
# 5. 定义下游任务模型(这里是文本分类模型)
# 解释:BERT基础模型(pretrained)只负责提取文本特征,不能直接做分类;
# 我们需要在它的基础上添加“分类头”(全连接层等),构建完整的分类模型
class Model(torch.nn.Module): # 继承PyTorch的Module类,这是自定义模型的标准写法
# 6. 模型初始化:定义模型的层结构(在__init__里声明所有要用到的层)
def __init__(self):
super().__init__() # 调用父类(Module)的初始化方法,必须写
# 定义dropout层:防止模型过拟合(随机让30%的神经元暂时“失效”)
# 0.3表示 dropout概率(即30%的神经元会被随机失活),值越大正则化越强
self.drop_out = torch.nn.Dropout(0.3)
# 定义全连接层(分类头):将BERT提取的特征转换为分类结果
# 输入维度768:BERT-base模型的隐藏层维度是768(每个token的特征是768维向量)
# 输出维度2:假设是二分类任务(如情感分析的“正面/负面”,输出2个类别的概率)
self.fc = torch.nn.Linear(768, 2)
# 7. 模型前向传播:定义数据在模型中的流动路径(核心逻辑)
# input_ids:文本分词后对应的索引序列(模型的核心输入)
# attention_mask:注意力掩码(标记哪些是有效token,哪些是填充的[PAD])
# token_type_ids:句子类型标记(单句任务中全为0,句子对任务中区分两个句子)
def forward(self, input_ids, attention_mask, token_type_ids):
# 第一步:用BERT预训练模型提取文本特征,且不参与训练(冻结预训练权重)
# with torch.no_grad():表示该代码块内的计算不计算梯度,权重不会更新
# 目的:固定BERT的预训练特征提取能力,只训练后面添加的分类头(适合小数据集,避免过拟合)
with torch.no_grad():
# 调用BERT模型,传入三个核心输入,得到模型输出
# out是BERT的输出对象,包含last_hidden_state(所有token的特征)、pooler_output(句子级特征)等
out = pretrained(input_ids=input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids)
# 第二步:从BERT输出中提取“句子级特征”(用于分类)
# out.last_hidden_state:形状是[batch_size, seq_length, 768](批次大小,序列长度,特征维度)
# [:, 0]:取每个序列的第0个token(即[CLS]标记)的特征,作为整个句子的特征(BERT的常用做法):详参文末解释
# 此时特征形状变为[batch_size, 768](每个样本对应768维特征)
sentence_feature = out.last_hidden_state[:, 0]
# 第三步:通过全连接层将特征映射到分类结果
# self.fc(sentence_feature):将768维特征转换为2维(对应2个类别的原始分数)
class_score = self.fc(sentence_feature)
# 第四步:通过dropout层防止过拟合(对全连接层的输出做随机失活)
class_score_dropout = self.drop_out(class_score)
# 第五步:将原始分数转换为概率(用softmax函数,确保所有类别的概率和为1)
# dim=1:表示在“类别维度”上做softmax(每个样本的2个类别概率和为1)
class_prob = class_score_dropout.softmax(dim=1)
# 返回最终的类别概率(模型输出)
return class_prob
import torch # 导入PyTorch深度学习框架
from torch.optim import AdamW # 导入PyTorch自带的AdamW优化器(用于更新模型参数)
from torch.utils.data import DataLoader # 导入数据加载器(批量加载数据,支持多线程)
from transformers import BertTokenizer # 导入BERT分词器(将文本转换为模型可识别的格式)
# 定义训练设备:优先使用GPU(cuda),如果没有则使用CPU
# GPU能大幅加速训练,尤其是深度学习模型;CPU训练会较慢
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 定义训练轮次:EPOCH=100表示整个数据集会被重复训练100次
# 轮次太少可能欠拟合(模型没学透),太多可能过拟合(模型只记住训练数据)
EPOCH = 10
# 加载BERT分词器:需要指定预训练模型的本地路径
# 分词器的作用是将中文文本拆分成BERT能理解的"token"(词或字),并转换为数字索引
token = BertTokenizer.from_pretrained(
"ckiplab/bert-base-chinese-pos"
)
# 自定义数据处理函数:将原始数据转换为模型训练所需的格式
# DataLoader会将批量数据传入该函数,返回处理后的特征和标签
def collate_fn(data):
# 从传入的批量数据中提取文本和标签
# data是一个列表,每个元素是(文本, 标签)的元组
sentes = [i[0] for i in data] # 提取所有文本,组成列表
label = [i[1] for i in data] # 提取所有标签,组成列表
# 使用分词器对批量文本进行编码处理
data = token.batch_encode_plus(
batch_text_or_text_pairs=sentes, # 要处理的文本列表
truncation=True, # 当文本长度超过max_length时,截断多余部分
padding="max_length", # 将所有文本填充到max_length长度(短文本补[PAD])
max_length=350, # 统一文本长度为350(根据数据特点设置,不宜过长/过短)
return_tensors="pt", # 返回PyTorch张量(模型只能处理张量格式)
return_length=True, # 返回每个文本的实际长度(这里暂时用不到)
)
# 从编码结果中提取模型需要的输入特征
input_ids = data["input_ids"] # 文本token对应的数字索引(核心输入)
attention_mask = data["attention_mask"] # 注意力掩码(1表示有效token,0表示[PAD]填充)
token_type_ids = data["token_type_ids"] # 句子类型标记(单句任务全为0,这里保留格式)
labels = torch.LongTensor(label) # 将标签转换为长整型张量(分类任务标签格式)
# 返回处理后的特征和标签,供模型训练使用
return input_ids, attention_mask, token_type_ids, labels
# 创建训练数据集:使用自定义的Mydataset类加载"train"子集(训练集)
# Mydataset类内部实现了从本地加载数据、区分训练/验证/测试集的逻辑
train_dataset = Mydataset("train")
# 创建DataLoader:批量加载训练数据,是PyTorch训练的标准组件
train_loader = DataLoader(
dataset=train_dataset, # 要加载的数据集
batch_size=32, # 每次加载32条数据(批次大小,根据GPU内存调整)
shuffle=True, # 训练时打乱数据顺序(避免模型记住数据顺序)
drop_last=True, # 如果最后一批数据不足32条,就丢弃(保证批次大小一致)
collate_fn=collate_fn # 指定使用上面定义的collate_fn处理数据
)
from sklearn.metrics import accuracy_score
# 主函数:当脚本直接运行时执行训练逻辑
if __name__ == '__main__':
# 打印当前使用的设备(确认是否用到GPU)
print("使用的设备:", DEVICE)
# 初始化模型,并移动到指定设备(CPU/GPU)
model = Model().to(DEVICE)
# 初始化优化器:用于更新模型参数,控制训练过程
# AdamW是常用的优化器,在Adam基础上改进了权重衰减(防止过拟合)
# lr=5e-4表示学习率(控制参数更新幅度,BERT通常用较小的学习率如2e-5,这里可根据效果调整)
optimizer = AdamW(model.parameters(), lr=5e-4)
# 定义损失函数:分类任务常用交叉熵损失(CrossEntropyLoss)
# 自动计算预测概率与真实标签之间的差距
loss_func = torch.nn.CrossEntropyLoss()
# 开启模型训练模式:会启用dropout等训练时特有的层
model.train()
# 开始训练:循环EPOCH轮次
for epoch in range(EPOCH):
# 遍历训练数据的每个批次(由DataLoader提供)
# enumerate用于获取批次索引i和批次数据
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的形状是[batch_size, num_labels],即每个样本的类别概率
out = model(input_ids, attention_mask, token_type_ids)
# 计算损失:预测结果与真实标签的差距
loss = loss_func(out, labels)
# 反向传播三步法:
# 1. 清空优化器的梯度(避免累积上一批的梯度)
optimizer.zero_grad()
# 2. 计算损失对所有可训练参数的梯度(自动求导)
loss.backward()
# 3. 根据梯度更新参数(优化器的核心作用)
optimizer.step()
# 每5个批次打印一次训练信息(监控训练进度)
if i % 5 == 0:
# 计算准确率:将概率最大的类别作为预测结果(argmax(dim=1))
out = out.argmax(dim=1)
# 统计预测正确的样本数,除以总样本数得到准确率
acc = (out == labels).sum().item() / len(labels)
# 打印轮次、批次索引、损失值、准确率
print(f"轮次: {epoch}, 批次: {i}, 损失: {loss.item():.4f}, 准确率: {acc:.4f}")
# -------------------------- 1. 加载测试数据集 --------------------------
# 1.1 用自定义的Mydataset类加载"test"子集(测试集,用来评估模型泛化能力)
# Mydataset类内部已实现:从本地读取数据、区分train/validation/test集的逻辑
test_dataset = Mydataset("test")
# 1.2 创建测试集的数据加载器DataLoader(批量加载测试数据,和训练集逻辑类似但有差异)
test_loader = DataLoader(
dataset=test_dataset, # 传入测试数据集
batch_size=32, # 批次大小:和训练集保持一致(32),避免格式问题
shuffle=False, # 测试时禁止打乱数据!—— 打乱不影响准确率,但会让预测结果和原始数据对应不上(如需后续分析错误样本,必须关闭)
drop_last=True, # 丢弃最后不足32条的批次:保证每个批次格式统一(避免后续计算报错)
collate_fn=collate_fn # 用和训练集相同的collate_fn处理数据:确保输入格式(input_ids等)和训练时完全一致
)
# -------------------------- 2. 模型评估核心逻辑 --------------------------
# 2.1 开启模型的"评估模式"(eval()):
# 作用:关闭训练时特有的层(如Dropout、BatchNorm),避免这些层影响评估结果(比如Dropout会随机失活神经元,评估时需要用完整模型)
model.eval()
# 2.2 初始化两个列表:分别存储所有测试样本的"预测标签"和"真实标签"
predictions = [] # 存模型预测的标签(比如0=负向,1=正向)
true_labels = [] # 存数据集中的真实标签
# 2.3 关闭梯度计算(with torch.no_grad()):
# 核心原因:评估阶段不需要更新模型参数,计算梯度会浪费内存和时间,所以直接禁用
with torch.no_grad():
# 2.4 遍历测试集的每个批次(batch)
for batch in test_loader:
# 2.5 把测试数据移动到和模型相同的设备(CPU/GPU)—— 必须和模型在同一设备,否则报错
# (修复原代码瑕疵:原代码没移设备,且直接用batch['xxx'],需先确保batch里的键和collate_fn输出一致)
# 注:根据你之前的collate_fn,返回的是(input_ids, attention_mask, token_type_ids, labels),不是字典,这里统一调整为元组取值
input_ids = batch[0].to(DEVICE) # 第0个元素是input_ids
attention_mask = batch[1].to(DEVICE) # 第1个元素是attention_mask
token_type_ids = batch[2].to(DEVICE) # 第2个元素是token_type_ids(你的模型需要这个输入,原代码漏了)
labels = batch[3].to(DEVICE) # 第3个元素是真实标签
# 2.6 模型前向计算:输入测试数据,得到预测结果
# (修复原代码瑕疵:原代码漏传token_type_ids,且输出变量名不统一)
outputs = model(input_ids=input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids)
# 2.7 从模型输出中提取"预测标签":
# outputs是模型输出的概率(如[0.1, 0.9]),torch.max(outputs, dim=1)取概率最大的索引(即预测标签)
# _ 表示忽略最大概率值(我们只需要标签索引),preds是当前批次的预测标签
_, preds = torch.max(outputs, dim=1)
# 2.8 把当前批次的预测标签和真实标签存入列表:
# 1. cpu():把GPU上的张量移到CPU(numpy不支持GPU张量)
# 2. numpy():把PyTorch张量转为numpy数组(方便后续计算准确率)
# 3. extend():把数组元素逐个加入列表(区别于append(),append()会加整个数组)
predictions.extend(preds.cpu().numpy())
true_labels.extend(labels.cpu().numpy())
# -------------------------- 3. 计算并打印评估指标(准确率) --------------------------
# 3.1 计算准确率:用sklearn的accuracy_score,比较"真实标签"和"预测标签"的匹配程度
# (需提前导入sklearn的accuracy_score:from sklearn.metrics import accuracy_score,原代码漏了)
from sklearn.metrics import accuracy_score
accuracy = accuracy_score(true_labels, predictions)
# 3.2 打印准确率:保留4位小数,直观查看模型在测试集上的泛化能力
print(f"测试集准确率(Accuracy): {accuracy:.4f}")
from sklearn.metrics import precision_score, recall_score, f1_score
precision = precision_score(true_labels, predictions, average='weighted')
recall = recall_score(true_labels, predictions, average='weighted')
f1 = f1_score(true_labels, predictions, average='weighted')
print(f"Precision: {precision:.4f}")
print(f"Recall: {recall:.4f}")
print(f"F1 Score: {f1:.4f}")
# -------------------------- 4. 保存当前轮次的模型参数 --------------------------
# 把模型的权重(state_dict())保存到指定路径:"params/轮次号bert.pt"
# 作用:1. 防止程序中断丢失进度;2. 后续可加载最优轮次的模型(比如测试集准确率最高的那轮)
torch.save(model.state_dict(), f"params/{epoch}bert.pt")
print(f"第 {epoch} 轮训练结束:模型参数已保存到 params/{epoch}bert.pt")
微博评论分析案例(多分类任务)
数据集介绍
数据集一共有8类,其中训练集50000条,测试集4571条,验证集5000条:

from datasets import load_dataset,load_from_disk
# 只加载训练集
# 在 load_dataset 函数中,path 参数并非指文件路径,而是数据集加载器的名称:指定数据格式类型(如 "csv"、"json"、"text" 等),告诉库用什么解析器处理数据。
data = load_dataset(path="csv",data_dir="data/Weibo/",split="train")
print(data)
for i in range(10):
print(data[i])
#全部加载
data = load_dataset(path="csv",data_files="data/Weibo/*.csv")
print(data)
Dataset({
features: ['label', 'text'],
num_rows: 50000
})
{'label': 1, 'text': '崩溃了。'}
{'label': 7, 'text': '文革之后还能保留这个习俗也算异数了。'}
{'label': 0, 'text': '接下来雕刻消磨余下的一点时间,今回北京行,和几个朋友都不约而同地聚在了首都,那心情可是鸡冻的,希望大家新的一年都身体健康,顺顺利利的!'}
{'label': 7, 'text': '其实更好。'}
{'label': 7, 'text': '我们一贯支持原创。'}
{'label': 7, 'text': '=___=@Xeraphina柒希'}
{'label': 7, 'text': '人在跳板上,最辛苦的不是跳下来那一刻,而是跳下来前心里的挣扎无助和患得患失。'}
{'label': 7, 'text': '心情,放开一点,对情绪有好处。'}
{'label': 0, 'text': '这里的宝贝很不错,大家可以去看看!'}
{'label': 7, 'text': '人们开始提名自己的候选人……'}
DatasetDict({
train: Dataset({
features: ['label', 'text'],
num_rows: 59571
})
})
加载数据集
MyData.py
from torch.utils.data import Dataset
from datasets import load_dataset
class MyDataset(Dataset):
def __init__(self,split):
#从磁盘加载csv数据
self.dataset = load_dataset(path="csv",data_dir="data/Weibo",split=split)
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("test")
print(len(dataset))
print(dataset)
for i in range(10):
print(dataset[i])
4571
<__main__.MyDataset object at 0x000001E005E3E6F0>
('真的很开心啊!!!!!!!', 2)
('咳咳。。。。', 7)
('//@陈宝存:回复@梅海东messi:这种非守法的公民存在,足见我们法制建设的艰难,你懂法吗?', 1)
('也许有一天,你突然醒来,发现自己还在十几岁的年纪,年少时喜欢的男生就在你面前,看着你温暖地笑,说你是个傻瓜,告诉你你从没有失恋过,告诉你是他的唯一,告诉你后来的一切都没发生过,告诉你他其实一直爱你。', 7)
('独栋别墅跟农家乐的区别就是:内在装修好,各种设施齐备,别的嘛——完全没区别!', 7)
('置身其中,一周的烦躁无影踪。', 7)
('高考都考这么多年了,就不应该搞个周年店庆么,考400送350,一本分数线7折,考三本送二本 体验券![偷笑]', 7)
('到纳斯达克上市,我感觉自己更像一个旅行者,走到了这一站但这个地方不是我的家,我只是到这里来,证明自己做了一些想做的事情', 7)
('六一儿童节到了。', 7)
('不要这样,也不要总觉得自己总缺什么,只要快乐,你就什么都不缺。', 7)
下游任务模型设计
模型创建
net.py
from transformers import BertModel
import torch
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
pretrained = BertModel.from_pretrained("bert-base-chinese").to(DEVICE)
print(pretrained)
#定义下游任务模型(将主干网络所提取的特征分类)
class Model(torch.nn.Module):
def __init__(self):
super().__init__()
self.fc = torch.nn.Linear(768,8)
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
模型训练
查看代码
import torch
from torch.optim import AdamW
from torch.optim.lr_scheduler import ReduceLROnPlateau # 新增学习率调度器
from MyData import MyDataset
from torch.utils.data import DataLoader
from net import Model
from transformers import BertTokenizer
import os # 新增用于创建目录
# 设置设备:优先使用GPU,否则使用CPU
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"使用设备: {DEVICE}")
# 超参数设置(集中管理,便于调整)
EPOCH = 50 # 减少 epoch 数量,30000 过于庞大
BATCH_SIZE = 100
VAL_BATCH_SIZE = 50
MAX_LENGTH = 500
LEARNING_RATE = 5e-4
PATIENCE = 5 # 早停耐心值:连续多少个epoch性能没有提升就停止
# 初始化BERT分词器
# 本地加载预训练的BERT中文分词器
tokenizer = BertTokenizer.from_pretrained(
r"D:\learn\learncode\huggingface\demo_4\trasnFormers_test\model\bert-base-chinese\models--bert-base-chinese\snapshots\c30a6ed22ab4564dc1e3b2ecbf6e766b0611a33f"
)
def collate_fn(data):
"""
数据处理函数:将原始数据转换为模型需要的格式
Args:
data: 原始数据列表,每个元素是(text, label)元组
Returns:
处理后的张量:input_ids, attention_mask, token_type_ids, labels
"""
# 分离文本和标签
sentences = [item[0] for item in data]
labels = [item[1] for item in data]
# 对文本进行编码
encoded_data = tokenizer.batch_encode_plus(
batch_text_or_text_pairs=sentences,
truncation=True, # 超过max_length时截断
padding="max_length", # 填充到max_length长度
max_length=MAX_LENGTH,
return_tensors="pt", # 返回PyTorch张量
return_length=True
)
# 提取编码后的结果
input_ids = encoded_data["input_ids"]
attention_mask = encoded_data["attention_mask"] # 区分真实token和填充token
token_type_ids = encoded_data["token_type_ids"] # 区分句子对(单句时全为0)
labels = torch.LongTensor(labels) # 转换标签为长整型张量
return input_ids, attention_mask, token_type_ids, labels
# 创建数据集
train_dataset = MyDataset("train")
val_dataset = MyDataset("validation")
print(f"训练集大小: {len(train_dataset)}, 验证集大小: {len(val_dataset)}")
# 创建数据加载器
train_loader = DataLoader(
dataset=train_dataset,
batch_size=BATCH_SIZE,
shuffle=True, # 训练集打乱顺序
drop_last=True, # 丢弃最后一个不完整的批次
collate_fn=collate_fn
)
val_loader = DataLoader(
dataset=val_dataset,
batch_size=VAL_BATCH_SIZE,
shuffle=False, # 验证集不需要打乱
drop_last=True,
collate_fn=collate_fn
)
# 创建保存模型参数的目录
os.makedirs("params", exist_ok=True)
if __name__ == '__main__':
# 初始化模型并移动到指定设备
model = Model().to(DEVICE)
# 初始化优化器:使用AdamW优化器(适合Transformer模型)
optimizer = AdamW(model.parameters(), lr=LEARNING_RATE)
# 初始化学习率调度器:当指标停止改善时降低学习率
scheduler = ReduceLROnPlateau(
optimizer,
mode='min', # 针对损失最小化
factor=0.5, # 学习率降低因子
patience=3, # 多少个epoch没有改善后调整
)
# 定义损失函数:交叉熵损失(适用于分类任务)
loss_func = torch.nn.CrossEntropyLoss()
# 早停机制相关变量
best_val_acc = 0.0
counter = 0 # 记录连续未改善的epoch数
# 开始训练
for epoch in range(EPOCH):
# 训练阶段
model.train() # 设置为训练模式
train_total_loss = 0.0
train_total_acc = 0.0
for batch_idx, (input_ids, attention_mask, token_type_ids, labels) in enumerate(train_loader):
# 将数据移动到目标设备
input_ids = input_ids.to(DEVICE)
attention_mask = attention_mask.to(DEVICE)
token_type_ids = token_type_ids.to(DEVICE)
labels = labels.to(DEVICE)
# 前向传播
outputs = model(input_ids, attention_mask, token_type_ids)
loss = loss_func(outputs, labels)
# 计算准确率
predictions = outputs.argmax(dim=1)
acc = (predictions == labels).sum().item() / len(labels)
# 反向传播和参数更新
optimizer.zero_grad() # 清空梯度
loss.backward() # 计算梯度
optimizer.step() # 更新参数
# 累计损失和准确率
train_total_loss += loss.item()
train_total_acc += acc
# 定期打印训练信息
if batch_idx % 5 == 0:
print(f"训练轮次:{epoch + 1}/{EPOCH}, 批次:{batch_idx}/{len(train_loader)}, "
f"损失:{loss.item():.4f}, 准确率:{acc:.4f}")
# 计算训练集平均损失和准确率
avg_train_loss = train_total_loss / len(train_loader)
avg_train_acc = train_total_acc / len(train_loader)
print(f"训练集==>轮次:{epoch + 1}, 平均损失:{avg_train_loss:.4f}, 平均准确率:{avg_train_acc:.4f}")
# 验证阶段
model.eval() # 设置为评估模式(关闭dropout等)
val_total_loss = 0.0
val_total_acc = 0.0
with torch.no_grad(): # 关闭梯度计算,节省内存和计算资源
for batch_idx, (input_ids, attention_mask, token_type_ids, labels) in enumerate(val_loader):
# 将数据移动到目标设备
input_ids = input_ids.to(DEVICE)
attention_mask = attention_mask.to(DEVICE)
token_type_ids = token_type_ids.to(DEVICE)
labels = labels.to(DEVICE)
# 前向传播(无需反向传播)
outputs = model(input_ids, attention_mask, token_type_ids)
loss = loss_func(outputs, labels)
# 计算准确率
predictions = outputs.argmax(dim=1)
acc = (predictions == labels).sum().item() / len(labels)
# 累计损失和准确率
val_total_loss += loss.item()
val_total_acc += acc
# 计算验证集平均损失和准确率
avg_val_loss = val_total_loss / len(val_loader)
avg_val_acc = val_total_acc / len(val_loader)
print(f"验证集==>轮次:{epoch + 1}, 平均损失:{avg_val_loss:.4f}, 平均准确率:{avg_val_acc:.4f}")
# 更新学习率调度器
scheduler.step(avg_val_loss)
# 早停机制检查
if avg_val_acc > best_val_acc:
best_val_acc = avg_val_acc
counter = 0 # 重置计数器
# 保存最佳模型
torch.save(model.state_dict(), "params/best_bert-weibo.pth")
print(f"轮次 {epoch + 1}: 验证准确率提升至 {best_val_acc:.4f}, 保存最佳模型")
else:
counter += 1
print(f"轮次 {epoch + 1}: 验证准确率未提升,计数器: {counter}/{PATIENCE}")
if counter >= PATIENCE:
print(f"早停触发:连续 {PATIENCE} 个轮次未提升验证准确率")
break
# 定期保存模型
# if (epoch + 1) % 10 == 0:
# torch.save(model.state_dict(), f"params/epoch_{epoch+1}_bert-weibo.pth")
# print(f"轮次 {epoch+1} 模型参数保存成功")
print("训练完成!")
使用设备: cpu
训练集大小: 50000, 验证集大小: 5000
训练轮次:1/50, 批次:0/500, 损失:2.0155, 准确率:0.3400
训练轮次:1/50, 批次:5/500, 损失:1.8310, 准确率:0.6800
训练轮次:1/50, 批次:10/500, 损失:1.7455, 准确率:0.6100
模型评估
import torch # 需确保导入PyTorch(若评估代码在单独文件中)
from torch.utils.data import DataLoader
from MyData import MyDataset # 导入自定义数据集类
from transformers import BertTokenizer # 需确保导入分词器(若单独文件)
# 假设Model类和DEVICE已在当前作用域(或需额外导入:from net import Model)
def collate_fn_eval(data, tokenizer, max_length=500):
"""
模型评估专用的数据处理函数:将原始测试数据转换为模型可接受的张量格式
Args:
data: 原始测试数据列表,每个元素为 (text, label) 元组(text是待评估文本,label是真实标签)
tokenizer: 预初始化的BERT分词器(避免在函数内重复初始化,节省资源)
max_length: 文本最大截断/填充长度(与训练时保持一致,避免分布偏移)
Returns:
input_ids: 文本编码后的token ID张量,shape=(batch_size, max_length)
attention_mask: 注意力掩码张量(区分真实token和填充token),shape同上
token_type_ids: 句子类型掩码张量(单句任务全为0),shape同上
labels: 真实标签张量(长整型),shape=(batch_size,)
"""
# 1. 从原始数据中分离文本和真实标签
texts = [item[0] for item in data] # 提取所有待评估文本
true_labels = [item[1] for item in data] # 提取所有真实标签(用于计算准确率)
# 2. 使用BERT分词器对文本进行批量编码(与训练时参数严格一致)
encoded_batch = tokenizer.batch_encode_plus(
batch_text_or_text_pairs=texts, # 待编码的批量文本
truncation=True, # 文本长度超过max_length时自动截断
padding="max_length", # 文本长度不足时用[PAD]填充到max_length
max_length=max_length, # 统一文本长度(必须与训练时相同)
return_tensors="pt", # 返回PyTorch张量格式
return_length=False # 评估阶段无需返回文本原始长度,减少计算量
)
# 3. 提取编码后的关键张量
input_ids = encoded_batch["input_ids"] # token ID矩阵
attention_mask = encoded_batch["attention_mask"] # 注意力掩码
token_type_ids = encoded_batch["token_type_ids"] # 句子类型掩码(单句任务用)
# 4. 将真实标签转换为PyTorch长整型张量(匹配模型输出格式)
true_labels_tensor = torch.LongTensor(true_labels)
return input_ids, attention_mask, token_type_ids, true_labels_tensor
def evaluate_model(model, test_dataset, tokenizer, DEVICE, batch_size=100, max_length=500):
"""
模型评估主函数:计算测试集上的整体准确率,并打印批次级评估结果
Args:
model: 已加载训练权重的模型(需确保已移至指定设备)
test_dataset: 测试集数据集对象(MyDataset实例)
tokenizer: 预初始化的BERT分词器
DEVICE: 评估使用的设备(CPU/GPU)
batch_size: 评估批次大小(根据设备内存调整,CPU建议减小)
max_length: 文本最大长度(与训练时保持一致)
Returns:
overall_acc: 测试集整体准确率(小数形式,如0.95表示95%)
"""
# 1. 创建测试集数据加载器(评估阶段无需打乱数据,shuffle=False)
test_loader = DataLoader(
dataset=test_dataset,
batch_size=batch_size,
shuffle=False, # 评估时打乱无意义,且会导致批次结果不可复现
drop_last=True, # 丢弃最后一个不完整批次(避免批次大小不一致导致的计算问题)
# 传入带参数的collate_fn(通过lambda封装,传递分词器和max_length)
collate_fn=lambda x: collate_fn_eval(x, tokenizer=tokenizer, max_length=max_length)
)
# 2. 初始化评估指标计数器
correct_predictions = 0 # 正确预测的样本总数
total_samples = 0 # 参与评估的样本总数(已排除不完整批次)
# 3. 进入评估模式(核心!关闭Dropout、BatchNorm等训练专属层)
model.eval()
# 4. 关闭梯度计算(评估阶段无需反向传播,大幅减少内存占用和计算时间)
with torch.no_grad():
# 遍历测试集所有批次
for batch_idx, (input_ids, attention_mask, token_type_ids, true_labels) in enumerate(test_loader):
# 4.1 将数据移至指定设备(与模型设备一致)
input_ids = input_ids.to(DEVICE)
attention_mask = attention_mask.to(DEVICE)
token_type_ids = token_type_ids.to(DEVICE)
true_labels = true_labels.to(DEVICE)
# 4.2 模型前向推理(仅计算输出,不计算梯度)
model_outputs = model(input_ids, attention_mask, token_type_ids)
# 4.3 从模型输出中获取预测标签(取概率最大的类别,dim=1表示按样本维度)
predicted_labels = model_outputs.argmax(dim=1)
# 4.4 统计当前批次的正确预测数和样本数
batch_correct = (predicted_labels == true_labels).sum().item() # 本批次正确数
batch_total = len(true_labels) # 本批次总样本数
# 4.5 累加至全局计数器
correct_predictions += batch_correct
total_samples += batch_total
# 4.6 打印当前批次评估结果(便于监控进度,可选)
print(f"评估批次 [{batch_idx+1}/{len(test_loader)}] | 本批次正确数: {batch_correct} | 本批次总数: {batch_total}")
# 5. 计算测试集整体准确率(避免除零错误)
if total_samples == 0:
print("警告:无有效样本参与评估(可能测试集为空或批次全被丢弃)")
overall_acc = 0.0
else:
overall_acc = correct_predictions / total_samples
# 打印最终评估结果(保留4位小数,更直观)
print(f"\n=== 测试集评估完成 ===")
print(f"总正确样本数: {correct_predictions}")
print(f"总参与样本数: {total_samples}")
print(f"测试集整体准确率: {overall_acc:.4f} ({overall_acc*100:.2f}%)")
return overall_acc
# ------------------- 评估函数调用示例 -------------------
if __name__ == "__main__":
# 1. 初始化必要组件(需根据实际路径调整)
# 1.1 加载分词器(与训练时使用的分词器一致,本地/在线加载均可)
tokenizer = BertTokenizer.from_pretrained(
r"D:\learn\learncode\huggingface\demo_4\trasnFormers_test\model\bert-base-chinese\models--bert-base-chinese\snapshots\c30a6ed22ab4564dc1e3b2ecbf6e766b0611a33f"
)
# 1.2 加载测试集数据集(指定split为"test")
test_dataset = MyDataset("test")
print(f"测试集总样本数: {len(test_dataset)}")
# 1.3 初始化模型并加载训练好的权重(关键!需确保模型结构与训练时一致)
model = Model().to(DEVICE) # 初始化模型并移至设备
# 加载最佳模型权重(路径需与训练时保存的路径一致)
model.load_state_dict(torch.load("params/best_bert-weibo.pth", map_location=DEVICE))
print(f"模型权重加载完成,使用设备: {DEVICE}")
# 2. 调用评估函数进行测试
test_accuracy = evaluate_model(
model=model,
test_dataset=test_dataset,
tokenizer=tokenizer,
DEVICE=DEVICE,
batch_size=100, # CPU建议设为32/64,避免内存不足
max_length=500 # 必须与训练时的max_length相同
)
模型测试
import torch
from net import Model
from transformers import BertTokenizer
import os # 用于检查权重文件是否存在
# -------------------------- 1. 基础配置(集中管理,便于修改) --------------------------
# 1.1 设备配置:优先GPU,无则用CPU
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 1.2 情绪类别映射(与训练时的label顺序严格一致!)
EMOTION_LABELS = [
"like", # 喜爱
"disgust", # 厌恶
"happiness", # 开心
"sadness", # 悲伤
"anger", # 愤怒
"surprise", # 惊讶
"fear", # 恐惧
"none" # 无特定情绪
]
# 1.3 模型相关路径(权重文件、分词器)
MODEL_WEIGHT_PATH = "params/2bert-weibo.pth" # 训练好的模型权重
TOKENIZER_NAME = "bert-base-chinese" # BERT中文分词器(本地/在线均可)
MAX_SEQ_LENGTH = 500 # 文本最大长度(必须与训练时一致!)
# -------------------------- 2. 单条文本预处理函数(简化逻辑,去除冗余) --------------------------
def preprocess_single_text(text, tokenizer, max_seq_len):
"""
单条文本预处理:将输入文本转换为模型可接受的张量格式(input_ids、attention_mask等)
Args:
text: 用户输入的原始文本(str)
tokenizer: 初始化后的BERT分词器
max_seq_len: 文本最大长度(与训练时一致)
Returns:
预处理后的张量(已移至指定设备)
"""
# 直接对单条文本编码(无需用列表包裹再解包,简化逻辑)
encoded = tokenizer.encode_plus(
text=text, # 单条输入文本
truncation=True, # 超过max_seq_len时截断
padding="max_length", # 不足时用[PAD]填充到max_seq_len
max_length=max_seq_len, # 统一文本长度
return_tensors="pt", # 返回PyTorch张量
return_attention_mask=True, # 返回注意力掩码
return_token_type_ids=True, # 返回句子类型掩码(单句任务用)
return_length=False # 预测阶段无需返回文本长度,去除冗余
)
# 提取张量并移至指定设备(避免后续重复操作)
input_ids = encoded["input_ids"].to(DEVICE)
attention_mask = encoded["attention_mask"].to(DEVICE)
token_type_ids = encoded["token_type_ids"].to(DEVICE)
return input_ids, attention_mask, token_type_ids
# -------------------------- 3. 模型初始化与加载(增加异常处理,提升鲁棒性) --------------------------
def init_model_and_tokenizer():
"""
初始化模型和分词器:加载权重、检查文件有效性,返回可用的模型和分词器
Returns:
model: 加载好权重的模型(已移至指定设备,处于评估模式)
tokenizer: 初始化后的BERT分词器
"""
# 3.1 初始化分词器(在线加载,若本地有缓存会自动使用)
try:
tokenizer = BertTokenizer.from_pretrained(TOKENIZER_NAME)
print(f"✅ 成功加载分词器:{TOKENIZER_NAME}")
except Exception as e:
raise RuntimeError(f"❌ 分词器加载失败!错误信息:{str(e)}")
# 3.2 检查模型权重文件是否存在
if not os.path.exists(MODEL_WEIGHT_PATH):
raise FileNotFoundError(f"❌ 模型权重文件不存在!路径:{MODEL_WEIGHT_PATH}")
# 3.3 初始化模型并加载权重(处理设备不匹配问题)
try:
# 初始化模型结构并移至设备
model = Model().to(DEVICE)
# 加载权重:用map_location确保设备兼容(如GPU训练→CPU预测)
model.load_state_dict(
torch.load(MODEL_WEIGHT_PATH, map_location=DEVICE)
)
# 设为评估模式(关闭Dropout、BatchNorm,确保预测结果稳定)
model.eval()
print(f"✅ 成功加载模型权重:{MODEL_WEIGHT_PATH}")
print(f"✅ 当前使用设备:{DEVICE}")
except Exception as e:
raise RuntimeError(f"❌ 模型加载失败!错误信息:{str(e)}")
return model, tokenizer
# -------------------------- 4. 交互式预测主函数(优化体验,增加异常处理) --------------------------
def interactive_prediction(model, tokenizer):
"""
交互式预测:循环接收用户输入,输出模型预测的情绪类别,支持退出
Args:
model: 加载好的模型(评估模式)
tokenizer: 初始化后的分词器
"""
print("\n" + "="*50)
print("🎯 情绪分类模型交互式预测(输入'q'或'quit'退出)")
print("📋 支持的情绪类别:", ", ".join(EMOTION_LABELS))
print("="*50 + "\n")
while True:
# 接收用户输入(处理空输入)
user_input = input("请输入要预测的文本:").strip()
# 退出逻辑(支持'q'或'quit',更灵活)
if user_input.lower() in ["q", "quit"]:
print("\n👋 测试结束,再见!")
break
# 处理空输入(避免无意义的预测)
if not user_input:
print("⚠️ 请不要输入空文本,请重新输入!\n")
continue
# -------------------------- 预测核心逻辑 --------------------------
try:
# 1. 预处理输入文本
input_ids, attention_mask, token_type_ids = preprocess_single_text(
text=user_input,
tokenizer=tokenizer,
max_seq_len=MAX_SEQ_LENGTH
)
# 2. 模型预测(关闭梯度计算,节省内存)
with torch.no_grad():
# 前向推理:获取模型输出(logits)
model_output = model(input_ids, attention_mask, token_type_ids)
# 取概率最大的类别(argmax(dim=1):按样本维度找最大值索引)
predicted_idx = model_output.argmax(dim=1).item() # .item()转为Python数值
# 映射为情绪类别名称
predicted_emotion = EMOTION_LABELS[predicted_idx]
# 3. 输出预测结果(格式友好)
print(f"✅ 模型预测结果:【{predicted_emotion}】\n")
# 捕获预测过程中的异常(避免脚本崩溃)
except Exception as e:
print(f"❌ 预测失败!错误信息:{str(e)}\n")
# -------------------------- 5. 主入口(统一调用,结构清晰) --------------------------
if __name__ == "__main__":
try:
# 初始化模型和分词器
model, tokenizer = init_model_and_tokenizer()
# 启动交互式预测
interactive_prediction(model, tokenizer)
# 捕获全局异常(如初始化失败)
except Exception as e:
print(f"\n❌ 程序启动失败:{str(e)}")
补充
下游任务模型设计为什么out数据只取:[:, 0]
一、先明确两个核心概念
二、用具体例子拆解(带数据表格)
步骤 1:原始文本与分词结果
步骤 2:out.last_hidden_state 的形状与数据形式
步骤 3:[:, 0] 操作 —— 提取 [CLS] 特征

浙公网安备 33010602011771号