--- title: "tidymodels Integration" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{tidymodels Integration} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} fixture_dir <- "tidymodels" recording <- nzchar(Sys.getenv("FOUNDRY_RECORD_DOCS")) have_fixtures <- dir.exists(fixture_dir) && length(list.files(fixture_dir)) > 0 run_api <- requireNamespace("httptest2", quietly = TRUE) && (recording || have_fixtures) have_tidymodels <- requireNamespace("tidymodels", quietly = TRUE) # Attach foundryR before start_vignette(): httptest2 only sources the package's # inst/httptest2/start-vignette.R (which sets replay placeholders) from attached # packages. library(foundryR) if (run_api) { httptest2::start_vignette(fixture_dir) } knitr::opts_chunk$set( collapse = TRUE, comment = "#>", eval = run_api && have_tidymodels ) ``` ## Introduction foundryR integrates with [tidymodels](https://www.tidymodels.org/) through `step_foundry_embed()`, a recipe step that converts text columns into embedding vectors. Use it when text should enter a model as numeric predictors rather than as bag-of-words counts. ## Why use embeddings in ML pipelines? Traditional text features like bag-of-words or TF-IDF capture word frequencies but miss semantic meaning. Embeddings provide dense vector representations that understand: - **Synonyms**: "happy" and "joyful" produce similar vectors - **Context**: "bank" (financial) vs "bank" (river) are distinguished - **Relationships**: Semantic similarities are preserved in vector space By converting text to embeddings within a recipe, you get: - **Reproducible preprocessing**: The embedding step is part of your documented workflow - **Consistent handling**: Training and test data are processed identically - **Pipeline integration**: Combine embeddings with other preprocessing steps ## Prerequisites Install tidymodels if you haven't already: ```{r install, eval = FALSE} install.packages("tidymodels") ``` Ensure foundryR is configured with your Azure credentials and you have an embedding model deployed. ## Basic Usage ### Creating a Recipe with Embeddings Use `step_foundry_embed()` to add embedding generation to your recipe. Creating the recipe is local and runs when the suggested `tidymodels` package is installed; `prep()` and `bake()` need the recorded API fixtures when rendering: ```{r basic-recipe, eval = have_tidymodels} library(tidymodels) library(foundryR) reviews <- tibble( text = c( "This product is useful and easy to use.", "The setup was confusing and slow.", "The examples were clear and helpful.", "I needed better instructions." ), sentiment = factor(c("positive", "negative", "positive", "negative")) ) recipe_spec <- recipe(sentiment ~ text, data = reviews) %>% step_foundry_embed( text, model = "text-embedding-3-small", keep_original = FALSE ) recipe_spec ``` ### Preparing and Baking the Recipe ```{r prep-bake} prepped_recipe <- prep(recipe_spec, training = reviews) baked_data <- bake(prepped_recipe, new_data = NULL) baked_data ``` The text column is replaced with 1,536 numeric embedding dimensions (the exact number depends on your embedding model). ## Complete ML Pipeline Example Here's a full example building a sentiment classifier. It is shown as code only because fitting and resampling would repeat embedding API calls: ```{r full-pipeline, eval = FALSE} library(tidymodels) library(foundryR) # Load your data set.seed(123) reviews <- tibble( review_text = c( # Positive reviews "Absolutely love this product! Works perfectly.", "Great quality and fast shipping. Very satisfied.", "Best purchase I've made this year. Highly recommend!", "Exceeded all expectations. Will buy again.", "Perfect fit and great value for money.", # Negative reviews "Complete waste of money. Broke after one use.", "Terrible customer service. Never buying again.", "Poor quality, doesn't work as advertised.", "Disappointed. Much smaller than expected.", "Arrived damaged and took forever to ship." ), sentiment = factor(rep(c("positive", "negative"), each = 5)) ) # Split data splits <- initial_split(reviews, prop = 0.8, strata = sentiment) train_data <- training(splits) test_data <- testing(splits) # Define recipe with embeddings embedding_recipe <- recipe(sentiment ~ review_text, data = train_data) %>% step_foundry_embed( review_text, model = "text-embedding-3-small", keep_original = FALSE ) %>% step_normalize(all_numeric_predictors()) # Normalize embedding dimensions # Define model log_reg_spec <- logistic_reg() %>% set_engine("glm") %>% set_mode("classification") # Create workflow sentiment_workflow <- workflow() %>% add_recipe(embedding_recipe) %>% add_model(log_reg_spec) # Fit the model fitted_workflow <- fit(sentiment_workflow, data = train_data) # Make predictions on test data predictions <- predict(fitted_workflow, test_data) %>% bind_cols(test_data) # Evaluate predictions %>% metrics(truth = sentiment, estimate = .pred_class) ``` ## Advanced Options ### Controlling Embedding Dimensions Some models support dimension reduction for faster processing: ```{r dimensions, eval = have_tidymodels} recipe_spec <- recipe(sentiment ~ text, data = reviews) %>% step_foundry_embed( text, model = "text-embedding-3-small", dimensions = 256, # Reduce from 1536 to 256 keep_original = FALSE ) ``` Lower dimensions mean: - Faster model training - Less memory usage - Some loss in semantic precision ### Multiple Text Columns Process multiple text columns independently: ```{r multi-column, eval = have_tidymodels} # Data with multiple text fields data <- tibble( title = c("Great Product", "Terrible Experience"), description = c("Works as expected", "Broke immediately"), outcome = c(1, 0) ) recipe_spec <- recipe(outcome ~ ., data = data) %>% step_foundry_embed(title, model = "text-embedding-3-small", prefix = "title_") %>% step_foundry_embed(description, model = "text-embedding-3-small", prefix = "desc_") %>% step_rm(title, description) # Remove original text columns ``` ### Keeping Original Columns Sometimes you want both the text and embeddings: ```{r keep-original, eval = have_tidymodels} recipe_spec <- recipe(sentiment ~ text, data = reviews) %>% step_foundry_embed( text, model = "text-embedding-3-small", keep_original = TRUE # Keep the text column ) # Useful when you also want to apply other text processing ``` ### Custom Column Prefix Control the naming of embedding columns: ```{r prefix, eval = have_tidymodels} recipe_spec <- recipe(sentiment ~ text, data = reviews) %>% step_foundry_embed( text, model = "text-embedding-3-small", prefix = "embed_" # Columns will be embed_001, embed_002, etc. ) ``` ## Resampling, caching, and cost `step_foundry_embed()` calls the embedding API when a recipe is prepared and when new data is baked. In a resampling workflow, each fold prepares its own recipe. That means the assessment and analysis sets can be embedded repeatedly across folds unless you cache or precompute embeddings. The default, `cache = "none"`, does not read or write a disk cache. To reuse embeddings for the same text, model, and dimensions, opt into `cache = "disk"`. Without an explicit `cache_dir`, the cache stays inside `tempdir()` for the current R session. For a separate temporary workflow, use `cache_dir <- tempfile("foundryR-cache-")`; after the workflow finishes, remove it with `unlink(cache_dir, recursive = TRUE)`. For persistent reuse, choose a directory you intend to keep. `foundry_cache_clear(cache_dir)` removes cached embedding files from that directory. For large or repeated experiments, another option is to embed the text once with `foundry_embed()` or `foundry_embed_batch()`, keep the resulting numeric columns, and resample those embeddings: ```{r precompute} embedded_reviews <- foundry_embed( reviews$text, model = "text-embedding-3-small" ) embedding_matrix <- do.call(rbind, embedded_reviews$embedding) embedding_cols <- as_tibble(embedding_matrix, .name_repair = "unique") precomputed <- bind_cols( reviews["sentiment"], embedding_cols ) precomputed[, 1:4] ``` Use the recipe step when preprocessing needs to be self-contained. Precompute when cost, rate limits, or repeated resampling runs matter more. ## Cross-validation Embeddings are generated during `prep()`, so cross-validation follows the usual tidymodels recipe lifecycle: ```{r cv, eval = FALSE} # Create CV folds folds <- vfold_cv(train_data, v = 5, strata = sentiment) # Fit resamples cv_results <- fit_resamples( sentiment_workflow, resamples = folds, metrics = metric_set(accuracy, roc_auc) ) # Collect metrics collect_metrics(cv_results) ``` ## Hyperparameter tuning Tune the embedding dimensions alongside model hyperparameters: ```{r tuning, eval = FALSE} # Recipe with tunable dimensions tunable_recipe <- recipe(sentiment ~ text, data = train_data) %>% step_foundry_embed( text, model = "text-embedding-3-small", dimensions = tune(), # Will be tuned keep_original = FALSE ) %>% step_normalize(all_numeric_predictors()) # Model with tunable parameters rf_spec <- rand_forest( mtry = tune(), trees = 500, min_n = tune() ) %>% set_engine("ranger") %>% set_mode("classification") # Workflow tunable_workflow <- workflow() %>% add_recipe(tunable_recipe) %>% add_model(rf_spec) # Define grid grid <- grid_regular( dimensions(range = c(128, 512)), # Embedding dimensions mtry(range = c(10, 50)), min_n(range = c(2, 10)), levels = 3 ) # Tune only after estimating the API calls and cost. tune_results <- tune_grid( tunable_workflow, resamples = folds, grid = grid, metrics = metric_set(accuracy, roc_auc) ) # Best parameters show_best(tune_results, metric = "roc_auc") ``` ## Performance considerations ### API Rate Limits Embedding generation makes API calls for each text. For large datasets: 1. Use batch processing with `foundry_embed_batch()` outside the recipe for large training sets. 2. Precompute and store embeddings for frequently used datasets. 3. Reduce cross-validation folds when repeated API calls are not worth the extra precision. ### Cost management Each embedding call incurs API costs. Strategies to manage costs: - Start with smaller dimension sizes during development - Use a subset of data for initial experimentation - Precompute embeddings for production datasets ### Memory usage With 1,536 dimensions per text and thousands of observations, memory can grow quickly: ```{r memory, eval = TRUE} # Estimate memory for 10,000 texts n_texts <- 10000 n_dims <- 1536 bytes_per_double <- 8 memory_mb <- (n_texts * n_dims * bytes_per_double) / 1024^2 print(paste(round(memory_mb), "MB for embeddings alone")) ``` Consider dimension reduction for large datasets. ## Troubleshooting ### "Column already exists" error If you run `prep()` multiple times, column names may conflict: ```{r troubleshoot, eval = have_tidymodels} # Use a unique prefix if reusing recipes recipe_spec <- recipe(sentiment ~ text, data = reviews) %>% step_foundry_embed(text, model = "my-model", prefix = paste0("v", format(Sys.time(), "%H%M%S"), "_")) ``` ### Rate limit errors If you hit rate limits during prep: ```{r rate-limit, eval = FALSE} # Prepare in smaller batches small_sample <- reviews %>% slice_sample(n = 100) prepped <- prep(recipe_spec, training = small_sample) ``` ### Missing credentials Ensure credentials are set before preparing or baking recipes. These configuration and network checks are not run during rendering: ```{r credentials, eval = FALSE} # Check setup foundry_check_setup() # Set credentials if needed. foundry_set_endpoint(Sys.getenv("AZURE_FOUNDRY_ENDPOINT")) foundry_set_key("your-api-key") ``` ## Next steps - Learn about [Text Embeddings](embeddings.html) in depth - Explore [Content Safety](content-safety.html) for responsible AI - Read the [tidymodels documentation](https://www.tidymodels.org/) for more preprocessing options ```{r cleanup, include = FALSE, eval = TRUE} if (run_api) { httptest2::end_vignette() } ```