0Pricing
Machine Learning Academy · Lesson

Feature Extraction: Freezing the Backbone

Learners will freeze all but the final classification head, train only new layers on a small custom dataset, and confirm dramatically reduced training time.

Feature Extraction: Freezing the Backbone is a free Machine Learning Academy lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Machine Learning Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Feature Extraction vs Fine-Tuning

Transfer learning has two main strategies. In feature extraction, the pre-trained backbone is completely frozen — its weights do not change during training. Only the new classification head, which you add on top, learns from your data. In fine-tuning, the entire network or at least some backbone layers are also updated.

Feature extraction is the right choice when your dataset is small (less than a few thousand images) or when your images are similar to ImageNet (natural photographs of everyday objects). It is much faster since gradients do not flow through the backbone, and it avoids destroying carefully learned features with noisy updates from too little data.

Freezing Parameters in PyTorch

In PyTorch, each parameter tensor has a requires_grad attribute. Setting it to False prevents gradient computation for that tensor, effectively freezing it. The simplest way to freeze all backbone parameters is to iterate over model.parameters() and set requires_grad = False, then replace the classification head (which starts with new random weights, so requires_grad=True by default).

This is efficient: PyTorch's autograd skips frozen parameters during the backward pass, reducing memory usage and speeding up training significantly compared to fine-tuning the whole network.

import torchvision.models as models
import torch.nn as nn

model = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V1)

# Freeze ALL backbone parameters
for param in model.parameters():
    param.requires_grad = False

# Replace the classification head (creates new trainable parameters)
num_classes = 10
model.fc = nn.Linear(model.fc.in_features, num_classes)
# model.fc.parameters() have requires_grad=True by default

trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
total = sum(p.numel() for p in model.parameters())
print(f'Trainable: {trainable:,} / Total: {total:,} ({trainable/total:.1%})')

Why Frozen Features Work Well

ImageNet pre-trained models learn a hierarchy of features: early layers detect low-level edges and colours, middle layers detect textures and parts, and later layers detect abstract objects. These features are general visual features that transfer broadly across image domains.

When your task involves natural images — flowers, animals, medical scans, satellite photos — these pre-learned features are far more informative than anything a randomly initialised network could extract from a small dataset. Feature extraction leverages this by treating the frozen backbone as a fixed feature transformer and only training a small linear classifier on top of the extracted features.

import torchvision.models as models
import torch

# Pre-extract features for all images (faster than forward-passing every epoch)
model = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V1)
model.fc = torch.nn.Identity()  # Remove classification head
model.eval()

# Extract 2048-dim features for all training images once
all_features, all_labels = [], []
with torch.no_grad():
    for images, labels in train_loader:
        features = model(images)  # Shape: (batch, 2048)
        all_features.append(features)
        all_labels.append(labels)

X_train = torch.cat(all_features)  # (N, 2048)
y_train = torch.cat(all_labels)    # (N,)

Training Only the Classification Head

Once features are frozen, training becomes fast. The optimiser only updates the new head's weights. You can either train a simple nn.Linear layer in PyTorch, or even pass the pre-extracted feature vectors to scikit-learn's LogisticRegression or SVC — both approaches work well for linear classification on top of rich pre-trained features.

Using a single linear layer is equivalent to training a logistic regression on the extracted features. For more complex tasks or when your classes require non-linear decision boundaries, you can use a small multi-layer head with ReLU activations and dropout between the frozen backbone and the output.

import torch.nn as nn
import torch.optim as optim

# Option 1: Simple linear head (logistic regression on features)
classifier = nn.Linear(2048, num_classes)
optimizer = optim.Adam(classifier.parameters(), lr=1e-3)
criterion = nn.CrossEntropyLoss()

# Option 2: Small MLP head for more complex tasks
mlp_head = nn.Sequential(
    nn.Linear(2048, 512),
    nn.ReLU(),
    nn.Dropout(0.3),
    nn.Linear(512, num_classes)
)

# scikit-learn option (useful for small datasets)
from sklearn.linear_model import LogisticRegression
clf = LogisticRegression(max_iter=1000, C=1.0)
clf.fit(X_train.numpy(), y_train.numpy())

DataLoader and Transform Setup

During feature extraction, image preprocessing must match the transforms used when training the original model. For ImageNet pre-trained models this means: resize to 256, centre-crop to 224×224, convert to tensor, and normalise with ImageNet mean and std.

When you use weights.transforms(), PyTorch automatically provides the correct preprocessing pipeline associated with those specific weights. This eliminates a common source of subtle bugs where you accidentally use wrong normalisation constants, which can reduce transfer learning accuracy by several percentage points.

from torchvision import transforms, datasets
from torch.utils.data import DataLoader
import torchvision.models as models

weights = models.ResNet50_Weights.IMAGENET1K_V1
preprocess = weights.transforms()  # Includes correct resize, crop, normalize

train_dataset = datasets.ImageFolder('data/train', transform=preprocess)
val_dataset   = datasets.ImageFolder('data/val',   transform=preprocess)

train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=4)
val_loader   = DataLoader(val_dataset,   batch_size=32, shuffle=False, num_workers=4)

print('Classes:', train_dataset.classes)
print('Training samples:', len(train_dataset))

Training Speed Comparison

Feature extraction is dramatically faster than full fine-tuning. When all backbone parameters have requires_grad=False, PyTorch does not compute gradients through them during loss.backward(), saving memory and computation proportional to the number of frozen layers.

For ResNet-50 (25M parameters, 2048 frozen features, only ~2K parameters in the head), a training epoch on a 5000-image dataset completes in seconds on CPU, compared to minutes for full fine-tuning. Pre-extracting features offline and then training only the head as a standard sklearn classifier is even faster, since the backbone forward pass only runs once per image.

import time

# Measuring training time difference
# With frozen backbone (feature extraction):
start = time.time()
for batch in train_loader:
    images, labels = batch
    with torch.no_grad():
        features = backbone(images)  # Fast: no grad tracking
    loss = criterion(classifier(features), labels)
    loss.backward()  # Gradients only through tiny classifier
    optimizer.step()
    optimizer.zero_grad()
print(f'Feature extraction epoch: {time.time()-start:.1f}s')

# Full fine-tuning trains 25M params instead of ~2K

Adapting EfficientNet for Feature Extraction

The approach is the same for EfficientNet: freeze all parameters, then replace the final classification layer. For EfficientNet the classifier is a Sequential block accessed via model.classifier, not model.fc. Always check the model architecture to find the correct attribute name.

EfficientNet-B0's classifier expects 1280-dimensional input features. When writing code that works with multiple architectures, introspect the final layer with in_features instead of hard-coding the dimension, making your code more reusable across different backbone choices.

import torchvision.models as models
import torch.nn as nn

model = models.efficientnet_b0(weights=models.EfficientNet_B0_Weights.IMAGENET1K_V1)

# Freeze backbone
for param in model.parameters():
    param.requires_grad = False

# EfficientNet uses model.classifier, not model.fc
print('Old classifier:', model.classifier)
in_features = model.classifier[1].in_features  # Access Linear inside Sequential
model.classifier = nn.Linear(in_features, num_classes)

trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f'Trainable parameters: {trainable:,}')

Adapting ViT for Feature Extraction

For ViT, the classification head is accessed via model.heads. The feature dimension from ViT-B/16's backbone is 768 (the transformer embedding dimension). After freezing all parameters, replace model.heads with a new nn.Linear(768, num_classes).

ViT benefits less from feature extraction than CNNs because its attention layers are more task-specific — they learn to attend to different image regions than what your task requires. If you use a ViT backbone, at minimum unfreeze the last few transformer encoder blocks for best performance. However, for a quick experiment, full feature extraction is still a valid starting point.

import torchvision.models as models
import torch.nn as nn

vit = models.vit_b_16(weights=models.ViT_B_16_Weights.IMAGENET1K_V1)

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

# Replace the classification head
print('Old head:', vit.heads)  # Sequential with Linear(768, 1000)
vit.heads = nn.Linear(768, num_classes)

print('ViT trainable params:', sum(p.numel() for p in vit.parameters() if p.requires_grad))

Evaluating Feature Extraction Results

After training with frozen features, evaluate on the validation set using standard classification metrics. A well-tuned feature extractor often achieves 90%+ accuracy on small custom datasets (500-5000 images per class) where training from scratch would overfit completely.

If accuracy is lower than expected, consider: (1) adding data augmentation during training, (2) using a dropout layer before the linear head, (3) using a slightly more expressive head (two linear layers with ReLU), or (4) moving to partial fine-tuning by unfreezing the last residual block. Monitor both train and validation accuracy to distinguish overfitting from underfitting.

from sklearn.metrics import classification_report
import torch

def evaluate(model, loader, device):
    model.eval()
    all_preds, all_labels = [], []
    with torch.no_grad():
        for images, labels in loader:
            images = images.to(device)
            logits = model(images)
            preds = logits.argmax(dim=1).cpu()
            all_preds.extend(preds.numpy())
            all_labels.extend(labels.numpy())
    print(classification_report(all_labels, all_preds,
                                target_names=class_names))

evaluate(model, val_loader, device)

When Feature Extraction Is Not Enough

Feature extraction works best when the source domain (ImageNet: natural photographs) and target domain are similar. When your data is very different — X-ray images, satellite imagery, microscopy, infrared thermal images — the low-level features still transfer (edge detectors are universal), but higher-level ImageNet features may be less useful.

Signs that feature extraction is underperforming: validation accuracy plateaus well below expectations, or train accuracy is much higher than validation (indicating the head is overfitting to too few features). In these cases, move to fine-tuning: unfreeze some backbone layers and train with a much lower learning rate to gradually adapt the pre-learned features to your domain.

Quick Check

Test your understanding of feature extraction with frozen backbones from this lesson.

Lesson Recap

In this lesson you learned: feature extraction freezes all backbone parameters and trains only a new classification head, giving fast training and strong results when your data resembles ImageNet, freezing is done by setting requires_grad=False on backbone parameters before replacing the final layer, and pre-extracting features offline is even faster by running the backbone once and caching the feature vectors. Next up we learn fine-tuning — how to carefully unfreeze backbone layers with low learning rates for even better accuracy.

Frequently asked questions

Is the “Feature Extraction: Freezing the Backbone” lesson free?

Yes — the full text of “Feature Extraction: Freezing the Backbone” is free to read here on the web, and the Machine Learning Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Machine Learning Academy course, upgrade to CoddyKit PRO.

What will I learn in “Feature Extraction: Freezing the Backbone”?

Learners will freeze all but the final classification head, train only new layers on a small custom dataset, and confirm dramatically reduced training time. You practise Machine Learning Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start Machine Learning Academy?

No prior experience is required. Machine Learning Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Feature Extraction: Freezing the Backbone” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this Machine Learning Academy lesson?

Yes. Every Machine Learning Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Pre-trained Models in torchvision: ResNet, EfficientNet, and ViT
  2. Feature Extraction: Freezing the Backbone
  3. Fine-Tuning: Unfreezing and Low Learning Rates
  4. Domain Adaptation: Medical Imaging with Scarce Labels
← Back to Machine Learning Academy