Conditional Random Fields › Computing the Partition Function
Language AI HandbookAll chapters Conditional Random Fields › Computing the Partition FunctionOne section · Sections in this chapter4 / 13 ↓
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
The partition function is the computational challenge of CRFs. It sums over all possible label sequences, and with labels and sequence length , there are such sequences. For labels and words, that's sequences, far too many to enumerate.

The Forward Algorithm
Fortunately, the linear-chain structure enables efficient computation via dynamic programming. The forward algorithm computes in time, similar to the forward algorithm for HMMs.
The key idea is to define a forward variable that accumulates scores for all partial label sequences ending in label at position :
where:
- : the forward variable at position for label
- : all possible label assignments for positions 1 through
- The sum aggregates scores over all ways to reach label at position
- The constraint fixes the label at position
Intuitively, represents the total "mass" of all paths that end in label at position . The useful property is that we can compute recursively from the previous position:
where:
- : iterates over all possible labels at position
- : the forward variable from the previous position
- : the score for transitioning from to at position
This recurrence says: to compute the total score of paths ending in at position , sum over all possible previous labels , multiplying the score of reaching by the score of the transition from to .
Once we've computed forward variables for all positions, the partition function is simply the sum over all labels at the final position:
where the sum is over all possible labels at the final position . This works because every complete label sequence must end with some label at position .
def forward_algorithm(words, labels, feature_weights, templates):
"""
Compute forward variables and partition function for a CRF.
Args:
words: list of words in the sequence
labels: list of possible labels
feature_weights: dict mapping feature names to weights
templates: list of feature templates
Returns:
alpha: forward variables, shape (n, K)
log_Z: log partition function
"""
n = len(words)
K = len(labels)
label_to_idx = {label: i for i, label in enumerate(labels)}
# Work in log space for numerical stability
# log_alpha[t, y] = log(alpha_t(y))
log_alpha = np.full((n, K), -np.inf)
# Initialize: t=0, no previous label (use special START)
for y_idx, y in enumerate(labels):
features = extract_features(words, 0, "<START>", y, templates)
score = sum(
feature_weights.get(f, 0.0) * v for f, v in features.items()
)
log_alpha[0, y_idx] = score
# Recurrence
for t in range(1, n):
for y_idx, y in enumerate(labels):
# Sum over all previous labels
log_scores = []
for y_prev_idx, y_prev in enumerate(labels):
features = extract_features(words, t, y_prev, y, templates)
score = sum(
feature_weights.get(f, 0.0) * v for f, v in features.items()
)
log_scores.append(log_alpha[t - 1, y_prev_idx] + score)
# Log-sum-exp for numerical stability
max_score = max(log_scores)
log_alpha[t, y_idx] = max_score + np.log(
sum(np.exp(s - max_score) for s in log_scores)
)
# Partition function: sum over final labels
max_final = np.max(log_alpha[n - 1, :])
log_Z = max_final + np.log(np.sum(np.exp(log_alpha[n - 1, :] - max_final)))
return log_alpha, log_Z
# Example with simple weights
example_labels = ["NOUN", "VERB", "DET", "ADJ"]
example_weights = {
"transition=('<START>', 'DET')": 2.0,
"transition=('DET', 'NOUN')": 2.5,
"transition=('DET', 'ADJ')": 1.5,
"transition=('ADJ', 'NOUN')": 2.0,
"current_word=('The', 'DET')": 3.0,
"current_word=('quick', 'ADJ')": 2.0,
"current_word=('fox', 'NOUN')": 2.5,
"current_word=('jumps', 'VERB')": 2.0,
}
simple_templates = [
("transition", lambda x, t, y_prev, y_cur: (y_prev, y_cur)),
("current_word", lambda x, t, y_prev, y_cur: (x[t], y_cur)),
]
log_alpha, log_Z = forward_algorithm(
sample_sentence, example_labels, example_weights, simple_templates
)Forward Algorithm Results: ============================================================ Sentence: ['The', 'quick', 'fox', 'jumps'] Labels: ['NOUN', 'VERB', 'DET', 'ADJ'] Log Forward Variables (log α_t(y)): ------------------------------------------------------------ Position Word NOUN VERB DET ADJ ------------------------------------------------------------ 0 The 0.00 0.00 5.00 0.00 1 quick 7.51 5.02 5.02 8.50 2 fox 13.10 8.86 8.86 8.93 3 jumps 13.37 15.15 13.15 13.19 Log Partition Function: log Z(x) = 15.5161 Partition Function: Z(x) = 5477008.5076
The forward variables show how probability mass accumulates through the sequence. At position 0, "The" has high scores for DET due to the matching feature weight. As we move through the sequence, the algorithm combines emission scores (how well each word matches each label) with transition scores (how likely each label sequence is). The log partition function aggregates all possible paths, enabling probability computation. Higher values indicate more probable label assignments at each position.


The forward algorithm exploits the linear-chain structure: at each position, we only need to consider transitions from labels at the previous position. This reduces the exponential enumeration to polynomial computation.
Log-Space Computation
Throughout CRF computations, we work in log space. Multiplying many probabilities (or exponentials of scores) quickly leads to numerical underflow. By using the log-sum-exp operation, we maintain numerical stability:
where:
- : the -th value we want to sum (in our case, log-scores for different label sequences)
- : the maximum value among all
- : the exponentiated value after subtracting the maximum
This identity is mathematically exact but numerically superior. The trick works because subtracting the maximum ensures that at least one term in the sum equals , while all other terms are less than 1. This prevents both overflow (from exponentiating large positive numbers) and underflow (from exponentiating large negative numbers).