kobkrit commited on
Commit
ff94fa4
Β·
verified Β·
1 Parent(s): e1ef045

v0.3: +5k SFT steps with weak-spot data (177k real + 78k targeted synthetic), recalibrated (public macro 63.2 -> 74.3)

Browse files
Files changed (4) hide show
  1. README.md +38 -31
  2. formatting.py +8 -1
  3. model.safetensors +1 -1
  4. 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 | 61.6 | 81.2 | 80.4 | 0.217 |
120
- | boolq | noul | 300 | 64.7 | 86.0 | 89.7 | 0.213 |
121
- | civil_comments | noul | 300 | **79.0** | 70.3 | 81.0 | 0.114 |
122
- | helpsteer2 | score | 250 | **41.6** | 39.0 | 34.1 | 0.400 |
123
- | massive-de-DE | choice | 350 | 67.4 | 83.4 | 86.9 | 0.169 |
124
- | massive-en-US | choice | 350 | 79.1 | 86.9 | 87.4 | 0.101 |
125
- | multinli | choice | 299 | **87.3** | 85.3 | 82.9 | 0.016 |
126
- | paws | noul | 250 | 68.0 | 82.8 | 89.2 | 0.172 |
127
- | pubmedqa | choice | 250 | 56.4 | 75.6 | 77.2 | 0.188 |
128
- | squad2 | noul | 299 | 50.2 | 80.6 | 82.9 | 0.473 |
129
- | summeval-consistency | score | 144 | **84.0** | 75.7 | 81.2 | 0.083 |
130
- | summeval-relevance | score | 240 | 14.2 | 49.2 | 35.0 | 0.810 |
131
- | vitaminc-dev | choice | 599 | 68.6 | 76.6 | 80.1 | 0.124 |
132
- | **macro average** | | | **63.2** | 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,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 | **88.6** | F1 0.862 | 0.049 | eval split |
145
- | Prachathai67k topics | choice | 3501 | **97.6** | F1 0.913 | 0.006 | eval split |
146
  | Prachathai67k topics | noul | 13119 | **94.2** | | 0.008 | eval split |
147
- | XNLI-th | choice | 2490 | **76.9** | F1 0.770 | 0.037 | eval split |
148
- | XNLI-th (entailment yes/no) | noul | 2490 | **85.0** | | 0.040 | eval split |
149
- | SIB-200 Thai topic (7-way) | choice | 204 | **74.0** | F1 0.708 | 0.098 | whole dataset held out (v0.1: 77.5) |
150
- | Thai contrastive pairs (one-fact flips) | choice | 296 | **79.7** | F1 0.712 | 0.109 | synthetic, eval-only |
151
- | Thai contrastive pairs | score | 56 | **76.8** | MAE 0.42 | 0.137 | synthetic, eval-only |
152
- | Thai contrastive pairs | noul | 248 | **82.7** | | 0.094 | synthetic, eval-only |
153
- | Wongnai review stars (1–5) | score | 6203 | **63.2** | MAE 0.44 | 0.062 | eval split |
154
- | Wisesight sentiment (4-class) | choice | 2671 | 51.5 | F1 0.448 | 0.322 | whole dataset held out β€” v0.1 was 38.7; the v0.2 Thai sentiment set lifted it to 51.5 (still the weakest Thai set) |
155
- | banking77 intent (77-way, English) | choice | 3076 | 37.8 | F1 0.341 | 0.177 | whole dataset held out β€” weak on fine-grained 77-way intents (v0.1: 32.7) |
156
- | xLAM tool selection (English) | choice | 884 | **99.4** | F1 0.986 | 0.004 | 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.2 learned temperatures: choice 1.055, noul 1.047, score 1.000 (the before/after table below was measured on v0.1;
164
- the v0.2 procedure is identical).
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.2 known weak spots (numbers above): extractive-QA style yes/no (squad2 at chance, boolq 65, pubmedqa
199
- 56), summary *relevance* scoring (14), fine-grained 77-way English intents (banking77
200
- 38) and Thai social sentiment (wisesight 51, up from 38.7). Targeted data for each is in progress for v0.3.
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 shuffle:
 
 
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:459965a55d29b7fd4efc0dc671165c373c65c67720e32e617285bf02de9e2ad6
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):