Tujuh chapter sebelumnya membangun representation dari nol: pretext task, pasangan kontras, topeng pada teks dan gambar, lalu alat ukurnya. Di praktik nyata, sebagian besar pekerjaan justru dimulai dari ujung lain — pretrained encoder sudah tersedia (buatan sendiri atau diunduh dari publik), dan tugas kita hanya memutuskan cara memakainya. Tiga pilihan utama membentuk sebuah tangga: membekukan seluruh encoder dan melatih kepala linear (probe), membuka seluruh bobot untuk dilatih ulang (fine-tune), atau mengabaikan pretrained sama sekali (dari nol). Chapter penutup ini membandingkan ketiganya dengan anggaran label yang sama, mengukur risiko domain mismatch, lalu menutup section dengan peta ekosistem pretrained model modern.

Tangga Tiga Level dan Biayanya #

Logika tangganya lugas. Representation yang sudah baik hanya butuh pemisah linear — cukup probe di atas backbone beku. Fine-tune memberi ruang lebih: seluruh bobot bisa menyesuaikan diri ke data baru, tetapi biaya dan risikonya naik — data berlabel sedikit bisa justru merusak representation yang sudah matang. Level-level itu dirangkum berikut:

LevelYang dilatihLabel dibutuhkanBiaya komputasiRisiko utama
Freeze + probekepala linear sajasedikitsangat ringanfitur kurang cocok ke tugas
Fine-tuneseluruh bobotsedangberat (bisa pakai lr kecil)lupa representation (catastrophic forgetting)
Dari nolseluruh bobotbanyakberat penuhdata label tak cukup

Praktik umum menyarankan memulai dari probe: jika akurasinya sudah memadai, fine-tune tidak diperlukan. Bila probe kurang, naik ke fine-tune dengan learning rate kecil pada backbone dan learning rate lebih besar pada kepala — strategi yang dipakai di demo ini.

Demo: Tiga Strategi pada Anggaran Label Sama #

Tahap pertama skrip melatih encoder kontras (mesin InfoNCE chapter Contrastive Learning, versi ringkas) pada tiga cincin konsentris tanpa label. Tahap kedua memberi anggaran 15 label (lima per kelas) dan membandingkan tiga strategi — semuanya memakai sampel label yang identik agar perbandingannya adil:

python
"""Penggunaan pretrained representation: tangga freeze -> probe -> fine-tune.

Tahap 1 (pretraining, 0 label): tiga cincin konsentris 2D; encoder mini
2->32->16 dilatih kontras -- pasangan positif = dua rotasi acak dari
titik yang sama, negatif = titik lain dalam batch, loss InfoNCE
(rincian di chapter Contrastive Learning).
Tahap 2 (downstream, 15 label = 5 per kelas): tiga strategi dibandingkan
pada data dan anggaran label yang sama:
  A. dari nol       : MLP baru berarsitektur setara, hanya 15 label.
  B. frozen + probe : encoder dibekukan, LogisticRegression pada fitur.
  C. fine-tune      : encoder + kepala baru; kepala lr besar, encoder lr kecil.
Uji domain: data uji dirotasi 90 derajat vs diskalakan 1.4x.
Diuji di CPU dengan PyTorch 2.14, seed tetap, deterministik.
"""

import math

import torch
import torch.nn as nn
import torch.nn.functional as F
from sklearn.linear_model import LogisticRegression

torch.manual_seed(0)

# ---------- 1. Data: tiga cincin konsentris (kelas = jari-jari) ----------
def cincin(r_lo, r_hi, n):
    r = r_lo + (r_hi - r_lo) * torch.rand(n)
    t = 2 * math.pi * torch.rand(n)
    return torch.stack([r * torch.cos(t), r * torch.sin(t)], dim=1)


N_PER = 500
X = torch.cat([cincin(0.3, 0.6, N_PER), cincin(1.2, 1.5, N_PER),
               cincin(2.1, 2.4, N_PER)])
y = torch.repeat_interleave(torch.arange(3), N_PER)
acak = torch.randperm(X.shape[0])
X, y = X[acak], y[acak]
X_latih, y_latih = X[:1200], y[:1200]
X_uji, y_uji = X[1200:], y[1200:]
print(f"data : {X_latih.shape[0]} latih + {X_uji.shape[0]} uji, 3 kelas cincin")
print(f"kelas hanya dipakai di tahap evaluasi, tidak untuk pretraining")
print()


# ---------- 2. Pretraining kontras (0 label) ----------
class Encoder(nn.Module):
    """Encoder f: 2 -> 32 -> 16 (ReLU) yang akan dipakai ulang."""

    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(nn.Linear(2, 32), nn.ReLU(),
                                 nn.Linear(32, 16), nn.ReLU())

    def forward(self, x):
        return self.net(x)


def rotasi(x, derajat_maks):
    sudut = (2 * torch.rand(x.shape[0]) - 1) * derajat_maks * math.pi / 180
    c, s = torch.cos(sudut), torch.sin(sudut)
    return torch.stack([x[:, 0] * c - x[:, 1] * s,
                        x[:, 0] * s + x[:, 1] * c], dim=1)


def pretrain(epoch=1200, B=256, tau=0.5, seed=5):
    """SimCLR-mini: positif = dua rotasi titik yang sama; negatif in-batch."""
    torch.manual_seed(seed)
    f = Encoder()
    g = nn.Linear(16, 8)                             # kepala proyeksi dibuang
    opt = torch.optim.Adam(list(f.parameters()) + list(g.parameters()), lr=3e-3)
    for _ in range(epoch):
        xb = X_latih[torch.randint(0, X_latih.shape[0], (B,))]
        v1 = rotasi(xb, 180) + 0.1 * torch.randn_like(xb)
        v2 = rotasi(xb, 180) + 0.1 * torch.randn_like(xb)
        z = F.normalize(torch.cat([g(f(v1)), g(f(v2))], 0), dim=1)
        sim = z @ z.T / tau
        sim.fill_diagonal_(float("-inf"))
        idx_pos = (torch.arange(2 * B) + B) % (2 * B)
        loss = -sim.log_softmax(1)[torch.arange(2 * B), idx_pos].mean()
        opt.zero_grad(); loss.backward(); opt.step()
    return f, loss.item()


print("=== Tahap 1: pretraining kontras (tanpa label) ===")
encoder, loss_akhir = pretrain()
print(f"  1200 epoch selesai, loss InfoNCE akhir {loss_akhir:.3f}")
print()

# ---------- 3. Tahap 2: tiga strategi dengan 15 label ----------
N_LABEL = 15                                        # 5 label per kelas


def sampel_label(seed):
    g = torch.Generator().manual_seed(seed)
    idx = torch.randperm(X_latih.shape[0], generator=g)[:N_LABEL]
    return idx


def dari_nol(seed, epoch=400):
    """Strategi A: MLP baru, arsitektur setara encoder + kepala."""
    torch.manual_seed(seed)
    idx = sampel_label(seed)
    m = nn.Sequential(nn.Linear(2, 32), nn.ReLU(), nn.Linear(32, 16),
                      nn.ReLU(), nn.Linear(16, 3))
    opt = torch.optim.Adam(m.parameters(), lr=3e-3)
    for _ in range(epoch):
        loss = nn.CrossEntropyLoss()(m(X_latih[idx]), y_latih[idx])
        opt.zero_grad(); loss.backward(); opt.step()
    with torch.no_grad():
        return (m(X_uji).argmax(1) == y_uji).float().mean().item()


def probe(ambil=5):
    """Strategi B: encoder dibekukan, LogisticRegression pada fitur."""
    total = 0.0
    with torch.no_grad():
        fitur_latih = encoder(X_latih).numpy()
        fitur_uji = encoder(X_uji).numpy()
    for s in range(ambil):
        idx = sampel_label(100 + s)
        clf = LogisticRegression(max_iter=2000)
        clf.fit(fitur_latih[idx], y_latih[idx].numpy())
        total += clf.score(fitur_uji, y_uji.numpy())
    return total / ambil


def fine_tune(seed, epoch=300):
    """Strategi C: encoder + kepala; lr kepala besar, lr encoder kecil."""
    torch.manual_seed(seed)
    idx = sampel_label(seed)
    f = Encoder()
    f.load_state_dict(encoder.state_dict())         # salin bobot pretrained
    kepala = nn.Linear(16, 3)
    opt = torch.optim.Adam([
        {"params": f.parameters(), "lr": 2e-4},     # encoder digeser pelan
        {"params": kepala.parameters(), "lr": 1e-2},  # kepala cepat
    ])
    for _ in range(epoch):
        loss = nn.CrossEntropyLoss()(kepala(f(X_latih[idx])), y_latih[idx])
        opt.zero_grad(); loss.backward(); opt.step()
    with torch.no_grad():
        return (kepala(f(X_uji)).argmax(1) == y_uji).float().mean().item()


print(f"=== Tahap 2: downstream {N_LABEL} label (rata-rata 5 pengambilan) ===")
with torch.no_grad():
    clf_raw = LogisticRegression(max_iter=2000)
    idx_raw = sampel_label(100)
    clf_raw.fit(X_latih[idx_raw].numpy(), y_latih[idx_raw].numpy())
    acc_raw = clf_raw.score(X_uji.numpy(), y_uji.numpy())
acc_nol = sum(dari_nol(100 + s) for s in range(5)) / 5
acc_probe = probe()
acc_ft = sum(fine_tune(100 + s) for s in range(5)) / 5
print(f"  regresi linear pada fitur mentah : {acc_raw:.3f}")
print(f"  A. dari nol (15 label saja)      : {acc_nol:.3f}")
print(f"  B. frozen encoder + linear probe : {acc_probe:.3f}")
print(f"  C. fine-tune (encoder + kepala)  : {acc_ft:.3f}")
print()


# ---------- 4. Biaya: jumlah bobot yang dilatih ----------
print("=== Biaya: bobot yang ikut dilatih per strategi ===")
n_enc = sum(p.numel() for p in encoder.parameters())
n_kepala = 16 * 3 + 3
print(f"  A. dari nol  : {n_enc + n_kepala} bobot dilatih dari nilai acak")
print(f"  B. probe     : {n_kepala} bobot ({n_enc} dibekukan)")
print(f"  C. fine-tune : {n_enc + n_kepala} bobot (mulai dari pretrained)")
print()


# ---------- 5. Uji domain: rotasi 90 vs skala 1.4 ----------
print("=== Uji domain (probe 60 label, data uji dimodifikasi) ===")
with torch.no_grad():
    fitur_latih = encoder(X_latih).numpy()
    idx60 = sampel_label(77)
    clf = LogisticRegression(max_iter=2000)
    clf.fit(fitur_latih[idx60], y_latih[idx60].numpy())
    X_rot = torch.stack([-X_uji[:, 1], X_uji[:, 0]], dim=1)   # rotasi 90
    X_skal = X_uji * 1.4                                       # skala 1.4x
    a_asli = clf.score(encoder(X_uji).numpy(), y_uji.numpy())
    a_rot = clf.score(encoder(X_rot).numpy(), y_uji.numpy())
    a_skal = clf.score(encoder(X_skal).numpy(), y_uji.numpy())
print(f"  data uji asli      : {a_asli:.3f}")
print(f"  dirotasi 90 derajat: {a_rot:.3f}  (pretraining memakai rotasi)")
print(f"  diskalakan 1.4x    : {a_skal:.3f}  (domain baru, akurasi turun)")

Output yang Dihasilkan:

text
data : 1200 latih + 300 uji, 3 kelas cincin
kelas hanya dipakai di tahap evaluasi, tidak untuk pretraining

=== Tahap 1: pretraining kontras (tanpa label) ===
  1200 epoch selesai, loss InfoNCE akhir 5.218

=== Tahap 2: downstream 15 label (rata-rata 5 pengambilan) ===
  regresi linear pada fitur mentah : 0.340
  A. dari nol (15 label saja)      : 0.807
  B. frozen encoder + linear probe : 0.863
  C. fine-tune (encoder + kepala)  : 0.911

=== Biaya: bobot yang ikut dilatih per strategi ===
  A. dari nol  : 675 bobot dilatih dari nilai acak
  B. probe     : 51 bobot (624 dibekukan)
  C. fine-tune : 675 bobot (mulai dari pretrained)

=== Uji domain (probe 60 label, data uji dimodifikasi) ===
  data uji asli      : 1.000
  dirotasi 90 derajat: 1.000  (pretraining memakai rotasi)
  diskalakan 1.4x    : 0.800  (domain baru, akurasi turun)

Mari kita telaah kode di atas baris per baris:

  1. Baris 26-38 (data): tiga cincin dengan celah radial cukup lebar; kelas hanya dipakai untuk evaluasi, tidak pernah dibaca pretraining.
  2. Baris 46-80 (pretraining): mesin InfoNCE versi ringkas dari chapter Contrastive Learning — dua rotasi acak per titik sebagai positif, seluruh batch sebagai negatif, kepala proyeksi g dibuang setelah selesai.
  3. Baris 89-96 (sampel_label): generator ber-seed memastikan strategi A, B, dan C menerima sampel 15 label yang persis sama — syarat perbandingan adil.
  4. Baris 99-110 (dari_nol): MLP baru berarsitektur setara encoder-plus-kepala dilatih hanya dari 15 label; hasil 0.807 karena jaringan harus melipat cincin dari nol.
  5. Baris 113-123 (probe): encoder(X) dihitung sekali di bawah no_grad, lalu LogisticRegression diulang lima kali; 624 dari 675 bobot tidak pernah tersentuh.
  6. Baris 126-143 (fine_tune): load_state_dict menyalin bobot pretrained, dan torch.optim.Adam menerima dua kelompok parameter — learning rate 1e-2 untuk kepala, 2e-4 untuk encoder — sehingga kepala cepat terlatih sementara representation bergeser hati-hati.
  7. Baris 171-185 (uji domain): probe 60 label diuji pada data asli, data dirotasi 90°, dan data diskalakan 1.4× — rotasi bertahan sempurna, skala menurunkan akurasi ke 0.800.

Membaca Tangga dan Batas Domain #

Empat angka tahap kedua membentuk tangga yang rapi: regresi linear mentah 0.340 (nyaris menebak), dari nol 0.807, probe 0.863, fine-tune 0.911. Urutan ini khas: pretrained mengalahkan dari nol, dan fine-tune mengungguli probe karena representation awal bagus plus ruang penyesuaian. Biayanya terbalik dengan akurasinya — probe melatih 51 bobot, dua strategi lainnya 675; pada model produksi rasionya jauh lebih ekstrem (belasan versus ratusan juta parameter).

Bagian uji domain adalah peringatan yang jujur: representation hanya andal pada pergeseran yang pretext task-nya memang cover. Encoder kita tahan rotasi karena dilatih dengan rotasi, tetapi rapuh terhadap skala karena tidak pernah melihatnya. Pada kasus nyata, encoder foto web bisa menurun tajam pada citra medis; solusinya fine-tune pada data domain tersebut, atau pretraining ulang bila datanya melimpah.

Ekosistem Pretrained Model Hari Ini #

Seluruh pola pada chapter ini — unduh encoder, bekukan, probe, fine-tune — kini menjadi alur kerja standar berkat ekosistem Hugging Face. Pustaka transformers menyediakan puluhan ribu model pretrained (BERT, CLIP, encoder vision) dengan API seragam; Hugging Face Hub menjadi tempat membagikannya lengkap dengan kartu model yang mencantumkan data pelatihan. Untuk embedding kalimat siap pakai, keluarga sentence-transformers menghasilkan vektor per kalimat — pasangan langsung bagi ukuran kemiripan chapter Embedding dan Similarity.

Sintesis Section #

Kita menutup section ini dengan satu kalimat rangkuman: data mentah membangun representation, label sedikit memandu pemakaiannya. Supervised vs Self-Supervised menunjukkan sinyal supervisi bisa lahir gratis; pasangan kontras menjadikannya mesin pembelajaran; masked modeling mengulang pola itu pada teks dan gambar; linear probe mengukur hasilnya; embedding menjadikannya produk yang bisa dicari; tangga freeze-probe-fine-tune menuntun pemakaiannya. Representasi yang baik adalah modal untuk dua arah berikutnya: menghasilkan konten baru di materi generative model, atau merawat model di evaluasi dan debugging.