引言
-
背景介绍:
-
简要介绍 BERT(Bidirectional Encoder Representations from Transformers)模型及其在自然语言处理(NLP)中的重要性。
-
说明本文的目标:通过一个完整的项目(包括
main.py
、train.py
、data.py
、model.py
),讲解如何使用 BERT 进行文本分类。
-
-
项目概述:
-
项目是一个二分类任务(例如情感分析、垃圾邮件分类等)。
-
使用 Hugging Face 的
transformers
库加载预训练的 BERT 模型。 -
代码结构清晰,分为数据加载、模型定义、训练和主程序四个部分
-
代码详解
1. data.py
:数据加载与预处理
-
功能:
-
加载文本数据。
-
对文本进行分词和编码。
-
构建数据加载器(
DataLoader
)。
-
from torch.utils.data import DataLoader, Dataset
from sklearn.model_selection import train_test_split
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("读取了%d条数据" % len(data))
return data, label
class jdDataset(Dataset):
def __init__(self, data, label):
self.X = data
self.Y = torch.LongTensor([int(i) for i in label]) # 标签转为整数
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):
data, label = read_file(path)
# 分割训练集和验证集
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)
# 构建 DataLoader
train_loader = DataLoader(train_set, batchsize, shuffle=True)
val_loader = DataLoader(val_set, batchsize, shuffle=True)
return train_loader, val_loader
具体步骤:
- 读取文件:从指定路径的文件中读取数据。每行数据按逗号分割一次,分割后的第一部分是标签,第二部分是文本数据。
-
自定义数据集类jdDataset:
-
self.X
:存储文本数据。 -
self.Y
:将标签转换为整数并存储为 PyTorch 的LongTensor
。 -
__getitem__
:返回指定索引的文本和标签。 -
__len__
:返回数据集的大小。
-
-
构建数据加载器:
-
train_test_split
:将数据分割为训练集和验证集,test_size=0.2
表示验证集占 20%。 -
shuffle=True
:打乱数据顺序。 -
stratify=label
:确保训练集和验证集的标签分布一致。 -
jdDataset
:将分割后的数据包装为自定义数据集。 -
DataLoader
:构建批量数据加载器,支持批量加载和打乱数据。
-
2. model.py
:模型定义
-
功能:
-
定义基于 BERT 的文本分类模型。
-
-
关键点:
-
使用
BertModel
加载预训练 BERT 模型。 -
在 BERT 的输出后添加一个全连接层(
nn.Linear
)用于分类。 -
使用
BertTokenizer
对输入文本进行分词和编码。
-
2.1 导入依赖库
import torch
import torch.nn as nn
from transformers import BertModel, BertTokenizer
-
torch
和torch.nn
:-
PyTorch 的核心库,用于构建神经网络。
-
nn.Module
是 PyTorch 中所有神经网络模块的基类。
-
-
transformers
:-
Hugging Face 的
transformers
库,提供了预训练的 BERT 模型和分词器。 -
BertModel
:BERT 模型的主体部分。 -
BertTokenizer
:用于将文本转换为 BERT 模型所需的输入格式。
-
2.2 定义 BERT 分类模型
class myBertModel(nn.Module):
def __init__(self, bert_path, num_class, device):
super(myBertModel, self).__init__()
self.bert = BertModel.from_pretrained(bert_path) # 加载预训练 BERT 模型
self.device = device
self.cls_head = nn.Linear(768, num_class) # 分类头
self.tokennizer = BertTokenizer.from_pretrained(bert_path) # 分词器
-
功能:
-
定义一个基于 BERT 的文本分类模型。
-
-
关键点:
-
bert_path
:预训练 BERT 模型的路径(如bert-base-chinese
)。 -
num_class
:分类任务的类别数(如二分类任务为 2)。 -
device
:模型运行的设备(如cuda
或cpu
)。 -
self.bert
:加载预训练的 BERT 模型。 -
self.cls_head
:在 BERT 的输出后添加一个全连接层(nn.Linear
),用于分类。-
输入维度为 768(BERT 输出的隐藏层大小)。
-
输出维度为
num_class
(分类类别数)。
-
-
self.tokennizer
:加载与 BERT 模型对应的分词器。
-
BertTokenizer
:这是transformers
库中专门为 BERT 模型设计的分词器类。它的主要作用是将文本字符串分割成一系列的词元(tokens),并将这些词元转换为对应的整数 ID,这些 ID 可以作为 BERT 模型的输入。
from_pretrained
方法:这是一个类方法,用于从预训练的模型路径加载分词器。bert_path
是预训练 BERT 模型的路径,可以是本地路径,也可以是 Hugging Face 模型库中的模型名称(如'bert-base-uncased'
)。通过这个方法,分词器会加载与预训练 BERT 模型相同的词表,确保分词结果与模型的输入要求一致。
2.3 前向传播逻辑
def forward(self, text):
input = self.tokennizer(text, return_tensors="pt", 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)
sequence_out, pooler_out = self.bert(input_ids=input_ids,
token_type_ids=token_type_ids,
attention_mask=attention_mask,
return_dict=False) # return_dict
pred = self.cls_head(pooler_out)
return pred
定义模型的前向传播逻辑,将输入文本转换为分类结果。
关键点:
-
分词与编码:
-
self.tokennizer(text, return_tensors="pt", truncation=True, padding="max_length", max_length=128)
:-
将输入文本分词并转换为 BERT 模型所需的输入格式。
-
return_tensors="pt"
:返回 PyTorch 张量。 -
truncation=True
:如果文本长度超过max_length
,则截断。 -
padding="max_length"
:如果文本长度不足max_length
,则填充。 -
max_length=128
:输入文本的最大长度。
-
-
返回的
input
包含以下字段:-
input_ids
:分词后的 token ID。 -
token_type_ids
:用于区分句子的 token 类型 ID(单句分类任务中通常为全 0)。 -
attention_mask
:用于标识哪些 token 是实际文本,哪些是填充部分。
-
-
-
设备迁移:
-
input_ids.to(self.device)
、token_type_ids.to(self.device)
、attention_mask.to(self.device)
:-
将输入数据迁移到指定设备(如 GPU)。
-
-
-
BERT 模型推理:
-
sequence_out, pooler_out = self.bert(...)
:-
sequence_out
:BERT 模型的所有隐藏层输出(形状为[batch_size, sequence_length, hidden_size]
)。 -
pooler_out
:BERT 模型的池化输出(形状为[batch_size, hidden_size]
),通常用于分类任务。
-
-
-
分类头:
-
pred = self.cls_head(pooler_out)
:-
将 BERT 的池化输出通过全连接层,得到分类结果。
-
-
-
返回结果:
-
return pred
:返回分类结果。
-
BertModel
是 Hugging Facetransformers
库中预训练的 BERT 模型,当调用BertModel
的前向传播方法时,它会根据输入的文本数据进行特征提取,并返回一系列的输出。在代码中,通过设置return_dict=False
,BertModel
会以元组的形式返回输出结果,其中第一个元素是sequence_out
,第二个元素是pooler_out
。
sequence_out
- 含义:
sequence_out
是 BERT 模型最后一层的隐藏状态(hidden states),它包含了输入序列中每个词元(token)的上下文表示。具体来说,sequence_out
是一个形状为(batch_size, sequence_length, hidden_size)
的张量。
batch_size
:表示输入的样本数量。sequence_length
:表示输入序列的长度,在代码中通过max_length=128
进行了限定。hidden_size
:表示 BERT 模型的隐藏层维度,对于bert-base
模型,这个值通常是 768。- 用途:
sequence_out
可以用于需要获取每个词元上下文表示的任务,例如命名实体识别(NER)、词性标注(POS tagging)等。在这些任务中,我们需要对输入序列中的每个词元进行分类,因此可以使用sequence_out
中每个词元的表示作为特征进行分类。
pooler_out
- 含义:
pooler_out
是 BERT 模型对整个输入序列的一个整体表示。它是通过对[CLS]
标记的隐藏状态进行线性变换和激活函数处理得到的。[CLS]
标记是 BERT 模型在输入序列开头添加的一个特殊标记,用于表示整个序列的分类信息。pooler_out
是一个形状为(batch_size, hidden_size)
的张量。- 用途:
pooler_out
通常用于需要对整个输入序列进行分类的任务,例如文本分类、情感分析等。在这些任务中,我们只需要一个表示整个序列的特征向量,因此可以使用pooler_out
作为输入传递给后续的分类器(如代码中的self.cls_head
)进行分类。
这里我们使用 pooler_out
作为输入传递给 self.cls_head
进行分类
3. train.py
文件详解
-
train.py
文件的主要功能是:-
定义训练和验证的逻辑。
-
计算损失和准确率。
-
保存最佳模型。
-
绘制训练和验证的损失和准确率曲线。
-
3.1 导入依赖库
import torch
import time
import matplotlib.pyplot as plt
import numpy as np
from tqdm import tqdm
-
torch
:PyTorch 的核心库,用于张量操作和模型训练。 -
time
:用于计算训练时间。 -
matplotlib.pyplot
:用于绘制损失和准确率曲线。 -
numpy
:用于数值计算。 -
tqdm
:用于显示训练进度条。
3.2 定义训练和验证函数
def train_val(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']
从参数字典 para
中提取模型、数据加载器、优化器、损失函数等参数。
-
关键点:
-
model
:训练和验证的模型。 -
train_loader
和val_loader
:训练集和验证集的数据加载器。 -
scheduler
:学习率调度器。 -
optimizer
:优化器。 -
loss
:损失函数。 -
epoch
:训练的总轮数。 -
device
:模型运行的设备(如cuda
或cpu
)。 -
save_path
:模型保存路径。 -
max_acc
:模型保存的准确率阈值。 -
val_epoch
:每隔多少轮进行一次验证。
-
3.3 初始化变量
plt_train_loss = []
plt_train_acc = []
plt_val_loss = []
plt_val_acc = []
val_rel = []
初始化用于存储训练和验证的损失和准确率的列表。
-
plt_train_loss
和plt_train_acc
:存储训练集的损失和准确率。 -
plt_val_loss
和plt_val_acc
:存储验证集的损失和准确率。 -
val_rel
:存储验证结果。
3.4 训练循环
for i in range(epoch):
start_time = time.time()
model.train()
train_loss = 0.0
train_acc = 0.0
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())
训练模型,计算损失和准确率。
-
关键点:
-
model.train()
:将模型设置为训练模式。 -
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()
:调整学习率。 -
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())
:计算训练准确率。
-
3.5 验证循环
if i % val_epoch == 0:
model.eval()
val_loss = 0.0
val_acc = 0.0
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 / len(val_loader.dataset))
plt_val_acc.append(val_acc / len(val_loader.dataset))
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]))
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]))
验证模型,计算验证损失和准确率。
-
关键点:
-
model.eval()
:将模型设置为验证模式。 -
with torch.no_grad()
:禁用梯度计算,减少内存消耗。 -
val_loss += val_bat_loss.cpu().item()
:累加验证损失。 -
val_acc += np.sum(np.argmax(val_pred.cpu().data.numpy(), axis=1) == val_labels.cpu().numpy())
:计算验证准确率。 -
if val_acc > max_acc
:如果验证准确率超过阈值,则保存模型。 -
plt_val_loss.append(val_loss / len(val_loader.dataset))
:存储验证损失。 -
plt_val_acc.append(val_acc / len(val_loader.dataset))
:存储验证准确率。
-
3.6 绘制曲线
plt.plot(plt_train_loss)
plt.plot(plt_val_loss)
plt.title('Loss')
plt.legend(['Train', 'Val'])
plt.show()
plt.plot(plt_train_acc)
plt.plot(plt_val_acc)
plt.title('Accuracy')
plt.legend(['Train', 'Val'])
plt.savefig('acc.png')
plt.show()
绘制训练和验证的损失和准确率曲线。
-
关键点:
-
plt.plot(plt_train_loss)
:绘制训练损失曲线。 -
plt.plot(plt_val_loss)
:绘制验证损失曲线。 -
plt.legend(['Train', 'Val'])
:添加图例。 -
plt.savefig('acc.png')
:保存准确率曲线图。
-
4. main.py
文件详解
4.1 导入依赖库
import os
import random
import numpy as np
import torch.nn as nn
import torch
from model_utils.data import get_data_loader
from model_utils.model import myBertModel
from model_utils.train import train_val
os
:用于操作系统相关的功能,如设置环境变量。-
random
和numpy
:用于设置随机种子,确保实验的可重复性。 -
torch
和torch.nn
:PyTorch 的核心库,用于构建神经网络。 -
get_data_loader
:从model_utils.data
中导入的数据加载函数。 -
myBertModel
:从model_utils.model
中导入的自定义 BERT 模型。 -
train_val
:从model_utils.train
中导入的训练和验证函数。
4.2 设置随机种子
def seed_everything(seed):
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
torch.backends.cudnn.benchmark = False
torch.backends.cudnn.deterministic = True
random.seed(seed)
np.random.seed(seed)
os.environ['PYTHONHASHSEED'] = str(seed)
seed_everything(0)
设置随机种子,确保实验的可重复性。
-
关键点:
-
torch.manual_seed(seed)
:设置 PyTorch 的随机种子。 -
torch.cuda.manual_seed(seed)
和torch.cuda.manual_seed_all(seed)
:设置 GPU 的随机种子。 -
torch.backends.cudnn.benchmark = False
和torch.backends.cudnn.deterministic = True
:禁用 CuDNN 的随机性,确保结果可重复。 -
random.seed(seed)
和np.random.seed(seed)
:设置 Python 和 NumPy 的随机种子。 -
os.environ['PYTHONHASHSEED'] = str(seed)
:设置 Python 的哈希种子。
-
4.3 设置超参数
lr = 0.0001
batchsize = 8
loss = nn.CrossEntropyLoss()
bert_path = "bert-base-chinese"
num_class = 2
data_path = "jiudian.txt"
device = "cuda" if torch.cuda.is_available() else "cpu"
max_acc = 0.6
设置训练的超参数。
-
关键点:
-
lr
:学习率,设置为0.0001
。 -
batchsize
:批量大小,设置为8
。 -
loss
:损失函数,使用交叉熵损失(nn.CrossEntropyLoss
)。 -
bert_path
:预训练 BERT 模型的路径,这里使用的是bert-base-chinese
。 -
num_class
:分类任务的类别数,设置为2
(二分类任务)。 -
data_path
:数据文件的路径,这里是jiudian.txt
。 -
device
:设备选择,如果有 GPU 则使用 GPU(cuda
),否则使用 CPU。 -
max_acc
:模型保存的准确率阈值,初始设置为0.6
。
-
4.4 初始化模型
model = myBertModel(bert_path, num_class, device).to(device)
初始化基于 BERT 的文本分类模型。
-
关键点:
-
调用
myBertModel
类,传入 BERT 模型路径、类别数和设备,将模型加载到指定设备上。
-
4.5 初始化优化器
optimizer = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=0.00001)
-
关键点:
-
使用
AdamW
优化器,传入模型参数、学习率和权重衰减系数。
-
4.6 加载数据
train_loader, val_loader = get_data_loader(data_path, batchsize)
加载训练集和验证集的数据加载器。
-
关键点:
-
调用
get_data_loader
函数,传入数据路径和批量大小,返回训练集和验证集的DataLoader
。
-
4.7 设置学习率调度器
scheduler = torch.optim.lr_scheduler.CosineAnnealingWarmRestarts(optimizer, T_0=20, eta_min=1e-9)
设置学习率调度器。
-
关键点:
-
使用余弦退火调度器(
CosineAnnealingWarmRestarts
),周期为20
,最小学习率为1e-9
。
-
4.8 打包参数
val_epoch = 1
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 # 训练多少轮验证一次
}
将模型、数据加载器、优化器、损失函数等参数打包到一个字典 para
中,方便传递给训练函数。
-
关键点:
-
val_epoch
:每隔多少轮进行一次验证。
-
4.9 启动训练
train_val(para)
-
功能:
-
调用
train_val
函数,传入参数字典para
,开始训练和验证模型。
-
关键点总结
-
超参数设置:
-
包括学习率、批量大小、损失函数、设备选择等。
-
-
模型初始化:
-
使用预训练的 BERT 模型,并添加一个全连接层用于分类。
-
-
数据加载:
-
使用
get_data_loader
函数加载训练集和验证集。
-
-
优化器和调度器:
-
使用
AdamW
优化器和余弦退火调度器。
-
-
训练启动:
-
调用
train_val
函数,开始训练和验证模型。
-