Span Corruption › Implementing Span Corruption

Michael BrenndoerferUpdated July 11, 20258 min read
Language AI HandbookAll chapters
Part 22 of 67Pre-training Objectives
Chapter 158Span Corruption › Implementing Span Corruption
Reading section 5 of 14 · Implementing Span Corruption · 8 minSection 5 of 14 · 8 min

Book contents

Section 5 of 14 · 8 min read

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:

  1. Determines how many spans to create (using our formula: ≈r⋅n/μ\approx r \cdot n / \mu)
  2. Samples each span's length from a geometric distribution
  3. 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.

In[6]:
Code
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
In[7]:
Code
# Test the span selection
test_length = 50
mask = select_corruption_mask(
    test_length, corruption_rate=0.15, mean_span_length=3
)
Out[8]:
Console
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.

In[9]:
Code
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
In[10]:
Code
# Find spans in our test mask
spans = get_span_boundaries(mask)
Out[11]:
Console
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:

  1. Corrupted input: The original sequence with each span replaced by its sentinel token (e.g., <extra_id_0>, <extra_id_1>)
  2. 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.

In[12]:
Code
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, target

Let's see this in action with a real sentence:

In[13]:
Code
# 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)
Out[14]:
Console
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:

  1. Understand context: Use the uncorrupted tokens surrounding each sentinel to infer what's missing
  2. Generate coherently: Produce complete phrases, not just isolated words, for each sentinel
  3. 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:

In[15]:
Code
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}
In[16]:
Code
# 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)
Out[17]:
Console
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:

  1. Span count estimation: (r⋅n)/μ(r \cdot n) / \mu spans needed for the target corruption rate
  2. Length sampling: Geometric distribution with p=1/μp = 1/\mu for natural length variation
  3. Boundary detection: Linear scan to identify contiguous corrupted regions
  4. 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.