情感分析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才能接近这个效果。
不过无所谓,这里只是一个流程罢了,可以方便大家以后延续使用。大家也可以更具自己需要,更换模型,模型更换非常简单。只需要改一句话就够了(下文会说)。



代码

需要装下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 

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)


自定义dataset类

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())

        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)


自定义模型

注意下次想要加载之前保存过的模型文件之前,要先运行下面这个模型定义。如何保存下文会讲。

class SST2(nn.Module):
    def __init__(self, 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, 2)
        
        # 激活函数
        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(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对比:

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]
        labels = torch.max(labels, dim=1)[1]
        if step < 4: # 只输出前四个
            print(predicted_labels)
            print(labels)
            print()



测试

看一下测试集与ground truth对比, 并放入列表中:

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]
        labels = torch.max(labels, dim=1)[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]))

猜你喜欢

转载自blog.csdn.net/weixin_43850253/article/details/122263916