手把手教你基于PaddlePaddle的情绪识别

手把手教你使用PaddlePaddle进行情绪识别

一、项目描述

  • 本项目为基于BERT模型的情绪识别项目,内部包含了大量的注释,代码风格极度友好,希望可以帮助到初学PaddlePaddle的朋友。
  • 本项目使用PaddleNLP的transformers实现模型代码编写、训练及测试。
  • 本项目的代码详细讲解,可以自行阅读代码,也可查看代码注释介绍:https://zhuanlan.zhihu.com/p/411826397。
  • 本项目可以在项目中运行,记得开GPU环境。
  • 所有代码(py文件)均在work目录下。
  • 所有代码均在Notebook的cell可运行。

二、文件结构(work目录下)

  • bert-paddle 存放预训练模型路径
    • vocab.txt 字典文件,该字典为大小为21128。
    • model_config.json 模型配置文件。
    • model_state.pdparams 模型参数文件。
  • data_dir 存放数据的文件夹
    • usual_train.txt 原始训练集文件。
    • usual_eval_labeled.txt 原始测试集文件。
  • data_helper.py 数据预处理文件,将数据进行简单的格式转换。
  • data_set.py 数据类文件,定义模型所需的数据类,方便模型训练使用。
  • model.py 情绪识别模型文件,主要对transformers包中BertPretrainedModel的重写。
  • train.py 情绪识别模型训练文件。
  • predict.py 根据训练好的模型,进行情绪预测,并且包含了动态图、onnx和静态图的时间评测。
  • requirements.txt 环境配置文件,按照一些额外的包。

三、数据集

数据集来自SMP2020微博情绪分类评测比赛中通用微博数据集。按照其蕴含的情绪分为以下六个类别之一:积极、愤怒、悲伤、恐惧、惊奇和无情绪。
SMP2020微博情绪分类评测比赛链接:https://smp2020ewect.github.io/

情绪文本
积极哥,你猜猜看和喜欢的人一起做公益是什么感觉呢。我们的项目已经进入一个新阶段了,现在特别有成就感。加油加油。
愤怒每个月都有特别气愤的时候。,多少个瞬间想甩手不干了,杂七杂八,当我是什么。
悲伤回忆起老爸的点点滴滴,心痛…为什么.接受不了
恐惧明明是一篇言情小说,看完之后为什么会恐怖的睡不着呢,越想越害怕[吃驚]
惊奇我竟然不知道kkw是丑女无敌里的那个
无情绪我们做不到选择缘分,却可以珍惜缘分。

四、数据预处理

数据预处理代码,主要是将其原始数据格式进行转换,查看数据集中各个类别的占比。其实,正常项目,还可以增加一些数据清洗的工作(本项目省略了数据清洗的部分)。

import json


def sentiment_analysis_trans_data(path, save_path):
    """
    数据预处理代码,将原始数据格式转换成模型所需格式数据,并统计各标签数据的数量
    Args:
        path: 原始数据路径
        save_path: 保存数据路径

    Returns:

    """
    fin = open(save_path, "w", encoding="utf-8")
    data_number = {}
    with open(path, "r", encoding="utf-8") as fh:
        # 加载原始数据
        data = json.load(fh)
        # 对原始数据进行遍历
        for i, line in enumerate(data):
            sample = {"text": line["content"], "label": line["label"]}
            # 如果标签在data_number中,直接对其value进行加1操作;如果不在,则将标签加入的data_number中,value设为1。
            if line["label"] not in data_number:
                data_number[line["label"]] = 1
            else:
                data_number[line["label"]] += 1
            # 将每一个文本和对应的标签,写入到保存文件中
            fin.write(json.dumps(sample, ensure_ascii=False) + "\n")
    print("data_number: ", data_number)
sentiment_analysis_ori_train_path = "work/data/usual_train.txt"
sentiment_analysis_train_path = "work/data/train.json"
sentiment_analysis_trans_data(sentiment_analysis_ori_train_path, sentiment_analysis_train_path)

sentiment_analysis_ori_test_path = "work/data/usual_eval_labeled.txt"
sentiment_analysis_test_path = "work/data/test.json"
sentiment_analysis_trans_data(sentiment_analysis_ori_test_path, sentiment_analysis_test_path)
data_number:  {'angry': 8344, 'happy': 5379, 'neutral': 5749, 'surprise': 2086, 'sad': 4990, 'fear': 1220}
data_number:  {'angry': 586, 'happy': 391, 'sad': 346, 'neutral': 420, 'fear': 87, 'surprise': 170}

五、数据类实现

数据类的作用是将文本数据转换成模型可以使用的索引数据,并预先存储下来。避免模型每训练一步,都进行无效的数据转换操作。

# 导入所需要的py包
from paddle.io import Dataset
from paddlenlp.data import Pad, Stack, Dict
import paddle
import json
import os
import logging

logger = logging.getLogger(__name__)

定义模型所需的SentimentAnalysisDataSet数据类,继承paddle.io.Dataset类,包含__init__函数、load_data函数、convert_featrue函数、__len__函数、以及__getitem__函数

class SentimentAnalysisDataSet(Dataset):
    def __init__(self, tokenizer, max_len, data_dir, data_set_name, path_file=None, is_overwrite=False):
        """
        模型所需的数据类,继承paddle.io.Dataset类
        Args:
            tokenizer: 分词器
            max_len: 文本最大长度
            data_dir: 保存缓存数据路径
            data_set_name: 数据集名字
            path_file: 原始数据文件路径
            is_overwrite: 是否对缓存文件进行重写
        """
        super(SentimentAnalysisDataSet, self).__init__()
        self.tokenizer = tokenizer
        self.max_len = max_len
        # 6种标签的标签字典
        self.label2id = {'angry': 0, 'happy': 1, 'neutral': 2, 'surprise': 3, 'sad': 4, 'fear': 5}
        self.id2label = {0: "angry", 1: "happy", 2: "neutral", 3: "surprise", 4: "sad", 5: "fear"}
        cached_feature_file = os.path.join(data_dir, "cached_{}_{}".format(data_set_name, max_len))
        # 判断如果存在缓存文件,则直接对其进行加载。
        if os.path.exists(cached_feature_file) and not is_overwrite:
            logger.info("已经存在缓存文件{},直接加载".format(cached_feature_file))
            self.data_set = paddle.load(cached_feature_file)["data_set"]
        else:
            # 如果不存在缓存文件,则调用load_data函数,进行数据预处理,再将其保存成缓存文件。
            logger.info("不存在缓存文件{},进行数据预处理操作".format(cached_feature_file))
            self.data_set = self.load_data(path_file)
            logger.info("数据预处理操作完成,将处理后的数据存到{}中,作为缓存文件".format(cached_feature_file))
            paddle.save({"data_set": self.data_set}, cached_feature_file)

    def load_data(self, path_file):
        """
        对原始数据种每一条数据进行数据预处理操作,将文本转换成模型可用的id索引形式。
        Args:
            path_file: 原始文件路径

        Returns:

        """
        data_set = []
        with open(path_file, "r", encoding="utf-8") as fh:
            for i, line in enumerate(fh):
                # 加载每一条数据
                sample = json.loads(line.strip())
                # 调用convert_featrue函数,对单条数据进行文本转换成操作
                input_ids, attention_mask, label = self.convert_featrue(sample)
                sample["input_ids"] = input_ids
                sample["attention_mask"] = attention_mask
                sample["label"] = label
                # 将数据存放到data_set中。
                data_set.append(sample)
        return data_set

    def convert_featrue(self, sample):
        """
        将单个样本转换成模型可用的id索引形式
        Args:
            sample: 单条样本

        Returns:

        """
        # 获取标签索引
        label = self.label2id[sample["label"]]
        # 将本文进行tokenize
        tokens = self.tokenizer.tokenize(sample["text"])
        # 进行长度判断,若长于最大长度,则进行截断
        if len(tokens) > self.max_len - 2:
            tokens = tokens[:self.max_len - 2]
        # 将其头尾加上[CLS]和[SEP]
        tokens = ["[CLS]"] + tokens + ["[SEP]"]
        # 将token转化成id
        input_ids = self.tokenizer.convert_tokens_to_ids(tokens)
        # 获取模型所需的attention_mask,大小与input_ids一致
        attention_mask = [1] * len(input_ids)
        assert len(input_ids) == len(attention_mask)
        return input_ids, attention_mask, label

    def __len__(self):
        """获取数据总长度"""
        return len(self.data_set)

    def __getitem__(self, idx):
        """按照索引,获取data_set中的指定数据"""
        instance = self.data_set[idx]
        return instance

在模型训练时,对batch数据进行tensor转换的函数,定义DataLoader所需的collate_fun函数,将数据处理成tensor形式。

def collate_func_sentiment_analysis(batch_data):
    """
    DataLoader所需的collate_fun函数,将数据处理成tensor形式
    Args:
        batch_data: batch数据

    Returns:

    """
    # 获取batch数据的大小
    batch_size = len(batch_data)
    # 如果batch_size为0,则返回一个空字典
    if batch_size == 0:
        return {}
    input_ids_list, attention_mask_list, labels_list = [], [], []
    # 遍历batch数据,将每一个数据,转换成tensor的形式
    for instance in batch_data:
        input_ids_temp = instance["input_ids"]
        attention_mask_temp = instance["attention_mask"]
        labels_temp = instance["label"]
        input_ids_list.append(paddle.to_tensor(input_ids_temp, dtype="int64"))
        attention_mask_list.append(paddle.to_tensor(attention_mask_temp, dtype="int64"))
        labels_list.append(labels_temp)
    # 对一个batch内的数据,进行padding
    return {"input_ids": Pad(pad_val=0, axis=0)(input_ids_list),
            "attention_mask": Pad(pad_val=0, axis=0)(attention_mask_list),
            "label": Stack(dtype="int64")(labels_list)}

六、模型代码实现

模型部分,主要使用PaddleNLP的transformers的BertPretrainedModel类实现模型代码。

BERT模型为主流的预训练语言模型,开启了自然语言处理的新范式。
BERT模型详细介绍见:https://paddlepedia.readthedocs.io/en/latest/tutorials/pretrain_model/bert.html
本人也整理了一些常用预训练语言模型的知识点,以QA的形式给出,见:https://zhuanlan.zhihu.com/p/406512290

import paddle
import paddle.nn as nn
from paddlenlp.transformers import BertPretrainedModel
import paddle.nn.functional as F


class SentimentAnalysisModel(BertPretrainedModel):
    base_model_prefix = "bert"
    def __init__(self, bert, number_label=3):
        """
        情绪识别模型继承paddlenlp.transformers.BertPretrainedModel类
        Args:
            bert: bert模型
            number_label: 标签个数
        """
        super(SentimentAnalysisModel, self).__init__()
        self.bert = bert
        self.classifier = nn.layer.Linear(self.bert.config["hidden_size"], number_label)
        self.loss_fct = nn.CrossEntropyLoss(soft_label=False, axis=-1)

    def forward(self, input_ids, attention_mask, label=None):
        # 将attention_mask进行维度变换,从2维变成4维。paddlenlp.transformers的实现与torch或tf不一样,不会自动进行维度扩充。
        attention_mask = paddle.unsqueeze(attention_mask, axis=[1, 2])
        # 获取[CLS]向量pooled_output
        pooled_output = self.bert(input_ids=input_ids, attention_mask=attention_mask)[1]
        # 对pooled_output进行全连接,映射到number_label上
        logits = self.classifier(pooled_output)
        # 使用softmax,获取每个标签类别的概率
        probs = F.softmax(logits, axis=1)
        # 获取标签类别概率最大的标签
        pred_label = paddle.argmax(logits, axis=-1)
        outputs = (pred_label, probs)
        # 如果label不是None,则使用CrossEntropyLoss求解loss
        if label is not None:
            loss = self.loss_fct(logits, label)
            outputs = (loss,) + outputs
        return outputs

七、模型训练

训练参数可自行添加,包含参数具体如下:

参数类型默认值描述
devicestr“0”设置设备编号
train_file_pathstr“work/data/train.json”训练集文件路径
test_file_pathstr“work/data/test.json”测试集文件路径
pretrained_model_pathstr“work/bert-paddle”预训练模型路径
vocab_pathstr“work/bert-paddle/vocab.txt”模型字典文件路径
data_dirstr“data”缓存文件保存路径
num_train_epochsint5训练轮数
train_batch_sizeint64训练的batch_size大小
test_batch_sizeint32测试的batch_size大小
learning_ratefloat5e-5学习率
warmup_proportionfloat0.1warm up概率,即训练总步长的百分之多少,进行warm up
weight_decayfloat0.01AdamW优化器的权重衰减系数
adam_epsilonfloat1e-8AdamW优化器的epsilon值
logging_stepsint5log记录步数
save_model_stepsint200模型验证步数
output_dirstr“work/output_dir/”模型输出路径
seedint2020随机种子
max_lenint256模型输入最大长度
num_labelsint6标签个数

导入所需的py包

import paddle
import os
import random
import numpy as np
import argparse
import logging
import json
from paddlenlp.transformers import BertTokenizer
from paddle.io import DataLoader, SequenceSampler, RandomSampler, BatchSampler
from paddlenlp.data import Pad, Stack, Dict
from paddlenlp.ops.optimizer import AdamW
from paddlenlp.transformers import LinearDecayWithWarmup
from tqdm import tqdm, trange
from sklearn.metrics import classification_report, f1_score, accuracy_score
import json
from tensorboardX import SummaryWriter

logging.basicConfig(format='%(asctime)s - %(levelname)s - %(name)s -   %(message)s',
                    datefmt='%m/%d/%Y %H:%M:%S',
                    level=logging.INFO)
logger = logging.getLogger(__name__)

定义模型训练时,每一次保存模型进行验证的evaluate函数

def evaluate(model, test_data, args):
    """
    对测试数据集进行模型测试
    Args:
        model: 模型
        test_data: 测试数据类
        args: 训练参数配置信息

    Returns:

    """
    # 通过BatchSampler和DataLoader构建测试所需的迭代器,注意shuffle需要设为False。
    test_sampler = BatchSampler(test_data, batch_size=args.test_batch_size, shuffle=False, drop_last=False)
    test_data_loader = DataLoader(test_data, batch_sampler=test_sampler, collate_fn=collate_func_sentiment_analysis)
    iter_bar = tqdm(test_data_loader, desc="iter", disable=False)
    eval_loss = 0.0
    true_label = []
    pre_label = []
    # 开始测试
    for step, batch in enumerate(iter_bar):
        model.eval()
        input_ids = batch["input_ids"]
        attention_mask = batch["attention_mask"]
        label = batch["label"]
        # 获取预测结果
        [loss, pred_label, _] = model.forward(input_ids, attention_mask, label)
        # 记录loss、预测结果、真实结果
        eval_loss += loss.item()
        true_label.extend(label.numpy())
        pre_label.extend(pred_label.numpy())
    true_label = np.array(true_label)
    pre_label = np.array(pre_label)
    # 计算测试集的loss、acc以及f1
    f1_micro = f1_score(true_label, pre_label, average='micro')
    f1_macro = f1_score(true_label, pre_label, average='macro')
    f1 = (f1_micro+f1_macro)/2.0
    acc = accuracy_score(true_label, pre_label)
    eval_loss = eval_loss / len(test_data_loader)
    return eval_loss, acc, f1

定义模型训练函数

def train(model, device, tokenizer, args):
    """
    训练模型
    Args:
        model: 模型
        device: 设备信息
        tokenizer: 分词器
        args: 训练参数配置信息

    Returns:

    """
    tb_write = SummaryWriter()
    # 通过SentimentAnalysisDataSet类构建训练所需的data_set
    train_data = SentimentAnalysisDataSet(tokenizer, args.max_len, args.data_dir, "train_sentiment_analysis",
                                          args.train_file_path)
    # 通过BatchSampler和DataLoader构建训练所需的迭代器
    train_sampler = BatchSampler(train_data, batch_size=args.train_batch_size, shuffle=True, drop_last=False)
    train_data_loader = DataLoader(train_data, batch_sampler=train_sampler, collate_fn=collate_func_sentiment_analysis)
    # 通过SentimentAnalysisDataSet类构建测试所需的data_set
    test_data = SentimentAnalysisDataSet(tokenizer, args.max_len, args.data_dir, "test_sentiment_analysis",
                                         args.test_file_path)
    
    # 计算模型训练所需的总步数
    total_steps = len(train_data_loader) * args.num_train_epochs
    logger.info("总训练步数为:{}".format(total_steps))
    # 将模型映射到指定的设备上
    model.to(device)

    # 设置优化器
    scheduler = LinearDecayWithWarmup(args.learning_rate, total_steps, args.warmup_proportion)
    decay_params = [
        p.name for n, p in model.named_parameters()
        if not any(nd in n for nd in ["bias", "norm"])
    ]
    optimizer = paddle.optimizer.AdamW(
        learning_rate=scheduler,
        parameters=model.parameters(),
        weight_decay=args.weight_decay,
        epsilon=args.adam_epsilon,
        apply_decay_param_fun=lambda x: x in decay_params)

    model.train()
    tr_loss, logging_loss, max_acc = 0.0, 0.0, 0.0
    global_step = 0
    # 开始训练模型
    for iepoch in trange(0, int(args.num_train_epochs), desc="Epoch", disable=False):
        iter_bar = tqdm(train_data_loader, desc="Iter (loss=X.XXX)", disable=False)
        for step, batch in enumerate(iter_bar):
            input_ids = batch["input_ids"]
            attention_mask = batch["attention_mask"]
            label = batch["label"]
            # 获取训练结果
            outputs = model.forward(input_ids, attention_mask, label)
            loss = outputs[0]
            tr_loss += loss.item()
            # 将损失值放到Iter中,方便观察
            iter_bar.set_description("Iter (loss=%5.3f)" % loss.item())
            # 损失进行回传
            loss.backward()
            # 参数进行优化
            optimizer.step()
            scheduler.step()
            # 清空梯度
            optimizer.clear_grad()
            global_step += 1
            # 如果步数整除logging_steps,则记录学习率和训练集损失值
            if args.logging_steps > 0 and global_step % args.logging_steps == 0:
                tb_write.add_scalar("lr", scheduler.get_lr(), global_step)
                tb_write.add_scalar("train_loss", (tr_loss - logging_loss) / args.logging_steps, global_step)
                logging_loss = tr_loss
            # 如果步数整除save_model_steps,则进行模型测试,记录测试集的损失、准确率以及F1
            if args.save_model_steps > 0 and global_step % args.save_model_steps == 0:
                eval_loss, eval_acc, eval_f1 = evaluate(model, test_data, args)
                logger.info("eval_loss is {}, eval_acc is {} , eval_f1 is {}".format(eval_loss, eval_acc, eval_f1))
                tb_write.add_scalar("eval_loss", eval_loss, global_step)
                tb_write.add_scalar("eval_acc", eval_acc, global_step)
                tb_write.add_scalar("eval_f1", eval_f1, global_step)
                # 当eval_f1大于max_acc时,更新保存模型,并进行记录
                if eval_f1 >= max_acc:
                    max_acc = eval_f1
                    output_dir = os.path.join(args.output_dir, "checkpoint")
                    # 更新保存模型
                    model.save_pretrained(output_dir)
                    json_output_dir = os.path.join(output_dir, "json_data.json")
                    # 记录对应指标
                    fin = open(json_output_dir, "w", encoding="utf-8")
                    fin.write(json.dumps(
                        {"eval_loss": eval_loss, "eval_acc": eval_acc, "eval_f1": eval_f1, "global_step": global_step},
                        ensure_ascii=False, indent=4) + "\n")
                    fin.close()
                model.train()

定义参数配置函数

def set_args():
    """设置训练模型所需参数"""
    parser = argparse.ArgumentParser()
    parser.add_argument('--device', default='0', type=str, help='设备编号')
    parser.add_argument('--train_file_path', default='work/data/train.json', type=str, help='训练集文件路径')
    parser.add_argument('--test_file_path', default='work/data/test.json', type=str, help='测试集文件路径')
    parser.add_argument('--vocab_path', default="work/bert-paddle/vocab.txt", type=str, help='模型字典文件路径')
    parser.add_argument('--pretrained_model_path', default="work/bert-paddle", type=str, help='预训练模型路径')
    parser.add_argument('--data_dir', default='data/', type=str, help='缓存文件保存路径')
    parser.add_argument('--num_train_epochs', default=5, type=int, help='训练轮数')
    parser.add_argument('--train_batch_size', default=64, type=int, help='训练的batch_size大小')
    parser.add_argument('--test_batch_size', default=32, type=int, help='测试的batch_size大小')
    parser.add_argument('--learning_rate', default=5e-5, type=float, help='学习率')
    parser.add_argument('--warmup_proportion', default=0.1, type=float, help='warm up概率,即训练总步长的百分之多少,进行warm up')
    parser.add_argument("--weight_decay", default=0.01, type=float, help='AdamW优化器的权重衰减系数')
    parser.add_argument('--adam_epsilon', default=1e-8, type=float, help='AdamW优化器的epsilon值')
    parser.add_argument('--save_model_steps', default=200, type=int, help='模型验证步数')
    parser.add_argument('--logging_steps', default=5, type=int, help='log记录步数')
    parser.add_argument('--output_dir', default='work/output_dir', type=str, help='模型输出路径')
    parser.add_argument('--seed', type=int, default=2020, help='随机种子')
    parser.add_argument('--max_len', type=int, default=256, help='模型输入最大长度')
    parser.add_argument('--num_labels', type=int, default=6, help='标签个数')
    return parser.parse_args(args=[])

开始模型训练

args = set_args()
# 设置显卡信息
os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID"
os.environ["CUDA_VISIBLE_DEVICES"] = args.device
# 获取device信息,用于模型训练
device = "gpu:{}".format(args.device) if paddle.fluid.is_compiled_with_cuda() and int(args.device) >= 0 else "cpu"
paddle.device.set_device(device)
# 设置随机种子,方便模型复现
if args.seed:
    paddle.seed(args.seed)
    random.seed(args.seed)
    np.random.seed(args.seed)
# 加载预训练模型,进行模型初始化
model = SentimentAnalysisModel.from_pretrained(args.pretrained_model_path, number_label=args.num_labels)
# 实例化tokenizer
tokenizer = BertTokenizer(args.vocab_path, do_lower_case=True)
# 创建模型的输出目录
if not os.path.exists(args.output_dir):
    os.mkdir(args.output_dir)
# 开始训练
train(model, device, tokenizer, args)

最优模型模型保存在"work/output_dir/checkpoint"下。

本项目提供的模型,共训练了5个epoch,训练时长大约为20分钟,模型最优效果如下:
{“eval_loss”:0.6070899059848179,“eval_acc”:0.7905,“eval_f1”:0.7779,“global_step”:800}

八、模型预测

模型测试部分,本项目提供了三种模型测试,分别是动态图模型测试、ONNX模型测试和静态图模型测试。

由于PaddlePaddle2.0主要推的是动态图操作,总所周知,动态图方便代码编写,便与debug;但是缺点就是速度较慢(每一次运算都会加载一遍图)。在工业界上,不光光要看效果,还要看速度。因此将模型加速是必不可少的步骤。在不修改模型参数的情况下,我们可以修改框架进行提速,比如将模型转成ONNX或者将动态图转成静态图。

# 安装所需的py包
!pip install paddle2onnx==0.8.2
!pip install onnx==1.9.0
!pip install onnxruntime-gpu==1.4.0
!pip install ppqi==1.0.4
Looking in indexes: https://mirror.baidu.com/pypi/simple/
Requirement already satisfied: paddle2onnx==0.8.2 in /opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages (0.8.2)
Requirement already satisfied: onnx<=1.9.0 in /opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages (from paddle2onnx==0.8.2) (1.9.0)
Requirement already satisfied: six in /opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages (from paddle2onnx==0.8.2) (1.15.0)
Requirement already satisfied: typing-extensions>=3.6.2.1 in /opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages (from onnx<=1.9.0->paddle2onnx==0.8.2) (3.10.0.2)
Requirement already satisfied: protobuf in /opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages (from onnx<=1.9.0->paddle2onnx==0.8.2) (3.14.0)
Requirement already satisfied: numpy>=1.16.6 in /opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages (from onnx<=1.9.0->paddle2onnx==0.8.2) (1.20.3)
Looking in indexes: https://mirror.baidu.com/pypi/simple/
Requirement already satisfied: onnx==1.9.0 in /opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages (1.9.0)
Requirement already satisfied: typing-extensions>=3.6.2.1 in /opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages (from onnx==1.9.0) (3.10.0.2)
Requirement already satisfied: numpy>=1.16.6 in /opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages (from onnx==1.9.0) (1.20.3)
Requirement already satisfied: protobuf in /opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages (from onnx==1.9.0) (3.14.0)
Requirement already satisfied: six in /opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages (from onnx==1.9.0) (1.15.0)
Looking in indexes: https://mirror.baidu.com/pypi/simple/
Requirement already satisfied: onnxruntime-gpu==1.4.0 in /opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages (1.4.0)
Requirement already satisfied: numpy>=1.16.6 in /opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages (from onnxruntime-gpu==1.4.0) (1.20.3)
Requirement already satisfied: protobuf in /opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages (from onnxruntime-gpu==1.4.0) (3.14.0)
Requirement already satisfied: six>=1.9 in /opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages (from protobuf->onnxruntime-gpu==1.4.0) (1.15.0)
Looking in indexes: https://mirror.baidu.com/pypi/simple/
Requirement already satisfied: ppqi==1.0.4 in /opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages (1.0.4)

(1)预测时的数据处理操作,将单个文本进行数据转换,得到模型所使用的id索引数据

import paddle
from paddlenlp.data import Pad
import os
import numpy as np
import time
import argparse
import onnx
import json
from ppqi import InferenceModel
try:
    import onnxruntime
except:
    pass
from paddlenlp.transformers import BertTokenizer


def convert_featrue(sample, max_len, tokenizer):
    """
    将单个文本,进行数据转换,得到模型所使用的id索引数据
    Args:
        sample: 单个文本,str类型
        max_len: 最大长度
        tokenizer: 分词器

    Returns:

    """
    # 对文本进行tokenize操作
    tokens = tokenizer.tokenize(sample)
    # 进行长度判断,若长于最大长度,则进行截断
    if len(tokens) > max_len - 2:
        tokens = tokens[:max_len - 2]
    # 将其头尾加上[CLS]和[SEP]
    tokens = ["[CLS]"] + tokens + ["[SEP]"]
    # 将token转化成id,并获取模型所需的attention_mask
    input_ids = tokenizer.convert_tokens_to_ids(tokens)
    attention_mask = [1] * len(input_ids)
    assert len(input_ids) == len(attention_mask)
    # 对input_ids和attention_mask进行补全操作,补到最大长度
    # 补全到最大长度,是由于后面会对动态图转onnx和静态图,输入需要定长
    if len(input_ids) < max_len:
        input_ids = input_ids + [0] * (max_len - len(input_ids))
        attention_mask = attention_mask + [0] * (max_len - len(attention_mask))
    return input_ids, attention_mask


def batch_data(sample_list, max_len, tokenizer):
    """
    将数据处理成tensor形式
    Args:
        batch_data: batch数据

    Returns:

    """
    input_ids_list, attention_mask_list = [], []
    for sample in sample_list:
        input_ids, attention_mask = convert_featrue(sample, max_len, tokenizer)
        input_ids_list.append(input_ids)
        attention_mask_list.append(attention_mask)
    return {"input_ids": paddle.to_tensor(Pad(pad_val=0, axis=0)(input_ids_list), dtype="int64"),
            "attention_mask": paddle.to_tensor(Pad(pad_val=0, axis=0)(attention_mask_list), dtype="int64")}

(2)对模型(动态图)进行测试

定义动态图预测函数

def predict_one_sample(sample_list, model, tokenizer, max_len, id2label):
    """
    对数据进行批量预测,获取每个样本对应的预测标签
    Args:
        sample_list: 样本序列,为一个list
        model: 模型
        tokenizer: 分词器
        max_len: 最大长度
        id2label: 标签字典

    Returns:

    """
    # 将数据转换成模型可使用的tensor形式
    batch = batch_data(sample_list, max_len, tokenizer)
    # 关掉模型的dropout
    model.eval()
    # 关掉模型的梯度计算
    with paddle.no_grad():
        input_ids = batch["input_ids"]
        attention_mask = batch["attention_mask"]
        # 获取模型预测结果
        [pred_label, _] = model.forward(input_ids, attention_mask)
        pred_label = pred_label.numpy()
    # 将模型预测结果转换成标签
    label_name = [id2label[pred] for pred in pred_label]
    return zip(sample_list, label_name)

设置设置模型预测所需参数

def set_args():
    """设置模型预测所需参数"""
    parser = argparse.ArgumentParser()
    parser.add_argument('--device', default='0', type=str, help='设备编号')
    parser.add_argument('--vocab_path', default="work/bert-paddle/vocab.txt", type=str, help='模型字典文件路径')
    parser.add_argument('--test_path', default="work/data/test.json", type=str, help='测试集文件路径')
    parser.add_argument('--model_path', default="work/output_dir/checkpoint", type=str, help='模型路径')
    parser.add_argument('--onnx_model_path', default="work/output_dir/checkpoint_onnx/model", type=str, help='onnx模型路径')
    parser.add_argument('--static_model_path', default="work/output_dir/checkpoint_static/model", type=str, help='静态图模型路径')
    parser.add_argument('--max_len', type=int, default=256, help='模型输入最大长度')
    parser.add_argument('--num_labels', type=int, default=6, help='标签个数')
    parser.add_argument('--use_mkldnn', type=bool, default=True, help='是否使用mkldnn')
    return parser.parse_args(args=[])


args = set_args()

加载动态图模型

# 设置显卡信息
os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID"
os.environ["CUDA_VISIBLE_DEVICES"] = args.device
# 获取device信息,用于模型训练
device = "gpu:{}".format(args.device) if paddle.fluid.is_compiled_with_cuda() and int(args.device) >= 0 else "cpu"
paddle.device.set_device(device)
# 加载已保存模型,进行模型初始化
model = SentimentAnalysisModel.from_pretrained(args.model_path, number_label=args.num_labels)
# 实例化tokenizer
tokenizer = BertTokenizer(args.vocab_path, do_lower_case=True)
model.to(device)
id2label = {0: "angry", 1: "happy", 2: "neutral", 3: "surprise", 4: "sad", 5: "fear"}

使用动态图进行单条预测

sample_list = ["妈妈说想和我聊天,她一定是有难过的事了。。。我要上课,所以我好难过。。"]
result = predict_one_sample(sample_list, model, tokenizer, args.max_len, id2label)
# 打印每个样本的结果
for sample, label in result:
    print("label: {}, text: {}".format(label, sample))
label: sad, text: 妈妈说想和我聊天,她一定是有难过的事了。。。我要上课,所以我好难过。。

使用动态图测试1000条样本,记录时间

# 计时,记录开始时间
T1 = time.time()
for i in range(1000):
    sample_list = ["妈妈说想和我聊天,她一定是有难过的事了。。。我要上课,所以我好难过。。"]
    result = predict_one_sample(sample_list, model, tokenizer, args.max_len, id2label)
# 计时,记录开始时间
T2 = time.time()
print("paddle模型,1000次的运行时间为{}秒".format(T2 - T1))
paddle模型,1000次的运行时间为29.458105087280273秒

(3)对onnx模型进行测试

将动态图转成onnx模型

def save_onnx_model(args):
    """将paddle模型转成onnx模型"""
    # 加载已保存模型,并进行参数初始化
    model = SentimentAnalysisModel.from_pretrained(args.model_path, number_label=args.num_labels)
    model.eval()
    # 定义输入节点input_ids和attention_mask
    input_ids = paddle.static.InputSpec([None, args.max_len], "int64", "input_ids")
    attention_mask = paddle.static.InputSpec([None, args.max_len], "int64", "attention_mask")
    # 使用paddle.onnx.export函数将模型转换成onnx模型,并保持
    paddle.onnx.export(model, args.onnx_model_path, input_spec=[input_ids, attention_mask], opset_version=12)
    # 检测onnx模型是否可用加载
    onnx_model = onnx.load(args.onnx_model_path + ".onnx")
    onnx.checker.check_model(onnx_model)


save_onnx_model(args)
2021-09-22 21:31:38 [INFO]	ONNX model saved in work/output_dir/checkpoint_onnx/model.onnx

加载onnx模型

# 设置显卡信息
os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID"
os.environ["CUDA_VISIBLE_DEVICES"] = args.device
# 实例化tokenizer
tokenizer = BertTokenizer(args.vocab_path, do_lower_case=True)
id2label = {0: "angry", 1: "happy", 2: "neutral", 3: "surprise", 4: "sad", 5: "fear"}
# 加载onnx模型
ort_sess = onnxruntime.InferenceSession(args.onnx_model_path + ".onnx")

使用onnx模型进行单条预测

sample_list = ["妈妈说想和我聊天,她一定是有难过的事了。。。我要上课,所以我好难过。。"]
batch = batch_data(sample_list, args.max_len, tokenizer)
input_ids = batch["input_ids"]
input_ids = input_ids.numpy()
attention_mask = batch["attention_mask"]
attention_mask = attention_mask.numpy()
# 构建onnx所需的feed_dict
ort_inputs = {ort_sess.get_inputs()[0].name: input_ids, ort_sess.get_inputs()[1].name: attention_mask}
# 模型预测
pred_label = ort_sess.run(None, ort_inputs)[0]
# 标签转换
label_name = [id2label[pred] for pred in pred_label]
# 打印每个样本的结果
for sample, label in zip(sample_list, label_name):
    print("label: {}, text: {}".format(label, sample))
label: sad, text: 妈妈说想和我聊天,她一定是有难过的事了。。。我要上课,所以我好难过。。

使用onnx模型测试1000条样本,记录时间

# 计时,记录开始时间
T1 = time.time()
for i in range(1000):
    sample_list = ["妈妈说想和我聊天,她一定是有难过的事了。。。我要上课,所以我好难过。。"]
    batch = batch_data(sample_list, args.max_len, tokenizer)
    input_ids = batch["input_ids"]
    input_ids = input_ids.numpy()
    attention_mask = batch["attention_mask"]
    attention_mask = attention_mask.numpy()
    # 构建onnx所需的feed_dict
    ort_inputs = {ort_sess.get_inputs()[0].name: input_ids, ort_sess.get_inputs()[1].name: attention_mask}
    # 模型预测
    pred_label = ort_sess.run(None, ort_inputs)[0]
# 计时,记录开始时间
T2 = time.time()
print("onnx模型,1000次的运行时间为{}秒".format(T2 - T1))
onnx模型,1000次的运行时间为10.867679834365845秒

(4)对静态图模型进行测试

将动态图转成静态图

def save_static_model(args):
    """将paddle动态图转成静态图"""
    # 加载已保存模型,并进行参数初始化
    model = SentimentAnalysisModel.from_pretrained(args.model_path, number_label=args.num_labels)
    model.eval()
    # 定义输入节点input_ids和attention_mask
    input_ids = paddle.static.InputSpec(shape=[None, args.max_len], dtype='int64', name='input_ids')
    attention_mask = paddle.static.InputSpec(shape=[None, args.max_len], dtype='int64', name='attention_mask')
    # 使用paddle.jit.to_static函数,将动态图转成静态图
    model = paddle.jit.to_static(model, input_spec=[input_ids, attention_mask])
    # 使用静态图进行模型预测
    sample_list = ["妈妈说想和我聊天,她一定是有难过的事了。。。我要上课,所以我好难过。。"]
    tokenizer = BertTokenizer(args.vocab_path, do_lower_case=True)
    batch = batch_data(sample_list, args.max_len, tokenizer)
    input_ids = batch["input_ids"]
    attention_mask = batch["attention_mask"]
    outputs = model(input_ids, attention_mask)
    # 对静态进行保存
    paddle.jit.save(layer=model, path=args.static_model_path, input_spec=[input_ids, attention_mask])


save_static_model(args)

加载静态图模型

# 设置显卡信息
os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID"
os.environ["CUDA_VISIBLE_DEVICES"] = args.device
device = "gpu:{}".format(args.device) if paddle.fluid.is_compiled_with_cuda() and int(args.device) >= 0 else "cpu"
paddle.device.set_device(device)
if "gpu" in device:
    use_gpu = True
else:
    use_gpu = False
# 使用InferenceModel进行模型封装
model = InferenceModel(modelpath=args.static_model_path, use_gpu=use_gpu, use_mkldnn=args.use_mkldnn)
model.eval()
# 实例化tokenizer
tokenizer = BertTokenizer(args.vocab_path, do_lower_case=True)
id2label = {0: "angry", 1: "happy", 2: "neutral", 3: "surprise", 4: "sad", 5: "fear"}

使用静态图模型进行单条预测

sample_list = ["妈妈说想和我聊天,她一定是有难过的事了。。。我要上课,所以我好难过。。"]
batch = batch_data(sample_list, args.max_len, tokenizer)
input_ids = batch["input_ids"]
attention_mask = batch["attention_mask"]
pred_label = model(input_ids, attention_mask)[0]
label_name = [id2label[pred] for pred in pred_label]
# 打印每个样本的结果
for sample, label in zip(sample_list, label_name):
    print("label: {}, text: {}".format(label, sample))
label: sad, text: 妈妈说想和我聊天,她一定是有难过的事了。。。我要上课,所以我好难过。。

使用静态图模型测试1000条样本,记录时间

# 计时,记录开始时间
T1 = time.time()
for i in range(1000):
    sample_list = ["妈妈说想和我聊天,她一定是有难过的事了。。。我要上课,所以我好难过。。"]
    batch = batch_data(sample_list, args.max_len, tokenizer)
    input_ids = batch["input_ids"]
    attention_mask = batch["attention_mask"]
    pred_label = model(input_ids, attention_mask)[0]
# 计时,记录开始时间
T2 = time.time()
bel_name = [id2label[pred] for pred in pred_label]
# 打印每个样本的结果
for sample, label in zip(sample_list, label_name):
    print("label: {}, text: {}".format(label, sample))
label: sad, text: 妈妈说想和我聊天,她一定是有难过的事了。。。我要上课,所以我好难过。。

使用静态图模型测试1000条样本,记录时间

# 计时,记录开始时间
T1 = time.time()
for i in range(1000):
    sample_list = ["妈妈说想和我聊天,她一定是有难过的事了。。。我要上课,所以我好难过。。"]
    batch = batch_data(sample_list, args.max_len, tokenizer)
    input_ids = batch["input_ids"]
    attention_mask = batch["attention_mask"]
    pred_label = model(input_ids, attention_mask)[0]
# 计时,记录开始时间
T2 = time.time()
print("静态图模型,1000次的运行时间为{}秒".format(T2 - T1))
静态图模型,1000次的运行时间为7.70054292678833秒

动态图运行1000次耗时29.46秒,onnx运行1000次耗时10.87秒,静态图运行1000次耗时7.70秒。

可以看出,动态图最慢、静态图最快。其实这里有些超出我的认知,我一直觉得onnx的最快的。不知道是不是跟onnx的版本有关。不过动态图转onnx还是有很多坑的,目前paddlepaddle有很多操作转onnx会报错,所以还是转静态图吧。

九、终端运行代码

(1)切换到work目录

cd work

(2)数据预处理

python data_helper.py

(3)模型训练

python train.py

(4)模型测试

python predict.py

十、联系作者

e-mail:logcongcong@gmail.com

知乎:刘聪NLP

公众号:NLP工作站

手把手教你基于PaddlePaddle的情绪识别

  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值