Kata “representation” sudah muncul berulang kali di section ini tanpa definisi operasional. Representation adalah bentuk data setelah diolah sejumlah layer — fitur per-tingkat yang baik membuat struktur data (klaster, manifold, kelas) makin mudah dipisahkan. Representation learning adalah seni melatih jaringan agar fitur-fitur itu menjadi baik, baik lewat label (supervised), lewat pretext task (self-supervised), atau keduanya. Pertanyaan praktisnya segera muncul: bagaimana mengukur keseberapa baik sebuah representation? Chapter ini memperkenalkan alat ukurnya — linear probe di atas backbone beku — lalu memakai alat itu untuk membandingkan fitur tiap layer jaringan terlatih melawan jaringan berbobot acak.

Fitur Berjenjang dan Cara Mengukurnya #

Sebuah jaringan saraf bisa dibaca sebagai rantai transformasi: masukan mentah → fitur layer-1 → fitur layer-2 → … → keluaran. Setiap tingkat punya klaim kualitas yang berbeda, dan klaim itu harus diukur dengan perlakuan yang setara supaya perbandingannya adil. Linear probe menjawabnya: bekukan seluruh jaringan (frozen backbone), ambil fitur pada tingkat tertentu, lalu latih hanya satu lapisan linear kecil — biasanya regresi logistik — di atasnya. Akurasi probe menjadi skor kualitas fitur: jika struktur kelas sudah terpisah linier pada fitur tersebut, probe kecil pun berhasil.

Mengapa harus linear? Karena probe yang kuat bisa menutupi kelemahan fitur: dengan kapasitas cukup, probe non-linear menyelesaikan banyak hal sendirian sehingga skor tidak lagi mencerminkan kualitas representation. Konvensi linear ini juga dipakai luas di literatur untuk menilai pretrained model — dari fitur ImageNet sampai encoder kontras seperti chapter Contrastive Learning. Sumber-sumber representation yang umum dirangkum berikut:

Sumber representationCara diperolehSifat khas
Fitur mentah (piksel, koordinat)tanpa pelatihanstruktur asli belum terlipat
Fitur rancangan manual (SIFT, HOG)dirancang manusiadomain-spesifik, tidak belajar
Layer-N jaringan terlatihsupervised pada tugas apa punmakin dalam makin terpisah linier
Encoder hasil pretraining SSLcontrastive / masked modelinglabel tak dibutuhkan

Demo: Probe Berjenjang pada Spiral Tiga Lengan #

Data kita berupa spiral tiga lengan 2D — tiga kelas yang batasnya melingkar sehingga tak mungkin dipisah garis lurus di ruang mentah. Sebuah MLP 2→32→32→3 dilatih terawasi penuh; sebagai efek samping, jaringan itu juga membangun representation di tiap layernya. Probe 60 label kemudian mengukur fitur mentah, fitur layer-1, dan fitur layer-2 — masing-masing dibandingkan dengan jaringan identik berbobot acak:

python
"""Representation learning: fitur per layer dan pengukuran linear probe.

Data: spiral tiga lengan 2D (tiga kelas; batasnya melingkar sehingga
 tidak bisa dipisahkan garis lurus di ruang mentah). Sebuah MLP
2->32->32->3 dilatih terawasi penuh; sebagai efek sampingnya jaringan
membangun representation berjenjang: layer pertama mulai melipat
ruang, layer kedua membuat kelas nyaris terpisah linier. Kualitas
tiap level diukur dengan linear probe: LogisticRegression memakai
60 label pada fitur yang DIBEKUKAN (frozen backbone). Pembanding:
jaringan berarsitektur sama dengan bobot acak. Diuji di CPU dengan
PyTorch 2.14, seed tetap.
"""

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

torch.manual_seed(0)

# ---------- 1. Data: spiral tiga lengan tanpa garis pemisah lurus ----------
import math

N_PER, DERAU = 300, 0.12
PUTAR = 3 * math.pi
titik, kelas = [], []
for c in range(3):
    t = torch.rand(N_PER) * PUTAR
    r = 0.3 + (t / PUTAR) * 1.9                  # jari-jari tumbuh seiring sudut
    th = t + c * 2 * math.pi / 3                 # offset antar lengan
    x = r * torch.cos(th) + DERAU * torch.randn(N_PER)
    y = r * torch.sin(th) + DERAU * torch.randn(N_PER)
    titik.append(torch.stack([x, y], dim=1))
    kelas.append(torch.full((N_PER,), c, dtype=torch.long))
X = torch.cat(titik)
y = torch.cat(kelas)
acak = torch.randperm(X.shape[0])
X, y = X[acak], y[acak]
X_latih, y_latih = X[:600], y[:600]
X_uji, y_uji = X[600:], y[600:]
print(f"data : {X_latih.shape[0]} latih + {X_uji.shape[0]} uji, 3 kelas spiral")
print(f"lengan berputar {PUTAR / math.pi:.1f}x pi, derau {DERAU}")
print()


# ---------- 2. MLP dengan pintu fitur per layer ----------
class MLPBertingkat(nn.Module):
    """MLP 2->32->32->3 yang bisa memuntaskan fitur tiap layer."""

    def __init__(self):
        super().__init__()
        self.l1 = nn.Linear(2, 32)
        self.l2 = nn.Linear(32, 32)
        self.l3 = nn.Linear(32, 3)

    def fitur(self, x):
        """Kembalikan fitur mentah, setelah layer-1, setelah layer-2."""
        z1 = torch.relu(self.l1(x))
        z2 = torch.relu(self.l2(z1))
        return x, z1, z2

    def forward(self, x):
        _, _, z2 = self.fitur(x)
        return self.l3(z2)


def latih_mlp(seed):
    torch.manual_seed(seed)
    model = MLPBertingkat()
    opt = torch.optim.Adam(model.parameters(), lr=0.01)
    for _ in range(1500):
        loss = nn.CrossEntropyLoss()(model(X_latih), y_latih)
        opt.zero_grad(); loss.backward(); opt.step()
    return model


model_terlatih = latih_mlp(seed=7)
with torch.no_grad():
    acc = (model_terlatih(X_uji).argmax(1) == y_uji).float().mean().item()
print(f"MLP terlatih penuh : akurasi uji {acc:.3f} (1500 epoch, 600 label)")
model_acak = MLPBertingkat()
torch.manual_seed(99)
for p in model_acak.parameters():          # bobot acak, tidak pernah dilatih
    torch.nn.init.normal_(p, std=0.5)
print()


# ---------- 3. Linear probe: 60 label pada fitur yang dibekukan ----------
NAMA_LEVEL = ["fitur mentah (x, y)     ", "setelah layer-1 (32 fitur)",
              "setelah layer-2 (32 fitur)"]


def probe(fitur_latih, fitur_uji, n_label=60, ambil=5):
    """Rata-rata akurasi probe LogisticRegression dari `ambil` sampel label."""
    total = 0.0
    for s in range(ambil):
        g = torch.Generator().manual_seed(300 + s)
        idx = torch.randperm(fitur_latih.shape[0], generator=g)[:n_label]
        clf = LogisticRegression(max_iter=2000)
        clf.fit(fitur_latih[idx].numpy(), y_latih[idx].numpy())
        total += clf.score(fitur_uji.numpy(), y_uji.numpy())
    return total / ambil


print("=== Linear probe 60 label (fitur dibekukan, rata-rata 5 sampel) ===")
print(f"{'sumber fitur':28s} {'bobot acak':>11s} {'terlatih':>9s}")
with torch.no_grad():
    f_acak = model_acak.fitur(X_latih), model_acak.fitur(X_uji)
    f_trai = model_terlatih.fitur(X_latih), model_terlatih.fitur(X_uji)
    for lv in range(3):
        a = probe(f_acak[0][lv], f_acak[1][lv])
        t = probe(f_trai[0][lv], f_trai[1][lv])
        print(f"{NAMA_LEVEL[lv]:28s} {a:11.3f} {t:9.3f}")
print()


# ---------- 4. Jumlah bobot yang dilatih: probe vs jaringan penuh ----------
print("=== Biaya: berapa bobot yang ikut dilatih ===")
n_prosen = sum(p.numel() for p in model_terlatih.l3.parameters())
n_semua = sum(p.numel() for p in model_terlatih.parameters())
print(f"  jaringan penuh (fine-tune) : {n_semua} bobot")
print(f"  kepala probe saja (frozen) : {n_prosen} bobot "
      f"({100 * n_prosen / n_semua:.1f}% dari total)")

Output yang Dihasilkan:

text
data : 600 latih + 300 uji, 3 kelas spiral
lengan berputar 3.0x pi, derau 0.12

MLP terlatih penuh : akurasi uji 0.897 (1500 epoch, 600 label)

=== Linear probe 60 label (fitur dibekukan, rata-rata 5 sampel) ===
sumber fitur                  bobot acak  terlatih
fitur mentah (x, y)                0.388     0.388
setelah layer-1 (32 fitur)         0.370     0.425
setelah layer-2 (32 fitur)         0.405     0.821

=== Biaya: berapa bobot yang ikut dilatih ===
  jaringan penuh (fine-tune) : 1251 bobot
  kepala probe saja (frozen) : 99 bobot (7.9% dari total)

Mari kita telaah kode di atas baris per baris:

  1. Baris 23-41 (spiral): tiap lengan dibangun dari sudut acak t dengan jari-jari yang tumbuh seiring t plus offset 120° antar kelas, lalu derau kecil — menghasilkan tiga kurva yang saling melingkar dan mustahil dipisah garis lurus.
  2. Baris 46-63 (MLPBertingkat): metode fitur mengembalikan tiga tingkat sekaligus — masukan mentah, keluaran ReLU layer-1, keluaran ReLU layer-2 — sementara forward memakai jalur yang sama sampai kepala l3. Arsitektur identik memastikan perbandingan probe adil.
  3. Baris 76-83 (dua jaringan): jaringan terlatih dilatih 1500 epoch penuh label; jaringan acak hanya diinisialisasi ulang dengan torch.nn.init.normal_ tanpa pernah belajar.
  4. Baris 92-98 (probe): lima sampel label berbeda (seed 300+s), masing-masing melatih LogisticRegression pada 60 label dan diuji ke 300 titik uji — fitur jaringan tidak pernah diubah.
  5. Baris 116-121 (biaya): probe hanya menyentuh bobot kepala l3 (99 dari 1251, kurang dari 8%), itulah arti praktis “backbone beku”.

Membaca Tabel Probe #

Tiga angka pada kolom terlatih membentuk tangga yang jelas: 0.388 pada fitur mentah, 0.425 setelah layer-1, 0.821 setelah layer-2. Kolom bobot acak nyaris datar di sekitar 0.4 — transformasi linear acak plus ReLU tidak melipat spiral secara berarti. Jadi bukan jumlah parameter yang menentukan kualitas fitur, melainkan pelatihannya: hanya jaringan yang belajar tugas yang menggeser struktur kelas menjadi terpisah linier, dan pergeseran itu baru tuntas di layer kedua. Menarik juga membandingkan probe 0.821 dengan jaringan penuh 0.897 — kepala l3 yang dilatih 600 label memang lebih tajam daripada probe 60 label, tetapi jaraknya kecil: hampir seluruh kemampuan jaringan ternyata tersimpan di representation-nya, bukan di kepalanya.

Dengan fitur yang sudah terbukti bagilah pretrained model modern dibagikan ke publik: pengguna membekukan backbone, melatih kepala kecil, atau membuka seluruh bobot untuk fine-tuning. Tiga level pemakaian itu — lengkap dengan biaya dan risikonya — menjadi topik chapter penutup section ini: Penggunaan Pretrained Representation.