❤️觉得内容不错的话,欢迎点赞收藏加关注,后续会继续输入更多优质内容❤️
随着自然语言技术以及预训练语言模型的不断发展,文本分类模型的性能也不断提升,本文基于预训练语言模型,实现一个文本分类模型的baseline,虽然说是baseline,但是该baseline帮助我在各大人工智能竞赛上取得了top成绩,总共拿到过超过30W的比赛奖金。
使用该baseline参加过的一些比赛:
华为DIGIX算法大赛
搜狐校园算法大赛
兴智杯人工智能大赛
粤港澳大湾区(黄埔)国际算法算例大赛等
baseline的文件路径比较简单,后续也可以根据自己的需求进行定制,例如我用到的预训练语言模型均是通过读取远端下载的本地的缓存文件,如果想用自己的预训练模型或者想自定义路径可以进行个性化修改。
首先是参数的设置,该baseline一共有多个可选参数可以设置,例如预训练语言模型的类型,文本长度,训练轮次,学习率等。
在设置好参数后会使用seed_everything函数固定随机种子,这个是为了保证所有的结果均可复现。
在设置好参数和随机种子之后,就需要从./data文件夹读取数据了,这块需要自定义数据输入,数据的输出均以一个dataframe形式输出,单文本分类的dataframe需要有text、label两个列,文本对分类任务需要有text1、text2、label三个列,下面展示了文本对分类的dataframe案例。
DataLoader这块支持两种形式,分别是单文本形式和文本对形式,定义DataLoader还需要对DataLoader的一些参数进行设定。
模型部分采用经典的bert模型,用cls token的文本向量作为文本的特征向量,下接一个全连接层输出。
优化器采用AdamW优化器,并采用了逐层递减的差分学习率,warmup采用随着epoch轮次增加减小的形式,损失函数采用交叉熵损失。
训练部分比较简单,将模型设置成训练模式,从DataLoader中读取数据,并输入到模型中,模型输出结果,计算模型输出的结果和真实标签的差异,也就是损失,采用优化器对模型进行优化,即反向传播的过程。
模型每轮训练完毕之后将模型设置成验证模式,验证模型的性能。
# _*_ coding:utf-8 _*_
# author:chaoswang
# ====================================================导入相关库==================================================== #
#基本运行包导入
from argparse import ArgumentParser
import re
import os
import random
from tqdm import tqdm
#数据处理相关包导入
import pandas as pd
import numpy as np
from sklearn.metrics import classification_report,f1_score
from sklearn.model_selection import StratifiedKFold
#pytorch相关包导入
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader
from torch.optim import AdamW
#transformers包导入
import transformers
from transformers import AutoModel, AutoConfig, AutoTokenizer, get_linear_schedule_with_warmup
#忽略一些警告
import warnings
warnings.filterwarnings('ignore')
transformers.logging.set_verbosity_error()
# ====================================================参数配置==================================================== #
class Config():
# 随机种子配置,用于固定随机数复现结果
seed = 2022
# 预训练模型路径
#中文base一般有:"IDEA-CCNL/Erlangshen-DeBERTa-v2-320M-Chinese"、"hfl/chinese-roberta-wwm-ext"、"hfl/chinese-macbert-base"、"Langboat/mengzi-bert-base"、"ckiplab/bert-base-chinese"、"IDEA-CCNL/Erlangshen-Roberta-110M-Sentiment"
#中文large一般有:"IDEA-CCNL/Erlangshen-DeBERTa-v2-710M-Chinese"、"hfl/chinese-roberta-wwm-ext-large"、"hfl/chinese-macbert-large"、"IDEA-CCNL/Erlangshen-Roberta-330M-Sentiment"
#英文base一般有:"roberta-base"、"bert-base-uncased"、"bert-base-cased"
#英文large一般有:"roberta-large"、"bert-large-uncased"、"bert-large-cased"
#其他模型可以从 https://huggingface.co/models 网站获取
pretrain_model_name = "hfl/chinese-roberta-wwm-ext"
tokenizer = AutoTokenizer.from_pretrained(pretrain_model_name)
n_class = 2 # 文本分类类别个数
max_len = 128 # 文本最大长度
batch_size = 64*4 # 模型运行批处理大小
n_fold = 5 # 交叉验证折数
trn_folds = [0,1,2,3,4] # 需要用的折数
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
epochs = 5 # 训练轮次
lr = 1e-4 # 训练过程中的最大学习率
eps = 1e-6
num_warmup_steps = 0.03
weight_decay = 0.001 # 权重衰减系数,类似模型正则项策略,避免模型过拟合
gradient_accumulation_steps = 1 # 梯度累加
max_grad_norm = 1.0 # 梯度剪切
save_prefix = None# 保存文件前缀
if save_prefix==None:
pretrain_model_name.split("/")[-1] # 保存文件前缀
# ====================================================固定随机种子==================================================== #
def seed_everything(seed):
random.seed(seed)
os.environ['PYTHONHASHSEED'] = str(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
# ====================================================定义Dataset类==================================================== #
#Dataset类的输入有两种,一种是单句子输入,即只有texts_1有输入,texts_2为空;另一种是texts_1,texts_2均有文本输入。
#Dataset类__getitem__的输出为一个字典,分别是{'text1': text_1,'text2': text_2,'input_ids': input_ids,'attention_mask': attention_mask,'token_type_ids':token_type_ids,'label': label}
#input_ids、attention_mask、token_type_ids、label的长度为max_len
#其中token_type_ids和attention_mask均为0,1变量,第一种输入的时候token_type_ids不起作用,为全0,attention_mask用来指示计算attention的时候忽略pad的位置。
class Text_Classification_Dataset(Dataset):
def __init__(self, tokenizer, max_len,texts_1, texts_2=None,labels=None):
self.texts_1 = texts_1
self.texts_2 = texts_2
self.labels = labels
self.tokenizer = tokenizer
self.max_len = max_len
if isinstance(texts_2,pd.Series):
self.have_texts_2 = True
else:
self.have_texts_2 = False
def __len__(self):
return len(self.texts_1)
def __getitem__(self, index):
"""
index 为数据索引,迭代取第index条数据
"""
text_1 = str(self.texts_1[index])
if self.have_texts_2:
text_2 = str(self.texts_2[index])
else:
text_2 = ''
label = self.labels[index]
if not self.have_texts_2:
encoding = self.tokenizer.encode_plus(text_1,truncation=True,
add_special_tokens=True,
max_length=self.max_len,
return_token_type_ids=True,
pad_to_max_length=True,
return_attention_mask=True,
return_tensors='pt',
)
else:
encoding = self.tokenizer.encode_plus(text_1,text_2,truncation=True,
add_special_tokens=True,
max_length=self.max_len,
return_token_type_ids=True,
pad_to_max_length=True,
return_attention_mask=True,
return_tensors='pt',
)
input_ids = encoding['input_ids'].flatten()
attention_mask = encoding['attention_mask'].flatten()
token_type_ids = encoding['token_type_ids'].flatten()
label = torch.tensor(label,dtype=torch.long)
return {
'text1': text_1,
'text2': text_2,
'input_ids': input_ids,
'attention_mask': attention_mask,
'token_type_ids':token_type_ids,
'label': label,
}
# ====================================================定义模型类==================================================== #
# 基础的文本分类模型类,self.model的输出也是一个列表,有'logit'和'hidden_states'两个key,其中'hidden_states'的值为一个num_hidden_layers的元组,分别存储着每一层的hidden_state,长度为batch_size*max_len*hidden_size。
#一般取最后一层的hidden_state的CLS层的向量作为文本向量输入到分类层用于文本分类,即outputs['hidden_states'][-1][:,0,:]
#基础的文本分类模型一般用一层线性映射作为分类层的模型,dropout可加可不加,因为中间的每一层都有dropout,加不加效果影响不大。
class Text_Classification_Model(nn.Module):
def __init__(self, pretrain_model_name, n_classes,tokenizer,freezing_parameters=False):
super().__init__()
self.model = AutoModel.from_pretrained(pretrain_model_name,output_hidden_states=True, return_dict=True)
self.linear = nn.Linear(self.model.config.hidden_size, n_classes)
self.dropout = nn.Dropout(p=0.5)
if freezing_parameters:#是否冻结参数,这个可以用来冻结前几层的参数,只更新后几层的参数
for name, param in self.model.named_parameters():
if 'layer.23' in name or 'layer.22' in name or 'layer.21' in name or 'layer.20' in name or 'layer.19' in name or 'layer.18' in name:
param.requires_grad = True
else:
param.requires_grad = False
def forward(self, input_ids, attention_mask, token_type_ids):
outputs = self.model(input_ids, attention_mask, token_type_ids)
hidden_states = outputs['hidden_states'][-1][:,0,:]
logits = self.linear(self.dropout(hidden_states))
return logits
# ====================================================参数设置函数==================================================== #
# 该函数用于设置差分学习率,因为预训练模型本身训练的足够好了,完全可以对文本进行高纬度表达,在微调阶段一般会将transformer的学习率设置低一点,分类层的学习率设置高一点
# 一般transformer的前几层提取的是一些语法和句法信息,后几层提取到的是语义信息,所以可以将通过差分的方式将前几层的学习率设置更低一点,后几层的学习率稍高一点
def get_parameters(model, model_init_lr, multiplier, classifier_lr):
parameters = []
lr = model_init_lr
for layer in range(model.model.config.num_hidden_layers,-1,-1):
layer_params = {
'params': [p for n,p in model.named_parameters() if f'encoder.layer.{layer}.' in n],
'lr': lr
}
parameters.append(layer_params)
lr *= multiplier
classifier_params = {
'params': [p for n,p in model.named_parameters() if 'layer_norm' in n or 'linear' in n or 'pooling' in n],
'lr': classifier_lr
}
parameters.append(classifier_params)
return parameters
为了展示baseline的使用,本文基于传统bert模型,使用中文文本匹配lcqmc作为训练和验证数据集,构造了一个文本匹配模型,详细展示了baseline的使用。
baseline使用说明
❤️觉得内容不错的话,欢迎点赞收藏加关注,后续会继续输入更多优质内容❤️