NLP文本分类学习笔记7:基于预训练模型的文本分类

预训练模型

预训练是一种迁移学习的思想,在一个大数据集上训练大模型,之后可以利用这个训练好的模型处理其他任务。预训练模型的使用方法一般有:

  • 用作特征提取:利用预训练模型提取数据特征,再将这些特征用作自己模型的训练,如word2vec,GLOVE
  • 使用模型结构参数:使用预训练模型的结构和参数,再输入自己的数据对模型参数进行训练调整
  • 使用模型结构参数,但冻结部分层的参数:使用预训练模型的结构和参数,再输入自己的数据,但不改变冻结层的参数

ELMO

NLP文本分类学习笔记1中,介绍了one-hot编码及之后的word2vec词嵌入向量,但是这种词的表示使得一个词只对应一个词向量,不能能理解同一单词在不同语境下的同一含义
ELMO则提供一个预训练模型,应用时根据输入动态调整原先词向量(主要是调整下面第三步所说的权重),生成新词向量,更能反映上下文信息,解决一词多义的问题

该模型主要包含三部分:
1、字符编码层。利用卷积对字符训练,提取特征向量
2、使用基于深层的(纵向和横向有很多层)双向的(两个独立的单向)LSTM
3、利用LSTM每一层的输出和开始提取的特征向量通过加权操作得到最后的词嵌入向量。在实际使用时,需要再次训练去得到这个新权重

GPT

GPT实际上是一个更大的transformer的解码器decoder(但是去掉了中间的多头自注意力层)

预训练

预训练任务是给定句子前面的词来预测之后的词(因为transformer的decoder是使用带mask多头注意力机制,所以自然预训练采用这样的任务,因此GPT模型只考虑了单向语言模型)
如下公式所示,句子序列U中词分别化为嵌入向量并加上位置嵌入向量\(W_p\),之后输入到n个transformer的decoder模块,最后的输出进行后一个词的预测

微调

微调时基本相同,任务变为了输入一个句子序列来预测一个标签。
所以之后我们使用该预训练模型来进行微调,都要先通过这种形式来使用,如下图所示,在做分类时,在句子前后各加两个记号start与extract,再输入到模型中,最后使用extract对应的输出(也就是最后一个输出)通过Linear层来进行分类

BERT

BERT实际上是一个更大的transformer的编码器encoder,输入是一个句子对。

  • 为了区分句子对,每一对加入segment embeddings,前一句和后一句分别用0和1表示
  • 句子对开始加入<cls>标记,句子间和尾部加入<sep>标记
  • 原本的位置嵌入position emebeddings变为可学习的参数

将这三部分相加后作为最终的输入,如下所示

预训练

模型通过两个任务进行预训练:

1、带掩码的语言模型

  • 每次随机以一定概率选中句子中一些词,以80%概率将选中的词替换为标记<mask>,以10%的概率替换为一个随机的词,10%的概率保持不变
  • 采取在句子中挖空,再预测空中的词填空,使得它考虑了句子中前后的联系,成为双向的模型
  • 不百分百挖空是由于之后的微调任务中不会出现<mask>标记,避免之后因此出现的误差

因此bert与GPT相比,bert是一个双向的语言模型(掩码语言模型,MLM),通过左右的语义来填空,而GPT是单向的,只用现在预测未来。而Elmo虽然考虑了双向,但是基于LSTM的架构,最后提取的双向特征只是简单拼在一起使用,直观上效果不如bert好(实际还是单向语言模型)

2、预测下一句

  • 50%的概率输入相邻的句子对,50%的概率输入不相邻的句子对
  • 最后将<cls>标记对应的输出,放到全连接层进行预测训练

微调

微调时与GPT相同,也需要根据不同任务进行改造,对于分类任务,输入句子在开始加入<cls>标记,句子间和尾部加入<sep>标记。最后的输出的第一个序列(也就是<cls>对应的输出)输入到其它结构,或者直接连接全连接层进行分类。

ALBert

对与Bert主要有以下改变:

  • 针对嵌入层参数因式分解,原始bert的嵌入层和隐藏层参数维数相同,但是隐藏层要学习上下文之间的联系,因此应该变大,但变大了会导致词嵌入矩阵变大,影响反向传播,所以先将词嵌入矩阵乘以一个矩阵进行降维,再乘以矩阵提升到隐藏层所需要的维度
  • 允许各层共享自注意力层和全连接层(Feed-Forward Networks)的参数
  • 将原始bert中预测下一句的任务变为预测连贯性的任务:输入两个连贯的句子和输入交换前后顺序的颠倒的两个句子
    因此ALBert相当于变“宽”了,但是对比bert,Albert的参数更少

Bert-wwm

针对中文对bert的升级,原始的bert对一个token(即一个英文单词)进行mask没有问题,但对中文,就是对一个字mask,这样就产生问题。该模型在预训练时对一个词进行mask。

pytorch实现基于BERT的文本分类

使用bert base版本进行训练,不更新预训练模型的参数,只加了全连接层用于分类,在10分类的任务中,对于测试集准确率为76.79%。猜测效果不好原因是:数据量不够,训练数据只有4万,参数却有1亿,显然是不行的。但是目前只有CPU,跑了一天半,暂时证明能跑通吧。

使用ALbert进行训练,不更新预训练模型的参数,只加了全连接层用于分类,在10分类的任务中,对于测试集准确率为61.65%,但是更新预训练模型的参数,准确率达到了86.26%

bert.py
使用Hugging Face预训练模型中的bert-base-chinese,可以下载下来使用
文档参数参考

import torch
import torch.nn as nn
from transformers import BertModel,BertTokenizer

class Config(object):

    def __init__(self):
        self.pre_bert_path="C:/Users/DELL/Downloads/bert-base-chinese"
        self.train_path = 'data/dataset_train.csv'  # 训练集
        self.dev_path = 'data/dataset_valid.csv'  # 验证集
        self.test_path = 'data/test.csv'  # 测试集
        self.class_path = 'data/class.json'  # 类别名单
        self.save_path ='mymodel/bert.pth'        # 模型训练结果
        self.num_classes=10
        self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')   # 设备

        self.epochs = 10  # epoch数
        self.batch_size = 128  # mini-batch大小
        self.maxlen = 32  # 每句话处理成的长度(短填长切)
        self.learning_rate = 5e-4                                       # 学习率
        self.hidden_size=768
        self.tokenizer = BertTokenizer.from_pretrained(self.pre_bert_path)

class Model(nn.Module):
    def __init__(self, config):
        super(Model, self).__init__()
        self.bert=BertModel.from_pretrained(config.pre_bert_path)
        #设置不更新预训练模型的参数
        for param in self.bert.parameters():
            param.requires_grad = False
        self.fc = nn.Linear(config.hidden_size, config.num_classes)
    def forward(self, input):
        out=self.bert(input_ids =input['input_ids'],attention_mask=input['attention_mask'],token_type_ids=input['token_type_ids'])
        #只取最后一层CLS对应的输出
        out = self.fc(out.pooler_output)
        return out

ALbert.py

ALbert的结构,使用使用Hugging Face预训练模型中的clue/albert_chinese_tiny

import torch.nn as nn
import torch
from transformers import BertTokenizer, AlbertModel

class Config(object):

    def __init__(self):
        self.pre_bert_path="clue/albert_chinese_tiny"
        self.train_path = 'data/dataset_train.csv'  # 训练集
        self.dev_path = 'data/dataset_valid.csv'  # 验证集
        self.test_path = 'data/test.csv'  # 测试集
        self.class_path = 'data/class.json'  # 类别名单
        self.save_path ='mymodel/albert.pth'        # 模型训练结果
        self.num_classes=10
        self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')   # 设备

        self.dropout = 0.5                                              # 随机失活
        self.epochs = 10  # epoch数
        self.batch_size = 128  # mini-batch大小
        self.maxlen = 32  # 每句话处理成的长度(短填长切)
        self.learning_rate = 5e-4                                       # 学习率
        self.hidden_size=312
        self.tokenizer = BertTokenizer.from_pretrained(self.pre_bert_path)

class Model(nn.Module):
    def __init__(self, config):
        super(Model, self).__init__()
        self.bert= AlbertModel.from_pretrained(config.pre_bert_path)
        for param in self.bert.parameters():
            param.requires_grad = False
        self.fc = nn.Linear(config.hidden_size, config.num_classes)
    def forward(self, input):
        out=self.bert(input_ids =input['input_ids'],attention_mask=input['attention_mask'],token_type_ids=input['token_type_ids'])
        out = self.fc(out.pooler_output)
        return out

runBert.py
用于运行代码模型,更多代码详情见NLP学习笔记0

import json
from mymodel import myBert,myAlbertl
import mydataset
import torch
import pandas as pd
from torch import nn,optim
from torch.utils.data import DataLoader

# config=myBert.Config()
config=myAlbertl.Config()

label_dict=json.load(open(config.class_path,'r',encoding='utf-8'))
# 加载训练,验证,测试数据集
train_df = pd.read_csv(config.train_path)
#这里将标签转化为数字
train_ds=mydataset.GetLoader(train_df['review'],[label_dict[i] for i in train_df['cat']])
train_dl=DataLoader(train_ds,batch_size=config.batch_size,shuffle=True)
valid_df = pd.read_csv(config.dev_path)
valid_ds=mydataset.GetLoader(valid_df['review'],[label_dict[i] for i in valid_df['cat']])
valid_dl=DataLoader(valid_ds,batch_size=config.batch_size,shuffle=True)
test_df = pd.read_csv(config.test_path)
test_ds=mydataset.GetLoader(test_df['review'],[label_dict[i] for i in test_df['cat']])
test_dl=DataLoader(test_ds,batch_size=config.batch_size,shuffle=True)

#计算准确率
def accuracys(pre,label):
    pre=torch.max(pre.data,1)[1]
    accuracy=pre.eq(label.data.view_as(pre)).sum()
    return accuracy,len(label)

#导入网络结构
# model=myBert.Model(config).to(config.device)
model=myAlbertl.Model(config).to(config.device)

#训练
criterion=nn.CrossEntropyLoss()
optimizer=optim.Adam(model.parameters(),lr=config.learning_rate)
best_loss=float('inf')
for epoch in range(config.epochs):
    train_acc = []
    for batch_idx,(data,target)in enumerate(train_dl):
        inputs = config.tokenizer(data,truncation=True, return_tensors="pt",padding=True,max_length=config.maxlen)
        model.train()
        out = model(inputs)
        loss=criterion(out,target)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        train_acc.append(accuracys(out,target))
        train_r = (sum(tup[0] for tup in train_acc), sum(tup[1] for tup in train_acc))
        print('当前epoch:{}\t[{}/{}]{:.0f}%\t损失:{:.6f}\t训练集准确率:{:.2f}%\t'.format(
            epoch, batch_idx, len(train_dl), 100. * batch_idx / len(train_dl), loss.data,
                   100. * train_r[0].numpy() / train_r[1]
        ))
        #每100批次进行一次验证
        if batch_idx%100==0 and batch_idx!=0:
            model.eval()
            val_acc=[]
            loss_total=0
            with torch.no_grad():
                for (data,target) in valid_dl:
                    inputs = config.tokenizer(data, truncation=True, return_tensors="pt", padding=True,
                                              max_length=config.maxlen)
                    out = model(inputs)
                    loss_total = criterion(out, target).data+loss_total
                    val_acc.append(accuracys(out,target))
            val_r = (sum(tup[0] for tup in val_acc), sum(tup[1] for tup in val_acc))
            print('损失:{:.6f}\t验证集准确率:{:.2f}%\t'.format(loss_total/len(valid_dl),100. * val_r[0].numpy() / val_r[1]))
            #如果验证损失低于最好损失,则保存模型
            if loss_total < best_loss:
                best_loss = loss_total
                torch.save(model.state_dict(), config.save_path)

#测试
model.load_state_dict(torch.load(config.save_path))
model.eval()
test_acc=[]
with torch.no_grad():
    for (data, target) in test_dl:
        inputs = config.tokenizer(data,truncation=True, return_tensors="pt",padding=True,max_length=config.maxlen)
        out = model(inputs)
        test_acc.append(accuracys(out, target))
test_r = (sum(tup[0] for tup in test_acc), sum(tup[1] for tup in test_acc))
print('测试集准确率:{:.2f}%\t'.format(100. * test_r[0].numpy() / test_r[1]))

参考

https://zhuanlan.zhihu.com/p/49271699(讲的很好)
http://www.sniper97.cn/index.php/note/deep-learning/bert/3836/
https://zhuanlan.zhihu.com/p/56382372
https://blog.csdn.net/libaominshouzhang/article/details/102995213
https://blog.csdn.net/lch551218/article/details/116243502
https://www.cnblogs.com/sandwichnlp/p/11947627.html

posted @ 2022-04-08 11:13  启林O_o  阅读(616)  评论(0编辑  收藏  举报