Fix glmnet lambda handling: predict-time fallback and fit-time cold-start convergence failure - #41
Open
ielbadisy wants to merge 4 commits into
Open
Fix glmnet lambda handling: predict-time fallback and fit-time cold-start convergence failure#41ielbadisy wants to merge 4 commits into
ielbadisy wants to merge 4 commits into
Conversation
predict_xy fell back to state$state$lambda[1] when spec$lambda was not passed at prediction time. glmnet orders its default regularization path largest-to-smallest, so lambda[1] is the *most* regularized value, not the tuned/intended one -- silently collapsing predictions toward an intercept-only model whenever a learner was fit with the default full path (lambda = NULL). Discovered via a downstream analysis pipeline where an Elastic Net learner produced a single constant predicted probability for every test row (AUROC exactly 0.5, sensitivity 0), traced to this fallback. Fix: record the lambda actually used at fit_xy time (either spec$lambda, or the smallest/least-regularized value in the default path) and always reuse that recorded value at predict_xy time, rather than re-deriving it from spec or guessing from the fitted path. Adds a regression test that fits without spec$lambda and asserts predictions are non-degenerate; confirmed this test fails (AUROC = 0.50) against the pre-fix code and passes after the fix.
The predict_xy fallback fixed in the previous commit was necessary but not sufficient: fit_xy itself requested a single lambda directly in glmnet::glmnet(..., lambda = spec$lambda), which cold-starts the coordinate-descent solver without a warm-start path. On the real downstream dataset that surfaced this bug (CLAVUS, ~35 raw predictors expanding to ~150+ columns after one-hot encoding of several high-cardinality categorical variables), this failed to converge for alpha=0.5, lambda=0.01 and glmnet silently returned an empty, all-zero model (fit$lambda == Inf), which predict_xy then dutifully used to produce a constant probability for every row (AUROC exactly 0.5). fit_xy now always fits the full regularization path (no lambda= passed to glmnet::glmnet directly); the intended lambda is recorded from spec and extracted via predict(..., s=) at predict_xy time, which is the supported, warm-started way to get predictions at an arbitrary point on the path. Verified against the real dataset that triggered this: the previously degenerate Elastic Net learner now reaches AUROC 0.781 / AUPRC 0.681 through funcml::tune() + predict(), matching a glmnet::cv.glmnet() reference fit on the same data exactly. Also strengthens the regression test to assert the fit_xy invariant directly (fit$state$state$lambda has length > 1, i.e. a full path was computed) rather than only checking predict_xy output, since the convergence failure is data-dependent and did not reproduce reliably in small synthetic examples.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… DESCRIPTION The hand-written CITATION was pinned to an old version and had no URL. Dropping it lets R build the citation from DESCRIPTION automatically, which stays in sync and includes the CRAN URL. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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.
Summary
Two related bugs in the
glmnetlearner, both surfaced by the same downstream failure: an Elastic Net learner in a real analysis pipeline (CLAVUS, VUS-reclassification prediction) produced a single constant predicted probability for every test row (AUROC exactly 0.5, sensitivity 0).1.
predict_xylambda fallback. Ifspec$lambdawasn't threaded through topredict_xy, it fell back tostate$state$lambda[1]-- the largest (most-regularized) value in glmnet's default path, not the tuned/intended one.2.
fit_xycold-lambda convergence failure (the actual root cause on real data).fit_xyrequested a single lambda directly viaglmnet::glmnet(..., lambda = spec$lambda), cold-starting the coordinate-descent solver with no warm-started path. On a wide/high-cardinality design matrix (~35 raw predictors, ~150+ columns after one-hot encoding several categorical variables with many levels), this failed to converge for the CV-selectedalpha=0.5, lambda=0.01and glmnet silently returned an empty, all-zero-coefficient model (fit$lambda == Inf), which then predicted the same constant probability for every observation -- independent of thepredict_xyfix in (1).Fix (1) alone was not sufficient to resolve the real pipeline's degenerate result; fix (2) was required and is the one that actually restores correct behavior on that data.
Fix
fit_xynow always fits the full regularization path (never passes a barelambda=toglmnet::glmnet()).spec$lambda, or the smallest/least-regularized path value if unset) is recorded at fit time and extracted viapredict(..., s=)at predict time -- the supported, warm-started way to get predictions at an arbitrary path point.Verification
funcml::tune()+predict()now reaches AUROC 0.781 / AUPRC 0.681 through the standard funcml path, exactly matching an independentglmnet::cv.glmnet()reference fit (s = 0.01) on the identical training/test data.tests/testthat/test-glmnet-predict-lambda.R: 3 tests -- non-degenerate predictions whenspec$lambdais unset at fit time, correct lambda reuse whenspec$lambdais set, and a direct structural check thatfit_xyalways produces a multi-point path (length(fit$state$state$lambda) > 1) rather than a single cold-fit lambda.tests/testthat/test-learner-audit-contract.R(glmnet fit/predict contract) andtest-estimate*.R(which also exercise glmnet as a meta-learner) still pass.Bumped to 0.7.3, NEWS.md updated with both fixes.