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.
Files changed (57) hide show
  1. {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/PKG-INFO +308 -4
  2. {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/README.md +305 -1
  3. {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/pyproject.toml +4 -4
  4. {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/setup.py +63 -1
  5. {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/__init__.py +714 -3
  6. scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/_loader_classifiers.py +190 -0
  7. scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/_loader_gramops_blas.py +75 -0
  8. scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/_loader_gramops_loops.py +75 -0
  9. scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/_loader_nmf_blas.py +29 -0
  10. scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/_loader_nmf_cublas.py +29 -0
  11. scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/_loader_nmf_loops.py +29 -0
  12. scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/classifiers.cpp +23 -0
  13. scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/classifiers.hpp +1950 -0
  14. scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/classifiers_capi.cpp +73 -0
  15. {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/_generated/derived_blas/lasso_design.hpp +12 -19
  16. {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/_generated/derived_cublas/lasso_design.hpp +13 -20
  17. {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/_generated/derived_loops/lasso_design.hpp +24 -26
  18. scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/gramops_blas/gramops.cpp +9 -0
  19. scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/gramops_blas/gramops.hpp +469 -0
  20. scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/gramops_blas/gramops_capi.cpp +31 -0
  21. scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/gramops_loops/gramops.cpp +9 -0
  22. scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/gramops_loops/gramops.hpp +499 -0
  23. scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/gramops_loops/gramops_capi.cpp +31 -0
  24. scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/nmf_blas/nmf_cd.cpp +5 -0
  25. scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/nmf_blas/nmf_cd.hpp +132 -0
  26. scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/nmf_blas/nmf_cd_capi.cpp +19 -0
  27. scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/nmf_cublas/nmf_cd.cpp +5 -0
  28. scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/nmf_cublas/nmf_cd.hpp +136 -0
  29. scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/nmf_cublas/nmf_cd_capi.cpp +19 -0
  30. scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/nmf_loops/nmf_cd.cpp +5 -0
  31. scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/nmf_loops/nmf_cd.hpp +167 -0
  32. scm2cpp_lasso-0.9.0/src/scm2cpp_lasso/_generated/nmf_loops/nmf_cd_capi.cpp +19 -0
  33. {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/_libfind.py +44 -0
  34. {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso.egg-info/PKG-INFO +308 -4
  35. {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso.egg-info/SOURCES.txt +25 -1
  36. {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/setup.cfg +0 -0
  37. {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/_generated/__init__.py +0 -0
  38. {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/_generated/_loader.py +0 -0
  39. {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/_generated/_loader_derived_blas.py +0 -0
  40. {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/_generated/_loader_derived_cublas.py +0 -0
  41. {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/_generated/_loader_derived_loops.py +0 -0
  42. {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/_generated/derived_blas/lasso_design.cpp +0 -0
  43. {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/_generated/derived_blas/lasso_design_capi.cpp +0 -0
  44. {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/_generated/derived_cublas/lasso_design.cpp +0 -0
  45. {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/_generated/derived_cublas/lasso_design_capi.cpp +0 -0
  46. {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/_generated/derived_loops/lasso_design.cpp +0 -0
  47. {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/_generated/derived_loops/lasso_design_capi.cpp +0 -0
  48. {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/_generated/lasso_cov.cpp +0 -0
  49. {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/_generated/lasso_cov.hpp +0 -0
  50. {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/_generated/lasso_cov_capi.cpp +0 -0
  51. {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/_generated/scm2cpp-blas.hpp +0 -0
  52. {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/_generated/scm2cpp-cublas.hpp +0 -0
  53. {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/_generated/scm2cpp.hpp +0 -0
  54. {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso/batch_capi.cu +0 -0
  55. {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso.egg-info/dependency_links.txt +0 -0
  56. {scm2cpp_lasso-0.8.0 → scm2cpp_lasso-0.9.0}/src/scm2cpp_lasso.egg-info/requires.txt +0 -0
  57. {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.8.0
4
- Summary: Lasso by covariance-update coordinate descent, translated from Scheme to C++ by scm2cpp; GPU batch solver included
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). Against a reference driven to
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