一、Bert介绍
Bert模型(Bidirectional Encoder Representations from Transformers),文本特征提取器,是一种由Google于2018年引入的自然语言处理(NLP)预训练技术。它基于Transformer架构,旨在理解句子中单词的上下文。
关键点:
-
双向上下文理解: 与以前的语言模型不同,BERT以双向方式阅读文本。传统的语言模型在处理文本时只关注前面或后面的上下文,而BERT考虑了句子中所有单词的上下文。
-
预训练: BERT通过在大规模文本数据上进行预训练,学习单词之间的关系和语境。这使得模型能够更好地理解自然语言的复杂性。其预训练包括两个自监督预训练:一是掩盖部分文本内容再预测原文,让bert更好的依据语境做出预测,二是预测句子是否连续的任务,能帮助bert对上下文关系有更好的理解。
-
Transformer架构: BERT基于Transformer模型,该模型使用自注意力机制来处理输入序列的不同部分,使其能够捕捉长距离依赖关系。
-
微调: 一旦在大规模文本上进行了预训练,BERT可以通过在特定任务上进行微调来适应具体的应用,如文本分类、命名实体识别等。
上图主要流程如下:
S1:输入文本信息以及位置信息
设input=“我爱你”
'input_ids': [101, 2769, 4263, 872, 102],开头为【cls】,结尾为【sep】,中间为我爱你三个字对应的词嵌入编码
'token_type_ids': [0, 0, 0, 0, 0]:句子的所在id,本句为第0句
'attention_mask': [1, 1, 1, 1, 1]:句子的掩码,1则有信息,0则无信息
-
input_ids
: 包含文本信息的编码序列,其中包括[CLS]和[SEP]标记,以及每个字的词嵌入编码。这种表示方式用于将自然语言文本转换为模型可理解的数值输入。 -
token_type_ids
: 表示每个字所属的句子,这里只有一个句子,因此所有值都为0。在多句子的情况下,可以使用不同的标识符来区分句子。 -
attention_mask
: 表示每个字的掩码,其中1表示有信息,0表示无信息。这有助于模型在处理变长序列时关注有效的部分,而忽略填充部分。
S2:自注意力机制(Self-Attention)
-
使用输入的编码、位置信息以及可能的位置嵌入构建Q(查询)、K(键)、V(值)矩阵。这些矩阵用于计算注意力分布。
-
在Transformer中,获取Q(Query)、K(Key)、V(Value)是通过对输入进行线性映射来实现的。具体来说,对于每个输入序列中的元素(比如每个词),通过三个不同的线性变换(Q= q*Wq, K=k*Wk, V=v*Wv)来获得Q、K、V,三个W初始随机生成,后续在学习训练过程中迭代更新。
-
对Q、K进行点积操作,然后通过softmax函数得到注意力分布。这一步使得每个字可以关注其他字的权重。
-
将注意力分布与V相乘,得到每个字对其他字的加权表示。这使得每个字都能够融合来自其他字的信息。
-
应用残差连接和层归一化,以减少梯度消失和爆炸。残差连接允许信息直接流经模块,减轻了训练中的梯度问题。层归一化有助于稳定训练过程。
S3:全连接层
- 对经过自注意力机制的输出进行全连接层的处理。这可能包括线性变换、激活函数等。全连接层的作用是将得到的复杂特征映射到更高维度的表示空间,以便更好地捕捉输入序列的语义信息。
bert模型下载地址:https://huggingface.co/bert-base-chinese/tree/main
二、主要代码
数据读取处理
from torch.utils.data import Dataset, DataLoader
import torch
from sklearn.model_selection import train_test_split # 把数据集分为训练集和测试集
def read_txt(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) # 只分一次
label.append(line[0]) # 标签写入列表
data.append(line[1]) # 写入数据
return data, label
if __name__ == "__main__": #仅在本文件下运行
read_txt("../jiudian.txt")
class JdDataset(Dataset): # 数据设置,以供Dataloader使用
def __init__(self, data, label):
self.X = data
self.Y = [int(i) for i in label] # label
self.Y = torch.LongTensor(self.Y)
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_txt(path) # stratify是否按标签的比例取val数据集
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
模型定义
import torch.nn as nn
import torch
from transformers import BertModel, BertConfig, BertTokenizer
class MyModel(nn.Module): # 创建模型
def __init__(self, bert_path, device, num_class):
super(MyModel, self).__init__()
self.device = device
self.bert = BertModel.from_pretrained(bert_path)
self.tokenizer = BertTokenizer.from_pretrained(bert_path) # 分词器
self.cls_head = nn.Linear(768, num_class) # 分类头微调至n类
def forward(self, text): # "pt"返回pytorch张量
input = self.tokenizer(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, #bert会有两个输出,一个是未池化输出128*768,一个是池化后的输出1*768
token_type_ids,
attention_mask,
return_dict=False)
out = self.cls_head(pooler_out)
return out
训练流程
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])
)
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()
主函数
import torch.nn as nn
import torch
import random
import numpy as np
import os
from model_utils.data import get_data_loader
from model_utils.model import MyModel
from model_utils.train import train_val
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)
model_name = 'MyModel'
num_class = 2
batchSize = 8
learning_rate = 0.0001
loss = nn.CrossEntropyLoss() #交叉熵损失
epoch = 5
device = 'cuda' if torch.cuda.is_available() else 'cpu'
data_path = "jiudian.txt"
bert_path = 'bert-base-chinese'
save_path = 'model_save/'
seed_everything(1)
##########################################
train_loader, val_loader = get_data_loader(data_path, batchsize=batchSize)
model = MyModel(bert_path, device, num_class=2).to(device) #模型也得放在gpu上
param_optimizer = list(model.parameters())
optimizer = torch.optim.AdamW(model.parameters(), lr=learning_rate,weight_decay=0.0001) #优化器
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,
'weight_decay' : 0.0001,
'use_lookahead' : True,
'loss': loss,
'epoch': epoch,
'device': device,
'save_path': save_path,
'max_acc': 0.85,
'val_epoch' : 1
}
train_val(trainpara)