Feature extraction pada Chapter Transfer Learning menyerahkan body pretrained apa adanya, dan hasilnya memang kuat: validasi 0.962 dengan 40 sampel. Namun body beku menyimpan fitur dari tugas asal — begitu tugas baru bergeser, fitur tersebut hanya pas sebagian dan angka akhir berhenti di bawah potensi model. Fine-tuning menutup kesenjangan itu: sebagian layer beku dibuka kembali, lalu dipoles dengan learning rate yang jauh lebih kecil daripada head. Urutan langkah dan besaran learning rate menentukan apakah bobot pretrained terjaga atau justru bergeser total dan performa menurun. Chapter ini membahas urutan yang benar, lalu membuktikan keduanya dengan angka nyata.
Urutan yang Benar: Bekukan, Latih Head, Buka, Poles #
Fine-tuning yang stabil selalu mengikuti empat langkah berurutan:
- Muat pretrained dan bekukan seluruh body —
requires_grad=Falsepada semua layer body. - Latih head baru sampai stabil — head berangkat dari bobot acak bergradien besar; bila body terbuka sejak awal, gradien itu menyeret bobot pretrained ke arah yang salah.
- Buka kembali sebagian atau seluruh body (
unfreeze) — barulah body diberi kesempatan menyesuaikan diri setelah head memberi sinyal yang masuk akal. - Latih ulang seluruhnya dengan differential learning rate — learning rate body diatur jauh lebih kecil daripada head agar pembaruan berbentuk polesan halus, bukan perombakan.
Differential Learning Rate #
PyTorch optimizer menerima daftar parameter group, dan setiap grup membawa learning rate sendiri. Kombinasi yang lazim: lr body sepuluh kali lebih kecil daripada lr head:
optimizer = torch.optim.SGD([
{"params": model[0].parameters(), "lr": 0.005}, # body: lr kecil
{"params": model[1].parameters(), "lr": 0.05}, # head: lr 10x lebih besar
])Mekanisme param group ini didokumentasikan di docs.pytorch.org/docs/stable/optim.html . Intuisinya sederhana: bobot body sudah berada di wilayah bagus hasil pretraining sehingga cukup digeser tipis; head masih jauh dari solusi sehingga boleh melangkah lebar — satu lr besar untuk semuanya memaksa body melangkah sama lebar, menjauh dari posisi hasil pretraining.
Demo: Tiga Tahap pada Tugas yang Diputar #
Berkas fine_tuning.py melanjutkan pola buatan Chapter sebelumnya dengan satu perubahan penting: tugas B kini berupa XOR yang batas kuadrannya diputar 20 derajat, sehingga fitur beku dari tugas A hanya pas sebagian. Setelah tahap 1 (freeze + latih head), skrip menjalankan dua versi tahap 2 dari titik awal yang persis sama: versi benar dengan lr body 0.005, dan versi jebakan dengan lr body 5.0:
"""Fine-tuning sebagai kelanjutan transfer learning — pola buatan yang sama.
Tugas A: XOR standar, 600 sampel — sumber bobot "pretrained".
Tugas B: XOR yang diputar 20 derajat, hanya 40 sampel — mirip tapi tidak
identik, sehingga fitur beku tidak sepenuhnya pas dan masih bisa dipoles.
Tahap 1 (feature extraction): body dibekukan, head baru dilatih dengan Adam.
Tahap 2a (fine-tuning yang benar): body dibuka kembali dengan learning rate
10x lebih kecil dari head (differential learning rate), SGD momentum.
Tahap 2b (jebakan): titik awal yang sama, tetapi lr body raksasa — bobot
pretrained bergeser total dan validasi anjlok.
"""
import copy
import math
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, sudut=0.0):
"""Data XOR; sudut memutar batas kuadran sebesar sudut tersebut."""
X = torch.randn(n, 2) * 1.15
c, s = math.cos(sudut), math.sin(sudut)
u = X[:, 0] * c + X[:, 1] * s
v = -X[:, 0] * s + X[:, 1] * c
label = (u > 0) ^ (v > 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()
def latih_minibatch(model, X, y, X_val, y_val, optimizer, epoch, batch=16):
"""Latih mini-batch; kembalikan akurasi validasi tiap epoch."""
criterion = nn.CrossEntropyLoss()
riwayat = []
for _ in range(epoch):
urutan = torch.randperm(len(X))
for i in range(0, len(X), batch):
idx = urutan[i:i + batch]
optimizer.zero_grad()
criterion(model(X[idx]), y[idx]).backward()
optimizer.step()
with torch.no_grad():
ak = (model(X_val).argmax(dim=1) == y_val).float().mean().item()
riwayat.append(ak)
return riwayat
# ---------- 1. "Pretrained" dari tugas A ----------
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 -> akurasi {akurasi_A:.3f}")
SUDUT = math.radians(20)
X_B, y_B = buat_xor(40, sudut=SUDUT)
X_val, y_val = buat_xor(400, sudut=SUDUT)
print("tugas B : XOR diputar 20 derajat, 40 sampel latih, 400 validasi\n")
# ---------- 2. Tahap 1: freeze -> latih head saja ----------
body = nn.Sequential(model_A[0], model_A[1])
for p in body.parameters():
p.requires_grad_(False)
model_head = nn.Sequential(body, nn.Linear(16, 2))
optimizer_head = torch.optim.Adam(model_head.parameters(), lr=0.05)
latih_minibatch(model_head, X_B, y_B, X_val, y_val, optimizer_head, epoch=20)
with torch.no_grad():
val_tahap1 = (model_head(X_val).argmax(dim=1) == y_val).float().mean().item()
print(f"tahap 1 : freeze body, latih head (Adam, 20 epoch) -> validasi {val_tahap1:.3f}")
bobot_awal = body[0].weight.clone()
# ---------- 3. Tahap 2a: buka body + differential learning rate ----------
model_a = copy.deepcopy(model_head) # salinan dari titik akhir tahap 1
for p in model_a[0].parameters():
p.requires_grad_(True)
optimizer_a = torch.optim.SGD([
{"params": model_a[0].parameters(), "lr": 0.005, "momentum": 0.9},
{"params": model_a[1].parameters(), "lr": 0.05, "momentum": 0.9},
])
riwayat_a = latih_minibatch(model_a, X_B, y_B, X_val, y_val, optimizer_a, epoch=12)
delta_a = (model_a[0][0].weight - bobot_awal).norm().item()
# ---------- 4. Tahap 2b: jebakan lr raksasa saat membuka body ----------
model_b = copy.deepcopy(model_head) # salinan yang sama dari tahap 1
for p in model_b[0].parameters():
p.requires_grad_(True)
optimizer_b = torch.optim.SGD([
{"params": model_b[0].parameters(), "lr": 5.0, "momentum": 0.9},
{"params": model_b[1].parameters(), "lr": 0.05, "momentum": 0.9},
])
riwayat_b = latih_minibatch(model_b, X_B, y_B, X_val, y_val, optimizer_b, epoch=12)
delta_b = (model_b[0][0].weight - bobot_awal).norm().item()
print("\ntahap 2 : buka body (unfreeze), SGD momentum 0.9, 12 epoch tambahan")
print("epoch | 2a lr body 0.005 | 2b lr body 5.0")
for ep in (2, 6, 12):
print(f"{ep:5d} | {riwayat_a[ep - 1]:.3f} | {riwayat_b[ep - 1]:.3f}")
print(f"\npergeseran bobot body : 2a {delta_a:.3f} | 2b {delta_b:.3f}")
print(f"validasi akhir : 2a {riwayat_a[-1]:.3f} | 2b {riwayat_b[-1]:.3f}")
print(f"selisih dari tahap 1 : 2a {riwayat_a[-1] - val_tahap1:+.3f} "
f"| 2b {riwayat_b[-1] - val_tahap1:+.3f}")Output yang Dihasilkan:
tugas A : XOR standar, 600 sampel -> akurasi 0.998
tugas B : XOR diputar 20 derajat, 40 sampel latih, 400 validasi
tahap 1 : freeze body, latih head (Adam, 20 epoch) -> validasi 0.913
tahap 2 : buka body (unfreeze), SGD momentum 0.9, 12 epoch tambahan
epoch | 2a lr body 0.005 | 2b lr body 5.0
2 | 0.913 | 0.848
6 | 0.905 | 0.825
12 | 0.920 | 0.815
pergeseran bobot body : 2a 0.095 | 2b 20.159
validasi akhir : 2a 0.920 | 2b 0.815
selisih dari tahap 1 : 2a +0.007 | 2b -0.098Mari kita telaah kode di atas baris per baris:
- Baris 20 (
torch.manual_seed(42)): mengunci data, inisialisasi, dan urutan mini-batch;copy.deepcopymemastikan kedua varian tahap 2 berangkat dari titik akhir tahap 1 yang sama. - Baris 28-35 (
buat_xordengansudut): koordinat data diputar oleh matriks rotasic, ssebelum aturan XOR diterapkan — sudut 20 derajat menggeser batas kuadran sehingga fitur tugas A tidak lagi sepenuhnya pas. - Baris 50-64 (
latih_minibatch): loop pelatihan mini-batch yang mengembalikan riwayat akurasi validasi per epoch; satu fungsi dipakai ketiga tahap agar perlakuannya seragam. - Baris 67-76: tugas A dilatih tuntas (0.998); tugas B menyediakan 40 sampel latih dan 400 validasi yang batasnya diputar.
- Baris 79-88 (tahap 1): body dua layer pertama dibekukan, head baru dilatih Adam 20 epoch — persis pola Chapter Transfer Learning — dan berhenti di validasi 0.913.
- Baris 90 (
bobot_awal): salinan bobot layer pertama body menjadi patokan untuk mengukur seberapa jauh tiap varian tahap 2 menggesernya. - Baris 93-99 (tahap 2a): salinan model tahap 1 dibuka dengan
requires_grad_(True), lalu optimizer SGD menerima dua param group — lr body 0.005, lr head 0.05, momentum 0.9. - Baris 101 (
delta_a): norma L2 selisih bobot body terhadap patokan — seberapa jauh bobot pretrained bergeser. - Baris 104-112 (tahap 2b): salinan yang sama dari tahap 1 dibuka kembali, tetapi lr body dijadikan 5.0 — seratus kali lr head — untuk mendemonstrasikan jebakan learning rate raksasa.
- Baris 114-122: tabel hasil; varian benar bergeser 0.095 dan validasi naik menjadi 0.920 (+0.007), varian jebakan bergeser 20.159 (sekitar 200x lebih jauh) dan validasi turun ke 0.815 (−0.098).
Membaca Angka: Polesan Halus versus Perombakan Total #
Dua varian tahap 2 berangkat dari bobot yang identik — satu-satunya perbedaan adalah lr body. Varian 2a menggeser bobot body sejauh 0.095: pembaruan kecil yang cukup untuk menyesuaikan fitur terhadap batas yang diputar, dan validasi naik tipis ke 0.920. Varian 2b menggeser bobot sejauh 20.159 — bobot hasil pretraining nyaris kehilangan keterkaitannya dengan posisi awal — dan validasi justru turun ke 0.815, lebih rendah daripada titik berangkatnya. Fine-tuning yang benar terlihat dari angka pergeseran yang kecil; angka besar adalah tanda lr terlalu tinggi.
Pilihan cakupan unfreeze mengikuti jumlah data yang tersedia:
| Strategi | lr body | Kebutuhan data | Kapan dipakai |
|---|---|---|---|
| Feature extraction | 0 (body tetap beku) | puluhan-ratusan sampel | tugas baru sangat mirip tugas asal |
| Fine-tuning sebagian (layer akhir body dibuka) | kecil, seragam | ratusan-ribuan | domain bergeser ringan; layer awal tetap universal |
| Fine-tuning penuh (seluruh body dibuka) | kecil, diskriminatif per grup layer | ribuan+ | tugas cukup berbeda tetapi masih satu rumpun |
Pada proyek nyata, pola ini bekerja pada skala jauh lebih besar — membuka seluruh atau sebagian ResNet untuk dataset gambar baru, atau menyesuaikan LLM dengan teknik hemat parameter seperti LoRA. Praktiknya dibahas pada Chapter Transfer Learning untuk Gambar dan disarikan di tutorial resmi pytorch.org/tutorials/beginner/transfer_learning_tutorial . Dengan fine-tuning, kotak perkakas kita kini lengkap dari sisi model dan data; Chapter penutup section ini menyatukan semuanya dalam satu kerangka — mengukur generalisasi dan memutuskan kapan masing-masing alat dipakai.