一、序
该项目预训练模型源自huggingface官网google-bert/bert-base-chinese,在本地计算机上我们只进行下游任务,通过微调预训练模型实现基于 BERT 的酒店评价情感分类。
google-bert/bert-base-chinese at main (hf-mirror.com)
一段话概述:
这篇博客概括得很好
二、Bert的具体训练框架
2.1 目录架构
从上往下说,
.idea目录
,含检查配置、模块配置等 XML 文件,不用管bert-base-chinese目录
,预训练模型文件,包含以下内容
这里的config,json代码如下:
{
"architectures": [
"BertForMaskedLM" // 模型架构类型,适用于掩码语言模型预训练任务
],
"attention_probs_dropout_prob": 0.1, // 注意力权重的dropout概率,防止过拟合
"directionality": "bidi", // 双向Transformer(BERT的核心特性)
"hidden_act": "gelu", // 隐藏层激活函数为GELU(Gaussian Error Linear Unit)
"hidden_dropout_prob": 0.1, // 隐藏层输出的dropout概率
"hidden_size": 768, // 隐藏层维度大小,即Transformer编码器的输出维度
"initializer_range": 0.02, // 权重初始化的标准差范围
"intermediate_size": 3072, // 前馈网络层的中间维度大小(通常为hidden_size的4倍)
"layer_norm_eps": 1e-12, // LayerNorm层的epsilon值,防止数值不稳定
"max_position_embeddings": 512, // 最大位置编码,支持的输入序列最大长度
"model_type": "bert", // 模型类型标识为BERT
"num_attention_heads": 12, // 多头注意力机制中的注意力头数量
"num_hidden_layers": 12, // Transformer编码器的层数(对应BERT-base)
"pad_token_id": 0, // 填充标记的ID,用于批次处理时对齐序列长度
"pooler_fc_size": 768, // 池化层全连接层的维度大小
"pooler_num_attention_heads": 12, // 池化层注意力头数量
"pooler_num_fc_layers": 3, // 池化层中全连接层的数量
"pooler_size_per_head": 128, // 每个注意力头的维度大小
"pooler_type": "first_token_transform", // 池化类型,使用[CLS]标记作为句子表示
"type_vocab_size": 2, // 句子类型词汇表大小(用于区分句子对A和B)
"vocab_size": 21128 // 词汇表大小,决定了模型能够处理的不同token数量
}
tokenizer.json部分代码示例如下:
就是分词器,原理和上一小节介绍的原理一样。
model_save
目录,存储训练过程中保存的模型 checkpoint(如best_model.pth5ckpt
),用于模型持久化和恢复训练。model_utils
目录(模型工具模块)
- 执行与测试文件
- 数据集文件
2.2 数据预处理
data.py
# data负责产生两个dataloader,即训练集和验证集
from torch.utils.data import DataLoader, Dataset
from sklearn.model_selection import train_test_split # 给X,Y和分割比例,分割出训练集和验证集的X, Y
import torch
def read_file(path):
"""读取文本文件,返回数据和标签列表"""
data = [] # 存储文本内容
label = [] # 存储对应标签
with open(path, "r", encoding="utf-8") as f:
for i, line in enumerate(f):
if i == 0: # 跳过标题行
continue
if i > 200 and i < 7500: # 跳过中间部分数据(可能是为了减少样本量)
continue
line = line.strip("\n") # 去除换行符
line = line.split(",", 1) # 按逗号分割,只分割一次(处理文本中可能包含的逗号)
data.append(line[1]) # 文本内容
label.append(line[0]) # 标签
print(f"读取了{len(data)}条数据")
return data, label
# file = "../jiudian.txt"
# read_file(file)
class jdDataset(Dataset):
"""自定义数据集类,用于包装文本数据和标签"""
def __init__(self, data, label):
self.X = data # 文本数据
self.Y = torch.LongTensor([int(i) for i in label]) # 标签转为LongTensor类型
def __getitem__(self, item):
return self.X[item], self.Y[item] # 返回索引对应的文本和标签
def __len__(self):
return len(self.Y) # 返回数据集大小
def get_data_loader(path, batchsize, val_size=0.2):
"""读取数据,分割为训练集和验证集,并返回对应的DataLoader"""
data, label = read_file(path) # 读取文件
# 按指定比例分割训练集和验证集,保持类别分布相同(stratify=label)
train_x, val_x, train_y, val_y = train_test_split(data, label, test_size=val_size, shuffle=True, stratify=label)
train_set = jdDataset(train_x, train_y) # 创建训练集
val_set = jdDataset(val_x, val_y) # 创建验证集
train_loader = DataLoader(train_set, batchsize, shuffle=True) # 训练集数据加载器
val_loader = DataLoader(val_set, batchsize, shuffle=True) # 验证集数据加载器
return train_loader, val_loader
if __name__ == "__main__":
get_data_loader("../waimai.txt", 2) # 测试数据加载功能,批次大小为2
2.3 模型搭建
model.py
import torch
import torch.nn as nn
from transformers import BertModel, BertTokenizer, BertConfig
class myBertModel(nn.Module):
"""基于BERT的文本分类模型"""
def __init__(self, bert_path, num_class, device):
"""
初始化模型
:param bert_path: BERT预训练模型路径
:param num_class: 分类类别数量
:param device: 运行设备(CPU/GPU)
"""
super(myBertModel, self).__init__()
# 加载预训练的BERT模型
self.bert = BertModel.from_pretrained(bert_path)
# 也可以通过配置文件加载模型(当前被注释)
# config = BertConfig.from_pretrained(bert_path)
# self.bert = BertModel(config)
self.device = device
# 分类头:将BERT的768维输出映射到指定类别数
self.cls_head = nn.Linear(768, num_class)
# 加载BERT分词器
self.tokenizer = BertTokenizer.from_pretrained(bert_path)
def forward(self, text):
"""
前向传播过程
:param text: 输入文本列表
:return: 分类预测结果
"""
# 对输入文本进行分词处理
input = self.tokenizer(text,
return_tensors="pt", # 返回PyTorch张量
truncation=True, # 截断过长文本
padding="max_length", # 填充至最大长度
max_length=128) # 最大序列长度
# 将输入张量移至指定设备
input_ids = input["input_ids"].to(self.device)
token_type_ids = input['token_type_ids'].to(self.device)
attention_mask = input['attention_mask'].to(self.device)
# 通过BERT模型获取输出
# sequence_out: 每个token的隐藏状态 [batch_size, seq_len, 768]
# pooler_out: [CLS]标记的聚合表示 [batch_size, 768]
sequence_out, pooler_out = self.bert(input_ids=input_ids,
token_type_ids=token_type_ids,
attention_mask=attention_mask,
return_dict=False) # 使用元组形式返回结果
# 通过分类头进行预测
pred = self.cls_head(pooler_out)
return pred
if __name__ == "__main__":
# 设置运行设备
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 实例化模型并移至指定设备
model = myBertModel("../bert-base-chinese", 2, device) # 二分类任务
model.to(device) # 将模型参数移至GPU/CPU
# 示例预测
text = "今天天气真好"
model.eval() # 设置为评估模式(关闭dropout等训练特有的层)
with torch.no_grad(): # 关闭梯度计算,节省内存和计算资源
pred = model(text)
print(pred) # 输出预测结果(未经过softmax,为logits值)
2.4 训练与验证
train.py
import torch
import time
import matplotlib.pyplot as plt
import numpy as np
from tqdm import tqdm
def train_val(para):
"""
模型训练和验证的主函数
:param para: 参数字典,包含模型、数据加载器、优化器等训练所需组件
"""
########################################################
# 从参数字典中解包训练所需组件
model = para['model']
train_loader = para['train_loader']
val_loader = para['val_loader']
scheduler = para['scheduler']
optimizer = para['optimizer']
loss = para['loss']
epoch = para['epoch']
device = para['device']
save_path = para['save_path']
max_acc = para['max_acc']
val_epoch = para['val_epoch']
#################################################
# 初始化训练和验证过程的记录列表
plt_train_loss = [] # 训练损失记录
plt_train_acc = [] # 训练准确率记录
plt_val_loss = [] # 验证损失记录
plt_val_acc = [] # 验证准确率记录
val_rel = [] # 验证预测结果记录
for i in range(epoch):
start_time = time.time() # 记录每个epoch开始时间
model.train() # 设置模型为训练模式
train_loss = 0.0 # 累积训练损失
train_acc = 0.0 # 累积训练准确率
val_acc = 0.0 # 累积验证准确率
val_loss = 0.0 # 累积验证损失
# 训练循环,使用tqdm显示进度条
for batch in tqdm(train_loader):
model.zero_grad() # 清除上一步的梯度
text, labels = batch[0], batch[1].to(device) # 获取文本和标签并移至设备
pred = model(text) # 前向传播
bat_loss = loss(pred, labels) # 计算损失
bat_loss.backward() # 反向传播
optimizer.step() # 更新参数
scheduler.step() # 学习率调度器更新学习率
optimizer.zero_grad() # 梯度清零
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) # 梯度裁剪,防止梯度爆炸
train_loss += bat_loss.item() # 累积损失
# 累积正确预测数
train_acc += np.sum(np.argmax(pred.cpu().data.numpy(), axis=1) == labels.cpu().numpy())
# 计算并记录训练损失和准确率
plt_train_loss.append(train_loss / train_loader.dataset.__len__())
plt_train_acc.append(train_acc / train_loader.dataset.__len__())
# 每val_epoch次执行一次验证
if i % val_epoch == 0:
model.eval() # 设置模型为评估模式
with torch.no_grad(): # 关闭梯度计算,节省内存和计算资源
for batch in tqdm(val_loader):
val_text, val_labels = batch[0], batch[1].to(device) # 获取验证数据
val_pred = model(val_text) # 前向传播
val_bat_loss = loss(val_pred, val_labels) # 计算损失
val_loss += val_bat_loss.cpu().item() # 累积损失
# 累积正确预测数
val_acc += np.sum(np.argmax(val_pred.cpu().data.numpy(), axis=1) == val_labels.cpu().numpy())
val_rel.append(val_pred) # 记录预测结果
# 如果当前验证准确率高于之前的最高准确率,保存模型
if val_acc > max_acc:
torch.save(model, save_path + str(epoch) + "ckpt")
max_acc = val_acc
# 记录验证损失和准确率
plt_val_loss.append(val_loss / val_loader.dataset.__len__())
plt_val_acc.append(val_acc / val_loader.dataset.__len__())
# 打印训练和验证指标
print('[%03d/%03d] %2.2f sec(s) TrainAcc : %3.6f TrainLoss : %3.6f | valAcc: %3.6f valLoss: %3.6f ' % \
(i, epoch, time.time() - start_time, plt_train_acc[-1], plt_train_loss[-1], plt_val_acc[-1], plt_val_loss[-1]))
# 每50个epoch保存一次模型
if i % 50 == 0:
torch.save(model, save_path + '-epoch:' + str(i) + '-%.2f' % plt_val_acc[-1])
else:
# 如果不执行验证,沿用上次的验证结果
plt_val_loss.append(plt_val_loss[-1])
plt_val_acc.append(plt_val_acc[-1])
print('[%03d/%03d] %2.2f sec(s) TrainAcc : %3.6f TrainLoss : %3.6f ' % \
(i, epoch, time.time() - start_time, plt_train_acc[-1], plt_train_loss[-1]))
main.py
import random
import torch
import torch.nn as nn
import numpy as np
import os
# 导入工具库
# random:设置随机种子
# torch:PyTorch深度学习框架
# torch.nn:神经网络模块
# numpy:数值计算库
# os:操作系统相关功能
from model_utils.data import get_data_loader
from model_utils.model import myBertModel
from model_utils.train import train_val
# 从自定义模块导入功能:
# data模块获取数据加载器
# model模块获取自定义BERT模型
# train模块获取训练验证主函数
def seed_everything(seed):
"""设置全局随机种子,确保实验可复现"""
torch.manual_seed(seed) # 设置CPU随机种子
torch.cuda.manual_seed(seed) # 设置当前GPU随机种子
torch.cuda.manual_seed_all(seed) # 设置所有GPU随机种子
torch.backends.cudnn.benchmark = False # 关闭CuDNN自动优化(保证确定性)
torch.backends.cudnn.deterministic = True # 强制CuDNN使用确定性算法
random.seed(seed) # 设置Python内置随机种子
np.random.seed(seed) # 设置NumPy随机种子
os.environ['PYTHONHASHSEED'] = str(seed) # 固定Python哈希种子
#################################################################
seed_everything(0) # 设置随机种子为0,确保实验可复现
###############################################
# 超参数配置
lr = 0.0001 # 学习率(AdamW优化器初始学习率)
batchsize = 16 # 批量大小(每次输入模型的样本数)
loss = nn.CrossEntropyLoss() # 损失函数(适用于多分类任务)
bert_path = "bert-base-chinese" # 预训练模型路径(中文BERT-base)
num_class = 2 # 分类类别数(二分类任务)
data_path = "waimai.txt" # 数据集路径(外卖文本数据)
max_acc = 0.6 # 初始最高验证准确率(用于模型保存对比)
device = "cuda" if torch.cuda.is_available() else "cpu" # 自动检测设备(优先GPU)
model = myBertModel(bert_path, num_class, device).to(device) # 实例化模型并移至目标设备
# 优化器配置
optimizer = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=0.00001)
# AdamW优化器:
# - model.parameters():优化模型所有参数
# - weight_decay:权重衰减(L2正则化),防止过拟合
# 数据加载
train_loader, val_loader = get_data_loader(data_path, batchsize)
# 调用数据模块函数:
# - 读取数据并分割为训练集/验证集
# - 返回DataLoader对象,支持批量加载数据
# 训练配置
epochs = 5 # 训练总轮数(epoch)
save_path = "model_save/best_model.pth" # 最佳模型保存路径
scheduler = torch.optim.lr_scheduler.CosineAnnealingWarmRestarts(optimizer, T_0=20, eta_min=1e-9)
# 学习率调度器:
# - CosineAnnealingWarmRestarts:余弦退火+重启机制
# - T_0=20:第一次重启前的epoch数
# - eta_min=1e-9:学习率最小值
val_epoch = 1 # 验证频率(每1个epoch执行一次验证)
# 参数字典(整合训练所需组件)
para = {
"model": model, # 模型对象
"train_loader": train_loader, # 训练数据加载器
"val_loader": val_loader, # 验证数据加载器
"scheduler": scheduler, # 学习率调度器
"optimizer": optimizer, # 优化器
"loss": loss, # 损失函数
"epoch": epochs, # 总训练轮数
"device": device, # 运行设备
"save_path": save_path, # 模型保存路径
"max_acc": max_acc, # 初始最高准确率
"val_epoch": val_epoch # 验证间隔轮数
}
train_val(para) # 启动训练和验证流程(调用train模块主函数)
2.5 运行
本小节结束。