Di akhir chapter Next-Token Prediction , decoder mini kita mampu melanjutkan korpusnya tanpa satu pun kesalahan — tetapi ketika diminta “ulangi huruf b empat kali”, model hanya melanjutkan pola kalimat yang dikenalnya, bukan mengerjakan perintah itu. Perilaku yang sama terjadi pada LLM yang hanya menjalani pretraining: instruksi pengguna diperlakukan sebagai teks biasa yang harus diteruskan, bukan sebagai perintah. Instruction tuning menutup jarak ini dengan melanjutkan pelatihan model pra-latih pada data berformat instruksi-respons, sehingga perilaku model bergeser dari “melanjutkan” menjadi “mengikuti”. Chapter ini membuktikan pergeseran itu pada model mini dengan angka nyata: sebelum tuning 0/5 instruksi dipatuhi, sesudah tuning 4/5.
Masalah Model Pretrain-Only #
Pretraining hanya memberi satu sinyal: prediksi token berikutnya. Ketika pengguna menulis prompt “Ringkas paragraf berikut dalam tiga poin”, model pretrain-only menghitung kelanjutan yang paling mungkin statistiknya — misalnya paragraf lain yang mirip, atau kalimat lanjutan seperti “poin-poin tersebut akan dibahas di bagian selanjutnya” — karena pola seperti itu paling sering muncul di korpus web. Tidak ada satu pun contoh pelatihan yang menyatakan “teks ini adalah perintah, kerjakan isinya”. Hasilnya, kemampuan bahasa model dan kemampuan mengikuti perintah adalah dua hal yang berbeda, dan hanya yang pertama yang dimiliki model pra-latih.
Masalah yang sama muncul pada demo kita. Decoder mini yang dilatih pada 40 sekuens huruf acak, ketika diberi prompt ulangi b #, hanya melanjutkan dengan dua huruf sembarang lalu berhenti — respons b a pada keluaran uji di bawah. Model tidak memiliki asosiasi apa pun antara token ulangi dan tindakan menyalin huruf.
Format Instruksi dan Percakapan #
Instruction tuning membutuhkan data berformat baru: pasangan (instruksi, respons), kadang dengan input tambahan. Dua dataset klasik yang memopulerkan pendekatan ini adalah FLAN — koleksi lebih dari 1.800 tugas NLP yang dibungkus ulang menjadi instruksi — dan Alpaca dari Stanford dengan 52 ribu contoh instruksi-respons yang dibangkitkan lewat resep self-instruct. Untuk asisten percakapan, formatnya diperluas menjadi multi-turn: seluruh percakapan dijadikan satu token stream dengan penanda peran.
Diagram di atas memperlihatkan struktur yang perlu kita pahami sekarang: giliran user dan respons model bergantian di dalam satu sekuens, dan hanya bagian respons yang menjadi target pelatihan. Detail pembungkusan token peran nyata (penanda khusus ChatML-style, [INST], sampai kebocoran system prompt) dibedah pada chapter Prompt Format dan Chat Template
.
Temuan penting dari makalah FLAN : instruction tuning pada banyak tugas menjadikan model mengerjakan tugas yang belum pernah dilihat secara zero-shot — tanpa contoh di prompt. Ini perkembangan langsung dari in-context learning yang kita kenal di chapter Pengenalan LLM : format instruksi yang konsisten mengubah kemampuan laten menjadi perilaku yang bisa dipanggil lewat perintah.
Demo: Dua Belas Ribu Parameter Belajar Patuh #
Kita pakai arsitektur yang sama dengan chapter sebelumnya — decoder dua blok, dimensi 32, vocabulary 10 token — dengan satu perbedaan penting: token kini simbol utuh (word-level mini), bukan karakter, sehingga instruksi ulangi, penanda #, dan tiap huruf adalah token tersendiri. Skenarionya meniru siklus LLM nyata dalam skala mini: pretraining pada teks acak, uji instruksi, instruction tuning pada empat contoh (huruf a-d), lalu uji ulang termasuk huruf e yang sengaja tidak pernah muncul di data tuning. Contoh diuji nyata pada CPU dengan PyTorch 2.14; seluruh proses selesai dalam sekitar 15 detik.
"""Instruction tuning mini: perilaku sebelum vs sesudah tuning instruksi.
Tahap 1 (pretrain umum): decoder mini 2 blok dilatih pada 40 sekuens huruf acak
tanpa instruksi apa pun. Tahap 2 (uji sebelum): diberi prompt instruksi
"ulangi <huruf> #", model diminta melanjutkan secara greedy. Tahap 3
(instruction tuning): model yang sama dilanjutkan pelatihannya pada 4
contoh instruksi-respons (huruf a-d saja). Tahap 4 (uji sesudah):
prompt yang sama diuji ulang, termasuk huruf e yang tidak pernah muncul
pada data tuning sebagai uji generalisasi. Token = simbol utuh
(word-level mini). Diuji di CPU dengan PyTorch 2.14, seed tetap,
deterministik.
"""
import torch
import torch.nn as nn
torch.manual_seed(0)
# ---------- 1. Vocabulary simbol + korpus ----------
KATA = ["<bos>", "<eos>", "ulangi", "#", ".", "a", "b", "c", "d", "e"]
ID = {k: i for i, k in enumerate(KATA)}
V = len(KATA)
BOS, EOS, AKHIR = ID["<bos>"], ID["<eos>"], ID["."]
HURUF = ["a", "b", "c", "d", "e"]
g = torch.Generator().manual_seed(7)
korpus_pre = [
[BOS] + [ID[HURUF[i]] for i in torch.randint(0, 5, (5,), generator=g)] + [EOS]
for _ in range(40)
]
data_instr = [
[BOS] + [ID["ulangi"], ID[x]] + [ID["#"]] + [ID[x]] * 4 + [AKHIR, EOS]
for x in "abcd"
]
print(f"vocabulary : {V} token -> {KATA}")
print(f"contoh sekuens pretrain : {[KATA[i] for i in korpus_pre[0]]}")
print(f"contoh data instruksi : {[KATA[i] for i in data_instr[0]]}")
print()
# ---------- 2. Decoder mini (token-level, panjang variabel) ----------
class BlokDecoder(nn.Module):
"""Satu blok decoder: causal self-attention + FFN + dua LayerNorm."""
def __init__(self, d, kepala):
super().__init__()
self.h, self.d_k = kepala, d // kepala
self.wq, self.wk, self.wv, self.wo = (nn.Linear(d, d) for _ in range(4))
self.ffn = nn.Sequential(nn.Linear(d, 64), nn.ReLU(), nn.Linear(64, d))
self.norm1, self.norm2 = nn.LayerNorm(d), nn.LayerNorm(d)
def forward(self, x):
n, T, d = x.shape
def pecah(z):
return z.view(n, T, self.h, self.d_k).transpose(1, 2)
q, k, v = pecah(self.wq(x)), pecah(self.wk(x)), pecah(self.wv(x))
skor = q @ k.transpose(-2, -1) / self.d_k ** 0.5
mask = torch.triu(torch.ones(T, T, dtype=torch.bool), diagonal=1)
a = skor.masked_fill(mask, float("-inf")).softmax(-1)
z = (a @ v).transpose(1, 2).contiguous().view(n, T, d)
x = self.norm1(x + self.wo(z))
x = self.norm2(x + self.ffn(x))
return x
class DecoderMini(nn.Module):
"""Transformer decoder: embedding + beberapa blok + kepala keluaran."""
def __init__(self, vocab, d=32, kepala=2, T_maks=12, n_blok=2):
super().__init__()
self.embed = nn.Embedding(vocab, d)
self.pos = nn.Embedding(T_maks, d)
self.blok = nn.ModuleList([BlokDecoder(d, kepala) for _ in range(n_blok)])
self.keluar = nn.Linear(d, vocab)
def forward(self, idx):
n, T = idx.shape
x = self.embed(idx) + self.pos(torch.arange(T))
for blok in self.blok:
x = blok(x)
return self.keluar(x)
model = DecoderMini(V)
print(f"parameter model: {sum(p.numel() for p in model.parameters())}")
print()
loss_fn = nn.CrossEntropyLoss()
def latih(sekuens, epoch, lr, judul):
"""Latih (atau lanjutkan melatih) model; target = token berikutnya."""
x = torch.tensor([s[:-1] for s in sekuens]) # (batch, panjang-1)
y = torch.tensor([s[1:] for s in sekuens]) # target tergeser satu posisi
opt = torch.optim.Adam(model.parameters(), lr=lr)
print(f"=== {judul} ===")
for ep in range(epoch):
logits = model(x)
loss = loss_fn(logits.reshape(-1, V), y.reshape(-1))
opt.zero_grad()
loss.backward()
opt.step()
if ep == 0 or ep == epoch // 2 or ep == epoch - 1:
print(f" epoch {ep:3d} loss = {loss.item():.4f}")
print()
def jawab(prompt_token, maks=12):
"""Lanjutkan prompt secara greedy sampai <eos> atau batas token."""
seq = list(prompt_token)
with torch.no_grad():
while len(seq) < maks:
logits = model(torch.tensor([seq]))[0, -1]
berikutnya = logits.argmax().item()
if berikutnya == EOS:
break
seq.append(berikutnya)
return [KATA[i] for i in seq[len(prompt_token):]]
def uji_semua(label):
"""Uji instruksi 'ulangi <huruf> #' untuk kelima huruf."""
print(f"--- {label} ---")
benar = 0
for x in HURUF:
respons = jawab([BOS, ID["ulangi"], ID[x], ID["#"]])
harapan = [x] * 4 + ["."]
ok = respons == harapan
benar += ok
tanda = "OK " if ok else "GAGAL"
print(f" ulangi {x} -> {' '.join(respons):20s} {tanda}")
print(f" akurasi instruksi: {benar}/5")
print()
return benar
# ---------- 3. Pretrain pada teks acak, lalu uji ----------
latih(korpus_pre, epoch=300, lr=1e-2, judul="Pretraining (40 sekuens huruf acak)")
sebelum = uji_semua("SEBELUM tuning: model pretrain-only")
# ---------- 4. Instruction tuning, lalu uji ulang ----------
print(f"data tuning: {len(data_instr)} sekuens instruksi (huruf a-d, "
f"tanpa huruf e)")
latih(data_instr, epoch=2500, lr=3e-3, judul="Instruction tuning (4 contoh huruf a-d)")
sesudah = uji_semua("SESUDAH tuning: huruf e tidak pernah ada di data tuning")
# ---------- 5. Ringkasan sebelum vs sesudah ----------
print(f"ringkasan: akurasi instruksi {sebelum}/5 -> {sesudah}/5 "
f"(huruf a-d: format instruksi dipatuhi; huruf e: gagal)")Output yang Dihasilkan:
vocabulary : 10 token -> ['<bos>', '<eos>', 'ulangi', '#', '.', 'a', 'b', 'c', 'd', 'e']
contoh sekuens pretrain : ['<bos>', 'a', 'c', 'b', 'b', 'd', '<eos>']
contoh data instruksi : ['<bos>', 'ulangi', 'a', '#', 'a', 'a', 'a', 'a', '.', '<eos>']
parameter model: 18122
=== Pretraining (40 sekuens huruf acak) ===
epoch 0 loss = 2.3399
epoch 150 loss = 0.6160
epoch 299 loss = 0.6170
--- SEBELUM tuning: model pretrain-only ---
ulangi a -> e d GAGAL
ulangi b -> b a GAGAL
ulangi c -> b a GAGAL
ulangi d -> c c GAGAL
ulangi e -> e d GAGAL
akurasi instruksi: 0/5
data tuning: 4 sekuens instruksi (huruf a-d, tanpa huruf e)
=== Instruction tuning (4 contoh huruf a-d) ===
epoch 0 loss = 7.9233
epoch 1250 loss = 0.1545
epoch 2499 loss = 0.1541
--- SESUDAH tuning: huruf e tidak pernah ada di data tuning ---
ulangi a -> a a a a . OK
ulangi b -> b b b b . OK
ulangi c -> c c c c . OK
ulangi d -> d d d d . OK
ulangi e -> # c c c . GAGAL
akurasi instruksi: 4/5
ringkasan: akurasi instruksi 0/5 -> 4/5 (huruf a-d: format instruksi dipatuhi; huruf e: gagal)Mari kita telaah kode di atas baris per baris, dengan fokus pada bagian yang membedakan instruction tuning dari pretraining biasa:
- Baris 26-34: dua sumber data dibangun.
korpus_preberisi 40 sekuens huruf acak — pra-latih tanpa instruksi.data_instrberisi empat sekuens berformat<bos> ulangi <huruf> # <huruf>×4 . <eos>— data instruksi untuk huruf a sampai d; huruf e sengaja ditiadakan sebagai uji generalisasi. - Baris 42-83: arsitektur
BlokDecoderdanDecoderMinisama dengan chapter Next-Token Prediction — causal mask, dua blok, kepala keluaran ke vocabulary. Kesamaan ini penting: instruction tuning tidak mengubah arsitektur, hanya mengubah data dan melanjutkan pelatihannya. - Baris 93-107: fungsi
latihadalah training loop standar — target selalu token berikutnya (teacher forcing). Baris 146 memakai fungsi yang sama padadata_instr; satu-satunya perubahan adalah data dan jumlah epoch, persis pola SFT nyata: bobot pra-latih dijadikan titik awal, bukan diacak ulang. - Baris 110-120: fungsi
jawabmengambil alih sisi pemakaian: prompt instruksi dilanjutkan greedy sampai<eos>. Model pretrain-only menghasilkan dua huruf sembarang lalu berhenti; model hasil tuning menghasilkan empat salinan huruf lalu.. - Baris 123-136: fungsi
uji_semuamenguji kelima huruf dengan kriteria ketat — respons harus persis<huruf>×4diakhiri.— lalu menghitung akurasi instruksi. - Baris 139-151: urutan eksperimen: pretrain → uji (0/5) → instruction tuning → uji ulang (4/5). Loss awal saat tuning melonjak ke 7,92 karena format data baru belum pernah dilihat, lalu turun ke 0,15.
Perbandingan sebelum dan sesudah tuning dirangkum pada tabel berikut:
| Instruksi | Respons sebelum tuning | Respons sesudah tuning | Hasil |
|---|---|---|---|
ulangi a | e d | a a a a . | dipatuhi |
ulangi b | b a | b b b b . | dipatuhi |
ulangi c | b a | c c c c . | dipatuhi |
ulangi d | c c | d d d d . | dipatuhi |
ulangi e | e d | # c c c . | gagal |
Pelajaran dari Huruf e #
Empat huruf yang pernah muncul di data tuning dikerjakan sempurna; huruf e — yang dikenal model dari pretraining tetapi tidak pernah muncul dalam format instruksi — tetap gagal. Model 18 ribu parameter ini mempelajari pemetaan huruf tertentu menuju respons tertentu, bukan aturan umum “salin huruf yang mengikuti kata ulangi”. Inilah alasan dataset instruksi nyata menekankan dua hal sekaligus: cakupan (Alpaca mengumpulkan 52 ribu contoh) dan keragaman (FLAN membungkus 1.800+ tugas berbeda). Dengan keragaman yang cukup, menghafal menjadi lebih mahal daripada mempelajari pola umum — dan kemampuan zero-shot pada tugas baru muncul, seperti temuan makalah FLAN.
Instruction tuning pada praktiknya dijalankan sebagai supervised fine-tuning: pasangan prompt-respons disusun menjadi tensor, loss dibatasi hanya pada token respons, dan seluruh bobot model diperbarui dengan learning rate kecil. Training loop teknisnya — termasuk cara mem-masking token prompt agar tidak dihitung dalam loss — kita bedah pada chapter berikutnya.