1. Recurrent Neural Network (RNN) Formalism

Recurrent Neural Networks model sequential dependencies by maintaining an internal hidden state memory across time steps $t \in \{1, 2, \dots, T\}$. Given an input sequence $\mathbf{x}_t \in \mathbb{R}^{d_x}$, the hidden state $\mathbf{h}_t \in \mathbb{R}^{d_h}$ updates recurrently:

$$\mathbf{h}_t = \tanh\left(\mathbf{W}_{hh} \mathbf{h}_{t-1} + \mathbf{W}_{xh} \mathbf{x}_t + \mathbf{b}_h\right)$$$$\hat{\mathbf{y}}_t = \mathbf{W}_{hy} \mathbf{h}_t + \mathbf{b}_y$$
            Recurrent Unrolling Across Time Steps
            
                h_0               h_1               h_2               h_T
                 │                 │                 │                 │
                 ▼                 ▼                 ▼                 ▼
             ┌───────┐         ┌───────┐         ┌───────┐         ┌───────┐
  x_1 ──────►│  RNN  │───h_1──►│  RNN  │───h_2──►│  RNN  │──...───►│  RNN  │──► y_T
             └───────┘         └───────┘         └───────┘         └───────┘
                 │                 │                 │                 │
                 ▼                 ▼                 ▼                 ▼
                y_1               y_2               y_3               y_T

2. Backpropagation Through Time (BPTT) & Vanishing Gradient Proof

Let $\mathcal{L} = \sum_{t=1}^T \mathcal{L}_t$ be the total sequence loss. Computing the gradient of $\mathcal{L}_T$ with respect to the recurrent weight matrix $\mathbf{W}_{hh}$ requires unrolling the chain rule across all preceding temporal states $k \le T$:

$$\frac{\partial \mathcal{L}_T}{\partial \mathbf{W}_{hh}} = \sum_{k=1}^T \frac{\partial \mathcal{L}_T}{\partial \mathbf{h}_T} \frac{\partial \mathbf{h}_T}{\partial \mathbf{h}_k} \frac{\partial \mathbf{h}_k}{\partial \mathbf{W}_{hh}}$$

The temporal Jacobian product $\frac{\partial \mathbf{h}_T}{\partial \mathbf{h}_k}$ expands into a sequential matrix product:

$$\frac{\partial \mathbf{h}_T}{\partial \mathbf{h}_k} = \prod_{j=k+1}^T \frac{\partial \mathbf{h}_j}{\partial \mathbf{h}_{j-1}} = \prod_{j=k+1}^T \text{diag}\left(1 - \tanh^2(\mathbf{a}_j)\right) \mathbf{W}_{hh}^T$$

Let $\gamma = \|\mathbf{W}_{hh}\|_2$ be the spectral norm of the recurrent transition matrix. Since $|1 - \tanh^2(z)| \le 1$:

$$\left\| \frac{\partial \mathbf{h}_T}{\partial \mathbf{h}_k} \right\| \le \gamma^{T - k}$$
  • If $\gamma < 1$, the gradient decays exponentially with temporal distance $T-k$, preventing vanilla RNNs from capturing long-range dependencies ($T > 20$).
  • If $\gamma > 1$, gradients explode exponentially, requiring gradient clipping.

3. Long Short-Term Memory (LSTM) Architecture

To preserve long-range gradient propagation, Long Short-Term Memory (LSTM) networks introduce an additive linear Cell State $\mathbf{c}_t \in \mathbb{R}^{d_h}$ regulated by three multiplicative gating mechanisms.

       LSTM Cell Internal Gating Architecture
       
   c_{t-1} ───────────────────(×)────────────────────(+)──────────────► c_t
                               ▲                      ▲
                               │ f_t                  │ i_t * g_t
                             ┌───┐                  ┌───┐
                             │ σ │                  │ σ │
                             └───┘                  └───┘
                               ▲                      ▲
                               │                      │
             h_{t-1}, x_t ─────┴──────────────────────┴───────► o_t ──► h_t

Exact LSTM Gating Equations

Given concatenated input $\mathbf{v}_t = [\mathbf{h}_{t-1}, \mathbf{x}_t]^T$:

$$\mathbf{f}_t = \sigma\left(\mathbf{W}_f \mathbf{v}_t + \mathbf{b}_f\right) \quad \text{(Forget Gate)}$$$$\mathbf{i}_t = \sigma\left(\mathbf{W}_i \mathbf{v}_t + \mathbf{b}_i\right) \quad \text{(Input Gate)}$$$$\tilde{\mathbf{c}}_t = \tanh\left(\mathbf{W}_c \mathbf{v}_t + \mathbf{b}_c\right) \quad \text{(Candidate Cell State)}$$$$\mathbf{c}_t = \mathbf{f}_t \odot \mathbf{c}_{t-1} + \mathbf{i}_t \odot \tilde{\mathbf{c}}_t \quad \text{(Additive Cell Update)}$$$$\mathbf{o}_t = \sigma\left(\mathbf{W}_o \mathbf{v}_t + \mathbf{b}_o\right) \quad \text{(Output Gate)}$$$$\mathbf{h}_t = \mathbf{o}_t \odot \tanh(\mathbf{c}_t) \quad \text{(Hidden State Update)}$$

Taking the derivative of the cell state $\mathbf{c}_t$ with respect to $\mathbf{c}_{t-1}$:

$$\frac{\partial \mathbf{c}_t}{\partial \mathbf{c}_{t-1}} = \text{diag}(\mathbf{f}_t)$$

When the forget gate $\mathbf{f}_t \approx \mathbf{1}$, the Jacobian $\frac{\partial \mathbf{c}_t}{\partial \mathbf{c}_{t-1}} = \mathbf{I}$, guaranteeing zero gradient attenuation along the cell state highway across hundreds of time steps.


4. Gated Recurrent Units (GRU) Formalism

GRUs streamline LSTMs by merging the cell state and hidden state into a single representation $\mathbf{h}_t$ regulated by Reset ($\mathbf{r}_t$) and Update ($\mathbf{z}_t$) gates:

$$\mathbf{z}_t = \sigma\left(\mathbf{W}_z \mathbf{x}_t + \mathbf{U}_z \mathbf{h}_{t-1}\right), \quad \mathbf{r}_t = \sigma\left(\mathbf{W}_r \mathbf{x}_t + \mathbf{U}_r \mathbf{h}_{t-1}\right)$$$$\tilde{\mathbf{h}}_t = \tanh\left(\mathbf{W}_h \mathbf{x}_t + \mathbf{U}_h (\mathbf{r}_t \odot \mathbf{h}_{t-1})\right)$$$$\mathbf{h}_t = (1 - \mathbf{z}_t) \odot \mathbf{h}_{t-1} + \mathbf{z}_t \odot \tilde{\mathbf{h}}_t$$

5. PyTorch Implementation: Attention-Augmented BiLSTM Sequence Classifier

Below is a production-grade canonical PyTorch implementation of a Bidirectional LSTM with Additive Attention pooling suitable for general sequence classification and sequential embedding representation.

import torch
import torch.nn as nn
import torch.nn.functional as F

class TemporalAdditiveAttention(nn.Module):
    """Additive attention mechanism pooling sequence hidden states into a single context vector."""
    def __init__(self, hidden_dim: int):
        super().__init__()
        self.attn_proj = nn.Sequential(
            nn.Linear(hidden_dim, hidden_dim // 2),
            nn.Tanh(),
            nn.Linear(hidden_dim // 2, 1, bias=False)
        )
        
    def forward(self, lstm_outputs: torch.Tensor, mask: torch.Tensor = None) -> torch.Tensor:
        # lstm_outputs shape: (Batch, SequenceLength, HiddenDim)
        attn_scores = self.attn_proj(lstm_outputs).squeeze(-1)  # (Batch, SeqLen)
        
        if mask is not None:
            attn_scores = attn_scores.masked_fill(~mask, -1e9)
            
        attn_weights = F.softmax(attn_scores, dim=-1).unsqueeze(-1)  # (Batch, SeqLen, 1)
        context_vector = torch.sum(lstm_outputs * attn_weights, dim=1)
        return context_vector, attn_weights

class CanonicalBiLSTMClassifier(nn.Module):
    """Bidirectional LSTM Sequence Classifier with Attention Pooling."""
    def __init__(self, vocab_size: int = 20000, embed_dim: int = 128, hidden_dim: int = 128,
                 num_classes: int = 5, num_layers: int = 2, dropout: float = 0.2):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0)
        self.bilstm = nn.LSTM(
            input_size=embed_dim,
            hidden_size=hidden_dim,
            num_layers=num_layers,
            batch_first=True,
            bidirectional=True,
            dropout=dropout if num_layers > 1 else 0.0
        )
        # Bidirectional doubles output feature dimension
        bilstm_out_dim = hidden_dim * 2
        self.attention = TemporalAdditiveAttention(bilstm_out_dim)
        self.classifier = nn.Sequential(
            nn.Linear(bilstm_out_dim, 64),
            nn.GELU(),
            nn.Dropout(dropout),
            nn.Linear(64, num_classes)
        )
        
    def forward(self, input_ids: torch.Tensor) -> torch.Tensor:
        # input_ids shape: (Batch, SequenceLength)
        mask = (input_ids != 0)
        embedded = self.embedding(input_ids)
        lstm_out, _ = self.bilstm(embedded)
        
        context_vec, _ = self.attention(lstm_out, mask=mask)
        logits = self.classifier(context_vec)
        return logits

# Verification: Forward pass on benchmark sequence token indices
if __name__ == "__main__":
    synthetic_sequences = torch.randint(1, 1000, (16, 50))  # Batch=16, SeqLen=50
    model = CanonicalBiLSTMClassifier(vocab_size=1000, num_classes=5)
    
    logits = model(synthetic_sequences)
    print("Input Sequence Batch Tensor Shape:", synthetic_sequences.shape)
    print("Output Class Logits Matrix Shape:", logits.shape)