diff --git a/coremltools/converters/mil/mil/ops/defs/iOS15/activation.py b/coremltools/converters/mil/mil/ops/defs/iOS15/activation.py index 0c261a1f2..aede13d7d 100644 --- a/coremltools/converters/mil/mil/ops/defs/iOS15/activation.py +++ b/coremltools/converters/mil/mil/ops/defs/iOS15/activation.py @@ -77,9 +77,14 @@ class clamped_relu(activation_with_alpha_and_beta): @precondition(allow=VALUE) def value_inference(self): - x = np.minimum(np.maximum(self.x.val, 0), self.beta.val) - y = np.minimum(np.minimum(self.x.val, 0) * self.alpha.val, self.beta.val) - return x + y + # Splitting into a positive and a negative half and adding them back together + # only works when at most one half is non-zero, which stops being true once + # beta is negative and clamps both of them. Apply the documented formula + # directly instead. + return np.minimum( + np.where(self.x.val >= 0, self.x.val, self.x.val * self.alpha.val), + self.beta.val, + ) @register_op diff --git a/coremltools/converters/mil/mil/ops/tests/iOS14/test_activation.py b/coremltools/converters/mil/mil/ops/tests/iOS14/test_activation.py index c80f90ddb..e851853df 100644 --- a/coremltools/converters/mil/mil/ops/tests/iOS14/test_activation.py +++ b/coremltools/converters/mil/mil/ops/tests/iOS14/test_activation.py @@ -55,6 +55,20 @@ def test_builder_eval(self): y = np.minimum(np.minimum(x_val, 0) * 2.0, 1.0) np.testing.assert_allclose(x + y, v.val, atol=1e-04, rtol=1e-05) + @ssa_fn + def test_builder_eval_negative_beta(self): + """ + Constant folding must agree with the runtime contract + ``f(x) = min((x >= 0 ? x : alpha * x), beta)`` for a negative beta too. + The alpha / beta combinations here match the ones the neural network backend + test for this layer already covers (test_numpy_nn_layers.test_clamped_relu_cpu). + """ + x_val = np.arange(-20, 20, dtype=np.float32) + for alpha, beta in itertools.product([0.0, 2.0, -3.0], [7.0, -8.0]): + v = mb.clamped_relu(x=x_val, alpha=alpha, beta=beta) + expected = np.minimum(beta, np.where(x_val >= 0, x_val, x_val * alpha)) + np.testing.assert_allclose(expected, v.val, atol=1e-04, rtol=1e-05) + @pytest.mark.parametrize( "compute_unit, backend, dim, alpha, beta", itertools.product(compute_units, backends, [2, 4, 8], [2.0, 3.0], [4.0, 5.0]),