Search Tech Journey

Find topics, journeys and posts

back to blog
mlintermediate 120m read

DL S028 · Transfer Learning — Frozen Backbones and Fine-Tuning

Take an ImageNet-pretrained ResNet and adapt it to a custom dataset in minutes. Freeze vs fine-tune, discriminative LRs, and when transfer wins vs when it doesn't.

🧠SoftwareM05 · Convnets + vision· Session 028 of 130 120 min

🎯 Use an ImageNet-pretrained ResNet to solve a custom vision task with orders of magnitude less data than training from scratch. Understand feature-extractor vs fine-tune vs discriminative-LR regimes.

Series: Deep Learning & LLMs From Scratch — 80 sessions · Session 28 / 80 · Module M05

The story

Story hook · the ImageNet moment nobody expected

In 2013, one year after AlexNet won ImageNet, Matthew Zeiler and Rob Fergus published "Visualizing and Understanding Convolutional Networks" — the paper that first cracked open a trained convnet and asked what did it actually learn? They ran a technique called deconvnet to project neurons back to input space. Layer 1 filters looked like Gabor edges (surprise: same thing V1 in your visual cortex does). Layer 2 detected corners, colour blobs, T-junctions. Layer 3 detected textures. Layer 4 started detecting object parts — dog faces, car wheels. Layer 5 detected whole categories.

The practical implication landed hard. If layers 1–4 of a network trained on ImageNet learned features useful for detecting any natural image concept — not just the 1000 ImageNet classes — then you could take the trained network, chop off the last layer, and use it as a general-purpose feature extractor for any vision task. Ali Sharif Razavian, Hossein Azizpour, Josephine Sullivan, and Stefan Carlsson at KTH proved this brutally clearly with "CNN Features off-the-shelf: an Astounding Baseline for Recognition" (CVPR 2014). They took a pretrained AlexNet, ran every image through it, extracted the 4096-dimensional penultimate-layer activation, and trained a linear SVM on top. On 15+ vision benchmarks — object classification, scene classification, fine-grained bird species, face verification, attribute prediction — this trivial "linear probe" pipeline beat or matched every specialised hand-designed system.

The subtitle of that paper ("an astounding baseline") was not a joke. The community's reaction was a mixture of excitement and existential dread. Excitement because suddenly every small-data vision problem became tractable overnight. Dread because a decade of hand-engineered features had just been rendered obsolete by 5 minutes of feature extraction from someone else's network.

That paper is why every modern vision pipeline starts with torchvision.models.resnet18(weights='DEFAULT') or timm.create_model('convnext_base.fb_in22k_ft_in1k', pretrained=True). The transferability of learned features is the reason the entire deep-learning ecosystem is downloadable — HuggingFace Hub, torch.hub, timm. Every one of those weights files is someone else's compute that you get to reuse for free.

And — this is the important bit — the trick is not specific to vision. The exact same argument extends to language models. When you fine-tune Llama-3 or Gemma-3 on your domain, you're doing transfer learning: the model spent millions of GPU-hours learning generic English, and you spend a few GPU-hours specialising it for medical Q&A or SQL generation. Sessions 058–065 in this series will do that in detail. What you learn today about vision transfer maps directly to LLM fine-tuning.

The most useful trick in applied deep learning

Here's a claim that would have been controversial in 2013 and is now completely uncontroversial: you should almost never train a vision model from scratch on your own data. Not "usually". Almost never. If you have <100,000 labelled images (which is basically everyone outside FAANG), start from a model someone else pretrained on ImageNet, LAION, or DINOv2, and fine-tune. You'll get better accuracy in less time with less compute.

This is transfer learning. It works because the early layers of any well-trained convnet learn edge detectors, blob detectors, colour opponents — features that are useful for any natural-image task. Only the last few layers are task-specific. So you keep 95% of the pretrained network and retrain only the 5% that cares about "cat vs dog" vs "malignant vs benign melanoma" vs "wing icing on a Cessna".

Today we do it three ways and understand when each is right.

You will be able to
  • Load a pretrained ResNet-18 from torchvision and swap its final layer for your own task.
  • Explain the three fine-tuning regimes: linear probe, full fine-tune, discriminative LR.
  • Know when to freeze BatchNorm layers (small target dataset, low target LR) and when to unfreeze them.
  • Pick a sensible LR for the head vs backbone (differ by 10× typically).
  • Diagnose the 'catastrophic forgetting' failure mode and how to avoid it.

Prerequisites

  • S027 (a working CIFAR training pipeline). We'll adapt it directly.
  • S026 (ResNet architecture — you should know why layer4 is the "last" backbone stage).


1 · What "transfer" actually means

Pretraining on ImageNet gives you a network whose layers look, in order:

  • Early conv layers (stem + layer1): edge detectors, colour blobs, orientation-selective filters. Universal — useful for every natural-image task.
  • Middle layers (layer2, layer3): texture, motif, part detectors. Semi-universal — useful for most tasks but slowly get more ImageNet-specific.
  • Late layers (layer4): object-level features (dog-face-ness, wheel-ness, plant-ness). ImageNet-specific — may not help if your task is X-rays or satellite imagery.
  • Final FC: literal ImageNet class scores. Always thrown away.
The analogy
🌍 Real world
💻 Code world

1.1 The three regimes

RegimeWhat's trainableWhen to use
Linear probeOnly the new final FCTarget dataset tiny (<1000 imgs) or you want a fast baseline
Full fine-tuneEverythingTarget dataset ~10K+ imgs, GPU budget available
Discriminative LREverything, but backbone gets 10× smaller LR than headBest of both worlds; nearly always the right default

2 · The scaffold — pretrained ResNet-18 with a swapped head

import torch
import torch.nn as nn
import torchvision
from torchvision import transforms
 
# 1. Get a pretrained model
model = torchvision.models.resnet18(weights=torchvision.models.ResNet18_Weights.IMAGENET1K_V1)
 
# 2. Replace the final classifier
num_features = model.fc.in_features         # 512 for ResNet-18
model.fc = nn.Linear(num_features, num_classes_you_have)   # e.g. num_classes=37 for Oxford-Pets
 
model = model.cuda()

That's it. The 25MB of weights you just downloaded are the result of 90 GPU-days of ImageNet training that someone else paid for. You'd never train that from scratch on your own hardware.

2.1 Preprocessing must match pretraining

Critical: the input pipeline must match the mean, std, and pixel range that the pretrained model saw. For torchvision's ImageNet models, that's:

IMAGENET_MEAN = (0.485, 0.456, 0.406)
IMAGENET_STD  = (0.229, 0.224, 0.225)
 
train_tfm = transforms.Compose([
    transforms.Resize(256),
    transforms.RandomResizedCrop(224),
    transforms.RandomHorizontalFlip(),
    transforms.ToTensor(),
    transforms.Normalize(IMAGENET_MEAN, IMAGENET_STD),
])
 
test_tfm = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(IMAGENET_MEAN, IMAGENET_STD),
])

If you feed the model images normalized with your own dataset's mean, the BatchNorm running statistics from ImageNet will be badly mis-calibrated and your accuracy will be inexplicably low. Every torchvision model has a weights.transforms() method that returns the correct preprocessor — use it if you're unsure.


3 · Regime 1 — Linear probe (freeze the backbone)

for p in model.parameters():
    p.requires_grad = False
# Unfreeze only the new head
for p in model.fc.parameters():
    p.requires_grad = True
 
opt = torch.optim.AdamW(model.fc.parameters(), lr=1e-3, weight_decay=1e-4)

Because only the head is trainable, memory usage drops sharply — no gradients or optimizer state for the frozen weights. On a 1000-image target dataset this can train in ~2 minutes and reach 85–90% of what full fine-tuning achieves.

3.1 The BatchNorm gotcha in the frozen regime

Setting requires_grad = False on BN weights does NOT put BN in eval mode. BN in .train() mode still updates its running statistics from your target dataset's batch stats, which will drift the model. Fix:

def freeze_bn(model):
    for m in model.modules():
        if isinstance(m, nn.BatchNorm2d):
            m.eval()
            for p in m.parameters():
                p.requires_grad = False
 
model.train()
freeze_bn(model)   # must call AFTER model.train()

Call this at the start of every epoch (because model.train() re-arms all modules). Without it, your linear probe will slowly degrade over training as BN drifts. Silent bug, ~1–2% accuracy loss.

Try itRace a linear probe against a from-scratch head on 200 images

Grab any small torchvision dataset — torchvision.datasets.OxfordIIITPet works, keep only 200 images — and run two 10-epoch experiments back-to-back:

  1. Pretrained + linear probe. model = resnet18(weights="IMAGENET1K_V1"), swap the head to nn.Linear(512, num_classes), freeze the backbone, train ONLY the head for 10 epochs.
  2. From scratch. Same architecture, same head, but weights=None. Everything trainable, 10 epochs.

Compare final test accuracy. The pretrained linear probe will typically reach 70–85% while the from-scratch model is barely above chance. The delta is the entire point of pretraining — you're renting 1.28M labelled ImageNet images for free. Now try one more variant: unfreeze the last stage (layer4) and train it with a 10× smaller LR than the head. That's a discriminative-LR fine-tune — note how much of the additional gain it recovers over the linear probe.

💡 Hint · Same dataset, same epochs, same optimizer — only the initial weights differ.

4 · Regime 2 — Full fine-tune

Everything trainable, small LR, short schedule:

for p in model.parameters():
    p.requires_grad = True
 
opt = torch.optim.SGD(model.parameters(),
                       lr=1e-2,          # 10× smaller than "from scratch"
                       momentum=0.9,
                       weight_decay=1e-4,
                       nesterov=True)
 
sched = torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max=10)   # 10 epochs

Two key rules:

  • LR ~10× smaller than from scratch. If from-scratch used lr=0.1, fine-tune uses 1e-2. Too high and you catastrophically forget the pretrained features.
  • Fewer epochs. 5–20 is standard, vs 90–300 for from-scratch.

5 · Regime 3 — Discriminative learning rates (the modern default)

Head gets 10× the backbone's LR, because the head is randomly initialized (needs big steps) while the backbone is near-optimal (only needs small nudges).

backbone_params, head_params = [], []
for name, p in model.named_parameters():
    if name.startswith("fc."):
        head_params.append(p)
    else:
        backbone_params.append(p)
 
opt = torch.optim.SGD([
    {"params": backbone_params, "lr": 1e-3},
    {"params": head_params,     "lr": 1e-2},
], momentum=0.9, weight_decay=1e-4, nesterov=True)

Even better: geometrically increasing LR per stage. The layer1 (very early features) gets the smallest LR, layer4 (more task-specific) gets more, and the head gets the most.

groups = []
for i, module in enumerate([model.stem if hasattr(model,'stem') else model.conv1,
                            model.layer1, model.layer2, model.layer3, model.layer4,
                            model.fc]):
    lr = 1e-4 * (10 ** (i * 0.5))    # ~3× per stage
    groups.append({"params": list(module.parameters()), "lr": lr})
opt = torch.optim.SGD(groups, momentum=0.9, weight_decay=1e-4)

This is the "gradually unfreeze" recipe from fast.ai and it consistently wins by ~1% over a single-LR fine-tune.


6 · When does transfer NOT help?

Transfer helps ~always in the 100 → 10K sample regime. It stops mattering (and can even hurt) in two cases:

  1. Target task is very different from source. ImageNet is natural photos. If your task is medical grayscale (chest X-rays), satellite multispectral, or grayscale document images, ImageNet features may transfer poorly. Try domain-specific pretraining (e.g., RadImageNet for chest, SentSat for satellite).
  2. Target dataset is huge. With >1M task-specific images, from-scratch is fine and sometimes better (no risk of inheriting bad biases).

For everything in between — which is 99% of applied projects — transfer is the right default.


War stories

War story The pretrained-model normalization bug

Every 6 months I encounter a colleague debugging "why does my fine-tune underperform from-scratch". Almost always: they used torchvision.transforms.Normalize with mean/std from their own dataset (e.g. [0.5, 0.5, 0.5]) instead of ImageNet's [0.485, 0.456, 0.406]. The pretrained BN stats are calibrated for ImageNet input distribution — feed it something else and every downstream feature is 5–10% off.

War story Catastrophic forgetting on a tiny dataset

Fine-tuned a ResNet on 200 dog-breed images with lr=0.1 (my usual from-scratch LR). Loss went to 0.001 in one epoch, and test accuracy dropped from ImageNet-baseline 60% (via 1-nn on pretrained features) to 30% on my target task. The full-strength gradient wiped out the pretrained features in half a pass. Rule: fine-tuning LR should be 10–100× smaller than from-scratch. Set your inner alarm for anything ≥1e-2.


7 · The 2025 pulse · pretraining has eaten transfer learning

Here is the shift that has happened over the last three years and that most tutorials haven't caught up to: for serious vision work in 2025, you don't fine-tune from ImageNet supervised pretraining anymore. You fine-tune from a self-supervised backbone.

The reason: supervised ImageNet pretraining maxes out around 88% ImageNet top-1 with ~1M labelled images. Self-supervised methods have shown they can learn features that transfer better by consuming unlabelled data at 100×–10000× the scale.

The three self-supervised paradigms that matter:

  1. Contrastive learning — SimCLR / MoCo / DINO. Two augmented views of the same image should have similar embeddings; two random images should not. SimCLR (Chen et al. 2020) was the breakthrough. DINO (Caron et al., FAIR 2021) refined the idea for ViT and became the reference approach.
  2. Masked image modelling — MAE. Mask out 75% of the patches of an image, ask the model to reconstruct them. MAE (He, Chen et al. 2021). Trivially scalable, no negative pairs needed. Now the default pretraining objective for ViT-scale models.
  3. Vision-language contrastive — CLIP / SigLIP. Pair each image with its caption; maximise the dot product between matched image-text embeddings. CLIP (Radford et al., OpenAI 2021) trained on 400M image-text pairs; SigLIP (Zhai et al., DeepMind 2023) uses a sigmoid loss instead of softmax for better scalability; **SigLIP-2 (Tschannen et al., DeepMind 2025)** adds decoder-based captioning objectives and self-distillation — the current SOTA multilingual vision-language backbone.

The 2024–2025 recommended stack — what to actually fine-tune from:

  • DINOv2 (Oquab et al., Meta AI 2023) — ViT-g/14 trained self-supervised on 142M curated images. Frozen features + linear probe already matches most supervised backbones. Fine-tuned features beat everything on segmentation, depth estimation, and dense-prediction tasks. If you're doing dense-prediction vision in 2025, this is the default.
  • SigLIP-2 (2025) — vision encoder used inside Gemma 3, PaliGemma 2, and a growing list of open multimodal models. Great when you need image↔text alignment (VQA, captioning, multimodal retrieval).
  • EVA-02 (Fang et al. 2023) — combines MAE pretraining with CLIP-based feature distillation. Strong classification + dense-prediction.
  • SAM 2 (Ravi et al., Meta AI 2024) — Segment Anything's video-and-image successor. Not a classification backbone but the reference for segmentation transfer.

The practical decision tree in 2025:

  1. Small custom dataset (< 5K labelled images), simple classification: torchvision.models.resnet18(weights='DEFAULT') + linear probe. Still works. Still fine.
  2. Medium dataset (5K–100K), classification or moderately complex task: timm.create_model('convnext_base.fb_in22k_ft_in1k', pretrained=True) + discriminative-LR fine-tune. Reliable 85-92% territory.
  3. Any dense-prediction task (segmentation, depth, detection): DINOv2 frozen features + task-specific head. Or fine-tune DINOv2 end-to-end with a very small LR.
  4. Multimodal (image + text): SigLIP-2 encoder + LLM head. This is how PaliGemma 2, Molmo, Qwen2-VL, and the newer Llama 4 vision variant are built.

A note on when transfer stops helping: if your target domain is very different from natural images — medical microscopy, satellite imagery, radar, molecular structure — ImageNet or LAION features are less useful. Domain-specific pretrained models exist: RadImageNet, SatMAE, MoleculeNet. Use those when available. If you have >1M labelled samples in your domain, you're now in "pretrain your own" territory.

Further reading · transfer & self-supervision in 2025

Transfer learning — what actually stuck

    Common misconception
    ✗ What most people think

    "I set requires_grad=False on the backbone, so it's frozen. Its outputs will be identical every epoch — I could cache them if I wanted."

    ✓ What is actually true

    Only the parameters are frozen. BatchNorm's running mean and variance are buffers, not parameters, and they are updated by the forward pass whenever the module is in training mode — entirely independently of gradients or of requires_grad. So a "frozen" backbone keeps recalibrating its normalisation statistics to your new dataset, epoch after epoch, and its features drift.

    Why the myth is so sticky

    The myth is seductive because requires_grad=False genuinely is the complete answer for every layer except the ones carrying buffers — which is to say, it is the complete answer for a linear layer, a convolution, an embedding, and every layer you would use to first learn what freezing means. The mental model "no gradient means no change" is correct for parameters and simply does not cover a second update mechanism that exists in parallel. And PyTorch does nothing to flag it: no warning, no error, and the training loss still falls. You discover it when a linear probe scores differently on the second epoch than on the first, which looks like ordinary run-to-run variation rather than a bug.

    Prove it to yourself

    Gradients off, statistics still moving:

    import torch, torch.nn as nn
    bn = nn.BatchNorm1d(4)
    for p in bn.parameters():
        p.requires_grad = False
    
    before = bn.running_mean.clone()
    bn.train()
    _ = bn(torch.randn(16, 4) * 10 + 5)     # forward only, no backward
    print(torch.equal(before, bn.running_mean))   # False -- buffer moved
    
    bn.eval()
    _ = bn(torch.randn(16, 4) * 10 + 5)
    print(torch.equal(bn.running_mean.clone(), bn.running_mean))  # eval: frozen
    From first principles
    Start with the question

    Why does transfer learning work at all? A network trained to separate a thousand ImageNet categories has no reason to be useful for your medical scans or your product photos — yet it reliably is.

    1. 1
      The first layers of a trained convolutional network converge to edge detectors, colour blobs, and oriented gradients — and they do so almost regardless of the dataset or the task.
      forced by · those are the features that carry the most information about natural images generally, so any objective on natural images pushes toward them
    2. 2
      That convergence is not a property of the labels. It is a property of the input statistics: natural images share low-level structure whether they are cats, chest X-rays, or circuit boards.
      forced by · edges and local texture arise from how light and surfaces work, not from what the objects happen to be called
    3. 3
      Later layers compose those primitives into progressively more task-specific combinations, so specificity to the original labels increases monotonically with depth.
      forced by · the classification loss is applied at the end, so gradient pressure toward task-specific representation is strongest nearest the head
    4. 4
      Therefore "how much of this network transfers" is really the question "at what depth does generic structure end and source-task specificity begin" — and that depth depends on how similar your images are to the source images.
      forced by · the shared-statistics argument only holds as far as the statistics actually are shared
    5. 5
      Fine-tuning is then a decision about where to cut: freeze up to the point where features are still generic for your domain, and retrain from there.
      forced by · frozen layers are free and cannot overfit; retrained layers cost data and can
    ⇒ Therefore

    Transfer works because early visual features are determined by image statistics rather than by labels, and image statistics are shared far more widely than labels are.

    And note what this predicts: transfer should degrade smoothly as your domain moves away from natural images, and it should degrade from the top down — deep layers stop being useful before shallow ones do. It also predicts the right response to a distant domain is to freeze less, not more. Verify the shape by running a linear probe on features taken from several different depths of a frozen backbone: on a near domain the deepest features win, on a far domain an intermediate layer wins. Where that peak sits is a direct measurement of how far your domain is from the source.

    Mental modelSpecificity increases with depth; cut where it stops helping

    Picture the backbone as a gradient from generic to specific. Bottom: edges and colour, useful for any image ever taken. Middle: textures and parts. Top: whole-object detectors tuned to the source label set. Your new task reuses the bottom for free and needs the top rebuilt.

    The only real decision in transfer learning is where along that gradient you place the cut — and dataset size decides it.

    • Small and similar dataset: freeze everything, train a linear head. Fewest trainable parameters, least opportunity to overfit.
    • Large and similar: fine-tune everything at a small learning rate. You have enough data to move the whole network without destroying it.
    • Different domain: fine-tune deeper, or all of it — the top layers were fitted to labels that do not apply to you.
    • Discriminative learning rates split the difference: low LR at the bottom, higher toward the head, so early layers are nudged while the head is genuinely trained.
    🔔 Fires when you see

    Fire this model the moment you see: a dataset with a few thousand labelled examples · "we don't have enough data to train from scratch" · a fine-tune that is somehow worse than a linear probe (learning rate too high — you erased the features) · a frozen backbone whose outputs are not reproducible · preprocessing that does not match what the checkpoint was pretrained with.

    The tradeoff

    Linear probe on a frozen backbone, full fine-tune, or discriminative learning rates?

    Linear probe
    + you gain trains in a fraction of the time, cannot overfit much because there are so few parameters, and the features can be extracted once and cached so subsequent epochs are nearly free
    − you pay caps accuracy at whatever a linear function of the frozen features can achieve; if the domain differs from the source, that ceiling can be low
    pick when you have on the order of a few hundred to a few thousand labelled examples in a domain close to the source — and always as the first baseline, because it establishes the floor in minutes
    Full fine-tune
    + you gain the highest ceiling — every layer can adapt, including the early ones if your domain genuinely needs different low-level features
    − you pay with too little data or too high a learning rate you overwrite pretrained features faster than you can relearn them, ending up worse than the probe; and every parameter needs gradients and optimizer state
    pick when your dataset is large enough that the model would train respectably from scratch — in that regime pretraining is a better starting point rather than a crutch
    Discriminative learning rates
    + you gain early layers move slowly enough to be preserved while the head trains at a proper rate; usually captures most of the full fine-tune's benefit without the catastrophic-forgetting risk
    − you pay parameter groups to construct and a per-group ratio to choose, and it is harder to reason about what actually changed
    pick when the dataset is mid-sized — too big for the probe's ceiling, too small to fine-tune everything safely — which is where most real applied problems land
    What a senior engineer actually does

    Run the linear probe first, always. It takes minutes and gives you a number that tells you whether the pretrained features are any good for your domain at all. If the probe is already close to your target, everything else is optimisation. If the probe is terrible, that is information: your domain is far from the source and you should be unfreezing more, not less.

    Then unfreeze progressively rather than all at once. And before any of it, check that your preprocessing exactly matches the checkpoint's pretraining — wrong normalisation constants degrade every one of these options silently, and no amount of learning-rate tuning will recover it.


    Retention scaffold

    Transfer learning · click to reveal · click to reveal
    ★ = stretch question

    One-line summary (write it in your own words): ______________________________________

    Spaced review: re-write the discriminative-LR loop from memory in 24 hours. On day 7, take a small target dataset of your choice and run all three regimes end-to-end; log the accuracy differences.

    Next session (S029): Data augmentation deep dive — RandAugment, MixUp, CutMix, and why they're worth 3-5% on any vision task.

    Sticky note:

    Head LR = 10× backbone LR · weights.transforms() · Freeze BN properly — the three-line summary of applied transfer learning in 2025.


    Legacy recall drills

    Retention scaffold

    Recall

    1. What are the three fine-tuning regimes, in order of "how much you change"?

    Linear probe (only the head), discriminative LR (head + backbone with different LRs), full fine-tune (everything at one LR). Almost always use discriminative LR.

    2. Why do you need to explicitly call .eval() on BN modules when freezing the backbone?

    requires_grad = False disables gradient updates but does not stop BN from updating its running mean/variance from batch stats during .train() mode. If you don't force BN into eval, running stats drift and inference-time accuracy degrades.

    3. Rule of thumb for fine-tune LR vs from-scratch LR?

    10× smaller. If from-scratch used 0.1, fine-tune uses 0.01. Too high and you catastrophically forget.

    4. What preprocessing do you need to match when using a torchvision pretrained model?

    The exact mean, std, and input resolution the model was trained on. weights.transforms() gives you the correct pipeline.

    5. When does transfer learning stop helping?

    When target domain is very different from source (medical/satellite/microscopy) or when target dataset is very large (millions of samples).

    Stretch

    Fine-tune a pretrained ResNet-18 on Oxford-IIIT Pets (37 classes, 200 imgs per class). Compare linear probe vs full fine-tune vs discriminative LR. Aim for 90%+ with discriminative LR, in 10 epochs, on a laptop GPU.

    In your own words

    Write 3 sentences: "when would I fine-tune vs train from scratch?"

    Spaced review

    • S021 (BatchNorm) — the .eval() gotcha lives at the intersection of BN and freezing.
    • S023 (LR schedules) — cosine, applied over 10 epochs instead of 100.

    Next session

    S029 — Data augmentation deep dive. RandAugment, Mixup, CutMix, and why they are worth 3–5% on any vision task.

    Bring back tomorrow

    • Discriminative LR: backbone 1e-3, head 1e-2, cosine over ~10 epochs.
    • weights.transforms() for the preprocessing.
    • Freeze BN with .eval() explicitly, not just requires_grad=False.

    Previous: ← DL S027 · Next: DL S029 →