-
Notifications
You must be signed in to change notification settings - Fork 42
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
Refactor of get_tune_schedule()
#978
base: tune-schedule
Are you sure you want to change the base?
Conversation
(note that otherwise the testing pane in Positron doesn't work)
|
||
# ------------------------------------------------------------------------------ | ||
get_param_info <- function(wflow) { | ||
param_info <- tune_args(wflow) %>% |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using tune_args()
here instead of a parameter set object, due to considerations I've put in #974 (comment)
mod_tune_bst <- boost_tree(trees = tune(), min_n = tune(), mode = "regression") | ||
mod_tune_rf <- rand_forest(min_n = tune(), mode = "regression") | ||
mod_tune_bst <- parsnip::boost_tree(trees = tune(), min_n = tune(), mode = "regression") | ||
mod_tune_rf <- parsnip::rand_forest(min_n = tune(), mode = "regression") | ||
|
||
if (rlang::is_installed("probably")) { | ||
|
||
adjust_tune_min <- |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that we usually use rec
in the name of recipes objects, I would like to advocate for calling tailor objects something with tailor
rather than adjust_
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Going to hold off on a proper review until I can carve out a solid chunk of time, but re:
I've made a PR into tune-schedule so that you can see the diffs to the previous version clearly. I understand that branch to be our place to work things out, so I'm happy to make a separate PR into main if we are happy with how get_tune_schedule() looks. I think the tests could also make use of that separation of the stages into smaller scheduling functions, but I didn't do this here because I wanted you to see how the tests changed for this refactor.
Totally makes sense, thanks! I'm definitely on board for the workflow of taking chunks of that PR and refactoring + reviewing more in-detail and then sending those smaller portions into main
as we do so.
Just eyeballing the diffs, it looks like this PR still makes use of the UseSpacesForTab: No
setting. I see that it probably makes sense to keep that setting around to prevent conflicts with—and more easily diff against—tune-schedule
, but I'd advocate for reverting back to UseSpacesForTab: Yes
and reformatting the smaller chunks at some point before we send them into main
. I can imagine a couple different ways that workflow could look (wait to reformat, merge into tune-schedule
, reformat that whole PR to line up with the rest of the repo, extract out the relevant bits and merge to main
?), but whatever results in the least work for the implementer has a thumbs-up from me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very readable, very concise. A huge step up from compute_grid_info()
or any of its refactors. Got a lot of joy from reviewing this one—bravo to yall!
+1 to working in some tests at the level of the newly separate functions, but fine with me to wait for a separate PR to make that happen.
Huzzah🙆
suppressPackageStartupMessages(library(tailor)) | ||
suppressPackageStartupMessages(library(purrr)) | ||
suppressPackageStartupMessages(library(dplyr)) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think my usual Do-Repeat-Yourself-ery would have these library calls within each test_that()
chunk, though I know that's probably not the standard set in other places in this repo
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Repeating the library calls in each test_that()
expression is actually my least favorite approach to this problem 😅
Because:
At least when executing such test code in the active console, the library calls have a lasting effect - but are less visible for quick cross-file scanning when they (unintentionally) affect something elsewhere. It could be that testthat sandboxes this behaviour but I think it might not.
library(testthat)
test_that("tune function is not found", {
expect_error(
tune_grid(),
"could not find"
)
})
#> Test passed 🎊
test_that("find it with library", {
library(tune)
expect_error(
tune_grid(),
"The first argument"
)
})
#> Test passed 🌈
test_that("tune function is still not found", {
expect_error(
tune_grid(),
"could not find"
)
})
#> ── Failure: tune function is still not found ───────────────────────────────────
#> `tune_grid()` threw an error with unexpected message.
#> Expected match: "could not find"
#> Actual message: "The first argument to [tune_grid()] should be either a model or workflow."
#> Backtrace:
#> ▆
#> 1. ├─testthat::expect_error(tune_grid(), "could not find")
#> 2. │ └─testthat:::quasi_capture(...)
#> 3. │ ├─testthat (local) .capture(...)
#> 4. │ │ └─base::withCallingHandlers(...)
#> 5. │ └─rlang::eval_bare(quo_get_expr(.quo), quo_get_env(.quo))
#> 6. ├─tune::tune_grid()
#> 7. └─tune:::tune_grid.default()
#> 8. └─rlang::abort(msg)
#> Error:
#> ! Test failed
Created on 2025-01-21 with reprex v2.1.0
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I see. Dang, doesn't seem like there's a great option then. Feel free to keep as-is. :)
schedule <- grid %>% | ||
tidyr::nest(.by = all_of(param_pre_stage), .key = "model_stage") | ||
|
||
# schedule next stages recursively |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apologies if this feels like a nit, but I struggled to wrap my head around this code a bit longer than I might've otherwise trying to work this comment into my mental model—is there actually any recursion happening in this code?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A) > hit me with your nits
B) Not a nit, but a valuable comment! You're right, I supposed it's not quite the right word. What would you call it? Something with "nested"? Or just "schedule next stages within `schedule_model_stage_i()"? I just want to give the reader a heads-up that all stages will be taken care of, even though you can only "see" scheduling the immediate next stage from that point in the code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I see what you're trying to call out! Maybe "nested iteration"? Or possibly just point out "each model stage will also iterate across predict and post stages"
# schedule model parameters | ||
schedule <- min_model_grid(model_stage, model_param, wflow) | ||
|
||
# push remaining paramters into the next stage |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
# push remaining paramters into the next stage | |
# push remaining parameters into the next stage |
|
||
# ------------------------------------------------------------------------------ | ||
# Merge in submodel fit value (if any) | ||
schedule_model_stage_i <- function(model_stage, param_info, wflow){ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is so so smart and i love it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Max worked it out and now I got to let it shine 😄
tidyr::unnest(post_stage), | ||
grid_nada | ||
) | ||
expect_equal(nrow(sched_nada), 0) | ||
|
||
expect_s3_class( | ||
sched_nada, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't comment on the line directly since it's unchanged in this PR, but tidy style would include no trailing newline within the test_that({})
expression
schedule_stages <- function(grid, param_info, wflow) { | ||
# schedule preprocessing stage and push the rest into a nested tibble | ||
param_pre_stage <- param_info %>% | ||
filter(source == "recipe") %>% |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
filter(source == "recipe") %>% | |
dplyr::filter(source == "recipe") %>% |
# schedule preprocessing stage and push the rest into a nested tibble | ||
param_pre_stage <- param_info %>% | ||
filter(source == "recipe") %>% | ||
pull(id) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pull(id) | |
dplyr::pull(id) |
|
||
# schedule next stages recursively | ||
schedule %>% | ||
mutate( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mutate( | |
dplyr::mutate( |
# Merge in submodel fit value (if any) | ||
schedule_model_stage_i <- function(model_stage, param_info, wflow){ | ||
model_param <- param_info %>% | ||
filter(source == "model_spec") %>% |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
filter(source == "model_spec") %>% | |
dplyr::filter(source == "model_spec") %>% |
schedule_model_stage_i <- function(model_stage, param_info, wflow){ | ||
model_param <- param_info %>% | ||
filter(source == "model_spec") %>% | ||
pull(id) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pull(id) | |
dplyr::pull(id) |
filter(source == "model_spec") %>% | ||
pull(id) | ||
non_submodel_param <- param_info %>% | ||
filter(source == "model_spec" & !has_submodel) %>% |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
filter(source == "model_spec" & !has_submodel) %>% | |
dplyr::filter(source == "model_spec" & !has_submodel) %>% |
pull(id) | ||
non_submodel_param <- param_info %>% | ||
filter(source == "model_spec" & !has_submodel) %>% | ||
pull(id) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pull(id) | |
dplyr::pull(id) |
|
||
# push remaining paramters into the next stage | ||
next_stage <- model_stage %>% | ||
tidyr::nest(.by = all_of(non_submodel_param), .key = "predict_stage") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tidyr::nest(.by = all_of(non_submodel_param), .key = "predict_stage") | |
tidyr::nest(.by = dplyr::all_of(non_submodel_param), .key = "predict_stage") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, I think that you get it at this point. This is all just protection for being invoked inside of worker processes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for all the code suggestions! I find it totally okay for the reviewer to do one example and then hand it back to whoever opened the PR.
About the worker process: Do we expect to execute get_tune_schedule()
in parallel? My understanding is that we first schedule and then send todos off to workers, i.e. we would not expect to call get_tune_schedule()
in parallel, no?
~.x %>% dplyr::group_nest(!!!symbs$sub, .key = "post_stage") | ||
) | ||
) | ||
schedule_stages <- function(grid, param_info, wflow) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need param_info
as an argument? Since it is created by get_param_info()
, we could call that immediately with wflow
.
if (any(param$source == "tailor")) { | ||
post_id <- param$id[param$source == "tailor"] | ||
og_cls <- class(schedule) | ||
if (nrow(param) == 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a little gross to code here but this class structure feels like a good solution (until we know that it is not)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, since I forgot to say it in the review... this looks great. Big improvement on may refactor.
Here's the refactor of
get_tune_schedule()
! The basic idea is to schedule the stages recursively, starting at the preprocessing stage down to the postprocessing stage, and always do one stage at a time, pushing the remaining parameters into a nested tibble.I've made a PR into
tune-schedule
so that you can see the diffs to the previous version clearly. I understand that branch to be our place to work things out, so I'm happy to make a separate PR intomain
if we are happy with howget_tune_schedule()
looks. I think the tests could also make use of that separation of the stages into smaller scheduling functions, but I didn't do this here because I wanted you to see how the tests changed for this refactor.The refactor leads to 0-row tibbles when there are no tuning parameters at all (which we discussed in the team meeting) and small changes in the order of the columns. The ordering of the rows (for preprocessing) also stays the same now between the ingoing grid and the outgoing schedule.
Since this is the second round of working over this scheduling function, no need to review "only" high-level, hit me with your nits so that this part is ready for main!