Download inference.py from Polifemus/process-fm: direct link, hf CLI and curl.
- Browser
- Download file 6.33 kB
-
https://huggingface.co/Polifemus/process-fm/resolve/main/inference.py
- Command line
-
hf download hf://Polifemus/process-fm/inference.py
-
curl -L -o inference.py https://huggingface.co/Polifemus/process-fm/resolve/main/inference.py
6.33 kB
| """Standalone inference for Process-FM checkpoints (fm-v6 and scaling/sc*). | |
| Dependency: torch + numpy only. The models use global activity/resource ids: | |
| activities 4..776 (UNK=1), resources 2..2739 (UNK=1), log ids from vocab["log_ids"]. | |
| """ | |
| import json | |
| from datetime import datetime | |
| from pathlib import Path | |
| import numpy as np | |
| import torch | |
| import torch.nn as nn | |
| N_NUM = 6 | |
| ACT_OFFSET = 4 | |
| RES_OFFSET = 2 | |
| UNK_ACT = 1 | |
| UNK_RES = 1 | |
| class EventTransformer(nn.Module): | |
| def __init__(self, n_act: int, n_res: int, n_logs: int, n_classes: int, | |
| d_model: int = 256, n_layers: int = 6, n_heads: int = 8, | |
| dropout: float = 0.1, max_len: int = 256, | |
| act_dim: int = 64, res_dim: int = 32, log_dim: int = 16): | |
| super().__init__() | |
| self.act_emb = nn.Embedding(n_act, act_dim, padding_idx=0) | |
| self.res_emb = nn.Embedding(n_res, res_dim, padding_idx=0) | |
| self.log_emb = nn.Embedding(n_logs, log_dim) | |
| self.proj = nn.Linear(act_dim + res_dim + log_dim + N_NUM, d_model) | |
| self.pos = nn.Embedding(max_len, d_model) | |
| layer = nn.TransformerEncoderLayer( | |
| d_model=d_model, nhead=n_heads, dim_feedforward=4 * d_model, | |
| dropout=dropout, batch_first=True, norm_first=True, activation="gelu", | |
| ) | |
| self.encoder = nn.TransformerEncoder(layer, num_layers=n_layers) | |
| self.norm = nn.LayerNorm(d_model) | |
| self.act_out = nn.Linear(d_model, n_classes) | |
| self.res_out = nn.Linear(d_model, n_res) | |
| self.max_len = max_len | |
| def forward(self, gact: torch.Tensor, gres: torch.Tensor, num: torch.Tensor, | |
| log_id: torch.Tensor, causal: bool = True, | |
| return_hidden: bool = False): | |
| b, t = gact.shape | |
| x = torch.cat([ | |
| self.act_emb(gact), | |
| self.res_emb(gres), | |
| self.log_emb(log_id).unsqueeze(1).expand(b, t, -1), | |
| num, | |
| ], dim=-1) | |
| x = self.proj(x) | |
| idx = torch.arange(t, device=gact.device).unsqueeze(0).expand(b, t) | |
| x = x + self.pos(idx) | |
| if causal: | |
| x = self.encoder(x, mask=torch.triu( | |
| torch.ones(t, t, device=gact.device, dtype=torch.bool), diagonal=1)) | |
| else: | |
| x = self.encoder(x) | |
| h = self.norm(x) | |
| if return_hidden: | |
| return h | |
| return self.act_out(h), self.res_out(h) | |
| def build_model(config: dict) -> EventTransformer: | |
| return EventTransformer( | |
| n_act=config["n_act"], n_res=config["n_res"], n_logs=config["n_logs"], | |
| n_classes=config["n_classes"], d_model=config["d_model"], | |
| n_layers=config["n_layers"], n_heads=config["n_heads"], | |
| dropout=config["dropout"], max_len=config["max_len"], | |
| act_dim=config["act_dim"], res_dim=config["res_dim"], log_dim=config["log_dim"], | |
| ) | |
| def _to_ns(ts) -> int: | |
| if isinstance(ts, datetime): | |
| return int(ts.timestamp() * 1e9) | |
| return int(np.datetime64(ts, "ns").astype("int64")) | |
| def featurize(events: list[dict], vocab: dict, amount: float = 0.0, | |
| log: str | None = None): | |
| max_len = min(int(vocab.get("max_prefix_len", 256)), 256) | |
| events = events[-max_len:] | |
| n = len(events) | |
| ts = np.array([_to_ns(e["timestamp"]) for e in events], dtype=np.int64) | |
| dt_prev = np.zeros(n, dtype=np.float64) | |
| dt_prev[1:] = (ts[1:] - ts[:-1]) / 1e9 | |
| dt_start = (ts - ts[0]) / 1e9 | |
| hours = ((ts // 3_600_000_000_000) % 24).astype(np.float64) | |
| weekday = ((ts // 86_400_000_000_000) + 3) % 7 | |
| amount_feat = np.log1p(max(amount, 0.0)) / 14.0 | |
| num = np.stack([ | |
| np.log1p(dt_prev) / 20.0, | |
| np.log1p(dt_start) / 20.0, | |
| np.sin(2 * np.pi * hours / 24.0), | |
| np.cos(2 * np.pi * hours / 24.0), | |
| weekday / 6.0, | |
| np.full(n, amount_feat), | |
| ], axis=1).astype(np.float32) | |
| act_ids = {str(a): ACT_OFFSET + i for i, a in enumerate(vocab["activities"])} | |
| res_ids = {str(r): RES_OFFSET + i for i, r in enumerate(vocab["resources"])} | |
| gact = np.array([act_ids.get(str(e.get("activity", "")), UNK_ACT) for e in events], | |
| dtype=np.int64) | |
| gres = np.array([res_ids.get(str(e.get("resource", "")), UNK_RES) for e in events], | |
| dtype=np.int64) | |
| log_name = log or vocab.get("default_log") | |
| log_id = int(vocab["log_ids"].get(log_name, vocab.get("log_unk_id", vocab["n_logs"] - 1))) | |
| return ( | |
| torch.from_numpy(gact)[None], | |
| torch.from_numpy(gres)[None], | |
| torch.from_numpy(num)[None], | |
| torch.tensor([log_id], dtype=torch.long), | |
| ) | |
| def load_model(release_dir: str | Path = "."): | |
| release_dir = Path(release_dir) | |
| config = json.loads((release_dir / "config.json").read_text()) | |
| vocab = json.loads((release_dir / "vocab.json").read_text()) | |
| model = build_model(config) | |
| state = torch.load(release_dir / "model.pt", map_location="cpu", weights_only=True) | |
| model.load_state_dict(state) | |
| model.eval() | |
| return model, vocab | |
| def predict(model: EventTransformer, vocab: dict, events: list[dict], | |
| amount: float = 0.0, top_k: int = 5, log: str | None = None) -> list[tuple[str, float]]: | |
| gact, gres, num, log_id = featurize(events, vocab, amount=amount, log=log) | |
| h = model(gact, gres, num, log_id, causal=True, return_hidden=True) | |
| logits = model.act_out(h[0, gact.shape[1] - 1].float()) | |
| probs = torch.softmax(logits, dim=-1) | |
| k = min(top_k, probs.numel() - 1) | |
| values, indices = torch.topk(probs, k) | |
| id_to_act = {ACT_OFFSET + i: str(a) for i, a in enumerate(vocab["activities"])} | |
| return [(id_to_act.get(int(i), "UNK"), float(v)) for v, i in zip(values, indices)] | |
| if __name__ == "__main__": | |
| here = Path(__file__).resolve().parent | |
| model, vocab = load_model(here / "fm-v6") | |
| activities = [str(a) for a in vocab["activities"][:3]] | |
| resources = [str(r) for r in vocab["resources"][:2]] | |
| events = [ | |
| {"activity": activities[0], "resource": resources[0], "timestamp": "2024-01-01 10:00:00"}, | |
| {"activity": activities[1], "resource": resources[1], "timestamp": "2024-01-01 11:00:00"}, | |
| {"activity": activities[2], "resource": resources[0], "timestamp": "2024-01-01 12:00:00"}, | |
| ] | |
| print(predict(model, vocab, events, top_k=3, log=vocab.get("default_log"))) | |