Download code/arrow_train/language.py from smerkd/arrow-heads: direct link, hf CLI and curl.
- Browser
- Download file 5.06 kB
-
https://huggingface.co/smerkd/arrow-heads/resolve/main/code/arrow_train/language.py
- Command line
-
hf download hf://smerkd/arrow-heads/code/arrow_train/language.py
-
curl -L -o language.py https://huggingface.co/smerkd/arrow-heads/resolve/main/code/arrow_train/language.py
5.06 kB
| """Per-pixel language targets (label_text v1: SigLIP2-base text embeddings of 624 concepts) for splatted features. | |
| Source: point4d-datasets tools/label_text/v1 (README there). Every semantic class / object category maps to one | |
| concept row of `targets [624, 768]` (L2-normalised google/siglip2-base-patch16-224 text embeddings); row -1 means | |
| IGNORE (unlabelled, void, background, ...), and ids or names missing from a table are IGNORE too. | |
| * table key "semantic id": rows = lut[semantic] | |
| * table key "category name": rows = classes[instances[str(instance)]["name"]]["row"] | |
| * no semantic channel but a semantic-id table (data-012 in the HF mirror): read instances[...]["semantic_id"] | |
| Loss (as recommended there): contrastive over all concepts, logits = cos(pred, targets) / tau with a learnable tau | |
| initialised at the fitted 0.009; plain cosine regression separates classes poorly. | |
| """ | |
| import json | |
| from pathlib import Path | |
| import numpy as np | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| DEFAULT_DIR = "/scratch/data/point4d-datasets/tools/label_text/v1" | |
| IGNORE_ROW = -1 | |
| class LabelText: | |
| def __init__(self, root=DEFAULT_DIR): | |
| self.root = Path(root) | |
| z = np.load(self.root / "label_embeddings_v1.npz") | |
| self.targets = z["targets"].astype(np.float32) # (C, 768), L2-normalised | |
| self.concepts = [str(c) for c in z["concept_names"]] | |
| self.tables = {} | |
| for path in self.root.glob("*_label_text.json"): | |
| table = json.loads(path.read_text()) | |
| classes = table.get("classes") or {} | |
| if not classes: | |
| continue # e.g. ADT: empty for now -> all IGNORE | |
| semantic = table.get("key", "").startswith("semantic") | |
| entry = dict(semantic=semantic, rows={k: int(v.get("row", IGNORE_ROW)) for k, v in classes.items()}) | |
| if semantic: | |
| ids = [int(k) for k in classes] | |
| lut = np.full(max(ids) + 1, IGNORE_ROW, np.int32) | |
| for k, row in entry["rows"].items(): | |
| lut[int(k)] = row | |
| entry["lut"] = lut | |
| self.tables[table.get("dataset", path.name.removesuffix("_label_text.json"))] = entry | |
| def rows(self, dataset, semantic=None, instance=None, instances=None): | |
| """(H, W) int32 concept rows for one frame, or None when the dataset has no usable channel/table.""" | |
| table = self.tables.get(dataset) | |
| if table is None: | |
| return None | |
| if table["semantic"]: | |
| ids = semantic | |
| if ids is None and instance is not None and instances: # semantic id via instances.json | |
| lut = np.full(int(instance.max()) + 1, -1, np.int64) | |
| for k, v in instances.items(): | |
| if int(k) <= instance.max() and v.get("semantic_id") is not None: | |
| lut[int(k)] = int(v["semantic_id"]) | |
| ids = lut[instance] | |
| if ids is None: | |
| return None | |
| ids = ids.astype(np.int64) | |
| lut = table["lut"] | |
| ok = (ids >= 0) & (ids < len(lut)) | |
| return np.where(ok, lut[np.clip(ids, 0, len(lut) - 1)], IGNORE_ROW).astype(np.int32) | |
| if instance is None or not instances: | |
| return None | |
| lut = np.full(int(instance.max()) + 1, IGNORE_ROW, np.int32) | |
| for k, v in instances.items(): | |
| name = v.get("name") | |
| if name is not None and int(k) <= instance.max(): | |
| lut[int(k)] = table["rows"].get(name, IGNORE_ROW) | |
| return lut[instance] | |
| class LanguageDecoder(nn.Module): | |
| """Splatted per-pixel latent (lang_dim) -> 768-d SigLIP2 space; contrastive logits over the concept targets.""" | |
| def __init__(self, lang_dim, targets, tau=0.009): | |
| super().__init__() | |
| self.proj = nn.Sequential(nn.Linear(lang_dim, 256), nn.GELU(), nn.Linear(256, targets.shape[1])) | |
| self.register_buffer("targets", F.normalize(torch.as_tensor(targets, dtype=torch.float32), dim=-1), persistent=False) | |
| self.log_tau = nn.Parameter(torch.tensor(float(np.log(tau)))) | |
| def embed(self, latent): | |
| return F.normalize(self.proj(latent.float()), dim=-1) | |
| def loss(self, latent, rows, max_pixels=4096): | |
| """Cross-entropy of cos(embed, targets) / tau against concept rows (IGNORE skipped); also top-1 accuracy.""" | |
| valid = rows >= 0 | |
| if not valid.any(): | |
| return latent.sum() * 0.0, {} | |
| lat, tgt = latent[valid], rows[valid].long() | |
| if len(tgt) > max_pixels: | |
| pick = torch.randperm(len(tgt), device=tgt.device)[:max_pixels] | |
| lat, tgt = lat[pick], tgt[pick] | |
| logits = self.embed(lat) @ self.targets.T / self.log_tau.exp() | |
| loss = F.cross_entropy(logits, tgt) | |
| return loss, dict(lang=float(loss.detach()), lang_acc=float((logits.argmax(-1) == tgt).float().mean()), | |
| lang_tau=float(self.log_tau.exp().detach())) | |