Keras models are backed by Python objects managed by TensorFlow/JAX. These objects live in the current R session and are represented as external pointers (externalptr) that become invalid as soon as the session ends, or even within the same session after saveRDS() / readRDS().
kerasnip handles this transparently so that fitted workflows can be saved, reloaded, and used for prediction without any manual restoration steps.
Before discussing the details, here is the full persistence workflow:
library(kerasnip) library(tidymodels) #> ── Attaching packages ───────────────── tidymodels 1.5.0 ── #> ✔ broom 1.0.13 ✔ recipes 1.3.3 #> ✔ dials 1.4.4 ✔ rsample 1.3.2 #> ✔ dplyr 1.2.1 ✔ tailor 0.1.0 #> ✔ ggplot2 4.0.3 ✔ tidyr 1.3.2 #> ✔ infer 1.1.0 ✔ tune 2.1.0 #> ✔ modeldata 1.5.1 ✔ workflows 1.3.0 #> ✔ parsnip 1.6.0 ✔ workflowsets 1.1.1 #> ✔ purrr 1.2.2 ✔ yardstick 1.4.0 #> ── Conflicts ──────────────────── tidymodels_conflicts() ── #> ✖ purrr::discard() masks scales::discard() #> ✖ dplyr::filter() masks stats::filter() #> ✖ parsnip::get_model_env() masks kerasnip::get_model_env() #> ✖ dplyr::lag() masks stats::lag() #> ✖ recipes::step() masks stats::step() library(keras3) #> #> Attaching package: 'keras3' #> The following object is masked from 'package:yardstick': #> #> get_weights #> The following object is masked from 'package:infer': #> #> generate # 1. Define Layer Blocks (Required by kerasnip) # The first block must initialize the sequential model input_block <- function(model, input_shape) { keras_model_sequential(input_shape = input_shape) } # Hidden layer block dense_block <- function(model, units = 32) { model |> layer_dense(units = units, activation = "relu") } # Output layer block (units = 1 for regression) output_block <- function(model, num_classes) { model |> layer_dense(units = 1) } # 2. Generate the parsnip specification create_keras_sequential_spec( model_name = "my_mlp", layer_blocks = list( input = input_block, hidden = dense_block, output = output_block ), mode = "regression" ) # 3. Use the newly created 'my_mlp' function mod_spec <- my_mlp(fit_epochs = 10) |> set_engine("keras") # 4. Standard tidymodels workflow rec_spec <- recipe(mpg ~ ., data = mtcars) |> step_normalize(all_predictors()) fit_wf <- workflow() |> add_recipe(rec_spec) |> add_model(mod_spec) |> fit(data = mtcars) #> 1/1 - 0s - 96ms/step # Predict new_data <- mtcars[1:3, ] predict(fit_wf, new_data) #> 1/1 - 0s - 80ms/step #> # A tibble: 3 × 1 #> .pred #> <dbl> #> 1 2.92 #> 2 2.60 #> 3 6.59
The first call to predict() detects that the Python pointer is invalid and restores the model from the stored bytes automatically.
kerasnip handles persistence automatically:
.kerasformat) and stored alongside the parsnip model_fit object.predict() automatically restores the model from those bytes before dispatching.This means you can use the persistence strategy that best suits your workflow without any extra boilerplate.
saveRDS() / readRDS()For most use cases: sharing a model file with a colleague, caching a fit between R sessions, or checkpointing during development; plain RDS is the simplest approach.
library(kerasnip) library(workflows) library(parsnip) library(recipes) # --- Save --- saveRDS(fit_wf, "my_model.rds") # --- Reload in the same or a new R session --- library(kerasnip) fit_wf <- readRDS("my_model.rds") # predict() restores the Keras model from bytes automatically predictions <- predict(fit_wf, new_data = new_data) #> 1/1 - 0s - 99ms/step predictions #> # A tibble: 3 × 1 #> .pred #> <dbl> #> 1 2.92 #> 2 2.60 #> 3 6.59
There is nothing special to do after readRDS(). The first call to predict() detects the invalid pointer, restores the model from the stored bytes, and then proceeds normally.
bundle / unbundleThe bundle package provides a standardized serialization interface used by vetiver, plumber, and other MLOps tools. It is the right choice when:
vetiver API or a Docker container.library(kerasnip) library(bundle) library(workflows) # --- Save --- bundled <- bundle(fit_wf) saveRDS(bundled, "my_model_bundle.rds") # --- Reload in any R session --- library(kerasnip) library(bundle) bundled <- readRDS("my_model_bundle.rds") fit_wf <- unbundle(bundled) predictions <- predict(fit_wf, new_data = new_data) #> 1/1 - 0s - 101ms/step predictions #> # A tibble: 3 × 1 #> .pred #> <dbl> #> 1 2.92 #> 2 2.60 #> 3 6.59
| | saveRDS / readRDS | bundle / unbundle |
|---|---|---|
| Works across sessions | ✅ | ✅ |
| Works across machines | ✅ (same R library) | ✅ |
| vetiver / Docker compatible | ❌ | ✅ |
| Extra dependency needed | ❌ | bundle package |
| Code complexity | Minimal | Minimal |
When kerasnip fits a model, the generic fit function calls keras_model_to_bytes(), which writes the model to a temporary .keras file using keras3::save_model() and reads the bytes back into R:
# Simplified version of what happens inside generic_sequential_fit() keras_bytes <- keras_model_to_bytes(model) # keras_bytes is a raw vector stored in object$fit$keras_bytes
When predict() is called on a reloaded object, predict.kerasnip_model_fit() runs:
# Simplified version of predict.kerasnip_model_fit() if (!is.null(object$fit$keras_bytes)) { is_valid <- tryCatch( { reticulate::py_validate_xptr(object$fit$fit) TRUE }, error = function(e) FALSE ) if (!is_valid) { object$fit$fit <- keras_model_from_bytes(object$fit$keras_bytes) } }
If keras_model_to_bytes() fails (e.g. if the model was compiled with a non-serialisable custom object), a warning is issued at fit time and keras_bytes is set to NULL. In that case, predict() after reload will fail with a clear error.
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.