当前位置:   article > 正文

使用bert模型做句子分类_chinese-bert-wwm

chinese-bert-wwm

    使用bert模型微调做下游任务,在goole发布的bert代码和huggingfacetransformer项目中都有相应的任务,有的时候只需要把代码做简单的修改即可使用。发现代码很多,我尝试着自己来实现一个用bert模型来做句子分类任务的网络——这个工作也很有必要,加深bert的理解,深度学习网络的创建和训练调参等。

一、数据集

LCQMC 是哈尔滨工业大学在自然语言处理国际顶会 COLING2018 构建的问题语义匹配数据集,其目标是判断两个问题的语义是否相同。数据集中训练集238766条数据,验证集8803条,测试集12501条。它们的正负样本比例分别为1.3:1、1:1和1:1,样本非常均衡,数据也挺好,数据质量非常高。具体的数据格式如下所示:

  1. text_a text_b label
  2. 喜欢打篮球的男生喜欢什么样的女生 爱打篮球的男生喜欢什么样的女生 1
  3. 我手机丢了,我想换个手机 我想买个新手机,求推荐 1
  4. 大家觉得她好看吗 大家觉得跑男好看吗? 0
  5. 求秋色之空漫画全集 求秋色之空全集漫画 1
  6. 晚上睡觉带着耳机听音乐有什么害处吗? 孕妇可以戴耳机听音乐吗? 0
  7. 学日语软件手机上的 手机学日语的软件 1
  8. 打印机和电脑怎样连接,该如何设置 如何把带无线的电脑连接到打印机上 0
  9. 侠盗飞车罪恶都市怎样改车 侠盗飞车罪恶都市怎么改车 1
  10. 什么花一年四季都开 什么花一年四季都是开的 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就是最终分类器的输入向量。

  1. embedding_a = self.bert(indextokens_a,input_mask_a)[0]
  2. embedding_b = self.bert(indextokens_b,input_mask_b)[0]
  3. embedding_a = torch.mean(embedding_a,1)
  4. embedding_b = torch.mean(embedding_b,1)
  5. abs = torch.abs(embedding_a - embedding_b)
  6. target_span_embedding = torch.cat((embedding_a, embedding_b,abs), dim=1)

分类器:分类器很简单,就是几层全连接,可以视为一个多层感知器。模型的最后一层要注意,由于这里是0-1二分类, class_num=2。也就是self.out = nn.Linear(384,2)。

整体上看整个网络也非常简单,具体代码如下:

  1. import torch.nn as nn
  2. import torch.nn.functional as F
  3. import torch
  4. from transformers import BertModel
  5. class SpanBertClassificationModel(nn.Module):
  6. def __init__(self):
  7. super(SpanBertClassificationModel,self).__init__()
  8. self.bert = BertModel.from_pretrained('pretrained_models/Chinese-BERT-wwm/').cuda()
  9. for param in self.bert.parameters():
  10. param.requires_grad = True
  11. self.hide1 = nn.Linear(768*3,768)
  12. self.hide2 = nn.Linear(768,384)
  13. self.dropout = nn.Dropout(0.5)
  14. self.out = nn.Linear(384,2)
  15. def forward(self, indextokens_a,input_mask_a,indextokens_b,input_mask_b):
  16. embedding_a = self.bert(indextokens_a,input_mask_a)[0]
  17. embedding_b = self.bert(indextokens_b,input_mask_b)[0]
  18. embedding_a = torch.mean(embedding_a,1)
  19. embedding_b = torch.mean(embedding_b,1)
  20. abs = torch.abs(embedding_a - embedding_b)
  21. target_span_embedding = torch.cat((embedding_a, embedding_b,abs), dim=1)
  22. hide_1 = F.relu(self.hide1(target_span_embedding))
  23. hide_2 = self.dropout(hide_1)
  24. hide = F.relu(self.hide2(hide_2))
  25. out_put = self.out(hide)
  26. return out_put

其中的中文bert使用的是哈工大的预训练模型,Chinese-BERT-wwm。

三、DataReader

这一部分的功能

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产生自定义的训练数据——这篇博客,说的很详细了。

这里有一个注意的细节就是:

  1. def convert_into_indextokens_and_segment_id(self,text):
  2. tokeniz_text = self.tokenizer.tokenize(text)
  3. indextokens = self.tokenizer.convert_tokens_to_ids(tokeniz_text)
  4. input_mask = [1] * len(indextokens)
  5. pad_indextokens = [0]*(self.max_sentence_length-len(indextokens))
  6. indextokens.extend(pad_indextokens)
  7. input_mask_pad = [0]*(self.max_sentence_length-len(input_mask))
  8. input_mask.extend(input_mask_pad)
  9. segment_id = [0]*self.max_sentence_length
  10. return indextokens,segment_id,input_mask

做句子序号化的时候,由于每个句子不一样长,需要做一个max_sequence_length的处理,那就需要对长度小于max_sequence_length做一个padding的处理,详细的代码如上。

完整代码如下:

  1. from torch.utils.data import DataLoader,Dataset
  2. from transformers import BertModel,BertTokenizer
  3. from allennlp.data.dataset_readers.dataset_utils import enumerate_spans
  4. import torch
  5. from tqdm import tqdm
  6. import time
  7. import pandas as pd
  8. class SpanClDataset(Dataset):
  9. def __init__(self,filename,repeat=1):
  10. self.max_sentence_length = 64
  11. self.max_spans_num = len(enumerate_spans(range(self.max_sentence_length),max_span_width=3))
  12. self.repeat = repeat
  13. self.tokenizer = BertTokenizer.from_pretrained('pretrained_models/Chinese-BERT-wwm/')
  14. self.data_list = self.read_file(filename)
  15. self.len = len(self.data_list)
  16. self.process_data_list = self.process_data()
  17. def convert_into_indextokens_and_segment_id(self,text):
  18. tokeniz_text = self.tokenizer.tokenize(text)
  19. indextokens = self.tokenizer.convert_tokens_to_ids(tokeniz_text)
  20. input_mask = [1] * len(indextokens)
  21. pad_indextokens = [0]*(self.max_sentence_length-len(indextokens))
  22. indextokens.extend(pad_indextokens)
  23. input_mask_pad = [0]*(self.max_sentence_length-len(input_mask))
  24. input_mask.extend(input_mask_pad)
  25. segment_id = [0]*self.max_sentence_length
  26. return indextokens,segment_id,input_mask
  27. def read_file(self,filename):
  28. data_list = []
  29. df = pd.read_csv(filename, sep='\t') # tsv文件
  30. s1, s2, labels = df['text_a'], df['text_b'], df['label']
  31. for sentence_a, sentence_b, label in tqdm(list(zip(s1, s2, labels)),desc="加载数据集处理数据集:"):
  32. if len(sentence_a) <= self.max_sentence_length and len(sentence_b) <= self.max_sentence_length:
  33. data_list.append((sentence_a, sentence_b, label))
  34. return data_list
  35. def process_data(self):
  36. process_data_list = []
  37. for ele in tqdm(self.data_list,desc="处理文本信息:"):
  38. res = self.do_process_data(ele)
  39. process_data_list.append(res)
  40. return process_data_list
  41. def do_process_data(self,params):
  42. res = []
  43. sentence_a = params[0]
  44. sentence_b = params[1]
  45. label = params[2]
  46. indextokens_a,segment_id_a,input_mask_a = self.convert_into_indextokens_and_segment_id(sentence_a)
  47. indextokens_a = torch.tensor(indextokens_a,dtype=torch.long)
  48. segment_id_a = torch.tensor(segment_id_a,dtype=torch.long)
  49. input_mask_a = torch.tensor(input_mask_a,dtype=torch.long)
  50. indextokens_b, segment_id_b, input_mask_b = self.convert_into_indextokens_and_segment_id(sentence_b)
  51. indextokens_b = torch.tensor(indextokens_b, dtype=torch.long)
  52. segment_id_b = torch.tensor(segment_id_b, dtype=torch.long)
  53. input_mask_b = torch.tensor(input_mask_b, dtype=torch.long)
  54. label = torch.tensor(int(label))
  55. res.append(indextokens_a)
  56. res.append(segment_id_a)
  57. res.append(input_mask_a)
  58. res.append(indextokens_b)
  59. res.append(segment_id_b)
  60. res.append(input_mask_b)
  61. res.append(label)
  62. return res
  63. def __getitem__(self, i):
  64. item = i
  65. indextokens_a = self.process_data_list[item][0]
  66. segment_id_a = self.process_data_list[item][1]
  67. input_mask_a = self.process_data_list[item][2]
  68. indextokens_b = self.process_data_list[item][3]
  69. segment_id_b = self.process_data_list[item][4]
  70. input_mask_b = self.process_data_list[item][5]
  71. label = self.process_data_list[item][6]
  72. return indextokens_a,input_mask_a,indextokens_b,input_mask_b,label
  73. def __len__(self):
  74. if self.repeat == None:
  75. data_len = 10000000
  76. else:
  77. data_len = len(self.process_data_list)
  78. return data_len

四、训练

重要的部分就是优化器和学习率的设置及调整,其他的部分就安装pytorch深度学习网络模型训练的步骤写就好了。

一看看模型的参数,网络的模型包含bert模型的所有参数和后面的几层全连接层的参数,根据huggingface提供的bert微调训练代码,这里也要对一些参数做权重衰减,可能会取得较好的效果。直接参考相关代码,至于为何要设置和为何要那样设置我就没有深究——有知道的人可以告知我。是不是有类似机器学习中正则化的效果,减小模型过拟合?

  1. no_decay = ['bias', 'LayerNorm.bias', 'LayerNorm.weight']
  2. #设置模型参数的权重衰减
  3. optimizer_grouped_parameters = [
  4. {'params': [p for n, p in param_optimizer if not any(nd in n for nd in no_decay)],
  5. 'weight_decay': 0.01},
  6. {'params': [p for n, p in param_optimizer if any(nd in n for nd in no_decay)], 'weight_decay': 0.0}
  7. ]

二优化器

pytorch提工的优化器有很多,他们之间也有很多区别,记得有篇博客提高SGD随机梯度下降优化器,只要会调参会取得最好的效果在所有的优化器中。这里我就用大众最喜欢用的AdamW优化器。

optimizer = AdamW(optimizer_grouped_parameters, **optimizer_params)

三 学习率设置和调整

初始学习率:1e-5,最小学习率:1e-7。验证集准确率5个epoc不升高,0.5的倍率降低学习率。
  1. #学习率的设置
  2. optimizer_params = {'lr': 1e-5, 'eps': 1e-6, 'correct_bias': False}
  3. #AdamW 这个优化器是主流优化器
  4. optimizer = AdamW(optimizer_grouped_parameters, **optimizer_params)
  5. #学习率调整器,检测准确率的状态,然后衰减学习率
  6. scheduler = ReduceLROnPlateau(optimizer,mode='max',factor=0.5,min_lr=1e-7, patience=5,verbose= True, threshold=0.0001, eps=1e-08)

完整训练代码:

  1. from model.sbert import SpanBertClassificationModel
  2. from Datareader.data_reader_new import SpanClDataset
  3. from torch.utils.data import DataLoader
  4. import torch.nn as nn
  5. import torch
  6. from torch.optim.lr_scheduler import ReduceLROnPlateau
  7. import torch.nn.functional as F
  8. from transformers import AdamW,WarmupLinearSchedule
  9. from tqdm import tqdm
  10. device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
  11. def train(model,train_loader,dev_loader):
  12. model.to(device)
  13. model.train()
  14. criterion = nn.CrossEntropyLoss()
  15. param_optimizer = list(model.named_parameters())
  16. no_decay = ['bias', 'LayerNorm.bias', 'LayerNorm.weight']
  17. #设置模型参数的权重衰减
  18. optimizer_grouped_parameters = [
  19. {'params': [p for n, p in param_optimizer if not any(nd in n for nd in no_decay)],
  20. 'weight_decay': 0.01},
  21. {'params': [p for n, p in param_optimizer if any(nd in n for nd in no_decay)], 'weight_decay': 0.0}
  22. ]
  23. #学习率的设置
  24. optimizer_params = {'lr': 1e-5, 'eps': 1e-6, 'correct_bias': False}
  25. #AdamW 这个优化器是主流优化器
  26. optimizer = AdamW(optimizer_grouped_parameters, **optimizer_params)
  27. #学习率调整器,检测准确率的状态,然后衰减学习率
  28. scheduler = ReduceLROnPlateau(optimizer,mode='max',factor=0.5,min_lr=1e-7, patience=5,verbose= True, threshold=0.0001, eps=1e-08)
  29. t_total = len(train_loader)
  30. total_epochs = 1500
  31. bestAcc = 0
  32. correct = 0
  33. total = 0
  34. print('Training begin!')
  35. for epoch in range(total_epochs):
  36. for step, (indextokens_a,input_mask_a,indextokens_b,input_mask_b,label) in enumerate(train_loader):
  37. 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)
  38. optimizer.zero_grad()
  39. out_put = model(indextokens_a,input_mask_a,indextokens_b,input_mask_b)
  40. loss = criterion(out_put, label)
  41. _, predict = torch.max(out_put.data, 1)
  42. correct += (predict == label).sum().item()
  43. total += label.size(0)
  44. loss.backward()
  45. optimizer.step()
  46. if (step + 1) % 2 == 0:
  47. train_acc = correct / total
  48. print("Train Epoch[{}/{}],step[{}/{}],tra_acc{:.6f} %,loss:{:.6f}".format(epoch + 1, total_epochs, step + 1, len(train_loader),train_acc*100,loss.item()))
  49. if (step + 1) % 500 == 0:
  50. train_acc = correct / total
  51. acc = dev(model, dev_loader)
  52. if bestAcc < acc:
  53. bestAcc = acc
  54. path = 'savedmodel/span_bert_hide_model.pkl'
  55. torch.save(model, path)
  56. 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()))
  57. scheduler.step(bestAcc)
  58. def dev(model,dev_loader):
  59. model.eval()
  60. with torch.no_grad():
  61. correct = 0
  62. total = 0
  63. for step, (
  64. indextokens_a, input_mask_a, indextokens_b, input_mask_b, label) in tqdm(enumerate(
  65. dev_loader),desc='Dev Itreation:'):
  66. indextokens_a, input_mask_a, indextokens_b, input_mask_b, label = indextokens_a.to(device), input_mask_a.to(
  67. device), indextokens_b.to(device), input_mask_b.to(device), label.to(device)
  68. out_put = model(indextokens_a, input_mask_a, indextokens_b, input_mask_b, mode)
  69. _, predict = torch.max(out_put.data, 1)
  70. correct += (predict==label).sum().item()
  71. total += label.size(0)
  72. res = correct / total
  73. return res
  74. def predict(model,test_loader,mode):
  75. model.to(device)
  76. model.eval()
  77. predicts = []
  78. predict_probs = []
  79. with torch.no_grad():
  80. correct = 0
  81. total = 0
  82. for step, (
  83. indextokens_a, input_mask_a, indextokens_b, input_mask_b, label) in enumerate(
  84. test_loader):
  85. indextokens_a, input_mask_a, indextokens_b, input_mask_b, label = indextokens_a.to(device), input_mask_a.to(
  86. device), indextokens_b.to(device), input_mask_b.to(device), label.to(device)
  87. out_put = model(indextokens_a, input_mask_a, indextokens_b, input_mask_b, mode)
  88. _, predict = torch.max(out_put.data, 1)
  89. pre_numpy = predict.cpu().numpy().tolist()
  90. predicts.extend(pre_numpy)
  91. probs = F.softmax(out_put).detach().cpu().numpy().tolist()
  92. predict_probs.extend(probs)
  93. correct += (predict==label).sum().item()
  94. total += label.size(0)
  95. res = correct / total
  96. print('predict_Accuracy : {} %'.format(100 * res))
  97. return predicts,predict_probs
  98. if __name__ == '__main__':
  99. batch_size = 48
  100. train_data = SpanClDataset('data/LCQMC/train.tsv')
  101. dev_data = SpanClDataset('data/LCQMC/dev.tsv')
  102. test_data = SpanClDataset('data/LCQMC/test.tsv')
  103. train_loader = DataLoader(dataset=train_data, batch_size=batch_size, shuffle=True)
  104. dev_loader = DataLoader(dataset=dev_data, batch_size=batch_size, shuffle=True)
  105. test_loader = DataLoader(dataset=test_data, batch_size=batch_size, shuffle=False)
  106. model = SpanBertClassificationModel()
  107. train(model,train_loader,dev_loader)
  108. path = 'savedmodel/span_bert_hide_model.pkl'
  109. model1 = torch.load(path)
  110. 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)

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/不正经/article/detail/625560
推荐阅读
相关标签
  

闽ICP备14008679号

        
cppcmd=keepalive&