Foundations/Regular Expressions and Text Normalization

Lesson 1.22,495 words

Regular Expressions and Text Normalization

Before any model touches text, the text has to be found and cleaned. Regular expressions give an algebra for describing string patterns; tokenization, case folding, and stemming turn raw characters into the units a model counts; and byte-pair encoding builds a subword vocabulary that spells out any word.

╌╌╌╌

Every language-processing system takes the same input: a stream of characters. Few downstream operations are defined on characters — they need words, sentences, prices, or a distance between two strings. This lesson supplies three layers between the character stream and the first model: a pattern language for finding structure (regular expressions), a set of tasks for imposing structure (text normalization), and one algorithm for comparing strings (minimum edit distance).

Weizenbaum's ELIZA, the earliest dialogue system, was a cascade of pattern-and-substitute rules — mapping I need X to What would it mean to you if you got X? — and nothing more.1 The pattern-matching layer persists in modern systems as the entry point where text enters the pipeline.

Regular expressions

A regular expression (RE) is an algebraic notation characterizing a set of strings — a regular language over an alphabet .2 Given a pattern and a corpus of text, an engine returns every substring with . Every RE compiles to an equivalent finite-state automaton, which is why matching runs in time linear in the input. The notation is portable across grep, editors, and Python; patterns are written between slash delimiters, /like this/ (the slashes are not part of the pattern).

The simplest pattern is a literal sequence of characters: /woodchuck/ matches the string woodchuck wherever it appears. Regular expressions are case sensitive, so /woodchuck/ will not match Woodchuck. Everything past literals is built from a small set of operators that let one pattern stand for many strings.

Character classes, ranges, and negation

Square brackets specify a disjunction of characters — a single position that may be any one of a set. /[wW]/ matches w or W, so /[wW]oodchuck/ catches both capitalizations. Inside brackets a dash denotes a range over the character ordering: /[a-z]/ is any lowercase letter, /[A-Z]/ any uppercase letter, /[0-9]/ any single digit. A caret as the first character inside the brackets negates the class: /[^a-z]/ matches any single character that is not a lowercase letter. (A caret anywhere else inside brackets is just a literal caret.)

PatternMatchesExample
/[wW]/w or WWoodchuck
/[abc]/one of a, b, cin uomini, in soldati
/[0-9]/a single digitChapter 1
/[A-Z]/an uppercase letterDrenched Blossoms
/[^A-Z]/any non-uppercase charOyfn pripetchik
/[^Ss]/neither S nor sI have no reason

Because a handful of classes come up constantly, there are backslash aliases: \d is [0-9], \w is a word character [a-zA-Z0-9_], \s is whitespace, and each has an uppercase-negated twin (\D, \W, \S). To match a character that is itself an operator — a literal period, star, or bracket — precede it with a backslash: /\./, /\*/, /\[/.

Counters — optionality, Kleene star and plus

A counter following an element sets its multiplicity. For an element , the counters denote:

CounterRepetitions of Language
e? or
e* or more (Kleene star)
e+ or more (Kleene plus)
e{n}exactly
e{n,m}between and
e{n,}at least

So /colou?r/ matches color and colour; /a*/ matches ; and /[0-9]+/ is the idiom for an integer. The wildcard /./ (an unescaped period) matches any single character, so /beg.n/ matches begin, begun, and beg'n alike, and /aardvark.*aardvark/ matches any line where aardvark appears twice.

The star is greedy: against once upon a time, /[a-z]*/ matches the longest prefix run. The non-greedy variants *? and +? match the fewest characters possible, which matters when a later subpattern must consume the remainder.

Anchors, disjunction, and grouping

Anchors match a position rather than a character. The caret ^ (outside brackets) matches the start of a line, $ matches the end, \b matches a word boundary, and \B a non-boundary. So /^The/ finds The only at the start of a line, and /\bthe\b/ matches the word the but not the buried inside other or theology.

The disjunction operator | (the pipe) matches either of two whole patterns: /cat|dog/ matches cat or dog. Disjunction has very low precedence — it splits the pattern at the widest scope — so /guppy|ies/ matches guppy or ies, notguppy or guppies. To scope the disjunction, use grouping parentheses: /gupp(y|ies)/ matches guppy and guppies, because the parentheses make (y|ies) behave as a single element. Grouping also lets a counter apply to a whole sub-pattern rather than one character: /(ab)+/ matches ab, abab, ababab.

Parentheses have a second job. A capture group stores whatever its sub-pattern matched in a numbered register, referred to later by \1, \2, and so on. This makes substitutions expressive: the substitution s/([0-9]+)/<\1>/ wraps every integer in angle brackets, and /the (.*)er they were, the \1er they will be/ forces the two blanks to match the same string, so it accepts the bigger they were, the bigger they will be but rejects a mismatched pair. When you want the grouping but not the register, a non-capturing group (?:...) groups without consuming a register number.

The precedence order, highest to lowest, is the one fact that resolves most ambiguous patterns:

PrecedenceOperatorsExample
highestparentheses(...)
counters* + ? {n,m}
sequences and anchorsthe, ^my, end$
lowestdisjunction|

Because counters bind tighter than sequences, /the*/ matches th, the, thee, theee — the star attaches to e, not to the whole word. Because sequences bind tighter than disjunction, /the|any/ matches the or any, not th followed by e-or-a followed by ny.

The regular-expression operator precedence hierarchy: parentheses bind tightest, then counters, then sequences and anchors, and disjunction loosest — the order that resolves an otherwise ambiguous pattern.

Two more operators round out the language. Substitution, written s/pattern/replacement/, rewrites every match — s/colour/color/ Americanizes a document. Lookahead assertions test whether a pattern matches without consuming any input: (?=...) is positive lookahead, (?!...) negative, both zero-width. Negative lookahead expresses exclusions cleanly — /^(?!Volcano)[A-Za-z]+/ matches any opening word that is not Volcano.

Pattern design balances two error types. A loose pattern produces false positives (, matching excluded strings); a tight pattern produces false negatives (, missing intended strings). With true positives ,

Tightening to cut raises precision; loosening to cut raises recall. Every pattern trades one against the other.3

Text normalization

Before almost any language processing, the text must be normalized into a standard form. Three tasks make up nearly every normalization pipeline: tokenizing words, normalizing word formats, and segmenting sentences.4

Tokenization

Tokenization is the task of segmenting running text into words, or tokens. The naive rule — split on whitespace — fails immediately, in both directions. It splits too little: Ph.D., AT&T, and m.p.h. contain internal punctuation that belongs to the token, and prices like $45.55 or dates like 01/02/06 must not be diced at the period. It also splits too much: New York and rock 'n' roll are arguably single tokens despite their spaces, and a clitic contraction like what're should expand to the two tokens what and are. Punctuation itself is usually worth keeping as its own token — commas cue parsers, periods cue sentence boundaries, question marks carry meaning — rather than discarded.

Tokenization runs first, so it must be fast: the standard approach compiles a deterministic RE into a finite-state automaton. The Penn Treebank standard separates clitics (doesn'tdoes n't), keeps hyphenated words together, and splits off punctuation. Two counts recur throughout the subject: the number of distinct words, the types or vocabulary size , and the total number of running words, the tokens . Empirically the two grow together as with (Herdan's law), so vocabulary keeps expanding with corpus size.

Not every language separates words with spaces. Chinese writes characters (hanzi) with no delimiters, and boundary judgments disagree even among native speakers; many Chinese tasks take each character as the token. Japanese and Thai require dedicated word-segmentation models. These difficulties motivate a third option that sidesteps the definition of a word altogether.

Byte-pair encoding

Words are a leaky abstraction for a model. A system trained on low, new, and newer but never lower cannot represent lower at test time — the unknown-word (out-of-vocabulary) problem. Modern tokenizers avoid it with a vocabulary of subwords: units smaller than words, often morphemes like -er or -est, so any unseen word is spelled from known pieces.5

The dominant scheme is byte-pair encoding (BPE), in two halves: a token learner inducing a vocabulary from a corpus, and a token segmenter applying it to new text. The learner initializes to the character set, then for iterations selects the most frequent adjacent symbol pair, merges it into a new symbol, and adds it to . This yields .

Algorithm:Byte-Pair-Encoding(C,k)\textsc{Byte-Pair-Encoding}(C, k) — learn a subword vocabulary from corpus CC with kk merges
  1. 1
    input: corpus CC (words as character sequences with a word-end marker), number of merges kk
  2. 2
    VV \gets set of all distinct characters in CC
  3. 3
    for i=1i = 1 to kk do
  4. 4
    (tL,tR)(t_L, t_R) \gets most frequent adjacent pair of symbols in CC
  5. 5
    tnewtL+tRt_{\text{new}} \gets t_L + t_R
    concatenate into one new symbol
  6. 6
    VV{tnew}V \gets V \cup \{t_{\text{new}}\}
  7. 7
    replace every occurrence of (tL,tR)(t_L, t_R) in CC with tnewt_{\text{new}}
  8. 8
    return VV

Run inside words with a special end-of-word marker _, on a tiny corpus of counted word tokens (low × 5, lowest × 2, newer × 6, wider × 3, new × 2), the first merges are easy to trace by hand. The pair e r is most frequent (9 occurrences, across newer and wider), so it merges to er; then er _ merges to er_; then n e to ne, and so on.

The first three BPE merges on the corpus low/lowest/newer/wider/new. The most frequent adjacent pair is merged into a single symbol, added to the vocabulary, and rewritten throughout the corpus, then the count repeats.

The segmenter applies the learned merges, in the order learned, to new text. A word seen in training collapses back to a few symbols; a genuinely novel word like lower is left as the pieces low and er_. With thousands of merges on a real corpus the effect is that common words become single tokens and only the rare and unknown ones get spelled out of parts — exactly the behavior a language model needs.

To see the segmenter run, take the learned merge list (in order) e rer, er _er_, n ene, ne wnew, l olo, lo wlow, and apply it to the unseen word lower_. Start from characters, l o w e r _, and scan for the first applicable merge in learned order. e r fires, giving l o w er _; then er _ fires, giving l o w er_; n e and ne w do not apply; l o fires, giving lo w er_; lo w fires, giving low er_. No further merge applies, so lower_ segments as low + er_ — two known pieces the model has embeddings for, even though lower never appeared in training. The unknown-word problem is dissolved: there is no out-of-vocabulary word, only a longer or shorter spelling out of subwords.

Segmenting the unseen word lower with the learned merge list. Starting from characters, each learned merge is applied in order where it fits; the word ends as low + er, two in-vocabulary pieces, with no unknown token.

Word normalization, lemmatization, and stemming

Even after tokenizing, the same word appears in many surface forms. Word normalization puts tokens into a standard form: mapping US, U.S., and USA to one representation, or collapsing uh-huh and uhhuh. Case folding — mapping everything to lowercase — is the bluntest version, and it helps for information retrieval, where a searcher wants Woodchuck and woodchuck treated alike. But case folding is not always wanted: for sentiment analysis and machine translation the difference between US the country and us the pronoun can outweigh the generalization it buys.

A deeper normalization collapses inflected forms. Lemmatization determines that two words share a root despite surface differences: am, are, and is all reduce to the lemma be; dinner and dinners to dinner. Done properly it requires morphological parsing — decomposing a word into its morphemes, the smallest meaning-bearing units, split into stems (the core meaning) and affixes (the -s, -ed, -ing that modify it). Lemmatizing He is reading detective stories yields He be read detective story.

Full morphological parsing is expensive; stemming is the cheaper shortcut, chopping affixes off the word end. The Porter stemmer is the classic instance: a cascade of rewrite rules applied in series, each pass feeding the next.6 A sampling of its rules:

RuleExample
ATIONALATErelational → relate
ING (if stem has a vowel)motoring → motor
SSESSSgrasses → grass

Stemming is fast and dictionary-free, but it errs in both directions — over-generalizing (organizationorgan, policypolice) and under-generalizing (European and Europe left unmerged). It is a useful approximation, not a substitute for real morphology.

The subword tokenizers behind modern models

The byte-pair encoding above was introduced for text compression (Gage, 1994); its adaptation to NLP is more recent and is worth stating precisely, because the exact tokenizer a model uses shapes what it can represent. Sennrich, Haddow & Birch (2016, ACL) reintroduced BPE as a tokenization method for neural machine translation, and their motivation was the same open-vocabulary problem the previous section named: a translation model with a fixed word vocabulary cannot emit a word it never saw, and it is the rare words — names, compounds, morphologically rich forms — that matter most.7 Splitting into subword units let a single fixed-size vocabulary spell out any word, and it improved translation of rare and unseen words. That paper is why nearly every transformer since tokenizes into subwords.

Three variants dominate, differing in the merge or split criterion.

TokenizerDirectionCriterion for a pair Used by
BPEbottom-up mergemaximize count GPT-2, GPT-3
WordPiecebottom-up mergemaximize (PMI-like)BERT
Unigram LMtop-down pruneminimize corpus likelihood loss from removing a subwordT5, ALBERT
  • BPE greedily merges the highest-count adjacent pair, times — simple and deterministic.
  • WordPiece (Schuster & Nakajima, 2012) instead merges the pair that most increases training-corpus likelihood under a unigram model, i.e. the pair maximizing the count ratio — the same pointwise-mutual-information quantity used in the sentiment lexicons.
  • Unigram-LM tokenization (Kudo, 2018) works top-down: fit a unigram distribution over a large candidate vocabulary, then iteratively prune the subwords whose removal costs the corpus likelihood least.8 It scores every segmentation and can retain several, enabling subword sampling during training.

The SentencePiece library (Kudo & Richardson, 2018) packages these so the tokenizer runs directly on raw text — treating the space as an ordinary symbol (often written as a visible underscore-like marker) — so the same tokenizer works on languages without spaces and the process is fully reversible from tokens back to the exact input string. The practical upshot: the word that this lesson opened with is not the unit modern models operate on. They operate on subwords chosen to balance a fixed vocabulary size against the ability to spell out anything, and the choice among BPE, WordPiece, and unigram-LM is a real engineering decision, not a detail.

Sentence segmentation

The last normalization task is sentence segmentation: breaking text into sentences. Question marks and exclamation points are nearly unambiguous sentence boundaries. The period is the hard case, because . marks both an end of sentence and an abbreviation — Mr., Inc., Ph.D. — and sometimes both at once. Segmenters therefore decide, per period, whether it belongs to the preceding token or ends the sentence, using an abbreviation dictionary (hand-built or learned) or a classifier. In practice sentence and word tokenization are settled jointly: a sentence ends when a sentence-final punctuation mark is not already glued into a token like an abbreviation or a number.

Footnotes

  1. Jurafsky & Martin, Speech and Language Processing (3rd ed.), Ch. 2 — ELIZA as a cascade of regular-expression substitutions, the earliest pattern-based dialogue system.
  2. Jurafsky & Martin, §2.1 — Regular Expressions: character classes, ranges, negation, Kleene star and plus, anchors, disjunction, grouping, capture groups, substitution, and lookahead.
  3. Jurafsky & Martin, §2.1.3 — false positives and false negatives, and the precision/recall tradeoff that governs pattern design.
  4. Jurafsky & Martin, §2.4 — Text Normalization: the three standard steps of tokenizing words, normalizing word formats, and segmenting sentences.
  5. Jurafsky & Martin, §2.4.3 — Byte-Pair Encoding: the unknown-word problem, subword tokenization, and the token-learner / token-segmenter split, with the low/newer/wider worked corpus.
  6. Jurafsky & Martin, §2.4.4 — Word Normalization, Lemmatization and Stemming: morphological parsing into stems and affixes, the Porter stemmer's cascade of rewrite rules, and its over- and under-generalization errors.
  7. R. Sennrich, B. Haddow, A. Birch, Neural Machine Translation of Rare Words with Subword Units, Proceedings of ACL, 2016 — reintroduces byte-pair encoding (originally Gage, A New Algorithm for Data Compression, 1994) as an open-vocabulary tokenization for neural MT, improving translation of rare and unseen words by spelling them from subword units in a single fixed vocabulary.
  8. T. Kudo, Subword Regularization: Improving Neural Network Translation Models with Multiple Subword Candidates, Proceedings of ACL, 2018 — the unigram-language-model tokenizer, which fits a unigram distribution over a candidate subword vocabulary and prunes by likelihood, and can sample among segmentations; packaged with BPE in T. Kudo & J. Richardson, SentencePiece, EMNLP (system demonstrations), 2018. WordPiece is M. Schuster & K. Nakajima, Japanese and Korean voice search, ICASSP, 2012.

╌╌ END ╌╌