Tabular Classification
Transformers
Safetensors
felatab
feature-extraction
fela
tabular
in-context-learning
prior-fitted-network
foundation-model
delta-rule
cpu
on-device
custom_code
Eval Results (legacy)
Instructions to use lowdown-labs/fela-tab with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use lowdown-labs/fela-tab with Transformers:
# Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("lowdown-labs/fela-tab", trust_remote_code=True, dtype="auto") - Notebooks
- Google Colab
- Kaggle
| from __future__ import annotations | |
| import json | |
| import math | |
| import os | |
| from dataclasses import dataclass | |
| import numpy as np | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| class FelaTabConfig: | |
| max_features: int = 100 | |
| max_classes: int = 10 | |
| dim: int = 1024 | |
| n_layers: int = 28 | |
| n_heads: int = 16 | |
| head_dim: int = 64 | |
| chunk: int = 64 | |
| ffn_mult: int = 3 | |
| dropout: float = 0.0 | |
| use_landmark: bool = True | |
| n_landmarks: int = 48 | |
| ln_eps: float = 1e-05 | |
| def __post_init__(self): | |
| assert self.dim % self.n_heads == 0 or self.n_heads * self.head_dim > 0 | |
| def delta_rule_chunk(q, k, v, beta, chunk): | |
| B, H, T, D = q.shape | |
| dev, dt = (q.device, q.dtype) | |
| pad = (chunk - T % chunk) % chunk | |
| if pad: | |
| q = F.pad(q, (0, 0, 0, pad)) | |
| k = F.pad(k, (0, 0, 0, pad)) | |
| v = F.pad(v, (0, 0, 0, pad)) | |
| beta = F.pad(beta, (0, pad)) | |
| Tp = T + pad | |
| nC = Tp // chunk | |
| q = q.view(B, H, nC, chunk, D) | |
| k = k.view(B, H, nC, chunk, D) | |
| v = v.view(B, H, nC, chunk, D) | |
| beta = beta.view(B, H, nC, chunk) | |
| tri = torch.tril(torch.ones(chunk, chunk, device=dev, dtype=dt), -1) | |
| eye = torch.eye(chunk, device=dev, dtype=dt) | |
| S = torch.zeros(B, H, D, D, device=dev, dtype=dt) | |
| outs = [] | |
| for c in range(nC): | |
| kc = k[:, :, c] | |
| vc = v[:, :, c] | |
| qc = q[:, :, c] | |
| bc = beta[:, :, c] | |
| KK = torch.einsum("bhtd,bhsd->bhts", kc, kc) | |
| A = bc.unsqueeze(-1) * KK * tri | |
| kS0 = torch.einsum("bhtd,bhde->bhte", kc, S) | |
| RHS = bc.unsqueeze(-1) * (vc - kS0) | |
| M = eye + A | |
| U = torch.linalg.solve_triangular(M, RHS, upper=False, unitriangular=True) | |
| qS0 = torch.einsum("bhtd,bhde->bhte", qc, S) | |
| QK = torch.einsum("bhtd,bhsd->bhts", qc, kc) * tri | |
| o = qS0 + torch.einsum("bhts,bhse->bhte", QK, U) | |
| outs.append(o) | |
| S = S + torch.einsum("bhtd,bhte->bhde", kc, U) | |
| o = torch.cat(outs, dim=2).view(B, H, Tp, D)[:, :, :T] | |
| return (o, S) | |
| def pool_landmarks(x, wm, G): | |
| B, T, C = x.shape | |
| ns = wm.sum(1).clamp(min=1) | |
| gs = torch.ceil(ns / G).clamp(min=1) | |
| pos = torch.arange(T, device=x.device)[None, :].expand(B, T) | |
| gid = torch.minimum((pos / gs[:, None]).floor().long(), torch.full_like(pos, G - 1)) | |
| valid = wm > 0 | |
| flat = (torch.arange(B, device=x.device)[:, None] * G + gid)[valid] | |
| src = x[valid] | |
| land = torch.zeros(B * G, C, device=x.device, dtype=x.dtype) | |
| cnt = torch.zeros(B * G, device=x.device, dtype=x.dtype) | |
| land.index_add_(0, flat, src) | |
| cnt.index_add_(0, flat, torch.ones(src.shape[0], device=x.device, dtype=x.dtype)) | |
| land = land / cnt.clamp(min=1)[:, None] | |
| return (land.view(B, G, C), (cnt > 0).view(B, G)) | |
| class DeltaAttn(nn.Module): | |
| def __init__(self, cfg): | |
| super().__init__() | |
| self.H, self.D, self.chunk = (cfg.n_heads, cfg.head_dim, cfg.chunk) | |
| inner = cfg.n_heads * cfg.head_dim | |
| self.q = nn.Linear(cfg.dim, inner, bias=False) | |
| self.k = nn.Linear(cfg.dim, inner, bias=False) | |
| self.v = nn.Linear(cfg.dim, inner, bias=False) | |
| self.b = nn.Linear(cfg.dim, cfg.n_heads) | |
| self.o = nn.Linear(inner, cfg.dim, bias=False) | |
| def forward(self, x, write_mask): | |
| B, T, _ = x.shape | |
| H, D = (self.H, self.D) | |
| q = self.q(x).view(B, T, H, D).transpose(1, 2) | |
| k = self.k(x).view(B, T, H, D).transpose(1, 2) | |
| v = self.v(x).view(B, T, H, D).transpose(1, 2) | |
| q = F.normalize(q, dim=-1) | |
| k = F.normalize(k, dim=-1) | |
| beta = torch.sigmoid(self.b(x)).transpose(1, 2) | |
| beta = beta * write_mask.unsqueeze(1) | |
| o, _ = delta_rule_chunk(q, k, v, beta, self.chunk) | |
| o = o.transpose(1, 2).reshape(B, T, H * D) | |
| return self.o(o) | |
| class LandmarkAttn(nn.Module): | |
| def __init__(self, cfg): | |
| super().__init__() | |
| self.H, self.D, self.G = (cfg.n_heads, cfg.head_dim, cfg.n_landmarks) | |
| inner = cfg.n_heads * cfg.head_dim | |
| self.q = nn.Linear(cfg.dim, inner, bias=False) | |
| self.k = nn.Linear(cfg.dim, inner, bias=False) | |
| self.v = nn.Linear(cfg.dim, inner, bias=False) | |
| self.o = nn.Linear(inner, cfg.dim, bias=False) | |
| self.gate = nn.Parameter(torch.zeros(1)) | |
| def forward(self, x, wm): | |
| B, T, _ = x.shape | |
| H, D, G = (self.H, self.D, self.G) | |
| land, lmask = pool_landmarks(x, wm, G) | |
| q = self.q(x).view(B, T, H, D).transpose(1, 2) | |
| k = self.k(land).view(B, G, H, D).transpose(1, 2) | |
| v = self.v(land).view(B, G, H, D).transpose(1, 2) | |
| att = torch.einsum("bhtd,bhgd->bhtg", q, k) / math.sqrt(D) | |
| att = att.masked_fill(~lmask[:, None, None, :], -1000000000.0) | |
| att = att.softmax(-1) | |
| o = torch.einsum("bhtg,bhgd->bhtd", att, v).transpose(1, 2).reshape(B, T, H * D) | |
| return self.o(o) * torch.tanh(self.gate) | |
| class Block(nn.Module): | |
| def __init__(self, cfg): | |
| super().__init__() | |
| self.n1 = nn.LayerNorm(cfg.dim, eps=cfg.ln_eps) | |
| self.attn = DeltaAttn(cfg) | |
| self.landmark = LandmarkAttn(cfg) if cfg.use_landmark else None | |
| self.n2 = nn.LayerNorm(cfg.dim, eps=cfg.ln_eps) | |
| hid = cfg.dim * cfg.ffn_mult | |
| self.ff = nn.Sequential( | |
| nn.Linear(cfg.dim, hid), nn.GELU(), nn.Linear(hid, cfg.dim) | |
| ) | |
| def forward(self, x, wm): | |
| h = self.n1(x) | |
| a = self.attn(h, wm) | |
| if self.landmark is not None: | |
| a = a + self.landmark(h, wm) | |
| x = x + a | |
| x = x + self.ff(self.n2(x)) | |
| return x | |
| class FelaTab(nn.Module): | |
| def __init__(self, cfg: FelaTabConfig): | |
| super().__init__() | |
| self.cfg = cfg | |
| self.feat_enc = nn.Linear(cfg.max_features, cfg.dim) | |
| self.cls_label_emb = nn.Embedding(cfg.max_classes + 1, cfg.dim) | |
| self.reg_label = nn.Linear(1, cfg.dim) | |
| self.reg_label_gate = nn.Parameter(torch.ones(1)) | |
| self.type_emb = nn.Embedding(2, cfg.dim) | |
| self.blocks = nn.ModuleList([Block(cfg) for _ in range(cfg.n_layers)]) | |
| self.norm = nn.LayerNorm(cfg.dim, eps=cfg.ln_eps) | |
| self.cls_head = nn.Linear(cfg.dim, cfg.max_classes) | |
| self.reg_head = nn.Linear(cfg.dim, 2) | |
| def encode_tokens(self, X, y, n_support, task_type_id): | |
| B, T, _ = X.shape | |
| tok = self.feat_enc(X) | |
| tok = tok + self.type_emb(task_type_id).view(1, 1, -1) | |
| idx = torch.arange(T, device=X.device).unsqueeze(0) | |
| wm = (idx < n_support.unsqueeze(1)).float() | |
| if int(task_type_id) == 0: | |
| yl = y.clamp(min=0).long() | |
| lab = self.cls_label_emb(yl) * wm.unsqueeze(-1) | |
| tok = tok + lab | |
| else: | |
| lab = ( | |
| self.reg_label(y.unsqueeze(-1).float()) | |
| * wm.unsqueeze(-1) | |
| * self.reg_label_gate | |
| ) | |
| tok = tok + lab | |
| return (tok, wm) | |
| def forward(self, X, y, n_support, task_type_id): | |
| tok, wm = self.encode_tokens(X, y, n_support, task_type_id) | |
| h = tok | |
| for blk in self.blocks: | |
| h = blk(h, wm) | |
| h = self.norm(h) | |
| if int(task_type_id) == 0: | |
| return self.cls_head(h) | |
| return self.reg_head(h) | |
| _CONFIG_FIELDS = set(FelaTabConfig.__dataclass_fields__.keys()) | |
| TIERS = { | |
| "big": dict(dim=1024, n_layers=28, n_heads=16, head_dim=64), | |
| "small": dict(dim=512, n_layers=14, n_heads=8, head_dim=64), | |
| } | |
| def _to_config(cfg_dict: dict) -> FelaTabConfig: | |
| return FelaTabConfig(**{k: v for k, v in cfg_dict.items() if k in _CONFIG_FIELDS}) | |
| def _read_json(path: str) -> dict: | |
| with open(path) as f: | |
| return json.load(f) | |
| def _dequant_state_dict(tensors: dict) -> dict: | |
| sd = {} | |
| for k, v in tensors.items(): | |
| if k.endswith(".scale"): | |
| continue | |
| if v.dtype == torch.int8: | |
| scale = tensors[k + ".scale"] | |
| sd[k] = v.float() * scale[:, None] | |
| else: | |
| sd[k] = v.float() | |
| return sd | |
| def load_model(path_or_repo: str = ".", tier: str = "big", config: dict = None): | |
| from safetensors.torch import load_file | |
| def _pick(dir_path): | |
| for base in ( | |
| f"model_{tier}_int8.safetensors", | |
| f"model_{tier}.safetensors", | |
| "model_int8.safetensors", | |
| "model.safetensors", | |
| ): | |
| p = os.path.join(dir_path, base) | |
| if os.path.isfile(p): | |
| return p | |
| raise FileNotFoundError(f"no model safetensors for tier '{tier}' in {dir_path}") | |
| def _cfg(dir_path): | |
| for base in (f"config_{tier}.json", "config.json"): | |
| p = os.path.join(dir_path, base) | |
| if os.path.isfile(p): | |
| return _read_json(p) | |
| return dict(TIERS[tier]) | |
| if os.path.isfile(path_or_repo) and path_or_repo.endswith(".safetensors"): | |
| weights_path = path_or_repo | |
| cfg_dict = config or _cfg(os.path.dirname(path_or_repo) or ".") | |
| elif os.path.isdir(path_or_repo): | |
| weights_path = _pick(path_or_repo) | |
| cfg_dict = config or _cfg(path_or_repo) | |
| else: | |
| from huggingface_hub import hf_hub_download | |
| cfg_dict = config | |
| if cfg_dict is None: | |
| for base in (f"config_{tier}.json", "config.json"): | |
| try: | |
| cfg_dict = _read_json(hf_hub_download(path_or_repo, base)) | |
| break | |
| except Exception: | |
| continue | |
| cfg_dict = cfg_dict or dict(TIERS[tier]) | |
| weights_path = None | |
| for base in (f"model_{tier}_int8.safetensors", f"model_{tier}.safetensors"): | |
| try: | |
| weights_path = hf_hub_download(path_or_repo, base) | |
| break | |
| except Exception: | |
| continue | |
| if weights_path is None: | |
| raise FileNotFoundError( | |
| f"no model_{tier}[_int8].safetensors in repo {path_or_repo}" | |
| ) | |
| for k, val in TIERS[tier].items(): | |
| cfg_dict.setdefault(k, val) | |
| tensors = load_file(weights_path) | |
| sd = _dequant_state_dict(tensors) | |
| model = FelaTab(_to_config(cfg_dict)) | |
| model.load_state_dict(sd) | |
| model.eval() | |
| return model | |
| from_pretrained = load_model | |
| def _standardize_fit(Xtr: np.ndarray): | |
| mean = Xtr.mean(axis=0) | |
| scale = np.sqrt(((Xtr - mean) ** 2).mean(axis=0)) | |
| scale[scale == 0] = 1.0 | |
| return (mean.astype(np.float32), scale.astype(np.float32)) | |
| def _prep(model, Xtr, Xte): | |
| cfg = model.cfg | |
| Xtr = np.asarray(Xtr, dtype=np.float32) | |
| Xte = np.asarray(Xte, dtype=np.float32) | |
| mean, scale = _standardize_fit(Xtr) | |
| Xtr2 = (Xtr - mean) / scale | |
| Xte2 = (Xte - mean) / scale | |
| F_ = cfg.max_features | |
| if Xtr2.shape[1] > F_: | |
| try: | |
| from sklearn.decomposition import PCA | |
| p = PCA(n_components=F_, random_state=0).fit(Xtr2) | |
| Xtr2, Xte2 = (p.transform(Xtr2), p.transform(Xte2)) | |
| except Exception: | |
| Xtr2, Xte2 = (Xtr2[:, :F_], Xte2[:, :F_]) | |
| def pad(A): | |
| out = np.zeros((A.shape[0], F_), np.float32) | |
| out[:, : min(A.shape[1], F_)] = A[:, :F_] | |
| return out | |
| return (np.nan_to_num(pad(Xtr2)), np.nan_to_num(pad(Xte2))) | |
| def predict( | |
| model, Xtr, ytr, Xte, task="classification", n_classes=None, support_cap=3000 | |
| ): | |
| is_cls = task.startswith("cls") or task == "classification" | |
| Xtr = np.asarray(Xtr, dtype=np.float32) | |
| ytr = np.asarray(ytr) | |
| Xte = np.asarray(Xte, dtype=np.float32) | |
| if len(Xtr) > support_cap: | |
| idx = np.random.RandomState(0).permutation(len(Xtr))[:support_cap] | |
| Xtr, ytr = (Xtr[idx], ytr[idx]) | |
| Xtr2, Xte2 = _prep(model, Xtr, Xte) | |
| ns = len(Xtr2) | |
| X = np.concatenate([Xtr2, Xte2], 0)[None] | |
| if is_cls: | |
| ncls = int(n_classes if n_classes is not None else ytr.max() + 1) | |
| y = np.concatenate([ytr, np.zeros(len(Xte2))]).astype(np.int64)[None] | |
| ttype = 0 | |
| else: | |
| ymu, ysd = (float(ytr.mean()), float(ytr.std() + 1e-06)) | |
| y = np.concatenate([(ytr - ymu) / ysd, np.zeros(len(Xte2))]).astype(np.float32)[ | |
| None | |
| ] | |
| ttype = 1 | |
| dev = next(model.parameters()).device | |
| Xt = torch.from_numpy(X).to(dev) | |
| yt = torch.from_numpy(y).to(dev) | |
| nst = torch.tensor([ns]).to(dev) | |
| out = model(Xt, yt, nst, torch.tensor(ttype, device=dev)).float() | |
| q = out[0, ns:] | |
| if is_cls: | |
| return F.softmax(q[:, :ncls], dim=-1).cpu().numpy() | |
| mean = q[:, 0].cpu().numpy() * ysd + ymu | |
| std = np.sqrt(np.exp(np.clip(q[:, 1].cpu().numpy(), -8, 8))) * ysd | |
| return (mean, std) | |
| def predict_bagged( | |
| model, | |
| Xtr, | |
| ytr, | |
| Xte, | |
| task="classification", | |
| n_classes=None, | |
| support_cap=3000, | |
| n_bag=16, | |
| base_seed=1000, | |
| ): | |
| is_cls = task.startswith("cls") or task == "classification" | |
| if not is_cls or n_bag <= 1: | |
| return predict(model, Xtr, ytr, Xte, task, n_classes, support_cap) | |
| Xtr = np.asarray(Xtr, dtype=np.float32) | |
| ytr = np.asarray(ytr) | |
| Xte = np.asarray(Xte, dtype=np.float32) | |
| ncls = int(n_classes if n_classes is not None else ytr.max() + 1) | |
| acc = [] | |
| for b in range(n_bag): | |
| rng = np.random.RandomState(base_seed + b) | |
| rp = rng.permutation(len(Xtr)) | |
| cperm = rng.permutation(Xtr.shape[1]) | |
| acc.append( | |
| predict( | |
| model, | |
| Xtr[rp][:, cperm], | |
| ytr[rp], | |
| Xte[:, cperm], | |
| "classification", | |
| ncls, | |
| support_cap, | |
| ) | |
| ) | |
| return np.mean(acc, axis=0) | |
| def count_params(m): | |
| return sum((p.numel() for p in m.parameters())) | |