Tag: artificial-intelligence

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

  • Pytorch implementation of paper: Bidirectional LSTMs (2005): Alex Graves and Schmidhuber published “Framewise Phoneme Classification with Bidirectional LSTM,” which allowed the model to look at both the past and the future context (very useful for speech and text).

    The 2005 paper by Alex Graves and Jürgen Schmidhuber was a major milestone for sequence labeling, particularly in speech recognition (phoneme classification). While bidirectional RNNs existed since 1997, Graves and Schmidhuber combined them with the LSTM‘s ability to handle long-term dependencies.

    The core idea is to process the input sequence using two separate hidden layers—one moving forward in time and one moving backward—and then concatenating their outputs at each time step.

    1. The Bidirectional Logic

    For any given time step $t$, the model has access to:

    1. Past context: Via the forward layer $\overrightarrow{h}_t$.
    2. Future context: Via the backward layer $\overleftarrow{h}_t$.

    The final representation for that time step is typically the concatenation: $h_t = [\overrightarrow{h}_t ; \overleftarrow{h}_t]$.


    2. PyTorch Implementation

    While PyTorch provides a bidirectional=True flag in its standard nn.LSTM, here is how you build it from the ground up using the LSTM cells we’ve discussed to understand the data flow.

    Python

    import torch
    import torch.nn as nn
    
    class BidirectionalLSTM(nn.Module):
        def __init__(self, input_size, hidden_size, output_size):
            super(BidirectionalLSTM, self).__init__()
            self.hidden_size = hidden_size
            
            # Two independent LSTM cells
            self.forward_cell = nn.LSTMCell(input_size, hidden_size)
            self.backward_cell = nn.LSTMCell(input_size, hidden_size)
            
            # The output layer takes the concatenated hidden states (2 * hidden_size)
            self.fc = nn.Linear(hidden_size * 2, output_size)
    
        def forward(self, x):
            # x shape: [batch_size, seq_len, input_size]
            batch_size, seq_len, _ = x.size()
            device = x.device
            
            # 1. Initialize states for both directions
            h_f = torch.zeros(batch_size, self.hidden_size).to(device)
            c_f = torch.zeros(batch_size, self.hidden_size).to(device)
            h_b = torch.zeros(batch_size, self.hidden_size).to(device)
            c_b = torch.zeros(batch_size, self.hidden_size).to(device)
            
            # Lists to store hidden states for each time step
            forward_states = [None] * seq_len
            backward_states = [None] * seq_len
            
            # 2. Forward Pass
            for t in range(seq_len):
                h_f, c_f = self.forward_cell(x[:, t, :], (h_f, c_f))
                forward_states[t] = h_f
                
            # 3. Backward Pass
            for t in reversed(range(seq_len)):
                h_b, c_b = self.backward_cell(x[:, t, :], (h_b, c_b))
                backward_states[t] = h_b
                
            # 4. Concatenation & Output
            # Combine [forward_t, backward_t] for every t
            combined_outputs = []
            for f, b in zip(forward_states, backward_states):
                combined_outputs.append(torch.cat((f, b), dim=1))
                
            # Stack to shape: [seq_len, batch_size, hidden_size * 2]
            out = torch.stack(combined_outputs)
            
            # 5. Framewise Classification (as per the paper)
            # Apply the linear layer to every time step
            logits = self.fc(out)
            
            return logits # [seq_len, batch_size, output_size]
    
    

    3. Why this was a breakthrough for Speech

    In speech recognition (phoneme classification), the sound you are hearing now is often disambiguated by the sound that comes immediately after it.

    • Standard LSTM: Only knows what has been said so far. It has to “guess” until the next phoneme arrives.
    • Bidirectional LSTM: Can “wait” to see the entire utterance before deciding what the phoneme at $t=5$ was, using context from $t=6, 7, 8…$

    4. Key Implementation Nuance: Padding

    When using bidirectional models on mini-batches of varying lengths, you must be careful with padding. If you don’t use pack_padded_sequence, the “backward” layer will start by processing a bunch of zero-padding tokens, which can corrupt the initial hidden state for the actual data.


  • Pytorch implementation of Paper: “Learning to Forget: Continual Prediction with LSTM”

    The paper “Learning to Forget: Continual Prediction with LSTM” (Gers, Schmidhuber, and Cummins, 2000) is the most significant update to the original LSTM. It introduced the Forget Gate, which allows the network to reset its internal state when a sequence ends or information becomes obsolete.

    Without this gate, the cell state ($c_t$) can grow indefinitely, leading to numerical instability and a “saturated” memory that can’t learn new patterns.

    1. The Core Innovation: The Forget Gate

    In the 1997 version, $c_t = c_{t-1} + i_t g_t$. In the 2000 version, the forget gate $f_t$ was added to scale the previous cell state:

    $$c_t = f_t \odot c_{t-1} + i_t \odot g_t$$


    2. PyTorch Implementation

    This implementation follows the precise equations from the 2000 paper, including the forget gate logic.

    Python

    import torch
    import torch.nn as nn
    
    class ForgetGateLSTMCell(nn.Module):
        def __init__(self, input_size, hidden_size):
            super(ForgetGateLSTMCell, self).__init__()
            self.input_size = input_size
            self.hidden_size = hidden_size
            
            # Linear layers for the 4 internal components
            # (f: forget, i: input, g: cell candidate, o: output)
            self.x2h = nn.Linear(input_size, 4 * hidden_size)
            self.h2h = nn.Linear(hidden_size, 4 * hidden_size)
    
        def forward(self, x, states):
            # states: (h_prev, c_prev)
            h_prev, c_prev = states
            
            # Calculate all gate inputs at once
            pre_gates = self.x2h(x) + self.h2h(h_prev)
            
            # Split into individual gates
            # In the 2000 paper, these are:
            # y_in_f (forget), y_in_i (input), y_in_c (cell), y_in_o (output)
            f_pre, i_pre, g_pre, o_pre = pre_gates.chunk(4, 1)
            
            # Gate activations (usually Sigmoid for gates, Tanh for cell)
            f = torch.sigmoid(f_pre)  # Forget Gate: f_t
            i = torch.sigmoid(i_pre)  # Input Gate: i_t
            o = torch.sigmoid(o_pre)  # Output Gate: o_t
            g = torch.tanh(g_pre)     # Cell Input: g_t
            
            # --- THE FORGET GATE UPDATE ---
            # c_t = f_t * c_{t-1} + i_t * g_t
            c_next = (f * c_prev) + (i * g)
            
            # Hidden state output
            h_next = o * torch.tanh(c_next)
            
            return h_next, c_next
    
    

    3. Comparison: 1997 vs. 2000

    The 2000 paper changed how the LSTM handles “continual” tasks where the data stream isn’t chopped into neat, independent segments.

    FeatureOriginal LSTM (1997)Forget Gate LSTM (2000)
    Cell Update$c_t = c_{t-1} + i_t g_t$$c_t = f_t c_{t-1} + i_t g_t$
    Gradient FlowConstant (1.0)Controlled by $f_t$
    MemoryNever resets; can saturateResets based on input context
    Ideal ForShort, segmented sequencesLong-term, continuous time series

    4. Why “Learning to Forget” Matters

    The authors demonstrated that on tasks like the Embedded Reber Grammar, the original LSTM would fail once the sequences became too long because the internal state would drift. By “forgetting,” the model can “clear its desk” once a specific sub-task is finished, allowing the same neurons to be reused for the next part of the sequence.

    If you are training this on modern hardware, you’ll find that $f_t$ is often initialized with a bias of 1.0. This ensures that at the start of training, the gate is wide open (not forgetting anything), preventing the vanishing gradient problem from returning prematurely.

  • Pytorch implementation of Paper: “Long Short-Term Memory”

    Following Sepp Hochreiter’s 1991 thesis, he collaborated with Jürgen Schmidhuber to publish “Long Short-Term Memory” (1997). This paper introduced the Constant Error Carousel (CEC) and Gating Units (specifically the Input and Output gates) to solve the vanishing gradient problem.

    Note: The “Forget Gate” was actually added later (Gers et al., 2000), but I will include it here as it is standard in modern PyTorch implementations.

    1. The LSTM Cell Architecture

    The core idea is the Cell State ($c_t$), which acts as a “conveyor belt” allowing information to flow through time with only minor linear interactions.

    Python

    import torch
    import torch.nn as nn
    import math
    
    class LSTMCell(nn.Module):
        def __init__(self, input_size, hidden_size):
            super(LSTMCell, self).__init__()
            self.input_size = input_size
            self.hidden_size = hidden_size
            
            # We combine all gates into one weight matrix for efficiency
            # Gates: Input (i), Forget (f), Cell (g), Output (o)
            self.x2h = nn.Linear(input_size, 4 * hidden_size)
            self.h2h = nn.Linear(hidden_size, 4 * hidden_size)
            
            self.reset_parameters()
    
        def reset_parameters(self):
            std = 1.0 / math.sqrt(self.hidden_size)
            for w in self.parameters():
                w.data.uniform_(-std, std)
    
        def forward(self, x, states):
            # states is a tuple (h_prev, c_prev)
            h_prev, c_prev = states
            
            # Batch computation of gate linear transformations
            gates = self.x2h(x) + self.h2h(h_prev)
            
            # Split the tensor into the 4 gate components
            i_gate, f_gate, g_gate, o_gate = gates.chunk(4, 1)
            
            # Apply activations
            i_gate = torch.sigmoid(i_gate) # Input gate
            f_gate = torch.sigmoid(f_gate) # Forget gate
            o_gate = torch.sigmoid(o_gate) # Output gate
            g_gate = torch.tanh(g_gate)    # New candidate cell state
            
            # Update Cell State: f_t * c_{t-1} + i_t * g_t
            # This addition is the "Constant Error Carousel" logic
            c_next = (f_gate * c_prev) + (i_gate * g_gate)
            
            # Update Hidden State
            h_next = o_gate * torch.tanh(c_next)
            
            return h_next, c_next
    
    

    2. The LSTM Model (Handling Sequences)

    To process a full sentence or time series, we wrap the cell in a loop. PyTorch’s built-in nn.LSTM does this in C++ for speed, but here is the manual implementation for clarity:

    Python

    class LSTMModel(nn.Module):
        def __init__(self, input_size, hidden_size, output_size):
            super(LSTMModel, self).__init__()
            self.hidden_size = hidden_size
            self.cell = LSTMCell(input_size, hidden_size)
            self.fc = nn.Linear(hidden_size, output_size)
            
        def forward(self, x):
            # x shape: [batch_size, sequence_length, input_size]
            batch_size, seq_len, _ = x.size()
            
            # Initialize h and c states with zeros
            h = torch.zeros(batch_size, self.hidden_size).to(x.device)
            c = torch.zeros(batch_size, self.hidden_size).to(x.device)
            
            # Loop through the sequence
            for t in range(seq_len):
                h, c = self.cell(x[:, t, :], (h, c))
                
            # Use the final hidden state for prediction
            out = self.fc(h)
            return out
    
    

    Why this solved the problem

    As highlighted in the 1997 paper, the LSTM avoids vanishing gradients because:

    1. Additive Updates: The cell state update $c_t = f_t c_{t-1} + i_t g_t$ uses addition rather than multiplication. When the forget gate $f_t$ is near 1, the gradient can flow backward through the cell state indefinitely without shrinking.
    2. Gating: The gates act as “valves” that learn when to let information in, when to clear the memory, and when to pass the memory to the next layer.
    ComponentFunctionMath Symbol
    Forget GateDecides what to discard from the cell state$f_t$
    Input GateDecides which new values to update$i_t$
    Cell StateThe long-term memory (CEC)$c_t$
    Output GateDecides what parts of the cell state to output$o_t$
  • 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.

  • Foundational Papers on : The Scaling Laws and Emergent Abilities (2020–2022)

    The era between 2020 and 2022 was when LLM development shifted from “art” to “predictive science.” Researchers discovered that model performance isn’t random—it follows strict mathematical laws—but that these smooth laws occasionally produce “magic” new abilities.

    Here are the foundational papers that defined this period.


    1. The Discovery of Scaling Laws (2020)

    Before this paper, training a giant model was a massive gamble. OpenAI turned it into a math problem.

    • Paper: Scaling Laws for Neural Language Models
    • Authors: Jared Kaplan, Sam McCandlish, Tom Henighan, et al. (OpenAI)
    • The Breakthrough: They discovered that Test Loss (error) follows a Power Law. If you increase your Compute ($C$), Dataset ($D$), or Parameters ($N$), the model’s performance improves in a perfectly straight line on a log-log plot.
    • The “Kaplan Rule”: They initially argued that if you have more compute, you should put most of it into making the model bigger ($N$) rather than giving it more data ($D$).

    2. The “Chinchilla” Correction (2022)

    DeepMind realized that the industry (including GPT-3) was building models that were “too big” and “under-trained.”

    • Paper: Training Compute-Optimal Large Language Models
    • Authors: Jordan Hoffmann, Sebastian Borgeaud, Arthur Mensch, et al. (DeepMind)
    • The Breakthrough: They proved that for a model to be “compute-optimal,” you must scale parameters and data equally.
    • The “Chinchilla Rule”: For every doubling of model size, you should also double the amount of training data. They suggested a ratio of roughly 20 tokens per 1 parameter.
      • Impact: This led to the creation of smaller, “denser” models like Llama, which are much cheaper to run but just as smart as giants.

    3. The Mystery of Emergence (2022)

    While scaling laws predicted that models would get “better” at predicting words, they didn’t explain why models suddenly learned to do math or code.

    • Paper: Emergent Abilities of Large Language Models
    • Authors: Jason Wei, Yi Tay, Rishi Bommasani, et al. (Google/Stanford)
    • The Breakthrough: Defined “Emergence” as an ability that is not present in small models but appears suddenly in large ones.
    • The Finding: Tasks like multi-step reasoning, translation, and common-sense logic don’t improve slowly; they stay at 0% accuracy until the model hits a certain “critical mass” (often around 10–100 billion parameters), at which point accuracy spikes to 50%+.

    4. The Benchmarking Giant (2022)

    To prove these laws and abilities, researchers needed a massive, difficult test that a model couldn’t just “memorize.”

    • Paper: Beyond the Imitation Game: Quantifying and Extrapolating the Capabilities of Language Models (known as BIG-bench)
    • Authors: Aarohi Srivastava, Guy Gur-Ari, et al. (Google & 400+ contributors)
    • The Breakthrough: A collaborative suite of 200+ tasks (from emoji-understanding to chess) designed to find the limits of LLMs. It provided the data that proved “Emergent Abilities” were a real phenomenon across many different model architectures.

    Summary of the Era

    YearKey PaperMain Takeaway
    2020Kaplan (Scaling Laws)AI performance is mathematically predictable.
    2022ChinchillaWe need more data, not just more parameters.
    2022Wei et al. (Emergence)Big models gain “unpredictable” skills suddenly.
    2022BIG-benchStandardized how we measure “intelligence” vs. “scale.”

    The 2024–2026 Update: The “Mirage” Debate

    It’s worth noting that a newer foundational paper (“Are Emergent Abilities of Large Language Models a Mirage?”) argued that emergence might just be a trick of how we measure things. For example, if you measure “Correct vs. Incorrect,” a model looks like it suddenly “gets it.” But if you measure “Token Probability,” the model was actually improving smoothly the whole time.

  • Foundational Papers on – The Great Divergence: BERT vs. GPT

    The “Great Divergence” occurred shortly after the Transformer was born, as the AI world split into two camps: those who wanted models to understand (Google’s BERT) and those who wanted models to generate (OpenAI’s GPT).

    These are the foundational papers that defined that era and the “Scale War” that followed.


    1. The “Understanding” Path (Encoder-Only)

    Google focused on using the Encoder half of the Transformer to create a “Swiss Army Knife” for language understanding.

    • BERT (2018):“BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding”
      • Authors: Jacob Devlin, Ming-Wei Chang, Kenton Lee, Kristina Toutanova.
      • The Breakthrough: Introduced Masked Language Modeling (MLM). By hiding 15% of words and making the model guess them using both left and right context, BERT became the best “reader” in history.
    • RoBERTa (2019):“RoBERTa: A Robustly Optimized BERT Pretraining Approach”
      • Authors: Yinhan Liu, Myle Ott, et al. (Facebook AI).
      • The Breakthrough: Proved that BERT was “significantly undertrained.” By training longer, on more data, and removing the “Next Sentence Prediction” task, they reached much higher performance, showing that Scale > Architectural tweaks.

    2. The “Generative” Path (Decoder-Only)

    OpenAI bet everything on the Decoder half, believing that if a model can perfectly predict the next word, it will eventually understand everything else.

    • GPT-1 (2018):“Improving Language Understanding by Generative Pre-Training”
      • Authors: Alec Radford, Karthik Narasimhan, et al.
      • The Breakthrough: The first to show that a generative model, pre-trained on a diverse corpus (BookCorpus), could be fine-tuned for many different tasks.
    • GPT-2 (2019):“Language Models are Unsupervised Multitask Learners”
      • Authors: Alec Radford, Jeffrey Wu, et al.
      • The Breakthrough: This started the Scale War. It jumped to 1.5 billion parameters and proved “Zero-Shot” learning: the model could do tasks (like translation) it was never specifically trained for, simply because it had “read the whole internet.”

    3. The Bridge: The Unified Path

    While GPT and BERT were fighting, Google released a “best of both worlds” model that remains a staple in research.

    • T5 (2019):“Exploring the Limits of Transfer Learning with a Unified Text-to-Text Transformer”
      • Authors: Colin Raffel, Noam Shazeer, et al.
      • The Breakthrough: Proposed that every NLP task (classification, translation, etc.) should be treated as a “text-to-text” problem. It used the full Encoder-Decoder stack and introduced the C4 dataset, which became the standard for training massive models.

    Summary: The Divergence in a Nutshell

    FeatureBERT PhilosophyGPT Philosophy
    Primary GoalFeature Extraction (Understanding)Next-Token Prediction (Generation)
    ContextBidirectional (Looks left & right)Unidirectional (Looks only at the past)
    Ideal TaskSearch, Sentiment, NERWriting, Coding, Chat
    OutcomeRevolutionized Google SearchLed to ChatGPT & the LLM era

    The “End” of the Divergence

    By 2021, the GPT Philosophy essentially “won” the Scale War for general-purpose AI. While BERT is still used for specific business tasks (like categorizing emails), the world moved toward Generative models because they demonstrated “Emergent Intelligence” at massive scales that encoders simply couldn’t match.

  • Building a Transformer in Pytorch

    Building a Transformer from scratch in PyTorch is a rite of passage for modern AI practitioners. While libraries like Hugging Face are great for production, understanding the “guts” involves implementing the architecture introduced in the Attention is All You Need paper.

    To keep this digestible, we’ll break the construction into its core components: Scaled Dot-Product Attention, Multi-Head Attention, and the Transformer Block.


    1. Scaled Dot-Product Attention

    This is the heart of the model. It calculates how much focus one word should have on another by comparing a Query (Q) and a Key (K), then applying that weight to the Value (V).

    Mathematically:

    $$\text{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
    
    def scaled_dot_product_attention(q, k, v, mask=None):
        d_k = q.size(-1)
        # Compute attention 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)
        
        weights = F.softmax(scores, dim=-1)
        return torch.matmul(weights, v), weights
    
    

    2. Multi-Head Attention (MHA)

    Instead of performing a single attention function, we split the embedding dimension into “heads.” This allows the model to simultaneously attend to information from different representation subspaces (e.g., one head focuses on grammar, another on the subject-verb relationship).

    Python

    class MultiHeadAttention(nn.Module):
        def __init__(self, d_model, num_heads):
            super().__init__()
            self.num_heads = num_heads
            self.d_model = d_model
            assert d_model % num_heads == 0
            
            self.d_k = d_model // num_heads
            
            # Linear layers for Q, K, V projections
            self.q_linear = nn.Linear(d_model, d_model)
            self.k_linear = nn.Linear(d_model, d_model)
            self.v_linear = nn.Linear(d_model, d_model)
            self.out_linear = nn.Linear(d_model, d_model)
    
        def forward(self, q, k, v, mask=None):
            batch_size = q.size(0)
            
            # 1. Linear projections and split into heads
            q = self.q_linear(q).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
            k = self.k_linear(k).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
            v = self.v_linear(v).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
            
            # 2. Apply scaled dot-product attention
            x, _ = scaled_dot_product_attention(q, k, v, mask)
            
            # 3. Concatenate heads and put back through final linear layer
            x = x.transpose(1, 2).contiguous().view(batch_size, -1, self.d_model)
            return self.out_linear(x)
    
    

    3. The Transformer Block (Encoder Layer)

    A standard Transformer layer consists of the MHA we just built, followed by a Feed-Forward Network (FFN). Crucially, we use Residual Connections and Layer Normalization to keep the gradients stable.

    Python

    class TransformerBlock(nn.Module):
        def __init__(self, d_model, num_heads, dropout=0.1):
            super().__init__()
            self.attention = MultiHeadAttention(d_model, num_heads)
            self.norm1 = nn.LayerNorm(d_model)
            self.norm2 = nn.LayerNorm(d_model)
            
            self.feed_forward = nn.Sequential(
                nn.Linear(d_model, 4 * d_model),
                nn.ReLU(),
                nn.Linear(4 * d_model, d_model)
            )
            self.dropout = nn.Dropout(dropout)
    
        def forward(self, x, mask=None):
            # Sublayer 1: Attention + Residual
            attn_out = self.attention(x, x, x, mask)
            x = self.norm1(x + self.dropout(attn_out))
            
            # Sublayer 2: Feed Forward + Residual
            ff_out = self.feed_forward(x)
            x = self.norm2(x + self.dropout(ff_out))
            return x
    
    

    4. Putting it Together: The Encoder

    A full Transformer Encoder is simply a stack of these blocks, preceded by an Embedding layer and Positional Encoding (since Transformers have no inherent sense of word order).

    Python

    class TransformerEncoder(nn.Module):
        def __init__(self, vocab_size, d_model, num_layers, num_heads, max_len=512):
            super().__init__()
            self.embedding = nn.Embedding(vocab_size, d_model)
            # Simplified positional encoding for brevity
            self.pos_embedding = nn.Embedding(max_len, d_model)
            
            self.layers = nn.ModuleList([
                TransformerBlock(d_model, num_heads) for _ in range(num_layers)
            ])
    
        def forward(self, x, mask=None):
            batch_size, seq_len = x.size()
            positions = torch.arange(0, seq_len).expand(batch_size, seq_len).to(x.device)
            
            out = self.embedding(x) + self.pos_embedding(positions)
            
            for layer in self.layers:
                out = layer(out, mask)
            return out
    
    

    Key Takeaways

    • Permutation Invariance: Transformers don’t “see” order unless you add Positional Encodings.
    • Complexity: The self-attention mechanism has a computational complexity of $O(n^2 \cdot d)$, where $n$ is the sequence length. This is why long documents are harder for Transformers to process.
    • The “Why”: Unlike RNNs, Transformers allow for massive parallelization during training because the entire sequence is processed at once.
  • Vanishing vs Exploding Gradients

    In deep learning, particularly with recurrent neural networks (RNNs) or very deep architectures, the “vanishing” and “exploding” gradient problems are two sides of the same coin. Both stem from the way we update weights using the Chain Rule during backpropagation.


    1. The Core Issue

    When calculating the gradient of the loss function with respect to a weight in an early layer, we multiply several gradients together. If we represent the gradient as $G$:

    • Vanishing: If the gradients are small (e.g., $< 1$), multiplying many of them causes the product to shrink exponentially toward zero. Early layers stop learning.
    • Exploding: If the gradients are large (e.g., $> 1$), the product grows exponentially. This leads to “NaN” (Not a Number) errors or weights that oscillate wildly.

    2. Comparison Table

    FeatureVanishing GradientExploding Gradient
    SymptomTraining slows down or stops; loss plateaus.Model weights become NaN; loss fluctuates or goes to infinity.
    Activation CauseSigmoid or Tanh (slopes are $< 1$ at extremes).Large weights + many layers + no normalization.
    Common ArchitecturesDeep Vanilla CNNs, standard RNNs.RNNs with long sequences, very deep networks without scaling.
    PyTorch Detectionweight.grad values are extremely close to 0.weight.grad values are huge or NaN.

    3. How to Fix Them in PyTorch

    Addressing Vanishing Gradients

    • Activation Functions: Use ReLU ($f(x) = \max(0, x)$) or its variants (Leaky ReLU). ReLU has a constant gradient of 1 for all positive inputs, preventing the shrink.
    • Weight Initialization: Use He Initialization (for ReLU) or Xavier/Glorot (for Sigmoid/Tanh). This keeps the variance of activations consistent across layers.Pythontorch.nn.init.kaiming_normal_(layer.weight, nonlinearity='relu')
    • Batch Normalization: nn.BatchNorm2d or nn.LayerNorm re-centers and re-scales data, keeping it in the “active” range of activation functions.
    • Residual Connections: Skip connections (ResNets) allow the gradient to “bypass” layers, flowing directly to earlier parts of the net.

    Addressing Exploding Gradients

    • Gradient Clipping: This is the most common fix in PyTorch, especially for RNNs. It forces the gradient norm to stay below a certain threshold.Python# After loss.backward(), before optimizer.step() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
    • Weight Regularization: Adding L2 penalty (weight_decay in your optimizer) keeps weight magnitudes small.
    • Smaller Learning Rate: A high learning rate compounds the explosion; lowering it can provide stability.

    4. Debugging in PyTorch

    If you suspect your gradients are misbehaving, you can inspect them manually after calling loss.backward():

    Python

    for name, param in model.named_parameters():
        if param.grad is not None:
            print(f"{name} grad norm: {param.grad.norm()}")
    
    
    • If the norms are consistently $1e-7$ or smaller, they are vanishing.
    • If they are $1e+5$ or NaN, they are exploding.