Jetson nano部署剪枝YOLOv8

目录

    • 前言
    • 一、YOLOv8模型剪枝训练
      • 1. Pretrain[option]
        • 1.1 项目的克隆
        • 1.2 数据集
        • 1.3 训练
      • 2. Constraint training
      • 3. Prune
      • 4. finetune
    • 二、YOLOv8模型剪枝部署
      • 1. 源码下载
      • 2. 环境配置
        • 2.1 trtexec环境变量设置
      • 3. ONNX导出
        • 3.1 Transpose节点的添加
        • 3.2 Resize节点解析的问题
      • 4. 运行
        • 4.1 engine生成
        • 4.2 源码修改
        • 4.3 编译运行
        • 4.4 拓展-摄像头检测
    • 三、讨论
    • 结语
    • 下载链接
    • 参考

前言

写本文的目的是刚学习完了剪枝与重参的课程,想跟着梁老师复现下对YOLOv8模型进行简单的剪枝,熟悉下整个流程。剪枝完成后我们可以将剪枝后的YOLOv8部署到Jetson嵌入式端,进一步提高其检测速度。于是乎我又可以水一篇文章了(哈哈哈,机智如我,我真是个天才😏)

默认大家对模型剪枝和模型部署都有一定的了解,大家可以查看我之前的剪枝与重参第七课:YOLOv8剪枝和Jetson nano部署YOLOv8,很多细节就不在这里赘述了。考虑到nano的算力,这里采用yolov8s.pt,本文主要分享yolov8s.pt模型剪枝和jetson nano部署剪枝yolov8两方面的内容。若有问题欢迎各位看官批评指正!!!😘

一、YOLOv8模型剪枝训练

YOLOv8模型剪枝训练的基本流程如下:

在这里插入图片描述

首先我们获得一个预训练模型,用做benchmark方便后续对比,然后进行约束训练,主要对BN层加上L1正则化,获得约束训练的模型后我们就可以对其进行剪枝了,最后将剪枝后的模型进行微调即可。

1. Pretrain[option]

获取预训练模型其实非必要,博主在这里为了方便对比,故选择进行预训练。

1.1 项目的克隆

yolov8的代码是开源的可直接从github官网上下载,源码下载地址是https://github.com/ultralytics/ultralytics,由于yolov8刚发布不久一个固定版本都没有,故只能采用主分支进行模型剪枝的训练和部署工作(PS:由于代码更新频繁,可能大家会遇到不同的bug)。Linux下代码克隆指令如下:

git clone https://github.com/ultralytics/ultralytics.git

1.2 数据集

训练采用的VOC数据集,这里给出下载链接Baidu Drive[pwd:yolo],本次训练并没有用到所有的数据,博主将train2007和val2007作为训练集,将test2007作为验证集,整个数据集文件夹内容如下图所示:

在这里插入图片描述

其中,images存放的内容是图片文件,labels存放的内容是YOLO格式的.txt标签文件,所有文件都可以从我分享的链接下载,大家可以按照上述方式将数据集进行整合。

1.3 训练

代码和数据集准备好后就可以进行训练了,训练修改的文件主要是两个即VOC.yaml用于指定数据集的相关路径和数据集包含的类别信息,default.yaml用于指定训练用到的权重和一些超参数,我们一个一个来修改。

VOC.yaml位于ultralytics/dataset下,其具体内容如下:

  • 首先path路径指定为上面整合的数据集的绝对路径,路径中最好不要含中文,在Windows下训练时最好将路径中的\替换为\\或者/,防止\转义。
  • train、val、test的内容就是VOC数据集下的用于训练、验证以及测试的图片
  • names不用修改
  • download内容全部删除即可
# Ultralytics YOLO 🚀, GPL-3.0 license
# PASCAL VOC dataset http://host.robots.ox.ac.uk/pascal/VOC by University of Oxford
# Example usage: yolo train data=VOC.yaml
# parent
# ├── ultralytics
# └── datasets
#     └── VOC  ← downloads here (2.8 GB)


# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
path: D:/YOLO/yolov8-prune/ultralytics/datasets/VOC
train: # train images (relative to 'path')  16551 images
  - images/train2007
  - images/val2007
val: # val images (relative to 'path')  4952 images
  - images/test2007
test: # test images (optional)
  - images/test2007

# Classes
names:
  0: aeroplane
  1: bicycle
  2: bird
  3: boat
  4: bottle
  5: bus
  6: car
  7: cat
  8: chair
  9: cow
  10: diningtable
  11: dog
  12: horse
  13: motorbike
  14: person
  15: pottedplant
  16: sheep
  17: sofa
  18: train
  19: tvmonitor

default.yaml是一个配置文件,位于ultralytics/yolo/cfg下,其需要修改的内容如下:

  • model即预训练模型,本次选用yolov8s.pt,可以从官网上下载好预训练模型放到v8/detect目录下,或者不用下载,你指定yolov8s.pt训练后会检测v8/detect路径下是否存在yolov8s.pt,不存在会直接去官网帮你下载(下载时需访问外网,如果没有代理还是手动下载好放到v8/detect目录下吧)
  • data即训练配置文件路径,也就是上面配置的VOC.yaml的绝对路径
  • epochs即训练迭代次数,这个得根据自己的显卡算力来,博主显卡不太顶用,100个epoch时间太长了,遭不住呀
  • amp即自动混合精度,有非常多的好处(比如…),但是在剪枝后需要finetune就不开启了,开启后需要很多地方设置,麻烦
# Train settings -------------------------------------------------------------------------------------------------------
model: yolov8s.pt  # path to model file, i.e. yolov8n.pt, yolov8n.yaml
data: D:/YOLO/yolov8-prune/ultralytics/datasets/VOC.yaml # path to data file, i.e. coco128.yaml
epochs: 50  # number of epochs to train for
amp: False  # Automatic Mixed Precision (AMP) training, choices=[True, False], True runs AMP check

将前面的步骤完成后,在ultralytics/yolo/v8/detect文件夹下找到train.py文件点击运行即可开始模型的训练。

博主训练的模型为yolov8s.pt且使用单个GPU进行训练,显卡为RTX3060矿卡(😂),操作系统为Windows10,pytorch版本为1.12.1,训练时长大概2个小时

在这里插入图片描述

训练完成后在detect文件夹下会生成一个runs文件夹,模型权重就保存在runs/detect/train/weights文件夹下,这里提供博主训练好的权重文件下载链接Baidu Driver[pwd:yolo]

2. Constraint training

约束训练主要是对模型进行BN层进行L1正则化,因此需要在trainer.py文件夹下添加BN层进行L1约束的代码,trainer.py文件位于ultralytics/yolo/engine文件夹下,添加的具体位置在327行,添加的具体内容如下:

# Backward
self.scaler.scale(self.loss).backward()

# ========== 新增 ==========
l1_lambda = 1e-2 * (1 - 0.9 * epoch / self.epochs)
for k, m in self.model.named_modules():
    if isinstance(m, nn.BatchNorm2d):
        m.weight.grad.data.add_(l1_lambda * torch.sign(m.weight.data))
        m.bias.grad.data.add_(1e-2 * torch.sign(m.bias.data))
# ========== 新增 ==========

# Optimize - https://pytorch.org/docs/master/notes/amp_examples.html
if ni - last_opt_step >= self.accumulate:
    self.optimizer_step()
    last_opt_step = ni

将代码修改好后,按照之前提到的Pretrain中将VOC.yaml和default.yaml修改好,点击train.py开始训练即可。这里提供博主训练好的权重文件下载链接Baidu Driver[pwd:yolo],约束训练完成效果如下图所示:

在这里插入图片描述

3. Prune

我们拿到约束训练的模型后就可以开始剪枝了,开工👨‍🏭,本次剪枝使用的是约束训练中的last.pt模型(我们不使用best.pt,通过result.csv你会发现mAP最高的模型在第一个epoch,主要是因为模型在COCO数据集上的预训练泛化性比较强,所以开始的mAP很高,这显然是不真实的),我们在根目录ultralytics-main文件夹下创建一个prune.py文件,用于我们的剪枝任务,同时将约束训练中的last.pt模型放到根目录下,prune.py文件的具体内容如下所示:

from ultralytics import YOLO
import torch
from ultralytics.nn.modules import Bottleneck, Conv, C2f, SPPF, Detect

# Load a model
yolo = YOLO("last.pt")  # build a new model from scratch
model = yolo.model

ws = []
bs = []

for name, m in model.named_modules():
    if isinstance(m, torch.nn.BatchNorm2d):
        w = m.weight.abs().detach()
        b = m.bias.abs().detach()
        ws.append(w)
        bs.append(b)
        # print(name, w.max().item(), w.min().item(), b.max().item(), b.min().item())
# keep
factor = 0.8
ws = torch.cat(ws)
threshold = torch.sort(ws, descending=True)[0][int(len(ws) * factor)]
print(threshold)

def prune_conv(conv1: Conv, conv2: Conv):
    gamma = conv1.bn.weight.data.detach()
    beta = conv1.bn.bias.data.detach()
    keep_idxs = []
    local_threshold = threshold
    while len(keep_idxs) < 8:
        keep_idxs = torch.where(gamma.abs() >= local_threshold)[0]
        local_threshold = local_threshold * 0.5
    n = len(keep_idxs)
    # n = max(int(len(idxs) * 0.8), p)
    # print(n / len(gamma) * 100)
    # scale = len(idxs) / n
    conv1.bn.weight.data = gamma[keep_idxs]
    conv1.bn.bias.data = beta[keep_idxs]
    conv1.bn.running_var.data = conv1.bn.running_var.data[keep_idxs]
    conv1.bn.running_mean.data = conv1.bn.running_mean.data[keep_idxs]
    conv1.bn.num_features = n
    conv1.conv.weight.data = conv1.conv.weight.data[keep_idxs]
    conv1.conv.out_channels = n

    if conv1.conv.bias is not None:
        conv1.conv.bias.data = conv1.conv.bias.data[keep_idxs]

    if not isinstance(conv2, list):
        conv2 = [conv2]

    for item in conv2:
        if item is not None:
            if isinstance(item, Conv):
                conv = item.conv
            else:
                conv = item
            conv.in_channels = n
            conv.weight.data = conv.weight.data[:, keep_idxs]


def prune(m1, m2):
    if isinstance(m1, C2f):  # C2f as a top conv
        m1 = m1.cv2

    if not isinstance(m2, list):  # m2 is just one module
        m2 = [m2]

    for i, item in enumerate(m2):
        if isinstance(item, C2f) or isinstance(item, SPPF):
            m2[i] = item.cv1

    prune_conv(m1, m2)


for name, m in model.named_modules():
    if isinstance(m, Bottleneck):
        prune_conv(m.cv1, m.cv2)

seq = model.model
for i in range(3, 9):
    if i in [6, 4, 9]: continue
    prune(seq[i], seq[i + 1])

detect: Detect = seq[-1]
last_inputs = [seq[15], seq[18], seq[21]]
colasts = [seq[16], seq[19], None]
for last_input, colast, cv2, cv3 in zip(last_inputs, colasts, detect.cv2, detect.cv3):
    prune(last_input, [colast, cv2[0], cv3[0]])
    prune(cv2[0], cv2[1])
    prune(cv2[1], cv2[2])
    prune(cv3[0], cv3[1])
    prune(cv3[1], cv3[2])

for name, p in yolo.model.named_parameters():
    p.requires_grad = True

# yolo.val() # 剪枝模型进行验证 yolo.val(workers=0)
# yolo.export(format="onnx") # 导出为onnx文件
# yolo.train(data="VOC.yaml", epochs=100) # 剪枝后直接训练微调

torch.save(yolo.ckpt, "prune.pt")

print("done")

我们通过上述代码可以完成剪枝工作并将剪枝好的模型进行保存,用于finetune,有以下几点说明:

  • 在本次剪枝中我们利用factor变量来控制剪枝的保留率

  • 我们用来剪枝的模型一定是约束训练的模型,即对BN层加上L1正则化后训练的模型

  • 约束训练后的b.min().item值非常小,接近于0或者等于0,可以依据此来判断加载的模型是否正确

  • 我们可以选择将yolo.train()取消注释,在剪枝完成直接进入微调训练,博主在这里选择先保存剪枝模型

  • 我们可以选择yolo.export()取消注释,将剪枝完成后的模型导出为ONNX,查看对应的大小和channels是否发生改变,以此确认我们完成了剪枝

  • yolo.val()用于进行模型验证,建议取消注释进行相关验证,之前梁老师说yolo.val()验证的mAP值完全等于0是不正常的,需要检查下剪枝过程是否存在错误,最好是有一个值,哪怕非常小,博主剪枝后进行验证的结果如下图所示,可以看到mAP值真的是惨不忍睹(🤣),所以后续需要finetune模型来恢复我们的精度

在这里插入图片描述

在我们打开yolo.val()进行模型剪枝任务时可能会出现如下问题,这个错误出现在Windows下面,原因在于Linux系统中可以使用多个子进程加载程序,而Windows则不能,解决办法就是将workers设置为0,将yolo.val()修改为如下代码,参考自解决RuntimeError: An attempt has been made to start a new process before…办法

yolo.val(workers=0)

在这里插入图片描述

我们拿到剪枝后的model后可以导出为ONNX,来看看剪枝前后模型的差异性,首先从模型大小来看,剪枝前的ONNX模型大小为42.6MB,剪枝后的ONNX模型大小为35.4MB,然后从ONNX模型对比图来看,channels发生了变化,具体可看下面的示例图,可以看到剪枝前后Conv的通道数发生了明显的变化。这里提供博主剪枝好的权重文件下载链接Baidu Driver[pwd:yolo]

在这里插入图片描述

4. finetune

拿到剪枝的模型后,我们需要先做两件事情

  • 1.切记!!!在进行finetune之前需要将我们在trainer.py为BN层添加的L1正则化的代码注释掉(也就是我们在第2节添加的内容)
  • 2.切记!!!剪枝后不要从yaml导入结构。如果我们直接将剪枝后的模型prune.pt放到v8/detect目录下修改default.yaml文件,然后点击train.py是会存在问题的,此时模型结构是通过yolov8.yaml加载的,而我们并没有修改yaml文件,因此finetune的模型其实并不是剪枝的模型

因此,正常finetune训练的步骤如下:

  • 1.在yolo/engine/trainer.py中注释掉为BN层加L1正则化的代码

  • 2.修改yolo/engine/model.py代码,让其不要从yaml导入网络结构,具体修改内容是BaseTrainer类中的setup_model方法中,代码大概在443行左右,新增一行代码即可,如下所示

    # ========== yolo/engine/trainer.py的443行 ==========
    self.model = self.get_model(cfg=cfg, weights=weights, verbose=RANK == -1)  # calls Model(cfg, weights)
    
    # ========== 新增该行代码 ==========
    self.model = weights
    
    return ckpt
    
  • 3.将剪枝完保存的模型放到yolo/v8/detect文件夹下

  • 4.修改default.yaml文件,主要修改model为prune.pt即剪枝完的模型,具体修改如下:

    model: prune.pt  # path to model file, i.e. yolov8n.pt, yolov8n.yaml
    
  • 5.点击train.py开始训练即可,博主在这里选择的是微调50个epoch,大家根据自己的实际情况来,尽可能的多finetune几个epoch

微调50个epoch后模型的表现如下图所示,可以看到精度恢复得还可以,可以训练更多epoch使其精度更加稳定。这里提供博主训练好的权重文件下载链接Baidu Driver[pwd:yolo]

在这里插入图片描述

OK!至此,YOLOv8模型剪枝训练部分完成,下面来开始部署剪枝模型🚀🚀🚀

二、YOLOv8模型剪枝部署

Jetson nano上yolov8的部署使用到的Github仓库是infer。想了解通过TensorRTLayer API一层层完成模型的搭建工作可参考Jetson嵌入式系列模型部署-2,想了解通过TensorRTONNX parser解析ONNX文件来完成模型的搭建工作可参考Jetson嵌入式模型部署-3、Jetson nano部署YOLOv7。本文主要利用infer来对剪枝后的yolov8完成部署,本文参考自Jetson nano部署YOLOv8,具体流程该文描述非常详细,这里再简单过一遍,本次训练的模型使用yolov8s.pt,类别数为20,数据集是VOC数据集,部署的模型是经过剪枝后finetune的模型。

1. 源码下载

infer的代码可以直接从github官网上下载,源码下载地址是https://github.com/shouxieai/infer,由于infer部署框架刚发布不久一个固定的版本都没有,故只能采用主分支进行yolov8的部署工作(PS:由于代码更新频繁,可能大家会遇到不同的bug)。Linux下代码克隆指令如下

$ git clone https://github.com/shouxieai/infer.git

也可手动点击下载,点击右上角的Code按键,将代码下载下来。至此整个项目就已经准备好了。也可以点击here[pwd:infer]下载博主准备好的源代码(注意代码下载于2023/3/15日,若有改动请参考最新)

2. 环境配置

需要使用的软件环境有TensorRT、CUDA、CUDNN、OpenCV。所有软件环境在JetPack镜像中已经安装完成,只需要添加下trtexec工具的环境变量即可。博主使用的jetpack版本为JetPack4.6.1(PS:关于jetson nano刷机就不再赘述了,需要各位看官自行配置好相关环境😄,外网访问较慢,这里提供Jetson nano的JetPack镜像下载链接Baidu Drive[password:nano]【更新完毕!!!】几个(PS:提供4.6和4.6.1两个版本,注意4GB和2GB的区别,不要刷错了),关于Jetson Nano 2GB和4GB的区别可参考链接Jetson NANO是什么?如何选?。(吐槽下这玩意上传忒慢了,超级会员不顶用呀,终于上传完了,折磨!!!)

在这里插入图片描述

2.1 trtexec环境变量设置

trtexec环境变量的添加主要参考这里,主要包含以下几步

1.打开bashrc文件

vim ~/.bashrc

2.按i进入输入模式,在最后一行添加如下语句

export PATH=/usr/src/tensorrt/bin:$PATH

3.按下esc,输入:wq!保存退出即可,最后刷新下环境变量

source ~/.bashrc

3. ONNX导出

  • 训练的模型使用yolov8n.pt,torch版本1.12.1,onnx版本1.13.1
  • 参考自YoloV8的动态静态batch如何理解和使用

关于静态batch和动态batch有以下几点说明,更多细节请查看视频

静态batch

  • 导出的onnx指定所有维度均为明确的数字,是静态shape模型
  • 在推理的时候,它永远都是同样的batch推理,即使你目前只有一个图推理,它也需要n个batch的耗时
  • 适用于大部分场景,整个代码逻辑非常简单

动态batch

  • 导出的时候指定特定维度为dynamic,也就是不确定状态
  • 模型推理时才决定所需推理的batch大小,耗时最优,但onnx复杂度提高了
  • 适用于如server有大量不均匀的请求时的场景

说明:本次为了方便仅使用静态batch,关于动态batch的使用可参考Jetson nano部署YOLOv8

3.1 Transpose节点的添加

将剪枝训练好的权重finetune_best.pt放在ultralytics-main主目录下,新建导出文件export.py,内容如下,执行完成后会在当前目录生成导出的finetune_best.onnx模型

from ultralytics import YOLO

yolo = YOLO("finetune_best.pt")

yolo.export(format="onnx", batch=1)

模型需要完成修改才能正确被infer框架使用,正常模型导出的输出为[1,24,8400],其中1代表batch,24分别代表cx,cy,w,h以及VOC中20个类别分数,8400代表框的个数。首先infer框架的输出只支持[1,8400,24]这种形式的输出,因此我们需要再原始的onnx的输出之前添加一个Transpose节点,infer仓库workspace/v8trans.py就是帮我们做这么一件事情,v8trans.py具体内容如下:

# v8trans.py
import onnx
import onnx.helper as helper
import sys
import os

def main():

    if len(sys.argv) < 2:
        print("Usage:\n python v8trans.py yolov8n.onnx")
        return 1

    file = sys.argv[1]
    if not os.path.exists(file):
        print(f"Not exist path: {file}")
        return 1

    prefix, suffix = os.path.splitext(file)
    dst = prefix + ".transd" + suffix

    model = onnx.load(file)
    node  = model.graph.node[-1]

    old_output = node.output[0]
    node.output[0] = "pre_transpose"

    for specout in model.graph.output:
        if specout.name == old_output:
            shape0 = specout.type.tensor_type.shape.dim[0]
            shape1 = specout.type.tensor_type.shape.dim[1]
            shape2 = specout.type.tensor_type.shape.dim[2]
            new_out = helper.make_tensor_value_info(
                specout.name,
                specout.type.tensor_type.elem_type,
                [0, 0, 0]
            )
            new_out.type.tensor_type.shape.dim[0].CopyFrom(shape0)
            new_out.type.tensor_type.shape.dim[2].CopyFrom(shape1)
            new_out.type.tensor_type.shape.dim[1].CopyFrom(shape2)
            specout.CopyFrom(new_out)

    model.graph.node.append(
        helper.make_node("Transpose", ["pre_transpose"], [old_output], perm=[0, 2, 1])
    )

    print(f"Model save to {dst}")
    onnx.save(model, dst)
    return 0

if __name__ == "__main__":
    sys.exit(main())

在命令行终端输入如下指令即可添加Transpose节点,执行完成之后在当前目录下生成finetune_best.transd.onnx模型,该模型添加了Transpose节点。

python v8trans.py finetune_best.onnx

下图对比了原始的finetune_best.onnx和finetune_best.transd.onnx之间的区别,从图中可以看出转换后的onnx模型在输出之前多了一个Transpose节点,且输出的1,2维度进行了交换,符合infer框架。

在这里插入图片描述

3.2 Resize节点解析的问题

先剧透下,当使用trtexec工具构建engine时会发生错误,我们一并解决,到时候可以直接生成engine,错误信息如下图所示,大概意思就是说Resize_118这个节点的scales没有初始化(应该是这样理解的吧🤔)

在这里插入图片描述

我们先通过Netron工具打开finetune_best.transd.onnx模型查看下Resize_118这个节点的相关信息,在找找其它的Resize节点对比看看,如下图所示,左边是Resize_102节点的相关信息,右边是Resize_118节点的相关信息,可以看到其对应的Scales确实存在区别,Resize_118节点Scales没有initializer,没有明确的值。

在这里插入图片描述

下面来看解决方案onnxsim

onnxoptimizer、onnxsim被誉为onnx的优化利器,其中onnxsim可以优化常量,onnxoptimizer可以对节点进行压缩,参考自onnxoptimizer、onnxsim使用记录。新建一个v8onnxsim.py文件,用于优化onnx文件,具体内容如下:

import onnx
from onnxsim import simplify

onnx_model = onnx.load("finetune_best.transd.onnx")
model_simp, check = simplify(onnx_model)
assert check, "Simplified ONNX model could not be Validated"
onnx.save(model_simp, "finetune_best.transd.sim.onnx")

运行后会在当前文件夹生成一个finetune_best.transd.sim.onnx模型,现在可以查看对应的Resize_118节点发生了改变

在这里插入图片描述

至此,模型导出已经完毕,后续通过导出的模型完成在jetson nano上的部署工作,导出的模型文件可点击here[pwd:yolo]下载

4. 运行

4.1 engine生成

与tensorRT_Pro模型构建方式不同,infer框架直接通过trtexec工具生成engine,infer框架拥有一个全新的tensorrt封装,可轻易继承各类任务,相比于tensorRT_Pro优点如下:

  • 轻易实现各类任务的生产者和消费者模型,并进行高性能推理
  • 没有复杂的封装,彻底解开耦合!
  • 参考自如何高效使用TensorRT

将第3节导出的ONNX模型放入到infer/workspace文件夹下,然后在jetson nano终端执行如下指令(以导出的静态batch模型为例)

trtexec --onnx=workspace/finetune_best.transd.sim.onnx --saveEngine=workspace/finetune_best.transd.sim.engine

在这里插入图片描述

模型构建完成后如下图所示,engine拿到手后就可以开工了👨‍🏭

在这里插入图片描述

:导出动态batch模型执行的指令与静态batch不同!!!,具体可参考infer/workspace/build.sh文件中的内容,指令如下:

trtexec --onnx=finetune_best.transd.sim.onnx --minShapes=images:1x3x640x640 --maxShapes=images:16x3x640x640 --optShapes=images:1x3x640x640 --saveEngine=finetune_best.transd.sim.engine

4.2 源码修改

yolo模型的推理代码主要在src/main.cpp文件中,需要推理的图片放在workspace/inference文件夹中,源码修改较简单主要有以下几点:

  • 1.main.cpp 134,135行注释,只进行单张图片的推理

  • 2.main.cpp 104行 修改加载的模型为finetune_best.transd.sim.engine且类型为V8

  • 3.main.cpp 10行 新增voclabels数组,添加voc的类别名称

  • 4.mian.cpp 115行 cocolabels修改为voclabels

具体修改如下

int main() {
  // perf();					//修改1 134 135行注释
  // batch_inference();			
  single_inference();
  return 0;
}

auto yolo = yolo::load("finetune_best.transd.sim.engine", yolo::Type::V8);	//  修改2

static const char *voclabels[] = {"aeroplane",   "bicycle", "bird",   "boat",       "bottle",
                                  "bus",         "car",     "cat",    "chair",      "cow",
                                  "diningtable", "dog",     "horse",  "motorbike",  "person",
                                  "pottedplant",  "sheep",  "sofa",   "train",      "tvmonitor"};	// 修改3 新增voclabels数组

auto name = voclabels[obj.class_label]		// 修改4 cocolabels修改为mylabels

4.3 编译运行

编译用到的Makefile文件需要修改,修改后的Makefile文件如下,详细的Makefile文件的分析可查看Makefile实战

cc        := g++
nvcc      = /usr/local/cuda-10.2/bin/nvcc

cpp_srcs  := $(shell find src -name "*.cpp")
cpp_objs  := $(cpp_srcs:.cpp=.cpp.o)
cpp_objs  := $(cpp_objs:src/%=objs/%)
cpp_mk	  := $(cpp_objs:.cpp.o=.cpp.mk)

cu_srcs	  := $(shell find src -name "*.cu")
cu_objs   := $(cu_srcs:.cu=.cu.o)
cu_objs	  := $(cu_objs:src/%=objs/%)
cu_mk	  := $(cu_objs:.cu.o=.cu.mk)

include_paths := src        \
			/usr/include/opencv4 \
			/usr/include/aarch64-linux-gnu \
			/usr/local/cuda-10.2/include

library_paths := /usr/lib/aarch64-linux-gnu \
			/usr/local/cuda-10.2/lib64

link_librarys := opencv_core opencv_highgui opencv_imgproc opencv_videoio opencv_imgcodecs \
			nvinfer nvinfer_plugin nvonnxparser \
			cuda cublas cudart cudnn \
			stdc++ dl

empty		  :=
export_path   := $(subst $(empty) $(empty),:,$(library_paths))

run_paths     := $(foreach item,$(library_paths),-Wl,-rpath=$(item))
include_paths := $(foreach item,$(include_paths),-I$(item))
library_paths := $(foreach item,$(library_paths),-L$(item))
link_librarys := $(foreach item,$(link_librarys),-l$(item))

cpp_compile_flags := -std=c++11 -fPIC -w -g -pthread -fopenmp -O0
cu_compile_flags  := -std=c++11 -g -w -O0 -Xcompiler "$(cpp_compile_flags)"
link_flags        := -pthread -fopenmp -Wl,-rpath='$$ORIGIN'

cpp_compile_flags += $(include_paths)
cu_compile_flags  += $(include_paths)
link_flags        += $(library_paths) $(link_librarys) $(run_paths)

ifneq ($(MAKECMDGOALS), clean)
-include $(cpp_mk) $(cu_mk)
endif

pro	   := workspace/pro
expath := library_path.txt

library_path.txt : 
	@echo LD_LIBRARY_PATH=$(export_path):"$$"LD_LIBRARY_PATH > $@

workspace/pro : $(cpp_objs) $(cu_objs)
		@echo Link $@
		@mkdir -p $(dir $@)
		@$(cc) $^ -o $@ $(link_flags)

objs/%.cpp.o : src/%.cpp
	@echo Compile CXX $<
	@mkdir -p $(dir $@)
	@$(cc) -c $< -o $@ $(cpp_compile_flags)

objs/%.cu.o : src/%.cu
	@echo Compile CUDA $<
	@mkdir -p $(dir $@)
	@$(nvcc) -c $< -o $@ $(cu_compile_flags)

objs/%.cpp.mk : src/%.cpp
	@echo Compile depends CXX $<
	@mkdir -p $(dir $@)
	@$(cc) -M $< -MF $@ -MT $(@:.cpp.mk=.cpp.o) $(cpp_compile_flags)
	
objs/%.cu.mk : src/%.cu
	@echo Compile depends CUDA $<
	@mkdir -p $(dir $@)
	@$(nvcc) -M $< -MF $@ -MT $(@:.cu.mk=.cu.o) $(cu_compile_flags)

run   : workspace/pro
		  @cd workspace && ./pro

clean :
	@rm -rf objs workspace/pro
	@rm -rf library_path.txt
	@rm -rf workspace/Result.jpg

# 导出符号,使得运行时能够链接上
export LD_LIBRARY_PATH:=$(export_path):$(LD_LIBRARY_PATH)

OK!源码也修改好了,Makefile文件也搞定了,可以编译运行了,直接在终端执行如下指令即可

make run

图解如下所示:

在这里插入图片描述

编译运行后的将在worksapce下生成Result.jpg为推理后的图片,如下所示,可以看到效果还是比较OK的。

在这里插入图片描述

4.4 拓展-摄像头检测

简单写了一个摄像头检测的demo,主要修改以下几点:

  • 1.main.cpp 新增yolo_video_demo()函数,具体内容参考下面

  • 2.main.cpp 新增调用yolo_video_demo()函数代码,具体内容参考下面

static void yolo_video_demo(const string& engine_file){		// 修改1 新增函数
  auto yolo = yolo::load(engine_file, yolo::Type::V8);
  if (yolo == nullptr)  return;
  
  // auto remote_show = create_zmq_remote_show();

  cv::Mat frame;
  cv::VideoCapture cap(0);
  if (!cap.isOpened()){
    printf("Engine is nullptr");
    return;
  }

  while(true){
    cap.read(frame);
    auto objs = yolo->forward(cvimg(frame));
    
    for(auto &obj : objs) {
      uint8_t b, g, r;
      tie(b, g, r) = yolo::random_color(obj.class_label);
      cv::rectangle(frame, cv::Point(obj.left, obj.top), cv::Point(obj.right, obj.bottom),
                    cv::Scalar(b, g, r), 5);
      
      auto name = voclabels[obj.class_label];
      auto caption = cv::format("%s %.2f", name, obj.confidence);
      int width = cv::getTextSize(caption, 0, 1, 2, nullptr).width + 10;
      cv::rectangle(frame, cv::Point(obj.left - 3, obj.top - 33),
                    cv::Point(obj.left + width, obj.top), cv::Scalar(b, g, r), -1);
      cv::putText(frame, caption, cv::Point(obj.left, obj.top - 5), 0, 1, cv::Scalar::all(0), 2, 16);
      imshow("frame", frame);
      // remote_show->post(frame);
      int key = cv::waitKey(1);
      if (key == 27)
          break;
    }
  }

  cap.release();
  cv::destroyAllWindows();
  return;
}

int main() {	// 修改2 调用该函数
  // perf();
  // batch_inference();
  // single_inference();
  yolo_video_demo("finetune_best.transd.sim.engine");
  return 0;
}

修改完成后执行make run即可看到对应的画面显示了

在这里插入图片描述

三、讨论

讨论1:我们来看看剪枝前后模型的差异性,首先,先来看mAP指标,剪枝前mAP50为0.808,剪枝后mAP50为0.789(:mAP略微下降,可能是迭代次数epoch太少)。

再来看检测速度,从两方面来看:

第一方面从trtexec工具构建的模型来看,我们只需要关注一个数字,就是latency中的mean,它代表平均推理一张图的耗时,但是不包含前后处理,不进行剪枝的模型使用trtexec构建工具,其latency中的mean为161.974ms(下图1);进行剪枝的模型使用trtexec构建工具,其latency中的mena为133.107ms(下图2);剪枝后的模型快了将近30ms,对比可知模型剪枝后的推理速度明显提高了

第二方面从infer中的perf()函数来看,我们只需要关注[BATCH1]的耗时,因为我们没有设置动态batch,且静态batch设置为1,它也代表平均一张图的耗时,但是是包含前后处理的,不进行剪枝的模型使用perf()函数测试性能,其BATCH1的耗时为166ms(下图3),进行剪枝的模型使用perf()函数测试性能,其BATCH1的耗时为137ms(下图4);剪枝后的模型快了将近30ms,从这里对比也可知模型剪枝后的推理速度提高了。

额外补充一句,将perf()函数测试的耗时减去trtexec工具测试的耗时就可以得出前后处理的时间,大概计算下,在Jetson nano上YOLOv8的前后处理时间仅仅耗时4ms左右,不得不佩服杜老师呀😂

在这里插入图片描述

图1 不进行剪枝模型的trtexec工具耗时(161.974ms)

在这里插入图片描述

图2 进行剪枝模型的trtexec工具耗时(133.107ms)

在这里插入图片描述

图3 不进行剪枝模型的perf函数推理耗时(BATCH1为166ms)

在这里插入图片描述

图4 进行剪枝模型的perf函数推理耗时(BATCH1为137ms)

讨论2:在之前的剪枝代码中,我们始终保持着channels大于8,小于8的时候我们就降低阈值,选择更多的通道数。可以看到剪枝后的channels千奇百怪,没有规律,而原始的ONNX的通道数都是8的倍数。我记得梁老师说过,对于保留的channels,它应该整除n才是最合适的,也就是说我们在prune剪枝的时候应该控制下channels的数量,让它能整除n,因为NVIDIA的硬件加速更加合适。n该如何选择呢?一般FP16模型,n选取8;INT8模型,n选取16。

罗里吧嗦一大堆,就是想表达下如果我们需要对剪枝后的模型进行量化加速时(比如利用NVIDIA的tensorRT),是不是不应该向上面剪枝剪得那么随意呢?🤔,不然channels无法整除8或者16呀,而当量化生成FP16或者INT8模型时,硬件加速就不太顶了呀

讨论3:在约束训练添加的代码中,我们将L1正则化的约束设置为1e-2,在剪枝即prune.py的代码中,我们将剪枝保留率设置为0.8,这些参数其实都是超参数,需要考虑模型复杂度和性能的平衡,对于不同的网络以及不同的需求这些超参数可能是不一致的。

结语

本篇博客简单重新实现了下之前梁老师讲解的YOLOv8模型剪枝,然后将剪枝后的模型部署到Jetson nano上,在mAP值略微下降的前提下(mAP下降0.019),大大提高了检测速度(推理速度快了30ms),也算把流程都简单过了一遍吧。博主在这里只做了最简单的实现,并没有做原理分析,具体原理和细节那就需要各位看官自行去了解啦😄。感谢各位看到最后,创作真心不易,读后有收获的看官请帮忙点个👍⭐️。

下载链接

  • 原始VOC数据集下载链接[pwd:yolo]

  • VOC权重文件下载链接[pwd:yolo]

  • JetPack镜像下载链接Baidu Drive[pwd:nano]【更新完毕!!!】

参考

  • 剪枝与重参第七课:YOLOv8剪枝

  • Jetson nano部署YOLOv8

  • YOLOv8

  • 解决RuntimeError: An attempt has been made to start a new process before…办法

  • infer

  • Jetson嵌入式系列模型部署-1

  • Jetson嵌入式系列模型部署-2

  • Jetson嵌入式系列模型部署-3

  • Jetson nano部署YOLOv7

  • Jetson nano部署YOLOv8

  • Jetson NANO是什么?如何选?

  • trtexec添加环境变量

  • YoloV8的动态静态batch如何理解和使用【视频】

  • Netron可视化工具

  • onnxoptimizer、onnxsim使用记录

  • 如何高效使用TensorRT【视频】

  • Makefile实战

  • Jetson nano部署YOLOv8

  • Jetson NANO是什么?如何选?

  • trtexec添加环境变量

  • YoloV8的动态静态batch如何理解和使用【视频】

  • Netron可视化工具

  • onnxoptimizer、onnxsim使用记录

  • 如何高效使用TensorRT【视频】

  • Makefile实战

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

FIFO的工作原理及其设计

1.简介 FIFO( First Input First Output)简单说就是指先进先出。FIFO存储器是一个先入先出的双口缓冲器&#xff0c;即第一个进入其内的数据第一个被移出&#xff0c;其中一个口是存储器的输入口&#xff0c;另一个口是存储器的输出口。 对于单片FIFO来说&#xff0c;主要有两种…

SHELL函数可课后作业

一、题目 1、编写函数&#xff0c;实现打印绿色OK和红色FAILED 判断是否有参数&#xff0c;存在为Ok&#xff0c;不存在为FAILED 2、编写函数&#xff0c;实现判断是否无位置参数&#xff0c;如无参数&#xff0c;提示错误 3、编写函数实现两个数字做为参数&#xff0c;返回最…

多线程 之 CAS与synchronized的优化过程

前言 本篇介绍什么是CAS与synchronized的优化过程&#xff0c;如有错误&#xff0c;请在评论区指正&#xff0c;让我们一起交流&#xff0c;共同进步&#xff01; 文章目录前言1. 什么是CAS&#xff1f;2. CAS实现的操作2.1 实现原子类2.2 实现自旋锁3. CAS的aba问题4. synchr…

【无人机】基于灰狼优化算法的无人机路径规划问题研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

链式二叉树及相关操作(前,中,后,层序遍历)

欢迎来到 Claffic 的博客 &#x1f49e;&#x1f49e;&#x1f49e; “春来无事&#xff0c;只为花忙。” 前言: 上一期给大家介绍了二叉树的一种顺序结构&#xff1a;堆&#xff0c;这一期承接上一期&#xff0c;给大家继续介绍二叉树的另一种结构&#xff1a;链式结构。 目录…

golang指针相关

指针相关的部分实在是没有搞太明白&#xff0c;抽时间来总结下。 1.指针相关基础知识 比如现在有一句话&#xff1a;『谜底666』&#xff0c;这句话在程序中一启动&#xff0c;就要加载到内存中&#xff0c;假如内存地址0x123456&#xff0c;然后我们可以将这句话复制给变量A&…

多线程(八):常见锁策略

目录 前言 1. 乐观锁 VS 悲观锁 乐观锁 悲观锁 2. 轻量级锁 VS 重量级锁 轻量级锁 3. 自旋锁 VS 挂起等待锁 自旋锁 挂起等待锁 4. 读写锁 VS 互斥锁 5. 可重入锁 vs 不可重入锁 死锁 发生死锁的情况 死锁产生的四个必要条件如下&#xff1a; 6. 公平锁和非公平锁…

【Java EE】-多线程编程(九) 锁策略CAS锁优化

作者&#xff1a;学Java的冬瓜 博客主页&#xff1a;☀冬瓜的主页&#x1f319; 专栏&#xff1a;【JavaEE】 分享&#xff1a; 主要内容&#xff1a;乐观锁VS悲观锁、轻量级锁VS重量级锁、自旋锁VS挂起等待锁、互斥锁VS读写锁、公平锁VS非公平锁、可重入锁VS不可重入锁。CAS实…

Python数据结构与算法-树

一、树的概念详情见 https://blog.csdn.net/little_limin/article/details/129845592 Python数据结构与算法-堆排序&#xff08;NB组&#xff09;—— 一、树的基础知识二、树的实例&#xff1a;模拟文件系统1、树的存储树结构也是链式存储的&#xff0c;与链表的结构相似&…

类ChatGPT代码级解读:如何从零起步实现Transformer、llama/ChatGLM

前言 最近一直在做类ChatGPT项目的部署 微调&#xff0c;关注比较多的是两个&#xff1a;一个LLaMA&#xff0c;一个ChatGLM&#xff0c;会发现有不少模型是基于这两个模型去做微调的&#xff0c;说到微调&#xff0c;那具体怎么微调呢&#xff0c;因此又详细了解了一下微调代…

Vulnhub_Pylington

目录 一、信息收集 &#xff08;一&#xff09;端口服务探测 &#xff08;二&#xff09;目录扫描 二、漏洞挖掘 &#xff08;一&#xff09;robots敏感信息泄露 &#xff08;二&#xff09;python IDE沙箱绕过RCE 1. python敏感函数沙盒绕过 2. exec(__import_…

【ES】搜索结果处理RestClient查询文档

【ES】搜索结果处理&RestClient查询文档2.搜索结果处理2.1.排序2.1.1.普通字段排序2.1.2.地理坐标排序2.2.分页2.2.1.基本的分页2.2.2.深度分页问题2.2.3.小结2.3.高亮2.3.1.高亮原理2.3.2.实现高亮2.4.总结3.RestClient查询文档3.1.快速入门3.1.1.发起查询请求3.1.2.解析响…

Python做个猫狗识别系统,给人美心善的邻居

嗨害大家好鸭&#xff01;我是爱摸鱼的芝士❤ 宠物真的看着好治愈 谁不想有一只属于自己的乖乖宠物捏~ 这篇文章中我放弃了以往的model.fit()训练方法&#xff0c; 改用model.train_on_batch方法。 两种方法的比较&#xff1a; model.fit()&#xff1a;用起来十分简单&#…

Kubernetes 部署 StarRocks 集群

文章目录StarRocks简介系统架构图安装部署StarRocks手动部署通过 Docker部署使用 StarGo 部署管理通过 StarRocks Manager部署管理通过 Kubernetes部署工作原理逻辑图部署 StarRocks Operator部署 StarRocks 集群访问 StarRocks 集群集群内访问 StarRocks 集群集群外访问 StarR…

【案例实践】R语言多元数据统计分析在生态环境中的实践应用

查看原文>>>R语言生物群落分析绘图、多元统计分析、CMIP6、遥感碳储量、GEE林业、InVEST等 生态环境领域研究中常常面对众多的不同类型的数据或变量&#xff0c;当要同时分析多个因变量&#xff08;y&#xff09;时需要用到多元统计分析&#xff08;multivariate sta…

Spark----DataFrame和DataSet

Spark之DataFrame和DataSet 文章目录Spark之DataFrame和DataSetDataFrameDSL 语法创建DataFrame查看DataFrame的Schema信息只查看列数据的6种方式按照“age”分区&#xff0c;查看数据条数增加列withColumn修改列名withColumnRenamedRDD 转换为 DataFrameDataFrame 转换为 RDD转…

音质蓝牙耳机哪款好用?2023公认音质好的四款蓝牙耳机推荐

现如今&#xff0c;蓝牙耳机越来越受欢迎&#xff0c;不少人在听歌、追剧、甚至是玩游戏的时候都会戴着它。最近看到很多人问&#xff0c;音质蓝牙耳机哪款好用&#xff1f;针对这个问题&#xff0c;我来给大家推荐四款公认音质好的蓝牙耳机&#xff0c;一起来看看吧。 一、南…

算法笔记:Frechet距离度量

曲线之间相似性的度量&#xff0c;它考虑了沿曲线的点的位置和顺序 1 概念 1.1 直观理解 主人走路径A&#xff0c;狗走路径B&#xff0c;他们有不同的配速方案主人和狗各自走完这两条路径过程中所需要的最短狗绳长度 &#xff08;在某一种配速下需要的狗绳长度&#xff09;&a…

考研复试确认神操作!

终于进行到了研究生考试的尾声&#xff0c;但让考生感到无力吐槽的事情&#xff0c;却还在继续上演&#xff0c;比如苏科大&#xff0c;再比如中地大、苏大&#xff0c;三所学校的神操作&#xff0c;着实让无数考生忍不住调侃&#xff1a;原来考研不仅拼实力&#xff0c;还得拼…

你的APP内存还在暴增吗?试着用Bitmap管理下内存~

作者&#xff1a;layz4android 相信伙伴们在日常的开发中&#xff0c;一定对图片加载有所涉猎&#xff0c;而且对于图片加载现有的第三方库也很多&#xff0c;例如Glide、coil等&#xff0c;使用这些三方库我们好像就没有啥担忧的&#xff0c;他们内部的内存管理和缓存策略做的…