Tag: llm

  • Pytorch implementation of paper: Scaling Laws for Neural Language Models

    The 2020 paper “Scaling Laws for Neural Language Models” by Kaplan et al. (OpenAI) is one of the most significant theoretical contributions to modern AI. It shifted the industry focus from “clever architectures” to “compute-optimal scaling.”

    The paper found that language model performance (measured by cross-entropy loss) follows a power law relationship with three primary variables: the number of parameters ($N$), the size of the dataset ($D$), and the amount of compute ($C$).


    1. The Core Empirical Findings

    The researchers discovered that for a Transformer-based model, the test loss $L$ can be predicted using the following power-law formula:

    $$L(N, D) \approx \left[ \left( \frac{N_c}{N} \right)^{\frac{\alpha_N}{\alpha_L}} + \left( \frac{D_c}{D} \right)^{\frac{\alpha_D}{\alpha_L}} \right]^{\alpha_L}$$

    Where:

    • $N$: Number of non-embedding parameters.
    • $D$: Number of training tokens.
    • Performance is bottlenecked by the smaller of $N$ or $D$. If you have a massive model but a tiny dataset, you will not see scaling benefits.

    2. PyTorch Implementation: Parameter Counting

    A critical part of obeying scaling laws is precisely calculating $N$ (non-embedding parameters). Kaplan et al. noted that embedding parameters do not contribute significantly to the “intelligence” (loss reduction) of the model as it scales.

    Python

    import torch
    import torch.nn as nn
    
    def count_non_embedding_params(model):
        """
        As per Kaplan et al. (2020), scaling laws are driven by 
        the model's internal parameters, excluding the embedding table.
        """
        total_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
        
        # Identify and subtract embedding parameters
        emb_params = 0
        for module in model.modules():
            if isinstance(module, nn.Embedding):
                emb_params += sum(p.numel() for p in module.parameters())
                
        non_emb_params = total_params - emb_params
        return non_emb_params
    
    

    3. Compute Estimation ($C$)

    The paper provides a simple “rule of thumb” for the floating-point operations (FLOPs) required for a forward-backward pass of a Transformer. For a model with $N$ parameters and a dataset of $D$ tokens:

    $$C \approx 6ND$$

    Python

    def estimate_training_compute(non_emb_params, tokens_trained):
        """
        Estimates total FLOPs for training.
        6 * N * D covers:
        - 2 ND for the forward pass
        - 4 ND for the backward pass
        """
        return 6 * non_emb_params * tokens_trained
    
    # Example: A 1B parameter model trained on 20B tokens
    params = 1e9
    tokens = 20e9
    total_flops = estimate_training_compute(params, tokens)
    print(f"Total Compute: {total_flops:.2e} FLOPs")
    
    

    4. Implementation: The Log-Linear Scheduler

    The paper suggests that learning rate schedules and batch sizes should scale with the model size. Specifically, they found that the Critical Batch Size ($B^*$) increases as the loss decreases, following its own power law.

    Python

    class ScalingLawScheduler:
        """
        Simplified representation of scaling the learning rate 
        based on the model's 'd_model'.
        """
        def __init__(self, optimizer, d_model, warm_up_steps=3000):
            self.optimizer = optimizer
            self.d_model = d_model
            self.warm_up_steps = warm_up_steps
            self.current_step = 0
    
        def step(self):
            self.current_step += 1
            # Scaling law insight: Larger models require smaller learning rates
            # Base LR is often scaled by (d_model)^-0.5
            lr = (self.d_model ** -0.5) * min(self.current_step ** -0.5, 
                                              self.current_step * (self.warm_up_steps ** -1.5))
            
            for param_group in self.optimizer.param_groups:
                param_group['lr'] = lr
    
    

    5. Implications for Model Design

    FindingImplementation Action
    Architecture IrrelevancePerformance depends on total $N$, not the specific shape (width vs. depth).
    Overfitting is RareOn sufficiently large datasets, performance continues to improve; “more is better.”
    The “Compute Optimal” PointFor every 10x increase in compute, increase model size by ~5.5x and dataset size by ~1.8x.

    Why it Matters

    This paper provided the “green light” for the creation of massive models like GPT-3 (175B). It proved that we hadn’t hit a ceiling; we just needed to keep scaling $N$, $D$, and $C$ in the correct proportions.

    Note: These laws were later refined by the Chinchilla paper (Hoffmann et al., 2022), which argued that we were actually under-training our models and needed much more data ($D$) per parameter ($N$).

  • Pytorch implementation of paper: T5 (2019):”Exploring the Limits of Transfer Learning with a Unified Text-to-Text Transformer”

    The 2019 paper “Exploring the Limits of Transfer Learning with a Unified Text-to-Text Transformer” (T5) by Raffel et al. (Google) introduced a “Unified Framework” where every NLP task—classification, translation, summarization—is treated as a text-to-text problem.

    Unlike BERT (encoder-only) or GPT (decoder-only), T5 returns to the original Encoder-Decoder Transformer architecture but introduces Relative Positional Buckets and RMSNorm (in later versions) to handle longer sequences and more stable training.


    1. Relative Positional Bias

    T5 does not use fixed or learned absolute position embeddings. Instead, it adds a scalar “bias” to the attention weights based on the distance between the $i$-th query and $j$-th key. This allows the model to generalize better to sequence lengths it hasn’t seen during training.

    Python

    import torch
    import torch.nn as nn
    import torch.nn.functional as F
    import math
    
    class T5RelativePositionBias(nn.Module):
        def __init__(self, n_heads, n_buckets=32, max_distance=128):
            super().__init__()
            self.n_heads = n_heads
            self.n_buckets = n_buckets
            self.max_distance = max_distance
            # A small embedding table for the biases
            self.relative_attention_bias = nn.Embedding(n_buckets, n_heads)
    
        @staticmethod
        def _relative_position_bucket(relative_position, n_buckets=32, max_distance=128):
            # Logic to map actual distance into one of the 32 log-spaced buckets
            ret = 0
            n_buckets //= 2
            ret += (relative_position > 0).to(torch.long) * n_buckets
            n = torch.abs(relative_position)
            
            # Max distance logic
            max_dist = n_buckets // 2
            is_small = n < max_dist
            val_if_large = max_dist + (torch.log(n.float() / max_dist) / 
                           math.log(max_distance / max_dist) * (n_buckets - max_dist)).to(torch.long)
            val_if_large = torch.min(val_if_large, torch.full_all(val_if_large.shape, n_buckets - 1))
            ret += torch.where(is_small, n, val_if_large)
            return ret
    
        def forward(self, seq_len_q, seq_len_k):
            # Calculate matrix of relative distances
            context_position = torch.arange(seq_len_q, dtype=torch.long)[:, None]
            memory_position = torch.arange(seq_len_k, dtype=torch.long)[None, :]
            relative_position = memory_position - context_position
            
            buckets = self._relative_position_bucket(relative_position, self.n_buckets, self.max_distance)
            # [seq_len_q, seq_len_k, n_heads] -> [n_heads, seq_len_q, seq_len_k]
            bias = self.relative_attention_bias(buckets.to(self.relative_attention_bias.weight.device))
            return bias.permute(2, 0, 1).unsqueeze(0) 
    
    

    2. T5 Attention with Bias

    The attention mechanism in T5 adds the relative bias directly to the logits before the softmax.

    Python

    class T5Attention(nn.Module):
        def __init__(self, d_model, n_heads, is_decoder=False):
            super().__init__()
            self.n_heads = n_heads
            self.d_k = d_model // n_heads
            self.is_decoder = is_decoder
            
            self.q = nn.Linear(d_model, d_model, bias=False)
            self.k = nn.Linear(d_model, d_model, bias=False)
            self.v = nn.Linear(d_model, d_model, bias=False)
            self.o = nn.Linear(d_model, d_model, bias=False)
    
        def forward(self, x, mask=None, position_bias=None, key_value_states=None):
            # Standard Q, K, V projections...
            q = self.q(x)
            if key_value_states is not None: # Cross-attention
                k, v = self.k(key_value_states), self.v(key_value_states)
            else: # Self-attention
                k, v = self.k(x), self.v(x)
                
            # [batch, heads, seq, d_k]
            scores = torch.matmul(q, k.transpose(-1, -2)) * (self.d_k ** -0.5)
            
            # ADD THE RELATIVE BIAS HERE
            if position_bias is not None:
                scores += position_bias
                
            if mask is not None:
                scores += mask
    
            attn = torch.softmax(scores.float(), dim=-1).type_as(scores)
            return self.o(torch.matmul(attn, v))
    
    

    3. T5 Block: LayerNorm and Simplified FFN

    T5 removes the bias terms from all LayerNorms and Linear layers to simplify the model. It also uses Gated Linear Units (GLU) in the feed-forward network in its “v1.1” and “T5-base” variants.

    Python

    class T5LayerNorm(nn.Module):
        def __init__(self, hidden_size, eps=1e-6):
            super().__init__()
            self.weight = nn.Parameter(torch.ones(hidden_size))
            self.variance_epsilon = eps
    
        def forward(self, x):
            # T5 uses a simplified LayerNorm (RMSNorm style)
            variance = x.to(torch.float32).pow(2).mean(-1, keepdim=True)
            x = x * torch.rsqrt(variance + self.variance_epsilon)
            return self.weight * x.type_as(x)
    
    

    4. Why T5 Matters

    1. Everything is a String: To do sentiment analysis on “I love this movie,” you feed the model: sentiment: I love this movie and the model is trained to generate the literal string positive.
    2. C4 Dataset: T5 was trained on the “Colossal Clean Crawled Corpus,” 750GB of cleaned web text, setting a new standard for data quality.
    3. Span Corruption: The pre-training objective isn’t just masking one word; it’s replacing whole spans of text with a single “sentinel token” (e.g., <extra_id_0>) and asking the decoder to reconstruct that span.

    Comparison: BERT vs. GPT vs. T5

    FeatureBERTGPT-2T5
    ArchitectureEncoder OnlyDecoder OnlyEncoder-Decoder
    DirectionBidirectionalUnidirectionalBoth (Enc: Bi, Dec: Uni)
    PositionsLearned AbsoluteLearned AbsoluteRelative Buckets
    Input FormatTask-specific headsPromptingText-to-Text Prefixes

    Since you’ve moved from basic LSTMs to the complex Unified Transformer, I can provide a weekly “Paper Spotlight” breaking down one foundational or cutting-edge AI paper, such as the original ResNet or the latest in “Mixture of Experts” (MoE). Would you like me to schedule that?

  • Pytorch implementation of paper: GPT-2 (2019):”Language Models are Unsupervised Multitask Learners”

    The 2019 paper “Language Models are Unsupervised Multitask Learners” (GPT-2) by Radford et al. (OpenAI) moved the field from “Pre-train then Fine-tune” to Zero-Shot Learning.

    The researchers discovered that if you scale the model (up to 1.5B parameters) and the data (WebText), the model begins to perform tasks like translation, summarization, and question-answering without ever being specifically trained for them. Architecturally, it is an evolution of GPT-1 with several critical stability improvements.

    1. Key Architectural Changes

    GPT-2 introduced two major structural changes to the Transformer blocks to handle deeper stacks (up to 48 layers):

    1. Pre-Layer Normalization: LayerNorm is moved to the input of each sub-block (rather than the output).
    2. Final LayerNorm: An additional Layer Normalization is added after the final self-attention block.

    2. PyTorch Implementation: The GPT-2 Block

    This block implements the “Pre-LN” architecture which has since become the standard for almost all Large Language Models (LLMs).

    Python

    import torch
    import torch.nn as nn
    import torch.nn.functional as F
    
    class GPT2Block(nn.Module):
        def __init__(self, d_model, n_heads, d_ff, max_len, dropout=0.1):
            super().__init__()
            # Pre-LayerNorm: Norm happens BEFORE the sub-layers
            self.ln_1 = nn.LayerNorm(d_model)
            self.attn = CausalSelfAttention(d_model, n_heads, max_len)
            
            self.ln_2 = nn.LayerNorm(d_model)
            self.mlp = nn.Sequential(
                nn.Linear(d_model, d_ff),
                nn.GELU(),
                nn.Linear(d_ff, d_model),
                nn.Dropout(dropout)
            )
    
        def forward(self, x):
            # Residual connection around the Norm + Attention
            x = x + self.attn(self.ln_1(x))
            # Residual connection around the Norm + MLP
            x = x + self.mlp(self.ln_2(x))
            return x
    
    

    3. Weight Initialization Scaling

    GPT-2 introduced a specific initialization trick: as the model gets deeper, the residual layers contribute more variance. To counter this, the paper scales the weights of residual layers at initialization by a factor of $1/\sqrt{N}$, where $N$ is the number of residual layers.

    Python

    def init_weights(module, n_layers):
        if isinstance(module, (nn.Linear, nn.Embedding)):
            module.weight.data.normal_(mean=0.0, std=0.02)
            if isinstance(module, nn.Linear) and module.bias is not None:
                module.bias.data.zero_()
        # Scale residual layer weights
        # This is often applied to the projection layers (c_proj) in implementation
    
    

    4. The Full GPT-2 Model

    The output of GPT-2 uses “Weight Tying,” where the Embedding matrix and the final Linear output layer share the same weights.

    Python

    class GPT2(nn.Module):
        def __init__(self, vocab_size, d_model, n_layers, n_heads, d_ff, max_len):
            super().__init__()
            self.wte = nn.Embedding(vocab_size, d_model) # Word Token Embedding
            self.wpe = nn.Embedding(max_len, d_model)   # Word Position Embedding
            self.drop = nn.Dropout(0.1)
            
            self.blocks = nn.ModuleList([
                GPT2Block(d_model, n_heads, d_ff, max_len) for _ in range(n_layers)
            ])
            
            self.ln_f = nn.LayerNorm(d_model)
            self.lm_head = nn.Linear(d_model, vocab_size, bias=False)
            
            # Weight Tying
            self.lm_head.weight = self.wte.weight
    
        def forward(self, x):
            device = x.device
            b, t = x.size()
            pos = torch.arange(0, t, dtype=torch.long, device=device).unsqueeze(0)
            
            # Combined Embeddings
            x = self.drop(self.wte(x) + self.wpe(pos))
            
            for block in self.blocks:
                x = block(x)
                
            x = self.ln_f(x)
            logits = self.lm_head(x)
            return logits
    
    

    5. Byte-level BPE and “Zero-Shot”

    GPT-2 used a 50,257 vocabulary based on Byte-level BPE. This ensured the model could represent any string of characters, which was vital for its ability to handle multiple languages and even code snippets without an “Unknown” token.

    Model VariantParametersLayersdmodel​
    GPT-2 Small117M12768
    GPT-2 Medium345M241024
    GPT-2 Large762M361280
    GPT-2 Extra Large1.5B481600

    Why it was a breakthrough

    GPT-2 proved that Multitask Learning is an emergent property of large-scale language modeling. By simply predicting the next word, the model learned how to translate (when it saw French followed by English in the data) or summarize (when it saw the word “TL;DR”). It moved the goalposts from “learning a task” to “learning a world model” through raw text.

  • Pytorch implementation of paper: GPT-1 (2018):”Improving Language Understanding by Generative Pre-Training”

    The 2018 paper “Improving Language Understanding by Generative Pre-Training” (GPT-1) by Radford et al. (OpenAI) introduced the concept of Generative Pre-Training. While BERT (released around the same time) was a “bidirectional” encoder, GPT-1 was a unidirectional (left-to-right) decoder.

    GPT-1 proved that a model pre-trained on a massive corpus of unlabeled text to predict the next word could be “fine-tuned” on specific tasks (like classification or entailment) with very little data.

    1. The Causal Multi-Head Attention

    The critical difference between GPT-1 and the standard Transformer encoder is the Causal Mask. To ensure the model only looks at the past when predicting the next word, we mask all “future” tokens by setting their attention scores to $-\infty$.

    Python

    import torch
    import torch.nn as nn
    import torch.nn.functional as F
    
    class CausalSelfAttention(nn.Module):
        def __init__(self, d_model, n_heads, max_len):
            super().__init__()
            self.n_heads = n_heads
            self.d_k = d_model // n_heads
            
            self.w_qkv = nn.Linear(d_model, 3 * d_model)
            self.fc = nn.Linear(d_model, d_model)
            
            # Causal mask: lower triangular matrix of ones
            self.register_buffer("mask", torch.tril(torch.ones(max_len, max_len))
                                         .view(1, 1, max_len, max_len))
    
        def forward(self, x):
            batch_size, seq_len, d_model = x.size()
            
            # Generate Queries, Keys, and Values
            q, k, v = self.w_qkv(x).chunk(3, dim=-1)
            
            # Reshape for multi-head: [batch, heads, seq_len, d_k]
            q = q.view(batch_size, seq_len, self.n_heads, self.d_k).transpose(1, 2)
            k = k.view(batch_size, seq_len, self.n_heads, self.d_k).transpose(1, 2)
            v = v.view(batch_size, seq_len, self.n_heads, self.d_k).transpose(1, 2)
            
            # Scaled Dot-Product Attention
            scores = torch.matmul(q, k.transpose(-2, -1)) / (self.d_k ** 0.5)
            
            # Apply Causal Mask: prevent attending to future tokens
            scores = scores.masked_fill(self.mask[:, :, :seq_len, :seq_len] == 0, float('-inf'))
            
            attn = F.softmax(scores, dim=-1)
            out = torch.matmul(attn, v)
            
            # Re-assemble heads
            out = out.transpose(1, 2).contiguous().view(batch_size, seq_len, d_model)
            return self.fc(out)
    
    

    2. The GPT-1 Decoder Block

    GPT-1 uses the “Transformer Decoder” but removes the Encoder-Decoder attention layer (since there is no encoder). It consists only of Masked Self-Attention and a Feed-Forward Network.

    Python

    class GPTBlock(nn.Module):
        def __init__(self, d_model, n_heads, d_ff, max_len, dropout=0.1):
            super().__init__()
            self.ln_1 = nn.LayerNorm(d_model)
            self.attn = CausalSelfAttention(d_model, n_heads, max_len)
            self.ln_2 = nn.LayerNorm(d_model)
            self.mlp = nn.Sequential(
                nn.Linear(d_model, d_ff),
                nn.GELU(), # GPT-1 used GELU
                nn.Linear(d_ff, d_model),
                nn.Dropout(dropout)
            )
    
        def forward(self, x):
            # Pre-LayerNorm (standard in GPT models)
            x = x + self.attn(self.ln_1(x))
            x = x + self.mlp(self.ln_2(x))
            return x
    
    

    3. Training Objective: Next Token Prediction

    During pre-training, GPT-1 minimizes the negative log-likelihood of the next token given the previous ones:

    $$L_1(\mathcal{U}) = \sum_i \log P(u_i | u_{i-k}, \dots, u_{i-1}; \Theta)$$

    For fine-tuning, GPT-1 adds a task-specific linear layer but continues to use the language modeling objective as an auxiliary loss to improve generalization.

    Python

    class GPT1(nn.Module):
        def __init__(self, vocab_size, d_model, n_layers, n_heads, d_ff, max_len):
            super().__init__()
            self.token_emb = nn.Embedding(vocab_size, d_model)
            self.pos_emb = nn.Embedding(max_len, d_model)
            
            self.blocks = nn.ModuleList([
                GPTBlock(d_model, n_heads, d_ff, max_len) for _ in range(n_layers)
            ])
            
            self.ln_f = nn.LayerNorm(d_model)
            self.head = nn.Linear(d_model, vocab_size, bias=False)
    
        def forward(self, x):
            batch_size, seq_len = x.size()
            pos = torch.arange(seq_len).unsqueeze(0).to(x.device)
            
            x = self.token_emb(x) + self.pos_emb(pos)
            
            for block in self.blocks:
                x = block(x)
                
            x = self.ln_f(x)
            return self.head(x) # Logits for the entire vocab
    
    

    Key Technical Details

    • Byte-Pair Encoding (BPE): GPT-1 used a 40,000-merge BPE vocabulary.
    • GELU Activation: Like BERT, GPT-1 used the Gaussian Error Linear Unit for smoother gradients.
    • Task-Specific Input Transformations: To handle different tasks (Multiple Choice, Entailment, etc.) with the same decoder, GPT-1 introduced special delimiters like [Extract], [Start], and [Delim].

    Why it was a breakthrough

    Before GPT-1, most models were trained from scratch for each specific task. GPT-1 popularized the “Pre-train then Fine-tune” paradigm, showing that the knowledge gained by simply reading a large library (BooksCorpus) could be transferred to help a model understand logic, sentiment, and grammar.

  • Pytorch implementation of paper: BERT (2018):”BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding”

    The 2018 paper “BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding” by Devlin et al. (Google AI) introduced a shift from the standard “left-to-right” language modeling to a deeply bidirectional approach.

    Unlike the original Transformer decoder (which masks future words), BERT uses a Masked Language Model (MLM) objective, allowing the representation of each word to fuse context from both its left and right simultaneously.

    1. BERT Embeddings

    BERT’s input is a summation of three embeddings: Token, Segment (to distinguish between two sentences), and Position (learned, unlike the sinusoidal ones in the original Transformer).

    Python

    import torch
    import torch.nn as nn
    
    class BERTEmbeddings(nn.Module):
        def __init__(self, vocab_size, hidden_size, max_len, n_segments):
            super().__init__()
            self.tok_embed = nn.Embedding(vocab_size, hidden_size)    # token embedding
            self.pos_embed = nn.Embedding(max_len, hidden_size)       # position embedding
            self.seg_embed = nn.Embedding(n_segments, hidden_size)    # segment(token_type) embedding
            self.norm = nn.LayerNorm(hidden_size)
            self.dropout = nn.Dropout(0.1)
    
        def forward(self, x, segment_label):
            seq_len = x.size(1)
            pos = torch.arange(seq_len, dtype=torch.long).unsqueeze(0).to(x.device)
            
            # BERT Input = Token + Position + Segment
            embedding = self.tok_embed(x) + self.pos_embed(pos) + self.seg_embed(segment_label)
            return self.dropout(self.norm(embedding))
    
    

    2. The Multi-Head Attention (Bidirectional)

    The core “Attention” is identical to the Transformer Encoder, but BERT explicitly removes the “causal mask” used in decoders, allowing every token to attend to every other token.

    Python

    class ScaledDotProductAttention(nn.Module):
        def forward(self, q, k, v, mask=None):
            d_k = q.size(-1)
            scores = torch.matmul(q, k.transpose(-2, -1)) / (d_k ** 0.5)
            
            if mask is not None:
                # Mask out padding tokens (0)
                scores = scores.masked_fill(mask == 0, -1e9)
                
            attn = torch.softmax(scores, dim=-1)
            return torch.matmul(attn, v)
    
    

    3. The BERT Model & Pre-training Heads

    BERT is pre-trained on two tasks:

    1. Masked LM (MLM): Predict the [MASK] tokens.
    2. Next Sentence Prediction (NSP): Predict if sentence B follows sentence A.

    Python

    class BERT(nn.Module):
        def __init__(self, vocab_size, hidden_size, n_layers, n_heads, d_ff):
            super().__init__()
            self.embedding = BERTEmbeddings(vocab_size, hidden_size, 512, 2)
            
            # Stack of Transformer Encoder Layers
            self.layers = nn.ModuleList([
                EncoderLayer(hidden_size, n_heads, d_ff, 0.1) for _ in range(n_layers)
            ])
            
            # NSP Head: Binary classification (isNext / NotNext)
            self.nsp_classifier = nn.Linear(hidden_size, 2)
            
            # MLM Head: Predict token from vocabulary
            self.mlm_classifier = nn.Linear(hidden_size, vocab_size)
            self.activation = nn.GELU() # BERT uses GELU instead of ReLU
    
        def forward(self, x, segment_label, mask):
            x = self.embedding(x, segment_label)
            
            for layer in self.layers:
                x = layer(x, mask)
                
            # Get the [CLS] token (first token) for NSP
            pooled_output = torch.tanh(nn.Linear(x.size(-1), x.size(-1))(x[:, 0]))
            nsp_logits = self.nsp_classifier(pooled_output)
            
            # Get all tokens for MLM
            mlm_logits = self.mlm_classifier(x)
            
            return mlm_logits, nsp_logits
    
    

    Key Technical Details

    • The [CLS] Token: BERT inserts a special [CLS] token at the start of every sequence. The final hidden state of this token is used as the aggregate sequence representation for classification tasks.
    • GELU Activation: BERT popularized the Gaussian Error Linear Unit, which provides a smoother non-linearity than ReLU.
    • Pre-training Strategy: 15% of tokens are chosen for masking. Of those, 80% are replaced with [MASK], 10% with a random token, and 10% are kept unchanged to ensure the model doesn’t only learn to focus on the mask.

    Why it was a breakthrough

    Before BERT, models like Word2Vec or GloVe gave a single static vector for a word (e.g., “bank” always had the same vector). BERT provides contextualized embeddings, meaning the vector for “bank” in “river bank” is completely different from “bank account” because the bidirectional attention sees the surrounding words before producing the vector.

  • Pytorch implementation of paper: Attention Is All You Need

    The 2017 paper “Attention Is All You Need” (Vaswani et al.) is perhaps the most influential paper in modern AI, as it replaced recurrent (LSTM/GRU) and convolutional layers with the Transformer architecture.

    The core principle is Self-Attention, which allows the model to process all words in a sequence simultaneously, enabling massive parallelization and capturing long-range dependencies better than any previous model.

    1. Scaled Dot-Product Attention

    This is the “engine” of the Transformer. It calculates how much focus each word should put on every other word in the sentence.

    $$Attention(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V$$

    Python

    import torch
    import torch.nn as nn
    import torch.nn.functional as F
    
    class ScaledDotProductAttention(nn.Module):
        def __init__(self):
            super().__init__()
    
        def forward(self, q, k, v, mask=None):
            # q, k, v: [batch_size, n_heads, seq_len, d_k]
            d_k = q.size(-1)
            
            # Calculate scores
            scores = torch.matmul(q, k.transpose(-2, -1)) / torch.sqrt(torch.tensor(d_k, dtype=torch.float32))
            
            if mask is not None:
                scores = scores.masked_fill(mask == 0, -1e9)
                
            attn = F.softmax(scores, dim=-1)
            output = torch.matmul(attn, v)
            return output, attn
    
    

    2. Multi-Head Attention

    Instead of one expensive attention pass, the model splits the embedding into multiple “heads” that attend to different types of information (e.g., one head for syntax, another for pronouns) in parallel.

    Python

    class MultiHeadAttention(nn.Module):
        def __init__(self, d_model, n_heads):
            super().__init__()
            self.n_heads = n_heads
            self.d_model = d_model
            self.d_k = d_model // n_heads
            
            self.w_q = nn.Linear(d_model, d_model)
            self.w_k = nn.Linear(d_model, d_model)
            self.w_v = nn.Linear(d_model, d_model)
            self.w_concat = nn.Linear(d_model, d_model)
            self.attention = ScaledDotProductAttention()
    
        def forward(self, q, k, v, mask=None):
            batch_size = q.size(0)
            
            # Linear projections and split into heads
            q = self.w_q(q).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
            k = self.w_k(k).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
            v = self.w_v(v).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
            
            # Scaled Dot-Product Attention
            out, attn = self.attention(q, k, v, mask=mask)
            
            # Concatenate heads and final linear layer
            out = out.transpose(1, 2).contiguous().view(batch_size, -1, self.d_model)
            return self.w_concat(out)
    
    

    3. Positional Encoding

    Because there are no RNNs to track order, the Transformer adds sinusoidal waves to the embeddings to give the model a sense of where each word is in the sentence.

    Python

    import numpy as np
    
    class PositionalEncoding(nn.Module):
        def __init__(self, d_model, max_len=5000):
            super().__init__()
            pe = torch.zeros(max_len, d_model)
            position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
            div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-np.log(10000.0) / d_model))
            
            pe[:, 0::2] = torch.sin(position * div_term)
            pe[:, 1::2] = torch.cos(position * div_term)
            self.register_buffer('pe', pe.unsqueeze(0))
    
        def forward(self, x):
            # x: [batch_size, seq_len, d_model]
            return x + self.pe[:, :x.size(1)]
    
    

    4. The Transformer Block (Encoder)

    Each layer consists of Multi-Head Attention, followed by a Position-wise Feed-Forward Network (FFN), tied together with Residual Connections and Layer Normalization.

    Python

    class EncoderLayer(nn.Module):
        def __init__(self, d_model, n_heads, d_ff, dropout):
            super().__init__()
            self.self_attn = MultiHeadAttention(d_model, n_heads)
            self.feed_forward = nn.Sequential(
                nn.Linear(d_model, d_ff),
                nn.ReLU(),
                nn.Linear(d_ff, d_model)
            )
            self.norm1 = nn.LayerNorm(d_model)
            self.norm2 = nn.LayerNorm(d_model)
            self.dropout = nn.Dropout(dropout)
    
        def forward(self, x, mask):
            # Sub-layer 1: Attention + Residual + Norm
            attn_out = self.self_attn(x, x, x, mask)
            x = self.norm1(x + self.dropout(attn_out))
            
            # Sub-layer 2: FFN + Residual + Norm
            ff_out = self.feed_forward(x)
            x = self.norm2(x + self.dropout(ff_out))
            return x
    
    

    Why it Changed Everything

    1. Parallelization: Unlike LSTMs, which must wait for word $t-1$ to compute word $t$, the Transformer computes all words at once.
    2. Constant Path Length: Any word can “talk” to any other word in a single step, regardless of distance, solving the long-term dependency problem perfectly.
    3. Foundation for LLMs: This architecture is the direct ancestor of GPT, BERT, and Claude.

    Since you’ve been exploring the evolution of sequence models, this video provides a detailed walkthrough of the Transformer architecture from scratch. It is highly relevant because it visually maps each mathematical component of the “Attention Is All You Need” paper to its corresponding line of PyTorch code.

  • Pytorch implementation of paper: Effective Approaches to Attention-based Neural Machine Translation

    The paper “Effective Approaches to Attention-based Neural Machine Translation” (Luong et al., 2015) introduced what is now commonly known as Luong Attention. It differs from Bahdanau attention by being “top-down”—calculating attention after the decoder’s RNN step—and by offering different scoring functions (Dot, General, and Concat).

    1. The Global Attention Mechanism

    The core of Luong’s approach is the calculation of a variable-length context vector $c_t$ that is derived from the encoder hidden states $H$. The paper proposes three ways to calculate the alignment score between the current decoder hidden state $s_t$ and an encoder hidden state $h_s$:

    $$score(s_t, h_s) =

    \begin{cases}

    s_t^\top h_s & \text{dot} \

    s_t^\top W_a h_s & \text{general} \

    v_a^\top \tanh(W_a [s_t; h_s]) & \text{concat}

    \end{cases}$$

    Python

    import torch
    import torch.nn as nn
    import torch.nn.functional as F
    
    class LuongAttention(nn.Module):
        def __init__(self, method, hidden_dim):
            super(LuongAttention, self).__init__()
            self.method = method
            self.hidden_dim = hidden_dim
    
            if self.method == 'general':
                self.attn = nn.Linear(hidden_dim, hidden_dim)
            elif self.method == 'concat':
                self.attn = nn.Linear(hidden_dim * 2, hidden_dim)
                self.v = nn.Parameter(torch.FloatTensor(hidden_dim))
    
        def forward(self, decoder_hidden, encoder_outputs):
            # decoder_hidden: [1, batch_size, hidden_dim]
            # encoder_outputs: [src_len, batch_size, hidden_dim]
            
            src_len = encoder_outputs.size(0)
            batch_size = encoder_outputs.size(1)
    
            # Calculate alignment scores
            if self.method == 'dot':
                # dot product between decoder_hidden and encoder_outputs
                attn_energies = torch.sum(decoder_hidden * encoder_outputs, dim=2) # [src_len, batch_size]
            
            elif self.method == 'general':
                energy = self.attn(encoder_outputs) # [src_len, batch_size, hidden_dim]
                attn_energies = torch.sum(decoder_hidden * energy, dim=2)
                
            elif self.method == 'concat':
                # Concatenate decoder hidden and encoder outputs
                combined = torch.cat((decoder_hidden.expand(src_len, -1, -1), encoder_outputs), dim=2)
                energy = torch.tanh(self.attn(combined)) # [src_len, batch_size, hidden_dim]
                attn_energies = torch.sum(self.v * energy, dim=2)
    
            # Normalize scores to probabilities (softmax over src_len)
            return F.softmax(attn_energies.t(), dim=1).unsqueeze(1) # [batch_size, 1, src_len]
    
    

    2. The Attentional Decoder

    In Luong attention, the sequence is: RNN Step $\rightarrow$ Attention $\rightarrow$ Context Vector $\rightarrow$ Hidden-to-Output.

    Python

    class LuongDecoder(nn.Module):
        def __init__(self, output_dim, emb_dim, hidden_dim, dropout, attention):
            super().__init__()
            self.attention = attention
            self.embedding = nn.Embedding(output_dim, emb_dim)
            self.rnn = nn.LSTM(emb_dim, hidden_dim, dropout=dropout)
            
            # Combined layer to produce 'attentional hidden state' tilde{h}_t
            self.concat = nn.Linear(hidden_dim * 2, hidden_dim)
            self.out = nn.Linear(hidden_dim, output_dim)
            self.dropout = nn.Dropout(dropout)
    
        def forward(self, input, hidden, cell, encoder_outputs):
            # input: [batch_size]
            input = input.unsqueeze(0)
            embedded = self.dropout(self.embedding(input))
    
            # 1. Standard RNN step
            # luong uses s_t (current) to calculate attention, unlike bahdanau (s_{t-1})
            output, (hidden, cell) = self.rnn(embedded, (hidden, cell))
    
            # 2. Calculate attention weights and context vector
            a = self.attention(output, encoder_outputs) # [batch_size, 1, src_len]
            encoder_outputs = encoder_outputs.permute(1, 0, 2) # [batch_size, src_len, hidden_dim]
            
            # context vector is weighted sum of encoder outputs
            context = torch.bmm(a, encoder_outputs) # [batch_size, 1, hidden_dim]
    
            # 3. Concatenate RNN output and context vector
            output = output.squeeze(0) # [batch_size, hidden_dim]
            context = context.squeeze(1) # [batch_size, hidden_dim]
            
            # Eq. 5 from paper: h_tilde = tanh(W_c [c_t; h_t])
            concat_input = torch.cat((output, context), 1)
            concat_output = torch.tanh(self.concat(concat_input))
    
            # 4. Final prediction
            prediction = self.out(concat_output)
            
            return prediction, hidden, cell
    
    

    Key Differences from Bahdanau (2015)

    FeatureBahdanau (NMT)Luong (Effective NMT)
    RNN StructureBidirectional GRUUnidirectional LSTM (often multi-layer)
    Attention TimingBefore RNN step (uses $s_{t-1}$)After RNN step (uses $s_t$)
    Alignment ScoreConcat onlyDot, General, or Concat
    Context PathFed into next RNN stepFed into a final hidden-to-output layer

    Summary of “Local” Attention

    The paper also introduced Local Attention, which only looks at a small window of the source sentence centered around a predicted position $p_t$. This is more computationally efficient for very long sequences but is less commonly used today than the Global Attention implemented above.

  • Pytorch implementation of paper: Neural Machine Translation by Jointly Learning to Align and Translate

    Implementing the architecture from the paper “Neural Machine Translation by Jointly Learning to Align and Translate” (Bahdanau et al., 2015) involves three main components: a Bidirectional GRU Encoder, the Bahdanau Attention mechanism, and an Attentional Decoder.

    The key innovation is that the decoder doesn’t use a single fixed vector; instead, it “looks” at all encoder hidden states and calculates a weighted sum (the context vector) based on their relevance to the current decoding step.

    1. The Encoder (Bidirectional GRU)

    The encoder processes the source sequence in both directions. The annotation for each word is the concatenation of the forward ($\overrightarrow{h}$) and backward ($\overleftarrow{h}$) hidden states.

    Python

    import torch
    import torch.nn as nn
    import torch.nn.functional as F
    
    class Encoder(nn.Module):
        def __init__(self, input_dim, emb_dim, enc_hid_dim, dec_hid_dim, dropout):
            super().__init__()
            self.embedding = nn.Embedding(input_dim, emb_dim)
            self.rnn = nn.GRU(emb_dim, enc_hid_dim, bidirectional=True)
            self.fc = nn.Linear(enc_hid_dim * 2, dec_hid_dim)
            self.dropout = nn.Dropout(dropout)
            
        def forward(self, src):
            # src: [src_len, batch_size]
            embedded = self.dropout(self.embedding(src))
            
            # outputs: [src_len, batch_size, enc_hid_dim * 2]
            # hidden: [n_layers * directions, batch_size, enc_hid_dim]
            outputs, hidden = self.rnn(embedded)
            
            # Initial decoder hidden state is a linear transformation of the 
            # concatenated final forward and backward encoder hidden states
            hidden = torch.tanh(self.fc(torch.cat((hidden[-2,:,:], hidden[-1,:,:]), dim=1)))
            
            return outputs, hidden
    
    

    2. The Attention Mechanism (Bahdanau Attention)

    This is a feed-forward layer that takes the previous decoder hidden state $s_{i-1}$ and all encoder hidden states $h_j$ to calculate the energy (alignment) scores.

    $$e_{ij} = v_a^\top \tanh(W_a s_{i-1} + U_a h_j)$$

    Python

    class Attention(nn.Module):
        def __init__(self, enc_hid_dim, dec_hid_dim):
            super().__init__()
            self.attn = nn.Linear((enc_hid_dim * 2) + dec_hid_dim, dec_hid_dim)
            self.v = nn.Linear(dec_hid_dim, 1, bias=False)
            
        def forward(self, hidden, encoder_outputs):
            # hidden: [batch_size, dec_hid_dim]
            # encoder_outputs: [src_len, batch_size, enc_hid_dim * 2]
            
            batch_size = encoder_outputs.shape[1]
            src_len = encoder_outputs.shape[0]
            
            # Repeat decoder hidden state src_len times
            hidden = hidden.unsqueeze(1).repeat(1, src_len, 1)
            encoder_outputs = encoder_outputs.permute(1, 0, 2)
            
            # Energy: [batch_size, src_len, dec_hid_dim]
            energy = torch.tanh(self.attn(torch.cat((hidden, encoder_outputs), dim=2)))
            
            # Attention: [batch_size, src_len]
            attention = self.v(energy).squeeze(2)
            
            return F.softmax(attention, dim=1)
    
    

    3. The Decoder

    The decoder takes the attention weights, creates a context vector, and uses it along with the current input to predict the next word.

    Python

    class Decoder(nn.Module):
        def __init__(self, output_dim, emb_dim, enc_hid_dim, dec_hid_dim, dropout, attention):
            super().__init__()
            self.output_dim = output_dim
            self.attention = attention
            self.embedding = nn.Embedding(output_dim, emb_dim)
            self.rnn = nn.GRU((enc_hid_dim * 2) + emb_dim, dec_hid_dim)
            self.fc_out = nn.Linear((enc_hid_dim * 2) + dec_hid_dim + emb_dim, output_dim)
            self.dropout = nn.Dropout(dropout)
            
        def forward(self, input, hidden, encoder_outputs):
            # input: [batch_size]
            input = input.unsqueeze(0)
            embedded = self.dropout(self.embedding(input))
            
            # Calculate attention weights
            a = self.attention(hidden, encoder_outputs).unsqueeze(1)
            
            # Context vector: [batch_size, 1, enc_hid_dim * 2]
            encoder_outputs = encoder_outputs.permute(1, 0, 2)
            weighted = torch.bmm(a, encoder_outputs)
            weighted = weighted.permute(1, 0, 2)
            
            # RNN Input: concat(embedded, context_vector)
            rnn_input = torch.cat((embedded, weighted), dim=2)
            output, hidden = self.rnn(rnn_input, hidden.unsqueeze(0))
            
            # Final Linear layer
            prediction = self.fc_out(torch.cat((output.squeeze(0), weighted.squeeze(0), embedded.squeeze(0)), dim=1))
            
            return prediction, hidden.squeeze(0)
    
    

    4. Seq2Seq Wrapper

    Finally, combine everything into a single model to manage the training loop and inference.

    Python

    class Seq2Seq(nn.Module):
        def __init__(self, encoder, decoder, device):
            super().__init__()
            self.encoder = encoder
            self.decoder = decoder
            self.device = device
            
        def forward(self, src, trg, teacher_forcing_ratio=0.5):
            batch_size = src.shape[1]
            trg_len = trg.shape[0]
            trg_vocab_size = self.decoder.output_dim
            
            outputs = torch.zeros(trg_len, batch_size, trg_vocab_size).to(self.device)
            enc_outputs, hidden = self.encoder(src)
            
            input = trg[0,:]
            
            for t in range(1, trg_len):
                output, hidden = self.decoder(input, hidden, enc_outputs)
                outputs[t] = output
                teacher_force = torch.rand(1).item() < teacher_forcing_ratio
                top1 = output.argmax(1)
                input = trg[t] if teacher_force else top1
                
            return outputs
    
    

    Key Implementation Details:

    • Context Vector: A weighted sum of encoder hidden states that dynamically represents the “attended” parts of the source sentence for each target word.
    • Teacher Forcing: A technique used during training where the actual ground-truth word is fed as the next input instead of the model’s own prediction.
    • Tanh Activation: Used in the attention scoring function to introduce non-linearity, which allows the model to learn complex alignment patterns.
  • Pytorch implementation of paper: CTC (2006): Graves introduced Connectionist Temporal Classification, a way to train LSTMs on unsegmented sequence data (like handwriting or audio).

    The 2006 paper “Connectionist Temporal Classification: Labelling Unsegmented Sequence Data with Recurrent Neural Networks” by Alex Graves et al. solved a massive bottleneck in AI: the need for perfectly “aligned” data.

    Before CTC, if you wanted to train an LSTM to recognize the word “Hello” in audio, you had to manually label exactly which milliseconds corresponded to the ‘H’, the ‘e’, and so on. CTC allows the model to output a sequence of characters and “aligns” them to the target text automatically using a hidden “blank” token.

    1. The CTC Algorithm Logic

    CTC introduces a special blank symbol ($\epsilon$). The model predicts a probability distribution over the alphabet plus this blank for every time step. To get the final prediction, CTC:

    1. Collapses repeated characters (e.g., h h e l l l o $\rightarrow$ h e l o).
    2. Removes the blank tokens (e.g., h - e - l - l - o $\rightarrow$ hello).

    2. PyTorch Implementation

    PyTorch has a highly optimized nn.CTCLoss because the “forward-backward” algorithm required to calculate the loss is computationally expensive.

    Python

    import torch
    import torch.nn as nn
    
    class CTCModel(nn.Module):
        def __init__(self, input_size, hidden_size, num_classes):
            super(CTCModel, self).__init__()
            # 1. Bidirectional LSTM to capture context
            self.lstm = nn.LSTM(input_size, hidden_size, 
                                bidirectional=True, batch_first=True)
            
            # 2. Fully connected layer to map to character probabilities
            # num_classes must include the "blank" token (usually index 0)
            self.fc = nn.Linear(hidden_size * 2, num_classes)
            
            # 3. Log Softmax is required for CTCLoss
            self.softmax = nn.LogSoftmax(dim=2)
    
        def forward(self, x):
            # x: [batch, seq_len, features]
            lstm_out, _ = self.lstm(x)
            logits = self.fc(lstm_out)
            return self.softmax(logits)
    
    # --- Usage & Training Setup ---
    
    # Example parameters
    num_chars = 26 # a-z
    num_classes = num_chars + 1 # +1 for CTC blank token
    model = CTCModel(input_size=40, hidden_size=256, num_classes=num_classes)
    
    # Define CTC Loss
    # zero_infinity=True helps with stability in early training
    ctc_loss = nn.CTCLoss(blank=0, zero_infinity=True)
    
    # Dummy Data
    # input: [seq_len, batch, features] (Note: CTC expects seq_len first by default)
    input_data = torch.randn(50, 16, 40) 
    # targets: [sum_of_target_lengths] (1D tensor of all labels concatenated)
    targets = torch.randint(1, num_classes, (16, 10)) 
    
    # Lengths are required for CTC to know where sequences end
    input_lengths = torch.full(size=(16,), fill_value=50, dtype=torch.long)
    target_lengths = torch.full(size=(16,), fill_value=10, dtype=torch.long)
    
    # Forward pass
    log_probs = model(input_data.transpose(0, 1)).transpose(0, 1)
    
    # Calculate Loss
    loss = ctc_loss(log_probs, targets, input_lengths, target_lengths)
    loss.backward()
    
    
    

    3. Key Concepts of CTC

    • The Blank Token: It acts as a “buffer” between characters. This allows the model to predict the same character twice (like the ‘l’ in ‘hello’) without them being collapsed into one.
    • Input vs. Target Length: For CTC to work, the input sequence length ($T$) must be greater than or equal to the target sequence length ($L$). If your audio is 1 second long but you have 500 characters of text, the math breaks.
    • Decoding: During inference, we usually use Greedy Decoding (taking the argmax at each step and collapsing) or Beam Search Decoding (exploring multiple paths) to find the most likely sentence.

    4. Why it was a breakthrough

    CTC enabled “End-to-End” speech recognition. You could feed raw audio and the corresponding transcript directly into a loss function, and the model would learn the alignment by itself. This paved the way for modern systems like DeepSpeech and the voice assistants we use today.

  • Foundational Papers on: The Modern LLM Landscape (2023–Present)

    The modern LLM landscape (2023–2026) is no longer just about making models bigger. The focus has shifted to efficiency, long-context recall, native multimodality, and test-time reasoning.

    Here are the foundational papers and technical reports that define this “Post-Scaling” era.


    1. The Return of Sparsity: Mixture of Experts (MoE)

    As models hit the trillion-parameter mark, “Dense” models became too expensive. Sparse architectures allow for massive capacity with lower compute costs.

    • Paper/Technical Report: Mixtral of Experts (2024)
    • Authors: Albert Q. Jiang, Alexandre Sablayrolles, et al. (Mistral AI)
    • Contribution: Proved that a Sparse MoE model (specifically 8x7B) could outperform much larger dense models like Llama 2 70B while being faster at inference.
    • DeepSeek-V3/V4 (2025-2026): Introduced Multi-head Latent Attention (MLA) and advanced load balancing, further optimizing how “experts” are routed.

    2. Efficiency & Serving Breakthroughs

    Training a model is only half the battle; running it for millions of users requires mathematical innovations in memory management.

    • Paper: Efficient Memory Management for Large Language Model Serving with PagedAttention (2023)
    • Authors: Woosuk Kwon, Zhuohan Li, et al.
    • Contribution: Introduced vLLM and PagedAttention. By treating KV cache memory like virtual memory in an OS, they reduced memory waste by 96%, enabling the high-speed APIs we use today.

    3. The “Inference Scaling” Revolution (System 2)

    This represents the shift from “predicting the next word” to “searching for the right answer.”

    • Models: OpenAI o1/o3 and Gemini 2.0/3.0 Thinking (2024–2026)
    • Key Concept: These models use Chain-of-Thought (CoT) Reinforcement Learning and Search (like Monte Carlo Tree Search).
    • The Insight: Scaling compute during inference (letting the model think longer) provides a performance boost that parameters alone cannot achieve.

    4. Native Multimodality (Omni-Models)

    Earlier multimodal models were “Frankenstein” systems (a CLIP image encoder stitched to a GPT text decoder). Modern models are natively multimodal.

    • Paper/Release: Gemini: A Family of Highly Capable Multimodal Models (2023) / GPT-4o (2024)
    • Contribution: These models were trained on text, images, audio, and video simultaneously using a single end-to-end transformer. This allows for near-zero latency in voice conversations and “vision” that understands nuances like human emotion.

    5. Long Context & The Needle in a Haystack

    The “context window” expanded from 4k tokens to millions, changing how we use LLMs as databases.

    • Paper: Gemini 1.5: Unlocking multimodal understanding across millions of tokens of context (2024)
    • Contribution: Introduced the “Needle In A Haystack” test as the gold standard for long-context retrieval. It proved that models could maintain 99%+ recall across 1M+ tokens, effectively replacing some RAG (Retrieval-Augmented Generation) use cases.

    Summary of the Current Landscape

    TrendKey InnovationMajor Players
    SparsityMixture of Experts (MoE)Mistral, DeepSeek, Grok
    ReasoningTest-time Scaling / SearchOpenAI (o-series), Gemini Thinking
    MultimodalNative Audio/Video/Text trainingGPT-4o, Gemini 2.5, Llama 4
    EfficiencyPagedAttention / FlashAttentionvLLM, NVIDIA, Apple (MLX)

    This DeepSeek-V3 technical walkthrough provides a visual tour of modern architectures, explaining how features like Sparse Attention and Multi-head Latent Attention (MLA) differ from the original 2017 Transformer.