赞
踩
使用bert模型微调做下游任务,在goole发布的bert代码和huggingface的transformer项目中都有相应的任务,有的时候只需要把代码做简单的修改即可使用。发现代码很多,我尝试着自己来实现一个用bert模型来做句子分类任务的网络——这个工作也很有必要,加深bert的理解,深度学习网络的创建和训练调参等。
LCQMC 是哈尔滨工业大学在自然语言处理国际顶会 COLING2018 构建的问题语义匹配数据集,其目标是判断两个问题的语义是否相同。数据集中训练集238766条数据,验证集8803条,测试集12501条。它们的正负样本比例分别为1.3:1、1:1和1:1,样本非常均衡,数据也挺好,数据质量非常高。具体的数据格式如下所示:
- text_a text_b label
- 喜欢打篮球的男生喜欢什么样的女生 爱打篮球的男生喜欢什么样的女生 1
- 我手机丢了,我想换个手机 我想买个新手机,求推荐 1
- 大家觉得她好看吗 大家觉得跑男好看吗? 0
- 求秋色之空漫画全集 求秋色之空全集漫画 1
- 晚上睡觉带着耳机听音乐有什么害处吗? 孕妇可以戴耳机听音乐吗? 0
- 学日语软件手机上的 手机学日语的软件 1
- 打印机和电脑怎样连接,该如何设置 如何把带无线的电脑连接到打印机上 0
- 侠盗飞车罪恶都市怎样改车 侠盗飞车罪恶都市怎么改车 1
- 什么花一年四季都开 什么花一年四季都是开的 1
这个数据集在网上有公布很常见,直接网上搜索就好。
这里的思想是这样的:使用bert模型分别获取text_a和text_b的向量,bert模型输出可以有12层也就是12个行向量,我们选取最后一层向量,把2个向量拼接起来,然后送于分类器,进行分类。注意到,bert模型本身就能够直接添加cls和sep把2个句子拼接起来进行训练,这和我这种简单粗暴的处理不同。
这里向量拼接处理:首先分别得到ext_a和text_b的向量embedding_a和embedding_b,它们的维度是[batch_size,sequence_lengtg,dim]。为了能够训练,把第二维的向量做均值处理,得到embedding_a_mean和embedding_b_mean。随后把embedding_a_mean和embedding_b_mean做差取绝对值,得到绝对差值abs。最后输入分类器的向量:embedding_a_mean+embedding_b_mean+abs,这里的思想是直接使用了一篇论文sentence-bert中的方法。下文代码中的 target_span_embedding就是最终分类器的输入向量。
- embedding_a = self.bert(indextokens_a,input_mask_a)[0]
- embedding_b = self.bert(indextokens_b,input_mask_b)[0]
-
- embedding_a = torch.mean(embedding_a,1)
- embedding_b = torch.mean(embedding_b,1)
-
- abs = torch.abs(embedding_a - embedding_b)
-
-
- target_span_embedding = torch.cat((embedding_a, embedding_b,abs), dim=1)
分类器:分类器很简单,就是几层全连接,可以视为一个多层感知器。模型的最后一层要注意,由于这里是0-1二分类, class_num=2。也就是self.out = nn.Linear(384,2)。
整体上看整个网络也非常简单,具体代码如下:
- import torch.nn as nn
- import torch.nn.functional as F
- import torch
- from transformers import BertModel
-
-
- class SpanBertClassificationModel(nn.Module):
- def __init__(self):
- super(SpanBertClassificationModel,self).__init__()
-
- self.bert = BertModel.from_pretrained('pretrained_models/Chinese-BERT-wwm/').cuda()
- for param in self.bert.parameters():
- param.requires_grad = True
-
- self.hide1 = nn.Linear(768*3,768)
- self.hide2 = nn.Linear(768,384)
-
- self.dropout = nn.Dropout(0.5)
- self.out = nn.Linear(384,2)
-
- def forward(self, indextokens_a,input_mask_a,indextokens_b,input_mask_b):
- embedding_a = self.bert(indextokens_a,input_mask_a)[0]
- embedding_b = self.bert(indextokens_b,input_mask_b)[0]
-
- embedding_a = torch.mean(embedding_a,1)
- embedding_b = torch.mean(embedding_b,1)
-
- abs = torch.abs(embedding_a - embedding_b)
-
-
- target_span_embedding = torch.cat((embedding_a, embedding_b,abs), dim=1)
-
-
- hide_1 = F.relu(self.hide1(target_span_embedding))
- hide_2 = self.dropout(hide_1)
- hide = F.relu(self.hide2(hide_2))
-
-
- out_put = self.out(hide)
- return out_put
-
-
其中的中文bert使用的是哈工大的预训练模型,Chinese-BERT-wwm。
这一部分的功能
1、从文件中读取数据,然后处理生成bert模型能够接受的输入。
indextokens_a,input_mask_a,segment_id_a
由于模型中是分别用bert模型提取向量,因此这里的segment_id_a可以省略,只要保留tokens和mask。至于他们的含义可以参考——Bert提取句子特征——这篇博客写的很详细。
2、构造成神经网络的DataLoader。
可以让数据按照设定的batch_size参与神经网络的训练。
DataLoader 这个是工程化的代码,具体的细节,这篇博客就不多说,可以参考——pytorch Dataset, DataLoader产生自定义的训练数据——这篇博客,说的很详细了。
这里有一个注意的细节就是:
- def convert_into_indextokens_and_segment_id(self,text):
- tokeniz_text = self.tokenizer.tokenize(text)
- indextokens = self.tokenizer.convert_tokens_to_ids(tokeniz_text)
- input_mask = [1] * len(indextokens)
-
-
- pad_indextokens = [0]*(self.max_sentence_length-len(indextokens))
- indextokens.extend(pad_indextokens)
- input_mask_pad = [0]*(self.max_sentence_length-len(input_mask))
- input_mask.extend(input_mask_pad)
-
- segment_id = [0]*self.max_sentence_length
- return indextokens,segment_id,input_mask
做句子序号化的时候,由于每个句子不一样长,需要做一个max_sequence_length的处理,那就需要对长度小于max_sequence_length做一个padding的处理,详细的代码如上。
完整代码如下:
- from torch.utils.data import DataLoader,Dataset
- from transformers import BertModel,BertTokenizer
- from allennlp.data.dataset_readers.dataset_utils import enumerate_spans
- import torch
- from tqdm import tqdm
- import time
- import pandas as pd
-
- class SpanClDataset(Dataset):
- def __init__(self,filename,repeat=1):
- self.max_sentence_length = 64
- self.max_spans_num = len(enumerate_spans(range(self.max_sentence_length),max_span_width=3))
- self.repeat = repeat
- self.tokenizer = BertTokenizer.from_pretrained('pretrained_models/Chinese-BERT-wwm/')
- self.data_list = self.read_file(filename)
- self.len = len(self.data_list)
- self.process_data_list = self.process_data()
-
-
- def convert_into_indextokens_and_segment_id(self,text):
- tokeniz_text = self.tokenizer.tokenize(text)
- indextokens = self.tokenizer.convert_tokens_to_ids(tokeniz_text)
- input_mask = [1] * len(indextokens)
-
-
- pad_indextokens = [0]*(self.max_sentence_length-len(indextokens))
- indextokens.extend(pad_indextokens)
- input_mask_pad = [0]*(self.max_sentence_length-len(input_mask))
- input_mask.extend(input_mask_pad)
-
- segment_id = [0]*self.max_sentence_length
- return indextokens,segment_id,input_mask
-
-
- def read_file(self,filename):
- data_list = []
- df = pd.read_csv(filename, sep='\t') # tsv文件
- s1, s2, labels = df['text_a'], df['text_b'], df['label']
-
- for sentence_a, sentence_b, label in tqdm(list(zip(s1, s2, labels)),desc="加载数据集处理数据集:"):
- if len(sentence_a) <= self.max_sentence_length and len(sentence_b) <= self.max_sentence_length:
- data_list.append((sentence_a, sentence_b, label))
- return data_list
-
- def process_data(self):
- process_data_list = []
- for ele in tqdm(self.data_list,desc="处理文本信息:"):
- res = self.do_process_data(ele)
- process_data_list.append(res)
- return process_data_list
-
- def do_process_data(self,params):
-
- res = []
- sentence_a = params[0]
- sentence_b = params[1]
- label = params[2]
-
- indextokens_a,segment_id_a,input_mask_a = self.convert_into_indextokens_and_segment_id(sentence_a)
- indextokens_a = torch.tensor(indextokens_a,dtype=torch.long)
- segment_id_a = torch.tensor(segment_id_a,dtype=torch.long)
- input_mask_a = torch.tensor(input_mask_a,dtype=torch.long)
-
- indextokens_b, segment_id_b, input_mask_b = self.convert_into_indextokens_and_segment_id(sentence_b)
- indextokens_b = torch.tensor(indextokens_b, dtype=torch.long)
- segment_id_b = torch.tensor(segment_id_b, dtype=torch.long)
- input_mask_b = torch.tensor(input_mask_b, dtype=torch.long)
-
- label = torch.tensor(int(label))
-
- res.append(indextokens_a)
- res.append(segment_id_a)
- res.append(input_mask_a)
-
-
- res.append(indextokens_b)
- res.append(segment_id_b)
- res.append(input_mask_b)
-
-
- res.append(label)
-
- return res
-
- def __getitem__(self, i):
- item = i
-
- indextokens_a = self.process_data_list[item][0]
- segment_id_a = self.process_data_list[item][1]
- input_mask_a = self.process_data_list[item][2]
-
-
-
- indextokens_b = self.process_data_list[item][3]
- segment_id_b = self.process_data_list[item][4]
- input_mask_b = self.process_data_list[item][5]
-
-
- label = self.process_data_list[item][6]
-
-
- return indextokens_a,input_mask_a,indextokens_b,input_mask_b,label
-
- def __len__(self):
- if self.repeat == None:
- data_len = 10000000
- else:
- data_len = len(self.process_data_list)
- return data_len
-
重要的部分就是优化器和学习率的设置及调整,其他的部分就安装pytorch深度学习网络模型训练的步骤写就好了。
一看看模型的参数,网络的模型包含bert模型的所有参数和后面的几层全连接层的参数,根据huggingface提供的bert微调训练代码,这里也要对一些参数做权重衰减,可能会取得较好的效果。直接参考相关代码,至于为何要设置和为何要那样设置我就没有深究——有知道的人可以告知我。是不是有类似机器学习中正则化的效果,减小模型过拟合?
- no_decay = ['bias', 'LayerNorm.bias', 'LayerNorm.weight']
- #设置模型参数的权重衰减
- optimizer_grouped_parameters = [
- {'params': [p for n, p in param_optimizer if not any(nd in n for nd in no_decay)],
- 'weight_decay': 0.01},
- {'params': [p for n, p in param_optimizer if any(nd in n for nd in no_decay)], 'weight_decay': 0.0}
- ]
二优化器
pytorch提工的优化器有很多,他们之间也有很多区别,记得有篇博客提高SGD随机梯度下降优化器,只要会调参会取得最好的效果在所有的优化器中。这里我就用大众最喜欢用的AdamW优化器。
optimizer = AdamW(optimizer_grouped_parameters, **optimizer_params)
三 学习率设置和调整
初始学习率:1e-5,最小学习率:1e-7。验证集准确率5个epoc不升高,0.5的倍率降低学习率。
- #学习率的设置
- optimizer_params = {'lr': 1e-5, 'eps': 1e-6, 'correct_bias': False}
- #AdamW 这个优化器是主流优化器
- optimizer = AdamW(optimizer_grouped_parameters, **optimizer_params)
-
- #学习率调整器,检测准确率的状态,然后衰减学习率
- scheduler = ReduceLROnPlateau(optimizer,mode='max',factor=0.5,min_lr=1e-7, patience=5,verbose= True, threshold=0.0001, eps=1e-08)
完整训练代码:
- from model.sbert import SpanBertClassificationModel
- from Datareader.data_reader_new import SpanClDataset
- from torch.utils.data import DataLoader
- import torch.nn as nn
- import torch
- from torch.optim.lr_scheduler import ReduceLROnPlateau
- import torch.nn.functional as F
-
-
- from transformers import AdamW,WarmupLinearSchedule
- from tqdm import tqdm
-
- device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
-
-
- def train(model,train_loader,dev_loader):
- model.to(device)
- model.train()
- criterion = nn.CrossEntropyLoss()
-
- param_optimizer = list(model.named_parameters())
- no_decay = ['bias', 'LayerNorm.bias', 'LayerNorm.weight']
- #设置模型参数的权重衰减
- optimizer_grouped_parameters = [
- {'params': [p for n, p in param_optimizer if not any(nd in n for nd in no_decay)],
- 'weight_decay': 0.01},
- {'params': [p for n, p in param_optimizer if any(nd in n for nd in no_decay)], 'weight_decay': 0.0}
- ]
- #学习率的设置
- optimizer_params = {'lr': 1e-5, 'eps': 1e-6, 'correct_bias': False}
- #AdamW 这个优化器是主流优化器
- optimizer = AdamW(optimizer_grouped_parameters, **optimizer_params)
-
- #学习率调整器,检测准确率的状态,然后衰减学习率
- scheduler = ReduceLROnPlateau(optimizer,mode='max',factor=0.5,min_lr=1e-7, patience=5,verbose= True, threshold=0.0001, eps=1e-08)
-
- t_total = len(train_loader)
- total_epochs = 1500
- bestAcc = 0
- correct = 0
- total = 0
- print('Training begin!')
- for epoch in range(total_epochs):
- for step, (indextokens_a,input_mask_a,indextokens_b,input_mask_b,label) in enumerate(train_loader):
- indextokens_a,input_mask_a,indextokens_b,input_mask_b,label = indextokens_a.to(device),input_mask_a.to(device),indextokens_b.to(device),input_mask_b.to(device),label.to(device)
- optimizer.zero_grad()
- out_put = model(indextokens_a,input_mask_a,indextokens_b,input_mask_b)
- loss = criterion(out_put, label)
- _, predict = torch.max(out_put.data, 1)
- correct += (predict == label).sum().item()
- total += label.size(0)
- loss.backward()
- optimizer.step()
-
- if (step + 1) % 2 == 0:
- train_acc = correct / total
- print("Train Epoch[{}/{}],step[{}/{}],tra_acc{:.6f} %,loss:{:.6f}".format(epoch + 1, total_epochs, step + 1, len(train_loader),train_acc*100,loss.item()))
-
- if (step + 1) % 500 == 0:
- train_acc = correct / total
- acc = dev(model, dev_loader)
- if bestAcc < acc:
- bestAcc = acc
- path = 'savedmodel/span_bert_hide_model.pkl'
- torch.save(model, path)
- print("DEV Epoch[{}/{}],step[{}/{}],tra_acc{:.6f} %,bestAcc{:.6f}%,dev_acc{:.6f} %,loss:{:.6f}".format(epoch + 1, total_epochs, step + 1, len(train_loader),train_acc*100,bestAcc*100,acc*100,loss.item()))
- scheduler.step(bestAcc)
-
- def dev(model,dev_loader):
- model.eval()
- with torch.no_grad():
- correct = 0
- total = 0
- for step, (
- indextokens_a, input_mask_a, indextokens_b, input_mask_b, label) in tqdm(enumerate(
- dev_loader),desc='Dev Itreation:'):
- indextokens_a, input_mask_a, indextokens_b, input_mask_b, label = indextokens_a.to(device), input_mask_a.to(
- device), indextokens_b.to(device), input_mask_b.to(device), label.to(device)
- out_put = model(indextokens_a, input_mask_a, indextokens_b, input_mask_b, mode)
- _, predict = torch.max(out_put.data, 1)
- correct += (predict==label).sum().item()
- total += label.size(0)
- res = correct / total
- return res
-
- def predict(model,test_loader,mode):
- model.to(device)
- model.eval()
- predicts = []
- predict_probs = []
- with torch.no_grad():
- correct = 0
- total = 0
- for step, (
- indextokens_a, input_mask_a, indextokens_b, input_mask_b, label) in enumerate(
- test_loader):
- indextokens_a, input_mask_a, indextokens_b, input_mask_b, label = indextokens_a.to(device), input_mask_a.to(
- device), indextokens_b.to(device), input_mask_b.to(device), label.to(device)
- out_put = model(indextokens_a, input_mask_a, indextokens_b, input_mask_b, mode)
- _, predict = torch.max(out_put.data, 1)
-
- pre_numpy = predict.cpu().numpy().tolist()
- predicts.extend(pre_numpy)
- probs = F.softmax(out_put).detach().cpu().numpy().tolist()
- predict_probs.extend(probs)
-
- correct += (predict==label).sum().item()
- total += label.size(0)
- res = correct / total
- print('predict_Accuracy : {} %'.format(100 * res))
- return predicts,predict_probs
-
- if __name__ == '__main__':
- batch_size = 48
- train_data = SpanClDataset('data/LCQMC/train.tsv')
- dev_data = SpanClDataset('data/LCQMC/dev.tsv')
- test_data = SpanClDataset('data/LCQMC/test.tsv')
-
-
- train_loader = DataLoader(dataset=train_data, batch_size=batch_size, shuffle=True)
- dev_loader = DataLoader(dataset=dev_data, batch_size=batch_size, shuffle=True)
- test_loader = DataLoader(dataset=test_data, batch_size=batch_size, shuffle=False)
-
-
- model = SpanBertClassificationModel()
- train(model,train_loader,dev_loader)
- path = 'savedmodel/span_bert_hide_model.pkl'
- model1 = torch.load(path)
- predicts,predict_probs = predict(model1,test_loader)
-
-
-
-
-
-
-
-
-
-
-
-
设置了1500个epoch确实是太大,一个epoc差不多需要1个多小时。这里展示一下6个epoc时候的训练集和验证集的准去率情况:
训练过程中显示,训练集准确率还可以慢慢的提升,验证集准确率也能提升。这里由于写博客的需要就只展示这个结果,这里感觉有点过拟合——训练集和验证集的准确率相差有点大。要严格判定是否过拟合可以使用机器学习中的方法,画学习曲线。
模型训练完成以后,就可以直接用这个模型进行预测了。代码也在上面的训练代码中。
以上就是使用bert模型做句子分类任务的神经网络以及结果展示。代码少,网络简单,就权当刚入坑深度学习和NLP的我的一个实践记录。方便复习,有大神可以指导一下方向。
附上我的github网址:
https://github.com/HUSTHY/Myown_sbert
参考文章:
pytorch Dataset, DataLoader产生自定义的训练数据
Bert提取句子特征(pytorch_transformers)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。