593b94c120
pytest / Unit Tests (push) Has been cancelled
pytest / Integration (integration_tests_a) (push) Has been cancelled
pytest / Integration (integration_tests_b) (push) Has been cancelled
pytest / Integration (integration_tests_c) (push) Has been cancelled
pytest / Integration (integration_tests_d) (push) Has been cancelled
pytest / Integration (integration_tests_e) (push) Has been cancelled
pytest / Integration (integration_tests_f) (push) Has been cancelled
pytest / Integration (integration_tests_g) (push) Has been cancelled
pytest / Integration (integration_tests_h) (push) Has been cancelled
pytest / Integration (integration_tests_i) (push) Has been cancelled
pytest / Integration (integration_tests_j) (push) Has been cancelled
pytest / Distributed (distributed_a) (push) Has been cancelled
pytest / Distributed (distributed_b) (push) Has been cancelled
pytest / Distributed (distributed_c) (push) Has been cancelled
pytest / Distributed (distributed_d) (push) Has been cancelled
pytest / Distributed (distributed_e) (push) Has been cancelled
pytest / Distributed (distributed_f) (push) Has been cancelled
pytest / Minimal Install (push) Has been cancelled
pytest / Event File (push) Has been cancelled
pytest (slow) / py-slow (push) Has been cancelled
Publish JSON Schema / publish-schema (push) Has been cancelled
191 lines
6.2 KiB
Python
191 lines
6.2 KiB
Python
# Colab: !pip install ludwig
|
|
"""Anomaly detection with Ludwig using Deep SVDD, Deep SAD, and DROCC losses.
|
|
|
|
Generates synthetic sensor data, trains all three model variants, evaluates on a
|
|
held-out test set containing both normal and anomalous samples, and prints an AUC-ROC
|
|
comparison table.
|
|
|
|
Usage:
|
|
python train.py
|
|
"""
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
from sklearn.metrics import roc_auc_score
|
|
|
|
from ludwig.api import LudwigModel
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 1. Generate synthetic sensor data
|
|
# ---------------------------------------------------------------------------
|
|
|
|
RNG = np.random.default_rng(42)
|
|
|
|
N_NORMAL = 800
|
|
N_ANOMALY = 200
|
|
|
|
# Normal samples: Gaussian near origin
|
|
normal = pd.DataFrame(
|
|
{
|
|
"sensor_a": RNG.normal(0.0, 1.0, N_NORMAL),
|
|
"sensor_b": RNG.normal(0.0, 1.0, N_NORMAL),
|
|
"sensor_c": RNG.normal(0.0, 1.0, N_NORMAL),
|
|
"timestamp_hour": RNG.integers(0, 24, N_NORMAL).astype(float),
|
|
"anomaly": 0.0,
|
|
}
|
|
)
|
|
|
|
# Anomalous samples: large offset from origin
|
|
anomalous = pd.DataFrame(
|
|
{
|
|
"sensor_a": RNG.normal(6.0, 1.0, N_ANOMALY),
|
|
"sensor_b": RNG.normal(6.0, 1.0, N_ANOMALY),
|
|
"sensor_c": RNG.normal(6.0, 1.0, N_ANOMALY),
|
|
"timestamp_hour": RNG.integers(0, 24, N_ANOMALY).astype(float),
|
|
"anomaly": 1.0,
|
|
}
|
|
)
|
|
|
|
all_data = pd.concat([normal, anomalous], ignore_index=True)
|
|
|
|
# Train split: ONLY normal samples (anomaly detection is unsupervised)
|
|
# Val split: mix of normal and anomalous for threshold selection
|
|
# Test split: mix for final evaluation
|
|
|
|
normal_idx = all_data[all_data["anomaly"] == 0].index.tolist()
|
|
anomaly_idx = all_data[all_data["anomaly"] == 1].index.tolist()
|
|
|
|
RNG.shuffle(normal_idx)
|
|
n_train = int(0.7 * len(normal_idx))
|
|
n_val = int(0.15 * len(normal_idx))
|
|
|
|
train_idx = normal_idx[:n_train]
|
|
val_normal_idx = normal_idx[n_train : n_train + n_val]
|
|
test_normal_idx = normal_idx[n_train + n_val :]
|
|
|
|
RNG.shuffle(anomaly_idx)
|
|
n_val_anom = len(anomaly_idx) // 2
|
|
val_anom_idx = anomaly_idx[:n_val_anom]
|
|
test_anom_idx = anomaly_idx[n_val_anom:]
|
|
|
|
# Assign split column: 0=train, 1=val, 2=test
|
|
all_data["split"] = -1
|
|
all_data.loc[train_idx, "split"] = 0
|
|
all_data.loc[val_normal_idx, "split"] = 1
|
|
all_data.loc[val_anom_idx, "split"] = 1
|
|
all_data.loc[test_normal_idx, "split"] = 2
|
|
all_data.loc[test_anom_idx, "split"] = 2
|
|
|
|
train_df = all_data[all_data["split"] == 0].copy()
|
|
test_df = all_data[all_data["split"].isin([1, 2])].copy()
|
|
|
|
train_df.to_csv("/tmp/sensors_train.csv", index=False)
|
|
test_df.to_csv("/tmp/sensors_test.csv", index=False)
|
|
|
|
print(f"Train samples: {len(train_df)} (all normal)")
|
|
print(f"Test samples: {len(test_df)} ({(test_df['anomaly'] == 1).sum()} anomalous)")
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 2. Helper: build Ludwig config dict
|
|
# ---------------------------------------------------------------------------
|
|
|
|
INPUT_FEATURES = [
|
|
{"name": "sensor_a", "type": "number", "preprocessing": {"normalization": "zscore"}},
|
|
{"name": "sensor_b", "type": "number", "preprocessing": {"normalization": "zscore"}},
|
|
{"name": "sensor_c", "type": "number", "preprocessing": {"normalization": "zscore"}},
|
|
{"name": "timestamp_hour", "type": "number", "preprocessing": {"normalization": "zscore"}},
|
|
]
|
|
|
|
COMBINER = {
|
|
"type": "concat",
|
|
"fc_layers": [{"output_size": 64}, {"output_size": 32}],
|
|
}
|
|
|
|
TRAINER = {"epochs": 20, "learning_rate": 0.001}
|
|
|
|
|
|
def make_config(loss: dict) -> dict:
|
|
return {
|
|
"model_type": "ecd",
|
|
"input_features": INPUT_FEATURES,
|
|
"output_features": [
|
|
{
|
|
"name": "anomaly",
|
|
"type": "anomaly",
|
|
"loss": loss,
|
|
}
|
|
],
|
|
"combiner": COMBINER,
|
|
"trainer": TRAINER,
|
|
}
|
|
|
|
|
|
CONFIGS = {
|
|
"Deep SVDD": make_config({"type": "deep_svdd", "nu": 0.1}),
|
|
"Deep SAD": make_config({"type": "deep_sad", "eta": 1.0}),
|
|
"DROCC": make_config({"type": "drocc", "perturbation_strength": 0.1, "num_perturbation_steps": 5}),
|
|
}
|
|
|
|
# Deep SAD: inject ~10% labeled anomalies into the training set
|
|
N_LABELED = max(1, int(0.1 * len(train_df)))
|
|
labeled_anom = anomalous.sample(n=N_LABELED, random_state=0).copy()
|
|
labeled_anom["split"] = 0
|
|
sad_train_df = pd.concat([train_df, labeled_anom], ignore_index=True)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 3. Train and evaluate each variant
|
|
# ---------------------------------------------------------------------------
|
|
|
|
results_table = []
|
|
|
|
for method_name, config in CONFIGS.items():
|
|
print(f"\n{'=' * 60}")
|
|
print(f"Training: {method_name}")
|
|
print("=" * 60)
|
|
|
|
train_data = sad_train_df if method_name == "Deep SAD" else train_df
|
|
|
|
model = LudwigModel(config, logging_level=30) # WARNING level
|
|
train_stats, _, _ = model.train(dataset=train_data)
|
|
|
|
predictions, _ = model.predict(dataset=test_df)
|
|
|
|
score_col = "anomaly_anomaly_score_predictions"
|
|
scores = predictions[score_col].values
|
|
true_labels = test_df["anomaly"].values
|
|
|
|
auc = roc_auc_score(true_labels, scores)
|
|
|
|
# Separation ratio: mean anomaly score / mean normal score
|
|
normal_scores = scores[true_labels == 0]
|
|
anom_scores = scores[true_labels == 1]
|
|
sep_ratio = anom_scores.mean() / (normal_scores.mean() + 1e-9)
|
|
|
|
results_table.append(
|
|
{
|
|
"Method": method_name,
|
|
"AUC-ROC": round(auc, 4),
|
|
"Mean normal score": round(float(normal_scores.mean()), 4),
|
|
"Mean anomaly score": round(float(anom_scores.mean()), 4),
|
|
"Separation ratio": round(float(sep_ratio), 2),
|
|
}
|
|
)
|
|
print(f" AUC-ROC: {auc:.4f}")
|
|
print(f" Mean normal score: {normal_scores.mean():.4f}")
|
|
print(f" Mean anomaly score:{anom_scores.mean():.4f}")
|
|
print(f" Separation ratio: {sep_ratio:.2f}x")
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 4. Print summary table
|
|
# ---------------------------------------------------------------------------
|
|
|
|
results_df = pd.DataFrame(results_table)
|
|
|
|
print("\n")
|
|
print("=" * 70)
|
|
print("ANOMALY DETECTION — SUMMARY")
|
|
print("=" * 70)
|
|
print(results_df.to_string(index=False))
|
|
print("=" * 70)
|
|
print("\nHigher AUC-ROC and separation ratio indicate better discrimination between normal and anomalous samples.")
|