Deep Learning

Your PyTorch Model is Broken. Here’s How to Fix It.

August 3, 2026 · 21 min read
In this article
  1. Table of Contents
  2. 1. The 30-Second Picture in Your Head
  3. 2. What’s Actually Happening? 
  4. 3. Why This Happens (And Why It’s Not Your Fault) 
  5. 4. How to Know If You’re Screwed :
  6. 5. PyTorch Hooks: Your Secret Weapon 
  7. 6. The Right Way to Clip Gradients 
  8. 7. Fix It Before It Breaks: Initialization & BatchNorm 
  9. 8. Residual Connections: The Gradient Superhighway 
  10. 9. Real-World: Fixing a Dead 100-Layer Network 
  11. 10. Freshers’ Interview Cheat Sheet 
  12. 11. Visual Guide: When to Use What 
  13. 12. FAQs :
  14. 13. The Bottom Line :

– Vanishing & Exploding Gradients in PyTorch: Detection, Debugging, and Fixes.

Loss stuck at 2.3? Suddenly turning into NaN? Learn how to detect and fix vanishing/exploding gradients using PyTorch hooks, gradient clipping, and proper initialization. Includes real code and interview prep.

Table of Contents

  1. The 30-Second Picture in Your Head

  2. What’s Actually Happening?

  3. Why This Happens (And Why It’s Not Your Fault)

  4. How to Know If You’re Screwed

  5. PyTorch Hooks: Your Secret Weapon

  6. The Right Way to Clip Gradients

  7. Fix It Before It Breaks: Initialization & BatchNorm

  8. Residual Connections: The Gradient Superhighway

  9. Real-World: Fixing a Dead 100-Layer Network

  10. Freshers’ Interview Cheat Sheet

  11. Visual Guide: When to Use What

  12. FAQs

  13. The Bottom Line


1. The 30-Second Picture in Your Head

Picture this.

You’re in a stadium. 100 people are standing in a line, passing a message from the front to the back.

Person 1 whispers to Person 2: “The answer is 42.”

Person 2 whispers to Person 3: “The answer is… 42?”

By the time the message reaches Person 100, it’s become: “The answer is… something about cats?”

That’s vanishing gradients.

Now imagine the opposite. Person 1 shouts: “THE ANSWER IS 42!” Person 2 shouts louder: “THE ANSWER IS 42!!” By Person 100, it’s a deafening roar that blows out everyone’s eardrums.

That’s exploding gradients.

Your neural network is that stadium. Gradients are the message. And if they vanish or explode, your model learns nothing or crashes entirely.

Comparison illustration showing vanishing gradients as a whisper that fades away through 100 layers, and exploding gradients as a shout that grows louder through 100 layers. Color-coded blue for vanishing and red for exploding


2. What’s Actually Happening? 

You’ve built your deep neural network. You’re excited. You hit “run training.” And then…

Scenario 1: The Dead Model:

Epoch 1: Loss = 2.3456
Epoch 2: Loss = 2.3455
Epoch 3: Loss = 2.3456
Epoch 4: Loss = 2.3455
...
Epoch 50: Loss = 2.3456

Nothing changes. Your model is learning absolutely nothing.

Why? The gradients reaching the early layers are so small they might as well be zero. Weights don’t update. Model stays frozen.

Scenario 2: The Exploding Model

Epoch 1: Loss = 2.3453
Epoch 2: Loss = 2.8912
Epoch 3: Loss = 5.2345
Epoch 4: Loss = 12.4567
Epoch 5: Loss = NaN

Everything was fine, and then suddenly — boom — your loss prints NaN. Your model just exploded.

Why? Gradients got so large they overflowed floating-point precision. Weights blew up. Training crashed.

Scenario 3: The Zombie Model

Your model predicts the exact same class for every single input. Dead neurons everywhere. The model is “alive” but brain-dead.

Why? ReLU neurons got stuck at zero. Once a neuron outputs zero, its gradient is zero forever. It never recovers.


3. Why This Happens (And Why It’s Not Your Fault) 

Let’s crack open the math. Don’t worry – I’ll keep it painless.

The Chain Rule from Hell :

Let’s crack open the math. To understand why your PyTorch model is dying, we need to look at backpropagation and how the chain rule in neural networks actually works.

When you call loss.backward(), PyTorch calculates the gradients by working backward from the output to the input. It multiplies the derivatives of every single layer together.

Mathematically, the gradient for the weights in your very first layer ($w_1$) looks like this:

$$\frac{\partial L}{\partial w_1} = \frac{\partial L}{\partial y} \cdot \frac{\partial y}{\partial h_n} \cdots \frac{\partial h_2}{\partial h_1} \cdot \frac{\partial h_1}{\partial w_1}$$

Every single fraction in this equation represents a derivative at a specific layer. And here is where the vanishing gradient problem starts: every multiplication is an opportunity for things to go wrong.

Why Gradients Vanish ?

If you’re using Sigmoid, its derivative is at most 0.25.

Sigmoid derivative ≤ 0.25

Now imagine 100 layers. Each layer multiplies gradients by ≤ 0.25 and weights that are often < 1.

0.25 × 0.9 × 0.25 × 0.9 × 0.25 × 0.9 × ... = effectively zero

After 100 layers, your gradient is basically 0.0000000000000000000000000000000000000000001.

Net result: The early layers learn nothing. They’re stuck forever.

Why Gradients Explode ?

If your initial weights are too large, each layer multiplies gradients by numbers > 1.

2.5 × 2.5 × 2.5 × 2.5 × ... = huge numbers → NaN

Net result: Weights blow up, loss becomes NaN, training crashes.

The Simple Comparison

ProblemWhat HappensWhat You See
VanishingGradients become ~0Loss is stuck
ExplodingGradients become infiniteLoss becomes NaN

4. How to Know If You’re Screwed :

Before you fix anything, you need to know if you have a problem. Here’s how.

Method 1: The Gradient Norm Check

This is the single most useful thing you can add to your training loop.

def check_gradient_health(model):
    """Are your gradients healthy or dying?"""
    total_norm = 0.0
    for p in model.parameters():
        if p.grad is not None:
            total_norm += p.grad.data.norm(2).item() ** 2
    total_norm = total_norm ** 0.5
    
    if total_norm < 1e-7:
        print(f"💀 GRADIENT VANISHING! Norm: {total_norm:.2e}")
        return "vanishing"
    elif total_norm > 10.0:
        print(f"💥 GRADIENT EXPLODING! Norm: {total_norm:.2f}")
        return "exploding"
    else:
        print(f"✅ Gradients healthy. Norm: {total_norm:.4f}")
        return "healthy"

But wait – those thresholds (1e-7 and 10.0) aren’t universal.

For a tiny model, 1e-5 might be fine. For a massive Transformer, 1e-8 might be normal. The context matters. Start with these, but adjust based on what you see.

Method 2: Inspect Individual Layers

Sometimes the problem is in one specific layer.

def inspect_layer_gradients(model):
    """Find which layer is causing problems"""
    for name, param in model.named_parameters():
        if param.requires_grad and param.grad is not None:
            grad = param.grad
            grad_norm = grad.norm().item()
            
            # Color code based on health
            if grad_norm < 1e-7:
                status = "🔴 VANISHING"
            elif grad_norm > 10.0:
                status = "🔥 EXPLODING"
            elif torch.isnan(grad).any():
                status = "💀 NaN DETECTED"
            else:
                status = "✅ OK"
            
            print(f"{name:40s} | Norm: {grad_norm:.4e} | {status}")

Method 3: Detect NaN the Instant It Happens

def catch_nan_gradient(model):
    """Find exactly which layer produces the first NaN"""
    def hook_fn(name):
        def hook(grad):
            if torch.isnan(grad).any():
                print(f"🚨 FIRST NaN FOUND in {name} at step {step_counter}")
                print(f"   Gradient stats: min={grad.min():.2e}, max={grad.max():.2e}")
                return grad
        return hook
    
    # Register hooks on all parameters
    for name, param in model.named_parameters():
        if param.requires_grad:
            param.register_hook(hook_fn(name))

Decision flowchart for checking gradient health in PyTorch models. Shows thresholds for detecting vanishing gradients (<1e-7) and exploding gradients (>10.0) with color-coded status indicators

Method 4: Check If Weights Are Actually Changing

def track_weight_changes(model, epoch):
    """Track how much weights change between epochs"""
    if not hasattr(track_weight_changes, 'previous'):
        track_weight_changes.previous = {}
        for name, param in model.named_parameters():
            if param.requires_grad:
                track_weight_changes.previous[name] = param.data.clone()
        return
    
    for name, param in model.named_parameters():
        if param.requires_grad:
            diff = (param.data - track_weight_changes.previous[name]).norm().item()
            if diff < 1e-7:
                print(f"⚠️ {name}: NOT LEARNING! Change: {diff:.2e}")
            track_weight_changes.previous[name] = param.data.clone()

5. PyTorch Hooks: Your Secret Weapon 

Hooks are the most powerful debugging tool you have. Most people don’t use them. That’s their loss.

What’s a Hook?

Think of a hook like a spy camera you install in your neural network. It watches what happens during the forward pass (forward hooks) or backward pass (backward hooks) and reports back to you.

You can also use hooks to change what happens – like secretly editing the message before it reaches the next person in the stadium.

The Simplest Hook (Print Gradients)

import torch
import torch.nn as nn

# Create a simple tensor
x = torch.ones(5, requires_grad=True)
y = 2 * x
y.retain_grad()  # Important: save gradient for non-leaf tensors

# Register a hook that prints the gradient
def print_gradient(grad):
    print(f"Gradient: {grad}")
    return grad

y.register_hook(print_gradient)

loss = y.mean()
loss.backward()  # This triggers the hook
# Output: Gradient: tensor([0.2000, 0.2000, ...])

Diagram showing PyTorch hooks as spy cameras attached to each layer of a neural network. Forward pass shown in blue, backward pass in red. Hooks feed data to a monitoring dashboard showing gradient statistics.

The Hook That Actually Saves Your Model

class GradientWatchdog:
    """A watchdog that detects and logs gradient problems"""
    
    def __init__(self, model, log_file=None):
        self.model = model
        self.log_file = log_file
        self.handles = []
        self.grad_history = {}
        
        # Install spies on every layer
        for name, param in model.named_parameters():
            if param.requires_grad:
                handle = param.register_hook(
                    lambda grad, n=name: self.watch_gradient(grad, n)
                )
                self.handles.append(handle)
                self.grad_history[name] = {'norms': [], 'means': []}
    
    def watch_gradient(self, grad, name):
        """Watch and log gradient statistics"""
        grad_norm = grad.norm().item()
        grad_mean = grad.mean().item()
        
        self.grad_history[name]['norms'].append(grad_norm)
        self.grad_history[name]['means'].append(grad_mean)
        
        # Raise alerts
        if grad_norm < 1e-7:
            print(f"🚨 {name}: VANISHING! Norm: {grad_norm:.2e}")
        elif grad_norm > 100:
            print(f"💥 {name}: EXPLODING! Norm: {grad_norm:.2f}")
        elif torch.isnan(grad).any():
            print(f"💀 {name}: NaN DETECTED!")
    
    def get_summary(self):
        """Get a summary of gradient health"""
        summary = {}
        for name, history in self.grad_history.items():
            if history['norms']:
                avg_norm = sum(history['norms']) / len(history['norms'])
                max_norm = max(history['norms'])
                min_norm = min(history['norms'])
                
                summary[name] = {
                    'avg_norm': avg_norm,
                    'max_norm': max_norm,
                    'min_norm': min_norm,
                    'status': '✅ OK' if (min_norm > 1e-7 and max_norm < 100) else '⚠️ ISSUE'
                }
        return summary
    
    def cleanup(self):
        """Remove all hooks to prevent memory leaks"""
        for handle in self.handles:
            handle.remove()

# Usage
watchdog = GradientWatchdog(model)

for epoch in range(10):
    for batch in dataloader:
        optimizer.zero_grad()
        loss = criterion(model(x), y)
        loss.backward()  # Watchdog catches problems here
        optimizer.step()
    
    summary = watchdog.get_summary()
    for name, stats in summary.items():
        if stats['status'] == '⚠️ ISSUE':
            print(f"⚠️ {name}: Avg norm = {stats['avg_norm']:.2e}")

watchdog.cleanup()  # Don't forget this!

Modifying Gradients in a Hook (Advanced)

You can actually change gradients as they flow backward. This is like editing the message as it passes through the stadium.

def gradient_clipper(max_val=1.0):
    """Clip gradients to prevent exploding"""
    def hook(grad):
        return torch.clamp(grad, -max_val, max_val)
    return hook

def gradient_scaler(scale=0.5):
    """Scale gradients down"""
    def hook(grad):
        return grad * scale
    return hook

# Apply to a specific layer
for name, param in model.named_parameters():
    if 'fc1' in name:  # Only clip fc1
        param.register_hook(gradient_clipper(1.0))

Common Hook Mistakes :

Mistake 1: Forgetting to Remove Hooks

# ❌ BAD - Memory leak!
handle = layer.register_forward_hook(my_hook)
# Hook stays forever

# ✅ GOOD - Clean up!
handle = layer.register_forward_hook(my_hook)
# Later...
handle.remove()  # Remove when done

Mistake 2: Checking Gradients Before .backward()

# ❌ BAD - This returns None
print(model.fc1.weight.grad)  # None!

# ✅ GOOD - Check after backward
loss = criterion(output, y)
loss.backward()
print(model.fc1.weight.grad)  # Now it works

Mistake 3: Not Returning Modified Gradients

# ❌ BAD - This does nothing
def bad_hook(grad):
    new_grad = grad * 2
    # No return! Original gradient used

# ✅ GOOD - Return modified gradient
def good_hook(grad):
    return grad * 2  # This replaces the gradient

6. The Right Way to Clip Gradients 

Gradient clipping is the simplest fix for exploding gradients. Here’s exactly how to do it.

The Correct Training Loop Order

for epoch in range(num_epochs):
    for batch in dataloader:
        # 1. Reset gradients
        optimizer.zero_grad()
        
        # 2. Forward pass
        output = model(x)
        loss = criterion(output, y)
        
        # 3. Backward pass (compute gradients)
        loss.backward()
        
        # 4. CLIP GRADIENTS RIGHT HERE
        #    After backward, before optimizer step!
        torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
        
        # 5. Update weights
        optimizer.step()

The Wrong Order (What Beginners Do) 

# ❌ WRONG - Clipping after optimizer step does nothing!
loss.backward()
optimizer.step()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)  # Too late!

clip_grad_norm_ vs clip_grad_value_

# Option 1: clip_grad_norm_ (Recommended)
# Clips by global norm. Preserves direction of gradients.
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

# Option 2: clip_grad_value_
# Clips each value individually. Changes direction.
torch.nn.utils.clip_grad_value_(model.parameters(), clip_value=0.5)
MethodWhat It DoesWhen to Use
clip_grad_norm_Scales so total norm ≤ max_normMost cases. Preserves direction.
clip_grad_value_Clips each value to ±clip_valueWhen individual outliers are the problem.

Why clip_grad_norm_ is usually better:

Imagine you have 100 gradients. 99 are normal, 1 is huge. clip_grad_norm_ scales all of them down slightly. clip_grad_value_ only clips the huge one. Which is better? Usually clip_grad_norm_ because it preserves the relative direction of all gradients.

How to Choose the Clip Value?

# Start with these values
max_norm = 1.0      # For clip_grad_norm_
clip_value = 0.5    # For clip_grad_value_

# Monitor gradient norms during training
# If gradients consistently exceed max_norm, increase it
# If training is unstable, decrease it

7. Fix It Before It Breaks: Initialization & BatchNorm 

The best way to fix vanishing/exploding gradients is to prevent them from happening in the first place.

Weight Initialization: The Right Way

The Problem: If you initialize weights too small, gradients vanish. If you initialize them too large, gradients explode.

The Solution: Use the correct initialization for your activation function.

import torch.nn as nn
import torch.nn.init as init

class ProperlyInitializedModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(784, 256)
        self.fc2 = nn.Linear(256, 128)
        self.fc3 = nn.Linear(128, 10)
        
        # Xavier Initialization (for Sigmoid/Tanh)
        # Keeps variance the same across layers
        init.xavier_uniform_(self.fc1.weight)
        init.xavier_uniform_(self.fc2.weight)
        init.xavier_uniform_(self.fc3.weight)
        
        # OR...
        
        # He Initialization (for ReLU)
        # Specifically designed for ReLU - prevents dying neurons
        init.kaiming_uniform_(self.fc1.weight, nonlinearity='relu')
        init.kaiming_uniform_(self.fc2.weight, nonlinearity='relu')
        init.kaiming_uniform_(self.fc3.weight, nonlinearity='relu')
        
        # Bias initialization (usually zero is fine)
        init.zeros_(self.fc1.bias)
        init.zeros_(self.fc2.bias)
        init.zeros_(self.fc3.bias)

When to Use Which Initialization:

Activation FunctionBest InitializationWhy
ReLUHe (Kaiming)Prevents dying ReLU, good gradient flow
SigmoidXavier (Glorot)Keeps activations in linear region
TanhXavier (Glorot)Keeps activations in linear region
LeakyReLUHe (Kaiming)Same as ReLU

Batch Normalization: The Gradient Savior

Batch Normalization normalizes activations so they stay in a healthy range. It’s like a thermostat for your neural network.

class ModelWithBatchNorm(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(784, 256)
        self.bn1 = nn.BatchNorm1d(256)  # Normalize after fc1
        self.fc2 = nn.Linear(256, 128)
        self.bn2 = nn.BatchNorm1d(128)  # Normalize after fc2
        self.fc3 = nn.Linear(128, 10)
        self.relu = nn.ReLU()
    
    def forward(self, x):
        x = self.fc1(x)
        x = self.bn1(x)     # Normalize BEFORE activation
        x = self.relu(x)
        
        x = self.fc2(x)
        x = self.bn2(x)     # Normalize BEFORE activation
        x = self.relu(x)
        
        x = self.fc3(x)
        return x

Why BatchNorm helps:

  1. Prevents vanishing gradients – Keeps activations from saturating

  2. Prevents exploding gradients – Keeps activations from growing out of control

  3. Allows higher learning rates – More stable training

  4. Reduces sensitivity to initialization – More forgiving

The Common Mistake: BatchNorm Placement

# ❌ WRONG - BatchNorm after activation
x = self.relu(x)
x = self.bn1(x)  # Too late - activations already saturated

# ✅ RIGHT - BatchNorm before activation
x = self.bn1(x)
x = self.relu(x)

BatchNorm should go before the activation function, not after. This ensures the activations entering the activation function are normalized.


8. Residual Connections: The Gradient Superhighway 

Residual connections (skip connections) create a highway for gradients to flow directly from the output to the input.

The Problem Without Residuals :

In a deep network, gradients have to pass through 100 layers to reach the input. Each layer is a toll booth that reduces the gradient.

The Solution: A Superhighway

With residual connections, gradients can skip the toll booths entirely.

class ResidualBlock(nn.Module):
    def __init__(self, dim):
        super().__init__()
        self.fc1 = nn.Linear(dim, dim)
        self.fc2 = nn.Linear(dim, dim)
        self.relu = nn.ReLU()
        self.bn1 = nn.BatchNorm1d(dim)
        self.bn2 = nn.BatchNorm1d(dim)
    
    def forward(self, x):
        # Save the input (the residual)
        residual = x
        
        # Main path
        out = self.fc1(x)
        out = self.bn1(out)
        out = self.relu(out)
        out = self.fc2(out)
        out = self.bn2(out)
        
        # Add the residual (skip connection)
        out = out + residual
        
        # Apply activation after adding
        out = self.relu(out)
        return out

Why Residuals Fix Vanishing Gradients ?

The gradient now has two paths:

  1. The long path through all layers (still has vanishing risk)

  2. The short path through the skip connection (no vanishing risk)

The gradient from the skip connection is ∂(out)/∂x = 1. It doesn’t shrink. Even if the long path vanishes, the short path keeps the gradient flowing.

Building a Deep Network with Residuals

class DeepResNet(nn.Module):
    def __init__(self, num_blocks=50):
        super().__init__()
        self.blocks = nn.ModuleList([
            ResidualBlock(256) for _ in range(num_blocks)
        ])
        self.fc_out = nn.Linear(256, 10)
    
    def forward(self, x):
        for block in self.blocks:
            x = block(x)
        return self.fc_out(x)

# This can be 100 layers deep without vanishing gradients!
model = DeepResNet(num_blocks=50)

9. Real-World: Fixing a Dead 100-Layer Network 

Let’s put it all together and fix a real model.

The Broken Model:

class BrokenModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.layers = nn.ModuleList()
        for i in range(100):
            self.layers.append(nn.Linear(256, 256))
            self.layers.append(nn.Sigmoid())  # This kills gradients
    
    def forward(self, x):
        for layer in self.layers:
            x = layer(x)
        return x

Problems:

  • ❌ Sigmoid activation → gradients shrink by 75% per layer

  • ❌ No skip connections → no gradient highway

  • ❌ Default initialization → weights are too small

Step 1: Detect the Problem

model = BrokenModel()
watchdog = GradientWatchdog(model)

# Training loop
for epoch in range(5):
    optimizer.zero_grad()
    output = model(x)
    loss = criterion(output, y)
    loss.backward()
    
    summary = watchdog.get_summary()
    for name, stats in summary.items():
        if stats['status'] == '⚠️ ISSUE':
            print(f"{name}: Average gradient norm = {stats['avg_norm']:.2e} (VANISHING!)")
    
    optimizer.step()

watchdog.cleanup()

Output:

layer.0.weight: Average gradient norm = 2.34e-12 (VANISHING!)
layer.1.weight: Average gradient norm = 1.87e-13 (VANISHING!)
layer.2.weight: Average gradient norm = 4.56e-14 (VANISHING!)
...

Step 2: Apply the Fixes

class FixedModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.blocks = nn.ModuleList()
        for i in range(50):  # 50 blocks, 2 layers each = 100 layers
            self.blocks.append(ResidualBlock(256))
    
    def forward(self, x):
        for block in self.blocks:
            x = block(x)
        return x

# Apply He initialization to all layers
def initialize_weights(model):
    for module in model.modules():
        if isinstance(module, nn.Linear):
            nn.init.kaiming_uniform_(module.weight, nonlinearity='relu')
            nn.init.zeros_(module.bias)

model = FixedModel()
initialize_weights(model)

# Now train
watchdog = GradientWatchdog(model)

for epoch in range(5):
    optimizer.zero_grad()
    output = model(x)
    loss = criterion(output, y)
    loss.backward()
    
    summary = watchdog.get_summary()
    for name, stats in summary.items():
        if stats['status'] == '⚠️ ISSUE':
            print(f"Still has issues in {name}")
        else:
            print(f"✅ {name}: {stats['avg_norm']:.2e}")
    
    optimizer.step()

watchdog.cleanup()

Output:

✅ block.0.fc1.weight: 4.23e-01
✅ block.0.fc2.weight: 3.87e-01
✅ block.1.fc1.weight: 4.56e-01
...

The gradients are now healthy!

The Complete Fixed Model

class ProductionReadyModel(nn.Module):
    """A 100-layer model that actually trains"""
    
    def __init__(self, input_dim=256, output_dim=10, num_blocks=50):
        super().__init__()
        
        # Input projection
        self.input_proj = nn.Linear(input_dim, 256)
        self.input_bn = nn.BatchNorm1d(256)
        
        # Residual blocks
        self.blocks = nn.ModuleList([
            ResidualBlock(256) for _ in range(num_blocks)
        ])
        
        # Output
        self.output_proj = nn.Linear(256, output_dim)
        
        # Proper initialization
        self._initialize_weights()
    
    def _initialize_weights(self):
        for module in self.modules():
            if isinstance(module, nn.Linear):
                nn.init.kaiming_uniform_(module.weight, nonlinearity='relu')
                if module.bias is not None:
                    nn.init.zeros_(module.bias)
    
    def forward(self, x):
        x = self.input_proj(x)
        x = self.input_bn(x)
        x = nn.functional.relu(x)
        
        for block in self.blocks:
            x = block(x)
        
        x = self.output_proj(x)
        return x

10. Freshers’ Interview Cheat Sheet 

If you’re preparing for ML interviews, here’s your cheat sheet.

Question 1: “What are Vanishing and Exploding Gradients?”

Short answer: Vanishing gradients happen when gradients become extremely small during backpropagation, causing early layers to stop learning. Exploding gradients happen when gradients become extremely large, causing weights to blow up and training to crash.

The analogy: Vanishing is like a whisper that fades away; exploding is like a shout that bursts eardrums.

Question 2: “What Causes Vanishing Gradients?”

  • Sigmoid/Tanh activation (derivative ≤ 0.25)

  • Deep networks (many multiplications)

  • Poor weight initialization (weights too small)

Question 3: “How Does ReLU Fix Vanishing Gradients?”

ReLU has derivative = 1 for positive inputs. So gradients don’t shrink when passing through ReLU. They maintain their size.

Question 4: “What’s the Dying ReLU Problem?”

When neurons output 0 for all inputs, they become permanently dead. The gradient through them is 0, so they never recover.

Fix: Use LeakyReLU or ELU.

# Instead of ReLU
nn.ReLU()

# Use LeakyReLU
nn.LeakyReLU(0.01)  # Allows small gradient even for negative inputs

Question 5: “How Do You Fix Vanishing Gradients?”

  1. Use ReLU instead of Sigmoid

  2. Use He initialization for ReLU

  3. Use Batch Normalization

  4. Use Residual connections

  5. Use gradient clipping (for exploding)

Question 6: “Where Do You Put Gradient Clipping?”

After loss.backward() and before optimizer.step().

loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()

Question 7: “Why Do LSTMs Not Suffer from Vanishing Gradients as Much as RNNs?”

LSTMs have a cell state with additive updates (not multiplicative). Gradients can flow through the cell state without shrinking. This is the “constant error carousel.”

Question 8: “Does Adding More Layers Always Make a Model Better?”

No. More layers mean more risk of vanishing/exploding gradients. Without proper initialization, normalization, and skip connections, deeper networks often perform worse.


11. Visual Guide: When to Use What 

Here’s a decision tree for diagnosing and fixing gradient problems.

Training Starts
     │
     ▼
Is loss stuck?
├── Yes → Check gradient norms
│          │
│          ▼
│   Is grad_norm < 1e-7?
│   ├── Yes → VANISHING GRADIENTS
│   │          Fix:
│   │          ├── Replace Sigmoid with ReLU
│   │          ├── Add Batch Normalization
│   │          ├── Use He initialization
│   │          └── Add Residual connections
│   └── No → Check if weights are updating
│             ├── Yes → Your model might just be too small
│             └── No → Increase learning rate carefully
│
└── No → Is loss becoming NaN?
         ├── Yes → EXPLODING GRADIENTS
         │          Fix:
         │          ├── Add gradient clipping (clip_grad_norm_)
         │          ├── Reduce learning rate
         │          ├── Use better initialization
         │          └── Add Batch Normalization
         └── No → Your model is training correctly! 🎉

Complete decision tree for diagnosing and fixing vanishing/exploding gradients in PyTorch. Shows paths for 'Loss stuck' leading to vanishing gradient fixes (ReLU, BatchNorm, He init, Residuals) and 'Loss NaN' leading to exploding gradient fixes (clip_grad_norm_, lower LR)

Quick Reference Table

ProblemSymptomDetectionFix
VanishingLoss stuck, weights not updatinggrad_norm < 1e-7ReLU, BatchNorm, He init, Residuals
ExplodingLoss becomes NaNgrad_norm > 100Gradient clipping, lower LR
Dead ReLUNeurons output 0Activation stats show zerosLeakyReLU, proper init
NaN GradientsLoss NaN, training crashestorch.isnan(grad).any()Lower LR, gradient clipping

12. FAQs :

Why is my PyTorch training loss stuck at a constant number?

Classic vanishing gradients. Check your activation functions. If you’re using Sigmoid in a deep network, switch to ReLU. Add Batch Normalization. Use He initialization.

Why does my PyTorch loss suddenly become NaN?

Exploding gradients. Immediately add torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) after loss.backward(). Also check your learning rate – it might be too high.

How to check if my weights are actually changing?

Track weight norms across epochs:

for name, param in model.named_parameters():
    if param.requires_grad:
        print(f"{name}: {param.data.norm().item():.4f}")

If the values don’t change, you have vanishing gradients.

Why is layer.weight.grad returning None?

You’re checking before loss.backward(). Gradients only exist after backward pass.

# ❌ BAD
print(model.fc1.weight.grad)  # None

# ✅ GOOD
loss.backward()
print(model.fc1.weight.grad)  # Now it works

How does Batch Normalization prevent vanishing gradients?

It normalizes activations to have mean 0 and variance 1. This keeps them in a healthy range where gradients don’t vanish or explode.

Does Automatic Mixed Precision cause vanishing gradients?

In FP16 mode, yes. Numbers below 2^-24 become zero. Use GradScaler to prevent underflow.

scaler = torch.cuda.amp.GradScaler()
with torch.cuda.amp.autocast():
    output = model(x)
    loss = criterion(output, y)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()

What’s the difference between clip_grad_norm_ and clip_grad_value_?

clip_grad_norm_ scales gradients so their total norm is ≤ max_norm. Preserves direction.
clip_grad_value_ clips each individual value to ±clip_value. Changes direction.


13. The Bottom Line :

ConceptWhat It Does
Vanishing GradientsGradients become zero → weights stop updating → loss stuck
Exploding GradientsGradients become infinite → weights blow up → loss NaN
PyTorch HooksSpy devices that monitor/modify gradients during backprop
Gradient ClippingMathematical fix for exploding gradients
He InitializationPrevent vanishing gradients for ReLU networks
Xavier InitializationPrevent vanishing gradients for Sigmoid/Tanh networks
Batch NormalizationNormalize activations to prevent both vanishing and exploding
Residual ConnectionsGradient superhighway that bypasses layers

Vanishing gradients kill training by making gradients zero; exploding gradients kill training by making gradients infinite.

Use PyTorch hooks to detect these problems in real-time, and use ReLU, BatchNorm, He initialization, and residual connections to prevent them.

For production, add gradient clipping and proper initialization to keep your gradients healthy and your training stable.

Your Action Plan

StepAction
1Add gradient logging to your training loop
2Use clip_grad_norm_ for exploding gradients
3Replace Sigmoid with ReLU for deep networks
4Use He initialization for ReLU networks
5Add Batch Normalization before activations
6Add residual connections for very deep networks
7Use hooks when debugging tricky issues

🚀 The Ultimate Checklist 

  • Check gradient norms in every epoch
  • Set clip_grad_norm_ after loss.backward()
  • Use ReLU for deep networks
  • Use He/Kaiming initialization for ReLU
  • Add Batch Normalization before activations
  • Add residual connections for 50+ layer networks
  • Monitor for NaN gradients
  • Track weight changes across epochs
  • Use hooks for deep debugging
  • Clean up hooks to prevent memory leaks

Never miss what we build next.

New articles and interactive labs, straight to your inbox the moment they ship — no fixed schedule, no fluff.

Logic Lama
Neural Ninjas
// Continuing from this article

Your Neural Path

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted
// STILL BROWSING?
Build along, don't just read.
Get labs & articles matched to what you're into — free, takes 30 seconds.
Start building free