Zero-Shot Classification
Transformers
Safetensors
Arabic
llama
feature-extraction
arabic
prompt-routing
router
text-generation-inference
Instructions to use oddadmix/Nawah-Router-v3 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use oddadmix/Nawah-Router-v3 with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("zero-shot-classification", model="oddadmix/Nawah-Router-v3")# Load model directly from transformers import AutoTokenizer, AutoModel tokenizer = AutoTokenizer.from_pretrained("oddadmix/Nawah-Router-v3") model = AutoModel.from_pretrained("oddadmix/Nawah-Router-v3", device_map="auto") - Notebooks
- Google Colab
- Kaggle
File size: 6,912 Bytes
9f0cc33 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | """
Merge the v1 and v2 router caches, shuffle lanes, audit for shortcuts, and write the splits.
v1 (out_router, task ids 100000-116063) is topical routing across 18 business verticals.
v2 (out_router_v2, ids 1000000+) adds compound lane sets, ten single-axis taxonomies and 45
verticals. The id ranges are disjoint by construction, so the two caches concatenate safely and
v1 never had to be regenerated.
Four splits, in increasing order of how much they ask of the model:
unseen_lanes - new lane sets, axes and domains seen in training
unseen_domain - three business verticals held out entirely (kept identical to v1 so the 0.6665
number stays comparable)
unseen_axis - `tools` and `retrieval` never appear in training at all. The strongest
zero-shot claim available: routing on a dimension the model was never taught.
hard - deliberately adjacent lanes
Lanes are shuffled here and the label recomputed, so whatever positional bias the generator had
cannot survive into training.
"""
import json
import os
import random
from collections import Counter, defaultdict
from pathlib import Path
import router_axes as ra
V1 = Path("out_router"); V2 = Path("out_router_v2")
OUT = Path(os.environ.get("DATA_DIR", "data_v2")); OUT.mkdir(exist_ok=True)
HELD_DOMAINS = ["فرز طبي أولي", "استشارات قانونية", "شركة تأمين"]
HELD_AXES = ["tools", "retrieval"]
EVAL_TASK_FRAC = float(os.environ.get("EVAL_TASK_FRAC", 0.02))
SEED = 42
def axes_map(cache):
m = {}
p = cache / "generations.jsonl"
if not p.exists():
return m
for line in open(p, encoding="utf-8"):
try:
r = json.loads(line)
except json.JSONDecodeError:
continue
m[r["task_id"]] = r["axes"]
return m
def load(cache, amap, default_axis):
kept, amb = [], 0
p = cache / "verified.jsonl"
if not p.exists():
return kept, amb
for line in open(p, encoding="utf-8"):
try:
r = json.loads(line)
except json.JSONDecodeError:
continue
if not r["agree"]:
amb += 1
continue
a = amap.get(r["task_id"], {})
axis = a.get("axis", default_axis)
r["axis"] = axis
r["family"] = axis.split(":")[0] if ":" in axis else axis
r["mode"] = a.get("mode", "topical")
kept.append(r)
return kept, amb
def main():
rng = random.Random(SEED)
a1, a2 = axes_map(V1), axes_map(V2)
r1, amb1 = load(V1, a1, "topical")
r2, amb2 = load(V2, a2, "topical")
print(f"v1 kept {len(r1):,} (ambiguous {amb1:,}) | v2 kept {len(r2):,} (ambiguous {amb2:,})")
rows = []
for r in r1 + r2:
order = list(range(len(r["routes"])))
rng.shuffle(order)
rows.append({"task_id": r["task_id"], "text": r["text"],
"routes": [r["routes"][i] for i in order],
"label": order.index(r["label"]),
"axis": r["axis"], "family": r["family"], "mode": r["mode"],
"domain": r.get("domain", ""), "style": r.get("style", "phrase"),
"difficulty": r.get("difficulty", "easy"),
"has_other": r.get("has_other", False)})
seen, ded = set(), []
for r in rows:
k = ra.dedup_key(r["text"])
if k in seen:
continue
seen.add(k); ded.append(r)
print(f"merged {len(rows):,} -> {len(ded):,} after dedup ({len(rows)-len(ded):,} duplicates)")
rows = ded
axis_rows = [r for r in rows if r["family"] in HELD_AXES]
rest = [r for r in rows if r["family"] not in HELD_AXES]
dom_rows = [r for r in rest if r["domain"] in HELD_DOMAINS]
rest = [r for r in rest if r["domain"] not in HELD_DOMAINS]
tasks = sorted({r["task_id"] for r in rest}); rng.shuffle(tasks)
ev_tasks = set(tasks[: max(1, int(len(tasks) * EVAL_TASK_FRAC))])
unseen_lanes = [r for r in rest if r["task_id"] in ev_tasks]
train = [r for r in rest if r["task_id"] not in ev_tasks]
# A lane set invented twice by different tasks would put a vocabulary the model trained on
# into a set labelled "unseen". Drop those from eval rather than weaken the claim.
tls = {ra.lane_set_key(r["routes"]) for r in train}
before = len(unseen_lanes), len(dom_rows), len(axis_rows)
unseen_lanes = [r for r in unseen_lanes if ra.lane_set_key(r["routes"]) not in tls]
dom_rows = [r for r in dom_rows if ra.lane_set_key(r["routes"]) not in tls]
axis_rows = [r for r in axis_rows if ra.lane_set_key(r["routes"]) not in tls]
print(f"lane-set overlap dropped: lanes {before[0]-len(unseen_lanes)}, "
f"domain {before[1]-len(dom_rows)}, axis {before[2]-len(axis_rows)}")
hard = [r for r in unseen_lanes if r["difficulty"] == "hard"]
for name, s in (("train", train), ("eval_unseen_lanes", unseen_lanes),
("eval_unseen_domain", dom_rows), ("eval_unseen_axis", axis_rows),
("eval_hard", hard)):
with open(OUT / f"{name}.jsonl", "w", encoding="utf-8") as fh:
for r in s:
fh.write(json.dumps(r, ensure_ascii=False) + "\n")
print(f" {name:<20}{len(s):>8,}")
leaked = {r["family"] for r in train} & set(HELD_AXES)
print(f"\nheld-out axes present in train: {leaked or 'none'} (must be none)")
print(f"train texts also in any eval: "
f"{len({r['text'] for r in train} & {r['text'] for r in unseen_lanes+dom_rows+axis_rows})}")
print("\n--- composition ---")
print("by mode:", dict(Counter(r["mode"] for r in rows).most_common()))
fam = Counter(r["family"] for r in rows)
print("by axis family:", dict(fam.most_common()))
print(f"distinct lane sets: {len({ra.lane_set_key(r['routes']) for r in rows}):,}")
print("\n--- shortcut audit ---")
groups = defaultdict(list)
for r in rows:
groups[len(r["routes"])].append(r)
for k in sorted(groups):
g = groups[k]
if len(g) < 200:
continue
c = Counter(r["label"] for r in g); exp = len(g) / k
chi = sum((c[i] - exp) ** 2 / exp for i in range(k))
crit = {1: 3.8, 2: 6.0, 3: 7.8, 4: 9.5, 5: 11.1, 6: 12.6, 7: 14.1, 8: 15.5}.get(k - 1, 15.5)
print(f" {k} lanes n={len(g):<7} chi2={chi:7.1f} crit={crit:<5} "
f"{'FLAT' if chi < crit else 'SKEWED'}")
ranks = []
for r in rows:
L = [len(x) for x in r["routes"]]
ranks.append(sorted(L, reverse=True).index(L[r["label"]]) / max(len(L) - 1, 1))
print(f" correct-lane length rank: {sum(ranks)/len(ranks):.3f} (0.5 = no bias)")
give = sum(1 for r in rows if r["routes"][r["label"]] in r["text"])
print(f" verbatim lane in prompt: {give} ({give/len(rows):.2%})")
print(f" TOTAL ROWS: {len(rows):,}")
if __name__ == "__main__":
main()
|