AI Knowledge Tree

HMM for POS tagging and NER

Define a first-order HMM with emission and transition matrices, run Viterbi decoding on a short sentence, and connect it to POS/NER sequence labeling.

⏱ 32 minPrerequisites:N-gram language models

Sequence labeling assigns a tag to every token in a sentence. Part-of-speech (POS) tagging maps each word to its grammatical category (noun, verb, adjective, ...); named entity recognition (NER) labels spans as Person, Organization, Location, or Other.

Both tasks share a structure: the output is a sequence of the same length as the input, and adjacent labels are strongly correlated (a noun is likely followed by a verb; an "I" (inside) tag must follow a "B" (begin) tag of the same entity type). The Hidden Markov Model is the canonical probabilistic model for this structure[1, 2].

HMM definition

An HMM over observation sequence x=x1,…,xT\mathbf{x} = x_1, \dots, x_T and hidden state sequence y=y1,…,yT\mathbf{y} = y_1, \dots, y_T (the tags) has three components:

Transition probabilities Aij=p(yt=j∣yt−1=i)A_{ij} = p(y_t = j \mid y_{t-1} = i): a K×KK \times K matrix (rows sum to 1) encoding how likely tag jj follows tag ii.

Emission probabilities Bik=p(xt=k∣yt=i)B_{ik} = p(x_t = k \mid y_t = i): a K×VK \times V matrix encoding how likely word kk is emitted from tag ii.

Initial probabilities πi=p(y1=i)\pi_i = p(y_1 = i): a length-KK vector.

The joint probability of an observation-state sequence pair:

p(x,y)=πy1∏t=1TByt,xt∏t=2TAyt−1,yt.p(\mathbf{x}, \mathbf{y}) = \pi_{y_1} \prod_{t=1}^{T} B_{y_t, x_t} \prod_{t=2}^{T} A_{y_{t-1}, y_t}.

Training: counting

Given a labeled corpus (sentences with gold POS tags), estimate by MLE:

A^ij=c(i→j)∑kc(i→k),B^ik=c(i,k)∑lc(i,l),π^i=c(starts with i)Nsentences.\hat{A}_{ij} = \frac{c(i \to j)}{\sum_k c(i \to k)}, \qquad \hat{B}_{ik} = \frac{c(i, k)}{\sum_l c(i, l)}, \qquad \hat{\pi}_i = \frac{c(\text{starts with } i)}{N_{\text{sentences}}}.

Each count is a simple corpus statistic. Add Laplace smoothing to handle unseen word-tag pairs.

Decoding: the Viterbi algorithm

At test time, we want the most probable tag sequence given the words:

y^=arg⁡max⁡yp(y∣x)=arg⁡max⁡yp(x,y).\hat{\mathbf{y}} = \arg\max_{\mathbf{y}} p(\mathbf{y} \mid \mathbf{x}) = \arg\max_{\mathbf{y}} p(\mathbf{x}, \mathbf{y}).

Brute force over KTK^T sequences is exponential. The Viterbi algorithm solves this in O(TK2)O(TK^2) with dynamic programming[3].

Define vt(j)=max⁡y1,…,yt−1p(y1,…,yt−1,yt=j,x1,…,xt)v_t(j) = \max_{y_1, \dots, y_{t-1}} p(y_1, \dots, y_{t-1}, y_t = j, x_1, \dots, x_t): the probability of the best path ending in state jj at time tt.

Initialization (t=1t=1): v1(j)=πj⋅Bj,x1.v_1(j) = \pi_j \cdot B_{j, x_1}.

Recursion (t>1t > 1): vt(j)=Bj,xt⋅max⁡i[vt−1(i)⋅Aij].v_t(j) = B_{j, x_t} \cdot \max_i \bigl[ v_{t-1}(i) \cdot A_{ij} \bigr].

Store the backpointer ψt(j)=arg⁡max⁡i[vt−1(i)⋅Aij]\psi_t(j) = \arg\max_i [v_{t-1}(i) \cdot A_{ij}].

Termination: y^T=arg⁡max⁡jvT(j)\hat{y}_T = \arg\max_j v_T(j). Backtrack through ψ\psi to recover the full sequence.

Worked example

Tags: {\{DT, NN, VB}\} (determiner, noun, verb). Words: {\{the, dog, runs}\}.

Tiny parameters (illustrative):

π=[0.6,0.3,0.1],A=(0.10.80.10.10.10.80.50.40.1),\pi = [0.6, 0.3, 0.1], \quad A = \begin{pmatrix} 0.1 & 0.8 & 0.1 \\ 0.1 & 0.1 & 0.8 \\ 0.5 & 0.4 & 0.1 \end{pmatrix}, B=(0.90.050.050.010.90.090.00.050.95).B = \begin{pmatrix} 0.9 & 0.05 & 0.05 \\ 0.01 & 0.9 & 0.09 \\ 0.0 & 0.05 & 0.95 \end{pmatrix}.

t=1t=1 ("the"): v1(DT)=0.6×0.9=0.54v_1(\text{DT}) = 0.6 \times 0.9 = 0.54, v1(NN)=0.3×0.05=0.015v_1(\text{NN}) = 0.3 \times 0.05 = 0.015, v1(VB)=0.1×0.0=0.0v_1(\text{VB}) = 0.1 \times 0.0 = 0.0.

t=2t=2 ("dog"): v2(NN)=0.9×max⁡[0.54×0.8,0.015×0.1,0]=0.9×0.432=0.389v_2(\text{NN}) = 0.9 \times \max[0.54 \times 0.8, 0.015 \times 0.1, 0] = 0.9 \times 0.432 = 0.389.

t=3t=3 ("runs"): v3(VB)=0.95×max⁡[v2(DT)×0.1,v2(NN)×0.8,v2(VB)×0.1]v_3(\text{VB}) = 0.95 \times \max[v_2(\text{DT}) \times 0.1, v_2(\text{NN}) \times 0.8, v_2(\text{VB}) \times 0.1].

The Viterbi path recovers DT-NN-VB, the correct tagging.

python · numpy
import numpy as np

# Tags: 0=DT, 1=NN, 2=VB  |  Words: 0=the, 1=dog, 2=runs
pi = np.array([0.6, 0.3, 0.1])
A  = np.array([[0.1, 0.8, 0.1],
               [0.1, 0.1, 0.8],
               [0.5, 0.4, 0.1]])
B  = np.array([[0.9,  0.05, 0.05],
               [0.01, 0.9,  0.09],
               [0.0,  0.05, 0.95]])

sentence = [0, 1, 2]  # the dog runs
K, T = len(pi), len(sentence)

v    = np.zeros((T, K))
psi  = np.zeros((T, K), dtype=int)

v[0] = pi * B[:, sentence[0]]
for t in range(1, T):
    scores = v[t - 1, :, None] * A          # (K_prev, K_next)
    psi[t] = scores.argmax(axis=0)
    v[t]   = scores.max(axis=0) * B[:, sentence[t]]

# Backtrack
path = np.zeros(T, dtype=int)
path[-1] = v[-1].argmax()
for t in range(T - 2, -1, -1):
    path[t] = psi[t + 1, path[t + 1]]

tag_names = ["DT", "NN", "VB"]
word_names = ["the", "dog", "runs"]
print("Viterbi path:", [tag_names[y] for y in path])
print("Viterbi log-prob:", np.log(v[-1, path[-1]]))

BIO labeling for NER

NER uses the BIO tag scheme: Beginning of an entity span, Inside, Outside. The sentence "Apple announced new products" might tag as:

Apple (B-ORG),announced (O),new (O),products (O).\text{Apple}\ (B\text{-ORG}),\quad \text{announced}\ (O),\quad \text{new}\ (O),\quad \text{products}\ (O).

The HMM transition matrix encodes the BIO constraint: AO,I=0A_{O, I} = 0 (can't enter an inside tag from outside without a begin tag), enforced by setting that entry to zero before normalization.

HMM limitations

The emission probability p(xt∣yt)p(x_t \mid y_t) conditions only on the current word. It cannot use surrounding words as features: "run" and "runs" have independent emission distributions.

Exercises

Exercise 1. Using the lesson's parameters, complete the worked example: compute v2(NN)v_2(\text{NN}), then the three candidate values inside v3(VB)v_3(\text{VB}), and confirm the Viterbi path is DT-NN-VB. Recall π=[0.6,0.3,0.1]\pi = [0.6, 0.3, 0.1], the rows of AA as given, and the emission column for "runs" is B[:,runs]=[0.05,0.09,0.95]B[:, \text{runs}] = [0.05, 0.09, 0.95].

Solution

Step 1. From initialization, v1(DT)=0.6⋅0.9=0.54v_1(\text{DT}) = 0.6 \cdot 0.9 = 0.54, v1(NN)=0.3⋅0.05=0.015v_1(\text{NN}) = 0.3 \cdot 0.05 = 0.015, v1(VB)=0.1⋅0.0=0v_1(\text{VB}) = 0.1 \cdot 0.0 = 0.

Step 2. Compute v2(NN)v_2(\text{NN}) for "dog" (BNN,dog=0.9B_{\text{NN},\text{dog}} = 0.9). The transitions into NN are ADT,NN=0.8A_{\text{DT},\text{NN}} = 0.8, ANN,NN=0.1A_{\text{NN},\text{NN}} = 0.1, AVB,NN=0.4A_{\text{VB},\text{NN}} = 0.4:

v2(NN)=0.9⋅max⁡(0.54⋅0.8, 0.015⋅0.1, 0⋅0.4)=0.9⋅0.432=0.3888.v_2(\text{NN}) = 0.9 \cdot \max(0.54 \cdot 0.8,\ 0.015 \cdot 0.1,\ 0 \cdot 0.4) = 0.9 \cdot 0.432 = 0.3888.

The argmax is DT, so the backpointer ψ2(NN)=DT\psi_2(\text{NN}) = \text{DT}.

Step 3. For v3(VB)v_3(\text{VB}) at "runs" (BVB,runs=0.95B_{\text{VB},\text{runs}} = 0.95), the three candidates use the transition column into VB, A⋅,VB=[0.1,0.8,0.1]A_{\cdot,\text{VB}} = [0.1, 0.8, 0.1]. Taking v2(NN)=0.3888v_2(\text{NN}) = 0.3888 as the dominant predecessor (the other v2v_2 entries are far smaller):

v2(NN)⋅ANN,VB=0.3888⋅0.8=0.31104,v_2(\text{NN}) \cdot A_{\text{NN},\text{VB}} = 0.3888 \cdot 0.8 = 0.31104,

which dominates the DT and VB predecessors. So v3(VB)=0.95⋅0.31104≈0.2955v_3(\text{VB}) = 0.95 \cdot 0.31104 \approx 0.2955, with ψ3(VB)=NN\psi_3(\text{VB}) = \text{NN}.

Step 4. Backtrack: the max at t=3t=3 is VB, whose backpointer is NN, whose backpointer is DT. The path is DT-NN-VB, the correct tagging of "the dog runs".

Exercise 2. The BIO scheme forbids an O→IO \to I transition (an inside tag cannot start a span). Show that setting AO,I=0A_{O, I} = 0 before row normalization makes any tag sequence containing an OO immediately followed by an II have joint probability zero under the HMM.

Solution

Step 1. The HMM joint factorizes as p(x,y)=πy1∏tByt,xt∏t≥2Ayt−1,ytp(\mathbf{x}, \mathbf{y}) = \pi_{y_1} \prod_t B_{y_t, x_t} \prod_{t \ge 2} A_{y_{t-1}, y_t}. Every transition between consecutive tags enters as a multiplicative factor Ayt−1,ytA_{y_{t-1}, y_t}.

Step 2. Suppose the sequence has yt−1=Oy_{t-1} = O and yt=Iy_t = I at some position tt. Then the product contains the factor AO,IA_{O, I}.

Step 3. By construction AO,I=0A_{O, I} = 0. A single zero factor in a product of nonnegative terms makes the whole product zero, so p(x,y)=0p(\mathbf{x}, \mathbf{y}) = 0 regardless of the other factors.

Step 4. Since Viterbi maximizes this joint, it never selects a path through a zeroed transition (any competing path with nonzero probability beats it). The hard BIO constraint is enforced exactly, not just discouraged.

Exercise 3 (code). Run Viterbi on the lesson's HMM but change the emission so "dog" is much more likely a verb than a noun (BNN,dog=0.1B_{\text{NN},\text{dog}} = 0.1, BVB,dog=0.85B_{\text{VB},\text{dog}} = 0.85). Confirm the decoded tag for "dog" flips from NN to VB.

Solution
python · numpy
import numpy as np

pi = np.array([0.6, 0.3, 0.1])
A  = np.array([[0.1, 0.8, 0.1],
               [0.1, 0.1, 0.8],
               [0.5, 0.4, 0.1]])
# "dog" emission changed: now strongly a verb
B  = np.array([[0.9,  0.05, 0.05],
               [0.01, 0.10, 0.89],   # NN row: dog now only 0.10
               [0.0,  0.85, 0.15]])  # VB row: dog now 0.85

sentence = [0, 1, 2]  # the dog runs
K, T = len(pi), len(sentence)
v   = np.zeros((T, K))
psi = np.zeros((T, K), dtype=int)
v[0] = pi * B[:, sentence[0]]
for t in range(1, T):
    scores = v[t - 1, :, None] * A
    psi[t] = scores.argmax(axis=0)
    v[t]   = scores.max(axis=0) * B[:, sentence[t]]

path = np.zeros(T, dtype=int)
path[-1] = v[-1].argmax()
for t in range(T - 2, -1, -1):
    path[t] = psi[t + 1, path[t + 1]]

tag_names = ["DT", "NN", "VB"]
assert tag_names[path[1]] == "VB"   # "dog" now decoded as a verb
print("Viterbi path:", [tag_names[y] for y in path])

With the emission shifted, the noun hypothesis for "dog" loses to the verb hypothesis even though the DT-NN transition is favored, so the decoded middle tag flips to VB.

The next lesson introduces CRFs, which replace this generative model with a discriminative one that can use arbitrary features of the full input sentence.


References

  1. [1]Jurafsky, D., & Martin, J. H. (2024). Speech and Language Processing (3rd ed. draft). HMMs, POS tagging, and named entity recognition.↩
  2. [2]Manning, C. D., & Schütze, H. (1999). Foundations of Statistical Natural Language Processing. MIT Press. Markov models and tagging.↩
  3. [3]Rabiner, L. R. (1989). "A tutorial on hidden Markov models and selected applications in speech recognition". Proceedings of the IEEE, 77(2), 257-286. The standard reference for the forward, Viterbi, and Baum-Welch algorithms. doi:10.1109/5.18626↩