Chunking › Regex-Based Chunking with NLTK

Michael BrenndoerferUpdated March 21, 20265 min read
Language AI HandbookAll chapters
Part 6 of 67Sequence Labeling
Chapter 39Chunking › Regex-Based Chunking with NLTK
Reading section 7 of 15 · Regex-Based Chunking with NLTK · 5 minSection 7 of 15 · 5 min

Book contents

Section 7 of 15 · 5 min read

Chunking › Regex-Based Chunking with NLTKOne section · Sections in this chapter7 / 15 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

NLTK provides a RegexpParser that lets you define chunk patterns using regular expressions over POS tags. This approach is intuitive: you specify what sequences of POS tags should be grouped into a chunk, and the parser applies those rules to POS-tagged input.

The grammar uses angle brackets around POS tags and standard regex quantifiers:

  • <DT> matches a determiner token
  • <JJ>* matches zero or more adjectives
  • <NN.*>+ matches one or more nouns (NN, NNS, NNP, NNPS)
  • <DT|PRP$> matches either a determiner or a possessive pronoun
In[24]:
Code
from nltk import RegexpParser

# Define a grammar for NP, VP, and PP chunking
np_grammar = r"""
    NP: {<DT|PRP\$>?<JJ>*<NN.*>+}
    VP: {<MD>?<VB.*>+}
    PP: {<IN>}
"""

# Create the parser
chunker = RegexpParser(np_grammar)


# A helper to extract chunks from NLTK trees
def tree_to_chunks(tree):
    """Extract (chunk_type, text) pairs from an NLTK parse tree."""
    chunks = []
    for subtree in tree:
        if hasattr(subtree, "label"):
            chunk_type = subtree.label()
            words = " ".join(word for word, _ in subtree.leaves())
            chunks.append((chunk_type, words))
    return chunks


# Test on several sentences
test_sentences = [
    "The quick brown fox jumps over the lazy dog.",
    "A beautiful sunset illuminated the entire valley.",
    "My old computer finally crashed yesterday.",
    "Scientists discovered a new species in the deep ocean.",
]

regex_results = []
for sent in test_sentences:
    tokens = word_tokenize(sent)
    tagged = pos_tag(tokens)
    tree = chunker.parse(tagged)
    chunks = tree_to_chunks(tree)
    regex_results.append((sent, tagged, chunks))
Out[25]:
Console
Regex-Based Chunking Results
=================================================================

Sentence: The quick brown fox jumps over the lazy dog.
  POS: The/DT quick/JJ brown/NN fox/NN jumps/VBZ over/IN...
  Chunks:
    [The quick brown fox] -> NP
    [jumps] -> VP
    [over] -> PP
    [the lazy dog] -> NP

Sentence: A beautiful sunset illuminated the entire valley.
  POS: A/DT beautiful/JJ sunset/NN illuminated/VBD the/DT entire/JJ...
  Chunks:
    [A beautiful sunset] -> NP
    [illuminated] -> VP
    [the entire valley] -> NP

Sentence: My old computer finally crashed yesterday.
  POS: My/PRP$ old/JJ computer/NN finally/RB crashed/VBN yesterday/NN...
  Chunks:
    [My old computer] -> NP
    [crashed] -> VP
    [yesterday] -> NP

Sentence: Scientists discovered a new species in the deep ocean.
  POS: Scientists/NNS discovered/VBD a/DT new/JJ species/NNS in/IN...
  Chunks:
    [Scientists] -> NP
    [discovered] -> VP
    [a new species] -> NP
    [in] -> PP
    [the deep ocean] -> NP

Let's break down exactly what the NP grammar pattern matches:

In[26]:
Code
# Breakdown of the NP pattern components
np_pattern_breakdown = {
    "<DT|PRP\\$>?": "Optional: determiner (the, a, an) OR possessive pronoun (my, your, its)",
    "<JJ>*": "Zero or more adjectives (modifiers before the noun)",
    "<NN.*>+": "One or more nouns: NN (singular), NNS (plural), NNP (proper), NNPS (proper plural)",
}

regex_quantifiers = {
    "?": "zero or one (optional element)",
    "*": "zero or more (any number)",
    "+": "one or more (at least one required)",
    "|": "alternation (either/or)",
}
Out[27]:
Console
NP Pattern Breakdown: {<DT|PRP\$>?<JJ>*<NN.*>+}
============================================================

  <DT|PRP\$>?
    Optional: determiner (the, a, an) OR possessive pronoun (my, your, its)

  <JJ>*
    Zero or more adjectives (modifiers before the noun)

  <NN.*>+
    One or more nouns: NN (singular), NNS (plural), NNP (proper), NNPS (proper plural)


Regex Quantifiers in NLTK Grammar:
----------------------------------------
  ?    zero or one (optional element)
  *    zero or more (any number)
  +    one or more (at least one required)
  |    alternation (either/or)

Chinking: Excluding Tokens from Chunks

Sometimes it's easier to specify what should NOT be in a chunk than what should be. Chinking removes tokens from existing chunks using the }{ syntax. You first chunk everything with {<.*>+}, then cut out the unwanted tokens.

In[28]:
Code
# Chinking grammar:
# Step 1: {<.*>+} chunks every token into one big NP
# Step 2: }<VB.*|IN|CC|\\.>{ removes (chinks) verbs, prepositions,
#         conjunctions, and punctuation from chunks
chink_grammar = r"""
    NP: {<.*>+}
        }<VB.*|IN|CC|\.>{
"""

chink_chunker = RegexpParser(chink_grammar)

chink_sentences = [
    "The cat and the dog sat on the mat.",
    "Alice ran quickly through the dark forest.",
]

chink_results = []
for sent in chink_sentences:
    tokens = word_tokenize(sent)
    tagged = pos_tag(tokens)
    tree = chink_chunker.parse(tagged)
    chunks = tree_to_chunks(tree)
    chink_results.append((sent, tagged, chunks))
Out[29]:
Console
Chinking Examples
============================================================

Strategy: chunk everything, then remove verbs/prepositions/conjunctions

Sentence: The cat and the dog sat on the mat.
  POS: The/DT cat/NN and/CC the/DT dog/NN sat/VBD on/IN the/DT mat/NN ./.
  Resulting NP chunks:
    [The cat] -> NP
    [the dog] -> NP
    [the mat] -> NP

Sentence: Alice ran quickly through the dark forest.
  POS: Alice/NNP ran/VBD quickly/RB through/IN the/DT dark/JJ forest/NN ./.
  Resulting NP chunks:
    [Alice] -> NP
    [quickly] -> NP
    [the dark forest] -> NP

The chinking approach inverts the chunking logic. Instead of specifying which POS sequences form a chunk, you specify which POS tags break a chunk. This works well when chunks are mostly contiguous sequences of "content words" interrupted by functional words.

Limitations of Regex Chunking

Regex-based chunking is simple and fast but has structural limitations that matter in practice. The patterns match local POS sequences without access to broader context, making several phenomena difficult:

In[30]:
Code
# Cases where regex chunking struggles
hard_cases = [
    {
        "sentence": "The man I saw yesterday left.",
        "issue": "Relative clause ('I saw yesterday') interrupts the NP",
        "expected_np": "The man",
        "regex_np": "The man I saw",  # incorrectly groups relative clause
    },
    {
        "sentence": "The old man the boats.",
        "issue": "Garden path: 'man' is a verb here, not a noun",
        "expected_vp": "man",
        "regex_problem": "POS tagger likely tags 'man' as NN",
    },
    {
        "sentence": "Flying planes can be dangerous.",
        "issue": "Attachment ambiguity: 'flying' modifies 'planes' or starts a VP",
        "expected": "Ambiguous without context",
    },
]

hard_results = []
for case in hard_cases:
    tokens = word_tokenize(case["sentence"])
    tagged = pos_tag(tokens)
    tree = chunker.parse(tagged)
    found_chunks = tree_to_chunks(tree)
    hard_results.append((case, tagged, found_chunks))
Out[31]:
Console
Regex Chunking Limitations
=================================================================

Sentence: The man I saw yesterday left.
  Issue: Relative clause ('I saw yesterday') interrupts the NP
  POS: The/DT man/NN I/PRP saw/VBD yesterday/NN left/VBD ./.
  Chunks found by regex:
    [The man] -> NP
    [saw] -> VP
    [yesterday] -> NP
    [left] -> VP

Sentence: The old man the boats.
  Issue: Garden path: 'man' is a verb here, not a noun
  POS: The/DT old/JJ man/NN the/DT boats/NNS ./.
  Chunks found by regex:
    [The old man] -> NP
    [the boats] -> NP

Sentence: Flying planes can be dangerous.
  Issue: Attachment ambiguity: 'flying' modifies 'planes' or starts a VP
  POS: Flying/VBG planes/NNS can/MD be/VB dangerous/JJ ./.
  Chunks found by regex:
    [Flying] -> VP
    [planes] -> NP
    [can be] -> VP

The fundamental limitation is that regex patterns operate only on local POS sequences. They cannot condition on distant context, handle long-range dependencies, or recover from POS tagging errors. Machine learning approaches address these limitations by learning from data.