Finding noun chunks from grammar links

Compare spaCy phrases with small dependency chunks

phrases and entities
noun chunks
workforce research
Learn how noun chunks group nouns with attached words, using spaCy and a local UDPipe dependency parse.

After counting adjacent phrases, the Riverton Workforce Lab has a practical choice to make: should basic spreadsheet skills become one review item or three separate words?

A grammar-aware method offers one convention for making that choice. It can group candidate phrases for review, but it does not create a single correct inventory of English phrases.

Note

The Riverton Workforce Lab, its job board, and its training flyer are fictional and were created for teaching.

TipWhat you will learn

Use this lesson to practice how to:

  • define a noun chunk in plain language;
  • extract noun chunks with a trained spaCy pipeline;
  • build simple chunks from a UDPipe dependency parse;
  • explain why chunk definitions differ across tools;
  • separate agreement from accuracy; and
  • explain why parser errors affect chunk rules.

Ask a trained pipeline for noun chunks

This setup chunk reads the sentence file, runs spaCy, and later supports a small UDPipe-based rule. A noun chunk is a short, flat phrase built around one noun: the noun plus the determiners, numbers, compounds, and describing words directly in front of it. Chunks do not nest and do not reach past the noun, so evening classes in October becomes evening classes and October, not one long phrase. Where a chunk stops is a convention, and tools draw the line in different places.

library(readr)
library(dplyr)
library(tibble)
library(purrr)
library(stringr)
library(tokenizers)
library(udpipe)
library(spacyr)
source("R/use-spacy.R")

sentences <- read_csv(
  "data/workforce/workforce_sentences.csv",
  na = character(),
  col_types = cols(
    sentence_id = col_character(),
    document_id = col_character(),
    source_line = col_character(),
    text = col_character(),
    reference_label = col_character(),
    uncertainty = col_character(),
    annotator_id = col_character(),
    rationale = col_character(),
    codebook_version = col_character(),
    codebook_hash = col_character(),
    derived = col_character(),
    transformation = col_character()
  )
)

pipeline <- use_project_spacy()
pipeline_info <- spacy_pipeline_version()
parsed_spacy <- spacy_parse(
  setNames(sentences$text, sentences$sentence_id),
  pos = TRUE,
  lemma = TRUE,
  entity = TRUE,
  dependency = TRUE,
  nounphrase = TRUE
)
spacy_phrases <- nounphrase_extract(parsed_spacy) |>
  as_tibble() |>
  transmute(doc_id, chunk = str_replace_all(nounphrase, "_", " "))

example_text <- "Riverton Skills Centre will run evening classes in October."
example_parsed <- spacy_parse(
  c(example = example_text),
  pos = TRUE,
  lemma = TRUE,
  entity = TRUE,
  dependency = TRUE,
  nounphrase = TRUE
)
example_phrases <- nounphrase_extract(example_parsed) |>
  as_tibble() |>
  transmute(chunk = str_replace_all(nounphrase, "_", " "))

spacy_finalize()

pipeline_table <- tibble(
  field = names(pipeline_info),
  value = unlist(pipeline_info, use.names = FALSE)
)

knitr::kable(
  pipeline_table,
  col.names = c("Pipeline field", "Value"),
  caption = "spaCy pipeline used for noun chunks",
  row.names = FALSE
)
spaCy pipeline used for noun chunks
Pipeline field Value
name core_web_sm
version 3.8.0
lang en
license MIT
spacy 3.8.7
knitr::kable(
  spacy_phrases |>
    slice_head(n = 12),
  col.names = c("Sentence ID", "spaCy noun chunk"),
  caption = "First spaCy noun chunks from the Riverton sentences",
  row.names = FALSE
)
First spaCy noun chunks from the Riverton sentences
Sentence ID spaCy noun chunk
s001 Paid 12-week training
s002 No prior data experience
s003 Evening schedules
s004 Applicants
s004 basic spreadsheet skills
s005 A high school diploma
s006 The employer
s006 certification training
s007 Rotating night shifts
s007 part
s007 the job
s008 Workers

The released spaCy pipeline returns 40 noun chunks for the 28 sentences. For the check sentence, it returns three flat chunks: Riverton Skills Centre, evening classes, and October.

Compare the two lists

Exact agreement is strict: the sentence ID and the chunk text must both match. That makes agreement a string match, not accuracy. Neither list is ground truth, and spaCy chunks are non-overlapping while the UDPipe-built list can overlap.

agreed_chunks <- inner_join(
  distinct(spacy_phrases),
  distinct(udpipe_chunks),
  by = c("doc_id", "chunk")
)

comparison_summary <- tibble(
  measure = c("spaCy chunks", "UDPipe-built chunks", "exact agreements"),
  count = c(nrow(spacy_phrases), nrow(udpipe_chunks), nrow(agreed_chunks))
)

different_examples <- tibble(
  method = c("spaCy", "UDPipe-built"),
  sentence_id = c("s005", "s005"),
  chunk = c("A high school diploma", "A high school"),
  explanation = c(
    "spaCy keeps the full education phrase.",
    "The dependency rule attaches words to school and leaves diploma outside this chunk."
  )
)

spreadsheet_examples <- bind_rows(
  spacy_phrases |>
    filter(doc_id == "s004", str_detect(chunk, "spreadsheet")) |>
    mutate(method = "spaCy"),
  udpipe_chunks |>
    filter(doc_id == "s004", str_detect(chunk, "spreadsheet")) |>
    mutate(method = "UDPipe-built")
) |>
  select(method, doc_id, chunk)

knitr::kable(
  comparison_summary,
  col.names = c("Measure", "Count"),
  caption = "String matches across two chunking conventions",
  row.names = FALSE
)
String matches across two chunking conventions
Measure Count
spaCy chunks 40
UDPipe-built chunks 56
exact agreements 25
knitr::kable(
  different_examples,
  col.names = c("Method", "Sentence ID", "Chunk", "Explanation"),
  caption = "One sentence where the two chunk methods differ",
  row.names = FALSE
)
One sentence where the two chunk methods differ
Method Sentence ID Chunk Explanation
spaCy s005 A high school diploma spaCy keeps the full education phrase.
UDPipe-built s005 A high school The dependency rule attaches words to school and leaves diploma outside this chunk.
knitr::kable(
  spreadsheet_examples,
  col.names = c("Method", "Sentence ID", "Chunk"),
  caption = "How each method handles the spreadsheet-skills phrase",
  row.names = FALSE
)
How each method handles the spreadsheet-skills phrase
Method Sentence ID Chunk
spaCy s004 basic spreadsheet skills
UDPipe-built s004 spreadsheet
UDPipe-built s004 need basic spreadsheet skills

The two lists have 25 exact string matches. That is agreement, not accuracy, and the counts 40 and 56 are not two measurements of the same thing. For the opening question, spaCy groups basic spreadsheet skills, while the UDPipe-built rule also emits spreadsheet and overreaches to need basic spreadsheet skills. A bigram takes neighbouring words even across phrase boundaries; a chunk groups words the parser links together. Whether that helps depends on whether the parser is right.

What to remember

  • Noun chunks are flat, non-overlapping spans under a tool-specific convention.
  • spaCy returns 40 noun chunks for these 28 sentences.
  • The UDPipe-built rule returns 56 overlapping chunks from a deliberately weak teaching model.
  • The 25 exact matches are agreement, not accuracy.
  • Paid week shows how a rule can manufacture text by pasting non-adjacent tokens.
  • Tokenization alone explains the 179 versus 178 row-count gap.

Review chunk lists as prompts for human inspection. When the two tools disagree, check the sentence, the tokenization, and the parse before drawing any conclusion about the phrase.

Sources