1.背景
现有一个酒店相关的数据集jiudian.txt,内容大致如下图。1表示好评,0表示差评。现有训练集和测试集,要区分测试集中评价的好坏。
包含文件如下:
bert-base-chinese:bert模型的一种,是Bert模型针对中文的一个版本,有十二个编码器层。
model_save:模型内容保存地址。
model_utils:将数据,模型和训练的方法包装在一个文件夹中,调用方便,main函数精简。
jiudian.txt:数据集文本。
2.代码
2.1数据
2.1.1导包
from torch.utils.data import Dataset,DataLoader
import torch
from sklearn.model_selection import train_test_split
from tqdm import tqdm
2.1.2读文件
#读文件,传入文件路径
def read_txt_data(path):
# 两个列表存放文本和标签
label = []
data = []
#原本这个文档是gbk方式来读,但这样不合法,所以需要改成utf-8
with open(path, "r", encoding="utf-8") as f:
for i, line in tqdm(enumerate(f)):
#第一行不是内容,舍去
if i == 0:
continue
#太慢了,少读一些加快速度,想要用完整数据训练可以不要这一步
if i > 200 and i < 7500:
continue
#删除换行符,不然\n会出现在列表中
line = line.strip('\n')
#仅在遇到第一个逗号时将句子分一次,后面的逗号就不管了
#经过以上操作,一个句子的0-1标签和内容就在一个列表中一分为二
line = line.split(",", 1)
#标签与内容分别加入对应列表
label.append(line[0])
data.append(line[1])
print(len(label))
print(len(data))
return data, label
2.1.3Dataset
class JdDataset(Dataset):
def __init__(self, x, label):
self.X = x
#label是一个字符串,必须要转为整型数字,又因为是整型,必须使用LongTensor
label = [int(i) for i in label]
self.Y = torch.LongTensor(label)
def __getitem__(self, item):
return self.X[item], self.Y[item]
def __len__(self):
return len(self.Y)
2.1.4划分验证集并取数据
#这个数据集没有验证集,要自己分,这边默认验证及比例为0.2
def get_dataloader(path, batchsize=1, valSize=0.2):
x, label = read_txt_data(path)
"""
要将内容x与标签label各划分两份作为训练集和验证集,故需要四个值承接
test_size:划分比例,如此处为0.2就是把验证与训练数据为1:4分布
shuffle:是否打乱顺序
stratify:必须按照stratify的比例取值,如1500个数据训练集与验证集为2:1(1000:500)。
若不加限制,test_size=0.2就会随便取如训练集取250个,测试集取50个
加上限制后,分配时也会保持test_size的比例,训练集取200个,验证集取100个
"""
train_x, val_x, train_y, val_y = train_test_split(x, label, test_size=valSize, shuffle=True, stratify=label)
train_set = JdDataset(train_x, train_y)
val_set = JdDataset(val_x, val_y)
train_loader = DataLoader(train_set, batch_size=batchsize)
val_loader = DataLoader(val_set, batch_size=batchsize)
return train_loader, val_loader
2.1.4限制
只有当前模块被直接执行时,接下来的代码才会被调用。当该模块被导入时不会执行。
if __name__ == '__main__':
get_dataloader("../jiudian.txt",batchsize=4)
2.2模型
2.2.1导包
import torch
import torch.nn as nn
from transformers import (BertPreTrainedModel, BertConfig, BertForSequenceClassification, BertTokenizer, BertModel,)
2.2.2模型类
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中配置的内容。
如果还有参数(pytroch_model.bin),还会调用设置好的参数。
"""
self.bert = BertModel.from_pretrained(bert_path)
#分词器-将文本转化为独立的token列表
self.tokenizer = BertTokenizer.from_pretrained(bert_path)
self.out = nn.Sequential(
nn.Linear(768, num_class)
)
def build_bert_input(self, text):
#经过分词器处理这句话,切记限定长度,padding, truncation,max_length参数不能少,也要用return_tensors转化为张量
Input = self.tokenizer(text, return_tensors='pt', padding='max_length', truncation=True, max_length=128)
#数据要放在gpu上
#输入的文本转化为数字的结果,不要忘了开头结尾的标识符
input_ids = Input["input_ids"].to(self.device)
#填充位用0表示,实际上的文字位置表示为1
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):
input_ids, attention_mask, token_type_ids = self.build_bert_input(text)
#将以上参数传入模型
#sequence_out是未经降维的
sequence_out, pooled_output = self.bert(input_ids=input_ids,
attention_mask=attention_mask,
token_type_ids=token_type_ids,
return_dict=False)
#只需要用到pooled_output
out = self.out(pooled_output)
return out
2.3训练
2.3.1导包
import torch
import time
import matplotlib.pyplot as plt
import numpy as np
from tqdm import tqdm
2.3.2训练过程
#相比于之前,这里用一个para涵盖所有数据就不用一个一个表示在函数里了,比较便捷
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_loss = 0.0
val_acc = 0.0
for batch in tqdm(train_loader):
#model.zero_grad()的作用是将所有模型参数的梯度置为0
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.Tensor的梯度
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])
)
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()
2.4主函数
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
#固定随机种子
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 = 4
#学习率
learning_rate = 0.0001
#损失值函数
loss = nn.CrossEntropyLoss()
#因为预处理时别人训练了很多次了,微调训练轮数就可以较少
epoch = 3
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_dataloader(data_path, batchsize=batchSize)
model = myBertModel(bert_path, num_class, device).to(device)
param_optimizer = list(model.parameters())
#AdamW相较于SGD效果更好
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)
3.结果
可见如果使用已有预训练模型,再加以微调,可以得到不错的准确率。相反,如果使用自己的模型效果可能反而较差。
7767it [00:00, 254674.20it/s]
467
467
100%|██████████| 94/94 [05:24<00:00, 3.45s/it]
100%|██████████| 24/24 [00:10<00:00, 2.30it/s]
[000/003] 336.08 sec(s) TrainAcc : 0.627346 TrainLoss : 0.159934 | valAcc: 0.670213 valLoss: 0.161169
100%|██████████| 94/94 [04:33<00:00, 2.91s/it]
100%|██████████| 24/24 [00:10<00:00, 2.31it/s]
0%| | 0/94 [00:00<?, ?it/s][001/003] 284.01 sec(s) TrainAcc : 0.680965 TrainLoss : 0.154044 | valAcc: 0.425532 valLoss: 0.178264
100%|██████████| 94/94 [05:39<00:00, 3.62s/it]
100%|██████████| 24/24 [00:13<00:00, 1.78it/s]
[002/003] 355.71 sec(s) TrainAcc : 0.632708 TrainLoss : 0.160546 | valAcc: 0.712766 valLoss: 0.156867
Process finished with exit code 0