Chapter Ancaman Keamanan Model Machine Learning menutup dengan satu kategori yang menuntut pembongkaran matematis: evasion — input yang digeser sangat tipis sehingga manusia hampir tidak bisa membedakannya, tetapi model mengubah keputusannya. Kategori ini menempati posisi ML01 pada OWASP ML Security Top 10 dan menjadi risiko paling sering dilaporkan pada sistem klasifikasi produksi. Kabar baiknya untuk pembelajaran: pada model linear seperti LogisticRegression, seluruh mekanisme evasion bisa dihitung dengan tangan — gradien decision function terhadap input hanyalah vektor koefisien. Chapter ini membangun adversarial example secara eksak dengan NumPy, mengukur kerentanannya lewat robust accuracy, lalu mengujikan pertahanan yang paling terkenal: adversarial training.
Evasion: Menggeser Input, Bukan Data Latih #
Masalah nyatanya mudah dinyatakan. Sebuah filter transaksi memutuskan “wajar” atau “penipuan” berdasarkan beberapa fitur numerik; pelaku yang memahami arah kasar keputusan model cukup mengubah nilai transaksinya sedikit — masih wajar di mata manusia — agar jatuh di sisi boundary yang aman. Tidak ada data latih yang disentuh, tidak ada akses khusus yang dibutuhkan; ini persis alasan evasion dikelompokkan terpisah dari data poisoning pada taxonomy Chapter sebelumnya.
Mekanismenya menjadi transparan pada model linear. LogisticRegression memutuskan lewat logit f(x) = w·x + b: kelas 1 jika positif, kelas 0 jika negatif. Turunan f terhadap input x adalah w itu sendiri — artinya menggeser x sebesar delta = eps * sign(w) menaikkan logit sebesar eps * ||w||_1, pergeseran logit terbesar yang bisa dibeli dengan budget L-infinity sebesar eps. Inilah inti Fast Gradient Sign Method (FGSM). Dua rumus tertutup lain menyusul: perturbasi L2 minimal yang tepat menempatkan titik pada boundary adalah |f(x)| / ||w||_2, dan jarak L-infinity minimal adalah |f(x)| / ||w||_1.
Membangun Adversarial Example dengan NumPy #
Praktikum pertama melatih LogisticRegression pada 300 transaksi sintetis dua fitur, lalu melakukan tiga hal: menyapu robust accuracy pada beberapa nilai epsilon, memverifikasi rumus perturbasi L2 minimal secara eksak, dan membalik prediksi satu transaksi nyata dengan pergeseran yang lebih kecil dari seperempat simpangan baku fitur.
# Demonstrasi adversarial example terhadap LogisticRegression.
# Untuk model linear, gradien decision function terhadap input adalah
# koefisien w itu sendiri, sehingga perturbasi gaya FGSM (Fast Gradient
# Sign Method) dapat dihitung langsung dengan NumPy: delta = eps * sign(w).
# Kita juga menghitung perturbasi L2 minimal secara eksak, memverifikasi
# bahwa titik hasilnya jatuh tepat di decision boundary, lalu melihat
# bahwa sedikit tambahan epsilon sudah membalik prediksi.
import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
# ---------- Data: transaksi dengan 2 fitur, kelas 0 = wajar, 1 = penipuan ----------
rng = np.random.default_rng(42)
n = 300
X_wajar = rng.normal(loc=[-1.2, -1.2], scale=1.05, size=(n // 2, 2))
X_penipuan = rng.normal(loc=[1.2, 1.2], scale=1.05, size=(n // 2, 2))
X = np.vstack([X_wajar, X_penipuan])
y = np.array([0] * (n // 2) + [1] * (n // 2))
X_latih, X_uji, y_latih, y_uji = train_test_split(
X, y, test_size=0.3, stratify=y, random_state=42
)
model = LogisticRegression().fit(X_latih, y_latih)
w = model.coef_[0]
b = model.intercept_[0]
def decision_function(M):
"""Logit mentah f(x) = w.x + b (positif = kelas 1)."""
return M @ w + b
# ---------- Robust accuracy: perturbasi optimal per titik ----------
# Arah optimal untuk model linear: kelas 1 didorong ke bawah logit,
# kelas 0 didorong ke atas. sign(w) adalah arah kenaikan logit tercepat
# per satuan L-infinity, gaya FGSM.
print("== Robust accuracy terhadap perturbasi FGSM ==")
print(f"||w||_1 = {np.abs(w).sum():.3f} | ||w||_2 = {np.linalg.norm(w):.3f}")
arah = (1 - 2 * y_uji).astype(float)[:, None] # +1 utk kelas 0, -1 utk kelas 1
print("epsilon | akurasi")
for eps in (0.0, 0.25, 0.5, 1.0, 2.0):
X_adv = X_uji + eps * arah * np.sign(w)[None, :]
akurasi = (model.predict(X_adv) == y_uji).mean()
print(f"{eps:7.2f} | {akurasi:.3f}")
# ---------- Satu contoh konkret: perturbasi minimal yang membalik prediksi ----------
# Pilih titik uji kelas 1 dengan margin logit sedang (0.5 sampai 1.5).
kandidat = np.where((y_uji == 1) & (decision_function(X_uji) > 0.5)
& (decision_function(X_uji) < 1.5))[0]
i = kandidat[0]
x = X_uji[i]
f_x = float(decision_function(x[None, :])[0])
p_x = float(model.predict_proba(x[None, :])[0, 1])
# Perturbasi L2 minimal eksak: dorong sepanjang -w/||w||_2 sejauh f/||w||_2,
# lalu beri 20% tambahan agar benar-benar melintasasi boundary.
eps_l2 = f_x / np.linalg.norm(w)
x_tepat = x - eps_l2 * w / np.linalg.norm(w)
f_tepat = float(decision_function(x_tepat[None, :])[0])
x_adv = x - 1.2 * eps_l2 * w / np.linalg.norm(w)
f_adv = float(decision_function(x_adv[None, :])[0])
p_adv = float(model.predict_proba(x_adv[None, :])[0, 1])
# Versi FGSM: epsilon sejauh 1.2 * f/||w||_1 pada arah -sign(w).
eps_fgsm = 1.2 * f_x / np.abs(w).sum()
delta_fgsm = -eps_fgsm * np.sign(w)
x_fgsm = x + delta_fgsm
print("\n== Satu transaksi kelas penipuan di ambang batas ==")
print(f"x asli : [{x[0]:+.3f}, {x[1]:+.3f}]")
print(f"logit f(x) : {f_x:+.3f} | peluang penipuan {p_x:.3f}")
print(f"prediksi : kelas {model.predict(x[None, :])[0]} (benar)")
print()
print("-- Teori: eps L2 minimal = f(x) / ||w||_2 --")
print(f"eps L2 minimal : {eps_l2:.3f}")
print(f"logit di titik tepat batas : {f_tepat:+.3f} "
f"(nol presisi, prediksi belum berubah)")
print(f"eps x 1.2 : {1.2 * eps_l2:.3f} | ||delta||_2 = "
f"{1.2 * eps_l2:.3f}")
print(f"x adversarial : [{x_adv[0]:+.3f}, {x_adv[1]:+.3f}]")
print(f"logit f(x_adv) : {f_adv:+.3f} | peluang penipuan {p_adv:.3f}")
print(f"prediksi : kelas {model.predict(x_adv[None, :])[0]} (salah)")
print()
print("-- Praktik: FGSM pada arah -sign(w) --")
print(f"eps FGSM (x 1.2) : {eps_fgsm:.3f} | ||delta||_2 = "
f"{np.linalg.norm(delta_fgsm):.3f}")
print(f"x FGSM : [{x_fgsm[0]:+.3f}, {x_fgsm[1]:+.3f}]")
print(f"prediksi FGSM : kelas {model.predict(x_fgsm[None, :])[0]} (salah)")
print()
print("skala fitur (std) :", np.round(X.std(axis=0), 3).tolist())
print("kesimpulan: pergeseran sekecil itu - sebagian kecil simpangan baku "
"fitur - sudah memindahkan prediksi ke kelas yang salah")Output yang Dihasilkan:
== Robust accuracy terhadap perturbasi FGSM ==
||w||_1 = 3.161 | ||w||_2 = 2.238
epsilon | akurasi
0.00 | 0.978
0.25 | 0.878
0.50 | 0.767
1.00 | 0.578
2.00 | 0.200
== Satu transaksi kelas penipuan di ambang batas ==
x asli : [-0.107, +0.267]
logit f(x) : +0.712 | peluang penipuan 0.671
prediksi : kelas 1 (benar)
-- Teori: eps L2 minimal = f(x) / ||w||_2 --
eps L2 minimal : 0.318
logit di titik tepat batas : +0.000 (nol presisi, prediksi belum berubah)
eps x 1.2 : 0.382 | ||delta||_2 = 0.382
x adversarial : [-0.362, -0.016]
logit f(x_adv) : -0.142 | peluang penipuan 0.464
prediksi : kelas 0 (salah)
-- Praktik: FGSM pada arah -sign(w) --
eps FGSM (x 1.2) : 0.270 | ||delta||_2 = 0.382
x FGSM : [-0.377, -0.003]
prediksi FGSM : kelas 0 (salah)
skala fitur (std) : [1.589, 1.587]
kesimpulan: pergeseran sekecil itu - sebagian kecil simpangan baku fitur - sudah memindahkan prediksi ke kelas yang salahMari kita bedah bagian-bagian pentingnya:
- Baris 16–23 (data dan pelatihan): dua klaster transaksi dibangkitkan
rngberseed 42;train_test_splitmemisahkan 90 baris uji. Model mencapai akurasi bersih 0,978 pada epsilon 0,00. - Baris 25–30 (
decision_function): fungsi kecil ini menghitung logit mentahM @ w + b— salinan eksak dari perhitungan internalLogisticRegression, sehingga semua rumus chapter ini bisa diverifikasi tanpa membongkar pustaka. - Baris 37–42 (sapuan robust accuracy):
arahmemilih tanda per titik — kelas 1 didorong ke bawah logit, kelas 0 ke atas — laluX_ujidigesereps * sign(w)per titik. Kolom akurasi runtuh dari 0,978 ke 0,200 saat epsilon naik ke 2,0: model yang tampak solid menjadi tidak bisa dipercaya di bawah gangguan terarah. - Baris 45–49 (pemilihan titik uji):
kandidatmenyaring titik uji kelas 1 dengan margin logit antara 0,5 dan 1,5 — cukup yakin bagi model, cukup dekat ke boundary bagi attacker. - Baris 56–62 (teori eksak):
eps_l2 = f_x / ||w||_2lalu titik digeser sepanjang-w/||w||_2. Keluaranlogit di titik tepat batas : +0.000memverifikasi rumus hingga presisi tiga desimal — titik hasil jatuh tepat di boundary, peluang 0,500. - Baris 64–69 (FGSM): versi tanda
delta = -eps * sign(w)denganeps = 1.2 * f/||w||_1membeli kebalikan yang sama dengan norma delta yang hampir identik (0,382) — pada koefisien yang hampir seimbang, FGSM praktis menyamai jalur L2 optimal.
Perhatikan pula skala relativnya: simpangan baku fitur sekitar 1,59, sedangkan ||delta||_2 pembalik prediksi hanya 0,382 — sekitar seperempat. Pada data berdimensi tinggi rasio ini makin ekstrem: dengan ratusan fitur, budget per koordinat menjadi sangat kecil, sehingga perubahan per fitur nyaris tak terdeteksi mata manusia, sementara akumulasi ratusan pergeseran searah sign(w) memindahkan logit jauh.
Pertahanan: Adversarial Training #
Pertahanan yang paling banyak dipelajari memakai mekanisme attacker itu sendiri sebagai bahan data latih. Adversarial training bekerja tiga langkah: hitung versi adversarial dari data latih dengan epsilon tetap, beri label asli (label tidak berubah — hanya posisi fitur yang digeser), gabungkan dengan data asli, lalu latih ulang. Model dipaksa membenarkan keputusan yang sama pada titik-titik yang sudah digeser ke arah paling merugikannya.
Praktikum kedua sengaja memakai data dengan dua fitur berskala berbeda — fitur kedua bervariasi sempit (simpangan baku 0,40) sehingga model standar meletakkan koefisien besar padanya. Pertanyaannya: apakah adversarial training menemukan ketimpangan itu sendiri?
# Demonstrasi adversarial training: data latih diperbesar dengan salinan
# adversarial (FGSM, epsilon tetap, label asli), lalu model dilatih ulang.
# Data ini sengaja memakai dua fitur dengan skala tidak seimbang -
# fitur kedua berstd kecil sehingga koefisiennya besar pada model standar.
# Model standar karenanya rapuh: menggeser fitur kedua sedikit saja sudah
# menggeser logit jauh. Adversarial training menemukan ketimpangan itu
# dari dalam dan menyeimbangkan kembali koefisiennya.
import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
# ---------- Data: 2 fitur informatif tapi berskala berbeda ----------
# fitur_1: simpangan baku 1.05 | fitur_2: simpangan baku 0.40
rng = np.random.default_rng(42)
n = 300
std_fitur = np.array([1.05, 0.40])
X_wajar = rng.normal(loc=[-1.2, -0.45], scale=std_fitur, size=(n // 2, 2))
X_penipuan = rng.normal(loc=[1.2, 0.45], scale=std_fitur, size=(n // 2, 2))
X = np.vstack([X_wajar, X_penipuan])
y = np.array([0] * (n // 2) + [1] * (n // 2))
X_latih, X_uji, y_latih, y_uji = train_test_split(
X, y, test_size=0.3, stratify=y, random_state=42
)
EPS_LATIH = 0.4 # besar perturbasi FGSM saat membuat contoh adversarial latih
def robust_accuracy(modelnya, eps):
"""Akurasi pada X_uji setelah tiap titik digeser optimal menurut model."""
w = modelnya.coef_[0]
arah = (1 - 2 * y_uji).astype(float)[:, None]
X_adv = X_uji + eps * arah * np.sign(w)[None, :]
return (modelnya.predict(X_adv) == y_uji).mean()
# ---------- Model standar: dilatih hanya pada data asli ----------
model_standar = LogisticRegression().fit(X_latih, y_latih)
# ---------- Adversarial training: tiga putaran augmentasi FGSM ----------
# Tiap putaran: buat salinan adversarial dari SELURUH data latih menurut
# koefisien model saat ini, gabungkan (label asli dipertahankan), latih
# ulang dari nol.
model_at = LogisticRegression().fit(X_latih, y_latih)
for putaran in range(3):
w = model_at.coef_[0]
arah = (1 - 2 * y_latih).astype(float)[:, None]
X_adv = X_latih + EPS_LATIH * arah * np.sign(w)[None, :]
X_perbesar = np.vstack([X_latih, X_adv])
y_perbesar = np.concatenate([y_latih, y_latih])
model_at = LogisticRegression().fit(X_perbesar, y_perbesar)
# ---------- Perbandingan ----------
print("== Adversarial training pada LogisticRegression ==")
print(f"epsilon latih : {EPS_LATIH} | fitur (std): "
f"{std_fitur.tolist()}")
print()
print("model | koefisien w | akurasi bersih")
for nama, m in (("standar", model_standar), ("adv-train", model_at)):
koef = np.round(m.coef_[0], 3).tolist()
print(f"{nama:9s}| {str(koef):18s} | {m.score(X_uji, y_uji):.3f}")
# ---------- Biaya serangan per fitur: eps * |w_j| ----------
# Pergeseran logit maksimum lewat fitur ke-j jika attacker menggesernya
# sejauh eps adalah eps * |w_j|. Fitur dengan biaya termahal adalah
# jalur serangan termurah bagi attacker.
print()
print("== Jalur serangan termurah: kontribusi per fitur (eps = 0.5) ==")
print("fitur | std data | model standar | model adv-train")
w_std = model_standar.coef_[0]
w_at = model_at.coef_[0]
for j in range(2):
print(f"{j + 1:6d} | {std_fitur[j]:8.2f} | eps*|w| = "
f"{0.5 * abs(w_std[j]):5.3f} | eps*|w| = {0.5 * abs(w_at[j]):.3f}")
# ---------- Kurva robustness ----------
print()
print("== Robust accuracy pada berbagai epsilon ==")
print("epsilon | standar | adv-train")
for eps in (0.0, 0.25, 0.5, 0.75, 1.0):
print(f"{eps:7.2f} | {robust_accuracy(model_standar, eps):7.3f} | "
f"{robust_accuracy(model_at, eps):.3f}")Output yang Dihasilkan:
== Adversarial training pada LogisticRegression ==
epsilon latih : 0.4 | fitur (std): [1.05, 0.4]
model | koefisien w | akurasi bersih
standar | [1.483, 3.034] | 0.956
adv-train| [1.487, 1.632] | 0.944
== Jalur serangan termurah: kontribusi per fitur (eps = 0.5) ==
fitur | std data | model standar | model adv-train
1 | 1.05 | eps*|w| = 0.742 | eps*|w| = 0.743
2 | 0.40 | eps*|w| = 1.517 | eps*|w| = 0.816
== Robust accuracy pada berbagai epsilon ==
epsilon | standar | adv-train
0.00 | 0.956 | 0.944
0.25 | 0.767 | 0.811
0.50 | 0.633 | 0.689
0.75 | 0.478 | 0.500
1.00 | 0.300 | 0.367Bedah baris-per-baris untuk contoh kedua:
- Baris 14–27 (data berskala timpang): kedua fitur sama-sama informatif — pusat kelasnya berjarak kira-kira 2,3 simpangan baku pada masing-masing fitur — tetapi simpangan baku fitur kedua hanya 0,40.
train_test_splitdenganrandom_state=42menjaga pembandingan adil. - Baris 29–35 (
robust_accuracy): fungsi penilaian menghitung arah serangan dari koefisien model yang sedang dinilai — model adv-train diuji dengan serangan yang disesuaikan terhadapnya, bukan serangan model lama. - Baris 43–51 (tiga putaran): setiap putaran membaca
model_at.coef_[0]terbaru, membangkitkan salinan adversarial seluruh data latih, lalu melatih ulang dari nol pada data gabungan 420 baris. Labely_perbesarmengulang label asli — hanya posisi titik yang digeser. - Baris 61–63 (tabel koefisien): model standar menulis
w = [1.483, 3.034]— mayoritas bobot jatuh pada fitur sempit. Model adv-train menulis[1.487, 1.632]: bobot fitur kedua terpangkas hampir separuh. - Baris 70–76 (biaya serangan per fitur): kolom
eps*|w|menerjemahkan koefisien menjadi daya beli attacker — menggeser fitur 2 sejauh 0,5 memindahkan logit 1,517 pada model standar, jalur termurah; setelah adversarial training jalur itu menjadi 0,816, hampir setara jalur fitur 1 (0,743).
Cara membaca hasilnya: akurasi bersih dibayar 0,956 → 0,944, dan robust accuracy naik di seluruh kurva epsilon (misalnya 0,633 → 0,689 pada 0,5; 0,300 → 0,367 pada 1,0). Kenaikan ini terlihat sederhana, tetapi mekanismenya yang penting: adversarial training menemukan sendiri bahwa fitur berskala sempit adalah pintu masuk termurah, lalu menyeimbangkan pertahanan — perilaku yang pada kasus ini juga bisa dicapai dengan StandardScaler, tetapi di sini dipelajari dari pola serangan, bukan dari asumsi preprocessing.
Batas Pertahanan dan Transferability #
Kejujuran angka di atas perlu dilanjutkan: pada model linear, kenaikan robustness dibatasi oleh jarak geometris antar kelas — titik yang benar-benar berada di wilayah tumpang tindih akan selalu bisa dibalik, oleh model apa pun. Laporan-literatur menunjukkan kenaikan yang jauh lebih besar pada jaringan dalam (deep networks), karena jaringan nonlinear punya ruang untuk melengkungkan boundary menjauhi titik data; implementasinya memakai pustaka differentiation otomatis seperti PyTorch — di luar cakupan lingkungan latihan kita, tetapi idenya identik dengan tiga putaran augmentasi di atas. Satu properti lain yang membuat evasion sulit: transferability — adversarial example yang dibuat terhadap satu model sering kali juga mengubah prediksi model lain yang dilatih pada data serupa, sehingga attacker tidak membutuhkan akses koefisien untuk memulai.
Tabel berikut merangkum keluarga pertahanan yang dikenal luas beserta posisinya.
| Keluarga pertahanan | Cara kerja | Catatan |
|---|---|---|
| Adversarial training | Melatih ulang dengan salinan input terganggu | Paling teruji; menaikkan robust accuracy, menurunkan akurasi bersih sedikit |
| Preprocessing / sanitasi input | Membersihkan atau meng-kompres input sebelum masuk model | Murah, tetapi bisa di-bypass jika preprocessing ikut dipelajari attacker |
| Deteksi input anomali | Menandai input yang dekat ke boundary atau ganjil secara statistik | Pelengkap, bukan pengganti — deteksi punya false positive sendiri |
| Ensemble beberapa model | Prediksi mayoritas menutup celah satu model | Membantu jika model beragam; transferability tetap menjadi risiko |
Pertahanan bertemu batasnya pada satu titik: sekuat apa pun modelnya, sistem yang membocorkan informasi lewat output-nya tetap menyisakan ruang bagi dua kategori OWASP berikutnya — model inversion dan membership inference. Keduanya tidak menyerang keputusan, melainkan data pribadi di balik model. Kita beralih ke sana di Chapter Privasi Data dan Machine Learning.
Referensi Resmi #
- Linear Models — scikit-learn User Guide — formulasi resmi logistic regression dan decision boundary-nya, dasar semua rumus chapter ini.
- sklearn.linear_model.LogisticRegression — API Reference
— atribut
coef_danintercept_yang menjadi gradien input pada model linear. - numpy.linalg — NumPy Documentation
— rujukan
normdan operasi vektor yang dipakai untuk menghitung perturbasi minimal.