PINTO0309 / onnx2tf

Self-Created Tools to convert ONNX files (NCHW) to TensorFlow/TFLite/Keras format (NHWC). The purpose of this tool is to solve the massive Transpose extrapolation problem in onnx-tensorflow (onnx-tf). I don't need a Star, but give me a pull request.
MIT License
712 stars 73 forks source link

Cannot use converted model with dynamic input shape #521

Closed sklum closed 1 year ago

sklum commented 1 year ago

Issue Type

Others

OS

Linux

onnx2tf version number

1.18.1

onnx version number

1.14.1

onnxruntime version number

1.16.0

onnxsim (onnx_simplifier) version number

0.4.31

tensorflow version number

2.14.0

Download URL for ONNX

https://drive.google.com/file/d/1XIRHjWYzWHwsZXOgcT4RLOJ6kxXE1BVT/view?usp=share_link

Parameter Replacement JSON

Unclear which parameters need replacement.

Description

Hi @PINTO0309, thanks for the great tool and all of your hard work.

I'm trying to convert a custom model from onnx to tensorflow to tfjs with a dynamic input shape and am having problems.

As an example, take the mobilenet0.25_Final.pth model from https://github.com/biubug6/Pytorch_Retinaface.

I'm converting from pytorch to onnx using the following (where the RetinaFace definition comes from here):

import torch
import onnx
import onnxruntime
import numpy as np
from model import RetinaFace

torch_model = RetinaFace(
    {
        "name": "mobilenet0.25",
        "min_sizes": [[16, 32], [64, 128], [256, 512]],
        "steps": [8, 16, 32],
        "variance": [0.1, 0.2],
        "clip": False,
        "loc_weight": 2.0,
        "gpu_train": True,
        "batch_size": 32,
        "ngpu": 1,
        "epoch": 250,
        "decay1": 190,
        "decay2": 220,
        "image_size": 640,
        "pretrain": False,
        "return_layers": {"stage1": 1, "stage2": 2, "stage3": 3},
        "in_channel": 32,
        "out_channel": 64,
    },
    "test",
)

torch_model.load_state_dict(
    torch.load(
        "./mobilenet0.25_Final.pth", map_location=torch.device("cpu")
    )
)

torch_model.eval()

output_file = "./retinaface.onnx"
example_input = torch.randn(1, 3, 448, 448, requires_grad=False)

torch_out = torch_model(example_input)

torch.onnx.export(
    torch_model,
    example_input,
    output_file,
    input_names=["input_1"],
    opset_version=18,
    dynamic_axes={"input_1": [2, 3]}
)

I check that the onnx model works on the python side with the following:

def onnx_forward(onnx_file, example_input):

    sess_options = onnxruntime.SessionOptions()
    session = onnxruntime.InferenceSession(onnx_file, sess_options, providers=['AzureExecutionProvider', 'CPUExecutionProvider'])
    input_name = session.get_inputs()[0].name
    output = session.run([], {input_name: example_input.numpy()})
    output = output[0]
    return output

example_dynamic_input = torch.randn(1, 3, 480, 640, requires_grad=False)
torch_dynamic_out = torch_model(example_dynamic_input)

onnx_model = onnx.load(output_file)
onnx.checker.check_model(onnx_model, full_check=True)

onnx_out = onnx_forward(output_file, example_input)
np.testing.assert_almost_equal(torch_out[0].data.numpy(), onnx_out, decimal=3)

onnx_dynamic_out = onnx_forward(output_file, example_dynamic_input)
np.testing.assert_almost_equal(torch_dynamic_out[0].data.numpy(), onnx_dynamic_out, decimal=3)

I then convert the onnx model to tf using:

onnx2tf -i retinaface.onnx -osd -o retinaface_tf -cotof

When running the above, the following is the first shape issue I have:

INFO: onnx_output_name: wa/fpn/Shape_3_output_0 tf_output_name: tf.compat.v1.shape/wa/fpn/Shape_3:0 shape: (4,) dtype: int64 validate_result: Skipped (Deleted or Shape Unmatched)

Beyond a number of errors / warnings like these, the model converts successfully, but when using it in my tfjs-based system (after converting with tensorflowjs_converter) I get the following shape mismatch at inference time:

Invalid TF_Status: 3
Message: Incompatible shapes: [1,28,28,64] vs. [1,64,28,64]

If remove the dynamic_axes, things work fine at the fixed input size. There are a number of layers with the [1,28,28,64] shape, so it's been challenging to track down which is the problematic layer.

FWIW, I've also tried wit the -kat and -nuo options.

This error doesn't happen during this workflow, but does the Alternatively, if the input OP has a dynamic dimension, use the-bor-oisoption to rewrite it to a static shape and try again. error message appearing in other places mean that dynamic shapes are not supported? Based on your recent commits I assume that they are indeed supported in some way.

Is there simply a need for a parameter replacement in my case or am I hitting an edge case in dynamic inputs somehow? Any guidance would be appreciated. Please let me know if more information / models would be helpful - I will provide what I can.

PINTO0309 commented 1 year ago

The price of onnx2tf's considerably higher model optimization efficiency compared to onnx-tensorflow is that the optimization operation may fail if there are two or more undefined dimensions in the input tensor. If there is a series of tensors with axis size None, the correct axis position will be lost in the process of model transformation.

image

Although the conversion of models with multiple undefined dimensions is originally supported, the probability of model conversion failure is higher, and the user must compensate for conversion errors. JSON files can be used to compensate for the axis transposition behavior of onnx2tf.

In the case of RetinaFace, there was an error in the axis correction of Gather and Concat. When the correction was instructed in JSON, the conversion was successful, and the inference operation could be performed without any problems and with variable axes.

If you do not understand what I am saying, I would not recommend dealing with a model that has a high conversion difficulty involving undefined dimensions.

When running the above, the following is the first shape issue I have:

INFO: onnx_output_name: wa/fpn/Shape_3_output_0 tf_output_name: tf.compat.v1.shape/wa/fpn/Shape_3:0 shape: (4,) dtype: int64 validate_result: Skipped (Deleted or Shape Unmatched)

Skipped (Deleted or Shape Unmatched) appears in all 1D OP output, so most can safely be ignored. If it appears in more than two dimensions of OP output, it suggests that the OP transposition operation has failed somewhere prior to that OP. In the case of your RetinaFace, I was getting a lot of these warning messages for all outputs above 2 dimensions immediately after Gather and Concat. The Gather and Concat operations are used to derive the tensor size for the Resize immediately following it. Thus, if onnx2tf misunderstands the Gather axis and the Concat axis, as you have posted this time, an inconsistent tensor will be generated by onnx2tf, resulting in an infeasible model. Wrong: [1,64,28,64]

image

https://github.com/PINTO0309/onnx2tf/releases/tag/1.18.2

pip install onnx2tf -U

wget https://github.com/PINTO0309/onnx2tf/releases/download/1.16.31/flatc.tar.gz \
  && tar -zxvf flatc.tar.gz \
  && sudo chmod +x flatc \
  && sudo mv flatc /usr/bin/
PINTO0309 commented 1 year ago

Redundant ONNX output from PyTorch was improved by performing a proprietary optimization to eliminate the need for JSON creation.

https://github.com/PINTO0309/onnx2tf/releases/tag/1.18.3

PINTO0309 commented 1 year ago

Good luck.

sklum commented 1 year ago

Thanks for all your help @PINTO0309. Everything here makes sense, but I wasn't able to get to this work today. I'll review in detail on Monday and let you know if I run into any additional issues with the updates.

sklum commented 1 year ago

Alright @PINTO0309, we're making great progress here. I can now run the dynamic input model without any shape issues on the tfjs side. However, I am having issues with the "correctness" of the output that I'm hoping you can help me with.

Circling back to the pytorch -> onnx conversion code above, I've changed the process to be as follows:

img = np.float32(np.ones([375,448,3]))
img -= (104, 117, 123)
img = img.transpose(2, 0, 1)
img = np.expand_dims(img, 0)
example_input = torch.from_numpy(img)
torch_out = torch_model(example_input)

This matches the general functionality in detect.py from here.

I have updated the onnx_forward function to return all the output tensors instead of the first. Then, as before, I check the model after conversion with:

onnx_model = onnx.load(output_file)
onnx.checker.check_model(onnx_model, full_check=True)

onnx_out = onnx_forward(output_file, example_input)
np.testing.assert_almost_equal(torch_out[1].data.numpy(), onnx_out[1], decimal=3)

This passes successfully. Printing the confidence outputs of these models (torch_out[1] and onnx_out[1]) at this point we get a (1, 6944, 2) tensor from both, where the values in the final dim of the tensor are [~1, ~0] across all 6944 elements. This makes sense as this model is trained and so it shouldn't be detecting anything in an input of ones.

Now, setting aside tfjs, if we run the tflite code you provided using the same input as above with a NHWC input instead of NCHW because of onnx2tf:

import numpy as np
import tensorflow as tf
from pprint import pprint
interpreter = tf.lite.Interpreter(model_path="retinaface_onnx_dynamic_float32.tflite")
tf_lite_model = interpreter.get_signature_runner()

img = np.float32(np.ones([375,448,3]))
img -= (104, 117, 123)
img = np.expand_dims(img, 0)

inputs = {
    'input_1': img,
}

tf_lite_output = tf_lite_model(**inputs)
print(f"[TFLite] Model Predictions shape: {tf_lite_output['525'].shape}")
print(f"[TFLite] Model Predictions shape: {tf_lite_output['606'].shape}")
print(f"[TFLite] Model Predictions shape: {tf_lite_output['607'].shape}")
print(f"[TFLite] Model Predictions:")
pprint(tf_lite_output)

The tf_lite_output for the confidences is:

 '607': array([[[4.6728298e-01, 5.3271705e-01],
        [4.7566253e-01, 5.2433747e-01],
        [5.1309991e-01, 4.8690012e-01],
        ...,
        [9.9740821e-01, 2.5917361e-03],
        [9.9948138e-01, 5.1866425e-04],
        [9.9974173e-01, 2.5822222e-04]]], dtype=float32)

Note that multiple elements that are [~.5, ~.5]. This is basically the same as the output you were showing in your comment above (and this aligns with what I'm getting on the tfjs side).

So, am I doing something wrong with the input shapes or transpositions here? What am I doing wrong such that the -cotof option isn't catching this? I assume something is going wrong with the expectation of transposition in the model, but it's not clear to me at the moment.

PINTO0309 commented 1 year ago

I was late checking the issue because I was training other models.

Models containing more than one None have a non-zero chance of making a transposition error. The checks performed in -cotof force a comparison between the NCHW tensor and the NHWC tensor.

Forced meaning compares, for example, NCHW: [1,9,128,9] with NHWC: [1,128,9,9]. To begin with, it is necessary to compare tensor values based on the assumption that the tensor shapes of ONNX and PyTorch are completely different from those of TensorFlow, so a brute force check is used to replace all combinations of each axis to find the arrangement with the smallest error before calculating the error.

Thus, if you are unlucky enough to have a model with a structure like [1,9,9,9,9] in the middle of the model, the check itself will succeed correctly, but the axis of the model transformation itself may still be wrong.

Structural checking of a model with multiple None is quite difficult even with the human eye, but for when such a situation arises, we have a function to check where we have made a mistake in transforming the model.

The -onimc option stops the conversion halfway through the model and outputs the model converted halfway through. It is a bit tedious work, but you need to try several transformations up to the midpoint of the model and run an inference test each time to see what part of the model you are mis-transposing.

For example,

# Split the model at the middle position for debugging
# Specify the output name of the OP
$ wget https://github.com/PINTO0309/onnx2tf/releases/download/0.0.2/resnet18-v1-7.onnx
$ onnx2tf -i resnet18-v1-7.onnx -onimc resnetv15_stage2_conv1_fwd resnetv15_stage2_conv2_fwd

Once you know where you have made a transposition error on an axis, you can use JSON to correct the transposition error.

For example,

https://github.com/PINTO0309/onnx2tf#parameter-replacement

https://github.com/PINTO0309/onnx2tf/blob/main/replace.json

# Parameter replacement (Resize,Transpose,Softmax)
$ rm replace.json
$ wget https://github.com/PINTO0309/onnx2tf/releases/download/1.1.27/human_segmentation_pphumanseg_2021oct.onnx
$ wget https://github.com/PINTO0309/onnx2tf/releases/download/1.1.27/replace.json
$ onnx2tf -i human_segmentation_pphumanseg_2021oct.onnx -prf replace.json

The reason why the -cotof check is likely to be Matches even though the axis is in the wrong position is OP, which does not rewrite the value itself, such as Gather. (This is a possibility, not a definitive list of problem areas for RetinaFace.)

It would be hard to blindly examine the wrong areas, so if I were you, I would venture to generate a fixed-resolution RetinaFace model and compare its structure with the model with None. This will make it easier to understand to some extent where Transpose is lacking, or conversely, where useless Transpose is extrapolated.

If I get enough time during the holidays I will check out the model too.