oddadmix commited on
Commit
9f0cc33
·
verified ·
1 Parent(s): 1a32a1c

Nawah-Router-v3: 52M, 0.9305 unseen lanes / 0.6976 unseen domains

Browse files

Trained on the 233,720-row arabic-prompt-routing corpus with 12 routing axes.
Adds an unseen_axis eval: two routing dimensions absent from training entirely.

README.md ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ language:
4
+ - ar
5
+ base_model: oddadmix/50M-2048-Emhotob
6
+ library_name: transformers
7
+ tags: [arabic, zero-shot-classification, prompt-routing, router]
8
+ ---
9
+
10
+ # Nawah-Router-v3 — موجّه عربي صفري
11
+
12
+ **52M parameters.** Give it a text and **any categories in plain Arabic**; it scores all of them in
13
+ **one forward pass**. Categories are chosen at inference — no fixed taxonomy.
14
+
15
+ > **بالعربية:** نموذج عربي يوجّه أي نص إلى فئة من فئات تكتبها أنت بلغة طبيعية، في مسار واحد.
16
+
17
+ ## Results
18
+
19
+ Trained on [`oddadmix/arabic-prompt-routing`](https://huggingface.co/datasets/oddadmix/arabic-prompt-routing)
20
+ (233,720 rows, 12 routing axes).
21
+
22
+ | eval | v3 | v2 (51K corpus) | random |
23
+ |---|---:|---:|---:|
24
+ | unseen category sets | **0.9305** | 0.9199 | 0.2137 |
25
+ | unseen domains | **0.6976** | 0.6665 | 0.2521 |
26
+ | **unseen axes** | **0.6130** | *(n/a)* | 0.2358 |
27
+ | deliberately adjacent categories | 0.9008 | 0.9149 | 0.2109 |
28
+
29
+ `unseen_axis` is the strongest claim here: `tools` and `retrieval` appear **nowhere** in training,
30
+ and the model still routes along them at 0.61 against a 0.24 baseline.
31
+
32
+ ## Usage
33
+
34
+ ```python
35
+ from transformers import AutoTokenizer
36
+ from routing_model import RouterModel, route # ships in this repo
37
+
38
+ M = "oddadmix/Nawah-Router-v3"
39
+ tok = AutoTokenizer.from_pretrained(M)
40
+ model = RouterModel.from_pretrained(M)
41
+
42
+ route(model, tok, "كم صار سعر صرف الدولار اليوم؟",
43
+ ["بحث في الويب", "حاسبة", "تقويم ومواعيد", "لا يحتاج أداة"])
44
+ ```
45
+
46
+ ## Choosing a checkpoint
47
+
48
+ No single configuration wins everything, and the trade-off is real:
49
+
50
+ | epochs | unseen lanes | unseen domains | unseen axes |
51
+ |---:|---:|---:|---:|
52
+ | 1 | **0.9327** | 0.6968 | 0.5950 |
53
+ | **2** (this) | 0.9305 | **0.6976** | 0.6130 |
54
+ | 3 | 0.9181 | 0.6496 | **0.6376** |
55
+
56
+ Longer training helps the hardest transfer (unseen axes) and costs the everyday cases. 2 epochs is
57
+ shipped as the balance. Higher learning rates are simply worse — 6e-4 and 1e-3 both degrade.
58
+
59
+ ## How the head works
60
+
61
+ Text and categories share one sequence, **text first**. Each category's span is mean-pooled into
62
+ its own vector and a **shared** scorer turns each into one logit; the softmax runs over the
63
+ categories supplied. Because the scorer is shared it reads category *content*, not slot index —
64
+ which is what makes the label set free text.
65
+
66
+ A fixed-slot head (`num_labels = max_lanes`) scored **exactly 1/n at every lane count**: its
67
+ weights were positional, and the corpus randomises category order, so there was nothing to learn.
68
+
69
+ ## Limitations
70
+
71
+ Unseen domains (0.70) and unseen axes (0.61) trail unseen category sets (0.93) — it generalises
72
+ best inside verticals and dimensions it has seen. Confidence is **not calibrated**: clear cases
73
+ saturate near 1.0, so use the ranking, not the number. Arabic, 1–3 line messages.
74
+
75
+ 3 epochs at LR 3e-4 cosine, batch 32, bf16, `max_length` 320.
build_router_dataset_v2.py ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Merge the v1 and v2 router caches, shuffle lanes, audit for shortcuts, and write the splits.
3
+
4
+ v1 (out_router, task ids 100000-116063) is topical routing across 18 business verticals.
5
+ v2 (out_router_v2, ids 1000000+) adds compound lane sets, ten single-axis taxonomies and 45
6
+ verticals. The id ranges are disjoint by construction, so the two caches concatenate safely and
7
+ v1 never had to be regenerated.
8
+
9
+ Four splits, in increasing order of how much they ask of the model:
10
+
11
+ unseen_lanes - new lane sets, axes and domains seen in training
12
+ unseen_domain - three business verticals held out entirely (kept identical to v1 so the 0.6665
13
+ number stays comparable)
14
+ unseen_axis - `tools` and `retrieval` never appear in training at all. The strongest
15
+ zero-shot claim available: routing on a dimension the model was never taught.
16
+ hard - deliberately adjacent lanes
17
+
18
+ Lanes are shuffled here and the label recomputed, so whatever positional bias the generator had
19
+ cannot survive into training.
20
+ """
21
+ import json
22
+ import os
23
+ import random
24
+ from collections import Counter, defaultdict
25
+ from pathlib import Path
26
+
27
+ import router_axes as ra
28
+
29
+ V1 = Path("out_router"); V2 = Path("out_router_v2")
30
+ OUT = Path(os.environ.get("DATA_DIR", "data_v2")); OUT.mkdir(exist_ok=True)
31
+ HELD_DOMAINS = ["فرز طبي أولي", "استشارات قانونية", "شركة تأمين"]
32
+ HELD_AXES = ["tools", "retrieval"]
33
+ EVAL_TASK_FRAC = float(os.environ.get("EVAL_TASK_FRAC", 0.02))
34
+ SEED = 42
35
+
36
+
37
+ def axes_map(cache):
38
+ m = {}
39
+ p = cache / "generations.jsonl"
40
+ if not p.exists():
41
+ return m
42
+ for line in open(p, encoding="utf-8"):
43
+ try:
44
+ r = json.loads(line)
45
+ except json.JSONDecodeError:
46
+ continue
47
+ m[r["task_id"]] = r["axes"]
48
+ return m
49
+
50
+
51
+ def load(cache, amap, default_axis):
52
+ kept, amb = [], 0
53
+ p = cache / "verified.jsonl"
54
+ if not p.exists():
55
+ return kept, amb
56
+ for line in open(p, encoding="utf-8"):
57
+ try:
58
+ r = json.loads(line)
59
+ except json.JSONDecodeError:
60
+ continue
61
+ if not r["agree"]:
62
+ amb += 1
63
+ continue
64
+ a = amap.get(r["task_id"], {})
65
+ axis = a.get("axis", default_axis)
66
+ r["axis"] = axis
67
+ r["family"] = axis.split(":")[0] if ":" in axis else axis
68
+ r["mode"] = a.get("mode", "topical")
69
+ kept.append(r)
70
+ return kept, amb
71
+
72
+
73
+ def main():
74
+ rng = random.Random(SEED)
75
+ a1, a2 = axes_map(V1), axes_map(V2)
76
+ r1, amb1 = load(V1, a1, "topical")
77
+ r2, amb2 = load(V2, a2, "topical")
78
+ print(f"v1 kept {len(r1):,} (ambiguous {amb1:,}) | v2 kept {len(r2):,} (ambiguous {amb2:,})")
79
+
80
+ rows = []
81
+ for r in r1 + r2:
82
+ order = list(range(len(r["routes"])))
83
+ rng.shuffle(order)
84
+ rows.append({"task_id": r["task_id"], "text": r["text"],
85
+ "routes": [r["routes"][i] for i in order],
86
+ "label": order.index(r["label"]),
87
+ "axis": r["axis"], "family": r["family"], "mode": r["mode"],
88
+ "domain": r.get("domain", ""), "style": r.get("style", "phrase"),
89
+ "difficulty": r.get("difficulty", "easy"),
90
+ "has_other": r.get("has_other", False)})
91
+
92
+ seen, ded = set(), []
93
+ for r in rows:
94
+ k = ra.dedup_key(r["text"])
95
+ if k in seen:
96
+ continue
97
+ seen.add(k); ded.append(r)
98
+ print(f"merged {len(rows):,} -> {len(ded):,} after dedup ({len(rows)-len(ded):,} duplicates)")
99
+ rows = ded
100
+
101
+ axis_rows = [r for r in rows if r["family"] in HELD_AXES]
102
+ rest = [r for r in rows if r["family"] not in HELD_AXES]
103
+ dom_rows = [r for r in rest if r["domain"] in HELD_DOMAINS]
104
+ rest = [r for r in rest if r["domain"] not in HELD_DOMAINS]
105
+ tasks = sorted({r["task_id"] for r in rest}); rng.shuffle(tasks)
106
+ ev_tasks = set(tasks[: max(1, int(len(tasks) * EVAL_TASK_FRAC))])
107
+ unseen_lanes = [r for r in rest if r["task_id"] in ev_tasks]
108
+ train = [r for r in rest if r["task_id"] not in ev_tasks]
109
+
110
+ # A lane set invented twice by different tasks would put a vocabulary the model trained on
111
+ # into a set labelled "unseen". Drop those from eval rather than weaken the claim.
112
+ tls = {ra.lane_set_key(r["routes"]) for r in train}
113
+ before = len(unseen_lanes), len(dom_rows), len(axis_rows)
114
+ unseen_lanes = [r for r in unseen_lanes if ra.lane_set_key(r["routes"]) not in tls]
115
+ dom_rows = [r for r in dom_rows if ra.lane_set_key(r["routes"]) not in tls]
116
+ axis_rows = [r for r in axis_rows if ra.lane_set_key(r["routes"]) not in tls]
117
+ print(f"lane-set overlap dropped: lanes {before[0]-len(unseen_lanes)}, "
118
+ f"domain {before[1]-len(dom_rows)}, axis {before[2]-len(axis_rows)}")
119
+ hard = [r for r in unseen_lanes if r["difficulty"] == "hard"]
120
+
121
+ for name, s in (("train", train), ("eval_unseen_lanes", unseen_lanes),
122
+ ("eval_unseen_domain", dom_rows), ("eval_unseen_axis", axis_rows),
123
+ ("eval_hard", hard)):
124
+ with open(OUT / f"{name}.jsonl", "w", encoding="utf-8") as fh:
125
+ for r in s:
126
+ fh.write(json.dumps(r, ensure_ascii=False) + "\n")
127
+ print(f" {name:<20}{len(s):>8,}")
128
+
129
+ leaked = {r["family"] for r in train} & set(HELD_AXES)
130
+ print(f"\nheld-out axes present in train: {leaked or 'none'} (must be none)")
131
+ print(f"train texts also in any eval: "
132
+ f"{len({r['text'] for r in train} & {r['text'] for r in unseen_lanes+dom_rows+axis_rows})}")
133
+
134
+ print("\n--- composition ---")
135
+ print("by mode:", dict(Counter(r["mode"] for r in rows).most_common()))
136
+ fam = Counter(r["family"] for r in rows)
137
+ print("by axis family:", dict(fam.most_common()))
138
+ print(f"distinct lane sets: {len({ra.lane_set_key(r['routes']) for r in rows}):,}")
139
+
140
+ print("\n--- shortcut audit ---")
141
+ groups = defaultdict(list)
142
+ for r in rows:
143
+ groups[len(r["routes"])].append(r)
144
+ for k in sorted(groups):
145
+ g = groups[k]
146
+ if len(g) < 200:
147
+ continue
148
+ c = Counter(r["label"] for r in g); exp = len(g) / k
149
+ chi = sum((c[i] - exp) ** 2 / exp for i in range(k))
150
+ 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)
151
+ print(f" {k} lanes n={len(g):<7} chi2={chi:7.1f} crit={crit:<5} "
152
+ f"{'FLAT' if chi < crit else 'SKEWED'}")
153
+ ranks = []
154
+ for r in rows:
155
+ L = [len(x) for x in r["routes"]]
156
+ ranks.append(sorted(L, reverse=True).index(L[r["label"]]) / max(len(L) - 1, 1))
157
+ print(f" correct-lane length rank: {sum(ranks)/len(ranks):.3f} (0.5 = no bias)")
158
+ give = sum(1 for r in rows if r["routes"][r["label"]] in r["text"])
159
+ print(f" verbatim lane in prompt: {give} ({give/len(rows):.2%})")
160
+ print(f" TOTAL ROWS: {len(rows):,}")
161
+
162
+
163
+ if __name__ == "__main__":
164
+ main()
config.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "LlamaModel"
4
+ ],
5
+ "attention_bias": false,
6
+ "attention_dropout": 0.0,
7
+ "bos_token_id": 0,
8
+ "dtype": "float32",
9
+ "eos_token_id": 2,
10
+ "head_dim": 64,
11
+ "hidden_act": "silu",
12
+ "hidden_size": 512,
13
+ "initializer_range": 0.02,
14
+ "intermediate_size": 1408,
15
+ "max_position_embeddings": 2048,
16
+ "mlp_bias": false,
17
+ "model_type": "llama",
18
+ "num_attention_heads": 8,
19
+ "num_hidden_layers": 12,
20
+ "num_key_value_heads": 4,
21
+ "pad_token_id": 1,
22
+ "pretraining_tp": 1,
23
+ "rms_norm_eps": 1e-06,
24
+ "rope_parameters": {
25
+ "rope_theta": 10000,
26
+ "rope_type": "default"
27
+ },
28
+ "tie_word_embeddings": true,
29
+ "transformers_version": "5.15.1",
30
+ "use_cache": false,
31
+ "vocab_size": 32000
32
+ }
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b5200d894303508b35530632e7f86e433596cb9dd9a0e6eb124aaf20a75b8f8c
3
+ size 207156480
router_axes.py ADDED
@@ -0,0 +1,460 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Arabic prompt-routing corpus v2 — many routing axes, and lane sets that mix them.
3
+
4
+ v1 (router_common.py) generated one axis only: topical intent inside a business vertical, 18
5
+ domains. It reached 0.9199 on unseen lane sets but stalled at 0.6665 on unseen domains, and the
6
+ reference demo shows why that is the wrong shape rather than merely too small. LiquidAI's shipped
7
+ presets mix axes inside a single lane set:
8
+
9
+ Simple function call · Simple tool use · Complex multi-step agentic task ·
10
+ Quick factual question · Casual conversation · Creative writing · Translation ·
11
+ Needs a bigger model
12
+
13
+ That is complexity, task type, tool use and an escalation lane together. Every v1 lane set was
14
+ single-axis, so the model never saw categories that differ along different dimensions at once.
15
+
16
+ Three generation modes here:
17
+
18
+ topical - v1's mode, widened from 18 to 45 verticals. The model invents the lanes.
19
+ axis - lanes drawn from one of ten taxonomies (task type, complexity, safety, retrieval,
20
+ tools, subject, subdomain, language, urgency, tone), each with several paraphrased
21
+ wordings so the model learns the concept and not the string.
22
+ mixed - lanes sampled across two or three axes, as the reference does. The largest share.
23
+
24
+ `router_common.py` is deliberately untouched: it defines build_task for task ids 100000-116063 and
25
+ editing it would silently redraw prompts for the 51,392 rows already generated. This module owns a
26
+ disjoint range (START_TASK=1000000) and its output is merged with v1's at build time.
27
+
28
+ Labels are still established by construction and confirmed by the independent back-classification
29
+ in verify_router.py, which is axis-agnostic and needs no change.
30
+ """
31
+ import random
32
+ import re
33
+ import unicodedata
34
+
35
+ ITEMS_PER_TASK = 4
36
+
37
+ C, Q, A, E = "### فئات", "### نص", "### الفئة", "### نهاية"
38
+
39
+ # Varied on purpose. v1 used one fixed string, which teaches the token rather than the concept;
40
+ # the reference ships a catch-all in most presets under several different names.
41
+ OTHER_LANES = ["أخرى / لا ينطبق", "غير ذلك", "لا ينطبق أي مما سبق", "أخرى",
42
+ "خارج التصنيفات المذكورة"]
43
+
44
+ # ---------------------------------------------------------------- axis taxonomies
45
+ # Each lane carries several wordings; build_task picks one per task so the corpus never ties a
46
+ # concept to a single surface form. Wordings deliberately avoid a shared give-away token
47
+ # (e.g. not every complexity lane contains بسيط/معقد).
48
+
49
+ AXES = {
50
+ "task_type": {
51
+ "hint": "مستخدم يطلب من مساعد ذكي مهمة",
52
+ "lanes": [
53
+ ["ترجمة", "نقل نص من لغة إلى أخرى", "طلب ترجمة"],
54
+ ["برمجة", "كتابة أو إصلاح كود", "مهمة برمجية"],
55
+ ["تلخيص", "اختصار نص طويل", "طلب ملخّص"],
56
+ ["كتابة إبداعية", "تأليف قصة أو نص أدبي", "صياغة محتوى إبداعي"],
57
+ ["رياضيات", "حساب أو مسألة رياضية", "عملية حسابية"],
58
+ ["استخراج بيانات", "انتزاع حقول من نص", "تحويل نص إلى بيانات منظمة"],
59
+ ["سؤال معلومات", "طلب معلومة مباشرة", "سؤال معرفي عام"],
60
+ ["محادثة", "دردشة غير مهمّية", "كلام عابر"],
61
+ ["إعادة صياغة", "تحسين صياغة نص قائم", "تحرير وتنقيح"],
62
+ ["تصنيف", "تحديد فئة نص", "فرز محتوى"],
63
+ ],
64
+ },
65
+ "complexity": {
66
+ "hint": "مستخدم يكتب طلبًا يتفاوت في صعوبته",
67
+ "lanes": [
68
+ ["طلب بسيط ومباشر", "يُنجز بخطوة واحدة", "مهمة سهلة لا تحتاج تفكيرًا"],
69
+ ["طلب متوسط", "يحتاج خطوتين أو ثلاثًا", "مهمة معتدلة"],
70
+ ["طلب معقّد متعدد الخطوات", "يحتاج تخطيطًا وتسلسلًا طويلًا", "مهمة مركّبة"],
71
+ ["يحتاج نموذجًا أكبر", "يفوق قدرة نموذج صغير", "يُحال إلى نموذج أقوى"],
72
+ ["يحتاج استدلالًا خطوة بخطوة", "يتطلب سلسلة تفكير", "حل يحتاج برهنة"],
73
+ ],
74
+ },
75
+ "safety": {
76
+ "hint": "رسالة واردة تحتاج فرزًا من ناحية السلامة",
77
+ "lanes": [
78
+ ["محتوى آمن", "لا مشكلة فيه", "مقبول للنشر"],
79
+ ["رسائل مزعجة", "إعلانات متكررة غير مرغوبة", "سبام تجاري"],
80
+ ["يحتوي بيانات شخصية", "فيه معلومات تعريف حساسة", "يكشف خصوصية أحدهم"],
81
+ ["لغة مسيئة", "ألفاظ جارحة", "إهانة أو تنمّر"],
82
+ ],
83
+ },
84
+ "retrieval": {
85
+ "hint": "سؤال قد يحتاج مصدرًا خارجيًا أو لا",
86
+ "lanes": [
87
+ ["يُجاب من المعرفة العامة", "لا يحتاج بحثًا", "إجابة مباشرة بلا مصادر"],
88
+ ["يحتاج بحثًا واحدًا", "يكفيه مصدر واحد", "استعلام واحد يكفي"],
89
+ ["يحتاج بحثًا متعدد الخطوات", "يجمع بين أكثر من مصدر", "يتطلب تتبع عدة معلومات"],
90
+ ["يحتاج معلومة لحظية", "يعتمد على بيانات متغيرة الآن", "يحتاج تحديثًا آنيًا"],
91
+ ],
92
+ },
93
+ "tools": {
94
+ "hint": "طلب قد يستدعي أداة معينة",
95
+ "lanes": [
96
+ ["بحث في الويب", "استعلام محرك بحث", "تصفح الإنترنت"],
97
+ ["حاسبة", "إجراء عملية حسابية", "أداة حساب"],
98
+ ["تقويم ومواعيد", "جدولة موعد", "إدارة الأجندة"],
99
+ ["بريد ورسائل", "إرسال رسالة", "التعامل مع البريد"],
100
+ ["قاعدة بيانات", "استعلام من سجلات", "جلب صف من جدول"],
101
+ ["لا يحتاج أداة", "يُجاب بلا استدعاء خارجي", "بدون أدوات"],
102
+ ["تشغيل كود", "تنفيذ سكربت", "مفسّر برمجي"],
103
+ ],
104
+ },
105
+ "subject": {
106
+ "hint": "سؤال أكاديمي في تخصص ما",
107
+ "lanes": [
108
+ ["أحياء", "علوم الحياة", "بيولوجيا"],
109
+ ["قانون", "مسائل قانونية", "تشريع وأنظمة"],
110
+ ["اقتصاد", "مسائل اقتصادية", "اقتصاد وأسواق"],
111
+ ["هندسة", "مسائل هندسية", "تصميم وتنفيذ هندسي"],
112
+ ["طب وصحة", "مسائل طبية", "صحة وعلاج"],
113
+ ["تاريخ", "أحداث ووقائع تاريخية", "ماضٍ وسير"],
114
+ ["فلسفة", "مسائل فلسفية", "فكر ومنطق"],
115
+ ["فيزياء", "مسائل فيزيائية", "قوانين الطبيعة"],
116
+ ["حاسوب", "علوم الحاسب", "تقنية معلومات"],
117
+ ["علم نفس", "مسائل نفسية", "سلوك وإدراك"],
118
+ ],
119
+ },
120
+ "language": {
121
+ "hint": "رسالة مكتوبة بلهجة أو لغة ما",
122
+ "lanes": [
123
+ ["عربية فصحى", "لغة عربية رسمية", "فصحى معاصرة"],
124
+ ["لهجة مصرية", "عامية مصر", "مصري"],
125
+ ["لهجة خليجية", "عامية الخليج", "خليجي"],
126
+ ["لهجة شامية", "عامية بلاد الشام", "شامي"],
127
+ ["لهجة مغاربية", "عامية المغرب العربي", "مغاربي"],
128
+ ["إنجليزية", "نص بالإنجليزية", "لغة إنجليزية"],
129
+ ["عربي مختلط بالإنجليزية", "كتابة مزدوجة اللغة", "عربيزي أو تبديل لغوي"],
130
+ ],
131
+ },
132
+ "urgency": {
133
+ "hint": "رسالة تتفاوت في إلحاحها",
134
+ "lanes": [
135
+ ["عاجل", "يحتاج تدخلًا فوريًا", "أولوية قصوى"],
136
+ ["أولوية عادية", "يمكن معالجته ضمن الدور", "غير مستعجل"],
137
+ ["أولوية منخفضة", "يمكن تأجيله", "لا يستدعي استعجالًا"],
138
+ ],
139
+ },
140
+ "tone": {
141
+ "hint": "رسالة من عميل بنبرة معينة",
142
+ "lanes": [
143
+ ["شكوى غاضبة", "امتعاض شديد", "نبرة تصعيد"],
144
+ ["استفسار محايد", "سؤال هادئ", "نبرة عادية"],
145
+ ["شكر وثناء", "رسالة امتنان", "إطراء على الخدمة"],
146
+ ["طلب اعتذار أو تعويض", "مطالبة بحق", "طلب جبر ضرر"],
147
+ ],
148
+ },
149
+ }
150
+
151
+ # Mixed lane sets use COMPOUND lanes, not a union of two axes' lanes.
152
+ #
153
+ # The first attempt sampled lanes from two axes at once and produced ill-posed rows: an insulting
154
+ # message written in Gulf dialect belongs to both 'لهجة خليجية' and 'إهانة أو تنمر', and the task
155
+ # is single-label. Constraining to "compatible" axes did not fix it either - an angry complaint is
156
+ # also urgent. Any two independent axes overlap by construction.
157
+ #
158
+ # The reference avoids this because its lanes are already compound: "Simple function call" fuses
159
+ # complexity with tool use, "Complex multi-step agentic task" fuses complexity with task type.
160
+ # Each lane names a point in the joint space, so the set partitions rather than overlaps. These
161
+ # pools mirror that, one per facet-family; a lane set is drawn from a single pool.
162
+ COMPOUND_POOLS = {
163
+ "assistant": (
164
+ "مستخدم يطلب من مساعد ذكي مهمة",
165
+ ["استدعاء دالة بسيط", "استخدام أداة بسيط", "مهمة وكيلة معقدة متعددة الخطوات",
166
+ "استدلال بسيط", "استدلال معقد يحتاج خطوات", "مهمة برمجية بسيطة",
167
+ "مهمة برمجية معقدة", "إصلاح خطأ برمجي صغير", "إعادة هيكلة كود واسعة",
168
+ "مراجعة كود", "كتابة اختبارات", "شرح كود قائم",
169
+ "مسألة حسابية بسيطة", "مسألة رياضية معقدة", "ترجمة", "تلخيص",
170
+ "كتابة إبداعية", "إعادة صياغة نص", "بحث عن معلومة مباشرة",
171
+ "سؤال سريع تُعرف إجابته", "محادثة عابرة", "يحتاج نموذجًا أكبر",
172
+ "يحتاج بحثًا في الويب", "يحتاج تنفيذ كود", "استعلام قاعدة بيانات",
173
+ "تحليل بيانات", "إنشاء رسم بياني", "استخراج حقول من نص",
174
+ "جدولة موعد في التقويم", "صياغة رسالة بريد"],
175
+ ),
176
+ "triage": (
177
+ "رسالة واردة إلى فريق خدمة العملاء",
178
+ ["شكوى غاضبة تحتاج تصعيدًا فوريًا", "استفسار عادي غير مستعجل",
179
+ "بلاغ عطل حرج يوقف الخدمة", "بلاغ عطل بسيط يمكن تأجيله",
180
+ "طلب استرجاع مبلغ", "طلب إلغاء اشتراك", "رسالة شكر وثناء",
181
+ "رسالة مزعجة إعلانية", "إساءة لفظية تستوجب حظرًا",
182
+ "طلب حذف بيانات شخصية", "استفسار عن الفاتورة", "طلب ميزة جديدة",
183
+ "مشكلة في الدخول إلى الحساب", "سؤال عام عن الخدمة"],
184
+ ),
185
+ "content": (
186
+ "نص يُراد تصنيف موضوعه ومستواه",
187
+ ["خبر سياسي", "خبر رياضي", "خبر اقتصادي", "تقرير علمي مبسّط",
188
+ "مقال رأي", "محتوى تعليمي للمبتدئين", "محتوى تقني متقدم",
189
+ "وصفة أو إرشادات عملية", "مراجعة منتج", "إعلان تجاري",
190
+ "نص قانوني رسمي", "نص طبي متخصص", "محتوى ترفيهي خفيف"],
191
+ ),
192
+ }
193
+
194
+ # "Which flavour of X" — the reference's `lang` preset generalised. The model invents the members.
195
+ SUBDOMAIN_FAMILIES = [
196
+ ("لغة البرمجة المستخدمة في السؤال", "مبرمج يسأل عن لغة معينة"),
197
+ ("نوع الوثيقة الرسمية المطلوبة", "مواطن يطلب مستندًا"),
198
+ ("فرع التأمين المعني", "عميل يسأل عن وثيقته"),
199
+ ("نوع الحساب البنكي المقصود", "عميل بنك"),
200
+ ("تخصص الطبيب المطلوب", "مريض يبحث عن عيادة"),
201
+ ("نوع العقار المطلوب", "باحث عن سكن"),
202
+ ("نوع الشحنة المرسلة", "عميل شركة شحن"),
203
+ ("مرحلة دراسية", "ولي أمر يسأل عن التسجيل"),
204
+ ]
205
+
206
+ # v1's 18, plus 27 more.
207
+ DOMAINS = [
208
+ ("دعم فني لشركة برمجيات", "مستخدم يراسل الدعم الفني"),
209
+ ("خدمة عملاء بنك", "عميل يتواصل مع بنكه"),
210
+ ("متجر إلكتروني", "مشترٍ يسأل عن طلب أو منتج"),
211
+ ("حجز سفر وطيران", "مسافر يرتب رحلة"),
212
+ ("خدمات حكومية إلكترونية", "مواطن يطلب معاملة"),
213
+ ("فرز طبي أولي", "مريض يصف حالته"),
214
+ ("شركة اتصالات", "مشترك يسأل عن باقته"),
215
+ ("منصة تعليمية", "طالب أو ولي أمر يستفسر"),
216
+ ("موارد بشرية داخل شركة", "موظف يراسل الموارد البشرية"),
217
+ ("وساطة عقارية", "باحث عن سكن أو مالك"),
218
+ ("تطبيق توصيل طعام", "زبون يتابع طلبه"),
219
+ ("استشارات قانونية", "شخص يسأل عن حقوقه"),
220
+ ("تصنيف مواضيع الأخبار", "قارئ يبحث عن خبر"),
221
+ ("مراجعة المحتوى والإبلاغ", "مستخدم يبلّغ عن محتوى"),
222
+ ("توجيه مساعد ذكي", "مستخدم يطلب من مساعد ذكي مهمة"),
223
+ ("إدارة مالية شخصية", "شخص يدير مصروفاته"),
224
+ ("شركة تأمين", "عميل يسأل عن وثيقته"),
225
+ ("صيانة منزلية", "ساكن يطلب صيانة"),
226
+ ("مطعم وحجز طاولات", "زبون يحجز أو يشتكي"),
227
+ ("صيدلية وطلب دواء", "مريض يسأل عن دواء"),
228
+ ("نادٍ رياضي واشتراكات", "مشترك في صالة رياضية"),
229
+ ("شحن ولوجستيات", "عميل يتابع شحنة"),
230
+ ("معرض وصيانة سيارات", "مالك سيارة"),
231
+ ("بنك استثماري ومحافظ", "مستثمر يسأل عن محفظته"),
232
+ ("سياحة دينية وحج وعمرة", "معتمر ينظّم رحلته"),
233
+ ("جامعة وقبول وتسجيل", "طالب جامعي"),
234
+ ("مكتب محاماة", "موكّل يتابع قضيته"),
235
+ ("زراعة وإرشاد زراعي", "مزارع يسأل عن محصوله"),
236
+ ("خدمة كهرباء ومياه", "مشترك يبلّغ عن انقطاع"),
237
+ ("تأجير سيارات", "مستأجر يحجز مركبة"),
238
+ ("فندق وضيافة", "نزيل يحجز أو يشتكي"),
239
+ ("منصة توظيف", "باحث عن عمل"),
240
+ ("متجر إلكترونيات وضمان", "مشترٍ يطالب بالضمان"),
241
+ ("خدمات بريدية", "مرسِل يتابع طردًا"),
242
+ ("مركز تدريب ودورات", "متدرب يسجّل في دورة"),
243
+ ("عيادة أسنان ومواعيد", "مريض يحجز موعدًا"),
244
+ ("شركة تنظيف منازل", "زبون يطلب خدمة"),
245
+ ("منصة بث ومحتوى", "مشترك يسأل عن اشتراكه"),
246
+ ("متجر ملابس ومقاسات", "زبون يستبدل قطعة"),
247
+ ("خدمة استضافة ونطاقات", "صاحب موقع"),
248
+ ("جمعية خيرية وتبرعات", "متبرع يسأل"),
249
+ ("نقل عام وتذاكر", "راكب يستفسر"),
250
+ ("مكتبة وبيع كتب", "قارئ يطلب كتابًا"),
251
+ ("خدمة محاسبة وضرائب", "صاحب منشأة"),
252
+ ("متجر حيوانات أليفة", "مربّي حيوان"),
253
+ ]
254
+
255
+ STYLES = {
256
+ "noun": ("كلمة واحدة أو كلمتين", "مثل: برمجة، فواتير، شكاوى"),
257
+ "phrase": ("عبارة قصيرة من ٢ إلى ٤ كلمات", "مثل: كتابة إبداعية، إلغاء الاشتراك"),
258
+ "desc": ("جملة وصفية كاملة", "مثل: أسئلة حول الفواتير والمدفوعات وطلبات الاسترجاع"),
259
+ }
260
+
261
+ DIFFICULTY = {
262
+ "easy": "اجعل الفئات متباعدة وواضحة الفروق بينها.",
263
+ "hard": ("اجعل بعض الفئات متقاربة جدًا في المعنى بحيث يصعب التمييز بينها دون قراءة النص "
264
+ "بعناية."),
265
+ }
266
+
267
+ # Mode 1: the model invents the lanes (topical / subdomain).
268
+ PROMPT_INVENT = """أنت تكتب بيانات تدريبية لنموذج يوجّه النصوص إلى فئات يحددها المستخدم.
269
+
270
+ المجال: {domain} ({hint})
271
+
272
+ الخطوة الأولى — اقترح {n} فئة للتوجيه في هذا المجال:
273
+ - اكتب كل فئة بأسلوب: {style_desc} ({style_ex}).
274
+ - {difficulty}
275
+ - الفئات يجب أن تغطي المجال دون تداخل كامل بينها.{other_note}
276
+
277
+ الخطوة الثانية — اكتب {k} نصوص مستقلة، كل نص من شخص حقيقي ({hint}).
278
+ النص رقم i يجب أن ينتمي إلى الفئة المطلوبة له في القائمة أدناه:
279
+ {targets}
280
+
281
+ {fmt}"""
282
+
283
+ # Mode 2/3: the lanes are given (single axis or mixed axes); the model only writes the texts.
284
+ PROMPT_GIVEN = """أنت تكتب بيانات تدريبية لنموذج يوجّه النصوص إلى فئات يحددها المستخدم.
285
+
286
+ الفئات المعطاة (استخدمها كما هي حرفيًا وبنفس الترتيب):
287
+ {lanes_block}
288
+
289
+ السياق: {hint}
290
+
291
+ اكتب {k} نصوص عربية مستقلة وواقعية. النص رقم i يجب أن ينتمي إلى الفئة المطلوبة له:
292
+ {targets}
293
+
294
+ {fmt}"""
295
+
296
+ FMT = """اكتب بهذا الشكل بالضبط:
297
+
298
+ {C}
299
+ (فئة في كل سطر، مسبوقة بـ "- "، بالترتيب وبنفس الصياغة في كل مرة)
300
+ {Q}
301
+ (النص العربي الأول، من سطر إلى ثلاثة أسطر)
302
+ {A}
303
+ (اسم الفئة التي ينتمي إليها هذا النص، منسوخًا حرفيًا من قائمة الفئات)
304
+ {Q}
305
+ (النص الثاني)
306
+ {A}
307
+ (فئته)
308
+ ... وهكذا حتى {k} نصوص
309
+ {E}
310
+
311
+ قواعد مهمة:
312
+ - لا تذكر اسم الفئة داخل النص نفسه. النص يجب أن يدل على فئته بالمعنى لا بالكلمة.
313
+ - اكتب نصوصًا طبيعية كما يكتبها الناس فعلًا، بأطوال مختلفة.
314
+ - نوّع الأسماء والتفاصيل والأسلوب بين النصوص.
315
+ - لا تكتب أي شيء خارج الوسوم."""
316
+
317
+
318
+ def _targets_block(rng, n_lanes, has_other):
319
+ real = list(range(n_lanes))
320
+ targets = [rng.choice(real) for _ in range(ITEMS_PER_TASK)]
321
+ if has_other:
322
+ targets[rng.randrange(ITEMS_PER_TASK)] = n_lanes
323
+ return targets, "\n".join(f"- النص {i+1}: الفئة رقم {t+1}"
324
+ for i, t in enumerate(targets))
325
+
326
+
327
+ def build_task(task_id: int, seed: int = 1234):
328
+ """task_id -> (axes, prompt). Deterministic, so a resumed run redraws identical prompts."""
329
+ rng = random.Random(seed * 7_000_003 + task_id)
330
+ mode = rng.choices(["mixed", "axis", "topical"], weights=[45, 35, 20])[0]
331
+ style = rng.choice(["noun", "phrase", "phrase", "desc"])
332
+ difficulty = "hard" if rng.random() < 0.4 else "easy"
333
+ has_other = rng.random() < 0.30 # v1 used 0.15; the reference uses a catch-all often
334
+ other_lane = rng.choice(OTHER_LANES)
335
+ fmt = FMT.format(C=C, Q=Q, A=A, E=E, k=ITEMS_PER_TASK)
336
+
337
+ if mode == "topical":
338
+ use_sub = rng.random() < 0.2
339
+ domain, hint = rng.choice(SUBDOMAIN_FAMILIES if use_sub else DOMAINS)
340
+ n = rng.choice([2, 3, 3, 4, 4, 4, 5, 5, 6, 7, 8])
341
+ sd, sx = STYLES[style]
342
+ other_note = (f"\n- أضف في نهاية القائمة فئة أخيرة اسمها بالضبط: {other_lane}"
343
+ "\n- واجعل أحد النصوص لا ينتمي لأي فئة إلا هذه الفئة الأخيرة.") \
344
+ if has_other else ""
345
+ targets, tgt = _targets_block(rng, n, has_other)
346
+ axes = {"mode": mode, "axis": "subdomain" if use_sub else "topical", "domain": domain,
347
+ "style": style, "difficulty": difficulty, "has_other": has_other,
348
+ # total lane count, Other included - the axis/mixed branch counts the same way
349
+ "n_routes": n + (1 if has_other else 0), "targets": targets}
350
+ return axes, PROMPT_INVENT.format(domain=domain, hint=hint, n=n, k=ITEMS_PER_TASK,
351
+ style_desc=sd, style_ex=sx,
352
+ difficulty=DIFFICULTY[difficulty],
353
+ other_note=other_note, targets=tgt, fmt=fmt)
354
+
355
+ if mode == "axis":
356
+ name = rng.choice(list(AXES))
357
+ spec = AXES[name]
358
+ k = min(len(spec["lanes"]), rng.choice([3, 3, 4, 4, 5, 5, 6, 7]))
359
+ picked = rng.sample(spec["lanes"], k)
360
+ lanes = [rng.choice(w) for w in picked] # one wording per lane, per task
361
+ hint = spec["hint"]
362
+ axis_label, domain = name, name
363
+ else: # mixed - compound lanes from one pool
364
+ pool_name = rng.choice(list(COMPOUND_POOLS))
365
+ hint, pool = COMPOUND_POOLS[pool_name]
366
+ k = min(len(pool), rng.choice([3, 4, 4, 5, 5, 6, 6, 7, 8]))
367
+ lanes = rng.sample(pool, k)
368
+ axis_label, domain = "compound:" + pool_name, pool_name
369
+
370
+ if has_other:
371
+ lanes = lanes + [other_lane]
372
+ rng.shuffle(lanes)
373
+ n = len(lanes) - (1 if has_other else 0)
374
+ other_idx = lanes.index(other_lane) if has_other else None
375
+ real_idx = [i for i in range(len(lanes)) if i != other_idx]
376
+ targets = [rng.choice(real_idx) for _ in range(ITEMS_PER_TASK)]
377
+ if has_other:
378
+ targets[rng.randrange(ITEMS_PER_TASK)] = other_idx
379
+ tgt = "\n".join(f"- النص {i+1}: الفئة رقم {t+1}" for i, t in enumerate(targets))
380
+ lanes_block = "\n".join(f"- {c}" for c in lanes)
381
+ axes = {"mode": mode, "axis": axis_label, "domain": domain, "style": style,
382
+ "difficulty": difficulty, "has_other": has_other,
383
+ "n_routes": len(lanes), "targets": targets}
384
+ return axes, PROMPT_GIVEN.format(lanes_block=lanes_block, hint=hint, k=ITEMS_PER_TASK,
385
+ targets=tgt, fmt=fmt)
386
+
387
+
388
+ # ---------------------------------------------------------------- parsing / validation
389
+ # Identical contract to router_common; the tag format and structural checks carry over unchanged.
390
+
391
+ BLOCK_RE = re.compile(re.escape(C) + r"(?P<cats>.*?)(?=" + re.escape(Q) + r")", re.S)
392
+ PAIR_RE = re.compile(re.escape(Q) + r"(?P<text>.*?)" + re.escape(A) + r"(?P<label>.*?)"
393
+ r"(?=" + re.escape(Q) + r"|" + re.escape(E) + r"|\Z)", re.S)
394
+
395
+
396
+ def norm(s: str) -> str:
397
+ s = unicodedata.normalize("NFKC", s or "")
398
+ s = re.sub(r"[ً-ْٰـ]", "", s)
399
+ return re.sub(r"\s+", " ", s).strip()
400
+
401
+
402
+ def parse_items(raw: str):
403
+ m = BLOCK_RE.search(raw or "")
404
+ if not m:
405
+ return []
406
+ routes = []
407
+ for line in m.group("cats").split("\n"):
408
+ line = line.strip()
409
+ c = re.match(r"^[-*•]\s*(.+)$", line) or re.match(r"^\d+[.)]\s*(.+)$", line)
410
+ if c:
411
+ r = norm(c.group(1)).strip(" .:،")
412
+ if 2 <= len(r) <= 120:
413
+ routes.append(r)
414
+ if not routes:
415
+ return []
416
+ out = []
417
+ for p in PAIR_RE.finditer(raw):
418
+ out.append({"routes": routes, "text": norm(p.group("text")),
419
+ "label_text": norm(p.group("label")).strip(" .:،").lstrip("- ")})
420
+ return out
421
+
422
+
423
+ def _match_route(label, routes):
424
+ if label in routes:
425
+ return routes.index(label)
426
+ hits = [i for i, r in enumerate(routes) if r and (r in label or label in r)]
427
+ return hits[0] if len(hits) == 1 else None
428
+
429
+
430
+ def validate(item, min_text=15, max_text=600):
431
+ """Structural only. Semantic correctness is decided by verify_router.py, not here."""
432
+ routes, text, label = item["routes"], item["text"], item["label_text"]
433
+ if not (2 <= len(routes) <= 9):
434
+ return False, "route_count"
435
+ if len(set(routes)) != len(routes):
436
+ return False, "duplicate_routes"
437
+ if not (min_text <= len(text) <= max_text):
438
+ return False, "text_length"
439
+ if any(t in text for t in (C, Q, A, E)):
440
+ return False, "tag_leak"
441
+ idx = _match_route(label, routes)
442
+ if idx is None:
443
+ return False, "label_not_in_routes"
444
+ if routes[idx] and routes[idx] in text:
445
+ return False, "lexical_giveaway"
446
+ if len(text.split()) < 4:
447
+ return False, "text_too_few_words"
448
+ return True, "ok"
449
+
450
+
451
+ def label_index(item):
452
+ return _match_route(item["label_text"], item["routes"])
453
+
454
+
455
+ def dedup_key(text: str) -> str:
456
+ return re.sub(r"\d+", "#", re.sub(r"\W+", "", norm(text)))
457
+
458
+
459
+ def lane_set_key(routes) -> str:
460
+ return "|".join(sorted(norm(r) for r in routes))
router_model.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:90ff8180f65ed7ae63a3445b5418bf3f35ea051816e6f7ff367450da44a7c0fc
3
+ size 208245555
routing_model.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Inference for Nawah-Router-v2 — an Arabic zero-shot router with a routing head.
3
+
4
+ The text and every category share one sequence. Each category's character span is mapped to token
5
+ indices through the tokenizer's offset mapping and mean-pooled into its own vector; a shared
6
+ scorer turns each into one logit, and the softmax runs over the categories actually supplied.
7
+ Because the scorer is shared across positions it reads category *content*, not slot index — which
8
+ is what makes the label set free text chosen at inference.
9
+
10
+ Layout is text-first, categories-second on purpose. The backbone is causal, so this ordering is
11
+ what lets every category token attend to the whole text; reversed, the categories would be
12
+ encoded blind to it.
13
+ """
14
+ import torch
15
+ import torch.nn as nn
16
+ from transformers import AutoModel, AutoTokenizer
17
+
18
+ MAX_ROUTES = 9
19
+ MAX_LENGTH = 320
20
+
21
+
22
+ class RouterModel(nn.Module):
23
+ def __init__(self, base_model):
24
+ super().__init__()
25
+ self.backbone = AutoModel.from_pretrained(base_model, dtype=torch.float32)
26
+ h = self.backbone.config.hidden_size
27
+ self.score = nn.Sequential(nn.Linear(h, h), nn.GELU(), nn.Linear(h, 1))
28
+ self.config = self.backbone.config
29
+
30
+ def forward(self, input_ids, attention_mask, cat_pool, n_routes):
31
+ hs = self.backbone(input_ids=input_ids, attention_mask=attention_mask).last_hidden_state
32
+ logits = self.score(torch.bmm(cat_pool.to(hs.dtype), hs)).squeeze(-1)
33
+ ar = torch.arange(logits.size(1), device=logits.device)[None, :]
34
+ return logits.masked_fill(ar >= n_routes[:, None], torch.finfo(logits.dtype).min)
35
+
36
+ @classmethod
37
+ def from_pretrained(cls, path, token=None):
38
+ import os
39
+ from huggingface_hub import hf_hub_download
40
+ m = cls(path)
41
+ w = (os.path.join(path, "router_model.pt") if os.path.isdir(path)
42
+ else hf_hub_download(path, "router_model.pt", token=token))
43
+ m.load_state_dict(torch.load(w, map_location="cpu", weights_only=True))
44
+ return m.eval()
45
+
46
+
47
+ def build_text(text, routes):
48
+ head = f"النص:\n{text}\n\nالفئات:\n"
49
+ s, spans = head, []
50
+ for c in routes:
51
+ s += "- "
52
+ spans.append((len(s), len(s) + len(c)))
53
+ s += c + "\n"
54
+ return s, spans
55
+
56
+
57
+ @torch.no_grad()
58
+ def route(model, tok, text, routes):
59
+ """-> [{'route': str, 'score': float}] sorted high to low."""
60
+ routes = [r for r in routes if r and r.strip()][:MAX_ROUTES]
61
+ if not text.strip() or not routes:
62
+ return []
63
+ full, spans = build_text(text, routes)
64
+ enc = tok(full, return_offsets_mapping=True, add_special_tokens=False,
65
+ truncation=True, max_length=MAX_LENGTH)
66
+ ids, offs = enc["input_ids"], enc["offset_mapping"]
67
+ pool = torch.zeros(1, MAX_ROUTES, len(ids))
68
+ for ci, (s, e) in enumerate(spans):
69
+ idx = [t for t, (a, b) in enumerate(offs) if a < e and b > s and a != b]
70
+ if idx:
71
+ pool[0, ci, idx] = 1.0 / len(idx)
72
+ logits = model(torch.tensor([ids]), torch.ones(1, len(ids), dtype=torch.long),
73
+ pool, torch.tensor([len(routes)]))
74
+ probs = logits.softmax(-1)[0][: len(routes)].tolist()
75
+ out = [{"route": r, "score": p} for r, p in zip(routes, probs)]
76
+ return sorted(out, key=lambda x: -x["score"])
77
+
78
+
79
+ if __name__ == "__main__":
80
+ M = "oddadmix/Nawah-Router-v2"
81
+ tok = AutoTokenizer.from_pretrained(M)
82
+ model = RouterModel.from_pretrained(M)
83
+ for r in route(model, tok, "الطلب تأخر ساعة والسائق ما رد على الاتصال",
84
+ ["استفسار عن التوصيل", "شكوى تأخير", "مشكلة في الدفع"]):
85
+ print(f"{r['score']:.3f} {r['route']}")
tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
tokenizer_config.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "backend": "tokenizers",
3
+ "bos_token": "<s>",
4
+ "eos_token": "</s>",
5
+ "is_local": false,
6
+ "local_files_only": false,
7
+ "model_max_length": 1000000000000000019884624838656,
8
+ "pad_token": "<pad>",
9
+ "tokenizer_class": "TokenizersBackend",
10
+ "unk_token": "<unk>"
11
+ }
train_metrics.json ADDED
@@ -0,0 +1,1157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "results": {
3
+ "unseen_lanes": {
4
+ "accuracy": 0.9304995345950977,
5
+ "random_baseline": 0.2137126998901743,
6
+ "n": 3223,
7
+ "by_n_routes": {
8
+ "2": {
9
+ "acc": 0.954954954954955,
10
+ "n": 111
11
+ },
12
+ "4": {
13
+ "acc": 0.9449877750611247,
14
+ "n": 818
15
+ },
16
+ "5": {
17
+ "acc": 0.9423076923076923,
18
+ "n": 624
19
+ },
20
+ "8": {
21
+ "acc": 0.9341317365269461,
22
+ "n": 334
23
+ },
24
+ "3": {
25
+ "acc": 0.9104938271604939,
26
+ "n": 324
27
+ },
28
+ "7": {
29
+ "acc": 0.907928388746803,
30
+ "n": 391
31
+ },
32
+ "6": {
33
+ "acc": 0.9224952741020794,
34
+ "n": 529
35
+ },
36
+ "9": {
37
+ "acc": 0.8913043478260869,
38
+ "n": 92
39
+ }
40
+ },
41
+ "by_difficulty": {
42
+ "easy": {
43
+ "acc": 0.9478603049680275,
44
+ "n": 2033
45
+ },
46
+ "hard": {
47
+ "acc": 0.9008403361344538,
48
+ "n": 1190
49
+ }
50
+ },
51
+ "by_mode": {
52
+ "topical": {
53
+ "acc": 0.884,
54
+ "n": 1500
55
+ },
56
+ "mixed": {
57
+ "acc": 0.9772549019607844,
58
+ "n": 1275
59
+ },
60
+ "axis": {
61
+ "acc": 0.953125,
62
+ "n": 448
63
+ }
64
+ }
65
+ },
66
+ "unseen_domain": {
67
+ "accuracy": 0.6976068889556424,
68
+ "random_baseline": 0.25206113185236045,
69
+ "n": 9987,
70
+ "by_n_routes": {
71
+ "5": {
72
+ "acc": 0.6869671132764921,
73
+ "n": 1642
74
+ },
75
+ "4": {
76
+ "acc": 0.727150084317032,
77
+ "n": 2965
78
+ },
79
+ "3": {
80
+ "acc": 0.7427476737821566,
81
+ "n": 1827
82
+ },
83
+ "2": {
84
+ "acc": 0.7923497267759563,
85
+ "n": 915
86
+ },
87
+ "6": {
88
+ "acc": 0.6202393906420022,
89
+ "n": 919
90
+ },
91
+ "7": {
92
+ "acc": 0.5959595959595959,
93
+ "n": 792
94
+ },
95
+ "8": {
96
+ "acc": 0.5976608187134503,
97
+ "n": 855
98
+ },
99
+ "9": {
100
+ "acc": 0.6666666666666666,
101
+ "n": 72
102
+ }
103
+ },
104
+ "by_difficulty": {
105
+ "easy": {
106
+ "acc": 0.6926144520657203,
107
+ "n": 6269
108
+ },
109
+ "hard": {
110
+ "acc": 0.7060247444862829,
111
+ "n": 3718
112
+ }
113
+ },
114
+ "by_mode": {
115
+ "topical": {
116
+ "acc": 0.6976068889556424,
117
+ "n": 9987
118
+ }
119
+ }
120
+ },
121
+ "unseen_axis": {
122
+ "accuracy": 0.6129888942539836,
123
+ "random_baseline": 0.23578575644002975,
124
+ "n": 12426,
125
+ "by_n_routes": {
126
+ "4": {
127
+ "acc": 0.541494632535095,
128
+ "n": 4844
129
+ },
130
+ "5": {
131
+ "acc": 0.6220699603317706,
132
+ "n": 2773
133
+ },
134
+ "3": {
135
+ "acc": 0.6389937106918239,
136
+ "n": 2385
137
+ },
138
+ "6": {
139
+ "acc": 0.7419898819561551,
140
+ "n": 1186
141
+ },
142
+ "7": {
143
+ "acc": 0.7033898305084746,
144
+ "n": 944
145
+ },
146
+ "8": {
147
+ "acc": 0.6836734693877551,
148
+ "n": 294
149
+ }
150
+ },
151
+ "by_difficulty": {
152
+ "hard": {
153
+ "acc": 0.6019339242546333,
154
+ "n": 4964
155
+ },
156
+ "easy": {
157
+ "acc": 0.6203430715625837,
158
+ "n": 7462
159
+ }
160
+ },
161
+ "by_mode": {
162
+ "axis": {
163
+ "acc": 0.6129888942539836,
164
+ "n": 12426
165
+ }
166
+ }
167
+ },
168
+ "hard": {
169
+ "accuracy": 0.9008403361344538,
170
+ "random_baseline": 0.21085734293717487,
171
+ "n": 1190,
172
+ "by_n_routes": {
173
+ "4": {
174
+ "acc": 0.9182156133828996,
175
+ "n": 269
176
+ },
177
+ "5": {
178
+ "acc": 0.9043824701195219,
179
+ "n": 251
180
+ },
181
+ "8": {
182
+ "acc": 0.9291338582677166,
183
+ "n": 127
184
+ },
185
+ "3": {
186
+ "acc": 0.8571428571428571,
187
+ "n": 126
188
+ },
189
+ "7": {
190
+ "acc": 0.8662420382165605,
191
+ "n": 157
192
+ },
193
+ "6": {
194
+ "acc": 0.9045226130653267,
195
+ "n": 199
196
+ },
197
+ "2": {
198
+ "acc": 1.0,
199
+ "n": 34
200
+ },
201
+ "9": {
202
+ "acc": 0.8148148148148148,
203
+ "n": 27
204
+ }
205
+ },
206
+ "by_difficulty": {
207
+ "hard": {
208
+ "acc": 0.9008403361344538,
209
+ "n": 1190
210
+ }
211
+ },
212
+ "by_mode": {
213
+ "topical": {
214
+ "acc": 0.8300180831826401,
215
+ "n": 553
216
+ },
217
+ "mixed": {
218
+ "acc": 0.9680851063829787,
219
+ "n": 470
220
+ },
221
+ "axis": {
222
+ "acc": 0.9461077844311377,
223
+ "n": 167
224
+ }
225
+ }
226
+ }
227
+ },
228
+ "base_model": "oddadmix/50M-2048-Emhotob",
229
+ "formulation": "routing_head",
230
+ "log_history": [
231
+ {
232
+ "loss": 0.8975238800048828,
233
+ "grad_norm": 11.03390121459961,
234
+ "learning_rate": 7.4625e-05,
235
+ "epoch": 0.030902348578491966,
236
+ "step": 200
237
+ },
238
+ {
239
+ "loss": 0.47223701477050783,
240
+ "grad_norm": 11.903738975524902,
241
+ "learning_rate": 0.000149625,
242
+ "epoch": 0.06180469715698393,
243
+ "step": 400
244
+ },
245
+ {
246
+ "loss": 0.4622153091430664,
247
+ "grad_norm": 6.198873996734619,
248
+ "learning_rate": 0.000224625,
249
+ "epoch": 0.09270704573547589,
250
+ "step": 600
251
+ },
252
+ {
253
+ "loss": 0.49717689514160157,
254
+ "grad_norm": 4.740701198577881,
255
+ "learning_rate": 0.000299625,
256
+ "epoch": 0.12360939431396786,
257
+ "step": 800
258
+ },
259
+ {
260
+ "loss": 0.4837868118286133,
261
+ "grad_norm": 3.6331026554107666,
262
+ "learning_rate": 0.0002998012771863284,
263
+ "epoch": 0.15451174289245984,
264
+ "step": 1000
265
+ },
266
+ {
267
+ "eval_unseen_lanes_loss": 0.5073266625404358,
268
+ "eval_unseen_lanes_accuracy": 0.8163201985727583,
269
+ "eval_unseen_lanes_runtime": 0.5548,
270
+ "eval_unseen_lanes_samples_per_second": 5809.493,
271
+ "eval_unseen_lanes_steps_per_second": 91.928,
272
+ "epoch": 0.15451174289245984,
273
+ "step": 1000
274
+ },
275
+ {
276
+ "eval_unseen_domain_loss": 0.9687234163284302,
277
+ "eval_unseen_domain_accuracy": 0.6065885651346751,
278
+ "eval_unseen_domain_runtime": 1.5808,
279
+ "eval_unseen_domain_samples_per_second": 6317.746,
280
+ "eval_unseen_domain_steps_per_second": 99.318,
281
+ "epoch": 0.15451174289245984,
282
+ "step": 1000
283
+ },
284
+ {
285
+ "eval_unseen_axis_loss": 1.1325784921646118,
286
+ "eval_unseen_axis_accuracy": 0.5135200386286818,
287
+ "eval_unseen_axis_runtime": 1.2551,
288
+ "eval_unseen_axis_samples_per_second": 9900.252,
289
+ "eval_unseen_axis_steps_per_second": 155.364,
290
+ "epoch": 0.15451174289245984,
291
+ "step": 1000
292
+ },
293
+ {
294
+ "eval_hard_loss": 0.5369061231613159,
295
+ "eval_hard_accuracy": 0.807563025210084,
296
+ "eval_hard_runtime": 0.2574,
297
+ "eval_hard_samples_per_second": 4623.428,
298
+ "eval_hard_steps_per_second": 73.819,
299
+ "epoch": 0.15451174289245984,
300
+ "step": 1000
301
+ },
302
+ {
303
+ "loss": 0.4474765396118164,
304
+ "grad_norm": 9.281235694885254,
305
+ "learning_rate": 0.00029920164204065217,
306
+ "epoch": 0.18541409147095178,
307
+ "step": 1200
308
+ },
309
+ {
310
+ "loss": 0.4180959320068359,
311
+ "grad_norm": 6.990286827087402,
312
+ "learning_rate": 0.00029820269436406154,
313
+ "epoch": 0.21631644004944375,
314
+ "step": 1400
315
+ },
316
+ {
317
+ "loss": 0.4012123489379883,
318
+ "grad_norm": 2.00594425201416,
319
+ "learning_rate": 0.00029680710766818503,
320
+ "epoch": 0.24721878862793573,
321
+ "step": 1600
322
+ },
323
+ {
324
+ "loss": 0.39807247161865233,
325
+ "grad_norm": 4.734849452972412,
326
+ "learning_rate": 0.0002950186170007609,
327
+ "epoch": 0.27812113720642767,
328
+ "step": 1800
329
+ },
330
+ {
331
+ "loss": 0.37501571655273436,
332
+ "grad_norm": 2.752845048904419,
333
+ "learning_rate": 0.0002928420089494242,
334
+ "epoch": 0.30902348578491967,
335
+ "step": 2000
336
+ },
337
+ {
338
+ "eval_unseen_lanes_loss": 0.406538724899292,
339
+ "eval_unseen_lanes_accuracy": 0.8569655600372323,
340
+ "eval_unseen_lanes_runtime": 0.5556,
341
+ "eval_unseen_lanes_samples_per_second": 5801.001,
342
+ "eval_unseen_lanes_steps_per_second": 91.794,
343
+ "epoch": 0.30902348578491967,
344
+ "step": 2000
345
+ },
346
+ {
347
+ "eval_unseen_domain_loss": 1.1472351551055908,
348
+ "eval_unseen_domain_accuracy": 0.5958746370281366,
349
+ "eval_unseen_domain_runtime": 1.575,
350
+ "eval_unseen_domain_samples_per_second": 6340.999,
351
+ "eval_unseen_domain_steps_per_second": 99.683,
352
+ "epoch": 0.30902348578491967,
353
+ "step": 2000
354
+ },
355
+ {
356
+ "eval_unseen_axis_loss": 1.353710412979126,
357
+ "eval_unseen_axis_accuracy": 0.49806856591018833,
358
+ "eval_unseen_axis_runtime": 1.3077,
359
+ "eval_unseen_axis_samples_per_second": 9502.1,
360
+ "eval_unseen_axis_steps_per_second": 149.116,
361
+ "epoch": 0.30902348578491967,
362
+ "step": 2000
363
+ },
364
+ {
365
+ "eval_hard_loss": 0.4623798727989197,
366
+ "eval_hard_accuracy": 0.8378151260504202,
367
+ "eval_hard_runtime": 0.2506,
368
+ "eval_hard_samples_per_second": 4749.401,
369
+ "eval_hard_steps_per_second": 75.831,
370
+ "epoch": 0.30902348578491967,
371
+ "step": 2000
372
+ },
373
+ {
374
+ "loss": 0.3629868316650391,
375
+ "grad_norm": 3.957963228225708,
376
+ "learning_rate": 0.0002902831088312284,
377
+ "epoch": 0.3399258343634116,
378
+ "step": 2200
379
+ },
380
+ {
381
+ "loss": 0.3620159912109375,
382
+ "grad_norm": 2.070098638534546,
383
+ "learning_rate": 0.00028734876510218624,
384
+ "epoch": 0.37082818294190356,
385
+ "step": 2400
386
+ },
387
+ {
388
+ "loss": 0.33227527618408204,
389
+ "grad_norm": 3.895007371902466,
390
+ "learning_rate": 0.00028404683102855547,
391
+ "epoch": 0.40173053152039556,
392
+ "step": 2600
393
+ },
394
+ {
395
+ "loss": 0.3365111541748047,
396
+ "grad_norm": 3.198639154434204,
397
+ "learning_rate": 0.0002803861436689221,
398
+ "epoch": 0.4326328800988875,
399
+ "step": 2800
400
+ },
401
+ {
402
+ "loss": 0.31873382568359376,
403
+ "grad_norm": 2.5327401161193848,
404
+ "learning_rate": 0.0002763765002233335,
405
+ "epoch": 0.4635352286773795,
406
+ "step": 3000
407
+ },
408
+ {
409
+ "eval_unseen_lanes_loss": 0.34330183267593384,
410
+ "eval_unseen_lanes_accuracy": 0.8836487744337573,
411
+ "eval_unseen_lanes_runtime": 0.553,
412
+ "eval_unseen_lanes_samples_per_second": 5828.318,
413
+ "eval_unseen_lanes_steps_per_second": 92.226,
414
+ "epoch": 0.4635352286773795,
415
+ "step": 3000
416
+ },
417
+ {
418
+ "eval_unseen_domain_loss": 0.9895414113998413,
419
+ "eval_unseen_domain_accuracy": 0.6378291779313107,
420
+ "eval_unseen_domain_runtime": 1.5853,
421
+ "eval_unseen_domain_samples_per_second": 6299.774,
422
+ "eval_unseen_domain_steps_per_second": 99.035,
423
+ "epoch": 0.4635352286773795,
424
+ "step": 3000
425
+ },
426
+ {
427
+ "eval_unseen_axis_loss": 1.0701825618743896,
428
+ "eval_unseen_axis_accuracy": 0.5737164010944793,
429
+ "eval_unseen_axis_runtime": 1.2712,
430
+ "eval_unseen_axis_samples_per_second": 9774.846,
431
+ "eval_unseen_axis_steps_per_second": 153.396,
432
+ "epoch": 0.4635352286773795,
433
+ "step": 3000
434
+ },
435
+ {
436
+ "eval_hard_loss": 0.38013485074043274,
437
+ "eval_hard_accuracy": 0.873109243697479,
438
+ "eval_hard_runtime": 0.2542,
439
+ "eval_hard_samples_per_second": 4681.85,
440
+ "eval_hard_steps_per_second": 74.752,
441
+ "epoch": 0.4635352286773795,
442
+ "step": 3000
443
+ },
444
+ {
445
+ "loss": 0.29870388031005857,
446
+ "grad_norm": 2.18878173828125,
447
+ "learning_rate": 0.00027202863181277745,
448
+ "epoch": 0.49443757725587145,
449
+ "step": 3200
450
+ },
451
+ {
452
+ "loss": 0.30184398651123046,
453
+ "grad_norm": 3.0138494968414307,
454
+ "learning_rate": 0.00026735417475918285,
455
+ "epoch": 0.5253399258343634,
456
+ "step": 3400
457
+ },
458
+ {
459
+ "loss": 0.32383251190185547,
460
+ "grad_norm": 7.008852005004883,
461
+ "learning_rate": 0.00026236563944280554,
462
+ "epoch": 0.5562422744128553,
463
+ "step": 3600
464
+ },
465
+ {
466
+ "loss": 0.3035639762878418,
467
+ "grad_norm": 2.690004825592041,
468
+ "learning_rate": 0.0002570763768203474,
469
+ "epoch": 0.5871446229913473,
470
+ "step": 3800
471
+ },
472
+ {
473
+ "loss": 0.2934083366394043,
474
+ "grad_norm": 1.9328616857528687,
475
+ "learning_rate": 0.0002515005426934173,
476
+ "epoch": 0.6180469715698393,
477
+ "step": 4000
478
+ },
479
+ {
480
+ "eval_unseen_lanes_loss": 0.30602237582206726,
481
+ "eval_unseen_lanes_accuracy": 0.8920260626745269,
482
+ "eval_unseen_lanes_runtime": 0.5511,
483
+ "eval_unseen_lanes_samples_per_second": 5848.268,
484
+ "eval_unseen_lanes_steps_per_second": 92.542,
485
+ "epoch": 0.6180469715698393,
486
+ "step": 4000
487
+ },
488
+ {
489
+ "eval_unseen_domain_loss": 0.983905553817749,
490
+ "eval_unseen_domain_accuracy": 0.6368278762391109,
491
+ "eval_unseen_domain_runtime": 1.5835,
492
+ "eval_unseen_domain_samples_per_second": 6306.791,
493
+ "eval_unseen_domain_steps_per_second": 99.146,
494
+ "epoch": 0.6180469715698393,
495
+ "step": 4000
496
+ },
497
+ {
498
+ "eval_unseen_axis_loss": 1.2108259201049805,
499
+ "eval_unseen_axis_accuracy": 0.5799130854659584,
500
+ "eval_unseen_axis_runtime": 1.2562,
501
+ "eval_unseen_axis_samples_per_second": 9891.534,
502
+ "eval_unseen_axis_steps_per_second": 155.227,
503
+ "epoch": 0.6180469715698393,
504
+ "step": 4000
505
+ },
506
+ {
507
+ "eval_hard_loss": 0.3655548691749573,
508
+ "eval_hard_accuracy": 0.8756302521008403,
509
+ "eval_hard_runtime": 0.2592,
510
+ "eval_hard_samples_per_second": 4591.918,
511
+ "eval_hard_steps_per_second": 73.316,
512
+ "epoch": 0.6180469715698393,
513
+ "step": 4000
514
+ },
515
+ {
516
+ "loss": 0.2829387664794922,
517
+ "grad_norm": 2.094444513320923,
518
+ "learning_rate": 0.00024565305982296296,
519
+ "epoch": 0.6489493201483313,
520
+ "step": 4200
521
+ },
522
+ {
523
+ "loss": 0.2714531135559082,
524
+ "grad_norm": 2.0051519870758057,
525
+ "learning_rate": 0.00023954957799106823,
526
+ "epoch": 0.6798516687268232,
527
+ "step": 4400
528
+ },
529
+ {
530
+ "loss": 0.26623159408569336,
531
+ "grad_norm": 2.2412750720977783,
532
+ "learning_rate": 0.00023320643211700308,
533
+ "epoch": 0.7107540173053152,
534
+ "step": 4600
535
+ },
536
+ {
537
+ "loss": 0.2522362518310547,
538
+ "grad_norm": 1.7817517518997192,
539
+ "learning_rate": 0.000226640598539622,
540
+ "epoch": 0.7416563658838071,
541
+ "step": 4800
542
+ },
543
+ {
544
+ "loss": 0.2480511474609375,
545
+ "grad_norm": 5.033050537109375,
546
+ "learning_rate": 0.00021986964958311316,
547
+ "epoch": 0.7725587144622992,
548
+ "step": 5000
549
+ },
550
+ {
551
+ "eval_unseen_lanes_loss": 0.27606716752052307,
552
+ "eval_unseen_lanes_accuracy": 0.9053676698727893,
553
+ "eval_unseen_lanes_runtime": 0.552,
554
+ "eval_unseen_lanes_samples_per_second": 5838.329,
555
+ "eval_unseen_lanes_steps_per_second": 92.384,
556
+ "epoch": 0.7725587144622992,
557
+ "step": 5000
558
+ },
559
+ {
560
+ "eval_unseen_domain_loss": 0.96076899766922,
561
+ "eval_unseen_domain_accuracy": 0.6541503955141684,
562
+ "eval_unseen_domain_runtime": 1.5806,
563
+ "eval_unseen_domain_samples_per_second": 6318.291,
564
+ "eval_unseen_domain_steps_per_second": 99.326,
565
+ "epoch": 0.7725587144622992,
566
+ "step": 5000
567
+ },
568
+ {
569
+ "eval_unseen_axis_loss": 1.4210246801376343,
570
+ "eval_unseen_axis_accuracy": 0.5544825366167713,
571
+ "eval_unseen_axis_runtime": 1.2599,
572
+ "eval_unseen_axis_samples_per_second": 9862.704,
573
+ "eval_unseen_axis_steps_per_second": 154.774,
574
+ "epoch": 0.7725587144622992,
575
+ "step": 5000
576
+ },
577
+ {
578
+ "eval_hard_loss": 0.3613678812980652,
579
+ "eval_hard_accuracy": 0.8857142857142857,
580
+ "eval_hard_runtime": 0.2606,
581
+ "eval_hard_samples_per_second": 4565.623,
582
+ "eval_hard_steps_per_second": 72.897,
583
+ "epoch": 0.7725587144622992,
584
+ "step": 5000
585
+ },
586
+ {
587
+ "loss": 0.24242013931274414,
588
+ "grad_norm": 2.4511566162109375,
589
+ "learning_rate": 0.00021291170652769533,
590
+ "epoch": 0.8034610630407911,
591
+ "step": 5200
592
+ },
593
+ {
594
+ "loss": 0.2558748817443848,
595
+ "grad_norm": 1.6439733505249023,
596
+ "learning_rate": 0.00020578539111112834,
597
+ "epoch": 0.8343634116192831,
598
+ "step": 5400
599
+ },
600
+ {
601
+ "loss": 0.24663785934448243,
602
+ "grad_norm": 3.4409291744232178,
603
+ "learning_rate": 0.00019850977569083514,
604
+ "epoch": 0.865265760197775,
605
+ "step": 5600
606
+ },
607
+ {
608
+ "loss": 0.24483919143676758,
609
+ "grad_norm": 3.071554183959961,
610
+ "learning_rate": 0.00019110433220001754,
611
+ "epoch": 0.896168108776267,
612
+ "step": 5800
613
+ },
614
+ {
615
+ "loss": 0.23662748336791992,
616
+ "grad_norm": 2.491429567337036,
617
+ "learning_rate": 0.0001835888800343768,
618
+ "epoch": 0.927070457354759,
619
+ "step": 6000
620
+ },
621
+ {
622
+ "eval_unseen_lanes_loss": 0.2571614384651184,
623
+ "eval_unseen_lanes_accuracy": 0.9081600992863792,
624
+ "eval_unseen_lanes_runtime": 0.5519,
625
+ "eval_unseen_lanes_samples_per_second": 5839.58,
626
+ "eval_unseen_lanes_steps_per_second": 92.404,
627
+ "epoch": 0.927070457354759,
628
+ "step": 6000
629
+ },
630
+ {
631
+ "eval_unseen_domain_loss": 0.9326336979866028,
632
+ "eval_unseen_domain_accuracy": 0.6525483128066486,
633
+ "eval_unseen_domain_runtime": 1.5742,
634
+ "eval_unseen_domain_samples_per_second": 6344.232,
635
+ "eval_unseen_domain_steps_per_second": 99.734,
636
+ "epoch": 0.927070457354759,
637
+ "step": 6000
638
+ },
639
+ {
640
+ "eval_unseen_axis_loss": 1.3221510648727417,
641
+ "eval_unseen_axis_accuracy": 0.5856269113149847,
642
+ "eval_unseen_axis_runtime": 1.2492,
643
+ "eval_unseen_axis_samples_per_second": 9946.782,
644
+ "eval_unseen_axis_steps_per_second": 156.094,
645
+ "epoch": 0.927070457354759,
646
+ "step": 6000
647
+ },
648
+ {
649
+ "eval_hard_loss": 0.33343571424484253,
650
+ "eval_hard_accuracy": 0.888235294117647,
651
+ "eval_hard_runtime": 0.2587,
652
+ "eval_hard_samples_per_second": 4600.467,
653
+ "eval_hard_steps_per_second": 73.453,
654
+ "epoch": 0.927070457354759,
655
+ "step": 6000
656
+ },
657
+ {
658
+ "loss": 0.23579851150512696,
659
+ "grad_norm": 2.5624895095825195,
660
+ "learning_rate": 0.00017598353300890969,
661
+ "epoch": 0.957972805933251,
662
+ "step": 6200
663
+ },
664
+ {
665
+ "loss": 0.21076148986816406,
666
+ "grad_norm": 0.9841051697731018,
667
+ "learning_rate": 0.00016830864552674267,
668
+ "epoch": 0.9888751545117429,
669
+ "step": 6400
670
+ },
671
+ {
672
+ "loss": 0.17771633148193358,
673
+ "grad_norm": 0.9096925258636475,
674
+ "learning_rate": 0.00016058475810407297,
675
+ "epoch": 1.019777503090235,
676
+ "step": 6600
677
+ },
678
+ {
679
+ "loss": 0.14182785034179687,
680
+ "grad_norm": 3.3751485347747803,
681
+ "learning_rate": 0.00015283254239701005,
682
+ "epoch": 1.0506798516687268,
683
+ "step": 6800
684
+ },
685
+ {
686
+ "loss": 0.1347336483001709,
687
+ "grad_norm": 0.5445594787597656,
688
+ "learning_rate": 0.00014507274587744398,
689
+ "epoch": 1.0815822002472189,
690
+ "step": 7000
691
+ },
692
+ {
693
+ "eval_unseen_lanes_loss": 0.2655007839202881,
694
+ "eval_unseen_lanes_accuracy": 0.9162271175923054,
695
+ "eval_unseen_lanes_runtime": 0.5535,
696
+ "eval_unseen_lanes_samples_per_second": 5823.011,
697
+ "eval_unseen_lanes_steps_per_second": 92.142,
698
+ "epoch": 1.0815822002472189,
699
+ "step": 7000
700
+ },
701
+ {
702
+ "eval_unseen_domain_loss": 1.0792171955108643,
703
+ "eval_unseen_domain_accuracy": 0.6691699208971663,
704
+ "eval_unseen_domain_runtime": 1.5827,
705
+ "eval_unseen_domain_samples_per_second": 6310.28,
706
+ "eval_unseen_domain_steps_per_second": 99.2,
707
+ "epoch": 1.0815822002472189,
708
+ "step": 7000
709
+ },
710
+ {
711
+ "eval_unseen_axis_loss": 1.5451632738113403,
712
+ "eval_unseen_axis_accuracy": 0.5832930951231289,
713
+ "eval_unseen_axis_runtime": 1.2555,
714
+ "eval_unseen_axis_samples_per_second": 9897.184,
715
+ "eval_unseen_axis_steps_per_second": 155.316,
716
+ "epoch": 1.0815822002472189,
717
+ "step": 7000
718
+ },
719
+ {
720
+ "eval_hard_loss": 0.3464004695415497,
721
+ "eval_hard_accuracy": 0.8966386554621849,
722
+ "eval_hard_runtime": 0.2591,
723
+ "eval_hard_samples_per_second": 4593.088,
724
+ "eval_hard_steps_per_second": 73.335,
725
+ "epoch": 1.0815822002472189,
726
+ "step": 7000
727
+ },
728
+ {
729
+ "loss": 0.13339081764221192,
730
+ "grad_norm": 2.522960901260376,
731
+ "learning_rate": 0.00013732613630600555,
732
+ "epoch": 1.1124845488257107,
733
+ "step": 7200
734
+ },
735
+ {
736
+ "loss": 0.11780735015869141,
737
+ "grad_norm": 1.1648684740066528,
738
+ "learning_rate": 0.0001296134461507269,
739
+ "epoch": 1.1433868974042027,
740
+ "step": 7400
741
+ },
742
+ {
743
+ "loss": 0.13406469345092772,
744
+ "grad_norm": 1.038225531578064,
745
+ "learning_rate": 0.00012195531710015758,
746
+ "epoch": 1.1742892459826946,
747
+ "step": 7600
748
+ },
749
+ {
750
+ "loss": 0.1309179973602295,
751
+ "grad_norm": 1.6545023918151855,
752
+ "learning_rate": 0.00011437224481943516,
753
+ "epoch": 1.2051915945611866,
754
+ "step": 7800
755
+ },
756
+ {
757
+ "loss": 0.13036264419555665,
758
+ "grad_norm": 1.7858803272247314,
759
+ "learning_rate": 0.00010688452409716325,
760
+ "epoch": 1.2360939431396787,
761
+ "step": 8000
762
+ },
763
+ {
764
+ "eval_unseen_lanes_loss": 0.26563557982444763,
765
+ "eval_unseen_lanes_accuracy": 0.9162271175923054,
766
+ "eval_unseen_lanes_runtime": 0.5528,
767
+ "eval_unseen_lanes_samples_per_second": 5830.741,
768
+ "eval_unseen_lanes_steps_per_second": 92.264,
769
+ "epoch": 1.2360939431396787,
770
+ "step": 8000
771
+ },
772
+ {
773
+ "eval_unseen_domain_loss": 1.1347131729125977,
774
+ "eval_unseen_domain_accuracy": 0.6633623710824071,
775
+ "eval_unseen_domain_runtime": 1.5784,
776
+ "eval_unseen_domain_samples_per_second": 6327.271,
777
+ "eval_unseen_domain_steps_per_second": 99.467,
778
+ "epoch": 1.2360939431396787,
779
+ "step": 8000
780
+ },
781
+ {
782
+ "eval_unseen_axis_loss": 1.7664040327072144,
783
+ "eval_unseen_axis_accuracy": 0.5963302752293578,
784
+ "eval_unseen_axis_runtime": 1.2484,
785
+ "eval_unseen_axis_samples_per_second": 9953.248,
786
+ "eval_unseen_axis_steps_per_second": 156.195,
787
+ "epoch": 1.2360939431396787,
788
+ "step": 8000
789
+ },
790
+ {
791
+ "eval_hard_loss": 0.34612923860549927,
792
+ "eval_hard_accuracy": 0.8957983193277311,
793
+ "eval_hard_runtime": 0.2561,
794
+ "eval_hard_samples_per_second": 4646.719,
795
+ "eval_hard_steps_per_second": 74.191,
796
+ "epoch": 1.2360939431396787,
797
+ "step": 8000
798
+ },
799
+ {
800
+ "loss": 0.13536205291748046,
801
+ "grad_norm": 1.5142947435379028,
802
+ "learning_rate": 9.95121945299005e-05,
803
+ "epoch": 1.2669962917181705,
804
+ "step": 8200
805
+ },
806
+ {
807
+ "loss": 0.12326237678527832,
808
+ "grad_norm": 5.0126051902771,
809
+ "learning_rate": 9.227498688962761e-05,
810
+ "epoch": 1.2978986402966626,
811
+ "step": 8400
812
+ },
813
+ {
814
+ "loss": 0.12851189613342284,
815
+ "grad_norm": 3.0441036224365234,
816
+ "learning_rate": 8.519227031772971e-05,
817
+ "epoch": 1.3288009888751544,
818
+ "step": 8600
819
+ },
820
+ {
821
+ "loss": 0.11189216613769531,
822
+ "grad_norm": 1.7037580013275146,
823
+ "learning_rate": 7.828300048682143e-05,
824
+ "epoch": 1.3597033374536465,
825
+ "step": 8800
826
+ },
827
+ {
828
+ "loss": 0.12371747016906738,
829
+ "grad_norm": 0.7055680751800537,
830
+ "learning_rate": 7.156566886914933e-05,
831
+ "epoch": 1.3906056860321385,
832
+ "step": 9000
833
+ },
834
+ {
835
+ "eval_unseen_lanes_loss": 0.23261435329914093,
836
+ "eval_unseen_lanes_accuracy": 0.9221222463543283,
837
+ "eval_unseen_lanes_runtime": 0.5543,
838
+ "eval_unseen_lanes_samples_per_second": 5814.13,
839
+ "eval_unseen_lanes_steps_per_second": 92.001,
840
+ "epoch": 1.3906056860321385,
841
+ "step": 9000
842
+ },
843
+ {
844
+ "eval_unseen_domain_loss": 0.9834672808647156,
845
+ "eval_unseen_domain_accuracy": 0.6840893161109443,
846
+ "eval_unseen_domain_runtime": 1.579,
847
+ "eval_unseen_domain_samples_per_second": 6324.919,
848
+ "eval_unseen_domain_steps_per_second": 99.43,
849
+ "epoch": 1.3906056860321385,
850
+ "step": 9000
851
+ },
852
+ {
853
+ "eval_unseen_axis_loss": 1.5792756080627441,
854
+ "eval_unseen_axis_accuracy": 0.593996459037502,
855
+ "eval_unseen_axis_runtime": 1.2867,
856
+ "eval_unseen_axis_samples_per_second": 9657.248,
857
+ "eval_unseen_axis_steps_per_second": 151.55,
858
+ "epoch": 1.3906056860321385,
859
+ "step": 9000
860
+ },
861
+ {
862
+ "eval_hard_loss": 0.31886979937553406,
863
+ "eval_hard_accuracy": 0.9,
864
+ "eval_hard_runtime": 0.2641,
865
+ "eval_hard_samples_per_second": 4505.693,
866
+ "eval_hard_steps_per_second": 71.94,
867
+ "epoch": 1.3906056860321385,
868
+ "step": 9000
869
+ },
870
+ {
871
+ "loss": 0.11327756881713867,
872
+ "grad_norm": 2.5306780338287354,
873
+ "learning_rate": 6.505825324734749e-05,
874
+ "epoch": 1.4215080346106304,
875
+ "step": 9200
876
+ },
877
+ {
878
+ "loss": 0.11213048934936523,
879
+ "grad_norm": 3.7060656547546387,
880
+ "learning_rate": 5.8778169599994366e-05,
881
+ "epoch": 1.4524103831891224,
882
+ "step": 9400
883
+ },
884
+ {
885
+ "loss": 0.11299308776855468,
886
+ "grad_norm": 2.2346725463867188,
887
+ "learning_rate": 5.274222549074192e-05,
888
+ "epoch": 1.4833127317676142,
889
+ "step": 9600
890
+ },
891
+ {
892
+ "loss": 0.10625232696533203,
893
+ "grad_norm": 2.6063334941864014,
894
+ "learning_rate": 4.696657508576255e-05,
895
+ "epoch": 1.5142150803461063,
896
+ "step": 9800
897
+ },
898
+ {
899
+ "loss": 0.10512189865112305,
900
+ "grad_norm": 4.146987438201904,
901
+ "learning_rate": 4.146667591990211e-05,
902
+ "epoch": 1.5451174289245984,
903
+ "step": 10000
904
+ },
905
+ {
906
+ "eval_unseen_lanes_loss": 0.2341764271259308,
907
+ "eval_unseen_lanes_accuracy": 0.9286379149860379,
908
+ "eval_unseen_lanes_runtime": 0.5514,
909
+ "eval_unseen_lanes_samples_per_second": 5845.486,
910
+ "eval_unseen_lanes_steps_per_second": 92.498,
911
+ "epoch": 1.5451174289245984,
912
+ "step": 10000
913
+ },
914
+ {
915
+ "eval_unseen_domain_loss": 1.1408356428146362,
916
+ "eval_unseen_domain_accuracy": 0.6845899669570441,
917
+ "eval_unseen_domain_runtime": 1.5844,
918
+ "eval_unseen_domain_samples_per_second": 6303.222,
919
+ "eval_unseen_domain_steps_per_second": 99.089,
920
+ "epoch": 1.5451174289245984,
921
+ "step": 10000
922
+ },
923
+ {
924
+ "eval_unseen_axis_loss": 1.6913553476333618,
925
+ "eval_unseen_axis_accuracy": 0.6064703042008691,
926
+ "eval_unseen_axis_runtime": 1.2705,
927
+ "eval_unseen_axis_samples_per_second": 9780.492,
928
+ "eval_unseen_axis_steps_per_second": 153.484,
929
+ "epoch": 1.5451174289245984,
930
+ "step": 10000
931
+ },
932
+ {
933
+ "eval_hard_loss": 0.3436157703399658,
934
+ "eval_hard_accuracy": 0.9042016806722689,
935
+ "eval_hard_runtime": 0.2553,
936
+ "eval_hard_samples_per_second": 4661.691,
937
+ "eval_hard_steps_per_second": 74.43,
938
+ "epoch": 1.5451174289245984,
939
+ "step": 10000
940
+ },
941
+ {
942
+ "loss": 0.10526069641113281,
943
+ "grad_norm": 1.0956754684448242,
944
+ "learning_rate": 3.625724752724645e-05,
945
+ "epoch": 1.5760197775030902,
946
+ "step": 10200
947
+ },
948
+ {
949
+ "loss": 0.0974786376953125,
950
+ "grad_norm": 1.1272306442260742,
951
+ "learning_rate": 3.135223204682058e-05,
952
+ "epoch": 1.6069221260815822,
953
+ "step": 10400
954
+ },
955
+ {
956
+ "loss": 0.101556396484375,
957
+ "grad_norm": 3.4264066219329834,
958
+ "learning_rate": 2.6764756908851686e-05,
959
+ "epoch": 1.637824474660074,
960
+ "step": 10600
961
+ },
962
+ {
963
+ "loss": 0.10870396614074707,
964
+ "grad_norm": 1.7836824655532837,
965
+ "learning_rate": 2.2507099701460036e-05,
966
+ "epoch": 1.6687268232385661,
967
+ "step": 10800
968
+ },
969
+ {
970
+ "loss": 0.09275879859924316,
971
+ "grad_norm": 1.3714878559112549,
972
+ "learning_rate": 1.8590655311806385e-05,
973
+ "epoch": 1.6996291718170582,
974
+ "step": 11000
975
+ },
976
+ {
977
+ "eval_unseen_lanes_loss": 0.21319718658924103,
978
+ "eval_unseen_lanes_accuracy": 0.9304995345950977,
979
+ "eval_unseen_lanes_runtime": 0.5553,
980
+ "eval_unseen_lanes_samples_per_second": 5804.033,
981
+ "eval_unseen_lanes_steps_per_second": 91.842,
982
+ "epoch": 1.6996291718170582,
983
+ "step": 11000
984
+ },
985
+ {
986
+ "eval_unseen_domain_loss": 1.022688627243042,
987
+ "eval_unseen_domain_accuracy": 0.6976068889556424,
988
+ "eval_unseen_domain_runtime": 1.5825,
989
+ "eval_unseen_domain_samples_per_second": 6310.792,
990
+ "eval_unseen_domain_steps_per_second": 99.208,
991
+ "epoch": 1.6996291718170582,
992
+ "step": 11000
993
+ },
994
+ {
995
+ "eval_unseen_axis_loss": 1.5096590518951416,
996
+ "eval_unseen_axis_accuracy": 0.6129888942539836,
997
+ "eval_unseen_axis_runtime": 1.2878,
998
+ "eval_unseen_axis_samples_per_second": 9649.329,
999
+ "eval_unseen_axis_steps_per_second": 151.426,
1000
+ "epoch": 1.6996291718170582,
1001
+ "step": 11000
1002
+ },
1003
+ {
1004
+ "eval_hard_loss": 0.31117716431617737,
1005
+ "eval_hard_accuracy": 0.9008403361344538,
1006
+ "eval_hard_runtime": 0.2589,
1007
+ "eval_hard_samples_per_second": 4596.879,
1008
+ "eval_hard_steps_per_second": 73.396,
1009
+ "epoch": 1.6996291718170582,
1010
+ "step": 11000
1011
+ },
1012
+ {
1013
+ "loss": 0.09593048095703124,
1014
+ "grad_norm": 3.177462100982666,
1015
+ "learning_rate": 1.5025905429636426e-05,
1016
+ "epoch": 1.73053152039555,
1017
+ "step": 11200
1018
+ },
1019
+ {
1020
+ "loss": 0.09772712707519532,
1021
+ "grad_norm": 2.102013349533081,
1022
+ "learning_rate": 1.1822390494841139e-05,
1023
+ "epoch": 1.7614338689740419,
1024
+ "step": 11400
1025
+ },
1026
+ {
1027
+ "loss": 0.08881115913391113,
1028
+ "grad_norm": 0.3349379897117615,
1029
+ "learning_rate": 8.98868416411046e-06,
1030
+ "epoch": 1.792336217552534,
1031
+ "step": 11600
1032
+ },
1033
+ {
1034
+ "loss": 0.09125228881835938,
1035
+ "grad_norm": 1.5826376676559448,
1036
+ "learning_rate": 6.532370365015982e-06,
1037
+ "epoch": 1.823238566131026,
1038
+ "step": 11800
1039
+ },
1040
+ {
1041
+ "loss": 0.1032994556427002,
1042
+ "grad_norm": 2.430258274078369,
1043
+ "learning_rate": 4.460022998933166e-06,
1044
+ "epoch": 1.854140914709518,
1045
+ "step": 12000
1046
+ },
1047
+ {
1048
+ "eval_unseen_lanes_loss": 0.21029867231845856,
1049
+ "eval_unseen_lanes_accuracy": 0.9329816940738442,
1050
+ "eval_unseen_lanes_runtime": 0.5539,
1051
+ "eval_unseen_lanes_samples_per_second": 5818.497,
1052
+ "eval_unseen_lanes_steps_per_second": 92.071,
1053
+ "epoch": 1.854140914709518,
1054
+ "step": 12000
1055
+ },
1056
+ {
1057
+ "eval_unseen_domain_loss": 1.0129146575927734,
1058
+ "eval_unseen_domain_accuracy": 0.700010013016922,
1059
+ "eval_unseen_domain_runtime": 1.5764,
1060
+ "eval_unseen_domain_samples_per_second": 6335.382,
1061
+ "eval_unseen_domain_steps_per_second": 99.595,
1062
+ "epoch": 1.854140914709518,
1063
+ "step": 12000
1064
+ },
1065
+ {
1066
+ "eval_unseen_axis_loss": 1.5864043235778809,
1067
+ "eval_unseen_axis_accuracy": 0.6117817479478512,
1068
+ "eval_unseen_axis_runtime": 1.2684,
1069
+ "eval_unseen_axis_samples_per_second": 9796.563,
1070
+ "eval_unseen_axis_steps_per_second": 153.737,
1071
+ "epoch": 1.854140914709518,
1072
+ "step": 12000
1073
+ },
1074
+ {
1075
+ "eval_hard_loss": 0.30691978335380554,
1076
+ "eval_hard_accuracy": 0.9084033613445378,
1077
+ "eval_hard_runtime": 0.2602,
1078
+ "eval_hard_samples_per_second": 4573.644,
1079
+ "eval_hard_steps_per_second": 73.025,
1080
+ "epoch": 1.854140914709518,
1081
+ "step": 12000
1082
+ },
1083
+ {
1084
+ "loss": 0.08737665176391601,
1085
+ "grad_norm": 1.072799801826477,
1086
+ "learning_rate": 2.7771883471252034e-06,
1087
+ "epoch": 1.8850432632880099,
1088
+ "step": 12200
1089
+ },
1090
+ {
1091
+ "loss": 0.08102713584899902,
1092
+ "grad_norm": 1.5238364934921265,
1093
+ "learning_rate": 1.4883702270751197e-06,
1094
+ "epoch": 1.9159456118665017,
1095
+ "step": 12400
1096
+ },
1097
+ {
1098
+ "loss": 0.09933463096618653,
1099
+ "grad_norm": 0.16273926198482513,
1100
+ "learning_rate": 5.970179387931151e-07,
1101
+ "epoch": 1.9468479604449938,
1102
+ "step": 12600
1103
+ },
1104
+ {
1105
+ "loss": 0.10335576057434082,
1106
+ "grad_norm": 0.17151758074760437,
1107
+ "learning_rate": 1.0551703335823225e-07,
1108
+ "epoch": 1.9777503090234858,
1109
+ "step": 12800
1110
+ },
1111
+ {
1112
+ "eval_unseen_lanes_loss": 0.21011516451835632,
1113
+ "eval_unseen_lanes_accuracy": 0.9336022339435309,
1114
+ "eval_unseen_lanes_runtime": 0.5534,
1115
+ "eval_unseen_lanes_samples_per_second": 5823.753,
1116
+ "eval_unseen_lanes_steps_per_second": 92.154,
1117
+ "epoch": 2.0,
1118
+ "step": 12944
1119
+ },
1120
+ {
1121
+ "eval_unseen_domain_loss": 1.0266432762145996,
1122
+ "eval_unseen_domain_accuracy": 0.7009111845399019,
1123
+ "eval_unseen_domain_runtime": 1.5817,
1124
+ "eval_unseen_domain_samples_per_second": 6314.155,
1125
+ "eval_unseen_domain_steps_per_second": 99.261,
1126
+ "epoch": 2.0,
1127
+ "step": 12944
1128
+ },
1129
+ {
1130
+ "eval_unseen_axis_loss": 1.6092302799224854,
1131
+ "eval_unseen_axis_accuracy": 0.6111379365845807,
1132
+ "eval_unseen_axis_runtime": 1.2767,
1133
+ "eval_unseen_axis_samples_per_second": 9732.892,
1134
+ "eval_unseen_axis_steps_per_second": 152.737,
1135
+ "epoch": 2.0,
1136
+ "step": 12944
1137
+ },
1138
+ {
1139
+ "eval_hard_loss": 0.3078996539115906,
1140
+ "eval_hard_accuracy": 0.9084033613445378,
1141
+ "eval_hard_runtime": 0.2718,
1142
+ "eval_hard_samples_per_second": 4377.996,
1143
+ "eval_hard_steps_per_second": 69.901,
1144
+ "epoch": 2.0,
1145
+ "step": 12944
1146
+ },
1147
+ {
1148
+ "train_runtime": 262.1825,
1149
+ "train_samples_per_second": 1579.793,
1150
+ "train_steps_per_second": 49.37,
1151
+ "total_flos": 0.0,
1152
+ "train_loss": 0.2282541081106707,
1153
+ "epoch": 2.0,
1154
+ "step": 12944
1155
+ }
1156
+ ]
1157
+ }
train_router_head.py ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Arabic zero-shot router with a real routing head.
3
+
4
+ Two earlier formulations, and why this one:
5
+
6
+ fixed-slot head - `num_labels = max_lanes` over one pooled vector. Scored exactly 1/n at every
7
+ lane count. The head is positional (slot i's logit is w_i . h) and the corpus
8
+ randomises lane order, so there is nothing to learn.
9
+ pairwise - score each (text, category) separately, argmax. Works (0.842 on unseen lane
10
+ sets) but costs n forward passes and each category is scored in isolation,
11
+ never against its competitors.
12
+
13
+ Here the categories live in the sequence and get their *own* pooled vectors, which a shared linear
14
+ head turns into one logit each; the softmax is over the categories present. The head is shared
15
+ across positions, so it scores category *content*, not slot index - which is what makes it
16
+ position-invariant and zero-shot.
17
+
18
+ Layout is text-first, categories-second, deliberately. The reference encoder is bidirectional so
19
+ order does not matter there; our base is causal, and this ordering is what lets every category
20
+ token attend to the whole text. Reversed, the categories would be encoded blind to the text.
21
+
22
+ النص:
23
+ {text}
24
+
25
+ الفئات:
26
+ - فئة أولى
27
+ - فئة ثانية
28
+
29
+ Each category's character span is mapped to token indices via the tokenizer's offset mapping and
30
+ mean-pooled. Slots beyond a row's category count are masked to -inf before the loss.
31
+ """
32
+ import json
33
+ import os
34
+ from collections import defaultdict
35
+ from pathlib import Path
36
+
37
+ import numpy as np
38
+ import torch
39
+ import torch.nn as nn
40
+ import torch.nn.functional as F
41
+ from torch.utils.data import Dataset
42
+ from transformers import AutoModel, AutoTokenizer, Trainer, TrainingArguments
43
+
44
+ os.environ.setdefault("CUDA_VISIBLE_DEVICES", "0")
45
+
46
+ BASE_MODEL = os.environ.get("BASE_MODEL", "oddadmix/50M-2048-Emhotob")
47
+ OUTPUT_DIR = os.environ.get("OUTPUT_DIR", "./Nawah-Router-head-50M")
48
+ DATA = Path(os.environ.get("DATA_DIR", "data"))
49
+ MAX_ROUTES = int(os.environ.get("MAX_ROUTES", 9))
50
+ MAX_LENGTH = int(os.environ.get("MAX_LENGTH", 320))
51
+ LR = float(os.environ.get("LR", 3e-4))
52
+ EPOCHS = float(os.environ.get("EPOCHS", 3))
53
+ BATCH_SIZE = int(os.environ.get("BATCH_SIZE", 32))
54
+ WARMUP = int(os.environ.get("WARMUP", 500))
55
+ SEED = 42
56
+
57
+
58
+ def build_text(row):
59
+ """-> (full string, [(start, end) char span per category])."""
60
+ head = f"النص:\n{row['text']}\n\nالفئات:\n"
61
+ s = head
62
+ spans = []
63
+ for c in row["routes"]:
64
+ s += "- "
65
+ spans.append((len(s), len(s) + len(c)))
66
+ s += c + "\n"
67
+ return s, spans
68
+
69
+
70
+ class RouterModel(nn.Module):
71
+ """Backbone + a shared linear scorer applied to each category's pooled span."""
72
+
73
+ def __init__(self, base_model):
74
+ super().__init__()
75
+ self.backbone = AutoModel.from_pretrained(base_model, dtype=torch.float32)
76
+ h = self.backbone.config.hidden_size
77
+ self.score = nn.Sequential(nn.Linear(h, h), nn.GELU(), nn.Linear(h, 1))
78
+ self.config = self.backbone.config
79
+
80
+ def forward(self, input_ids, attention_mask, cat_pool, n_routes, labels=None):
81
+ hs = self.backbone(input_ids=input_ids, attention_mask=attention_mask).last_hidden_state
82
+ # cat_pool: (B, K, T) row-normalised selector -> (B, K, H)
83
+ cat_vecs = torch.bmm(cat_pool.to(hs.dtype), hs)
84
+ logits = self.score(cat_vecs).squeeze(-1) # (B, K)
85
+ ar = torch.arange(logits.size(1), device=logits.device)[None, :]
86
+ logits = logits.masked_fill(ar >= n_routes[:, None], torch.finfo(logits.dtype).min)
87
+ loss = F.cross_entropy(logits, labels) if labels is not None else None
88
+ return {"loss": loss, "logits": logits}
89
+
90
+
91
+ def load(name):
92
+ p = DATA / f"{name}.jsonl"
93
+ return [json.loads(l) for l in open(p, encoding="utf-8")] if p.exists() else []
94
+
95
+
96
+ class RouterDataset(Dataset):
97
+ def __init__(self, rows, tok, max_length):
98
+ self.rows, self.tok, self.max_length = rows, tok, max_length
99
+
100
+ def __len__(self):
101
+ return len(self.rows)
102
+
103
+ def __getitem__(self, i):
104
+ r = self.rows[i]
105
+ text, spans = build_text(r)
106
+ enc = self.tok(text, return_offsets_mapping=True, add_special_tokens=False,
107
+ truncation=True, max_length=self.max_length)
108
+ ids, offs = enc["input_ids"], enc["offset_mapping"]
109
+ pool = torch.zeros(MAX_ROUTES, len(ids))
110
+ for ci, (s, e) in enumerate(spans[:MAX_ROUTES]):
111
+ idxs = [t for t, (a, b) in enumerate(offs) if a < e and b > s and a != b]
112
+ if idxs:
113
+ pool[ci, idxs] = 1.0 / len(idxs)
114
+ return {"input_ids": torch.tensor(ids, dtype=torch.long), "cat_pool": pool,
115
+ "n_routes": torch.tensor(min(len(r["routes"]), MAX_ROUTES), dtype=torch.long),
116
+ "labels": torch.tensor(min(r["label"], MAX_ROUTES - 1), dtype=torch.long)}
117
+
118
+
119
+ class Collator:
120
+ def __init__(self, pad_id):
121
+ self.pad_id = pad_id
122
+
123
+ def __call__(self, feats):
124
+ T = max(f["input_ids"].size(0) for f in feats)
125
+ ids, att, pools = [], [], []
126
+ for f in feats:
127
+ n = f["input_ids"].size(0); pad = T - n
128
+ ids.append(torch.cat([f["input_ids"], torch.full((pad,), self.pad_id, dtype=torch.long)]))
129
+ att.append(torch.cat([torch.ones(n, dtype=torch.long), torch.zeros(pad, dtype=torch.long)]))
130
+ pools.append(F.pad(f["cat_pool"], (0, pad)))
131
+ return {"input_ids": torch.stack(ids), "attention_mask": torch.stack(att),
132
+ "cat_pool": torch.stack(pools),
133
+ "n_routes": torch.stack([f["n_routes"] for f in feats]),
134
+ "labels": torch.stack([f["labels"] for f in feats])}
135
+
136
+
137
+ def metrics_fn(p):
138
+ return {"accuracy": float((np.asarray(p.predictions).argmax(-1) ==
139
+ np.asarray(p.label_ids)).mean())}
140
+
141
+
142
+ @torch.no_grad()
143
+ def report(model, tok, rows, name, batch=64):
144
+ model.eval()
145
+ ds = RouterDataset(rows, tok, MAX_LENGTH); coll = Collator(tok.pad_token_id)
146
+ preds = []
147
+ for i in range(0, len(rows), batch):
148
+ b = coll([ds[j] for j in range(i, min(i + batch, len(rows)))])
149
+ b = {k: v.to(next(model.parameters()).device) for k, v in b.items()}
150
+ b.pop("labels")
151
+ preds += model(**b)["logits"].argmax(-1).tolist()
152
+ correct = [int(p == r["label"]) for p, r in zip(preds, rows)]
153
+ acc = sum(correct) / len(rows)
154
+ rand = sum(1 / len(r["routes"]) for r in rows) / len(rows)
155
+ print(f"\n[{name}] n={len(rows):,} route accuracy {acc:.4f} random {rand:.4f}")
156
+ out = {"accuracy": acc, "random_baseline": rand, "n": len(rows)}
157
+ for key in ("n_routes", "difficulty", "mode"):
158
+ if key == "mode" and not all("mode" in r for r in rows):
159
+ continue
160
+ b = defaultdict(lambda: [0, 0])
161
+ for c, r in zip(correct, rows):
162
+ k = len(r["routes"]) if key == "n_routes" else r[key]
163
+ b[k][1] += 1; b[k][0] += c
164
+ print(f" by {key:<10} " +
165
+ " ".join(f"{k}:{v[0]/v[1]:.3f}(n={v[1]})" for k, v in sorted(b.items(), key=str)))
166
+ out[f"by_{key}"] = {str(k): {"acc": v[0]/v[1], "n": v[1]} for k, v in b.items()}
167
+ return out
168
+
169
+
170
+ def main():
171
+ tok = AutoTokenizer.from_pretrained(BASE_MODEL)
172
+ if tok.pad_token_id is None:
173
+ tok.pad_token = tok.eos_token
174
+ model = RouterModel(BASE_MODEL)
175
+ print(f"[*] {BASE_MODEL} | params {sum(p.numel() for p in model.parameters())/1e6:.2f}M "
176
+ f"| hidden {model.config.hidden_size}")
177
+
178
+ train = load("train")
179
+ evals = {"unseen_lanes": load("eval_unseen_lanes"),
180
+ "unseen_domain": load("eval_unseen_domain"),
181
+ # v2 only: axes held out of training entirely. The strongest zero-shot test, so the
182
+ # checkpoint is selected on it when it exists.
183
+ "unseen_axis": load("eval_unseen_axis"),
184
+ "hard": load("eval_hard")}
185
+ evals = {k: v for k, v in evals.items() if v}
186
+ print(f"[*] train {len(train):,} | " + " | ".join(f"{k} {len(v):,}" for k, v in evals.items()))
187
+
188
+ args = TrainingArguments(
189
+ output_dir=OUTPUT_DIR, num_train_epochs=EPOCHS,
190
+ per_device_train_batch_size=BATCH_SIZE, per_device_eval_batch_size=64,
191
+ learning_rate=LR, lr_scheduler_type="cosine", warmup_steps=WARMUP,
192
+ max_grad_norm=1.0, bf16=True, logging_steps=200,
193
+ eval_strategy="steps", eval_steps=1000, save_strategy="steps", save_steps=1000,
194
+ save_total_limit=2, load_best_model_at_end=True,
195
+ metric_for_best_model=("eval_unseen_axis_accuracy" if "unseen_axis" in evals
196
+ else "eval_unseen_domain_accuracy"), greater_is_better=True,
197
+ report_to=[], seed=SEED, dataloader_num_workers=4, remove_unused_columns=False,
198
+ label_names=["labels"])
199
+
200
+ trainer = Trainer(model=model, args=args,
201
+ train_dataset=RouterDataset(train, tok, MAX_LENGTH),
202
+ eval_dataset={k: RouterDataset(v, tok, MAX_LENGTH)
203
+ for k, v in evals.items() if v},
204
+ data_collator=Collator(tok.pad_token_id), compute_metrics=metrics_fn)
205
+ trainer.train()
206
+
207
+ Path(OUTPUT_DIR).mkdir(exist_ok=True)
208
+ torch.save(model.state_dict(), Path(OUTPUT_DIR, "router_model.pt"))
209
+ model.backbone.save_pretrained(OUTPUT_DIR); tok.save_pretrained(OUTPUT_DIR)
210
+ results = {k: report(model, tok, v, k) for k, v in evals.items() if v}
211
+ Path(OUTPUT_DIR, "train_metrics.json").write_text(json.dumps(
212
+ {"results": results, "base_model": BASE_MODEL, "formulation": "routing_head",
213
+ "log_history": trainer.state.log_history}, ensure_ascii=False, indent=2), encoding="utf-8")
214
+ print(f"\n[+] done -> {OUTPUT_DIR}")
215
+
216
+
217
+ if __name__ == "__main__":
218
+ main()