fela-tab / modeling.py
itstheraj's picture
initial commit
c902bdf
Raw
History Blame Contribute Delete
13.7 kB
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
@dataclass
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)))
@torch.no_grad()
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()))