Text Classification
Transformers
Safetensors
English
modernbert
multi-head-classification
llm-routing
llm-gateway
model-routing
prompt-classification
query-classification
data-classification
retrieval-augmented-generation
calibration
synthetic-data
Instructions to use s-ttp/aiva-rlcd-modernbert with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use s-ttp/aiva-rlcd-modernbert with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-classification", model="s-ttp/aiva-rlcd-modernbert")# pip install -U transformers accelerate # Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("s-ttp/aiva-rlcd-modernbert", device_map="auto") - Notebooks
- Google Colab
- Kaggle
Download training/evaluate.py from s-ttp/aiva-rlcd-modernbert: direct link, hf CLI and curl.
- Browser
- Download file 9.37 kB
-
https://huggingface.co/s-ttp/aiva-rlcd-modernbert/resolve/main/training/evaluate.py
- Command line
-
hf download hf://s-ttp/aiva-rlcd-modernbert/training/evaluate.py
-
curl -L -o evaluate.py https://huggingface.co/s-ttp/aiva-rlcd-modernbert/resolve/main/training/evaluate.py
9.37 kB
| # Copyright 2026 The AiVa proof-of-concept team. Licensed under the Apache License, Version 2.0. | |
| """Score an RLCD checkpoint on a labelled split, the way the published model was judged. | |
| python evaluate.py --model rlcd-mine --data data/test.jsonl --latency 200 \ | |
| --prior public=0.146,internal=0.527,confidential=0.297,restricted=0.024,sovereign-only=0.006 | |
| Reports, per head: accuracy, macro-F1, expected calibration error, and coverage / accuracy at confidence | |
| thresholds (to choose your own). For the ordered sensitivity head, under the tighten-only rule (the highest | |
| level L with P(class >= L) >= --tighten): how many sensitive requests are classified too low, overall and per | |
| level, and how many public or internal ones are raised, with 95% Wilson intervals. If rows carry a "dlp" | |
| level (your DLP's verdict), it also reports the rate with DLP alongside. `--prior` re-weights rows to your | |
| production sensitivity mix: weighted figures are reported beside the as-sampled ones, never instead of them. | |
| Macro-F1 averages over every class of a head, as the published figures do (a class absent from the split | |
| scores 0); `--macro-f1-over present` averages over the classes present instead, for small or partial splits. | |
| `--latency N` times N single requests on this machine, tokenizer included. | |
| `--model` is a folder or a Hub repo; `--revision` pins a Hub revision. | |
| """ | |
| import argparse | |
| import itertools | |
| import json | |
| import math | |
| import time | |
| from pathlib import Path | |
| import torch | |
| from huggingface_hub import snapshot_download | |
| from rlcd import RlcdClassifier, render | |
| def wilson(hits: float, n: float) -> tuple[float, float]: | |
| if n == 0: | |
| return (0.0, 1.0) | |
| p, z = hits / n, 1.96 | |
| centre, half = p + z * z / (2 * n), z * math.sqrt(p * (1 - p) / n + z * z / (4 * n * n)) | |
| return ((centre - half) / (1 + z * z / n), (centre + half) / (1 + z * z / n)) | |
| def tighten(distribution: dict[str, float], order: list[str], threshold: float) -> str: | |
| mass = 0.0 | |
| for level in reversed(order): | |
| mass += distribution[level] | |
| if mass >= threshold: | |
| return level | |
| return order[0] | |
| def main() -> None: | |
| parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) | |
| parser.add_argument("--model", required=True) | |
| parser.add_argument("--revision", default=None) | |
| parser.add_argument("--data", type=Path, required=True) | |
| parser.add_argument("--prior", default=None, help="level=share,... to weight rows to production") | |
| parser.add_argument("--tighten", type=float, default=0.5) | |
| parser.add_argument("--sensitive-from", default="confidential", help="the first level that counts as sensitive") | |
| parser.add_argument("--thresholds", default="0.5,0.6,0.7,0.8,0.9,0.95") | |
| parser.add_argument("--macro-f1-over", choices=["all", "present"], default="all") | |
| parser.add_argument("--latency", type=int, default=0) | |
| parser.add_argument("--out", type=Path, default=None, help="write the report as JSON") | |
| args = parser.parse_args() | |
| path = Path(args.model) if Path(args.model).is_dir() else Path(snapshot_download(args.model, revision=args.revision)) | |
| model = RlcdClassifier(path) | |
| heads, temperatures = model.config["heads"], model.config["temperatures"] | |
| rows = [json.loads(line) for line in args.data.read_text().splitlines() if line.strip()] | |
| texts = [r.get("text") or render(r["messages"]) for r in rows] | |
| probs: dict[str, list[torch.Tensor]] = {h: [] for h in heads} | |
| with torch.no_grad(): | |
| for i in range(0, len(texts), 16): | |
| logits = model.logits(texts[i : i + 16]) | |
| for h in heads: | |
| probs[h].append((logits[h] / temperatures[h]).softmax(-1)) | |
| probs = {h: torch.cat(p) for h, p in probs.items()} | |
| weights = torch.ones(len(rows)) | |
| if args.prior: | |
| prior = {k: float(v) for k, v in (item.split("=") for item in args.prior.split(","))} | |
| levels = [r.get("sensitivity") for r in rows] | |
| share = {lvl: levels.count(lvl) / len(levels) for lvl in set(levels)} | |
| weights = torch.tensor([prior.get(lvl, 0.0) / share[lvl] for lvl in levels]) | |
| report: dict = {"rows": len(rows), "provenance": "measured", "heads": {}} | |
| thresholds = [float(t) for t in args.thresholds.split(",")] | |
| def metrics(p: torch.Tensor, labels: torch.Tensor, w: torch.Tensor, classes: int) -> dict: | |
| confidence, prediction = p.max(-1) | |
| correct = prediction.eq(labels).float() | |
| over = range(classes) if args.macro_f1_over == "all" else sorted(set(labels.tolist()) | set(prediction.tolist())) | |
| f1s = [] | |
| for c in over: | |
| tp = (w * ((prediction == c) & (labels == c))).sum() | |
| fp, fn = (w * ((prediction == c) & (labels != c))).sum(), (w * ((prediction != c) & (labels == c))).sum() | |
| f1s.append(0.0 if tp == 0 else (2 * tp / (2 * tp + fp + fn)).item()) | |
| ece = 0.0 | |
| for low, high in itertools.pairwise(torch.linspace(0, 1, 16)): | |
| in_bin = (confidence > low) & (confidence <= high) | |
| if in_bin.any(): | |
| bw = w[in_bin] | |
| gap = ((confidence[in_bin] * bw).sum() - (correct[in_bin] * bw).sum()) / bw.sum() | |
| ece += (bw.sum() / w.sum()).item() * abs(gap.item()) | |
| curve = {} | |
| for t in thresholds: | |
| kept = confidence >= t | |
| accuracy = ((correct[kept] * w[kept]).sum() / w[kept].sum()).item() if kept.any() else None | |
| curve[str(t)] = {"coverage": round((w[kept].sum() / w.sum()).item(), 4), | |
| "accuracy": None if accuracy is None else round(accuracy, 4)} | |
| return {"accuracy": round(((correct * w).sum() / w.sum()).item(), 4), | |
| "macro_f1": round(sum(f1s) / max(1, len(f1s)), 4), "ece": round(ece, 4), "coverage_accuracy": curve} | |
| for h, classes in heads.items(): | |
| keep = [i for i, r in enumerate(rows) if r.get(h) in classes] | |
| if not keep: | |
| continue | |
| labels = torch.tensor([classes.index(rows[i][h]) for i in keep]) | |
| report["heads"][h] = {"rows": len(keep), **metrics(probs[h][keep], labels, torch.ones(len(keep)), len(classes))} | |
| if args.prior: # beside the as-sampled figures, never instead of them | |
| report["heads"][h]["under_prior"] = metrics(probs[h][keep], labels, weights[keep], len(classes)) | |
| if "sensitivity" in heads: | |
| order = heads["sensitivity"] | |
| start = order.index(args.sensitive_from) | |
| rank = order.index | |
| safety: dict = {"tighten_at": args.tighten} | |
| variants = {"rlcd": lambda i, lvl: lvl} | |
| if any("dlp" in r for r in rows): | |
| variants["rlcd_or_dlp"] = lambda i, lvl: max(lvl, rows[i].get("dlp") or order[0], key=rank) | |
| for name, combine in variants.items(): | |
| effective = [combine(i, tighten(dict(zip(order, probs["sensitivity"][i].tolist(), strict=True)), order, args.tighten)) | |
| for i in range(len(rows))] | |
| sensitive = [i for i, r in enumerate(rows) if r.get("sensitivity") in order[start:]] | |
| under = [i for i in sensitive if rank(effective[i]) < rank(rows[i]["sensitivity"])] | |
| total, hits = sum(weights[i].item() for i in sensitive), sum(weights[i].item() for i in under) | |
| entry = {"under_classified": {"rate": round(len(under) / len(sensitive), 4) if sensitive else None, | |
| "of": len(sensitive), "ci95": [round(x, 4) for x in wilson(len(under), len(sensitive))], | |
| **({"rate_under_prior": round(hits / total, 4) if total else None} if args.prior else {})}} | |
| for level in order[start:]: | |
| at = [i for i in sensitive if rows[i]["sensitivity"] == level] | |
| n_under = sum(1 for i in at if rank(effective[i]) < rank(level)) | |
| entry[f"under_{level}"] = {"rate": round(n_under / len(at), 4) if at else None, "of": len(at), | |
| "ci95": [round(x, 4) for x in wilson(n_under, len(at))]} | |
| plain = [i for i, r in enumerate(rows) if r.get("sensitivity") in order[:start]] | |
| for level in order[start:]: | |
| raised = sum(1 for i in plain if rank(effective[i]) >= rank(level)) | |
| entry[f"raised_to_{level}_or_above"] = {"rate": round(raised / len(plain), 4) if plain else None, | |
| "of": len(plain)} | |
| safety[name] = entry | |
| report["sensitivity_safety"] = safety | |
| if args.latency: | |
| sample = [rows[i % len(rows)] for i in range(args.latency)] | |
| timings = [] | |
| for row in sample: | |
| started = time.perf_counter() | |
| model.predict(row.get("messages") or [{"role": "user", "content": row["text"]}]) | |
| timings.append((time.perf_counter() - started) * 1000) | |
| timings.sort() | |
| report["latency_ms"] = {"p50": round(timings[len(timings) // 2], 1), | |
| "p95": round(timings[min(len(timings) - 1, int(0.95 * len(timings)))], 1), | |
| "requests": len(timings), "threads": torch.get_num_threads()} | |
| print(json.dumps(report, indent=1)) | |
| if args.out: | |
| args.out.write_text(json.dumps(report, indent=1)) | |
| if __name__ == "__main__": | |
| main() | |