Skip to content

Blind Spot Gate Duplikat: Cakupan Vektor dan Batas 0,50

Adityo Guni Waluyo

Gate duplikat berbasis cosine similarity cuma sekuat cakupan indeks vektornya. Pelajaran dari blind spot 32 artikel dan angka 0,50 yang dikalibrasi sendiri.

Ringkasan

Gate cek duplikat kelihatan normal padahal 32 artikel nggak ke-embed, jadi skornya ngableng tanpa peringatan. Fix-nya backfill indeks plus self-check: judul artikel sendiri harus balikin slug-nya sendiri. Threshold 0,50 juga bukan angka sakral; ukur dulu distribusi skor korpus sendiri, baru taruh garis potong.

Pagi itu saya menjalankan articles.py check-title untuk topik kandidat yang mau dicek duplikatnya. Layar menampilkan JSON berisi best_slug dan skor sim yang tampak meyakinkan. Saya hampir langsung percaya. Untung ada kebiasaan lama: membuka dulu berkas work/embeddings.json sebelum percaya angka. Ternyata indeks itu cuma memuat 164 vektor artikel, sementara artikel yang sudah terbit ada 196 (komit fea59b1). Artinya sekitar 16% korpus tidak pernah muncul di radar gate duplikat saya.

Tebakan awal saya memang keliru. Begitu expose subcommand check-title (komit e3c443a), saya merasa pekerjaan duplikat-check sudah selesai. Padahal gate yang menjawab dari indeks setengah jalan hanya menghasilkan kepastian yang salah. Pengecekan topik baru bisa mengembalikan skor yang dihitung melawan korpus yang hilang sebagian, dan saya tidak akan pernah tahu kapan hasilnya menyesatkan.

Blind spot cakupan vektor

Masalahnya tidak di rumus, tapi di bahan masukan. Kesamaan kosinus dihitung sebagai 1 − (u·v)/(‖u‖‖v‖), dan scipy.spatial.distance.cosine menghitungnya untuk array 1-D [1]. Rumus itu bekerja pada apa pun yang ada di indeks; ia tidak bisa memberi tahu artikel mana yang belum diverktorkan. Selama 32 artikel belum punya vektor, mereka kebal terhadap deteksi duplikat: topic check yang seharusnya menabrak salah satu dari artikel itu akan kembali dengan skor rendah dan lolos begitu saja.

Perbaikannya mekanis. Backfill mengisi indeks dari 164 menjadi 196 baris (komit fea59b1), dan sejak itu setiap artikel baru di-embed saat terbit. Saya juga menambahkan self-check sederhana: setelah embed, check-title dengan judul artikel sendiri harus mengembalikan slug-nya sendiri sebagai best_slug. Kalau tidak, embed-nya gagal dan saya tahu sebelum gate dipakai untuk keputusan.

Yang bikin blind spot ini licik: gate tidak pernah error saat datanya kurang. Ia tetap mengembalikan jawaban, cukup dari artikel yang ada di indeks saja. Tidak ada log merah, tidak ada peringatan, cuma skor yang terlihat normal. Kegagalan tipe ini beda dengan crash yang langsung kelihatan; ia lebih mirip timbangan yang belum dikalibrasi, angkanya tampil rapi padahal dasarnya bergeser. Makanya self-check di atas saya anggap wajib, bukan pelengkap.

0,50 bukan hukum universal

Kesalahan kedua lebih halus. Setelah gate jalan, saya hampir memperlakukan angka 0,50 sebagai konstanta alam. Angka itu cuma masuk akal karena saya mengukurnya dari distribusi skor pasangan nyata di korpus sendiri: pasangan tak terkait berkumpul di ≤ 0,46, pasangan satu-domain di ≥ 0,68, dan 0,50 dipilih di celah antara keduanya.

Angka kemiripan juga nggak bisa dibaca seperti persen. Skor 0,50 bukan berarti setengah mirip; sebarannya tergantung korpus. Artikel-artikel di blog saya sama-sama membahas deploy, API, dan docker, jadi dua topik yang beda pun sudah berbagi kosakata dasar. Baseline kemiripannya otomatis lebih tinggi daripada korpus campuran topik acak. Di korpus lain, celah antara pasangan tak terkait dan satu-domain bisa jatuh di angka yang berbeda. Itu sebabnya menyalin cutoff orang lain sama saja menebak.

Kalau korpusnya berubah, distribusinya bisa bergeser, dan cutoff lama bisa salah sasaran. Ini alasan saya menolak mengkopi threshold dari proyek lain begitu saja. Kesamaan kosinus memang rekomendasi standar untuk mengukur keterkaitan embedding [2], dan vektor ternormalisasi panjang 1 membuat dot product memberi peringkat yang sama. Rekomendasi itu bicara soal rumus jarak; soal di mana garis potongnya, kamu yang menentukan dari data sendiri. OpenAI sendiri menjelaskan embedding sebagai vektor float yang mengukur keterkaitan teks [2]; tidak ada halaman dokumentasi mana pun yang tahu distribusi skor korpusmu.

Benchmark menilai model, bukan cutoff

MTEB adalah evaluasi embedding lintas bahasa dan modalitas yang jadi rujukan untuk membandingkan model [3]. Nilai bagus di MTEB memberi alasan untuk memilih model itu, selesai. Ia tidak bisa memberi tahu cutoff duplikat untuk korpus artikel berbahasa campuran milik saya, karena cutoff itu ditentukan oleh sebaran datanya sendiri.

Pola yang saya pakai sekarang: lengkapi dulu cakupan indeks, ukur distribusi skor dari pasangan nyata, baru tarik garis potong di celah yang jelas. Urutannya tidak boleh dibalik. Gate yang dikalibrasi dari distribusi sendiri baru berhak memutuskan sebuah topik "baru".

Sumber

  1. scipy.spatial.distance.cosine — SciPy documentation
  2. Vector embeddings — OpenAI documentation
  3. MTEB: State-of-the-art evaluation of embeddings across languages and modalities

Artikel terkait