Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:49:20 +08:00

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.")