Rantai transformasi kita kini lengkap: teks dibersihkan, dipecah menjadi token, dipetakan ke token ID, dan makna kata telah kita wakili sebagai vektor pada chapter Word Embedding
. Satu keping terakhir yang menyatukan semuanya di dalam arsitektur model adalah torch.nn.Embedding — layer PyTorch yang menyimpan seluruh vektor kata dan menyeleksi baris yang sesuai setiap kali token ID masuk. Layer ini terlihat nyaris terlalu sederhana untuk disebut layer, tetapi pilihan inisialisasinya, status trainable-nya, dan penanganan padding-nya memengaruhi kualitas model secara nyata. Chapter ini membedah setiap perilakunya dengan angka konkret.
Tabel Pencari Berbentuk (V, D) #
nn.Embedding(V, D) menyimpan satu matriks parameter weight berbentuk (V, D) — satu baris untuk setiap token ID, satu kolom untuk setiap dimensi embedding. Operasi forward tidak melakukan perkalian matriks biasa: layer ini memilih baris. Token ID bertindak sebagai indeks baris, sehingga masukan batch berbentuk (N, T) langsung menghasilkan keluaran (N, T, D) tanpa layer tambahan apa pun.
Secara matematis, pemilihan baris ini identik dengan mengalikan vektor one-hot berdimensi V dengan matriks weight — one-hot yang bernilai 1 pada posisi k akan “menyalin” baris ke-k. Kesetaraan tersebut menjelaskan posisi embedding layer pada chapter sebelumnya, sementara implementasi lookup jauh lebih hemat: tidak ada vektor one-hot sepanjang V yang perlu dibuat, dan penyalinan baris bekerja dalam waktu konstan. Detail lengkap parameter dan perilaku layer tersedia pada dokumentasi nn.Embedding .
Program berikut memeriksa seluruh klaim tersebut secara langsung:
"""nn.Embedding sebagai tabel pencari (lookup table).
Dibedah: shape weight (V, D), lookup pada batch token ID (N, T) ->
(N, T, D), identitas baris keluaran dengan baris weight, kesetaraan
matematis dengan one-hot @ weight, perilaku padding_idx pada gradien,
dan pembekuan parameter (trainable vs frozen)."""
import torch
import torch.nn as nn
import torch.nn.functional as F
torch.manual_seed(7)
# ---------- 1. Tabel pencari (V, D) ----------
V, D = 10, 8
tabel = nn.Embedding(V, D, padding_idx=0)
print("=== 1. Tabel pencari nn.Embedding(V, D) ===")
print("shape weight :", tuple(tabel.weight.shape))
print("jumlah parameter :", tabel.weight.numel())
print("baris 0 (padding) :", bool((tabel.weight[0] == 0).all()))
print()
# ---------- 2. Lookup batch token ID ----------
token_ids = torch.tensor([
[1, 4, 4, 0, 0],
[3, 2, 5, 6, 0],
]) # shape (N=2, T=5), token 0 = <pad>
out = tabel(token_ids)
print("=== 2. Lookup batch token ID ===")
print("masukan token_ids :", tuple(token_ids.shape))
print("keluaran embedding :", tuple(out.shape))
print()
# ---------- 3. Keluaran = baris tabel yang diindeks ----------
print("=== 3. Verifikasi: out[i, t] == weight[token_ids[i, t]] ===")
print("out[0,1] == weight[4] :", torch.equal(out[0, 1], tabel.weight[4]))
print("out[0,2] == weight[4] :", torch.equal(out[0, 2], tabel.weight[4]))
print("out[1,4] == weight[0] :", torch.equal(out[1, 4], tabel.weight[0]))
print("out[1,0] == weight[3] :", torch.equal(out[1, 0], tabel.weight[3]))
print()
# ---------- 4. Kesetaraan dengan one-hot kali matriks ----------
print("=== 4. Kesetaraan lookup vs one-hot @ weight ===")
one_hot = F.one_hot(torch.tensor(4), num_classes=V).to(torch.float32)
hasil_kali = one_hot @ tabel.weight
selisih = float((hasil_kali - tabel.weight[4]).abs().max().detach())
print("max |one_hot @ W - W[4]| =", selisih)
print()
# ---------- 5. padding_idx: gradien baris 0 ditolak ----------
print("=== 5. padding_idx pada gradien ===")
loss = out.sum()
loss.backward()
grad = tabel.weight.grad
print("grad baris 0 (padding) :", bool((grad[0] == 0).all()))
print("grad baris 4 (dipakai) :", bool((grad[4] != 0).any()))
print("grad baris 9 (tak dipakai):", bool((grad[9] == 0).all()))
print()
# ---------- 6. Membekukan tabel (trainable vs frozen) ----------
print("=== 6. Trainable vs frozen ===")
print("requires_grad awal :", tabel.weight.requires_grad)
tabel.weight.requires_grad_(False)
print("requires_grad setelah :", tabel.weight.requires_grad)
trainable = sum(p.numel() for p in tabel.parameters() if p.requires_grad)
print("parameter trainable :", trainable)Output yang Dihasilkan:
=== 1. Tabel pencari nn.Embedding(V, D) ===
shape weight : (10, 8)
jumlah parameter : 80
baris 0 (padding) : True
=== 2. Lookup batch token ID ===
masukan token_ids : (2, 5)
keluaran embedding : (2, 5, 8)
=== 3. Verifikasi: out[i, t] == weight[token_ids[i, t]] ===
out[0,1] == weight[4] : True
out[0,2] == weight[4] : True
out[1,4] == weight[0] : True
out[1,0] == weight[3] : True
=== 4. Kesetaraan lookup vs one-hot @ weight ===
max |one_hot @ W - W[4]| = 0.0
=== 5. padding_idx pada gradien ===
grad baris 0 (padding) : True
grad baris 4 (dipakai) : True
grad baris 9 (tak dipakai): True
=== 6. Trainable vs frozen ===
requires_grad awal : True
requires_grad setelah : False
parameter trainable : 0Mari kita telaah kode di atas baris per baris:
- Baris 15-16:
nn.Embedding(10, 8, padding_idx=0)membuat tabel 10 baris × 8 kolom. Argumenpadding_idx=0merujuk ID<pad>yang kita cadangkan pada slot nol di chapter Vocabulary. - Baris 25-30: batch masukan
(2, 5)berisi dua kalimat lima token, dengan token 0 sebagai padding di posisi akhir. Forward pass menghasilkan(2, 5, 8)— bentuk(N, T, D)yang sama dengan konvensi data sekuens. - Baris 35-39:
torch.equalmembandingkan nilai elemen demi elemen. Dua posisi berbeda yang memuat token ID 4 (out[0,1]danout[0,2]) sama-sama menyalin barisweight[4]— kata yang sama selalu mendapat vektor yang sama, di posisi mana pun ia muncul. - Baris 46-49:
F.one_hotmembuat vektor 10 dimensi bernilai 1 pada posisi 4; perkalianone_hot @ weightmenghasilkan vektor yang identik denganweight[4](selisih maksimum 0.0), membuktikan kesetaraan matematis lookup dan one-hot. - Baris 54-59: loss sederhana
out.sum()memberi gradien pada setiap baris yang terpakai. Baris 4 menerima gradien, baris 9 (token yang tidak muncul) tidak, dan baris 0 tetap tanpa gradien meskipun posisi padding ikut dijumlahkan —padding_idxmenolak pembaruan untuk baris tersebut. - Baris 63-68:
requires_grad_(False)membekukan seluruh tabel sehingga jumlah parameter trainable jatuh ke 0 — optimizer tidak akan lagi mengubah baris mana pun.
padding_idx Menjaga Baris #
Keluaran bagian kelima menutup satu masalah yang kita tinggalkan pada chapter Vocabulary: posisi <pad> ikut terhitung dalam operasi tensor. padding_idx=0 menjawab dengan dua jaminan — baris 0 diinisialisasi nol (bagian pertama keluaran) dan gradiennya selalu dinolkan (bagian kelima). Kombinasi keduanya membuat vektor padding netral: bobot nol tidak menyumbang makna, dan bobot itu tidak akan pernah bergeser oleh training. Alternatif yang lebih umum pada model nyata adalah memakai mask agar posisi padding diabaikan loss dan attention.
Trainable atau Frozen #
Tabel embedding punya dua moda pemakaian. Mode trainable membiarkan requires_grad=True: vektor kata mulai acak dan ikut belajar dari loss tugas utama — persis seperti dua tabel w_masuk dan w_keluar pada skip-gram yang kita latih sendiri. Mode frozen memuat vektor hasil pelatihan besar (Word2Vec, fastText, atau embedding model bahasa) lewat weight.copy_(...) di bawah torch.no_grad(), lalu memanggil requires_grad_(False) agar vektor terlatih tidak rusak oleh gradient tugas baru.
Dengan embedding layer, arsitektur NLP kita telah siap menerima teks mentah yang sudah melewati seluruh rantai transformasi. Keluaran (N, T, D) yang dihasilkannya adalah masukan standar bagi model sekuens dan klasifikasi teks — dan tepat di situ penerapan pertamanya kita bangun pada chapter berikutnya: text classification.