Kita sekarang memiliki seluruh perbendaharaan arsitektur: VGG, Inception, ResNet, EfficientNet. Masalahnya, arsitektur-arsitektur itu dilatih pada ImageNet — 1,2 juta gambar dari 1.000 kelas. Dataset milik kita sendiri biasanya jauh lebih kecil: ratusan foto produk toko, beberapa ribu hasil rontgen, atau puluhan gambar kerusakan mesin. Melatih CNN besar dari nol pada data sebanyak itu hampir pasti berakhir overfitting. Kabar baiknya, kita tidak perlu memulai dari nol — chapter ini mempraktikkan teori transfer learning dari section Regularisasi dan Generalisasi khusus untuk tugas visual: pakai backbone pretrained sebagai pengekstrak fitur, bekukan bobotnya, lalu latih classifier head baru milik kita.
Pola kerjanya tiga langkah. Pertama, siapkan backbone CNN yang bobotnya telah terlatih (di dunia nyata: torchvision.models dengan weights=...). Kedua, buang head klasifikasinya dan sambungkan head baru sesuai jumlah kelas kita, lalu bekukan seluruh parameter backbone (requires_grad = False). Ketiga, latih hanya head tersebut — ratusan ribu parameter ketimbang puluhan juta, sehingga data sedikit pun cukup.
Eksperimen: Backbone Buatan, Data 100 Sampel #
Mesin uji kita tidak mengunduh bobot ImageNet (ratusan megabita). Sebagai gantinya, berkas transfer_gambar.py membangun situasi yang setara secara struktural: backbone CNN dilatih sendiri pada tugas A (mengenali digit 0-4 dari 901 sampel), lalu dipindahkan ke tugas B (mengenali digit 5-9) dengan hanya 100 sampel latih. Pembandingnya: arsitektur yang persis sama dilatih dari nol pada 100 sampel yang sama. Satu detail penting yang sering terlewat juga didemokan: normalisasi tugas B memakai mean/std dari data tugas A — statistik dataset asal backbone harus ikut berpindah:
"""Transfer learning untuk gambar: backbone tugas A -> head baru tugas B.
Backbone CNN dilatih pada tugas A (digit 0-4, citra 32x32). Model kemudian
dipindah ke tugas B (digit 5-9) dengan hanya 100 sampel latih: backbone
dibekukan (requires_grad=False) dan hanya classifier head baru yang
dilatih. Pembanding: arsitektur sama dilatih dari nol pada 100 sampel
yang sama. Normalisasi memakai mean/std data tugas A -- statistik dataset
asal backbone ikut dipakai di tugas baru.
"""
import torch
import torch.nn as nn
import torch.nn.functional as F
from sklearn.datasets import load_digits
torch.manual_seed(42)
# ---------- 1. Data ----------
digits = load_digits()
X = digits.data.astype("float32") / 16.0 # piksel 0..16 -> 0..1
y = digits.target.astype("int64")
mask_A = y <= 4 # tugas A: digit 0-4
mask_B = y >= 5 # tugas B: digit 5-9
XA, yA = X[mask_A], y[mask_A]
XB, yB = X[mask_B], y[mask_B] - 5 # relabel 5..9 -> 0..4
# normalisasi dari statistik data tugas A (dataset asal backbone)
mu, sigma = XA.mean(), XA.std()
XA = (XA - mu) / sigma
XB = (XB - mu) / sigma
def ke_gambar_32(matriks):
"""(N, 64) -> (N, 1, 32, 32) lewat upscale bilinear."""
kecil = torch.tensor(matriks).reshape(-1, 1, 8, 8)
return F.interpolate(kecil, size=(32, 32), mode="bilinear",
align_corners=False)
XA_t, yA_t = ke_gambar_32(XA), torch.tensor(yA)
XB_t, yB_t = ke_gambar_32(XB), torch.tensor(yB)
# tugas B: hanya 100 sampel latih (20 per kelas), sisanya untuk evaluasi
indeks_B = torch.randperm(len(XB_t),
generator=torch.Generator().manual_seed(42))
latih_B, uji_B = indeks_B[:100], indeks_B[100:]
X_latih_B, y_latih_B = XB_t[latih_B], yB_t[latih_B]
X_uji_B, y_uji_B = XB_t[uji_B], yB_t[uji_B]
print(f"tugas A: {len(XA_t)} sampel (digit 0-4) -> melatih backbone")
print(f"tugas B: {len(latih_B)} sampel latih + {len(uji_B)} sampel uji "
f"(digit 5-9)\n")
# ---------- 2. Arsitektur: backbone + head ----------
class CNN(nn.Module):
"""Backbone konvolusi + classifier head yang bisa diganti."""
def __init__(self, jumlah_kelas):
super().__init__()
self.backbone = nn.Sequential(
nn.Conv2d(1, 32, kernel_size=3, padding=1),
nn.ReLU(),
nn.MaxPool2d(2), # 32x32 -> 16x16
nn.Conv2d(32, 64, kernel_size=3, padding=1),
nn.ReLU(),
nn.MaxPool2d(2), # 16x16 -> 8x8
nn.Conv2d(64, 64, kernel_size=3, padding=1),
nn.ReLU(),
nn.MaxPool2d(2), # 8x8 -> 4x4
nn.Flatten(), # 64 x 4 x 4 = 1024 fitur
)
self.head = nn.Linear(64 * 4 * 4, jumlah_kelas)
def forward(self, x):
return self.head(self.backbone(x))
def latih_epoch(model, optimizer, X, t, urutan, ukuran_batch):
"""Satu epoch mini-batch pada urutan indeks yang diberikan."""
for awal in range(0, len(urutan), ukuran_batch):
pilih = urutan[awal:awal + ukuran_batch]
optimizer.zero_grad()
loss = criterion(model(X[pilih]), t[pilih])
loss.backward()
optimizer.step()
def akurasi_uji(model, X, t):
"""Akurasi pada seluruh sampel uji, mode eval, tanpa gradien."""
model.eval()
with torch.no_grad():
return (model(X).argmax(dim=1) == t).float().mean().item()
# ---------- 3. Latih backbone pada tugas A ----------
model_A = CNN(jumlah_kelas=5)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model_A.parameters(), lr=1e-3)
gen_A = torch.Generator().manual_seed(42)
for epoch in range(10):
urutan = torch.randperm(len(XA_t), generator=gen_A)
latih_epoch(model_A, optimizer, XA_t, yA_t, urutan, 32)
print(f"backbone tugas A selesai: akurasi A {akurasi_uji(model_A, XA_t, yA_t):.3f}")
print("backbone ini berperan sebagai 'pretrained model' buatan\n")
# ---------- 4. Transfer: bekukan backbone, latih head baru ----------
model_transfer = CNN(jumlah_kelas=5)
model_transfer.backbone.load_state_dict(model_A.backbone.state_dict())
for p in model_transfer.backbone.parameters():
p.requires_grad = False # bekukan seluruh backbone
bobot_sebelum = [p.clone() for p in model_transfer.backbone.parameters()]
optimizer_transfer = torch.optim.Adam(
model_transfer.head.parameters(), lr=1e-3) # hanya head ikut optimizer
# pembanding: arsitektur sama, dari nol
model_nol = CNN(jumlah_kelas=5)
optimizer_nol = torch.optim.Adam(model_nol.parameters(), lr=1e-3)
print(f"{'epoch':>5s} {'transfer (freeze)':>18s} {'dari nol':>10s}")
gen_B = torch.Generator().manual_seed(42)
for epoch in range(1, 11):
urutan = torch.randperm(len(X_latih_B), generator=gen_B)
for model, optimizer in [(model_transfer, optimizer_transfer),
(model_nol, optimizer_nol)]:
model.train()
latih_epoch(model, optimizer, X_latih_B, y_latih_B, urutan, 20)
baris = f"{epoch:>5d}"
for model in (model_transfer, model_nol):
nilai = akurasi_uji(model, X_uji_B, y_uji_B)
baris += f" {nilai:>18.3f}" if model is model_transfer else f" {nilai:>10.3f}"
print(baris)
# ---------- 5. Verifikasi mekanisme freeze ----------
bobot_sesudah = list(model_transfer.backbone.parameters())
tetap = all(torch.equal(a, b) for a, b in zip(bobot_sebelum, bobot_sesudah))
grad_beku = all(p.grad is None
for p in model_transfer.backbone.parameters())
akurasi_transfer = akurasi_uji(model_transfer, X_uji_B, y_uji_B)
akurasi_nol = akurasi_uji(model_nol, X_uji_B, y_uji_B)
print(f"\nakurasi akhir tugas B | transfer: {akurasi_transfer:.3f} "
f"| dari nol: {akurasi_nol:.3f}")
print(f"bobot backbone tetap sama setelah training : {tetap}")
print(f"backbone tidak punya gradien (.grad None) : {grad_beku}")Output yang Dihasilkan:
tugas A: 901 sampel (digit 0-4) -> melatih backbone
tugas B: 100 sampel latih + 796 sampel uji (digit 5-9)
backbone tugas A selesai: akurasi A 1.000
backbone ini berperan sebagai 'pretrained model' buatan
epoch transfer (freeze) dari nol
1 0.495 0.196
2 0.818 0.583
3 0.921 0.837
4 0.863 0.910
5 0.916 0.923
6 0.923 0.931
7 0.938 0.923
8 0.932 0.931
9 0.937 0.943
10 0.943 0.915
akurasi akhir tugas B | transfer: 0.943 | dari nol: 0.915
bobot backbone tetap sama setelah training : True
backbone tidak punya gradien (.grad None) : TrueMari kita telaah kode di atas baris per baris:
- Baris 17-27 (persiapan data): dataset digits dibagi dua tugas — mask A untuk digit 0-4, mask B untuk digit 5-9 yang direlabel menjadi 0-4. Normalisasi dihitung dari data tugas A (
mu, sigma = XA.mean(), XA.std()) lalu diterapkan ke kedua tugas — kebiasaan penting saat memakai model pretrained. - Baris 29-34 (
ke_gambar_32): citra 8x8 di-upscale bilinear ke 32x32 agar backbone konvolusi tiga blok pooling punya ruang resolusi yang cukup. - Baris 37-40: tugas B sengaja dibuat miskin — hanya 100 sampel latih (20 per kelas) diambil dari permutasi ber-seed; 796 sampel sisanya menjadi uji. Kemiskinan inilah yang membuat transfer learning menonjol.
- Baris 45-65 (
CNN): arsitektur dipisah dua bagian —self.backbone(tiga blokConv→ReLU→MaxPoolberakhirFlatten, menghasilkan 1024 fitur) danself.head(Linearsesuai jumlah kelas). Pemisahan ini yang memungkinkan head diganti tanpa menyentuh backbone. - Baris 88-96: backbone dilatih penuh pada tugas A hingga akurasi 1,000 — versi mini dari “model pretrained”.
- Baris 99-107 (inti transfer):
load_state_dictmenyalin bobot backbone dari model A; seluruh parameternya dibekukanrequires_grad = False; optimizer hanya menerimamodel_transfer.head.parameters()— dua syarat yang membuat backbone benar-benar beku. - Baris 110-112 (pembanding):
model_nolarsitektur sama, semua bobot acak, optimizer menerima seluruh parameternya. - Baris 114-131: kedua model dilatih bergantian pada batch yang sama (permutasi ber-seed yang sama) selama 10 epoch; akurasi uji keduanya dicetak berdampingan tiap epoch.
- Baris 133-145 (verifikasi mekanisme): dua bukti bahwa freeze bekerja —
torch.equalmemastikan bobot backbone tidak berubah satu bit, dan.grad is Nonememastikan autograd tidak pernah menghitung gradien untuknya.
Membaca Hasil: Lompatan Awal yang Menentukan #
Kolom transfer pada keluaran memperlihatkan jejak khas transfer learning: akurasi 0,495 pada epoch pertama, melompat ke 0,818 pada epoch kedua, dan 0,921 pada epoch ketiga. Model dari nol butuh lima epoch untuk mengejar ketertinggalan itu. Pada dataset hanya 100 sampel, hasil akhir 0,943 berbanding 0,915 — unggul, meski selisih akhir menyempit karena tugas digits termasuk mudah. Pada dataset nyata yang lebih sulit (foto medis, citra industri), lompatan awal seperti itu sering kali menentukan keberhasilan total: model dari nol tidak sempat konvergen sebelum data habis dipelajari hafalan.
Dua baris terakhir keluaran adalah verifikasi mekanis yang perlu dibiasakan: setelah training, bobot backbone tetap identik (True) dan tidak ada gradien tersimpan untuknya (True). Di luar angka akurasi, verifikasi ini memastikan eksperimen benar-benar mengukur apa yang diklaimnya — kebiasaan dari chapter Reproducible Experiments
.
Diagram di atas merangkum alur lengkap yang barusan kita jalankan: gambar masuk dinormalisasi dengan statistik dataset asal, melewati backbone yang seluruh bobotnya beku, dan hanya head kecil yang belajar.
Transfer Learning Nyata dengan torchvision #
Pada praktik sehari-hari, backbone tidak perlu dilatih sendiri. Pustaka torchvision
menyediakan puluhan arsitektur pretrained — resnet18, efficientnet_b0, dan lainnya — lengkap dengan bobot ImageNet:
import torch
import torch.nn as nn
from torchvision import models
# 1. Muat backbone dengan bobot pretrained (unduh sekali, lalu tersimpan lokal)
backbone = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1)
# 2. Buang head klasifikasi ImageNet (1000 kelas), ganti head milik kita
jumlah_kelas = 4
backbone.fc = nn.Linear(backbone.fc.in_features, jumlah_kelas)
# 3. Bekukan seluruh layer kecuali head
for nama, parameter in backbone.named_parameters():
parameter.requires_grad = nama.startswith("fc")
# 4. Optimizer hanya menerima parameter head
optimizer = torch.optim.Adam(
[p for p in backbone.parameters() if p.requires_grad], lr=1e-3)Empat langkah pada contoh di atas memetakan persis ke eksperimen transfer_gambar.py kita: muat bobot terlatih, ganti head, bekukan backbone, latih hanya head. Dua hal tambahan yang wajib di dunia nyata: masukan dinormalisasi dengan mean dan std ImageNet ([0.485, 0.456, 0.406] dan [0.229, 0.224, 0.225] untuk tiap channel RGB — angka resmi dari dokumentasi torchvision), dan resolusi masukan disesuaikan dengan yang dipakai backbone saat pelatihan (224x224 untuk ResNet).
Dengan pola ini, sejarah arsitektur yang kita pelajari sepanjang section — dari convolution dasar hingga EfficientNet — menjadi modal yang bisa diambil siapa saja tanpa melatih ulang. Section Computer Vision berikutnya memakai modal itu untuk tugas-tugas visual yang lebih kaya: deteksi objek, segmentasi, dan embedding gambar. Tutorial resmi PyTorch untuk transfer learning tersedia di pytorch.org/tutorials .