scm2cpp-learn 0.18.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_learn-0.18.0/PKG-INFO +1171 -0
- scm2cpp_learn-0.18.0/README.md +1153 -0
- scm2cpp_learn-0.18.0/pyproject.toml +34 -0
- scm2cpp_learn-0.18.0/setup.cfg +4 -0
- scm2cpp_learn-0.18.0/setup.py +356 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/__init__.py +3804 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_compat.py +284 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/__init__.py +0 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/_loader.py +41 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/_loader_accel_cublas.py +173 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/_loader_classifiers.py +308 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/_loader_derived_blas.py +29 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/_loader_derived_cublas.py +29 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/_loader_derived_loops.py +29 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/_loader_gpu_cuda.py +313 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/_loader_gramops_blas.py +692 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/_loader_gramops_loops.py +711 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/_loader_nmf_blas.py +29 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/_loader_nmf_cublas.py +29 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/_loader_nmf_loops.py +29 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/accel_cublas/accel.cpp +24 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/accel_cublas/accel.hpp +1178 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/accel_cublas/accel_capi.cpp +76 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/classifiers.cpp +37 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/classifiers.hpp +3745 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/classifiers_capi.cpp +115 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/derived_blas/lasso_design.cpp +5 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/derived_blas/lasso_design.hpp +156 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/derived_blas/lasso_design_capi.cpp +19 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/derived_cublas/lasso_design.cpp +5 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/derived_cublas/lasso_design.hpp +158 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/derived_cublas/lasso_design_capi.cpp +19 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/derived_loops/lasso_design.cpp +5 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/derived_loops/lasso_design.hpp +179 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/derived_loops/lasso_design_capi.cpp +19 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/gpu_cuda/gpu.cpp +37 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/gpu_cuda/gpu.hpp +3846 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/gpu_cuda/gpu_capi.cpp +308 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/gramops_blas/gramops.cpp +82 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/gramops_blas/gramops.hpp +5411 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/gramops_blas/gramops_capi.cpp +244 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/gramops_loops/gramops.cpp +82 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/gramops_loops/gramops.hpp +6395 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/gramops_loops/gramops_capi.cpp +250 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/lasso_cov.cpp +7 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/lasso_cov.hpp +152 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/lasso_cov_capi.cpp +25 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/nmf_blas/nmf_cd.cpp +5 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/nmf_blas/nmf_cd.hpp +132 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/nmf_blas/nmf_cd_capi.cpp +19 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/nmf_cublas/nmf_cd.cpp +5 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/nmf_cublas/nmf_cd.hpp +136 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/nmf_cublas/nmf_cd_capi.cpp +19 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/nmf_loops/nmf_cd.cpp +5 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/nmf_loops/nmf_cd.hpp +167 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/nmf_loops/nmf_cd_capi.cpp +19 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/scm2cpp-blas.hpp +158 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/scm2cpp-cublas.hpp +155 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/scm2cpp-cuda.hpp +285 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_generated/scm2cpp.hpp +1627 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/_libfind.py +153 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn/batch_capi.cu +428 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn.egg-info/PKG-INFO +1171 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn.egg-info/SOURCES.txt +65 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn.egg-info/dependency_links.txt +1 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn.egg-info/requires.txt +1 -0
- scm2cpp_learn-0.18.0/src/scm2cpp_learn.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,1171 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: scm2cpp-learn
|
|
3
|
+
Version: 0.18.0
|
|
4
|
+
Summary: scikit-learn estimators (lasso, elastic net, ridge, RidgeCV, ARD, SVM, SVR, logistic regression, GLMs, Huber, Theil-Sen, F-score selection, RFE, NMF, Gaussian mixture, FastICA, PCA, KernelPCA, LDA, TruncatedSVD, graphical lasso, RANSAC, SGD, OMP, LARS) translated from Scheme to C++ by scm2cpp; GPU batch solver and CUDA kernels included
|
|
5
|
+
Author: Hirotaka Niitsuma
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/niitsuma/scm2cpp
|
|
8
|
+
Project-URL: Source, https://github.com/niitsuma/scm2cpp/tree/master/python/scm2cpp-learn
|
|
9
|
+
Keywords: scikit-learn,lasso,elastic-net,ridge,svm,logistic-regression,pca,lda,coordinate-descent,lapack,cuda,compiler
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: C++
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Requires-Python: >=3.8
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: numpy>=1.17
|
|
18
|
+
|
|
19
|
+
# scm2cpp-learn
|
|
20
|
+
|
|
21
|
+
[](https://colab.research.google.com/github/niitsuma/scm2cpp/blob/master/notebooks/compare-sklearn.ipynb)
|
|
22
|
+
|
|
23
|
+
*[Japanese (README.ja.md)](README.ja.md)*
|
|
24
|
+
|
|
25
|
+
About fifty scikit-learn estimators -- the lasso and its relatives,
|
|
26
|
+
ridge and RidgeCV, SVMs, GLMs, PCA, KernelPCA, LDA, TruncatedSVD,
|
|
27
|
+
mixtures, feature selection and more -- each written in Scheme and
|
|
28
|
+
translated to C++ by [scm2cpp](https://github.com/niitsuma/scm2cpp),
|
|
29
|
+
with BLAS/LAPACK, OpenMP and an optional GPU path.
|
|
30
|
+
|
|
31
|
+
Up to 0.17.0 this package was published as `scm2cpp-lasso` (imported as
|
|
32
|
+
`scm2cpp_lasso`). That name still installs it: `scm2cpp-lasso` 0.18.0
|
|
33
|
+
depends on this package, and `import scm2cpp_lasso` gives this module,
|
|
34
|
+
with a DeprecationWarning.
|
|
35
|
+
|
|
36
|
+
```console
|
|
37
|
+
$ pip install scm2cpp-learn
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
On Linux x86_64, macOS arm64 (macOS 14 or later) and Windows AMD64
|
|
41
|
+
this installs a wheel: compiled, nothing to build (with OpenBLAS
|
|
42
|
+
inside on Linux and macOS; Windows has the loop variants). Elsewhere pip builds the sdist,
|
|
43
|
+
which needs a C++17 compiler and nothing else. The wheels carry no
|
|
44
|
+
GPU code (a wheel cannot know the device); for the GPU builds, install
|
|
45
|
+
from the sdist on a machine with `nvcc` on the path:
|
|
46
|
+
|
|
47
|
+
```console
|
|
48
|
+
$ pip install --no-binary scm2cpp-learn scm2cpp-learn
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Built from the sdist, the products and the eigen- and singular value
|
|
52
|
+
problems go to Intel MKL where pkg-config knows it
|
|
53
|
+
(`mkl-dynamic-lp64-gomp`, GNU OpenMP threading, as Gentoo's
|
|
54
|
+
`sci-libs/mkl` installs it), else to an OpenBLAS with LAPACKE, else to
|
|
55
|
+
the translated loops; `SCM2CPP_NO_MKL=1` skips MKL. The translated
|
|
56
|
+
loops that the dependence test proves independent run on OpenMP where
|
|
57
|
+
the compiler has it (`SCM2CPP_NO_OPENMP=1` builds them serial). Both
|
|
58
|
+
use every core unless told otherwise: `OMP_NUM_THREADS` (and
|
|
59
|
+
`MKL_NUM_THREADS` / `OPENBLAS_NUM_THREADS`) set the threads.
|
|
60
|
+
|
|
61
|
+
The batched GPU solver, the cuBLAS variants and the `--cuda`
|
|
62
|
+
estimators are then built as well; without `nvcc` the package installs
|
|
63
|
+
and works exactly the same, minus those.
|
|
64
|
+
|
|
65
|
+
Three of a hundred candidate columns carry the signal; the solver is
|
|
66
|
+
asked which, and answers with them and nothing else:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
import numpy as np
|
|
70
|
+
from scm2cpp_learn import CovLasso
|
|
71
|
+
|
|
72
|
+
rng = np.random.default_rng(0)
|
|
73
|
+
X = rng.standard_normal((500, 100)) # 500 rows, 100 candidates
|
|
74
|
+
beta = np.zeros(100)
|
|
75
|
+
beta[[7, 23, 61]] = [2.0, -1.5, 0.8] # only three of them matter
|
|
76
|
+
y = X @ beta + 0.1 * rng.standard_normal(500)
|
|
77
|
+
|
|
78
|
+
model = CovLasso(X, y) # the Gram matrix, built once
|
|
79
|
+
path = model.fit_path(model.lambda_grid()) # 100 lambdas, warm-started
|
|
80
|
+
|
|
81
|
+
fit = path[60]
|
|
82
|
+
for j in np.flatnonzero(np.abs(fit) > 1e-6):
|
|
83
|
+
print(f" x{j:<3} {fit[j]:+.3f}")
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```console
|
|
87
|
+
x7 +1.965
|
|
88
|
+
x23 -1.474
|
|
89
|
+
x61 +0.770
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The other 97 are exactly zero -- that is what the penalty is for -- and
|
|
93
|
+
the coefficients are the planted ones, shrunk toward zero by it.
|
|
94
|
+
|
|
95
|
+
The rest of the interface:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from scm2cpp_learn import cuda_available
|
|
99
|
+
|
|
100
|
+
lambdas = model.lambda_grid(num=100) # from lambda_max down
|
|
101
|
+
path = model.fit_path(lambdas) # warm-started, sequential
|
|
102
|
+
grid = model.fit_path_batch(lambdas) # every lambda from zero
|
|
103
|
+
one = model.fit(lambdas[60]) # a single lambda, if you must
|
|
104
|
+
print("GPU:", cuda_available())
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`fit` is the one-element special case of `fit_path` -- the Gram matrix
|
|
108
|
+
was built in the constructor, so a whole warm-started path costs barely
|
|
109
|
+
more than one penalty, which is why the path is the primary interface.
|
|
110
|
+
|
|
111
|
+
The objective is scikit-learn's, with `fit_intercept=False`:
|
|
112
|
+
|
|
113
|
+
(1 / 2 nobs) ||y - X b||^2 + lam ||b||_1
|
|
114
|
+
|
|
115
|
+
Penalties are compared against correlations on the features' own
|
|
116
|
+
scale, so the useful range of `lam` depends on the data.
|
|
117
|
+
`lambda_max()` is the smallest penalty that leaves every coefficient
|
|
118
|
+
at zero, and `lambda_grid()` walks down from it -- the construction
|
|
119
|
+
scikit-learn uses. If your columns differ wildly in scale, standardize
|
|
120
|
+
them first; this solver does not do it for you.
|
|
121
|
+
|
|
122
|
+
## scikit-learn compatibility
|
|
123
|
+
|
|
124
|
+
From 0.14.0 every estimator with a `fit(X, y)` (or `fit(X)`) derives
|
|
125
|
+
from scikit-learn's `BaseEstimator` and the matching mixin, so `clone`,
|
|
126
|
+
`get_params` / `set_params`, `GridSearchCV`, `Pipeline` and
|
|
127
|
+
`cross_val_score` take them, and all 44 pass
|
|
128
|
+
`sklearn.utils.estimator_checks.check_estimator` (`check-sklearn.py`
|
|
129
|
+
runs it; the one expected failure is `LBFGSLogisticRegressionCV`'s
|
|
130
|
+
`n_iter_` of 0 for a C whose warm start is already converged, which
|
|
131
|
+
scikit-learn's own `LogisticRegressionCV` reports on the same data).
|
|
132
|
+
scikit-learn stays optional: without it a numpy-only stand-in gives
|
|
133
|
+
`get_params`, `set_params`, `score` and the same input checks.
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from sklearn.model_selection import GridSearchCV
|
|
137
|
+
from scm2cpp_learn import GramLasso
|
|
138
|
+
|
|
139
|
+
GridSearchCV(GramLasso(), {"alpha": [0.01, 0.1, 1.0]}, cv=5).fit(X, y)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`CovLasso`, `CovRidge`, `DerivedLasso`, `CovLogistic`,
|
|
143
|
+
`CovGroupLasso` and `CovMultiTaskLasso` keep their
|
|
144
|
+
data-in-the-constructor interface, which solves a whole path at once;
|
|
145
|
+
each has a `fit(X, y)` form with the intercept beside it (0.15.0):
|
|
146
|
+
|
|
147
|
+
| data in the constructor | `fit(X, y)` | scikit-learn |
|
|
148
|
+
|---|---|---|
|
|
149
|
+
| `CovLasso` | `GramLasso`, `GramElasticNet` | Lasso, ElasticNet |
|
|
150
|
+
| `CovRidge` | `GramRidge` | Ridge |
|
|
151
|
+
| `DerivedLasso` | `DesignLasso` | Lasso |
|
|
152
|
+
| `CovLogistic` | `GramLogisticRegression` | LogisticRegression(l1_ratio=1, solver="liblinear") |
|
|
153
|
+
| `CovGroupLasso` | `GramGroupLasso` (`groups=None`: the lasso) | (none) |
|
|
154
|
+
| `CovMultiTaskLasso` | `GramMultiTaskLasso`, `GramMultiTaskElasticNet` | MultiTaskLasso, MultiTaskElasticNet |
|
|
155
|
+
|
|
156
|
+
Against scikit-learn on 400 x 30 with an intercept: `DesignLasso` and
|
|
157
|
+
`GramGroupLasso(groups=None)` agree with `Lasso` to 1.5e-10, the
|
|
158
|
+
multi-task pair with `MultiTaskLasso` / `MultiTaskElasticNet` to
|
|
159
|
+
5e-14, and `GramLogisticRegression` reaches liblinear's objective to
|
|
160
|
+
eight digits with the same predictions.
|
|
161
|
+
|
|
162
|
+
Against the class each one wraps (one alpha, one core), which fits no
|
|
163
|
+
intercept and is unchanged: with `fit_intercept=False` the input checks
|
|
164
|
+
add 15 to 30 us a call (up to +50% on a 0.06 ms fit) and the finiteness
|
|
165
|
+
sum 8 to 14% on 20000 x 200 (none under `assume_finite`); with the
|
|
166
|
+
intercept, the column means add 20 to 30% on 20000 x 200 for the
|
|
167
|
+
Gram-matrix ones, which correct X'X
|
|
168
|
+
for the means rather than copy X to centre it, and about 55% for
|
|
169
|
+
`DesignLasso`, whose kernel needs the centred X itself.
|
|
170
|
+
`GramLogisticRegression` is slower than liblinear by its algorithm, 2.5x
|
|
171
|
+
at 20000 x 200 and more on small problems; `L1LogisticRegression` is
|
|
172
|
+
the fast one for that problem.
|
|
173
|
+
|
|
174
|
+
What it costs is the input checks: X must be a finite 2-D float64
|
|
175
|
+
array with the feature count of the fit. A plain C-ordered float64
|
|
176
|
+
ndarray is checked directly (one sum for finiteness); anything else (a
|
|
177
|
+
list, a DataFrame, another dtype) goes through scikit-learn's
|
|
178
|
+
`validate_data`, which raises the errors scikit-learn raises. The
|
|
179
|
+
checks alone, per call (`bench/sklearn-compat-overhead.py --checks`):
|
|
180
|
+
|
|
181
|
+
| X | fit | predict | predict, 50 rows | validate_data |
|
|
182
|
+
|---|---|---|---|---|
|
|
183
|
+
| 200 x 10 | 13 us | 7 us | 9 us | 173 us |
|
|
184
|
+
| 2000 x 50 | 47 us | 36 us | 9 us | 224 us |
|
|
185
|
+
| 20000 x 200 | 2.7 ms | 3.2 ms | 13 us | 3.2 ms |
|
|
186
|
+
|
|
187
|
+
The finiteness sum is the size-dependent part; under
|
|
188
|
+
`sklearn.set_config(assume_finite=True)` every row is about 3 us.
|
|
189
|
+
Whole calls, before and after (`bench/sklearn-compat-overhead.py`, 30
|
|
190
|
+
estimators, three shapes, the two packages alternating on one core of
|
|
191
|
+
a loaded machine): fits of a millisecond or more are unchanged within
|
|
192
|
+
the run-to-run spread (about 10%); fits of about 0.1 ms take 10 to
|
|
193
|
+
30% (10 to 40 us) longer; predict on 20000 x 200 takes about twice as
|
|
194
|
+
long (3 ms to 6 ms, the finiteness sum costing as much as the product
|
|
195
|
+
itself), and on small inputs 5 to 10 us longer.
|
|
196
|
+
|
|
197
|
+
## Estimators at a glance
|
|
198
|
+
|
|
199
|
+
Every estimator, the scikit-learn estimator it reproduces, the Scheme
|
|
200
|
+
kernel under `examples/kernel-only/` it is translated from, the builds
|
|
201
|
+
it runs on, and whether Intel's sklearnex accelerates the scikit-learn
|
|
202
|
+
side. Builds: *loops* is the plain translation, *BLAS* / *cuBLAS* the
|
|
203
|
+
products handed to (cu)BLAS, *--cuda* the translator's GPU kernels
|
|
204
|
+
(`gpu_cuda/`), *GPU batch* the hand-written CUDA batch solver; the
|
|
205
|
+
package picks the fastest one present.
|
|
206
|
+
|
|
207
|
+
| class | scikit-learn | Scheme kernel | builds | sklearnex accelerates |
|
|
208
|
+
|---|---|---|---|---|
|
|
209
|
+
| `GramLasso` | Lasso(precompute=True), fit(X, y) with the intercept | `lasso-cov.scm` | CPU | no |
|
|
210
|
+
| `GramElasticNet` | ElasticNet(precompute=True), fit(X, y) with the intercept | `lasso-cov.scm` | CPU | no |
|
|
211
|
+
| `GramRidge` | Ridge, fit(X, y) with the intercept, one or many targets | `gram.scm` (Cholesky; dposv) | loops / BLAS | Ridge |
|
|
212
|
+
| `GramRidgeCV` | RidgeCV (cv=None), leave-one-out over every alpha from one eigenproblem | `ridge-gcv.scm` | loops / BLAS | no |
|
|
213
|
+
| `GramMultiTaskLasso` | MultiTaskLasso, fit(X, Y) with the intercept | `lasso-cov.scm` | CPU | no |
|
|
214
|
+
| `GramMultiTaskElasticNet` | MultiTaskElasticNet, fit(X, Y) with the intercept | `lasso-cov.scm` | CPU | no |
|
|
215
|
+
| `GramGroupLasso` | (none; skglm GroupLasso), fit(X, y) with the intercept | `lasso-cov.scm` | CPU | - |
|
|
216
|
+
| `GramLogisticRegression` | LogisticRegression(l1_ratio=1, solver='liblinear'), by majorization | `lasso-cov.scm` | CPU | no |
|
|
217
|
+
| `DesignLasso` | Lasso, fit(X, y) with the intercept, from the design | `lasso-design.scm (--derive)` | loops / BLAS / cuBLAS | no |
|
|
218
|
+
| `CovLasso` | Lasso, ElasticNet, lasso_path(precompute=True) | `lasso-cov.scm` | CPU; GPU batch (fit_path_batch, bootstrap) | no |
|
|
219
|
+
| `CovLassoCV` | LassoCV, ElasticNetCV | `lasso-cov.scm` | CPU; GPU batch | no |
|
|
220
|
+
| `DerivedLasso` | lasso_path, from the design | `lasso-design.scm (--derive)` | loops / BLAS / cuBLAS | no |
|
|
221
|
+
| `CovMultiTaskLasso` | MultiTaskLasso, MultiTaskElasticNet | `lasso-cov.scm` | CPU | no |
|
|
222
|
+
| `CovMultiTaskLassoCV` | MultiTaskLassoCV, MultiTaskElasticNetCV | `lasso-cov.scm` | CPU; GPU batch | no |
|
|
223
|
+
| `CovGroupLasso` | (none; skglm GroupLasso) | `lasso-cov.scm` | CPU | - |
|
|
224
|
+
| `CovLogistic` | LogisticRegression(penalty='l1'), by majorization | `lasso-cov.scm` | CPU | no |
|
|
225
|
+
| `CovRidge` | Ridge(fit_intercept=False), a whole alpha path | `lasso-cov.scm` | CPU | Ridge |
|
|
226
|
+
| `CovBayesianRidge` | BayesianRidge | `lasso-cov.scm` | CPU | no |
|
|
227
|
+
| `CovARDRegression` | ARDRegression (n >= p) | `ard.scm` | loops / BLAS | no |
|
|
228
|
+
| `GramLars` | Lars | `lars.scm` | CPU | no |
|
|
229
|
+
| `GramLassoLars` | LassoLars | `lars.scm` | CPU | no |
|
|
230
|
+
| `GramLassoLarsIC` | LassoLarsIC | `lars.scm` | CPU; device Gram | no |
|
|
231
|
+
| `GramLarsCV` | LarsCV | `lars.scm` | CPU | no |
|
|
232
|
+
| `GramLassoLarsCV` | LassoLarsCV | `lars.scm` | CPU; device Gram | no |
|
|
233
|
+
| `GramOMP` | OrthogonalMatchingPursuit | `omp.scm` | loops / BLAS | no |
|
|
234
|
+
| `GramOMPCV` | OrthogonalMatchingPursuitCV | `omp.scm` | loops / BLAS | no |
|
|
235
|
+
| `LBFGSLogisticRegression` | LogisticRegression(solver='lbfgs') | `logistic-lbfgs.scm, lbfgs.scm` | loops / BLAS / --cuda | LogisticRegression |
|
|
236
|
+
| `LBFGSLogisticRegressionCV` | LogisticRegressionCV | `logistic-lbfgs.scm, lbfgs.scm` | loops / BLAS / --cuda | no |
|
|
237
|
+
| `LBFGSHuberRegressor` | HuberRegressor | `huber.scm, lbfgs.scm` | loops / BLAS / --cuda | no |
|
|
238
|
+
| `LBFGSPoissonRegressor` | PoissonRegressor | `glm.scm, lbfgs.scm` | loops / BLAS / --cuda | no |
|
|
239
|
+
| `LBFGSGammaRegressor` | GammaRegressor | `glm.scm, lbfgs.scm` | loops / BLAS / --cuda | no |
|
|
240
|
+
| `LBFGSTweedieRegressor` | TweedieRegressor | `glm.scm, lbfgs.scm` | loops / BLAS / --cuda | no |
|
|
241
|
+
| `LinearSVM` | LinearSVC(loss='hinge') | `svm-liblinear.scm` | CPU | no |
|
|
242
|
+
| `L1LinearSVM` | LinearSVC(penalty='l1', dual=False) | `svm-l1.scm` | CPU | no |
|
|
243
|
+
| `KernelSVM` | SVC | `svm-smo.scm` | CPU | SVC |
|
|
244
|
+
| `LinearSVR` | LinearSVR | `svr-liblinear.scm` | CPU | no |
|
|
245
|
+
| `KernelSVR` | SVR | `svm-smo.scm` | CPU | SVR |
|
|
246
|
+
| `L1LogisticRegression` | LogisticRegression(penalty='l1', solver='liblinear') | `logistic-l1.scm` | CPU | no |
|
|
247
|
+
| `SGDLinearClassifier` | SGDClassifier(shuffle=False) | `sgd.scm` | CPU | no |
|
|
248
|
+
| `SGDLinearRegressor` | SGDRegressor(shuffle=False) | `sgd.scm` | CPU | no |
|
|
249
|
+
| `SGDPerceptron` | Perceptron(shuffle=False) | `sgd.scm` | CPU | no |
|
|
250
|
+
| `LinearRANSAC` | RANSACRegressor (given subsets) | `ransac.scm` | CPU | no |
|
|
251
|
+
| `WeiszfeldTheilSen` | TheilSenRegressor | `theilsen.scm` | CPU | no |
|
|
252
|
+
| `CDNMF` | NMF(solver='cd') | `nmf-cd.scm (--derive)` | loops / BLAS / cuBLAS | no |
|
|
253
|
+
| `CDGraphicalLasso` | GraphicalLasso | `graphical-lasso.scm` | loops / BLAS | no |
|
|
254
|
+
| `EMGaussianMixture` | GaussianMixture(covariance_type='full') | `gmm.scm` | loops / BLAS / --cuda | no |
|
|
255
|
+
| `ParallelFastICA` | FastICA | `fastica.scm` | loops / BLAS / --cuda | no |
|
|
256
|
+
| `CovPCA` | PCA(svd_solver='covariance_eigh') | `pca.scm` | loops / BLAS / cuBLAS | PCA |
|
|
257
|
+
| `GramKernelPCA` | KernelPCA (no inverse_transform) | `kernel-pca.scm` | loops / BLAS | no |
|
|
258
|
+
| `GramLDA` | LinearDiscriminantAnalysis (svd, lsqr, eigen) | `lda.scm` | loops / BLAS | no |
|
|
259
|
+
| `GramTruncatedSVD` | TruncatedSVD on dense X (exact, as arpack) | `truncated-svd.scm` | loops / BLAS | no |
|
|
260
|
+
| `f_regression, f_classif` | f_regression, f_classif | `fscore.scm` | loops / BLAS / cuBLAS | no |
|
|
261
|
+
| `SelectKBest, SelectPercentile` | SelectKBest, SelectPercentile | `fscore.scm` | loops / BLAS / cuBLAS | no |
|
|
262
|
+
| `GramRFE` | RFE(Ridge), RFE(LinearRegression) | `rfe.scm` | loops / BLAS / cuBLAS | inside: Ridge |
|
|
263
|
+
|
|
264
|
+
## Two kernels: `CovLasso` and `DerivedLasso`
|
|
265
|
+
|
|
266
|
+
Everything above, and the cross-validation, bootstrap and GPU methods
|
|
267
|
+
below, runs on one kernel: numpy forms the Gram matrix, and a
|
|
268
|
+
hand-written coordinate-descent sweep over it
|
|
269
|
+
(`examples/kernel-only/lasso-cov.scm` in the repository, translated to
|
|
270
|
+
C++) does the rest. That is the usable one.
|
|
271
|
+
|
|
272
|
+
`DerivedLasso` is the other kernel, and it is there to show the
|
|
273
|
+
translator's work rather than to be the better estimator. Its source
|
|
274
|
+
is `examples/kernel-only/lasso-design.scm`: the residual-form
|
|
275
|
+
coordinate descent -- the algorithm inside scikit-learn's
|
|
276
|
+
`Lasso(precompute=False)` -- written as flat loops over X exactly as
|
|
277
|
+
numpy holds it (n rows of p, `x[i*p+j]`), the residual copied from y
|
|
278
|
+
and the column norms taken inside, with no Gram matrix anywhere in
|
|
279
|
+
it. The translator's `--derive` turns
|
|
280
|
+
that into the Gram form: it raises the loops to an array algebra,
|
|
281
|
+
notices that the residual is only ever updated by a scaled column and
|
|
282
|
+
read in dot products against columns, replaces it by the memo `c =
|
|
283
|
+
X'r` kept current through a Gram matrix it hoists in front of the
|
|
284
|
+
penalty path, folds the column norms into that Gram's diagonal, folds
|
|
285
|
+
the three products into one `dsyrk` and two `dgemv` calls (`--blas`), and puts the whole thing under
|
|
286
|
+
`(if (< p n) ..)` with the sweep as written in the else branch --
|
|
287
|
+
scikit-learn's `precompute='auto'` rule, derived rather than written.
|
|
288
|
+
The C++ of that derivation, as the translator emitted it, is what
|
|
289
|
+
`DerivedLasso` calls; numpy prepares nothing -- X and y go in as they
|
|
290
|
+
are, no transposed copy, no norms.
|
|
291
|
+
|
|
292
|
+
```python
|
|
293
|
+
from scm2cpp_learn import DerivedLasso, derived_backend
|
|
294
|
+
|
|
295
|
+
model = DerivedLasso(X, y) # X and y; the kernel builds its own Gram
|
|
296
|
+
path = model.fit_path(model.lambda_grid()) # one call, the whole warm-started path
|
|
297
|
+
print(derived_backend()) # 'cublas', 'blas' or 'loops': what this machine built
|
|
298
|
+
gpu = DerivedLasso(X, y, backend="cublas") # insist on one
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
A lambda is done when no coefficient moved by more than `tol`
|
|
302
|
+
(absolute, default 1e-8) in a sweep, or at `max_sweeps`; `tol=0.0`
|
|
303
|
+
asks for the exact fixed point. Three translations of the one
|
|
304
|
+
derivation are committed and `pip` builds what the machine allows:
|
|
305
|
+
`--derive --cublas` where the CUDA toolkit is (the three products on
|
|
306
|
+
the device through cuBLAS, X uploaded once per `fit_path` call --
|
|
307
|
+
taken by default when a device answers), `--derive --blas` where
|
|
308
|
+
`cblas.h` and an OpenBLAS are, and `--derive` alone otherwise, the
|
|
309
|
+
same program with its Gram matrix as a scalar loop nest, correct but
|
|
310
|
+
slow on large designs. `check-derived.py` in the package directory runs
|
|
311
|
+
the two kernels and scikit-learn on the same path and prints the
|
|
312
|
+
differences and the times; on AR(1) designs (rho 0.9), 20 penalties
|
|
313
|
+
from lambda_max to 0.01 lambda_max, one core, best of three (load
|
|
314
|
+
about 3), whole calls with the Gram matrix wherever it is built:
|
|
315
|
+
|
|
316
|
+
| n x p | 1,800 x 200 | 5,000 x 1000 | 100,000 x 200 | 100,000 x 500 |
|
|
317
|
+
|-----------------------------------------|-------------|--------------|---------------|---------------|
|
|
318
|
+
| `DerivedLasso`, cuBLAS (upload included) | 0.003 s | 0.024 s | 0.052 s | 0.11 s |
|
|
319
|
+
| `DerivedLasso`, CBLAS | 0.003 s | 0.076 s | 0.12 s | 0.44 s |
|
|
320
|
+
| `CovLasso(X, y).fit_path`, tol 1e-8 | 0.004 s | 0.080 s | 0.11 s | 0.42 s |
|
|
321
|
+
| sklearn `lasso_path(precompute=True)`, tol 1e-4 | 0.005 s | 0.10 s | 0.24 s | 0.76 s |
|
|
322
|
+
|
|
323
|
+
The CBLAS run is the same `dsyrk` and the same sweeps as `CovLasso`'s
|
|
324
|
+
numpy Gram and path, and times the same; the cuBLAS run is the same
|
|
325
|
+
program with the three products on the device, the upload of X
|
|
326
|
+
counted, and is 4.2x, 4.5x and 6.7x ahead of scikit-learn at the
|
|
327
|
+
three larger shapes -- the figures of the repository's equal-work
|
|
328
|
+
table, now reached from the package with nothing in front of the
|
|
329
|
+
kernel: the constructor keeps X and y and the kernel takes the
|
|
330
|
+
residual and the column norms itself (the norms folded into the Gram
|
|
331
|
+
diagonal by the derivation). Threads change the picture less than one might expect: scikit-learn's
|
|
332
|
+
sweeps are single-threaded Cython and only its Gram product scales,
|
|
333
|
+
so with 20 BLAS threads `lasso_path` takes 0.051 / 0.227 / 0.450 s on
|
|
334
|
+
the three larger shapes, against 0.023 / 0.050 / 0.113 s for the
|
|
335
|
+
cuBLAS build (2.2x, 4.5x, 4.0x) and 0.025 / 0.036 / 0.123 s for the
|
|
336
|
+
CBLAS build (2.0x, 6.3x, 3.7x), whose `dsyrk` scales with the
|
|
337
|
+
threads; the GPU figures do not move. Against a reference driven to
|
|
338
|
+
tol=1e-12, `DerivedLasso` lands within 5e-8 on every backend (its
|
|
339
|
+
tolerance is absolute, on one sweep's largest move), `CovLasso`
|
|
340
|
+
within 2e-10 (it tests every 20 sweeps and overshoots), and
|
|
341
|
+
scikit-learn at its default within 1e-3.
|
|
342
|
+
|
|
343
|
+
### Deriving it yourself
|
|
344
|
+
|
|
345
|
+
Everything `DerivedLasso` runs is reproducible from the Scheme with
|
|
346
|
+
the translator; this is what `regenerate.sh` does for the package,
|
|
347
|
+
spelled out for one kernel (Racket 8 and a C++17 compiler; a CBLAS
|
|
348
|
+
for `--blas`, the CUDA toolkit for `--cublas`):
|
|
349
|
+
|
|
350
|
+
```console
|
|
351
|
+
$ git clone https://github.com/niitsuma/scm2cpp && cd scm2cpp
|
|
352
|
+
$ raco link --user vendor/rkanren # once per machine
|
|
353
|
+
$ mkdir /tmp/k && cp examples/kernel-only/lasso-design.scm examples/kernel-only/soft-threshold.scm /tmp/k/
|
|
354
|
+
$ racket scm2cpp-file.scm -t scm2c.typ --derive --cublas -M -S /tmp/k/lasso-design.scm
|
|
355
|
+
derive: lasso-design: raise differencing merge matmul guard
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
`derive:` on stderr names what fired: the loops raised to the array
|
|
359
|
+
algebra, the residual differenced into the memo, the column norms
|
|
360
|
+
merged into the Gram diagonal, the products folded to contractions,
|
|
361
|
+
the `(if (< p n) ..)` guard. The run leaves, beside the source,
|
|
362
|
+
`lasso-design.hpp` (the derived kernel as C++: one `blas_syrk`, two
|
|
363
|
+
`blas_gemv`, the sweep), `lasso-design_capi.cpp` and
|
|
364
|
+
`lasso_design.py` (the `-M` C ABI and its ctypes loader -- what the
|
|
365
|
+
package's `_loader_derived_*.py` are), and `lasso-design.expanded.scm`
|
|
366
|
+
(`-S`: the derived program as Scheme, before C++ -- read this to see
|
|
367
|
+
what the derivation did). `--blas` instead of `--cublas` gives the
|
|
368
|
+
CBLAS variant, neither the loop variant. To call it from numpy
|
|
369
|
+
without the package:
|
|
370
|
+
|
|
371
|
+
```console
|
|
372
|
+
$ g++ -O2 -std=c++17 -shared -fPIC -I. -I$CUDA_HOME/include -o /tmp/k/liblasso-design.so \
|
|
373
|
+
/tmp/k/lasso-design_capi.cpp -L$CUDA_HOME/lib64 -lcublas -lcudart # -lopenblas for --blas
|
|
374
|
+
$ cd /tmp/k && python3 -c 'import lasso_design' # lasso_design.lasso_design(x, y, resid, xnorm, beta, lams, betas, iters, tol, n, p, nlam)
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
(`-M` names the Python module after the file with `-` read as `_`,
|
|
378
|
+
and the loader looks for `liblasso-design.so` beside it.)
|
|
379
|
+
`examples/kernel-only/README.md` walks through the same `-M` flow on
|
|
380
|
+
the row-major kernel, and `python/PACKAGING.md` through turning such
|
|
381
|
+
a kernel into a pip package.
|
|
382
|
+
|
|
383
|
+
## Against the other lassos pip can install
|
|
384
|
+
|
|
385
|
+
`bench/lasso-compare.py` in the repository runs the same
|
|
386
|
+
cross-validation-shaped grid -- 4096 lambdas, each solved from zero,
|
|
387
|
+
p=200, n=1800 -- against every lasso a `pip install` produces:
|
|
388
|
+
scikit-learn 1.9.1, celer 0.7.4, skglm 0.5, and RAPIDS cuML 26.8.
|
|
389
|
+
Every solver runs at its own default tolerance (ours at the tighter
|
|
390
|
+
1e-8), each gets one untimed warm-up fit, cuML's data is on the device
|
|
391
|
+
before the clock starts, and the last column is each solver's
|
|
392
|
+
objective minus ours at the smallest lambda -- a fast row with a loose
|
|
393
|
+
answer would show as one:
|
|
394
|
+
|
|
395
|
+
| solver | time | objective gap |
|
|
396
|
+
|----------------------------------------|---------|---------------|
|
|
397
|
+
| scm2cpp-learn, 1 CPU core (tol 1e-8) | 0.91 s | 0 |
|
|
398
|
+
| scm2cpp-learn, GPU, one block/lambda | 0.044 s | 0 |
|
|
399
|
+
| sklearn `Lasso.fit` per lambda | 9.6 s | +1.6e-09 |
|
|
400
|
+
| celer per lambda | 13.5 s | 0 |
|
|
401
|
+
| skglm per lambda | 14.5 s | +2.8e-17 |
|
|
402
|
+
| cuML per lambda, GPU | 53.6 s | +9.1e-07 |
|
|
403
|
+
|
|
404
|
+
(One sitting on an otherwise idle machine, best of three runs; cuML
|
|
405
|
+
26.08 on CUDA 12.9.)
|
|
406
|
+
|
|
407
|
+
celer and skglm are built for very large sparse designs, where their
|
|
408
|
+
screening rules dominate; at this size they pay their setup per fit.
|
|
409
|
+
cuML parallelises *within* one fit, which wins when a fit is large; at
|
|
410
|
+
this size its launch overhead dominates, while our GPU row
|
|
411
|
+
parallelises *across* lambdas -- one block of CUDA threads per
|
|
412
|
+
penalty, the axis a cross-validation grid actually offers.
|
|
413
|
+
|
|
414
|
+
`CovLassoCV` is scikit-learn's `LassoCV` over this machinery -- same
|
|
415
|
+
grid construction, same contiguous folds, same minimum-mean-MSE choice
|
|
416
|
+
-- with two structural savings. A fold's training Gram is a
|
|
417
|
+
*subtraction* (the Gram is additive over rows, so G - Xf'Xf costs one
|
|
418
|
+
fold's work where rebuilding costs four folds'), and once the Grams
|
|
419
|
+
exist nothing downstream ever touches the n rows again -- which is why
|
|
420
|
+
the gap grows with n. With a GPU the whole grid is one launch: one
|
|
421
|
+
*block* of threads per (fold, alpha) cell, the fold's Gram read in
|
|
422
|
+
place (cv Gram matrices on the device, not cv x 100), thread 0 of the
|
|
423
|
+
block doing the coordinate step and the block spreading the O(p)
|
|
424
|
+
update of the correlations. cv x 100 = 500 cells are too few
|
|
425
|
+
problems to fill a device one thread each, and a block per problem is
|
|
426
|
+
what turns that count into enough parallel work; `bench/cv-grid-designs.py`
|
|
427
|
+
times the alternatives (one thread per cell, warm runs of several
|
|
428
|
+
alphas per thread or block) side by side, and the block per cell wins
|
|
429
|
+
at every size tried. The GPU is the default when a device answers
|
|
430
|
+
(`force_cpu` takes the warm CPU path). Measured at cv=5, 100 alphas,
|
|
431
|
+
one CPU core, sklearn 1.9.1, best of three runs, whole estimator
|
|
432
|
+
including the Grams:
|
|
433
|
+
|
|
434
|
+
| n | p | CPU | CUDA | sklearn `LassoCV` |
|
|
435
|
+
|---------|------|--------|--------|-------------------|
|
|
436
|
+
| 1,800 | 200 | 0.21 s | 0.02 s | 0.10 s |
|
|
437
|
+
| 5,000 | 1000 | 1.48 s | 0.25 s | 0.90 s |
|
|
438
|
+
| 100,000 | 200 | 0.49 s | 0.34 s | 1.72 s |
|
|
439
|
+
| 100,000 | 500 | 1.32 s | 1.09 s | 4.82 s |
|
|
440
|
+
|
|
441
|
+
(Taken on an otherwise idle machine, load 2-3 on 20 threads. sklearn's
|
|
442
|
+
`LassoCV` at its default `precompute='auto'` takes the Gram route at
|
|
443
|
+
every size here, n > p, rebuilding each fold's Gram from that fold's
|
|
444
|
+
rows where we subtract it from the whole.) The grid
|
|
445
|
+
launch itself takes 9 ms at n=1,800 and 64 ms at n=5,000, p=1000; at
|
|
446
|
+
n=100,000 the CUDA column is nearly all Gram products, which the CPU
|
|
447
|
+
path pays as well. Large n is where the machinery pays either way:
|
|
448
|
+
nothing downstream of the Grams touches the 100,000 rows again. At
|
|
449
|
+
n=5,000, p=1000 sklearn's raw-X descent beats our Gram-heavy CPU path,
|
|
450
|
+
and the GPU makes up the difference three and a half times over. CPU and CUDA
|
|
451
|
+
choose the same alpha everywhere and agree to 1e-14; against sklearn
|
|
452
|
+
the pick is exact at n=5,000 and one grid step apart on a near-tie
|
|
453
|
+
elsewhere (mean-MSE relative gap at most 2.6e-4), and sklearn at
|
|
454
|
+
tol=1e-10 picks our alpha at every size.
|
|
455
|
+
|
|
456
|
+
The elastic net cross-validates the same way: `CovLassoCV(l1_ratio=0.5)`
|
|
457
|
+
against `ElasticNetCV(l1_ratio=0.5)` on the same folds and alpha grid
|
|
458
|
+
(`bench/lasso-compare.py --cv-only --l1-ratio 0.5`, same conditions,
|
|
459
|
+
load under 1):
|
|
460
|
+
|
|
461
|
+
| n | p | CPU | CUDA | sklearn `ElasticNetCV` |
|
|
462
|
+
|---------|------|--------|--------|------------------------|
|
|
463
|
+
| 1,800 | 200 | 0.35 s | 0.02 s | 0.11 s |
|
|
464
|
+
| 5,000 | 1000 | 1.55 s | 0.26 s | 0.98 s |
|
|
465
|
+
| 100,000 | 200 | 0.52 s | 0.35 s | 1.76 s |
|
|
466
|
+
| 100,000 | 500 | 1.38 s | 1.15 s | 4.97 s |
|
|
467
|
+
|
|
468
|
+
The picture is the lasso's: on the CPU, 3.4-3.6x at 100,000 rows and
|
|
469
|
+
slower than sklearn at the two small shapes; with CUDA, 3.8-5.5x
|
|
470
|
+
everywhere. The chosen alpha is sklearn's at all four sizes, the
|
|
471
|
+
coefficients within 8e-6 (sklearn's default tolerance).
|
|
472
|
+
|
|
473
|
+
`CovMultiTaskLasso` and `CovMultiTaskLassoCV` are the multi-task
|
|
474
|
+
family over the same machinery: scikit-learn's `MultiTaskLasso`,
|
|
475
|
+
`MultiTaskElasticNet` (through `l1_ratio`), `MultiTaskLassoCV` and
|
|
476
|
+
`MultiTaskElasticNetCV`. The penalty ties each feature's row of W
|
|
477
|
+
together across tasks, so the coordinate update becomes a block soft
|
|
478
|
+
threshold on the row's L2 norm; C = X'Y - GW is maintained per task
|
|
479
|
+
exactly as the single-task C is, and nothing downstream of the Grams
|
|
480
|
+
touches the n rows -- the kernel is the same translated Scheme
|
|
481
|
+
(`mt-descend`). Folds are independent and both the descent (ctypes)
|
|
482
|
+
and the products (BLAS) release the GIL, so `n_jobs` threads scale the
|
|
483
|
+
CV across folds; the default is sequential, as scikit-learn's
|
|
484
|
+
`n_jobs=None` is, and the benchmark pins BLAS to one thread on both
|
|
485
|
+
sides either way. With a GPU the CV grid is one launch exactly as
|
|
486
|
+
`CovLassoCV`'s is, a block per (fold, alpha) cell with the p x T
|
|
487
|
+
coefficient block in shared memory (`force_cpu` opts out). Same
|
|
488
|
+
protocol as above, 8 tasks; single fits at alpha = 0.1 lambda_max;
|
|
489
|
+
the CV pairs share the 100-alpha grid:
|
|
490
|
+
|
|
491
|
+
| estimator | n | p | ours | ours `n_jobs=5` | ours CUDA | sklearn |
|
|
492
|
+
|-----------------------|---------|-----|--------|-----------------|-----------|---------|
|
|
493
|
+
| MultiTaskLasso | 100,000 | 200 | 0.14 s | -- | -- | 0.76 s |
|
|
494
|
+
| MultiTaskLasso | 100,000 | 500 | 0.53 s | -- | -- | 1.7 s |
|
|
495
|
+
| MultiTaskLassoCV | 1,800 | 200 | 1.4 s | 0.51 s | 0.07 s | 0.42 s |
|
|
496
|
+
| MultiTaskLassoCV | 100,000 | 200 | 1.6 s | 0.64 s | 1.3 s | 74 s |
|
|
497
|
+
| MultiTaskElasticNetCV | 1,800 | 200 | 1.4 s | 0.49 s | 0.08 s | 0.75 s |
|
|
498
|
+
| MultiTaskElasticNetCV | 100,000 | 200 | 1.6 s | 0.68 s | 1.3 s | 162 s |
|
|
499
|
+
|
|
500
|
+
(`MultiTaskElasticNet` single fits time the same as `MultiTaskLasso`'s;
|
|
501
|
+
every column was taken in one sitting on the idle machine of the
|
|
502
|
+
`CovLassoCV` table above, sklearn 1.9.1. scikit-learn has no Gram
|
|
503
|
+
route for its multi-task estimators -- its path code sets
|
|
504
|
+
`precompute=False` for 2-D targets, "No Gram variant of multi-task
|
|
505
|
+
exists right now" -- so its column is raw-X descent at every n, which
|
|
506
|
+
is why it grows with n where `LassoCV`'s did not.) At n=1,800 our sequential CV
|
|
507
|
+
is slower than sklearn's: the default tol=1e-8 runs more sweeps than
|
|
508
|
+
sklearn's 1e-4 dual-gap stop, and at that size there are no rows to
|
|
509
|
+
save -- and the GPU grid takes the whole thing in a tenth of a second.
|
|
510
|
+
At n=100,000 the Gram route is 46x and 100x ahead, essentially
|
|
511
|
+
unchanged from n=1,800 in absolute time; the CUDA column there is
|
|
512
|
+
the Gram products, the same ones the CPU path forms. Coefficients agree with scikit-learn's to 1e-15 at tight
|
|
513
|
+
tolerance, and the CV pair picks the same alpha with coefficients to
|
|
514
|
+
1e-13.
|
|
515
|
+
|
|
516
|
+
## Which method
|
|
517
|
+
|
|
518
|
+
`fit_path` walks a single path, each lambda starting from the previous
|
|
519
|
+
solution -- the descent is exactly resumable, so this costs almost
|
|
520
|
+
nothing per lambda after the first. `fit_path_batch` solves every
|
|
521
|
+
lambda from zero, which is what a cross-validation grid needs, where
|
|
522
|
+
the folds differ and warm starting across lambdas is not on offer; the
|
|
523
|
+
problems are independent, so they go to the GPU together.
|
|
524
|
+
|
|
525
|
+
Over a 400-lambda path at p=200 and 1800 rows, on an RTX 4090 and an
|
|
526
|
+
i9-10900X:
|
|
527
|
+
|
|
528
|
+
| call | time |
|
|
529
|
+
|---------------------------------------|---------|
|
|
530
|
+
| `fit_path` (warm, sequential) | 0.091 s |
|
|
531
|
+
| `fit_path_batch` (GPU) | 0.047 s |
|
|
532
|
+
| `fit_path_batch(force_cpu=True)` | 0.178 s |
|
|
533
|
+
|
|
534
|
+
GPU and CPU agree to 2e-14, and the objectives are within 3e-17 of
|
|
535
|
+
scikit-learn's `lasso_path` on the same grid.
|
|
536
|
+
|
|
537
|
+
## Elastic net and ridge
|
|
538
|
+
|
|
539
|
+
The same Gram matrix serves two more estimators. `fit_path` and
|
|
540
|
+
`fit_path_batch` take `l1_ratio` (scikit-learn's mixing parameter):
|
|
541
|
+
the L2 share of the penalty enters only the update's denominator, so
|
|
542
|
+
the elastic net runs on the identical machinery, GPU path included,
|
|
543
|
+
and `l1_ratio=1` is bit-for-bit the lasso.
|
|
544
|
+
|
|
545
|
+
`CovRidge` is the closed form: one symmetric eigendecomposition,
|
|
546
|
+
then every alpha costs O(p^2), so thousands of alphas cost what one
|
|
547
|
+
does. Its objective matches scikit-learn's `Ridge` with
|
|
548
|
+
`fit_intercept=False` (which, unlike the lasso, is not scaled by the
|
|
549
|
+
number of rows), and agrees with it to machine precision.
|
|
550
|
+
|
|
551
|
+
```python
|
|
552
|
+
path = model.fit_path(lambdas, l1_ratio=0.5) # elastic net
|
|
553
|
+
ridge = CovRidge(X, y)
|
|
554
|
+
betas = ridge.fit_path(ridge.alpha_grid()) # a whole ridge path
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
Measured (`bench/ridge-enet-compare.py` in the repository: AR(1)
|
|
558
|
+
designs, 20 penalties, whole calls with the Gram matrix wherever one
|
|
559
|
+
is built, one core, best of three):
|
|
560
|
+
|
|
561
|
+
| n x p | 1,800 x 200 | 5,000 x 1000 | 100,000 x 200 | 100,000 x 500 |
|
|
562
|
+
|----------------------------------------------|-------------|--------------|---------------|---------------|
|
|
563
|
+
| elastic net, `CovLasso.fit_path(l1_ratio=0.5)`, tol 1e-8 | 0.007 s | 0.092 s | 0.11 s | 0.40 s |
|
|
564
|
+
| elastic net, sklearn `enet_path(precompute=True)`, tol 1e-4 | 0.005 s | 0.099 s | 0.23 s | 0.74 s |
|
|
565
|
+
| ridge, `CovRidge.fit_path(alphas)` | 0.004 s | 0.21 s | 0.10 s | 0.41 s |
|
|
566
|
+
| ridge, sklearn `Ridge(alpha).fit` per alpha | 0.060 s | 2.05 s | 3.37 s | 11.2 s |
|
|
567
|
+
| ridge, sklearn `RidgeCV(alphas)` (its LOO path) | 0.093 s | 3.40 s | 5.38 s | 19.0 s |
|
|
568
|
+
|
|
569
|
+
The elastic net is the same coordinate descent on both sides, so the
|
|
570
|
+
two rows differ by the Gram and the warm path, not the sweeps: level
|
|
571
|
+
at small n, 1.8-2.2x at 100,000 rows, and ours lands within 2e-10 of
|
|
572
|
+
a tight reference where scikit-learn's default tolerance lands within
|
|
573
|
+
6e-4. The ridge rows compare a whole path against what a user
|
|
574
|
+
writes -- one `Ridge.fit` per alpha, each forming X'X and solving
|
|
575
|
+
afresh, O(np^2 + p^3) every time -- so the gap is the path's: 10-33x,
|
|
576
|
+
agreeing to 1e-14. `RidgeCV` is given for scale (it picks one alpha
|
|
577
|
+
by leave-one-out rather than returning the path). Neither row runs
|
|
578
|
+
on the GPU: the ridge is one eigendecomposition and the elastic net's
|
|
579
|
+
sweeps are the lasso's; `fit_path_batch(l1_ratio=..)` is the GPU
|
|
580
|
+
grid for the elastic net.
|
|
581
|
+
|
|
582
|
+
## Leave-one-out ridge: `GramRidgeCV`
|
|
583
|
+
|
|
584
|
+
`GramRidgeCV` is scikit-learn's `RidgeCV` with `cv=None`: the alpha
|
|
585
|
+
whose leave-one-out mean squared error is least, without a refit per
|
|
586
|
+
row. With the centred design Xc and H = Xc (Xc'Xc + alpha I)^-1 Xc'
|
|
587
|
+
(plus 1/n for the intercept), the leave-one-out residual is
|
|
588
|
+
(y - yhat) / (1 - diag H). One eigenproblem gives H = Z diag(1 /
|
|
589
|
+
(lam + alpha)) Z' for every alpha: Z = Xc V from Xc'Xc = V diag(lam) V'
|
|
590
|
+
when n > p (X'X by dsyrk), Z = Q diag(sqrt mu) from Xc Xc' = Q diag(mu)
|
|
591
|
+
Q' otherwise. Then diag H and yhat at all the alphas are two products,
|
|
592
|
+
(Z o Z) W and Z (R o W) with W[k, a] = 1 / (lam_k + alpha_a): the
|
|
593
|
+
alphas cost one dgemm, where scikit-learn loops over them in numpy
|
|
594
|
+
(`examples/kernel-only/ridge-gcv.scm`; the eigenproblem is LAPACK's
|
|
595
|
+
dsyevd in the BLAS build).
|
|
596
|
+
|
|
597
|
+
```python
|
|
598
|
+
from scm2cpp_learn import GramRidgeCV
|
|
599
|
+
m = GramRidgeCV(alphas=np.logspace(-2, 4, 50)).fit(X, y)
|
|
600
|
+
m.alpha_, m.best_score_, m.coef_, m.intercept_
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
It takes `fit_intercept`, `gcv_mode` (`"svd"` / `"eigen"` choose the
|
|
604
|
+
two Zs), `store_cv_results` and `alpha_per_target`, y one target or
|
|
605
|
+
several; `cv` and `scoring` must stay None. The chosen alpha is
|
|
606
|
+
scikit-learn's, the coefficients agree to 1e-12 and `cv_results_` to
|
|
607
|
+
1e-7 relative (the wide case near interpolation). Measured
|
|
608
|
+
(`bench/eigen-compare.py`, the MKL build, best of three):
|
|
609
|
+
|
|
610
|
+
| case | scikit-learn `RidgeCV` | `GramRidgeCV` | ratio, one core | ratio, 20 cores |
|
|
611
|
+
|---|---|---|---|---|
|
|
612
|
+
| 200,000 x 200, 3 alphas | 2.04 s | 0.80 s | 2.6x | 3.6x |
|
|
613
|
+
| 200,000 x 200, 50 alphas | 26.6 s | 0.96 s | 27.7x | 29.0x |
|
|
614
|
+
| 20,000 x 500, 50 alphas | 9.64 s | 0.33 s | 28.9x | 29.2x |
|
|
615
|
+
| 2,000 x 5,000, 50 alphas | 3.16 s | 1.47 s | 2.1x | 4.5x |
|
|
616
|
+
|
|
617
|
+
## Kernel PCA: `GramKernelPCA`
|
|
618
|
+
|
|
619
|
+
`GramKernelPCA` is scikit-learn's `KernelPCA`: the kernel matrix
|
|
620
|
+
(linear, rbf, poly, sigmoid, cosine or precomputed), centred in feature
|
|
621
|
+
space, its largest eigenpairs with svd_flip's signs
|
|
622
|
+
(`examples/kernel-only/kernel-pca.scm`). Where the eigenpairs come from
|
|
623
|
+
is what differs:
|
|
624
|
+
|
|
625
|
+
- fewer than 10 components of more than 200 rows, where scikit-learn
|
|
626
|
+
takes ARPACK (Lanczos, one matrix-vector product at a time): subspace
|
|
627
|
+
iteration on c + 25 columns, so each step is one dgemm, stopped when
|
|
628
|
+
every residual |K u - lam u| is under 1e-12 of the largest value
|
|
629
|
+
(falling back to the dense route if it is not reached);
|
|
630
|
+
- otherwise LAPACK's dsyevr over the wanted index range, which is what
|
|
631
|
+
scipy's `eigh(subset_by_index=..)` runs for scikit-learn;
|
|
632
|
+
- the linear kernel with no more components than features, n > p:
|
|
633
|
+
through Xc'Xc, p x p (the eigenvectors of Xc Xc' are Xc v / sqrt(lam)),
|
|
634
|
+
where scikit-learn forms the n x n kernel.
|
|
635
|
+
|
|
636
|
+
```python
|
|
637
|
+
from scm2cpp_learn import GramKernelPCA
|
|
638
|
+
Z = GramKernelPCA(5, kernel="rbf", gamma=0.05).fit_transform(X)
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
Eigenvalues, eigenvectors and transforms agree with scikit-learn's to
|
|
642
|
+
1e-12 on every kernel; no `inverse_transform`. Measured
|
|
643
|
+
(`bench/eigen-compare.py`, the MKL build, best of three):
|
|
644
|
+
|
|
645
|
+
| case | scikit-learn `KernelPCA` | `GramKernelPCA` | ratio, one core | ratio, 20 cores |
|
|
646
|
+
|---|---|---|---|---|
|
|
647
|
+
| rbf, 3,000 x 20, 5 components | 1.11 s (ARPACK) | 0.35 s (subspace) | 3.2x | 3.0x |
|
|
648
|
+
| rbf, 6,000 x 20, 5 components | 4.63 s (ARPACK) | 1.33 s (subspace) | 3.5x | 4.2x |
|
|
649
|
+
| rbf, 3,000 x 20, 50 components | 3.48 s (dense) | 3.63 s (dense) | 1.0x | 1.2x |
|
|
650
|
+
| linear, 5,000 x 50, 10 components | 19.8 s (ARPACK on 5,000 x 5,000) | 0.002 s (X'X) | 10,000x | 2,100x |
|
|
651
|
+
|
|
652
|
+
The linear row is the algorithm (an n x n problem against a p x p one),
|
|
653
|
+
not the code.
|
|
654
|
+
|
|
655
|
+
## Linear discriminant analysis: `GramLDA`
|
|
656
|
+
|
|
657
|
+
`GramLDA` is scikit-learn's `LinearDiscriminantAnalysis` with the n x p
|
|
658
|
+
work moved to the Gram matrix (`examples/kernel-only/lda.scm`): one pass
|
|
659
|
+
gives the class means and X'X (dsyrk), and the within-class covariance
|
|
660
|
+
is Sw = (X'X - sum_k n_k mu_k mu_k') / n. Each solver then runs its own
|
|
661
|
+
steps on p x p matrices:
|
|
662
|
+
|
|
663
|
+
- `"svd"` (the default) takes the SVD of the standardised within-class
|
|
664
|
+
design Xc D^-1 / sqrt(n), n x p. Its right singular vectors and values
|
|
665
|
+
are the eigenvectors and square roots of the eigenvalues of
|
|
666
|
+
D^-1 Sw D^-1, so one p x p eigenproblem (dsyevd) replaces it; the
|
|
667
|
+
second, classes x rank SVD and the coefficients follow as in
|
|
668
|
+
scikit-learn;
|
|
669
|
+
- `"lsqr"` solves Sw W = M' by Cholesky;
|
|
670
|
+
- `"eigen"` solves Sb v = lam Sw v (`sym-eigen-gen`, LAPACK's dsygvd).
|
|
671
|
+
|
|
672
|
+
```python
|
|
673
|
+
from scm2cpp_learn import GramLDA
|
|
674
|
+
clf = GramLDA().fit(X, y) # solver="svd"
|
|
675
|
+
clf.predict(X), clf.transform(X)
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
`shrinkage` takes None or a float (it is linear in Sw, so the Gram route
|
|
679
|
+
keeps it), not "auto"; custom `priors` form a Gram per class. The
|
|
680
|
+
coefficients, intercepts, probabilities and explained variance agree
|
|
681
|
+
with scikit-learn's to 1e-12, the predictions exactly, and `transform`
|
|
682
|
+
up to the sign of each column. Measured (`bench/eigen-compare.py`, the
|
|
683
|
+
MKL build, best of three):
|
|
684
|
+
|
|
685
|
+
| case | scikit-learn | `GramLDA` | ratio, one core | ratio, 20 cores |
|
|
686
|
+
|---|---|---|---|---|
|
|
687
|
+
| svd (default), 200,000 x 100, 5 classes | 4.80 s | 0.092 s | 52x | 35x |
|
|
688
|
+
| svd, 50,000 x 500, 10 classes | 4.07 s | 0.25 s | 16x | 14x |
|
|
689
|
+
| lsqr, 200,000 x 100, 5 classes | 0.42 s | 0.089 s | 4.7x | 8.9x |
|
|
690
|
+
| eigen, 200,000 x 100, 5 classes | 0.56 s | 0.094 s | 6.0x | 12x |
|
|
691
|
+
|
|
692
|
+
## Truncated SVD: `GramTruncatedSVD`
|
|
693
|
+
|
|
694
|
+
`GramTruncatedSVD` is scikit-learn's `TruncatedSVD` on a dense X
|
|
695
|
+
(`examples/kernel-only/truncated-svd.scm`): the leading pairs of X'X
|
|
696
|
+
(X'X by dsyrk; X X' when X is wide) by dsyevr over the wanted index
|
|
697
|
+
range, the transformed data by one dgemm, svd_flip's signs. The answer
|
|
698
|
+
is the exact truncated SVD, which `algorithm="arpack"` computes and the
|
|
699
|
+
default `"randomized"` approximates: against arpack the components agree
|
|
700
|
+
to 1e-13, against randomized they differ by its error (up to 1.3 on a
|
|
701
|
+
slowly falling spectrum). Measured (`bench/eigen-compare.py`, the MKL
|
|
702
|
+
build, best of three):
|
|
703
|
+
|
|
704
|
+
| case | scikit-learn | `GramTruncatedSVD` | ratio, one core | ratio, 20 cores |
|
|
705
|
+
|---|---|---|---|---|
|
|
706
|
+
| 200,000 x 100, 10 components, randomized | 1.16 s | 0.22 s | 5.4x | 7.0x |
|
|
707
|
+
| 200,000 x 100, 10 components, arpack | 4.46 s | 0.22 s | 20x | 25x |
|
|
708
|
+
| 50,000 x 500, 20 components, randomized | 1.27 s | 0.39 s | 3.3x | 5.1x |
|
|
709
|
+
| 50,000 x 500, 20 components, arpack | 10.1 s | 0.41 s | 24x | 21x |
|
|
710
|
+
| 5,000 x 2,000, 20 components, randomized | 0.46 s | 0.96 s | 0.5x | 3.5x |
|
|
711
|
+
| 5,000 x 2,000, 20 components, arpack | 5.17 s | 1.01 s | 5.1x | 9.1x |
|
|
712
|
+
|
|
713
|
+
The exact route costs O(n p^2 + p^3); with few components of many
|
|
714
|
+
features (2,000 here) the randomized one does less work, and on one core
|
|
715
|
+
it is faster, at its accuracy. Sparse X is not taken yet.
|
|
716
|
+
|
|
717
|
+
## Bayesian ridge
|
|
718
|
+
|
|
719
|
+
`CovBayesianRidge` is scikit-learn's `BayesianRidge` -- the same model,
|
|
720
|
+
the same defaults, the same iteration (MacKay's updates of the noise
|
|
721
|
+
precision `alpha_` and the weight precision `lambda_`, stopping when
|
|
722
|
+
the posterior mean moves by less than `tol`) -- with the linear
|
|
723
|
+
algebra moved into the eigenbasis of the Gram matrix. scikit-learn
|
|
724
|
+
takes the SVD of X once and multiplies by X again in every iteration
|
|
725
|
+
for the squared error. Here X enters once, through X'X (X X' when
|
|
726
|
+
n <= p): one eigendecomposition, after which the posterior mean, its
|
|
727
|
+
norm and the squared error are sums over the eigenvalues, O(p) per
|
|
728
|
+
iteration. `sigma_` (the posterior covariance) and
|
|
729
|
+
`predict(return_std=True)` are there as in scikit-learn.
|
|
730
|
+
|
|
731
|
+
```python
|
|
732
|
+
from scm2cpp_learn import CovBayesianRidge
|
|
733
|
+
m = CovBayesianRidge().fit(X, y) # fit_intercept=True, as sklearn
|
|
734
|
+
mean, std = m.predict(X_new, return_std=True)
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
Measured (`bench/bayesian-ridge-compare.py` in the repository:
|
|
738
|
+
Gaussian designs, a sparse truth plus noise, whole fits including
|
|
739
|
+
`sigma_`, one core, best of three, sklearn 1.9.1):
|
|
740
|
+
|
|
741
|
+
| n x p | `CovBayesianRidge` | sklearn `BayesianRidge` | ratio | iterations (both) | max coefficient difference |
|
|
742
|
+
|----------------|--------------------|-------------------------|-------|-------------------|----------------------------|
|
|
743
|
+
| 1,800 x 200 | 0.005 s | 0.026 s | 5.2x | 3 | 3.6e-15 |
|
|
744
|
+
| 5,000 x 1000 | 0.257 s | 1.231 s | 4.8x | 4 | 6.7e-15 |
|
|
745
|
+
| 100,000 x 200 | 0.126 s | 3.495 s | 27.8x | 3 | 2.8e-14 |
|
|
746
|
+
| 100,000 x 500 | 0.475 s | 8.909 s | 18.8x | 3 | 1.6e-14 |
|
|
747
|
+
| 500 x 5000 | 0.662 s | 8.689 s | 13.1x | 71 | 1.7e-15 |
|
|
748
|
+
|
|
749
|
+
The iteration counts are identical and the coefficients agree to
|
|
750
|
+
rounding, so the gap is the linear algebra alone: X'X by one BLAS
|
|
751
|
+
product against a full SVD of the tall X, and no pass over X inside
|
|
752
|
+
the loop. At 500 x 5000 scikit-learn takes the full 5000 x 5000 SVD
|
|
753
|
+
for `sigma_`; here `sigma_` comes from the 500 x 500 kernel side by
|
|
754
|
+
the Woodbury identity. This estimator is numpy, not translated code:
|
|
755
|
+
the eigendecomposition is LAPACK's either way.
|
|
756
|
+
|
|
757
|
+
## Classifiers and SVRs: `LinearSVM`, `KernelSVM`, `LinearSVR`, `KernelSVR`, `L1LogisticRegression`
|
|
758
|
+
|
|
759
|
+
Three classifiers and two regressors, each the algorithm scikit-learn runs, written out
|
|
760
|
+
in the Scheme subset and translated plainly (no derivation; the C++
|
|
761
|
+
is committed under `_generated/classifiers*`):
|
|
762
|
+
|
|
763
|
+
| class | algorithm | scikit-learn counterpart |
|
|
764
|
+
|---|---|---|
|
|
765
|
+
| `LinearSVM` | liblinear's dual coordinate descent for the hinge loss (random order, shrinking) | `LinearSVC(loss="hinge", dual=True, fit_intercept=False)` |
|
|
766
|
+
| `KernelSVM` | libsvm's SMO (WSS3, shrinking, the bias) over the whole kernel matrix | `SVC` (`kernel="rbf"`, `"linear"` or `"precomputed"`) |
|
|
767
|
+
| `L1LogisticRegression` | liblinear's newGLMNET (Newton over coordinate descent, line search) | `LogisticRegression(l1_ratio=1, solver="liblinear", fit_intercept=False)` |
|
|
768
|
+
| `LinearSVR` | liblinear's dual coordinate descent for the epsilon-insensitive loss (random order, shrinking) | `LinearSVR(loss="epsilon_insensitive", dual=True, fit_intercept=False)` |
|
|
769
|
+
| `KernelSVR` | the same SMO as `KernelSVM`, set up as libsvm's epsilon-SVR (2n variables) | `SVR` (`kernel="rbf"`, `"linear"` or `"precomputed"`) |
|
|
770
|
+
|
|
771
|
+
```python
|
|
772
|
+
from scm2cpp_learn import LinearSVM, KernelSVM, L1LogisticRegression
|
|
773
|
+
LinearSVM(C=1.0).fit(X, y).predict(X_new)
|
|
774
|
+
KernelSVM(C=1.0, gamma="scale").fit(X, y).decision_function(X_new)
|
|
775
|
+
L1LogisticRegression(C=0.1).fit(X, y).predict_proba(X_new)
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
The classifiers take two classes (the second of `classes_` is the
|
|
779
|
+
positive one), and `tol` / `max_iter` mean what they mean in scikit-learn.
|
|
780
|
+
`KernelSVM` and `KernelSVR` build the kernel matrix in numpy and keep all of it,
|
|
781
|
+
n x n doubles (800 MB at n = 10,000), where `SVC` computes rows on
|
|
782
|
+
demand through a cache; that is its price.
|
|
783
|
+
|
|
784
|
+
Measured with `check-classifiers.py` (in this directory): the same
|
|
785
|
+
problem and stopping rule on both sides, one core, best of three,
|
|
786
|
+
sklearn 1.9.1, load average under 2. Ours includes building the kernel
|
|
787
|
+
matrix (`KernelSVM`) or transposing X into columns
|
|
788
|
+
(`L1LogisticRegression`):
|
|
789
|
+
|
|
790
|
+
| problem | ours | scikit-learn | ratio | objective (ours / sklearn) | same predictions |
|
|
791
|
+
|---|---|---|---|---|---|
|
|
792
|
+
| `LinearSVM`, 5,000 x 200 | 0.379 s | 0.598 s | 1.6x | 1541.06 / 1541.29 | 99.5% |
|
|
793
|
+
| `LinearSVM`, 100,000 x 200 | 23.0 s | 41.7 s | 1.8x | 34503.5 / 34500.7 | 98.9% |
|
|
794
|
+
| `LinearSVM`, 2,000 x 20,000 | 0.975 s | 1.894 s | 1.9x | 0.052484 / 0.052428 | 100% |
|
|
795
|
+
| `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% |
|
|
796
|
+
| `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% |
|
|
797
|
+
| `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% |
|
|
798
|
+
| `L1LogisticRegression`, 5,000 x 200, C=0.1 | 0.064 s | 0.103 s | 1.6x | 61.676698 / 61.676708 | 100% |
|
|
799
|
+
| `L1LogisticRegression`, 20,000 x 500, C=0.05 | 1.20 s | 2.23 s | 1.9x | 106.77913 / 106.77915 | 100% |
|
|
800
|
+
| `L1LogisticRegression`, 100,000 x 200, C=0.05 | 9.06 s | 18.7 s | 2.1x | 349.61154 / 349.61216 | 100% |
|
|
801
|
+
|
|
802
|
+
On 2,000 x 20,000 neither side reaches the optimum at tol 1e-4 (the
|
|
803
|
+
objective at tol 1e-10 is 0.0519588), and where each stops depends on
|
|
804
|
+
the coordinate order: over eight seeds ours stops at 0.05240 to
|
|
805
|
+
0.05251 (0.9 to 1.1% above the optimum), `LinearSVC`, whose order is
|
|
806
|
+
random unless `random_state` is given, at 0.05242 to 0.05319 (0.9 to
|
|
807
|
+
2.4%). When `LinearSVC` draws one of its worse orders the gap passes
|
|
808
|
+
the script's 0.5% bound and the row prints `<-- disagrees`; ours is
|
|
809
|
+
then the better solution of the two, so the flag there is not a fault.
|
|
810
|
+
|
|
811
|
+
The regressors, from the same script (C = 1, epsilon 0.1):
|
|
812
|
+
|
|
813
|
+
| problem | ours | scikit-learn | ratio | agreement |
|
|
814
|
+
|---|---|---|---|---|
|
|
815
|
+
| `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 |
|
|
816
|
+
| `LinearSVR`, 20,000 x 500 | 22.1 s | 36.6 s | 1.7x | both at the cap; objective 3741.4 / 3712.2 |
|
|
817
|
+
| `KernelSVR` rbf, 1,000 x 10 | 0.023 s | 0.043 s | 1.9x | iterations 2,172 / 2,177; predictions within 4e-4 |
|
|
818
|
+
| `KernelSVR` rbf, 3,000 x 10 | 0.246 s | 0.332 s | 1.3x | iterations 7,471 / 7,581; within 5e-4 |
|
|
819
|
+
| `KernelSVR` rbf, 6,000 x 10 | 1.18 s | 1.38 s | 1.2x | iterations 16,805 / 16,630; within 5e-4 |
|
|
820
|
+
|
|
821
|
+
`LinearSVR` on these data does not reach its tolerance in 1,000 sweeps on
|
|
822
|
+
either side, and at the cap the random order alone moves the objective
|
|
823
|
+
by about 1% (scikit-learn's own seeds give 776-789 at 5,000 x 200);
|
|
824
|
+
with 20,000 sweeps both land on 728.58. `KernelSVR`'s gain shrinks
|
|
825
|
+
with n here because the 10 features make each kernel row cheap for
|
|
826
|
+
libsvm to compute, while ours reads the rows of a matrix of n^2
|
|
827
|
+
doubles; the predictions agree to within scikit-learn's tolerance.
|
|
828
|
+
|
|
829
|
+
The objectives agree to the stopping tolerance on both sides, not to
|
|
830
|
+
rounding. The linear SVM on tall data stops at the 1,000-sweep cap on
|
|
831
|
+
both sides, so there the two land a hair apart and 1% of the
|
|
832
|
+
predictions differ. The gain is the same in all three: the same
|
|
833
|
+
algorithm, run as dense loops that vectorise, against libsvm's and
|
|
834
|
+
liblinear's sparse rows. The classifiers are compiled at `-O3` with
|
|
835
|
+
`-fstrict-overflow`: Python's build flags carry
|
|
836
|
+
`-fno-strict-overflow`, which keeps the compiler from treating the
|
|
837
|
+
flat index `i*p + j` as an induction variable and makes the linear
|
|
838
|
+
SVM 25% slower.
|
|
839
|
+
|
|
840
|
+
## NMF: `CDNMF`
|
|
841
|
+
|
|
842
|
+
`CDNMF` is scikit-learn's `NMF(solver="cd")` -- Frobenius loss, no
|
|
843
|
+
regularization, no shuffling -- as the translator derives it.
|
|
844
|
+
`examples/kernel-only/nmf-cd.scm` writes the half step with the
|
|
845
|
+
residual R = X - W H and no Gram matrix. `--derive` memoises R H' over
|
|
846
|
+
the hoisted H H' and distributes the residual through the memo, so the
|
|
847
|
+
compiled half step is scikit-learn's `XHt - W HHt` with R never formed.
|
|
848
|
+
Like `DerivedLasso`, it is built in three variants (`nmf_backend()`
|
|
849
|
+
says which is taken: cublas when a device answers, else blas, else
|
|
850
|
+
loops).
|
|
851
|
+
|
|
852
|
+
```python
|
|
853
|
+
from scm2cpp_learn import CDNMF
|
|
854
|
+
model = CDNMF(n_components=20, random_state=0) # init="random", as sklearn's
|
|
855
|
+
W = model.fit_transform(X) # H in model.components_
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
`init="random"` draws the start as scikit-learn does, so the same seed
|
|
859
|
+
gives the same W and H. scikit-learn's default `"nndsvda"` is not
|
|
860
|
+
offered; pass `init="custom"` with its W and H for that start.
|
|
861
|
+
Measured with `check-classifiers.py --only nmf` against
|
|
862
|
+
`NMF(solver="cd", init="random")` from the same seed, tol 1e-4, at most
|
|
863
|
+
200 iterations, one core:
|
|
864
|
+
|
|
865
|
+
| n x p, k | `CDNMF` cuBLAS | `CDNMF` BLAS | sklearn | iterations | max \|H - H_sklearn\| |
|
|
866
|
+
|---|---|---|---|---|---|
|
|
867
|
+
| 1,000 x 500, 10 | 0.441 s (0.6x) | 0.212 s (1.15x) | 0.244 s | 200 / 200 | 1.3e-13 |
|
|
868
|
+
| 5,000 x 2,000, 20 | 7.01 s (1.6x) | 8.27 s (1.36x) | 11.25 s | 200 / 200 | 4.1e-12 |
|
|
869
|
+
| 20,000 x 1,000, 30 | 16.9 s (1.8x) | 20.6 s (1.46x) | 30.1 s | 200 / 200 | 2.4e-12 |
|
|
870
|
+
|
|
871
|
+
The iterates are scikit-learn's to rounding. The cuBLAS variant pays
|
|
872
|
+
from the middle size on; at 1,000 x 500 its transfers cost more than
|
|
873
|
+
the products save, and `backend="blas"` is the faster choice there.
|
|
874
|
+
|
|
875
|
+
## Graphical lasso and RANSAC: `CDGraphicalLasso`, `LinearRANSAC`
|
|
876
|
+
|
|
877
|
+
Two more of scikit-learn's algorithms, written out in the Scheme subset
|
|
878
|
+
and translated plainly.
|
|
879
|
+
|
|
880
|
+
- `CDGraphicalLasso` is `GraphicalLasso(mode="cd")`: `_graphical_lasso`
|
|
881
|
+
with `enet_coordinate_descent_gram` inside it, its two matrix-vector
|
|
882
|
+
products as dgemv. `alpha`, `tol`, `enet_tol`, `max_iter` and
|
|
883
|
+
`assume_centered` mean what they mean in scikit-learn. scikit-learn's
|
|
884
|
+
`costs_` (a log-determinant every iteration) is not computed.
|
|
885
|
+
- `LinearRANSAC` is `RANSACRegressor` around `LinearRegression`, with
|
|
886
|
+
the same defaults. With scikit-learn installed, the subsets are drawn
|
|
887
|
+
exactly as it draws them, so `random_state` gives scikit-learn's fit.
|
|
888
|
+
|
|
889
|
+
```python
|
|
890
|
+
from scm2cpp_learn import CDGraphicalLasso, LinearRANSAC
|
|
891
|
+
prec = CDGraphicalLasso(alpha=0.1).fit(X).precision_
|
|
892
|
+
fit = LinearRANSAC(random_state=0).fit(X, y) # fit.coef_, fit.inlier_mask_
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
Measured through the package, one core, best of three, against
|
|
896
|
+
scikit-learn on the same data (an AR(1) design with n = 20p for the
|
|
897
|
+
graphical lasso, 100 iterations on both sides; a linear model with
|
|
898
|
+
outliers for RANSAC, max_trials 1000):
|
|
899
|
+
|
|
900
|
+
| problem | ours | scikit-learn | ratio | agreement |
|
|
901
|
+
|---|---|---|---|---|
|
|
902
|
+
| `CDGraphicalLasso`, p = 50 | 0.0069 s | 0.306 s | 44.5x | 100 / 100 iterations, precision to 1.8e-14 |
|
|
903
|
+
| `CDGraphicalLasso`, p = 200 | 0.286 s | 1.62 s | 5.7x | precision to 2.6e-14 |
|
|
904
|
+
| `CDGraphicalLasso`, p = 800 | 33.9 s | 43.8 s | 1.3x | precision to 5.0e-14 |
|
|
905
|
+
| `LinearRANSAC`, 10,000 x 5 | 0.0254 s | 0.0916 s | 3.6x | 98 / 98 trials, same inliers |
|
|
906
|
+
| `LinearRANSAC`, 100,000 x 10 | 0.153 s | 0.241 s | 1.6x | 97 / 97 trials, same inliers |
|
|
907
|
+
| `LinearRANSAC`, 20,000 x 20 | 0.470 s | 1.19 s | 2.5x | 1,000 / 1,000 trials, same inliers |
|
|
908
|
+
|
|
909
|
+
Both gains are mostly scikit-learn's Python between its compiled steps:
|
|
910
|
+
the per-column bookkeeping of the graphical lasso, and the per-trial
|
|
911
|
+
validation and fitting of RANSAC. They shrink as the compiled part
|
|
912
|
+
grows (the graphical lasso at large p).
|
|
913
|
+
|
|
914
|
+
## SGD, OMP and LARS: `SGDLinearClassifier`, `SGDLinearRegressor`, `GramOMP`, `GramLars`, `GramLassoLars`
|
|
915
|
+
|
|
916
|
+
scikit-learn's own algorithms, translated:
|
|
917
|
+
|
|
918
|
+
- `SGDLinearClassifier` and `SGDLinearRegressor` are
|
|
919
|
+
`SGDClassifier` / `SGDRegressor` at their defaults, run by
|
|
920
|
+
`_plain_sgd` with the points in order. Against
|
|
921
|
+
`shuffle=False` the weights are bit-identical. scikit-learn's
|
|
922
|
+
shuffling generator is not reproduced.
|
|
923
|
+
- `GramOMP` is `OrthogonalMatchingPursuit` over the Gram matrix (its
|
|
924
|
+
route for n > p, taken here always; X'X and X'y by dsyrk and dgemv).
|
|
925
|
+
- `GramLars` and `GramLassoLars` are `Lars` and `LassoLars` over the
|
|
926
|
+
Gram (their route for n > p, taken here always; the Gram formed in
|
|
927
|
+
numpy).
|
|
928
|
+
|
|
929
|
+
```python
|
|
930
|
+
from scm2cpp_learn import SGDLinearClassifier, GramOMP, GramLassoLars
|
|
931
|
+
SGDLinearClassifier().fit(X, y).predict(X_new)
|
|
932
|
+
GramOMP(n_nonzero_coefs=10).fit(X, y).coef_
|
|
933
|
+
GramLassoLars(alpha=0.01).fit(X, y).coef_
|
|
934
|
+
```
|
|
935
|
+
|
|
936
|
+
Each gives scikit-learn's coefficients, to the bit for SGD and to 1e-15
|
|
937
|
+
for the rest, with the same number of epochs or steps. The speed is the
|
|
938
|
+
repository README's table: 1.0-3.3x for SGD, 1.1-2.8x for OMP and
|
|
939
|
+
1.2-5.0x for LARS, largest on small problems, where scikit-learn's
|
|
940
|
+
Python between steps dominates.
|
|
941
|
+
|
|
942
|
+
## L2 logistic regression and Huber: `LBFGSLogisticRegression`, `LBFGSHuberRegressor`
|
|
943
|
+
|
|
944
|
+
`LogisticRegression(solver="lbfgs")` (the default; two classes
|
|
945
|
+
binomial, more multinomial) and `HuberRegressor`, with scipy's
|
|
946
|
+
L-BFGS-B written out in the subset under them. The products are dgemm
|
|
947
|
+
or dgemv where a CBLAS links.
|
|
948
|
+
|
|
949
|
+
```python
|
|
950
|
+
from scm2cpp_learn import LBFGSLogisticRegression, LBFGSHuberRegressor
|
|
951
|
+
LBFGSLogisticRegression(C=1.0).fit(X, y).predict_proba(X_new)
|
|
952
|
+
LBFGSHuberRegressor(epsilon=1.35).fit(X, y).coef_
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
Both take scikit-learn's iterations one for one and give its
|
|
956
|
+
coefficients to rounding (1e-13 at worst). The speed is the repository
|
|
957
|
+
README's table: 1.1-8x for the logistic regression (near 1x on a large
|
|
958
|
+
binary problem, where both sides are two matrix-vector products per
|
|
959
|
+
evaluation) and 2.5-18x for Huber.
|
|
960
|
+
|
|
961
|
+
## Gaussian mixture, FastICA, PCA: `EMGaussianMixture`, `ParallelFastICA`, `CovPCA`
|
|
962
|
+
|
|
963
|
+
`GaussianMixture(covariance_type="full")`, `FastICA` at its defaults
|
|
964
|
+
and `PCA` by the covariance's eigenvectors (scikit-learn's
|
|
965
|
+
"covariance_eigh", what "auto" picks for a tall design), each
|
|
966
|
+
scikit-learn's algorithm translated, the products dgemm / dsyrk where a
|
|
967
|
+
CBLAS links and the eigenproblem written in the subset.
|
|
968
|
+
|
|
969
|
+
```python
|
|
970
|
+
from scm2cpp_learn import EMGaussianMixture, ParallelFastICA, CovPCA
|
|
971
|
+
EMGaussianMixture(4, random_state=0).fit(X).predict(X) # KMeans start: scikit-learn's
|
|
972
|
+
ParallelFastICA(random_state=0).fit(X).transform(X)
|
|
973
|
+
CovPCA(n_components=10).fit(X).components_
|
|
974
|
+
```
|
|
975
|
+
|
|
976
|
+
From the same `random_state` each takes scikit-learn's iterations and
|
|
977
|
+
lands on its parameters to rounding. The speed is the repository
|
|
978
|
+
README's table: 1.6-2.4x for the mixture, 1.1-1.4x for FastICA, and
|
|
979
|
+
for PCA 1.3-4x up to about 100 features, 0.6-0.8x at 200-500, where
|
|
980
|
+
LAPACK's eigensolver wins.
|
|
981
|
+
|
|
982
|
+
## Perceptron, ARD, LassoLarsIC, L1 linear SVM: `SGDPerceptron`, `CovARDRegression`, `GramLassoLarsIC`, `L1LinearSVM`
|
|
983
|
+
|
|
984
|
+
- `SGDPerceptron` is `Perceptron`. `SGDLinearClassifier` /
|
|
985
|
+
`SGDLinearRegressor` also take `loss="perceptron"` /
|
|
986
|
+
`"epsilon_insensitive"`, `learning_rate="constant"` / `"pa1"` /
|
|
987
|
+
`"pa2"` and `penalty=None`, which covers scikit-learn's replacements
|
|
988
|
+
for the passive-aggressive estimators. All run with shuffle=False.
|
|
989
|
+
- `CovARDRegression` is `ARDRegression` (n >= p). With `hoist=True`,
|
|
990
|
+
the default, the kept features' Gram comes from X'X formed once;
|
|
991
|
+
`hoist=False` forms it every iteration, as scikit-learn does.
|
|
992
|
+
- `GramLassoLarsIC` is `LassoLarsIC`. The whole lasso path comes from
|
|
993
|
+
the translated LARS, and the criterion is evaluated from the Gram.
|
|
994
|
+
- `L1LinearSVM` is `LinearSVC(penalty="l1", dual=False)`, liblinear's
|
|
995
|
+
solve_l1r_l2_svc. It agrees with scikit-learn to the tolerance,
|
|
996
|
+
because the coordinate order comes from another generator.
|
|
997
|
+
|
|
998
|
+
```python
|
|
999
|
+
from scm2cpp_learn import SGDPerceptron, CovARDRegression, GramLassoLarsIC, L1LinearSVM
|
|
1000
|
+
SGDPerceptron().fit(X, y)
|
|
1001
|
+
CovARDRegression().fit(X, y).coef_
|
|
1002
|
+
GramLassoLarsIC("bic").fit(X, y).alpha_
|
|
1003
|
+
L1LinearSVM(C=1.0).fit(X, y).coef_
|
|
1004
|
+
```
|
|
1005
|
+
|
|
1006
|
+
Speed, from the repository README's table: SGD 1.2-3x, ARD 1.6-1.9x
|
|
1007
|
+
(3x with the Gram hoisted), LassoLarsIC 8-20x, the L1 SVM 1.1-1.5x.
|
|
1008
|
+
|
|
1009
|
+
## GLMs, CV and Theil-Sen: `LBFGSPoissonRegressor`, `LBFGSGammaRegressor`, `LBFGSTweedieRegressor`, `LBFGSLogisticRegressionCV`, `GramLarsCV`, `GramLassoLarsCV`, `GramOMPCV`, `WeiszfeldTheilSen`
|
|
1010
|
+
|
|
1011
|
+
- The GLMs are `PoissonRegressor`, `GammaRegressor` and
|
|
1012
|
+
`TweedieRegressor` (solver "lbfgs"), over the same L-BFGS-B as
|
|
1013
|
+
`LBFGSLogisticRegression`.
|
|
1014
|
+
- The CV classes reproduce `LogisticRegressionCV`, `LarsCV`,
|
|
1015
|
+
`LassoLarsCV` and `OrthogonalMatchingPursuitCV`: the same folds (KFold
|
|
1016
|
+
/ StratifiedKFold without shuffling), the same choice of C, alpha or
|
|
1017
|
+
number of columns, and the same refit.
|
|
1018
|
+
- `WeiszfeldTheilSen` is `TheilSenRegressor` on scikit-learn's own
|
|
1019
|
+
subsets.
|
|
1020
|
+
|
|
1021
|
+
```python
|
|
1022
|
+
from scm2cpp_learn import LBFGSPoissonRegressor, LBFGSLogisticRegressionCV, GramLassoLarsCV
|
|
1023
|
+
LBFGSPoissonRegressor(alpha=1e-3).fit(X, counts).predict(X_new)
|
|
1024
|
+
LBFGSLogisticRegressionCV().fit(X, labels).C_
|
|
1025
|
+
GramLassoLarsCV().fit(X, y).alpha_
|
|
1026
|
+
```
|
|
1027
|
+
|
|
1028
|
+
Speed, from the repository README's table: the GLMs 1.1-6x, the CV
|
|
1029
|
+
estimators 1.6-6.6x, Theil-Sen 1.3-1.8x (its subset drawing dominates).
|
|
1030
|
+
|
|
1031
|
+
## Feature selection: `f_regression`, `f_classif`, `SelectKBest`, `SelectPercentile`, `GramRFE`
|
|
1032
|
+
|
|
1033
|
+
scikit-learn's univariate F statistics and its selectors over them,
|
|
1034
|
+
and `RFE` around `Ridge` / `LinearRegression`, translated.
|
|
1035
|
+
`f_regression` also takes an n x q array of targets: the q results in
|
|
1036
|
+
one product, on the device when one answers.
|
|
1037
|
+
|
|
1038
|
+
```python
|
|
1039
|
+
from scm2cpp_learn import SelectKBest, GramRFE, f_regression
|
|
1040
|
+
SelectKBest("f_regression", k=20).fit(X, y).get_support()
|
|
1041
|
+
GramRFE(alpha=1.0, n_features_to_select=10).fit(X, y).ranking_
|
|
1042
|
+
F, p = f_regression(X, Y) # Y: n x q, every target at once
|
|
1043
|
+
```
|
|
1044
|
+
|
|
1045
|
+
The same scores, ranking and features as scikit-learn. RFE is 80-1,000x
|
|
1046
|
+
faster (one Gram and one Cholesky factor, downdated, instead of a refit
|
|
1047
|
+
per round), and f_regression over many targets 30-50x. The Gram-route
|
|
1048
|
+
estimators (`GramLassoLarsIC`, `GramLarsCV`, `GramLassoLarsCV`,
|
|
1049
|
+
`GramLars`, `GramLassoLars`, `CovPCA`) form a large X'X on the device
|
|
1050
|
+
when one answers. `SCM2CPP_NO_DEVICE=1` keeps everything on the CPU.
|
|
1051
|
+
|
|
1052
|
+
## Against Intel's sklearnex
|
|
1053
|
+
|
|
1054
|
+
sklearnex (scikit-learn-intelex 2026.2, oneDAL underneath) replaces
|
|
1055
|
+
some scikit-learn estimators with its own. `bench/sklearnex-compare.py`
|
|
1056
|
+
times the ones with a counterpart here, sklearnex's estimators
|
|
1057
|
+
imported directly (not patched in) so that scikit-learn itself is
|
|
1058
|
+
timed in the same process. The ratios are scikit-learn's or
|
|
1059
|
+
sklearnex's time over the package's best (its CPU build, or the device
|
|
1060
|
+
build where one answers). The package was built against Intel MKL
|
|
1061
|
+
(see the build section), its loops under OpenMP; the load average was
|
|
1062
|
+
about 2.3, so the times move by some percent.
|
|
1063
|
+
|
|
1064
|
+
| estimator, n x p | iterations (sk / sklearnex / ours) | vs scikit-learn, one core | vs sklearnex, one core | vs sklearnex, 20 cores |
|
|
1065
|
+
|---|---|---|---|---|
|
|
1066
|
+
| LogisticRegression, 200,000 x 100 | 6 / 12 / 6 | 6.8x (device) | 6.4x (device) | 1.8x (device); on the CPU 0.9x |
|
|
1067
|
+
| Ridge (alpha 1, no intercept), 200,000 x 200 | - | 1.7x | 0.8x | 2.3x |
|
|
1068
|
+
| PCA (10 components), 100,000 x 50 | - | 1.4x | 2.2x | 1.4x |
|
|
1069
|
+
| PCA (10 components), 50,000 x 500 | - | 1.1x | 1.4x | 1.6x (device); on the CPU 1.2x |
|
|
1070
|
+
| SVC (rbf), 5,000 x 20 | 4,858 / 7 / 4,882 | 1.4x | 1.6x | 0.5x |
|
|
1071
|
+
| SVR (rbf), 5,000 x 20 | 12,821 / 10 / 12,824 | 1.4x | 1.4x | 0.4x |
|
|
1072
|
+
| RFE(Ridge), 20 of 200 kept, 20,000 x 200 | - | 138x | 106x | 116x |
|
|
1073
|
+
|
|
1074
|
+
On one core the package is ahead everywhere but Ridge, where both
|
|
1075
|
+
sides are one symmetric rank-k update of the same size and sklearnex
|
|
1076
|
+
is 0.180 s to 0.219 s. With 20 cores both sides use every core: the
|
|
1077
|
+
package's loops run under OpenMP and its products in MKL's threads,
|
|
1078
|
+
so it is ahead on Ridge (the Gram by dsyrk and a Cholesky solve), PCA,
|
|
1079
|
+
the device build of logistic regression and RFE (an algorithm that
|
|
1080
|
+
does less work: the Gram once, the Cholesky factor downdated per drop,
|
|
1081
|
+
where scikit-learn refits), level on logistic regression on the CPU,
|
|
1082
|
+
and behind on the SVMs: SMO takes thousands of short steps, each
|
|
1083
|
+
waiting on the last, where sklearnex's solver takes 7 to 10 long ones. sklearnex is
|
|
1084
|
+
not scikit-learn's arithmetic: its logistic regression takes 12
|
|
1085
|
+
iterations to scikit-learn's 6 and its coefficients differ from
|
|
1086
|
+
scikit-learn's by 6e-3, where the package's agree to 4e-15; its SVMs
|
|
1087
|
+
report a different iteration count. This sklearnex accelerates no
|
|
1088
|
+
lasso, so the lasso tables above stand against scikit-learn alone.
|
|
1089
|
+
|
|
1090
|
+
## The GPU build
|
|
1091
|
+
|
|
1092
|
+
Where the CUDA toolkit is at install time, `ParallelFastICA`,
|
|
1093
|
+
`EMGaussianMixture`, `LBFGSLogisticRegression`,
|
|
1094
|
+
`LBFGSLogisticRegressionCV`, `LBFGSHuberRegressor` and the `LBFGS*`
|
|
1095
|
+
GLMs also get the same Scheme translated with `--cuda`
|
|
1096
|
+
(`libscm2cpp_gpu.so`). Its loops are CUDA kernels and its products
|
|
1097
|
+
cuBLAS, on arrays that stay on the device. The classes take it when a
|
|
1098
|
+
device answers and the data has a million entries or more, and give
|
|
1099
|
+
the CPU build's iterations and results to the last digits. Measured
|
|
1100
|
+
against scikit-learn: 10-22x for the L-BFGS fits, 6.7x FastICA, 3x the
|
|
1101
|
+
mixture and LogisticRegressionCV (the repository README's table).
|
|
1102
|
+
`SCM2CPP_NO_DEVICE=1` keeps everything on the CPU.
|
|
1103
|
+
|
|
1104
|
+
## Logistic regression with L1
|
|
1105
|
+
|
|
1106
|
+
`CovLogistic` solves L1-penalized logistic regression by
|
|
1107
|
+
majorization: the logistic Hessian is bounded by X'X/4, so the
|
|
1108
|
+
quadratic term is the same Gram matrix, fixed once, and each outer
|
|
1109
|
+
round costs one gradient pass before handing the majorizer to the
|
|
1110
|
+
same coordinate descent the lasso uses. Objectives agree with
|
|
1111
|
+
scikit-learn's `LogisticRegression(penalty="l1",
|
|
1112
|
+
fit_intercept=False)` at `C = 1/(n lam)` to 9e-15.
|
|
1113
|
+
|
|
1114
|
+
## Group lasso
|
|
1115
|
+
|
|
1116
|
+
`CovGroupLasso` penalizes whole groups -- `lam * sum_g sqrt(|g|)
|
|
1117
|
+
||b_g||` -- so correlated features enter or leave together. Block
|
|
1118
|
+
coordinate descent on the same Gram machinery: each block visit is
|
|
1119
|
+
one majorized proximal step (the group's Gram block dominated by its
|
|
1120
|
+
top eigenvalue, found once), descending monotonically. With size-one
|
|
1121
|
+
groups it reduces exactly to the lasso -- verified against sklearn to
|
|
1122
|
+
9e-16 -- and at convergence the group KKT conditions hold to 2e-12.
|
|
1123
|
+
|
|
1124
|
+
## Bootstrap on the GPU
|
|
1125
|
+
|
|
1126
|
+
`bootstrap` draws pairs-bootstrap resamples and refits them all at
|
|
1127
|
+
one lambda. Each resample's Gram matrix is `X' diag(m) X` for its
|
|
1128
|
+
multiplicity counts `m`, written as `Xs' Xs` with `Xs = diag(sqrt m) X`
|
|
1129
|
+
so that numpy hands it to the symmetric rank-k product (`dsyrk`,
|
|
1130
|
+
half the flops of `X' (diag(m) X)`) -- one BLAS call -- and because
|
|
1131
|
+
the problems are independent, the descents run as one batch: on the
|
|
1132
|
+
GPU, one block of threads per resample, each reading its own Gram
|
|
1133
|
+
matrix. The products are the cost: at n=5000, p=1000 a hundred
|
|
1134
|
+
resamples took 18.1 s as general products and 11.2 s as `dsyrk`,
|
|
1135
|
+
descents included.
|
|
1136
|
+
|
|
1137
|
+
```python
|
|
1138
|
+
betas = model.bootstrap(lam, n_boot=500, seed=0) # (500, p)
|
|
1139
|
+
freq = (abs(betas) > 1e-9).mean(axis=0) # selection frequency
|
|
1140
|
+
```
|
|
1141
|
+
|
|
1142
|
+
Requires constructing the model from `X, y` (a Gram matrix alone
|
|
1143
|
+
cannot be resampled by rows). GPU and CPU agree to machine
|
|
1144
|
+
precision.
|
|
1145
|
+
|
|
1146
|
+
## A design with structure
|
|
1147
|
+
|
|
1148
|
+
When the design matrix has structure, forming X'X the general way is
|
|
1149
|
+
the wrong move. `kernel` exposes the translated functions directly,
|
|
1150
|
+
and [`scm2cpp-tfs`](https://pypi.org/project/scm2cpp-tfs/) does exactly
|
|
1151
|
+
that for moving-average designs: it builds the Gram matrix from a
|
|
1152
|
+
series' prefix sums in O(n p) time, never forming the design. That
|
|
1153
|
+
package stands alone -- it carries its own copy of this descent -- so
|
|
1154
|
+
neither installs the other.
|
|
1155
|
+
|
|
1156
|
+
## Where this comes from
|
|
1157
|
+
|
|
1158
|
+
The C++ this compiles is committed to `python/scm2cpp-learn/` in the
|
|
1159
|
+
scm2cpp repository. `CovLasso` and the rest run on the translation of
|
|
1160
|
+
`examples/kernel-only/lasso-cov.scm`, a covariance-update sweep
|
|
1161
|
+
written by hand at the Gram level; `DerivedLasso` runs on the
|
|
1162
|
+
translation of `examples/kernel-only/lasso-design.scm`, the naive
|
|
1163
|
+
residual-form descent over X as numpy holds it, with the translator's `--derive --blas`
|
|
1164
|
+
deriving the same covariance form from it. The two are the same
|
|
1165
|
+
sweep over the same `dsyrk`, and they time the same. To refresh the
|
|
1166
|
+
committed C++ after changing the Scheme, run `regenerate.sh` -- that
|
|
1167
|
+
step, and only that step, needs Racket.
|
|
1168
|
+
|
|
1169
|
+
## License
|
|
1170
|
+
|
|
1171
|
+
MIT, the same as scm2cpp.
|