Kontras menarik dua tampilan menjauh; masked language modeling (MLM) bekerja dengan cara yang lebih langsung: sebagian token disembunyikan, model menebak token aslinya dari sisa konteks. Dari tugas sesederhana itu, BERT mempelajari representation bahasa yang dua arah — setiap token dipahami berdasarkan kata di kiri dan kanannya sekaligus — dan melakukannya pada miliaran token tanpa satu anotasi. Chapter ini membangun MLM mini pada korpus karakter kecil, membandingkannya dengan varian satu arah ala model autoregressive, lalu melihat angka mana yang membuktikan manfaat konteks dua arah.
Topeng sebagai Sinyal Supervisi #
Mekanismenya satu kalimat: pilih posisi acak, ganti token dengan token topeng [M], minta model memprediksi token asli. BERT asli menyembunyikan 15% token tiap kalimat; pada 15% itu sebagian diganti [M], sebagian kecil diganti token acak, dan sebagian dibiarkan — skema campuran yang membantu model tetap waspada terhadap token yang tampak normal. Target prediksi diambil dari kalimat asli, jadi seluruh supervisi lahir dari data mentah itu sendiri, persis pola pretext task pada chapter pertama section ini.
Ciri pembeda MLM adalah arah konteksnya. Karena token topeng ditebak dari kedua sisi, representation tiap posisi menyedot informasi dari seluruh kalimat. Sebaliknya, model autoregressive seperti GPT memprediksi token berikutnya hanya dari konteks kiri — pilihan yang cocok untuk menghasilkan teks, tetapi representation token awal kalimat tidak pernah melihat kata-kata setelahnya. Kedua paradigma dibandingkan pada tabel berikut:
| Aspek | Masked (BERT-style) | Autoregressive (GPT-style) |
|---|---|---|
| Target prediksi | token yang disembunyikan | token berikutnya |
| Konteks yang boleh dibaca | kiri dan kanan posisi topeng | kiri saja |
| Arah representation | dua arah | satu arah |
| Kecocokan tugas | pemahaman (klasifikasi, QA) | pembangkitan teks |
| Bab terkait | chapter ini | Next-Token Prediction |
Demo: MLM Mini pada Korpus Karakter #
Korpus kita berupa pola kalimat tentang tiga hewan yang diulang, ditokenisasi per karakter. Setiap jendela 8 karakter menyembunyikan satu posisi acak; encoder mini berbasis MLP harus mengisi kekosongan itu dari konteks kiri dan kanan. Pembandingnya jaringan yang sama tetapi slot di kanan posisi topeng dinolkan — meniru pembatasan arah autoregressive:
"""Masked language modeling (MLM) mini ala BERT pada korpus karakter.
Korpus: kalimat berpola tentang tiga hewan (sama dengan chapter
Pengenalan LLM, ditambah pola "tidur"). Setiap jendela 8 karakter
menyembunyikan SATU karakter acak dengan token topeng [M]; model
mini -- encoder dua arah berbasis MLP per-posisi -- harus menebak
karakter aslinya dari konteks KIRI dan KANAN sekaligus.
Pembanding: model yang sama tetapi slot setelah posisi topeng
dinolkan (hanya konteks kiri, seperti arah decoder autoregressive).
Evaluasi: akurasi karakter topeng pada blok korpus yang tidak
pernah dilatih. Diuji di CPU dengan PyTorch 2.14, seed tetap.
"""
import torch
import torch.nn as nn
torch.manual_seed(0)
# ---------- 1. Korpus dan vocabulary ----------
blok = ("kucing makan ikan. anjing makan daging. burung makan biji. "
"kucing tidur sofa. anjing tidur kasur. burung tidur ranting. ")
korpus = blok * 12
LATIH, UJI = korpus[: -len(blok)], korpus[-len(blok):]
karakter = sorted(set(korpus))
id_kar = {c: i for i, c in enumerate(karakter)}
V = len(karakter)
MASK = V # token topeng sebagai indeks tambahan
JENDELA = 8
print(f"korpus latih : {len(LATIH)} karakter uji: {len(UJI)} karakter")
print(f"vocabulary : {V} karakter + 1 token topeng [M]")
print()
# ---------- 2. Jendela ber-topeng ----------
def jendela_topeng(teks, seed):
"""Tiap jendela 8 karakter menyembunyikan 1 posisi acak."""
rng = torch.Generator().manual_seed(seed)
X, y, pos = [], [], []
for i in range(len(teks) - JENDELA):
j = int(torch.randint(0, JENDELA, (1,), generator=rng).item())
potongan = [id_kar[c] for c in teks[i : i + JENDELA]]
y.append(potongan[j])
potongan[j] = MASK
X.append(potongan)
pos.append(j)
return torch.tensor(X), torch.tensor(y), torch.tensor(pos)
X_latih, y_latih, p_latih = jendela_topeng(LATIH, 123)
X_uji, y_uji, p_uji = jendela_topeng(UJI, 456)
print(f"contoh latih : {X_latih.shape[0]} jendela, uji: {X_uji.shape[0]} jendela")
j = p_latih[3].item()
tampil = list(LATIH[3 : 3 + JENDELA])
tampil[j] = "[M]"
print(f" jendela-3 : {''.join(tampil)!r} target: {karakter[y_latih[3]]!r}")
print()
# ---------- 3. Encoder mini dua arah ----------
class MLMMini(nn.Module):
"""Slot = [embedding(token), one-hot posisi] -> MLP -> digabung -> head.
dua_arah=False menolkan slot di kanan posisi topeng sehingga model
hanya boleh memakai konteks kiri (meniru arah autoregressive).
"""
def __init__(self, dua_arah=True):
super().__init__()
self.emb = nn.Embedding(V + 1, 8)
self.slot = nn.Sequential(nn.Linear(8 + JENDELA, 16), nn.ReLU())
self.head = nn.Sequential(
nn.Linear(16 * JENDELA, 96), nn.ReLU(), nn.Linear(96, V)
)
self.dua_arah = dua_arah
def forward(self, x, pos_topeng):
e = self.emb(x) # (B, 8, 8)
p = torch.eye(JENDELA).expand(x.shape[0], -1, -1) # (B, 8, 8)
fitur = self.slot(torch.cat([e, p], dim=2)) # (B, 8, 16)
if not self.dua_arah:
batas = pos_topeng[:, None, None]
idx = torch.arange(JENDELA)[None, :, None]
fitur = fitur * (idx <= batas).float() # nol-kan kanan topeng
return self.head(fitur.flatten(1)) # logits (B, V)
def latih(dua_arah, epoch=300, lr=3e-3):
torch.manual_seed(7)
model = MLMMini(dua_arah)
opt = torch.optim.Adam(model.parameters(), lr=lr, weight_decay=1e-4)
for ep in range(epoch):
loss = nn.CrossEntropyLoss()(model(X_latih, p_latih), y_latih)
opt.zero_grad(); loss.backward(); opt.step()
return model, loss.item()
print("=== Pelatihan (loss akhir) ===")
model_2arah, loss_2 = latih(True)
model_kiri, loss_1 = latih(False)
print(f" encoder dua arah (MLM) : {loss_2:.4f}")
print(f" encoder konteks kiri saja : {loss_1:.4f}")
print()
# ---------- 4. Evaluasi akurasi karakter topeng ----------
with torch.no_grad():
acc_2 = (model_2arah(X_uji, p_uji).argmax(1) == y_uji).float().mean().item()
acc_1 = (model_kiri(X_uji, p_uji).argmax(1) == y_uji).float().mean().item()
print("=== Akurasi karakter topeng pada blok uji ===")
print(f" dua arah (konteks kiri+kanan): {acc_2:.3f}")
print(f" satu arah (konteks kiri saja): {acc_1:.3f}")
print()
# ---------- 5. Contoh pengisian topeng ----------
print("=== Contoh pengisian topeng (model dua arah) ===")
with torch.no_grad():
benar = (model_2arah(X_uji, p_uji).argmax(1) == y_uji).nonzero().flatten()
dipilih = [i for i in benar.tolist()
if p_uji[i].item() not in (0, 7)
and karakter[y_uji[i]] not in (" ", ".")][:3]
for i in dipilih:
j = p_uji[i].item()
teks = list(UJI[i : i + JENDELA])
teks[j] = "[?]"
pred = karakter[model_2arah(X_uji[i : i + 1], p_uji[i : i + 1])
.argmax(1).item()]
print(f" {''.join(teks)!r} -> tebakan {pred!r} (benar: "
f"{karakter[y_uji[i]]!r})")
# satu kasus topeng di posisi 0: konteks kiri kosong
pos0 = (p_uji == 0).nonzero().flatten()
if len(pos0):
i = pos0[0].item()
teks = list(UJI[i : i + JENDELA]); teks[0] = "[?]"
with torch.no_grad():
tebak2 = karakter[model_2arah(X_uji[i : i + 1], p_uji[i : i + 1]).argmax(1).item()]
tebak1 = karakter[model_kiri(X_uji[i : i + 1], p_uji[i : i + 1]).argmax(1).item()]
print(f" {''.join(teks)!r} -> dua arah: {tebak2!r}, kiri saja: {tebak1!r} "
f"(benar: {karakter[y_uji[i]]!r})")Output yang Dihasilkan:
korpus latih : 1320 karakter uji: 120 karakter
vocabulary : 18 karakter + 1 token topeng [M]
contoh latih : 1312 jendela, uji: 112 jendela
jendela-3 : 'in[M] maka' target: 'g'
=== Pelatihan (loss akhir) ===
encoder dua arah (MLM) : 0.0176
encoder konteks kiri saja : 0.5204
=== Akurasi karakter topeng pada blok uji ===
dua arah (konteks kiri+kanan): 0.911
satu arah (konteks kiri saja): 0.759
=== Contoh pengisian topeng (model dua arah) ===
'cin[?] mak' -> tebakan 'g' (benar: 'g')
'i[?]g maka' -> tebakan 'n' (benar: 'n')
' mak[?]n i' -> tebakan 'a' (benar: 'a')
'[?]njing m' -> dua arah: 'a', kiri saja: 'n' (benar: 'a')Mari kita telaah kode di atas baris per baris:
- Baris 20-30 (korpus): blok kalimat berpola diulang 12 kali; blok terakhir disisihkan sebagai uji sehingga model dievaluasi pada pola yang sama tetapi tidak pernah dilatih. Vocabulary dibangun dari karakter unik plus satu indeks tambahan untuk
[M]. - Baris 36-47 (
jendela_topeng): setiap posisi awal jendela menghasilkan satu contoh latih — satu posisi acak dalam jendela digantiMASK, karakter aslinya menjadi targety, posisinya disimpan diposagar model tahu letak topeng. - Baris 61-85 (
MLMMini): tiap slot jendela diwakili gabungan embedding token dan one-hot posisi, diolahslotMLP, lalu seluruh slot digabung menjadi satu vektor masukan bagihead. Dua baris terakhirforwardadalah kunci pembanding:dua_arah=Falsemengalikan fitur dengan mask(idx <= batas)sehingga semua slot di kanan topeng bernilai nol. - Baris 88-95 (
latih): kedua varian dilatih dengan seed sama danweight_decaykecil; hanya arah konteks yang berbeda. - Baris 107-113 (evaluasi): akurasi dihitung pada blok uji — angka 0.911 versus 0.759 adalah bukti langsung nilai tambah konteks kanan.
- Baris 117-141 (contoh): tiga tebakan sukses ditampilkan, ditutup kasus topeng di posisi 0 — konteks kiri kosong, model satu arah menebak ’n’ (meneruskan ’n’ sebelumnya menjadi ’nn’), sedangkan model dua arah melihat ’njing’ di kanan dan menjawab ‘a’.
Dua Arah Membayar Diri Sendiri #
Selisih akurasi 0.911 lawan 0.759 lahir dari satu-satunya perbedaan: akses konteks kanan. Kasus terakhir keluaran memperlihatkan mekanismenya secara gamblang — tebakan ’n’ versus ‘a’ pada kata yang sama, hanya karena satu model boleh membaca huruf setelah topeng. Inilah alasan model pemahaman bahasa modern memilih arsitektur encoder dua arah ala BERT; sementara untuk pembangkitan teks, urutan kiri-ke-kanan tetap keharusan, seperti dibahas tuntas di chapter Next-Token Prediction . Detail arsitektur transformer yang menjalankan keduanya bisa Anda simak di chapter Arsitektur Transformer Modern .
Ide “menyembunyikan lalu menebak” tidak berhenti di teks. Gambar juga bisa dibelah menjadi patch, disembunyikan sebagian besar, lalu direkonstruksi — kita bahas di chapter berikutnya: Masked Image Modeling.