Pada sistem rekomendasi dan pencarian berbasis vektor, kalkulasi similarity dilakukan jutaan kali per detik. Masalah muncul ketika kalkulasi cosine similarity menghasilkan nilai NaN (Not a Number) atau nilai di luar batas teoretis seperti 1.0000002. Dalam pipeline produksi backend berpresisi tunggal (float32), anomali ini memicu kegagalan serialisasi JSON, rusaknya urutan sorting ranking, hingga Internal Server Error (HTTP 500).
Gejala Bug pada Pipeline Rekomendasi
Bug numerik pada cosine similarity umumnya bermanifestasi dalam dua bentuk:
- Nilai NaN: Terjadi saat salah satu vektor memiliki magnitudo nol (vektor kosong atau hasil masking padding). Operasi pembagian dot product dengan magnitudo menghasilkan
0.0 / 0.0, yang langsung dievaluasi menjadiNaNoleh spesifikasi IEEE 754. - Skor di Luar Domain [-1.0, 1.0]: Dua vektor identik yang diproses dengan presisi
float32dapat menghasilkan nilai dot product dibagi magnitudo sebesar1.0000001atau-1.0000002. Jika nilai ini diteruskan ke fungsi hilir sepertinumpy.arccosuntuk menghitung sudut jarak, runtime akan melempar invalid value warning dan menghasilkanNaN.
Analisis Root Cause
Formulasi standar cosine similarity didefinisikan sebagai:
similarity = (A · B) / (||A|| * ||B||)1. Division-by-Zero Tanpa Batas Toleransi
Jika teks input kosong atau diisi karakter di luar kosakata tokenizer, model embedding dapat menghasilkan vektor nol (semua elemen bernilai 0.0). Magnitude ||A|| bernilai 0.0. Menguji ||A|| == 0.0 secara eksak dalam floating-point sering gagal mendeteksi vektor dengan magnitude sangat kecil (misalnya 1e-39 pada float32) yang mengalami underflow saat dikalikan dengan ||B|| di penyebut.
2. Akumulasi Error dan Catastrophic Cancellation
Format IEEE 754 float32 hanya menyediakan 24 bit presisi signifikan (~7 digit desimal). Saat melakukan dot product pada vektor berdimensi tinggi (misalnya 768 atau 1536 dimensi), akumulasi pembulatan (rounding error) pada penjumlahan berulang dapat membuat hasil pembagian melampaui batas batas matematis 1.0.
Solusi Rekayasa
Untuk memastikan stabilitas numerik tanpa mengorbankan performa, terapkan tiga proteksi berikut:
- Validasi Zero-Norm dengan Machine Epsilon: Tetapkan nilai ambang batas bawah (floor tolerance) berbasis
numpy.finfo(np.float32).epsatau batas minimum operasional (misalnya1e-12). Jika norm berada di bawah batas ini, kembalikan skor default (misalnya0.0). - Normalisasi Vektor ke Unit Vector Lebih Awal: Bagi vektor dengan magnitudonya sebelum menghitung dot product. Mengubah kalkulasi menjadi
dot(normalize(A), normalize(B))membatalkan kebutuhan perkalian norm di penyebut dan memangkas kompleksitas komputasi saat inferensi berulang. - Clamping Intervensi: Gunakan fungsi pemotongan nilai ke dalam interval
[-1.0, 1.0]untuk mengeliminasi deviasi bit pembulatan.
Implementasi Kode
Berikut adalah implementasi Python mandiri yang menangani kasus tepi zero-vector, magnitude sangat kecil, dan rounding drift:
import numpy as np
def safe_cosine_similarity(a: np.ndarray, b: np.ndarray, eps: float = 1e-9) -> float:
# ponytail: validasi bentuk 1D, tingkatkan ke 2D batching jika throughput naik
if a.shape != b.shape:
raise ValueError(f"Dimensi tidak cocok: {a.shape} vs {b.shape}")
norm_a = float(np.linalg.norm(a))
norm_b = float(np.linalg.norm(b))
# Guard: Zero-norm check
if norm_a < eps or norm_b < eps:
return 0.0
# L2-normalization awal
unit_a = a / norm_a
unit_b = b / norm_b
# Dot product pada unit vector
dot_product = float(np.dot(unit_a, unit_b))
# Machine epsilon clamping ke interval matematis [-1.0, 1.0]
return max(-1.0, min(1.0, dot_product))
# ==========================================
# Runnable Unit Checks
# ==========================================
if __name__ == "__main__":
# 1. Test Zero Vector (Mencegah NaN)
v_zero = np.zeros(128, dtype=np.float32)
v_random = np.random.randn(128).astype(np.float32)
sim_zero = safe_cosine_similarity(v_zero, v_random)
assert sim_zero == 0.0, f"Expected 0.0, got {sim_zero}"
# 2. Test Identical Vector Floating Drift (Mencegah > 1.0)
# Membuat vektor bernilai besar yang rentan akumulasi drift pada float32
v_drift = np.full(768, 1.234567, dtype=np.float32)
sim_drift = safe_cosine_similarity(v_drift, v_drift)
assert -1.0 <= sim_drift <= 1.0, f"Value out of bounds: {sim_drift}"
assert np.isclose(sim_drift, 1.0, atol=1e-6), f"Expected ~1.0, got {sim_drift}"
# 3. Test Opposing Vector (Mencegah < -1.0)
sim_opposing = safe_cosine_similarity(v_drift, -v_drift)
assert -1.0 <= sim_opposing <= 1.0, f"Value out of bounds: {sim_opposing}"
assert np.isclose(sim_opposing, -1.0, atol=1e-6), f"Expected ~-1.0, got {sim_opposing}"
print("Semua pengujian stabilitas floating-point lolos.")
Rekomendasi Produksi: Lakukan unit normalization (L2) satu kali pada tahap ingestion data vektor ke database. Saat vektor disimpan dalam bentuk unit-length, operasi cosine similarity tereduksi secara komputasi menjadi perkalian dot product murni yang jauh lebih cepat dan minim resiko instabilitas.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!