使用bert模型做句子分类

    使用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。

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

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

    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)

 

  • 10
    点赞
  • 66
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值