123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575 |
- # -*- coding: utf-8 -*-
- """
- GPFA util functions.
- :copyright: Copyright 2015-2019 by the Elephant team, see AUTHORS.txt.
- :license: Modified BSD, see LICENSE.txt for details.
- """
- from __future__ import division, print_function, unicode_literals
- import warnings
- import numpy as np
- import quantities as pq
- import scipy as sp
- from elephant.conversion import BinnedSpikeTrain
- from elephant.utils import deprecated_alias
- @deprecated_alias(binsize='bin_size')
- def get_seqs(data, bin_size, use_sqrt=True):
- """
- Converts the data into a rec array using internally BinnedSpikeTrain.
- Parameters
- ----------
- data : list of list of neo.SpikeTrain
- The outer list corresponds to trials and the inner list corresponds to
- the neurons recorded in that trial, such that data[l][n] is the
- spike train of neuron n in trial l. Note that the number and order of
- neo.SpikeTrains objects per trial must be fixed such that data[l][n]
- and data[k][n] refer to the same spike generator for any choice of l,k
- and n.
- bin_size: quantity.Quantity
- Spike bin width
- use_sqrt: bool
- Boolean specifying whether or not to use square-root transform on
- spike counts (see original paper for motivation).
- Default: True
- Returns
- -------
- seq : np.recarray
- data structure, whose nth entry (corresponding to the nth experimental
- trial) has fields
- T : int
- number of timesteps in the trial
- y : (yDim, T) np.ndarray
- neural data
- Raises
- ------
- ValueError
- if `bin_size` is not a pq.Quantity.
- """
- if not isinstance(bin_size, pq.Quantity):
- raise ValueError("'bin_size' must be of type pq.Quantity")
- seqs = []
- for dat in data:
- sts = dat
- binned_spiketrain = BinnedSpikeTrain(sts, bin_size=bin_size)
- if use_sqrt:
- binned = np.sqrt(binned_spiketrain.to_array())
- else:
- binned = binned_spiketrain.to_array()
- seqs.append(
- (binned_spiketrain.n_bins, binned))
- seqs = np.array(seqs, dtype=[('T', np.int), ('y', 'O')])
- # Remove trials that are shorter than one bin width
- if len(seqs) > 0:
- trials_to_keep = seqs['T'] > 0
- seqs = seqs[trials_to_keep]
- return seqs
- def cut_trials(seq_in, seg_length=20):
- """
- Extracts trial segments that are all of the same length. Uses
- overlapping segments if trial length is not integer multiple
- of segment length. Ignores trials with length shorter than
- one segment length.
- Parameters
- ----------
- seq_in : np.recarray
- data structure, whose nth entry (corresponding to the nth experimental
- trial) has fields
- T : int
- number of timesteps in trial
- y : (yDim, T) np.ndarray
- neural data
- seg_length : int
- length of segments to extract, in number of timesteps. If infinite,
- entire trials are extracted, i.e., no segmenting.
- Default: 20
- Returns
- -------
- seqOut : np.recarray
- data structure, whose nth entry (corresponding to the nth experimental
- trial) has fields
- T : int
- number of timesteps in segment
- y : (yDim, T) np.ndarray
- neural data
- Raises
- ------
- ValueError
- If `seq_length == 0`.
- """
- if seg_length == 0:
- raise ValueError("At least 1 extracted trial must be returned")
- if np.isinf(seg_length):
- seqOut = seq_in
- return seqOut
- dtype_seqOut = [('segId', np.int), ('T', np.int),
- ('y', np.object)]
- seqOut_buff = []
- for n, seqIn_n in enumerate(seq_in):
- T = seqIn_n['T']
- # Skip trials that are shorter than segLength
- if T < seg_length:
- warnings.warn(
- 'trial corresponding to index {} shorter than one segLength...'
- 'skipping'.format(n))
- continue
- numSeg = np.int(np.ceil(float(T) / seg_length))
- # Randomize the sizes of overlaps
- if numSeg == 1:
- cumOL = np.array([0, ])
- else:
- totalOL = (seg_length * numSeg) - T
- probs = np.ones(numSeg - 1, np.float) / (numSeg - 1)
- randOL = np.random.multinomial(totalOL, probs)
- cumOL = np.hstack([0, np.cumsum(randOL)])
- seg = np.empty(numSeg, dtype_seqOut)
- seg['T'] = seg_length
- for s, seg_s in enumerate(seg):
- tStart = seg_length * s - cumOL[s]
- seg_s['y'] = seqIn_n['y'][:, tStart:tStart + seg_length]
- seqOut_buff.append(seg)
- if len(seqOut_buff) > 0:
- seqOut = np.hstack(seqOut_buff)
- else:
- seqOut = np.empty(0, dtype_seqOut)
- return seqOut
- def rdiv(a, b):
- """
- Returns the solution to x b = a. Equivalent to MATLAB right matrix
- division: a / b
- """
- return np.linalg.solve(b.T, a.T).T
- def logdet(A):
- """
- log(det(A)) where A is positive-definite.
- This is faster and more stable than using log(det(A)).
- Written by Tom Minka
- (c) Microsoft Corporation. All rights reserved.
- """
- U = np.linalg.cholesky(A)
- return 2 * (np.log(np.diag(U))).sum()
- def make_k_big(params, n_timesteps):
- """
- Constructs full GP covariance matrix across all state dimensions and
- timesteps.
- Parameters
- ----------
- params : dict
- GPFA model parameters
- n_timesteps : int
- number of timesteps
- Returns
- -------
- K_big : np.ndarray
- GP covariance matrix with dimensions (xDim * T) x (xDim * T).
- The (t1, t2) block is diagonal, has dimensions xDim x xDim, and
- represents the covariance between the state vectors at timesteps t1 and
- t2. K_big is sparse and striped.
- K_big_inv : np.ndarray
- Inverse of K_big
- logdet_K_big : float
- Log determinant of K_big
- Raises
- ------
- ValueError
- If `params['covType'] != 'rbf'`.
- """
- if params['covType'] != 'rbf':
- raise ValueError("Only 'rbf' GP covariance type is supported.")
- xDim = params['C'].shape[1]
- K_big = np.zeros((xDim * n_timesteps, xDim * n_timesteps))
- K_big_inv = np.zeros((xDim * n_timesteps, xDim * n_timesteps))
- Tdif = np.tile(np.arange(0, n_timesteps), (n_timesteps, 1)).T \
- - np.tile(np.arange(0, n_timesteps), (n_timesteps, 1))
- logdet_K_big = 0
- for i in range(xDim):
- K = (1 - params['eps'][i]) * np.exp(-params['gamma'][i] / 2 *
- Tdif ** 2) \
- + params['eps'][i] * np.eye(n_timesteps)
- K_big[i::xDim, i::xDim] = K
- # the original MATLAB program uses here a special algorithm, provided
- # in C and MEX, for inversion of Toeplitz matrix:
- # [K_big_inv(idx+i, idx+i), logdet_K] = invToeplitz(K);
- # TODO: use an inversion method optimized for Toeplitz matrix
- # Below is an attempt to use such a method, not leading to a speed-up.
- # # K_big_inv[i::xDim, i::xDim] = sp.linalg.solve_toeplitz((K[:, 0],
- # K[0, :]), np.eye(T))
- K_big_inv[i::xDim, i::xDim] = np.linalg.inv(K)
- logdet_K = logdet(K)
- logdet_K_big = logdet_K_big + logdet_K
- return K_big, K_big_inv, logdet_K_big
- def inv_persymm(M, blk_size):
- """
- Inverts a matrix that is block persymmetric. This function is
- faster than calling inv(M) directly because it only computes the
- top half of inv(M). The bottom half of inv(M) is made up of
- elements from the top half of inv(M).
- WARNING: If the input matrix M is not block persymmetric, no
- error message will be produced and the output of this function will
- not be meaningful.
- Parameters
- ----------
- M : (blkSize*T, blkSize*T) np.ndarray
- The block persymmetric matrix to be inverted.
- Each block is blkSize x blkSize, arranged in a T x T grid.
- blk_size : int
- Edge length of one block
- Returns
- -------
- invM : (blkSize*T, blkSize*T) np.ndarray
- Inverse of M
- logdet_M : float
- Log determinant of M
- """
- T = int(M.shape[0] / blk_size)
- Thalf = np.int(np.ceil(T / 2.0))
- mkr = blk_size * Thalf
- invA11 = np.linalg.inv(M[:mkr, :mkr])
- invA11 = (invA11 + invA11.T) / 2
- # Multiplication of a sparse matrix by a dense matrix is not supported by
- # SciPy. Making A12 a sparse matrix here an error later.
- off_diag_sparse = False
- if off_diag_sparse:
- A12 = sp.sparse.csr_matrix(M[:mkr, mkr:])
- else:
- A12 = M[:mkr, mkr:]
- term = invA11.dot(A12)
- F22 = M[mkr:, mkr:] - A12.T.dot(term)
- res12 = rdiv(-term, F22)
- res11 = invA11 - res12.dot(term.T)
- res11 = (res11 + res11.T) / 2
- # Fill in bottom half of invM by picking elements from res11 and res12
- invM = fill_persymm(np.hstack([res11, res12]), blk_size, T)
- logdet_M = -logdet(invA11) + logdet(F22)
- return invM, logdet_M
- def fill_persymm(p_in, blk_size, n_blocks, blk_size_vert=None):
- """
- Fills in the bottom half of a block persymmetric matrix, given the
- top half.
- Parameters
- ----------
- p_in : (xDim*Thalf, xDim*T) np.ndarray
- Top half of block persymmetric matrix, where Thalf = ceil(T/2)
- blk_size : int
- Edge length of one block
- n_blocks : int
- Number of blocks making up a row of Pin
- blk_size_vert : int, optional
- Vertical block edge length if blocks are not square.
- `blk_size` is assumed to be the horizontal block edge length.
- Returns
- -------
- Pout : (xDim*T, xDim*T) np.ndarray
- Full block persymmetric matrix
- """
- if blk_size_vert is None:
- blk_size_vert = blk_size
- Nh = blk_size * n_blocks
- Nv = blk_size_vert * n_blocks
- Thalf = np.int(np.floor(n_blocks / 2.0))
- THalf = np.int(np.ceil(n_blocks / 2.0))
- Pout = np.empty((blk_size_vert * n_blocks, blk_size * n_blocks))
- Pout[:blk_size_vert * THalf, :] = p_in
- for i in range(Thalf):
- for j in range(n_blocks):
- Pout[Nv - (i + 1) * blk_size_vert:Nv - i * blk_size_vert,
- Nh - (j + 1) * blk_size:Nh - j * blk_size] \
- = p_in[i * blk_size_vert:(i + 1) *
- blk_size_vert,
- j * blk_size:(j + 1) * blk_size]
- return Pout
- def make_precomp(seqs, xDim):
- """
- Make the precomputation matrices specified by the GPFA algorithm.
- Usage: [precomp] = makePautoSum( seq , xDim )
- Parameters
- ----------
- seqs : np.recarray
- The sequence struct of inferred latents, etc.
- xDim : int
- The dimension of the latent space.
- Returns
- -------
- precomp : np.recarray
- The precomp struct will be updated with the posterior covaraince and
- the other requirements.
- Notes
- -----
- All inputs are named sensibly to those in `learnGPparams`.
- This code probably should not be called from anywhere but there.
- We bother with this method because we
- need this particular matrix sum to be
- as fast as possible. Thus, no error checking
- is done here as that would add needless computation.
- Instead, the onus is on the caller (which should be
- learnGPparams()) to make sure this is called correctly.
- Finally, see the notes in the GPFA README.
- """
- Tall = seqs['T']
- Tmax = (Tall).max()
- Tdif = np.tile(np.arange(0, Tmax), (Tmax, 1)).T \
- - np.tile(np.arange(0, Tmax), (Tmax, 1))
- # assign some helpful precomp items
- # this is computationally cheap, so we keep a few loops in MATLAB
- # for ease of readability.
- precomp = np.empty(xDim, dtype=[(
- 'absDif', np.object), ('difSq', np.object), ('Tall', np.object),
- ('Tu', np.object)])
- for i in range(xDim):
- precomp[i]['absDif'] = np.abs(Tdif)
- precomp[i]['difSq'] = Tdif ** 2
- precomp[i]['Tall'] = Tall
- # find unique numbers of trial lengths
- trial_lengths_num_unique = np.unique(Tall)
- # Loop once for each state dimension (each GP)
- for i in range(xDim):
- precomp_Tu = np.empty(len(trial_lengths_num_unique), dtype=[(
- 'nList', np.object), ('T', np.int), ('numTrials', np.int),
- ('PautoSUM', np.object)])
- for j, trial_len_num in enumerate(trial_lengths_num_unique):
- precomp_Tu[j]['nList'] = np.where(Tall == trial_len_num)[0]
- precomp_Tu[j]['T'] = trial_len_num
- precomp_Tu[j]['numTrials'] = len(precomp_Tu[j]['nList'])
- precomp_Tu[j]['PautoSUM'] = np.zeros((trial_len_num,
- trial_len_num))
- precomp[i]['Tu'] = precomp_Tu
- # at this point the basic precomp is built. The previous steps
- # should be computationally cheap. We now try to embed the
- # expensive computation in a MEX call, defaulting to MATLAB if
- # this fails. The expensive computation is filling out PautoSUM,
- # which we initialized previously as zeros.
- ############################################################
- # Fill out PautoSum
- ############################################################
- # Loop once for each state dimension (each GP)
- for i in range(xDim):
- # Loop once for each trial length (each of Tu)
- for j in range(len(trial_lengths_num_unique)):
- # Loop once for each trial (each of nList)
- for n in precomp[i]['Tu'][j]['nList']:
- precomp[i]['Tu'][j]['PautoSUM'] += seqs[n]['VsmGP'][:, :, i] \
- + np.outer(seqs[n]['latent_variable'][i, :],
- seqs[n]['latent_variable'][i, :])
- return precomp
- def grad_betgam(p, pre_comp, const):
- """
- Gradient computation for GP timescale optimization.
- This function is called by minimize.m.
- Parameters
- ----------
- p : float
- variable with respect to which optimization is performed,
- where :math:`p = log(1 / timescale^2)`
- pre_comp : np.recarray
- structure containing precomputations
- const : dict
- contains hyperparameters
- Returns
- -------
- f : float
- value of objective function E[log P({x},{y})] at p
- df : float
- gradient at p
- """
- Tall = pre_comp['Tall']
- Tmax = Tall.max()
- # temp is Tmax x Tmax
- temp = (1 - const['eps']) * np.exp(-np.exp(p) / 2 * pre_comp['difSq'])
- Kmax = temp + const['eps'] * np.eye(Tmax)
- dKdgamma_max = -0.5 * temp * pre_comp['difSq']
- dEdgamma = 0
- f = 0
- for j in range(len(pre_comp['Tu'])):
- T = pre_comp['Tu'][j]['T']
- Thalf = np.int(np.ceil(T / 2.0))
- Kinv = np.linalg.inv(Kmax[:T, :T])
- logdet_K = logdet(Kmax[:T, :T])
- KinvM = Kinv[:Thalf, :].dot(dKdgamma_max[:T, :T]) # Thalf x T
- KinvMKinv = (KinvM.dot(Kinv)).T # Thalf x T
- dg_KinvM = np.diag(KinvM)
- tr_KinvM = 2 * dg_KinvM.sum() - np.fmod(T, 2) * dg_KinvM[-1]
- mkr = np.int(np.ceil(0.5 * T ** 2))
- numTrials = pre_comp['Tu'][j]['numTrials']
- PautoSUM = pre_comp['Tu'][j]['PautoSUM']
- pauto_kinv_dot = PautoSUM.ravel('F')[:mkr].dot(
- KinvMKinv.ravel('F')[:mkr])
- pauto_kinv_dot_rest = PautoSUM.ravel('F')[-1:mkr - 1:- 1].dot(
- KinvMKinv.ravel('F')[:(T ** 2 - mkr)])
- dEdgamma = dEdgamma - 0.5 * numTrials * tr_KinvM \
- + 0.5 * pauto_kinv_dot \
- + 0.5 * pauto_kinv_dot_rest
- f = f - 0.5 * numTrials * logdet_K \
- - 0.5 * (PautoSUM * Kinv).sum()
- f = -f
- # exp(p) is needed because we're computing gradients with
- # respect to log(gamma), rather than gamma
- df = -dEdgamma * np.exp(p)
- return f, df
- def orthonormalize(x, l):
- """
- Orthonormalize the columns of the loading matrix and apply the
- corresponding linear transform to the latent variables.
- In the following description, yDim and xDim refer to data dimensionality
- and latent dimensionality, respectively.
- Parameters
- ----------
- x : (xDim, T) np.ndarray
- Latent variables
- l : (yDim, xDim) np.ndarray
- Loading matrix
- Returns
- -------
- latent_variable_orth : (xDim, T) np.ndarray
- Orthonormalized latent variables
- Lorth : (yDim, xDim) np.ndarray
- Orthonormalized loading matrix
- TT : (xDim, xDim) np.ndarray
- Linear transform applied to latent variables
- """
- xDim = l.shape[1]
- if xDim == 1:
- TT = np.sqrt(np.dot(l.T, l))
- Lorth = rdiv(l, TT)
- latent_variable_orth = np.dot(TT, x)
- else:
- UU, DD, VV = sp.linalg.svd(l, full_matrices=False)
- # TT is transform matrix
- TT = np.dot(np.diag(DD), VV)
- Lorth = UU
- latent_variable_orth = np.dot(TT, x)
- return latent_variable_orth, Lorth, TT
- def segment_by_trial(seqs, x, fn):
- """
- Segment and store data by trial.
- Parameters
- ----------
- seqs : np.recarray
- Data structure that has field T, the number of timesteps
- x : np.ndarray
- Data to be segmented (any dimensionality x total number of timesteps)
- fn : str
- New field name of seq where segments of X are stored
- Returns
- -------
- seqs_new : np.recarray
- Data structure with new field `fn`
- Raises
- ------
- ValueError
- If `seqs['T']) != x.shape[1]`.
- """
- if np.sum(seqs['T']) != x.shape[1]:
- raise ValueError('size of X incorrect.')
- dtype_new = [(i, seqs[i].dtype) for i in seqs.dtype.names]
- dtype_new.append((fn, np.object))
- seqs_new = np.empty(len(seqs), dtype=dtype_new)
- for dtype_name in seqs.dtype.names:
- seqs_new[dtype_name] = seqs[dtype_name]
- ctr = 0
- for n, T in enumerate(seqs['T']):
- seqs_new[n][fn] = x[:, ctr:ctr + T]
- ctr += T
- return seqs_new
|