NPU 推理Yolov8n量化模型的性能提升

System :
Fenix 1.7.3 Ubuntu 22.04.5 LTS Linux 5.15.137
KSNN 版本:1.4
目前的问题:
训练完成的yolov8n模型在x86推理时精度很高,但是经过量化后的进度低的吓人。
平均精度低于0.001。且推理结果出现了错误。


在量化阶段我采用了如下的命令:
./convert --model-name yolov5s
–platform onnx
–model best.onnx
–mean-values ‘0 0 0 0.00390625’
–quantized-dtype asymmetric_affine
–source-files ./data/dataset/dataset2.txt
–batch-size 2
–iterations 177
–kboard VIM3
–print-level 1
我选用了640x640的训练集和val集的354张图片进行量化,并且在推理时的图片也是640x640大小。
但是选用我的图片进行量化后的精度下降了100倍。在使用原始的data进行量化时最高置信度为0.12.
–mean-values 这个参数有什么关联性。我在文档中找不到一点头绪。
大家有什么建议或者指导性的建议嘛。

Hello @pigpigfang ,

0.001这个精度不正常,听你的描述感觉是KSNN哪里出了问题。你有尝试过把你转换模型的batch-size参数改成1吗。

mean-values这个参数是模型的前处理归一化参数,前面三个参数是三个通道的均值,最后一个参数是方差。如果你使用的是YOLO官方的代码且没有对归一化参数做改动,你使用的这个参数是正确的。

batch-size=1时的置信度最高也是0.12,这个也算正常吗,f32的精度能到0.8的缺陷在量化模型上甚至检测不到

Hello @pigpigfang ,

比0.001正常了点,但精度丢的还是有点多,你可以试一下int8量化和int16量化。

1 Like

好的。我稍后尝试一下
官方站点的文档能都更新下,或者我来进行修改一下,写明注意事项。

Hello @pigpigfang ,

我这边联系文档负责人更新一下。batch-size不是1的时候KSNN的支持可能有bug。

可以把源页面文档开放出来,我来添加我使用成功的案例,随后你们审核。

Hello @pigpigfang ,

你可以把你添加的内容发给我,我这边添加进去。

模型训练:

第一步:
选择python版本为3.9 并克隆存储库
git clone https://github.com/ultralytics/ultralytics

3.10版本支持的torch版本>=1.11.0.
第二步
随后将tag切换为8.0.86
第三步
随后安装torch 1.8.0 ,原有的torch 1.10.1 可能会出错。有条件安装cuda版本.
第四步
修改
ultralytics/ultralytics/nn/modules.py中的

class Detect(nn.Module):
    """YOLOv8 Detect head for detection models."""
    dynamic = False  # force grid reconstruction
    export = False  # export mode
    shape = None
    anchors = torch.empty(0)  # init
    strides = torch.empty(0)  # init

    def __init__(self, nc=80, ch=()):  # detection layer
        super().__init__()
        self.nc = nc  # number of classes
        self.nl = len(ch)  # number of detection layers
        self.reg_max = 16  # DFL channels (ch[0] // 16 to scale 4/8/12/16/20 for n/s/m/l/x)
        self.no = nc + self.reg_max * 4  # number of outputs per anchor
        self.stride = torch.zeros(self.nl)  # strides computed during build
        c2, c3 = max((16, ch[0] // 4, self.reg_max * 4)), max(ch[0], self.nc)  # channels
        self.cv2 = nn.ModuleList(
            nn.Sequential(Conv(x, c2, 3), Conv(c2, c2, 3), nn.Conv2d(c2, 4 * self.reg_max, 1)) for x in ch)
        self.cv3 = nn.ModuleList(nn.Sequential(Conv(x, c3, 3), Conv(c3, c3, 3), nn.Conv2d(c3, self.nc, 1)) for x in ch)
        self.dfl = DFL(self.reg_max) if self.reg_max > 1 else nn.Identity()

    def forward(self, x):
        """Concatenates and returns predicted bounding boxes and class probabilities."""

        if torch.onnx.is_in_onnx_export():

            return self.forward_export(x)

        shape = x[0].shape  # BCHW
        for i in range(self.nl):
            x[i] = torch.cat((self.cv2[i](x[i]), self.cv3[i](x[i])), 1)
        if self.training:
            return x
        elif self.dynamic or self.shape != shape:
            self.anchors, self.strides = (x.transpose(0, 1) for x in make_anchors(x, self.stride, 0.5))
            self.shape = shape

        x_cat = torch.cat([xi.view(shape[0], self.no, -1) for xi in x], 2)
        if self.export and self.format in ('saved_model', 'pb', 'tflite', 'edgetpu', 'tfjs'):  # avoid TF FlexSplitV ops
            box = x_cat[:, :self.reg_max * 4]
            cls = x_cat[:, self.reg_max * 4:]
        else:
            box, cls = x_cat.split((self.reg_max * 4, self.nc), 1)
        dbox = dist2bbox(self.dfl(box), self.anchors.unsqueeze(0), xywh=True, dim=1) * self.strides
        y = torch.cat((dbox, cls.sigmoid()), 1)
        return y if self.export else (y, x)

    def bias_init(self):
        """Initialize Detect() biases, WARNING: requires stride availability."""
        m = self  # self.model[-1]  # Detect() module
        # cf = torch.bincount(torch.tensor(np.concatenate(dataset.labels, 0)[:, 0]).long(), minlength=nc) + 1
        # ncf = math.log(0.6 / (m.nc - 0.999999)) if cf is None else torch.log(cf / cf.sum())  # nominal class frequency
        for a, b, s in zip(m.cv2, m.cv3, m.stride):  # from
            a[-1].bias.data[:] = 1.0  # box
            b[-1].bias.data[:m.nc] = math.log(5 / m.nc / (640 / s) ** 2)  # cls (.01 objects, 80 classes, 640 img)

    def forward_export(self, x):
        results = []
        for i in range(self.nl):
            dfl = self.cv2[i](x[i]).contiguous()
            cls = self.cv3[i](x[i]).contiguous()
            results.append(torch.cat([cls, dfl], 1))

        return tuple(results)

这段代码,其中forward函数和forward_export 是被修改过的,注意切换tag分支。
第五步
训练自己的yolov8n的模型。

其他:基本一致。
就是需要切换分支以及torch,其他版本的torch没有尝试,但是
torch==1.8.0+cu111以及ultralytics tag 8.0.86 (代码分支,无需下载库)被证实可用。

还有一个问题就是,我在训练时的图像大小和量化的图片我应该如何选择大小呢。例如我的训练集都是14401080的图片,实际检测时也是14401080,如果将其resize为640*640会存在图像扭曲的现象。

Hello @pigpigfang ,

如果你不希望检测目标发生扭曲,你可以考虑对输入图像进行padding操作。将1440×1080的图片padding成1440×1440再resize成640×640。像这样。

我觉的我需要你的帮助了。
我尝试了一天,但是精度始终低于0.1。
但是板子用开源库中提供的内容精度能达到0.7到0.5.
可能我的量化存在问题。如果可以的话我可以发送邮件附件携带我的完好的yolov8模型和检测图片。

Hello @pigpigfang ,

提供一下原始的onnx模型,量化图片和测试图片,我这边下周看一下。
我的邮箱 louis.liu@wesion.com

已经发送邮件了。感谢帮助。 :grinning: :grinning: :grinning: :grinning: :grinning: :grinning:

Hello @pigpigfang ,

原模型精度偏低,使用int8量化精度量化精度低的模型损失会更大一点。这边试了一下int16量化,效果和原模型几乎差不多了。

另外,如果实际使用场景图片输入大小和你提供的测试图片大小相同,建议使用padding。这个宽高比直接resize形变比较大。

原模型结果

int16量化模型结果

转换命令

./convert --model-name yolov8n_customer \
               --platform onnx \
               --model yolov8n_customer.onnx \
               --mean-values '0 0 0 0.00392156' \
               --quantized-dtype dynamic_fixed_point \
               --qtype int16 --source-files ./dataset.txt \
               --kboard VIM3 --print-level 0 \
               --iterations 500

修改部分代码

def draw(image, boxes, scores, classes, padding_image):

    for box, score, cl in zip(boxes, scores, classes):
        x1, y1, x2, y2 = box
        print('class: {}, score: {}'.format(CLASSES[cl], score))
        print('box coordinate left,top,right,down: [{}, {}, {}, {}]'.format(x1, y1, x2, y2))
        x1 *= padding_image.shape[1]
        y1 *= padding_image.shape[0]
        x2 *= padding_image.shape[1]
        y2 *= padding_image.shape[0]
        left = max(0, np.floor(x1 + 0.5).astype(int))
        top = max(0, np.floor(y1 + 0.5).astype(int))
        right = min(image.shape[1], np.floor(x2 + 0.5).astype(int))
        bottom = min(image.shape[0], np.floor(y2 + 0.5).astype(int))

        cv.rectangle(image, (left, top), (right, bottom), (255, 0, 0), 2)
        cv.putText(image, '{0} {1:.2f}'.format(CLASSES[cl], score),
                    (left - 50, top + 20),
                    cv.FONT_HERSHEY_SIMPLEX,
                    0.6, (0, 0, 255), 2)


def init_ksnn():
    yolov8 = KSNN('VIM3')
    print(' |---+ KSNN Version: {} +---| '.format(yolov8.get_nn_version()))
    print('Start init neural network ...')
    yolov8.nn_init(library='./models/libnn_yolov8n_customer_int16.so', model='./models/yolov8n_customer_int16.nb', level=1)
    print('Done.')
    return yolov8



def get_input_data(input_folder,yolov8,output_folder):
    try:
        for filename in os.listdir(input_folder):
            if filename.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp')):  # 仅处理图片文件
                input_path = os.path.join(input_folder, filename)
                output_path = os.path.join(output_folder, filename)
                # 读取并预处理图片
                orig_img = cv.imread(input_path, cv.IMREAD_COLOR)
                orig_img_height, orig_img_width, _ = orig_img.shape
                
                if orig_img_height > orig_img_width:
                    new_width = orig_img_height
                    new_height = orig_img_height
                else:
                    new_width = orig_img_width
                    new_height = orig_img_width
                
                padding_img = np.zeros((new_height, new_width, 3))
                padding_img[:orig_img_height, :orig_img_width] = orig_img
                
                img = cv.resize(padding_img, (640, 640)).astype(np.float32)
                cv_img = list()
                img[:, :, 0] = img[:, :, 0] - mean[0]
                img[:, :, 1] = img[:, :, 1] - mean[1]
                img[:, :, 2] = img[:, :, 2] - mean[2]
                img = img / var[0]
                img = img.transpose(2, 0, 1)
                cv_img.append(img)

                # 模型推理
                print(f'Start inference on {filename} ...')
                start = time.time()
                data = yolov8.nn_inference(cv_img, platform='ONNX',reorder='2 1 0', output_tensor=3,output_format=output_format.OUT_FORMAT_FLOAT32)
                end = time.time()
                print(f'Done. Inference time: {end - start:.2f}s')

                input0_data = data[2]
                input1_data = data[1]
                input2_data = data[0]

                input0_data = input0_data.reshape(SPAN, LISTSIZE, GRID0, GRID0)
                input1_data = input1_data.reshape(SPAN, LISTSIZE, GRID1, GRID1)
                input2_data = input2_data.reshape(SPAN, LISTSIZE, GRID2, GRID2)

                input_data = list()
                input_data.append(np.transpose(input0_data, (2, 3, 0, 1)))
                input_data.append(np.transpose(input1_data, (2, 3, 0, 1)))
                input_data.append(np.transpose(input2_data, (2, 3, 0, 1)))

                boxes, scores, classes = yolov8_post_process(input_data)

                if boxes is not None:
                    draw(orig_img, boxes, scores, classes, padding_img)

                # 保存结果
                cv.imwrite(output_path, orig_img)
                print(f'Result saved to {output_path}')
    except Exception as e:
        print(f'Error occurred: {e}')
1 Like

如果可以,请帮忙删除一下上一条的图片。谢谢 :kissing_closed_eyes:

Hello @pigpigfang ,

已删除

1 Like

还有一个问题不知道你有没有遇到过,就是我在我本机的wsl中量化模型很快,但是我采用其他机器时会很慢。本机cpu时14900hx,远程的训练机使用的是13700k。这会导致明显差别还是因为其他什么可能的原因导致的量化速度慢吗?
image
卡在了这里。但是本机的wsl 并没有卡住。而是直接进入量化步骤。开始
image

Hello @pigpigfang ,

卡主的问题我们没有遇到过,我们一直是在自己搭的一个服务器上做量化,量化YOLOv8n 500张量化图片大概十多分钟。