sparsetrace commited on
Commit
966a16d
·
verified ·
1 Parent(s): a29ae7b

Update LISAx.py

Browse files
Files changed (1) hide show
  1. LISAx.py +81 -131
LISAx.py CHANGED
@@ -1,13 +1,18 @@
1
  # LISA.py
2
  # ============================================================
3
- # LISA: NLSA encoder + GPLM nonlinear baseline decoder
4
- # + dense in-context Gaussian Process Regression (GPR)
5
  # on residuals in diffusion coordinate space.
6
  #
7
- # This version adds:
8
- # - Automatic inducing/anchor selection for GPLM (m auto-scales with N)
9
- # based on a compute budget (default 1000**3).
10
- # - Optional cap on IC context windows to avoid O(K_ctx^2) blowups at call-time.
 
 
 
 
 
11
  # ============================================================
12
 
13
  from __future__ import annotations
@@ -29,12 +34,9 @@ except Exception:
29
  from NLSA import NLSA # type: ignore
30
 
31
  try:
32
- from .GPLM import GPLM # type: ignore
33
  except Exception:
34
- try:
35
- from GPLM import GPLM # type: ignore
36
- except Exception:
37
- from gplm import GPLM # type: ignore
38
 
39
 
40
  # ============================================================
@@ -95,65 +97,25 @@ def _estimate_rbf_ell_from_d2(
95
  return float(max(ell, min_ell))
96
 
97
 
98
- def _auto_gplm_m(
99
- n_train: int,
100
- *,
101
- budget: int = 1000**3,
102
- m_min: int = 32,
103
- m_max: Optional[int] = None,
104
- ) -> int:
105
- """
106
- Choose inducing count m for GPLM given training size n_train,
107
- constrained by an approximate compute budget.
108
-
109
- GPLM training costs (dominant terms):
110
- - Accumulating G = C^T C: O(n_train * m^2)
111
- - Cholesky on A (m x m): O(m^3)
112
-
113
- We choose:
114
- m <= budget^(1/3) (controls m^3)
115
- m <= sqrt(budget/n) (controls n*m^2)
116
-
117
- and of course:
118
- m <= n_train
119
- m >= m_min
120
-
121
- This yields:
122
- - n=1000 -> m=1000 (exact within budget)
123
- - n=5000 -> m≈447
124
- - n=50000 -> m≈141
125
- """
126
- n = int(max(1, n_train))
127
- b = int(max(1, budget))
128
-
129
- m_cubic = int(np.floor(b ** (1.0 / 3.0))) # budget for m^3
130
- m_nm2 = int(np.floor(np.sqrt(b / float(n)))) # budget for n*m^2
131
-
132
- m = min(n, m_cubic, m_nm2)
133
- m = max(int(m_min), int(m))
134
-
135
- if m_max is not None:
136
- m = min(m, int(m_max))
137
-
138
- # final sanity
139
- m = int(max(1, min(m, n)))
140
- return m
141
-
142
-
143
  # ============================================================
144
  # LISA main class
145
  # ============================================================
146
 
147
  class LISA:
148
  """
149
- LISA: NLSA encoder + GPLM baseline + dense GPR residual correction.
 
 
 
 
150
 
151
- - If prefix length ℓ == L: no in-context pairs -> baseline AR only.
152
- - If ℓ > L: residual GP is fit on the prefix windows and applied during rollout.
 
153
 
154
- Notes:
155
- - Residual GP is multi-output with independent output dims sharing a kernel.
156
- That means GP mean is vector in R^D, but variance is a scalar (same for all dims).
157
  """
158
 
159
  def __init__(
@@ -171,11 +133,6 @@ class LISA:
171
  nlsa_kwargs: Optional[dict] = None,
172
  # ------------------ GPLM baseline decoder hyperparams ----------
173
  gplm_kwargs: Optional[dict] = None,
174
- # --- NEW: auto anchor/inducing scaling for large N -------------
175
- gplm_auto_m: bool = True,
176
- gplm_budget: int = 1000**3, # your requested budget baseline
177
- gplm_m_min: int = 32,
178
- gplm_m_max: Optional[int] = None,
179
  # ------------------ IC / GPR controls -------------------------
180
  ctx_min_windows: Optional[int] = None,
181
  ctx_k0: float = 10.0, # base mixing K_ctx/(K_ctx+ctx_k0)
@@ -183,8 +140,8 @@ class LISA:
183
  gp_kernel: str = "rbf", # "rbf" or "linear"
184
  gp_rbf_ell: float | None = None, # fixed, or auto from context
185
  gp_rbf_q: float = 0.5,
186
- # --- NEW: prevent call-time blowups when prefix is huge --------
187
- gp_max_ctx: Optional[int] = 4096, # cap context windows used by dense GP
188
  gp_ctx_mode: str = "recent", # "recent" or "uniform"
189
  # ------------------ stability / trust gating -----------------
190
  use_var_gate: bool = True,
@@ -219,7 +176,7 @@ class LISA:
219
  self.D = int(base.D_)
220
  self.r = int(base.psi_.shape[1])
221
 
222
- # mean handling: baseline GPLM will be trained in *centered output space*
223
  self.center_outputs = bool(base.center)
224
  self.mu_X = base.mu_.reshape(-1).astype(np.float64) if self.center_outputs else np.zeros((self.D,), dtype=np.float64)
225
 
@@ -235,32 +192,46 @@ class LISA:
235
  dtype=np.float64
236
  ) # (K-1,D) centered if base.center
237
 
 
238
  gkw = dict(gplm_kwargs)
239
  gkw.setdefault("seed", int(seed))
240
- gkw.setdefault("center_X", False) # IMPORTANT: Y_train is already centered
241
- gkw.setdefault("sigma2", 1e-5)
 
 
 
 
 
 
 
242
  gkw.setdefault("jitter", 1e-8)
243
- gkw.setdefault("inducing", "kmeans_medoids")
244
- gkw.setdefault("dtype", np.float32)
245
- gkw.setdefault("fit_block", 8192)
246
-
247
- # ------------------------------------------------------------
248
- # NEW: automatic inducing m scaling (for N up to 50k+)
249
- # ------------------------------------------------------------
250
- if "m" not in gkw:
251
- if bool(gplm_auto_m):
252
- m_auto = _auto_gplm_m(
253
- X_train.shape[0],
254
- budget=int(gplm_budget),
255
- m_min=int(gplm_m_min),
256
- m_max=gplm_m_max,
257
- )
258
- gkw["m"] = int(m_auto)
259
- else:
260
- gkw["m"] = min(1024, X_train.shape[0])
261
 
262
- self.gplm_m_ = int(gkw["m"])
263
- self.gplm_budget_ = int(gplm_budget)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
 
265
  self.gplm = GPLM(X_train, Y_train_c, **gkw)
266
 
@@ -278,7 +249,7 @@ class LISA:
278
  self.gp_rbf_q = float(gp_rbf_q)
279
  self.last_gp_rbf_ell_: Optional[float] = None
280
 
281
- # NEW: cap call-time dense GP context size
282
  self.gp_max_ctx = None if gp_max_ctx is None else int(gp_max_ctx)
283
  self.gp_ctx_mode = str(gp_ctx_mode).lower().strip()
284
  if self.gp_ctx_mode not in ("recent", "uniform"):
@@ -315,7 +286,7 @@ class LISA:
315
  Psi = np.asarray(Psi_Br, dtype=np.float64)
316
  if Psi.ndim == 1:
317
  Psi = Psi[None, :]
318
- Yc = self.gplm(Psi) # (B,D)
319
  return np.asarray(Yc, dtype=np.float64)
320
 
321
  def _baseline_centered_one(self, psi_r: np.ndarray) -> np.ndarray:
@@ -335,7 +306,6 @@ class LISA:
335
  if self.gp_kernel == "linear":
336
  return Psi_ctx @ Psi_ctx.T, None
337
 
338
- # RBF
339
  d2 = _pairwise_sq_dists(Psi_ctx)
340
  if self.gp_rbf_ell is None:
341
  ell_used = _estimate_rbf_ell_from_d2(d2, q=self.gp_rbf_q)
@@ -382,7 +352,6 @@ class LISA:
382
 
383
  if self.gate_mode == "exp":
384
  return float(np.exp(-v / tau2))
385
- # rational
386
  return float(tau2 / (tau2 + v))
387
 
388
  # ============================================================
@@ -417,22 +386,8 @@ class LISA:
417
  """
418
  Autoregressive forecast from prefix (ℓ,D), ℓ >= L.
419
 
420
- If ℓ == L (IC nullset): baseline AR only (NLSA+GPLM).
421
- If ℓ > L: uses in-context dense GPR on residuals.
422
-
423
- Parameters
424
- ----------
425
- prefix : (ℓ,D)
426
- steps : horizon H
427
- return_var : return scalar GP function variance per step (H,)
428
- sample : sample GP residual instead of mean (generative mode)
429
- rng : RNG for sampling
430
- include_obs_noise : if sample=True, draw using var_y = var_f + gp_noise2
431
-
432
- Returns
433
- -------
434
- preds : (H,D) (or (D,) if H==1)
435
- vars : (H,) if return_var=True
436
  """
437
  prefix = _as_2d(prefix)
438
  ell, D = prefix.shape
@@ -469,7 +424,7 @@ class LISA:
469
  return preds[0] if H == 1 else preds
470
 
471
  # ---------------------------
472
- # Dense GP context cap (NEW)
473
  # ---------------------------
474
  if self.gp_max_ctx is None:
475
  K_ctx = int(K_ctx_full)
@@ -481,17 +436,16 @@ class LISA:
481
  start = int(K_ctx_full - K_ctx)
482
  ctx_idx = None
483
  else:
484
- # uniform subsample of context indices
485
  ctx_idx = np.linspace(0, K_ctx_full - 1, num=K_ctx, dtype=np.int64)
486
- start = 0 # unused
487
 
488
  # ---------------------------
489
  # Build context windows + targets
490
  # ---------------------------
491
- W_all = _sliding_windows(prefix, self.L) # (K_n,L,D), K_n = ell-L+1
492
 
493
  if ctx_idx is None:
494
- W_ctx = np.ascontiguousarray(W_all[start : start + K_ctx, :, :]) # (K_ctx,L,D)
495
  Y_ctx = np.asarray(prefix[self.L + start : self.L + start + K_ctx, :], dtype=np.float64)
496
  else:
497
  W_ctx = np.ascontiguousarray(W_all[ctx_idx, :, :])
@@ -501,26 +455,24 @@ class LISA:
501
  Y_ctx_c = (Y_ctx - self.mu_X[None, :]) if self.center_outputs else Y_ctx
502
 
503
  # encode ψ for context
504
- Psi_ctx = self._encode_batch(W_ctx) # (K_ctx,r)
505
 
506
  # baseline predictions on context
507
- Y_glob_ctx_c = self._baseline_centered_batch(Psi_ctx) # (K_ctx,D)
508
 
509
  # residual table
510
- E_ctx = Y_ctx_c - Y_glob_ctx_c # (K_ctx,D)
511
 
512
  # ---------------------------
513
  # Fit dense GP on residuals
514
  # ---------------------------
515
- K_mat, ell_used = self._gp_kernel_matrix(Psi_ctx) # (K_ctx,K_ctx)
516
  K_reg = K_mat + self.gp_noise2 * np.eye(K_ctx, dtype=np.float64)
517
 
518
- # Cholesky factorization
519
  cf = cho_factor(K_reg, lower=True, check_finite=False)
520
- alpha = cho_solve(cf, E_ctx, check_finite=False) # (K_ctx,D)
521
  Lfac, lower = cf
522
 
523
- # base context mixing weight
524
  w_ctx_base = float(K_ctx) / float(K_ctx + self.ctx_k0) if self.ctx_k0 > 0 else 1.0
525
 
526
  preds = np.zeros((H, self.D), dtype=np.float64)
@@ -530,14 +482,14 @@ class LISA:
530
  # AR rollout with GP residual
531
  # ---------------------------
532
  for h in range(H):
533
- psi_q = self.base.encode_window(cur) # (r,)
534
 
535
  # baseline
536
- y_glob_c = self._baseline_centered_one(psi_q) # (D,)
537
 
538
  # GP residual mean
539
- k_eval = self._gp_kernel_eval(Psi_ctx, psi_q, ell_used) # (K_ctx,)
540
- e_mean = k_eval @ alpha # (D,)
541
 
542
  # GP residual variance (function variance)
543
  u = solve_triangular(Lfac, k_eval, lower=lower, check_finite=False)
@@ -547,11 +499,9 @@ class LISA:
547
  if return_var:
548
  vars_out[h] = float(var_f)
549
 
550
- # trust gating from variance
551
  w_gate = self._gate_from_var(var_f)
552
  w_eff = w_ctx_base * w_gate
553
 
554
- # optionally sample residual
555
  e_use = e_mean
556
  if sample:
557
  var_y = var_f + (self.gp_noise2 if include_obs_noise else 0.0)
@@ -578,7 +528,7 @@ class LISA:
578
 
579
  def _rollout_baseline(self, seed_LD: np.ndarray, H: int) -> np.ndarray:
580
  """
581
- Baseline AR rollout only: NLSA encode + GPLM decode.
582
  """
583
  cur = np.asarray(seed_LD, dtype=np.float64).copy()
584
  out = np.zeros((H, self.D), dtype=np.float64)
 
1
  # LISA.py
2
  # ============================================================
3
+ # LISA: NLSA encoder + GPLM baseline decoder
4
+ # + (optional) dense in-context Gaussian Process Regression (GPR)
5
  # on residuals in diffusion coordinate space.
6
  #
7
+ # This version is compatible with the "sparse-kernel KRR" GPLM.py:
8
+ # - GPLM trains by building a sparse kNN kernel on training latents ψ
9
+ # - solves (K + sigma2 I) S = Y (mean GP / KRR)
10
+ # - predicts using only pred_k neighbors per query
11
+ #
12
+ # Key advantages vs Nyström GPLM:
13
+ # - No anchors/inducing points required
14
+ # - Strong nonlinearity => very local sparse kernel => scalable
15
+ #
16
  # ============================================================
17
 
18
  from __future__ import annotations
 
34
  from NLSA import NLSA # type: ignore
35
 
36
  try:
37
+ from .GPLMx import GPLM # type: ignore
38
  except Exception:
39
+ from GPLMx import GPLM # type: ignore
 
 
 
40
 
41
 
42
  # ============================================================
 
97
  return float(max(ell, min_ell))
98
 
99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  # ============================================================
101
  # LISA main class
102
  # ============================================================
103
 
104
  class LISA:
105
  """
106
+ LISA: NLSA encoder + GPLM baseline + optional dense GPR residual correction.
107
+
108
+ - Baseline:
109
+ window W (L,D) -> ψ(W) in R^r (via NLSA Nyström OOS)
110
+ y_glob = GPLM(ψ) in R^D
111
 
112
+ - In-context (optional):
113
+ Given prefix length ℓ > L, fit dense GP on residuals e(ψ)
114
+ using context windows inside prefix, then apply during rollout.
115
 
116
+ Note:
117
+ The IC GP here is dense and scales O(K_ctx^2) memory / O(K_ctx^3) time.
118
+ A default cap gp_max_ctx prevents accidental blowups with huge prefixes.
119
  """
120
 
121
  def __init__(
 
133
  nlsa_kwargs: Optional[dict] = None,
134
  # ------------------ GPLM baseline decoder hyperparams ----------
135
  gplm_kwargs: Optional[dict] = None,
 
 
 
 
 
136
  # ------------------ IC / GPR controls -------------------------
137
  ctx_min_windows: Optional[int] = None,
138
  ctx_k0: float = 10.0, # base mixing K_ctx/(K_ctx+ctx_k0)
 
140
  gp_kernel: str = "rbf", # "rbf" or "linear"
141
  gp_rbf_ell: float | None = None, # fixed, or auto from context
142
  gp_rbf_q: float = 0.5,
143
+ # ---- NEW safety: cap dense GP context used in __call__ -------
144
+ gp_max_ctx: Optional[int] = 4096, # None disables cap
145
  gp_ctx_mode: str = "recent", # "recent" or "uniform"
146
  # ------------------ stability / trust gating -----------------
147
  use_var_gate: bool = True,
 
176
  self.D = int(base.D_)
177
  self.r = int(base.psi_.shape[1])
178
 
179
+ # mean handling (consistent w/ NLSA centering)
180
  self.center_outputs = bool(base.center)
181
  self.mu_X = base.mu_.reshape(-1).astype(np.float64) if self.center_outputs else np.zeros((self.D,), dtype=np.float64)
182
 
 
192
  dtype=np.float64
193
  ) # (K-1,D) centered if base.center
194
 
195
+ # ---- Defaults tuned for sparse-kernel GPLM ----
196
  gkw = dict(gplm_kwargs)
197
  gkw.setdefault("seed", int(seed))
198
+
199
+ # IMPORTANT: LISA already provides centered Y, so disable GPLM centering
200
+ gkw.setdefault("center_X", False)
201
+
202
+ # Regularization for sparse KRR (lambda in K + lambda I)
203
+ # (You may tune this up if solves are noisy/unstable.)
204
+ gkw.setdefault("sigma2", 1e-4)
205
+
206
+ # Numerical diagonal stabilizer
207
  gkw.setdefault("jitter", 1e-8)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
 
209
+ # Sparse kernel build on ψ_train
210
+ gkw.setdefault("k_graph", 64) # training graph sparsity
211
+ gkw.setdefault("mutual_knn", True)
212
+ gkw.setdefault("include_self", True)
213
+ gkw.setdefault("normalize_rows", False)
214
+
215
+ # eps estimation for RBF weights (median kNN distance)
216
+ gkw.setdefault("k_eps", 256)
217
+ gkw.setdefault("eps_use_kth", True)
218
+ gkw.setdefault("eps_mul", 1.0)
219
+
220
+ # Solver controls for (K + lambda I) S = Y
221
+ gkw.setdefault("solve_method", "auto")
222
+ gkw.setdefault("solve_tol", 1e-6)
223
+ gkw.setdefault("solve_maxiter", 800)
224
+ gkw.setdefault("solve_verbose", False)
225
+
226
+ # Inference neighbor truncation
227
+ gkw.setdefault("pred_k", 128)
228
+
229
+ # Latent preconditioning (optional)
230
+ # On ψ it sometimes helps, sometimes hurts. Leave default False unless needed.
231
+ gkw.setdefault("whiten_latent", False)
232
+
233
+ # dtype for ANN storage
234
+ gkw.setdefault("dtype", np.float32)
235
 
236
  self.gplm = GPLM(X_train, Y_train_c, **gkw)
237
 
 
249
  self.gp_rbf_q = float(gp_rbf_q)
250
  self.last_gp_rbf_ell_: Optional[float] = None
251
 
252
+ # Dense GP context cap
253
  self.gp_max_ctx = None if gp_max_ctx is None else int(gp_max_ctx)
254
  self.gp_ctx_mode = str(gp_ctx_mode).lower().strip()
255
  if self.gp_ctx_mode not in ("recent", "uniform"):
 
286
  Psi = np.asarray(Psi_Br, dtype=np.float64)
287
  if Psi.ndim == 1:
288
  Psi = Psi[None, :]
289
+ Yc = self.gplm(Psi) # (B,D) (already centered because center_X=False + mean_X=0)
290
  return np.asarray(Yc, dtype=np.float64)
291
 
292
  def _baseline_centered_one(self, psi_r: np.ndarray) -> np.ndarray:
 
306
  if self.gp_kernel == "linear":
307
  return Psi_ctx @ Psi_ctx.T, None
308
 
 
309
  d2 = _pairwise_sq_dists(Psi_ctx)
310
  if self.gp_rbf_ell is None:
311
  ell_used = _estimate_rbf_ell_from_d2(d2, q=self.gp_rbf_q)
 
352
 
353
  if self.gate_mode == "exp":
354
  return float(np.exp(-v / tau2))
 
355
  return float(tau2 / (tau2 + v))
356
 
357
  # ============================================================
 
386
  """
387
  Autoregressive forecast from prefix (ℓ,D), ℓ >= L.
388
 
389
+ If ℓ == L: baseline AR only (NLSA + sparse GPLM).
390
+ If ℓ > L: uses dense in-context GPR on residuals (optional).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
391
  """
392
  prefix = _as_2d(prefix)
393
  ell, D = prefix.shape
 
424
  return preds[0] if H == 1 else preds
425
 
426
  # ---------------------------
427
+ # Dense GP context cap (safety)
428
  # ---------------------------
429
  if self.gp_max_ctx is None:
430
  K_ctx = int(K_ctx_full)
 
436
  start = int(K_ctx_full - K_ctx)
437
  ctx_idx = None
438
  else:
 
439
  ctx_idx = np.linspace(0, K_ctx_full - 1, num=K_ctx, dtype=np.int64)
440
+ start = 0
441
 
442
  # ---------------------------
443
  # Build context windows + targets
444
  # ---------------------------
445
+ W_all = _sliding_windows(prefix, self.L) # (ell-L+1,L,D)
446
 
447
  if ctx_idx is None:
448
+ W_ctx = np.ascontiguousarray(W_all[start : start + K_ctx, :, :])
449
  Y_ctx = np.asarray(prefix[self.L + start : self.L + start + K_ctx, :], dtype=np.float64)
450
  else:
451
  W_ctx = np.ascontiguousarray(W_all[ctx_idx, :, :])
 
455
  Y_ctx_c = (Y_ctx - self.mu_X[None, :]) if self.center_outputs else Y_ctx
456
 
457
  # encode ψ for context
458
+ Psi_ctx = self._encode_batch(W_ctx) # (K_ctx,r)
459
 
460
  # baseline predictions on context
461
+ Y_glob_ctx_c = self._baseline_centered_batch(Psi_ctx) # (K_ctx,D)
462
 
463
  # residual table
464
+ E_ctx = Y_ctx_c - Y_glob_ctx_c # (K_ctx,D)
465
 
466
  # ---------------------------
467
  # Fit dense GP on residuals
468
  # ---------------------------
469
+ K_mat, ell_used = self._gp_kernel_matrix(Psi_ctx) # (K_ctx,K_ctx)
470
  K_reg = K_mat + self.gp_noise2 * np.eye(K_ctx, dtype=np.float64)
471
 
 
472
  cf = cho_factor(K_reg, lower=True, check_finite=False)
473
+ alpha = cho_solve(cf, E_ctx, check_finite=False) # (K_ctx,D)
474
  Lfac, lower = cf
475
 
 
476
  w_ctx_base = float(K_ctx) / float(K_ctx + self.ctx_k0) if self.ctx_k0 > 0 else 1.0
477
 
478
  preds = np.zeros((H, self.D), dtype=np.float64)
 
482
  # AR rollout with GP residual
483
  # ---------------------------
484
  for h in range(H):
485
+ psi_q = self.base.encode_window(cur)
486
 
487
  # baseline
488
+ y_glob_c = self._baseline_centered_one(psi_q)
489
 
490
  # GP residual mean
491
+ k_eval = self._gp_kernel_eval(Psi_ctx, psi_q, ell_used)
492
+ e_mean = k_eval @ alpha
493
 
494
  # GP residual variance (function variance)
495
  u = solve_triangular(Lfac, k_eval, lower=lower, check_finite=False)
 
499
  if return_var:
500
  vars_out[h] = float(var_f)
501
 
 
502
  w_gate = self._gate_from_var(var_f)
503
  w_eff = w_ctx_base * w_gate
504
 
 
505
  e_use = e_mean
506
  if sample:
507
  var_y = var_f + (self.gp_noise2 if include_obs_noise else 0.0)
 
528
 
529
  def _rollout_baseline(self, seed_LD: np.ndarray, H: int) -> np.ndarray:
530
  """
531
+ Baseline AR rollout only: NLSA encode + sparse GPLM decode.
532
  """
533
  cur = np.asarray(seed_LD, dtype=np.float64).copy()
534
  out = np.zeros((H, self.D), dtype=np.float64)