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

  • 更多实践案例(AI识虫,基于PaddleX实现森林火灾监测,眼疾识别,智能相册分类等)、深度学习资料,请参考:awesome-DeepLearning
  • 更多学习资料请参阅飞桨深度学习平台

一、项目描述

  • 本项目为基于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 jsondef 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"]] = 1else: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 logginglogger = 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 = tokenizerself.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_idssample["attention_mask"] = attention_masksample["label"] = label# 将数据存放到data_set中。data_set.append(sample)return data_setdef convert_featrue(self, sample):"""将单个样本转换成模型可用的id索引形式Args:sample: 单条样本Returns:"""# 获取标签索引label = self.label2id[sample["label"]]# 将本文进行tokenizetokens = 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转化成idinput_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, labeldef __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内的数据,进行paddingreturn {"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 Fclass 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 = bertself.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_outputpooled_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求解lossif label is not None:loss = self.loss_fct(logits, label)outputs = (loss,) + outputsreturn outputs

七、模型训练

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

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

导入所需的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 SummaryWriterlogging.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.0true_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以及f1f1_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.0acc = 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_settrain_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_settest_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_epochslogger.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.0global_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,则进行模型测试,记录测试集的损失、准确率以及F1if 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_f1output_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 BertTokenizerdef 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_maskinput_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_maskdef 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: 样本序列,为一个listmodel: 模型tokenizer: 分词器max_len: 最大长度id2label: 标签字典Returns:"""# 将数据转换成模型可使用的tensor形式batch = batch_data(sample_list, max_len, tokenizer)# 关掉模型的dropoutmodel.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_maskinput_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_dictort_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_maskinput_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的情绪识别

手把手教你基于PaddlePaddle的情绪识别相关推荐

  1. 实战七:手把手教你用TensorFlow进行验证码识别(上)

    实战七:手把手教你用TensorFlow进行验证码识别(上) github下载地址 目录 准备模型开发环境 生成验证码数据集 输入与输出数据处理 模型结构设计 模型损失函数设计 模型训练过程分析 模型 ...

  2. python人脸识别门禁系统毕设_开源|手把手教你用Python进行人脸识别(附源代码)...

    原标题:开源|手把手教你用Python进行人脸识别(附源代码) 全球人工智能 来源:Github 翻译:黄玮 想要了解目前世界上最简洁的人脸识别库吗?现在小编带大家来学习使用Python语言或命令行进 ...

  3. 手把手教你:岩石样本智能识别系统

    系列文章 第十一章.手把手教你:基于TensorFlow的语音识别系统 第十章.手把手教你:基于Django的用户画像可视化系统 第九章.手把手教你:个人信贷违约预测模型 目录 系列文章 一.项目简介 ...

  4. 基于脑电图的情绪识别BCI应用于DOC患者

    点击上面"脑机接口社区"关注我们 更多技术干货第一时间送达 ‍‍‍ 基于脑电图(EEG)信号的人类情感识别已引起广泛关注.现有的大多数研究都集中在离线分析上,使用脑计算机接口(BC ...

  5. ❀数据集❀基于计算机视觉的情绪识别数据集

    数据集介绍以及下载链接,免费!!! 1.FER2013 Fer2013 包含大约 30,000 张不同表情的面部 RGB 图像,尺寸限制为 48×48,其主要标签可分为 7 种类型:0=愤怒,1=厌恶 ...

  6. 基于面部表情的情绪识别-论文学习

    论文题目:<Emotion recognition using facial expressions> 实验方法 文章使用Kinect采集了6名受试者(25-60岁)的数据,每个受试者距离 ...

  7. 基于语音的情绪识别系统(Python)

    代码简介 源代码连接 emotion-recognition-using-speech git 上下载比较慢,我已经克隆到我的码云上. 原链接是给的原始的版本,我在 win10 上运行会报错,比如下边 ...

  8. paddle基于bert的情绪识别

    文件结构 * bert-paddle 存放预训练模型路径* vocab.txt 字典文件,该字典为大小为21128.* model_config.json 模型配置文件.* model_state.p ...

  9. python 人脸识别_手把手教你用python实现人脸识别,识别率高达99.38%

    之前本人在实训时需要实现人脸识别这个功能,当时是借助百度的人脸识别api,这个需要注册账号,还需要用到密钥.操作起来也不麻烦,代码也不多.就是如果网速跟不上,返回的结果时,速度有点慢.当时也没那么在意 ...

  10. 深度神经网络对基于EEG的情绪识别的关键频带和通道的研究

    目录 情感识别介绍: 基于脑电的情感识别过程 论文实验设计 探究 情感识别介绍: 对于情感的研究来说,它是一个跨学科的领域,涉及着计算机科学,心理学,认知科学以及神经学等,每一领域的研究成果都在为情感 ...

最新文章

  1. sqlmap 跑access_sqlmap注入Access
  2. Java 基本功之(三)Java 核心技术
  3. 11组软件工程组队项目失物招领系统——进度汇报和下周目标
  4. C语言指针实现计算平均分等功能
  5. java1a2b3c4d5e6f_用两个线程,一个输出字母,一个输出数字,交替输出1A2B3C4D...26Z...
  6. 超好看的引导购买页源码
  7. UI素材|让设计有愉悦的体验,app交互动效的重要性!
  8. 2.9_double_link_list_双链表
  9. Jupyter Notebook——Windows平台上中如何切换虚拟环境
  10. 第一章 什么是数组名?
  11. JavaSE基础 ——流程控制语句
  12. 微信小程序之----加载中提示框loading
  13. Java数据库的介绍和使用
  14. 2019保定中考计算机时间安排,2019年保定中考考试时间安排,保定中考考试科目时间安排表...
  15. Python数据分析与应用 ---- 航空公司客户价值分析
  16. Linux服务器监控性能测试
  17. 原来收汇宝真的是很烂
  18. win10下载文件夹变成英文了该怎么办?下载文件夹变成英文的修复方法
  19. canvas下雪效果(原生js)
  20. ArcGIS教程:填挖的工作原理

热门文章

  1. kettle-java代码执行hive相关ktr时报错: database type with plugin id [HIVE2] couldn‘t be found!
  2. [每日100问][2011-10-06]iphone开发笔记,今天你肿了么
  3. 道格拉斯普克算法(简化线段点)
  4. 2019-06-12-pintos 实验1
  5. jsr、jcp和harmony的介绍
  6. 【组合数学】 卢卡斯定理详解(证明+模板)
  7. 127.0.0.1和localhost和本机IP三者的区别!!!
  8. itunes下载的软件所在目录
  9. GreemPlum6.7.1 Centos7部署文档
  10. QIIME 2教程. 05粪菌移植分析练习Fecal microbiota transplant(2021.2)