Commit Β·
0bc036b
1
Parent(s): c662d51
Add c3-charm SDK usage section (install, client, forecast, embeddings) (#1)
Browse files- Add c3-charm SDK usage section (install, client, forecast, embeddings) (9008e7dab42012fca2ba7118e8fe285d576a1753)
- Expand model card with full c3-charm SDK documentation (53df55bbffd3f20d4b57f1e685e01051a0d1cfb2)
Co-authored-by: Gerardo Pastrana <gerar444@users.noreply.huggingface.co>
README.md
CHANGED
|
@@ -17,9 +17,10 @@ It produces full quantile forecasts (99 quantile levels) for arbitrary horizons
|
|
| 17 |
is evaluated zero-shot on the [GIFT-Eval](https://huggingface.co/spaces/Salesforce/GIFT-Eval)
|
| 18 |
benchmark β no GIFT-Eval data is used in training.
|
| 19 |
|
| 20 |
-
> **Availability:** CHARM
|
| 21 |
-
>
|
| 22 |
-
>
|
|
|
|
| 23 |
|
| 24 |
## Model summary
|
| 25 |
|
|
@@ -40,6 +41,664 @@ Zero-shot probabilistic forecasting of univariate and multivariate time series
|
|
| 40 |
across domains (energy, transport, sales, healthcare, nature, web/cloud-ops,
|
| 41 |
econ/finance). The model is applied without any per-dataset fine-tuning.
|
| 42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
## GIFT-Eval results
|
| 44 |
|
| 45 |
Evaluated zero-shot on the full GIFT-Eval benchmark (97 dataset/frequency/term
|
|
|
|
| 17 |
is evaluated zero-shot on the [GIFT-Eval](https://huggingface.co/spaces/Salesforce/GIFT-Eval)
|
| 18 |
benchmark β no GIFT-Eval data is used in training.
|
| 19 |
|
| 20 |
+
> **Availability:** CHARM model weights and training/replication code are
|
| 21 |
+
> **not publicly released**. The model is served behind an API and consumed
|
| 22 |
+
> through the open-source **`c3-charm` Python SDK** (see [Using CHARM](#using-charm--the-c3-charm-sdk) below).
|
| 23 |
+
> This page serves as the model card and benchmark record.
|
| 24 |
|
| 25 |
## Model summary
|
| 26 |
|
|
|
|
| 41 |
across domains (energy, transport, sales, healthcare, nature, web/cloud-ops,
|
| 42 |
econ/finance). The model is applied without any per-dataset fine-tuning.
|
| 43 |
|
| 44 |
+
## Using CHARM β the `c3-charm` SDK
|
| 45 |
+
|
| 46 |
+
CHARM is served behind an API and consumed through the open-source [`c3-charm`](https://pypi.org/project/c3-charm/) Python SDK. The SDK provides **embeddings** (multivariate time series β vectors), **forecast/backcast** (quantile predictions), and an optional **toolkit** for downstream tasks (anomaly detection, retrieval, classification, reconstruction, forecasting). The reference below is the full SDK documentation.
|
| 47 |
+
|
| 48 |
+
> **Dual-model serving.** A CHARM server can be backed by two independent checkpoints β one for embeddings (`/predict`, `client.embeddings`) and a separate one for forecasting (`/forecast`, `client.prediction`). They may differ in architecture, patch size, and embedding dimension, so read per-model properties from `client.model_info()` rather than hardcoding.
|
| 49 |
+
|
| 50 |
+
### Installation
|
| 51 |
+
|
| 52 |
+
```bash
|
| 53 |
+
pip install c3-charm # core SDK only (embeddings + forecast)
|
| 54 |
+
pip install c3-charm[toolkit] # includes PyTorch models, datasets, trainers
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
Or from source:
|
| 58 |
+
|
| 59 |
+
```bash
|
| 60 |
+
git clone https://github.com/c3ai/c3-charm.git
|
| 61 |
+
cd c3-charm
|
| 62 |
+
poetry install # core SDK only
|
| 63 |
+
poetry install --with toolkit # include toolkit dependencies
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
#### CPU vs CUDA torch
|
| 67 |
+
|
| 68 |
+
`pip install c3-charm` pulls the **default torch wheel from PyPI**, which on
|
| 69 |
+
Linux is the full **CUDA** build (~4 GB). The SDK itself never requires a
|
| 70 |
+
GPU β the model runs server-side, and toolkit training heads are small
|
| 71 |
+
enough to fit on CPU β so if you want the smaller CPU-only wheel, install
|
| 72 |
+
torch from the CPU index **before** `c3-charm`:
|
| 73 |
+
|
| 74 |
+
```bash
|
| 75 |
+
pip install torch --index-url https://download.pytorch.org/whl/cpu
|
| 76 |
+
pip install c3-charm
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
Or from source, use `bash setup.sh`, which forces the CPU wheel.
|
| 80 |
+
|
| 81 |
+
If you instead want a specific CUDA build, install it manually **after**
|
| 82 |
+
`c3-charm`:
|
| 83 |
+
|
| 84 |
+
```bash
|
| 85 |
+
pip install --force-reinstall --no-deps torch \
|
| 86 |
+
--index-url https://download.pytorch.org/whl/cu124
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
Swap `cu124` for the CUDA version that matches your driver (`cu121`,
|
| 90 |
+
`cu126`, `cu128`, β¦). `--no-deps` is important β it stops pip from
|
| 91 |
+
re-resolving your torch install.
|
| 92 |
+
|
| 93 |
+
---
|
| 94 |
+
|
| 95 |
+
### Core SDK
|
| 96 |
+
|
| 97 |
+
#### Client initialization
|
| 98 |
+
|
| 99 |
+
```python
|
| 100 |
+
from charm import CharmClient
|
| 101 |
+
|
| 102 |
+
client = CharmClient(
|
| 103 |
+
base_url="http://your-server:8080",
|
| 104 |
+
api_key="your-api-key", # or set CHARM_API_KEY env var
|
| 105 |
+
timeout=300, # override; SDK default is 15s β raise it for /forecast (server allows up to ~220s)
|
| 106 |
+
max_retries=3,
|
| 107 |
+
)
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
#### Embeddings β `client.embeddings.create()`
|
| 111 |
+
|
| 112 |
+
Converts time series windows into dense vectors.
|
| 113 |
+
|
| 114 |
+
```python
|
| 115 |
+
response = client.embeddings.create(
|
| 116 |
+
descriptions=[["sensor_A", "sensor_B"]], # (N, C) channel names
|
| 117 |
+
ts_array=[[[1.0, 2.0], [1.1, 2.1], ...]], # (N, T, C) values
|
| 118 |
+
batch_size=32,
|
| 119 |
+
return_tensors="np", # "list", "np", or "torch"
|
| 120 |
+
aggregate=True, # True β (N, D); False β (N, T_, C, D)
|
| 121 |
+
progress=True,
|
| 122 |
+
)
|
| 123 |
+
embeddings = response.embeds # shape (N, D) when aggregate=True
|
| 124 |
+
```
|
| 125 |
+
|
| 126 |
+
**`aggregate` parameter:**
|
| 127 |
+
- `True` (default): Returns flattened embeddings `(N, D)` β one vector per series. Best for retrieval, classification, clustering.
|
| 128 |
+
- `False`: Returns per-patch, per-channel embeddings `(N, T_, C, D)` where `T_ = ceil(T / patch_size)`. Best for fine-grained tasks or custom heads.
|
| 129 |
+
|
| 130 |
+
**Async** (faster for large datasets):
|
| 131 |
+
|
| 132 |
+
```python
|
| 133 |
+
response = await client.embeddings.async_create(
|
| 134 |
+
descriptions=descriptions,
|
| 135 |
+
ts_array=ts_array,
|
| 136 |
+
max_B_per_request=32,
|
| 137 |
+
concurrency_per_call=8,
|
| 138 |
+
return_tensors="np",
|
| 139 |
+
aggregate=True,
|
| 140 |
+
)
|
| 141 |
+
```
|
| 142 |
+
|
| 143 |
+
#### Forecast / Backcast β `client.prediction.create()`
|
| 144 |
+
|
| 145 |
+
Zero-shot quantile predictions β no training required.
|
| 146 |
+
|
| 147 |
+
```python
|
| 148 |
+
response = client.prediction.create(
|
| 149 |
+
descriptions=[["sensor_A", "sensor_B"]],
|
| 150 |
+
ts_array=[[[1.0, 2.0], [1.1, 2.1], ...]],
|
| 151 |
+
target_len=10, # positive = forecast, negative = backcast
|
| 152 |
+
return_tensors="np",
|
| 153 |
+
)
|
| 154 |
+
forecast = response.denormalized_predictions # (N, target_len=10, C, Q) β Q quantiles (model-dependent, e.g. 21)
|
| 155 |
+
median = response.median # (N, 10, C) β point forecast (median quantile)
|
| 156 |
+
```
|
| 157 |
+
|
| 158 |
+
**Backcast** (reconstruct past values):
|
| 159 |
+
|
| 160 |
+
```python
|
| 161 |
+
response = client.prediction.create(
|
| 162 |
+
descriptions=descriptions,
|
| 163 |
+
ts_array=ts_array,
|
| 164 |
+
target_len=-8, # reconstruct last 8 steps
|
| 165 |
+
return_tensors="np",
|
| 166 |
+
)
|
| 167 |
+
```
|
| 168 |
+
|
| 169 |
+
#### Input constraints
|
| 170 |
+
|
| 171 |
+
| Constraint | Limit |
|
| 172 |
+
|---|---|
|
| 173 |
+
| Timesteps per series | 1 β€ T β€ 8192 (the model's training window) |
|
| 174 |
+
| Channels per series | No hard limit (large C grows memory ~O(CΒ²) under cross-channel attention) |
|
| 175 |
+
| Per-request size | N Γ C Γ T β€ 500,000 β SDK client-side batching guard, not a server limit |
|
| 176 |
+
| Batch consistency | All series in a request must share the same T and C |
|
| 177 |
+
| Best accuracy | T β€ 8192 (the model's training window); T need not be a multiple of the patch size |
|
| 178 |
+
|
| 179 |
+
The model was trained on windows of up to 8192 timesteps (patch size 16).
|
| 180 |
+
Inputs are not required to be a multiple of the patch size β they are padded
|
| 181 |
+
to a patch boundary internally. The architecture can technically accept more
|
| 182 |
+
than 8192 (up to 1500 patches), but longer inputs rely on positions seen only
|
| 183 |
+
in pretraining, so quality degrades. Query `client.model_info()` for the
|
| 184 |
+
served model's actual patch size and embedding dimension.
|
| 185 |
+
|
| 186 |
+
The SDK handles client-side batching automatically when you set `batch_size` (sync) or `max_B_per_request` (async).
|
| 187 |
+
|
| 188 |
+
#### Output shapes
|
| 189 |
+
|
| 190 |
+
| Method | Output field | Shape |
|
| 191 |
+
|---|---|---|
|
| 192 |
+
| `embeddings.create(aggregate=True)` | `response.embeds` | (N, D) |
|
| 193 |
+
| `embeddings.create(aggregate=False)` | `response.embeds` | (N, T_, C, D), T_ = ceil(T / patch_size) |
|
| 194 |
+
| `prediction.create(target_len > 0)` | `response.denormalized_predictions` | (N, target_len, C, Q) |
|
| 195 |
+
| `prediction.create(target_len < 0)` | `response.denormalized_predictions` | (N, abs(target_len), C, Q) |
|
| 196 |
+
| `prediction.create(...)` | `response.median` | (N, abs(target_len), C) |
|
| 197 |
+
|
| 198 |
+
#### Channel descriptions
|
| 199 |
+
|
| 200 |
+
Descriptions are **required** and affect embedding quality. They tell the model what each channel represents.
|
| 201 |
+
|
| 202 |
+
**Good descriptions** β use meaningful, consistent names:
|
| 203 |
+
```python
|
| 204 |
+
descriptions = [["engine_temperature", "oil_pressure", "rpm"]]
|
| 205 |
+
```
|
| 206 |
+
|
| 207 |
+
**Acceptable** β short but informative:
|
| 208 |
+
```python
|
| 209 |
+
descriptions = [["temp", "pressure", "speed"]]
|
| 210 |
+
```
|
| 211 |
+
|
| 212 |
+
**Avoid** β generic or positional names reduce model effectiveness:
|
| 213 |
+
```python
|
| 214 |
+
descriptions = [["col_0", "col_1", "col_2"]] # works but suboptimal
|
| 215 |
+
```
|
| 216 |
+
|
| 217 |
+
When working with pandas DataFrames, use column names directly:
|
| 218 |
+
```python
|
| 219 |
+
descriptions = [df.columns.tolist()] * N
|
| 220 |
+
```
|
| 221 |
+
|
| 222 |
+
#### Scaling
|
| 223 |
+
|
| 224 |
+
**No pre-processing needed.** CHARM normalizes internally. Send raw data as-is. Do not apply StandardScaler, MinMaxScaler, or log transforms before calling the API.
|
| 225 |
+
|
| 226 |
+
#### Error handling
|
| 227 |
+
|
| 228 |
+
```python
|
| 229 |
+
from charm import CharmError, AuthenticationError, InvalidRequestError, RateLimitError
|
| 230 |
+
|
| 231 |
+
try:
|
| 232 |
+
response = client.embeddings.create(...)
|
| 233 |
+
except AuthenticationError:
|
| 234 |
+
# bad API key
|
| 235 |
+
except InvalidRequestError as e:
|
| 236 |
+
# shape violations, empty input
|
| 237 |
+
except RateLimitError:
|
| 238 |
+
# back off and retry
|
| 239 |
+
except CharmError as e:
|
| 240 |
+
# catch-all for other SDK errors
|
| 241 |
+
```
|
| 242 |
+
|
| 243 |
+
---
|
| 244 |
+
|
| 245 |
+
### Toolkit β Downstream Tasks
|
| 246 |
+
|
| 247 |
+
The toolkit (`pip install c3-charm[toolkit]`) provides PyTorch models, dataset utilities, and training infrastructure for fine-tuning on top of CHARM embeddings.
|
| 248 |
+
|
| 249 |
+
#### Retrieval β `charm_toolkit.retrieval`
|
| 250 |
+
|
| 251 |
+
Find similar time series by embedding similarity.
|
| 252 |
+
|
| 253 |
+
```python
|
| 254 |
+
from charm_toolkit.retrieval import (
|
| 255 |
+
l2_normalize,
|
| 256 |
+
cosine_similarity_matrix,
|
| 257 |
+
knn_search,
|
| 258 |
+
retrieval_metrics,
|
| 259 |
+
)
|
| 260 |
+
|
| 261 |
+
# Embed your data
|
| 262 |
+
response = client.embeddings.create(
|
| 263 |
+
descriptions=descriptions,
|
| 264 |
+
ts_array=windows_list,
|
| 265 |
+
return_tensors="np",
|
| 266 |
+
)
|
| 267 |
+
embeddings = response.embeds # (N, D)
|
| 268 |
+
|
| 269 |
+
# Similarity search
|
| 270 |
+
sim = cosine_similarity_matrix(embeddings, embeddings)
|
| 271 |
+
|
| 272 |
+
# kNN search
|
| 273 |
+
indices, scores = knn_search(query_emb, corpus_emb, k=5)
|
| 274 |
+
|
| 275 |
+
# Evaluation metrics
|
| 276 |
+
metrics = retrieval_metrics(
|
| 277 |
+
query_emb=query_emb,
|
| 278 |
+
corpus_emb=corpus_emb,
|
| 279 |
+
query_labels=query_labels,
|
| 280 |
+
corpus_labels=corpus_labels,
|
| 281 |
+
k_values=[1, 3, 5, 10],
|
| 282 |
+
exclude_self=True,
|
| 283 |
+
query_ids=query_dataset_names,
|
| 284 |
+
corpus_ids=corpus_dataset_names,
|
| 285 |
+
)
|
| 286 |
+
# Returns: precision@k, ndcg@k, hit_rate@k
|
| 287 |
+
```
|
| 288 |
+
|
| 289 |
+
#### Anomaly Detection β `charm_toolkit.anomaly_detection`
|
| 290 |
+
|
| 291 |
+
Detect anomalies via kNN distance scoring on windowed CHARM embeddings.
|
| 292 |
+
|
| 293 |
+
```python
|
| 294 |
+
from charm_toolkit.anomaly_detection import (
|
| 295 |
+
sliding_window_embeddings,
|
| 296 |
+
knn_anomaly_scores,
|
| 297 |
+
window_scores_to_pointwise,
|
| 298 |
+
)
|
| 299 |
+
|
| 300 |
+
# 1. Embed sliding windows
|
| 301 |
+
train_emb = sliding_window_embeddings(
|
| 302 |
+
client, train_data, descriptions,
|
| 303 |
+
window_size=128, stride=1, batch_size=64,
|
| 304 |
+
)
|
| 305 |
+
test_emb = sliding_window_embeddings(
|
| 306 |
+
client, test_data, descriptions,
|
| 307 |
+
window_size=128, stride=1, batch_size=64,
|
| 308 |
+
)
|
| 309 |
+
|
| 310 |
+
# 2. Score test windows by distance to train
|
| 311 |
+
window_scores = knn_anomaly_scores(
|
| 312 |
+
test_emb=test_emb,
|
| 313 |
+
reference_emb=train_emb,
|
| 314 |
+
k=5,
|
| 315 |
+
distance="cosine", # "cosine", "l2", "l1"
|
| 316 |
+
aggregation="mean", # "mean", "max"
|
| 317 |
+
)
|
| 318 |
+
|
| 319 |
+
# 3. Aggregate to per-timestep scores
|
| 320 |
+
pointwise_scores = window_scores_to_pointwise(
|
| 321 |
+
window_scores=window_scores,
|
| 322 |
+
window_size=128,
|
| 323 |
+
stride=1,
|
| 324 |
+
total_length=len(test_data),
|
| 325 |
+
method="mean", # "mean", "max", "last", "center"
|
| 326 |
+
)
|
| 327 |
+
```
|
| 328 |
+
|
| 329 |
+
**Pointwise aggregation methods:**
|
| 330 |
+
|
| 331 |
+
Each timestep is covered by multiple overlapping windows. The `method` parameter controls how to assign a single score per timestep:
|
| 332 |
+
|
| 333 |
+
| Method | Behavior | Use case |
|
| 334 |
+
|--------|----------|----------|
|
| 335 |
+
| `"mean"` | Average of all windows covering the point | Smooth, best for offline evaluation |
|
| 336 |
+
| `"max"` | Max score among covering windows | Conservative, catches isolated spikes |
|
| 337 |
+
| `"last"` | Score of the most recently *completed* window | Online/streaming β score only updates when a window finishes processing |
|
| 338 |
+
| `"center"` | Score of the window centered on each point | Minimal time-shift, tightest temporal alignment |
|
| 339 |
+
|
| 340 |
+
**Zero-shot recipes (no clean reference set required) β recommended methods, in order of strength:**
|
| 341 |
+
|
| 342 |
+
1. **Bootstrap k-NN** (best). Two steps: run `sklearn.ensemble.IsolationForest` on the embedding matrix and take the bottom ~70% by score as a presumed-clean reference; then call `knn_anomaly_scores` against that reference.
|
| 343 |
+
2. **CBLOF**: `pyod.models.cblof.CBLOF` on the embedding matrix.
|
| 344 |
+
3. **IsolationForest**: `sklearn.ensemble.IsolationForest` directly on the embedding matrix.
|
| 345 |
+
|
| 346 |
+
L2-normalize embeddings beforehand to use cosine geometry. See `demo_retrieval_anomaly_detection.ipynb`.
|
| 347 |
+
|
| 348 |
+
**Best practices (validated on TSB-AD, metric VUS-PR):**
|
| 349 |
+
|
| 350 |
+
1. **Ensemble the embedding score with per-window statistics.** The encoder
|
| 351 |
+
instance-normalizes each window, erasing amplitude/level-shift anomalies (spikes,
|
| 352 |
+
steps β the most common kind) from the embedding. Recover them with
|
| 353 |
+
`window_statistics` (no extra model call) and combine via `ensemble_scores`
|
| 354 |
+
(`method="zscore"` β z-score each detector and sum; parameter-free and as strong as a
|
| 355 |
+
tuned weighted ensemble). This is worth **~+7 pp VUS-PR** supervised.
|
| 356 |
+
|
| 357 |
+
```python
|
| 358 |
+
from charm_toolkit.anomaly_detection import window_statistics, ensemble_scores
|
| 359 |
+
|
| 360 |
+
emb_score = knn_anomaly_scores(test_emb, train_emb, k=3, distance="cosine")
|
| 361 |
+
S_tr = window_statistics(train_data, 128, 1)
|
| 362 |
+
mu, sd = S_tr.mean(0, keepdims=True), S_tr.std(0, keepdims=True) + 1e-8
|
| 363 |
+
stats_score = knn_anomaly_scores(
|
| 364 |
+
(window_statistics(test_data, 128, 1) - mu) / sd, (S_tr - mu) / sd,
|
| 365 |
+
k=3, distance="l2")
|
| 366 |
+
window_scores = ensemble_scores([emb_score, stats_score], method="zscore")
|
| 367 |
+
# zero-shot: build the reference by bootstrap (above) and use method="rank"
|
| 368 |
+
```
|
| 369 |
+
|
| 370 |
+
2. **Multivariate: do NOT pool channels when the channel count is high.** Channel-mean
|
| 371 |
+
pooling dilutes an anomaly confined to a few channels across all of them. Keep channels
|
| 372 |
+
separate with `sliding_window_channel_embeddings` + `per_channel_knn_scores`
|
| 373 |
+
(`channel_pool="adaptive"`). The benefit grows with the number of channels β negligible
|
| 374 |
+
at Cβ€3, sizeable at Cβ₯20, large at C>60 β so it is **advisable whenever C is high**. For
|
| 375 |
+
few channels, plain mean-pooling is equally good and cheaper.
|
| 376 |
+
|
| 377 |
+
```python
|
| 378 |
+
from charm_toolkit.anomaly_detection import (
|
| 379 |
+
sliding_window_channel_embeddings, per_channel_knn_scores)
|
| 380 |
+
|
| 381 |
+
train_pc = sliding_window_channel_embeddings(client, train_data, descriptions) # (N, C, D)
|
| 382 |
+
test_pc = sliding_window_channel_embeddings(client, test_data, descriptions)
|
| 383 |
+
emb_score = per_channel_knn_scores(test_pc, train_pc, k=3, channel_pool="adaptive")
|
| 384 |
+
```
|
| 385 |
+
|
| 386 |
+
#### ReconstructionModel β anomaly detection via learned head
|
| 387 |
+
|
| 388 |
+
```python
|
| 389 |
+
from charm_toolkit import (
|
| 390 |
+
ReconstructionModel, create_reconstruction_datasets,
|
| 391 |
+
collator, TrainerClass,
|
| 392 |
+
)
|
| 393 |
+
from torch.utils.data import DataLoader
|
| 394 |
+
import torch.nn as nn
|
| 395 |
+
|
| 396 |
+
train_ds, val_ds, test_ds = create_reconstruction_datasets(
|
| 397 |
+
raw_data, # (T, C) numpy array or torch tensor
|
| 398 |
+
descriptions=channel_names,
|
| 399 |
+
window_size=256,
|
| 400 |
+
stride=1,
|
| 401 |
+
train_ratio=0.7,
|
| 402 |
+
val_ratio=0.15,
|
| 403 |
+
sequential=True,
|
| 404 |
+
scale=True,
|
| 405 |
+
)
|
| 406 |
+
|
| 407 |
+
model = ReconstructionModel(
|
| 408 |
+
embedding_client=client,
|
| 409 |
+
reconstructor="linear", # "linear", "mlp", or custom nn.Module
|
| 410 |
+
hidden_dim=128,
|
| 411 |
+
dropout=0.1,
|
| 412 |
+
)
|
| 413 |
+
|
| 414 |
+
trainer = TrainerClass(
|
| 415 |
+
model=model,
|
| 416 |
+
train_loader=DataLoader(train_ds, batch_size=512, collate_fn=collator),
|
| 417 |
+
val_loader=DataLoader(val_ds, batch_size=512, collate_fn=collator),
|
| 418 |
+
epochs=1000,
|
| 419 |
+
patience=5,
|
| 420 |
+
lr=1e-3,
|
| 421 |
+
criterion=nn.HuberLoss(),
|
| 422 |
+
)
|
| 423 |
+
trainer.fit()
|
| 424 |
+
```
|
| 425 |
+
|
| 426 |
+
#### ForecastingModel β embedding-based forecasting
|
| 427 |
+
|
| 428 |
+
```python
|
| 429 |
+
from charm_toolkit import ForecastingModel, create_forecasting_datasets, collator, TrainerClass
|
| 430 |
+
from torch.utils.data import DataLoader
|
| 431 |
+
|
| 432 |
+
train_ds, val_ds, test_ds = create_forecasting_datasets(
|
| 433 |
+
raw_data,
|
| 434 |
+
descriptions=channel_names,
|
| 435 |
+
train_horizon=96,
|
| 436 |
+
test_horizon=96,
|
| 437 |
+
train_ratio=0.7,
|
| 438 |
+
val_ratio=0.15,
|
| 439 |
+
sequential=True,
|
| 440 |
+
scale=True,
|
| 441 |
+
)
|
| 442 |
+
|
| 443 |
+
model = ForecastingModel(
|
| 444 |
+
embedding_client=client,
|
| 445 |
+
horizon=96,
|
| 446 |
+
input_size=96,
|
| 447 |
+
head="linear",
|
| 448 |
+
hidden_dim=128,
|
| 449 |
+
mode="last", # "last", "avg", "none"
|
| 450 |
+
per_channel=True,
|
| 451 |
+
num_channels=len(channel_names),
|
| 452 |
+
)
|
| 453 |
+
|
| 454 |
+
trainer = TrainerClass(
|
| 455 |
+
model=model,
|
| 456 |
+
train_loader=DataLoader(train_ds, batch_size=512, collate_fn=collator),
|
| 457 |
+
val_loader=DataLoader(val_ds, batch_size=512, collate_fn=collator),
|
| 458 |
+
epochs=1000,
|
| 459 |
+
patience=10,
|
| 460 |
+
lr=1e-2,
|
| 461 |
+
)
|
| 462 |
+
trainer.fit()
|
| 463 |
+
```
|
| 464 |
+
|
| 465 |
+
#### ClassificationModel β time series classification
|
| 466 |
+
|
| 467 |
+
```python
|
| 468 |
+
from charm_toolkit import ClassificationModel, create_classification_datasets, collator, TrainerClass
|
| 469 |
+
from torch.utils.data import DataLoader
|
| 470 |
+
import torch.nn as nn
|
| 471 |
+
|
| 472 |
+
train_ds, val_ds, test_ds = create_classification_datasets(
|
| 473 |
+
raw_data, # (N, T, C)
|
| 474 |
+
labels=labels, # list of N integer labels
|
| 475 |
+
descriptions=channel_names,
|
| 476 |
+
train_ratio=0.7,
|
| 477 |
+
val_ratio=0.15,
|
| 478 |
+
)
|
| 479 |
+
|
| 480 |
+
model = ClassificationModel(
|
| 481 |
+
embedding_client=client,
|
| 482 |
+
num_classes=num_classes,
|
| 483 |
+
hidden_dim=128,
|
| 484 |
+
pooling_over_t="mean",
|
| 485 |
+
pooling_over_channels="mean",
|
| 486 |
+
classifier_type="mlp",
|
| 487 |
+
)
|
| 488 |
+
|
| 489 |
+
trainer = TrainerClass(
|
| 490 |
+
model=model,
|
| 491 |
+
train_loader=DataLoader(train_ds, batch_size=32, collate_fn=collator),
|
| 492 |
+
val_loader=DataLoader(val_ds, batch_size=32, collate_fn=collator),
|
| 493 |
+
epochs=100,
|
| 494 |
+
patience=10,
|
| 495 |
+
lr=1e-3,
|
| 496 |
+
criterion=nn.CrossEntropyLoss(),
|
| 497 |
+
)
|
| 498 |
+
trainer.fit()
|
| 499 |
+
```
|
| 500 |
+
|
| 501 |
+
#### Precomputing embeddings (critical for training)
|
| 502 |
+
|
| 503 |
+
Toolkit models call the API every forward pass. For training with hundreds of windows per epoch, **precompute embeddings once**:
|
| 504 |
+
|
| 505 |
+
```python
|
| 506 |
+
from charm_toolkit import precompute_dataset_embeddings, PrecomputedEmbeddingsDataset
|
| 507 |
+
|
| 508 |
+
# Compute once, save to disk as memmap
|
| 509 |
+
train_shape = precompute_dataset_embeddings(
|
| 510 |
+
client=client, dataset=train_ds,
|
| 511 |
+
output_path="./outputs/train_embeddings.pt", memory_batch_size=8192
|
| 512 |
+
)
|
| 513 |
+
val_shape = precompute_dataset_embeddings(
|
| 514 |
+
client=client, dataset=val_ds,
|
| 515 |
+
output_path="./outputs/val_embeddings.pt", memory_batch_size=8192
|
| 516 |
+
)
|
| 517 |
+
|
| 518 |
+
# Wrap datasets β model skips API calls when "embeds" key present
|
| 519 |
+
train_ds = PrecomputedEmbeddingsDataset(train_ds, "./outputs/train_embeddings.pt", train_shape)
|
| 520 |
+
val_ds = PrecomputedEmbeddingsDataset(val_ds, "./outputs/val_embeddings.pt", val_shape)
|
| 521 |
+
|
| 522 |
+
# Training now uses cached embeddings β orders of magnitude faster
|
| 523 |
+
train_loader = DataLoader(train_ds, batch_size=512, shuffle=True, collate_fn=collator)
|
| 524 |
+
```
|
| 525 |
+
|
| 526 |
+
#### Trainer API
|
| 527 |
+
|
| 528 |
+
```python
|
| 529 |
+
from charm_toolkit import TrainerClass
|
| 530 |
+
|
| 531 |
+
trainer = TrainerClass(
|
| 532 |
+
model=model,
|
| 533 |
+
train_loader=train_loader,
|
| 534 |
+
val_loader=val_loader,
|
| 535 |
+
test_loader=test_loader, # optional
|
| 536 |
+
lr=1e-3,
|
| 537 |
+
weight_decay=1e-4,
|
| 538 |
+
epochs=1000,
|
| 539 |
+
patience=5,
|
| 540 |
+
min_delta=1e-4,
|
| 541 |
+
max_grad_norm=5.0,
|
| 542 |
+
criterion=None, # defaults to MSELoss
|
| 543 |
+
)
|
| 544 |
+
trainer.fit()
|
| 545 |
+
test_loss = trainer.evaluate(test_loader)
|
| 546 |
+
```
|
| 547 |
+
|
| 548 |
+
#### Dataset factory functions
|
| 549 |
+
|
| 550 |
+
All return `(train_dataset, val_dataset, test_dataset)`:
|
| 551 |
+
|
| 552 |
+
| Function | Input shape | Key args |
|
| 553 |
+
|---|---|---|
|
| 554 |
+
| `create_reconstruction_datasets(raw_data, ...)` | (T, C) | `window_size`, `stride`, `train_ratio`, `val_ratio` |
|
| 555 |
+
| `create_forecasting_datasets(raw_data, ...)` | (T, C) | `train_horizon`, `test_horizon`, `stride`, `train_ratio`, `val_ratio` |
|
| 556 |
+
| `create_classification_datasets(raw_data, labels, ...)` | (N, T, C) | `train_ratio`, `val_ratio` |
|
| 557 |
+
|
| 558 |
+
Reconstruction and forecasting expect a single long time series `(T, C)` split temporally. Classification expects pre-windowed `(N, T, C)`.
|
| 559 |
+
|
| 560 |
+
#### collator
|
| 561 |
+
|
| 562 |
+
All DataLoaders using toolkit datasets require `collator` as the `collate_fn`:
|
| 563 |
+
|
| 564 |
+
```python
|
| 565 |
+
from charm_toolkit import collator
|
| 566 |
+
# or equivalently:
|
| 567 |
+
from charm_toolkit.Datasets import collator
|
| 568 |
+
```
|
| 569 |
+
|
| 570 |
+
---
|
| 571 |
+
|
| 572 |
+
### Embeddings as features
|
| 573 |
+
|
| 574 |
+
CHARM embeddings work as drop-in feature vectors for any sklearn model:
|
| 575 |
+
|
| 576 |
+
```python
|
| 577 |
+
import numpy as np
|
| 578 |
+
from sklearn.ensemble import IsolationForest
|
| 579 |
+
from sklearn.linear_model import LogisticRegression
|
| 580 |
+
from charm_toolkit.retrieval import cosine_similarity_matrix
|
| 581 |
+
|
| 582 |
+
response = client.embeddings.create(
|
| 583 |
+
descriptions=descriptions,
|
| 584 |
+
ts_array=windows_list,
|
| 585 |
+
return_tensors="np",
|
| 586 |
+
)
|
| 587 |
+
X = response.embeds # (N, D)
|
| 588 |
+
|
| 589 |
+
# Anomaly detection with isolation forest
|
| 590 |
+
clf = IsolationForest(contamination=0.05)
|
| 591 |
+
anomaly_labels = clf.fit_predict(X)
|
| 592 |
+
|
| 593 |
+
# Similarity search
|
| 594 |
+
sim = cosine_similarity_matrix(X, X)
|
| 595 |
+
|
| 596 |
+
# As features for any classifier
|
| 597 |
+
clf = LogisticRegression().fit(X_train, y_train)
|
| 598 |
+
```
|
| 599 |
+
|
| 600 |
+
---
|
| 601 |
+
|
| 602 |
+
### Local Deployment
|
| 603 |
+
|
| 604 |
+
Deploy models locally from GitHub releases β no remote server needed:
|
| 605 |
+
|
| 606 |
+
```python
|
| 607 |
+
with CharmClient(tag="experiment-2026-03-15_10-30-00") as client:
|
| 608 |
+
response = client.embeddings.create(...)
|
| 609 |
+
# Server shuts down automatically
|
| 610 |
+
```
|
| 611 |
+
|
| 612 |
+
When `tag` is provided:
|
| 613 |
+
1. Checks for GPU availability (falls back to CPU)
|
| 614 |
+
2. Clones repo at the specified tag (shallow clone)
|
| 615 |
+
3. Downloads model weights from the GitHub release
|
| 616 |
+
4. Launches the serving stack locally
|
| 617 |
+
5. Polls health endpoint until ready
|
| 618 |
+
|
| 619 |
+
Files cached at `~/.charm/models/<tag>/` for fast subsequent runs.
|
| 620 |
+
|
| 621 |
+
```python
|
| 622 |
+
CharmClient(
|
| 623 |
+
tag="experiment-tag", # required for local mode
|
| 624 |
+
repo_url="https://...", # default: c3-e/research
|
| 625 |
+
cache_dir="/path/to/cache", # default: ~/.charm/models
|
| 626 |
+
port=8080, # 0 = auto-select
|
| 627 |
+
)
|
| 628 |
+
```
|
| 629 |
+
|
| 630 |
+
---
|
| 631 |
+
|
| 632 |
+
### Best practices
|
| 633 |
+
|
| 634 |
+
#### Input & preprocessing
|
| 635 |
+
- Send data as `(N, T, C)` β N series, each T timesteps Γ C channels; all series in one request must share the same T and C.
|
| 636 |
+
- Keep `T β€ 8192` (the training window). The model can accept more, but quality degrades on lengths it wasn't trained on. T does **not** need to be a multiple of the patch size β inputs are padded to a patch boundary internally.
|
| 637 |
+
- **Do not pre-scale** your data. CHARM normalizes internally (asinh z-score); applying StandardScaler/MinMaxScaler/log yourself hurts results.
|
| 638 |
+
|
| 639 |
+
#### Channel descriptions (a real quality lever)
|
| 640 |
+
- Use meaningful, consistent channel names (`"engine_temperature"`, not `"col_0"`) β the model is channel-aware and descriptions materially affect embeddings.
|
| 641 |
+
- Reuse the same names across requests so embeddings stay comparable (retrieval, clustering).
|
| 642 |
+
|
| 643 |
+
#### Batching & throughput
|
| 644 |
+
- Respect the per-request budget: `batch_size Γ C Γ T β€ 500,000` (SDK-enforced client-side).
|
| 645 |
+
- Use `async_create` for large N β it batches concurrently; the sync client is sequential and slow past ~100 series. Tune `max_B_per_request` / `concurrency_per_call` instead of one giant request.
|
| 646 |
+
|
| 647 |
+
#### Timeouts & retries
|
| 648 |
+
- Raise `timeout` for forecasting β the SDK default is 15s, but the server allows forecasts up to ~220s. Use `timeout β 220+` for `prediction.create`.
|
| 649 |
+
- Keep the built-in retries (exponential backoff on 429/5xx) rather than hand-rolling.
|
| 650 |
+
|
| 651 |
+
#### Embeddings
|
| 652 |
+
- `aggregate=True` (default) β `(N, D)` for retrieval / classification / clustering. `aggregate=False` β `(N, T_, C, D)` only when you need per-patch/per-channel detail for a custom head.
|
| 653 |
+
- L2-normalize before cosine similarity (embeddings are unit-normed by the encoder, but normalize again after any pooling you do).
|
| 654 |
+
- Discover `D` at runtime via `client.model_info()` β it's model-dependent; don't hardcode.
|
| 655 |
+
|
| 656 |
+
#### Forecasting
|
| 657 |
+
- `target_len > 0` = forecast, `< 0` = backcast; `0` is invalid.
|
| 658 |
+
- Keep `T + abs(target_len) β€ 8192` (the training window) for best forecast/backcast quality.
|
| 659 |
+
- Use `response.median` for a point forecast, or the full quantile axis for intervals. `Q` is model-dependent (e.g. 21 or 99) β read `denormalized_predictions.shape[-1]`.
|
| 660 |
+
|
| 661 |
+
#### Classification
|
| 662 |
+
- For classification heads, **don't pool over channels** β keeping the per-channel embeddings flat (rather than averaging them) boosts accuracy, at a modest cost in head size/complexity. In the toolkit `ClassificationModel`, set `pooling_over_channels="flatten"` (and pass `num_channels=C`, required for flatten) instead of the default `"mean"`.
|
| 663 |
+
|
| 664 |
+
#### Dual-model awareness
|
| 665 |
+
- Treat `/predict` (embeddings) and `/forecast` as **separate models** β they may have different patch sizes / embedding dims. Read the per-role `models` map from `client.model_info()` instead of assuming they match.
|
| 666 |
+
|
| 667 |
+
#### Training on top of CHARM
|
| 668 |
+
- Precompute embeddings once (`precompute_dataset_embeddings` + `PrecomputedEmbeddingsDataset`) β toolkit models otherwise call the API every forward pass.
|
| 669 |
+
|
| 670 |
+
#### Reliability & ops
|
| 671 |
+
- Catch specific errors (`InvalidRequestError`, `AuthenticationError`, `RateLimitError`, or the base `CharmError`) and back off on rate limits.
|
| 672 |
+
- Use the context manager (`with CharmClient(...) as client:`) so local deployments shut down cleanly.
|
| 673 |
+
- Set credentials via env (`CHARM_API_KEY`, `CHARM_BASE_URL`) rather than hardcoding.
|
| 674 |
+
|
| 675 |
+
---
|
| 676 |
+
|
| 677 |
+
### Decision guide
|
| 678 |
+
|
| 679 |
+
#### When to use CHARM
|
| 680 |
+
|
| 681 |
+
- Multivariate time series (multiple channels measured over time)
|
| 682 |
+
- Each window has at least a few patches (patch size is 16, so ~48+ timesteps is a good floor)
|
| 683 |
+
- You want a strong starting point without feature engineering
|
| 684 |
+
|
| 685 |
+
#### When to use classical methods instead
|
| 686 |
+
|
| 687 |
+
- Tabular data without a time dimension β use LightGBM, XGBoost
|
| 688 |
+
- Very short series (< 10 timesteps)
|
| 689 |
+
- Single scalar features β still works but may not outperform ARIMA/ETS
|
| 690 |
+
|
| 691 |
+
#### Zero-shot vs fine-tuned
|
| 692 |
+
|
| 693 |
+
| Approach | When | Effort |
|
| 694 |
+
|---|---|---|
|
| 695 |
+
| `prediction.create(target_len=H)` | Quick forecast baseline, no labeled data | None β one API call |
|
| 696 |
+
| Embeddings + sklearn | Moderate data, combine with other features | Minutes |
|
| 697 |
+
| Embeddings + kNN (retrieval/AD) | Unlabeled anomaly detection or search | Minutes |
|
| 698 |
+
| Toolkit model (Reconstruction/Forecasting/Classification) | Have labeled data, want best performance | Train a small head (~minutes on CPU) |
|
| 699 |
+
|
| 700 |
+
---
|
| 701 |
+
|
| 702 |
## GIFT-Eval results
|
| 703 |
|
| 704 |
Evaluated zero-shot on the full GIFT-Eval benchmark (97 dataset/frequency/term
|