8. bert实战
在本次实战中,我们要完成的是下游的文字分类任务,不做预训练
输入是语句,输出是它的情感类别
一句话在输入的时候首先要经过分词处理
1. 训练前的知识储备
1.1 分词器(tokenizer)
一句话进入分词器就会被拆开,变成一个字一个字的
会产生三个东西
- input ids :汉字编号
- mask :这句话有多长
- seq_ids :句子编码
模型的输入是固定的————不能一会长一会短
所以要以mask为准,超出就截断,不够就padding
mask只是用来确定字数,后面不再出现
1.2 embedding
input ids————linear(21128,768)————Token Embeddings
seq_ids————linear(2,768)————Segment Embeddings
idx————linear(512,768)————position Embeddings
position Embeddings的最大长度不是句子的长度而是模型的长度
所以是512到768
但是实际上使用的话,一句话配的idx长度应该就是mask长度啊
1.3 bert参数计算
embedding:
linear(21128,768)
linear(2,768)
linear(512,768)
bert layers:
qkv:3*768*768
add&norm中的dense:768*768 (负责进一步提取维度特征)
feed forward:768*(4*768)+(4*768)*768
1.4 hagging-face
如果想自己下载模型
可以直接进入hf-mirror去搜索
然后从 model card 转到 files and versions ,只下 pytorch 版及其他配置文件
1.5 config
介绍一下模型中的参数
"attention_probs_dropout_prob": 0.1, 残差丢弃率 "directionality": "bidi", "hidden_act": "gelu",损失函数 "hidden_dropout_prob": 0.1,残差 "hidden_size": 768,模型维度 "initializer_range": 0.02, "intermediate_size": 3072,前向传播那里的中间维度 "layer_norm_eps": 1e-12, "max_position_embeddings": 512,位置长度 "model_type": "bert", "num_attention_heads": 12,注意力头数量 "num_hidden_layers": 12,bert层数 "pad_token_id": 0, "pooler_fc_size": 768, "pooler_num_attention_heads": 12, "pooler_num_fc_layers": 3, "pooler_size_per_head": 128, "pooler_type": "first_token_transform", "type_vocab_size": 2,几种句子 "vocab_size": 21128 词汇总数
词表
1.6 bert的参数量与结构
bert模型代码长的原因是:其中很多兼容性代码
1.6.1 模型参数
接下来我们先计算模型参数
测试:模型是否真的是使用下载下来的文件
结论:确实会使用自己下载来的bert模型,会在同一目录文件夹下去找
本地有bert
模型参数量为102267648
本地无bert
尝试自己计算模型参数量
emb_num=21128*768+2*768+512*768 #忽略bias(偏置) self_att_num=768*768*3+768*768+768*3028+3028*768 all_att_num=12*self_att_num print(emb_num+all_att_num)差距很少,因为只是少算了些偏置项
还可以再看每一层的参数
#每一层的参数 for name,para in bert.named_parameters(): print( name, para.shape )报错:
shape不用加括号,size()才需要括号
结果如下:
开头
结尾
可以发现其中的pooler层是有参数的
所以再重新手动计算参数,这次加上pooler层
差距又变小了
但是不是每个模型的pooler层都有参数
1.6.2 分词器
将padding设为max_length
2. data
数据集来源:某酒店的评价
data.py这个文件中
我们要产生的是两个dataloader
数据文件中没有训练集和验证集,需要我们自己来分
在哪里去分?
get_data_loader():函数中去分
这里要用到sklearn这个包
strip("\n") 可以去除空格
split(",",1) 把这句话按 , 来分割,分割次数为1
stratify,按标签的比例对数据进行分割
比如此时比例为0.2
如果使用stratify,那么可以将各个标签之下的数据,都分百分之二十出来作为验证集
然后注意一下self.Y
我们文本中的1和0都是字符型
需要转为整形
label = [int(i) for i in label]然后转为pytorch中的LongTensor
data.py代码如下:
from torch.utils.data import Dataset,DataLoader 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 #跳过第一行 line = line.strip() line = line.split(",",1) #1表示分割次数 label.append(line[0]) data.append(line[1]) print(len(data)) print(len(label)) return data, label class My_jdDataset(Dataset): def __init__(self, data, label): self.X = data label = [int(i) for i in label] self.Y = torch.LongTensor(label) def __len__(self): return len(self.X) def __getitem__(self, idx): return self.X[idx], self.Y[idx] def get_dataloader(path, batch_size, val_size=0.2): data,label = read_txt_data(path) train_data, val_data, train_label, val_label = train_test_split(data, label, test_size=val_size, shuffle=True, stratify=label) train_set = My_jdDataset(train_data, train_label) val_set = My_jdDataset(val_data, val_label) train_loader = DataLoader(train_set, batch_size=batch_size, shuffle=True) val_loader = DataLoader(val_set, batch_size=batch_size, shuffle=False) return train_loader, val_loader if __name__ == '__main__': get_dataloader("../jiudian.txt",batch_size=4)这样最重要的模块就写好了
3. model
要注意的有两处
注意tokenizer的输出包含input_ids, attention_mask, token_type_ids,需要分别取出
bert层有两个输出,一个是原始输出,一个是池化输出
还要记得
return_tensors='pt' 返回张量
return_dict=False 简化输出
代码如下:
import torch import torch.nn as nn from transformers import BertModel, BertTokenizer class myBertModel(nn.Module): def __init__(self, bert_path, num_class): super(myBertModel, self).__init__() self.bert = BertModel.from_pretrained(bert_path) self.cls_heads = nn.Linear(768, num_class) self.tokenizer = BertTokenizer.from_pretrained(bert_path) def forward(self, text): input = self.tokenizer(text , return_tensors='pt',truncation=True, padding="max_length", max_length=128) # 注意tokenizer的输出包含input_ids, attention_mask, token_type_ids,需要分别取出 input_ids = input['input_ids'] attention_mask = input['attention_mask'] token_type_ids = input['token_type_ids'] # bert层有两个输出,一个是原始输出,一个是池化输出 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.cls_heads(pooled_output) pass return out model=myBertModel("../bert-base-chinese",2) pred=model("今天天气不错")4. train
要注意的还是只有两点
scheduler.step() 可以动态调整学习率,每次优化之后,就调整学习率
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) 防止梯度爆炸
代码如下:
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()5. mian
这部分用于写超参数和调试运行
5.1 超参
batchsize=4 要小,因为文字占显存,使用小批量,但是又不能太小,会引发新的问题
epochs=5 文字不用太多轮
5.2 调试
5.2.1 报错
pred还在cpu,模型也要记得放入gpu
报错信息:
5.2.2 现象
val_acc的固定
原因1 数据不平衡
先修改data,让它只读前200张
结果
valAcc一直为1,这是不正常的
导致它的原因是我们只取前200张图,里面标签全为1,所以模型就只会把结果设为一
原因2 batchsize太小
模型不知道该往哪里走
结果
模型一直是平的
5.2.3 只用架构
如果只用大佬们的模型而不用参数
结果
跳不出数据不平衡的坑,因为自己训练的效果太差了
对比一下大佬的模型
结论就是,使用大佬的模型实在是太重要了
6. bert代码
初始化如下
总体结构上的三层
经过embedding层
(16,128)————》(16,128,768)
接下来深度分析一下
转到BertModel
转到BertEncoder
转到BertLayer
转到BertAttention
如下
qkv如下
第一个token:[cls]
return_dict = False 的原因:简化输出
作业
外卖数据集
自己调试bert模型变化
暂无