Conditional Random Fields › Computing the Partition Function

Michael BrenndoerferUpdated May 6, 20259 min read
Language AI HandbookAll chapters
Part 6 of 67Sequence Labeling
Chapter 42Conditional Random Fields › Computing the Partition Function
Reading section 4 of 13 · Computing the Partition Function · 9 minSection 4 of 13 · 9 min

Book contents

Section 4 of 13 · 9 min read

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 Z(x)Z(\mathbf{x}) is the computational challenge of CRFs. It sums over all possible label sequences, and with KK labels and sequence length nn, there are KnK^n such sequences. For K=10K=10 labels and n=20n=20 words, that's 102010^{20} sequences, far too many to enumerate.

Out[12]:
Visualization
Line plot showing exponential growth of possible sequences as sequence length increases, with separate lines for different numbers of labels.
The exponential explosion of possible label sequences. With just 5 labels, a 20-word sentence has over 95 trillion possible labelings. This makes naive enumeration impossible and motivates the need for dynamic programming algorithms like the forward algorithm.

The Forward Algorithm

Fortunately, the linear-chain structure enables efficient computation via dynamic programming. The forward algorithm computes Z(x)Z(\mathbf{x}) in O(nK2)O(nK^2) time, similar to the forward algorithm for HMMs.

The key idea is to define a forward variable αt(y)\alpha_t(y) that accumulates scores for all partial label sequences ending in label yy at position tt:

αt(y)=∑y1:t−1exp⁡(∑t′=1t∑kλkfk(yt′−1,yt′,x,t′))where yt=y\alpha_t(y) = \sum_{\mathbf{y}_{1:t-1}} \exp\left( \sum_{t'=1}^{t} \sum_k \lambda_k f_k(y_{t'-1}, y_{t'}, \mathbf{x}, t') \right) \quad \text{where } y_t = y

where:

  • αt(y)\alpha_t(y): the forward variable at position tt for label yy
  • y1:t−1\mathbf{y}_{1:t-1}: all possible label assignments for positions 1 through t−1t-1
  • The sum aggregates scores over all ways to reach label yy at position tt
  • The constraint yt=yy_t = y fixes the label at position tt

Intuitively, αt(y)\alpha_t(y) represents the total "mass" of all paths that end in label yy at position tt. The useful property is that we can compute αt(y)\alpha_t(y) recursively from the previous position:

αt(y)=∑y′αt−1(y′)⋅exp⁡(∑kλkfk(y′,y,x,t))\alpha_t(y) = \sum_{y'} \alpha_{t-1}(y') \cdot \exp\left( \sum_k \lambda_k f_k(y', y, \mathbf{x}, t) \right)

where:

  • y′y': iterates over all possible labels at position t−1t-1
  • αt−1(y′)\alpha_{t-1}(y'): the forward variable from the previous position
  • exp⁡(∑kλkfk(y′,y,x,t))\exp\left( \sum_k \lambda_k f_k(y', y, \mathbf{x}, t) \right): the score for transitioning from y′y' to yy at position tt

This recurrence says: to compute the total score of paths ending in yy at position tt, sum over all possible previous labels y′y', multiplying the score of reaching y′y' by the score of the transition from y′y' to yy.

Once we've computed forward variables for all positions, the partition function is simply the sum over all labels at the final position:

Z(x)=∑yαn(y)Z(\mathbf{x}) = \sum_y \alpha_n(y)

where the sum is over all possible labels yy at the final position nn. This works because every complete label sequence must end with some label at position nn.

In[13]:
Code
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
)
Out[14]:
Console
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.

Out[15]:
Visualization
Heatmap with positions on x-axis, labels on y-axis, showing log-alpha values with color intensity.
Heatmap of log forward variables showing how probability mass accumulates through the sequence. Brighter cells indicate higher scores (more probable label assignments). Notice how DET dominates at position 0 for 'The', then ADJ becomes likely for 'quick', and NOUN for 'fox'. The pattern reveals how the CRF learns word-label associations.
Out[16]:
Visualization
Trellis diagram showing forward variable computation with arrows indicating transitions between label states.
Trellis diagram of the forward algorithm. Each node shows the log forward variable α for that label at that position. Arrows indicate transitions from t=0 to t=1. The partition function Z(x) sums exp(α) over all final labels.

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:

log⁡(∑iexi)=max⁡i(xi)+log⁡(∑iexi−max⁡i(xi))\log\left( \sum_i e^{x_i} \right) = \max_i(x_i) + \log\left( \sum_i e^{x_i - \max_i(x_i)} \right)

where:

  • xix_i: the ii-th value we want to sum (in our case, log-scores for different label sequences)
  • max⁡i(xi)\max_i(x_i): the maximum value among all xix_i
  • exi−max⁡i(xi)e^{x_i - \max_i(x_i)}: 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 e0=1e^0 = 1, while all other terms are less than 1. This prevents both overflow (from exponentiating large positive numbers) and underflow (from exponentiating large negative numbers).