Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

subsampling LOO estimates with diff-est-srs-wor start #496

Open
wants to merge 96 commits into
base: master
Choose a base branch
from

Conversation

avehtari
Copy link
Collaborator

@avehtari avehtari commented Apr 10, 2024

Work in progress.

Tested with

  • family="normal" and stats %in% c("elpd", "mlpd", "gmpd", "mse", "rmse")
  • family="bernoulli" and stats %in% c("acc","pctcorr")
  • family="poisson" usings trad and latent approaches
  • tests pass expect some plot tests fail with svglite: ... Graphics API version mismatch

Notes

  • n_loo matters only if validate_search = TRUE (with FALSE there is no speed advantage)
  • in cv_varsel if nloo<n and fast PSIS-LOO result is not yet available, fast PSIS-LOO result is computed
  • in cv_varsel if nloo<n, fast PSIS-LOO result is stored in slot $summaries_fast
  • the subsampling indices are stored in slot $loo_inds
  • the actual subsampling estimating happens in summary_funs.R get_stat()
  • removed some NA checking and need to recheck if those need to put back
  • improved quantiles for "mse" if the value is close to 0 (can't get negative lq)
  • "auc" is not supported (complicated)

Next

  • add support for incrementally increasing nloo?

tagging @n-kall

@avehtari avehtari requested a review from fweber144 April 12, 2024 13:03
@avehtari
Copy link
Collaborator Author

avehtari commented Apr 15, 2024

I wanted to use R2, and as I had rewrote summary stats anyway, added R2 and made all mse, rmse, and R2 to use only normal approximation with as much shared computation as possible

With the added R2 support, this PR will close also #483

@n-kall
Copy link
Collaborator

n-kall commented Apr 16, 2024

I can take a look

@n-kall n-kall self-assigned this Apr 16, 2024
@avehtari avehtari requested a review from n-kall April 16, 2024 13:25
@avehtari avehtari assigned avehtari and unassigned n-kall Apr 16, 2024
R/cv_varsel.R Outdated Show resolved Hide resolved
R/cv_varsel.R Outdated Show resolved Hide resolved
R/cv_varsel.R Outdated Show resolved Hide resolved
R/cv_varsel.R Outdated Show resolved Hide resolved
R/glmfun.R Show resolved Hide resolved
R/misc.R Show resolved Hide resolved
@fweber144
Copy link
Collaborator

fweber144 commented Apr 21, 2024

I have added some comments, but I'm not done with the review yet.

Besides, I think documentation needs to be updated (at least re-roxygenized, but also progressr should be mentioned), the vignettes perhaps as well, and I haven't run R CMD check (including the unit tests) yet. The NEWS file would also need to be updated.

@fweber144 fweber144 mentioned this pull request Apr 21, 2024
new column behavior of `summary.vsel()`, and the omittance of option `"best"` of
argument `baseline`
@fweber144
Copy link
Collaborator

Commit a7458b9 reverts the changes with respect to the new "mixed deltas" variant of plot.vsel(), the new column behavior of summary.vsel(), and the omittance of option "best" of argument baseline (the latter is now re-introduced, but disallowed for subsampled LOO-CV).

Now I still need to do points 1 and 3 from #496 (comment) and, as mentioned in #496 (comment), some other things are still missing (documentation, vignettes, R CMD check (including unit tests), NEWS file), apart from open discussions here.

@fweber144
Copy link
Collaborator

Commit a7458b9 reverts the changes with respect to the new "mixed deltas" variant of plot.vsel(), the new column behavior of summary.vsel(), and the omittance of option "best" of argument baseline (the latter is now re-introduced, but disallowed for subsampled LOO-CV).

Branch mixed_deltas_plot holds the state before reverting those things. I'll try to merge any future commits that are unrelated to those features into mixed_deltas_plot.

@avehtari
Copy link
Collaborator Author

A question related to your comment here: Is it mentioned in Magnusson et al. (2020) that the variances from eq. (8) and eq. (9) need to be summed? I couldn't find such a statement at the first glance, although it would make sense.

It is not mentioned there, but it is required to get the appropriate variance for the plots. I first followed the paper, and then the variance can be really small, and then thinking it through realized that at least in our use case we need to add them together.

@avehtari
Copy link
Collaborator Author

btw. I had kept loo_inds for future use. Eventually it would be useful to be able to run sub-sampling-LOO first with e.g. nloo=50, and then add another 50 if the variances are too big.

@fweber144
Copy link
Collaborator

btw. I had kept loo_inds for future use. Eventually it would be useful to be able to run sub-sampling-LOO first with e.g. nloo=50, and then add another 50 if the variances are too big.

Makes sense. Did I break something with respect to loo_inds? If yes, that was not intentional.

@avehtari
Copy link
Collaborator Author

Makes sense. Did I break something with respect to loo_inds? If yes, that was not intentional.

It's not used, but I thought it's better to keep it there until the incremental sub-sampling is supported, and then decide whether to keep depending on how the incremental sub-sampling is implemented.

@fweber144
Copy link
Collaborator

It's not used

Yes, it wasn't, but in commit 19948e3, I changed that (note that there is also a fixup commit: 519dac2). I think using loo_inds in get_stat() is safer than relying on NAs (remember how hard a time we had finding out under which circumstances NAs can occur).

subsampled LOO (`n_loo < n_full`) and everything else (`n_loo == n_full`)
does not change from definition of `var_mse_e` to use of `var_mse_e`, the extra
definition of `var_mse_e` can be avoided
`is.null(summaries_baseline)`); one reason is that I'm not sure whether it was
supposed to read `mu_baseline <- y` in that case (instead of `mu_baseline <- 0`)
# simple transformation of mse
value <- sqrt(mse_e) - ifelse(is.null(summaries_baseline), 0, sqrt(mse_b))
# the first-order Taylor approximation of the variance
value_se <- sqrt(value_se^2 / mse_e / 4)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If is.null(summaries_baseline) (i.e., deltas = FALSE), this line should be correct. But if !is.null(summaries_baseline) (i.e., deltas = TRUE), isn't mse_b also a random variable that needs to be taken into account? That would require a bivariate (instead of scalar) delta method (which is not a problem, because the delta method has a straightforward multivariate extension). What I mean is that the function for which the Taylor series approximation is made would then be $f(x_1, x_2) = \sqrt{x_1} - \sqrt{x_2}$ with gradient $\nabla f(x_1, x_2) = (\frac{1}{2 \sqrt{x_1}}, -\frac{1}{2 \sqrt{x_2}})^T$.

value.se <- weighted.sd((mu - y)^2 - (mu.bs - y)^2, wcv,
na.rm = TRUE) /
sqrt(n_notna)
# Use normal approximation for mse and delta method for rmse and R2
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this supposed to refer to the standard error estimation method or the CI method? The first part ("normal approximation") refers to a CI method, but the second part ("delta method") to a standard error estimation method.

Comment on lines +502 to +503
# Compute mean and variance in log scale by matching the variance of a
# log-normal approximation
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it common to assume a log-normal distribution as the sampling distribution of the MSE and RMSE estimators? I haven't seen that yet (I think), but it might be perfectly fine (motivated by the central limit theorem, I guess).

# store for later calculations
mse_e <- value
if (!is.null(summaries_baseline)) {
# delta=TRUE, variance of difference of two normally distributed
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is something missing at the end; perhaps "random variables"?

((mu_baseline - y)^2 - mse_b))[loo_inds],
y_idx = loo_inds,
w = wobs)
cov_mse_e_b <- srs_diffe$y_hat / n_full^2
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for my understanding: This procedure for estimating the covariance between mse_e and mse_b assumes that the summands within mse_e and mse_b coming from different observations are uncorrelated, right? At first, I thought this was violated here because mu, mu_baseline, and summaries_fast$mu are model-based and hence there is potential for cross-observations dependencies, but then I realized that mu, mu_baseline, and summaries_fast$mu all are based on the leave-one-out principle, so is this the reason why we can assume a cross-observation correlation of zero here?

@avehtari
Copy link
Collaborator Author

When I changed several bootstraps to analytic approximations and improved other approximations, I thought the math I was writing in the code was so trivial that I didn't write all the derivations and assumptions separately. Now I see I should have done that, as it takes also me a bit of time to re-check any of these when you ask a question, so they are not as obvious as I thought them to be. If you like, I can some day write the equations and assumptions for easier checking. Before that, at least every approximation I wrote is based on the tests at least as accurate as the earlier bootstrap, but much faster.

mse_y <- mean(wobs * (mean(y) - y)^2)
value <- 1 - mse_e / mse_y - ifelse(is.null(summaries_baseline), 0, 1 - mse_b / mse_y)
# the first-order Taylor approximation of the variance
var_mse_y <- .weighted_sd((mean(y) - y)^2, wobs)^2 / n_full
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For mean(y), don't we need to take wobs into account? I think this is similar to line var_mse_b <- .weighted_sd((mu_baseline - y)^2, wobs)^2 / n_full where the parameter estimates from which mu_baseline is computed also take wobs into account.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also concerns several other occurrences of mean(y) here in get_stat().

@fweber144 fweber144 mentioned this pull request Sep 2, 2024
Comment on lines +387 to +393
if (!is.null(summaries_baseline)) {
# delta=TRUE
mse_e <- mse_e - mse_b
}
value_se <- sqrt((value_se^2 -
2 * mse_e / mse_y * cov_mse_e_y +
(mse_e / mse_y)^2 * var_mse_y) / mse_y^2)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If my understanding from stan-dev/loo#205 (comment) is correct, then I think we would need a trivariate delta method in the !is.null(summaries_baseline) case (because mse_b comes in, too). I haven't checked whether such a trivariate delta method would give the same formula as used here. Have you checked this?

@fweber144
Copy link
Collaborator

The tests run if I change in setup.R

nobsv <- 41L

to

nobsv <- 43L

but then of course all the results change. But this shows it's a problem with the small nobsv and random data

For me,

nobsv <- 43L

does not work (runs into some error, similarly as nobsv <- 41L did for you). However,

nobsv <- 39L

works for me. Does it work for you as well (on master)? Then I would pick that for the time being. Ultimately, it would be desirable to completely revise the tests because currently, we mainly test the "big" user-level functions, with the tests for all the "small" internal functions being quite short or not existing at all. The principle of testing should rather be to test the underlying functions extensively, because then it is easier to keep the tests for the "big" (and hence slow) user-level functions short.

@avehtari
Copy link
Collaborator Author

avehtari commented Sep 6, 2024

With nobsv <- 39L I get [ FAIL 0 | WARN 902 | SKIP 2 | PASS 60545 ]

@fweber144
Copy link
Collaborator

With nobsv <- 39L I get [ FAIL 0 | WARN 902 | SKIP 2 | PASS 60545 ]

That sounds good. The warnings probably arise from the first creation of the snapshots. If you are running the tests via R CMD check, then from the second run on, you can avoid these warnings by (locally) removing the entry ^tests/testthat/bfits$ from the .Rbuildignore file. For the two skips, I don't know where they are coming from, but they are probably due to a suggested package that is not installed.

Since this solution seems to be working for you, I will push a commit to master (and merge it here) that changes nobsv to 39. As mentioned above, this is only a quick workaround.

This fixes commit 0d73c8e. However, before
commit 0d73c8e, `is.null(mu_baseline)` should
have never occurred because if `summaries_baseline` was `NULL`, then
`mu_baseline` was set to `0` (and if `summaries_baseline` was not `NULL`, then
`mu_baseline` was set to `summaries_baseline$mu` which should not be `NULL`
either). Hence, this fixup here does not only fix commit
0d73c8e, but also the incorrect behavior which
existed before it.
# log-normal approximation
# https://en.wikipedia.org/wiki/Log-normal_distribution#Arithmetic_moments
mul <- log(value^2 / sqrt(value_se^2 + value^2))
varl <- log(1 + value_se^2 / value^2)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to use log1p() here (for numerical stability)?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants