Repository: sktime/sktime

Issue:
[BUG] `RuntimeError` when `SignatureTransformer` is run on large datasets
SignatureTransformer throws a RuntimeError when calling fit on the DuckDuckGeese dataset. I assume this is an issue with the number of dimensions?

```
File "E:\_ProgramFiles\Anaconda3\envs\sktime-workshop-boss\lib\site-packages\esig\backends.py", line 118, in compute_signature
    return tosig.stream2sig(stream, depth)
RuntimeError: Legitimate width 2 <-> 256 exceeded
```

Dataset | Train | Test | Class | Length | Dimension 
-- | -- | -- | -- | -- | -- 
DuckDuckGeese | 50 | 50 | 5 | 270 | 1345 



Discussion:
For some more context, we are running signatures on the whole UEA multivariate archive. I would like to include the method in our future evaluations.

Pinging @jambo6 as they may know more.
This is actually an error thrown by `esig` to help prevent memory errors down the line. 

Signature feature dimension increases exponentially with the input dimension of the data. The input dimension here is `1345`, a signature of `depth=2` would then have on the order of `1345 ** 2` features (nearly 2 million). You can see how this quickly becomes a problem. 

Without additional modifications (e.g. using random projections) signatures do not scale well to large input dimensions. My advice would be to skip any examples where dimension exceeds 256 and chalk signatures down as DNF. 

That's fine, I think these limitations should be noted in the docs, however. Could maybe be handled a bit better than a RuntimeError in the esig package as well, but that's less important. 

I stopped after the first dataset error, but quite a few more also seem to have problems. Some examples:


Dataset | Train | Test | Class | Length | Dimension | Error
-- | -- | -- | -- | -- | -- | --
FingerMovements | 316 | 100 | 2 | 50 | 28 | `RuntimeError: Legitimate depth of 2<->3 for records with width 29 exceeds limit`
Heartbeat | 204 | 205 | 2 | 405 | 61 | `RuntimeError: Legitimate depth of 2<->2 for records with width 62 exceeds limit`
FaceDetection | 5890 | 3524 | 2 | 62 | 144 | `RuntimeError: Legitimate width 2 <-> 256 exceeded`

I assume this is still dimension related, the limit seems to be ~20 ish.




I encountered the same problem in my work. What I did was to replace the backend of the esig by iisignature in _compute(see https://github.com/datasig-ac-uk/esig), which can handle paths with more dimensions. Note that this requires iisignature, in addition to esig, to be installed on your machine. Happy to write a patch for this issue if that's useful. 

Resolving patch:
diff --git a/sktime/transformations/panel/signature_based/_augmentations.py b/sktime/transformations/panel/signature_based/_augmentations.py
--- a/sktime/transformations/panel/signature_based/_augmentations.py
+++ b/sktime/transformations/panel/signature_based/_augmentations.py
@@ -43,11 +43,11 @@ def _make_augmentation_pipeline(augmentation_list):
     if augmentation_list is not None:
         if isinstance(augmentation_list, str):
             augmentation_list = (augmentation_list,)
-    assert all(
-        [x in list(AUGMENTATIONS.keys()) for x in augmentation_list]
-    ), "augmentation_list must only contain string elements from {}. Given: {}.".format(
-        list(AUGMENTATIONS.keys()), augmentation_list
-    )
+        if not [x in list(AUGMENTATIONS.keys()) for x in augmentation_list]:
+            raise ValueError(
+                "augmentation_list must only contain string elements from "
+                f" {list(AUGMENTATIONS.keys())}. Found: {augmentation_list}"
+            )
 
     # Setup pipeline
     if augmentation_list is not None:
diff --git a/sktime/transformations/panel/signature_based/_compute.py b/sktime/transformations/panel/signature_based/_compute.py
--- a/sktime/transformations/panel/signature_based/_compute.py
+++ b/sktime/transformations/panel/signature_based/_compute.py
@@ -45,6 +45,7 @@ def __init__(
         sig_tfm=None,
         sig_depth=None,
         rescaling=None,
+        backend=None,
     ):
         super().__init__()
         self.window_name = window_name
@@ -54,6 +55,7 @@ def __init__(
         self.sig_tfm = sig_tfm
         self.sig_depth = sig_depth
         self.rescaling = rescaling
+        self.backend = backend
 
         self.window = _window_getter(
             self.window_name, self.window_depth, self.window_length, self.window_step
@@ -62,6 +64,9 @@ def __init__(
     def _transform(self, X, y=None):
         import esig
 
+        if self.backend == "iisignature":
+            esig.set_backend("iisignature")
+
         depth = self.sig_depth
         data = np.swapaxes(X, 1, 2)
 
diff --git a/sktime/transformations/panel/signature_based/_signature_method.py b/sktime/transformations/panel/signature_based/_signature_method.py
--- a/sktime/transformations/panel/signature_based/_signature_method.py
+++ b/sktime/transformations/panel/signature_based/_signature_method.py
@@ -9,6 +9,8 @@
 from sktime.transformations.panel.signature_based._compute import (
     _WindowSignatureTransform,
 )
+from sktime.utils.validation._dependencies import _check_soft_dependencies
+from sktime.utils.warnings import warn
 
 
 class SignatureTransformer(BaseTransformer):
@@ -19,19 +21,34 @@ class SignatureTransformer(BaseTransformer):
 
     Parameters
     ----------
-    augmentation_list: tuple of strings, contains the augmentations to be
-        applied before application of the signature transform.
-    window_name: str, The name of the window transform to apply.
-    window_depth: int, The depth of the dyadic window. (Active only if
-        `window_name == 'dyadic'`).
-    window_length: int, The length of the sliding/expanding window. (Active
-        only if `window_name in ['sliding, 'expanding']`.
-    window_step: int, The step of the sliding/expanding window. (Active
-        only if `window_name in ['sliding, 'expanding']`.
-    rescaling: str or None, The method of signature rescaling.
-    sig_tfm: str, String to specify the type of signature transform. One of:
-        ['signature', 'logsignature']).
-    depth: int, Signature truncation depth.
+    augmentation_list: list or tuple of strings, possible strings are
+        ['leadlag', 'ir', 'addtime', 'cumsum', 'basepoint']
+        Augmentations to apply to the data before computing the signature.
+        The order of the augmentations is the order in which they are applied.
+        default: ('basepoint', 'addtime')
+    window_name: str, one of ``['global', 'sliding', 'expanding', 'dyadic']``
+        default: 'dyadic'
+        Type of the window to use for the signature transform.
+    window_depth: int, default=3
+        The depth of the dyadic window.
+        Ignored unless ``window_name`` is ``'dyadic'``.
+    window_length: None (default) or int
+        The length of the sliding/expanding window. (Active
+        Ignored unless ``window_name`` is one of ``['sliding, 'expanding']``.
+    window_step: None (default) or int
+        The step of the sliding/expanding window.
+        Ignored unless ``window_name`` is one of ``['sliding, 'expanding']``.
+    rescaling: None (default) or str, "pre" or "post",
+        None: No rescaling is applied.
+        "pre": rescale the path last signature term should be roughly O(1)
+        "post": Rescales the output signature by multiplying the depth-d term by d!.
+            Aim is that every term becomes ~O(1).
+    sig_tfm: str, one of ``['signature', 'logsignature']``. default: ``'signature'``
+        The type of signature transform to use, plain or logaritmic.
+    depth: int, default=4
+        Signature truncation depth.
+    backend: str, one of: ``'esig'`` (default), or ``'iisignature'``.
+        The backend to use for signature computation.
 
     Attributes
     ----------
@@ -62,6 +79,7 @@ def __init__(
         rescaling=None,
         sig_tfm="signature",
         depth=4,
+        backend="esig",
     ):
         self.augmentation_list = augmentation_list
         self.window_name = window_name
@@ -71,8 +89,27 @@ def __init__(
         self.rescaling = rescaling
         self.sig_tfm = sig_tfm
         self.depth = depth
+        self.backend = backend
 
         super().__init__()
+
+        if backend == "esig":
+            _check_soft_dependencies("esig")
+        elif backend == "iisignature":
+            _check_soft_dependencies("iisignature")
+            warn(
+                "iisignature backend of SignatureTransformer is experimental "
+                "and not systematically tested, due to lack of stable installation "
+                "process for iisignature via pip. Kindly exercise caution, "
+                "and report any issues on the sktime issue tracker.",
+                stacklevel=2,
+            )
+        else:
+            raise ValueError(
+                "Error in SignatureTransformer, backend "
+                "must be one of 'esig' or 'iisignature'"
+            )
+
         self.setup_feature_pipeline()
 
     def setup_feature_pipeline(self):
@@ -86,6 +123,7 @@ def setup_feature_pipeline(self):
             sig_tfm=self.sig_tfm,
             sig_depth=self.depth,
             rescaling=self.rescaling,
+            backend=self.backend,
         )
 
         # The so-called 'signature method' as defined in the reference paper


Test patch: