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:
| Ukuran | Peka terhadap | Cocok untuk | Catatan praktik |
|---|---|---|---|
| Kosinus | arah saja | teks, fitur semantik, embedding neural | magnitudo tidak memengaruhi skor |
| Euclidean | arah + magnitudo | koordinat fisik, data terkalibrasi | setara kosinus bila semua vektor unit |
| Perkalian titik | arah × magnitudo | skor kecocokan cepat, attention | magnitudo 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:
"""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:
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:
- Baris 34-41 (dua fungsi ukur):
kosinusmembagi perkalian titik dengan hasil kali panjang kedua vektor — implementasi langsung dari rumus pada chapter Word Embedding;euclideanmemakainp.linalg.normatas selisih. - 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. - Baris 55-63 (matriks): seluruh vektor dinormalkan per baris lewat pembagian
np.linalg.norm(..., axis=1, keepdims=True), sehingganorm @ norm.Tmenghasilkan matriks kosinus 8×8 sekaligus — perkalian matriks tunggal menggantikan 64 panggilan fungsi. - Baris 68-72 (
neighbor): skor query ditimpa-infpada posisinya sendiri agar item tidak menjadi tetangganya;np.argsort(-skor)mengurutkan menurun lalu mengambil tiga teratas. - Baris 80-95 (item berganda skala):
E2menambahkan “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. - 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.