tune tune_sim_anneal() in R: Simulated Annealing Tuning
The tune tune_sim_anneal() function in R, from the finetune package, runs iterative simulated annealing over a tidymodels workflow, proposing a neighbor candidate each round and accepting or rejecting it by metric improvement plus a temperature-controlled probability.
tune_sim_anneal(wf, resamples = folds) # default iter = 10 from random initial tune_sim_anneal(wf, resamples = folds, iter = 30) # longer search tune_sim_anneal(wf, resamples = folds, initial = init_res) # warm start from tune_grid() result tune_sim_anneal(wf, resamples = folds, initial = 6) # space-filling initial design size tune_sim_anneal(wf, resamples = folds, metrics = mset) # custom metric set tune_sim_anneal(wf, resamples = folds, control = ctrl_sa) # control_sim_anneal(): cooling, radius tune_sim_anneal(wf, resamples = folds, param_info = params) # custom parameter ranges autoplot(res, type = "performance") # trace metric across iterations
Need explanation? Read on for examples and pitfalls.
What tune_sim_anneal() does in one sentence
tune_sim_anneal() walks the parameter space one neighbor at a time. You give it a workflow with tune() placeholders, a resample object, and an iteration budget. The function scores a small initial design (or accepts a prior tune_grid() result), then at each iteration picks a neighbor of the current best candidate inside a shrinking radius. If the neighbor scores better the algorithm moves there; if worse, it still moves with a probability that depends on how far the metric dropped and the current temperature. The temperature cools each iteration, so the walk explores broadly at first and refines locally near the end.
The function lives in the finetune package, which ships separately from tidymodels core. Install it once with install.packages("finetune") and load alongside tidymodels.
Set up a tunable workflow
You need the same three pieces every iterative tuner expects: spec, recipe, and resamples. Simulated annealing benefits most from continuous parameters with broad ranges, since neighbor proposals draw from a Gaussian centered on the current point.
Five folds is a reasonable floor here because every iteration of the annealer is one full resample cycle. Many folds plus many iterations gets expensive fast.
tune_sim_anneal() syntax and arguments
The signature mirrors tune_bayes(); the annealing knobs live in control_sim_anneal().
| Argument | Description |
|---|---|
object |
A workflow or a model spec. If a spec, pass preprocessor next. |
resamples |
An rset such as vfold_cv(). Each iteration scores one candidate on every fold. |
iter |
Number of annealing steps after the initial design. 20 to 40 is the common range. |
initial |
Integer = space-filling design of that size; a tune_results object = warm start. |
metrics |
A metric_set(); the FIRST metric drives the acceptance decision. |
control |
A control_sim_anneal() object with cooling_coef, radius, flip, no_improve, restart, verbose_iter. |
metric_set(rmse, rsq, mae) anneals on RMSE; the others ride along for diagnostics. Put the target metric first.The control knobs that matter most:
cooling_coef = 0.02: how fast the temperature falls. Smaller = slower cooling, more exploration.radius = c(0.05, 0.15): neighborhood radius range as a fraction of the parameter range. Wider = bolder jumps.no_improve = 10L: iterations without improvement before stopping early.restart = 8L: iterations without improvement before jumping back to the best-so-far point.verbose_iter = FALSE: set TRUE to log every accept and reject.
Examples by use case
Warm-start from a small tune_grid() result; the surrogate-free walk pays off when you know roughly where to look.
show_best(), select_best(), collect_metrics(), and finalize_workflow() work on the result exactly as they do for tune_grid(). Iterations 0 are the initial design rows; iterations 1 to iter are the annealed proposals.
To watch the search trajectory across iterations, plot it with autoplot().
tune_sim_anneal() versus alternatives
Pick by how smooth your metric surface looks and how cheap your fits are.
| Function | When to reach for it |
|---|---|
tune_sim_anneal() |
Smooth metric surface, continuous params, you want local refinement from a good starting point. Sequential, one candidate per iteration. |
tune_bayes() |
Continuous params, expensive fits, no good starting point. The Gaussian process surrogate proposes globally. |
tune_grid() |
Small fixed candidate list, or downstream stacking that needs every candidate scored on every fold. |
finetune::tune_race_anova() |
Large fixed grid, runtime per fit matters. Drops losing candidates with a parametric test instead of searching. |
Annealing returns an iteration_results object with the same shape as a tune_bayes() output. Downstream helpers like select_best(), finalize_workflow(), and last_fit() treat the two interchangeably.
Common pitfalls
Three issues account for most disappointing annealing runs.
- No warm start. Calling with
initial = 1starts from a single random point and burns iterations climbing out of nowhere. Pass a smalltune_grid()result viainitial = init_resso the walk begins in a sensible region. - Too aggressive cooling. A large
cooling_coefcools the temperature fast, so the walk freezes before it explores. Drop tocooling_coef = 0.01when iterations end clustered at the starting point. - Discrete or categorical heavy params. Annealing proposes Gaussian neighbors, then snaps to the nearest legal value for discrete dials. Heavily discrete grids defeat the neighbor proposal logic; reach for
tune_grid()ortune_race_anova()instead.
set.seed() ahead of tune_sim_anneal(), two runs on the same data produce different traces and different best candidates.Try it yourself
Try it: Anneal a knn classifier on iris with 5-fold cross-validation, 15 iterations, and accuracy as the metric. Warm-start from a 4-point tune_grid() and pick the best neighbors value.
Click to reveal solution
Explanation: The four-point initial grid gives the annealer a sense of the metric landscape across neighbors. From there it proposes Gaussian neighbors rounded to integers and accepts or rejects each by the temperature schedule. Fifteen iterations is enough on iris because the surface across neighbors is smooth.
Related tidymodels functions
Annealing sits in the iterative tuners corner of the tidymodels family.
tune_grid()for the unraced, unannealed baseline.tune_bayes()for surrogate-driven global search.finetune::tune_race_anova()andfinetune::tune_race_win_loss()for early-stopping racing on fixed grids.control_sim_anneal()to set cooling_coef, radius, no_improve, restart, and verbose_iter.autoplot()withtype = "performance"for the iteration-by-iteration trace.select_best(),finalize_workflow(),last_fit()to lock in the survivor on full train and test.
External reference: the finetune package documentation at finetune.tidymodels.org.
FAQ
How is tune_sim_anneal() different from tune_bayes()?
tune_bayes() fits a Gaussian process surrogate to past metric values and proposes the next candidate by maximizing an acquisition function across the parameter space. tune_sim_anneal() ignores the global surrogate; it picks a Gaussian neighbor of the current best, then accepts or rejects by metric improvement plus a temperature-controlled probability. Annealing is cheaper per iteration and shines on smooth landscapes. Bayes is more sample-efficient but pays the surrogate-fitting cost each round.
When should I use tune_sim_anneal() versus tune_race_anova()?
Racing assumes a fixed grid and scores the survivors efficiently. Annealing proposes candidates dynamically as the search proceeds. Use racing when you can enumerate the candidates and runtime per fit dominates. Use annealing when the parameter space is continuous and you want to refine around a starting point.
Do I need a warm start with initial = tune_results?
It is not required, but it is the single biggest leverage point for a productive anneal. Starting with initial = 1 means the first proposal walks away from a single random point, which wastes early iterations climbing out of an arbitrary location. A 4 to 6 point tune_grid() result gives the annealer a sense of the landscape, so the temperature-controlled walk concentrates the iteration budget on improving rather than discovering.
What control_sim_anneal() defaults should I tune first?
no_improve is the highest-leverage knob. Default 10 stops the search after ten unproductive iterations; lower it for fast experiments, raise it when fits are cheap and you want to extract every drop. restart = 8 jumps the walk back to the best-so-far point after eight stale iterations, which helps escape shallow local optima. Leave cooling_coef = 0.02 unless iterations end clustered at the start (drop it) or end scattered far from the best (raise it).
Can I parallelize tune_sim_anneal()?
You can parallelize the resample evaluations inside each iteration, but the iterations themselves are sequential by design. Register a parallel backend with library(doFuture); plan(multisession, workers = 4) before the call. Each iteration then fans out the folds across workers, fits them in parallel, and the annealer waits for the fold metrics to make its accept-or-reject decision before proposing the next neighbor.