Week 6
Week 6 — From tokens to a small language model: understanding prediction through one example
Key questionHow does a model turn you like AI into numbers, score every candidate using context, and learn to assign more probability to the correct next token?
Learning objectives
- Distinguish tokens, tokenizers, vocabularies, token IDs, embeddings, context representations, logits, and probabilities.
- Explain how an output head uses dot products and biases to produce raw logits; calculate Softmax and cross-entropy by hand.
- Follow the same batch through [3,3] → [3,2] → [3,2,4] → [3,2,5] → [6,5] → scalar loss.
- Run Bigram training and generation, then explain its missing access to earlier context and how attention can supply it.
100 min estimated reading time
We will start with three sentences whose next tokens we already know, not with probability notation. You need Week 5's tensors, matrix multiplication, and one training step. The English teaching vocabulary is fixed as [you, like, AI, study, we]. Its IDs will stay the same when we introduce attention. These English sentences preserve the Chinese edition's numerical input and target patterns; they are an adaptation, not a word-for-word translation.
Scroll horizontally to view all columns.
| Study session | The problem this session solves |
|---|---|
| 1. Turn text into prediction tasks | Start with tokens, then inputs and targets, then a batch. Explain why B counts sample rows, T counts input positions in each row, and T input positions require T+1 original tokens. |
| 2. Represent, score, and measure error | Distinguish C representation features from V vocabulary candidates. Follow embedding → output head applied to a given h → logits → Softmax → cross-entropy. |
| 3. Connect the steps and learn | Connect the calculations for all six prediction tasks. Then run one complete Bigram training step and repeat the training loop. |
| 4. Generate text and identify the limits | First explain how to select and append a token. Then show why a model that sees only the last token cannot distinguish different prefixes. This limitation motivates attention. |
Completion criteria: calculate p(like) ≈ 0.5923 and cross-entropy ≈ 0.5237 by hand, explain g = p − one-hot, and run python week06_bigram.py. You may return to the derivative of the natural logarithm and perplexity on a second pass. Do not skip the output head's actual multiplications and additions, probability normalization, or input/target alignment.
Alternate reading, hand calculation, and changing the code. You can split each study session into several shorter sittings. The original section numbers remain for existing links and reference; follow the page from top to bottom rather than jumping around to restore the old numbering.
Week 6 goal: turn text into six prediction tasks
Throughout this week, the tokenizer splits on spaces. The vocabulary, corpus, and dimensions stay fixed across sections. During training, the model learns from six current-token → actual-next-token tasks together. During generation, it appends just one token at a time.
Scroll horizontally to view all columns.
| ID | Token |
|---|---|
| 0 | you |
| 1 | like |
| 2 | AI |
| 3 | study |
| 4 | we |
Corpus:
you like AI
we like you
you study AI
raw IDs = [[0,1,2], [4,1,0], [0,3,2]]
inputs = [[0,1], [4,1], [0,3]]
targets = [[1,2], [1,0], [3,2]]Scroll horizontally to view all columns.
| Symbol | Fixed value | Meaning |
|---|---|---|
| B | 3 | The three sequences in the batch |
| N | 3 | Tokens in each original sequence, before shifting the targets |
| T | 2 | Next-token training positions per sequence after aligning inputs with the following tokens |
| C | 4 | Continuous-valued features in each token embedding |
| V | 5 | Vocabulary candidates / rows in the embedding table |
- raw IDs [3,3]
- shifted inputs [3,2]
- embeddings [3,2,4]
- logits [3,2,5]
- flattened logits [6,5]
- flattened targets [6]
- scalar mean cross-entropy loss
Knowledge check
What are the six fixed prediction tasks this week?
1. Token, tokenizer, vocabulary, and token ID
A token is a basic unit that a tokenizer passes to a model. It need not be a whole word: it might be a character, a subword, or a byte. For this week's hand calculations, our tokenizer splits on spaces. The vocabulary is the finite list of allowed tokens. A token ID is a token's integer address in that list.
Different tokenizers can turn the same text into different numbers of tokens. For example, learning might be one token or might split into learn and ing. Models read and generate tokens, so the splitting method affects sequence length, how much text fits in the context window, and the computation needed for training and inference. GPT-style systems commonly use subword or byte-based methods; Week 9 examines these in more detail.
Scroll horizontally to view all columns.
| Original text | Tokens | IDs |
|---|---|---|
| you like AI | [you, like, AI] | [0,1,2] |
| we like you | [we, like, you] | [4,1,0] |
| you study AI | [you, study, AI] | [0,3,2] |
Knowledge check
Why does we like you encode as [4,1,0]? What does ID 4 express?
8. Inputs and targets: create prediction tasks from text by offsetting the answers by one position
The language-model training task is not to repeat the input unchanged. It is to predict the next token from the text available so far. The original text therefore supplies both the questions and their correct answers: each position predicts the token immediately after it. This is called self-supervised learning because the labels come from the text itself, rather than someone labeling every sentence by hand.
raw IDs [B,N] = [[0,1,2], [4,1,0], [0,3,2]] # [3,3]
inputs [B,T] = [[0,1], [4,1], [0,3]] # [3,2]
targets [B,T] = [[1,2], [1,0], [3,2]] # [3,2]
T = N - 1 = 2Now put the three independent sequences in a batch: row 0 is you like AI, row 1 is we like you, and row 2 is you study AI. B = 3 means that we process three samples together. They are not concatenated and do not supply context to each other. Each contributes T = 2 prediction positions, giving 3×2 = 6 tasks in total.
Scroll horizontally to view all columns.
| Task position [b,t] | Original sequence | Context available to a causal language model here | Correct target | Which probability does cross-entropy use? |
|---|---|---|---|---|
| [0,0] | you like AI | you | like / ID 1 | p[0,0,1] |
| [0,1] | you like AI | you like | AI / ID 2 | p[0,1,2] |
| [1,0] | we like you | we | like / ID 1 | p[1,0,1] |
| [1,1] | we like you | we like | you / ID 0 | p[1,1,0] |
| [2,0] | you study AI | you | study / ID 3 | p[2,0,3] |
| [2,1] | you study AI | you study | AI / ID 2 | p[2,1,2] |
- Take original token windows R [B,N] from text
- Offset inputs X [B,T] and targets Y [B,T], where T = N−1
- Use a causal mask so each Transformer representation h [B,T,C] depends only on the current and earlier positions
- The output head produces five logits per task [B,T,V]; Softmax converts them to probabilities
- The target ID selects the correct probability p[b,t,target_id] for each task
- Cross-entropy computes and averages loss across all B×T tasks
Knowledge check
Why does the original sequence you like AI produce only two tasks? What are the target and visible context for task 0?
4. Start with a batch: what do B, T, C, and V do?
Do not treat [B,T,C] as a formula to memorize. Think of a table with three indexing steps: choose a training sample, choose a prediction position within that sample, then read the continuous-valued features at that position. Start with just one sample.
One sentence: you like AI
Input IDs = [0, 1] shape [T] = [2]
Target IDs = [1, 2] shape [T] = [2]
Position 0: current token you; predict like
Position 1: current token like; predict AIHere T = 2 means that this sample supplies two next-token prediction positions. The original sentence has N = 3 tokens; offsetting the targets gives T = N−1 = 2 tasks. T is not a universal constant. It is the number of input positions per sequence in the current batch.
Scroll horizontally to view all columns.
| Before putting a sample in a batch | The question it answers |
|---|---|
| inputs[0] = you | like |
| inputs[1] = like | AI |
A batch puts several independent samples in one tensor so the CPU or GPU can compute them together. In our training setup, we then average the losses from their prediction positions. This does not concatenate three sentences into one long sentence or pass information from sample 0 to sample 1. In this course's Transformer, attention never mixes different samples in the batch.
B = 3: process three independent samples together
inputs [B,T] = [[0,1], [4,1], [0,3]] # shape [3,2]
targets[B,T] = [[1,2], [1,0], [3,2]] # shape [3,2]
b=0: you -> like; like -> AI
b=1: we -> like; like -> you
b=2: you -> study; study -> AIScroll horizontally to view all columns.
| Symbol | Value in this example | What it counts | How to read it here |
|---|---|---|---|
| B | 3 | Independent samples in the batch | First choose a sentence |
| T | 2 | Prediction positions in each sample | Then choose a position within that sentence |
| B×T | 6 | Total next-token tasks in this training step | Two tasks from each of three samples |
Now add embedding. inputs[b,t] is still just an integer address. The embedding lookup retrieves a floating-point vector of length C for every address. B and T remain because we still need to identify the sample and position. A new final axis of length C holds the internal representation at that position.
Embedding table E has shape [V,C] = [5,4]
inputs[0,1] = 1 # sample 0, position 1: like
x[0,1,:] = E[1] # retrieve all C=4 numbers
= [0.60, 0.30,-0.20,0.10]
[B,T] --Embedding--> [B,T,C]
[3,2] ------------> [3,2,4]The colon in x[b,t,:] means all C features at this position. For example, x[1,0,:] contains the four numbers for we in sample 1. C is the representation width allocated to each position. We deliberately choose 4 for hand calculation; real models usually use much larger widths. Individual coordinates do not have predefined semantic names; training adjusts them.
x.shape = [3,2,4] # three axes: rank=3
x[1].shape = [2,4] # sample 1: two positions, four features each
x[1,0].shape = [4] # position 0: the whole representation of we
x[1,0,2] = 0.30 # feature 2: a scalar
b: which sample?
t: which position in that sample?
c: which coordinate of that position's representation?An axis tells you which question an index answers; its size tells you how many choices that index has. Shape [3,2,4] has rank 3 because it has three indexing axes: b, t, and c. It contains 3×2×4 = 24 scalar values. Rank 3 does not mean three values.
Scroll horizontally to view all columns.
| Symbol | This example | Its role | Where it appears |
|---|---|---|---|
| B | 3 | How many independent samples are processed together | [B,T]、[B,T,C]、[B,T,V] |
| T | 2 | How many prediction positions each sample currently has | [B,T]、[B,T,C]、[B,T,V] |
| C | 4 | How wide each position's continuous representation is | Embedding table [V,C] and representations [B,T,C] |
| V | 5 | How many candidate tokens the vocabulary contains; not an extra feature of input x | Rows of the embedding table [V,C]; later, candidate scores in logits [B,T,V] |
Scroll horizontally to view all columns.
| If we change | What changes directly | Main effect |
|---|---|---|
| B:3 → 1 | Each step contains only one sample | The model architecture stays the same; parallel work, memory use, and samples contributing to one gradient change |
| T:2 → 4 | Each sample contains four ordered input positions | The current window is longer; attention requires more computation between positions |
| C:4 → 8 | Each position's representation grows from four numbers to eight | Representation width, associated weight matrices, parameter count, and computation change |
| V:5 → 10 | The vocabulary contains ten candidate tokens | Both the embedding row count and output logits per position become ten |
Token IDs [B,T] = [3,2] one integer address per position
Embedding / context state [B,T,C] = [3,2,4] four internal features per position
Output logits [B,T,V] = [3,2,5] five candidate scores per positionKnowledge check
What is x[1,0,:] in this example? Why are there six tasks rather than one sentence of six tokens?
2. Why turn a token ID into an embedding?
An embedding looks up a learnable continuous vector using a discrete ID. Our input table is nn.Embedding(V,C) = nn.Embedding(5,4): five vocabulary rows with four learnable features per row. The values below are specified teaching initializations, not human-written definitions of words.
Using IDs directly as numerical features would impose nonexistent relationships such as we = 4 being twice AI = 2. The ID only says where to look. The embedding provides floating-point features that later layers can combine, compare, and adjust through gradients.
Scroll horizontally to view all columns.
| ID / Token | c₀ | c₁ | c₂ | c₃ |
|---|---|---|---|---|
| 0 / you | 0.20 | -0.10 | 0.70 | 0.30 |
| 1 / like | 0.60 | 0.30 | -0.20 | 0.10 |
| 2 / AI | -0.40 | 0.80 | 0.50 | -0.30 |
| 3 / study | 0.10 | 0.20 | 0.90 | 0.40 |
| 4 / we | -0.70 | 0.40 | 0.30 | 0.60 |
For example, you selects E[0] = [0.20,-0.10,0.70,0.30], while we selects E[4] = [-0.70,0.40,0.30,0.60]. Training adjusts these coordinates through loss calculation, backpropagation, and an optimizer update.
Knowledge check
Does input ID 4 retrieve a scalar or a vector?
3. One-hot equivalence and the [V,C] embedding matrix
Scroll horizontally to view all columns.
| ID / Token | c₀ | c₁ | c₂ | c₃ |
|---|---|---|---|---|
| 0 / you | 0.20 | -0.10 | 0.70 | 0.30 |
| 1 / like | 0.60 | 0.30 | -0.20 | 0.10 |
| 2 / AI | -0.40 | 0.80 | 0.50 | -0.30 |
| 3 / study | 0.10 | 0.20 | 0.90 | 0.40 |
| 4 / we | -0.70 | 0.40 | 0.30 | 0.60 |
one_hot(4) = [0,0,0,0,1]
one_hot(4) @ E
= E[4]
= [-0.70,0.40,0.30,0.60]
nn.Embedding(5,4).weight.shape == [5,4]A one-hot vector acts as a selector: here it selects row 4. An embedding lookup does not need to allocate a large vector of length V; it reads E[id] directly. The two methods produce the same forward result.
Knowledge check
What are the one-hot vector and lookup result for AI?
9. Logits: calculate the raw scores of all five candidates
We now predict the token after the prefix you. Suppose the input embedding and context processor produce h = [0.20,-0.10,0.70,0.30] at this position. To keep the hand calculation simple, this step uses h equal to the input embedding of you. In a full Transformer, h has usually incorporated context and passed through several layers of computation.
Scroll horizontally to view all columns.
| Context feature | h value | How to interpret it |
|---|---|---|
| h₀ | 0.20 | Learned context feature 0 |
| h₁ | -0.10 | Learned context feature 1 |
| h₂ | 0.70 | Learned context feature 2 |
| h₃ | 0.30 | Learned context feature 3 |
The output head stores a row of output weights and a bias for each vocabulary candidate. Each row is a learnable scoring rule: weight the same four features of h according to that candidate's rule, sum them, and add its bias.
Scroll horizontally to view all columns.
| Candidate | Output Weight wᵢ | Bias bᵢ | Substitute and calculate | Logit |
|---|---|---|---|---|
| you | [1,0,0,0] | -0.2 | 1(0.2)−0.2 | 0 |
| like | [0,1,2,2] | 0.1 | −0.1+2(0.7)+2(0.3)+0.1 | 2 |
| AI | [0,0,1,1] | 0 | 0.7+0.3 | 1 |
| study | [0,3,−1,0] | 0 | 3(−0.1)−0.7 | −1 |
| we | [1,2,0,0] | 0 | 0.2+2(−0.1) | 0 |
For like, the full calculation is 0×0.20 + 1×(−0.10) + 2×0.70 + 2×0.30 + 0.10 = 2. None of these operations looks up the correct answer. The current h and the scoring weights for like simply combine to produce a relatively high score.
import torch
# Four internal features of the current context
h = torch.tensor([
0.20,
-0.10,
0.70,
0.30,
]) # [C] = [4]
# One row of scoring weights per candidate token
W_out = torch.tensor([
[1.0, 0.0, 0.0, 0.0], # you
[0.0, 1.0, 2.0, 2.0], # like
[0.0, 0.0, 1.0, 1.0], # AI
[0.0, 3.0, -1.0, 0.0], # study
[1.0, 2.0, 0.0, 0.0], # we
]) # [V,C] = [5,4]
bias = torch.tensor([
-0.2,
0.1,
0.0,
0.0,
0.0,
]) # [V] = [5]
logits = W_out @ h + bias
print(logits)
# tensor([0., 2., 1., -1., 0.])By hand, we calculate five dot products separately. W_out stacks the five scoring rules into rows, so W_out @ h + bias produces all five scores at once. The bias vector [5] supplies a learnable baseline offset for each candidate, independent of the current h.
# H: [B,T,C] = [3,2,4]
# W_out.T: [C,V] = [4,5]
logits = H @ W_out.T + bias
# logits: [B,T,V] = [3,2,5]Logits matter to Softmax because of the differences between candidates. If one logit exceeds another by 1, its Softmax probability is e¹ ≈ 2.718 times as large. Raising only the logit for like increases its probability relative to the others. Adding 100 to every logit leaves all differences unchanged, so it leaves the probability distribution unchanged too.
Knowledge check
How does the logit for like become 2 in this example? Does it mean 200%?
10. Softmax: turn logits into probabilities
Start with just two scores, [0,1]. Exponentiating gives approximately [1,2.718], whose sum is 3.718. Dividing by this sum gives approximately [0.269,0.731]. Both entries are nonnegative and they sum to 1. Our five-candidate table uses the same calculation with three more entries.
The preceding section calculated logits = [0,2,1,-1,0] from h, W_out, and bias. These scores can be compared, but they are not probabilities: 2 does not mean 200%, and −1 does not mean a negative probability. Sampling and the probability interpretation of cross-entropy require a valid distribution: nonnegative entries summing to 1.
Step 1 is exponentiation: e⁰ = 1, e² ≈ 7.389, e¹ ≈ 2.718, and e⁻¹ ≈ 0.368. Exponentiating a finite negative logit still gives a positive number. That candidate therefore retains a chance, although its relative weight is smaller.
Scroll horizontally to view all columns.
| candidate order | logit | exponential | probability |
|---|---|---|---|
| you | 0 | 1.000 | 0.080 |
| like | 2 | 7.389 | 0.592 |
| AI | 1 | 2.718 | 0.218 |
| study | -1 | 0.368 | 0.029 |
| we | 0 | 1.000 | 0.080 |
Step 2 is normalization. The five exponential weights sum to approximately 12.475. Dividing each by that same sum gives approximately [0.080,0.592,0.218,0.029,0.080]. The exact probabilities sum to 1; these displayed rounded values sum to approximately 1.
exponentials = [1.000,7.389,2.718,0.368,1.000]
sum = 12.475
probabilities = [0.080,0.592,0.218,0.029,0.080]
0.080 + 0.592 + 0.218 + 0.029 + 0.080 ≈ 1- Exponentiation: every finite logit becomes a positive weight, including negative logits.
- Order preservation: if zᵢ > zⱼ, then exp(zᵢ) > exp(zⱼ). The higher-scoring candidate stays higher.
- Relative weighting: pᵢ/pⱼ = exp(zᵢ−zⱼ). The difference between logits determines the probability ratio.
- Normalization: divide by the sum of all candidate weights so that the results sum to 1.
import torch
import torch.nn.functional as F
logits = torch.tensor([0.0, 2.0, 1.0, -1.0, 0.0])
probabilities = F.softmax(logits, dim=-1)
print(probabilities)
# tensor([0.0802, 0.5923, 0.2179, 0.0295, 0.0802])
print(probabilities.sum())
# tensor(1.)For logits shaped [B,T,V] = [3,2,5], dim=-1 applies Softmax separately to the five vocabulary candidates at each [b,t] position. It does not mix sentences or positions into the same denominator.
In the two-candidate example, adding 100 to both scores gives [100,101] without changing the probabilities: the difference is still 1. Implementations often subtract the maximum first, turning [0,1] into [-1,0], to avoid very large exponentials. Softmax depends on score differences, not an absolute zero point.
Knowledge check
Why does Softmax exponentiate first, then divide by the sum of the exponentials?
11. Cross-entropy: from the correct answer's probability to loss and gradients
Softmax has produced five candidate probabilities. Suppose the actual next token is like. We first select the model's probability for this correct answer, p_correct ≈ 0.592. We want loss to approach 0 as that probability approaches 1, and the penalty to grow as it approaches 0.
Scroll horizontally to view all columns.
| p(correct) | −ln(p) | Training interpretation |
|---|---|---|
| 0.99 | 0.010 | Almost certain of the correct answer; penalty close to 0 |
| 0.90 | 0.105 | Confident in the correct answer |
| 0.60 | 0.511 | Favors the correct answer but remains noticeably uncertain |
| 0.50 | 0.693 | Only half the probability goes to the correct answer |
| 0.10 | 2.303 | Low probability for the correct answer |
| 0.01 | 4.605 | Very little probability for the correct answer; a large penalty |
Scroll horizontally to view all columns.
| Context | target | p(correct) | loss |
|---|---|---|---|
| you | like (ID 1) | 0.592 | -ln(0.592) ≈ 0.524 |
| you (if it were the target instead) | AI | 0.218 | Approximately 1.524 |
| you (if it were the target instead) | we | 0.080 | Approximately 2.524 |
Cross-entropy does more than check whether argmax is correct. If the answer is like, increasing its probability from 0.51 to 0.90 still reduces loss, even if it was already the top candidate. For several positions, mean loss averages the negative log-probability of the correct target at each position.
Vocabulary order = [you, like, AI, study, we]
predicted probabilities = [0.080, 0.592, 0.218, 0.029, 0.080]
one_hot(target=like) = [0, 1, 0, 0, 0]
L = -Σ yᵢ ln(pᵢ)
= -(0 ln 0.080 + 1 ln 0.592 + 0 ln 0.218
+ 0 ln 0.029 + 0 ln 0.080)
= -ln(0.592)
≈ 0.524Scroll horizontally to view all columns.
| Candidate | pᵢ | Target yᵢ | gᵢ=pᵢ−yᵢ | Direction of a direct gradient-descent step on logits |
|---|---|---|---|---|
| you | 0.0802 | 0 | +0.0802 | Lower z_you slightly |
| like (correct) | 0.5923 | 1 | −0.4077 | Raise z_like slightly |
| AI | 0.2179 | 0 | +0.2179 | Lower z_AI slightly |
| study | 0.0295 | 0 | +0.0295 | Lower z_study slightly |
| we | 0.0802 | 0 | +0.0802 | Lower z_we slightly |
p = [0.080, 0.592, 0.218, 0.029, 0.080]
one_hot(target=like) = [0, 1, 0, 0, 0]
p - one_hot(target) = [0.080,-0.408, 0.218, 0.029, 0.080]
In a direct-logit illustration, gradient descent subtracts this gradient:
like has a negative gradient, so its logit increases;
the other candidates have positive gradients, so their logits decrease.Notation reminder: y = 1 can denote the integer ID of the correct class. In the one-hot formula, yᵢ denotes entry i of the corresponding target vector: 1 only when i = 1, otherwise 0. It does not split the integer 1 into many ones.
Knowledge check
If the target is like and p_like = 0.5923, what are g_like and g_AI? What directions do they give gradient descent?
12. Why does PyTorch F.cross_entropy take logits?
F.cross_entropy combines log-Softmax with negative log-likelihood in a numerically stable way. Its score input is therefore raw logits. This course uses hard labels: torch.long target IDs from 0 to V−1, with one correct class per task.
import torch
import torch.nn.functional as F
# One task, five raw candidate logits
logits_one = torch.tensor(
[[0.0, 2.0, 1.0, -1.0, 0.0]],
requires_grad=True,
) # [1,5]
# The correct answer is like, ID=1
target_one = torch.tensor([1], dtype=torch.long) # [1]
loss_one = F.cross_entropy(logits_one, target_one)
loss_one.backward()
print(loss_one.item())
# approximately 0.524
print(logits_one.grad)
# approximately [[0.080, -0.408, 0.218, 0.029, 0.080]]This code follows the previous hand calculation exactly. [1,5] means one task with five candidate scores; [1] supplies the one correct class ID it needs. Once the single-task calculation is clear, arrange the batch's six positions as six tasks.
Scroll horizontally to view all columns.
| flattened row | batch position | input / context | target token | target ID |
|---|---|---|---|---|
| 0 | [0,0] | you / you | like | 1 |
| 1 | [0,1] | like / you like | AI | 2 |
| 2 | [1,0] | we / we | like | 1 |
| 3 | [1,1] | like / we like | you | 0 |
| 4 | [2,0] | you / you | study | 3 |
| 5 | [2,1] | study / you study | AI | 2 |
import torch
import torch.nn.functional as F
# logits: [B,T,V] = [3,2,5]; targets: [B,T] = [3,2]
B, T, V = logits.shape
logits_2d = logits.reshape(B * T, V) # [6,5]
targets_1d = targets.reshape(B * T) # [6], dtype=torch.long
loss = F.cross_entropy(
logits_2d,
targets_1d,
) # scalar mean loss
# Incorrect: F.cross_entropy expects logits, not already-softmaxed probabilities.
# probabilities = F.softmax(logits_2d, dim=-1)
# loss = F.cross_entropy(probabilities, targets_1d)API clarification: PyTorch cross-entropy also supports floating-point class-probability targets with the same shape as the logits, including soft labels. Saying it never accepts one-hot targets is therefore inaccurate. This course uses integer hard labels to keep the six tasks and six answers easy to match: [BT,V] logits and [BT] long targets.
Knowledge check
Why is logits_2d shaped [6,5], while targets_1d is [6]?
6. Language models: what questions do these five probabilities answer?
The preceding calculation followed task [0,0]: the visible text is you and the correct next token is like. A context representation h is passed to the output head to produce logits z = [0,2,1,−1,0]. Softmax then gives p ≈ [0.0802,0.5923,0.2179,0.0295,0.0802]. We will not repeat the dot products or exponentials here. Instead, ask what each of these five numbers means for language.
Scroll horizontally to view all columns.
| Part of the formula | Meaning here | Example: you like AI |
|---|---|---|
| x | A sequence of tokens, usually represented in code by token IDs | you, like, and AI are x₀, x₁, and x₂ respectively. |
| t | The position whose prediction task we are considering | If t = 1, the model has seen positions 0 and 1. |
| x≤t | The visible prefix from the start through t | x≤1 is you like. |
| | | Given that … | Not division; read it as given the prefix you like. |
| P | The model's probability rule | P(AI | you like) is the model's probability that AI is the next token. |
Read P(xₜ₊₁ | x≤ₜ) as: after seeing prefix x≤ₜ, what is the probability of next token xₜ₊₁? In practice, the model does not return just this one number. It returns a distribution p over the whole vocabulary. For cross-entropy, we select the entry pᵢ corresponding to the correct target.
We can now read the main computation precisely: the context processor produces h; the output head calculates z = W_out h + b_out; and p = Softmax(z) gives the conditional probabilities of the candidates. h holds features computed from context, z contains unnormalized candidate scores, and p is the distribution that answers next-token probability questions.
Knowledge check
What does P(AI | you like) ask? How does it connect to p, z, and h?
5. Putting it together: how do six tasks become one loss?
This exercise combines the earlier sections. Original numbers and links are preserved, but the reading order has changed: first work through inputs/targets, batches, embeddings, logits, Softmax, and cross-entropy, then connect them here. Do not try to memorize a single diagram full of unfamiliar terms.
Scroll horizontally to view all columns.
| Object | Value or shape | Where it comes from and what it does |
|---|---|---|
| Original text | you like AI; we like you; you study AI | Training material prepared by a person, not generated by the model. |
| raw IDs | [[0,1,2],[4,1,0],[0,3,2]] | Encode with the fixed vocabulary, keeping three consecutive tokens in each original sample. |
| inputs / targets | [[0,1],[4,1],[0,3]] / [[1,2],[1,0],[3,2]] | Offset the input and target positions within each row to obtain six labeled tasks. |
| embedding / h | [3,2,4] / [3,2,4] | The first is a lookup; the second is produced by the chosen model processing the visible context. |
| logits | [3,2,5] | Use the same output head at every task to score all five candidates. |
| mean loss | One number | Select the corresponding target probability in each row, take its negative logarithm, and average the six losses. |
First identify just one task: [0,0] has input you and target_id = 1, meaning like. To check the output head, the hand-calculation example supplies h = [0.20,−0.10,0.70,0.30] and fixed output weights that produce z = [0,2,1,−1,0]. Here h is a given input to a numerical demonstration, not a claim that a full Transformer always sets h equal to the embedding. Weeks 7–8 will open up the component that calculates h.
After vocabulary-axis Softmax, like has probability approximately 0.5923, giving this task loss ≈ 0.5237. If another task sees only you at the same position, the same deterministic model must produce the same distribution. If its target is study, select approximately 0.02949 from that distribution instead, giving loss ≈ 3.5237. The target does not enter the prediction computation.
Trace one [b,t] position:
inputs[b,t] → embedding[b,t,:] → h[b,t,:]
→ logits[b,t,:] → p[b,t,:]
targets[b,t] ───────────────→ select the correct entry to compute L[b,t]
Only then take L.mean(). The actual CE API receives logits directly.Flattening only changes the arrangement: [0,0], [0,1], [1,0], [1,1], [2,0], and [2,1] become rows 0–5 in that order. It does not concatenate contexts from different sentences. Attention has already processed each batch row separately before flattening.
# Loss-calculation fragment: logits and targets come from the forward pass and data preparation above.
flat_logits = logits.reshape(6, 5)
flat_targets = targets.reshape(6) # [1,2,1,0,3,2]
loss = F.cross_entropy(flat_logits, flat_targets)EX06-Integration: without looking at the answer, explain three things. Which task does flat_logits[4] represent? Why can h and the embedding have the same shape but different values? After backward, which persistent parameters receive gradients and which integers do not?
Knowledge check
Check your answers after completing the integration exercise.
13. Bigram: the smallest language model that sees only the current token
Bigram means a pair of adjacent tokens. This model learns one relationship: given current token x_t, which token tends to follow it in the training data? For example, it can learn that like follows we. It does not examine text preceding the current token. It is not a replacement for a complete GPT, but a minimal baseline whose calculation we can inspect fully.
Scroll horizontally to view all columns.
| table | shape | row meaning | column / feature meaning |
|---|---|---|---|
| input embedding E | [V,C]=[5,4] | Current token | Four learnable features |
| Bigram W_bigram | [V,V]=[5,5] | Current token | Five candidate next-token logits |
import torch
import torch.nn as nn
import torch.nn.functional as F
class BigramLanguageModel(nn.Module):
def __init__(self, vocab_size: int):
super().__init__()
# Each ID retrieves next-token logits, not a semantic feature embedding.
self.token_table = nn.Embedding(vocab_size, vocab_size)
def forward(self, token_ids, targets=None):
# token_ids: [B, T]
logits = self.token_table(token_ids)
# logits: [B, T, V_vocab]
if targets is None:
return logits, None
B, T, V_vocab = logits.shape
loss = F.cross_entropy(
logits.reshape(B * T, V_vocab),
targets.reshape(B * T),
)
return logits, lossKnowledge check
What does a Bigram do, and why can it not distinguish you like from we like?
14. Work through one Bigram training step: form tasks, measure error, and update the score table
The previous Bigram can predict by lookup, but a newly created table has not learned what follows you. Training adjusts that table by repeatedly using the actual next tokens as labels, encouraging suitable candidates to receive more probability. The loop is simply the program structure that repeats these operations.
The training-script body below runs after the BigramLanguageModel class in Section 13. Each step clears previous gradients and makes a fresh prediction using the same tasks and current parameters. Create the model and optimizer only once, outside the loop.
# Run after the imports and BigramLanguageModel definition in Section 13.
inputs = torch.tensor([
[0, 1], # you→like, like→AI
[4, 1], # we→like, like→you
[0, 3], # you→study, study→AI
], dtype=torch.long)
targets = torch.tensor([
[1, 2],
[1, 0],
[3, 2],
], dtype=torch.long)
torch.manual_seed(7)
model = BigramLanguageModel(vocab_size=5)
with torch.no_grad():
model.token_table.weight.zero_() # Match the all-zero hand calculation.
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)
model.train()
for step in range(500):
optimizer.zero_grad(set_to_none=True) # Clear previous gradients.
logits, loss = model(inputs, targets) # [3,2,5], scalar
loss.backward() # Gradients at current parameters.
optimizer.step() # Update the score table.
if step % 100 == 0:
print("step", step, "loss_before_update", loss.item())
# Recompute with the final parameters to obtain loss after the update.
with torch.no_grad():
_, final_loss = model(inputs, targets)
print("loss_after_500_updates", final_loss.item())Scroll horizontally to view all columns.
| Step | State affected |
|---|---|
| forward | Use the actual inputs to compute all [3,2,5] logits and the mean loss |
| zero_grad | Clear gradients accumulated in the previous iteration |
| backward | Differentiate the loss for the six targets with respect to the parameters |
| step | Use the gradients to update the [5,5] parameter table |
Knowledge check
Why does the like row receive training signals from different targets?
7. Autoregressive generation: choose a token, append it, and predict again
Think of generation as a small loop. The model reads the current prompt and produces a probability for every vocabulary candidate. A generator selects one token and adds it to the prompt. The next prompt has changed, so this simple implementation computes the next h, logits, and probabilities again. Feeding a selected output back as the next input is autoregressive generation.
Now compare training and generation. Training text already supplies the actual next tokens, so many input/target positions can be prepared and scored together. During generation, the actual next token is unavailable: we must choose and append the first next_id before we know the input for round 2.
Scroll horizontally to view all columns.
| Aspect | Training: teacher forcing | Generation: autoregressive |
|---|---|---|
| Where the next token comes from | The original training text supplies the correct target | Argmax or sampling chooses next_id from the current distribution |
| The next input | All offset input positions can be constructed in advance | It becomes available only after appending the chosen next_id |
| Positions processed | Score all B×T known training positions together | Select one new token per sample using only the current final position |
| Do parameters change? | Loss → backward → optimizer.step updates them | No; we select and append IDs using a fixed model |
Knowledge check
After generating like from you, what is the next model input? Why not keep using only you?
15. Select the next token from the final position: argmax and sampling
prompt: you like -> [[0,1]], shape [1,2]
logits: [1,2,5]
logits[:, 0, :]: what follows you?
logits[:, 1, :]: what follows like at the final position?
last_logits = logits[:, -1, :] # [1,5], used to choose this round's appended tokenScroll horizontally to view all columns.
| strategy | rule | result characteristic |
|---|---|---|
| Argmax | Always choose a highest-probability candidate | Repeatable for fixed inputs and model state, but potentially repetitive |
| Sampling | Sample according to Softmax probabilities | Higher-probability tokens occur more often; lower-probability tokens remain possible |
@torch.no_grad()
def generate(model, token_ids, max_new_tokens, temperature=1.0):
if temperature <= 0:
raise ValueError("temperature must be greater than 0")
model.eval()
for _ in range(max_new_tokens):
logits, _ = model(token_ids) # [B, T, V_vocab]
last_logits = logits[:, -1, :] # [B, V_vocab]
probabilities = F.softmax(
last_logits / temperature,
dim=-1,
) # [B, V_vocab]
next_id = torch.multinomial(
probabilities,
num_samples=1,
) # [B, 1]
token_ids = torch.cat(
(token_ids, next_id),
dim=1,
) # [B, T + 1]
return token_ids
# Greedy alternative:
# next_id = torch.argmax(last_logits, dim=-1, keepdim=True) # [B,1]For positive finite temperature τ, use softmax(last_logits / τ): τ = 1 preserves the distribution, τ < 1 sharpens it, and τ > 1 flattens it. Our teaching vocabulary has no end-of-sequence token, so max_new_tokens bounds the loop. This small generation fragment assumes a valid nonempty input and a nonnegative integer token count, and leaves the model in eval mode; later chapters introduce a stricter generation interface.
Knowledge check
Why should next_id have shape [B,1] rather than [B]?
16. The Bigram's key limitation: only the final token matters
prompt_a = torch.tensor([[0, 1]]) # you like
prompt_b = torch.tensor([[4, 1]]) # we like
logits_a, _ = model(prompt_a)
logits_b, _ = model(prompt_b)
last_a = logits_a[:, -1, :]
last_b = logits_b[:, -1, :]
assert torch.equal(last_a, last_b)Both prompts end in ID 1, so the Bigram retrieves W_bigram[1,:] for both. This is not an accidental training result: with a current-token-only lookup into the [V_vocab,V_vocab] table, the earlier you or we has no computational path to affect the final-position output.
Our fixed-vocabulary table has only 5×5 = 25 logits. It can learn one-step transitions, making it a useful teaching and data-pipeline baseline, but cannot represent different continuations for the same current token under different longer prefixes.
Scroll horizontally to view all columns.
| A Bigram sees only the final token | Targets among the six tasks | Ideal limiting probability allocation |
|---|---|---|
| you, occurring twice | like / study | 1/2 each |
| like, occurring twice | AI / you | 1/2 each |
| we, occurring once | like | Can approach 1 |
| study, occurring once | AI | Can approach 1 |
The ideal Bigram's mean-loss infimum on these six tasks is therefore 4×ln(2)/6 ≈ 0.462, not zero. Finite logits can only approach probability 1. The causal Mini GPT in Week 11 can distinguish [you,like] from [we,like], leaving only the two conflicting targets for the identical prefix [you]. Its ideal lower limit is consequently 2×ln(2)/6 ≈ 0.231. The difference comes from available context, not inconsistent arithmetic, and does not promise that a particular small model will train to either limit.
Knowledge check
Why must [you,like] and [we,like] produce identical final logits in the Bigram?
17. Perplexity: how well does the model predict the observed next tokens?
Scroll horizontally to view all columns.
| Probability assigned to each actual token | mean_NLL | PPL | Intuition |
|---|---|---|---|
| 1/5: uniform over five candidates | ln(5) ≈ 1.609 | 5 | Equivalent to the likelihood of a uniform five-candidate prediction |
| 1/2 | ln(2) ≈ 0.693 | 2 | Equivalent average likelihood to a uniform two-candidate prediction |
import math
@torch.no_grad()
def evaluate_perplexity(model, data_loader):
model.eval()
total_nll = 0.0
total_tokens = 0
for inputs, targets in data_loader:
logits, _ = model(inputs) # [B, T, V_vocab]
V_vocab = logits.size(-1)
nll_sum = F.cross_entropy(
logits.reshape(-1, V_vocab),
targets.reshape(-1),
reduction="sum",
)
total_nll += float(nll_sum)
total_tokens += targets.numel() # This example assumes no ignored/padded targets.
if total_tokens == 0:
raise ValueError("No tokens are available for evaluation")
return math.exp(total_nll / total_tokens)PPL = exp(mean token loss), the reciprocal of the geometric mean of the correct-token probabilities. For probabilities 0.5 and 0.125, mean loss is [ln(2)+ln(8)]/2 = ln(4), so PPL = 4. It is not the arithmetic mean of their reciprocals, (2+8)/2 = 5, nor does it mean the model literally considered four equally likely tokens. Different tokenizers change the units being scored, so perplexity alone cannot directly compare their language capability.
Knowledge check
When is perplexity = torch.exp(loss) appropriate?
18. Week 6 recap: one complete minimal language-model process
- Encode text with the tokenizer into raw IDs [3,3]
- Offset inputs and targets to obtain [3,2]
- General LM: input embeddings [3,2,4] → context processor / output head → logits [3,2,5]
- Bigram: IDs [3,2] → directly retrieve rows of a [5,5] score table → logits [3,2,5]
- Flatten into logits [6,5] and targets [6]
- F.cross_entropy returns a scalar mean loss
- Training: backward → optimizer update. Generation: select from the final position and append
Week 6 fixed batch:
raw IDs [3,3]
shifted inputs [3,2]
General language model path:
IDs [3,2]
input embedding [3,2,4]
context model + output head [3,2,5] vocabulary logits
Bigram shortcut path:
IDs [3,2]
direct W_bigram [5,5] lookup [3,2,5] vocabulary logits
flattened logits [6,5]
flattened targets [6]
mean cross-entropy loss scalar
Generation from prompt [[0,1]]:
prompt IDs [1,2]
model logits [1,2,5]
last logits[:, -1, :] [1,5]
next_id [1,1]
appended prompt [1,3]Scroll horizontally to view all columns.
| Training | Generation |
|---|---|
| Text → IDs → offset inputs/targets → [B,T,V] logits → flatten → cross-entropy → backward → update | prompt IDs → [B,T,V] logits → logits[:, -1, :] [B,V] → Argmax/Sampling → next_id [B,1] → append → repeat |
Knowledge check
Why do we keep logits, cross-entropy, and logits[:, -1, :] after adding attention in Week 7?
19. From Bigram to attention: let the current position read the prefix
Self-attention lets each position dynamically combine information from positions it may see. A query supplies matching features for the current position; a key supplies matching features for a candidate position; a value supplies the content that will actually be combined. These are typically different learned projections of the same hidden representations, not three extra tokens. The descriptions are useful intuitions, not predefined semantic labels on the coordinates.
Scroll horizontally to view all columns.
| quantity | typical shape | meaning |
|---|---|---|
| query, key | [B,T,d_k] | Used to match positions |
| value_states | [B,T,d_v] | The content combined using the weights |
| attention scores / weights | [B,T,T] | Scores / normalized weights for every query and key-position pair |
| context | [B,T,d_v] | Weighted values |
| final vocabulary logits | [B,T,V_vocab] | The same next-token interface as Week 6 |
import math
# query, key: [B, T, d_k]
# value_states: [B, T, d_v]
scores = query @ key.transpose(-2, -1) # [B, T, T]
scores = scores / math.sqrt(d_k)
# causal_mask broadcasts to [B,T,T]; True means a visible position.
scores = scores.masked_fill(~causal_mask, float("-inf"))
weights = F.softmax(scores, dim=-1) # [B, T, T]
context = weights @ value_states # [B, T, d_v]Prompts A = [you,like] and B = [we,like] have the same token at position 1, but attention can read their different position-0 inputs. Their final logits can therefore differ. This supplies a path for using longer context; it does not guarantee a particular answer before training or establish understanding on its own.
Knowledge check
How can attention give different final logits to two prompts that both end in like?