记一次ONNX问题排查(关联mmcls, mmdeploy)

起因是在opencls中,自定义的mobilenetv2的config文件,训练两个标签,数据量合计1000左右,转换到onnx模型后, b标签的精度严重降低。

特别感谢@hanrui1sensetime在我问题排查中提供的帮助

情况描述

自定义的mobilenetv2的config文件,训练两个标签,数据量合计1000左右,转换到onnx模型后, b标签的精度严重降低。

  • 使用训练生成的 best_accuracy_top-1_xxx.pth, 对val数据集进行测试,命令为 python tools/test.py configs\mobilenet_v2\mobilenet-v2_custom.py work_dirs\mobilenet-v2_custom\best_accuracy_top-1_xxx.pth --out data\my_dataset\result.json , 截取一部分的输出结果为:

    [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
    

    val中a,b 标签对半,看上去是正常的。

  • 不论使用 tools/deployment/pytorch2onnx.py,还是使用 mmdeploy生成的onnx文件,对val中b标签数据的准确率为31.2%.

  • mmcls的生成命令:

    python tools/deployment/pytorch2onnx.py \
    	configs\mobilenet_v2\mobilenet-v2_custom.py \
    	--checkpoint path/to/pth/best_accuracy_top-1_xxx.pth \
    	--output-file path/to/onnx/output.onnx
    
  • mmdeploy中的命令片段如下:

    python ./tools/deploy.py \
        configs/mmcls/classification_onnxruntime_dynamic.py \
        path/to/mmclassification/mobilenet-v2_custom.py \
        path/to/pth/best_accuracy_top-1_xxx.pth \
        path/to/img/val/b/a.jpg \
        --test-img path/to/img/val/b/b.jpg \
        --work-dir path/to/output \
        --device cpu \
        --log-level INFO \
        --show \
        --dump-info
    
  • pip list | grep "mmcv\|mmcls\|^torch" 命令的输出:

    • mmcls 0.23.1 d:\projects\github\mmclassification

    • mmcv-full 1.5.3

    • torch 1.8.2+cu111

    • torchaudio 0.8.2

    • torchvision 0.9.2+cu111

  • 自己的配置文件:

    _base_ = [
        '../_base_/models/mobilenet_v2_1x.py',
        '../_base_/schedules/imagenet_bs256_epochstep.py',
        '../_base_/default_runtime.py'
    ]
    
    dataset_type = 'CustomDataset'
    classes = ['a', 'b']
    
    img_norm_cfg = dict(
        mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True)
    train_pipeline = [
        dict(type='LoadImageFromFile'),
        dict(type='RandomResizedCrop', size=224, backend='pillow'),
        dict(type='RandomFlip', flip_prob=0.5, direction='horizontal'),
        dict(type='Normalize', **img_norm_cfg),
        dict(type='ImageToTensor', keys=['img']),
        dict(type='ToTensor', keys=['gt_label']),
        dict(type='Collect', keys=['img', 'gt_label'])
    ]
    test_pipeline = [
        dict(type='LoadImageFromFile'),
        dict(type='Resize', size=(256, -1), backend='pillow'),
        dict(type='CenterCrop', crop_size=224),
        dict(type='Normalize', **img_norm_cfg),
        dict(type='ImageToTensor', keys=['img']),
        dict(type='Collect', keys=['img'])
    ]
    
    data = dict(
        samples_per_gpu=64,
        workers_per_gpu=1,
        train=dict(
            type = dataset_type,
            data_prefix = 'data/my_dataset/train',
            classes = classes,
            pipeline=train_pipeline
        ),
        val=dict(
            type = dataset_type,
            data_prefix = 'data/my_dataset/val',
            classes = classes,
            pipeline=test_pipeline
        ),
        test=dict(
            type = dataset_type,
            data_prefix = 'data/my_dataset/test',
            classes = classes,
            pipeline=test_pipeline
        )
    )
    evaluation = dict(interval=1, save_best='auto', metric='accuracy', metric_options={'topk': (1, )})
    
    checkpoint_config = dict(interval=10)
    runner = dict(type='EpochBasedRunner', max_epochs=300)
    

想到的可能

  • 导出到pth时精度降低:测试无这个问题
  • pth到onnx时哪里不对:一开始我推测是这样,可能是模型哪里bug了,或者说导出工具没支持到算子,后来用了其他两个官方支持的模型,也出现了精度问题。总不能全部模型都出问题吧。

后续的交流

之后@hanrui1sensetime提醒了我,可以用官方模型,官方数据集来进行测试,确定是哪里的问题。因为按理来说,官方config是经过测试的。

于是我去测了。导出后的onnx依然测不准。于是我带着demo代码去提了issue。

import numpy as np
import onnxruntime as ort
from PIL import Image

session= ort.InferenceSession("path/to/end2end.onnx")
path = "path/to/img"
image = Image.open(path).resize((224,224))
image_data = np.array(image).transpose(2, 0, 1)

input_name = session.get_inputs()[0].name
label_name = session.get_outputs()[0].name
pred_onx = session.run([label_name], {input_name: [img_data]})[0]
print(pred_onx)
print(pred_onx.argmax())

然后回复是没有归一化…config那边做的预处理,这边也要再做一次。

下面是修复后的代码:

import numpy as np
import onnxruntime as ort
from PIL import Image

session= ort.InferenceSession("path/to/end2end.onnx")
path = "path/to/img"
image = Image.open(path).resize((224,224))
image_data = np.array(image).transpose(2, 0, 1).astype(np.float64)

mean=np.array([123.675, 116.28, 103.53])
image_data[0,:,:] -= mean[0]
image_data[1,:,:] -= mean[1]
image_data[2,:,:] -= mean[2]
std=np.array([58.395, 57.12, 57.375])
image_data[0,:,:] /= std[0]
image_data[1,:,:] /= std[1]
image_data[2,:,:] /= std[2]

input_name = session.get_inputs()[0].name
label_name = session.get_outputs()[0].name
pred_onx = session.run([label_name], {input_name: [image_data]})[0]
print(pred_onx)
print(pred_onx.argmax())

所以一定要多翻翻文档。但是这个好像也没说,也可能是我菜了。

C++部署,问题再现

本以为Python踩过坑了,在Cpp上会轻松很多,没想到C++也一样是重重困难。

首先是Cmake加依赖,demo用到的是onnxruntime和opencv。opencv还好,之前有踩过坑,所以很轻松。而onnxruntime给的预编译是不带cmake的,就需要手动加include和lib,又去翻了一遍cmake教程属于是。

当然,上面的困难还不算大困难,之后遇到了onnx的各种东西怎么调用,几经周转参考了这篇文章。就基本弄明白调用了。

c++大致流程是创建env等一大堆东西,在run的时候都丢进去

但是又遇到了之前的精度问题。这次当然是已经归一化了。于是考虑别的原因。

直到我打印输入数据的大小,比图像大了一倍!

然后就想起了我对代码的改动:input里,别人用array,我用的vector,array当然要预先声明长度,vector不需要啊!那我提前申请了一倍大小的vector,当然最终结果是2倍大小了。

笑死,光速改了代码,精度正常。

To be continued…

Q.E.D.