Span Corruption › Implementing Span Corruption
Language AI HandbookAll chapters Span Corruption › Implementing Span CorruptionOne section · Sections in this chapter5 / 14 ↓
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
Now that we understand the mathematics behind span selection, let's translate these concepts into working code. We'll build the algorithm incrementally, starting from the core span selection logic and gradually assembling the complete corruption pipeline. By the end, you'll have a clear picture of how each formula we derived connects to the implementation.
Selecting Span Boundaries
Our first task is to decide which token positions fall within corrupted spans. We need an algorithm that:
- Determines how many spans to create (using our formula: )
- Samples each span's length from a geometric distribution
- Places spans at random positions without overlap
We'll represent the result as a corruption mask: a boolean array where True indicates that token should be part of some corrupted span.
def select_corruption_mask(
seq_length, corruption_rate=0.15, mean_span_length=3
):
"""
Create a boolean mask indicating which positions to corrupt.
Spans are selected to achieve approximately the target corruption rate.
"""
# Calculate expected number of spans
num_tokens_to_corrupt = int(seq_length * corruption_rate)
num_spans = max(1, num_tokens_to_corrupt // mean_span_length)
# Sample span lengths from geometric distribution
p = 1 / mean_span_length
span_lengths = np.random.geometric(p, size=num_spans)
# Clip to ensure we don't exceed the corruption budget
total_corrupted = span_lengths.sum()
if total_corrupted > num_tokens_to_corrupt:
# Scale down span lengths proportionally
scale = num_tokens_to_corrupt / total_corrupted
span_lengths = np.maximum(1, (span_lengths * scale).astype(int))
# Randomly place spans without overlap
mask = np.zeros(seq_length, dtype=bool)
available_positions = list(range(seq_length))
for length in span_lengths:
if len(available_positions) < length:
break
# Choose a starting position
valid_starts = [i for i in range(len(available_positions) - length + 1)]
if not valid_starts:
break
start_idx = np.random.choice(valid_starts)
# Mark positions as corrupted
for offset in range(length):
pos = available_positions[start_idx]
mask[pos] = True
available_positions.remove(pos)
return mask# Test the span selection
test_length = 50
mask = select_corruption_mask(
test_length, corruption_rate=0.15, mean_span_length=3
)Sequence length: 50 Tokens corrupted: 3 (6.0%) Corruption mask (1 = corrupted): ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░██░░░░░░░░░░░█
The visualization shows corrupted positions as filled blocks (█) and uncorrupted positions as empty blocks (░). Notice how the corrupted tokens cluster into contiguous groups rather than scattering randomly across the sequence. This is the defining characteristic of span corruption: tokens within each span will be replaced by a single sentinel and reconstructed together.
With approximately 15% of tokens corrupted, we've created distinct "holes" in the sequence that the model must learn to fill. The spans vary in length according to our geometric distribution, some containing just one or two tokens, others stretching across several positions.
Identifying Span Boundaries
The corruption mask tells us which positions are corrupted, but for the next step we need to know where each span begins and ends. This boundary information lets us replace each contiguous span with exactly one sentinel token and extract the corresponding target tokens.
def get_span_boundaries(mask):
"""
Extract start and end indices for each contiguous span in the mask.
Returns list of (start, end) tuples where end is exclusive.
"""
spans = []
in_span = False
start = 0
for i, is_corrupted in enumerate(mask):
if is_corrupted and not in_span:
# Starting a new span
start = i
in_span = True
elif not is_corrupted and in_span:
# Ending current span
spans.append((start, i))
in_span = False
# Handle span at end of sequence
if in_span:
spans.append((start, len(mask)))
return spans# Find spans in our test mask
spans = get_span_boundaries(mask)Found 2 spans: Span 0: positions 36-37 (length 2) Span 1: positions 49-49 (length 1)
The algorithm finds each contiguous run of True values in the mask. Each span gets a unique index (0, 1, 2, ...) that will serve as its identifier when we assign sentinel tokens. Notice how span lengths vary: some spans contain just one token, others two or three, following our geometric distribution.
Building Input and Target Sequences
With span boundaries identified, we can now construct the two sequences that define the training example:
- Corrupted input: The original sequence with each span replaced by its sentinel token (e.g.,
<extra_id_0>,<extra_id_1>) - Target sequence: A concatenation of all corrupted spans, each preceded by its corresponding sentinel
This format creates a clear mapping: the model sees a sentinel in the input and learns to generate the corresponding tokens in the target.
def corrupt_sequence(tokens, mask, sentinel_prefix="<extra_id_"):
"""
Apply span corruption to a token sequence.
Args:
tokens: List of tokens
mask: Boolean array indicating corrupted positions
sentinel_prefix: Prefix for sentinel tokens
Returns:
corrupted_input: Input sequence with sentinels replacing spans
target: Target sequence for reconstruction
"""
spans = get_span_boundaries(mask)
# Build corrupted input
corrupted_input = []
last_end = 0
for span_idx, (start, end) in enumerate(spans):
# Add uncorrupted tokens before this span
corrupted_input.extend(tokens[last_end:start])
# Add sentinel for this span
corrupted_input.append(f"{sentinel_prefix}{span_idx}>")
last_end = end
# Add remaining uncorrupted tokens
corrupted_input.extend(tokens[last_end:])
# Build target sequence
target = []
for span_idx, (start, end) in enumerate(spans):
# Add sentinel
target.append(f"{sentinel_prefix}{span_idx}>")
# Add original span tokens
target.extend(tokens[start:end])
return corrupted_input, targetLet's see this in action with a real sentence:
# Example sentence
sentence = "The quick brown fox jumps over the lazy dog near the riverbank"
tokens = sentence.split()
# Create corruption mask for this specific example
mask = select_corruption_mask(
len(tokens), corruption_rate=0.25, mean_span_length=2
)
# Apply corruption
corrupted_input, target = corrupt_sequence(tokens, mask)Original tokens: ['The', 'quick', 'brown', 'fox', 'jumps', 'over', 'the', 'lazy', 'dog', 'near', 'the', 'riverbank'] Corruption mask: ░░░░░░░░░░██ Corrupted input (11 tokens): ['The', 'quick', 'brown', 'fox', 'jumps', 'over', 'the', 'lazy', 'dog', 'near', '<extra_id_0>'] Target sequence (3 tokens): ['<extra_id_0>', 'the', 'riverbank']
Examine the output carefully. The corrupted input is noticeably shorter than the original: multi-token spans have collapsed into single sentinel tokens. The target sequence contains exactly the tokens that were removed, each group prefixed by its sentinel to maintain the correspondence.
This structure shows how span corruption works in practice. The model must:
- Understand context: Use the uncorrupted tokens surrounding each sentinel to infer what's missing
- Generate coherently: Produce complete phrases, not just isolated words, for each sentinel
- Maintain boundaries: Know when one span ends and the next begins, guided by the sentinel markers
Complete Span Corruption Pipeline
With all the pieces in place, let's combine them into a reusable class that encapsulates the full span corruption logic:
class SpanCorruptor:
"""Applies T5-style span corruption to text sequences."""
def __init__(
self, corruption_rate=0.15, mean_span_length=3, num_sentinels=100
):
self.corruption_rate = corruption_rate
self.mean_span_length = mean_span_length
self.num_sentinels = num_sentinels
self.sentinels = [f"<extra_id_{i}>" for i in range(num_sentinels)]
def corrupt(self, tokens):
"""
Corrupt a token sequence using span corruption.
Args:
tokens: List of string tokens
Returns:
dict with 'input' and 'target' token lists
"""
if len(tokens) < 2:
return {"input": tokens, "target": []}
# Select spans to corrupt
mask = select_corruption_mask(
len(tokens), self.corruption_rate, self.mean_span_length
)
# Get span boundaries
spans = get_span_boundaries(mask)
if len(spans) > self.num_sentinels:
# Too many spans, merge some
spans = spans[: self.num_sentinels]
# Build sequences
corrupted_input = []
target = []
last_end = 0
for span_idx, (start, end) in enumerate(spans):
corrupted_input.extend(tokens[last_end:start])
corrupted_input.append(self.sentinels[span_idx])
target.append(self.sentinels[span_idx])
target.extend(tokens[start:end])
last_end = end
corrupted_input.extend(tokens[last_end:])
return {"input": corrupted_input, "target": target}# Test the complete pipeline
corruptor = SpanCorruptor(corruption_rate=0.15, mean_span_length=3)
text = """Natural language processing enables computers to understand
and generate human language in meaningful ways"""
tokens = text.split()
result = corruptor.corrupt(tokens)Original (14 tokens): Natural language processing enables computers to understand and generate human language in meaningful ways Corrupted input (13 tokens): Natural language processing enables computers to understand and <extra_id_0> language in meaningful ways Target (3 tokens): <extra_id_0> generate human
The results confirm our earlier calculations. The original sequence becomes a shorter corrupted input (the corruption plus sentinels compress the sequence) and a compact target containing only the corrupted material plus sentinel markers.
This completes our span corruption implementation. Starting from the mathematical foundations, we've built each component:
- Span count estimation: spans needed for the target corruption rate
- Length sampling: Geometric distribution with for natural length variation
- Boundary detection: Linear scan to identify contiguous corrupted regions
- Sequence construction: Parallel building of input (with sentinels) and target (spans preceded by sentinels)
The algorithm is efficient, running in linear time relative to sequence length, and produces the exact format expected by T5-style encoder-decoder training.