Unexpected input data type. Actual: (tensor(double)) , expected: (tensor(float))
Asked Answered
G

2

8

I am learning this new ONNX framework that allows us to deploy the deep learning (and others) model into production.

However, there is one thing I am missing. I thought that the main reason for having such a framework is so that for inference purposes e.g. when we have a trained model and want to use it in a different venv (where for example we cannot have PyTorch) the model still can be used.

I have preped a "from scratch" example here:

# Modules
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
from torch.utils.data import DataLoader, TensorDataset
import torchvision
import onnx
import onnxruntime
import matplotlib.pyplot as plt
import numpy as np


# %config Completer.use_jedi = False

# MNIST Example dataset
train_loader = torch.utils.data.DataLoader(
      torchvision.datasets.MNIST(
          'data', train=True, download=True,
          transform=torchvision.transforms.Compose([
              torchvision.transforms.ToTensor(),
          ])),
      batch_size=800)


# Take data and labels "by hand"
inputs_batch, labels_batch = next(iter(train_loader))


# Simple Model

class CNN(nn.Module):
    def __init__(self, in_channels, num_classes):
        super(CNN, self).__init__()
        self.conv1 = nn.Conv2d(in_channels=in_channels, 
                               out_channels = 10, kernel_size = (3, 3), stride = (1, 1), padding=(1, 1))
        self.pool = nn.MaxPool2d(kernel_size=(2, 2), stride = (2, 2))
        self.conv2 = nn.Conv2d(in_channels = 10, out_channels=16, kernel_size = (3, 3), stride = (1, 1), padding=(1, 1))
        self.fc1 = nn.Linear(16*7*7, num_classes)
        
    def forward(self, x):
        x = F.relu(self.conv1(x))
        x = self.pool(x)
        x = F.relu(self.conv2(x))
        x = self.pool(x)
        x = x.reshape(x.shape[0], -1)
        x = self.fc1(x)
        
        return x

# Training setting
device = 'cpu'
batch_size = 64
learning_rate = 0.001
n_epochs = 10

# Dataset prep
dataset = TensorDataset(inputs_batch, labels_batch)
TRAIN_DF = DataLoader(dataset = dataset, batch_size = batch_size, shuffle = True)

# Model Init
model = CNN(in_channels=1, num_classes=10)
optimizer = optim.Adam(model.parameters(), lr = learning_rate)


# Training Loop
for epoch in range(n_epochs):
    for data, labels in TRAIN_DF:
        model.train()
        # Send Data to GPU
        data = data.to(device)
        # Send Data to GPU
        labels = labels.to(device)
        
#       data = data.reshape(data.shape[0], -1)
        
        # Forward
        pred = model(data)
        loss = F.cross_entropy(pred, labels)
        
        # Backward
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        
# Check Accuracy
def check_accuracy(loader, model):
    num_correct = 0
    num_total = 0
    
    model.eval()
    
    with torch.no_grad():
        for x, y in loader:
            x = x.to(device)
            y = y.to(device)

    #             x = x.reshape(x.shape[0], -1)

            scores = model(x)
            _, pred = scores.max(1)

            num_correct += (pred == y).sum()
            num_total += pred.size(0)

    print(F"Got {num_correct} / {num_total} with accuracy {float(num_correct)/float(num_total)*100: .2f}")
        
check_accuracy(TRAIN_DF, model)

# Inference with ONNX


# Create Artifical data of the same size
img_size = 28
dummy_data = torch.randn(1, img_size, img_size)
dummy_input = torch.autograd.Variable(dummy_data).unsqueeze(0)

input_name = "input"
output_name = "output"

model_eval = model.eval()

torch.onnx.export(
    model_eval,
    dummy_input,
    "model_CNN.onnx",
    input_names=["input"],
    output_names=["output"],
)

# Take Random Image from Training Data
X_pred = data[4].unsqueeze(0)

# Convert the Tensor image to PURE numpy and pretend we are working in venv where we only have numpy - NO PYTORCH
X_pred_np = X_pred.numpy()

X_pred_np = np.array(X_pred_np)

IMG_Rando = np.random.rand(1, 1, 28, 28)

np.shape(X_pred_np) == np.shape(IMG_Rando)

ort_session = onnxruntime.InferenceSession(
    "model_CNN.onnx"
)


def to_numpy(tensor):
    return (
        tensor.detach().gpu().numpy()
        if tensor.requires_grad
        else tensor.cpu().numpy()
    )


# compute ONNX Runtime output prediction

# WORKS
# ort_inputs = {ort_session.get_inputs()[0].name: X_pred_np}

# DOES NOT WORK
ort_inputs = {ort_session.get_inputs()[0].name: IMG_Rando}

# WORKS
# ort_inputs = {ort_session.get_inputs()[0].name: to_numpy(X_pred)}
ort_outs = ort_session.run(None, ort_inputs)

ort_outs

Firstly, we create a simple model and train it on the MNIST dataset.

Then we export the trained model using the ONNX framework. Now, when I want to classify an image using the X_pred_np It works even though it is a "pure" NumPy, which is what I want.

However, I suspect that this particular case works only because it has been derived from the PyTorch tensor object, and thus "under the hood" it still has PyTorch attributes. While when I try to inference on the random "pure" NumPy object IMG_Rando, there seems to be a problem:

Unexpected input data type. Actual: (tensor(double)) , expected: (tensor(float)).

Referring that PyTorch form is needed. Is there a way how to be able to use only numpy Images for the ONNX predictions?. So the inference can be performed in separated venv where no pytorch is installed?

Secondly, is there a way that ONNX would remember the actual classes?

In this particular case, the index corresponds to the label of the image. However, in animal classification, ONNX would not provide us with the "DOG" and "CAT" and other labels but would only provide us the index of the predicted label. Which we would need to run throw our own "prediction dictionary" so we know that the fifth label is associated with "cat" and sixth label is associated with "dog" etc.

Greed answered 27/6, 2021 at 15:26 Comment(0)
J
4

As an improvement to the accepted answer, the idiomatic way to generate random numbers in Numpy is now by using a Generator. This offers the benefit of being able to create the array in the right type directly, rather than using the expensive astype operation, which copies the array (as in the accepted answer). Thus, the improved solution would look like:

rng = np.random.default_rng() # set seed if desired
IMG_Rando = rng.random((1, 1, 28, 28), dtype=np.float32)
Jin answered 18/11, 2021 at 15:30 Comment(0)
S
11

Numpy defaults to float64 while pytorch defaults to float32. Cast the input to float32 before the inference:

IMG_Rando = np.random.rand(1, 1, 28, 28).astype(np.float32)

double is short for double-precision floating-point format, which is a floating point number representation on 64 bits, while float refers to a floating point number on 32 bits.

Spavin answered 28/6, 2021 at 12:14 Comment(0)
J
4

As an improvement to the accepted answer, the idiomatic way to generate random numbers in Numpy is now by using a Generator. This offers the benefit of being able to create the array in the right type directly, rather than using the expensive astype operation, which copies the array (as in the accepted answer). Thus, the improved solution would look like:

rng = np.random.default_rng() # set seed if desired
IMG_Rando = rng.random((1, 1, 28, 28), dtype=np.float32)
Jin answered 18/11, 2021 at 15:30 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.