Skip to content

Text Generation with Language Models

A character-level language model predicts one distribution over the next character, and generation is just sampling from it repeatedly. Every interesting decision is in how you sample, and that decision is usually explained with three printed samples and an assertion about which looks best.

It can be measured instead. The corpus is real English, so word validity — the share of produced tokens that are dictionary words — is a quality proxy you can compute, and distinct n-grams measures diversity. Both move, in opposite directions:

TemperatureWord validityDistinct wordsDistinct bigramsLongest repeated run
0.20.93010.23580.48995.46
0.50.77800.56750.93492.33
0.80.54490.80140.99421.71
1.00.39470.90520.99811.12
1.50.20210.98811.00001.00
the corpus itself1.00000.81390.97821.46

Note the last row. Real text is not at either extreme — it sits at 0.8139 distinct words, close to temperature 0.8, while scoring validity 1.0000 that no sampling temperature reaches.

  • What temperature does to a softmax, and the measured trade it produces.
  • Why greedy decoding scored a perfect 1.0000 word validity and is still the worst setting here.
  • How top-k and nucleus sampling differ from temperature, measured on the same axes.
  • Why validation loss kept falling after the text stopped improving.

Temperature divides the logits before the softmax:

pi=exp(zi/T)jexp(zj/T)p_i = \frac{\exp(z_i / T)}{\sum_j \exp(z_j / T)}

T<1T < 1 sharpens the distribution towards the argmax; T>1T > 1 flattens it towards uniform; T=1T = 1 leaves the model’s own distribution alone. It is a single number that moves the sampler between “always the most likely character” and “ignore the model”.

Temperature, then optionally top-k or nucleus
scaled = np.log(np.clip(probabilities, 1e-12, None)) / temperature
scaled = np.exp(scaled - scaled.max())
scaled /= scaled.sum()
 
if top_k:                                  # keep the k most likely, renormalise
    keep = np.argsort(scaled)[-top_k:]
    mask = np.zeros_like(scaled); mask[keep] = scaled[keep]
    scaled = mask / mask.sum()
 
if top_p:                                  # keep the smallest set summing to p
    order = np.argsort(scaled)[::-1]
    cut = int(np.searchsorted(np.cumsum(scaled[order]), top_p)) + 1
    mask = np.zeros_like(scaled); mask[order[:cut]] = scaled[order[:cut]]
    scaled = mask / mask.sum()

The + 1 in the nucleus cut is not a rounding detail — without it, a step where one character already exceeds pp would keep nothing at all.

figure 24 samples of 240 characters at each setting matplotlib
Two panels. Left: word validity falling from 0.9301 at temperature 0.2 to 0.2021 at 1.5, with error bars, against a dashed line at 1.0000 for the corpus itself. Right: distinct word bigrams rising from 0.4899 to 1.0000 and distinct words rising from 0.2358 to 0.9881 across the same range, with the corpus bigram level at 0.9782 marked. Two panels. Left: word validity falling from 0.9301 at temperature 0.2 to 0.2021 at 1.5, with error bars, against a dashed line at 1.0000 for the corpus itself. Right: distinct word bigrams rising from 0.4899 to 1.0000 and distinct words rising from 0.2358 to 0.9881 across the same range, with the corpus bigram level at 0.9782 marked.
The two panels are mirror images, which is the whole point: there is no temperature that is best on both axes. The corpus reference lines matter more than the curves — real text scores 1.0000 validity and 0.9782 distinct bigrams simultaneously, and no sampling temperature gets close to both, because the model is not good enough. Temperature cannot fix a weak model, only choose which way it fails.

Greedy decoding scores perfectly and is useless

Section titled “Greedy decoding scores perfectly and is useless”

Setting temperature to zero — always take the most likely character — produced this:

greedy, 220 characters
the the the the the the the the the the the the the the the the the the the the the the the
the the the the the the the the the the the the the the the the the the the the the the the

Its measured scores:

StrategyWord validityDistinct wordsDistinct bigramsLongest repeated run
greedy1.00000.01710.017458.33
T = 0.50.77800.56750.93492.33
T = 1.00.39470.90520.99811.12
top-k = 5, T = 1.00.66190.68940.98681.62
nucleus p = 0.9, T = 1.00.49120.82550.99661.17

Word validity 1.0000 — a perfect score. the is a dictionary word, so a model that writes nothing else is, by that metric alone, flawless. The longest-repeat column is what exposes it: 58.33 consecutive repetitions against 1.46 for real English.

This is the same failure the evaluation page found when 200 copied images beat real data on the Fréchet distance. A single generative metric is not a score to maximise — it is one of several constraints, and the degenerate solution usually maxes exactly one of them.

figure Same model, same number of characters, five decoding rules matplotlib
Left: grouped bars of word validity and distinct bigrams for five strategies, with greedy showing validity 1.0 and near-zero diversity, and dotted lines marking the corpus values. Right: horizontal bars of the longest repeated run, dominated by greedy at 58.33 against roughly 1 to 2.3 for every sampling method and 1.46 for the corpus. Left: grouped bars of word validity and distinct bigrams for five strategies, with greedy showing validity 1.0 and near-zero diversity, and dotted lines marking the corpus values. Right: horizontal bars of the longest repeated run, dominated by greedy at 58.33 against roughly 1 to 2.3 for every sampling method and 1.46 for the corpus.
Greedy is off the scale on the right panel and perfect on the left — the clearest demonstration in this phase that a metric with a degenerate optimum will find it. Among the real strategies, top-k = 5 lands closest to the corpus on both axes: validity 0.6619 with 0.9868 distinct bigrams, against the corpus's 1.0000 and 0.9782.

Top-k and nucleus sampling both attack the same problem from a different direction than temperature. Temperature reweights every character, including ones the model considers absurd; top-k and nucleus delete the tail first and then sample normally from what remains. That is why top-k = 5 at T = 1.0 (validity 0.6619) beats plain T = 1.0 (0.3947) while keeping almost all its diversity — 0.9868 distinct bigrams against 0.9981.

diagram Diagram mermaid
sketch Temperature, top-k and nucleus on one distribution p5.js
Drag the temperature slider and click a truncation rule. The bars are the resulting sampling distribution, with the measured validity and diversity for that temperature.

Loss keeps falling after the text stops improving

Section titled “Loss keeps falling after the text stops improving”
figure 10 epochs, 90,643 parameters, 372s matplotlib
Left: training and validation cross-entropy per character both falling smoothly from about 2.90 and 2.62 to 2.07 over ten epochs. Right: word validity of a sample at temperature 0.5, rising unevenly from 0.5122 to 0.8571 with large fluctuations between epochs. Left: training and validation cross-entropy per character both falling smoothly from about 2.90 and 2.62 to 2.07 over ten epochs. Right: word validity of a sample at temperature 0.5, rising unevenly from 0.5122 to 0.8571 with large fluctuations between epochs.
The left panel is the smooth curve everyone reports. The right one is what it buys, and it is far noisier — validity moves 0.7222, 0.7778, 0.8333, 0.7838 across consecutive epochs while the loss falls monotonically. A single sample scored at one checkpoint is a noisy measurement, which is why the temperature table above averages 24 samples per setting.
EpochLossValidation lossSample validity
12.90122.61690.5122
32.36182.31390.8333
62.20512.17910.8333
102.07272.06770.8571

Cross-entropy per character fell 2.9012 → 2.0727 with no sign of overfitting — validation tracks training within 0.005 at the end, which says the model is under-fitting and would benefit from more capacity and more data rather than from regularisation.

What the samples look like at each stage is the more honest progress report:

epoch 1, T=0.5
o te te are te aie te wte hes aor t rh toie thh tlo ta tot f tos aort so t torn eo t taihe
epoch 10, T=0.5
 at rore the fill br how of the the macter the br the br the of of the the prove the the reack

By epoch 10 the model has learned English letter statistics and a handful of very common words (the, of, and, in), and nothing about meaning. br appears constantly because the corpus is IMDB reviews decoded from token ids, where br — from the HTML line breaks in the original reviews — survived tokenisation as a frequent word.

sketch The measured table, ranked p5.js
Click a column to rank every row by it. The bars are that column's values and the highest and lowest are computed from the numbers, not written in.
  • Reporting one sample per temperature. Validity varies by ±0.03 to ±0.07 across samples at a fixed setting; the tables here average 24.
  • Maximising a single text metric. Greedy decoding scored 1.0000 word validity by repeating one word 58 times.
  • Reading validation loss as sample quality. Loss fell monotonically while sample validity moved 0.7222 → 0.7778 → 0.8333 → 0.7838 between consecutive epochs.
  • Confusing temperature with truncation. Temperature reweights the whole distribution; top-k and nucleus delete the tail. Top-k = 5 at T = 1.0 scored 0.6619 against plain T = 1.0’s 0.3947, at almost the same diversity.
  • Forgetting the + 1 in the nucleus cut. A step where one character already exceeds p would otherwise keep no characters at all.
  • Generating one sample at a time. A forward pass costs the same for 1 row as for 24, so batch the samples — generating them serially multiplies the wall-clock for nothing.
  • Expecting sentences from a character model this size. 90,643 parameters and ten epochs buy letter statistics and common short words.
  • Temperature divides the logits before the softmax, trading validity against diversity: 0.9301 validity at T = 0.2 down to 0.2021 at T = 1.5, with distinct words moving 0.2358 → 0.9881 the other way.
  • The corpus itself scores 1.0000 validity and 0.9782 distinct bigrams — no temperature setting reaches both, because temperature cannot repair a weak model.
  • Greedy decoding is the degenerate optimum of word validity: a perfect 1.0000, with a 58.33-token repeated run.
  • Truncation beats reweighting here: top-k = 5 at T = 1.0 gave 0.6619 validity with 0.9868 distinct bigrams.
  • Loss and sample quality decouple — validation loss fell smoothly while validity bounced.
  • Always average several samples per setting, and always report a corpus baseline.

Two techniques that generate without a generator at all — freeze a trained network and optimise the input instead: DeepDream.

pch.quizTag pch.quizDefaultTitle
  1. Greedy decoding scored a perfect 1.0000 word validity. Why is it still the worst strategy measured here?

    pch.quizShowAnswer

    B — Because it produced one word repeated 58 times — 'the' is a dictionary word, so a degenerate output maximises that single metric while scoring 0.0171 on distinct words — This is the same shape of failure as copied images beating real data on FID: any single generative metric has a degenerate optimum, so you need at least two that pull in opposite directions.

  2. What does raising the sampling temperature do to the softmax?

    pch.quizShowAnswer

    B — It divides the logits before the softmax, flattening the distribution towards uniform — which raised distinct words from 0.2358 to 0.9881 and dropped validity from 0.9301 to 0.2021 — Temperature reweights every character including the implausible ones, which is exactly why very high temperatures produce diverse nonsense.

  3. How does top-k sampling differ from simply lowering the temperature?

    pch.quizShowAnswer

    B — It deletes the unlikely tail entirely and then samples normally, rather than reweighting everything — which gave 0.6619 validity at T=1.0 against 0.3947 for plain T=1.0, at nearly the same diversity — Truncation removes the characters the model considers absurd while leaving the relative probabilities of the plausible ones untouched.

  4. Validation loss fell smoothly from 2.6169 to 2.0677 while sample validity moved 0.7222, 0.7778, 0.8333, 0.7838 across consecutive epochs. What follows?

    pch.quizShowAnswer

    B — Loss and sample quality are only loosely coupled, and a single sample per checkpoint is a noisy measurement — which is why the temperature results average 24 samples per setting — Validation loss also tracked training loss to within 0.005, which indicates under-fitting rather than overfitting.

  5. The corpus itself scored validity 1.0000 and distinct bigrams 0.9782, while no temperature achieved both. What does that tell you?

    pch.quizShowAnswer

    B — Temperature only chooses how a weak model fails — it cannot move the model onto the corpus's operating point, which would require a better model — That is why reporting a corpus baseline matters: without it, temperature 0.2's 0.9301 validity looks like near-perfect text rather than repetitive fragments.

pch.coffeeTagline

pch.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading