Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
dataset/
result/
ts_benchmark/baselines/LLM/checkpoints/
ts_benchmark/baselines/pre_train/checkpoints/
.idea/
.vscode/
.git
*.DS_Store
venv/
venv/
.venv/
__pycache__/
*.pyc
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# TAB: Unified Benchmarking of Time Series Anomaly Detection Methods

[![PVLDB](https://img.shields.io/badge/PVLDB'25-TAB-orange)](https://arxiv.org/pdf/2403.20150.pdf) [![Python](https://img.shields.io/badge/Python-3.8%2B-blue)](https://www.python.org/) [![PyTorch](https://img.shields.io/badge/PyTorch-2.4.1-blue)](https://pytorch.org/) ![Stars](https://img.shields.io/github/stars/decisionintelligence/TAB)
[![PVLDB](https://img.shields.io/badge/PVLDB'25-TAB-orange)](https://arxiv.org/pdf/2403.20150.pdf) [![Python](https://img.shields.io/badge/Python-3.12-blue)](https://www.python.org/) [![PyTorch](https://img.shields.io/badge/PyTorch-2.4.1-blue)](https://pytorch.org/) ![Stars](https://img.shields.io/github/stars/decisionintelligence/TAB)


> [!IMPORTANT]
Expand Down Expand Up @@ -58,16 +58,25 @@ The table below provides a visual overview of how TAB's key features compare to

> [!IMPORTANT]
>
> this project is fully tested under python 3.8, it is recommended that you set the Python version to 3.8.
> this project supports **Python 3.12**. A plain virtual environment (`.venv`) is all you need — no conda required.

1. Installation:

Given a python environment (**note**: this project is fully tested under **python 3.8**), install the dependencies with the following command:
Create a virtual environment and install the dependencies:

```shell
python3.12 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```

> [!NOTE]
>
> All baseline families work on Python 3.12, including TODS and Merlion:
>
> - The TODS baselines (`tods.hbosski`, `tods.lofski`, ...) are implemented directly on top of `pyod` — same algorithms, with the default hyperparameters of the historical d3m primitives injected explicitly, and no dependency on the abandoned `d3m` ecosystem. Because the underlying pyod version is newer than the one frozen by d3m in 2022, individual scores may differ slightly from historical runs (validated: 7 of 9 finite AUC-ROC values reproduced within 0.001 of published results on real datasets, remaining deviations shown to pre-date this implementation). The original d3m-based implementation is kept under `ts_benchmark/baselines/tods/third_party` for Python 3.8 reproduction (see `requirements-optional.txt`). The only unavailable model is `tods.lstmodetectorski` (TensorFlow model internal to TODS, unused by the benchmark scripts).
> - The Merlion baselines (`merlion.IsolationForest`, ...) use `salesforce-merlion`, installed from `requirements.txt`. `merlion.RandomCutForest` additionally requires a Java runtime (Java 8+).

2. Data preparation

Prepare Data. You can obtain the well pre-processed datasets from [Google Drive](https://drive.google.com/file/d/1V5BAHWBKU8uih3hE1R7WdF6_crZlIbQT/view?usp=drive_link). Then place the downloaded data under the folder `./dataset`.
Expand Down
27 changes: 27 additions & 0 deletions requirements-optional.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Legacy d3m stack for the original TODS wrappers (ts_benchmark/baselines/tods/third_party).
#
# WARNING: Python 3.8 ONLY. Do NOT install this file into the Python 3.12 environment —
# these packages are incompatible with it and are NOT needed for normal use: the TODS
# baselines are implemented directly on top of pyod (see tods_models.py) and work on
# Python 3.12 out of the box.
#
# Install this file in a separate Python 3.8 virtual environment only if you need to
# run the original d3m-based wrappers, e.g. to reproduce historical results bit-for-bit:
# python3.8 -m venv .venv38 && .venv38/bin/pip install -r requirements-optional.txt
#
# Note: the only model that strictly requires this legacy stack is `tods.lstmodetectorski`
# (TensorFlow LSTM detector internal to TODS, no pyod equivalent, unused by the
# benchmark scripts).
#
# `merlion.RandomCutForest` does not need anything from this file, but requires a Java
# runtime (Java 8+ is supported).

tamu_d3m==2022.05.23
nimfa==1.4.0
combo
tensorflow
numpy==1.21.0
numba==0.55.2
scikit-learn
pandas
pyod
40 changes: 21 additions & 19 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,28 +1,30 @@
# Core dependencies — tested under Python 3.12 with a plain venv (.venv)
matplotlib>=3.6.2
numpy==1.21.0
numba==0.55.2
stumpy==1.4.0
numpy>=1.26,<2.0
numba>=0.59
stumpy>=1.12
xgboost
pandas
scikit-learn
scipy
pandas>=2.0,<3.0
scikit-learn>=1.3
scipy>=1.11
statsmodels>=0.14.0
ray>=2.6.3
ray>=2.9
tqdm>=4.64.0
dash>=2.9.3
dash-bootstrap-components>=1.5.0
reformer-pytorch==1.4.4
reformer-pytorch>=1.4.4
lightgbm>=4.1.0
tamu_d3m==2022.05.23
nimfa==1.4.0
PyWavelets>=1.1.1
combo
tensorflow
torch>=1.11.0
salesforce-merlion
PyWavelets>=1.4
torch>=2.1
timm
transformers
# LLM baselines (CALF, GPT4TS, ...) vendor GPT-2 forward code written against the
# transformers 4.x API (GenerationMixin on base models, get_head_mask, ...)
transformers>=4.40,<4.50
peft
pytorch_lightning
tslearn
rotary_embedding_torch
pytorch_lightning>=2.0
tslearn>=0.6.3
rotary_embedding_torch
einops
accelerate
pyod
salesforce-merlion
2 changes: 1 addition & 1 deletion ts_benchmark/baselines/LLM/submodules/CALF/CALF.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def __init__(self, configs, device):
self.gpt2_text.h = self.gpt2_text.h[:configs.gpt_layers]
self.gpt2 = get_peft_model(self.gpt2, peft_config)

word_embedding = torch.tensor(torch.load(configs.word_embedding_path)).to(device=device)
word_embedding = torch.tensor(torch.load(configs.word_embedding_path, weights_only=False)).to(device=device)

for i, (name, param) in enumerate(self.gpt2.named_parameters()):
if 'ln' in name or 'wpe' in name or 'lora' in name:
Expand Down
45 changes: 45 additions & 0 deletions ts_benchmark/baselines/merlion/merlion_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,51 @@
from sklearn.preprocessing import StandardScaler


def _install_java8_gateway_fallback() -> None:
"""
Merlion launches the RandomCutForest JVM with ``--add-opens`` options that only
exist on Java 9+; on Java 8 the JVM refuses to start and py4j fails with
``ValueError: invalid literal for int()``. Those options are unnecessary on
Java 8 (no module system), so retry without them when the launch fails.
"""
from os.path import abspath, dirname, join
from os import pathsep

from py4j.java_gateway import JavaGateway
import merlion.models.anomaly.random_cut_forest as rcf_module

@classmethod
def gateway(cls):
if cls._gateway is None:
resource_dir = join(
dirname(dirname(dirname(abspath(rcf_module.__file__)))), "resources"
)
jars = [
"gson-2.8.9.jar",
"randomcutforest-core-1.0.jar",
"randomcutforest-serialization-json-1.0.jar",
]
classpath = pathsep.join(join(resource_dir, jar) for jar in jars)
javaopts = [
"--add-opens=java.base/java.util=ALL-UNNAMED",
"--add-opens=java.base/java.nio=ALL-UNNAMED",
]
try:
cls._gateway = JavaGateway.launch_gateway(
classpath=classpath, javaopts=javaopts
)
except ValueError:
cls._gateway = JavaGateway.launch_gateway(
classpath=classpath, javaopts=[]
)
return cls._gateway

rcf_module.JVMSingleton.gateway = gateway


_install_java8_gateway_fallback()


class MerlionModelAdapter:
"""
Merlion model adapter class, used to adapt models in the Merlion framework to meet the requirements of prediction strategies.
Expand Down
2 changes: 1 addition & 1 deletion ts_benchmark/baselines/pre_train/model/units.py
Original file line number Diff line number Diff line change
Expand Up @@ -971,7 +971,7 @@ def __init__(
pretrain_weight_path = "ts_benchmark/baselines/pre_train/checkpoints/units/units_x32_pretrain_checkpoint.pth"

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
state_dict = torch.load(pretrain_weight_path, map_location=device)['student']
state_dict = torch.load(pretrain_weight_path, map_location=device, weights_only=False)['student']
ckpt = {}
for k, v in state_dict.items():
if not ('cls_prompts' in k):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ def __init__(self, configs):
else:
print('loading model: ', self.ckpt_path)
if self.ckpt_path.endswith('.pth'):
self.backbone.load_state_dict(torch.load(self.ckpt_path))
self.backbone.load_state_dict(torch.load(self.ckpt_path, weights_only=False))
elif self.ckpt_path.endswith('.ckpt'):
sd = torch.load(self.ckpt_path, map_location="cpu")["state_dict"]
sd = torch.load(self.ckpt_path, map_location="cpu", weights_only=False)["state_dict"]
sd = {k[6:]: v for k, v in sd.items()}
self.backbone.load_state_dict(sd, strict=True)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ def __init__(self, configs):
else:
print('loading model: ', self.ckpt_path)
if self.ckpt_path.endswith('.pth'):
self.backbone.load_state_dict(torch.load(self.ckpt_path))
self.backbone.load_state_dict(torch.load(self.ckpt_path, weights_only=False))
elif self.ckpt_path.endswith('.ckpt'):
sd = torch.load(self.ckpt_path, map_location="cpu")["state_dict"]
sd = torch.load(self.ckpt_path, map_location="cpu", weights_only=False)["state_dict"]
sd = {k[6:]: v for k, v in sd.items()}
self.backbone.load_state_dict(sd, strict=True)

Expand Down
11 changes: 7 additions & 4 deletions ts_benchmark/baselines/self_impl/LOF/lof.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ def detect_score(self, X: pd.DataFrame) -> np.ndarray:
:param X: The data of the score to be calculated.
:return: Anomaly score array.
"""
X = X.values.reshape(-1, 1)
# keep one sample per timestamp: (T, D) with channels as features
X = X.values

self.detector_ = LocalOutlierFactor(
n_neighbors=self.n_neighbors,
Expand All @@ -93,7 +94,7 @@ def detect_score(self, X: pd.DataFrame) -> np.ndarray:
.fit_transform(self.decision_scores_.reshape(-1, 1))
.ravel()
)
return score
return score, score

def detect_label(self, X: pd.DataFrame) -> np.ndarray:
"""
Expand All @@ -102,7 +103,8 @@ def detect_label(self, X: pd.DataFrame) -> np.ndarray:
:param X: The data to be tested.
:return: Anomaly label array.
"""
X = X.values.reshape(-1, 1)
# keep one sample per timestamp: (T, D) with channels as features
X = X.values

self.detector_ = LocalOutlierFactor(
n_neighbors=self.n_neighbors,
Expand All @@ -123,7 +125,8 @@ def detect_label(self, X: pd.DataFrame) -> np.ndarray:
.fit_transform(self.decision_scores_.reshape(-1, 1))
.ravel()
)
return score
preds = (score > np.percentile(score, 100 * (1 - self.contamination))).astype(int)
return preds, score

def __repr__(self) -> str:
"""
Expand Down
6 changes: 3 additions & 3 deletions ts_benchmark/baselines/self_impl/TFAD/model/fft_aug.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def seasonal_shift(

xlen = int(np.ceil(multi * x.shape[0]))
print("xlen is", xlen)
a = np.complex(0 + 0j)
a = np.complex128(0 + 0j)
fft_yn_new = a * np.arange(xlen)

if multi < 1:
Expand Down Expand Up @@ -59,7 +59,7 @@ def with_noise(
flag = np.random.randint(low=0, high=2)
xn = x.numpy()
fft_yn = fft(xn - np.mean(xn))
a = np.complex(0 + 0j)
a = np.complex128(0 + 0j)
fft_yn_new = a * np.arange(x.shape[0])

prop = np.random.uniform(0.01, 0.5)
Expand Down Expand Up @@ -98,7 +98,7 @@ def other_fftshift(
flag = np.random.randint(low=0, high=4)
xn = x.numpy()
fft_yn = fft(xn - np.mean(xn))
a = np.complex(0 + 0j)
a = np.complex128(0 + 0j)
fft_yn_new = a * np.arange(x.shape[0])

prop = np.random.uniform(0.01, 0.25)
Expand Down
5 changes: 5 additions & 0 deletions ts_benchmark/baselines/tods/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
"pcaodetectorski",
"isolationforestski",
"cblofski",
"cofski",
"autoencoderski",
]

from ts_benchmark.baselines.tods.tods_models import hbosski # noqa
Expand All @@ -19,3 +21,6 @@
from ts_benchmark.baselines.tods.tods_models import pcaodetectorski # noqa
from ts_benchmark.baselines.tods.tods_models import isolationforestski # noqa
from ts_benchmark.baselines.tods.tods_models import cblofski # noqa
from ts_benchmark.baselines.tods.tods_models import cofski # noqa
from ts_benchmark.baselines.tods.tods_models import autoencoderski # noqa
from ts_benchmark.baselines.tods.tods_models import lstmodetectorski # noqa
Loading