USAAIO Lesson 93, from Phase 3. It explains why bidirectional BERT is slow at inference and how each variant fixes a different limitation: XLNet's permutation language modeling, with two-stream attention and separate content and query masks; RoBERTa's dynamic masking, dropped NSP, and larger batches; T5's unified text-to-text seq2seq framing; DeBERTa's disentangled content and position attention; and ALBERT's cross-layer parameter sharing, whose six-fold reduction is verified in PyTorch at d=64. All the parameter counts, mask tables, and shape traces were verified with torch 2.7.1+cpu. The lesson runs to 31 slides.
Subject: Machine Learning · 56 slides · code lesson
Open the interactive version of this deck · Homework for this lesson
Title
USAAIO · Lesson 93 · Phase 3
Five papers, five distinct fixes to BERT's limitations: inference speed, training signal efficiency, task framing, position attention, and parameter count. Build each core mechanism in PyTorch and understand when to prefer which.
Objectives
Warm-up
Discussion prompt
Before we open Lesson 93: BERT Variants — XLNet, RoBERTa, T5, DeBERTa, ALBERT: without looking back, what was the main idea of Vision Transformer (ViT), and what could you do by the end of it that you could not do before?
Hint: One sentence for the idea, one for the skill. If the second one is blank, that is the part to revisit.
Answer:
patch embedding via nn.Conv2d (stride=P), CLS token, learned positional embeddings, standard transformer encoder applied to patch sequences, classification head on CLS token, fine-tuning vs scratch, and ViT vs CNN scaling behavior.
Section
Part 1 of 5
Concept
BERT (Lesson 90) is bidirectional: the [MASK] training objective lets every token attend to every other token, giving rich contextual representations. But this design has a hidden cost at inference time.
[MASK] tokens; inference never does — a distribution shift baked into the architecture| model | generation | bidirectional ctx | [MASK] artifact |
|---|---|---|---|
| GPT-2 (causal LM) | yes | no (left only) | none |
| BERT (MLM) | no | yes (full) | yes — train/test gap |
| XLNet (perm LM) | yes | yes (via perm) | none |
| T5 (seq2seq) | yes | encoder only | none |
Discrimination
Sort into buckets
Sort these by generation, from memory, without looking back at BERT's bidirectionality bottleneck. Telling them apart on the spot is the skill; the table is only where the answer happens to be written down.
Section
Part 2 of 5
Concept
XLNet trains an autoregressive LM (no [MASK]) but over all permutations of the token order. Predicting token at position t in a permutation means the model has seen all other positions already — capturing bidirectional context without the mask artifact.
\[ \max_\theta \; \mathbb{E}_{\mathbf{z}\sim\mathcal{Z}_T}\left[\sum_{t=1}^{T} \log p_\theta(x_{z_t}\mid x_{\mathbf{z}_{<t}})\right] \]
Here z is a permutation of positions 1..T. The expectation is over all permutations — the model must predict each token from the tokens that appear before it in the permutation order, not the original left-to-right order.
Estimation
Predict first
XLNet needs two attention streams because the model must both use position t's content (for other positions to attend to) and predict it without seeing itself. Standard self-attention cannot do both simultaneously.
Commit before you compute: what does Two-stream attention — content vs query masks come out to? A rough magnitude and the right form is enough — the point is to have something concrete to be wrong about.
Correct: permutation=[3,0,1,5,2,4]; ranks=[1,2,4,0,5,3]; target pos=3 is rank 0 (first in perm order)
Why: A prediction you can defend turns the computation into a check rather than a leap of faith — and an answer that contradicts it is caught on the spot. Position 3 appears first in this permutation, so in the content stream it can see itself (c_mask[3,3]=1).
Worked example
XLNet needs two attention streams because the model must both use position t's content (for other positions to attend to) and predict it without seeing itself. Standard self-attention cannot do both simultaneously.
import torch
def permutation_masks(seq_len, target_pos, seed=7):
torch.manual_seed(seed)
perm = torch.randperm(seq_len) # e.g. [3,0,1,5,2,4]
# rank of each position in the permutation
perm_rank = torch.zeros(seq_len, dtype=torch.long)
for rank, pos in enumerate(perm):
perm_rank[pos] = rank
# content mask: position i can attend j if rank(j) <= rank(i)
c_mask = (perm_rank.unsqueeze(0) <= perm_rank.unsqueeze(1)).float()
# query mask: same but target cannot attend to itself
q_mask = c_mask.clone()
q_mask[target_pos, target_pos] = 0.0
return perm, perm_rank, c_mask, q_mask
perm, ranks, cm, qm = permutation_masks(seq_len=6, target_pos=3)
print('permutation:', perm.tolist())
print('perm ranks: ', ranks.tolist())
print('target=3 is rank', ranks[3].item())
print('content mask row 3:', cm[3].int().tolist())
print('query mask row 3:', qm[3].int().tolist())permutation=[3,0,1,5,2,4]; ranks=[1,2,4,0,5,3]; target pos=3 is rank 0 (first in perm order)
Why: Position 3 appears first in this permutation, so in the content stream it can see itself (c_mask[3,3]=1). In the query stream that self-link is zeroed (q_mask[3,3]=0) so the model predicts position 3 without peeking at its own content.
| stream | self-attention (pos=3) | can see past context | used for |
|---|---|---|---|
| content stream | yes (c_mask[3,3]=1) | yes (rank < own rank) | key/value for other positions |
| query stream | no (q_mask[3,3]=0) | yes (rank < own rank) | predict x_3 from context |
Comparison
Comparison matrix
From Two-stream attention — content vs query masks: refill the can see past context column from what you know. The rest of the table is as it appeared.
| stream | self-attention (pos=3) | can see past context | used for |
|---|---|---|---|
| content stream | yes (c_mask[3,3]=1) | yes (rank < own rank) | key/value for other positions |
| query stream | no (q_mask[3,3]=0) | yes (rank < own rank) | predict x_3 from context |
Anomaly
Predict first
A student writes this, and it looks reasonable:
XLNet randomizes the token order, so the model receives tokens in the permuted sequence order and positional embeddings reflect the permuted positions.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: The token sequence itself stays in its original left-to-right order; only the attention mask changes.
Tokens remain in original order. The permutation is expressed entirely through the attention mask — which positions are allowed to attend to which.
Why: The token sequence itself stays in its original left-to-right order; only the attention mask changes. Positional embeddings reflect original positions, not permutation rank.
Trap
XLNet randomizes the token order, so the model receives tokens in the permuted sequence order and positional embeddings reflect the permuted positions.
Assign positional embedding i to the token at permutation position i
Why: Wrong. The token sequence itself stays in its original left-to-right order; only the attention mask changes. Positional embeddings reflect original positions, not permutation rank.
Tokens remain in original order. The permutation is expressed entirely through the attention mask — which positions are allowed to attend to which.
Keep tokens at their original positions; vary only the content/query masks per permutation
Why: This is critical: no shuffling of input tensors occurs. c_mask[i,j] = 1 means token i can attend token j in this permutation. The same embedding matrix and positional encodings are reused.
Section
Part 3 of 5
Concept
RoBERTa (Liu 2019) is BERT with better training — same architecture, three training changes that each demonstrably improve downstream performance.
Counterexample
Discussion prompt
RoBERTa (Liu 2019) is BERT with better training — same architecture, three training changes that each demonstrably improve downstream performance.
That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.
Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.
Missing information
Discussion prompt
Static masking (BERT): mask is computed once at preprocessing; every epoch trains on the same masked positions. Dynamic masking (RoBERTa): each forward pass draws a fresh mask at 15% independently.
What do you need to know — or decide — before the first line can be written? List everything the problem has to hand you.
Hint: Anything you would have to invent to get started is a thing the problem must supply.
Answer:
Over 40 epochs of RoBERTa training, dynamic masking means a 20-token sequence contributes ~(40 x 0.15 x 20) = 120 masked-token training events vs only (0.15 x 20) = 3 with static masking — 40x more diverse signal.
Worked example
Static masking (BERT): mask is computed once at preprocessing; every epoch trains on the same masked positions. Dynamic masking (RoBERTa): each forward pass draws a fresh mask at 15% independently.
import numpy as np
def static_mask(seq_len, mask_prob=0.15, seed=42):
rng = np.random.RandomState(seed)
return (rng.random(seq_len) < mask_prob).astype(int)
def dynamic_mask(seq_len, mask_prob=0.15, seed=None):
rng = np.random.RandomState(seed)
return (rng.random(seq_len) < mask_prob).astype(int)
seq_len = 20
static = static_mask(seq_len, seed=42)
print('static (same every epoch): ', static.tolist())
for ep, s in enumerate([42, 99, 7, 13], 1):
print(f'dynamic epoch {ep} (seed={s}): ', dynamic_mask(seq_len, seed=s).tolist())Static: positions [6,10] masked every epoch. Dynamic: epoch 1=[6,10], epoch 2=[1,9,12,17], epoch 3=[0,2,16,18], epoch 4=[7,8,13]
Why: Over 40 epochs of RoBERTa training, dynamic masking means a 20-token sequence contributes ~(40 x 0.15 x 20) = 120 masked-token training events vs only (0.15 x 20) = 3 with static masking — 40x more diverse signal.
| masking strategy | positions masked | diversity per sequence | epochs to match |
|---|---|---|---|
| static (BERT) | fixed at preprocessing | 1 pattern total | 40 epochs = same pattern x40 |
| dynamic (RoBERTa) | fresh each epoch | 40 distinct patterns | 40 epochs = 40 distinct |
Trade off
Comparison matrix
From Dynamic masking in code: every row here is a choice with a cost. Fill the positions masked column, then say which row you would actually pick and what you give up for it.
| masking strategy | positions masked | diversity per sequence | epochs to match |
|---|---|---|---|
| static (BERT) | fixed at preprocessing | 1 pattern total | 40 epochs = same pattern x40 |
| dynamic (RoBERTa) | fresh each epoch | 40 distinct patterns | 40 epochs = 40 distinct |
Section
Part 4 of 5
Concept
T5 (Raffel 2020) uses a single encoder-decoder for every task. The task type is specified by a text prefix in the input; the model always generates a text string as output. There is no task-specific head architecture.
| task | input prefix + text | target output |
|---|---|---|
| sentiment | sst2 sentence: This movie is great. | positive |
| translation | translate English to German: The house... | Das Haus... |
| summarization | summarize: Scientists discover... | <summary> |
| QA | question: Who leads the team? context: ... | Jane Smith |
| STS (regress) | stsb sentence1: ... sentence2: ... | 3.8 |
Even regression (similarity score 0-5) is cast as text generation: the model outputs the digit string "3.8". This unification lets T5 train on all tasks simultaneously with a single cross-entropy loss over the output vocabulary.
Estimation
Predict first
Trace a minimal T5 encoder-decoder with vocab=32, d=64, 2 encoder layers, 2 decoder layers. Input: src tokens (batch=2, len=8). Target: tgt tokens (batch=2, len=3). Predict the logit shape.
Commit before you compute: what does T5-style encoder-decoder forward pass come out to? A rough magnitude and the right form is enough — the point is to have something concrete to be wrong about.
Correct: enc_out=(2,8,64); dec_out=(2,3,64); logits=(2,3,32)
Why: A prediction you can defend turns the computation into a check rather than a leap of faith — and an answer that contradicts it is caught on the spot. The encoder reads the full source in parallel (bidirectional).
Worked example
Trace a minimal T5 encoder-decoder with vocab=32, d=64, 2 encoder layers, 2 decoder layers. Input: src tokens (batch=2, len=8). Target: tgt tokens (batch=2, len=3). Predict the logit shape.
import torch, torch.nn as nn
torch.manual_seed(42)
VOCAB, D, N_HEADS = 32, 64, 4
enc_layer = nn.TransformerEncoderLayer(D, N_HEADS, dim_feedforward=D*4, batch_first=True)
encoder = nn.TransformerEncoder(enc_layer, num_layers=2)
dec_layer = nn.TransformerDecoderLayer(D, N_HEADS, dim_feedforward=D*4, batch_first=True)
decoder = nn.TransformerDecoder(dec_layer, num_layers=2)
embed = nn.Embedding(VOCAB, D)
out_proj = nn.Linear(D, VOCAB)
src = torch.randint(0, VOCAB, (2, 8))
tgt = torch.randint(0, VOCAB, (2, 3))
src_e = embed(src) # (2, 8, 64)
tgt_e = embed(tgt) # (2, 3, 64)
enc_out = encoder(src_e) # (2, 8, 64)
# causal mask: decoder can only see left context
causal = nn.Transformer.generate_square_subsequent_mask(3)
dec_out = decoder(tgt_e, enc_out, tgt_mask=causal) # (2, 3, 64)
logits = out_proj(dec_out) # (2, 3, 32)
print('enc_out:', enc_out.shape)
print('dec_out:', dec_out.shape)
print('logits: ', logits.shape)enc_out=(2,8,64); dec_out=(2,3,64); logits=(2,3,32)
Why: The encoder reads the full source in parallel (bidirectional). The decoder applies cross-attention to enc_out at every step while generating left-to-right under a causal mask — same dual-stream as the original Transformer (Lesson 88).
| tensor | shape | what it represents |
|---|---|---|
| src tokens | (2, 8) | batch=2, src len=8 |
| enc_out | (2, 8, 64) | encoder hidden state for each src token |
| tgt tokens | (2, 3) | batch=2, tgt len=3 (teacher forced) |
| dec_out | (2, 3, 64) | decoder hidden state per tgt position |
| logits | (2, 3, 32) | vocab distribution at each generated step |
Reverse engineer
Discussion prompt
Work backwards. The example finished here:
enc_out=(2,8,64); dec_out=(2,3,64); logits=(2,3,32)
What was it asked to do, and what must it have been given? Reconstruct the problem from its answer.
Hint: Every quantity in the result had to enter somewhere. Account for each one.
Answer:
Trace a minimal T5 encoder-decoder with vocab=32, d=64, 2 encoder layers, 2 decoder layers. Input: src tokens (batch=2, len=8). Target: tgt tokens (batch=2, len=3). Predict the logit shape.
Section
Part 5 of 5
Concept
Standard BERT attention fuses content and position into a single query/key vector before computing dot products. DeBERTa disentangles them: it computes three separate score terms and sums them.
\[ A_{i,j} = \underbrace{\mathbf{Q}_i^c \cdot \mathbf{K}_j^c}_{\text{content-content}} + \underbrace{\mathbf{Q}_i^c \cdot \mathbf{K}_{\delta(i,j)}^r}_{\text{content-position}} + \underbrace{\mathbf{Q}_{\delta(i,j)}^r \cdot \mathbf{K}_j^c}_{\text{position-content}} \]
Here delta(i,j) is the relative distance between positions i and j (clipped to a max range). The relative position embedding K^r is shared across all layers. This lets the model independently learn what (content) and where (position) relationships.
Analogy
Discussion prompt
Explain DeBERTa: disentangled content + position attention by analogy to something with no Machine Learning in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.
Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.
Answer:
Standard BERT attention fuses content and position into a single query/key vector before computing dot products. DeBERTa disentangles them: it computes three separate score terms and sums them.
Concept
ALBERT reduces parameter count by sharing the same weight tensor across every encoder layer. Instead of L distinct layers, one layer's weights are applied L times. The model's computational cost (FLOPs) stays the same; only memory drops.
E=128, projected to hidden dim H=768 — reduces embedding params from V·H to V·E + E·HEstimation
Predict first
Implement ALBERT-style sharing: one TransformerLayer applied 6 times vs 6 distinct layers. Verify the 6x parameter reduction. Use d=64, n_heads=4.
Commit before you compute: what does ALBERT cross-layer sharing in PyTorch come out to? A rough magnitude and the right form is enough — the point is to have something concrete to be wrong about.
Correct: Standard: 299,904 params. ALBERT-style: 49,984 params. Reduction: 6x. Output shape: (2, 10, 64) — identical.
Why: A prediction you can defend turns the computation into a check rather than a leap of faith — and an answer that contradicts it is caught on the spot. Sharing one layer across 6 passes uses 1/6 the parameters.
Worked example
Implement ALBERT-style sharing: one TransformerLayer applied 6 times vs 6 distinct layers. Verify the 6x parameter reduction. Use d=64, n_heads=4.
import torch, torch.nn as nn
torch.manual_seed(42)
class TransformerLayer(nn.Module):
def __init__(self, d=64, n_heads=4):
super().__init__()
self.norm1 = nn.LayerNorm(d)
self.attn = nn.MultiheadAttention(d, n_heads, batch_first=True)
self.norm2 = nn.LayerNorm(d)
self.mlp = nn.Sequential(
nn.Linear(d, d*4), nn.GELU(), nn.Linear(d*4, d)
)
def forward(self, x):
a, _ = self.attn(self.norm1(x), self.norm1(x), self.norm1(x))
x = x + a
return x + self.mlp(self.norm2(x))
# Standard 6-layer encoder (6 distinct sets of weights)
class StandardEncoder(nn.Module):
def __init__(self, n_layers=6):
super().__init__()
self.layers = nn.ModuleList([TransformerLayer() for _ in range(n_layers)])
def forward(self, x):
for layer in self.layers: x = layer(x)
return x
# ALBERT-style: one shared layer applied 6 times
class ALBERTEncoder(nn.Module):
def __init__(self, n_layers=6):
super().__init__()
self.shared = TransformerLayer()
self.n_layers = n_layers
def forward(self, x):
for _ in range(self.n_layers): x = self.shared(x)
return x
std = StandardEncoder()
alb = ALBERTEncoder()
std_p = sum(p.numel() for p in std.parameters())
alb_p = sum(p.numel() for p in alb.parameters())
print(f'Standard 6-layer: {std_p} params')
print(f'ALBERT 6-shared: {alb_p} params')
print(f'Reduction: {std_p // alb_p}x')
x = torch.randn(2, 10, 64)
print('Output shape:', alb(x).shape)Standard: 299,904 params. ALBERT-style: 49,984 params. Reduction: 6x. Output shape: (2, 10, 64) — identical.
Why: Sharing one layer across 6 passes uses 1/6 the parameters. The forward-pass cost is the same (6 matrix multiplies either way) but memory and storage shrink 6x. BERT-base-scale: 85M encoder params reduced to ~7M with full sharing.
| configuration | params (d=64, 6L) | params (d=768, 12L BERT-scale) | output shape |
|---|---|---|---|
| standard (distinct layers) | 299,904 | 85,054,464 | (2,10,64) |
| ALBERT (shared layer x6/12) | 49,984 | 7,087,872 | (2,10,64) |
| reduction factor | 6x | 12x | same |
Comparison
Comparison matrix
From ALBERT cross-layer sharing in PyTorch: refill the output shape column from what you know. The rest of the table is as it appeared.
| configuration | params (d=64, 6L) | params (d=768, 12L BERT-scale) | output shape |
|---|---|---|---|
| standard (distinct layers) | 299,904 | 85,054,464 | (2,10,64) |
| ALBERT (shared layer x6/12) | 49,984 | 7,087,872 | (2,10,64) |
| reduction factor | 6x | 12x | same |
Anomaly
Predict first
A student writes this, and it looks reasonable:
ALBERT shares weights → 6x fewer parameters → 6x faster inference. Smaller model = proportionally faster forward pass.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: ALBERT still executes 6 forward passes through the shared layer — the same number of matrix multiplications.
ALBERT reduces parameter count and storage, not compute. The shared layer still runs once per layer — the FLOPs are the same as the standard model.
Why: ALBERT still executes 6 forward passes through the shared layer — the same number of matrix multiplications. FLOPs are nearly identical; only memory and checkpoint size shrink.
Trap
ALBERT shares weights → 6x fewer parameters → 6x faster inference. Smaller model = proportionally faster forward pass.
Expect ALBERT inference to be 6x faster than a standard 6-layer model
Why: Wrong. ALBERT still executes 6 forward passes through the shared layer — the same number of matrix multiplications. FLOPs are nearly identical; only memory and checkpoint size shrink.
ALBERT reduces parameter count and storage, not compute. The shared layer still runs once per layer — the FLOPs are the same as the standard model.
ALBERT benefit: smaller checkpoint, less memory, fewer gradients to store. No speedup at inference.
Why: This is a critical distinction on the USAAIO exam: parameter count and FLOPs are different quantities. A model can have 6x fewer params and still be equally slow at inference.
Break the constraint
Discussion prompt
The rule this trap just fixed:
ALBERT reduces parameter count and storage, not compute. The shared layer still runs once per layer — the FLOPs are the same as the standard model.
Now break it on purpose. Build a case that violates it and follow the consequences until something visibly fails. Where does the failure first show up — and would you have noticed it if you had not been looking?
Hint: The dangerous rules are the ones whose violation still produces an answer. If yours fails loudly, try to find one that fails quietly.
Answer:
ALBERT still executes 6 forward passes through the shared layer — the same number of matrix multiplications. FLOPs are nearly identical; only memory and checkpoint size shrink.
Constraint
Discussion prompt
Run The BERT-variant decision map with this step confiscated:
Better position-content interaction? DeBERTa: disentangled Q_c·K_c + Q_c·K_r + Q_r·K_c relative attention.
Is it still possible? If it is, say what takes its place and what it costs you. If it is not, say exactly what that step was providing that nothing else does.
Hint: A step you can drop for free was never load-bearing. If you cannot drop it, name the thing that goes wrong the moment it is gone.
Answer:
Pattern
Edge cases
Discussion prompt
The BERT-variant decision map works on the cases you have just seen. Push it to the edge: what is the most degenerate input it still handles — empty, zero, one item, everything equal — and what is the first case where it stops being true? Name the case, not just "it breaks".
Hint: Try the smallest legal input, then the largest, then the one where two things collide. Methods are specified at their edges; the middle takes care of itself.
Answer:
Elimination
Eliminate the wrong options
In XLNet's two-stream attention, why does the query stream mask out position t's own content when predicting x_t?
3 of these 4 are wrong. Strike them one at a time, and say what rules each one out before you strike the next. The survivor is the answer.
Survives elimination: A
Why: The query stream is responsible for predicting x_t. If it could attend to x_t's own content embedding, it would see the answer during training — information leakage. Zeroing q_mask[t,t] forces the model to predict from context only, exactly like BERT's [MASK] objective but without the [MASK] token.
Check
Work through the logic before clicking.
Check your understanding
In XLNet's two-stream attention, why does the query stream mask out position t's own content when predicting x_t?
Answer: A
Why: The query stream is responsible for predicting x_t. If it could attend to x_t's own content embedding, it would see the answer during training — information leakage. Zeroing q_mask[t,t] forces the model to predict from context only, exactly like BERT's [MASK] objective but without the [MASK] token.
Prediction
Predict first
Which of the following is NOT a change RoBERTa makes relative to BERT?
Answer it in your own words, now, with nothing to choose from. The options are on the next slide — and picking the right one off a list is an easier skill than producing it.
Correct: Replacing the MLM objective with a permutation language model
Why: RoBERTa keeps MLM as its pretraining objective — it just removes NSP, switches to dynamic masking, and scales up batch size and data. Replacing MLM with a permutation LM is XLNet's contribution, not RoBERTa's.
Check
Identify the exact change.
Check your understanding
Which of the following is NOT a change RoBERTa makes relative to BERT?
Answer: A
Why: RoBERTa keeps MLM as its pretraining objective — it just removes NSP, switches to dynamic masking, and scales up batch size and data. Replacing MLM with a permutation LM is XLNet's contribution, not RoBERTa's.
Elimination
Eliminate the wrong options
ALBERT-base uses cross-layer parameter sharing across its 12 encoder layers. Compared to BERT-base (12 distinct layers), ALBERT-base has approximately:
3 of these 4 are wrong. Strike them one at a time, and say what rules each one out before you strike the next. The survivor is the answer.
Survives elimination: A
Why: ALBERT shares one layer's weights across all 12 layers: 7,087,872 encoder params vs BERT's 85,054,464 (verified in PyTorch, ~12x). But it still applies the shared layer 12 times — same FLOPs as BERT. Inference speed is unchanged; only storage and memory shrink.
Check
Apply the FLOPs vs params distinction.
Check your understanding
ALBERT-base uses cross-layer parameter sharing across its 12 encoder layers. Compared to BERT-base (12 distinct layers), ALBERT-base has approximately:
Answer: A
Why: ALBERT shares one layer's weights across all 12 layers: 7,087,872 encoder params vs BERT's 85,054,464 (verified in PyTorch, ~12x). But it still applies the shared layer 12 times — same FLOPs as BERT. Inference speed is unchanged; only storage and memory shrink.
Section
Project
Concept
Three milestones: (1) implement ALBERT-style cross-layer sharing and verify the param reduction; (2) wire up a minimal T5 encoder-decoder and trace shapes; (3) write a dynamic masking function and compare masking diversity.
| # | milestone | target output |
|---|---|---|
| 1 | ALBERTEncoder vs StandardEncoder, n_layers=6, d=64 | 299,904 vs 49,984 params (6x) |
| 2 | T5EncoderDecoder: src=(2,8), tgt=(2,3), vocab=32, d=64 | logits shape (2,3,32) |
| 3 | dynamic_mask() called 10 times on len-20 seq, mask_prob=0.15 | 10 distinct masked-position lists |
Build rules: use torch.manual_seed(42) for reproducibility; print shapes after every operation; count params with sum(p.numel() for p in model.parameters()) not model.parameters().__len__().
Counterexample
Discussion prompt
Build rules: use torch.manual_seed(42) for reproducibility; print shapes after every operation; count params with sum(p.numel() for p in model.parameters()) not model.parameters().__len__().
That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.
Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.
Worked example
Your turn: build StandardEncoder (6 distinct TransformerLayer) and ALBERTEncoder (one shared layer applied 6 times). Predict the ratio before running.
Hint: nn.ModuleList registers multiple distinct layers; assigning one layer to self.shared and calling it N times in forward shares weights. Use sum(p.numel() for p in m.parameters()).
import torch, torch.nn as nn
torch.manual_seed(42)
class TL(nn.Module):
def __init__(self, d=64, h=4):
super().__init__()
self.n1=nn.LayerNorm(d); self.n2=nn.LayerNorm(d)
self.attn=nn.MultiheadAttention(d, h, batch_first=True)
self.mlp=nn.Sequential(nn.Linear(d,d*4), nn.GELU(), nn.Linear(d*4,d))
def forward(self, x):
a,_=self.attn(self.n1(x),self.n1(x),self.n1(x))
return x+a+self.mlp(self.n2(x+a))
class Std(nn.Module):
def __init__(self):
super().__init__()
self.layers=nn.ModuleList([TL() for _ in range(6)])
def forward(self, x):
for l in self.layers: x=l(x)
return x
class Alb(nn.Module):
def __init__(self):
super().__init__()
self.shared=TL()
def forward(self, x):
for _ in range(6): x=self.shared(x)
return x
std_p=sum(p.numel() for p in Std().parameters())
alb_p=sum(p.numel() for p in Alb().parameters())
print(f'Standard: {std_p} ALBERT: {alb_p} ratio: {std_p//alb_p}x')| model | params | ratio |
|---|---|---|
| StandardEncoder (6 layers) | 299,904 | 6x |
| ALBERTEncoder (1 shared x6) | 49,984 | 1x (baseline) |
Worked example
Your turn: build a minimal encoder-decoder (2 enc layers, 2 dec layers, d=64). Feed src=(2,8) and tgt=(2,3). Trace every tensor shape. Predict the logit shape before running.
Hint: the decoder needs a causal mask (nn.Transformer.generate_square_subsequent_mask(tgt_len)). Cross-attention memory comes from enc_out. The output projection maps from d to vocab_size.
import torch, torch.nn as nn
torch.manual_seed(42)
V, D, H = 32, 64, 4
enc = nn.TransformerEncoder(nn.TransformerEncoderLayer(D,H,D*4,batch_first=True), 2)
dec = nn.TransformerDecoder(nn.TransformerDecoderLayer(D,H,D*4,batch_first=True), 2)
emb = nn.Embedding(V, D)
proj = nn.Linear(D, V)
src = torch.randint(0, V, (2, 8))
tgt = torch.randint(0, V, (2, 3))
enc_out = enc(emb(src))
cm = nn.Transformer.generate_square_subsequent_mask(3)
dec_out = dec(emb(tgt), enc_out, tgt_mask=cm)
logits = proj(dec_out)
print('enc_out:', enc_out.shape, ' dec_out:', dec_out.shape, ' logits:', logits.shape)| tensor | shape | note |
|---|---|---|
| src embedded | (2, 8, 64) | batch=2, src_len=8 |
| enc_out | (2, 8, 64) | full source context |
| tgt embedded | (2, 3, 64) | batch=2, tgt_len=3 |
| dec_out | (2, 3, 64) | causal + cross-attn applied |
| logits | (2, 3, 32) | vocab distribution per step |
Trade off
Comparison matrix
From Milestone 2 — T5 encoder-decoder shapes: every row here is a choice with a cost. Fill the note column, then say which row you would actually pick and what you give up for it.
| tensor | shape | note |
|---|---|---|
| src embedded | (2, 8, 64) | batch=2, src_len=8 |
| enc_out | (2, 8, 64) | full source context |
| tgt embedded | (2, 3, 64) | batch=2, tgt_len=3 |
| dec_out | (2, 3, 64) | causal + cross-attn applied |
| logits | (2, 3, 32) | vocab distribution per step |
Worked example
Your turn: write dynamic_mask(seq_len, mask_prob, seed) and call it 4 times with different seeds on a 20-token sequence. Predict whether any two masks will be identical.
Hint: use np.random.RandomState(seed) so each call is reproducible but distinct. Compare static (one mask forever) vs dynamic (new mask each epoch).
import numpy as np
def dynamic_mask(seq_len=20, mask_prob=0.15, seed=None):
rng = np.random.RandomState(seed)
masked = np.where(rng.random(seq_len) < mask_prob)[0].tolist()
return masked
static = dynamic_mask(seed=42)
print('static (fixed):', static)
for ep, s in enumerate([42, 99, 7, 13], 1):
dm = dynamic_mask(seed=s)
print(f'epoch {ep} (seed={s}): {dm}')| epoch / strategy | masked positions | unique? |
|---|---|---|
| static (seed=42) | [6, 10] | same every epoch |
| epoch 1 (seed=42) | [6, 10] | identical to static |
| epoch 2 (seed=99) | [1, 9, 12, 17] | distinct |
| epoch 3 (seed=7) | [0, 2, 16, 18] | distinct |
| epoch 4 (seed=13) | [7, 8, 13] | distinct |
Discrimination
Sort into buckets
Sort these by unique?, from memory, without looking back at Milestone 3 — dynamic masking diversity. Telling them apart on the spot is the skill; the table is only where the answer happens to be written down.
Concept
import torch, torch.nn as nn, numpy as np
torch.manual_seed(42)
# ---- ALBERT cross-layer sharing ----
class TL(nn.Module):
def __init__(self, d=64, h=4):
super().__init__()
self.n1=nn.LayerNorm(d); self.n2=nn.LayerNorm(d)
self.attn=nn.MultiheadAttention(d,h,batch_first=True)
self.mlp=nn.Sequential(nn.Linear(d,d*4),nn.GELU(),nn.Linear(d*4,d))
def forward(self,x):
a,_=self.attn(self.n1(x),self.n1(x),self.n1(x))
return x+a+self.mlp(self.n2(x+a))
class Std(nn.Module):
def __init__(self): super().__init__(); self.ls=nn.ModuleList([TL() for _ in range(6)])
def forward(self,x):
for l in self.ls: x=l(x)
return x
class Alb(nn.Module):
def __init__(self): super().__init__(); self.s=TL()
def forward(self,x):
for _ in range(6): x=self.s(x)
return x
std_p=sum(p.numel() for p in Std().parameters())
alb_p=sum(p.numel() for p in Alb().parameters())
print(f'Standard: {std_p} ALBERT: {alb_p} ({std_p//alb_p}x reduction)')
# ---- T5 encoder-decoder ----
V,D,H=32,64,4
enc=nn.TransformerEncoder(nn.TransformerEncoderLayer(D,H,D*4,batch_first=True),2)
dec=nn.TransformerDecoder(nn.TransformerDecoderLayer(D,H,D*4,batch_first=True),2)
emb=nn.Embedding(V,D); proj=nn.Linear(D,V)
src=torch.randint(0,V,(2,8)); tgt=torch.randint(0,V,(2,3))
enc_out=enc(emb(src))
cm=nn.Transformer.generate_square_subsequent_mask(3)
logits=proj(dec(emb(tgt),enc_out,tgt_mask=cm))
print('T5 logits:', logits.shape)
# ---- dynamic masking ----
def dyn(n=20,p=0.15,s=None):
rng=np.random.RandomState(s)
return np.where(rng.random(n)<p)[0].tolist()
for ep,s in enumerate([42,99,7,13],1):
print(f'epoch {ep}: {dyn(seed=s)}')| component | verified output |
|---|---|
| StandardEncoder params | 299,904 |
| ALBERTEncoder params | 49,984 (6x reduction) |
| T5 logits shape | (2, 3, 32) |
| Dynamic masks | 4 distinct token sets across epochs |
Comparison
Comparison matrix
From The full program: refill the verified output column from what you know. The rest of the table is as it appeared.
| component | verified output |
|---|---|
| StandardEncoder params | 299,904 |
| ALBERTEncoder params | 49,984 (6x reduction) |
| T5 logits shape | (2, 3, 32) |
| Dynamic masks | 4 distinct token sets across epochs |
Concept
Out loud, slides closed: (1) explain the XLNet content mask vs query mask distinction and why the target's self-link is zeroed; (2) list RoBERTa's three changes to BERT training; (3) explain why ALBERT's parameter reduction does not translate to faster inference.
Stretch (homework from the lesson plan): load T5 from HuggingFace (from transformers import T5ForConditionalGeneration) and use it for QA as seq2seq. Compare its cross-attention patterns with a BERT-based extractive QA model. Implement ALBERT-style embedding factorization: nn.Embedding(vocab, E=128) followed by nn.Linear(128, 768) and measure the param savings vs nn.Embedding(vocab, 768) for vocab=30,000.
Connect it up
Draw it
One page, no notation unless you need it: draw how these connect — BERT's limitation and why it matters · XLNet — permutation language model · RoBERTa — removing BERT's training shortcuts · T5 — text-to-text unification · DeBERTa & ALBERT — attention and size · Your turn: ALBERT sharing + T5 framing. Put an arrow wherever one of them is what makes another possible, and label the arrow with why.
Recap
| variant | key fix | architecture change |
|---|---|---|
| XLNet | no [MASK] + bidirectional ctx | permutation LM + two-stream attention |
| RoBERTa | better training signal | dynamic mask, no NSP, 8k batch |
| T5 | unified generation | encoder-decoder, text prefix framing |
| DeBERTa | richer position modeling | disentangled Q_c/K_c + Q_c/K_r + Q_r/K_c |
| ALBERT | parameter efficiency | shared layer weights + embedding factorization |
Want this taught 1-on-1? Alexander tutors Machine Learning — $55/session, free consultation.