Tutorial 2: Build a classifier
Source:vignettes/tutorial-build-classifier.Rmd
tutorial-build-classifier.RmdYou will build a sentiment classifier and run it three ways: on one
text, on a vector of texts, and on a data frame with
run_dataset(). It uses the signature syntax from Tutorial 1.
library(dsprrr)
library(ellmer)
chat <- chat_openai(model = "gpt-6-luna")The answers on this page were recorded with gpt-4.1 and
are replayed when the site is built, so gpt-6-luna may word
them differently.
Define the classifier
Start from the sentiment signature in Tutorial 1 and print the module:
classifier <- module(
signature("text -> sentiment: enum('positive', 'negative', 'neutral')")
)
classifier
#>
#> ── PredictModule ──
#>
#> ── Signature
#>
#> ── Signature ──
#>
#> ── Inputs
#> • text: "string" - Input: text
#>
#> ── Output
#> Type: "object(sentiment: enum(positive, negative, neutral))"
#>
#> ── Instructions
#> Given the fields `text`, produce the fields `sentiment`.The printout shows the inputs, the output type and the instructions that start every prompt. This signature has no instructions of its own, so dsprrr wrote a default from the field names.
Classify one text
run(classifier, text = "I absolutely loved this movie!", .llm = chat)
#> $sentiment
#> [1] "positive"
run(classifier, text = "This was a complete waste of time.", .llm = chat)
#> $sentiment
#> [1] "negative"
run(classifier, text = "It was okay, I guess.", .llm = chat)
#> $sentiment
#> [1] "neutral"
run(
classifier,
text = "The service was terrible but the food was amazing.",
.llm = chat
)
#> $sentiment
#> [1] "neutral"The mixed review came back neutral. Whether that is
right depends on what you need the labels for; instructions, below, let
you decide.
Classify a vector of texts
Pass a vector and run() returns a list with one result
per element, in the same order. Each result is the named list you saw
above, so purrr::map_chr() can pull out the labels:
reviews <- c(
"Best purchase I've ever made!",
"Broke after one day. Total garbage.",
"Does what it says. Nothing special.",
"Exceeded all my expectations!",
"Would not recommend to anyone."
)
sentiments <- run(classifier, text = reviews, .llm = chat)
purrr::map_chr(sentiments, "sentiment")
#> [1] "positive" "negative" "neutral" "positive" "negative"Each element is still its own request to the model, and by default
they are sent one after another. concurrency_control() lets
several run at once.
Add instructions
Instructions replace the default line. Use them to settle cases the labels leave open, such as mixed reviews:
sig <- signature(
"text -> sentiment: enum('positive', 'negative', 'neutral')",
instructions = "Classify the overall sentiment. If mixed, choose the dominant emotion."
)
classifier2 <- module(sig)The new module takes the same inputs, one at a time or as a vector:
run(classifier2, text = "This is fantastic!", .llm = chat)
#> $sentiment
#> [1] "positive"
run(
classifier2,
text = c("Love it!", "Hate it!", "It's fine"),
.llm = chat
)
#> [[1]]
#> [[1]]$sentiment
#> [1] "positive"
#>
#>
#> [[2]]
#> [[2]]$sentiment
#> [1] "negative"
#>
#>
#> [[3]]
#> [[3]]$sentiment
#> [1] "neutral"This is the full return value of a vectorized call: a list of named lists.
Classify a data frame
run_dataset() takes a data frame with a column for each
input. It returns the data frame with a result list-column
added, and other columns, like id, come through
unchanged:
reviews_df <- tibble::tibble(
id = 1:4,
text = c(
"Absolutely wonderful experience!",
"Never buying from them again.",
"Solid product, fair price.",
"Changed my life for the better."
)
)
results <- run_dataset(classifier2, reviews_df, .llm = chat)
results$sentiment <- purrr::map_chr(results$result, "sentiment")
results
#> # A tibble: 4 × 4
#> id text result sentiment
#> <int> <chr> <list> <chr>
#> 1 1 Absolutely wonderful experience! <named list [1]> positive
#> 2 2 Never buying from them again. <named list [1]> negative
#> 3 3 Solid product, fair price. <named list [1]> positive
#> 4 4 Changed my life for the better. <named list [1]> positiveLike run(), run_dataset() sends one request
per row. The module records every call, and
summarize_traces(classifier2) reports how many requests it
made and the tokens they used.
Describe the inputs
A signature can also be built from input() objects and
an ellmer type. An input’s description becomes its heading in the
prompt, in place of the input’s name:
sig <- signature(
inputs = list(
input("review_text", description = "Customer review to classify")
),
output_type = type_enum(values = c("positive", "negative", "neutral")),
instructions = "Classify the customer sentiment."
)
detailed_classifier <- module(sig)
run(
detailed_classifier,
review_text = "Five stars! Would buy again!",
.llm = chat
)
#> [1] "positive"The output type here is a bare type_enum() rather than a
named field, so run() returns the label itself instead of a
list.
Next, Tutorial 3 returns several fields at once, including lists and nested records.