基于SKEP预训练模型进行目标级情感分析

众所周知,人类自然语言中包含了丰富的情感色彩:表达人的情绪(如悲伤、快乐)、表达人的心情(如倦怠、忧郁)、表达人的喜好(如喜欢、讨厌)、表达人的个性特征和表达人的立场等等。情感分析在商品喜好、消费决策、舆情分析等场景中均有应用。利用机器自动分析这些情感倾向,不但有助于帮助企业了解消费者对其产品的感受,为产品改进提供依据;同时还有助于企业分析商业伙伴们的态度,以便更好地进行商业决策。

本实践将进行基于目标级的情感分析任务,与语句级别的情感分析不同,目标级的情感分析将对一句话中涉及的某个方面进行分析,如下面这句话所示。

这个薯片口味有点咸,太辣了,不过口感很脆。

对于这句话来讲,显然关于薯片的口味方面是一个负向评价(咸,太辣),然而对于口感方面却是一个正向评价(很脆)。

为方便目标级别的情感分析任务建模,同样可将情感极性分为正向、负向、中性三个类别,从而将情感分析任务转变为一个分类问题,如图1所示:

  • 正向: 表示正面积极的情感,如高兴,幸福,惊喜,期待等;
  • 负向: 表示负面消极的情感,如难过,伤心,愤怒,惊恐等;
  • 中性: 其他类型的情感;

图1 情感分析任务

本实践将基于预训练SKEP模型,在seabsa16-phns
数据集上进行目标级别的情感分析任务。


学习资源


⭐ ⭐ ⭐ 欢迎点个小小的Star,开源不易,希望大家多多支持~⭐ ⭐ ⭐

1. 方案设计

本实践的设计思路如图2所示,SKEP模型的输入有两部分,一部分是要评价的对象(Aspect),另一个方面是相应的评论文本。将两者拼接之后便可以传入SKEP模型中,SKEP模型对该文本串进行语义编码后,本实践将CLS位置的token输出向量作为最终的语义编码向量。接下来将根据该语义编码向量进行情感分类。需要注意的是:seabsa16-phns数据集是个二分类数据集,其情感极性只包含正向和负向两个类别。


图2 目标级情感分析建模图

2. 数据处理

2.1 数据集介绍

本实践将在数据集seabsa16-phns进行情感分析任务。该数据集是个目标级别的情感分析数据集,其中训练集包含1334条训练样本,529条测试样本。下面展示了该数据集中的一条样本:

{

    "label":1
    "text":"display#quality",
    "text_pair":"今天有幸拿到了港版白色iPhone 5真机,试玩了一下,说说感受吧:1. 真机尺寸宽度与4/4s保持一致没有变化,长度多了大概一厘米,也就是之前所说的多了一排的图标。2. 真机重量比上一代轻了很多,个人感觉跟i9100的重量差不多。(用惯上一代的朋友可能需要一段时间适应了)3. 由于目前还没有版的SIM卡,无法插卡使用,有购买的朋友要注意了,并非简单的剪卡就可以用,而是需要去运营商更换新一代的SIM卡。4. 屏幕显示效果确实比上一代有进步,不论是从清晰度还是不同角度的视角,iPhone 5绝对要更上一层,我想这也许是相对上一代最有意义的升级了。5. 新的数据接口更小,比上一代更好用更方便,使用的过程会有这样的体会。6. 从简单的几个操作来讲速度比4s要快,这个不用测试软件也能感受出来,比如程序的调用以及照片的拍摄和浏览。不过,目前水货市场上坑爹的价格,最好大家可以再观望一下,不要急着出手。",

}

2.2 数据加载

本节我们将训练、评估和测试数据集加载到内存中。 由于ChnSentiCorp数据集属于paddlenlp的内置数据集,所以本实践我们将直接从paddlenlp里面加载数据。相关代码如下。

import os
import copy
import argparse
import numpy as np
from functools import partial
import paddle
import paddle.nn.functional as F
from paddlenlp.datasets import load_dataset
from paddlenlp.data import Pad, Stack, Tuple
from paddlenlp.metrics import AccuracyAndF1
from paddlenlp.transformers import SkepTokenizer, SkepModel, LinearDecayWithWarmup
from utils.utils import set_seed
from utils.data import convert_example_to_feature

train_ds = load_dataset("seabsa16", "phns", splits=["train"])

100%|██████████| 381/381 [00:00<00:00, 22249.56it/s]

2.3 将数据转换成特征形式

在将数据加载完成后,接下来,我们将训练和开发集数据转换成适合输入模型的特征形式,即将文本字符串数据转换成字典id的形式。这里我们要加载paddleNLP中的SkepTokenizer,其将帮助我们完成这个字符串到字典id的转换。


model_name = "skep_ernie_1.0_large_ch"
batch_size = 16
max_seq_len = 256

tokenizer = SkepTokenizer.from_pretrained(model_name)
trans_func = partial(convert_example_to_feature, tokenizer=tokenizer, max_seq_len=max_seq_len)
train_ds = train_ds.map(trans_func, lazy=False)
[2021-11-10 14:43:12,930] [    INFO] - Downloading skep_ernie_1.0_large_ch.vocab.txt from https://paddlenlp.bj.bcebos.com/models/transformers/skep/skep_ernie_1.0_large_ch.vocab.txt
100%|██████████| 55/55 [00:00<00:00, 2032.88it/s]

2.4 构造DataLoader

接下来,我们需要根据加载至内存的数据构造DataLoader,该DataLoader将支持以batch的形式将数据进行划分,从而以batch的形式训练相应模型。

batchify_fn = lambda samples, fn=Tuple(
        Pad(axis=0, pad_val=tokenizer.pad_token_id),
        Pad(axis=0, pad_val=tokenizer.pad_token_type_id),
        Stack(dtype="int64")
    ): fn(samples)

train_batch_sampler = paddle.io.BatchSampler(train_ds, batch_size=batch_size, shuffle=True)
train_loader = paddle.io.DataLoader(train_ds, batch_sampler=train_batch_sampler, collate_fn=batchify_fn)

3. 模型构建

本案例中,我们将基于SKEP模型实现图2所展示的情感分析功能。具体来讲,我们将处理好的文本数据输入SLEP模型中,ERNIE将会对文本的每个token进行编码,产生对应向量序列,我们任务CLS位置对应的输出向量能够代表语句的完整语义,所以将利用该向量进行情感分类。相应代码如下。

class SkepForSquenceClassification(paddle.nn.Layer):
    def __init__(self, skep, num_classes=2, dropout=None):
        super(SkepForSquenceClassification, self).__init__()
        self.num_classes = num_classes
        self.skep = skep
        self.dropout = paddle.nn.Dropout(p=dropout if dropout is not None else self.skep.config["hidden_dropout_prob"])
        self.classifier = paddle.nn.Linear(self.skep.config["hidden_size"], self.num_classes)

    def forward(self, input_ids, token_type_ids=None, position_ids=None, attention_mask=None):
        _, pooled_output = self.skep(input_ids, token_type_ids=token_type_ids, position_ids=position_ids, attention_mask=attention_mask)
        pooled_output = self.dropout(pooled_output)
        logits = self.classifier(pooled_output)

        return logits

4. 训练配置

接下来,定义情感分析模型训练时的环境,包括:配置训练参数、配置模型参数,定义模型的实例化对象,指定模型训练迭代的优化算法等,相关代码如下。

# model hyperparameter  setting
num_epoch = 3
learning_rate = 3e-5
weight_decay = 0.01
warmup_proportion = 0.1
max_grad_norm = 1.0
log_step = 20
eval_step = 100
seed = 1000
checkpoint = "./checkpoint/"

set_seed(seed)
use_gpu = True if paddle.get_device().startswith("gpu") else False
if use_gpu:
    paddle.set_device("gpu:0")
if not os.path.exists(checkpoint):
    os.mkdir(checkpoint)

skep = SkepModel.from_pretrained(model_name)
model = SkepForSquenceClassification(skep, num_classes=len(train_ds.label_list))

num_training_steps = len(train_loader) * num_epoch
lr_scheduler = LinearDecayWithWarmup(learning_rate=learning_rate, total_steps=num_training_steps, warmup=warmup_proportion)
decay_params = [p.name for n, p in model.named_parameters() if not any(nd in n for nd in ["bias", "norm"])]
grad_clip = paddle.nn.ClipGradByGlobalNorm(max_grad_norm)
optimizer = paddle.optimizer.AdamW(learning_rate=lr_scheduler, parameters=model.parameters(), weight_decay=weight_decay, apply_decay_param_fun=lambda x: x in decay_params, grad_clip=grad_clip)

metric = AccuracyAndF1()
[2021-11-10 14:47:07,184] [    INFO] - Already cached /home/aistudio/.paddlenlp/models/skep_ernie_1.0_large_ch/skep_ernie_1.0_large_ch.pdparams

5. 模型训练

本节我们将定义一个train函数,我们将在该函数中进行训练模型。在训练过程中,每隔log_steps步打印一次日志,以观测模型训练效果。相关代码如下:

def evaluate(model, data_loader, metric):

    model.eval()
    metric.reset()
    for idx, batch_data in enumerate(data_loader):
        input_ids, token_type_ids, labels = batch_data
        logits = model(input_ids, token_type_ids=token_type_ids)        

        # count metric
        correct = metric.compute(logits, labels)
        metric.update(correct)

    accuracy, precision, recall, f1, _ = metric.accumulate()

    return accuracy, precision, recall, f1

def train():
    global_step = 1
    model.train()
    for epoch in range(1, num_epoch+1):
        for batch_data in train_loader():
            input_ids, token_type_ids, labels = batch_data
            logits = model(input_ids, token_type_ids=token_type_ids)
            loss = F.cross_entropy(logits, labels)

            loss.backward()
            lr_scheduler.step()
            optimizer.step()
            optimizer.clear_grad()

            if global_step > 0 and global_step % log_step == 0:
                print(f"epoch: {epoch} - global_step: {global_step}/{num_training_steps} - loss:{loss.numpy().item():.6f}")
            
            global_step += 1

    paddle.save(model.state_dict(), f"{checkpoint}/final.pdparams")

train()
epoch: 1 - global_step: 20/252 - loss:0.664008
epoch: 1 - global_step: 40/252 - loss:0.815231
epoch: 1 - global_step: 60/252 - loss:0.662032
epoch: 1 - global_step: 80/252 - loss:0.609795
epoch: 2 - global_step: 100/252 - loss:0.606419
epoch: 2 - global_step: 120/252 - loss:0.653587
epoch: 2 - global_step: 140/252 - loss:0.592340
epoch: 2 - global_step: 160/252 - loss:0.961530
epoch: 3 - global_step: 180/252 - loss:0.514817
epoch: 3 - global_step: 200/252 - loss:0.630167
epoch: 3 - global_step: 220/252 - loss:0.496368
epoch: 3 - global_step: 240/252 - loss:0.686450

6. 模型推理

实现一个模型预测的函数,实现任意输入一串带有情感的文本串,如:“交通方便;环境很好;服务态度很好 房间较大”,期望能够输出这段文本描述中所蕴含的情感类别。首先我们先加载训练好的模型参数,然后进行推理。相关代码如下。

# process data related
model_name = "skep_ernie_1.0_large_ch"
model_path = os.path.join(checkpoint, "final.pdparams")
id2label = {0:"Negative", 1:"Positive"}

# load model
loaded_state_dict = paddle.load(model_path)
ernie = SkepModel.from_pretrained(model_name)
model = SkepForSquenceClassification(ernie, num_classes=len(train_ds.label_list))    
model.load_dict(loaded_state_dict)
[2021-11-10 14:51:00,184] [    INFO] - Already cached /home/aistudio/.paddlenlp/models/skep_ernie_1.0_large_ch/skep_ernie_1.0_large_ch.pdparams
def predict(text, text_pair, model, tokenizer, id2label, max_seq_len=256):

    model.eval()
    # processing input text
    encoded_inputs = tokenizer(text=text, text_pair=text_pair, max_seq_len=max_seq_len)
    input_ids = paddle.to_tensor([encoded_inputs["input_ids"]])
    token_type_ids = paddle.to_tensor([encoded_inputs["token_type_ids"]])

    # predict by model and decoding result 
    logits = model(input_ids, token_type_ids=token_type_ids)
    label_id =  paddle.argmax(logits, axis=1).numpy()[0]

    # print predict result
    print(f"text: {text} \ntext_pair:{text_pair} \nlabel: {id2label[label_id]}")


text = "display#quality"
text_pair = "mk16i用后的体验感觉不错,就是有点厚,屏幕分辨率高,运行流畅,就是不知道能不能刷4.0的系统啊"

predict(text, text_pair, model, tokenizer, id2label, max_seq_len=max_seq_len)

text: display#quality 
text_pair:mk16i用后的体验感觉不错,就是有点厚,屏幕分辨率高,运行流畅,就是不知道能不能刷4.0的系统啊 
label: Positive

7. 更多深度学习资源

7.1 一站式深度学习平台awesome-DeepLearning

  • 深度学习入门课
  • 深度学习百问
  • 特色课
  • 产业实践

PaddleEdu使用过程中有任何问题欢迎在awesome-DeepLearning提issue,同时更多深度学习资料请参阅飞桨深度学习平台

记得点个Star⭐收藏噢~~

7.2 飞桨技术交流群(QQ)

目前QQ群已有2000+同学一起学习,欢迎扫码加入

Logo

学大模型,用大模型上飞桨星河社区!每天8点V100G算力免费领!免费领取ERNIE 4.0 100w Token >>>

更多推荐