Beam Search Decoding - LLM Generation & Decoding
Implement Beam Search Decoding in Python with NumPy. medium exercise with examples, hints, and instant feedback. No setup or signup required.
Beam Search Decoding
What you'll build
Beam search over a table of per-step log-probabilities: keep the beam_width best partial sequences at every step, with optional end-of-sequence handling, and return the best sequence and its score.
Background
Greedy decoding commits to the single best token at each step and can miss a sequence whose first token looked worse. Beam search keeps several hypotheses alive. It is used where there is one "right" output (translation, speech recognition, constrained or structured generation); open-ended chat uses sampling instead, because beam search tends to produce bland, repetitive text.
In a real model, the next-token distribution depends on the prefix. To keep this problem self-contained, log_probs[t] is the same for every beam at step t. Without EOS that makes greedy already optimal, so the interesting case is EOS: a beam that ends early stops paying log-probability costs.
Algorithm
Scores are sums of log-probabilities (never multiply raw probabilities: they underflow).
- Start with one empty beam:
[([], 0.0)]. - For each step
tin0 .. min(max_length, num_steps) - 1:- For each beam, in order: if it ends with
eos_token, carry it to the candidate list unchanged; otherwise add one candidate per tokenv = 0 .. vocab_size - 1:(seq + [v], score + log_probs[t, v]). - Sort all candidates by score, descending, with a stable sort (ties keep candidate order: earlier beam first, then lower token id), and keep the first
beam_width.
- For each beam, in order: if it ends with
- Return the first (best) beam.
Inputs
log_probs: float array(num_steps, vocab_size)of log-probabilitiesbeam_width: int >= 1max_length: int or None (None meansnum_steps)eos_token: int or None (None disables EOS handling)
Expected Return Format
A tuple (best_sequence, best_score):
best_sequence: list of Python intsbest_score: total log-probability of that sequence, rounded to 4 decimals
Edge cases and conventions
- Finished (EOS) beams compete with unfinished ones on their raw score: no length normalization here (see the follow-up).
beam_width = 1is greedy decoding.
Constraints
NumPy and plain Python lists.
Follow-up
- Raw log-probability scores favour short sequences. Add length normalization
score / len(seq)^alpha(alphaabout 0.6 to 0.7, Wu et al., 2016). Where in the algorithm must it be applied? - Complexity per step is
O(B · V)candidates; how do real implementations avoid sorting all of them (top-k per beam first)?
Examples
Input: beam_search(np.log(np.array([[0.6, 0.3, 0.1], [0.2, 0.7, 0.1], [0.1, 0.3, 0.6]])), beam_width=1) Output: ([0, 1, 2], -1.3783)
Greedy: log(0.6) + log(0.7) + log(0.6) = -1.3783.
Input: beam_search(np.log(np.array([[0.5, 0.45, 0.05], [0.3, 0.3, 0.4], [0.3, 0.3, 0.4]])), beam_width=2, eos_token=1) Output: ([1], -0.7985)
The beam [1] ends at once with log(0.45). Greedy (beam_width=1) drops it and ends with [0, 2, 2] at -2.5257.
Practice in Python
import numpy as np
def beam_search(log_probs, beam_width, max_length=None, eos_token=None):
"""
Beam search decoding over pre-computed log probabilities.
Args:
log_probs: Log probability matrix (num_steps, vocab_size);
log_probs[t] gives the log probs for step t
beam_width: Number of beams to keep
max_length: Maximum sequence length (defaults to num_steps)
eos_token: End-of-sequence token id (optional); a beam ending with it
is carried forward unchanged
Returns:
best_sequence: List of token ids (ints) for the best beam
best_score: Its total log-probability, rounded to 4 decimals
"""
# Your code here
pass
The interactive editor loads with Python and NumPy. Run your implementation against 6 tests, or use the progressive hints.