sktime.dists_kernels.signature_kernel 源代码

"""Signature kernels from Kiraly et al, 2016."""

__author__ = ["fkiraly"]

import collections
from functools import partial

import numpy as np
from scipy.sparse.linalg import svds
from sklearn.base import BaseEstimator, TransformerMixin

from sktime.dists_kernels.base import BasePairwiseTransformerPanel

# cumsum varia
# ------------


def _coerce_to_list_or_tuple(i):
    """Coerce integers to list of integers."""
    if not isinstance(i, (list, tuple)):
        return [i]
    else:
        return i


def cumsum_rev_first(array):
    """Reverse cumsum over 0th axis."""
    out = np.zeros_like(array)
    out[:-1] = np.cumsum(array[:0:-1], axis=0)[::-1]
    return out


def cumsum_rev(array):
    """Reverse cumsum over both axes 0 and 1."""
    out = np.zeros_like(array)
    out[:-1, :-1] = np.cumsum(np.cumsum(array[:0:-1, :0:-1], 0), 1)[::-1, ::-1]
    return out


def cumsum_mult(array, dims):
    """Cumsum over all axes in dims."""
    dims = _coerce_to_list_or_tuple(dims)
    for dimind in dims:
        array = np.cumsum(array, axis=dimind)
    return array


def roll_mult(array, shift, dims):
    """Roll over all axes in dims."""
    dims = _coerce_to_list_or_tuple(dims)
    for dimind in dims:
        array = np.roll(array, shift, axis=dimind)
    return array


def makeinds(indlist):
    """Make mesh from sequences."""
    return np.ix_(*indlist)


def cumsum_shift_mult(array, dims):
    """Apply cumsum and shift to all axes in dims."""
    array = cumsum_mult(array, dims)
    array = roll_mult(array, 1, dims)

    arrayshape = array.shape
    indarr = []
    for ind in range(len(arrayshape)):
        indarr = indarr + [range(arrayshape[ind])]

    for dimind in dims:
        slicearr = indarr[:]
        slicearr[dimind] = [0]
        array[makeinds(slicearr)] = 0

    return array


# low rank reduction utilities
# ----------------------------


def rankreduce(array, rankbound):
    """Project 2D array on top rankbound singular values."""
    arraysvd = svds(array.astype("f"), k=rankbound)
    return np.dot(arraysvd[0], np.diag(arraysvd[1]))


def rankreduce_batch(arrays, rankbound):
    """Apply rankreduce to axis 0 stack of arrays."""
    resultarrays = np.zeros([arrays.shape[0], arrays.shape[1], rankbound])
    for i in range(arrays.shape[0]):
        resultarrays[i, :, :] = rankreduce(arrays[i, :, :], rankbound)
    return resultarrays


# kernels and distances
# ---------------------


def sqdist(X, Y):
    """Row-wise squared distance between 2D array X and 2D array Y."""
    M = np.shape(X)[0]
    N = np.shape(Y)[0]
    X_squared = np.tile((X * X).sum(-1), [N, 1]).T
    Y_squared = np.tile((Y * Y).sum(-1), [M, 1])
    X_times_Y = np.inner(X, Y)
    return X_squared + Y_squared - 2 * X_times_Y


def k_polynom(x, y, scale, deg):
    """Polynomial kernel of degree deg, with scale coeff."""
    return (1 + scale * np.inner(x, y)) ** deg


def k_gauss(x, y, scale):
    """Gaussian kernel with scale coeff."""
    return np.exp(-(scale**2) * sqdist(x, y) / 2)


def k_euclid(x, y, scale):
    """Euclidean kernel with scale coeff."""
    return scale * np.inner(x, y)


def k_laplace(x, y, scale):
    """Laplace kernel with scale coeff."""
    return np.exp(-scale * np.sqrt(np.inner(x - y, x - y)))


def k_tanh(x, y, off, scale):
    """Tanh kernel with scale and offset."""
    return np.tanh(off + scale * np.inner(x, y))


def mirror(K):
    """Mirrors an upper triangular kernel matrix, helper for Sqize_kernel."""
    return K - np.diag(np.diag(K)) + np.transpose(K)


# sequential kernel implementation
# --------------------------------


def sqize_kernel(K, L, theta=1.0, normalize=False):
    """Compute the sequential kernel from a pairwise kernel matrix.

    aka compute "sequentialization of the kernel K"

    Parameters
    ----------
    K : 2D np.ndarray
        the kernel matrix of increments, i.e.,
        K[i,j] is the kernel between the i-th increment of path 1 (rows),
        and the j-th increment of path 2 (columns)
    L : an integer >= 1, representing the level of truncation
    theta : float, optional, default=1.0
        a positive scaling factor for the levels, i-th level is scaled by theta^i
    normalize : bool, optional, default = False
        whether the output kernel matrix is normalized
        if True, sums and cumsums are divided by prod(K.shape)

    Returns
    -------
    a real number, the sequential kernel between path 1 (rows of K) and path 2 (cols)
    """
    # L-1 runs through loop;
    # returns R_ij=(1+\sum_i2>i,j2>j A_i2,j2(1+\sum A_iLjL)...)
    if normalize:
        normfac = np.prod(K.shape)
        Id = np.ones(K.shape)
        R = np.ones(K.shape)
        for _ in range(L - 1):
            R = (Id + theta * cumsum_rev(K * R) / normfac) / (1 + theta)
        return (1 + theta * np.sum(K * R) / normfac) / (1 + theta)
    else:
        Id = np.ones(K.shape)
        R = np.ones(K.shape)
        for _ in range(L - 1):
            R = Id + cumsum_rev(K * R)  # A*R is componentwise
        # outermost bracket: since i1>=1 and not i1>1 we do it outside of loop
        return 1 + np.sum(K * R)


def sqize_kernel_ho(K, L, D=1, theta=1.0, normalize=False):
    """Compute the higher-order sequential kernel from a pairwise kernel matrix.

    aka compute "sequentialization of the kernel K", higher-order approximation

    Parameters
    ----------
    K : 2D np.ndarray
        the kernel matrix of increments, i.e.,
        K[i,j] is the kernel between the i-th increment of path 1 (rows),
        and the j-th increment of path 2 (columns)
    L : an integer >= 1, representing the level of truncation
    D : int, optional, default = 1
        an integer >= 1, representing the order of approximation
    theta : float, optional, default=1.0
        a positive scaling factor for the levels, i-th level is scaled by theta^i
    normalize : bool, optional, default = False
        whether the output kernel matrix is normalized
        if True, sums and cumsums are divided by prod(K.shape)

    Returns
    -------
    a real number, the sequential kernel between path 1 (rows of K) and path 2 (cols)
    """
    A = np.zeros(np.concatenate(([L, D, D], K.shape)))
    Id = np.ones(K.shape)

    for ell in range(1, L):
        Dprime = min(D, ell)
        Acs = cumsum_shift_mult(np.sum(A[ell - 1, :, :, :, :], (0, 1)), (0, 1))
        A[ell, 0, 0, :, :] = K * (Id + Acs)
        for d1 in range(1, Dprime):
            Acs1 = cumsum_shift_mult(np.sum(A[ell - 1, d1 - 1, :, :, :], 0), 1)
            Acs2 = cumsum_shift_mult(np.sum(A[ell - 1, :, d1 - 1, :, :], 0), 0)
            A[ell, d1, 0, :, :] = A[ell, d1, 0, :, :] + (1 / d1) * K * Acs1
            A[ell, :, d1, :, :] = A[ell, 0, d1, :, :] + (1 / d1) * K * Acs2

            for d2 in range(1, Dprime):
                Acs12 = cumsum_shift_mult(
                    np.sum(A[ell - 1, d1 - 1, d2 - 1, :, :], 0), 0
                )
                A[ell, d1, d2, :, :] = (
                    A[ell, d1, d2, :, :] + (1 / (d1 * d2)) * K * Acs12
                )
    return 1 + np.sum(A[L - 1, :, :, :, :])


# low-rank decomposition
# ----------------------

# LRdec object
#  models matrix A = U x V.T
#  U and V should be *arrays*, not *matrices*
LRdec = collections.namedtuple("LRdec", ["U", "V"])


def get_low_rank_matrix(K):
    """Produce the matrix from the LRdec object.

    Parameters
    ----------
    K : LRdec type object

    Returns
    -------
    2D np.ndarray the matrix K.U x K.V.T modelled by the LRdec object
    """
    return np.inner(K.U, K.V)


def add_low_rank(K, R):
    """Efficient addition of two low-rank matrices.

    efficient computation of sum of low-rank representations
    using this and then get_low_rank_matrix is more efficient than an
    explicit computation if the rank of the final matrix is not full

    Parameters
    ----------
    K, R : LRdec type objects to add

    Returns
    -------
    LRdec type object for sum of K and R
    """
    return LRdec(np.concatenate((K.U, R.U), axis=1), np.concatenate((K.V, R.V), axis=1))


def add_low_rank_one(U, P):
    """Efficient addition of low-rank matrices (symmetric)."""
    return np.concatenate((U, P), axis=1)


def mult_low_rank(K, theta):
    """Efficient multiplication of two low-rank matrices.

    efficient multiplication of sum of low-rank representations
    using this and then get_low_rank_matrix is more efficient than an
    explicit computation if the rank of the final matrix is not full

    Parameters
    ----------
    K, R : LRdec type objects to add

    Returns
    -------
    LRdec type object for product of K and R
    """
    return LRdec(theta * K.U, theta * K.V)


def hadamard_low_rank(K, R):
    """Efficient Hadamard product of two low-rank matrices.

    efficient multiplication of sum of low-rank representations
    using this and then get_low_rank_matrix is more efficient than an
    explicit computation if the rank of the final matrix is not full

    Parameters
    ----------
    K, R : LRdec type objects to add

    Returns
    -------
    LRdec type object for Hadamard product of K and R
    """
    rankK = K.U.shape[1]
    rankR = R.U.shape[1]
    U = np.tile(K.U, rankR) * np.repeat(R.U, rankK, 1)
    V = np.tile(K.V, rankR) * np.repeat(R.V, rankK, 1)
    return LRdec(U, V)


# multiplies U with every component (1st index) of P
# def HadamardLowRankBatch(U, P):
#    rankU = U.shape[1]
#    N = P.shape[0]
#    rankP = P.shape[2]
#    return (np.repeat(np.repeat(np.array(U,ndmin = 3), rankP, 2),N,0)
#           *np.repeat(P,rankU,2))


def hadamard_low_rank_batch(U, P):
    """Hadamard multiply U and P component-wise (1st)."""
    rankU = U.shape[2]
    rankP = P.shape[2]
    return np.tile(U, rankP) * np.repeat(P, rankU, 2)


def hadamard_low_rank_subsample(U, P, rho):
    """Hadamard multiply U and P component-wise (1st), with Nyström type subsampling."""
    rankU = U.shape[2]
    rankP = P.shape[2]
    permut = np.sort(np.random.permutation(range(rankU * rankP))[range(rho)])
    return (np.tile(U, rankP) * np.repeat(P, rankU, 2))[:, :, permut]


def cumsum_rev_low_rank(K):
    """Compute cumsum for LRdec type collections.

    equivalent of cumsum_rev for LRdec type objects

    Parameters
    ----------
    K : LRdec type object to compute cumsum of

    Return
    ------
    LRdec type object for cumsum_rev of K
    """
    return LRdec(cumsum_rev_first(K.U), cumsum_rev_first(K.V))


def sum_low_rank(K):
    """Compute cumsum for LRdec type collections.

    equivalent of sum for LRdec type objects

    Parameters
    ----------
    K : LRdec type object to compute sum of

    Return
    ------
    LRdec type object for sum of K
    """
    return np.inner(sum(K.U), sum(K.V))


# sequential kernel - low-rank version
# ------------------------------------


def sqize_kernel_low_rank(K, L, theta=1.0, normalize=False, rankbound=float("inf")):
    """Compute the sequential kernel from kernel matrix, with low-rank approximation.

    Parameters
    ----------
    K : 2D np.ndarray
        the kernel matrix of increments, i.e.,
        K[i,j] is the kernel between the i-th increment of path 1 (rows),
        and the j-th increment of path 2 (columns)
    L : an integer >= 1, representing the level of truncation
    theta : float, optional, default=1.0
        a positive scaling factor for the levels, i-th level is scaled by theta^i
    normalize : bool, optional, default = False
        whether the output kernel matrix is normalized
        if True, sums and cumsums are divided by prod(K.shape)
    rankbound : int, optional, default = infinity
        a hard threshold for the rank of the level matrices

    Returns
    -------
    a real number, the sequential kernel between path 1 (rows of K) and path 2 (cols)
    """
    # L-1 runs through loop;
    # returns R_ij=(1+\sum_i2>i,j2>j A_i2,j2(1+\sum A_iLjL)...)
    if normalize:
        K = get_low_rank_matrix(K)
        normfac = np.prod(K.shape)
        Id = np.ones(K.shape)
        R = np.ones(K.shape)
        for _ in range(L - 1):
            R = (Id + theta * cumsum_rev(K * R) / normfac) / (1 + theta)
        return (1 + theta * np.sum(K * R) / normfac) / (1 + theta)
    else:
        Id = LRdec(np.ones([K.U.shape[0], 1]), np.ones([K.V.shape[0], 1]))
        # Id = np.ones(K.shape)
        R = Id
        for _ in range(L - 1):
            # todo: execute only if rank is lower than rankbound
            #       reduce to rank
            R = add_low_rank(
                Id, mult_low_rank(cumsum_rev_low_rank(hadamard_low_rank(K, R)), theta)
            )
            # R = Id + cumsum_rev(K * R)
        return 1 + theta * sum_low_rank(hadamard_low_rank(K, R))
        # return 1 + np.sum(K * R)
        # outermost bracket: since i1>=1 and not i1>1 we do it outside of loop


def sqize_kernel_low_rank_fast(
    K,
    L,
    theta=1.0,
    normalize=False,
    rankbound=float("inf"),
):
    """Compute the sequential kernel from kernel matrix, with low-rank approximation.

    Vectorized across series, and using (faster) low-rank approximation across series.

    Parameters
    ----------
    K : 3D np.ndarray
        containing joint low-rank factors (symmetric singular factor)
            1st index counts sequences
            2nd index counts time
            3rd index counts features
        so K[m,:,:] is the mth factor,
        and K[m,:,:] x K[m,:,:]^t is the kernel matrix of the mth factor
    L : an integer >= 1, representing the level of truncation
    theta : float, optional, default=1.0
        a positive scaling factor for the levels, i-th level is scaled by theta^i
    normalize : bool, optional, default = False
        whether the output kernel matrix is normalized
        if True, sums and cumsums are divided by prod(K.shape)
    rankbound : int, optional, default = infinity
        a hard threshold for the rank of the level matrices

    Returns
    -------
    np.ndarray of shape (m, r), where r = min(rankbound, K.shape[2])
        a matrix R such that R*R^t is the sequential kernel matrix
        R*R^t[i,j] is sequential kernel (low-rank) between i-th and j-th sequence in K
    """
    if normalize:
        Ksize = K.shape[0]
        B = np.ones([Ksize, 1, 1])
        R = np.ones([Ksize, 1])

        for _ in range(L):
            P = np.sqrt(theta) * hadamard_low_rank_batch(K, B) / Ksize
            B = cumsum_shift_mult(P, [1])

            if rankbound < B.shape[2]:
                # B = rankreduce_batch(B, rankbound)
                permut = np.sort(
                    np.random.permutation(range(B.shape[2]))[range(rankbound)]
                )
                B = B[:, :, permut]

            R = np.concatenate((R, np.sum(B, axis=1)), axis=1) / (np.sqrt(1 + theta))

        return R

    else:
        Ksize = K.shape[0]
        B = np.ones([Ksize, 1, 1])
        R = np.ones([Ksize, 1])

        for _ in range(L):
            # todo: execute only if rank is lower than rankbound
            #       reduce to rank
            P = np.sqrt(theta) * hadamard_low_rank_batch(K, B)
            B = cumsum_shift_mult(P, [1])

            if rankbound < B.shape[2]:
                # B = rankreduce_batch(B, rankbound)
                permut = np.sort(
                    np.random.permutation(range(B.shape[2]))[range(rankbound)]
                )
                B = B[:, :, permut]

            R = np.concatenate((R, np.sum(B, axis=1)), axis=1)

        return R


# sequential kernel - wraps all versions
# --------------------------------------


def seq_kernel(
    X,
    kernelfun=None,
    L=2,
    D=1,
    theta=1.0,
    normalize=False,
    lowrank=False,
    rankbound=float("inf"),
):
    """Compute the sequential kernel between sequence/time series.

    Provides interface for vanilla sequential kernel, low-rank, and higher-order.

    Parameters
    ----------
    X : 3D np.ndarray of shape (N, d, _)
        collection of sequences/time series
        1st index = instance index
        2nd index = variable/feature index
        3rd index = time index
    kernelfun : function (2D np.ndarray x 2D np.ndarray) -> 2D np.ndarray
        pairwise kernel function, matrix sizes (n, d) x (m, d) -> (n x m)
        optional, default = Euclidean (linear) kernel with scale parameter 1
    L : int, optional, default = 2
        an integer >= 1, representing the level of truncation
    D : int, optional, default = 1
        an integer >= 1, representing the order of approximation
        can be set only if lowrank = False, otherwise ignored (always = 1)
    theta : float, optional, default=1.0
        a positive scaling factor for the levels, i-th level is scaled by theta^i
    normalize : bool, optional, default = False
        whether the output kernel matrix is normalized
        if True, sums and cumsums are divided by prod(K.shape)
    lowrank : bool, optional, default = False
        whether to use low rank approximation in computing the kernel
    rankbound : int, optional, default = infinity
        a hard threshold for the rank of the level matrices
        used only if lowrank = True

    Returns
    -------
    np.ndarray of shape (N, N), sequential kernel matrix
        [i,j]-th entry is sequential kernel between X[i] and X[j]
    """
    N = np.shape(X)[0]
    KSeq = np.zeros((N, N))

    if kernelfun is None:
        kernelfun = partial(k_euclid, scale=1)

    if not lowrank:
        if D == 1:
            for row1ind in range(N):
                for row2ind in range(row1ind + 1):
                    KSeq[row1ind, row2ind] = sqize_kernel(
                        K=kernelfun(X[row1ind].T, X[row2ind].T),
                        L=L,
                        theta=theta,
                        normalize=normalize,
                    )
        else:
            for row1ind in range(N):
                for row2ind in range(row1ind + 1):
                    KSeq[row1ind, row2ind] = sqize_kernel_ho(
                        K=kernelfun(X[row1ind].T, X[row2ind].T),
                        L=L,
                        D=D,
                        theta=theta,
                        normalize=normalize,
                    )
    else:
        R = sqize_kernel_low_rank_fast(
            K=X.transpose([0, 2, 1]),
            L=L,
            theta=theta,
            normalize=normalize,
            rankbound=rankbound,
        )
        KSeq = np.inner(R, R)
        # todo: kernelfun gives back a LRdec object
        #  for now, linear low-rank approximation is done
        # KSeq[row1ind,row2ind] = qqize_kernel_low_rank(
        #   kernelfun(X[row1ind].T,X[row2ind].T),L,theta,normalize = True)
    return mirror(KSeq)


def seq_kernel_XY(
    X,
    Y=None,
    kernelfun=None,
    L=2,
    D=1,
    theta=1.0,
    normalize=False,
    lowrank=False,
    rankbound=float("inf"),
):
    """Compute the sequential kernel between two different collections of sequence.

    Provides interface for vanilla sequential kernel, low-rank, and higher-order.

    Parameters
    ----------
    X : 3D np.ndarray of shape (N, d, _)
        collection of sequences/time series
        1st index = instance index
        2nd index = variable/feature index
        3rd index = time index
    Y : 3D np.ndarray of shape (M, d, _)
        collection of sequences/time series
        1st index = instance index
        2nd index = variable/feature index
        3rd index = time index
    kernelfun : function (2D np.ndarray x 2D np.ndarray) -> 2D np.ndarray
        pairwise kernel function, matrix sizes (n, d) x (m, d) -> (n x m)
        optional, default = Euclidean (linear) kernel with scale parameter 1
    L : int, optional, default = 2
        an integer >= 1, representing the level of truncation
    D : int, optional, default = 1
        an integer >= 1, representing the order of approximation
        can be set only if lowrank = False, otherwise ignored (always = 1)
    theta : float, optional, default=1.0
        a positive scaling factor for the levels, i-th level is scaled by theta^i
    normalize : bool, optional, default = False
        whether the output kernel matrix is normalized
        if True, sums and cumsums are divided by prod(K.shape)
    lowrank : bool, optional, default = False
        whether to use low rank approximation in computing the kernel
    rankbound : int, optional, default = infinity
        a hard threshold for the rank of the level matrices
        used only if lowrank = True

    Returns
    -------
    np.ndarray of shape (N, M), sequential kernel matrix
        [i,j]-th entry is sequential kernel between X[i] and Y[j]
    """
    # if no Y is passed, call seq_kernel
    if Y is None:
        return seq_kernel(
            X=X,
            kernelfun=kernelfun,
            L=L,
            D=D,
            theta=theta,
            normalize=normalize,
            lowrank=lowrank,
            rankbound=rankbound,
        )

    N = np.shape(X)[0]
    M = np.shape(Y)[0]

    KSeq = np.zeros((N, M))

    if kernelfun is None:
        kernelfun = partial(k_euclid, scale=1)

    kwargs = {"L": L, "theta": theta, "normalize": normalize}

    if not lowrank:
        if D == 1:
            for row1ind in range(N):
                for row2ind in range(M):
                    KSeq[row1ind, row2ind] = sqize_kernel(
                        K=kernelfun(X[row1ind].T, Y[row2ind].T), **kwargs
                    )
        else:
            for row1ind in range(N):
                for row2ind in range(M):
                    KSeq[row1ind, row2ind] = sqize_kernel_ho(
                        K=kernelfun(X[row1ind].T, Y[row2ind].T), D=D, **kwargs
                    )
    else:
        U = sqize_kernel_low_rank_fast(
            K=X.transpose([0, 2, 1]), rankbound=rankbound, **kwargs
        )
        V = sqize_kernel_low_rank_fast(
            K=Y.transpose([0, 2, 1]), rankbound=rankbound, **kwargs
        )
        KSeq = np.inner(U, V)
        # KSeq = np.inner(sqize_kernel_low_rank_fast(X, L, theta, normalize),
        # wqize_kernel_low_rank_fast(Y, L, theta, normalize))

    return KSeq


def data_tabulator(X):
    """Tabulates sequence 3D np.ndarray into sklearn compatible 2D np.ndarray format."""
    Xshape = np.shape(X)
    return np.reshape(X, (Xshape[0], np.prod(Xshape[1:])))


def time_series_reshaper(Xflat, numfeatures, subsample=1, differences=True):
    """Convert 2D np.ndarray into a time series 3D np.ndarray.

    Useful as part of sklearn pipeline for internal conversion to time series 3D format.

    optionally, subsamples or differences time series

    Parameters
    ----------
    Xflat : 2D np.ndarray (instances, flattened time series)
    numfeatures : number of features/variables in the time series
    subsample : int, optional, default = 1
        time index step size to sub-sample
    differences : bool, optional, default = True
        whether to take first temporal differences (True) or not (False)

    Returns
    -------
    Xflat, as a 3D array
        regular subsampling is applied if subsample > 1
        differencing is applied (after subsampling) if differences=True
    """
    flatXshape = np.shape(Xflat)
    Xshape = (flatXshape[0], numfeatures, flatXshape[1] / numfeatures)
    X = np.reshape(Xflat, Xshape)[:, :, ::subsample]

    if differences:
        return np.diff(X)
    else:
        return X


# scikit-learn estimator (original 2016 paper)
# --------------------------------------------
# for historical reasons - old scikit-learn version of the estimator
class SeqKernelizer(BaseEstimator, TransformerMixin):
    """Compute the sequential kernel matrix row features on collection of series.

    Original implementation of signature kernel in [1]_ and [2]_.
    The algorithm used therein is sklearn GridSearchCV wrapping
    the sklearn pipeline of SeqKernelizer and SVC.

    Identical behaviour can be obtained in sktime as
    a grid search wrapped DistanceFeatures(SignatureKernel()) * SVC() pipeline

    Included for historical purposes only, as reference to original paper code,
    and for reproduction of the original experiments in the JMLR publication.
    Users and developers should use/modify SequentialKernel instead.

    This sklearn estimator requires passing of integer "numfeatures" as parameter,
    and will interpret rows of X as time series with ``numfeatures`` features/vars,
    and X.shape[1]/numfeatures time stamps, reshaped in (vars, time stamps) order.

    In transform, will transform a series to the row of the kernel matrix
    between that series and all the series seen in fit,, via seq_kernel_XY.

    Parameters
    ----------
    level : int, optional, default = 2
        an integer >= 1, representing the level of truncation of the sequential kernel
    degree : int, optional, default = 1
        an integer >= 1, representing the order of approximation of sequential kernel
        can be set only if lowrank = False, otherwise ignored (always = 1)
    theta : float, optional, default=1.0
        a positive scaling factor for the levels, i-th level is scaled by theta^i
    kernel : str, one of "linear", "Gauss", "Laplace", "poly"
        code for inner kernel in the sequential kernel, with kernel parameters
        "linear" - Euclidean kernel with scale parameter
        "Gauss" - Gaussian kernel with scale parameter
        "Laplace" - Laplace kernel with scale parameter
        "poly" - polynomial kernel with degree deg and scale parameter
    scale : float, optional, default = 1.0
        a positive scaling factor for the inner kernel
    degree : int, optional, default = 1, used only for polynomial kernel (kernel="poly")
        degree of the polynomial kernel (if used)
    numfeatures : int, optional, default = 2
        number of features/variables in the time series
    subsample : int, optional, default = 1
        time index step size to sub-sample
    differences : bool, optional, default = True
        whether to take first temporal differences (True) or not (False)
    normalize : bool, optional, default = False
        whether the output kernel matrix is normalized
        if True, sums and cumsums are divided by prod(K.shape)
    lowrank : bool, optional, default = False
        whether to use low rank approximation in computing the kernel
    rankbound : int, optional, default = infinity
        a hard threshold for the rank of the level matrices
        used only if lowrank = True

    References
    ----------
    .. [1] F. Kiraly, H. Oberhauser. 2016. "Kernels for sequentially ordered data.",
        arXiv: 1601.08169.
    .. [2] F. Kiraly, H. Oberhauser. 2019. "Kernels for sequentially ordered data.",
        Journal of Machine Learning Research.
    """

    def __init__(
        self,
        level=2,
        degree=1,
        theta=1,
        kernel="linear",
        scale=1,
        deg=2,
        numfeatures=2,
        subsample=100,
        differences=True,
        normalize=False,
        lowrank=False,
        rankbound=float("inf"),
    ):
        self.level = level
        self.degree = degree
        self.theta = theta
        self.kernel = kernel
        self.scale = scale
        self.deg = deg
        self.numfeatures = numfeatures
        self.subsample = subsample
        self.differences = differences
        self.normalize = normalize
        self.lowrank = lowrank
        self.rankbound = rankbound

        self._reshape_kwargs = {
            "numfeatures": numfeatures,
            "subsample": subsample,
            "differences": differences,
        }
        self._kern_kwargs = {
            "level": level,
            "degree": degree,
            "theta": theta,
            "normalize": normalize,
            "lowrank": lowrank,
            "rankbound": rankbound,
        }

    def fit(self, X, y=None):
        """Fit = reshape the series X."""
        self._X = time_series_reshaper(X, **self._reshape_kwargs)
        return self

    def transform(self, X):
        """Transform the data to kernel matrix rows."""
        X = time_series_reshaper(X, **self._reshape_kwargs)

        def kernselect(kername):
            switcher = {
                "linear": partial(k_euclid, scale=self.scale),
                "Gauss": partial(k_gauss, scale=self.scale),
                "Laplace": partial(k_laplace, scale=self.scale),
                "poly": partial(k_polynom, scale=self.scale, deg=self.deg),
            }
            return switcher.get(kername, "nothing")

        KSeq = seq_kernel_XY(X, self._X, kernselect(self.kernel), **self._kern_kwargs)

        return KSeq


# sktime interface - pairwise transformer
# ---------------------------------------


[文档]class SignatureKernel(BasePairwiseTransformerPanel): """Time series signature kernel, including high-order and low-rank variants. Implements the signature kernel of Kiraly et al, see [1]_ and [2]_, including higher-order and low-rank approximation variants described therein. Parameters ---------- kernel : sktime pairwise (tabular) transformer, callable, or None inner (tabular) kernel used in the signature sequence kernel if callable: function (2D np.ndarray x 2D np.ndarray) -> 2D np.ndarray pairwise kernel function, matrix sizes (n, d) x (m, d) -> (n x m) optional, default = None = Euclidean (linear) kernel with scale parameter 1 level : int, optional, default = 2 an integer >= 1, representing the level of truncation of the sequential kernel degree : int, optional, default = 1 an integer >= 1, representing the order of approximation of sequential kernel can be set only if lowrank = False, otherwise ignored (always = 1) theta : float, optional, default=1.0 a positive scaling factor for the levels, i-th level is scaled by theta^i normalize : bool, optional, default = False whether the output kernel matrix is normalized if True, sums and cumsums are divided by prod(K.shape) lowrank : bool, optional, default = False whether to use low rank approximation in computing the kernel rankbound : int, optional, default = infinity a hard threshold for the rank of the level matrices used only if lowrank = True References ---------- .. [1] F. Kiraly, H. Oberhauser. 2016. "Kernels for sequentially ordered data.", arXiv: 1601.08169. .. [2] F. Kiraly, H. Oberhauser. 2019. "Kernels for sequentially ordered data.", Journal of Machine Learning Research. """ _tags = { "authors": "fkiraly", "X_inner_mtype": "numpy3D", "pwtrafo_type": "kernel", } def __init__( self, kernel=None, level=2, degree=1, theta=1, normalize=False, lowrank=False, rankbound=float("inf"), ): self.kernel = kernel self.level = level self.degree = degree self.theta = theta self.normalize = normalize self.lowrank = lowrank self.rankbound = rankbound if kernel is None: self._kernel = partial(k_euclid, scale=1) else: self._kernel = kernel self._kern_kwargs = { "L": level, "D": degree, "theta": theta, "normalize": normalize, "lowrank": lowrank, "rankbound": rankbound, } super().__init__() def _transform(self, X, X2=None): """Compute distance/kernel matrix. private _transform containing core logic, called from public transform Behaviour: returns pairwise distance/kernel matrix between samples in X and X2 if X2 is not passed, is equal to X if X/X2 is a pd.DataFrame and contains non-numeric columns, these are removed before computation Parameters ---------- X: 3D np.array of shape [num_instances, num_vars, num_time_points] X2: 3D np.array of shape [num_instances, num_vars, num_time_points], optional default X2 = X Returns ------- distmat: np.array of shape [n, m] (i,j)-th entry contains distance/kernel between X[i] and X2[j] """ kwargs = self._kern_kwargs kernel = self._kernel if X2 is None: return seq_kernel(X, kernelfun=kernel, **kwargs) else: return seq_kernel_XY(X, X2, kernelfun=kernel, **kwargs)
[文档] @classmethod def get_test_params(cls, parameter_set="default"): """Return testing parameter settings for the estimator. Parameters ---------- parameter_set : str, default="default" Name of the set of test parameters to return, for use in tests. If no special parameters are defined for a value, will return ``"default"`` set. There are currently no reserved values for distance/kernel transformers. Returns ------- params : dict or list of dict, default = {} Parameters to create testing instances of the class Each dict are parameters to construct an "interesting" test instance, i.e., ``MyClass(**params)`` or ``MyClass(**params[i])`` creates a valid test instance. ``create_test_instance`` uses the first (or only) dictionary in ``params`` """ param1 = {} # test higher level and normalization param2 = {"level": 3, "normalize": True} # test higher-order function param3 = {"degree": 2} # test low-rank approximation param4 = {"lowrank": True} paramlist = [param1, param2, param3, param4] return paramlist