scm2cpp-lasso 0.5.2__tar.gz → 0.5.3.1__tar.gz
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.
- {scm2cpp_lasso-0.5.2 → scm2cpp_lasso-0.5.3.1}/PKG-INFO +58 -1
- {scm2cpp_lasso-0.5.2 → scm2cpp_lasso-0.5.3.1}/README.md +57 -0
- {scm2cpp_lasso-0.5.2 → scm2cpp_lasso-0.5.3.1}/pyproject.toml +1 -1
- {scm2cpp_lasso-0.5.2 → scm2cpp_lasso-0.5.3.1}/src/scm2cpp_lasso/__init__.py +128 -4
- {scm2cpp_lasso-0.5.2 → scm2cpp_lasso-0.5.3.1}/src/scm2cpp_lasso.egg-info/PKG-INFO +58 -1
- {scm2cpp_lasso-0.5.2 → scm2cpp_lasso-0.5.3.1}/setup.cfg +0 -0
- {scm2cpp_lasso-0.5.2 → scm2cpp_lasso-0.5.3.1}/setup.py +0 -0
- {scm2cpp_lasso-0.5.2 → scm2cpp_lasso-0.5.3.1}/src/scm2cpp_lasso/_generated/__init__.py +0 -0
- {scm2cpp_lasso-0.5.2 → scm2cpp_lasso-0.5.3.1}/src/scm2cpp_lasso/_generated/_loader.py +0 -0
- {scm2cpp_lasso-0.5.2 → scm2cpp_lasso-0.5.3.1}/src/scm2cpp_lasso/_generated/lasso_cov.cpp +0 -0
- {scm2cpp_lasso-0.5.2 → scm2cpp_lasso-0.5.3.1}/src/scm2cpp_lasso/_generated/lasso_cov.hpp +0 -0
- {scm2cpp_lasso-0.5.2 → scm2cpp_lasso-0.5.3.1}/src/scm2cpp_lasso/_generated/lasso_cov_capi.cpp +0 -0
- {scm2cpp_lasso-0.5.2 → scm2cpp_lasso-0.5.3.1}/src/scm2cpp_lasso/_generated/scm2cpp.hpp +0 -0
- {scm2cpp_lasso-0.5.2 → scm2cpp_lasso-0.5.3.1}/src/scm2cpp_lasso/_libfind.py +0 -0
- {scm2cpp_lasso-0.5.2 → scm2cpp_lasso-0.5.3.1}/src/scm2cpp_lasso/batch_capi.cu +0 -0
- {scm2cpp_lasso-0.5.2 → scm2cpp_lasso-0.5.3.1}/src/scm2cpp_lasso.egg-info/SOURCES.txt +0 -0
- {scm2cpp_lasso-0.5.2 → scm2cpp_lasso-0.5.3.1}/src/scm2cpp_lasso.egg-info/dependency_links.txt +0 -0
- {scm2cpp_lasso-0.5.2 → scm2cpp_lasso-0.5.3.1}/src/scm2cpp_lasso.egg-info/requires.txt +0 -0
- {scm2cpp_lasso-0.5.2 → scm2cpp_lasso-0.5.3.1}/src/scm2cpp_lasso.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scm2cpp-lasso
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.3.1
|
|
4
4
|
Summary: Lasso by covariance-update coordinate descent, translated from Scheme to C++ by scm2cpp; GPU batch solver included
|
|
5
5
|
Author: Hirotaka Niitsuma
|
|
6
6
|
License: MIT
|
|
@@ -70,9 +70,14 @@ from scm2cpp_lasso import cuda_available
|
|
|
70
70
|
lambdas = model.lambda_grid(num=100) # from lambda_max down
|
|
71
71
|
path = model.fit_path(lambdas) # warm-started, sequential
|
|
72
72
|
grid = model.fit_path_batch(lambdas) # every lambda from zero
|
|
73
|
+
one = model.fit(lambdas[60]) # a single lambda, if you must
|
|
73
74
|
print("GPU:", cuda_available())
|
|
74
75
|
```
|
|
75
76
|
|
|
77
|
+
`fit` is the one-element special case of `fit_path` -- the Gram matrix
|
|
78
|
+
was built in the constructor, so a whole warm-started path costs barely
|
|
79
|
+
more than one penalty, which is why the path is the primary interface.
|
|
80
|
+
|
|
76
81
|
The objective is scikit-learn's, with `fit_intercept=False`:
|
|
77
82
|
|
|
78
83
|
(1 / 2 nobs) ||y - X b||^2 + lam ||b||_1
|
|
@@ -84,6 +89,58 @@ at zero, and `lambda_grid()` walks down from it -- the construction
|
|
|
84
89
|
scikit-learn uses. If your columns differ wildly in scale, standardize
|
|
85
90
|
them first; this solver does not do it for you.
|
|
86
91
|
|
|
92
|
+
## Against the other lassos pip can install
|
|
93
|
+
|
|
94
|
+
`bench/lasso-compare.py` in the repository runs the same
|
|
95
|
+
cross-validation-shaped grid -- 4096 lambdas, each solved from zero,
|
|
96
|
+
p=200, n=1800 -- against every lasso a `pip install` produces:
|
|
97
|
+
scikit-learn 1.9.0, celer 0.7.4, skglm 0.5, and RAPIDS cuML 26.8.
|
|
98
|
+
Every solver runs at its own default tolerance (ours at the tighter
|
|
99
|
+
1e-8), each gets one untimed warm-up fit, cuML's data is on the device
|
|
100
|
+
before the clock starts, and the last column is each solver's
|
|
101
|
+
objective minus ours at the smallest lambda -- a fast row with a loose
|
|
102
|
+
answer would show as one:
|
|
103
|
+
|
|
104
|
+
| solver | time | objective gap |
|
|
105
|
+
|----------------------------------------|---------|---------------|
|
|
106
|
+
| scm2cpp-lasso, 1 CPU core (tol 1e-8) | 0.9 s | 0 |
|
|
107
|
+
| scm2cpp-lasso, GPU, one thread/lambda | 0.2 s | 0 |
|
|
108
|
+
| sklearn `Lasso.fit` per lambda | 12.5 s | +1.6e-09 |
|
|
109
|
+
| celer per lambda | 17.3 s | 0 |
|
|
110
|
+
| skglm per lambda | 16.4 s | +2.8e-17 |
|
|
111
|
+
| cuML per lambda, GPU | 57.5 s | +9.1e-07 |
|
|
112
|
+
|
|
113
|
+
celer and skglm are built for very large sparse designs, where their
|
|
114
|
+
screening rules dominate; at this size they pay their setup per fit.
|
|
115
|
+
cuML parallelises *within* one fit, which wins when a fit is large; at
|
|
116
|
+
this size its launch overhead dominates, while our GPU row
|
|
117
|
+
parallelises *across* lambdas -- one CUDA thread per penalty, the axis
|
|
118
|
+
a cross-validation grid actually offers.
|
|
119
|
+
|
|
120
|
+
`CovLassoCV` is scikit-learn's `LassoCV` over this machinery -- same
|
|
121
|
+
grid construction, same contiguous folds, same minimum-mean-MSE choice
|
|
122
|
+
-- with two structural savings. A fold's training Gram is a
|
|
123
|
+
*subtraction* (the Gram is additive over rows, so G - Xf'Xf costs one
|
|
124
|
+
fold's work where rebuilding costs four folds'), and once the Grams
|
|
125
|
+
exist nothing downstream ever touches the n rows again -- which is why
|
|
126
|
+
the gap grows with n. On the GPU every fold and every alpha can run
|
|
127
|
+
as one thread of a single launch; that launch replicates each fold's
|
|
128
|
+
Gram per alpha, so it is chosen only while the replication stays under
|
|
129
|
+
512 MB, the CPU warm path taking over at large p. Measured at cv=5,
|
|
130
|
+
100 alphas, one CPU core:
|
|
131
|
+
|
|
132
|
+
| n | p | `CovLassoCV` | sklearn `LassoCV` |
|
|
133
|
+
|---------|------|--------------|-------------------|
|
|
134
|
+
| 1,800 | 200 | 0.23 s | 0.20 s |
|
|
135
|
+
| 5,000 | 1000 | 1.1 s | 3.4 s |
|
|
136
|
+
| 100,000 | 200 | 0.44 s | 5.7 s |
|
|
137
|
+
| 100,000 | 500 | 0.90 s | 8.0 s |
|
|
138
|
+
|
|
139
|
+
At n=100,000 the chosen alpha is identical and the coefficients agree
|
|
140
|
+
to 7e-7; at small n a near-tie can leave the picks one grid step
|
|
141
|
+
apart, which sklearn resolves our way once its tolerance is
|
|
142
|
+
tightened. GPU and CPU agree to 1e-14 wherever both run.
|
|
143
|
+
|
|
87
144
|
## Which method
|
|
88
145
|
|
|
89
146
|
`fit_path` walks a single path, each lambda starting from the previous
|
|
@@ -52,9 +52,14 @@ from scm2cpp_lasso import cuda_available
|
|
|
52
52
|
lambdas = model.lambda_grid(num=100) # from lambda_max down
|
|
53
53
|
path = model.fit_path(lambdas) # warm-started, sequential
|
|
54
54
|
grid = model.fit_path_batch(lambdas) # every lambda from zero
|
|
55
|
+
one = model.fit(lambdas[60]) # a single lambda, if you must
|
|
55
56
|
print("GPU:", cuda_available())
|
|
56
57
|
```
|
|
57
58
|
|
|
59
|
+
`fit` is the one-element special case of `fit_path` -- the Gram matrix
|
|
60
|
+
was built in the constructor, so a whole warm-started path costs barely
|
|
61
|
+
more than one penalty, which is why the path is the primary interface.
|
|
62
|
+
|
|
58
63
|
The objective is scikit-learn's, with `fit_intercept=False`:
|
|
59
64
|
|
|
60
65
|
(1 / 2 nobs) ||y - X b||^2 + lam ||b||_1
|
|
@@ -66,6 +71,58 @@ at zero, and `lambda_grid()` walks down from it -- the construction
|
|
|
66
71
|
scikit-learn uses. If your columns differ wildly in scale, standardize
|
|
67
72
|
them first; this solver does not do it for you.
|
|
68
73
|
|
|
74
|
+
## Against the other lassos pip can install
|
|
75
|
+
|
|
76
|
+
`bench/lasso-compare.py` in the repository runs the same
|
|
77
|
+
cross-validation-shaped grid -- 4096 lambdas, each solved from zero,
|
|
78
|
+
p=200, n=1800 -- against every lasso a `pip install` produces:
|
|
79
|
+
scikit-learn 1.9.0, celer 0.7.4, skglm 0.5, and RAPIDS cuML 26.8.
|
|
80
|
+
Every solver runs at its own default tolerance (ours at the tighter
|
|
81
|
+
1e-8), each gets one untimed warm-up fit, cuML's data is on the device
|
|
82
|
+
before the clock starts, and the last column is each solver's
|
|
83
|
+
objective minus ours at the smallest lambda -- a fast row with a loose
|
|
84
|
+
answer would show as one:
|
|
85
|
+
|
|
86
|
+
| solver | time | objective gap |
|
|
87
|
+
|----------------------------------------|---------|---------------|
|
|
88
|
+
| scm2cpp-lasso, 1 CPU core (tol 1e-8) | 0.9 s | 0 |
|
|
89
|
+
| scm2cpp-lasso, GPU, one thread/lambda | 0.2 s | 0 |
|
|
90
|
+
| sklearn `Lasso.fit` per lambda | 12.5 s | +1.6e-09 |
|
|
91
|
+
| celer per lambda | 17.3 s | 0 |
|
|
92
|
+
| skglm per lambda | 16.4 s | +2.8e-17 |
|
|
93
|
+
| cuML per lambda, GPU | 57.5 s | +9.1e-07 |
|
|
94
|
+
|
|
95
|
+
celer and skglm are built for very large sparse designs, where their
|
|
96
|
+
screening rules dominate; at this size they pay their setup per fit.
|
|
97
|
+
cuML parallelises *within* one fit, which wins when a fit is large; at
|
|
98
|
+
this size its launch overhead dominates, while our GPU row
|
|
99
|
+
parallelises *across* lambdas -- one CUDA thread per penalty, the axis
|
|
100
|
+
a cross-validation grid actually offers.
|
|
101
|
+
|
|
102
|
+
`CovLassoCV` is scikit-learn's `LassoCV` over this machinery -- same
|
|
103
|
+
grid construction, same contiguous folds, same minimum-mean-MSE choice
|
|
104
|
+
-- with two structural savings. A fold's training Gram is a
|
|
105
|
+
*subtraction* (the Gram is additive over rows, so G - Xf'Xf costs one
|
|
106
|
+
fold's work where rebuilding costs four folds'), and once the Grams
|
|
107
|
+
exist nothing downstream ever touches the n rows again -- which is why
|
|
108
|
+
the gap grows with n. On the GPU every fold and every alpha can run
|
|
109
|
+
as one thread of a single launch; that launch replicates each fold's
|
|
110
|
+
Gram per alpha, so it is chosen only while the replication stays under
|
|
111
|
+
512 MB, the CPU warm path taking over at large p. Measured at cv=5,
|
|
112
|
+
100 alphas, one CPU core:
|
|
113
|
+
|
|
114
|
+
| n | p | `CovLassoCV` | sklearn `LassoCV` |
|
|
115
|
+
|---------|------|--------------|-------------------|
|
|
116
|
+
| 1,800 | 200 | 0.23 s | 0.20 s |
|
|
117
|
+
| 5,000 | 1000 | 1.1 s | 3.4 s |
|
|
118
|
+
| 100,000 | 200 | 0.44 s | 5.7 s |
|
|
119
|
+
| 100,000 | 500 | 0.90 s | 8.0 s |
|
|
120
|
+
|
|
121
|
+
At n=100,000 the chosen alpha is identical and the coefficients agree
|
|
122
|
+
to 7e-7; at small n a near-tie can leave the picks one grid step
|
|
123
|
+
apart, which sklearn resolves our way once its tolerance is
|
|
124
|
+
tightened. GPU and CPU agree to 1e-14 wherever both run.
|
|
125
|
+
|
|
69
126
|
## Which method
|
|
70
127
|
|
|
71
128
|
`fit_path` walks a single path, each lambda starting from the previous
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "scm2cpp-lasso"
|
|
7
|
-
version = "0.5.
|
|
7
|
+
version = "0.5.3.1"
|
|
8
8
|
description = "Lasso by covariance-update coordinate descent, translated from Scheme to C++ by scm2cpp; GPU batch solver included"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -31,7 +31,7 @@ from ._libfind import load_batch_lib
|
|
|
31
31
|
|
|
32
32
|
__all__ = ["CovLasso", "CovRidge", "CovLogistic", "CovGroupLasso",
|
|
33
33
|
"cuda_available", "kernel"]
|
|
34
|
-
__version__ = "0.5.
|
|
34
|
+
__version__ = "0.5.3.1"
|
|
35
35
|
|
|
36
36
|
_BATCH = load_batch_lib()
|
|
37
37
|
_DP = ctypes.POINTER(ctypes.c_double)
|
|
@@ -257,7 +257,10 @@ def _batch_descend_multi(grams, corrs, lam, nobs, p, tol=1e-8, chunk=20,
|
|
|
257
257
|
beta = np.zeros((B, p))
|
|
258
258
|
c = np.ascontiguousarray(corrs.copy())
|
|
259
259
|
g = np.ascontiguousarray(grams)
|
|
260
|
-
|
|
260
|
+
# lam may be one penalty for every thread or one per thread; the
|
|
261
|
+
# kernel always reads a per-thread array
|
|
262
|
+
lams = (np.ascontiguousarray(lam, dtype=np.float64)
|
|
263
|
+
if np.ndim(lam) else np.full(B, float(lam)))
|
|
261
264
|
if _BATCH is not None and not force_cpu:
|
|
262
265
|
dp = ctypes.POINTER(ctypes.c_double)
|
|
263
266
|
rc = _BATCH.scm2cpp_batch_descend(
|
|
@@ -270,11 +273,12 @@ def _batch_descend_multi(grams, corrs, lam, nobs, p, tol=1e-8, chunk=20,
|
|
|
270
273
|
prev = np.empty(p)
|
|
271
274
|
for b in range(B):
|
|
272
275
|
gb, cb, bb = g[b], c[b], beta[b]
|
|
276
|
+
lam_b = float(lams[b])
|
|
273
277
|
swept = 0
|
|
274
278
|
while swept < max_sweeps:
|
|
275
279
|
prev[:] = bb
|
|
276
|
-
kernel_fn(gb, cb, bb,
|
|
277
|
-
|
|
280
|
+
kernel_fn(gb, cb, bb, lam_b * l1_ratio,
|
|
281
|
+
lam_b * (1.0 - l1_ratio), chunk,
|
|
278
282
|
float(nobs), p)
|
|
279
283
|
swept += chunk
|
|
280
284
|
if np.max(np.abs(bb - prev)) < tol * max(
|
|
@@ -283,6 +287,126 @@ def _batch_descend_multi(grams, corrs, lam, nobs, p, tol=1e-8, chunk=20,
|
|
|
283
287
|
return beta
|
|
284
288
|
|
|
285
289
|
|
|
290
|
+
|
|
291
|
+
class CovLassoCV:
|
|
292
|
+
"""Cross-validated lasso, scikit-learn's LassoCV semantics.
|
|
293
|
+
|
|
294
|
+
The lambda grid comes from the full data (lambda_max down, the
|
|
295
|
+
sklearn construction), the folds are contiguous splits in order
|
|
296
|
+
(sklearn's KFold default), each fold's whole path is solved from
|
|
297
|
+
zero -- which is exactly the workload fit_path_batch exists for,
|
|
298
|
+
one GPU thread per lambda -- and validation error is one matrix
|
|
299
|
+
product per fold. alpha_ minimises the mean validation MSE, ties
|
|
300
|
+
going to the larger penalty as in scikit-learn, and coef_ is the
|
|
301
|
+
refit on all rows at alpha_.
|
|
302
|
+
|
|
303
|
+
Attributes after fit: alphas_, mse_path_ (num, cv), alpha_, coef_.
|
|
304
|
+
"""
|
|
305
|
+
|
|
306
|
+
def __init__(self, cv=5, num=100, eps=1e-3, tol=1e-8, l1_ratio=1.0,
|
|
307
|
+
force_cpu=False):
|
|
308
|
+
self.cv = int(cv)
|
|
309
|
+
self.num = int(num)
|
|
310
|
+
self.eps = float(eps)
|
|
311
|
+
self.tol = float(tol)
|
|
312
|
+
self.l1_ratio = float(l1_ratio)
|
|
313
|
+
self.force_cpu = force_cpu
|
|
314
|
+
|
|
315
|
+
def fit(self, X, y):
|
|
316
|
+
X = np.ascontiguousarray(X, dtype=np.float64)
|
|
317
|
+
y = np.ascontiguousarray(y, dtype=np.float64)
|
|
318
|
+
n = len(y)
|
|
319
|
+
full = CovLasso(X, y)
|
|
320
|
+
self.alphas_ = full.lambda_grid(num=self.num, eps=self.eps,
|
|
321
|
+
l1_ratio=self.l1_ratio)
|
|
322
|
+
# contiguous folds in row order, sizes as even as they come --
|
|
323
|
+
# sklearn's KFold(shuffle=False)
|
|
324
|
+
bounds = np.linspace(0, n, self.cv + 1).astype(int)
|
|
325
|
+
self.mse_path_ = np.empty((self.num, self.cv))
|
|
326
|
+
# The single GPU launch replicates each fold's Gram once per
|
|
327
|
+
# alpha; at large p that replication is gigabytes of transfer
|
|
328
|
+
# and the CPU warm path wins, so the choice is made by the
|
|
329
|
+
# replication's size rather than by the GPU's mere presence.
|
|
330
|
+
gram_bytes = self.cv * self.num * full.p * full.p * 8
|
|
331
|
+
gpu = (cuda_available() and not self.force_cpu
|
|
332
|
+
and gram_bytes <= (1 << 29)) # 512 MB
|
|
333
|
+
G = full.g.reshape(full.p, full.p)
|
|
334
|
+
if gpu:
|
|
335
|
+
return self._fit_gpu(X, y, full, G, bounds, n)
|
|
336
|
+
for k in range(self.cv):
|
|
337
|
+
lo, hi = bounds[k], bounds[k + 1]
|
|
338
|
+
Xf, yf = X[lo:hi], y[lo:hi]
|
|
339
|
+
# The Gram is additive over rows, so a fold's training Gram
|
|
340
|
+
# is a subtraction: G_full - Xf'Xf costs O(n_fold p^2) where
|
|
341
|
+
# rebuilding from the complement costs O((n - n_fold) p^2).
|
|
342
|
+
# Over five folds that is the whole Gram work twice instead
|
|
343
|
+
# of four times, and the Gram is the dominant O(n p^2) step.
|
|
344
|
+
m = CovLasso(gram=(G - Xf.T @ Xf).ravel(),
|
|
345
|
+
corr=full.c0 - Xf.T @ yf,
|
|
346
|
+
nobs=n - (hi - lo))
|
|
347
|
+
# On the GPU every lambda is its own thread, so the batch is
|
|
348
|
+
# the right shape; on the CPU the warm-started path is: after
|
|
349
|
+
# the Gram, each lambda resumes where the previous stopped
|
|
350
|
+
# and costs O(p) per moving coordinate. That warm path is
|
|
351
|
+
# what makes searching the alpha grid cheap -- the grid
|
|
352
|
+
# costs barely more than its hardest single alpha.
|
|
353
|
+
if gpu:
|
|
354
|
+
path = m.fit_path_batch(self.alphas_, tol=self.tol,
|
|
355
|
+
l1_ratio=self.l1_ratio)
|
|
356
|
+
else:
|
|
357
|
+
path = m.fit_path(self.alphas_, tol=self.tol,
|
|
358
|
+
l1_ratio=self.l1_ratio)
|
|
359
|
+
resid = Xf @ path.T - yf[:, None]
|
|
360
|
+
self.mse_path_[:, k] = np.mean(resid * resid, axis=0)
|
|
361
|
+
mean = self.mse_path_.mean(axis=1)
|
|
362
|
+
# alphas_ descends, so argmin's first hit is the larger penalty
|
|
363
|
+
self.alpha_ = float(self.alphas_[int(np.argmin(mean))])
|
|
364
|
+
self.coef_ = full.fit(self.alpha_, tol=self.tol,
|
|
365
|
+
l1_ratio=self.l1_ratio)
|
|
366
|
+
return self
|
|
367
|
+
|
|
368
|
+
def _fit_gpu(self, X, y, full, G, bounds, n):
|
|
369
|
+
# Every fold and every alpha is an independent problem, so all of
|
|
370
|
+
# them go to the device as one batch: cv * num threads, each with
|
|
371
|
+
# its fold's Gram (the per-thread-Gram path the bootstrap added)
|
|
372
|
+
# and its own penalty. Fold sizes may differ by a row; the kernel
|
|
373
|
+
# only ever uses lam through the product lam * nobs, so each
|
|
374
|
+
# thread's nobs is absorbed into its lambda and nobs is passed
|
|
375
|
+
# as 1 -- exact, not approximate.
|
|
376
|
+
p_ = full.p
|
|
377
|
+
num, cv = self.num, self.cv
|
|
378
|
+
grams = np.empty((cv, p_ * p_))
|
|
379
|
+
corrs = np.empty((cv, p_))
|
|
380
|
+
nobs_k = np.empty(cv)
|
|
381
|
+
for k in range(cv):
|
|
382
|
+
lo, hi = bounds[k], bounds[k + 1]
|
|
383
|
+
Xf, yf = X[lo:hi], y[lo:hi]
|
|
384
|
+
grams[k] = (G - Xf.T @ Xf).ravel()
|
|
385
|
+
corrs[k] = full.c0 - Xf.T @ yf
|
|
386
|
+
nobs_k[k] = n - (hi - lo)
|
|
387
|
+
gr = np.repeat(grams, num, axis=0)
|
|
388
|
+
cr = np.repeat(corrs, num, axis=0)
|
|
389
|
+
lams = (np.tile(self.alphas_, cv) * np.repeat(nobs_k, num))
|
|
390
|
+
betas = _batch_descend_multi(gr, cr, lams, 1.0, p_,
|
|
391
|
+
tol=self.tol,
|
|
392
|
+
l1_ratio=self.l1_ratio,
|
|
393
|
+
force_cpu=self.force_cpu,
|
|
394
|
+
kernel_fn=kernel.enet_descend)
|
|
395
|
+
betas = betas.reshape(cv, num, p_)
|
|
396
|
+
for k in range(cv):
|
|
397
|
+
lo, hi = bounds[k], bounds[k + 1]
|
|
398
|
+
resid = X[lo:hi] @ betas[k].T - y[lo:hi, None]
|
|
399
|
+
self.mse_path_[:, k] = np.mean(resid * resid, axis=0)
|
|
400
|
+
mean = self.mse_path_.mean(axis=1)
|
|
401
|
+
self.alpha_ = float(self.alphas_[int(np.argmin(mean))])
|
|
402
|
+
self.coef_ = full.fit(self.alpha_, tol=self.tol,
|
|
403
|
+
l1_ratio=self.l1_ratio)
|
|
404
|
+
return self
|
|
405
|
+
|
|
406
|
+
def predict(self, X):
|
|
407
|
+
return np.ascontiguousarray(X, dtype=np.float64) @ self.coef_
|
|
408
|
+
|
|
409
|
+
|
|
286
410
|
class CovRidge:
|
|
287
411
|
"""Ridge regression over a design given by its Gram matrix.
|
|
288
412
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scm2cpp-lasso
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.3.1
|
|
4
4
|
Summary: Lasso by covariance-update coordinate descent, translated from Scheme to C++ by scm2cpp; GPU batch solver included
|
|
5
5
|
Author: Hirotaka Niitsuma
|
|
6
6
|
License: MIT
|
|
@@ -70,9 +70,14 @@ from scm2cpp_lasso import cuda_available
|
|
|
70
70
|
lambdas = model.lambda_grid(num=100) # from lambda_max down
|
|
71
71
|
path = model.fit_path(lambdas) # warm-started, sequential
|
|
72
72
|
grid = model.fit_path_batch(lambdas) # every lambda from zero
|
|
73
|
+
one = model.fit(lambdas[60]) # a single lambda, if you must
|
|
73
74
|
print("GPU:", cuda_available())
|
|
74
75
|
```
|
|
75
76
|
|
|
77
|
+
`fit` is the one-element special case of `fit_path` -- the Gram matrix
|
|
78
|
+
was built in the constructor, so a whole warm-started path costs barely
|
|
79
|
+
more than one penalty, which is why the path is the primary interface.
|
|
80
|
+
|
|
76
81
|
The objective is scikit-learn's, with `fit_intercept=False`:
|
|
77
82
|
|
|
78
83
|
(1 / 2 nobs) ||y - X b||^2 + lam ||b||_1
|
|
@@ -84,6 +89,58 @@ at zero, and `lambda_grid()` walks down from it -- the construction
|
|
|
84
89
|
scikit-learn uses. If your columns differ wildly in scale, standardize
|
|
85
90
|
them first; this solver does not do it for you.
|
|
86
91
|
|
|
92
|
+
## Against the other lassos pip can install
|
|
93
|
+
|
|
94
|
+
`bench/lasso-compare.py` in the repository runs the same
|
|
95
|
+
cross-validation-shaped grid -- 4096 lambdas, each solved from zero,
|
|
96
|
+
p=200, n=1800 -- against every lasso a `pip install` produces:
|
|
97
|
+
scikit-learn 1.9.0, celer 0.7.4, skglm 0.5, and RAPIDS cuML 26.8.
|
|
98
|
+
Every solver runs at its own default tolerance (ours at the tighter
|
|
99
|
+
1e-8), each gets one untimed warm-up fit, cuML's data is on the device
|
|
100
|
+
before the clock starts, and the last column is each solver's
|
|
101
|
+
objective minus ours at the smallest lambda -- a fast row with a loose
|
|
102
|
+
answer would show as one:
|
|
103
|
+
|
|
104
|
+
| solver | time | objective gap |
|
|
105
|
+
|----------------------------------------|---------|---------------|
|
|
106
|
+
| scm2cpp-lasso, 1 CPU core (tol 1e-8) | 0.9 s | 0 |
|
|
107
|
+
| scm2cpp-lasso, GPU, one thread/lambda | 0.2 s | 0 |
|
|
108
|
+
| sklearn `Lasso.fit` per lambda | 12.5 s | +1.6e-09 |
|
|
109
|
+
| celer per lambda | 17.3 s | 0 |
|
|
110
|
+
| skglm per lambda | 16.4 s | +2.8e-17 |
|
|
111
|
+
| cuML per lambda, GPU | 57.5 s | +9.1e-07 |
|
|
112
|
+
|
|
113
|
+
celer and skglm are built for very large sparse designs, where their
|
|
114
|
+
screening rules dominate; at this size they pay their setup per fit.
|
|
115
|
+
cuML parallelises *within* one fit, which wins when a fit is large; at
|
|
116
|
+
this size its launch overhead dominates, while our GPU row
|
|
117
|
+
parallelises *across* lambdas -- one CUDA thread per penalty, the axis
|
|
118
|
+
a cross-validation grid actually offers.
|
|
119
|
+
|
|
120
|
+
`CovLassoCV` is scikit-learn's `LassoCV` over this machinery -- same
|
|
121
|
+
grid construction, same contiguous folds, same minimum-mean-MSE choice
|
|
122
|
+
-- with two structural savings. A fold's training Gram is a
|
|
123
|
+
*subtraction* (the Gram is additive over rows, so G - Xf'Xf costs one
|
|
124
|
+
fold's work where rebuilding costs four folds'), and once the Grams
|
|
125
|
+
exist nothing downstream ever touches the n rows again -- which is why
|
|
126
|
+
the gap grows with n. On the GPU every fold and every alpha can run
|
|
127
|
+
as one thread of a single launch; that launch replicates each fold's
|
|
128
|
+
Gram per alpha, so it is chosen only while the replication stays under
|
|
129
|
+
512 MB, the CPU warm path taking over at large p. Measured at cv=5,
|
|
130
|
+
100 alphas, one CPU core:
|
|
131
|
+
|
|
132
|
+
| n | p | `CovLassoCV` | sklearn `LassoCV` |
|
|
133
|
+
|---------|------|--------------|-------------------|
|
|
134
|
+
| 1,800 | 200 | 0.23 s | 0.20 s |
|
|
135
|
+
| 5,000 | 1000 | 1.1 s | 3.4 s |
|
|
136
|
+
| 100,000 | 200 | 0.44 s | 5.7 s |
|
|
137
|
+
| 100,000 | 500 | 0.90 s | 8.0 s |
|
|
138
|
+
|
|
139
|
+
At n=100,000 the chosen alpha is identical and the coefficients agree
|
|
140
|
+
to 7e-7; at small n a near-tie can leave the picks one grid step
|
|
141
|
+
apart, which sklearn resolves our way once its tolerance is
|
|
142
|
+
tightened. GPU and CPU agree to 1e-14 wherever both run.
|
|
143
|
+
|
|
87
144
|
## Which method
|
|
88
145
|
|
|
89
146
|
`fit_path` walks a single path, each lambda starting from the previous
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{scm2cpp_lasso-0.5.2 → scm2cpp_lasso-0.5.3.1}/src/scm2cpp_lasso/_generated/lasso_cov_capi.cpp
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{scm2cpp_lasso-0.5.2 → scm2cpp_lasso-0.5.3.1}/src/scm2cpp_lasso.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|