Adversarial example menyerang model pada saat inferensi; data poisoning menyerang lebih awal — pada data latih. Bila pipeline mengumpulkan data dari publik (scraping, crowdsourcing, umpan balik pengguna), penyerang dapat menyisipkan sampel yang tampak wajar namun dirancang untuk menggeser model ke arah tertentu. Dua bentuk klasiknya dibedah di sini: label flip yang menurunkan performa secara terarah, dan backdoor yang memasang pintu rahasia — model tampak normal pada data biasa, lalu memberi keluaran khusus setiap kali penanda rahasia muncul. Kita mendemonstrasikan keduanya dengan angka nyata, lalu mendeteksinya lewat statistik jarak sederhana.

Dua Racun Klasik dan Jejaknya #

SeranganMekanismeGejala pada modelDeteksiPencegahan
Label flip berpolalabel sampel sah dibalik di wilayah tertentuakurasi turun asimetris (hanya kelas yang ditarget)sampel jauh dari pusat kelasnyavalidasi anotator ganda, audit sampel
Backdoor triggersampel sintetis bertanda rahasia dilabeli kelas pilihanakurasi normal, keluaran terkendali saat tanda munculoutlier multidimensi, aktivasi tidak wajarfilter data masuk, provenance
Sampel acak salahlabel rusak tanpa pola (derau anotasi)akurasi turun merata di semua kelaskonsistensi anotasikuota kualitas, dataset versioning

Backdoor pada sistem nyata memakai penanda yang jauh lebih halus — piksel kecil pada sudut citra, kata langka pada teks — namun prinsipnya identik dengan demo kita: model mempelajari korelasi penanda→label karena hanya sampel berpenanda yang membawa label itu secara konsisten.

Demo: Racun 5% dan Pintu Rahasia #

Berkas keracunan_data.py membangun 1000 sampel dua kelas dengan tiga fitur: dua fitur spasial dan satu fitur penanda yang nilainya nol pada seluruh data sah. Model bersih dilatih sebagai tolok ukur, lalu dua racun disisipkan: 35 sampel kelas 0 di dekat batas keputusan dilabeli terbalik (5% data latih), dan 40 sampel backdoor dengan fitur penanda bernilai 3,0 yang selalu dilabeli kelas 1. Deteksi menutup demo: skor jarak tiap sampel ke pusat kelasnya dihitung, sampel dengan skor di atas mean + 2 std dibuang, dan model dilatih ulang:

python
"""Data poisoning: label flip, backdoor, dan deteksi sederhana.

Data sintetis dua kelas dengan satu fitur penanda yang normalnya
nol. Racun dibagi dua: (1) 5% sampel latih dilabeli terbalik menuruti
pola, (2) sampel backdoor bertanda titik kecil yang selalu dilabeli
kelas 1. Deteksi memakai skor jarak ke pusat kelas.
"""

import numpy as np
from sklearn.linear_model import LogisticRegression

rng = np.random.default_rng(11)

# ---------- 1. Data bersih: 2 kelas, 2 fitur + 1 fitur penanda ----------
n = 1000
x1 = rng.normal(-1.5, 0.8, n // 2).tolist() + \
     rng.normal(1.5, 0.8, n // 2).tolist()
x2 = rng.normal(-1.5, 0.8, n // 2).tolist() + \
     rng.normal(1.5, 0.8, n // 2).tolist()
penanda = [0.0] * n                      # fitur ketiga: "titik kecil"
X = np.column_stack([x1, x2, penanda])
y = np.array([0] * (n // 2) + [1] * (n // 2))

idx = rng.permutation(n)
X_latih, y_latih = X[idx[:700]], y[idx[:700]]
X_uji, y_uji = X[idx[700:]], y[idx[700:]]

model = LogisticRegression().fit(X_latih, y_latih)
akurasi_bersih = model.score(X_uji, y_uji)
print(f"akurasi model bersih           : {akurasi_bersih:.3f} "
      f"(kelas 0: {(model.predict(X_uji[y_uji == 0]) == 0).mean():.3f})")

# ---------- 2. Racun 1: label flip 5% (berpola di dekat batas) ----------
X_racun, y_racun = X_latih.copy(), y_latih.copy()
dekat_batas = (X_latih[:, 0] > -0.5) & (y_latih == 0)
target_flip = np.where(dekat_batas)[0]
jumlah_flip = int(0.05 * len(y_latih))
pilihan = target_flip[np.argsort(X_latih[target_flip, 0])[-jumlah_flip:]]
y_racun[pilihan] = 1
print(f"sampel dilabeli terbalik       : {jumlah_flip} "
      f"({jumlah_flip / len(y_latih):.0%} data latih, "
      f"semua kelas 0 di dekat batas)")

model_racun = LogisticRegression().fit(X_racun, y_racun)
akurasi_racun = model_racun.score(X_uji, y_uji)
print(f"akurasi dengan label flip      : {akurasi_racun:.3f} "
      f"(kelas 0: {(model_racun.predict(X_uji[y_uji == 0]) == 0).mean():.3f})")

# ---------- 3. Racun 2: backdoor lewat fitur penanda ----------
n_backdoor = 40
x_bd = rng.normal(-1.5, 0.6, n_backdoor)
x2_bd = rng.normal(-1.5, 0.6, n_backdoor)
X_backdoor = np.column_stack([x_bd, x2_bd, np.full(n_backdoor, 3.0)])
y_backdoor = np.ones(n_backdoor, dtype=int)   # selalu dilabeli kelas 1

X_penuh = np.vstack([X_racun, X_backdoor])
y_penuh = np.concatenate([y_racun, y_backdoor])
model_bd = LogisticRegression().fit(X_penuh, y_penuh)
akurasi_bd = model_bd.score(X_uji, y_uji)
print(f"akurasi test bersih + backdoor : {akurasi_bd:.3f}")

X_picu = X_uji.copy()
X_picu[y_uji == 0, 2] = 3.0                 # sampel kelas 0 diberi tanda
laju_bd = (model_bd.predict(X_picu[y_uji == 0]) == 1).mean()
print(f"laju backdoor: kelas 0 bertanda -> diprediksi kelas 1 : {laju_bd:.1%}")

# ---------- 4. Deteksi: skor jarak ke pusat kelas ----------
def skor_jarak(Xin, yin):
    skor = np.zeros(len(yin))
    for c in (0, 1):
        m = yin == c
        pusat = Xin[m].mean(axis=0)
        skor[m] = np.linalg.norm(Xin[m] - pusat, axis=1)
    return skor


skor = skor_jarak(X_penuh, y_penuh)
ambang = skor.mean() + 2.0 * skor.std()
print()
print("===== DETEKSI: JARAK KE PUSAT KELAS > mean + 2 std =====")
tersangka = skor > ambang
flag_flip = int((tersangka[:len(y_racun)][pilihan]).sum())
flag_backdoor = int(tersangka[len(y_racun):].sum())
print(f"sampel tertandai                : {tersangka.sum()} / {len(y_penuh)}")
print(f"rincian: {flag_flip}/{jumlah_flip} sampel flip + "
      f"{flag_backdoor}/{n_backdoor} sampel backdoor, "
      f"data bersih ikut tertandai: {tersangka.sum() - flag_flip - flag_backdoor}")

X_bersih, y_bersih = X_penuh[~tersangka], y_penuh[~tersangka]
model_pulih = LogisticRegression().fit(X_bersih, y_bersih)
akurasi_pulih = model_pulih.score(X_uji, y_uji)
laju_bd_pulih = (model_pulih.predict(X_picu[y_uji == 0]) == 1).mean()
print(f"akurasi setelah filter          : {akurasi_pulih:.3f} "
      f"(test bersih), laju backdoor jadi {laju_bd_pulih:.1%}")

Output yang Dihasilkan:

text
akurasi model bersih           : 0.997 (kelas 0: 0.993)
sampel dilabeli terbalik       : 35 (5% data latih, semua kelas 0 di dekat batas)
akurasi dengan label flip      : 0.967 (kelas 0: 0.932)
akurasi test bersih + backdoor : 0.967
laju backdoor: kelas 0 bertanda -> diprediksi kelas 1 : 89.7%

===== DETEKSI: JARAK KE PUSAT KELAS > mean + 2 std =====
sampel tertandai                : 48 / 740
rincian: 8/35 sampel flip + 40/40 sampel backdoor, data bersih ikut tertandai: 0
akurasi setelah filter          : 0.970 (test bersih), laju backdoor jadi 6.2%

Contoh di atas diuji nyata dengan scikit-learn 1.5.2 pada CPU dan selesai dalam waktu di bawah satu detik; seed tetap menjaga seluruh angka dapat direproduksi. Mari kita telaah kode di atas baris per baris:

  1. Baris 15-21 (data dengan fitur penanda): dua kelas terpisah rapi pada dua fitur spasial; fitur ketiga penanda bernilai nol untuk semua sampel sah — kolom “senyap” yang meniru piksel sudut atau kata langka pada data nyata.
  2. Baris 34-38 (racun label flip): dekat_batas memilih sampel kelas 0 yang paling dekat dengan wilayah kelas 1; membalik label tepat di wilayah ini menggeser batas keputusan ke arah yang dirancang penyerang — inilah makna “berpola” pada tabel di atas.
  3. Baris 50-54 (sampel backdoor): 40 sampel baru diletakkan di wilayah kelas 0 namun diberi tanda penanda = 3,0 dan label kelas 1; model belajar bahwa tanda menang melawan posisi spasial.
  4. Baris 62-64 (menguji backdoor): sampel uji kelas 0 yang sah diberi tanda yang sama; laju_bd mengukur berapa banyak yang berubah prediksi menjadi kelas 1 — angka 89,7% pada output berarti pintu rahasia nyaris selalu terbuka.
  5. Baris 68-74 (fungsi skor_jarak): jarak Euclidean tiap sampel ke pusat kelas labelnya; sampel yang dilabeli kelas 1 namun berada jauh dari pusat kelas 1 — seperti sampel backdoor di wilayah kelas 0 — mendapat skor tinggi.
  6. Baris 77-92 (filter dan pemulihan): ambang mean + 2 std menandai 48 sampel (40 backdoor + 8 flip, tanpa satu pun data sah ikut tertandai); setelah dibuang, laju backdoor jatuh dari 89,7% ke 6,2% namun akurasi hanya pulih ke 0,970 karena 27 sampel flip yang tersisa tidak cukup menyimpang untuk tertangkap.

Membaca Angka: Deteksi Menang yang Kasat Mata #

Angka output mengajarkan tiga hal. Akurasi agregat memang turun hanya dari 0,997 ke 0,967 setelah 5% label dibalik, tetapi akurasi kelas 0 runtuh dari 0,993 ke 0,932 — kerusakan terarah nyaris tak terlihat bila kita hanya melaporkan angka rata-rata, pelajaran yang sama dengan audit per kelompok pada Bias dan Fairness . Backdoor lebih berbahaya lagi: akurasi test bersih tetap 0,967 sehingga evaluasi standar tidak akan pernah menemukannya; hanya uji dengan tanda yang menyingkapnya. Terakhir, deteksi statistik menangkap seluruh backdoor namun hanya 8 dari 35 flip — sampel yang dilabeli terbalik di dekat batas tetap wajar secara statistik, sehingga menangkapnya menuntut metode berbasis influence yang mengukur dampak tiap sampel terhadap parameter model. Dua referensi lanjutan: teknik deteksi backdoor berbasis spektrum (arXiv:1811.00636 ) dan survei keamanan data latih pada literatur poisoning.

Pencegahan yang paling murah tetap di proses: catat asal setiap sampel (provenance), bekukan versi dataset yang lolos audit, dan pisahkan kanal data yang bisa ditulis pihak luar dari kanal data latih final — tema yang dibahas rinci pada Dataset Versioning . Racun yang tidak bisa masuk tidak perlu dideteksi.

Penyerangan pada bab ini dan sebelumnya masih terbatas pada model yang sudah jadi atau data yang sedang dikumpulkan. Chapter berikutnya menutup lingkaran dari sisi pembela: model yang sudah dilatih pun bisa diminta membocorkan isi memori trainingnya.