NVIDIA TensorRT—推理引擎加速深度学习推理

模型转换工具: https://convertmodel.com/

深度学习的工作流程,如下图所示,可分为训练和推理两个部分。

训练过程通过设定数据处理方式,并设计合适的网络模型结构以及损失函数和优化算法,在此基础上将数据集以小批量的方式(mini-batch)反复进行前向计算并计算损失,然后 反向计算梯度利用特定的优化函数来更新模型,来使得损失函数达到最优的结果。训练过程最重要的就是梯度的计算和反向传播。

而推理就是在训练好的模型结构和参数基础上,做一次前向传播得到模型输出的过程。相对于训练而言,推理不涉及梯度和损失优化。推理的最终目标是将训练好的模型部署生产环境中。

高性能推理引擎的工作项

虽然推理就是数据经过模型的一次前向计算,但是推理是面向不同的终端部署,一般推理需要满足:

  • 精度要求: 推理的精度需要和训练的精度保持一致,
  • 效率要求:性能尽可能的快
  • 异构的推理设备:生产环境因为场景不同,支持不同的设备如TPU,CPU,GPU, NPU等

所以推理框架一般包括模型优化和推理加速,以便于支持高性能的推理要求。

那么一个推理框架要做哪些事情呢?

首先,因为推理框架要支持现有流行的深度学习框架如TensorFlow和Pytorch等,而不同的深度学习框内在的不一致性,就要求推理框架需要有一种同一个表达形式,来统一外部的不一致性,这就需要推理框架外部模型解析和转换为内在形式的功能。

其次,为了追求性能的提升,需要能够对训练好的模型针对特定推理设备进行特定的优化,主要优化可以包括

  • 低精度优化:FP16低精度转换,INT8后训练量化
  • 算子编译优化
  • 内存优化
  • 计算图调度

低精度优化

一般模型训练过程中都是采用FP32或者FP64高精度的方式进行存储模型参数,主要是因为梯度计算更新的可能是很小的一个小数。高精度使得模型更大,并且计算很耗时。而在推理不需要梯度更新,所以通常如果精度从FP32降低到FP16,模型就会变小很多,并且计算量也下降,而相对于模型的推理效果几乎不会有任何的变化,一般都会做FP16的精度裁剪

而FP32如果转换到INT8,推理性能会提高很多,但是裁剪不是直接裁剪,参数变动很多,会影响模型的推理效果,需要做重新的训练,来尽可能保持模型的效果

算子编译优化

我们先来了解下计算图的概念,计算图是由算子和张量构建成一个数据计算流向图,通常深度学习网络都可以看成一个计算图。而推理可以理解成数据从计算图起点到终点的过程。

算子编译优化其中一项优化就是计算图的优化。计算图优化的目标是对计算图进行等价的组合变换,使得减少算子的读写操作提供效率。

最简单的情况,就是算子融合。比如常见Conv+ReLu的两个算子,因为Conv需要做大量卷积计算,需要密集的计算单元支持,而Relu几乎不需要计算,如果Relu算子单独运算,则不仅需要一个计算单元支持其实不需要怎么计算的算子,同时又要对前端的数据进行一次读操作,很浪费资源和增加I/O操作; 此时,可以将Conv和Relu合并融合成一个算子,可以节省I/O访问和带宽开销,也可以节省计算单元。

这种算子融合对于所有推理设备都是支持,是通用的硬件优化。有些是针对特定硬件优化,比如某些硬件的计算单元不支持过大算子输入,此时就需要对算子进行拆解。

计算图的优化可以总结为算子拆解、算子聚合、算子重建,以便达到在硬件设备上更好的性能。

算子编译优化的另一个优化就是数据排布优化。我们知道,在TensorFlow框架的输入格式NHWC,而pytorch是NCHW。这些格式是框架抽象出来的矩阵格式,实际在内存中的存储都是按照1维的形式存储。这就涉及物理存储和逻辑存储之间的映射关系,如何更好的布局数据能带来存储数据的访问是一个优化方向;另外在硬件层面,有些硬件在某种存储下有最佳的性能,通常可以根据硬件的读写特点进行优化。

内存优化

我们推理的时候都需要借助额外的硬件设备来达到高速推理,如GPU,NPU等,此时就需要再CPU和这些硬件设备进行交互;以GPU为例,推理时需要将CPU中的数据copy到GPU显存中,然后进行模型推理,推理完成后的数据是在GPU显存中,此时又需要将GPU显存中的数据copy回cpu中。

这个过程就涉及到存储设备的申请、释放以及内存对齐等操作,而这部分也是比较耗时的。

因此内存优化的方向,通常是减少频繁的设备内存空间的申请和尽量做到内存的复用。

一般的,可以根据张量生命周期来申请空间:

  • 静态内存分配:比如一些固定的算子在整个计算图中都会使用,此时需要再模型初始化时一次性申请完内存空间,在实际推理时不需要频繁申请操作,提高性能
  • 动态内存分配:对于中间临时的内存需求,可以进行临时申请和释放,节省内存使用,提高模型并发能力
  • 内存复用:对于同一类同一个大小的内存形式,又满足临时性,可以复用内存地址,减少内存申请。

计算图调度

在计算图中,存在某些算子是串行依赖,而某些算子是不依赖性;这些相互独立的子计算图,就可以进行并行计算,提高推理速度,这就是计算图的调度。

TensorRT

我们讲解了推理引擎的一般工作流程和优化思路,这一部分介绍一个具体的推理引擎框架:TensorRT。NVIDIA TensorRT 是一个用于深度学习推理的 SDK 。 TensorRT 提供了 API 和解析器,可以从所有主要的深度学习框架中导入经过训练的模型。然后,它生成可在数据中心以及汽车和嵌入式环境中部署的优化运行时引擎。TensorRT是NVIDIA出品的针对深度学习的高性能推理SDK。

目前,TensorRT只支持NVIDIA自家的设备的推理服务,如服务器GPUTesla v100、NVIDIA GeForce系列以及支持边缘的NVIDIA Jetson等。

TensorRT通过将现有深度学习框架如TensorFlow、mxnet、pytorch、caffe2以及theano等训练好的模型进行转换和优化,并生成TensorRT的运行时(Runtime Engine),利用TensorRT提供的推理接口(支持不同前端语言如c++/python等),部署不同的NVIDIA GPU设备上,提供高性能人工智能的服务。

在性能方面,TensorRT在自家的设备上提供了优越的性能:

对于TensorRT而言,主要优化如下:

  • 算子和张量的融合 Layer & Tensor Fusion

以上面Inception模块的计算图为例子,左边是未优化原始的结构图,右边是经过TensorRT优化过的计算图。优化的目标是减少GPU核数的使用,以便于减少GPU核计算需要的数据读写,提高GPU核数的计算效率

  • 首先是合并conv+bias+relu为一个CBR模块,减少2/3 核的使用
  • 然后是对于同一输入1x1conv,合并为一个大的CBR,输出保持不变,减少了2次的相同数据的读写
  • 有没有发现还少了一个concat层,这个是怎么做到的?concat操作可以理解为数据的合并,TensorRT采用预先先申请足够的缓存,直接把需要concat的数据放到相应的位置就可以达到concat的效果。

经过优化,使得整个模型层数更少,占用更少GPU核,运行效率更快。

  • 精度裁剪 Precision Calibration
    这个是所有推理引擎都有部分,TensorRT支持低精度FP16和INT8的模型精度裁剪,在尽量不降低模型性能的情况,通过裁剪精度,降低模型大小,提供推理速度。但需要注意的是:不一定FP16就一定比FP32的要快。这取决于设备的不同精度计算单元的数量,比如在GeForce 1080Ti设备上由于FP16的计算单元要远少于FP32的,裁剪后反而效率降低,而GeForce 2080Ti则相反。
  • Dynamic Tensor Memory: 这属于提高内存利用率
  • Multi-Stream Execution: 这属于内部执行进程控制,支持多路并行执行,提供效率
  • Auto-Tuning 可理解为TensorRT针对NVIDIA GPU核,设计有针对性的GPU核优化模型,如上面所说的算子编译优化。

 TensorRT安装

了解了TensorRT是什么和如何做优化,我们实际操作下TensorRT, 先来看看TensorRT的安装。

TensorRT是针对NVIDIA GPU的推理引擎,所以需要CUDA和cudnn的支持,需要注意版本的对应关系; 以TensorRT 7.1.3.4为例,需要至少CUDA10.2和cudnn 8.x。

本质上 TensorRT的安装包就是动态库文件(CUDA和cudnn也是如此),需要注意的是TensorRT提供的模型转换工具。

下载可参考

rpm -i cuda-repo-rhel7-10-2-local-10.2.89-440.33.01-1.0-1.x86_64.rpm
tar -zxvf cudnn-10.2-linux-x64-v8.0.1.13.tgz
# tar -xzvf TensorRT-${version}.Linux.${arch}-gnu.${cuda}.${cudnn}.tar.gz
tar -xzvf TensorRT-7.1.3.4.CentOS-7.6.x86_64-gnu.cuda-10.2.cudnn8.0.tar.gz

TensorRT也提供了python版本(底层还是c的动态库)

#1.创建虚拟环境 tensorrt
  conda create -n tensorrt python=3.6
  
  #安装其他需要的工具包, 按需包括深度学习框架
  pip install keras,opencv-python,numpy,tensorflow-gpu==1.14,pytorch,torchvision
 
#2. 安装pycuda
  #首先使用nvcc确认cuda版本是否满足要求: nvcc -V
  pip install 'pycuda>=2019.1.1'
       
#3. 安装TensorRT
  # 下载解压的tar包
  tar -xzvf TensorRT-7.1.3.4.CentOS-7.6.x86_64-gnu.cuda-10.2.cudnn8.0.tar.gz
  
  #解压得到 TensorRT-7.1.3.4的文件夹,将里面lib绝对路径添加到环境变量中
  export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/TensorRT-7.1.3.4/lib
  
  #安装TensorRT
  cd TensorRT-7.1.3.4/python
  pip install pip install tensorrt-7.1.3.4-cp36-none-linux_x86_64.whl
 
#4.安装UFF
  cd TensorRT-7.1.3.4/uff
  pip install uff-0.6.9-py2.py3-none-any.whl
 
#5. 安装graphsurgeon
  cd TensorRT-7.1.3.4/graphsurgeon
  pip install uff-0.6.9-py2.py3-none-any.whl
 
#6. 环境测试
  #进入python shell,导入相关包没有报错,则安装成功
  import tensorrt
  import uff

安装完成后,在该路径的samples/python给了很多使用tensorrt的python接口进行推理的例子(图像分类、目标检测等),以及如何使用不同的模型解析接口(uff,onnx,caffe)。

另外给了一个common.py文件,封装了tensorrt如何为engine分配显存,如何进行推理等操作,我们可以直接调用该文件内的相关函数进行tensorrt的推理工作。

TensorRT工作流程

在安装TensorRT之后,如何使用TensorRT呢?我们先来了解下TensorRT的工作流程

总体流程可以拆分成两块:

  • 模型转换
    TensorRT需要将不同训练框架训练出来的模型,转换为TensorRT支持的中间表达(IR),并做计算图的优化等,并序列化生成plan文件。
  • 模型推理:在模型转换好后之后,在推理时,需要加plan文件进行反序列化加载模型,并通过TensorRT运行时进行模型推理,输出结果

模型转换

由于不同的深度学习框架的实现逻辑不同,TensorRT在转换模型时采用不同适配方法。以当前最流行深度学习框架TensorFlow和Pytorch为例为例。

由于pytorch采用动态的计算图,也就是没有图的概念,需要借助ONNX生成静态图。

Open Neural Network Exchange(ONNX,开放神经网络交换)格式,是一个用于表示深度学习模型的标准,可使模型在不同框架之间进行转移.最初的ONNX专注于推理(评估)所需的功能。 ONNX解释计算图的可移植,它使用graph的序列化格式

pth 转换为onnx

import onnx
import torch
def export_onnx(onnx_model_path, model, cuda, height, width, dummy_input=None):
    model.eval()
    if dummy_input is None:
        dummy_input = torch.randn(1, 3, height, width).float()
    dummy_input.requires_grad = True
    print("dummy_input shape: ", dummy_input.shape, dummy_input.requires_grad)

    if cuda:
        dummy_input = dummy_input.cuda()

    torch.onnx.export(
        model,  # model being run
        dummy_input,  # model input (or a tuple for multiple inputs)
        onnx_model_path,  # where to save the model (can be a file or file-like object)
        export_params=True,  # store the trained parameter weights inside the model file
        opset_version=10,  # the ONNX version to export the model to
        do_constant_folding=True,  # whether to execute constant folding for optimization
        verbose=True,
        input_names=['input'],  # the model's input names
        output_names=['output'],  # the model's output names
    )

从上可知,onnx通过pytorch模型完成一次模型输入和输出的过程来遍历整个网络的方式来构建完成的计算图的中间表示。

这里需要注意三个重要的参数:

  • opset_version: 这个是onnx支持的op算子的集合的版本,因为onnx目标是在不同深度学习框架之间做模型转换的中间格式,理论上onnx应该支持其他框架的所有算子,但是实际上onnx支持的算子总是滞后的,所以需要知道那个版本支持什么算子,如果转换存在问题,大部分当前的版本不支持需要转换的算子。
  • input_names:模型的输入,如果是多个输入,用列表的方式表示,如[“input”, “scale”]
  • output_names: 模型的输出, 多个输出,通input_names

onnx转换为plan engine模型

这里给出的通过TensorRT的python接口来完成onnx到plan engine模型的转换。

import tensorrt as trt
def build_engine(onnx_path):
          EXPLICIT_BATCH = 1 << (int)(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)
        with trt.Builder(TRT_LOGGER) as builder, builder.create_network(EXPLICIT_BATCH) as network, trt.OnnxParser(network, TRT_LOGGER) as parser:
            builder.max_batch_size = 128
            builder.max_workspace_size = 1<<15
            builder.fp16_mode = True
            builder.strict_type_constraints = True
            with open(onnx_path, 'rb') as model:
                parser.parse(model.read())
            # Build and return an engine.
            return builder.build_cuda_engine(network)

从上面的转换过程可知,TensortRT的转换涉及到几个关键的概念:builder 、 network 、parser

  • builder:TensorRT构建器,在构建器中设置模型,解析器和推理的参数设置等 trt.Builder(TRT_LOGGER)
  • network: TensorRT能识别的模型结构(计算图)
  • parser:这里是指解析onnx模型结构(计算图)

从总体上看,TensorRT的转换模型是,将onnx的模型结构(以及参数)转换到TensorRT的network中,同时设置模型推理和优化的参数(如精度裁剪等)。 用一张图来总结下上述过程:

保存engine和读取engine

#解析模型,构建engine并保存
with build_engine(onnx_path) as engine:
    with open(engine_path, "wb") as f:
        f.write(engine.serialize())

#直接加载engine   
with open(engine_path, "rb") as f, trt.Runtime(TRT_LOGGER) as runtime:
    engine = runtime.deserialize_cuda_engine(f.read())

TensorFlow / Keras

TensorFlow或者Keras(后台为TensorFlow)采用的是静态的计算图,本身就有图的完整结构,一般模型训练过程会保留ckpt格式,有很多冗余的信息,需要转换为pb格式。针对TensorFlow,TensorRT提供了两种转换方式,一种是pb直接转换,这种方式加速效果有限所以不推荐;另一种是转换uff格式,加速效果明显。

  • 转换为pb
from tensorflow.python.framework import graph_io
from tensorflow.python.framework import graph_util
from tensorflow.python.platform import gfile
# 设置输出节点为固定名称
OUTPUT_NODE_PREFIX = 'output_'
NUMBER_OF_OUTPUTS = 1
#输入和输出节点名称
output_names = ['output_']
input_names = ['input_']
input_tensor_name = input_names[0] + ":0"
output_tensor_name = output_names[0] + ":0"

def keras_to_pb(model_path, pb_path):
    K.clear_session()#可以保持输入输出节点的名称每次执行都一致
    K.set_learning_phase(0)
    sess = K.get_session()
    try:
        model = load_model(model_path)# h5 model file_path
    except ValueError as err:
        print('Please check the input saved model file')
        raise err

    output = [None]*NUMBER_OF_OUTPUTS
    output_node_names = [None]*NUMBER_OF_OUTPUTS
    for i in range(NUMBER_OF_OUTPUTS):
        output_node_names[i] = OUTPUT_NODE_PREFIX+str(i)
        output[i] = tf.identity(model.outputs[i], name=output_node_names[i])
    
    try:
        frozen_graph = graph_util.convert_variables_to_constants(sess, sess.graph.as_graph_def(), output_node_names)
        graph_io.write_graph(frozen_graph, os.path.dirname(pb_path), os.path.basename(pb_path), as_text=False)
        print('Frozen graph ready for inference/serving at {}'.format(pb_path))
    except:
        print("error !")
  • pb 到uff

采用TensorRT提供的uff模块的from_tensorflow_frozen_model()将pb格式模型转换成uff格式模型

import uff
def pb_to_uff(pb_path, uff_path, output_names):
        uff_model = uff.from_tensorflow_frozen_model(pb_path, output_names, output_filename=uff_path)

uff转换成plan engine模型

import tensorrt as trt

TRT_LOGGER = trt.Logger(trt.Logger.INFO)
img_size_tr = (3,224,224) #CHW
input_names = ['input_0']
output_names = ['output_0']

def build_engine(uff_path):
    with trt.Builder(TRT_LOGGER) as builder, builder.create_network() as network, trt.UffParser() as parser:
        builder.max_batch_size = 128 #must bigger than batch_size
        builder.max_workspace_size =1<<15  #cuda buffer size
        builder.fp16_mode = True  #set dtype: fp32, fp16, int8
        builder.strict_type_constraints = True
        # Parse the Uff Network
        parser.register_input(input_names[0], img_size_tr)#NCHW
        parser.register_output(output_names[0])
        parser.parse(uff_path, network)
        # Build and return an engine.
        return builder.build_cuda_engine(network)

在绑定完输入输出节点之后,parser.parse()可以解析uff格式文件,并保存相应网络到network。而后通过builder.build_cuda_engine()得到可以直接在cuda执行的engine文件。该engine文件的构建需要一定时间,可以保存下来,下次直接加载该文件,而不需要解析模型后再构建。

TensorFlow的模型转换基本和onnx是一样的,主要是解析器不一样是UffParser。

#解析模型,构建engine并保存
with build_engine(uff_path) as engine:
    with open(engine_path, "wb") as f:
        f.write(engine.serialize())

#直接加载engine   
with open(engine_path, "rb") as f, trt.Runtime(TRT_LOGGER) as runtime:
    engine = runtime.deserialize_cuda_engine(f.read())

模型推理

通过TensorRT的模型转换后,外部训练好的模型都被TensorRT统一成TensorRT可识别的engine文件(并优化过)。在推理时,只要通过TensorRT的推理SDK就可以完成推理。

具体的推理过程如下:

  • 通过TensorRT运行时,加载转换好的engine
  • 推理前准备:(1)在CPU中处理好输入(如读取数据和标准化等)(2)利用TensorRT的推理SDK中common模块进行输入和输出GPU显存分配
  • 执行推理:(1)将CPU的输入拷贝到GPU中 (2)在GPU中进行推理,并将模型输出放入GPU显存中
  • 推理后处理:(1)将输出从GPU显存中拷贝到CPU中 (2)在CPU中进行其他后处理
import common
import numpy as np
import cv2
import tensorrt as trt

def inference_test(engine_path, img_file):

    # process input
    input_image = cv2.imread(img_file)
    input_image = input_image[..., ::-1] / 255.0
    input_image = np.expand_dims(input_image, axis=0)
    
    input_image = input_image.transpose((0, 3, 1, 2))  # NCHW for pytorch
    input_image = input_image.reshape(1, -1)  # .ravel()
        
    # infer
    batch_size = 1
    TRT_LOGGER = trt.Logger(trt.Logger.INFO)
    with open(engine_path, "rb") as f, trt.Runtime(TRT_LOGGER) as runtime:
        engine = runtime.deserialize_cuda_engine(f.read())
        # Allocate buffers and create a CUDA stream
        inputs, outputs, bindings, stream = common.allocate_buffers(engine, batch_size)
        # Contexts are used to perform inference.
        with engine.create_execution_context() as context:
             np.copyto(inputs[0].host, input_image)
             [output] = common.do_inference(context, bindings=bindings, inputs=inputs, outputs=outputs, stream=stream, batch_size=batch_size)

TensorRT进阶和缺点

前面较全面了介绍了TensorRT的特点(优点)和工作流程;希望能感受到TensorRT的魅力所在。

在实际代码中主要是通过python的接口来讲解,TensorRT也提供了C++的转换和推理方式,但是主要的关键概念是一样

那TensorRT有什么局限性吗?

首先,TensorRT只支持NVIDIA自家的设备,并根据自家设备的特点,做了很多的优化,如果是其他设备,TensorRT就不适用了。这时候可以考虑其他的推理框架,比如以推理编译为基础的TVM, 针对移动平台推理NCNN,MACE、MNN以及TFLite等,以及针对Intel CPU的OPENVINO。

其次,算子的支持程度;这几乎是所有第三方推理框架都遇到的问题,TensorRT在某些不支持的算子的情况下,TensorRT提供了plugin的方式,plugin提供了标准接口,允许自己开发新的算子,并以插件的方式加入TensorRT(后面会专门介绍,欢迎关注)。

总结

  • 训练需要前向计算和反向梯度更新,推理只需要前向计算
  • 推理框架优化:低精度优化、算子编译优化、内存优化、计算图调度
  • TensorRT是针对NVIDIA设备的高性能推理框架
  • TensorRT工作流程包括模型转换和模型推理
  • 针对Pytorch, TensorRT模型转换链路为:pth->onnx->trt plan
  • 针对TensorFlow,TensorRT模型转换链路为:ckpt->pb->uff->trt plan
  • TensorRT模型转换关键点为build,network和parse
  • TensorRT模型推理关键点为:tensorrt runtime,engine context,显存操作和推理

ONNX—-模型部署教程(1)

转自:mmdeploy

模型转换工具 https://convertmodel.com/

官网1:https://pytorch.org/docs/stable/onnx.html#functions

官网2:https://onnxruntime.ai/docs/get-started/

前言

OpenMMLab 的算法如何部署?是很多社区用户的困惑。而模型部署工具箱 MMDeploy 的开源,强势打通了从算法模型到应用程序这 “最后一公里”!

今天我们将开启模型部署入门系列教程,在模型部署开源库 MMDeploy 的辅助下,介绍以下内容:

  • 中间表示 ONNX 的定义标准
  • PyTorch 模型转换到 ONNX 模型的方法
  • 推理引擎 ONNX Runtime、TensorRT 的使用方法
  • 部署流水线 PyTorch – ONNX – ONNX Runtime/TensorRT 的示例及常见部署问题的解决方法
  • MMDeploy C/C++ 推理 SDK

希望通过本系列教程,带领大家学会如何把自己的 PyTorch 模型部署到 ONNX Runtime/TensorRT 上,并学会如何把 OpenMMLab 开源体系中各个计算机视觉任务的模型用 MMDeploy 部署到各个推理引擎上。

初识模型部署

在软件工程中,部署指把开发完毕的软件投入使用的过程,包括环境配置、软件安装等步骤。类似地,对于深度学习模型来说,模型部署指让训练好的模型在特定环境中运行的过程。相比于软件部署,模型部署会面临更多的难题:

1)运行模型所需的环境难以配置。深度学习模型通常是由一些框架编写,比如 PyTorch、TensorFlow。由于框架规模、依赖环境的限制,这些框架不适合在手机、开发板等生产环境中安装。

2)深度学习模型的结构通常比较庞大,需要大量的算力才能满足实时运行的需求。模型的运行效率需要优化。

因为这些难题的存在,模型部署不能靠简单的环境配置与安装完成。经过工业界和学术界数年的探索,模型部署有了一条流行的流水线:

为了让模型最终能够部署到某一环境上,开发者们可以使用任意一种深度学习框架来定义网络结构,并通过训练确定网络中的参数。之后,模型的结构和参数会被转换成一种只描述网络结构的中间表示,一些针对网络结构的优化会在中间表示上进行。最后,用面向硬件的高性能编程框架(如 CUDA,OpenCL)编写,能高效执行深度学习网络中算子的推理引擎会把中间表示转换成特定的文件格式,并在对应硬件平台上高效运行模型。

这一条流水线解决了模型部署中的两大问题:使用对接深度学习框架和推理引擎的中间表示,开发者不必担心如何在新环境中运行各个复杂的框架;通过中间表示的网络结构优化和推理引擎对运算的底层优化,模型的运算效率大幅提升。

中间表示 – ONNX

在介绍 ONNX 之前,我们先从本质上来认识一下神经网络的结构。神经网络实际上只是描述了数据计算的过程,其结构可以用计算图表示。比如 a+b 可以用下面的计算图来表示:

为了加速计算,一些框架会使用对神经网络“先编译,后执行”的静态图来描述网络。静态图的缺点是难以描述控制流(比如 if-else 分支语句和 for 循环语句),直接对其引入控制语句会导致产生不同的计算图。比如循环执行 n 次 a=a+b,对于不同的 n,会生成不同的计算图:

ONNX (Open Neural Network Exchange)是 Facebook 和微软在2017年共同发布的,用于标准描述计算图的一种格式。目前,在数家机构的共同维护下,ONNX 已经对接了多种深度学习框架和多种推理引擎。因此,ONNX 被当成了深度学习框架到推理引擎的桥梁,就像编译器的中间语言一样。由于各框架兼容性不一,我们通常只用 ONNX 表示更容易部署的静态图。

创建 PyTorch 模型

让我们用 PyTorch 实现一个超分辨率模型,并把模型部署到 ONNX Runtime 这个推理引擎上。

# 安装 ONNX Runtime, ONNX, OpenCV 
pip install onnxruntime onnx opencv-python

在一切都配置完毕后,用下面的代码来创建一个经典的超分辨率模型 SRCNN。

import os 
 
import cv2 
import numpy as np 
import requests 
import torch 
import torch.onnx 
from torch import nn 
 
class SuperResolutionNet(nn.Module): 
    def __init__(self, upscale_factor): 
        super().__init__() 
        self.upscale_factor = upscale_factor 
        self.img_upsampler = nn.Upsample( 
            scale_factor=self.upscale_factor, 
            mode='bicubic', 
            align_corners=False) 
 
        self.conv1 = nn.Conv2d(3,64,kernel_size=9,padding=4) 
        self.conv2 = nn.Conv2d(64,32,kernel_size=1,padding=0) 
        self.conv3 = nn.Conv2d(32,3,kernel_size=5,padding=2) 
 
        self.relu = nn.ReLU() 
 
    def forward(self, x): 
        x = self.img_upsampler(x) 
        out = self.relu(self.conv1(x)) 
        out = self.relu(self.conv2(out)) 
        out = self.conv3(out) 
        return out 
 
# Download checkpoint and test image 
urls = ['https://download.openmmlab.com/mmediting/restorers/srcnn/srcnn_x4k915_1x16_1000k_div2k_20200608-4186f232.pth', 
    'https://raw.githubusercontent.com/open-mmlab/mmediting/master/tests/data/face/000001.png'] 
names = ['srcnn.pth', 'face.png'] 
for url, name in zip(urls, names): 
    if not os.path.exists(name): 
        open(name, 'wb').write(requests.get(url).content) 
 
def init_torch_model(): 
    torch_model = SuperResolutionNet(upscale_factor=3) 
 
    state_dict = torch.load('srcnn.pth')['state_dict'] 
 
    # Adapt the checkpoint 
    for old_key in list(state_dict.keys()): 
        new_key = '.'.join(old_key.split('.')[1:]) 
        state_dict[new_key] = state_dict.pop(old_key) 
 
    torch_model.load_state_dict(state_dict) 
    torch_model.eval() 
    return torch_model 
 
model = init_torch_model() 
input_img = cv2.imread('face.png').astype(np.float32) 
 
# HWC to NCHW 
input_img = np.transpose(input_img, [2, 0, 1]) 
input_img = np.expand_dims(input_img, 0) 
 
# Inference 
torch_output = model(torch.from_numpy(input_img)).detach().numpy() 
 
# NCHW to HWC 
torch_output = np.squeeze(torch_output, 0) 
torch_output = np.clip(torch_output, 0, 255) 
torch_output = np.transpose(torch_output, [1, 2, 0]).astype(np.uint8) 
 
# Show image 
cv2.imwrite("face_torch.png", torch_output)

SRCNN 先把图像上采样到对应分辨率,再用 3 个卷积层处理图像。为了方便起见,我们跳过训练网络的步骤,直接下载模型权重(由于 MMEditing 中 SRCNN 的权重结构和我们定义的模型不太一样,我们修改了权重字典的 key 来适配我们定义的模型),同时下载好输入图片。为了让模型输出成正确的图片格式,我们把模型的输出转换成 HWC 格式,并保证每一通道的颜色值都在 0~255 之间。如果脚本正常运行的话,一幅超分辨率的人脸照片会保存在 “face_torch.png” 中。

在 PyTorch 模型测试正确后,我们来正式开始部署这个模型。我们下一步的任务是把 PyTorch 模型转换成用中间表示 ONNX 描述的模型。

让我们用下面的代码来把 PyTorch 的模型转换成 ONNX 格式的模型:

x = torch.randn(1, 3, 256, 256) 
 
with torch.no_grad(): 
    torch.onnx.export( 
        model, 
        x, 
        "srcnn.onnx", 
        opset_version=11, 
        input_names=['input'], 
        output_names=['output'])

其中,torch.onnx.export 是 PyTorch 自带的把模型转换成 ONNX 格式的函数。让我们先看一下前三个必选参数:前三个参数分别是要转换的模型、模型的任意一组输入、导出的 ONNX 文件的文件名。转换模型时,需要原模型和输出文件名是很容易理解的,但为什么需要为模型提供一组输入呢?这就涉及到 ONNX 转换的原理了。从 PyTorch 的模型到 ONNX 的模型,本质上是一种语言上的翻译。直觉上的想法是像编译器一样彻底解析原模型的代码,记录所有控制流。但前面也讲到,我们通常只用 ONNX 记录不考虑控制流的静态图。因此,PyTorch 提供了一种叫做追踪(trace)的模型转换方法:给定一组输入,再实际执行一遍模型,即把这组输入对应的计算图记录下来,保存为 ONNX 格式。export 函数用的就是追踪导出方法,需要给任意一组输入,让模型跑起来。我们的测试图片是三通道,256×256大小的,这里也构造一个同样形状的随机张量。

剩下的参数中,opset_version 表示 ONNX 算子集的版本。深度学习的发展会不断诞生新算子,为了支持这些新增的算子,ONNX会经常发布新的算子集,目前已经更新15个版本。我们令 opset_version = 11,即使用第11个 ONNX 算子集,是因为 SRCNN 中的 bicubic (双三次插值)在 opset11 中才得到支持。剩下的两个参数 input_names, output_names 是输入、输出 tensor 的名称,我们稍后会用到这些名称。

如果上述代码运行成功,目录下会新增一个”srcnn.onnx”的 ONNX 模型文件。我们可以用下面的脚本来验证一下模型文件是否正确。

import onnx 
 
onnx_model = onnx.load("srcnn.onnx") 
try: 
    onnx.checker.check_model(onnx_model) 
except Exception: 
    print("Model incorrect") 
else: 
    print("Model correct")

其中,onnx.load 函数用于读取一个 ONNX 模型。onnx.checker.check_model 用于检查模型格式是否正确,如果有错误的话该函数会直接报错。我们的模型是正确的,控制台中应该会打印出”Model correct”。

接下来,让我们来看一看 ONNX 模型具体的结构是怎么样的。我们可以使用 Netron (开源的模型可视化工具)来可视化 ONNX 模型。把 srcnn.onnx 文件从本地的文件系统拖入网站,即可看到如下的可视化结果:

点击 input 或者 output,可以查看 ONNX 模型的基本信息,包括模型的版本信息,以及模型输入、输出的名称和数据类型。

点击某一个算子节点,可以看到算子的具体信息。比如点击第一个 Conv 可以看到:

每个算子记录了算子属性、图结构、权重三类信息。

  • 算子属性信息即图中 attributes 里的信息,对于卷积来说,算子属性包括了卷积核大小(kernel_shape)、卷积步长(strides)等内容。这些算子属性最终会用来生成一个具体的算子。
  • 图结构信息指算子节点在计算图中的名称、邻边的信息。对于图中的卷积来说,该算子节点叫做 Conv_2,输入数据叫做 11,输出数据叫做 12。根据每个算子节点的图结构信息,就能完整地复原出网络的计算图。
  • 权重信息指的是网络经过训练后,算子存储的权重信息。对于卷积来说,权重信息包括卷积核的权重值和卷积后的偏差值。点击图中 conv1.weight, conv1.bias 后面的加号即可看到权重信息的具体内容。

现在,我们有了 SRCNN 的 ONNX 模型。让我们看看最后该如何把这个模型运行起来。

推理引擎 -ONNX Runtime

ONNX Runtime 是由微软维护的一个跨平台机器学习推理加速器,也就是我们前面提到的”推理引擎“。ONNX Runtime 是直接对接 ONNX 的,即 ONNX Runtime 可以直接读取并运行 .onnx 文件, 而不需要再把 .onnx 格式的文件转换成其他格式的文件。也就是说,对于 PyTorch – ONNX – ONNX Runtime 这条部署流水线,只要在目标设备中得到 .onnx 文件,并在 ONNX Runtime 上运行模型,模型部署就算大功告成了。

通过刚刚的操作,我们把 PyTorch 编写的模型转换成了 ONNX 模型,并通过可视化检查了模型的正确性。最后,让我们用 ONNX Runtime 运行一下模型,完成模型部署的最后一步。

ONNX Runtime 提供了 Python 接口。接着刚才的脚本,我们可以添加如下代码运行模型:

import onnxruntime 
 
ort_session = onnxruntime.InferenceSession("srcnn.onnx") 
ort_inputs = {'input': input_img} 
ort_output = ort_session.run(['output'], ort_inputs)[0] 
 
ort_output = np.squeeze(ort_output, 0) 
ort_output = np.clip(ort_output, 0, 255) 
ort_output = np.transpose(ort_output, [1, 2, 0]).astype(np.uint8) 
cv2.imwrite("face_ort.png", ort_output)

这段代码中,除去后处理操作外,和 ONNX Runtime 相关的代码只有三行。让我们简单解析一下这三行代码。onnxruntime.InferenceSession用于获取一个 ONNX Runtime 推理器,其参数是用于推理的 ONNX 模型文件。推理器的 run 方法用于模型推理,其第一个参数为输出张量名的列表,第二个参数为输入值的字典。其中输入值字典的 key 为张量名,value 为 numpy 类型的张量值。输入输出张量的名称需要和torch.onnx.export 中设置的输入输出名对应。

如果代码正常运行的话,另一幅超分辨率照片会保存在”face_ort.png”中。这幅图片和刚刚得到的”face_torch.png”是一模一样的。这说明 ONNX Runtime 成功运行了 SRCNN 模型,模型部署完成了!以后有用户想实现超分辨率的操作,我们只需要提供一个 “srcnn.onnx” 文件,并帮助用户配置好 ONNX Runtime 的 Python 环境,用几行代码就可以运行模型了。或者还有更简便的方法,我们可以利用 ONNX Runtime 编译出一个可以直接执行模型的应用程序。我们只需要给用户提供 ONNX 模型文件,并让用户在应用程序选择要执行的 ONNX 模型文件名就可以运行模型了。

总结

  • 模型部署,指把训练好的模型在特定环境中运行的过程。模型部署要解决模型框架兼容性差和模型运行速度慢这两大问题。
  • 模型部署的常见流水线是“深度学习框架-中间表示-推理引擎”。其中比较常用的一个中间表示是 ONNX。
  • 深度学习模型实际上就是一个计算图。模型部署时通常把模型转换成静态的计算图,即没有控制流(分支语句、循环语句)的计算图。
  • PyTorch 框架自带对 ONNX 的支持,只需要构造一组随机的输入,并对模型调用 torch.onnx.export 即可完成 PyTorch 到 ONNX 的转换。
  • 推理引擎 ONNX Runtime 对 ONNX 模型有原生的支持。给定一个 .onnx 文件,只需要简单使用 ONNX Runtime 的 Python API 就可以完成模型推理。

关于模型部署

当我们千辛万苦完成了前面的数据获取、数据清洗、模型训练、模型评估等等步骤之后,终于等到“上线”啦。想到辛苦训练出来的模型要被调用还有点小激动呢,可是真当下手的时候就有点懵了:模型要怎么部署?部署在哪里?有什么限制或要求?

模型训练重点关注的是如何通过训练策略来得到一个性能更好的模型,其过程似乎包含着各种“玄学”,被戏称为“炼丹”。整个流程包含从训练样本的获取(包括数据采集与标注),模型结构的确定,损失函数和评价指标的确定,到模型参数的训练,这部分更多是业务方去承接相关工作。一旦“炼丹”完成(即训练得到了一个指标不错的模型),如何将这颗“丹药”赋能到实际业务中,充分发挥其能力,这就是部署方需要承接的工作。

目前来说,我还没有真正的接触工业界的模型应用,仅仅只是在学术界进行模型训练和探索。部署只是简单的将模型推理代码直接部署在服务器上,因此也没有考虑过模型在工业界的部署,对于工业应用来说,模型的表现和推理速度同等重要,因此,如何提高模型的推理速度成为模型能否落地的关键因素。

部署流程大致分为以下几个步骤:模型转换、模型量化压缩、模型打包封装 SDK。这里我们主要探讨模型转换。

模型转换主要用于模型在不同框架之间的流转,常用于训练和推理场景的连接。目前主流的框架都以 ONNX 或者 caffe 为模型的交换格式,另外,根据需要,还可以在中间插入计算图优化,对计算机进行推理加速(诸如常见的 CONV/BN 的算子融合)。

模型部署

在软件工程中,部署指把开发完毕的软件投入使用的过程,包括环境配置、软件安装等步骤。类似地,对于深度学习模型来说,模型部署指让训练好的模型在特定环境中运行的过程。相比于软件部署,模型部署会面临更多的难题:

1)运行模型所需的环境难以配置。深度学习模型通常是由一些框架编写,比如 PyTorch、TensorFlow。由于框架规模、依赖环境的限制,这些框架不适合在手机、开发板等生产环境中安装。

2)深度学习模型的结构通常比较庞大,需要大量的算力才能满足实时运行的需求。模型的运行效率需要优化。

因为这些难题的存在,模型部署不能靠简单的环境配置与安装完成。经过工业界和学术界数年的探索,模型部署有了一条流行的流水线:

为了让模型最终能够部署到某一环境上,开发者们可以使用任意一种深度学习框架来定义网络结构,并通过训练确定网络中的参数。之后,模型的结构和参数会被转换成一种只描述网络结构的中间表示,一些针对网络结构的优化会在中间表示上进行。最后,用面向硬件的高性能编程框架(如 CUDA,OpenCL)编写,能高效执行深度学习网络中算子的推理引擎会把中间表示转换成特定的文件格式,并在对应硬件平台上高效运行模型。

这一条流水线解决了模型部署中的两大问题:使用对接深度学习框架和推理引擎的中间表示,开发者不必担心如何在新环境中运行各个复杂的框架;通过中间表示的网络结构优化和推理引擎对运算的底层优化,模型的运算效率大幅提升。

模型压缩

最近在做的yolo网络硬件加速项目,需要去对原始网络进行压缩,因此记录下相关知识:

相关综述:

A Survey of Model Compression and Acceleration for Deep Neural Networks

《A Comprehensive Survey on Model Compression and Acceleration》

目前,在模型压缩和加速方面常用的方法大致可以分为四类:剪枝与量化(parameter pruning and quantization)、低秩因子分解(low-rank factorization)、迁移/压缩卷积滤波器(transferred/compact convolutional filters)、蒸馏学习(knowledge distillation)。

模型压缩方法

背景

近年来,深度神经网络(deep neural networks,DNN)逐渐受到各行各业的关注。它是指具有更深层(不止一个隐藏层)的神经网络,是深度学习的基础。很多实际的工作通常依赖于数百万甚至数十亿个参数的深度网络,这样复杂的大规模模型通常对计算机的CPU和GPU有着极高的要求,并且会消耗大量内存,产生巨大的计算成本。随着一些便携式设备(如移动电话)的快速发展,如何将这些复杂的计算系统部署到资源有限的设备上就成为了需要应对的全新挑战。这些设备通常内存有限,而且计算能力较低,不支持大模型的在线计算。因此需要对模型进行压缩和加速,以求在基本不损失模型精度的条件下,节约参数并降低其计算时间。

剪枝与量化主要针对模型中的冗余参数进行删减;低秩因子分解使用张量分解的方法来估计神经网络的参数;迁移/压缩卷积滤波器则是设计了一个特殊结构的卷积滤波器,能够减少参数空间并且节约内存;蒸馏学习是先训练一个较大的模型,再训练一个较小的神经网络以达到跟大模型同样的效果。其中,低秩因子分解和迁移/压缩卷积滤波器两种方法提供了端到端的管道,可以在CPU/GPU环境中轻松实现;而剪枝与量化使用二进制及稀疏约束等方法来实现目标。此外,剪枝与量化和低秩因子分解方法可以从预训练的模型中提取或者是从头开始训练,而另外两种方法仅支持从头开始的训练。这四种方法大多是独立设计的,但又相互补充,在实际应用中常常可以一起使用,实现对模型进一步的压缩或加速。接下来将分别对这四种方法进行介绍。

剪枝与量化(parameter pruning and quantization)

早期的研究表明,对构建的网络进行剪枝和量化在降低网络复杂性以及解决过拟合问题方面是有效的(Gong et al. 2014)。同剪枝与量化有关的方法可以进一步分为三个子类:量化与二值化(quantization and binarization)、网络剪枝(network pruning)、结构矩阵(structural matrix)。

1.量化与二值化(quantization and binarization)

在DNN中,权重通常是以32位浮点数的形式(即32-bit)进行存储,量化法则是通过减少表示每个权重需要的比特数(the number of bits)来压缩原始网络。此时权重可以量化为16-bit、8-bit、4-bit甚至是1-bit(这是量化的一种特殊情况,权重仅用二进制表示,称为权重二值化)。8-bit的参数量化已经可以在损失小部分准确率的同时实现大幅度加速(Vanhoucke et al. 2011)。图2展示了基于修剪、量化和编码三个过程的压缩法:首先修剪小权重的连接,然后使用权重共享来量化权重,最后将哈夫曼编码应用于量化后的权重和码本上。

此方法的缺点是,在处理大型CNN(如GoogleNet)时,二值网络的精度明显降低。此外,现有的二值化方法大多基于简单的矩阵近似,忽略了二值化对精度损失产生的影响。

2.网络剪枝(network pruning)

剪枝是指通过修剪影响较小的连接来显著减少DNN模型的存储和计算成本,目前比较主流的剪枝方法主要有以下几种:

  • 权重剪枝(weight pruning):此方法主要应用于对不重要的连接权重进行修剪。如果连接权重低于预先设定的某个阈值,则该连接权重将会被修剪(Han et al. 2015)。
  • 神经元剪枝(neuron pruning):此方法与逐个修剪权重的方法不同,它直接移除某个冗余的神经元。这样一来,该神经元的所有传入和传出连接也将被移除(Srinivas and Babu 2015)。
  • 卷积核剪枝(filter pruning):此方法依据卷积核的重要程度将其进行排序,并从网络中修剪最不重要/排名最低的卷积核。卷积核的重要程度可以通过或范数或一些其他方法计算(Li et al. 2016)。
  • 层剪枝(layer pruning):此方法主要应用于一些非常深度的网络,可以直接修剪其中的某些层(Chen and Zhao 2018)。

按照剪枝的对象分类,可以分为在全连接层上剪枝和在卷积层上剪枝两种。DNN中的全连接层是存储密集的,对全连接层中的参数进行剪枝能够显著降低存储成本。对于卷积层而言,每个卷积层中都有许多卷积核,从卷积层修剪不重要的卷积核也能够减少计算成本并加速模型。

在全连接层上剪枝:考虑一个输入层、隐藏层和输出层分别具有3、2和1个神经元的前馈神经网络,如图3所示。

其中, x1​、x2​、x3​ 是网络的输入, wijl​ 是从当前层中节点 i 的层 l 到下一层中的节点 j 的权重。从图3(a)可以清楚地看出,目前总共有8个连接权重,如果删除两个橙色(虚线)的连接,那么总连接权重将减少到6个。类似地,从图3(b)中,如果移除红色神经元,那么其所有相关的连接权重(虚线)也将被移除,导致总连接权重减少到4个(参数数量减少50%)。

  • 在卷积层上剪枝: 在卷积神经网络中, 卷积核 WRh×w×ic×f 应用于每个输入的图像 I,IRm×n×ic, 并且经过卷积操作后输出特征映射 T,TRp×q×f 。其中, h 和 w 是卷积核的尺寸, ic 是输入图像中输入通道的数量, f 是应用的卷积核 的数量, m 和 n 是输入图像的尺寸, p 和 q 是结果特征映射的输出尺寸。输出特征映射的形状计算如下:

其中, s 为步长 (stride), p 为填充(padding)。图4显示了最简单的CNN形式,其 中输入图像的大小为 4×4×3, 应用的卷积核大小为 3×3×3×2 (2是卷积核的数 量)。

受到早期剪枝方法和神经网络过度参数化问题的启发,Han et al.(2015)提出了三步法来进行剪枝。其思想是,首先修剪激活小于某个预定义阈值的所有连接权重(不重要的连接),随后再识别那些重要的连接权重。最后,为了补偿由于修剪连接权重而导致的精度损失,再次微调/重新训练剪枝模型。这样的剪枝和再训练过程将重复数次,以减小模型的大小,将密集网络转换为稀疏网络。这种方法能够对全连接层和卷积层进行修剪,而且卷积层比全连接层对修剪更加敏感。

从卷积层修剪一些不重要的卷积核能够直接减少计算成本并且加速模型。但是,使用网络剪枝方法同样存在着一些问题。首先,使用或正则化进行剪枝比常规方法需要更多的迭代次数才能收敛。其次,所有的剪枝都需要手动设置神经网络层的灵敏度,这需要对参数进行微调,在某些应用中可能会十分复杂。最后,网络剪枝虽然通常能够使大模型变小,但是却不能够提高训练的效率。

3.结构矩阵(structural matrix)

神经网络各层之间使用的是非线性变换 f(x,N)=σ(Mx), 这里的 σ(⋅) 是对每个 元素特异的非线性算子, x 是输入向量, M 代表 m×n 维的参数矩阵, 此时的运算复 杂度为 O(mn) (V. Sindhwani et al. 2015) 。一个直观的剪枝方法就是使用参数化的 结构矩阵。一个大小为 m×n, 但是参数量却小于 mn 的矩阵就叫做结构矩阵。Cheng et al. ( 2015 ) 提 出了一种 基于循环预测的简单方法, 对于一个向量 r=(r0​,r1​,⋯,r(d−1)​), 其对应的 d×d 维循环矩阵定义如下:

这样一来存储的成本就从O (d2) 变成了O (d) 。给定 d 维 r 向量的条件下, 上式中的 一层循环神经网络的时间复杂度为 O(dlogd) 。

结构矩阵不仅能够降低内存成本,而且能够通过矩阵向量和梯度计算大幅度加快训练的速度。但是这种方法的缺点在于,结构约束通常会给模型带来偏差,从而损害模型的性能。再者,如何找到合适的结构矩阵也是一个难题,目前还没有理论上的方法能够推导出结构矩阵。

低秩因子分解(low-rank factorization)

低秩分解的思想是, 如果原始权重矩阵具有维数 m×n 和秩 r, 则满秩矩阵可以分 解为一个 m×r 的权重矩阵和一个 r×n 的权重矩阵。该方法通过将大矩阵分解为小矩 阵, 以减小模型的尺寸。CNN通常由许多层组成, 每层都有一组权重矩阵, 这些权重可以用张量 (Tensor) 来表示。图5展示了一个维数为 X×Y×Z 的三维张量。

给定一个维数为 N×N×D, 且有 K 个卷积核的卷积层, 其权重矩阵 W 可以表示为一个 N×N×D×K 维的张量 (Granés and Santamaria 2017) 。对于全连接层而言, W 可以用矩阵 (2阶张量) 来表示。因此对权重矩阵进行分解就是对张量进行分解。张量分解指的是, 用标量 (O阶张量) 、向量 (1阶张量) 、矩阵 (2阶张量) 和一些其他高阶的张量来表示原始张量的方法。对矩阵可以应用满秩分解 (full-rank decomposition) 和奇异值分解 (singular value decomposition, SVD), 对三维及三维以上张量可以应用 Tucker 分解和 CP分解 (Canonical Polyadic) (Deng et al.2020) 。

1.对矩阵的分解

  • 满秩分解。对任何给定的矩阵 AR(m×n), 其秩 rmin(m,n), 则 A 的满秩分解可以表示为 A=WH, 其中 WR(m×r),HR(r×n) 。如果 r 远小于 m 或 n,我们称 A 为低秩矩阵 (low-rank matrix) 。通过满秩分解可以将空间复杂度从O(mn) 显著减小到 O(r(m+n)) 。特别地, 当 m 和 n 非常接近, 并且原始矩阵是行(或列) 满秩时, 这种减小空间复杂度的作用会失效。满秩分解方法对于全连接层十分有效, 特别是当两层之间的神经元数量相差很大或权重矩阵低秩稀疏时。给定一个较小的正整数 k<r, 可以通过如下的式子求解最优的 WR(m×k),HR(k×n), 其中, F 表示Frobenius范数。
  • SVD。SVD是一种将原始权重矩阵分解为三个较小的矩阵以替换原始权重矩阵的 方法。对于任意的矩阵 AR(m×n), 存在分解 A=USVT, 其中, UR(m×r), SR(r×r),VTR(r×n) 。 U 和 V 是正交矩阵, S 是对角线上只有奇异值的对角矩 阵, 其中的每一个元素都比其下一个对角线上的元素大。这种方法可以使空间复 杂度从 O(mn) 减小到 O(r(m+n+1)) 。实际应用中, 可以用更小的 k 替换 r, 这 种方法称为截断奇异值分解 (truncated SVD, TSVD) 。在前馈神经网络和卷积神 经网络中, SVD是一种常用的分解方法, 主要用于减少参数的个数。

2.对三维及三维以上张量的分解

  • Tucker分解。该方法是将TSVD方法中的对角矩阵扩展为张量的一种方法。TSVD和Tucker分解之间的关系可以用图来表示:
  • CP分解。该分解是Tucker分解的一种特殊形式。如果Tucker分解中的每个 ri​ 等于正 整数 rC​, 并且核张量 K 满足, 除了 K(x1​,x2​,…,xd​),x1​=x2​=⋯=xd​ 之外 的所有元素都是 0 , 此时Tucker分解就成为了CP分解。与Tucker分解相比, CP分解 常用于解释数据的组成成分, 而前者主要用于数据压缩。图7展示了三阶张量 xR(I×J×K) 被 R 个组成部分分解的过程, 这个过程也可以用如下的公式来表示, 其中, ar​∈RI,br​∈RJ,cr​∈RK (Marcella Astrid and Seung- and Ik Lee 2018)。

基于低秩近似的方法虽然是模型压缩和加速的前沿,然而具体实现却并非易事。因为这涉及到分解操作,需要付出高昂的计算成本。此外,当前的方法仍集中于逐层执行低秩近似,因此无法执行全局的参数压缩。但全局的参数压缩十分重要,因为不同的层包含不同的信息。最后,与原始的模型相比,因子分解需要对大量的模型进行再训练以实现收敛。

迁移/压缩卷积滤波器(transferred/compact convolutional filters)

Cohen and Welling (2016) 提出了使用卷积滤波器压缩CNN模型的想法, 并在 研究中引入了等变群理论 (the equivariant group theory)。让 x 作为输入, Φ(⋅) 作为 一个神经网络或者网络层, Γ(⋅) 作为迁移矩阵, 则等价的概念定义如下:Γ′(Φ(x))=Φ(Γ(x))

这样的定义指的是, 迁移矩阵 Γ(⋅) 先对输入x进行变换, 再将其传输到 Φ(⋅) 所得到 的结果应该跟先将输入 x 映射到神经网络 Φ(⋅) 上再做变换 Γ(⋅) 得到的结果相同。值得注 意的是, Γ(⋅) 和 Γ′(⋅) 不一定相同, 因为它们作用在不同的对象上。根据这样的理论, 通过将变换应用于层或者滤波器 Φ(⋅) 来压缩整个网络模型就十分合理。从经验来看, 使用一组大的卷积滤波器也对深层CNN有益, 具体方法是将一些变换 Γ(⋅) 应用于一组 充当模型正则化器的小型基滤波器上。

沿着这一研究方向, 近期的许多研究提出了从一组基滤波器出发构建卷积层的思 想。它们的共同点是, 迁移矩阵 Γ(⋅) 是只在卷积滤波器的空间域中操作的一类函数。 例如, Shang et al. (2016) 发现, CNN的较低卷积层通过学习㐌余的滤波器来提取 输入信号的正负相位信息, 并将 Γ(⋅) 定义为简单的否定函数:

其中, Wx​ 是基础的卷积滤波器, Wx−​是由激活与 Wx​ 相反的移位 (shift) 构成的滤波 器, 并且这些移位是在最大池 (max-pooling) 操作后选择的。通过这样操作, 就可以 很容易的实现在所有卷积层上的二倍压缩率。它还表明, 否定变换作为一个强大的正 则化方法, 能够用以提高分类精度。一种直观的理解是, 具有成对正负约束的学习算 法可以产生实用而不是冗余的的卷积滤波器。此外, Zhai et al. (2016) 将 Γ(⋅) 定义为 应用于 2 维滤波器的平移函数集:Γ′Φ(x)=T(⋅,x,y)x,y∈{−k,…,k},(x,y)=(0,0)

其中, T(⋅,x,y) 表示第一个操作数沿其空间维度平移 (x,y), 并在边界处进行适当的零 填充以保持形状。提出的框架可用于公式 (1) 改善分类精度的问题, 进而作为 maxout网络的正则化版本。

对于将变换约束应用于卷积滤波器的方法,还有几个问题需要解决。首先,这些方法可以在宽/平的架构(如VGGNet,AlexNet)上实现有竞争力的性能,但是在窄/深的架构(如ResNet)上则不行。其次,迁移假设有时太强,无法指导学习过程,导致得到的结果在某些情况下不稳定。此外,使用紧凑的卷积滤波器虽然可以直接降低计算成本,但关键思想是要用紧凑的块替换松散的和过度参数化的滤波器以提高计算速度。

蒸馏学习(knowledge distillation)

蒸馏学习(knowledge distillation,KD)是指通过构建一个轻量化的小模型,利用性能更好的大模型的监督信息,来训练这个小模型,以期达到更好的性能和精度。KD与迁移学习(transfer learning)不同,在迁移学习中,我们使用相同的模型体系结构和学习的权重,仅根据应用的要求使用新层来替换部分全连接层。而在KD中,通过在大数据集上训练的更大的复杂网络(也称之为教师模型(teacher model))学习到的知识可以迁移到一个更小、更轻的网络上(也称之为学生模型(student model))。前一个大模型可以是单个的大模型,也可以是独立训练模型的集合。KD方法的主要思想是通过softmax函数学习课堂分布输出,将知识从大型教师模型转换为一个更小的学生模型。从教师模型训练学生模型的主要目的是学习教师模型的泛化能力。

在现有的KD方法中,学生模型的学习依赖于教师模型,是一个两阶段的过程。Lan et al.(2018)提出了实时本地集成(On-the-fly Native Ensemble,ONE),这是一种高效的单阶段在线蒸馏学习方法。在训练期间,ONE添加辅助分支以创建目标网络的多分支变体,然后从所有分支中创建本地集成教师模型。对于相同的目标标签约束,可以同时学习学生和每个分支。每个分支使用两个损失项进行训练,其中最常用的就是最大交叉熵损失(softmax cross-entropy loss)和蒸馏损失(distillation loss)。

在网络压缩这一步,可以使用深度神经网络方法来解决这个问题。Romero et al.(2015)提出了一种训练薄而深的网络的方法,称为FitNets,用以压缩宽且相对较浅(但实际上仍然很深)的网络。该方法扩展了原来的思想,允许得到更薄、更深的学生模型。为了学习教师网络的中间表示,FitNet让学生模仿老师的完全特征图。然而,这样的假设太过于严格,因为教师和学生的能力可能会有很大的差别。

基于蒸馏学习的方法可以使模型的深度变浅,并且能够显著降低计算成本。然而,这个方法也存在一些弊端。其中之一是KD方法只能应用于具有softmax损失函数的任务中。再者就是,与其他类型的方法相比,基于蒸馏学习的方法往往具有较差的竞争性能。

面临的问题

在文章的最后一部分,作者总结了现有的这些模型压缩和加速的方法仍然面临的一些问题与挑战,主要有以下几个方面:

  1. 当前的大多数先进方法建立在精心设计的CNN模型之上,这些模型限制了更改配置的自由度(例如,网络架构、超参数等)。为了处理更复杂的任务,未来应该提供更加合理的方法来配置压缩模型。
  2. 各种小型平台(例如移动设备、机器人、自动驾驶汽车等)的硬件限制仍然是阻碍深层CNN扩展的主要问题。如何充分利用有限的计算资源以及如何为这些平台设计特殊的压缩方法仍然是需要解决的问题。
  3. 剪枝是压缩和加速CNN的有效方法。目前的剪枝技术大多是为了修剪神经元之间的连接而设计的。此外,对通道进行剪枝能够直接减少特征映射的宽度并压缩模型。这种方法虽然很有效,但是修剪通道可能会显著地改变下一层的输入,因此也存在挑战性。
  4. 如前所述,结构矩阵和迁移卷积滤波器的方法必须使模型具有人类的先验知识,这将会显著影响模型的性能和稳定性。研究如何控制强加这些先验知识带来的影响至关重要。
  5. 蒸馏学习的方法具有很多的优点,比如无需特定的硬件就能够直接加速模型。开发基于KD的更多方法并且探索如何提高其性能是未来主要的发展方向。
  6. 尽管这些压缩方法取得了巨大的成就,但是黑箱机制(black box mechanism)仍然是其应用的关键障碍。比如,某些神经元/连接被修剪的原因尚不清楚。探索这些方法的解释能力仍然是一个重大挑战。

知识蒸馏(KD)综述

https://cloud.tencent.com/developer/article/1763873

https://www.cvmart.net/community/detail/5865

知识蒸馏总的思路:通过采用与训练好的复杂模型(teacher model)的输出作为监督信号,同label标签一起去做监督训练,训练一个简单的模型(student model)

摘要

近年来,深度神经网络在工业界和学术界都取得了成功,尤其是在计算机视觉任务方面。深度学习的巨大成功主要归因于其可扩展性以编码大规模数据并操纵数十亿个模型参数。但是,将这些繁琐的深度模型部署在资源有限的设备(例如,移动电话和嵌入式设备)上是一个挑战,这不仅是因为计算复杂性高,而且还有庞大的存储需求。为此,已经开发了多种模型压缩和加速技术。作为模型压缩和加速的代表类型,知识蒸馏有效地从大型教师模型中学习小型学生模型。它已迅速受到业界的关注。本文从知识类别,训练框架,师生架构,蒸馏算法,性能比较和应用的角度对知识蒸馏进行了全面的调查。此外,简要概述了知识蒸馏中的挑战,并讨论和转发了对未来研究的评论。

知识蒸馏简介

知识蒸馏,已经受到业界越来越多的关注。大型深度模型在实践中往往会获得良好的性能,因为当考虑新数据时,过度参数化会提高泛化性能。在知识蒸馏中,小模型(学生模型)通常是由一个大模型(教师模型)监督,算法的关键问题是如何从老师模型转换的知识传授给学生模型。一个知识蒸馏系统由三个主要部分组成:知识,蒸馏算法,和师生架构

知识蒸馏框架

用于模型压缩的知识蒸馏类似于人类学习的方式。受此启发,最近的知识蒸馏方法已扩展到师生学习,相互学习,辅助教学,终身学习和自学。知识蒸馏的大多数扩展都集中在压缩深度神经网络上。由此产生的轻量级学生网络可以轻松部署在视觉识别,语音识别和自然语言处理(NLP)等应用程序中。此外,知识蒸馏中的知识从一种模型到另一种模型的转移可以扩展到其他任务,例如对抗攻击,数据增强,数据隐私和安全性。通过知识蒸馏的动机进行模型压缩,知识转移的思想已被进一步用于压缩训练数据,即数据集蒸馏,这将知识从大型数据集转移到小型数据集以减轻深度模型的训练负担

早期知识蒸馏框架通常包含一个或多个大型的预训练教师模型和小型的学生模型。教师模型通常比学生模型大得多。主要思想是在教师模型的指导下训练高效的学生模型以获得相当的准确性。来自教师模型的监督信号(通常称为教师模型学到的“知识”)可以帮助学生模型模仿教师模型的行为。

在典型的图像分类任务中,logit(例如深层神经网络中最后一层的输出)被用作教师模型中知识的载体,而训练数据样本未明确提供该模型。例如,猫的图像被错误地归类为狗的可能性非常低,但是这种错误的可能性仍然比将猫误认为汽车的可能性高很多倍。另一个示例是,手写数字2的图像与数字3相比,与数字7更相似。这种由教师模型学习的知识也称为暗知识(“dark knowledge”)

早期的知识蒸馏中转移 dark knowledge 的方法如下。给定对数向量 z作为深度模型的最后一个全连接层的输出,则zi是第 i 类的对数,则输入属于第 i 类的概率 pi可以为 由softmax 函数估算:

因此,通过教师模型获得的软目标的预测包含暗知识,并且可以用作监督者,以将知识从教师模型转移到学生模型。同样,one-hot 标签也称为硬目标。关于软目标和硬目标的直观示例如图3所示。此外,引入温度因子T来控制每个软目标的重要性

较高的温度会在各个类别上产生较弱的概率分布。具体来说,当 T→∞时,所有类别都具有相同的概率。当 T→0时,软目标变为 one-hot 标记,即硬目标。教师模型提供的软目标(distillation loss)和ground-truth label提供的硬目标(student loss)对于提高学生模型的绩效都非常重要。

定义蒸馏损失以匹配教师模型和学生模型之间的 logits ,即:

其中 zt和 zs分别是教师和学生模型的logits。教师模型的logits通过交叉熵梯度与学生模型的 logits 匹配, 然后可以将相对于 logit zsi的梯度评估为:

如果温度 T 比 logits 高得多,

则可以根据其泰勒级数近似得出:

如果进一步假设每个转移训练样本的 logits 为零 (比如

则上式可以简化为:

因此,根据上式,在高温和零均值 logits 的情况下,蒸馏损失等于匹配教师模型和学生模型之间的 logit ,即最小化:(zsi−zti)

因此,通过与高温匹配的 logit 进行蒸馏可以传达非常有用的知识信息,这些信息是由教师模型学到的以训练学生模型。

学生损失(student loss)定义为 ground truth 标签和学生模型的软对数之间的交叉熵:

代表交叉熵损失,y 是一个 ground truth 向量,其中只有一个元素为1,它表示转移训练样本的 ground truth 标签,其他元素为0。在蒸馏和学生损失中,两者均使用学生模型的相同 logit,但温度不同。温度在学生损失中为T = 1,在蒸馏损失中为T = t。最后,传统知识蒸馏的基准模型是蒸馏和学生损失的结合:

其中 x 是转移集上的训练输入,W是学生模型的参数,并且是调节参数。为了轻松理解知识蒸馏,下图显示了传统知识蒸馏与教师和学生模型联合的特定体系结构。在下图所示的知识蒸馏中,始终首先对教师模型进行预训练,然后再进行训练。仅使用来自预训练教师模型的软目标的知识来训练学生模型。实际上,这就是离线知识提炼与基于响应的知识。

he specific architecture of the benchmark knowledge distillation(Hinton et al., 2015)

知识

知识的三种形式

Response-Based Knowledge

基于响应的知识通常是指教师模型最后输出层的神经响应。主要思想是直接模仿教师模型的最终预测。基于响应的知识蒸馏简单但有效地进行了模型压缩,已被广泛用于不同的任务和应用中。最流行的基于响应的图像分类知识被称为软目标。基于响应的知识的蒸馏损失可以表示为

其中LKL表示Kullback-Leibler(KL)散度损失。典型的基于响应的KD模型如下图所示。基于响应的知识可用于不同类型的模型预测。例如,对象检测任务中的响应可能包含logit以及边界框的偏移量。在语义地标定位任务中,例如人体姿态估计,教师模型的响应可能包括每个地标的热图。最近,基于响应的知识得到了进一步的探索,以解决将地面标签信息作为条件目标的问题。

基于响应的知识

基于响应的知识的概念是简单易懂的,尤其是在“黑暗知识(dark knowledge)”的情况下。从另一个角度看,软目标的有效性类似于标签平滑或正则化器。但是,基于响应的知识通常依赖于最后一层的输出(例如,软目标),因此无法解决教师模型在监督,这对于使用非常深层神经网络的表示学习非常重要。由于 soft logits 实际上是类概率分布,因此基于响应的知识蒸馏也仅限于监督学习。

Feature-Based Knowledge

深度神经网络擅长通过增加抽象来学习多个级别的特征表示。这就是代表性学习。因此,最后一层的输出和中间层的输出,即特征图,都可以用作监督学生模型训练的知识。具体来说,来自中间层的基于特征的知识是基于响应的知识的良好扩展,尤其是对于更薄和更深的网络的训练而言。

中间表示法首先在 Fitnets 中引入,通过提供 hints,以改善学生模型的训练。主要思想是直接匹配老师和学生的特征激活。受此启发,已经提出了多种其他方法来间接匹配特征从原始特征图中得出了一个“注意图”来表达知识。Huang和Wang(2017)使用神经元选择性转移对注意力图进行了概括。Passalis和Tefas(2018)通过匹配特征空间中的概率分布来传递知识。为了更容易地转移教师知识,Kim等人。(2018年)引入了所谓的“因素”,作为一种更易于理解的中间表示形式。为了缩小师生之间的绩效差距,Jin等人。(2019)提出了路线约束式提示学习,该方法通过教师提示层的输出来监督学生。最近,Heo等。(2019c)建议使用隐藏神经元的激活边界进行知识转移。有趣的是,教师模型中间层的参数共享以及基于响应的知识也可以被用作教师知识(Zhou et al。,2018)。

通常,基于特征的知识转移的蒸馏损失可以用公式表达为:

其中 ft(x),fs(x) 分别是教师模型和学生模型的中间层的特征图。转换函数Φt(ft(x)),Φs(fs(x)),通常在教师和学生模型的特征图不是同一形状时应用。LF(.)表示用于匹配老师和学生模型的特征图的相似度函数。一个通用的基于特征的KD模型如下图所示。

本文还从特征类型,源层和蒸馏损失的角度总结了不同类型的基于特征的知识,如下表所示。

具体地说,L2(.),L1(.),LCE(.),LMMD(.) 分别表示l2-范数距离,l1-范数距离,交叉熵损失和最大平均差异损失。尽管基于特征的知识转移为学生模型的学习提供了有利的信息,但是如何有效地从教师模型中选择提示层和从学生模型中选择引导层仍然有待进一步研究。由于 hint 层和 guided 层的大小之间存在显着差异,因此还需要探索如何正确匹配教师和学生的特征表示

Relation-Based Knowledge

基于响应的知识和基于特征的知识都使用教师模型中特定层的输出。基于关系的知识进一步探索了不同层或数据样本之间的关系

为了探索不同特征图之间的关系,Yim等人。(2017)提出了一种解决方案流程(FSP),该流程由两层之间的Gram矩阵定义。FSP 矩阵总结了特征图对之间的关系。它是使用两层要素之间的内积来计算的。利用特征图之间的相关性作为蒸馏的知识,(Lee et al。,2018)提出了通过奇异值分解的知识蒸馏来提取特征图中的关键信息。为了利用多位教师的知识,Zhang和Peng(2018)分别以每个教师模型的 logits 和特征为节点,形成了两个图。具体来说,在知识转移之前,不同的教师的重要性和关系通过 logits 和表示图进行建模(Zhang and Peng,2018)。Lee and Song(2019)提出了基于多头图的知识蒸馏。图知识是通过多头注意力网络在任意两个特征图之间的内部数据关系。为了探索成对的提示信息,学生模型还模拟了教师模型的成对的提示层之间的互信息(Passalis等,2020b)。通常,基于特征图的关系的知识的蒸馏损失可以表示为:

其中 ft和 fs分别是老师和学生模型的特征图。教师模型选取的成对特征图表达为:^ft,ˇft,学生模型选择的成对特征图表达为:^fs,ˇfs。Ψt(.)和Ψs(.)是来自教师和学生模型的成对特征图的相似性函数。LR1(.)

表示教师和学生特征图之间的相关函数。

传统的知识转移方法通常涉及个人知识的提炼。老师的软目标直接提炼给学生。实际上,提炼的知识不仅包含特征信息,还包含数据样本的相互关系。具体来说,刘等。(2019g)通过实例关系图提出了一种鲁棒而有效的知识提炼方法。实例关系图中传递的知识包含实例特征,实例关系和特征空间转换跨层。Park等。(2019)提出了一种关系知识蒸馏,该知识蒸馏了实例关系中的知识。基于流形学习的思想,通过特征嵌入来学习学生网络,这保留了教师网络中间层中样本的特征相似性(Chen等人,2020b)。使用数据的特征表示将数据样本之间的关系建模为概率分布(Passalis和Tefas,2018; Passalis等,2020a)。师生的概率分布与知识转移相匹配。(Tung and Mori,2019)提出了一种保留相似性的知识提炼方法。尤其是,将教师网络中输入对的相似激活所产生的保持相似性的知识转移到学生网络中,并保持成对相似性。Peng等。(2019a)提出了一种基于相关一致性的知识蒸馏方法,其中蒸馏的知识既包含实例级信息,又包含实例之间的相关性。使用关联一致性进行蒸馏,学生网络可以了解实例之间的关联。

典型的基于实例关系的KD模型如下图所示。

可以将提取的知识从不同的角度进行分类,例如数据的结构化知识,有关输入功能的特权信息。下表显示了基于关系的知识的不同网络类别的摘要。

尽管最近提供了一些类型的基于关系的知识,但是如何根据特征图或数据样本对关系信息进行建模(作为知识)仍然值得进一步研究

蒸馏

蒸馏的几种形式:

离线蒸馏(Offline Distillation)

大多数以前的知识蒸馏方法都可以脱机工作。在常见的知识蒸馏中,知识从预先训练的教师模型转移到学生模型。因此,整个训练过程有两个阶段,即:

  • 大型教师模型是在蒸馏之前首先在一组训练样本上训练的。
  • 教师模型用于提取logit或中间特征形式的知识,然后用于指导蒸馏过程中学生模型的训练。

离线蒸馏的第一阶段通常不作为知识蒸馏的一部分进行讨论,即,假定教师模型是预先定义的。很少关注教师模型结构及其与学生模型的关系。因此,离线方法主要集中于改进知识转移的不同部分,包括知识的设计以及用于匹配特征或分布匹配的损失函数。离线方法的主要优点在于它们简单易行。例如,教师模型可以包含使用可能位于不同机器上的不同软件包训练的一组模型。可以提取知识并将其存储在缓存中。

离线蒸馏方法通常采用单向知识转移和两阶段训练程序。然而,不可避免的是,复杂的高容量教师模型具有很长的训练时间,而离线蒸馏中对学生模型的训练通常在教师模型的指导下是有效的。此外,大型教师和小型学生之间的能力差距始终存在,而学生在很大程度上依赖于教师。

在线蒸馏(Online Distillation)

尽管离线蒸馏方法简单有效,但离线蒸馏中的一些问题已引起研究界的越来越多的关注。为了克服离线蒸馏的局限性,提出了在线蒸馏以进一步改善学生模型的性能,特别是在没有大容量高性能教师模型的情况下。在在线蒸馏中,教师模型和学生模型同时更新,并且整个知识蒸馏框架是端到端可训练的。

在最近三年中,已经提出了多种在线知识蒸馏方法。具体来说,在深度相互学习中(Zhang等人,2018b),多个神经网络以协作方式工作。在训练过程中,任何一个网络都可以作为学生模型,其他模型可以作为老师。为了提高泛化能力,通过使用 soft Logits 的集合来扩展深度相互学习(Guo等,2020)。Chen等。(2020a)进一步将辅助同伴(auxiliary peers)和小组负责人(group leader)引入深度相互学习中,以形成一套多样化的同伴模型。为了降低计算成本,Zhu和Gong(2018)提出了一种多分支架构,其中每个分支表示一个学生模型,不同分支共享相同的骨干网络。Kim等人(2019b)没有使用Logits,引入了特征融合模块来构建教师分类器。谢等。(2019)用便宜的卷积运算代替了卷积层以形成学生模型。Anil等。(2018)使用在线蒸馏来训练大规模分布式神经网络,并提出了在线蒸馏的一种变体,称为共蒸馏。并行共蒸馏以相同的架构训练多个模型,并且通过从其他模型转移知识来训练任何一个模型。最近,提出了一种在线对抗知识蒸馏方法,以利用来自类别概率和特征图的知识,同时由鉴别者训练多个网络(Chung等,2020)。

在线蒸馏是一种具有高效并行计算功能的单阶段端到端训练方案。然而,现有的在线方法(例如,相互学习)通常不能解决在线设置中的高能力教师,这使得在在线设置中进一步探索教师与学生模型之间的关系成为一个有趣的话题。

自我蒸馏(Self-Distillation)

在自我蒸馏中,教师和学生模型采用相同的网络。这可以视为在线蒸馏的特殊情况。具体来说,Zhang等。(2019b)提出了一种新的自蒸馏方法,其中将来自网络较深部分的知识蒸馏为浅层部分。与(Zhang et al。,2019b)中的自蒸馏相似,有人提出了一种自注意蒸馏方法进行车道检测(Hou et al。,2019)。该网络利用其自身层的注意力图作为其较低层的蒸馏目标。快照蒸馏(Yang et al。,2019b)是自我蒸馏的一种特殊变体,其中网络早期(教师)的知识被转移到其后期(学生)以支持在同一时期内的监督训练过程网络。为了进一步减少通过提前退出的推理时间,Phuong和Lampert(2019b)提出了基于蒸馏的训练方案,其中提前退出层尝试在训练过程中模仿后续退出层的输出。

另外,最近提出了一些有趣的自蒸馏方法。具体来说,袁等。提出了一种基于标签平滑规则化(label smoothing regularization)分析的无教师知识蒸馏方法(Yuan et al。,2020)。Hahn和Choi提出了一种新颖的自我知识蒸馏方法,其中自我知识由预测概率而不是传统的软概率组成(Hahn和Choi,2019)。这些预测的概率由训练模型的特征表示来定义。它们反映了特征嵌入空间中数据的相似性。Yun等。提出了分类自知识蒸馏,以匹配同一模型中同一来源内的类内样本和扩充样本之间的训练模型的输出分布(Yun et al。,2020)。此外,采用Lee等人(2019a)提出的自蒸馏进行数据增强,并将增强的自知性蒸馏为模型本身。还采用自我蒸馏中以一对一地优化具有相同架构的深度模型(教师或学生网络)(Furlanello等,2018; Bagherinezhad等,2018)。每个网络都使用教师优化来蒸馏先前网络的知识。

此外,还可以从人类师生学习的角度直观地了解离线,在线和自我蒸馏中。离线蒸馏是指知识渊博的老师向学生传授知识;在线蒸馏是指老师和学生互相学习;自我蒸馏是指学生自己学习知识。而且,就像人类学习一样,这三种蒸馏由于自身的优势可以结合起来互相补充。

师生架构

在知识蒸馏中,师生架构是形成知识转移的通用载体。换句话说,从老师到学生的知识获取和蒸馏的质量也取决于如何设计老师和学生的网络。在人类学习习惯方面,我们希望学生能够找到合适的老师。因此,如何在知识蒸馏中完成知识的提取和提取,如何选择或设计合适的师生结构是非常重要而又困难的问题。最近,在蒸馏过程中,教师和学生的模型设置几乎都预先设置了不变的大小和结构,从而容易造成模型容量差距。但是,几乎不存在如何特别设计教师和学生的体系结构以及为什么由这些模型设置确定其体系结构的方法。在本节中,将讨论下图所示的教师模型和学生模型的结构之间的关系。

师生架构关系

知识蒸馏以前曾被设计为压缩深度神经网络的方法之一。深度神经网络的复杂性主要来自两个维度:深度和宽度。通常需要将知识从更深和更广的神经网络转移到更浅和更薄的神经网络。学生网络通常选择为:

  • 教师网络的简化版本,每层中的层数更少且通道更少。
  • 教师网络的量化版本,其中保留了网络的结构。
  • 具有高效基本操作的小型网络。
  • 具有优化的全局网络结构的小型网络。
  • 与教师使用同一网络。

大型深层神经网络和小型学生神经网络之间的模型能力差距会降低知识转移的速度。为了有效地将知识转移到学生网络,已提出了多种方法来控制模型复杂度的可控降低。具体来说,Mirzadeh等。(2020)引入了助教来减轻教师模型和学生模型之间的训练差距。(Gao et al。,2020)通过残差学习进一步缩小了差距,即使用辅助结构来学习残差。另一方面,最近的几种方法也集中在最小化学生模型和教师模型的结构差异上。例如,Polino等。(2018)将网络量化与知识蒸馏相结合,即学生模型很小,是教师模型的量化版本。Nowak和Corso(2018)提出了一种结构压缩方法,该方法涉及将多层学习的知识转移到单层。Wang等。(2018a)逐步执行从教师网络到学生网络的块状知识转移,同时保留接受领域。在在线环境中,教师网络通常是学生网络的集合,其中学生模型彼此共享相似的结构(或相同的结构)。

最近,深度可分离卷积已被广泛用于为移动或嵌入式设备设计有效的神经网络。受神经架构搜索(或NAS)成功的启发,通过基于有效元操作或块的全局结构搜索,小型神经网络的性能得到了进一步改善。此外,动态搜索知识转移机制的想法也出现在知识蒸馏中,例如,使用强化学习以数据驱动的方式自动删除冗余层,并在给定教师网络条件下搜索最佳学生网络

以前的大多数工作都着重于设计教师和学生模型的结构或它们之间的知识转移方案。为了使小型学生模型与大型教师模型很好地匹配,以提高知识蒸馏的绩效,自适应的师生学习体系结构是必要的。最近,在知识蒸馏中进行神经体系结构搜索(NAS)的想法,即在教师模型的指导下联合搜索学生结构和知识转移,将是未来研究的一个有趣课题。

蒸馏算法

对抗蒸馏(Adversarial Distillation)

多教师蒸馏(Multi-Teacher Distillation)

跨模态蒸馏(Cross-Modal Distillation)

图蒸馏(Graph-Based Distillation)

注意力蒸馏(Attention-Based Distillation)

由于注意力可以很好地反映卷积神经网络的神经元激活,因此在知识蒸馏中使用了一些注意力机制来改善学生网络的性能。在这些基于注意力的KD方法中,定义了不同的注意力转移机制,用于从教师网络向学生蒸馏知识网络。注意转移的核心是定义用于特征嵌入神经网络各层的关注图。也就是说,使用关注图功能来传递关于特征嵌入的知识

无数据蒸馏(Data-Free Distillation)

量化蒸馏(Quantized Distillation)

网络量化通过将高精度网络(例如32位浮点)转换为低精度网络(例如2位和8位)来降低神经网络的计算复杂度。同时,知识蒸馏的目的是训练小型模型以产生与复杂模型相当的性能。目前已经有多篇文章提出了在量化过程使用教师-学生框架中的一些KD方法。量化蒸馏方法的框架如下图所示。

具体来说,Polino等。(2018)提出了一种量化蒸馏方法,将知识转移到权重量化的学生网络中。在(Mishra和Marr,2018年)中,提出的量化KD被称为“学徒”。高精度教师网络将知识转移到小型的低精度学生网络。为了确保小型学生网络准确地模仿大型教师网络,首先在特征图上对高精度教师网络进行量化,然后将知识从量化教师转移到量化学生网络(Wei等人,2018年) )。Kim等。(2019a)提出了基于量化学生网络的自学,以及基于师生网络与知识转移的共同研究的量化意识知识蒸馏。此外,Shin等。(2019)使用蒸馏和量化进行了深度神经网络的经验分析,同时考虑了知识蒸馏的超参数,例如教师网络的大小和蒸馏温度。

终身蒸馏(Lifelong Distillation)

终身学习,包括持续学习和元学习,旨在以与人类相似的方式进行学习。它积累了以前学到的知识,还将学到的知识转移到未来的学习中。知识蒸馏提供了一种有效的方法来保存和转移所学知识,而不会造成灾难性的遗忘。最近,基于终生学习的KD变体数量不断增加。

关于元学习:Jang等。(2019)设计了元转移网络,可以确定在师生架构中转移的内容和地点。Flennerhag等。(2019)提出了一个轻量级的框架,称为Leap,用于通过将知识从一种学习过程转移到另一种学习过程来对任务流形进行元学习。Peng等。(2019b)设计了一种用于少拍图像识别的新知识转移网络架构。该体系结构同时合并了来自图像和先验知识的视觉信息。刘等。(2019e)提出了一种用于图像检索的语义感知知识保存方法。从图像模态和语义信息中获得的教师知识将得到保存和转移。

此外,为了解决终身学习中的灾难性遗忘问题,全局蒸馏(Lee等人,2019b),基于知识蒸馏的终身GAN(Zhai等人,2019),多模型蒸馏(Zhou等人,2020) )和其他基于KD的方法(Li and Hoiem,2017; Shmelkov et al。,2017)已经开发出来,以提取学习到的知识并在新任务上教给学生网络。

NAS蒸馏(NAS-Based Distillation)

神经体系结构搜索(NAS)是最流行的自动机器学习(或AutoML)技术之一,旨在自动识别深度神经模型并自适应地学习适当的深度神经结构。在知识蒸馏中,知识转移的成功不仅取决于老师的知识,还取决于学生的架构。但是,大型教师模型和小型学生模型之间可能存在能力差距,从而使学生难以向老师学习。为了解决这个问题,已经有工作采用 NAS 来找到 oracle-based 和 architecture-aware 的合适的学生架构实现知识蒸馏。此外,知识蒸馏被用于提高神经架构搜索的效率,例如,具有蒸馏架构知识的 NAS(AdaNAS)以及教师指导的架构搜索(TGSA)。在TGSA中,指导每个体系结构搜索步骤以模仿教师网络的中间特征表示,通过有效搜索学生的可能结构,老师可以有效地监督特征转移。

性能对比

知识蒸馏是用于模型压缩的出色技术。通过捕获教师的知识并在教师学习中使用蒸馏策略,它可以提高轻量级学生模型的性能。近来,许多知识蒸馏方法致力于改善性能,尤其是在图像分类任务中。在本节中,为了清楚地证明知识蒸馏的有效性,总结了一些典型的KD方法在两个流行的图像分类数据集上的分类性能。

这两个数据集是 CIFAR10 和 CIFAR100,分别由分别来自 10 和 100 个类别的 32×32 RGB 图像组成。两者都具有 50000 个训练图像和 10000 个测试图像,并且每个类具有相同数量的训练和测试图像。为了公平比较,KD 方法的实验分类准确度结果(%)直接来自相应的原始论文,如 CIFAR10 的表5和 CIFAR100 的表6所示。当使用不同类型的知识,蒸馏方案和教师/学生模型的结构时,报告了不同方法的性能。具体而言,括号中的准确度是教师和学生模型的分类结果,它们是经过单独训练的。应该注意的是,DML 和 DCM 的成对精度是在线蒸馏后师生的表现。

总结和讨论

近年来,知识蒸馏及其应用引起了相当大的关注。本文从知识,蒸馏方案,师生架构,蒸馏算法,性能比较和应用的角度对知识蒸馏进行了全面综述。下面,讨论知识蒸馏的挑战,并对知识蒸馏的未来研究提供一些见识。

挑战

对于知识蒸馏,关键是:1)从教师那里提取丰富的知识;2)从教师那里转移知识以指导学生的训练。因此,本文从以下几个方面讨论知识蒸馏的挑战:知识的均等性,蒸馏的类型,师生体系结构的设计以及知识蒸馏的理论基础

大多数KD方法利用各种知识的组合,包括基于响应的知识,基于特征的知识和基于关系的知识。因此,重要的是要了解每种知识类型的影响,并知道不同种类的知识如何以互补的方式互相帮助。例如,基于响应的知识具有相似的动机来进行标签平滑和模型正则化; 基于特征的知识通常用于模仿教师的中间过程,而基于关系的知识则用于捕获不同样本之间的关系。为此,在统一和互补的框架中对不同类型的知识进行建模仍然是挑战。例如,来自不同提示层的知识可能对学生模型的训练有不同的影响:1)基于响应的知识来自最后一层;2)来自较深的提示/指导层的基于特征的知识可能会遭受过度规范化的困扰。

如何将丰富的知识从老师传授给学生是知识蒸馏的关键一步。通常,现有的蒸馏方法可分为离线蒸馏,在线蒸馏和自蒸馏。离线蒸馏通常用于从复杂的教师模型中转移知识,而教师模型和学生模型在在线蒸馏和自我蒸馏的设置中具有可比性。为了提高知识转移的效率,应进一步研究模型复杂性与现有蒸馏方案或其他新颖蒸馏方案之间的关系

目前,大多数KD方法都将重点放在新型知识或蒸馏损失函数上,而对师生体系结构的设计研究不足。实际上,除了知识和蒸馏算法之外,教师和学生的结构之间的关系也显着影响知识蒸馏的性能。例如,一方面,最近的一些研究发现,由于教师模型和学生模型之间的模型能力差距,学生模型无法从某些教师模型中学习到很多东西;另一方面,从对神经网络容量的一些早期理论分析来看,浅层网络能够学习与深层神经网络相同的表示。因此,设计有效的学生模型或构建合适的教师模型仍然是知识蒸馏中的难题。

尽管有大量的知识蒸馏方法和应用,但对知识蒸馏的理解(包括理论解释和实证评估)仍然不够。例如,蒸馏可以被视为一种获得特权信息的学习形式。线性教师模型和学生模型的假设使得能够通过蒸馏来研究学生学习特征的理论解释。此外,Cho和Hariharan(2019)对知识蒸馏的功效进行了一些实证评估和分析。但是,仍然很难获得对知识提升的可概括性的深刻理解,尤其是如何衡量知识的质量或师生架构的质量。

未来发展方向

为了提高知识蒸馏的性能,最重要的因素包括:怎样设计师生网络体系结构,从老师网络中学习什么样的知识,以及在何处提炼到学生网络中

深层神经网络的模型压缩和加速方法通常分为四个不同类别,即模型剪枝和量化,低秩分解,紧凑型卷积滤波器和知识蒸馏。在现有的知识蒸馏方法中,只有很少的相关工作讨论了知识蒸馏与其他压缩方法的结合。例如,量化知识蒸馏可以看作是一种参数修剪方法,它将网络量化整合到师生架构中。因此,为了学习用于在便携式平台上部署的高效轻巧的深度模型,由于大多数压缩技术都需要重新训练/微调过程,因此需要通过知识蒸馏和其他压缩技术进行混合压缩的方法。此外,如何决定使用不同压缩方法的正确顺序将是未来研究的有趣话题

除了用于深度神经网络加速的模型压缩之外,由于教师架构上知识转移的自然特性,知识蒸馏还可以用于其他问题。最近,知识蒸馏已应用于数据隐私和安全性,深度模型的对抗攻击,跨模态,多个域,灾难性遗忘,加速深度模型的学习,神经结构搜索的效率,自我监督和数据增强。另一个有趣的例子是,知识从小型教师网络向大型学生网络的转移可以加速学生的学习。这与传统的知识蒸馏有很大不同。大型模型从未标记的数据中学习的特征表示也可以通过蒸馏来监督目标模型。为此,将知识蒸馏扩展到其他目的和应用可能是有意义的未来方向。

知识蒸馏的学习类似于人类的学习。将知识转移推广到经典和传统的机器学习方法是可行的。例如,基于知识蒸馏的思想,传统的两阶段分类适用于单老师单学生问题。此外,知识蒸馏可以灵活地部署到各种学习方案中,例如对抗学习,自动机器学习,终身学习,和强化学习。因此,将来将知识蒸馏与其他学习方案整合起来以应对实际挑战将是有用的。