Upload via Autoresearch export_data
Browse files- README.md +84 -1
- export/config.json +55 -0
- export/model.safetensors +3 -0
- modular_ts/NOTICE +6 -0
- modular_ts/TIMESTRAL_LICENSE +202 -0
- modular_ts/distill.py +177 -0
- modular_ts/eval_gift.py +261 -0
- modular_ts/model.py +220 -0
- modular_ts/realdata.py +363 -0
- modular_ts/realdata2.py +81 -0
- modular_ts/synth.py +296 -0
- modular_ts/synth2.py +206 -0
- modular_ts/teacher_gen.py +139 -0
- modular_ts/train.py +225 -0
- modular_ts/tta.py +147 -0
- results/all_results.csv +98 -0
README.md
CHANGED
|
@@ -1,3 +1,86 @@
|
|
| 1 |
---
|
| 2 |
-
license:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
license: cc-by-nc-4.0
|
| 3 |
+
library_name: pytorch
|
| 4 |
+
pipeline_tag: time-series-forecasting
|
| 5 |
+
tags:
|
| 6 |
+
- time-series
|
| 7 |
+
- forecasting
|
| 8 |
+
- foundation-model
|
| 9 |
+
- zero-shot
|
| 10 |
+
- probabilistic-forecasting
|
| 11 |
+
- gift-eval
|
| 12 |
---
|
| 13 |
+
|
| 14 |
+
# Modular-TS-2.4M
|
| 15 |
+
|
| 16 |
+
**A 2.4M-parameter time-series foundation model, pretrained from scratch by [Modular Computing](https://huggingface.co/Modularcomputing).**
|
| 17 |
+
|
| 18 |
+
Modular-TS-2.4M is a small, fast, zero-shot probabilistic forecaster: give it any univariate or multivariate series and it returns 21 quantiles for every future step, with no fine-tuning. It is built for the GPU-poor: it runs comfortably on a CPU or a phone-class device.
|
| 19 |
+
|
| 20 |
+
## Results (GIFT-Eval, full 97 configurations, zero-shot)
|
| 21 |
+
|
| 22 |
+
Geometric mean relative to Seasonal Naive (lower is better).
|
| 23 |
+
|
| 24 |
+
| Model | Params | MASE | CRPS |
|
| 25 |
+
|---|---|---|---|
|
| 26 |
+
| **Modular-TS-2.4M (ours)** | **2.41M** | **0.745** | **0.511** |
|
| 27 |
+
| IBM TTM-R3 (PT) | ~1–5M | 0.724 | 0.520 |
|
| 28 |
+
| TimeSSM-2.5M | 2.5M | 0.754 | 0.513 |
|
| 29 |
+
| Seasonal Naive | – | 1.000 | 1.000 |
|
| 30 |
+
|
| 31 |
+
- Best CRPS (probabilistic accuracy) of any open model at 2.5M parameters or fewer that we evaluated.
|
| 32 |
+
- Beats TimeSSM-2.5M on both metrics and TTM-R3 on CRPS; TTM-R3 is still ahead on MASE.
|
| 33 |
+
- Our numbers are strictly zero-shot: the model never sees a test dataset's own training split at inference time. (TTM-R3-PT uses each dataset's train split for backtest ensembling.)
|
| 34 |
+
- Evaluated with the official GIFT-Eval harness at leaderboard settings; the same harness reproduces published leaderboard scores for reference models exactly.
|
| 35 |
+
|
| 36 |
+
## Model
|
| 37 |
+
|
| 38 |
+
| | |
|
| 39 |
+
|---|---|
|
| 40 |
+
| Architecture | Decoder-style patch transformer (Timestral-style), d_model 128, 2 heads, MLP 320, 8 layers alternating time and variate attention, patch 32 |
|
| 41 |
+
| Parameters | 2,407,680 (embed 107,392 / trunk 1,804,672 / head 495,616) |
|
| 42 |
+
| Context | up to 8,192 steps |
|
| 43 |
+
| Output | 21 monotone quantiles per step (plus mean) |
|
| 44 |
+
|
| 45 |
+
The architecture code adapts layers from Mistral AI's Apache-2.0 `timestral` inference package. **No Timestral weights were loaded, fine-tuned, or distilled at any point.** Every weight in this repo was trained from random initialisation.
|
| 46 |
+
|
| 47 |
+
## Training data
|
| 48 |
+
|
| 49 |
+
- **Synthetic (majority):** a GPU-vectorised generator with Gaussian-process kernels, regime-switching Ornstein–Uhlenbeck processes, waveform and load-curve templates, steps, growth curves, intermittent counts, multivariate lead-lag mixing, covariates and missingness, followed by an augmentation cascade.
|
| 50 |
+
- **Real (~40% in the final stage):** GiftEvalPretrain, resolution-sampled with TSMixup, after a contamination filter that removes any dataset overlapping the GIFT-Eval test sets.
|
| 51 |
+
|
| 52 |
+
## Usage
|
| 53 |
+
|
| 54 |
+
```python
|
| 55 |
+
from huggingface_hub import snapshot_download
|
| 56 |
+
path = snapshot_download("Modularcomputing/Modular-TS-2.4M")
|
| 57 |
+
# path/export/model.safetensors + path/export/config.json
|
| 58 |
+
# Load with the bundled modular_ts package (see modular_ts/README.md),
|
| 59 |
+
# which wraps the Apache-2.0 timestral forecaster around these weights.
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
Files:
|
| 63 |
+
- `export/model.safetensors`, `export/config.json`: inference weights and config
|
| 64 |
+
- `modular_ts/`: training, synthetic data generation and evaluation code
|
| 65 |
+
- `results/all_results.csv`: per-configuration GIFT-Eval scores
|
| 66 |
+
|
| 67 |
+
## Licence
|
| 68 |
+
|
| 69 |
+
- **Weights:** CC BY-NC 4.0 (research and non-commercial use), because the real-data portion of training mixes datasets under several licences.
|
| 70 |
+
- **Code (`modular_ts/`):** Apache-2.0, including the adapted `timestral` components (see `modular_ts/NOTICE`).
|
| 71 |
+
|
| 72 |
+
## Limitations
|
| 73 |
+
|
| 74 |
+
- Trails TTM-R3 on point accuracy (MASE); weakest on some long-horizon, high-frequency energy and weather series.
|
| 75 |
+
- A small model: it captures common seasonal and trend structure well but can miss rare regime changes.
|
| 76 |
+
|
| 77 |
+
## Citation
|
| 78 |
+
|
| 79 |
+
```bibtex
|
| 80 |
+
@misc{modularts2026,
|
| 81 |
+
title = {Modular-TS-2.4M: a small time-series foundation model trained from scratch},
|
| 82 |
+
author = {Akuegbo, Chima Emmanuel and Modular Computing},
|
| 83 |
+
year = {2026},
|
| 84 |
+
url = {https://huggingface.co/Modularcomputing/Modular-TS-2.4M}
|
| 85 |
+
}
|
| 86 |
+
```
|
export/config.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"patch_size": 32,
|
| 3 |
+
"d_model": 128,
|
| 4 |
+
"num_heads": 2,
|
| 5 |
+
"mlp_hidden_dim": 320,
|
| 6 |
+
"block_schedule": [
|
| 7 |
+
"time",
|
| 8 |
+
"time",
|
| 9 |
+
"time",
|
| 10 |
+
"time_variate",
|
| 11 |
+
"time",
|
| 12 |
+
"time",
|
| 13 |
+
"time",
|
| 14 |
+
"time_variate"
|
| 15 |
+
],
|
| 16 |
+
"quantile_knots": [
|
| 17 |
+
0.01,
|
| 18 |
+
0.05,
|
| 19 |
+
0.1,
|
| 20 |
+
0.15,
|
| 21 |
+
0.2,
|
| 22 |
+
0.25,
|
| 23 |
+
0.3,
|
| 24 |
+
0.35,
|
| 25 |
+
0.4,
|
| 26 |
+
0.45,
|
| 27 |
+
0.5,
|
| 28 |
+
0.55,
|
| 29 |
+
0.6,
|
| 30 |
+
0.65,
|
| 31 |
+
0.7,
|
| 32 |
+
0.75,
|
| 33 |
+
0.8,
|
| 34 |
+
0.85,
|
| 35 |
+
0.9,
|
| 36 |
+
0.95,
|
| 37 |
+
0.99
|
| 38 |
+
],
|
| 39 |
+
"block_size": 32,
|
| 40 |
+
"context_length": 8192,
|
| 41 |
+
"rope_theta": 10000.0,
|
| 42 |
+
"rope_partial_factor": 0.5,
|
| 43 |
+
"norm_eps": 1e-06,
|
| 44 |
+
"attn_bias": false,
|
| 45 |
+
"mlp_bias": false,
|
| 46 |
+
"time_attn_gate": "sdpa_sigmoid",
|
| 47 |
+
"variate_attn_gate": "sdpa_sigmoid",
|
| 48 |
+
"minimum_scale": 1e-06,
|
| 49 |
+
"use_asinh": true,
|
| 50 |
+
"stabilize_with_global": false,
|
| 51 |
+
"covariate_enabled": true,
|
| 52 |
+
"split_context_horizon_attn": true,
|
| 53 |
+
"attend_missing_patches": true,
|
| 54 |
+
"quantile_width_factor": 3.0
|
| 55 |
+
}
|
export/model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:04d4c57329c8fe0a67716bff5207adea92c632d69810d33e877d64c9299bf4dc
|
| 3 |
+
size 9638720
|
modular_ts/NOTICE
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Modular-TS — Apache-2.0.
|
| 2 |
+
This package imports and adapts code from the `timestral` inference package
|
| 3 |
+
(https://github.com/mistralai/timestral, Copyright Mistral AI, Apache-2.0; see TIMESTRAL_LICENSE):
|
| 4 |
+
layers, masks, scaler, and the forecaster used at evaluation time.
|
| 5 |
+
No Timestral model weights are loaded, fine-tuned or distilled anywhere in this code.
|
| 6 |
+
All weights are trained from scratch.
|
modular_ts/TIMESTRAL_LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
Apache License
|
| 3 |
+
Version 2.0, January 2004
|
| 4 |
+
http://www.apache.org/licenses/
|
| 5 |
+
|
| 6 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 7 |
+
|
| 8 |
+
1. Definitions.
|
| 9 |
+
|
| 10 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 11 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 12 |
+
|
| 13 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 14 |
+
the copyright owner that is granting the License.
|
| 15 |
+
|
| 16 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 17 |
+
other entities that control, are controlled by, or are under common
|
| 18 |
+
control with that entity. For the purposes of this definition,
|
| 19 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 20 |
+
direction or management of such entity, whether by contract or
|
| 21 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 22 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 23 |
+
|
| 24 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 25 |
+
exercising permissions granted by this License.
|
| 26 |
+
|
| 27 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 28 |
+
including but not limited to software source code, documentation
|
| 29 |
+
source, and configuration files.
|
| 30 |
+
|
| 31 |
+
"Object" form shall mean any form resulting from mechanical
|
| 32 |
+
transformation or translation of a Source form, including but
|
| 33 |
+
not limited to compiled object code, generated documentation,
|
| 34 |
+
and conversions to other media types.
|
| 35 |
+
|
| 36 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 37 |
+
Object form, made available under the License, as indicated by a
|
| 38 |
+
copyright notice that is included in or attached to the work
|
| 39 |
+
(an example is provided in the Appendix below).
|
| 40 |
+
|
| 41 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 42 |
+
form, that is based on (or derived from) the Work and for which the
|
| 43 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 44 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 45 |
+
of this License, Derivative Works shall not include works that remain
|
| 46 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 47 |
+
the Work and Derivative Works thereof.
|
| 48 |
+
|
| 49 |
+
"Contribution" shall mean any work of authorship, including
|
| 50 |
+
the original version of the Work and any modifications or additions
|
| 51 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 52 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 53 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 54 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 55 |
+
means any form of electronic, verbal, or written communication sent
|
| 56 |
+
to the Licensor or its representatives, including but not limited to
|
| 57 |
+
communication on electronic mailing lists, source code control systems,
|
| 58 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 59 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 60 |
+
excluding communication that is conspicuously marked or otherwise
|
| 61 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 62 |
+
|
| 63 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 64 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 65 |
+
subsequently incorporated within the Work.
|
| 66 |
+
|
| 67 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 68 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 69 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 70 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 71 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 72 |
+
Work and such Derivative Works in Source or Object form.
|
| 73 |
+
|
| 74 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 75 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 76 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 77 |
+
(except as stated in this section) patent license to make, have made,
|
| 78 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 79 |
+
where such license applies only to those patent claims licensable
|
| 80 |
+
by such Contributor that are necessarily infringed by their
|
| 81 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 82 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 83 |
+
institute patent litigation against any entity (including a
|
| 84 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 85 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 86 |
+
or contributory patent infringement, then any patent licenses
|
| 87 |
+
granted to You under this License for that Work shall terminate
|
| 88 |
+
as of the date such litigation is filed.
|
| 89 |
+
|
| 90 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 91 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 92 |
+
modifications, and in Source or Object form, provided that You
|
| 93 |
+
meet the following conditions:
|
| 94 |
+
|
| 95 |
+
(a) You must give any other recipients of the Work or
|
| 96 |
+
Derivative Works a copy of this License; and
|
| 97 |
+
|
| 98 |
+
(b) You must cause any modified files to carry prominent notices
|
| 99 |
+
stating that You changed the files; and
|
| 100 |
+
|
| 101 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 102 |
+
that You distribute, all copyright, patent, trademark, and
|
| 103 |
+
attribution notices from the Source form of the Work,
|
| 104 |
+
excluding those notices that do not pertain to any part of
|
| 105 |
+
the Derivative Works; and
|
| 106 |
+
|
| 107 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 108 |
+
distribution, then any Derivative Works that You distribute must
|
| 109 |
+
include a readable copy of the attribution notices contained
|
| 110 |
+
within such NOTICE file, excluding those notices that do not
|
| 111 |
+
pertain to any part of the Derivative Works, in at least one
|
| 112 |
+
of the following places: within a NOTICE text file distributed
|
| 113 |
+
as part of the Derivative Works; within the Source form or
|
| 114 |
+
documentation, if provided along with the Derivative Works; or,
|
| 115 |
+
within a display generated by the Derivative Works, if and
|
| 116 |
+
wherever such third-party notices normally appear. The contents
|
| 117 |
+
of the NOTICE file are for informational purposes only and
|
| 118 |
+
do not modify the License. You may add Your own attribution
|
| 119 |
+
notices within Derivative Works that You distribute, alongside
|
| 120 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 121 |
+
that such additional attribution notices cannot be construed
|
| 122 |
+
as modifying the License.
|
| 123 |
+
|
| 124 |
+
You may add Your own copyright statement to Your modifications and
|
| 125 |
+
may provide additional or different license terms and conditions
|
| 126 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 127 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 128 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 129 |
+
the conditions stated in this License.
|
| 130 |
+
|
| 131 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 132 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 133 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 134 |
+
this License, without any additional terms or conditions.
|
| 135 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 136 |
+
the terms of any separate license agreement you may have executed
|
| 137 |
+
with Licensor regarding such Contributions.
|
| 138 |
+
|
| 139 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 140 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 141 |
+
except as required for reasonable and customary use in describing the
|
| 142 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 143 |
+
|
| 144 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 145 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 146 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 147 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 148 |
+
implied, including, without limitation, any warranties or conditions
|
| 149 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 150 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 151 |
+
appropriateness of using or redistributing the Work and assume any
|
| 152 |
+
risks associated with Your exercise of permissions under this License.
|
| 153 |
+
|
| 154 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 155 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 156 |
+
unless required by applicable law (such as deliberate and grossly
|
| 157 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 158 |
+
liable to You for damages, including any direct, indirect, special,
|
| 159 |
+
incidental, or consequential damages of any character arising as a
|
| 160 |
+
result of this License or out of the use or inability to use the
|
| 161 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 162 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 163 |
+
other commercial damages or losses), even if such Contributor
|
| 164 |
+
has been advised of the possibility of such damages.
|
| 165 |
+
|
| 166 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 167 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 168 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 169 |
+
or other liability obligations and/or rights consistent with this
|
| 170 |
+
License. However, in accepting such obligations, You may act only
|
| 171 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 172 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 173 |
+
defend, and hold each Contributor harmless for any liability
|
| 174 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 175 |
+
of your accepting any such warranty or additional liability.
|
| 176 |
+
|
| 177 |
+
END OF TERMS AND CONDITIONS
|
| 178 |
+
|
| 179 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 180 |
+
|
| 181 |
+
To apply the Apache License to your work, attach the following
|
| 182 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 183 |
+
replaced with your own identifying information. (Don't include
|
| 184 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 185 |
+
comment syntax for the file format. We also recommend that a
|
| 186 |
+
file or class name and description of purpose be included on the
|
| 187 |
+
same "printed page" as the copyright notice for easier
|
| 188 |
+
identification within third-party archives.
|
| 189 |
+
|
| 190 |
+
Copyright [yyyy] [name of copyright owner]
|
| 191 |
+
|
| 192 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 193 |
+
you may not use this file except in compliance with the License.
|
| 194 |
+
You may obtain a copy of the License at
|
| 195 |
+
|
| 196 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 197 |
+
|
| 198 |
+
Unless required by applicable law or agreed to in writing, software
|
| 199 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 200 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 201 |
+
See the License for the specific language governing permissions and
|
| 202 |
+
limitations under the License.
|
modular_ts/distill.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Stage B: fine-tune a Stage-A checkpoint with ground-truth pinball + teacher-ensemble pinball (TimesFM-2.5 + Chronos-2).
|
| 2 |
+
|
| 3 |
+
python -m modular_ts.distill --init runs/run2_8k/ckpt.pt --teach teach/synth,teach/gep --lam 0.5 --minutes 45 --out runs/run3
|
| 4 |
+
|
| 5 |
+
Batches: with prob --p_distill a teacher batch (V=1, T=C+F windows, min-context split s=C/32;
|
| 6 |
+
synthetic rows: GT pinball + teacher pinball; GiftEvalPretrain rows: teacher pinball ONLY, their
|
| 7 |
+
future is filled with the teacher-ensemble median just for the normalisation frame and never used
|
| 8 |
+
as a target), else a fresh synthetic Stage-A batch (GT pinball only) to avoid forgetting.
|
| 9 |
+
Teacher ensemble = mean of the two teachers' quantiles (Vincentisation) at 0.1..0.9.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import argparse
|
| 15 |
+
import glob
|
| 16 |
+
import json
|
| 17 |
+
import math
|
| 18 |
+
import os
|
| 19 |
+
import time
|
| 20 |
+
|
| 21 |
+
import numpy as np
|
| 22 |
+
import torch
|
| 23 |
+
|
| 24 |
+
from modular_ts.model import ModularTS, count_params, make_config
|
| 25 |
+
from modular_ts.synth import SynthStream
|
| 26 |
+
from modular_ts.train import export, lr_at
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def load_teach(dirs, dev, max_per_dir=0):
|
| 30 |
+
ctx, fut, q, src = [], [], [], []
|
| 31 |
+
for si, d in enumerate(dirs):
|
| 32 |
+
files = sorted(glob.glob(os.path.join(d, "shard_*.npz")))
|
| 33 |
+
n = 0
|
| 34 |
+
for f in files:
|
| 35 |
+
z = np.load(f)
|
| 36 |
+
qe = 0.5 * (z["q_tfm"].astype(np.float32) + z["q_cr2"].astype(np.float32))
|
| 37 |
+
qe = np.sort(qe, -1)
|
| 38 |
+
ctx.append(z["ctx"]); fut.append(z["fut"]); q.append(qe); src.append(np.full(len(qe), si))
|
| 39 |
+
n += len(qe)
|
| 40 |
+
if max_per_dir and n >= max_per_dir:
|
| 41 |
+
break
|
| 42 |
+
print(f"[teach] {d}: {n} windows from {len(files)} shards", flush=True)
|
| 43 |
+
c = torch.from_numpy(np.concatenate(ctx)).to(dev)
|
| 44 |
+
f = torch.from_numpy(np.concatenate(fut)).to(dev)
|
| 45 |
+
qq = torch.from_numpy(np.concatenate(q)).to(dev)
|
| 46 |
+
s = torch.from_numpy(np.concatenate(src)).to(dev)
|
| 47 |
+
return c, f, qq, s
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def main():
|
| 51 |
+
ap = argparse.ArgumentParser()
|
| 52 |
+
ap.add_argument("--init", required=True, help="Stage-A ckpt.pt (model weights only are used)")
|
| 53 |
+
ap.add_argument("--teach", required=True, help="comma list of teacher shard dirs; dirs containing 'gep' are teacher-only")
|
| 54 |
+
ap.add_argument("--lam", type=float, default=0.5)
|
| 55 |
+
ap.add_argument("--p_distill", type=float, default=0.6)
|
| 56 |
+
ap.add_argument("--lr", type=float, default=3e-4)
|
| 57 |
+
ap.add_argument("--warmup", type=int, default=200)
|
| 58 |
+
ap.add_argument("--minutes", type=float, default=45)
|
| 59 |
+
ap.add_argument("--tokens_per_batch", type=int, default=262144)
|
| 60 |
+
ap.add_argument("--ctx", type=int, default=0, help="model context_length for export (0 = from init ckpt)")
|
| 61 |
+
ap.add_argument("--compile", action="store_true")
|
| 62 |
+
ap.add_argument("--log_every", type=int, default=100)
|
| 63 |
+
ap.add_argument("--seed", type=int, default=7)
|
| 64 |
+
ap.add_argument("--out", required=True)
|
| 65 |
+
args = ap.parse_args()
|
| 66 |
+
os.makedirs(args.out, exist_ok=True)
|
| 67 |
+
dev = torch.device("cuda")
|
| 68 |
+
torch.manual_seed(args.seed)
|
| 69 |
+
torch.backends.cuda.matmul.allow_tf32 = True
|
| 70 |
+
st = torch.load(args.init, map_location=dev)
|
| 71 |
+
ctx_len = args.ctx or st["args"]["ctx"]
|
| 72 |
+
cfg = make_config(st["args"].get("size", "2.5M"), context_length=ctx_len)
|
| 73 |
+
model = ModularTS(cfg).to(dev)
|
| 74 |
+
model.load_state_dict(st["model"])
|
| 75 |
+
print("params", count_params(model), "init", args.init, "step", st["step"], flush=True)
|
| 76 |
+
|
| 77 |
+
dirs = [d for d in args.teach.split(",") if d]
|
| 78 |
+
tc, tf, tq, tsrc = load_teach(dirs, dev)
|
| 79 |
+
teacher_only = torch.tensor(["gep" in d for d in dirs], device=dev)[tsrc]
|
| 80 |
+
C, F, H = tc.shape[1], tf.shape[1], tq.shape[1]
|
| 81 |
+
T = C + F
|
| 82 |
+
P = cfg.patch_size
|
| 83 |
+
Bd = max(1, args.tokens_per_batch // (T // P))
|
| 84 |
+
nT = tc.shape[0]
|
| 85 |
+
print(f"[teach] total {nT} windows (teacher-only {int(teacher_only.sum())}), C={C} F={F} H={H}, rows/batch {Bd}", flush=True)
|
| 86 |
+
# hold out 2% of synthetic teacher windows for a GT/KD validation number
|
| 87 |
+
g = torch.Generator(device=dev); g.manual_seed(args.seed)
|
| 88 |
+
perm = torch.randperm(nT, generator=g, device=dev)
|
| 89 |
+
syn_idx = perm[~teacher_only[perm]]
|
| 90 |
+
val_idx = syn_idx[:min(4096, max(1, len(syn_idx) // 50))]
|
| 91 |
+
trmask = torch.ones(nT, dtype=torch.bool, device=dev); trmask[val_idx] = False
|
| 92 |
+
tr_idx = torch.nonzero(trmask).squeeze(1)
|
| 93 |
+
|
| 94 |
+
def distill_batch(idx):
|
| 95 |
+
c, f, q, to = tc[idx], tf[idx], tq[idx], teacher_only[idx]
|
| 96 |
+
med = q[:, :, 4]
|
| 97 |
+
fill = torch.full_like(f, float("nan"))
|
| 98 |
+
fill[:, :H] = med
|
| 99 |
+
f = torch.where(to[:, None], fill, f)
|
| 100 |
+
vals = torch.cat([c, f], 1)
|
| 101 |
+
obs = torch.isfinite(vals)
|
| 102 |
+
vals = torch.where(obs, vals, torch.zeros_like(vals))
|
| 103 |
+
teach = torch.zeros(len(idx), T, q.shape[-1], device=dev)
|
| 104 |
+
teach[:, C:C + H] = q
|
| 105 |
+
tobs = torch.zeros(len(idx), T, dtype=torch.bool, device=dev)
|
| 106 |
+
tobs[:, C:C + H] = True
|
| 107 |
+
R = len(idx)
|
| 108 |
+
return dict(values=vals[:, None], observed=obs[:, None], roles=torch.zeros(R, 1, dtype=torch.long, device=dev),
|
| 109 |
+
s=torch.full((R,), C // P, dtype=torch.long, device=dev), teacher=teach, teacher_obs=tobs,
|
| 110 |
+
gt_row=(~to).float())
|
| 111 |
+
|
| 112 |
+
synth = SynthStream(4096, args.tokens_per_batch, dev, seed=args.seed + 99)
|
| 113 |
+
|
| 114 |
+
decay, no_decay = [], []
|
| 115 |
+
for n, p in model.named_parameters():
|
| 116 |
+
(decay if p.dim() >= 2 else no_decay).append(p)
|
| 117 |
+
opt = torch.optim.AdamW([{"params": decay, "weight_decay": 0.05}, {"params": no_decay, "weight_decay": 0.0}],
|
| 118 |
+
lr=args.lr, betas=(0.9, 0.95), eps=1e-8, fused=True)
|
| 119 |
+
fwd = torch.compile(model.forward_train, dynamic=False) if args.compile else model.forward_train
|
| 120 |
+
amp = torch.autocast(device_type="cuda", dtype=torch.bfloat16)
|
| 121 |
+
lam = args.lam
|
| 122 |
+
|
| 123 |
+
def evaluate():
|
| 124 |
+
model.eval()
|
| 125 |
+
out = {"val_gt": 0.0, "val_kd": 0.0}
|
| 126 |
+
with torch.no_grad(), amp:
|
| 127 |
+
for i in range(0, len(val_idx), Bd):
|
| 128 |
+
b = distill_batch(val_idx[i:i + Bd])
|
| 129 |
+
l, s_ = model.forward_train(b["values"], b["observed"], b["roles"], b["s"], teacher=b["teacher"],
|
| 130 |
+
teacher_obs=b["teacher_obs"], gt_row=b["gt_row"], lam=lam)
|
| 131 |
+
out["val_gt"] += float(s_["pinball"]); out["val_kd"] += float(s_["kd"])
|
| 132 |
+
n = math.ceil(len(val_idx) / Bd)
|
| 133 |
+
model.train()
|
| 134 |
+
return {k: v / n for k, v in out.items()}
|
| 135 |
+
|
| 136 |
+
log = open(os.path.join(args.out, "log.jsonl"), "a")
|
| 137 |
+
rng = np.random.default_rng(args.seed)
|
| 138 |
+
t0 = time.time(); step = 0
|
| 139 |
+
rec0 = {"step": 0, **evaluate()}
|
| 140 |
+
print(json.dumps(rec0), flush=True); log.write(json.dumps(rec0) + "\n")
|
| 141 |
+
model.train()
|
| 142 |
+
while (time.time() - t0) / 60 < args.minutes:
|
| 143 |
+
frac = (time.time() - t0) / 60 / args.minutes
|
| 144 |
+
lr = lr_at(step, max(step + 1, int(step / max(frac, 1e-6))) if step > args.warmup else 10**8, args.warmup, args.lr)
|
| 145 |
+
for gp in opt.param_groups:
|
| 146 |
+
gp["lr"] = lr
|
| 147 |
+
is_d = rng.random() < args.p_distill
|
| 148 |
+
with amp:
|
| 149 |
+
if is_d:
|
| 150 |
+
idx = tr_idx[torch.randint(0, len(tr_idx), (Bd,), device=dev)]
|
| 151 |
+
b = distill_batch(idx)
|
| 152 |
+
loss, s_ = fwd(b["values"], b["observed"], b["roles"], b["s"], False, b["teacher"], b["teacher_obs"], b["gt_row"], lam)
|
| 153 |
+
else:
|
| 154 |
+
b = synth.next_batch()
|
| 155 |
+
loss, s_ = fwd(b["values"], b["observed"], b["roles"], b["s"])
|
| 156 |
+
if not torch.isfinite(loss):
|
| 157 |
+
opt.zero_grad(set_to_none=True); step += 1
|
| 158 |
+
continue
|
| 159 |
+
loss.backward()
|
| 160 |
+
gn = torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
|
| 161 |
+
opt.step(); opt.zero_grad(set_to_none=True)
|
| 162 |
+
step += 1
|
| 163 |
+
if step % args.log_every == 0:
|
| 164 |
+
rec = {"step": step, "lr": lr, "loss": float(loss), "kind": "distill" if is_d else "synth",
|
| 165 |
+
"kd": float(s_.get("kd", float("nan"))), "gnorm": float(gn), "min": (time.time() - t0) / 60}
|
| 166 |
+
if step % (args.log_every * 10) == 0:
|
| 167 |
+
rec.update(evaluate())
|
| 168 |
+
print(json.dumps(rec), flush=True); log.write(json.dumps(rec) + "\n"); log.flush()
|
| 169 |
+
torch.save({"model": model.state_dict(), "step": step, "args": {**st["args"], "distill": vars(args)}}, os.path.join(args.out, "ckpt.pt"))
|
| 170 |
+
export(model, os.path.join(args.out, "export"))
|
| 171 |
+
fin = {"steps": step, **evaluate(), "minutes": (time.time() - t0) / 60, "lam": lam}
|
| 172 |
+
print("FINAL", json.dumps(fin), flush=True)
|
| 173 |
+
json.dump(fin, open(os.path.join(args.out, "final.json"), "w"))
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
if __name__ == "__main__":
|
| 177 |
+
main()
|
modular_ts/eval_gift.py
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""GIFT-Eval harness for Modular-TS (official gift-eval Dataset + gluonts evaluate_model).
|
| 2 |
+
|
| 3 |
+
Adapted from gift-eval/notebooks/timestral.ipynb (Apache-2.0). Our weights load into
|
| 4 |
+
the Apache-2.0 `timestral.TimestralForecaster` (block-AR decoding, advancing refine frame,
|
| 5 |
+
clampsym TTA). Metrics, settings and the leaderboard aggregate match the official board:
|
| 6 |
+
evaluate_model(axis=None, mask_invalid_label=True, allow_nan_forecast=False), 9
|
| 7 |
+
quantiles 0.1..0.9 for CRPS (mean_weighted_sum_quantile_loss), then the geometric mean
|
| 8 |
+
over configs of (model / Seasonal_Naive) for MASE and CRPS.
|
| 9 |
+
|
| 10 |
+
python -m modular_ts.eval_gift --export runs/smoke/export --mode quick --out runs/smoke/gift_quick
|
| 11 |
+
python -m modular_ts.eval_gift --export ... --mode full # all 97 configs -> all_results.csv
|
| 12 |
+
|
| 13 |
+
Requires: pip install --no-deps git+https://github.com/SalesforceAIResearch/gift-eval
|
| 14 |
+
pip install "gluonts~=0.15.1" python-dotenv toolz orjson datasets pyarrow pandas
|
| 15 |
+
Data: Salesforce/GiftEval is downloaded (only the needed folders) to $GIFT_EVAL.
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
|
| 20 |
+
import argparse
|
| 21 |
+
import json
|
| 22 |
+
import logging
|
| 23 |
+
import math
|
| 24 |
+
import os
|
| 25 |
+
import time
|
| 26 |
+
import warnings
|
| 27 |
+
from pathlib import Path
|
| 28 |
+
|
| 29 |
+
import numpy as np
|
| 30 |
+
import pandas as pd
|
| 31 |
+
import torch
|
| 32 |
+
|
| 33 |
+
import modular_ts.model # noqa: F401 (puts the vendored timestral package on sys.path)
|
| 34 |
+
|
| 35 |
+
ROOT = Path(__file__).resolve().parent.parent
|
| 36 |
+
LB_DIR = ROOT / "gift-eval" / "results"
|
| 37 |
+
PROPS = ROOT / "gift-eval" / "notebooks" / "dataset_properties.json"
|
| 38 |
+
|
| 39 |
+
SHORT_DATASETS = "m4_yearly m4_quarterly m4_monthly m4_weekly m4_daily m4_hourly electricity/15T electricity/H electricity/D electricity/W solar/10T solar/H solar/D solar/W hospital covid_deaths us_births/D us_births/M us_births/W saugeenday/D saugeenday/M saugeenday/W temperature_rain_with_missing kdd_cup_2018_with_missing/H kdd_cup_2018_with_missing/D car_parts_with_missing restaurant hierarchical_sales/D hierarchical_sales/W LOOP_SEATTLE/5T LOOP_SEATTLE/H LOOP_SEATTLE/D SZ_TAXI/15T SZ_TAXI/H M_DENSE/H M_DENSE/D ett1/15T ett1/H ett1/D ett1/W ett2/15T ett2/H ett2/D ett2/W jena_weather/10T jena_weather/H jena_weather/D bitbrains_fast_storage/5T bitbrains_fast_storage/H bitbrains_rnd/5T bitbrains_rnd/H bizitobs_application bizitobs_service bizitobs_l2c/5T bizitobs_l2c/H"
|
| 40 |
+
MED_LONG_DATASETS = "electricity/15T electricity/H solar/10T solar/H kdd_cup_2018_with_missing/H LOOP_SEATTLE/5T LOOP_SEATTLE/H SZ_TAXI/15T M_DENSE/H ett1/15T ett1/H ett2/15T ett2/H jena_weather/10T jena_weather/H bitbrains_fast_storage/5T bitbrains_rnd/5T bizitobs_application bizitobs_service bizitobs_l2c/5T bizitobs_l2c/H"
|
| 41 |
+
PRETTY = {"saugeenday": "saugeen", "temperature_rain_with_missing": "temperature_rain",
|
| 42 |
+
"kdd_cup_2018_with_missing": "kdd_cup_2018", "car_parts_with_missing": "car_parts"}
|
| 43 |
+
# Quick subset: small, domain-diverse configs (12 short + 2 long). Used for smoke checks only.
|
| 44 |
+
QUICK = [("m4_hourly", "short"), ("m4_weekly", "short"), ("us_births/D", "short"), ("saugeenday/D", "short"),
|
| 45 |
+
("hospital", "short"), ("covid_deaths", "short"), ("ett1/H", "short"), ("jena_weather/H", "short"),
|
| 46 |
+
("bizitobs_service", "short"), ("car_parts_with_missing", "short"), ("electricity/D", "short"),
|
| 47 |
+
("LOOP_SEATTLE/D", "short"), ("ett2/H", "long"), ("bizitobs_l2c/H", "long")]
|
| 48 |
+
QUANTILE_LEVELS = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
|
| 49 |
+
METRIC_COLUMNS = ["MSE[mean]", "MSE[0.5]", "MAE[0.5]", "MASE[0.5]", "MAPE[0.5]", "sMAPE[0.5]", "MSIS",
|
| 50 |
+
"RMSE[mean]", "NRMSE[mean]", "ND[0.5]", "mean_weighted_sum_quantile_loss"]
|
| 51 |
+
COMPARE = ["Seasonal_Naive", "TimeSSM-2.5M", "Timestral-5M", "TTM-R3-PT", "TinyCast"]
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def all_configs(properties):
|
| 55 |
+
short, ml = SHORT_DATASETS.split(), MED_LONG_DATASETS.split()
|
| 56 |
+
out = []
|
| 57 |
+
for name in dict.fromkeys(short + ml):
|
| 58 |
+
for term in ["short", "medium", "long"]:
|
| 59 |
+
if term != "short" and name not in ml:
|
| 60 |
+
continue
|
| 61 |
+
out.append((name, term, config_key(name, term, properties)))
|
| 62 |
+
assert len(out) == 97
|
| 63 |
+
return out
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def config_key(name, term, properties):
|
| 67 |
+
if "/" in name:
|
| 68 |
+
k, f = name.split("/")
|
| 69 |
+
k = PRETTY.get(k.lower(), k.lower())
|
| 70 |
+
else:
|
| 71 |
+
k = PRETTY.get(name.lower(), name.lower())
|
| 72 |
+
f = properties[k]["frequency"]
|
| 73 |
+
return f"{k}/{f}/{term}"
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def ensure_data(names, root):
|
| 77 |
+
from huggingface_hub import snapshot_download
|
| 78 |
+
tops = sorted({n.split("/")[0] for n in names})
|
| 79 |
+
need = [t for t in tops if not (Path(root) / t).exists()]
|
| 80 |
+
if need:
|
| 81 |
+
snapshot_download("Salesforce/GiftEval", repo_type="dataset", local_dir=root,
|
| 82 |
+
allow_patterns=[f"{t}/**" for t in need])
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
class Predictor:
|
| 86 |
+
def __init__(self, fc, prediction_length, use_past_covariates, max_rows=1024, max_tokens=2**24):
|
| 87 |
+
self.fc, self.h, self.cov = fc, prediction_length, use_past_covariates
|
| 88 |
+
self.max_rows, self.max_tokens = max_rows, max_tokens
|
| 89 |
+
lv = list(fc.quantile_levels)
|
| 90 |
+
self.qi = [next(i for i, l in enumerate(lv) if abs(l - q) < 1e-9) for q in QUANTILE_LEVELS]
|
| 91 |
+
self.keys = [str(q) for q in QUANTILE_LEVELS] + ["mean"]
|
| 92 |
+
|
| 93 |
+
def predict(self, test_input):
|
| 94 |
+
from gluonts.model.forecast import QuantileForecast
|
| 95 |
+
entries = list(test_input)
|
| 96 |
+
mc = self.fc.max_context
|
| 97 |
+
tg, cv = [], []
|
| 98 |
+
for e in entries:
|
| 99 |
+
t = np.asarray(e["target"], dtype=np.float32)
|
| 100 |
+
tg.append((t[None] if t.ndim == 1 else t)[:, -mc:])
|
| 101 |
+
c = e.get("past_feat_dynamic_real") if self.cov else None
|
| 102 |
+
if c is not None:
|
| 103 |
+
c = np.asarray(c, dtype=np.float32)
|
| 104 |
+
c = (c[None] if c.ndim == 1 else c)[:, -mc:]
|
| 105 |
+
cv.append(c)
|
| 106 |
+
nrows = tg[0].shape[0] + (cv[0].shape[0] if cv[0] is not None else 0)
|
| 107 |
+
outs = [None] * len(entries)
|
| 108 |
+
order = sorted(range(len(entries)), key=lambda i: tg[i].shape[-1])
|
| 109 |
+
s = 0
|
| 110 |
+
while s < len(order):
|
| 111 |
+
e_ = s + 1
|
| 112 |
+
while e_ < len(order):
|
| 113 |
+
L = tg[order[e_]].shape[-1] + self.h
|
| 114 |
+
if (e_ + 1 - s) * nrows > self.max_rows or (e_ + 1 - s) * nrows * L > self.max_tokens:
|
| 115 |
+
break
|
| 116 |
+
e_ += 1
|
| 117 |
+
self._batch(order[s:e_], tg, cv, outs)
|
| 118 |
+
s = e_
|
| 119 |
+
res = []
|
| 120 |
+
for e, q in zip(entries, outs):
|
| 121 |
+
arr = np.concatenate([q, q[..., QUANTILE_LEVELS.index(0.5), None]], -1).transpose(2, 1, 0)
|
| 122 |
+
if np.asarray(e["target"]).ndim == 1:
|
| 123 |
+
arr = arr[..., 0]
|
| 124 |
+
res.append(QuantileForecast(forecast_arrays=arr, forecast_keys=self.keys,
|
| 125 |
+
start_date=e["start"] + np.asarray(e["target"]).shape[-1], item_id=e.get("item_id")))
|
| 126 |
+
return res
|
| 127 |
+
|
| 128 |
+
def _batch(self, idx, tg, cv, outs):
|
| 129 |
+
try:
|
| 130 |
+
L = max(tg[i].shape[-1] for i in idx)
|
| 131 |
+
ctx = np.stack([_lpad(tg[i], L) for i in idx])
|
| 132 |
+
past = np.stack([_lpad(cv[i], L) for i in idx]) if cv[idx[0]] is not None else None
|
| 133 |
+
f = self.fc.forecast(ctx, self.h, past_covariates=past)
|
| 134 |
+
q = f.quantiles[..., self.qi].float().cpu().numpy()
|
| 135 |
+
except torch.cuda.OutOfMemoryError:
|
| 136 |
+
torch.cuda.empty_cache()
|
| 137 |
+
if len(idx) == 1:
|
| 138 |
+
raise
|
| 139 |
+
hlf = len(idx) // 2
|
| 140 |
+
self._batch(idx[:hlf], tg, cv, outs)
|
| 141 |
+
self._batch(idx[hlf:], tg, cv, outs)
|
| 142 |
+
return
|
| 143 |
+
for j, i in enumerate(idx):
|
| 144 |
+
outs[i] = q[j]
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def _lpad(x, L):
|
| 148 |
+
p = L - x.shape[-1]
|
| 149 |
+
return x if p == 0 else np.concatenate([np.full((x.shape[0], p), np.nan, np.float32), x], -1)
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def evaluate_config(fc, name, term):
|
| 153 |
+
from gift_eval.data import Dataset
|
| 154 |
+
from gluonts.ev.metrics import MAE, MAPE, MASE, MSE, MSIS, ND, NRMSE, RMSE, SMAPE, MeanWeightedSumQuantileLoss
|
| 155 |
+
from gluonts.model import evaluate_model
|
| 156 |
+
from gluonts.time_feature import get_seasonality
|
| 157 |
+
metrics = [MSE(forecast_type="mean"), MSE(forecast_type=0.5), MAE(), MASE(), MAPE(), SMAPE(), MSIS(), RMSE(),
|
| 158 |
+
NRMSE(), ND(), MeanWeightedSumQuantileLoss(quantile_levels=QUANTILE_LEVELS)]
|
| 159 |
+
ds = Dataset(name=name, term=term, to_univariate=False)
|
| 160 |
+
pred = Predictor(fc, ds.prediction_length, bool(ds.past_feat_dynamic_real_dim), max_rows=int(os.environ.get("MAX_ROWS", "1024")))
|
| 161 |
+
res = evaluate_model(pred, test_data=ds.test_data, metrics=metrics, batch_size=512, axis=None,
|
| 162 |
+
mask_invalid_label=True, allow_nan_forecast=False, seasonality=get_seasonality(ds.freq))
|
| 163 |
+
return {k: float(res[k].iloc[0]) for k in res.columns}
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def gmean(x):
|
| 167 |
+
return float(math.exp(np.log(np.asarray(x, dtype=float)).mean()))
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def aggregate(per_cfg: dict[str, dict]) -> dict:
|
| 171 |
+
keys = sorted(per_cfg)
|
| 172 |
+
sn = pd.read_csv(LB_DIR / "Seasonal_Naive" / "all_results.csv").set_index("dataset")
|
| 173 |
+
out = {}
|
| 174 |
+
m = [per_cfg[k]["MASE[0.5]"] / sn.loc[k, "eval_metrics/MASE[0.5]"] for k in keys]
|
| 175 |
+
c = [per_cfg[k]["mean_weighted_sum_quantile_loss"] / sn.loc[k, "eval_metrics/mean_weighted_sum_quantile_loss"] for k in keys]
|
| 176 |
+
out["ours"] = {"MASE": gmean(m), "CRPS": gmean(c)}
|
| 177 |
+
for name in COMPARE:
|
| 178 |
+
p = LB_DIR / name / "all_results.csv"
|
| 179 |
+
if not p.exists():
|
| 180 |
+
continue
|
| 181 |
+
df = pd.read_csv(p).set_index("dataset")
|
| 182 |
+
if not all(k in df.index for k in keys):
|
| 183 |
+
continue
|
| 184 |
+
out[name] = {
|
| 185 |
+
"MASE": gmean([df.loc[k, "eval_metrics/MASE[0.5]"] / sn.loc[k, "eval_metrics/MASE[0.5]"] for k in keys]),
|
| 186 |
+
"CRPS": gmean([df.loc[k, "eval_metrics/mean_weighted_sum_quantile_loss"] / sn.loc[k, "eval_metrics/mean_weighted_sum_quantile_loss"] for k in keys]),
|
| 187 |
+
}
|
| 188 |
+
out["n_configs"] = len(keys)
|
| 189 |
+
return out
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def main():
|
| 193 |
+
ap = argparse.ArgumentParser()
|
| 194 |
+
ap.add_argument("--export", required=True)
|
| 195 |
+
ap.add_argument("--mode", default="quick", choices=["quick", "full"])
|
| 196 |
+
ap.add_argument("--configs", default="", help="override: 'name:term,name:term'")
|
| 197 |
+
ap.add_argument("--out", required=True)
|
| 198 |
+
ap.add_argument("--gift_eval_dir", default=os.environ.get("GIFT_EVAL", str(ROOT / "data" / "gift_eval")))
|
| 199 |
+
ap.add_argument("--max_context", type=int, default=0, help="0 = model's trained context_length")
|
| 200 |
+
ap.add_argument("--model_name", default="Modular-TS-2.5M")
|
| 201 |
+
ap.add_argument("--tta", default="", help="v3 test-time ensemble spec, e.g. 'c8192,c2048,d4' (modular_ts.tta)")
|
| 202 |
+
ap.add_argument("--tta_alpha", type=float, default=0.5)
|
| 203 |
+
ap.add_argument("--tta_backtest", action="store_true")
|
| 204 |
+
ap.add_argument("--tta_tau", type=float, default=0.5)
|
| 205 |
+
ap.add_argument("--tta_bt_windows", type=int, default=1)
|
| 206 |
+
ap.add_argument("--tta_prior", type=float, default=0.0)
|
| 207 |
+
ap.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
|
| 208 |
+
args = ap.parse_args()
|
| 209 |
+
|
| 210 |
+
logging.getLogger("gluonts.model.forecast").setLevel(logging.ERROR)
|
| 211 |
+
warnings.filterwarnings("ignore")
|
| 212 |
+
os.environ["GIFT_EVAL"] = args.gift_eval_dir
|
| 213 |
+
if os.environ.get("EVAL_MEM_FRAC") and torch.cuda.is_available(): # share the GPU with a training job
|
| 214 |
+
torch.cuda.set_per_process_memory_fraction(float(os.environ["EVAL_MEM_FRAC"]))
|
| 215 |
+
props = json.load(open(PROPS))
|
| 216 |
+
if args.configs:
|
| 217 |
+
cfgs = [tuple(x.split(":")) for x in args.configs.split(",")]
|
| 218 |
+
elif args.mode == "quick":
|
| 219 |
+
cfgs = QUICK
|
| 220 |
+
else:
|
| 221 |
+
cfgs = [(n, t) for n, t, _ in all_configs(props)]
|
| 222 |
+
ensure_data([n for n, _ in cfgs], args.gift_eval_dir)
|
| 223 |
+
|
| 224 |
+
from timestral import TimestralForecaster
|
| 225 |
+
fc = TimestralForecaster.from_pretrained(args.export, device=args.device,
|
| 226 |
+
max_context=args.max_context or None)
|
| 227 |
+
if args.tta:
|
| 228 |
+
from modular_ts.tta import EnsembleForecaster
|
| 229 |
+
fc = EnsembleForecaster(fc, args.tta, alpha=args.tta_alpha, backtest=args.tta_backtest, tau=args.tta_tau, bt_windows=args.tta_bt_windows, prior=args.tta_prior)
|
| 230 |
+
out = Path(args.out)
|
| 231 |
+
(out / "per_config").mkdir(parents=True, exist_ok=True)
|
| 232 |
+
per = {}
|
| 233 |
+
for name, term in cfgs:
|
| 234 |
+
key = config_key(name, term, props)
|
| 235 |
+
p = out / "per_config" / (key.replace("/", "__") + ".json")
|
| 236 |
+
if p.exists():
|
| 237 |
+
per[key] = json.loads(p.read_text())["metrics"]
|
| 238 |
+
continue
|
| 239 |
+
t = time.time()
|
| 240 |
+
try:
|
| 241 |
+
met = evaluate_config(fc, name, term)
|
| 242 |
+
except Exception as e: # keep going; a failed config is reported, not silently dropped
|
| 243 |
+
print(f"{key}: Error {e!r}"[:400], flush=True)
|
| 244 |
+
continue
|
| 245 |
+
p.write_text(json.dumps({"dataset": key, "metrics": met, "seconds": time.time() - t}, indent=1))
|
| 246 |
+
per[key] = met
|
| 247 |
+
print(f"{key}: MASE={met['MASE[0.5]']:.4f} CRPS={met['mean_weighted_sum_quantile_loss']:.4f} ({time.time()-t:.0f}s)", flush=True)
|
| 248 |
+
agg = aggregate(per)
|
| 249 |
+
json.dump(agg, open(out / "summary.json", "w"), indent=1)
|
| 250 |
+
print("SUMMARY", json.dumps(agg), flush=True)
|
| 251 |
+
if args.mode == "full":
|
| 252 |
+
rows = []
|
| 253 |
+
for k, met in sorted(per.items()):
|
| 254 |
+
ds_key = k.split("/")[0]
|
| 255 |
+
rows.append({"dataset": k, "model": args.model_name, **{f"eval_metrics/{c}": met[c] for c in METRIC_COLUMNS},
|
| 256 |
+
"domain": props[ds_key]["domain"], "num_variates": props[ds_key]["num_variates"]})
|
| 257 |
+
pd.DataFrame(rows).to_csv(out / "all_results.csv", index=False)
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
if __name__ == "__main__":
|
| 261 |
+
main()
|
modular_ts/model.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Modular-TS training model: a Timestral-style network trained from scratch with DHF.
|
| 2 |
+
|
| 3 |
+
The network *is* `timestral.model.TimestralModel` (Apache-2.0 architecture code, random
|
| 4 |
+
init), so parameter names match the open inference package and a trained checkpoint
|
| 5 |
+
exports to `config.json` + `model.safetensors` that `timestral.TimestralForecaster`
|
| 6 |
+
loads unchanged. We add `forward_train`, the Decoupled-Horizon-Forecasting training pass
|
| 7 |
+
(our reconstruction; Mistral has not published it):
|
| 8 |
+
|
| 9 |
+
- batch layout (B groups, V rows per group, T = N * patch steps); rows are target,
|
| 10 |
+
past-only covariate, known-future covariate, or padding;
|
| 11 |
+
- Enc = all N patch embeddings (causal per patch, past-only scaling + asinh);
|
| 12 |
+
Dec = N copies of the learned forecast query; per group a min-context s, decoder
|
| 13 |
+
blocks of K patches anchored at s, s+K, ...; Dec token in block b reads Enc[< b] plus
|
| 14 |
+
the causal Dec chain of its own block (timestral.masks.build_two_stream_time_mask);
|
| 15 |
+
- variate attention per patch slot over the V rows of a group with the covariate
|
| 16 |
+
firewall (covariates never read targets; known-future rows never read causal rows);
|
| 17 |
+
- target of Dec token p = true patch p normalized in the causal frame of patches < p
|
| 18 |
+
(the "advancing refine frame" the inference code decodes in), asinh-squashed;
|
| 19 |
+
- loss = pinball over the 21 quantile knots on observed target values of active patches.
|
| 20 |
+
|
| 21 |
+
NEVER load Timestral weights into this model (Modified-MIT licence, see NOTICE).
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
from __future__ import annotations
|
| 25 |
+
|
| 26 |
+
import math
|
| 27 |
+
import os
|
| 28 |
+
import sys
|
| 29 |
+
|
| 30 |
+
import torch
|
| 31 |
+
import torch.nn.functional as F
|
| 32 |
+
from torch import Tensor
|
| 33 |
+
|
| 34 |
+
_HERE = os.path.dirname(os.path.abspath(__file__))
|
| 35 |
+
_TS_SRC = os.path.join(os.path.dirname(_HERE), "timestral", "src")
|
| 36 |
+
if os.path.isdir(_TS_SRC) and _TS_SRC not in sys.path:
|
| 37 |
+
sys.path.insert(0, _TS_SRC)
|
| 38 |
+
|
| 39 |
+
from timestral.config import TimestralConfig # noqa: E402
|
| 40 |
+
from timestral.layers import Attention, _apply_rotary # noqa: E402
|
| 41 |
+
from timestral.masks import build_two_stream_time_mask, compute_enc_key_validity # noqa: E402
|
| 42 |
+
from timestral.model import ROLE_KNOWN_COV, ROLE_PAST_COV, ROLE_TARGET, TimestralModel # noqa: E402
|
| 43 |
+
from timestral.scaler import apply_affine_asinh_masked, causal_patch_loc_scale # noqa: E402
|
| 44 |
+
|
| 45 |
+
ROLE_PAD = -1
|
| 46 |
+
QUANTILES_21 = (0.01, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5,
|
| 47 |
+
0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def make_config(size: str = "2.5M", context_length: int = 4096) -> TimestralConfig:
|
| 51 |
+
presets = {
|
| 52 |
+
# d_model, heads, mlp, layers-groups
|
| 53 |
+
"2.5M": (128, 2, 320, 2),
|
| 54 |
+
"5M": (192, 3, 512, 2),
|
| 55 |
+
"28M": (384, 6, 1024, 3),
|
| 56 |
+
"tiny": (64, 1, 128, 2), # CPU tests only
|
| 57 |
+
}
|
| 58 |
+
d, h, mlp, k = presets[size]
|
| 59 |
+
return TimestralConfig(
|
| 60 |
+
patch_size=32, d_model=d, num_heads=h, mlp_hidden_dim=mlp,
|
| 61 |
+
block_schedule=("time", "time", "time", "time_variate") * k,
|
| 62 |
+
quantile_knots=QUANTILES_21, block_size=32, context_length=context_length,
|
| 63 |
+
rope_theta=1e4, rope_partial_factor=0.5, norm_eps=1e-6, attn_bias=False, mlp_bias=False,
|
| 64 |
+
time_attn_gate="sdpa_sigmoid", variate_attn_gate="sdpa_sigmoid", minimum_scale=1e-6,
|
| 65 |
+
use_asinh=True, stabilize_with_global=False, covariate_enabled=True,
|
| 66 |
+
split_context_horizon_attn=True, attend_missing_patches=True, quantile_width_factor=3.0,
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def count_params(model: torch.nn.Module) -> dict:
|
| 71 |
+
tot = sum(p.numel() for p in model.parameters())
|
| 72 |
+
emb = sum(p.numel() for n, p in model.named_parameters() if n.startswith("representation"))
|
| 73 |
+
head = sum(p.numel() for n, p in model.named_parameters() if n.startswith("head"))
|
| 74 |
+
return {"total": tot, "embed": emb, "trunk": tot - emb - head, "head": head}
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def _variate_attention(attn: Attention, x: Tensor, mask: Tensor, B: int, V: int) -> Tensor:
|
| 78 |
+
"""Variate attention per (group, slot) over V rows. x: (B*V, S, D); mask: (B*S, 1, V, V)."""
|
| 79 |
+
R, S, D = x.shape
|
| 80 |
+
inp = x.view(B, V, S, D).permute(0, 2, 1, 3).reshape(B * S, V, D)
|
| 81 |
+
q, k, v = attn._qkv(inp) # (B*S, V, H, hd)
|
| 82 |
+
out = F.scaled_dot_product_attention(q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2), attn_mask=mask)
|
| 83 |
+
out = out.transpose(1, 2).reshape(B * S, V, D)
|
| 84 |
+
if attn._gate is not None:
|
| 85 |
+
out = out * attn._gate(inp).sigmoid()
|
| 86 |
+
out = attn.wO(out)
|
| 87 |
+
return out.view(B, S, V, D).permute(0, 2, 1, 3).reshape(R, S, D)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def _time_attention(attn: Attention, x: Tensor, mask: Tensor, cos: Tensor, sin: Tensor) -> Tensor:
|
| 91 |
+
q, k, v = attn._qkv(x)
|
| 92 |
+
q, k = _apply_rotary(q, k, cos, sin)
|
| 93 |
+
out = F.scaled_dot_product_attention(q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2), attn_mask=mask)
|
| 94 |
+
out = out.transpose(1, 2).reshape(x.shape[0], x.shape[1], -1)
|
| 95 |
+
if attn._gate is not None:
|
| 96 |
+
out = out * attn._gate(x).sigmoid()
|
| 97 |
+
return attn.wO(out)
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
class ModularTS(TimestralModel):
|
| 101 |
+
"""TimestralModel + DHF training pass (identical parameter names)."""
|
| 102 |
+
|
| 103 |
+
def __init__(self, config: TimestralConfig) -> None:
|
| 104 |
+
super().__init__(config)
|
| 105 |
+
self.register_buffer("q_levels", torch.tensor(config.quantile_knots, dtype=torch.float32), persistent=False)
|
| 106 |
+
self._init_weights()
|
| 107 |
+
|
| 108 |
+
def _init_weights(self) -> None:
|
| 109 |
+
for name, p in self.named_parameters():
|
| 110 |
+
if p.dim() == 2:
|
| 111 |
+
torch.nn.init.normal_(p, std=0.02)
|
| 112 |
+
if name.endswith("wO.weight") or name.endswith("w3.weight"):
|
| 113 |
+
p.data.mul_(1.0 / math.sqrt(2 * len(self.config.block_schedule)))
|
| 114 |
+
elif name.endswith(".bias"):
|
| 115 |
+
torch.nn.init.zeros_(p)
|
| 116 |
+
torch.nn.init.normal_(self.representation.forecast_query, std=0.02)
|
| 117 |
+
|
| 118 |
+
# ------------------------------------------------------------------ masks
|
| 119 |
+
def build_masks(self, observed: Tensor, roles: Tensor, s: Tensor):
|
| 120 |
+
"""observed (B,V,T) bool, roles (B,V) long, s (B,) long -> masks/positions/key validity."""
|
| 121 |
+
c = self.config
|
| 122 |
+
B, V, T = observed.shape
|
| 123 |
+
P, N, R = c.patch_size, T // c.patch_size, B * V
|
| 124 |
+
dev = observed.device
|
| 125 |
+
obs_pp = observed.view(R, N, P).any(-1)
|
| 126 |
+
not_pad = (roles != ROLE_PAD).view(R)
|
| 127 |
+
key_valid = compute_enc_key_validity(obs_pp, c.attend_missing_patches) & not_pad[:, None]
|
| 128 |
+
known = (roles == ROLE_KNOWN_COV).view(R)
|
| 129 |
+
target = (roles == ROLE_TARGET).view(R)
|
| 130 |
+
s_rows = s.repeat_interleave(V)
|
| 131 |
+
time_mask = build_two_stream_time_mask(s_rows, N, c.block_size, key_valid, bidirectional_rows=known)
|
| 132 |
+
# variate mask (B*2N, 1, V, V)
|
| 133 |
+
slot_valid = torch.cat([key_valid, not_pad[:, None].expand(R, N)], dim=1).view(B, V, 2 * N)
|
| 134 |
+
keyok = slot_valid.permute(0, 2, 1)[:, :, None, :] # (B, 2N, 1, Vk)
|
| 135 |
+
tg = target.view(B, V)
|
| 136 |
+
kn = known.view(B, V)
|
| 137 |
+
forbid = ((~tg)[:, :, None] & tg[:, None, :]) | (kn[:, :, None] & (~kn)[:, None, :]) # (B, Vq, Vk)
|
| 138 |
+
vmask = keyok & ~forbid[:, None]
|
| 139 |
+
eye = torch.eye(V, dtype=torch.bool, device=dev)
|
| 140 |
+
vmask = (vmask | eye).reshape(B * 2 * N, 1, V, V)
|
| 141 |
+
# positions anchored at each group's first observed patch
|
| 142 |
+
grp_obs = obs_pp.view(B, V, N).any(1) # (B, N)
|
| 143 |
+
first = torch.where(grp_obs.any(-1), grp_obs.float().argmax(-1), torch.zeros(B, device=dev)).long()
|
| 144 |
+
p = torch.arange(N, device=dev)
|
| 145 |
+
pos = (p[None] - first[:, None]).repeat_interleave(V, 0)
|
| 146 |
+
positions = torch.cat([pos, pos], dim=1)
|
| 147 |
+
return time_mask, vmask, positions, obs_pp, target, known
|
| 148 |
+
|
| 149 |
+
# ---------------------------------------------------------------- forward
|
| 150 |
+
def forward_train(self, values: Tensor, observed: Tensor, roles: Tensor, s: Tensor, return_preds: bool = False,
|
| 151 |
+
teacher: Tensor | None = None, teacher_obs: Tensor | None = None, gt_row: Tensor | None = None,
|
| 152 |
+
lam: float = 0.5, teacher_q_idx: tuple = (2, 4, 6, 8, 10, 12, 14, 16, 18)):
|
| 153 |
+
"""values (B,V,T) float (anything where unobserved), observed (B,V,T) bool,
|
| 154 |
+
roles (B,V) long in {-1,0,1,2}, s (B,) min-context patches. Returns (loss, stats)."""
|
| 155 |
+
c = self.config
|
| 156 |
+
B, V, T = values.shape
|
| 157 |
+
P, N, R = c.patch_size, T // c.patch_size, B * V
|
| 158 |
+
x = torch.where(observed, values, torch.zeros_like(values)).float().view(R, T)
|
| 159 |
+
obs = observed.view(R, T)
|
| 160 |
+
|
| 161 |
+
with torch.no_grad():
|
| 162 |
+
time_mask, vmask, positions, obs_pp, target_rows, known_rows = self.build_masks(observed, roles, s)
|
| 163 |
+
loc, scale = causal_patch_loc_scale(x[:, None], obs[:, None], P, c.minimum_scale, False)
|
| 164 |
+
loc, scale = loc[:, 0], scale[:, 0]
|
| 165 |
+
scaled = apply_affine_asinh_masked(x, loc, scale, obs, use_asinh=c.use_asinh)
|
| 166 |
+
scaled, _, _ = self._known_cov_scaling(x, obs, scaled, loc, scale, known_rows)
|
| 167 |
+
# DHF targets in the frame of patches < p
|
| 168 |
+
loc_pp = loc.view(R, N, P)[..., 0]
|
| 169 |
+
sc_pp = scale.view(R, N, P)[..., 0]
|
| 170 |
+
f_loc = torch.cat([loc_pp[:, :1], loc_pp[:, :-1]], dim=1)
|
| 171 |
+
f_sc = torch.cat([sc_pp[:, :1], sc_pp[:, :-1]], dim=1)
|
| 172 |
+
y = torch.asinh((x.view(R, N, P) - f_loc[..., None]) / f_sc[..., None].clamp_min(1e-6))
|
| 173 |
+
cnt_before = torch.cumsum(obs.view(R, N, P).sum(-1), 1) - obs.view(R, N, P).sum(-1)
|
| 174 |
+
p_idx = torch.arange(N, device=x.device)
|
| 175 |
+
active = p_idx[None] >= s.repeat_interleave(V)[:, None]
|
| 176 |
+
w = (obs.view(R, N, P) & target_rows[:, None, None] & active[..., None] & (cnt_before >= 2)[..., None]).float()
|
| 177 |
+
if gt_row is not None: # rows whose ground truth may not be used (teacher-only inputs)
|
| 178 |
+
w = w * gt_row.view(R, 1, 1).float()
|
| 179 |
+
y = torch.where(w > 0, y, torch.zeros_like(y))
|
| 180 |
+
if teacher is not None:
|
| 181 |
+
# teacher (R, T, Qt) raw-space quantiles (anything where ~teacher_obs) -> same advancing frame + asinh
|
| 182 |
+
Qt = teacher.shape[-1]
|
| 183 |
+
tv = torch.where(teacher_obs.view(R, T, 1), teacher.view(R, T, Qt).float(), torch.zeros(1, device=x.device))
|
| 184 |
+
yt = torch.asinh((tv.view(R, N, P, Qt) - f_loc[..., None, None]) / f_sc[..., None, None].clamp_min(1e-6))
|
| 185 |
+
wt = (teacher_obs.view(R, N, P) & target_rows[:, None, None] & active[..., None] & (cnt_before >= 2)[..., None]).float()
|
| 186 |
+
yt = torch.where(wt[..., None] > 0, yt, torch.zeros_like(yt))
|
| 187 |
+
|
| 188 |
+
x_enc = self.representation(scaled, obs)
|
| 189 |
+
x_dec = self.representation.forecast_tokens(R, N)
|
| 190 |
+
h = torch.cat([x_enc, x_dec.to(x_enc.dtype)], dim=1)
|
| 191 |
+
cos, sin = self.transformer.rope(h, positions)
|
| 192 |
+
for layer in self.transformer.layers:
|
| 193 |
+
for norm, attn in zip(layer.attn_norms, layer.attns):
|
| 194 |
+
if attn.is_time:
|
| 195 |
+
h = h + _time_attention(attn, norm(h), time_mask, cos, sin)
|
| 196 |
+
else:
|
| 197 |
+
h = h + _variate_attention(attn, norm(h), vmask, B, V)
|
| 198 |
+
h = h + layer.mlp(layer.mlp_norm(h))
|
| 199 |
+
h = self.transformer.out_norm(h)
|
| 200 |
+
preds = self.head(h[:, N:]).view(R, N, P, -1).float() # (R,N,P,Q) normalized, monotone
|
| 201 |
+
|
| 202 |
+
if return_preds:
|
| 203 |
+
return preds, f_loc, f_sc
|
| 204 |
+
err = y[..., None] - preds
|
| 205 |
+
tau = self.q_levels.view(1, 1, 1, -1)
|
| 206 |
+
pin = torch.maximum(tau * err, (tau - 1) * err).mean(-1)
|
| 207 |
+
denom = w.sum().clamp_min(1.0)
|
| 208 |
+
loss = (pin * w).sum() / denom
|
| 209 |
+
with torch.no_grad():
|
| 210 |
+
mae = ((y - preds[..., c.median_index]).abs() * w).sum() / denom
|
| 211 |
+
stats = {"pinball": loss.detach(), "mae_med": mae, "n_tgt": denom}
|
| 212 |
+
if teacher is not None:
|
| 213 |
+
pq = preds[..., list(teacher_q_idx)]
|
| 214 |
+
tq = self.q_levels[list(teacher_q_idx)].view(1, 1, 1, -1)
|
| 215 |
+
et = yt - pq
|
| 216 |
+
kd = torch.maximum(tq * et, (tq - 1) * et).mean(-1)
|
| 217 |
+
kd_loss = (kd * wt).sum() / wt.sum().clamp_min(1.0)
|
| 218 |
+
loss = (1 - lam) * loss + lam * kd_loss
|
| 219 |
+
stats["kd"] = kd_loss.detach()
|
| 220 |
+
return loss, stats
|
modular_ts/realdata.py
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Optional real-data source: Salesforce/GiftEvalPretrain (OFF by default).
|
| 2 |
+
|
| 3 |
+
GiftEvalPretrain's card says Apache-2.0 but "for research purposes only"; the user has
|
| 4 |
+
not chosen between synthetic-only and synthetic+GiftEvalPretrain, so train.py keeps this
|
| 5 |
+
behind `--data synthetic+gifteval` (default `--data synthetic`).
|
| 6 |
+
|
| 7 |
+
Contamination filter: every GiftEvalPretrain sub-dataset whose normalised name matches
|
| 8 |
+
(or is an explicit alias of) a dataset in the GIFT-Eval test pool, fev-bench tasks or the
|
| 9 |
+
TIME benchmark is dropped. The resolved include/exclude lists are written next to each
|
| 10 |
+
checkpoint (data_manifest.json) for the model card's contamination audit.
|
| 11 |
+
|
| 12 |
+
Data layout on the Hub: one folder per sub-dataset holding HF `save_to_disk` arrow
|
| 13 |
+
shards (`data-0000k-of-0000n.arrow`) with columns item_id, start, freq, target
|
| 14 |
+
(1-D or 2-D list). We download shards lazily, memory-map them, and crop random windows
|
| 15 |
+
on a background thread.
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
|
| 20 |
+
import queue
|
| 21 |
+
import math
|
| 22 |
+
import random
|
| 23 |
+
import re
|
| 24 |
+
import threading
|
| 25 |
+
|
| 26 |
+
import numpy as np
|
| 27 |
+
import torch
|
| 28 |
+
|
| 29 |
+
REPO = "Salesforce/GiftEvalPretrain"
|
| 30 |
+
|
| 31 |
+
GIFT_EVAL_TEST = [
|
| 32 |
+
"m4_yearly", "m4_quarterly", "m4_monthly", "m4_weekly", "m4_daily", "m4_hourly", "electricity", "solar",
|
| 33 |
+
"hospital", "covid_deaths", "us_births", "saugeenday", "temperature_rain_with_missing",
|
| 34 |
+
"kdd_cup_2018_with_missing", "car_parts_with_missing", "restaurant", "hierarchical_sales", "LOOP_SEATTLE",
|
| 35 |
+
"SZ_TAXI", "M_DENSE", "ett1", "ett2", "jena_weather", "bitbrains_fast_storage", "bitbrains_rnd",
|
| 36 |
+
"bizitobs_application", "bizitobs_service", "bizitobs_l2c",
|
| 37 |
+
]
|
| 38 |
+
FEV_BENCH = [
|
| 39 |
+
"ETT", "LOOP_SEATTLE", "M_DENSE", "SZ_TAXI", "australian_tourism", "bizitobs_l2c", "boomlet", "ecdc_ili",
|
| 40 |
+
"entsoe", "epf_be", "epf_de", "epf_fr", "epf_np", "epf_pjm", "ercot", "favorita_stores", "favorita_transactions",
|
| 41 |
+
"fred_md_2025", "fred_qd_2025", "gvar", "hermes", "hierarchical_sales", "hospital", "hospital_admissions",
|
| 42 |
+
"jena_weather", "kdd_cup_2022", "m5", "proenfo_gfc12", "proenfo_gfc14", "proenfo_gfc17", "redset", "restaurant",
|
| 43 |
+
"rohlik_orders", "rohlik_sales", "rossmann", "solar", "solar_with_weather", "uci_air_quality", "uk_covid_nation",
|
| 44 |
+
"uk_covid_utla", "us_consumption", "walmart", "world_co2_emissions", "world_life_expectancy", "world_tourism",
|
| 45 |
+
]
|
| 46 |
+
TIME_BENCH = [
|
| 47 |
+
"Australia_Solar", "Auto_Production_SF", "CPHL", "Coastal_T_S", "Commodity_Import", "Commodity_Production",
|
| 48 |
+
"Crypto", "ECDC_COVID", "EWELD_Load", "Finland_Traffic", "Global_Influenza", "Global_Price", "Housing_Inventory",
|
| 49 |
+
"JOLTS", "Job_Claims", "MetroPT-3", "NE_China_Wind", "Oil_Price", "Online_Retail_2_UCI", "OpenElectricity_NEM",
|
| 50 |
+
"Port_Activity", "SG_Carpark", "SG_PM25", "SG_Weather", "Smart_Manufacturing", "Supply_Chain_Customer",
|
| 51 |
+
"Supply_Chain_Location", "US_Labor", "US_Term_Structure", "Uncertainty_1M", "Vehicle_Sales", "Vehicle_Supply",
|
| 52 |
+
"WUI_Global", "Water_Quality_Darwin", "azure2019", "current_velocity", "epf_electricity_price",
|
| 53 |
+
]
|
| 54 |
+
# GiftEvalPretrain names that are the same source as a benchmark dataset under another name.
|
| 55 |
+
ALIASES_EXCLUDE = {
|
| 56 |
+
"favorita_sales": "fev favorita_stores", "favorita_transactions": "fev favorita_transactions",
|
| 57 |
+
"m5": "fev m5", "kdd2022": "fev kdd_cup_2022", "gfc12_load": "fev proenfo_gfc12",
|
| 58 |
+
"gfc14_load": "fev proenfo_gfc14", "gfc17_load": "fev proenfo_gfc17", "fred_md": "fev fred_md_2025",
|
| 59 |
+
"solar_power": "GIFT-Eval solar (conservative)", "ecdc_ili": "fev ecdc_ili",
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def _norm(s: str) -> str:
|
| 64 |
+
s = s.lower()
|
| 65 |
+
s = re.sub(r"_with_missing$", "", s)
|
| 66 |
+
s = re.sub(r"_(\d+[a-z]+|daily|hourly|weekly|monthly|yearly|quarterly)$", "", s)
|
| 67 |
+
return re.sub(r"[^a-z0-9]", "", s)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def list_pretrain_datasets() -> list[str]:
|
| 71 |
+
from huggingface_hub import HfApi
|
| 72 |
+
tree = HfApi().list_repo_tree(REPO, repo_type="dataset")
|
| 73 |
+
return sorted(t.path for t in tree if "." not in t.path)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def filter_datasets(names: list[str]) -> tuple[list[str], dict[str, str]]:
|
| 77 |
+
bench = {}
|
| 78 |
+
for pool, lst in (("GIFT-Eval", GIFT_EVAL_TEST), ("fev-bench", FEV_BENCH), ("TIME", TIME_BENCH)):
|
| 79 |
+
for b in lst:
|
| 80 |
+
bench[_norm(b)] = f"{pool} {b}"
|
| 81 |
+
keep, dropped = [], {}
|
| 82 |
+
for n in names:
|
| 83 |
+
k = _norm(n)
|
| 84 |
+
hit = ALIASES_EXCLUDE.get(n) or bench.get(k)
|
| 85 |
+
if hit is None:
|
| 86 |
+
hit = next((v for bk, v in bench.items() if len(bk) >= 5 and (bk in k or k in bk)), None)
|
| 87 |
+
if hit:
|
| 88 |
+
dropped[n] = hit
|
| 89 |
+
else:
|
| 90 |
+
keep.append(n)
|
| 91 |
+
return keep, dropped
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
class GiftEvalPretrainStream:
|
| 95 |
+
"""Random fixed-length windows from GiftEvalPretrain, batched as (B, V, T) like SynthStream.
|
| 96 |
+
|
| 97 |
+
Univariate series give V=1 groups; multivariate series give up to `max_v` variates.
|
| 98 |
+
`datasets` is optional; used only to memory-map arrow shards.
|
| 99 |
+
"""
|
| 100 |
+
|
| 101 |
+
def __init__(self, T: int, tokens_per_batch: int, device="cpu", seed=0, datasets_subset: list[str] | None = None,
|
| 102 |
+
max_shards_per_dataset: int = 2, max_v: int = 16, patch=32, min_ctx=4, cache_dir=None, prefetch=4):
|
| 103 |
+
from datasets import Dataset as HFDataset
|
| 104 |
+
from huggingface_hub import HfApi, hf_hub_download
|
| 105 |
+
|
| 106 |
+
names = datasets_subset or list_pretrain_datasets()
|
| 107 |
+
self.keep, self.dropped = filter_datasets(names)
|
| 108 |
+
self.T, self.N, self.tok, self.patch = T, T // patch, tokens_per_batch, patch
|
| 109 |
+
self.dev, self.min_ctx, self.max_v = torch.device(device), min_ctx, max_v
|
| 110 |
+
self.rng = random.Random(seed)
|
| 111 |
+
api = HfApi()
|
| 112 |
+
self.tables = []
|
| 113 |
+
for name in self.keep:
|
| 114 |
+
files = sorted(f for f in api.list_repo_files(REPO, repo_type="dataset") if f.startswith(name + "/") and f.endswith(".arrow"))
|
| 115 |
+
for f in files[:max_shards_per_dataset]:
|
| 116 |
+
p = hf_hub_download(REPO, f, repo_type="dataset", cache_dir=cache_dir)
|
| 117 |
+
self.tables.append((name, HFDataset.from_file(p)))
|
| 118 |
+
assert self.tables, "no GiftEvalPretrain shards left after filtering"
|
| 119 |
+
self.q: queue.Queue = queue.Queue(maxsize=prefetch)
|
| 120 |
+
threading.Thread(target=self._worker, daemon=True).start()
|
| 121 |
+
|
| 122 |
+
def manifest(self) -> dict:
|
| 123 |
+
return {"source": REPO, "included": self.keep, "excluded": self.dropped}
|
| 124 |
+
|
| 125 |
+
def _window(self):
|
| 126 |
+
name, tab = self.rng.choice(self.tables)
|
| 127 |
+
row = tab[self.rng.randrange(len(tab))]
|
| 128 |
+
x = np.asarray(row["target"], dtype=np.float32)
|
| 129 |
+
x = x[None] if x.ndim == 1 else x
|
| 130 |
+
if x.shape[0] > self.max_v:
|
| 131 |
+
x = x[self.rng.sample(range(x.shape[0]), self.max_v)]
|
| 132 |
+
L = x.shape[1]
|
| 133 |
+
if L >= self.T:
|
| 134 |
+
st = self.rng.randrange(L - self.T + 1)
|
| 135 |
+
w = x[:, st : st + self.T]
|
| 136 |
+
else: # left-pad with NaN (unobserved)
|
| 137 |
+
w = np.concatenate([np.full((x.shape[0], self.T - L), np.nan, np.float32), x], 1)
|
| 138 |
+
return w
|
| 139 |
+
|
| 140 |
+
def _make(self):
|
| 141 |
+
# bucket by V: draw one window, then fill the batch with windows of the same V
|
| 142 |
+
first = self._window()
|
| 143 |
+
V = first.shape[0]
|
| 144 |
+
Bg = max(1, self.tok // (V * self.N))
|
| 145 |
+
ws = [first]
|
| 146 |
+
tries = 0
|
| 147 |
+
while len(ws) < Bg and tries < 20 * Bg:
|
| 148 |
+
w = self._window()
|
| 149 |
+
tries += 1
|
| 150 |
+
if w.shape[0] == V:
|
| 151 |
+
ws.append(w)
|
| 152 |
+
arr = np.stack(ws)
|
| 153 |
+
obs = np.isfinite(arr)
|
| 154 |
+
vals = np.nan_to_num(arr, nan=0.0, posinf=0.0, neginf=0.0)
|
| 155 |
+
return vals, obs
|
| 156 |
+
|
| 157 |
+
def _worker(self):
|
| 158 |
+
while True:
|
| 159 |
+
self.q.put(self._make())
|
| 160 |
+
|
| 161 |
+
def next_batch(self, V=None) -> dict:
|
| 162 |
+
vals, obs = self.q.get()
|
| 163 |
+
Bg, V, _ = vals.shape
|
| 164 |
+
s = torch.randint(self.min_ctx, self.N - 1, (Bg,))
|
| 165 |
+
return {
|
| 166 |
+
"values": torch.from_numpy(vals).to(self.dev, non_blocking=True),
|
| 167 |
+
"observed": torch.from_numpy(obs).to(self.dev, non_blocking=True),
|
| 168 |
+
"roles": torch.zeros(Bg, V, dtype=torch.long, device=self.dev),
|
| 169 |
+
"s": s.to(self.dev),
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
class MixedStream:
|
| 174 |
+
"""Draw from `real` with probability `real_frac`, else from `synth`."""
|
| 175 |
+
|
| 176 |
+
def __init__(self, synth, real, real_frac: float, seed=0):
|
| 177 |
+
self.synth, self.real, self.p = synth, real, real_frac
|
| 178 |
+
self.rng = random.Random(seed)
|
| 179 |
+
|
| 180 |
+
def next_batch(self, V=None):
|
| 181 |
+
return self.real.next_batch() if self.rng.random() < self.p else self.synth.next_batch(V)
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
# ---------------------------------------------------------------------------------------------
|
| 185 |
+
# Fast GPU-resident stream (used for run1). The thread/row-based stream above cannot keep up
|
| 186 |
+
# with ~12+ steps/s x 1024 windows; this one loads a size-capped selection of shards once into
|
| 187 |
+
# one flat float32 tensor on the GPU and crops whole batches with a single vectorised gather.
|
| 188 |
+
# ---------------------------------------------------------------------------------------------
|
| 189 |
+
|
| 190 |
+
def _arrow_channels(path):
|
| 191 |
+
"""Return (values float32 1-D, ch_off int64, ch_len int64, row_first_ch, row_nvar)."""
|
| 192 |
+
import pyarrow as pa
|
| 193 |
+
try:
|
| 194 |
+
tab = pa.ipc.open_stream(path).read_all()
|
| 195 |
+
except Exception:
|
| 196 |
+
tab = pa.ipc.open_file(path).read_all()
|
| 197 |
+
col = tab.column("target").combine_chunks()
|
| 198 |
+
t = col.type
|
| 199 |
+
if pa.types.is_list(t) and not (pa.types.is_list(t.value_type) or pa.types.is_fixed_size_list(t.value_type)):
|
| 200 |
+
inner, nvar = col, np.ones(len(col), np.int64)
|
| 201 |
+
elif pa.types.is_fixed_size_list(t):
|
| 202 |
+
inner, nvar = col.flatten(), np.full(len(col), t.list_size, np.int64)
|
| 203 |
+
else: # list<list<float>>
|
| 204 |
+
inner = col.flatten()
|
| 205 |
+
off = np.asarray(col.offsets)
|
| 206 |
+
nvar = (off[1:] - off[:-1]).astype(np.int64)
|
| 207 |
+
off = np.asarray(inner.offsets).astype(np.int64)
|
| 208 |
+
vals = inner.values.to_numpy(zero_copy_only=False).astype(np.float32, copy=False)
|
| 209 |
+
base = off[0]
|
| 210 |
+
vals = vals[base:off[-1]]
|
| 211 |
+
ch_off = off[:-1] - base
|
| 212 |
+
ch_len = off[1:] - off[:-1]
|
| 213 |
+
row_first = np.concatenate([[0], np.cumsum(nvar)[:-1]]).astype(np.int64)
|
| 214 |
+
return vals, ch_off, ch_len, row_first, nvar
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
class FastGiftEvalPretrain:
|
| 218 |
+
def __init__(self, T, tokens_per_batch, device, selection: dict, seed=0, patch=32, cache_dir=None,
|
| 219 |
+
mv_frac=0.3, max_v=16, short_ctx_p=0.3, ds_temp=0.5, workers=16):
|
| 220 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 221 |
+
from huggingface_hub import hf_hub_download
|
| 222 |
+
names = sorted(selection)
|
| 223 |
+
self.keep, self.dropped = filter_datasets(names)
|
| 224 |
+
assert set(self.keep) == set(names), f"selection contains filtered datasets: {set(names) - set(self.keep)}"
|
| 225 |
+
self.T, self.P, self.N, self.tok = T, patch, T // patch, tokens_per_batch
|
| 226 |
+
self.dev, self.max_v, self.mv_frac, self.short_ctx_p = torch.device(device), max_v, mv_frac, short_ctx_p
|
| 227 |
+
self.g = torch.Generator(device=self.dev); self.g.manual_seed(seed + 777)
|
| 228 |
+
jobs = [(n, f) for n in names for f in selection[n]]
|
| 229 |
+
with ThreadPoolExecutor(workers) as ex:
|
| 230 |
+
paths = list(ex.map(lambda nf: hf_hub_download(REPO, nf[1], repo_type="dataset", cache_dir=cache_dir), jobs))
|
| 231 |
+
print(f"[gep] downloaded {len(paths)} shards", flush=True)
|
| 232 |
+
with ThreadPoolExecutor(8) as ex:
|
| 233 |
+
parsed = list(ex.map(_arrow_channels, paths))
|
| 234 |
+
vals_all, ch_off_all, ch_len_all, ch_ds, mv_rows = [], [], [], [], [] # mv_rows: (ds, first_ch_global, nvar)
|
| 235 |
+
base, nch = 0, 0
|
| 236 |
+
ds_idx = {n: i for i, n in enumerate(names)}
|
| 237 |
+
self.ds_points = np.zeros(len(names))
|
| 238 |
+
for (n, _), (v, co, cl, rf, nv) in zip(jobs, parsed):
|
| 239 |
+
keep = cl >= 8
|
| 240 |
+
d = ds_idx[n]
|
| 241 |
+
vals_all.append(v)
|
| 242 |
+
ch_off_all.append(co + base); ch_len_all.append(cl); ch_ds.append(np.full(len(cl), d, np.int64))
|
| 243 |
+
m = (nv >= 2)
|
| 244 |
+
if m.any():
|
| 245 |
+
mv_rows.append(np.stack([np.full(m.sum(), d), rf[m] + nch, nv[m]], 1))
|
| 246 |
+
self.ds_points[d] += cl[keep].sum()
|
| 247 |
+
base += len(v); nch += len(cl)
|
| 248 |
+
flat = np.concatenate(vals_all)
|
| 249 |
+
ch_off, ch_len, ch_ds = map(np.concatenate, (ch_off_all, ch_len_all, ch_ds))
|
| 250 |
+
good = ch_len >= 8
|
| 251 |
+
print(f"[gep] {len(names)} datasets, {nch} channels, {len(flat)/1e9:.2f}B points", flush=True)
|
| 252 |
+
self.flat = torch.from_numpy(flat).to(self.dev)
|
| 253 |
+
del flat, vals_all
|
| 254 |
+
# univariate channel pool grouped by dataset
|
| 255 |
+
order = np.argsort(ch_ds[good], kind="stable")
|
| 256 |
+
gi = np.nonzero(good)[0][order]
|
| 257 |
+
self.u_off = torch.from_numpy(ch_off[gi]).to(self.dev)
|
| 258 |
+
self.u_len = torch.from_numpy(ch_len[gi]).to(self.dev)
|
| 259 |
+
cnt = np.bincount(ch_ds[gi], minlength=len(names))
|
| 260 |
+
self.u_start = torch.from_numpy(np.concatenate([[0], np.cumsum(cnt)[:-1]])).to(self.dev)
|
| 261 |
+
self.u_cnt = torch.from_numpy(cnt).to(self.dev)
|
| 262 |
+
w = np.where(cnt > 0, self.ds_points ** ds_temp, 0.0)
|
| 263 |
+
self.u_w = torch.tensor(w / w.sum(), dtype=torch.float, device=self.dev)
|
| 264 |
+
self.weights = {n: float(x) for n, x in zip(names, w / w.sum())}
|
| 265 |
+
# multivariate rows
|
| 266 |
+
self.ch_off_t = torch.from_numpy(ch_off).to(self.dev)
|
| 267 |
+
self.ch_len_t = torch.from_numpy(ch_len).to(self.dev)
|
| 268 |
+
self.mv = None
|
| 269 |
+
if mv_rows:
|
| 270 |
+
mvr = np.concatenate(mv_rows)
|
| 271 |
+
mvr = mvr[ch_len[mvr[:, 1]] >= 8]
|
| 272 |
+
self.mv = mvr
|
| 273 |
+
self.mv_by_v = {}
|
| 274 |
+
for V in (2, 4, 8, 16):
|
| 275 |
+
sub = mvr[mvr[:, 2] >= V]
|
| 276 |
+
if len(sub) == 0:
|
| 277 |
+
continue
|
| 278 |
+
dcnt = np.bincount(sub[:, 0], minlength=len(names))
|
| 279 |
+
o = np.argsort(sub[:, 0], kind="stable"); sub = sub[o]
|
| 280 |
+
wv = np.where(dcnt > 0, self.ds_points ** ds_temp, 0.0)
|
| 281 |
+
self.mv_by_v[V] = dict(first=torch.from_numpy(sub[:, 1]).to(self.dev), nvar=torch.from_numpy(sub[:, 2]).to(self.dev),
|
| 282 |
+
start=torch.from_numpy(np.concatenate([[0], np.cumsum(dcnt)[:-1]])).to(self.dev),
|
| 283 |
+
cnt=torch.from_numpy(dcnt).to(self.dev), w=torch.tensor(wv / wv.sum(), dtype=torch.float, device=self.dev))
|
| 284 |
+
self.rng = random.Random(seed)
|
| 285 |
+
self.ar = torch.arange(T, device=self.dev)
|
| 286 |
+
|
| 287 |
+
def manifest(self):
|
| 288 |
+
try:
|
| 289 |
+
excl = filter_datasets(list_pretrain_datasets())[1]
|
| 290 |
+
except Exception as e: # offline
|
| 291 |
+
excl = f"unavailable: {e!r}"
|
| 292 |
+
return {"source": REPO, "selection": "size-capped shard selection (gep_selection.json)", "included": self.keep,
|
| 293 |
+
"excluded_by_contamination_filter": excl, "sampling_weights": self.weights, "points_per_dataset": {n: int(x) for n, x in zip(sorted(self.weights), self.ds_points)}}
|
| 294 |
+
|
| 295 |
+
def _rand(self, n):
|
| 296 |
+
return torch.rand(n, generator=self.g, device=self.dev)
|
| 297 |
+
|
| 298 |
+
def _pick(self, w, start, cnt, n):
|
| 299 |
+
d = torch.multinomial(w, n, replacement=True, generator=self.g)
|
| 300 |
+
return start[d] + (self._rand(n) * cnt[d]).long().clamp_max(cnt[d] - 1)
|
| 301 |
+
|
| 302 |
+
def _crop(self, off, L):
|
| 303 |
+
"""off, L: (R,) -> values (R,T), observed (R,T). Same start for rows of one group handled by caller."""
|
| 304 |
+
T = self.T
|
| 305 |
+
st = torch.where(L >= T, (self._rand(L.shape[0]) * (L - T + 1)).long(), L - T)
|
| 306 |
+
return st
|
| 307 |
+
|
| 308 |
+
def _gather(self, off, L, st, keep_len):
|
| 309 |
+
pos = st[:, None] + self.ar[None]
|
| 310 |
+
valid = (pos >= 0) & (pos < L[:, None]) & (self.ar[None] >= (self.T - keep_len)[:, None])
|
| 311 |
+
idx = (off[:, None] + pos.clamp(min=0)).clamp_max(self.flat.numel() - 1)
|
| 312 |
+
v = self.flat[idx]
|
| 313 |
+
obs = valid & torch.isfinite(v)
|
| 314 |
+
return torch.where(obs, v, torch.zeros_like(v)), obs
|
| 315 |
+
|
| 316 |
+
def _s(self, obs_bv):
|
| 317 |
+
# obs_bv (B,V,T): first observed patch per group -> min-context split after it
|
| 318 |
+
B = obs_bv.shape[0]
|
| 319 |
+
any_p = obs_bv.view(B, -1, self.N, self.P).any(-1).any(1) # (B,N)
|
| 320 |
+
fs = torch.where(any_p.any(1), any_p.float().argmax(1), torch.zeros(B, dtype=torch.long, device=self.dev))
|
| 321 |
+
span = (self.N - 2 - fs).clamp_min(1)
|
| 322 |
+
s = fs + 1 + (self._rand(B) * span).long()
|
| 323 |
+
return s.clamp(1, self.N - 2)
|
| 324 |
+
|
| 325 |
+
def _keep_len(self, n):
|
| 326 |
+
k = torch.full((n,), self.T, device=self.dev, dtype=torch.long)
|
| 327 |
+
short = self._rand(n) < self.short_ctx_p
|
| 328 |
+
kl = torch.exp(math.log(64) + self._rand(n) * (math.log(self.T) - math.log(64))).long()
|
| 329 |
+
return torch.where(short, kl, k)
|
| 330 |
+
|
| 331 |
+
@torch.no_grad()
|
| 332 |
+
def next_batch(self, V=None):
|
| 333 |
+
if self.mv is not None and self.rng.random() < self.mv_frac:
|
| 334 |
+
V = self.rng.choice(sorted(self.mv_by_v))
|
| 335 |
+
m = self.mv_by_v[V]
|
| 336 |
+
Bg = max(1, self.tok // (V * self.N))
|
| 337 |
+
r = self._pick(m["w"], m["start"], m["cnt"], Bg)
|
| 338 |
+
first, nvar = m["first"][r], m["nvar"][r]
|
| 339 |
+
j0 = (self._rand(Bg) * (nvar - V + 1)).long()
|
| 340 |
+
ch = first[:, None] + j0[:, None] + torch.arange(V, device=self.dev)[None] # (Bg,V)
|
| 341 |
+
off, L = self.ch_off_t[ch], self.ch_len_t[ch]
|
| 342 |
+
Lmin = L.min(1).values
|
| 343 |
+
st = self._crop(None, Lmin)
|
| 344 |
+
kl = self._keep_len(Bg)
|
| 345 |
+
v, o = self._gather(off.reshape(-1), L.reshape(-1), st.repeat_interleave(V), kl.repeat_interleave(V))
|
| 346 |
+
v, o = v.view(Bg, V, self.T), o.view(Bg, V, self.T)
|
| 347 |
+
else:
|
| 348 |
+
V = 1
|
| 349 |
+
Bg = max(1, self.tok // self.N)
|
| 350 |
+
c = self._pick(self.u_w, self.u_start, self.u_cnt, Bg)
|
| 351 |
+
off, L = self.u_off[c], self.u_len[c]
|
| 352 |
+
st = self._crop(off, L)
|
| 353 |
+
v, o = self._gather(off, L, st, self._keep_len(Bg))
|
| 354 |
+
v, o = v[:, None], o[:, None]
|
| 355 |
+
return {"values": v, "observed": o, "roles": torch.zeros(Bg, V, dtype=torch.long, device=self.dev), "s": self._s(o)}
|
| 356 |
+
|
| 357 |
+
|
| 358 |
+
if __name__ == "__main__":
|
| 359 |
+
names = list_pretrain_datasets()
|
| 360 |
+
keep, dropped = filter_datasets(names)
|
| 361 |
+
print(f"{len(names)} sub-datasets; keep {len(keep)}; drop {len(dropped)}")
|
| 362 |
+
for k, v in dropped.items():
|
| 363 |
+
print(f" DROP {k:40s} <- {v}")
|
modular_ts/realdata2.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""GiftEvalPretrain stream v2 (Modular-TS v3): resolution + mixup augmentation on real data.
|
| 2 |
+
|
| 3 |
+
Adds to FastGiftEvalPretrain (run1):
|
| 4 |
+
* TTM-style "diverse resolution sampling" (Ekambaram et al., arXiv 2401.03955): with prob
|
| 5 |
+
`ds_p` a univariate batch is drawn at a coarser resolution, mean-pooling k in {2,4,8}
|
| 6 |
+
consecutive raw steps (NaN-aware), creating new frequencies from the same corpus;
|
| 7 |
+
* TSMixup (Chronos, arXiv 2403.07815): with prob `mix_p` rows are replaced by a convex
|
| 8 |
+
combination of 2-3 standardised real series (Dirichlet weights), rescaled;
|
| 9 |
+
* flatter dataset temperature (ds_temp 0.3 instead of 0.5) so small datasets are seen more.
|
| 10 |
+
Contamination filter and GPU-resident layout are inherited unchanged.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
import torch
|
| 16 |
+
|
| 17 |
+
from modular_ts.realdata import FastGiftEvalPretrain
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class FastGiftEvalPretrain2(FastGiftEvalPretrain):
|
| 21 |
+
def __init__(self, *a, ds_p=0.3, ds_factors=(2, 4, 8), mix_p=0.2, **k):
|
| 22 |
+
k.setdefault("ds_temp", 0.3)
|
| 23 |
+
super().__init__(*a, **k)
|
| 24 |
+
self.ds_p, self.ds_factors, self.mix_p = ds_p, ds_factors, mix_p
|
| 25 |
+
|
| 26 |
+
def _gather_k(self, off, L, kf):
|
| 27 |
+
"""Gather T*kf raw steps ending at a random point and mean-pool by kf."""
|
| 28 |
+
T, R = self.T, off.shape[0]
|
| 29 |
+
TK = T * kf
|
| 30 |
+
st = torch.where(L >= TK, (self._rand(R) * (L - TK + 1)).long(), L - TK)
|
| 31 |
+
ar = torch.arange(TK, device=self.dev)
|
| 32 |
+
pos = st[:, None] + ar[None]
|
| 33 |
+
valid = (pos >= 0) & (pos < L[:, None])
|
| 34 |
+
idx = (off[:, None] + pos.clamp(min=0)).clamp_max(self.flat.numel() - 1)
|
| 35 |
+
v = self.flat[idx]
|
| 36 |
+
ok = valid & torch.isfinite(v)
|
| 37 |
+
v = torch.where(ok, v, torch.zeros_like(v)).view(R, T, kf)
|
| 38 |
+
c = ok.view(R, T, kf).sum(-1)
|
| 39 |
+
out = v.sum(-1) / c.clamp_min(1)
|
| 40 |
+
obs = c > 0
|
| 41 |
+
return torch.where(obs, out, torch.zeros_like(out)), obs
|
| 42 |
+
|
| 43 |
+
def _mixup(self, v, o):
|
| 44 |
+
R, T = v.shape
|
| 45 |
+
both = o & o.roll(1, 0) & o.roll(2, 0)
|
| 46 |
+
def zs(x, m):
|
| 47 |
+
n = m.sum(-1, keepdim=True).clamp_min(1)
|
| 48 |
+
mu = (x * m).sum(-1, keepdim=True) / n
|
| 49 |
+
sd = (((x - mu) ** 2) * m).sum(-1, keepdim=True).div(n).sqrt().clamp_min(1e-6)
|
| 50 |
+
return (x - mu) / sd, sd, mu
|
| 51 |
+
z0, sd0, mu0 = zs(v, o)
|
| 52 |
+
z1, _, _ = zs(v.roll(1, 0), o.roll(1, 0))
|
| 53 |
+
z2, _, _ = zs(v.roll(2, 0), o.roll(2, 0))
|
| 54 |
+
w = torch.distributions.Dirichlet(torch.full((3,), 1.5, device=self.dev)).sample((R,))
|
| 55 |
+
w[:, 2] *= (self._rand(R) < 0.5).float()
|
| 56 |
+
w = w / w.sum(-1, keepdim=True)
|
| 57 |
+
m = w[:, :1] * z0 + w[:, 1:2] * z1 + w[:, 2:3] * z2
|
| 58 |
+
m = m * sd0 + mu0
|
| 59 |
+
sel = (self._rand(R) < self.mix_p)[:, None]
|
| 60 |
+
return torch.where(sel, torch.where(both, m, 0.0), v), torch.where(sel, both, o)
|
| 61 |
+
|
| 62 |
+
@torch.no_grad()
|
| 63 |
+
def next_batch(self, V=None):
|
| 64 |
+
if (self.mv is not None and self.rng.random() < self.mv_frac) or self.rng.random() >= self.ds_p:
|
| 65 |
+
b = super().next_batch(V)
|
| 66 |
+
if b["values"].shape[1] == 1 and self.mix_p > 0:
|
| 67 |
+
v, o = self._mixup(b["values"][:, 0], b["observed"][:, 0])
|
| 68 |
+
b["values"], b["observed"] = v[:, None], o[:, None]
|
| 69 |
+
b["s"] = self._s(b["observed"])
|
| 70 |
+
return b
|
| 71 |
+
kf = self.rng.choice(self.ds_factors)
|
| 72 |
+
Bg = max(1, self.tok // self.N)
|
| 73 |
+
c = self._pick(self.u_w, self.u_start, self.u_cnt, Bg)
|
| 74 |
+
off, L = self.u_off[c], self.u_len[c]
|
| 75 |
+
v, o = self._gather_k(off, L, kf)
|
| 76 |
+
o = o & (torch.arange(self.T, device=self.dev)[None] >= (self.T - self._keep_len(Bg))[:, None])
|
| 77 |
+
v = torch.where(o, v, torch.zeros_like(v))
|
| 78 |
+
if self.mix_p > 0:
|
| 79 |
+
v, o = self._mixup(v, o)
|
| 80 |
+
v, o = v[:, None], o[:, None]
|
| 81 |
+
return {"values": v, "observed": o, "roles": torch.zeros(Bg, 1, dtype=torch.long, device=self.dev), "s": self._s(o)}
|
modular_ts/synth.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Streamed, fully vectorised synthetic time-series generator (torch; runs on GPU).
|
| 2 |
+
|
| 3 |
+
Univariate bases (each switched on per series with its own probability, summed or
|
| 4 |
+
multiplied):
|
| 5 |
+
* KernelSynth-style GP components via random Fourier features: RBF (log-uniform
|
| 6 |
+
length-scale), rational-quadratic (scale-mixture of RBFs), periodic kernels at
|
| 7 |
+
frequency-typical periods with random harmonics, linear (trend), white noise;
|
| 8 |
+
* TSI-style trends: linear / quadratic / piecewise-linear with change points / damped;
|
| 9 |
+
* AR(1) / random walk (FFT convolution with phi^k kernels);
|
| 10 |
+
* heteroscedastic noise (log-GP volatility, GARCH-like clustering), student-t tails;
|
| 11 |
+
* level shifts, spikes / outliers;
|
| 12 |
+
* intermittent (Croston-like zero-inflated count) and bursty (Poisson bursts with
|
| 13 |
+
exponential decay) observability-style signals;
|
| 14 |
+
* non-negativity transforms, random offset / scale over many orders of magnitude.
|
| 15 |
+
Multivariatizers (Chronos-2 / CauKer style): latent univariate drivers mixed into each
|
| 16 |
+
row by a random sparse matrix through random nonlinearities with lead-lag delays, plus
|
| 17 |
+
idiosyncratic noise; optional covariate tasks where some rows are past-only or
|
| 18 |
+
known-future covariates (incl. 0/1 event / calendar indicators) and targets respond to
|
| 19 |
+
them through random lagged nonlinear effects.
|
| 20 |
+
Masks: iid missing points, contiguous gaps, late series starts (left padding).
|
| 21 |
+
|
| 22 |
+
`SynthStream(...).next_batch()` returns dict(values, observed, roles, s) ready for
|
| 23 |
+
`ModularTS.forward_train`.
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
from __future__ import annotations
|
| 27 |
+
|
| 28 |
+
import math
|
| 29 |
+
|
| 30 |
+
import torch
|
| 31 |
+
from torch import Tensor
|
| 32 |
+
|
| 33 |
+
PERIOD_BANK = torch.tensor([4, 5, 7, 12, 24, 48, 52, 60, 96, 144, 168, 288, 336, 365, 672, 1008, 1440, 2016], dtype=torch.float32)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def _std(x: Tensor) -> Tensor:
|
| 37 |
+
return x.std(-1, keepdim=True).clamp_min(1e-6)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def _z(x: Tensor) -> Tensor:
|
| 41 |
+
return (x - x.mean(-1, keepdim=True)) / _std(x)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class SynthGen:
|
| 45 |
+
def __init__(self, T: int, device: str | torch.device = "cpu", seed: int = 0, n_rff: int = 32):
|
| 46 |
+
self.T = T
|
| 47 |
+
self.dev = torch.device(device)
|
| 48 |
+
self.g = torch.Generator(device=self.dev)
|
| 49 |
+
self.g.manual_seed(seed)
|
| 50 |
+
self.n_rff = n_rff
|
| 51 |
+
self.t = torch.arange(T, device=self.dev, dtype=torch.float32)
|
| 52 |
+
|
| 53 |
+
# -- random helpers
|
| 54 |
+
def U(self, *shape, lo=0.0, hi=1.0):
|
| 55 |
+
return lo + (hi - lo) * torch.rand(*shape, generator=self.g, device=self.dev)
|
| 56 |
+
|
| 57 |
+
def N(self, *shape):
|
| 58 |
+
return torch.randn(*shape, generator=self.g, device=self.dev)
|
| 59 |
+
|
| 60 |
+
def B(self, p, *shape):
|
| 61 |
+
return (self.U(*shape) < p).float()
|
| 62 |
+
|
| 63 |
+
def logU(self, *shape, lo, hi):
|
| 64 |
+
return torch.exp(self.U(*shape, lo=math.log(lo), hi=math.log(hi)))
|
| 65 |
+
|
| 66 |
+
# -- components (n, T)
|
| 67 |
+
def rff(self, n, lengthscale: Tensor) -> Tensor:
|
| 68 |
+
M = self.n_rff
|
| 69 |
+
w = self.N(n, M, 1) / lengthscale.view(n, 1, 1)
|
| 70 |
+
b = self.U(n, M, 1, hi=2 * math.pi)
|
| 71 |
+
a = self.N(n, M, 1)
|
| 72 |
+
return (a * torch.cos(w * self.t + b)).sum(1) * math.sqrt(2.0 / M)
|
| 73 |
+
|
| 74 |
+
def rbf(self, n):
|
| 75 |
+
return self.rff(n, self.logU(n, lo=self.T / 400, hi=self.T / 1.5))
|
| 76 |
+
|
| 77 |
+
def rq(self, n): # scale mixture: sum of RBFs at several length-scales
|
| 78 |
+
out = 0
|
| 79 |
+
for _ in range(3):
|
| 80 |
+
out = out + self.U(n, 1) * self.rff(n, self.logU(n, lo=self.T / 1000, hi=self.T / 4))
|
| 81 |
+
return out
|
| 82 |
+
|
| 83 |
+
def periodic(self, n):
|
| 84 |
+
k = len(PERIOD_BANK)
|
| 85 |
+
idx = torch.randint(0, k, (n,), generator=self.g, device=self.dev)
|
| 86 |
+
per = PERIOD_BANK.to(self.dev)[idx]
|
| 87 |
+
rnd = self.logU(n, lo=3.0, hi=max(4.0, self.T / 3))
|
| 88 |
+
per = torch.where(self.U(n) < 0.25, rnd, per)
|
| 89 |
+
per = per * self.U(n, lo=0.97, hi=1.03) * torch.where(self.U(n) < 0.1, 1.0 + 0.2 * self.N(n), torch.ones(n, device=self.dev))
|
| 90 |
+
H = 6
|
| 91 |
+
h = torch.arange(1, H + 1, device=self.dev, dtype=torch.float32).view(1, H, 1)
|
| 92 |
+
decay = self.U(n, 1, 1, lo=0.3, hi=1.5)
|
| 93 |
+
amp = self.N(n, H, 1) / h.pow(decay) * self.B(0.8, n, H, 1).clamp_min((h == 1).float())
|
| 94 |
+
ph = self.U(n, H, 1, hi=2 * math.pi)
|
| 95 |
+
sig = (amp * torch.sin(2 * math.pi * h * self.t / per.view(n, 1, 1) + ph)).sum(1)
|
| 96 |
+
# optional slowly varying amplitude (multiplicative modulation)
|
| 97 |
+
mod = 1 + 0.3 * self.B(0.3, n, 1) * torch.tanh(self.rbf(n))
|
| 98 |
+
return sig * mod
|
| 99 |
+
|
| 100 |
+
def trend(self, n):
|
| 101 |
+
tt = self.t / self.T
|
| 102 |
+
lin = self.N(n, 1) * tt
|
| 103 |
+
quad = self.N(n, 1) * 0.5 * tt**2 * self.B(0.3, n, 1)
|
| 104 |
+
# piecewise linear with up to 4 change points
|
| 105 |
+
K = 4
|
| 106 |
+
cps = self.U(n, K, 1)
|
| 107 |
+
slopes = self.N(n, K, 1) * self.B(0.5, n, K, 1)
|
| 108 |
+
pw = (slopes * torch.relu(tt.view(1, 1, -1) - cps)).sum(1)
|
| 109 |
+
damp = self.N(n, 1) * (1 - torch.exp(-tt / self.U(n, 1, lo=0.05, hi=0.5))) * self.B(0.2, n, 1)
|
| 110 |
+
return lin + quad + pw + damp
|
| 111 |
+
|
| 112 |
+
def ar(self, n):
|
| 113 |
+
phi = torch.where(self.U(n) < 0.3, torch.ones(n, device=self.dev), self.U(n, lo=0.3, hi=0.995))
|
| 114 |
+
e = self.N(n, self.T)
|
| 115 |
+
L = min(self.T, 1024)
|
| 116 |
+
k = phi.view(n, 1) ** torch.arange(L, device=self.dev, dtype=torch.float32)
|
| 117 |
+
nfft = 1 << (self.T + L - 1).bit_length()
|
| 118 |
+
y = torch.fft.irfft(torch.fft.rfft(e, nfft) * torch.fft.rfft(k, nfft), nfft)[:, : self.T]
|
| 119 |
+
rw = torch.cumsum(e, -1)
|
| 120 |
+
return torch.where((phi == 1).view(n, 1), rw, y)
|
| 121 |
+
|
| 122 |
+
def noise(self, n):
|
| 123 |
+
z = self.N(n, self.T)
|
| 124 |
+
# heavy tails: scale mixture of normals (generator-friendly stand-in for student-t)
|
| 125 |
+
heavy = self.B(0.3, n, 1)
|
| 126 |
+
tz = z * (1 + heavy * (self.N(n, self.T).abs() ** 2) * 0.5)
|
| 127 |
+
vol = torch.where(self.U(n, 1) < 0.25, torch.exp(0.7 * self.rbf(n).clamp(-3, 3)), torch.ones(n, 1, device=self.dev))
|
| 128 |
+
return tz * vol
|
| 129 |
+
|
| 130 |
+
def level_shifts(self, n):
|
| 131 |
+
K = 3
|
| 132 |
+
loc = self.U(n, K, 1) * self.T
|
| 133 |
+
mag = self.N(n, K, 1) * self.B(0.6, n, K, 1)
|
| 134 |
+
return (mag * (self.t.view(1, 1, -1) > loc).float()).sum(1)
|
| 135 |
+
|
| 136 |
+
def spikes(self, n):
|
| 137 |
+
p = self.logU(n, 1, lo=1e-3, hi=2e-2)
|
| 138 |
+
return (self.U(n, self.T) < p).float() * self.N(n, self.T) * self.U(n, 1, lo=3, hi=10)
|
| 139 |
+
|
| 140 |
+
def bursts(self, n):
|
| 141 |
+
rate = self.logU(n, 1, lo=1e-3, hi=2e-2)
|
| 142 |
+
ev = (self.U(n, self.T) < rate).float() * self.logU(n, self.T, lo=1.0, hi=20.0)
|
| 143 |
+
tau = self.logU(n, lo=2.0, hi=60.0)
|
| 144 |
+
L = min(self.T, 512)
|
| 145 |
+
k = torch.exp(-torch.arange(L, device=self.dev).float().view(1, -1) / tau.view(n, 1))
|
| 146 |
+
nfft = 1 << (self.T + L - 1).bit_length()
|
| 147 |
+
return torch.fft.irfft(torch.fft.rfft(ev, nfft) * torch.fft.rfft(k, nfft), nfft)[:, : self.T]
|
| 148 |
+
|
| 149 |
+
# -- univariate
|
| 150 |
+
def univariate(self, n) -> Tensor:
|
| 151 |
+
z = torch.zeros(n, self.T, device=self.dev)
|
| 152 |
+
on = lambda p: self.B(p, n, 1) # noqa: E731
|
| 153 |
+
w = lambda: self.logU(n, 1, lo=0.1, hi=2.0) # noqa: E731
|
| 154 |
+
trend = on(0.6) * w() * _z(self.trend(n))
|
| 155 |
+
seas = on(0.7) * w() * _z(self.periodic(n)) + on(0.25) * w() * _z(self.periodic(n))
|
| 156 |
+
smooth = on(0.5) * w() * _z(self.rbf(n)) + on(0.25) * w() * _z(self.rq(n))
|
| 157 |
+
arp = on(0.4) * w() * _z(self.ar(n))
|
| 158 |
+
sig = trend + seas + smooth + arp + z
|
| 159 |
+
mult = on(0.25)
|
| 160 |
+
sig = torch.where(mult > 0, (1 + 0.4 * torch.tanh(trend + smooth)) * seas + trend + arp, sig)
|
| 161 |
+
sig = sig + on(0.15) * w() * self.level_shifts(n)
|
| 162 |
+
noise_lvl = self.logU(n, 1, lo=0.01, hi=1.0)
|
| 163 |
+
sig = sig + noise_lvl * self.noise(n)
|
| 164 |
+
sig = sig + on(0.12) * self.spikes(n)
|
| 165 |
+
# intermittent / bursty regimes (non-negative count-like)
|
| 166 |
+
inter = on(0.10)
|
| 167 |
+
lam = torch.exp(0.8 * _z(sig).clamp(-4, 4) + self.U(n, 1, lo=-3, hi=1.5))
|
| 168 |
+
p0 = self.U(n, 1, lo=0.2, hi=0.9)
|
| 169 |
+
cnt = torch.poisson(lam, generator=self.g) * (self.U(n, self.T) > p0).float()
|
| 170 |
+
sig = torch.where(inter > 0, cnt, sig)
|
| 171 |
+
burst = on(0.07)
|
| 172 |
+
sig = torch.where(burst > 0, self.U(n, 1, lo=0, hi=2) + 0.1 * self.noise(n).abs() + self.bursts(n), sig)
|
| 173 |
+
# non-negativity transforms on some continuous series
|
| 174 |
+
nn_ = on(0.2) * (1 - inter) * (1 - burst)
|
| 175 |
+
sig = torch.where(nn_ > 0, torch.nn.functional.softplus(sig * self.U(n, 1, lo=0.5, hi=3)), sig)
|
| 176 |
+
return sig
|
| 177 |
+
|
| 178 |
+
def affine(self, x: Tensor) -> Tensor:
|
| 179 |
+
n = x.shape[0]
|
| 180 |
+
scale = self.logU(n, 1, lo=1e-2, hi=1e4)
|
| 181 |
+
off = self.N(n, 1) * scale * self.logU(n, 1, lo=0.01, hi=20.0) * self.B(0.6, n, 1)
|
| 182 |
+
sgn = torch.where(self.U(n, 1) < 0.5, -1.0, 1.0)
|
| 183 |
+
pos = (x.min(-1, keepdim=True).values >= 0).float() # keep count-like series non-negative
|
| 184 |
+
return torch.where(pos > 0, x * scale, sgn * x * scale + off)
|
| 185 |
+
|
| 186 |
+
# -- multivariate group batch
|
| 187 |
+
def group_batch(self, Bg: int, V: int, cov_prob: float = 0.35):
|
| 188 |
+
"""Returns values (Bg,V,T), roles (Bg,V)."""
|
| 189 |
+
T = self.T
|
| 190 |
+
R = Bg * V
|
| 191 |
+
if V == 1:
|
| 192 |
+
vals = self.affine(self.univariate(Bg)).view(Bg, 1, T)
|
| 193 |
+
return vals, torch.zeros(Bg, 1, dtype=torch.long, device=self.dev)
|
| 194 |
+
K = max(1, V // 2)
|
| 195 |
+
lat = _z(self.univariate(Bg * K)).view(Bg, K, T)
|
| 196 |
+
# lead-lag delays per (row, latent)
|
| 197 |
+
lag = torch.randint(0, 48, (Bg, V, K), generator=self.g, device=self.dev)
|
| 198 |
+
lag = lag * self.B(0.5, Bg, V, K).long()
|
| 199 |
+
idx = (self.t.long().view(1, 1, 1, T) - lag[..., None]).clamp_min(0)
|
| 200 |
+
shifted = torch.gather(lat[:, None].expand(Bg, V, K, T), 3, idx)
|
| 201 |
+
W = self.N(Bg, V, K, 1) * self.B(0.6, Bg, V, K, 1)
|
| 202 |
+
W[:, :, 0] = W[:, :, 0] + 0.5
|
| 203 |
+
mixed = (W * shifted).sum(2)
|
| 204 |
+
f = torch.randint(0, 5, (Bg, V, 1), generator=self.g, device=self.dev)
|
| 205 |
+
nl = torch.stack([mixed, torch.tanh(mixed), mixed.abs(), torch.relu(mixed), mixed + 0.3 * mixed**2], 0)
|
| 206 |
+
mixed = torch.gather(nl, 0, f[None].expand(1, Bg, V, T))[0]
|
| 207 |
+
own = _z(self.univariate(R)).view(Bg, V, T)
|
| 208 |
+
a = self.U(Bg, V, 1, lo=0.0, hi=1.0)
|
| 209 |
+
indep = self.B(0.15, Bg, 1, 1)
|
| 210 |
+
rows = torch.where(indep > 0, own, (1 - a) * _z(mixed) + a * own)
|
| 211 |
+
roles = torch.zeros(Bg, V, dtype=torch.long, device=self.dev)
|
| 212 |
+
|
| 213 |
+
# covariate tasks
|
| 214 |
+
cov_g = self.U(Bg) < cov_prob
|
| 215 |
+
if cov_g.any():
|
| 216 |
+
n_cov = torch.randint(1, V, (Bg,), generator=self.g, device=self.dev)
|
| 217 |
+
ridx = torch.arange(V, device=self.dev).view(1, V)
|
| 218 |
+
is_cov = (ridx >= (V - n_cov).view(Bg, 1)) & cov_g.view(Bg, 1)
|
| 219 |
+
known = is_cov & (self.U(Bg, V) < 0.6)
|
| 220 |
+
# some covariates are 0/1 event indicators (holiday / promo / calendar)
|
| 221 |
+
ev_rate = self.logU(Bg, V, 1, lo=0.005, hi=0.15)
|
| 222 |
+
per = PERIOD_BANK.to(self.dev)[torch.randint(0, len(PERIOD_BANK), (Bg, V, 1), generator=self.g, device=self.dev)]
|
| 223 |
+
cal = ((self.t.view(1, 1, T) % per) < torch.clamp(per * self.U(Bg, V, 1, lo=0.05, hi=0.3), min=1)).float()
|
| 224 |
+
evt = (self.U(Bg, V, T) < ev_rate).float()
|
| 225 |
+
ind = torch.where(self.U(Bg, V, 1) < 0.5, cal, evt)
|
| 226 |
+
binary = is_cov & (self.U(Bg, V) < 0.4)
|
| 227 |
+
cov_vals = torch.where(binary[..., None], ind, rows)
|
| 228 |
+
# target response to covariates: lagged nonlinear effects
|
| 229 |
+
beta = self.N(Bg, V, 1) * self.U(Bg, 1, 1, lo=0.5, hi=2.5)
|
| 230 |
+
clag = torch.randint(0, 8, (Bg, V, 1), generator=self.g, device=self.dev) * (~known[..., None]).long()
|
| 231 |
+
cidx = (self.t.long().view(1, 1, T) - clag).clamp_min(0)
|
| 232 |
+
cl = torch.gather(cov_vals, 2, cidx)
|
| 233 |
+
eff_in = torch.where(binary[..., None], cl, _z(cl))
|
| 234 |
+
eff = (beta * torch.where(self.U(Bg, V, 1) < 0.5, eff_in, torch.tanh(eff_in)) * is_cov[..., None].float()).sum(1, keepdim=True)
|
| 235 |
+
tgt_mask = (~is_cov)[..., None].float()
|
| 236 |
+
rows = torch.where(is_cov[..., None], cov_vals, rows + tgt_mask * eff * cov_g.view(Bg, 1, 1).float())
|
| 237 |
+
roles = torch.where(is_cov, torch.where(known, 2, 1), roles)
|
| 238 |
+
bin_rows = binary.view(R)
|
| 239 |
+
else:
|
| 240 |
+
bin_rows = torch.zeros(R, dtype=torch.bool, device=self.dev)
|
| 241 |
+
flat = rows.reshape(R, T)
|
| 242 |
+
aff = self.affine(flat)
|
| 243 |
+
flat = torch.where(bin_rows[:, None], flat, aff)
|
| 244 |
+
return flat.view(Bg, V, T), roles
|
| 245 |
+
|
| 246 |
+
def masks(self, vals: Tensor, roles: Tensor) -> Tensor:
|
| 247 |
+
Bg, V, T = vals.shape
|
| 248 |
+
obs = torch.ones(Bg, V, T, dtype=torch.bool, device=self.dev)
|
| 249 |
+
# iid missing
|
| 250 |
+
pm = self.U(Bg, V, 1, hi=0.15) * self.B(0.2, Bg, V, 1)
|
| 251 |
+
obs &= self.U(Bg, V, T) >= pm
|
| 252 |
+
# contiguous gaps
|
| 253 |
+
gap = self.B(0.15, Bg, V, 1) > 0
|
| 254 |
+
g0 = self.U(Bg, V, 1) * T
|
| 255 |
+
gl = self.logU(Bg, V, 1, lo=8, hi=T / 4)
|
| 256 |
+
in_gap = (self.t.view(1, 1, T) >= g0) & (self.t.view(1, 1, T) < g0 + gl)
|
| 257 |
+
obs &= ~(gap & in_gap)
|
| 258 |
+
# late start (left padding), per group
|
| 259 |
+
late = self.B(0.25, Bg, 1, 1) > 0
|
| 260 |
+
st = (self.U(Bg, 1, 1) * T * 0.8)
|
| 261 |
+
obs &= ~(late & (self.t.view(1, 1, T) < st))
|
| 262 |
+
return obs
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
class SynthStream:
|
| 266 |
+
"""Endless batches of roughly `tokens_per_batch` encoder patch tokens."""
|
| 267 |
+
|
| 268 |
+
V_CHOICES = (1, 2, 4, 8, 16)
|
| 269 |
+
V_PROBS = (0.45, 0.1, 0.15, 0.15, 0.15)
|
| 270 |
+
|
| 271 |
+
def __init__(self, T: int, tokens_per_batch: int, device="cpu", seed=0, patch=32, block=32, min_ctx=4, chunk=8):
|
| 272 |
+
self.gen = SynthGen(T, device, seed)
|
| 273 |
+
self.chunk = chunk # batches generated per call (amortises kernel-launch overhead on GPU)
|
| 274 |
+
self.cache: dict[int, list] = {}
|
| 275 |
+
self.T, self.patch, self.N = T, patch, T // patch
|
| 276 |
+
self.tok = tokens_per_batch
|
| 277 |
+
self.min_ctx = min_ctx
|
| 278 |
+
self.dev = torch.device(device)
|
| 279 |
+
self.rng = torch.Generator().manual_seed(seed + 1)
|
| 280 |
+
|
| 281 |
+
def next_batch(self, V: int | None = None) -> dict:
|
| 282 |
+
if V is None:
|
| 283 |
+
i = torch.multinomial(torch.tensor(self.V_PROBS), 1, generator=self.rng).item()
|
| 284 |
+
V = self.V_CHOICES[i]
|
| 285 |
+
Bg = max(1, self.tok // (V * self.N))
|
| 286 |
+
if not self.cache.get(V):
|
| 287 |
+
vals, roles = self.gen.group_batch(Bg * self.chunk, V)
|
| 288 |
+
obs = self.gen.masks(vals, roles)
|
| 289 |
+
vals = torch.nan_to_num(vals, nan=0.0, posinf=0.0, neginf=0.0)
|
| 290 |
+
s = torch.randint(self.min_ctx, self.N - 1, (Bg * self.chunk,), generator=self.gen.g, device=self.dev)
|
| 291 |
+
self.cache[V] = [
|
| 292 |
+
{"values": vals[i * Bg:(i + 1) * Bg], "observed": obs[i * Bg:(i + 1) * Bg],
|
| 293 |
+
"roles": roles[i * Bg:(i + 1) * Bg], "s": s[i * Bg:(i + 1) * Bg]}
|
| 294 |
+
for i in range(self.chunk)
|
| 295 |
+
]
|
| 296 |
+
return self.cache[V].pop()
|
modular_ts/synth2.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Synthetic prior v2 (Modular-TS v3): new generator families + augmentation cascade.
|
| 2 |
+
|
| 3 |
+
Motivated by the run2/run3 GIFT-Eval diagnosis (v3/INSIGHTS.md) and TempoPFN's
|
| 4 |
+
leave-one-out prior ablation (arXiv 2510.25502, Table 3: removing the regime-switching
|
| 5 |
+
OU/SDE prior costs the most, then CauKer, spikes, stochastic rhythms, step functions,
|
| 6 |
+
sawtooth; the augmentation cascade is worth ~5% CRPS):
|
| 7 |
+
|
| 8 |
+
New univariate families (all vectorised torch, run on the GPU):
|
| 9 |
+
* ou_regime - Ornstein-Uhlenbeck with Markov regime switching of mean / vol / speed
|
| 10 |
+
* waveform - non-sinusoidal periodic waves: sawtooth, square (random duty), triangle,
|
| 11 |
+
spike trains, with optional slow period drift (phase-drifting cycles)
|
| 12 |
+
* template - "load-curve" seasonality: a random smooth+spiky daily profile tiled at a
|
| 13 |
+
bank period, optionally modulated by a weekly profile (24x7, 96x7, 288x7)
|
| 14 |
+
and multiplicative trend; near-deterministic, low-noise (m4_hourly-like)
|
| 15 |
+
* steps - piecewise-constant levels with Poisson change points (+ small noise)
|
| 16 |
+
* growth - integrated / cumulative processes: cumsum of a positive rate (covid-like),
|
| 17 |
+
logistic / Gompertz growth with noise, compound-interest trends
|
| 18 |
+
Augmentations (applied after generation, each with its own probability):
|
| 19 |
+
time reversal, integration (cumsum), differencing, quantisation to a random grid,
|
| 20 |
+
censoring (clip at a random quantile), random smoothing conv filter, sample-and-hold
|
| 21 |
+
resampling artefacts, amplitude modulation, TSMixup of two series.
|
| 22 |
+
|
| 23 |
+
`SynthGen2.univariate` returns a mixture of the v1 prior (`SynthGen.univariate`) and the new
|
| 24 |
+
families; everything downstream (multivariatizers, covariates, masks) is inherited.
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
from __future__ import annotations
|
| 28 |
+
|
| 29 |
+
import math
|
| 30 |
+
|
| 31 |
+
import torch
|
| 32 |
+
from torch import Tensor
|
| 33 |
+
|
| 34 |
+
from modular_ts.synth import PERIOD_BANK, SynthGen, SynthStream, _z
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _fft_filter(e: Tensor, phi: Tensor, L: int) -> Tensor:
|
| 38 |
+
"""y_t = sum_k phi^k e_{t-k} for each row (AR(1) filter via FFT), phi (n,)."""
|
| 39 |
+
n, T = e.shape
|
| 40 |
+
L = min(T, L)
|
| 41 |
+
k = phi.view(n, 1) ** torch.arange(L, device=e.device, dtype=torch.float32)
|
| 42 |
+
nfft = 1 << (T + L - 1).bit_length()
|
| 43 |
+
return torch.fft.irfft(torch.fft.rfft(e, nfft) * torch.fft.rfft(k, nfft), nfft)[:, :T]
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
class SynthGen2(SynthGen):
|
| 47 |
+
def __init__(self, *a, p_new: float = 0.55, p_aug: float = 0.5, **k):
|
| 48 |
+
super().__init__(*a, **k)
|
| 49 |
+
self.p_new, self.p_aug = p_new, p_aug
|
| 50 |
+
|
| 51 |
+
# ------------------------------------------------------------------ families
|
| 52 |
+
def _regimes(self, n, rate_lo=1e-4, rate_hi=3e-3):
|
| 53 |
+
"""Piecewise-constant regime index field (n,T) in [0,1) via Poisson change points."""
|
| 54 |
+
T = self.T
|
| 55 |
+
rate = self.logU(n, 1, lo=rate_lo, hi=rate_hi)
|
| 56 |
+
jumps = (self.U(n, T) < rate).float()
|
| 57 |
+
seg = torch.cumsum(jumps, -1).long() % 64 # segment id
|
| 58 |
+
table = self.U(n, 64)
|
| 59 |
+
return torch.gather(table, 1, seg)
|
| 60 |
+
|
| 61 |
+
def ou_regime(self, n):
|
| 62 |
+
r = self._regimes(n)
|
| 63 |
+
mu = (r - 0.5) * self.U(n, 1, lo=0.0, hi=6.0)
|
| 64 |
+
vol = torch.exp(self.U(n, 1, lo=-1.0, hi=1.0) * (self._regimes(n) - 0.5) * 2)
|
| 65 |
+
theta = self.logU(n, lo=1e-3, hi=0.3)
|
| 66 |
+
e = theta.view(n, 1) * mu + 0.3 * vol * self.N(n, self.T)
|
| 67 |
+
return _fft_filter(e, 1 - theta, 2048)
|
| 68 |
+
|
| 69 |
+
def _period(self, n):
|
| 70 |
+
idx = torch.randint(0, len(PERIOD_BANK), (n,), generator=self.g, device=self.dev)
|
| 71 |
+
per = PERIOD_BANK.to(self.dev)[idx]
|
| 72 |
+
rnd = self.logU(n, lo=3.0, hi=max(4.0, self.T / 4))
|
| 73 |
+
return torch.where(self.U(n) < 0.3, rnd, per)
|
| 74 |
+
|
| 75 |
+
def _phase(self, n, per):
|
| 76 |
+
"""Cycle phase in [0,1) with optional slow period drift (phase-drifting cycles)."""
|
| 77 |
+
drift = torch.where(self.U(n) < 0.3, self.N(n) * 0.02, torch.zeros(n, device=self.dev))
|
| 78 |
+
inst = 1.0 / per.view(n, 1) * (1 + drift.view(n, 1) * torch.tanh(self.rbf(n)))
|
| 79 |
+
return torch.frac(torch.cumsum(inst.expand(n, self.T), -1) + self.U(n, 1))
|
| 80 |
+
|
| 81 |
+
def waveform(self, n):
|
| 82 |
+
per = self._period(n)
|
| 83 |
+
ph = self._phase(n, per)
|
| 84 |
+
kind = torch.randint(0, 4, (n, 1), generator=self.g, device=self.dev)
|
| 85 |
+
duty = self.U(n, 1, lo=0.1, hi=0.9)
|
| 86 |
+
saw = ph * 2 - 1
|
| 87 |
+
sq = torch.where(ph < duty, 1.0, -1.0)
|
| 88 |
+
tri = 1 - 4 * (ph - 0.5).abs()
|
| 89 |
+
spk = torch.exp(-((ph - 0.5) ** 2) / (2 * self.logU(n, 1, lo=1e-4, hi=1e-2)))
|
| 90 |
+
w = torch.where(kind == 0, saw, torch.where(kind == 1, sq, torch.where(kind == 2, tri, spk)))
|
| 91 |
+
w = torch.where(self.U(n, 1) < 0.5, -w, w)
|
| 92 |
+
return w
|
| 93 |
+
|
| 94 |
+
def template(self, n):
|
| 95 |
+
"""Tiled random daily profile (smooth + bumps) x optional weekly profile."""
|
| 96 |
+
T = self.T
|
| 97 |
+
per = PERIOD_BANK.to(self.dev)[torch.randint(4, len(PERIOD_BANK), (n,), generator=self.g, device=self.dev)]
|
| 98 |
+
per = torch.where(self.U(n) < 0.6, torch.tensor([24.0, 96.0, 288.0, 48.0, 12.0, 7.0], device=self.dev)[
|
| 99 |
+
torch.randint(0, 6, (n,), generator=self.g, device=self.dev)], per)
|
| 100 |
+
ph = self._phase(n, per) # (n,T) in [0,1)
|
| 101 |
+
# profile = sum of K random bumps on the circle + low-order Fourier terms
|
| 102 |
+
K = 4
|
| 103 |
+
c = self.U(n, K, 1)
|
| 104 |
+
wdt = self.logU(n, K, 1, lo=0.02, hi=0.25)
|
| 105 |
+
a = self.N(n, K, 1).abs() * torch.where(self.U(n, K, 1) < 0.2, -1.0, 1.0)
|
| 106 |
+
d = torch.minimum((ph[:, None] - c).abs(), 1 - (ph[:, None] - c).abs())
|
| 107 |
+
prof = (a * torch.exp(-0.5 * (d / wdt) ** 2)).sum(1)
|
| 108 |
+
h = torch.arange(1, 4, device=self.dev, dtype=torch.float32).view(1, 3, 1)
|
| 109 |
+
prof = prof + (0.3 * self.N(n, 3, 1) / h * torch.sin(2 * math.pi * h * ph[:, None] + self.U(n, 3, 1, hi=6.3))).sum(1)
|
| 110 |
+
# weekly modulation for intraday periods
|
| 111 |
+
wk = self.U(n, 7, lo=0.5, hi=1.2)
|
| 112 |
+
wk = torch.where(self.U(n, 1) < 0.5, wk, torch.ones_like(wk))
|
| 113 |
+
day = (torch.cumsum((ph[:, 1:] < ph[:, :-1]).float(), -1)).long() % 7
|
| 114 |
+
day = torch.cat([torch.zeros(n, 1, dtype=torch.long, device=self.dev), day], -1)
|
| 115 |
+
mod = torch.gather(wk, 1, day)
|
| 116 |
+
sig = prof * mod
|
| 117 |
+
# slowly varying level and tiny noise (near-deterministic series)
|
| 118 |
+
sig = sig + self.B(0.5, n, 1) * 0.3 * _z(self.trend(n)) + self.logU(n, 1, lo=1e-3, hi=0.2) * self.N(n, T)
|
| 119 |
+
return sig
|
| 120 |
+
|
| 121 |
+
def steps(self, n):
|
| 122 |
+
r = self._regimes(n, 2e-4, 1e-2)
|
| 123 |
+
lv = torch.where(self.U(n, 1) < 0.5, torch.round(r * self.U(n, 1, lo=2, hi=20)), r)
|
| 124 |
+
return lv + self.logU(n, 1, lo=1e-3, hi=0.1) * self.N(n, self.T)
|
| 125 |
+
|
| 126 |
+
def growth(self, n):
|
| 127 |
+
T = self.T
|
| 128 |
+
tt = self.t / T
|
| 129 |
+
kind = torch.randint(0, 3, (n, 1), generator=self.g, device=self.dev)
|
| 130 |
+
# cumulative counts: cumsum of positive, slowly varying rate (+ noise)
|
| 131 |
+
rate = torch.exp(0.8 * _z(self.rbf(n)).clamp(-3, 3)) * (1 + 0.3 * self.N(n, T).abs() * self.B(0.5, n, 1))
|
| 132 |
+
cum = torch.cumsum(rate, -1)
|
| 133 |
+
# logistic / gompertz
|
| 134 |
+
t0 = self.U(n, 1, lo=-0.2, hi=1.2)
|
| 135 |
+
k = self.logU(n, 1, lo=2.0, hi=30.0)
|
| 136 |
+
logi = torch.sigmoid(k * (tt - t0))
|
| 137 |
+
gomp = torch.exp(-torch.exp(-k * (tt - t0)))
|
| 138 |
+
sig = torch.where(kind == 0, cum, torch.where(kind == 1, logi, gomp))
|
| 139 |
+
sig = sig * (1 + self.logU(n, 1, lo=1e-3, hi=0.05) * self.N(n, T))
|
| 140 |
+
return sig
|
| 141 |
+
|
| 142 |
+
# ------------------------------------------------------------------ augmentations
|
| 143 |
+
def augment(self, x: Tensor) -> Tensor:
|
| 144 |
+
n, T = x.shape
|
| 145 |
+
on = lambda p: self.U(n, 1) < p * self.p_aug * 2 # noqa: E731
|
| 146 |
+
# time reversal
|
| 147 |
+
x = torch.where(on(0.15), x.flip(-1), x)
|
| 148 |
+
# integration / differencing
|
| 149 |
+
xi = torch.cumsum(x - x.mean(-1, keepdim=True), -1)
|
| 150 |
+
x = torch.where(on(0.06), xi, x)
|
| 151 |
+
xd = torch.cat([x[:, :1] * 0, x[:, 1:] - x[:, :-1]], -1)
|
| 152 |
+
x = torch.where(on(0.04), xd, x)
|
| 153 |
+
# amplitude modulation
|
| 154 |
+
x = torch.where(on(0.1), x * (1 + 0.5 * torch.tanh(self.rbf(n))), x)
|
| 155 |
+
# random smoothing conv filter (moving average of random width via cumsum trick)
|
| 156 |
+
w = torch.randint(2, 16, (n, 1), generator=self.g, device=self.dev)
|
| 157 |
+
cs = torch.cumsum(torch.nn.functional.pad(x, (1, 0)), -1)
|
| 158 |
+
idx = torch.arange(1, T + 1, device=self.dev).view(1, T)
|
| 159 |
+
lo = (idx - w).clamp_min(0)
|
| 160 |
+
ma = (cs[:, 1:] - torch.gather(cs, 1, lo)) / (idx - lo).float()
|
| 161 |
+
x = torch.where(on(0.1), ma, x)
|
| 162 |
+
# sample-and-hold resampling artefacts
|
| 163 |
+
k = torch.randint(2, 8, (n, 1), generator=self.g, device=self.dev)
|
| 164 |
+
hold = torch.gather(x, 1, (torch.arange(T, device=self.dev).view(1, T) // k) * k)
|
| 165 |
+
x = torch.where(on(0.05), hold, x)
|
| 166 |
+
# TSMixup with a rolled partner
|
| 167 |
+
lam = self.U(n, 1, lo=0.2, hi=0.8)
|
| 168 |
+
xm = lam * _z(x) + (1 - lam) * _z(x.roll(1, 0))
|
| 169 |
+
x = torch.where(on(0.1), xm, x)
|
| 170 |
+
# censoring at a random quantile (one side)
|
| 171 |
+
qv = torch.quantile(x[:, :: max(1, T // 512)], 0.8 + 0.19 * torch.rand(1, device=self.dev).item(), dim=-1, keepdim=True)
|
| 172 |
+
x = torch.where(on(0.05), torch.minimum(x, qv), x)
|
| 173 |
+
return x
|
| 174 |
+
|
| 175 |
+
def quantize(self, x: Tensor) -> Tensor:
|
| 176 |
+
"""Round some series to a coarse grid (integer-valued / low-resolution sensors)."""
|
| 177 |
+
n = x.shape[0]
|
| 178 |
+
step = x.std(-1, keepdim=True).clamp_min(1e-6) * self.logU(n, 1, lo=0.02, hi=0.5)
|
| 179 |
+
q = torch.round(x / step) * step
|
| 180 |
+
return torch.where(self.U(n, 1) < 0.08, q, x)
|
| 181 |
+
|
| 182 |
+
# ------------------------------------------------------------------ mixture
|
| 183 |
+
def univariate(self, n) -> Tensor:
|
| 184 |
+
base = super().univariate(n)
|
| 185 |
+
fam = torch.randint(0, 5, (n, 1), generator=self.g, device=self.dev)
|
| 186 |
+
new = torch.zeros(n, self.T, device=self.dev)
|
| 187 |
+
gens = (self.ou_regime, self.waveform, self.template, self.steps, self.growth)
|
| 188 |
+
for i, g in enumerate(gens):
|
| 189 |
+
new = torch.where(fam == i, _z(g(n)), new)
|
| 190 |
+
# optional combination of the new family with a v1 component (additive)
|
| 191 |
+
comb = self.U(n, 1) < 0.35
|
| 192 |
+
new = torch.where(comb, new + self.logU(n, 1, lo=0.05, hi=1.0) * _z(base), new)
|
| 193 |
+
use_new = self.U(n, 1) < self.p_new
|
| 194 |
+
x = torch.where(use_new, new, base)
|
| 195 |
+
x = self.augment(x)
|
| 196 |
+
x = torch.nan_to_num(x, nan=0.0, posinf=0.0, neginf=0.0)
|
| 197 |
+
return x
|
| 198 |
+
|
| 199 |
+
def affine(self, x: Tensor) -> Tensor:
|
| 200 |
+
return self.quantize(super().affine(x))
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
class SynthStream2(SynthStream):
|
| 204 |
+
def __init__(self, T, tokens_per_batch, device="cpu", seed=0, patch=32, block=32, min_ctx=4, chunk=8, p_new=0.55):
|
| 205 |
+
super().__init__(T, tokens_per_batch, device, seed, patch, block, min_ctx, chunk)
|
| 206 |
+
self.gen = SynthGen2(T, device, seed, p_new=p_new)
|
modular_ts/teacher_gen.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Stage B: cache teacher quantile forecasts (TimesFM-2.5-200M + Chronos-2) for distillation.
|
| 2 |
+
|
| 3 |
+
Run in the teacher venv (~/tvenv, system torch):
|
| 4 |
+
~/tvenv/bin/python -m modular_ts.teacher_gen --source synth --n 200000 --out teach/synth
|
| 5 |
+
~/tvenv/bin/python -m modular_ts.teacher_gen --source gep --n 100000 --out teach/gep --gep_selection gep_selection.json
|
| 6 |
+
|
| 7 |
+
Each shard teach/<src>/shard_XXXX.npz holds
|
| 8 |
+
ctx (n, C) float32 context, NaN = unobserved (left-padded for short contexts)
|
| 9 |
+
fut (n, F) float32 ground-truth continuation (synthetic only; NaN for GEP = teacher-only input)
|
| 10 |
+
q_tfm, q_cr2 (n, H, 9) float32 teacher quantiles 0.1..0.9 for the first H future steps
|
| 11 |
+
Teacher weights: google/timesfm-2.5-200m-pytorch (Apache-2.0), amazon/chronos-2 (Apache-2.0).
|
| 12 |
+
GiftEvalPretrain inputs use the same GIFT-Eval/fev/TIME leakage filter as run1 (realdata.filter_datasets).
|
| 13 |
+
NEVER uses Timestral weights.
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
import argparse
|
| 19 |
+
import json
|
| 20 |
+
import math
|
| 21 |
+
import os
|
| 22 |
+
import time
|
| 23 |
+
|
| 24 |
+
import numpy as np
|
| 25 |
+
import torch
|
| 26 |
+
|
| 27 |
+
Q9 = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def interp_nans(x: np.ndarray) -> np.ndarray:
|
| 31 |
+
m = np.isfinite(x)
|
| 32 |
+
if m.all():
|
| 33 |
+
return x
|
| 34 |
+
idx = np.arange(len(x))
|
| 35 |
+
return np.interp(idx, idx[m], x[m]).astype(np.float32)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class Teachers:
|
| 39 |
+
def __init__(self, C, H, dev="cuda", tfm_bs=256, cr_bs=512):
|
| 40 |
+
import timesfm
|
| 41 |
+
from chronos import Chronos2Pipeline
|
| 42 |
+
torch.set_float32_matmul_precision("high")
|
| 43 |
+
self.H = H
|
| 44 |
+
self.cr = Chronos2Pipeline.from_pretrained("amazon/chronos-2", device_map=dev)
|
| 45 |
+
self.cr_bs = cr_bs
|
| 46 |
+
self.tfm = timesfm.TimesFM_2p5_200M_torch.from_pretrained("google/timesfm-2.5-200m-pytorch")
|
| 47 |
+
self.tfm.compile(timesfm.ForecastConfig(max_context=C, max_horizon=H, normalize_inputs=True, per_core_batch_size=tfm_bs,
|
| 48 |
+
use_continuous_quantile_head=True, force_flip_invariance=True,
|
| 49 |
+
infer_is_positive=True, fix_quantile_crossing=True))
|
| 50 |
+
|
| 51 |
+
def __call__(self, ctx: np.ndarray):
|
| 52 |
+
"""ctx (n, C) with NaN -> q_tfm, q_cr2 (n, H, 9)."""
|
| 53 |
+
seqs = []
|
| 54 |
+
for row in ctx:
|
| 55 |
+
f = np.isfinite(row)
|
| 56 |
+
first = int(np.argmax(f)) if f.any() else len(row) - 1
|
| 57 |
+
seqs.append(row[first:])
|
| 58 |
+
with torch.no_grad():
|
| 59 |
+
qs, _ = self.cr.predict_quantiles([torch.from_numpy(s) for s in seqs], prediction_length=self.H,
|
| 60 |
+
quantile_levels=Q9, batch_size=self.cr_bs)
|
| 61 |
+
q_cr2 = torch.stack([q[0] for q in qs]).float().cpu().numpy()
|
| 62 |
+
_, qt = self.tfm.forecast(horizon=self.H, inputs=[interp_nans(s) for s in seqs])
|
| 63 |
+
q_tfm = np.asarray(qt)[:, :self.H, 1:10].astype(np.float32)
|
| 64 |
+
return q_tfm, q_cr2
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def synth_windows(gen, n, C, F, rng):
|
| 68 |
+
vals, roles = gen.group_batch(n, 1)
|
| 69 |
+
v = vals[:, 0].float().cpu().numpy()
|
| 70 |
+
T = C + F
|
| 71 |
+
# context length: 50% full C, else log-uniform in [48, C]; 15% of series get iid missing values
|
| 72 |
+
clen = np.where(rng.random(n) < 0.5, C, np.exp(rng.uniform(math.log(48), math.log(C), n))).astype(int)
|
| 73 |
+
obs = np.ones((n, T), bool)
|
| 74 |
+
obs[np.arange(T)[None] < (C - clen)[:, None]] = False
|
| 75 |
+
miss = rng.random(n) < 0.15
|
| 76 |
+
pm = rng.uniform(0, 0.1, n)[:, None]
|
| 77 |
+
obs &= ~(miss[:, None] & (rng.random((n, T)) < pm))
|
| 78 |
+
v = np.where(obs, v, np.nan).astype(np.float32)
|
| 79 |
+
return v[:, :C], v[:, C:]
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def main():
|
| 83 |
+
ap = argparse.ArgumentParser()
|
| 84 |
+
ap.add_argument("--source", choices=["synth", "gep"], required=True)
|
| 85 |
+
ap.add_argument("--n", type=int, default=100000)
|
| 86 |
+
ap.add_argument("--shard", type=int, default=8192)
|
| 87 |
+
ap.add_argument("--C", type=int, default=2048)
|
| 88 |
+
ap.add_argument("--F", type=int, default=1024)
|
| 89 |
+
ap.add_argument("--H", type=int, default=512)
|
| 90 |
+
ap.add_argument("--out", required=True)
|
| 91 |
+
ap.add_argument("--seed", type=int, default=123)
|
| 92 |
+
ap.add_argument("--gep_selection", default="gep_selection.json")
|
| 93 |
+
ap.add_argument("--max_minutes", type=float, default=0)
|
| 94 |
+
args = ap.parse_args()
|
| 95 |
+
os.makedirs(args.out, exist_ok=True)
|
| 96 |
+
dev = torch.device("cuda")
|
| 97 |
+
rng = np.random.default_rng(args.seed)
|
| 98 |
+
T = args.C + args.F
|
| 99 |
+
if args.source == "synth":
|
| 100 |
+
from modular_ts.synth import SynthGen
|
| 101 |
+
gen = SynthGen(T, dev, seed=args.seed)
|
| 102 |
+
else:
|
| 103 |
+
from modular_ts.realdata import FastGiftEvalPretrain
|
| 104 |
+
gep = FastGiftEvalPretrain(T, args.shard * 2 * (T // 32), dev, json.load(open(args.gep_selection)), seed=args.seed,
|
| 105 |
+
mv_frac=0.0, short_ctx_p=0.3)
|
| 106 |
+
json.dump(gep.manifest(), open(os.path.join(args.out, "gep_manifest.json"), "w"), indent=1)
|
| 107 |
+
tea = Teachers(args.C, args.H)
|
| 108 |
+
t0, done, k = time.time(), 0, 0
|
| 109 |
+
while done < args.n:
|
| 110 |
+
if args.max_minutes and (time.time() - t0) / 60 > args.max_minutes:
|
| 111 |
+
print("time limit", flush=True)
|
| 112 |
+
break
|
| 113 |
+
p = os.path.join(args.out, f"shard_{k:04d}.npz")
|
| 114 |
+
if os.path.exists(p):
|
| 115 |
+
done += args.shard; k += 1
|
| 116 |
+
continue
|
| 117 |
+
if args.source == "synth":
|
| 118 |
+
gen = SynthGen(T, dev, seed=args.seed * 1000 + k) # per-shard seed -> resumable without duplicates
|
| 119 |
+
rng = np.random.default_rng(args.seed * 1000 + k)
|
| 120 |
+
ctx, fut = synth_windows(gen, args.shard, args.C, args.F, rng)
|
| 121 |
+
else:
|
| 122 |
+
gep.g.manual_seed(args.seed * 1000 + k); gep.rng.seed(args.seed * 1000 + k)
|
| 123 |
+
b = gep.next_batch()
|
| 124 |
+
v = torch.where(b["observed"][:, 0], b["values"][:, 0], torch.full_like(b["values"][:, 0], float("nan"))).cpu().numpy()
|
| 125 |
+
ok = np.isfinite(v[:, :args.C]).sum(1) >= 48
|
| 126 |
+
v = v[ok][:args.shard]
|
| 127 |
+
ctx, fut = v[:, :args.C], np.full((len(v), args.F), np.nan, np.float32)
|
| 128 |
+
ts = time.time()
|
| 129 |
+
q_tfm, q_cr2 = tea(ctx)
|
| 130 |
+
good = np.isfinite(q_tfm).all((1, 2)) & np.isfinite(q_cr2).all((1, 2))
|
| 131 |
+
np.savez(p, ctx=ctx[good], fut=fut[good], q_tfm=q_tfm[good].astype(np.float32), q_cr2=q_cr2[good].astype(np.float32))
|
| 132 |
+
done += int(good.sum()); k += 1
|
| 133 |
+
print(json.dumps({"shard": k, "n": int(good.sum()), "dropped": int((~good).sum()), "teach_s": round(time.time() - ts, 1),
|
| 134 |
+
"done": done, "min": round((time.time() - t0) / 60, 2)}), flush=True)
|
| 135 |
+
print("TEACH_DONE", done, flush=True)
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
if __name__ == "__main__":
|
| 139 |
+
main()
|
modular_ts/train.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Train Modular-TS from scratch with DHF + pinball loss.
|
| 2 |
+
|
| 3 |
+
python -m modular_ts.train --size 2.5M --ctx 4096 --tokens_per_batch 65536 --steps 20000 \
|
| 4 |
+
--lr 2e-3 --warmup 1000 --out runs/smoke --max_minutes 35 [--compile] [--data synthetic]
|
| 5 |
+
|
| 6 |
+
Logs JSONL to <out>/log.jsonl (step, loss, val, lr, enc tokens/s, grad norm),
|
| 7 |
+
checkpoints to <out>/ckpt.pt and an inference export (config.json + model.safetensors,
|
| 8 |
+
loadable by timestral.TimestralForecaster) to <out>/export/.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
import argparse
|
| 14 |
+
import json
|
| 15 |
+
import math
|
| 16 |
+
import os
|
| 17 |
+
import time
|
| 18 |
+
|
| 19 |
+
import torch
|
| 20 |
+
|
| 21 |
+
from modular_ts.model import ModularTS, count_params, make_config
|
| 22 |
+
from modular_ts.synth import SynthStream
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def export(model: ModularTS, out_dir: str) -> None:
|
| 26 |
+
from safetensors.torch import save_file
|
| 27 |
+
os.makedirs(out_dir, exist_ok=True)
|
| 28 |
+
sd = {k: v.detach().float().cpu().contiguous() for k, v in model.state_dict().items()}
|
| 29 |
+
save_file(sd, os.path.join(out_dir, "model.safetensors"))
|
| 30 |
+
model.config.save(os.path.join(out_dir, "config.json"))
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def lr_at(step, total, warmup, peak, final_frac=0.05):
|
| 34 |
+
if step < warmup:
|
| 35 |
+
return peak * (step + 1) / warmup
|
| 36 |
+
prog = min(1.0, (step - warmup) / max(1, total - warmup))
|
| 37 |
+
return peak * (final_frac + (1 - final_frac) * 0.5 * (1 + math.cos(math.pi * prog)))
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def main():
|
| 41 |
+
ap = argparse.ArgumentParser()
|
| 42 |
+
ap.add_argument("--size", default="2.5M")
|
| 43 |
+
ap.add_argument("--ctx", type=int, default=4096)
|
| 44 |
+
ap.add_argument("--tokens_per_batch", type=int, default=65536, help="encoder patch tokens per step")
|
| 45 |
+
ap.add_argument("--steps", type=int, default=20000, help="schedule length (cosine horizon)")
|
| 46 |
+
ap.add_argument("--max_minutes", type=float, default=0, help="stop early after this wall time (0 = off)")
|
| 47 |
+
ap.add_argument("--lr", type=float, default=2e-3)
|
| 48 |
+
ap.add_argument("--warmup", type=int, default=1000)
|
| 49 |
+
ap.add_argument("--wd", type=float, default=0.05)
|
| 50 |
+
ap.add_argument("--clip", type=float, default=1.0)
|
| 51 |
+
ap.add_argument("--data", default="synthetic", choices=["synthetic", "synthetic+gifteval"])
|
| 52 |
+
ap.add_argument("--real_frac", type=float, default=0.5)
|
| 53 |
+
ap.add_argument("--gifteval_subset", default="", help="comma list of GiftEvalPretrain sub-datasets (default all kept)")
|
| 54 |
+
ap.add_argument("--gep_selection", default="", help="json {dataset: [shard files]} -> fast GPU-resident GiftEvalPretrain stream")
|
| 55 |
+
ap.add_argument("--synth", default="v1", choices=["v1", "v2"], help="v2 = modular_ts.synth2 prior (v3)")
|
| 56 |
+
ap.add_argument("--p_new", type=float, default=0.55, help="synth v2: share of new generator families")
|
| 57 |
+
ap.add_argument("--real_v2", action="store_true", help="v3 real stream: resolution sampling + TSMixup + ds_temp 0.3")
|
| 58 |
+
ap.add_argument("--min_lr_frac", type=float, default=0.05)
|
| 59 |
+
ap.add_argument("--time_schedule", action="store_true", help="cosine progress = elapsed / max_minutes (smoke runs)")
|
| 60 |
+
ap.add_argument("--compile", action="store_true")
|
| 61 |
+
ap.add_argument("--bf16", action="store_true", default=True)
|
| 62 |
+
ap.add_argument("--no_bf16", dest="bf16", action="store_false")
|
| 63 |
+
ap.add_argument("--log_every", type=int, default=25)
|
| 64 |
+
ap.add_argument("--val_every", type=int, default=250)
|
| 65 |
+
ap.add_argument("--ckpt_every", type=int, default=1000)
|
| 66 |
+
ap.add_argument("--out", default="runs/dev")
|
| 67 |
+
ap.add_argument("--seed", type=int, default=0)
|
| 68 |
+
ap.add_argument("--resume", action="store_true")
|
| 69 |
+
ap.add_argument("--resume_from", default="", help="ckpt to resume model+opt from (default <out>/ckpt.pt)")
|
| 70 |
+
ap.add_argument("--reset_schedule", action="store_true", help="on resume, restart warmup+cosine at the resumed step")
|
| 71 |
+
ap.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
|
| 72 |
+
args = ap.parse_args()
|
| 73 |
+
|
| 74 |
+
os.makedirs(args.out, exist_ok=True)
|
| 75 |
+
torch.manual_seed(args.seed)
|
| 76 |
+
dev = torch.device(args.device)
|
| 77 |
+
if dev.type == "cuda":
|
| 78 |
+
torch.backends.cuda.matmul.allow_tf32 = True
|
| 79 |
+
torch.backends.cudnn.allow_tf32 = True
|
| 80 |
+
|
| 81 |
+
cfg = make_config(args.size, context_length=args.ctx)
|
| 82 |
+
model = ModularTS(cfg).to(dev)
|
| 83 |
+
pc = count_params(model)
|
| 84 |
+
print("params", pc, flush=True)
|
| 85 |
+
|
| 86 |
+
if args.synth == "v2":
|
| 87 |
+
from modular_ts.synth2 import SynthStream2
|
| 88 |
+
synth = SynthStream2(args.ctx, args.tokens_per_batch, dev, seed=args.seed, p_new=args.p_new)
|
| 89 |
+
else:
|
| 90 |
+
synth = SynthStream(args.ctx, args.tokens_per_batch, dev, seed=args.seed)
|
| 91 |
+
stream = synth
|
| 92 |
+
manifest = {"data": args.data, "synth_version": args.synth, "real_v2": args.real_v2, "synthetic": "modular_ts.synth (KernelSynth-style GP + TSI + AR + intermittent/bursty + multivariatizers + covariate tasks)"}
|
| 93 |
+
if args.data == "synthetic+gifteval":
|
| 94 |
+
from modular_ts.realdata import GiftEvalPretrainStream, MixedStream
|
| 95 |
+
sub = [s for s in args.gifteval_subset.split(",") if s] or None
|
| 96 |
+
if args.gep_selection:
|
| 97 |
+
from modular_ts.realdata import FastGiftEvalPretrain
|
| 98 |
+
if args.real_v2:
|
| 99 |
+
from modular_ts.realdata2 import FastGiftEvalPretrain2 as FastGiftEvalPretrain
|
| 100 |
+
real = FastGiftEvalPretrain(args.ctx, args.tokens_per_batch, dev, json.load(open(args.gep_selection)), seed=args.seed)
|
| 101 |
+
else:
|
| 102 |
+
real = GiftEvalPretrainStream(args.ctx, args.tokens_per_batch, dev, seed=args.seed, datasets_subset=sub)
|
| 103 |
+
stream = MixedStream(synth, real, args.real_frac, seed=args.seed)
|
| 104 |
+
manifest.update(real.manifest())
|
| 105 |
+
json.dump(manifest, open(os.path.join(args.out, "data_manifest.json"), "w"), indent=1)
|
| 106 |
+
|
| 107 |
+
# fixed held-out validation batches from a different seed (one per V bucket)
|
| 108 |
+
vgen = SynthStream(args.ctx, args.tokens_per_batch // 2, dev, seed=10_000 + args.seed, chunk=1)
|
| 109 |
+
val_batches = [vgen.next_batch(V) for V in (1, 4, 16)]
|
| 110 |
+
|
| 111 |
+
decay, no_decay = [], []
|
| 112 |
+
for n, p in model.named_parameters():
|
| 113 |
+
(decay if p.dim() >= 2 else no_decay).append(p)
|
| 114 |
+
opt = torch.optim.AdamW([{"params": decay, "weight_decay": args.wd}, {"params": no_decay, "weight_decay": 0.0}],
|
| 115 |
+
lr=args.lr, betas=(0.9, 0.95), eps=1e-8, fused=dev.type == "cuda")
|
| 116 |
+
step = 0
|
| 117 |
+
ck = os.path.join(args.out, "ckpt.pt")
|
| 118 |
+
rck = args.resume_from or ck
|
| 119 |
+
step0 = 0
|
| 120 |
+
prev_elapsed = 0.0
|
| 121 |
+
if args.resume and os.path.exists(rck):
|
| 122 |
+
st = torch.load(rck, map_location=dev)
|
| 123 |
+
model.load_state_dict(st["model"])
|
| 124 |
+
opt.load_state_dict(st["opt"])
|
| 125 |
+
step = st["step"]
|
| 126 |
+
if args.reset_schedule:
|
| 127 |
+
step0 = step
|
| 128 |
+
elif args.time_schedule and "elapsed_min" in st:
|
| 129 |
+
prev_elapsed = st["elapsed_min"] # continue the same time-based schedule after a crash
|
| 130 |
+
print("resumed at", step, "from", rck, flush=True)
|
| 131 |
+
|
| 132 |
+
fwd = model.forward_train
|
| 133 |
+
if args.compile:
|
| 134 |
+
try:
|
| 135 |
+
fwd = torch.compile(model.forward_train, dynamic=False)
|
| 136 |
+
except Exception as e: # pragma: no cover
|
| 137 |
+
print("compile failed, eager:", e, flush=True)
|
| 138 |
+
|
| 139 |
+
amp = torch.autocast(device_type=dev.type, dtype=torch.bfloat16, enabled=args.bf16 and dev.type == "cuda")
|
| 140 |
+
|
| 141 |
+
def evaluate():
|
| 142 |
+
model.eval()
|
| 143 |
+
tot = 0.0
|
| 144 |
+
with torch.no_grad(), amp:
|
| 145 |
+
for b in val_batches:
|
| 146 |
+
l, _ = model.forward_train(b["values"], b["observed"], b["roles"], b["s"])
|
| 147 |
+
tot += l.item()
|
| 148 |
+
model.train()
|
| 149 |
+
return tot / len(val_batches)
|
| 150 |
+
|
| 151 |
+
log = open(os.path.join(args.out, "log.jsonl"), "a")
|
| 152 |
+
t0 = time.time() - prev_elapsed * 60
|
| 153 |
+
t_last, tok_acc, gen_t = time.time(), 0, 0.0
|
| 154 |
+
ema = None
|
| 155 |
+
model.train()
|
| 156 |
+
while step < args.steps:
|
| 157 |
+
if args.max_minutes and (time.time() - t0) / 60 > args.max_minutes:
|
| 158 |
+
print("time limit reached", flush=True)
|
| 159 |
+
break
|
| 160 |
+
prof = step % args.log_every == 0 # measure data-gen share only on log steps (sync is costly)
|
| 161 |
+
if prof and dev.type == "cuda":
|
| 162 |
+
torch.cuda.synchronize()
|
| 163 |
+
tg = time.time()
|
| 164 |
+
b = stream.next_batch()
|
| 165 |
+
if prof and dev.type == "cuda":
|
| 166 |
+
torch.cuda.synchronize()
|
| 167 |
+
if prof:
|
| 168 |
+
gen_t += (time.time() - tg) * args.log_every
|
| 169 |
+
ss = step - step0
|
| 170 |
+
if args.time_schedule and args.max_minutes:
|
| 171 |
+
frac = (time.time() - t0) / 60 / args.max_minutes
|
| 172 |
+
lr = lr_at(ss, max(ss + 1, int(ss / max(frac, 1e-6))) if ss > args.warmup else args.steps, args.warmup, args.lr, args.min_lr_frac)
|
| 173 |
+
else:
|
| 174 |
+
lr = lr_at(ss, args.steps, args.warmup, args.lr, args.min_lr_frac)
|
| 175 |
+
for g in opt.param_groups:
|
| 176 |
+
g["lr"] = lr
|
| 177 |
+
try:
|
| 178 |
+
with amp:
|
| 179 |
+
loss, st = fwd(b["values"], b["observed"], b["roles"], b["s"])
|
| 180 |
+
except Exception as e:
|
| 181 |
+
if fwd is model.forward_train:
|
| 182 |
+
raise
|
| 183 |
+
print("compiled forward failed, falling back to eager:", repr(e)[:500], flush=True)
|
| 184 |
+
fwd = model.forward_train
|
| 185 |
+
with amp:
|
| 186 |
+
loss, st = fwd(b["values"], b["observed"], b["roles"], b["s"])
|
| 187 |
+
if not torch.isfinite(loss):
|
| 188 |
+
print(f"non-finite loss at step {step}; skipping batch", flush=True)
|
| 189 |
+
opt.zero_grad(set_to_none=True)
|
| 190 |
+
step += 1
|
| 191 |
+
continue
|
| 192 |
+
loss.backward()
|
| 193 |
+
gn = torch.nn.utils.clip_grad_norm_(model.parameters(), args.clip)
|
| 194 |
+
opt.step()
|
| 195 |
+
opt.zero_grad(set_to_none=True)
|
| 196 |
+
tok_acc += b["values"].shape[0] * b["values"].shape[1] * (args.ctx // cfg.patch_size)
|
| 197 |
+
step += 1
|
| 198 |
+
lv = loss.item() if step % args.log_every == 0 or step == 1 else None
|
| 199 |
+
if lv is not None:
|
| 200 |
+
ema = lv if ema is None else 0.9 * ema + 0.1 * lv
|
| 201 |
+
if dev.type == "cuda":
|
| 202 |
+
torch.cuda.synchronize()
|
| 203 |
+
dt = time.time() - t_last
|
| 204 |
+
rec = {"step": step, "loss": lv, "loss_ema": ema, "lr": lr, "gnorm": float(gn),
|
| 205 |
+
"enc_tok_s": tok_acc / dt, "gen_frac": gen_t / dt, "elapsed_min": (time.time() - t0) / 60}
|
| 206 |
+
if step % args.val_every == 0:
|
| 207 |
+
rec["val"] = evaluate()
|
| 208 |
+
if dev.type == "cuda":
|
| 209 |
+
rec["mem_gb"] = torch.cuda.max_memory_allocated() / 1e9
|
| 210 |
+
print(json.dumps(rec), flush=True)
|
| 211 |
+
log.write(json.dumps(rec) + "\n")
|
| 212 |
+
log.flush()
|
| 213 |
+
t_last, tok_acc, gen_t = time.time(), 0, 0.0
|
| 214 |
+
if step % args.ckpt_every == 0:
|
| 215 |
+
torch.save({"model": model.state_dict(), "opt": opt.state_dict(), "step": step, "args": vars(args), "elapsed_min": (time.time() - t0) / 60}, ck)
|
| 216 |
+
export(model, os.path.join(args.out, "export"))
|
| 217 |
+
torch.save({"model": model.state_dict(), "opt": opt.state_dict(), "step": step, "args": vars(args), "elapsed_min": (time.time() - t0) / 60}, ck)
|
| 218 |
+
export(model, os.path.join(args.out, "export"))
|
| 219 |
+
fin = {"final_step": step, "val": evaluate(), "params": pc, "minutes": (time.time() - t0) / 60}
|
| 220 |
+
print("FINAL", json.dumps(fin), flush=True)
|
| 221 |
+
json.dump(fin, open(os.path.join(args.out, "final.json"), "w"))
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
if __name__ == "__main__":
|
| 225 |
+
main()
|
modular_ts/tta.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Test-time ensembling for Modular-TS (v3): context-length ensembles, temporal-hierarchy
|
| 2 |
+
(multi-resolution) reconciliation and in-context backtest weighting. Inference only.
|
| 3 |
+
|
| 4 |
+
Members (spec string, comma separated):
|
| 5 |
+
cN - the base forecaster with max_context N (e.g. c8192, c2048, c512)
|
| 6 |
+
dK - downsampled member: context mean-pooled by K (aligned to the forecast origin, so it
|
| 7 |
+
sees up to K x the history at the same token budget), forecast ceil(H/K) coarse
|
| 8 |
+
steps, step-upsampled back. Used as a *level corrector* for the fine forecasts
|
| 9 |
+
(temporal hierarchies / THieF, Athanasopoulos et al. 2017): fine + a*(coarse - pool(fine))
|
| 10 |
+
instead of being averaged in directly, so intra-period shape comes from full res.
|
| 11 |
+
Combination of the c-members: quantile averaging (Vincentisation), uniform or, with
|
| 12 |
+
backtest=True, per-series softmax weights from each member's scaled MAE on the last H
|
| 13 |
+
observed steps of the context (a zero-shot, in-context model selection - it only uses the
|
| 14 |
+
series' own history).
|
| 15 |
+
|
| 16 |
+
fc = EnsembleForecaster(TimestralForecaster.from_pretrained(exp), "c8192,c2048,d4", alpha=0.5)
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
from __future__ import annotations
|
| 20 |
+
|
| 21 |
+
import math
|
| 22 |
+
from dataclasses import dataclass
|
| 23 |
+
|
| 24 |
+
import numpy as np
|
| 25 |
+
import torch
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
@dataclass
|
| 29 |
+
class _F:
|
| 30 |
+
quantiles: torch.Tensor
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def _pool(x: np.ndarray | None, k: int) -> np.ndarray | None:
|
| 34 |
+
"""(B,V,T) NaN-aware mean pool by k aligned to the end."""
|
| 35 |
+
if x is None:
|
| 36 |
+
return None
|
| 37 |
+
B, V, T = x.shape
|
| 38 |
+
n = T // k
|
| 39 |
+
if n == 0:
|
| 40 |
+
return None
|
| 41 |
+
y = x[..., T - n * k:].reshape(B, V, n, k)
|
| 42 |
+
with np.errstate(all="ignore"):
|
| 43 |
+
cnt = np.isfinite(y).sum(-1)
|
| 44 |
+
s = np.nansum(y, -1)
|
| 45 |
+
out = np.where(cnt > 0, s / np.maximum(cnt, 1), np.nan)
|
| 46 |
+
return out.astype(np.float32)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class EnsembleForecaster:
|
| 50 |
+
def __init__(self, fc, spec: str = "c8192", alpha: float = 0.5, backtest: bool = False, tau: float = 0.5, bt_windows: int = 1, prior: float = 0.0,
|
| 51 |
+
min_coarse_ctx: int = 256, min_extra: float = 1.5, min_ratio: float = 8.0):
|
| 52 |
+
self.fc = fc
|
| 53 |
+
self.members = [m.strip() for m in spec.split(",") if m.strip()]
|
| 54 |
+
cl = fc.model.config.context_length # never exceed the trained context (RoPE does not extrapolate)
|
| 55 |
+
self.ctxs = [min(int(m[1:]), cl) for m in self.members if m[0] == "c"] or [fc.max_context]
|
| 56 |
+
self.ks = [int(m[1:]) for m in self.members if m[0] == "d"]
|
| 57 |
+
self.alpha, self.backtest, self.tau = alpha, backtest, tau
|
| 58 |
+
self.bt_windows, self.prior = bt_windows, prior # prior: logit bonus for the first (longest) member
|
| 59 |
+
self.min_coarse_ctx, self.min_extra, self.min_ratio = min_coarse_ctx, min_extra, min_ratio # member ctx >= min_ratio*H
|
| 60 |
+
self.base_ctx = fc.max_context
|
| 61 |
+
self.max_context = max(self.ctxs + [self.base_ctx * k for k in self.ks] + [self.base_ctx])
|
| 62 |
+
self.quantile_levels = fc.quantile_levels
|
| 63 |
+
self.mi = list(fc.quantile_levels).index(0.5) if 0.5 in list(fc.quantile_levels) else len(fc.quantile_levels) // 2
|
| 64 |
+
|
| 65 |
+
def _one(self, ctx, h, past, mc):
|
| 66 |
+
self.fc.max_context = mc
|
| 67 |
+
try:
|
| 68 |
+
return self.fc.forecast(ctx, h, past_covariates=past).quantiles.float().cpu()
|
| 69 |
+
finally:
|
| 70 |
+
self.fc.max_context = self.base_ctx
|
| 71 |
+
|
| 72 |
+
def _members(self, ctx, h, past):
|
| 73 |
+
T = ctx.shape[-1]
|
| 74 |
+
outs, used = [], set()
|
| 75 |
+
for i, c in enumerate(self.ctxs):
|
| 76 |
+
if i > 0 and c < self.min_ratio * h: # a short context cannot support a long horizon
|
| 77 |
+
continue
|
| 78 |
+
eff = min(c, T)
|
| 79 |
+
if eff in used: # a shorter series makes several members identical
|
| 80 |
+
continue
|
| 81 |
+
used.add(eff)
|
| 82 |
+
outs.append(self._one(ctx[..., -c:], h, None if past is None else past[..., -c:], c))
|
| 83 |
+
return outs
|
| 84 |
+
|
| 85 |
+
def forecast(self, context, prediction_length, past_covariates=None):
|
| 86 |
+
ctx = np.asarray(context, np.float32)
|
| 87 |
+
if ctx.ndim == 2:
|
| 88 |
+
ctx = ctx[:, None]
|
| 89 |
+
past = None if past_covariates is None else np.asarray(past_covariates, np.float32)
|
| 90 |
+
h = prediction_length
|
| 91 |
+
T = ctx.shape[-1]
|
| 92 |
+
outs = self._members(ctx, h, past)
|
| 93 |
+
if len(outs) == 1:
|
| 94 |
+
q = outs[0]
|
| 95 |
+
elif self.backtest and T - h >= 2 * 32:
|
| 96 |
+
E, ok = 0.0, True
|
| 97 |
+
nw = 0
|
| 98 |
+
for wdw in range(1, self.bt_windows + 1):
|
| 99 |
+
if T - wdw * h < 2 * 32:
|
| 100 |
+
break
|
| 101 |
+
cut = T - wdw * h
|
| 102 |
+
bt = self._members(ctx[..., :cut], h, None if past is None else past[..., :cut])
|
| 103 |
+
if len(bt) != len(outs):
|
| 104 |
+
ok = False
|
| 105 |
+
break
|
| 106 |
+
y = torch.from_numpy(ctx[..., cut:cut + h])
|
| 107 |
+
hist = torch.from_numpy(ctx[..., :cut])
|
| 108 |
+
dd = hist[..., 1:] - hist[..., :-1]
|
| 109 |
+
fin = torch.isfinite(dd)
|
| 110 |
+
scale = (torch.where(fin, dd.abs(), torch.zeros_like(dd)).sum(-1) / fin.sum(-1).clamp_min(1)).clamp_min(1e-8)
|
| 111 |
+
errs = []
|
| 112 |
+
for b in bt:
|
| 113 |
+
e = (b[..., self.mi] - y).abs()
|
| 114 |
+
fe = torch.isfinite(e)
|
| 115 |
+
e = torch.where(fe, e, torch.zeros_like(e)).sum(-1) / fe.sum(-1).clamp_min(1) / scale
|
| 116 |
+
errs.append(e.mean(-1))
|
| 117 |
+
E = E + torch.stack(errs, 0)
|
| 118 |
+
nw += 1
|
| 119 |
+
if not ok or nw == 0:
|
| 120 |
+
q = torch.stack(outs).mean(0)
|
| 121 |
+
else:
|
| 122 |
+
E = E / nw
|
| 123 |
+
logit = -E / (self.tau * E.min(0, keepdim=True).values.clamp_min(1e-3))
|
| 124 |
+
logit[0] = logit[0] + self.prior
|
| 125 |
+
w = torch.softmax(logit, 0)
|
| 126 |
+
q = (torch.stack(outs) * w[:, :, None, None, None]).sum(0)
|
| 127 |
+
else:
|
| 128 |
+
q = torch.stack(outs).mean(0)
|
| 129 |
+
# temporal-hierarchy level correction from downsampled members
|
| 130 |
+
for k in self.ks:
|
| 131 |
+
hk = math.ceil(h / k)
|
| 132 |
+
pooled = _pool(ctx, k)
|
| 133 |
+
if pooled is None or pooled.shape[-1] < self.min_coarse_ctx:
|
| 134 |
+
continue
|
| 135 |
+
# only worth it when the coarse view sees materially more history than the fine one
|
| 136 |
+
if T < self.min_extra * self.base_ctx and hk < 2:
|
| 137 |
+
continue
|
| 138 |
+
pp = _pool(past, k) if past is not None else None
|
| 139 |
+
cq = self._one(pooled[..., -self.base_ctx:], hk, None if pp is None else pp[..., -self.base_ctx:], self.base_ctx)
|
| 140 |
+
cmed = cq[..., self.mi] # (B,V,hk)
|
| 141 |
+
fmed = q[..., self.mi]
|
| 142 |
+
pad = hk * k - h
|
| 143 |
+
fm = torch.cat([fmed, fmed[..., -1:].expand(*fmed.shape[:-1], pad)], -1) if pad else fmed
|
| 144 |
+
fpool = fm.view(*fm.shape[:-1], hk, k).mean(-1)
|
| 145 |
+
delta = (cmed - fpool).repeat_interleave(k, -1)[..., :h]
|
| 146 |
+
q = q + self.alpha / len(self.ks) * delta[..., None]
|
| 147 |
+
return _F(q)
|
results/all_results.csv
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
dataset,model,eval_metrics/MSE[mean],eval_metrics/MSE[0.5],eval_metrics/MAE[0.5],eval_metrics/MASE[0.5],eval_metrics/MAPE[0.5],eval_metrics/sMAPE[0.5],eval_metrics/MSIS,eval_metrics/RMSE[mean],eval_metrics/NRMSE[mean],eval_metrics/ND[0.5],eval_metrics/mean_weighted_sum_quantile_loss,domain,num_variates
|
| 2 |
+
bitbrains_fast_storage/5T/long,Modular-TS-2.4M-v3_M1,3678060.0939310347,3678060.0939310347,375.4961381185165,0.9167681594477102,4.714564867919659,0.7251918487783172,15.841790583442892,1917.826919701315,5.068243351659421,0.9923240653485829,0.6959523690328288,Web/CloudOps,2
|
| 3 |
+
bitbrains_fast_storage/5T/medium,Modular-TS-2.4M-v3_M1,2794097.8588460106,2794097.8588460106,301.5700941306518,1.248188037721358,4.705945084704349,0.7298914125066623,23.65806409550792,1671.5555207189532,5.078647577538354,0.9162532796732381,0.674956798323308,Web/CloudOps,2
|
| 4 |
+
bitbrains_fast_storage/5T/short,Modular-TS-2.4M-v3_M1,1789367.908427151,1789367.908427151,170.94836712944672,0.7464365081982969,2.2107092294123354,0.6847539590206893,14.157463028441592,1337.6725714565396,4.199961010964384,0.5367355899732063,0.4188247283434475,Web/CloudOps,2
|
| 5 |
+
bitbrains_fast_storage/H/short,Modular-TS-2.4M-v3_M1,2857251.909621366,2857251.909621366,324.96432151762076,1.0518938446957118,3.765954716264035,0.5135839053460005,19.937172636928338,1690.340767307399,4.818020494788251,0.9262539195814864,0.6779741560340291,Web/CloudOps,2
|
| 6 |
+
bitbrains_rnd/5T/long,Modular-TS-2.4M-v3_M1,2335002.1897918363,2335002.1897918363,202.03773498891655,3.340738878079062,2.732445501293894,0.6418363842517235,116.04497928950197,1528.0713955152214,5.853697246156131,0.7739610442256082,0.6664847615792028,Web/CloudOps,2
|
| 7 |
+
bitbrains_rnd/5T/medium,Modular-TS-2.4M-v3_M1,2197458.6406868207,2197458.6406868207,166.13444570183873,4.413797604303608,1.2794170885289742,0.6578795845955763,160.08923169435977,1482.3827578216162,6.237140002367876,0.699012310815642,0.6454088859518597,Web/CloudOps,2
|
| 8 |
+
bitbrains_rnd/5T/short,Modular-TS-2.4M-v3_M1,1717739.5983574833,1717739.5983574833,129.86860276201207,1.7188269009296444,1.114256719613909,0.6338544360460594,53.535910841778204,1310.6256514953013,5.362701787992878,0.5313848294067788,0.4247043445566765,Web/CloudOps,2
|
| 9 |
+
bitbrains_rnd/H/short,Modular-TS-2.4M-v3_M1,1760008.1728826563,1760008.1728826563,156.42778973615788,5.9169358495513436,1.6377402633165954,0.5359674461057023,204.34510912470066,1326.6529964096326,5.806082855626521,0.684604573003369,0.6252948443035078,Web/CloudOps,2
|
| 10 |
+
bizitobs_application/10S/long,Modular-TS-2.4M-v3_M1,5275426.702222222,5275426.702222222,1418.387638888889,3.0462642085000406,0.04374827490912543,0.044643965827094186,34.158310869955976,2296.8297068398915,0.08857954110209905,0.054701541774522924,0.04655703725886792,Web/CloudOps,2
|
| 11 |
+
bizitobs_application/10S/medium,Modular-TS-2.4M-v3_M1,3028799.36,3028799.36,970.73,2.2233368341100945,0.031767988204956056,0.03231649398803711,19.42404117346401,1740.344609552947,0.06833821067690701,0.0381177100709006,0.03148191823098797,Web/CloudOps,2
|
| 12 |
+
bizitobs_application/10S/short,Modular-TS-2.4M-v3_M1,629297.9911111111,629297.9911111111,429.53465277777775,1.232171827879832,0.017635783089531794,0.01781905280219184,8.829611381439575,793.2830460252577,0.03059375624223889,0.016565434658564034,0.012766240040952294,Web/CloudOps,2
|
| 13 |
+
bizitobs_l2c/5T/long,Modular-TS-2.4M-v3_M1,92.45116071428572,92.45116071428572,6.359086061507936,0.6284250899642787,0.5096323568723897,0.7791392328354297,3.653899407257271,9.615152662037442,0.5398764472943504,0.35705317550293086,0.2669678027002204,Web/CloudOps,7
|
| 14 |
+
bizitobs_l2c/5T/medium,Modular-TS-2.4M-v3_M1,80.05903486394558,80.05903486394558,5.660879703443878,0.5515346011572464,0.38841309866420504,0.6572395147049294,3.432537186556386,8.947571450619748,0.47077383803909884,0.297845519242355,0.228644277283759,Web/CloudOps,7
|
| 15 |
+
bizitobs_l2c/5T/short,Modular-TS-2.4M-v3_M1,19.891878255208333,19.891878255208333,2.5970302036830355,0.26936388067215716,0.14014765634067433,0.16574777941924232,2.293561869201875,4.460031194420991,0.15317552406028445,0.08919252917942418,0.07143140322408012,Web/CloudOps,7
|
| 16 |
+
bizitobs_l2c/H/long,Modular-TS-2.4M-v3_M1,80.8273685515873,80.8273685515873,5.249859716021826,0.5630390129701736,0.6261864244101014,0.7163555186958651,5.3501780426093,8.990404248507812,0.5491598394034193,0.32067658348283234,0.26036159148559485,Web/CloudOps,7
|
| 17 |
+
bizitobs_l2c/H/medium,Modular-TS-2.4M-v3_M1,75.64733072916667,75.64733072916667,5.013981701078869,0.5068527865795754,0.5128123874838597,0.6639396817558852,3.3392014091576043,8.697547397350972,0.5266497057254618,0.3036042078011892,0.23915307887691264,Web/CloudOps,7
|
| 18 |
+
bizitobs_l2c/H/short,Modular-TS-2.4M-v3_M1,57.78172278025794,57.78172278025794,4.74778578016493,0.47055571124351386,0.441387441804369,0.5357121708376895,2.600709930494657,7.601428995936089,0.40973653327113746,0.255917839569219,0.19489947674121633,Web/CloudOps,7
|
| 19 |
+
bizitobs_service/10S/long,Modular-TS-2.4M-v3_M1,130062.9671957672,130062.9671957672,75.6047619047619,1.3220944818431355,0.08974954917948082,0.09647402162905093,17.90537812216811,360.64243676495863,0.2671754466043876,0.0560104246425329,0.051381319986138645,Web/CloudOps,2
|
| 20 |
+
bizitobs_service/10S/medium,Modular-TS-2.4M-v3_M1,37549.83365079365,37549.83365079365,44.102624007936505,1.0659872190913706,0.07613135443793403,0.07608071947854662,8.761180060093817,193.77779452453692,0.14584171713131502,0.03319267014621623,0.02467642624887996,Web/CloudOps,2
|
| 21 |
+
bizitobs_service/10S/short,Modular-TS-2.4M-v3_M1,3464.6414814814816,3464.6414814814816,21.6134126984127,0.8154586392523805,0.07128501932457011,0.05466276300016534,6.416263373524299,58.861205233001144,0.04360626258204015,0.01601190709042046,0.012609605005872552,Web/CloudOps,2
|
| 22 |
+
car_parts/M/short,Modular-TS-2.4M-v3_M1,1.305600452790527,1.305600452790527,0.5138089770543491,0.9264228697044318,0.7229986347697147,1.815765007228728,13.205239812801091,1.1426287467023255,2.7399065232330053,1.232061220225577,1.0199670264999154,Sales,1
|
| 23 |
+
covid_deaths/D/short,Modular-TS-2.4M-v3_M1,390569.1909774436,390569.1909774436,124.35499686716793,36.53481413039805,0.10018785763625955,0.1363243285045092,572.0192411858611,624.9553511871417,0.2351424922666607,0.04678917275228266,0.03960264190873637,Healthcare,1
|
| 24 |
+
electricity/15T/long,Modular-TS-2.4M-v3_M1,387027.38936486485,387027.38936486485,61.74539771021021,0.8999086727862419,0.13413104448677174,0.12631141205794552,7.4110272826156764,622.1152540846952,0.9814631068825673,0.0974109370803379,0.07878154973566881,Energy,1
|
| 25 |
+
electricity/15T/medium,Modular-TS-2.4M-v3_M1,324140.55405292794,324140.55405292794,57.261868524774776,0.8602740891433444,0.12796559968104762,0.1250541946200342,7.40904239422722,569.3334295937029,0.9810188500772348,0.0986679676504098,0.08014002920089768,Energy,1
|
| 26 |
+
electricity/15T/short,Modular-TS-2.4M-v3_M1,159011.70030686937,159011.70030686937,58.58624731665259,1.0991763686633387,0.17856490707210804,0.16560820949532176,12.54439727462825,398.76271178091537,0.7860399099717378,0.11548504212115139,0.09610354583362528,Energy,1
|
| 27 |
+
electricity/D/short,Modular-TS-2.4M-v3_M1,1324453454.931027,1324453454.931027,4420.498882882883,1.4642161417491881,0.5259458230883358,0.0996492496888516,11.876414361582455,36393.04129817989,0.5980585584955973,0.07264348060574302,0.05708177686933791,Energy,1
|
| 28 |
+
electricity/H/long,Modular-TS-2.4M-v3_M1,7372469.746162162,7372469.746162162,268.9585945945946,1.1839789805436962,0.294873560923189,0.13517404831015878,9.636234992963349,2715.229225343997,1.0708856186022901,0.10607719166485649,0.0846881171621528,Energy,1
|
| 29 |
+
electricity/H/medium,Modular-TS-2.4M-v3_M1,5435094.20972973,5435094.20972973,246.52309403153154,1.1023261199038497,0.24864662046620367,0.1266396394450642,8.89635907075197,2331.328850619262,0.9099944376457132,0.09622608336027569,0.07738372993314419,Energy,1
|
| 30 |
+
electricity/H/short,Modular-TS-2.4M-v3_M1,1876482.3957207208,1876482.3957207208,198.68760152730857,1.048592430136885,0.2125099229835893,0.12904541010787124,10.67673108397427,1369.847581200449,0.6466658874640627,0.09379473740952059,0.07583874279304725,Energy,1
|
| 31 |
+
electricity/W/short,Modular-TS-2.4M-v3_M1,63847587411.94955,63847587411.94955,35648.821621621624,1.8002616835096779,0.26463992427529304,0.107129864744331,12.682172214599442,252680.80143127128,0.5765342039285258,0.08133884679086692,0.06238429748467943,Energy,1
|
| 32 |
+
ett1/15T/long,Modular-TS-2.4M-v3_M1,8.64753410218254,8.64753410218254,1.6084292844742063,1.0242327428538225,0.7725774866299528,0.3994036380828373,7.362441077699103,2.940668988883744,0.560494416846029,0.3065682119433832,0.23851112316934234,Energy,7
|
| 33 |
+
ett1/15T/medium,Modular-TS-2.4M-v3_M1,8.946388268849207,8.946388268849207,1.612916976686508,1.0142896203616822,0.7179076932443985,0.4008093067956349,6.9522545201894985,2.99105136513053,0.5700972801996723,0.3074235341848529,0.23753058892474854,Energy,7
|
| 34 |
+
ett1/15T/short,Modular-TS-2.4M-v3_M1,4.493948800223214,4.493948800223214,1.0467429024832589,0.6744832518361066,0.4831348848598496,0.23317115420386905,4.85612060712067,2.1198935822873786,0.40074808391760935,0.19787795766233757,0.15605220939196554,Energy,7
|
| 35 |
+
ett1/D/short,Modular-TS-2.4M-v3_M1,41473.67619047619,41473.67619047619,136.99885912698412,1.6938340222853872,1.5330467587425596,0.47015250069754466,11.039966279309825,203.65086837643534,0.5348957609590255,0.3598320477956078,0.27754000750616026,Energy,7
|
| 36 |
+
ett1/H/long,Modular-TS-2.4M-v3_M1,190.66746031746032,190.66746031746032,8.587733548280424,1.5527086794374731,5316620674772.501,0.5386861488301918,8.817916161074745,13.808238856474794,0.6612466522675325,0.41124795988760454,0.316052518207568,Energy,7
|
| 37 |
+
ett1/H/medium,Modular-TS-2.4M-v3_M1,162.93642113095237,162.93642113095237,7.93248523530506,1.426481905997391,4927664332637.245,0.4986232212611607,8.261870012206636,12.764655151274255,0.6113967232377264,0.37994723888118587,0.2912179401657243,Energy,7
|
| 38 |
+
ett1/H/short,Modular-TS-2.4M-v3_M1,101.40430617559524,101.40430617559524,5.343412853422619,0.8934073410569346,0.5035806565057663,0.2850081307547433,5.960154567403204,10.06997051513038,0.4700067218968723,0.24939893867666016,0.1950639093326618,Energy,7
|
| 39 |
+
ett1/W/short,Modular-TS-2.4M-v3_M1,1106313.857142857,1106313.857142857,817.4416155133929,1.4995751755326385,0.6967250960213798,0.5054152693067279,9.828438170708507,1051.8145545403224,0.41854211569731004,0.3252795293040285,0.25851552338781847,Energy,7
|
| 40 |
+
ett2/15T/long,Modular-TS-2.4M-v3_M1,12.21656746031746,12.21656746031746,2.289798332093254,0.9465275728595461,0.14278422674529917,0.17554123651413692,7.384640025056532,3.4952206597463142,0.18626662345647246,0.1220274898885009,0.09823390820536648,Energy,7
|
| 41 |
+
ett2/15T/medium,Modular-TS-2.4M-v3_M1,11.67773933531746,11.67773933531746,2.1916136532738095,0.9034327604282267,0.1354037787471785,0.166792728484623,6.875634428994166,3.417270743637012,0.1821125316019545,0.11679505097284643,0.09285252044934728,Energy,7
|
| 42 |
+
ett2/15T/short,Modular-TS-2.4M-v3_M1,7.374314081101191,7.374314081101191,1.7099256243024554,0.7340474423392392,0.10527994779395827,0.12591985974993025,5.222691349888079,2.7155688319578997,0.1289177129693672,0.08117625237061474,0.06418560197946685,Energy,7
|
| 43 |
+
ett2/D/short,Modular-TS-2.4M-v3_M1,105054.83174603175,105054.83174603175,196.08004712301587,1.2984840916817926,0.4009605286613343,0.13548792278955854,12.586524599283038,324.12163109862286,0.18550334007335306,0.11222177162249401,0.09302339248607955,Energy,7
|
| 44 |
+
ett2/H/long,Modular-TS-2.4M-v3_M1,250.87258597883599,250.87258597883599,9.74873511904762,1.0779024421450007,0.1888666196931598,0.17896481468563988,7.821690076035867,15.83895785646379,0.21690233728199415,0.13350142427467618,0.10684645595914224,Energy,7
|
| 45 |
+
ett2/H/medium,Modular-TS-2.4M-v3_M1,232.85926339285714,232.85926339285714,9.49784923735119,1.0546525783918868,0.184112399827706,0.17202008565266927,7.232294896354205,15.259726845289766,0.20563461951715362,0.12798961829102862,0.10264518121230139,Energy,7
|
| 46 |
+
ett2/H/short,Modular-TS-2.4M-v3_M1,110.36870349702382,110.36870349702382,6.621943591889881,0.7615127738607653,0.12385460601547361,0.1129866463797433,5.21876475954453,10.505651026805708,0.13324088597847747,0.08398466966317847,0.06666944395920799,Energy,7
|
| 47 |
+
ett2/W/short,Modular-TS-2.4M-v3_M1,3541620.5714285714,3541620.5714285714,1345.2218191964287,0.9547622085461569,0.1449977159500122,0.15665956905909947,11.340939022920598,1881.919384944151,0.15786415525532294,0.11284346599404156,0.09697388059895506,Energy,7
|
| 48 |
+
hierarchical_sales/D/short,Modular-TS-2.4M-v3_M1,28.936732637453492,28.936732637453492,2.3071339525630425,0.7453639144930956,0.6165533933772638,1.0357622986776354,6.798132751789339,5.379287372640868,1.6503057939121941,0.7078031466790955,0.5757825059383115,Sales,1
|
| 49 |
+
hierarchical_sales/W/short,Modular-TS-2.4M-v3_M1,477.0483315677966,477.0483315677966,9.167869438559322,0.7432259541889759,0.5635383778173443,0.4685462046477754,6.674547896652912,21.84143611504968,1.002751015482967,0.42090137026274516,0.35890123597072937,Sales,1
|
| 50 |
+
hospital/M/short,Modular-TS-2.4M-v3_M1,3125.2115384615386,3125.2115384615386,18.713556945349847,0.767503522036095,0.192932274796454,0.1733501290093811,5.795029146932404,55.90359146299581,0.20294301861673855,0.06793455726470443,0.0545404261615041,Healthcare,1
|
| 51 |
+
jena_weather/10T/long,Modular-TS-2.4M-v3_M1,1443.2055555555555,1443.2055555555555,10.116731770833333,0.7593568382098755,1.100752820766555,0.49222818745996527,5.903263138358273,37.98954534547045,0.23246454209202339,0.06190601643657213,0.0515347532084138,Nature,21
|
| 52 |
+
jena_weather/10T/medium,Modular-TS-2.4M-v3_M1,1446.1549783549783,1446.1549783549783,9.601532061688312,0.6357975527659445,0.9295913873316204,0.4859170127441035,5.517174917876551,38.02834440723101,0.23337927368603792,0.058924431598551034,0.04895998333926589,Nature,21
|
| 53 |
+
jena_weather/10T/short,Modular-TS-2.4M-v3_M1,752.6180555555555,752.6180555555555,5.232061089409722,0.27035257312197475,0.3470003990971884,0.42719930557880287,2.2899825126672466,27.433885170634426,0.17039882350965418,0.03249765932243417,0.027317755948203546,Nature,21
|
| 54 |
+
jena_weather/D/short,Modular-TS-2.4M-v3_M1,358.2728670634921,358.2728670634921,9.786906312003968,1.1468402426850592,0.7556325844701777,0.4527134486607143,7.451680589994401,18.92809729115666,0.11397808223330899,0.05893317195492466,0.0474915151842114,Nature,21
|
| 55 |
+
jena_weather/H/long,Modular-TS-2.4M-v3_M1,1242.4757936507935,1242.4757936507935,10.863038401124339,0.8130128803640841,2.96361049556809,0.5416168209643959,6.04634515756844,35.24877010125025,0.2122366701561103,0.06540753312555465,0.054081654231405804,Nature,21
|
| 56 |
+
jena_weather/H/medium,Modular-TS-2.4M-v3_M1,1334.2667658730159,1334.2667658730159,10.666871279761905,0.7757337503072408,3.033535544209986,0.6149863686895949,5.766084326924138,36.52761648223185,0.22351894657938667,0.06527247221044566,0.05414278224434971,Nature,21
|
| 57 |
+
jena_weather/H/short,Modular-TS-2.4M-v3_M1,1118.265873015873,1118.265873015873,8.77562395572264,0.5575230640846863,2.1529343567204986,0.5692911840960002,4.7894401276209715,33.440482547593014,0.2049880216277734,0.053794014206363795,0.04447337600489246,Nature,21
|
| 58 |
+
kdd_cup_2018/D/short,Modular-TS-2.4M-v3_M1,2937.0604625889355,2937.0604625889355,21.22017127214035,1.1948516372972833,0.511777291233548,0.46432526976086136,9.314079710765023,54.19465345021532,1.2140178020228125,0.47535437623965665,0.3776508562461224,Nature,1
|
| 59 |
+
kdd_cup_2018/H/long,Modular-TS-2.4M-v3_M1,4117.359196395615,4117.359196395615,23.66399017941364,1.003633075334812,1.0376693110834994,0.6020821082508269,7.688290315191491,64.16665174680392,1.5056089083022801,0.5552528213672531,0.44193315910112674,Nature,1
|
| 60 |
+
kdd_cup_2018/H/medium,Modular-TS-2.4M-v3_M1,5299.258908786141,5299.258908786141,25.391270108120917,1.0468693908626807,0.9595902537350729,0.5642321082769655,8.73280114304173,72.79600887951304,1.5237522924414542,0.531485264518917,0.42880623103540333,Nature,1
|
| 61 |
+
kdd_cup_2018/H/short,Modular-TS-2.4M-v3_M1,4584.175843487953,4584.175843487953,22.749892904784264,0.95232434344199,0.890807433928544,0.5088257350694734,7.662817227431168,67.70654210257642,1.4172205403315912,0.4761964577390122,0.3829445880060442,Nature,1
|
| 62 |
+
loop_seattle/5T/long,Modular-TS-2.4M-v3_M1,77.26814915147345,77.26814915147345,5.110872122606352,0.80926892145504,0.1596513534141727,0.11667130228713378,5.974691681224069,8.790230324142449,0.15541307062057544,0.0903612648171242,0.07131325128347951,Transport,1
|
| 63 |
+
loop_seattle/5T/medium,Modular-TS-2.4M-v3_M1,65.26931017801857,65.26931017801857,4.754036780830753,0.7570855931174018,0.15041783068079206,0.10974240849261682,5.513218410195204,8.078942392294834,0.1437409880385142,0.08458408426080426,0.06666107906952709,Transport,1
|
| 64 |
+
loop_seattle/5T/short,Modular-TS-2.4M-v3_M1,42.28828225780444,42.28828225780444,3.693087041045859,0.584577183016778,0.09555870071899288,0.07878280734130104,4.261568654004953,6.502944122303715,0.11151113514682257,0.06332828952513796,0.04999500278718841,Transport,1
|
| 65 |
+
loop_seattle/D/short,Modular-TS-2.4M-v3_M1,19.247128160474716,19.247128160474716,3.069865941853715,0.9222919478266817,0.05698735145592468,0.056664226520172216,6.831595807621177,4.387154905001044,0.07840432343320114,0.05486260855008722,0.04409556422971058,Transport,1
|
| 66 |
+
loop_seattle/H/long,Modular-TS-2.4M-v3_M1,61.99619453044376,61.99619453044376,4.348210206505848,0.9057515030505758,0.1377619010786195,0.10102539482418516,7.543673818871382,7.8737662227452345,0.1407147465779697,0.07770833930892147,0.062404691134790226,Transport,1
|
| 67 |
+
loop_seattle/H/medium,Modular-TS-2.4M-v3_M1,65.77274735552116,65.77274735552116,4.492230331688597,0.921160909128047,0.12503000895182292,0.10257727246043359,7.348116805943949,8.11003991084638,0.14334404367348244,0.07939966608496138,0.06444749087666797,Transport,1
|
| 68 |
+
loop_seattle/H/short,Modular-TS-2.4M-v3_M1,61.149788382285045,61.149788382285045,4.268878590541999,0.8667326508127984,0.12220229219881393,0.09891576271249822,7.2416823477751615,7.81983301498728,0.13841899749822176,0.07556349270015796,0.061376456341542145,Transport,1
|
| 69 |
+
m4_daily/D/short,Modular-TS-2.4M-v3_M1,392397.3660481936,392397.3660481936,181.16333851895976,3.3476633916432093,0.03983545669163223,0.03301116227579889,26.376863236475945,626.4162881408765,0.09676400471083228,0.02798472912303381,0.02220331743107636,Econ/Fin,1
|
| 70 |
+
m4_hourly/H/short,Modular-TS-2.4M-v3_M1,1324520.2962962964,1324520.2962962964,237.69789150563608,1.081845233732063,0.11952784441519475,0.10734349249257749,8.872149782593194,1150.8780544854856,0.15712025744314306,0.032451008828867055,0.02570204011312775,Econ/Fin,1
|
| 71 |
+
m4_monthly/M/short,Modular-TS-2.4M-v3_M1,1934938.9624814815,1934938.9624814815,585.6214935619213,1.0030714222778911,0.16685686432873761,0.1375792439955252,8.349537856486776,1391.0208346683673,0.2891018823308095,0.12171225038659843,0.09736162749611771,Econ/Fin,1
|
| 72 |
+
m4_quarterly/Q/short,Modular-TS-2.4M-v3_M1,1874165.072,1874165.072,603.2542565104167,1.316495197534114,0.12405512468020122,0.10885652160644531,9.885131678659691,1369.001487216139,0.22914674122762205,0.10097413940152244,0.08072033966550607,Econ/Fin,1
|
| 73 |
+
m4_weekly/W/short,Modular-TS-2.4M-v3_M1,335642.3844011142,335642.3844011142,290.3625991000643,2.4103909647970867,0.07060589247810424,0.07127483360563532,19.266476231809886,579.346514964157,0.10554795669707111,0.05289956570837176,0.04221483984337579,Econ/Fin,1
|
| 74 |
+
m4_yearly/A/short,Modular-TS-2.4M-v3_M1,3688188.9997388353,3688188.9997388353,936.5003173877717,3.5548652369615543,0.17382724042770525,0.1558134202350262,29.47083697666009,1920.465828839148,0.3079555980318765,0.15017216706871636,0.12110404592475733,Econ/Fin,1
|
| 75 |
+
m_dense/D/short,Modular-TS-2.4M-v3_M1,10159.522962962963,10159.522962962963,53.3965625,0.7801702443914319,0.13779236247046422,0.1134519788953993,7.483199837382785,100.79445898938573,0.17449011796416458,0.09243734807373728,0.07591254278413982,Transport,1
|
| 76 |
+
m_dense/H/long,Modular-TS-2.4M-v3_M1,36196.61432098765,36196.61432098765,87.6086574074074,0.7551546647160053,0.3013599794595034,0.21297898553485448,7.125273549664825,190.2540783294478,0.3293579342551789,0.1516635368867529,0.1240570907774525,Transport,1
|
| 77 |
+
m_dense/H/medium,Modular-TS-2.4M-v3_M1,37816.72888888889,37816.72888888889,88.91314236111111,0.7605924991518306,0.2978922554395164,0.21514732452903898,7.291474351876949,194.46523825323868,0.33834494648820845,0.15469763472633177,0.1258787633837597,Transport,1
|
| 78 |
+
m_dense/H/short,Modular-TS-2.4M-v3_M1,48915.275555555556,48915.275555555556,100.25403645833333,0.8735990399490393,0.3199904790093199,0.23692531760679472,8.501058392911478,221.16798040303112,0.39229688570665994,0.17782567897241194,0.14547168121836587,Transport,1
|
| 79 |
+
restaurant/D/short,Modular-TS-2.4M-v3_M1,150.43012748669096,150.43012748669096,7.4691381806294945,0.7131750289869679,0.7115707333330414,0.4073099264791725,4.815310569254752,12.264996024731968,0.5644635347684277,0.3437470448918538,0.26815136235708237,Sales,1
|
| 80 |
+
saugeen/D/short,Modular-TS-2.4M-v3_M1,1255.1364583333334,1255.1364583333334,13.298326009114584,2.9542435988005167,0.29903676350911457,0.33566294352213544,25.569303454472077,35.42790507965908,1.147518286774784,0.43073594796643183,0.3473857615991493,Nature,1
|
| 81 |
+
saugeen/M/short,Modular-TS-2.4M-v3_M1,398.76102120535717,398.76102120535717,12.247690836588541,0.7174716361510609,0.342522212437221,0.35552256447928293,4.534491556471845,19.969001507470452,0.5995293316977024,0.3677124216427659,0.28603135309760525,Nature,1
|
| 82 |
+
saugeen/W/short,Modular-TS-2.4M-v3_M1,1000.85078125,1000.85078125,15.224264526367188,1.2349741504575213,0.3950361728668213,0.4176785469055176,11.806592395561532,31.636225774418794,0.9549657813238388,0.45955708408994433,0.37746652189469726,Nature,1
|
| 83 |
+
solar/10T/long,Modular-TS-2.4M-v3_M1,23.032447219687754,23.032447219687754,1.9713294753966444,0.8621459578505782,3.446850755508605,1.3579222325089273,4.592986812658656,4.799213187563953,1.0372731537285507,0.4260713292089982,0.3338359879421979,Energy,1
|
| 84 |
+
solar/10T/medium,Modular-TS-2.4M-v3_M1,23.399660611590356,23.399660611590356,1.9647820587259457,0.8598613118615777,3.9067165151966625,1.3391397518249897,4.916194768083654,4.837319568892504,1.0734204229639002,0.43599294164316454,0.3459019924979136,Energy,1
|
| 85 |
+
solar/10T/short,Modular-TS-2.4M-v3_M1,33.723401740229626,33.723401740229626,2.723277234451034,1.1968402326041616,6.204456295299063,1.5001876465957038,5.100924566936104,5.8071853543889596,1.6893095907126325,0.7922010525891977,0.6068997591975502,Energy,1
|
| 86 |
+
solar/D/short,Modular-TS-2.4M-v3_M1,135535.72749391728,135535.72749391728,250.01791362530415,0.9752235465681917,1.1072755094282238,0.4214751380493461,6.18232538588262,368.15177236286297,0.5318539169598063,0.3611907279932196,0.2825565340308264,Energy,1
|
| 87 |
+
solar/H/long,Modular-TS-2.4M-v3_M1,778.3379562043796,778.3379562043796,13.486013534063261,1.0291823076641544,5.149863372380082,1.276069171517705,5.4384003716373455,27.898708862676415,0.9672990802210861,0.4675846667872307,0.3518338960682997,Energy,1
|
| 88 |
+
solar/H/medium,Modular-TS-2.4M-v3_M1,736.2314476885645,736.2314476885645,12.329110591545012,0.9441481311907173,4.591132831562747,1.2722219049250862,5.789483995718961,27.13358523469695,0.9774150702516566,0.44412333979240187,0.3478506380884181,Energy,1
|
| 89 |
+
solar/H/short,Modular-TS-2.4M-v3_M1,715.8867962847355,715.8867962847355,11.702336005905646,0.9044365748556973,4.6407299556635095,1.1701262443745015,6.501452797930489,26.756060926166533,0.9862373044853525,0.4313519972351118,0.3508833002839567,Energy,1
|
| 90 |
+
solar/W/short,Modular-TS-2.4M-v3_M1,2389976.7591240876,2389976.7591240876,1241.9268932481752,1.3624889021851072,0.2861955183265853,0.2382669936131387,8.87072004666349,1545.954966719305,0.3155680594890949,0.2535083286166676,0.18192297054488138,Energy,1
|
| 91 |
+
sz_taxi/15T/long,Modular-TS-2.4M-v3_M1,15.928574608262108,15.928574608262108,2.7806888243411683,0.5244470608982269,6650318795664.556,0.41094389912749285,4.537647377087957,3.9910618396940567,0.3694055745349831,0.25737560429218964,0.20908997649672162,Transport,1
|
| 92 |
+
sz_taxi/15T/medium,Modular-TS-2.4M-v3_M1,16.384969284188035,16.384969284188035,2.7931724091880343,0.5450982003341567,9861828586658.79,0.4125929351545807,4.257375272013037,4.047835135499967,0.37671862381263804,0.25995131492201445,0.20716505142307973,Transport,1
|
| 93 |
+
sz_taxi/15T/short,Modular-TS-2.4M-v3_M1,16.7533195970696,16.7533195970696,2.753968011765253,0.5500745426844621,1067602223315.3171,0.4005391792791085,3.952657790711455,4.093081919174059,0.3827206648290862,0.2575077873333941,0.20260036043551058,Transport,1
|
| 94 |
+
sz_taxi/H/short,Modular-TS-2.4M-v3_M1,7.632657042935363,7.632657042935363,1.927211402827858,0.5869074094720954,1.2931420741937099,0.305347410022703,4.3569741325911115,2.7627263785860814,0.2573650106397508,0.1795316347787159,0.14225309101194755,Transport,1
|
| 95 |
+
temperature_rain/D/short,Modular-TS-2.4M-v3_M1,189.0777322179368,189.0777322179368,5.9719531694435695,1.3929113280636496,31.342278410802678,1.4597783411489158,18.62782191179909,13.75055388767801,1.6188037505079726,0.7030567835682812,0.5648490663882909,Nature,1
|
| 96 |
+
us_births/D/short,Modular-TS-2.4M-v3_M1,208488.02666666667,208488.02666666667,290.93140625,0.42816387846273435,0.0278005854288737,0.027437305450439452,4.140300088215134,456.60489119879855,0.04280300341490426,0.027272458563743076,0.02229785791913427,Healthcare,1
|
| 97 |
+
us_births/M/short,Modular-TS-2.4M-v3_M1,88368842.66666667,88368842.66666667,7829.018229166667,0.8869402107460148,0.023900558551152546,0.02429335316022237,6.003885858475542,9400.47034284278,0.029197796793272567,0.024316877242219714,0.018599029498638203,Healthcare,1
|
| 98 |
+
us_births/W/short,Modular-TS-2.4M-v3_M1,4173123.4285714286,4173123.4285714286,1518.942103794643,1.3805942500939652,0.02031937667301723,0.020534809146608626,9.69547943307617,2042.8224172872758,0.027731114006895916,0.020619490120016678,0.0164939195793787,Healthcare,1
|