Workshop Deep Learning

Sesi 1 / Pipeline lengkap

Training ANN untuk deteksi intrusi

Versi jadi dari kode yang kita tulis ulang dari nol di kelas. train.py hanya berisi 7 langkah berurutan — detail tiap langkah ada di modul kecil yang bisa dibaca satu per satu.

8 file363 barisPyTorch±15 detik di CPU

Jalankan dari root repo

$ uv run python sesi-1-ann/train.py
Dataset NSL-KDD otomatis diunduh ke sesi-1-ann/data/ saat pertama jalan. Model, grafik, dan metrik tersimpan di sesi-1-ann/outputs/. Ingin bereksperimen? Cukup ubah angka di config.py.

Alur train.py → modul

  1. 01persiapanconfig.py
  2. 02muat datadata.py
  3. 03pra-pemrosesanpreprocessing.py
  4. 04modelmodel.py
  5. 05trainingtraining.py
  6. 06evaluasievaluation.py
  7. 07simpan hasilplots.py

sesi-1-ann/train.py

Alur utama — dibaca dari atas ke bawah · 62 baris

GitHub
"""Sesi 1 — ANN untuk Deteksi Intrusi Jaringan (NSL-KDD)Workshop Deep Learning · Teknik Telekomunikasi ITERA Jalankan dari root repo:    uv run python sesi-1-ann/train.py Pengaturan (epoch, learning rate, dll.) ada di config.py.Hasil (model, grafik, metrik) disimpan di sesi-1-ann/outputs/.""" import json import numpy as npimport torch import configfrom data import load_nslkddfrom evaluation import evaluate, predictfrom model import build_model, count_parametersfrom plots import plot_confusion_matrix, plot_lossfrom preprocessing import encode_features, make_labels, make_loader, scale_features, split_train_valfrom training import train_model # 1. PERSIAPAN — angka acak tetap supaya hasil bisa diulangnp.random.seed(config.SEED)torch.manual_seed(config.SEED) # 2. MUAT DATAtrain_df, test_df = load_nslkdd() # 3. PRA-PEMROSESANy_all = make_labels(train_df)y_test = make_labels(test_df)X_all, X_test = encode_features(train_df, test_df)X_train, X_val, y_train, y_val = split_train_val(X_all, y_all)X_train, X_val, X_test = scale_features(X_train, X_val, X_test) train_loader = make_loader(X_train, y_train, shuffle=True)val_loader = make_loader(X_val, y_val) # 4. MODELmodel = build_model(n_features=X_train.shape[1])print(model)print(f"Total parameter: {count_parameters(model):,}") # 5. TRAININGhistory = train_model(model, train_loader, val_loader) # 6. EVALUASIval_metrics = evaluate("Validasi", y_val, predict(model, X_val))y_pred = predict(model, X_test)test_metrics = evaluate("Test (KDDTest+)", y_test, y_pred) # 7. SIMPAN HASILconfig.OUTPUT_DIR.mkdir(exist_ok=True)torch.save(model.state_dict(), config.OUTPUT_DIR / "model.pt")plot_loss(history, config.OUTPUT_DIR / "loss_curve.png")plot_confusion_matrix(y_test, y_pred, config.OUTPUT_DIR / "confusion_matrix.png")with open(config.OUTPUT_DIR / "metrics.json", "w") as f:    json.dump({"validation": val_metrics, "test": test_metrics, "history": history}, f, indent=2)print("\nHasil disimpan di folder sesi-1-ann/outputs/")

sesi-1-ann/config.py

Semua pengaturan eksperimen · 44 baris

GitHub
"""Pengaturan eksperimen.Ubah angka di sini lalu jalankan ulang train.py untuk mencoba hal baru.""" from pathlib import Path # ---- Hyperparameter training ----EPOCHS = 10            # berapa kali model melihat seluruh data latihBATCH_SIZE = 64        # jumlah data per satu langkah update bobotLEARNING_RATE = 0.001  # besar langkah gradient descentVAL_SIZE = 0.2         # 20% data latih disisihkan untuk validasiPATIENCE = 3           # berhenti jika val loss tidak membaik 3 epoch berturut-turutSEED = 42              # angka acak tetap -> hasil bisa diulang # ---- Arsitektur model: 122 input -> 64 -> 32 -> 1 output ----HIDDEN_1 = 64HIDDEN_2 = 32 # ---- Lokasi file (relatif terhadap folder ini) ----FOLDER = Path(__file__).parentDATA_DIR = FOLDER / "data"OUTPUT_DIR = FOLDER / "outputs" # ---- Dataset NSL-KDD (sumber asli: https://www.unb.ca/cic/datasets/nsl.html) ----TRAIN_URL = "https://raw.githubusercontent.com/defcom17/NSL_KDD/master/KDDTrain%2B.txt"TEST_URL = "https://raw.githubusercontent.com/defcom17/NSL_KDD/master/KDDTest%2B.txt" # 41 fitur koneksi jaringan + label serangan + tingkat kesulitanCOLUMNS = [    "duration", "protocol_type", "service", "flag", "src_bytes", "dst_bytes", "land",    "wrong_fragment", "urgent", "hot", "num_failed_logins", "logged_in", "num_compromised",    "root_shell", "su_attempted", "num_root", "num_file_creations", "num_shells",    "num_access_files", "num_outbound_cmds", "is_host_login", "is_guest_login", "count",    "srv_count", "serror_rate", "srv_serror_rate", "rerror_rate", "srv_rerror_rate",    "same_srv_rate", "diff_srv_rate", "srv_diff_host_rate", "dst_host_count",    "dst_host_srv_count", "dst_host_same_srv_rate", "dst_host_diff_srv_rate",    "dst_host_same_src_port_rate", "dst_host_srv_diff_host_rate", "dst_host_serror_rate",    "dst_host_srv_serror_rate", "dst_host_rerror_rate", "dst_host_srv_rerror_rate",    "label", "difficulty",] # fitur berupa teks (bukan angka) -> perlu one-hot encodingCATEGORICAL = ["protocol_type", "service", "flag"]

sesi-1-ann/data.py

Unduh & baca NSL-KDD · 29 baris

GitHub
"""Mengunduh dan membaca dataset NSL-KDD.""" import urllib.request import pandas as pd import config  def download(url, filename):    """Unduh file ke folder data/ (hanya jika belum ada)."""    path = config.DATA_DIR / filename    if not path.exists():        print(f"Mengunduh {filename} ...")        config.DATA_DIR.mkdir(exist_ok=True)        urllib.request.urlretrieve(url, path)    return path  def load_nslkdd():    """Baca data latih (KDDTrain+) dan data uji (KDDTest+) sebagai tabel."""    train_path = download(config.TRAIN_URL, "KDDTrain+.txt")    test_path = download(config.TEST_URL, "KDDTest+.txt")     train_df = pd.read_csv(train_path, names=config.COLUMNS)    test_df = pd.read_csv(test_path, names=config.COLUMNS)     print(f"KDDTrain+: {len(train_df):,} koneksi | KDDTest+: {len(test_df):,} koneksi")    return train_df, test_df

sesi-1-ann/preprocessing.py

Label, one-hot, split, scaling, DataLoader · 58 baris

GitHub
"""Mengubah tabel mentah menjadi angka yang siap dimasukkan ke model.""" import numpy as npimport pandas as pdimport torchfrom sklearn.model_selection import train_test_splitfrom sklearn.preprocessing import StandardScalerfrom torch.utils.data import DataLoader, TensorDataset import config  def make_labels(df):    """Label biner: normal -> 0, serangan apa pun (DoS, Probe, R2L, U2R) -> 1."""    is_attack = df["label"] != "normal"    return is_attack.to_numpy().astype(np.float32)  def encode_features(train_df, test_df):    """One-hot encoding untuk 3 fitur teks. Hasil: 41 fitur -> 122 kolom angka."""    features = train_df.drop(columns=["label", "difficulty"])    X_train = pd.get_dummies(features, columns=config.CATEGORICAL)     features = test_df.drop(columns=["label", "difficulty"])    X_test = pd.get_dummies(features, columns=config.CATEGORICAL)     # samakan kolom data uji dengan data latih (kategori yang tidak ada diisi 0)    X_test = X_test.reindex(columns=X_train.columns, fill_value=0)     print(f"Jumlah fitur setelah one-hot: {X_train.shape[1]}")    return X_train.to_numpy(np.float32), X_test.to_numpy(np.float32)  def split_train_val(X, y):    """Sisihkan sebagian data latih untuk validasi (proporsi normal/serangan dijaga)."""    X_train, X_val, y_train, y_val = train_test_split(        X, y, test_size=config.VAL_SIZE, stratify=y, random_state=config.SEED    )    print(f"Data latih: {len(X_train):,} | Data validasi: {len(X_val):,}")    return X_train, X_val, y_train, y_val  def scale_features(X_train, X_val, X_test):    """Standardization: rata-rata 0, standar deviasi 1.    Scaler HANYA belajar dari data latih, lalu dipakai ke validasi & uji."""    scaler = StandardScaler()    X_train = scaler.fit_transform(X_train).astype(np.float32)    X_val = scaler.transform(X_val).astype(np.float32)    X_test = scaler.transform(X_test).astype(np.float32)    return X_train, X_val, X_test  def make_loader(X, y, shuffle=False):    """Bungkus data menjadi DataLoader yang membagi data ke dalam batch."""    inputs = torch.tensor(X)    targets = torch.tensor(y).reshape(-1, 1)  # bentuk (N, 1), sama dengan output model    dataset = TensorDataset(inputs, targets)    return DataLoader(dataset, batch_size=config.BATCH_SIZE, shuffle=shuffle)

sesi-1-ann/model.py

Arsitektur MLP 122-64-32-1 · 23 baris

GitHub
"""Arsitektur ANN (Multi-Layer Perceptron).""" import torch.nn as nn import config  def build_model(n_features):    """MLP: input -> hidden 1 (ReLU) -> hidden 2 (ReLU) -> 1 output."""    model = nn.Sequential(        nn.Linear(n_features, config.HIDDEN_1),        nn.ReLU(),        nn.Linear(config.HIDDEN_1, config.HIDDEN_2),        nn.ReLU(),        nn.Linear(config.HIDDEN_2, 1),        # tanpa sigmoid di sini: sudah termasuk di dalam BCEWithLogitsLoss    )    return model  def count_parameters(model):    """Jumlah seluruh bobot + bias yang dipelajari model."""    return sum(p.numel() for p in model.parameters())

sesi-1-ann/training.py

Training loop & early stopping · 71 baris

GitHub
"""Proses training: forward, loss, backward, update bobot.""" import copy import torchimport torch.nn as nn import config  def train_one_epoch(model, loader, loss_fn, optimizer):    """Satu epoch = satu putaran melihat seluruh data latih, batch demi batch."""    model.train()    total_loss = 0.0    for inputs, targets in loader:        outputs = model(inputs)             # 1. forward        loss = loss_fn(outputs, targets)    # 2. hitung loss        optimizer.zero_grad()               # 3. hapus gradien lama        loss.backward()                     # 4. backpropagation        optimizer.step()                    # 5. update bobot        total_loss += loss.item() * len(inputs)    return total_loss / len(loader.dataset)  def validate(model, loader, loss_fn):    """Hitung loss di data validasi (tanpa mengubah bobot)."""    model.eval()    total_loss = 0.0    with torch.no_grad():  # tidak perlu gradien -> lebih cepat        for inputs, targets in loader:            outputs = model(inputs)            total_loss += loss_fn(outputs, targets).item() * len(inputs)    return total_loss / len(loader.dataset)  def train_model(model, train_loader, val_loader):    """Latih model beberapa epoch dan simpan bobot dengan val loss terbaik."""    loss_fn = nn.BCEWithLogitsLoss()  # sigmoid + binary cross-entropy    optimizer = torch.optim.Adam(model.parameters(), lr=config.LEARNING_RATE)     history = {"train": [], "val": []}    best_val_loss = float("inf")    best_weights = None    epochs_without_improvement = 0     print(f"Iterasi per epoch: {len(train_loader)}")    for epoch in range(1, config.EPOCHS + 1):        train_loss = train_one_epoch(model, train_loader, loss_fn, optimizer)        val_loss = validate(model, val_loader, loss_fn)        history["train"].append(train_loss)        history["val"].append(val_loss)         mark = ""        if val_loss < best_val_loss:            # model membaik -> simpan salinan bobotnya            best_val_loss = val_loss            best_weights = copy.deepcopy(model.state_dict())            epochs_without_improvement = 0            mark = " *"        else:            epochs_without_improvement += 1         print(f"Epoch {epoch:2d}/{config.EPOCHS} | train loss {train_loss:.4f} | val loss {val_loss:.4f}{mark}")         # early stopping: berhenti jika sudah lama tidak membaik (mencegah overfitting)        if epochs_without_improvement == config.PATIENCE:            print(f"Early stopping: val loss tidak membaik selama {config.PATIENCE} epoch")            break     model.load_state_dict(best_weights)  # kembalikan ke bobot terbaik    return history

sesi-1-ann/evaluation.py

Prediksi & metrik · 33 baris

GitHub
"""Mengukur kinerja model: prediksi, metrik, dan confusion matrix.""" import torchfrom sklearn.metrics import accuracy_score, confusion_matrix, f1_score, precision_score, recall_score  def predict(model, X):    """Prediksi 0 (normal) atau 1 (serangan) untuk setiap koneksi."""    model.eval()    with torch.no_grad():        outputs = model(torch.tensor(X))        probabilities = torch.sigmoid(outputs)  # ubah output menjadi peluang 0..1    predictions = (probabilities > 0.5).float()   # ambang batas 0.5    return predictions.numpy().flatten()  def evaluate(name, y_true, y_pred):    """Cetak metrik evaluasi dan kembalikan dalam bentuk dictionary."""    metrics = {        "accuracy": accuracy_score(y_true, y_pred),        "precision": precision_score(y_true, y_pred),        "recall": recall_score(y_true, y_pred),        "f1": f1_score(y_true, y_pred),    }    tn, fp, fn, tp = confusion_matrix(y_true, y_pred).ravel()     print(f"\n[{name}]")    print(f"  Akurasi  : {metrics['accuracy']:.4f}")    print(f"  Precision: {metrics['precision']:.4f}")    print(f"  Recall   : {metrics['recall']:.4f}   <- serangan yang terdeteksi")    print(f"  F1       : {metrics['f1']:.4f}")    print(f"  TP={tp:,}  FN={fn:,} (serangan lolos)  FP={fp:,} (alarm palsu)  TN={tn:,}")    return metrics

sesi-1-ann/plots.py

Grafik loss & confusion matrix · 43 baris

GitHub
"""Membuat grafik hasil training.""" import matplotlib matplotlib.use("Agg")  # simpan grafik ke file, tanpa membuka jendelaimport matplotlib.pyplot as pltfrom sklearn.metrics import confusion_matrix  def plot_loss(history, path):    """Kurva loss train vs validasi per epoch."""    epochs = range(1, len(history["train"]) + 1)    plt.figure(figsize=(6, 4))    plt.plot(epochs, history["train"], "o-", color="#0B1F3A", label="train")    plt.plot(epochs, history["val"], "o-", color="#C8102E", label="validasi")    plt.xlabel("Epoch")    plt.ylabel("BCE loss")    plt.title("Kurva loss training")    plt.grid(alpha=0.3)    plt.legend()    plt.tight_layout()    plt.savefig(path, dpi=150)    plt.close()  def plot_confusion_matrix(y_true, y_pred, path):    """Confusion matrix: baris = label asli, kolom = prediksi."""    cm = confusion_matrix(y_true, y_pred)    labels = ["Normal", "Serangan"]    plt.figure(figsize=(4.5, 4))    plt.imshow(cm, cmap="Blues")    for i in range(2):        for j in range(2):            color = "white" if cm[i, j] > cm.max() / 2 else "black"            plt.text(j, i, f"{cm[i, j]:,}", ha="center", va="center", color=color)    plt.xticks([0, 1], labels)    plt.yticks([0, 1], labels)    plt.xlabel("Prediksi")    plt.ylabel("Label asli")    plt.title("Confusion matrix (KDDTest+)")    plt.tight_layout()    plt.savefig(path, dpi=150)    plt.close()

Output / config bawaan (seed 42, 10 epoch)

Hasil yang diharapkan

Angka di laptopmu bisa sedikit berbeda (versi library / CPU), tetapi polanya harus sama.

Akurasi validasi

99.5%

Akurasi test

79.6%

Precision test

92.8%

Recall test

69.5%

Bahan diskusi

Kenapa validasi 99% tetapi test hanya ±80%?

KDDTest+ memuat 17 jenis serangan baru yang tidak pernah muncul di KDDTrain+ (mis. mailbomb, apache2, snmpguess) — 29% dari seluruh serangan di test. Model hanya mendeteksi ±52% serangan jenis baru ini, dibanding ±77% untuk jenis yang sudah dikenal. Validasi diambil dari distribusi yang sama dengan data latih, sehingga terlalu optimistis. Di jaringan sungguhan, penyerang selalu mencoba cara baru.

terminal

KDDTrain+: 125,973 koneksi | KDDTest+: 22,544 koneksi
Jumlah fitur setelah one-hot: 122
Data latih: 100,778 | Data validasi: 25,195
Sequential(
  (0): Linear(in_features=122, out_features=64, bias=True)
  (1): ReLU()
  (2): Linear(in_features=64, out_features=32, bias=True)
  (3): ReLU()
  (4): Linear(in_features=32, out_features=1, bias=True)
)
Total parameter: 9,985
Iterasi per epoch: 1575
Epoch  1/10 | train loss 0.0580 | val loss 0.0253 *
Epoch  2/10 | train loss 0.0226 | val loss 0.0208 *
Epoch  3/10 | train loss 0.0189 | val loss 0.0201 *
Epoch  4/10 | train loss 0.0170 | val loss 0.0178 *
Epoch  5/10 | train loss 0.0158 | val loss 0.0171 *
Epoch  6/10 | train loss 0.0148 | val loss 0.0159 *
Epoch  7/10 | train loss 0.0137 | val loss 0.0158 *
Epoch  8/10 | train loss 0.0136 | val loss 0.0148 *
Epoch  9/10 | train loss 0.0127 | val loss 0.0149
Epoch 10/10 | train loss 0.0122 | val loss 0.0163

[Validasi]
  Akurasi  : 0.9949
  Precision: 0.9952
  Recall   : 0.9939   <- serangan yang terdeteksi
  F1       : 0.9945
  TP=11,654  FN=72 (serangan lolos)  FP=56 (alarm palsu)  TN=13,413

[Test (KDDTest+)]
  Akurasi  : 0.7960
  Precision: 0.9284
  Recall   : 0.6953   <- serangan yang terdeteksi
  F1       : 0.7951
  TP=8,923  FN=3,910 (serangan lolos)  FP=688 (alarm palsu)  TN=9,023

Hasil disimpan di folder sesi-1-ann/outputs/
Kurva loss train dan validasi selama 10 epoch; val loss terendah di epoch 8
Confusion matrix KDDTest+: TN 9.023, FP 688, FN 3.910, TP 8.923

Coba ubah sendiri

Ganti nilai di config.py, jalankan ulang train.py, lalu bandingkan grafik dan metriknya.

EPOCHS = 20
PATIENCE = 5

Latih lebih lama. Apakah val loss masih turun, atau mulai overfitting?

LEARNING_RATE = 0.01

Learning rate 10× lebih besar. Perhatikan kurva loss-nya.

BATCH_SIZE = 256

Batch lebih besar: iterasi per epoch berkurang, lebih cepat — akurasinya?

HIDDEN_1 = 128
HIDDEN_2 = 64

Model lebih besar: 24.065 parameter (dari 9.985). Apakah hasil test ikut naik?