TensorRT量化实战课YOLOv7量化:YOLOv7-PTQ量化(二)

news2024/12/24 3:16:41

目录

    • 前言
    • 1. YOLOv7-PTQ量化流程
    • 2. 模型标定
    • 3. 敏感层分析

前言

手写 AI 推出的全新 TensorRT 模型量化实战课程,链接。记录下个人学习笔记,仅供自己参考。

该实战课程主要基于手写 AI 的 Latte 老师所出的 TensorRT下的模型量化,在其课程的基础上,所整理出的一些实战应用。

本次课程为 YOLOv7 量化实战第三课,主要介绍 YOLOv7-PTQ 量化

课程大纲可看下面的思维导图

在这里插入图片描述

1. YOLOv7-PTQ量化流程

在上节课程中我们介绍了 YOLOv7-PTQ 量化中 QDQ 节点的插入,这节课我们将会完成 PTQ 模型的量化和导出。

从上面的思维导图我们可以看到 YOLOv7-PTQ 量化的步骤,我们代码的讲解和编写都是按照这个流程来的。

在编写代码开始之前我们还是再来梳理下整个 YOLOv7-PTQ 量化的过程,如下:

1. 准备工作

首先是我们的准备工作,我们需要下载 YOLOv7 官方代码和预训练模型以及 COCO 数据集,并编写代码完成模型和数据的加载工作。

2. 插入 QDQ 节点

第二个就是我们需要对模型插入 QDQ 节点,它有以下两种方式:

  • 自动插入
    • 使用 quant_modules.initialize() 自动插入量化节点
  • 手动插入
    • 使用 quant_modules.initialize() 初始化量化操作或使用 QuantDescriptor() 自定义初始化量化操作
    • 编写代码为模型插入量化节点

3. 标定

第三部分就是我们的标定,其流程如下:

  • 1. 通过将标定数据送到网络并收集网络每个层的输入输出信息
  • 2. 根据统计出的信息,计算动态范围 range 和 scale,并保存在 QDQ 节点中

4. 敏感层分析

第四部分是敏感层分析,大致流程如下:

  • 1. 进行单一逐层量化,只开启某一层的量化其他层都不开启
  • 2. 在验证集上进行模型精度测试
  • 3. 选出前 10 个对模型精度影响比较大的层,关闭这 10 个层的量化,在前向计算时使用 float16 而不去使用 int8

5. 导出 PTQ 模型

第五个就是我们在标定之后需要导出 PTQ 模型,导出流程如下:

  • 1. 需要将我们上节课所说的 quant_nn.TensorQuantizer.use_fb_fake_quant 属性设置为 true
  • 2. torch.onnx.export() 导出 ONNX 模型

6. 性能对比

第六个就是性能的对比,包括精度和速度的对比。

上节课我们完成了 YOLOv7-PTQ 量化流程中的准备工作和插入 QDQ 节点,这节我们继续按照流程走,先来实现模型的标定工作,让我们开始吧!!!🚀🚀🚀

2. 模型标定

模型量化校准主要是由以下三个函数完成的:

1. calibrate_model

def calibrate_model(model, dataloader, device):

    # 收集前向信息
    collect_stats(model, dataloader, device)

    # 获取动态范围,计算 amax 值,scale 值
    compute_amax(model, method = 'mse')

该函数主要是讲两个校准步骤组合起来,用于模型的整体校准,整体步骤如下:

  • 使用 collect_stats 函数收集前向传播的统计信息
  • 调用 compute_amax 函数计算量化的尺度因子 amax

2. collect_stats

def collect_stats(model, data_loader, device, num_batch = 200):
    model.eval()

    # 开启校准器
    for name, module in model.named_modules():
        if isinstance(module, quant_nn.TensorQuantizer):
            if module._calibrator is not None:
                module.disable_quant()
                module.enable_calib()
            else:
                module.disable()

    # test
    with torch.no_grad():
        for i, datas in enumerate(data_loader):
            imgs = datas[0].to(device, non_blocking=True).float() / 255.0
            model(imgs)

            if i >= num_batch:
                break
    
    # 关闭校准器
    for name, module in model.named_modules():
        if isinstance(module, quant_nn.TensorQuantizer):
            if module._calibrator is not None:
                module.enable_quant()
                module.disable_calib()
            else:
                module.enable()

该函数的目的是收集模型在给定数据集上的激活统计信息,这通常是模型量化校准过程中的第一步,具体步骤如下:

  • 设置模型为 eval 模型,确保不启用如 dropout 这样的训练特有的行为
  • 遍历模型的所有模块,对于每一个 TensorQuantizer 实例
    • 如果有校准器存在,则禁用量化(不对输入进行量化)并启动校准模式(收集统计信息)
    • 如果没有校准器,则完全禁用该量化器(不执行任何操作)
  • 使用 data_loader 来提供数据,并通过模型执行前向传播
    • 讲数据转移到 device 上,并进行适当的归一化
    • 对每个批次数据,模型进行推理,但不进行梯度计算
    • 收集激活统计信息直到处理指定数量的批次
  • 最后,遍历模型的所有模块,对于每一个 TensorQuantizer 实例
    • 如果有校准器存在,则启用量化并禁用校准模式
    • 如果没有校准器,则重新启用该量化器

3. compute_amax

def compute_amax(model, **kwargs):
    
    for name, module in model.named_modules():
        if isinstance(module, quant_nn.TensorQuantizer):
            if module._calibrator is not None:
                if isinstance(module._calibrator, calib.MaxCalibrator):
                    module.load_calib_amax()
                else:
                    module.load_calib_amax(**kwargs)
                module._amax = module._amax.to(device)

一旦收集了激活的统计信息,该函数就会计算量化的尺度因子 amax(动态范围的最大值),这通常是模型量化校准过程中的第二步,步骤如下:

  • 遍历模型的所有模块,对于每一个 TensorQuantizer 实例
    • 如果有校准器存在,则根据收集的统计信息计算 amax 值,这个值代表了激活的最大幅值,用于确定量化的尺度
    • 将 amax 值转移到 device 上,以便在后续中使用

下面我们简单总结下模型量化校准的流程:

  • 1.数据准备: 准备用于标定的数据集,通常是模型训练或验证数据集的一个子集。

  • 2.收集统计信息: 通过 collect_stats 函数进行前向传播,以收集模型各层的激活分布统计信息。

  • 3.计算 amax: 使用 compute_amax 函数基于收集的统计信息计算量化参数(如最大激活值 amax)。

通过上述步骤,模型就可以得到合适的量化参数,从而在量化后保持性能并减小精度损失。

完整的示例代码如下:

import os
import yaml
import test
import torch
import collections
from pathlib import Path
from models.yolo import Model
from pytorch_quantization import calib
from absl import logging as quant_logging
from utils.datasets import create_dataloader
from pytorch_quantization import quant_modules
from pytorch_quantization import nn as quant_nn
from pytorch_quantization.tensor_quant import QuantDescriptor
from pytorch_quantization.nn.modules import _utils as quant_nn_utils

def load_yolov7_model(weight, device='cpu'):
    ckpt  = torch.load(weight, map_location=device)
    model = Model("cfg/training/yolov7.yaml", ch=3, nc=80).to(device)
    state_dict = ckpt['model'].float().state_dict()
    model.load_state_dict(state_dict, strict=False)
    return model

def prepare_val_dataset(cocodir, batch_size=32):
    dataloader = create_dataloader(
        f"{cocodir}/val2017.txt",
        imgsz=640,
        batch_size=batch_size,
        opt=collections.namedtuple("Opt", "single_cls")(False),
        augment=False, hyp=None, rect=True, cache=False, stride=32, pad=0.5, image_weights=False
    )[0]
    return dataloader

def prepare_train_dataset(cocodir, batch_size=32):
    
    with open("data/hyp.scratch.p5.yaml") as f:
        hyp = yaml.load(f, Loader=yaml.SafeLoader)

    dataloader = create_dataloader(
        f"{cocodir}/train2017.txt",
        imgsz=640,
        batch_size=batch_size,
        opt=collections.namedtuple("Opt", "single_cls")(False),
        augment=True, hyp=hyp, rect=True, cache=False, stride=32, pad=0, image_weights=False
    )[0]
    return dataloader

# input: Max ==> Histogram
def initialize():
    quant_desc_input = QuantDescriptor(calib_method='histogram')
    quant_nn.QuantConv2d.set_default_quant_desc_input(quant_desc_input)
    quant_nn.QuantMaxPool2d.set_default_quant_desc_input(quant_desc_input)
    quant_nn.QuantLinear.set_default_quant_desc_input(quant_desc_input)

    quant_logging.set_verbosity(quant_logging.ERROR)

def prepare_model(weight, device):
    # quant_modules.initialize()
    initialize()
    model = load_yolov7_model(weight, device)
    model.float()
    model.eval()
    with torch.no_grad():
        model.fuse()    # conv bn 进行层的合并, 加速
    return model

def tranfer_torch_to_quantization(nn_instance, quant_module):
    
    quant_instances = quant_module.__new__(quant_module)

    # 属性赋值
    for k, val in vars(nn_instance).items():
        setattr(quant_instances, k, val)

    # 初始化
    def __init__(self):
        # 返回两个 QuantDescriptor 的实例 self.__class__ 是 quant_instance 的类, QuantConv2d
        quant_desc_input, quant_desc_weight = quant_nn_utils.pop_quant_desc_in_kwargs(self.__class__)
        if isinstance(self, quant_nn_utils.QuantInputMixin):
            self.init_quantizer(quant_desc_input)
            # 加快量化速度
            if isinstance(self._input_quantizer._calibrator, calib.HistogramCalibrator):
                self._input_quantizer._calibrator._torch_hist = True
        else:
            self.init_quantizer(quant_desc_input, quant_desc_weight)
            if isinstance(self._input_quantizer._calibrator, calib.HistogramCalibrator):
                self._input_quantizer._calibrator._torch_hist = True
                self._weight_quantizer._calibrator._torch_hist = True

    __init__(quant_instances)
    return quant_instances

def torch_module_find_quant_module(model, module_list, prefix=''):
    for name in model._modules:
        submodule = model._modules[name]
        path = name if prefix == '' else prefix + '.' + name
        torch_module_find_quant_module(submodule, module_list, prefix=path) # 递归

        submodule_id = id(type(submodule))
        if submodule_id in module_list:
            # 转换
            model._modules[name] = tranfer_torch_to_quantization(submodule, module_list[submodule_id])
        
def replace_to_quantization_model(model):
    
    module_list = {}
    
    for entry in quant_modules._DEFAULT_QUANT_MAP:
        module = getattr(entry.orig_mod, entry.mod_name)  # module -> torch.nn.modules.conv.Conv1d
        module_list[id(module)] = entry.replace_mod
    
    torch_module_find_quant_module(model, module_list)


def evaluate_coco(model, loader, save_dir='', conf_thres=0.001, iou_thres=0.65):
    
    if save_dir and os.path.dirname(save_dir) != "":
        os.makedirs(os.path.dirname(save_dir), exist_ok=True)
    
    return test.test(
        "data/coco.yaml",
        save_dir=Path(save_dir),
        conf_thres=conf_thres,
        iou_thres=iou_thres,
        model=model,
        dataloader=loader,
        is_coco=True,
        plots=False,
        half_precision=True,
        save_json=False
    )[0][3]

def collect_stats(model, data_loader, device, num_batch = 200):
    model.eval()

    # 开启校准器
    for name, module in model.named_modules():
        if isinstance(module, quant_nn.TensorQuantizer):
            if module._calibrator is not None:
                module.disable_quant()
                module.enable_calib()
            else:
                module.disable()

    # test
    with torch.no_grad():
        for i, datas in enumerate(data_loader):
            imgs = datas[0].to(device, non_blocking=True).float() / 255.0
            model(imgs)

            if i >= num_batch:
                break
    
    # 关闭校准器
    for name, module in model.named_modules():
        if isinstance(module, quant_nn.TensorQuantizer):
            if module._calibrator is not None:
                module.enable_quant()
                module.disable_calib()
            else:
                module.enable()
            
def compute_amax(model, **kwargs):
    
    for name, module in model.named_modules():
        if isinstance(module, quant_nn.TensorQuantizer):
            if module._calibrator is not None:
                if isinstance(module._calibrator, calib.MaxCalibrator):
                    module.load_calib_amax()
                else:
                    module.load_calib_amax(**kwargs)
                module._amax = module._amax.to(device)


def calibrate_model(model, dataloader, device):

    # 收集前向信息
    collect_stats(model, dataloader, device)

    # 获取动态范围,计算 amax 值,scale 值
    compute_amax(model, method = 'mse')

if __name__ == "__main__":

    weight = "yolov7.pt"
    device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')

    # 加载数据
    print("Evalute Dataset...")
    cocodir = "dataset/coco2017"
    val_dataloader   = prepare_val_dataset(cocodir)
    train_dataloader = prepare_train_dataset(cocodir)

    # 加载 pth 模型
    pth_model = load_yolov7_model(weight, device)
    # pth 模型验证
    print("Evalute Origin...")
    ap = evaluate_coco(pth_model, val_dataloader)

    # 获取伪量化模型(手动 initial(), 手动插入 QDQ)
    model = prepare_model(weight, device)
    replace_to_quantization_model(model)

    # 模型标定
    calibrate_model(model, train_dataloader, device)

    # # PTQ 模型验证
    print("Evaluate PTQ...")
    ptq_ap = evaluate_coco(model, val_dataloader)

值得注意的是我们校准时是在训练集上完成的,测试时是在验证集上完成的,运行效果如下:

在这里插入图片描述

可以看到量化校准后的模型的 mAP 仅仅下降了 0.003 个点。

博主学得有点混淆了,先梳理下一些概念,我们收集统计信息的目的是为了确定当前 tensor 的 amax 即幅度的最大值,然后根据不同的校准方法和获取的统计信息去校准计算 amax,其中包括 Max 和直方图两种校准方法,Max 校准方法直接选择 tensor 统计信息的最大值来作为 amax,而直方图校准中又包含 entropy、mse、percentile 三种方法来计算 amax,上述过程仅仅是进行了校准确定了 amax 值,得到了量化时所需要的 scale,但是还没有利用 scale 进行具体的量化操作,模型的权重或激活值还没有改变,应该是这么理解的吧😂

下面我们来对比下 Max 和直方图校准方法的 PTQ 模型的对比,来看看不同的校准方法对模型的影响

上面我们测试了直方图校准后的 PTQ 模型性能,下面我们来看 Max 校准方法,我们将 prepare_model 函数中的手动 initialize 函数注释,打开自动初始化 quant_module.initialize

再次执行代码如下所示:

在这里插入图片描述

可以看到我们使用默认的 Max 校准方法得到的 mAP 值是 0.444,相比于之前直方图校准的效果要差一些,因此后续我们可能就使用直方图校准的方式来进行量化。

下面我们来看看 PTQ 模型的导出,导出函数如下:

def export_ptq(model, save_file, device, dynamic_batch = True):
    
    input_dummy = torch.randn(1, 3, 640, 640, device=device)
    
    # 打开 fake 算子
    quant_nn.TensorQuantizer.use_fb_fake_quant = True

    model.eval()

    with torch.no_grad():
        torch.onnx.export(model, input_dummy, save_file, opset_version=13,
                          input_names=['input'], output_names=['output'],
                          dynamic_axes={'input': {0: 'batch'}, 'output': {0: 'batch'}} if dynamic_batch else None)

执行后效果如下:

在这里插入图片描述

我们将导出的 PTQ 模型和原始的 YOLOv7 模型对比,

在这里插入图片描述

左边是我们原始的 ONNX,右边是我们 PTQ 模型的 ONNX,可以看到导出的 PTQ 模型中多了 QDQ 节点的插入,其中包含了校准量化信息 scale。

以上就是 torch 和 PTQ 模型的对比,下面我们来进行敏感层的分析。

3. 敏感层分析

To be continue…

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1174325.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

el-tree中展示项换行展示

文章目录 效果如下所示:没有换行展示的效果修改样式换行之后的展示效果 想要了解el-tree使用的详情往下看代码和数据如下所示Vue代码中可能使用到的数据如下Vue的代码如下:没有换行展示的效果换行之后的展示效果样式调试 效果如下所示: 没有…

仅以此文,纪念毕业一年后的日子

22年6月份从华农毕业了之后,拿到了好几份不错的offer,最后我进入了我学生时代十分憧憬的一家公司(腾讯)工作,加上实习的时间,已经在腾讯差不多工作了两年了。 从一开始实习的时候的懵懂学生气到现在的清醒…

Azure 机器学习 - 使用 ONNX 对来自 AutoML 的计算机视觉模型进行预测

目录 一、环境准备二、下载 ONNX 模型文件2.1 Azure 机器学习工作室2.2 Azure 机器学习 Python SDK2.3 生成模型进行批量评分多类图像分类 三、加载标签和 ONNX 模型文件四、获取 ONNX 模型的预期输入和输出详细信息ONNX 模型的预期输入和输出格式多类图像分类 多类图像分类输入…

【Liunx基础】之指令(一)

【Liunx基础】之指令(一) 1.ls指令2.pwd命令3.cd指令4.touch指令5.mkdir指令(重要)6.rmdir指令与rm指令(重要)7.man指令(重要)8.cp指令(重要) 📃博客主页: 小…

核电堆芯组件动态特性试验研究

u 核电试验概述 反应堆是核电事业的核心组成部分之一,堆内构件、堆芯燃料组件等部件在冷却剂流动冲击下,会诱发剧烈振动,导致堆芯内试验件流道不稳定。为了保障反应堆的安全运行,根据国家核安全法规规定,有必要对受冷…

单链表的应用(2)

环形链表的约瑟夫问题 编号为 1 到 n 的 n 个人围成一圈。从编号为 1 的人开始报数,报到 m 的人离开。 下一个人继续从 1 开始报数。 n-1 轮结束以后,只剩下一个人,问最后留下的这个人编号是多少? 利用链表实现 思路&#xff1…

基础Redis-结构与命令

结构与命令 1.基础-Redisa.Redis数据结构介绍b.Redis通用命令c.key的结构d.String类型e.Hash类型f.List类型g.Set类型h.SortedSet类型 1.基础-Redis a.Redis数据结构介绍 Redis是一个key-value的数据库,key一般是String类型,不过value的类型多种多样&a…

idea中配置spring boot单项目多端口启动

参照文章 https://zhuanlan.zhihu.com/p/610767685 项目配置如下 下面为 idea 2023,不同版本的设置有区别,但是没那么大,idea 2023默认使用新布局,切换为经典布局即可。 在项目根目录的.idea/workspace.xml文件里添加如下配置 &l…

Java用log4j写日志

日志可以方便追踪和调试问题,以前用log4net写日志,换Java了改用log4j写日志,用法和log4net差不多。 到apache包下载下载log4j的包,解压后把下图两个jar包引入工程 先到网站根下加一个log4j2.xml的配置文件来配置日志的格式和参…

测试开发面试宝典,涨价倒计时

大家好,我是洋子,相信在面试软件测试、测试开发岗位的小伙伴都深有体会,考察的知识点越来越多 不仅会考察到软件测试的理论,让你对某种功能进行测试用例的设计,更难一点会给出一个测试场景进行测试方案的设计&#xf…

C++二分算法:平衡子序列的最大和

涉及知识点 二分 动态规划 #题目 给你一个下标从 0 开始的整数数组 nums 。 nums 一个长度为 k 的 子序列 指的是选出 k 个 下标 i0 < i1 < … < ik-1 &#xff0c;如果这个子序列满足以下条件&#xff0c;我们说它是 平衡的 &#xff1a; 对于范围 [1, k - 1] 内的所…

openGauss学习笔记-115 openGauss 数据库管理-设置安全策略-设置密码安全策略

文章目录 openGauss学习笔记-115 openGauss 数据库管理-设置安全策略-设置密码安全策略115.1 操作步骤 openGauss学习笔记-115 openGauss 数据库管理-设置安全策略-设置密码安全策略 115.1 操作步骤 用户密码存储在系统表pg_authid中&#xff0c;为防止用户密码泄露&#xff…

2023-2024-1高级语言程序设计-一维数组

7-1 逆序输出数组元素的值 从键盘输入n个整数存入一维数组中&#xff0c;然后将数组元素的值逆序输出。 输入格式: 第一行输入整数个数n&#xff1b; 第二行输入n 个整数&#xff0c;数据之间以空格隔开。 输出格式: 逆序输出数组元素的值&#xff0c;每个数据之后跟一个空…

[React] React-Redux 快速入门

文章目录 1.安装 Redux Toolkit 和 React Redux2.创建 Redux Store3.为 React 提供 Redux Store​4.创建 Redux State Slice5.添加 Slice Reducers 到 Store6.在 React 组件中使用 Redux State 和 Actions​7.总结 1.安装 Redux Toolkit 和 React Redux npm install reduxjs/t…

测试服务器端口是否开通,计算退休时间

本案例知识点 netstat -tuln | grep 80 nestat 目前主机打开的网络服务端口&#xff0c;-tuln目前主机启动的服务&#xff0c;如图 报错说参数太多&#xff0c;仔细检查发现if后的中括号内&#xff0c;变量少双引号导致&#xff0c;改完之后运行显示22,25端口开放&#xff0…

关于编程不得不说的事

这些年&#xff0c;互联网爆炸式的发展&#xff0c;促生了无数程序员&#xff0c;也促生了大量 IT培训机构。短短数年间&#xff0c;科班出生的程序员和培训机构出生的程序员呈指数增长。程序员的职业也不再是金饭碗。写了这么多代码&#xff0c;有些感触&#xff0c;所以写下来…

挑战100天 AI In LeetCode Day01(1)

挑战100天 AI In LeetCode Day01&#xff08;1&#xff09; 一、LeetCode介绍二、LeetCode 热题 HOT 100-12.1 题目2.2 题解 三、面试经典 150 题-13.1 题目3.2 题解 一、LeetCode介绍 LeetCode是一个在线编程网站&#xff0c;提供各种算法和数据结构的题目&#xff0c;面向程序…

回顾十大数据恢复软件,帮助用于恢复丢失的文件!

您是否因丢失计算机上的重要文件而感到恐慌&#xff1f;你不是一个人&#xff01;数据丢失是许多人面临的严重问题&#xff0c;但幸运的是&#xff0c;有许多解决方案可以恢复数据。 在本文中&#xff0c;我将回顾十大数据恢复软件&#xff0c;以帮助您恢复丢失的文件&#xf…

线性【SVM】数学原理和算法实现

一. 数学原理 SVM是一类有监督的分类算法&#xff0c;它的大致思想是&#xff1a;假设样本空间上有两类点&#xff0c;如下图所示&#xff0c;我们希望找到一个划分超平面&#xff0c;将这两类样本分开&#xff0c;我们希望这个间隔能够最大化来使得模型泛化能力最强。 如上图所…

Vulnhub-DC-3 靶机复现完整过程

啰嗦两句&#xff1a; 提权之前完成是一个月前做的&#xff0c;当时在提权处出了点问题就搁置了&#xff0c;今天才完成&#xff0c;所以IP地址可能会会有变化 注意&#xff1a;后续出现的IP地址为192.168.200.55同样是靶机IP地址&#xff0c;若本文能有帮助到你的地方&#xf…