Lompat ke konten
nalar

Deteksi Anomali Tingkat Lanjut (Isolation Forest & Autoencoder)

Deteksi anomali tanpa label untuk predictive maintenance — Isolation Forest, Autoencoder dengan reconstruction error, penentuan ambang, dan arsitektur hybrid laten + Isolation Forest.

Notebook lab (pratinjau)

Unduh lalu jalankan di Jupyter, VS Code, atau Google Colab.

Unduh lab.ipynb

Lab Minggu 16 — Deteksi Anomali: Isolation Forest, Autoencoder & Hybrid

Skenario: Kamu data scientist di pabrik. Mesin baru dipasang 5 sensor (suhu, getaran, tekanan, arus, rpm). Belum ada catatan kerusakan (cold-start). Tugas: bangun detektor anomali yang belajar dari data operasi normal.

Tujuan lab:

  1. Eksplorasi data sensor & jenis anomali.
  2. Isolation Forest (rumus + scikit-learn).
  3. Autoencoder dengan PyTorch, skor = reconstruction error.
  4. Hybrid: laten autoencoder + Isolation Forest.
  5. Bandingkan performa dengan label (yang hanya dipakai untuk evaluasi).
[1]
import sys
sys.path.insert(0, "..")

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.ensemble import IsolationForest
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import precision_score, recall_score, f1_score, roc_auc_score

from common.datasets import sensor_mesin

FITUR = ["suhu", "getaran", "tekanan", "arus", "rpm"]
df = sensor_mesin()
print(df.shape, "| anomali:", df.anomali.sum())
df.head()

1. Eksplorasi

Label anomali hanya untuk evaluasi akhir. Model tidak boleh melihatnya.

[2]
df.groupby("anomali")[FITUR].mean().round(3)
[3]
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
pasangan = [("rpm", "tekanan"), ("suhu", "arus"), ("rpm", "getaran")]
for ax, (a, b) in zip(axes, pasangan):
    ax.scatter(df[a], df[b], c=df.anomali, cmap="coolwarm", s=8)
    ax.set_xlabel(a); ax.set_ylabel(b)
plt.suptitle("Merah = anomali (hanya untuk visualisasi)"); plt.show()

Perhatikan: sebagian anomali (relasional) tidak ekstrem di satu sensor, tetapi kombinasinya aneh (rpm normal, tekanan tinggi, arus rendah).

2. Skema cold-start

Ambil 1000 baris normal pertama sebagai "data operasi harian" untuk latihan. Sisanya data uji (berisi semua anomali).

[4]
idx_latih = df.index[df.anomali == 0][:1000]
X_latih = df.loc[idx_latih, FITUR].to_numpy()
uji = df.drop(index=idx_latih)
X_uji, y_uji = uji[FITUR].to_numpy(), uji.anomali.to_numpy()
print(X_latih.shape, X_uji.shape, y_uji.sum())

def laporan(nama, skor_latih, skor_uji, q=99):
    ambang = np.percentile(skor_latih, q)
    pred = (skor_uji > ambang).astype(int)
    return {"model": nama,
            "precision": precision_score(y_uji, pred, zero_division=0),
            "recall": recall_score(y_uji, pred),
            "f1": f1_score(y_uji, pred),
            "roc_auc": roc_auc_score(y_uji, skor_uji)}

3. Isolation Forest

3a. Rumus c(n) dan skor s(x, n)

[5]
EULER = 0.5772156649
def c_n(n):
    if n <= 1: return 0.0
    if n == 2: return 1.0
    return 2 * (np.log(n - 1) + EULER) - 2 * (n - 1) / n

print("c(256) =", round(c_n(256), 3))
h = np.linspace(0, 20, 100)
plt.plot(h, 2 ** (-h / c_n(256)))
plt.axhline(0.5, ls="--", c="gray"); plt.axvline(c_n(256), ls="--", c="gray")
plt.xlabel("E[h(x)] (rata-rata panjang jalur)"); plt.ylabel("skor s"); plt.title("Skor anomali vs panjang jalur"); plt.show()

3b. scikit-learn

[6]
iso = IsolationForest(n_estimators=200, random_state=0).fit(X_latih)
s_latih_if = -iso.score_samples(X_latih)   # makin besar makin anomali
s_uji_if = -iso.score_samples(X_uji)
hasil = [laporan("Isolation Forest", s_latih_if, s_uji_if)]
pd.DataFrame(hasil).round(3)

Distribusi skor titik normal vs anomali pada data uji:

[7]
plt.hist(s_uji_if[y_uji == 0], bins=40, alpha=0.6, label="normal", density=True)
plt.hist(s_uji_if[y_uji == 1], bins=20, alpha=0.6, label="anomali", density=True)
plt.axvline(np.percentile(s_latih_if, 99), c="k", ls="--", label="ambang p99")
plt.legend(); plt.title("Distribusi skor Isolation Forest"); plt.show()

4. Autoencoder (PyTorch)

[8]
import torch
from torch import nn

torch.manual_seed(0)
scaler = StandardScaler().fit(X_latih)
Z_latih = torch.tensor(scaler.transform(X_latih), dtype=torch.float32)
Z_uji = torch.tensor(scaler.transform(X_uji), dtype=torch.float32)

class Autoencoder(nn.Module):
    def __init__(self, d=5, laten=3):
        super().__init__()
        self.encoder = nn.Sequential(nn.Linear(d, 8), nn.ReLU(), nn.Linear(8, laten))
        self.decoder = nn.Sequential(nn.ReLU(), nn.Linear(laten, 8), nn.ReLU(), nn.Linear(8, d))
    def forward(self, x):
        return self.decoder(self.encoder(x))

ae = Autoencoder()
opt = torch.optim.Adam(ae.parameters(), lr=1e-2)
loss_fn = nn.MSELoss()
riwayat = []
for epoch in range(300):
    opt.zero_grad()
    loss = loss_fn(ae(Z_latih), Z_latih)
    loss.backward(); opt.step()
    riwayat.append(loss.item())
plt.plot(riwayat); plt.yscale("log"); plt.xlabel("epoch"); plt.ylabel("MSE"); plt.title("Kurva latih autoencoder"); plt.show()
[9]
@torch.no_grad()
def error_rekon(Z):
    return ((ae(Z) - Z) ** 2).mean(dim=1).numpy()

e_latih, e_uji = error_rekon(Z_latih), error_rekon(Z_uji)
hasil.append(laporan("Autoencoder (MSE)", e_latih, e_uji))
pd.DataFrame(hasil).round(3)

Error per sensor membantu diagnosis: sensor mana yang paling "tidak cocok"?

[10]
with torch.no_grad():
    err_sensor = ((ae(Z_uji) - Z_uji) ** 2).numpy()
pd.DataFrame(err_sensor[y_uji == 1], columns=FITUR).head(8).round(2)

5. Hybrid: laten + error → Isolation Forest

[11]
with torch.no_grad():
    L_latih = ae.encoder(Z_latih).numpy()
    L_uji = ae.encoder(Z_uji).numpy()
F_latih = np.column_stack([L_latih, e_latih])
F_uji = np.column_stack([L_uji, e_uji])

iso_h = IsolationForest(n_estimators=200, random_state=0).fit(F_latih)
hasil.append(laporan("Hybrid (laten+MSE → IF)", -iso_h.score_samples(F_latih), -iso_h.score_samples(F_uji)))
tabel = pd.DataFrame(hasil).set_index("model").round(3)
tabel
[12]
tabel[["precision", "recall", "f1"]].plot.bar(figsize=(8, 4), rot=0, ylim=(0, 1.05))
plt.title("Perbandingan detektor (ambang persentil 99)"); plt.show()

6. Pengaruh ambang

[13]
qs = [90, 95, 97, 99, 99.5, 99.9]
rows = []
for q in qs:
    ambang = np.percentile(e_latih, q)
    pred = (e_uji > ambang).astype(int)
    rows.append({"q": q, "alarm": pred.sum(),
                 "precision": precision_score(y_uji, pred, zero_division=0),
                 "recall": recall_score(y_uji, pred)})
pd.DataFrame(rows).round(3)

✍️ Coba sendiri

  1. Ubah ukuran laten autoencoder menjadi 1, 2, dan 4. Bagaimana recall & precision berubah?
  2. Latih Isolation Forest hanya pada fitur suhu dan getaran. Anomali jenis apa yang terlewat?
  3. Pakai contamination=0.02 di IsolationForest lalu predict. Bandingkan dengan ambang persentil.
  4. Kalau biaya 1 kerusakan terlewat = Rp 50 juta dan 1 alarm palsu = Rp 500 ribu, ambang persentil berapa yang paling murah? (pakai tabel bagian 6)
  5. Jelaskan mengapa hybrid lebih menarik untuk 120 sensor dibanding 5 sensor.
[14]
# 1.
[15]
# 2.
[16]
# 3.
[17]
# 4.

Jawaban no. 5:

  • ...

Selanjutnya

Kerjakan kuis dan exercise.py, lalu jalankan python check.py 16 dari folder utama.