statgpu 0.1.0__py3-none-any.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.
- statgpu/__init__.py +174 -0
- statgpu/_base.py +544 -0
- statgpu/_config.py +127 -0
- statgpu/anova/__init__.py +5 -0
- statgpu/anova/_oneway.py +194 -0
- statgpu/backends/__init__.py +83 -0
- statgpu/backends/_array_ops.py +529 -0
- statgpu/backends/_base.py +184 -0
- statgpu/backends/_cupy.py +453 -0
- statgpu/backends/_factory.py +65 -0
- statgpu/backends/_gpu_inference_cupy.py +214 -0
- statgpu/backends/_gpu_inference_torch.py +422 -0
- statgpu/backends/_numpy.py +324 -0
- statgpu/backends/_torch.py +685 -0
- statgpu/backends/_torch_safe.py +47 -0
- statgpu/backends/_utils.py +423 -0
- statgpu/core/__init__.py +10 -0
- statgpu/core/formula/__init__.py +33 -0
- statgpu/core/formula/_design.py +99 -0
- statgpu/core/formula/_parser.py +191 -0
- statgpu/core/formula/_terms.py +70 -0
- statgpu/core/formula/tests/__init__.py +0 -0
- statgpu/core/formula/tests/test_parser.py +194 -0
- statgpu/covariance/__init__.py +6 -0
- statgpu/covariance/_empirical.py +310 -0
- statgpu/covariance/_shrinkage.py +248 -0
- statgpu/cross_validation/__init__.py +31 -0
- statgpu/cross_validation/_base.py +410 -0
- statgpu/cross_validation/_engine.py +167 -0
- statgpu/diagnostics/__init__.py +7 -0
- statgpu/diagnostics/_regression_diagnostics.py +188 -0
- statgpu/feature_selection/__init__.py +24 -0
- statgpu/feature_selection/_knockoff.py +870 -0
- statgpu/feature_selection/_knockoff_utils.py +1003 -0
- statgpu/feature_selection/_stepwise.py +300 -0
- statgpu/glm_core/__init__.py +81 -0
- statgpu/glm_core/_base.py +202 -0
- statgpu/glm_core/_family.py +362 -0
- statgpu/glm_core/_fused.py +149 -0
- statgpu/glm_core/_gamma.py +111 -0
- statgpu/glm_core/_inverse_gaussian.py +62 -0
- statgpu/glm_core/_irls.py +561 -0
- statgpu/glm_core/_logistic.py +82 -0
- statgpu/glm_core/_negative_binomial.py +68 -0
- statgpu/glm_core/_poisson.py +60 -0
- statgpu/glm_core/_solver_legacy.py +100 -0
- statgpu/glm_core/_squared.py +53 -0
- statgpu/glm_core/_tweedie.py +74 -0
- statgpu/inference/__init__.py +239 -0
- statgpu/inference/_distributions_backend.py +2610 -0
- statgpu/inference/_multiple_testing.py +391 -0
- statgpu/inference/_resampling.py +1400 -0
- statgpu/inference/_results.py +265 -0
- statgpu/linear_model/__init__.py +75 -0
- statgpu/linear_model/_gaussian_inference.py +306 -0
- statgpu/linear_model/_glm_base.py +1261 -0
- statgpu/linear_model/_ordered_logit.py +52 -0
- statgpu/linear_model/_ordered_probit.py +50 -0
- statgpu/linear_model/_stats.py +170 -0
- statgpu/linear_model/cv/__init__.py +13 -0
- statgpu/linear_model/cv/_elasticnet_cv.py +892 -0
- statgpu/linear_model/cv/_lasso_cv.py +253 -0
- statgpu/linear_model/cv/_logistic_cv.py +895 -0
- statgpu/linear_model/cv/_ridge_cv.py +1160 -0
- statgpu/linear_model/legacy/__init__.py +1 -0
- statgpu/linear_model/legacy/_distributions_legacy_gpu.py +340 -0
- statgpu/linear_model/legacy/_elasticnet_legacy.py +936 -0
- statgpu/linear_model/legacy/_lasso_legacy.py +4876 -0
- statgpu/linear_model/legacy/_penalized_legacy.py +1174 -0
- statgpu/linear_model/legacy/_ridge_legacy.py +863 -0
- statgpu/linear_model/legacy/_solver_legacy.py +104 -0
- statgpu/linear_model/penalized/__init__.py +25 -0
- statgpu/linear_model/penalized/_base.py +437 -0
- statgpu/linear_model/penalized/_fit_mixin.py +1877 -0
- statgpu/linear_model/penalized/_inference_mixin.py +1179 -0
- statgpu/linear_model/penalized/_penalized_cv.py +2699 -0
- statgpu/linear_model/penalized/_penalized_gamma.py +86 -0
- statgpu/linear_model/penalized/_penalized_inverse_gaussian.py +62 -0
- statgpu/linear_model/penalized/_penalized_linear.py +236 -0
- statgpu/linear_model/penalized/_penalized_logistic.py +100 -0
- statgpu/linear_model/penalized/_penalized_negative_binomial.py +65 -0
- statgpu/linear_model/penalized/_penalized_poisson.py +62 -0
- statgpu/linear_model/penalized/_penalized_tweedie.py +65 -0
- statgpu/linear_model/penalized/_predict_mixin.py +182 -0
- statgpu/linear_model/wrappers/__init__.py +31 -0
- statgpu/linear_model/wrappers/_adaptive_lasso.py +63 -0
- statgpu/linear_model/wrappers/_elasticnet.py +75 -0
- statgpu/linear_model/wrappers/_gamma.py +67 -0
- statgpu/linear_model/wrappers/_inverse_gaussian.py +47 -0
- statgpu/linear_model/wrappers/_lasso.py +2124 -0
- statgpu/linear_model/wrappers/_linear.py +1127 -0
- statgpu/linear_model/wrappers/_logistic.py +1435 -0
- statgpu/linear_model/wrappers/_mcp.py +58 -0
- statgpu/linear_model/wrappers/_negative_binomial.py +58 -0
- statgpu/linear_model/wrappers/_poisson.py +48 -0
- statgpu/linear_model/wrappers/_ridge.py +166 -0
- statgpu/linear_model/wrappers/_scad.py +58 -0
- statgpu/linear_model/wrappers/_tweedie.py +57 -0
- statgpu/metrics/__init__.py +21 -0
- statgpu/metrics/_classification.py +591 -0
- statgpu/nonparametric/__init__.py +50 -0
- statgpu/nonparametric/kernel_methods/__init__.py +25 -0
- statgpu/nonparametric/kernel_methods/_kernels.py +246 -0
- statgpu/nonparametric/kernel_methods/_krr.py +234 -0
- statgpu/nonparametric/kernel_methods/_krr_cv.py +380 -0
- statgpu/nonparametric/kernel_smoothing/__init__.py +39 -0
- statgpu/nonparametric/kernel_smoothing/_bandwidth_selection.py +1083 -0
- statgpu/nonparametric/kernel_smoothing/_kde.py +761 -0
- statgpu/nonparametric/kernel_smoothing/_kernel_common.py +348 -0
- statgpu/nonparametric/kernel_smoothing/_kernel_regression.py +748 -0
- statgpu/nonparametric/splines/__init__.py +5 -0
- statgpu/nonparametric/splines/_bspline_basis.py +336 -0
- statgpu/nonparametric/splines/_penalized.py +349 -0
- statgpu/panel/__init__.py +19 -0
- statgpu/panel/_covariance.py +140 -0
- statgpu/panel/_fixed_effects.py +420 -0
- statgpu/panel/_random_effects.py +385 -0
- statgpu/panel/_utils.py +482 -0
- statgpu/penalties/__init__.py +139 -0
- statgpu/penalties/_adaptive_l1.py +313 -0
- statgpu/penalties/_base.py +261 -0
- statgpu/penalties/_categories.py +39 -0
- statgpu/penalties/_elasticnet.py +98 -0
- statgpu/penalties/_group_lasso.py +678 -0
- statgpu/penalties/_group_mcp.py +553 -0
- statgpu/penalties/_group_scad.py +605 -0
- statgpu/penalties/_l1.py +107 -0
- statgpu/penalties/_l2.py +77 -0
- statgpu/penalties/_mcp.py +237 -0
- statgpu/penalties/_scad.py +260 -0
- statgpu/semiparametric/__init__.py +5 -0
- statgpu/semiparametric/_gam.py +401 -0
- statgpu/solvers/__init__.py +24 -0
- statgpu/solvers/_admm.py +241 -0
- statgpu/solvers/_constants.py +15 -0
- statgpu/solvers/_convergence.py +6 -0
- statgpu/solvers/_fista.py +436 -0
- statgpu/solvers/_fista_bb.py +513 -0
- statgpu/solvers/_fista_lla.py +541 -0
- statgpu/solvers/_lbfgs.py +206 -0
- statgpu/solvers/_newton.py +149 -0
- statgpu/solvers/_utils.py +277 -0
- statgpu/survival/__init__.py +14 -0
- statgpu/survival/_cox.py +3974 -0
- statgpu/survival/_cox_breslow_triton_kernel.py +106 -0
- statgpu/survival/_cox_cv.py +1159 -0
- statgpu/survival/_cox_efron_cuda.py +1280 -0
- statgpu/survival/_cox_efron_triton.py +359 -0
- statgpu/unsupervised/__init__.py +29 -0
- statgpu/unsupervised/_agglomerative.py +307 -0
- statgpu/unsupervised/_dbscan.py +263 -0
- statgpu/unsupervised/_dbscan_cpu.pyx +125 -0
- statgpu/unsupervised/_gmm.py +332 -0
- statgpu/unsupervised/_incremental_pca.py +176 -0
- statgpu/unsupervised/_kmeans.py +261 -0
- statgpu/unsupervised/_minibatch_kmeans.py +299 -0
- statgpu/unsupervised/_minibatch_nmf.py +252 -0
- statgpu/unsupervised/_nmf.py +190 -0
- statgpu/unsupervised/_pca.py +189 -0
- statgpu/unsupervised/_truncated_svd.py +132 -0
- statgpu/unsupervised/_tsne.py +192 -0
- statgpu/unsupervised/_umap.py +224 -0
- statgpu/unsupervised/_utils.py +134 -0
- statgpu-0.1.0.dist-info/METADATA +245 -0
- statgpu-0.1.0.dist-info/RECORD +168 -0
- statgpu-0.1.0.dist-info/WHEEL +5 -0
- statgpu-0.1.0.dist-info/licenses/LICENSE +199 -0
- statgpu-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,892 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ElasticNetCV: Cross-validated Elastic Net regression with GPU support.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
__all__ = ["ElasticNetCV"]
|
|
6
|
+
|
|
7
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
8
|
+
from collections import OrderedDict
|
|
9
|
+
import hashlib
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
from statgpu._config import Device, cuda_available
|
|
13
|
+
from statgpu.cross_validation._base import CVEstimatorBase, batch_mse as _batch_mse_cv
|
|
14
|
+
from statgpu.backends import get_backend
|
|
15
|
+
from statgpu.linear_model.wrappers._elasticnet import ElasticNet
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# =============================================================================
|
|
19
|
+
# CV Cache
|
|
20
|
+
# =============================================================================
|
|
21
|
+
|
|
22
|
+
import threading
|
|
23
|
+
|
|
24
|
+
_ELASTICNET_CV_CACHE_MAXSIZE = int(64)
|
|
25
|
+
_ELASTICNET_CV_CACHE: "OrderedDict[Tuple[Any, ...], Dict[str, Any]]" = OrderedDict()
|
|
26
|
+
_ELASTICNET_CV_CACHE_LOCK = threading.Lock()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _elasticnet_cv_cache_get(cache_key: Optional[Tuple[Any, ...]]) -> Optional[Dict[str, Any]]:
|
|
30
|
+
"""Get cached ElasticNet CV results."""
|
|
31
|
+
if cache_key is None:
|
|
32
|
+
return None
|
|
33
|
+
with _ELASTICNET_CV_CACHE_LOCK:
|
|
34
|
+
val = _ELASTICNET_CV_CACHE.get(cache_key)
|
|
35
|
+
if val is not None:
|
|
36
|
+
_ELASTICNET_CV_CACHE.move_to_end(cache_key)
|
|
37
|
+
return val
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _elasticnet_cv_cache_put(cache_key: Optional[Tuple[Any, ...]], value: Dict[str, Any]) -> None:
|
|
41
|
+
"""Put cached ElasticNet CV results."""
|
|
42
|
+
if cache_key is None:
|
|
43
|
+
return
|
|
44
|
+
with _ELASTICNET_CV_CACHE_LOCK:
|
|
45
|
+
_ELASTICNET_CV_CACHE[cache_key] = value
|
|
46
|
+
_ELASTICNET_CV_CACHE.move_to_end(cache_key)
|
|
47
|
+
while len(_ELASTICNET_CV_CACHE) > _ELASTICNET_CV_CACHE_MAXSIZE:
|
|
48
|
+
_ELASTICNET_CV_CACHE.popitem(last=False)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _make_elasticnet_cv_auto_cache_key(
|
|
52
|
+
X_shape: Tuple[int, ...],
|
|
53
|
+
y_shape: Tuple[int, ...],
|
|
54
|
+
l1_ratios: Tuple[float, ...],
|
|
55
|
+
alphas: Optional[np.ndarray],
|
|
56
|
+
n_alphas: int,
|
|
57
|
+
alpha_min_ratio: float,
|
|
58
|
+
folds: List[Tuple[np.ndarray, np.ndarray]],
|
|
59
|
+
fit_intercept: bool,
|
|
60
|
+
use_gpu: bool,
|
|
61
|
+
max_iter: int,
|
|
62
|
+
tol: float,
|
|
63
|
+
sample_weight_shape: Optional[Tuple[int, ...]] = None,
|
|
64
|
+
data_digest: Optional[bytes] = None,
|
|
65
|
+
) -> Tuple[Any, ...]:
|
|
66
|
+
"""Generate automatic cache key for ElasticNet CV."""
|
|
67
|
+
h = hashlib.blake2b(digest_size=32)
|
|
68
|
+
h.update(np.asarray(X_shape, dtype=np.int64).tobytes())
|
|
69
|
+
h.update(np.asarray(y_shape, dtype=np.int64).tobytes())
|
|
70
|
+
if data_digest is not None:
|
|
71
|
+
h.update(data_digest)
|
|
72
|
+
h.update(np.asarray(l1_ratios, dtype=np.float64).tobytes())
|
|
73
|
+
if alphas is not None:
|
|
74
|
+
h.update(np.asarray(alphas, dtype=np.float64).tobytes())
|
|
75
|
+
h.update(str(n_alphas).encode("utf-8"))
|
|
76
|
+
h.update(str(alpha_min_ratio).encode("utf-8"))
|
|
77
|
+
h.update(str(fit_intercept).encode("utf-8"))
|
|
78
|
+
h.update(str(use_gpu).encode("utf-8"))
|
|
79
|
+
h.update(str(max_iter).encode("utf-8"))
|
|
80
|
+
h.update(str(tol).encode("utf-8"))
|
|
81
|
+
h.update(str(len(folds)).encode("utf-8"))
|
|
82
|
+
for train_idx, test_idx in folds:
|
|
83
|
+
train_idx_arr = (
|
|
84
|
+
train_idx
|
|
85
|
+
if isinstance(train_idx, np.ndarray) and train_idx.dtype == np.int64
|
|
86
|
+
else np.asarray(train_idx, dtype=np.int64)
|
|
87
|
+
)
|
|
88
|
+
test_idx_arr = (
|
|
89
|
+
test_idx
|
|
90
|
+
if isinstance(test_idx, np.ndarray) and test_idx.dtype == np.int64
|
|
91
|
+
else np.asarray(test_idx, dtype=np.int64)
|
|
92
|
+
)
|
|
93
|
+
h.update(train_idx_arr.tobytes())
|
|
94
|
+
h.update(test_idx_arr.tobytes())
|
|
95
|
+
if sample_weight_shape is not None:
|
|
96
|
+
h.update(np.asarray(sample_weight_shape, dtype=np.int64).tobytes())
|
|
97
|
+
return h.hexdigest()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
from statgpu.cross_validation._base import hash_cv_data as _hash_data
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# =============================================================================
|
|
104
|
+
# K-fold helpers
|
|
105
|
+
# =============================================================================
|
|
106
|
+
|
|
107
|
+
from statgpu.cross_validation._base import kfold_indices as _kfold_indices, folds_are_complete as _folds_are_complete
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# =============================================================================
|
|
111
|
+
# Alpha grid generation for ElasticNet
|
|
112
|
+
# =============================================================================
|
|
113
|
+
|
|
114
|
+
def _default_elasticnet_alpha_grid(
|
|
115
|
+
X,
|
|
116
|
+
y,
|
|
117
|
+
l1_ratio: float = 0.5,
|
|
118
|
+
n_alphas: int = 100,
|
|
119
|
+
alpha_min_ratio: float = 1e-3,
|
|
120
|
+
) -> np.ndarray:
|
|
121
|
+
"""
|
|
122
|
+
Generate default alpha grid for ElasticNet.
|
|
123
|
+
|
|
124
|
+
Parameters
|
|
125
|
+
----------
|
|
126
|
+
X : array-like
|
|
127
|
+
Design matrix (n_samples, n_features).
|
|
128
|
+
y : array-like
|
|
129
|
+
Response vector.
|
|
130
|
+
l1_ratio : float
|
|
131
|
+
L1 ratio (0.0 = Ridge, 1.0 = Lasso).
|
|
132
|
+
n_alphas : int
|
|
133
|
+
Number of alpha values.
|
|
134
|
+
alpha_min_ratio : float
|
|
135
|
+
Minimum alpha as a ratio of max alpha.
|
|
136
|
+
|
|
137
|
+
Returns
|
|
138
|
+
-------
|
|
139
|
+
alphas : ndarray
|
|
140
|
+
Log-spaced alpha values.
|
|
141
|
+
"""
|
|
142
|
+
X_arr = np.asarray(X, dtype=np.float64)
|
|
143
|
+
y_arr = np.asarray(y, dtype=np.float64).reshape(-1)
|
|
144
|
+
|
|
145
|
+
n_samples, n_features = X_arr.shape
|
|
146
|
+
|
|
147
|
+
# Handle intercept by centering
|
|
148
|
+
X_mean = np.mean(X_arr, axis=0)
|
|
149
|
+
y_mean = np.mean(y_arr)
|
|
150
|
+
X_centered = X_arr - X_mean
|
|
151
|
+
y_centered = y_arr - y_mean
|
|
152
|
+
|
|
153
|
+
# Compute correlation for alpha_max
|
|
154
|
+
Xty = X_centered.T @ y_centered
|
|
155
|
+
|
|
156
|
+
# alpha_max = max(|X'c yc|) / (n * l1_ratio)
|
|
157
|
+
# For l1_ratio=1 (Lasso): max(|X'y|) / n
|
|
158
|
+
# For l1_ratio<1: larger because L2 penalty contributes less
|
|
159
|
+
_l1r = max(float(l1_ratio), 1e-6)
|
|
160
|
+
alpha_max = float(np.max(np.abs(Xty))) / (n_samples * _l1r)
|
|
161
|
+
alpha_max = max(alpha_max, 1e-6)
|
|
162
|
+
|
|
163
|
+
if alpha_max <= 0:
|
|
164
|
+
alpha_max = 1.0
|
|
165
|
+
|
|
166
|
+
# Log-spaced grid
|
|
167
|
+
if int(n_alphas) <= 1:
|
|
168
|
+
return np.asarray([alpha_max], dtype=np.float64)
|
|
169
|
+
|
|
170
|
+
alpha_min = max(float(alpha_min_ratio) * alpha_max, 1e-6)
|
|
171
|
+
return np.geomspace(alpha_max, alpha_min, num=int(n_alphas)).astype(np.float64)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _default_elasticnet_alpha_grid_backend(
|
|
175
|
+
X,
|
|
176
|
+
y,
|
|
177
|
+
backend,
|
|
178
|
+
l1_ratio: float = 0.5,
|
|
179
|
+
n_alphas: int = 100,
|
|
180
|
+
alpha_min_ratio: float = 1e-3,
|
|
181
|
+
) -> np.ndarray:
|
|
182
|
+
"""
|
|
183
|
+
Generate default alpha grid for ElasticNet using backend abstraction.
|
|
184
|
+
|
|
185
|
+
Parameters
|
|
186
|
+
----------
|
|
187
|
+
X : array-like
|
|
188
|
+
Design matrix.
|
|
189
|
+
y : array-like
|
|
190
|
+
Response vector.
|
|
191
|
+
backend : BackendBase
|
|
192
|
+
Backend instance.
|
|
193
|
+
l1_ratio : float
|
|
194
|
+
L1 ratio.
|
|
195
|
+
n_alphas : int
|
|
196
|
+
Number of alpha values.
|
|
197
|
+
alpha_min_ratio : float
|
|
198
|
+
Minimum alpha ratio.
|
|
199
|
+
|
|
200
|
+
Returns
|
|
201
|
+
-------
|
|
202
|
+
alphas : ndarray
|
|
203
|
+
Log-spaced alpha values.
|
|
204
|
+
"""
|
|
205
|
+
X_arr = backend.asarray(X, dtype=backend.float64)
|
|
206
|
+
y_arr = backend.asarray(y, dtype=backend.float64).reshape(-1)
|
|
207
|
+
|
|
208
|
+
n_samples = int(X_arr.shape[0])
|
|
209
|
+
|
|
210
|
+
# Center data
|
|
211
|
+
X_mean = backend.mean(X_arr, axis=0)
|
|
212
|
+
y_mean = backend.mean(y_arr)
|
|
213
|
+
X_centered = X_arr - X_mean
|
|
214
|
+
y_centered = y_arr - y_mean
|
|
215
|
+
|
|
216
|
+
# Compute Xty
|
|
217
|
+
Xty = X_centered.T @ y_centered
|
|
218
|
+
|
|
219
|
+
# Alpha max: max(|X'y|) / (n * l1_ratio)
|
|
220
|
+
_l1r = max(float(l1_ratio), 1e-6)
|
|
221
|
+
alpha_max = float(backend.max(backend.abs(Xty))) / (n_samples * _l1r)
|
|
222
|
+
|
|
223
|
+
if alpha_max <= 0:
|
|
224
|
+
alpha_max = 1.0
|
|
225
|
+
|
|
226
|
+
if int(n_alphas) <= 1:
|
|
227
|
+
return np.asarray([alpha_max], dtype=np.float64)
|
|
228
|
+
|
|
229
|
+
alpha_min = max(float(alpha_min_ratio) * alpha_max, 1e-6)
|
|
230
|
+
return np.geomspace(alpha_max, alpha_min, num=int(n_alphas)).astype(np.float64)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# =============================================================================
|
|
234
|
+
# CV main function
|
|
235
|
+
# =============================================================================
|
|
236
|
+
|
|
237
|
+
def _select_elasticnet_params_cv(
|
|
238
|
+
X,
|
|
239
|
+
y,
|
|
240
|
+
*,
|
|
241
|
+
l1_ratios=None,
|
|
242
|
+
alphas=None,
|
|
243
|
+
n_alphas: int = 100,
|
|
244
|
+
alpha_min_ratio: float = 1e-3,
|
|
245
|
+
cv_folds: int = 5,
|
|
246
|
+
cv_splits=None,
|
|
247
|
+
random_state: Optional[int] = None,
|
|
248
|
+
sample_weight=None,
|
|
249
|
+
fit_intercept: bool = True,
|
|
250
|
+
device: Union[str, Device] = Device.CPU,
|
|
251
|
+
max_iter: int = 1000,
|
|
252
|
+
tol: float = 1e-4,
|
|
253
|
+
return_details: bool = False,
|
|
254
|
+
cache_key: Optional[Tuple[Any, ...]] = None,
|
|
255
|
+
):
|
|
256
|
+
"""
|
|
257
|
+
Select alpha and l1_ratio for Elastic Net via K-fold cross-validation.
|
|
258
|
+
|
|
259
|
+
Parameters
|
|
260
|
+
----------
|
|
261
|
+
X : array-like
|
|
262
|
+
Design matrix (n_samples, n_features).
|
|
263
|
+
y : array-like
|
|
264
|
+
Response vector.
|
|
265
|
+
l1_ratios : array-like or None
|
|
266
|
+
L1 ratios to try. If None, uses [0.2, 0.5, 0.7, 0.8, 0.9, 0.95, 0.99].
|
|
267
|
+
alphas : array-like or None
|
|
268
|
+
Alpha values to try. If None, generates n_alphas values.
|
|
269
|
+
n_alphas : int
|
|
270
|
+
Number of alpha values (if alphas is None).
|
|
271
|
+
alpha_min_ratio : float
|
|
272
|
+
Minimum alpha ratio.
|
|
273
|
+
cv_folds : int
|
|
274
|
+
Number of CV folds.
|
|
275
|
+
cv_splits : list or None
|
|
276
|
+
Pre-computed CV splits.
|
|
277
|
+
random_state : int or None
|
|
278
|
+
Random seed.
|
|
279
|
+
sample_weight : array-like or None
|
|
280
|
+
Sample weights.
|
|
281
|
+
fit_intercept : bool
|
|
282
|
+
Whether to fit intercept.
|
|
283
|
+
device : str or Device
|
|
284
|
+
Device to use.
|
|
285
|
+
max_iter : int
|
|
286
|
+
Maximum iterations.
|
|
287
|
+
tol : float
|
|
288
|
+
Convergence tolerance.
|
|
289
|
+
return_details : bool
|
|
290
|
+
Whether to return full CV details.
|
|
291
|
+
cache_key : tuple or None
|
|
292
|
+
Cache key.
|
|
293
|
+
|
|
294
|
+
Returns
|
|
295
|
+
-------
|
|
296
|
+
best_alpha : float
|
|
297
|
+
best_l1_ratio : float
|
|
298
|
+
details : dict (if return_details=True)
|
|
299
|
+
"""
|
|
300
|
+
if isinstance(device, Device):
|
|
301
|
+
device_name = device.value
|
|
302
|
+
else:
|
|
303
|
+
device_name = str(device).lower()
|
|
304
|
+
if device_name.startswith("device."):
|
|
305
|
+
enum_name = device_name.split(".", 1)[1].upper()
|
|
306
|
+
if enum_name not in Device.__members__:
|
|
307
|
+
valid = ", ".join(sorted(d.value for d in Device))
|
|
308
|
+
raise ValueError(f"Invalid device '{device}'. Expected one of: {valid}")
|
|
309
|
+
device_name = Device[enum_name].value
|
|
310
|
+
if device_name == Device.AUTO.value:
|
|
311
|
+
use_gpu = bool(cuda_available())
|
|
312
|
+
elif device_name in (Device.CUDA.value, Device.TORCH.value):
|
|
313
|
+
use_gpu = True
|
|
314
|
+
else:
|
|
315
|
+
use_gpu = False
|
|
316
|
+
gpu_requested = use_gpu
|
|
317
|
+
|
|
318
|
+
# Detect GPU input
|
|
319
|
+
gpu_input_cupy = False
|
|
320
|
+
gpu_input_torch = False
|
|
321
|
+
if use_gpu:
|
|
322
|
+
try:
|
|
323
|
+
import cupy as cp
|
|
324
|
+
gpu_input_cupy = isinstance(X, cp.ndarray) and isinstance(y, cp.ndarray)
|
|
325
|
+
if sample_weight is not None and not isinstance(sample_weight, cp.ndarray):
|
|
326
|
+
gpu_input_cupy = False
|
|
327
|
+
except Exception:
|
|
328
|
+
pass
|
|
329
|
+
if not gpu_input_cupy:
|
|
330
|
+
try:
|
|
331
|
+
import torch
|
|
332
|
+
gpu_input_torch = isinstance(X, torch.Tensor) and isinstance(y, torch.Tensor)
|
|
333
|
+
if sample_weight is not None and not isinstance(sample_weight, torch.Tensor):
|
|
334
|
+
gpu_input_torch = False
|
|
335
|
+
except Exception:
|
|
336
|
+
pass
|
|
337
|
+
|
|
338
|
+
# Validate inputs
|
|
339
|
+
X_np = None
|
|
340
|
+
y_np = None
|
|
341
|
+
sample_weight_np = None
|
|
342
|
+
|
|
343
|
+
if gpu_input_cupy or gpu_input_torch:
|
|
344
|
+
if len(tuple(X.shape)) != 2:
|
|
345
|
+
raise ValueError("X must be a 2D array")
|
|
346
|
+
n_samples = int(X.shape[0])
|
|
347
|
+
backend = get_backend(backend='auto', device='cuda')
|
|
348
|
+
y_check = backend.asarray(y).reshape(-1)
|
|
349
|
+
if int(y_check.shape[0]) != n_samples:
|
|
350
|
+
raise ValueError("y must have the same number of rows as X")
|
|
351
|
+
else:
|
|
352
|
+
X_np = np.asarray(X, dtype=np.float64)
|
|
353
|
+
y_np = np.asarray(y, dtype=np.float64).reshape(-1)
|
|
354
|
+
if sample_weight is not None:
|
|
355
|
+
sample_weight_np = np.asarray(sample_weight, dtype=np.float64).reshape(-1)
|
|
356
|
+
if X_np.ndim != 2:
|
|
357
|
+
raise ValueError("X must be a 2D array")
|
|
358
|
+
if y_np.shape[0] != X_np.shape[0]:
|
|
359
|
+
raise ValueError("y must have the same number of rows as X")
|
|
360
|
+
n_samples = int(X_np.shape[0])
|
|
361
|
+
|
|
362
|
+
# Default l1_ratios
|
|
363
|
+
if l1_ratios is None:
|
|
364
|
+
l1_ratios_arr = np.asarray([0.2, 0.5, 0.7, 0.8, 0.9, 0.95, 0.99], dtype=np.float64)
|
|
365
|
+
else:
|
|
366
|
+
l1_ratios_arr = np.asarray(l1_ratios, dtype=np.float64)
|
|
367
|
+
l1_ratios_arr = l1_ratios_arr[(l1_ratios_arr >= 0.0) & (l1_ratios_arr <= 1.0)]
|
|
368
|
+
|
|
369
|
+
if l1_ratios_arr.size == 0:
|
|
370
|
+
l1_ratios_arr = np.asarray([0.5], dtype=np.float64)
|
|
371
|
+
|
|
372
|
+
n_l1_ratios = int(l1_ratios_arr.size)
|
|
373
|
+
|
|
374
|
+
# Generate alpha grids for each l1_ratio (use integer index as key)
|
|
375
|
+
alpha_grids = {}
|
|
376
|
+
for l1_idx, l1r in enumerate(l1_ratios_arr):
|
|
377
|
+
if alphas is None:
|
|
378
|
+
if gpu_input_cupy or gpu_input_torch:
|
|
379
|
+
backend = get_backend(backend='torch' if gpu_input_torch else 'cupy', device='cuda')
|
|
380
|
+
alpha_grids[l1_idx] = _default_elasticnet_alpha_grid_backend(
|
|
381
|
+
X, y, backend, l1_ratio=l1r, n_alphas=n_alphas, alpha_min_ratio=alpha_min_ratio
|
|
382
|
+
)
|
|
383
|
+
else:
|
|
384
|
+
alpha_grids[l1_idx] = _default_elasticnet_alpha_grid(
|
|
385
|
+
X_np, y_np, l1_ratio=l1r, n_alphas=n_alphas, alpha_min_ratio=alpha_min_ratio
|
|
386
|
+
)
|
|
387
|
+
else:
|
|
388
|
+
alpha_grid = np.asarray(alphas, dtype=np.float64)
|
|
389
|
+
alpha_grid = alpha_grid[np.isfinite(alpha_grid)]
|
|
390
|
+
alpha_grid = alpha_grid[alpha_grid > 0.0]
|
|
391
|
+
if alpha_grid.size == 0:
|
|
392
|
+
if gpu_input_cupy or gpu_input_torch:
|
|
393
|
+
backend = get_backend(backend='torch' if gpu_input_torch else 'cupy', device='cuda')
|
|
394
|
+
alpha_grids[l1_idx] = _default_elasticnet_alpha_grid_backend(
|
|
395
|
+
X, y, backend, l1_ratio=l1r, n_alphas=n_alphas, alpha_min_ratio=alpha_min_ratio
|
|
396
|
+
)
|
|
397
|
+
else:
|
|
398
|
+
alpha_grids[l1_idx] = _default_elasticnet_alpha_grid(
|
|
399
|
+
X_np, y_np, l1_ratio=l1r, n_alphas=n_alphas, alpha_min_ratio=alpha_min_ratio
|
|
400
|
+
)
|
|
401
|
+
else:
|
|
402
|
+
alpha_grids[l1_idx] = alpha_grid
|
|
403
|
+
|
|
404
|
+
# Handle degenerate cases
|
|
405
|
+
if int(n_samples) < 4 or int(cv_folds) < 2:
|
|
406
|
+
# Use first l1_ratio and first alpha
|
|
407
|
+
l1r0 = float(l1_ratios_arr[0])
|
|
408
|
+
alpha0 = float(alpha_grids[0][0])
|
|
409
|
+
if not return_details:
|
|
410
|
+
return alpha0, l1r0
|
|
411
|
+
details = {
|
|
412
|
+
"alpha": alpha0,
|
|
413
|
+
"l1_ratio": l1r0,
|
|
414
|
+
"alphas": alpha_grids[0].astype(np.float64),
|
|
415
|
+
"l1_ratios": l1_ratios_arr.astype(np.float64),
|
|
416
|
+
"mse_path": np.full((int(n_l1_ratios), int(alpha_grids[0].size), 1), np.nan, dtype=np.float64),
|
|
417
|
+
"mean_mse": np.full((int(n_l1_ratios), int(alpha_grids[0].size)), np.nan, dtype=np.float64),
|
|
418
|
+
}
|
|
419
|
+
return alpha0, l1r0, details
|
|
420
|
+
|
|
421
|
+
# Generate CV folds
|
|
422
|
+
if cv_splits is not None:
|
|
423
|
+
from statgpu.linear_model.wrappers._lasso import _normalize_cv_splits
|
|
424
|
+
folds = _normalize_cv_splits(cv_splits, n_samples=int(n_samples))
|
|
425
|
+
else:
|
|
426
|
+
folds = _kfold_indices(n_samples=int(n_samples), n_splits=int(cv_folds), random_state=random_state)
|
|
427
|
+
|
|
428
|
+
n_folds = int(len(folds))
|
|
429
|
+
|
|
430
|
+
# Auto-cache disabled by default to prevent stale results across datasets.
|
|
431
|
+
cache_key_eff = cache_key
|
|
432
|
+
|
|
433
|
+
cached_result = _elasticnet_cv_cache_get(cache_key_eff)
|
|
434
|
+
if cached_result is not None:
|
|
435
|
+
if return_details:
|
|
436
|
+
return cached_result["alpha"], cached_result["l1_ratio"], cached_result
|
|
437
|
+
return cached_result["alpha"], cached_result["l1_ratio"]
|
|
438
|
+
|
|
439
|
+
# Initialize MSE storage
|
|
440
|
+
# mse_path: (n_l1_ratios, n_alphas, n_folds)
|
|
441
|
+
max_n_alphas = max(len(ag) for ag in alpha_grids.values())
|
|
442
|
+
mse_path = np.full((n_l1_ratios, max_n_alphas, n_folds), np.nan, dtype=np.float64)
|
|
443
|
+
|
|
444
|
+
# Get backend
|
|
445
|
+
if gpu_input_torch:
|
|
446
|
+
backend = get_backend(backend='torch', device='cuda')
|
|
447
|
+
elif gpu_input_cupy:
|
|
448
|
+
backend = get_backend(backend='cupy', device='cuda')
|
|
449
|
+
else:
|
|
450
|
+
backend = get_backend(backend='auto', device='cuda' if use_gpu else 'cpu')
|
|
451
|
+
|
|
452
|
+
xp = backend.xp
|
|
453
|
+
|
|
454
|
+
# Check if we should use warm-start path optimization
|
|
455
|
+
# Warm-start works when: CPU backend, no sample_weight, fit_intercept handled by centering
|
|
456
|
+
use_warm_start = (
|
|
457
|
+
backend.name == 'numpy'
|
|
458
|
+
and not use_gpu
|
|
459
|
+
and sample_weight_np is None
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
# Precompute per-fold data and XtX (independent of l1_ratio)
|
|
463
|
+
fold_data = [] # (X_train, y_train, X_val, y_val, sw_train, sw_val)
|
|
464
|
+
fold_xtx = [] # (XtX_fold, L_fold) for warm-start path
|
|
465
|
+
|
|
466
|
+
for fold_idx, (train_idx, val_idx) in enumerate(folds):
|
|
467
|
+
train_idx_arr = backend.asarray(train_idx)
|
|
468
|
+
val_idx_arr = backend.asarray(val_idx)
|
|
469
|
+
|
|
470
|
+
# Split data
|
|
471
|
+
if gpu_input_cupy or gpu_input_torch:
|
|
472
|
+
X_train_raw = X[train_idx_arr]
|
|
473
|
+
y_train_raw = y[train_idx_arr]
|
|
474
|
+
X_val = X[val_idx_arr]
|
|
475
|
+
y_val = y[val_idx_arr]
|
|
476
|
+
if sample_weight is not None:
|
|
477
|
+
sw_train = sample_weight[train_idx_arr]
|
|
478
|
+
sw_val = sample_weight[val_idx_arr]
|
|
479
|
+
else:
|
|
480
|
+
sw_train = None
|
|
481
|
+
sw_val = None
|
|
482
|
+
X_train = X_train_raw
|
|
483
|
+
y_train = y_train_raw
|
|
484
|
+
else:
|
|
485
|
+
X_train_np = X_np[train_idx]
|
|
486
|
+
y_train_np = y_np[train_idx]
|
|
487
|
+
X_val = backend.asarray(X_np[val_idx])
|
|
488
|
+
y_val = backend.asarray(y_np[val_idx])
|
|
489
|
+
if sample_weight_np is not None:
|
|
490
|
+
sw_train = backend.asarray(sample_weight_np[train_idx])
|
|
491
|
+
sw_val = backend.asarray(sample_weight_np[val_idx])
|
|
492
|
+
else:
|
|
493
|
+
sw_train = None
|
|
494
|
+
sw_val = None
|
|
495
|
+
X_train = X_train_np
|
|
496
|
+
y_train = y_train_np
|
|
497
|
+
|
|
498
|
+
fold_data.append((X_train, y_train, X_val, y_val, sw_train, sw_val))
|
|
499
|
+
|
|
500
|
+
# Precompute XtX and Lipschitz for warm-start path (independent of l1_ratio)
|
|
501
|
+
if use_warm_start:
|
|
502
|
+
if fit_intercept:
|
|
503
|
+
X_mean_fold = np.mean(X_train_np, axis=0)
|
|
504
|
+
y_mean_fold = np.mean(y_train_np)
|
|
505
|
+
Xc = X_train_np - X_mean_fold
|
|
506
|
+
yc = y_train_np - y_mean_fold
|
|
507
|
+
else:
|
|
508
|
+
Xc = X_train_np
|
|
509
|
+
yc = y_train_np
|
|
510
|
+
|
|
511
|
+
XtX_fold = Xc.T @ Xc
|
|
512
|
+
eig_max = np.linalg.eigvalsh(XtX_fold)[-1]
|
|
513
|
+
L_fold = float(eig_max / len(train_idx))
|
|
514
|
+
fold_xtx.append((XtX_fold, L_fold))
|
|
515
|
+
else:
|
|
516
|
+
fold_xtx.append(None)
|
|
517
|
+
|
|
518
|
+
# CV loop: iterate over l1_ratio and folds
|
|
519
|
+
for l1_idx, l1_ratio in enumerate(l1_ratios_arr):
|
|
520
|
+
alpha_grid = alpha_grids[l1_idx]
|
|
521
|
+
n_alphas_this = len(alpha_grid)
|
|
522
|
+
|
|
523
|
+
for fold_idx, (train_idx, val_idx) in enumerate(folds):
|
|
524
|
+
X_train, y_train, X_val, y_val, sw_train, sw_val = fold_data[fold_idx]
|
|
525
|
+
|
|
526
|
+
# For CPU warm-start path: precompute per-fold data to avoid redundant work
|
|
527
|
+
if use_warm_start:
|
|
528
|
+
# The alpha grid should be sorted descending for warm-start to work well
|
|
529
|
+
# (largest alpha first -> sparsest solution -> warm-start to smaller alpha)
|
|
530
|
+
alpha_grid_sorted = np.sort(alpha_grid)[::-1]
|
|
531
|
+
sort_indices = np.argsort(alpha_grid)[::-1]
|
|
532
|
+
|
|
533
|
+
# Sort alpha_grid for warm-start path
|
|
534
|
+
alpha_grid_ws = alpha_grid_sorted
|
|
535
|
+
|
|
536
|
+
# Reuse precomputed XtX and Lipschitz
|
|
537
|
+
XtX_fold, L_fold = fold_xtx[fold_idx]
|
|
538
|
+
|
|
539
|
+
# Fit alphas with warm-start (descending order)
|
|
540
|
+
prev_coef = None
|
|
541
|
+
for alpha_idx_ws, alpha in enumerate(alpha_grid_ws):
|
|
542
|
+
orig_idx = sort_indices[alpha_idx_ws]
|
|
543
|
+
|
|
544
|
+
# Create model with known L to avoid recomputation
|
|
545
|
+
model = ElasticNet(
|
|
546
|
+
alpha=alpha,
|
|
547
|
+
l1_ratio=l1_ratio,
|
|
548
|
+
max_iter=max_iter,
|
|
549
|
+
tol=tol,
|
|
550
|
+
fit_intercept=fit_intercept,
|
|
551
|
+
device='cpu',
|
|
552
|
+
lipschitz_L=L_fold,
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
model.fit(X_train, y_train, initial_coef=prev_coef)
|
|
556
|
+
|
|
557
|
+
# Store result
|
|
558
|
+
mse_val = _batch_mse_cv(
|
|
559
|
+
X_val, y_val,
|
|
560
|
+
model.coef_.reshape(1, -1),
|
|
561
|
+
np.array([model.intercept_]),
|
|
562
|
+
)
|
|
563
|
+
mse_path[l1_idx, orig_idx, fold_idx] = float(mse_val[0])
|
|
564
|
+
prev_coef = model.coef_.copy()
|
|
565
|
+
else:
|
|
566
|
+
# Original approach for GPU or with sample weights
|
|
567
|
+
for alpha_idx, alpha in enumerate(alpha_grid):
|
|
568
|
+
# Convert backend to device string that ElasticNet understands
|
|
569
|
+
if backend.name == 'numpy':
|
|
570
|
+
enet_device = 'cpu'
|
|
571
|
+
elif backend.name == 'cupy':
|
|
572
|
+
enet_device = 'cuda'
|
|
573
|
+
elif backend.name == 'torch':
|
|
574
|
+
enet_device = 'torch'
|
|
575
|
+
else:
|
|
576
|
+
enet_device = 'cpu'
|
|
577
|
+
|
|
578
|
+
model = ElasticNet(
|
|
579
|
+
alpha=alpha,
|
|
580
|
+
l1_ratio=l1_ratio,
|
|
581
|
+
max_iter=max_iter,
|
|
582
|
+
tol=tol,
|
|
583
|
+
fit_intercept=fit_intercept,
|
|
584
|
+
device=enet_device,
|
|
585
|
+
)
|
|
586
|
+
model.fit(X_train, y_train, sample_weight=sw_train)
|
|
587
|
+
|
|
588
|
+
# Compute validation MSE
|
|
589
|
+
mse_val = _batch_mse_cv(
|
|
590
|
+
X_val, y_val,
|
|
591
|
+
model.coef_.reshape(1, -1),
|
|
592
|
+
np.array([model.intercept_]),
|
|
593
|
+
sample_weight=sw_val,
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
mse_path[l1_idx, alpha_idx, fold_idx] = float(mse_val[0])
|
|
597
|
+
|
|
598
|
+
# Compute mean and std MSE across folds
|
|
599
|
+
mean_mse = np.nanmean(mse_path, axis=2) # (n_l1_ratios, n_alphas)
|
|
600
|
+
std_mse = np.nanstd(mse_path, axis=2)
|
|
601
|
+
|
|
602
|
+
# Find best (l1_ratio, alpha) combination
|
|
603
|
+
best_l1_idx, best_alpha_idx = np.unravel_index(np.nanargmin(mean_mse), mean_mse.shape)
|
|
604
|
+
|
|
605
|
+
best_l1_ratio = float(l1_ratios_arr[best_l1_idx])
|
|
606
|
+
best_alpha_grid = alpha_grids[best_l1_idx]
|
|
607
|
+
best_alpha = float(best_alpha_grid[best_alpha_idx])
|
|
608
|
+
best_mse = float(mean_mse[best_l1_idx, best_alpha_idx])
|
|
609
|
+
|
|
610
|
+
# Prepare details
|
|
611
|
+
details = {
|
|
612
|
+
"alpha": best_alpha,
|
|
613
|
+
"l1_ratio": best_l1_ratio,
|
|
614
|
+
"alphas": alpha_grids,
|
|
615
|
+
"l1_ratios": l1_ratios_arr.astype(np.float64),
|
|
616
|
+
"mse_path": mse_path.astype(np.float64),
|
|
617
|
+
"mean_mse": mean_mse.astype(np.float64),
|
|
618
|
+
"std_mse": std_mse.astype(np.float64),
|
|
619
|
+
"best_mse": best_mse,
|
|
620
|
+
"n_folds": n_folds,
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
# Cache result
|
|
624
|
+
if _ELASTICNET_CV_CACHE_MAXSIZE > 0:
|
|
625
|
+
_elasticnet_cv_cache_put(cache_key_eff, details)
|
|
626
|
+
|
|
627
|
+
if return_details:
|
|
628
|
+
return best_alpha, best_l1_ratio, details
|
|
629
|
+
|
|
630
|
+
return best_alpha, best_l1_ratio
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
# =============================================================================
|
|
634
|
+
# ElasticNetCV Class
|
|
635
|
+
# =============================================================================
|
|
636
|
+
|
|
637
|
+
class ElasticNetCV(CVEstimatorBase):
|
|
638
|
+
"""
|
|
639
|
+
Cross-validated Elastic Net regression with GPU support.
|
|
640
|
+
|
|
641
|
+
Elastic Net combines L1 (Lasso) and L2 (Ridge) regularization:
|
|
642
|
+
|
|
643
|
+
minimize (1/(2n)) * ||y - Xw||²₂ + α * l1_ratio * ||w||₁ + 0.5 * α * (1 - l1_ratio) * ||w||²₂
|
|
644
|
+
|
|
645
|
+
This class uses K-fold cross-validation to select the optimal alpha and l1_ratio.
|
|
646
|
+
|
|
647
|
+
Parameters
|
|
648
|
+
----------
|
|
649
|
+
l1_ratio : float or array-like, default=0.5
|
|
650
|
+
L1 regularization ratio. 0.0 = Ridge, 1.0 = Lasso.
|
|
651
|
+
If array-like, CV is performed over all values.
|
|
652
|
+
alphas : array-like or None
|
|
653
|
+
Alpha values to try. If None, generates n_alphas values.
|
|
654
|
+
n_alphas : int, default=100
|
|
655
|
+
Number of alpha values (if alphas is None).
|
|
656
|
+
alpha_min_ratio : float, default=1e-3
|
|
657
|
+
Minimum alpha as a ratio of max alpha.
|
|
658
|
+
cv : int, default=5
|
|
659
|
+
Number of CV folds.
|
|
660
|
+
fit_intercept : bool, default=True
|
|
661
|
+
Whether to fit intercept.
|
|
662
|
+
max_iter : int, default=1000
|
|
663
|
+
Maximum iterations for solver.
|
|
664
|
+
tol : float, default=1e-4
|
|
665
|
+
Convergence tolerance.
|
|
666
|
+
device : str or Device, default=Device.AUTO
|
|
667
|
+
Computation device: 'cpu', 'cuda', or 'auto'.
|
|
668
|
+
compute_inference : bool, default=False
|
|
669
|
+
Whether to compute inference statistics.
|
|
670
|
+
random_state : int or None
|
|
671
|
+
Random seed for CV splits.
|
|
672
|
+
n_jobs : int or None
|
|
673
|
+
Number of parallel jobs (not yet implemented).
|
|
674
|
+
|
|
675
|
+
Attributes
|
|
676
|
+
----------
|
|
677
|
+
alpha_ : float
|
|
678
|
+
Selected alpha value.
|
|
679
|
+
l1_ratio_ : float
|
|
680
|
+
Selected l1_ratio value.
|
|
681
|
+
coef_ : ndarray
|
|
682
|
+
Coefficients of the final model.
|
|
683
|
+
intercept_ : float
|
|
684
|
+
Intercept of the final model.
|
|
685
|
+
cv_results_ : dict
|
|
686
|
+
CV results including mse_path and mean_mse.
|
|
687
|
+
best_score_ : float
|
|
688
|
+
Best (minimum) MSE across CV folds.
|
|
689
|
+
|
|
690
|
+
Examples
|
|
691
|
+
--------
|
|
692
|
+
>>> import numpy as np
|
|
693
|
+
>>> from statgpu.linear_model import ElasticNetCV
|
|
694
|
+
>>> X = np.random.randn(1000, 50)
|
|
695
|
+
>>> y = X @ np.random.randn(50) + 0.1 * np.random.randn(1000)
|
|
696
|
+
>>> model = ElasticNetCV(l1_ratio=[0.2, 0.5, 0.8], cv=5, device='cuda')
|
|
697
|
+
>>> model.fit(X, y)
|
|
698
|
+
>>> print(f"Selected alpha: {model.alpha_:.4f}")
|
|
699
|
+
>>> print(f"Selected l1_ratio: {model.l1_ratio_:.4f}")
|
|
700
|
+
"""
|
|
701
|
+
|
|
702
|
+
def __init__(
|
|
703
|
+
self,
|
|
704
|
+
l1_ratio=0.5,
|
|
705
|
+
*,
|
|
706
|
+
alphas=None,
|
|
707
|
+
n_alphas: int = 100,
|
|
708
|
+
alpha_min_ratio: float = 1e-3,
|
|
709
|
+
cv: int = 5,
|
|
710
|
+
cv_splits=None,
|
|
711
|
+
fit_intercept: bool = True,
|
|
712
|
+
device: Union[str, Device] = Device.AUTO,
|
|
713
|
+
n_jobs: Optional[int] = None,
|
|
714
|
+
compute_inference: bool = False,
|
|
715
|
+
max_iter: int = 1000,
|
|
716
|
+
tol: float = 1e-4,
|
|
717
|
+
random_state: Optional[int] = None,
|
|
718
|
+
):
|
|
719
|
+
super().__init__(
|
|
720
|
+
cv=cv,
|
|
721
|
+
random_state=random_state,
|
|
722
|
+
device=device,
|
|
723
|
+
n_jobs=n_jobs,
|
|
724
|
+
)
|
|
725
|
+
self.l1_ratio = l1_ratio
|
|
726
|
+
self.alphas = alphas
|
|
727
|
+
self.n_alphas = int(n_alphas)
|
|
728
|
+
self.alpha_min_ratio = float(alpha_min_ratio)
|
|
729
|
+
self.cv = int(cv)
|
|
730
|
+
self.cv_splits = cv_splits
|
|
731
|
+
self.fit_intercept = bool(fit_intercept)
|
|
732
|
+
self.compute_inference = bool(compute_inference)
|
|
733
|
+
self.max_iter = int(max_iter)
|
|
734
|
+
self.tol = float(tol)
|
|
735
|
+
|
|
736
|
+
# Output attributes
|
|
737
|
+
self.alpha_ = None
|
|
738
|
+
self.l1_ratio_ = None
|
|
739
|
+
self.coef_ = None
|
|
740
|
+
self.intercept_ = None
|
|
741
|
+
self.cv_results_ = None
|
|
742
|
+
self.best_score_ = None
|
|
743
|
+
self.n_iter_ = None
|
|
744
|
+
self.estimator_ = None
|
|
745
|
+
|
|
746
|
+
def _fit_cv(self, X, y, sample_weight=None):
|
|
747
|
+
"""
|
|
748
|
+
Fit Elastic Net with K-fold cross-validation.
|
|
749
|
+
|
|
750
|
+
Parameters
|
|
751
|
+
----------
|
|
752
|
+
X : array-like
|
|
753
|
+
Design matrix.
|
|
754
|
+
y : array-like
|
|
755
|
+
Response vector.
|
|
756
|
+
sample_weight : array-like, optional
|
|
757
|
+
Sample weights.
|
|
758
|
+
|
|
759
|
+
Returns
|
|
760
|
+
-------
|
|
761
|
+
self
|
|
762
|
+
"""
|
|
763
|
+
compute_device = self._get_compute_device()
|
|
764
|
+
|
|
765
|
+
# Normalize l1_ratio to list
|
|
766
|
+
if isinstance(self.l1_ratio, (list, tuple, np.ndarray)):
|
|
767
|
+
l1_ratios = np.asarray(self.l1_ratio, dtype=np.float64)
|
|
768
|
+
else:
|
|
769
|
+
l1_ratios = np.asarray([self.l1_ratio], dtype=np.float64)
|
|
770
|
+
|
|
771
|
+
# Perform CV
|
|
772
|
+
best_alpha, best_l1_ratio, details = _select_elasticnet_params_cv(
|
|
773
|
+
X, y,
|
|
774
|
+
l1_ratios=l1_ratios,
|
|
775
|
+
alphas=self.alphas,
|
|
776
|
+
n_alphas=self.n_alphas,
|
|
777
|
+
alpha_min_ratio=self.alpha_min_ratio,
|
|
778
|
+
cv_folds=self.cv,
|
|
779
|
+
cv_splits=self.cv_splits,
|
|
780
|
+
random_state=self.random_state,
|
|
781
|
+
sample_weight=sample_weight,
|
|
782
|
+
fit_intercept=self.fit_intercept,
|
|
783
|
+
device=compute_device,
|
|
784
|
+
max_iter=self.max_iter,
|
|
785
|
+
tol=self.tol,
|
|
786
|
+
return_details=True,
|
|
787
|
+
)
|
|
788
|
+
|
|
789
|
+
# Store CV results
|
|
790
|
+
self.alpha_ = best_alpha
|
|
791
|
+
self.l1_ratio_ = best_l1_ratio
|
|
792
|
+
self.cv_results_ = {
|
|
793
|
+
"mse_path": details["mse_path"],
|
|
794
|
+
"mean_mse": details["mean_mse"],
|
|
795
|
+
"std_mse": details["std_mse"],
|
|
796
|
+
"alphas": details["alphas"],
|
|
797
|
+
"l1_ratios": details["l1_ratios"],
|
|
798
|
+
"best_alpha": self.alpha_,
|
|
799
|
+
"best_l1_ratio": self.l1_ratio_,
|
|
800
|
+
}
|
|
801
|
+
# sklearn convention: best_score_ is negative MSE (higher is better)
|
|
802
|
+
self.best_score_ = -float(details["best_mse"])
|
|
803
|
+
|
|
804
|
+
# Fit final model on full data with best parameters
|
|
805
|
+
final_model = ElasticNet(
|
|
806
|
+
alpha=self.alpha_,
|
|
807
|
+
l1_ratio=self.l1_ratio_,
|
|
808
|
+
max_iter=self.max_iter,
|
|
809
|
+
tol=self.tol,
|
|
810
|
+
fit_intercept=self.fit_intercept,
|
|
811
|
+
device=self.device,
|
|
812
|
+
)
|
|
813
|
+
final_model.fit(X, y, sample_weight=sample_weight)
|
|
814
|
+
|
|
815
|
+
self.coef_ = final_model.coef_.copy()
|
|
816
|
+
self.intercept_ = final_model.intercept_
|
|
817
|
+
self.n_iter_ = final_model.n_iter_
|
|
818
|
+
self.estimator_ = final_model
|
|
819
|
+
self._fitted = True
|
|
820
|
+
|
|
821
|
+
return self
|
|
822
|
+
|
|
823
|
+
def fit(self, X, y, sample_weight=None):
|
|
824
|
+
"""
|
|
825
|
+
Fit Elastic Net model with cross-validation.
|
|
826
|
+
|
|
827
|
+
Parameters
|
|
828
|
+
----------
|
|
829
|
+
X : array-like
|
|
830
|
+
Design matrix (n_samples, n_features).
|
|
831
|
+
y : array-like
|
|
832
|
+
Response vector (n_samples,).
|
|
833
|
+
sample_weight : array-like, optional
|
|
834
|
+
Sample weights.
|
|
835
|
+
|
|
836
|
+
Returns
|
|
837
|
+
-------
|
|
838
|
+
self
|
|
839
|
+
"""
|
|
840
|
+
return self._fit_cv(X, y, sample_weight=sample_weight)
|
|
841
|
+
|
|
842
|
+
def predict(self, X):
|
|
843
|
+
"""
|
|
844
|
+
Predict using Elastic Net model.
|
|
845
|
+
|
|
846
|
+
Parameters
|
|
847
|
+
----------
|
|
848
|
+
X : array-like
|
|
849
|
+
Test features.
|
|
850
|
+
|
|
851
|
+
Returns
|
|
852
|
+
-------
|
|
853
|
+
y_pred : ndarray
|
|
854
|
+
Predicted values.
|
|
855
|
+
"""
|
|
856
|
+
if self.coef_ is None:
|
|
857
|
+
raise ValueError("Model not fitted. Call fit() first.")
|
|
858
|
+
|
|
859
|
+
# Delegate to fitted estimator for backend-aware prediction
|
|
860
|
+
if self.estimator_ is not None:
|
|
861
|
+
return self.estimator_.predict(X)
|
|
862
|
+
X_arr = np.asarray(X, dtype=np.float64)
|
|
863
|
+
return X_arr @ self.coef_ + self.intercept_
|
|
864
|
+
|
|
865
|
+
def score(self, X, y):
|
|
866
|
+
"""
|
|
867
|
+
Return R² score.
|
|
868
|
+
|
|
869
|
+
Parameters
|
|
870
|
+
----------
|
|
871
|
+
X : array-like
|
|
872
|
+
Test features.
|
|
873
|
+
y : array-like
|
|
874
|
+
True values.
|
|
875
|
+
|
|
876
|
+
Returns
|
|
877
|
+
-------
|
|
878
|
+
score : float
|
|
879
|
+
R² score.
|
|
880
|
+
"""
|
|
881
|
+
# Delegate to fitted estimator for backend-aware scoring
|
|
882
|
+
if self.estimator_ is not None:
|
|
883
|
+
return self.estimator_.score(X, y)
|
|
884
|
+
y_pred = self.predict(X)
|
|
885
|
+
y_arr = np.asarray(y, dtype=np.float64).reshape(-1)
|
|
886
|
+
|
|
887
|
+
ss_res = np.sum((y_arr - y_pred) ** 2)
|
|
888
|
+
ss_tot = np.sum((y_arr - np.mean(y_arr)) ** 2)
|
|
889
|
+
|
|
890
|
+
if ss_tot == 0.0:
|
|
891
|
+
return 0.0 if ss_res == 0.0 else float('-inf')
|
|
892
|
+
return 1.0 - ss_res / ss_tot
|