We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Tokenization & Embeddings step 10 of 15
Train MLM Pretraining Step
Implement train_mlm_step: corrupt selected input tokens, predict their original IDs with a bidirectional transformer, and take one SGD step. Compute every parameter gradient by hand.
The training signal comes only from selected positions. Other positions still provide context through attention, so their embeddings can receive gradients. The output head is a separate matrix in this exercise; it is not tied to w_emb.
Inputs and return value
- tokens: (N, T), integer-valued IDs delivered as floats. Cast to integers.
- w_emb: (V, D), token embeddings, with V = vocab_size.
- pos_embed: (T, D), position embeddings shared across examples.
- blocks_weights: (L, 6, D, D), L encoder blocks. Each contains [w_q, w_k, w_v, w_o, w_mlp1, w_mlp2].
- w_head: (D, V), the vocabulary projection.
- num_heads: a positive integer dividing D.
- lr: a finite, nonnegative learning rate.
- mask_token_id: an integer in [0, V), as are all input IDs.
- seed: the integer seed for the random recipe below.
N, T, V and D are positive; L may be zero. Parameters are finite and share a floating-point dtype and device. tokens is on that device. Shapes agree. Leave inputs unchanged and preserve the parameter dtype and device.
Return a one-dimensional tensor containing the updated parameters in this order: w_emb, pos_embed, blocks_weights, w_head. Flatten each in row-major order and concatenate. The length is VD + TD + L6DD + DV. Return weights, not gradients or a loss value. With lr=0, return the original parameters in this order.
1. Choose prediction targets and corrupt the input
Use a private CPU generator and make these three draws in order. Draw whole (N, T) arrays, including entries for positions that will not be selected. Keep the uniform draws in float32 regardless of the parameter dtype.
gen = torch.Generator(device="cpu").manual_seed(int(seed))
select_probs = torch.rand(N, T, generator=gen, dtype=torch.float32, device="cpu")
corrupt_probs = torch.rand(N, T, generator=gen, dtype=torch.float32, device="cpu")
random_tokens = torch.randint(0, V, (N, T), generator=gen, device="cpu")
Move these arrays to the input device before combining them with tokens. Do not change the global random-generator state. Select positions where select_probs is strictly less than 0.15. Start from a copy of the integer input IDs. For each selected position:
- corrupt_probs < 0.8: replace its ID with mask_token_id.
- 0.8 <= corrupt_probs < 0.9: replace it with the corresponding random_tokens ID.
- corrupt_probs >= 0.9: keep its original ID, but still include it in the loss.
A random replacement may equal the original ID or the mask ID. Unselected positions keep their IDs. If nothing was selected across the entire batch, select position (0, 0) and replace it with mask_token_id. This exercise uses that fallback so the loss always has at least one target. Do not resample.
The target for every selected position is its original ID, before corruption. No IDs have special padding behavior, including zero and mask_token_id.
2. Run the encoder and output head
Start with x = w_emb[corrupted_ids] + pos_embed, broadcasting positions over the batch. Apply the L blocks in order, with this pre-normalization architecture:
h = LN(x)
q, k, v = h @ w_q, h @ w_k, h @ w_v
y = x + multi_head_attention(q, k, v) @ w_o
x = y + GELU(LN(y) @ w_mlp1) @ w_mlp2
LN uses each token's feature mean and population variance: (token - mean) / sqrt(variance + 1e-5). There is no learned scale or bias. Split projected features into consecutive heads of width D/num_heads. Divide q @ k.T by sqrt(D/num_heads), apply stable softmax over key positions and concatenate heads in their original order. All positions in one example may attend to each other; different examples never attend to each other.
Use the elementwise approximation: GELU(u) = 0.5u(1 + tanh(sqrt(2/pi)(u + 0.044715u**3))). There are no biases, dropout, causal masks or final normalization. With L=0, use token-plus-position embeddings directly. Compute logits = x @ w_head.
3. Differentiate the selected-position loss
Let M be the number of selected positions after the fallback. Compute mean cross-entropy against their original IDs, averaging over M across the whole batch. Use stable log-softmax if you calculate the loss. The returned value does not include it.
At each selected row, the logit gradient is (softmax(logits) - one_hot(target)) / M. Other logit rows have zero gradient. There is no further division by N or T.
Backpropagate through the head and then through blocks in reverse order. Each residual addition sends the incoming gradient through both branches. For a shared matrix, flatten batch and position axes before computing its weight gradient; for example, dw_head = x_flat.T @ dlogits_flat.
Sum embedding gradients at every occurrence of each corrupted input ID. Repeated IDs accumulate into the same row. Sum position-embedding gradients across examples. The corruption choices and integer IDs are held fixed during this calculation. An unselected token can receive an embedding gradient through its contribution to a selected token's attention context.
4. Update all parameters once
Compute every gradient using the original weights, then apply new_parameter = parameter - lr * gradient. Do not update one weight early and use it to calculate another gradient. There is no weight decay, clipping, momentum or optimizer state.
Build normalization, GELU, attention, softmax and their derivatives from basic tensor operations. Do not use autograd, backward(), packaged transformer layers or optimizer helpers. Cache the forward intermediates needed for the manual backward pass.
Understand the ideas behind this exercise. The Problem tab contains the exact implementation requirements.
Match each prediction to the token it should predict
A causal language model predicts the next token from the tokens before it. For
sequence [A, B, C], logits produced after reading A are trained against B,
and logits after reading B are trained against C. This is why input and target
sequences are usually shifted by one position. A causal attention mask prevents
access to later tokens that would reveal the answer.
A masked language model instead predicts selected tokens from a corrupted sequence and can use context on both sides. The supervised positions are not necessarily just those showing a mask token. A corruption rule may leave some selected tokens unchanged or replace them randomly while still training on them. Compute the loss over the specified supervised positions only.
Teacher forcing supplies the correct previous tokens
In encoder-decoder training, teacher forcing feeds the decoder the correct previous target tokens. During generation, it usually receives its own previous choices instead. A mistake can therefore change later inputs, which helps explain why training loss alone does not describe generation quality.
Convert mean token loss to perplexity
For M valid target tokens, mean negative log likelihood is
$$L=-\frac1M\sum_t\log p(y_t).$$
With natural logarithms, perplexity is exp(L). A model that gives the correct
token probability 1/4 at every step has perplexity 4. A uniform distribution
over a vocabulary of size V has perplexity V.
When batches contain different numbers of valid tokens, add their token losses and valid-token counts before dividing. Averaging batch means would give small batches too much weight. Exclude padding and other unsupervised positions as specified, and define what happens when no tokens are supervised.
Perplexity also depends on tokenization. Scores based on different token units are not directly comparable just because both are called perplexity.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
PyTorch reference solution
Sign in to track your attempts toward unlocking the reference solution.