Skip to content

Commit 63ffb0f

Browse files
first try at simple CV
1 parent 530c55a commit 63ffb0f

File tree

2 files changed

+176
-0
lines changed

2 files changed

+176
-0
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""
2+
===================================
3+
Cross Validation for Generalized Linear Estimator
4+
===================================
5+
"""
6+
import numpy as np
7+
from sklearn.datasets import make_regression
8+
from skglm.penalties.generalized_linear_cv import GeneralizedLinearEstimatorCV
9+
from skglm.datafits import Quadratic
10+
from skglm.penalties import L1_plus_L2
11+
from skglm.solvers import AndersonCD
12+
13+
14+
X, y = make_regression(n_samples=100, n_features=20, noise=0.1, random_state=42)
15+
16+
estimator = GeneralizedLinearEstimatorCV(
17+
datafit=Quadratic(),
18+
penalty=L1_plus_L2(alpha=1.0, l1_ratio=0.5),
19+
solver=AndersonCD(max_iter=50, tol=1e-4),
20+
cv=6,
21+
)
22+
estimator.fit(X, y)
23+
24+
# Print results
25+
print(f"Best alpha: {estimator.alpha_:.3f}")
26+
print(f"L1 ratio: {estimator.penalty.l1_ratio:.3f}")
27+
print(f"Number of non-zero coefficients: {np.sum(estimator.coef_ != 0)}")
28+
29+
# TODO: add plot
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import numpy as np
2+
from joblib import Parallel, delayed
3+
from sklearn.metrics import get_scorer
4+
from sklearn.utils.extmath import softmax
5+
from skglm.datafits import Logistic, QuadraticSVC
6+
from skglm.estimators import GeneralizedLinearEstimator
7+
8+
9+
def _kfold_split(n_samples, k, rng):
10+
indices = rng.permutation(n_samples)
11+
fold_size = n_samples // k
12+
extra = n_samples % k
13+
14+
start = 0
15+
for i in range(k):
16+
end = start + fold_size + (1 if i < extra else 0)
17+
test = indices[start:end]
18+
train = np.concatenate([indices[:start], indices[end:]])
19+
yield train, test
20+
start = end
21+
22+
23+
class GeneralizedLinearEstimatorCV(GeneralizedLinearEstimator):
24+
"""CV wrapper for GeneralizedLinearEstimator."""
25+
26+
def __init__(self, datafit, penalty, solver, alphas=None, l1_ratio=None,
27+
cv=4, n_jobs=1, random_state=None, scoring=None,
28+
eps=1e-3, n_alphas=100):
29+
super().__init__(datafit=datafit, penalty=penalty, solver=solver)
30+
self.alphas = alphas
31+
self.l1_ratio = l1_ratio
32+
self.cv = cv
33+
self.n_jobs = n_jobs
34+
self.random_state = random_state
35+
self.scoring = scoring
36+
self.eps = eps
37+
self.n_alphas = n_alphas
38+
39+
def _score(self, y_true, y_pred):
40+
"""Compute the loss or performance score (lower is better)."""
41+
if hasattr(self.datafit, "loss"):
42+
return float(np.mean(self.datafit.loss(y_true, y_pred)))
43+
44+
if self.scoring is not None:
45+
if callable(self.scoring):
46+
return -float(self.scoring(y_true, y_pred))
47+
scorer = get_scorer(self.scoring)
48+
return -float(scorer._score_func(y_true, y_pred, **scorer._kwargs))
49+
50+
if isinstance(self.datafit, (Logistic, QuadraticSVC)):
51+
return -float(np.mean(y_true == (y_pred > 0)))
52+
return float(np.mean((y_true - y_pred) ** 2))
53+
54+
def fit(self, X, y):
55+
"""Fit the model using cross-validation."""
56+
if not hasattr(self.penalty, "alpha"):
57+
raise ValueError("'penalty' must expose an 'alpha' hyper-parameter.")
58+
X, y = np.asarray(X), np.asarray(y)
59+
n, p = X.shape
60+
rng = np.random.RandomState(self.random_state)
61+
62+
if self.alphas is not None:
63+
alphas = np.sort(self.alphas)[::-1]
64+
else:
65+
alpha_max = np.max(np.abs(X.T @ y)) / n
66+
alphas = np.logspace(
67+
np.log10(alpha_max),
68+
np.log10(alpha_max * self.eps),
69+
self.n_alphas
70+
)[::-1]
71+
has_l1_ratio = hasattr(self.penalty, "l1_ratio")
72+
l1_ratios = [1.] if not has_l1_ratio else np.atleast_1d(
73+
self.l1_ratio if self.l1_ratio is not None else [1.])
74+
75+
folds = list(_kfold_split(n, self.cv, rng))
76+
n_folds = len(folds)
77+
78+
mse_path = np.empty((len(l1_ratios), len(alphas), n_folds))
79+
best_loss = np.inf
80+
81+
def _solve_fold(k, train, test, alpha, l1, w_start):
82+
pen_kwargs = {k: v for k, v in self.penalty.__dict__.items()
83+
if k not in ("alpha", "l1_ratio")}
84+
if has_l1_ratio:
85+
pen_kwargs['l1_ratio'] = l1
86+
pen = type(self.penalty)(alpha=alpha, **pen_kwargs)
87+
88+
kw = dict(X=X[train], y=y[train], datafit=self.datafit, penalty=pen)
89+
if 'w' in self.solver.solve.__code__.co_varnames:
90+
kw['w'] = w_start
91+
w = self.solver.solve(**kw)
92+
w = w[0] if isinstance(w, tuple) else w
93+
94+
coef, intercept = (w[:p], w[p]) if w.size == p + 1 else (w, 0.0)
95+
96+
y_pred = X[test] @ coef + intercept
97+
return w, self._score(y[test], y_pred)
98+
99+
for idx_ratio, l1_ratio in enumerate(l1_ratios):
100+
warm_start = [None] * n_folds
101+
102+
for idx_alpha, alpha in enumerate(alphas):
103+
fold_results = Parallel(self.n_jobs)(
104+
delayed(_solve_fold)(k, tr, te, alpha, l1_ratio, warm_start[k])
105+
for k, (tr, te) in enumerate(folds)
106+
)
107+
108+
for k, (w_fold, loss_fold) in enumerate(fold_results):
109+
warm_start[k] = w_fold
110+
mse_path[idx_ratio, idx_alpha, k] = loss_fold
111+
112+
mean_loss = np.mean(mse_path[idx_ratio, idx_alpha])
113+
if mean_loss < best_loss:
114+
best_loss = mean_loss
115+
self.alpha_ = float(alpha)
116+
self.l1_ratio_ = float(l1_ratio) if l1_ratio is not None else None
117+
118+
# Refit on full dataset
119+
self.penalty.alpha = self.alpha_
120+
if hasattr(self.penalty, "l1_ratio"):
121+
self.penalty.l1_ratio = self.l1_ratio_
122+
super().fit(X, y)
123+
self.alphas_ = alphas
124+
self.mse_path_ = mse_path
125+
return self
126+
127+
def predict(self, X):
128+
"""Predict using the linear model."""
129+
X = np.asarray(X)
130+
if isinstance(self.datafit, (Logistic, QuadraticSVC)):
131+
return (X @ self.coef_ + self.intercept_ > 0).astype(int)
132+
return X @ self.coef_ + self.intercept_
133+
134+
def predict_proba(self, X):
135+
"""Probability estimates for classification tasks."""
136+
if not isinstance(self.datafit, (Logistic, QuadraticSVC)):
137+
raise AttributeError(
138+
"predict_proba is only available for classification tasks"
139+
)
140+
X = np.asarray(X)
141+
decision = X @ self.coef_ + self.intercept_
142+
decision_2d = np.c_[-decision, decision]
143+
return softmax(decision_2d, copy=False)
144+
145+
def score(self, X, y):
146+
"""Return a 'higher = better' performance metric."""
147+
return -self._score(np.asarray(y), self.predict(X))

0 commit comments

Comments
 (0)