§3.5The Classification Head & The Decoder

Part I NLPT pp. 19–21 · ~5 min read

  • classification head
  • body
  • cls token
  • masked self-attention
  • causal mask
  • encoder-decoder attention

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 (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] token — and run it through a dropout and a linear layer:

encoder hidden states (one per token):[CLS]timeflieslikearrowselected ↓DropoutLinear (768 → 3)3 class logits
The classification head: select the [CLS] hidden state, then dropout + a linear layer produce one logit per class.
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:

decoder block× 6
x (same shape)
x (shifted output)

Click a sublayer to see what it does. Data flows bottom → top; the output has the same shape as the input.

Figure 3-7 (recreated). The decoder block — three sublayers instead of the encoder’s two.

Masked self-attention — no peeking ahead

The decoder generates one token at a time, so at position ii it must only see positions i\le i. If it could see the future, it would “cheat” during training by copying the answer. Masked self-attention enforces this with a causal mask : 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 -\infty before the softmax. Since e=0e^{-\infty} = 0, 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) →
timeflieslikeanarrow
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.

Causal (masked) self-attention: each token attends only to itself and earlier tokens. Notice “flies” can no longer reach the future token “arrow”.

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 . 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?

6 questions