Data augmentation memperbanyak data milik kita, tetapi mengumpulkan dan menganotasi data sering kali mahal, sementara model dari nol menuntut data banyak. Transfer learning mematahkan asumsi bahwa setiap tugas harus dilatih dari awal — kita meminjam representasi dari model yang sudah dilatih pada dataset raksasa, lalu menempelkan bagian kecil yang dipelajari khusus untuk tugas kita. Pola ini menjadi standar de facto di industri: hampir semua model visi dan bahasa modern dimulai dari pretrained model. Chapter ini membahas dua strateginya, mekanisme pembekuan layer, dan demo teruji nyata yang meniru seluruh alurnya tanpa unduhan.
Representasi yang Dapat Dipakai Ulang #
Layer awal jaringan belajar fitur yang umum, bukan fitur milik satu tugas. Pada CNN ImageNet, layer konvolusi pertama menangkap tepi dan bercak warna, layer tengah menyusun tekstur dan pola, layer akhir merangkai bagian objek — urutan yang berguna untuk klasifikasi apa pun; model bahasa membangun representasi serupa dari korpus besar. Ketika tugas baru hanya punya 40 sampel berlabel, fitur umum hasil pretraining jauh lebih baik daripada fitur acak inisialisasi awal.
Model pretrained dipotong dua: body — layer awal pembawa fitur umum dari pretraining, dan head — layer keluaran tugas asal yang dibuang lalu diganti sesuai jumlah kelas tugas baru.
Dua Strategi: Feature Extraction dan Fine-Tuning #
Ada dua cara memperlangkan body pretrained, dan keduanya dibedakan oleh satu pertanyaan: apakah bobot body boleh ikut berubah?
| Aspek | Feature extraction | Fine-tuning |
|---|---|---|
| Perlakuan body | dibekukan, requires_grad=False | dibuka kembali, dilatih dengan lr kecil |
| Parameter yang dilatih | hanya head baru | head baru + sebagian atau seluruh body |
| Kebutuhan data | puluhan-ratusan sampel cukup | ribuan sampel agar aman |
| Komputasi | ringan; gradien body tidak dihitung | lebih berat; seluruh graph ikut berjalan |
| Kapan dipilih | tugas baru mirip tugas asal, data sangat sedikit | tugas bergeser dari tugas asal, data tersedia cukup |
Chapter ini membedah feature extraction sampai tuntas; fine-tuning — membuka body dengan learning rate kecil — dibahas pada Chapter Fine-Tuning .
Membekukan Body dengan requires_grad #
Autograd di PyTorch melacak setiap tensor yang memiliki requires_grad=True. Menuliskan p.requires_grad_(False) pada parameter body membuat autograd berhenti menghitung gradien untuk parameter tersebut saat loss.backward() dipanggil; gradien body bernilai None, sehingga optimizer tidak pernah menggesernya — mekanisme yang didokumentasikan di docs.pytorch.org/docs/stable/notes/autograd
. Alur lengkap strategi feature extraction tampak seperti berikut:
Head baru sengaja dilatih terakhir: head dimulai dari bobot acak dengan gradien besar pada epoch awal — bila body ikut terbuka, gradien tersebut menyeret bobot pretrained sebelum head sempat belajar apa pun.
Demo: “Pretrained” Buatan Tanpa Unduhan #
Berkas transfer_learning.py meniru keseluruhan alur dengan bahan buatan: tugas A berupa XOR standar dengan 600 sampel berlabel murah (peran dataset raksasa), tugas B berupa XOR serupa dengan 40 sampel berlabel (peran tugas nyata berdata langka). Sebuah MLP dilatih tuntas pada tugas A, body-nya dibekukan, head baru dilatih pada tugas B, lalu dibandingkan dengan MLP identik dari nol berjadwal sama:
"""Transfer learning dengan "pretrained" buatan — tanpa unduhan apa pun.
Tugas A: XOR standar pada bidang (x1, x2), 600 sampel berlabel murah.
Tugas B: aturan keputusan XOR yang sama, distribusi sedikit melebar,
hanya 40 sampel berlabel — meniru skenario dunia nyata: label mahal,
model pretrained sudah tersedia.
Sebuah MLP dilatih tuntas pada tugas A, layer awalnya dibekukan
(requires_grad=False), lalu head baru dilatih pada tugas B (strategi
feature extraction). Sebagai pembanding, MLP identik dilatih dari nol
dengan jadwal yang sama persis.
"""
import torch
import torch.nn as nn
torch.manual_seed(42)
def buat_model():
"""MLP kecil [2, 16, 2] untuk klasifikasi biner di bidang 2D."""
return nn.Sequential(nn.Linear(2, 16), nn.ReLU(), nn.Linear(16, 2))
def buat_xor(n, skala=1.0):
"""Data XOR: kelas 1 jika tepat satu dari x1, x2 bernilai positif."""
X = torch.randn(n, 2) * skala
label = (X[:, 0] > 0) ^ (X[:, 1] > 0)
return X, label.long()
def latih_penuh(model, X, y, epoch=300, lr=0.05):
"""Latih full-batch pada seluruh data tugas A."""
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
criterion = nn.CrossEntropyLoss()
for _ in range(epoch):
optimizer.zero_grad()
criterion(model(X), y).backward()
optimizer.step()
with torch.no_grad():
return (model(X).argmax(dim=1) == y).float().mean().item()
# ---------- 1. Tugas A: bangun "pretrained" ----------
X_A, y_A = buat_xor(600)
model_A = buat_model()
akurasi_A = latih_penuh(model_A, X_A, y_A)
print(f"tugas A : XOR standar, 600 sampel, 300 epoch -> akurasi {akurasi_A:.3f}")
# ---------- 2. Strategi feature extraction: bekukan body ----------
body = nn.Sequential(model_A[0], model_A[1]) # Linear(2,16) + ReLU
for p in body.parameters():
p.requires_grad_(False)
jumlah_beku = sum(p.numel() for p in body.parameters())
print(f"freeze : {jumlah_beku} parameter body -> requires_grad=False")
# ---------- 3. Tugas B: data terbatas ----------
X_B, y_B = buat_xor(40, skala=1.3) # hanya 40 sampel berlabel
X_val, y_val = buat_xor(400, skala=1.3)
print(f"tugas B : XOR serupa (skala 1.3), 40 sampel latih, 400 validasi\n")
model_transfer = nn.Sequential(body, nn.Linear(16, 2)) # head baru
model_nol = buat_model() # dari nol
optimizer_t = torch.optim.Adam(model_transfer.parameters(), lr=0.05)
optimizer_n = torch.optim.Adam(model_nol.parameters(), lr=0.05)
criterion = nn.CrossEntropyLoss()
bobot_body_awal = body[0].weight.clone()
EPOCH = 20
riwayat_t, riwayat_n = [], []
for ep in range(1, EPOCH + 1):
for model, optimizer in ((model_transfer, optimizer_t), (model_nol, optimizer_n)):
urutan = torch.randperm(len(X_B))
for i in range(0, len(X_B), 16):
idx = urutan[i:i + 16]
optimizer.zero_grad()
criterion(model(X_B[idx]), y_B[idx]).backward()
optimizer.step()
with torch.no_grad():
ak_t = (model_transfer(X_val).argmax(dim=1) == y_val).float().mean().item()
ak_n = (model_nol(X_val).argmax(dim=1) == y_val).float().mean().item()
riwayat_t.append(ak_t)
riwayat_n.append(ak_n)
if ep == 1 or ep % 2 == 0:
print(f"epoch {ep:2d} | validasi transfer {ak_t:.3f} | validasi dari nol {ak_n:.3f}")
# ---------- 4. Bukti body benar-benar beku ----------
loss_B = criterion(model_transfer(X_B), y_B)
loss_B.backward()
grad_body = body[0].weight.grad
print(f"\ngrad body setelah backward : {grad_body}")
print(f"bobot body tidak berubah : {torch.equal(bobot_body_awal, body[0].weight)}")
ep_t = next((i + 1 for i, a in enumerate(riwayat_t) if a >= 0.95), "belum")
ep_n = next((i + 1 for i, a in enumerate(riwayat_n) if a >= 0.95), "belum")
print(f"epoch pertama validasi >= 0.95 : transfer {ep_t} | dari nol {ep_n}")
print(f"validasi akhir (epoch {EPOCH}) : transfer {riwayat_t[-1]:.3f} "
f"| dari nol {riwayat_n[-1]:.3f}")Output yang Dihasilkan:
tugas A : XOR standar, 600 sampel, 300 epoch -> akurasi 0.998
freeze : 48 parameter body -> requires_grad=False
tugas B : XOR serupa (skala 1.3), 40 sampel latih, 400 validasi
epoch 1 | validasi transfer 0.765 | validasi dari nol 0.757
epoch 2 | validasi transfer 0.905 | validasi dari nol 0.748
epoch 4 | validasi transfer 0.970 | validasi dari nol 0.820
epoch 6 | validasi transfer 0.933 | validasi dari nol 0.905
epoch 8 | validasi transfer 0.942 | validasi dari nol 0.905
epoch 10 | validasi transfer 0.942 | validasi dari nol 0.910
epoch 12 | validasi transfer 0.960 | validasi dari nol 0.892
epoch 14 | validasi transfer 0.973 | validasi dari nol 0.910
epoch 16 | validasi transfer 0.975 | validasi dari nol 0.933
epoch 18 | validasi transfer 0.965 | validasi dari nol 0.918
epoch 20 | validasi transfer 0.962 | validasi dari nol 0.908
grad body setelah backward : None
bobot body tidak berubah : True
epoch pertama validasi >= 0.95 : transfer 4 | dari nol belum
validasi akhir (epoch 20) : transfer 0.962 | dari nol 0.908Mari kita telaah kode di atas baris per baris:
- Baris 16 (
torch.manual_seed(42)): mengunci pembangkitan data XOR, inisialisasi bobot, dan urutanrandpermsehingga keluaran identik di setiap eksekusi. - Baris 19-21 (
buat_model): MLP[2, 16, 2]yang dipakai untuk model pretrained, model transfer, dan pembanding dari nol agar perbandingan adil. - Baris 24-28 (
buat_xor): label ditentukan aturan XOR — kelas 1 jika tepat satu darix1, x2positif; argumenskalamelebarkan sebaran data untuk membuat tugas B sedikit berbeda. - Baris 31-40 (
latih_penuh): full-batch Adam 300 epoch pada seluruh tugas A — tiruan kompak dari pretraining berskala besar. - Baris 44-47: model A mencapai akurasi 0.998; bobot
model_Akini berstatus “pretrained” dan siap dipotong. - Baris 50-52: body diambil dari dua layer pertama
model_A(Linear+ReLU) lalu dibekukan — total 48 parameter (16×2 bobot + 16 bias). - Baris 58-60: tugas B hanya menyediakan 40 sampel latih dan 400 validasi; skala 1.3 melebarkan sebaran datanya sedikit dari tugas A.
- Baris 62-63:
model_transfer= body beku + head baruLinear(16, 2);model_nolberarsitektur identik dari inisialisasi acak. - Baris 65-67: kedua optimizer Adam
lr=0.05identik — satu-satunya perbedaan kedua model adalah titik berangkat bobotnya. - Baris 69: salinan bobot body disimpan sebagai patokan pembuktian bahwa bobot tersebut tidak tersentuh pelatihan.
- Baris 71-87: loop 20 epoch mini-batch 16; kedua model dilatih pada batch yang sama dan akurasi validasi dicatat tiap epoch.
- Baris 90-94:
loss.backward()dijalankan sekali lagi:body[0].weight.gradbernilaiNonedantorch.equalmembuktikan bobot body identik dengan salinan awal — bukti mekanis bahwa freeze bekerja. - Baris 96-100: ringkasan akhir — transfer menembus 0.95 pada epoch 4, dari nol tidak pernah (maksimum 0.933, akhir 0.908).
Membaca Angka: Konvergen Lebih Cepat dengan Data 15x Lebih Sedikit #
Selisih paling mencolok muncul di epoch awal: epoch 4 mencatat validasi transfer 0.970 berbanding dari nol 0.820. Body beku sudah mengodekan geometri XOR sejak menit pertama, sehingga head baru hanya perlu belajar pemetaan linier kecil di atas fitur yang sudah terbentuk — cukup dengan 40 sampel. Model dari nol harus membangun keduanya sekaligus dari bobot acak, dan 20 epoch belum memadai — pola inilah yang membuat transfer learning nyaris wajib pada dataset kecil dunia nyata.
Satu pertanyaan tersisa dari keluaran di atas: transfer berhenti di kisaran 0.96, bukan 0.998 seperti model aslinya — fitur beku dari tugas A tidak persis pas untuk tugas B yang sebarannya melebar. Kesenjangan inilah yang ditutup strategi kedua: membuka kembali body dan memolesnya dengan learning rate kecil, topik Chapter Fine-Tuning .