Reproduction of the DeepAnT / RW / RW-1 anomaly detectors from Afonso Baldo's 2025 FEUP MSc thesis "Read & Write Machine Learning for Outlier Detection in Multivariate Time Series" on the TSB-AD-M benchmark (180 files / 199 result rows), plus AutoCEGAR — a residual-CEGAR gate built on top of the reproduced RW-1.
The reproduction itself was run inside the TSB-AD repo
(/ocean/projects/cis260190p/yhwang2/TSB-AD) via benchmark_exp.Run_Detector_M.
This repo holds the reproduction-successful model code (copied here, made
self-contained) and the novel AutoCEGAR extension.
Each method folder holds the model code plus a reproduction/ subfolder
with the evidence (per-dataset & per-family summaries, the exact SLURM submit
script, and a short README with the numbers vs the paper).
rwml-autocegar/
tsb_common/ vendored TSB-AD utils so the models run standalone
utility.py get_activation_by_name
torch_utility.py get_gpu, min_memory_id, EarlyStoppingTorch
dataset.py ForecastDataset
deepant/
cnn.py DeepAnT baseline (= TSB-AD CNN.py)
reproduction/ summary_per_dataset.csv, summary_per_family.csv,
submit_cnn.sh, README.md (AUC-PR 0.350)
rw/
cnn_uns.py RW — paper Algorithm 1 (= TSB-AD CNN_uns.py)
cnn_rw.py RW-1 — paper Algorithm 2 (= TSB-AD CNN_RW.py, fixed)
reproduction/ RW summaries + submit_rw.sh + README (RW AUC-PR 0.321;
RW-1 best-HP AUC-PR 0.289)
autocegar/
scale_grad.py ScaleGrad autograd function
gate.py gate(confidence, wrongness)
controllers.py tail-ratio lambda / valley-detection tau controllers
residual_signals.py ResidualStats + E_t/C_t (PLACEHOLDER formulas)
rw_cegar.py CNN_RW_CEGAR = reproduced RW-1 + CEGAR gate
reproduction/ smoke_result.txt + README (runs-end-to-end evidence)
experiments/
exp_e_algofaithful/ RW-1 diagnostics, L1-weight sweep, analysis scripts
backup/ pre-restructure standalone track + tsb_ad_models history
(gitignored — local only; git history is the real backup)
02_2025_FEUP_MSc_Afonso_Baldo.pdf (thesis; gitignored)
| Method | Code | Paper section | Idea |
|---|---|---|---|
| DeepAnT | deepant/cnn.py |
baseline | CNN forecaster; large prediction error = anomaly |
| RW | rw/cnn_uns.py |
Algorithm 1 | correct the input by its gradient; correction size = score |
| RW-1 | rw/cnn_rw.py |
Algorithm 2 | separate correction tensor (init -X) + L1 sparsity |
| RW-2 | — (dropped) | Algorithm 3 | RW-1 + Savitzky-Golay smoothing — out of scope |
Common config (RW / RW-1): window=50, epochs=200, lr=8e-4, batch=256, correction_rate=0.1; model = Adam, correction = RMSprop, epoch-wise update.
| Method | ours AUC-PR | ours AUC-ROC | paper AUC-PR | status |
|---|---|---|---|---|
| DeepAnT (TSB-AD CNN) | 0.350 | 0.770 | 0.33 | ✅ reproduced |
| RW | 0.321 | 0.703 | 0.29 (T6.1) / 0.34 (best-HP) | ✅ reproduced (per-dataset corr 0.99) |
| RW-1 | 0.289 | 0.725 | 0.28 (T6.1) / 0.35 (best-HP) | ✅ reproduced (best-HP, n=199) |
Per-dataset / per-family numbers and the exact run scripts live in each
method's reproduction/ folder (deepant/reproduction/, rw/reproduction/,
autocegar/reproduction/).
Paper headline (Table 6.1, avg over its 17 datasets): RW 0.29 / RW-1 0.28. The 0.34 / 0.35 in Table 6.5 are best-HP-per-dataset.
The first RW-1 rewrite diverged from Algorithm 2 in two places, both fixed in
rw/cnn_rw.py:
-
ReLU gradient gating — Algorithm 2 has no activation on the correction gradient. A
relu(grad)had been added (from Table 6.5's "ReLU" row); withcorrection = -Xthe L1 gradient issign(-X) ≈ -1everywhere, soreluzeroed the gradient and froze the correction (Genesis AUC-ROC 0.137 vs paper 0.954). → defaultactivation='linear'. -
L1 penalty ~100-1000x too strong —
torch.norm(correction, p=1)sums over all elements, crushingcorrectionto 0 over 200 epochs (all points reintroduced -> anomaly signal collapses/inverts). RW-1 actually hits paper-level AUC in the first few epochs then decays. → addedl1_weight(default0.001); recovered Genesis final AUC-PR to 0.033 (paper 0.032), GECCO to 0.61 (0.62).
Because the best l1_weight is dataset-dependent, RW-1 is reproduced the way the
paper reports it — best-HP per dataset: sweep l1_weight in {1.0, 0.1, 0.01, 0.001} over all 180 files and take the best AUC-PR per dataset. See
experiments/exp_e_algofaithful/ (sweep submit + combine_rw1_besthp.py).
CNN_RW_CEGAR subclasses the reproduced RW-1 and injects a CEGAR gate into the
per-window predictive loss:
residual = y - y_hat
E_t = clamp(residual / residual_q95, 1.0) # wrongness
C_t = sigmoid(k * (tau - residual_ema)) # confidence (historical accuracy)
gate = clamp(E_t * C_t, 0, 1)
loss = mean( per_window_RMSE * (1 + lam*gate) ) + l1_weight*||correction||_1
Windows the model gets wrong while historically accurate (likely true
anomalies) are up-weighted, steering correction toward them. Init=-X, L1,
linear-activated RMSprop step and the |correction| score are inherited
unchanged from the reproduced RW-1.
PLACEHOLDER: the
E_t/C_tformulas andtau/kconstants are scaffolding pending Luis's confidence/wrongness notebook. The wiring (residual -> signals -> gate -> gated loss) is final.
import sys; sys.path.insert(0, ".") # run from repo root
from rw.cnn_rw import CNN_RW # RW-1
from autocegar import CNN_RW_CEGAR # RW-1 + CEGAR
clf = CNN_RW(window_size=50, feats=n_feats, l1_weight=0.001) # activation='linear'
scores = clf.fit(data) # data: [T, n_feats]; scores: [T]GPU smoke test: sbatch submit_autocegar_smoke.sh (runs plain RW-1 vs
RW-1+CEGAR on Genesis).
deepant/cnn.py,rw/cnn_uns.py,rw/cnn_rw.pycopied from theTSB-ADrepo models (TSB_AD/models/{CNN,CNN_uns,CNN_RW}.py), imports repointed totsb_common/.- CEGAR gate machinery (
autocegar/{scale_grad,gate,controllers,residual_signals}.py) extracted from the Auto-CEGAR repo. - The pre-restructure standalone track (old vendored DeepAnT package,
rw/trainers,cegar/,run_all_tsb.py, results,tsb_ad_models/version history) is preserved underbackup/pre_restructure_2026-07-01/.