File size: 13,385 Bytes
a1ede48 eb50150 01cd1d1 a1ede48 eb50150 01cd1d1 eb50150 a1ede48 eb50150 01cd1d1 eb50150 a1ede48 eb50150 01cd1d1 eb50150 01cd1d1 eb50150 01cd1d1 eb50150 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 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 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 | ---
license: apache-2.0
base_model: Qwen/Qwen3-0.6B-Base
language:
- tr
- en
tags:
- text-classification
- decision-making
- triage
- moderation
- turkish
pipeline_tag: text-classification
---

[](./README.md)
[](./README.md)
[](./README.md)
[](./README.md)
# ARBITER-general — 0.6B single-pass decision layer
**State in, typed decisions out. One forward pass, zero generated tokens.**
ARBITER is a fine-tune of `Qwen/Qwen3-0.6B-Base` that answers structured questions —
`choice` (up to 52 options), `score` (ordered levels), `noul` (yes/no) — in a single
forward pass, in Turkish or English. It never generates text: option-letter logits
(`A`–`Z`, `a`–`z`) are read at a fixed `Answer: (` slot and soft-maxed into calibrated
probabilities with a confidence score.
```python
from arbiter import Arbiter
arb = Arbiter("Utiric/arbiter-general") # a local directory path also works
out = arb.decide(
state="My package never arrived, I want a refund.",
questions=[
{"id": "intent", "type": "choice",
"instructions": "What does the customer want?",
"options": {"Refund": "wants money back",
"Whereabouts": "asks where the package is",
"Cancel": "wants to cancel the subscription",
"Greeting": "just saying hello"}},
{"id": "urgent", "type": "noul",
"instructions": "Does this need immediate action?",
"criteria": {"true": "angry, threatening, or time-critical",
"false": "routine request"}},
{"id": "tone", "type": "score",
"instructions": "Customer anger?",
"levels": ["calm", "annoyed", "angry", "furious"]},
],
)
# {"intent": {"choice": "Refund", "probabilities": {...}, "confidence": 0.61},
# "urgent": {"noul": 0.12, "confidence": 0.76, ...},
# "tone": {"level": "2", "score": 2.1, ...}}
```
Turkish works the same way:
```python
out = arb.decide(
state="Kargo iki günde geldi, ürün fotoğraftaki gibi. Teşekkürler!",
questions=[{"id": "sentiment", "type": "choice",
"instructions": "What is the overall sentiment polarity expressed in this text?",
"options": {"Positive": "satisfaction, praise, thanks",
"Neutral": "factual objective information",
"Negative": "complaint, anger, disappointment"}}],
)
# {"sentiment": {"choice": "Positive", "probabilities": {...}}}
```
> **No pip package (yet).** Copy `arbiter.py` from this repo (~200 lines, only
> `torch` + `transformers`) next to your code. No training stack, no server, no API
> key needed. The file mirrors the exact training prompt format — do not rephrase
> the prompt template inside it.
## Requirements
- Python 3.10+, `torch`, `transformers`. That is the whole dependency list.
- Weights are 1.2 GB (bf16). Comfortable in ~2 GB of VRAM; CPU works too
(float32, slower).
- No server, no API key, no Inference Providers deployment: you run `arbiter.py`
wherever your code already runs.
Measured latency (T4 GPU, September 2026, single questions, temperature 1.6):
| Situation | Latency |
|---|---|
| first call (warmup) | ~1 s |
| steady state, per question | 60–90 ms |
Latency grows with prompt length (prefill cost) and is flat in the number of
questions per pass: asking 6 questions at once costs barely more than asking 1.
---
## How it works

1. State and question are rendered into one prompt wrapped in `<state>` tags with
`Question (choice|yes/no|score):` labels and `(A) option — description` lines,
ending in `Answer: (`.
2. The model runs **once**. The hidden state at the slot position is projected
through the LM-head rows of the 52 letter tokens only.
3. Softmax over the valid options gives the distribution. Temperature is a **single
global value (1.6)**, fit post-hoc on held-out data and shipped in `s1_config.json`
— not per-task, not per-slice. `confidence` = top-1 minus top-2 probability gap.
4. `noul` returns P(yes); `score` returns the argmax level plus the expectation
over levels.
Latency scales with prompt length only (prefill cost) — there is no per-token
generation loop.
## Getting good answers (read this before prompting)
ARBITER is prompt-format sensitive: it answers from the exact prompt shape it
trained on. Three habits decide whether you get 0.88 or 0.37:
1. **Frame the state like training data.** A short domain prefix plus the raw
text works far better than a bare sentence, e.g.
`[SUPPORT_TIER: return_desk] | Ticket #43998 Customer purchased a wireless
headphones 15 days ago. Reason: 'item defective on arrival'...`
We measured this directly on these weights: 8 hand-written free-form probes
scored 3/8, while 96 verbatim held-out rows in training format scored 85/96.
2. **Describe every option.** `{"Refund": "wants money back"}` beats
`["Refund", "Whereabouts", ...]`. The description is the signal; the name is
just a key.
3. **Gate on confidence.** `confidence` is the top-1 minus top-2 probability gap:
```python
a = out["intent"]
if a["confidence"] < 0.2:
escalate(state) # human review or a stronger judge
else:
route(a["choice"])
```
## When to use it (and when not)
Good fit: support-ticket triage and routing, Discord/forum moderation
pre-filtering, dialogue turn-taking, code-review triage, sentiment buckets —
any pipeline step shaped "text in, one of N labels out" in Turkish or English.
Not a fit: arithmetic or reasoning (`2 + 2` with options `[3, 4, 5, 22]`
picks `22` — it matches strings, it does not compute), free-text generation,
standalone medical or legal decisions, more than 52 options in one question,
languages beyond TR/EN.
## API reference
`Arbiter(path, device=None, temperature=None, max_state_tokens=2048)`
| Argument | Meaning |
|---|---|
| `path` | HF repo id or local directory with weights |
| `device` | `"cuda"` / `"cpu"` (auto-detected by default) |
| `temperature` | softmax temperature; default read from `s1_config.json` (1.6) |
| `max_state_tokens` | state truncation budget (70% head / 30% tail, marked) |
`decide(state, questions)` — `state` is a string (or JSON-serializable object).
Each question needs `id`, `type` (`choice` \| `score` \| `noul`), and:
| Type | Fields | Returns |
|---|---|---|
| `choice` | `instructions`, `options` (list or `{name: description}`) | `choice`, `probabilities`, `confidence` |
| `score` | `instructions`, `levels` (ordered list) | `level`, `score` (expectation), `confidence`, `probabilities` |
| `noul` | `instructions`, optional `criteria: {true, false}` | `noul` = P(yes), `confidence`, `probabilities` |
Budgets: **2048** state tokens · **52** options per pass · Turkish + English tested.
---
## Evaluation — held-out (8 tasks × 500, full eval, temperature 1.6)
Held-out rows never appeared in training (exact-dedup isolated). Macro **0.8438** vs chance 0.2958.
| Slice | acc | ECE | Note |
|---|---|---|---|
| code_pr_review | 1.000 | 0.053 | |
| customer_action | 0.796 | 0.073 | weakest link, under active work |
| dialogue_turn_taking | 0.992 | 0.014 | |
| irony_sarcasm | 0.620 | 0.117 | noisy slice — do not use standalone |
| legal_contract_risk | 0.914 | 0.025 | routing signal, not legal advice |
| medical_triage | 0.778 | 0.112 | routing signal, never autonomous diagnosis |
| moderation_discord | 0.704 | 0.084 | tier-1 pre-filter; escalate on low confidence |
| multilingual_sentiment | 0.946 | 0.028 | |
| **macro** | **0.8438** | 0.063 | Brier 0.220 |
## Evaluation — public zero-shot (0-shot, slot-logit scoring, no generation)
| Benchmark (split, n) | ARBITER | Random / Majority |
|---|---|---|
| Belebele tur_Latn (900) / eng_Latn (900) | 0.284 / 0.340 | 0.25 / 0.28 |
| MMLU 6-subset avg (1,425) | 0.373 | 0.25 / 0.32 |
| Banking77 sample-500 | 0.035 | 0.013 |
| OffensEval-TR (3,528) | 0.797 | 0.50 / 0.80 |
| TweetEval offensive+hate (3,830) | 0.632 | 0.50 / 0.61 |
| XCOPA-TR (500) | 0.486 | 0.50 |
| XNLI-TR (1k sample) | 0.407 | 0.33 / 0.35 |
Read honestly: strong on trained domains, modest zero-shot — reported with random and
majority baselines everywhere, as it should be. XCOPA is chance-level; Banking77's
77-way zero-shot is 3× random but low in absolute terms. Treat these as a floor for
fine-tuning, not a ceiling.
## Evaluation — permutation consistency (position-bias check)
Same question, three different option orders (50 held-out rows × 8 slices, seeded
shuffles); a content-driven model must pick the same option every time.
| Slice | consistent |
|---|---|
| code_pr_review | 1.000 |
| customer_action | 0.980 |
| dialogue_turn_taking | 0.940 |
| irony_sarcasm | 0.880 |
| legal_contract_risk | 0.980 |
| medical_triage | 0.960 |
| moderation_discord | 0.900 |
| multilingual_sentiment | 0.960 |
| **overall (400 rows)** | **0.950** |
Position bias is dead: option-shuffle training (uniform Check 4 + per-epoch shuffle)
holds, weakest on the already-noisy irony slice. Nothing above is "not yet measured"
anymore.
## Training journey (held-out macro)
| Stage | macro | What changed |
|---|---|---|
| v1 SFT | 0.497 | baseline; overfit on 1.5 epochs |
| v2 + teacher relabel | 0.768 | 26k teacher-written descriptions |
| v3 + moderation data | 0.787 | moderation slice relabeled |
| v4 cherry-pick | 0.811 | kept gains, restored casualties |
| DPO-1 | 0.822 | 16k preference pairs |
| v5–v7 + new domains | 0.818 | legal/medical/multilingual siblings, irony-train contradiction found and dropped |
| v8 SFT | 0.840 | customer siblings (+19 pts on customer); moderation dipped 0.750 → 0.626 |
| **DPO-4 (this release)** | **0.844** | preference pairs recovered moderation to 0.704, kept the rest |
Recipe: full fine-tune of the 0.6B backbone, CE + 0.3·Brier loss, 1 epoch,
held-out-Brier early stopping, then Decision-DPO (β=1.0 + 0.1 CE anchor, LR 1e-5).
## Data card (summary)
- ~130k training rows across 26 tasks; ~26k teacher-(re)labeled rows.
- The teacher model (Ministral-14B) writes **descriptions only**; gold labels are
deterministic (rules, dataset labels, star ratings). Teacher-written golds proved
unreliable on irony and were reverted to dataset labels there.
- Gates on every iteration: exact-dedup train↔held-out, test-set contamination scan
(**0 overlaps** across 11.4k public-benchmark rows), option-name leakage scan on
teacher-written descriptions.
## Limitations (please read before deploying)
- A classifier head — not a reasoner, generator, or judge. No free text, ever.
- No arithmetic or multi-step reasoning. `2 + 2` with options `[3, 4, 5, 22]`
answers `22`: it matches surface strings, it does not compute.
- Medical and legal outputs are triage signals for pipelines **with humans in the loop**.
- Moderation output is a tier-1 signal; escalate to a stronger judge and a human
on low confidence.
- Turkish + English only. Customer-action and irony are the weakest slices.
- Not hardened against adversarial prompt injection.
## FAQ
**Why is temperature 1.6, above 1.0?**
Decision-DPO sharpens the distributions (expected side effect). 1.6 is a single
global value fit post-hoc on held-out data to re-calibrate them — not per-task,
not per-slice. If you fine-tune or DPO further, refit it.
**What does `confidence` mean?**
Top-1 probability minus top-2. A 0.90 prediction with a 0.08 runner-up
(gap 0.82) is a sure thing; 0.45 vs 0.44 (gap 0.01) is a coin flip. Threshold it.
**More than 52 options?**
Chunk them: score 40-option windows with a `none of the above` catcher, then
re-score the finalists in one pass. `arbiter.py` covers the single-pass case
(≤52); port the chunking from `s1/engine.py` for the rest.
**Several questions at once?**
Yes — pass a list. Every question gets its own answer slot in one forward pass
(the quickstart answers intent + urgency + tone together).
**Can I fine-tune it?**
It is a stock Qwen3-0.6B-Base fine-tune (full, not LoRA), so any HF trainer
works. Keep option shuffling on and early-stop on held-out Brier, not train
loss — 1.5 epochs already overfit this recipe once
(v1: 0.94 in-task, 0.50 held-out).
**Why no Inference Providers / widget?**
The hosted inference API generates text; this model never generates — it is read
at a logit slot. A widget would return nonsense, so the model is deliberately
left undeployed there. Run `arbiter.py` locally instead.
## Files
| File | What |
|---|---|
| `model.safetensors` (+ `config.json`, `tokenizer.*`, `s1_config.json`) | weights, Qwen3-0.6B-Base format |
| `arbiter.py` | standalone inference client — copy it, nothing to install |
| `arbiter-banner.svg`, `arbiter-how.svg` | release artwork |
| `README.md` | this card |
## License & citation
Apache-2.0 (base-model license carried over).
```bibtex
@misc{arbiter2026,
title = {ARBITER-general: a 0.6B single-pass decision layer for TR+EN},
author = {Utiric},
year = {2026},
note = {Held-out macro 0.844 across 8 tasks; Qwen3-0.6B-Base fine-tune + Decision-DPO}
}
```
|