Skip to content

Commit 9a76df6

Browse files
authored
Merge pull request #33 from davidrsch/tune_autoplot_doc
Add vignette on tuning multiple similar parameters for autoplot uniqueness
2 parents 3571233 + 2bba440 commit 9a76df6

File tree

7 files changed

+289
-41
lines changed

7 files changed

+289
-41
lines changed

R/dials.R

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#' Dials Parameter for Keras Optimizers
2+
#' @param values A character vector of possible optimizers. Defaults to all
3+
#' known optimizers (keras defaults + custom registered).
4+
#' @keywords internal
5+
#' @export
6+
#' @return A `dials` parameter object for Keras optimizers.
7+
optimizer_function <- function(values = NULL) {
8+
if (is.null(values)) {
9+
values <- unique(c(
10+
keras_optimizers,
11+
names(.kerasnip_custom_objects$optimizers)
12+
))
13+
}
14+
dials::new_qual_param(
15+
type = "character",
16+
values = values,
17+
label = c(optimizer_function = "Optimizer Function"),
18+
finalize = NULL
19+
)
20+
}
21+
22+
#' Dials Parameter for Keras Loss Functions
23+
#' @param values A character vector of possible loss functions. Defaults to all
24+
#' known losses (keras defaults + custom registered).
25+
#' @keywords internal
26+
#' @export
27+
#' @return A `dials` parameter object for Keras loss.
28+
loss_function_keras <- function(values = NULL) {
29+
if (is.null(values)) {
30+
values <- unique(c(keras_losses, names(.kerasnip_custom_objects$losses)))
31+
}
32+
dials::new_qual_param(
33+
type = "character",
34+
values = values,
35+
label = c(loss_function_keras = "Loss Function"),
36+
finalize = NULL
37+
)
38+
}

R/utils.R

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -142,45 +142,6 @@ get_keras_object <- function(
142142
name
143143
}
144144

145-
#' Dials Parameter for Keras Optimizers
146-
#' @param values A character vector of possible optimizers. Defaults to all
147-
#' known optimizers (keras defaults + custom registered).
148-
#' @keywords internal
149-
#' @export
150-
#' @return A `dials` parameter object for Keras optimizers.
151-
optimizer_function <- function(values = NULL) {
152-
if (is.null(values)) {
153-
values <- unique(c(
154-
keras_optimizers,
155-
names(.kerasnip_custom_objects$optimizers)
156-
))
157-
}
158-
dials::new_qual_param(
159-
type = "character",
160-
values = values,
161-
label = c(optimizer_function = "Optimizer Function"),
162-
finalize = NULL
163-
)
164-
}
165-
166-
#' Dials Parameter for Keras Loss Functions
167-
#' @param values A character vector of possible loss functions. Defaults to all
168-
#' known losses (keras defaults + custom registered).
169-
#' @keywords internal
170-
#' @export
171-
#' @return A `dials` parameter object for Keras loss.
172-
loss_function_keras <- function(values = NULL) {
173-
if (is.null(values)) {
174-
values <- unique(c(keras_losses, names(.kerasnip_custom_objects$losses)))
175-
}
176-
dials::new_qual_param(
177-
type = "character",
178-
values = values,
179-
label = c(loss_function_keras = "Loss Function"),
180-
finalize = NULL
181-
)
182-
}
183-
184145
#' Process Predictor Input for Keras (Functional API)
185146
#'
186147
#' @description

_pkgdown.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ guides:
2222
- workflows_sequential
2323
- workflows_functional
2424
- tuning_fit_compile_args
25+
- applications
26+
- autoplot_uniqueness
2527

2628
# examples:
2729

@@ -92,6 +94,8 @@ navbar:
9294
- text: "Tuning"
9395
- text: "Tuning Fit and Compile Arguments"
9496
href: articles/tuning_fit_compile_args.html
97+
- text: "Ensuring autoplot Uniqueness"
98+
href: articles/autoplot_uniqueness.html
9599
- text: "Applications"
96100
- text: "Transfer Learning"
97101
href: articles/applications.html

man/loss_function_keras.Rd

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/optimizer_function.Rd

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
test_that("autoplot works with multiple hidden units parameters", {
2+
skip_if_no_keras()
3+
skip_if_not_installed("ggplot2")
4+
5+
# 1. Define a spec with multiple hidden unit parameters
6+
model_name <- "autoplot_spec"
7+
on.exit(suppressMessages(remove_keras_spec(model_name)), add = TRUE)
8+
create_keras_sequential_spec(
9+
model_name = model_name,
10+
layer_blocks = list(
11+
input = function(model, input_shape) {
12+
keras3::keras_model_sequential(input_shape = input_shape)
13+
},
14+
dense1 = function(model, units = 10) {
15+
model |> keras3::layer_dense(units = units)
16+
},
17+
dense2 = function(model, units = 10) {
18+
model |> keras3::layer_dense(units = units)
19+
},
20+
output = function(model, num_classes) {
21+
model |>
22+
keras3::layer_dense(units = num_classes, activation = "softmax")
23+
}
24+
),
25+
mode = "classification"
26+
)
27+
28+
tune_spec <- autoplot_spec(
29+
dense1_units = tune(id = "denseone"),
30+
dense2_units = tune(id = "densetwo")
31+
) |>
32+
set_engine("keras")
33+
34+
# 2. Set up workflow and tuning grid
35+
rec <- recipes::recipe(Species ~ ., data = iris)
36+
tune_wf <- workflows::workflow(rec, tune_spec)
37+
38+
params <- tune::extract_parameter_set_dials(tune_wf)
39+
40+
# The user code should not need to change.
41+
# `hidden_units` will be `kerasnip::hidden_units` which auto-detects the id.
42+
params <- params |>
43+
update(
44+
denseone = hidden_units(range = c(4L, 8L)),
45+
densetwo = hidden_units(range = c(4L, 8L))
46+
)
47+
params$name
48+
params$id
49+
params$source
50+
params$component
51+
params$component_id
52+
params$object
53+
54+
grid <- dials::grid_regular(params, levels = 2)
55+
control <- tune::control_grid(save_pred = FALSE, verbose = FALSE)
56+
57+
# 3. Run tuning
58+
tune_res <- tune::tune_grid(
59+
tune_wf,
60+
resamples = rsample::vfold_cv(iris, v = 2),
61+
grid = grid,
62+
control = control
63+
)
64+
65+
# 4. Assert that autoplot works without error
66+
expect_no_error(
67+
ggplot2::autoplot(tune_res)
68+
)
69+
})

vignettes/autoplot_uniqueness.Rmd

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
---
2+
title: "Tuning Multiple Similar Parameters: Ensuring `autoplot` Uniqueness"
3+
output: rmarkdown::html_vignette
4+
vignette: >
5+
%\VignetteIndexEntry{Tuning Multiple Similar Parameters: Ensuring `autoplot` Uniqueness}
6+
%\VignetteEngine{knitr::rmarkdown}
7+
%\VignetteEncoding{UTF-8}
8+
---
9+
10+
```{r setup, include = FALSE}
11+
knitr::opts_chunk$set(
12+
collapse = TRUE,
13+
comment = "#>",
14+
eval = reticulate::py_module_available("keras") &&
15+
requireNamespace("keras3", quietly = TRUE) &&
16+
requireNamespace("ggplot2", quietly = TRUE) &&
17+
requireNamespace("tune", quietly = TRUE) &&
18+
requireNamespace("dials", quietly = TRUE) &&
19+
requireNamespace("parsnip", quietly = TRUE) &&
20+
requireNamespace("workflows", quietly = TRUE) &&
21+
requireNamespace("recipes", quietly = TRUE) &&
22+
requireNamespace("rsample", quietly = TRUE)
23+
)
24+
```
25+
26+
## Introduction
27+
28+
When using `kerasnip` to define and tune Keras models within the `tidymodels` framework, you might encounter situations where you want to tune multiple parameters that, by default, map to the same underlying `dials` parameter type. A common example is tuning the number of units in multiple `layer_dense` blocks within the same model.
29+
30+
While `kerasnip` intelligently maps these parameters (e.g., `dense1_units` and `dense2_units` both map to `dials::hidden_units()`), this can lead to ambiguity when visualizing tuning results with `ggplot2::autoplot()`. Without a way to distinguish between these otherwise identical parameter types, `autoplot()` may produce errors or misleading plots.
31+
32+
This vignette demonstrates how to explicitly provide unique identifiers to your tuned parameters, ensuring `autoplot()` can correctly visualize the results for each distinct parameter.
33+
34+
## The Problem (Implicit)
35+
36+
Consider a model with two dense layers, each with a `units` parameter. If you were to define them for tuning without unique `id`s, `autoplot()` would encounter an issue because it cannot distinguish between the two parameters.
37+
38+
For example, if you were to run `ggplot2::autoplot(tune_res)` without unique `id`s, you might encounter an error similar to this:
39+
40+
```{r}
41+
#> Error in `dplyr::rename()`:
42+
#> ! Names must be unique.
43+
#> ✖ These names are duplicated:
44+
#> * "# Hidden Units" at locations 1 and 2.
45+
```
46+
47+
This error clearly indicates that `autoplot()` is trying to rename columns for plotting, but it finds duplicate names like "# Hidden Units" because both `dense1_units` and `dense2_units` are generically identified as `hidden_units` by `dials` without further distinction. This makes it impossible for `autoplot()` to differentiate their tuning results.
48+
49+
## The Solution: Using Unique `id`s with `tune()`
50+
51+
The solution is to provide a unique `id` argument to the `tune()` function for each parameter you wish to distinguish.
52+
53+
Let's define a simple sequential Keras model with two dense layers:
54+
55+
```{r model_definition}
56+
library(kerasnip)
57+
library(keras3)
58+
library(parsnip)
59+
library(dials)
60+
library(workflows)
61+
library(recipes)
62+
library(rsample)
63+
library(tune)
64+
library(ggplot2)
65+
66+
# Define a spec with multiple hidden unit parameters
67+
model_name <- "autoplot_unique_spec"
68+
# Clean up the spec if it already exists from a previous run
69+
if (exists(model_name, mode = "function")) {
70+
suppressMessages(remove_keras_spec(model_name))
71+
}
72+
73+
input_block <- function(model, input_shape) {
74+
keras3::keras_model_sequential(input_shape = input_shape)
75+
}
76+
77+
dense_block <- function(model, units = 10) {
78+
model |> keras3::layer_dense(units = units)
79+
}
80+
81+
output_block <- function(model, num_classes) {
82+
model |>
83+
keras3::layer_dense(units = num_classes, activation = "softmax")
84+
}
85+
86+
create_keras_sequential_spec(
87+
model_name = model_name,
88+
layer_blocks = list(
89+
input = input_block,
90+
dense1 = dense_block,
91+
dense2 = dense_block,
92+
output = output_block
93+
),
94+
mode = "classification"
95+
)
96+
97+
# Now, create the model specification and assign unique IDs for tuning
98+
tune_spec <- autoplot_unique_spec(
99+
dense1_units = tune(id = "dense_layer_one_units"),
100+
dense2_units = tune(id = "dense_layer_two_units")
101+
) |>
102+
set_engine("keras")
103+
104+
print(tune_spec)
105+
```
106+
107+
Notice how `dense1_units` and `dense2_units` are both passed to `tune()`, but each is given a distinct `id`. This `id` acts as a label that `autoplot()` can use to differentiate the parameters.
108+
109+
### Setting up the Tuning Workflow
110+
111+
Next, we'll set up a `tidymodels` workflow, define the parameter ranges, and create a tuning grid.
112+
113+
```{r tuning_setup}
114+
# Set up workflow and tuning grid
115+
rec <- recipes::recipe(Species ~ ., data = iris)
116+
tune_wf <- workflows::workflow(rec, tune_spec)
117+
118+
params <- tune::extract_parameter_set_dials(tune_wf)
119+
120+
# Update the parameter ranges using kerasnip::hidden_units
121+
# The `id`s provided in tune() are automatically detected and used here.
122+
params <- params |>
123+
update(
124+
dense_layer_one_units = hidden_units(range = c(4L, 8L)),
125+
dense_layer_two_units = hidden_units(range = c(4L, 8L))
126+
)
127+
128+
grid <- dials::grid_regular(params, levels = 2)
129+
control <- tune::control_grid(save_pred = FALSE, verbose = FALSE)
130+
131+
print(grid)
132+
```
133+
134+
### Running the Tuning Process
135+
136+
Now, we run `tune::tune_grid` to perform the actual tuning. For demonstration purposes, we'll use a small number of resamples and a simple dataset.
137+
138+
```{r run_tuning}
139+
# Run tuning
140+
tune_res <- tune::tune_grid(
141+
tune_wf,
142+
resamples = rsample::vfold_cv(iris, v = 2),
143+
grid = grid,
144+
control = control
145+
)
146+
147+
print(tune_res)
148+
```
149+
150+
### Visualizing Results with `autoplot()`
151+
152+
With the tuning complete, we can now use `ggplot2::autoplot()` to visualize the results. Because we provided unique `id`s, `autoplot()` can correctly generate separate plots for each tuned parameter.
153+
154+
```{r autoplot_results, fig.width=7, fig.height=5}
155+
# Assert that autoplot works without error
156+
ggplot2::autoplot(tune_res)
157+
```
158+
159+
As you can see, `autoplot()` successfully generates a plot showing the performance across the different values for `dense_layer_one_units` and `dense_layer_two_units` independently.
160+
161+
## Why Unique `id`s are Necessary
162+
163+
Internally, `kerasnip` maps arguments like `units` from your `layer_blocks` functions to appropriate `dials` parameter objects (e.g., `dials::hidden_units()`). When multiple such arguments exist, they all point to the *same type* of `dials` parameter.
164+
165+
The `id` argument in `tune()` serves as a unique identifier that `tune` and `ggplot2::autoplot()` use to distinguish between different instances of these parameter types. Without it, `autoplot()` would see multiple parameters of type `hidden_units` and wouldn't know how to plot them separately, leading to errors or combining them incorrectly.
166+
167+
## Best Practices
168+
169+
* **Always use unique `id`s:** When tuning multiple parameters that are conceptually similar (e.g., `units` in different layers, `rate` in different dropout layers), always provide a descriptive and unique `id` to the `tune()` function.
170+
* **Descriptive `id`s:** Choose `id`s that clearly indicate which part of the model the parameter belongs to (e.g., `dense_layer_one_units`, `conv_filter_size`). This improves readability and understanding of your tuning results.
171+
172+
By following this practice, you ensure that your `kerasnip` models are robustly tunable and that their results can be clearly visualized using the `tidymodels` ecosystem.
173+
174+
```{r cleanup, include=FALSE}
175+
suppressMessages(remove_keras_spec(model_name))
176+
```

0 commit comments

Comments
 (0)