CRF Training › The Forward-Backward Algorithm
Language AI HandbookAll chapters CRF Training › The Forward-Backward AlgorithmOne section · Sections in this chapter3 / 11 ↓
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
Computing the partition function by enumerating all sequences is intractable. The forward-backward algorithm exploits the sequential structure of the model to compute it in polynomial time.
The key insight is that the CRF score decomposes into local terms. At each position , the contribution depends only on the label at and the label at . This Markov property enables dynamic programming: we can build up the sum over all sequences incrementally, position by position.
Think of the forward-backward algorithm as a two-pass bookkeeping system for computing partial sums efficiently. The forward pass sweeps left to right, and at each position it records the total score of all partial paths that end at that position with each possible label. The backward pass sweeps right to left and records the total score of all partial paths that extend from each position to the end. When you multiply the forward score at a node with its backward score, you get the total score of all complete paths that pass through that node. This product is proportional to the marginal probability of being in that state at that position.
The algorithm draws its name and structure from the forward-backward algorithm for Hidden Markov Models, which you may have encountered in the previous chapter on HMMs. The CRF version works the same way conceptually, but in log space to avoid numerical underflow when sequences grow long. In practice, working in log space converts multiplications into additions and uses the log-sum-exp trick to combine scores safely. The transition from probability space to log space is a consistent theme in modern sequence modeling, and it applies here with the same motivation: multiplying many small probabilities together quickly produces values too tiny for floating-point arithmetic to represent accurately.
The forward-backward algorithm is an application of dynamic programming, which works whenever a problem has optimal substructure: the solution to a larger problem can be built from solutions to smaller subproblems. Here, the key observation is that summing over all paths through a trellis can be decomposed into summing over paths to each intermediate state and then combining those partial sums. This is exactly the same structure that makes the Viterbi algorithm efficient for finding the single best path. The forward-backward algorithm is essentially Viterbi's cousin, computing the sum over all paths rather than the maximum.
Forward Variables
Think of the forward variable as an accumulator. As you sweep through the sequence from left to right, the forward variable at each position records everything you need to know about paths that have arrived at each possible label state. You never need to remember which specific path got you there, only the total aggregated score of all such paths. This is the essence of dynamic programming: discard the details, keep the aggregate.
The forward variable represents the sum of scores for all partial label sequences that end with label at position :
where:
- : all possible label sequences from position 1 to
- : constrained to end with label
- The summation accumulates scores of all paths reaching state at time
We compute forward variables recursively:
Base case (t = 1):
where is a special start symbol.
Recursive case:
This recursion says: to get the total score of paths ending in at position , sum over all possible previous labels , multiplying the forward score at (all paths ending in ) by the transition score from to .
Notice that the recursion reuses the previous time step's forward variables rather than recomputing everything from scratch. Each requires only the values from , so the total storage is and the total computation is : for each of positions and each of possible current labels, we sum over possible previous labels. This is a dramatic improvement over the cost of brute-force enumeration.
The partition function is simply the sum of all forward variables at the final position:
Backward Variables
The backward variable is symmetric to the forward variable but runs in the opposite direction. While the forward variable answers "what is the total score of all paths that reach state at position coming from the left?", the backward variable answers "what is the total score of all paths that extend from state at position to the right end of the sequence?" Together, a forward value and a backward value bracket a specific state at a specific position, and their product gives the total score of all complete paths that pass through that state.
The backward variable represents the sum of scores for all partial sequences starting from position , given that position has label :
where is fixed.
Base case (t = T):
Recursive case:
Computing Marginal Probabilities
The forward and backward variables together give us the probability of any label at any position. In practice, this is why we run both passes: the forward pass alone gives us the partition function, but both passes together give us the full set of marginal probabilities that we need for gradient computation. The posterior marginal probability of label at position is:
And the probability of a specific transition from to at positions to :
where is the transition score matrix at position .
Let's implement the forward-backward algorithm:
import numpy as np
def forward_backward(log_potentials, transition_scores):
"""
Compute forward and backward variables for a CRF.
Args:
log_potentials: Array of shape (T, num_labels) with emission scores
transition_scores: Array of shape (num_labels, num_labels) with transition scores
Returns:
alpha: Forward variables (T, num_labels)
beta: Backward variables (T, num_labels)
log_Z: Log partition function
"""
T, num_labels = log_potentials.shape
# Forward pass (in log space for numerical stability)
log_alpha = np.zeros((T, num_labels))
log_alpha[0] = log_potentials[0]
for t in range(1, T):
for j in range(num_labels):
# Sum over all previous labels
scores = (
log_alpha[t - 1]
+ transition_scores[:, j]
+ log_potentials[t, j]
)
log_alpha[t, j] = np.logaddexp.reduce(scores)
# Backward pass
log_beta = np.zeros((T, num_labels))
log_beta[T - 1] = 0 # log(1) = 0
for t in range(T - 2, -1, -1):
for i in range(num_labels):
# Sum over all next labels
scores = (
log_beta[t + 1]
+ transition_scores[i, :]
+ log_potentials[t + 1]
)
log_beta[t, i] = np.logaddexp.reduce(scores)
# Partition function
log_Z = np.logaddexp.reduce(log_alpha[T - 1])
return log_alpha, log_beta, log_Z# Example: 5-position sequence with 3 labels (O, B-PER, I-PER)
np.random.seed(42)
T, num_labels = 5, 3
label_names = ["O", "B-PER", "I-PER"]
# Emission scores (from features)
log_potentials = np.random.randn(T, num_labels)
# Transition scores (learned)
transition_scores = np.random.randn(num_labels, num_labels)
# Make I-PER after O unlikely (BIO constraint approximation)
transition_scores[0, 2] = -5 # O -> I-PER
log_alpha, log_beta, log_Z = forward_backward(log_potentials, transition_scores)
# Compute marginal probabilities
log_marginals = log_alpha + log_beta - log_Z
marginals = np.exp(log_marginals)Forward-Backward Algorithm Results ================================================== Sequence length: 5 Number of labels: 3 Log partition function: 4.9991 Partition function Z(x): 148.2794 Marginal probabilities P(y_t = j | x): -------------------------------------------------- Position O B-PER I-PER -------------------------------------------------- 1 0.2108 0.3561 0.4332 2 0.5345 0.1528 0.3127 3 0.3839 0.5196 0.0965 4 0.3917 0.1740 0.4343 5 0.7945 0.0880 0.1175
Notice how the marginals sum to 1.0 at each position. This is because they represent the probability distribution over labels at that position, marginalized over all possible label sequences. The forward-backward algorithm computes these marginals in time, compared to for brute-force enumeration.
In practice, a few details require care. First, the raw forward and backward variables are products of many numbers less than one, so they underflow to zero for long sequences. Implementing everything in log space, as the code above does using np.logaddexp, avoids this problem. Second, the log-sum-exp computation must be done carefully: naively exponentiating large scores before summing can overflow. The standard trick is to subtract the maximum value before exponentiating, compute the sum, then add the maximum back. NumPy's logaddexp handles this internally.
The implementation above uses a simple nested loop that is easy to read but not fast. Real CRF libraries vectorize this computation using matrix operations: the recursion can be computed as a matrix-vector product in log space, reducing the constant factor significantly without changing the asymptotic complexity.

The trellis visualization shows how forward and backward passes combine. Each node's color intensity represents its marginal probability. States with high marginal probability (darker blue) are more likely to be part of the optimal sequence.