Code Architecture — 077 ALBERT
Overview
This notebook implements a from-scratch BERT-style Transformer encoder and introduces the two key ALBERT parameter-reduction techniques:
- Standard BERT — full embedding matrix (V×H), separate parameters per layer, NSP loss
- ALBERT — factorized embedding (V×E + E×H), shared parameters across all layers, SOP loss
Both models are pretrained with Masked Language Modeling (MLM) on the same toy corpus. The notebook compares total parameter counts and pretraining loss curves to demonstrate that ALBERT achieves comparable or better loss with dramatically fewer parameters — the core finding of the paper.
Models are intentionally small (4 Transformer layers, 128 hidden dim, 4 attention heads, ~500 vocabulary, embedding dim 64) to run within Kaggle’s 30-minute GPU limit while demonstrating all core ALBERT mechanisms.
Section-by-Section Breakdown
1. Imports & Configuration
- Imports:
torch,torch.nn,torch.nn.functional,math,random,collections,matplotlib.pyplot,numpy - Hyperparameters:
d_model=128,n_heads=4,n_layers=4,vocab_size(built from corpus),max_len=64,d_embedding=64(ALBERT factorized embedding dim),dropout=0.1 - BERT config: separate parameters per layer, full V×H embedding, NSP loss
- ALBERT config: shared parameters across layers, factorized V×E + E×H embedding, SOP loss
- Both:
lr=5e-4,epochs=20,batch_size=32
2. Toy Corpus Construction
- A small collection of English sentences organised into short paragraph documents (2-4 sentences each)
- For NSP training (BERT): sentence pairs (A, B) where B is either the actual next sentence or a random sentence from the corpus
- For SOP training (ALBERT): sentence pairs (A, B) where B is either the actual next sentence or the same pair swapped (B, A)
- Word-level tokenizer (split on whitespace, lowercase) — deliberately simpler than WordPiece/BPE for clarity
3. Vocabulary Building
- Build vocabulary from all words in the toy corpus
- Special tokens:
[PAD]=0,[UNK]=1,[CLS]=2,[SEP]=3,[MASK]=4 - Word-to-id and id-to-word mappings (~500 tokens)
4. Masking Function (MLM)
- Shared by both BERT and ALBERT — identical MLM masking
- Select 15% of tokens: 80% →
[MASK], 10% → random token, 10% → unchanged - MLM labels: original token at masked positions, -100 elsewhere (PyTorch convention)
- Returns: masked_input_ids, mlm_labels
5. Dataset Construction
5a. BERT Dataset (with NSP)
- Each example:
[CLS] A [SEP] B [SEP]with segment IDs (0 for A, 1 for B) - NSP label: 1 if B follows A in the corpus, 0 if random
- MLM labels computed via masking function
- Returns: (input_ids, segment_ids, attention_mask, mlm_labels, nsp_label)
5b. ALBERT Dataset (with SOP)
- Each example:
[CLS] A [SEP] B [SEP]with segment IDs (0 for A, 1 for B) - SOP label: 1 if (A, B) are in correct order, 0 if swapped
- MLM labels computed via masking function
- Returns: (input_ids, segment_ids, attention_mask, mlm_labels, sop_label)
6. Model Architecture
6a. Multi-Head Self-Attention
- Standard scaled dot-product attention: Q, K, V projections per head
attention = softmax(QK^T / sqrt(d_k)) V- Multi-head:
n_headsparallel attention heads, outputs concatenated and projected - This is identical for both BERT and ALBERT — the architecture is the same; only parameter usage differs
6b. Transformer Encoder Layer
- Each layer: Multi-Head Attention → Add & LayerNorm → Feed-Forward (2 linear layers with GELU) → Add & LayerNorm
- Feed-forward:
Linear(d_model, d_ff)→ GELU →Linear(d_ff, d_model)whered_ff = 4 * d_model - BERT: each layer has its own instance of this module (separate weights)
- ALBERT: a single shared instance is applied N times (same weights reused)
6c. BERT Model (standard)
- Embedding:
nn.Embedding(vocab_size, d_model)— full V×H matrix + positional embedding + segment embedding - Encoder:
nn.ModuleList([TransformerLayer() for _ in range(n_layers)])— each layer has unique weights - NSP head:
Linear(d_model, 2)— binary classification (is_next / not_next) - MLM head:
Linear(d_model, vocab_size)— predict original tokens
6d. ALBERT Model (parameter-reduced)
- Factorized Embedding:
nn.Embedding(vocab_size, d_embedding)→Linear(d_embedding, d_model)— V×E + E×H instead of V×H - Encoder: a single
TransformerLayer()appliedn_layerstimes in a loop — all layers share the same weights - SOP head:
Linear(d_model, 2)— binary classification (correct_order / swapped) - MLM head:
Linear(d_model, vocab_size)— same as BERT
7. Parameter Count Comparison
- Count and print total parameters for BERT vs ALBERT
- Break down by component: embedding, encoder, heads
- Expected: ALBERT embedding ~4× smaller (factorized), ALBERT encoder ~4× smaller (shared across 4 layers)
- Print a comparison table
8. Training Loop
- Standard training loop for both models:
- Forward pass: compute MLM logits and NSP/SOP logits
- Loss: MLM cross-entropy (ignore -100) + NSP/SOP cross-entropy
- Backward pass + Adam optimizer step
- Record loss per epoch for both models
- Same learning rate, batch size, and number of epochs for fair comparison
9. Results & Visualization
- Plot MLM loss curves (BERT vs ALBERT) on the same graph
- Plot total loss curves (MLM + NSP/SOP)
- Print final parameter counts side-by-side
- Print a summary table: model, total params, embedding params, encoder params, final MLM loss, final total loss
Key Functions/Classes
| Component | Description |
|---|---|
MultiHeadAttention | Standard multi-head scaled dot-product attention |
TransformerLayer | One encoder layer: attention + FFN with residual connections |
BERTModel | Standard BERT with per-layer weights, full embedding, NSP head |
ALBERTModel | ALBERT with shared layer weights, factorized embedding, SOP head |
build_vocab(corpus) | Word-level vocabulary from toy corpus |
mask_tokens(input_ids, vocab_size) | MLM masking (15% selected, 80/10/10 split) |
BERTDataset / ALBERTDataset | PyTorch Datasets with NSP / SOP labels |
train_model(model, dataloader, epochs) | Training loop returning loss history |
Data Flow / Shapes
Input sentence pair → Tokenize → [CLS] A [SEP] B [SEP]
→ input_ids: [batch, seq_len]
→ segment_ids: [batch, seq_len]
→ mask_tokens → masked_ids, mlm_labels
Embedding:
BERT: input_ids [B, L] → nn.Embedding(V, H) → [B, L, H]
ALBERT: input_ids [B, L] → nn.Embedding(V, E) → [B, L, E] → Linear(E, H) → [B, L, H]
Encoder (per layer):
[B, L, H] → MultiHeadAttention → [B, L, H] → FFN → [B, L, H]
BERT: 4 separate layers (4× params)
ALBERT: 1 layer applied 4 times (1× params)
Heads:
MLM: [B, L, H] → Linear(H, V) → [B, L, V] (cross-entropy with mlm_labels)
NSP: [CLS] [B, H] → Linear(H, 2) → [B, 2] (BERT)
SOP: [CLS] [B, H] → Linear(H, 2) → [B, 2] (ALBERT)
Deliberate Simplifications vs Full Paper
- Scale: The real ALBERT uses 12–24 layers, H=768–4096, V=30,000. This notebook uses 4 layers, H=128, V~500 for demonstration.
- Tokenization: Word-level tokenization instead of WordPiece/SentencePiece. No subword tokens.
- Pretraining data: A toy corpus of ~50 sentences vs Wikipedia + BookCorpus (3.3B words).
- Masking: Static masking (same as BERT) rather than dynamic masking (RoBERTa improvement) — the focus here is on ALBERT’s parameter reduction, not masking strategy.
- No downstream evaluation: The paper evaluates on GLUE, RACE, SQuAD. This notebook only compares pretraining loss, not fine-tuned downstream accuracy.
- No LAMB optimizer: The paper uses LAMB for large batch training. This notebook uses standard Adam.
- SOP vs NSP comparison: Both losses are implemented, but the comparison focuses on the auxiliary task difference alongside the parameter reduction, not the SOP vs NSP ablation in isolation (which the paper does separately).