torch.multinomial seems magic. It is not.
Up until now we’ve spent the module learning distributions. This lesson is the inverse problem: given a categorical , how do we actually draw a sample from it? Not call a library function, but write the function, line by line.
The reason this matters: by the end of module 18 you will have trained a transformer. The transformer outputs a categorical at every position. Generating text means sampling from that categorical, position after position. The function you’ll paste into the capstone is the same one we’re about to write here, in eight lines.
Every PRNG hands you one thing: a uniform
JavaScript’s Math.random(), NumPy’s np.random.random(), PyTorch’s torch.rand: every pseudo-random number generator on earth ultimately exposes the same primitive, a uniform random variable on the interval . Every other random thing (Gaussians, categoricals, Beta distributions, all of them) is constructed by transforming uniforms.
So the entire problem of sampling reduces to: given a uniform, what arithmetic produces a draw from the distribution I want?
Inverse-CDF sampling for a categorical
Recall the cumulative distribution. For a categorical , the CDF is the running cumulative sum:
So , , and the values partition into adjacent intervals whose widths are exactly .
To sample: draw . Find the smallest such that . Return .
Drop a ball on the widget below to watch it happen. The dashed yellow line is the chosen ; the bin it lands in (highlighted in yellow on the staircase) is the chosen token. Drop more; watch the empirical histogram on the right converge on the bars you set on the left.
The interval lands in has width , exactly, so the procedure returns each with probability . That’s the entire proof.
Walk through it by hand
Let over the values . Then .
Draw . Is ? No. Is ? No. Is ? Yes, return .
Draw . Is ? Yes, return .
That’s the algorithm. Walk the cumulative sum, return the first index where you’ve passed .
Find the bin
With over and , what index does inverse-CDF sampling return?
The eight-line sampler
Here is the function. Copy it. Paste it into the capstone in module 18. It is the literal sampler.
function sampleCategorical(p) {
const u = Math.random();
let cum = 0;
for (let i = 0; i < p.length; i++) {
cum += p[i];
if (cum >= u) return i;
}
return p.length - 1; // numeric safety: if probs don't sum to exactly 1
}Eight lines. No external library. No special hardware. This is the entire generative engine of a language model, once you have a probability vector. Module 18’s sampler is structurally this; it just runs once per token and feeds the previous token back as context.
There are clever variants: cumsum + binary-search for huge vocabularies, or the alias method for sampling after a one-time preprocess. They’re optimizations; the algorithm above is the truth.
Sampling a Gaussian: Box-Muller
For continuous distributions the same idea applies (invert the CDF), but the CDF of a Gaussian has no closed form (it’s the error function), so the direct approach is annoying. There’s a trick.
Box-Muller: Given two independent uniforms ,
are two independent draws from . The intuition (no derivation): the first uniform sets a radius via the radial CDF of the 2D Gaussian; the second sets an angle uniformly. The pair is the polar form of a 2D standard-normal draw; the rectangular components are the Gaussian samples.
Two uniforms in. Two standard Gaussians out. That’s all the magic.
Any Gaussian is an affine transform of N(0,1)
Once you can draw , drawing is one line:
Multiply by the standard deviation, add the mean. This is exactly what torch.randn() * sigma + mu does, and also exactly what BatchNorm’s inverse transform does at inference time, using its learned and . Whenever you see “multiply by , add ” in ML code, it’s this identity.
Sample 10k draws below to confirm: histogram converges on the bell, , .
Sampling vs argmax: same vector, two policies
Given a probability vector, there are two ways to “use” it.
Greedy / argmax: Return the index with the highest probability. Deterministic. Picks the mode of the distribution.
Sample: Apply inverse-CDF. Stochastic. Picks each index with probability .
They are completely different policies operating on the same vector. Argmax of always returns the first index. Sampling returns the first index about 42% of the time and the second about 25%.
For a language model, greedy decoding produces repetitive, mode-collapsed output: it keeps picking the single most probable continuation, which on average is bland. Real generative LMs sample. They get adjusted via “temperature” and “top-” (module 17), which reshape the vector before sampling. The actual sampling step is always the same inverse-CDF algorithm we just wrote.
The widget below is the bigram count table from the last lesson. The bars on the right when you click a row label are exactly the categorical you’d sample from to choose the next character. Eight lines of JavaScript and a row of bars; that’s a generative model.
Bias-variance, in one page
Last detour before we close. When you estimate something from a sample (the mean, a model, a prediction) your estimator has two kinds of error.
Bias is systematic error: how wrong, on average, the estimator is compared to the truth.
Variance is spread: how much the estimator wobbles across different samples.
Total expected squared error decomposes as
Three terms: bias squared, variance, irreducible noise. No estimator can drive all three to zero with finite data. Simple models have high bias and low variance; they miss the same way every time. Complex models have low bias and high variance; they fit different patterns to every sample. The tradeoff is real, and module 13 will name its empirical signature in deep learning (“when val loss diverges from train loss, your variance is winning”).
That's module 8. Here's what you carry forward.
You can now read every probabilistic line in the rest of the course.
When module 9 talks about cross-entropy, it’s the same NLL you just learned to minimize.
When module 13 talks about initialization variance, it’s the variance-add rule you just used to derive .
When module 14 talks about the bigram model, it’s the categorical MLE you just clicked through.
When module 15 talks about causal attention, it’s a parameterization of the chain rule of probability.
When module 17 talks about temperature, top-, top-, those are transforms on the same vector that the eight-line sampler eats.
When module 18 calls sample() to generate text, it is, line for line, the function from earlier in this lesson.
Training a language model is one thing: tune so is as large as possible. Sampling from it is the other thing: inverse-CDF on the categorical it outputs at every position. Every word in every chapter ahead is a sentence in the dialect you just learned.
Module 9 is next. It renames negative log-likelihood as cross-entropy and tells you what units the resulting loss is measured in (bits, or nats, depending on the log base). The story continues.
Lesson complete
Nice tinkering.
Before you go