Danh sách bài viết

Bài 32: Transfer Learning — fine-tune ResNet pretrained

Transfer Learning là cách tận dụng một model đã được train trên dataset lớn (thường ImageNet) cho một task mới với ít data hơn. Thay vì train từ đầu hàng triệu tham số, ta giữ lại backbone đã học general features (edge, texture, shape) và chỉ tinh chỉnh phần cuối. Bài này đi qua hai strategy chính (feature extraction và fine-tuning), quy tắc chọn strategy theo data size và domain, code PyTorch fine-tune ResNet-18 trên CIFAR-10, differential learning rate, các thư viện torchvision / timm / HuggingFace, và limitations khi domain mới khác xa ImageNet.

24/05/2026
14 phút đọc
1 lượt xem
1

Mục tiêu bài học

Sau bài học, bạn sẽ:

  • Giải thích được Transfer Learning là gì và vì sao backbone pretrained giúp ít data vẫn cho accuracy cao.
  • Phân biệt rõ hai strategy: feature extraction (freeze) và fine-tuning (tune nhẹ backbone).
  • Chọn được strategy phù hợp dựa trên data size và độ tương đồng giữa task mới với ImageNet.
  • Load và fine-tune ResNet pretrained bằng torchvision.models.
  • Cấu hình differential learning rate cho backbone và head.
  • So sánh kết quả TL với train from scratch trên CIFAR-10.

Bài này nối Bài 31 — Xây dựng CNN CIFAR-10 (train from scratch) với Bài 33 — Các kiến trúc CNN nổi bật.

2

Transfer Learning là gì

Transfer Learning là kỹ thuật chuyển kiến thức từ một model đã train trên dataset nguồn (source) sang task đích (target) với data ít hơn. Trong computer vision, source thường là ImageNet (1.28 triệu ảnh, 1000 class) — Russakovsky et al., 2015.

Quy trình điển hình:

  1. Lấy backbone đã pretrain trên ImageNet (ResNet, EfficientNet, ViT...).
  2. Bỏ classifier head cũ (1000 class).
  3. Gắn head mới phù hợp số class của task đích.
  4. Train trên data đích — có thể freeze hoặc tune backbone.

Yosinski et al. (2014) — "How transferable are features in deep neural networks?" (arXiv:1411.1792) — đã chứng minh feature ở layer thấp (gần input) transferable giữa các task, trong khi layer cao phụ thuộc vào task gốc.

3

Vì sao Transfer Learning hiệu quả

Có ba lý do cụ thể:

  • Backbone đã học general features: layer đầu phát hiện edge / blob / color gradient, layer giữa học texture và part, layer cuối học object. Edge với texture là tín hiệu chung cho phần lớn ảnh tự nhiên — không phải học lại từ đầu.
  • Ít data đủ dùng: với 100–1000 ảnh / class, train from scratch thường overfit nặng. Backbone pretrained đóng vai trò "regularizer mạnh" qua khởi tạo có cấu trúc.
  • Hội tụ nhanh và accuracy cao hơn: model bắt đầu ở vùng tham số tốt, gradient nhỏ, vài epoch là đạt accuracy mà train from scratch cần hàng chục epoch.

Ví dụ trong Kornblith, Shlens & Le (2019) — "Do Better ImageNet Models Transfer Better?" (arXiv:1805.08974): trên 12 dataset downstream, model có accuracy ImageNet cao hơn thường cho accuracy transfer cao hơn (correlation rõ rệt nhưng không tuyệt đối).

4

Strategy 1 — Feature Extraction

Feature Extraction coi backbone pretrained như một bộ trích xuất feature cố định:

  • Freeze backbone: đặt requires_grad = False cho mọi tham số ngoài head. Optimizer sẽ không cập nhật chúng.
  • Replace classifier head: lớp FC cuối (1000 chiều cho ImageNet) thay bằng FC mới ứng với số class của task đích.
  • Train chỉ head trên data mới. Backward vẫn chạy qua backbone để tính gradient cho head, nhưng weight backbone giữ nguyên.

Đặc điểm:

  • Train rất nhanh — chỉ vài chục nghìn param của head được update.
  • VRAM thấp — không cần lưu gradient cho backbone (PyTorch tự bỏ qua khi requires_grad=False).
  • Ít risk overfit khi data nhỏ.

Use case: data rất ít (vài trăm ảnh), task tương tự ImageNet (vd phân loại loài chó, loài hoa, đồ vật thông thường).

5

Strategy 2 — Fine-tuning

Fine-tuning cho phép backbone được cập nhật nhẹ trên data mới:

  • Replace classifier head (giống Strategy 1).
  • Không freeze backbone, hoặc chỉ freeze vài layer đầu.
  • Train cả backbone với LR thấp (vd 1e-5) + head với LR cao hơn (vd 1e-3).
  • Backbone được "tune nhẹ" để thích nghi với domain mới mà không phá hỏng feature đã học.

Tại sao LR thấp cho backbone? Vì gradient lớn có thể làm backbone "quên" general features (catastrophic forgetting). LR thấp giúp chuyển dịch chậm trong không gian tham số.

Use case: data nhiều hơn (vài nghìn ảnh trở lên), task khác xa ImageNet nhưng vẫn là ảnh tự nhiên (vd phân loại sản phẩm e-commerce, mèo theo giống đặc biệt).

6

Quy tắc chọn strategy theo data + domain

Hai trục cần xem: data sizekhoảng cách domain với ImageNet.

Data sizeDomain so với ImageNetStrategy
Rất ít (< 1k ảnh)Tương tựFeature Extraction — freeze backbone, train head
Vừa (1k – 10k)Tương tựFine-tune vài layer cuối backbone + head
Nhiều (> 10k)Tương tựFine-tune full backbone với LR thấp
Nhiều (> 10k)Khác xa (medical, satellite)Fine-tune full backbone, LR vừa; cân nhắc domain-specific pretrain
Rất nhiều (> 100k)Khác hoàn toànCó thể train from scratch hoặc init từ pretrained tùy benchmark
Rất ítKhác xaTrường hợp khó — TL có thể không giúp; cần augment mạnh hoặc thu thêm data

Quy tắc nhanh: data càng ít thì freeze càng nhiều; domain càng xa thì tune càng nhiều layer. Khi không chắc, làm baseline cả hai và so sánh trên validation set.

7

Load pretrained ResNet trong PyTorch

torchvision.models cung cấp hàng loạt model với pretrained weights:

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

# Load ResNet-18 với weight tốt nhất hiện có
model = models.resnet18(weights="DEFAULT")

# Hoặc chỉ định version cụ thể
model_v2 = models.resnet50(weights="IMAGENET1K_V2")

# Không load weight (train from scratch)
model_blank = models.resnet18(weights=None)

API weights= được giới thiệu từ torchvision v0.13 (2022), thay cho cú pháp cũ pretrained=True (đã deprecated). Mỗi enum weight đi kèm metadata (accuracy, transforms khuyến nghị).

8

Freeze backbone, replace head

Pattern chuẩn cho Feature Extraction:

import torch.nn as nn
from torchvision import models

model = models.resnet18(weights="DEFAULT")

# 1) Freeze toàn bộ backbone
for param in model.parameters():
    param.requires_grad = False

# 2) Replace head — fc cuối của ResNet
num_classes = 10
model.fc = nn.Linear(model.fc.in_features, num_classes)

# Sau bước này, chỉ model.fc có requires_grad=True

Tại sao chỉ model.fc còn requires_grad=True? Vì khi gán nn.Linear mới, layer này có weight được khởi tạo lại với requires_grad=True mặc định, ghi đè lên layer cũ đã bị freeze.

Với architecture khác, tên head khác nhau. Vài ví dụ:

  • ResNet, ResNeXt: model.fc
  • VGG, AlexNet: model.classifier[-1]
  • EfficientNet: model.classifier[1]
  • ViT: model.heads.head

Luôn print(model) để xem cấu trúc trước khi thay head.

9

Inspect requires_grad

Cách kiểm tra layer nào đang train, layer nào freeze:

for name, param in model.named_parameters():
    print(f"{name:40s} requires_grad={param.requires_grad}")

# Đếm số param sẽ được train
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:,}")

Với ResNet-18 freeze backbone + head 10 class: trainable khoảng 5,130 / total khoảng 11,181,642. Tỷ lệ train chưa đến 0.05%.

Tip: khi truyền vào optimizer, lọc luôn param đã freeze để tránh warning:

trainable_params = [p for p in model.parameters() if p.requires_grad]
optimizer = torch.optim.AdamW(trainable_params, lr=1e-3)
10

Pretrained weights và cache

  • weights="DEFAULT": weight tốt nhất hiện có. Khuyến nghị cho phần lớn use case.
  • weights="IMAGENET1K_V1": phiên bản đầu tiên (training recipe gốc).
  • weights="IMAGENET1K_V2": training recipe cải tiến (TrivialAugment, longer training, label smoothing). Thường accuracy cao hơn V1 vài %.
  • weights=None: không load — train from scratch.

Lần đầu gọi, PyTorch auto download và cache trong ~/.cache/torch/hub/checkpoints/ (Linux/macOS) hoặc %USERPROFILE%/.cache/torch/hub/checkpoints/ (Windows). Lần sau load từ cache.

Xem accuracy ImageNet của từng weight:

from torchvision.models import ResNet50_Weights

print(ResNet50_Weights.IMAGENET1K_V2.meta["_metrics"])
# {'ImageNet-1K': {'acc@1': 80.858, 'acc@5': 95.434}}
11

Common pretrained backbone

Họ modelVariant phổ biếnGhi chú
ResNet18 / 50 / 101 / 152Baseline cổ điển; 18 nhẹ, 50 cân bằng, 152 deep
EfficientNetB0 → B7Compound scaling, B0 nhẹ nhất, B7 mạnh nhưng nặng
MobileNetV2 / V3-small / V3-largeCho edge / mobile, depthwise separable conv
ViTvit_b_16, vit_l_16Vision Transformer, cần data lớn hoặc pretrained tốt
ConvNeXttiny / small / base / largeModern CNN, accuracy ngang ViT
RegNetregnet_y_8gf, ...Họ model do Facebook AI thiết kế

Quy tắc thực dụng: bắt đầu bằng ResNet-50 (cân bằng compute / accuracy). Nếu cần nhẹ cho mobile, dùng MobileNet hoặc EfficientNet-B0. Nếu có nhiều GPU và muốn accuracy cao, thử ConvNeXt hoặc ViT.

12

Pre-processing theo pretrained

Mỗi pretrained model được train với một pipeline pre-processing cụ thể (resize, normalize). Phải áp dụng pipeline tương ứng khi inference / fine-tune; nếu sai, accuracy giảm rõ rệt.

Stats normalize chuẩn ImageNet:

mean = [0.485, 0.456, 0.406]
std  = [0.229, 0.224, 0.225]

torchvision cung cấp helper weights.transforms() trả về đúng pipeline:

from torchvision.models import ResNet18_Weights

weights = ResNet18_Weights.DEFAULT
preprocess = weights.transforms()
# preprocess là một callable: img -> tensor đã resize + normalize
print(preprocess)
# ImageClassification(
#     crop_size=[224]
#     resize_size=[256]
#     mean=[0.485, 0.456, 0.406]
#     std=[0.229, 0.224, 0.225]
#     interpolation=InterpolationMode.BILINEAR
# )

Khi fine-tune với input nhỏ (vd CIFAR 32x32), thường resize lên 224 hoặc giảm input size phù hợp (vd 96, 128) tùy compute. Resize quá thô có thể làm mất chi tiết — luôn so sánh accuracy theo nhiều input size.

13

Differential learning rate

Khi fine-tune cả backbone, ta thường chia thành 2 nhóm param với LR khác nhau:

import torch

# Head: train mới hoàn toàn -> LR cao
# Backbone: tune nhẹ -> LR thấp
optimizer = torch.optim.AdamW([
    {"params": model.fc.parameters(), "lr": 1e-3},
    {"params": [p for n, p in model.named_parameters() if "fc" not in n],
     "lr": 1e-5},
], weight_decay=1e-4)

Lý do: head có weight ngẫu nhiên — cần gradient lớn để học từ đầu; backbone đã ở vùng tham số tốt — chỉ cần dịch chuyển nhẹ. Tỷ lệ phổ biến: lr_head / lr_backbone = 10x đến 100x.

Trong PyTorch, mỗi dict trong params là một "param group" độc lập, có thể đặt cả lr, weight_decay, momentum riêng:

optimizer.param_groups[0]["lr"]   # 1e-3
optimizer.param_groups[1]["lr"]   # 1e-5
14

Layer-wise LR decay và discriminative LR

Layer-wise LR decay (LLRD): LR giảm dần từ head xuống layer đầu — layer càng sâu (gần input) càng giữ general features, không nên tune mạnh. Công thức điển hình:

\[ \text{lr}_l = \text{lr}_\text{head} \cdot \alpha^{(L - l)} \]

Với \( L \) là số block, \( l \) là index từ đầu, \( \alpha \approx 0.65 \div 0.9 \). Phổ biến khi fine-tune ViT, BERT — He et al. (2022), Howard & Ruder (2018) ULMFiT (arXiv:1801.06146).

Discriminative LR (fast.ai): chia model thành 3 nhóm — early / middle / late layer — mỗi nhóm một LR riêng. Đơn giản hơn LLRD, hiệu quả gần tương đương trong nhiều benchmark.

Trong thực tế, với CNN nhỏ (ResNet-18, ResNet-50) và dataset cỡ trung, differential LR 2 nhóm (head vs backbone) thường đủ. LLRD chỉ thực sự cần với model rất sâu (ViT-Large, ConvNeXt-Base).

15

timm — vision model collection

timm (PyTorch Image Models, Wightman) là thư viện tổng hợp hàng ngàn vision model với pretrained weight, API thống nhất.

pip install timm
import timm

# Liệt kê model có pretrained
print(len(timm.list_models(pretrained=True)))  # > 1000

# Tạo model với head tùy chỉnh
model = timm.create_model(
    "resnet50",
    pretrained=True,
    num_classes=10,        # tự thay head
)

# Hoặc với EfficientNet-B0
model = timm.create_model("efficientnet_b0", pretrained=True, num_classes=10)

Lợi ích so với torchvision.models:

  • Nhiều model hơn (ConvNeXt-V2, MaxViT, EVA, DINOv2 backbone...).
  • Tham số num_classes tự động thay head.
  • Hỗ trợ features_only=True để lấy multi-scale feature cho detection / segmentation.
  • Helper timm.data.resolve_data_config tự tìm pre-processing chuẩn.

Khi chọn model ngoài ResNet/VGG, timm là default tốt hơn torchvision.

16

HuggingFace Transformers cho NLP

Transfer Learning cũng là pattern chuẩn cho NLP. HuggingFace Transformers cung cấp API tương tự cho text:

from transformers import AutoModelForSequenceClassification, AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
model = AutoModelForSequenceClassification.from_pretrained(
    "bert-base-uncased",
    num_labels=3,        # task có 3 class
)

BERT pretrained trên masked language modeling — học general language representation, sau đó fine-tune cho classification, NER, QA. Cùng triết lý với fine-tune ResNet pretrained.

Series 4 (ai-llm-genai) sẽ đi sâu vào TL cho NLP, prompt engineering, LoRA, RLHF.

17

Use case TL phổ biến

  • Medical imaging: X-ray, MRI, CT — phân loại bệnh, segment tổn thương. Data label thường ít (cần bác sĩ), TL từ ImageNet hoặc từ pretrained medical (RadImageNet) giúp đáng kể.
  • Wildlife classification: nhận diện loài chim, thú từ camera trap.
  • Quality control manufacturing: phát hiện lỗi sản phẩm (vết nứt, vết bẩn) trên dây chuyền — dữ liệu công ty ít, TL là chuẩn.
  • Custom object detection: dùng backbone pretrained làm feature extractor cho Faster R-CNN, YOLO, DETR.
  • Satellite imagery: phân loại đất sử dụng, phát hiện công trình.
  • OCR và document understanding: backbone CNN/ViT pretrained kết hợp với head riêng cho từng task.
18

Limitations và domain shift

Pretrained không phải lúc nào tốt:

  • Pretrained dataset bias: ImageNet thiên về vật thể trong ảnh "Western, web-curated". Object hiếm trong dataset gốc → feature backbone yếu cho object đó.
  • Domain shift quá lớn: medical X-ray, satellite, audio spectrogram, công nghiệp — phân bố ảnh khác xa ImageNet. TL có thể chỉ giúp một phần nhỏ; đôi khi train from scratch với data đủ lớn cho kết quả tương đương.
  • Compute vẫn cần: dù ít hơn train from scratch, fine-tune ResNet-50 trên dataset vài chục nghìn ảnh vẫn cần GPU.
  • Catastrophic forgetting: nếu LR quá cao và data nhỏ, backbone có thể bị overwrite, mất general feature đã học.

Một nghiên cứu đáng đọc: Raghu et al. (2019) — "Transfusion: Understanding Transfer Learning for Medical Imaging" (arXiv:1902.07208). Kết quả: với một số medical task, model nhỏ train from scratch ngang ngửa ResNet-50 pretrained — pretrained chủ yếu giúp ở scaling / weight statistics chứ không phải reuse high-level feature.

Khi vào domain đặc biệt, cân nhắc:

  • Pretrain trên dataset cùng domain (RadImageNet cho medical, BigEarthNet cho satellite).
  • Self-supervised pretrain trên data unlabel của chính domain (SimCLR, DINOv2, MAE).
  • Nếu data đủ lớn (> 100k), thử cả TL và from scratch để so sánh.
19

Save và share model

Sau khi fine-tune, lưu weight bằng state_dict:

torch.save(model.state_dict(), "resnet18_cifar10.pth")

# Load lại
model = models.resnet18(weights=None)
model.fc = nn.Linear(model.fc.in_features, 10)
model.load_state_dict(torch.load("resnet18_cifar10.pth", map_location="cpu"))
model.eval()

Để share công khai, dùng HuggingFace Hub:

from huggingface_hub import PyTorchModelHubMixin

# Thêm mixin vào custom class
class MyResNet(nn.Module, PyTorchModelHubMixin):
    ...

model = MyResNet(...)
model.push_to_hub("user/resnet18-cifar10")

Lúc inference, bất cứ ai cũng có thể: MyResNet.from_pretrained("user/resnet18-cifar10").

20

Code Python — fine-tune ResNet-18 trên CIFAR-10

Bài này so sánh trực tiếp với SimpleCNN ở Bài 31. Cùng dataset, cùng số epoch.

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

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# 1) Dataset + transform — resize lên 224 cho ResNet pretrained
mean = [0.485, 0.456, 0.406]
std  = [0.229, 0.224, 0.225]
train_tf = transforms.Compose([
    transforms.Resize(224),
    transforms.RandomHorizontalFlip(),
    transforms.ToTensor(),
    transforms.Normalize(mean, std),
])
test_tf = transforms.Compose([
    transforms.Resize(224),
    transforms.ToTensor(),
    transforms.Normalize(mean, std),
])

train_set = datasets.CIFAR10("./data", train=True, download=True, transform=train_tf)
test_set  = datasets.CIFAR10("./data", train=False, download=True, transform=test_tf)
train_loader = DataLoader(train_set, batch_size=64, shuffle=True, num_workers=2)
test_loader  = DataLoader(test_set, batch_size=128, shuffle=False, num_workers=2)

# 2) Model: ResNet-18 pretrained, freeze backbone, head 10 class
model = models.resnet18(weights="DEFAULT")
for p in model.parameters():
    p.requires_grad = False
model.fc = nn.Linear(model.fc.in_features, 10)
model = model.to(device)

# 3) Loss + Optimizer (chỉ train head)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.AdamW(
    [p for p in model.parameters() if p.requires_grad],
    lr=1e-3,
)

# 4) Train loop
def train_one_epoch():
    model.train()
    for x, y in train_loader:
        x, y = x.to(device), y.to(device)
        logits = model(x)
        loss = criterion(logits, y)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

@torch.no_grad()
def evaluate():
    model.eval()
    correct = total = 0
    for x, y in test_loader:
        x, y = x.to(device), y.to(device)
        pred = model(x).argmax(dim=1)
        correct += (pred == y).sum().item()
        total += y.size(0)
    return correct / total

for epoch in range(5):
    train_one_epoch()
    acc = evaluate()
    print(f"Epoch {epoch + 1}: test acc = {acc:.4f}")

Kết quả tham khảo (5 epoch trên GPU):

  • SimpleCNN Bài 31 (train from scratch): ~70–75% test accuracy.
  • ResNet-18 frozen + new head: ~80–85%.
  • ResNet-18 fine-tune toàn bộ + differential LR: ~92–94%.

Phiên bản fine-tune toàn bộ với differential LR:

model = models.resnet18(weights="DEFAULT")
model.fc = nn.Linear(model.fc.in_features, 10)  # head mới, backbone vẫn requires_grad=True
model = model.to(device)

optimizer = torch.optim.AdamW([
    {"params": model.fc.parameters(), "lr": 1e-3},
    {"params": [p for n, p in model.named_parameters() if "fc" not in n], "lr": 1e-5},
], weight_decay=1e-4)

Phần train loop / evaluate giữ nguyên. Khác biệt chỉ ở chỗ backbone được tune nhẹ.

21

Bài tập

  1. Fine-tune ResNet-18 pretrained trên CIFAR-10 (5 epoch, frozen backbone). So sánh test accuracy với SimpleCNN Bài 31 cùng số epoch.
  2. Lặp lại bài 1 nhưng không freeze backbone (fine-tune toàn bộ với differential LR 1e-3 / 1e-5). So sánh accuracy và training time.
  3. Đếm số tham số trainable trong 3 trường hợp: SimpleCNN, ResNet-18 frozen + new head, ResNet-18 fine-tune toàn bộ. Comment về tỷ lệ.
  4. Dùng weights.transforms() của ResNet18_Weights.DEFAULT thay cho pipeline tự viết, train lại. Verify accuracy giữ nguyên hoặc tốt hơn.
  5. Thử timm.create_model("efficientnet_b0", pretrained=True, num_classes=10) trên CIFAR-10 với cùng pipeline. So sánh accuracy với ResNet-18.
  6. Thử freeze chỉ layer1layer2 của ResNet-18, fine-tune layer3, layer4, và fc. Quan sát accuracy so với hai cách extreme (freeze tất cả / tune tất cả).
  7. Lưu model fine-tune bằng torch.save, load lại trên process mới, verify inference cho ra output giống hệt.
  8. Đọc paper Raghu et al. (2019) Transfusion. Tóm tắt 3 kết luận chính về TL trong medical imaging.
Đáp án ngắn
  1. Frozen backbone thường vượt SimpleCNN 5–10% accuracy với cùng số epoch, dù ít trainable param hơn.
  2. Fine-tune toàn bộ đạt cao nhất (~92–94%), nhưng training time gấp 2–3x do gradient lan tỏa toàn bộ backbone.
  3. SimpleCNN ~100k–500k param trainable; ResNet-18 frozen ~5k (chỉ head); ResNet-18 full tune ~11.2M. Accuracy không tỷ lệ thuận với số trainable param — feature pretrained quan trọng hơn.
  4. Accuracy tương đương; weights.transforms() đảm bảo dùng đúng resize / normalize gốc.
  5. EfficientNet-B0 thường ngang ResNet-18 trên CIFAR-10, ít param hơn (~5M vs 11M). Trade-off: B0 chậm hơn một chút do depthwise conv.
  6. Freeze layer1+layer2 + tune layer3+layer4+fc thường cho accuracy giữa hai extreme, training time ngắn hơn full tune.
  7. Cần đặt model.eval() trước inference để tắt BatchNorm running stats update. torch.allclose giữa 2 lần forward trên cùng input.
  8. Tóm tắt Raghu et al.: (1) trên medical task, model nhỏ train from scratch có thể ngang ResNet-50 pretrained; (2) feature reuse từ ImageNet hạn chế cho medical; (3) lợi ích pretrained chủ yếu từ weight statistics, không phải high-level feature.

Bài kế tiếp: Bài 33 — Các kiến trúc CNN nổi bật — đi qua LeNet, AlexNet, VGG, GoogLeNet, ResNet, DenseNet, EfficientNet, ConvNeXt và quy luật thiết kế chung.