Task06——PyTorch生态以及模型部署

Pytorch生态

PyTorch的强大并不仅局限于自身的易用性,更在于开源社区围绕PyTorch所产生的一系列工具包(一般是Python package)和程序,这些优秀的工具包极大地方便了PyTorch在特定领域的使用。比如对于计算机视觉,有TorchVision、TorchVideo等用于图片和视频处理;对于自然语言处理,有torchtext;对于图卷积网络,有PyTorch Geometric ······。这里只是举例,每个领域还有很多优秀的工具包供社区使用。这些工具包共同构成了PyTorch的生态(EcoSystem)。

1.torchvision

torchvision 软件包由流行的数据集、模型架构和计算机视觉的常见图像转换组成。

  • torchvision.datasets(获取数据及其对应的label)torchvision.datasets主要包含了一些我们在计算机视觉中常见的数据集:Caltech,CelebA,CIFAR,Cityscapes,EMNIST,FakeData,Fashion-MNIST……等等,以Fashion-MNIST为例:
from torchvision import datasets

train_data = datasets.FashionMNIST(root='./data/',train=True, transform=data_transform, download=True)
test_data = datasets.FashionMNIST(root='./data/', train=False, transform=data_transform, download=True)
  • torchvision.transforms(对现有图片数据进行各种处理,比如缩小,放大,翻转等)部分transforms操作:
    • transforms.ToPILImage()是转换数据格式,把数据转换为tensfroms格式。只有转换为tensfroms格式才能进行后面的处理。
    • transforms.Resize(256)是按照比例把图像最小的一个边长放缩到256,另一边按照相同比例放缩。
    • transforms.RandomResizedCrop(224,scale=(0.5,1.0))是把图像按照中心随机切割成224正方形大小的图片。
    • transforms.ToTensor() 转换为tensor格式,这个格式可以直接输入进神经网络了。
    • transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])对像素值进行归一化处理。
    • 代码示例:
from torchvision import transforms
data_transform = transforms.Compose([
      transforms.ToPILImage(),   # 这一步取决于后续的数据读取方式,如果使用内置数据集则不需要
      transforms.Resize(image_size),
      transforms.ToTensor()
      ])
  • torchvision.models(提供一些预训练好的模型)
    • 图像分类模型:AlexNet,VGG,ResNet,SqueezeNet,DenseNet,Inception v3,GoogLeNet……等等
from torchvision import models
net = models.AlexNet()
net

 AlexNet(
(features): Sequential(
(0): Conv2d(3, 64, kernel_size=(11, 11), stride=(4, 4), padding=(2, 2))
(1): ReLU(inplace=True)
(2): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
(3): Conv2d(64, 192, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))
(4): ReLU(inplace=True)
(5): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
(6): Conv2d(192, 384, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(7): ReLU(inplace=True)
(8): Conv2d(384, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(9): ReLU(inplace=True)
...
  • 语义分割模型:(语义分割的预训练模型是在COCO train2017的子集上进行训练的,提供了20个类别,包括background, aeroplane, bicycle, bird, boat, bottle, bus, car, cat, chair, cow, diningtable, dog, horse, motorbike, person, pottedplant, sheep, sofa,train, tvmonitor)
    • FCN ResNet50,FCN ResNet101,DeepLabV3 ResNet50,DeepLabV3 ResNet101,LR-ASPP MobileNetV3-Large,DeepLabV3 MobileNetV3-Large等
  • torchvision.io(提供了视频、图片和文件的 IO 操作的功能,它们包括读取、写入、编解码处理操作)
  • torchvision.ops(提供了许多计算机视觉的特定操作,包括但不仅限于NMS,RoIAlign(MASK R-CNN中应用的一种方法),RoIPool(Fast R-CNN中用到的一种方法))
  • torchvision.utils(提供了一些可视化的方法,可以帮助我们将若干张图片拼接在一起、可视化检测和分割的效果。)

2.PyTorchVideo

有关视频的深度学习模型存在的缺点:

  • 计算资源耗费更多,并且没有高质量的model zoo,不能像图片一样进行迁移学习和论文复现。
  • 数据集处理较麻烦,但没有一个很好的视频处理工具。
  • 随着多模态越来越流行,亟需一个工具来处理其他模态。

为了解决这些问题,Meta推出了PyTorchVideo深度学习库。(如图所示)PyTorchVideo 是使用PyTorch开发的,支持不同的深度学习视频组件,如视频模型、视频数据集和视频特定转换。

Model zoo 和 benchmark

  • Kinetics-400
  • Something-Something V2
  • Charades
  • AVA (V2.2)

使用 PyTorchVideo model zoo

PyTorchVideo提供了三种使用方法,并且给每一种都配备了tutorial

  • TorchHub,这些模型都已经在TorchHub存在。我们可以根据实际情况来选择需不需要使用预训练模型。除此之外,官方也给出了TorchHub使用的 tutorial 。
  • PySlowFast,使用 PySlowFast workflow 去训练或测试PyTorchVideo models/datasets.
  • PyTorch Lightning建立一个工作流进行处理,点击查看官方 tutorial。

3.torchtext

(PyTorch官方用于自然语言处理(NLP)的工具包torchtext)

NLP相比于CV(计算机视觉)的相关工具包的差异:

  • 数据集(dataset)定义方式不同
  • 数据预处理工具
  • 没有琳琅满目的model zoo

torchtext主要组成部分

  • 数据处理工具 torchtext.data.functional、torchtext.data.utils
  • 数据集 torchtext.data.datasets
  • 词表工具 torchtext.vocab
  • 评测指标 torchtext.metrics

构建数据集

  • Field及其使用

Field是torchtext中定义数据类型以及转换为张量的指令。

  tokenize = lambda x: x.split()  #将英文句子单词化。
  TEXT = data.Field(sequential=True, tokenize=tokenize, lower=True, fix_length=200)
  LABEL = data.Field(sequential=False, use_vocab=False)

其中:

sequential设置数据是否是顺序表示的;

​ tokenize用于设置将字符串标记为顺序实例的函数

​ lower设置是否将字符串全部转为小写;

​ fix_length设置此字段所有实例都将填充到一个固定的长度,方便后续处理;

​ use_vocab设置是否引入Vocab object,如果为False,则需要保证之后输入field中的data都是numerical的

  • 构建dataset
from torchtext import data
def get_dataset(csv_data, text_field, label_field, test=False):
      fields = [("id", None), 
           ("comment_text", text_field), ("toxic", label_field)]       
examples = []

if test:
  # 如果为测试集,则不加载label
  for text in tqdm(csv_data['comment_text']):
      examples.append(data.Example.fromlist([None, text, None], fields))
else:
  for text, label in tqdm(zip(csv_data['comment_text'], csv_data['toxic'])):
      examples.append(data.Example.fromlist([None, text, label], fields))
      return examples, fields

comment_text"和"toxic"两列,分别对应text和label。

定义Field对象完成后,通过get_dataset函数可以读入数据的文本和标签,将二者(examples)连同field一起送到torchtext.data.Dataset类中,即可完成数据集的构建:

  train_data = pd.read_csv('train_toxic_comments.csv')
  valid_data = pd.read_csv('valid_toxic_comments.csv')
  test_data = pd.read_csv("test_toxic_comments.csv")
  TEXT = data.Field(sequential=True, tokenize=tokenize, lower=True)
  LABEL = data.Field(sequential=False, use_vocab=False)

  # 得到构建Dataset所需的examples和fields
  train_examples, train_fields = get_dataset(train_data, TEXT, LABEL)
  valid_examples, valid_fields = get_dataset(valid_data, TEXT, LABEL)
  test_examples, test_fields = get_dataset(test_data, TEXT, None, test=True)
  # 构建Dataset数据集
  train = data.Dataset(train_examples, train_fields)
  valid = data.Dataset(valid_examples, valid_fields)
  test = data.Dataset(test_examples, test_fields)

使用以下命令可以看下读入的数据情况:

  # 检查keys是否正确
  print(train[0].__dict__.keys())
  print(test[0].__dict__.keys())
  # 抽查内容是否正确
  print(train[0].comment_text)
  • 词汇表(vocab)

在NLP中,将字符串形式的词语(word)转变为数字形式的向量表示(embedding)是非常重要的一步,被称为Word Embedding。

在torchtext中可以使用Field自带的build_vocab函数完成词汇表构建:

  TEXT.build_vocab(train)
  • 数据迭代器

torchtext中的DataLoader,torchtext支持只对一个dataset和同时对多个dataset构建数据迭代器。

  from torchtext.data import Iterator, BucketIterator
  # 若只针对训练集构造迭代器
  # train_iter = data.BucketIterator(dataset=train, batch_size=8, shuffle=True, sort_within_batch=False, repeat=False)

  # 同时对训练集和验证集进行迭代器的构建
  train_iter, val_iter = BucketIterator.splits(
        (train, valid), # 构建数据集所需的数据集
        batch_sizes=(8, 8),
        device=-1, # 如果使用gpu,此处将-1更换为GPU的编号
        sort_key=lambda x: len(x.comment_text), # the BucketIterator needs to be told what function it should use to group the data.
        sort_within_batch=False
        )

  test_iter = Iterator(test, batch_size=8, device=-1, sort=False, sort_within_batch=False)
  • 使用自带数据集

与torchvision类似,torchtext也提供若干常用的数据集方便快速进行算法测试。具体参考:https://pytorch.org/text/stable/datasets.html

  • 评测指标(metric)

NLP中部分任务的评测不是通过准确率等指标完成的,比如机器翻译任务常用BLEU (bilingual evaluation understudy) score来评价预测文本和标签文本之间的相似程度。torchtext.data.metrics.bleu_score来快速实现BLEU:

  from torchtext.data.metrics import bleu_score
  candidate_corpus = [['My', 'full', 'pytorch', 'test'], ['Another', 'Sentence']]
  references_corpus = [[['My', 'full', 'pytorch', 'test'], ['Completely', 'Different']], [['No', 'Match']]]
  bleu_score(candidate_corpus, references_corpus)

  0.8408964276313782
  • torchtext模型主要通过torch.nn来实现。比如torch.nn.LSTM、torch.nn.RNN等。

部分transforms实战代码

具体参考:https://datawhalechina.github.io/thorough-pytorch/第八章/transforms实操.html

Pytorch模型部署

深度学习的最终目的是要实现模型的部署以方便我们的生活和解决传统方法不能解决的问题。通常人们会将模型部署在手机端、开发板,嵌入式设备上,但是这些设备上由于框架的规模,环境依赖,算力的限制,我们无法直接使用训练好的权重进行推理,因此我们需要将得到的权重进行变换才能使我们的模型可以成功部署在上述设备上。模型部署pipeline:

ONNX(Open Neural Network Exchange)和ONNX Runtime

  • ONNX

ONNX通过定义一组与环境和平台无关的标准格式,使AI模型可以在不同框架和环境下交互使用,ONNX可以看作深度学习框架和部署端的桥梁,就像编译器的中间语言一样。由于各框架兼容性不一,我们通常只用 ONNX 表示更容易部署的静态图。目前,ONNX主要关注在模型预测方面,使用不同框架训练的模型,转化为ONNX格式后,可以很容易的部署在兼容ONNX的运行环境中。ONNX 已经对接了下图的多种深度学习框架和多种推理引擎。

  • ONNX Runtime

ONNX Runtime 直接对接ONNX,可以直接读取.onnx文件并实现推理,不需要再把 .onnx 格式的文件转换成其他格式的文件。PyTorch借助ONNX Runtime也完成了部署的最后一公里,构建了 PyTorch --> ONNX --> ONNX Runtime 部署流水线,我们只需要将模型转换为 .onnx 文件,并在 ONNX Runtime 上运行模型即可。

模型导出为ONNX

  • 模型转换为ONNX格式使用torch.onnx.export()把模型转换成 ONNX 格式的函数,必须调用model.eval()或者model.train(False)以确保我们的模型处在推理模式下,避免因为dropout或batchnorm等运算符在推理和训练模式下的不同产生错误。代码示例:
import torch.onnx 
# 转换的onnx格式的名称,文件后缀需为.onnx
onnx_file_name = "xxxxxx.onnx"
# 我们需要转换的模型,将torch_model设置为自己的模型
model = torch_model
# 加载权重,将model.pth转换为自己的模型权重
# 如果模型的权重是使用多卡训练出来,我们需要去除权重中多的module. 具体操作可以见5.4节
model = model.load_state_dict(torch.load("model.pth"))
# 导出模型前,必须调用model.eval()或者model.train(False)
model.eval()
# dummy_input就是一个输入的实例,仅提供输入shape、type等信息 
batch_size = 1 # 随机的取值,当设置dynamic_axes后影响不大
dummy_input = torch.randn(batch_size, 1, 224, 224, requires_grad=True) 
# 这组输入对应的模型输出
output = model(dummy_input)
# 导出模型
torch.onnx.export(model,        # 模型的名称
            dummy_input,   # 一组实例化输入
            onnx_file_name,   # 文件保存路径/名称
            export_params=True,        #  如果指定为True或默认, 参数也会被导出. 如果你要导出一个没训练过的就设为 False.
            opset_version=10,          # ONNX 算子集的版本,当前已更新到15
            do_constant_folding=True,  # 是否执行常量折叠优化
            input_names = ['input'],   # 输入模型的张量的名称
            output_names = ['output'], # 输出模型的张量的名称
            # dynamic_axes将batch_size的维度指定为动态,
            # 后续进行推理的数据可以与导出的dummy_input的batch_size不同
            dynamic_axes={'input' : {0 : 'batch_size'},    
                          'output' : {0 : 'batch_size'}})
  • ONNX模型的检验

得到一个ONNX 模型文件之后,我们需要检测下我们的模型文件是否可用,我们将通过onnx.checker.check_model()进行检验,具体方法如下:

  import onnx
  # 我们可以使用异常处理的方法进行检验
  try:
        # 当我们的模型不可用时,将会报出异常
        onnx.checker.check_model(self.onnx_model)
  except onnx.checker.ValidationError as e:
        print("The model is invalid: %s"%e)
  else:
        # 模型可用时,将不会报出异常,并会输出“The model is valid!”
        print("The model is valid!")
  • ONNX可视化

随着Netron(类似于tensorboard的出现,我们也可以实现onnx的可视化。 下载地址:https://github.com/lutzroeder/netron

  • 使用ONNX Runtime进行推理

使用ONNX Runtime运行一下转化后的模型,看一下推理后的结果。

  # 导入onnxruntime
  import onnxruntime
  # 需要进行推理的onnx模型文件名称
  onnx_file_name = "xxxxxx.onnx"

  # onnxruntime.InferenceSession用于获取一个 ONNX Runtime 推理器
  ort_session = onnxruntime.InferenceSession(onnx_file_name)  

  # 构建字典的输入数据,字典的key需要与我们构建onnx模型时的input_names相同
  # 输入的input_img 也需要改变为ndarray格式
  ort_inputs = {'input': input_img} 
  # 我们更建议使用下面这种方法,因为避免了手动输入key
  # ort_inputs = {ort_session.get_inputs()[0].name:input_img}

  # run是进行模型的推理,第一个参数为输出张量名的列表,一般情况可以设置为None
  # 第二个参数为构建的输入值的字典
  # 由于返回的结果被列表嵌套,因此我们需要进行[0]的索引
  ort_output = ort_session.run(None,ort_inputs)[0]
  # output = {ort_session.get_outputs()[0].name}
  # ort_output = ort_session.run([output], ort_inputs)[0]

注意:

1.ONNX的输入为array

2.输入的array的shape应该和我们导出模型的dummy_input的shape相同

3.run的结果是一个列表,需要索引得到array

4.在构建输入的字典时,我们需要注意字典的key应与导出ONNX格式设置的input_name相同

浙ICP备19012682号