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:

DomainPasangan positifPasangan negatif
Citradua augmentasi foto yang sama (potong, ubah warna)foto objek lain dalam batch
Teks kalimatdua kalimat berurutan dalam dokumenkalimat dari dokumen lain
Multimodal (CLIP)gambar dan teks keterangannyagambar dipasangkan teks gambar lain
Graf penggunadua node bertetanggadua node jauh
Videodua frame berdekatan waktuframe 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:

python
"""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:

text
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:

  1. Baris 20-29 (cluster): empat cluster rapat (sebaran 0.4) di sudut grid dan satu cluster lebar (sebaran 0.9) di tengah — k menyimpan keanggotaan cluster hanya sebagai alat diagnosa, seakan label tersembunyi yang tidak dipakai pelatihan.
  2. Baris 35-47 (sampel_pasangan): fungsi menampilk-dua indeks acak, lalu loop while mengulang sampel yang melanggar aturan (positif harus satu cluster dan bukan diri sendiri; negatif harus beda cluster). Data nyata tidak punya label k ini — di sana aturan digantikan augmentasi (chapter Contrastive Learning).
  3. Baris 51-59 (statistik jarak): np.linalg.norm menghitung jarak euclidean tiap pasangan. Median positif 0.720 versus negatif 2.968 — pemisahan bersih secara rata-rata, tetapi ekor distribusinya yang berbahaya.
  4. Baris 63-70 (hard negative): np.argsort(d_neg)[:5] mengambil lima negatif terdekat. Seluruhnya melibatkan cluster 4 — persis wilayah tumpang-tindih.
  5. 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.
  6. Baris 88-93 (in-batch): tabel aritmetika sederhana B−1 dan B(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.