KI: PRAKTEK 12: Privacy-Preserving AI (Differential Privacy)

From OnnoWiki
Jump to navigation Jump to search

Tujuan

Di akhir sesi ini, mahasiswa bisa:

  • Memahami trade-off: privasi vs akurasi (utility).
  • Menerapkan differential privacy (DP) pada analitik sederhana (mean, count, histogram).
  • Mengukur utility loss (mis. MAE/RMSE) setelah diberi noise.
  • Menghasilkan dataset/hasil analitik yang lebih aman untuk dibagikan.
  • (Opsional) Mengamankan file hasil rilis memakai GnuPG (enkripsi & tanda tangan).

Inti mindset: “Kalau datanya akan dibagi, asumsi terburuk: musuh punya informasi tambahan.” DP membantu mengurangi risiko “kebocoran lewat statistik”.

Konsep Inti yang Perlu Dipahami

1. Apa itu Differential Privacy?

Secara sederhana: sebuah mekanisme memenuhi differential privacy jika hasil rilis tidak berubah drastis walaupun 1 orang di dataset ditambahkan atau dihapus.

  • Parameter utama: ε (epsilon)
    • ε kecil ⇒ privasi lebih kuat ⇒ noise lebih besar ⇒ akurasi turun
    • ε besar ⇒ privasi lebih lemah ⇒ noise kecil ⇒ akurasi naik

2. Kenapa “Noise Injection” masuk akal?

Karena banyak kebocoran privasi terjadi bukan dari “membuka row data”, tapi dari:

  • rilis mean / count / histogram
  • dashboard statistik UMKM/kampus/kelas
  • laporan “rata-rata nilai” + filter kecil (mis. per prodi, per angkatan)

DP menambahkan noise terukur agar statistik tetap berguna, tapi sulit dipakai untuk menebak data individu.

3. Istilah penting (biar tidak bingung)

  • Sensitivity: seberapa besar output statistik bisa berubah jika 1 data berubah.
  • Laplace mechanism: noise dari distribusi Laplace (umum untuk count/mean).
  • Clipping: membatasi nilai agar sensitivity terkendali (praktik wajib).

Rule utama DP di praktikum ini: sebelum DP, lakukan clipping agar sensitivity jelas.

Tools (Open Source)

  • Ubuntu 24.04
  • Python 3 (venv, pip)
  • Library: numpy, pandas
  • (Opsional) matplotlib untuk visualisasi
  • (Opsional) GnuPG untuk mengamankan file rilis

Skenario Real yang Bisa Diimplementasi

Bayangkan Anda punya dataset internal:

  • age (umur)
  • income (pendapatan)
  • score (nilai)
  • department (kategori)

Anda ingin merilis:

  • Rata-rata pendapatan per kelas/kelompok
  • Jumlah orang per kategori
  • Histogram nilai untuk laporan

Tanpa DP, mean/count bisa jadi celah (mis. difference attack saat seseorang tahu semua orang kecuali 1).

LANGKAH PRAKTIKUM (Ubuntu 24.04)

Step 0 — Setup Environment

sudo apt update
sudo apt install -y python3 python3-venv python3-pip gnupg
mkdir -p praktek12_dp && cd praktek12_dp
python3 -m venv .venv
source .venv/bin/activate
pip install -U pip
pip install numpy pandas matplotlib

Step 1 — Siapkan Dataset Contoh (atau pakai data Anda)

Kita buat dataset sintetis supaya aman untuk latihan, tapi strukturnya realistis.

Buat file: generate_dataset.py

import numpy as np
import pandas as pd

def main():
    rng = np.random.default_rng(42)
    n = 2000

    dept = rng.choice(["TI", "SI", "DKV", "Bisnis"], size=n, p=[0.35, 0.30, 0.20, 0.15])
    age = rng.integers(18, 45, size=n)

    # income realistis (lognormal) dalam juta rupiah
    income = rng.lognormal(mean=2.2, sigma=0.45, size=n)  # ~ 5-20 jutaan
    income = income * 1_000_000

    # score 0-100
    score = np.clip(rng.normal(loc=78, scale=10, size=n), 0, 100)

    df = pd.DataFrame({
        "department": dept,
        "age": age,
        "income": income.round(0).astype(int),
        "score": score.round(1),
    })

    df.to_csv("raw_dataset.csv", index=False)
    print("OK: raw_dataset.csv dibuat. Jumlah row:", len(df))
    print(df.head(5))

if __name__ == "__main__":
    main()

Jalankan:

python generate_dataset.py

Step 2 — Implementasi Differential Privacy (Noise Injection + Clipping)

Kita akan:

  • clip income ke rentang wajar (mis. 0..50 juta)
  • hitung mean/count/histogram
  • tambahkan noise Laplace dengan parameter ε
  • bandingkan utility (error)

Buat file: dp_release.py

import argparse
import numpy as np
import pandas as pd

def laplace_noise(scale: float, rng: np.random.Generator) -> float:
    # Laplace(0, b) => scale = b
    return rng.laplace(0.0, scale)

def dp_count(true_count: int, epsilon: float, rng: np.random.Generator) -> int:
    # sensitivity untuk count = 1
    scale = 1.0 / epsilon
    noisy = true_count + laplace_noise(scale, rng)
    return int(max(0, round(noisy)))

def dp_mean(values: np.ndarray, epsilon: float, clip_min: float, clip_max: float, rng: np.random.Generator) -> float:
    """
    DP mean via Laplace:
    - clip values => sensitivity mean = (clip_max - clip_min) / n
    - add Laplace noise to mean
    """
    clipped = np.clip(values, clip_min, clip_max)
    n = len(clipped)
    if n == 0:
        return float("nan")

    true_mean = float(np.mean(clipped))
    sensitivity = (clip_max - clip_min) / n
    scale = sensitivity / epsilon
    noisy_mean = true_mean + laplace_noise(scale, rng)
    return noisy_mean

def dp_histogram(values: np.ndarray, bins: np.ndarray, epsilon: float, rng: np.random.Generator) -> np.ndarray:
    """
    DP histogram:
    - hitung count per bin
    - tambahkan Laplace noise ke setiap bin
    Catatan: ini menghabiskan budget DP per bin (komposisi).
    Untuk praktikum, kita pakai epsilon_per_bin = epsilon / k
    """
    counts, _ = np.histogram(values, bins=bins)
    k = len(counts)
    epsilon_per_bin = epsilon / max(1, k)

    noisy = []
    for c in counts:
        noisy.append(dp_count(int(c), epsilon_per_bin, rng))
    return np.array(noisy, dtype=int)

def utility_metrics(true_arr: np.ndarray, noisy_arr: np.ndarray) -> dict:
    err = noisy_arr.astype(float) - true_arr.astype(float)
    mae = float(np.mean(np.abs(err)))
    rmse = float(np.sqrt(np.mean(err**2)))
    return {"MAE": mae, "RMSE": rmse}

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--input", default="raw_dataset.csv")
    parser.add_argument("--epsilon", type=float, default=1.0)
    parser.add_argument("--seed", type=int, default=123)
    parser.add_argument("--clip_income_max", type=float, default=50_000_000.0)  # 50 juta
    args = parser.parse_args()

    rng = np.random.default_rng(args.seed)
    df = pd.read_csv(args.input)

    eps = args.epsilon
    if eps <= 0:
        raise ValueError("epsilon harus > 0")

    # ====== RELEASE 1: DP Count per department ======
    dept_counts = df["department"].value_counts().sort_index()
    true_counts = dept_counts.values

    noisy_counts = np.array([dp_count(int(c), eps, rng) for c in true_counts], dtype=int)

    # ====== RELEASE 2: DP Mean income (global) ======
    income = df["income"].to_numpy(dtype=float)
    dp_income_mean = dp_mean(income, eps, clip_min=0.0, clip_max=args.clip_income_max, rng=rng)
    true_income_mean = float(np.mean(np.clip(income, 0.0, args.clip_income_max)))

    # ====== RELEASE 3: DP Histogram score ======
    score = df["score"].to_numpy(dtype=float)
    bins = np.array([0,10,20,30,40,50,60,70,80,90,100], dtype=float)
    true_hist, _ = np.histogram(score, bins=bins)
    dp_hist = dp_histogram(score, bins=bins, epsilon=eps, rng=rng)

    # ====== Utility evaluation ======
    count_metrics = utility_metrics(true_counts, noisy_counts)
    hist_metrics = utility_metrics(true_hist, dp_hist)

    result = {
        "epsilon": eps,
        "release": {
            "dp_count_per_department": {
                "departments": dept_counts.index.tolist(),
                "true_counts": true_counts.tolist(),
                "noisy_counts": noisy_counts.tolist(),
                "utility": count_metrics,
            },
            "dp_mean_income": {
                "clip_max": args.clip_income_max,
                "true_mean": true_income_mean,
                "noisy_mean": dp_income_mean,
                "abs_error": abs(dp_income_mean - true_income_mean),
            },
            "dp_hist_score": {
                "bins": bins.tolist(),
                "true_hist": true_hist.tolist(),
                "noisy_hist": dp_hist.tolist(),
                "utility": hist_metrics,
            }
        }
    }

    # Simpan "dataset aman" versi rilis (bukan raw rows!)
    # Konsep penting: seringkali yang aman itu *released statistics*, bukan "anonymized rows"
    import json
    with open("dp_release.json", "w") as f:
        json.dump(result, f, indent=2)

    print("OK: dp_release.json dibuat.")
    print("Ringkas:")
    print("- epsilon:", eps)
    print("- mean income true vs noisy:", true_income_mean, "=>", dp_income_mean)
    print("- count utility (MAE/RMSE):", count_metrics)
    print("- hist utility (MAE/RMSE):", hist_metrics)

if __name__ == "__main__":
    main()

Jalankan beberapa eksperimen epsilon:

python dp_release.py --epsilon 0.2
python dp_release.py --epsilon 1.0
python dp_release.py --epsilon 5.0

Yang harus kamu amati:

  • ε=0.2 → privasi kuat, noise besar, error naik
  • ε=5.0 → privasi lemah, noise kecil, error turun

Step 3 — Bandingkan Utility Secara “Data Scientist”

Biar fun, bikin tabel perbandingan. Jalankan:

for e in 0.2 0.5 1 2 5; do
  echo "==== EPSILON $e ===="
  python dp_release.py --epsilon $e | tail -n 6
done

Tantangan (menantang tapi doable):

  • Tentukan epsilon “layak rilis” untuk laporan kampus/UMKM.
  • Target utility misalnya:
    • MAE count per kategori < 10
    • Error mean < 3% dari true mean

Output Praktikum (Yang Dikumpulkan)

Mahasiswa mengumpulkan:

  • dp_release.json (hasil rilis DP)
  • Laporan singkat (1–2 halaman) berisi:
    • nilai ε yang dicoba
    • perbandingan error (MAE/RMSE) + interpretasi
    • rekomendasi ε final dan alasan
    • catatan trade-off yang terjadi

Catatan penting: yang Anda rilis adalah privacy-preserving analytics, bukan “dataset baris-per-baris”.

BONUS: Amankan File Rilis dengan GnuPG (Enkripsi + Tanda Tangan)

Ini realistis banget: Anda mau kirim dp_release.json ke dosen/tim.

1. Generate key (sekali saja)

gpg --full-generate-key

Pilih:

  • jenis: RSA and RSA
  • size: 3072 atau 4096
  • isi nama & email

Cek:

gpg --list-keys

2. Enkripsi file untuk penerima (recommended)

Misal penerima punya email dosen@example.ac.id (harus sudah ada public key-nya di keyring Anda). Import public key dulu jika perlu:

gpg --import dosen_publickey.asc

Enkripsi:

gpg --encrypt --recipient dosen@example.ac.id dp_release.json
# hasil: dp_release.json.gpg

3. Decrypt (di sisi penerima)

gpg --decrypt dp_release.json.gpg > dp_release_decrypted.json

4. Tanda tangan file (integrity)

gpg --detach-sign dp_release.json
  1. hasil: dp_release.json.sig

Verifikasi:

gpg --verify dp_release.json.sig dp_release.json

Diskusi Kelas

Pertanyaan pemantik:

  • Kalau Anda rilis mean income per departemen, berapa minimal ukuran kelompok agar aman?
  • Apa yang terjadi kalau filter dibuat terlalu spesifik (mis. “TI semester 1 kelas A”)?
  • Apakah DP cukup? Kapan perlu tambahan: access control, audit log, dan encryption?

Takeaway yang wajib dibawa pulang:

  • Privasi itu desain sistem, bukan cuma “hapus kolom NIK”.
  • DP adalah alat untuk membuat rilis statistik lebih aman dan terukur.

Pranala Menarik