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