Repository: OpenMined/PySyft

Issue:
Sigmoid in SMPC failing for some shapes
**Describe the bug**
 Examples to illustrate:

Working
```python
a = torch.Tensor([1,2,3])
a = a.fix_precision().share(bob, alice, crypto_provider = crypto_provider, requires_grad = True)
torch.nn.Sigmoid()(a)
```
Not working
```python
a = torch.Tensor([[1,2,3],[4,5,6]])
a = a.fix_precision().share(bob, alice, crypto_provider = crypto_provider, requires_grad = True)
torch.nn.Sigmoid()(a)
```
Error: `AssertionError: Must be batches of square matrices`

(same with `requires_grad=False`)

The error seems linked to tensor shape



Discussion:
It worked before by the way. Like a month ago
Might need to add a test for this.
I can take it @LaRiffle 

Resolving patch:
diff --git a/syft/frameworks/torch/tensors/interpreters/precision.py b/syft/frameworks/torch/tensors/interpreters/precision.py
--- a/syft/frameworks/torch/tensors/interpreters/precision.py
+++ b/syft/frameworks/torch/tensors/interpreters/precision.py
@@ -449,6 +449,10 @@ def matmul(self, *args, **kwargs):
     __matmul__ = matmul
     mm = matmul
 
+    def reciprocal(self):
+        ones = self * 0 + 1
+        return ones / self
+
     # Approximations:
     def inverse(self, iterations=8):
         """
@@ -482,45 +486,93 @@ def exp(self, iterations=8):
         """
         return (1 + self / 2 ** iterations) ** (2 ** iterations)
 
-    def sigmoid(self, method="exp"):
+    def sign(self):
+        return (self > 0) + (self < 0) * (-1)
+
+    @staticmethod
+    def _sigmoid_exp(tensor):
         """
-        Approximates the sigmoid function
+        Implementation taken from FacebookResearch - CrypTen project
+
+        Compute the sigmoid using the exp approximation
+        sigmoid(x) = 1 / (1 + exp(-x))
+
+        For stability:
+            sigmoid(x) = (sigmoid(|x|) - 0.5) * sign(x) + 0.5
+
+        Ref: https://timvieira.github.io/blog/post/2014/02/11/exp-normalize-trick/#numerically_stable_sigmoid_function
 
         Args:
-            self: the fixed precision tensor
-            method (str): (default = "exp")
-                "exp": Use the exponential approximation and the sigmoid definition
-                    sigmoid(x) = 1 / (1 + exp(-x))
-                "maclaurin": Use the Maclaurin / Taylor approximation, with polynomial
-                    interpolation of degree 5 over [-8,8]
-                    NOTE: This method is faster but not as precise as "exp"
-                    Ref: https://mortendahl.github.io/2017/04/17/private-deep-learning-with-mpc/#approximating-sigmoid
-        """
-
-        if method == "exp":
-            # Inverse can only be used on matrices
-            if len(self.shape) == 1:
-                one = self * 0 + 1
-                result = one / (1 + (self * -1).exp())
-            else:
-                result = (1 + (self * -1).exp()).inverse()
+            tensor (tensor): values where sigmoid should be approximated
+        """
 
-        elif method == "maclaurin":
-            weights = (
-                torch.tensor([0.5, 1.91204779e-01, -4.58667307e-03, 4.20690803e-05])
-                .fix_precision(**self.get_class_attributes())
-                .child
-            )
-            degrees = [0, 1, 3, 5]
+        sign = tensor.sign()
+
+        # Make sure the elements are all positive
+        x = tensor * sign
+        ones = tensor * 0 + 1
+        half = ones.div(2)
+        result = (ones + (-ones * x).exp()).reciprocal()
+        return (result - half) * sign + half
+
+    @staticmethod
+    def _sigmoid_maclaurin(tensor):
+        """
+        Approximates the sigmoid function using Maclaurin, with polynomial
+        interpolation of degree 5 over [-8,8]
+        NOTE: This method is faster but not as precise as "exp"
+        Ref: https://mortendahl.github.io/2017/04/17/private-deep-learning-with-mpc/#approximating-sigmoid
+
+        Args:
+            tensor (tensor): values where sigmoid should be approximated
+        """
+
+        weights = (
+            torch.tensor([0.5, 1.91204779e-01, -4.58667307e-03, 4.20690803e-05])
+            .fix_precision(**tensor.get_class_attributes())
+            .child
+        )
+        degrees = [0, 1, 3, 5]
 
-            # initiate with term of degree 0 to avoid errors with tensor ** 0
-            one = self * 0 + 1
-            result = one * weights[0]
-            for i, d in enumerate(degrees[1:]):
-                result += (self ** d) * weights[i + 1]
+        # initiate with term of degree 0 to avoid errors with tensor ** 0
+        one = tensor * 0 + 1
+        result = one * weights[0]
+        for i, d in enumerate(degrees[1:]):
+            result += (tensor ** d) * weights[i + 1]
 
         return result
 
+    @staticmethod
+    def _sigmoid_chebyshev(tensor, maxval: int = 6, terms: int = 32):
+        """
+        Implementation taken from FacebookResearch - CrypTen project
+        Computes the sigmoid function as
+                 sigmoid(x) = (tanh(x /2) + 1) / 2
+
+        Tanh is approximated using chebyshev polynomials
+        Args:
+             maxval (int): interval width used for tanh chebyshev polynomials
+             terms (int): highest degree of Chebyshev polynomials for tanh.
+                          Must be even and at least 6.
+        """
+        tanh_approx = tensor._tanh_chebyshev(tensor.div(2), maxval, terms)
+
+        return tanh_approx.div(2) + 0.5
+
+    def sigmoid(tensor, method="exp"):
+        """
+        Approximates the sigmoid function using a given method
+
+        Args:
+            tensor: the fixed precision tensor
+            method (str): (default = "chebyshev")
+                Possible values: "exp", "maclaurin", "chebyshev"
+        """
+
+        sigmoid_f = getattr(tensor, f"_sigmoid_{method}")
+
+        return sigmoid_f(tensor)
+
     def log(self, iterations=2, exp_iterations=8):
         """Approximates the natural logarithm using 8th order modified Householder iterations.
         Recall that Householder method is an algorithm to solve a non linear equation f(x) = 0.
@@ -560,6 +612,7 @@ def _tanh_chebyshev(tensor, maxval: int = 6, terms: int = 32):
           where c_i is the ith Chebyshev series coefficient and P_i is ith polynomial.
         The approximation is truncated to +/-1 outside [-maxval, maxval].
         Args:
+            tensor (tensor): values where the tanh needs to be approximated
             maxval (int): interval width used for computing chebyshev polynomials
             terms (int): highest degree of Chebyshev polynomials.
                          Must be even and at least 6.
@@ -593,9 +646,12 @@ def _tanh_chebyshev(tensor, maxval: int = 6, terms: int = 32):
     @staticmethod
     def _tanh_sigmoid(tensor):
         """
-        Compute the tanh using the sigmoid
+        Compute the tanh using the sigmoid approximation
 
+        Args:
+            tensor (tensor): values where tanh should be approximated
         """
+
         return 2 * torch.sigmoid(2 * tensor) - 1
 
     def tanh(tensor, method="chebyshev"):


Test patch:
diff --git a/test/conftest.py b/test/conftest.py
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -17,6 +17,20 @@
 from syft.workers.websocket_server import WebsocketServerWorker
 
 
+def pytest_sessionstart(session):
+    session.failed_tests = set()
+
+
+def pytest_runtest_makereport(item, call):  # pragma: no cover
+    if call.excinfo is not None and item.originalname:
+        item.session.failed_tests.add(item.originalname)
+
+
+def pytest_runtest_setup(item):  # pragma: no cover
+    if item.originalname in item.session.failed_tests:
+        pytest.skip("previous test failed (%s)" % item.name)
+
+
 def _start_proc(participant, dataset: str = None, **kwargs):  # pragma: no cover
     """Helper function for spinning up a websocket participant."""
 
diff --git a/test/torch/tensors/test_precision.py b/test/torch/tensors/test_precision.py
--- a/test/torch/tensors/test_precision.py
+++ b/test/torch/tensors/test_precision.py
@@ -414,114 +414,110 @@ def test_torch_inverse_approx(workers):
             assert (diff / (tolerance * norm)) < 1
 
 
-def test_torch_exp_approx(workers):
+@pytest.mark.parametrize("prec_frac, tolerance", [(3, 20 / 100), (4, 5 / 100), (5, 4 / 100)])
+def test_torch_exp_approx(prec_frac, tolerance, workers):
     """
     Test the approximate exponential with different tolerance depending on
     the precision_fractional considered
     """
     alice, bob, james = workers["alice"], workers["bob"], workers["james"]
 
-    fix_prec_tolerance = {3: 20 / 100, 4: 5 / 100, 5: 5 / 100}
-
-    for prec_frac, tolerance in fix_prec_tolerance.items():
-        cumsum = torch.zeros(5)
-        for i in range(10):
-            t = torch.tensor([0.0, 1, 2, 3, 4])
-            t_sh = t.fix_precision(precision_fractional=prec_frac).share(
-                alice, bob, crypto_provider=james
-            )
-            r_sh = t_sh.exp()
-            r = r_sh.get().float_prec()
-            t = t.exp()
-            diff = (r - t).abs()
-            norm = (r + t) / 2
-            cumsum += diff / (tolerance * norm)
-
-        cumsum /= 10
-        assert (cumsum < 1).all()
-
-
-def test_torch_sigmoid_approx(workers):
+    cumsum = torch.zeros(5)
+    for i in range(10):
+        t = torch.tensor([0.0, 1, 2, 3, 4])
+        t_sh = t.fix_precision(precision_fractional=prec_frac).share(
+            alice, bob, crypto_provider=james
+        )
+        r_sh = t_sh.exp()
+        r = r_sh.get().float_prec()
+        t = t.exp()
+        diff = (r - t).abs()
+        norm = (r + t) / 2
+        cumsum += diff / (tolerance * norm)
+
+    cumsum /= 10
+    assert (cumsum < 1).all()
+
+
+@pytest.mark.parametrize(
+    "method, prec_frac, tolerance",
+    [
+        ("chebyshev", 3, 6 / 100),
+        ("chebyshev", 4, 1 / 1000),
+        ("exp", 3, 6 / 100),
+        ("exp", 4, 1 / 100),
+        ("maclaurin", 3, 7 / 100),
+        ("maclaurin", 4, 15 / 100),
+    ],
+)
+def test_torch_sigmoid_approx(method, prec_frac, tolerance, workers):
     """
     Test the approximate sigmoid with different tolerance depending on
     the precision_fractional considered
     """
     alice, bob, james = workers["alice"], workers["bob"], workers["james"]
 
-    fix_prec_tolerance_by_method = {
-        "exp": {3: 6 / 100, 4: 1 / 100, 5: 1 / 100},
-        "maclaurin": {3: 7 / 100, 4: 15 / 100, 5: 15 / 100},
-    }
-
-    for method, fix_prec_tolerance in fix_prec_tolerance_by_method.items():
-        for prec_frac, tolerance in fix_prec_tolerance.items():
-            t = torch.tensor(range(-10, 10)) * 0.5
-            t_sh = t.fix_precision(precision_fractional=prec_frac).share(
-                alice, bob, crypto_provider=james
-            )
-            r_sh = t_sh.sigmoid(method=method)
-            r = r_sh.get().float_prec()
-            t = t.sigmoid()
-            diff = (r - t).abs().max()
-            norm = (r + t).abs().max() / 2
-
-            assert (diff / (tolerance * norm)) < 1
-
-
-def test_torch_tanh_approx(workers):
+    t = torch.tensor(range(-10, 10)) * 0.5
+    t_sh = t.fix_precision(precision_fractional=prec_frac).share(alice, bob, crypto_provider=james)
+    r_sh = t_sh.sigmoid(method=method)
+    r = r_sh.get().float_prec()
+    t = t.sigmoid()
+    diff = (r - t).abs().max()
+    norm = (r + t).abs().max() / 2
+
+    assert (diff / (tolerance * norm)) < 1
+
+
+@pytest.mark.parametrize(
+    "method, prec_frac, tolerance",
+    [
+        ("chebyshev", 3, 3 / 100),
+        ("chebyshev", 4, 2 / 100),
+        ("sigmoid", 3, 10 / 100),
+        ("sigmoid", 4, 5 / 100),
+    ],
+)
+def test_torch_tanh_approx(method, prec_frac, tolerance, workers):
     """
     Test the approximate tanh with different tolerance depending on
     the precision_fractional considered
     """
     alice, bob, james = workers["alice"], workers["bob"], workers["james"]
 
-    fix_prec_tolerance_by_method = {
-        "chebyshev": {3: 3 / 100, 4: 3 / 100, 5: 3 / 100},
-        "sigmoid": {3: 10 / 100, 4: 15 / 100, 5: 15 / 100},
-    }
+    t = torch.tensor(range(-10, 10)) * 0.5
+    t_sh = t.fix_precision(precision_fractional=prec_frac).share(alice, bob, crypto_provider=james)
+    r_sh = t_sh.tanh(method)
+    r = r_sh.get().float_prec()
+    t = t.tanh()
+    diff = (r - t).abs().max()
+    norm = (r + t).abs().max() / 2
 
-    for method, fix_prec_tolerance in fix_prec_tolerance_by_method.items():
-        for prec_frac, tolerance in fix_prec_tolerance.items():
-            t = torch.tensor(range(-6, 6)) * 0.5
-            t_sh = t.fix_precision(precision_fractional=prec_frac).share(
-                alice, bob, crypto_provider=james
-            )
-            r_sh = t_sh.tanh(method)
-            r = r_sh.get().float_prec()
-            t = t.tanh()
-            print(method, prec_frac, tolerance)
-            print(r)
-            print(t)
-            diff = (r - t).abs().max()
-            norm = (r + t).abs().max() / 2
-
-            assert (diff / (tolerance * norm)) < 1
+    assert (diff / (tolerance * norm)) < 1
 
 
-def test_torch_log_approx(workers):
+@pytest.mark.parametrize("prec_frac, tolerance", [(3, 100 / 100), (4, 3 / 100),])
+def test_torch_log_approx(prec_frac, tolerance, workers):
     """
     Test the approximate logarithm with different tolerance depending on
     the precision_fractional considered
     """
     alice, bob, james = workers["alice"], workers["bob"], workers["james"]
-    fix_prec_tolerance = {3: 100 / 100, 4: 3 / 100, 5: 2 / 100}
-
-    for prec_frac, tolerance in fix_prec_tolerance.items():
-        cumsum = torch.zeros(9)
-        for i in range(10):
-            t = torch.tensor([0.1, 0.5, 2, 5, 10, 20, 50, 100, 250])
-            t_sh = t.fix_precision(precision_fractional=prec_frac).share(
-                alice, bob, crypto_provider=james
-            )
-            r_sh = t_sh.log()
-            r = r_sh.get().float_prec()
-            t = t.log()
-            diff = (r - t).abs()
-            norm = (r + t) / 2
-            cumsum += diff / (tolerance * norm)
 
-        cumsum /= 10
-        assert (cumsum.abs() < 1).all()
+    cumsum = torch.zeros(9)
+    for i in range(10):
+        t = torch.tensor([0.1, 0.5, 2, 5, 10, 20, 50, 100, 250])
+        t_sh = t.fix_precision(precision_fractional=prec_frac).share(
+            alice, bob, crypto_provider=james
+        )
+        r_sh = t_sh.log()
+        r = r_sh.get().float_prec()
+        t = t.log()
+        diff = (r - t).abs()
+        norm = (r + t) / 2
+        cumsum += diff / (tolerance * norm)
+
+    cumsum /= 10
+    assert (cumsum.abs() < 1).all()
 
 
 def test_torch_conv2d(workers):