mirror of
https://github.com/luxfi/torus-ml.git
synced 2026-10-30 15:15:23 +00:00
236 lines
7.9 KiB
Python
236 lines
7.9 KiB
Python
"""Tests for the torch to numpy module."""
|
|
|
|
from functools import partial
|
|
|
|
import numpy
|
|
import pytest
|
|
import torch
|
|
from torch import nn
|
|
|
|
from torus.ml.pytest.torch_models import (
|
|
CNN,
|
|
FC,
|
|
CNNGrouped,
|
|
CNNInvalid,
|
|
CNNMaxPool,
|
|
NetWithConcatUnsqueeze,
|
|
NetWithLoops,
|
|
QATTestModule,
|
|
)
|
|
from torus.ml.torch import NumpyModule
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"model_class, input_shape",
|
|
[
|
|
pytest.param(FC, (100, 32 * 32 * 3)),
|
|
pytest.param(partial(CNN, input_output=3), (5, 3, 32, 32)),
|
|
pytest.param(partial(CNNGrouped, input_output=6, groups=3), (5, 6, 7, 7)),
|
|
pytest.param(partial(CNNMaxPool, input_output=3), (5, 3, 32, 32)),
|
|
pytest.param(
|
|
partial(NetWithLoops, input_output=32 * 32 * 3, n_fc_layers=4), (100, 32 * 32 * 3)
|
|
),
|
|
pytest.param(
|
|
partial(NetWithConcatUnsqueeze, input_output=32 * 32 * 3, n_fc_layers=4),
|
|
(100, 32 * 32 * 3),
|
|
),
|
|
pytest.param(QATTestModule, (5, 3, 6, 6)),
|
|
],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"activation_function",
|
|
[
|
|
pytest.param(nn.Sigmoid, id="Sigmoid"),
|
|
pytest.param(nn.ReLU, id="ReLU"),
|
|
pytest.param(nn.ReLU6, id="ReLU6"),
|
|
pytest.param(nn.Tanh, id="Tanh"),
|
|
pytest.param(nn.ELU, id="ELU"),
|
|
pytest.param(nn.Hardsigmoid, id="Hardsigmoid"),
|
|
pytest.param(nn.Hardtanh, id="Hardtanh"),
|
|
pytest.param(nn.LeakyReLU, id="LeakyReLU"),
|
|
pytest.param(nn.SELU, id="SELU"),
|
|
pytest.param(nn.CELU, id="CELU"),
|
|
pytest.param(nn.Softplus, id="Softplus"),
|
|
pytest.param(nn.PReLU, id="PReLU"),
|
|
pytest.param(nn.Hardswish, id="Hardswish"),
|
|
pytest.param(nn.SiLU, id="SiLU"),
|
|
pytest.param(nn.Mish, id="Mish"),
|
|
pytest.param(nn.Tanhshrink, id="Tanhshrink"),
|
|
pytest.param(partial(nn.Threshold, threshold=0, value=0), id="Threshold"),
|
|
pytest.param(nn.Softshrink, id="Softshrink"),
|
|
pytest.param(nn.Softsign, id="Softsign"),
|
|
pytest.param(nn.LogSigmoid, id="LogSigmoid"),
|
|
pytest.param(nn.GELU, id="GELU"),
|
|
# Some issues are still encountered with some activations
|
|
# FIXME: https://github.com/luxfi/torus-ml-internal/issues/335
|
|
#
|
|
# Other problems, certainly related to tests:
|
|
# Required positional arguments: 'embed_dim' and 'num_heads' and fails with a partial
|
|
# pytest.param(nn.MultiheadAttention, id="MultiheadAttention"),
|
|
# Activation with a RandomUniformLike
|
|
# pytest.param(nn.RReLU, id="RReLU"),
|
|
# Halving dimension must be even, but dimension 3 is size 3
|
|
# pytest.param(nn.GLU, id="GLU"),
|
|
],
|
|
)
|
|
def test_torch_to_numpy(model_class, input_shape, activation_function, check_r2_score):
|
|
"""Test the different model architecture from torch numpy."""
|
|
|
|
# Define the torch model
|
|
torch_fc_model = model_class(activation_function=activation_function)
|
|
|
|
# Since we have networks with Batch Normalization, we need to manually set them to evaluation
|
|
# mode. ONNX export does the same, so to ensure torch results are the same as the results
|
|
# when running the ONNX graph through NumpyMode we need to call .eval()
|
|
# Calling .eval() fixes the mean/var of the batchnormalization layer and stops is being updated
|
|
# during .forward
|
|
torch_fc_model.eval()
|
|
|
|
# Create random input
|
|
torch_input_1 = torch.randn(input_shape)
|
|
|
|
# Predict with torch model
|
|
torch_predictions = torch_fc_model(torch_input_1).detach().numpy()
|
|
|
|
# Create corresponding numpy model
|
|
numpy_fc_model = NumpyModule(torch_fc_model, torch_input_1)
|
|
|
|
# Torch input to numpy
|
|
numpy_input_1 = torch_input_1.detach().numpy()
|
|
|
|
# Predict with numpy model
|
|
numpy_predictions = numpy_fc_model(numpy_input_1)
|
|
|
|
# Test: the output of the numpy model is the same as the torch model.
|
|
assert numpy_predictions.shape == torch_predictions.shape
|
|
|
|
# Test: prediction from the numpy model are the same as the torch model.
|
|
check_r2_score(torch_predictions, numpy_predictions)
|
|
|
|
# Test: dynamics between layers is working (quantized input and activations)
|
|
torch_input_2 = torch.randn(input_shape)
|
|
|
|
# Make sure both inputs are different
|
|
assert (torch_input_1 != torch_input_2).any()
|
|
|
|
# Predict with torch
|
|
torch_predictions = torch_fc_model(torch_input_2).detach().numpy()
|
|
|
|
# Torch input to numpy
|
|
numpy_input_2 = torch_input_2.detach().numpy()
|
|
|
|
# Numpy predictions using the previous model
|
|
numpy_predictions = numpy_fc_model(numpy_input_2)
|
|
check_r2_score(torch_predictions, numpy_predictions)
|
|
|
|
|
|
def test_raises():
|
|
"""Function to test incompatible layers."""
|
|
|
|
torch_incompatible_model = CNNInvalid(nn.ReLU, False)
|
|
|
|
error_msg_pattern = (
|
|
"The following ONNX operators are required to convert the torch model to numpy but are"
|
|
" not currently implemented: ReduceMean*"
|
|
)
|
|
|
|
with pytest.raises(
|
|
Exception,
|
|
match=error_msg_pattern,
|
|
):
|
|
dummy_input = torch.randn(1, 3, 32, 32)
|
|
mod = NumpyModule(torch_incompatible_model, dummy_input)
|
|
mod(dummy_input.numpy())
|
|
|
|
|
|
class AddTest(nn.Module):
|
|
"""Simple torch module to test ONNX 'Add' operator."""
|
|
|
|
def __init__(self) -> None:
|
|
super().__init__()
|
|
self.bias = 1.0
|
|
|
|
def forward(self, x):
|
|
"""Forward pass."""
|
|
return x + self.bias
|
|
|
|
|
|
class MatmulTest(nn.Module):
|
|
"""Simple torch module to test ONNX 'Matmul' operator."""
|
|
|
|
def __init__(self) -> None:
|
|
super().__init__()
|
|
self.matmul = nn.Linear(3, 10, bias=False)
|
|
|
|
def forward(self, x):
|
|
"""Forward pass."""
|
|
return self.matmul(x)
|
|
|
|
|
|
class ReluTest(nn.Module):
|
|
"""Simple torch module to test ONNX 'Relu' operator."""
|
|
|
|
# Store a ReLU op to avoid pylint warnings
|
|
def __init__(self) -> None:
|
|
super().__init__()
|
|
self.relu = nn.ReLU()
|
|
|
|
def forward(self, x):
|
|
"""Forward pass."""
|
|
return self.relu(x)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"model_class,input_shape",
|
|
[
|
|
pytest.param(AddTest, (10, 10)),
|
|
pytest.param(MatmulTest, (10, 3)),
|
|
pytest.param(ReluTest, (10, 10)),
|
|
],
|
|
)
|
|
def test_torch_to_numpy_onnx_ops(model_class, input_shape, check_r2_score):
|
|
"""Test the different model architecture from torch numpy."""
|
|
|
|
# Define the torch model
|
|
torch_fc_model = model_class()
|
|
# Create random input
|
|
torch_input_1 = torch.randn(input_shape)
|
|
# Predict with torch model
|
|
torch_predictions = torch_fc_model(torch_input_1).detach().numpy()
|
|
# Create corresponding numpy model
|
|
numpy_fc_model = NumpyModule(torch_fc_model, torch_input_1)
|
|
# Torch input to numpy
|
|
numpy_input_1 = torch_input_1.detach().numpy()
|
|
# Predict with numpy model
|
|
numpy_predictions = numpy_fc_model(numpy_input_1)
|
|
|
|
# Test: the output of the numpy model is the same as the torch model.
|
|
assert numpy_predictions.shape == torch_predictions.shape
|
|
# Test: prediction from the numpy model are the same as the torch model.
|
|
check_r2_score(torch_predictions, numpy_predictions)
|
|
|
|
# Test: dynamics between layers is working (quantized input and activations)
|
|
torch_input_2 = torch.randn(input_shape)
|
|
# Make sure both inputs are different
|
|
assert (torch_input_1 != torch_input_2).any()
|
|
# Predict with torch
|
|
torch_predictions = torch_fc_model(torch_input_2).detach().numpy()
|
|
# Torch input to numpy
|
|
numpy_input_2 = torch_input_2.detach().numpy()
|
|
# Numpy predictions using the previous model
|
|
numpy_predictions = numpy_fc_model(numpy_input_2)
|
|
check_r2_score(torch_predictions, numpy_predictions)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"incompatible_model",
|
|
[pytest.param("STRING")],
|
|
)
|
|
def test_raises_incompatible_model_type(incompatible_model):
|
|
"""Test an incompatible model type."""
|
|
with pytest.raises(
|
|
ValueError,
|
|
match=("model must be a torch.nn.Module or an onnx.ModelProto, got str"),
|
|
):
|
|
_ = NumpyModule(incompatible_model, torch.from_numpy(numpy.array([1, 2, 3])))
|