/
ssvkernel.py
323 lines (271 loc) · 9.96 KB
/
ssvkernel.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
import numpy as np
def ssvkernel(x, tin=None, M=80, nbs=1e2, WinFunc='Boxcar'):
"""
Generates a locally adaptive kernel-density estimate for one-dimensional
data.
The user provides a one-dimensional vector of samples drawn from some
underlying unknown distribution, and optionally the values where they want
to estimate the probability density of that distribution. The algorithm
solves an optimization problem to identify variable bandwidths across the
domain where the data is provided.
The optimization is based on a principle of minimizing expected L2 loss
function between the kernel estimate and an unknown underlying density
function. An assumption is merely that samples are drawn from the density
independently of each other.
The locally adaptive bandwidth is obtained by iteratively computing optimal
fixed-size bandwidths wihtihn local intervals. The optimal bandwidths are
selected such that they are selected in the intervals that are gamma times
larger than the optimal bandwidths themselves. The paramter gamma is
optimized by minimizing the L2 risk estimate.
Parameters
----------
x : array_like
The one-dimensional samples drawn from the underlying density
tin : array_like, optional
The values where the density estimate is to be evaluated in generating
the output 'y'. Default value = None.
M : int, optional
The number of window sizes to evaluate. Default value = 80.
nbs : int, optional
The number of bootstrap samples to use in estimating the [0.05, 0.95]
confidence interval of the output 'y'.
WinFunc : string, optional
The type of window function to use in estimating local bandwidth.
Choose from one of 'Boxcar', 'Laplace', 'Cauchy' and 'Gauss'. Default
value = 'Gauss'.
Returns
-------
y : array_like
The estimated density, evaluated at points t / tin.
t : array_like
The points where the density estimate 'y' is evaluated.
optw : array_like
The optimal local kernel bandwidths at 't'.
gs : array_like
The stiffness constants of the variables bandwidths evaluated.
C : array_like
Cost functions associated with stiffness constraints.
confb95 : array_like
The 5% and 95% confidence interval of the kernel density estimate 'y'.
Has dimensions 2 x len(y). confb95[0,:] corresponds to the 5% interval,
and confb95[1,:] corresponds to the 95% interval.
yb : array_like
The bootstrap samples used in estimating confb95. Each row corresponds
to one bootstrap sample.
See Also
--------
sshist, sskernel
References
----------
.. [1] H. Shimazaki and S. Shinomoto, "Kernel Bandwidth Optimization in
Spike Rate Estimation," in Journal of Computational Neuroscience
29(1-2): 171–182, 2010 http://dx.doi.org/10.1007/s10827-009-0180-4
"""
# set argument 't' if not provided
if tin is None:
T = np.max(x) - np.min(x)
dx = np.sort(np.diff(np.sort(x)))
dt_samp = dx[np.nonzero(dx)][0]
tin = np.linspace(np.min(x), np.max(x), min(np.ceil(T / dt_samp), 1e3))
t = tin
x_ab = x[(x >= min(tin)) & (x <= max(tin))]
else:
T = np.max(x) - np.min(x)
x_ab = x[(x >= min(tin)) & (x <= max(tin))]
dx = np.sort(np.diff(np.sort(x)))
dt_samp = dx[np.nonzero(dx)][0]
if dt_samp > min(np.diff(tin)):
t = np.linspace(min(tin), max(tin), min(np.ceil(T / dt_samp), 1e3))
else:
t = tin
# calculate delta t
dt = min(np.diff(t))
# create the finest histogram
thist = np.concatenate((t, (t[-1]+dt)[np.newaxis]))
y_hist = np.histogram(x_ab, thist-dt/2)[0] / dt
L = y_hist.size
N = sum(y_hist * dt).astype(np.float)
# initialize window sizes
W = logexp(np.linspace(ilogexp(5 * dt), ilogexp(T), M))
# compute local cost functions
c = np.zeros((M, L))
for j in range(M):
w = W[j]
yh = fftkernel(y_hist, w / dt)
c[j, :] = yh**2 - 2 * yh * y_hist + 2 / (2 * np.pi)**0.5 / w * y_hist
# initialize optimal ws
optws = np.zeros((M, L))
for i in range(M):
Win = W[i]
C_local = np.zeros((M, L))
for j in range(M):
C_local[j, :] = fftkernelWin(c[j, :], Win / dt, WinFunc)
n = np.argmin(C_local, axis=0)
optws[i, :] = W[n]
# golden section search for stiffness parameter of variable bandwidths
k = 0
gs = np.zeros((30, 1))
C = np.zeros((30, 1))
tol = 1e-5
a = 1e-12
b = 1
phi = (5**0.5 + 1) / 2
c1 = (phi - 1) * a + (2 - phi) * b
c2 = (2 - phi) * a + (phi - 1) * b
f1 = CostFunction(y_hist, N, t, dt, optws, W, WinFunc, c1)[0]
f2 = CostFunction(y_hist, N, t, dt, optws, W, WinFunc, c2)[0]
while (np.abs(b-a) > tol * (abs(c1) + abs(c2))) & (k < 30):
if f1 < f2:
b = c2
c2 = c1
c1 = (phi - 1) * a + (2 - phi) * b
f2 = f1
f1, yv1, optwp1 = CostFunction(y_hist, N, t, dt, optws, W,
WinFunc, c1)
yopt = yv1 / np.sum(yv1 * dt)
optw = optwp1
else:
a = c1
c1 = c2
c2 = (2 - phi) * a + (phi - 1) * b
f1 = f2
f2, yv2, optwp2 = CostFunction(y_hist, N, t, dt, optws, W,
WinFunc, c2)
yopt = yv2 / np.sum(yv2 * dt)
optw = optwp2
# capture estimates and increment iteration counter
gs[k] = c1
C[k] = f1
k = k + 1
# discard unused entries in gs, C
gs = gs[0:k]
C = C[0:k]
# estimate confidence intervals by bootstrapping
nbs = np.asarray(nbs)
yb = np.zeros((nbs, tin.size))
for i in range(nbs):
Nb = np.random.poisson(lam=N)
idx = np.random.randint(0, N, Nb)
xb = x_ab[idx]
thist = np.concatenate((t, (t[-1]+dt)[np.newaxis]))
y_histb = np.histogram(xb, thist - dt / 2)[0]
idx = y_histb.nonzero()
y_histb_nz = y_histb[idx]
t_nz = t[idx]
yb_buf = np.zeros((L, ))
for k in range(L):
yb_buf[k] = np.sum(y_histb_nz * Gauss(t[k] - t_nz, optw[k])) / Nb
yb_buf = yb_buf / np.sum(yb_buf * dt)
yb[i, :] = np.interp(tin, t, yb_buf)
ybsort = np.sort(yb, axis=0)
y95b = ybsort[np.int(np.floor(0.05 * nbs)), :]
y95u = ybsort[np.int(np.floor(0.95 * nbs)), :]
confb95 = np.concatenate((y95b[np.newaxis], y95u[np.newaxis]), axis=0)
# return outputs
y = np.interp(tin, t, yopt)
optw = np.interp(tin, t, optw)
t = tin
return y, t, optw, gs, C, confb95, yb
def CostFunction(y_hist, N, t, dt, optws, WIN, WinFunc, g):
L = y_hist.size
optwv = np.zeros((L, ))
for k in range(L):
gs = optws[:, k] / WIN
if g > np.max(gs):
optwv[k] = np.min(WIN)
else:
if g < min(gs):
optwv[k] = np.max(WIN)
else:
idx = np.max(np.nonzero(gs >= g))
optwv[k] = g * WIN[idx]
# Nadaraya-Watson kernel regression
optwp = np.zeros((L, ))
for k in range(L):
if WinFunc == 'Boxcar':
Z = Boxcar(t[k]-t, optwv / g)
elif WinFunc == 'Laplace':
Z = Laplace(t[k]-t, optwv / g)
elif WinFunc == 'Cauchy':
Z = Cauchy(t[k]-t, optwv / g)
else: # WinFunc == 'Gauss'
Z = Gauss(t[k]-t, optwv / g)
optwp[k] = np.sum(optwv * Z) / np.sum(Z)
# speed-optimized baloon estimator
idx = y_hist.nonzero()
y_hist_nz = y_hist[idx]
t_nz = t[idx]
yv = np.zeros((L, ))
for k in range(L):
yv[k] = np.sum(y_hist_nz * dt * Gauss(t[k]-t_nz, optwp[k]))
yv = yv * N / np.sum(yv * dt)
# cost function of estimated kernel
cg = yv**2 - 2 * yv * y_hist + 2 / (2 * np.pi)**0.5 / optwp * y_hist
Cg = np.sum(cg * dt)
return Cg, yv, optwp
def fftkernel(x, w):
# forward padded transform
L = x.size
Lmax = L + 3 * w
n = 2 ** np.ceil(np.log2(Lmax))
X = np.fft.fft(x, n.astype(np.int))
# generate kernel domain
f = np.linspace(0, n-1, n) / n
f = np.concatenate((-f[0: np.int(n / 2 + 1)],
f[1: np.int(n / 2 - 1 + 1)][::-1]))
# evaluate kernel
K = np.exp(-0.5 * (w * 2 * np.pi * f) ** 2)
# convolve and transform back from frequency domain
y = np.real(np.fft.ifft(X * K, n))
y = y[0:L]
return y
def fftkernelWin(x, w, WinFunc):
# forward padded transform
L = x.size
Lmax = L + 3 * w
n = 2 ** np.ceil(np.log2(Lmax))
X = np.fft.fft(x, n.astype(np.int))
# generate kernel domain
f = np.linspace(0, n-1, n) / n
f = np.concatenate((-f[0: np.int(n / 2 + 1)],
f[1: np.int(n / 2 - 1 + 1)][::-1]))
t = 2 * np.pi * f
# determine window function - evaluate kernel
if WinFunc == 'Boxcar':
a = 12**0.5 * w
K = 2 * np.sin(a * t / 2) / (a * t)
K[0] = 1
elif WinFunc == 'Laplace':
K = 1 / (1 + (w * 2 * np.pi * f)**2 / 2)
elif WinFunc == 'Cauchy':
K = np.exp(-w * np.abs(2 * np.pi * f))
else: # WinFunc == 'Gauss'
K = np.exp(-0.5 * (w * 2 * np.pi * f)**2)
# convolve and transform back from frequency domain
y = np.real(np.fft.ifft(X * K, n))
y = y[0:L]
return y
def Gauss(x, w):
y = 1 / (2 * np.pi)**2 / w * np.exp(-x**2 / 2 / w**2)
return y
def Laplace(x, w):
y = 1 / 2**0.5 / w * np.exp(-(2**0.5) / w / np.abs(x))
return y
def Cauchy(x, w):
y = 1 / (np.pi * w * (1 + (x / w)**2))
return y
def Boxcar(x, w):
a = 12**0.5 * w
y = 1 / a
y[np.abs(x) > a / 2] = 0
return y
def logexp(x):
y = np.zeros(x.shape)
y[x < 1e2] = np.log(1+np.exp(x[x < 1e2]))
y[x >= 1e2] = x[x >= 1e2]
return y
def ilogexp(x):
y = np.zeros(x.shape)
y[x < 1e2] = np.log(np.exp(x[x < 1e2]) - 1)
y[x >= 1e2] = x[x >= 1e2]
return y