jonny commited on
Commit
4b1014f
·
verified ·
1 Parent(s): 0013b78

scope head b4: macro-F1 0.512 on classify-8, 15-label (14 scopes + none), trained 2026-08-15

Browse files
README.md ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ language:
4
+ - en
5
+ base_model: distilbert-base-uncased
6
+ pipeline_tag: text-classification
7
+ tags:
8
+ - topos
9
+ - scope-routing
10
+ - multi-label
11
+ - on-device
12
+ - privacy
13
+ datasets:
14
+ - AmazonScience/massive
15
+ - clinc/oos-eval
16
+ ---
17
+
18
+ # Topos Scope Head
19
+
20
+ An on-device, multi-label scope router for [Topos](https://github.com/dialoguesai/topos).
21
+ Given a free-text question, it predicts which UMA personal-data **scopes** the question
22
+ touches (e.g. `health:read`, `schedule:read`), predicts an explicit **`none`** class for
23
+ questions that touch no personal data, or signals **uncertainty** so the caller can
24
+ escalate to an LLM. It runs *before* the permission gate — the raw question is the one
25
+ input the pipeline sees pre-grant, which is why this model exists: so that step never
26
+ has to leave the device.
27
+
28
+ **This model is a router, not an authorization mechanism.** Every scope it emits is
29
+ still permission-checked downstream. A node without this model escalates to an LLM more
30
+ often; it does not become less safe.
31
+
32
+ ## Intended use
33
+
34
+ Deployed as the `scope` role in a Topos model pack, behind a four-branch ladder:
35
+
36
+ ```
37
+ labels ≥ tau_high, none low, nothing in band → ACT on the scope set (1..N scopes)
38
+ high none, nothing else → confident abstain (no personal data)
39
+ any label in [tau_low, tau_high) → ESCALATE to the pack's LLM (ambiguity)
40
+ nothing anywhere, none included → ESCALATE to the pack's LLM (ignorance)
41
+ ```
42
+
43
+ It is **not** intended to be used without the escalation path, and not intended for any
44
+ classification task other than Topos scope routing (its 14 scope labels + `none` are the
45
+ Topos scope registry, versioned; the loader refuses the artifact if the live registry
46
+ has drifted).
47
+
48
+ ## Numbers, with the method
49
+
50
+ All measured on `role_classify_8` (1,071 cases, 818 positive / 253 negative), template-
51
+ disjoint from training, threshold 0.70 selected on a pre-training grouped holdout —
52
+ never on this benchmark.
53
+
54
+ **Do not anchor these numbers against standard intent benchmarks.** This benchmark is
55
+ deliberately adversarial: every positive uses phrasing templates the model never saw,
56
+ gold is multi-label, and 24% of cases are near-miss negatives (third-party probes,
57
+ mechanism/definition traps) built to punish keyword matching. For scale on the *same*
58
+ benchmark: **mistral:7b, a 4.4 GB LLM, scores 0.495 macro-F1** — this 265 MB model beats
59
+ it while running every turn on-device.
60
+
61
+ | metric | value | mistral:7b, same benchmark |
62
+ |---|---|---|
63
+ | macro-F1 | **0.512** | 0.495 |
64
+ | exact set match | **0.496** | 0.243 |
65
+ | negatives abstained | **0.984** | 0.126 |
66
+ | single-gold vs multi-gold recall | 0.388 / 0.425 (gap −0.036) | — |
67
+ | dead rate (no opinion incl. `none`) | 0.171 | — |
68
+ | disjoint rate (acted, set shares nothing with gold) | 0.217 | — |
69
+ | per-scope recall ≥ 0.60 | 2 / 14 | 9 / 14 |
70
+
71
+ ### As the hybrid it is designed to front (head + LLM escalation)
72
+
73
+ Composed per-case: the head's verdict stands on act/abstain; escalated turns take the
74
+ LLM's actual answer on those exact cases.
75
+
76
+ | arm | macro-F1 | exact | neg-abstain | wrong-scope/turns | LLM sees |
77
+ |---|---|---|---|---|---|
78
+ | LLM-only mistral:7b (4.4 GB) | 0.495 | 0.243 | 0.126 | 0.237 | 100% |
79
+ | **hybrid: this model + mistral:7b** | **0.550** | **0.524** | **0.972** | **0.143** | **16.4%** |
80
+ | LLM-only llama3.2 (2 GB) | 0.376 | 0.261 | 0.510 | 0.204 | 100% |
81
+ | **hybrid: this model + llama3.2** | **0.500** | **0.508** | **0.976** | 0.146 | 16.4% |
82
+
83
+ The result that matters for low-RAM machines: **a 2 GB-LLM machine running the hybrid
84
+ (0.500) outperforms a 4.4 GB-LLM machine running LLM-only (0.495)** — the head is 265 MB
85
+ and identical on every hardware tier, so only ~1/6th of traffic degrades with weaker
86
+ hardware.
87
+
88
+ ## Known limitations — read before relying on it
89
+
90
+ * **Confident-none swallowing.** 222 of 818 benchmark positives (27%) are answered with
91
+ a confident `none` — silently, with no escalation. This is the model's dominant defect.
92
+ Measured mechanism: template familiarity (heldout phrasings fall to `none`), not class
93
+ balance. Callers should treat a `none` on a plausibly-personal question with suspicion.
94
+ * **Per-scope recall is far below production floors** (12 of 14 scopes under 0.60).
95
+ This artifact has **not** cleared its promotion gate and fronts an LLM in shadow /
96
+ advisory postures only.
97
+ * All numbers above are **synthetic-benchmark** numbers. Real-traffic behaviour is being
98
+ measured in shadow mode; median confidence on real language is substantially lower
99
+ than on generated text.
100
+ * English only. Trained entirely on synthetic templates + the public corpora below —
101
+ **no user data, ever** (the Topos loader refuses artifacts whose manifest violates
102
+ this).
103
+
104
+ ## Training data & attribution
105
+
106
+ | source | licence | rows | role |
107
+ |---|---|---|---|
108
+ | schema-grounded synthetic (Topos) | internal/synthetic | 1,927 | positives |
109
+ | compound conjunctions of the above | internal/synthetic | 1,772 | multi-scope positives |
110
+ | negative twins + concept negatives | internal/synthetic | 1,465 | boundary negatives |
111
+ | [AmazonScience/massive](https://huggingface.co/datasets/AmazonScience/massive) | CC BY 4.0 | 3,500 | real-register `none` + cue-mapped positives |
112
+ | [CLINC150 OOS](https://github.com/clinc/oos-eval) | CC BY 3.0 | 800 | out-of-scope `none` |
113
+
114
+ This model card is the attribution notice CC BY requires. No share-alike or
115
+ non-commercial data was used; the Topos loader enforces that class of licence refusal
116
+ at load time.
117
+
118
+ ## Architecture
119
+
120
+ DistilBERT-base-uncased, 15-label multi-label head (14 scopes + `none`), sigmoid
121
+ outputs, `BCEWithLogitsLoss` with per-label `pos_weight`, max_length 64. Artifact format
122
+ `topos-scope-head-1`: `head.json` (labels, thresholds, corpus manifest, metrics) +
123
+ `model/` (HF-standard). Load through `topos.query.scope_head.load_head`, which validates
124
+ labels against the live scope registry and the manifest against the licence policy
125
+ before any weight is touched.
head.json ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "format": "topos-scope-head-1",
3
+ "kind": "encoder",
4
+ "labels": [
5
+ "activity:read",
6
+ "ai_conversations:read",
7
+ "attention:read",
8
+ "availability:read",
9
+ "complexity:read",
10
+ "contacts:resolve",
11
+ "health:read",
12
+ "messages:read",
13
+ "places:read",
14
+ "public_bio:read",
15
+ "relationship_context:read",
16
+ "resources:read",
17
+ "schedule:read",
18
+ "work_context:read",
19
+ "none"
20
+ ],
21
+ "base_model": "distilbert-base-uncased",
22
+ "embedding_model": "",
23
+ "tau_high": 0.7,
24
+ "tau_low": 0.5499999999999999,
25
+ "max_length": 64,
26
+ "corpus_manifest": {
27
+ "corpora": [
28
+ {
29
+ "source": "schema-grounded (G3)",
30
+ "licence": "internal",
31
+ "url": "-",
32
+ "rows": 1927,
33
+ "note": "the only source for the nine uncovered scopes; `train` split only (1927 of 4447) \u2014 the rest belongs to the benchmark"
34
+ },
35
+ {
36
+ "source": "compound positives (B1)",
37
+ "licence": "internal",
38
+ "url": "-",
39
+ "rows": 1772,
40
+ "note": "multi-scope conjunctions of train-split parents at 0.92x positives; the corpus was 100% single-label and trained AGAINST co-activation (multi-gold recall 0.165 vs 0.356 single)"
41
+ },
42
+ {
43
+ "source": "negative twins (G4)",
44
+ "licence": "internal",
45
+ "url": "-",
46
+ "rows": 1156,
47
+ "note": "third-party + generalize at 0.60x positives (2461 available); teaches 'whose data', which M1 found rung 1 cannot represent"
48
+ },
49
+ {
50
+ "source": "concept negatives (G4b)",
51
+ "licence": "internal",
52
+ "url": "-",
53
+ "rows": 309,
54
+ "note": "mechanism + definition frames over the positives' own concept vocabulary; replaces the mined QA band, which yielded 102 usable rows from 119,700 questions and reached only 8 of 14 scopes"
55
+ },
56
+ {
57
+ "source": "AmazonScience/massive",
58
+ "licence": "CC BY-4.0",
59
+ "url": "https://amazon-massive-nlu-dataset.s3.amazonaws.com/amazon-massive-dataset-1.1.tar.gz",
60
+ "rows": 3500,
61
+ "note": "en-US only; positives cue-filtered (see POSITIVE_CUES)"
62
+ },
63
+ {
64
+ "source": "clinc/oos-eval",
65
+ "licence": "CC BY-3.0",
66
+ "url": "https://raw.githubusercontent.com/clinc/oos-eval/master/data/data_full.json",
67
+ "rows": 800,
68
+ "note": "all `none`; the OOS split is the near-miss anchor"
69
+ }
70
+ ],
71
+ "total_rows": 9464,
72
+ "attribution_required": [
73
+ "AmazonScience/massive",
74
+ "clinc/oos-eval"
75
+ ]
76
+ },
77
+ "metrics": {
78
+ "n": 1071,
79
+ "macro_f1": 0.5116596438030812,
80
+ "exact": 0.4957983193277311,
81
+ "negatives_abstained": 0.9841897233201581,
82
+ "scopes_below_floor": [
83
+ "activity:read",
84
+ "ai_conversations:read",
85
+ "attention:read",
86
+ "availability:read",
87
+ "complexity:read",
88
+ "health:read",
89
+ "messages:read",
90
+ "places:read",
91
+ "relationship_context:read",
92
+ "resources:read",
93
+ "schedule:read",
94
+ "work_context:read"
95
+ ],
96
+ "recall_single_gold": 0.38816738816738816,
97
+ "recall_multi_gold": 0.4245614035087719,
98
+ "multi_gap": -0.036394015341383756,
99
+ "dead_rate": 0.17114914425427874,
100
+ "disjoint_rate": 0.21710526315789475,
101
+ "threshold": 0.7
102
+ },
103
+ "trained_at": "2026-08-15"
104
+ }
model/config.json ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "activation": "gelu",
3
+ "architectures": [
4
+ "DistilBertForSequenceClassification"
5
+ ],
6
+ "attention_dropout": 0.1,
7
+ "bos_token_id": null,
8
+ "dim": 768,
9
+ "dropout": 0.1,
10
+ "dtype": "float32",
11
+ "eos_token_id": null,
12
+ "hidden_dim": 3072,
13
+ "id2label": {
14
+ "0": "LABEL_0",
15
+ "1": "LABEL_1",
16
+ "2": "LABEL_2",
17
+ "3": "LABEL_3",
18
+ "4": "LABEL_4",
19
+ "5": "LABEL_5",
20
+ "6": "LABEL_6",
21
+ "7": "LABEL_7",
22
+ "8": "LABEL_8",
23
+ "9": "LABEL_9",
24
+ "10": "LABEL_10",
25
+ "11": "LABEL_11",
26
+ "12": "LABEL_12",
27
+ "13": "LABEL_13",
28
+ "14": "LABEL_14"
29
+ },
30
+ "initializer_range": 0.02,
31
+ "label2id": {
32
+ "LABEL_0": 0,
33
+ "LABEL_1": 1,
34
+ "LABEL_10": 10,
35
+ "LABEL_11": 11,
36
+ "LABEL_12": 12,
37
+ "LABEL_13": 13,
38
+ "LABEL_14": 14,
39
+ "LABEL_2": 2,
40
+ "LABEL_3": 3,
41
+ "LABEL_4": 4,
42
+ "LABEL_5": 5,
43
+ "LABEL_6": 6,
44
+ "LABEL_7": 7,
45
+ "LABEL_8": 8,
46
+ "LABEL_9": 9
47
+ },
48
+ "max_position_embeddings": 512,
49
+ "model_type": "distilbert",
50
+ "n_heads": 12,
51
+ "n_layers": 6,
52
+ "pad_token_id": 0,
53
+ "problem_type": "multi_label_classification",
54
+ "qa_dropout": 0.1,
55
+ "seq_classif_dropout": 0.2,
56
+ "sinusoidal_pos_embds": false,
57
+ "tie_weights_": true,
58
+ "tie_word_embeddings": true,
59
+ "transformers_version": "5.10.2",
60
+ "vocab_size": 30522
61
+ }
model/model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:91a54633cd655bc8ca3cd71e9a90088136576d0fb2589d333167f8ce54f372a1
3
+ size 267872556
model/tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
model/tokenizer_config.json ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "backend": "tokenizers",
3
+ "cls_token": "[CLS]",
4
+ "do_lower_case": true,
5
+ "is_local": false,
6
+ "local_files_only": false,
7
+ "mask_token": "[MASK]",
8
+ "model_max_length": 512,
9
+ "pad_token": "[PAD]",
10
+ "sep_token": "[SEP]",
11
+ "strip_accents": null,
12
+ "tokenize_chinese_chars": true,
13
+ "tokenizer_class": "BertTokenizer",
14
+ "unk_token": "[UNK]"
15
+ }