Index Ai Academy
Phase 5 · Transformers · Language modeling

Sampling strategies

Temperature, top-k and top-p: turning probabilities into text.

About 30 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

Why it matters

A language model never writes a word by itself. At each step it hands you a list of chances, one for every token it knows. Something still has to pick one. The rule that does the picking is called the decoding strategy. With one rule, the same model sounds dull and repeats itself. With another, it sounds natural. With a third, it talks nonsense. The "temperature" and "top-p" settings in many AI tools are dials on this rule. Knowing how they work explains why the same prompt can give different answers, and why turning the randomness all the way down isn't always better.

What you'll be able to do

  • Turn a model's next-token probabilities into a choice: greedy versus sampling
  • Reshape the distribution with temperature
  • Cut off the unlikely tail with top-k and top-p (nucleus) sampling
  • Know which of these settings today's APIs expose

What it covers

  1. Greedy or random?
  2. Temperature
  3. Worked example: three words at three temperatures
  4. Temperature on a longer list
  5. Top-k and top-p
  6. On a real model
  7. In practice

Greedy or random?

Say a model has read "It is" and must choose the next word. It gives "light" a 40% chance, "warm" 35% and "purple" 25%. You have two basic ways to choose.

  • Greedy decoding: always take the most likely token. Here that's "light", every time. The result never changes, which is handy. But the text tends to be dull, and it can get stuck in a loop. The most likely next words are often a common phrase, and that phrase can lead straight back to itself, over and over.
  • Sampling: pick at random, but weight the draw by the chances. "light" wins 40% of the time, "warm" 35% and "purple" 25%. The text comes out varied and natural. Now and then, though, it picks something unlikely, and the sentence goes off the rails.

Think of choosing a restaurant. Greedy is going to your favorite place every night. Sampling is drawing a name from a hat, with your favorite written on more slips than the rest. Once in a while you end up somewhere odd.

Every method in this lesson keeps the variety of sampling and cuts down its risk.

Quick check
A model gives the next word these chances: "the" 0.4, "a" 0.35, "my" 0.25. You run greedy decoding on the same prompt three times. What do you get?
  1. "my", because it is the least common
  2. "the" all three times
  3. "the", "a" and "my", once each
  4. A random word each time, weighted by the chances
Show the answer

"the" all three times

Greedy always takes the single most likely token, so it gives the same answer every run. Sampling is the method that would sometimes pick "a" or "my".

Temperature

Temperature is a dial for how bold the choices are. You met it in the softmax lesson. Before the model turns its raw scores into chances, you divide every score by one number, the temperature. The raw scores are the logits, and softmax is the step that turns them into chances that add up to 1.

  • A temperature below 1 makes the gaps between scores bigger. The top choices grab even more of the chance. Push it close to 0 and the top token wins almost every time. That's greedy decoding.
  • A temperature above 1 makes the gaps smaller. The chances even out, so unlikely tokens get picked more often.

Here is the same idea as a formula. Each token has a raw score, written ziz_i. The small ii says which token the score belongs to. The temperature is written TT. The chance of picking token ii is written pip_i.

pi=ezi/T∑jezj/Tp_i = \frac{e^{z_i / T}}{\sum_j e^{z_j / T}}

The letter ee is the number 2.718 from the exponentials lesson. The symbol ∑\sum means "add these all up". The letter jj runs through every token to build that total.

In plain words: divide every score by the temperature. Then do softmax as usual: raise ee to the power of each result, and divide each one by the total.

Go deeper Why a small temperature makes the top token win

In softmax, each extra point of score makes a token about 2.718 times as likely. Take two tokens whose scores differ by 1. At T=1T = 1, the top one is about 2.7 times as likely as the other.

Now set T=0.1T = 0.1. Dividing by 0.1 is the same as multiplying by 10, so the gap of 1 becomes a gap of 10. Now the top token is about e10e^{10}, or 22,026, times as likely. As TT gets closer to 0, that number keeps growing. In the limit the top token takes all the chance, which is greedy decoding.

A very large TT does the opposite. Every gap shrinks toward 0, so every token ends up about equally likely.

Worked example

Worked example: three words at three temperatures

A model gives three next words these raw scores: "light" 2, "warm" 1 and "purple" 0. Work out their chances at temperatures 0.5, 1 and 2.

import math

scores = [2.0, 1.0, 0.0]                         # light, warm, purple
for T in [0.5, 1.0, 2.0]:
    powers = [math.exp(z / T) for z in scores]   # divide by T, then e to that power
    total = sum(powers)                          # add them all up
    print("T =", T, [round(p / total, 3) for p in powers])
It runs right here in your browser, with nothing to install.

At T=1T = 1 nothing changes from plain softmax. The powers are 7.389, 2.718 and 1, with a total of 11.107. So "light" gets 0.665, "warm" 0.245 and "purple" 0.09.

At T=0.5T = 0.5 the scores double to 4, 2 and 0. The powers jump to 54.598, 7.389 and 1. Now "light" gets 0.867 and "purple" only 0.016. The favorite pulls further ahead.

At T=2T = 2 the scores halve to 1, 0.5 and 0. The chances even out to 0.506, 0.307 and 0.186. "purple" is now picked almost one time in five. The order never changes at any temperature. Only the gaps do.

Temperature on a longer list

Now try seven possible next words after "It is", from a product catalog. The code prints each word's chance at three temperatures and draws them on one chart.

import numpy as np
import matplotlib.pyplot as plt

words = ["light", "warm", "durable", "packable", "waterproof", "purple", "edible"]
logits = np.array([2.0, 1.6, 1.4, 1.1, 0.9, -1.0, -2.5])      # next word after "It is"

def softmax(z, T=1.0):
    e = np.exp((z - z.max()) / T)
    return e / e.sum()

for T in [0.5, 1.0, 2.0]:
    p = softmax(logits, T)
    print(f"T = {T}: " + "  ".join(f"{w} {q:.2f}" for w, q in zip(words, p)))
    plt.plot(range(len(words)), p, marker="o", label=f"T = {T}")
plt.xticks(range(len(words)), words, rotation=30)
plt.ylabel("probability")
plt.legend()

At T=0.5T = 0.5, "light" alone takes about half the chance, and the top few words crowd out the rest. At T=2T = 2, the line flattens. "edible" climbs from almost nothing to about 2.6% (printed as 0.03), so roughly one pick in 40 would call the product edible. That's how a high temperature lets odd words slip in.

The softmax here subtracts the largest score before it does anything else. That keeps the numbers small enough for the computer to handle, and it doesn't change the answer.

Quick check
What does lowering the temperature toward 0 do?
  1. Makes the model more accurate on facts
  2. Removes the least likely tokens entirely
  3. Makes all tokens equally likely
  4. Concentrates probability on the most likely tokens, approaching greedy decoding
Show the answer

Concentrates probability on the most likely tokens, approaching greedy decoding

Dividing the logits by a small T makes the gaps between them much bigger, so the top token takes almost all the chance.

Top-k and top-p

Temperature reshapes the whole list, but it never removes anything. Even a silly word keeps a tiny chance. The next two methods cut the unlikely words, called the tail, out of the list. Then they share the chance among the words that are left, so it adds up to 1 again. This step is called renormalizing.

  • Top-k: keep only the kk most likely tokens. Here kk is a whole number you choose, such as 3 or 50.
  • Top-p, also called nucleus sampling: sort the tokens from most to least likely. Then keep adding tokens until their chances add up to at least pp, a share you choose, such as 0.9.

Picture packing a suitcase. Top-k says "take exactly five items." Top-p says "take the most useful items until you have 90% of what you need." For a weekend trip, a few items reach 90%. For a long trip, you need many more. In the same way, top-p keeps one or two tokens when the model is sure of itself and many when it isn't. A fixed kk can't adapt like that.

Here's top-p on the catalog words, with p=0.9p = 0.9. Add up the chances from the top: light 0.33, then 0.55 with warm, 0.73 with durable, 0.87 with packable. That's still short of 0.9. Adding waterproof brings the total to 0.98, so top-p keeps those five words and drops "purple" and "edible".

import numpy as np

words = ["light", "warm", "durable", "packable", "waterproof", "purple", "edible"]
p = np.exp([2.0, 1.6, 1.4, 1.1, 0.9, -1.0, -2.5])
p /= p.sum()

def top_k(p, k):
    keep = np.zeros_like(p)
    idx = np.argsort(-p)[:k]
    keep[idx] = p[idx]
    return keep / keep.sum()

def top_p(p, threshold):
    order = np.argsort(-p)
    cutoff = np.searchsorted(np.cumsum(p[order]), threshold) + 1   # how many tokens reach the threshold
    keep = np.zeros_like(p)
    keep[order[:cutoff]] = p[order[:cutoff]]
    return keep / keep.sum()

for name, q in [("original", p), ("top-k, k=3", top_k(p, 3)), ("top-p, p=0.9", top_p(p, 0.9))]:
    print(f"{name:<13}" + "  ".join(f"{w} {v:.2f}" for w, v in zip(words, q)))

With k=3k = 3, only light, warm and durable survive. Their chances grow to 0.45, 0.30 and 0.25 so they add up to 1 again. With p=0.9p = 0.9, five words survive, each a little larger than before.

Each function follows the same three steps. Sort the tokens with np.argsort(-p), which lists them from most to least likely. Copy the ones you keep into an array of zeros. Then divide by the new total to renormalize.

Go deeper How the top-p line finds its cutoff

np.cumsum makes a running total: 0.33, 0.55, 0.73, 0.87, 0.98 and so on. np.searchsorted then finds the first place in that running total where the threshold, 0.9, would fit. Here that's position 4, counting from 0, because 0.9 falls between 0.87 and 0.98. Adding 1 turns a position into a count: keep 5 tokens. The token at that position is the one that pushes the total past the threshold, so it stays in.

Quick check
What does this print?
probs = {"a": 0.5, "b": 0.3, "c": 0.2}
keep = sorted(probs, key=probs.get, reverse=True)[:2]
print(round(sum(probs[k] for k in keep), 1))
  1. 1.0
  2. 0.8
  3. 0.7
  4. 0.5
Show the answer

0.8

Top-k keeps the two most likely. They hold 0.8 of the odds and get rescaled to add up to 1.

Try it now

Try temperature 0.3, then 2. Then cut the list with top-k, or with top-p, and press Sample 100 to see the real draws match the bars.

Quick check
A model's next-token chances, sorted, are 0.6, 0.25, 0.1 and 0.05. You use top-p with p = 0.8. How many tokens are kept?
  1. 4
  2. 2
  3. 1
  4. 3
Show the answer

2

The first token alone gives 0.6, which is short of 0.8. Adding the second gives 0.85, which reaches it. So top-p keeps two tokens and renormalizes them.

On a real model

Now use the bigram model from the last lesson. It predicts each character from the one before it. The code builds its table of counts, then writes 60 characters from the start of a product description. It does this five times, with a different strategy each time.

import numpy as np
import requests

text = requests.get("https://api.gradient.test/v1/corpus").json()["text"]
chars = sorted(set(text))
stoi = {c: i for i, c in enumerate(chars)}
ids = np.array([stoi[c] for c in text])
counts = np.full((len(chars), len(chars)), 0.1)
np.add.at(counts, (ids[:-1], ids[1:]), 1)
logits = np.log(counts / counts.sum(axis=1, keepdims=True))

def generate(T=None, k=None, steps=60, seed=0):
    rng = np.random.default_rng(seed)
    cur, out = stoi["\n"], ""
    for _ in range(steps):
        z = logits[cur]
        if T is None:                                   # greedy
            cur = int(np.argmax(z))
        else:
            p = np.exp((z - z.max()) / T)
            if k is not None:
                p[np.argsort(-p)[k:]] = 0
            cur = rng.choice(len(chars), p=p / p.sum())
        out += chars[cur]
    return repr(out)

print("greedy        ", generate())
print("T = 0.5       ", generate(T=0.5))
print("T = 1.0       ", generate(T=1.0))
print("T = 1.0, k = 5", generate(T=1.0, k=5))
print("T = 2.0       ", generate(T=2.0))

A few lines are worth a closer look:

  • The table starts every count at 0.1, so no pair of characters ever has a chance of exactly zero.
  • np.log turns each row of chances into scores. At T=1T = 1, softmax turns those scores straight back into the same chances.
  • When T is left out, the code takes the top character every time. That's greedy decoding.
  • The seed=0 fixes the random draws, so you see the same text each time you run it.

Compare the five lines. Greedy decoding falls into a loop almost at once. Low temperature repeats the most common bits of text. High temperature produces junk. A moderate temperature with a top-k cutoff keeps the feel of the catalog without the wildest characters. Large models show the same trade-offs. They play out with words and ideas instead of single letters.

In practice

  • Open models and many APIs offer settings called temperature, top_p and top_k. Some add others. Min-p keeps only tokens that are at least some fraction as likely as the top one. A repetition penalty lowers the scores of tokens the model has already used. Common starting points are a temperature of 0.7 to 1.0 and a top-p of 0.9 to 0.95.
  • Use a low temperature for tasks with one right answer, such as pulling facts out of text (extraction) or sorting it into groups (classification). Use a higher one for brainstorming and creative copy.
  • Even temperature 0 isn't perfectly repeatable on most serving systems. The reason is how the computer chips that run models (GPUs) group many requests together, which can change tiny rounding details.
  • As Phase 1 noted, the current Claude API version doesn't accept temperature, top_p or top_k. The provider manages sampling for you. You steer behavior through the prompt and the effort setting instead.

For marketers: when you ask for ten subject-line variants, you're relying on sampling. When you need the same classification every time, design for it. Use structured output, clear instructions and evals (tests that score the model's answers). Don't count on randomness being switched off.

Quick check
You're using an open model to sort 1,000 support emails into fixed categories. You want the same label for the same email every time. Which setup fits best?
  1. Top-k with k = 50, so many labels stay possible
  2. Greedy decoding alone, which guarantees identical labels on every server
  3. A low temperature, plus clear instructions and an eval to check the labels
  4. A temperature of 2, so the model considers more categories
Show the answer

A low temperature, plus clear instructions and an eval to check the labels

Sorting into categories has one right answer, so a low temperature suits it. Even temperature 0 isn't perfectly repeatable on most servers, so clear instructions and an eval do the rest.

Practice what you just read

  • 4 code examples you can edit and run
  • a practice exercise that checks your code
  • a 5-question quiz
  • 5 review cards for spaced repetition

Create a free account

Try a quiz question

Why does greedy decoding often produce repetitive text?

  1. The most likely continuation is often a common phrase that leads back to itself, and greedy never deviates
  2. Greedy decoding uses a smaller vocabulary, so it runs out of different words to pick from and has to reuse the same ones
  3. It samples too randomly, so it keeps landing on the most common words and phrases in the training data, again and again
  4. It ignores the prompt after the first few tokens, so the text has nothing to steer it and drifts back to the same phrases

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson