Hidup dengan vektor statis

Kemiripan kata, aritmetika analogi, dan batas polisemi pada vektor statis.

Daftar isi

Vektor statis dipakai dengan tiga cara yang mendasar: mengukur kemiripan kosinus antarkata, menjalankan aritmetika analogi, dan hidup dengan batas polisemi (polysemy), yakni kondisi satu kata memuat beberapa makna. Topik ini memperkenalkan ketiganya, menjalankan demo pada korpus kecil dengan keluaran apa adanya, lalu membaca hasilnya secara jujur, termasuk analogi yang meleset dan kemiripan yang tercampur oleh polisemi. Setelah membaca topik ini, penggunaan embedding pada pekerjaan nyata dapat berpijak pada perilaku yang benar-benar terjadi, bukan pada harapan.

Kemiripan kosinus sehari-hari#

Kemiripan kosinus adalah ukuran default saat membaca embedding: dua kata dianggap mirip bila arah vektornya berdekatan, tanpa memandang panjangnya. Rumusnya membagi perkalian titik dengan panjang kedua vektor:

kosinus⁡(u,v)=u⋅v∥u∥ ∥v∥ \operatorname{kosinus}(\mathbf{u}, \mathbf{v}) = \frac{\mathbf{u} \cdot \mathbf{v}}{\|\mathbf{u}\| \, \|\mathbf{v}\|}

Nilai 1 berarti arah sama, nilai 0 berarti tegak lurus, dan nilai -1 berarti berlawanan. Pada model word2vec, panjang vektor memang berbeda antarkata dan cenderung mengikuti frekuensi kemunculan, sehingga pembagian oleh panjang meniadakan perbedaan itu sebelum perbandingan. Yang dibaca hanyalah arah, dan arah itulah tempat informasi makna disimpan.

Demo berikut menghubungkan sudut dua vektor dengan nilai kosinusnya. Geser sudut vektor B dan amati perubahan angkanya, kemudian geser panjang B pada sudut yang sama dan amati bahwa nilai kosinusnya tidak berubah.

Kode sumber demo
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="utf-8">
<title>Demo kemiripan kosinus</title>
<style>
  body { font-family: sans-serif; margin: 14px; color: #1f3a4d; }
  #kanvas { border: 1px solid #c8d8e6; border-radius: 6px; background: #f7fafc; display: block; }
  .baris { display: flex; align-items: center; gap: 8px; margin-top: 10px; }
  .baris label { width: 120px; font-size: 13px; }
  .baris input { flex: 1; }
  .nilai { font-family: monospace; width: 52px; text-align: right; }
  .bacaan { margin-top: 10px; font-size: 14px; }
  .kosinus { font-family: monospace; font-size: 16px; font-weight: bold; }
  .tafsir { margin-top: 4px; font-size: 13px; color: #3a6b8f; }
</style>
</head>
<body>
  <canvas id="kanvas" width="360" height="200"></canvas>

  <div class="baris">
    <label for="sudut">Sudut vektor B</label>
    <input type="range" id="sudut" min="0" max="180" value="40">
    <span class="nilai" id="tampil-sudut">40&deg;</span>
  </div>
  <div class="baris">
    <label for="panjang">Panjang vektor B</label>
    <input type="range" id="panjang" min="0.3" max="2" step="0.1" value="1">
    <span class="nilai" id="tampil-panjang">1.0</span>
  </div>

  <div class="bacaan">
    kosinus = <span class="kosinus" id="tampil-kosinus"></span>
    <div class="tafsir" id="tafsir"></div>
  </div>

<script>
  var kanvas = document.getElementById('kanvas');
  var ctx = kanvas.getContext('2d');
  var pusatX = 36, pusatY = 172, skala = 80;

  function gambarPanah(ex, ey, warna, label, lx, ly) {
    ctx.strokeStyle = warna;
    ctx.fillStyle = warna;
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.moveTo(pusatX, pusatY);
    ctx.lineTo(ex, ey);
    ctx.stroke();
    var sudut = Math.atan2(ey - pusatY, ex - pusatX);
    ctx.beginPath();
    ctx.moveTo(ex, ey);
    ctx.lineTo(ex - 9 * Math.cos(sudut - Math.PI / 7), ey - 9 * Math.sin(sudut - Math.PI / 7));
    ctx.lineTo(ex - 9 * Math.cos(sudut + Math.PI / 7), ey - 9 * Math.sin(sudut + Math.PI / 7));
    ctx.closePath();
    ctx.fill();
    ctx.font = '13px sans-serif';
    ctx.fillText(label, lx, ly);
  }

  function perbarui() {
    var sudut = parseFloat(document.getElementById('sudut').value);
    var panjang = parseFloat(document.getElementById('panjang').value);
    var rad = sudut * Math.PI / 180;
    var kos = Math.cos(rad);

    ctx.clearRect(0, 0, kanvas.width, kanvas.height);
    ctx.strokeStyle = '#c8d8e6';
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.moveTo(16, pusatY);
    ctx.lineTo(344, pusatY);
    ctx.moveTo(pusatX, 12);
    ctx.lineTo(pusatX, 188);
    ctx.stroke();

    gambarPanah(pusatX + skala, pusatY, '#3a6b8f', 'A', pusatX + skala + 6, pusatY + 4);
    var bx = pusatX + panjang * skala * Math.cos(rad);
    var by = pusatY - panjang * skala * Math.sin(rad);
    gambarPanah(bx, by, '#c47f17', 'B', bx + 6, by - 6);

    document.getElementById('tampil-sudut').textContent = sudut + '\u00B0';
    document.getElementById('tampil-panjang').textContent = panjang.toFixed(1);
    document.getElementById('tampil-kosinus').textContent = kos.toFixed(3);
    var tafsir;
    if (kos > 0.5) {
      tafsir = 'kosinus tinggi: arah keduanya mirip';
    } else if (kos < -0.5) {
      tafsir = 'kosinus negatif: arah berlawanan';
    } else {
      tafsir = 'kosinus kecil: hampir tegak lurus, hubungan lemah';
    }
    document.getElementById('tafsir').textContent = tafsir;
  }

  document.getElementById('sudut').addEventListener('input', perbarui);
  document.getElementById('panjang').addEventListener('input', perbarui);
  perbarui();
</script>
</body>
</html>

Pada demo, kosinus menurun dari 1 menuju 0 saat sudut mendekati 90 derajat, lalu berlanjut ke -1 saat arahnya berbalik. Pergerakan panjang B tidak mengubah angka kosinus sedikit pun. Perilaku kedua itulah alasan kosinus dipilih untuk embedding: panjang vektor dipengaruhi frekuensi kata, sedangkan kemiripan yang dicari menyangkut arah.

Aritmetika analogi#

Aritmetika analogi menelusuri relasi antarkata dengan menggeser vektor. Dari pasangan membaca dan perpustakaan, vektor perpustakaan dikurangkan dari vektor membaca, vektor pasar ditambahkan, lalu kata terdekat dari hasil geseran itu dicari. Analogi kanoniknya memakai korpus raksasa, misalnya vektor raja dikurangi vektor pria dan ditambah vektor wanita mengarah ke ratu. Operasi ini pada Gensim dijalankan melalui most_similar dengan parameter positive untuk kata yang ditambahkan dan negative untuk kata yang dikurangkan. Perlu dicatat bahwa pencarian kata terdekat pada Gensim mengeluarkan kata-kata masukan dari daftar jawabannya, sehingga perpustakaan dan pasar tidak kembali sebagai jawaban sendiri.

Harapan yang wajar pada korpus demo adalah analogi membaca:perpustakaan diteruskan menjadi membeli:pasar, karena korpusnya memang memuat dua pola itu secara berulang. Korpus demo berukuran 55 kalimat, jauh dari skala korpus tempat analogi kanonik berhasil, dan keluaran pada demo berikut menunjukkan apa adanya.

Polisemi: satu vektor untuk semua makna#

Vektor statis memberi satu vektor tetap untuk satu kata, tanpa memandang makna yang sedang dipakai. Kata jalan dalam kalimat Budi jalan kaki ke perpustakaan berperan sebagai kegiatan, sedangkan dalam kalimat truk melintas di jalan raya berperan sebagai sarana. Kedua kalimat itu memakai vektor jalan yang sama, sehingga vektor tersebut menjadi campuran berbobot dari semua konteks kemunculannya. Kata polisem sejenis tidak sulit dicari, misalnya lama yang berarti usia maupun durasi, serta bulan yang berarti benda langit maupun satuan waktu.

Konsekuensinya terbaca pada daftar tetangga. Konteks makna yang lebih banyak muncul atau lebih khas akan menarik vektor ke sisinya, sedangkan makna lain melemah. Kemiripan antarkata pun ikut tercampur, karena dua kata bisa dekat melalui makna yang satu dan jauh melalui makna yang lain pada saat yang sama. Batas ini bukan cacat pelatihan yang dapat diperbaiki dengan parameter, melainkan konsekuensi langsung dari struktur satu vektor per kata, dan keluarga model kontekstual yang menghasilkan vektor berbeda untuk setiap kemunculan kata justru lahir untuk menutup celah ini.

Demo terpadu pada korpus kecil#

Skrip berikut melatih satu model word2vec pada korpus 55 kalimat yang memuat kata jalan dalam dua maknanya, lalu menjalankan ketiga cara hidup dengan vektor statis: kemiripan kosinus, aritmetika analogi, dan pembacaan tetangga kata polisem.

python uji_vektor_statis.py
import warnings
warnings.filterwarnings("ignore")
import logging
logging.getLogger("gensim").setLevel(logging.ERROR)

from gensim.models import Word2Vec

def buat_korpus():
    kalimat = []
    # tema perpustakaan
    for nama in ["Budi", "Citra", "Dewi"]:
        for objek in ["buku", "majalah"]:
            kalimat.append(f"{nama} membaca {objek} di perpustakaan")
            kalimat.append(f"{nama} meminjam {objek} di perpustakaan")
    kalimat.append("petugas menata buku di rak")
    kalimat.append("petugas menata majalah di rak")
    kalimat.append("Budi mencari buku di rak")
    kalimat.append("Citra mencari majalah di rak")
    # tema pasar
    for nama in ["Budi", "Citra", "Dewi"]:
        for objek in ["sayur", "buah"]:
            kalimat.append(f"{nama} membeli {objek} di pasar")
            kalimat.append(f"{nama} memilih {objek} di pasar")
    kalimat.append("Citra menghitung harga sayur")
    kalimat.append("Dewi menghitung harga buah")
    # tema jadwal belajar
    for nama in ["Budi", "Citra", "Dewi"]:
        kalimat.append(f"{nama} mencatat materi pelajaran")
        kalimat.append(f"{nama} mengerjakan latihan soal")
    kalimat.append("Citra menyalin jadwal belajar")
    kalimat.append("Dewi menempel jadwal belajar di dinding")
    kalimat.append("Budi mengisi jadwal belajar")
    # jalan sebagai kegiatan berjalan
    kalimat_jalan_kegiatan = "Budi jalan kaki ke perpustakaan"
    for nama in ["Budi", "Citra", "Dewi"]:
        kalimat.append(f"{nama} jalan kaki ke perpustakaan")
        kalimat.append(f"{nama} jalan kaki ke sekolah")
    kalimat.append("Budi jalan pagi di taman")
    kalimat.append("Citra jalan pelan di taman")
    kalimat.append("Dewi jalan sore di taman")
    # jalan sebagai sarana lalu lintas
    kalimat_jalan_sarana = "truk melintas di jalan raya"
    kalimat.append("truk melintas di jalan raya")
    kalimat.append("motor melaju di jalan raya")
    kalimat.append("sepeda melaju di jalan raya")
    kalimat.append("polisi mengatur lalu lintas di jalan")
    kalimat.append("lampu penerangan jalan menyala setiap malam")
    kalimat.append("petugas memasang papan penunjuk di jalan")
    kalimat.append("pengendara memperlambat kendaraan di jalan")
    return [k.split() for k in kalimat], kalimat_jalan_kegiatan, kalimat_jalan_sarana

kalimat_token, kalimat_jalan_kegiatan, kalimat_jalan_sarana = buat_korpus()
print("jumlah kalimat:", len(kalimat_token))

model = Word2Vec(
    sentences=kalimat_token,
    vector_size=100,
    window=2,
    min_count=1,
    workers=1,
    seed=42,
    sg=1,
    epochs=30,
)

print()
print("kemiripan jalan-kaki:", round(model.wv.similarity("jalan", "kaki"), 3))
print("kemiripan jalan-raya:", round(model.wv.similarity("jalan", "raya"), 3))
print()
print("kata paling mirip dengan jalan:")
for kata, skor in model.wv.most_similar("jalan", topn=6):
    print(f"  {kata}: {skor:.3f}")

print()
print("analogi membaca:perpustakaan = ? :pasar")
for kata, skor in model.wv.most_similar(
    positive=["membaca", "pasar"], negative=["perpustakaan"], topn=3
):
    print(f"  {kata}: {skor:.3f}")

print()
print("kalimat dengan jalan sebagai kegiatan:", kalimat_jalan_kegiatan)
print("kalimat dengan jalan sebagai sarana :", kalimat_jalan_sarana)
text Keluaran
jumlah kalimat: 55

kemiripan jalan-kaki: 0.181
kemiripan jalan-raya: 0.298

kata paling mirip dengan jalan:
  di: 0.437
  menyala: 0.374
  lalu: 0.341
  papan: 0.326
  memperlambat: 0.312
  penunjuk: 0.301

analogi membaca:perpustakaan = ? :pasar
  memilih: 0.276
  penunjuk: 0.230
  sekolah: 0.214

kalimat dengan jalan sebagai kegiatan: Budi jalan kaki ke perpustakaan
kalimat dengan jalan sebagai sarana : truk melintas di jalan raya

Pembacaan keluaran dimulai dari tetangga jalan. Keenam tetangga teratas, yaitu di, menyala, lalu, papan, memperlambat, dan penunjuk, semuanya berasal dari makna sarana lalu lintas, sedangkan tetangga makna berjalan seperti kaki dan taman tidak muncul. Alasannya terukur: dalam kalimat jalan kaki, tetangga langsung jalan sebagian besar adalah nama Budi, Citra, dan Dewi yang juga muncul di semua tema lain, sehingga sinyal makna kegiatan melemah, sedangkan makna sarana dilatih dengan kata-kata khas yang tidak dipakai di mana pun. Satu vektor berhenti di makna dominannya.

Analogi membaca:perpustakaan diteruskan ke pasar tidak menemukan membeli. Jawabannya justru memilih, penunjuk, dan sekolah, tiga kata yang tidak membentuk relasi yang dicari. Pergeseran vektor mengandaikan geometri yang rapi, sedangkan korpus 55 kalimat menghasilkan geometri yang bising, sehingga arah relasi tidak tersusun. Kemiripan jalan-kaki sebesar 0.181 dan jalan-raya sebesar 0.298 menutup daftar ini: urutannya benar, tetapi keduanya rendah dan berdekatan, mencerminkan satu vektor yang melayani dua makna sekaligus. Dua kalimat polisem di baris akhir memakai vektor jalan yang persis sama.

Kesalahan umum#

  1. Menuntut analogi benar pada korpus kecil. Aritmetika analogi membutuhkan geometri vektor yang rapi, dan geometri seperti itu baru terbentuk pada korpus besar. Perlakukan analogi korpus kecil sebagai ilustrasi perilaku, bukan alat kerja.
  2. Mengukur kemiripan dengan jarak Euclid antarvektor kata. Panjang vektor word2vec berbeda antarkata dan mengikuti frekuensi, sehingga jarak lurus ikut dipengaruhi frekuensi. Gunakan kosinus yang bekerja pada arah.
  3. Menyimpulkan model gagal saat tetangga kata polisem terlihat campur aduk. Perilaku itu konsekuensi struktur satu vektor per kata, bukan kesalahan pelatihan. Pilih model kontekstual bila tugasnya benar-benar menuntut pemisahan makna per kemunculan.

Ringkasan#

  • Kemiripan kosinus mengukur kedekatan arah dua vektor dan tidak terpengaruh panjangnya.
  • Aritmetika analogi menggeser vektor lewat most_similar dengan parameter positive dan negative, tetapi pada korpus kecil hasilnya meleset karena geometrinya belum rapi.
  • Satu vektor statis melayani semua makna kata polisem, sehingga tetangga teratas jalan berasal dari makna dominannya saja.
  • Batas ini lahir dari struktur vektor statis, bukan dari kesalahan pelatihan, dan menjadi pemicu lahirnya embedding kontekstual.

Referensi#