第七节:Bert实战-文字序列分类任务

1.背景

现有一个酒店相关的数据集jiudian.txt,内容大致如下图。1表示好评,0表示差评。现有训练集和测试集,要区分测试集中评价的好坏。

包含文件如下:

bert-base-chinese:bert模型的一种,是Bert模型针对中文的一个版本,有十二个编码器层。

model_save:模型内容保存地址。

model_utils:将数据,模型和训练的方法包装在一个文件夹中,调用方便,main函数精简。

jiudian.txt:数据集文本。

2.代码

2.1数据

2.1.1导包

from torch.utils.data import Dataset,DataLoader
import torch
from sklearn.model_selection import train_test_split
from tqdm import tqdm

2.1.2读文件

#读文件,传入文件路径
def read_txt_data(path):
    # 两个列表存放文本和标签
    label = []
    data = []
    #原本这个文档是gbk方式来读,但这样不合法,所以需要改成utf-8
    with open(path, "r", encoding="utf-8") as f:
        for i, line in tqdm(enumerate(f)):
            #第一行不是内容,舍去
            if i == 0:
                continue
            #太慢了,少读一些加快速度,想要用完整数据训练可以不要这一步
            if i > 200 and i < 7500:
                continue
            #删除换行符,不然\n会出现在列表中
            line = line.strip('\n')
            #仅在遇到第一个逗号时将句子分一次,后面的逗号就不管了
            #经过以上操作,一个句子的0-1标签和内容就在一个列表中一分为二
            line = line.split(",", 1)
            #标签与内容分别加入对应列表
            label.append(line[0])
            data.append(line[1])
    print(len(label))
    print(len(data))
    return data, label


2.1.3Dataset

class JdDataset(Dataset):
    def __init__(self, x, label):
        self.X = x
        #label是一个字符串,必须要转为整型数字,又因为是整型,必须使用LongTensor
        label = [int(i) for i in label]
        self.Y = torch.LongTensor(label)

    def __getitem__(self, item):
        return self.X[item], self.Y[item]

    def __len__(self):
        return len(self.Y)

2.1.4划分验证集并取数据

#这个数据集没有验证集,要自己分,这边默认验证及比例为0.2
def get_dataloader(path, batchsize=1, valSize=0.2):
    x, label = read_txt_data(path)
    """
    要将内容x与标签label各划分两份作为训练集和验证集,故需要四个值承接
    test_size:划分比例,如此处为0.2就是把验证与训练数据为1:4分布
    shuffle:是否打乱顺序
    stratify:必须按照stratify的比例取值,如1500个数据训练集与验证集为2:1(1000:500)。
    若不加限制,test_size=0.2就会随便取如训练集取250个,测试集取50个
    加上限制后,分配时也会保持test_size的比例,训练集取200个,验证集取100个
    """
    train_x, val_x, train_y, val_y = train_test_split(x, label, test_size=valSize, shuffle=True, stratify=label)
    train_set = JdDataset(train_x, train_y)
    val_set = JdDataset(val_x, val_y)
    train_loader = DataLoader(train_set, batch_size=batchsize)
    val_loader = DataLoader(val_set, batch_size=batchsize)
    return train_loader, val_loader

2.1.4限制

只有当前模块被直接执行时,接下来的代码才会被调用。当该模块被导入时不会执行。

if __name__ == '__main__':

    get_dataloader("../jiudian.txt",batchsize=4)

2.2模型

2.2.1导包

import torch
import torch.nn as nn
from transformers import (BertPreTrainedModel, BertConfig, BertForSequenceClassification, BertTokenizer, BertModel,)

2.2.2模型类

class myBertModel(nn.Module):
    def __init__(self, bert_path, num_class, device):
        super(myBertModel, self).__init__()
        self.device = device
        #二分类
        self.num_class = 2
        """
        加载预训练模型
        创建一个bert模型,需要提供路径。
        在这个路径下,会直接读取config中配置的内容。
        如果还有参数(pytroch_model.bin),还会调用设置好的参数。
        """
        self.bert = BertModel.from_pretrained(bert_path)
        #分词器-将文本转化为独立的token列表
        self.tokenizer = BertTokenizer.from_pretrained(bert_path)
        self.out = nn.Sequential(
            nn.Linear(768, num_class)
        )

    def build_bert_input(self, text):
        #经过分词器处理这句话,切记限定长度,padding, truncation,max_length参数不能少,也要用return_tensors转化为张量
        Input = self.tokenizer(text, return_tensors='pt', padding='max_length', truncation=True, max_length=128)
        #数据要放在gpu上
        #输入的文本转化为数字的结果,不要忘了开头结尾的标识符
        input_ids = Input["input_ids"].to(self.device)
        #填充位用0表示,实际上的文字位置表示为1
        attention_mask = Input["attention_mask"].to(self.device)
        #句子编码,数字表示这个文字属于第几句
        token_type_ids = Input["token_type_ids"].to(self.device)
        return input_ids, attention_mask, token_type_ids

    def forward(self, text):
        input_ids, attention_mask, token_type_ids = self.build_bert_input(text)
        #将以上参数传入模型
        #sequence_out是未经降维的
        sequence_out, pooled_output = self.bert(input_ids=input_ids,
                                                attention_mask=attention_mask,
                                                token_type_ids=token_type_ids,
                                                return_dict=False)
        #只需要用到pooled_output
        out = self.out(pooled_output)
        return out

2.3训练

2.3.1导包

import torch
import time
import matplotlib.pyplot as plt
import numpy as np
from tqdm import tqdm

2.3.2训练过程

#相比于之前,这里用一个para涵盖所有数据就不用一个一个表示在函数里了,比较便捷
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_loss = 0.0
        val_acc = 0.0
        for batch in tqdm(train_loader):
            #model.zero_grad()的作用是将所有模型参数的梯度置为0
            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.Tensor的梯度
            optimizer.zero_grad()
            #梯度剪裁:设置一个梯度大小的上限,防止梯度爆炸:参数+上限
            torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
            train_loss += bat_loss.item()
            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__())

        #每val_epoch轮进行一次验证
        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()

2.4主函数

import torch.nn as nn
import torch
import random
import numpy  as np
import os
#导入上述模块
from model_utils.data import get_dataloader
from model_utils.model import myBertModel
from model_utils.train import train_val

#固定随机种子
def seed_everything(seed):
    torch.manual_seed(seed)
    torch.cuda.manual_seed(seed)
    torch.cuda.manual_seed_all(seed)
    torch.backends.cudnn.benchmark = False
    torch.backends.cudnn.deterministic = True
    random.seed(seed)
    np.random.seed(seed)
    os.environ['PYTHONHASHSEED'] = str(seed)


model_name = 'MyModel'
#二分类
num_class = 2
#每批处理数据数
batchSize = 4
#学习率
learning_rate = 0.0001
#损失值函数
loss = nn.CrossEntropyLoss()
#因为预处理时别人训练了很多次了,微调训练轮数就可以较少
epoch = 3
device = 'cuda' if torch.cuda.is_available() else 'cpu'

#数据集路径
data_path = "jiudian.txt"
#模型路径
bert_path = 'bert-base-chinese'
#存放路径
save_path = 'model_save/'
seed_everything(1)


train_loader, val_loader = get_dataloader(data_path, batchsize=batchSize)

model = myBertModel(bert_path, num_class, device).to(device)

param_optimizer = list(model.parameters())
#AdamW相较于SGD效果更好
optimizer = torch.optim.AdamW(model.parameters(), lr=learning_rate,weight_decay=0.0001)
#学习率变化
scheduler = torch.optim.lr_scheduler.CosineAnnealingWarmRestarts(optimizer,T_0=20,eta_min=1e-9)

#用一个字典来存储数据,更加直观
trainpara = {'model': model,
             'train_loader': train_loader,
             'val_loader': val_loader,
             'scheduler': scheduler,
             'optimizer': optimizer,
             'learning_rate': learning_rate,
             'warmup_ratio' : 0.1,
             'weight_decay' : 0.0001,
             'use_lookahead' : True,
             'loss': loss,
             'epoch': epoch,
             'device': device,
             'save_path': save_path,
             'max_acc': 0.85,
             'val_epoch' : 1
             }
train_val(trainpara)

3.结果

可见如果使用已有预训练模型,再加以微调,可以得到不错的准确率。相反,如果使用自己的模型效果可能反而较差。

7767it [00:00, 254674.20it/s]
467
467
100%|██████████| 94/94 [05:24<00:00,  3.45s/it]
100%|██████████| 24/24 [00:10<00:00,  2.30it/s]
[000/003] 336.08 sec(s) TrainAcc : 0.627346 TrainLoss : 0.159934 | valAcc: 0.670213 valLoss: 0.161169  
100%|██████████| 94/94 [04:33<00:00,  2.91s/it]
100%|██████████| 24/24 [00:10<00:00,  2.31it/s]
  0%|          | 0/94 [00:00<?, ?it/s][001/003] 284.01 sec(s) TrainAcc : 0.680965 TrainLoss : 0.154044 | valAcc: 0.425532 valLoss: 0.178264  
100%|██████████| 94/94 [05:39<00:00,  3.62s/it]
100%|██████████| 24/24 [00:13<00:00,  1.78it/s]
[002/003] 355.71 sec(s) TrainAcc : 0.632708 TrainLoss : 0.160546 | valAcc: 0.712766 valLoss: 0.156867  

Process finished with exit code 0

  • 9
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值