千言数据集:文本相似度——BERT完成NSP任务

以下学习笔记来源于 Coggle 30 Days of ML(22年1&2月)
链接:https://coggle.club/blog/30days-of-ml-202201

比赛链接:https://aistudio.baidu.com/aistudio/competition/detail/45/0/task-definition

了解BERT和NSP

BERT

BERT的全称为Bidirectional Encoder Representation from Transformers,是一个预训练的语言表征模型。它强调了不再像以往一样采用传统的单向语言模型或者把两个单向语言模型进行浅层拼接的方法进行预训练,而是采用新的masked language model(MLM),以致能生成深度的双向语言表征。在多个NLP任务中取得非常好的结果。

Next Sentence Prediction(NSP)

一些如问答、自然语言推断等任务需要理解两个句子之间的关系,而MLM任务倾向于抽取token层次的表征,因此不能直接获取句子层次的表征。为了使模型能够有能力理解句子间的关系,BERT使用了NSP任务来预训练,简单来说就是预测两个句子是否连在一起。具体的做法是:对于每一个训练样例,我们在语料库中挑选出句子A和句子B来组成,50%的时候句子B就是句子A的下一句(标注为IsNext),剩下50%的时候句子B是语料库中的随机句子(标注为NotNext)。接下来把训练样例输入到BERT模型中,用[CLS]对应的C信息去进行二分类的预测。
(参考自https://zhuanlan.zhihu.com/p/98855346)

使用BERT完成NSP任务

NSP简单来说就是预测两个句子是否连在一起,对于文本相似度任务中的两个句子text1和text2,若其相似,我们可以认为text2是text1的next sentence,反之则不是。千言数据集的文本相似度数据有bq、lcqmc和pawsx,我们分别读取这些数据并训练对应数据的模型,最后使用模型预测对应的测试数据集并提交结果(可以尝试将所有训练数据合在一起训练后预测测试数据,目前还没有试,不知道效果如何)。

导入基本库

import pandas as pd
import torch
from sklearn.model_selection import train_test_split
from torch.utils.data import Dataset, DataLoader, TensorDataset
import numpy as np
import pandas as pd
import random
import re

读取数据

#读取tsv文件的方法
def read_tsv(input_file,columns):
    with open(input_file,"r",encoding="utf-8") as file:
        lines = []
        for line in file:
            if len(line.strip().split("\t")) != 1:
                lines.append(line.strip().split("\t"))
        df = pd.DataFrame(lines)
        df.columns = columns
    return df

bq_train=read_tsv('./bq_corpus/train.tsv',['text1','text2','label'])
lcqmc_train=read_tsv('./lcqmc/train.tsv',['text1','text2','label'])
pawsx_train=read_tsv('./paws-x-zh/train.tsv',['text1','text2','label'])

bq_test=read_tsv('./bq_corpus/test.tsv',['text1','text2'])
lcqmc_test=read_tsv('./lcqmc/test.tsv',['text1','text2'])
pawsx_test=read_tsv('./paws-x-zh/test.tsv',['text1','text2'])

自定义DataSet

#数据集读取
class QYanDataSet(Dataset):
    def __init__(self,encodings,labels):
        self.encodings=encodings
        self.labels=labels
    #读取单个样本
    def __getitem__(self,idx):
        item={key:torch.tensor(val[idx]) for key,val in self.encodings.items()}
        item['labels']=torch.tensor(int(self.labels[idx]))
        return item
    def __len__(self):
        return len(self.labels)

定义计算accuracy函数

#accuracy计算
def flat_accuracy(preds,labels):
    '''
    flatten是numpy.ndarray.flatten的一个函数,即返回一个一维数组。
    a.flatten():a是个数组,a.flatten()就是把a降到一维,默认是按行的方向降 。
    '''
    pred_flat=np.argmax(preds,axis=1).flatten()
    labels_flat=labels.flatten()
    return np.sum(pred_flat==labels_flat)/len(labels_flat)

训练&验证函数

def solve_train(df_train,name):
    q1_train, q1_val, q2_train, q2_val, train_label, test_label =  train_test_split(
        df_train['text1'].iloc[:], 
        df_train['text2'].iloc[:],
        df_train['label'].iloc[:],
        test_size=0.1, 
        stratify=df_train['label'].iloc[:] #设置stratify参数,可以处理数据不平衡问题
    )
    # 利用分词器进行编码,encode仅返回input_ids,encode_plus返回所有编码信息
    # input_ids:是单词在词典中的编码
    # token_type_ids:标识是第一个句子还是第二个句子,区分两个句子的编码(上句全为0,下句全为1)
    # attention_mask:标识是不是填充,指定对哪些词进行self-Attention操作
    from transformers import BertTokenizer
    # 分词器,词典
    tokenizer=BertTokenizer.from_pretrained('bert-base-chinese')
    train_encoding=tokenizer(list(q1_train),list(q2_train),truncation=True,
                             padding=True,max_length=100)
    val_encoding=tokenizer(list(q1_val),list(q2_val),truncation=True,
                          padding=True,max_length=100)
    
    train_dataset=QYanDataSet(train_encoding,list(train_label))
    val_dataset=QYanDataSet(val_encoding,list(test_label))
    
    from transformers import BertForNextSentencePrediction,AdamW,get_linear_schedule_with_warmup
    model=BertForNextSentencePrediction.from_pretrained('bert-base-chinese')
    device=torch.device('cuda' if torch.cuda.is_available() else 'cpu')
#     device=torch.device('cpu')
    model.to(device)

    #单个读取到批量读取
    train_loader=DataLoader(train_dataset,batch_size=4,shuffle=True)
    val_loader=DataLoader(val_dataset,batch_size=4,shuffle=True)

    #优化方法
    optim=AdamW(model.parameters(),lr=1e-5)
    
    # 训练函数
    def train():
        model.train()
        total_train_loss = 0
        iter_num = 0
        total_iter = len(train_loader)
        for batch in train_loader:
            # 正向传播
            optim.zero_grad()
            input_ids = batch['input_ids'].to(device)
            attention_mask = batch['attention_mask'].to(device)
            labels = batch['labels'].to(device)
            outputs = model(input_ids, attention_mask=attention_mask, labels=labels)
            loss = outputs[0]
            total_train_loss += loss.item()

            # 反向梯度信息
            loss.backward()
            torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) #梯度剪切函数,为防止梯度爆炸
            
            if hasattr(torch.cuda, 'empty_cache'):
                torch.cuda.empty_cache()
            # 参数更新
            optim.step()
            
            iter_num += 1
            if(iter_num % 100==0):
                print("epoth: %d, iter_num: %d, loss: %.4f, %.2f%%" % (epoch, iter_num, loss.item(), iter_num/total_iter*100))

        print("Epoch: %d, Average training loss: %.4f"%(epoch, total_train_loss/len(train_loader)))

    def validation():
        model.eval()
        total_eval_accuracy = 0
        total_eval_loss = 0
        for batch in val_loader:
            with torch.no_grad():
                # 正常传播
                input_ids = batch['input_ids'].to(device)
                attention_mask = batch['attention_mask'].to(device)
                labels = batch['labels'].to(device)
                outputs = model(input_ids, attention_mask=attention_mask, labels=labels)

            loss = outputs[0]
            logits = outputs[1]

            total_eval_loss += loss.item()
            logits = logits.detach().cpu().numpy()
            label_ids = labels.to('cpu').numpy()
            total_eval_accuracy += flat_accuracy(logits, label_ids)

        avg_val_accuracy = total_eval_accuracy / len(val_loader)
        print("Accuracy: %.4f" % (avg_val_accuracy))
        print("Average testing loss: %.4f"%(total_eval_loss/len(val_loader)))
        print("-------------------------------")


    for epoch in range(5):
        print("------------Epoch: %d ----------------" % epoch)
        train()
        validation()
        torch.save(model.state_dict(), f'model_{name}_{epoch}.pt')

预测函数

def solve_predict(df_test,model_path,save_path):
    from transformers import BertTokenizer,BertForNextSentencePrediction
    model=BertForNextSentencePrediction.from_pretrained('bert-base-chinese')
    model.load_state_dict(torch.load(model_path))

    df_test['label']=[0.0 for i in range(len(df_test))]
    tokenizer=BertTokenizer.from_pretrained('bert-base-chinese')
    test_encoding=tokenizer(list(df_test['text1']),list(df_test['text2']),truncation=True,
                                padding=True,max_length=100)
    test_dataset=QYanDataSet(test_encoding,list(df_test['label']))
    test_loader=DataLoader(test_dataset,batch_size=16,shuffle=False)

    device=torch.device('cpu')
    def predict():
        model.eval()
        test_predict = []
        for batch in test_loader:
            with torch.no_grad():
                # 正常传播
                input_ids = batch['input_ids'].to(device)
                attention_mask = batch['attention_mask'].to(device)
                labels = batch['labels'].to(device)
                outputs = model(input_ids, attention_mask=attention_mask, labels=labels)

            loss = outputs[0]
            logits = outputs[1]

            logits = logits.detach().cpu().numpy()
            #label_ids = labels.to('cpu').numpy()
            test_predict += list(np.argmax(logits, axis=1).flatten())

        return test_predict

    test_label = predict()
    tmp=[i for i in range(len(test_label))]
    pd.DataFrame({'index':tmp,'prediction':test_label}).to_csv(save_path,index=None, sep='\t')

训练模型

因为自己的笔记本跑的比较慢,就只跑了一个epoch,可以多几轮,取效果较好的模型进行预测。

solve_train(pawsx_train,'pawsx')

千言数据集:文本相似度——BERT完成NSP任务_第1张图片

solve_train(bq_train,'bq')

千言数据集:文本相似度——BERT完成NSP任务_第2张图片

solve_train(lcqmc_train,'lcqmc')

千言数据集:文本相似度——BERT完成NSP任务_第3张图片

预测结果

solve_predict(pawsx_test,f'model_pawsx_0.pt','./submit/paws-x.tsv')
solve_predict(bq_test,f'model_bq_0.pt','./submit/bq_corpus.tsv')
solve_predict(lcqmc_test,f'model_lcqmc_0.pt','./submit/lcqmc.tsv')

提交结果

请添加图片描述
0.8062的分数,比之前的树模型提升了挺多的。

改进方法

  • 可以尝试将数据中的停用词、特殊符号等进行相应处理。
  • 适当的增加训练轮数
  • 改进模型(才开始学,不太会)
  • 进行模型融合

你可能感兴趣的:(bert,自然语言处理,深度学习)