Loss InfoNCE pada chapter sebelumnya menghitung skor atas “pasangan”, tetapi keberhasilannya sebenarnya ditentukan jauh sebelum loss dijalankan: oleh kualitas pasangan positif dan negatif yang kita bangun. Satu pasangan positif yang keliru bisa menarik dua objek berbeda ke titik yang sama; satu negatif yang terlalu mudah membuat loss turun tanpa encoder belajar apa pun. Chapter ini membedah tiga hal praktis di balik konstruksi pasangan: definisi positif-negatif per domain, efisiensi in-batch negatives, serta dua fenomena khas — hard negative dan false negative. Seluruhnya kita ukur dengan angka nyata pada cluster sintetis berisi tumpang-tindih.
Definisi Positif dan Negatif per Domain #
Positif selalu berarti “dua tampilan yang seharusnya dipetakan ke representasi sama”, sedangkan negatif berarti “seharusnya berbeda”. Cara mewujudkannya bergantung pada struktur datanya:
| Domain | Pasangan positif | Pasangan negatif |
|---|---|---|
| Citra | dua augmentasi foto yang sama (potong, ubah warna) | foto objek lain dalam batch |
| Teks kalimat | dua kalimat berurutan dalam dokumen | kalimat dari dokumen lain |
| Multimodal (CLIP) | gambar dan teks keterangannya | gambar dipasangkan teks gambar lain |
| Graf pengguna | dua node bertetangga | dua node jauh |
| Video | dua frame berdekatan waktu | frame dari video lain |
Satu keputusan desain penting: pasangan negatif tidak perlu dihitung manual satu per satu. Jika satu batch memuat B objek berbeda, setiap objek otomatis mendapat B−1 negatif dari anggota batch lainnya — inilah in-batch negatives. Banyaknya negatif tumbuh kuadratik terhadap ukuran batch: batch 4096 menghasilkan lebih dari 16 juta pasangan terbanding dalam satu langkah pelatihan. Fakta inilah alasan SimCLR memakai batch berukuran ribuan dan MoCo membangun bank antrean agar negatif tersedia melimpah tanpa membebani ukuran batch.
Hard Negative dan False Negative #
Dua fenomena menghantui kualitas pasangan. Pertama, hard negative: pasangan negatif yang jaraknya sangat dekat — misalnya foto kucing persia versus foto anjing berbulu tebal. Negatif seperti ini paling informatif karena memaksa encoder mencari fitur pembeda yang halus, tetapi juga paling berisiko salah kalau anotatornya lengah. Kedua, false negative: pasangan yang kita labeli negatif padahal kedua objeknya secara semantik sejenis — dua foto kucing berbeda yang kebetulan dianggap “objek lain”. False negative memberi sinyal kontradiktif: loss memaksa encoder menjauhkan dua hal yang seharusnya didekatkan, sehingga representation kelas tersebut merenggang.
Kita bangun lima cluster 2D — empat rapat di sudut grid, satu lebar di tengah yang tumpang-tindih dengan keempatnya — lalu mengukur ketiga hal tersebut dari pasangan yang disampel:
"""Pembangunan pasangan positif-negatif dari cluster sintetis.
Lima cluster 2D dibangun: empat cluster rapat di sudut grid dan satu
cluster lebar di tengah yang tumpang-tindih dengan keempatnya.
Dari data tanpa label ini kita bangun:
- pasangan positif : dua titik dari cluster yang sama,
- pasangan negatif : dua titik dari cluster berbeda.
Statistik jarak kedua jenis pasangan dihitung, lalu tiga fenomena
dibedah dengan angka konkret: hard negative (negatif yang sangat
dekat), false negative (negatif yang nyatanya satu kelas semantik),
dan jumlah negatif dalam-batch sebagai fungsi ukuran batch.
Murni NumPy, seed tetap, deterministik.
"""
import numpy as np
rng = np.random.default_rng(42)
# ---------- 1. Lima cluster: 4 rapat + 1 lebar di tengah ----------
PUSAT = np.array([[0, 0], [3, 0], [0, 3], [3, 3], [1.5, 1.5]])
SEBARAN = np.array([0.4, 0.4, 0.4, 0.4, 0.9]) # cluster ke-5 lebar
N_PER = 50
titik, kelas = [], []
for c, (p, s) in enumerate(zip(PUSAT, SEBARAN)):
titik.append(p + s * rng.standard_normal((N_PER, 2)))
kelas.append(np.full(N_PER, c))
X = np.concatenate(titik) # (250, 2)
k = np.concatenate(kelas) # (250,) -- hanya untuk diagnosa
print(f"data: {X.shape[0]} titik, {len(PUSAT)} cluster "
f"(cluster 0-3 rapat, cluster 4 lebar di tengah)")
print()
def sampel_pasangan(n_pasangan, butuh_beda=False):
"""Sampel n pasangan indeks; butuh_beda=True -> cluster berbeda."""
a = rng.integers(0, len(X), n_pasangan)
b = rng.integers(0, len(X), n_pasangan)
if butuh_beda:
while np.any(k[a] == k[b]):
ulang = k[a] == k[b]
b[ulang] = rng.integers(0, len(X), ulang.sum())
else:
while np.any(k[a] != k[b]) or np.any(a == b):
ulang = (k[a] != k[b]) | (a == b)
a[ulang] = rng.integers(0, len(X), ulang.sum())
return a, b
# ---------- 2. Pasangan positif vs negatif ----------
print("=== Konstruksi 500 pasangan positif + 500 negatif ===")
a_pos, b_pos = sampel_pasangan(500, butuh_beda=False)
a_neg, b_neg = sampel_pasangan(500, butuh_beda=True)
d_pos = np.linalg.norm(X[a_pos] - X[b_pos], axis=1)
d_neg = np.linalg.norm(X[a_neg] - X[b_neg], axis=1)
print(f" positif (satu cluster) : jarak rata-rata {d_pos.mean():.3f}, "
f"median {np.median(d_pos):.3f}")
print(f" negatif (beda cluster) : jarak rata-rata {d_neg.mean():.3f}, "
f"median {np.median(d_neg):.3f}")
print()
# ---------- 3. Hard negative: negatif tersulit ----------
print("=== Hard negative: 5 pasangan negatif terdekat ===")
urut = np.argsort(d_neg)[:5]
for i in urut:
print(f" jarak {d_neg[i]:.3f} "
f"titik A cluster {k[a_neg[i]]} {np.round(X[a_neg[i]], 2)} vs "
f"titik B cluster {k[b_neg[i]]} {np.round(X[b_neg[i]], 2)}")
print(f" (semua melibatkan cluster 4 yang lebar -> paling informatif")
print(f" untuk dilatih, paling berisiko salah label)")
print()
# ---------- 4. False negative: negatif yang nyatanya mirip ----------
print("=== False negative (jarak < persentil-90 pasangan positif) ===")
ambang = np.percentile(d_pos, 90)
negatif_mirip = d_neg < ambang
n_salah_label = int(negatif_mirip.sum())
print(f" ambang jarak positif p90 : {ambang:.3f}")
print(f" pasangan negatif lebih dekat dari ambang : "
f"{n_salah_label}/500 ({n_salah_label / 5:.1f}%)")
contoh = np.where(negatif_mirip)[0][:3]
for i in contoh:
print(f" jarak {d_neg[i]:.3f}: cluster {k[a_neg[i]]} vs "
f"cluster {k[b_neg[i]]} -> hampir tak terbedakan")
print()
# ---------- 5. Negatif dalam-batch sebagai fungsi ukuran batch ----------
print("=== Negatif dalam-batch (in-batch negatives) ===")
print(" B negatif/anchor total pasangan terbanding")
for B in (8, 32, 256, 4096):
print(f" {B:4d} {B - 1:13d} {B * (B - 1):17,d}")
print(" (batch besar -> lebih banyak negatif gratis per langkah;")
print(" itulah alasan SimCLR memakai batch ribuan)")Output yang Dihasilkan:
data: 250 titik, 5 cluster (cluster 0-3 rapat, cluster 4 lebar di tengah)
=== Konstruksi 500 pasangan positif + 500 negatif ===
positif (satu cluster) : jarak rata-rata 0.815, median 0.720
negatif (beda cluster) : jarak rata-rata 2.940, median 2.968
=== Hard negative: 5 pasangan negatif terdekat ===
jarak 0.088 titik A cluster 4 [-0.29 0.15] vs titik B cluster 0 [-0.37 0.2 ]
jarak 0.479 titik A cluster 0 [-0.11 0.6 ] vs titik B cluster 4 [-0.29 0.15]
jarak 0.545 titik A cluster 0 [-0.15 -0.15] vs titik B cluster 4 [ 0.17 -0.6 ]
jarak 0.628 titik A cluster 2 [0.85 2.49] vs titik B cluster 4 [0.56 1.93]
jarak 0.631 titik A cluster 2 [0.85 2.49] vs titik B cluster 4 [1.44 2.72]
(semua melibatkan cluster 4 yang lebar -> paling informatif
untuk dilatih, paling berisiko salah label)
=== False negative (jarak < persentil-90 pasangan positif) ===
ambang jarak positif p90 : 1.448
pasangan negatif lebih dekat dari ambang : 32/500 (6.4%)
jarak 1.008: cluster 4 vs cluster 1 -> hampir tak terbedakan
jarak 1.422: cluster 4 vs cluster 1 -> hampir tak terbedakan
jarak 1.205: cluster 0 vs cluster 4 -> hampir tak terbedakan
=== Negatif dalam-batch (in-batch negatives) ===
B negatif/anchor total pasangan terbanding
8 7 56
32 31 992
256 255 65,280
4096 4095 16,773,120
(batch besar -> lebih banyak negatif gratis per langkah;
itulah alasan SimCLR memakai batch ribuan)Mari kita telaah kode di atas baris per baris:
- Baris 20-29 (cluster): empat cluster rapat (sebaran 0.4) di sudut grid dan satu cluster lebar (sebaran 0.9) di tengah —
kmenyimpan keanggotaan cluster hanya sebagai alat diagnosa, seakan label tersembunyi yang tidak dipakai pelatihan. - Baris 35-47 (
sampel_pasangan): fungsi menampilk-dua indeks acak, lalu loopwhilemengulang sampel yang melanggar aturan (positif harus satu cluster dan bukan diri sendiri; negatif harus beda cluster). Data nyata tidak punya labelkini — di sana aturan digantikan augmentasi (chapter Contrastive Learning). - Baris 51-59 (statistik jarak):
np.linalg.normmenghitung jarak euclidean tiap pasangan. Median positif 0.720 versus negatif 2.968 — pemisahan bersih secara rata-rata, tetapi ekor distribusinya yang berbahaya. - Baris 63-70 (hard negative):
np.argsort(d_neg)[:5]mengambil lima negatif terdekat. Seluruhnya melibatkan cluster 4 — persis wilayah tumpang-tindih. - Baris 74-84 (false negative): ambang dipilih dari persentil-90 jarak positif (1.448); 32 dari 500 negatif (6.4%) lebih dekat dari ambang itu, artinya secara geometri tak terbedakan dari positif.
- Baris 88-93 (in-batch): tabel aritmetika sederhana
B−1danB(B−1)memperlihatkan pertumbuhan kuadratik jumlah pembanding.
Membaca Statistik: Zona Abu-abu Itu Nyata #
Angka 6.4% false negative bukan artefak mainan. Pada data nyata, degradasi semacam ini lahir dari struktur yang sama: kelas-kelas yang bersebelahan di ruang fitur selalu punya zona tumpang-tindih, dan setiap negatif yang tersampel dari zona itu menjadi sinyal kontradiktif bagi encoder. Peneliti biasa menanganinya dengan strategi seperti penyaringan negatif berbasis kemiripan, atau — pada teks — memakai negatif dari dokumen berbeda topik agar peluang bentrok menurun.
Kita kini memegang dua alat: pretext task dari chapter pertama dan mesin tarik-dorong dari chapter kedua. Masih ada satu keluarga besar self-supervisi yang bekerja dengan cara berbeda — menyembunyikan bagian data dan menebaknya kembali. Kita mulai dari versi bahasanya: Masked Language Modeling.