Skip to content

Commit 8cbd450

Browse files
committed
Add tests for tests for logsoftmax, softplus, softsign
1 parent c371cf8 commit 8cbd450

File tree

4 files changed

+433
-2
lines changed

4 files changed

+433
-2
lines changed

onnx2pytorch/convert/operations.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -271,9 +271,12 @@ def convert_operations(onnx_graph, opset_version, batch_dim=0, enable_pruning=Tr
271271
kwargs.update(extract_attributes(node))
272272
op = nn.LogSoftmax(**kwargs)
273273
elif node.op_type == "Softplus":
274-
op = nn.Softplus(**extract_attributes(node))
274+
# ONNX Softplus has no attributes: y = ln(exp(x) + 1)
275+
# PyTorch Softplus with beta=1 matches ONNX spec
276+
op = nn.Softplus(beta=1)
275277
elif node.op_type == "Softsign":
276-
op = nn.Softsign(**extract_attributes(node))
278+
# ONNX Softsign has no attributes: y = x / (1 + |x|)
279+
op = nn.Softsign()
277280
elif node.op_type == "Split":
278281
kwargs = extract_attributes(node)
279282
# if the split_size_or_sections is not in node attributes,
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import numpy as np
2+
import onnxruntime as ort
3+
import pytest
4+
import torch
5+
from onnx import helper, TensorProto
6+
7+
from onnx2pytorch.convert import ConvertModel
8+
9+
10+
@pytest.mark.parametrize(
11+
"axis,input_shape",
12+
[
13+
(-1, [2, 3, 4]), # Default axis=-1
14+
(0, [2, 3, 4]),
15+
(1, [2, 3, 4]),
16+
(2, [2, 3, 4]),
17+
(-2, [2, 3, 4]),
18+
(1, [5, 10]), # 2D input
19+
(-1, [8]), # 1D input
20+
],
21+
)
22+
def test_logsoftmax_onnxruntime(axis, input_shape):
23+
"""Test LogSoftmax against onnxruntime."""
24+
np.random.seed(42)
25+
26+
# Create input
27+
X = np.random.randn(*input_shape).astype(np.float32)
28+
29+
# Create ONNX graph with LogSoftmax node
30+
input_tensor = helper.make_tensor_value_info("X", TensorProto.FLOAT, input_shape)
31+
output_tensor = helper.make_tensor_value_info("Y", TensorProto.FLOAT, input_shape)
32+
33+
logsoftmax_node = helper.make_node(
34+
"LogSoftmax",
35+
inputs=["X"],
36+
outputs=["Y"],
37+
axis=axis,
38+
)
39+
40+
graph = helper.make_graph(
41+
[logsoftmax_node],
42+
"logsoftmax_test",
43+
[input_tensor],
44+
[output_tensor],
45+
)
46+
47+
model = helper.make_model(
48+
graph, opset_imports=[helper.make_opsetid("", 13)], ir_version=8
49+
)
50+
51+
# Run with onnxruntime
52+
ort_session = ort.InferenceSession(model.SerializeToString())
53+
ort_outputs = ort_session.run(None, {"X": X})
54+
expected_Y = ort_outputs[0]
55+
56+
# Convert to PyTorch and run
57+
o2p_model = ConvertModel(model, experimental=True)
58+
X_torch = torch.from_numpy(X)
59+
60+
with torch.no_grad():
61+
o2p_output = o2p_model(X_torch)
62+
63+
# Compare outputs
64+
torch.testing.assert_close(
65+
o2p_output,
66+
torch.from_numpy(expected_Y),
67+
rtol=1e-5,
68+
atol=1e-5,
69+
)
70+
71+
72+
def test_logsoftmax_default_axis():
73+
"""Test LogSoftmax with default axis=-1."""
74+
np.random.seed(42)
75+
76+
input_shape = [2, 3, 4]
77+
X = np.random.randn(*input_shape).astype(np.float32)
78+
79+
# Create ONNX graph WITHOUT specifying axis (should default to -1)
80+
input_tensor = helper.make_tensor_value_info("X", TensorProto.FLOAT, input_shape)
81+
output_tensor = helper.make_tensor_value_info("Y", TensorProto.FLOAT, input_shape)
82+
83+
logsoftmax_node = helper.make_node(
84+
"LogSoftmax",
85+
inputs=["X"],
86+
outputs=["Y"],
87+
# No axis specified - should default to -1
88+
)
89+
90+
graph = helper.make_graph(
91+
[logsoftmax_node],
92+
"logsoftmax_test",
93+
[input_tensor],
94+
[output_tensor],
95+
)
96+
97+
model = helper.make_model(
98+
graph, opset_imports=[helper.make_opsetid("", 13)], ir_version=8
99+
)
100+
101+
# Run with onnxruntime
102+
ort_session = ort.InferenceSession(model.SerializeToString())
103+
ort_outputs = ort_session.run(None, {"X": X})
104+
expected_Y = ort_outputs[0]
105+
106+
# Convert to PyTorch and run
107+
o2p_model = ConvertModel(model, experimental=True)
108+
X_torch = torch.from_numpy(X)
109+
110+
with torch.no_grad():
111+
o2p_output = o2p_model(X_torch)
112+
113+
# Compare outputs
114+
torch.testing.assert_close(
115+
o2p_output,
116+
torch.from_numpy(expected_Y),
117+
rtol=1e-5,
118+
atol=1e-5,
119+
)
120+
121+
122+
def test_logsoftmax_properties():
123+
"""Test mathematical properties of LogSoftmax."""
124+
# LogSoftmax(x) = log(Softmax(x))
125+
X = torch.randn(2, 5)
126+
127+
logsoftmax_output = torch.nn.functional.log_softmax(X, dim=-1)
128+
softmax_output = torch.nn.functional.softmax(X, dim=-1)
129+
log_of_softmax = torch.log(softmax_output)
130+
131+
torch.testing.assert_close(logsoftmax_output, log_of_softmax, rtol=1e-5, atol=1e-5)
132+
133+
# Sum of exp(log_softmax) should be 1
134+
sum_exp = torch.exp(logsoftmax_output).sum(dim=-1)
135+
torch.testing.assert_close(sum_exp, torch.ones_like(sum_exp), rtol=1e-5, atol=1e-5)
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import numpy as np
2+
import onnxruntime as ort
3+
import pytest
4+
import torch
5+
from onnx import helper, TensorProto
6+
7+
from onnx2pytorch.convert import ConvertModel
8+
9+
10+
@pytest.mark.parametrize(
11+
"input_shape",
12+
[
13+
[2, 3, 4],
14+
[5, 10],
15+
[8],
16+
[1, 1, 5, 5],
17+
],
18+
)
19+
def test_softplus_default_onnxruntime(input_shape):
20+
"""Test Softplus with default parameters against onnxruntime."""
21+
np.random.seed(42)
22+
23+
# Create input
24+
X = np.random.randn(*input_shape).astype(np.float32)
25+
26+
# Create ONNX graph with Softplus node (default parameters)
27+
input_tensor = helper.make_tensor_value_info("X", TensorProto.FLOAT, input_shape)
28+
output_tensor = helper.make_tensor_value_info("Y", TensorProto.FLOAT, input_shape)
29+
30+
softplus_node = helper.make_node(
31+
"Softplus",
32+
inputs=["X"],
33+
outputs=["Y"],
34+
)
35+
36+
graph = helper.make_graph(
37+
[softplus_node],
38+
"softplus_test",
39+
[input_tensor],
40+
[output_tensor],
41+
)
42+
43+
model = helper.make_model(
44+
graph, opset_imports=[helper.make_opsetid("", 11)], ir_version=8
45+
)
46+
47+
# Run with onnxruntime
48+
ort_session = ort.InferenceSession(model.SerializeToString())
49+
ort_outputs = ort_session.run(None, {"X": X})
50+
expected_Y = ort_outputs[0]
51+
52+
# Convert to PyTorch and run
53+
o2p_model = ConvertModel(model, experimental=True)
54+
X_torch = torch.from_numpy(X)
55+
56+
with torch.no_grad():
57+
o2p_output = o2p_model(X_torch)
58+
59+
# Compare outputs
60+
torch.testing.assert_close(
61+
o2p_output,
62+
torch.from_numpy(expected_Y),
63+
rtol=1e-5,
64+
atol=1e-5,
65+
)
66+
67+
68+
def test_softplus_properties():
69+
"""Test mathematical properties of Softplus."""
70+
# Softplus(x) = log(1 + exp(x))
71+
X = torch.randn(10, 20)
72+
73+
softplus_output = torch.nn.functional.softplus(X)
74+
manual_output = torch.log(1 + torch.exp(X))
75+
76+
# Note: For very large X, exp(X) overflows, so softplus uses approximation
77+
# Compare only for reasonable values
78+
mask = X < 10
79+
torch.testing.assert_close(
80+
softplus_output[mask], manual_output[mask], rtol=1e-5, atol=1e-5
81+
)
82+
83+
# Softplus should always be positive
84+
assert (softplus_output > 0).all()
85+
86+
# For large positive x, softplus(x) ≈ x
87+
large_x = torch.tensor([10.0, 20.0, 50.0])
88+
softplus_large = torch.nn.functional.softplus(large_x)
89+
torch.testing.assert_close(softplus_large, large_x, rtol=1e-2, atol=1e-2)
90+
91+
# For large negative x, softplus(x) ≈ 0
92+
small_x = torch.tensor([-10.0, -20.0, -50.0])
93+
softplus_small = torch.nn.functional.softplus(small_x)
94+
assert (softplus_small < 0.01).all()
95+
96+
97+
def test_softplus_vs_relu():
98+
"""Test that Softplus is a smooth approximation of ReLU."""
99+
X = torch.linspace(-5, 5, 100)
100+
101+
softplus_output = torch.nn.functional.softplus(X)
102+
relu_output = torch.nn.functional.relu(X)
103+
104+
# Softplus should be close to ReLU for large positive values
105+
mask = X > 3
106+
torch.testing.assert_close(
107+
softplus_output[mask], relu_output[mask], rtol=0.1, atol=0.1
108+
)
109+
110+
# Softplus should be smooth (no sharp corner at 0 like ReLU)
111+
# At x=0: softplus(0) = log(2) ≈ 0.693, relu(0) = 0
112+
softplus_at_zero = torch.nn.functional.softplus(torch.tensor([0.0]))
113+
assert abs(softplus_at_zero.item() - 0.693) < 0.01
114+
115+
116+
def test_softplus_gradient():
117+
"""Test that Softplus gradient is sigmoid."""
118+
# d/dx softplus(x) = sigmoid(x) = 1/(1 + exp(-x))
119+
X = torch.randn(5, 5, requires_grad=True)
120+
121+
output = torch.nn.functional.softplus(X)
122+
output.sum().backward()
123+
124+
# Gradient should be sigmoid(X)
125+
expected_grad = torch.sigmoid(X)
126+
127+
torch.testing.assert_close(X.grad, expected_grad, rtol=1e-5, atol=1e-5)

0 commit comments

Comments
 (0)