Repository navigation
Add Scholar.Linear.ElasticNet and Scholar.Linear.Lasso - #372
Merged
josevalim merged 6 commits intoOct 26, 2026
Merged
Conversation
Scholar had no L1 regularized linear model, so there was no way to fit a sparse linear model. ElasticNet minimizes the squared error with a mix of L1 and L2 penalties, controlled by :alpha and :l1_ratio. It is fitted with cyclic coordinate descent, the same algorithm as scikit-learn's enet_coordinate_descent: the same soft-thresholding update per coordinate, the duality gap checked once the largest update of an iteration falls below :tol, and the fit stopped once the gap is below :tol times ||y||^2. On the diabetes data the f64 coefficients match scikit-learn 1.6.1 to about 1e-14 after the same number of iterations. The scalar hyperparameters are passed as options instead of numerical definition arguments, because a number given as an argument becomes an f32 tensor and an f64 fit would see alpha 0.1 as 0.10000000149. The struct also returns dual_gap and num_iterations, the equivalents of scikit-learn's dual_gap_ and n_iter_, since a defn cannot warn when the fit stops before converging.
Lasso is the elastic net with l1_ratio 1.0, so it reuses the coordinate descent of ElasticNet, the same way scikit-learn's Lasso subclasses its ElasticNet. It has its own options, without :l1_ratio, and its own struct, so predict/2 and the documentation stay specific to it.
The weights follow scikit-learn: they are rescaled to sum to the number of samples, so the penalties keep their scale and only the ratios between weights matter, then the data is centered with the weighted means and multiplied by the square root of the weights. The weights are validated in the transform and passed to the numerical definition as an argument, not inside the options, so they become a parameter of the compiled function instead of a constant inlined into it.
Constrains the coefficients to be non-negative, as scikit-learn's positive=True: a coordinate whose correlation with the residual is negative is set to zero, and the duality gap uses the one-sided dual norm, the maximum of X^T R instead of its maximum absolute value. The option is named :positive? to follow the other boolean options of Scholar.Linear, such as :fit_intercept?.
Targets of shape {num_samples, num_targets} now give coefficients of
shape {num_targets, num_features}, plus one intercept, duality gap and
iteration count per target, the same layout as RidgeRegression and
scikit-learn.
The coordinate descent solves all targets together, one column each.
scikit-learn fits them one at a time, so each target is frozen once it
converges: its coefficients, gap and iteration count stop changing while
the other targets keep iterating. Each target therefore stops at the same
iteration as when it is fitted alone, which the tests check against
scikit-learn.
josevalim
approved these changes
Oct 26, 2026
Contributor
|
Thank you!!!!!! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #362.
Problem
Scholar.Linearhas ordinary least squares, ridge, logistic, Bayesian ridge, isotonic and polynomial regression, but noScholar.Optimizeapplies either: bfgs, brent, golden_section and nelder_mead all assume a smooth objective, and theSolution
Two new models, both fitted with cyclic coordinate descent.
Scholar.Linear.ElasticNetminimizeswhere$\rho$ is
:l1_ratio.Scholar.Linear.Lassois the same model withl1_ratio: 1.0, so it reuses the solver, the way scikit-learn'sLassosubclasses itsElasticNet. It keeps its own options (without:l1_ratio) and its own struct, sopredict/2and the documentation stay specific to it.The solver follows scikit-learn's$||y||^2_2$ .
enet_coordinate_descent: the same soft-thresholding update per coordinate, the duality gap computed once the largest update of an iteration falls below:tolrelative to the largest coefficient, and the fit stopped once that gap is below:toltimesOptions
:alphaand:l1_ratio- regularization strength and the:fit_intercept?,:max_iterations,:tol.:sample_weights- rescaled to sum to the number of samples, so the penalties keep their scale and only the ratios between weights matter, then the data is centered with the weighted means and multiplied by the square root of the weights. Same as scikit-learn.:positive?- constrains the coefficients to be non-negative, as scikit-learn'spositive=True. Named with the?suffix to follow the other boolean options ofScholar.Linear, such as:fit_intercept?.Return values
Besides
:coefficientsand:intercept, the struct carries:dual_gapand:num_iterations, the equivalents of scikit-learn'sdual_gap_andn_iter_, since adefncannot warn when the fit stops before converging.Targets of shape
{num_samples, num_targets}give coefficients of shape{num_targets, num_features}, plus one intercept, duality gap and iteration count per target, the same layout asScholar.Linear.RidgeRegression. The coordinate descent solves all targets together, one column each. scikit-learn fits them one at a time, so each target is frozen once it converges: its coefficients, gap and iteration count stop changing while the other targets keep iterating. Each target therefore stops at the same iteration as when it is fitted alone.Implementation notes
The scalar hyperparameters are passed as options rather than as numerical definition arguments, because a number given as an argument becomes an f32 tensor and an f64 fit would then see
alpha: 0.1as0.10000000149.:sample_weights, which can be a tensor, is passed as an argument so it becomes a parameter of the compiled function instead of a constant inlined into it.Validation
Checked against scikit-learn 1.6.1 (
precompute=False) on 240+ randomly generated f64 problems, varying the number of samples and features,:alpha,:l1_ratio,:fit_intercept?,:positive?,:sample_weights, the number of targets,:max_iterations,:tol, and including constant, all-zero and nearly collinear columns. Coefficients and intercepts agree to about 1e-13 and the iteration counts are identical.The tests cover this against the diabetes data and smaller fixtures, plus two checks that do not depend on scikit-learn:
l1_ratio: 0.0reproducesScholar.Linear.RidgeRegressionandalpha: 0.0reproducesnumpy.linalg.lstsq.Known limitation
With
l1_ratio: 0.0the duality gap used as the stopping rule degenerates, so the fit runs until:max_iterations. This is inherited from the algorithm and happens in scikit-learn too; the:l1_ratiodocumentation points atScholar.Linear.RidgeRegressionfor that case, the way the:alphadocumentation already points atScholar.Linear.LinearRegressionforalpha: 0.0.