Part of Language AI Handbook
Covers Conditional Random Field training with the forward-backward algorithm, gradient computation, and L-BFGS optimization for sequence labeling tasks.
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
CRF Training
Conditional Random Fields are powerful models for sequence labeling, but their power comes with computational challenges. Unlike simple classifiers that make independent predictions for each token, CRFs model dependencies between labels across the entire sequence. Training a CRF means finding weights that maximize the probability of correct label sequences given their input features, while accounting for all possible alternative labelings.
To appreciate why training is non-trivial, consider what the model must accomplish. A CRF over a sentence of 20 tokens with 9 possible labels must implicitly reason over possible label sequences every time it evaluates the probability of a correct labeling. You cannot enumerate that many sequences at training time or at prediction time. Yet somehow the model must compute, to high precision, how probable the correct sequence is relative to every alternative. That requirement is what drives the mathematical machinery covered in this chapter.
The solution comes in two steps. First, the CRF restricts how labels can depend on each other: each label may only directly influence its immediate neighbor, not arbitrary positions elsewhere in the sequence. This Markov structure means the score of any complete label sequence can be decomposed into a product of local terms. Second, the forward-backward algorithm exploits that decomposition through dynamic programming, collapsing the exponential sum into a polynomial-time computation. These two ideas together make CRF training feasible.
Once you can compute gradients efficiently, the question becomes how to optimize. The log-likelihood objective for a CRF turns out to be concave, a property that guarantees any gradient-based optimizer will find the global maximum. But gradient descent converges slowly for this class of problems. The L-BFGS optimizer, a quasi-Newton method that builds a running estimate of the objective's curvature, typically reaches the optimum in tens to a few hundred iterations rather than thousands. Understanding why L-BFGS works so well here requires understanding what makes the CRF objective well-behaved compared to neural network loss surfaces.
Feature design and regularization round out the practical picture. The CRF itself is just a scoring function; the features you feed it determine what patterns it can and cannot learn. Well-designed features encode linguistic intuitions directly, letting a small training set teach the model meaningful distinctions. Regularization prevents the optimizer from chasing statistical noise in sparse features. This ensures that learned weights generalize to new text.
This chapter explores how to train CRFs effectively. You'll learn the mathematical foundation of the log-likelihood objective, understand why the forward-backward algorithm is essential for efficient gradient computation, see how modern optimizers like L-BFGS converge faster than simple gradient descent, and discover how feature templates and regularization shape what a CRF can learn. By the end, you'll be able to train CRFs on real sequence labeling tasks and understand what's happening under the hood.
The Log-Likelihood Objective
Training a CRF means finding feature weights that make correct label sequences probable. Given training data consisting of observation sequences and their correct label sequences , we want to maximize the conditional probability of seeing these correct labels.
The key insight is that we are learning a discriminative model, not a generative one. We never model the probability of observing a particular sentence; we only model the probability of a label sequence given that sentence. This is the same philosophical stance taken by logistic regression versus naive Bayes: rather than modeling the joint distribution , we model directly. For sequence labeling, this matters because modeling the sentence itself is hard (it requires a language model), while modeling labels conditioned on a sentence is much more tractable. The CRF inherits this advantage from its discriminative design.
Think of each training example as a contest between the correct label sequence and every competing alternative. The CRF assigns a score to each sequence using a weighted sum of features. Maximizing the log-likelihood of the correct sequences is equivalent to training the model to assign the correct sequence a score that is higher, on average, than the scores of all competing sequences. The partition function is what forces this competition: it aggregates the scores of all alternatives and normalizes them into a probability distribution. Making the correct sequence more probable necessarily means making alternative sequences less probable in relative terms.
The log-likelihood is the sum of log probabilities of the correct label sequences across all training examples. Taking the log converts products into sums and makes optimization numerically stable.
For a single training example with observation sequence and label sequence , the CRF defines the conditional probability as:
where:
- : the label sequence of length
- : the observation sequence (features at each position)
- : the -th feature function, which examines the previous label , current label , the full observation sequence , and position
- : the weight for feature (what we're learning)
- : the partition function, which sums over all possible label sequences to normalize probabilities
The partition function deserves special attention:
where:
- : ranges over all possible label sequences
- The sum includes sequences, where is the number of possible labels
This summation over exponentially many sequences is what makes CRF training computationally interesting. With 9 labels and 20 tokens, we'd have possible sequences. Enumerating them directly is impossible. Even if each evaluation took a nanosecond, computing the partition function by brute force would take longer than the age of the universe. The forward-backward algorithm, covered in the next section, is how we escape this combinatorial trap.
The Objective Function
Taking the log of the conditional probability gives us the log-likelihood for a single example:
where:
- The first term is the score of the correct label sequence, a simple weighted sum of features that fire along the correct path
- The second term, , is the log of the partition function and represents the log-sum-exp of scores over all possible sequences
The first term is cheap to compute: just walk along the correct label sequence and sum the feature weights at each step. The second term is the bottleneck. It requires, at least conceptually, evaluating the score of every possible label sequence and then computing a softmax-like normalization. This is where the forward-backward algorithm earns its keep.
Notice that the objective is a difference of two quantities. The first rewards the correct sequence by adding its score. The second penalizes all sequences by subtracting the log of their collective scored mass. Maximizing this difference means pushing the correct sequence score up while keeping the partition function from growing too large, which is exactly the behavior you want from a probabilistic classifier.
For the full training set with examples, we sum the log-likelihoods:
where:
- : the total log-likelihood as a function of all weights
- : the vector of all feature weights
Our goal is to find .
Let's visualize what this objective looks like for a simple case:

The log-likelihood function for CRFs has a important property: it is concave. This means there are no local maxima to get trapped in. Any hill-climbing algorithm will find the global optimum given enough iterations. This mathematical guarantee makes CRF training well-behaved compared to neural network training, where the loss surface contains local minima and saddle points.
The concavity follows from the log-sum-exp structure of the partition function. The log-sum-exp of linear functions is convex, so subtracting it from a linear function yields a concave result. In practice, this means you can initialize weights to zero and trust that any reasonable optimizer will converge to the same solution regardless of starting point. When your CRF training converges poorly, the culprit is almost always numerical issues, inappropriate learning rates, or degenerate features rather than getting stuck in a bad local optimum.
The log-likelihood objective for CRFs is deeply related to maximum entropy models, which were popular in NLP throughout the 1990s and early 2000s. A maximum entropy model (also called a log-linear model or multinomial logistic regression) assigns the distribution over labels that is as uniform as possible while matching the empirical feature expectations observed in training data. Lafferty, McCallum, and Pereira's landmark 2001 paper "Conditional Random Fields: Probabilistic Models for Segmenting and Labeling Sequence Data" extended this maximum entropy philosophy to sequential outputs by allowing label transitions to appear as features. The result was a structured model that inherited the convex training objective of maximum entropy classifiers while capturing the dependencies that positional classifiers ignore.
Consider a minimal CRF with two positions and two labels: P (person) and O (other). Suppose we have two features: if the current word is capitalized and the current label is P, and if the transition is PO. With weights and , and input tokens ["Alice", "runs"], the correct label sequence is [P, O].
The score of the correct sequence [P, O] is (feature 1 fires for "Alice"/P, feature 2 fires for the PO transition). The score of [O, P] is 0 (neither feature fires). The score of [P, P] is 2 (only feature 1 fires). The score of [O, O] is 0. The partition function . The probability of the correct sequence is , so its log-likelihood is approximately . If we increase to 3, the correct sequence gets more probability and the log-likelihood improves.
The Forward-Backward Algorithm
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.
Gradient Computation
To optimize the log-likelihood, we need its gradient with respect to each weight . The gradient has an elegant form that emerges from the structure of the CRF, and understanding this form gives deep insight into what training accomplishes.
Taking the derivative of the log-likelihood for a single example:
where:
- The first term is the observed feature count: how often feature fires on the correct label sequence
- The second term is the expected feature count: the expected number of times feature would fire under the model's current probability distribution
This gradient has an intuitive interpretation:
- If a feature fires more often in the training data than the model expects, increase its weight (positive gradient)
- If a feature fires less often than expected, decrease its weight (negative gradient)
- When observed counts match expected counts, the gradient is zero (we've reached a stationary point)
The key insight is that the gradient is a simple difference: observed minus expected. This mirrors the gradient of logistic regression and maximum entropy models. In fact, you can view CRF training as a natural generalization of logistic regression to structured outputs: instead of matching observed class frequencies with expected class frequencies, you match observed feature counts along label sequences with expected feature counts under the model's current distribution.
The elegance of this formulation makes the convergence criterion transparent. You know the model has converged when, for every feature, the number of times it fires in the training data equals the number of times the model expects it to fire. This is a moment-matching condition, and it has a beautiful statistical interpretation: the model has learned to reproduce the sufficient statistics of the training data under its own probability distribution.
This has a practical consequence. Features that are rare in the training data will have noisy observed counts, which means their gradients are noisy too. A feature that fires exactly once in a large dataset has an observed count of 1 and a very uncertain expected count. Regularization, which we cover later, is partly a response to this noise: by penalizing large weights, it prevents the optimizer from placing too much confidence in observations from very rare features.
Computing Expected Feature Counts
The expected feature count requires summing over all positions and all label pairs, weighted by their posterior probabilities. This is where the pairwise marginals come in:
where:
- : the posterior probability that the label pair occurs at positions and , marginalized over all other positions
- : the value of feature when the previous label is , the current label is , and we are at position
The forward-backward algorithm gives us exactly what we need: the pairwise marginals . Each call to forward-backward for a single training example produces all the information needed to compute the gradient contribution from that example. This is computationally efficient: rather than running a separate algorithm for inference and a separate algorithm for gradient computation, the forward-backward algorithm serves both purposes simultaneously.
def compute_gradient(
log_potentials, transition_scores, true_labels, log_alpha, log_beta, log_Z
):
"""
Compute gradient of log-likelihood for one training example.
Returns gradients for emission potentials and transition scores.
"""
T, num_labels = log_potentials.shape
# Gradient for emission scores
emission_grad = np.zeros_like(log_potentials)
# Observed counts: feature fires at correct positions
for t, label in enumerate(true_labels):
emission_grad[t, label] += 1.0
# Expected counts: subtract marginal probabilities
marginals = np.exp(log_alpha + log_beta - log_Z)
emission_grad -= marginals
# Gradient for transition scores
transition_grad = np.zeros_like(transition_scores)
# Observed transition counts
for t in range(1, len(true_labels)):
prev_label, curr_label = true_labels[t - 1], true_labels[t]
transition_grad[prev_label, curr_label] += 1.0
# Expected transition counts
for t in range(1, T):
for i in range(num_labels):
for j in range(num_labels):
log_prob = (
log_alpha[t - 1, i]
+ transition_scores[i, j]
+ log_potentials[t, j]
+ log_beta[t, j]
- log_Z
)
transition_grad[i, j] -= np.exp(log_prob)
return emission_grad, transition_grad# Example: gradient computation
true_labels = [0, 1, 2, 2, 0] # O, B-PER, I-PER, I-PER, O
emission_grad, transition_grad = compute_gradient(
log_potentials, transition_scores, true_labels, log_alpha, log_beta, log_Z
)Gradient Analysis ================================================== True labels: ['O', 'B-PER', 'I-PER', 'I-PER', 'O'] Emission Gradient (observed - expected): -------------------------------------------------- Position O B-PER I-PER -------------------------------------------------- 1 +0.7892 -0.3561 -0.4332 ← true: O 2 -0.5345 +0.8472 -0.3127 ← true: B-PER 3 -0.3839 -0.5196 +0.9035 ← true: I-PER 4 -0.3917 -0.1740 +0.5657 ← true: I-PER 5 +0.2055 -0.0880 -0.1175 ← true: O Transition Gradient (sample): -------------------------------------------------- O → O: -1.0741 O → B-PER: +0.5573 B-PER → O: -0.2358 B-PER → B-PER: -0.0744 B-PER → I-PER: +0.1078 I-PER → O: +0.2053 I-PER → B-PER: -0.4172 I-PER → I-PER: +0.9352
The gradient reveals which adjustments would improve the model. Positive gradients at the true labels mean the model should increase those scores. Negative gradients at incorrect labels mean those should be suppressed. The transition gradient shows similar patterns: transitions that occurred in the true sequence but have low model probability will have positive gradients.
In practice, you will often visualize gradient magnitudes during training to diagnose convergence problems. A gradient norm that decreases steadily toward zero indicates smooth convergence. A gradient norm that oscillates or increases may indicate a learning rate that is too large (for gradient descent) or a numerical issue in the forward-backward computation. A gradient norm that plateaus above zero for many iterations suggests the model has hit a flat region and may benefit from better initialization or regularization.
One subtlety worth understanding: the gradient computation above is for a single training example. In practice, we sum gradients across all training examples and optionally normalize by the number of examples. When computing the gradient over the full training set, you are essentially comparing the model's expected feature frequencies with the empirical feature frequencies in the corpus. The model is trying to learn a distribution that matches the corpus statistics exactly, subject to the constraints imposed by the feature set and the regularization penalty.

L-BFGS Optimization
Simple gradient descent can optimize the CRF objective, but it converges slowly. Each step moves in the direction of the gradient, but the step size is tricky to tune: too large and you overshoot, too small and you crawl toward the optimum.
L-BFGS (Limited-memory Broyden-Fletcher-Goldfarb-Shanno) is a quasi-Newton method that approximates the curvature of the objective function. Instead of using only the gradient direction, it uses gradient history to estimate how the objective curves in different directions, enabling larger steps in flat directions and smaller steps in steep directions.
L-BFGS is a quasi-Newton optimization algorithm that uses a limited memory approximation to the inverse Hessian matrix. It converges much faster than gradient descent for convex problems like CRF training, typically requiring 50-200 iterations instead of thousands.
The Broyden-Fletcher-Goldfarb-Shanno (BFGS) algorithm was independently discovered by four researchers in 1970 and has since become one of the most widely used optimization algorithms in scientific computing. The original BFGS method maintains a dense approximation to the inverse Hessian matrix, which requires memory for parameters. This is impractical for problems with thousands or millions of parameters. Jorge Nocedal addressed this in 1980 by proposing the L-BFGS variant, which stores only the last pairs of gradient differences rather than the full matrix. By using a two-loop recursion discovered later, L-BFGS can apply the implicit inverse Hessian approximation using only these stored vectors, reducing memory from to . For CRF training, where features can number in the millions but is typically just 5 to 20, this is a massive practical improvement. L-BFGS became the de facto optimizer for CRF training and maximum entropy models throughout the 2000s.
To understand why L-BFGS converges faster than gradient descent, consider what the Hessian tells you. The Hessian matrix of second derivatives describes the curvature of the objective in every direction. Directions with high curvature (large second derivatives) require small steps to avoid overshooting; directions with low curvature (small second derivatives) can tolerate large steps. Gradient descent uses the same step size (or a line-searched step size) in all directions, which means it is either too aggressive in high-curvature directions or too conservative in low-curvature ones. By approximating the inverse Hessian, L-BFGS can scale each direction appropriately, taking large steps in flat directions and small steps in steep ones. This is the key reason it converges in far fewer iterations.
For CRF training specifically, the curvature of the log-likelihood objective depends on the feature correlations in the training data. Features that are highly correlated with each other create directions of low curvature that gradient descent handles poorly. L-BFGS handles this naturally by building up an estimate of these correlations over iterations.
The key advantages of L-BFGS for CRF training are:
- Faster convergence: Uses curvature information to take better steps
- No learning rate tuning: Line search automatically finds good step sizes
- Memory efficient: Stores only the last gradient pairs (typically ), not the full Hessian
- Stable: Works well on the convex CRF objective without careful hyperparameter tuning
def crf_objective(params, X_features, y_sequences, num_labels):
"""
Compute negative log-likelihood and gradient for CRF.
Args:
params: Flattened parameter vector (emissions + transitions)
X_features: List of feature matrices, one per sequence
y_sequences: List of label sequences
num_labels: Number of possible labels
Returns:
neg_ll: Negative log-likelihood (to minimize)
neg_grad: Negative gradient
"""
# Unpack parameters
# For simplicity, we use emission weights per label and transition matrix
emission_weights = params[:num_labels]
transition_matrix = params[num_labels:].reshape(num_labels, num_labels)
total_ll = 0.0
total_emission_grad = np.zeros(num_labels)
total_transition_grad = np.zeros((num_labels, num_labels))
for X, y in zip(X_features, y_sequences):
T = len(y)
# Compute emission potentials
log_potentials = np.outer(np.ones(T), emission_weights) + X
# Forward-backward
log_alpha, log_beta, log_Z = forward_backward(
log_potentials, transition_matrix
)
# Log-likelihood contribution
sequence_score = sum(log_potentials[t, y[t]] for t in range(T))
sequence_score += sum(
transition_matrix[y[t - 1], y[t]] for t in range(1, T)
)
total_ll += sequence_score - log_Z
# Gradient contribution
emission_grad, transition_grad = compute_gradient(
log_potentials, transition_matrix, y, log_alpha, log_beta, log_Z
)
total_emission_grad += emission_grad.sum(axis=0)
total_transition_grad += transition_grad
# Return negative (for minimization)
neg_grad = np.concatenate(
[-total_emission_grad, -total_transition_grad.flatten()]
)
return -total_ll, neg_grad# Generate synthetic training data
np.random.seed(42)
num_sequences = 50
seq_length = 10
num_labels = 3
# Create synthetic sequences with some structure
X_features = []
y_sequences = []
for _ in range(num_sequences):
# Random features
X = np.random.randn(seq_length, num_labels) * 0.5
# Generate labels with realistic BIO patterns
y = []
in_entity = False
for t in range(seq_length):
if in_entity:
if np.random.random() < 0.7: # Continue entity
y.append(2) # I-PER
else: # End entity
y.append(0) # O
in_entity = False
else:
if np.random.random() < 0.2: # Start entity
y.append(1) # B-PER
in_entity = True
else:
y.append(0) # O
X_features.append(X)
y_sequences.append(y)
# Initial parameters
init_params = np.zeros(num_labels + num_labels * num_labels)
# Track optimization progress
history = {"ll": [], "grad_norm": []}
def callback(params):
ll, grad = crf_objective(params, X_features, y_sequences, num_labels)
history["ll"].append(-ll) # Convert back to positive LL
history["grad_norm"].append(np.linalg.norm(grad))from scipy.optimize import minimize
# Run L-BFGS optimization
result = minimize(
crf_objective,
init_params,
args=(X_features, y_sequences, num_labels),
method="L-BFGS-B",
jac=True, # Function returns both value and gradient
callback=callback,
options={"maxiter": 100, "disp": False},
)
optimal_params = result.xL-BFGS Optimization Results ================================================== Number of sequences: 50 Sequence length: 10 Number of labels: 3 Optimization converged: True Number of iterations: 50 Number of function evaluations: 61 Final log-likelihood: -295.0034 Final gradient norm: 0.007801 Learned emission weights: O: +7.4421 B-PER: +5.7706 I-PER: -13.2127 Learned transition matrix: From/To O B-PER I-PER O +2.9210 +2.4835 -26.2274 B-PER +2.2342 -13.0289 +23.8245 I-PER +2.1959 -17.7993 +23.3965


The convergence plots show the characteristic behavior of L-BFGS on a convex problem. The log-likelihood rises steeply at first, then levels off as we approach the optimum. The gradient norm drops exponentially, indicating we're getting closer to a point where the gradient is zero.
Notice that the gradient norm does not decrease monotonically at each step. L-BFGS performs a line search along the descent direction computed from the approximate inverse Hessian, and this line search may occasionally accept a step that temporarily increases the gradient norm. This is normal and expected: the optimizer is exploring the curvature structure of the objective in order to build a better approximation. Over a window of several iterations, the trend should be consistently downward.
In practice, L-BFGS convergence on a well-conditioned CRF typically takes 50 to 200 iterations. The exact count depends on the number of training examples, the number of features, and the regularization strength. Stronger regularization generally leads to faster convergence because it smooths the objective and reduces the condition number of the Hessian. If your CRF training is taking thousands of iterations to converge, suspect either a very large feature set with many correlated features, a very small regularization coefficient, or numerical issues in the gradient computation.
Feature Template Design
CRF features are the heart of the model. Well-designed features capture the patterns that distinguish correct label sequences from incorrect ones. Feature templates define how to extract features from the input, and the CRF learns which features matter most.
A feature template is a pattern that generates binary features from observations. For example, the template "current word = X" generates one feature for each unique word X in the vocabulary. Templates let you define feature types; the actual features are instantiated from training data.
The distinction between a feature template and a feature instance matters. A template like "current word = X" is a recipe that generates thousands of concrete features, one for each vocabulary word. You define the template once; the feature extraction code instantiates all the concrete features automatically when it encounters each word in the training data. This design is elegant because it allows the feature set to scale with the vocabulary without requiring you to enumerate every possible feature in advance.
Feature design for CRFs is both an art and an empirical practice. The goal is to encode enough signal that the CRF can distinguish between correct and incorrect label assignments, without being so redundant or noisy that the model overfits to training artifacts. A useful mental model is to think about what a linguistically informed human annotator would notice when deciding on a label. If a feature captures something that a human would use as evidence, it is probably worth including. If it captures something accidental about the training data, it will hurt generalization.
Common Feature Types
Features for sequence labeling typically fall into several categories:
- Lexical features: The word itself, lowercase form, word shape (capitalization pattern)
- Contextual features: Surrounding words in a window
- Morphological features: Prefixes, suffixes, presence of digits or punctuation
- Part-of-speech features: POS tags from a tagger (if available)
- Transition features: Combinations of previous and current labels
Each category captures a different kind of evidence. Lexical features are the most direct: the word "London" is almost always a location. But lexical features are also the most data-hungry: to learn that "London" is a location, the model needs to see it in training data. Morphological features generalize better because they fire for unseen words: a word ending in "-shire" is probably a British county even if the specific word never appeared in training. Context features capture relational information: a capitalized word immediately after "President" is likely a person's name.
The most powerful features are often combinations: "the previous word is 'Dr.' and the current word is capitalized" is a much stronger signal for a person name than either feature alone. CRFs can represent such combinations as conjunctive features, and many feature engineering libraries automatically generate pairwise combinations of base features to capture these interactions.
def extract_features(tokens, position):
"""
Extract features for a token at a given position.
Returns a dictionary of feature_name: value pairs.
Features are designed for NER tagging.
"""
features = {}
word = tokens[position]
# Current word features
features["word.lower"] = word.lower()
features["word.isupper"] = word.isupper()
features["word.istitle"] = word.istitle()
features["word.isdigit"] = word.isdigit()
# Word shape
shape = ""
for char in word:
if char.isupper():
shape += "X"
elif char.islower():
shape += "x"
elif char.isdigit():
shape += "d"
else:
shape += char
features["word.shape"] = shape
# Prefix/suffix
features["word.prefix2"] = (
word[:2].lower() if len(word) >= 2 else word.lower()
)
features["word.suffix2"] = (
word[-2:].lower() if len(word) >= 2 else word.lower()
)
features["word.prefix3"] = (
word[:3].lower() if len(word) >= 3 else word.lower()
)
features["word.suffix3"] = (
word[-3:].lower() if len(word) >= 3 else word.lower()
)
# Context features
if position > 0:
prev_word = tokens[position - 1]
features["prev.word.lower"] = prev_word.lower()
features["prev.word.istitle"] = prev_word.istitle()
else:
features["BOS"] = True # Beginning of sentence
if position < len(tokens) - 1:
next_word = tokens[position + 1]
features["next.word.lower"] = next_word.lower()
features["next.word.istitle"] = next_word.istitle()
else:
features["EOS"] = True # End of sentence
return features# Example: extract features for a sample sentence
sample_sentence = [
"John",
"Smith",
"works",
"at",
"Google",
"in",
"California",
".",
]
sample_labels = ["B-PER", "I-PER", "O", "O", "B-ORG", "O", "B-LOC", "O"]
all_features = []
for i in range(len(sample_sentence)):
features = extract_features(sample_sentence, i)
all_features.append(features)Feature Extraction Example ============================================================ Sentence: John Smith works at Google in California . Labels: ['B-PER', 'I-PER', 'O', 'O', 'B-ORG', 'O', 'B-LOC', 'O'] Features for selected tokens: ------------------------------------------------------------ Token: 'John' (Label: B-PER) BOS: True next.word.istitle: True next.word.lower: smith word.isdigit: False word.istitle: True word.isupper: False word.lower: john word.prefix2: jo word.prefix3: joh word.shape: Xxxx word.suffix2: hn word.suffix3: ohn Token: 'Google' (Label: B-ORG) next.word.istitle: False next.word.lower: in prev.word.istitle: False prev.word.lower: at word.isdigit: False word.istitle: True word.isupper: False word.lower: google word.prefix2: go word.prefix3: goo word.shape: Xxxxxx word.suffix2: le word.suffix3: gle Token: 'California' (Label: B-LOC) next.word.istitle: False next.word.lower: . prev.word.istitle: False prev.word.lower: in word.isdigit: False word.istitle: True word.isupper: False word.lower: california word.prefix2: ca word.prefix3: cal word.shape: Xxxxxxxxxx word.suffix2: ia word.suffix3: nia
The feature extractor captures patterns that are predictive of entity labels. Title-cased words are often names. Words ending in "-ia" might be locations. Words preceded by "at" might be organizations. The CRF learns which of these patterns are reliable predictors.
One thing to notice about the feature extractor above is that it uses the raw word itself as one of the features. This creates one binary feature per unique word in the vocabulary, and those features will have zero weight for any word not seen in training. This is the "unknown word" problem, and it is one of the fundamental challenges in classical NLP. Morphological features like prefixes and suffixes partially address it: even an unseen word shares its suffix with many seen words, allowing the model to make reasonable guesses. Treating unknown words as a special token is another common approach, at the cost of losing information about what the unknown word looks like.
Feature Templates in Practice
Real NER systems use extensive feature templates. The richness of the feature set often matters more than the choice of optimizer or regularization strength. McCallum and Li's influential 2003 paper on CRF-based NER used over 100,000 features on the CoNLL 2003 benchmark, carefully designed to capture capitalization, word shapes, gazetteers of known names, and contextual patterns from several words before and after each token. At the time, this was state of the art for English NER.
The sklearn-crfsuite library provides a convenient way to train CRFs with custom features:
# Install sklearn-crfsuite for CRF training
# uv pip install sklearn-crfsuitedef prepare_sentence_features(tokens, labels):
"""Convert a sentence to CRF training format."""
sentence_features = []
for i in range(len(tokens)):
features = extract_features(tokens, i)
# Convert to string features for sklearn-crfsuite
str_features = {k: str(v) for k, v in features.items()}
sentence_features.append(str_features)
return sentence_features, labels
# Create a small dataset for demonstration
sentences = [
(
["John", "Smith", "works", "at", "Google", "."],
["B-PER", "I-PER", "O", "O", "B-ORG", "O"],
),
(
["Mary", "visited", "Paris", "last", "summer", "."],
["B-PER", "O", "B-LOC", "O", "O", "O"],
),
(
["Apple", "announced", "a", "new", "iPhone", "."],
["B-ORG", "O", "O", "O", "O", "O"],
),
(
["Dr.", "Jones", "from", "MIT", "gave", "a", "talk", "."],
["O", "B-PER", "O", "B-ORG", "O", "O", "O", "O"],
),
(
["The", "Eiffel", "Tower", "is", "in", "France", "."],
["O", "B-LOC", "I-LOC", "O", "O", "B-LOC", "O"],
),
]
# Prepare training data
X_train = []
y_train = []
for tokens, labels in sentences:
features, lbls = prepare_sentence_features(tokens, labels)
X_train.append(features)
y_train.append(lbls)import sklearn_crfsuite
# Train CRF model
crf = sklearn_crfsuite.CRF(
algorithm="lbfgs",
c1=0.1, # L1 regularization
c2=0.1, # L2 regularization
max_iterations=100,
all_possible_transitions=True,
)
crf.fit(X_train, y_train)CRF Training Complete ================================================== Number of features: 122 Number of labels: 6 Labels: ['B-PER', 'I-PER', 'O', 'B-ORG', 'B-LOC', 'I-LOC'] Top positive features by label: -------------------------------------------------- B-PER: word.prefix2:jo: +1.0393 word.shape:Xxxx: +0.9872 word.lower:jones: +0.3844 I-PER: word.lower:smith: +0.4325 word.prefix2:sm: +0.4325 word.suffix2:th: +0.4325 O: word.istitle:False: +2.4189 word.isupper:False: +1.5046 word.isdigit:False: +0.7216 B-ORG: word.suffix2:le: +1.2615 word.lower:mit: +0.4305 word.isupper:True: +0.4305 B-LOC: word.shape:Xxxxxx: +0.6492 word.lower:paris: +0.3769 word.prefix2:pa: +0.3769 I-LOC: word.lower:tower: +0.4375 word.prefix2:to: +0.4375 word.prefix3:tow: +0.4375
The learned feature weights reveal what the CRF considers important. High positive weights for "word.istitle=True" on B-PER indicate that title-cased words are strong signals for person names. The CRF automatically discovers these patterns from the training data.
Inspecting feature weights is one of the most valuable diagnostic tools for CRF practitioners. Unlike neural networks, where the learned representations are distributed and opaque, CRF weights are directly interpretable: each weight has a straightforward meaning as the log-odds contribution of that feature to that label. A large positive weight for "word[-3:]=ian" on I-PER means the model has learned that words ending in "-ian" (like "Iranian" or "Brazilian") often appear inside person entity spans. A large negative weight for the same feature on B-ORG means such words are unlikely to start organization spans. Debugging a CRF that makes systematic errors often starts with examining the weights of features that seem relevant to the error type.
Regularization
Without regularization, CRF training can overfit to the training data. If a feature appears only once in training and perfectly predicts a label, the optimal weight is infinity. Regularization prevents extreme weights and improves generalization.
Regularization adds a penalty term to the objective function that discourages large parameter values. This prevents overfitting by trading off training set fit against model complexity.
To understand why CRFs need regularization, consider what happens with a feature like "word=Schenectady" that fires exactly once in the training data, where the word was labeled B-LOC. If we maximize log-likelihood without any penalty, the optimal weight for this feature approaches infinity: making that weight arbitrarily large makes the CRF assign arbitrarily high probability to labeling "Schenectady" as B-LOC, which perfectly explains the one training example. But at test time, if the model sees "Schenectady" in a context where it is labeled differently (perhaps in a business name), the overly large weight will cause errors.
Regularization resolves this by adding a cost for large weights. The optimizer must balance two competing objectives: maximize the fit to training data, and keep weights small. For features that appear only once or twice, the training data provides weak evidence, so the optimizer accepts some loss in training fit to keep the weight reasonable. For features that appear hundreds of times with consistent labels, the training signal is strong enough to overcome the regularization cost, and the weight grows to reflect the recurring pattern.
L1 and L2 Regularization
The regularized log-likelihood objective is:
where:
- : the original log-likelihood
- : L1 regularization coefficient (larger = less regularization)
- : L2 regularization coefficient (larger = less regularization)
- : absolute value of weight (L1 penalty)
- : squared weight (L2 penalty)
L1 and L2 regularization have different effects:
| Regularization | Penalty | Effect | Use Case |
|---|---|---|---|
| L1 | Drives weights to exactly zero | Feature selection, sparse models | |
| L2 | Shrinks all weights toward zero | General regularization | |
| L1 + L2 | Both | Combines sparsity with shrinkage | Often works best in practice |
# Compare different regularization strengths
regularization_strengths = [0.01, 0.1, 1.0, 10.0]
results = []
for c in regularization_strengths:
crf_reg = sklearn_crfsuite.CRF(
algorithm="lbfgs",
c1=c,
c2=c,
max_iterations=100,
all_possible_transitions=True,
)
crf_reg.fit(X_train, y_train)
# Count non-zero features
nonzero_features = sum(
1 for _, weight in crf_reg.state_features_.items() if abs(weight) > 1e-6
)
# Get max weight magnitude
max_weight = max(
abs(weight) for _, weight in crf_reg.state_features_.items()
)
results.append(
{"c": c, "nonzero": nonzero_features, "max_weight": max_weight}
)Effect of Regularization Strength ================================================== C (reg coef) Non-zero features Max |weight| -------------------------------------------------- 0.01 141 3.3624 0.10 122 2.4189 1.00 25 1.4400 10.00 3 0.2209


The regularization plots show clear trends. With strong regularization (small C), many features get zeroed out, creating a sparse model that uses only the most informative features. Maximum weight magnitudes also decrease, preventing the model from over-relying on any single feature.
Choosing the right regularization strength is a hyperparameter tuning problem. The common practice is to try a geometric grid of values, such as , and evaluate each on a held-out development set using the F1 score or other task-specific metric. For NER, the metric of interest is usually entity-level F1, which requires predicting both the boundary and the type of each entity correctly. A regularization coefficient that maximizes token-level accuracy might produce many boundary errors; measuring F1 at the entity level is more informative for the deployment use case.
In practice, L2 regularization () is the safer default choice because it does not produce discontinuous gradients. L1 regularization produces a gradient discontinuity at zero: the subgradient at is not unique, which requires special handling in gradient-based optimizers. L-BFGS in scipy handles L1 via the OWL-QN extension, which operates on the L-BFGS-B variant. The key practical difference is that L1 often produces better-compressed models (many exact zeros) while L2 tends to produce models where all features have small nonzero weights. For deployment scenarios where model size or inference speed matters, L1 or combined L1+L2 may be preferable despite the added optimization complexity.
Training a Complete NER Model
Let's put everything together and train a CRF on a real NER dataset. This section is both a demonstration of the full pipeline and a worked example of how the concepts from earlier sections appear in a practical setting. the training loop implicitly performs the forward-backward pass for each sentence during each L-BFGS iteration, how the regularization parameters shape the learned feature weights, and how the transition matrix encodes the label structure of the NER task.
from nltk.corpus import conll2002
# Load Spanish NER data (smaller than English, good for demonstration)
train_sents = list(conll2002.iob_sents("esp.train"))[
:100
] # Use subset for speed
test_sents = list(conll2002.iob_sents("esp.testa"))[:30]
def word2features(sent, i):
"""Extract features for word at position i in sentence."""
word = sent[i][0]
postag = sent[i][1]
features = {
"bias": 1.0,
"word.lower": word.lower(),
"word[-3:]": word[-3:],
"word[-2:]": word[-2:],
"word.isupper": word.isupper(),
"word.istitle": word.istitle(),
"word.isdigit": word.isdigit(),
"postag": postag,
"postag[:2]": postag[:2],
}
if i > 0:
word1 = sent[i - 1][0]
postag1 = sent[i - 1][1]
features.update(
{
"-1:word.lower": word1.lower(),
"-1:word.istitle": word1.istitle(),
"-1:postag": postag1,
}
)
else:
features["BOS"] = True
if i < len(sent) - 1:
word1 = sent[i + 1][0]
postag1 = sent[i + 1][1]
features.update(
{
"+1:word.lower": word1.lower(),
"+1:word.istitle": word1.istitle(),
"+1:postag": postag1,
}
)
else:
features["EOS"] = True
return features
def sent2features(sent):
return [word2features(sent, i) for i in range(len(sent))]
def sent2labels(sent):
return [label for token, postag, label in sent]
# Prepare data
X_train_ner = [sent2features(s) for s in train_sents]
y_train_ner = [sent2labels(s) for s in train_sents]
X_test_ner = [sent2features(s) for s in test_sents]
y_test_ner = [sent2labels(s) for s in test_sents]# Train the CRF
crf_ner = sklearn_crfsuite.CRF(
algorithm="lbfgs",
c1=0.1,
c2=0.1,
max_iterations=50,
all_possible_transitions=True,
)
crf_ner.fit(X_train_ner, y_train_ner)
# Make predictions
y_pred_ner = crf_ner.predict(X_test_ner)NER Model Training Results
============================================================
Training sentences: 100
Test sentences: 30
Labels: ['B-LOC', 'B-MISC', 'B-ORG', 'B-PER', 'I-LOC', 'I-MISC', 'I-ORG', 'I-PER', 'O']
Classification Report (entity labels only):
------------------------------------------------------------
precision recall f1-score support
B-LOC 0.750 0.484 0.588 31
B-ORG 0.643 0.250 0.360 36
B-PER 0.435 0.625 0.513 16
I-PER 0.452 0.560 0.500 25
B-MISC 0.200 0.375 0.261 8
I-ORG 0.421 0.333 0.372 24
I-LOC 0.000 0.000 0.000 19
I-MISC 0.317 0.722 0.441 18
micro avg 0.442 0.407 0.424 177
macro avg 0.402 0.419 0.379 177
weighted avg 0.464 0.407 0.400 177
Weighted F1 Score: 0.4003Example Predictions: ============================================================ Sentence 1: -------------------------------------------------- Token True Predicted Match -------------------------------------------------- Sao B-LOC B-PER ✗ Paulo I-LOC I-PER ✗ ( O O ✓ Brasil B-LOC B-LOC ✓ ) O O ✓ , O O ✓ 23 O O ✓ may O O ✓ ( O O ✓ EFECOM B-ORG B-ORG ✓ ... Sentence 2: -------------------------------------------------- Token True Predicted Match -------------------------------------------------- - O O ✓ Sentence 3: -------------------------------------------------- Token True Predicted Match -------------------------------------------------- La O O ✓ multinacional O O ✓ española O O ✓ Telefónica B-ORG O ✗ ha O O ✓ impuesto O O ✓ un O O ✓ récord O O ✓ mundial O O ✓ al O O ✓ ...
The trained CRF achieves reasonable performance even on this limited training set. The model correctly identifies many entities by learning patterns from the feature templates. Errors often occur at entity boundaries or with rare entity types that don't have enough training examples.
Even with just 100 training sentences, the model captures meaningful patterns. This is one of the strengths of CRFs relative to purely neural approaches: by encoding domain knowledge into features, you can get useful performance with limited data. A neural model trained on 100 sentences would struggle to generalize, while the CRF with good features can use general linguistic knowledge (capitalization, word shape, context) that applies across sentences. The tradeoff is that the CRF cannot discover patterns that you did not think to encode as features, while a neural model might discover them from raw text given enough data.

The transition matrix reveals that the CRF has learned BIO constraints. High weights appear for valid transitions like O→B-LOC (starting a location entity) and B-LOC→I-LOC (continuing a location). Low or negative weights appear for invalid transitions like O→I-LOC (continuation without a beginning).
This automatic discovery of BIO constraints is one of the most useful practical advantages of CRFs over independent classifiers. A per-token classifier trained to predict NER labels will sometimes predict I-PER at the start of a sentence, which violates the BIO convention and is meaningless. The CRF cannot make that mistake: the learned transition weight from O (or from the start symbol) to I-PER will be strongly negative, because the training data never contains such a transition. The model is implicitly learning the structural rules of the tagging scheme from examples, without needing those rules to be hard-coded.
An important nuance: the CRF only learns BIO constraints as soft preferences, not hard rules. If the evidence from the emission features is strong enough (e.g., the word strongly looks like it continues an entity), the model can still produce invalid BIO sequences when the transition cost does not overwhelm the emission score. Some practitioners handle this by using a post-processing step that converts CRF output to valid BIO sequences, or by using a constrained Viterbi decoding that enforces hard BIO constraints during inference.
Limitations and Impact
CRF training has both strengths and limitations that shape when and how to use these models effectively.
The forward-backward algorithm makes CRF training tractable, but it still requires time per sequence. For tasks with many labels like fine-grained NER or semantic role labeling, this quadratic scaling becomes expensive. A sequence with 50 labels and 100 tokens requires computing 250,000 transition scores per training example. This cost compounds across thousands of training sentences and hundreds of optimization iterations. For real-time applications or very large training corpora, even this polynomial cost can become a bottleneck, and practitioners sometimes turn to approximate inference methods or batch the training examples to exploit parallelism.
Feature engineering remains both a strength and weakness. Hand-crafted features like word shapes, prefixes, and context windows capture linguistic knowledge effectively. But designing features requires domain expertise and extensive experimentation. Features that work well for English NER may perform poorly on German or Japanese: German capitalizes all nouns, so capitalization is a weaker signal for named entities; Japanese has no spaces between words, making the notion of a "word" itself a pre-processing challenge. Adapting a CRF to a new language or domain often requires redesigning the feature set from scratch, which is time-consuming and requires linguistic knowledge of the target language.
CRFs also struggle with long-range dependencies. The Markov assumption means each label depends only on the immediately preceding label. If a token's correct label depends on context five words away, the CRF must propagate that information through intervening transitions. In practice, this limits how much global context the model can exploit. You can partially compensate by including context features that look several words in either direction, but these are fixed features rather than learned representations, so they cannot capture the full richness of long-range dependencies.
Another fundamental limitation is the inability to handle overlapping entities. Standard CRFs produce exactly one label per token, so they cannot represent the case where "New York City" is simultaneously a location and part of an organization name. More complex models like semi-CRFs or factorial CRFs address some of these limitations but at greater computational cost.
Despite these limitations, CRFs made sequence labeling practical before deep learning. Their contribution extends beyond the models themselves to the training paradigms they established. The idea of discriminatively training structured models, using dynamic programming for efficient inference, and designing features to capture domain knowledge influenced subsequent developments in neural NLP. Modern neural CRF layers, which place a CRF on top of BiLSTM or transformer encoders, combine learned representations with the structured prediction framework that CRFs pioneered. When you use a BERT-based NER model with a CRF output layer today, you are using a direct descendant of the approach described in this chapter.
For practitioners today, CRFs remain valuable in several scenarios. When training data is limited (hundreds rather than thousands of examples), CRFs often outperform neural models because their features encode prior knowledge that the neural model cannot learn from a small sample. When interpretability matters, examining feature weights reveals what the model has learned in terms a domain expert can evaluate and critique. When computational resources are constrained, CRFs train on CPUs in minutes rather than requiring GPUs for hours. And as components in hybrid systems, CRF output layers on top of neural encoders often improve boundary accuracy compared to using softmax alone, because they enforce global consistency across the label sequence.
The historical importance of CRFs also lies in what they taught the NLP community about structured prediction. Before CRFs, many NLP systems used pipeline architectures: a POS tagger trained independently, then an NER model trained independently using POS tags as features. CRFs enabled joint training, where the output structure itself is modeled as a random variable and the model is optimized end-to-end over the full output. This shift from pipeline to joint modeling was one of the conceptual breakthroughs that set the stage for the end-to-end neural models that came later.
Summary
Training Conditional Random Fields requires balancing computational efficiency with model expressiveness. The chapter covered each of the key components that make CRF training work in practice.
The log-likelihood objective is the foundation: CRF training maximizes the probability of correct label sequences. The objective is concave, guaranteeing a unique global optimum without the pathological local minima that complicate neural network training. This concavity follows from the log-sum-exp structure of the partition function and makes CRFs one of the most theoretically clean models in NLP.
The forward-backward algorithm is the computational engine. Dynamic programming computes the partition function and all marginal probabilities in time, making training tractable despite the exponential number of possible label sequences. The algorithm runs in log space to avoid numerical underflow, using the log-sum-exp trick at each step. Both forward and backward passes are required: the forward pass alone gives the partition function, but both together give the pairwise marginals needed for gradient computation.
Gradient computation follows directly from the forward-backward pass. The gradient equals observed feature counts minus expected feature counts. This moment-matching interpretation makes the convergence criterion transparent: training is complete when the model's distribution over label sequences reproduces the empirical feature statistics of the training data, subject to the regularization constraint.
L-BFGS optimization closes the loop between gradient computation and weight updates. Quasi-Newton methods converge much faster than gradient descent by approximating curvature information from gradient history. Most CRF implementations use L-BFGS by default because the convex CRF objective is well-suited to second-order methods and does not require the careful learning rate scheduling that gradient descent demands.
Feature templates encode domain knowledge into the model. Hand-designed features capture patterns like word shapes, context windows, morphological properties, and neighboring POS tags. Good feature design is often more important than optimizer choice or regularization strength, because the CRF can only learn patterns that the features can represent.
Regularization prevents overfitting by penalizing large weights. L1 regularization (c1 in sklearn-crfsuite) produces sparse models by driving many weights to exactly zero. L2 regularization (c2) shrinks all weights toward zero without enforcing sparsity. In practice, a small amount of both is often most effective. Regularization strength should be chosen by cross-validation on a development set.
CRFs established the foundation for structured prediction in NLP. While neural approaches now dominate many leaderboards, the principles of discriminative training, dynamic programming, and structured output remain central to modern sequence labeling systems. Neural CRF models directly inherit this framework, placing the CRF training objective on top of learned feature representations. Understanding how CRFs are trained makes these modern hybrid models much easier to reason about and debug.
Quiz
Ready to test your understanding? Take this quick quiz to reinforce what you've learned about CRF training.
CRF Training Quiz
Reference
Citation details
Cite or share this article.
Continue with the full handbook
This chapter is part of Language AI Handbook. Use the handbook page to browse the complete table of contents and continue reading in sequence.
Explore Language AI HandbookStay up to date
Get articles, book updates, and news delivered to your inbox.
No spam, unsubscribe anytime.
Join the community
Sign in to remove popups, track your reading progress, and join the discussion.

Comments
No comments yet. Be the first to share your thoughts!