A concrete ambiguity
For the sourcethe room code is one oh five, the candidate builder produces
these two edits over the same source character interval, [17, 28):
The kinds describe which Rust realizers produced an output. They are features
for scoring, not separate copies of an identical replacement. If the expected
sentence ends in
105, the time edit is a valid candidate but an incorrect
interpretation in this context. The graph must also allow the unchanged source
and edits to other, non-overlapping parts of a sentence.
The source graph
Let the source string haven characters. Graph positions are the boundaries
0 through n; they are source character positions, not tokenizer IDs.
Each candidate is a directed edge from its half-open char_start to
char_end, weighted by the scorer’s scalar output. A one-character KEEP edge
from i to i + 1 has weight zero at every source position.
A complete path starts at 0, ends at n, and advances only forward. It
covers every source character once, either with an edit or with KEEP.
Non-overlapping edits can coexist; overlapping edits cannot occur on the same
path. Untouched spaces and punctuation are covered by KEEP even though the
candidate builder enumerates token spans. For example,
meet at four thirty on march fourth can select both four thirty →
04:30 and fourth → 4th in one path.
For path p, the score is the sum of its selected candidate scores:
The scores are learned compatibility values, not independent probabilities.
The all-KEEP path always scores zero. This gives the model a meaningful
alternative to every proposed edit without training a separate KEEP score.
The exact gold graph
Training receives a sourcex and an expected output y.
build_gold_graph() tracks alignment states (i, j), where i is a source
character position and j is an output character position. It starts at
(0, 0) and accepts only a path that reaches (|x|, |y|).
There are three permitted forward transitions:
- Exact
KEEP: whenx[i] == y[j], advance to(i + 1, j + 1)with score zero. - Candidate edit: if candidate
cstarts atiandybegins withc.replacementatj, advance to(c.char_end, j + |c.replacement|)with scores(c). - Restricted attachment: one source whitespace character may advance
iwithout advancingjwhen a matching candidate immediately after it writes a symbol such as%that may attach to the previous output. This is howthe rate five percentcan align tothe rate 5%. It is not a general whitespace deletion rule.
Train by marginalizing complete paths
LetP(x) contain every complete path through the source graph. Let
G(x, y) contain the complete paths retained in the gold graph. The loss is:
This is the negative log probability assigned to all exact gold paths under
the source graph. A wrong but valid interpretation contributes to the first
term, not the second. For the room-code example, the 01:05 edge competes
with the 105 edge without a manually authored negative label.
all_paths_log_partition() computes the first log-sum-exp with a forward
dynamic program over source positions. From each position it propagates the
current value through KEEP and each candidate beginning there. The gold
partition uses the same operation over the pruned (source, target) states.
torch.logaddexp combines alternatives in log space without listing every
complete path. Because the gold term sums all exact derivations, a second
valid derivation is rewarded rather than treated as an error.
The source recurrence is compact. Initialize F[0] = 0 and every other
position to negative infinity. At each source position i, update
F[i + 1] with F[i] for KEEP; for every candidate c starting at i,
update F[c.char_end] with F[i] + s(c). Each update uses logaddexp.
The final F[n] is the all-path log partition. The gold recurrence uses the
same update on retained alignment states and their exact transitions.
The gradient for one candidate score is its marginal selection frequency
over all paths minus its marginal frequency over gold paths. This is the
mechanism behind contextual hard negatives: a wrong candidate with high
all-path mass receives pressure to lose to exact target paths.
prepare_training_example() builds the candidates, exact gold graph, and
tokenized inputs. For a batch, the scorer emits one flattened score vector;
candidate_offsets divides it back into sentences. structured_batch_loss()
computes one structured loss per sentence and averages them. The graph
topology stays on the host while scores remain PyTorch tensors, so gradients
flow through candidate scores into the scorer.
Decode with max-sum instead of log-sum-exp
At inference there is no target or gold graph.max_path_indices() runs over
the same source intervals and replaces log-sum-exp with a maximum. It stores a
predecessor and optional candidate index for each best position, then
backtracks from n. The result is the exact highest-scoring compatible
path among the generated candidates, with no beam search.
KEEP makes the choice conservative: any negative-score candidate can be
replaced by zero-score KEEP edges over the same interval, leaving the other
selected edits compatible and increasing the path score. The decoder rejects
non-finite scores and invalid candidate spans. Equal totals retain the path
already recorded because updates use a strict > comparison; callers should
not interpret ties as a learned preference.
apply_candidate_replacements() sorts the selected edits, checks that their
spans do not overlap and still match the source, then copies untouched source
gaps verbatim. It applies the same restricted attachment rule used by gold
alignment to remove one separator before eligible symbols. The final string
therefore comes from validated replacements plus preserved source text, not
from unconstrained text generation.
Once candidates and scores exist, the source-graph dynamic programs take
O(n + C) time for n source characters and C candidate edges. Candidate
generation and model scoring have separate costs; this bound is for path
selection only. A form absent from the Rust candidate set cannot be recovered
by training or decoding.
For tokenization and scorer features, see the
architecture. The implementation lives in
candidates.py,
structured_loss.py,
training_batch.py,
and decoder.py.