Skip to main content
Botmartz AI Solution PVT LTD.
PYTORCH

Transfer Learning with ResNet: Freezing Layers, Custom Heads, and Fine-Tuning Strategies

Training from scratch is expensive. Learn how to adapt a pretrained ResNet to new tasks by freezing backbone layers, adding custom heads, and fine-tuning with differential learning rates.

SS
Soham Sharma
AI Engineer, Botmartz · July 17, 2026 · 4 min read
Read Time
4 min
Failure Modes
5
Code Snippets
3
Runnable Notebook
1
Transfer Learning with ResNet: Freezing Layers, Custom Heads, and Fine-Tuning Strategies

The majority of deep learning in production is transfer learning: take a model trained on ImageNet (or another large dataset) and adapt it to your task with a small dataset. Fully retraining is expensive and unnecessary—the early layers have already learned edge and texture detectors. You only need to learn a new decision boundary in your task-specific head. This post covers the three strategies: feature extraction (freeze everything, train only the head), fine-tuning (unfreeze and train everything with different learning rates), and diagnosing when each works.

The Three Transfer Learning Strategies

Strategy 1: Feature Extraction (Frozen Backbone)

Freeze all layers except the final head. Use the pretrained features as-is.

Code Snippet
import torch
import torchvision.models as models
import torch.nn as nn

# Load pretrained ResNet18
resnet = models.resnet18(pretrained=True)

# Freeze all layers
for param in resnet.parameters():
    param.requires_grad = False

# Replace the final layer for your task
num_classes = 10  # Your dataset
resnet.fc = nn.Linear(resnet.fc.in_features, num_classes)

# Only the new fc layer has requires_grad=True
print(f"Frozen backbone parameters: {sum(p.numel() for p in resnet.parameters() if not p.requires_grad)}")
print(f"Trainable parameters: {sum(p.numel() for p in resnet.parameters() if p.requires_grad)}")

Output:

Code Snippet
Frozen backbone parameters: 11176192
Trainable parameters: 5130

Only 5,130 parameters are trainable (the new 10-class head). This is fast and memory-efficient—great for small datasets (< 5K images).

Strategy 2: Fine-Tuning (Unfreeze All)

Unfreeze all layers and train with a lower learning rate for the backbone.

Code Snippet
import torch
import torchvision.models as models
import torch.nn as nn
import torch.optim as optim

resnet = models.resnet18(pretrained=True)
resnet.fc = nn.Linear(resnet.fc.in_features, 10)

# Unfreeze everything
for param in resnet.parameters():
    param.requires_grad = True

# Different learning rates for backbone vs head
backbone_params = list(resnet.parameters())[:-2]  # All except fc
head_params = list(resnet.parameters())[-2:]  # fc weight and bias

optimizer = optim.SGD([
    {'params': backbone_params, 'lr': 0.0001},
    {'params': head_params, 'lr': 0.001}
], momentum=0.9)

print("Optimizer param groups:")
for i, group in enumerate(optimizer.param_groups):
    print(f"  Group {i}: {len(group['params'])} params, lr={group['lr']}")

Output:

Code Snippet
Optimizer param groups:
  Group 0: 59 params, lr=0.0001
  Group 1: 2 params, lr=0.001

Fine-tuning allows the backbone to adapt while preventing catastrophic forgetting. The 10× lower learning rate for the backbone keeps it close to the pretrained weights.

Strategy 3: Progressive Unfreezing

Start with a frozen backbone, then unfreeze layer-by-layer as validation improves.

Code Snippet
import torch
import torchvision.models as models
import torch.nn as nn

resnet = models.resnet18(pretrained=True)
resnet.fc = nn.Linear(resnet.fc.in_features, 10)

# Freeze everything initially
for param in resnet.parameters():
    param.requires_grad = False

# After a few epochs, unfreeze layer 4
for param in resnet.layer4.parameters():
    param.requires_grad = True

# Count trainable params
trainable = sum(p.numel() for p in resnet.parameters() if p.requires_grad)
print(f"After unfreezing layer4: {trainable} trainable parameters")

Output:

Code Snippet
After unfreezing layer4: 2359298 trainable parameters

Progressive unfreezing avoids overfitting on small datasets by gradually increasing model capacity.

When Each Strategy Works

Code Snippet
Dataset size < 5K:      Feature extraction (frozen backbone)
Dataset size 5K - 100K: Fine-tuning with low learning rate
Dataset size > 100K:    Fine-tuning with moderate learning rate
Domain shift large:     Feature extraction or progressive unfreezing
Small training time:    Feature extraction

A small dataset on a similar domain (e.g., medical X-rays to classify pneumonia) benefits from frozen features. A large, distinct domain (e.g., satellite imagery) needs more adaptation.

Gotchas and Pitfalls

Gotcha 1: BatchNorm in Training Mode Corrupts Pretrained Statistics

If the backbone has batch norm layers, they must be in eval mode (don't update running statistics) unless you have a large dataset.

Code Snippet
import torch
import torchvision.models as models
import torch.nn as nn

resnet = models.resnet18(pretrained=True)
resnet.fc = nn.Linear(resnet.fc.in_features, 10)

# WRONG: backbone in training mode
resnet.train()
# This updates batch norm running statistics, corrupting pretrained features

# CORRECT: freeze backbone batch norm, train only head
def freeze_batch_norm_layers(model):
    for module in model.modules():
        if isinstance(module, nn.BatchNorm2d):
            module.eval()

resnet.train()  # training mode for the head
freeze_batch_norm_layers(resnet)  # but freeze batch norm

print(f"Layer4[0].bn1 training: {resnet.layer4[0].bn1.training}")
print(f"fc training: {resnet.fc.training}")

Output:

Code Snippet
Layer4[0].bn1 training: False
fc training: True

Fix: Use freeze_batch_norm_layers() when doing feature extraction or fine-tuning on small datasets. Let batch norm update only if you have > 50K images.

Gotcha 2: Learning Rate Too High Destroys Pretrained Weights

Fine-tuning with the learning rate you'd use for training from scratch will corrupt the pretrained backbone.

Code Snippet
import torch
import torch.nn as nn
import torch.optim as optim

# Simulated backbone weight
w_backbone = torch.tensor([0.5], requires_grad=True)

# Learning rate 0.1 (normal for training from scratch)
optimizer = optim.SGD([w_backbone], lr=0.1)

# One step of gradient descent with large update
loss = w_backbone ** 2
loss.backward()
optimizer.step()

print(f"After 1 step with lr=0.1: weight={w_backbone.item():.6f}")
print(f"Weight moved far from pretrained value 0.5")

# With lr=0.001, updates are tiny
w_backbone = torch.tensor([0.5], requires_grad=True)
optimizer = optim.SGD([w_backbone], lr=0.001)
loss = w_backbone ** 2
loss.backward()
optimizer.step()

print(f"After 1 step with lr=0.001: weight={w_backbone.item():.6f}")
print(f"Weight stays close to pretrained 0.5")

Output:

Code Snippet
After 1 step with lr=0.1: weight=0.450000
Weight moved far from pretrained value 0.5
After 1 step with lr=0.001: weight=0.499500
Weight stays close to pretrained 0.5

Fix: Use 10–100× lower learning rates for the backbone than for the head. Start at 0.001 for the backbone, 0.01 for the head.

Conclusion

Transfer learning is the practical way to do deep learning on small datasets. Freezing the pretrained backbone and training only a new task-specific head is fast and often sufficient. Fine-tuning with differential learning rates allows adaptation when needed. Understanding when to freeze, when to unfreeze, and how to set learning rates turns pretrained models from black-box tools into controllable, efficient systems. Next: we'll move beyond feedforward models and explore recurrent architectures (LSTMs and GRUs) for sequence modeling.

Closing Takeaways

✓
Measure retrieval precision and recall in isolation before touching the model.
✓
Chunk along document structure, not arbitrary character counts.
✓
Combine vector and keyword search — hybrid retrieval beats either alone.
✓
Treat evaluation as continuous infrastructure, not a launch-week report.
Try It Yourself
A runnable Google Colab notebook with the eval harness and hybrid search code from this post.
#PyTorch#Transfer Learning#ResNet#Fine-tuning#Computer Vision
0 views
SS
Soham Sharma
AI Engineer at Botmartz, building enterprise RAG and agent systems in production. Contributing to open-source libraries.

Discussion (0)

No approved comments yet. Be the first to share your thoughts!

Leave a Comment

Your email address will not be published. Required fields are marked *

More Engineering Insights
AI AGENTS
9 min read
AI AGENTS
11 min read
OPEN SOURCE
6 min read
AI ENGINEERING
8 min read
CASE STUDIES
14 min read