情感分析bert家族 pytorch实现(ing

前言

由于被宿友问了很多问题,于是就果断在2021最后一天自己从头实现自定义dataset, 自定义模型,写了训练代码,预测输出代码。
准确率虽然不高,但这个过程让我清楚了中间处理的一些细节。另外本文对于如何使用huggingface的transformers模型去解决特定任务,具有一定的参考学习意义。

数据集: https://dl.fbaipublicfiles.com/glue/data/SST-2.zip

我一开始发现结果不是很好,以为是我模型参数,优化器之类的没调好…(后来发现错误见下文)

另外本文代码绝大部分都是凭我自己意思写出来的,可能规范性啥的还不算好,请路过的大佬不吝赐教。


2022.1.2补充: 对不起,是我缩进没注意, 按理是要每个step(因为每一个step就是一个batch)就要反向传播更新一次权重。

可以看到损失降下去了

这里只为演示,我的epoch_num设置为1轮,一般都是5~10, 不过也看具体任务。
至于只训练一个epoch为什么会这么好呢,主要是我模型中的主干网络distilBert加载了在SST2数据集上进行预训练的distilbert-base-uncased-finetuned-sst-2-english权重。可能从distilbert-base-uncased权重开始训练的话要多几个epoch才能接近这个效果。
不过无所谓,这里只是一个流程罢了,可以方便大家以后延续使用。大家也可以更具自己需要,更换模型,模型更换非常简单。只需要改一句话就够了(下文会说)。



2022.1.3补充: 对不起,上述结果是有问题的。是偏乐观的。因为加载了fine-tune的权重,虽然我没有泄露用来当测试集的数据,但我那从train.tsv分出来的15%的数据,实际上是被别人fine-tune过了,于是会使得结果更加乐观。有空我再更正,不过不是大问题,大家使用的时候,读入的数据注意一下就好。
另外值得一提的事,也可以把padding操作写成一个函数,然后放入dataloader的collate_fn参数中,这样就不用在内存存着全部数据的padding的部分了,而是只存当前batch的padding部分。



代码

需要装下transformers, NLPer应该不陌生了
bash下:

!pip install transformers

使用的数据集 bash下:

!wget https://dl.fbaipublicfiles.com/glue/data/SST-2.zip

解压一下:

!unzip SST-2.zip



引入库

import numpy as np
import pandas as pd
from sklearn.utils import shuffle
import torch
import transformers
import warnings
import pandas as pd
import torch.nn as nn
import torch.utils.data as Data
import torch.nn.functional as F
from sklearn import metrics
from transformers import AutoTokenizer, AutoModel

warnings.filterwarnings('ignore')

声明好跑的设备

# device = 'cpu'
device = 'cuda'  # gpu

数据准备

读取数据文件

df = pd.read_csv('/content/SST-2/train.tsv', delimiter='\t', skiprows=[0], header=None)
df_len = len(df)
print(df)
print('df_len: ', df_len)

划分训练集和测试集

# 划分训练集和测试集
split_rate = 0.85    # 训练集所占比例,剩下的为测试集
train_df = df[:int(df_len * split_rate)]
test_df = df[int(df_len * split_rate):]

设置一下参数

# 设置一些超参数
lr = 0.001          # 学习比例 
epoch_num = 5
train_batch_size = 64
test_batch_size = 64

加载一下分词器,并指定一下使用的模型名字(关于model_name_str设置为甚么具体大家看下面代码的注释)。

# 也可以先去这里下载https://huggingface.co/distilbert-base-uncased/tree/main,
# 把config.json, pytorch_model.bin, tokenizer.json, tokenizer_config.json, vocab.txt下载到一个文件夹中
# 然后再加载本地路径(该文件夹路径)。国内服务器的话建议这样
# 不过在国外服务器,例如colab,直接指定即可

# For DistilBERT:(旧款api)
# model_class, tokenizer_class, pretrained_weights = (transformers.DistilBertModel, transformers.DistilBertTokenizer, 'distilbert-base-uncased')
# 使用Bert可:(旧款api)
#model_class, tokenizer_class, pretrained_weights = (transformers.BertModel, transformers.BertTokenizer, 'bert-base-uncased')
# Load pretrained model/tokenizer
# tokenizer = tokenizer_class.from_pretrained(pretrained_weights) # 旧框api

# 使用的模型的名字, 在这里找 https://huggingface.co/models, 也可以下文件,换成文件夹路径
# model_name_str = 'distilbert-base-uncased'
model_name_str = 'distilbert-base-uncased-finetuned-sst-2-english'

# 新款api, 推荐√
tokenizer = AutoTokenizer.from_pretrained(model_name_str)

也可以先下好权重文件和配置文件,然后model_name_str指定文件夹即可。


自定义dataset类

根据crossEntropyLoss的用法, labels不去onehot也是可以的, 详见 torch.nn.CrossEntropyLoss用法

class SentimentDataset(Data.Dataset):
    def __init__(self, rawdata_df):
        super(SentimentDataset, self).__init__()

        # assert flag in ['train', 'test']
        # self.training_flag = flag == 'train'
        
        # 查看正负例的数量, df: [feature, label]
        print(rawdata_df[1].value_counts())
		
		# 根据crossentropyloss的用法, 这一句可直接下列语句不在进行onehot
		self.labels = torch.tensor(rawdata_df[1].values).long()
        # self.labels = F.one_hot(torch.tensor(rawdata_df[1].values)).float()
        
        # string -> idx
        tokenized = rawdata_df[0].apply((lambda x:tokenizer.encode(x, add_special_tokens = True)))

        # padding
        max_len = 0
        for i in tokenized.values:
            if len(i) > max_len:
                max_len = len(i)
        padded = np.array([i + [0] * (max_len - len(i)) for i in tokenized.values])
        print('padded.shape:', padded.shape)

        # masking
        attention_mask = np.where(padded != 0, 1, 0)
        print('attention_mask.shape: ', attention_mask.shape)

        self.data = torch.LongTensor(padded)
        self.attention_mask = torch.tensor(attention_mask)

        print('build dataset succeed!\n')

    def __len__(self):
        return len(self.labels)
  
    def __getitem__(self, idx):
        return self.data[idx].to(device), self.attention_mask[idx].to(device), self.labels[idx].to(device)

train_dataset = SentimentDataset(train_df)
test_dataset  = SentimentDataset(test_df)

train_loader = Data.DataLoader(train_dataset, train_batch_size, shuffle=True)
test_loader = Data.DataLoader(test_dataset, test_batch_size, shuffle=True)


自定义模型

注意下次想要加载之前保存过的模型文件之前,要先运行下面这个模型定义。如何保存下文会讲。
这里提一下SST2类的forward函数里的这两句。
按照transformers的bert类实现,返回的输出是个元组(如果return_dict参数为False情况下,该参数默认值为False),那么x[0]就是输出的特征, x[1:]就是输出的一些列中间结果。
这里我们取x[0]即输出的特征
然后我们对每一个batch取第一个(第0个)token即 【cls】 特殊字符的词向量.
第一个 : 指的是取所有batch, 0是指取第0个token, 第三个 : 是指取出词向量的所有值(bert默认为768)。

# 取出[CLS]token对应的向量, [:, 0, :]
features = x[0][:, 0, :] # (batch_size, hidden_dim(768))
x = self.fc1(features)
class SST2(nn.Module):
    def __init__(self, class_num=2, no_grad=True):
        super(SST2, self).__init__() #实现父类的初始化

        # 一些超参数
        hidden_dim = 768

        # 加载于训练模型
        # self.backbone = model_class.from_pretrained(pretrained_weights) # 旧版api
        self.backbone = AutoModel.from_pretrained(model_name_str)  # 新版api 推荐√
        # 根据no_grad的条件冻结与训练模型参数
        if no_grad:
            for layer in list(self.backbone.parameters()):
                layer.requires_grad = False

        self.fc1 = nn.Linear(hidden_dim, hidden_dim * 4)
        self.fc2 = nn.Linear(hidden_dim * 4, class_num)
        
        # 激活函数
        self.relu = nn.ReLU()
        self.sigmoid = nn.Sigmoid()
  
    def forward(self, x, attention_mask):
        x = self.backbone(x, attention_mask=attention_mask)
        # BERT模型输出的张量尺寸为[bz, seq_len, 768]
        # 取出[CLS]token对应的向量, [:, 0, :]
        features = x[0][:, 0, :] # (batch_size, hidden_dim(768))
        x = self.fc1(features)
        x = self.relu(x)
        x = self.fc2(x)
        x = self.sigmoid(x)
        return x

    # 显示骨干模型参数
    def show_backbone(self):
        for name in self.backbone.state_dict():
            print("{:30s}: {}, require_grad={}".format(name, self.backbone.state_dict()[name].shape))
        
    # 预测后直接输出标签
    def predict_label(self, x, attention_mask):
        with torch.no_grad():
            x = self.forward(x, attention_mask)
            predict_labels = torch.max(x, dim=1)[1]
            return predict_labels

    # 预测一个案例,输入是一个句子, return_proba决定是否返回置信度,默认返回
    def predict_example(self, input_str_lt, return_proba=True):
        tokenized_input = []
        for input_str in input_str_lt:
          tokenized_input.append(tokenizer.encode(input_str, add_special_tokens = True))

        # masking
        data = torch.tensor(tokenized_input).to(device)
        attention_mask = torch.ones_like(data).to(device)
        # data = torch.LongTensor([tokenized_input]).to(device)
        print('attention_mask.shape: ', attention_mask.shape)

        predicted = model(x=data, attention_mask=attention_mask) # [1, 2]
        proba, labels = torch.max(predicted, dim=1)

        if return_proba:
          return labels, proba
        else:
          return labels



实例化模型

# model = SST2().to(device)
model = SST2(class_num = 2, no_grad=False).to(device)

假如你是6分类,那么是

model = SST2(class_num = 6,no_grad=False).to(device)



def get_parameter_number(model_analyse):
    #  打印模型参数量
    total_num = sum(p.numel() for p in model_analyse.parameters())
    trainable_num = sum(p.numel() for p in model_analyse.parameters() if p.requires_grad)
    return 'Total parameters: {}, Trainable parameters: {}'.format(total_num, trainable_num)

查看一下模型总的参数量和可学习参数量

get_parameter_number(model)



# 查看可学习的参数
for name, param in model.named_parameters():
    if param.requires_grad:
        print(name)

损失函数和优化器

criterion = nn.CrossEntropyLoss()
# optimizer = torch.optim.SGD(model.parameters(), lr=1e-3, momentum=0.99)
optimizer = torch.optim.AdamW(model.parameters(), lr=2e-5, weight_decay=1e-4)
# 学习率先线性warmup一个epoch,然后cosine式下降
scheduler = transformers.get_cosine_schedule_with_warmup(optimizer, num_warmup_steps=len(train_loader),
                                            num_training_steps=epoch_num*len(train_loader))



训练

for epoch in range(epoch_num):
    for step, (input_ids, att_mask, labels) in enumerate(train_loader):
        predicted = model(x=input_ids, attention_mask=att_mask).to(device) # [bz, 2]
        loss = criterion(predicted, labels)

        print('epoch {:3}, step {:3}, loss = {}'.format(epoch, step, loss))

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        scheduler.step()   # 学习率变化

可以看到一个epoch需要894个step



看一下训练集与ground truth对比:
注意: 请注意查看注释那一句 “如果你在dataset时没有对labels进行one_hot,请把这下面这一句取max下标注释掉!”

for step, (input_ids, att_mask, labels) in enumerate(train_loader):
        predicted_labels = model.predict_label(x=input_ids, attention_mask=att_mask) # [bz, 2]
        # 如果你在dataset时没有对labels进行one_hot,请把这下面这一句取max下标注释掉!
        labels = torch.max(labels, dim=1)[1]
        # 等同于labels = torch.argmax(labels, dim=-1)
        if step < 4: # 只输出前四个
            print(predicted_labels)
            print(labels)
            print()



测试

看一下测试集与ground truth对比, 并放入列表中:
注意: 请注意查看注释那一句 “如果你在dataset时没有对labels进行one_hot,请把这下面这一句取max下标注释掉!”

total_predict_lt = []  # 预测值
total_label_lt = []    # 真实值

for step, (input_ids, att_mask, labels) in enumerate(test_loader):
        predicted_labels = model.predict_label(x=input_ids, attention_mask=att_mask) # [bz, 2]
        # 如果你在dataset时没有对labels进行one_hot,请把这下面这一句取max下标注释掉!
        labels = torch.max(labels, dim=1)[1]
		# 等同于labels = torch.argmax(labels, dim=-1)
        total_predict_lt.extend(predicted_labels.tolist())
        total_label_lt.extend(labels.tolist())
        if step < 4:
            print(predicted_labels)
            print(labels)
            print()



看下acc

metrics.accuracy_score(total_label_lt, total_predict_lt)


保存与读取

#保存
torch.save(model, 'SST2.pt')

下次加载模型可以这样子(就不需要训练了,当然你也可以继续进行fine-tune)

# 读取,注意load之前要运行一下class SST2那个模块
model = torch.load('SST2.pt')



预测输出

test_lt = ['I love you', 'I hate you']
label_lt, proba_lt = model.predict_example(input_str_lt=test_lt, return_proba=True)

for i in range(len(test_lt)):
  print('{} 是否积极:{}, 置信度: {}'.format(test_lt[i], label_lt[i], proba_lt[i]))
  • 6
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值