Sampling strategies
Temperature, top-k and top-p: turning probabilities into text.
Create a free account Try part of it here
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
- Greedy or random?
- Temperature
- Worked example: three words at three temperatures
- Temperature on a longer list
- Top-k and top-p
- On a real model
- 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.
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 . The small says which token the score belongs to. The temperature is written . The chance of picking token is written .
The letter is the number 2.718 from the exponentials lesson. The symbol means "add these all up". The letter runs through every token to build that total.
In plain words: divide every score by the temperature. Then do softmax as usual: raise 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 , the top one is about 2.7 times as likely as the other.
Now set . 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 , or 22,026, times as likely. As 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 does the opposite. Every gap shrinks toward 0, so every token ends up about equally likely.
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])
At 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 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 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 , "light" alone takes about half the chance, and the top few words crowd out the rest. At , 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.
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 most likely tokens. Here 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 , 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 can't adapt like that.
Here's top-p on the catalog words, with . 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 , 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 , 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.
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))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 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.
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.logturns each row of chances into scores. At , softmax turns those scores straight back into the same chances.- When
Tis left out, the code takes the top character every time. That's greedy decoding. - The
seed=0fixes 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_pandtop_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_portop_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.
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
Try a quiz question
Why does greedy decoding often produce repetitive text?