Chapter Word Embedding menunjukkan kata sebagai vektor; chapter Image Embedding menunjukkan gambar sebagai vektor. Begitu semua benda menjadi vektor, pertanyaan berikutnya seragam untuk seluruh domain: seberapa mirip dua vektor itu? Jawaban yang kita pilih menentukan perilaku pencarian, pengelompokan, dan sistem rekomendasi yang dibangun di atasnya. Chapter ini membedah dua ukuran kemiripan yang paling umum — kosinus dan euclidean — memperlihatkan keduanya sepakat dan berselisih pada angka konkret, lalu menghitung mengapa pencarian neighbor pada skala jutaan vektor menuntut pendekatan approximate nearest neighbor (ANN).

Kosinus Membaca Arah, Euclidean Membaca Jarak #

Kosinus mengukur sudut antara dua vektor: hasil 1 berarti sejajar, 0 berarti tegak lurus. Euclidean mengukur panjang selisih kedua vektor. Keduanya sering sepakat, tetapi berbeda justru pada kasus yang paling sering muncul di praktik: vektor dengan magnitudo berbeda. Sebuah dokumen yang membahas topik sama dua kali lebih panjang menghasilkan embedding yang arahnya sama tetapi magnitudonya lebih besar — kosinus menganggap keduanya identik (1.0), euclidean menganggap keduanya jauh. Pada representasi yang lazim dinormalkan (encoder kontras pada chapter Contrastive Learning menghasilkan embedding unit lewat F.normalize), kedua ukuran menjadi ekuivalen; persoalan muncul saat normalisasi tidak dilakukan.

Pemilihan ukuran juga mengikuti sifat datanya, dirangkum tabel berikut:

UkuranPeka terhadapCocok untukCatatan praktik
Kosinusarah sajateks, fitur semantik, embedding neuralmagnitudo tidak memengaruhi skor
Euclideanarah + magnitudokoordinat fisik, data terkalibrasisetara kosinus bila semua vektor unit
Perkalian titikarah × magnitudoskor kecocokan cepat, attentionmagnitudo ikut menaikkan skor

Demo: Angka Kemiripan yang Bisa Dibaca #

Delapan item minuman-makanan dipetakan ke empat dimensi fitur buatan (manis, cair, panas, kafein). Skrip mengukur dua pasangan ekstrem, mencetak matriks kemiripan 8×8, menjalankan pencarian nearest neighbor, lalu menghitung biaya brute-force versus ANN secara aritmetika:

python
"""Embedding dan similarity: kosinus vs euclidean, matriks kemiripan,
pencarian nearest neighbor, dan aritmetika biaya pencarian brute-force
vs approximate nearest neighbor (ANN).

Delapan item minuman-makanan dipetakan ke 4 dimensi fitur buatan
(manis, cair, panas, kafein). Angka konkret memperlihatkan: euclidean
sensitif terhadap panjang vektor, kosinus hanya peka terhadap arah;
pencarian neighbor berubah saat satu item digandakan skalanya; biaya
brute-force tumbuh linier terhadap jumlah vektor sehingga ANN perlu.
Murni NumPy, seed tetap, deterministik.
"""

import numpy as np

# urutan dimensi: [manis, cair, panas, kafein]
NAMA_FITUR = ["manis", "cair", "panas", "kafein"]
ITEM = {
    "kopi":    [0.6, 0.2, 0.9, 1.0],
    "teh":     [0.4, 0.3, 0.8, 0.4],
    "susu":    [0.7, 0.9, 0.3, 0.0],
    "air":     [0.0, 1.0, 0.1, 0.0],
    "jus":     [0.9, 0.9, 0.1, 0.0],
    "kue":     [0.9, 0.0, 0.2, 0.0],
    "cokelat": [0.8, 0.1, 0.2, 0.2],
    "sirup":   [1.0, 0.8, 0.0, 0.0],
}
nama = list(ITEM)
E = np.array([ITEM[k] for k in nama])               # (8, 4)
print("embedding 8 item x 4 dimensi "
      f"({', '.join(NAMA_FITUR)})")
print()

# ---------- 1. Kosinus vs euclidean pada dua pasangan ekstrem ----------
def kosinus(a, b):
    return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))


def euclidean(a, b):
    return float(np.linalg.norm(a - b))


print("=== Kosinus vs euclidean: dua pasangan ekstrem ===")
v, v_besar = E[0], 3.0 * E[0]                       # kopi vs "kopi 3 porsi"
print(f"  kopi           : {v}")
print(f"  kopi x3 (skala): {v_besar}")
print(f"  pasangan sama arah  : euclidean {euclidean(v, v_besar):.3f}, "
      f"kosinus {kosinus(v, v_besar):.3f}")
a, b = E[0], E[5]                                   # kopi vs kue
print(f"  pasangan kopi vs kue: euclidean {euclidean(a, b):.3f}, "
      f"kosinus {kosinus(a, b):.3f}")
print("  (euclidean menghukum skala besar; kosinus membaca arah saja)")
print()


# ---------- 2. Matriks kemiripan kosinus 8x8 ----------
print("=== Matriks kemiripan kosinus (baris = query, kolom = kandidat) ===")
norm = E / np.linalg.norm(E, axis=1, keepdims=True)
S = norm @ norm.T
header = "         " + "".join(f"{k[:6]:>7s}" for k in nama)
print(header)
for i, k in enumerate(nama):
    baris = "".join(f"{S[i, j]:7.2f}" for j in range(len(nama)))
    print(f"  {k:7s}{baris}")
print()


# ---------- 3. Nearest neighbor dengan dan tanpa item berganda skala ----------
def neighbor(query_idx, matriks, daftar_nama, k=3):
    skor = matriks[query_idx].copy()
    skor[query_idx] = -np.inf                       # jangan pilih diri sendiri
    urut = np.argsort(-skor)[:k]
    return [(daftar_nama[j], skor[j]) for j in urut]


print("=== 3 nearest neighbor item 'teh' (kosinus) ===")
for k, v_skor in neighbor(1, S, nama):
    print(f"  {k:8s} kosinus {v_skor:.3f}")
print()

print("=== Item 'teh 3 porsi' (vektor teh dikali 3) ditambahkan ===")
E2 = np.vstack([E, 3.0 * E[1]])
nama2 = nama + ["teh x3"]
norm2 = E2 / np.linalg.norm(E2, axis=1, keepdims=True)
S2 = norm2 @ norm2.T
D2 = np.linalg.norm(E2[:, None, :] - E2[None, :, :], axis=2)
print("  ranking kosinus untuk 'teh':")
for k, v_skor in neighbor(1, S2, nama2):
    print(f"    {k:8s} kosinus {v_skor:.3f}")
print("  ranking euclidean untuk 'teh':")
skor_d = D2[1].copy()
skor_d[1] = np.inf
urut = np.argsort(skor_d)[:3]
for j in urut:
    print(f"    {nama2[j]:8s} euclidean {skor_d[j]:.3f}")
print("  (kosinus menempatkan teh x3 paling mirip; euclidean menjauhkannya)")
print()


# ---------- 4. Biaya pencarian: brute-force vs ANN (aritmetika) ----------
print("=== Biaya per 1 query: jumlah vektor yang dibandingkan ===")
N, D = 1_000_000, 768
N_KLASTER, N_PROBE = 1024, 32
print(f"  basis data : {N:,} vektor x {D} dimensi")
print(f"  {'metode':28s} {'vektor dibanding':>18s} {'operasi':>15s}")
brute = N
ivf = N * N_PROBE // N_KLASTER
print(f"  {'brute-force (penuh)':28s} {brute:>18,} {brute * D:>15,}")
print(f"  {'ANN index (IVF, probe 32)':28s} {ivf:>18,} {ivf * D:>15,}")
print(f"  penghematan : {brute / ivf:.0f}x lebih sedikit vektor "
      f"({100 * ivf / brute:.1f}% data dipindai)")

Output yang Dihasilkan:

text
embedding 8 item x 4 dimensi (manis, cair, panas, kafein)

=== Kosinus vs euclidean: dua pasangan ekstrem ===
  kopi           : [0.6 0.2 0.9 1. ]
  kopi x3 (skala): [1.8 0.6 2.7 3. ]
  pasangan sama arah  : euclidean 2.973, kosinus 1.000
  pasangan kopi vs kue: euclidean 1.273, kosinus 0.525
  (euclidean menghukum skala besar; kosinus membaca arah saja)

=== Matriks kemiripan kosinus (baris = query, kolom = kandidat) ===
            kopi    teh   susu    air    jus    kue cokela  sirup
  kopi      1.00   0.93   0.50   0.19   0.43   0.53   0.69   0.40
  teh       0.93   1.00   0.65   0.37   0.54   0.55   0.67   0.49
  susu      0.50   0.65   1.00   0.78   0.98   0.63   0.70   0.94
  air       0.19   0.37   0.78   1.00   0.71   0.02   0.14   0.62
  jus       0.43   0.54   0.98   0.71   1.00   0.71   0.76   0.99
  kue       0.53   0.55   0.63   0.02   0.71   1.00   0.96   0.76
  cokelat   0.69   0.67   0.70   0.14   0.76   0.96   1.00   0.80
  sirup     0.40   0.49   0.94   0.62   0.99   0.76   0.80   1.00

=== 3 nearest neighbor item 'teh' (kosinus) ===
  kopi     kosinus 0.932
  cokelat  kosinus 0.674
  susu     kosinus 0.654

=== Item 'teh 3 porsi' (vektor teh dikali 3) ditambahkan ===
  ranking kosinus untuk 'teh':
    teh x3   kosinus 1.000
    kopi     kosinus 0.932
    cokelat  kosinus 0.674
  ranking euclidean untuk 'teh':
    kopi     euclidean 0.648
    cokelat  euclidean 0.775
    kue      euclidean 0.927
  (kosinus menempatkan teh x3 paling mirip; euclidean menjauhkannya)

=== Biaya per 1 query: jumlah vektor yang dibandingkan ===
  basis data : 1,000,000 vektor x 768 dimensi
  metode                         vektor dibanding         operasi
  brute-force (penuh)                   1,000,000     768,000,000
  ANN index (IVF, probe 32)                31,250      24,000,000
  penghematan : 32x lebih sedikit vektor (3.1% data dipindai)

Mari kita telaah kode di atas baris per baris:

  1. Baris 34-41 (dua fungsi ukur): kosinus membagi perkalian titik dengan hasil kali panjang kedua vektor — implementasi langsung dari rumus pada chapter Word Embedding; euclidean memakai np.linalg.norm atas selisih.
  2. Baris 42-51 (kasus ekstrem): v_besar = 3.0 * E[0] menggandakan vektor kopi tanpa mengubah arah. Hasilnya pasangan dengan euclidean 2.973 tetapi kosinus 1.000, kontras dengan pasangan kopi-kue yang euclidean-nya lebih kecil (1.273) namun kosinusnya hanya 0.525.
  3. Baris 55-63 (matriks): seluruh vektor dinormalkan per baris lewat pembagian np.linalg.norm(..., axis=1, keepdims=True), sehingga norm @ norm.T menghasilkan matriks kosinus 8×8 sekaligus — perkalian matriks tunggal menggantikan 64 panggilan fungsi.
  4. Baris 68-72 (neighbor): skor query ditimpa -inf pada posisinya sendiri agar item tidak menjadi tetangganya; np.argsort(-skor) mengurutkan menurun lalu mengambil tiga teratas.
  5. Baris 80-95 (item berganda skala): E2 menambahkan “teh x3” berupa vektor teh dikali tiga. Ranking kosinus menempatkannya di puncak (1.000), sementara ranking euclidean menjatuhkannya dari tiga besar — bukti konkret perbedaan perilaku kedua ukuran.
  6. Baris 99-109 (aritmetika ANN): index IVF membagi satu juta vektor ke 1024 klaster; query cukup memindai 32 klaster terdekat sehingga hanya 31.250 vektor (3.1%) dibandingkan — penghematan 32 kali dibanding brute-force.

Membaca Matriks dan Ranking #

Matriks kemiripan memperlihatkan blok struktur yang masuk akal: “susu” bersahabat dengan “jus” (0.98) dan “sirup” (0.94) karena sama-sama manis-cair; “kue” dengan “cokelat” (0.96) karena sama-sama padat-manis; sedangkan “air” nyaris tegak lurus terhadap “kue” (0.02). Uji “teh x3” kemudian memperlihatkan kapan pilihan ukuran berakibat nyata: aplikasi pencarian semantik umumnya memilih kosinus agar dokumen panjang dan pendek bertopik sama tetap saling ditemukan.

Pencarian pada Skala Jutaan Vektor #

Angka pada bagian terakhir keluaran menjelaskan mengapa pustaka seperti FAISS menjadi standar industri: pencarian brute-force atas satu juta embedding 768 dimensi menuntut 768 juta operasi perkalian-tambah per query. Index ANN bertipe IVF memangkas pemindaian ke sebagian kecil klaster — 32 kali lebih hemat pada konfigurasi contoh kita — dengan konsekuensi hasil kadang melewatkan tetangga sejati (itulah makna kata approximate). Dokumentasi scikit-learn tentang NearestNeighbors merinci keluarga algoritma pencarian tetangga, termasuk varian berbasis graf yang dipakai banyak sistem produksi.

Embedding dan ukuran kemiripannya adalah hasil akhir representation learning. Pertanyaan penutup section ini praktis: setelah pretrained encoder tersedia (milik sendiri atau unduhan publik), bagaimana cara memakainya dengan benar — berapa banyak yang dibekukan, kapan fine-tune layak, dan apa yang terjadi bila domain data berbeda? Chapter berikutnya menjawab semuanya.