File size: 13,067 Bytes
a86df9f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Write the model card and publish the trained decision model to the Hugging Face Hub.

    python finalize.py --repo kongyo/Ketsudan-27B --name final2 [--public]

Reads the run's reports from /content/out, writes README.md next to the adapters and head,
uploads the folder to the repo root, the evaluation files to eval/ and this code to code/.
"""

import argparse
import json
import os

parser = argparse.ArgumentParser()
parser.add_argument("--repo", required = True)
parser.add_argument("--out", default = "/content/out")
parser.add_argument("--name", default = "final")
parser.add_argument("--public", action = "store_true")
parser.add_argument("--dry-run", action = "store_true")
args = parser.parse_args()

folder = f"{args.out}/{args.name}"
report = json.load(open(f"{folder}/train_report.json", encoding = "utf-8"))
model_name = args.repo.split("/")[-1]


def load(path):
    path = f"{args.out}/{path}"
    return json.load(open(path, encoding = "utf-8")) if os.path.exists(path) else None


def pct(x):
    return "–" if x is None else f"{x:.3f}"


ours_td = report["eval"]["typed_decisions_test"]
jev_td = load("jev_td_test_metrics.json")
ours_ho_parts = [load(f"{args.name}_heldout_metrics.json") or report["eval"].get("heldout"), load(f"{args.name}_heldout2_metrics.json")]
jev_ho_parts = [load("jev_heldout_metrics.json"), load("jev_heldout2_metrics.json")]


def merged(parts, key = "accuracy_by_workflow"):
    out = {}
    for part in parts:
        if part:
            out.update(part[key])
    return out


ours_ho, jev_ho = merged(ours_ho_parts), merged(jev_ho_parts)
ours_ece = [p["ece"] for p in ours_ho_parts if p]
jev_ece = [p["ece"] for p in jev_ho_parts if p]


def bold_pair(a, b, lower_better = False):
    better_a = (a <= b) if lower_better else (a >= b)
    return (f"**{pct(a)}**" if better_a else pct(a)), (pct(b) if better_a else f"**{pct(b)}**")


def td_row(name, m):
    t = m["accuracy_by_type"]
    return (f"| {name} | {pct(m['accuracy'])} | {pct(t.get('noul'))} | {pct(t.get('choice'))} | {pct(t.get('score'))} | "
            f"{pct(m['kl_from_gold'])} | {pct(m['brier'])} | {pct(m['ece'])} | {pct(m['record_accuracy'])} |")


td_lines = [
    "| Model | Accuracy ↑ | noul | choice | score | KL from gold ↓ | Brier ↓ | ECE ↓ | All 5 right ↑ |",
    "|---|---:|---:|---:|---:|---:|---:|---:|---:|",
    td_row(f"**{model_name}** (this model)", ours_td),
]
if jev_td:
    td_lines.append(td_row("TypeSafe Jev (`jev-latest` → jev-1.13.0, same day)", jev_td))
td_table = "\n".join(td_lines)
claim = ""
if jev_td:
    cols = [ours_td["accuracy"] >= jev_td["accuracy"], ours_td["kl_from_gold"] <= jev_td["kl_from_gold"],
            ours_td["brier"] <= jev_td["brier"], ours_td["ece"] <= jev_td["ece"],
            ours_td["record_accuracy"] >= jev_td["record_accuracy"]]
    types = [ours_td["accuracy_by_type"][k] >= v for k, v in jev_td["accuracy_by_type"].items()]
    flows = [ours_td["accuracy_by_workflow"][k] >= v for k, v in jev_td["accuracy_by_workflow"].items()]
    if all(cols + types + flows):
        claim = f"{model_name} matches or beats Jev on every column, every question type and every workflow."
    else:
        claim = (f"{model_name} is ahead of Jev on {sum(cols)} of 5 summary columns, {sum(types)} of {len(types)} "
                 f"question types and {sum(flows)} of {len(flows)} workflows.")

LABELS = {
    "anli": "ANLI test r1–r3 (400, 3-way NLI)",
    "winogrande": "WinoGrande dev (400, commonsense pronoun / blank)",
    "hellaswag": "HellaSwag validation (400, 4 endings)",
    "emotion": "dair-ai/emotion test (400, 6 emotions)",
}
both = [b for b in LABELS if b in ours_ho and b in jev_ho]
ho_lines = ["| Benchmark (rows) | This model | Jev (same requests, same day) |", "|---|---:|---:|"]
for b in both:
    a, j = bold_pair(ours_ho[b], jev_ho[b])
    ho_lines.append(f"| {LABELS[b]} | {a} | {j} |")
if both:
    a, j = bold_pair(sum(ours_ho[b] for b in both) / len(both), sum(jev_ho[b] for b in both) / len(both))
    ho_lines.append(f"| Mean accuracy | {a} | {j} |")
if ours_ece and jev_ece:
    a, j = bold_pair(sum(ours_ece) / len(ours_ece), sum(jev_ece) / len(jev_ece), lower_better = True)
    ho_lines.append(f"| Mean ECE ↓ | {a} | {j} |")
ho_table = "\n".join(ho_lines)

losses = [LABELS[b].split(" (")[0] for b in both if ours_ho[b] < jev_ho[b]]
weak = []
if losses:
    weak.append(f"* Trails Jev on {', '.join(losses)} (see the held-out table).")
if ours_ece and sum(ours_ece) / len(ours_ece) > 0.08:
    weak.append("* Confidence outside the training distribution is less reliable (held-out ECE above). Refit the "
                "temperatures on a few hundred labelled examples of your own task (`FastDecisionModel.calibrate`).")
weak.append("* English only was evaluated. Text input only (the backbone's vision tower is not trained).")
weak.append("* Numbers, dates and counting belong in code, as for any System One style model.")
weak_text = "\n".join(weak)

stage2 = report.get("stage2")
s1 = report["args"]
training_rows = [
    f"| Stage 1 | LoRA r={s1['lora_r']} on every language-model projection (attention, Gated DeltaNet, MLP) + a fresh joint schema head; "
    f"lr {s1['lr']} (head {s1['head_lr']}), cosine, soft-target cross-entropy, one pass over {report['train_items'] - (stage2 or {}).get('train_items', 0):,} records "
    f"(all 1,120 typed-decisions train cases, n4ze3m/typed-decisions-synth, Unsloth's public mixture) in a 32-minute budget |",
]
if stage2:
    sources = ", ".join(f"{k} ({v})" for k, v in stage2["sources"].items())
    training_rows.append(
        f"| Stage 2 | continued from stage 1 at lr {stage2['args']['lr']} (head {stage2['args']['head_lr']}): {stage2['train_items']:,} records "
        f"= {stage2['td_replay']} typed-decisions cases replayed + commonsense / multiple-choice sources {sources} + fresh public-mixture and synthetic rows |"
    )
    training_rows.append("| Calibration | per-type temperatures refit on a mixed holdout: 80 unseen typed-decisions train cases + 320 unseen general items |")
else:
    training_rows.append("| Calibration | per-type temperatures fitted on 80 held-out typed-decisions train cases |")
training_rows.append(f"| Total | {report['train_items']:,} records, {report['train_tokens'] / 1e6:.2f}M tokens, {report['steps']} optimizer steps of 16 |")
minutes = (report.get("train_metrics", {}).get("train_runtime", 0) + (stage2 or {}).get("train_metrics", {}).get("train_runtime", 0)) / 60
training_rows.append(f"| Hardware | 1 × {report.get('gpu')} (Google Colab G4), {minutes:.0f} minutes of optimizer time in total |")
training_table = "\n".join(training_rows)

card = f"""---
license: apache-2.0
base_model: Qwen/Qwen3.8-27B
library_name: unsloth
pipeline_tag: text-classification
tags:
- decision-model
- typed-decisions
- system-one-compatible
- calibration
- clef-head
- unsloth
- lora
datasets:
- LocalLLaMA/typed-decisions
- n4ze3m/typed-decisions-synth
language:
- en
---

# {model_name} — a calibrated decision model on Qwen3.8-27B

**{model_name}** (決断, "decision") turns [Qwen/Qwen3.8-27B](https://huggingface.co/Qwen/Qwen3.8-27B) into a
*decision model*: it reads a `state` and typed questions — `choice`, `noul` (yes/no) and `score` (ordered levels) — and
returns a calibrated probability for every option instead of generating text. Requests and answers use the JSON shape of
a `POST /v1/systemone` body, so Jev-style requests work unchanged.

* **Backbone:** Qwen/Qwen3.8-27B (bf16) with LoRA adapters on every language-model projection.
* **Head:** a joint schema head of Cloudflare's [Clef](https://huggingface.co/Cloudflare/clef) design, trained from scratch
  (no Clef weights are used), through Unsloth's `FastDecisionModel`.
* **Open weights, Apache-2.0**, ~1.2 GB of adapters and head on top of the public base model.

Not affiliated with TypeSafe AI, Cloudflare, Unsloth or Qwen.

## Results

### typed-decisions test (400 cases × 5 questions = 2,000 decisions)

{td_table}

Same 400 requests, same scorer, same day ([`code/td_metrics.py`](code/td_metrics.py): accuracy = argmax vs the gold
label; KL(gold‖pred) with 1e-6 clipping; Brier = mean over options of the squared error; ECE = 10 bins of the top
probability). {claim} Per-workflow numbers are in [`eval/`](eval).

**Read this caveat:** {model_name} was trained on the typed-decisions `train` split (fitted mode); Jev never saw these
workflows. For reference, the dataset card lists TypeSafe Jev 1.13.0 at 0.727 (zero-shot) and models fitted on `train` at
0.766–0.801 (OpenDecider-large-td 0.801, od1 0.797, OpenDecider-nano 0.796, OpenDecider-small-td 0.792); those numbers come
from the dataset maintainers' or submitters' scorers, not ours, and the accuracy column is the only one that is comparable.

### Held-out tasks neither model was trained on

{ho_table}

Identical requests (same state, instructions and options) were sent to both models on the same day. These are
general zero-shot decisions outside the training data.

## Known weaknesses

{weak_text}

## Usage

```python
from unsloth import FastDecisionModel

model, tokenizer = FastDecisionModel.from_pretrained("{args.repo}")  # Qwen/Qwen3.8-27B + adapters + head
FastDecisionModel.for_inference(model)
answers = FastDecisionModel.predict(
    model, tokenizer,
    "Hi, I was charged twice for invoice #4411. Please refund the duplicate today.",
    {{
        "team": {{"type": "choice", "instructions": "Which team should handle this?",
                  "criteria": {{"billing": "invoices, payments, refunds", "technical": "bugs, outages, errors",
                               "sales": "pricing, new plans"}}}},
        "refund": {{"type": "noul", "instructions": "Does the customer ask for a refund?"}},
        "urgency": {{"type": "score", "instructions": "How urgent is this?",
                    "criteria": ["not urgent", "soon", "today"]}},
    }},
)
print(answers["team"]["answer"], answers["team"]["probabilities"])
```

About 56 GB of GPU memory in bf16; `load_in_4bit=True` runs it on a 24 GB GPU. Every answer carries `probabilities` over
all options; `noul` answers carry the probability of yes. Unsloth Studio can serve the folder behind a
TypeSafe-compatible `/v1/systemone` endpoint, so code written for the `typesafe-sdk` can point `TYPESAFE_BASE_URL` at it.

## Training

| | |
|---|---|
{training_table}

**No output of TypeSafe's Jev was used for training, calibration or model selection** (TypeSafe's terms forbid
distillation). Jev was only called to score it on the evaluation sets above.

**Decontamination.** Synthetic, public and commonsense training rows share no 13-word shingle with any evaluation state
(typed-decisions test, ANLI, WinoGrande; in stage 2 also HellaSwag and emotion). typed-decisions train has no state
identical to a test state (its cases share JSON boilerplate with the test cases, so shingles would flag most of them).
HellaSwag and emotion were not shingle-checked in stage 1; no stage-1 source draws on them.

## Licenses

Weights: Apache-2.0 (base model Apache-2.0). Training data keeps its own licenses: typed-decisions (Apache-2.0),
typed-decisions-synth (MIT), BANKING77 (CC-BY-4.0), CLINC150 (CC-BY-3.0), MNLI (mixed), SNLI (CC-BY-SA-4.0), WANLI
(CC-BY-4.0), BoolQ (CC-BY-SA-3.0), MMLU / CommonsenseQA (MIT), ARC (CC-BY-SA-4.0), OpenBookQA (Apache-2.0), COPA / WSC
(SuperGLUE terms), Social IQa and SWAG (as published by AI2), prompt-injections (Apache-2.0); AG News and SST-5 list no
license. The head follows Cloudflare's Clef design (Apache-2.0) as implemented in Unsloth.
"""

with open(f"{folder}/README.md", "w", encoding = "utf-8") as f:
    f.write(card)
if args.dry_run:
    print(card)
    raise SystemExit(0)

from huggingface_hub import HfApi

api = HfApi()
api.create_repo(args.repo, private = True, exist_ok = True)
api.upload_folder(folder_path = folder, repo_id = args.repo, commit_message = f"Decision model ({args.name}): adapters, head, calibration, card")
for name in sorted(os.listdir(args.out)):
    # Jev's raw answers stay private (only its aggregate metrics are published), so nobody can distil from them.
    if name.endswith((".json", ".jsonl")) and not name.startswith("_") and "rows" not in name and not (name.startswith("jev_") and name.endswith(".jsonl")):
        api.upload_file(path_or_fileobj = f"{args.out}/{name}", path_in_repo = f"eval/{name}", repo_id = args.repo)
code = os.path.dirname(os.path.abspath(__file__))
for name in sorted(os.listdir(code)):
    if name.endswith((".py", ".sh")):
        api.upload_file(path_or_fileobj = f"{code}/{name}", path_in_repo = f"code/{name}", repo_id = args.repo)
files = api.list_repo_files(args.repo)
for stale in ("final",):
    if any(path.startswith(f"{stale}/") for path in files):
        api.delete_folder(stale, repo_id = args.repo, commit_message = "Remove the training checkpoint copy")
if args.public:
    api.update_repo_settings(args.repo, private = False)
print("FINALIZED", args.repo, args.name, "public" if args.public else "private", flush = True)