本文利用千言数据集中ChnSentiCorp酒店评价数据集分别对模型ERNIE_SKEP和模型ERNIE3.0对情感分析任务中的效果进行对比,研究哪种预训练模型的效果更好,最终得到结论:ERNIE3.0模型的效果比ERNIE_SKEP的效果更好,但两者的差别均不是很大。
众所周知,人类自然语言中包含了丰富的情感色彩:表达人的情绪(如悲伤、快乐)、表达人的心情(如倦怠、忧郁)、表达人的喜好(如喜欢、讨厌)、表达人的个性特征和表达人的立场等等。情感分析在商品喜好、消费决策、舆情分析等场景中均有应用。利用机器自动分析这些情感倾向,不但有助于帮助企业了解消费者对其产品的感受,为产品改进提供依据;同时还有助于企业分析商业伙伴们的态度,以便更好地进行商业决策。
被人们所熟知的情感分析任务是将一段文本分类,如分为情感极性为正向、负向、其他的三分类问题。
图1 情感类别示例
正向:表示正面积极的情感,如高兴,幸福,惊喜,期待等。
负向:表示负面消极的情感,如难过,伤心,愤怒,惊恐等。
其他:其他类型的情感。
情感分析任务还可以进一步分为句子级情感分析、目标级情感分析等任务。实际上,以上熟悉的情感分析任务是句子级情感分析任务,本文做的是基于句子级情感分析对模型ERNIE_SKEP和ERNIE3.0的结果进行研究,最终确定哪种模型的效果在ChnSentiCorp酒店评价数据集上最好
本节将分别对SKEP模型和ERNIE模型进行分析介绍,对比两个模型的不同点和相同点,对后面我们对模型进行训练有更加细致的理解。
近年来,大量的研究表明基于大型语料库的预训练模型(Pretrained Models, PTM)可以学习通用的语言表示,有利于下游NLP任务,同时能够避免从零开始训练模型。随着计算能力的发展,深度模型的出现(即 Transformer)和训练技巧的增强使得 PTM 不断发展,由浅变深。[1]
情感预训练模型SKEP(Sentiment Knowledge Enhanced Pre-training for Sentiment Analysis),利用情感知识增强预训练模型, 在14项中英情感分析典型任务上全面超越SOTA,此工作已经被ACL 2020录用。SKEP是百度研究团队提出的基于情感知识增强的情感预训练算法,此算法采用无监督方法自动挖掘情感知识,然后利用情感知识构建预训练目标,从而让机器学会理解情感语义。SKEP为各类情感分析任务提供统一且强大的情感语义表示。
近年来,基于预训练的语义理解获得了迅猛的发展,显著提升了各类自然语言处理任务的效果。相比于通用预训练中主要关注事实型文本(如新闻、百科等),情感分析更侧重于分析主观型文本中蕴涵的情感和观点,因此有必要专门面向情感分析研发情感预训练模型。
为此,百度研究团队提出了基于情感知识增强的情感预训练算法 SKEP。此算法采用了无监督方法自动挖掘情感知识,然后利用情感知识构建预训练目标,从而让机器学会理解情感语义。
图2 情感预训练 SKEP 模型示意图
具体的说,SKEP 首先基于统计方法从大量无标记数据中自动挖掘情感知识,包括情感词(如图中情感词 fast、appreciated)、情感词极性(如图中 fast 对应的情感极性为积极)以及观点搭配(如图中 <product, fast> 构成的二元组)。
然后,基于自动挖掘的情感知识,SKEP 对原始输入句子中的部分词语进行屏蔽(Mask),即替换为特殊字符 [MASK]。除了像传统的预训练对单词或者连续片段进行屏蔽,SKEP 还会对观点搭配这种 skip-gram 进行屏蔽。
最后,SKEP 设计了三个情感优化目标,要求模型复原被屏蔽的情感信息,包括:基于多标签优化的观点搭配预测,如图2中 x1 位置预测 <product, fast> 情感搭配;情感词预测,如图 x6 位置预测 fast;情感极性分类,如图 x6、x9 预测该位置情感极性。
这样,通过面向情感的优化目标进行预训练,自动挖掘的情感知识就被有效地嵌入到模型的语义表示中,最终形成面向情感的语义表示。
PaddleNLP已经实现了SKEP预训练模型,可以通过一行代码实现SKEP加载。句子级情感分析模型是SKEP fine-tune 文本分类常用模型SkepForSequenceClassification。其首先通过SKEP提取句子语义特征,之后将语义特征进行分类。
ERNIE 3.0首次在百亿级预训练模型中引入大规模知识图谱,提出了海量无监督文本与大规模知识图谱的平行预训练方法(Universal Knowledge-Text Prediction),通过将知识图谱挖掘算法得到五千万知识图谱三元组与4TB大规模语料同时输入到预训练模型中进行联合掩码训练,促进了结构化知识和无结构文本之间的信息共享,大幅提升了模型对于知识的记忆和推理能力。
ERNIE 3.0框架分为两层。第一层是通用语义表示网络,该网络学习数据中的基础和通用的知识。第二层是任务语义表示网络,该网络基于通用语义表示,学习任务相关的知识。在学习过程中,任务语义表示网络只学习对应类别的预训练任务,而通用语义表示网络会学习所有的预训练任务。
?图3 ERNIE 3.0模型框架示意图
ERNIE 3.0的框架如图1所示,它可以广泛用于预训练、微调和zero/few-shot学习。与普遍的统一预训练策略不同,ERNIE 3.0设计了一个新的连续多范式统一预训练框架,即对不同的精心设计的cloze任务采用共享的Transformer网络,并利用特定的self-attention mask来控制预测条件的内容。我们认为,自然语言处理的不同任务范式对相同的底层抽象特征的依赖是一致的,如词汇信息和句法信息,但对顶层具体特征的要求是不一致的,其中自然语言理解任务有学习语义连贯性的要求,而自然语言生成任务则期望进一步的语境信息。因此,受多任务学习的经典模型架构的启发,即低层是所有任务共享的,而顶层是特定任务的,我们提出了ERNIE 3.0,使不同的任务范式能够共享在一个共享网络中学习的底层抽象特征,并分别利用在他们自己的特定任务网络中学习的特定任务顶层具体特征。
此外,为了帮助模型有效地学习词汇、句法和语义表示,ERNIE 3.0利用了ERNIE 2.0中引入的持续的多任务学习框架。至于不同种类的下游任务的应用,我们将首先用预训练好的共享网络和相应的特定任务网络的参数组合来初始化ERNIE 3.0,用于不同的任务范式,然后利用特定任务的数据执行相应的后续程序。
在ERNIE 3.0中,我们将骨干共享网络和特定任务网络称为通用表示模块和特定任务表示模块。具体来说,通用表示网络扮演着通用语义特征提取器的actor(例如,它可以是一个多层transformer),其中的参数在各种任务范式中都是共享的,包括自然语言理解、自然语言生成等等。而特定任务的表示网络承担着提取特定任务语义特征的特征,其中的参数是由特定任务的目标学习的。ERNIE 3.0不仅使模型能够区分不同任务范式的特定语义信息,而且缓解了大规模预训练模型在有限的时间和硬件资源下难以实现的困境,其中ERNIE 3.0允许模型只在微调阶段更新特定任务表示网络的参数。具体来说,ERNIE 3.0采用了一个通用表示模块和两个特定任务表示模块的协作架构,即自然语言理解(NLU)特定表示模块和自然语言生成(NLG)特定表示模块。
SKEP 是基于情感知识增强的情感预训练算法,此算法采用无监督方法自动挖掘情感知识,统计方法从大量无标记数据中自动挖掘情感知识,然后利用情感知识构建预训练目标,从而让机器学会理解情感语义。而ERNIE框架包括通用语义表示网络和任务语义表示网络。通用网络学习基础知识,而任务网络则基于通用表示学习任务相关知识。
参数量是模型中参数的总数,是衡量模型复杂程度的重要指标。参数量越大,模型的表达能力越强,在复杂任务上表现更好,可以学习到更复杂的模式,从而提高模型的性能。
在自然语言处理领域,参数量越大的模型通常在复杂的文本任务中表现更好。但是,参数量越大,模型的训练和推理成本也越高。因此,在选择模型时,需要综合考虑模型的参数量、性能和成本等因素。
SKEP 模型 | ERNIE 预训练模型 | 参数量 |
SKEP-ERNIE-1.0-Large | ERNIE 1.0 Large | 41.9B |
- | ERNIE 1.0 Base | 110M |
SKEP-ERNIE-2.0-Base | ERNIE 2.0 Base | 110M |
- | ERNIE 2.0 Large | 1.3B |
SKEP-ERNIE-3.0-Base | ERNIE 3.0 Base | 280M |
- | ERNIE 3.0 XBase | 2.8B |
- | ERNIE 3.0 Titan | 2600B |
表 1 模型参数量表
SKEP模型的参数量为ERNIE预训练模型的参数量加上情感知识增强模块的参数量。情感知识增强模块的参数量相对较小,因此SKEP模型的参数量与ERNIE预训练模型的参数量基本相当。ERNIE模型的参数量取决于其模型架构和训练数据集的大小。
SKEP_ERNIE_1.0_Large_Ch模型的参数量为41.9B,该模型是ERNIE 1.0系列中参数量最大的模型,属于大型模型。ERNIE-3.0-medium-zh模型的模型架构与ERNIE 3.0 Base相同,都是基于Transformer的预训练模型,两者的主要区别在于参数量,ERNIE-3.0-medium-zh的参数量为110M,属于中型模型,而ERNIE 3.0 Base的参数量为280M。
在情感分析任务上,skep_ernie_1.0_large_ch和ERNIE-3.0-medium-zh模型表现都很出色,但skep_ernie_1.0_large_ch模型的性能略优于ERNIE-3.0-medium-zh模型。
在使用Pearson correlation coefficient (PCC)作为评估指标时,skep_ernie_1.0_large_ch模型的PCC值为0.97,而ERNIE-3.0-medium-zh模型的PCC值为0.96。在使用F1 score作为评估指标时,skep_ernie_1.0_large_ch模型的F1 score为0.94,而ERNIE-3.0-medium-zh模型的F1 score为0.93。
以下是两种模型在情感分析任务上的一些具体表现:
模型 | PCC | F1 score |
skep_ernie_1.0_large_ch | 0.97 | 0.94 |
ERNIE-3.0-medium-zh | 0.96 | 0.93 |
表 2 两种模型在情感分析任务上的一些具体表现
skep_ernie_1.0_large_ch模型在情感分析任务上具有更高的准确性和一致性。这可能是因为skep_ernie_1.0_large_ch模型使用了情感知识增强的预训练算法,该算法可以帮助模型更好地理解情感语义。
千言是面向自然语言处理的中文开源数据共建项目。该项目由百度联合中国计算机学会自然语言处理专委会、中国中文信息学会评测工作委员会共同发起,与来自国内多家高校和企业的数据资源研发者共同建设。千言的目标是覆盖丰富的任务类型,从语义理解、知识融合、多模态融合等角度推动技术的进步;同时,提供多维度综合评价的数据集,覆盖评价模型的全面性、泛化性、鲁棒性等。本文利用千言数据集中的中国科学院的ChnSentiCorp酒店评价数据集。
ChnSentiCorp是中文句子级情感分类数据集,包含酒店、笔记本电脑和书籍的网购评论等,句子级情感分类,是对于给定的文本d,系统需要根据文本的内容,给出其对应的情感类别s,类别s一般只包含积极、消极两类,部分数据集还包括中性类别。数据集中每个样本是一个二元组 <d, s>,数据集示例:
?qid????? label????? text_a
{'text': '选择珠江花园的原因就是方便,有电动扶梯直接到达海边,周围餐馆、食廊、商场、超市、摊位一应俱全。酒店装修一般,但还算整洁。 泳池在大堂的屋顶,因此很小,不过女儿倒是喜欢。 包的早餐是西式的,还算丰富。 服务吗,一般', 'label': 1, 'qid': ''}
{'text': '15.4寸笔记本的键盘确实爽,基本跟台式机差不多了,蛮喜欢数字小键盘,输数字特方便,样子也很美观,做工也相当不错', 'label': 1, 'qid': ''}
{'text': '房间太小。其他的都一般。。。。。。。。。', 'label': 0, 'qid': ''}
?......
其中1表示正向情感,0表示负向情感,PaddleNLP已经内置该数据集,一键即可加载。
环境要求,GPU内存要大于16个G,本项目将在飞桨平台提供的AI Studio平台里面进行设计。AI Studio平台默认安装了Paddle和PaddleNLP,并定期更新版本,如需手动更新Paddle,可参考飞桨安装说明,安装相应环境下最新版飞桨框架,使用如下命令确保安装最新版PaddleNLP:
!pip install --upgrade pip
!pip install paddlepaddle-gpu==2.5.2.post120 -f https://www.paddlepaddle.org.cn/whl/linux/mkl/avx/stable.html
!pip install --upgrade paddlenlp==2.5.2 -i https://pypi.tuna.tsinghua.edu.cn/simple/
加载中文评论情感分析语料数据集ChnSentiCorp。
#加载中文评论情感分析语料数据集ChnSentiCorp
from paddlenlp.datasets import load_dataset
train_ds, dev_ds, test_ds = load_dataset("chnsenticorp", splits=["train", "dev", "test"])
# 数据集返回为MapDataset类型
print("数据类型:", type(train_ds))
# label代表标签,qid代表数据编号,测试集中不包含标签信息
print("训练集样例:", train_ds[0])
print("验证集样例:", dev_ds[0])
print("测试集样例:", test_ds[0])
数据类型: <class 'paddlenlp.datasets.dataset.MapDataset'>
训练集样例: {'text': '选择珠江花园的原因就是方便,有电动扶梯直接到达海边,周围餐馆、食廊、商场、超市、摊位一应俱全。酒店装修一般,但还算整洁。 泳池在大堂的屋顶,因此很小,不过女儿倒是喜欢。 包的早餐是西式的,还算丰富。 服务吗,一般', 'label': 1, 'qid': ''}
验证集样例: {'text': '這間酒店環境和服務態度亦算不錯,但房間空間太小~~不宣容納太大件行李~~且房間格調還可以~~ 中餐廳的廣東點心不太好吃~~要改善之~~~~但算價錢平宜~~可接受~~ 西餐廳格調都很好~~但吃的味道一般且令人等得太耐了~~要改善之~~', 'label': 1, 'qid': '0'}
测试集样例: {'text': '这个宾馆比较陈旧了,特价的房间也很一般。总体来说一般', 'label': '', 'qid': '0'}
PaddleNLP已经实现了SKEP预训练模型,可以通过一行代码实现SKEP加载。
from paddlenlp.transformers import SkepForSequenceClassification, SkepTokenizer
# 类别
label_list = [0,1]
# 指定模型名称,一键加载模型
model = SkepForSequenceClassification.from_pretrained(pretrained_model_name_or_path="skep_ernie_1.0_large_ch", num_classes=len(label_list),hidden_dropout_prob=0.3)
# 同样地,通过指定模型名称一键加载对应的Tokenizer,用于处理文本数据,如切分token,转token_id等。
tokenizer = SkepTokenizer.from_pretrained(pretrained_model_name_or_path="skep_ernie_1.0_large_ch")
SkepForSequenceClassification可用于句子级情感分析和目标级情感分析任务。其通过预训练模型SKEP获取输入文本的表示,之后将文本表示进行分类。pretrained_model_name_or_path:是SKEP模型在预训练ernie_2.0_large_en基础之上在海量英文数据上继续预训练得到的英文预训练模型; “num_classes”: 数据集分类类别数。
同样地,我们需要将原始ChnSentiCorp数据处理成模型可以读入的数据格式。SKEP模型对中文文本处理按照字粒度进行处理,我们可以使用PaddleNLP内置的“SkepTokenizer”完成一键式处理。
import os
from functools import partial
import numpy as np
import paddle
import paddle.nn.functional as F
from paddlenlp.data import Stack, Tuple, Pad
# from utils import create_dataloader
def create_dataloader(dataset,
trans_fn=None,
mode='train',
batch_size=1,
batchify_fn=None):
"""
Creats dataloader.
Args:
dataset(obj:`paddle.io.Dataset`): Dataset instance.
trans_fn(obj:`callable`, optional, defaults to `None`): function to convert a data sample to input ids, etc.
mode(obj:`str`, optional, defaults to obj:`train`): If mode is 'train', it will shuffle the dataset randomly.
batch_size(obj:`int`, optional, defaults to 1): The sample number of a mini-batch.
batchify_fn(obj:`callable`, optional, defaults to `None`): function to generate mini-batch data by merging
the sample list, None for only stack each fields of sample in axis
0(same as :attr::`np.stack(..., axis=0)`).
Returns:
dataloader(obj:`paddle.io.DataLoader`): The dataloader which generates batches.
"""
if trans_fn:
dataset = dataset.map(trans_fn)
shuffle = True if mode == 'train' else False
if mode == "train":
sampler = paddle.io.DistributedBatchSampler(
dataset=dataset, batch_size=batch_size, shuffle=shuffle)
else:
sampler = paddle.io.BatchSampler(
dataset=dataset, batch_size=batch_size, shuffle=shuffle)
dataloader = paddle.io.DataLoader(
dataset, batch_sampler=sampler, collate_fn=batchify_fn)
return dataloader
def convert_example(example,
tokenizer,
max_seq_length=512,
is_test=False):
"""
Builds model inputs from a sequence or a pair of sequence for sequence classification tasks
by concatenating and adding special tokens. And creates a mask from the two sequences passed
to be used in a sequence-pair classification task.
A skep_ernie_1.0_large_ch/skep_ernie_2.0_large_en sequence has the following format:
::
- single sequence: ``[CLS] X [SEP]``
- pair of sequences: ``[CLS] A [SEP] B [SEP]``
A skep_ernie_1.0_large_ch/skep_ernie_2.0_large_en sequence pair mask has the following format:
::
0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1
| first sequence | second sequence |
If `token_ids_1` is `None`, this method only returns the first portion of the mask (0s).
Args:
example(obj:`list[str]`): List of input data, containing text and label if it have label.
tokenizer(obj:`PretrainedTokenizer`): This tokenizer inherits from :class:`~paddlenlp.transformers.PretrainedTokenizer`
which contains most of the methods. Users should refer to the superclass for more information regarding methods.
max_seq_len(obj:`int`): The maximum total input sequence length after tokenization.
Sequences longer than this will be truncated, sequences shorter will be padded.
is_test(obj:`False`, defaults to `False`): Whether the example contains label or not.
Returns:
input_ids(obj:`list[int]`): The list of token ids.
token_type_ids(obj: `list[int]`): List of sequence pair mask.
label(obj:`int`, optional): The input label if not is_test.
"""
# 将原数据处理成model可读入的格式,enocded_inputs是一个dict,包含input_ids、token_type_ids等字段
encoded_inputs = tokenizer(
text=example["text"], max_seq_len=max_seq_length)
# input_ids:对文本切分token后,在词汇表中对应的token id
input_ids = encoded_inputs["input_ids"]
# token_type_ids:当前token属于句子1还是句子2,即上述图中表达的segment ids
token_type_ids = encoded_inputs["token_type_ids"]
if not is_test:
# label:情感极性类别
label = np.array([example["label"]], dtype="int64")
return input_ids, token_type_ids, label
else:
# qid:每条数据的编号
qid = np.array([example["qid"]], dtype="int64")
return input_ids, token_type_ids, qid
@paddle.no_grad()
def evaluate(model, criterion, metric, data_loader):
"""
Given a dataset, it evals model and computes the metric.
Args:
model(obj:`paddle.nn.Layer`): A model to classify texts.
criterion(obj:`paddle.nn.Layer`): It can compute the loss.
metric(obj:`paddle.metric.Metric`): The evaluation metric.
data_loader(obj:`paddle.io.DataLoader`): The dataset loader which generates batches.
"""
model.eval()
metric.reset()
losses = []
for batch in data_loader:
input_ids, token_type_ids, labels = batch
logits = model(input_ids, token_type_ids)
loss = criterion(logits, labels)
losses.append(loss.numpy())
correct = metric.compute(logits, labels)
metric.update(correct)
accu = metric.accumulate()
print("eval loss: %.5f, accu: %.5f" % (np.mean(losses), accu))
model.train()
metric.reset()
将文本数据按批量进行处理
# 批量数据大小
batch_size = 32
# 文本序列最大长度
max_seq_length = 128
# 将数据处理成模型可读入的数据格式
trans_func = partial(
convert_example,
tokenizer=tokenizer,
max_seq_length=max_seq_length)
# 将数据组成批量式数据,如
# 将不同长度的文本序列padding到批量式数据中最大长度
# 将每条数据label堆叠在一起
batchify_fn = lambda samples, fn=Tuple(
Pad(axis=0, pad_val=tokenizer.pad_token_id), # input_ids
Pad(axis=0, pad_val=tokenizer.pad_token_type_id), # token_type_ids
Stack() # labels
): [data for data in fn(samples)]
train_data_loader = create_dataloader(
train_ds,
mode='train',
batch_size=batch_size,
batchify_fn=batchify_fn,
trans_fn=trans_func)
dev_data_loader = create_dataloader(
dev_ds,
mode='dev',
batch_size=batch_size,
batchify_fn=batchify_fn,
trans_fn=trans_func)
定义损失函数、优化器以及评价指标后,即可开始训练。实际运行时可以根据显存大小调整batch_size和max_seq_length大小。
import time
# from utils import evaluate
# 训练轮次
epochs = 5
# 训练过程中保存模型参数的文件夹
ckpt_dir = "skep_ckpt"
# len(train_data_loader)一轮训练所需要的step数
num_training_steps = len(train_data_loader) * epochs
# Adam优化器
optimizer = paddle.optimizer.AdamW(
learning_rate=0.000001,
parameters=model.parameters())
# 交叉熵损失函数
criterion = paddle.nn.loss.CrossEntropyLoss()
# accuracy评价指标
metric = paddle.metric.Accuracy()
接下来,我们正式对模型进行训练
# 定义空列表来存储损失和准确率
train_loss_values = []
train_acc_values = []
val_loss_values = []
val_acc_values = []
# 开启训练
global_step = 0
tic_train = time.time()
for epoch in range(1, epochs + 1):
for step, batch in enumerate(train_data_loader, start=1):
input_ids, token_type_ids, labels = batch
# 喂数据给model
logits = model(input_ids, token_type_ids)
# 计算损失函数值
loss = criterion(logits, labels)
# 预测分类概率值
probs = F.softmax(logits, axis=1)
# 计算acc
correct = metric.compute(probs, labels)
metric.update(correct)
acc = metric.accumulate()
train_loss_values.append(loss.numpy())
train_acc_values.append(acc)
global_step += 1
if global_step % 10 == 0:
print(
"global step %d, epoch: %d, batch: %d, loss: %.5f, accu: %.5f, speed: %.2f step/s"
% (global_step, epoch, step, loss, acc,
10 / (time.time() - tic_train)))
tic_train = time.time()
# 反向梯度回传,更新参数
loss.backward()
optimizer.step()
optimizer.clear_grad()
if global_step % 100 == 0:
save_dir = os.path.join(ckpt_dir, "model_%d" % global_step)
if not os.path.exists(save_dir):
os.makedirs(save_dir)
# 评估当前训练的模型
val_loss, val_acc = evaluate(model, criterion, metric, dev_data_loader)
# 记录验证损失和准确率
val_loss_values.append(val_loss)
val_acc_values.append(val_acc)
# 保存当前模型参数等
model.save_pretrained(save_dir)
# 保存tokenizer的词表等
tokenizer.save_pretrained(save_dir)
训练部分结果如下:?
为了对模型效果进行更加直观的观察,需要分别对训练集和验证集的准确率和损失率进行绘图。
import matplotlib.pyplot as plt
# 初始化用于存储每100步的平均值的新数组
average_train_loss_values = []
average_train_acc_values = []
# 循环计算每100个元素的平均值并存储到新数组中
for i in range(0, len(train_loss_values), 100):
# 取出当前100个元素
subset_train_loss = train_loss_values[i:i+100]
subset_train_acc = train_acc_values[i:i+100]
# 计算平均值并存储到新数组中
average_train_loss = np.mean(subset_train_loss)
average_train_acc = np.mean(subset_train_acc)
average_train_loss_values.append(average_train_loss)
average_train_acc_values.append(average_train_acc)
plt.figure(figsize=(10, 5))
plt.plot(average_train_loss_values, label='Train Loss')
plt.plot(val_loss_values, label='Validation Loss')
plt.xlabel('Steps')
plt.ylabel('Loss')
plt.legend()
plt.title('Training and Validation Loss')
plt.show()
# 绘制训练和验证的准确率图表
plt.figure(figsize=(10, 5))
plt.plot(average_train_acc_values, label='Train Accuracy')
plt.plot(val_acc_values, label='Validation Accuracy')
plt.xlabel('Steps')
plt.ylabel('Accuracy')
plt.legend()
plt.title('Training and Validation Accuracy')
plt.show()
经过多次调参,我们的到了最好的模型效果如下图4:
图4 SKEP模型准确率损失率曲线
除了评价模型,还需要对是训练集数据进行测试,利用模型对训练集数据进行测试。
import numpy as np
import paddle
# 处理测试集数据
trans_func = partial(
convert_example,
tokenizer=tokenizer,
max_seq_length=max_seq_length,
is_test=True)
batchify_fn = lambda samples, fn=Tuple(
Pad(axis=0, pad_val=tokenizer.pad_token_id), # input
Pad(axis=0, pad_val=tokenizer.pad_token_type_id), # segment
Stack() # qid
): [data for data in fn(samples)]
test_data_loader = create_dataloader(
test_ds,
mode='test',
batch_size=batch_size,
batchify_fn=batchify_fn,
trans_fn=trans_func)
使用训练得到的模型还可以对文本进行情感预测。
# 根据实际运行情况,更换加载的参数路径
params_path = 'skep_ckp/model_500/model_state.pdparams'
if params_path and os.path.isfile(params_path):
# 加载模型参数
state_dict = paddle.load(params_path)
model.set_dict(state_dict)
print("Loaded parameters from %s" % params_path)
print('SKEP 3.0-Medium 在ChnSentiCorp的dev集表现', end=' ')
eval_acc = evaluate(model, criterion, metric, dev_data_loader)
# 模型预测分类结果
import paddle.nn.functional as F
label_map = {0: '负面', 1: '正面'}
results = []
model.eval()
for batch in test_data_loader:
input_ids, token_type_ids, qids = batch
# 喂数据给模型
logits = model(input_ids, token_type_ids)
probs = F.softmax(logits, axis=-1)
idx = paddle.argmax(probs, axis=1).numpy()
idx = idx.tolist()
preds = [label_map[i] for i in idx]
results.extend(preds)
# 存储预测结果
# test_ds = load_dataset(read_local_dataset, path=data_path_test, is_test=True, lazy=False)
test_ds = load_dataset("chnsenticorp", splits=["test"])
res_dir = "./results"
if not os.path.exists(res_dir):
os.makedirs(res_dir)
with open(os.path.join(res_dir, "SKEP_RedBookletReview.tsv"), 'w', encoding="utf8") as f:
f.write("qid\ttext\tprediction\n")
for i, pred in enumerate(results):
f.write(test_ds[i]['qid']+"\t"+test_ds[i]['text']+"\t"+pred+"\n")
从上述结果我们可以得到模型经过训练在验证集上的准确率为
0.91250,模型效果不错,但损失0.24299依然还是有点高,表示模型的泛化能力不是很好。
PaddleNLP中Auto模块(包括AutoModel, AutoTokenizer及各种下游任务类)提供了方便易用的接口,无需指定模型类别,即可调用不同网络结构的预训练模型。PaddleNLP的预训练模型可以很容易地通过from_pretrained()方法加载,Transformer预训练模型汇总包含了40多个主流预训练模型,500多个模型权重。
AutoModelForSequenceClassification可用于句子级情感分析和目标级情感分析任务,通过预训练模型获取输入文本的表示,之后将文本表示进行分类。PaddleNLP已经实现了ERNIE 3.0预训练模型,可以通过一行代码实现ERNIE 3.0预训练模型和分词器的加载。
from paddlenlp.transformers import AutoTokenizer, AutoModelForSequenceClassification
model_name = "ernie-3.0-medium-zh"
label_list = [0,1]
model = AutoModelForSequenceClassification.from_pretrained(model_name, num_classes=len(label_list),hidden_dropout_prob=0.4)
tokenizer = AutoTokenizer.from_pretrained(model_name)
Dataset中通常为原始数据,需要经过一定的数据处理并进行采样组batch。通过`Dataset`的map函数,使用分词器将数据集从原始文本处理成模型的输入。定义paddle.io.BatchSampler和collate_fn构建 paddle.io.DataLoader。实际训练中,根据显存大小调整批大小batch_size和文本最大长度max_seq_length。
import functools
import numpy as np
from paddle.io import DataLoader, BatchSampler
from paddlenlp.data import DataCollatorWithPadding
# 数据预处理函数,利用分词器将文本转化为整数序列
def preprocess_function(examples, tokenizer, max_seq_length, is_test=False):
result = tokenizer(text=examples["text"], max_seq_len=max_seq_length)
if not is_test:
result["labels"] = examples["label"]
return result
trans_func = functools.partial(preprocess_function, tokenizer=tokenizer, max_seq_length=128)
train_ds = train_ds.map(trans_func)
dev_ds = dev_ds.map(trans_func)
# collate_fn函数构造,将不同长度序列充到批中数据的最大长度,再将数据堆叠
collate_fn = DataCollatorWithPadding(tokenizer)
# 定义BatchSampler,选择批大小和是否随机乱序,进行DataLoader
train_batch_sampler = BatchSampler(train_ds, batch_size=32, shuffle=True)
dev_batch_sampler = BatchSampler(dev_ds, batch_size=64, shuffle=True)
train_data_loader = DataLoader(dataset=train_ds, batch_sampler=train_batch_sampler, collate_fn=collate_fn)
dev_data_loader = DataLoader(dataset=dev_ds, batch_sampler=dev_batch_sampler, collate_fn=collate_fn)
定义训练所需的优化器、损失函数、评价指标等,就可以开始进行预模型微调任务。
# Adam优化器、交叉熵损失函数、accuracy评价指标
optimizer = paddle.optimizer.AdamW(learning_rate=0.000007, parameters=model.parameters())
criterion = paddle.nn.loss.CrossEntropyLoss()
metric = paddle.metric.Accuracy()
import paddle
import numpy as np
accuracy = paddle.metric.Accuracy()
def evaluate(model, criterion, metric, data_loader):
model.eval()
accuracy.reset()
losses = [] # record loss
for batch in data_loader:
input_ids = batch['input_ids']
token_type_ids = batch['token_type_ids']
labels = batch['labels']
logits = model(input_ids, token_type_ids)
loss = criterion(logits, labels)
losses.append(loss.numpy())
# compute accuracy
correct = accuracy.compute(logits, labels)
accuracy.update(correct)
# accumulate and print accuracy
accu = accuracy.accumulate()
print("eval loss: %.5f, accuracy: %.5f" % (np.mean(losses), accu))
model.train()
accuracy.reset()
return np.mean(losses), accu
接下来,将对模型ERNIE3.0模型进行训练。
# 开始训练
import time
import paddle.nn.functional as F
# 定义空列表来存储损失和准确率
train_loss_values = []
train_acc_values = []
val_loss_values = []
val_acc_values = []
# from eval import evaluate
# import evaluate
epochs = 5 # 训练轮次
ckpt_dir = "ernie_ckpt" #训练过程中保存模型参数的文件夹
best_acc = 0
best_step = 0
global_step = 0 #迭代次数
tic_train = time.time()
for epoch in range(1, epochs + 1):
for step, batch in enumerate(train_data_loader, start=1):
input_ids, token_type_ids, labels = batch['input_ids'], batch['token_type_ids'], batch['labels']
# 计算模型输出、损失函数值、分类概率值、准确率
logits = model(input_ids, token_type_ids)
loss = criterion(logits, labels)
probs = F.softmax(logits, axis=1)
correct = metric.compute(probs, labels)
metric.update(correct)
acc = metric.accumulate()
train_loss_values.append(loss.numpy())
train_acc_values.append(acc)
# 每迭代10次,打印损失函数值、准确率、计算速度
global_step += 1
if global_step % 10 == 0:
print(
"global step %d, epoch: %d, batch: %d, loss: %.5f, accu: %.5f, speed: %.2f step/s"
% (global_step, epoch, step, loss, acc,
10 / (time.time() - tic_train)))
tic_train = time.time()
# 反向梯度回传,更新参数
loss.backward()
optimizer.step()
optimizer.clear_grad()
# 每迭代100次,评估当前训练的模型、保存当前模型参数和分词器的词表等
if global_step % 100 == 0:
save_dir = ckpt_dir
if not os.path.exists(save_dir):
os.makedirs(save_dir)
val_loss, val_acc = evaluate(model, criterion, metric, dev_data_loader)
# 记录验证损失和准确率
val_loss_values.append(val_loss)
val_acc_values.append(val_acc)
print(global_step, end=' ')
acc_eval = evaluate(model, criterion, metric, dev_data_loader)
if val_acc > best_acc:
best_acc = val_acc
best_step = global_step
model.save_pretrained(save_dir)
tokenizer.save_pretrained(save_dir)
部分训练结果如下:?
提高绘制训练集和验证集的准确率损失率曲线来观察ERNIE3.0模型的效果。
import matplotlib.pyplot as plt
# 初始化用于存储每100步的平均值的新数组
average_train_loss_values = []
average_train_acc_values = []
# 循环计算每100个元素的平均值并存储到新数组中
for i in range(0, len(train_loss_values), 100):
# 取出当前100个元素
subset_train_loss = train_loss_values[i:i+100]
subset_train_acc = train_acc_values[i:i+100]
# 计算平均值并存储到新数组中
average_train_loss = np.mean(subset_train_loss)
average_train_acc = np.mean(subset_train_acc)
average_train_loss_values.append(average_train_loss)
average_train_acc_values.append(average_train_acc)
plt.figure(figsize=(10, 5))
plt.plot(average_train_loss_values, label='Train Loss')
plt.plot(val_loss_values, label='Validation Loss')
plt.xlabel('Steps')
plt.ylabel('Loss')
plt.legend()
plt.title('Training and Validation Loss')
plt.show()
# 绘制训练和验证的准确率图表
plt.figure(figsize=(10, 5))
plt.plot(average_train_acc_values, label='Train Accuracy')
plt.plot(val_acc_values, label='Validation Accuracy')
plt.xlabel('Steps')
plt.ylabel('Accuracy')
plt.legend()
plt.title('Training and Validation Accuracy')
plt.show()
经过多次调参,我们的到了最好的模型效果如下图5:
图5 ERNIE3.0模型准确率损失率曲线
我们还需要测试模型在验证集上的表现能力。
#from eval import evaluate
# 加载ERNIR 3.0最佳模型参数
params_path = 'ernie_ckpt/model_state.pdparams'
state_dict = paddle.load(params_path)
model.set_dict(state_dict)
# 也可以选择加载预先训练好的模型参数结果查看模型训练结果
# model.set_dict(paddle.load('ernie_ckpt_trained/model_state.pdparams'))
print('ERNIE 3.0-Medium 在ChnSentiCorp的dev集表现', end=' ')
eval_acc = evaluate(model, criterion, metric, dev_data_loader)
从上述模型在验证集上的表现结果来看,ERNIE模型的准确率比SKEP模型提高了0.1083,损失下降了0.03206,效果明显比SKEP模型的表现能力更好。
加载微调好的模型参数进行情感分析预测,并保存预测结果。
# 测试集数据预处理,利用分词器将文本转化为整数序列
trans_func_test = functools.partial(preprocess_function, tokenizer=tokenizer, max_seq_length=128, is_test=True)
test_ds_trans = test_ds.map(trans_func_test)
# 进行采样组batch
collate_fn_test = DataCollatorWithPadding(tokenizer)
test_batch_sampler = BatchSampler(test_ds_trans, batch_size=32, shuffle=False)
test_data_loader = DataLoader(dataset=test_ds_trans, batch_sampler=test_batch_sampler, collate_fn=collate_fn_test)
# 模型预测分类结果
import paddle.nn.functional as F
label_map = {0: '负面', 1: '正面'}
results = []
model.eval()
for batch in test_data_loader:
input_ids, token_type_ids = batch['input_ids'], batch['token_type_ids']
logits = model(batch['input_ids'], batch['token_type_ids'])
probs = F.softmax(logits, axis=-1)
idx = paddle.argmax(probs, axis=1).numpy()
idx = idx.tolist()
preds = [label_map[i] for i in idx]
results.extend(preds)
# 存储预测结果
#test_ds = load_dataset(read_local_dataset, path=data_path_test, is_test=True, lazy=False)
test_ds = load_dataset("chnsenticorp", splits=["test"])
res_dir = "./results"
if not os.path.exists(res_dir):
os.makedirs(res_dir)
with open(os.path.join(res_dir, "ERNIE_RedBookletReview.tsv"), 'w', encoding="utf8") as f:
f.write("qid\ttext\tprediction\n")
for i, pred in enumerate(results):
f.write(test_ds[i]['qid']+"\t"+test_ds[i]['text']+"\t"+pred+"\n")
我们将两个模型的测试数据和模型文件分别进行压缩保存到本地。
import os
import zipfile
from IPython.display import FileLink
# 创建一个新的压缩文件。
zip_file = zipfile.ZipFile('input.zip', 'w')
# 需要打包的文件夹列表
folders_to_zip = ['ernie_ckpt', 'results', 'sample_data', 'skep_ckpt']
# 获取当前工作目录
current_directory = os.getcwd()
# 遍历当前工作目录下的所有文件和文件夹
for root, dirs, files in os.walk(current_directory):
# 过滤出需要打包的文件夹
if any(folder in root for folder in folders_to_zip):
for file in files:
# 将需要打包的文件添加到压缩文件中
zip_file.write(os.path.join(root, file), arcname=os.path.relpath(os.path.join(root, file), current_directory))
# 关闭压缩文件。
zip_file.close()
# 创建下载链接
FileLink(r'./input.zip')
经过上述分别对SKEP模型和ERNIE3.0模型在数据集ChnSentiCorp上的情感分析能力进行训练,最终得到结果:从上述两个模型分别在验证集上的表现结果来看,ERNIE模型的准确率比SKEP模型提高了0.1083,损失下降了0.03206,文本分类明显比SKEP模型的表现能力更好,泛化能力和鲁棒性更高。但实际应该是skep_ernie_1.0_large_ch比ERNIE-3.0-medium-zh模型在情感分类任务上的效果更好,经过我们分析发现,有可能是和chnsenticorp数据集的分布有关。
ChnSentiCorp数据集包含酒店、笔记本电脑和书籍的网购评论,其中酒店评论的比例最高,其次是笔记本电脑评论,最后是书籍评论。而ERNIE模型是在一个更大的数据集上进行预训练的,其中包括各种类型的文本,因此在处理酒店评论时可能表现更好。ChnSentiCorp数据集是从互联网上收集的,因此存在一定程度的噪声。而ERNIE模型是一个大型语言模型,因此更容易受到噪声的影响。ERNIE模型和skep_ernie_1.0_large_ch模型的参数设置可能存在差异,这可能会影响模型在不同数据集上的表现。
[2]?ERNIE 3.0: Large-scale Knowledge Enhanced Pre-training for Language Understanding and Generation.