Repetition Penalties › The Repetition Penalty

Michael BrenndoerferUpdated August 1, 202514 min read
Language AI HandbookAll chapters
Part 28 of 67GPT Architecture
Chapter 205Repetition Penalties › The Repetition Penalty
Reading section 3 of 13 · The Repetition Penalty · 14 minSection 3 of 13 · 14 min

Book contents

Section 3 of 13 · 14 min read

Repetition Penalties › The Repetition PenaltyOne section · Sections in this chapter3 / 13 ↓
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

The most direct approach to preventing repetition modifies the logits for tokens that have already appeared in the generated sequence. The repetition penalty, introduced by Keskar et al. (2019) in the CTRL paper, divides the logits of previously seen tokens by a penalty factor, making them less likely to be selected.

Think of the repetition penalty as a "familiarity tax." Every token that has appeared before faces a handicap when competing for the next position. Fresh tokens, those not yet used, compete on their own merits. Used tokens start at a disadvantage proportional to the strength of the penalty. The effect is to tilt the playing field toward novelty without eliminating previously used tokens entirely.

This approach has an appealing simplicity. It requires only a single parameter, the penalty factor θ\theta, which has a natural interpretation: at θ=1.0\theta = 1.0 there is no effect, and higher values impose stronger penalties. Implementation requires only tracking which tokens have appeared, a set that is trivially maintained as generation proceeds. The computational overhead is minimal: one division or multiplication per token in the used-token set per generation step.

Repetition Penalty

The repetition penalty modifies logits for tokens that appear in the existing context. For each token tt that has already been generated, its logit is divided by the penalty factor θ\theta if positive, or multiplied by θ\theta if negative. This reduces the probability of repeating tokens without eliminating them entirely.

Mathematical Formulation

To understand how repetition penalty works, we need to trace the path from model output to token selection. When a language model predicts the next token, it doesn't output probabilities directly. Instead, it produces logits: raw, unnormalized scores for each token in the vocabulary. These logits then pass through the softmax function to become probabilities, and finally, we sample from that probability distribution.

This pipeline gives us a natural intervention point. If we want to reduce the probability of certain tokens, we can modify their logits before softmax. The question is: how should we modify them?

The Core Insight

Our goal is straightforward: make previously used tokens less likely to appear again. Since softmax converts logits to probabilities, reducing a logit reduces its corresponding probability. But there's a subtlety. Logits can be positive or negative, and the operation that reduces a positive number (division) increases a negative number (makes it closer to zero). We need an operation that pushes logits in the "less likely" direction regardless of their sign.

The solution is a piecewise function that applies different operations based on the sign of the logit. Let ziz_i be the logit for token ii and let GG be the set of tokens that have already appeared in the generated sequence. The repetition penalty modifies logits as follows:

zi′={zi/θif i∈G and zi>0zi⋅θif i∈G and zi≤0ziif i∉Gz'_i = \begin{cases} z_i / \theta & \text{if } i \in G \text{ and } z_i > 0 \\ z_i \cdot \theta & \text{if } i \in G \text{ and } z_i \leq 0 \\ z_i & \text{if } i \notin G \end{cases}

where:

  • ziz_i: the original logit for token ii, the raw score from the model before softmax
  • zi′z'_i: the modified logit after applying the penalty
  • θ\theta: the repetition penalty factor, typically in the range [1.0,2.0][1.0, 2.0]
  • GG: the set of token indices that have appeared in the context
  • i∈Gi \in G: notation meaning "token ii is a member of set GG" (i.e., token ii has appeared before)

The formula reads: for each token, check if it has appeared before. If it has and its logit is positive, divide by the penalty factor. If it has and its logit is zero or negative, multiply by the penalty factor. If it hasn't appeared, leave it unchanged.

Why does this formula make sense? Notice that in both the positive and negative cases, the modification moves the logit away from large positive values. For a positive logit, division reduces the magnitude (e.g., 4.0 becomes 2.0). For a negative logit, multiplication increases the magnitude in the negative direction (e.g., -2.0 becomes -3.0). Both operations reduce the value in the mathematical sense (the result is smaller or more negative), and because softmax is an increasing function of each logit, a smaller logit always maps to a smaller probability. The penalty is thus correctly reducing probability in both cases.

Why the Asymmetric Treatment?

This asymmetry might seem arbitrary, but it follows directly from how softmax works. Recall that softmax converts a logit ziz_i to a probability:

P(i)=ezi∑jezjP(i) = \frac{e^{z_i}}{\sum_j e^{z_j}}

The probability depends on ezie^{z_i}, the exponential of the logit. The exponential function is strictly increasing: larger inputs produce larger outputs. So to reduce P(i)P(i), we must reduce ziz_i.

where:

  • P(i)P(i): the probability assigned to token ii
  • ezie^{z_i}: the exponential of token ii's logit, which must be positive
  • ∑jezj\sum_j e^{z_j}: the sum of exponentials across all tokens in the vocabulary, serving as the normalizing constant

For a positive logit like zi=2.0z_i = 2.0, dividing by θ=1.5\theta = 1.5 gives zi′=1.33z'_i = 1.33. Since 1.33<2.01.33 < 2.0, we've reduced the logit and thus reduced the probability. Division works as intended.

For a negative logit like zi=−2.0z_i = -2.0, the same division gives zi′=−2.0/1.5=−1.33z'_i = -2.0 / 1.5 = -1.33. But −1.33>−2.0-1.33 > -2.0 (it's closer to zero), so we've increased the logit and the probability. Division fails here.

The fix is multiplication. Multiplying −2.0-2.0 by θ=1.5\theta = 1.5 gives zi′=−3.0z'_i = -3.0. Since −3.0<−2.0-3.0 < -2.0, we've pushed the logit further negative, reducing the probability. Both operations, division for positive logits and multiplication for negative ones, consistently reduce probability.

Why does this asymmetry arise in the first place? The root cause is that division by a number greater than 1 is not a "reduction" operation for negative numbers. Division by 1.5 takes -2.0 to -1.33, which is closer to 0 and thus larger. Multiplication by the same factor takes -2.0 to -3.0, which is further from 0 and thus smaller. The piecewise formula selects the operation that achieves the desired effect in each case.

The Neutral Case

When θ=1.0\theta = 1.0, the penalty has no effect. Dividing by 1 or multiplying by 1 leaves any number unchanged. This gives us a natural "off switch" for the penalty and a baseline for comparison. As θ\theta increases above 1.0, the penalty strengthens, pushing repeated tokens further toward improbability.

Out[6]:
Visualization
Line plot showing logit transformations for positive values under different penalty strengths.
Positive logits are divided by the penalty factor, pulling values toward zero and reducing probability.
Line plot showing logit transformations for negative values under different penalty strengths.
Negative logits are multiplied by the penalty factor, pushing values further negative and reducing probability.

The visualization shows the key insight: for positive logits, division pulls values toward zero (reducing probability), while for negative logits, multiplication pushes values further negative (also reducing probability). Both transformations achieve the same goal through different arithmetic operations. The dashed line at θ=1.0\theta = 1.0 shows the identity case where the penalty has no effect, serving as a useful reference for understanding how each higher θ\theta value moves tokens further from their original probability.

Implementation

Let's implement the repetition penalty from scratch to see exactly how it works:

In[7]:
Code
def apply_repetition_penalty(logits, generated_ids, penalty):
    """
    Apply repetition penalty to logits for tokens that have been generated.

    Args:
        logits: Tensor of shape (vocab_size,) containing raw logits
        generated_ids: Set of token IDs that have been generated
        penalty: Penalty factor (1.0 = no penalty, >1.0 = reduce repetition)

    Returns:
        Modified logits with penalty applied to generated tokens
    """
    modified_logits = logits.clone()

    for token_id in generated_ids:
        if modified_logits[token_id] > 0:
            modified_logits[token_id] = modified_logits[token_id] / penalty
        else:
            modified_logits[token_id] = modified_logits[token_id] * penalty

    return modified_logits

The function iterates through each token that has appeared in the context and applies the appropriate modification based on the sign of its logit. This is conceptually simple but reveals an important property: the penalty treats all repeated tokens equally, whether they appeared once or a hundred times. "The" after one use faces the same penalty as "the" after fifty uses. This equality is a design choice with consequences we will revisit when comparing to frequency penalty.

In[8]:
Code
# Create example logits and demonstrate the effect
vocab_size = 10
example_logits = torch.tensor(
    [2.5, -1.0, 3.2, 0.5, -0.3, 1.8, -2.0, 0.0, 1.2, -0.5]
)
token_names = [
    "the",
    "cat",
    "sat",
    "on",
    "mat",
    "dog",
    "ran",
    "and",
    "big",
    "small",
]

# Suppose tokens 0, 2, 3 have been generated (the, sat, on)
generated_tokens = {0, 2, 3}
penalty = 1.5

modified_logits = apply_repetition_penalty(
    example_logits, generated_tokens, penalty
)
Out[9]:
Console
Effect of repetition penalty (θ = 1.5):
------------------------------------------------------------
Token         Logit   Modified    P(orig)     P(mod)
------------------------------------------------------------
the            2.50       1.67      0.241      0.194 *
cat           -1.00      -1.00      0.007      0.013  
sat            3.20       2.13      0.485      0.309 *
on             0.50       0.33      0.033      0.051 *
mat           -0.30      -0.30      0.015      0.027  
dog            1.80       1.80      0.120      0.221  
ran           -2.00      -2.00      0.003      0.005  
and            0.00       0.00      0.020      0.037  
big            1.20       1.20      0.066      0.121  
small         -0.50      -0.50      0.012      0.022  
------------------------------------------------------------
* = token appeared in context (penalty applied)

The table shows how the repetition penalty redistributes probability mass. Tokens that appeared in the context (marked with *) have their probabilities reduced, while tokens that haven't appeared receive proportionally higher probabilities. Notice that "sat" (token 2), which had the highest original logit, is no longer the most likely token after applying the penalty. The probability mass that was taken from the penalized tokens flows to unpenalized tokens, effectively boosting the chances of less familiar choices.

Visualizing the Effect

Out[10]:
Visualization
Bar chart showing original token probabilities.
Original probability distribution before applying any penalty.
Bar chart showing penalized token probabilities with decreased probability for repeated tokens.
Distribution after repetition penalty (θ = 1.5). Red bars show penalized tokens with reduced probability.

Choosing the Penalty Value

The repetition penalty parameter requires careful tuning. Too low, and repetition persists. Too high, and the model avoids necessary repetition, producing awkward text that never uses "the" or "a" more than once. Finding the right value is partly a matter of matching the penalty to the type of content being generated.

The challenge is that function words like "the", "a", "is", "and" legitimately appear hundreds of times in any long document. A penalty strong enough to suppress pathological repetition of content words will, if applied uniformly, also heavily penalize these necessary function words. The repetition penalty applies to all tokens equally regardless of their type, which means tuning it always involves a trade-off between suppressing content repetition and preserving grammatical fluency. This limitation motivates the frequency and presence penalties discussed later, which can be calibrated to allow some repetition (of function words) while punishing excessive repetition (of content words).

Common values and their effects:

  • θ=1.0\theta = 1.0: No penalty applied. Baseline behavior with full repetition potential.
  • θ=1.1\theta = 1.1 to 1.21.2: Light penalty. Gently discourages repetition while allowing natural patterns. Good starting point for most applications.
  • θ=1.3\theta = 1.3 to 1.51.5: Moderate penalty. Noticeably reduces repetition. May affect fluency for texts requiring repeated terms (technical writing, legal documents).
  • θ>1.5\theta > 1.5: Strong penalty. Significantly suppresses any repeated token. Can produce stilted or unnatural text, especially for longer outputs.
In[11]:
Code
def generate_with_penalty(model, tokenizer, prompt, penalty, max_tokens=80):
    """Generate text with specified repetition penalty."""
    input_ids = tokenizer.encode(prompt, return_tensors="pt")

    with torch.no_grad():
        output = model.generate(
            input_ids,
            max_new_tokens=max_tokens,
            do_sample=True,
            temperature=0.8,
            top_p=0.9,
            repetition_penalty=penalty,
            pad_token_id=tokenizer.eos_token_id,
        )

    return tokenizer.decode(output[0], skip_special_tokens=True)

As the penalty increases, you'll notice the text becomes more varied in vocabulary but may also become less coherent. The model is forced to find alternative words even when repetition would be natural. At high penalty values, the output can read as if someone deliberately avoided using the same word twice, which is not how fluent writing works. The sweet spot depends on the specific task: creative writing benefits from more diversity, while technical writing where precision requires consistent terminology benefits from milder penalties.

Out[12]:
Visualization
Heatmap showing probability reduction percentages for different combinations of original logit values and penalty strengths.
Probability reduction as a function of original logit and penalty strength. Higher original logits experience larger absolute probability reductions, but the relative reduction is more uniform.

The heatmap reveals an important pattern: the probability reduction depends on both the penalty strength and the token's original standing. Tokens with moderate positive logits (around 1-3) experience the largest percentage reductions because they start with meaningful probability that can be substantially reduced. Very high logits still dominate even after penalty, while very low logits have little probability to lose. This means that the most problematic tokens, the ones the model is most confident about and most likely to repeat, are also the most resistant to moderate penalties. Extremely confident predictions require strong penalties to dislodge, while less confident predictions can be steered with mild ones.