Lantian72 commited on
Commit
0880420
·
verified ·
1 Parent(s): 0d9eb84

Align inference API with official GitHub implementation

Browse files

Add the official TimeRCDDetector package and documentation; re-export the official univariate checkpoint for Transformers; align long-sequence normalization semantics.

.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ zero-shot.png filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ pip-wheel-metadata/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ *.manifest
32
+ *.spec
33
+
34
+ # Installer logs
35
+ pip-log.txt
36
+ pip-delete-this-directory.txt
37
+
38
+ # Unit test / coverage reports
39
+ htmlcov/
40
+ .tox/
41
+ .nox/
42
+ .coverage
43
+ .coverage.*
44
+ .cache
45
+ nosetests.xml
46
+ coverage.xml
47
+ *.cover
48
+ *.py,cover
49
+ .hypothesis/
50
+ .pytest_cache/
51
+
52
+ # Translations
53
+ *.mo
54
+ *.pot
55
+
56
+ # Django stuff:
57
+ *.log
58
+ local_settings.py
59
+ db.sqlite3
60
+ db.sqlite3-journal
61
+
62
+ # Flask stuff:
63
+ instance/
64
+ .webassets-cache
65
+
66
+ # Scrapy stuff:
67
+ .scrapy
68
+
69
+ # Sphinx documentation
70
+ docs/_build/
71
+
72
+ # PyBuilder
73
+ target/
74
+
75
+ # Jupyter Notebook
76
+ .ipynb_checkpoints
77
+
78
+ # IPython
79
+ profile_default/
80
+ ipython_config.py
81
+
82
+ # pyenv
83
+ .python-version
84
+
85
+ # pipenv
86
+ Pipfile.lock
87
+
88
+ # PEP 582
89
+ __pypackages__/
90
+
91
+ # Celery stuff
92
+ celerybeat-schedule
93
+ celerybeat.pid
94
+
95
+ # SageMath parsed files
96
+ *.sage.py
97
+
98
+ # Environments
99
+ .env
100
+ .venv
101
+ env/
102
+ venv/
103
+ ENV/
104
+ env.bak/
105
+ venv.bak/
106
+
107
+ # Spyder project settings
108
+ .spyderproject
109
+ .spyproject
110
+
111
+ # Rope project settings
112
+ .ropeproject
113
+
114
+ # mkdocs documentation
115
+ /site
116
+
117
+ # mypy
118
+ .mypy_cache/
119
+ .dmypy.json
120
+ dmypy.json
121
+
122
+ # Pyre type checker
123
+ .pyre/
124
+
125
+ # IDE
126
+ .vscode/
127
+ .idea/
128
+ *.swp
129
+ *.swo
130
+ *~
131
+
132
+ # OS
133
+ .DS_Store
134
+ Thumbs.db
135
+
136
+ datasets
137
+ checkpoints
138
+ Uni_Time_RCD
139
+ TSAD_dataset_gen-clean_version/
140
+ *.zip
141
+ *.pkl
142
+ *.csv
143
+ *.cpython-310.pyc
144
+ *.pyc
145
+ scripts/
LICENSE ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ Copyright 2025-2026 Tsinghua SAIL Lab and contributors
179
+
180
+ Licensed under the Apache License, Version 2.0 (the "License");
181
+ you may not use this file except in compliance with the License.
182
+ You may obtain a copy of the License at
183
+
184
+ http://www.apache.org/licenses/LICENSE-2.0
185
+
186
+ Unless required by applicable law or agreed to in writing, software
187
+ distributed under the License is distributed on an "AS IS" BASIS,
188
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
189
+ See the License for the specific language governing permissions and
190
+ limitations under the License.
README.md CHANGED
@@ -1,174 +1,186 @@
1
  ---
2
  library_name: transformers
 
3
  tags:
4
  - time-series
5
  - anomaly-detection
6
  - custom_code
7
  ---
8
 
 
 
 
 
9
  # Time-RCD
10
 
11
- **Official Hugging Face model repository** for [Time-RCD](https://github.com/thu-sail-lab/Time-RCD): _Towards Foundation Models for Zero-Shot Time Series Anomaly Detection: Leveraging Synthetic Data and Relative Context Discrepancy_.
 
 
 
 
12
 
13
- This repository hosts the official pretrained weights, Transformers-compatible model code, and processor for zero-shot time series anomaly detection. The full training, evaluation, and TSB-AD benchmark pipeline is maintained in the companion [official GitHub repository](https://github.com/thu-sail-lab/Time-RCD).
14
 
15
- | Resource | Link |
16
- | --- | --- |
17
- | Paper | [arXiv:2509.21190](https://arxiv.org/abs/2509.21190) |
18
- | Code | [github.com/thu-sail-lab/Time-RCD](https://github.com/thu-sail-lab/Time-RCD) |
19
- | Demo | [Hugging Face Space](https://huggingface.co/spaces/thu-sail-lab/Time_RCD) |
 
 
20
 
21
- On the TSB-AD benchmark, Time-RCD achieves a **univariate VUS-PR of 0.52** and a **multivariate VUS-PR of 0.32**.
22
 
23
- ## News
24
 
25
- - **2026.05:** Time-RCD has been accepted by **ICML 2026**. We also release the pre-trained dataset generation code and hyperparameters.
26
  - **2026.04:** With a new dataset and new checkpoints, Time-RCD achieves better results. The univariate setting improves VUS-PR by an **absolute 6.7 points**, and the multivariate setting improves VUS-PR by an **absolute 4.5 points**.
27
 
28
- ## Model Details
29
 
30
- - **Architecture:** Time-RCD with an 8-layer Transformer encoder
31
- - **Context window:** 5,000 time steps
32
- - **Patch size:** 16
33
- - **Default input features:** 1 (univariate)
34
- - **Output:** An anomaly score per time step; higher scores indicate a higher likelihood of anomaly
35
 
36
- The model normalizes each input window internally. For sequences longer than 5,000 points, use `model.zero_shot()`, which splits the input into windows and returns scores for each window.
37
 
38
- ### Repository Contents
39
 
40
- ```
41
- .
42
- ├── config.json
43
- ├── model.safetensors
44
- ├── preprocessor_config.json
45
- ├── configuration_time_rcd.py
46
- ├── modeling_time_rcd.py
47
- ├── processing_time_rcd.py
48
- ├── requirements.txt
49
- └── best_model/
50
- ├── pretrain_checkpoint_best_uni.pth
51
- └── pretrain_checkpoint_best_multi.pth
52
- ```
53
 
54
- - `model.safetensors`: Default univariate checkpoint in Safetensors format for Transformers inference.
55
- - `best_model/`: Original PyTorch checkpoints for univariate and multivariate settings.
56
 
57
- ## Installation
 
 
 
 
 
 
 
 
58
 
59
  ```bash
60
  conda create -n Time-RCD python=3.10
61
  conda activate Time-RCD
62
- pip install -r requirements.txt
 
 
 
63
  ```
64
 
65
- Or install dependencies directly:
66
 
67
  ```bash
68
- pip install "torch>=2.0.0" "transformers>=4.30.0" "numpy>=1.20.0" "scikit-learn>=1.0.0"
69
  ```
70
 
71
- ## Quick Start
72
 
73
- This repository ships custom model and processor code. Set `trust_remote_code=True` when loading.
 
 
74
 
75
- ### Long sequences (recommended)
 
 
76
 
77
  ```python
78
  import numpy as np
79
- import torch
80
- from transformers import AutoModel
81
-
82
- model_id = "thu-sail-lab/Time-RCD"
83
- device = "cuda" if torch.cuda.is_available() else "cpu"
84
 
85
- model = AutoModel.from_pretrained(model_id, trust_remote_code=True).to(device)
86
- model.eval()
87
 
88
- # Shape: (num_timesteps,) or (num_timesteps, num_features)
89
- time_series = np.random.randn(10_000).astype(np.float32)
90
-
91
- with torch.no_grad():
92
- scores_by_window, logits_by_window = model.zero_shot(time_series)
93
-
94
- anomaly_scores = np.concatenate(scores_by_window)
95
  ```
96
 
97
- ### Single window (≤ 5,000 steps)
98
 
99
  ```python
100
- import torch
101
- from transformers import AutoModel
102
-
103
- model = AutoModel.from_pretrained(
104
- "thu-sail-lab/Time-RCD",
105
- trust_remote_code=True,
106
- ).eval()
107
 
108
- time_series = torch.randn(1, 5000, 1) # (batch, seq_len, features)
109
- with torch.no_grad():
110
- outputs = model(time_series=time_series)
111
 
112
- anomaly_scores = outputs.anomaly_scores # (batch, seq_len)
 
 
 
 
113
  ```
114
 
115
- ## Download Locally
116
-
117
- Download the full official repository:
118
 
119
  ```bash
120
- hf download thu-sail-lab/Time-RCD --local-dir ./Time-RCD
121
  ```
122
 
123
- Download only the original PyTorch checkpoints:
124
 
125
- ```bash
126
- hf download thu-sail-lab/Time-RCD \
127
- --include "best_model/pretrain_checkpoint_best_uni.pth" \
128
- --local-dir ./Time-RCD
129
 
130
- hf download thu-sail-lab/Time-RCD \
131
- --include "best_model/pretrain_checkpoint_best_multi.pth" \
132
- --local-dir ./Time-RCD
133
- ```
134
 
135
- For servers in mainland China, use the Hugging Face mirror:
 
 
136
 
137
- ```bash
138
- HF_ENDPOINT=https://hf-mirror.com hf download thu-sail-lab/Time-RCD --local-dir ./Time-RCD
 
 
 
 
 
 
139
  ```
140
 
141
- ## Training and Benchmark Evaluation
 
 
 
142
 
143
- For dataset preparation, pre-training, and TSB-AD benchmark evaluation, please use the [official GitHub repository](https://github.com/thu-sail-lab/Time-RCD):
 
 
144
 
145
- ```bash
146
- git clone https://github.com/thu-sail-lab/Time-RCD.git
147
- cd Time-RCD
 
 
 
 
 
 
 
 
148
  ```
149
 
150
- - **Univariate evaluation:** `python main.py`
151
- - **Multivariate evaluation:** `python main.py --mode multi`
152
- - **Pre-training:** `python training.py --mode single --gpus 0 --num-workers 0`
153
 
154
- ## Limitations
 
 
 
155
 
156
- - The default published config uses `num_features=1`. Multivariate inference may require the multivariate checkpoint and the full pipeline in the GitHub repository.
157
- - The model outputs anomaly likelihoods, not calibrated binary labels. Choose a task-specific threshold and validate it on representative data.
158
- - This Hugging Face repository is optimized for inference via Transformers. Training scripts, benchmark datasets, and evaluation utilities live in the GitHub repository.
159
 
160
- ## Citation
161
 
162
  If you find this work useful, please cite our paper:
163
 
164
  ```bibtex
165
  @misc{lan2025foundationmodelszeroshottime,
166
- title={Towards Foundation Models for Zero-Shot Time Series Anomaly Detection: Leveraging Synthetic Data and Relative Context Discrepancy},
167
- author={Tian Lan and Hao Duong Le and Jinbo Li and Wenjun He and Meng Wang and Chenghao Liu and Chen Zhang},
168
- year={2025},
169
- eprint={2509.21190},
170
- archivePrefix={arXiv},
171
- primaryClass={cs.LG},
172
- url={https://arxiv.org/abs/2509.21190}
173
  }
174
  ```
 
1
  ---
2
  library_name: transformers
3
+ license: apache-2.0
4
  tags:
5
  - time-series
6
  - anomaly-detection
7
  - custom_code
8
  ---
9
 
10
+ > **Official model repository.** This Hugging Face repository hosts the checkpoints used by the official [Time-RCD GitHub project](https://github.com/thu-sail-lab/Time-RCD), as well as a Transformers-compatible model implementation. The recommended inference API is `TimeRCDDetector` below.
11
+
12
+ <div align="center">
13
+
14
  # Time-RCD
15
 
16
+ _Towards Foundation Models for Zero-Shot Time Series Anomaly Detection: Leveraging Synthetic Data and Relative Context Discrepancy_
17
+
18
+ [![arXiv](https://img.shields.io/badge/arXiv-2509.21190-b31b1b.svg)](https://arxiv.org/abs/2509.21190)
19
+ [![Hugging Face](https://img.shields.io/badge/🤗%20Hugging%20Face-Demo-yellow)](https://huggingface.co/spaces/thu-sail-lab/Time_RCD)
20
+ [![时空探索之旅](https://img.shields.io/badge/时空探索之旅-black?logo=wechat&logoColor=white)](https://mp.weixin.qq.com/s/79M3jsEhMKBzbNYpROOBCw)
21
 
22
+ </div>
23
 
24
+ <p align="center">
25
+ 📰&nbsp;<a href="#-news">News</a>
26
+ | 🔍&nbsp;<a href="#-about">About</a>
27
+ | 🎯&nbsp;<a href="#-use-on-your-own-data">Use on Your Own Data</a>
28
+ | 📁&nbsp;<a href="#-project-structure">Project Structure</a>
29
+ | 🔗&nbsp;<a href="#-citation">Citation</a>
30
+ </p>
31
 
32
+ ## 📰 News
33
 
34
+ - **2026.05:** Time-RCD has been accepted by **ICML 2026**. We also release the [pre-trained dataset generation code and hyperparameters](https://github.com/thu-sail-lab/TSAD_dataset_gen_public).
35
 
 
36
  - **2026.04:** With a new dataset and new checkpoints, Time-RCD achieves better results. The univariate setting improves VUS-PR by an **absolute 6.7 points**, and the multivariate setting improves VUS-PR by an **absolute 4.5 points**.
37
 
38
+ ## 🔍 About
39
 
40
+ **Time-RCD** is a zero-shot foundation model for time series anomaly detection. Given a univariate or multivariate series, it outputs a per-timestep anomaly score without any task-specific training on your data.
 
 
 
 
41
 
42
+ 🐘 On the [TSB-AD benchmark](https://thedatumorg.github.io/TSB-AD/), Time-RCD achieves a **Univariate VUS-PR of 0.52** and a **Multivariate VUS-PR of 0.32**.
43
 
44
+ **[🌟 Live Demo on Hugging Face Spaces](https://huggingface.co/spaces/thu-sail-lab/Time_RCD)** — try Time-RCD interactively in your browser.
45
 
46
+ <div align="center">
47
+ <img src="https://raw.githubusercontent.com/thu-sail-lab/Time-RCD/main/zero-shot.png" style="width:95%;" />
48
+ </div>
 
 
 
 
 
 
 
 
 
 
49
 
50
+ This repository contains:
 
51
 
52
+ 1. **`time_rcd/`** — a lightweight Python API for inference on your own data
53
+
54
+ For a step-by-step guide, see **[Tutorial.md](https://github.com/thu-sail-lab/Time-RCD/blob/main/Tutorial.md)**.
55
+
56
+ ---
57
+
58
+ ## 🎯 Use on Your Own Data
59
+
60
+ ### Installation
61
 
62
  ```bash
63
  conda create -n Time-RCD python=3.10
64
  conda activate Time-RCD
65
+
66
+ git clone https://github.com/thu-sail-lab/Time-RCD.git
67
+ cd Time-RCD
68
+ pip install .
69
  ```
70
 
71
+ When working from a local clone of this Hugging Face repository, install the same official inference package with:
72
 
73
  ```bash
74
+ pip install .
75
  ```
76
 
77
+ ### Python API (recommended)
78
 
79
+ Checkpoints are downloaded from Hugging Face automatically on first use and cached locally.
80
+ For servers in China, set `HF_ENDPOINT=https://hf-mirror.com` before running
81
+ the examples or loading a checkpoint.
82
 
83
+ ```bash
84
+ export HF_ENDPOINT=https://hf-mirror.com
85
+ ```
86
 
87
  ```python
88
  import numpy as np
89
+ from time_rcd import TimeRCDDetector
 
 
 
 
90
 
91
+ data = np.load("my_series.npy") # shape (T,) or (T, C)
 
92
 
93
+ detector = TimeRCDDetector.from_pretrained(variant="uni") # or "multi"
94
+ scores = detector.predict(data) # shape (T,)
 
 
 
 
 
95
  ```
96
 
97
+ **Multivariate series** use `variant="multi"` when `C > 1`:
98
 
99
  ```python
100
+ detector = TimeRCDDetector.from_pretrained(variant="multi")
101
+ scores = detector.predict(multivariate_data) # shape (T, C) -> scores (T,)
102
+ ```
 
 
 
 
103
 
104
+ **Local checkpoint** if you already downloaded weights:
 
 
105
 
106
+ ```python
107
+ detector = TimeRCDDetector.from_local(
108
+ "best_model/pretrain_checkpoint_best_uni.pth",
109
+ variant="uni",
110
+ )
111
  ```
112
 
113
+ ### Quick example
 
 
114
 
115
  ```bash
116
+ python examples/quickstart.py
117
  ```
118
 
119
+ See **[Tutorial.md](https://github.com/thu-sail-lab/Time-RCD/blob/main/Tutorial.md)** for CSV loading, hyperparameters, and more examples.
120
 
121
+ ### Transformers API
 
 
 
122
 
123
+ This repository also supports Transformers-based inference. The official
124
+ `TimeRCDDetector` API above is recommended, especially for multivariate data.
125
+ For univariate data, the following loads the same official `uni` checkpoint:
 
126
 
127
+ ```python
128
+ import numpy as np
129
+ from transformers import AutoModel
130
 
131
+ model = AutoModel.from_pretrained(
132
+ "thu-sail-lab/Time-RCD",
133
+ trust_remote_code=True,
134
+ ).eval()
135
+
136
+ data = np.load("my_series.npy") # shape: (T,)
137
+ score_chunks, _ = model.zero_shot(data)
138
+ scores = np.concatenate([chunk.reshape(-1) for chunk in score_chunks])[: len(data)]
139
  ```
140
 
141
+ `zero_shot()` applies the same global normalization and windowing semantics as
142
+ the official `TimeRCDDetector` inference API. The published Transformers
143
+ configuration is univariate; use `TimeRCDDetector.from_pretrained(variant="multi")`
144
+ for multivariate inference.
145
 
146
+ ---
147
+
148
+ ## 📁 Project Structure
149
 
150
+ ```
151
+ .
152
+ ├── time_rcd/ # User-facing inference API
153
+ │ ├── detector.py # TimeRCDDetector
154
+ │ └── _core/ # Time-RCD inference model implementation
155
+ ├── examples/
156
+ │ └── quickstart.py # Minimal inference example
157
+ ├── Tutorial.md # Guide for your own data
158
+ ├── pyproject.toml # Package metadata and dependencies
159
+ ├── zero-shot.png # Model overview
160
+ └── README.md
161
  ```
162
 
163
+ ### TSB-AD benchmark code
 
 
164
 
165
+ The original benchmark integration, evaluation scripts, and baseline
166
+ implementations are maintained in the
167
+ [`tsb-ad-integration`](https://github.com/thu-sail-lab/Time-RCD/tree/tsb-ad-integration)
168
+ branch. For the lightweight zero-shot inference API, use the `main` branch.
169
 
170
+ ---
 
 
171
 
172
+ ## 🔗 Citation
173
 
174
  If you find this work useful, please cite our paper:
175
 
176
  ```bibtex
177
  @misc{lan2025foundationmodelszeroshottime,
178
+ title={Towards Foundation Models for Zero-Shot Time Series Anomaly Detection: Leveraging Synthetic Data and Relative Context Discrepancy},
179
+ author={Tian Lan and Hao Duong Le and Jinbo Li and Wenjun He and Meng Wang and Chenghao Liu and Chen Zhang},
180
+ year={2025},
181
+ eprint={2509.21190},
182
+ archivePrefix={arXiv},
183
+ primaryClass={cs.LG},
184
+ url={https://arxiv.org/abs/2509.21190},
185
  }
186
  ```
Tutorial.md ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Time-RCD Tutorial
2
+
3
+ Time-RCD is a zero-shot foundation model for time series anomaly detection. Given a univariate or multivariate series, it outputs an anomaly score in `[0, 1]` for each time step. Higher scores indicate a higher likelihood of an anomaly.
4
+
5
+ ## 1. Installation
6
+
7
+ Python 3.10 is recommended:
8
+
9
+ ```bash
10
+ conda create -n Time-RCD python=3.10
11
+ conda activate Time-RCD
12
+
13
+ git clone https://github.com/thu-sail-lab/Time-RCD.git
14
+ cd Time-RCD
15
+ pip install .
16
+ ```
17
+
18
+ If you are in mainland China, set the Hugging Face mirror before the first automatic checkpoint download:
19
+
20
+ ```bash
21
+ export HF_ENDPOINT=https://hf-mirror.com
22
+ ```
23
+
24
+ This variable only affects Hugging Face downloads. Cached checkpoints and weights loaded from local paths are unaffected.
25
+
26
+ ## 2. Univariate data
27
+
28
+ Univariate input can be a NumPy array of shape `(T,)` or `(T, 1)`, where `T` is the number of time steps.
29
+
30
+ ```python
31
+ import numpy as np
32
+
33
+ from time_rcd import TimeRCDDetector
34
+
35
+ data = np.load("series.npy") # shape: (T,) or (T, 1)
36
+
37
+ detector = TimeRCDDetector.from_pretrained(variant="uni")
38
+ scores = detector.predict(data)
39
+
40
+ print(scores.shape) # (T,)
41
+ ```
42
+
43
+ ## 3. Multivariate data
44
+
45
+ Multivariate input must have shape `(T, C)` with `C > 1`. Each column is one feature or sensor channel.
46
+
47
+ ```python
48
+ import numpy as np
49
+
50
+ from time_rcd import TimeRCDDetector
51
+
52
+ data = np.load("multivariate_series.npy") # shape: (T, C), C > 1
53
+
54
+ detector = TimeRCDDetector.from_pretrained(variant="multi")
55
+ scores = detector.predict(data)
56
+
57
+ print(scores.shape) # (T,)
58
+ ```
59
+
60
+ The model is initialized on the first call to `predict()`, using the channel count `C`. If the same detector is later used on data with a different number of channels, the model is rebuilt automatically.
61
+
62
+ ## 4. Loading from CSV
63
+
64
+ Rows should be in chronological order. Timestamp columns are not model features and should be removed before inference.
65
+
66
+ ```python
67
+ import pandas as pd
68
+
69
+ from time_rcd import TimeRCDDetector
70
+
71
+ frame = pd.read_csv("sensor_data.csv")
72
+ feature_columns = ["temperature", "pressure", "flow"]
73
+ data = frame[feature_columns].to_numpy()
74
+
75
+ detector = TimeRCDDetector.from_pretrained(variant="multi")
76
+ scores = detector.predict(data)
77
+
78
+ frame["anomaly_score"] = scores
79
+ frame.to_csv("scored_sensor_data.csv", index=False)
80
+ ```
81
+
82
+ For a univariate CSV, select one numeric column and use `variant="uni"`:
83
+
84
+ ```python
85
+ data = frame["value"].to_numpy()
86
+ detector = TimeRCDDetector.from_pretrained(variant="uni")
87
+ scores = detector.predict(data)
88
+ ```
89
+
90
+ ## 5. Local checkpoints and offline inference
91
+
92
+ If checkpoints are already on disk, you can avoid network access:
93
+
94
+ ```python
95
+ from time_rcd import TimeRCDDetector
96
+
97
+ detector = TimeRCDDetector.from_local(
98
+ "best_model/pretrain_checkpoint_best_uni.pth",
99
+ variant="uni",
100
+ )
101
+ scores = detector.predict(data)
102
+ ```
103
+
104
+ To use the local cache without contacting the Hub:
105
+
106
+ ```python
107
+ detector = TimeRCDDetector.from_pretrained(
108
+ variant="uni",
109
+ local_files_only=True,
110
+ )
111
+ ```
112
+
113
+ If nothing is cached, `local_files_only=True` will fail. Download the checkpoint once while online, or use `from_local()` instead.
114
+
115
+ ## 6. Common parameters
116
+
117
+ ```python
118
+ detector = TimeRCDDetector.from_pretrained(
119
+ variant="uni",
120
+ win_size=5000,
121
+ batch_size=64,
122
+ device="cuda", # or "cpu"
123
+ )
124
+ ```
125
+
126
+ - `win_size`: Sliding window length. Default is `5000`. Shorter sequences use the full sequence length.
127
+ - `batch_size`: Inference batch size. Defaults to `64` (uni) or `1` (multi). Reduce it if you run out of GPU memory.
128
+ - `device`: When omitted, CUDA is used if available; otherwise CPU.
129
+ - `return_logits=True`: Return raw anomaly logits in addition to probability scores.
130
+
131
+ The same detector can be reused on sequences of different lengths. A short sequence does not change the window setting for later predictions.
132
+
133
+ ## 7. Interpreting anomaly scores
134
+
135
+ `scores` has one value per input time step, in `[0, 1]`. Higher values mean the model considers that time step more anomalous. Time-RCD does not ship a universal fixed threshold, because a good threshold depends on your data distribution, false-alarm cost, and expected anomaly rate.
136
+
137
+ A simple starting point is a high quantile of scores from known normal data:
138
+
139
+ ```python
140
+ import numpy as np
141
+
142
+ threshold = np.quantile(normal_scores, 0.995)
143
+ is_anomaly = scores >= threshold
144
+ ```
145
+
146
+ Calibrate the threshold on labeled validation data or known normal history. Do not deploy the example quantile directly to production without evaluation.
config.json CHANGED
@@ -5,11 +5,10 @@
5
  ],
6
  "auto_map": {
7
  "AutoConfig": "configuration_time_rcd.TimeRCDConfig",
8
- "AutoModel": "modeling_time_rcd.Time_RCD",
9
- "AutoProcessor": "processing_time_rcd.TimeRCDProcessor"
10
  },
11
  "batch_size": 64,
12
- "d_ff_dropout": 0.05,
13
  "d_model": 512,
14
  "d_proj": 256,
15
  "dropout": 0.1,
@@ -20,7 +19,7 @@
20
  "num_heads": 8,
21
  "num_layers": 8,
22
  "patch_size": 16,
23
- "transformers_version": "4.56.2",
24
  "use_rope": true,
25
  "win_size": 5000
26
  }
 
5
  ],
6
  "auto_map": {
7
  "AutoConfig": "configuration_time_rcd.TimeRCDConfig",
8
+ "AutoModel": "modeling_time_rcd.Time_RCD"
 
9
  },
10
  "batch_size": 64,
11
+ "d_ff_dropout": 0.1,
12
  "d_model": 512,
13
  "d_proj": 256,
14
  "dropout": 0.1,
 
19
  "num_heads": 8,
20
  "num_layers": 8,
21
  "patch_size": 16,
22
+ "transformers_version": "5.4.0",
23
  "use_rope": true,
24
  "win_size": 5000
25
  }
examples/quickstart.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Minimal Time-RCD inference example on synthetic data."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ import numpy as np
10
+
11
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
12
+
13
+ from time_rcd import TimeRCDDetector
14
+
15
+
16
+ def main() -> None:
17
+ rng = np.random.default_rng(42)
18
+ length = 2048
19
+ data = rng.normal(size=length)
20
+
21
+ # Inject a simple anomaly spike.
22
+ data[1000:1010] += 8.0
23
+
24
+ detector = TimeRCDDetector.from_pretrained(variant="uni")
25
+ scores = detector.predict(data)
26
+
27
+ print(f"Input shape: {data.shape}")
28
+ print(f"Score shape: {scores.shape}")
29
+ print(f"Top-5 anomaly indices: {np.argsort(scores)[-5:][::-1]}")
30
+
31
+
32
+ if __name__ == "__main__":
33
+ main()
model.safetensors CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:73a44d4a4fd9d1e47a878d1d454df30a6f597c154b39a8ced16b3c1095dcd2ac
3
  size 148240612
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0ee52a69b6f242d8178ce85910348d8e95126681ef93dc283356a8929ecb046c
3
  size 148240612
modeling_time_rcd.py CHANGED
@@ -133,6 +133,7 @@ class Time_RCD(PreTrainedModel):
133
  time_series: torch.Tensor,
134
  attention_mask: Optional[torch.Tensor] = None,
135
  return_dict: Optional[bool] = None,
 
136
  ) -> Union[Tuple, TimeRCDOutput]:
137
  """
138
  Forward pass through Time_RCD model
@@ -144,13 +145,18 @@ class Time_RCD(PreTrainedModel):
144
  Mask to avoid performing attention on padding token indices.
145
  return_dict (`bool`, *optional*):
146
  Whether to return a ModelOutput instead of a plain tuple.
 
 
 
147
  """
148
- return_dict = return_dict if return_dict is not None else self.config.use_return_dict
149
 
150
  batch_size, seq_len, num_features = time_series.shape
151
 
152
- # Normalize time series (exactly like your original)
153
- time_series = (time_series - time_series.mean(dim=1, keepdim=True)) / (time_series.std(dim=1, keepdim=True) + 1e-8)
 
 
154
 
155
  # Get embeddings from encoder
156
  embeddings = self.ts_encoder(time_series, attention_mask) # (B, seq_len, num_features, d_proj)
@@ -176,92 +182,100 @@ class Time_RCD(PreTrainedModel):
176
 
177
  def zero_shot(self, data: np.ndarray, batch_size: int = 64, win_size: int = 5000) -> tuple:
178
  """
179
- Zero-shot inference method matching AnomalyCLIP structure.
180
-
181
- The model handles normalization internally, so no external processor needed!
182
- This method only handles windowing for long sequences.
183
-
 
184
  Args:
185
  data: Input time series data of shape (n_samples, n_features) or (n_samples,)
186
  batch_size: Batch size for processing
187
- win_size: Window size for processing (only used if data > win_size)
188
-
189
  Returns:
190
- tuple: (scores, logits) where:
191
- - scores: list of anomaly score arrays per batch
192
- - logits: list of anomaly logit arrays per batch
193
  """
194
- import tqdm
195
- from torch.utils.data import DataLoader, TensorDataset
196
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  self.eval()
198
  device = next(self.parameters()).device
199
-
200
- # Ensure numpy and 2D shape
201
- data = np.asarray(data)
202
  if data.ndim == 1:
203
  data = data.reshape(-1, 1)
204
-
205
- # Adjust window size if data is too short
206
- if len(data) <= win_size:
207
- win_size = len(data)
208
-
209
- # Create windows if data is longer than win_size
210
- windows = []
211
- masks = []
212
-
213
- if len(data) > win_size:
214
- # Create non-overlapping windows
215
- for i in range(0, len(data), win_size):
216
- window = data[i:i + win_size]
217
- if len(window) < win_size:
218
- # Pad last window if needed
219
- padded = np.zeros((win_size, data.shape[1]))
220
- padded[:len(window)] = window
221
- window = padded
222
- mask = np.zeros(win_size, dtype=bool)
223
- mask[:len(window)] = True
224
- else:
225
- mask = np.ones(win_size, dtype=bool)
226
- windows.append(window)
227
- masks.append(mask)
228
- else:
229
- # Single window
230
- windows.append(data)
231
- masks.append(np.ones(len(data), dtype=bool))
232
-
233
- # Convert to tensors
234
- time_series_windows = torch.tensor(np.array(windows), dtype=torch.float32)
235
- attention_masks = torch.tensor(np.array(masks), dtype=torch.bool)
236
-
237
- # Create dataloader
238
- dataset = TensorDataset(time_series_windows, attention_masks)
239
- dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=False)
240
-
241
- loop = tqdm.tqdm(enumerate(dataloader), total=len(dataloader), leave=True)
242
  scores = []
243
  logits = []
244
-
245
  with torch.no_grad():
246
- for i, (batch_ts, batch_mask) in loop:
247
- batch_ts = batch_ts.to(device)
248
- batch_mask = batch_mask.to(device)
249
-
250
- # Forward pass (model normalizes internally!)
251
  outputs = self(
252
  time_series=batch_ts,
253
  attention_mask=batch_mask,
254
- return_dict=True
 
255
  )
256
-
257
- # Extract scores and logits
258
- anomaly_probs = outputs.anomaly_scores.cpu().numpy() # (B, seq_len)
259
- anomaly_logits = outputs.anomaly_logits # (B, seq_len, 2)
260
- logit_diff = anomaly_logits[..., 1] - anomaly_logits[..., 0] # (B, seq_len)
261
-
262
  scores.append(anomaly_probs)
263
  logits.append(logit_diff.cpu().numpy())
264
-
265
  return scores, logits
266
 
267
  @classmethod
 
133
  time_series: torch.Tensor,
134
  attention_mask: Optional[torch.Tensor] = None,
135
  return_dict: Optional[bool] = None,
136
+ normalize_input: bool = True,
137
  ) -> Union[Tuple, TimeRCDOutput]:
138
  """
139
  Forward pass through Time_RCD model
 
145
  Mask to avoid performing attention on padding token indices.
146
  return_dict (`bool`, *optional*):
147
  Whether to return a ModelOutput instead of a plain tuple.
148
+ normalize_input (`bool`, *optional*, defaults to `True`):
149
+ Whether to normalize each input window. Set this to `False`
150
+ only when the input has already been normalized globally.
151
  """
152
+ return_dict = return_dict if return_dict is not None else self.config.return_dict
153
 
154
  batch_size, seq_len, num_features = time_series.shape
155
 
156
+ if normalize_input:
157
+ time_series = (
158
+ time_series - time_series.mean(dim=1, keepdim=True)
159
+ ) / (time_series.std(dim=1, keepdim=True) + 1e-8)
160
 
161
  # Get embeddings from encoder
162
  embeddings = self.ts_encoder(time_series, attention_mask) # (B, seq_len, num_features, d_proj)
 
182
 
183
  def zero_shot(self, data: np.ndarray, batch_size: int = 64, win_size: int = 5000) -> tuple:
184
  """
185
+ Zero-shot inference for long time series.
186
+
187
+ Windowing and padding follow the official GitHub inference backend in
188
+ ``time_rcd/_inference.py``. Scores are returned in batch chunks; flatten
189
+ them with ``np.concatenate([chunk.reshape(-1) for chunk in scores])``.
190
+
191
  Args:
192
  data: Input time series data of shape (n_samples, n_features) or (n_samples,)
193
  batch_size: Batch size for processing
194
+ win_size: Sliding window length
195
+
196
  Returns:
197
+ tuple: (scores, logits) where each item is a list of numpy arrays,
198
+ one per inference batch.
 
199
  """
200
+ from torch.utils.data import DataLoader, Dataset
201
+
202
+ class _WindowDataset(Dataset):
203
+ """Split a time series into non-overlapping padded windows."""
204
+
205
+ def __init__(self, series: np.ndarray, window_size: int) -> None:
206
+ self.original_length = len(series)
207
+ mean = np.mean(series, axis=0)
208
+ std = np.where(np.std(series, axis=0) == 0, 1e-8, np.std(series, axis=0))
209
+ series = (series - mean) / std
210
+ padding = (-len(series)) % window_size
211
+ if padding:
212
+ series = np.vstack(
213
+ [series, np.repeat(series[-1:, :], padding, axis=0)]
214
+ )
215
+ self.data = series
216
+ self.window_size = window_size
217
+
218
+ def __len__(self) -> int:
219
+ return len(self.data) // self.window_size
220
+
221
+ def __getitem__(self, index: int):
222
+ start = index * self.window_size
223
+ end = start + self.window_size
224
+ valid_length = min(self.window_size, self.original_length - start)
225
+ mask = torch.zeros(self.window_size, dtype=torch.bool)
226
+ mask[:valid_length] = True
227
+ return (
228
+ torch.tensor(self.data[start:end], dtype=torch.float32),
229
+ mask,
230
+ )
231
+
232
+ def _collate_windows(batch):
233
+ time_series, attention_mask = zip(*batch)
234
+ return {
235
+ "time_series": torch.stack(time_series),
236
+ "attention_mask": torch.stack(attention_mask),
237
+ }
238
+
239
  self.eval()
240
  device = next(self.parameters()).device
241
+
242
+ data = np.asarray(data, dtype=np.float64)
 
243
  if data.ndim == 1:
244
  data = data.reshape(-1, 1)
245
+ elif data.ndim != 2:
246
+ raise ValueError(f"data must be 1D or 2D, got shape {data.shape}")
247
+
248
+ window_size = min(len(data), win_size)
249
+ dataset = _WindowDataset(data, window_size)
250
+ dataloader = DataLoader(
251
+ dataset,
252
+ batch_size=batch_size,
253
+ collate_fn=_collate_windows,
254
+ num_workers=0,
255
+ shuffle=False,
256
+ )
257
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  scores = []
259
  logits = []
260
+
261
  with torch.no_grad():
262
+ for batch in dataloader:
263
+ batch_ts = batch["time_series"].to(device)
264
+ batch_mask = batch["attention_mask"].to(device)
 
 
265
  outputs = self(
266
  time_series=batch_ts,
267
  attention_mask=batch_mask,
268
+ return_dict=True,
269
+ normalize_input=False,
270
  )
271
+
272
+ anomaly_probs = outputs.anomaly_scores.cpu().numpy()
273
+ anomaly_logits = outputs.anomaly_logits
274
+ logit_diff = anomaly_logits[..., 1] - anomaly_logits[..., 0]
275
+
 
276
  scores.append(anomaly_probs)
277
  logits.append(logit_diff.cpu().numpy())
278
+
279
  return scores, logits
280
 
281
  @classmethod
pyproject.toml ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "time-rcd"
7
+ version = "0.1.0"
8
+ description = "Zero-shot time series anomaly detection with Time-RCD"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { file = "LICENSE" }
12
+ authors = [
13
+ { name = "THU SAIL Lab" },
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Science/Research",
18
+ "License :: OSI Approved :: Apache Software License",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
24
+ ]
25
+ dependencies = [
26
+ "einops>=0.7.0",
27
+ "huggingface_hub>=0.20.0",
28
+ "jaxtyping>=0.2.25",
29
+ "numpy>=1.24.0",
30
+ "torch>=2.0.0",
31
+ ]
32
+
33
+ [project.urls]
34
+ Homepage = "https://github.com/thu-sail-lab/Time-RCD"
35
+ Repository = "https://github.com/thu-sail-lab/Time-RCD"
36
+
37
+ [tool.setuptools]
38
+ packages = ["time_rcd", "time_rcd._core"]
requirements.txt CHANGED
@@ -1,4 +1,7 @@
 
 
 
 
 
1
  torch>=2.0.0
2
  transformers>=4.30.0
3
- numpy>=1.20.0
4
- scikit-learn>=1.0.0
 
1
+ einops>=0.7.0
2
+ huggingface_hub>=0.20.0
3
+ jaxtyping>=0.2.25
4
+ numpy>=1.24.0
5
+ scikit-learn>=1.0.0
6
  torch>=2.0.0
7
  transformers>=4.30.0
 
 
time_rcd.egg-info/PKG-INFO ADDED
@@ -0,0 +1,377 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Metadata-Version: 2.4
2
+ Name: time-rcd
3
+ Version: 0.1.0
4
+ Summary: Zero-shot time series anomaly detection with Time-RCD
5
+ Author: THU SAIL Lab
6
+ License: Apache License
7
+ Version 2.0, January 2004
8
+ http://www.apache.org/licenses/
9
+
10
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
11
+
12
+ 1. Definitions.
13
+
14
+ "License" shall mean the terms and conditions for use, reproduction,
15
+ and distribution as defined by Sections 1 through 9 of this document.
16
+
17
+ "Licensor" shall mean the copyright owner or entity authorized by
18
+ the copyright owner that is granting the License.
19
+
20
+ "Legal Entity" shall mean the union of the acting entity and all
21
+ other entities that control, are controlled by, or are under common
22
+ control with that entity. For the purposes of this definition,
23
+ "control" means (i) the power, direct or indirect, to cause the
24
+ direction or management of such entity, whether by contract or
25
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
26
+ outstanding shares, or (iii) beneficial ownership of such entity.
27
+
28
+ "You" (or "Your") shall mean an individual or Legal Entity
29
+ exercising permissions granted by this License.
30
+
31
+ "Source" form shall mean the preferred form for making modifications,
32
+ including but not limited to software source code, documentation
33
+ source, and configuration files.
34
+
35
+ "Object" form shall mean any form resulting from mechanical
36
+ transformation or translation of a Source form, including but
37
+ not limited to compiled object code, generated documentation,
38
+ and conversions to other media types.
39
+
40
+ "Work" shall mean the work of authorship, whether in Source or
41
+ Object form, made available under the License, as indicated by a
42
+ copyright notice that is included in or attached to the work
43
+ (an example is provided in the Appendix below).
44
+
45
+ "Derivative Works" shall mean any work, whether in Source or Object
46
+ form, that is based on (or derived from) the Work and for which the
47
+ editorial revisions, annotations, elaborations, or other modifications
48
+ represent, as a whole, an original work of authorship. For the purposes
49
+ of this License, Derivative Works shall not include works that remain
50
+ separable from, or merely link (or bind by name) to the interfaces of,
51
+ the Work and Derivative Works thereof.
52
+
53
+ "Contribution" shall mean any work of authorship, including
54
+ the original version of the Work and any modifications or additions
55
+ to that Work or Derivative Works thereof, that is intentionally
56
+ submitted to Licensor for inclusion in the Work by the copyright owner
57
+ or by an individual or Legal Entity authorized to submit on behalf of
58
+ the copyright owner. For the purposes of this definition, "submitted"
59
+ means any form of electronic, verbal, or written communication sent
60
+ to the Licensor or its representatives, including but not limited to
61
+ communication on electronic mailing lists, source code control systems,
62
+ and issue tracking systems that are managed by, or on behalf of, the
63
+ Licensor for the purpose of discussing and improving the Work, but
64
+ excluding communication that is conspicuously marked or otherwise
65
+ designated in writing by the copyright owner as "Not a Contribution."
66
+
67
+ "Contributor" shall mean Licensor and any individual or Legal Entity
68
+ on behalf of whom a Contribution has been received by Licensor and
69
+ subsequently incorporated within the Work.
70
+
71
+ 2. Grant of Copyright License. Subject to the terms and conditions of
72
+ this License, each Contributor hereby grants to You a perpetual,
73
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
74
+ copyright license to reproduce, prepare Derivative Works of,
75
+ publicly display, publicly perform, sublicense, and distribute the
76
+ Work and such Derivative Works in Source or Object form.
77
+
78
+ 3. Grant of Patent License. Subject to the terms and conditions of
79
+ this License, each Contributor hereby grants to You a perpetual,
80
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
81
+ (except as stated in this section) patent license to make, have made,
82
+ use, offer to sell, sell, import, and otherwise transfer the Work,
83
+ where such license applies only to those patent claims licensable
84
+ by such Contributor that are necessarily infringed by their
85
+ Contribution(s) alone or by combination of their Contribution(s)
86
+ with the Work to which such Contribution(s) was submitted. If You
87
+ institute patent litigation against any entity (including a
88
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
89
+ or a Contribution incorporated within the Work constitutes direct
90
+ or contributory patent infringement, then any patent licenses
91
+ granted to You under this License for that Work shall terminate
92
+ as of the date such litigation is filed.
93
+
94
+ 4. Redistribution. You may reproduce and distribute copies of the
95
+ Work or Derivative Works thereof in any medium, with or without
96
+ modifications, and in Source or Object form, provided that You
97
+ meet the following conditions:
98
+
99
+ (a) You must give any other recipients of the Work or
100
+ Derivative Works a copy of this License; and
101
+
102
+ (b) You must cause any modified files to carry prominent notices
103
+ stating that You changed the files; and
104
+
105
+ (c) You must retain, in the Source form of any Derivative Works
106
+ that You distribute, all copyright, patent, trademark, and
107
+ attribution notices from the Source form of the Work,
108
+ excluding those notices that do not pertain to any part of
109
+ the Derivative Works; and
110
+
111
+ (d) If the Work includes a "NOTICE" text file as part of its
112
+ distribution, then any Derivative Works that You distribute must
113
+ include a readable copy of the attribution notices contained
114
+ within such NOTICE file, excluding those notices that do not
115
+ pertain to any part of the Derivative Works, in at least one
116
+ of the following places: within a NOTICE text file distributed
117
+ as part of the Derivative Works; within the Source form or
118
+ documentation, if provided along with the Derivative Works; or,
119
+ within a display generated by the Derivative Works, if and
120
+ wherever such third-party notices normally appear. The contents
121
+ of the NOTICE file are for informational purposes only and
122
+ do not modify the License. You may add Your own attribution
123
+ notices within Derivative Works that You distribute, alongside
124
+ or as an addendum to the NOTICE text from the Work, provided
125
+ that such additional attribution notices cannot be construed
126
+ as modifying the License.
127
+
128
+ You may add Your own copyright statement to Your modifications and
129
+ may provide additional or different license terms and conditions
130
+ for use, reproduction, or distribution of Your modifications, or
131
+ for any such Derivative Works as a whole, provided Your use,
132
+ reproduction, and distribution of the Work otherwise complies with
133
+ the conditions stated in this License.
134
+
135
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
136
+ any Contribution intentionally submitted for inclusion in the Work
137
+ by You to the Licensor shall be under the terms and conditions of
138
+ this License, without any additional terms or conditions.
139
+ Notwithstanding the above, nothing herein shall supersede or modify
140
+ the terms of any separate license agreement you may have executed
141
+ with Licensor regarding such Contributions.
142
+
143
+ 6. Trademarks. This License does not grant permission to use the trade
144
+ names, trademarks, service marks, or product names of the Licensor,
145
+ except as required for reasonable and customary use in describing the
146
+ origin of the Work and reproducing the content of the NOTICE file.
147
+
148
+ 7. Disclaimer of Warranty. Unless required by applicable law or
149
+ agreed to in writing, Licensor provides the Work (and each
150
+ Contributor provides its Contributions) on an "AS IS" BASIS,
151
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
152
+ implied, including, without limitation, any warranties or conditions
153
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
154
+ PARTICULAR PURPOSE. You are solely responsible for determining the
155
+ appropriateness of using or redistributing the Work and assume any
156
+ risks associated with Your exercise of permissions under this License.
157
+
158
+ 8. Limitation of Liability. In no event and under no legal theory,
159
+ whether in tort (including negligence), contract, or otherwise,
160
+ unless required by applicable law (such as deliberate and grossly
161
+ negligent acts) or agreed to in writing, shall any Contributor be
162
+ liable to You for damages, including any direct, indirect, special,
163
+ incidental, or consequential damages of any character arising as a
164
+ result of this License or out of the use or inability to use the
165
+ Work (including but not limited to damages for loss of goodwill,
166
+ work stoppage, computer failure or malfunction, or any and all
167
+ other commercial damages or losses), even if such Contributor
168
+ has been advised of the possibility of such damages.
169
+
170
+ 9. Accepting Warranty or Additional Liability. While redistributing
171
+ the Work or Derivative Works thereof, You may choose to offer,
172
+ and charge a fee for, acceptance of support, warranty, indemnity,
173
+ or other liability obligations and/or rights consistent with this
174
+ License. However, in accepting such obligations, You may act only
175
+ on Your own behalf and on Your sole responsibility, not on behalf
176
+ of any other Contributor, and only if You agree to indemnify,
177
+ defend, and hold each Contributor harmless for any liability
178
+ incurred by, or claims asserted against, such Contributor by reason
179
+ of your accepting any such warranty or additional liability.
180
+
181
+ END OF TERMS AND CONDITIONS
182
+
183
+ Copyright 2025-2026 Tsinghua SAIL Lab and contributors
184
+
185
+ Licensed under the Apache License, Version 2.0 (the "License");
186
+ you may not use this file except in compliance with the License.
187
+ You may obtain a copy of the License at
188
+
189
+ http://www.apache.org/licenses/LICENSE-2.0
190
+
191
+ Unless required by applicable law or agreed to in writing, software
192
+ distributed under the License is distributed on an "AS IS" BASIS,
193
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
194
+ See the License for the specific language governing permissions and
195
+ limitations under the License.
196
+
197
+ Project-URL: Homepage, https://github.com/thu-sail-lab/Time-RCD
198
+ Project-URL: Repository, https://github.com/thu-sail-lab/Time-RCD
199
+ Classifier: Development Status :: 4 - Beta
200
+ Classifier: Intended Audience :: Science/Research
201
+ Classifier: License :: OSI Approved :: Apache Software License
202
+ Classifier: Programming Language :: Python :: 3
203
+ Classifier: Programming Language :: Python :: 3.10
204
+ Classifier: Programming Language :: Python :: 3.11
205
+ Classifier: Programming Language :: Python :: 3.12
206
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
207
+ Requires-Python: >=3.10
208
+ Description-Content-Type: text/markdown
209
+ License-File: LICENSE
210
+ Requires-Dist: einops>=0.7.0
211
+ Requires-Dist: huggingface_hub>=0.20.0
212
+ Requires-Dist: jaxtyping>=0.2.25
213
+ Requires-Dist: numpy>=1.24.0
214
+ Requires-Dist: torch>=2.0.0
215
+ Dynamic: license-file
216
+
217
+ ---
218
+ library_name: transformers
219
+ license: apache-2.0
220
+ tags:
221
+ - time-series
222
+ - anomaly-detection
223
+ - custom_code
224
+ ---
225
+
226
+ > **Official model repository.** This Hugging Face repository hosts the checkpoints used by the official [Time-RCD GitHub project](https://github.com/thu-sail-lab/Time-RCD), as well as a Transformers-compatible model implementation. The recommended inference API is `TimeRCDDetector` below.
227
+
228
+ <div align="center">
229
+
230
+ # Time-RCD
231
+
232
+ _Towards Foundation Models for Zero-Shot Time Series Anomaly Detection: Leveraging Synthetic Data and Relative Context Discrepancy_
233
+
234
+ [![arXiv](https://img.shields.io/badge/arXiv-2509.21190-b31b1b.svg)](https://arxiv.org/abs/2509.21190)
235
+ [![Hugging Face](https://img.shields.io/badge/🤗%20Hugging%20Face-Demo-yellow)](https://huggingface.co/spaces/thu-sail-lab/Time_RCD)
236
+ [![时空探索之旅](https://img.shields.io/badge/时空探索之旅-black?logo=wechat&logoColor=white)](https://mp.weixin.qq.com/s/79M3jsEhMKBzbNYpROOBCw)
237
+
238
+ </div>
239
+
240
+ <p align="center">
241
+ 📰&nbsp;<a href="#-news">News</a>
242
+ | 🔍&nbsp;<a href="#-about">About</a>
243
+ | 🎯&nbsp;<a href="#-use-on-your-own-data">Use on Your Own Data</a>
244
+ | 📁&nbsp;<a href="#-project-structure">Project Structure</a>
245
+ | 🔗&nbsp;<a href="#-citation">Citation</a>
246
+ </p>
247
+
248
+ ## 📰 News
249
+
250
+ - **2026.05:** Time-RCD has been accepted by **ICML 2026**. We also release the [pre-trained dataset generation code and hyperparameters](https://github.com/thu-sail-lab/TSAD_dataset_gen_public).
251
+
252
+ - **2026.04:** With a new dataset and new checkpoints, Time-RCD achieves better results. The univariate setting improves VUS-PR by an **absolute 6.7 points**, and the multivariate setting improves VUS-PR by an **absolute 4.5 points**.
253
+
254
+ ## 🔍 About
255
+
256
+ **Time-RCD** is a zero-shot foundation model for time series anomaly detection. Given a univariate or multivariate series, it outputs a per-timestep anomaly score without any task-specific training on your data.
257
+
258
+ 🐘 On the [TSB-AD benchmark](https://thedatumorg.github.io/TSB-AD/), Time-RCD achieves a **Univariate VUS-PR of 0.52** and a **Multivariate VUS-PR of 0.32**.
259
+
260
+ **[🌟 Live Demo on Hugging Face Spaces](https://huggingface.co/spaces/thu-sail-lab/Time_RCD)** — try Time-RCD interactively in your browser.
261
+
262
+ <div align="center">
263
+ <img src="https://raw.githubusercontent.com/thu-sail-lab/Time-RCD/main/zero-shot.png" style="width:95%;" />
264
+ </div>
265
+
266
+ This repository contains:
267
+
268
+ 1. **`time_rcd/`** — a lightweight Python API for inference on your own data
269
+
270
+ For a step-by-step guide, see **[Tutorial.md](https://github.com/thu-sail-lab/Time-RCD/blob/main/Tutorial.md)**.
271
+
272
+ ---
273
+
274
+ ## 🎯 Use on Your Own Data
275
+
276
+ ### Installation
277
+
278
+ ```bash
279
+ conda create -n Time-RCD python=3.10
280
+ conda activate Time-RCD
281
+
282
+ git clone https://github.com/thu-sail-lab/Time-RCD.git
283
+ cd Time-RCD
284
+ pip install .
285
+ ```
286
+
287
+ When working from a local clone of this Hugging Face repository, install the same official inference package with:
288
+
289
+ ```bash
290
+ pip install .
291
+ ```
292
+
293
+ ### Python API (recommended)
294
+
295
+ Checkpoints are downloaded from Hugging Face automatically on first use and cached locally.
296
+ For servers in China, set `HF_ENDPOINT=https://hf-mirror.com` before running
297
+ the examples or loading a checkpoint.
298
+
299
+ ```bash
300
+ export HF_ENDPOINT=https://hf-mirror.com
301
+ ```
302
+
303
+ ```python
304
+ import numpy as np
305
+ from time_rcd import TimeRCDDetector
306
+
307
+ data = np.load("my_series.npy") # shape (T,) or (T, C)
308
+
309
+ detector = TimeRCDDetector.from_pretrained(variant="uni") # or "multi"
310
+ scores = detector.predict(data) # shape (T,)
311
+ ```
312
+
313
+ **Multivariate series** — use `variant="multi"` when `C > 1`:
314
+
315
+ ```python
316
+ detector = TimeRCDDetector.from_pretrained(variant="multi")
317
+ scores = detector.predict(multivariate_data) # shape (T, C) -> scores (T,)
318
+ ```
319
+
320
+ **Local checkpoint** — if you already downloaded weights:
321
+
322
+ ```python
323
+ detector = TimeRCDDetector.from_local(
324
+ "best_model/pretrain_checkpoint_best_uni.pth",
325
+ variant="uni",
326
+ )
327
+ ```
328
+
329
+ ### Quick example
330
+
331
+ ```bash
332
+ python examples/quickstart.py
333
+ ```
334
+
335
+ See **[Tutorial.md](https://github.com/thu-sail-lab/Time-RCD/blob/main/Tutorial.md)** for CSV loading, hyperparameters, and more examples.
336
+
337
+ ---
338
+
339
+ ## 📁 Project Structure
340
+
341
+ ```
342
+ .
343
+ ├── time_rcd/ # User-facing inference API
344
+ │ ├── detector.py # TimeRCDDetector
345
+ │ └── _core/ # Time-RCD inference model implementation
346
+ ├── examples/
347
+ │ └── quickstart.py # Minimal inference example
348
+ ├── Tutorial.md # Guide for your own data
349
+ ├── pyproject.toml # Package metadata and dependencies
350
+ ├── zero-shot.png # Model overview
351
+ └── README.md
352
+ ```
353
+
354
+ ### TSB-AD benchmark code
355
+
356
+ The original benchmark integration, evaluation scripts, and baseline
357
+ implementations are maintained in the
358
+ [`tsb-ad-integration`](https://github.com/thu-sail-lab/Time-RCD/tree/tsb-ad-integration)
359
+ branch. For the lightweight zero-shot inference API, use the `main` branch.
360
+
361
+ ---
362
+
363
+ ## 🔗 Citation
364
+
365
+ If you find this work useful, please cite our paper:
366
+
367
+ ```bibtex
368
+ @misc{lan2025foundationmodelszeroshottime,
369
+ title={Towards Foundation Models for Zero-Shot Time Series Anomaly Detection: Leveraging Synthetic Data and Relative Context Discrepancy},
370
+ author={Tian Lan and Hao Duong Le and Jinbo Li and Wenjun He and Meng Wang and Chenghao Liu and Chen Zhang},
371
+ year={2025},
372
+ eprint={2509.21190},
373
+ archivePrefix={arXiv},
374
+ primaryClass={cs.LG},
375
+ url={https://arxiv.org/abs/2509.21190},
376
+ }
377
+ ```
time_rcd.egg-info/SOURCES.txt ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ time_rcd/__init__.py
5
+ time_rcd/_inference.py
6
+ time_rcd/detector.py
7
+ time_rcd.egg-info/PKG-INFO
8
+ time_rcd.egg-info/SOURCES.txt
9
+ time_rcd.egg-info/dependency_links.txt
10
+ time_rcd.egg-info/requires.txt
11
+ time_rcd.egg-info/top_level.txt
12
+ time_rcd/_core/TimeRCD_pretrain_multi.py
13
+ time_rcd/_core/__init__.py
14
+ time_rcd/_core/dataset.py
15
+ time_rcd/_core/full_reconstruction.py
16
+ time_rcd/_core/time_rcd_config.py
17
+ time_rcd/_core/ts_encoder_bi_bias.py
time_rcd.egg-info/dependency_links.txt ADDED
@@ -0,0 +1 @@
 
 
1
+
time_rcd.egg-info/requires.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ einops>=0.7.0
2
+ huggingface_hub>=0.20.0
3
+ jaxtyping>=0.2.25
4
+ numpy>=1.24.0
5
+ torch>=2.0.0
time_rcd.egg-info/top_level.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ time_rcd
time_rcd/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from time_rcd.detector import TimeRCDDetector
2
+
3
+ __all__ = ["TimeRCDDetector"]
time_rcd/_core/TimeRCD_pretrain_multi.py ADDED
@@ -0,0 +1,236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import datetime
2
+ import itertools
3
+ import os
4
+ import torch
5
+ import torch.nn as nn
6
+ import torch.optim as optim
7
+ from torch.utils.data import DataLoader, DistributedSampler
8
+ import torch.nn.functional as F
9
+ import torch.distributed as dist
10
+ import torch.multiprocessing as mp
11
+ from torch.nn.parallel import DistributedDataParallel as DDP
12
+ import random
13
+ import numpy as np
14
+ from typing import Tuple, List, Dict, Any, Union, Optional
15
+ from dataclasses import dataclass
16
+
17
+ from .dataset import ChatTSTimeRCDPretrainDataset
18
+ from .ts_encoder_bi_bias import TimeSeriesEncoder
19
+ from .time_rcd_config import TimeRCDConfig, default_config
20
+
21
+ import warnings
22
+
23
+ warnings.filterwarnings("ignore")
24
+
25
+ @dataclass
26
+ class PretrainBatch:
27
+ """Batch structure for pretraining tasks."""
28
+ time_series: torch.Tensor
29
+ labels: torch.Tensor
30
+ masked_time_series: torch.Tensor
31
+ mask_indices: torch.Tensor
32
+
33
+
34
+ class TimeSeriesPretrainModel(nn.Module):
35
+ """Model for time series pretraining with masked reconstruction and anomaly detection."""
36
+
37
+ def __init__(self, config: TimeRCDConfig):
38
+ super().__init__()
39
+ self.config = config
40
+
41
+ # Extract TimeSeriesEncoder parameters from config
42
+ ts_config = config.ts_config
43
+ self.ts_encoder = TimeSeriesEncoder(
44
+ d_model=ts_config.d_model,
45
+ d_proj=ts_config.d_proj,
46
+ patch_size=ts_config.patch_size,
47
+ num_layers=ts_config.num_layers,
48
+ num_heads=ts_config.num_heads,
49
+ d_ff_dropout=ts_config.d_ff_dropout,
50
+ use_rope=ts_config.use_rope,
51
+ num_features=ts_config.num_features,
52
+ activation=ts_config.activation
53
+ )
54
+
55
+ # Masked reconstruction head
56
+ self.reconstruction_head = nn.Sequential(
57
+ nn.Linear(config.ts_config.d_proj, config.ts_config.d_proj * 4),
58
+ nn.GELU(),
59
+ nn.Dropout(config.dropout),
60
+ nn.Linear(config.ts_config.d_proj * 4, config.ts_config.d_proj * 4),
61
+ nn.GELU(),
62
+ nn.Dropout(config.dropout),
63
+ nn.Linear(config.ts_config.d_proj * 4, 1) # (B, seq_len, num_features, 1)
64
+ )
65
+
66
+ # Anomaly detection head
67
+ self.anomaly_head = nn.Sequential(
68
+ nn.Linear(config.ts_config.d_proj, config.ts_config.d_proj // 2),
69
+ nn.GELU(),
70
+ nn.Dropout(config.dropout),
71
+ nn.Linear(config.ts_config.d_proj // 2, 2) # (B, seq_len, num_features, 2) for binary classification
72
+ )
73
+
74
+ def forward(self, time_series: torch.Tensor, mask: Optional[torch.Tensor] = None):
75
+ """Forward pass through the encoder."""
76
+ local_embeddings = self.ts_encoder(time_series, mask)
77
+ return local_embeddings
78
+
79
+ def masked_reconstruction_loss(self,
80
+ local_embeddings: torch.Tensor, # (B, seq_len, num_features, d_proj)
81
+ original_time_series: torch.Tensor, # (B, seq_len, num_features),
82
+ mask: torch.Tensor # (B, seq_len)
83
+ ) -> torch.Tensor:
84
+ """Compute masked reconstruction loss."""
85
+ batch_size, seq_len, num_features = original_time_series.shape
86
+ patch_size = self.config.ts_config.patch_size
87
+
88
+ mask = mask.bool()
89
+
90
+ # local_embeddings: [B, seq_len, num_features, d_proj]
91
+ reconstructed = self.reconstruction_head(local_embeddings) # (B, seq_len, num_features, 1)
92
+ reconstructed = reconstructed.view(batch_size, seq_len, num_features)
93
+
94
+ mask_expanded = mask.unsqueeze(-1).expand(-1, -1, num_features) # (B, seq_len, num_features)
95
+ reconstruction_loss = F.mse_loss(
96
+ reconstructed[mask_expanded],
97
+ original_time_series[mask_expanded]
98
+ )
99
+ return reconstruction_loss
100
+
101
+ def anomaly_detection_loss(self,
102
+ local_embeddings: torch.Tensor, # (B, seq_len, num_features, d_proj)
103
+ labels: torch.Tensor) -> torch.Tensor: # (B, seq_len)
104
+ """Compute anomaly detection loss for each timestep."""
105
+ # Project local embeddings to anomaly scores
106
+ logits = self.anomaly_head(local_embeddings) # (B, seq_len, num_features, 2)
107
+ logits = torch.mean(logits, dim=-2) # Average over num_features to get (B, seq_len, 2)
108
+
109
+ # Reshape for loss computation
110
+ batch_size, seq_len, _ = logits.shape
111
+ logits = logits.view(-1, 2) # (B*seq_len, 2)
112
+ labels = labels.view(-1) # (B*seq_len)
113
+ labels = (labels > 0.5).long()
114
+ # Create mask for valid labels (not padding)
115
+ valid_mask = (labels != -1)
116
+
117
+ # Compute loss only on valid timesteps
118
+ if valid_mask.sum() > 0:
119
+ anomaly_loss = F.cross_entropy(
120
+ logits[valid_mask],
121
+ labels[valid_mask]
122
+ )
123
+ else:
124
+ anomaly_loss = torch.tensor(0.0, device=logits.device)
125
+
126
+ return anomaly_loss
127
+
128
+
129
+ def create_random_mask(time_series: torch.Tensor, # (B, max_seq_len, num_features)
130
+ attention_mask: torch.Tensor, # (B, max_seq_len)
131
+ mask_ratio: float = 0.15) -> Tuple[torch.Tensor, torch.Tensor]:
132
+ """Create random mask for time series patches, only masking valid sequence parts."""
133
+ batch_size, seq_len, num_features = time_series.shape
134
+ patch_size = default_config.ts_config.patch_size
135
+
136
+ mask = torch.zeros(batch_size, seq_len) # (B, max_seq_len)
137
+
138
+ for i in range(batch_size):
139
+ # Get valid sequence length for this sample
140
+ valid_length = attention_mask[i].sum().item()
141
+
142
+ # Calculate number of patches in valid sequence
143
+ num_valid_patches = (valid_length - 1) // patch_size + 1
144
+ num_masked = int(num_valid_patches * mask_ratio)
145
+
146
+ if num_masked > 0:
147
+ # Only select patches from valid sequence
148
+ masked_patches = torch.randperm(num_valid_patches)[:num_masked]
149
+ for j in masked_patches:
150
+ start_idx = j * patch_size
151
+ end_idx = min((j + 1) * patch_size, valid_length) # Don't exceed valid length
152
+ mask[i, start_idx:end_idx] = 1
153
+
154
+ # Create masked time series - only mask valid parts
155
+ masked_time_series = time_series.clone()
156
+ mask_indices = mask.bool() & attention_mask # Only mask where both mask and attention_mask are True
157
+ mask_expanded = mask_indices.unsqueeze(-1).expand(-1, -1, num_features) # (B, max_seq_len, num_features)
158
+ masked_time_series[mask_expanded] = torch.randn_like(masked_time_series[mask_expanded]) * 0.1
159
+
160
+ # Update mask to only include valid parts
161
+ mask = mask * attention_mask.float()
162
+
163
+ return masked_time_series, mask # (B, max_seq_len, num_features), (B, max_seq_len)
164
+
165
+
166
+ def collate_fn(batch):
167
+ """Collate function for pretraining dataset."""
168
+ time_series_list, normal_time_series_list, labels_list, attribute_list = zip(*batch)
169
+
170
+ # Convert to tensors and pad sequences
171
+ if time_series_list[0].ndim == 1:
172
+ time_series_tensors = [ts.unsqueeze(-1) for ts in time_series_list] # Add feature dimension
173
+ normal_time_series_tensors = [nts.unsqueeze(-1) for nts in normal_time_series_list]
174
+ else:
175
+ time_series_tensors = [ts for ts in time_series_list]
176
+ normal_time_series_tensors = [nts for nts in normal_time_series_list]
177
+
178
+ # standardize time series
179
+ concatenated = torch.cat(time_series_tensors, dim=0) # (total_length, num_features)
180
+ mean = concatenated.mean(dim=0, keepdim=True) # (1, num_features)
181
+ std = concatenated.std(dim=0, keepdim=True) # (1, num_features)
182
+ std = std + 1e-4
183
+ time_series_tensors_std = [(ts - mean) / std for ts in time_series_tensors]
184
+ normal_time_series_tensors_std = [(nts - mean) / std for nts in normal_time_series_tensors]
185
+ time_series_tensors = time_series_tensors_std
186
+ normal_time_series_tensors = normal_time_series_tensors_std
187
+
188
+ # labels_tensor = torch.stack(labels_list)
189
+ labels = [label for label in labels_list]
190
+ # Pad time series to same length
191
+ padded_time_series = torch.nn.utils.rnn.pad_sequence(
192
+ time_series_tensors, batch_first=True, padding_value=0.0
193
+ ) # (B, max_seq_len, num_features)
194
+ padded_normal_time_series = torch.nn.utils.rnn.pad_sequence(
195
+ normal_time_series_tensors, batch_first=True, padding_value=0.0
196
+ ) # (B, max_seq_len, num_features)
197
+ padded_labels = torch.nn.utils.rnn.pad_sequence(
198
+ labels, batch_first=True, padding_value=-1
199
+ ) # (B, max_seq_len)
200
+
201
+ sequence_lengths = [ts.size(0) for ts in time_series_tensors]
202
+ B, max_seq_len, num_features = padded_time_series.shape
203
+ attention_mask = torch.zeros(B, max_seq_len, dtype=torch.bool) # (B, max_seq_len)
204
+ for i, length in enumerate(sequence_lengths):
205
+ attention_mask[i, :length] = True
206
+
207
+ # Create random masks for reconstruction task - only mask valid sequence parts
208
+ masked_time_series, mask = create_random_mask(padded_time_series, attention_mask)
209
+
210
+ return {
211
+ 'time_series': padded_time_series,
212
+ 'normal_time_series': padded_normal_time_series,
213
+ 'masked_time_series': masked_time_series,
214
+ 'mask': mask, # for reconstruction task
215
+ 'labels': padded_labels,
216
+ 'attention_mask': attention_mask, # for padding
217
+ 'attribute': attribute_list
218
+ }
219
+
220
+ def test_collate_fn(batch):
221
+ """Collate function for pretraining dataset."""
222
+ # Unpack the batch correctly - batch is a list of (time_series, mask) tuples
223
+ time_series_list, mask_list = zip(*batch)
224
+
225
+ # Stack into batch format instead of concatenating
226
+ # This maintains the batch dimension: (B, seq_len, num_features)
227
+ batched_time_series = torch.stack(time_series_list, dim=0)
228
+ print(f"batched_time_series shape: {batched_time_series.shape}")
229
+ # Stack masks into batch format: (B, seq_len)
230
+ batched_mask = torch.stack(mask_list, dim=0)
231
+ print(f"batched_mask shape: {batched_mask.shape}")
232
+
233
+ return {
234
+ 'time_series': batched_time_series,
235
+ 'attention_mask': batched_mask, # for padding
236
+ }
time_rcd/_core/__init__.py ADDED
File without changes
time_rcd/_core/dataset.py ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import numpy as np
3
+ import torch
4
+ from torch.utils.data import Dataset
5
+ import random
6
+ import os
7
+ import pickle
8
+ from typing import Dict, List, Union, Optional, Tuple
9
+ from pathlib import Path
10
+
11
+
12
+ class ChatTSTimeRCDPretrainDataset(Dataset):
13
+ def __init__(self,
14
+ dataset_dir: str,
15
+ filename: str,
16
+ split: str = 'train',
17
+ train_ratio: float = 0.95,
18
+ seed: int = 42):
19
+ file_path = os.path.join(dataset_dir, filename)
20
+ with open(file_path, 'rb') as f:
21
+ dataset = pickle.load(f)
22
+ random.seed(seed)
23
+ indices = list(range(len(dataset)))
24
+ random.shuffle(indices)
25
+ num_train = int(len(dataset) * train_ratio)
26
+ if split == 'train':
27
+ selected_indices = indices[:num_train]
28
+ elif split == 'test':
29
+ selected_indices = indices[num_train:]
30
+ else:
31
+ raise ValueError("split must be 'train' or 'test'")
32
+ self.data = [dataset[i] for i in selected_indices]
33
+
34
+ def __len__(self):
35
+ return len(self.data)
36
+
37
+ def __getitem__(self, idx):
38
+ sample = self.data[idx]
39
+ time_series = torch.tensor(sample['time_series'], dtype=torch.float32)
40
+ normal_time_series = torch.tensor(sample['normal_time_series'], dtype=torch.float32)
41
+ labels = torch.tensor(sample['labels'], dtype=torch.long)
42
+ attribute = sample['attribute']
43
+ return time_series, normal_time_series, labels, attribute
44
+
45
+
46
+ class ChatTSTimeRCDQADataset(Dataset):
47
+ """Dataset class for time series anomaly detection with QA pairs.
48
+
49
+ This dataset loads time series data and corresponding question-answer pairs
50
+ for anomaly detection tasks. It supports train/val split and efficient loading
51
+ of series data from the time_rcd_datasets format.
52
+
53
+ Attributes:
54
+ split (str): Dataset split, either 'train' or 'val'
55
+ series_dir (Path): Directory containing series JSON files
56
+ metadata (Dict): Dataset metadata loaded from metadata.json
57
+ series_files (List[str]): List of series file paths
58
+ window_size_range (Tuple[int, int]): Range of window sizes used in the dataset
59
+ """
60
+
61
+ def __init__(
62
+ self,
63
+ dataset_dir: str,
64
+ split: str = 'train',
65
+ train_ratio: float = 0.95,
66
+ seed: int = 42,
67
+ cache_size: int = 1000
68
+ ) -> None:
69
+ """Initialize the dataset.
70
+
71
+ Args:
72
+ dataset_dir: Path to the dataset directory containing metadata.json and series/
73
+ split: Dataset split, either 'train' or 'val'
74
+ train_ratio: Ratio of training samples (default: 0.8)
75
+ seed: Random seed for reproducibility (default: 42)
76
+ cache_size: Number of series files to keep in memory (default: 1000)
77
+ """
78
+ self.split = split
79
+ self.series_dir = Path(dataset_dir) / 'series'
80
+
81
+ # Get all series files and shuffle them
82
+ self.series_files = sorted(self.series_dir.glob('series_*.json'))
83
+ random.seed(seed)
84
+ random.shuffle(self.series_files)
85
+
86
+ # Split into train/val
87
+ split_idx = int(len(self.series_files) * train_ratio)
88
+ self.series_files = self.series_files[:split_idx] if split == 'train' else self.series_files[split_idx:]
89
+
90
+ # Initialize LRU cache for series data
91
+ self._cache = {}
92
+ self._cache_size = cache_size
93
+ self._cache_order = []
94
+
95
+ def _load_series(self, file_path: Path) -> Dict:
96
+ """Load a series file with caching.
97
+
98
+ Args:
99
+ file_path: Path to the series JSON file
100
+
101
+ Returns:
102
+ Dictionary containing the series data
103
+ """
104
+ if file_path in self._cache:
105
+ # Update cache order
106
+ self._cache_order.remove(file_path)
107
+ self._cache_order.append(file_path)
108
+ return self._cache[file_path]
109
+
110
+ # Load new file
111
+ with open(file_path, 'r') as f:
112
+ data = json.load(f)
113
+
114
+ # Update cache
115
+ if len(self._cache) >= self._cache_size:
116
+ # Remove oldest item
117
+ oldest = self._cache_order.pop(0)
118
+ del self._cache[oldest]
119
+
120
+ self._cache[file_path] = data
121
+ self._cache_order.append(file_path)
122
+ return data
123
+
124
+ def __len__(self) -> int:
125
+ """Return the number of samples in the dataset."""
126
+ return len(self.series_files)
127
+
128
+ def __getitem__(self, idx: int) -> Dict[str, Union[torch.Tensor, List[Dict]]]:
129
+ """Get a sample from the dataset.
130
+
131
+ Args:
132
+ idx: Index of the sample to retrieve
133
+
134
+ Returns:
135
+ Dictionary containing:
136
+ - time_series: Time series data as torch.Tensor
137
+ - windows: List of window data containing QA pairs
138
+ - sample_id: Unique identifier for the sample
139
+ """
140
+ file_path = self.series_files[idx]
141
+ data = self._load_series(file_path)
142
+
143
+ # Convert time series to tensor
144
+ time_series = np.array(data['original_data']['time_series'])
145
+ time_series_tensor = torch.FloatTensor(time_series)
146
+
147
+ return {
148
+ 'time_series': time_series_tensor,
149
+ 'analysis_data': data['windows']
150
+ }
151
+
time_rcd/_core/full_reconstruction.py ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import datetime
2
+ import itertools
3
+ import os
4
+ import torch
5
+ import torch.nn as nn
6
+ import torch.optim as optim
7
+ from torch.utils.data import DataLoader, DistributedSampler
8
+ import torch.nn.functional as F
9
+ import torch.distributed as dist
10
+ import torch.multiprocessing as mp
11
+ from torch.nn.parallel import DistributedDataParallel as DDP
12
+ import random
13
+ import numpy as np
14
+ from typing import Tuple, List, Dict, Any, Union, Optional
15
+ from dataclasses import dataclass
16
+
17
+ from .dataset import ChatTSTimeRCDPretrainDataset
18
+ from .ts_encoder_bi_bias import TimeSeriesEncoder
19
+ from .time_rcd_config import TimeRCDConfig, default_config
20
+
21
+ import warnings
22
+ warnings.filterwarnings("ignore")
23
+
24
+ @dataclass
25
+ class PretrainBatch:
26
+ """Batch structure for pretraining tasks."""
27
+ time_series: torch.Tensor
28
+ labels: torch.Tensor
29
+ masked_time_series: torch.Tensor
30
+ mask_indices: torch.Tensor
31
+
32
+
33
+ class TimeSeriesPretrainModel(nn.Module):
34
+ """Model for time series pretraining with masked reconstruction and anomaly detection."""
35
+
36
+ def __init__(self, config: TimeRCDConfig):
37
+ super().__init__()
38
+ self.config = config
39
+
40
+ # Extract TimeSeriesEncoder parameters from config
41
+ ts_config = config.ts_config
42
+ self.ts_encoder = TimeSeriesEncoder(
43
+ d_model=ts_config.d_model,
44
+ d_proj=ts_config.d_proj,
45
+ patch_size=ts_config.patch_size,
46
+ num_layers=ts_config.num_layers,
47
+ num_heads=ts_config.num_heads,
48
+ d_ff_dropout=ts_config.d_ff_dropout,
49
+ use_rope=ts_config.use_rope,
50
+ num_features=ts_config.num_features,
51
+ activation=ts_config.activation
52
+ )
53
+
54
+ # Masked reconstruction head
55
+ self.reconstruction_head = nn.Sequential(
56
+ nn.Linear(config.ts_config.d_proj, config.ts_config.d_proj * 4),
57
+ nn.GELU(),
58
+ nn.Dropout(config.dropout),
59
+ nn.Linear(config.ts_config.d_proj * 4, config.ts_config.d_proj * 4),
60
+ nn.GELU(),
61
+ nn.Dropout(config.dropout),
62
+ nn.Linear(config.ts_config.d_proj * 4, 1) # (B, seq_len, num_features, 1)
63
+ )
64
+ self.reconstruction_head.apply(self._init_weights)
65
+
66
+ def _init_weights(self, module):
67
+ if isinstance(module, nn.Linear):
68
+ nn.init.xavier_normal_(module.weight)
69
+ if module.bias is not None:
70
+ nn.init.zeros_(module.bias)
71
+
72
+ def forward(self, time_series: torch.Tensor, mask: Optional[torch.Tensor] = None):
73
+ """Forward pass through the encoder."""
74
+ local_embeddings = self.ts_encoder(time_series, mask)
75
+ return local_embeddings
76
+
77
+ def masked_reconstruction_loss(self,
78
+ local_embeddings: torch.Tensor, # (B, seq_len, num_features, d_proj)
79
+ original_time_series: torch.Tensor, # (B, seq_len, num_features),
80
+ mask: torch.Tensor # (B, seq_len)
81
+ ) -> torch.Tensor:
82
+ """Compute masked reconstruction loss."""
83
+ batch_size, seq_len, num_features = original_time_series.shape
84
+ patch_size = self.config.ts_config.patch_size
85
+
86
+ mask = mask.bool()
87
+
88
+ # local_embeddings: [B, seq_len, num_features, d_proj]
89
+ reconstructed = self.reconstruction_head(local_embeddings) # (B, seq_len, num_features, 1)
90
+ reconstructed = reconstructed.view(batch_size, seq_len, num_features)
91
+
92
+ mask_expanded = mask.unsqueeze(-1).expand(-1, -1, num_features) # (B, seq_len, num_features)
93
+ reconstruction_loss = F.mse_loss(
94
+ reconstructed[mask_expanded],
95
+ original_time_series[mask_expanded]
96
+ )
97
+ return reconstruction_loss
98
+
99
+ def create_random_mask(time_series: torch.Tensor, #(B, max_seq_len, num_features)
100
+ attention_mask: torch.Tensor, # (B, max_seq_len)
101
+ mask_ratio: float = 0.15) -> Tuple[torch.Tensor, torch.Tensor]:
102
+ """Create random mask for time series patches, only masking valid sequence parts."""
103
+ batch_size, seq_len, num_features = time_series.shape
104
+ patch_size = default_config.ts_config.patch_size
105
+
106
+ mask = torch.zeros(batch_size, seq_len) # (B, max_seq_len)
107
+
108
+ for i in range(batch_size):
109
+ # Get valid sequence length for this sample
110
+ valid_length = attention_mask[i].sum().item()
111
+
112
+ # Calculate number of patches in valid sequence
113
+ num_valid_patches = (valid_length - 1) // patch_size + 1
114
+ num_masked = int(num_valid_patches * mask_ratio)
115
+
116
+ if num_masked > 0:
117
+ # Only select patches from valid sequence
118
+ masked_patches = torch.randperm(num_valid_patches)[:num_masked]
119
+ for j in masked_patches:
120
+ start_idx = j * patch_size
121
+ end_idx = min((j + 1) * patch_size, valid_length) # Don't exceed valid length
122
+ mask[i, start_idx:end_idx] = 1
123
+
124
+ # Create masked time series - only mask valid parts
125
+ masked_time_series = time_series.clone()
126
+ mask_indices = mask.bool() & attention_mask # Only mask where both mask and attention_mask are True
127
+ mask_expanded = mask_indices.unsqueeze(-1).expand(-1, -1, num_features) # (B, max_seq_len, num_features)
128
+ masked_time_series[mask_expanded] = torch.randn_like(masked_time_series[mask_expanded]) * 0.1
129
+
130
+ # Update mask to only include valid parts
131
+ mask = mask * attention_mask.float()
132
+
133
+ return masked_time_series, mask # (B, max_seq_len, num_features), (B, max_seq_len)
134
+
135
+
136
+ def collate_fn(batch):
137
+ """Collate function for pretraining dataset."""
138
+ time_series_list, normal_time_series_list, labels_list, attribute_list = zip(*batch)
139
+
140
+ # Convert to tensors and pad sequences
141
+ if time_series_list[0].ndim == 1:
142
+ time_series_tensors = [ts.unsqueeze(-1) for ts in time_series_list] # Add feature dimension
143
+ normal_time_series_tensors = [nts.unsqueeze(-1) for nts in normal_time_series_list]
144
+ else:
145
+ time_series_tensors = [ts for ts in time_series_list]
146
+ normal_time_series_tensors = [nts for nts in normal_time_series_list]
147
+
148
+ # standardize time series
149
+ # concatenated = torch.cat(time_series_tensors, dim=0) # (total_length, num_features)
150
+ # mean = concatenated.mean(dim=0, keepdim=True) # (1, num_features)
151
+ # std = concatenated.std(dim=0, keepdim=True) # (1, num_features)
152
+ # std = std + 1e-4
153
+ # time_series_tensors_std = [(ts - mean) / std for ts in time_series_tensors]
154
+ # normal_time_series_tensors_std = [(nts - mean) / std for nts in normal_time_series_tensors]
155
+ # time_series_tensors = time_series_tensors_std
156
+ # normal_time_series_tensors = normal_time_series_tensors_std
157
+
158
+ means = []
159
+ stds = []
160
+ for i in range(len(time_series_tensors)):
161
+ ts = time_series_tensors[i]
162
+ mean = ts.mean(dim=0, keepdim=True)
163
+ std = ts.std(dim=0, keepdim=True) + 1e-4
164
+ means.append(mean)
165
+ stds.append(std)
166
+ time_series_tensors[i] = (ts - mean) / std
167
+ for i in range(len(normal_time_series_tensors)):
168
+ nts = normal_time_series_tensors[i]
169
+ mean = means[i]
170
+ std = stds[i]
171
+ normal_time_series_tensors[i] = (nts - mean) / std
172
+
173
+ # labels_tensor = torch.stack(labels_list)
174
+ labels = [label for label in labels_list]
175
+ # Pad time series to same length
176
+ padded_time_series = torch.nn.utils.rnn.pad_sequence(
177
+ time_series_tensors, batch_first=True, padding_value=0.0
178
+ ) # (B, max_seq_len, num_features)
179
+ padded_normal_time_series = torch.nn.utils.rnn.pad_sequence(
180
+ normal_time_series_tensors, batch_first=True, padding_value=0.0
181
+ ) # (B, max_seq_len, num_features)
182
+ padded_labels = torch.nn.utils.rnn.pad_sequence(
183
+ labels, batch_first=True, padding_value=-1
184
+ ) # (B, max_seq_len)
185
+
186
+ sequence_lengths = [ts.size(0) for ts in time_series_tensors]
187
+ B, max_seq_len, num_features = padded_time_series.shape
188
+ attention_mask = torch.zeros(B, max_seq_len, dtype=torch.bool) # (B, max_seq_len)
189
+ for i, length in enumerate(sequence_lengths):
190
+ attention_mask[i, :length] = True
191
+
192
+ # Create random masks for reconstruction task - only mask valid sequence parts
193
+ masked_time_series, mask = create_random_mask(padded_time_series, attention_mask)
194
+
195
+ return {
196
+ 'time_series': padded_time_series,
197
+ 'normal_time_series': padded_normal_time_series,
198
+ 'masked_time_series': masked_time_series,
199
+ 'mask': mask, # for reconstruction task
200
+ 'labels': padded_labels,
201
+ 'attention_mask': attention_mask, # for padding
202
+ 'attribute': attribute_list
203
+ }
204
+
205
+
206
+ def set_seed(seed: int) -> None:
207
+ """Set random seed for reproducibility."""
208
+ random.seed(seed)
209
+ np.random.seed(seed)
210
+ torch.manual_seed(seed)
211
+ if torch.cuda.is_available():
212
+ torch.cuda.manual_seed_all(seed)
213
+ torch.backends.cudnn.deterministic = True
214
+ torch.backends.cudnn.benchmark = False
time_rcd/_core/time_rcd_config.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass, field
2
+ from typing import Dict, Optional
3
+
4
+
5
+ @dataclass
6
+ class TimeSeriesConfig:
7
+ """Configuration for time series encoder.
8
+
9
+ Attributes:
10
+ d_model: Dimension of model hidden states.
11
+ d_proj: Dimension of projection layer.
12
+ patch_size: Size of time series patches.
13
+ num_layers: Number of transformer layers.
14
+ num_heads: Number of attention heads.
15
+ d_ff_dropout: Dropout rate for feed-forward networks.
16
+ use_rope: Whether to use Rotary Position Embedding.
17
+ activation: Activation function name.
18
+ num_features: Number of input features.
19
+ """
20
+ d_model: int = 512
21
+ d_proj: int = 256
22
+ patch_size: int = 4
23
+ num_query_tokens: int = 1
24
+ num_layers: int = 8
25
+ num_heads: int = 8
26
+ d_ff_dropout: float = 0.1
27
+ use_rope: bool = True
28
+ activation: str = "gelu"
29
+ num_features: int = 1
30
+
31
+
32
+ @dataclass
33
+ class TimeRCDConfig:
34
+ """Configuration class for Time_RCD model.
35
+
36
+ This class contains all hyperparameters and settings for the Time_RCD model.
37
+ It is implemented as a dataclass for easy instantiation and modification.
38
+
39
+ Attributes:
40
+ ts_config: Configuration for time series encoder.
41
+ batch_size: Training batch size.
42
+ learning_rate: Learning rate for optimization.
43
+ num_epochs: Number of training epochs.
44
+ max_seq_len: Maximum sequence length.
45
+ dropout: Dropout rate.
46
+ accumulation_steps: Gradient accumulation steps.
47
+ weight_decay: Weight decay for optimization.
48
+ enable_ts_train: Whether to train the time series encoder.
49
+ seed: Random seed for reproducibility.
50
+ """
51
+
52
+ # Model configurations
53
+ ts_config: TimeSeriesConfig = field(default_factory=TimeSeriesConfig)
54
+
55
+ # Training parameters
56
+ batch_size: int = 3
57
+ learning_rate: float = 1e-4
58
+ num_epochs: int = 1000
59
+ max_seq_len: int = 512
60
+ dropout: float = 0.1
61
+ accumulation_steps: int = 1
62
+ weight_decay: float = 1e-5
63
+ enable_ts_train: bool = False
64
+ seed: int = 72
65
+
66
+ def to_dict(self) -> Dict[str, any]:
67
+ return {
68
+ "ts_config": self.ts_config.__dict__,
69
+ }
70
+
71
+ default_config = TimeRCDConfig()
time_rcd/_core/ts_encoder_bi_bias.py ADDED
@@ -0,0 +1,376 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ import torch.nn.functional as F
4
+ import math
5
+ from jaxtyping import Float, Int
6
+ from einops import rearrange
7
+
8
+
9
+ class RMSNorm(nn.Module):
10
+ """Root Mean Square Normalization layer."""
11
+
12
+ def __init__(self, size: int, dim: int = -1, eps: float = 1e-5) -> None:
13
+ super().__init__()
14
+ self.scale = nn.Parameter(torch.ones(size))
15
+ self.eps = eps
16
+ self.dim = dim
17
+
18
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
19
+ norm_x = x.to(torch.float32).pow(2).mean(dim=self.dim, keepdim=True)
20
+ x_normed = x * torch.rsqrt(norm_x + self.eps)
21
+ return (self.scale * x_normed).type_as(x)
22
+
23
+
24
+ class RotaryEmbedding(nn.Module):
25
+ """Rotary Positional Embedding for injecting positional information."""
26
+
27
+ def __init__(self, dim):
28
+ super().__init__()
29
+ inv_freq = 1.0 / (10000 ** (torch.arange(0, dim, 2).float() / dim))
30
+ self.register_buffer("inv_freq", inv_freq)
31
+
32
+ def forward(self, seq_len):
33
+ t = torch.arange(seq_len, device=self.inv_freq.device).type_as(self.inv_freq)
34
+ freqs = torch.einsum("i,j->ij", t, self.inv_freq)
35
+ return freqs # Shape: (seq_len, dim // 2)
36
+
37
+
38
+ class BinaryAttentionBias(nn.Module):
39
+ """Binary Variate Attention for time series data."""
40
+
41
+ def __init__(self,
42
+ num_heads: Int):
43
+ super().__init__()
44
+ self.num_heads = num_heads
45
+ self.emd = nn.Embedding(2, num_heads)
46
+
47
+ def forward(self,
48
+ query_id: Int[torch.Tensor, "batch_size q_len"],
49
+ kv_id: Int[torch.Tensor, "batch_size kv_len"],
50
+ ) -> Float[torch.Tensor, "batch_size num_heads q_len kv_len"]:
51
+ ind = torch.eq(query_id.unsqueeze(-1), kv_id.unsqueeze(-2))
52
+ ind = ind.unsqueeze(1) # (batch_size, 1, q_len, kv_len)
53
+ weight = rearrange(self.emd.weight, "two num_heads -> two num_heads 1 1") # (2, num_heads, 1, 1)
54
+ bias = ~ind * weight[:1] + ind * weight[1:] # (batch_size, num_heads, q_len, kv_len)
55
+ return bias
56
+
57
+
58
+ class MultiheadAttentionWithRoPE(nn.Module):
59
+ """Multi-head Attention with Rotary Positional Encoding (RoPE), non-causal by default."""
60
+ "========== NOtice that this applies BinaryAttentionBias ==========="
61
+
62
+ def __init__(self, embed_dim, num_heads, num_features):
63
+ super().__init__()
64
+ self.embed_dim = embed_dim
65
+ self.num_heads = num_heads
66
+ self.head_dim = embed_dim // num_heads
67
+ self.num_features = num_features
68
+ assert self.head_dim * num_heads == embed_dim, "embed_dim must be divisible by num_heads"
69
+
70
+ # Linear projections for Q, K, V, and output
71
+ self.q_proj = nn.Linear(embed_dim, embed_dim, bias=False)
72
+ self.k_proj = nn.Linear(embed_dim, embed_dim, bias=False)
73
+ self.v_proj = nn.Linear(embed_dim, embed_dim, bias=False)
74
+ self.out_proj = nn.Linear(embed_dim, embed_dim, bias=False)
75
+
76
+ # Binary attention bias for time series
77
+ if num_features > 1:
78
+ self.binary_attention_bias = BinaryAttentionBias(num_heads)
79
+
80
+ def apply_rope(self, x, freqs):
81
+ """Apply Rotary Positional Encoding to the input tensor."""
82
+ B, seq_len, embed_dim = x.shape
83
+ assert embed_dim == self.embed_dim, "Embedding dimension mismatch"
84
+ assert freqs.shape == (seq_len, embed_dim // 2), "freqs shape mismatch"
85
+
86
+ # Reshape for rotation: split embed_dim into pairs
87
+ x_ = x.view(B, seq_len, embed_dim // 2, 2)
88
+ cos = freqs.cos().unsqueeze(0) # (1, seq_len, embed_dim // 2, 1)
89
+ sin = freqs.sin().unsqueeze(0) # (1, seq_len, embed_dim // 2, 1)
90
+
91
+ # Apply rotation to each pair
92
+ x_rot = torch.stack(
93
+ [
94
+ x_[..., 0] * cos - x_[..., 1] * sin,
95
+ x_[..., 0] * sin + x_[..., 1] * cos,
96
+ ],
97
+ dim=-1
98
+ )
99
+ return x_rot.view(B, seq_len, embed_dim)
100
+
101
+ def forward(self, query, key, value, freqs, query_id=None, kv_id=None, attn_mask=None):
102
+ """
103
+ Forward pass for multi-head attention with RoPE.
104
+
105
+ Args:
106
+ query (Tensor): Shape (B, T, C)
107
+ key (Tensor): Shape (B, T, C)
108
+ value (Tensor): Shape (B, T, C)
109
+ freqs (Tensor): RoPE frequencies, shape (T, embed_dim // 2)
110
+ query_id (Tensor, optional): Shape (B, q_len), feature IDs for query
111
+ kv_id (Tensor, optional): Shape (B, kv_len), feature IDs for key/value
112
+ attn_mask (Tensor, optional): Shape (B, T), True for valid positions, False for padding.
113
+
114
+ Returns:
115
+ Tensor: Attention output, shape (B, T, C)
116
+ """
117
+ B, T, C = query.shape
118
+ assert key.shape == (B, T, C) and value.shape == (B, T, C), "query, key, value shapes must match"
119
+
120
+ # Project inputs to Q, K, V
121
+ Q = self.q_proj(query)
122
+ K = self.k_proj(key)
123
+ V = self.v_proj(value)
124
+
125
+ # Apply RoPE to Q and K
126
+ Q_rot = self.apply_rope(Q, freqs)
127
+ K_rot = self.apply_rope(K, freqs)
128
+
129
+ # Reshape for multi-head attention
130
+ Q_rot = Q_rot.view(B, T, self.num_heads, self.head_dim).transpose(1, 2) # (B, nh, T, hs)
131
+ K_rot = K_rot.view(B, T, self.num_heads, self.head_dim).transpose(1, 2) # (B, nh, T, hs)
132
+ V = V.view(B, T, self.num_heads, self.head_dim).transpose(1, 2) # (B, nh, T, hs)
133
+
134
+ # Prepare attention mask for padding
135
+ if attn_mask is not None:
136
+ attn_mask = attn_mask.unsqueeze(1).unsqueeze(2) # (B, 1, 1, T)
137
+ else:
138
+ attn_mask = None
139
+
140
+ if query_id is not None and kv_id is not None:
141
+ # Add binary attention bias
142
+ attn_bias = self.binary_attention_bias(query_id, kv_id) # (B, num_heads, q_len, kv_len)
143
+ scores = torch.matmul(Q_rot, K_rot.transpose(-2, -1)) / math.sqrt(
144
+ self.head_dim) # (B, num_heads, q_len, kv_len)
145
+ scores += attn_bias
146
+ if attn_mask is not None:
147
+ scores = scores.masked_fill(~attn_mask, float('-inf'))
148
+ attn_weights = F.softmax(scores, dim=-1) # (B, num_heads, q_len, kv_len)
149
+ y = torch.matmul(attn_weights, V) # (B, num_heads, q_len, hs)
150
+
151
+ else:
152
+ # Compute scaled dot-product attention (non-causal) without binary bias
153
+ # for param in self.binary_attention_bias.parameters():
154
+ # param.requires_grad = False
155
+ y = F.scaled_dot_product_attention(
156
+ Q_rot, K_rot, V,
157
+ attn_mask=attn_mask,
158
+ is_causal=False # Non-causal attention for encoder
159
+ ) # (B, nh, T, hs)
160
+
161
+ # Reshape and project output
162
+ y = y.transpose(1, 2).contiguous().view(B, T, C)
163
+ y = self.out_proj(y)
164
+ return y
165
+
166
+
167
+ class LlamaMLP(nn.Module):
168
+ def __init__(self, d_model, dim_feedforward=2048):
169
+ super().__init__()
170
+ self.hidden_size = d_model
171
+ self.intermediate_size = dim_feedforward
172
+ self.gate_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=True)
173
+ self.up_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=True)
174
+ self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=True)
175
+ self.act_fn = F.gelu
176
+
177
+ def forward(self, x):
178
+ down_proj = self.down_proj(self.act_fn(self.gate_proj(x)) * self.up_proj(x))
179
+ return down_proj
180
+
181
+
182
+ class TransformerEncoderLayerWithRoPE(nn.Module):
183
+ """Transformer Encoder Layer with RoPE and RMSNorm."""
184
+
185
+ def __init__(self, d_model, nhead, dim_feedforward=2048, dropout=0.1, activation="relu", num_features=1):
186
+ super().__init__()
187
+ self.self_attn = MultiheadAttentionWithRoPE(d_model, nhead, num_features)
188
+ self.dropout = nn.Dropout(dropout)
189
+ self.input_norm = RMSNorm(d_model)
190
+ self.output_norm = RMSNorm(d_model)
191
+ self.mlp = LlamaMLP(d_model, dim_feedforward)
192
+ self.dropout1 = nn.Dropout(dropout)
193
+ self.dropout2 = nn.Dropout(dropout)
194
+ self.activation = F.relu if activation == "relu" else F.gelu
195
+
196
+ def forward(self, src, freqs, src_id=None, attn_mask=None):
197
+ residual = src
198
+ src = self.input_norm(src)
199
+ src = self.self_attn(src, src, src, freqs, src_id, src_id, attn_mask=attn_mask)
200
+ src = src + residual
201
+ residual = src
202
+ src = self.output_norm(src)
203
+ src = self.mlp(src)
204
+ src = residual + self.dropout2(src)
205
+ return src
206
+
207
+
208
+ class CustomTransformerEncoder(nn.Module):
209
+ """Stack of Transformer Encoder Layers."""
210
+
211
+ def __init__(self, d_model, nhead, dim_feedforward, dropout, activation, num_layers, num_features):
212
+ super().__init__()
213
+ self.layers = nn.ModuleList([
214
+ TransformerEncoderLayerWithRoPE(
215
+ d_model=d_model,
216
+ nhead=nhead,
217
+ dim_feedforward=dim_feedforward,
218
+ dropout=dropout,
219
+ activation=activation,
220
+ num_features=num_features
221
+ ) for _ in range(num_layers)
222
+ ])
223
+
224
+ def forward(self, src, freqs, src_id=None, attn_mask=None):
225
+ output = src
226
+ for layer in self.layers:
227
+ output = layer(output, freqs, src_id, attn_mask=attn_mask)
228
+ return output
229
+
230
+
231
+ class TimeSeriesEncoder(nn.Module):
232
+ """
233
+ Time Series Encoder with PatchTST-like patching, RoPE.
234
+
235
+ Args:
236
+ d_model (int): Model dimension
237
+ d_proj (int): Projection dimension
238
+ patch_size (int): Size of each patch
239
+ num_layers (int): Number of encoder layers
240
+ num_heads (int): Number of attention heads
241
+ d_ff_dropout (float): Dropout rate
242
+ max_total_tokens (int): Maximum sequence length
243
+ use_rope (bool): Use RoPE if True
244
+ num_features (int): Number of features in the time series
245
+ activation (str): "relu" or "gelu"
246
+
247
+ Inputs:
248
+ time_series (Tensor): Shape (batch_size, seq_len, num_features)
249
+ mask (Tensor): Shape (batch_size, seq_len)
250
+
251
+ Outputs:
252
+ local_embeddings (Tensor): Shape (batch_size, seq_len, num_features, d_proj)
253
+ """
254
+
255
+ def __init__(self, d_model=2048, d_proj=512, patch_size=32, num_layers=6, num_heads=8,
256
+ d_ff_dropout=0.1, max_total_tokens=8192, use_rope=True, num_features=1,
257
+ activation="relu"):
258
+ super().__init__()
259
+ self.patch_size = patch_size
260
+ self.d_model = d_model
261
+ self.d_proj = d_proj
262
+ self.num_layers = num_layers
263
+ self.num_heads = num_heads
264
+ self.d_ff_dropout = d_ff_dropout
265
+ self.max_total_tokens = max_total_tokens
266
+ self.use_rope = use_rope
267
+ self.num_features = num_features
268
+ self.activation = activation
269
+
270
+ # Patch embedding layer
271
+ self.embedding_layer = nn.Linear(patch_size, d_model)
272
+
273
+ if use_rope:
274
+ # Initialize RoPE and custom encoder
275
+ self.rope_embedder = RotaryEmbedding(d_model)
276
+ self.transformer_encoder = CustomTransformerEncoder(
277
+ d_model=d_model,
278
+ nhead=num_heads,
279
+ dim_feedforward=d_model * 4,
280
+ dropout=d_ff_dropout,
281
+ activation=activation,
282
+ num_layers=num_layers,
283
+ num_features=num_features
284
+ )
285
+ else:
286
+ # Standard encoder without RoPE
287
+ encoder_layer = nn.TransformerEncoderLayer(
288
+ d_model=d_model,
289
+ nhead=num_heads,
290
+ dim_feedforward=d_model * 4,
291
+ dropout=d_ff_dropout,
292
+ batch_first=True,
293
+ activation=activation
294
+ )
295
+ self.transformer_encoder = nn.TransformerEncoder(encoder_layer, num_layers)
296
+
297
+ # Output projection layers
298
+ self.projection_layer = nn.Linear(d_model, patch_size * d_proj)
299
+ self._init_parameters()
300
+
301
+ def _init_parameters(self):
302
+ for name, param in self.named_parameters():
303
+ if 'weight' in name and 'linear' in name:
304
+ if self.activation == "relu":
305
+ nn.init.kaiming_uniform_(param, nonlinearity='relu')
306
+ elif self.activation == "gelu":
307
+ nn.init.kaiming_uniform_(param, nonlinearity='gelu')
308
+ elif 'bias' in name:
309
+ nn.init.constant_(param, 0.0)
310
+
311
+ def forward(self, time_series, mask):
312
+ """Forward pass to generate local embeddings."""
313
+ if time_series.dim() == 2:
314
+ time_series = time_series.unsqueeze(-1)
315
+ device = time_series.device
316
+ B, seq_len, num_features = time_series.size()
317
+ assert num_features == self.num_features, f"Number of features mismatch with data: {num_features} vs param: {self.num_features}"
318
+ assert mask.size() == (B, seq_len), "Mask shape mismatch"
319
+
320
+ # Pad sequence to be divisible by patch_size
321
+ padded_length = math.ceil(seq_len / self.patch_size) * self.patch_size
322
+ if padded_length > seq_len:
323
+ pad_amount = padded_length - seq_len
324
+ time_series = F.pad(time_series, (0, 0, 0, pad_amount), value=0)
325
+ mask = F.pad(mask, (0, pad_amount), value=0)
326
+
327
+ # Convert to patches
328
+ num_patches = padded_length // self.patch_size
329
+ total_length = num_patches * num_features
330
+ patches = time_series.view(B, num_patches, self.patch_size, num_features)
331
+ patches = patches.permute(0, 3, 1, 2).contiguous() # (B, num_features, num_patches, patch_size)
332
+ patches = patches.view(B, num_features * num_patches, self.patch_size) # (B, L, patch_size)
333
+ # Create feature IDs for patches
334
+ feature_id = torch.arange(num_features, device=device).repeat_interleave(
335
+ num_patches) # (num_features * num_patches = L,)
336
+ feature_id = feature_id.unsqueeze(0).expand(B, -1) # (B, L)
337
+
338
+ # Embed patches
339
+ embedded_patches = self.embedding_layer(patches) # (B, L, d_model)
340
+
341
+ # Create patch-level mask
342
+ mask = mask.view(B, num_patches, self.patch_size)
343
+ patch_mask = mask.sum(dim=-1) > 0 # (B, num_patches)
344
+ full_mask = patch_mask.unsqueeze(1).expand(-1, num_features, -1) # (B, num_features, num_patches)
345
+ full_mask = full_mask.reshape(B, num_features * num_patches) # (B, L)
346
+
347
+ # Generate RoPE frequencies if applicable
348
+ if self.use_rope:
349
+ freqs = self.rope_embedder(total_length).to(device)
350
+ else:
351
+ freqs = None
352
+
353
+ # Encode sequence
354
+ if num_features > 1:
355
+ output = self.transformer_encoder(
356
+ embedded_patches,
357
+ freqs=freqs,
358
+ src_id=feature_id,
359
+ attn_mask=full_mask
360
+ )
361
+ else:
362
+ output = self.transformer_encoder(
363
+ embedded_patches,
364
+ freqs=freqs,
365
+ attn_mask=full_mask
366
+ )
367
+
368
+ # Extract and project local embeddings
369
+ patch_embeddings = output # (B, L, d_model)
370
+ patch_proj = self.projection_layer(patch_embeddings) # (B, L, patch_size * d_proj)
371
+ local_embeddings = patch_proj.view(B, num_features, num_patches, self.patch_size, self.d_proj)
372
+ local_embeddings = local_embeddings.permute(0, 2, 3, 1, 4) # (B, num_patches, patch_size, num_features, d_proj)
373
+ local_embeddings = local_embeddings.view(B, -1, num_features, self.d_proj)[:, :seq_len, :,
374
+ :] # (B, seq_len, num_features, d_proj)
375
+
376
+ return local_embeddings
time_rcd/_inference.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Minimal inference backend packaged with the public Time-RCD API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Tuple
7
+
8
+ import numpy as np
9
+ import torch
10
+ import torch.nn.functional as F
11
+ from torch.utils.data import DataLoader, Dataset
12
+
13
+ from ._core.TimeRCD_pretrain_multi import TimeSeriesPretrainModel
14
+ from ._core.time_rcd_config import TimeRCDConfig
15
+
16
+
17
+ class _WindowDataset(Dataset):
18
+ """Split a normalized time series into non-overlapping padded windows."""
19
+
20
+ def __init__(self, data: np.ndarray, window_size: int) -> None:
21
+ mean = np.mean(data, axis=0)
22
+ std = np.where(np.std(data, axis=0) == 0, 1e-8, np.std(data, axis=0))
23
+ normalized = (data - mean) / std
24
+
25
+ padding = (-len(normalized)) % window_size
26
+ if padding:
27
+ normalized = np.vstack(
28
+ [normalized, np.repeat(normalized[-1:, :], padding, axis=0)]
29
+ )
30
+ self.data = normalized
31
+ self.window_size = window_size
32
+ self.original_length = len(data)
33
+
34
+ def __len__(self) -> int:
35
+ return len(self.data) // self.window_size
36
+
37
+ def __getitem__(self, index: int) -> Tuple[torch.Tensor, torch.Tensor]:
38
+ start = index * self.window_size
39
+ end = start + self.window_size
40
+ valid_length = min(self.window_size, self.original_length - start)
41
+ mask = torch.zeros(self.window_size, dtype=torch.bool)
42
+ mask[:valid_length] = True
43
+ return (
44
+ torch.tensor(self.data[start:end], dtype=torch.float32),
45
+ mask,
46
+ )
47
+
48
+
49
+ def _collate_windows(
50
+ batch: list[Tuple[torch.Tensor, torch.Tensor]],
51
+ ) -> dict[str, torch.Tensor]:
52
+ time_series, attention_mask = zip(*batch)
53
+ return {
54
+ "time_series": torch.stack(time_series),
55
+ "attention_mask": torch.stack(attention_mask),
56
+ }
57
+
58
+
59
+ class TimeRCDPretrainTester:
60
+ """Inference-only wrapper for a pretrained Time-RCD checkpoint."""
61
+
62
+ def __init__(self, checkpoint_path: str, config: TimeRCDConfig) -> None:
63
+ self.config = config
64
+ self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
65
+ self.win_size = config.win_size
66
+ self.batch_size = config.batch_size
67
+ self.model = TimeSeriesPretrainModel(config).to(self.device)
68
+ self.load_checkpoint(checkpoint_path)
69
+ self.model.eval()
70
+
71
+ def load_checkpoint(self, checkpoint_path: str) -> None:
72
+ path = Path(checkpoint_path)
73
+ if not path.is_file():
74
+ raise FileNotFoundError(f"Checkpoint not found: {path}")
75
+
76
+ try:
77
+ checkpoint = torch.load(
78
+ path, map_location=self.device, weights_only=True
79
+ )
80
+ except TypeError:
81
+ checkpoint = torch.load(path, map_location=self.device)
82
+ state_dict = checkpoint.get("model_state_dict", checkpoint)
83
+ state_dict = {
84
+ key.removeprefix("module."): value for key, value in state_dict.items()
85
+ }
86
+ self.model.load_state_dict(state_dict)
87
+
88
+ def zero_shot(
89
+ self, data: np.ndarray
90
+ ) -> tuple[list[np.ndarray], list[np.ndarray]]:
91
+ window_size = min(len(data), self.win_size)
92
+ dataset = _WindowDataset(data, window_size)
93
+ loader = DataLoader(
94
+ dataset,
95
+ batch_size=self.batch_size,
96
+ collate_fn=_collate_windows,
97
+ num_workers=0,
98
+ shuffle=False,
99
+ )
100
+
101
+ scores: list[np.ndarray] = []
102
+ logits: list[np.ndarray] = []
103
+ with torch.no_grad():
104
+ for batch in loader:
105
+ time_series = batch["time_series"].to(self.device)
106
+ attention_mask = batch["attention_mask"].to(self.device)
107
+ local_embeddings = self.model(
108
+ time_series=time_series, mask=attention_mask
109
+ )
110
+ anomaly_logits = self.model.anomaly_head(local_embeddings)
111
+ anomaly_logits = torch.mean(anomaly_logits, dim=-2)
112
+ anomaly_probs = F.softmax(anomaly_logits, dim=-1)[..., 1]
113
+ scores.append(anomaly_probs.cpu().numpy())
114
+ logits.append(
115
+ (anomaly_logits[..., 1] - anomaly_logits[..., 0]).cpu().numpy()
116
+ )
117
+ return scores, logits
time_rcd/detector.py ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """User-facing zero-shot inference API for Time-RCD."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from copy import deepcopy
6
+ from pathlib import Path
7
+ from typing import Literal, Optional, Tuple, Union
8
+
9
+ import numpy as np
10
+
11
+ HF_REPO_ID = "thu-sail-lab/Time-RCD"
12
+ CHECKPOINT_FILES = {
13
+ "uni": "best_model/pretrain_checkpoint_best_uni.pth",
14
+ "multi": "best_model/pretrain_checkpoint_best_multi.pth",
15
+ }
16
+ DEFAULT_WIN_SIZE = 5000
17
+ DEFAULT_BATCH_SIZE = {"uni": 64, "multi": 1}
18
+
19
+
20
+ class TimeRCDDetector:
21
+ """Zero-shot time series anomaly detector powered by Time-RCD.
22
+
23
+ Parameters
24
+ ----------
25
+ checkpoint_path:
26
+ Path to a local ``.pth`` checkpoint.
27
+ variant:
28
+ ``"uni"`` for univariate series, ``"multi"`` for multivariate series.
29
+ win_size:
30
+ Sliding window length. Sequences shorter than this value use the full
31
+ sequence length instead.
32
+ batch_size:
33
+ Inference batch size. Defaults to 64 (uni) or 1 (multi).
34
+ device:
35
+ PyTorch device string, e.g. ``"cuda"`` or ``"cpu"``. Auto-detected when
36
+ omitted.
37
+
38
+ Notes
39
+ -----
40
+ The model is initialized on the first call to :meth:`predict`, when the
41
+ number of input features is known.
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ checkpoint_path: str,
47
+ variant: Literal["uni", "multi"] = "uni",
48
+ win_size: int = DEFAULT_WIN_SIZE,
49
+ batch_size: Optional[int] = None,
50
+ device: Optional[str] = None,
51
+ ) -> None:
52
+ if variant not in CHECKPOINT_FILES:
53
+ raise ValueError(f"variant must be one of {list(CHECKPOINT_FILES)}, got {variant!r}")
54
+
55
+ self.variant = variant
56
+ self.win_size = win_size
57
+ self.batch_size = batch_size if batch_size is not None else DEFAULT_BATCH_SIZE[variant]
58
+ self.checkpoint_path = str(checkpoint_path)
59
+ self.device = device
60
+ self._tester = None
61
+ self._num_features: Optional[int] = None
62
+
63
+ def _ensure_tester(self, num_features: int) -> None:
64
+ """Initialize a model compatible with ``num_features`` when needed."""
65
+ if self._tester is not None and self._num_features == num_features:
66
+ return
67
+
68
+ from ._core.time_rcd_config import default_config
69
+ from ._inference import TimeRCDPretrainTester
70
+
71
+ # ``default_config`` is a module-level template. Each detector must own
72
+ # its configuration so its runtime options cannot affect other instances.
73
+ config = deepcopy(default_config)
74
+ config.ts_config.patch_size = 16
75
+ config.win_size = self.win_size
76
+ config.batch_size = self.batch_size
77
+ config.ts_config.num_features = num_features
78
+
79
+ self._tester = TimeRCDPretrainTester(self.checkpoint_path, config)
80
+ self._num_features = num_features
81
+ if self.device is not None:
82
+ import torch
83
+
84
+ self._tester.device = torch.device(self.device)
85
+ self._tester.model.to(self._tester.device)
86
+
87
+ @classmethod
88
+ def from_pretrained(
89
+ cls,
90
+ repo_id: str = HF_REPO_ID,
91
+ variant: Literal["uni", "multi"] = "uni",
92
+ cache_dir: Optional[str] = None,
93
+ local_files_only: bool = False,
94
+ **kwargs,
95
+ ) -> "TimeRCDDetector":
96
+ """Load a checkpoint from Hugging Face Hub (cached locally after first use)."""
97
+ from huggingface_hub import hf_hub_download
98
+
99
+ checkpoint_path = hf_hub_download(
100
+ repo_id=repo_id,
101
+ filename=CHECKPOINT_FILES[variant],
102
+ cache_dir=cache_dir,
103
+ local_files_only=local_files_only,
104
+ )
105
+ return cls(checkpoint_path=checkpoint_path, variant=variant, **kwargs)
106
+
107
+ @classmethod
108
+ def from_local(
109
+ cls,
110
+ checkpoint_path: Union[str, Path],
111
+ variant: Literal["uni", "multi"] = "uni",
112
+ **kwargs,
113
+ ) -> "TimeRCDDetector":
114
+ """Load a checkpoint from a local path."""
115
+ path = Path(checkpoint_path)
116
+ if not path.is_file():
117
+ raise FileNotFoundError(f"Checkpoint not found: {path}")
118
+ return cls(checkpoint_path=str(path), variant=variant, **kwargs)
119
+
120
+ def predict(
121
+ self,
122
+ data: Union[np.ndarray, list],
123
+ return_logits: bool = False,
124
+ ) -> Union[np.ndarray, Tuple[np.ndarray, np.ndarray]]:
125
+ """Run zero-shot anomaly scoring on a single time series.
126
+
127
+ Parameters
128
+ ----------
129
+ data:
130
+ Array of shape ``(T,)`` or ``(T, C)`` where ``T`` is time steps and
131
+ ``C`` is the number of channels.
132
+ return_logits:
133
+ When ``True``, also return raw anomaly logits.
134
+
135
+ Returns
136
+ -------
137
+ scores:
138
+ Anomaly scores in ``[0, 1]``, one value per time step.
139
+ logits:
140
+ Returned only when ``return_logits=True``.
141
+ """
142
+ data = np.asarray(data, dtype=np.float64)
143
+ if data.ndim == 1:
144
+ data = data.reshape(-1, 1)
145
+ elif data.ndim != 2:
146
+ raise ValueError(f"data must be 1D or 2D, got shape {data.shape}")
147
+
148
+ if self.variant == "uni" and data.shape[1] != 1:
149
+ raise ValueError(
150
+ "variant='uni' expects a univariate series with shape (T,) or (T, 1). "
151
+ f"Got {data.shape[1]} channels; use variant='multi' instead."
152
+ )
153
+ if self.variant == "multi" and data.shape[1] < 2:
154
+ raise ValueError(
155
+ "variant='multi' expects at least two channels with shape (T, C), where C > 1. "
156
+ "Use variant='uni' for a univariate series."
157
+ )
158
+
159
+ original_length = data.shape[0]
160
+ self._ensure_tester(data.shape[1])
161
+ assert self._tester is not None
162
+
163
+ score_chunks, logit_chunks = self._tester.zero_shot(data)
164
+ scores = np.concatenate([np.asarray(chunk).reshape(-1) for chunk in score_chunks], axis=0)
165
+ logits = np.concatenate([np.asarray(chunk).reshape(-1) for chunk in logit_chunks], axis=0)
166
+
167
+ scores = scores[:original_length]
168
+ logits = logits[:original_length]
169
+
170
+ if return_logits:
171
+ return scores, logits
172
+ return scores
zero-shot.png ADDED

Git LFS Details

  • SHA256: 09dcc0f0a3bd42995915180e7e0f97b34adc1630c3acb887fb4c02bd2ab7b8e5
  • Pointer size: 131 Bytes
  • Size of remote file: 294 kB