scm2cpp-learn 0.18.0__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_learn/__init__.py +3836 -0
- scm2cpp_learn/_classifiers.cp312-win_amd64.pyd +0 -0
- scm2cpp_learn/_compat.py +284 -0
- scm2cpp_learn/_generated/__init__.py +0 -0
- scm2cpp_learn/_generated/_loader.py +41 -0
- scm2cpp_learn/_generated/_loader_accel_cublas.py +173 -0
- scm2cpp_learn/_generated/_loader_classifiers.py +308 -0
- scm2cpp_learn/_generated/_loader_derived_blas.py +29 -0
- scm2cpp_learn/_generated/_loader_derived_cublas.py +29 -0
- scm2cpp_learn/_generated/_loader_derived_loops.py +29 -0
- scm2cpp_learn/_generated/_loader_gpu_cuda.py +313 -0
- scm2cpp_learn/_generated/_loader_gramops_blas.py +692 -0
- scm2cpp_learn/_generated/_loader_gramops_loops.py +711 -0
- scm2cpp_learn/_generated/_loader_nmf_blas.py +29 -0
- scm2cpp_learn/_generated/_loader_nmf_cublas.py +29 -0
- scm2cpp_learn/_generated/_loader_nmf_loops.py +29 -0
- scm2cpp_learn/_generated/accel_cublas/accel.cpp +24 -0
- scm2cpp_learn/_generated/accel_cublas/accel.hpp +1178 -0
- scm2cpp_learn/_generated/accel_cublas/accel_capi.cpp +76 -0
- scm2cpp_learn/_generated/classifiers.cpp +37 -0
- scm2cpp_learn/_generated/classifiers.hpp +3745 -0
- scm2cpp_learn/_generated/classifiers_capi.cpp +115 -0
- scm2cpp_learn/_generated/derived_blas/lasso_design.cpp +5 -0
- scm2cpp_learn/_generated/derived_blas/lasso_design.hpp +156 -0
- scm2cpp_learn/_generated/derived_blas/lasso_design_capi.cpp +19 -0
- scm2cpp_learn/_generated/derived_cublas/lasso_design.cpp +5 -0
- scm2cpp_learn/_generated/derived_cublas/lasso_design.hpp +158 -0
- scm2cpp_learn/_generated/derived_cublas/lasso_design_capi.cpp +19 -0
- scm2cpp_learn/_generated/derived_loops/lasso_design.cpp +5 -0
- scm2cpp_learn/_generated/derived_loops/lasso_design.hpp +179 -0
- scm2cpp_learn/_generated/derived_loops/lasso_design_capi.cpp +19 -0
- scm2cpp_learn/_generated/gpu_cuda/gpu.cpp +37 -0
- scm2cpp_learn/_generated/gpu_cuda/gpu.hpp +3846 -0
- scm2cpp_learn/_generated/gpu_cuda/gpu_capi.cpp +308 -0
- scm2cpp_learn/_generated/gramops_blas/gramops.cpp +82 -0
- scm2cpp_learn/_generated/gramops_blas/gramops.hpp +5411 -0
- scm2cpp_learn/_generated/gramops_blas/gramops_capi.cpp +244 -0
- scm2cpp_learn/_generated/gramops_loops/gramops.cpp +82 -0
- scm2cpp_learn/_generated/gramops_loops/gramops.hpp +6395 -0
- scm2cpp_learn/_generated/gramops_loops/gramops_capi.cpp +250 -0
- scm2cpp_learn/_generated/lasso_cov.cpp +7 -0
- scm2cpp_learn/_generated/lasso_cov.hpp +152 -0
- scm2cpp_learn/_generated/lasso_cov_capi.cpp +25 -0
- scm2cpp_learn/_generated/nmf_blas/nmf_cd.cpp +5 -0
- scm2cpp_learn/_generated/nmf_blas/nmf_cd.hpp +132 -0
- scm2cpp_learn/_generated/nmf_blas/nmf_cd_capi.cpp +19 -0
- scm2cpp_learn/_generated/nmf_cublas/nmf_cd.cpp +5 -0
- scm2cpp_learn/_generated/nmf_cublas/nmf_cd.hpp +136 -0
- scm2cpp_learn/_generated/nmf_cublas/nmf_cd_capi.cpp +19 -0
- scm2cpp_learn/_generated/nmf_loops/nmf_cd.cpp +5 -0
- scm2cpp_learn/_generated/nmf_loops/nmf_cd.hpp +167 -0
- scm2cpp_learn/_generated/nmf_loops/nmf_cd_capi.cpp +19 -0
- scm2cpp_learn/_generated/scm2cpp-blas.hpp +158 -0
- scm2cpp_learn/_generated/scm2cpp-cublas.hpp +155 -0
- scm2cpp_learn/_generated/scm2cpp-cuda.hpp +285 -0
- scm2cpp_learn/_generated/scm2cpp.hpp +1627 -0
- scm2cpp_learn/_gramops_blas.cp312-win_amd64.pyd +0 -0
- scm2cpp_learn/_lasso_cov.cp312-win_amd64.pyd +0 -0
- scm2cpp_learn/_lasso_derived_blas.cp312-win_amd64.pyd +0 -0
- scm2cpp_learn/_libfind.py +153 -0
- scm2cpp_learn/_nmf_blas.cp312-win_amd64.pyd +0 -0
- scm2cpp_learn/batch_capi.cu +428 -0
- scm2cpp_learn-0.18.0.dist-info/DELVEWHEEL +2 -0
- scm2cpp_learn-0.18.0.dist-info/METADATA +1171 -0
- scm2cpp_learn-0.18.0.dist-info/RECORD +72 -0
- scm2cpp_learn-0.18.0.dist-info/WHEEL +5 -0
- scm2cpp_learn-0.18.0.dist-info/top_level.txt +1 -0
- scm2cpp_learn.libs/.load-order-scm2cpp_learn-0.18.0 +4 -0
- scm2cpp_learn.libs/libopenblas-0486735c359a67419d8832e3c40e2837.dll +4 -0
- scm2cpp_learn.libs/msvcp140-73438573e87f9a2fd944b01def13566d.dll +0 -0
- scm2cpp_learn.libs/vcruntime140-052ad6a20d375957e82aa6a3c441ea54.dll +0 -0
- scm2cpp_learn.libs/vcruntime140_1-a198370e11c6e7469d1cf0ef5b3f433d.dll +0 -0
|
@@ -0,0 +1,3836 @@
|
|
|
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_learn 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
|
+
|
|
26
|
+
|
|
27
|
+
# start delvewheel patch
|
|
28
|
+
def _delvewheel_patch_1_13_2():
|
|
29
|
+
import ctypes
|
|
30
|
+
import os
|
|
31
|
+
import platform
|
|
32
|
+
import sys
|
|
33
|
+
libs_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, 'scm2cpp_learn.libs'))
|
|
34
|
+
is_conda_cpython = platform.python_implementation() == 'CPython' and (hasattr(ctypes.pythonapi, 'Anaconda_GetVersion') or 'packaged by conda-forge' in sys.version)
|
|
35
|
+
if sys.version_info[:2] >= (3, 8) and not is_conda_cpython or sys.version_info[:2] >= (3, 10):
|
|
36
|
+
if os.path.isdir(libs_dir):
|
|
37
|
+
os.add_dll_directory(libs_dir)
|
|
38
|
+
else:
|
|
39
|
+
load_order_filepath = os.path.join(libs_dir, '.load-order-scm2cpp_learn-0.18.0')
|
|
40
|
+
if os.path.isfile(load_order_filepath):
|
|
41
|
+
import ctypes.wintypes
|
|
42
|
+
with open(os.path.join(libs_dir, '.load-order-scm2cpp_learn-0.18.0'), encoding='utf-8') as file:
|
|
43
|
+
load_order = file.read().split()
|
|
44
|
+
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
|
|
45
|
+
kernel32.LoadLibraryExW.restype = ctypes.wintypes.HMODULE
|
|
46
|
+
kernel32.LoadLibraryExW.argtypes = ctypes.wintypes.LPCWSTR, ctypes.wintypes.HANDLE, ctypes.wintypes.DWORD
|
|
47
|
+
for lib in load_order:
|
|
48
|
+
lib_path = os.path.join(os.path.join(libs_dir, lib))
|
|
49
|
+
if os.path.isfile(lib_path) and not kernel32.LoadLibraryExW(lib_path, None, 8):
|
|
50
|
+
raise OSError('Error loading {}; {}'.format(lib, ctypes.FormatError(ctypes.get_last_error())))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
_delvewheel_patch_1_13_2()
|
|
54
|
+
del _delvewheel_patch_1_13_2
|
|
55
|
+
# end delvewheel patch
|
|
56
|
+
|
|
57
|
+
import ctypes
|
|
58
|
+
import warnings
|
|
59
|
+
|
|
60
|
+
import numpy as np
|
|
61
|
+
|
|
62
|
+
from ._generated import _loader as kernel
|
|
63
|
+
from ._libfind import load_batch_lib, derived_backends, nmf_backends
|
|
64
|
+
from ._compat import (BaseEstimator, ClassifierMixin, RegressorMixin, TransformerMixin,
|
|
65
|
+
DensityMixin, Tagged, fit_xy, fit_x, pred_x, classes_of,
|
|
66
|
+
check_is_fitted)
|
|
67
|
+
|
|
68
|
+
# the derived kernel's variants that were built, each bound to its own
|
|
69
|
+
# library: {'cublas': module, 'blas': module, 'loops': module}
|
|
70
|
+
_DERIVED = {}
|
|
71
|
+
for _v in derived_backends():
|
|
72
|
+
try:
|
|
73
|
+
_DERIVED[_v] = __import__("scm2cpp_learn._generated._loader_derived_" + _v,
|
|
74
|
+
fromlist=["lasso"])
|
|
75
|
+
except ImportError:
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
__all__ = ["GramLasso", "GramElasticNet", "GramRidge", "GramRidgeCV", "CovLasso", "CovRidge", "CovBayesianRidge", "CovLogistic", "CovGroupLasso",
|
|
79
|
+
"LinearSVM", "KernelSVM", "LinearSVR", "KernelSVR", "L1LogisticRegression",
|
|
80
|
+
"CDNMF", "nmf_backend", "CDGraphicalLasso", "LinearRANSAC",
|
|
81
|
+
"SGDLinearClassifier", "SGDLinearRegressor", "SGDPerceptron", "GramOMP", "GramLars", "GramLassoLars", "GramLassoLarsIC",
|
|
82
|
+
"LBFGSLogisticRegression", "LBFGSHuberRegressor", "LBFGSLogisticRegressionCV",
|
|
83
|
+
"LBFGSPoissonRegressor", "LBFGSGammaRegressor", "LBFGSTweedieRegressor",
|
|
84
|
+
"GramLarsCV", "GramLassoLarsCV", "GramOMPCV",
|
|
85
|
+
"EMGaussianMixture", "CovPCA", "GramKernelPCA", "GramLDA", "GramTruncatedSVD", "ParallelFastICA", "CovARDRegression",
|
|
86
|
+
"L1LinearSVM", "WeiszfeldTheilSen", "f_regression", "f_classif", "SelectKBest",
|
|
87
|
+
"SelectPercentile", "GramRFE",
|
|
88
|
+
"CovMultiTaskLasso", "CovMultiTaskLassoCV", "DerivedLasso",
|
|
89
|
+
"GramLogisticRegression", "GramGroupLasso", "GramMultiTaskLasso",
|
|
90
|
+
"GramMultiTaskElasticNet", "DesignLasso",
|
|
91
|
+
"cuda_available", "derived_backend", "kernel"]
|
|
92
|
+
__version__ = "0.18.0"
|
|
93
|
+
|
|
94
|
+
_BATCH = load_batch_lib()
|
|
95
|
+
_DP = ctypes.POINTER(ctypes.c_double)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _scale(beta):
|
|
99
|
+
"""What `tol` is measured against.
|
|
100
|
+
|
|
101
|
+
An absolute test on the coefficients is a trap when the features
|
|
102
|
+
are large: the Gram diagonal is then large too, every sweep moves
|
|
103
|
+
each coefficient by very little, and the descent declares victory
|
|
104
|
+
a long way from the solution. Measuring the move against the size
|
|
105
|
+
of the coefficients themselves makes the test mean the same thing
|
|
106
|
+
at any scale, and falls back to absolute when they are still near
|
|
107
|
+
zero.
|
|
108
|
+
"""
|
|
109
|
+
m = float(np.max(np.abs(beta))) if beta.size else 0.0
|
|
110
|
+
return m if m > 1.0 else 1.0
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def cuda_available():
|
|
114
|
+
"""True when the GPU batch path was built and a device answers."""
|
|
115
|
+
if _BATCH is None:
|
|
116
|
+
return False
|
|
117
|
+
g, c, b = np.ones(1), np.zeros(1), np.zeros(1)
|
|
118
|
+
lam = np.array([1e9]) # a lambda that zeroes everything
|
|
119
|
+
return 0 == _BATCH.scm2cpp_batch_descend(
|
|
120
|
+
g.ctypes.data_as(_DP), 1, c.ctypes.data_as(_DP),
|
|
121
|
+
b.ctypes.data_as(_DP), lam.ctypes.data_as(_DP), 1.0, 1, 1, 1.0,
|
|
122
|
+
20, 20, 1e-8)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class CovLasso:
|
|
126
|
+
"""Lasso over a design given by its Gram matrix.
|
|
127
|
+
|
|
128
|
+
Either hand it the design and the target, in which case X'X and
|
|
129
|
+
X'y are formed here, or hand it a Gram matrix and correlations
|
|
130
|
+
somebody else built more cheaply.
|
|
131
|
+
|
|
132
|
+
Parameters
|
|
133
|
+
----------
|
|
134
|
+
X : array (nobs, p), optional
|
|
135
|
+
The design matrix.
|
|
136
|
+
y : array (nobs,), optional
|
|
137
|
+
The target.
|
|
138
|
+
gram : array (p, p) or (p*p,), optional
|
|
139
|
+
X'X, if it is already known.
|
|
140
|
+
corr : array (p,), optional
|
|
141
|
+
X'y, likewise.
|
|
142
|
+
nobs : int, optional
|
|
143
|
+
Rows behind `gram`; required when passing one, since the
|
|
144
|
+
penalty is scaled by it as scikit-learn's alpha is.
|
|
145
|
+
|
|
146
|
+
The objective is the usual one, matching scikit-learn's `Lasso`
|
|
147
|
+
with `fit_intercept=False`:
|
|
148
|
+
|
|
149
|
+
(1 / 2 nobs) ||y - X b||^2 + lam ||b||_1
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
def __init__(self, X=None, y=None, *, gram=None, corr=None, nobs=None):
|
|
153
|
+
if gram is not None:
|
|
154
|
+
if corr is None or nobs is None:
|
|
155
|
+
raise ValueError("gram needs corr and nobs alongside it")
|
|
156
|
+
g = np.ascontiguousarray(gram, dtype=np.float64).ravel()
|
|
157
|
+
c = np.ascontiguousarray(corr, dtype=np.float64)
|
|
158
|
+
p = c.size
|
|
159
|
+
if g.size != p * p:
|
|
160
|
+
raise ValueError(f"gram is {g.size} entries; expected {p*p}")
|
|
161
|
+
self.p, self.nobs, self.g, self.c0 = p, int(nobs), g, c
|
|
162
|
+
else:
|
|
163
|
+
if X is None or y is None:
|
|
164
|
+
raise ValueError("pass X and y, or gram, corr and nobs")
|
|
165
|
+
X = np.ascontiguousarray(X, dtype=np.float64)
|
|
166
|
+
y = np.ascontiguousarray(y, dtype=np.float64)
|
|
167
|
+
if X.ndim != 2 or y.ndim != 1 or X.shape[0] != y.size:
|
|
168
|
+
raise ValueError(
|
|
169
|
+
f"X is {X.shape} and y is {y.shape}; expected (n, p) "
|
|
170
|
+
"and (n,)")
|
|
171
|
+
self.nobs, self.p = X.shape
|
|
172
|
+
self.g = np.ascontiguousarray((X.T @ X).ravel())
|
|
173
|
+
self.c0 = np.ascontiguousarray(X.T @ y)
|
|
174
|
+
self.X, self.y = X, y
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# ------------------------- choosing lambdas -------------------------
|
|
178
|
+
|
|
179
|
+
def lambda_max(self, l1_ratio=1.0):
|
|
180
|
+
"""The smallest penalty that leaves every coefficient at zero.
|
|
181
|
+
|
|
182
|
+
Above it the solution is exactly zero, so a path that starts
|
|
183
|
+
here starts where the first coefficient is about to enter --
|
|
184
|
+
which is the only scale-free place to start, since the penalty
|
|
185
|
+
is compared against correlations on the features' own scale.
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
return float(np.max(np.abs(self.c0))) / (self.nobs * l1_ratio)
|
|
189
|
+
|
|
190
|
+
def lambda_grid(self, num=100, eps=1e-3, l1_ratio=1.0):
|
|
191
|
+
"""A log-spaced path from lambda_max down to eps * lambda_max.
|
|
192
|
+
|
|
193
|
+
The same construction scikit-learn uses, and the same default
|
|
194
|
+
ratio; give the result to fit_path, which expects descending
|
|
195
|
+
lambdas.
|
|
196
|
+
"""
|
|
197
|
+
hi = self.lambda_max(l1_ratio)
|
|
198
|
+
return hi * np.logspace(0, np.log10(eps), int(num))
|
|
199
|
+
|
|
200
|
+
# ---------------------------- fitting ----------------------------
|
|
201
|
+
|
|
202
|
+
def fit(self, lam, **kw):
|
|
203
|
+
"""Coefficients at one lambda.
|
|
204
|
+
|
|
205
|
+
A single penalty is the one-element special case of the path:
|
|
206
|
+
the Gram matrix was built in the constructor, so this costs one
|
|
207
|
+
descent and nothing per row -- and a warm-started path over a
|
|
208
|
+
whole grid costs barely more, which is why fit_path is the
|
|
209
|
+
primary interface rather than this convenience.
|
|
210
|
+
"""
|
|
211
|
+
return self.fit_path([lam], **kw)[0]
|
|
212
|
+
|
|
213
|
+
def fit_path(self, lambdas, tol=1e-8, chunk=20, max_sweeps=100000,
|
|
214
|
+
l1_ratio=1.0):
|
|
215
|
+
"""Coefficients per lambda, each warm-started from the last.
|
|
216
|
+
|
|
217
|
+
Returns an array of shape (len(lambdas), p). Give lambdas in
|
|
218
|
+
descending order: that is what makes the warm start pay.
|
|
219
|
+
"""
|
|
220
|
+
lambdas = np.asarray(lambdas, dtype=np.float64)
|
|
221
|
+
beta, c = np.zeros(self.p), self.c0.copy()
|
|
222
|
+
prev = np.empty(self.p)
|
|
223
|
+
out = np.empty((lambdas.size, self.p))
|
|
224
|
+
for i, lam in enumerate(lambdas):
|
|
225
|
+
swept = 0
|
|
226
|
+
while swept < max_sweeps:
|
|
227
|
+
prev[:] = beta
|
|
228
|
+
kernel.enet_descend(self.g, c, beta,
|
|
229
|
+
float(lam) * l1_ratio,
|
|
230
|
+
float(lam) * (1.0 - l1_ratio), chunk,
|
|
231
|
+
float(self.nobs), self.p)
|
|
232
|
+
swept += chunk
|
|
233
|
+
if np.max(np.abs(beta - prev)) < tol * _scale(beta):
|
|
234
|
+
break
|
|
235
|
+
out[i] = beta
|
|
236
|
+
return out
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def bootstrap(self, lam, n_boot=200, l1_ratio=1.0, seed=None,
|
|
240
|
+
force_cpu=False, **kw):
|
|
241
|
+
"""Pairs-bootstrap coefficients at one lambda, (n_boot, p).
|
|
242
|
+
|
|
243
|
+
Each resample draws rows with replacement; its Gram matrix is
|
|
244
|
+
X' diag(m) X with the multiplicity counts m, formed as Xs' Xs
|
|
245
|
+
for Xs = diag(sqrt m) X so that numpy takes the symmetric
|
|
246
|
+
rank-k product (dsyrk, half the flops of a general product)
|
|
247
|
+
-- one BLAS call per resample -- and the descents run as one
|
|
248
|
+
batch: on the GPU one block of threads per resample over its
|
|
249
|
+
own Gram, since the problems are independent. Needs the
|
|
250
|
+
design matrix, so it is unavailable when the model was built
|
|
251
|
+
from a Gram matrix alone.
|
|
252
|
+
"""
|
|
253
|
+
if not hasattr(self, "X"):
|
|
254
|
+
raise ValueError("bootstrap needs X; construct from X and y")
|
|
255
|
+
rng = np.random.default_rng(seed)
|
|
256
|
+
B = int(n_boot)
|
|
257
|
+
grams = np.empty((B, self.p * self.p))
|
|
258
|
+
corrs = np.empty((B, self.p))
|
|
259
|
+
for b in range(B):
|
|
260
|
+
m = rng.multinomial(self.nobs,
|
|
261
|
+
np.full(self.nobs, 1.0 / self.nobs))
|
|
262
|
+
Xs = self.X * np.sqrt(m)[:, None]
|
|
263
|
+
grams[b] = (Xs.T @ Xs).ravel()
|
|
264
|
+
corrs[b] = self.X.T @ (m * self.y)
|
|
265
|
+
lams = (np.ascontiguousarray(lam, dtype=np.float64)
|
|
266
|
+
if np.ndim(lam) else np.full(B, float(lam)))
|
|
267
|
+
if not force_cpu:
|
|
268
|
+
beta = _grid_descend(grams, corrs, lams, self.p, 0,
|
|
269
|
+
l1_ratio=l1_ratio, nobs=float(self.nobs),
|
|
270
|
+
**kw)
|
|
271
|
+
if beta is not None:
|
|
272
|
+
return beta
|
|
273
|
+
return _batch_descend_multi(grams, corrs, lams, self.nobs, self.p,
|
|
274
|
+
l1_ratio=l1_ratio, force_cpu=True,
|
|
275
|
+
kernel_fn=kernel.enet_descend, **kw)
|
|
276
|
+
|
|
277
|
+
def fit_path_batch(self, lambdas, tol=1e-8, chunk=20,
|
|
278
|
+
max_sweeps=100000, force_cpu=False, l1_ratio=1.0):
|
|
279
|
+
"""Coefficients per lambda, every lambda solved from zero.
|
|
280
|
+
|
|
281
|
+
Independent problems, so they go to the GPU together when one
|
|
282
|
+
is available: one launch, one block of threads per lambda, all
|
|
283
|
+
sharing this Gram matrix. This is the shape of a
|
|
284
|
+
cross-validation grid, where warm starting across lambdas is
|
|
285
|
+
not on offer anyway.
|
|
286
|
+
"""
|
|
287
|
+
lambdas = np.ascontiguousarray(lambdas, dtype=np.float64)
|
|
288
|
+
batch = lambdas.size
|
|
289
|
+
if not force_cpu:
|
|
290
|
+
beta = _grid_descend(self.g[None, :], self.c0[None, :], lambdas,
|
|
291
|
+
self.p, 0, tol=tol, chunk=chunk,
|
|
292
|
+
max_sweeps=max_sweeps, l1_ratio=l1_ratio,
|
|
293
|
+
nobs=float(self.nobs))
|
|
294
|
+
if beta is not None:
|
|
295
|
+
return beta
|
|
296
|
+
# a device that refused the work is not a reason to fail
|
|
297
|
+
beta = np.zeros((batch, self.p))
|
|
298
|
+
c = np.tile(self.c0, (batch, 1))
|
|
299
|
+
prev = np.empty(self.p)
|
|
300
|
+
for t in range(batch):
|
|
301
|
+
bt, ct, swept = beta[t], c[t], 0
|
|
302
|
+
while swept < max_sweeps:
|
|
303
|
+
prev[:] = bt
|
|
304
|
+
kernel.enet_descend(self.g, ct, bt,
|
|
305
|
+
float(lambdas[t]) * l1_ratio,
|
|
306
|
+
float(lambdas[t]) * (1.0 - l1_ratio), chunk,
|
|
307
|
+
float(self.nobs), self.p)
|
|
308
|
+
swept += chunk
|
|
309
|
+
if np.max(np.abs(bt - prev)) < tol * _scale(bt):
|
|
310
|
+
break
|
|
311
|
+
return beta
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
class DerivedLasso:
|
|
315
|
+
"""Lasso from the design matrix, by the kernel the translator derived.
|
|
316
|
+
|
|
317
|
+
CovLasso above is the usable one: numpy forms the Gram matrix, a
|
|
318
|
+
hand-written sweep (lasso-cov.scm) runs over it, and cross-
|
|
319
|
+
validation, bootstrap and the GPU grid all feed that sweep their
|
|
320
|
+
own Gram matrices. This class exists to show the translator's
|
|
321
|
+
work instead. Its kernel is examples/kernel-only/lasso-design.scm
|
|
322
|
+
-- the residual-form coordinate descent, the algorithm inside
|
|
323
|
+
scikit-learn's Lasso(precompute=False), written as flat loops over
|
|
324
|
+
X as numpy holds it (n rows of p), the residual and the column
|
|
325
|
+
norms taken inside, no Gram matrix in sight -- translated with
|
|
326
|
+
--derive and --blas:
|
|
327
|
+
the translator raised the loops to the array algebra, replaced the
|
|
328
|
+
residual by the memo c = X'r kept current through a Gram matrix it
|
|
329
|
+
hoisted in front of the penalty path, folded the column norms into
|
|
330
|
+
that Gram's diagonal, folded the products into one dsyrk and two
|
|
331
|
+
dgemv calls, and put the result under (if (< p n) ..)
|
|
332
|
+
with the original sweep in the else branch -- scikit-learn's
|
|
333
|
+
precompute='auto' rule, derived. Nothing in it is written at the Gram level by hand, and numpy
|
|
334
|
+
prepares nothing: X and y go in as they are.
|
|
335
|
+
|
|
336
|
+
Three variants of the same derivation can be built: 'cublas'
|
|
337
|
+
(--derive --cublas: the products on the device, X uploaded once per
|
|
338
|
+
fit_path call), 'blas' (--derive --blas, CBLAS) and 'loops'
|
|
339
|
+
(--derive alone: the Gram a scalar loop nest, much slower on large
|
|
340
|
+
designs). backend=None takes the best one this machine built and
|
|
341
|
+
can run -- cublas only when a device answers -- and
|
|
342
|
+
derived_backend() says which that is; backend='blas' etc. insists.
|
|
343
|
+
|
|
344
|
+
The constructor only checks shapes and keeps X (C-contiguous, as
|
|
345
|
+
numpy makes it) and y; everything else happens inside fit_path.
|
|
346
|
+
|
|
347
|
+
Convergence is the kernel's: a sweep in which no coefficient moved
|
|
348
|
+
by more than tol (absolute) ends the fit at that lambda, max_sweeps
|
|
349
|
+
bounds it; tol=0.0 asks for the exact fixed point. Same objective
|
|
350
|
+
as CovLasso.
|
|
351
|
+
"""
|
|
352
|
+
|
|
353
|
+
def __init__(self, X, y, backend=None):
|
|
354
|
+
if backend is None:
|
|
355
|
+
backend = derived_backend()
|
|
356
|
+
if backend is None:
|
|
357
|
+
raise ImportError(
|
|
358
|
+
"scm2cpp_learn: the derived kernel was not built; "
|
|
359
|
+
"reinstall the package (pip install --force-reinstall "
|
|
360
|
+
"scm2cpp-learn)")
|
|
361
|
+
elif backend not in _DERIVED:
|
|
362
|
+
raise ValueError(f"derived kernel backend {backend!r} is not built; "
|
|
363
|
+
f"built: {sorted(_DERIVED)}")
|
|
364
|
+
self.backend = backend
|
|
365
|
+
self._kernel = _DERIVED[backend]
|
|
366
|
+
X = np.ascontiguousarray(X, dtype=np.float64)
|
|
367
|
+
y = np.ascontiguousarray(y, dtype=np.float64)
|
|
368
|
+
if X.ndim != 2 or y.ndim != 1 or X.shape[0] != y.size:
|
|
369
|
+
raise ValueError(
|
|
370
|
+
f"X is {X.shape} and y is {y.shape}; expected (n, p) and (n,)")
|
|
371
|
+
self.nobs, self.p = X.shape
|
|
372
|
+
self.X, self.y = X, y
|
|
373
|
+
self._c0 = None
|
|
374
|
+
|
|
375
|
+
@property
|
|
376
|
+
def c0(self):
|
|
377
|
+
# X'y, needed only to place the penalty grid
|
|
378
|
+
if self._c0 is None:
|
|
379
|
+
self._c0 = self.X.T @ self.y
|
|
380
|
+
return self._c0
|
|
381
|
+
|
|
382
|
+
def lambda_max(self):
|
|
383
|
+
return float(np.max(np.abs(self.c0))) / self.nobs
|
|
384
|
+
|
|
385
|
+
def lambda_grid(self, num=100, eps=1e-3):
|
|
386
|
+
hi = self.lambda_max()
|
|
387
|
+
return hi * np.logspace(0, np.log10(eps), int(num))
|
|
388
|
+
|
|
389
|
+
def fit(self, lam, **kw):
|
|
390
|
+
return self.fit_path([lam], **kw)[0]
|
|
391
|
+
|
|
392
|
+
def fit_path(self, lambdas, tol=1e-8, max_sweeps=100000):
|
|
393
|
+
"""Coefficients per lambda, warm-started, shape (len(lambdas), p).
|
|
394
|
+
|
|
395
|
+
One call into the derived kernel: it takes the residual and
|
|
396
|
+
the column norms (folded into the Gram's diagonal when p < n),
|
|
397
|
+
builds the Gram matrix, walks the penalties, and leaves the
|
|
398
|
+
coefficient at each in a row of the result. tol is the largest move of
|
|
399
|
+
one coefficient in a sweep below which that lambda is done. The
|
|
400
|
+
kernel's value, the sweeps run over the whole path, is kept as
|
|
401
|
+
n_iter_.
|
|
402
|
+
"""
|
|
403
|
+
lams = np.ascontiguousarray(lambdas, dtype=np.float64).ravel()
|
|
404
|
+
beta = np.zeros(self.p)
|
|
405
|
+
resid = np.empty(self.nobs) # filled by the kernel from y
|
|
406
|
+
xnorm = np.empty(self.p) # the column norms, taken inside
|
|
407
|
+
betas = np.zeros(lams.size * self.p)
|
|
408
|
+
self.n_iter_ = int(self._kernel.lasso_design(
|
|
409
|
+
self.X, self.y, resid, xnorm, beta, lams, betas, int(max_sweeps), float(tol),
|
|
410
|
+
self.nobs, self.p, lams.size))
|
|
411
|
+
return betas.reshape(lams.size, self.p)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
class DesignLasso(Tagged, RegressorMixin, BaseEstimator):
|
|
415
|
+
"""scikit-learn's ``Lasso`` interface over DerivedLasso: fit(X, y)
|
|
416
|
+
centres the data when ``fit_intercept`` and runs the derived kernel
|
|
417
|
+
at one penalty ``alpha``, same objective as GramLasso. ``tol`` is
|
|
418
|
+
the kernel's (the largest absolute move of one coefficient in a
|
|
419
|
+
sweep), ``max_iter`` its sweep cap and ``backend`` DerivedLasso's.
|
|
420
|
+
|
|
421
|
+
Attributes after fit: coef_, intercept_, n_iter_ (the sweeps the
|
|
422
|
+
kernel ran), n_features_in_.
|
|
423
|
+
"""
|
|
424
|
+
|
|
425
|
+
def __init__(self, alpha=1.0, *, fit_intercept=True, tol=1e-8, max_iter=100000,
|
|
426
|
+
backend=None):
|
|
427
|
+
self.alpha, self.fit_intercept, self.tol, self.max_iter = alpha, fit_intercept, tol, max_iter
|
|
428
|
+
self.backend = backend
|
|
429
|
+
|
|
430
|
+
def fit(self, X, y):
|
|
431
|
+
X, y = fit_xy(self, X, y)
|
|
432
|
+
Xc, yc, xm, ym = _centered(X, y, self.fit_intercept)
|
|
433
|
+
m = DerivedLasso(Xc, yc, backend=self.backend)
|
|
434
|
+
self.coef_ = m.fit(float(self.alpha), tol=float(self.tol), max_sweeps=int(self.max_iter))
|
|
435
|
+
self.n_iter_ = m.n_iter_
|
|
436
|
+
self.intercept_ = float(ym - xm @ self.coef_)
|
|
437
|
+
return self
|
|
438
|
+
|
|
439
|
+
def predict(self, X):
|
|
440
|
+
return pred_x(self, X) @ self.coef_ + self.intercept_
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def derived_backend():
|
|
444
|
+
"""The derived kernel variant DerivedLasso takes by default: 'cublas'
|
|
445
|
+
when built and a device answers, else 'blas', else 'loops'; None
|
|
446
|
+
when none was built."""
|
|
447
|
+
for v in ("cublas", "blas", "loops"):
|
|
448
|
+
if v in _DERIVED and (v != "cublas" or cuda_available()):
|
|
449
|
+
return v
|
|
450
|
+
return None
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _batch_descend_multi(grams, corrs, lam, nobs, p, tol=1e-8, chunk=20,
|
|
454
|
+
max_sweeps=100000, l1_ratio=1.0, force_cpu=False,
|
|
455
|
+
kernel_fn=None):
|
|
456
|
+
"""Descend a batch where every problem has its own Gram matrix.
|
|
457
|
+
|
|
458
|
+
grams is (B, p*p) and corrs (B, p); one thread per problem on the
|
|
459
|
+
GPU when it is there (the kernel before 0.7.0, kept for the
|
|
460
|
+
comparison in bench/cv-grid-designs.py), the same chunked-tolerance
|
|
461
|
+
loop on the CPU when it is not -- which is what the bootstrap
|
|
462
|
+
falls back to.
|
|
463
|
+
"""
|
|
464
|
+
B = corrs.shape[0]
|
|
465
|
+
import ctypes
|
|
466
|
+
beta = np.zeros((B, p))
|
|
467
|
+
c = np.ascontiguousarray(corrs.copy())
|
|
468
|
+
g = np.ascontiguousarray(grams)
|
|
469
|
+
# lam may be one penalty for every thread or one per thread; the
|
|
470
|
+
# kernel always reads a per-thread array
|
|
471
|
+
lams = (np.ascontiguousarray(lam, dtype=np.float64)
|
|
472
|
+
if np.ndim(lam) else np.full(B, float(lam)))
|
|
473
|
+
if _BATCH is not None and not force_cpu:
|
|
474
|
+
dp = ctypes.POINTER(ctypes.c_double)
|
|
475
|
+
rc = _BATCH.scm2cpp_batch_descend(
|
|
476
|
+
g.ctypes.data_as(dp), int(B), c.ctypes.data_as(dp),
|
|
477
|
+
beta.ctypes.data_as(dp), lams.ctypes.data_as(dp),
|
|
478
|
+
float(l1_ratio), int(B), int(p), float(nobs),
|
|
479
|
+
int(max_sweeps), int(chunk), float(tol))
|
|
480
|
+
if rc == 0:
|
|
481
|
+
return beta
|
|
482
|
+
prev = np.empty(p)
|
|
483
|
+
for b in range(B):
|
|
484
|
+
gb, cb, bb = g[b], c[b], beta[b]
|
|
485
|
+
lam_b = float(lams[b])
|
|
486
|
+
swept = 0
|
|
487
|
+
while swept < max_sweeps:
|
|
488
|
+
prev[:] = bb
|
|
489
|
+
kernel_fn(gb, cb, bb, lam_b * l1_ratio,
|
|
490
|
+
lam_b * (1.0 - l1_ratio), chunk,
|
|
491
|
+
float(nobs), p)
|
|
492
|
+
swept += chunk
|
|
493
|
+
if np.max(np.abs(bb - prev)) < tol * max(
|
|
494
|
+
1.0, float(np.max(np.abs(bb)))):
|
|
495
|
+
break
|
|
496
|
+
return beta
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _grid_descend(grams, corrs, lams, p, ntask, tol=1e-8, chunk=20,
|
|
500
|
+
max_sweeps=100000, l1_ratio=1.0, nobs=1.0):
|
|
501
|
+
"""A batch of cold descents on the GPU, one block per problem.
|
|
502
|
+
|
|
503
|
+
grams is (n_grams, p*p), corrs (n_grams, width) with width p or
|
|
504
|
+
p*ntask, and lams the penalties of the batch in Gram-major order:
|
|
505
|
+
problem i uses Gram i // (batch // n_grams). Every problem starts
|
|
506
|
+
from zero and nothing is replicated: a cross-validation grid of cv
|
|
507
|
+
folds by num penalties moves cv Gram matrices, a fit_path_batch
|
|
508
|
+
one, a bootstrap one per resample. Returns (batch, width), or
|
|
509
|
+
None when the GPU path is not there or declined the shape, in
|
|
510
|
+
which case the caller runs its CPU path.
|
|
511
|
+
"""
|
|
512
|
+
fn = getattr(_BATCH, "scm2cpp_cv_descend", None) if _BATCH else None
|
|
513
|
+
if fn is None:
|
|
514
|
+
return None
|
|
515
|
+
n_grams, batch = corrs.shape[0], lams.size
|
|
516
|
+
if batch % n_grams:
|
|
517
|
+
return None
|
|
518
|
+
width = p * (ntask if ntask > 0 else 1)
|
|
519
|
+
g = np.ascontiguousarray(grams, dtype=np.float64)
|
|
520
|
+
c = np.ascontiguousarray(np.repeat(corrs, batch // n_grams, axis=0),
|
|
521
|
+
dtype=np.float64)
|
|
522
|
+
w = np.zeros((batch, width))
|
|
523
|
+
lams = np.ascontiguousarray(lams, dtype=np.float64)
|
|
524
|
+
rc = fn(g.ctypes.data_as(_DP), int(n_grams), c.ctypes.data_as(_DP),
|
|
525
|
+
w.ctypes.data_as(_DP), lams.ctypes.data_as(_DP),
|
|
526
|
+
float(l1_ratio), int(batch), int(p), int(ntask), float(nobs),
|
|
527
|
+
int(max_sweeps), int(chunk), float(tol), 1, 1)
|
|
528
|
+
return w if rc == 0 else None
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
class CovLassoCV(Tagged, RegressorMixin, BaseEstimator):
|
|
532
|
+
"""Cross-validated lasso, scikit-learn's LassoCV semantics.
|
|
533
|
+
|
|
534
|
+
The lambda grid comes from the full data (lambda_max down, the
|
|
535
|
+
sklearn construction), the folds are contiguous splits in order
|
|
536
|
+
(sklearn's KFold default), and validation error is one matrix
|
|
537
|
+
product per fold. alpha_ minimises the mean validation MSE, ties
|
|
538
|
+
going to the larger penalty as in scikit-learn, and coef_ is the
|
|
539
|
+
refit on all rows at alpha_.
|
|
540
|
+
|
|
541
|
+
With a GPU the whole grid -- cv folds by num penalties, each from
|
|
542
|
+
zero -- is one launch with one block of threads per problem, the
|
|
543
|
+
fold's Gram matrix shared by its problems; on the CPU each fold's
|
|
544
|
+
path is walked warm. force_cpu picks the CPU path; force_gpu is
|
|
545
|
+
accepted for compatibility (the GPU is now taken whenever it is
|
|
546
|
+
there and force_cpu is not set).
|
|
547
|
+
|
|
548
|
+
Attributes after fit: alphas_, mse_path_ (num, cv), alpha_, coef_.
|
|
549
|
+
"""
|
|
550
|
+
|
|
551
|
+
def __init__(self, cv=5, num=100, eps=1e-3, tol=1e-8, l1_ratio=1.0,
|
|
552
|
+
force_cpu=False, force_gpu=False, n_jobs=1):
|
|
553
|
+
# Folds are independent, the descent is a ctypes call and the
|
|
554
|
+
# products are BLAS calls, both of which release the GIL, so
|
|
555
|
+
# plain threads scale the CPU path across folds. Default 1:
|
|
556
|
+
# sequential, like scikit-learn's n_jobs=None.
|
|
557
|
+
self.cv, self.num, self.eps, self.tol = cv, num, eps, tol
|
|
558
|
+
self.l1_ratio, self.force_cpu, self.force_gpu = l1_ratio, force_cpu, force_gpu
|
|
559
|
+
self.n_jobs = n_jobs
|
|
560
|
+
|
|
561
|
+
def fit(self, X, y):
|
|
562
|
+
X, y = fit_xy(self, X, y)
|
|
563
|
+
n = len(y)
|
|
564
|
+
cv, num = int(self.cv), int(self.num)
|
|
565
|
+
tol, l1_ratio = float(self.tol), float(self.l1_ratio)
|
|
566
|
+
full = CovLasso(X, y)
|
|
567
|
+
self.alphas_ = full.lambda_grid(num=num, eps=float(self.eps),
|
|
568
|
+
l1_ratio=l1_ratio)
|
|
569
|
+
# contiguous folds in row order, sizes as even as they come --
|
|
570
|
+
# sklearn's KFold(shuffle=False)
|
|
571
|
+
bounds = np.linspace(0, n, cv + 1).astype(int)
|
|
572
|
+
self.mse_path_ = np.empty((num, cv))
|
|
573
|
+
G = full.g.reshape(full.p, full.p)
|
|
574
|
+
if cuda_available() and not self.force_cpu:
|
|
575
|
+
done = self._fit_gpu(X, y, full, G, bounds, n)
|
|
576
|
+
if done is not None:
|
|
577
|
+
return done
|
|
578
|
+
|
|
579
|
+
def one_fold(k):
|
|
580
|
+
lo, hi = bounds[k], bounds[k + 1]
|
|
581
|
+
Xf, yf = X[lo:hi], y[lo:hi]
|
|
582
|
+
# The Gram is additive over rows, so a fold's training Gram
|
|
583
|
+
# is a subtraction: G_full - Xf'Xf costs O(n_fold p^2) where
|
|
584
|
+
# rebuilding from the complement costs O((n - n_fold) p^2).
|
|
585
|
+
# Over five folds that is the whole Gram work twice instead
|
|
586
|
+
# of four times, and the Gram is the dominant O(n p^2) step.
|
|
587
|
+
m = CovLasso(gram=(G - Xf.T @ Xf).ravel(),
|
|
588
|
+
corr=full.c0 - Xf.T @ yf,
|
|
589
|
+
nobs=n - (hi - lo))
|
|
590
|
+
# The warm-started path: after the Gram, each lambda resumes
|
|
591
|
+
# where the previous stopped and costs O(p) per moving
|
|
592
|
+
# coordinate. That warm path is what makes searching the
|
|
593
|
+
# alpha grid cheap -- the grid costs barely more than its
|
|
594
|
+
# hardest single alpha.
|
|
595
|
+
path = m.fit_path(self.alphas_, tol=tol, l1_ratio=l1_ratio)
|
|
596
|
+
resid = Xf @ path.T - yf[:, None]
|
|
597
|
+
self.mse_path_[:, k] = np.mean(resid * resid, axis=0)
|
|
598
|
+
|
|
599
|
+
if int(self.n_jobs) > 1:
|
|
600
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
601
|
+
with ThreadPoolExecutor(max_workers=int(self.n_jobs)) as ex:
|
|
602
|
+
list(ex.map(one_fold, range(cv)))
|
|
603
|
+
else:
|
|
604
|
+
for k in range(cv):
|
|
605
|
+
one_fold(k)
|
|
606
|
+
return self._finish(full)
|
|
607
|
+
|
|
608
|
+
def _fit_gpu(self, X, y, full, G, bounds, n):
|
|
609
|
+
# Every fold and every alpha is an independent problem, so all of
|
|
610
|
+
# them go to the device as one batch, one block per problem,
|
|
611
|
+
# the problems of a fold sharing its Gram on the device. Fold
|
|
612
|
+
# sizes may differ by a row; the kernel only ever uses lam
|
|
613
|
+
# through the product lam * nobs, so each problem's nobs is
|
|
614
|
+
# absorbed into its lambda and nobs is passed as 1 -- exact,
|
|
615
|
+
# not approximate. Returns None when the device declined, and
|
|
616
|
+
# the CPU path takes over.
|
|
617
|
+
p_ = full.p
|
|
618
|
+
num, cv = int(self.num), int(self.cv)
|
|
619
|
+
grams = np.empty((cv, p_ * p_))
|
|
620
|
+
corrs = np.empty((cv, p_))
|
|
621
|
+
nobs_k = np.empty(cv)
|
|
622
|
+
for k in range(cv):
|
|
623
|
+
lo, hi = bounds[k], bounds[k + 1]
|
|
624
|
+
Xf, yf = X[lo:hi], y[lo:hi]
|
|
625
|
+
grams[k] = (G - Xf.T @ Xf).ravel()
|
|
626
|
+
corrs[k] = full.c0 - Xf.T @ yf
|
|
627
|
+
nobs_k[k] = n - (hi - lo)
|
|
628
|
+
lams = (np.tile(self.alphas_, cv) * np.repeat(nobs_k, num))
|
|
629
|
+
betas = _grid_descend(grams, corrs, lams, p_, 0, tol=float(self.tol),
|
|
630
|
+
l1_ratio=float(self.l1_ratio))
|
|
631
|
+
if betas is None:
|
|
632
|
+
return None
|
|
633
|
+
betas = betas.reshape(cv, num, p_)
|
|
634
|
+
for k in range(cv):
|
|
635
|
+
lo, hi = bounds[k], bounds[k + 1]
|
|
636
|
+
resid = X[lo:hi] @ betas[k].T - y[lo:hi, None]
|
|
637
|
+
self.mse_path_[:, k] = np.mean(resid * resid, axis=0)
|
|
638
|
+
return self._finish(full)
|
|
639
|
+
|
|
640
|
+
def _finish(self, full):
|
|
641
|
+
mean = self.mse_path_.mean(axis=1)
|
|
642
|
+
# alphas_ descends, so argmin's first hit is the larger penalty
|
|
643
|
+
self.alpha_ = float(self.alphas_[int(np.argmin(mean))])
|
|
644
|
+
self.coef_ = full.fit(self.alpha_, tol=float(self.tol),
|
|
645
|
+
l1_ratio=float(self.l1_ratio))
|
|
646
|
+
self.intercept_ = 0.0 if self.coef_.ndim == 1 else np.zeros(self.coef_.shape[1])
|
|
647
|
+
return self
|
|
648
|
+
|
|
649
|
+
def predict(self, X):
|
|
650
|
+
return pred_x(self, X) @ self.coef_
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
class CovRidge:
|
|
654
|
+
"""Ridge regression over a design given by its Gram matrix.
|
|
655
|
+
|
|
656
|
+
The objective matches scikit-learn's ``Ridge`` with
|
|
657
|
+
``fit_intercept=False``:
|
|
658
|
+
|
|
659
|
+
||y - X b||^2 + alpha ||b||^2
|
|
660
|
+
|
|
661
|
+
(note: unlike the lasso objective, scikit-learn does not scale this
|
|
662
|
+
one by the number of rows), so the solution is
|
|
663
|
+
``b = (X'X + alpha I)^{-1} X'y``.
|
|
664
|
+
|
|
665
|
+
The design matrix enters only through X'X and X'y, so the
|
|
666
|
+
constructor takes either the design or a Gram matrix somebody built
|
|
667
|
+
more cheaply. One symmetric eigendecomposition is done on first
|
|
668
|
+
use, O(p^3); every alpha after that costs O(p^2), which is what
|
|
669
|
+
makes a whole path cheap.
|
|
670
|
+
"""
|
|
671
|
+
|
|
672
|
+
def __init__(self, X=None, y=None, *, gram=None, corr=None):
|
|
673
|
+
if gram is not None:
|
|
674
|
+
if corr is None:
|
|
675
|
+
raise ValueError("gram needs corr alongside it")
|
|
676
|
+
g = np.ascontiguousarray(gram, dtype=np.float64).ravel()
|
|
677
|
+
c = np.ascontiguousarray(corr, dtype=np.float64)
|
|
678
|
+
p = c.size
|
|
679
|
+
if g.size != p * p:
|
|
680
|
+
raise ValueError(f"gram is {g.size} entries; expected {p*p}")
|
|
681
|
+
self.p, self.g, self.c0 = p, g, c
|
|
682
|
+
else:
|
|
683
|
+
if X is None or y is None:
|
|
684
|
+
raise ValueError("pass X and y, or gram and corr")
|
|
685
|
+
X = np.ascontiguousarray(X, dtype=np.float64)
|
|
686
|
+
y = np.ascontiguousarray(y, dtype=np.float64)
|
|
687
|
+
if X.ndim != 2 or y.ndim != 1 or X.shape[0] != y.size:
|
|
688
|
+
raise ValueError(
|
|
689
|
+
f"X is {X.shape} and y is {y.shape}; expected (n, p) "
|
|
690
|
+
"and (n,)")
|
|
691
|
+
self.p = X.shape[1]
|
|
692
|
+
self.g = np.ascontiguousarray(_gram(X).ravel())
|
|
693
|
+
self.c0 = np.ascontiguousarray(_gram_xy(X, y))
|
|
694
|
+
self._eig = None
|
|
695
|
+
|
|
696
|
+
def _eigen(self):
|
|
697
|
+
if self._eig is None:
|
|
698
|
+
w, Q = np.linalg.eigh(self.g.reshape(self.p, self.p))
|
|
699
|
+
self._eig = (w, Q, Q.T @ self.c0)
|
|
700
|
+
return self._eig
|
|
701
|
+
|
|
702
|
+
def alpha_grid(self, num=100, eps=1e-6):
|
|
703
|
+
"""A log-spaced path from the top eigenvalue of X'X downward.
|
|
704
|
+
|
|
705
|
+
At ``alpha`` around the largest eigenvalue the fit is heavily
|
|
706
|
+
shrunk; by ``eps`` times it the fit is essentially least
|
|
707
|
+
squares. Descending, like the lasso grids.
|
|
708
|
+
"""
|
|
709
|
+
w, _, _ = self._eigen()
|
|
710
|
+
hi = float(w.max())
|
|
711
|
+
return hi * np.logspace(0, np.log10(eps), int(num))
|
|
712
|
+
|
|
713
|
+
def fit(self, alpha):
|
|
714
|
+
"""Coefficients at one alpha: one solve of X'X + alpha I while no
|
|
715
|
+
path has needed the eigendecomposition (at p = 200 a few
|
|
716
|
+
milliseconds against eigh's tenth of a second), through it after."""
|
|
717
|
+
if self._eig is None and float(alpha) > 0.0:
|
|
718
|
+
b = _ridge_solve(self.g.reshape(self.p, self.p), self.c0, float(alpha))
|
|
719
|
+
if b is not None:
|
|
720
|
+
return b
|
|
721
|
+
w, Q, qc = self._eigen()
|
|
722
|
+
return Q @ (qc / (w + float(alpha)))
|
|
723
|
+
|
|
724
|
+
def fit_path(self, alphas):
|
|
725
|
+
"""Coefficients per alpha, shape (len(alphas), p).
|
|
726
|
+
|
|
727
|
+
After the one eigendecomposition this is a single dense
|
|
728
|
+
product, so thousands of alphas cost what one does.
|
|
729
|
+
"""
|
|
730
|
+
w, Q, qc = self._eigen()
|
|
731
|
+
alphas = np.asarray(alphas, dtype=np.float64)
|
|
732
|
+
return (Q @ (qc[:, None] / (w[:, None] + alphas[None, :]))).T
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
def _centered(X, y, fit_intercept):
|
|
736
|
+
# the data the intercept-free solvers see: centred when an intercept
|
|
737
|
+
# is fitted (scikit-learn's _preprocess_data), as they are otherwise
|
|
738
|
+
if not fit_intercept:
|
|
739
|
+
return X, y, np.zeros(X.shape[1]), np.zeros(y.shape[1:]) if y.ndim > 1 else 0.0
|
|
740
|
+
xm, ym = X.mean(axis=0), y.mean(axis=0)
|
|
741
|
+
return np.ascontiguousarray(X - xm), y - ym, xm, ym
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
def _centered_gram(X, y, fit_intercept):
|
|
745
|
+
"""(Xc'Xc, Xc'yc, xm, ym) for the centred data Xc, yc, without
|
|
746
|
+
forming Xc: X'X - n xm xm' and X'y - n xm ym, which saves the copy
|
|
747
|
+
of X (as long as the Gram product itself on 20000 x 200 rows, where
|
|
748
|
+
it took the intercept from free to twice the fit). The correction
|
|
749
|
+
cancels digits when a column's mean is large against its spread,
|
|
750
|
+
about (mean / sd)^2 ulps, so a column with mean over 100 sd (or no
|
|
751
|
+
spread at all) sends the whole fit through the centred copy."""
|
|
752
|
+
n = X.shape[0]
|
|
753
|
+
G, c = _gram(X), _gram_xy(X, y)
|
|
754
|
+
if not fit_intercept:
|
|
755
|
+
return G, c, np.zeros(X.shape[1]), np.zeros(y.shape[1:]) if y.ndim > 1 else 0.0
|
|
756
|
+
xm, ym = X.mean(axis=0), y.mean(axis=0)
|
|
757
|
+
nm2 = n * xm * xm
|
|
758
|
+
if np.all(np.diagonal(G) - nm2 > 1e-4 * nm2):
|
|
759
|
+
G -= np.outer(xm, n * xm)
|
|
760
|
+
c -= np.multiply.outer(n * xm, ym)
|
|
761
|
+
return G, c, xm, ym
|
|
762
|
+
Xc, yc = np.ascontiguousarray(X - xm), y - ym
|
|
763
|
+
return Xc.T @ Xc, Xc.T @ yc, xm, ym
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
class GramElasticNet(Tagged, RegressorMixin, BaseEstimator):
|
|
767
|
+
"""scikit-learn's ``ElasticNet(precompute=True)`` interface over
|
|
768
|
+
CovLasso: fit(X, y) centres the data when ``fit_intercept``, forms
|
|
769
|
+
the Gram matrix once and runs the covariance sweep at ``alpha``,
|
|
770
|
+
|
|
771
|
+
(1 / 2 n) ||y - X b - b0||^2 + alpha l1_ratio ||b||_1
|
|
772
|
+
+ (alpha / 2)(1 - l1_ratio) ||b||^2.
|
|
773
|
+
|
|
774
|
+
``tol`` is CovLasso's (the largest coefficient move in a sweep,
|
|
775
|
+
relative to the coefficients' size) and ``max_iter`` its sweep cap.
|
|
776
|
+
For a whole path or a grid, CovLasso itself (fit_path,
|
|
777
|
+
fit_path_batch) is the faster interface.
|
|
778
|
+
|
|
779
|
+
Attributes after fit: coef_, intercept_, n_features_in_.
|
|
780
|
+
"""
|
|
781
|
+
|
|
782
|
+
def __init__(self, alpha=1.0, *, l1_ratio=0.5, fit_intercept=True, tol=1e-8,
|
|
783
|
+
max_iter=100000):
|
|
784
|
+
self.alpha, self.l1_ratio, self.fit_intercept = alpha, l1_ratio, fit_intercept
|
|
785
|
+
self.tol, self.max_iter = tol, max_iter
|
|
786
|
+
|
|
787
|
+
def fit(self, X, y):
|
|
788
|
+
X, y = fit_xy(self, X, y)
|
|
789
|
+
n, p = X.shape
|
|
790
|
+
G, c, xm, ym = _centered_gram(X, y, self.fit_intercept)
|
|
791
|
+
g = np.ascontiguousarray(G.ravel())
|
|
792
|
+
c = np.ascontiguousarray(c)
|
|
793
|
+
lam, r = float(self.alpha), float(self.l1_ratio)
|
|
794
|
+
tol, cap = float(self.tol), int(self.max_iter)
|
|
795
|
+
beta, prev = np.zeros(p), np.empty(p)
|
|
796
|
+
swept = 0
|
|
797
|
+
# CovLasso.fit_path's loop at one lambda, counting the sweeps
|
|
798
|
+
while swept < cap:
|
|
799
|
+
chunk = min(20, cap - swept)
|
|
800
|
+
prev[:] = beta
|
|
801
|
+
kernel.enet_descend(g, c, beta, lam * r, lam * (1.0 - r), chunk, float(n), p)
|
|
802
|
+
swept += chunk
|
|
803
|
+
if np.max(np.abs(beta - prev)) < tol * _scale(beta):
|
|
804
|
+
break
|
|
805
|
+
self.coef_, self.n_iter_ = beta, swept
|
|
806
|
+
self.intercept_ = float(ym - xm @ self.coef_)
|
|
807
|
+
return self
|
|
808
|
+
|
|
809
|
+
def predict(self, X):
|
|
810
|
+
return pred_x(self, X) @ self.coef_ + self.intercept_
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
class GramLasso(GramElasticNet):
|
|
814
|
+
"""scikit-learn's ``Lasso(precompute=True)`` interface over CovLasso:
|
|
815
|
+
GramElasticNet with l1_ratio 1.
|
|
816
|
+
|
|
817
|
+
Attributes after fit: coef_, intercept_, n_features_in_.
|
|
818
|
+
"""
|
|
819
|
+
|
|
820
|
+
def __init__(self, alpha=1.0, *, fit_intercept=True, tol=1e-8, max_iter=100000):
|
|
821
|
+
self.alpha, self.fit_intercept, self.tol, self.max_iter = alpha, fit_intercept, tol, max_iter
|
|
822
|
+
|
|
823
|
+
@property
|
|
824
|
+
def l1_ratio(self):
|
|
825
|
+
return 1.0
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
class GramRidge(Tagged, RegressorMixin, BaseEstimator):
|
|
829
|
+
"""scikit-learn's ``Ridge`` interface over the Gram matrix:
|
|
830
|
+
(X'X + alpha I) b = X'y on the centred data by one Cholesky solve
|
|
831
|
+
(least squares when alpha is 0 and X'X singular), y one target or
|
|
832
|
+
several (coef_ then (n_targets, p), as Ridge has it). For many
|
|
833
|
+
alphas, CovRidge's fit_path is the faster interface.
|
|
834
|
+
|
|
835
|
+
Attributes after fit: coef_, intercept_, n_features_in_.
|
|
836
|
+
"""
|
|
837
|
+
_multi_output = True
|
|
838
|
+
|
|
839
|
+
def __init__(self, alpha=1.0, *, fit_intercept=True):
|
|
840
|
+
self.alpha, self.fit_intercept = alpha, fit_intercept
|
|
841
|
+
|
|
842
|
+
def fit(self, X, y):
|
|
843
|
+
X, y = fit_xy(self, X, y, multi_output=True)
|
|
844
|
+
p = X.shape[1]
|
|
845
|
+
A, b, xm, ym = _centered_gram(X, y, self.fit_intercept)
|
|
846
|
+
coef = _ridge_solve(A, b, float(self.alpha))
|
|
847
|
+
if coef is None:
|
|
848
|
+
A.flat[::p + 1] += float(self.alpha)
|
|
849
|
+
try:
|
|
850
|
+
coef = np.linalg.solve(A, b)
|
|
851
|
+
except np.linalg.LinAlgError:
|
|
852
|
+
coef = np.linalg.lstsq(A, b, rcond=None)[0]
|
|
853
|
+
self.coef_ = coef.T if coef.ndim > 1 else coef
|
|
854
|
+
self.intercept_ = ym - xm @ coef
|
|
855
|
+
if np.ndim(self.intercept_) == 0:
|
|
856
|
+
self.intercept_ = float(self.intercept_)
|
|
857
|
+
return self
|
|
858
|
+
|
|
859
|
+
def predict(self, X):
|
|
860
|
+
return pred_x(self, X) @ self.coef_.T + self.intercept_
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
class GramRidgeCV(Tagged, RegressorMixin, BaseEstimator):
|
|
864
|
+
"""scikit-learn's ``RidgeCV`` with ``cv=None``: the alpha whose
|
|
865
|
+
leave-one-out mean squared error is least, every alpha from one
|
|
866
|
+
eigenproblem (``examples/kernel-only/ridge-gcv.scm``). With the hat
|
|
867
|
+
matrix H = Z diag(1 / (lam + alpha)) Z' (plus 1/n with an
|
|
868
|
+
intercept), the leave-one-out residual is (y - yhat) / (1 - diag H),
|
|
869
|
+
and diag H and yhat at every alpha are two products. Z comes from
|
|
870
|
+
the eigenvectors of Xc'Xc (``gcv_mode="svd"``, the default when
|
|
871
|
+
n > p, X'X by dsyrk) or of Xc Xc' (``"eigen"``, the default
|
|
872
|
+
otherwise); the eigenproblem is LAPACK's dsyevd where the BLAS
|
|
873
|
+
build is here. Only ``cv=None`` and ``scoring=None`` are taken.
|
|
874
|
+
|
|
875
|
+
Attributes after fit: alpha_, best_score_ (minus the mean squared
|
|
876
|
+
leave-one-out error), coef_, intercept_, cv_results_ (with
|
|
877
|
+
``store_cv_results``: the squared errors, (n, n_alphas) or
|
|
878
|
+
(n, n_targets, n_alphas)), n_features_in_.
|
|
879
|
+
"""
|
|
880
|
+
_multi_output = True
|
|
881
|
+
|
|
882
|
+
def __init__(self, alphas=(0.1, 1.0, 10.0), *, fit_intercept=True, scoring=None,
|
|
883
|
+
cv=None, gcv_mode=None, store_cv_results=False, alpha_per_target=False):
|
|
884
|
+
self.alphas, self.fit_intercept, self.scoring, self.cv = alphas, fit_intercept, scoring, cv
|
|
885
|
+
self.gcv_mode, self.store_cv_results = gcv_mode, store_cv_results
|
|
886
|
+
self.alpha_per_target = alpha_per_target
|
|
887
|
+
|
|
888
|
+
def fit(self, X, y):
|
|
889
|
+
if self.cv is not None or self.scoring is not None:
|
|
890
|
+
raise ValueError("GramRidgeCV takes cv=None and scoring=None only "
|
|
891
|
+
"(leave-one-out by GCV, mean squared error)")
|
|
892
|
+
if self.gcv_mode not in (None, "auto", "svd", "eigen"):
|
|
893
|
+
raise ValueError(f"gcv_mode={self.gcv_mode!r}: expected None, 'auto', 'svd' or 'eigen'")
|
|
894
|
+
alphas = np.ascontiguousarray(np.atleast_1d(np.asarray(self.alphas, dtype=np.float64)))
|
|
895
|
+
if alphas.ndim != 1 or alphas.size == 0 or not np.all(alphas > 0.0):
|
|
896
|
+
raise ValueError(f"alphas={self.alphas!r}: expected positive values")
|
|
897
|
+
X, y = fit_xy(self, X, y, multi_output=True)
|
|
898
|
+
n, p = X.shape
|
|
899
|
+
Y = y.reshape(n, -1)
|
|
900
|
+
t = Y.shape[1]
|
|
901
|
+
k = _gramops_kernels()
|
|
902
|
+
if self.fit_intercept:
|
|
903
|
+
xm, ym = X.mean(axis=0), Y.mean(axis=0)
|
|
904
|
+
else:
|
|
905
|
+
xm, ym = np.zeros(p), np.zeros(t)
|
|
906
|
+
yc = np.ascontiguousarray(Y - ym)
|
|
907
|
+
mode = self.gcv_mode if self.gcv_mode in ("svd", "eigen") else ("svd" if n > p else "eigen")
|
|
908
|
+
if mode == "svd":
|
|
909
|
+
G = _gram(X)
|
|
910
|
+
Xs, xs = X, xm
|
|
911
|
+
nm2 = n * xm * xm
|
|
912
|
+
if self.fit_intercept and not np.all(np.diagonal(G) - nm2 > 1e-4 * nm2):
|
|
913
|
+
# a column whose mean dwarfs its spread: the correction
|
|
914
|
+
# X'X - n xm xm' would cancel its digits (_centered_gram)
|
|
915
|
+
Xs, xs = np.ascontiguousarray(X - xm), np.zeros(p)
|
|
916
|
+
G = _gram(Xs)
|
|
917
|
+
else:
|
|
918
|
+
G -= np.outer(xm, n * xm)
|
|
919
|
+
m = p
|
|
920
|
+
V, lam, Z, R = np.zeros((p, p)), np.zeros(p), np.zeros((n, p)), np.zeros((p, t))
|
|
921
|
+
k.ridge_gcv_primal(Xs, xs, yc, np.ascontiguousarray(G), V, lam, Z, R, n, p, t)
|
|
922
|
+
else:
|
|
923
|
+
Xc = np.ascontiguousarray(X - xm)
|
|
924
|
+
K = _gram(np.ascontiguousarray(Xc.T))
|
|
925
|
+
m = n
|
|
926
|
+
V, lam, Z, R = np.zeros((n, n)), np.zeros(n), np.zeros((n, n)), np.zeros((n, t))
|
|
927
|
+
k.ridge_gcv_dual(K, V, lam, yc, Z, R, n, t)
|
|
928
|
+
na = alphas.size
|
|
929
|
+
mse = np.zeros((na, t))
|
|
930
|
+
loo = np.zeros((n, t, na)) if self.store_cv_results else np.zeros(1)
|
|
931
|
+
k.ridge_gcv_scan(Z, lam, R, yc, alphas, 1.0 / n if self.fit_intercept else 0.0,
|
|
932
|
+
mse, loo, int(bool(self.store_cv_results)), n, m, t, na)
|
|
933
|
+
if self.alpha_per_target and t > 1:
|
|
934
|
+
best = np.argmin(mse, axis=0)
|
|
935
|
+
al = alphas[best]
|
|
936
|
+
self.alpha_ = al
|
|
937
|
+
self.best_score_ = -mse[best, np.arange(t)]
|
|
938
|
+
else:
|
|
939
|
+
best = int(np.argmin(mse.mean(axis=1)))
|
|
940
|
+
al = np.full(t, alphas[best])
|
|
941
|
+
self.alpha_ = float(alphas[best])
|
|
942
|
+
self.best_score_ = float(-mse[best].mean())
|
|
943
|
+
if mode == "svd":
|
|
944
|
+
coef = V @ (R / (lam[:, None] + al[None, :]))
|
|
945
|
+
else:
|
|
946
|
+
# (Xc Xc' + alpha I)^-1 yc by the eigenvectors, then Xc' of it
|
|
947
|
+
qy = V.T @ yc
|
|
948
|
+
coef = Xc.T @ (V @ (qy / (lam[:, None] + al[None, :])))
|
|
949
|
+
if y.ndim == 1:
|
|
950
|
+
self.coef_ = coef[:, 0]
|
|
951
|
+
self.intercept_ = float(ym[0] - xm @ coef[:, 0])
|
|
952
|
+
if self.alpha_per_target:
|
|
953
|
+
self.alpha_ = float(np.ravel(self.alpha_)[0])
|
|
954
|
+
self.best_score_ = float(np.ravel(self.best_score_)[0])
|
|
955
|
+
else:
|
|
956
|
+
self.coef_ = coef.T
|
|
957
|
+
self.intercept_ = ym - xm @ coef
|
|
958
|
+
if self.store_cv_results:
|
|
959
|
+
self.cv_results_ = loo[:, 0, :] if y.ndim == 1 else loo
|
|
960
|
+
return self
|
|
961
|
+
|
|
962
|
+
def predict(self, X):
|
|
963
|
+
return pred_x(self, X) @ self.coef_.T + self.intercept_
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
class CovBayesianRidge(Tagged, RegressorMixin, BaseEstimator):
|
|
967
|
+
"""Bayesian ridge regression, scikit-learn's ``BayesianRidge``,
|
|
968
|
+
with the linear algebra in the eigenbasis of the Gram matrix.
|
|
969
|
+
|
|
970
|
+
The model, the defaults (``max_iter=300``, ``tol=1e-3``, the four
|
|
971
|
+
gamma hyperpriors at 1e-6, ``alpha`` starting at ``1/var(y)``,
|
|
972
|
+
``lambda`` at 1) and the iteration are scikit-learn's: MacKay's
|
|
973
|
+
updates of the noise precision ``alpha`` and the weight precision
|
|
974
|
+
``lambda``, the posterior mean recomputed after each, a stop when
|
|
975
|
+
the mean moves by less than ``tol`` in L1. What differs is where
|
|
976
|
+
the work goes. scikit-learn takes the SVD of X once and then, per
|
|
977
|
+
iteration, multiplies by X again for the squared error, O(n p).
|
|
978
|
+
Here, for n > p, X enters once, through X'X and X'y: one symmetric
|
|
979
|
+
eigendecomposition, after which the posterior mean, its squared
|
|
980
|
+
norm and the squared error are sums over the p eigenvalues, O(p) per
|
|
981
|
+
iteration. For n <= p the same is done with the n x n kernel
|
|
982
|
+
matrix X X'. With ``fit_intercept`` the data are centred, as in
|
|
983
|
+
scikit-learn, and the Gram is centred in place.
|
|
984
|
+
|
|
985
|
+
Attributes after fit: coef_, intercept_, alpha_, lambda_, sigma_
|
|
986
|
+
(the posterior covariance of the weights, p x p), n_iter_.
|
|
987
|
+
"""
|
|
988
|
+
|
|
989
|
+
def __init__(self, *, max_iter=300, tol=1e-3, alpha_1=1e-6,
|
|
990
|
+
alpha_2=1e-6, lambda_1=1e-6, lambda_2=1e-6,
|
|
991
|
+
alpha_init=None, lambda_init=None, fit_intercept=True):
|
|
992
|
+
self.max_iter, self.tol = max_iter, tol
|
|
993
|
+
self.alpha_1, self.alpha_2 = alpha_1, alpha_2
|
|
994
|
+
self.lambda_1, self.lambda_2 = lambda_1, lambda_2
|
|
995
|
+
self.alpha_init, self.lambda_init = alpha_init, lambda_init
|
|
996
|
+
self.fit_intercept = fit_intercept
|
|
997
|
+
|
|
998
|
+
def fit(self, X, y):
|
|
999
|
+
X, y = fit_xy(self, X, y)
|
|
1000
|
+
n, p = X.shape
|
|
1001
|
+
max_iter, tol = int(self.max_iter), float(self.tol)
|
|
1002
|
+
a1, a2 = float(self.alpha_1), float(self.alpha_2)
|
|
1003
|
+
l1, l2 = float(self.lambda_1), float(self.lambda_2)
|
|
1004
|
+
y_var = float(y.var())
|
|
1005
|
+
if self.fit_intercept:
|
|
1006
|
+
xm, ym = X.mean(axis=0), float(y.mean())
|
|
1007
|
+
else:
|
|
1008
|
+
xm, ym = np.zeros(p), 0.0
|
|
1009
|
+
tall = n > p
|
|
1010
|
+
if tall:
|
|
1011
|
+
# X'X and X'y of the centred data, without centring X
|
|
1012
|
+
G = X.T @ X
|
|
1013
|
+
c = X.T @ y
|
|
1014
|
+
if self.fit_intercept:
|
|
1015
|
+
G -= n * np.outer(xm, xm)
|
|
1016
|
+
c -= n * ym * xm
|
|
1017
|
+
e, V = np.linalg.eigh(G)
|
|
1018
|
+
z = V.T @ c
|
|
1019
|
+
yy = float(y @ y) - n * ym * ym
|
|
1020
|
+
else:
|
|
1021
|
+
Xc = X - xm if self.fit_intercept else X
|
|
1022
|
+
yc = y - ym
|
|
1023
|
+
e, U = np.linalg.eigh(Xc @ Xc.T)
|
|
1024
|
+
u = U.T @ yc
|
|
1025
|
+
e = np.maximum(e, 0.0)
|
|
1026
|
+
|
|
1027
|
+
def mean_stats(alpha, lam):
|
|
1028
|
+
# the posterior mean at (alpha, lam), its squared norm, and
|
|
1029
|
+
# the squared error, without touching X when n > p
|
|
1030
|
+
r = lam / alpha
|
|
1031
|
+
if tall:
|
|
1032
|
+
w = z / (e + r)
|
|
1033
|
+
coef = V @ w
|
|
1034
|
+
sse = yy - 2.0 * float(w @ z) + float((e * w) @ w)
|
|
1035
|
+
return coef, float(w @ w), max(sse, 0.0)
|
|
1036
|
+
d = u / (e + r)
|
|
1037
|
+
coef = Xc.T @ (U @ d)
|
|
1038
|
+
resid = yc - Xc @ coef
|
|
1039
|
+
return coef, float((e * d) @ d), float(resid @ resid)
|
|
1040
|
+
|
|
1041
|
+
eps = np.finfo(np.float64).eps
|
|
1042
|
+
alpha = 1.0 / (y_var + eps) if self.alpha_init is None else float(self.alpha_init)
|
|
1043
|
+
lam = 1.0 if self.lambda_init is None else float(self.lambda_init)
|
|
1044
|
+
old = None
|
|
1045
|
+
for it in range(max_iter):
|
|
1046
|
+
coef, cc, sse = mean_stats(alpha, lam)
|
|
1047
|
+
gamma = float(np.sum(alpha * e / (lam + alpha * e)))
|
|
1048
|
+
lam = (gamma + 2 * l1) / (cc + 2 * l2)
|
|
1049
|
+
alpha = (n - gamma + 2 * a1) / (sse + 2 * a2)
|
|
1050
|
+
if it != 0 and float(np.sum(np.abs(old - coef))) < tol:
|
|
1051
|
+
break
|
|
1052
|
+
old = coef
|
|
1053
|
+
self.n_iter_ = it + 1
|
|
1054
|
+
self.alpha_, self.lambda_ = alpha, lam
|
|
1055
|
+
self.coef_, _, _ = mean_stats(alpha, lam)
|
|
1056
|
+
self.intercept_ = ym - float(xm @ self.coef_) if self.fit_intercept else 0.0
|
|
1057
|
+
self.X_offset_ = xm
|
|
1058
|
+
# the posterior covariance (lambda I + alpha X'X)^-1
|
|
1059
|
+
if tall:
|
|
1060
|
+
self.sigma_ = (V / (alpha * e + lam)) @ V.T
|
|
1061
|
+
else:
|
|
1062
|
+
# Woodbury through the kernel side: (1/lam)(I - X'U D U'X),
|
|
1063
|
+
# D = alpha / (lam + alpha e)
|
|
1064
|
+
B = Xc.T @ U
|
|
1065
|
+
self.sigma_ = (np.eye(p) - (B * (alpha / (lam + alpha * e))) @ B.T) / lam
|
|
1066
|
+
return self
|
|
1067
|
+
|
|
1068
|
+
def predict(self, X, return_std=False):
|
|
1069
|
+
X = pred_x(self, X)
|
|
1070
|
+
mean = X @ self.coef_ + self.intercept_
|
|
1071
|
+
if not return_std:
|
|
1072
|
+
return mean
|
|
1073
|
+
Xc = X - self.X_offset_
|
|
1074
|
+
var = np.einsum("ij,jk,ik->i", Xc, self.sigma_, Xc) + 1.0 / self.alpha_
|
|
1075
|
+
return mean, np.sqrt(var)
|
|
1076
|
+
|
|
1077
|
+
def _classifier_kernels():
|
|
1078
|
+
# imported on first use: the classifiers are their own library
|
|
1079
|
+
from ._generated import _loader_classifiers
|
|
1080
|
+
return _loader_classifiers
|
|
1081
|
+
|
|
1082
|
+
|
|
1083
|
+
def _binary_labels(y):
|
|
1084
|
+
"""Two classes as (classes_, y in {-1, +1}), classes_[1] the +1."""
|
|
1085
|
+
y = np.asarray(y)
|
|
1086
|
+
classes = np.unique(y)
|
|
1087
|
+
if classes.size == 1:
|
|
1088
|
+
raise ValueError("expected two classes, got 1 class")
|
|
1089
|
+
if classes.size != 2:
|
|
1090
|
+
raise ValueError("Only binary classification is supported. The type of the target "
|
|
1091
|
+
f"is multiclass ({classes.size} classes).")
|
|
1092
|
+
return classes, np.where(y == classes[1], 1.0, -1.0)
|
|
1093
|
+
|
|
1094
|
+
|
|
1095
|
+
def _seed(random_state):
|
|
1096
|
+
# the solvers' Park-Miller generator wants a seed in [1, 2^31 - 2]
|
|
1097
|
+
return int(random_state) % 2147483646 + 1
|
|
1098
|
+
|
|
1099
|
+
|
|
1100
|
+
class LinearSVM(Tagged, ClassifierMixin, BaseEstimator):
|
|
1101
|
+
"""The L1-loss (hinge) linear SVM by liblinear's own algorithm.
|
|
1102
|
+
|
|
1103
|
+
``examples/kernel-only/svm-liblinear.scm`` translated plainly:
|
|
1104
|
+
liblinear's solve_l2r_l1l2_svc, dual coordinate descent with a
|
|
1105
|
+
random order in every sweep, shrinking, and the projected-gradient
|
|
1106
|
+
stop -- what scikit-learn's ``LinearSVC(loss="hinge", dual=True,
|
|
1107
|
+
fit_intercept=False)`` runs, on the same problem
|
|
1108
|
+
|
|
1109
|
+
min_w 1/2 ||w||^2 + C sum_i max(0, 1 - y_i w.x_i).
|
|
1110
|
+
|
|
1111
|
+
No intercept (as ``fit_intercept=False``); two classes, the second
|
|
1112
|
+
of ``classes_`` being the positive one. ``tol`` and ``max_iter``
|
|
1113
|
+
are LinearSVC's (the projected-gradient span and the sweep cap).
|
|
1114
|
+
|
|
1115
|
+
Attributes after fit: classes_, coef_, dual_coef_ (the alphas),
|
|
1116
|
+
n_iter_.
|
|
1117
|
+
"""
|
|
1118
|
+
|
|
1119
|
+
_binary_only = True
|
|
1120
|
+
|
|
1121
|
+
def __init__(self, C=1.0, tol=1e-4, max_iter=1000, random_state=0):
|
|
1122
|
+
self.C, self.tol, self.max_iter = C, tol, max_iter
|
|
1123
|
+
self.random_state = random_state
|
|
1124
|
+
|
|
1125
|
+
def fit(self, X, y):
|
|
1126
|
+
X, y = fit_xy(self, X, y, classifier=True)
|
|
1127
|
+
n, p = X.shape
|
|
1128
|
+
self.classes_, ys = _binary_labels(y)
|
|
1129
|
+
w = np.zeros(p)
|
|
1130
|
+
alpha = np.zeros(n)
|
|
1131
|
+
xnorm = np.zeros(n)
|
|
1132
|
+
self.n_iter_ = _classifier_kernels().svm_liblinear(
|
|
1133
|
+
X, ys, w, alpha, xnorm, float(self.C), int(self.max_iter), float(self.tol),
|
|
1134
|
+
_seed(self.random_state), n, p)
|
|
1135
|
+
self.coef_, self.dual_coef_ = w, alpha
|
|
1136
|
+
return self
|
|
1137
|
+
|
|
1138
|
+
def decision_function(self, X):
|
|
1139
|
+
return pred_x(self, X) @ self.coef_
|
|
1140
|
+
|
|
1141
|
+
def predict(self, X):
|
|
1142
|
+
z = self.decision_function(X)
|
|
1143
|
+
return self.classes_[(z > 0).astype(int)]
|
|
1144
|
+
|
|
1145
|
+
|
|
1146
|
+
class _KernelMatrix:
|
|
1147
|
+
# the kernel matrix of KernelSVM and KernelSVR, as SVC and SVR take
|
|
1148
|
+
# it: rbf (gamma "scale", "auto" or a number), linear, or precomputed
|
|
1149
|
+
def _k(self, A, B):
|
|
1150
|
+
if self.kernel == "linear":
|
|
1151
|
+
return A @ B.T
|
|
1152
|
+
if self.kernel == "rbf":
|
|
1153
|
+
if A.shape[0] * B.shape[0] < 200_000:
|
|
1154
|
+
# numpy's vectorised exp, while the matrix sits in cache:
|
|
1155
|
+
# 0.14 ms to the loop's 0.31 at 200 x 200
|
|
1156
|
+
sa = (A * A).sum(1)[:, None]
|
|
1157
|
+
sb = (B * B).sum(1)[None, :]
|
|
1158
|
+
return np.exp(-self._gamma * np.maximum(sa + sb - 2.0 * (A @ B.T), 0.0))
|
|
1159
|
+
# the product by BLAS, the rest in place by svm-smo.scm's
|
|
1160
|
+
# rbf-from-products (on every thread of an OpenMP build;
|
|
1161
|
+
# 76 ms to numpy's 96 at 3,000 on one core, 50 to 105 on 20)
|
|
1162
|
+
G = np.ascontiguousarray(A @ B.T)
|
|
1163
|
+
_classifier_kernels().rbf_from_products(
|
|
1164
|
+
G, (A * A).sum(1), (B * B).sum(1), float(self._gamma), A.shape[0], B.shape[0])
|
|
1165
|
+
return G
|
|
1166
|
+
raise ValueError(f"kernel {self.kernel!r}: expected rbf, linear or precomputed")
|
|
1167
|
+
|
|
1168
|
+
def _train_kernel(self, X):
|
|
1169
|
+
if self.kernel == "precomputed":
|
|
1170
|
+
return X
|
|
1171
|
+
p = X.shape[1]
|
|
1172
|
+
if self.gamma == "scale":
|
|
1173
|
+
v = float(X.var())
|
|
1174
|
+
self._gamma = 1.0 / (p * v) if v > 0 else 1.0
|
|
1175
|
+
elif self.gamma == "auto":
|
|
1176
|
+
self._gamma = 1.0 / p
|
|
1177
|
+
else:
|
|
1178
|
+
self._gamma = float(self.gamma)
|
|
1179
|
+
return self._k(X, X)
|
|
1180
|
+
|
|
1181
|
+
def _support_kernel(self, X):
|
|
1182
|
+
X = pred_x(self, X)
|
|
1183
|
+
if self.kernel == "precomputed":
|
|
1184
|
+
return X[:, self.support_]
|
|
1185
|
+
return self._k(X, self.support_vectors_)
|
|
1186
|
+
|
|
1187
|
+
|
|
1188
|
+
class KernelSVM(Tagged, ClassifierMixin, _KernelMatrix, BaseEstimator):
|
|
1189
|
+
"""C-SVC by libsvm's own algorithm, over the whole kernel matrix.
|
|
1190
|
+
|
|
1191
|
+
``examples/kernel-only/svm-smo.scm`` translated plainly: libsvm's
|
|
1192
|
+
Solver::Solve -- SMO with the second-order working-set selection
|
|
1193
|
+
(WSS3), shrinking with gradient reconstruction, and the bias -- the
|
|
1194
|
+
solver scikit-learn's ``SVC`` runs, on the same problem (with the
|
|
1195
|
+
intercept). The kernel matrix is built once in numpy and held
|
|
1196
|
+
whole, n x n doubles (800 MB at n = 10,000): libsvm with every row
|
|
1197
|
+
in its cache. ``kernel`` is "rbf" (``gamma`` "scale", "auto" or a
|
|
1198
|
+
number, as in SVC), "linear", or "precomputed" (X is then the
|
|
1199
|
+
kernel matrix, and decision_function takes the kernel between new
|
|
1200
|
+
points and the training points).
|
|
1201
|
+
|
|
1202
|
+
Attributes after fit: classes_, support_, dual_coef_ (alpha_i y_i
|
|
1203
|
+
over the support), intercept_, n_iter_; support_vectors_ unless
|
|
1204
|
+
precomputed.
|
|
1205
|
+
"""
|
|
1206
|
+
|
|
1207
|
+
_binary_only = True
|
|
1208
|
+
_pairwise = True
|
|
1209
|
+
|
|
1210
|
+
def __init__(self, C=1.0, kernel="rbf", gamma="scale", tol=1e-3, max_iter=-1):
|
|
1211
|
+
self.C, self.kernel, self.gamma = C, kernel, gamma
|
|
1212
|
+
self.tol, self.max_iter = tol, max_iter
|
|
1213
|
+
|
|
1214
|
+
def fit(self, X, y):
|
|
1215
|
+
X, y = fit_xy(self, X, y, classifier=True)
|
|
1216
|
+
self.classes_, ys = _binary_labels(y)
|
|
1217
|
+
n = ys.size
|
|
1218
|
+
K = self._train_kernel(X)
|
|
1219
|
+
alpha = np.zeros(n)
|
|
1220
|
+
rho = np.zeros(1)
|
|
1221
|
+
iters = int(self.max_iter) if self.max_iter > 0 else 10_000_000
|
|
1222
|
+
self.n_iter_ = _classifier_kernels().svm_smo(
|
|
1223
|
+
np.ascontiguousarray(K), ys, alpha, rho, float(self.C), iters, float(self.tol), n)
|
|
1224
|
+
self.support_ = np.flatnonzero(alpha > 0)
|
|
1225
|
+
self.dual_coef_ = (alpha * ys)[self.support_]
|
|
1226
|
+
self.intercept_ = -float(rho[0])
|
|
1227
|
+
if self.kernel != "precomputed":
|
|
1228
|
+
self.support_vectors_ = X[self.support_]
|
|
1229
|
+
return self
|
|
1230
|
+
|
|
1231
|
+
def decision_function(self, X):
|
|
1232
|
+
return self._support_kernel(X) @ self.dual_coef_ + self.intercept_
|
|
1233
|
+
|
|
1234
|
+
def predict(self, X):
|
|
1235
|
+
z = self.decision_function(X)
|
|
1236
|
+
return self.classes_[(z > 0).astype(int)]
|
|
1237
|
+
|
|
1238
|
+
|
|
1239
|
+
class LinearSVR(Tagged, RegressorMixin, BaseEstimator):
|
|
1240
|
+
"""The linear epsilon-SVR with the L1 loss, by liblinear's own algorithm.
|
|
1241
|
+
|
|
1242
|
+
``examples/kernel-only/svr-liblinear.scm`` translated plainly:
|
|
1243
|
+
liblinear's solve_l2r_l1l2_svr (dual coordinate descent, random
|
|
1244
|
+
order, shrinking) -- what scikit-learn's
|
|
1245
|
+
``LinearSVR(loss="epsilon_insensitive", dual=True,
|
|
1246
|
+
fit_intercept=False)`` runs, on the same problem
|
|
1247
|
+
|
|
1248
|
+
min_w 1/2 ||w||^2 + C sum_i max(0, |y_i - w.x_i| - epsilon).
|
|
1249
|
+
|
|
1250
|
+
``epsilon`` defaults to 0, ``tol`` to 1e-4 and ``max_iter`` to 1000
|
|
1251
|
+
sweeps, as in LinearSVR; no intercept.
|
|
1252
|
+
|
|
1253
|
+
Attributes after fit: coef_, dual_coef_ (the betas), n_iter_.
|
|
1254
|
+
"""
|
|
1255
|
+
|
|
1256
|
+
def __init__(self, C=1.0, epsilon=0.0, tol=1e-4, max_iter=1000, random_state=0):
|
|
1257
|
+
self.C, self.epsilon = C, epsilon
|
|
1258
|
+
self.tol, self.max_iter = tol, max_iter
|
|
1259
|
+
self.random_state = random_state
|
|
1260
|
+
|
|
1261
|
+
def fit(self, X, y):
|
|
1262
|
+
X, y = fit_xy(self, X, y)
|
|
1263
|
+
n, p = X.shape
|
|
1264
|
+
w = np.zeros(p)
|
|
1265
|
+
beta = np.zeros(n)
|
|
1266
|
+
xnorm = np.zeros(n)
|
|
1267
|
+
self.n_iter_ = _classifier_kernels().svr_liblinear(
|
|
1268
|
+
X, y, w, beta, xnorm, float(self.C), float(self.epsilon), int(self.max_iter),
|
|
1269
|
+
float(self.tol), _seed(self.random_state), n, p)
|
|
1270
|
+
self.coef_, self.dual_coef_ = w, beta
|
|
1271
|
+
self.intercept_ = 0.0
|
|
1272
|
+
return self
|
|
1273
|
+
|
|
1274
|
+
def predict(self, X):
|
|
1275
|
+
return pred_x(self, X) @ self.coef_
|
|
1276
|
+
|
|
1277
|
+
|
|
1278
|
+
class KernelSVR(Tagged, RegressorMixin, _KernelMatrix, BaseEstimator):
|
|
1279
|
+
"""epsilon-SVR by libsvm's own algorithm, over the whole kernel matrix.
|
|
1280
|
+
|
|
1281
|
+
The same translated SMO solver as KernelSVM (``svm-smo.scm``), set
|
|
1282
|
+
up as libsvm's solve_epsilon_svr sets it up: the 2n variables
|
|
1283
|
+
alpha and alpha* with linear terms epsilon -/+ y_i. scikit-learn's
|
|
1284
|
+
``SVR``, with the intercept; ``epsilon=0.1``, ``tol=1e-3`` and the
|
|
1285
|
+
kernels as there. The kernel matrix is held whole.
|
|
1286
|
+
|
|
1287
|
+
Attributes after fit: support_, dual_coef_ (alpha - alpha* over the
|
|
1288
|
+
support), intercept_, n_iter_; support_vectors_ unless precomputed.
|
|
1289
|
+
"""
|
|
1290
|
+
|
|
1291
|
+
_pairwise = True
|
|
1292
|
+
|
|
1293
|
+
def __init__(self, C=1.0, epsilon=0.1, kernel="rbf", gamma="scale", tol=1e-3, max_iter=-1):
|
|
1294
|
+
self.C, self.epsilon, self.kernel, self.gamma = C, epsilon, kernel, gamma
|
|
1295
|
+
self.tol, self.max_iter = tol, max_iter
|
|
1296
|
+
|
|
1297
|
+
def fit(self, X, y):
|
|
1298
|
+
X, y = fit_xy(self, X, y)
|
|
1299
|
+
n = y.size
|
|
1300
|
+
K = self._train_kernel(X)
|
|
1301
|
+
beta = np.zeros(n)
|
|
1302
|
+
rho = np.zeros(1)
|
|
1303
|
+
iters = int(self.max_iter) if self.max_iter > 0 else 10_000_000
|
|
1304
|
+
self.n_iter_ = _classifier_kernels().svr_smo(
|
|
1305
|
+
np.ascontiguousarray(K), y, beta, rho, float(self.C), float(self.epsilon), iters,
|
|
1306
|
+
float(self.tol), n)
|
|
1307
|
+
self.support_ = np.flatnonzero(beta != 0)
|
|
1308
|
+
self.dual_coef_ = beta[self.support_]
|
|
1309
|
+
self.intercept_ = -float(rho[0])
|
|
1310
|
+
if self.kernel != "precomputed":
|
|
1311
|
+
self.support_vectors_ = X[self.support_]
|
|
1312
|
+
return self
|
|
1313
|
+
|
|
1314
|
+
def predict(self, X):
|
|
1315
|
+
return self._support_kernel(X) @ self.dual_coef_ + self.intercept_
|
|
1316
|
+
|
|
1317
|
+
|
|
1318
|
+
def nmf_backend():
|
|
1319
|
+
"""The variant CDNMF takes by default: cublas when a device
|
|
1320
|
+
answers, else blas, else loops."""
|
|
1321
|
+
built = nmf_backends()
|
|
1322
|
+
if not built:
|
|
1323
|
+
raise ImportError("scm2cpp_learn: no variant of the derived NMF was built")
|
|
1324
|
+
if "cublas" in built and cuda_available():
|
|
1325
|
+
return "cublas"
|
|
1326
|
+
cpu = [v for v in built if v != "cublas"]
|
|
1327
|
+
return cpu[0] if cpu else built[0]
|
|
1328
|
+
|
|
1329
|
+
|
|
1330
|
+
class CDNMF(Tagged, TransformerMixin, BaseEstimator):
|
|
1331
|
+
"""Non-negative matrix factorization by coordinate descent, the
|
|
1332
|
+
translator's derivation end to end.
|
|
1333
|
+
|
|
1334
|
+
``examples/kernel-only/nmf-cd.scm`` writes scikit-learn's
|
|
1335
|
+
NMF(solver="cd") iteration the plain way, with the residual
|
|
1336
|
+
R = X - W H: each half step sweeps the coordinates of one factor and
|
|
1337
|
+
moves R along the other. ``--derive`` turns that half step into
|
|
1338
|
+
scikit-learn's own form -- the memo X H' - W H H' over the hoisted
|
|
1339
|
+
Gram H H', with R never formed -- and ``--blas`` / ``--cublas`` hand
|
|
1340
|
+
the products to (cu)BLAS. Nothing of the Gram form is written by
|
|
1341
|
+
hand. The iteration, the stopping rule (the projected-gradient
|
|
1342
|
+
violation down to ``tol`` times the first iteration's) and the
|
|
1343
|
+
result are scikit-learn's with Frobenius loss, no regularization and
|
|
1344
|
+
no shuffling.
|
|
1345
|
+
|
|
1346
|
+
``init`` is "random" (scikit-learn's: |N(0, 1)| scaled by
|
|
1347
|
+
sqrt(mean(X) / k), H drawn before W, from ``random_state``) or
|
|
1348
|
+
"custom" (W and H given to fit_transform). scikit-learn's default
|
|
1349
|
+
"nndsvda" is not offered; pass "custom" with its W and H for an
|
|
1350
|
+
exact comparison. ``backend`` is "cublas", "blas" or "loops", or
|
|
1351
|
+
None for the best built (``nmf_backend()``).
|
|
1352
|
+
|
|
1353
|
+
``transform`` solves for W with H fixed by the same coordinate
|
|
1354
|
+
descent (scikit-learn's NMF.transform), in numpy.
|
|
1355
|
+
|
|
1356
|
+
Attributes after fit: components_ (H, k x p), n_components_, n_iter_,
|
|
1357
|
+
reconstruction_err_ (||X - W H||_F).
|
|
1358
|
+
"""
|
|
1359
|
+
_positive_X = True
|
|
1360
|
+
|
|
1361
|
+
def __init__(self, n_components=None, *, init="random", tol=1e-4, max_iter=200,
|
|
1362
|
+
random_state=None, backend=None):
|
|
1363
|
+
self.n_components = n_components
|
|
1364
|
+
self.init, self.tol, self.max_iter = init, tol, max_iter
|
|
1365
|
+
self.random_state = random_state
|
|
1366
|
+
self.backend = backend
|
|
1367
|
+
|
|
1368
|
+
def _lib(self):
|
|
1369
|
+
import importlib
|
|
1370
|
+
v = self.backend or nmf_backend()
|
|
1371
|
+
return importlib.import_module(f"{__name__}._generated._loader_nmf_{v}")
|
|
1372
|
+
|
|
1373
|
+
def fit_transform(self, X, y=None, W=None, H=None):
|
|
1374
|
+
X = fit_x(self, X)
|
|
1375
|
+
if X.min() < 0:
|
|
1376
|
+
raise ValueError("Negative values in data passed to CDNMF (input X)")
|
|
1377
|
+
n, p = X.shape
|
|
1378
|
+
k = p if self.n_components is None else int(self.n_components)
|
|
1379
|
+
if k < 1:
|
|
1380
|
+
raise ValueError(f"n_components must be at least 1, got {self.n_components!r}")
|
|
1381
|
+
if self.init == "custom":
|
|
1382
|
+
if W is None or H is None:
|
|
1383
|
+
raise ValueError('init="custom" needs W and H')
|
|
1384
|
+
W = np.array(W, dtype=np.float64, copy=True)
|
|
1385
|
+
H = np.array(H, dtype=np.float64)
|
|
1386
|
+
elif self.init == "random":
|
|
1387
|
+
rng = np.random.RandomState(self.random_state)
|
|
1388
|
+
avg = np.sqrt(X.mean() / k)
|
|
1389
|
+
H = np.abs(avg * rng.standard_normal(size=(k, p)))
|
|
1390
|
+
W = np.abs(avg * rng.standard_normal(size=(n, k)))
|
|
1391
|
+
else:
|
|
1392
|
+
raise ValueError(f"init {self.init!r}: expected 'random' or 'custom'")
|
|
1393
|
+
W = np.ascontiguousarray(W)
|
|
1394
|
+
HT = np.ascontiguousarray(H.T)
|
|
1395
|
+
XT = np.ascontiguousarray(X.T)
|
|
1396
|
+
self.n_iter_ = self._lib().nmf(X, XT, W, HT, int(self.max_iter), float(self.tol),
|
|
1397
|
+
n, p, k)
|
|
1398
|
+
self.components_ = np.ascontiguousarray(HT.T)
|
|
1399
|
+
self.n_components_ = k
|
|
1400
|
+
self.reconstruction_err_ = float(np.linalg.norm(X - W @ self.components_))
|
|
1401
|
+
return W
|
|
1402
|
+
|
|
1403
|
+
def fit(self, X, y=None, W=None, H=None):
|
|
1404
|
+
self.fit_transform(X, W=W, H=H)
|
|
1405
|
+
return self
|
|
1406
|
+
|
|
1407
|
+
def transform(self, X):
|
|
1408
|
+
X = pred_x(self, X)
|
|
1409
|
+
if X.min() < 0:
|
|
1410
|
+
raise ValueError("Negative values in data passed to CDNMF (input X)")
|
|
1411
|
+
H = self.components_
|
|
1412
|
+
k = H.shape[0]
|
|
1413
|
+
HHt = H @ H.T
|
|
1414
|
+
XHt = X @ H.T
|
|
1415
|
+
avg = np.sqrt(X.mean() / k) if X.size else 0.0
|
|
1416
|
+
W = np.full((X.shape[0], k), avg)
|
|
1417
|
+
tol = float(self.tol)
|
|
1418
|
+
# the half step of the fit on W alone, H fixed; the stop on the
|
|
1419
|
+
# projected gradient, measured against the first sweep's, as there
|
|
1420
|
+
first = None
|
|
1421
|
+
for _ in range(int(self.max_iter)):
|
|
1422
|
+
viol = 0.0
|
|
1423
|
+
for j in range(k):
|
|
1424
|
+
if HHt[j, j] <= 0.0:
|
|
1425
|
+
continue
|
|
1426
|
+
grad = W @ HHt[:, j] - XHt[:, j]
|
|
1427
|
+
pg = np.where(W[:, j] == 0.0, np.minimum(grad, 0.0), grad)
|
|
1428
|
+
viol += float(np.abs(pg).sum())
|
|
1429
|
+
W[:, j] = np.maximum(W[:, j] - grad / HHt[j, j], 0.0)
|
|
1430
|
+
if first is None:
|
|
1431
|
+
first = viol
|
|
1432
|
+
if first == 0.0 or viol / first <= tol:
|
|
1433
|
+
break
|
|
1434
|
+
return W
|
|
1435
|
+
|
|
1436
|
+
|
|
1437
|
+
class CDGraphicalLasso(Tagged, BaseEstimator):
|
|
1438
|
+
"""The graphical lasso, scikit-learn's own algorithm translated.
|
|
1439
|
+
|
|
1440
|
+
``examples/kernel-only/graphical-lasso.scm`` is scikit-learn's
|
|
1441
|
+
_graphical_lasso in mode "cd" and its inner solver
|
|
1442
|
+
enet_coordinate_descent_gram (the gap-safe rule included), written
|
|
1443
|
+
out whole; ``--blas`` makes its two matrix-vector products dgemv.
|
|
1444
|
+
The same model and stopping rule as ``GraphicalLasso(mode="cd")``:
|
|
1445
|
+
``alpha``, ``tol``, ``enet_tol``, ``max_iter`` and
|
|
1446
|
+
``assume_centered`` mean what they mean there. scikit-learn's
|
|
1447
|
+
``costs_`` (the objective each iteration, a log-determinant) is not
|
|
1448
|
+
computed.
|
|
1449
|
+
|
|
1450
|
+
Attributes after fit: covariance_, precision_, location_, n_iter_.
|
|
1451
|
+
"""
|
|
1452
|
+
|
|
1453
|
+
def __init__(self, alpha=0.01, *, tol=1e-4, enet_tol=1e-4, max_iter=100,
|
|
1454
|
+
assume_centered=False):
|
|
1455
|
+
self.alpha, self.tol, self.enet_tol = alpha, tol, enet_tol
|
|
1456
|
+
self.max_iter, self.assume_centered = max_iter, assume_centered
|
|
1457
|
+
|
|
1458
|
+
def fit(self, X, y=None):
|
|
1459
|
+
import importlib
|
|
1460
|
+
from ._libfind import gramops_backend
|
|
1461
|
+
X = fit_x(self, X, ensure_min_samples=2, ensure_min_features=2)
|
|
1462
|
+
n, p = X.shape
|
|
1463
|
+
self.location_ = np.zeros(p) if self.assume_centered else X.mean(axis=0)
|
|
1464
|
+
Xc = X - self.location_
|
|
1465
|
+
emp = np.ascontiguousarray(Xc.T @ Xc / n)
|
|
1466
|
+
lib = importlib.import_module(f"{__name__}._generated._loader_gramops_{gramops_backend()}")
|
|
1467
|
+
self.covariance_ = np.zeros((p, p))
|
|
1468
|
+
self.precision_ = np.zeros((p, p))
|
|
1469
|
+
self.n_iter_ = lib.graphical_lasso(emp, self.covariance_, self.precision_,
|
|
1470
|
+
float(self.alpha), int(self.max_iter),
|
|
1471
|
+
float(self.tol), float(self.enet_tol), p)
|
|
1472
|
+
if not np.isfinite(self.precision_).all():
|
|
1473
|
+
raise FloatingPointError("the system is too ill-conditioned for this solver")
|
|
1474
|
+
return self
|
|
1475
|
+
|
|
1476
|
+
|
|
1477
|
+
class LinearRANSAC(Tagged, RegressorMixin, BaseEstimator):
|
|
1478
|
+
"""RANSAC for a linear model, scikit-learn's algorithm translated.
|
|
1479
|
+
|
|
1480
|
+
``examples/kernel-only/ransac.scm`` is RANSACRegressor.fit around
|
|
1481
|
+
LinearRegression, written out whole: per trial, least squares with
|
|
1482
|
+
an intercept on a subset of ``min_samples`` points, the inliers
|
|
1483
|
+
within ``residual_threshold``, the fit kept by inlier count and then
|
|
1484
|
+
R^2, the trial count lowered by scikit-learn's _dynamic_max_trials;
|
|
1485
|
+
finally least squares on the best inliers. The defaults are
|
|
1486
|
+
RANSACRegressor's (min_samples p + 1, the threshold the MAD of y,
|
|
1487
|
+
max_trials 100, stop_probability 0.99).
|
|
1488
|
+
|
|
1489
|
+
The subsets are drawn here, ``max_trials`` of them, before the
|
|
1490
|
+
trials run. With scikit-learn installed they are drawn exactly as
|
|
1491
|
+
RANSACRegressor draws them (sample_without_replacement on a
|
|
1492
|
+
RandomState of ``random_state``), so the result is scikit-learn's;
|
|
1493
|
+
without it, numpy's choice draws different subsets.
|
|
1494
|
+
|
|
1495
|
+
Attributes after fit: coef_, intercept_, inlier_mask_, n_trials_.
|
|
1496
|
+
"""
|
|
1497
|
+
|
|
1498
|
+
def __init__(self, *, min_samples=None, residual_threshold=None, max_trials=100,
|
|
1499
|
+
stop_probability=0.99, random_state=None):
|
|
1500
|
+
self.min_samples, self.residual_threshold = min_samples, residual_threshold
|
|
1501
|
+
self.max_trials, self.stop_probability = max_trials, stop_probability
|
|
1502
|
+
self.random_state = random_state
|
|
1503
|
+
|
|
1504
|
+
def fit(self, X, y):
|
|
1505
|
+
X, y = fit_xy(self, X, y)
|
|
1506
|
+
n, p = X.shape
|
|
1507
|
+
max_trials = int(self.max_trials)
|
|
1508
|
+
ms = p + 1 if self.min_samples is None else int(self.min_samples)
|
|
1509
|
+
thr = (float(np.median(np.abs(y - np.median(y)))) if self.residual_threshold is None
|
|
1510
|
+
else float(self.residual_threshold))
|
|
1511
|
+
rs = np.random.RandomState(self.random_state)
|
|
1512
|
+
try:
|
|
1513
|
+
from sklearn.utils.random import sample_without_replacement
|
|
1514
|
+
draw = lambda: sample_without_replacement(n, ms, random_state=rs)
|
|
1515
|
+
except ImportError:
|
|
1516
|
+
draw = lambda: rs.choice(n, ms, replace=False)
|
|
1517
|
+
if ms > n:
|
|
1518
|
+
raise ValueError(f"min_samples may not be larger than the number of samples: "
|
|
1519
|
+
f"n_samples = {n}")
|
|
1520
|
+
subsets = np.array([draw() for _ in range(max_trials)], dtype=np.float64)
|
|
1521
|
+
coef = np.zeros(p)
|
|
1522
|
+
mask = np.zeros(n)
|
|
1523
|
+
out = np.zeros(1)
|
|
1524
|
+
self.n_trials_ = _classifier_kernels().ransac(
|
|
1525
|
+
X, y, subsets, coef, mask, out, max_trials, ms, thr, float(self.stop_probability),
|
|
1526
|
+
n, p)
|
|
1527
|
+
self.coef_, self.intercept_ = coef, float(out[0])
|
|
1528
|
+
self.inlier_mask_ = mask.astype(bool)
|
|
1529
|
+
return self
|
|
1530
|
+
|
|
1531
|
+
def predict(self, X):
|
|
1532
|
+
return pred_x(self, X) @ self.coef_ + self.intercept_
|
|
1533
|
+
|
|
1534
|
+
|
|
1535
|
+
def _center(X, y, fit_intercept):
|
|
1536
|
+
X = np.ascontiguousarray(X, dtype=np.float64)
|
|
1537
|
+
y = np.ascontiguousarray(y, dtype=np.float64)
|
|
1538
|
+
if fit_intercept:
|
|
1539
|
+
xm, ym = X.mean(axis=0), float(y.mean())
|
|
1540
|
+
return np.ascontiguousarray(X - xm), y - ym, xm, ym
|
|
1541
|
+
return X, y, np.zeros(X.shape[1]), 0.0
|
|
1542
|
+
|
|
1543
|
+
|
|
1544
|
+
_SGD_LOSS = {"hinge": 0, "squared_error": 1, "perceptron": 2, "epsilon_insensitive": 3}
|
|
1545
|
+
_SGD_LR = {"optimal": 0, "invscaling": 1, "constant": 2, "pa1": 3, "pa2": 4}
|
|
1546
|
+
|
|
1547
|
+
|
|
1548
|
+
class _SGDLinear(BaseEstimator):
|
|
1549
|
+
# the shared run; the subclasses' __init__ name the parameters
|
|
1550
|
+
# (scikit-learn reads them off the signature), so this one has none
|
|
1551
|
+
def _set(self, *, loss, penalty, alpha, max_iter, tol, n_iter_no_change, fit_intercept,
|
|
1552
|
+
learning_rate, eta0, power_t, epsilon=0.1):
|
|
1553
|
+
self.loss, self.penalty, self.alpha = loss, penalty, alpha
|
|
1554
|
+
self.max_iter, self.tol = max_iter, tol
|
|
1555
|
+
self.n_iter_no_change, self.fit_intercept = n_iter_no_change, fit_intercept
|
|
1556
|
+
self.learning_rate, self.eta0, self.power_t = learning_rate, eta0, power_t
|
|
1557
|
+
self.epsilon = epsilon
|
|
1558
|
+
|
|
1559
|
+
def _run(self, X, y):
|
|
1560
|
+
loss = getattr(self, "loss", "perceptron")
|
|
1561
|
+
lr = getattr(self, "learning_rate", "constant")
|
|
1562
|
+
penalty = getattr(self, "penalty", None)
|
|
1563
|
+
if loss not in _SGD_LOSS or lr not in _SGD_LR:
|
|
1564
|
+
raise ValueError("loss is one of %s, learning_rate one of %s"
|
|
1565
|
+
% (sorted(_SGD_LOSS), sorted(_SGD_LR)))
|
|
1566
|
+
if penalty not in ("l2", None):
|
|
1567
|
+
raise ValueError("penalty is 'l2' or None")
|
|
1568
|
+
n, p = X.shape
|
|
1569
|
+
w = np.zeros(p)
|
|
1570
|
+
out = np.zeros(1)
|
|
1571
|
+
self.n_iter_ = _classifier_kernels().sgd(
|
|
1572
|
+
X, np.ascontiguousarray(y, dtype=np.float64), w, out,
|
|
1573
|
+
_SGD_LOSS[loss], _SGD_LR[lr],
|
|
1574
|
+
1 if penalty == "l2" else 0, float(getattr(self, "alpha", 1e-4)),
|
|
1575
|
+
float(self.eta0), float(getattr(self, "power_t", 0.5)),
|
|
1576
|
+
float(getattr(self, "epsilon", 0.1)), int(self.max_iter), float(self.tol),
|
|
1577
|
+
int(self.n_iter_no_change), 1 if self.fit_intercept else 0, n, p)
|
|
1578
|
+
self.coef_, self.intercept_ = w, float(out[0])
|
|
1579
|
+
return self
|
|
1580
|
+
|
|
1581
|
+
def _linear(self, X):
|
|
1582
|
+
return pred_x(self, X) @ self.coef_ + self.intercept_
|
|
1583
|
+
|
|
1584
|
+
|
|
1585
|
+
class SGDLinearClassifier(Tagged, ClassifierMixin, _SGDLinear):
|
|
1586
|
+
"""scikit-learn's SGDClassifier by its own _plain_sgd translated
|
|
1587
|
+
(``examples/kernel-only/sgd.scm``), with shuffle=False: the points
|
|
1588
|
+
are visited in order. scikit-learn's shuffling draws from its own
|
|
1589
|
+
xorshift generator, which the kernel does not reproduce, so compare
|
|
1590
|
+
against ``SGDClassifier(shuffle=False)``, which this matches to
|
|
1591
|
+
rounding. loss "hinge" or "perceptron", penalty "l2" or None,
|
|
1592
|
+
learning_rate "optimal", "constant", "invscaling", "pa1" or "pa2"
|
|
1593
|
+
(the passive-aggressive steps, eta0 their C: scikit-learn's
|
|
1594
|
+
replacement for PassiveAggressiveClassifier). Two classes, the
|
|
1595
|
+
second of ``classes_`` positive.
|
|
1596
|
+
|
|
1597
|
+
Attributes after fit: classes_, coef_, intercept_, n_iter_.
|
|
1598
|
+
"""
|
|
1599
|
+
|
|
1600
|
+
def __init__(self, *, loss="hinge", penalty="l2", alpha=1e-4, max_iter=1000, tol=1e-3,
|
|
1601
|
+
n_iter_no_change=5, fit_intercept=True, learning_rate="optimal",
|
|
1602
|
+
eta0=0.01, power_t=0.5):
|
|
1603
|
+
self._set(loss=loss, penalty=penalty, alpha=alpha, max_iter=max_iter,
|
|
1604
|
+
tol=tol, n_iter_no_change=n_iter_no_change,
|
|
1605
|
+
fit_intercept=fit_intercept, learning_rate=learning_rate,
|
|
1606
|
+
eta0=eta0, power_t=power_t)
|
|
1607
|
+
del self.epsilon
|
|
1608
|
+
|
|
1609
|
+
_binary_only = True
|
|
1610
|
+
|
|
1611
|
+
def fit(self, X, y):
|
|
1612
|
+
X, y = fit_xy(self, X, y, classifier=True)
|
|
1613
|
+
self.classes_, ys = _binary_labels(y)
|
|
1614
|
+
return self._run(X, ys)
|
|
1615
|
+
|
|
1616
|
+
def decision_function(self, X):
|
|
1617
|
+
return self._linear(X)
|
|
1618
|
+
|
|
1619
|
+
def predict(self, X):
|
|
1620
|
+
z = self.decision_function(X)
|
|
1621
|
+
return self.classes_[(z > 0).astype(int)]
|
|
1622
|
+
|
|
1623
|
+
|
|
1624
|
+
class SGDLinearRegressor(Tagged, RegressorMixin, _SGDLinear):
|
|
1625
|
+
"""scikit-learn's SGDRegressor by its own _plain_sgd translated, with
|
|
1626
|
+
shuffle=False (see SGDLinearClassifier). loss "squared_error" or
|
|
1627
|
+
"epsilon_insensitive" (``epsilon``), penalty "l2" or None,
|
|
1628
|
+
learning_rate "invscaling" (the default), "constant", "optimal",
|
|
1629
|
+
"pa1" or "pa2" (scikit-learn's replacement for
|
|
1630
|
+
PassiveAggressiveRegressor).
|
|
1631
|
+
|
|
1632
|
+
Attributes after fit: coef_, intercept_, n_iter_.
|
|
1633
|
+
"""
|
|
1634
|
+
|
|
1635
|
+
def __init__(self, *, loss="squared_error", penalty="l2", alpha=1e-4, max_iter=1000,
|
|
1636
|
+
tol=1e-3, n_iter_no_change=5, fit_intercept=True,
|
|
1637
|
+
learning_rate="invscaling", eta0=0.01, power_t=0.25, epsilon=0.1):
|
|
1638
|
+
self._set(loss=loss, penalty=penalty, alpha=alpha, max_iter=max_iter,
|
|
1639
|
+
tol=tol, n_iter_no_change=n_iter_no_change,
|
|
1640
|
+
fit_intercept=fit_intercept, learning_rate=learning_rate,
|
|
1641
|
+
eta0=eta0, power_t=power_t, epsilon=epsilon)
|
|
1642
|
+
|
|
1643
|
+
def fit(self, X, y):
|
|
1644
|
+
X, y = fit_xy(self, X, y)
|
|
1645
|
+
return self._run(X, y)
|
|
1646
|
+
|
|
1647
|
+
def predict(self, X):
|
|
1648
|
+
return self._linear(X)
|
|
1649
|
+
|
|
1650
|
+
|
|
1651
|
+
class SGDPerceptron(SGDLinearClassifier):
|
|
1652
|
+
"""scikit-learn's Perceptron: SGDClassifier with the perceptron's
|
|
1653
|
+
hinge (threshold 0), no penalty and the constant step eta0 = 1, by
|
|
1654
|
+
the same translated _plain_sgd, shuffle=False.
|
|
1655
|
+
|
|
1656
|
+
Attributes after fit: classes_, coef_, intercept_, n_iter_.
|
|
1657
|
+
"""
|
|
1658
|
+
|
|
1659
|
+
# the loss, penalty and step rule are fixed, not parameters: _run
|
|
1660
|
+
# falls back to the perceptron's when the attributes are absent
|
|
1661
|
+
def __init__(self, *, max_iter=1000, tol=1e-3, n_iter_no_change=5, fit_intercept=True,
|
|
1662
|
+
eta0=1.0):
|
|
1663
|
+
self.max_iter, self.tol, self.n_iter_no_change = max_iter, tol, n_iter_no_change
|
|
1664
|
+
self.fit_intercept, self.eta0 = fit_intercept, eta0
|
|
1665
|
+
|
|
1666
|
+
|
|
1667
|
+
class GramOMP(Tagged, RegressorMixin, BaseEstimator):
|
|
1668
|
+
"""Orthogonal matching pursuit over the Gram matrix, scikit-learn's
|
|
1669
|
+
own algorithm translated (``examples/kernel-only/omp.scm``:
|
|
1670
|
+
orthogonal_mp_gram and _gram_omp, X'X and X'y by dsyrk and dgemv).
|
|
1671
|
+
``OrthogonalMatchingPursuit`` takes this route for n > p
|
|
1672
|
+
(precompute="auto"); here it is taken always. ``n_nonzero_coefs``
|
|
1673
|
+
defaults, as there, to 10% of the features.
|
|
1674
|
+
|
|
1675
|
+
Attributes after fit: coef_, intercept_, n_iter_.
|
|
1676
|
+
"""
|
|
1677
|
+
|
|
1678
|
+
def __init__(self, *, n_nonzero_coefs=None, fit_intercept=True):
|
|
1679
|
+
self.n_nonzero_coefs, self.fit_intercept = n_nonzero_coefs, fit_intercept
|
|
1680
|
+
|
|
1681
|
+
def fit(self, X, y):
|
|
1682
|
+
import importlib
|
|
1683
|
+
from ._libfind import gramops_backend
|
|
1684
|
+
X, y = fit_xy(self, X, y)
|
|
1685
|
+
Xc, yc, xm, ym = _center(X, y, self.fit_intercept)
|
|
1686
|
+
n, p = Xc.shape
|
|
1687
|
+
k = max(int(0.1 * p), 1) if self.n_nonzero_coefs is None else int(self.n_nonzero_coefs)
|
|
1688
|
+
lib = importlib.import_module(f"{__name__}._generated._loader_gramops_{gramops_backend()}")
|
|
1689
|
+
self.coef_ = np.zeros(p)
|
|
1690
|
+
self.n_iter_ = lib.omp(Xc, yc, self.coef_, np.zeros(1), 0, k, n, p)
|
|
1691
|
+
self.intercept_ = ym - float(xm @ self.coef_)
|
|
1692
|
+
return self
|
|
1693
|
+
|
|
1694
|
+
def predict(self, X):
|
|
1695
|
+
return pred_x(self, X) @ self.coef_ + self.intercept_
|
|
1696
|
+
|
|
1697
|
+
|
|
1698
|
+
_GRAMOPS = []
|
|
1699
|
+
|
|
1700
|
+
|
|
1701
|
+
def _gramops_kernels():
|
|
1702
|
+
# looked up once: several calls a fit, each a glob before
|
|
1703
|
+
if not _GRAMOPS:
|
|
1704
|
+
import importlib
|
|
1705
|
+
from ._libfind import gramops_backend
|
|
1706
|
+
_GRAMOPS.append(importlib.import_module(
|
|
1707
|
+
f"{__name__}._generated._loader_gramops_{gramops_backend()}"))
|
|
1708
|
+
return _GRAMOPS[0]
|
|
1709
|
+
|
|
1710
|
+
|
|
1711
|
+
class LBFGSLogisticRegression(Tagged, ClassifierMixin, BaseEstimator):
|
|
1712
|
+
"""L2-penalised logistic regression, scikit-learn's
|
|
1713
|
+
``LogisticRegression(solver="lbfgs")`` translated whole
|
|
1714
|
+
(``examples/kernel-only/logistic-lbfgs.scm`` over ``lbfgs.scm``):
|
|
1715
|
+
the mean binomial loss for two classes, the multinomial for more,
|
|
1716
|
+
plus |W|^2 / (2 C n), minimised by scipy's L-BFGS-B as written in
|
|
1717
|
+
the subset (ten pairs, the More-Thuente search, gtol ``tol``, ftol
|
|
1718
|
+
64 eps). The products X W' and R' X are dgemm where a CBLAS links.
|
|
1719
|
+
On the same data it takes the same iterations as scikit-learn and
|
|
1720
|
+
lands on the same coefficients to rounding.
|
|
1721
|
+
|
|
1722
|
+
Attributes after fit: classes_, coef_, intercept_, n_iter_.
|
|
1723
|
+
"""
|
|
1724
|
+
|
|
1725
|
+
def __init__(self, *, C=1.0, tol=1e-4, max_iter=100, fit_intercept=True):
|
|
1726
|
+
self.C, self.tol, self.max_iter, self.fit_intercept = C, tol, max_iter, fit_intercept
|
|
1727
|
+
|
|
1728
|
+
def fit(self, X, y):
|
|
1729
|
+
X, y = fit_xy(self, X, y, classifier=True)
|
|
1730
|
+
self.classes_, yi = classes_of(y)
|
|
1731
|
+
n, p = X.shape
|
|
1732
|
+
nc = len(self.classes_)
|
|
1733
|
+
if nc < 2:
|
|
1734
|
+
raise ValueError("This solver needs samples of at least 2 classes in the data, "
|
|
1735
|
+
f"but the data contains only one class: {self.classes_[0]!r}")
|
|
1736
|
+
multi = 1 if nc > 2 else 0
|
|
1737
|
+
k = nc if multi else 1
|
|
1738
|
+
coef = np.zeros(k * (p + 1))
|
|
1739
|
+
out = np.zeros(2)
|
|
1740
|
+
it = _gpu_or_cpu(n * p, getattr(self, "backend", None)).logistic_lbfgs(
|
|
1741
|
+
X, yi.astype(np.float64), coef, out, float(self.C), int(self.max_iter),
|
|
1742
|
+
float(self.tol), multi, 1 if self.fit_intercept else 0, n, p, k)
|
|
1743
|
+
self.coef_ = coef[:k * p].reshape(k, p).copy()
|
|
1744
|
+
self.intercept_ = coef[k * p:].copy()
|
|
1745
|
+
self.n_iter_ = np.array([min(it, self.max_iter)])
|
|
1746
|
+
return self
|
|
1747
|
+
|
|
1748
|
+
def decision_function(self, X):
|
|
1749
|
+
z = pred_x(self, X) @ self.coef_.T + self.intercept_
|
|
1750
|
+
return z[:, 0] if z.shape[1] == 1 else z
|
|
1751
|
+
|
|
1752
|
+
def predict_proba(self, X):
|
|
1753
|
+
z = self.decision_function(X)
|
|
1754
|
+
if z.ndim == 1:
|
|
1755
|
+
p1 = 1.0 / (1.0 + np.exp(-z))
|
|
1756
|
+
return np.column_stack([1.0 - p1, p1])
|
|
1757
|
+
e = np.exp(z - z.max(axis=1, keepdims=True))
|
|
1758
|
+
return e / e.sum(axis=1, keepdims=True)
|
|
1759
|
+
|
|
1760
|
+
def predict(self, X):
|
|
1761
|
+
z = self.decision_function(X)
|
|
1762
|
+
if z.ndim == 1:
|
|
1763
|
+
return self.classes_[(z > 0).astype(int)]
|
|
1764
|
+
return self.classes_[np.argmax(z, axis=1)]
|
|
1765
|
+
|
|
1766
|
+
|
|
1767
|
+
def _kfold(n, k):
|
|
1768
|
+
"""scikit-learn's KFold(k) without shuffling: contiguous test blocks,
|
|
1769
|
+
the first n % k of them one longer."""
|
|
1770
|
+
sizes = np.full(k, n // k)
|
|
1771
|
+
sizes[: n % k] += 1
|
|
1772
|
+
out, start = [], 0
|
|
1773
|
+
for sz in sizes:
|
|
1774
|
+
test = np.arange(start, start + sz)
|
|
1775
|
+
out.append((np.r_[np.arange(0, start), np.arange(start + sz, n)], test))
|
|
1776
|
+
start += sz
|
|
1777
|
+
return out
|
|
1778
|
+
|
|
1779
|
+
|
|
1780
|
+
def _stratified_kfold(y, k):
|
|
1781
|
+
"""scikit-learn's StratifiedKFold(k) without shuffling: classes in
|
|
1782
|
+
order of first appearance, each spread over the folds as the sorted
|
|
1783
|
+
labels dealt k ways."""
|
|
1784
|
+
y = np.asarray(y)
|
|
1785
|
+
_, y_idx, y_inv = np.unique(y, return_index=True, return_inverse=True)
|
|
1786
|
+
_, class_perm = np.unique(y_idx, return_inverse=True)
|
|
1787
|
+
y_enc = class_perm[y_inv]
|
|
1788
|
+
nc = len(y_idx)
|
|
1789
|
+
y_order = np.sort(y_enc)
|
|
1790
|
+
alloc = np.asarray([np.bincount(y_order[i::k], minlength=nc) for i in range(k)])
|
|
1791
|
+
test_folds = np.empty(len(y), dtype=int)
|
|
1792
|
+
for c in range(nc):
|
|
1793
|
+
test_folds[y_enc == c] = np.arange(k).repeat(alloc[:, c])
|
|
1794
|
+
return [(np.flatnonzero(test_folds != f), np.flatnonzero(test_folds == f))
|
|
1795
|
+
for f in range(k)]
|
|
1796
|
+
|
|
1797
|
+
|
|
1798
|
+
class _LBFGSGLM(Tagged, RegressorMixin, BaseEstimator):
|
|
1799
|
+
_family = None
|
|
1800
|
+
|
|
1801
|
+
def fit(self, X, y):
|
|
1802
|
+
X, y = fit_xy(self, X, y)
|
|
1803
|
+
n, p = X.shape
|
|
1804
|
+
fam, _ = self._family_power()
|
|
1805
|
+
if fam in (0, 2) and (y < 0).any() or fam == 1 and (y <= 0).any():
|
|
1806
|
+
raise ValueError(f"Some value(s) of y are out of the valid range of the loss "
|
|
1807
|
+
f"{type(self).__name__}")
|
|
1808
|
+
coef = np.zeros(p + 1)
|
|
1809
|
+
out = np.zeros(2)
|
|
1810
|
+
fam, power = self._family_power()
|
|
1811
|
+
it = _gpu_or_cpu(n * p, getattr(self, "backend", None)).glm(X, y, coef, out, fam, float(power), float(self.alpha),
|
|
1812
|
+
int(self.max_iter), float(self.tol),
|
|
1813
|
+
1 if self.fit_intercept else 0, n, p)
|
|
1814
|
+
self.coef_, self.intercept_ = coef[:p].copy(), float(coef[p])
|
|
1815
|
+
self.n_iter_ = min(it, self.max_iter)
|
|
1816
|
+
return self
|
|
1817
|
+
|
|
1818
|
+
def _linear(self, X):
|
|
1819
|
+
return pred_x(self, X) @ self.coef_ + self.intercept_
|
|
1820
|
+
|
|
1821
|
+
def predict(self, X):
|
|
1822
|
+
z = self._linear(X)
|
|
1823
|
+
return z if self._family_power() == (3, 0.0) else np.exp(z)
|
|
1824
|
+
|
|
1825
|
+
|
|
1826
|
+
class LBFGSPoissonRegressor(_LBFGSGLM):
|
|
1827
|
+
_positive_y = True
|
|
1828
|
+
|
|
1829
|
+
"""scikit-learn's ``PoissonRegressor`` (solver "lbfgs") translated
|
|
1830
|
+
whole (``examples/kernel-only/glm.scm`` over ``lbfgs.scm``): the mean
|
|
1831
|
+
half Poisson deviance with the log link plus alpha |w|^2 / 2, from
|
|
1832
|
+
scikit-learn's start. Same iterations and coefficients to rounding.
|
|
1833
|
+
|
|
1834
|
+
Attributes after fit: coef_, intercept_, n_iter_.
|
|
1835
|
+
"""
|
|
1836
|
+
|
|
1837
|
+
def __init__(self, *, alpha=1.0, fit_intercept=True, max_iter=100, tol=1e-4):
|
|
1838
|
+
self.alpha, self.fit_intercept, self.max_iter, self.tol = alpha, fit_intercept, max_iter, tol
|
|
1839
|
+
|
|
1840
|
+
def _family_power(self):
|
|
1841
|
+
return 0, 1.0
|
|
1842
|
+
|
|
1843
|
+
|
|
1844
|
+
class LBFGSGammaRegressor(LBFGSPoissonRegressor):
|
|
1845
|
+
"""scikit-learn's ``GammaRegressor`` (solver "lbfgs"), as
|
|
1846
|
+
LBFGSPoissonRegressor with the half Gamma deviance.
|
|
1847
|
+
|
|
1848
|
+
Attributes after fit: coef_, intercept_, n_iter_.
|
|
1849
|
+
"""
|
|
1850
|
+
|
|
1851
|
+
def _family_power(self):
|
|
1852
|
+
return 1, 2.0
|
|
1853
|
+
|
|
1854
|
+
|
|
1855
|
+
class LBFGSTweedieRegressor(_LBFGSGLM):
|
|
1856
|
+
"""scikit-learn's ``TweedieRegressor`` (solver "lbfgs"): ``power`` 0
|
|
1857
|
+
with the identity link (the default), or a power of 1 or more with
|
|
1858
|
+
the log link, as link="auto" chooses.
|
|
1859
|
+
|
|
1860
|
+
Attributes after fit: coef_, intercept_, n_iter_.
|
|
1861
|
+
"""
|
|
1862
|
+
|
|
1863
|
+
def __init__(self, *, power=0.0, alpha=1.0, fit_intercept=True, max_iter=100, tol=1e-4):
|
|
1864
|
+
self.power, self.alpha, self.fit_intercept = power, alpha, fit_intercept
|
|
1865
|
+
self.max_iter, self.tol = max_iter, tol
|
|
1866
|
+
|
|
1867
|
+
def _family_power(self):
|
|
1868
|
+
if self.power == 0:
|
|
1869
|
+
return 3, 0.0
|
|
1870
|
+
if self.power < 1:
|
|
1871
|
+
raise ValueError("power is 0 or at least 1 here")
|
|
1872
|
+
return 2, float(self.power)
|
|
1873
|
+
|
|
1874
|
+
|
|
1875
|
+
class LBFGSLogisticRegressionCV(Tagged, ClassifierMixin, BaseEstimator):
|
|
1876
|
+
"""scikit-learn's ``LogisticRegressionCV`` with its defaults (the L2
|
|
1877
|
+
penalty, solver "lbfgs", Cs 10 values from 1e-4 to 1e4,
|
|
1878
|
+
StratifiedKFold(5) without shuffling, accuracy, refit): each fold's
|
|
1879
|
+
path over the Cs warm-started from the previous C by the translated
|
|
1880
|
+
LogisticRegression (``logistic-lbfgs.scm``), the C of the best summed
|
|
1881
|
+
score, and the refit from the folds' mean coefficients at that C, as
|
|
1882
|
+
scikit-learn starts it.
|
|
1883
|
+
|
|
1884
|
+
Attributes after fit: classes_, Cs_, C_, coef_, intercept_, scores_,
|
|
1885
|
+
n_iter_.
|
|
1886
|
+
"""
|
|
1887
|
+
|
|
1888
|
+
def __init__(self, *, Cs=10, cv=5, tol=1e-4, max_iter=100, fit_intercept=True):
|
|
1889
|
+
self.Cs, self.cv, self.tol, self.max_iter = Cs, cv, tol, max_iter
|
|
1890
|
+
self.fit_intercept = fit_intercept
|
|
1891
|
+
|
|
1892
|
+
def _path(self, X, yi, Cs, coef, multi, k):
|
|
1893
|
+
n, p = X.shape
|
|
1894
|
+
lib = _gpu_or_cpu(n * p, getattr(self, "backend", None))
|
|
1895
|
+
coefs, iters = [], []
|
|
1896
|
+
out = np.zeros(2)
|
|
1897
|
+
for C in Cs:
|
|
1898
|
+
it = lib.logistic_lbfgs(X, yi, coef, out, float(C), int(self.max_iter),
|
|
1899
|
+
float(self.tol), multi, 1 if self.fit_intercept else 0,
|
|
1900
|
+
n, p, k)
|
|
1901
|
+
coefs.append(coef.copy())
|
|
1902
|
+
iters.append(min(it, self.max_iter))
|
|
1903
|
+
return coefs, iters
|
|
1904
|
+
|
|
1905
|
+
def fit(self, X, y):
|
|
1906
|
+
X, y = fit_xy(self, X, y, classifier=True)
|
|
1907
|
+
self.classes_, yi = classes_of(y)
|
|
1908
|
+
nc = len(self.classes_)
|
|
1909
|
+
if nc < 2:
|
|
1910
|
+
raise ValueError("This solver needs samples of at least 2 classes in the data, "
|
|
1911
|
+
f"but the data contains only one class: {self.classes_[0]!r}")
|
|
1912
|
+
multi = 1 if nc > 2 else 0
|
|
1913
|
+
k = nc if multi else 1
|
|
1914
|
+
n, p = X.shape
|
|
1915
|
+
Cs = (np.logspace(-4, 4, int(self.Cs)) if np.isscalar(self.Cs)
|
|
1916
|
+
else np.asarray(self.Cs, float))
|
|
1917
|
+
self.Cs_ = Cs
|
|
1918
|
+
yf = yi.astype(np.float64)
|
|
1919
|
+
folds = _stratified_kfold(y, int(self.cv))
|
|
1920
|
+
paths, scores, iters = [], [], []
|
|
1921
|
+
for train, test in folds:
|
|
1922
|
+
coefs, it = self._path(np.ascontiguousarray(X[train]), yf[train], Cs,
|
|
1923
|
+
np.zeros(k * (p + 1)), multi, k)
|
|
1924
|
+
paths.append(coefs)
|
|
1925
|
+
iters.append(it)
|
|
1926
|
+
sc = []
|
|
1927
|
+
for c in coefs:
|
|
1928
|
+
z = X[test] @ c[:k * p].reshape(k, p).T + c[k * p:]
|
|
1929
|
+
pred = (z[:, 0] > 0).astype(int) if k == 1 else np.argmax(z, axis=1)
|
|
1930
|
+
sc.append(float(np.mean(pred == yi[test])))
|
|
1931
|
+
scores.append(sc)
|
|
1932
|
+
scores = np.asarray(scores)
|
|
1933
|
+
best = int(np.argmax(scores.sum(axis=0)))
|
|
1934
|
+
self.C_ = float(Cs[best])
|
|
1935
|
+
start = np.mean([paths[f][best] for f in range(len(folds))], axis=0)
|
|
1936
|
+
coefs, it = self._path(X, yf, [self.C_], start.copy(), multi, k)
|
|
1937
|
+
c = coefs[0]
|
|
1938
|
+
self.coef_ = c[:k * p].reshape(k, p).copy()
|
|
1939
|
+
self.intercept_ = c[k * p:].copy()
|
|
1940
|
+
self.scores_ = scores
|
|
1941
|
+
self.n_iter_ = np.asarray(iters)
|
|
1942
|
+
return self
|
|
1943
|
+
|
|
1944
|
+
def decision_function(self, X):
|
|
1945
|
+
z = pred_x(self, X) @ self.coef_.T + self.intercept_
|
|
1946
|
+
return z[:, 0] if z.shape[1] == 1 else z
|
|
1947
|
+
|
|
1948
|
+
def predict(self, X):
|
|
1949
|
+
z = self.decision_function(X)
|
|
1950
|
+
if z.ndim == 1:
|
|
1951
|
+
return self.classes_[(z > 0).astype(int)]
|
|
1952
|
+
return self.classes_[np.argmax(z, axis=1)]
|
|
1953
|
+
|
|
1954
|
+
|
|
1955
|
+
class LBFGSHuberRegressor(Tagged, RegressorMixin, BaseEstimator):
|
|
1956
|
+
"""The Huber regressor, scikit-learn's ``HuberRegressor`` translated
|
|
1957
|
+
whole (``examples/kernel-only/huber.scm`` over ``lbfgs.scm``): its
|
|
1958
|
+
_huber_loss_and_gradient over the weights, the intercept and the
|
|
1959
|
+
scale, minimised by L-BFGS-B with the scale bounded below by 10 eps
|
|
1960
|
+
(the generalized Cauchy point and the subspace step for that one
|
|
1961
|
+
bound, as L-BFGS-B takes them), scipy's defaults otherwise. X w
|
|
1962
|
+
and X'c are dgemv where a CBLAS links. Same iterations and
|
|
1963
|
+
coefficients as scikit-learn to rounding.
|
|
1964
|
+
|
|
1965
|
+
Attributes after fit: coef_, intercept_, scale_, n_iter_, outliers_.
|
|
1966
|
+
"""
|
|
1967
|
+
|
|
1968
|
+
def __init__(self, *, epsilon=1.35, alpha=1e-4, max_iter=100, tol=1e-5,
|
|
1969
|
+
fit_intercept=True):
|
|
1970
|
+
self.epsilon, self.alpha, self.max_iter, self.tol = epsilon, alpha, max_iter, tol
|
|
1971
|
+
self.fit_intercept = fit_intercept
|
|
1972
|
+
|
|
1973
|
+
def fit(self, X, y):
|
|
1974
|
+
X, y = fit_xy(self, X, y)
|
|
1975
|
+
n, p = X.shape
|
|
1976
|
+
coef = np.zeros(p + 2)
|
|
1977
|
+
coef[-1] = 1.0
|
|
1978
|
+
out = np.zeros(2)
|
|
1979
|
+
it = _gpu_or_cpu(n * p, getattr(self, "backend", None)).huber(
|
|
1980
|
+
X, y, coef, out, float(self.epsilon), float(self.alpha), int(self.max_iter),
|
|
1981
|
+
float(self.tol), 1 if self.fit_intercept else 0, n, p)
|
|
1982
|
+
self.coef_ = coef[:p].copy()
|
|
1983
|
+
self.intercept_ = float(coef[p])
|
|
1984
|
+
self.scale_ = float(coef[p + 1])
|
|
1985
|
+
self.n_iter_ = min(it, self.max_iter)
|
|
1986
|
+
self.outliers_ = np.abs(y - X @ self.coef_ - self.intercept_) > self.scale_ * self.epsilon
|
|
1987
|
+
return self
|
|
1988
|
+
|
|
1989
|
+
def predict(self, X):
|
|
1990
|
+
return pred_x(self, X) @ self.coef_ + self.intercept_
|
|
1991
|
+
|
|
1992
|
+
|
|
1993
|
+
class EMGaussianMixture(Tagged, DensityMixin, BaseEstimator):
|
|
1994
|
+
"""The Gaussian mixture by EM with full covariances, scikit-learn's
|
|
1995
|
+
``GaussianMixture(covariance_type="full")`` translated whole
|
|
1996
|
+
(``examples/kernel-only/gmm.scm``): its M step (means and
|
|
1997
|
+
covariances by products, the Cholesky factor, the precision factor
|
|
1998
|
+
L^-T), its E step (the log densities through X U - mu U, a product;
|
|
1999
|
+
log-sum-exp; the lower bound) and its stop on the bound. The start
|
|
2000
|
+
is scikit-learn's: ``init_params="kmeans"`` takes the labels of
|
|
2001
|
+
``KMeans(n_init=1)`` (scikit-learn's, so it must be installed) drawn
|
|
2002
|
+
from ``RandomState(random_state)``, ``"random"`` uniform draws
|
|
2003
|
+
normalised; with the same ``random_state`` the fit is
|
|
2004
|
+
scikit-learn's to rounding. One initialisation (n_init=1).
|
|
2005
|
+
|
|
2006
|
+
Attributes after fit: weights_, means_, precisions_cholesky_,
|
|
2007
|
+
precisions_, covariances_, converged_, n_iter_, lower_bound_.
|
|
2008
|
+
"""
|
|
2009
|
+
|
|
2010
|
+
def __init__(self, n_components=1, *, tol=1e-3, reg_covar=1e-6, max_iter=100,
|
|
2011
|
+
init_params="kmeans", random_state=None):
|
|
2012
|
+
self.n_components, self.tol, self.reg_covar = n_components, tol, reg_covar
|
|
2013
|
+
self.max_iter, self.init_params, self.random_state = max_iter, init_params, random_state
|
|
2014
|
+
|
|
2015
|
+
def fit(self, X, y=None):
|
|
2016
|
+
X = fit_x(self, X, ensure_min_samples=2)
|
|
2017
|
+
n, p = X.shape
|
|
2018
|
+
k = int(self.n_components)
|
|
2019
|
+
if k > n:
|
|
2020
|
+
raise ValueError(f"Expected n_samples >= n_components but got n_components = {k}, "
|
|
2021
|
+
f"n_samples = {n}")
|
|
2022
|
+
rs = np.random.RandomState(self.random_state)
|
|
2023
|
+
if self.init_params == "kmeans":
|
|
2024
|
+
try:
|
|
2025
|
+
from sklearn.cluster import KMeans
|
|
2026
|
+
except ImportError as e:
|
|
2027
|
+
raise ImportError("init_params='kmeans' runs scikit-learn's KMeans; "
|
|
2028
|
+
"install scikit-learn or pass init_params='random'") from e
|
|
2029
|
+
labels = KMeans(n_clusters=k, n_init=1, random_state=rs).fit(X).labels_
|
|
2030
|
+
resp = np.zeros((n, k))
|
|
2031
|
+
resp[np.arange(n), labels] = 1.0
|
|
2032
|
+
elif self.init_params == "random":
|
|
2033
|
+
resp = rs.uniform(size=(n, k))
|
|
2034
|
+
resp /= resp.sum(axis=1)[:, None]
|
|
2035
|
+
else:
|
|
2036
|
+
raise ValueError("init_params is 'kmeans' or 'random'")
|
|
2037
|
+
resp = np.ascontiguousarray(resp)
|
|
2038
|
+
self.weights_ = np.zeros(k)
|
|
2039
|
+
self.means_ = np.zeros((k, p))
|
|
2040
|
+
self.precisions_cholesky_ = np.zeros((k, p, p))
|
|
2041
|
+
labels = np.zeros(n)
|
|
2042
|
+
out = np.zeros(2)
|
|
2043
|
+
it = _gpu_or_cpu(n * p, getattr(self, "backend", None)).gmm(X, resp, self.weights_, self.means_,
|
|
2044
|
+
self.precisions_cholesky_, labels, out,
|
|
2045
|
+
int(self.max_iter), float(self.tol),
|
|
2046
|
+
float(self.reg_covar), n, p, k)
|
|
2047
|
+
self.n_iter_ = it
|
|
2048
|
+
self.lower_bound_ = float(out[0])
|
|
2049
|
+
self.converged_ = bool(out[1] == 1.0)
|
|
2050
|
+
self.precisions_ = np.einsum("kij,klj->kil", self.precisions_cholesky_,
|
|
2051
|
+
self.precisions_cholesky_)
|
|
2052
|
+
self.covariances_ = np.linalg.inv(self.precisions_)
|
|
2053
|
+
return self
|
|
2054
|
+
|
|
2055
|
+
def _weighted_log_prob(self, X):
|
|
2056
|
+
X = pred_x(self, X)
|
|
2057
|
+
n, p = X.shape
|
|
2058
|
+
out = np.empty((n, len(self.weights_)))
|
|
2059
|
+
for c, (mu, u) in enumerate(zip(self.means_, self.precisions_cholesky_)):
|
|
2060
|
+
y = X @ u - mu @ u
|
|
2061
|
+
out[:, c] = (-0.5 * (p * np.log(2 * np.pi) + (y * y).sum(axis=1))
|
|
2062
|
+
+ np.log(np.diag(u)).sum() + np.log(self.weights_[c]))
|
|
2063
|
+
return out
|
|
2064
|
+
|
|
2065
|
+
def predict_proba(self, X):
|
|
2066
|
+
w = self._weighted_log_prob(X)
|
|
2067
|
+
w -= w.max(axis=1, keepdims=True)
|
|
2068
|
+
e = np.exp(w)
|
|
2069
|
+
return e / e.sum(axis=1, keepdims=True)
|
|
2070
|
+
|
|
2071
|
+
def predict(self, X):
|
|
2072
|
+
return np.argmax(self._weighted_log_prob(X), axis=1)
|
|
2073
|
+
|
|
2074
|
+
def fit_predict(self, X, y=None):
|
|
2075
|
+
return self.fit(X).predict(X)
|
|
2076
|
+
|
|
2077
|
+
def score_samples(self, X):
|
|
2078
|
+
"""The log density of each row under the mixture."""
|
|
2079
|
+
w = self._weighted_log_prob(X)
|
|
2080
|
+
m = w.max(axis=1)
|
|
2081
|
+
return m + np.log(np.exp(w - m[:, None]).sum(axis=1))
|
|
2082
|
+
|
|
2083
|
+
def score(self, X, y=None):
|
|
2084
|
+
"""The mean log-likelihood, as GaussianMixture.score."""
|
|
2085
|
+
return float(self.score_samples(X).mean())
|
|
2086
|
+
|
|
2087
|
+
|
|
2088
|
+
class CovPCA(Tagged, TransformerMixin, BaseEstimator):
|
|
2089
|
+
"""PCA through the covariance's eigenvectors, scikit-learn's
|
|
2090
|
+
``PCA(svd_solver="covariance_eigh")`` -- what its "auto" picks for a
|
|
2091
|
+
tall design (p <= 1000, n >= 10 p) -- translated whole
|
|
2092
|
+
(``examples/kernel-only/pca.scm``: X'X by dsyrk where a CBLAS links,
|
|
2093
|
+
the eigenproblem by sym-eigen.scm -- which the BLAS build hands to
|
|
2094
|
+
LAPACK's dsyevd, as numpy's eigh does, and the loop build runs as
|
|
2095
|
+
Householder reduction and QL iteration in the subset -- and
|
|
2096
|
+
svd_flip's signs). The BLAS build is level with scikit-learn on one
|
|
2097
|
+
core at 50,000 x 500 and with sklearnex on twenty; the loop build's
|
|
2098
|
+
eigensolver falls behind LAPACK's from about a hundred features.
|
|
2099
|
+
|
|
2100
|
+
Attributes after fit: components_, explained_variance_,
|
|
2101
|
+
explained_variance_ratio_, singular_values_, mean_, noise_variance_,
|
|
2102
|
+
n_components_.
|
|
2103
|
+
"""
|
|
2104
|
+
|
|
2105
|
+
def __init__(self, n_components=None, *, backend=None):
|
|
2106
|
+
self.n_components, self.backend = n_components, backend
|
|
2107
|
+
|
|
2108
|
+
def fit(self, X, y=None):
|
|
2109
|
+
X = fit_x(self, X)
|
|
2110
|
+
n, p = X.shape
|
|
2111
|
+
if self.n_components is not None and not 0 <= int(self.n_components) <= min(n, p):
|
|
2112
|
+
raise ValueError(f"n_components={self.n_components!r} must be between 0 and "
|
|
2113
|
+
f"min(n_samples, n_features)={min(n, p)}")
|
|
2114
|
+
comps = np.zeros((p, p)); ev = np.zeros(p); sv = np.zeros(p); mean = np.zeros(p)
|
|
2115
|
+
# the device forms the covariance faster, but its build keeps
|
|
2116
|
+
# sym-eigen.scm's eigensolver; where the CPU build has LAPACK's
|
|
2117
|
+
# (gramops "blas") that is the faster whole, so the device is
|
|
2118
|
+
# taken by default only without it
|
|
2119
|
+
from ._libfind import gramops_backend
|
|
2120
|
+
dev = self.backend == "cublas" or (self.backend is None and gramops_backend() != "blas")
|
|
2121
|
+
lib = _accel(float(n) * p * p) if dev else None
|
|
2122
|
+
(lib or _gramops_kernels()).pca(X, comps, ev, sv, mean, n, p)
|
|
2123
|
+
k = min(n, p) if self.n_components is None else int(self.n_components)
|
|
2124
|
+
self.n_components_ = k
|
|
2125
|
+
self.mean_ = mean
|
|
2126
|
+
self.components_ = comps[:k].copy()
|
|
2127
|
+
self.explained_variance_ = ev[:k].copy()
|
|
2128
|
+
tot = ev.sum()
|
|
2129
|
+
self.explained_variance_ratio_ = ev[:k] / tot if tot > 0 else np.zeros(k)
|
|
2130
|
+
self.singular_values_ = sv[:k].copy()
|
|
2131
|
+
self.noise_variance_ = float(ev[k:].mean()) if k < min(n, p) else 0.0
|
|
2132
|
+
return self
|
|
2133
|
+
|
|
2134
|
+
def transform(self, X):
|
|
2135
|
+
return (pred_x(self, X) - self.mean_) @ self.components_.T
|
|
2136
|
+
|
|
2137
|
+
def fit_transform(self, X, y=None):
|
|
2138
|
+
return self.fit(X).transform(X)
|
|
2139
|
+
|
|
2140
|
+
|
|
2141
|
+
_KPCA_KERNELS = {"linear": 0, "rbf": 1, "poly": 2, "sigmoid": 3, "cosine": 4}
|
|
2142
|
+
|
|
2143
|
+
|
|
2144
|
+
def _psd_eigenvalues(vals):
|
|
2145
|
+
# scikit-learn's _check_psd_eigenvalues (double precision): refuse
|
|
2146
|
+
# a matrix far from PSD, zero the small negative and positive noise
|
|
2147
|
+
vals = np.array(vals, dtype=np.float64)
|
|
2148
|
+
top = vals.max() if vals.size else 0.0
|
|
2149
|
+
if top < 0:
|
|
2150
|
+
raise ValueError(f"All eigenvalues are negative (maximum is {top:g}). Either the "
|
|
2151
|
+
"matrix is not PSD, or there was an issue while computing the "
|
|
2152
|
+
"eigendecomposition of the matrix.")
|
|
2153
|
+
low = vals.min() if vals.size else 0.0
|
|
2154
|
+
if low < -1e-5 * top and low < -1e-10:
|
|
2155
|
+
raise ValueError(f"There are significant negative eigenvalues ({-low / top:g} of the "
|
|
2156
|
+
"maximum positive). Either the matrix is not PSD, or there was an "
|
|
2157
|
+
"issue while computing the eigendecomposition of the matrix.")
|
|
2158
|
+
vals[vals < 0] = 0.0
|
|
2159
|
+
vals[vals < 1e-12 * top] = 0.0
|
|
2160
|
+
return vals
|
|
2161
|
+
|
|
2162
|
+
|
|
2163
|
+
class GramKernelPCA(Tagged, TransformerMixin, BaseEstimator):
|
|
2164
|
+
"""scikit-learn's ``KernelPCA``: the kernel matrix (linear, rbf,
|
|
2165
|
+
poly, sigmoid, cosine or precomputed), centred in feature space, its
|
|
2166
|
+
largest eigenpairs with svd_flip's signs (``examples/kernel-only/
|
|
2167
|
+
kernel-pca.scm``). Three routes to the eigenpairs:
|
|
2168
|
+
|
|
2169
|
+
- the linear kernel with n_components <= p < n: through Xc'Xc
|
|
2170
|
+
(p x p, X'X by dsyrk), O(n p^2) instead of the n x n matrix;
|
|
2171
|
+
- a few pairs of a large matrix (``eigen_solver="auto"``, where
|
|
2172
|
+
scikit-learn takes ARPACK, and ``"subspace"``): subspace iteration
|
|
2173
|
+
whose product is a dgemm, stopped when every residual
|
|
2174
|
+
|K u - lam u| is under ``tol`` (1e-12 for 0) of the largest value,
|
|
2175
|
+
the dense route when it does not converge in ``max_iter`` steps;
|
|
2176
|
+
- otherwise dense: LAPACK's dsyevr over the wanted index range, as
|
|
2177
|
+
scipy's eigh(subset_by_index) that scikit-learn calls.
|
|
2178
|
+
|
|
2179
|
+
No inverse transform. Attributes after fit: eigenvalues_,
|
|
2180
|
+
eigenvectors_, X_fit_, gamma_, n_iter_ (the subspace steps, 1 for a
|
|
2181
|
+
direct solve), n_features_in_.
|
|
2182
|
+
"""
|
|
2183
|
+
|
|
2184
|
+
def __init__(self, n_components=None, *, kernel="linear", gamma=None, degree=3, coef0=1,
|
|
2185
|
+
eigen_solver="auto", tol=0, max_iter=None, remove_zero_eig=False,
|
|
2186
|
+
random_state=None):
|
|
2187
|
+
self.n_components, self.kernel, self.gamma = n_components, kernel, gamma
|
|
2188
|
+
self.degree, self.coef0, self.eigen_solver = degree, coef0, eigen_solver
|
|
2189
|
+
self.tol, self.max_iter, self.remove_zero_eig = tol, max_iter, remove_zero_eig
|
|
2190
|
+
self.random_state = random_state
|
|
2191
|
+
|
|
2192
|
+
def _kernel(self, A, B):
|
|
2193
|
+
k = _gramops_kernels()
|
|
2194
|
+
if B is None:
|
|
2195
|
+
K = np.zeros((A.shape[0], A.shape[0]))
|
|
2196
|
+
k.kpca_kernel_self(A, K, self._kind, self.gamma_, float(self.degree),
|
|
2197
|
+
float(self.coef0), A.shape[0], A.shape[1])
|
|
2198
|
+
else:
|
|
2199
|
+
K = np.zeros((A.shape[0], B.shape[0]))
|
|
2200
|
+
k.kpca_kernel(A, B, K, self._kind, self.gamma_, float(self.degree),
|
|
2201
|
+
float(self.coef0), A.shape[0], B.shape[0], A.shape[1])
|
|
2202
|
+
return K
|
|
2203
|
+
|
|
2204
|
+
def fit(self, X, y=None):
|
|
2205
|
+
X = fit_x(self, X)
|
|
2206
|
+
n, p = X.shape
|
|
2207
|
+
if self.kernel != "precomputed" and self.kernel not in _KPCA_KERNELS:
|
|
2208
|
+
raise ValueError(f"kernel={self.kernel!r}: expected one of "
|
|
2209
|
+
f"{sorted(_KPCA_KERNELS) + ['precomputed']}")
|
|
2210
|
+
if self.eigen_solver not in ("auto", "dense", "subspace"):
|
|
2211
|
+
raise ValueError(f"eigen_solver={self.eigen_solver!r}: expected 'auto', 'dense' "
|
|
2212
|
+
"or 'subspace'")
|
|
2213
|
+
if self.n_components is not None and int(self.n_components) < 1:
|
|
2214
|
+
raise ValueError(f"n_components={self.n_components!r} must be at least 1")
|
|
2215
|
+
if self.kernel == "precomputed" and n != p:
|
|
2216
|
+
raise ValueError(f"precomputed kernel is {n} x {p}; expected a square matrix")
|
|
2217
|
+
self._kind = _KPCA_KERNELS.get(self.kernel, -1)
|
|
2218
|
+
self.gamma_ = 1.0 / p if self.gamma is None else float(self.gamma)
|
|
2219
|
+
c = n if self.n_components is None else min(n, int(self.n_components))
|
|
2220
|
+
k = _gramops_kernels()
|
|
2221
|
+
vecs, vals = np.zeros((n, c)), np.zeros(c)
|
|
2222
|
+
m = np.zeros(n + 1)
|
|
2223
|
+
self.n_iter_ = 1
|
|
2224
|
+
if self.kernel == "linear" and c <= p < n:
|
|
2225
|
+
xm = X.mean(axis=0)
|
|
2226
|
+
G = _gram(X)
|
|
2227
|
+
nm2 = n * xm * xm
|
|
2228
|
+
if np.all(np.diagonal(G) - nm2 > 1e-4 * nm2):
|
|
2229
|
+
G -= np.outer(xm, n * xm)
|
|
2230
|
+
Xs, xs = X, xm
|
|
2231
|
+
else:
|
|
2232
|
+
Xs, xs = np.ascontiguousarray(X - xm), np.zeros(p)
|
|
2233
|
+
G = _gram(Xs)
|
|
2234
|
+
k.kpca_linear(Xs, xs, np.ascontiguousarray(G), vecs, vals, n, p, c)
|
|
2235
|
+
# KernelCenterer's statistics of K = X X', for transform
|
|
2236
|
+
m[:n] = X @ xm
|
|
2237
|
+
m[n] = float(xm @ xm)
|
|
2238
|
+
else:
|
|
2239
|
+
K = np.array(X, dtype=np.float64, order="C") if self.kernel == "precomputed" \
|
|
2240
|
+
else self._kernel(X, None)
|
|
2241
|
+
k.kpca_centre(K, m, n)
|
|
2242
|
+
# c + 25 columns: on an rbf kernel whose leading values fall
|
|
2243
|
+
# slowly (0.92 of the first by the fifth), 13 took 235 steps
|
|
2244
|
+
# to 1e-12 and 30 took 15
|
|
2245
|
+
b = min(n, max(2 * c, c + 25))
|
|
2246
|
+
# where scikit-learn's "auto" takes ARPACK; for 50 components
|
|
2247
|
+
# of 3,000 the 100 columns lost to dsyevr (6.2 s to 3.6 s)
|
|
2248
|
+
sub = self.eigen_solver == "subspace" or (
|
|
2249
|
+
self.eigen_solver == "auto" and n > 200 and c < 10)
|
|
2250
|
+
done = False
|
|
2251
|
+
if sub and b < n:
|
|
2252
|
+
rs = self.random_state
|
|
2253
|
+
rng = rs if isinstance(rs, np.random.RandomState) else \
|
|
2254
|
+
np.random.RandomState(0 if rs is None else int(rs))
|
|
2255
|
+
Q = rng.standard_normal((n, b))
|
|
2256
|
+
tol = float(self.tol) if float(self.tol) > 0 else 1e-12
|
|
2257
|
+
it = 1000 if self.max_iter is None else int(self.max_iter)
|
|
2258
|
+
steps = k.kpca_top_subspace(K, Q, vecs, vals, n, b, c, tol, it)
|
|
2259
|
+
done = steps >= 0
|
|
2260
|
+
if done:
|
|
2261
|
+
self.n_iter_ = steps
|
|
2262
|
+
if not done:
|
|
2263
|
+
k.kpca_top_dense(K, vecs, vals, n, c)
|
|
2264
|
+
vals = _psd_eigenvalues(vals)
|
|
2265
|
+
if self.remove_zero_eig or self.n_components is None:
|
|
2266
|
+
keep = vals > 0
|
|
2267
|
+
vecs, vals = vecs[:, keep], vals[keep]
|
|
2268
|
+
self.eigenvalues_ = vals
|
|
2269
|
+
self.eigenvectors_ = np.ascontiguousarray(vecs)
|
|
2270
|
+
self.X_fit_ = X
|
|
2271
|
+
self._m = m
|
|
2272
|
+
return self
|
|
2273
|
+
|
|
2274
|
+
def fit_transform(self, X, y=None):
|
|
2275
|
+
self.fit(X)
|
|
2276
|
+
return self.eigenvectors_ * np.sqrt(self.eigenvalues_)
|
|
2277
|
+
|
|
2278
|
+
def transform(self, X):
|
|
2279
|
+
X = pred_x(self, X)
|
|
2280
|
+
n = self.X_fit_.shape[0]
|
|
2281
|
+
if self.kernel == "precomputed":
|
|
2282
|
+
if X.shape[1] != n:
|
|
2283
|
+
raise ValueError(f"precomputed kernel has {X.shape[1]} columns; expected {n}")
|
|
2284
|
+
K = np.array(X, dtype=np.float64, order="C")
|
|
2285
|
+
else:
|
|
2286
|
+
K = self._kernel(X, self.X_fit_)
|
|
2287
|
+
_gramops_kernels().kpca_centre_new(K, self._m, K.shape[0], n)
|
|
2288
|
+
nz = self.eigenvalues_ > 0
|
|
2289
|
+
scaled = np.zeros_like(self.eigenvectors_)
|
|
2290
|
+
scaled[:, nz] = self.eigenvectors_[:, nz] / np.sqrt(self.eigenvalues_[nz])
|
|
2291
|
+
return K @ scaled
|
|
2292
|
+
|
|
2293
|
+
|
|
2294
|
+
class GramLDA(Tagged, ClassifierMixin, TransformerMixin, BaseEstimator):
|
|
2295
|
+
"""scikit-learn's ``LinearDiscriminantAnalysis`` over the Gram matrix
|
|
2296
|
+
(``examples/kernel-only/lda.scm``): X'X by dsyrk and the class means
|
|
2297
|
+
in one pass, then the three solvers' own steps on p x p matrices --
|
|
2298
|
+
``"svd"`` (the default) takes its first SVD, of the n x p
|
|
2299
|
+
standardised within-class design, as the eigenproblem of
|
|
2300
|
+
D^-1 Sw D^-1 (dsyevd); ``"lsqr"`` solves Sw W = M' by Cholesky;
|
|
2301
|
+
``"eigen"`` is Sb v = lam Sw v (dsygvd). ``shrinkage`` takes None or a
|
|
2302
|
+
float (not "auto"); no ``covariance_estimator``.
|
|
2303
|
+
|
|
2304
|
+
Attributes after fit: classes_, coef_, intercept_, means_, priors_,
|
|
2305
|
+
covariance_ (lsqr, eigen, or store_covariance), scalings_ and
|
|
2306
|
+
explained_variance_ratio_ (svd, eigen), xbar_ (svd), n_features_in_.
|
|
2307
|
+
"""
|
|
2308
|
+
|
|
2309
|
+
def __init__(self, solver="svd", shrinkage=None, priors=None, n_components=None,
|
|
2310
|
+
store_covariance=False, tol=1e-4):
|
|
2311
|
+
self.solver, self.shrinkage, self.priors = solver, shrinkage, priors
|
|
2312
|
+
self.n_components, self.store_covariance, self.tol = n_components, store_covariance, tol
|
|
2313
|
+
|
|
2314
|
+
@staticmethod
|
|
2315
|
+
def _shrunk(C, s):
|
|
2316
|
+
# shrunk_covariance: (1 - s) C + s tr(C) / p I
|
|
2317
|
+
if s is None:
|
|
2318
|
+
return C
|
|
2319
|
+
p = C.shape[0]
|
|
2320
|
+
out = (1.0 - s) * C
|
|
2321
|
+
out.flat[::p + 1] += s * np.trace(C) / p
|
|
2322
|
+
return out
|
|
2323
|
+
|
|
2324
|
+
def fit(self, X, y):
|
|
2325
|
+
if self.solver not in ("svd", "lsqr", "eigen"):
|
|
2326
|
+
raise ValueError(f"solver={self.solver!r}: expected 'svd', 'lsqr' or 'eigen'")
|
|
2327
|
+
s = self.shrinkage
|
|
2328
|
+
if s is not None and (isinstance(s, str) or not 0.0 <= float(s) <= 1.0):
|
|
2329
|
+
raise ValueError(f"shrinkage={s!r}: GramLDA takes None or a float in [0, 1]")
|
|
2330
|
+
if self.solver == "svd" and s is not None:
|
|
2331
|
+
raise NotImplementedError("shrinkage not supported with 'svd' solver.")
|
|
2332
|
+
s = None if s is None else float(s)
|
|
2333
|
+
X, y = fit_xy(self, X, y, classifier=True)
|
|
2334
|
+
n, p = X.shape
|
|
2335
|
+
if n < 2:
|
|
2336
|
+
raise ValueError(f"Found array with {n} sample(s) (shape={X.shape}) while a "
|
|
2337
|
+
"minimum of 2 is required.")
|
|
2338
|
+
self.classes_, yi = classes_of(y)
|
|
2339
|
+
K = len(self.classes_)
|
|
2340
|
+
if n == K:
|
|
2341
|
+
raise ValueError("The number of samples must be more than the number of classes.")
|
|
2342
|
+
if K < 2:
|
|
2343
|
+
raise ValueError("The number of classes has to be greater than one; got 1 class")
|
|
2344
|
+
k = _gramops_kernels()
|
|
2345
|
+
lab = np.ascontiguousarray(yi, dtype=np.int32)
|
|
2346
|
+
G, means, counts = np.zeros((p, p)), np.zeros((K, p)), np.zeros(K)
|
|
2347
|
+
k.lda_moments(X, lab, G, means, counts, n, p, K)
|
|
2348
|
+
if self.priors is None:
|
|
2349
|
+
priors = counts / n
|
|
2350
|
+
else:
|
|
2351
|
+
priors = np.asarray(self.priors, dtype=np.float64)
|
|
2352
|
+
if np.any(priors < 0):
|
|
2353
|
+
raise ValueError("priors must be non-negative")
|
|
2354
|
+
if abs(priors.sum() - 1.0) > 1e-5:
|
|
2355
|
+
warnings.warn("The priors do not sum to 1. Renormalizing", UserWarning)
|
|
2356
|
+
priors = priors / priors.sum()
|
|
2357
|
+
self.priors_, self.means_ = priors, means
|
|
2358
|
+
top = min(K - 1, p)
|
|
2359
|
+
if self.n_components is not None and int(self.n_components) > top:
|
|
2360
|
+
raise ValueError("n_components cannot be larger than min(n_features, n_classes - 1).")
|
|
2361
|
+
self._max_components = top if self.n_components is None else int(self.n_components)
|
|
2362
|
+
xx = np.diagonal(G).copy()
|
|
2363
|
+
k.lda_within(G, means, counts, n, p, K) # G is now Sw, empirical priors
|
|
2364
|
+
if not np.all(np.diagonal(G) > 1e-8 * xx / n):
|
|
2365
|
+
# a column whose class means dwarf its spread: the correction
|
|
2366
|
+
# cancelled its digits, so take the centred rows instead
|
|
2367
|
+
Xc = np.ascontiguousarray(X - means[yi])
|
|
2368
|
+
G = _gram(Xc) / n
|
|
2369
|
+
Sw_emp = G
|
|
2370
|
+
|
|
2371
|
+
def class_cov():
|
|
2372
|
+
# scikit-learn's _class_cov: sum_k prior_k (shrunk) C_k; with
|
|
2373
|
+
# the empirical priors that is the shrunk Sw, else per class
|
|
2374
|
+
if self.priors is None or np.allclose(priors, counts / n):
|
|
2375
|
+
return self._shrunk(Sw_emp, s)
|
|
2376
|
+
C = np.zeros((p, p))
|
|
2377
|
+
for c in range(K):
|
|
2378
|
+
Xk = np.ascontiguousarray(X[yi == c] - means[c])
|
|
2379
|
+
C += priors[c] * self._shrunk(_gram(Xk) / max(Xk.shape[0], 1), s)
|
|
2380
|
+
return C
|
|
2381
|
+
|
|
2382
|
+
if self.solver == "svd":
|
|
2383
|
+
if self.store_covariance:
|
|
2384
|
+
self.covariance_ = class_cov()
|
|
2385
|
+
self.xbar_ = priors @ means
|
|
2386
|
+
std, V, S = np.zeros(p), np.zeros((p, p)), np.zeros(p)
|
|
2387
|
+
k.lda_svd_first(np.ascontiguousarray(Sw_emp), std, V, S, p)
|
|
2388
|
+
rank = int(np.sum(S > self.tol))
|
|
2389
|
+
scal = (V[:, :rank] / std[:, None]) / S[:rank]
|
|
2390
|
+
fac = 1.0 if K == 1 else 1.0 / (K - 1)
|
|
2391
|
+
Z = (np.sqrt(n * priors * fac) * (means - self.xbar_).T).T @ scal
|
|
2392
|
+
_, S2, Vt2 = np.linalg.svd(Z, full_matrices=False)
|
|
2393
|
+
self.explained_variance_ratio_ = (S2 ** 2 / np.sum(S2 ** 2))[:self._max_components] \
|
|
2394
|
+
if self._max_components else np.empty((0,))
|
|
2395
|
+
rank2 = int(np.sum(S2 > self.tol * S2[0]))
|
|
2396
|
+
self.scalings_ = scal @ Vt2.T[:, :rank2]
|
|
2397
|
+
coef = (means - self.xbar_) @ self.scalings_
|
|
2398
|
+
self.intercept_ = -0.5 * np.sum(coef ** 2, axis=1) + np.log(priors)
|
|
2399
|
+
self.coef_ = coef @ self.scalings_.T
|
|
2400
|
+
self.intercept_ -= self.xbar_ @ self.coef_.T
|
|
2401
|
+
else:
|
|
2402
|
+
Sw = class_cov()
|
|
2403
|
+
self.covariance_ = Sw
|
|
2404
|
+
if self.solver == "lsqr":
|
|
2405
|
+
coef = _ridge_solve(Sw, np.ascontiguousarray(means.T), 0.0)
|
|
2406
|
+
if coef is None:
|
|
2407
|
+
coef = np.linalg.lstsq(Sw, means.T, rcond=None)[0]
|
|
2408
|
+
self.coef_ = coef.T
|
|
2409
|
+
self.intercept_ = -0.5 * np.diag(means @ self.coef_.T) + np.log(priors)
|
|
2410
|
+
else:
|
|
2411
|
+
xbar = counts @ means / n
|
|
2412
|
+
St = self._shrunk(Sw_emp + (means - xbar).T @ ((counts / n)[:, None] * (means - xbar)), s)
|
|
2413
|
+
V, ev = np.zeros((p, p)), np.zeros(p)
|
|
2414
|
+
k.lda_eigen(np.ascontiguousarray(St - Sw), np.ascontiguousarray(Sw.copy()), V, ev, p)
|
|
2415
|
+
self.explained_variance_ratio_ = (ev / np.sum(ev))[:self._max_components]
|
|
2416
|
+
self.scalings_ = V
|
|
2417
|
+
self.coef_ = means @ V @ V.T
|
|
2418
|
+
self.intercept_ = -0.5 * np.diag(means @ self.coef_.T) + np.log(priors)
|
|
2419
|
+
if K == 2:
|
|
2420
|
+
self.coef_ = (self.coef_[1, :] - self.coef_[0, :]).reshape(1, -1)
|
|
2421
|
+
self.intercept_ = np.asarray(self.intercept_[1] - self.intercept_[0]).reshape(1)
|
|
2422
|
+
return self
|
|
2423
|
+
|
|
2424
|
+
def decision_function(self, X):
|
|
2425
|
+
check_is_fitted(self)
|
|
2426
|
+
d = pred_x(self, X) @ self.coef_.T + self.intercept_
|
|
2427
|
+
return d.ravel() if d.shape[1] == 1 else d
|
|
2428
|
+
|
|
2429
|
+
def predict(self, X):
|
|
2430
|
+
d = self.decision_function(X)
|
|
2431
|
+
idx = (d > 0).astype(int) if d.ndim == 1 else d.argmax(axis=1)
|
|
2432
|
+
return self.classes_[idx]
|
|
2433
|
+
|
|
2434
|
+
def predict_proba(self, X):
|
|
2435
|
+
d = self.decision_function(X)
|
|
2436
|
+
if d.ndim == 1:
|
|
2437
|
+
p1 = 1.0 / (1.0 + np.exp(-d))
|
|
2438
|
+
return np.stack([1 - p1, p1], axis=1)
|
|
2439
|
+
e = np.exp(d - d.max(axis=1, keepdims=True))
|
|
2440
|
+
return e / e.sum(axis=1, keepdims=True)
|
|
2441
|
+
|
|
2442
|
+
def predict_log_proba(self, X):
|
|
2443
|
+
P = self.predict_proba(X)
|
|
2444
|
+
with np.errstate(divide="ignore"):
|
|
2445
|
+
return np.log(P)
|
|
2446
|
+
|
|
2447
|
+
def transform(self, X):
|
|
2448
|
+
if self.solver == "lsqr":
|
|
2449
|
+
raise NotImplementedError(
|
|
2450
|
+
"transform not implemented for 'lsqr' solver (use 'svd' or 'eigen').")
|
|
2451
|
+
check_is_fitted(self)
|
|
2452
|
+
X = pred_x(self, X)
|
|
2453
|
+
Xn = (X - self.xbar_) @ self.scalings_ if self.solver == "svd" else X @ self.scalings_
|
|
2454
|
+
return Xn[:, :self._max_components]
|
|
2455
|
+
|
|
2456
|
+
|
|
2457
|
+
class GramTruncatedSVD(Tagged, TransformerMixin, BaseEstimator):
|
|
2458
|
+
"""scikit-learn's ``TruncatedSVD`` on a dense X, through the Gram
|
|
2459
|
+
matrix (``examples/kernel-only/truncated-svd.scm``): the leading
|
|
2460
|
+
``n_components`` pairs of X'X (n >= p; X'X by dsyrk) or of X X'
|
|
2461
|
+
(wide) by dsyevr over the wanted index range, the transformed data
|
|
2462
|
+
by one dgemm, svd_flip's signs (u_based_decision=False).
|
|
2463
|
+
|
|
2464
|
+
The result is the exact truncated SVD, what ``algorithm="arpack"``
|
|
2465
|
+
computes; scikit-learn's default randomized solver approximates it
|
|
2466
|
+
(5 power iterations), so against that one the components agree to
|
|
2467
|
+
its approximation error. ``algorithm``, ``n_iter``,
|
|
2468
|
+
``n_oversamples``, ``power_iteration_normalizer``, ``random_state``
|
|
2469
|
+
and ``tol`` are taken for the interface and do not change the
|
|
2470
|
+
answer.
|
|
2471
|
+
|
|
2472
|
+
Attributes after fit: components_, explained_variance_,
|
|
2473
|
+
explained_variance_ratio_, singular_values_, n_features_in_.
|
|
2474
|
+
"""
|
|
2475
|
+
|
|
2476
|
+
def __init__(self, n_components=2, *, algorithm="randomized", n_iter=5, n_oversamples=10,
|
|
2477
|
+
power_iteration_normalizer="auto", random_state=None, tol=0.0):
|
|
2478
|
+
self.n_components, self.algorithm, self.n_iter = n_components, algorithm, n_iter
|
|
2479
|
+
self.n_oversamples = n_oversamples
|
|
2480
|
+
self.power_iteration_normalizer = power_iteration_normalizer
|
|
2481
|
+
self.random_state, self.tol = random_state, tol
|
|
2482
|
+
|
|
2483
|
+
def fit(self, X, y=None):
|
|
2484
|
+
self.fit_transform(X)
|
|
2485
|
+
return self
|
|
2486
|
+
|
|
2487
|
+
def fit_transform(self, X, y=None):
|
|
2488
|
+
if self.algorithm not in ("randomized", "arpack"):
|
|
2489
|
+
raise ValueError(f"algorithm={self.algorithm!r}: expected 'randomized' or 'arpack'")
|
|
2490
|
+
X = fit_x(self, X, ensure_min_features=2)
|
|
2491
|
+
n, p = X.shape
|
|
2492
|
+
c = int(self.n_components)
|
|
2493
|
+
if not 1 <= c <= min(n, p):
|
|
2494
|
+
raise ValueError(f"n_components({c}) must be between 1 and min(n_samples, "
|
|
2495
|
+
f"n_features)={min(n, p)}.")
|
|
2496
|
+
k = _gramops_kernels()
|
|
2497
|
+
V, s, Z = np.zeros((p, c)), np.zeros(c), np.zeros((n, c))
|
|
2498
|
+
(k.tsvd_tall if n >= p else k.tsvd_wide)(X, V, s, Z, n, p, c)
|
|
2499
|
+
self.components_ = np.ascontiguousarray(V.T)
|
|
2500
|
+
self.singular_values_ = s
|
|
2501
|
+
self.explained_variance_ = np.var(Z, axis=0)
|
|
2502
|
+
full = np.var(X, axis=0).sum()
|
|
2503
|
+
self.explained_variance_ratio_ = self.explained_variance_ / full if full > 0 \
|
|
2504
|
+
else np.zeros(c)
|
|
2505
|
+
return Z
|
|
2506
|
+
|
|
2507
|
+
def transform(self, X):
|
|
2508
|
+
check_is_fitted(self)
|
|
2509
|
+
return pred_x(self, X) @ self.components_.T
|
|
2510
|
+
|
|
2511
|
+
def inverse_transform(self, X):
|
|
2512
|
+
check_is_fitted(self)
|
|
2513
|
+
return np.asarray(X) @ self.components_
|
|
2514
|
+
|
|
2515
|
+
|
|
2516
|
+
class ParallelFastICA(Tagged, TransformerMixin, BaseEstimator):
|
|
2517
|
+
"""FastICA, scikit-learn's ``FastICA`` at its defaults translated
|
|
2518
|
+
whole (``examples/kernel-only/fastica.scm``: algorithm="parallel",
|
|
2519
|
+
fun="logcosh", whiten="unit-variance", whiten_solver="svd" -- the
|
|
2520
|
+
singular vectors of X' taken from the eigenvectors of X'X, signed as
|
|
2521
|
+
scikit-learn signs them). The start is ``RandomState(random_state)
|
|
2522
|
+
.normal(size=(k, k))``, scikit-learn's draw, unless ``w_init`` is
|
|
2523
|
+
given; with the same start the fit is scikit-learn's to rounding.
|
|
2524
|
+
|
|
2525
|
+
Attributes after fit: components_, mixing_, mean_, whitening_, n_iter_.
|
|
2526
|
+
"""
|
|
2527
|
+
|
|
2528
|
+
def __init__(self, n_components=None, *, max_iter=200, tol=1e-4, w_init=None,
|
|
2529
|
+
random_state=None):
|
|
2530
|
+
self.n_components, self.max_iter, self.tol = n_components, max_iter, tol
|
|
2531
|
+
self.w_init, self.random_state = w_init, random_state
|
|
2532
|
+
|
|
2533
|
+
def fit(self, X, y=None):
|
|
2534
|
+
X = fit_x(self, X, ensure_min_samples=2)
|
|
2535
|
+
n, p = X.shape
|
|
2536
|
+
k = min(n, p) if self.n_components is None else min(int(self.n_components), n, p)
|
|
2537
|
+
if self.w_init is None:
|
|
2538
|
+
w0 = np.random.RandomState(self.random_state).normal(size=(k, k))
|
|
2539
|
+
else:
|
|
2540
|
+
w0 = np.asarray(self.w_init, dtype=np.float64)
|
|
2541
|
+
w0 = np.ascontiguousarray(w0)
|
|
2542
|
+
self.components_ = np.zeros((k, p)); self.whitening_ = np.zeros((k, p))
|
|
2543
|
+
self._unmixing = np.zeros((k, k)); self.mean_ = np.zeros(p)
|
|
2544
|
+
self.n_iter_ = _gpu_or_cpu(n * p, getattr(self, "backend", None)).fastica(
|
|
2545
|
+
X, w0, self.components_, self.whitening_, self._unmixing, self.mean_,
|
|
2546
|
+
int(self.max_iter), float(self.tol), n, p, k)
|
|
2547
|
+
self.mixing_ = np.linalg.pinv(self.components_)
|
|
2548
|
+
return self
|
|
2549
|
+
|
|
2550
|
+
def transform(self, X):
|
|
2551
|
+
return (pred_x(self, X) - self.mean_) @ self.components_.T
|
|
2552
|
+
|
|
2553
|
+
def fit_transform(self, X, y=None):
|
|
2554
|
+
return self.fit(X).transform(X)
|
|
2555
|
+
|
|
2556
|
+
|
|
2557
|
+
class CovARDRegression(Tagged, RegressorMixin, BaseEstimator):
|
|
2558
|
+
"""Automatic relevance determination, scikit-learn's ``ARDRegression``
|
|
2559
|
+
for n >= p translated whole (``examples/kernel-only/ard.scm``): each
|
|
2560
|
+
iteration sigma = pinvh(diag(lambda) + alpha G) over the kept
|
|
2561
|
+
features (the eigenproblem in the subset, scipy's cutoff), then
|
|
2562
|
+
coef, gamma, lambda, alpha and the pruning at ``threshold_lambda``,
|
|
2563
|
+
stopping on the coefficients' summed change. ``hoist=True`` (the
|
|
2564
|
+
default) takes the kept features' Gram from X'X formed once;
|
|
2565
|
+
``hoist=False`` forms it from X every iteration, as scikit-learn
|
|
2566
|
+
does. Same iterations and coefficients to rounding either way.
|
|
2567
|
+
|
|
2568
|
+
Attributes after fit: coef_, intercept_, alpha_, lambda_, sigma_,
|
|
2569
|
+
n_iter_.
|
|
2570
|
+
"""
|
|
2571
|
+
|
|
2572
|
+
def __init__(self, *, max_iter=300, tol=1e-3, alpha_1=1e-6, alpha_2=1e-6,
|
|
2573
|
+
lambda_1=1e-6, lambda_2=1e-6, threshold_lambda=1e4, fit_intercept=True,
|
|
2574
|
+
hoist=True):
|
|
2575
|
+
self.max_iter, self.tol = max_iter, tol
|
|
2576
|
+
self.alpha_1, self.alpha_2, self.lambda_1, self.lambda_2 = alpha_1, alpha_2, lambda_1, lambda_2
|
|
2577
|
+
self.threshold_lambda, self.fit_intercept, self.hoist = threshold_lambda, fit_intercept, hoist
|
|
2578
|
+
|
|
2579
|
+
def fit(self, X, y):
|
|
2580
|
+
X, y = fit_xy(self, X, y)
|
|
2581
|
+
Xc, yc, xm, ym = _center(X, y, self.fit_intercept)
|
|
2582
|
+
n, p = Xc.shape
|
|
2583
|
+
if n < p:
|
|
2584
|
+
raise ValueError(f"CovARDRegression is the n >= p route (_update_sigma); got "
|
|
2585
|
+
f"n_samples = {n} for {p} features")
|
|
2586
|
+
self.coef_ = np.zeros(p)
|
|
2587
|
+
self.lambda_ = np.zeros(p)
|
|
2588
|
+
sigma = np.zeros(p * p)
|
|
2589
|
+
out = np.zeros(2)
|
|
2590
|
+
self.n_iter_ = _gramops_kernels().ard(
|
|
2591
|
+
Xc, np.ascontiguousarray(yc), self.coef_, sigma, self.lambda_, out,
|
|
2592
|
+
int(self.max_iter), float(self.tol), float(self.alpha_1), float(self.alpha_2),
|
|
2593
|
+
float(self.lambda_1), float(self.lambda_2), float(self.threshold_lambda),
|
|
2594
|
+
1 if self.hoist else 0, n, p)
|
|
2595
|
+
k = int(out[1])
|
|
2596
|
+
self.alpha_ = float(out[0])
|
|
2597
|
+
self.sigma_ = sigma[:k * k].reshape(k, k).copy()
|
|
2598
|
+
self.intercept_ = ym - float(xm @ self.coef_)
|
|
2599
|
+
return self
|
|
2600
|
+
|
|
2601
|
+
def predict(self, X):
|
|
2602
|
+
return pred_x(self, X) @ self.coef_ + self.intercept_
|
|
2603
|
+
|
|
2604
|
+
|
|
2605
|
+
def _gpu_or_cpu(size, backend=None):
|
|
2606
|
+
"""The --cuda translation (kernels and device-resident cuBLAS) when it
|
|
2607
|
+
was built, a device answers, the backend allows it and the problem has
|
|
2608
|
+
at least a million entries; the CPU library otherwise."""
|
|
2609
|
+
import os
|
|
2610
|
+
from ._libfind import gpu_built
|
|
2611
|
+
if (backend in (None, "cuda") and size >= 1e6 and not os.environ.get("SCM2CPP_NO_DEVICE")
|
|
2612
|
+
and gpu_built() and cuda_available()):
|
|
2613
|
+
import importlib
|
|
2614
|
+
return importlib.import_module(f"{__name__}._generated._loader_gpu_cuda")
|
|
2615
|
+
return _gramops_kernels()
|
|
2616
|
+
|
|
2617
|
+
|
|
2618
|
+
def _accel(work):
|
|
2619
|
+
"""The --cublas build of the selection and Gram products, when it was
|
|
2620
|
+
built, a device answers, and WORK (flops) is large enough to pay for
|
|
2621
|
+
the transfer; else None."""
|
|
2622
|
+
import os
|
|
2623
|
+
from ._libfind import accel_built
|
|
2624
|
+
if (work < 2e9 or os.environ.get("SCM2CPP_NO_DEVICE") or not accel_built()
|
|
2625
|
+
or not cuda_available()):
|
|
2626
|
+
return None
|
|
2627
|
+
import importlib
|
|
2628
|
+
return importlib.import_module(f"{__name__}._generated._loader_accel_cublas")
|
|
2629
|
+
|
|
2630
|
+
|
|
2631
|
+
# Products under this many multiply-adds go to numpy: no BLAS threads
|
|
2632
|
+
# them, and a ctypes call to the package's library costs 10 us more than
|
|
2633
|
+
# numpy's (a 200 x 10 GramRidge took twice as long through it)
|
|
2634
|
+
_SMALL = 1e6
|
|
2635
|
+
|
|
2636
|
+
|
|
2637
|
+
def _gram(Xc, backend=None):
|
|
2638
|
+
"""X'X: gram.scm's gram-xx where the BLAS build is here (one dsyrk on
|
|
2639
|
+
every core, half the flops of numpy's X.T @ X, which is a dgemm), on
|
|
2640
|
+
the device for a large product when one answers, else numpy. With
|
|
2641
|
+
the BLAS build the device is taken by default only from p = 4000:
|
|
2642
|
+
the product reads X once, so its copy to the device is most of the
|
|
2643
|
+
device's time, and twenty cores' dsyrk was faster up to p = 2000
|
|
2644
|
+
(200,000 x 200: 0.044 s against 0.096 s)."""
|
|
2645
|
+
from ._libfind import gramops_backend
|
|
2646
|
+
n, p = Xc.shape
|
|
2647
|
+
if backend is None and float(n) * p * p < _SMALL:
|
|
2648
|
+
return np.ascontiguousarray(Xc.T @ Xc)
|
|
2649
|
+
blas = gramops_backend() == "blas"
|
|
2650
|
+
dev = backend == "cublas" or (backend is None and (not blas or p >= 4000))
|
|
2651
|
+
lib = _accel(float(n) * p * p) if dev else None
|
|
2652
|
+
if lib is None:
|
|
2653
|
+
if not blas:
|
|
2654
|
+
return np.ascontiguousarray(Xc.T @ Xc)
|
|
2655
|
+
lib = _gramops_kernels()
|
|
2656
|
+
G = np.zeros((p, p))
|
|
2657
|
+
lib.gram_xx(np.ascontiguousarray(Xc), G, n, p)
|
|
2658
|
+
return G
|
|
2659
|
+
|
|
2660
|
+
|
|
2661
|
+
def _gram_xy(Xc, y):
|
|
2662
|
+
"""X'y (y one target or several) by gram.scm's gram-xy where the BLAS
|
|
2663
|
+
build is here, so that the Gram-route fits run on one BLAS: numpy's
|
|
2664
|
+
OpenBLAS keeps its threads spinning after a call, and the next dsyrk
|
|
2665
|
+
of the other library then shared the cores with them (CovRidge at
|
|
2666
|
+
200,000 x 200 took 0.14 s for a 0.044 s product)."""
|
|
2667
|
+
from ._libfind import gramops_backend
|
|
2668
|
+
n, p = Xc.shape
|
|
2669
|
+
q = 1 if y.ndim == 1 else y.shape[1]
|
|
2670
|
+
if gramops_backend() != "blas" or float(n) * p * q < _SMALL:
|
|
2671
|
+
return Xc.T @ y
|
|
2672
|
+
c = np.zeros((p, q))
|
|
2673
|
+
_gramops_kernels().gram_xy(np.ascontiguousarray(Xc), np.ascontiguousarray(y), c, n, p, q)
|
|
2674
|
+
return c.ravel() if y.ndim == 1 else c
|
|
2675
|
+
|
|
2676
|
+
|
|
2677
|
+
def _ridge_solve(G, c, alpha):
|
|
2678
|
+
"""(G + alpha I) b = c by gram.scm's gram-ridge-solve (LAPACK's dposv in the
|
|
2679
|
+
BLAS build), c one column or several; None when the factor breaks
|
|
2680
|
+
down (alpha 0 on a singular G), for the caller's least squares. In
|
|
2681
|
+
the package's library rather than numpy's for the reason _gram_xy
|
|
2682
|
+
gives: numpy's solve between two dsyrk calls cost the next one half
|
|
2683
|
+
its speed. (Until 0.17 this was rfe.scm's Cholesky in loops, 3.7x
|
|
2684
|
+
numpy's solve at p = 200.)"""
|
|
2685
|
+
p = G.shape[0]
|
|
2686
|
+
cols = np.array(c.reshape(p, -1), dtype=np.float64, order="C")
|
|
2687
|
+
if _gramops_kernels().gram_ridge_solve(np.ascontiguousarray(G, dtype=np.float64), cols,
|
|
2688
|
+
float(alpha), p, cols.shape[1]) != 0:
|
|
2689
|
+
return None
|
|
2690
|
+
return cols.ravel() if c.ndim == 1 else cols
|
|
2691
|
+
|
|
2692
|
+
|
|
2693
|
+
def _cpu_select_lib():
|
|
2694
|
+
return _gramops_kernels()
|
|
2695
|
+
|
|
2696
|
+
|
|
2697
|
+
def f_regression(X, y, *, center=True, backend=None):
|
|
2698
|
+
"""scikit-learn's ``f_regression`` translated (``fscore.scm``): F and
|
|
2699
|
+
p-values of each feature against y. ``y`` may also be n x q, the q
|
|
2700
|
+
targets in one call (one product X'Y, on the device when one answers
|
|
2701
|
+
and the product is large) -- what a loop of scikit-learn's calls
|
|
2702
|
+
gives, column by column. Infinite F (perfect correlation) becomes
|
|
2703
|
+
the largest double and NaN F zero with p-value 1, as
|
|
2704
|
+
force_finite=True has it. p-values need scipy (else None)."""
|
|
2705
|
+
X = np.ascontiguousarray(X, dtype=np.float64)
|
|
2706
|
+
Y = np.asarray(y, dtype=np.float64)
|
|
2707
|
+
one = Y.ndim == 1
|
|
2708
|
+
Y = np.ascontiguousarray(Y.reshape(len(Y), -1))
|
|
2709
|
+
n, p = X.shape
|
|
2710
|
+
q = Y.shape[1]
|
|
2711
|
+
lib = _accel(float(n) * p * q) if backend in (None, "cublas") else None
|
|
2712
|
+
if lib is None:
|
|
2713
|
+
lib = _cpu_select_lib()
|
|
2714
|
+
r = np.zeros((p, q))
|
|
2715
|
+
F = np.zeros((p, q))
|
|
2716
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
2717
|
+
lib.f_regression(X, Y, r, F, 1 if center else 0, n, p, q)
|
|
2718
|
+
dof = n - (2 if center else 1)
|
|
2719
|
+
try:
|
|
2720
|
+
from scipy import stats
|
|
2721
|
+
pv = stats.f.sf(F, 1, dof)
|
|
2722
|
+
except ImportError:
|
|
2723
|
+
pv = None
|
|
2724
|
+
inf = np.isinf(F)
|
|
2725
|
+
F[inf] = np.finfo(np.float64).max
|
|
2726
|
+
nan = np.isnan(F)
|
|
2727
|
+
F[nan] = 0.0
|
|
2728
|
+
if pv is not None:
|
|
2729
|
+
pv[nan] = 1.0
|
|
2730
|
+
if one:
|
|
2731
|
+
return F[:, 0], (None if pv is None else pv[:, 0])
|
|
2732
|
+
return F, pv
|
|
2733
|
+
|
|
2734
|
+
|
|
2735
|
+
def f_classif(X, y):
|
|
2736
|
+
"""scikit-learn's ``f_classif`` translated (``fscore.scm``: f_oneway's
|
|
2737
|
+
sums of squares, the class sums one product). p-values need scipy."""
|
|
2738
|
+
X = np.ascontiguousarray(X, dtype=np.float64)
|
|
2739
|
+
classes, lab = np.unique(np.asarray(y), return_inverse=True)
|
|
2740
|
+
n, p = X.shape
|
|
2741
|
+
k = len(classes)
|
|
2742
|
+
F = np.zeros(p)
|
|
2743
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
2744
|
+
_cpu_select_lib().f_classif(X, lab.astype(np.int32), F, n, p, k)
|
|
2745
|
+
try:
|
|
2746
|
+
from scipy import special
|
|
2747
|
+
pv = special.fdtrc(k - 1, n - k, F)
|
|
2748
|
+
except ImportError:
|
|
2749
|
+
pv = None
|
|
2750
|
+
return F, pv
|
|
2751
|
+
|
|
2752
|
+
|
|
2753
|
+
def _clean_nans(scores):
|
|
2754
|
+
scores = np.array(scores, dtype=np.float64, copy=True)
|
|
2755
|
+
scores[np.isnan(scores)] = np.finfo(np.float64).min
|
|
2756
|
+
return scores
|
|
2757
|
+
|
|
2758
|
+
|
|
2759
|
+
class _SelectBase(Tagged, TransformerMixin, BaseEstimator):
|
|
2760
|
+
def fit(self, X, y):
|
|
2761
|
+
X, y = fit_xy(self, X, y, y_numeric=False)
|
|
2762
|
+
sf = self.score_func
|
|
2763
|
+
fn = {"f_classif": f_classif, "f_regression": f_regression}.get(sf, sf)
|
|
2764
|
+
self.scores_, self.pvalues_ = fn(X, y)
|
|
2765
|
+
return self
|
|
2766
|
+
|
|
2767
|
+
def transform(self, X):
|
|
2768
|
+
X = pred_x(self, X)
|
|
2769
|
+
return X[:, self.get_support()]
|
|
2770
|
+
|
|
2771
|
+
def fit_transform(self, X, y):
|
|
2772
|
+
return self.fit(X, y).transform(X)
|
|
2773
|
+
|
|
2774
|
+
|
|
2775
|
+
class SelectKBest(_SelectBase):
|
|
2776
|
+
"""scikit-learn's ``SelectKBest`` over the translated F statistics
|
|
2777
|
+
(``score_func`` "f_classif", "f_regression", or one of this module's
|
|
2778
|
+
functions): the k best scores by a stable sort, NaN scores last.
|
|
2779
|
+
|
|
2780
|
+
Attributes after fit: scores_, pvalues_.
|
|
2781
|
+
"""
|
|
2782
|
+
|
|
2783
|
+
def __init__(self, score_func="f_classif", *, k=10):
|
|
2784
|
+
self.score_func, self.k = score_func, k
|
|
2785
|
+
|
|
2786
|
+
def get_support(self):
|
|
2787
|
+
check_is_fitted(self)
|
|
2788
|
+
if self.k == "all":
|
|
2789
|
+
return np.ones(len(self.scores_), dtype=bool)
|
|
2790
|
+
mask = np.zeros(len(self.scores_), dtype=bool)
|
|
2791
|
+
if self.k == 0:
|
|
2792
|
+
return mask
|
|
2793
|
+
mask[np.argsort(_clean_nans(self.scores_), kind="mergesort")[-self.k:]] = True
|
|
2794
|
+
return mask
|
|
2795
|
+
|
|
2796
|
+
|
|
2797
|
+
class SelectPercentile(_SelectBase):
|
|
2798
|
+
"""scikit-learn's ``SelectPercentile`` over the translated F
|
|
2799
|
+
statistics: scores above the (100 - percentile)th percentile, ties
|
|
2800
|
+
at it kept in order up to the percentile's count.
|
|
2801
|
+
|
|
2802
|
+
Attributes after fit: scores_, pvalues_.
|
|
2803
|
+
"""
|
|
2804
|
+
|
|
2805
|
+
def __init__(self, score_func="f_classif", *, percentile=10):
|
|
2806
|
+
self.score_func, self.percentile = score_func, percentile
|
|
2807
|
+
|
|
2808
|
+
def get_support(self):
|
|
2809
|
+
check_is_fitted(self)
|
|
2810
|
+
if self.percentile == 100:
|
|
2811
|
+
return np.ones(len(self.scores_), dtype=bool)
|
|
2812
|
+
if self.percentile == 0:
|
|
2813
|
+
return np.zeros(len(self.scores_), dtype=bool)
|
|
2814
|
+
scores = _clean_nans(self.scores_)
|
|
2815
|
+
threshold = np.percentile(scores, 100 - self.percentile)
|
|
2816
|
+
mask = scores > threshold
|
|
2817
|
+
ties = np.where(scores == threshold)[0]
|
|
2818
|
+
if len(ties):
|
|
2819
|
+
max_feats = int(len(scores) * self.percentile / 100)
|
|
2820
|
+
mask[ties[: max_feats - mask.sum()]] = True
|
|
2821
|
+
return mask
|
|
2822
|
+
|
|
2823
|
+
|
|
2824
|
+
class GramRFE(Tagged, RegressorMixin, TransformerMixin, BaseEstimator):
|
|
2825
|
+
"""scikit-learn's ``RFE`` around ``Ridge(alpha)`` (``alpha=0``:
|
|
2826
|
+
``LinearRegression()``) translated whole (``rfe.scm``): the centred
|
|
2827
|
+
Gram formed once (on the device for a large one when one answers),
|
|
2828
|
+
each round's fit from its Cholesky factor with the dropped features
|
|
2829
|
+
taken out by plane rotations, the features ranked by coef^2 in a
|
|
2830
|
+
stable order. scikit-learn refits from X every round; the ranking
|
|
2831
|
+
and the final coefficients are the same to rounding.
|
|
2832
|
+
|
|
2833
|
+
Attributes after fit: support_, ranking_, n_features_, coef_,
|
|
2834
|
+
intercept_.
|
|
2835
|
+
"""
|
|
2836
|
+
|
|
2837
|
+
def __init__(self, alpha=1.0, *, n_features_to_select=None, step=1, backend=None):
|
|
2838
|
+
self.alpha, self.n_features_to_select, self.step = alpha, n_features_to_select, step
|
|
2839
|
+
self.backend = backend
|
|
2840
|
+
|
|
2841
|
+
def fit(self, X, y):
|
|
2842
|
+
X, y = fit_xy(self, X, y)
|
|
2843
|
+
n, p = X.shape
|
|
2844
|
+
k = self.n_features_to_select
|
|
2845
|
+
nsel = p // 2 if k is None else (int(k) if isinstance(k, (int, np.integer)) else int(p * k))
|
|
2846
|
+
step = int(max(1, self.step * p)) if 0.0 < self.step < 1.0 else int(self.step)
|
|
2847
|
+
lib = _accel(float(n) * p * p) if self.backend in (None, "cublas") else None
|
|
2848
|
+
if lib is None:
|
|
2849
|
+
lib = _cpu_select_lib()
|
|
2850
|
+
coef = np.zeros(p)
|
|
2851
|
+
support = np.zeros(p, dtype=np.int32)
|
|
2852
|
+
ranking = np.zeros(p, dtype=np.int32)
|
|
2853
|
+
lib.rfe(X, y, coef, support, ranking, float(self.alpha), nsel, step, n, p)
|
|
2854
|
+
self.support_ = support.astype(bool)
|
|
2855
|
+
self.ranking_ = ranking.astype(int)
|
|
2856
|
+
self.n_features_ = int(self.support_.sum())
|
|
2857
|
+
self.coef_ = coef[self.support_].copy()
|
|
2858
|
+
self.intercept_ = float(y.mean() - X.mean(axis=0)[self.support_] @ self.coef_)
|
|
2859
|
+
return self
|
|
2860
|
+
|
|
2861
|
+
def transform(self, X):
|
|
2862
|
+
return pred_x(self, X)[:, self.support_]
|
|
2863
|
+
|
|
2864
|
+
def predict(self, X):
|
|
2865
|
+
return self.transform(X) @ self.coef_ + self.intercept_
|
|
2866
|
+
|
|
2867
|
+
def fit_transform(self, X, y):
|
|
2868
|
+
return self.fit(X, y).transform(X)
|
|
2869
|
+
|
|
2870
|
+
|
|
2871
|
+
class _GramLarsBase(Tagged, RegressorMixin, BaseEstimator):
|
|
2872
|
+
def _fit_lars(self, X, y, method, alpha, max_iter):
|
|
2873
|
+
X, y = fit_xy(self, X, y)
|
|
2874
|
+
Xc, yc, xm, ym = _center(X, y, self.fit_intercept)
|
|
2875
|
+
n, p = Xc.shape
|
|
2876
|
+
G = _gram(Xc)
|
|
2877
|
+
cov = Xc.T @ yc
|
|
2878
|
+
self.coef_ = np.zeros(p)
|
|
2879
|
+
self.n_iter_ = _classifier_kernels().lars(
|
|
2880
|
+
G, cov, self.coef_, np.zeros(1), np.zeros(1), 0, float(n), float(alpha), method,
|
|
2881
|
+
int(max_iter), float(np.finfo(np.float64).eps), p)
|
|
2882
|
+
self.intercept_ = ym - float(xm @ self.coef_)
|
|
2883
|
+
return self
|
|
2884
|
+
|
|
2885
|
+
def predict(self, X):
|
|
2886
|
+
return pred_x(self, X) @ self.coef_ + self.intercept_
|
|
2887
|
+
|
|
2888
|
+
|
|
2889
|
+
class GramLars(_GramLarsBase):
|
|
2890
|
+
"""Least angle regression, scikit-learn's _lars_path_solver with a
|
|
2891
|
+
Gram translated (``examples/kernel-only/lars.scm``, method "lar").
|
|
2892
|
+
``Lars`` takes the Gram route for n > p (precompute="auto"); here it
|
|
2893
|
+
is taken always, the Gram X'X formed in numpy. ``n_nonzero_coefs``
|
|
2894
|
+
defaults to 500, as there.
|
|
2895
|
+
|
|
2896
|
+
Attributes after fit: coef_, intercept_, n_iter_.
|
|
2897
|
+
"""
|
|
2898
|
+
|
|
2899
|
+
def __init__(self, *, n_nonzero_coefs=500, fit_intercept=True):
|
|
2900
|
+
self.n_nonzero_coefs, self.fit_intercept = n_nonzero_coefs, fit_intercept
|
|
2901
|
+
|
|
2902
|
+
def fit(self, X, y):
|
|
2903
|
+
return self._fit_lars(X, y, 0, 0.0, int(self.n_nonzero_coefs))
|
|
2904
|
+
|
|
2905
|
+
|
|
2906
|
+
class GramLassoLars(_GramLarsBase):
|
|
2907
|
+
"""The lasso by least angle regression, scikit-learn's
|
|
2908
|
+
_lars_path_solver with a Gram translated (method "lasso": variables
|
|
2909
|
+
leave the active set when their coefficient crosses zero, the
|
|
2910
|
+
Cholesky factor downdated by Givens rotations). ``LassoLars`` takes
|
|
2911
|
+
the Gram route for n > p; here it is taken always. ``alpha``
|
|
2912
|
+
defaults to 1.0 and ``max_iter`` to 500, as there.
|
|
2913
|
+
|
|
2914
|
+
Attributes after fit: coef_, intercept_, n_iter_.
|
|
2915
|
+
"""
|
|
2916
|
+
|
|
2917
|
+
def __init__(self, alpha=1.0, *, max_iter=500, fit_intercept=True):
|
|
2918
|
+
self.alpha, self.max_iter, self.fit_intercept = alpha, max_iter, fit_intercept
|
|
2919
|
+
|
|
2920
|
+
def fit(self, X, y):
|
|
2921
|
+
return self._fit_lars(X, y, 1, float(self.alpha), int(self.max_iter))
|
|
2922
|
+
|
|
2923
|
+
|
|
2924
|
+
class _GramLarsCVBase(Tagged, RegressorMixin, BaseEstimator):
|
|
2925
|
+
"""LarsCV / LassoLarsCV: KFold(cv) without shuffling; on each fold
|
|
2926
|
+
the whole path by the translated LARS over the training fold's Gram
|
|
2927
|
+
(centred on the training means), the test residuals interpolated
|
|
2928
|
+
onto the union of the folds' alphas as scikit-learn interpolates
|
|
2929
|
+
them (the path's coefficients interpolated, the residuals being
|
|
2930
|
+
affine in them, the mean square from the test fold's Gram); the
|
|
2931
|
+
alpha of the least mean square error, refitted on all the data."""
|
|
2932
|
+
|
|
2933
|
+
def __init__(self, *, fit_intercept=True, max_iter=500, cv=5, max_n_alphas=1000):
|
|
2934
|
+
self.fit_intercept, self.max_iter, self.cv = fit_intercept, max_iter, cv
|
|
2935
|
+
self.max_n_alphas = max_n_alphas
|
|
2936
|
+
|
|
2937
|
+
def _lars(self, Xc, yc, alpha_min, rec):
|
|
2938
|
+
n, p = Xc.shape
|
|
2939
|
+
G = _gram(Xc)
|
|
2940
|
+
cov = Xc.T @ yc
|
|
2941
|
+
coef = np.zeros(p)
|
|
2942
|
+
mi = int(self.max_iter)
|
|
2943
|
+
path = np.zeros((mi + 1, p)) if rec else np.zeros(1)
|
|
2944
|
+
alphas = np.zeros(mi + 1) if rec else np.zeros(1)
|
|
2945
|
+
it = _classifier_kernels().lars(G, cov, coef, path, alphas, 1 if rec else 0, float(n),
|
|
2946
|
+
float(alpha_min), self._method, mi,
|
|
2947
|
+
float(np.finfo(np.float64).eps), p)
|
|
2948
|
+
return it, coef, (path[:it + 1] if rec else None), (alphas[:it + 1] if rec else None)
|
|
2949
|
+
|
|
2950
|
+
def fit(self, X, y):
|
|
2951
|
+
X, y = fit_xy(self, X, y)
|
|
2952
|
+
n, p = X.shape
|
|
2953
|
+
if n < int(self.cv):
|
|
2954
|
+
raise ValueError(f"Cannot have number of splits n_splits={self.cv} greater than "
|
|
2955
|
+
f"the number of samples: n_samples={n}.")
|
|
2956
|
+
folds = []
|
|
2957
|
+
for train, test in _kfold(n, int(self.cv)):
|
|
2958
|
+
Xtr, ytr, Xte, yte = X[train], y[train], X[test], y[test]
|
|
2959
|
+
if self.fit_intercept:
|
|
2960
|
+
xm, ym = Xtr.mean(axis=0), ytr.mean()
|
|
2961
|
+
Xtr, Xte, ytr, yte = Xtr - xm, Xte - xm, ytr - ym, yte - ym
|
|
2962
|
+
_, _, path, alphas = self._lars(np.ascontiguousarray(Xtr), ytr, 0.0, True)
|
|
2963
|
+
folds.append((alphas, path, _gram(Xte), Xte.T @ yte, float(yte @ yte), len(test)))
|
|
2964
|
+
all_alphas = np.unique(np.concatenate([f[0] for f in folds]))
|
|
2965
|
+
stride = int(max(1, int(len(all_alphas) / float(self.max_n_alphas))))
|
|
2966
|
+
all_alphas = all_alphas[::stride]
|
|
2967
|
+
mse = np.empty((len(all_alphas), len(folds)))
|
|
2968
|
+
for j, (alphas, path, Gt, cyt, yy, nt) in enumerate(folds):
|
|
2969
|
+
a, c = alphas[::-1], path[::-1]
|
|
2970
|
+
if a[0] != 0:
|
|
2971
|
+
a, c = np.r_[0, a], np.r_[c[:1], c]
|
|
2972
|
+
if a[-1] != all_alphas[-1]:
|
|
2973
|
+
a, c = np.r_[a, all_alphas[-1]], np.r_[c, c[-1:]]
|
|
2974
|
+
ci = np.column_stack([np.interp(all_alphas, a, c[:, q]) for q in range(p)])
|
|
2975
|
+
mse[:, j] = (((ci @ Gt) * ci).sum(axis=1) - 2.0 * ci @ cyt + yy) / nt
|
|
2976
|
+
mask = np.all(np.isfinite(mse), axis=-1)
|
|
2977
|
+
all_alphas, mse = all_alphas[mask], mse[mask]
|
|
2978
|
+
self.alpha_ = float(all_alphas[int(np.argmin(mse.mean(axis=-1)))])
|
|
2979
|
+
self.cv_alphas_, self.mse_path_ = all_alphas, mse
|
|
2980
|
+
Xc, yc, xm, ym = _center(X, y, self.fit_intercept)
|
|
2981
|
+
self.n_iter_, self.coef_, _, _ = self._lars(Xc, yc, self.alpha_, False)
|
|
2982
|
+
self.intercept_ = ym - float(xm @ self.coef_)
|
|
2983
|
+
return self
|
|
2984
|
+
|
|
2985
|
+
def predict(self, X):
|
|
2986
|
+
return pred_x(self, X) @ self.coef_ + self.intercept_
|
|
2987
|
+
|
|
2988
|
+
|
|
2989
|
+
class GramLarsCV(_GramLarsCVBase):
|
|
2990
|
+
"""scikit-learn's ``LarsCV`` (method "lar") over the translated LARS;
|
|
2991
|
+
see _GramLarsCVBase. Attributes after fit: alpha_, coef_, intercept_,
|
|
2992
|
+
cv_alphas_, mse_path_, n_iter_."""
|
|
2993
|
+
_method = 0
|
|
2994
|
+
|
|
2995
|
+
|
|
2996
|
+
class GramLassoLarsCV(_GramLarsCVBase):
|
|
2997
|
+
"""scikit-learn's ``LassoLarsCV`` (method "lasso") over the translated
|
|
2998
|
+
LARS; see _GramLarsCVBase. Attributes after fit: alpha_, coef_,
|
|
2999
|
+
intercept_, cv_alphas_, mse_path_, n_iter_."""
|
|
3000
|
+
_method = 1
|
|
3001
|
+
|
|
3002
|
+
|
|
3003
|
+
class GramOMPCV(Tagged, RegressorMixin, BaseEstimator):
|
|
3004
|
+
"""scikit-learn's ``OrthogonalMatchingPursuitCV``: KFold(cv) without
|
|
3005
|
+
shuffling; on each fold the path of the translated OMP (``omp.scm``,
|
|
3006
|
+
the path recorded) up to ``max_iter`` columns on the centred training
|
|
3007
|
+
fold, the test fold's mean square error after each column, the number
|
|
3008
|
+
of columns that minimises the folds' mean (over the shortest path),
|
|
3009
|
+
refitted as GramOMP. ``max_iter`` defaults, as there, to
|
|
3010
|
+
min(max(10% of p, 5), p).
|
|
3011
|
+
|
|
3012
|
+
Attributes after fit: n_nonzero_coefs_, coef_, intercept_, n_iter_.
|
|
3013
|
+
"""
|
|
3014
|
+
|
|
3015
|
+
def __init__(self, *, fit_intercept=True, max_iter=None, cv=5):
|
|
3016
|
+
self.fit_intercept, self.max_iter, self.cv = fit_intercept, max_iter, cv
|
|
3017
|
+
|
|
3018
|
+
def fit(self, X, y):
|
|
3019
|
+
X, y = fit_xy(self, X, y)
|
|
3020
|
+
n, p = X.shape
|
|
3021
|
+
if n < int(self.cv):
|
|
3022
|
+
raise ValueError(f"Cannot have number of splits n_splits={self.cv} greater than "
|
|
3023
|
+
f"the number of samples: n_samples={n}.")
|
|
3024
|
+
kmax = min(max(int(0.1 * p), 5), p) if self.max_iter is None else int(self.max_iter)
|
|
3025
|
+
lib = _gramops_kernels()
|
|
3026
|
+
mses = []
|
|
3027
|
+
for train, test in _kfold(n, int(self.cv)):
|
|
3028
|
+
Xtr, ytr, Xte, yte = X[train], y[train], X[test], y[test]
|
|
3029
|
+
if self.fit_intercept:
|
|
3030
|
+
xm, ym = Xtr.mean(axis=0), ytr.mean()
|
|
3031
|
+
Xtr, Xte, ytr, yte = Xtr - xm, Xte - xm, ytr - ym, yte - ym
|
|
3032
|
+
path = np.zeros((kmax, p))
|
|
3033
|
+
k = lib.omp(np.ascontiguousarray(Xtr), np.ascontiguousarray(ytr), np.zeros(p),
|
|
3034
|
+
path, 1, kmax, len(train), p)
|
|
3035
|
+
res = Xte @ path[:k].T - yte[:, None]
|
|
3036
|
+
mses.append((res ** 2).mean(axis=0))
|
|
3037
|
+
m = min(len(f) for f in mses)
|
|
3038
|
+
best = int(np.argmin(np.mean([f[:m] for f in mses], axis=0))) + 1
|
|
3039
|
+
self.n_nonzero_coefs_ = best
|
|
3040
|
+
Xc, yc, xm, ym = _center(X, y, self.fit_intercept)
|
|
3041
|
+
self.coef_ = np.zeros(p)
|
|
3042
|
+
self.n_iter_ = lib.omp(Xc, yc, self.coef_, np.zeros(1), 0, best, n, p)
|
|
3043
|
+
self.intercept_ = ym - float(xm @ self.coef_)
|
|
3044
|
+
return self
|
|
3045
|
+
|
|
3046
|
+
def predict(self, X):
|
|
3047
|
+
return pred_x(self, X) @ self.coef_ + self.intercept_
|
|
3048
|
+
|
|
3049
|
+
|
|
3050
|
+
class GramLassoLarsIC(Tagged, RegressorMixin, BaseEstimator):
|
|
3051
|
+
"""LassoLars with the penalty chosen by AIC or BIC, scikit-learn's
|
|
3052
|
+
``LassoLarsIC``: the whole "lasso" path by the translated
|
|
3053
|
+
_lars_path_solver (``examples/kernel-only/lars.scm``, the path
|
|
3054
|
+
recorded as lars_path(return_path=True) returns it), then
|
|
3055
|
+
scikit-learn's criterion n log(2 pi s^2) + RSS / s^2 + K df at
|
|
3056
|
+
each knot, s^2 the OLS noise variance RSS_ols / (n - p - 1). The
|
|
3057
|
+
residual sums are taken from the Gram (|y - X c|^2 = y'y - 2 c'X'y
|
|
3058
|
+
+ c'X'Xc, the OLS fit by its Cholesky factor) rather than from X
|
|
3059
|
+
at every knot as scikit-learn takes them -- the same numbers to
|
|
3060
|
+
rounding, without the n x knots product. n > p + 1, as there.
|
|
3061
|
+
|
|
3062
|
+
Attributes after fit: alpha_, coef_, intercept_, alphas_, criterion_,
|
|
3063
|
+
noise_variance_, n_iter_.
|
|
3064
|
+
"""
|
|
3065
|
+
|
|
3066
|
+
def __init__(self, criterion="aic", *, fit_intercept=True, max_iter=500,
|
|
3067
|
+
noise_variance=None):
|
|
3068
|
+
self.criterion, self.fit_intercept, self.max_iter = criterion, fit_intercept, max_iter
|
|
3069
|
+
self.noise_variance = noise_variance
|
|
3070
|
+
|
|
3071
|
+
def fit(self, X, y):
|
|
3072
|
+
X, y = fit_xy(self, X, y)
|
|
3073
|
+
Xc, yc, xm, ym = _center(X, y, self.fit_intercept)
|
|
3074
|
+
n, p = Xc.shape
|
|
3075
|
+
mi = int(self.max_iter)
|
|
3076
|
+
G0 = _gram(Xc)
|
|
3077
|
+
cov0 = Xc.T @ yc
|
|
3078
|
+
yy = float(yc @ yc)
|
|
3079
|
+
path = np.zeros((mi + 1, p))
|
|
3080
|
+
alphas = np.zeros(mi + 1)
|
|
3081
|
+
coef = np.zeros(p)
|
|
3082
|
+
it = _classifier_kernels().lars(
|
|
3083
|
+
G0.copy(), cov0.copy(), coef, path, alphas, 1, float(n), 0.0, 1, mi,
|
|
3084
|
+
float(np.finfo(np.float64).eps), p)
|
|
3085
|
+
cp, al = path[:it + 1], alphas[:it + 1]
|
|
3086
|
+
rss = yy - 2.0 * (cp @ cov0) + ((cp @ G0) * cp).sum(axis=1)
|
|
3087
|
+
df = (np.abs(cp) > np.finfo(np.float64).eps).sum(axis=1)
|
|
3088
|
+
if self.noise_variance is None:
|
|
3089
|
+
if n <= p + int(self.fit_intercept):
|
|
3090
|
+
raise ValueError(f"n must exceed p (+1 with an intercept) to estimate the "
|
|
3091
|
+
f"noise variance; got n_samples = {n} for {p} features; "
|
|
3092
|
+
"pass noise_variance")
|
|
3093
|
+
L = np.linalg.cholesky(G0)
|
|
3094
|
+
beta = np.linalg.solve(L.T, np.linalg.solve(L, cov0))
|
|
3095
|
+
self.noise_variance_ = (yy - 2.0 * beta @ cov0 + beta @ G0 @ beta) / (
|
|
3096
|
+
n - p - int(self.fit_intercept))
|
|
3097
|
+
else:
|
|
3098
|
+
self.noise_variance_ = float(self.noise_variance)
|
|
3099
|
+
factor = {"aic": 2.0, "bic": np.log(n)}[self.criterion]
|
|
3100
|
+
self.criterion_ = (n * np.log(2 * np.pi * self.noise_variance_)
|
|
3101
|
+
+ rss / self.noise_variance_ + factor * df)
|
|
3102
|
+
best = int(np.argmin(self.criterion_))
|
|
3103
|
+
self.alphas_, self.n_iter_ = al, it
|
|
3104
|
+
self.alpha_ = float(al[best])
|
|
3105
|
+
self.coef_ = cp[best].copy()
|
|
3106
|
+
self.intercept_ = ym - float(xm @ self.coef_)
|
|
3107
|
+
return self
|
|
3108
|
+
|
|
3109
|
+
def predict(self, X):
|
|
3110
|
+
return pred_x(self, X) @ self.coef_ + self.intercept_
|
|
3111
|
+
|
|
3112
|
+
|
|
3113
|
+
class WeiszfeldTheilSen(Tagged, RegressorMixin, BaseEstimator):
|
|
3114
|
+
"""scikit-learn's ``TheilSenRegressor`` translated whole
|
|
3115
|
+
(``examples/kernel-only/theilsen.scm``): the least squares fit of
|
|
3116
|
+
every subset of n_subsamples rows (p + 1 by default: each an exact
|
|
3117
|
+
solve, the minimum-norm fit for a singular one, as gelss gives it)
|
|
3118
|
+
and the spatial median of the fits by the modified Weiszfeld
|
|
3119
|
+
iteration. The subsets are scikit-learn's: all the combinations
|
|
3120
|
+
when there are at most ``max_subpopulation``, else
|
|
3121
|
+
``RandomState(random_state).choice`` draws, made here the same way
|
|
3122
|
+
(and on large problems that drawing is most of the time, on both
|
|
3123
|
+
sides).
|
|
3124
|
+
|
|
3125
|
+
Attributes after fit: coef_, intercept_, n_iter_, n_subpopulation_,
|
|
3126
|
+
breakdown_.
|
|
3127
|
+
"""
|
|
3128
|
+
|
|
3129
|
+
def __init__(self, *, fit_intercept=True, max_subpopulation=1e4, n_subsamples=None,
|
|
3130
|
+
max_iter=300, tol=1e-3, random_state=None):
|
|
3131
|
+
self.fit_intercept, self.max_subpopulation = fit_intercept, max_subpopulation
|
|
3132
|
+
self.n_subsamples, self.max_iter, self.tol = n_subsamples, max_iter, tol
|
|
3133
|
+
self.random_state = random_state
|
|
3134
|
+
|
|
3135
|
+
def fit(self, X, y):
|
|
3136
|
+
from itertools import combinations
|
|
3137
|
+
from math import comb
|
|
3138
|
+
X, y = fit_xy(self, X, y)
|
|
3139
|
+
n, p = X.shape
|
|
3140
|
+
fi = 1 if self.fit_intercept else 0
|
|
3141
|
+
ns = min(p + fi, n) if self.n_subsamples is None else int(self.n_subsamples)
|
|
3142
|
+
total = max(1, comb(n, ns))
|
|
3143
|
+
self.n_subpopulation_ = int(min(self.max_subpopulation, total))
|
|
3144
|
+
self.breakdown_ = 1 - (0.5 ** (1 / ns) * (n - ns + 1) + ns - 1) / n
|
|
3145
|
+
if total <= self.max_subpopulation:
|
|
3146
|
+
idx = np.array(list(combinations(range(n), ns)), dtype=np.int32)
|
|
3147
|
+
else:
|
|
3148
|
+
rs = np.random.RandomState(self.random_state)
|
|
3149
|
+
idx = np.array([rs.choice(n, size=ns, replace=False)
|
|
3150
|
+
for _ in range(self.n_subpopulation_)], dtype=np.int32)
|
|
3151
|
+
nsub = len(idx)
|
|
3152
|
+
w = np.zeros((nsub, p + fi))
|
|
3153
|
+
c = np.zeros(p + fi)
|
|
3154
|
+
self.n_iter_ = _classifier_kernels().theilsen(X, y, idx, w, c, nsub, ns, fi,
|
|
3155
|
+
int(self.max_iter), float(self.tol), p)
|
|
3156
|
+
self.intercept_ = float(c[0]) if fi else 0.0
|
|
3157
|
+
self.coef_ = c[fi:].copy()
|
|
3158
|
+
return self
|
|
3159
|
+
|
|
3160
|
+
def predict(self, X):
|
|
3161
|
+
return pred_x(self, X) @ self.coef_ + self.intercept_
|
|
3162
|
+
|
|
3163
|
+
|
|
3164
|
+
class L1LinearSVM(Tagged, ClassifierMixin, BaseEstimator):
|
|
3165
|
+
"""The L1-regularized L2-loss linear SVM by liblinear's own
|
|
3166
|
+
algorithm: ``examples/kernel-only/svm-l1.scm`` translated plainly,
|
|
3167
|
+
liblinear's solve_l1r_l2_svc (coordinate descent with a Newton step,
|
|
3168
|
+
a line search and shrinking) -- what scikit-learn's
|
|
3169
|
+
``LinearSVC(penalty="l1", dual=False)`` runs, on
|
|
3170
|
+
|
|
3171
|
+
min_w sum_j |w_j| + C sum_i max(0, 1 - y_i w.x_i)^2,
|
|
3172
|
+
|
|
3173
|
+
the intercept a ones column (``intercept_scaling``) penalised with
|
|
3174
|
+
the rest, as liblinear has it. The coordinate order is random from
|
|
3175
|
+
a different generator than liblinear's, so the fit agrees with
|
|
3176
|
+
scikit-learn's to its tolerance. Two classes.
|
|
3177
|
+
|
|
3178
|
+
Attributes after fit: classes_, coef_, intercept_, n_iter_.
|
|
3179
|
+
"""
|
|
3180
|
+
|
|
3181
|
+
def __init__(self, C=1.0, tol=1e-4, max_iter=1000, fit_intercept=True,
|
|
3182
|
+
intercept_scaling=1.0, random_state=0):
|
|
3183
|
+
self.C, self.tol, self.max_iter = C, tol, max_iter
|
|
3184
|
+
self.fit_intercept, self.intercept_scaling = fit_intercept, intercept_scaling
|
|
3185
|
+
self.random_state = random_state
|
|
3186
|
+
|
|
3187
|
+
_binary_only = True
|
|
3188
|
+
|
|
3189
|
+
def fit(self, X, y):
|
|
3190
|
+
X, y = fit_xy(self, X, y, classifier=True)
|
|
3191
|
+
n, p = X.shape
|
|
3192
|
+
self.classes_, ys = _binary_labels(y)
|
|
3193
|
+
scaling = float(self.intercept_scaling)
|
|
3194
|
+
if self.fit_intercept:
|
|
3195
|
+
X = np.hstack([X, np.full((n, 1), scaling)])
|
|
3196
|
+
q = X.shape[1]
|
|
3197
|
+
w = np.zeros(q)
|
|
3198
|
+
# liblinear walks the design by columns
|
|
3199
|
+
self.n_iter_ = _classifier_kernels().svm_l1(
|
|
3200
|
+
np.ascontiguousarray(X.T), ys, w, float(self.C), float(self.tol), int(self.max_iter),
|
|
3201
|
+
_seed(self.random_state), n, q)
|
|
3202
|
+
self.coef_ = w[:p].copy()
|
|
3203
|
+
self.intercept_ = float(w[p] * scaling) if self.fit_intercept else 0.0
|
|
3204
|
+
return self
|
|
3205
|
+
|
|
3206
|
+
def decision_function(self, X):
|
|
3207
|
+
return pred_x(self, X) @ self.coef_ + self.intercept_
|
|
3208
|
+
|
|
3209
|
+
def predict(self, X):
|
|
3210
|
+
z = self.decision_function(X)
|
|
3211
|
+
return self.classes_[(z > 0).astype(int)]
|
|
3212
|
+
|
|
3213
|
+
|
|
3214
|
+
class L1LogisticRegression(Tagged, ClassifierMixin, BaseEstimator):
|
|
3215
|
+
"""L1-regularized logistic regression by liblinear's own algorithm.
|
|
3216
|
+
|
|
3217
|
+
``examples/kernel-only/logistic-l1.scm`` translated plainly:
|
|
3218
|
+
liblinear's solve_l1r_lr (newGLMNET: Newton iterations over a
|
|
3219
|
+
coordinate-descent subproblem with random order and shrinking, and
|
|
3220
|
+
a line search) -- what scikit-learn's
|
|
3221
|
+
``LogisticRegression(l1_ratio=1, solver="liblinear",
|
|
3222
|
+
fit_intercept=False)`` runs, on the same problem
|
|
3223
|
+
|
|
3224
|
+
min_w sum_j |w_j| + C sum_i log(1 + exp(-y_i w.x_i)).
|
|
3225
|
+
|
|
3226
|
+
``tol`` and ``max_iter`` are LogisticRegression's (the tolerance is
|
|
3227
|
+
scaled as liblinear scales it; the cap counts Newton iterations).
|
|
3228
|
+
No intercept; two classes. ``CovLogistic`` solves the same problem
|
|
3229
|
+
by majorization over the Gram matrix instead.
|
|
3230
|
+
|
|
3231
|
+
Attributes after fit: classes_, coef_, n_iter_.
|
|
3232
|
+
"""
|
|
3233
|
+
|
|
3234
|
+
_binary_only = True
|
|
3235
|
+
|
|
3236
|
+
def __init__(self, C=1.0, tol=1e-4, max_iter=100, random_state=0):
|
|
3237
|
+
self.C, self.tol, self.max_iter = C, tol, max_iter
|
|
3238
|
+
self.random_state = random_state
|
|
3239
|
+
|
|
3240
|
+
def fit(self, X, y):
|
|
3241
|
+
X, y = fit_xy(self, X, y, classifier=True)
|
|
3242
|
+
n, p = X.shape
|
|
3243
|
+
self.classes_, ys = _binary_labels(y)
|
|
3244
|
+
w = np.zeros(p)
|
|
3245
|
+
# liblinear walks the design by columns
|
|
3246
|
+
self.n_iter_ = _classifier_kernels().logistic_l1(
|
|
3247
|
+
np.ascontiguousarray(X.T), ys, w, float(self.C), float(self.tol), int(self.max_iter),
|
|
3248
|
+
_seed(self.random_state), n, p)
|
|
3249
|
+
self.coef_ = w
|
|
3250
|
+
self.intercept_ = 0.0
|
|
3251
|
+
return self
|
|
3252
|
+
|
|
3253
|
+
def decision_function(self, X):
|
|
3254
|
+
return pred_x(self, X) @ self.coef_
|
|
3255
|
+
|
|
3256
|
+
def predict_proba(self, X):
|
|
3257
|
+
p1 = 1.0 / (1.0 + np.exp(-self.decision_function(X)))
|
|
3258
|
+
return np.column_stack([1.0 - p1, p1])
|
|
3259
|
+
|
|
3260
|
+
def predict(self, X):
|
|
3261
|
+
z = self.decision_function(X)
|
|
3262
|
+
return self.classes_[(z > 0).astype(int)]
|
|
3263
|
+
|
|
3264
|
+
|
|
3265
|
+
class CovLogistic:
|
|
3266
|
+
"""L1-penalized logistic regression, by majorization over the Gram.
|
|
3267
|
+
|
|
3268
|
+
The objective is
|
|
3269
|
+
|
|
3270
|
+
(1/n) sum_i [log(1 + exp(eta_i)) - y_i eta_i] + lam ||b||_1
|
|
3271
|
+
|
|
3272
|
+
with eta = X b and y in {0,1} -- scikit-learn's ``LogisticRegression``
|
|
3273
|
+
with ``penalty="l1"``, ``fit_intercept=False`` and ``C = 1/(n lam)``.
|
|
3274
|
+
|
|
3275
|
+
The logistic Hessian is bounded by X'X / 4 (Bohning's bound), so
|
|
3276
|
+
instead of reweighting the Gram matrix every round -- which would
|
|
3277
|
+
cost O(n p^2) each time and break the design-free construction --
|
|
3278
|
+
the quadratic term is fixed at G/4 once and only the gradient
|
|
3279
|
+
moves. Each outer round costs one pass for eta and one for
|
|
3280
|
+
X'(y - mu), then hands the majorizer to the same coordinate
|
|
3281
|
+
descent the lasso uses: warm-started, exactly resumable, and with
|
|
3282
|
+
the pleasant identity that the working correlations are exactly
|
|
3283
|
+
X'(y - mu). Majorization descends monotonically to the optimum
|
|
3284
|
+
of this convex problem.
|
|
3285
|
+
"""
|
|
3286
|
+
|
|
3287
|
+
def __init__(self, X, y):
|
|
3288
|
+
X = np.ascontiguousarray(X, dtype=np.float64)
|
|
3289
|
+
y = np.ascontiguousarray(y, dtype=np.float64)
|
|
3290
|
+
if X.ndim != 2 or y.ndim != 1 or X.shape[0] != y.size:
|
|
3291
|
+
raise ValueError(
|
|
3292
|
+
f"X is {X.shape} and y is {y.shape}; expected (n, p) "
|
|
3293
|
+
"and (n,)")
|
|
3294
|
+
if not np.all((y == 0.0) | (y == 1.0)):
|
|
3295
|
+
raise ValueError("y must be 0/1")
|
|
3296
|
+
self.X, self.y = X, y
|
|
3297
|
+
self.nobs, self.p = X.shape
|
|
3298
|
+
self.g4 = np.ascontiguousarray((X.T @ X).ravel() / 4.0)
|
|
3299
|
+
|
|
3300
|
+
def _grad_corr(self, beta):
|
|
3301
|
+
mu = 1.0 / (1.0 + np.exp(-(self.X @ beta)))
|
|
3302
|
+
return self.X.T @ (self.y - mu)
|
|
3303
|
+
|
|
3304
|
+
def lambda_max(self):
|
|
3305
|
+
"""The smallest penalty keeping every coefficient at zero.
|
|
3306
|
+
|
|
3307
|
+
At beta = 0 the gradient is X'(y - 1/2); the penalty must beat
|
|
3308
|
+
its largest entry, scaled as the objective is.
|
|
3309
|
+
"""
|
|
3310
|
+
return float(np.max(np.abs(self.X.T @ (self.y - 0.5)))) / self.nobs
|
|
3311
|
+
|
|
3312
|
+
def lambda_grid(self, num=50, eps=1e-2):
|
|
3313
|
+
"""A log-spaced descending path from lambda_max."""
|
|
3314
|
+
return self.lambda_max() * np.logspace(0, np.log10(eps), int(num))
|
|
3315
|
+
|
|
3316
|
+
def fit_path(self, lambdas, tol=1e-7, inner_sweeps=200,
|
|
3317
|
+
max_rounds=500):
|
|
3318
|
+
"""Coefficients per lambda, warm-started down the path."""
|
|
3319
|
+
lambdas = np.asarray(lambdas, dtype=np.float64)
|
|
3320
|
+
beta = np.zeros(self.p)
|
|
3321
|
+
out = np.empty((lambdas.size, self.p))
|
|
3322
|
+
for i, lam in enumerate(lambdas):
|
|
3323
|
+
beta = self._fit_one(beta, float(lam), tol, inner_sweeps,
|
|
3324
|
+
max_rounds)
|
|
3325
|
+
out[i] = beta
|
|
3326
|
+
return out
|
|
3327
|
+
|
|
3328
|
+
def fit(self, lam, **kw):
|
|
3329
|
+
"""Coefficients at one lambda, from zero."""
|
|
3330
|
+
return self._fit_one(np.zeros(self.p), float(lam),
|
|
3331
|
+
kw.get("tol", 1e-7),
|
|
3332
|
+
kw.get("inner_sweeps", 200),
|
|
3333
|
+
kw.get("max_rounds", 500))
|
|
3334
|
+
|
|
3335
|
+
def _fit_one(self, beta, lam, tol, inner_sweeps, max_rounds):
|
|
3336
|
+
beta = beta.copy()
|
|
3337
|
+
self._rounds = 0
|
|
3338
|
+
for _ in range(max_rounds):
|
|
3339
|
+
self._rounds += 1
|
|
3340
|
+
prev = beta.copy()
|
|
3341
|
+
# the majorizer at beta: quadratic G/4, working
|
|
3342
|
+
# correlations exactly X'(y - mu)
|
|
3343
|
+
c = np.ascontiguousarray(self._grad_corr(beta))
|
|
3344
|
+
kernel.cov_descend(self.g4, c, beta, lam, inner_sweeps,
|
|
3345
|
+
float(self.nobs), self.p)
|
|
3346
|
+
if np.max(np.abs(beta - prev)) < tol * max(
|
|
3347
|
+
1.0, float(np.max(np.abs(beta)))):
|
|
3348
|
+
break
|
|
3349
|
+
return beta
|
|
3350
|
+
|
|
3351
|
+
|
|
3352
|
+
class GramLogisticRegression(Tagged, ClassifierMixin, BaseEstimator):
|
|
3353
|
+
"""scikit-learn's ``LogisticRegression(l1_ratio=1, solver="liblinear")``
|
|
3354
|
+
interface over CovLogistic: the same problem,
|
|
3355
|
+
|
|
3356
|
+
min_w ||w||_1 + C sum_i log(1 + exp(-y_i (w.x_i + b))),
|
|
3357
|
+
|
|
3358
|
+
solved by majorization over the Gram matrix instead of liblinear's
|
|
3359
|
+
Newton steps. As in liblinear the intercept is the weight of a
|
|
3360
|
+
constant column of value ``intercept_scaling`` and is penalized
|
|
3361
|
+
with the rest. ``tol`` is CovLogistic's (the largest coefficient
|
|
3362
|
+
move in a round, relative to their size) and ``max_iter`` caps the
|
|
3363
|
+
rounds. Two classes.
|
|
3364
|
+
|
|
3365
|
+
Attributes after fit: classes_, coef_, intercept_, n_iter_.
|
|
3366
|
+
"""
|
|
3367
|
+
|
|
3368
|
+
_binary_only = True
|
|
3369
|
+
|
|
3370
|
+
def __init__(self, C=1.0, *, fit_intercept=True, intercept_scaling=1.0, tol=1e-7,
|
|
3371
|
+
max_iter=500):
|
|
3372
|
+
self.C, self.fit_intercept, self.intercept_scaling = C, fit_intercept, intercept_scaling
|
|
3373
|
+
self.tol, self.max_iter = tol, max_iter
|
|
3374
|
+
|
|
3375
|
+
def fit(self, X, y):
|
|
3376
|
+
X, y = fit_xy(self, X, y, classifier=True)
|
|
3377
|
+
n = X.shape[0]
|
|
3378
|
+
self.classes_, ys = _binary_labels(y)
|
|
3379
|
+
s = float(self.intercept_scaling)
|
|
3380
|
+
if self.fit_intercept:
|
|
3381
|
+
X = np.column_stack([X, np.full(n, s)])
|
|
3382
|
+
m = CovLogistic(X, (ys > 0).astype(np.float64))
|
|
3383
|
+
lam = 1.0 / (n * float(self.C))
|
|
3384
|
+
beta = m._fit_one(np.zeros(m.p), lam, float(self.tol), 200, int(self.max_iter))
|
|
3385
|
+
self.n_iter_ = m._rounds
|
|
3386
|
+
if self.fit_intercept:
|
|
3387
|
+
self.coef_, self.intercept_ = beta[:-1].copy(), float(beta[-1]) * s
|
|
3388
|
+
else:
|
|
3389
|
+
self.coef_, self.intercept_ = beta, 0.0
|
|
3390
|
+
return self
|
|
3391
|
+
|
|
3392
|
+
def decision_function(self, X):
|
|
3393
|
+
return pred_x(self, X) @ self.coef_ + self.intercept_
|
|
3394
|
+
|
|
3395
|
+
def predict_proba(self, X):
|
|
3396
|
+
p1 = 1.0 / (1.0 + np.exp(-self.decision_function(X)))
|
|
3397
|
+
return np.column_stack([1.0 - p1, p1])
|
|
3398
|
+
|
|
3399
|
+
def predict(self, X):
|
|
3400
|
+
z = self.decision_function(X)
|
|
3401
|
+
return self.classes_[(z > 0).astype(int)]
|
|
3402
|
+
|
|
3403
|
+
|
|
3404
|
+
class CovGroupLasso:
|
|
3405
|
+
"""Group lasso over a design given by its Gram matrix.
|
|
3406
|
+
|
|
3407
|
+
The objective groups the penalty:
|
|
3408
|
+
|
|
3409
|
+
(1/2n) ||y - X b||^2 + lam * sum_g w_g ||b_g||_2
|
|
3410
|
+
|
|
3411
|
+
with the usual weights w_g = sqrt(|g|), so whole groups enter or
|
|
3412
|
+
leave together. Block coordinate descent runs on the Gram matrix
|
|
3413
|
+
exactly as the lasso's coordinate descent does -- the working
|
|
3414
|
+
correlations c = X'y - G b are maintained across visits -- and
|
|
3415
|
+
each block visit is one majorized proximal step: the group's Gram
|
|
3416
|
+
block is dominated by its top eigenvalue L_g (found once), and the
|
|
3417
|
+
resulting subproblem has the group soft-threshold in closed form.
|
|
3418
|
+
Majorization descends monotonically; a group of size one reduces
|
|
3419
|
+
exactly to the lasso's coordinate update.
|
|
3420
|
+
|
|
3421
|
+
``groups`` is a list of index arrays, a partition of 0..p-1.
|
|
3422
|
+
"""
|
|
3423
|
+
|
|
3424
|
+
def __init__(self, X=None, y=None, *, gram=None, corr=None,
|
|
3425
|
+
nobs=None, groups=None):
|
|
3426
|
+
if gram is not None:
|
|
3427
|
+
if corr is None or nobs is None:
|
|
3428
|
+
raise ValueError("gram needs corr and nobs alongside it")
|
|
3429
|
+
g = np.ascontiguousarray(gram, dtype=np.float64)
|
|
3430
|
+
c = np.ascontiguousarray(corr, dtype=np.float64)
|
|
3431
|
+
self.p = c.size
|
|
3432
|
+
self.G = g.reshape(self.p, self.p)
|
|
3433
|
+
self.c0, self.nobs = c, int(nobs)
|
|
3434
|
+
else:
|
|
3435
|
+
if X is None or y is None:
|
|
3436
|
+
raise ValueError("pass X and y, or gram, corr and nobs")
|
|
3437
|
+
X = np.ascontiguousarray(X, dtype=np.float64)
|
|
3438
|
+
y = np.ascontiguousarray(y, dtype=np.float64)
|
|
3439
|
+
self.nobs, self.p = X.shape
|
|
3440
|
+
self.G = X.T @ X
|
|
3441
|
+
self.c0 = X.T @ y
|
|
3442
|
+
if groups is None:
|
|
3443
|
+
raise ValueError("groups is required")
|
|
3444
|
+
self.groups = [np.asarray(g, dtype=np.intp) for g in groups]
|
|
3445
|
+
seen = np.concatenate(self.groups)
|
|
3446
|
+
if sorted(seen.tolist()) != list(range(self.p)):
|
|
3447
|
+
raise ValueError("groups must partition 0..p-1")
|
|
3448
|
+
self.wg = np.array([np.sqrt(len(g)) for g in self.groups])
|
|
3449
|
+
# the majorizer per group: its Gram block's top eigenvalue
|
|
3450
|
+
self.Lg = np.array(
|
|
3451
|
+
[float(np.linalg.eigvalsh(self.G[np.ix_(g, g)])[-1])
|
|
3452
|
+
for g in self.groups])
|
|
3453
|
+
|
|
3454
|
+
def lambda_max(self):
|
|
3455
|
+
"""The smallest penalty at which every group stays zero."""
|
|
3456
|
+
return max(float(np.linalg.norm(self.c0[g])) / (self.nobs * w)
|
|
3457
|
+
for g, w in zip(self.groups, self.wg))
|
|
3458
|
+
|
|
3459
|
+
def lambda_grid(self, num=50, eps=1e-2):
|
|
3460
|
+
"""A log-spaced descending path from lambda_max."""
|
|
3461
|
+
return self.lambda_max() * np.logspace(0, np.log10(eps), int(num))
|
|
3462
|
+
|
|
3463
|
+
def fit_path(self, lambdas, tol=1e-8, max_sweeps=10000):
|
|
3464
|
+
"""Coefficients per lambda, warm-started down the path."""
|
|
3465
|
+
lambdas = np.asarray(lambdas, dtype=np.float64)
|
|
3466
|
+
beta = np.zeros(self.p)
|
|
3467
|
+
c = self.c0.copy()
|
|
3468
|
+
out = np.empty((lambdas.size, self.p))
|
|
3469
|
+
for i, lam in enumerate(lambdas):
|
|
3470
|
+
beta, c = self._fit_one(beta, c, float(lam), tol, max_sweeps)
|
|
3471
|
+
out[i] = beta
|
|
3472
|
+
return out
|
|
3473
|
+
|
|
3474
|
+
def fit(self, lam, **kw):
|
|
3475
|
+
"""Coefficients at one lambda, from zero."""
|
|
3476
|
+
b, _ = self._fit_one(np.zeros(self.p), self.c0.copy(), float(lam),
|
|
3477
|
+
kw.get("tol", 1e-8),
|
|
3478
|
+
kw.get("max_sweeps", 10000))
|
|
3479
|
+
return b
|
|
3480
|
+
|
|
3481
|
+
def _fit_one(self, beta, c, lam, tol, max_sweeps):
|
|
3482
|
+
thr = lam * self.nobs
|
|
3483
|
+
self._sweeps = 0
|
|
3484
|
+
for _ in range(max_sweeps):
|
|
3485
|
+
self._sweeps += 1
|
|
3486
|
+
moved = 0.0
|
|
3487
|
+
for g, w, L in zip(self.groups, self.wg, self.Lg):
|
|
3488
|
+
if L <= 0.0: # all-zero columns: the group stays at zero
|
|
3489
|
+
continue
|
|
3490
|
+
bg = beta[g]
|
|
3491
|
+
z = bg + c[g] / L
|
|
3492
|
+
nz = float(np.linalg.norm(z))
|
|
3493
|
+
scale = max(0.0, 1.0 - thr * w / (L * nz)) if nz > 0 else 0.0
|
|
3494
|
+
nb = scale * z
|
|
3495
|
+
d = nb - bg
|
|
3496
|
+
dmax = float(np.max(np.abs(d))) if d.size else 0.0
|
|
3497
|
+
if dmax > 0.0:
|
|
3498
|
+
c -= self.G[:, g] @ d
|
|
3499
|
+
beta[g] = nb
|
|
3500
|
+
moved = max(moved, dmax)
|
|
3501
|
+
if moved < tol * max(1.0, float(np.max(np.abs(beta)))):
|
|
3502
|
+
break
|
|
3503
|
+
return beta, c
|
|
3504
|
+
|
|
3505
|
+
def objective(self, y_or_none, beta, lam):
|
|
3506
|
+
"""The penalized objective, up to the constant ||y||^2/(2n).
|
|
3507
|
+
|
|
3508
|
+
Computable from the Gram alone: (1/2n)(b'Gb - 2 c0'b) plus the
|
|
3509
|
+
penalty; add ||y||^2/(2n) yourself if you want the absolute
|
|
3510
|
+
value.
|
|
3511
|
+
"""
|
|
3512
|
+
quad = 0.5 * (beta @ self.G @ beta) - self.c0 @ beta
|
|
3513
|
+
pen = sum(w * float(np.linalg.norm(beta[g]))
|
|
3514
|
+
for g, w in zip(self.groups, self.wg))
|
|
3515
|
+
return quad / self.nobs + lam * pen
|
|
3516
|
+
|
|
3517
|
+
|
|
3518
|
+
class GramGroupLasso(Tagged, RegressorMixin, BaseEstimator):
|
|
3519
|
+
"""A ``Lasso``-style interface over CovGroupLasso: fit(X, y) centres
|
|
3520
|
+
the data when ``fit_intercept`` and runs the block descent at
|
|
3521
|
+
``alpha``,
|
|
3522
|
+
|
|
3523
|
+
(1 / 2 n) ||y - X b - b0||^2 + alpha sum_g sqrt(|g|) ||b_g||_2.
|
|
3524
|
+
|
|
3525
|
+
``groups`` is a list of index arrays partitioning the columns, or
|
|
3526
|
+
None for one group per column (the lasso). ``tol`` is
|
|
3527
|
+
CovGroupLasso's and ``max_iter`` its sweep cap.
|
|
3528
|
+
|
|
3529
|
+
Attributes after fit: coef_, intercept_, n_iter_, n_features_in_.
|
|
3530
|
+
"""
|
|
3531
|
+
|
|
3532
|
+
def __init__(self, alpha=1.0, *, groups=None, fit_intercept=True, tol=1e-8,
|
|
3533
|
+
max_iter=10000):
|
|
3534
|
+
self.alpha, self.groups, self.fit_intercept = alpha, groups, fit_intercept
|
|
3535
|
+
self.tol, self.max_iter = tol, max_iter
|
|
3536
|
+
|
|
3537
|
+
def fit(self, X, y):
|
|
3538
|
+
X, y = fit_xy(self, X, y)
|
|
3539
|
+
n, p = X.shape
|
|
3540
|
+
G, c, xm, ym = _centered_gram(X, y, self.fit_intercept)
|
|
3541
|
+
groups = [[j] for j in range(p)] if self.groups is None else self.groups
|
|
3542
|
+
m = CovGroupLasso(gram=G, corr=c, nobs=n, groups=groups)
|
|
3543
|
+
beta, _ = m._fit_one(np.zeros(p), m.c0.copy(), float(self.alpha), float(self.tol),
|
|
3544
|
+
int(self.max_iter))
|
|
3545
|
+
self.coef_, self.n_iter_ = beta, m._sweeps
|
|
3546
|
+
self.intercept_ = float(ym - xm @ self.coef_)
|
|
3547
|
+
return self
|
|
3548
|
+
|
|
3549
|
+
def predict(self, X):
|
|
3550
|
+
return pred_x(self, X) @ self.coef_ + self.intercept_
|
|
3551
|
+
|
|
3552
|
+
|
|
3553
|
+
class CovMultiTaskLasso:
|
|
3554
|
+
"""Multi-task lasso and elastic net over a design's Gram matrix.
|
|
3555
|
+
|
|
3556
|
+
The penalty ties each feature's row of W together across tasks --
|
|
3557
|
+
the row enters or leaves for every task at once -- matching
|
|
3558
|
+
scikit-learn's ``MultiTaskLasso`` / ``MultiTaskElasticNet`` with
|
|
3559
|
+
``fit_intercept=False``:
|
|
3560
|
+
|
|
3561
|
+
(1/2n)||Y - XW||^2_F + alpha l1_ratio sum_j ||W_j||_2
|
|
3562
|
+
+ (alpha/2)(1 - l1_ratio) ||W||^2_F
|
|
3563
|
+
|
|
3564
|
+
The covariance route carries over unchanged: the solver keeps
|
|
3565
|
+
C = X'Y - G W per task, so a row's block update is O(p T) and never
|
|
3566
|
+
touches the n rows. Coefficients are (p, n_tasks) -- the transpose
|
|
3567
|
+
of scikit-learn's ``coef_``.
|
|
3568
|
+
"""
|
|
3569
|
+
|
|
3570
|
+
def __init__(self, X=None, Y=None, *, gram=None, corr=None, nobs=None):
|
|
3571
|
+
if gram is not None:
|
|
3572
|
+
if corr is None or nobs is None:
|
|
3573
|
+
raise ValueError("gram needs corr and nobs alongside it")
|
|
3574
|
+
c = np.ascontiguousarray(corr, dtype=np.float64)
|
|
3575
|
+
if c.ndim != 2:
|
|
3576
|
+
raise ValueError("corr must be (p, n_tasks)")
|
|
3577
|
+
self.p, self.n_tasks = c.shape
|
|
3578
|
+
g = np.ascontiguousarray(gram, dtype=np.float64).ravel()
|
|
3579
|
+
if g.size != self.p * self.p:
|
|
3580
|
+
raise ValueError(f"gram is {g.size} entries; expected p*p")
|
|
3581
|
+
self.nobs, self.g, self.c0 = int(nobs), g, c.ravel()
|
|
3582
|
+
else:
|
|
3583
|
+
if X is None or Y is None:
|
|
3584
|
+
raise ValueError("pass X and Y, or gram, corr and nobs")
|
|
3585
|
+
X = np.ascontiguousarray(X, dtype=np.float64)
|
|
3586
|
+
Y = np.ascontiguousarray(Y, dtype=np.float64)
|
|
3587
|
+
if Y.ndim != 2 or X.shape[0] != Y.shape[0]:
|
|
3588
|
+
raise ValueError(
|
|
3589
|
+
f"X is {X.shape} and Y is {Y.shape}; expected (n, p) "
|
|
3590
|
+
"and (n, n_tasks)")
|
|
3591
|
+
self.nobs, self.p = X.shape
|
|
3592
|
+
self.n_tasks = Y.shape[1]
|
|
3593
|
+
self.g = np.ascontiguousarray((X.T @ X).ravel())
|
|
3594
|
+
self.c0 = np.ascontiguousarray((X.T @ Y).ravel())
|
|
3595
|
+
self.X, self.Y = X, Y
|
|
3596
|
+
|
|
3597
|
+
def lambda_max(self, l1_ratio=1.0):
|
|
3598
|
+
"""The smallest penalty that zeroes every row of W.
|
|
3599
|
+
|
|
3600
|
+
The single-task |X_j'y| becomes the L2 norm of the row X_j'Y:
|
|
3601
|
+
below it the block soft threshold leaves the row at zero.
|
|
3602
|
+
"""
|
|
3603
|
+
C = self.c0.reshape(self.p, self.n_tasks)
|
|
3604
|
+
return (float(np.max(np.linalg.norm(C, axis=1)))
|
|
3605
|
+
/ (self.nobs * l1_ratio))
|
|
3606
|
+
|
|
3607
|
+
def lambda_grid(self, num=100, eps=1e-3, l1_ratio=1.0):
|
|
3608
|
+
"""Log-spaced from lambda_max down, as the single-task grid is."""
|
|
3609
|
+
hi = self.lambda_max(l1_ratio)
|
|
3610
|
+
return hi * np.logspace(0, np.log10(eps), int(num))
|
|
3611
|
+
|
|
3612
|
+
def fit(self, lam, **kw):
|
|
3613
|
+
"""Coefficients (p, n_tasks) at one penalty."""
|
|
3614
|
+
return self.fit_path([lam], **kw)[0]
|
|
3615
|
+
|
|
3616
|
+
def fit_path(self, lambdas, tol=1e-8, chunk=20, max_sweeps=100000,
|
|
3617
|
+
l1_ratio=1.0):
|
|
3618
|
+
"""Coefficients per lambda, warm-started; (len(lambdas), p, n_tasks).
|
|
3619
|
+
|
|
3620
|
+
Give lambdas in descending order, as with the single-task path.
|
|
3621
|
+
"""
|
|
3622
|
+
lambdas = np.asarray(lambdas, dtype=np.float64)
|
|
3623
|
+
pt = self.p * self.n_tasks
|
|
3624
|
+
W, c = np.zeros(pt), self.c0.copy()
|
|
3625
|
+
prev = np.empty(pt)
|
|
3626
|
+
out = np.empty((lambdas.size, self.p, self.n_tasks))
|
|
3627
|
+
for i, lam in enumerate(lambdas):
|
|
3628
|
+
swept = 0
|
|
3629
|
+
while swept < max_sweeps:
|
|
3630
|
+
prev[:] = W
|
|
3631
|
+
kernel.mt_descend(self.g, c, W,
|
|
3632
|
+
float(lam) * l1_ratio,
|
|
3633
|
+
float(lam) * (1.0 - l1_ratio), chunk,
|
|
3634
|
+
float(self.nobs), self.p, self.n_tasks)
|
|
3635
|
+
swept += chunk
|
|
3636
|
+
if np.max(np.abs(W - prev)) < tol * _scale(W):
|
|
3637
|
+
break
|
|
3638
|
+
out[i] = W.reshape(self.p, self.n_tasks)
|
|
3639
|
+
return out
|
|
3640
|
+
|
|
3641
|
+
def fit_path_batch(self, lambdas, tol=1e-8, chunk=20,
|
|
3642
|
+
max_sweeps=100000, force_cpu=False, l1_ratio=1.0):
|
|
3643
|
+
"""Coefficients per lambda, every lambda solved from zero.
|
|
3644
|
+
|
|
3645
|
+
(len(lambdas), p, n_tasks). On the GPU one block per lambda,
|
|
3646
|
+
all sharing this Gram; the CPU fallback is the cold loop.
|
|
3647
|
+
"""
|
|
3648
|
+
lambdas = np.ascontiguousarray(lambdas, dtype=np.float64)
|
|
3649
|
+
pt = self.p * self.n_tasks
|
|
3650
|
+
if not force_cpu:
|
|
3651
|
+
W = _grid_descend(self.g[None, :], self.c0[None, :], lambdas,
|
|
3652
|
+
self.p, self.n_tasks, tol=tol, chunk=chunk,
|
|
3653
|
+
max_sweeps=max_sweeps, l1_ratio=l1_ratio,
|
|
3654
|
+
nobs=float(self.nobs))
|
|
3655
|
+
if W is not None:
|
|
3656
|
+
return W.reshape(lambdas.size, self.p, self.n_tasks)
|
|
3657
|
+
out = np.empty((lambdas.size, pt))
|
|
3658
|
+
prev = np.empty(pt)
|
|
3659
|
+
for i, lam in enumerate(lambdas):
|
|
3660
|
+
W, c, swept = np.zeros(pt), self.c0.copy(), 0
|
|
3661
|
+
while swept < max_sweeps:
|
|
3662
|
+
prev[:] = W
|
|
3663
|
+
kernel.mt_descend(self.g, c, W, float(lam) * l1_ratio,
|
|
3664
|
+
float(lam) * (1.0 - l1_ratio), chunk,
|
|
3665
|
+
float(self.nobs), self.p, self.n_tasks)
|
|
3666
|
+
swept += chunk
|
|
3667
|
+
if np.max(np.abs(W - prev)) < tol * _scale(W):
|
|
3668
|
+
break
|
|
3669
|
+
out[i] = W
|
|
3670
|
+
return out.reshape(lambdas.size, self.p, self.n_tasks)
|
|
3671
|
+
|
|
3672
|
+
|
|
3673
|
+
class GramMultiTaskElasticNet(Tagged, RegressorMixin, BaseEstimator):
|
|
3674
|
+
"""scikit-learn's ``MultiTaskElasticNet`` interface over
|
|
3675
|
+
CovMultiTaskLasso: fit(X, Y) centres the data when
|
|
3676
|
+
``fit_intercept``, forms the Gram matrix once and runs the row-block
|
|
3677
|
+
sweep at ``alpha``. Y is (n, n_tasks); coef_ is (n_tasks, p) as
|
|
3678
|
+
scikit-learn has it. ``tol`` is CovMultiTaskLasso's and
|
|
3679
|
+
``max_iter`` its sweep cap.
|
|
3680
|
+
|
|
3681
|
+
Attributes after fit: coef_, intercept_, n_iter_, n_features_in_.
|
|
3682
|
+
"""
|
|
3683
|
+
|
|
3684
|
+
_multi_output = "only"
|
|
3685
|
+
|
|
3686
|
+
def __init__(self, alpha=1.0, *, l1_ratio=0.5, fit_intercept=True, tol=1e-8,
|
|
3687
|
+
max_iter=100000):
|
|
3688
|
+
self.alpha, self.l1_ratio, self.fit_intercept = alpha, l1_ratio, fit_intercept
|
|
3689
|
+
self.tol, self.max_iter = tol, max_iter
|
|
3690
|
+
|
|
3691
|
+
def fit(self, X, y):
|
|
3692
|
+
X, y = fit_xy(self, X, y, multi_output=True)
|
|
3693
|
+
if y.ndim != 2:
|
|
3694
|
+
raise ValueError("For mono-task outputs, use GramElasticNet or GramLasso; "
|
|
3695
|
+
"y must be (n_samples, n_tasks)")
|
|
3696
|
+
n, p = X.shape
|
|
3697
|
+
t = y.shape[1]
|
|
3698
|
+
G, c, xm, ym = _centered_gram(X, y, self.fit_intercept)
|
|
3699
|
+
g = np.ascontiguousarray(G.ravel())
|
|
3700
|
+
c = np.ascontiguousarray(c.ravel())
|
|
3701
|
+
lam, r = float(self.alpha), float(self.l1_ratio)
|
|
3702
|
+
tol, cap = float(self.tol), int(self.max_iter)
|
|
3703
|
+
W, prev = np.zeros(p * t), np.empty(p * t)
|
|
3704
|
+
swept = 0
|
|
3705
|
+
while swept < cap:
|
|
3706
|
+
chunk = min(20, cap - swept)
|
|
3707
|
+
prev[:] = W
|
|
3708
|
+
kernel.mt_descend(g, c, W, lam * r, lam * (1.0 - r), chunk, float(n), p, t)
|
|
3709
|
+
swept += chunk
|
|
3710
|
+
if np.max(np.abs(W - prev)) < tol * _scale(W):
|
|
3711
|
+
break
|
|
3712
|
+
W = W.reshape(p, t)
|
|
3713
|
+
self.coef_, self.n_iter_ = np.ascontiguousarray(W.T), swept
|
|
3714
|
+
self.intercept_ = ym - xm @ W
|
|
3715
|
+
return self
|
|
3716
|
+
|
|
3717
|
+
def predict(self, X):
|
|
3718
|
+
return pred_x(self, X) @ self.coef_.T + self.intercept_
|
|
3719
|
+
|
|
3720
|
+
|
|
3721
|
+
class GramMultiTaskLasso(GramMultiTaskElasticNet):
|
|
3722
|
+
"""scikit-learn's ``MultiTaskLasso`` interface over CovMultiTaskLasso:
|
|
3723
|
+
GramMultiTaskElasticNet with l1_ratio 1.
|
|
3724
|
+
|
|
3725
|
+
Attributes after fit: coef_, intercept_, n_iter_, n_features_in_.
|
|
3726
|
+
"""
|
|
3727
|
+
|
|
3728
|
+
def __init__(self, alpha=1.0, *, fit_intercept=True, tol=1e-8, max_iter=100000):
|
|
3729
|
+
self.alpha, self.fit_intercept, self.tol, self.max_iter = alpha, fit_intercept, tol, max_iter
|
|
3730
|
+
|
|
3731
|
+
@property
|
|
3732
|
+
def l1_ratio(self):
|
|
3733
|
+
return 1.0
|
|
3734
|
+
|
|
3735
|
+
|
|
3736
|
+
class CovMultiTaskLassoCV(Tagged, RegressorMixin, BaseEstimator):
|
|
3737
|
+
"""scikit-learn's ``MultiTaskLassoCV`` (``MultiTaskElasticNetCV``
|
|
3738
|
+
with ``l1_ratio``) over the covariance machinery.
|
|
3739
|
+
|
|
3740
|
+
Same construction as ``CovLassoCV``: the grid from the full data,
|
|
3741
|
+
contiguous folds, each fold's training Gram a subtraction from the
|
|
3742
|
+
full one, the whole path walked warm per fold, alpha_ by minimum
|
|
3743
|
+
mean validation MSE (over samples and tasks) with ties to the
|
|
3744
|
+
larger penalty, refit on all rows. With a GPU the grid is one
|
|
3745
|
+
launch, one block per (fold, penalty) problem, as in CovLassoCV.
|
|
3746
|
+
|
|
3747
|
+
Attributes after fit: alphas_, mse_path_ (num, cv), alpha_, coef_
|
|
3748
|
+
(p, n_tasks).
|
|
3749
|
+
"""
|
|
3750
|
+
|
|
3751
|
+
_multi_output = "only"
|
|
3752
|
+
|
|
3753
|
+
def __init__(self, cv=5, num=100, eps=1e-3, tol=1e-8, l1_ratio=1.0,
|
|
3754
|
+
force_cpu=False, force_gpu=False, n_jobs=1):
|
|
3755
|
+
# Same fold-thread parallelism as CovLassoCV; default sequential.
|
|
3756
|
+
self.cv, self.num, self.eps, self.tol = cv, num, eps, tol
|
|
3757
|
+
self.l1_ratio, self.force_cpu, self.force_gpu = l1_ratio, force_cpu, force_gpu
|
|
3758
|
+
self.n_jobs = n_jobs
|
|
3759
|
+
|
|
3760
|
+
def fit(self, X, Y):
|
|
3761
|
+
X, Y = fit_xy(self, X, Y, multi_output=True)
|
|
3762
|
+
if Y.ndim != 2:
|
|
3763
|
+
raise ValueError("For mono-task outputs, use CovLassoCV; Y must be (n, n_tasks)")
|
|
3764
|
+
n = X.shape[0]
|
|
3765
|
+
cv, num = int(self.cv), int(self.num)
|
|
3766
|
+
tol, l1_ratio = float(self.tol), float(self.l1_ratio)
|
|
3767
|
+
full = CovMultiTaskLasso(X, Y)
|
|
3768
|
+
self.alphas_ = full.lambda_grid(num=num, eps=float(self.eps),
|
|
3769
|
+
l1_ratio=l1_ratio)
|
|
3770
|
+
bounds = np.linspace(0, n, cv + 1).astype(int)
|
|
3771
|
+
self.mse_path_ = np.empty((num, cv))
|
|
3772
|
+
G = full.g.reshape(full.p, full.p)
|
|
3773
|
+
C = full.c0.reshape(full.p, full.n_tasks)
|
|
3774
|
+
if cuda_available() and not self.force_cpu:
|
|
3775
|
+
done = self._fit_gpu(X, Y, full, G, C, bounds, n)
|
|
3776
|
+
if done is not None:
|
|
3777
|
+
return done
|
|
3778
|
+
|
|
3779
|
+
def one_fold(k):
|
|
3780
|
+
lo, hi = bounds[k], bounds[k + 1]
|
|
3781
|
+
Xf, Yf = X[lo:hi], Y[lo:hi]
|
|
3782
|
+
m = CovMultiTaskLasso(gram=(G - Xf.T @ Xf).ravel(),
|
|
3783
|
+
corr=C - Xf.T @ Yf,
|
|
3784
|
+
nobs=n - (hi - lo))
|
|
3785
|
+
path = m.fit_path(self.alphas_, tol=tol, l1_ratio=l1_ratio)
|
|
3786
|
+
# all validation residuals in one product: (nf, num, T)
|
|
3787
|
+
resid = np.tensordot(Xf, path, axes=([1], [1])) - Yf[:, None, :]
|
|
3788
|
+
self.mse_path_[:, k] = np.mean(resid * resid, axis=(0, 2))
|
|
3789
|
+
|
|
3790
|
+
if int(self.n_jobs) > 1:
|
|
3791
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
3792
|
+
with ThreadPoolExecutor(max_workers=int(self.n_jobs)) as ex:
|
|
3793
|
+
list(ex.map(one_fold, range(cv)))
|
|
3794
|
+
else:
|
|
3795
|
+
for k in range(cv):
|
|
3796
|
+
one_fold(k)
|
|
3797
|
+
return self._finish(full)
|
|
3798
|
+
|
|
3799
|
+
def _fit_gpu(self, X, Y, full, G, C, bounds, n):
|
|
3800
|
+
# The grid as one launch, the fold's Gram shared by its
|
|
3801
|
+
# problems and nobs absorbed into each problem's lambda as in
|
|
3802
|
+
# CovLassoCV; None when the device declined.
|
|
3803
|
+
p_, T = full.p, full.n_tasks
|
|
3804
|
+
num, cv = int(self.num), int(self.cv)
|
|
3805
|
+
grams = np.empty((cv, p_ * p_))
|
|
3806
|
+
corrs = np.empty((cv, p_ * T))
|
|
3807
|
+
nobs_k = np.empty(cv)
|
|
3808
|
+
for k in range(cv):
|
|
3809
|
+
lo, hi = bounds[k], bounds[k + 1]
|
|
3810
|
+
Xf, Yf = X[lo:hi], Y[lo:hi]
|
|
3811
|
+
grams[k] = (G - Xf.T @ Xf).ravel()
|
|
3812
|
+
corrs[k] = (C - Xf.T @ Yf).ravel()
|
|
3813
|
+
nobs_k[k] = n - (hi - lo)
|
|
3814
|
+
lams = np.tile(self.alphas_, cv) * np.repeat(nobs_k, num)
|
|
3815
|
+
W = _grid_descend(grams, corrs, lams, p_, T, tol=float(self.tol),
|
|
3816
|
+
l1_ratio=float(self.l1_ratio))
|
|
3817
|
+
if W is None:
|
|
3818
|
+
return None
|
|
3819
|
+
W = W.reshape(cv, num, p_, T)
|
|
3820
|
+
for k in range(cv):
|
|
3821
|
+
lo, hi = bounds[k], bounds[k + 1]
|
|
3822
|
+
resid = (np.tensordot(X[lo:hi], W[k], axes=([1], [1]))
|
|
3823
|
+
- Y[lo:hi, None, :])
|
|
3824
|
+
self.mse_path_[:, k] = np.mean(resid * resid, axis=(0, 2))
|
|
3825
|
+
return self._finish(full)
|
|
3826
|
+
|
|
3827
|
+
def _finish(self, full):
|
|
3828
|
+
mean = self.mse_path_.mean(axis=1)
|
|
3829
|
+
self.alpha_ = float(self.alphas_[int(np.argmin(mean))])
|
|
3830
|
+
self.coef_ = full.fit(self.alpha_, tol=float(self.tol),
|
|
3831
|
+
l1_ratio=float(self.l1_ratio))
|
|
3832
|
+
self.intercept_ = 0.0 if self.coef_.ndim == 1 else np.zeros(self.coef_.shape[1])
|
|
3833
|
+
return self
|
|
3834
|
+
|
|
3835
|
+
def predict(self, X):
|
|
3836
|
+
return pred_x(self, X) @ self.coef_
|