def setup(obj): torch.set_num_threads(1) obj.path = torch.rand(obj.size, dtype=torch.float, requires_grad=True) shape = obj.size[-3], signatory.logsignature_channels(obj.size[-1], obj.depth) obj.grad = torch.rand(shape) obj.logsignature = signatory.LogSignature(obj.depth)(obj.path)
def setup(obj): obj.path = torch.rand(obj.size, dtype=torch.float, requires_grad=True, device='cuda') shape = obj.size[-3], signatory.logsignature_channels( obj.size[-1], obj.depth) obj.grad = torch.rand(shape, device='cuda') obj.logsignature = signatory.LogSignature(obj.depth)(obj.path)
def __init__(self, input_dim, depth, logsig, basepoint=True): super().__init__() self.basepoint = basepoint self.depth = depth self.logsig = logsig self.sig_func = signatory.logsignature if self.logsig else signatory.signature self.terms = signatory.logsignature_channels( input_dim, self.depth) if self.logsig else signatory.signature_channels( input_dim, self.depth) if self.terms >= TERMS_LIMIT: raise ImportError( "Number of signature terms {} is greater than {}..".format( self.terms, TERMS_LIMIT))
def transform(self, sequence) -> torch.Tensor: embedded = sequence['embedded'] sig_func = signatory.logsignature if self.logsig else signatory.signature terms = signatory.logsignature_channels( embedded.shape[-1], self.depth) if self.logsig else signatory.signature_channels( embedded.shape[-1], self.depth) if terms >= TERMS_LIMIT: raise ImportError( "Number of signature terms is greater than {}..".format( TERMS_LIMIT)) signature_terms = sig_func(path=embedded, depth=self.depth, basepoint=self.basepoint) return signature_terms
def __init__(self, in_channels, depth, p=2, include_time=True, pseudometric=True, metric_type='general'): """ Called with some path `f` and some shapelet `g` such that ``` f, g \colon [0, T] \to R^in_channels, ``` which are described by as being the unique continuous piecewise linear functions such that ``` f(times[i]) == path1[i] for all i, g(times[i]) == path2[i] for all i, ``` where `times` is a strictly increasing 1D tensor of shape (length,) for some value `length`, and `path1` is a tensor of shape (..., length, in_channels) and `path2` is a tensor of shape (*, length, in_channels), where '...' and '*' represent potentially different batch dimensions. Then let `q = logsig(f, depth) - logsig(g, depth)` be the difference in their logsignatures to depth `depth`, which will be a vector of size `l = logsignature_channels(in_channels, depth)`. Then this discrepancy calculates ``` ||Aq||_p ``` where ||.||_p denotes the L^p vector norm, and A is a matrix of shape (l, l), which: - will be the identity matrix if pseudometric==False - will be learnt and diagonal if pseudometric=True and metric_type == 'diagonal' - will be learnt and square if pseudometric==True and metric_type == 'general'. The return value will be a tensor of shape (..., *). Arguments: in_channels: The number of input channels of the path. depth: An integer describing the depth of the logsignature transform to take. p: A number in [1, \infty] specifying the parameter p of the distance. Defaults to 2. include_time: Boolean. Whether to take the logsignature discrepancy of the time-augmented path or not. Defaults to True. (Setting this to False produces a pseudometric rather similar in spirit to dynamic time warping, in that it's reparameterisation invariant.) pseudometric: Whether to take a learnt linear transformation beforehand. Defaults to True. metric_type: Either 'general' or 'diagonal'. Whether to take a general learnt linear transformation or just a diagonal one. Defaults to 'general'. """ super(LogsignatureDiscrepancy, self).__init__() assert metric_type in ('general', 'diagonal'), "Valid values for 'metric_type' are 'general' and 'diagonal'." if signatory is None: raise ImportError("Signatory must be installed to compute logsignature discrepancies. It can be found at " "`https://github.com/patrick-kidger/signatory`. See also the installation instructions " "for `torchshapelets` at " "`https://github.com/jambo6/generalised_shapelets/tree/master/torchshapelets`. ") self.in_channels = in_channels self.depth = depth self.p = p self.include_time = include_time self.pseudometric = pseudometric self.metric_type = metric_type if pseudometric: channels = in_channels if include_time: channels += 1 logsignature_channels = signatory.logsignature_channels(channels, depth) if metric_type == 'general': linear = torch.randn(logsignature_channels, logsignature_channels, requires_grad=True) torch.nn.init.kaiming_uniform_(linear, a=math.sqrt(5)) else: linear = torch.randn(logsignature_channels, requires_grad=True) torch.nn.init.uniform_(linear, 0.9, 1.1) self.linear = torch.nn.Parameter(linear) else: self.register_parameter('linear', None) self.logsignature = signatory.Logsignature(depth=depth)
def _logsignature_windows(x, depth, window_length, t, _version): t = misc.validate_input_path(x, t) # slightly roundabout way of doing things (rather than using arange) so that it's constructed differentiably timespan = t[-1] - t[0] num_pieces = (timespan / window_length).ceil().to(int).item() end_t = t[0] + num_pieces * window_length new_t = torch.linspace(t[0], end_t, num_pieces + 1, dtype=t.dtype, device=t.device) new_t = torch.min(new_t, t.max()) t_index = 0 new_t_unique = [] new_t_indices = [] for new_t_elem in new_t: while True: lequal = (new_t_elem <= t[t_index]) close = new_t_elem.allclose(t[t_index]) if lequal or close: break t_index += 1 new_t_indices.append(t_index + len(new_t_unique)) if close: continue new_t_unique.append(new_t_elem.unsqueeze(0)) batch_dimensions = x.shape[:-2] missing_X = torch.full((1, ), float('nan'), dtype=x.dtype, device=x.device).expand(*batch_dimensions, 1, x.size(-1)) if len(new_t_unique) > 0: # no-op if len == 0, so skip for efficiency t, indices = torch.cat([t, *new_t_unique]).sort() x = torch.cat([x, missing_X], dim=-2)[..., indices.clamp(0, x.size(-2)), :] # Fill in any missing data linearly (linearly because that's what signatures do in between observations anyway) # and conveniently that's what this already does. Here 'missing data' includes the NaNs we've just added. x = interpolation_linear.linear_interpolation_coeffs(x, t) # Flatten batch dimensions for compatibility with Signatory flatten_X = x.view(-1, x.size(-2), x.size(-1)) first_increment = torch.zeros(*batch_dimensions, signatory.logsignature_channels( x.size(-1), depth), dtype=x.dtype, device=x.device) first_increment[..., :x.size(-1)] = x[..., 0, :] logsignatures = [first_increment] compute_logsignature = signatory.Logsignature(depth=depth) for index, next_index, time, next_time in zip(new_t_indices[:-1], new_t_indices[1:], new_t[:-1], new_t[1:]): logsignature = compute_logsignature(flatten_X[..., index:next_index + 1, :]) logsignature = logsignature.view(*batch_dimensions, -1) if _version == 0: logsignature = logsignature * (next_time - time) elif _version == 1: pass else: raise RuntimeError logsignatures.append(logsignature) logsignatures = torch.stack(logsignatures, dim=-2) logsignatures = logsignatures.cumsum(dim=-2) if _version == 0: return logsignatures, new_t elif _version == 1: return logsignatures else: raise RuntimeError
def logsignature_windows(x, depth, window_length, t=None): """Calculates logsignatures over multiple windows, for the batch of controls given, as in the log-ODE method. This corresponds to a transform of the time series, and should be used prior to applying one of the interpolation schemes. Arguments: x: tensor of values, of shape (..., length, input_channels), where ... is some number of batch dimensions. This is interpreted as a (batch of) paths taking values in an input_channels-dimensional real vector space, with length-many observations. Missing values are supported, and should be represented as NaNs. depth: What depth to compute the logsignatures to. window_length: How long a time interval to compute logsignatures over. t: Optional one dimensional tensor of times. Must be monotonically increasing. If not passed will default to tensor([0., 1., ..., length - 1]). Warning: If there are missing values then calling this function can be pretty slow. Make sure to cache the result, and don't reinstantiate it on every forward pass, if at all possible. Returns: A tuple of two tensors, which are the values and times of the transformed path. """ t = misc.validate_input_path(x, t) # slightly roundabout way of doing things (rather than using arange) so that it's constructed differentiably timespan = t[-1] - t[0] num_pieces = (timespan / window_length).ceil().to(int).item() end_t = t[0] + num_pieces * window_length new_t = torch.linspace(t[0], end_t, num_pieces + 1, dtype=t.dtype, device=t.device) new_t = torch.min(new_t, t.max()) t_index = 0 new_t_unique = [] new_t_indices = [] for new_t_elem in new_t: while True: lequal = (new_t_elem <= t[t_index]) close = new_t_elem.allclose(t[t_index]) if lequal or close: break t_index += 1 new_t_indices.append(t_index + len(new_t_unique)) if close: continue new_t_unique.append(new_t_elem.unsqueeze(0)) batch_dimensions = x.shape[:-2] missing_X = torch.full((1, ), float('nan'), dtype=x.dtype, device=x.device).expand(*batch_dimensions, 1, x.size(-1)) if len(new_t_unique) > 0: # no-op if len == 0, so skip for efficiency t, indices = torch.cat([t, *new_t_unique]).sort() x = torch.cat([x, missing_X], dim=-2)[..., indices.clamp(0, x.size(-2)), :] # Fill in any missing data linearly (linearly because that's what signatures do in between observations anyway) # and conveniently that's what this already does. Here 'missing data' includes the NaNs we've just added. x = interpolation_linear.linear_interpolation_coeffs(x, t) # Flatten batch dimensions for compatibility with Signatory flatten_X = x.view(-1, x.size(-2), x.size(-1)) first_increment = torch.zeros(*batch_dimensions, signatory.logsignature_channels( x.size(-1), depth), dtype=x.dtype, device=x.device) first_increment[..., :x.size(-1)] = x[..., 0, :] logsignatures = [first_increment] compute_logsignature = signatory.Logsignature(depth=depth) for index, next_index, time, next_time in zip(new_t_indices[:-1], new_t_indices[1:], new_t[:-1], new_t[1:]): logsignature = compute_logsignature(flatten_X[..., index:next_index + 1, :]) logsignature = logsignature.view(*batch_dimensions, -1) * (next_time - time) logsignatures.append(logsignature) logsignatures = torch.stack(logsignatures, dim=-2) logsignatures = logsignatures.cumsum(dim=-2) return logsignatures, new_t