Pada chapter sequence-to-sequence, decoder selama pelatihan menerima bantuan yang besar tapi senyap: token target yang asli. Setiap kali decoder harus menghasilkan token ke-k, masukan pada langkah tersebut adalah token ke-(k-1) yang benar — bukan hasil tebakannya sendiri. Teknik ini bernama teacher forcing, dan tanpa teknik ini pelatihan decoder rekuren nyaris tidak praktis: kesalahan kecil di awal akan digulirkan ke seluruh langkah berikutnya sehingga gradien penuh derau. Namun bantuan itu juga menabung utang: saat inferensi berjalan, guru tidak ada lagi dan decoder harus memakan prediksinya sendiri — kondisi yang tidak pernah dialaminya selama pelatihan. Chapter penutup section ini mengukur dua sisi tersebut dengan angka pada satu decoder mini.
Dua Jalur Masukan Decoder #
Mekanisme teacher forcing paling mudah dipahami sebagai dua jalur masukan pada posisi yang sama. Jalur pelatihan mengalirkan token target; jalur inferensi mengalirkan token prediksi:
Pada jalur pelatihan, decoder mempelajari pemetaan lokal “dari token benar ke token berikutnya” — setiap keputusan diambil dari masukan yang selalu benar. Pada jalur inferensi, satu prediksi yang keliru menjadi masukan yang keliru pula di langkah berikutnya, dan kesalahannya menumpuk. Ketidakcocokan distribusi masukan antara pelatihan dan inferensi inilah yang disebut exposure bias.
Mengukur Selisihnya dengan Angka #
Decoder mini kita mempelajari deret berpola t_{k+1} = (2 t_k + 1) mod 10 dari token awal s — aturan lokal yang mudah, tetapi satu kesalahan prediksi membuat seluruh deret sesudahnya melenceng karena aturan diterapkan pada token yang salah. Program berikut menghitung loss yang sama pada dua mode — masukan token asli versus masukan prediksi sendiri — di tiga titik waktu pelatihan:
"""Teacher forcing vs bangkitan mandiri pada decoder mini.
Tugas: diberikan token awal s, deret target mengikuti aturan lokal
t_{k+1} = (2*t_k + 1) mod 10. Saat training, teacher forcing memberi
token ASLI sebagai masukan langkah berikutnya; saat inferensi, decoder
hanya boleh memakai prediksinya sendiri. Selisih loss kedua mode =
exposure bias yang dibayar saat inferensi.
"""
import torch
import torch.nn as nn
torch.manual_seed(7)
V, E, H, K = 11, 12, 32, 10 # vocab: digit 0-9 + BOS(10)
BOS = 10
SEMUA_S = torch.arange(10) # 10 sekuens latih (s = 0..9)
def deret(s):
"""Deret target: t1 = s, t_{k+1} = (2 t_k + 1) mod 10."""
t = [s]
for _ in range(K - 1):
t.append((2 * t[-1] + 1) % 10)
return torch.tensor(t)
target = torch.stack([deret(int(s)) for s in SEMUA_S]) # (10, K)
masukan_tf = torch.cat(
[torch.full((10, 1), BOS), target[:, :-1]], dim=1
) # [BOS, t1..t9]
class Decoder(nn.Module):
def __init__(self):
super().__init__()
self.emb = nn.Embedding(V, E)
self.lstm = nn.LSTM(E, H, batch_first=True)
self.konteks = nn.Linear(E, H) # token awal -> hidden awal
self.out = nn.Linear(H, V)
def forward(self, awal, masukan):
"""awal: (N,) token start; masukan: (N, K) token per langkah."""
h0 = self.konteks(self.emb(awal)).unsqueeze(0)
c0 = torch.zeros_like(h0)
hh, _ = self.lstm(self.emb(masukan), (h0, c0))
return self.out(hh) # logits (N, K, V)
model = Decoder()
optim = torch.optim.Adam(model.parameters(), lr=0.02)
loss_fn = nn.CrossEntropyLoss()
def loss_tf():
"""Loss teacher forcing: masukan langkah berikut = token ASLI."""
logits = model(SEMUA_S, masukan_tf)
return loss_fn(logits.reshape(-1, V), target.reshape(-1)).item()
def loss_mandiri():
"""Loss bangkitan mandiri: masukan langkah berikut = prediksi sendiri."""
h = model.konteks(model.emb(SEMUA_S)).unsqueeze(0)
c = torch.zeros_like(h)
token = torch.full((10, 1), BOS)
total = 0.0
for k in range(K):
hh, (h, c) = model.lstm(model.emb(token), (h, c))
total += loss_fn(model.out(hh[:, -1]), target[:, k]).item()
token = model.out(hh[:, -1]).argmax(dim=1, keepdim=True)
return total / K
def bangkit_mandiri():
"""Prediksi sekuens penuh tanpa melihat target sama sekali."""
h = model.konteks(model.emb(SEMUA_S)).unsqueeze(0)
c = torch.zeros_like(h)
token = torch.full((10, 1), BOS)
hasil = []
for _ in range(K):
hh, (h, c) = model.lstm(model.emb(token), (h, c))
token = model.out(hh[:, -1]).argmax(dim=1, keepdim=True)
hasil.append(token.squeeze(1))
return torch.stack(hasil, dim=1)
print("Loss pada 10 sekuens latih, dua mode evaluasi:")
print(f"{'epoch':>6} {'teacher forcing':>16} {'mandiri':>10}")
print(f"{0:>6} {loss_tf():>16.4f} {loss_mandiri():>10.4f}")
EPOCH = 15
for epoch in range(1, EPOCH + 1):
logits = model(SEMUA_S, masukan_tf)
loss = loss_fn(logits.reshape(-1, V), target.reshape(-1))
optim.zero_grad()
loss.backward()
optim.step()
if epoch in (5, 15):
print(f"{epoch:>6} {loss_tf():>16.4f} {loss_mandiri():>10.4f}")
print()
# ---------- Akurasi per token dan per sekuens penuh ----------
with torch.no_grad():
logits = model(SEMUA_S, masukan_tf)
benar_tf = logits.argmax(dim=2) == target
prediksi = bangkit_mandiri()
benar_fr = prediksi == target
print(f"Token benar, teacher forcing : {benar_tf.float().mean():.3f}")
print(f"Token benar, mandiri : {benar_fr.float().mean():.3f}")
print(f"Sekuens cocok, teacher forcing: {int(benar_tf.all(dim=1).sum())}/10")
print(f"Sekuens cocok, mandiri : {int(benar_fr.all(dim=1).sum())}/10")
# ---------- Contoh divergensi ----------
gagal = (~benar_fr.all(dim=1)).nonzero().flatten()
print()
if gagal.numel() > 0:
print(f"Contoh divergensi ({int(gagal.numel())} sekuens gagal dari 10):")
for idx in gagal[:3].tolist():
print(f" s={idx} target {target[idx].tolist()}")
print(f" predik {prediksi[idx].tolist()}")
else:
print("Tidak ada sekuens yang menyimpang pada seed ini.")Output yang Dihasilkan:
Loss pada 10 sekuens latih, dua mode evaluasi:
epoch teacher forcing mandiri
0 2.4293 2.4377
5 1.2536 1.6484
15 0.1596 1.3875
Token benar, teacher forcing : 0.960
Token benar, mandiri : 0.710
Sekuens cocok, teacher forcing: 6/10
Sekuens cocok, mandiri : 6/10
Contoh divergensi (4 sekuens gagal dari 10):
s=0 target [0, 1, 3, 7, 5, 1, 3, 7, 5, 1]
predik [0, 9, 9, 9, 9, 9, 9, 9, 9, 9]
s=4 target [4, 9, 9, 9, 9, 9, 9, 9, 9, 9]
predik [9, 9, 9, 9, 9, 9, 9, 9, 9, 9]
s=6 target [6, 3, 7, 5, 1, 3, 7, 5, 1, 3]
predik [5, 1, 3, 7, 5, 1, 3, 7, 5, 1]Mari kita telaah kode di atas baris per baris:
- Baris 15-17: vocabulary berisi 11 token (digit 0-9 plus BOS); seluruh 10 sekuens latih dibangkitkan dari nilai awal s = 0 sampai 9.
- Baris 20-31: fungsi
deretmembangkitkan target dengan aturan penggandaan;masukan_tfmenyusun jalur teacher forcing[BOS, t_1, ..., t_9]yang berjalan seiring target[t_1, ..., t_10]. - Baris 34-47: kelas
Decoder. Lapisankonteksmengubah token awal menjadi hidden state awal — memainkan peran context vector dari chapter seq2seq — lalu LSTM membaca token demi token danLinearmemproyeksikan ke vocabulary. - Baris 55-58:
loss_tfmenghitung loss mode teacher forcing: seluruh masukan adalah token asli. - Baris 61-71:
loss_mandirimensimulasikan inferensi: setiap langkah mengambil argmax keluaran, lalu token hasil itulah — bukan token asli — yang dijadikan masukan langkah berikutnya. Loss tetap dihitung terhadap target yang benar. - Baris 87-99: kedua loss dievaluasi pada epoch 0, 5, dan 15. Di epoch 0 keduanya hampir sama (2,43 vs 2,44) karena model masih acak; di epoch 5 mulai terpisah (1,25 vs 1,65); di epoch 15 loss teacher forcing 0,16 sementara loss mandiri 1,39 — sembilan kali lebih besar.
- Baris 101-110: akurasi per token 96 persen pada mode teacher forcing, tetapi hanya 71 persen pada mode mandiri — kesalahan kecil menumpuk saat prediksi sendiri diumpankan balik.
- Baris 112-121: contoh divergensi tercetak. Kasus s = 6 paling jelas: prediksi langkah pertama salah memilih 5, lalu model menerapkan aturannya dengan benar pada token yang salah — menghasilkan deret milik s = 5 secara konsisten. Aturan dikuasai, masukannya yang bergeser.
Pro, Kontra, dan Jalan Tengah #
| Aspek | Teacher forcing | Tanpa teacher forcing |
|---|---|---|
| Kecepatan konvergensi | cepat, gradien bersih | sangat lambat, derau menumpuk |
| Kestabilan pelatihan | stabil sejak epoch awal | mudah melenceng di epoch awal |
| Kecocokan dengan inferensi | tidak cocok (exposure bias) | cocok secara prinsip |
| Biaya implementasi | satu baris (geser target) | bangkitan bertahap per langkah |
Jalan tengah yang populer adalah scheduled sampling: pada awal pelatihan semua masukan adalah token asli, lalu secara bertahap digantikan prediksi model dengan peluang yang naik seiring epoch, sehingga decoder berlatih pada kondisi inferensi tanpa kehilangan stabilitas awal. Teknik lain yang lebih modern — professor forcing — memakai adversarial loss agar distribusi state pelatihan menyerupai inferensi.
Dengan itu, perlengkapan sequence modeling kita lengkap: data sekuens, RNN, BPTT, diagnosa vanishing gradient, LSTM, GRU, dua arah, encoder-decoder, dan strategi pelatihan decodernya. Namun konteks panjang, bottleneck context vector, dan biaya komputasi berurutan tetap menjadi batas keluarga rekuren. Section berikutnya, Attention dan Transformer , memulai dari kritik atas keterbatasan tersebut lalu membangun arsitektur yang kini mendominasi — dimulai dari chapter Keterbatasan RNN .