Home ← Back to the HMM page

From Counts to Context

One NER task, three generations of taggers: HMM → LSTM → BERT

The same task, climbing the model ladder

The companion page builds a Hidden Markov Model from scratch to tag named entities. This page picks up where that one ends and walks up two more rungs of the modelling ladder - a recurrent LSTM tagger, and a fine-tuned BERT/DistilBERT tagger - all three solving the same problem so they can be compared apples-to-apples.

The task is fixed throughout: named entity recognition (NER) over the CoNLL-2003 English corpus (data/eng.traindata/eng.val). Every token gets one of a small set of entity tags; the question each model answers differently is how much context, and in what representation, do we bring to bear on each tagging decision?

The arc is a single idea: we keep widening the window of context a tagging decision can see, and we keep enriching how a word is represented - from atomic word-IDs (HMM), to learned dense vectors read left-to-right (LSTM), to pretrained contextual subword vectors that see the whole sentence at once (BERT).

HMM count-based generative word = atomic ID context = prev tag decode = Viterbi Markov assumption LSTM recurrent discriminative word = learned vector context = all left words decode = per-token argmax memory cell BERT pretrained + fine-tuned word = contextual subword context = whole sentence decode = per-token argmax self-attention
Three taggers, one task. Each rung widens the context window and enriches the word representation.

Provenance: all three live in the CMSI 5370 (NLP) problem-set-2 archive. The HMM and LSTM were the submitted assignment; the BERT tagger is the post-submission “kept going” branch (copy_ps2-scottn66/) that adds HuggingFace fine-tuning.

1  Hidden Markov Model - the count-based baseline

Full treatment lives on the HMM page; here is the one-paragraph recap so the comparison stands on its own.

The HMM is a generative, count-based model. Training is a single pass of maximum-likelihood counting over the labelled corpus that fills three tables:

All three tables are initialised to ones rather than zeros, which folds add-one (Laplace) smoothing straight into the model so no unseen event gets zero probability. Decoding finds the best tag sequence two ways: greedy (pick the locally best tag at each step) or Viterbi (a dynamic program in log-space that recovers the single most-likely whole-sequence tagging via a trellis + backpointers).

What it gets right - and where it caps out

Right: trains in seconds on a CPU, is fully interpretable (every number is a count), and Viterbi gives a provably optimal decode under the model. The in-code benchmark hint targets ~90% token accuracy with greedy and ~91% with Viterbi.

Caps out because: a word is an atomic ID - “Paris” and “London” share nothing, and any word unseen in training collapses to <UNK>. The Markov-1 assumption means a tag sees only the single previous tag, never the words around it. Those two limits are exactly what the next two models attack.

2  LSTM tagger - learned representations + memory

The LSTM tagger (nn.py / lstm.py, PyTorch) swaps counting for learning. It is a small discriminative network with three layers:

word IDs ─▶ nn.Embedding(vocab, embed_dim=32)      # learn a dense vector per word
         ─▶ nn.LSTM(embed_dim=32, hidden_dim=12)   # carry a memory cell across the sentence
         ─▶ nn.Linear(hidden_dim=12, num_tags)      # project each step's state to tag scores

Two things change versus the HMM. First, words become learned dense vectors instead of atomic IDs, so the model can discover that “Paris” and “London” behave alike. Second, the recurrent cell means the tag for word i is informed by a hidden state that has absorbed every word to its left - far more context than the HMM's single previous tag. Training is gradient descent (Adam, lr=0.001, 10 epochs), one sentence per backward pass.

⚠ Honest notes from the code

The LSTM fixes the HMM's representation and left-context limits, but it still reads strictly left-to-right and learns its embeddings from scratch on a small corpus. The next model removes both of those constraints at once.

3  BERT / DistilBERT tagger - pretrained, bidirectional context

The transformer tagger (bert.py) fine-tunes DistilBERT - a distilled, ~66M-parameter BERT - for token classification through the HuggingFace Trainer API:

datasets   = load_dataset('conll2003')                       # gold NER data
tokenizer  = AutoTokenizer.from_pretrained('distilbert-base-cased')
model      = AutoModelForTokenClassification.from_pretrained(
                 'distilbert-base-cased', num_labels=len(label_list))
args       = TrainingArguments(lr=2e-5, batch=16, epochs=3, weight_decay=0.01)
trainer    = Trainer(model, args, train_dataset=..., data_collator=...)
trainer.train()

Two qualitative leaps over the LSTM:

This branch isn't just code: a fine-tuned model was pushed to the HuggingFace Hub as scottn66/distilbert-base-uncased-finetuned-ner, and trans4mers.py wires it into live POS / NER / question-answering pipelines on sample text.

The result, measured

The original archived bert.py couldn't be trusted to report a number: it had a train/eval leak (train_dataset == eval_dataset) and fed word-level labels to the Trainer without aligning them to DistilBERT's subword tokens. A hardened rewrite (proper subword label alignment, a held-out split, and seqeval scoring - see the roadmap below) fixes all of that and produces real, reproducible numbers.

Fine-tuned DistilBERT-cased, 3 epochs on CoNLL-2003 (HF 9-label BIO version) scores, on the held-out test set: entity-level F1 = 0.888 (precision 0.880, recall 0.896) and token accuracy = 0.978. On the validation set the best epoch reaches 0.934 entity-F1 / 0.989 token accuracy. The ~88.8% test entity-F1 is right in the expected band for DistilBERT on CoNLL-2003.

For a strict apples-to-apples check against the HMM and LSTM - the same local 5-class data, the same preprocessing - the fine-tuned model reaches 98.5% token accuracy (macro token-F1 0.933) versus the HMM's ~91% Viterbi token accuracy. Same task, same labels: +7 points from contextual pretraining.

✅ Improving the BERT tagger - done

The leaky baseline was rewritten into a properly evaluated DistilBERT NER model. Each fix below is now implemented:

  1. ✓ Subword label alignment. Tokenize with is_split_into_words=True and use word_ids() to spread each word's gold tag onto its first subword while masking continuation pieces and specials with -100 (ignored by the loss). This is the single change that makes the model correct. Verified against the live tokenizer: Kubernetes splits to Ku/##ber/##net/##es, and only the first piece keeps the word's tag (the rest become -100).
  2. ✓ Real held-out evaluation. Train on the train split, evaluate on validation, report on the held-out test split - the train/eval leak is gone.
  3. ✓ Entity-level metrics. A compute_metrics runs seqeval for entity-level precision / recall / F1 (the standard CoNLL score) on the BIO data, and falls back to token-level scoring for the bare 5-class data.
  4. ✓ Use the loaded data & drop dead code. Trains on the actual datasets object; the unused BERTPoSTagger, CustomDataset, and precomputed-then-discarded encoded_inputs are gone.
  5. ✓ Modernize the API. Real transformers import (no trans4mers shim) and a complete requirements.txt. Going further than planned: the rewrite runs on transformers 5.x (processing_class replaces the removed tokenizer= arg) and datasets 5.x (script loading was removed, so it pulls a parquet CoNLL-2003 mirror).
  6. Remaining stretch goals. Try bert-base-cased vs DistilBERT; reconcile the cased/uncased split with the published uncased Hub model; optionally add a CRF decode head so the transformer, like the HMM, optimizes the whole tag sequence rather than each token independently.

Result: 0.888 test entity-F1 (0.934 on validation) - squarely in the expected band, and now trustworthy because the evaluation is honest.

Final comparison

The same task seen through three lenses. Where a cell would require a number the source never trustworthily produced, it says so rather than inventing one.

Dimension HMM LSTM BERT / DistilBERT
Paradigm Generative, count-based Discriminative, trained from scratch Pretrained + fine-tuned (transfer learning)
Word representation Atomic ID; <UNK> for unseen Learned dense vector (32-dim) Pretrained contextual subword vectors (768-dim)
Context per decision Previous tag only (Markov-1) All words to the left (unidirectional) Entire sentence, both directions (self-attention)
Out-of-vocabulary words Brittle (smoothed <UNK>) Single <UNK> embedding Graceful (WordPiece subwords)
Training One MLE counting pass; seconds, CPU Backprop, Adam, 10 epochs, 1 sentence/step Fine-tune 3 epochs from pretrained; GPU-friendly
Parameters Count tables, O(tags² + tags·|V|) Tiny (embed 32, hidden 12) ~66M (DistilBERT)
Sequence decoding Viterbi (global optimum) or greedy Per-token argmax Per-token argmax (CRF head = stretch goal)
Interpretability High - every value is a count Low Low
Documented result ~90% greedy / ~91% Viterbi token acc. (in-code hint) Not recorded (+ NLLLoss/logits caveat) 0.888 entity-F1 (CoNLL test) · 98.5% token-acc on local 5-class (vs HMM ~91%)
Main limitation Markov-1 + atomic words Left-only, tiny, no pretraining Per-token decode (no CRF); cased/uncased to reconcile

The takeaway

Read top to bottom, the three models tell one story: each generation buys richer context and richer word representations at the cost of interpretability and compute. The HMM is transparent and instant but myopic; the LSTM learns representations and remembers its left context; BERT brings world knowledge from pretraining and sees the whole sentence at once. The HMM still wins on transparency and on globally optimal decoding - which is why a CRF head on top of BERT (giving it Viterbi-style sequence optimization) is the natural place these two ends of the ladder meet.

Where this fits

The HMM at the base of this ladder is the same row-stochastic transition matrix that the conscious-agents page repurposes as the substrate of a perception theory. This page climbs the other direction - from that Markov baseline toward modern neural taggers.

← The HMM, in full From Markov Chains to Conscious Agents →