Instructions to use thu-sail-lab/Time-RCD with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use thu-sail-lab/Time-RCD with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("feature-extraction", model="thu-sail-lab/Time-RCD", trust_remote_code=True)# Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("thu-sail-lab/Time-RCD", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
Align inference API with official GitHub implementation
Browse filesAdd the official TimeRCDDetector package and documentation; re-export the official univariate checkpoint for Transformers; align long-sequence normalization semantics.
- .gitattributes +1 -0
- .gitignore +145 -0
- LICENSE +190 -0
- README.md +114 -102
- Tutorial.md +146 -0
- config.json +3 -4
- examples/quickstart.py +33 -0
- model.safetensors +1 -1
- modeling_time_rcd.py +85 -71
- pyproject.toml +38 -0
- requirements.txt +5 -2
- time_rcd.egg-info/PKG-INFO +377 -0
- time_rcd.egg-info/SOURCES.txt +17 -0
- time_rcd.egg-info/dependency_links.txt +1 -0
- time_rcd.egg-info/requires.txt +5 -0
- time_rcd.egg-info/top_level.txt +1 -0
- time_rcd/__init__.py +3 -0
- time_rcd/_core/TimeRCD_pretrain_multi.py +236 -0
- time_rcd/_core/__init__.py +0 -0
- time_rcd/_core/dataset.py +151 -0
- time_rcd/_core/full_reconstruction.py +214 -0
- time_rcd/_core/time_rcd_config.py +71 -0
- time_rcd/_core/ts_encoder_bi_bias.py +376 -0
- time_rcd/_inference.py +117 -0
- time_rcd/detector.py +172 -0
- zero-shot.png +3 -0
.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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
-
|
| 14 |
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
|
| 18 |
-
|
|
| 19 |
-
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
-
|
| 22 |
|
| 23 |
-
|
| 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 |
-
##
|
| 29 |
|
| 30 |
-
|
| 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 |
-
|
| 37 |
|
| 38 |
-
|
| 39 |
|
| 40 |
-
|
| 41 |
-
.
|
| 42 |
-
|
| 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 |
-
|
| 55 |
-
- `best_model/`: Original PyTorch checkpoints for univariate and multivariate settings.
|
| 56 |
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
|
| 59 |
```bash
|
| 60 |
conda create -n Time-RCD python=3.10
|
| 61 |
conda activate Time-RCD
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
| 63 |
```
|
| 64 |
|
| 65 |
-
|
| 66 |
|
| 67 |
```bash
|
| 68 |
-
pip install
|
| 69 |
```
|
| 70 |
|
| 71 |
-
##
|
| 72 |
|
| 73 |
-
|
|
|
|
|
|
|
| 74 |
|
| 75 |
-
|
|
|
|
|
|
|
| 76 |
|
| 77 |
```python
|
| 78 |
import numpy as np
|
| 79 |
-
import
|
| 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 |
-
|
| 86 |
-
model.eval()
|
| 87 |
|
| 88 |
-
|
| 89 |
-
|
| 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 |
-
|
| 98 |
|
| 99 |
```python
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
model = AutoModel.from_pretrained(
|
| 104 |
-
"thu-sail-lab/Time-RCD",
|
| 105 |
-
trust_remote_code=True,
|
| 106 |
-
).eval()
|
| 107 |
|
| 108 |
-
|
| 109 |
-
with torch.no_grad():
|
| 110 |
-
outputs = model(time_series=time_series)
|
| 111 |
|
| 112 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
```
|
| 114 |
|
| 115 |
-
##
|
| 116 |
-
|
| 117 |
-
Download the full official repository:
|
| 118 |
|
| 119 |
```bash
|
| 120 |
-
|
| 121 |
```
|
| 122 |
|
| 123 |
-
|
| 124 |
|
| 125 |
-
|
| 126 |
-
hf download thu-sail-lab/Time-RCD \
|
| 127 |
-
--include "best_model/pretrain_checkpoint_best_uni.pth" \
|
| 128 |
-
--local-dir ./Time-RCD
|
| 129 |
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
```
|
| 134 |
|
| 135 |
-
|
|
|
|
|
|
|
| 136 |
|
| 137 |
-
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
```
|
| 140 |
|
| 141 |
-
|
|
|
|
|
|
|
|
|
|
| 142 |
|
| 143 |
-
|
|
|
|
|
|
|
| 144 |
|
| 145 |
-
```
|
| 146 |
-
|
| 147 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
```
|
| 149 |
|
| 150 |
-
-
|
| 151 |
-
- **Multivariate evaluation:** `python main.py --mode multi`
|
| 152 |
-
- **Pre-training:** `python training.py --mode single --gpus 0 --num-workers 0`
|
| 153 |
|
| 154 |
-
|
|
|
|
|
|
|
|
|
|
| 155 |
|
| 156 |
-
-
|
| 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 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 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 |
+
[](https://arxiv.org/abs/2509.21190)
|
| 19 |
+
[](https://huggingface.co/spaces/thu-sail-lab/Time_RCD)
|
| 20 |
+
[](https://mp.weixin.qq.com/s/79M3jsEhMKBzbNYpROOBCw)
|
| 21 |
|
| 22 |
+
</div>
|
| 23 |
|
| 24 |
+
<p align="center">
|
| 25 |
+
📰 <a href="#-news">News</a>
|
| 26 |
+
| 🔍 <a href="#-about">About</a>
|
| 27 |
+
| 🎯 <a href="#-use-on-your-own-data">Use on Your Own Data</a>
|
| 28 |
+
| 📁 <a href="#-project-structure">Project Structure</a>
|
| 29 |
+
| 🔗 <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.
|
| 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.
|
| 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:
|
| 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.
|
| 149 |
|
| 150 |
batch_size, seq_len, num_features = time_series.shape
|
| 151 |
|
| 152 |
-
|
| 153 |
-
|
|
|
|
|
|
|
| 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
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 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:
|
| 188 |
-
|
| 189 |
Returns:
|
| 190 |
-
tuple: (scores, logits) where
|
| 191 |
-
|
| 192 |
-
- logits: list of anomaly logit arrays per batch
|
| 193 |
"""
|
| 194 |
-
import
|
| 195 |
-
|
| 196 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
self.eval()
|
| 198 |
device = next(self.parameters()).device
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
data = np.asarray(data)
|
| 202 |
if data.ndim == 1:
|
| 203 |
data = data.reshape(-1, 1)
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 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
|
| 247 |
-
batch_ts =
|
| 248 |
-
batch_mask =
|
| 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 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 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 |
+
[](https://arxiv.org/abs/2509.21190)
|
| 235 |
+
[](https://huggingface.co/spaces/thu-sail-lab/Time_RCD)
|
| 236 |
+
[](https://mp.weixin.qq.com/s/79M3jsEhMKBzbNYpROOBCw)
|
| 237 |
+
|
| 238 |
+
</div>
|
| 239 |
+
|
| 240 |
+
<p align="center">
|
| 241 |
+
📰 <a href="#-news">News</a>
|
| 242 |
+
| 🔍 <a href="#-about">About</a>
|
| 243 |
+
| 🎯 <a href="#-use-on-your-own-data">Use on Your Own Data</a>
|
| 244 |
+
| 📁 <a href="#-project-structure">Project Structure</a>
|
| 245 |
+
| 🔗 <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
|