Restructure model card: pitch, access, skill, notebooks, GIFT-Eval, paper
Browse files
README.md
CHANGED
|
@@ -6,759 +6,151 @@ tags:
|
|
| 6 |
- foundation-model
|
| 7 |
- zero-shot
|
| 8 |
- gift-eval
|
|
|
|
| 9 |
pipeline_tag: time-series-forecasting
|
| 10 |
library_name: pytorch
|
| 11 |
---
|
| 12 |
|
| 13 |
# CHARM
|
| 14 |
|
| 15 |
-
CHARM
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
benchmark — no GIFT-Eval data is used in training.
|
| 19 |
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
|
|
|
| 24 |
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
| | |
|
| 28 |
-
|---|---|
|
| 29 |
-
| Parameters | ~63.3M (encoder ~59M) |
|
| 30 |
-
| Hidden size (d_model) | 384 |
|
| 31 |
-
| Projection | TCN-pool (patch size 16, causal conv stack + residual gate) |
|
| 32 |
-
| Backbone | Transformer encoder with RoPE attention |
|
| 33 |
-
| Decoder | Quantile decoder, 99 levels (0.01–0.99) |
|
| 34 |
-
| Max context length | 8192 |
|
| 35 |
-
| Output | Probabilistic (quantile) forecasts, multivariate-capable |
|
| 36 |
-
| Precision | float32 |
|
| 37 |
-
|
| 38 |
-
## Intended use
|
| 39 |
-
|
| 40 |
-
Zero-shot probabilistic forecasting of univariate and multivariate time series
|
| 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 |
-
|
|
|
|
| 45 |
|
| 46 |
-
|
| 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 |
-
### Example notebooks
|
| 51 |
-
|
| 52 |
-
Runnable notebooks live in [`notebooks/`](./notebooks) in this repo. Open them on the Hub, or download the folder and run locally after `pip install c3-charm[toolkit]` (set `CHARM_BASE_URL` / `CHARM_API_KEY` first). The classification and reconstruction/forecasting demos read the small sample datasets in [`notebooks/data/`](./notebooks/data).
|
| 53 |
-
|
| 54 |
-
| Notebook | What it covers |
|
| 55 |
-
|---|---|
|
| 56 |
-
| [`getting_started.ipynb`](./notebooks/getting_started.ipynb) | Client setup, first embeddings call, inspecting `model_info()`. |
|
| 57 |
-
| [`charm_toolkit_demo.ipynb`](./notebooks/charm_toolkit_demo.ipynb) | Tour of the `charm_toolkit` — datasets, precompute, trainer, and each task head. |
|
| 58 |
-
| [`demo_forecasting.ipynb`](./notebooks/demo_forecasting.ipynb) | Zero-shot quantile forecasting and the embedding-based `ForecastingModel` head (weather data). |
|
| 59 |
-
| [`demo_reconstruction.ipynb`](./notebooks/demo_reconstruction.ipynb) | Backcast reconstruction and the `ReconstructionModel` head for anomaly detection (weather data). |
|
| 60 |
-
| [`demo_classification.ipynb`](./notebooks/demo_classification.ipynb) | Time-series classification with the `ClassificationModel` head (BasicMotions data). |
|
| 61 |
-
| [`demo_retrieval_anomaly_detection.ipynb`](./notebooks/demo_retrieval_anomaly_detection.ipynb) | Embedding retrieval + kNN / zero-shot anomaly-detection recipes. |
|
| 62 |
-
|
| 63 |
-
### Installation
|
| 64 |
|
| 65 |
```bash
|
| 66 |
-
pip install c3-charm
|
| 67 |
-
pip install c3-charm[toolkit] # includes PyTorch models, datasets, trainers
|
| 68 |
```
|
| 69 |
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
```
|
| 73 |
-
|
| 74 |
-
cd c3-charm
|
| 75 |
-
poetry install # core SDK only
|
| 76 |
-
poetry install --with toolkit # include toolkit dependencies
|
| 77 |
-
```
|
| 78 |
|
| 79 |
-
##
|
| 80 |
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
GPU — the model runs server-side, and toolkit training heads are small
|
| 84 |
-
enough to fit on CPU — so if you want the smaller CPU-only wheel, install
|
| 85 |
-
torch from the CPU index **before** `c3-charm`:
|
| 86 |
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
pip install c3-charm
|
| 90 |
-
```
|
| 91 |
|
| 92 |
-
|
| 93 |
|
| 94 |
-
|
| 95 |
-
|
| 96 |
|
| 97 |
```bash
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
```
|
| 101 |
-
|
| 102 |
-
Swap `cu124` for the CUDA version that matches your driver (`cu121`,
|
| 103 |
-
`cu126`, `cu128`, …). `--no-deps` is important — it stops pip from
|
| 104 |
-
re-resolving your torch install.
|
| 105 |
-
|
| 106 |
-
---
|
| 107 |
-
|
| 108 |
-
### Core SDK
|
| 109 |
-
|
| 110 |
-
#### Client initialization
|
| 111 |
-
|
| 112 |
-
```python
|
| 113 |
-
from charm import CharmClient
|
| 114 |
-
|
| 115 |
-
client = CharmClient(
|
| 116 |
-
base_url="http://your-server:8080",
|
| 117 |
-
api_key="your-api-key", # or set CHARM_API_KEY env var
|
| 118 |
-
timeout=300, # override; SDK default is 15s — raise it for /forecast (server allows up to ~220s)
|
| 119 |
-
max_retries=3,
|
| 120 |
-
)
|
| 121 |
-
```
|
| 122 |
-
|
| 123 |
-
#### Embeddings — `client.embeddings.create()`
|
| 124 |
-
|
| 125 |
-
Converts time series windows into dense vectors.
|
| 126 |
-
|
| 127 |
-
```python
|
| 128 |
-
response = client.embeddings.create(
|
| 129 |
-
descriptions=[["sensor_A", "sensor_B"]], # (N, C) channel names
|
| 130 |
-
ts_array=[[[1.0, 2.0], [1.1, 2.1], ...]], # (N, T, C) values
|
| 131 |
-
batch_size=32,
|
| 132 |
-
return_tensors="np", # "list", "np", or "torch"
|
| 133 |
-
aggregate=True, # True → (N, D); False → (N, T_, C, D)
|
| 134 |
-
progress=True,
|
| 135 |
-
)
|
| 136 |
-
embeddings = response.embeds # shape (N, D) when aggregate=True
|
| 137 |
-
```
|
| 138 |
-
|
| 139 |
-
**`aggregate` parameter:**
|
| 140 |
-
- `True` (default): Returns flattened embeddings `(N, D)` — one vector per series. Best for retrieval, classification, clustering.
|
| 141 |
-
- `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.
|
| 142 |
-
|
| 143 |
-
**Async** (faster for large datasets):
|
| 144 |
-
|
| 145 |
-
```python
|
| 146 |
-
response = await client.embeddings.async_create(
|
| 147 |
-
descriptions=descriptions,
|
| 148 |
-
ts_array=ts_array,
|
| 149 |
-
max_B_per_request=32,
|
| 150 |
-
concurrency_per_call=8,
|
| 151 |
-
return_tensors="np",
|
| 152 |
-
aggregate=True,
|
| 153 |
-
)
|
| 154 |
-
```
|
| 155 |
-
|
| 156 |
-
#### Forecast / Backcast — `client.prediction.create()`
|
| 157 |
-
|
| 158 |
-
Zero-shot quantile predictions — no training required.
|
| 159 |
-
|
| 160 |
-
```python
|
| 161 |
-
response = client.prediction.create(
|
| 162 |
-
descriptions=[["sensor_A", "sensor_B"]],
|
| 163 |
-
ts_array=[[[1.0, 2.0], [1.1, 2.1], ...]],
|
| 164 |
-
target_len=10, # positive = forecast, negative = backcast
|
| 165 |
-
return_tensors="np",
|
| 166 |
-
)
|
| 167 |
-
forecast = response.denormalized_predictions # (N, target_len=10, C, Q) — Q quantiles (model-dependent, e.g. 21)
|
| 168 |
-
median = response.median # (N, 10, C) — point forecast (median quantile)
|
| 169 |
-
```
|
| 170 |
-
|
| 171 |
-
**Backcast** (reconstruct past values):
|
| 172 |
-
|
| 173 |
-
```python
|
| 174 |
-
response = client.prediction.create(
|
| 175 |
-
descriptions=descriptions,
|
| 176 |
-
ts_array=ts_array,
|
| 177 |
-
target_len=-8, # reconstruct last 8 steps
|
| 178 |
-
return_tensors="np",
|
| 179 |
-
)
|
| 180 |
-
```
|
| 181 |
-
|
| 182 |
-
#### Input constraints
|
| 183 |
-
|
| 184 |
-
| Constraint | Limit |
|
| 185 |
-
|---|---|
|
| 186 |
-
| Timesteps per series | 1 ≤ T ≤ 8192 (the model's training window) |
|
| 187 |
-
| Channels per series | No hard limit (large C grows memory ~O(C²) under cross-channel attention) |
|
| 188 |
-
| Per-request size | N × C × T ≤ 500,000 — SDK client-side batching guard, not a server limit |
|
| 189 |
-
| Batch consistency | All series in a request must share the same T and C |
|
| 190 |
-
| Best accuracy | T ≤ 8192 (the model's training window); T need not be a multiple of the patch size |
|
| 191 |
-
|
| 192 |
-
The model was trained on windows of up to 8192 timesteps (patch size 16).
|
| 193 |
-
Inputs are not required to be a multiple of the patch size — they are padded
|
| 194 |
-
to a patch boundary internally. The architecture can technically accept more
|
| 195 |
-
than 8192 (up to 1500 patches), but longer inputs rely on positions seen only
|
| 196 |
-
in pretraining, so quality degrades. Query `client.model_info()` for the
|
| 197 |
-
served model's actual patch size and embedding dimension.
|
| 198 |
-
|
| 199 |
-
The SDK handles client-side batching automatically when you set `batch_size` (sync) or `max_B_per_request` (async).
|
| 200 |
-
|
| 201 |
-
#### Output shapes
|
| 202 |
-
|
| 203 |
-
| Method | Output field | Shape |
|
| 204 |
-
|---|---|---|
|
| 205 |
-
| `embeddings.create(aggregate=True)` | `response.embeds` | (N, D) |
|
| 206 |
-
| `embeddings.create(aggregate=False)` | `response.embeds` | (N, T_, C, D), T_ = ceil(T / patch_size) |
|
| 207 |
-
| `prediction.create(target_len > 0)` | `response.denormalized_predictions` | (N, target_len, C, Q) |
|
| 208 |
-
| `prediction.create(target_len < 0)` | `response.denormalized_predictions` | (N, abs(target_len), C, Q) |
|
| 209 |
-
| `prediction.create(...)` | `response.median` | (N, abs(target_len), C) |
|
| 210 |
-
|
| 211 |
-
#### Channel descriptions
|
| 212 |
-
|
| 213 |
-
Descriptions are **required** and affect embedding quality. They tell the model what each channel represents.
|
| 214 |
-
|
| 215 |
-
**Good descriptions** — use meaningful, consistent names:
|
| 216 |
-
```python
|
| 217 |
-
descriptions = [["engine_temperature", "oil_pressure", "rpm"]]
|
| 218 |
-
```
|
| 219 |
-
|
| 220 |
-
**Acceptable** — short but informative:
|
| 221 |
-
```python
|
| 222 |
-
descriptions = [["temp", "pressure", "speed"]]
|
| 223 |
-
```
|
| 224 |
-
|
| 225 |
-
**Avoid** — generic or positional names reduce model effectiveness:
|
| 226 |
-
```python
|
| 227 |
-
descriptions = [["col_0", "col_1", "col_2"]] # works but suboptimal
|
| 228 |
-
```
|
| 229 |
-
|
| 230 |
-
When working with pandas DataFrames, use column names directly:
|
| 231 |
-
```python
|
| 232 |
-
descriptions = [df.columns.tolist()] * N
|
| 233 |
-
```
|
| 234 |
-
|
| 235 |
-
#### Scaling
|
| 236 |
-
|
| 237 |
-
**No pre-processing needed.** CHARM normalizes internally. Send raw data as-is. Do not apply StandardScaler, MinMaxScaler, or log transforms before calling the API.
|
| 238 |
-
|
| 239 |
-
#### Error handling
|
| 240 |
-
|
| 241 |
-
```python
|
| 242 |
-
from charm import CharmError, AuthenticationError, InvalidRequestError, RateLimitError
|
| 243 |
-
|
| 244 |
-
try:
|
| 245 |
-
response = client.embeddings.create(...)
|
| 246 |
-
except AuthenticationError:
|
| 247 |
-
# bad API key
|
| 248 |
-
except InvalidRequestError as e:
|
| 249 |
-
# shape violations, empty input
|
| 250 |
-
except RateLimitError:
|
| 251 |
-
# back off and retry
|
| 252 |
-
except CharmError as e:
|
| 253 |
-
# catch-all for other SDK errors
|
| 254 |
-
```
|
| 255 |
-
|
| 256 |
-
---
|
| 257 |
-
|
| 258 |
-
### Toolkit — Downstream Tasks
|
| 259 |
-
|
| 260 |
-
The toolkit (`pip install c3-charm[toolkit]`) provides PyTorch models, dataset utilities, and training infrastructure for fine-tuning on top of CHARM embeddings.
|
| 261 |
-
|
| 262 |
-
#### Retrieval — `charm_toolkit.retrieval`
|
| 263 |
-
|
| 264 |
-
Find similar time series by embedding similarity.
|
| 265 |
-
|
| 266 |
-
```python
|
| 267 |
-
from charm_toolkit.retrieval import (
|
| 268 |
-
l2_normalize,
|
| 269 |
-
cosine_similarity_matrix,
|
| 270 |
-
knn_search,
|
| 271 |
-
retrieval_metrics,
|
| 272 |
-
)
|
| 273 |
-
|
| 274 |
-
# Embed your data
|
| 275 |
-
response = client.embeddings.create(
|
| 276 |
-
descriptions=descriptions,
|
| 277 |
-
ts_array=windows_list,
|
| 278 |
-
return_tensors="np",
|
| 279 |
-
)
|
| 280 |
-
embeddings = response.embeds # (N, D)
|
| 281 |
-
|
| 282 |
-
# Similarity search
|
| 283 |
-
sim = cosine_similarity_matrix(embeddings, embeddings)
|
| 284 |
-
|
| 285 |
-
# kNN search
|
| 286 |
-
indices, scores = knn_search(query_emb, corpus_emb, k=5)
|
| 287 |
-
|
| 288 |
-
# Evaluation metrics
|
| 289 |
-
metrics = retrieval_metrics(
|
| 290 |
-
query_emb=query_emb,
|
| 291 |
-
corpus_emb=corpus_emb,
|
| 292 |
-
query_labels=query_labels,
|
| 293 |
-
corpus_labels=corpus_labels,
|
| 294 |
-
k_values=[1, 3, 5, 10],
|
| 295 |
-
exclude_self=True,
|
| 296 |
-
query_ids=query_dataset_names,
|
| 297 |
-
corpus_ids=corpus_dataset_names,
|
| 298 |
-
)
|
| 299 |
-
# Returns: precision@k, ndcg@k, hit_rate@k
|
| 300 |
-
```
|
| 301 |
-
|
| 302 |
-
#### Anomaly Detection — `charm_toolkit.anomaly_detection`
|
| 303 |
-
|
| 304 |
-
Detect anomalies via kNN distance scoring on windowed CHARM embeddings.
|
| 305 |
-
|
| 306 |
-
```python
|
| 307 |
-
from charm_toolkit.anomaly_detection import (
|
| 308 |
-
sliding_window_embeddings,
|
| 309 |
-
knn_anomaly_scores,
|
| 310 |
-
window_scores_to_pointwise,
|
| 311 |
-
)
|
| 312 |
-
|
| 313 |
-
# 1. Embed sliding windows
|
| 314 |
-
train_emb = sliding_window_embeddings(
|
| 315 |
-
client, train_data, descriptions,
|
| 316 |
-
window_size=128, stride=1, batch_size=64,
|
| 317 |
-
)
|
| 318 |
-
test_emb = sliding_window_embeddings(
|
| 319 |
-
client, test_data, descriptions,
|
| 320 |
-
window_size=128, stride=1, batch_size=64,
|
| 321 |
-
)
|
| 322 |
-
|
| 323 |
-
# 2. Score test windows by distance to train
|
| 324 |
-
window_scores = knn_anomaly_scores(
|
| 325 |
-
test_emb=test_emb,
|
| 326 |
-
reference_emb=train_emb,
|
| 327 |
-
k=5,
|
| 328 |
-
distance="cosine", # "cosine", "l2", "l1"
|
| 329 |
-
aggregation="mean", # "mean", "max"
|
| 330 |
-
)
|
| 331 |
-
|
| 332 |
-
# 3. Aggregate to per-timestep scores
|
| 333 |
-
pointwise_scores = window_scores_to_pointwise(
|
| 334 |
-
window_scores=window_scores,
|
| 335 |
-
window_size=128,
|
| 336 |
-
stride=1,
|
| 337 |
-
total_length=len(test_data),
|
| 338 |
-
method="mean", # "mean", "max", "last", "center"
|
| 339 |
-
)
|
| 340 |
```
|
| 341 |
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
Each timestep is covered by multiple overlapping windows. The `method` parameter controls how to assign a single score per timestep:
|
| 345 |
-
|
| 346 |
-
| Method | Behavior | Use case |
|
| 347 |
-
|--------|----------|----------|
|
| 348 |
-
| `"mean"` | Average of all windows covering the point | Smooth, best for offline evaluation |
|
| 349 |
-
| `"max"` | Max score among covering windows | Conservative, catches isolated spikes |
|
| 350 |
-
| `"last"` | Score of the most recently *completed* window | Online/streaming — score only updates when a window finishes processing |
|
| 351 |
-
| `"center"` | Score of the window centered on each point | Minimal time-shift, tightest temporal alignment |
|
| 352 |
-
|
| 353 |
-
**Zero-shot recipes (no clean reference set required) — recommended methods, in order of strength:**
|
| 354 |
-
|
| 355 |
-
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.
|
| 356 |
-
2. **CBLOF**: `pyod.models.cblof.CBLOF` on the embedding matrix.
|
| 357 |
-
3. **IsolationForest**: `sklearn.ensemble.IsolationForest` directly on the embedding matrix.
|
| 358 |
-
|
| 359 |
-
L2-normalize embeddings beforehand to use cosine geometry. See `demo_retrieval_anomaly_detection.ipynb`.
|
| 360 |
-
|
| 361 |
-
**Best practices (validated on TSB-AD, metric VUS-PR):**
|
| 362 |
-
|
| 363 |
-
1. **Ensemble the embedding score with per-window statistics.** The encoder
|
| 364 |
-
instance-normalizes each window, erasing amplitude/level-shift anomalies (spikes,
|
| 365 |
-
steps — the most common kind) from the embedding. Recover them with
|
| 366 |
-
`window_statistics` (no extra model call) and combine via `ensemble_scores`
|
| 367 |
-
(`method="zscore"` — z-score each detector and sum; parameter-free and as strong as a
|
| 368 |
-
tuned weighted ensemble). This is worth **~+7 pp VUS-PR** supervised.
|
| 369 |
-
|
| 370 |
-
```python
|
| 371 |
-
from charm_toolkit.anomaly_detection import window_statistics, ensemble_scores
|
| 372 |
-
|
| 373 |
-
emb_score = knn_anomaly_scores(test_emb, train_emb, k=3, distance="cosine")
|
| 374 |
-
S_tr = window_statistics(train_data, 128, 1)
|
| 375 |
-
mu, sd = S_tr.mean(0, keepdims=True), S_tr.std(0, keepdims=True) + 1e-8
|
| 376 |
-
stats_score = knn_anomaly_scores(
|
| 377 |
-
(window_statistics(test_data, 128, 1) - mu) / sd, (S_tr - mu) / sd,
|
| 378 |
-
k=3, distance="l2")
|
| 379 |
-
window_scores = ensemble_scores([emb_score, stats_score], method="zscore")
|
| 380 |
-
# zero-shot: build the reference by bootstrap (above) and use method="rank"
|
| 381 |
-
```
|
| 382 |
-
|
| 383 |
-
2. **Multivariate: do NOT pool channels when the channel count is high.** Channel-mean
|
| 384 |
-
pooling dilutes an anomaly confined to a few channels across all of them. Keep channels
|
| 385 |
-
separate with `sliding_window_channel_embeddings` + `per_channel_knn_scores`
|
| 386 |
-
(`channel_pool="adaptive"`). The benefit grows with the number of channels — negligible
|
| 387 |
-
at C≤3, sizeable at C≥20, large at C>60 — so it is **advisable whenever C is high**. For
|
| 388 |
-
few channels, plain mean-pooling is equally good and cheaper.
|
| 389 |
-
|
| 390 |
-
```python
|
| 391 |
-
from charm_toolkit.anomaly_detection import (
|
| 392 |
-
sliding_window_channel_embeddings, per_channel_knn_scores)
|
| 393 |
-
|
| 394 |
-
train_pc = sliding_window_channel_embeddings(client, train_data, descriptions) # (N, C, D)
|
| 395 |
-
test_pc = sliding_window_channel_embeddings(client, test_data, descriptions)
|
| 396 |
-
emb_score = per_channel_knn_scores(test_pc, train_pc, k=3, channel_pool="adaptive")
|
| 397 |
-
```
|
| 398 |
-
|
| 399 |
-
#### ReconstructionModel — anomaly detection via learned head
|
| 400 |
-
|
| 401 |
-
```python
|
| 402 |
-
from charm_toolkit import (
|
| 403 |
-
ReconstructionModel, create_reconstruction_datasets,
|
| 404 |
-
collator, TrainerClass,
|
| 405 |
-
)
|
| 406 |
-
from torch.utils.data import DataLoader
|
| 407 |
-
import torch.nn as nn
|
| 408 |
-
|
| 409 |
-
train_ds, val_ds, test_ds = create_reconstruction_datasets(
|
| 410 |
-
raw_data, # (T, C) numpy array or torch tensor
|
| 411 |
-
descriptions=channel_names,
|
| 412 |
-
window_size=256,
|
| 413 |
-
stride=1,
|
| 414 |
-
train_ratio=0.7,
|
| 415 |
-
val_ratio=0.15,
|
| 416 |
-
sequential=True,
|
| 417 |
-
scale=True,
|
| 418 |
-
)
|
| 419 |
-
|
| 420 |
-
model = ReconstructionModel(
|
| 421 |
-
embedding_client=client,
|
| 422 |
-
reconstructor="linear", # "linear", "mlp", or custom nn.Module
|
| 423 |
-
hidden_dim=128,
|
| 424 |
-
dropout=0.1,
|
| 425 |
-
)
|
| 426 |
-
|
| 427 |
-
trainer = TrainerClass(
|
| 428 |
-
model=model,
|
| 429 |
-
train_loader=DataLoader(train_ds, batch_size=512, collate_fn=collator),
|
| 430 |
-
val_loader=DataLoader(val_ds, batch_size=512, collate_fn=collator),
|
| 431 |
-
epochs=1000,
|
| 432 |
-
patience=5,
|
| 433 |
-
lr=1e-3,
|
| 434 |
-
criterion=nn.HuberLoss(),
|
| 435 |
-
)
|
| 436 |
-
trainer.fit()
|
| 437 |
-
```
|
| 438 |
-
|
| 439 |
-
#### ForecastingModel — embedding-based forecasting
|
| 440 |
-
|
| 441 |
-
```python
|
| 442 |
-
from charm_toolkit import ForecastingModel, create_forecasting_datasets, collator, TrainerClass
|
| 443 |
-
from torch.utils.data import DataLoader
|
| 444 |
-
|
| 445 |
-
train_ds, val_ds, test_ds = create_forecasting_datasets(
|
| 446 |
-
raw_data,
|
| 447 |
-
descriptions=channel_names,
|
| 448 |
-
train_horizon=96,
|
| 449 |
-
test_horizon=96,
|
| 450 |
-
train_ratio=0.7,
|
| 451 |
-
val_ratio=0.15,
|
| 452 |
-
sequential=True,
|
| 453 |
-
scale=True,
|
| 454 |
-
)
|
| 455 |
-
|
| 456 |
-
model = ForecastingModel(
|
| 457 |
-
embedding_client=client,
|
| 458 |
-
horizon=96,
|
| 459 |
-
input_size=96,
|
| 460 |
-
head="linear",
|
| 461 |
-
hidden_dim=128,
|
| 462 |
-
mode="last", # "last", "avg", "none"
|
| 463 |
-
per_channel=True,
|
| 464 |
-
num_channels=len(channel_names),
|
| 465 |
-
)
|
| 466 |
-
|
| 467 |
-
trainer = TrainerClass(
|
| 468 |
-
model=model,
|
| 469 |
-
train_loader=DataLoader(train_ds, batch_size=512, collate_fn=collator),
|
| 470 |
-
val_loader=DataLoader(val_ds, batch_size=512, collate_fn=collator),
|
| 471 |
-
epochs=1000,
|
| 472 |
-
patience=10,
|
| 473 |
-
lr=1e-2,
|
| 474 |
-
)
|
| 475 |
-
trainer.fit()
|
| 476 |
-
```
|
| 477 |
-
|
| 478 |
-
#### ClassificationModel — time series classification
|
| 479 |
-
|
| 480 |
-
```python
|
| 481 |
-
from charm_toolkit import ClassificationModel, create_classification_datasets, collator, TrainerClass
|
| 482 |
-
from torch.utils.data import DataLoader
|
| 483 |
-
import torch.nn as nn
|
| 484 |
-
|
| 485 |
-
train_ds, val_ds, test_ds = create_classification_datasets(
|
| 486 |
-
raw_data, # (N, T, C)
|
| 487 |
-
labels=labels, # list of N integer labels
|
| 488 |
-
descriptions=channel_names,
|
| 489 |
-
train_ratio=0.7,
|
| 490 |
-
val_ratio=0.15,
|
| 491 |
-
)
|
| 492 |
-
|
| 493 |
-
model = ClassificationModel(
|
| 494 |
-
embedding_client=client,
|
| 495 |
-
num_classes=num_classes,
|
| 496 |
-
hidden_dim=128,
|
| 497 |
-
pooling_over_t="mean",
|
| 498 |
-
pooling_over_channels="mean",
|
| 499 |
-
classifier_type="mlp",
|
| 500 |
-
)
|
| 501 |
-
|
| 502 |
-
trainer = TrainerClass(
|
| 503 |
-
model=model,
|
| 504 |
-
train_loader=DataLoader(train_ds, batch_size=32, collate_fn=collator),
|
| 505 |
-
val_loader=DataLoader(val_ds, batch_size=32, collate_fn=collator),
|
| 506 |
-
epochs=100,
|
| 507 |
-
patience=10,
|
| 508 |
-
lr=1e-3,
|
| 509 |
-
criterion=nn.CrossEntropyLoss(),
|
| 510 |
-
)
|
| 511 |
-
trainer.fit()
|
| 512 |
-
```
|
| 513 |
-
|
| 514 |
-
#### Precomputing embeddings (critical for training)
|
| 515 |
-
|
| 516 |
-
Toolkit models call the API every forward pass. For training with hundreds of windows per epoch, **precompute embeddings once**:
|
| 517 |
-
|
| 518 |
-
```python
|
| 519 |
-
from charm_toolkit import precompute_dataset_embeddings, PrecomputedEmbeddingsDataset
|
| 520 |
-
|
| 521 |
-
# Compute once, save to disk as memmap
|
| 522 |
-
train_shape = precompute_dataset_embeddings(
|
| 523 |
-
client=client, dataset=train_ds,
|
| 524 |
-
output_path="./outputs/train_embeddings.pt", memory_batch_size=8192
|
| 525 |
-
)
|
| 526 |
-
val_shape = precompute_dataset_embeddings(
|
| 527 |
-
client=client, dataset=val_ds,
|
| 528 |
-
output_path="./outputs/val_embeddings.pt", memory_batch_size=8192
|
| 529 |
-
)
|
| 530 |
-
|
| 531 |
-
# Wrap datasets — model skips API calls when "embeds" key present
|
| 532 |
-
train_ds = PrecomputedEmbeddingsDataset(train_ds, "./outputs/train_embeddings.pt", train_shape)
|
| 533 |
-
val_ds = PrecomputedEmbeddingsDataset(val_ds, "./outputs/val_embeddings.pt", val_shape)
|
| 534 |
-
|
| 535 |
-
# Training now uses cached embeddings — orders of magnitude faster
|
| 536 |
-
train_loader = DataLoader(train_ds, batch_size=512, shuffle=True, collate_fn=collator)
|
| 537 |
-
```
|
| 538 |
-
|
| 539 |
-
#### Trainer API
|
| 540 |
-
|
| 541 |
-
```python
|
| 542 |
-
from charm_toolkit import TrainerClass
|
| 543 |
-
|
| 544 |
-
trainer = TrainerClass(
|
| 545 |
-
model=model,
|
| 546 |
-
train_loader=train_loader,
|
| 547 |
-
val_loader=val_loader,
|
| 548 |
-
test_loader=test_loader, # optional
|
| 549 |
-
lr=1e-3,
|
| 550 |
-
weight_decay=1e-4,
|
| 551 |
-
epochs=1000,
|
| 552 |
-
patience=5,
|
| 553 |
-
min_delta=1e-4,
|
| 554 |
-
max_grad_norm=5.0,
|
| 555 |
-
criterion=None, # defaults to MSELoss
|
| 556 |
-
)
|
| 557 |
-
trainer.fit()
|
| 558 |
-
test_loss = trainer.evaluate(test_loader)
|
| 559 |
-
```
|
| 560 |
-
|
| 561 |
-
#### Dataset factory functions
|
| 562 |
-
|
| 563 |
-
All return `(train_dataset, val_dataset, test_dataset)`:
|
| 564 |
-
|
| 565 |
-
| Function | Input shape | Key args |
|
| 566 |
-
|---|---|---|
|
| 567 |
-
| `create_reconstruction_datasets(raw_data, ...)` | (T, C) | `window_size`, `stride`, `train_ratio`, `val_ratio` |
|
| 568 |
-
| `create_forecasting_datasets(raw_data, ...)` | (T, C) | `train_horizon`, `test_horizon`, `stride`, `train_ratio`, `val_ratio` |
|
| 569 |
-
| `create_classification_datasets(raw_data, labels, ...)` | (N, T, C) | `train_ratio`, `val_ratio` |
|
| 570 |
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
#### collator
|
| 574 |
-
|
| 575 |
-
All DataLoaders using toolkit datasets require `collator` as the `collate_fn`:
|
| 576 |
-
|
| 577 |
-
```python
|
| 578 |
-
from charm_toolkit import collator
|
| 579 |
-
# or equivalently:
|
| 580 |
-
from charm_toolkit.Datasets import collator
|
| 581 |
-
```
|
| 582 |
-
|
| 583 |
-
---
|
| 584 |
-
|
| 585 |
-
### Embeddings as features
|
| 586 |
-
|
| 587 |
-
CHARM embeddings work as drop-in feature vectors for any sklearn model:
|
| 588 |
-
|
| 589 |
-
```python
|
| 590 |
-
import numpy as np
|
| 591 |
-
from sklearn.ensemble import IsolationForest
|
| 592 |
-
from sklearn.linear_model import LogisticRegression
|
| 593 |
-
from charm_toolkit.retrieval import cosine_similarity_matrix
|
| 594 |
-
|
| 595 |
-
response = client.embeddings.create(
|
| 596 |
-
descriptions=descriptions,
|
| 597 |
-
ts_array=windows_list,
|
| 598 |
-
return_tensors="np",
|
| 599 |
-
)
|
| 600 |
-
X = response.embeds # (N, D)
|
| 601 |
-
|
| 602 |
-
# Anomaly detection with isolation forest
|
| 603 |
-
clf = IsolationForest(contamination=0.05)
|
| 604 |
-
anomaly_labels = clf.fit_predict(X)
|
| 605 |
-
|
| 606 |
-
# Similarity search
|
| 607 |
-
sim = cosine_similarity_matrix(X, X)
|
| 608 |
-
|
| 609 |
-
# As features for any classifier
|
| 610 |
-
clf = LogisticRegression().fit(X_train, y_train)
|
| 611 |
-
```
|
| 612 |
-
|
| 613 |
-
---
|
| 614 |
|
| 615 |
-
|
|
|
|
| 616 |
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
``
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
When `tag` is provided:
|
| 626 |
-
1. Checks for GPU availability (falls back to CPU)
|
| 627 |
-
2. Clones repo at the specified tag (shallow clone)
|
| 628 |
-
3. Downloads model weights from the GitHub release
|
| 629 |
-
4. Launches the serving stack locally
|
| 630 |
-
5. Polls health endpoint until ready
|
| 631 |
-
|
| 632 |
-
Files cached at `~/.charm/models/<tag>/` for fast subsequent runs.
|
| 633 |
-
|
| 634 |
-
```python
|
| 635 |
-
CharmClient(
|
| 636 |
-
tag="experiment-tag", # required for local mode
|
| 637 |
-
repo_url="https://...", # default: c3-e/research
|
| 638 |
-
cache_dir="/path/to/cache", # default: ~/.charm/models
|
| 639 |
-
port=8080, # 0 = auto-select
|
| 640 |
-
)
|
| 641 |
-
```
|
| 642 |
-
|
| 643 |
-
---
|
| 644 |
-
|
| 645 |
-
### Best practices
|
| 646 |
-
|
| 647 |
-
#### Input & preprocessing
|
| 648 |
-
- 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.
|
| 649 |
-
- 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.
|
| 650 |
-
- **Do not pre-scale** your data. CHARM normalizes internally (asinh z-score); applying StandardScaler/MinMaxScaler/log yourself hurts results.
|
| 651 |
-
|
| 652 |
-
#### Channel descriptions (a real quality lever)
|
| 653 |
-
- Use meaningful, consistent channel names (`"engine_temperature"`, not `"col_0"`) — the model is channel-aware and descriptions materially affect embeddings.
|
| 654 |
-
- Reuse the same names across requests so embeddings stay comparable (retrieval, clustering).
|
| 655 |
-
|
| 656 |
-
#### Batching & throughput
|
| 657 |
-
- Respect the per-request budget: `batch_size × C × T ≤ 500,000` (SDK-enforced client-side).
|
| 658 |
-
- 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.
|
| 659 |
-
|
| 660 |
-
#### Timeouts & retries
|
| 661 |
-
- Raise `timeout` for forecasting — the SDK default is 15s, but the server allows forecasts up to ~220s. Use `timeout ≈ 220+` for `prediction.create`.
|
| 662 |
-
- Keep the built-in retries (exponential backoff on 429/5xx) rather than hand-rolling.
|
| 663 |
-
|
| 664 |
-
#### Embeddings
|
| 665 |
-
- `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.
|
| 666 |
-
- L2-normalize before cosine similarity (embeddings are unit-normed by the encoder, but normalize again after any pooling you do).
|
| 667 |
-
- Discover `D` at runtime via `client.model_info()` — it's model-dependent; don't hardcode.
|
| 668 |
-
|
| 669 |
-
#### Forecasting
|
| 670 |
-
- `target_len > 0` = forecast, `< 0` = backcast; `0` is invalid.
|
| 671 |
-
- Keep `T + abs(target_len) ≤ 8192` (the training window) for best forecast/backcast quality.
|
| 672 |
-
- 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]`.
|
| 673 |
-
|
| 674 |
-
#### Classification
|
| 675 |
-
- 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"`.
|
| 676 |
-
|
| 677 |
-
#### Dual-model awareness
|
| 678 |
-
- 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.
|
| 679 |
-
|
| 680 |
-
#### Training on top of CHARM
|
| 681 |
-
- Precompute embeddings once (`precompute_dataset_embeddings` + `PrecomputedEmbeddingsDataset`) — toolkit models otherwise call the API every forward pass.
|
| 682 |
-
|
| 683 |
-
#### Reliability & ops
|
| 684 |
-
- Catch specific errors (`InvalidRequestError`, `AuthenticationError`, `RateLimitError`, or the base `CharmError`) and back off on rate limits.
|
| 685 |
-
- Use the context manager (`with CharmClient(...) as client:`) so local deployments shut down cleanly.
|
| 686 |
-
- Set credentials via env (`CHARM_API_KEY`, `CHARM_BASE_URL`) rather than hardcoding.
|
| 687 |
-
|
| 688 |
-
---
|
| 689 |
-
|
| 690 |
-
### Decision guide
|
| 691 |
-
|
| 692 |
-
#### When to use CHARM
|
| 693 |
-
|
| 694 |
-
- Multivariate time series (multiple channels measured over time)
|
| 695 |
-
- Each window has at least a few patches (patch size is 16, so ~48+ timesteps is a good floor)
|
| 696 |
-
- You want a strong starting point without feature engineering
|
| 697 |
-
|
| 698 |
-
#### When to use classical methods instead
|
| 699 |
|
| 700 |
-
|
| 701 |
-
- Very short series (< 10 timesteps)
|
| 702 |
-
- Single scalar features — still works but may not outperform ARIMA/ETS
|
| 703 |
|
| 704 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 705 |
|
| 706 |
-
|
| 707 |
-
|---|---|---|
|
| 708 |
-
| `prediction.create(target_len=H)` | Quick forecast baseline, no labeled data | None — one API call |
|
| 709 |
-
| Embeddings + sklearn | Moderate data, combine with other features | Minutes |
|
| 710 |
-
| Embeddings + kNN (retrieval/AD) | Unlabeled anomaly detection or search | Minutes |
|
| 711 |
-
| Toolkit model (Reconstruction/Forecasting/Classification) | Have labeled data, want best performance | Train a small head (~minutes on CPU) |
|
| 712 |
|
| 713 |
-
-
|
|
|
|
|
|
|
| 714 |
|
| 715 |
## GIFT-Eval results
|
| 716 |
|
| 717 |
-
Evaluated zero-shot on the full GIFT-Eval benchmark (97 dataset/frequency/term
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
lower is better):
|
| 721 |
|
| 722 |
-
| Metric
|
| 723 |
-
|---|---|
|
| 724 |
-
| MASE
|
| 725 |
-
| CRPS (mean weighted sum quantile loss) | 0.4776
|
| 726 |
|
| 727 |
Per-term (geometric mean, normalized to Seasonal Naive):
|
| 728 |
|
| 729 |
-
| Term
|
| 730 |
-
|---|---|---|
|
| 731 |
-
| short
|
| 732 |
| medium | 0.7577 | 0.4452 |
|
| 733 |
-
| long
|
| 734 |
|
| 735 |
-
Scores are the geometric mean of per-config metric / Seasonal Naive across all
|
| 736 |
-
|
| 737 |
-
Full per-config results are in `all_results.csv`.
|
| 738 |
|
| 739 |
## Evaluation protocol
|
| 740 |
|
| 741 |
- Benchmark: GIFT-Eval, 97 configs (short / medium / long terms).
|
| 742 |
-
- Metrics: MSE[mean], MSE[0.5], MAE[0.5], MASE[0.5], MAPE[0.5], sMAPE[0.5],
|
| 743 |
-
|
| 744 |
-
|
| 745 |
- Context length: 8192; forecasts are full quantile distributions.
|
| 746 |
-
- Zero-shot: no GIFT-Eval train/test data is seen during pretraining
|
| 747 |
-
(`testdata_leakage = No`).
|
| 748 |
|
| 749 |
## Limitations
|
| 750 |
|
| 751 |
-
- Forecast quality varies by domain and horizon; very long horizons and highly
|
| 752 |
-
|
| 753 |
- Quantile calibration is learned and may drift on out-of-distribution scales.
|
| 754 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 755 |
## Citation
|
| 756 |
|
| 757 |
-
```
|
| 758 |
-
@
|
| 759 |
-
title
|
| 760 |
-
author
|
| 761 |
-
|
| 762 |
-
|
|
|
|
|
|
|
|
|
|
| 763 |
}
|
| 764 |
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
- foundation-model
|
| 7 |
- zero-shot
|
| 8 |
- gift-eval
|
| 9 |
+
- arxiv:2605.31580
|
| 10 |
pipeline_tag: time-series-forecasting
|
| 11 |
library_name: pytorch
|
| 12 |
---
|
| 13 |
|
| 14 |
# CHARM
|
| 15 |
|
| 16 |
+
CHARM (**CH**annel-**A**ware **R**epresentation **M**odel) is C3 AI's foundation model for
|
| 17 |
+
**multivariate time series**. It is served behind an API and consumed through the open-source
|
| 18 |
+
`c3-charm` Python SDK:
|
|
|
|
| 19 |
|
| 20 |
+
- **Embeddings**: turn `(T, C)` windows into dense vectors for search, clustering, anomaly
|
| 21 |
+
detection and features.
|
| 22 |
+
- **Zero-shot forecasting**: quantile forecasts and backcasts with no training data.
|
| 23 |
+
- **Toolkit** (`charm_toolkit`): anomaly-detection recipes, retrieval metrics, and small
|
| 24 |
+
trainable heads for forecasting, reconstruction and classification.
|
| 25 |
|
| 26 |
+
The model is channel-aware (channel names are part of the input) and normalizes internally, so you
|
| 27 |
+
send raw values. It runs server-side; the SDK is the client.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
+
> **Availability:** CHARM model weights and training/replication code are **not publicly
|
| 30 |
+
> released**. The model is served behind an API and consumed through the `c3-charm` SDK.
|
| 31 |
|
| 32 |
+
## Use it
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
```bash
|
| 35 |
+
pip install c3-charm # Python >= 3.10
|
|
|
|
| 36 |
```
|
| 37 |
|
| 38 |
+
Full SDK documentation (install, data format, client, embeddings, forecasting, anomaly detection,
|
| 39 |
+
trained heads, HTTP API) ships inside the package — find it with
|
| 40 |
+
`python -c "import charm.docs as d; print(d.get_docs_path())"` and start at `index.md`.
|
| 41 |
+
Package: [c3-charm on PyPI](https://pypi.org/project/c3-charm/).
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
+
## Getting access
|
| 44 |
|
| 45 |
+
CHARM is served behind an API. Set `CHARM_BASE_URL` and `CHARM_API_KEY` to point the SDK at an
|
| 46 |
+
endpoint. To obtain access, contact **gerardo.pastrana@c3.ai** and **dhruv.mehta@c3.ai**.
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
+
The model will also be available on **Azure AI Foundry** — deployment instructions will be added
|
| 49 |
+
here.
|
|
|
|
|
|
|
| 50 |
|
| 51 |
+
## Using an AI coding agent?
|
| 52 |
|
| 53 |
+
The **CHARM agent skill** teaches Claude Code, Cursor, Codex and others when and how to apply
|
| 54 |
+
CHARM. Install it:
|
| 55 |
|
| 56 |
```bash
|
| 57 |
+
claude plugin marketplace add https://huggingface.co/c3aiia3c/CHARM
|
| 58 |
+
claude plugin install charm@c3-charm
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
```
|
| 60 |
|
| 61 |
+
Other agents, or manual install: see [`skills/charm/README.md`](skills/charm/README.md).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
+
## Example notebooks
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
+
Runnable notebooks (with sample data) live in [`notebooks/`](notebooks). Run them locally after
|
| 66 |
+
`pip install "c3-charm[toolkit]"` (set `CHARM_BASE_URL` / `CHARM_API_KEY` first).
|
| 67 |
|
| 68 |
+
| Notebook | What it covers |
|
| 69 |
+
| --- | --- |
|
| 70 |
+
| [getting_started.ipynb](notebooks/getting_started.ipynb) | Client setup, first embeddings call, inspecting `model_info()` |
|
| 71 |
+
| [charm_toolkit_demo.ipynb](notebooks/charm_toolkit_demo.ipynb) | Tour of the toolkit: datasets, precompute, trainer, each task head |
|
| 72 |
+
| [demo_forecasting.ipynb](notebooks/demo_forecasting.ipynb) | Zero-shot forecasting and the embedding-based ForecastingModel head |
|
| 73 |
+
| [demo_reconstruction.ipynb](notebooks/demo_reconstruction.ipynb) | Backcast reconstruction and the ReconstructionModel head |
|
| 74 |
+
| [demo_classification.ipynb](notebooks/demo_classification.ipynb) | Time-series classification with the ClassificationModel head |
|
| 75 |
+
| [demo_retrieval_anomaly_detection.ipynb](notebooks/demo_retrieval_anomaly_detection.ipynb) | Embedding retrieval + kNN / zero-shot anomaly-detection recipes |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
+
## Model summary
|
|
|
|
|
|
|
| 78 |
|
| 79 |
+
| Parameters | ~63.3M (encoder ~59M) |
|
| 80 |
+
| ---------------------- | ----------------------------------------------------------- |
|
| 81 |
+
| Hidden size (d_model) | 384 |
|
| 82 |
+
| Projection | TCN-pool (patch size 16, causal conv stack + residual gate) |
|
| 83 |
+
| Backbone | Transformer encoder with RoPE attention |
|
| 84 |
+
| Decoder | Quantile decoder, 99 levels (0.01–0.99) |
|
| 85 |
+
| Max context length | 8192 |
|
| 86 |
+
| Output | Probabilistic (quantile) forecasts, multivariate-capable |
|
| 87 |
+
| Precision | float32 |
|
| 88 |
|
| 89 |
+
## Intended use
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
|
| 91 |
+
Zero-shot probabilistic forecasting of univariate and multivariate time series across domains
|
| 92 |
+
(energy, transport, sales, healthcare, nature, web/cloud-ops, econ/finance). The model is applied
|
| 93 |
+
without any per-dataset fine-tuning.
|
| 94 |
|
| 95 |
## GIFT-Eval results
|
| 96 |
|
| 97 |
+
Evaluated zero-shot on the full GIFT-Eval benchmark (97 dataset/frequency/term configurations)
|
| 98 |
+
using the standard 11-metric protocol. Aggregate scores (geometric mean of per-config metrics
|
| 99 |
+
normalized to the Seasonal Naive baseline; lower is better):
|
|
|
|
| 100 |
|
| 101 |
+
| Metric | Score (rel. Seasonal Naive) |
|
| 102 |
+
| -------------------------------------- | --------------------------- |
|
| 103 |
+
| MASE | 0.7582 |
|
| 104 |
+
| CRPS (mean weighted sum quantile loss) | 0.4776 |
|
| 105 |
|
| 106 |
Per-term (geometric mean, normalized to Seasonal Naive):
|
| 107 |
|
| 108 |
+
| Term | MASE | CRPS |
|
| 109 |
+
| ------ | ------ | ------ |
|
| 110 |
+
| short | 0.7463 | 0.5036 |
|
| 111 |
| medium | 0.7577 | 0.4452 |
|
| 112 |
+
| long | 0.7911 | 0.4460 |
|
| 113 |
|
| 114 |
+
Scores are the geometric mean of per-config metric / Seasonal Naive across all 97 GIFT-Eval
|
| 115 |
+
configurations (lower is better; < 1.0 beats Seasonal Naive).
|
|
|
|
| 116 |
|
| 117 |
## Evaluation protocol
|
| 118 |
|
| 119 |
- Benchmark: GIFT-Eval, 97 configs (short / medium / long terms).
|
| 120 |
+
- Metrics: MSE[mean], MSE[0.5], MAE[0.5], MASE[0.5], MAPE[0.5], sMAPE[0.5], MSIS, RMSE[mean],
|
| 121 |
+
NRMSE[mean], ND[0.5], mean_weighted_sum_quantile_loss (computed with gluonts
|
| 122 |
+
`evaluate_forecasts`).
|
| 123 |
- Context length: 8192; forecasts are full quantile distributions.
|
| 124 |
+
- Zero-shot: no GIFT-Eval train/test data is seen during pretraining (`testdata_leakage = No`).
|
|
|
|
| 125 |
|
| 126 |
## Limitations
|
| 127 |
|
| 128 |
+
- Forecast quality varies by domain and horizon; very long horizons and highly non-stationary
|
| 129 |
+
series remain challenging.
|
| 130 |
- Quantile calibration is learned and may drift on out-of-distribution scales.
|
| 131 |
|
| 132 |
+
## Paper
|
| 133 |
+
|
| 134 |
+
Utsav Dutta, Gerardo Pastrana, Sina Khoshfetrat Pakazad, Henrik Ohlsson.
|
| 135 |
+
*Giving Sensors a Voice: Multimodal JEPA for Semantic Time-Series Embeddings.* ICML 2026.
|
| 136 |
+
[arXiv:2605.31580](https://arxiv.org/abs/2605.31580) ·
|
| 137 |
+
[Hugging Face Papers](https://huggingface.co/papers/2605.31580)
|
| 138 |
+
|
| 139 |
## Citation
|
| 140 |
|
| 141 |
+
```bibtex
|
| 142 |
+
@inproceedings{dutta2026charm,
|
| 143 |
+
title = {Giving Sensors a Voice: Multimodal JEPA for Semantic Time-Series Embeddings},
|
| 144 |
+
author = {Dutta, Utsav and Pastrana, Gerardo and Khoshfetrat Pakazad, Sina and Ohlsson, Henrik},
|
| 145 |
+
booktitle = {Proceedings of the 43rd International Conference on Machine Learning (ICML)},
|
| 146 |
+
series = {Proceedings of Machine Learning Research},
|
| 147 |
+
volume = {306},
|
| 148 |
+
year = {2026},
|
| 149 |
+
url = {https://arxiv.org/abs/2605.31580},
|
| 150 |
}
|
| 151 |
```
|
| 152 |
+
|
| 153 |
+
## License
|
| 154 |
+
|
| 155 |
+
Apache License 2.0 for the SDK and skill. See [c3-charm on PyPI](https://pypi.org/project/c3-charm/).
|
| 156 |
+
Model weights are not released.
|