We’ve built the entire encoder. Two things remain to round out the architecture: how to turn the encoder’s per-token outputs into a single prediction (a head), and what makes the decoder different.
Adding a classification head
Transformer models split cleanly into a task-independent body body The task-independent transformer trunk that produces hidden states, to which different task-specific heads are attached. defined in ch. 3 — open in glossary (everything we built in §3.1–3.4) and a task-specific head. The encoder gives us one hidden state per token, but a text classifier needs one prediction. The convention is to take the first token — the [CLS] cls token A special token prepended to the input whose final hidden state is conventionally used to represent the whole sequence for classification. defined in ch. 3 — open in glossary token — and run it through a dropout and a linear layer:
class TransformerForSequenceClassification(nn.Module):
def __init__(self, config):
super().__init__()
self.encoder = TransformerEncoder(config)
self.dropout = nn.Dropout(config.hidden_dropout_prob)
self.classifier = nn.Linear(config.hidden_size, config.num_labels)
def forward(self, x):
x = self.encoder(x)[:, 0, :] # take the [CLS] token's hidden state
x = self.dropout(x)
x = self.classifier(x)
return x
config.num_labels = 3
encoder_classifier = TransformerForSequenceClassification(config)
encoder_classifier(inputs.input_ids).size() # torch.Size([1, 3])
For each example we now get unnormalized logits over the three classes — exactly the shape a classifier needs. Swap the head, keep the body, and the same transformer serves masked-language-modeling, token classification, or the question-answering reader of Chapter 7.
The decoder
The decoder looks like the encoder but has two attention sublayers instead of one. Click through the block:
Click a sublayer to see what it does. Data flows bottom → top; the output has the same shape as the input.
Masked self-attention — no peeking ahead
The decoder generates one token at a time, so at position it must only see positions . If it could see the future, it would “cheat” during training by copying the answer. Masked self-attention masked self-attention Decoder self-attention with a causal mask so each position attends only to itself and earlier positions, preventing it from seeing future tokens. defined in ch. 3 — open in glossary enforces this with a causal mask causal mask A lower-triangular mask that sets attention scores to future tokens to −∞, so softmax gives them zero weight. defined in ch. 3 — open in glossary : a lower-triangular matrix of ones.
seq_len = inputs.input_ids.size(-1)
mask = torch.tril(torch.ones(seq_len, seq_len)).unsqueeze(0)
mask[0]
# tensor([[1., 0., 0., 0., 0.],
# [1., 1., 0., 0., 0.],
# [1., 1., 1., 0., 0.],
# [1., 1., 1., 1., 0.],
# [1., 1., 1., 1., 1.]])
Wherever the mask is zero (the future), we set the score to before the softmax. Since , those positions get exactly zero weight:
scores.masked_fill(mask == 0, -float("inf"))
# tensor([[[26.8082, -inf, -inf, -inf, -inf],
# [-0.6981, 26.9043, -inf, -inf, -inf],
# [-2.3190, 1.2928, 27.8710, -inf, -inf],
# [-0.5897, 0.3497, -0.3807, 27.5488, -inf],
# [ 0.5275, 2.0493, -0.4869, 1.6100, 29.0893]]])
Here is the masked attention on our running sentence. The upper triangle is greyed out — every token attends only backward:
causal attention pattern
| key (attended to) → | |||||
| time | flies | like | an | arrow | |
| query↓time | 1.00 | · | · | · | · |
| flies | .95 | · | · | · | |
| like | .11 | .11 | .79 | · | · |
| an | .06 | .06 | .44 | .44 | · |
| arrow | .10 | .27 | .10 | .10 | .44 |
Each row is one query token's attention distribution over all keys — it sums to 1. Darker = more attention. Hover a cell for the exact weight.
Folding masking into the §3.2 attention function is a one-line change:
def scaled_dot_product_attention(query, key, value, mask=None):
dim_k = query.size(-1)
scores = torch.bmm(query, key.transpose(1, 2)) / sqrt(dim_k)
if mask is not None:
scores = scores.masked_fill(mask == 0, float("-inf"))
weights = F.softmax(scores, dim=-1)
return weights.bmm(value)
Encoder–decoder (cross) attention
The second sublayer is encoder–decoder attention encoder-decoder attention Cross-attention in the decoder where the queries come from the decoder while the keys and values come from the encoder's output. defined in ch. 3 — open in glossary . It runs multi-head attention where the queries come from the decoder but the keys and values come from the encoder’s output. This is how information crosses from the source sequence into the generated one.
Analogy — cheating on the exam
You (the decoder) must predict the next word but only have the words so far. Your neighbour (the encoder) has the full source text — in a language you can’t read. So you sketch a cartoon of what you have (the query); your neighbour finds the matching passage (the key), sketches the word that follows (the value), and passes it back. With that system you ace the exam.Note — cross-attention scores are rectangular
Unlike self-attention, the decoder’s queries and the encoder’s keys can have different lengths (source and target sentences usually differ), so the cross-attention score matrix is rectangular, not square.That completes every moving part of the Transformer. Next we step back and survey the family of models that grew from this architecture.
Check yourself: heads and the decoder
1.How does a sequence classifier turn per-token hidden states into a single prediction?
2.With config.num_labels = 3, what is the output shape of the classifier for one input example?
3.Why does the decoder use masked self-attention?
4.How is the causal mask actually applied to the scores?
5.In encoder–decoder (cross) attention, where do the queries, keys, and values come from?
6.Why can the cross-attention score matrix be rectangular rather than square?