Repository navigation
Conversation
…its for all draws, predictions from the coefficients and a vectorized cross-entropy
…for all draws in one C++ call
This branch has not been deployed
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.
This PR was assisted by Claude.
Summary
For
brms::cumulative(),rstanarm::stan_polr()andbrms::categorical()reference models, the augmented-data projection fits each projected draw withMASS::polr()(fit_cumul()) ornnet::multinom()(fit_categ()). Both build the model frame of the augmented data (C copies of the n observations) and minimize the negative log-likelihood with BFGS;polr()evaluates its objective and gradient in R and finds start values withglm.fit(). After each projection,init_submodl()predicts each draw's fit withpredict()(which builds the model frame again) and computes the cross-entropy with a loop over observations and draws. In profiles ofcv_varsel(validate_search = TRUE), the fitter took 87 % of the time for an ordinal model with 6 categories (N 800, 11 predictors) and 82 % for a categorical model with 4 categories (N 400, 20 predictors).This PR has two commits.
cumul_native_multi(), whichdivmin_augdat()calls before the draw-wise loop for cumulative-link submodels without group-level terms, builds the model matrix once and writes the augmented weights of each draw as an n x C matrix of category weights. The new C++ functioncumul_fit_multi_c()then fits all draws.MASS::polr(): its negative log-likelihood, including the truncation of the latent-scale arguments to [-100, 100], and itspfun/dfun. All links ofpolr()are supported.polr()itself fails in such cases with "initial value in 'vmmin' is not finite".cumul_native_multi()returnspolrobjects with the elements thatpredict.polr(),coef(),as.matrix.projection()and the convergence check use.subprd_augdat()first calls the newsubprd_augdat_direct(): forpolrfits (andmultinomfits) of the same submodel, it builds the model matrix fornewdataonce and computes the linear predictors from the coefficients (polr,multinom) instead of onepredict()call per draw followed by a round trip through the probabilities.family$ce_ptwise()of the augmented-data families sums over the categories with onerowSums()call.categ_native_multi()does the same for multinomial-logit submodels without group-level terms, with the new C++ functioncateg_fit_multi_c(). It minimizes the objective ofnnet::multinom()(no weight decay, as infit_categ(); the objective is convex) with Newton steps with the analytic gradient and Hessian, from the intercepts of the model without predictors.nnet::multinom()itself uses BFGS.categ_native_multi()returns copies of onemultinomobject fromnnet::multinom(maxit = 0)per projection, with the weights, objective, deviance, AIC, fitted values, residuals and convergence code of each draw, sopredict.multinom(),coef()andas.matrix.projection()work as before. Withrang = 0,nnet::multinom()draws no random numbers, so the random number stream is unchanged.In both cases,
divmin_augdat()uses the draw-wise fitter as before if...contains arguments thatfit_cumul()/fit_categ()pass on topolr()/optim()/multinom(), if the model frame has offsets or missing values, or if the augmented data do not have the expected layout. The internal optionprojpred.augdat_native = FALSEswitches the native fitters off.Results
The fitters change from BFGS to Newton iterations to convergence, so the submodel fits agree with those of
polr()andmultinom()up to their convergence tolerance. Againstpolr()withreltol = 1e-15, coefficients and thresholds agree within 2e-6; againstmultinom(), the objective was at most 9e-8 lower and the predicted probabilities agreed within 4e-6. The direct predictions differ from the probability-based ones of master by at most 2e-13 (relative), and the cross-entropy is bitwise identical. In the bench below, the rankings, the suggested sizes and the CV ranking proportions are identical to master, and the largest relative difference in the ELPD estimates is 4.5e-6.Bench: winequality-red, random subsample N = 800, 11 standardized predictors, quality with 6 categories (brms
cumulative("logit"), rstanarmstan_polr()); simulated data with 4 categories, N = 400, 20 correlated predictors (brmscategorical()); forward search;vs=varsel()(nterms_max11 or 15),cvt10=cv_varsel(validate_search = TRUE, nloo = 10, refit_prj = TRUE)(nterms_max8 or 10); 1 CPU per run, all versions in the same job on one node (CSC Roihu, AMD EPYC 9965). Times in seconds.cumulative()cumulative()stan_polr()stan_polr()categorical()categorical()After this PR, the C++ fits take most of the time for ordinal models; for categorical models, brms's inverse link (
inv_link_categorical(), called viafamily$linkinv) takes about a third.Tests
The test suite passes with
NOT_CRAN=true: 261 tests, 55 887 expectations, 0 failures, 0 errors, the same 4 warnings as master. Tested with this change applied to master 73cf0ee (projpred 2.11.0.9000), R 4.6.1, MASS 7.3-65, nnet 7.3-20, rstanarm 2.32.2, brms 2.23.0, on CSC Roihu (Linux, AMD EPYC).New tests:
tests/testthat/test_cumul_native.R:cumul_fit_multi_c()gives thepolr()estimates (tolerance 1e-5) and a deviance not above that ofpolr()for the logistic, probit, loglog and cauchit links; for cloglog, the numeric gradient is zero at the fit; several weight matrices in one call give the same fits as one at a time.cumul_native_multi()returnspolrobjects with the names ofpolr(), the same predicted probabilities, a converged state, and a named empty coefficient vector without predictors;NULLforpolr()arguments in..., with the option set toFALSE, and for permuted augmented data.tests/testthat/test_augdat_predict.R:subprd_augdat_direct()gives the linear predictors and dimension names of the probability-based path forpolr(logistic, probit, cloglog; factor predictor; no predictors) andmultinomfits;NULLfor fits of different submodels, mixed classes and other fits;ce_ptwise()gives exactly the values of the loop, also with zero probabilities.tests/testthat/test_categ_native.R:categ_fit_multi_c()gives the estimates of a tightly convergedmultinom()(tolerance 1e-5) and an objective not above it;categ_native_multi()returnsmultinomobjects with the names, levels, objective, predicted probabilities and fitted values ofmultinom(), also without predictors;NULLformultinom()arguments in..., with the option set toFALSE, and for permuted augmented data.The new tests fail on master (the functions do not exist there).
Overlap with other PRs
divmin_augdat(). The second PR to merge needs a rebase. The resolution keeps the removal of the observations with weight 0 from Leave the left-out observation out of the fold projections in validated LOO CV #580 first. The native paths of this PR then get the data without these observations. Both native paths return early, so each of them must set the attributeprojpred_lost_lvlsonoutdminbeforereturn(outdmin), asdivmin_augdat()does at its end. Leave the left-out observation out of the fold projections in validated LOO CV #580 also multiplies the augmented-data weights (projpred_ws_aug) by the observation weights inproj_to_submodl(). The native paths useprojpred_ws_augas their only weights, so they need no change for this part.