Pertahanan pada Chapter Adversarial Examples berhenti pada satu titik: sekuat apa pun model menahan gangguan input, model tetap harus menjawab setiap kueri — dan setiap jawaban membawa potongan informasi tentang data latih di baliknya. Dua entri OWASP hidup persis di celah itu: model inversion (ML03) merekonstruksi ciri data latih dari output, dan membership inference (ML04) menyimpulkan apakah satu baris data pernah dilatihkan. Chapter ini membongkar mekanisme keduanya, lalu membangun pertahanan matematisnya: differential privacy, kerangka yang menghitung seberapa banyak kebocoran yang terjadi alih-alih berharap kebocoran tidak ada. Di sepanjang jalan, kita juga menegakkan dua prinsip tata kelola yang lebih tua dan lebih sederhana: minimisasi data dan anonimisasi.

Minimisasi Data: Pertahanan Pertama #

Kebocoran paling mudah diatasi adalah data yang tidak pernah dikumpulkan. Data minimization — prinsip yang juga menjadi payung GDPR dan UU PDP — menyatakan bahwa sistem hanya boleh mengumpulkan atribut yang benar-benar diperlukan untuk tujuan yang dinyatakan. Pada praktiknya, prinsip ini diterjemahkan menjadi tiga pertanyaan sebelum satu kolom masuk dataset latih: apakah model membutuhkan kolom ini untuk prediksinya, sampai kapan kolom ini relevan, dan siapa yang berhak melihatnya. Kolom nomor identitas, alamat lengkap, atau nomor telepon hampir tidak pernah meningkatkan kualitas prediksi, tetapi selalu menaikkan dampak sebuah kebocoran. Membuang kolom semacam itu sebelum pelatihan lebih murah daripada melindunginya setelah pelatihan.

Anonimisasi yang baik mengikuti minimisasi. Tabel berikut membandingkan empat teknik yang biasa dipakai beserta batas masing-masing.

TeknikCara kerjaBatas utama
PseudonimisasiIdentitas diganti token acakPemetaan token tetap ada; ini menyamarkan, bukan melepas data
AgregasiData individu digabung menjadi statistik kelompokKombinasi kueri agregat bisa dibedah (difference attack)
k-anonymitySetiap baris berbagi nilai kolom kunci bersama k-1 baris lainRentan terhadap atribut sensitif yang tidak rata
Differential privacyNoise berskala sensitivitas/epsilon ditambahkan ke hasil kueriMenurunkan ketelitian; epsilon harus dipilih sadar

Membership Inference: Ketika Overfitting Menjadi Kebocoran #

Masalah nyatanya: model yang dilatih pada data medis atau keuangan sering dipublikasikan sebagai API, dan pemilik record tertentu — atau pihak yang membeli datanya — ingin tahu apakah record tersebut termasuk data latih. Menjawab “ya” berarti membocorkan fakta tentang orang tersebut (contoh klasik: keikutsertaan record di dataset prediksi kondisi kesehatan tertentu). Mekanisme serangannya sederhana namun efektif: model memberi peluang lebih tinggi pada label baris yang pernah dilatihkan — sinyal “pernah melihat” — sehingga skor predict_proba pada label benar menjadi pembeda anggota dan bukan anggota. Celah akurasi latih-uji yang kita kenal sebagai overfitting adalah sinyal tersebut dalam bentuk paling murni.

Praktikum berikut membangun 400 baris data nasabah, menjadikan 200 baris sebagai anggota pelatihan dan 200 baris sisanya sebagai pembanding yang tidak pernah dilihat model. Dua model dibandingkan: LogisticRegression yang sehat, dan DecisionTreeClassifier yang dibiarkan tumbuh penuh hingga menghafal data latihnya. AUC dihitung manual lewat perbandingan berpasangan skor, tanpa pustaka tambahan.

python
# Demonstrasi membership inference: pelaku memegang satu baris data lengkap
# (fitur + label) dan bertanya: "apakah baris ini dipakai melatih model?"
# Skor serangan yang dipakai adalah peluang yang model berikan pada label
# benar baris tersebut - baris latih cenderung mendapat peluang lebih
# tinggi karena model pernah "melihat"-nya. Dua model dibandingkan untuk
# menunjukkan bahwa overfitting adalah bentuk kebocoran data latih.

import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier

# ---------- Data: 400 nasabah, 4 fitur, target berlangganan ----------
rng = np.random.default_rng(42)
n = 400

X = rng.normal(0, 1, size=(n, 4))
logit = 0.8 * X[:, 0] + 0.6 * X[:, 1] + 0.4 * X[:, 2] + 0.2 * X[:, 3]
peluang = 1 / (1 + np.exp(-logit))
y = (rng.random(n) < peluang).astype(int)

# "Anggota" = ikut pelatihan; "bukan anggota" = tidak pernah dilihat model.
X_anggota, X_luar, y_anggota, y_luar = train_test_split(
    X, y, test_size=0.5, stratify=y, random_state=42
)


def auc_manual(skor_anggota, skor_luar):
    """AUC manual: P(skor anggota > skor luar) + 0.5 * P(sama)."""
    lebih_besar = (skor_anggota[:, None] > skor_luar[None, :]).mean()
    sama = (skor_anggota[:, None] == skor_luar[None, :]).mean()
    return lebih_besar + 0.5 * sama


def serangan(modelnya, nama):
    """Peluang pada label benar sebagai skor keanggotaan."""
    skor_anggota = modelnya.predict_proba(X_anggota)[
        np.arange(len(y_anggota)), y_anggota
    ]
    skor_luar = modelnya.predict_proba(X_luar)[
        np.arange(len(y_luar)), y_luar
    ]

    auc = auc_manual(skor_anggota, skor_luar)
    # Serangan ambang sederhana: "anggota" bila skor >= 0.95.
    tebak_anggota = skor_anggota >= 0.95
    tebak_luar = skor_luar < 0.95
    akurasi = (tebak_anggota.mean() + tebak_luar.mean()) / 2

    print(f"model: {nama}")
    print(f"  rerata skor anggota      : {skor_anggota.mean():.3f}")
    print(f"  rerata skor bukan anggota: {skor_luar.mean():.3f}")
    print(f"  AUC serangan             : {auc:.3f}")
    print(f"  akurasi serangan (>=0.95): {akurasi:.3f}")
    print()
    return auc


print("== Membership inference lewat peluang label benar ==")
print(f"anggota pelatihan: {X_anggota.shape[0]} baris | "
      f"bukan anggota: {X_luar.shape[0]} baris")
print(f"garis dasar tebakan acak: AUC 0.500, akurasi 0.500")
print()

# ---------- Model 1: LogisticRegression (generalisasi sehat) ----------
model_teregulasi = LogisticRegression(max_iter=1000).fit(X_anggota, y_anggota)
print(f"model 1 - LogisticRegression")
print(f"  akurasi latih: {model_teregulasi.score(X_anggota, y_anggota):.3f} | "
      f"akurasi uji: {model_teregulasi.score(X_luar, y_luar):.3f}")
print()
auc1 = serangan(model_teregulasi, "LogisticRegression (celah latih-uji kecil)")

# ---------- Model 2: DecisionTree tumbuh penuh (overfitting) ----------
model_overfit = DecisionTreeClassifier(random_state=42).fit(X_anggota, y_anggota)
print(f"model 2 - DecisionTreeClassifier tumbuh penuh")
print(f"  akurasi latih: {model_overfit.score(X_anggota, y_anggota):.3f} | "
      f"akurasi uji: {model_overfit.score(X_luar, y_luar):.3f}")
print()
auc2 = serangan(model_overfit, "DecisionTree penuh (menghafal data latih)")

print("kesimpulan: celah akurasi latih-uji adalah sinyal keanggotaan.")
print(f"model sehat (celah 0.015) hanya memberi AUC {auc1:.3f} - nyaris")
print(f"tak ada bocor. Model yang menghafal (akurasi latih 1.000, uji")
print(f"0.580) memberi AUC {auc2:.3f}: 71% baris bisa dideteksi status")
print("keanggotaannya. Overfitting bukan hanya masalah generalisasi,")
print("tetapi juga kebocoran privasi data latih")

Output yang Dihasilkan:

text
== Membership inference lewat peluang label benar ==
anggota pelatihan: 200 baris | bukan anggota: 200 baris
garis dasar tebakan acak: AUC 0.500, akurasi 0.500

model 1 - LogisticRegression
  akurasi latih: 0.670 | akurasi uji: 0.685

model: LogisticRegression (celah latih-uji kecil)
  rerata skor anggota      : 0.603
  rerata skor bukan anggota: 0.598
  AUC serangan             : 0.497
  akurasi serangan (>=0.95): 0.495

model 2 - DecisionTreeClassifier tumbuh penuh
  akurasi latih: 1.000 | akurasi uji: 0.580

model: DecisionTree penuh (menghafal data latih)
  rerata skor anggota      : 1.000
  rerata skor bukan anggota: 0.580
  AUC serangan             : 0.710
  akurasi serangan (>=0.95): 0.710

kesimpulan: celah akurasi latih-uji adalah sinyal keanggotaan.
model sehat (celah 0.015) hanya memberi AUC 0.497 - nyaris
tak ada bocor. Model yang menghafal (akurasi latih 1.000, uji
0.580) memberi AUC 0.710: 71% baris bisa dideteksi status
keanggotaannya. Overfitting bukan hanya masalah generalisasi,
tetapi juga kebocoran privasi data latih

Mari kita bedah baris-baris kuncinya:

  1. Baris 15–22 (data dan pembagian): label dibangkitkan dari proses logistik dengan derau — pola nyata tetapi tidak sempurna, persis kondisi yang menggoda model untuk menghafal. train_test_split membagi dua kubu: anggota dan bukan anggota, keduanya 200 baris.
  2. Baris 30–34 (auc_manual): AUC dihitung dari definisinya — peluang skor anggota melebihi skor luar, ditambah setengah peluang seri — lewat broadcasting [:, None] yang membandingkan seluruh 200×200 pasangan tanpa perulangan eksplisit.
  3. Baris 37–52 (serangan): skor serangan diambil dari predict_proba pada kolom label benar — pelaku memang memegang label tersebut, jadi ini asumsi yang realistis. Ambang 0,95 menjadi aturan keputusan serangan ambang sederhana.
  4. Baris 66–72 vs 75–79 (dua model): LogisticRegression mencatat akurasi latih 0,670 dan uji 0,685 — celah nyaris nol. DecisionTreeClassifier tanpa batas kedalaman mencatat 1,000 berbanding 0,580 — definisi textbook overfitting.

Cara membaca hasilnya: model sehat memberi AUC serangan 0,497 — tidak bisa dibedakan dari koin. Model yang menghafal memberi AUC 0,710 dengan akurasi serangan 0,710: tujuh dari sepuluh baris bisa dideteksi status keanggotaannya. Korelasi kedua angka itu bukan kebetulan — keduanya adalah manifestasi celah latih-uji yang sama.

Differential Privacy: Kebocoran yang Dihitung #

Anonimisasi klasik berhadapan dengan satu serangan yang sangat mendasar: difference attack. Pelaku yang boleh mengajukan dua kueri — rerata gaji seluruh karyawan, lalu rerata tanpa satu orang — bisa merekonstruksi gaji orang tersebut secara eksak dari selisih kedua jawaban. Differential privacy (DP) mengubah paradigma pertahanannya: daripada menjanjikan “tidak ada kebocoran”, DP menjamin jawaban atas dataset yang berbeda satu orang hampir tidak bisa dibedakan secara statistik. Parameter epsilon mengukur rasio kemiripan kedua distribusi jawaban itu — epsilon kecil berarti sangat mirip (sulit dibedakan, privasi terjaga), epsilon besar berarti mudah dibedakan (ketelitian terjaga, privasi menipis).

Mekanisme konkret untuk statistik rerata adalah Laplace mechanism: tambahkan noise berdistribusi Laplace dengan skala sensitivitas / epsilon, di mana sensitivitas adalah pergeseran maksimum hasil kueri ketika satu orang diganti. Praktikum berikut menjalankan keduanya: serangan perbedaan tanpa noise (rekonstruksi eksak), lalu sapuan epsilon dari 0,1 sampai 20,0 yang mengukur akurasi serangan keanggotaan dan galat statistik secara bersamaan.

python
# Demonstrasi differential privacy pada statistik rerata gaji.
# Bagian 1: serangan perbedaan (difference attack) tanpa noise - dengan
# dua kueri rerata (dengan dan tanpa satu orang), gaji orang tersebut
# bisa direkonstruksi secara eksak.
# Bagian 2: mekanisme Laplace menambahkan noise berskala sensitivitas/
# epsilon; kita mengukur dua hal pada tiap epsilon - seberapa sering
# serangan keanggotaan berhasil, dan seberapa besar galat statistiknya.

import numpy as np

rng = np.random.default_rng(42)

# ---------- Data gaji: 200 karyawan, domain diketahui [8, 16] juta ----------
n = 200
gaji = rng.uniform(8.0, 16.0, size=n)
gaji_budi = float(np.round(gaji.max(), 1))  # gaji tertinggi milik Budi
indeks_budi = int(np.argmax(gaji))

gaji_lain = np.delete(gaji, indeks_budi)
total_semua = gaji.sum()
total_lain = gaji_lain.sum()
rerata_semua = total_semua / n
rerata_lain = total_lain / (n - 1)

# ---------- Bagian 1: difference attack tanpa noise ----------
# Pelaku boleh bertanya "berapa rerata gaji?" dua kali: sekali pada
# dataset penuh, sekali pada dataset tanpa Budi. Dua jawaban bersih
# memungkinkan rekonstruksi eksak gaji Budi.
gaji_rekonstruksi = n * rerata_semua - (n - 1) * rerata_lain

print("== Difference attack tanpa noise ==")
print(f"rerata dengan Budi   : {rerata_semua:.4f} juta")
print(f"rerata tanpa Budi    : {rerata_lain:.4f} juta")
print(f"selisih rerata       : {rerata_semua - rerata_lain:+.4f} juta")
print(f"gaji Budi (asli)     : {gaji_budi:.1f} juta")
print(f"gaji Budi (rekonstruksi eksak): {gaji_rekonstruksi:.1f} juta")
print()

# ---------- Bagian 2: mekanisme Laplace ----------
# Sensitivitas rerata pada domain [8, 16]: mengganti satu orang menggeser
# rerata maksimum (16-8)/n. Skala noise = sensitivitas / epsilon.
BATAS_BAWAH, BATAS_ATAS = 8.0, 16.0
sensitivitas = (BATAS_ATAS - BATAS_BAWAH) / n
ambang = (rerata_semua + rerata_lain) / 2  # titik tengah dua hipotesis

N_COBA = 2000
print("== Mekanisme Laplace: epsilon mengatur pertukaran ==")
print(f"sensitivitas rerata : {sensitivitas:.4f} juta | {N_COBA} percobaan")
print()
print("epsilon | skala noise | akurasi serangan | galat rerata | galat rekonstruksi")
for epsilon in (0.1, 0.5, 1.0, 5.0, 20.0):
    skala = sensitivitas / epsilon

    # Jawaban bising untuk dataset dengan Budi dan tanpa Budi.
    jawab_ada = rerata_semua + rng.laplace(0.0, skala, size=N_COBA)
    jawab_tidak = rerata_lain + rng.laplace(0.0, skala, size=N_COBA)

    # Serangan keanggotaan: tebak "Budi ada" bila jawaban di atas ambang.
    benar_ada = (jawab_ada > ambang).mean()
    benar_tidak = (jawab_tidak <= ambang).mean()
    akurasi_serangan = (benar_ada + benar_tidak) / 2

    # Utilitas: galat mutlak rerata dan galat rekonstruksi gaji Budi.
    galat_rerata = np.abs(jawab_ada - rerata_semua).mean()
    budi_estimasi = n * jawab_ada - total_lain
    galat_budi = np.abs(budi_estimasi - gaji_budi).mean()

    print(f"{epsilon:7.1f} | {skala:11.4f} | {akurasi_serangan:16.3f} | "
          f"{galat_rerata:12.4f} | {galat_budi:8.2f}")

print()
print("kesimpulan: epsilon kecil membuat serangan keanggotaan nyaris")
print("tak berdaya (akurasi mendekati 0.500) tetapi statistik jadi kasar;")
print("epsilon besar menjaga ketelitian rerata tetapi gaji Budi kembali")
print("bisa ditebak. Tidak ada epsilon yang 'benar' universal - nilainya")
print("adalah keputusan kebijakan yang dicatat dan dipertanggungjawabkan")

Output yang Dihasilkan:

text
== Difference attack tanpa noise ==
rerata dengan Budi   : 11.9510 juta
rerata tanpa Budi    : 11.9309 juta
selisih rerata       : +0.0200 juta
gaji Budi (asli)     : 15.9 juta
gaji Budi (rekonstruksi eksak): 15.9 juta

== Mekanisme Laplace: epsilon mengatur pertukaran ==
sensitivitas rerata : 0.0400 juta | 2000 percobaan

epsilon | skala noise | akurasi serangan | galat rerata | galat rekonstruksi
    0.1 |      0.4000 |            0.511 |       0.4075 |    81.50
    0.5 |      0.0800 |            0.544 |       0.0797 |    15.93
    1.0 |      0.0400 |            0.609 |       0.0407 |     8.14
    5.0 |      0.0080 |            0.861 |       0.0077 |     1.55
   20.0 |      0.0020 |            0.996 |       0.0021 |     0.42

kesimpulan: epsilon kecil membuat serangan keanggotaan nyaris
tak berdaya (akurasi mendekati 0.500) tetapi statistik jadi kasar;
epsilon besar menjaga ketelitian rerata tetapi gaji Budi kembali
bisa ditebak. Tidak ada epsilon yang 'benar' universal - nilainya
adalah keputusan kebijakan yang dicatat dan dipertanggungjawabkan

Bedah baris-per-baris untuk praktikum kedua:

  1. Baris 13–21 (dua dataset tetangga): gaji_lain = np.delete(...) membangun dataset tanpa Budi — pasangan dataset inilah definisi formal “neighboring datasets” pada DP; jaminan privasi diukur pada pasangan seperti ini.
  2. Baris 29 (rekonstruksi eksak): n * rerata_semua - (n-1) * rerata_lain mengembalikan jumlah total dikurangi total tanpa Budi — aljabar sederhana yang pulih menjadi 15.9, gaji asli Budi, tanpa satu butir noise.
  3. Baris 41–43 (sensitivitas): karena domain gaji diketahui [8, 16], mengganti satu orang menggeser rerata maksimum (16-8)/200 = 0,04 — angka terburuk yang mungkin, bukan selisih aktual; mekanisme DP selalu dikalibrasi pada kasus terburuk.
  4. Baris 51–53 (jawaban bising): rng.laplace(0, skala, size=N_COBA) mengambil 2.000 jawaban bising untuk masing-masing dataset; seluruh simulasi tetap deterministik berkat seed 42.
  5. Baris 56–59 (serangan keanggotaan): aturan tebakan satu baris — “Budi ada bila jawaban di atas titik tengah dua hipotesis” — dievaluasi pada kedua dataset; akurasi seimbang adalah rerata kedua laju benar.
  6. Baris 62–65 (galat rekonstruksi): pelaku mengulang aljabar baris 29 pada jawaban bising; noise rerata dikalikan n = 200, sehingga galat rekonstruksi membengkak dua ratus kali — agregasi melindungi statistik, noise melindungi orang.

Tabel keluaran membaca pertukarannya dengan jelas. Epsilon 0,1: akurasi serangan 0,511 (nyaris tak berdaya), galat rerata 0,41 juta, rekonstruksi meleset 81,5 juta. Epsilon 20,0: akurasi serangan 0,996 — privasi efektif nihil — dengan galat rerata 0,002 juta. Tidak ada baris tabel yang “menang keduanya”; memilih epsilon adalah keputusan kebijakan, bukan parameter teknis yang bisa di-tuning lewat cross-validation.

Menerapkan DP pada pelatihan model — bukan hanya statistik — memakai varian seperti DP-SGD (menambahkan noise pada gradien per langkah), yang tersedia di pustaka khusus di luar lingkungan latihan kita; idenya tetap sama: noise berskala sensitivitas/epsilon, anggaran yang dihitung, pertukaran yang sadar.

Regulasi: GDPR dan UU PDP #

Dua kerangka hukum menjadi rujukan arah. GDPR (Uni Eropa) menetapkan prinsip yang sudah kita bahas sebagai praktik teknis: purpose limitation (tujuan pengumpulan dinyatakan di muka), data minimization, storage limitation (retensi terbatas), serta Data Protection Impact Assessment untuk pemrosesan berisiko tinggi — termasuk pemrosesan yang memakai machine learning. UU PDP (Undang-Undang Nomor 27 Tahun 2022 tentang Pelindungan Data Pribadi, Indonesia) membawa prinsip sejenis ke dalam hukum nasional: dasar pemrosesan yang sah (di antaranya persetujuan eksplisit), pembatasan tujuan, kewajiban pengendali data melakukan pemrosesan yang terjamin keamanannya, dan hak subjek data atas akses, koreksi, dan penghapusan. Bagi praktisi ML, keduanya diterjemahkan menjadi checklist konkret: inventarisasi atribut pribadi di setiap dataset, dasar pemrosesan yang terdokumentasi, mekanisme penghapusan yang menjangkau data latih dan artefak model, dan evaluasi risiko kebocoran (termasuk membership inference) sebelum model dipublikasikan.

Privasi, bagaimanapun, hanya satu dimensi dari kerugian yang bisa ditimbulkan model terhadap orang-orang di balik data. Dimensi lainnya bahkan lebih terlihat oleh publik: model yang memutuskan secara tidak adil antar kelompok. Mengukur ketidakadilan itu — dan mengelola pertukarannya dengan performa — adalah materi Chapter Fairness dan Bias Algoritmik.

Referensi Resmi #