multimodalart's picture
multimodalart HF Staff
Upload folder using huggingface_hub
793c2b8 verified
Raw
History Blame Contribute Delete
11 kB
from typing import Tuple
import torch
import torch.nn as nn
from einops import rearrange
from timee.model.layers import (
ClassEmbedding,
CLSTokens,
DecoderMLP,
InstanceNorm,
Patch,
PatchEncoder,
TransformerEncoderLayer,
)
class TimeeModel(nn.Module):
"""TIMEE: in-context learning (ICL) model for time series classification.
Architecture:
1. Patch the input time series (non-overlapping patches).
2. Normalize each series (arcsinh instance norm) and across series.
3. Embed patches linearly; add patch-level and series-level stats embeddings.
4. Inject class embeddings into training-set patches.
5. Run n_horizontal_layers of temporal self-attention (RoPE), followed by
n_vertical_layers of cross-series attention.
6. Compress each series to a fixed-size representation (num_cls_tokens CLS
tokens) via a final attention layer.
7. Run n_icl_layers of ICL attention over the compressed representations.
8. Decode test-set representations to class logits via an MLP.
Default arguments match the released checkpoint.
"""
def __init__(
self,
n_max_classes: int = 10,
patch_size: int = 16,
patch_stride: int = 16,
d_model: int = 128,
n_horizontal_layers: int = 5,
n_vertical_layers: int = 5,
n_icl_layers: int = 2,
n_heads: int = 4,
d_kv: int = 32,
d_kv_icl: int = 64,
mlp_hidden_dim: int = 512,
decoder_mlp_hidden_dim: int = 512,
dropout_rate: float = 0.1,
num_cls_tokens: int = 4,
) -> None:
super().__init__()
self.n_max_classes = n_max_classes
self.patch_size = patch_size
self.patch_stride = patch_stride
self.d_model = d_model
self.n_horizontal_layers = n_horizontal_layers
self.n_vertical_layers = n_vertical_layers
self.n_icl_layers = n_icl_layers
self.n_heads = n_heads
self.d_kv = d_kv
self.d_kv_icl = d_kv_icl
self.mlp_hidden_dim = mlp_hidden_dim
self.decoder_mlp_hidden_dim = decoder_mlp_hidden_dim
self.dropout_rate = dropout_rate
self.num_cls_tokens = num_cls_tokens
self.icl_dim = d_model * num_cls_tokens
self.instance_norm_sequence = InstanceNorm(use_arcsinh=True)
self.instance_norm_series = InstanceNorm()
self.patch = Patch(patch_size=patch_size, patch_stride=patch_stride)
self.patch_encoder = PatchEncoder(patch_size=patch_size, d_model=d_model)
self.patch_stats_projection = nn.Linear(2, d_model, bias=False)
self.series_stats_projection = nn.Linear(2, d_model, bias=False)
self.cls_tokens = CLSTokens(num_cls_tokens, d_model)
layer_kwargs = dict(
d_model=d_model,
n_heads=n_heads,
d_kv=d_kv,
mlp_hidden_dim=mlp_hidden_dim,
dropout=dropout_rate,
)
self.encoder_horizontal = nn.ModuleList(
[
TransformerEncoderLayer(**layer_kwargs, use_rope=True)
for _ in range(n_horizontal_layers)
]
)
self.encoder_vertical = nn.ModuleList(
[
TransformerEncoderLayer(**layer_kwargs, use_rope=False)
for _ in range(n_vertical_layers)
]
)
self.last_horizontal_encoder = TransformerEncoderLayer(**layer_kwargs, use_rope=True)
self.cls_tokens_ln = nn.LayerNorm(d_model)
self.class_embedding_for_patches = ClassEmbedding(
num_embeddings=n_max_classes,
embedding_dim=d_model,
)
self.class_embedding_icl = ClassEmbedding(
num_embeddings=n_max_classes,
embedding_dim=self.icl_dim,
)
self.icl_block = nn.ModuleList(
[
TransformerEncoderLayer(
d_model=self.icl_dim,
n_heads=n_heads,
d_kv=d_kv_icl,
mlp_hidden_dim=mlp_hidden_dim,
dropout=dropout_rate,
use_rope=False,
)
for _ in range(n_icl_layers)
]
)
self.decoder_mlp_ln = nn.LayerNorm(self.icl_dim)
self.decoder_mlp = DecoderMLP(
d_model=self.icl_dim,
hidden_dim=decoder_mlp_hidden_dim,
output_dim=n_max_classes,
dropout=dropout_rate,
)
def _compute_stats_embeddings(self, x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
"""Compute scale-aware stat embeddings from raw (un-normalized) x (B, N, S).
Returns (patch_stats, series_stats):
patch_stats (B, N, P, 2): arcsinh(mean, std) per patch, z-scored within each series.
series_stats (B, N, 1, 2): arcsinh(mean, std) per series, z-scored within the episode.
"""
x_BNPQ = self.patch(x)
raw_mean = torch.nan_to_num(torch.nanmean(x_BNPQ, dim=-1, keepdim=True), nan=0.0)
raw_std = ((x_BNPQ - raw_mean).nan_to_num(0.0) ** 2).mean(dim=-1, keepdim=True).sqrt()
patch_stats = torch.arcsinh(torch.cat([raw_mean, raw_std], dim=-1))
p_mean = patch_stats.mean(dim=2, keepdim=True)
p_std = patch_stats.std(dim=2, correction=0, keepdim=True).clamp(min=1e-5)
patch_stats = (patch_stats - p_mean) / p_std
series_mean = torch.nan_to_num(torch.nanmean(x, dim=-1, keepdim=True), nan=0.0)
series_std = ((x - series_mean).nan_to_num(0.0) ** 2).mean(dim=-1, keepdim=True).sqrt()
series_stats = torch.arcsinh(torch.cat([series_mean, series_std], dim=-1))
ep_mean = series_stats.mean(dim=1, keepdim=True)
ep_std = series_stats.std(dim=1, keepdim=True).clamp(min=1e-5)
series_stats = ((series_stats - ep_mean) / ep_std).unsqueeze(2)
return patch_stats, series_stats
def forward(
self,
x: torch.Tensor,
y: torch.Tensor,
eval_pos: int,
) -> torch.Tensor:
"""
Args:
x: (B, N, S) float β€” N series per episode (support first, then queries).
y: (B, N) int β€” class labels; query positions (>= eval_pos) are ignored.
eval_pos: number of support series; x[:, :eval_pos] are labeled,
x[:, eval_pos:] are the queries to classify.
Returns:
logits: (B, N βˆ’ eval_pos, n_max_classes).
"""
x_BNS = x
y_BN = y
n_train = eval_pos
batch_size, num_series, _ = x_BNS.shape
patch_stats_BNPC, series_stats_BN12 = self._compute_stats_embeddings(x_BNS)
# Normalize: instance norm per series, then cross-series norm.
x_BNS, _ = self.instance_norm_sequence(x_BNS)
x_BSN = rearrange(x_BNS, "b n s -> b s n")
x_BSN, _ = self.instance_norm_series(x_BSN, eval_pos=n_train)
x_BNS = rearrange(x_BSN, "b s n -> b n s")
x_BNPQ = self.patch(x_BNS)
x_BNPQ = torch.nan_to_num(x_BNPQ, nan=0.0)
x_BNPD = self.patch_encoder(x_BNPQ)
x_BNPD = x_BNPD + self.patch_stats_projection(patch_stats_BNPC)
x_BNPD = x_BNPD + self.series_stats_projection(
series_stats_BN12.expand(-1, -1, x_BNPD.shape[2], -1)
)
# Inject class embeddings into train patches.
class_embed_BN1D = self.class_embedding_for_patches(y_BN[:, :n_train], n_train).unsqueeze(2)
x_BNPD = torch.cat([x_BNPD[:, :n_train] + class_embed_BN1D, x_BNPD[:, n_train:]], dim=1)
x_BNPD = torch.cat([self.cls_tokens(batch_size, num_series), x_BNPD], dim=2)
num_patches_plus_cls = x_BNPD.shape[2]
num_patches = num_patches_plus_cls - self.num_cls_tokens
# Mask convention: True = attend, False = blocked.
# Horizontal mask is fully open β€” each series attends to all its own patches.
horizontal_mask = torch.ones(
(batch_size * num_series, num_patches_plus_cls, num_patches_plus_cls),
device=x_BNPD.device,
dtype=torch.bool,
)
# Queries cannot attend to other queries; they can only see support series (and themselves).
vertical_mask = torch.ones(
(batch_size * num_patches_plus_cls, num_series, num_series),
device=x_BNPD.device,
dtype=torch.bool,
)
vertical_mask[:, :, n_train:] = False
vertical_mask |= torch.eye(num_series, device=x_BNPD.device, dtype=torch.bool)
position_ids = (
torch.arange(num_patches, device=x_BNPD.device)
.unsqueeze(0)
.repeat(batch_size * num_series, 1)
)
for layer in self.encoder_horizontal:
x_BNPD = rearrange(x_BNPD, "b n p d -> (b n) p d")
x_BNPD = layer(x=x_BNPD, attention_mask=horizontal_mask, position_ids=position_ids)
x_BNPD = rearrange(x_BNPD, "(b n) p d -> b n p d", b=batch_size, n=num_series)
for layer in self.encoder_vertical:
x_BNPD = rearrange(x_BNPD, "b n p d -> (b p) n d")
x_BNPD = layer(x=x_BNPD, attention_mask=vertical_mask, position_ids=None)
x_BNPD = rearrange(x_BNPD, "(b p) n d -> b n p d", b=batch_size, p=num_patches_plus_cls)
# No position attends to CLS tokens as keys, so CLS tokens aggregate only from patches.
compression_mask = torch.ones(
(batch_size * num_series, num_patches_plus_cls, num_patches_plus_cls),
device=x_BNPD.device,
dtype=torch.bool,
)
compression_mask[:, :, : self.num_cls_tokens] = False
x_BNPD = rearrange(x_BNPD, "b n p d -> (b n) p d")
x_BNPD = self.last_horizontal_encoder(
x=x_BNPD,
attention_mask=compression_mask,
position_ids=position_ids,
)
x_BNPD = rearrange(x_BNPD, "(b n) p d -> b n p d", b=batch_size, n=num_series)
row_repr_BNDc = self.cls_tokens_ln(x_BNPD[:, :, : self.num_cls_tokens]).flatten(-2, -1)
# ICL: cross-series attention over compressed representations.
# Same masking policy as vertical layers β€” queries attend to support + self only.
icl_mask = torch.ones(
(batch_size, num_series, num_series),
device=x_BNPD.device,
dtype=torch.bool,
)
icl_mask[:, :, n_train:] = False
icl_mask |= torch.eye(num_series, device=x_BNPD.device, dtype=torch.bool)
class_embed_icl = self.class_embedding_icl(y_BN[:, :n_train], n_train)
row_repr_BNDc = torch.cat(
[row_repr_BNDc[:, :n_train] + class_embed_icl, row_repr_BNDc[:, n_train:]], dim=1
)
for layer in self.icl_block:
row_repr_BNDc = layer(x=row_repr_BNDc, attention_mask=icl_mask, position_ids=None)
return self.decoder_mlp(self.decoder_mlp_ln(row_repr_BNDc[:, n_train:]))