Approach: use the standard dynamic-programming Viterbi for a linear-chain CRF in log-space to avoid underflow. At each time step t compute best score for each tag y_t as max_{y_{t-1}} (score_{t-1}[y_{t-1}] + transition[y_{t-1},y_t] + emission[t,y_t]). Store backpointers to reconstruct the best path.python
# pseudocode (python-like)
def viterbi_decode(emissions, transitions, start_trans, end_trans, mask):
"""
emissions: (L, T) log-potentials for each position and tag
transitions: (T, T) matrix, from_tag -> to_tag (log-space)
start_trans: (T,) log score to start with tag
end_trans: (T,) log score to end from tag
mask: (L,) boolean mask for valid positions (if variable length)
Returns: best_path (L,) tag indices, best_score
"""
L, T = emissions.shape
dp = [-inf] * T # dp for t-1
backpointers = [[0]*T for _ in range(L)]
# init
for y in range(T):
dp[y] = start_trans[y] + emissions[0, y]
# forward pass
for t in range(1, L):
if not mask[t]:
break
next_dp = [-inf] * T
for y in range(T): # current tag
# compute best previous tag
best_prev = argmax_y_prev(dp[y_prev] + transitions[y_prev, y])
next_dp[y] = dp[best_prev] + transitions[best_prev, y] + emissions[t, y]
backpointers[t][y] = best_prev
dp = next_dp
# termination
for y in range(T):
dp[y] += end_trans[y]
best_last = argmax_y(dp)
best_score = dp[best_last]
# backtrack
best_path = [0]*L
idx = best_last
for t in reversed(range(L)):
best_path[t] = idx
idx = backpointers[t][idx]
return best_path, best_score
Key points:- Use log-space additions and argmax; store backpointers to reconstruct path.- Enforce BIO constraints by masking illegal transitions (set transition log-prob = -inf).Time & space complexity:- Time: O(L * T^2) per sequence (L = sequence length, T = num tags) because for each position and current tag we consider all previous tags.- Space: O(L * T) to store backpointers and O(T) per time-step for dp.Batch decoding in production:- Vectorize with tensors (PyTorch/TF): represent emissions as (B, L, T), transitions as (T, T). Compute dp across batch using broadcasting and torch.max over prev-tag axis to reduce the inner loop; this yields GPU-accelerated O(B * L * T^2) but high throughput.- Handle variable lengths with masks and pack/unpack or by sorting+bucketing by length to reduce wasted compute.- Optimize: reduce T via tag constraints (BIO pruning), use beam search (beam size K) to reduce complexity to O(L * T * K), or use 1-best greedy if acceptable.- Memory: keep backpointers as int tensors (B, L, T) and reconstruct per batch element; consider checkpointing or streaming backpointers for very long sequences.- Production tips: precompile transition constraints, run on GPU for large B, use mixed precision for throughput, and test correctness with masked positions and BIO-validity checks.