scm2cpp-lasso 0.8.0__tar.gz → 0.9.0__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.8.0 → scm2cpp_lasso-0.9.0}/PKG-INFO +308 -4
- {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/README.md +305 -1
- {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/pyproject.toml +4 -4
- {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/setup.py +63 -1
- {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/__init__.py +714 -3
- scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/_loader_classifiers.py +190 -0
- scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/_loader_gramops_blas.py +75 -0
- scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/_loader_gramops_loops.py +75 -0
- scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/_loader_nmf_blas.py +29 -0
- scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/_loader_nmf_cublas.py +29 -0
- scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/_loader_nmf_loops.py +29 -0
- scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/classifiers.cpp +23 -0
- scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/classifiers.hpp +1950 -0
- scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/classifiers_capi.cpp +73 -0
- {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/_generated/derived_blas/lasso_design.hpp +12 -19
- {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/_generated/derived_cublas/lasso_design.hpp +13 -20
- {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/_generated/derived_loops/lasso_design.hpp +24 -26
- scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/gramops_blas/gramops.cpp +9 -0
- scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/gramops_blas/gramops.hpp +469 -0
- scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/gramops_blas/gramops_capi.cpp +31 -0
- scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/gramops_loops/gramops.cpp +9 -0
- scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/gramops_loops/gramops.hpp +499 -0
- scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/gramops_loops/gramops_capi.cpp +31 -0
- scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/nmf_blas/nmf_cd.cpp +5 -0
- scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/nmf_blas/nmf_cd.hpp +132 -0
- scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/nmf_blas/nmf_cd_capi.cpp +19 -0
- scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/nmf_cublas/nmf_cd.cpp +5 -0
- scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/nmf_cublas/nmf_cd.hpp +136 -0
- scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/nmf_cublas/nmf_cd_capi.cpp +19 -0
- scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/nmf_loops/nmf_cd.cpp +5 -0
- scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/nmf_loops/nmf_cd.hpp +167 -0
- scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/nmf_loops/nmf_cd_capi.cpp +19 -0
- {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/_libfind.py +44 -0
- {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso.egg-info/PKG-INFO +308 -4
- {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso.egg-info/SOURCES.txt +25 -1
- {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/setup.cfg +0 -0
- {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/_generated/__init__.py +0 -0
- {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/_generated/_loader.py +0 -0
- {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/_generated/_loader_derived_blas.py +0 -0
- {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/_generated/_loader_derived_cublas.py +0 -0
- {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/_generated/_loader_derived_loops.py +0 -0
- {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/_generated/derived_blas/lasso_design.cpp +0 -0
- {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/_generated/derived_blas/lasso_design_capi.cpp +0 -0
- {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/_generated/derived_cublas/lasso_design.cpp +0 -0
- {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/_generated/derived_cublas/lasso_design_capi.cpp +0 -0
- {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/_generated/derived_loops/lasso_design.cpp +0 -0
- {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/_generated/derived_loops/lasso_design_capi.cpp +0 -0
- {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/_generated/lasso_cov.cpp +0 -0
- {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/_generated/lasso_cov.hpp +0 -0
- {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/_generated/lasso_cov_capi.cpp +0 -0
- {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/_generated/scm2cpp-blas.hpp +0 -0
- {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/_generated/scm2cpp-cublas.hpp +0 -0
- {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/_generated/scm2cpp.hpp +0 -0
- {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/batch_capi.cu +0 -0
- {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso.egg-info/dependency_links.txt +0 -0
- {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso.egg-info/requires.txt +0 -0
- {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso.egg-info/top_level.txt +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scm2cpp-lasso
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Lasso
|
|
3
|
+
Version: 0.9.0
|
|
4
|
+
Summary: Lasso, elastic net, ridge, SVM, SVR, L1 logistic, NMF, graphical lasso, RANSAC, SGD, OMP and LARS translated from Scheme to C++ by scm2cpp; GPU batch solver included
|
|
5
5
|
Author: Hirotaka Niitsuma
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/niitsuma/scm2cpp
|
|
8
8
|
Project-URL: Source, https://github.com/niitsuma/scm2cpp/tree/master/python/scm2cpp-lasso
|
|
9
|
-
Keywords: lasso,elastic-net,ridge,coordinate-descent,cuda,compiler
|
|
9
|
+
Keywords: lasso,elastic-net,ridge,svm,logistic-regression,coordinate-descent,cuda,compiler
|
|
10
10
|
Classifier: License :: OSI Approved :: MIT License
|
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
|
12
12
|
Classifier: Programming Language :: C++
|
|
@@ -156,12 +156,58 @@ three larger shapes -- the figures of the repository's equal-work
|
|
|
156
156
|
table, now reached from the package with nothing in front of the
|
|
157
157
|
kernel: the constructor keeps X and y and the kernel takes the
|
|
158
158
|
residual and the column norms itself (the norms folded into the Gram
|
|
159
|
-
diagonal by the derivation).
|
|
159
|
+
diagonal by the derivation). Threads change the picture less than one might expect: scikit-learn's
|
|
160
|
+
sweeps are single-threaded Cython and only its Gram product scales,
|
|
161
|
+
so with 20 BLAS threads `lasso_path` takes 0.051 / 0.227 / 0.450 s on
|
|
162
|
+
the three larger shapes, against 0.023 / 0.050 / 0.113 s for the
|
|
163
|
+
cuBLAS build (2.2x, 4.5x, 4.0x) and 0.025 / 0.036 / 0.123 s for the
|
|
164
|
+
CBLAS build (2.0x, 6.3x, 3.7x), whose `dsyrk` scales with the
|
|
165
|
+
threads; the GPU figures do not move. Against a reference driven to
|
|
160
166
|
tol=1e-12, `DerivedLasso` lands within 5e-8 on every backend (its
|
|
161
167
|
tolerance is absolute, on one sweep's largest move), `CovLasso`
|
|
162
168
|
within 2e-10 (it tests every 20 sweeps and overshoots), and
|
|
163
169
|
scikit-learn at its default within 1e-3.
|
|
164
170
|
|
|
171
|
+
### Deriving it yourself
|
|
172
|
+
|
|
173
|
+
Everything `DerivedLasso` runs is reproducible from the Scheme with
|
|
174
|
+
the translator; this is what `regenerate.sh` does for the package,
|
|
175
|
+
spelled out for one kernel (Racket 8 and a C++17 compiler; a CBLAS
|
|
176
|
+
for `--blas`, the CUDA toolkit for `--cublas`):
|
|
177
|
+
|
|
178
|
+
```console
|
|
179
|
+
$ git clone https://github.com/niitsuma/scm2cpp && cd scm2cpp
|
|
180
|
+
$ raco link --user vendor/rkanren # once per machine
|
|
181
|
+
$ mkdir /tmp/k && cp examples/kernel-only/lasso-design.scm examples/kernel-only/soft-threshold.scm /tmp/k/
|
|
182
|
+
$ racket scm2cpp-file.scm -t scm2c.typ --derive --cublas -M -S /tmp/k/lasso-design.scm
|
|
183
|
+
derive: lasso-design: raise differencing merge matmul guard
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
`derive:` on stderr names what fired: the loops raised to the array
|
|
187
|
+
algebra, the residual differenced into the memo, the column norms
|
|
188
|
+
merged into the Gram diagonal, the products folded to contractions,
|
|
189
|
+
the `(if (< p n) ..)` guard. The run leaves, beside the source,
|
|
190
|
+
`lasso-design.hpp` (the derived kernel as C++: one `blas_syrk`, two
|
|
191
|
+
`blas_gemv`, the sweep), `lasso-design_capi.cpp` and
|
|
192
|
+
`lasso_design.py` (the `-M` C ABI and its ctypes loader -- what the
|
|
193
|
+
package's `_loader_derived_*.py` are), and `lasso-design.expanded.scm`
|
|
194
|
+
(`-S`: the derived program as Scheme, before C++ -- read this to see
|
|
195
|
+
what the derivation did). `--blas` instead of `--cublas` gives the
|
|
196
|
+
CBLAS variant, neither the loop variant. To call it from numpy
|
|
197
|
+
without the package:
|
|
198
|
+
|
|
199
|
+
```console
|
|
200
|
+
$ g++ -O2 -std=c++17 -shared -fPIC -I. -I$CUDA_HOME/include -o /tmp/k/liblasso-design.so \
|
|
201
|
+
/tmp/k/lasso-design_capi.cpp -L$CUDA_HOME/lib64 -lcublas -lcudart # -lopenblas for --blas
|
|
202
|
+
$ cd /tmp/k && python3 -c 'import lasso_design' # lasso_design.lasso_design(x, y, resid, xnorm, beta, lams, betas, iters, tol, n, p, nlam)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
(`-M` names the Python module after the file with `-` read as `_`,
|
|
206
|
+
and the loader looks for `liblasso-design.so` beside it.)
|
|
207
|
+
`examples/kernel-only/README.md` walks through the same `-M` flow on
|
|
208
|
+
the row-major kernel, and `python/PACKAGING.md` through turning such
|
|
209
|
+
a kernel into a pip package.
|
|
210
|
+
|
|
165
211
|
## Against the other lassos pip can install
|
|
166
212
|
|
|
167
213
|
`bench/lasso-compare.py` in the repository runs the same
|
|
@@ -235,6 +281,23 @@ the pick is exact at n=5,000 and one grid step apart on a near-tie
|
|
|
235
281
|
elsewhere (mean-MSE relative gap at most 2.6e-4), and sklearn at
|
|
236
282
|
tol=1e-10 picks our alpha at every size.
|
|
237
283
|
|
|
284
|
+
The elastic net cross-validates the same way: `CovLassoCV(l1_ratio=0.5)`
|
|
285
|
+
against `ElasticNetCV(l1_ratio=0.5)` on the same folds and alpha grid
|
|
286
|
+
(`bench/lasso-compare.py --cv-only --l1-ratio 0.5`, same conditions,
|
|
287
|
+
load under 1):
|
|
288
|
+
|
|
289
|
+
| n | p | CPU | CUDA | sklearn `ElasticNetCV` |
|
|
290
|
+
|---------|------|--------|--------|------------------------|
|
|
291
|
+
| 1,800 | 200 | 0.35 s | 0.02 s | 0.11 s |
|
|
292
|
+
| 5,000 | 1000 | 1.55 s | 0.26 s | 0.98 s |
|
|
293
|
+
| 100,000 | 200 | 0.52 s | 0.35 s | 1.76 s |
|
|
294
|
+
| 100,000 | 500 | 1.38 s | 1.15 s | 4.97 s |
|
|
295
|
+
|
|
296
|
+
The picture is the lasso's: on the CPU, 3.4-3.6x at 100,000 rows and
|
|
297
|
+
slower than sklearn at the two small shapes; with CUDA, 3.8-5.5x
|
|
298
|
+
everywhere. The chosen alpha is sklearn's at all four sizes, the
|
|
299
|
+
coefficients within 8e-6 (sklearn's default tolerance).
|
|
300
|
+
|
|
238
301
|
`CovMultiTaskLasso` and `CovMultiTaskLassoCV` are the multi-task
|
|
239
302
|
family over the same machinery: scikit-learn's `MultiTaskLasso`,
|
|
240
303
|
`MultiTaskElasticNet` (through `l1_ratio`), `MultiTaskLassoCV` and
|
|
@@ -319,6 +382,247 @@ ridge = CovRidge(X, y)
|
|
|
319
382
|
betas = ridge.fit_path(ridge.alpha_grid()) # a whole ridge path
|
|
320
383
|
```
|
|
321
384
|
|
|
385
|
+
Measured (`bench/ridge-enet-compare.py` in the repository: AR(1)
|
|
386
|
+
designs, 20 penalties, whole calls with the Gram matrix wherever one
|
|
387
|
+
is built, one core, best of three):
|
|
388
|
+
|
|
389
|
+
| n x p | 1,800 x 200 | 5,000 x 1000 | 100,000 x 200 | 100,000 x 500 |
|
|
390
|
+
|----------------------------------------------|-------------|--------------|---------------|---------------|
|
|
391
|
+
| elastic net, `CovLasso.fit_path(l1_ratio=0.5)`, tol 1e-8 | 0.007 s | 0.092 s | 0.11 s | 0.40 s |
|
|
392
|
+
| elastic net, sklearn `enet_path(precompute=True)`, tol 1e-4 | 0.005 s | 0.099 s | 0.23 s | 0.74 s |
|
|
393
|
+
| ridge, `CovRidge.fit_path(alphas)` | 0.004 s | 0.21 s | 0.10 s | 0.41 s |
|
|
394
|
+
| ridge, sklearn `Ridge(alpha).fit` per alpha | 0.060 s | 2.05 s | 3.37 s | 11.2 s |
|
|
395
|
+
| ridge, sklearn `RidgeCV(alphas)` (its LOO path) | 0.093 s | 3.40 s | 5.38 s | 19.0 s |
|
|
396
|
+
|
|
397
|
+
The elastic net is the same coordinate descent on both sides, so the
|
|
398
|
+
two rows differ by the Gram and the warm path, not the sweeps: level
|
|
399
|
+
at small n, 1.8-2.2x at 100,000 rows, and ours lands within 2e-10 of
|
|
400
|
+
a tight reference where scikit-learn's default tolerance lands within
|
|
401
|
+
6e-4. The ridge rows compare a whole path against what a user
|
|
402
|
+
writes -- one `Ridge.fit` per alpha, each forming X'X and solving
|
|
403
|
+
afresh, O(np^2 + p^3) every time -- so the gap is the path's: 10-33x,
|
|
404
|
+
agreeing to 1e-14. `RidgeCV` is given for scale (it picks one alpha
|
|
405
|
+
by leave-one-out rather than returning the path). Neither row runs
|
|
406
|
+
on the GPU: the ridge is one eigendecomposition and the elastic net's
|
|
407
|
+
sweeps are the lasso's; `fit_path_batch(l1_ratio=..)` is the GPU
|
|
408
|
+
grid for the elastic net.
|
|
409
|
+
|
|
410
|
+
## Bayesian ridge
|
|
411
|
+
|
|
412
|
+
`CovBayesianRidge` is scikit-learn's `BayesianRidge` -- the same model,
|
|
413
|
+
the same defaults, the same iteration (MacKay's updates of the noise
|
|
414
|
+
precision `alpha_` and the weight precision `lambda_`, stopping when
|
|
415
|
+
the posterior mean moves by less than `tol`) -- with the linear
|
|
416
|
+
algebra moved into the eigenbasis of the Gram matrix. scikit-learn
|
|
417
|
+
takes the SVD of X once and multiplies by X again in every iteration
|
|
418
|
+
for the squared error. Here X enters once, through X'X (X X' when
|
|
419
|
+
n <= p): one eigendecomposition, after which the posterior mean, its
|
|
420
|
+
norm and the squared error are sums over the eigenvalues, O(p) per
|
|
421
|
+
iteration. `sigma_` (the posterior covariance) and
|
|
422
|
+
`predict(return_std=True)` are there as in scikit-learn.
|
|
423
|
+
|
|
424
|
+
```python
|
|
425
|
+
from scm2cpp_lasso import CovBayesianRidge
|
|
426
|
+
m = CovBayesianRidge().fit(X, y) # fit_intercept=True, as sklearn
|
|
427
|
+
mean, std = m.predict(X_new, return_std=True)
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
Measured (`bench/bayesian-ridge-compare.py` in the repository:
|
|
431
|
+
Gaussian designs, a sparse truth plus noise, whole fits including
|
|
432
|
+
`sigma_`, one core, best of three, sklearn 1.9.1):
|
|
433
|
+
|
|
434
|
+
| n x p | `CovBayesianRidge` | sklearn `BayesianRidge` | ratio | iterations (both) | max coefficient difference |
|
|
435
|
+
|----------------|--------------------|-------------------------|-------|-------------------|----------------------------|
|
|
436
|
+
| 1,800 x 200 | 0.005 s | 0.026 s | 5.2x | 3 | 3.6e-15 |
|
|
437
|
+
| 5,000 x 1000 | 0.257 s | 1.231 s | 4.8x | 4 | 6.7e-15 |
|
|
438
|
+
| 100,000 x 200 | 0.126 s | 3.495 s | 27.8x | 3 | 2.8e-14 |
|
|
439
|
+
| 100,000 x 500 | 0.475 s | 8.909 s | 18.8x | 3 | 1.6e-14 |
|
|
440
|
+
| 500 x 5000 | 0.662 s | 8.689 s | 13.1x | 71 | 1.7e-15 |
|
|
441
|
+
|
|
442
|
+
The iteration counts are identical and the coefficients agree to
|
|
443
|
+
rounding, so the gap is the linear algebra alone: X'X by one BLAS
|
|
444
|
+
product against a full SVD of the tall X, and no pass over X inside
|
|
445
|
+
the loop. At 500 x 5000 scikit-learn takes the full 5000 x 5000 SVD
|
|
446
|
+
for `sigma_`; here `sigma_` comes from the 500 x 500 kernel side by
|
|
447
|
+
the Woodbury identity. This estimator is numpy, not translated code:
|
|
448
|
+
the eigendecomposition is LAPACK's either way.
|
|
449
|
+
|
|
450
|
+
## Classifiers and SVRs: `LinearSVM`, `KernelSVM`, `LinearSVR`, `KernelSVR`, `L1LogisticRegression`
|
|
451
|
+
|
|
452
|
+
Three classifiers and two regressors, each the algorithm scikit-learn runs, written out
|
|
453
|
+
in the Scheme subset and translated plainly (no derivation; the C++
|
|
454
|
+
is committed under `_generated/classifiers*`):
|
|
455
|
+
|
|
456
|
+
| class | algorithm | scikit-learn counterpart |
|
|
457
|
+
|---|---|---|
|
|
458
|
+
| `LinearSVM` | liblinear's dual coordinate descent for the hinge loss (random order, shrinking) | `LinearSVC(loss="hinge", dual=True, fit_intercept=False)` |
|
|
459
|
+
| `KernelSVM` | libsvm's SMO (WSS3, shrinking, the bias) over the whole kernel matrix | `SVC` (`kernel="rbf"`, `"linear"` or `"precomputed"`) |
|
|
460
|
+
| `L1LogisticRegression` | liblinear's newGLMNET (Newton over coordinate descent, line search) | `LogisticRegression(l1_ratio=1, solver="liblinear", fit_intercept=False)` |
|
|
461
|
+
| `LinearSVR` | liblinear's dual coordinate descent for the epsilon-insensitive loss (random order, shrinking) | `LinearSVR(loss="epsilon_insensitive", dual=True, fit_intercept=False)` |
|
|
462
|
+
| `KernelSVR` | the same SMO as `KernelSVM`, set up as libsvm's epsilon-SVR (2n variables) | `SVR` (`kernel="rbf"`, `"linear"` or `"precomputed"`) |
|
|
463
|
+
|
|
464
|
+
```python
|
|
465
|
+
from scm2cpp_lasso import LinearSVM, KernelSVM, L1LogisticRegression
|
|
466
|
+
LinearSVM(C=1.0).fit(X, y).predict(X_new)
|
|
467
|
+
KernelSVM(C=1.0, gamma="scale").fit(X, y).decision_function(X_new)
|
|
468
|
+
L1LogisticRegression(C=0.1).fit(X, y).predict_proba(X_new)
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
The classifiers take two classes (the second of `classes_` is the
|
|
472
|
+
positive one), and `tol` / `max_iter` mean what they mean in scikit-learn.
|
|
473
|
+
`KernelSVM` and `KernelSVR` build the kernel matrix in numpy and keep all of it,
|
|
474
|
+
n x n doubles (800 MB at n = 10,000), where `SVC` computes rows on
|
|
475
|
+
demand through a cache; that is its price.
|
|
476
|
+
|
|
477
|
+
Measured with `check-classifiers.py` (in this directory): the same
|
|
478
|
+
problem and stopping rule on both sides, one core, best of three,
|
|
479
|
+
sklearn 1.9.1, load average under 2. Ours includes building the kernel
|
|
480
|
+
matrix (`KernelSVM`) or transposing X into columns
|
|
481
|
+
(`L1LogisticRegression`):
|
|
482
|
+
|
|
483
|
+
| problem | ours | scikit-learn | ratio | objective (ours / sklearn) | same predictions |
|
|
484
|
+
|---|---|---|---|---|---|
|
|
485
|
+
| `LinearSVM`, 5,000 x 200 | 0.379 s | 0.598 s | 1.6x | 1541.06 / 1541.29 | 99.5% |
|
|
486
|
+
| `LinearSVM`, 100,000 x 200 | 23.0 s | 41.7 s | 1.8x | 34503.5 / 34500.7 | 98.9% |
|
|
487
|
+
| `LinearSVM`, 2,000 x 20,000 | 0.975 s | 1.894 s | 1.9x | 0.052484 / 0.052428 | 100% |
|
|
488
|
+
| `KernelSVM` rbf, 2,000 x 50 | 0.068 s | 0.139 s (precomputed 0.081 s) | 2.1x (1.2x) | -1065.5026 / -1065.5026 | 100% |
|
|
489
|
+
| `KernelSVM` rbf, 5,000 x 50 | 0.501 s | 1.032 s (precomputed 0.588 s) | 2.1x (1.2x) | -2628.1422 / -2628.1422 | 100% |
|
|
490
|
+
| `KernelSVM` rbf, 10,000 x 50 | 2.12 s | 4.15 s (precomputed 2.61 s) | 2.0x (1.2x) | -4994.9982 / -4994.9982 | 100% |
|
|
491
|
+
| `L1LogisticRegression`, 5,000 x 200, C=0.1 | 0.064 s | 0.103 s | 1.6x | 61.676698 / 61.676708 | 100% |
|
|
492
|
+
| `L1LogisticRegression`, 20,000 x 500, C=0.05 | 1.20 s | 2.23 s | 1.9x | 106.77913 / 106.77915 | 100% |
|
|
493
|
+
| `L1LogisticRegression`, 100,000 x 200, C=0.05 | 9.06 s | 18.7 s | 2.1x | 349.61154 / 349.61216 | 100% |
|
|
494
|
+
|
|
495
|
+
The regressors, from the same script (C = 1, epsilon 0.1):
|
|
496
|
+
|
|
497
|
+
| problem | ours | scikit-learn | ratio | agreement |
|
|
498
|
+
|---|---|---|---|---|
|
|
499
|
+
| `LinearSVR`, 5,000 x 200 | 1.32 s | 2.53 s | 1.9x | both at the 1,000-sweep cap; objective 779.99 / 790.72 |
|
|
500
|
+
| `LinearSVR`, 20,000 x 500 | 22.1 s | 36.6 s | 1.7x | both at the cap; objective 3741.4 / 3712.2 |
|
|
501
|
+
| `KernelSVR` rbf, 1,000 x 10 | 0.023 s | 0.043 s | 1.9x | iterations 2,172 / 2,177; predictions within 4e-4 |
|
|
502
|
+
| `KernelSVR` rbf, 3,000 x 10 | 0.246 s | 0.332 s | 1.3x | iterations 7,471 / 7,581; within 5e-4 |
|
|
503
|
+
| `KernelSVR` rbf, 6,000 x 10 | 1.18 s | 1.38 s | 1.2x | iterations 16,805 / 16,630; within 5e-4 |
|
|
504
|
+
|
|
505
|
+
`LinearSVR` on these data does not reach its tolerance in 1,000 sweeps on
|
|
506
|
+
either side, and at the cap the random order alone moves the objective
|
|
507
|
+
by about 1% (scikit-learn's own seeds give 776-789 at 5,000 x 200);
|
|
508
|
+
with 20,000 sweeps both land on 728.58. `KernelSVR`'s gain shrinks
|
|
509
|
+
with n here because the 10 features make each kernel row cheap for
|
|
510
|
+
libsvm to compute, while ours reads the rows of a matrix of n^2
|
|
511
|
+
doubles; the predictions agree to within scikit-learn's tolerance.
|
|
512
|
+
|
|
513
|
+
The objectives agree to the stopping tolerance on both sides, not to
|
|
514
|
+
rounding. The linear SVM on tall data stops at the 1,000-sweep cap on
|
|
515
|
+
both sides, so there the two land a hair apart and 1% of the
|
|
516
|
+
predictions differ. The gain is the same in all three: the same
|
|
517
|
+
algorithm, run as dense loops that vectorise, against libsvm's and
|
|
518
|
+
liblinear's sparse rows. The classifiers are compiled at `-O3` with
|
|
519
|
+
`-fstrict-overflow`: Python's build flags carry
|
|
520
|
+
`-fno-strict-overflow`, which keeps the compiler from treating the
|
|
521
|
+
flat index `i*p + j` as an induction variable and makes the linear
|
|
522
|
+
SVM 25% slower.
|
|
523
|
+
|
|
524
|
+
## NMF: `CDNMF`
|
|
525
|
+
|
|
526
|
+
`CDNMF` is scikit-learn's `NMF(solver="cd")` -- Frobenius loss, no
|
|
527
|
+
regularization, no shuffling -- as the translator derives it.
|
|
528
|
+
`examples/kernel-only/nmf-cd.scm` writes the half step with the
|
|
529
|
+
residual R = X - W H and no Gram matrix. `--derive` memoises R H' over
|
|
530
|
+
the hoisted H H' and distributes the residual through the memo, so the
|
|
531
|
+
compiled half step is scikit-learn's `XHt - W HHt` with R never formed.
|
|
532
|
+
Like `DerivedLasso`, it is built in three variants (`nmf_backend()`
|
|
533
|
+
says which is taken: cublas when a device answers, else blas, else
|
|
534
|
+
loops).
|
|
535
|
+
|
|
536
|
+
```python
|
|
537
|
+
from scm2cpp_lasso import CDNMF
|
|
538
|
+
model = CDNMF(n_components=20, random_state=0) # init="random", as sklearn's
|
|
539
|
+
W = model.fit_transform(X) # H in model.components_
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
`init="random"` draws the start as scikit-learn does, so the same seed
|
|
543
|
+
gives the same W and H. scikit-learn's default `"nndsvda"` is not
|
|
544
|
+
offered; pass `init="custom"` with its W and H for that start.
|
|
545
|
+
Measured with `check-classifiers.py --only nmf` against
|
|
546
|
+
`NMF(solver="cd", init="random")` from the same seed, tol 1e-4, at most
|
|
547
|
+
200 iterations, one core:
|
|
548
|
+
|
|
549
|
+
| n x p, k | `CDNMF` cuBLAS | `CDNMF` BLAS | sklearn | iterations | max \|H - H_sklearn\| |
|
|
550
|
+
|---|---|---|---|---|---|
|
|
551
|
+
| 1,000 x 500, 10 | 0.441 s (0.6x) | 0.212 s (1.15x) | 0.244 s | 200 / 200 | 1.3e-13 |
|
|
552
|
+
| 5,000 x 2,000, 20 | 7.01 s (1.6x) | 8.27 s (1.36x) | 11.25 s | 200 / 200 | 4.1e-12 |
|
|
553
|
+
| 20,000 x 1,000, 30 | 16.9 s (1.8x) | 20.6 s (1.46x) | 30.1 s | 200 / 200 | 2.4e-12 |
|
|
554
|
+
|
|
555
|
+
The iterates are scikit-learn's to rounding. The cuBLAS variant pays
|
|
556
|
+
from the middle size on; at 1,000 x 500 its transfers cost more than
|
|
557
|
+
the products save, and `backend="blas"` is the faster choice there.
|
|
558
|
+
|
|
559
|
+
## Graphical lasso and RANSAC: `CDGraphicalLasso`, `LinearRANSAC`
|
|
560
|
+
|
|
561
|
+
Two more of scikit-learn's algorithms, written out in the Scheme subset
|
|
562
|
+
and translated plainly.
|
|
563
|
+
|
|
564
|
+
- `CDGraphicalLasso` is `GraphicalLasso(mode="cd")`: `_graphical_lasso`
|
|
565
|
+
with `enet_coordinate_descent_gram` inside it, its two matrix-vector
|
|
566
|
+
products as dgemv. `alpha`, `tol`, `enet_tol`, `max_iter` and
|
|
567
|
+
`assume_centered` mean what they mean in scikit-learn. scikit-learn's
|
|
568
|
+
`costs_` (a log-determinant every iteration) is not computed.
|
|
569
|
+
- `LinearRANSAC` is `RANSACRegressor` around `LinearRegression`, with
|
|
570
|
+
the same defaults. With scikit-learn installed, the subsets are drawn
|
|
571
|
+
exactly as it draws them, so `random_state` gives scikit-learn's fit.
|
|
572
|
+
|
|
573
|
+
```python
|
|
574
|
+
from scm2cpp_lasso import CDGraphicalLasso, LinearRANSAC
|
|
575
|
+
prec = CDGraphicalLasso(alpha=0.1).fit(X).precision_
|
|
576
|
+
fit = LinearRANSAC(random_state=0).fit(X, y) # fit.coef_, fit.inlier_mask_
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
Measured through the package, one core, best of three, against
|
|
580
|
+
scikit-learn on the same data (an AR(1) design with n = 20p for the
|
|
581
|
+
graphical lasso, 100 iterations on both sides; a linear model with
|
|
582
|
+
outliers for RANSAC, max_trials 1000):
|
|
583
|
+
|
|
584
|
+
| problem | ours | scikit-learn | ratio | agreement |
|
|
585
|
+
|---|---|---|---|---|
|
|
586
|
+
| `CDGraphicalLasso`, p = 50 | 0.0069 s | 0.306 s | 44.5x | 100 / 100 iterations, precision to 1.8e-14 |
|
|
587
|
+
| `CDGraphicalLasso`, p = 200 | 0.286 s | 1.62 s | 5.7x | precision to 2.6e-14 |
|
|
588
|
+
| `CDGraphicalLasso`, p = 800 | 33.9 s | 43.8 s | 1.3x | precision to 5.0e-14 |
|
|
589
|
+
| `LinearRANSAC`, 10,000 x 5 | 0.0254 s | 0.0916 s | 3.6x | 98 / 98 trials, same inliers |
|
|
590
|
+
| `LinearRANSAC`, 100,000 x 10 | 0.153 s | 0.241 s | 1.6x | 97 / 97 trials, same inliers |
|
|
591
|
+
| `LinearRANSAC`, 20,000 x 20 | 0.470 s | 1.19 s | 2.5x | 1,000 / 1,000 trials, same inliers |
|
|
592
|
+
|
|
593
|
+
Both gains are mostly scikit-learn's Python between its compiled steps:
|
|
594
|
+
the per-column bookkeeping of the graphical lasso, and the per-trial
|
|
595
|
+
validation and fitting of RANSAC. They shrink as the compiled part
|
|
596
|
+
grows (the graphical lasso at large p).
|
|
597
|
+
|
|
598
|
+
## SGD, OMP and LARS: `SGDLinearClassifier`, `SGDLinearRegressor`, `GramOMP`, `GramLars`, `GramLassoLars`
|
|
599
|
+
|
|
600
|
+
scikit-learn's own algorithms, translated:
|
|
601
|
+
|
|
602
|
+
- `SGDLinearClassifier` and `SGDLinearRegressor` are
|
|
603
|
+
`SGDClassifier` / `SGDRegressor` at their defaults, run by
|
|
604
|
+
`_plain_sgd` with the points in order. Against
|
|
605
|
+
`shuffle=False` the weights are bit-identical. scikit-learn's
|
|
606
|
+
shuffling generator is not reproduced.
|
|
607
|
+
- `GramOMP` is `OrthogonalMatchingPursuit` over the Gram matrix (its
|
|
608
|
+
route for n > p, taken here always; X'X and X'y by dsyrk and dgemv).
|
|
609
|
+
- `GramLars` and `GramLassoLars` are `Lars` and `LassoLars` over the
|
|
610
|
+
Gram (their route for n > p, taken here always; the Gram formed in
|
|
611
|
+
numpy).
|
|
612
|
+
|
|
613
|
+
```python
|
|
614
|
+
from scm2cpp_lasso import SGDLinearClassifier, GramOMP, GramLassoLars
|
|
615
|
+
SGDLinearClassifier().fit(X, y).predict(X_new)
|
|
616
|
+
GramOMP(n_nonzero_coefs=10).fit(X, y).coef_
|
|
617
|
+
GramLassoLars(alpha=0.01).fit(X, y).coef_
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
Each gives scikit-learn's coefficients, to the bit for SGD and to 1e-15
|
|
621
|
+
for the rest, with the same number of epochs or steps. The speed is the
|
|
622
|
+
repository README's table: 1.0-3.3x for SGD, 1.1-2.8x for OMP and
|
|
623
|
+
1.2-5.0x for LARS, largest on small problems, where scikit-learn's
|
|
624
|
+
Python between steps dominates.
|
|
625
|
+
|
|
322
626
|
## Logistic regression with L1
|
|
323
627
|
|
|
324
628
|
`CovLogistic` solves L1-penalized logistic regression by
|