--- title: "Using textrecipes as a sparse matrix engine" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Using textrecipes as a sparse matrix engine} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r} #| include: false knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` ```{r} #| label: setup library(textrecipes) library(recipes) ``` This vignette walks through getting a document-term matrix, such as one to hand to glmnet, xgboost, a clustering routine, or your own code, straight out of a recipe. Build a recipe, `prep()` it on your training text, then `bake()` with `composition = "dgCMatrix"`. # Creating a sparse matrix We will use a handful of short documents. ```{r} train <- tibble::tibble( text = c( "the cat sat on the mat", "the dog chased the cat", "dogs and cats are pets", "a mat is a small rug" ) ) new_docs <- tibble::tibble( text = c("the cat chased a small dog", "a rug for the mat") ) ``` Set `sparse = "yes"` in the step to get sparse output. ```{r} rec <- recipe(~text, data = train) |> step_tokenize(text) |> step_tfidf(text, sparse = "yes") |> prep() ``` Baking the training data with `composition = "dgCMatrix"` returns a sparse matrix from the **Matrix** package, one row per document and one column per token. ```{r} x_train <- bake(rec, new_data = NULL, composition = "dgCMatrix") x_train ``` The prepped recipe remembers the vocabulary and the inverse document frequencies learned from the training data, so new documents are mapped onto the same columns. ```{r} x_new <- bake(rec, new_data = new_docs, composition = "dgCMatrix") x_new identical(colnames(x_train), colnames(x_new)) ``` Tokens that weren't seen during `prep()` are dropped, so the columns always line up between the training and new data. The result is a regular `dgCMatrix`, which is the format that many R packages take as input. ```{r} #| eval: false # Fit a lasso model with glmnet fit <- glmnet::glmnet(x_train, y_train, family = "binomial") predict(fit, newx = x_new) ``` # Steps that produce sparse data These steps have a `sparse` argument, which needs to be set to `"yes"` to get sparse output: - `step_tf()`: term frequencies - `step_tfidf()`: term frequency-inverse document frequency - `step_texthash()`: feature hashing of tokens - `step_dummy_hash()`: feature hashing of nominal variables # Controlling the number of columns A vocabulary can grow very large, so you will often want to put a cap on the number of columns. There are two options. Use `step_tokenfilter()` to keep only the most frequent tokens. ```{r} #| eval: false recipe(~text, data = large_corpus) |> step_tokenize(text) |> step_tokenfilter(text, max_tokens = 5000) |> step_tf(text, sparse = "yes") |> prep() |> bake(new_data = NULL, composition = "dgCMatrix") ``` Or use feature hashing with `step_texthash()`. The number of columns is fixed up front with `num_terms`, no matter how large the vocabulary is, and no vocabulary has to be stored. This makes it a good match for sparse output when the vocabulary is too big to hold in memory. ```{r} #| eval: false recipe(~text, data = large_corpus) |> step_tokenize(text) |> step_texthash(text, num_terms = 4096, sparse = "yes") |> prep() |> bake(new_data = NULL, composition = "dgCMatrix") ``` `step_dummy_hash()` does the same for nominal predictors with many levels. # Using workflows instead If you are fitting a model with tidymodels, you don't need to set `sparse` yourself. A workflow turns it on for models that support sparse data. See `?sparse_data` in the **recipes** package for details.