Neural language models
An n-gram model treats words as unrelated symbols. Suppose the training text contains "the cat ate the fish", "the dog ate the meat", "the cow ate the hay", and "the goat ate the grass" many times, and also "the sheep grazed" many times, but never once a sentence where a sheep eats. Ask a 4-gram model what follows "the sheep ate the". It has never seen that context, so it backs off to "ate the", and there more of the training eaters were meat eaters than plant eaters. Its best guess is fish or meat.
You know better, because you know that a sheep is the same kind of thing as a cow and a goat. Nothing in the counts encodes that. The 4-gram table has a separate row for "sheep ate the" and "cow ate the", and learning one row teaches the other nothing. This is the curse of dimensionality for language. A model with a free parameter for every context needs to see every context, and there are of them.
Bengio, Ducharme, Vincent, and Jauvin (2003) proposed the fix that every language model since has kept. Give each word a learned vector, make the prediction a smooth function of the context's vectors, and let similar words end up with similar vectors because that helps prediction. Then whatever the model learns about cows transfers to sheep for free. This section builds their model, derives its gradients, trains it, and shows exactly where it beats counting.
The model is an embedding, a hidden layer, and a softmax
Fix a context size of previous tokens, exactly as in an n-gram model. The neural probabilistic language model computes the next-token distribution in four steps.
- Look up. Each context token indexes a row of an embedding matrix , the same lookup table as §1.2.
- Concatenate. Stack the vectors into one long vector . The order matters, so the model knows which word was immediately before and which was further back.
- Hidden layer. Compute , with and .
- Output. Compute one score per vocabulary word and normalize,
with , , and an optional .
In plain words, the context words are turned into vectors, a small neural network mixes them, and a softmax turns the network's scores into a probability for every possible next word. The term is the paper's direct connections from the embeddings straight to the output, a linear shortcut that is optional (set to remove it). In the paper's own symbols the output is , so their are our .
Everything is trained jointly, the embeddings included, by maximizing the log-likelihood of the training text, which is the same as minimizing the cross-entropy loss that §3.1 called the log of perplexity,
The paper also added a weight decay penalty on the weights and the embedding table (not the biases), which you will see matters a great deal on small data.
Counting the parameters shows where the cost is
The parameter count is
Take the paper's best Brown configuration, , a context of words, , , and no direct connections. The embedding table has parameters, the hidden layer , and the output layer , for in total. More than three quarters of the model is the output layer, and the expensive part of every forward pass is the same, the matrix multiply that produces a score for every word. A 5-gram table, by contrast, has possible entries. The neural model has about two million parameters and assigns a sensible probability to every one of those 5-grams.
Count the parameters of a neural language model
Implement nplm_params(V, d, n, dh, direct) returning the number of parameters of the model above (embedding table, hidden layer with bias, output layer with bias, and the direct connections if direct is true).
Why it generalizes better than counts
The answer is smoothness. The predicted distribution is a continuous function of the context vector . If two words have nearby embeddings, swapping one for the other in the context moves only a little, so the prediction moves only a little. And training makes similar words nearby, because whenever cow and sheep appear in similar contexts, the gradient pushes their embeddings in similar directions to predict the same next words. Statistical strength flows between every context that shares a word, which is exactly what the n-gram table lacks.
Let's test the opening example. The training text below has three meat eaters and three plant eaters. Every animal appears in sentences like "the cow grazed" or "the fox hunted", but the sheep is never seen eating. We train a context-3 neural model in NumPy and compare it with a 4-gram model that backs off to the longest context it has seen.
The training loop is the forward pass and the backward pass we derive in the next part, written out in a few lines. Read it now for the shape of the computation, and come back to it after the derivation.
For the familiar context "cow ate the", both models agree, grass or hay. For "sheep ate the", the n-gram model has to back off to "ate the" and guesses fish or meat about 60% of the time, because that is what most training eaters ate. The neural model puts almost all its probability on grass and hay. It never saw a sheep eat. It saw sheep behave like cows and goats in other sentences ("grazed"), which placed the sheep's embedding where cow-like predictions come out. That is generalization through shared representations, in the smallest example that shows it.
The gradients, layer by layer
Training needs for every parameter. The derivation is the backpropagation recipe of §0.3 applied to four operations, and it is worth doing once by hand because every language model in this book has the same last two steps.
For one example with target (so the loss is ), write . Going backward,
with , , and .
In plain words, the error at the output is "predicted probabilities minus the one-hot target", every weight matrix gets the outer product of the error arriving at its output with the activation arriving at its input, and the error passes backward through the tanh by multiplying with its derivative .
∂ Each step, with shapes
Softmax plus cross-entropy. With , the derivative with respect to is . So . This is the result §0.2 derives in general.
Output layer. , so , which is the outer product . And , which is .
Through the tanh. with , and , so .
Hidden layer. The same outer-product rule gives . The input feeds both and , so its gradient is the sum of both paths.
Embeddings. is a concatenation, so split into blocks of size . Block is the gradient for the embedding row of the -th context word, and it is added to that row. When the same word appears twice in a context, or many times in a minibatch, the contributions accumulate, which is the scatter-add of §1.2. For a minibatch, average the per-example gradients (or equivalently divide by before multiplying, as the cell above does).
In code we store each weight matrix transposed, as W1 with shape ((n-1)d, dh), so that a minibatch of row vectors can be multiplied on the left (x @ W1). The math and the code compute the same thing.
Forward pass and loss of an MLP language model
Implement nplm_loss(P, X, Y). P is a dict with E (V, d), W1 ((n-1)d, dh), b1 (dh,), W2 (dh, V), b2 (V,), and W3 either None or ((n-1)d, V). X holds context ids with shape (B, n-1), oldest word first, and Y holds the (B,) target ids. Return (loss, logits), where the loss is the mean cross-entropy and the logits have shape (B, V). Concatenate the context embeddings in order, oldest first, and compute the loss stably.
Backpropagate through the MLP language model
Implement nplm_grads(P, X, Y) returning a dict of gradients of the mean loss with respect to E, W1, b1, W2, b2 (no direct connections in this exercise, P["W3"] is None). The tests compare with finite differences and include a batch where the same word appears many times.
A makemore-style character model
Words are not the only choice of token. Andrej Karpathy's makemore series rebuilds the Bengio model at the character level, which is a perfect laboratory because the vocabulary is tiny and you can watch everything. Below, the same model trains live in your browser on the 435-word story from §3.1, predicting each character from the previous few. The dashed line is the best held-out perplexity of any add-k n-gram model, tuned over both and .
Three things are worth noticing. First, with the default weight decay of 0.003 the neural model ends below the tuned n-gram baseline on held-out text (in our runs it settles around 4.5 to 4.6, against 4.87 for the best add-k model, a 4-gram with ), even on this tiny corpus. Second, with weight decay set to zero, training perplexity keeps falling while held-out perplexity bottoms out near 5.1 after about a thousand steps and then climbs past 10. The model is memorizing the story, and weight decay (the paper's choice too) is what keeps it honest. Third, look at the embedding panel. With no instruction, the vowels tend to end up apart from most consonants, because they behave alike as predictors (each is likely after a consonant and rarely after another vowel). On a story this small the effect is modest and the layout changes with the settings, but a grouping by behavior is something counting has no way to express.
In PyTorch
The production version is a few lines. nn.Embedding does the lookup, view(B, -1) concatenates, and F.cross_entropy fuses the log-softmax and the negative log-likelihood into one numerically stable operation. Here it is trained on the same animal corpus as the NumPy cells.
At the scale of real models, two details change. The output layer's weights are often tied to the embedding table, using as (with ), which saves parameters and often helps (Press and Wolf 2017). GPT-2 does this, as you will see in §6.2. And the hidden layer became much wider and deeper, but its job of mixing context vectors and passing them to a softmax is unchanged. The feed-forward block inside every Transformer (§5.3) is a descendant of this hidden layer, although it acts on one position at a time and leaves the mixing of context to attention.
What the MLP model still cannot do
The Bengio model fixed the generalization problem of counts, but it kept the n-gram's other limitation. Its context is a fixed window of tokens, and every position in the window has its own block of . That has two costs. Widening the window grows linearly, and the model must learn separately what cat means as the last word, as the second-to-last, and so on, because there is no sharing across positions. Nothing beyond the window can ever influence a prediction, no matter how important.
| n-gram (Kneser-Ney) | MLP language model | |
|---|---|---|
| parameters | one per seen n-gram, grows with data | fixed, about |
| unseen contexts | backs off to shorter contexts | smooth function of embeddings, so similar words share |
| training | counting, minutes on a CPU | gradient descent, the softmax over dominates |
| context length | fixed , sparsity limits to about 5 | fixed , parameters grow linearly with it |
| what it learns about words | nothing | embeddings, as a byproduct |
What we want is a model that reads a context of any length, shares the same weights at every position, and summarizes everything it has read into a vector. The obvious way to do that is to apply one function over and over, feeding its own output back in. That is a recurrent network, and it is §3.3.
Read more
- Bengio, Ducharme, Vincent, and Jauvin (2003). A Neural Probabilistic Language Model. The original paper, readable today. Section 1 on the curse of dimensionality is the clearest statement of why embeddings help.
- Karpathy (2022). Building makemore Part 2, MLP. Rebuilds this exact model on character-level names, with every implementation detail and the initialization pitfalls.
- Mikolov et al. (2013). Efficient Estimation of Word Representations in Vector Space. Starts from the neural language model and strips it down to CBOW and skip-gram.
- Press and Wolf (2017). Using the Output Embedding to Improve Language Models. Weight tying between the input and output tables.
- Yang, Dai, Salakhutdinov, and Cohen (2018). Breaking the Softmax Bottleneck. The rank argument for the output layer.