scm2cpp-lasso 0.13.2__py3-none-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. scm2cpp_lasso/__init__.py +2808 -0
  2. scm2cpp_lasso/_classifiers.cp312-win_amd64.pyd +0 -0
  3. scm2cpp_lasso/_generated/__init__.py +0 -0
  4. scm2cpp_lasso/_generated/_loader.py +41 -0
  5. scm2cpp_lasso/_generated/_loader_accel_cublas.py +140 -0
  6. scm2cpp_lasso/_generated/_loader_classifiers.py +281 -0
  7. scm2cpp_lasso/_generated/_loader_derived_blas.py +29 -0
  8. scm2cpp_lasso/_generated/_loader_derived_cublas.py +29 -0
  9. scm2cpp_lasso/_generated/_loader_derived_loops.py +29 -0
  10. scm2cpp_lasso/_generated/_loader_gpu_cuda.py +294 -0
  11. scm2cpp_lasso/_generated/_loader_gramops_blas.py +496 -0
  12. scm2cpp_lasso/_generated/_loader_gramops_loops.py +496 -0
  13. scm2cpp_lasso/_generated/_loader_nmf_blas.py +29 -0
  14. scm2cpp_lasso/_generated/_loader_nmf_cublas.py +29 -0
  15. scm2cpp_lasso/_generated/_loader_nmf_loops.py +29 -0
  16. scm2cpp_lasso/_generated/accel_cublas/accel.cpp +20 -0
  17. scm2cpp_lasso/_generated/accel_cublas/accel.hpp +1023 -0
  18. scm2cpp_lasso/_generated/accel_cublas/accel_capi.cpp +64 -0
  19. scm2cpp_lasso/_generated/classifiers.cpp +34 -0
  20. scm2cpp_lasso/_generated/classifiers.hpp +2825 -0
  21. scm2cpp_lasso/_generated/classifiers_capi.cpp +106 -0
  22. scm2cpp_lasso/_generated/derived_blas/lasso_design.cpp +5 -0
  23. scm2cpp_lasso/_generated/derived_blas/lasso_design.hpp +153 -0
  24. scm2cpp_lasso/_generated/derived_blas/lasso_design_capi.cpp +19 -0
  25. scm2cpp_lasso/_generated/derived_cublas/lasso_design.cpp +5 -0
  26. scm2cpp_lasso/_generated/derived_cublas/lasso_design.hpp +155 -0
  27. scm2cpp_lasso/_generated/derived_cublas/lasso_design_capi.cpp +19 -0
  28. scm2cpp_lasso/_generated/derived_loops/lasso_design.cpp +5 -0
  29. scm2cpp_lasso/_generated/derived_loops/lasso_design.hpp +176 -0
  30. scm2cpp_lasso/_generated/derived_loops/lasso_design_capi.cpp +19 -0
  31. scm2cpp_lasso/_generated/gpu_cuda/gpu.cpp +35 -0
  32. scm2cpp_lasso/_generated/gpu_cuda/gpu.hpp +3715 -0
  33. scm2cpp_lasso/_generated/gpu_cuda/gpu_capi.cpp +289 -0
  34. scm2cpp_lasso/_generated/gramops_blas/gramops.cpp +57 -0
  35. scm2cpp_lasso/_generated/gramops_blas/gramops.hpp +3033 -0
  36. scm2cpp_lasso/_generated/gramops_blas/gramops_capi.cpp +175 -0
  37. scm2cpp_lasso/_generated/gramops_loops/gramops.cpp +57 -0
  38. scm2cpp_lasso/_generated/gramops_loops/gramops.hpp +3302 -0
  39. scm2cpp_lasso/_generated/gramops_loops/gramops_capi.cpp +175 -0
  40. scm2cpp_lasso/_generated/lasso_cov.cpp +7 -0
  41. scm2cpp_lasso/_generated/lasso_cov.hpp +152 -0
  42. scm2cpp_lasso/_generated/lasso_cov_capi.cpp +25 -0
  43. scm2cpp_lasso/_generated/nmf_blas/nmf_cd.cpp +5 -0
  44. scm2cpp_lasso/_generated/nmf_blas/nmf_cd.hpp +132 -0
  45. scm2cpp_lasso/_generated/nmf_blas/nmf_cd_capi.cpp +19 -0
  46. scm2cpp_lasso/_generated/nmf_cublas/nmf_cd.cpp +5 -0
  47. scm2cpp_lasso/_generated/nmf_cublas/nmf_cd.hpp +136 -0
  48. scm2cpp_lasso/_generated/nmf_cublas/nmf_cd_capi.cpp +19 -0
  49. scm2cpp_lasso/_generated/nmf_loops/nmf_cd.cpp +5 -0
  50. scm2cpp_lasso/_generated/nmf_loops/nmf_cd.hpp +167 -0
  51. scm2cpp_lasso/_generated/nmf_loops/nmf_cd_capi.cpp +19 -0
  52. scm2cpp_lasso/_generated/scm2cpp-blas.hpp +77 -0
  53. scm2cpp_lasso/_generated/scm2cpp-cublas.hpp +155 -0
  54. scm2cpp_lasso/_generated/scm2cpp-cuda.hpp +285 -0
  55. scm2cpp_lasso/_generated/scm2cpp.hpp +1627 -0
  56. scm2cpp_lasso/_gramops_loops.cp312-win_amd64.pyd +0 -0
  57. scm2cpp_lasso/_lasso_cov.cp312-win_amd64.pyd +0 -0
  58. scm2cpp_lasso/_lasso_derived_loops.cp312-win_amd64.pyd +0 -0
  59. scm2cpp_lasso/_libfind.py +149 -0
  60. scm2cpp_lasso/_nmf_loops.cp312-win_amd64.pyd +0 -0
  61. scm2cpp_lasso/batch_capi.cu +428 -0
  62. scm2cpp_lasso-0.13.2.dist-info/METADATA +920 -0
  63. scm2cpp_lasso-0.13.2.dist-info/RECORD +65 -0
  64. scm2cpp_lasso-0.13.2.dist-info/WHEEL +5 -0
  65. scm2cpp_lasso-0.13.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,2808 @@
1
+ """Lasso by coordinate descent over a Gram matrix, from Scheme via C++.
2
+
3
+ The solver is `examples/kernel-only/lasso-cov.scm` in the scm2cpp
4
+ repository, translated to C++ and compiled at install time. It takes
5
+ the covariance route: given X'X and X'y, each coordinate step costs
6
+ O(p) instead of O(n), so a whole regularization path is cheap once the
7
+ Gram matrix exists. The descent is exactly resumable, which is what
8
+ lets a path be walked warm and a batch of lambdas be handed to a GPU.
9
+
10
+ from scm2cpp_lasso import CovLasso
11
+
12
+ model = CovLasso(X, y) # forms the Gram matrix
13
+ path = model.fit_path(lambdas) # warm, sequential
14
+ grid = model.fit_path_batch(lambdas) # cold, one thread each
15
+
16
+ `fit_path_batch` uses CUDA when the package was built with nvcc present
17
+ and a device is visible, and falls back to the CPU otherwise; ask
18
+ `cuda_available()` if you need to know which you got.
19
+
20
+ Designs with structure need not form X'X the general way. `kernel`
21
+ exposes the translated functions directly, and `scm2cpp-tfs` uses them
22
+ to build the Gram matrix of a moving-average design in O(n p) time and
23
+ no design matrix at all.
24
+ """
25
+ import ctypes
26
+
27
+ import numpy as np
28
+
29
+ from ._generated import _loader as kernel
30
+ from ._libfind import load_batch_lib, derived_backends, nmf_backends
31
+
32
+ # the derived kernel's variants that were built, each bound to its own
33
+ # library: {'cublas': module, 'blas': module, 'loops': module}
34
+ _DERIVED = {}
35
+ for _v in derived_backends():
36
+ try:
37
+ _DERIVED[_v] = __import__("scm2cpp_lasso._generated._loader_derived_" + _v,
38
+ fromlist=["lasso"])
39
+ except ImportError:
40
+ pass
41
+
42
+ __all__ = ["CovLasso", "CovRidge", "CovBayesianRidge", "CovLogistic", "CovGroupLasso",
43
+ "LinearSVM", "KernelSVM", "LinearSVR", "KernelSVR", "L1LogisticRegression",
44
+ "CDNMF", "nmf_backend", "CDGraphicalLasso", "LinearRANSAC",
45
+ "SGDLinearClassifier", "SGDLinearRegressor", "SGDPerceptron", "GramOMP", "GramLars", "GramLassoLars", "GramLassoLarsIC",
46
+ "LBFGSLogisticRegression", "LBFGSHuberRegressor", "LBFGSLogisticRegressionCV",
47
+ "LBFGSPoissonRegressor", "LBFGSGammaRegressor", "LBFGSTweedieRegressor",
48
+ "GramLarsCV", "GramLassoLarsCV", "GramOMPCV",
49
+ "EMGaussianMixture", "CovPCA", "ParallelFastICA", "CovARDRegression",
50
+ "L1LinearSVM", "WeiszfeldTheilSen", "f_regression", "f_classif", "SelectKBest",
51
+ "SelectPercentile", "GramRFE",
52
+ "CovMultiTaskLasso", "CovMultiTaskLassoCV", "DerivedLasso",
53
+ "cuda_available", "derived_backend", "kernel"]
54
+ __version__ = "0.13.2"
55
+
56
+ _BATCH = load_batch_lib()
57
+ _DP = ctypes.POINTER(ctypes.c_double)
58
+
59
+
60
+ def _scale(beta):
61
+ """What `tol` is measured against.
62
+
63
+ An absolute test on the coefficients is a trap when the features
64
+ are large: the Gram diagonal is then large too, every sweep moves
65
+ each coefficient by very little, and the descent declares victory
66
+ a long way from the solution. Measuring the move against the size
67
+ of the coefficients themselves makes the test mean the same thing
68
+ at any scale, and falls back to absolute when they are still near
69
+ zero.
70
+ """
71
+ m = float(np.max(np.abs(beta))) if beta.size else 0.0
72
+ return m if m > 1.0 else 1.0
73
+
74
+
75
+ def cuda_available():
76
+ """True when the GPU batch path was built and a device answers."""
77
+ if _BATCH is None:
78
+ return False
79
+ g, c, b = np.ones(1), np.zeros(1), np.zeros(1)
80
+ lam = np.array([1e9]) # a lambda that zeroes everything
81
+ return 0 == _BATCH.scm2cpp_batch_descend(
82
+ g.ctypes.data_as(_DP), 1, c.ctypes.data_as(_DP),
83
+ b.ctypes.data_as(_DP), lam.ctypes.data_as(_DP), 1.0, 1, 1, 1.0,
84
+ 20, 20, 1e-8)
85
+
86
+
87
+ class CovLasso:
88
+ """Lasso over a design given by its Gram matrix.
89
+
90
+ Either hand it the design and the target, in which case X'X and
91
+ X'y are formed here, or hand it a Gram matrix and correlations
92
+ somebody else built more cheaply.
93
+
94
+ Parameters
95
+ ----------
96
+ X : array (nobs, p), optional
97
+ The design matrix.
98
+ y : array (nobs,), optional
99
+ The target.
100
+ gram : array (p, p) or (p*p,), optional
101
+ X'X, if it is already known.
102
+ corr : array (p,), optional
103
+ X'y, likewise.
104
+ nobs : int, optional
105
+ Rows behind `gram`; required when passing one, since the
106
+ penalty is scaled by it as scikit-learn's alpha is.
107
+
108
+ The objective is the usual one, matching scikit-learn's `Lasso`
109
+ with `fit_intercept=False`:
110
+
111
+ (1 / 2 nobs) ||y - X b||^2 + lam ||b||_1
112
+ """
113
+
114
+ def __init__(self, X=None, y=None, *, gram=None, corr=None, nobs=None):
115
+ if gram is not None:
116
+ if corr is None or nobs is None:
117
+ raise ValueError("gram needs corr and nobs alongside it")
118
+ g = np.ascontiguousarray(gram, dtype=np.float64).ravel()
119
+ c = np.ascontiguousarray(corr, dtype=np.float64)
120
+ p = c.size
121
+ if g.size != p * p:
122
+ raise ValueError(f"gram is {g.size} entries; expected {p*p}")
123
+ self.p, self.nobs, self.g, self.c0 = p, int(nobs), g, c
124
+ else:
125
+ if X is None or y is None:
126
+ raise ValueError("pass X and y, or gram, corr and nobs")
127
+ X = np.ascontiguousarray(X, dtype=np.float64)
128
+ y = np.ascontiguousarray(y, dtype=np.float64)
129
+ if X.ndim != 2 or y.ndim != 1 or X.shape[0] != y.size:
130
+ raise ValueError(
131
+ f"X is {X.shape} and y is {y.shape}; expected (n, p) "
132
+ "and (n,)")
133
+ self.nobs, self.p = X.shape
134
+ self.g = np.ascontiguousarray((X.T @ X).ravel())
135
+ self.c0 = np.ascontiguousarray(X.T @ y)
136
+ self.X, self.y = X, y
137
+
138
+
139
+ # ------------------------- choosing lambdas -------------------------
140
+
141
+ def lambda_max(self, l1_ratio=1.0):
142
+ """The smallest penalty that leaves every coefficient at zero.
143
+
144
+ Above it the solution is exactly zero, so a path that starts
145
+ here starts where the first coefficient is about to enter --
146
+ which is the only scale-free place to start, since the penalty
147
+ is compared against correlations on the features' own scale.
148
+ """
149
+
150
+ return float(np.max(np.abs(self.c0))) / (self.nobs * l1_ratio)
151
+
152
+ def lambda_grid(self, num=100, eps=1e-3, l1_ratio=1.0):
153
+ """A log-spaced path from lambda_max down to eps * lambda_max.
154
+
155
+ The same construction scikit-learn uses, and the same default
156
+ ratio; give the result to fit_path, which expects descending
157
+ lambdas.
158
+ """
159
+ hi = self.lambda_max(l1_ratio)
160
+ return hi * np.logspace(0, np.log10(eps), int(num))
161
+
162
+ # ---------------------------- fitting ----------------------------
163
+
164
+ def fit(self, lam, **kw):
165
+ """Coefficients at one lambda.
166
+
167
+ A single penalty is the one-element special case of the path:
168
+ the Gram matrix was built in the constructor, so this costs one
169
+ descent and nothing per row -- and a warm-started path over a
170
+ whole grid costs barely more, which is why fit_path is the
171
+ primary interface rather than this convenience.
172
+ """
173
+ return self.fit_path([lam], **kw)[0]
174
+
175
+ def fit_path(self, lambdas, tol=1e-8, chunk=20, max_sweeps=100000,
176
+ l1_ratio=1.0):
177
+ """Coefficients per lambda, each warm-started from the last.
178
+
179
+ Returns an array of shape (len(lambdas), p). Give lambdas in
180
+ descending order: that is what makes the warm start pay.
181
+ """
182
+ lambdas = np.asarray(lambdas, dtype=np.float64)
183
+ beta, c = np.zeros(self.p), self.c0.copy()
184
+ prev = np.empty(self.p)
185
+ out = np.empty((lambdas.size, self.p))
186
+ for i, lam in enumerate(lambdas):
187
+ swept = 0
188
+ while swept < max_sweeps:
189
+ prev[:] = beta
190
+ kernel.enet_descend(self.g, c, beta,
191
+ float(lam) * l1_ratio,
192
+ float(lam) * (1.0 - l1_ratio), chunk,
193
+ float(self.nobs), self.p)
194
+ swept += chunk
195
+ if np.max(np.abs(beta - prev)) < tol * _scale(beta):
196
+ break
197
+ out[i] = beta
198
+ return out
199
+
200
+
201
+ def bootstrap(self, lam, n_boot=200, l1_ratio=1.0, seed=None,
202
+ force_cpu=False, **kw):
203
+ """Pairs-bootstrap coefficients at one lambda, (n_boot, p).
204
+
205
+ Each resample draws rows with replacement; its Gram matrix is
206
+ X' diag(m) X with the multiplicity counts m, formed as Xs' Xs
207
+ for Xs = diag(sqrt m) X so that numpy takes the symmetric
208
+ rank-k product (dsyrk, half the flops of a general product)
209
+ -- one BLAS call per resample -- and the descents run as one
210
+ batch: on the GPU one block of threads per resample over its
211
+ own Gram, since the problems are independent. Needs the
212
+ design matrix, so it is unavailable when the model was built
213
+ from a Gram matrix alone.
214
+ """
215
+ if not hasattr(self, "X"):
216
+ raise ValueError("bootstrap needs X; construct from X and y")
217
+ rng = np.random.default_rng(seed)
218
+ B = int(n_boot)
219
+ grams = np.empty((B, self.p * self.p))
220
+ corrs = np.empty((B, self.p))
221
+ for b in range(B):
222
+ m = rng.multinomial(self.nobs,
223
+ np.full(self.nobs, 1.0 / self.nobs))
224
+ Xs = self.X * np.sqrt(m)[:, None]
225
+ grams[b] = (Xs.T @ Xs).ravel()
226
+ corrs[b] = self.X.T @ (m * self.y)
227
+ lams = (np.ascontiguousarray(lam, dtype=np.float64)
228
+ if np.ndim(lam) else np.full(B, float(lam)))
229
+ if not force_cpu:
230
+ beta = _grid_descend(grams, corrs, lams, self.p, 0,
231
+ l1_ratio=l1_ratio, nobs=float(self.nobs),
232
+ **kw)
233
+ if beta is not None:
234
+ return beta
235
+ return _batch_descend_multi(grams, corrs, lams, self.nobs, self.p,
236
+ l1_ratio=l1_ratio, force_cpu=True,
237
+ kernel_fn=kernel.enet_descend, **kw)
238
+
239
+ def fit_path_batch(self, lambdas, tol=1e-8, chunk=20,
240
+ max_sweeps=100000, force_cpu=False, l1_ratio=1.0):
241
+ """Coefficients per lambda, every lambda solved from zero.
242
+
243
+ Independent problems, so they go to the GPU together when one
244
+ is available: one launch, one block of threads per lambda, all
245
+ sharing this Gram matrix. This is the shape of a
246
+ cross-validation grid, where warm starting across lambdas is
247
+ not on offer anyway.
248
+ """
249
+ lambdas = np.ascontiguousarray(lambdas, dtype=np.float64)
250
+ batch = lambdas.size
251
+ if not force_cpu:
252
+ beta = _grid_descend(self.g[None, :], self.c0[None, :], lambdas,
253
+ self.p, 0, tol=tol, chunk=chunk,
254
+ max_sweeps=max_sweeps, l1_ratio=l1_ratio,
255
+ nobs=float(self.nobs))
256
+ if beta is not None:
257
+ return beta
258
+ # a device that refused the work is not a reason to fail
259
+ beta = np.zeros((batch, self.p))
260
+ c = np.tile(self.c0, (batch, 1))
261
+ prev = np.empty(self.p)
262
+ for t in range(batch):
263
+ bt, ct, swept = beta[t], c[t], 0
264
+ while swept < max_sweeps:
265
+ prev[:] = bt
266
+ kernel.enet_descend(self.g, ct, bt,
267
+ float(lambdas[t]) * l1_ratio,
268
+ float(lambdas[t]) * (1.0 - l1_ratio), chunk,
269
+ float(self.nobs), self.p)
270
+ swept += chunk
271
+ if np.max(np.abs(bt - prev)) < tol * _scale(bt):
272
+ break
273
+ return beta
274
+
275
+
276
+ class DerivedLasso:
277
+ """Lasso from the design matrix, by the kernel the translator derived.
278
+
279
+ CovLasso above is the usable one: numpy forms the Gram matrix, a
280
+ hand-written sweep (lasso-cov.scm) runs over it, and cross-
281
+ validation, bootstrap and the GPU grid all feed that sweep their
282
+ own Gram matrices. This class exists to show the translator's
283
+ work instead. Its kernel is examples/kernel-only/lasso-design.scm
284
+ -- the residual-form coordinate descent, the algorithm inside
285
+ scikit-learn's Lasso(precompute=False), written as flat loops over
286
+ X as numpy holds it (n rows of p), the residual and the column
287
+ norms taken inside, no Gram matrix in sight -- translated with
288
+ --derive and --blas:
289
+ the translator raised the loops to the array algebra, replaced the
290
+ residual by the memo c = X'r kept current through a Gram matrix it
291
+ hoisted in front of the penalty path, folded the column norms into
292
+ that Gram's diagonal, folded the products into one dsyrk and two
293
+ dgemv calls, and put the result under (if (< p n) ..)
294
+ with the original sweep in the else branch -- scikit-learn's
295
+ precompute='auto' rule, derived. Nothing in it is written at the Gram level by hand, and numpy
296
+ prepares nothing: X and y go in as they are.
297
+
298
+ Three variants of the same derivation can be built: 'cublas'
299
+ (--derive --cublas: the products on the device, X uploaded once per
300
+ fit_path call), 'blas' (--derive --blas, CBLAS) and 'loops'
301
+ (--derive alone: the Gram a scalar loop nest, much slower on large
302
+ designs). backend=None takes the best one this machine built and
303
+ can run -- cublas only when a device answers -- and
304
+ derived_backend() says which that is; backend='blas' etc. insists.
305
+
306
+ The constructor only checks shapes and keeps X (C-contiguous, as
307
+ numpy makes it) and y; everything else happens inside fit_path.
308
+
309
+ Convergence is the kernel's: a sweep in which no coefficient moved
310
+ by more than tol (absolute) ends the fit at that lambda, max_sweeps
311
+ bounds it; tol=0.0 asks for the exact fixed point. Same objective
312
+ as CovLasso.
313
+ """
314
+
315
+ def __init__(self, X, y, backend=None):
316
+ if backend is None:
317
+ backend = derived_backend()
318
+ if backend is None:
319
+ raise ImportError(
320
+ "scm2cpp_lasso: the derived kernel was not built; "
321
+ "reinstall the package (pip install --force-reinstall "
322
+ "scm2cpp-lasso)")
323
+ elif backend not in _DERIVED:
324
+ raise ValueError(f"derived kernel backend {backend!r} is not built; "
325
+ f"built: {sorted(_DERIVED)}")
326
+ self.backend = backend
327
+ self._kernel = _DERIVED[backend]
328
+ X = np.ascontiguousarray(X, dtype=np.float64)
329
+ y = np.ascontiguousarray(y, dtype=np.float64)
330
+ if X.ndim != 2 or y.ndim != 1 or X.shape[0] != y.size:
331
+ raise ValueError(
332
+ f"X is {X.shape} and y is {y.shape}; expected (n, p) and (n,)")
333
+ self.nobs, self.p = X.shape
334
+ self.X, self.y = X, y
335
+ self._c0 = None
336
+
337
+ @property
338
+ def c0(self):
339
+ # X'y, needed only to place the penalty grid
340
+ if self._c0 is None:
341
+ self._c0 = self.X.T @ self.y
342
+ return self._c0
343
+
344
+ def lambda_max(self):
345
+ return float(np.max(np.abs(self.c0))) / self.nobs
346
+
347
+ def lambda_grid(self, num=100, eps=1e-3):
348
+ hi = self.lambda_max()
349
+ return hi * np.logspace(0, np.log10(eps), int(num))
350
+
351
+ def fit(self, lam, **kw):
352
+ return self.fit_path([lam], **kw)[0]
353
+
354
+ def fit_path(self, lambdas, tol=1e-8, max_sweeps=100000):
355
+ """Coefficients per lambda, warm-started, shape (len(lambdas), p).
356
+
357
+ One call into the derived kernel: it takes the residual and
358
+ the column norms (folded into the Gram's diagonal when p < n),
359
+ builds the Gram matrix, walks the penalties, and leaves the
360
+ coefficient at each in a row of the result. tol is the largest move of
361
+ one coefficient in a sweep below which that lambda is done.
362
+ """
363
+ lams = np.ascontiguousarray(lambdas, dtype=np.float64).ravel()
364
+ beta = np.zeros(self.p)
365
+ resid = np.empty(self.nobs) # filled by the kernel from y
366
+ xnorm = np.empty(self.p) # the column norms, taken inside
367
+ betas = np.zeros(lams.size * self.p)
368
+ self._kernel.lasso_design(self.X, self.y, resid, xnorm, beta, lams,
369
+ betas, int(max_sweeps), float(tol),
370
+ self.nobs, self.p, lams.size)
371
+ return betas.reshape(lams.size, self.p)
372
+
373
+
374
+ def derived_backend():
375
+ """The derived kernel variant DerivedLasso takes by default: 'cublas'
376
+ when built and a device answers, else 'blas', else 'loops'; None
377
+ when none was built."""
378
+ for v in ("cublas", "blas", "loops"):
379
+ if v in _DERIVED and (v != "cublas" or cuda_available()):
380
+ return v
381
+ return None
382
+
383
+
384
+ def _batch_descend_multi(grams, corrs, lam, nobs, p, tol=1e-8, chunk=20,
385
+ max_sweeps=100000, l1_ratio=1.0, force_cpu=False,
386
+ kernel_fn=None):
387
+ """Descend a batch where every problem has its own Gram matrix.
388
+
389
+ grams is (B, p*p) and corrs (B, p); one thread per problem on the
390
+ GPU when it is there (the kernel before 0.7.0, kept for the
391
+ comparison in bench/cv-grid-designs.py), the same chunked-tolerance
392
+ loop on the CPU when it is not -- which is what the bootstrap
393
+ falls back to.
394
+ """
395
+ B = corrs.shape[0]
396
+ import ctypes
397
+ beta = np.zeros((B, p))
398
+ c = np.ascontiguousarray(corrs.copy())
399
+ g = np.ascontiguousarray(grams)
400
+ # lam may be one penalty for every thread or one per thread; the
401
+ # kernel always reads a per-thread array
402
+ lams = (np.ascontiguousarray(lam, dtype=np.float64)
403
+ if np.ndim(lam) else np.full(B, float(lam)))
404
+ if _BATCH is not None and not force_cpu:
405
+ dp = ctypes.POINTER(ctypes.c_double)
406
+ rc = _BATCH.scm2cpp_batch_descend(
407
+ g.ctypes.data_as(dp), int(B), c.ctypes.data_as(dp),
408
+ beta.ctypes.data_as(dp), lams.ctypes.data_as(dp),
409
+ float(l1_ratio), int(B), int(p), float(nobs),
410
+ int(max_sweeps), int(chunk), float(tol))
411
+ if rc == 0:
412
+ return beta
413
+ prev = np.empty(p)
414
+ for b in range(B):
415
+ gb, cb, bb = g[b], c[b], beta[b]
416
+ lam_b = float(lams[b])
417
+ swept = 0
418
+ while swept < max_sweeps:
419
+ prev[:] = bb
420
+ kernel_fn(gb, cb, bb, lam_b * l1_ratio,
421
+ lam_b * (1.0 - l1_ratio), chunk,
422
+ float(nobs), p)
423
+ swept += chunk
424
+ if np.max(np.abs(bb - prev)) < tol * max(
425
+ 1.0, float(np.max(np.abs(bb)))):
426
+ break
427
+ return beta
428
+
429
+
430
+ def _grid_descend(grams, corrs, lams, p, ntask, tol=1e-8, chunk=20,
431
+ max_sweeps=100000, l1_ratio=1.0, nobs=1.0):
432
+ """A batch of cold descents on the GPU, one block per problem.
433
+
434
+ grams is (n_grams, p*p), corrs (n_grams, width) with width p or
435
+ p*ntask, and lams the penalties of the batch in Gram-major order:
436
+ problem i uses Gram i // (batch // n_grams). Every problem starts
437
+ from zero and nothing is replicated: a cross-validation grid of cv
438
+ folds by num penalties moves cv Gram matrices, a fit_path_batch
439
+ one, a bootstrap one per resample. Returns (batch, width), or
440
+ None when the GPU path is not there or declined the shape, in
441
+ which case the caller runs its CPU path.
442
+ """
443
+ fn = getattr(_BATCH, "scm2cpp_cv_descend", None) if _BATCH else None
444
+ if fn is None:
445
+ return None
446
+ n_grams, batch = corrs.shape[0], lams.size
447
+ if batch % n_grams:
448
+ return None
449
+ width = p * (ntask if ntask > 0 else 1)
450
+ g = np.ascontiguousarray(grams, dtype=np.float64)
451
+ c = np.ascontiguousarray(np.repeat(corrs, batch // n_grams, axis=0),
452
+ dtype=np.float64)
453
+ w = np.zeros((batch, width))
454
+ lams = np.ascontiguousarray(lams, dtype=np.float64)
455
+ rc = fn(g.ctypes.data_as(_DP), int(n_grams), c.ctypes.data_as(_DP),
456
+ w.ctypes.data_as(_DP), lams.ctypes.data_as(_DP),
457
+ float(l1_ratio), int(batch), int(p), int(ntask), float(nobs),
458
+ int(max_sweeps), int(chunk), float(tol), 1, 1)
459
+ return w if rc == 0 else None
460
+
461
+
462
+ class CovLassoCV:
463
+ """Cross-validated lasso, scikit-learn's LassoCV semantics.
464
+
465
+ The lambda grid comes from the full data (lambda_max down, the
466
+ sklearn construction), the folds are contiguous splits in order
467
+ (sklearn's KFold default), and validation error is one matrix
468
+ product per fold. alpha_ minimises the mean validation MSE, ties
469
+ going to the larger penalty as in scikit-learn, and coef_ is the
470
+ refit on all rows at alpha_.
471
+
472
+ With a GPU the whole grid -- cv folds by num penalties, each from
473
+ zero -- is one launch with one block of threads per problem, the
474
+ fold's Gram matrix shared by its problems; on the CPU each fold's
475
+ path is walked warm. force_cpu picks the CPU path; force_gpu is
476
+ accepted for compatibility (the GPU is now taken whenever it is
477
+ there and force_cpu is not set).
478
+
479
+ Attributes after fit: alphas_, mse_path_ (num, cv), alpha_, coef_.
480
+ """
481
+
482
+ def __init__(self, cv=5, num=100, eps=1e-3, tol=1e-8, l1_ratio=1.0,
483
+ force_cpu=False, force_gpu=False, n_jobs=1):
484
+ self.cv = int(cv)
485
+ self.num = int(num)
486
+ self.eps = float(eps)
487
+ self.tol = float(tol)
488
+ self.l1_ratio = float(l1_ratio)
489
+ self.force_cpu = force_cpu
490
+ # Folds are independent, the descent is a ctypes call and the
491
+ # products are BLAS calls, both of which release the GIL, so
492
+ # plain threads scale the CPU path across folds. Default 1:
493
+ # sequential, like scikit-learn's n_jobs=None.
494
+ self.n_jobs = int(n_jobs)
495
+ self.force_gpu = force_gpu
496
+
497
+ def fit(self, X, y):
498
+ X = np.ascontiguousarray(X, dtype=np.float64)
499
+ y = np.ascontiguousarray(y, dtype=np.float64)
500
+ n = len(y)
501
+ full = CovLasso(X, y)
502
+ self.alphas_ = full.lambda_grid(num=self.num, eps=self.eps,
503
+ l1_ratio=self.l1_ratio)
504
+ # contiguous folds in row order, sizes as even as they come --
505
+ # sklearn's KFold(shuffle=False)
506
+ bounds = np.linspace(0, n, self.cv + 1).astype(int)
507
+ self.mse_path_ = np.empty((self.num, self.cv))
508
+ G = full.g.reshape(full.p, full.p)
509
+ if cuda_available() and not self.force_cpu:
510
+ done = self._fit_gpu(X, y, full, G, bounds, n)
511
+ if done is not None:
512
+ return done
513
+
514
+ def one_fold(k):
515
+ lo, hi = bounds[k], bounds[k + 1]
516
+ Xf, yf = X[lo:hi], y[lo:hi]
517
+ # The Gram is additive over rows, so a fold's training Gram
518
+ # is a subtraction: G_full - Xf'Xf costs O(n_fold p^2) where
519
+ # rebuilding from the complement costs O((n - n_fold) p^2).
520
+ # Over five folds that is the whole Gram work twice instead
521
+ # of four times, and the Gram is the dominant O(n p^2) step.
522
+ m = CovLasso(gram=(G - Xf.T @ Xf).ravel(),
523
+ corr=full.c0 - Xf.T @ yf,
524
+ nobs=n - (hi - lo))
525
+ # The warm-started path: after the Gram, each lambda resumes
526
+ # where the previous stopped and costs O(p) per moving
527
+ # coordinate. That warm path is what makes searching the
528
+ # alpha grid cheap -- the grid costs barely more than its
529
+ # hardest single alpha.
530
+ path = m.fit_path(self.alphas_, tol=self.tol,
531
+ l1_ratio=self.l1_ratio)
532
+ resid = Xf @ path.T - yf[:, None]
533
+ self.mse_path_[:, k] = np.mean(resid * resid, axis=0)
534
+
535
+ if self.n_jobs > 1:
536
+ from concurrent.futures import ThreadPoolExecutor
537
+ with ThreadPoolExecutor(max_workers=self.n_jobs) as ex:
538
+ list(ex.map(one_fold, range(self.cv)))
539
+ else:
540
+ for k in range(self.cv):
541
+ one_fold(k)
542
+ return self._finish(full)
543
+
544
+ def _fit_gpu(self, X, y, full, G, bounds, n):
545
+ # Every fold and every alpha is an independent problem, so all of
546
+ # them go to the device as one batch, one block per problem,
547
+ # the problems of a fold sharing its Gram on the device. Fold
548
+ # sizes may differ by a row; the kernel only ever uses lam
549
+ # through the product lam * nobs, so each problem's nobs is
550
+ # absorbed into its lambda and nobs is passed as 1 -- exact,
551
+ # not approximate. Returns None when the device declined, and
552
+ # the CPU path takes over.
553
+ p_ = full.p
554
+ num, cv = self.num, self.cv
555
+ grams = np.empty((cv, p_ * p_))
556
+ corrs = np.empty((cv, p_))
557
+ nobs_k = np.empty(cv)
558
+ for k in range(cv):
559
+ lo, hi = bounds[k], bounds[k + 1]
560
+ Xf, yf = X[lo:hi], y[lo:hi]
561
+ grams[k] = (G - Xf.T @ Xf).ravel()
562
+ corrs[k] = full.c0 - Xf.T @ yf
563
+ nobs_k[k] = n - (hi - lo)
564
+ lams = (np.tile(self.alphas_, cv) * np.repeat(nobs_k, num))
565
+ betas = _grid_descend(grams, corrs, lams, p_, 0, tol=self.tol,
566
+ l1_ratio=self.l1_ratio)
567
+ if betas is None:
568
+ return None
569
+ betas = betas.reshape(cv, num, p_)
570
+ for k in range(cv):
571
+ lo, hi = bounds[k], bounds[k + 1]
572
+ resid = X[lo:hi] @ betas[k].T - y[lo:hi, None]
573
+ self.mse_path_[:, k] = np.mean(resid * resid, axis=0)
574
+ return self._finish(full)
575
+
576
+ def _finish(self, full):
577
+ mean = self.mse_path_.mean(axis=1)
578
+ # alphas_ descends, so argmin's first hit is the larger penalty
579
+ self.alpha_ = float(self.alphas_[int(np.argmin(mean))])
580
+ self.coef_ = full.fit(self.alpha_, tol=self.tol,
581
+ l1_ratio=self.l1_ratio)
582
+ return self
583
+
584
+ def predict(self, X):
585
+ return np.ascontiguousarray(X, dtype=np.float64) @ self.coef_
586
+
587
+
588
+ class CovRidge:
589
+ """Ridge regression over a design given by its Gram matrix.
590
+
591
+ The objective matches scikit-learn's ``Ridge`` with
592
+ ``fit_intercept=False``:
593
+
594
+ ||y - X b||^2 + alpha ||b||^2
595
+
596
+ (note: unlike the lasso objective, scikit-learn does not scale this
597
+ one by the number of rows), so the solution is
598
+ ``b = (X'X + alpha I)^{-1} X'y``.
599
+
600
+ The design matrix enters only through X'X and X'y, so the
601
+ constructor takes either the design or a Gram matrix somebody built
602
+ more cheaply. One symmetric eigendecomposition is done on first
603
+ use, O(p^3); every alpha after that costs O(p^2), which is what
604
+ makes a whole path cheap.
605
+ """
606
+
607
+ def __init__(self, X=None, y=None, *, gram=None, corr=None):
608
+ if gram is not None:
609
+ if corr is None:
610
+ raise ValueError("gram needs corr alongside it")
611
+ g = np.ascontiguousarray(gram, dtype=np.float64).ravel()
612
+ c = np.ascontiguousarray(corr, dtype=np.float64)
613
+ p = c.size
614
+ if g.size != p * p:
615
+ raise ValueError(f"gram is {g.size} entries; expected {p*p}")
616
+ self.p, self.g, self.c0 = p, g, c
617
+ else:
618
+ if X is None or y is None:
619
+ raise ValueError("pass X and y, or gram and corr")
620
+ X = np.ascontiguousarray(X, dtype=np.float64)
621
+ y = np.ascontiguousarray(y, dtype=np.float64)
622
+ if X.ndim != 2 or y.ndim != 1 or X.shape[0] != y.size:
623
+ raise ValueError(
624
+ f"X is {X.shape} and y is {y.shape}; expected (n, p) "
625
+ "and (n,)")
626
+ self.p = X.shape[1]
627
+ self.g = np.ascontiguousarray((X.T @ X).ravel())
628
+ self.c0 = np.ascontiguousarray(X.T @ y)
629
+ self._eig = None
630
+
631
+ def _eigen(self):
632
+ if self._eig is None:
633
+ w, Q = np.linalg.eigh(self.g.reshape(self.p, self.p))
634
+ self._eig = (w, Q, Q.T @ self.c0)
635
+ return self._eig
636
+
637
+ def alpha_grid(self, num=100, eps=1e-6):
638
+ """A log-spaced path from the top eigenvalue of X'X downward.
639
+
640
+ At ``alpha`` around the largest eigenvalue the fit is heavily
641
+ shrunk; by ``eps`` times it the fit is essentially least
642
+ squares. Descending, like the lasso grids.
643
+ """
644
+ w, _, _ = self._eigen()
645
+ hi = float(w.max())
646
+ return hi * np.logspace(0, np.log10(eps), int(num))
647
+
648
+ def fit(self, alpha):
649
+ """Coefficients at one alpha."""
650
+ w, Q, qc = self._eigen()
651
+ return Q @ (qc / (w + float(alpha)))
652
+
653
+ def fit_path(self, alphas):
654
+ """Coefficients per alpha, shape (len(alphas), p).
655
+
656
+ After the one eigendecomposition this is a single dense
657
+ product, so thousands of alphas cost what one does.
658
+ """
659
+ w, Q, qc = self._eigen()
660
+ alphas = np.asarray(alphas, dtype=np.float64)
661
+ return (Q @ (qc[:, None] / (w[:, None] + alphas[None, :]))).T
662
+
663
+
664
+ class CovBayesianRidge:
665
+ """Bayesian ridge regression, scikit-learn's ``BayesianRidge``,
666
+ with the linear algebra in the eigenbasis of the Gram matrix.
667
+
668
+ The model, the defaults (``max_iter=300``, ``tol=1e-3``, the four
669
+ gamma hyperpriors at 1e-6, ``alpha`` starting at ``1/var(y)``,
670
+ ``lambda`` at 1) and the iteration are scikit-learn's: MacKay's
671
+ updates of the noise precision ``alpha`` and the weight precision
672
+ ``lambda``, the posterior mean recomputed after each, a stop when
673
+ the mean moves by less than ``tol`` in L1. What differs is where
674
+ the work goes. scikit-learn takes the SVD of X once and then, per
675
+ iteration, multiplies by X again for the squared error, O(n p).
676
+ Here, for n > p, X enters once, through X'X and X'y: one symmetric
677
+ eigendecomposition, after which the posterior mean, its squared
678
+ norm and the squared error are sums over the p eigenvalues, O(p) per
679
+ iteration. For n <= p the same is done with the n x n kernel
680
+ matrix X X'. With ``fit_intercept`` the data are centred, as in
681
+ scikit-learn, and the Gram is centred in place.
682
+
683
+ Attributes after fit: coef_, intercept_, alpha_, lambda_, sigma_
684
+ (the posterior covariance of the weights, p x p), n_iter_.
685
+ """
686
+
687
+ def __init__(self, *, max_iter=300, tol=1e-3, alpha_1=1e-6,
688
+ alpha_2=1e-6, lambda_1=1e-6, lambda_2=1e-6,
689
+ alpha_init=None, lambda_init=None, fit_intercept=True):
690
+ self.max_iter = int(max_iter)
691
+ self.tol = float(tol)
692
+ self.alpha_1, self.alpha_2 = float(alpha_1), float(alpha_2)
693
+ self.lambda_1, self.lambda_2 = float(lambda_1), float(lambda_2)
694
+ self.alpha_init, self.lambda_init = alpha_init, lambda_init
695
+ self.fit_intercept = fit_intercept
696
+
697
+ def fit(self, X, y):
698
+ X = np.ascontiguousarray(X, dtype=np.float64)
699
+ y = np.ascontiguousarray(y, dtype=np.float64)
700
+ if X.ndim != 2 or y.ndim != 1 or X.shape[0] != y.size:
701
+ raise ValueError(f"X is {X.shape} and y is {y.shape}; expected "
702
+ "(n, p) and (n,)")
703
+ n, p = X.shape
704
+ y_var = float(y.var())
705
+ if self.fit_intercept:
706
+ xm, ym = X.mean(axis=0), float(y.mean())
707
+ else:
708
+ xm, ym = np.zeros(p), 0.0
709
+ tall = n > p
710
+ if tall:
711
+ # X'X and X'y of the centred data, without centring X
712
+ G = X.T @ X
713
+ c = X.T @ y
714
+ if self.fit_intercept:
715
+ G -= n * np.outer(xm, xm)
716
+ c -= n * ym * xm
717
+ e, V = np.linalg.eigh(G)
718
+ z = V.T @ c
719
+ yy = float(y @ y) - n * ym * ym
720
+ else:
721
+ Xc = X - xm if self.fit_intercept else X
722
+ yc = y - ym
723
+ e, U = np.linalg.eigh(Xc @ Xc.T)
724
+ u = U.T @ yc
725
+ e = np.maximum(e, 0.0)
726
+
727
+ def mean_stats(alpha, lam):
728
+ # the posterior mean at (alpha, lam), its squared norm, and
729
+ # the squared error, without touching X when n > p
730
+ r = lam / alpha
731
+ if tall:
732
+ w = z / (e + r)
733
+ coef = V @ w
734
+ sse = yy - 2.0 * float(w @ z) + float((e * w) @ w)
735
+ return coef, float(w @ w), max(sse, 0.0)
736
+ d = u / (e + r)
737
+ coef = Xc.T @ (U @ d)
738
+ resid = yc - Xc @ coef
739
+ return coef, float((e * d) @ d), float(resid @ resid)
740
+
741
+ eps = np.finfo(np.float64).eps
742
+ alpha = 1.0 / (y_var + eps) if self.alpha_init is None else float(self.alpha_init)
743
+ lam = 1.0 if self.lambda_init is None else float(self.lambda_init)
744
+ old = None
745
+ for it in range(self.max_iter):
746
+ coef, cc, sse = mean_stats(alpha, lam)
747
+ gamma = float(np.sum(alpha * e / (lam + alpha * e)))
748
+ lam = (gamma + 2 * self.lambda_1) / (cc + 2 * self.lambda_2)
749
+ alpha = (n - gamma + 2 * self.alpha_1) / (sse + 2 * self.alpha_2)
750
+ if it != 0 and float(np.sum(np.abs(old - coef))) < self.tol:
751
+ break
752
+ old = coef
753
+ self.n_iter_ = it + 1
754
+ self.alpha_, self.lambda_ = alpha, lam
755
+ self.coef_, _, _ = mean_stats(alpha, lam)
756
+ self.intercept_ = ym - float(xm @ self.coef_) if self.fit_intercept else 0.0
757
+ self.X_offset_ = xm
758
+ # the posterior covariance (lambda I + alpha X'X)^-1
759
+ if tall:
760
+ self.sigma_ = (V / (alpha * e + lam)) @ V.T
761
+ else:
762
+ # Woodbury through the kernel side: (1/lam)(I - X'U D U'X),
763
+ # D = alpha / (lam + alpha e)
764
+ B = Xc.T @ U
765
+ self.sigma_ = (np.eye(p) - (B * (alpha / (lam + alpha * e))) @ B.T) / lam
766
+ return self
767
+
768
+ def predict(self, X, return_std=False):
769
+ X = np.ascontiguousarray(X, dtype=np.float64)
770
+ mean = X @ self.coef_ + self.intercept_
771
+ if not return_std:
772
+ return mean
773
+ Xc = X - self.X_offset_
774
+ var = np.einsum("ij,jk,ik->i", Xc, self.sigma_, Xc) + 1.0 / self.alpha_
775
+ return mean, np.sqrt(var)
776
+
777
+ def _classifier_kernels():
778
+ # imported on first use: the classifiers are their own library
779
+ from ._generated import _loader_classifiers
780
+ return _loader_classifiers
781
+
782
+
783
+ def _binary_labels(y):
784
+ """Two classes as (classes_, y in {-1, +1}), classes_[1] the +1."""
785
+ y = np.asarray(y)
786
+ classes = np.unique(y)
787
+ if classes.size != 2:
788
+ raise ValueError(f"expected two classes, got {classes.size}")
789
+ return classes, np.where(y == classes[1], 1.0, -1.0)
790
+
791
+
792
+ def _seed(random_state):
793
+ # the solvers' Park-Miller generator wants a seed in [1, 2^31 - 2]
794
+ return int(random_state) % 2147483646 + 1
795
+
796
+
797
+ class LinearSVM:
798
+ """The L1-loss (hinge) linear SVM by liblinear's own algorithm.
799
+
800
+ ``examples/kernel-only/svm-liblinear.scm`` translated plainly:
801
+ liblinear's solve_l2r_l1l2_svc, dual coordinate descent with a
802
+ random order in every sweep, shrinking, and the projected-gradient
803
+ stop -- what scikit-learn's ``LinearSVC(loss="hinge", dual=True,
804
+ fit_intercept=False)`` runs, on the same problem
805
+
806
+ min_w 1/2 ||w||^2 + C sum_i max(0, 1 - y_i w.x_i).
807
+
808
+ No intercept (as ``fit_intercept=False``); two classes, the second
809
+ of ``classes_`` being the positive one. ``tol`` and ``max_iter``
810
+ are LinearSVC's (the projected-gradient span and the sweep cap).
811
+
812
+ Attributes after fit: classes_, coef_, dual_coef_ (the alphas),
813
+ n_iter_.
814
+ """
815
+
816
+ def __init__(self, C=1.0, tol=1e-4, max_iter=1000, random_state=0):
817
+ self.C, self.tol, self.max_iter = float(C), float(tol), int(max_iter)
818
+ self.random_state = random_state
819
+
820
+ def fit(self, X, y):
821
+ X = np.ascontiguousarray(X, dtype=np.float64)
822
+ n, p = X.shape
823
+ self.classes_, ys = _binary_labels(y)
824
+ w = np.zeros(p)
825
+ alpha = np.zeros(n)
826
+ xnorm = np.zeros(n)
827
+ self.n_iter_ = _classifier_kernels().svm_liblinear(
828
+ X, ys, w, alpha, xnorm, self.C, self.max_iter, self.tol,
829
+ _seed(self.random_state), n, p)
830
+ self.coef_, self.dual_coef_ = w, alpha
831
+ return self
832
+
833
+ def decision_function(self, X):
834
+ return np.ascontiguousarray(X, dtype=np.float64) @ self.coef_
835
+
836
+ def predict(self, X):
837
+ return self.classes_[(self.decision_function(X) > 0).astype(int)]
838
+
839
+
840
+ class _KernelMatrix:
841
+ # the kernel matrix of KernelSVM and KernelSVR, as SVC and SVR take
842
+ # it: rbf (gamma "scale", "auto" or a number), linear, or precomputed
843
+ def _k(self, A, B):
844
+ if self.kernel == "linear":
845
+ return A @ B.T
846
+ if self.kernel == "rbf":
847
+ sa = (A * A).sum(1)[:, None]
848
+ sb = (B * B).sum(1)[None, :]
849
+ return np.exp(-self._gamma * np.maximum(sa + sb - 2.0 * (A @ B.T), 0.0))
850
+ raise ValueError(f"kernel {self.kernel!r}: expected rbf, linear or precomputed")
851
+
852
+ def _train_kernel(self, X):
853
+ if self.kernel == "precomputed":
854
+ return X
855
+ p = X.shape[1]
856
+ if self.gamma == "scale":
857
+ v = float(X.var())
858
+ self._gamma = 1.0 / (p * v) if v > 0 else 1.0
859
+ elif self.gamma == "auto":
860
+ self._gamma = 1.0 / p
861
+ else:
862
+ self._gamma = float(self.gamma)
863
+ return self._k(X, X)
864
+
865
+ def _support_kernel(self, X):
866
+ X = np.ascontiguousarray(X, dtype=np.float64)
867
+ if self.kernel == "precomputed":
868
+ return X[:, self.support_]
869
+ return self._k(X, self.support_vectors_)
870
+
871
+
872
+ class KernelSVM(_KernelMatrix):
873
+ """C-SVC by libsvm's own algorithm, over the whole kernel matrix.
874
+
875
+ ``examples/kernel-only/svm-smo.scm`` translated plainly: libsvm's
876
+ Solver::Solve -- SMO with the second-order working-set selection
877
+ (WSS3), shrinking with gradient reconstruction, and the bias -- the
878
+ solver scikit-learn's ``SVC`` runs, on the same problem (with the
879
+ intercept). The kernel matrix is built once in numpy and held
880
+ whole, n x n doubles (800 MB at n = 10,000): libsvm with every row
881
+ in its cache. ``kernel`` is "rbf" (``gamma`` "scale", "auto" or a
882
+ number, as in SVC), "linear", or "precomputed" (X is then the
883
+ kernel matrix, and decision_function takes the kernel between new
884
+ points and the training points).
885
+
886
+ Attributes after fit: classes_, support_, dual_coef_ (alpha_i y_i
887
+ over the support), intercept_, n_iter_; support_vectors_ unless
888
+ precomputed.
889
+ """
890
+
891
+ def __init__(self, C=1.0, kernel="rbf", gamma="scale", tol=1e-3, max_iter=-1):
892
+ self.C, self.kernel, self.gamma = float(C), kernel, gamma
893
+ self.tol, self.max_iter = float(tol), int(max_iter)
894
+
895
+ def fit(self, X, y):
896
+ X = np.ascontiguousarray(X, dtype=np.float64)
897
+ self.classes_, ys = _binary_labels(y)
898
+ n = ys.size
899
+ K = self._train_kernel(X)
900
+ alpha = np.zeros(n)
901
+ rho = np.zeros(1)
902
+ iters = self.max_iter if self.max_iter > 0 else 10_000_000
903
+ self.n_iter_ = _classifier_kernels().svm_smo(
904
+ np.ascontiguousarray(K), ys, alpha, rho, self.C, iters, self.tol, n)
905
+ self.support_ = np.flatnonzero(alpha > 0)
906
+ self.dual_coef_ = (alpha * ys)[self.support_]
907
+ self.intercept_ = -float(rho[0])
908
+ if self.kernel != "precomputed":
909
+ self.support_vectors_ = X[self.support_]
910
+ return self
911
+
912
+ def decision_function(self, X):
913
+ return self._support_kernel(X) @ self.dual_coef_ + self.intercept_
914
+
915
+ def predict(self, X):
916
+ return self.classes_[(self.decision_function(X) > 0).astype(int)]
917
+
918
+
919
+ class LinearSVR:
920
+ """The linear epsilon-SVR with the L1 loss, by liblinear's own algorithm.
921
+
922
+ ``examples/kernel-only/svr-liblinear.scm`` translated plainly:
923
+ liblinear's solve_l2r_l1l2_svr (dual coordinate descent, random
924
+ order, shrinking) -- what scikit-learn's
925
+ ``LinearSVR(loss="epsilon_insensitive", dual=True,
926
+ fit_intercept=False)`` runs, on the same problem
927
+
928
+ min_w 1/2 ||w||^2 + C sum_i max(0, |y_i - w.x_i| - epsilon).
929
+
930
+ ``epsilon`` defaults to 0, ``tol`` to 1e-4 and ``max_iter`` to 1000
931
+ sweeps, as in LinearSVR; no intercept.
932
+
933
+ Attributes after fit: coef_, dual_coef_ (the betas), n_iter_.
934
+ """
935
+
936
+ def __init__(self, C=1.0, epsilon=0.0, tol=1e-4, max_iter=1000, random_state=0):
937
+ self.C, self.epsilon = float(C), float(epsilon)
938
+ self.tol, self.max_iter = float(tol), int(max_iter)
939
+ self.random_state = random_state
940
+
941
+ def fit(self, X, y):
942
+ X = np.ascontiguousarray(X, dtype=np.float64)
943
+ y = np.ascontiguousarray(y, dtype=np.float64)
944
+ n, p = X.shape
945
+ w = np.zeros(p)
946
+ beta = np.zeros(n)
947
+ xnorm = np.zeros(n)
948
+ self.n_iter_ = _classifier_kernels().svr_liblinear(
949
+ X, y, w, beta, xnorm, self.C, self.epsilon, self.max_iter, self.tol,
950
+ _seed(self.random_state), n, p)
951
+ self.coef_, self.dual_coef_ = w, beta
952
+ return self
953
+
954
+ def predict(self, X):
955
+ return np.ascontiguousarray(X, dtype=np.float64) @ self.coef_
956
+
957
+
958
+ class KernelSVR(_KernelMatrix):
959
+ """epsilon-SVR by libsvm's own algorithm, over the whole kernel matrix.
960
+
961
+ The same translated SMO solver as KernelSVM (``svm-smo.scm``), set
962
+ up as libsvm's solve_epsilon_svr sets it up: the 2n variables
963
+ alpha and alpha* with linear terms epsilon -/+ y_i. scikit-learn's
964
+ ``SVR``, with the intercept; ``epsilon=0.1``, ``tol=1e-3`` and the
965
+ kernels as there. The kernel matrix is held whole.
966
+
967
+ Attributes after fit: support_, dual_coef_ (alpha - alpha* over the
968
+ support), intercept_, n_iter_; support_vectors_ unless precomputed.
969
+ """
970
+
971
+ def __init__(self, C=1.0, epsilon=0.1, kernel="rbf", gamma="scale", tol=1e-3, max_iter=-1):
972
+ self.C, self.epsilon, self.kernel, self.gamma = float(C), float(epsilon), kernel, gamma
973
+ self.tol, self.max_iter = float(tol), int(max_iter)
974
+
975
+ def fit(self, X, y):
976
+ X = np.ascontiguousarray(X, dtype=np.float64)
977
+ y = np.ascontiguousarray(y, dtype=np.float64)
978
+ n = y.size
979
+ K = self._train_kernel(X)
980
+ beta = np.zeros(n)
981
+ rho = np.zeros(1)
982
+ iters = self.max_iter if self.max_iter > 0 else 10_000_000
983
+ self.n_iter_ = _classifier_kernels().svr_smo(
984
+ np.ascontiguousarray(K), y, beta, rho, self.C, self.epsilon, iters, self.tol, n)
985
+ self.support_ = np.flatnonzero(beta != 0)
986
+ self.dual_coef_ = beta[self.support_]
987
+ self.intercept_ = -float(rho[0])
988
+ if self.kernel != "precomputed":
989
+ self.support_vectors_ = X[self.support_]
990
+ return self
991
+
992
+ def predict(self, X):
993
+ return self._support_kernel(X) @ self.dual_coef_ + self.intercept_
994
+
995
+
996
+ def nmf_backend():
997
+ """The variant CDNMF takes by default: cublas when a device
998
+ answers, else blas, else loops."""
999
+ built = nmf_backends()
1000
+ if not built:
1001
+ raise ImportError("scm2cpp_lasso: no variant of the derived NMF was built")
1002
+ if "cublas" in built and cuda_available():
1003
+ return "cublas"
1004
+ cpu = [v for v in built if v != "cublas"]
1005
+ return cpu[0] if cpu else built[0]
1006
+
1007
+
1008
+ class CDNMF:
1009
+ """Non-negative matrix factorization by coordinate descent, the
1010
+ translator's derivation end to end.
1011
+
1012
+ ``examples/kernel-only/nmf-cd.scm`` writes scikit-learn's
1013
+ NMF(solver="cd") iteration the plain way, with the residual
1014
+ R = X - W H: each half step sweeps the coordinates of one factor and
1015
+ moves R along the other. ``--derive`` turns that half step into
1016
+ scikit-learn's own form -- the memo X H' - W H H' over the hoisted
1017
+ Gram H H', with R never formed -- and ``--blas`` / ``--cublas`` hand
1018
+ the products to (cu)BLAS. Nothing of the Gram form is written by
1019
+ hand. The iteration, the stopping rule (the projected-gradient
1020
+ violation down to ``tol`` times the first iteration's) and the
1021
+ result are scikit-learn's with Frobenius loss, no regularization and
1022
+ no shuffling.
1023
+
1024
+ ``init`` is "random" (scikit-learn's: |N(0, 1)| scaled by
1025
+ sqrt(mean(X) / k), H drawn before W, from ``random_state``) or
1026
+ "custom" (W and H given to fit_transform). scikit-learn's default
1027
+ "nndsvda" is not offered; pass "custom" with its W and H for an
1028
+ exact comparison. ``backend`` is "cublas", "blas" or "loops", or
1029
+ None for the best built (``nmf_backend()``).
1030
+
1031
+ Attributes after fit: components_ (H, k x p), n_iter_,
1032
+ reconstruction_err_ (||X - W H||_F).
1033
+ """
1034
+
1035
+ def __init__(self, n_components, *, init="random", tol=1e-4, max_iter=200,
1036
+ random_state=None, backend=None):
1037
+ self.n_components = int(n_components)
1038
+ self.init, self.tol, self.max_iter = init, float(tol), int(max_iter)
1039
+ self.random_state = random_state
1040
+ self.backend = backend
1041
+
1042
+ def _lib(self):
1043
+ import importlib
1044
+ v = self.backend or nmf_backend()
1045
+ return importlib.import_module(f"{__name__}._generated._loader_nmf_{v}")
1046
+
1047
+ def fit_transform(self, X, W=None, H=None):
1048
+ X = np.ascontiguousarray(X, dtype=np.float64)
1049
+ if X.min() < 0:
1050
+ raise ValueError("NMF needs a non-negative X")
1051
+ n, p = X.shape
1052
+ k = self.n_components
1053
+ if self.init == "custom":
1054
+ if W is None or H is None:
1055
+ raise ValueError('init="custom" needs W and H')
1056
+ W = np.array(W, dtype=np.float64, copy=True)
1057
+ H = np.array(H, dtype=np.float64)
1058
+ elif self.init == "random":
1059
+ rng = np.random.RandomState(self.random_state)
1060
+ avg = np.sqrt(X.mean() / k)
1061
+ H = np.abs(avg * rng.standard_normal(size=(k, p)))
1062
+ W = np.abs(avg * rng.standard_normal(size=(n, k)))
1063
+ else:
1064
+ raise ValueError(f"init {self.init!r}: expected 'random' or 'custom'")
1065
+ W = np.ascontiguousarray(W)
1066
+ HT = np.ascontiguousarray(H.T)
1067
+ XT = np.ascontiguousarray(X.T)
1068
+ self.n_iter_ = self._lib().nmf(X, XT, W, HT, self.max_iter, self.tol, n, p, k)
1069
+ self.components_ = np.ascontiguousarray(HT.T)
1070
+ self.reconstruction_err_ = float(np.linalg.norm(X - W @ self.components_))
1071
+ return W
1072
+
1073
+ def fit(self, X, W=None, H=None):
1074
+ self.fit_transform(X, W, H)
1075
+ return self
1076
+
1077
+
1078
+ class CDGraphicalLasso:
1079
+ """The graphical lasso, scikit-learn's own algorithm translated.
1080
+
1081
+ ``examples/kernel-only/graphical-lasso.scm`` is scikit-learn's
1082
+ _graphical_lasso in mode "cd" and its inner solver
1083
+ enet_coordinate_descent_gram (the gap-safe rule included), written
1084
+ out whole; ``--blas`` makes its two matrix-vector products dgemv.
1085
+ The same model and stopping rule as ``GraphicalLasso(mode="cd")``:
1086
+ ``alpha``, ``tol``, ``enet_tol``, ``max_iter`` and
1087
+ ``assume_centered`` mean what they mean there. scikit-learn's
1088
+ ``costs_`` (the objective each iteration, a log-determinant) is not
1089
+ computed.
1090
+
1091
+ Attributes after fit: covariance_, precision_, location_, n_iter_.
1092
+ """
1093
+
1094
+ def __init__(self, alpha=0.01, *, tol=1e-4, enet_tol=1e-4, max_iter=100,
1095
+ assume_centered=False):
1096
+ self.alpha, self.tol, self.enet_tol = float(alpha), float(tol), float(enet_tol)
1097
+ self.max_iter, self.assume_centered = int(max_iter), assume_centered
1098
+
1099
+ def fit(self, X):
1100
+ import importlib
1101
+ from ._libfind import gramops_backend
1102
+ X = np.asarray(X, dtype=np.float64)
1103
+ n, p = X.shape
1104
+ self.location_ = np.zeros(p) if self.assume_centered else X.mean(axis=0)
1105
+ Xc = X - self.location_
1106
+ emp = np.ascontiguousarray(Xc.T @ Xc / n)
1107
+ lib = importlib.import_module(f"{__name__}._generated._loader_gramops_{gramops_backend()}")
1108
+ self.covariance_ = np.zeros((p, p))
1109
+ self.precision_ = np.zeros((p, p))
1110
+ self.n_iter_ = lib.graphical_lasso(emp, self.covariance_, self.precision_, self.alpha,
1111
+ self.max_iter, self.tol, self.enet_tol, p)
1112
+ if not np.isfinite(self.precision_).all():
1113
+ raise FloatingPointError("the system is too ill-conditioned for this solver")
1114
+ return self
1115
+
1116
+
1117
+ class LinearRANSAC:
1118
+ """RANSAC for a linear model, scikit-learn's algorithm translated.
1119
+
1120
+ ``examples/kernel-only/ransac.scm`` is RANSACRegressor.fit around
1121
+ LinearRegression, written out whole: per trial, least squares with
1122
+ an intercept on a subset of ``min_samples`` points, the inliers
1123
+ within ``residual_threshold``, the fit kept by inlier count and then
1124
+ R^2, the trial count lowered by scikit-learn's _dynamic_max_trials;
1125
+ finally least squares on the best inliers. The defaults are
1126
+ RANSACRegressor's (min_samples p + 1, the threshold the MAD of y,
1127
+ max_trials 100, stop_probability 0.99).
1128
+
1129
+ The subsets are drawn here, ``max_trials`` of them, before the
1130
+ trials run. With scikit-learn installed they are drawn exactly as
1131
+ RANSACRegressor draws them (sample_without_replacement on a
1132
+ RandomState of ``random_state``), so the result is scikit-learn's;
1133
+ without it, numpy's choice draws different subsets.
1134
+
1135
+ Attributes after fit: coef_, intercept_, inlier_mask_, n_trials_.
1136
+ """
1137
+
1138
+ def __init__(self, *, min_samples=None, residual_threshold=None, max_trials=100,
1139
+ stop_probability=0.99, random_state=None):
1140
+ self.min_samples, self.residual_threshold = min_samples, residual_threshold
1141
+ self.max_trials, self.stop_probability = int(max_trials), float(stop_probability)
1142
+ self.random_state = random_state
1143
+
1144
+ def fit(self, X, y):
1145
+ X = np.ascontiguousarray(X, dtype=np.float64)
1146
+ y = np.ascontiguousarray(y, dtype=np.float64)
1147
+ n, p = X.shape
1148
+ ms = p + 1 if self.min_samples is None else int(self.min_samples)
1149
+ thr = (float(np.median(np.abs(y - np.median(y)))) if self.residual_threshold is None
1150
+ else float(self.residual_threshold))
1151
+ rs = np.random.RandomState(self.random_state)
1152
+ try:
1153
+ from sklearn.utils.random import sample_without_replacement
1154
+ draw = lambda: sample_without_replacement(n, ms, random_state=rs)
1155
+ except ImportError:
1156
+ draw = lambda: rs.choice(n, ms, replace=False)
1157
+ subsets = np.array([draw() for _ in range(self.max_trials)], dtype=np.float64)
1158
+ coef = np.zeros(p)
1159
+ mask = np.zeros(n)
1160
+ out = np.zeros(1)
1161
+ self.n_trials_ = _classifier_kernels().ransac(
1162
+ X, y, subsets, coef, mask, out, self.max_trials, ms, thr, self.stop_probability, n, p)
1163
+ self.coef_, self.intercept_ = coef, float(out[0])
1164
+ self.inlier_mask_ = mask.astype(bool)
1165
+ return self
1166
+
1167
+ def predict(self, X):
1168
+ return np.ascontiguousarray(X, dtype=np.float64) @ self.coef_ + self.intercept_
1169
+
1170
+
1171
+ def _center(X, y, fit_intercept):
1172
+ X = np.ascontiguousarray(X, dtype=np.float64)
1173
+ y = np.ascontiguousarray(y, dtype=np.float64)
1174
+ if fit_intercept:
1175
+ xm, ym = X.mean(axis=0), float(y.mean())
1176
+ return np.ascontiguousarray(X - xm), y - ym, xm, ym
1177
+ return X, y, np.zeros(X.shape[1]), 0.0
1178
+
1179
+
1180
+ _SGD_LOSS = {"hinge": 0, "squared_error": 1, "perceptron": 2, "epsilon_insensitive": 3}
1181
+ _SGD_LR = {"optimal": 0, "invscaling": 1, "constant": 2, "pa1": 3, "pa2": 4}
1182
+
1183
+
1184
+ class _SGDLinear:
1185
+ def __init__(self, *, loss, penalty="l2", alpha=1e-4, max_iter=1000, tol=1e-3,
1186
+ n_iter_no_change=5, fit_intercept=True, learning_rate, eta0, power_t,
1187
+ epsilon=0.1):
1188
+ self.loss, self.penalty, self.alpha = loss, penalty, float(alpha)
1189
+ self.max_iter, self.tol = int(max_iter), float(tol)
1190
+ self.n_iter_no_change, self.fit_intercept = int(n_iter_no_change), fit_intercept
1191
+ self.learning_rate, self.eta0, self.power_t = learning_rate, float(eta0), float(power_t)
1192
+ self.epsilon = float(epsilon)
1193
+
1194
+ def _run(self, X, y):
1195
+ if self.loss not in _SGD_LOSS or self.learning_rate not in _SGD_LR:
1196
+ raise ValueError("loss is one of %s, learning_rate one of %s"
1197
+ % (sorted(_SGD_LOSS), sorted(_SGD_LR)))
1198
+ if self.penalty not in ("l2", None):
1199
+ raise ValueError("penalty is 'l2' or None")
1200
+ X = np.ascontiguousarray(X, dtype=np.float64)
1201
+ n, p = X.shape
1202
+ w = np.zeros(p)
1203
+ out = np.zeros(1)
1204
+ self.n_iter_ = _classifier_kernels().sgd(
1205
+ X, np.ascontiguousarray(y, dtype=np.float64), w, out,
1206
+ _SGD_LOSS[self.loss], _SGD_LR[self.learning_rate],
1207
+ 1 if self.penalty == "l2" else 0, self.alpha, self.eta0, self.power_t,
1208
+ self.epsilon, self.max_iter, self.tol, self.n_iter_no_change,
1209
+ 1 if self.fit_intercept else 0, n, p)
1210
+ self.coef_, self.intercept_ = w, float(out[0])
1211
+ return self
1212
+
1213
+ def decision_function(self, X):
1214
+ return np.ascontiguousarray(X, dtype=np.float64) @ self.coef_ + self.intercept_
1215
+
1216
+
1217
+ class SGDLinearClassifier(_SGDLinear):
1218
+ """scikit-learn's SGDClassifier by its own _plain_sgd translated
1219
+ (``examples/kernel-only/sgd.scm``), with shuffle=False: the points
1220
+ are visited in order. scikit-learn's shuffling draws from its own
1221
+ xorshift generator, which the kernel does not reproduce, so compare
1222
+ against ``SGDClassifier(shuffle=False)``, which this matches to
1223
+ rounding. loss "hinge" or "perceptron", penalty "l2" or None,
1224
+ learning_rate "optimal", "constant", "invscaling", "pa1" or "pa2"
1225
+ (the passive-aggressive steps, eta0 their C: scikit-learn's
1226
+ replacement for PassiveAggressiveClassifier). Two classes, the
1227
+ second of ``classes_`` positive.
1228
+
1229
+ Attributes after fit: classes_, coef_, intercept_, n_iter_.
1230
+ """
1231
+
1232
+ def __init__(self, *, loss="hinge", penalty="l2", alpha=1e-4, max_iter=1000, tol=1e-3,
1233
+ n_iter_no_change=5, fit_intercept=True, learning_rate="optimal",
1234
+ eta0=0.01, power_t=0.5):
1235
+ super().__init__(loss=loss, penalty=penalty, alpha=alpha, max_iter=max_iter,
1236
+ tol=tol, n_iter_no_change=n_iter_no_change,
1237
+ fit_intercept=fit_intercept, learning_rate=learning_rate,
1238
+ eta0=eta0, power_t=power_t)
1239
+
1240
+ def fit(self, X, y):
1241
+ self.classes_, ys = _binary_labels(y)
1242
+ return self._run(X, ys)
1243
+
1244
+ def predict(self, X):
1245
+ return self.classes_[(self.decision_function(X) > 0).astype(int)]
1246
+
1247
+
1248
+ class SGDLinearRegressor(_SGDLinear):
1249
+ """scikit-learn's SGDRegressor by its own _plain_sgd translated, with
1250
+ shuffle=False (see SGDLinearClassifier). loss "squared_error" or
1251
+ "epsilon_insensitive" (``epsilon``), penalty "l2" or None,
1252
+ learning_rate "invscaling" (the default), "constant", "optimal",
1253
+ "pa1" or "pa2" (scikit-learn's replacement for
1254
+ PassiveAggressiveRegressor).
1255
+
1256
+ Attributes after fit: coef_, intercept_, n_iter_.
1257
+ """
1258
+
1259
+ def __init__(self, *, loss="squared_error", penalty="l2", alpha=1e-4, max_iter=1000,
1260
+ tol=1e-3, n_iter_no_change=5, fit_intercept=True,
1261
+ learning_rate="invscaling", eta0=0.01, power_t=0.25, epsilon=0.1):
1262
+ super().__init__(loss=loss, penalty=penalty, alpha=alpha, max_iter=max_iter,
1263
+ tol=tol, n_iter_no_change=n_iter_no_change,
1264
+ fit_intercept=fit_intercept, learning_rate=learning_rate,
1265
+ eta0=eta0, power_t=power_t, epsilon=epsilon)
1266
+
1267
+ def fit(self, X, y):
1268
+ return self._run(X, y)
1269
+
1270
+ def predict(self, X):
1271
+ return self.decision_function(X)
1272
+
1273
+
1274
+ class SGDPerceptron(SGDLinearClassifier):
1275
+ """scikit-learn's Perceptron: SGDClassifier with the perceptron's
1276
+ hinge (threshold 0), no penalty and the constant step eta0 = 1, by
1277
+ the same translated _plain_sgd, shuffle=False.
1278
+
1279
+ Attributes after fit: classes_, coef_, intercept_, n_iter_.
1280
+ """
1281
+
1282
+ def __init__(self, *, max_iter=1000, tol=1e-3, n_iter_no_change=5, fit_intercept=True,
1283
+ eta0=1.0):
1284
+ super().__init__(loss="perceptron", penalty=None, max_iter=max_iter, tol=tol,
1285
+ n_iter_no_change=n_iter_no_change, fit_intercept=fit_intercept,
1286
+ learning_rate="constant", eta0=eta0)
1287
+
1288
+
1289
+ class GramOMP:
1290
+ """Orthogonal matching pursuit over the Gram matrix, scikit-learn's
1291
+ own algorithm translated (``examples/kernel-only/omp.scm``:
1292
+ orthogonal_mp_gram and _gram_omp, X'X and X'y by dsyrk and dgemv).
1293
+ ``OrthogonalMatchingPursuit`` takes this route for n > p
1294
+ (precompute="auto"); here it is taken always. ``n_nonzero_coefs``
1295
+ defaults, as there, to 10% of the features.
1296
+
1297
+ Attributes after fit: coef_, intercept_, n_iter_.
1298
+ """
1299
+
1300
+ def __init__(self, *, n_nonzero_coefs=None, fit_intercept=True):
1301
+ self.n_nonzero_coefs, self.fit_intercept = n_nonzero_coefs, fit_intercept
1302
+
1303
+ def fit(self, X, y):
1304
+ import importlib
1305
+ from ._libfind import gramops_backend
1306
+ Xc, yc, xm, ym = _center(X, y, self.fit_intercept)
1307
+ n, p = Xc.shape
1308
+ k = max(int(0.1 * p), 1) if self.n_nonzero_coefs is None else int(self.n_nonzero_coefs)
1309
+ lib = importlib.import_module(f"{__name__}._generated._loader_gramops_{gramops_backend()}")
1310
+ self.coef_ = np.zeros(p)
1311
+ self.n_iter_ = lib.omp(Xc, yc, self.coef_, np.zeros(1), 0, k, n, p)
1312
+ self.intercept_ = ym - float(xm @ self.coef_)
1313
+ return self
1314
+
1315
+ def predict(self, X):
1316
+ return np.ascontiguousarray(X, dtype=np.float64) @ self.coef_ + self.intercept_
1317
+
1318
+
1319
+ def _gramops_kernels():
1320
+ import importlib
1321
+ from ._libfind import gramops_backend
1322
+ return importlib.import_module(f"{__name__}._generated._loader_gramops_{gramops_backend()}")
1323
+
1324
+
1325
+ class LBFGSLogisticRegression:
1326
+ """L2-penalised logistic regression, scikit-learn's
1327
+ ``LogisticRegression(solver="lbfgs")`` translated whole
1328
+ (``examples/kernel-only/logistic-lbfgs.scm`` over ``lbfgs.scm``):
1329
+ the mean binomial loss for two classes, the multinomial for more,
1330
+ plus |W|^2 / (2 C n), minimised by scipy's L-BFGS-B as written in
1331
+ the subset (ten pairs, the More-Thuente search, gtol ``tol``, ftol
1332
+ 64 eps). The products X W' and R' X are dgemm where a CBLAS links.
1333
+ On the same data it takes the same iterations as scikit-learn and
1334
+ lands on the same coefficients to rounding.
1335
+
1336
+ Attributes after fit: classes_, coef_, intercept_, n_iter_.
1337
+ """
1338
+
1339
+ def __init__(self, *, C=1.0, tol=1e-4, max_iter=100, fit_intercept=True):
1340
+ self.C, self.tol, self.max_iter, self.fit_intercept = C, tol, max_iter, fit_intercept
1341
+
1342
+ def fit(self, X, y):
1343
+ X = np.ascontiguousarray(X, dtype=np.float64)
1344
+ self.classes_, yi = np.unique(np.asarray(y), return_inverse=True)
1345
+ n, p = X.shape
1346
+ nc = len(self.classes_)
1347
+ if nc < 2:
1348
+ raise ValueError("LBFGSLogisticRegression needs at least two classes")
1349
+ multi = 1 if nc > 2 else 0
1350
+ k = nc if multi else 1
1351
+ coef = np.zeros(k * (p + 1))
1352
+ out = np.zeros(2)
1353
+ it = _gpu_or_cpu(n * p, getattr(self, "backend", None)).logistic_lbfgs(
1354
+ X, yi.astype(np.float64), coef, out, float(self.C), int(self.max_iter),
1355
+ float(self.tol), multi, 1 if self.fit_intercept else 0, n, p, k)
1356
+ self.coef_ = coef[:k * p].reshape(k, p).copy()
1357
+ self.intercept_ = coef[k * p:].copy()
1358
+ self.n_iter_ = np.array([min(it, self.max_iter)])
1359
+ return self
1360
+
1361
+ def decision_function(self, X):
1362
+ z = np.ascontiguousarray(X, dtype=np.float64) @ self.coef_.T + self.intercept_
1363
+ return z[:, 0] if z.shape[1] == 1 else z
1364
+
1365
+ def predict_proba(self, X):
1366
+ z = self.decision_function(X)
1367
+ if z.ndim == 1:
1368
+ p1 = 1.0 / (1.0 + np.exp(-z))
1369
+ return np.column_stack([1.0 - p1, p1])
1370
+ e = np.exp(z - z.max(axis=1, keepdims=True))
1371
+ return e / e.sum(axis=1, keepdims=True)
1372
+
1373
+ def predict(self, X):
1374
+ z = self.decision_function(X)
1375
+ if z.ndim == 1:
1376
+ return self.classes_[(z > 0).astype(int)]
1377
+ return self.classes_[np.argmax(z, axis=1)]
1378
+
1379
+
1380
+ def _kfold(n, k):
1381
+ """scikit-learn's KFold(k) without shuffling: contiguous test blocks,
1382
+ the first n % k of them one longer."""
1383
+ sizes = np.full(k, n // k)
1384
+ sizes[: n % k] += 1
1385
+ out, start = [], 0
1386
+ for sz in sizes:
1387
+ test = np.arange(start, start + sz)
1388
+ out.append((np.r_[np.arange(0, start), np.arange(start + sz, n)], test))
1389
+ start += sz
1390
+ return out
1391
+
1392
+
1393
+ def _stratified_kfold(y, k):
1394
+ """scikit-learn's StratifiedKFold(k) without shuffling: classes in
1395
+ order of first appearance, each spread over the folds as the sorted
1396
+ labels dealt k ways."""
1397
+ y = np.asarray(y)
1398
+ _, y_idx, y_inv = np.unique(y, return_index=True, return_inverse=True)
1399
+ _, class_perm = np.unique(y_idx, return_inverse=True)
1400
+ y_enc = class_perm[y_inv]
1401
+ nc = len(y_idx)
1402
+ y_order = np.sort(y_enc)
1403
+ alloc = np.asarray([np.bincount(y_order[i::k], minlength=nc) for i in range(k)])
1404
+ test_folds = np.empty(len(y), dtype=int)
1405
+ for c in range(nc):
1406
+ test_folds[y_enc == c] = np.arange(k).repeat(alloc[:, c])
1407
+ return [(np.flatnonzero(test_folds != f), np.flatnonzero(test_folds == f))
1408
+ for f in range(k)]
1409
+
1410
+
1411
+ class _LBFGSGLM:
1412
+ _family = None
1413
+
1414
+ def fit(self, X, y):
1415
+ X = np.ascontiguousarray(X, dtype=np.float64)
1416
+ y = np.ascontiguousarray(y, dtype=np.float64)
1417
+ n, p = X.shape
1418
+ coef = np.zeros(p + 1)
1419
+ out = np.zeros(2)
1420
+ fam, power = self._family_power()
1421
+ it = _gpu_or_cpu(n * p, getattr(self, "backend", None)).glm(X, y, coef, out, fam, float(power), float(self.alpha),
1422
+ int(self.max_iter), float(self.tol),
1423
+ 1 if self.fit_intercept else 0, n, p)
1424
+ self.coef_, self.intercept_ = coef[:p].copy(), float(coef[p])
1425
+ self.n_iter_ = min(it, self.max_iter)
1426
+ return self
1427
+
1428
+ def _linear(self, X):
1429
+ return np.ascontiguousarray(X, dtype=np.float64) @ self.coef_ + self.intercept_
1430
+
1431
+ def predict(self, X):
1432
+ z = self._linear(X)
1433
+ return z if self._family_power() == (3, 0.0) else np.exp(z)
1434
+
1435
+
1436
+ class LBFGSPoissonRegressor(_LBFGSGLM):
1437
+ """scikit-learn's ``PoissonRegressor`` (solver "lbfgs") translated
1438
+ whole (``examples/kernel-only/glm.scm`` over ``lbfgs.scm``): the mean
1439
+ half Poisson deviance with the log link plus alpha |w|^2 / 2, from
1440
+ scikit-learn's start. Same iterations and coefficients to rounding.
1441
+
1442
+ Attributes after fit: coef_, intercept_, n_iter_.
1443
+ """
1444
+
1445
+ def __init__(self, *, alpha=1.0, fit_intercept=True, max_iter=100, tol=1e-4):
1446
+ self.alpha, self.fit_intercept, self.max_iter, self.tol = alpha, fit_intercept, max_iter, tol
1447
+
1448
+ def _family_power(self):
1449
+ return 0, 1.0
1450
+
1451
+
1452
+ class LBFGSGammaRegressor(LBFGSPoissonRegressor):
1453
+ """scikit-learn's ``GammaRegressor`` (solver "lbfgs"), as
1454
+ LBFGSPoissonRegressor with the half Gamma deviance.
1455
+
1456
+ Attributes after fit: coef_, intercept_, n_iter_.
1457
+ """
1458
+
1459
+ def _family_power(self):
1460
+ return 1, 2.0
1461
+
1462
+
1463
+ class LBFGSTweedieRegressor(_LBFGSGLM):
1464
+ """scikit-learn's ``TweedieRegressor`` (solver "lbfgs"): ``power`` 0
1465
+ with the identity link (the default), or a power of 1 or more with
1466
+ the log link, as link="auto" chooses.
1467
+
1468
+ Attributes after fit: coef_, intercept_, n_iter_.
1469
+ """
1470
+
1471
+ def __init__(self, *, power=0.0, alpha=1.0, fit_intercept=True, max_iter=100, tol=1e-4):
1472
+ self.power, self.alpha, self.fit_intercept = power, alpha, fit_intercept
1473
+ self.max_iter, self.tol = max_iter, tol
1474
+
1475
+ def _family_power(self):
1476
+ if self.power == 0:
1477
+ return 3, 0.0
1478
+ if self.power < 1:
1479
+ raise ValueError("power is 0 or at least 1 here")
1480
+ return 2, float(self.power)
1481
+
1482
+
1483
+ class LBFGSLogisticRegressionCV:
1484
+ """scikit-learn's ``LogisticRegressionCV`` with its defaults (the L2
1485
+ penalty, solver "lbfgs", Cs 10 values from 1e-4 to 1e4,
1486
+ StratifiedKFold(5) without shuffling, accuracy, refit): each fold's
1487
+ path over the Cs warm-started from the previous C by the translated
1488
+ LogisticRegression (``logistic-lbfgs.scm``), the C of the best summed
1489
+ score, and the refit from the folds' mean coefficients at that C, as
1490
+ scikit-learn starts it.
1491
+
1492
+ Attributes after fit: classes_, Cs_, C_, coef_, intercept_, scores_,
1493
+ n_iter_.
1494
+ """
1495
+
1496
+ def __init__(self, *, Cs=10, cv=5, tol=1e-4, max_iter=100, fit_intercept=True):
1497
+ self.Cs, self.cv, self.tol, self.max_iter = Cs, cv, tol, max_iter
1498
+ self.fit_intercept = fit_intercept
1499
+
1500
+ def _path(self, X, yi, Cs, coef, multi, k):
1501
+ n, p = X.shape
1502
+ lib = _gpu_or_cpu(n * p, getattr(self, "backend", None))
1503
+ coefs, iters = [], []
1504
+ out = np.zeros(2)
1505
+ for C in Cs:
1506
+ it = lib.logistic_lbfgs(X, yi, coef, out, float(C), int(self.max_iter),
1507
+ float(self.tol), multi, 1 if self.fit_intercept else 0,
1508
+ n, p, k)
1509
+ coefs.append(coef.copy())
1510
+ iters.append(min(it, self.max_iter))
1511
+ return coefs, iters
1512
+
1513
+ def fit(self, X, y):
1514
+ X = np.ascontiguousarray(X, dtype=np.float64)
1515
+ y = np.asarray(y)
1516
+ self.classes_, yi = np.unique(y, return_inverse=True)
1517
+ nc = len(self.classes_)
1518
+ multi = 1 if nc > 2 else 0
1519
+ k = nc if multi else 1
1520
+ n, p = X.shape
1521
+ Cs = np.logspace(-4, 4, self.Cs) if np.isscalar(self.Cs) else np.asarray(self.Cs, float)
1522
+ self.Cs_ = Cs
1523
+ yf = yi.astype(np.float64)
1524
+ folds = _stratified_kfold(y, int(self.cv))
1525
+ paths, scores, iters = [], [], []
1526
+ for train, test in folds:
1527
+ coefs, it = self._path(np.ascontiguousarray(X[train]), yf[train], Cs,
1528
+ np.zeros(k * (p + 1)), multi, k)
1529
+ paths.append(coefs)
1530
+ iters.append(it)
1531
+ sc = []
1532
+ for c in coefs:
1533
+ z = X[test] @ c[:k * p].reshape(k, p).T + c[k * p:]
1534
+ pred = (z[:, 0] > 0).astype(int) if k == 1 else np.argmax(z, axis=1)
1535
+ sc.append(float(np.mean(pred == yi[test])))
1536
+ scores.append(sc)
1537
+ scores = np.asarray(scores)
1538
+ best = int(np.argmax(scores.sum(axis=0)))
1539
+ self.C_ = float(Cs[best])
1540
+ start = np.mean([paths[f][best] for f in range(len(folds))], axis=0)
1541
+ coefs, it = self._path(X, yf, [self.C_], start.copy(), multi, k)
1542
+ c = coefs[0]
1543
+ self.coef_ = c[:k * p].reshape(k, p).copy()
1544
+ self.intercept_ = c[k * p:].copy()
1545
+ self.scores_ = scores
1546
+ self.n_iter_ = np.asarray(iters)
1547
+ return self
1548
+
1549
+ def decision_function(self, X):
1550
+ z = np.ascontiguousarray(X, dtype=np.float64) @ self.coef_.T + self.intercept_
1551
+ return z[:, 0] if z.shape[1] == 1 else z
1552
+
1553
+ def predict(self, X):
1554
+ z = self.decision_function(X)
1555
+ if z.ndim == 1:
1556
+ return self.classes_[(z > 0).astype(int)]
1557
+ return self.classes_[np.argmax(z, axis=1)]
1558
+
1559
+
1560
+ class LBFGSHuberRegressor:
1561
+ """The Huber regressor, scikit-learn's ``HuberRegressor`` translated
1562
+ whole (``examples/kernel-only/huber.scm`` over ``lbfgs.scm``): its
1563
+ _huber_loss_and_gradient over the weights, the intercept and the
1564
+ scale, minimised by L-BFGS-B with the scale bounded below by 10 eps
1565
+ (the generalized Cauchy point and the subspace step for that one
1566
+ bound, as L-BFGS-B takes them), scipy's defaults otherwise. X w
1567
+ and X'c are dgemv where a CBLAS links. Same iterations and
1568
+ coefficients as scikit-learn to rounding.
1569
+
1570
+ Attributes after fit: coef_, intercept_, scale_, n_iter_, outliers_.
1571
+ """
1572
+
1573
+ def __init__(self, *, epsilon=1.35, alpha=1e-4, max_iter=100, tol=1e-5,
1574
+ fit_intercept=True):
1575
+ self.epsilon, self.alpha, self.max_iter, self.tol = epsilon, alpha, max_iter, tol
1576
+ self.fit_intercept = fit_intercept
1577
+
1578
+ def fit(self, X, y):
1579
+ X = np.ascontiguousarray(X, dtype=np.float64)
1580
+ y = np.ascontiguousarray(y, dtype=np.float64)
1581
+ n, p = X.shape
1582
+ coef = np.zeros(p + 2)
1583
+ coef[-1] = 1.0
1584
+ out = np.zeros(2)
1585
+ it = _gpu_or_cpu(n * p, getattr(self, "backend", None)).huber(
1586
+ X, y, coef, out, float(self.epsilon), float(self.alpha), int(self.max_iter),
1587
+ float(self.tol), 1 if self.fit_intercept else 0, n, p)
1588
+ self.coef_ = coef[:p].copy()
1589
+ self.intercept_ = float(coef[p])
1590
+ self.scale_ = float(coef[p + 1])
1591
+ self.n_iter_ = min(it, self.max_iter)
1592
+ self.outliers_ = np.abs(y - X @ self.coef_ - self.intercept_) > self.scale_ * self.epsilon
1593
+ return self
1594
+
1595
+ def predict(self, X):
1596
+ return np.ascontiguousarray(X, dtype=np.float64) @ self.coef_ + self.intercept_
1597
+
1598
+
1599
+ class EMGaussianMixture:
1600
+ """The Gaussian mixture by EM with full covariances, scikit-learn's
1601
+ ``GaussianMixture(covariance_type="full")`` translated whole
1602
+ (``examples/kernel-only/gmm.scm``): its M step (means and
1603
+ covariances by products, the Cholesky factor, the precision factor
1604
+ L^-T), its E step (the log densities through X U - mu U, a product;
1605
+ log-sum-exp; the lower bound) and its stop on the bound. The start
1606
+ is scikit-learn's: ``init_params="kmeans"`` takes the labels of
1607
+ ``KMeans(n_init=1)`` (scikit-learn's, so it must be installed) drawn
1608
+ from ``RandomState(random_state)``, ``"random"`` uniform draws
1609
+ normalised; with the same ``random_state`` the fit is
1610
+ scikit-learn's to rounding. One initialisation (n_init=1).
1611
+
1612
+ Attributes after fit: weights_, means_, precisions_cholesky_,
1613
+ precisions_, covariances_, converged_, n_iter_, lower_bound_.
1614
+ """
1615
+
1616
+ def __init__(self, n_components=1, *, tol=1e-3, reg_covar=1e-6, max_iter=100,
1617
+ init_params="kmeans", random_state=None):
1618
+ self.n_components, self.tol, self.reg_covar = n_components, tol, reg_covar
1619
+ self.max_iter, self.init_params, self.random_state = max_iter, init_params, random_state
1620
+
1621
+ def fit(self, X, y=None):
1622
+ X = np.ascontiguousarray(X, dtype=np.float64)
1623
+ n, p = X.shape
1624
+ k = int(self.n_components)
1625
+ rs = np.random.RandomState(self.random_state)
1626
+ if self.init_params == "kmeans":
1627
+ try:
1628
+ from sklearn.cluster import KMeans
1629
+ except ImportError as e:
1630
+ raise ImportError("init_params='kmeans' runs scikit-learn's KMeans; "
1631
+ "install scikit-learn or pass init_params='random'") from e
1632
+ labels = KMeans(n_clusters=k, n_init=1, random_state=rs).fit(X).labels_
1633
+ resp = np.zeros((n, k))
1634
+ resp[np.arange(n), labels] = 1.0
1635
+ elif self.init_params == "random":
1636
+ resp = rs.uniform(size=(n, k))
1637
+ resp /= resp.sum(axis=1)[:, None]
1638
+ else:
1639
+ raise ValueError("init_params is 'kmeans' or 'random'")
1640
+ resp = np.ascontiguousarray(resp)
1641
+ self.weights_ = np.zeros(k)
1642
+ self.means_ = np.zeros((k, p))
1643
+ self.precisions_cholesky_ = np.zeros((k, p, p))
1644
+ labels = np.zeros(n)
1645
+ out = np.zeros(2)
1646
+ it = _gpu_or_cpu(n * p, getattr(self, "backend", None)).gmm(X, resp, self.weights_, self.means_,
1647
+ self.precisions_cholesky_, labels, out,
1648
+ int(self.max_iter), float(self.tol),
1649
+ float(self.reg_covar), n, p, k)
1650
+ self.n_iter_ = it
1651
+ self.lower_bound_ = float(out[0])
1652
+ self.converged_ = bool(out[1] == 1.0)
1653
+ self.precisions_ = np.einsum("kij,klj->kil", self.precisions_cholesky_,
1654
+ self.precisions_cholesky_)
1655
+ self.covariances_ = np.linalg.inv(self.precisions_)
1656
+ return self
1657
+
1658
+ def _weighted_log_prob(self, X):
1659
+ X = np.ascontiguousarray(X, dtype=np.float64)
1660
+ n, p = X.shape
1661
+ out = np.empty((n, len(self.weights_)))
1662
+ for c, (mu, u) in enumerate(zip(self.means_, self.precisions_cholesky_)):
1663
+ y = X @ u - mu @ u
1664
+ out[:, c] = (-0.5 * (p * np.log(2 * np.pi) + (y * y).sum(axis=1))
1665
+ + np.log(np.diag(u)).sum() + np.log(self.weights_[c]))
1666
+ return out
1667
+
1668
+ def predict_proba(self, X):
1669
+ w = self._weighted_log_prob(X)
1670
+ w -= w.max(axis=1, keepdims=True)
1671
+ e = np.exp(w)
1672
+ return e / e.sum(axis=1, keepdims=True)
1673
+
1674
+ def predict(self, X):
1675
+ return np.argmax(self._weighted_log_prob(X), axis=1)
1676
+
1677
+
1678
+ class CovPCA:
1679
+ """PCA through the covariance's eigenvectors, scikit-learn's
1680
+ ``PCA(svd_solver="covariance_eigh")`` -- what its "auto" picks for a
1681
+ tall design (p <= 1000, n >= 10 p) -- translated whole
1682
+ (``examples/kernel-only/pca.scm``: X'X by dsyrk where a CBLAS links,
1683
+ the eigenproblem by Householder reduction and QL iteration in the
1684
+ subset, svd_flip's signs). Faster than scikit-learn up to p of
1685
+ about 100; at several hundred features LAPACK's blocked eigensolver
1686
+ wins.
1687
+
1688
+ Attributes after fit: components_, explained_variance_,
1689
+ explained_variance_ratio_, singular_values_, mean_, noise_variance_,
1690
+ n_components_.
1691
+ """
1692
+
1693
+ def __init__(self, n_components=None, *, backend=None):
1694
+ self.n_components, self.backend = n_components, backend
1695
+
1696
+ def fit(self, X, y=None):
1697
+ X = np.ascontiguousarray(X, dtype=np.float64)
1698
+ n, p = X.shape
1699
+ comps = np.zeros((p, p)); ev = np.zeros(p); sv = np.zeros(p); mean = np.zeros(p)
1700
+ lib = _accel(float(n) * p * p) if self.backend in (None, "cublas") else None
1701
+ (lib or _gramops_kernels()).pca(X, comps, ev, sv, mean, n, p)
1702
+ k = min(n, p) if self.n_components is None else int(self.n_components)
1703
+ self.n_components_ = k
1704
+ self.mean_ = mean
1705
+ self.components_ = comps[:k].copy()
1706
+ self.explained_variance_ = ev[:k].copy()
1707
+ self.explained_variance_ratio_ = ev[:k] / ev.sum()
1708
+ self.singular_values_ = sv[:k].copy()
1709
+ self.noise_variance_ = float(ev[k:].mean()) if k < min(n, p) else 0.0
1710
+ return self
1711
+
1712
+ def transform(self, X):
1713
+ return (np.asarray(X, dtype=np.float64) - self.mean_) @ self.components_.T
1714
+
1715
+ def fit_transform(self, X, y=None):
1716
+ return self.fit(X).transform(X)
1717
+
1718
+
1719
+ class ParallelFastICA:
1720
+ """FastICA, scikit-learn's ``FastICA`` at its defaults translated
1721
+ whole (``examples/kernel-only/fastica.scm``: algorithm="parallel",
1722
+ fun="logcosh", whiten="unit-variance", whiten_solver="svd" -- the
1723
+ singular vectors of X' taken from the eigenvectors of X'X, signed as
1724
+ scikit-learn signs them). The start is ``RandomState(random_state)
1725
+ .normal(size=(k, k))``, scikit-learn's draw, unless ``w_init`` is
1726
+ given; with the same start the fit is scikit-learn's to rounding.
1727
+
1728
+ Attributes after fit: components_, mixing_, mean_, whitening_, n_iter_.
1729
+ """
1730
+
1731
+ def __init__(self, n_components=None, *, max_iter=200, tol=1e-4, w_init=None,
1732
+ random_state=None):
1733
+ self.n_components, self.max_iter, self.tol = n_components, max_iter, tol
1734
+ self.w_init, self.random_state = w_init, random_state
1735
+
1736
+ def fit(self, X, y=None):
1737
+ X = np.ascontiguousarray(X, dtype=np.float64)
1738
+ n, p = X.shape
1739
+ k = min(n, p) if self.n_components is None else min(int(self.n_components), n, p)
1740
+ if self.w_init is None:
1741
+ w0 = np.random.RandomState(self.random_state).normal(size=(k, k))
1742
+ else:
1743
+ w0 = np.asarray(self.w_init, dtype=np.float64)
1744
+ w0 = np.ascontiguousarray(w0)
1745
+ self.components_ = np.zeros((k, p)); self.whitening_ = np.zeros((k, p))
1746
+ self._unmixing = np.zeros((k, k)); self.mean_ = np.zeros(p)
1747
+ self.n_iter_ = _gpu_or_cpu(n * p, getattr(self, "backend", None)).fastica(
1748
+ X, w0, self.components_, self.whitening_, self._unmixing, self.mean_,
1749
+ int(self.max_iter), float(self.tol), n, p, k)
1750
+ self.mixing_ = np.linalg.pinv(self.components_)
1751
+ return self
1752
+
1753
+ def transform(self, X):
1754
+ return (np.asarray(X, dtype=np.float64) - self.mean_) @ self.components_.T
1755
+
1756
+ def fit_transform(self, X, y=None):
1757
+ return self.fit(X).transform(X)
1758
+
1759
+
1760
+ class CovARDRegression:
1761
+ """Automatic relevance determination, scikit-learn's ``ARDRegression``
1762
+ for n >= p translated whole (``examples/kernel-only/ard.scm``): each
1763
+ iteration sigma = pinvh(diag(lambda) + alpha G) over the kept
1764
+ features (the eigenproblem in the subset, scipy's cutoff), then
1765
+ coef, gamma, lambda, alpha and the pruning at ``threshold_lambda``,
1766
+ stopping on the coefficients' summed change. ``hoist=True`` (the
1767
+ default) takes the kept features' Gram from X'X formed once;
1768
+ ``hoist=False`` forms it from X every iteration, as scikit-learn
1769
+ does. Same iterations and coefficients to rounding either way.
1770
+
1771
+ Attributes after fit: coef_, intercept_, alpha_, lambda_, sigma_,
1772
+ n_iter_.
1773
+ """
1774
+
1775
+ def __init__(self, *, max_iter=300, tol=1e-3, alpha_1=1e-6, alpha_2=1e-6,
1776
+ lambda_1=1e-6, lambda_2=1e-6, threshold_lambda=1e4, fit_intercept=True,
1777
+ hoist=True):
1778
+ self.max_iter, self.tol = int(max_iter), float(tol)
1779
+ self.alpha_1, self.alpha_2, self.lambda_1, self.lambda_2 = alpha_1, alpha_2, lambda_1, lambda_2
1780
+ self.threshold_lambda, self.fit_intercept, self.hoist = threshold_lambda, fit_intercept, hoist
1781
+
1782
+ def fit(self, X, y):
1783
+ Xc, yc, xm, ym = _center(X, y, self.fit_intercept)
1784
+ n, p = Xc.shape
1785
+ if n < p:
1786
+ raise ValueError("CovARDRegression is the n >= p route (_update_sigma)")
1787
+ self.coef_ = np.zeros(p)
1788
+ self.lambda_ = np.zeros(p)
1789
+ sigma = np.zeros(p * p)
1790
+ out = np.zeros(2)
1791
+ self.n_iter_ = _gramops_kernels().ard(
1792
+ Xc, np.ascontiguousarray(yc), self.coef_, sigma, self.lambda_, out,
1793
+ self.max_iter, self.tol, float(self.alpha_1), float(self.alpha_2),
1794
+ float(self.lambda_1), float(self.lambda_2), float(self.threshold_lambda),
1795
+ 1 if self.hoist else 0, n, p)
1796
+ k = int(out[1])
1797
+ self.alpha_ = float(out[0])
1798
+ self.sigma_ = sigma[:k * k].reshape(k, k).copy()
1799
+ self.intercept_ = ym - float(xm @ self.coef_)
1800
+ return self
1801
+
1802
+ def predict(self, X):
1803
+ return np.ascontiguousarray(X, dtype=np.float64) @ self.coef_ + self.intercept_
1804
+
1805
+
1806
+ def _gpu_or_cpu(size, backend=None):
1807
+ """The --cuda translation (kernels and device-resident cuBLAS) when it
1808
+ was built, a device answers, the backend allows it and the problem has
1809
+ at least a million entries; the CPU library otherwise."""
1810
+ import os
1811
+ from ._libfind import gpu_built
1812
+ if (backend in (None, "cuda") and size >= 1e6 and not os.environ.get("SCM2CPP_NO_DEVICE")
1813
+ and gpu_built() and cuda_available()):
1814
+ import importlib
1815
+ return importlib.import_module(f"{__name__}._generated._loader_gpu_cuda")
1816
+ return _gramops_kernels()
1817
+
1818
+
1819
+ def _accel(work):
1820
+ """The --cublas build of the selection and Gram products, when it was
1821
+ built, a device answers, and WORK (flops) is large enough to pay for
1822
+ the transfer; else None."""
1823
+ import os
1824
+ from ._libfind import accel_built
1825
+ if (work < 2e9 or os.environ.get("SCM2CPP_NO_DEVICE") or not accel_built()
1826
+ or not cuda_available()):
1827
+ return None
1828
+ import importlib
1829
+ return importlib.import_module(f"{__name__}._generated._loader_accel_cublas")
1830
+
1831
+
1832
+ def _gram(Xc, backend=None):
1833
+ """X'X: on the device for a large product when one answers, else numpy."""
1834
+ n, p = Xc.shape
1835
+ lib = _accel(float(n) * p * p) if backend in (None, "cublas") else None
1836
+ if lib is None:
1837
+ return np.ascontiguousarray(Xc.T @ Xc)
1838
+ G = np.zeros((p, p))
1839
+ lib.gram_xx(np.ascontiguousarray(Xc), G, n, p)
1840
+ return G
1841
+
1842
+
1843
+ def _cpu_select_lib():
1844
+ return _gramops_kernels()
1845
+
1846
+
1847
+ def f_regression(X, y, *, center=True, backend=None):
1848
+ """scikit-learn's ``f_regression`` translated (``fscore.scm``): F and
1849
+ p-values of each feature against y. ``y`` may also be n x q, the q
1850
+ targets in one call (one product X'Y, on the device when one answers
1851
+ and the product is large) -- what a loop of scikit-learn's calls
1852
+ gives, column by column. Infinite F (perfect correlation) becomes
1853
+ the largest double and NaN F zero with p-value 1, as
1854
+ force_finite=True has it. p-values need scipy (else None)."""
1855
+ X = np.ascontiguousarray(X, dtype=np.float64)
1856
+ Y = np.asarray(y, dtype=np.float64)
1857
+ one = Y.ndim == 1
1858
+ Y = np.ascontiguousarray(Y.reshape(len(Y), -1))
1859
+ n, p = X.shape
1860
+ q = Y.shape[1]
1861
+ lib = _accel(float(n) * p * q) if backend in (None, "cublas") else None
1862
+ if lib is None:
1863
+ lib = _cpu_select_lib()
1864
+ r = np.zeros((p, q))
1865
+ F = np.zeros((p, q))
1866
+ with np.errstate(divide="ignore", invalid="ignore"):
1867
+ lib.f_regression(X, Y, r, F, 1 if center else 0, n, p, q)
1868
+ dof = n - (2 if center else 1)
1869
+ try:
1870
+ from scipy import stats
1871
+ pv = stats.f.sf(F, 1, dof)
1872
+ except ImportError:
1873
+ pv = None
1874
+ inf = np.isinf(F)
1875
+ F[inf] = np.finfo(np.float64).max
1876
+ nan = np.isnan(F)
1877
+ F[nan] = 0.0
1878
+ if pv is not None:
1879
+ pv[nan] = 1.0
1880
+ if one:
1881
+ return F[:, 0], (None if pv is None else pv[:, 0])
1882
+ return F, pv
1883
+
1884
+
1885
+ def f_classif(X, y):
1886
+ """scikit-learn's ``f_classif`` translated (``fscore.scm``: f_oneway's
1887
+ sums of squares, the class sums one product). p-values need scipy."""
1888
+ X = np.ascontiguousarray(X, dtype=np.float64)
1889
+ classes, lab = np.unique(np.asarray(y), return_inverse=True)
1890
+ n, p = X.shape
1891
+ k = len(classes)
1892
+ F = np.zeros(p)
1893
+ with np.errstate(divide="ignore", invalid="ignore"):
1894
+ _cpu_select_lib().f_classif(X, lab.astype(np.int32), F, n, p, k)
1895
+ try:
1896
+ from scipy import special
1897
+ pv = special.fdtrc(k - 1, n - k, F)
1898
+ except ImportError:
1899
+ pv = None
1900
+ return F, pv
1901
+
1902
+
1903
+ def _clean_nans(scores):
1904
+ scores = np.array(scores, dtype=np.float64, copy=True)
1905
+ scores[np.isnan(scores)] = np.finfo(np.float64).min
1906
+ return scores
1907
+
1908
+
1909
+ class _SelectBase:
1910
+ def fit(self, X, y):
1911
+ sf = self.score_func
1912
+ fn = {"f_classif": f_classif, "f_regression": f_regression}.get(sf, sf)
1913
+ self.scores_, self.pvalues_ = fn(X, y)
1914
+ return self
1915
+
1916
+ def transform(self, X):
1917
+ return np.asarray(X)[:, self.get_support()]
1918
+
1919
+ def fit_transform(self, X, y):
1920
+ return self.fit(X, y).transform(X)
1921
+
1922
+
1923
+ class SelectKBest(_SelectBase):
1924
+ """scikit-learn's ``SelectKBest`` over the translated F statistics
1925
+ (``score_func`` "f_classif", "f_regression", or one of this module's
1926
+ functions): the k best scores by a stable sort, NaN scores last.
1927
+
1928
+ Attributes after fit: scores_, pvalues_.
1929
+ """
1930
+
1931
+ def __init__(self, score_func="f_classif", *, k=10):
1932
+ self.score_func, self.k = score_func, k
1933
+
1934
+ def get_support(self):
1935
+ if self.k == "all":
1936
+ return np.ones(len(self.scores_), dtype=bool)
1937
+ mask = np.zeros(len(self.scores_), dtype=bool)
1938
+ if self.k == 0:
1939
+ return mask
1940
+ mask[np.argsort(_clean_nans(self.scores_), kind="mergesort")[-self.k:]] = True
1941
+ return mask
1942
+
1943
+
1944
+ class SelectPercentile(_SelectBase):
1945
+ """scikit-learn's ``SelectPercentile`` over the translated F
1946
+ statistics: scores above the (100 - percentile)th percentile, ties
1947
+ at it kept in order up to the percentile's count.
1948
+
1949
+ Attributes after fit: scores_, pvalues_.
1950
+ """
1951
+
1952
+ def __init__(self, score_func="f_classif", *, percentile=10):
1953
+ self.score_func, self.percentile = score_func, percentile
1954
+
1955
+ def get_support(self):
1956
+ if self.percentile == 100:
1957
+ return np.ones(len(self.scores_), dtype=bool)
1958
+ if self.percentile == 0:
1959
+ return np.zeros(len(self.scores_), dtype=bool)
1960
+ scores = _clean_nans(self.scores_)
1961
+ threshold = np.percentile(scores, 100 - self.percentile)
1962
+ mask = scores > threshold
1963
+ ties = np.where(scores == threshold)[0]
1964
+ if len(ties):
1965
+ max_feats = int(len(scores) * self.percentile / 100)
1966
+ mask[ties[: max_feats - mask.sum()]] = True
1967
+ return mask
1968
+
1969
+
1970
+ class GramRFE:
1971
+ """scikit-learn's ``RFE`` around ``Ridge(alpha)`` (``alpha=0``:
1972
+ ``LinearRegression()``) translated whole (``rfe.scm``): the centred
1973
+ Gram formed once (on the device for a large one when one answers),
1974
+ each round's fit from its Cholesky factor with the dropped features
1975
+ taken out by plane rotations, the features ranked by coef^2 in a
1976
+ stable order. scikit-learn refits from X every round; the ranking
1977
+ and the final coefficients are the same to rounding.
1978
+
1979
+ Attributes after fit: support_, ranking_, n_features_, coef_,
1980
+ intercept_.
1981
+ """
1982
+
1983
+ def __init__(self, alpha=1.0, *, n_features_to_select=None, step=1, backend=None):
1984
+ self.alpha, self.n_features_to_select, self.step = alpha, n_features_to_select, step
1985
+ self.backend = backend
1986
+
1987
+ def fit(self, X, y):
1988
+ X = np.ascontiguousarray(X, dtype=np.float64)
1989
+ y = np.ascontiguousarray(y, dtype=np.float64)
1990
+ n, p = X.shape
1991
+ k = self.n_features_to_select
1992
+ nsel = p // 2 if k is None else (int(k) if isinstance(k, (int, np.integer)) else int(p * k))
1993
+ step = int(max(1, self.step * p)) if 0.0 < self.step < 1.0 else int(self.step)
1994
+ lib = _accel(float(n) * p * p) if self.backend in (None, "cublas") else None
1995
+ if lib is None:
1996
+ lib = _cpu_select_lib()
1997
+ coef = np.zeros(p)
1998
+ support = np.zeros(p, dtype=np.int32)
1999
+ ranking = np.zeros(p, dtype=np.int32)
2000
+ lib.rfe(X, y, coef, support, ranking, float(self.alpha), nsel, step, n, p)
2001
+ self.support_ = support.astype(bool)
2002
+ self.ranking_ = ranking.astype(int)
2003
+ self.n_features_ = int(self.support_.sum())
2004
+ self.coef_ = coef[self.support_].copy()
2005
+ self.intercept_ = float(y.mean() - X.mean(axis=0)[self.support_] @ self.coef_)
2006
+ return self
2007
+
2008
+ def transform(self, X):
2009
+ return np.asarray(X)[:, self.support_]
2010
+
2011
+ def predict(self, X):
2012
+ return self.transform(np.asarray(X, dtype=np.float64)) @ self.coef_ + self.intercept_
2013
+
2014
+
2015
+ class _GramLarsBase:
2016
+ def _fit_lars(self, X, y, method, alpha, max_iter):
2017
+ Xc, yc, xm, ym = _center(X, y, self.fit_intercept)
2018
+ n, p = Xc.shape
2019
+ G = _gram(Xc)
2020
+ cov = Xc.T @ yc
2021
+ self.coef_ = np.zeros(p)
2022
+ self.n_iter_ = _classifier_kernels().lars(
2023
+ G, cov, self.coef_, np.zeros(1), np.zeros(1), 0, float(n), float(alpha), method,
2024
+ int(max_iter), float(np.finfo(np.float64).eps), p)
2025
+ self.intercept_ = ym - float(xm @ self.coef_)
2026
+ return self
2027
+
2028
+ def predict(self, X):
2029
+ return np.ascontiguousarray(X, dtype=np.float64) @ self.coef_ + self.intercept_
2030
+
2031
+
2032
+ class GramLars(_GramLarsBase):
2033
+ """Least angle regression, scikit-learn's _lars_path_solver with a
2034
+ Gram translated (``examples/kernel-only/lars.scm``, method "lar").
2035
+ ``Lars`` takes the Gram route for n > p (precompute="auto"); here it
2036
+ is taken always, the Gram X'X formed in numpy. ``n_nonzero_coefs``
2037
+ defaults to 500, as there.
2038
+
2039
+ Attributes after fit: coef_, intercept_, n_iter_.
2040
+ """
2041
+
2042
+ def __init__(self, *, n_nonzero_coefs=500, fit_intercept=True):
2043
+ self.n_nonzero_coefs, self.fit_intercept = int(n_nonzero_coefs), fit_intercept
2044
+
2045
+ def fit(self, X, y):
2046
+ return self._fit_lars(X, y, 0, 0.0, self.n_nonzero_coefs)
2047
+
2048
+
2049
+ class GramLassoLars(_GramLarsBase):
2050
+ """The lasso by least angle regression, scikit-learn's
2051
+ _lars_path_solver with a Gram translated (method "lasso": variables
2052
+ leave the active set when their coefficient crosses zero, the
2053
+ Cholesky factor downdated by Givens rotations). ``LassoLars`` takes
2054
+ the Gram route for n > p; here it is taken always. ``alpha``
2055
+ defaults to 1.0 and ``max_iter`` to 500, as there.
2056
+
2057
+ Attributes after fit: coef_, intercept_, n_iter_.
2058
+ """
2059
+
2060
+ def __init__(self, alpha=1.0, *, max_iter=500, fit_intercept=True):
2061
+ self.alpha, self.max_iter, self.fit_intercept = float(alpha), int(max_iter), fit_intercept
2062
+
2063
+ def fit(self, X, y):
2064
+ return self._fit_lars(X, y, 1, self.alpha, self.max_iter)
2065
+
2066
+
2067
+ class _GramLarsCVBase:
2068
+ """LarsCV / LassoLarsCV: KFold(cv) without shuffling; on each fold
2069
+ the whole path by the translated LARS over the training fold's Gram
2070
+ (centred on the training means), the test residuals interpolated
2071
+ onto the union of the folds' alphas as scikit-learn interpolates
2072
+ them (the path's coefficients interpolated, the residuals being
2073
+ affine in them, the mean square from the test fold's Gram); the
2074
+ alpha of the least mean square error, refitted on all the data."""
2075
+
2076
+ def __init__(self, *, fit_intercept=True, max_iter=500, cv=5, max_n_alphas=1000):
2077
+ self.fit_intercept, self.max_iter, self.cv = fit_intercept, int(max_iter), cv
2078
+ self.max_n_alphas = max_n_alphas
2079
+
2080
+ def _lars(self, Xc, yc, alpha_min, rec):
2081
+ n, p = Xc.shape
2082
+ G = _gram(Xc)
2083
+ cov = Xc.T @ yc
2084
+ coef = np.zeros(p)
2085
+ path = np.zeros((self.max_iter + 1, p)) if rec else np.zeros(1)
2086
+ alphas = np.zeros(self.max_iter + 1) if rec else np.zeros(1)
2087
+ it = _classifier_kernels().lars(G, cov, coef, path, alphas, 1 if rec else 0, float(n),
2088
+ float(alpha_min), self._method, self.max_iter,
2089
+ float(np.finfo(np.float64).eps), p)
2090
+ return it, coef, (path[:it + 1] if rec else None), (alphas[:it + 1] if rec else None)
2091
+
2092
+ def fit(self, X, y):
2093
+ X = np.ascontiguousarray(X, dtype=np.float64)
2094
+ y = np.ascontiguousarray(y, dtype=np.float64)
2095
+ n, p = X.shape
2096
+ folds = []
2097
+ for train, test in _kfold(n, int(self.cv)):
2098
+ Xtr, ytr, Xte, yte = X[train], y[train], X[test], y[test]
2099
+ if self.fit_intercept:
2100
+ xm, ym = Xtr.mean(axis=0), ytr.mean()
2101
+ Xtr, Xte, ytr, yte = Xtr - xm, Xte - xm, ytr - ym, yte - ym
2102
+ _, _, path, alphas = self._lars(np.ascontiguousarray(Xtr), ytr, 0.0, True)
2103
+ folds.append((alphas, path, _gram(Xte), Xte.T @ yte, float(yte @ yte), len(test)))
2104
+ all_alphas = np.unique(np.concatenate([f[0] for f in folds]))
2105
+ stride = int(max(1, int(len(all_alphas) / float(self.max_n_alphas))))
2106
+ all_alphas = all_alphas[::stride]
2107
+ mse = np.empty((len(all_alphas), len(folds)))
2108
+ for j, (alphas, path, Gt, cyt, yy, nt) in enumerate(folds):
2109
+ a, c = alphas[::-1], path[::-1]
2110
+ if a[0] != 0:
2111
+ a, c = np.r_[0, a], np.r_[c[:1], c]
2112
+ if a[-1] != all_alphas[-1]:
2113
+ a, c = np.r_[a, all_alphas[-1]], np.r_[c, c[-1:]]
2114
+ ci = np.column_stack([np.interp(all_alphas, a, c[:, q]) for q in range(p)])
2115
+ mse[:, j] = (((ci @ Gt) * ci).sum(axis=1) - 2.0 * ci @ cyt + yy) / nt
2116
+ mask = np.all(np.isfinite(mse), axis=-1)
2117
+ all_alphas, mse = all_alphas[mask], mse[mask]
2118
+ self.alpha_ = float(all_alphas[int(np.argmin(mse.mean(axis=-1)))])
2119
+ self.cv_alphas_, self.mse_path_ = all_alphas, mse
2120
+ Xc, yc, xm, ym = _center(X, y, self.fit_intercept)
2121
+ self.n_iter_, self.coef_, _, _ = self._lars(Xc, yc, self.alpha_, False)
2122
+ self.intercept_ = ym - float(xm @ self.coef_)
2123
+ return self
2124
+
2125
+ def predict(self, X):
2126
+ return np.ascontiguousarray(X, dtype=np.float64) @ self.coef_ + self.intercept_
2127
+
2128
+
2129
+ class GramLarsCV(_GramLarsCVBase):
2130
+ """scikit-learn's ``LarsCV`` (method "lar") over the translated LARS;
2131
+ see _GramLarsCVBase. Attributes after fit: alpha_, coef_, intercept_,
2132
+ cv_alphas_, mse_path_, n_iter_."""
2133
+ _method = 0
2134
+
2135
+
2136
+ class GramLassoLarsCV(_GramLarsCVBase):
2137
+ """scikit-learn's ``LassoLarsCV`` (method "lasso") over the translated
2138
+ LARS; see _GramLarsCVBase. Attributes after fit: alpha_, coef_,
2139
+ intercept_, cv_alphas_, mse_path_, n_iter_."""
2140
+ _method = 1
2141
+
2142
+
2143
+ class GramOMPCV:
2144
+ """scikit-learn's ``OrthogonalMatchingPursuitCV``: KFold(cv) without
2145
+ shuffling; on each fold the path of the translated OMP (``omp.scm``,
2146
+ the path recorded) up to ``max_iter`` columns on the centred training
2147
+ fold, the test fold's mean square error after each column, the number
2148
+ of columns that minimises the folds' mean (over the shortest path),
2149
+ refitted as GramOMP. ``max_iter`` defaults, as there, to
2150
+ min(max(10% of p, 5), p).
2151
+
2152
+ Attributes after fit: n_nonzero_coefs_, coef_, intercept_, n_iter_.
2153
+ """
2154
+
2155
+ def __init__(self, *, fit_intercept=True, max_iter=None, cv=5):
2156
+ self.fit_intercept, self.max_iter, self.cv = fit_intercept, max_iter, cv
2157
+
2158
+ def fit(self, X, y):
2159
+ X = np.ascontiguousarray(X, dtype=np.float64)
2160
+ y = np.ascontiguousarray(y, dtype=np.float64)
2161
+ n, p = X.shape
2162
+ kmax = min(max(int(0.1 * p), 5), p) if self.max_iter is None else int(self.max_iter)
2163
+ lib = _gramops_kernels()
2164
+ mses = []
2165
+ for train, test in _kfold(n, int(self.cv)):
2166
+ Xtr, ytr, Xte, yte = X[train], y[train], X[test], y[test]
2167
+ if self.fit_intercept:
2168
+ xm, ym = Xtr.mean(axis=0), ytr.mean()
2169
+ Xtr, Xte, ytr, yte = Xtr - xm, Xte - xm, ytr - ym, yte - ym
2170
+ path = np.zeros((kmax, p))
2171
+ k = lib.omp(np.ascontiguousarray(Xtr), np.ascontiguousarray(ytr), np.zeros(p),
2172
+ path, 1, kmax, len(train), p)
2173
+ res = Xte @ path[:k].T - yte[:, None]
2174
+ mses.append((res ** 2).mean(axis=0))
2175
+ m = min(len(f) for f in mses)
2176
+ best = int(np.argmin(np.mean([f[:m] for f in mses], axis=0))) + 1
2177
+ self.n_nonzero_coefs_ = best
2178
+ Xc, yc, xm, ym = _center(X, y, self.fit_intercept)
2179
+ self.coef_ = np.zeros(p)
2180
+ self.n_iter_ = lib.omp(Xc, yc, self.coef_, np.zeros(1), 0, best, n, p)
2181
+ self.intercept_ = ym - float(xm @ self.coef_)
2182
+ return self
2183
+
2184
+ def predict(self, X):
2185
+ return np.ascontiguousarray(X, dtype=np.float64) @ self.coef_ + self.intercept_
2186
+
2187
+
2188
+ class GramLassoLarsIC:
2189
+ """LassoLars with the penalty chosen by AIC or BIC, scikit-learn's
2190
+ ``LassoLarsIC``: the whole "lasso" path by the translated
2191
+ _lars_path_solver (``examples/kernel-only/lars.scm``, the path
2192
+ recorded as lars_path(return_path=True) returns it), then
2193
+ scikit-learn's criterion n log(2 pi s^2) + RSS / s^2 + K df at
2194
+ each knot, s^2 the OLS noise variance RSS_ols / (n - p - 1). The
2195
+ residual sums are taken from the Gram (|y - X c|^2 = y'y - 2 c'X'y
2196
+ + c'X'Xc, the OLS fit by its Cholesky factor) rather than from X
2197
+ at every knot as scikit-learn takes them -- the same numbers to
2198
+ rounding, without the n x knots product. n > p + 1, as there.
2199
+
2200
+ Attributes after fit: alpha_, coef_, intercept_, alphas_, criterion_,
2201
+ noise_variance_, n_iter_.
2202
+ """
2203
+
2204
+ def __init__(self, criterion="aic", *, fit_intercept=True, max_iter=500,
2205
+ noise_variance=None):
2206
+ self.criterion, self.fit_intercept, self.max_iter = criterion, fit_intercept, int(max_iter)
2207
+ self.noise_variance = noise_variance
2208
+
2209
+ def fit(self, X, y):
2210
+ Xc, yc, xm, ym = _center(X, y, self.fit_intercept)
2211
+ n, p = Xc.shape
2212
+ G0 = _gram(Xc)
2213
+ cov0 = Xc.T @ yc
2214
+ yy = float(yc @ yc)
2215
+ path = np.zeros((self.max_iter + 1, p))
2216
+ alphas = np.zeros(self.max_iter + 1)
2217
+ coef = np.zeros(p)
2218
+ it = _classifier_kernels().lars(
2219
+ G0.copy(), cov0.copy(), coef, path, alphas, 1, float(n), 0.0, 1, self.max_iter,
2220
+ float(np.finfo(np.float64).eps), p)
2221
+ cp, al = path[:it + 1], alphas[:it + 1]
2222
+ rss = yy - 2.0 * (cp @ cov0) + ((cp @ G0) * cp).sum(axis=1)
2223
+ df = (np.abs(cp) > np.finfo(np.float64).eps).sum(axis=1)
2224
+ if self.noise_variance is None:
2225
+ if n <= p + int(self.fit_intercept):
2226
+ raise ValueError("n must exceed p (+1 with an intercept) to estimate the "
2227
+ "noise variance; pass noise_variance")
2228
+ L = np.linalg.cholesky(G0)
2229
+ beta = np.linalg.solve(L.T, np.linalg.solve(L, cov0))
2230
+ self.noise_variance_ = (yy - 2.0 * beta @ cov0 + beta @ G0 @ beta) / (
2231
+ n - p - int(self.fit_intercept))
2232
+ else:
2233
+ self.noise_variance_ = float(self.noise_variance)
2234
+ factor = {"aic": 2.0, "bic": np.log(n)}[self.criterion]
2235
+ self.criterion_ = (n * np.log(2 * np.pi * self.noise_variance_)
2236
+ + rss / self.noise_variance_ + factor * df)
2237
+ best = int(np.argmin(self.criterion_))
2238
+ self.alphas_, self.n_iter_ = al, it
2239
+ self.alpha_ = float(al[best])
2240
+ self.coef_ = cp[best].copy()
2241
+ self.intercept_ = ym - float(xm @ self.coef_)
2242
+ return self
2243
+
2244
+ def predict(self, X):
2245
+ return np.ascontiguousarray(X, dtype=np.float64) @ self.coef_ + self.intercept_
2246
+
2247
+
2248
+ class WeiszfeldTheilSen:
2249
+ """scikit-learn's ``TheilSenRegressor`` translated whole
2250
+ (``examples/kernel-only/theilsen.scm``): the least squares fit of
2251
+ every subset of n_subsamples rows (p + 1 by default: each an exact
2252
+ solve, the minimum-norm fit for a singular one, as gelss gives it)
2253
+ and the spatial median of the fits by the modified Weiszfeld
2254
+ iteration. The subsets are scikit-learn's: all the combinations
2255
+ when there are at most ``max_subpopulation``, else
2256
+ ``RandomState(random_state).choice`` draws, made here the same way
2257
+ (and on large problems that drawing is most of the time, on both
2258
+ sides).
2259
+
2260
+ Attributes after fit: coef_, intercept_, n_iter_, n_subpopulation_,
2261
+ breakdown_.
2262
+ """
2263
+
2264
+ def __init__(self, *, fit_intercept=True, max_subpopulation=1e4, n_subsamples=None,
2265
+ max_iter=300, tol=1e-3, random_state=None):
2266
+ self.fit_intercept, self.max_subpopulation = fit_intercept, max_subpopulation
2267
+ self.n_subsamples, self.max_iter, self.tol = n_subsamples, max_iter, tol
2268
+ self.random_state = random_state
2269
+
2270
+ def fit(self, X, y):
2271
+ from itertools import combinations
2272
+ from math import comb
2273
+ X = np.ascontiguousarray(X, dtype=np.float64)
2274
+ y = np.ascontiguousarray(y, dtype=np.float64)
2275
+ n, p = X.shape
2276
+ fi = 1 if self.fit_intercept else 0
2277
+ ns = min(p + fi, n) if self.n_subsamples is None else int(self.n_subsamples)
2278
+ total = max(1, comb(n, ns))
2279
+ self.n_subpopulation_ = int(min(self.max_subpopulation, total))
2280
+ self.breakdown_ = 1 - (0.5 ** (1 / ns) * (n - ns + 1) + ns - 1) / n
2281
+ if total <= self.max_subpopulation:
2282
+ idx = np.array(list(combinations(range(n), ns)), dtype=np.int32)
2283
+ else:
2284
+ rs = np.random.RandomState(self.random_state)
2285
+ idx = np.array([rs.choice(n, size=ns, replace=False)
2286
+ for _ in range(self.n_subpopulation_)], dtype=np.int32)
2287
+ nsub = len(idx)
2288
+ w = np.zeros((nsub, p + fi))
2289
+ c = np.zeros(p + fi)
2290
+ self.n_iter_ = _classifier_kernels().theilsen(X, y, idx, w, c, nsub, ns, fi,
2291
+ int(self.max_iter), float(self.tol), p)
2292
+ self.intercept_ = float(c[0]) if fi else 0.0
2293
+ self.coef_ = c[fi:].copy()
2294
+ return self
2295
+
2296
+ def predict(self, X):
2297
+ return np.ascontiguousarray(X, dtype=np.float64) @ self.coef_ + self.intercept_
2298
+
2299
+
2300
+ class L1LinearSVM:
2301
+ """The L1-regularized L2-loss linear SVM by liblinear's own
2302
+ algorithm: ``examples/kernel-only/svm-l1.scm`` translated plainly,
2303
+ liblinear's solve_l1r_l2_svc (coordinate descent with a Newton step,
2304
+ a line search and shrinking) -- what scikit-learn's
2305
+ ``LinearSVC(penalty="l1", dual=False)`` runs, on
2306
+
2307
+ min_w sum_j |w_j| + C sum_i max(0, 1 - y_i w.x_i)^2,
2308
+
2309
+ the intercept a ones column (``intercept_scaling``) penalised with
2310
+ the rest, as liblinear has it. The coordinate order is random from
2311
+ a different generator than liblinear's, so the fit agrees with
2312
+ scikit-learn's to its tolerance. Two classes.
2313
+
2314
+ Attributes after fit: classes_, coef_, intercept_, n_iter_.
2315
+ """
2316
+
2317
+ def __init__(self, C=1.0, tol=1e-4, max_iter=1000, fit_intercept=True,
2318
+ intercept_scaling=1.0, random_state=0):
2319
+ self.C, self.tol, self.max_iter = float(C), float(tol), int(max_iter)
2320
+ self.fit_intercept, self.intercept_scaling = fit_intercept, float(intercept_scaling)
2321
+ self.random_state = random_state
2322
+
2323
+ def fit(self, X, y):
2324
+ X = np.asarray(X, dtype=np.float64)
2325
+ n, p = X.shape
2326
+ self.classes_, ys = _binary_labels(y)
2327
+ if self.fit_intercept:
2328
+ X = np.hstack([X, np.full((n, 1), self.intercept_scaling)])
2329
+ q = X.shape[1]
2330
+ w = np.zeros(q)
2331
+ # liblinear walks the design by columns
2332
+ self.n_iter_ = _classifier_kernels().svm_l1(
2333
+ np.ascontiguousarray(X.T), ys, w, self.C, self.tol, self.max_iter,
2334
+ _seed(self.random_state), n, q)
2335
+ self.coef_ = w[:p].copy()
2336
+ self.intercept_ = float(w[p] * self.intercept_scaling) if self.fit_intercept else 0.0
2337
+ return self
2338
+
2339
+ def decision_function(self, X):
2340
+ return np.ascontiguousarray(X, dtype=np.float64) @ self.coef_ + self.intercept_
2341
+
2342
+ def predict(self, X):
2343
+ return self.classes_[(self.decision_function(X) > 0).astype(int)]
2344
+
2345
+
2346
+ class L1LogisticRegression:
2347
+ """L1-regularized logistic regression by liblinear's own algorithm.
2348
+
2349
+ ``examples/kernel-only/logistic-l1.scm`` translated plainly:
2350
+ liblinear's solve_l1r_lr (newGLMNET: Newton iterations over a
2351
+ coordinate-descent subproblem with random order and shrinking, and
2352
+ a line search) -- what scikit-learn's
2353
+ ``LogisticRegression(l1_ratio=1, solver="liblinear",
2354
+ fit_intercept=False)`` runs, on the same problem
2355
+
2356
+ min_w sum_j |w_j| + C sum_i log(1 + exp(-y_i w.x_i)).
2357
+
2358
+ ``tol`` and ``max_iter`` are LogisticRegression's (the tolerance is
2359
+ scaled as liblinear scales it; the cap counts Newton iterations).
2360
+ No intercept; two classes. ``CovLogistic`` solves the same problem
2361
+ by majorization over the Gram matrix instead.
2362
+
2363
+ Attributes after fit: classes_, coef_, n_iter_.
2364
+ """
2365
+
2366
+ def __init__(self, C=1.0, tol=1e-4, max_iter=100, random_state=0):
2367
+ self.C, self.tol, self.max_iter = float(C), float(tol), int(max_iter)
2368
+ self.random_state = random_state
2369
+
2370
+ def fit(self, X, y):
2371
+ X = np.ascontiguousarray(X, dtype=np.float64)
2372
+ n, p = X.shape
2373
+ self.classes_, ys = _binary_labels(y)
2374
+ w = np.zeros(p)
2375
+ # liblinear walks the design by columns
2376
+ self.n_iter_ = _classifier_kernels().logistic_l1(
2377
+ np.ascontiguousarray(X.T), ys, w, self.C, self.tol, self.max_iter,
2378
+ _seed(self.random_state), n, p)
2379
+ self.coef_ = w
2380
+ return self
2381
+
2382
+ def decision_function(self, X):
2383
+ return np.ascontiguousarray(X, dtype=np.float64) @ self.coef_
2384
+
2385
+ def predict_proba(self, X):
2386
+ p1 = 1.0 / (1.0 + np.exp(-self.decision_function(X)))
2387
+ return np.column_stack([1.0 - p1, p1])
2388
+
2389
+ def predict(self, X):
2390
+ return self.classes_[(self.decision_function(X) > 0).astype(int)]
2391
+
2392
+
2393
+ class CovLogistic:
2394
+ """L1-penalized logistic regression, by majorization over the Gram.
2395
+
2396
+ The objective is
2397
+
2398
+ (1/n) sum_i [log(1 + exp(eta_i)) - y_i eta_i] + lam ||b||_1
2399
+
2400
+ with eta = X b and y in {0,1} -- scikit-learn's ``LogisticRegression``
2401
+ with ``penalty="l1"``, ``fit_intercept=False`` and ``C = 1/(n lam)``.
2402
+
2403
+ The logistic Hessian is bounded by X'X / 4 (Bohning's bound), so
2404
+ instead of reweighting the Gram matrix every round -- which would
2405
+ cost O(n p^2) each time and break the design-free construction --
2406
+ the quadratic term is fixed at G/4 once and only the gradient
2407
+ moves. Each outer round costs one pass for eta and one for
2408
+ X'(y - mu), then hands the majorizer to the same coordinate
2409
+ descent the lasso uses: warm-started, exactly resumable, and with
2410
+ the pleasant identity that the working correlations are exactly
2411
+ X'(y - mu). Majorization descends monotonically to the optimum
2412
+ of this convex problem.
2413
+ """
2414
+
2415
+ def __init__(self, X, y):
2416
+ X = np.ascontiguousarray(X, dtype=np.float64)
2417
+ y = np.ascontiguousarray(y, dtype=np.float64)
2418
+ if X.ndim != 2 or y.ndim != 1 or X.shape[0] != y.size:
2419
+ raise ValueError(
2420
+ f"X is {X.shape} and y is {y.shape}; expected (n, p) "
2421
+ "and (n,)")
2422
+ if not np.all((y == 0.0) | (y == 1.0)):
2423
+ raise ValueError("y must be 0/1")
2424
+ self.X, self.y = X, y
2425
+ self.nobs, self.p = X.shape
2426
+ self.g4 = np.ascontiguousarray((X.T @ X).ravel() / 4.0)
2427
+
2428
+ def _grad_corr(self, beta):
2429
+ mu = 1.0 / (1.0 + np.exp(-(self.X @ beta)))
2430
+ return self.X.T @ (self.y - mu)
2431
+
2432
+ def lambda_max(self):
2433
+ """The smallest penalty keeping every coefficient at zero.
2434
+
2435
+ At beta = 0 the gradient is X'(y - 1/2); the penalty must beat
2436
+ its largest entry, scaled as the objective is.
2437
+ """
2438
+ return float(np.max(np.abs(self.X.T @ (self.y - 0.5)))) / self.nobs
2439
+
2440
+ def lambda_grid(self, num=50, eps=1e-2):
2441
+ """A log-spaced descending path from lambda_max."""
2442
+ return self.lambda_max() * np.logspace(0, np.log10(eps), int(num))
2443
+
2444
+ def fit_path(self, lambdas, tol=1e-7, inner_sweeps=200,
2445
+ max_rounds=500):
2446
+ """Coefficients per lambda, warm-started down the path."""
2447
+ lambdas = np.asarray(lambdas, dtype=np.float64)
2448
+ beta = np.zeros(self.p)
2449
+ out = np.empty((lambdas.size, self.p))
2450
+ for i, lam in enumerate(lambdas):
2451
+ beta = self._fit_one(beta, float(lam), tol, inner_sweeps,
2452
+ max_rounds)
2453
+ out[i] = beta
2454
+ return out
2455
+
2456
+ def fit(self, lam, **kw):
2457
+ """Coefficients at one lambda, from zero."""
2458
+ return self._fit_one(np.zeros(self.p), float(lam),
2459
+ kw.get("tol", 1e-7),
2460
+ kw.get("inner_sweeps", 200),
2461
+ kw.get("max_rounds", 500))
2462
+
2463
+ def _fit_one(self, beta, lam, tol, inner_sweeps, max_rounds):
2464
+ beta = beta.copy()
2465
+ for _ in range(max_rounds):
2466
+ prev = beta.copy()
2467
+ # the majorizer at beta: quadratic G/4, working
2468
+ # correlations exactly X'(y - mu)
2469
+ c = np.ascontiguousarray(self._grad_corr(beta))
2470
+ kernel.cov_descend(self.g4, c, beta, lam, inner_sweeps,
2471
+ float(self.nobs), self.p)
2472
+ if np.max(np.abs(beta - prev)) < tol * max(
2473
+ 1.0, float(np.max(np.abs(beta)))):
2474
+ break
2475
+ return beta
2476
+
2477
+
2478
+ class CovGroupLasso:
2479
+ """Group lasso over a design given by its Gram matrix.
2480
+
2481
+ The objective groups the penalty:
2482
+
2483
+ (1/2n) ||y - X b||^2 + lam * sum_g w_g ||b_g||_2
2484
+
2485
+ with the usual weights w_g = sqrt(|g|), so whole groups enter or
2486
+ leave together. Block coordinate descent runs on the Gram matrix
2487
+ exactly as the lasso's coordinate descent does -- the working
2488
+ correlations c = X'y - G b are maintained across visits -- and
2489
+ each block visit is one majorized proximal step: the group's Gram
2490
+ block is dominated by its top eigenvalue L_g (found once), and the
2491
+ resulting subproblem has the group soft-threshold in closed form.
2492
+ Majorization descends monotonically; a group of size one reduces
2493
+ exactly to the lasso's coordinate update.
2494
+
2495
+ ``groups`` is a list of index arrays, a partition of 0..p-1.
2496
+ """
2497
+
2498
+ def __init__(self, X=None, y=None, *, gram=None, corr=None,
2499
+ nobs=None, groups=None):
2500
+ if gram is not None:
2501
+ if corr is None or nobs is None:
2502
+ raise ValueError("gram needs corr and nobs alongside it")
2503
+ g = np.ascontiguousarray(gram, dtype=np.float64)
2504
+ c = np.ascontiguousarray(corr, dtype=np.float64)
2505
+ self.p = c.size
2506
+ self.G = g.reshape(self.p, self.p)
2507
+ self.c0, self.nobs = c, int(nobs)
2508
+ else:
2509
+ if X is None or y is None:
2510
+ raise ValueError("pass X and y, or gram, corr and nobs")
2511
+ X = np.ascontiguousarray(X, dtype=np.float64)
2512
+ y = np.ascontiguousarray(y, dtype=np.float64)
2513
+ self.nobs, self.p = X.shape
2514
+ self.G = X.T @ X
2515
+ self.c0 = X.T @ y
2516
+ if groups is None:
2517
+ raise ValueError("groups is required")
2518
+ self.groups = [np.asarray(g, dtype=np.intp) for g in groups]
2519
+ seen = np.concatenate(self.groups)
2520
+ if sorted(seen.tolist()) != list(range(self.p)):
2521
+ raise ValueError("groups must partition 0..p-1")
2522
+ self.wg = np.array([np.sqrt(len(g)) for g in self.groups])
2523
+ # the majorizer per group: its Gram block's top eigenvalue
2524
+ self.Lg = np.array(
2525
+ [float(np.linalg.eigvalsh(self.G[np.ix_(g, g)])[-1])
2526
+ for g in self.groups])
2527
+
2528
+ def lambda_max(self):
2529
+ """The smallest penalty at which every group stays zero."""
2530
+ return max(float(np.linalg.norm(self.c0[g])) / (self.nobs * w)
2531
+ for g, w in zip(self.groups, self.wg))
2532
+
2533
+ def lambda_grid(self, num=50, eps=1e-2):
2534
+ """A log-spaced descending path from lambda_max."""
2535
+ return self.lambda_max() * np.logspace(0, np.log10(eps), int(num))
2536
+
2537
+ def fit_path(self, lambdas, tol=1e-8, max_sweeps=10000):
2538
+ """Coefficients per lambda, warm-started down the path."""
2539
+ lambdas = np.asarray(lambdas, dtype=np.float64)
2540
+ beta = np.zeros(self.p)
2541
+ c = self.c0.copy()
2542
+ out = np.empty((lambdas.size, self.p))
2543
+ for i, lam in enumerate(lambdas):
2544
+ beta, c = self._fit_one(beta, c, float(lam), tol, max_sweeps)
2545
+ out[i] = beta
2546
+ return out
2547
+
2548
+ def fit(self, lam, **kw):
2549
+ """Coefficients at one lambda, from zero."""
2550
+ b, _ = self._fit_one(np.zeros(self.p), self.c0.copy(), float(lam),
2551
+ kw.get("tol", 1e-8),
2552
+ kw.get("max_sweeps", 10000))
2553
+ return b
2554
+
2555
+ def _fit_one(self, beta, c, lam, tol, max_sweeps):
2556
+ thr = lam * self.nobs
2557
+ for _ in range(max_sweeps):
2558
+ moved = 0.0
2559
+ for g, w, L in zip(self.groups, self.wg, self.Lg):
2560
+ bg = beta[g]
2561
+ z = bg + c[g] / L
2562
+ nz = float(np.linalg.norm(z))
2563
+ scale = max(0.0, 1.0 - thr * w / (L * nz)) if nz > 0 else 0.0
2564
+ nb = scale * z
2565
+ d = nb - bg
2566
+ dmax = float(np.max(np.abs(d))) if d.size else 0.0
2567
+ if dmax > 0.0:
2568
+ c -= self.G[:, g] @ d
2569
+ beta[g] = nb
2570
+ moved = max(moved, dmax)
2571
+ if moved < tol * max(1.0, float(np.max(np.abs(beta)))):
2572
+ break
2573
+ return beta, c
2574
+
2575
+ def objective(self, y_or_none, beta, lam):
2576
+ """The penalized objective, up to the constant ||y||^2/(2n).
2577
+
2578
+ Computable from the Gram alone: (1/2n)(b'Gb - 2 c0'b) plus the
2579
+ penalty; add ||y||^2/(2n) yourself if you want the absolute
2580
+ value.
2581
+ """
2582
+ quad = 0.5 * (beta @ self.G @ beta) - self.c0 @ beta
2583
+ pen = sum(w * float(np.linalg.norm(beta[g]))
2584
+ for g, w in zip(self.groups, self.wg))
2585
+ return quad / self.nobs + lam * pen
2586
+
2587
+
2588
+ class CovMultiTaskLasso:
2589
+ """Multi-task lasso and elastic net over a design's Gram matrix.
2590
+
2591
+ The penalty ties each feature's row of W together across tasks --
2592
+ the row enters or leaves for every task at once -- matching
2593
+ scikit-learn's ``MultiTaskLasso`` / ``MultiTaskElasticNet`` with
2594
+ ``fit_intercept=False``:
2595
+
2596
+ (1/2n)||Y - XW||^2_F + alpha l1_ratio sum_j ||W_j||_2
2597
+ + (alpha/2)(1 - l1_ratio) ||W||^2_F
2598
+
2599
+ The covariance route carries over unchanged: the solver keeps
2600
+ C = X'Y - G W per task, so a row's block update is O(p T) and never
2601
+ touches the n rows. Coefficients are (p, n_tasks) -- the transpose
2602
+ of scikit-learn's ``coef_``.
2603
+ """
2604
+
2605
+ def __init__(self, X=None, Y=None, *, gram=None, corr=None, nobs=None):
2606
+ if gram is not None:
2607
+ if corr is None or nobs is None:
2608
+ raise ValueError("gram needs corr and nobs alongside it")
2609
+ c = np.ascontiguousarray(corr, dtype=np.float64)
2610
+ if c.ndim != 2:
2611
+ raise ValueError("corr must be (p, n_tasks)")
2612
+ self.p, self.n_tasks = c.shape
2613
+ g = np.ascontiguousarray(gram, dtype=np.float64).ravel()
2614
+ if g.size != self.p * self.p:
2615
+ raise ValueError(f"gram is {g.size} entries; expected p*p")
2616
+ self.nobs, self.g, self.c0 = int(nobs), g, c.ravel()
2617
+ else:
2618
+ if X is None or Y is None:
2619
+ raise ValueError("pass X and Y, or gram, corr and nobs")
2620
+ X = np.ascontiguousarray(X, dtype=np.float64)
2621
+ Y = np.ascontiguousarray(Y, dtype=np.float64)
2622
+ if Y.ndim != 2 or X.shape[0] != Y.shape[0]:
2623
+ raise ValueError(
2624
+ f"X is {X.shape} and Y is {Y.shape}; expected (n, p) "
2625
+ "and (n, n_tasks)")
2626
+ self.nobs, self.p = X.shape
2627
+ self.n_tasks = Y.shape[1]
2628
+ self.g = np.ascontiguousarray((X.T @ X).ravel())
2629
+ self.c0 = np.ascontiguousarray((X.T @ Y).ravel())
2630
+ self.X, self.Y = X, Y
2631
+
2632
+ def lambda_max(self, l1_ratio=1.0):
2633
+ """The smallest penalty that zeroes every row of W.
2634
+
2635
+ The single-task |X_j'y| becomes the L2 norm of the row X_j'Y:
2636
+ below it the block soft threshold leaves the row at zero.
2637
+ """
2638
+ C = self.c0.reshape(self.p, self.n_tasks)
2639
+ return (float(np.max(np.linalg.norm(C, axis=1)))
2640
+ / (self.nobs * l1_ratio))
2641
+
2642
+ def lambda_grid(self, num=100, eps=1e-3, l1_ratio=1.0):
2643
+ """Log-spaced from lambda_max down, as the single-task grid is."""
2644
+ hi = self.lambda_max(l1_ratio)
2645
+ return hi * np.logspace(0, np.log10(eps), int(num))
2646
+
2647
+ def fit(self, lam, **kw):
2648
+ """Coefficients (p, n_tasks) at one penalty."""
2649
+ return self.fit_path([lam], **kw)[0]
2650
+
2651
+ def fit_path(self, lambdas, tol=1e-8, chunk=20, max_sweeps=100000,
2652
+ l1_ratio=1.0):
2653
+ """Coefficients per lambda, warm-started; (len(lambdas), p, n_tasks).
2654
+
2655
+ Give lambdas in descending order, as with the single-task path.
2656
+ """
2657
+ lambdas = np.asarray(lambdas, dtype=np.float64)
2658
+ pt = self.p * self.n_tasks
2659
+ W, c = np.zeros(pt), self.c0.copy()
2660
+ prev = np.empty(pt)
2661
+ out = np.empty((lambdas.size, self.p, self.n_tasks))
2662
+ for i, lam in enumerate(lambdas):
2663
+ swept = 0
2664
+ while swept < max_sweeps:
2665
+ prev[:] = W
2666
+ kernel.mt_descend(self.g, c, W,
2667
+ float(lam) * l1_ratio,
2668
+ float(lam) * (1.0 - l1_ratio), chunk,
2669
+ float(self.nobs), self.p, self.n_tasks)
2670
+ swept += chunk
2671
+ if np.max(np.abs(W - prev)) < tol * _scale(W):
2672
+ break
2673
+ out[i] = W.reshape(self.p, self.n_tasks)
2674
+ return out
2675
+
2676
+ def fit_path_batch(self, lambdas, tol=1e-8, chunk=20,
2677
+ max_sweeps=100000, force_cpu=False, l1_ratio=1.0):
2678
+ """Coefficients per lambda, every lambda solved from zero.
2679
+
2680
+ (len(lambdas), p, n_tasks). On the GPU one block per lambda,
2681
+ all sharing this Gram; the CPU fallback is the cold loop.
2682
+ """
2683
+ lambdas = np.ascontiguousarray(lambdas, dtype=np.float64)
2684
+ pt = self.p * self.n_tasks
2685
+ if not force_cpu:
2686
+ W = _grid_descend(self.g[None, :], self.c0[None, :], lambdas,
2687
+ self.p, self.n_tasks, tol=tol, chunk=chunk,
2688
+ max_sweeps=max_sweeps, l1_ratio=l1_ratio,
2689
+ nobs=float(self.nobs))
2690
+ if W is not None:
2691
+ return W.reshape(lambdas.size, self.p, self.n_tasks)
2692
+ out = np.empty((lambdas.size, pt))
2693
+ prev = np.empty(pt)
2694
+ for i, lam in enumerate(lambdas):
2695
+ W, c, swept = np.zeros(pt), self.c0.copy(), 0
2696
+ while swept < max_sweeps:
2697
+ prev[:] = W
2698
+ kernel.mt_descend(self.g, c, W, float(lam) * l1_ratio,
2699
+ float(lam) * (1.0 - l1_ratio), chunk,
2700
+ float(self.nobs), self.p, self.n_tasks)
2701
+ swept += chunk
2702
+ if np.max(np.abs(W - prev)) < tol * _scale(W):
2703
+ break
2704
+ out[i] = W
2705
+ return out.reshape(lambdas.size, self.p, self.n_tasks)
2706
+
2707
+
2708
+ class CovMultiTaskLassoCV:
2709
+ """scikit-learn's ``MultiTaskLassoCV`` (``MultiTaskElasticNetCV``
2710
+ with ``l1_ratio``) over the covariance machinery.
2711
+
2712
+ Same construction as ``CovLassoCV``: the grid from the full data,
2713
+ contiguous folds, each fold's training Gram a subtraction from the
2714
+ full one, the whole path walked warm per fold, alpha_ by minimum
2715
+ mean validation MSE (over samples and tasks) with ties to the
2716
+ larger penalty, refit on all rows. With a GPU the grid is one
2717
+ launch, one block per (fold, penalty) problem, as in CovLassoCV.
2718
+
2719
+ Attributes after fit: alphas_, mse_path_ (num, cv), alpha_, coef_
2720
+ (p, n_tasks).
2721
+ """
2722
+
2723
+ def __init__(self, cv=5, num=100, eps=1e-3, tol=1e-8, l1_ratio=1.0,
2724
+ force_cpu=False, force_gpu=False, n_jobs=1):
2725
+ self.cv = int(cv)
2726
+ self.num = int(num)
2727
+ self.eps = float(eps)
2728
+ self.tol = float(tol)
2729
+ self.l1_ratio = float(l1_ratio)
2730
+ self.force_cpu = force_cpu
2731
+ self.force_gpu = force_gpu
2732
+ # Same fold-thread parallelism as CovLassoCV; default sequential.
2733
+ self.n_jobs = int(n_jobs)
2734
+
2735
+ def fit(self, X, Y):
2736
+ X = np.ascontiguousarray(X, dtype=np.float64)
2737
+ Y = np.ascontiguousarray(Y, dtype=np.float64)
2738
+ n = X.shape[0]
2739
+ full = CovMultiTaskLasso(X, Y)
2740
+ self.alphas_ = full.lambda_grid(num=self.num, eps=self.eps,
2741
+ l1_ratio=self.l1_ratio)
2742
+ bounds = np.linspace(0, n, self.cv + 1).astype(int)
2743
+ self.mse_path_ = np.empty((self.num, self.cv))
2744
+ G = full.g.reshape(full.p, full.p)
2745
+ C = full.c0.reshape(full.p, full.n_tasks)
2746
+ if cuda_available() and not self.force_cpu:
2747
+ done = self._fit_gpu(X, Y, full, G, C, bounds, n)
2748
+ if done is not None:
2749
+ return done
2750
+
2751
+ def one_fold(k):
2752
+ lo, hi = bounds[k], bounds[k + 1]
2753
+ Xf, Yf = X[lo:hi], Y[lo:hi]
2754
+ m = CovMultiTaskLasso(gram=(G - Xf.T @ Xf).ravel(),
2755
+ corr=C - Xf.T @ Yf,
2756
+ nobs=n - (hi - lo))
2757
+ path = m.fit_path(self.alphas_, tol=self.tol,
2758
+ l1_ratio=self.l1_ratio)
2759
+ # all validation residuals in one product: (nf, num, T)
2760
+ resid = np.tensordot(Xf, path, axes=([1], [1])) - Yf[:, None, :]
2761
+ self.mse_path_[:, k] = np.mean(resid * resid, axis=(0, 2))
2762
+
2763
+ if self.n_jobs > 1:
2764
+ from concurrent.futures import ThreadPoolExecutor
2765
+ with ThreadPoolExecutor(max_workers=self.n_jobs) as ex:
2766
+ list(ex.map(one_fold, range(self.cv)))
2767
+ else:
2768
+ for k in range(self.cv):
2769
+ one_fold(k)
2770
+ return self._finish(full)
2771
+
2772
+ def _fit_gpu(self, X, Y, full, G, C, bounds, n):
2773
+ # The grid as one launch, the fold's Gram shared by its
2774
+ # problems and nobs absorbed into each problem's lambda as in
2775
+ # CovLassoCV; None when the device declined.
2776
+ p_, T = full.p, full.n_tasks
2777
+ num, cv = self.num, self.cv
2778
+ grams = np.empty((cv, p_ * p_))
2779
+ corrs = np.empty((cv, p_ * T))
2780
+ nobs_k = np.empty(cv)
2781
+ for k in range(cv):
2782
+ lo, hi = bounds[k], bounds[k + 1]
2783
+ Xf, Yf = X[lo:hi], Y[lo:hi]
2784
+ grams[k] = (G - Xf.T @ Xf).ravel()
2785
+ corrs[k] = (C - Xf.T @ Yf).ravel()
2786
+ nobs_k[k] = n - (hi - lo)
2787
+ lams = np.tile(self.alphas_, cv) * np.repeat(nobs_k, num)
2788
+ W = _grid_descend(grams, corrs, lams, p_, T, tol=self.tol,
2789
+ l1_ratio=self.l1_ratio)
2790
+ if W is None:
2791
+ return None
2792
+ W = W.reshape(cv, num, p_, T)
2793
+ for k in range(cv):
2794
+ lo, hi = bounds[k], bounds[k + 1]
2795
+ resid = (np.tensordot(X[lo:hi], W[k], axes=([1], [1]))
2796
+ - Y[lo:hi, None, :])
2797
+ self.mse_path_[:, k] = np.mean(resid * resid, axis=(0, 2))
2798
+ return self._finish(full)
2799
+
2800
+ def _finish(self, full):
2801
+ mean = self.mse_path_.mean(axis=1)
2802
+ self.alpha_ = float(self.alphas_[int(np.argmin(mean))])
2803
+ self.coef_ = full.fit(self.alpha_, tol=self.tol,
2804
+ l1_ratio=self.l1_ratio)
2805
+ return self
2806
+
2807
+ def predict(self, X):
2808
+ return np.ascontiguousarray(X, dtype=np.float64) @ self.coef_