Embedding

nn.Embedding dan representasi vektor kata.

Daftar isi

Embedding adalah tabel lookup yang memetakan setiap indeks token menjadi vektor padat berukuran tetap, dan vektor tersebut menjadi parameter yang dipelajari model. Topik ini membahas penggunaan nn.Embedding, bentuk weight dan output-nya, kesetaraannya dengan perwakilan one-hot, perlakuan khusus untuk token pad melalui padding_idx, serta cara bobot tabel membentuk kemiripan antara kata. Setelah topik ini, indeks token pada data sekuens dapat dikonversi menjadi vektor yang siap diproses layer berikutnya.

Tabel lookup dan bentuknya#

nn.Embedding dikonstruksi dengan dua argumen utama, yaitu num_embeddings untuk banyaknya baris tabel dan embedding_dim untuk panjang vektor setiap baris. Weight-nya berbentuk (num_embeddings, embedding_dim), dan layer ini memerlukan input bertipe bilangan bulat berupa indeks token. Untuk batch sekuens berbentuk (batch, panjang_sekuens), keluarannya berbentuk (batch, panjang_sekuens, embedding_dim), yaitu satu vektor untuk setiap posisi token.

python embedding-dasar.py
import torch

torch.manual_seed(0)

# tabel lookup untuk 6 token (indeks 0 = <pad>), setiap token menjadi vektor 3 angka
embedding = torch.nn.Embedding(num_embeddings=6, embedding_dim=3)

# input: (2, 4) = (batch, panjang_sekuens), nilai berupa indeks token
token = torch.tensor([[1, 2, 3, 0], [1, 2, 5, 0]])

# output: (2, 4, 3) = (batch, panjang_sekuens, embedding_dim)
hasil = embedding(token)

print("Bentuk weight:", tuple(embedding.weight.shape))
print("Bentuk hasil :", tuple(hasil.shape))
print("Vektor indeks 2 dari hasil :", [round(v, 3) for v in hasil[0, 1].tolist()])
print("Baris indeks 2 dari weight :", [round(v, 3) for v in embedding.weight[2].tolist()])
text Keluaran
Bentuk weight: (6, 3)
Bentuk hasil : (2, 4, 3)
Vektor indeks 2 dari hasil : [-0.341, 1.853, 0.468]
Baris indeks 2 dari weight : [-0.341, 1.853, 0.468]

Dua baris terakhir pada keluaran menunjukkan sifat dasar lookup ini. Vektor untuk indeks 2 pada hasil sama persis dengan baris indeks 2 pada weight, karena nn.Embedding tidak melakukan perhitungan lain selain mengambil baris yang sesuai. Setiap pemanggilan dengan indeks yang sama selalu menghasilkan baris yang sama, dan perubahan bobot selama pelatihan langsung mengubah vektor yang dikembalikan.

Jumlah parameter tabel mudah dihitung sebelum model dibangun, yaitu num_embeddings dikalikan embedding_dim. Tabel untuk kosakata 30.000 token dengan dimensi 300 memuat sembilan juta parameter, angka yang jauh lebih besar daripada banyak layer klasifikasi. Besarnya tabel inilah alasan dimensi embedding dipilih secukupnya pada model kecil, misalnya 50 sampai 100 untuk kosakata terbatas.

Kesetaraan dengan perwakilan one-hot#

Cara lain memandang embedding adalah melalui perwakilan one-hot, yaitu vektor sepanjang num_embeddings yang berisi angka satu di posisi indeks token dan nol di tempat lain. Apabila vektor one-hot itu dikalikan dengan matriks weight, hasilnya adalah persis satu baris weight yang dipilih oleh angka satu tersebut. Layer nn.Embedding mengerjakan langkah ini secara langsung tanpa benar-benar membuat tensor one-hot, sehingga hemat memori, tetapi hasilnya identik.

python one-hot-vs-lookup.py
import torch
import torch.nn.functional as F

torch.manual_seed(0)

num_embeddings, embedding_dim = 6, 3
embedding = torch.nn.Embedding(num_embeddings, embedding_dim)

token = torch.tensor([1, 2, 3])             # (3,) = (panjang_sekuens,)
hasil_lookup = embedding(token)             # (3, 3)

# jalur equivalen: one-hot lalu perkalian matriks dengan weight
satu_hot = F.one_hot(token, num_classes=num_embeddings).float()   # (3, 6)
hasil_linear = satu_hot @ embedding.weight                        # (3, 6) @ (6, 3) -> (3, 3)

print("Hasil lookup :", [[round(v, 3) for v in baris] for baris in hasil_lookup.tolist()])
print("Hasil one-hot:", [[round(v, 3) for v in baris] for baris in hasil_linear.tolist()])
print("Identik      :", torch.equal(hasil_lookup, hasil_linear))
text Keluaran
Hasil lookup : [[0.794, 0.599, -1.555], [-0.341, 1.853, 0.468], [-0.158, -0.173, 0.183]]
Hasil one-hot: [[0.794, 0.599, -1.555], [-0.341, 1.853, 0.468], [-0.158, -0.173, 0.183]]
Identik      : True

Kesetaraan ini menjelaskan mengapa embedding disebut parameter yang dipelajari. Gradien yang mengalir ke weight melalui lookup sama dengan gradien yang mengalir melalui perkalian matriks, sehingga tabel embedding tumbuh menjadi representasi yang disesuaikan oleh loss, bukan tabel yang ditulis tangan.

Perbandingan ini juga menegaskan pola gradiennya. Satu langkah lookup hanya menyentuh baris yang indeksnya muncul pada batch, sehingga baris lain menerima gradien nol, dan perilaku yang sama terjadi pada perkalian dengan matriks one-hot yang kolomnya berisi nol. Dari sisi pelatihan, kedua jalur benar-benar tidak dapat dibedakan. Dari sisi memori, tensor one-hot berukuran panjang sekuens kali jumlah kosakata jauh lebih boros daripada mengambil baris tabel secara langsung, dan perbedaan itulah yang membuat lookup menjadi pilihan bawaan.

padding_idx untuk token isian#

Token pad dilapisi untuk menyeragamkan panjang sekuens, dan vektor isian tersebut seharusnya tidak dipelajari sebab token ini tidak membawa makna. Argumen padding_idx menandai satu indeks untuk diperlakukan khusus, yaitu diinisialisasi berisi nol dan dikecualikan dari pembaruan gradien sehingga barisnya tetap nol sepanjang pelatihan. Indeks yang ditunjuk harus sama dengan indeks pad pada tabel kosakata, yang pada contoh sebelumnya adalah 0.

python embedding-padding.py
import torch

torch.manual_seed(0)

# padding_idx=0: baris 0 diinisialisasi nol dan tidak diperbarui pelatihan
embedding = torch.nn.Embedding(num_embeddings=6, embedding_dim=3, padding_idx=0)

print("Baris pad awal   :", embedding.weight[0].tolist())

# batch: (2, 4) = (batch, panjang_sekuens); indeks 0 berarti <pad>
token = torch.tensor([[1, 2, 0, 0], [3, 4, 5, 0]])

# output: (2, 4, 3) = (batch, panjang_sekuens, embedding_dim)
hasil = embedding(token)
print("Vektor token pad :", hasil[0, 2].tolist())

loss = hasil.sum()
loss.backward()

grad = embedding.weight.grad
print("Gradien baris pad:", grad[0].abs().sum().item())
print("Gradien baris 1  :", grad[1].abs().sum().item() > 0)
text Keluaran
Baris pad awal   : [0.0, 0.0, 0.0]
Vektor token pad : [0.0, 0.0, 0.0]
Gradien baris pad: 0.0
Gradien baris 1  : True

Keluaran memperlihatkan tiga fakta sekaligus. Baris pad diinisialisasi nol, lookup terhadap indeks 0 menghasilkan vektor nol, dan setelah backward() dipanggil gradien baris pad tetap 0.0 sementara baris lain menerima gradien. Tanpa argumen ini, baris pad justru ikut terlatih seperti token biasa, dan loss yang menyertakan posisi pad akan menggeser vektornya tanpa ada makna yang dilindungi.

Bobot yang dipelajari#

Bobot tabel dimulai dari angka acak dan berubah mengikuti loss selama pelatihan. Token yang sering muncul pada konteks serupa terdorong untuk memiliki vektor yang berdekatan, sehingga arah vektor pada akhirnya menyimpan jejak distribusi kata dalam data pelatihan. Ukuran kemiripan yang lazim adalah kesamaan kosinus, yaitu kosinus sudut antara dua vektor yang bernilai satu untuk arah yang sama dan nol untuk arah yang saling tegak lurus.

Demo berikut menggunakan vektor dua dimensi yang disetel manual agar arahnya mudah diamati. Klik salah satu kata, kemudian amati urutan kesamaan kosinusnya beserta posisi titik pada bidang.

Pada pelatihan nyata, dimensi embedding lazimnya berkisar 50 sampai 300, dan kemiripannya dihitung terhadap jutaan token sekaligus. Prinsipnya tetap sama dengan demo di atas, yaitu vektor yang arahnya serupa menunjukkan kemiripan yang besar. Tutorial resmi Word Embeddings: Encoding Lexical Semantics pada situs PyTorch memperlihatkan proses ini dilatih langsung dengan nn.Embedding.

Kesalahan umum#

Kesalahan pertama adalah memberikan tensor float sebagai indeks. Layer nn.Embedding menuntut indeks bertipe bulat, dan pemanggilan dengan tensor float berhenti dengan pesan berikut:

python float-ke-embedding.py
import torch

embedding = torch.nn.Embedding(num_embeddings=6, embedding_dim=3)

# tensor float diberikan sebagai indeks, padahal indeks harus bilangan bulat
token = torch.ones(2, 4, dtype=torch.float32)   # (2, 4) = (batch, panjang_sekuens)

try:
    hasil = embedding(token)
except (RuntimeError, IndexError, TypeError) as e:
    pesan = str(e).splitlines()[0]
    print(type(e).__name__ + ":", pesan)
text Keluaran
RuntimeError: Expected tensor for argument #1 'indices' to have one of the following scalar types: Long, Int; but got torch.FloatTensor instead (while checking arguments for embedding)

Pesan errornya muncul sebelum lookup dijalankan karena layer memeriksa tipe data input terlebih dahulu. Perbaikannya bukan mengubah tabel, melainkan memastikan data yang masuk masih berupa indeks bilangan bulat, misalnya dengan memeriksa dtype tensor sebelum diberikan ke layer.

Kesalahan kedua adalah menyusutkan tabel hingga lebih kecil dari kosakata. Indeks yang melebihi num_embeddings - 1 memicu error indeks di luar jangkauan:

python indeks-berlebih.py
import torch

# tabel hanya memuat 6 baris, yaitu indeks 0 sampai 5
embedding = torch.nn.Embedding(num_embeddings=6, embedding_dim=3)

# indeks 7 tidak ada pada tabel
token = torch.tensor([[1, 7, 3]])   # (1, 3) = (batch, panjang_sekuens)

try:
    hasil = embedding(token)
except (RuntimeError, IndexError) as e:
    pesan = str(e).splitlines()[0]
    print(type(e).__name__ + ":", pesan)
text Keluaran
IndexError: index out of range in self

Penyebabnya hampir selalu tabel kosakata yang berubah setelah layer dibangun, misalnya ada token baru yang belum diperhitungkan, sehingga ukuran tabel wajib disesuaikan dengan kosakata akhir sebelum pelatihan dimulai. Kesalahan ketiga berkaitan dengan token pad dan muncul dalam dua bentuk, yaitu lupa menetapkan padding_idx sehingga baris pad ikut terlatih, serta menjumlahkan atau merata-ratakan vektor sekuens tanpa menyaring posisi pad. Kedua kebiasaan itu membuat vektor isian yang tidak bermakna ikut menyumbang pada representasi akhir, dan penanganannya adalah padding_idx pada sisi tabel serta penyaringan posisi pad pada sisi perhitungan.

Ringkasan#

  • nn.Embedding adalah tabel lookup dengan weight berbentuk (num_embeddings, embedding_dim), dan input (batch, panjang_sekuens) menghasilkan (batch, panjang_sekuens, embedding_dim).
  • Lookup setara dengan mengalikan vektor one-hot token terhadap matriks weight, tetapi dikerjakan tanpa menyusun tensor one-hot.
  • Argumen padding_idx menginisialisasi baris pad menjadi nol dan menghentikan pembaruannya, sehingga token isian tidak ikut terlatih.
  • Bobot tabel dipelajari dari loss, dan token dengan konteks serupa terdorong untuk memiliki vektor yang berdekatan menurut kesamaan kosinus.

Referensi#