动态图和静态图介绍
目前市面上比较流行的深度学习框架主要分类两大类:动态图框架和静态图框架,Pytorch、TF、Caffeine等框架最大的区别就是它们拥有不同的计算图表现形式。TF使用静态图,意味着开发人员需要先定义计算图,然后不断的使用它,而Pytorch,每次都会重新构建一个新的计算图。
动态计算意味着程序会按照研发人员编写命令的顺序进行执行。这种机制将使得调试更加容易,并且也使得我们将大脑中的想法转化为实际代码变得更加容易。而静态计算则意味着程序程序在编译执行时将先生成神经网络的结构,然后再执行相应操作。而静态计算是通过先定义后执行的方式,之后再次运行的时候就不再需要重新构建计算图,所以速度会比动态图更快。从理论上讲,静态计算这样的机制允许编译器进行更大程度的优化,但是这也意味着你所期望的程序与编译器实际执行之间存在着更多的代沟。这也意味着,代码中的错误将更加难以发现(比如,如果计算图的结构出现问题,你可能只有在代码执行到相应操作的时候才能发现它)。
tensorflow静态图
import tensorflow as tf first_counter = tf.constant(0) second_counter = tf.constant(10) def cond(first_counter, second_counter, *args): return first_counter < second_counter def body(first_counter, second_counter): first_counter = tf.add(first_counter, 2) second_counter = tf.add(second_counter, 1) return first_counter, second_counter c1, c2 = tf.while_loop(cond, body, [first_counter, second_counter]) with tf.Session() as sess: counter_1_res, counter_2_res = sess.run([c1, c2]) print(counter_1_res) print(counter_2_res)
可以看到 TensorFlow 需要将整个图构建成静态的,换句话说,每次运行的时候图都是一样的,是不能够改变的,所以不能直接使用 Python 的 while 循环语句,需要使用辅助函数 tf.while_loop
写成 TensorFlow 内部的形式.
pytorch动态图
import torch first_counter = torch.Tensor([0]) second_counter = torch.Tensor([10]) while (first_counter < second_counter)[0]: first_counter += 2 second_counter += 1 print(first_counter) print(second_counter)
可以看到 PyTorch 的写法跟 Python 的写法是完全一致的,没有任何额外的学习成本.
总结
动态图方便代码编写,方便debug,但是缺点就是速度慢(每一次运算都会加载一遍计算图)。在工业上,不但要看效果,还要看速度。因此将模型加速是必不可少的步骤。
动态图加速
本次以一个paddlepaddle训练的情感分类模型,展示如何进行动态图加速。情感分类模型是基于paddlenlp框架,利用Bert模型,进行的情感多分类模型。本文不包含训练部分,只叙述模型加速部分。
动态图预测
# -*- coding: utf-8 -*- # @Time : 2021/10/23 23:22 # @Author : RJ 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 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 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")} 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('--model_path', default="work/output_dir/checkpoint", type=str, 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) # 加载已保存模型,进行模型初始化 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次的运行时间为27.153663396835327秒
动态图转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)
加载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.31327486038208秒
动态图转静态图预测
动态图转静态图
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"}
使用静态图模型测试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.70秒
总结
动态图运行1000次耗时29.46秒,onnx运行1000次耗时10.87秒,静态图运行1000次耗时7.70秒。 可以看出,动态图最慢、静态图最快。
参考文章
机器学习中常见的动态图和静态图_xiaomu_347的博客-CSDN博客_动态图和静态图
PaddlePaddle的静态图与动态图_梁小憨憨的博客-CSDN博客
深度学习框架 の 动态图 vs 静态图 - 简书
初探PaddlePaddle的静态图与动态图_下大禹了的博客-CSDN博客