self-attention于bert
·
###main.py###
import torch.nn as nn
import torch
import random
import numpy as np
import os
from model_utils.data import get_dataloader
from model_utils.model import myBertModel
from model_utils.train import train_val
# 固定随机种子函数
# 机器学习实验必须固定种子,否则每次运行的随机初始化(模型参数、数据打乱、优化器更新)会导致结果不一致,无法判断模型改进是否有效;
# seed=1 是常用的固定值,也可设为 42、1024 等,只要固定即可。
def seed_everything(seed):
# 固定PyTorch CPU随机种子
torch.manual_seed(seed)
# 固定单个GPU随机种子
torch.cuda.manual_seed(seed)
# 固定所有GPU随机种子(多卡训练时用)
torch.cuda.manual_seed_all(seed)
# 关闭cudnn自动优化(避免不同运行的算法选择不同导致结果不一致)
torch.backends.cudnn.benchmark = False
# 强制cudnn使用确定性算法(保证结果可复现)
torch.backends.cudnn.deterministic = True
# 固定Python原生随机种子
random.seed(seed)
# 固定NumPy随机种子
np.random.seed(seed)
# 固定Python哈希种子(避免字典/集合等哈希操作的随机性)
os.environ['PYTHONHASHSEED'] = str(seed)
# 超参数配置(核心可调整项)
#
#
model_name = 'MyModel'
num_class = 2 # 分类任务的类别数(二分类,对应酒店评论的正面/负面)
batchSize = 32 # 批量大小(需根据GPU显存调整,6GB显存建议设为8/16,32可能显存不足)
learning_rate = 0.0001 # 学习率(BERT微调的经典学习率,远小于普通CNN/RNN的1e-3)
loss = nn.CrossEntropyLoss() # 损失函数(二分类/多分类的标准损失,自动处理one-hot标签)
epoch = 3 # 训练轮数(小数据集设3-5轮,避免过拟合)
device = 'cuda' if torch.cuda.is_available() else 'cpu'
# 数据文件路径(酒店评论数据)
data_path = "jiudian.txt"
# BERT预训练模型路径(使用huggingface的中文BERT-Base)
bert_path = 'bert-base-chinese'
# 模型保存路径(训练过程中保存最优模型)
save_path = 'model_save/'
seed_everything(1)
# 数据加载与模型初始化
##########################################
# 加载训练/验证DataLoader(batchsize=32)
train_loader, val_loader = get_dataloader(data_path, batchsize=batchSize)
# 初始化自定义BERT模型:传入预训练路径、类别数、设备,移到指定设备(GPU/CPU)
model = myBertModel(bert_path, num_class, device).to(device) #由于报错说数据不在同一设备,所以·这里传入设备保证统一
# tokenizer, model = build_model_and_tokenizer(bert_path)
# 优化器与学习率调度器配置
# (无用代码)获取模型参数列表(仅定义未使用)
param_optimizer = list(model.parameters())
# 定义优化器:AdamW(BERT微调的标准优化器,带权重衰减)
# lr=学习率,weight_decay=权重衰减(L2正则,防止过拟合)
optimizer = torch.optim.AdamW(model.parameters(), lr=learning_rate,weight_decay=0.0001)
# 学习率调度器:余弦退火+预热重启(CosineAnnealingWarmRestarts)
# T_0=20:每20个epoch重启一次学习率,eta_min=1e-9:学习率最低降到1e-9
scheduler = torch.optim.lr_scheduler.CosineAnnealingWarmRestarts(optimizer,T_0=20,eta_min=1e-9)
# 数据加载与模型初始化
#
#
trainpara = {'model': model,
'train_loader': train_loader,
'val_loader': val_loader,
'scheduler': scheduler,
'optimizer': optimizer,
'learning_rate': learning_rate,
'warmup_ratio' : 0.1, # 预热比例(0.1):BERT 微调经典配置,前 10% 的 step 学习率线性上升,避免初始震荡
'weight_decay' : 0.0001, # 权重衰减:和优化器中的配置一致,若train_val()内需要重新初始化优化器则会用到
'use_lookahead' : True, #是否启用 Lookahead 优化:对 AdamW 做改进,提升训练稳定性(可选功能)
'loss': loss,
'epoch': epoch,
'device': device,
'save_path': save_path,
'max_acc': 0.85, #验证集准确率阈值:初始设 0.85,只有超过该值才保存模型,避免保存效果差的模型
'val_epoch' : 1 #训练多少轮验证一次
}
train_val(trainpara)

###model.py###
import torch #导入 PyTorch 核心库
import torch.nn as nn #导入 PyTorch 神经网络模块,并简写为nn
import numpy as np #处理数值计算(比如数据预处理、数组转换),虽然当前代码中没直接用,但属于机器学习标配(后续扩展会用到)
# from timm.models.vision_transformer import PatchEmbed, Block
# import torchvision.models as models #torchvision是处理图像的库(比如 ResNet、VGG),完全用不到,可直接删除`
import transformers #作为下面具体类的 “父库”,确保后续导入的 BERT 相关类能正常调用
from transformers import (BertPreTrainedModel, BertConfig,
BertForSequenceClassification, BertTokenizer,BertModel,
)
# BertPreTrainedModel封装了 BERT 模型的通用功能(比如加载预训练权重、初始化参数、配置设备),BertModel、BertForSequenceClassification都是继承自它。
# BertConfig相当于 BERT 模型的 “配置文件”,告诉模型 “该用多大的隐藏层、多少个注意力头、词表有多大”。
# BertForSequenceClassification相当于把BertModel(特征提取)+ nn.Linear(分类头)打包好了,直接传入文本就能输出分类结果。相比BertModel不需要手动分类头
# BertTokenizer把中文文本(比如 “这家酒店很好”)转换成 BERT 能看懂的数字编码(input_ids)、注意力掩码(attention_mask)等。
# BertModel是BERT 的核心部分,接收分词后的输入,输出文本的特征向量(768 维),是整个模型的 “特征提取器”。
class myBertModel(nn.Module):
def __init__(self, bert_path, num_class, device):
super(myBertModel, self).__init__()
self.device = device
self.num_class = 2
# bert_config = BertConfig.from_pretrained(bert_path)
# self.bert = BertModel(bert_config) #只加载设备,不加载参数
# self.bert
# 预训练的神经网络结构 + 权重参数
# 接收数字张量,输出768维语义特征
# 看不懂任何文字(中文 / 英文)
# self.tokenizer
# 中文词表(文字→数字的映射规则)
# 把文字转成数字编码(input_ids)、处理长度
# 无法做语义计算、特征提取
self.bert = BertModel.from_pretrained(bert_path) #既BertModel加载设备,又加载参数
self.tokenizer = BertTokenizer.from_pretrained(bert_path)
# 是自定义 BERT 分类模型的核心分类头—— 本质是把 BERT 输出的 768 维文本特征,转换成num_class=2维的分类得分(对应负面 / 正面)
self.out = nn.Sequential(
nn.Linear(768,num_class)
)
# 文本预处理核心方法—— 作用是把原始文本(比如 “这家酒店太差了”)转换成 BERT 模型能识别的张量格式,并放到指定设备(GPU/CPU)上,是 “原始文本” 到 “模型输入” 的必经步骤
def build_bert_input(self, text):
# 分词器处理文本,转换成模型可识别的格式
# return_tensors='pt',指定返回 PyTorch 张量(pt=PyTorch,若写tf则返回 TensorFlow 张量),符合你的模型框架;
# padding = 'max_length', 核心:不足按max_length填充
# truncation = True, 配合:超过max_length则截断
# max_length = 128) , 目标长度:128
Input = self.tokenizer(text,return_tensors='pt', padding='max_length', truncation=True, max_length=128)
# 下面几行代码将分词器处理后的 3 个核心张量(input_ids/attention_mask/token_type_ids)从默认的 CPU 设备,迁移到模型运行的目标设备(GPU/CPU),并返回这 3 个对齐设备的张量,供 BERT 模型直接使用
# 把input_ids张量迁移到指定设备(如GPU)
input_ids = Input["input_ids"].to(self.device)
attention_mask = Input["attention_mask"].to(self.device)
token_type_ids = Input["token_type_ids"].to(self.device)
return input_ids, attention_mask, token_type_ids
def forward(self, text):
# 文本预处理:将原始文本转换成BERT需要的输入格式
# 返回值:
# - input_ids:文本对应的token数字编码
# - attention_mask:注意力掩码(区分真实token和填充的[PAD])
# - token_type_ids:句子类型编码(区分单/双句,单句时全为0)
input_ids, attention_mask, token_type_ids = self.build_bert_input(text)
# 送入BERT模型,获取输出
# return_dict=False:返回元组(而非字典),包含序列特征和池化特征
# sequence_out:[batch_size, seq_len, hidden_dim] 每个token的特征
# pooled_output:[batch_size, hidden_dim] 整个句子的聚合特征(<CLS>位置)
sequence_out, pooled_output = self.bert(input_ids=input_ids,
attention_mask=attention_mask,
token_type_ids=token_type_ids,
return_dict=False)
# 将句子级的池化特征送入输出层(通常是全连接层),得到最终预测
out = self.out(pooled_output)
return out
# def model_Datapara(model, device, pre_path=None):
# model = torch.nn.DataParallel(model).to(device)
# if pre_path != None:
# model_dict = torch.load(pre_path).module.state_dict()
# model.module.load_state_dict(model_dict)
# return model
###train.py###
import torch
import time
import matplotlib.pyplot as plt
import numpy as np
from tqdm import tqdm
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']
#################################################
plt_train_loss = []
plt_train_acc = []
plt_val_loss = []
plt_val_acc = []
val_rel = []
for i in range(epoch):
start_time = time.time()
model.train()
train_loss = 0.0
train_acc = 0.0
val_acc = 0.0
val_loss = 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() #.detach 表示去掉梯度
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__())
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 轮强制保存一次模型(即使不是最优),用于断点续训或回溯训练过程
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])
)
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()
###data.py###
from torch.utils.data import Dataset,DataLoader,ConcatDataset
import torch
from sklearn.model_selection import train_test_split
from tqdm import tqdm
import numpy as np
def read_txt_data(path):
label = []
data = []
with open(path, "r", encoding="utf-8") as f:
for i, line in tqdm(enumerate(f)):
if i == 0:
continue # continue 表示立即执行下一次循环
if i > 200 and i < 7500:
continue
line = line.strip('\n')
line = line.split(",", 1) # 1表示分割次数
label.append(line[0])
data.append(line[1])
print(len(label))
print(len(data))
return data, label
class JdDataset(Dataset):
def __init__(self, x, label):
self.X = x
label = [int(i) for i in label]
self.Y = torch.LongTensor(label)
def __getitem__(self, item):
return self.X[item], self.Y[item] # 数据集一般不让返回str, 要写在字典中,或者转为矩阵。
def __len__(self):
return len(self.Y)
def get_dataloader(path, batchsize=1, valSize=0.2):
x, label = read_txt_data(path)
train_x, val_x, train_y, val_y = train_test_split(
x, # 待拆分的特征数据(你的场景:酒店评论文本列表)
label, # 待拆分的标签数据(你的场景:评论的正负标签列表)
test_size=valSize, # 验证集占比(你设的valSize=0.2 → 20%数据做验证,80%做训练)
shuffle=True, # 拆分前是否打乱数据顺序
stratify=label # 按标签“分层抽样”,保证训练/验证集的标签分布和原始数据一致
) #valSize为测试集占比例,
train_set = JdDataset(train_x, train_y)
val_set = JdDataset(val_x, val_y)
train_loader = DataLoader(train_set, batch_size=batchsize)
# PyTorch 中构建训练集批量数据加载器的核心代码,作用是将自定义的训练数据集 train_set(JdDataset 实例)封装成可迭代的 DataLoader 对象,让模型训练时能按指定批次大小(batchsize)逐批读取数据,而非一次性加载全部数据(避免内存溢出)。
#
val_loader = DataLoader(val_set, batch_size=batchsize)
return train_loader, val_loader
if __name__ == '__main__':
get_dataloader("../jiudian.txt",batchsize=4)
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)