timee-in-context-tsc / timee /classifier.py
multimodalart's picture
multimodalart HF Staff
Upload folder using huggingface_hub
793c2b8 verified
Raw
History Blame Contribute Delete
8.07 kB
"""TimeeClassifier: end-to-end time series classification via in-context learning."""
from __future__ import annotations
from pathlib import Path
from typing import Callable, Union
import numpy as np
import torch
from sklearn.preprocessing import LabelEncoder
from timee.model.model import TimeeModel
from timee.transforms import default_ensemble_transforms
EnsembleTransform = Callable[[np.ndarray, np.ndarray], tuple[np.ndarray, np.ndarray]]
def _infer_device() -> torch.device:
if torch.cuda.is_available():
return torch.device("cuda")
if torch.backends.mps.is_available():
return torch.device("mps")
return torch.device("cpu")
class TimeeClassifier:
"""Pretrained TIMEE model for in-context time series classification.
A single forward pass classifies test series given labeled training examples.
No per-dataset training or fine-tuning required.
Usage::
from timee import TimeeClassifier
clf = TimeeClassifier.from_pretrained("path/to/checkpoint/")
# X arrays: (n_samples, n_channels, seq_len) float32 numpy
predictions, probabilities = clf.predict(X_train, y_train, X_test)
Datasets with more than 10 classes are handled automatically via one-vs-rest (OvR).
"""
def __init__(
self,
model: torch.nn.Module,
device: torch.device,
transforms: list[EnsembleTransform] | None = None,
) -> None:
self.device = device
self._model = model.to(device)
self._model.eval()
self.transforms = transforms
self.max_classes = getattr(model, "n_max_classes", None) or getattr(
model, "n_classes", None
)
if self.max_classes is None:
raise ValueError("Model must have an n_max_classes attribute")
@classmethod
def from_pretrained(
cls,
path: Union[str, Path] = "liamsbhoo/timee",
device: Union[str, torch.device, None] = None,
use_ensemble: bool = True,
) -> "TimeeClassifier":
"""Load a pretrained TimeeClassifier.
Args:
path: Local directory containing ``model.safetensors``, or a
HuggingFace Hub repo ID (e.g. ``"liamsbhoo/timee"``).
Defaults to the official checkpoint.
device: Torch device for inference. Defaults to auto-detection (CUDA > MPS > CPU).
use_ensemble: If True (default), use the 4-member preprocessing ensemble from the
paper (interpolate×{256,512} × {raw, first_difference}).
Set to False for faster single-pass inference without preprocessing.
Returns:
A ready-to-use TimeeClassifier.
"""
if device is None:
device = _infer_device()
device = torch.device(device) if isinstance(device, str) else device
local = Path(path)
if local.is_dir():
weights_path = local / "model.safetensors"
if not weights_path.exists():
raise FileNotFoundError(f"model.safetensors not found in {local}")
else:
from huggingface_hub import hf_hub_download
weights_path = hf_hub_download(repo_id=str(path), filename="model.safetensors")
from safetensors.torch import load_file
model = TimeeModel()
model.load_state_dict(load_file(weights_path))
transforms = default_ensemble_transforms() if use_ensemble else None
return cls(model=model, device=device, transforms=transforms)
def _prepare_inputs(
self,
X_train: np.ndarray,
y_train_encoded: np.ndarray,
X_test: np.ndarray,
) -> tuple[torch.Tensor, torch.Tensor]:
inputs_np = np.concatenate([X_train, X_test], axis=0)
targets_np = np.concatenate(
[y_train_encoded.astype(np.int64), np.zeros(len(X_test), dtype=np.int64)],
axis=0,
)
if inputs_np.ndim == 3 and inputs_np.shape[1] > 1:
# Multivariate (N, C, T) -> (N, T, C) -> (1, N, T, C)
inputs = (
torch.from_numpy(inputs_np.transpose(0, 2, 1)).float().to(self.device).unsqueeze(0)
)
else:
# Univariate (N, 1, T) -> squeeze -> (N, T) -> (1, N, T)
inputs = torch.from_numpy(inputs_np).float().to(self.device).squeeze().unsqueeze(0)
targets = torch.from_numpy(targets_np).long().to(self.device).unsqueeze(0)
return inputs, targets
def _forward(
self,
X_train: np.ndarray,
y_enc: np.ndarray,
X_test_batch: np.ndarray,
n_classes: int,
) -> np.ndarray:
inputs, targets = self._prepare_inputs(X_train, y_enc, X_test_batch)
logits = self._model(x=inputs, y=targets, eval_pos=len(X_train)).squeeze(0)[:, :n_classes]
return torch.softmax(logits, dim=1).cpu().numpy()
def _forward_ovr(
self,
X_train: np.ndarray,
y_enc: np.ndarray,
X_test_batch: np.ndarray,
n_classes: int,
) -> np.ndarray:
inputs, targets = self._prepare_inputs(X_train, y_enc, X_test_batch)
eval_pos = len(X_train)
class_probs = []
for c in range(n_classes):
targets_binary = torch.zeros_like(targets)
targets_binary[:, :eval_pos] = (targets[:, :eval_pos] == c).long()
logits = self._model(x=inputs, y=targets_binary, eval_pos=eval_pos).squeeze(0)[:, :2]
class_probs.append(torch.softmax(logits, dim=1).cpu().numpy()[:, 1])
probabilities = np.column_stack(class_probs)
probabilities /= probabilities.sum(axis=1, keepdims=True)
return probabilities
def _predict_batched(
self,
X_train: np.ndarray,
y_enc: np.ndarray,
X_test: np.ndarray,
n_classes: int,
batch_fn: Callable,
) -> np.ndarray:
n_test = len(X_test)
batch_size = n_test
while batch_size >= 1:
try:
chunks = []
for start in range(0, n_test, batch_size):
chunks.append(
batch_fn(X_train, y_enc, X_test[start : start + batch_size], n_classes)
)
if self.device.type == "cuda":
torch.cuda.empty_cache()
return np.concatenate(chunks, axis=0)
except torch.cuda.OutOfMemoryError:
torch.cuda.empty_cache()
batch_size //= 2
raise RuntimeError(
"GPU OOM even with a single test sample -- "
"training context alone exceeds available memory."
)
def predict(
self,
X_train: np.ndarray,
y_train: np.ndarray,
X_test: np.ndarray,
) -> tuple[np.ndarray, np.ndarray]:
"""Classify test samples given labeled training examples.
Args:
X_train: Training time series, shape (n_train, n_channels, seq_len).
y_train: Training labels, shape (n_train,). Any hashable label type.
X_test: Test time series, shape (n_test, n_channels, seq_len).
Returns:
predictions: Predicted class labels, shape (n_test,). Same dtype as y_train.
probabilities: Class probability estimates, shape (n_test, n_classes).
"""
le = LabelEncoder().fit(y_train)
n_classes = len(le.classes_)
y_enc = le.transform(y_train)
batch_fn = self._forward_ovr if n_classes > self.max_classes else self._forward
x_transforms: list[EnsembleTransform | None] = self.transforms or [None]
with torch.no_grad():
all_probs = []
for x_transform in x_transforms:
X_tr_t, X_te_t = x_transform(X_train, X_test) if x_transform else (X_train, X_test)
all_probs.append(self._predict_batched(X_tr_t, y_enc, X_te_t, n_classes, batch_fn))
probabilities = np.mean(all_probs, axis=0)
return le.inverse_transform(np.argmax(probabilities, axis=1)), probabilities