v0.3: +5k SFT steps with weak-spot data (177k real + 78k targeted synthetic), recalibrated (public macro 63.2 -> 74.3)
Browse files- README.md +38 -31
- formatting.py +8 -1
- model.safetensors +1 -1
- types.py +5 -0
README.md
CHANGED
|
@@ -116,20 +116,20 @@ on `scripts/06b_public_benchmarks.py` rebuilds of the same subsets.
|
|
| 116 |
|
| 117 |
| subset | type | n | **OpenThai 0.8B** | Nimble-9B | Jev 1.13.0 | our ECE |
|
| 118 |
|---|---|---|---|---|---|---|
|
| 119 |
-
| aegis2 | noul | 250 |
|
| 120 |
-
| boolq | noul | 300 |
|
| 121 |
-
| civil_comments | noul | 300 | **79.0** | 70.3 | 81.0 | 0.
|
| 122 |
-
| helpsteer2 | score | 250 | **41.6** | 39.0 | 34.1 | 0.
|
| 123 |
-
| massive-de-DE | choice | 350 |
|
| 124 |
-
| massive-en-US | choice | 350 |
|
| 125 |
-
| multinli | choice | 299 | **
|
| 126 |
-
| paws | noul | 250 |
|
| 127 |
-
| pubmedqa | choice | 250 |
|
| 128 |
-
| squad2 | noul | 299 |
|
| 129 |
-
| summeval-consistency | score | 144 |
|
| 130 |
-
| summeval-relevance | score | 240 |
|
| 131 |
-
| vitaminc-dev | choice | 599 |
|
| 132 |
-
| **macro average** | | | **
|
| 133 |
|
| 134 |
For scale: Bespoke reports raw Qwen3.5-0.8B at 45.4 on their *private* 324-item holdout (not this bench), Nimble-9B at 90.1, Jev at 93.2.
|
| 135 |
`choice`/`noul` report accuracy; `score` reports exact-level match. Bold = ahead of Bespoke-Nimble-9B.
|
|
@@ -141,27 +141,27 @@ summary *relevance* scoring, which is the one subset where our score head is bad
|
|
| 141 |
|
| 142 |
| set | type | n | accuracy | macro-F1 / MAE | ECE | note |
|
| 143 |
|---|---|---|---|---|---|---|
|
| 144 |
-
| MASSIVE-th intent (60-way) | choice | 5007 | **
|
| 145 |
-
| Prachathai67k topics | choice | 3501 | **
|
| 146 |
| Prachathai67k topics | noul | 13119 | **94.2** | | 0.008 | eval split |
|
| 147 |
-
| XNLI-th | choice | 2490 | **
|
| 148 |
-
| XNLI-th (entailment yes/no) | noul | 2490 | **
|
| 149 |
-
| SIB-200 Thai topic (7-way) | choice | 204 | **
|
| 150 |
-
| Thai contrastive pairs (one-fact flips) | choice | 296 | **
|
| 151 |
-
| Thai contrastive pairs | score | 56 | **
|
| 152 |
-
| Thai contrastive pairs | noul | 248 | **
|
| 153 |
-
| Wongnai review stars (1β5) | score | 6203 | **63.
|
| 154 |
-
| Wisesight sentiment (4-class) | choice | 2671 | 51.
|
| 155 |
-
| banking77 intent (77-way, English) | choice | 3076 |
|
| 156 |
-
| xLAM tool selection (English) | choice | 884 | **99.4** | F1 0.986 | 0.
|
| 157 |
|
| 158 |
Batch-1 latency, one question with 255 options, H100 shared with a training job: **44 ms** (public bench run),
|
| 159 |
48 ms (held-out run). A 3-question Thai ticket (166 tokens): ~40 ms on H100, 154 ms on a MacBook M3 Max (MPS).
|
| 160 |
|
| 161 |
### Calibration (Stage 3)
|
| 162 |
|
| 163 |
-
v0.
|
| 164 |
-
the
|
| 165 |
|
| 166 |
After SFT (12k steps) the backbone was frozen and the slot head plus one temperature per question type were trained for
|
| 167 |
400 steps on the SFT mixture with cross-entropy + Brier loss (`configs/calib.yaml`, `brier_weight: 1.0`,
|
|
@@ -195,20 +195,27 @@ Thai sets and on NLI/topic tasks, and route low-confidence English yes/no decisi
|
|
| 195 |
* Text only. Up to 255 options per question in one stage (bucket into groups for more). 64k tokens per request.
|
| 196 |
* It is a small model: use the `confidence` field and route low-confidence cases to a bigger model or a human.
|
| 197 |
* Not a reasoning model: it will not do multi-step verification or arithmetic.
|
| 198 |
-
* v0.
|
| 199 |
-
|
| 200 |
-
|
| 201 |
* English calibration is weaker than Thai (median ECE 0.15 vs β€ 0.05): the training mix is Thai-heavy by design.
|
| 202 |
|
| 203 |
## Versions
|
| 204 |
|
| 205 |
| version | date | change | public macro | Wisesight |
|
| 206 |
|---|---|---|---|---|
|
|
|
|
| 207 |
| v0.2 | 2026-09-21 | +3,000 SFT steps from v0.1 with a 22k-record synthetic Thai social-sentiment set (4/3/5-class, yes/no, score schemes), re-calibrated | **63.2** | 51.5 |
|
| 208 |
| v0.1 | 2026-09-20 | initial release: Thai CPT 4.47B tokens, 12k-step SFT, calibration | 61.9 | 38.7 |
|
| 209 |
|
| 210 |
## Changelog
|
| 211 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
**v0.2 β 2026-09-21**
|
| 213 |
- Continued fine-tuning for 3,000 steps from v0.1 with a new 22k-record synthetic Thai social-media sentiment set
|
| 214 |
(4-class with the "question" class, 3/5-class variants, yes/no flags, 5-level score), then re-calibrated.
|
|
|
|
| 116 |
|
| 117 |
| subset | type | n | **OpenThai 0.8B** | Nimble-9B | Jev 1.13.0 | our ECE |
|
| 118 |
|---|---|---|---|---|---|---|
|
| 119 |
+
| aegis2 | noul | 250 | **83.2** | 81.2 | 80.4 | 0.065 |
|
| 120 |
+
| boolq | noul | 300 | 79.7 | 86.0 | 89.7 | 0.049 |
|
| 121 |
+
| civil_comments | noul | 300 | **79.0** | 70.3 | 81.0 | 0.087 |
|
| 122 |
+
| helpsteer2 | score | 250 | **41.6** | 39.0 | 34.1 | 0.371 |
|
| 123 |
+
| massive-de-DE | choice | 350 | **88.3** | 83.4 | 86.9 | 0.057 |
|
| 124 |
+
| massive-en-US | choice | 350 | **88.3** | 86.9 | 87.4 | 0.070 |
|
| 125 |
+
| multinli | choice | 299 | **89.0** | 85.3 | 82.9 | 0.053 |
|
| 126 |
+
| paws | noul | 250 | **94.0** | 82.8 | 89.2 | 0.035 |
|
| 127 |
+
| pubmedqa | choice | 250 | 64.0 | 75.6 | 77.2 | 0.259 |
|
| 128 |
+
| squad2 | noul | 299 | **89.3** | 80.6 | 82.9 | 0.041 |
|
| 129 |
+
| summeval-consistency | score | 144 | 75.0 | 75.7 | 81.2 | 0.076 |
|
| 130 |
+
| summeval-relevance | score | 240 | 21.7 | 49.2 | 35.0 | 0.356 |
|
| 131 |
+
| vitaminc-dev | choice | 599 | 72.5 | 76.6 | 80.1 | 0.118 |
|
| 132 |
+
| **macro average** | | | **74.3** | 74.8 | 76.0 | |
|
| 133 |
|
| 134 |
For scale: Bespoke reports raw Qwen3.5-0.8B at 45.4 on their *private* 324-item holdout (not this bench), Nimble-9B at 90.1, Jev at 93.2.
|
| 135 |
`choice`/`noul` report accuracy; `score` reports exact-level match. Bold = ahead of Bespoke-Nimble-9B.
|
|
|
|
| 141 |
|
| 142 |
| set | type | n | accuracy | macro-F1 / MAE | ECE | note |
|
| 143 |
|---|---|---|---|---|---|---|
|
| 144 |
+
| MASSIVE-th intent (60-way) | choice | 5007 | **90.0** | F1 0.869 | 0.043 | eval split |
|
| 145 |
+
| Prachathai67k topics | choice | 3501 | **98.1** | F1 0.938 | 0.004 | eval split |
|
| 146 |
| Prachathai67k topics | noul | 13119 | **94.2** | | 0.008 | eval split |
|
| 147 |
+
| XNLI-th | choice | 2490 | **77.1** | F1 0.772 | 0.045 | eval split |
|
| 148 |
+
| XNLI-th (entailment yes/no) | noul | 2490 | **84.3** | | 0.049 | eval split |
|
| 149 |
+
| SIB-200 Thai topic (7-way) | choice | 204 | **77.9** | F1 0.759 | 0.084 | whole dataset held out (v0.1: 77.5) |
|
| 150 |
+
| Thai contrastive pairs (one-fact flips) | choice | 296 | **80.7** | F1 0.734 | 0.098 | synthetic, eval-only |
|
| 151 |
+
| Thai contrastive pairs | score | 56 | **78.6** | MAE 0.35 | 0.156 | synthetic, eval-only |
|
| 152 |
+
| Thai contrastive pairs | noul | 248 | **83.5** | | 0.109 | synthetic, eval-only |
|
| 153 |
+
| Wongnai review stars (1β5) | score | 6203 | **63.5** | MAE 0.44 | 0.039 | eval split |
|
| 154 |
+
| Wisesight sentiment (4-class) | choice | 2671 | 51.6 | F1 0.448 | 0.353 | whole dataset held out β v0.1 38.7 β v0.2 51.5 β now 51.6 (weakest Thai set; use order-invariant mode) |
|
| 155 |
+
| banking77 intent (77-way, English) | choice | 3076 | 45.4 | F1 0.417 | 0.236 | whole dataset held out β 77-way near-duplicate intents (v0.1 32.7; 61.7 with order-invariant mode on v0.2) |
|
| 156 |
+
| xLAM tool selection (English) | choice | 884 | **99.4** | F1 0.986 | 0.006 | eval slice |
|
| 157 |
|
| 158 |
Batch-1 latency, one question with 255 options, H100 shared with a training job: **44 ms** (public bench run),
|
| 159 |
48 ms (held-out run). A 3-question Thai ticket (166 tokens): ~40 ms on H100, 154 ms on a MacBook M3 Max (MPS).
|
| 160 |
|
| 161 |
### Calibration (Stage 3)
|
| 162 |
|
| 163 |
+
v0.3 learned temperatures: choice 1.055, noul 1.047, score 1.008 (the before/after table below was measured on v0.1;
|
| 164 |
+
the procedure is identical in every version).
|
| 165 |
|
| 166 |
After SFT (12k steps) the backbone was frozen and the slot head plus one temperature per question type were trained for
|
| 167 |
400 steps on the SFT mixture with cross-entropy + Brier loss (`configs/calib.yaml`, `brier_weight: 1.0`,
|
|
|
|
| 195 |
* Text only. Up to 255 options per question in one stage (bucket into groups for more). 64k tokens per request.
|
| 196 |
* It is a small model: use the `confidence` field and route low-confidence cases to a bigger model or a human.
|
| 197 |
* Not a reasoning model: it will not do multi-step verification or arithmetic.
|
| 198 |
+
* v0.3 known weak spots (numbers above): summary *relevance* scoring (22) and helpfulness
|
| 199 |
+
scoring (42) β the two 5-level rating tasks; fine-grained 77-way English intents (banking77 45 single-order); Thai social
|
| 200 |
+
sentiment (wisesight 52); PubMedQA 3-way (64).
|
| 201 |
* English calibration is weaker than Thai (median ECE 0.15 vs β€ 0.05): the training mix is Thai-heavy by design.
|
| 202 |
|
| 203 |
## Versions
|
| 204 |
|
| 205 |
| version | date | change | public macro | Wisesight |
|
| 206 |
|---|---|---|---|---|
|
| 207 |
+
| v0.3 | 2026-09-22 | +5,000 SFT steps from v0.2 with 177k real train-split records + 78k targeted synthetic records for the weak spots (grounded QA, summary rating, fine-grained intents, safety, paraphrase), re-calibrated | **74.3** | 51.6 |
|
| 208 |
| v0.2 | 2026-09-21 | +3,000 SFT steps from v0.1 with a 22k-record synthetic Thai social-sentiment set (4/3/5-class, yes/no, score schemes), re-calibrated | **63.2** | 51.5 |
|
| 209 |
| v0.1 | 2026-09-20 | initial release: Thai CPT 4.47B tokens, 12k-step SFT, calibration | 61.9 | 38.7 |
|
| 210 |
|
| 211 |
## Changelog
|
| 212 |
|
| 213 |
+
**v0.3 β 2026-09-22**
|
| 214 |
+
- Continued fine-tuning for 5,000 steps from v0.2 with the weak-spot data: real train splits (SQuAD2, BoolQ, PubMedQA-artificial, PAWS, Aegis2, ToxicChat, XQuAD-th, MASSIVE-de; 177k records) and five targeted synthetic sets generated with Qwen3.6-35B-A3B and blind-checked (grounded yes/no QA with near-miss unanswerables 30k, summary rating on the SummEval rubrics 20k, 40β120-way near-duplicate intent taxonomies 12k, safety judgments 8k, adversarial paraphrases 8k); sentiment set weight lowered from 4Γ to 2Γ; re-calibrated.
|
| 215 |
+
- Public 13-subset macro 63.2 β **74.3** (Nimble-9B 74.8): SQuAD2 50.2 β 89.3, PAWS 68.0 β 94.0, Aegis2 61.6 β 83.2, MASSIVE-de 67.4 β 88.3, BoolQ 64.7 β 79.7, MASSIVE-en 79.1 β 88.3, PubMedQA 56.4 β 64.0, SummEval-relevance 14.2 β 21.7, VitaminC 68.6 β 72.5, MultiNLI 87.3 β 89.0. ECE improved on 11 of 13 subsets.
|
| 216 |
+
- Regression: SummEval-consistency 84.0 β 75.0 (the synthetic consistency set only covers levels 1/3/5; levels 2/4 will be added). Thai held-out sets all flat or up (SIB-200 74.0 β 77.9, banking77 37.8 β 45.4 single-order).
|
| 217 |
+
- Note: SQuAD2, BoolQ, PAWS, Aegis2 and MASSIVE-de are no longer "never trained on": their *train* splits are now in the mix; the public-bench subsets still use the validation/test splits only.
|
| 218 |
+
|
| 219 |
**v0.2 β 2026-09-21**
|
| 220 |
- Continued fine-tuning for 3,000 steps from v0.1 with a new 22k-record synthetic Thai social-media sentiment set
|
| 221 |
(4-class with the "question" class, 3/5-class variants, yes/no flags, 5-level score), then re-calibrated.
|
formatting.py
CHANGED
|
@@ -86,12 +86,14 @@ def question_to_spec(
|
|
| 86 |
shuffle: bool = False,
|
| 87 |
rng: Optional[random.Random] = None,
|
| 88 |
drop_label: bool = False,
|
|
|
|
| 89 |
) -> QuestionSpec:
|
| 90 |
"""Flatten a typed question.
|
| 91 |
|
| 92 |
label: for training. Choice -> option name; Score -> level index (int); Noul -> bool.
|
| 93 |
shuffle: permute option order (Choice only; Score/Noul order is semantic).
|
| 94 |
drop_label: remove the correct option from a Choice so the target becomes ABSTAIN_SLOT.
|
|
|
|
| 95 |
"""
|
| 96 |
if isinstance(q, Choice):
|
| 97 |
names = list(q.criteria.keys())
|
|
@@ -107,7 +109,9 @@ def question_to_spec(
|
|
| 107 |
raise ValueError("cannot drop the only option")
|
| 108 |
idx.remove(label_idx)
|
| 109 |
label_idx = None
|
| 110 |
-
if
|
|
|
|
|
|
|
| 111 |
(rng or random).shuffle(idx)
|
| 112 |
names_p = [names[i] for i in idx]
|
| 113 |
descs_p = [descs[i] for i in idx]
|
|
@@ -196,10 +200,12 @@ class Formatter:
|
|
| 196 |
drop_label_for: Optional[Sequence[str]] = None,
|
| 197 |
rng: Optional[random.Random] = None,
|
| 198 |
state_indent: Optional[int] = None,
|
|
|
|
| 199 |
) -> Encoded:
|
| 200 |
rng = rng or random.Random()
|
| 201 |
labels = labels or {}
|
| 202 |
drop = set(drop_label_for or [])
|
|
|
|
| 203 |
qids = list(questions.keys())
|
| 204 |
if shuffle_questions:
|
| 205 |
rng.shuffle(qids)
|
|
@@ -212,6 +218,7 @@ class Formatter:
|
|
| 212 |
shuffle=shuffle_options,
|
| 213 |
rng=rng,
|
| 214 |
drop_label=qid in drop,
|
|
|
|
| 215 |
)
|
| 216 |
for qid in qids
|
| 217 |
]
|
|
|
|
| 86 |
shuffle: bool = False,
|
| 87 |
rng: Optional[random.Random] = None,
|
| 88 |
drop_label: bool = False,
|
| 89 |
+
perm: Optional[Sequence[int]] = None,
|
| 90 |
) -> QuestionSpec:
|
| 91 |
"""Flatten a typed question.
|
| 92 |
|
| 93 |
label: for training. Choice -> option name; Score -> level index (int); Noul -> bool.
|
| 94 |
shuffle: permute option order (Choice only; Score/Noul order is semantic).
|
| 95 |
drop_label: remove the correct option from a Choice so the target becomes ABSTAIN_SLOT.
|
| 96 |
+
perm: explicit option order for a Choice (list of original indices), e.g. a cyclic shift for order-invariant inference.
|
| 97 |
"""
|
| 98 |
if isinstance(q, Choice):
|
| 99 |
names = list(q.criteria.keys())
|
|
|
|
| 109 |
raise ValueError("cannot drop the only option")
|
| 110 |
idx.remove(label_idx)
|
| 111 |
label_idx = None
|
| 112 |
+
if perm is not None:
|
| 113 |
+
idx = [i for i in perm if i in idx]
|
| 114 |
+
elif shuffle:
|
| 115 |
(rng or random).shuffle(idx)
|
| 116 |
names_p = [names[i] for i in idx]
|
| 117 |
descs_p = [descs[i] for i in idx]
|
|
|
|
| 200 |
drop_label_for: Optional[Sequence[str]] = None,
|
| 201 |
rng: Optional[random.Random] = None,
|
| 202 |
state_indent: Optional[int] = None,
|
| 203 |
+
option_orders: Optional[Dict[str, Sequence[int]]] = None,
|
| 204 |
) -> Encoded:
|
| 205 |
rng = rng or random.Random()
|
| 206 |
labels = labels or {}
|
| 207 |
drop = set(drop_label_for or [])
|
| 208 |
+
option_orders = option_orders or {}
|
| 209 |
qids = list(questions.keys())
|
| 210 |
if shuffle_questions:
|
| 211 |
rng.shuffle(qids)
|
|
|
|
| 218 |
shuffle=shuffle_options,
|
| 219 |
rng=rng,
|
| 220 |
drop_label=qid in drop,
|
| 221 |
+
perm=option_orders.get(qid),
|
| 222 |
)
|
| 223 |
for qid in qids
|
| 224 |
]
|
model.safetensors
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
size 1505386774
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e5bf18fbd975eb55de3ae8dd328f6e48263fea9d3e47b7cfe52306e98ae5f55a
|
| 3 |
size 1505386774
|
types.py
CHANGED
|
@@ -101,12 +101,17 @@ Answer = Union[NoulAnswer, ChoiceAnswer, ScoreAnswer]
|
|
| 101 |
class Usage(BaseModel):
|
| 102 |
input_tokens: int
|
| 103 |
output_tokens: int = 0
|
|
|
|
| 104 |
|
| 105 |
|
| 106 |
class SystemOneRequest(BaseModel):
|
| 107 |
state: Union[str, Dict[str, Any], List[Any]]
|
| 108 |
model: str = "openthai-systemone"
|
| 109 |
questions: Dict[str, Question] = Field(discriminator=None)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
|
| 111 |
@model_validator(mode="after")
|
| 112 |
def _non_empty(self):
|
|
|
|
| 101 |
class Usage(BaseModel):
|
| 102 |
input_tokens: int
|
| 103 |
output_tokens: int = 0
|
| 104 |
+
permutations: int = 1 # OpenThai extension: number of option orders averaged (order-invariant mode)
|
| 105 |
|
| 106 |
|
| 107 |
class SystemOneRequest(BaseModel):
|
| 108 |
state: Union[str, Dict[str, Any], List[Any]]
|
| 109 |
model: str = "openthai-systemone"
|
| 110 |
questions: Dict[str, Question] = Field(discriminator=None)
|
| 111 |
+
# OpenThai extensions. order_invariant=True averages the answer over several option orders (removes position
|
| 112 |
+
# bias, ~2x latency); None = automatic (on for choice questions with > 10 options); permutations overrides the count.
|
| 113 |
+
order_invariant: Optional[bool] = None
|
| 114 |
+
permutations: Optional[int] = Field(default=None, ge=1, le=32)
|
| 115 |
|
| 116 |
@model_validator(mode="after")
|
| 117 |
def _non_empty(self):
|