Dua chapter sebelumnya menutup siklus adaptasi dengan satu kalimat yang jarang dipertanyakan: seluruh bobot model ikut diperbarui. Pada decoder mini 18 ribu parameter, pilihan itu tidak terasa. Pada LLM 7 miliar parameter, full fine-tuning menuntut seluruh bobot hadir di memori sebagai gradien dan status optimizer, menghasilkan salinan model utuh untuk setiap tugas, dan menggeser kemampuan lama — catastrophic forgetting yang sudah kita ukur naik 40% pada chapter Supervised Fine-Tuning . Parameter-efficient fine-tuning (PEFT) membalik pilihan itu: bekukan hampir seluruh bobot, latih hanya modul kecil yang disisipkan atau ditambahkan. Bab ini memetakan keluarga metodenya, lalu membuktikan konsep intinya lewat demo adapter — 4,6% parameter, akurasi di atas full fine-tuning, kemampuan lama utuh.
Tiga Biaya Full Fine-Tuning #
Biaya pertama adalah memori pelatihan. Untuk setiap parameter yang dilatih, Adam menyimpan bobot, gradien, dan dua status (momentum serta varians): kira-kira 16 byte per parameter dalam fp32. Model 7 miliar parameter membutuhkan lebih dari 100 GB hanya untuk ketiganya — sebelum memperhitungkan aktivasi — jauh melebihi GPU konsumen. Biaya kedua adalah penyimpanan: setiap adaptasi menghasilkan salinan model utuh (7B dalam fp16 sudah 14 GB), sehingga sepuluh tugas berarti sepuluh salinan penuh. Biaya ketiga sudah kita buktikan dengan angka: menggeser seluruh bobot ke distribusi data baru merusak kemampuan pra-latih.
Asumsi kerja PEFT menjawab ketiganya sekaligus: adaptasi ke tugas baru tidak membutuhkan perubahan seluruh representasi — cukup koreksi kecil pada jalur yang tepat, sementara representasi umum hasil pretraining dibiarkan apa adanya.
Keluarga Metode PEFT #
Empat pendekatan utama berkembang hampir bersamaan. Adapter (Houlsby dkk.
) menyisipkan module kecil di dalam tiap blok transformer: proyeksi turun ke dimensi sempit, non-linearitas, proyeksi naik kembali, dengan jalur residual agar module bisa di-nol-kan saat awal. LoRA mengganti sebagian bobot proyeksi dengan koreksi rank rendah ΔW = B·A yang dilatih di atas bobot beku — dan karena koreksinya berbentuk matriks, koreksi bisa digabungkan ke bobot saat deployment sehingga tidak ada latensi tambahan. Prompt tuning (Lester dkk.
) melatih embedding beberapa token virtual yang ditautkan di depan input; bobot model tidak disentuh sama sekali. BitFit (Ben Zaken dkk.
) paling sederhana: hanya vektor bias tiap lapis yang diperbarui.
Diagram di atas menunjukkan penempatan yang kita pakai di demo: dua sisipan adapter di antara lapis-lapis yang dibekukan. Pada transformer nyata, adapter disisipkan setelah sub-layer attention dan feed-forward di setiap blok. Perbandingan keempat metode dirangkum berikut:
| Metode | Yang dilatih | Skala (model 7B) | Karakter |
|---|---|---|---|
| Full fine-tuning | seluruh bobot | 7 miliar (100%) | batas atas kualitas; paling mahal |
| Adapter | module kecil yang disisipkan | puluhan juta (<1%) | menambah kedalaman; sedikit menambah latensi inference |
| LoRA | ΔW = B·A rank rendah | jutaan (0,1-1%) | digabung ke bobot saat deploy; tanpa latensi ekstra |
| Prefix / prompt tuning | embedding token virtual | ribuan-jutaan | tidak menyentuh bobot model sama sekali |
| Bias-only (BitFit) | vektor bias | jutaan (<0,1%) | paling sederhana; kapasitas adaptasi terbatas |
Demo: Empat Koma Enam Persen Parameter #
Demo memakai MLP tiga lapis (128 → 256 → 256 → 2, total 99.330 parameter) sebagai model dasar yang di-pra-latih pada Tugas A: sinyal kelas pada dimensi 0. Tugas B memindahkan sinyal ke dimensi 1 dan menjadikan dimensi 0 penanda berlawanan — keputusan desain yang sama dengan demo LoRA pada chapter berikutnya, sehingga angka kedua chapter bisa dibandingkan. Contoh diuji nyata pada CPU dengan PyTorch 2.14 dan selesai dalam sekitar 8 detik.
"""Parameter-efficient fine-tuning mini: adapter pada model dasar beku.
Bagian 1: MLP dasar (128-256-256-2) di-pra-latih pada Tugas A: sinyal kelas
pada dimensi 0. Tugas B memindahkan sinyal ke dimensi 1 dan
menjadikan dimensi 0 penanda lawan — model beku tertipu.
Bagian 2: bukti inisialisasi identitas: sisipan adapter (proyeksi turun
r=4 -> ReLU -> proyeksi naik, residual) di-nol-kan pada proyeksi
naik sehingga logits model beradapter = logits model dasar.
Bagian 3: dua jalur adaptasi Tugas B — full fine-tuning seluruh bobot vs
adapter dengan seluruh bobot dasar dibekukan.
Bagian 4: anggaran parameter + ukuran berkas per tugas, lalu akurasi
Tugas A diukur ulang: full FT lupa, adapter mempertahankan.
Diuji di CPU dengan PyTorch 2.14, seed tetap, deterministik.
"""
import copy
import torch
import torch.nn as nn
import torch.nn.functional as F
torch.manual_seed(0)
D_IN, D_H, R = 128, 256, 4 # dimensi masukan, dimensi tersembunyi, rank adapter
def buat_data(n, dim_sinyal, dim_lawan, seed):
"""Klasifikasi 2 kelas: sinyal kelas pada satu dimensi fitur."""
g = torch.Generator().manual_seed(seed)
x = torch.randn(n, D_IN, generator=g)
s = torch.randint(0, 2, (n,), generator=g) * 2 - 1 # label -1 / +1
x[:, dim_sinyal] += s * 2.5 # sinyal kelas
if dim_lawan is not None:
x[:, dim_lawan] -= s * 2.5 # penanda lawan
return x, (s > 0).long()
# Tugas A: sinyal di dimensi 0. Tugas B: sinyal pindah ke dimensi 1,
# dimensi 0 justru membawa penanda berlawanan.
xA, yA = buat_data(512, 0, None, 11)
xA_uji, yA_uji = buat_data(256, 0, None, 12)
xB, yB = buat_data(512, 1, 0, 21)
xB_uji, yB_uji = buat_data(256, 1, 0, 22)
class ModelDasar(nn.Module):
"""MLP tiga lapis yang berperan sebagai model pra-latih."""
def __init__(self):
super().__init__()
self.l1 = nn.Linear(D_IN, D_H)
self.l2 = nn.Linear(D_H, D_H)
self.keluar = nn.Linear(D_H, 2)
def forward(self, x):
h = F.relu(self.l1(x))
h = F.relu(self.l2(h))
return self.keluar(h)
class Adapter(nn.Module):
"""Sisipan kecil: turun ke rank r, non-linear, naik kembali + residual."""
def __init__(self, d, r):
super().__init__()
self.turun = nn.Linear(d, r)
self.naik = nn.Linear(r, d)
nn.init.zeros_(self.naik.weight) # koreksi nol saat awal ->
nn.init.zeros_(self.naik.bias) # model beradapter == model dasar
def forward(self, h):
return h + self.naik(F.relu(self.turun(h)))
class ModelBeradapter(nn.Module):
"""Model dasar utuh + dua sisipan adapter setelah tiap lapis tersembunyi."""
def __init__(self, dasar, r):
super().__init__()
self.dasar = dasar
self.ad1 = Adapter(D_H, r)
self.ad2 = Adapter(D_H, r)
def forward(self, x):
h = F.relu(self.dasar.l1(x))
h = self.ad1(h)
h = F.relu(self.dasar.l2(h))
h = self.ad2(h)
return self.dasar.keluar(h)
def akurasi(model, x, y):
with torch.no_grad():
return (model(x).argmax(1) == y).float().mean().item()
def latih(model, x, y, epoch, lr):
"""Training loop penuh-batch; hanya parameter requires_grad dilatih."""
opt = torch.optim.Adam([p for p in model.parameters() if p.requires_grad],
lr=lr)
loss = None
for ep in range(epoch):
loss = F.cross_entropy(model(x), y)
opt.zero_grad()
loss.backward()
opt.step()
return loss.item()
# ---------- 1. Pretrain Tugas A ----------
dasar = ModelDasar()
total_param = sum(p.numel() for p in dasar.parameters())
loss_A = latih(dasar, xA, yA, epoch=300, lr=1e-3)
acc_A_awal = akurasi(dasar, xA_uji, yA_uji)
print("== 1. pretrain Tugas A (sinyal di dimensi 0) ==")
print(f"parameter model dasar : {total_param} "
f"({D_IN} -> {D_H} -> {D_H} -> 2)")
print(f"loss akhir : {loss_A:.4f} | akurasi uji A: {acc_A_awal:.3f}")
print()
# ---------- 2. Model beku menghadapi Tugas B ----------
acc_beku = akurasi(dasar, xB_uji, yB_uji)
print("== 2. model beku menghadapi Tugas B ==")
print(f"sinyal pindah ke dimensi 1, dimensi 0 jadi penanda lawan")
print(f"akurasi B tanpa adaptasi: {acc_beku:.3f} (tertipu penanda lama)")
print()
# ---------- 3. Pasang adapter, bukti inisialisasi identitas ----------
m_ad = ModelBeradapter(copy.deepcopy(dasar), R)
with torch.no_grad():
selisih = (m_ad(xA_uji) - dasar(xA_uji)).abs().max().item()
print("== 3. pasang adapter (2 sisipan, rank r=4), cek inisialisasi ==")
print(f"selisih logits maksimum model dasar vs beradapter: {selisih:.6f}")
print("(proyeksi naik di-nol-kan -> koreksi awal nol persis)")
print()
# ---------- 4. Jalur adaptasi 1: full fine-tuning ----------
m_full = copy.deepcopy(dasar)
loss_full = latih(m_full, xB, yB, epoch=300, lr=1e-3)
acc_full_B = akurasi(m_full, xB_uji, yB_uji)
acc_full_A = akurasi(m_full, xA_uji, yA_uji)
print("== 4. jalur 1: full fine-tuning (seluruh bobot dilatih) ==")
print(f"loss akhir B : {loss_full:.4f} | akurasi B: {acc_full_B:.3f}")
print(f"akurasi A sesudah: {acc_full_A:.3f} "
f"(turun dari {acc_A_awal:.3f} -> catastrophic forgetting)")
print()
# ---------- 5. Jalur adaptasi 2: adapter, bobot dasar beku ----------
for p in m_ad.dasar.parameters():
p.requires_grad_(False) # seluruh bobot dasar dibekukan
latih_ad = sum(p.numel() for p in m_ad.parameters() if p.requires_grad)
loss_ad = latih(m_ad, xB, yB, epoch=600, lr=3e-3)
acc_ad_B = akurasi(m_ad, xB_uji, yB_uji)
acc_ad_A_lepas = akurasi(m_ad.dasar, xA_uji, yA_uji)
acc_ad_A_aktif = akurasi(m_ad, xA_uji, yA_uji)
print("== 5. jalur 2: adapter (hanya sisipan kecil yang dilatih) ==")
print(f"loss akhir B : {loss_ad:.4f} | akurasi B: {acc_ad_B:.3f}")
print(f"akurasi A, adapter dilepas (dasar beku) : {acc_ad_A_lepas:.3f}")
print(f"akurasi A, adapter aktif : {acc_ad_A_aktif:.3f}")
print("(adapter spesialis Tugas B; kemampuan lama dipakai dengan melepasnya)")
print()
# ---------- 6. Anggaran parameter dan penyimpanan ----------
uk_full = total_param * 4 / 1024
uk_ad = latih_ad * 4 / 1024
print("== 6. anggaran parameter dan penyimpanan per tugas (fp32) ==")
print(f"model dasar total : {total_param} parameter")
print(f"full fine-tuning : {total_param} dilatih (100,0%)"
f" | salinan model {uk_full:.0f} KB")
print(f"adapter r={R} (2 sisipan) : {latih_ad} dilatih "
f"({100 * latih_ad / total_param:.1f}%) | berkas adapter {uk_ad:.0f} KB")
print()
# ---------- 7. Ringkasan ----------
print("== 7. ringkasan (Tugas A -> Tugas B) ==")
print(f"model beku : A {acc_A_awal:.3f} | B {acc_beku:.3f}")
print(f"full FT : A {acc_full_A:.3f} | B {acc_full_B:.3f} "
f"| {total_param} parameter dilatih")
print(f"adapter {100 * latih_ad / total_param:.1f}% : A {acc_ad_A_lepas:.3f} | "
f"B {acc_ad_B:.3f} | {latih_ad} parameter dilatih")Output yang Dihasilkan:
== 1. pretrain Tugas A (sinyal di dimensi 0) ==
parameter model dasar : 99330 (128 -> 256 -> 256 -> 2)
loss akhir : 0.0000 | akurasi uji A: 0.977
== 2. model beku menghadapi Tugas B ==
sinyal pindah ke dimensi 1, dimensi 0 jadi penanda lawan
akurasi B tanpa adaptasi: 0.020 (tertipu penanda lama)
== 3. pasang adapter (2 sisipan, rank r=4), cek inisialisasi ==
selisih logits maksimum model dasar vs beradapter: 0.000000
(proyeksi naik di-nol-kan -> koreksi awal nol persis)
== 4. jalur 1: full fine-tuning (seluruh bobot dilatih) ==
loss akhir B : 0.0005 | akurasi B: 0.969
akurasi A sesudah: 0.379 (turun dari 0.977 -> catastrophic forgetting)
== 5. jalur 2: adapter (hanya sisipan kecil yang dilatih) ==
loss akhir B : 0.0001 | akurasi B: 0.996
akurasi A, adapter dilepas (dasar beku) : 0.977
akurasi A, adapter aktif : 0.074
(adapter spesialis Tugas B; kemampuan lama dipakai dengan melepasnya)
== 6. anggaran parameter dan penyimpanan per tugas (fp32) ==
model dasar total : 99330 parameter
full fine-tuning : 99330 dilatih (100,0%) | salinan model 388 KB
adapter r=4 (2 sisipan) : 4616 dilatih (4.6%) | berkas adapter 18 KB
== 7. ringkasan (Tugas A -> Tugas B) ==
model beku : A 0.977 | B 0.020
full FT : A 0.379 | B 0.969 | 99330 parameter dilatih
adapter 4.6% : A 0.977 | B 0.996 | 4616 parameter dilatihMari kita telaah kode di atas baris per baris, dengan fokus pada mekanisme membekukan dan menyisipkan:
- Baris 27-38: fungsi
buat_datamenanam sinyal kelas pada satu dimensi fitur;dim_lawanmenanam penanda berlawanan. Karena penanda itu berlawanan, model lama tidak sekadar gagal — model lama aktif memilih kelas yang salah, terlihat dari akurasi 0,020 (jauh di bawah tebakan acak 0,500). - Baris 46-57:
ModelDasar— MLP tiga lapis dengan 99.330 parameter yang berperan sebagai model pra-latih. Seluruh angka efisiensi berikutnya dibandingkan terhadap jumlah ini. - Baris 61-72: class
Adapter— duaLinearkecil (turun 256→4, naik 4→256) plus jalur residual. Baris 67-68 meng-nol-kan bobot dan bias proyeksi naik sehingga koreksi awal nol persis; keluaran bagian 3 membuktikannya dengan selisih logits 0,000000. - Baris 75-88:
ModelBeradaptermenempatkan sisipan setelah tiap lapis tersembunyi.self.dasaradalah referensi langsung ke model yang sama — bukan salinan konseptual — sehingga membekukannya membekukan model utuh. - Baris 97-107: fungsi
latihmenyaring[p for p in model.parameters() if p.requires_grad]untuk optimizer. Baris inilah seluruh mekanisme PEFT pada PyTorch: parameter yang tidak masuk daftar tidak menerima gradien dan tidak menempati memori optimizer. - Baris 149-151: pembekuan dilakukan eksplisit —
requires_grad_(False)untuk seluruh parameter dasar — lalu dihitung ulang jumlah parameter yang masih dilatih: 4.616 dari 99.330 (4,6%). - Baris 163-172: anggaran penyimpanan dihitung dari jumlah parameter × 4 byte: salinan model utuh 388 KB versus berkas adapter 18 KB per tugas — rasio 1 banding 21.
Tiga angka pada ringkasan menutup argumen. Adapter mencapai akurasi Tugas B 0,996 — di atas full fine-tuning 0,969 — karena kapasitas yang kecil bekerja sebagai regularisasi pada data 512 sampel. Akurasi Tugas A tetap 0,977 karena bobot dasar tidak pernah tersentuh gradien; bandingkan dengan full fine-tuning yang jatuh ke 0,379. Dan ketika adapter aktif pada data Tugas A (0,074), perilakunya menegaskan sifat PEFT: adapter adalah spesialis per tugas yang dipasang-lepas, bukan model serbaguna baru.
Dari Demo ke Pustaka #
Pada praktik nyata kita tidak menulis class adapter sendiri: pustaka PEFT dari Hugging Face
menyatukan LoRA, prefix tuning, prompt tuning, adapter, BitFit, dan varian lain di balik satu antarmuka — misalnya get_peft_model(model, config) yang otomatis membekukan bobot dasar dan melaporkan persentase parameter yang dilatih. Hasilnya berformat berkas kecil yang bisa dibagikan dan dipasang ke model dasar yang sama, persis pola 18 KB pada demo kita.
Di antara seluruh keluarga PEFT, LoRA naik menjadi pilihan paling umum di industri: tanpa latensi tambahan saat deployment dan mudah dikombinasikan dengan bobot terkuantisasi 4-bit. Dua sifat itulah yang dibedah tuntas pada chapter berikutnya — dari derivasi ΔW = B·A sampai QLoRA yang membawa fine-tuning model besar ke GPU kecil.