Tag: machine-learning

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

  • Foundational Papers of LSTM Era

    The “LSTM Era” wasn’t defined by a single paper, but by a decade-long evolution that solved the fundamental problem of neural networks: short-term memory.

    While the 1997 paper is the most famous, several other foundational works introduced the components that modern LSTMs still use today.

    1. The Discovery of the Problem (1991)

    Before the solution, the problem had to be mathematically defined.

    • Paper: Untersuchungen zu dynamischen neuronalen Netzen (Diploma Thesis)
    • Author: Sepp Hochreiter
    • Contribution: This work (originally in German) was the first to rigorously analyze the Vanishing Gradient Problem. It proved that in standard RNNs, the error signal used for learning essentially disappears as it is multiplied over many time steps, making it impossible for the model to learn long-term dependencies.

    2. The Original LSTM (1997)

    This is the seminal paper that introduced the “Constant Error Carousel” (CEC) to keep gradients flowing.

    • Paper: “Long Short-Term Memory”
    • Authors: Sepp Hochreiter & Jürgen Schmidhuber
    • Contribution: Introduced the core LSTM architecture, including the Input Gate and Output Gate. It allowed neural networks to bridge time lags of over 1,000 steps, which was previously unthinkable.

    3. The Missing Piece: The Forget Gate (1999)

    Surprisingly, the original 1997 LSTM couldn’t “reset” its memory, which caused its internal state to grow indefinitely.

    • Paper: “Learning to Forget: Continual Prediction with LSTM”
    • Authors: Felix Gers, Jürgen Schmidhuber, & Fred Cummins
    • Contribution: Introduced the Forget Gate. This allowed the model to decide which information was no longer relevant, making it much more effective for processing continuous streams of data. This is the version of the LSTM used in almost all modern applications.

    4. Modern Refinements (2000–2005)

    These papers polished the architecture into the high-performance tool that eventually powered Google Translate and Siri.

    • Peephole Connections (2000): Gers and Schmidhuber introduced “Peephole” connections, allowing gates to “look” at the cell state directly for better timing.
    • 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).
    • CTC (2006): Graves introduced Connectionist Temporal Classification, a way to train LSTMs on unsegmented sequence data (like handwriting or audio).

    Summary of the “LSTM Era” Evolution

    YearInnovationKey Significance
    1991Vanishing GradientDefined why RNNs were failing.
    1997Original LSTMCreated the “memory cell” to preserve gradients.
    1999Forget GateAllowed the model to clear its own memory.
    2005BidirectionalEnabled the model to use context from both directions.

    LSTM: The Comeback Story?

    This video features Sepp Hochreiter, the inventor of LSTMs, discussing the history of the architecture and how it is evolving to compete with modern Transformers.