sigtorch 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sigtorch/__init__.py ADDED
@@ -0,0 +1,23 @@
1
+ """sigtorch: differentiable signatures and logsignatures in pure PyTorch, with the API of signatory.
2
+
3
+ ``import sigtorch as signatory`` is intended to work as a drop-in replacement for code written against signatory 1.2.
4
+ """
5
+
6
+ from .augment import Augment
7
+ from .deprecated import max_parallelism
8
+ from .logsignature_module import (LogSignature, Logsignature, SignatureToLogSignature, SignatureToLogsignature,
9
+ logsignature, logsignature_channels, signature_to_logsignature)
10
+ from .path import Path
11
+ from .signature_inversion_module import invert_signature
12
+ from .signature_module import (Signature, extract_signature_term, multi_signature_combine, signature,
13
+ signature_channels, signature_combine)
14
+ from .utility import all_words, lyndon_brackets, lyndon_words
15
+
16
+ __version__ = "0.1.0"
17
+
18
+ __all__ = [
19
+ "Augment", "LogSignature", "Logsignature", "Path", "Signature", "SignatureToLogSignature",
20
+ "SignatureToLogsignature", "all_words", "extract_signature_term", "invert_signature", "logsignature",
21
+ "logsignature_channels", "lyndon_brackets", "lyndon_words", "max_parallelism", "multi_signature_combine",
22
+ "signature", "signature_channels", "signature_combine", "signature_to_logsignature",
23
+ ]
sigtorch/_backends.py ADDED
@@ -0,0 +1,89 @@
1
+ """Signature levels of a piecewise-linear path, given its increments.
2
+
3
+ Depths 1-3 use closed forms built from cumulative sums and batched matrix products, which never materialise the
4
+ per-step level-2/3 tensors of the non-stream signature. Deeper signatures combine per-segment exponentials with Chen's
5
+ identity in a balanced tree (non-stream) or a parallel prefix scan (stream), so the number of sequential steps grows
6
+ with log(length) rather than length.
7
+ """
8
+
9
+ import torch
10
+
11
+ from . import _tensor_algebra as ta
12
+
13
+
14
+ def signature_levels(dX: torch.Tensor, depth: int, stream: bool) -> ta.Levels:
15
+ """Signature levels of the path with increments ``dX`` of shape (B, N, C).
16
+
17
+ Returns levels of shape (B, C**k), or (B, N, C**k) holding the signature of every prefix if ``stream``.
18
+ """
19
+ if depth <= 3:
20
+ return _closed_form_stream(dX, depth) if stream else _closed_form(dX, depth)
21
+ return _scan(dX, depth) if stream else _tree(dX, depth)
22
+
23
+
24
+ def _exclusive_cumsum(x: torch.Tensor) -> torch.Tensor:
25
+ """Cumulative sum over dim 1 of everything strictly before each step."""
26
+ return x.cumsum(dim=1) - x
27
+
28
+
29
+ def _closed_form(dX: torch.Tensor, depth: int) -> ta.Levels:
30
+ batch, _, channels = dX.shape
31
+ levels = [dX.sum(dim=1)]
32
+ if depth >= 2:
33
+ before = _exclusive_cumsum(dX) # S1 up to the start of each segment
34
+ # sum_t S1_before[t] (x) dX[t] + sum_t dX[t] (x) dX[t] / 2
35
+ S2 = torch.bmm(before.transpose(1, 2), dX) + 0.5 * torch.bmm(dX.transpose(1, 2), dX)
36
+ levels.append(S2.reshape(batch, channels ** 2))
37
+ if depth >= 3:
38
+ outer = torch.einsum('bti,btj->btij', dX, dX)
39
+ S2_before = _exclusive_cumsum(torch.einsum('bti,btj->btij', before, dX) + 0.5 * outer)
40
+ # Chen: S3 += S2_before (x) dX + S1_before (x) dX^2 / 2 + dX^3 / 6, summed over segments
41
+ S3 = (torch.einsum('btij,btk->bijk', S2_before, dX)
42
+ + 0.5 * torch.einsum('bti,btjk->bijk', before, outer)
43
+ + torch.einsum('bti,btjk->bijk', dX, outer) / 6)
44
+ levels.append(S3.reshape(batch, channels ** 3))
45
+ return levels
46
+
47
+
48
+ def _closed_form_stream(dX: torch.Tensor, depth: int) -> ta.Levels:
49
+ batch, length, channels = dX.shape
50
+ S1 = dX.cumsum(dim=1)
51
+ levels = [S1]
52
+ if depth >= 2:
53
+ before = S1 - dX
54
+ outer = torch.einsum('bti,btj->btij', dX, dX)
55
+ S2 = (torch.einsum('bti,btj->btij', before, dX) + 0.5 * outer).cumsum(dim=1)
56
+ levels.append(S2.reshape(batch, length, channels ** 2))
57
+ if depth >= 3:
58
+ S2_before = torch.cat([torch.zeros_like(S2[:, :1]), S2[:, :-1]], dim=1)
59
+ S3 = (torch.einsum('btij,btk->btijk', S2_before, dX)
60
+ + 0.5 * torch.einsum('bti,btjk->btijk', before, outer)
61
+ + torch.einsum('bti,btjk->btijk', dX, outer) / 6).cumsum(dim=1)
62
+ levels.append(S3.reshape(batch, length, channels ** 3))
63
+ return levels
64
+
65
+
66
+ def _tree(dX: torch.Tensor, depth: int) -> ta.Levels:
67
+ """Multiplies the segment exponentials pairwise, halving their number each round."""
68
+ levels = ta.restricted_exp(dX, depth)
69
+ while levels[0].size(1) > 1:
70
+ tail = None
71
+ if levels[0].size(1) % 2: # an odd one out joins the next round at the end
72
+ tail = [x[:, -1:] for x in levels]
73
+ levels = [x[:, :-1] for x in levels]
74
+ levels = ta.mult([x[:, 0::2] for x in levels], [x[:, 1::2] for x in levels])
75
+ if tail is not None:
76
+ levels = [torch.cat([x, t], dim=1) for x, t in zip(levels, tail)]
77
+ return [x[:, 0] for x in levels]
78
+
79
+
80
+ def _scan(dX: torch.Tensor, depth: int) -> ta.Levels:
81
+ """Inclusive prefix products of the segment exponentials (Hillis-Steele scan)."""
82
+ levels = ta.restricted_exp(dX, depth)
83
+ length = levels[0].size(1)
84
+ offset = 1
85
+ while offset < length:
86
+ combined = ta.mult([x[:, :-offset] for x in levels], [x[:, offset:] for x in levels])
87
+ levels = [torch.cat([x[:, :offset], c], dim=1) for x, c in zip(levels, combined)]
88
+ offset *= 2
89
+ return levels
@@ -0,0 +1,75 @@
1
+ """Operations on elements of the truncated tensor algebra.
2
+
3
+ An element is a list ``[x_1, ..., x_depth]`` where ``x_k`` has shape ``(..., C**k)``: the level-k tensor flattened in
4
+ row-major order, after any number of leading batch dimensions. The scalar term is implicit: 1 for signatures (group
5
+ elements), 0 for log-signatures (Lie elements). All operations broadcast over the leading dimensions.
6
+ """
7
+
8
+ from typing import List, Optional
9
+
10
+ import torch
11
+
12
+ Levels = List[torch.Tensor]
13
+
14
+
15
+ def split_levels(flat: torch.Tensor, channels: int, depth: int) -> Levels:
16
+ """Splits a ``(..., C + C**2 + ... + C**depth)`` tensor into its levels."""
17
+ return list(torch.split(flat, [channels ** k for k in range(1, depth + 1)], dim=-1))
18
+
19
+
20
+ def _outer(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
21
+ """(..., m) x (..., n) -> (..., m * n), the flattened tensor product."""
22
+ return (a.unsqueeze(-1) * b.unsqueeze(-2)).flatten(-2)
23
+
24
+
25
+ def restricted_exp(x: torch.Tensor, depth: int) -> Levels:
26
+ """exp(x) for a level-1 element ``x`` of shape (..., C): the signature of a single straight segment."""
27
+ levels = [x]
28
+ for k in range(2, depth + 1):
29
+ levels.append(_outer(levels[-1], x) / k)
30
+ return levels
31
+
32
+
33
+ def _product(a: List[Optional[torch.Tensor]], b: List[Optional[torch.Tensor]]) -> List[Optional[torch.Tensor]]:
34
+ """Truncated product of two elements with zero scalar term. ``None`` marks a level known to be zero."""
35
+ depth = len(a)
36
+ out: List[Optional[torch.Tensor]] = [None] * depth
37
+ for k in range(1, depth): # output level k + 1
38
+ for i in range(k): # a at level i + 1, b at level k - i
39
+ if a[i] is not None and b[k - 1 - i] is not None:
40
+ term = _outer(a[i], b[k - 1 - i])
41
+ out[k] = term if out[k] is None else out[k] + term
42
+ return out
43
+
44
+
45
+ def mult(a: Levels, b: Levels) -> Levels:
46
+ """(1 + a) * (1 + b) - 1: the product of two group elements, e.g. Chen's identity for signatures."""
47
+ cross = _product(a, b)
48
+ return [x + y if c is None else x + y + c for x, y, c in zip(a, b, cross)]
49
+
50
+
51
+ def log(a: Levels) -> Levels:
52
+ """log(1 + a) = a - a^2 / 2 + a^3 / 3 - ..., truncated."""
53
+ result = list(a)
54
+ power: List[Optional[torch.Tensor]] = list(a)
55
+ for n in range(2, len(a) + 1):
56
+ power = _product(power, a)
57
+ coeff = (-1) ** (n + 1) / n
58
+ result = [r if p is None else r + coeff * p for r, p in zip(result, power)]
59
+ return result
60
+
61
+
62
+ def inverse(a: Levels) -> Levels:
63
+ """Inverse of a group-like element such as a signature: reverse every word and multiply level k by (-1)^k.
64
+
65
+ This equals the signature of the time-reversed path.
66
+ """
67
+ channels = a[0].size(-1)
68
+ out = []
69
+ for k, x in enumerate(a, start=1):
70
+ batch_shape = x.shape[:-1]
71
+ nb = len(batch_shape)
72
+ x = x.reshape(*batch_shape, *([channels] * k))
73
+ x = x.permute(*range(nb), *range(nb + k - 1, nb - 1, -1)).reshape(*batch_shape, channels ** k)
74
+ out.append(x if k % 2 == 0 else -x)
75
+ return out
sigtorch/augment.py ADDED
@@ -0,0 +1,69 @@
1
+ """A torch.nn.Module for augmenting streams of data before taking signatures.
2
+
3
+ Adapted from signatory's augment.py (Copyright 2019 Patrick Kidger, Apache License 2.0, see LICENSE-signatory);
4
+ rewritten for sigtorch's pure-PyTorch implementation.
5
+ """
6
+
7
+ from typing import Callable, Tuple, Union
8
+
9
+ import torch
10
+ from torch import nn
11
+ from torch.nn import functional as F
12
+
13
+
14
+ class Augment(nn.Module):
15
+ """Sweeps a small feedforward network along a stream, as in Bonnier et al. 2019, "Deep Signature Transforms".
16
+
17
+ The first layer is a ``Conv1d`` with the given kernel, stride, padding and dilation; later layers are pointwise
18
+ ``Conv1d`` layers with ``activation`` in between (so the network's output is linear). Input has shape
19
+ (batch, stream, in_channels); the output has the network's output channels, preceded by the original stream (if
20
+ ``include_original``) and a time channel running from 0 to 1 (if ``include_time``). Those two are aligned with
21
+ the last point under each kernel window, so they assume stride 1 and no padding.
22
+ """
23
+
24
+ def __init__(self, in_channels: int, layer_sizes: Union[int, Tuple[int, ...]], kernel_size: int, stride: int = 1,
25
+ padding: int = 0, dilation: int = 1, bias: bool = True,
26
+ activation: Callable[[torch.Tensor], torch.Tensor] = F.relu, include_original: bool = True,
27
+ include_time: bool = True, **kwargs):
28
+ super().__init__(**kwargs)
29
+ if isinstance(layer_sizes, int):
30
+ layer_sizes = (layer_sizes,)
31
+ self.in_channels = in_channels
32
+ self.layer_sizes = layer_sizes
33
+ self.kernel_size = kernel_size
34
+ self.stride = stride
35
+ self.padding = padding
36
+ self.dilation = dilation
37
+ self.bias = bias
38
+ self.activation = activation
39
+ self.include_original = include_original
40
+ self.include_time = include_time
41
+
42
+ self.convs = nn.ModuleList()
43
+ if layer_sizes:
44
+ self.convs.append(nn.Conv1d(in_channels, layer_sizes[0], kernel_size=kernel_size, stride=stride,
45
+ padding=padding, dilation=dilation, bias=bias))
46
+ for in_size, out_size in zip(layer_sizes[:-1], layer_sizes[1:]):
47
+ self.convs.append(nn.Conv1d(in_size, out_size, kernel_size=1, bias=bias))
48
+
49
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
50
+ if x.ndim != 3:
51
+ raise RuntimeError(f"Argument x should have three dimensions, (batch, stream, channel). Given shape "
52
+ f"{tuple(x.shape)}.")
53
+ length = x.size(1) - self.kernel_size + 1
54
+ pieces = []
55
+ if self.include_original:
56
+ pieces.append(x.narrow(1, self.kernel_size - 1, length))
57
+ if self.include_time:
58
+ time = torch.linspace(0, 1, length, dtype=x.dtype, device=x.device)
59
+ pieces.append(time.view(1, length, 1).expand(x.size(0), length, 1))
60
+ if self.layer_sizes:
61
+ out = self.convs[0](x.transpose(1, 2))
62
+ for conv in self.convs[1:]:
63
+ out = conv(self.activation(out))
64
+ pieces.append(out.transpose(1, 2))
65
+ return torch.cat(pieces, dim=2)
66
+
67
+ def extra_repr(self) -> str:
68
+ return (f'activation={self.activation}, include_original={self.include_original}, '
69
+ f'include_time={self.include_time}')
sigtorch/deprecated.py ADDED
@@ -0,0 +1,21 @@
1
+ """Deprecated interfaces, kept so that code written for signatory keeps running.
2
+
3
+ Adapted from signatory's deprecated.py (Copyright 2019 Patrick Kidger, Apache License 2.0, see LICENSE-signatory);
4
+ rewritten for sigtorch's pure-PyTorch implementation.
5
+ """
6
+
7
+ import warnings
8
+ from typing import Optional
9
+
10
+ _max_parallelism = [8]
11
+
12
+
13
+ def max_parallelism(value: Optional[int] = None) -> int:
14
+ """Deprecated and does nothing (as in signatory). Use torch.set_num_threads / torch.get_num_threads instead."""
15
+ warnings.warn("max_parallelism is deprecated and now does nothing. Use torch.set_num_threads and "
16
+ "torch.get_num_threads to control the amount of parallelism.", DeprecationWarning)
17
+ if value == -1:
18
+ value = float('inf')
19
+ if value is not None:
20
+ _max_parallelism[0] = value
21
+ return _max_parallelism[0]
@@ -0,0 +1,145 @@
1
+ """The logsignature transform."""
2
+
3
+ from typing import Union
4
+
5
+ import torch
6
+ from torch import nn
7
+
8
+ from . import _tensor_algebra as ta
9
+ from . import signature_module as smodule
10
+ from . import utility
11
+
12
+ _MODES = ("words", "brackets", "expand")
13
+
14
+
15
+ def _check_mode(mode: str) -> None:
16
+ if mode not in _MODES:
17
+ raise ValueError("Invalid values for argument 'mode'. Valid values are 'expand', 'brackets', or 'words'.")
18
+
19
+
20
+ def _mobius(n: int) -> int:
21
+ result, p = 1, 2
22
+ while p * p <= n:
23
+ if n % p == 0:
24
+ n //= p
25
+ if n % p == 0:
26
+ return 0
27
+ result = -result
28
+ p += 1
29
+ return -result if n > 1 else result
30
+
31
+
32
+ def logsignature_channels(in_channels: int, depth: int) -> int:
33
+ """Number of channels of a logsignature with ``mode`` "words" or "brackets" (the dimension of the truncated free
34
+ Lie algebra, from Witt's formula)."""
35
+ if in_channels < 1:
36
+ raise ValueError("in_channels must be at least 1")
37
+ if depth < 1:
38
+ raise ValueError("depth must be at least 1")
39
+ return sum(sum(_mobius(k // d) * in_channels ** d for d in range(1, k + 1) if k % d == 0) // k
40
+ for k in range(1, depth + 1))
41
+
42
+
43
+ def _check_signature_tensor(signature: torch.Tensor, channels: int, depth: int, stream: bool,
44
+ scalar_term: bool) -> None:
45
+ ndim, dims = (3, "(batch, stream, channel)") if stream else (2, "(batch, channel)")
46
+ if signature.ndim != ndim:
47
+ raise ValueError(f"Argument 'signature' must be a {ndim}-dimensional tensor, with dimensions corresponding "
48
+ f"to {dims} respectively.")
49
+ if 0 in signature.shape:
50
+ raise ValueError("Argument 'signature' cannot have dimensions of size zero.")
51
+ if signature.size(-1) != smodule.signature_channels(channels, depth, scalar_term):
52
+ raise ValueError("Argument 'signature' has the wrong number of channels for the specified channels and depth.")
53
+ if not signature.is_floating_point():
54
+ raise ValueError("Argument 'signature' must be of floating point type.")
55
+
56
+
57
+ class SignatureToLogSignature(nn.Module):
58
+ """:class:`torch.nn.Module` wrapper around :func:`sigtorch.signature_to_logsignature`."""
59
+
60
+ def __init__(self, channels: int, depth: int, stream: bool = False, mode: str = "words",
61
+ scalar_term: bool = False, **kwargs):
62
+ super().__init__(**kwargs)
63
+ _check_mode(mode)
64
+ utility._check_channels_depth(channels, depth)
65
+ self._channels = channels
66
+ self._depth = depth
67
+ self._stream = stream
68
+ self._mode = mode
69
+ self._scalar_term = scalar_term
70
+
71
+ def forward(self, signature: torch.Tensor) -> torch.Tensor:
72
+ _check_signature_tensor(signature, self._channels, self._depth, self._stream, self._scalar_term)
73
+ levels = ta.split_levels(signature[..., int(self._scalar_term):], self._channels, self._depth)
74
+ levels = ta.log(levels)
75
+ if self._mode == "expand":
76
+ return torch.cat(levels, dim=-1)
77
+ out = []
78
+ for length, level in enumerate(levels, start=1):
79
+ words = level[..., utility.lyndon_flat_indices(self._channels, length, level.device)]
80
+ if self._mode == "brackets":
81
+ words = utility.words_to_brackets(words, self._channels, length)
82
+ out.append(words)
83
+ return torch.cat(out, dim=-1)
84
+
85
+ def extra_repr(self) -> str:
86
+ return (f'channels={self._channels}, depth={self._depth}, stream={self._stream}, mode={self._mode!r}')
87
+
88
+
89
+ SignatureToLogsignature = SignatureToLogSignature
90
+
91
+
92
+ def signature_to_logsignature(signature: torch.Tensor, channels: int, depth: int, stream: bool = False,
93
+ mode: str = "words", scalar_term: bool = False) -> torch.Tensor:
94
+ """The logsignature corresponding to a signature computed by :func:`sigtorch.signature` with the same
95
+ ``channels``, ``depth``, ``stream`` and ``scalar_term``. ``mode`` as :func:`sigtorch.logsignature`."""
96
+ return SignatureToLogSignature(channels, depth, stream, mode, scalar_term)(signature)
97
+
98
+
99
+ class LogSignature(nn.Module):
100
+ """:class:`torch.nn.Module` wrapper around :func:`sigtorch.logsignature`."""
101
+
102
+ def __init__(self, depth: int, stream: bool = False, inverse: bool = False, mode: str = "words", **kwargs):
103
+ super().__init__(**kwargs)
104
+ _check_mode(mode)
105
+ self._depth = depth
106
+ self._stream = stream
107
+ self._inverse = inverse
108
+ self._mode = mode
109
+
110
+ def prepare(self, in_channels: int) -> None:
111
+ """Builds the Lyndon tables for paths with ``in_channels`` channels ahead of the first call."""
112
+ for length in range(1, self._depth + 1):
113
+ utility._lyndon_flat_indices_cpu(in_channels, length)
114
+ if self._mode == "brackets":
115
+ utility._words_to_brackets_cpu(in_channels, length)
116
+
117
+ def forward(self, path: torch.Tensor, basepoint: Union[bool, torch.Tensor] = False) -> torch.Tensor:
118
+ signature = smodule.signature(path, self._depth, stream=self._stream, basepoint=basepoint,
119
+ inverse=self._inverse)
120
+ return signature_to_logsignature(signature, path.size(-1), self._depth, self._stream, self._mode)
121
+
122
+ def extra_repr(self) -> str:
123
+ return (f'depth={self._depth}, stream={self._stream}, inverse={self._inverse}, mode={self._mode!r}')
124
+
125
+
126
+ Logsignature = LogSignature
127
+
128
+
129
+ def logsignature(path: torch.Tensor, depth: int, stream: bool = False, basepoint: Union[bool, torch.Tensor] = False,
130
+ inverse: bool = False, mode: str = "words") -> torch.Tensor:
131
+ """Logsignature transform of a batch of paths, as ``signatory.logsignature``.
132
+
133
+ Arguments ``path``, ``depth``, ``stream``, ``basepoint`` and ``inverse`` are as in :func:`sigtorch.signature`.
134
+ ``mode`` selects the representation:
135
+
136
+ - ``"words"`` (default): coefficients of the Lyndon words in the tensor-algebra logarithm, one channel per Lyndon
137
+ word (see :func:`sigtorch.lyndon_words`).
138
+ - ``"brackets"``: coefficients in the Lyndon basis of the free Lie algebra (see :func:`sigtorch.lyndon_brackets`).
139
+ - ``"expand"``: every coefficient of the tensor-algebra logarithm; same shape as the signature.
140
+
141
+ Returns:
142
+ Tensor of shape (batch, logsignature_channels(channels, depth)) for "words"/"brackets", with a stream
143
+ dimension inserted as for :func:`sigtorch.signature` if ``stream``.
144
+ """
145
+ return LogSignature(depth, stream=stream, inverse=inverse, mode=mode)(path, basepoint=basepoint)