kmamaroziqov commited on
Commit
386ee7f
·
verified ·
1 Parent(s): 6d77a9b

Release NeuronAI-4B card, benchmark runner, visuals, and non-commercial license

Browse files
.gitattributes CHANGED
@@ -34,3 +34,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  tokenizer.json filter=lfs diff=lfs merge=lfs -text
 
 
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  tokenizer.json filter=lfs diff=lfs merge=lfs -text
37
+ assets/checkpoint_selection.png filter=lfs diff=lfs merge=lfs -text
LICENSE.md ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # NeuronAI-4B License
2
+
3
+ Copyright 2026 NeuronUz.
4
+
5
+ NeuronAI-4B is licensed under the **Creative Commons Attribution-NonCommercial
6
+ 4.0 International Public License (CC BY-NC 4.0)**.
7
+
8
+ - License deed: https://creativecommons.org/licenses/by-nc/4.0/
9
+ - Legal code: https://creativecommons.org/licenses/by-nc/4.0/legalcode.en
10
+
11
+ You may share and adapt the model for non-commercial purposes provided that you
12
+ give appropriate credit, link to the license, and indicate changes. Uses that
13
+ are primarily intended for commercial advantage or monetary compensation are
14
+ not licensed by CC BY-NC 4.0.
15
+
16
+ For a separate commercial license, contact **neuronaiuz@gmail.com**.
17
+
18
+ This notice is a convenience summary and does not replace or modify the CC
19
+ BY-NC 4.0 legal code. Third-party components remain subject to their applicable
20
+ licenses and terms.
README.md CHANGED
@@ -3,168 +3,245 @@ language:
3
  - uz
4
  - en
5
  - ru
 
6
  library_name: transformers
7
  pipeline_tag: text-generation
 
8
  tags:
 
9
  - uzbek
10
- - qwen3_5
11
- - instruct
12
  - translation
13
- base_model: Qwen/Qwen3.5-4B
 
 
 
 
 
 
 
14
  ---
15
 
16
- # NeuronAI 4B (Qwen3.5-4B, Uzbek-adapted)
 
 
 
 
 
17
 
18
- An Uzbek-focused instruct model built on Qwen3.5-4B: continued pretraining on an
19
- Uzbek corpus with an extended Uzbek tokenizer, an instruction-data anneal, then
20
- full-parameter SFT.
21
 
22
- Checkpoint: **step 6565 (epoch 1.25 of a 3-epoch run)**. This is not the last
23
- checkpoint — it is the best one. Benchmarks were run every half epoch and peaked
24
- here; later checkpoints scored the same or slightly lower (see *Checkpoint
25
- selection*).
26
 
27
  ## Quick start
28
 
 
 
 
 
 
 
29
  ```python
30
  import torch
31
  from transformers import AutoModelForCausalLM, AutoTokenizer
32
 
33
- model_id = "kmamaroziqov/qwen3.5-4b"
34
- tok = AutoTokenizer.from_pretrained(model_id)
35
  model = AutoModelForCausalLM.from_pretrained(
36
- model_id, dtype=torch.bfloat16, device_map="auto"
 
 
37
  ).eval()
38
 
39
- messages = [{"role": "user", "content": "O'zbekistonning poytaxti qaysi shahar?"}]
40
- text = tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
41
- inputs = tok(text, return_tensors="pt").to(model.device)
42
-
43
- out = model.generate(**inputs, max_new_tokens=256, do_sample=False)
44
- print(tok.decode(out[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True))
45
- # -> O'zbekistonning poytaxti - Toshkent.
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  ```
47
 
48
- No `eos_token_id` argument is needed: `generation_config.json` lists both
49
- `<|endoftext|>` (248044) and `<|im_end|>` (248046), and the model terminates
50
- assistant turns with `<|im_end|>`.
51
 
52
- ### vLLM
53
 
54
- This is a **hybrid attention/Mamba (GDN)** architecture. vLLM needs three extra
55
- arguments or it fails at load with `assert mamba_block_size is not None`:
 
 
 
 
 
 
 
56
 
57
- ```python
58
- from vllm import LLM, SamplingParams
59
-
60
- llm = LLM(
61
- model="kmamaroziqov/qwen3.5-4b",
62
- dtype="bfloat16",
63
- max_model_len=4096,
64
- mamba_block_size=16, # required
65
- mamba_cache_mode="align", # required
66
- language_model_only=True, # text-only checkpoint
67
- )
68
- params = SamplingParams(temperature=0.0, max_tokens=256,
69
- stop_token_ids=[248044, 248046])
70
  ```
71
 
72
- ### Chat format
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
- ChatML. The template emits a **pre-closed, empty** thinking block
75
- (`<think>\n\n</think>`) — this model is trained in non-thinking mode, so it
76
- answers directly and does not produce reasoning traces.
77
 
 
 
 
78
  ```
79
- <|im_start|>user
80
- {message}<|im_end|>
81
- <|im_start|>assistant
82
- <think>
83
 
84
- </think>
85
 
86
- {reply}<|im_end|>
 
 
87
  ```
88
 
89
- ## Training
90
-
91
- | stage | detail |
92
- |---|---|
93
- | base | Qwen3.5-4B |
94
- | tokenizer | extended for Uzbek |
95
- | continued pretraining | Uzbek corpus |
96
- | anneal | bilingual instruction data |
97
- | SFT | full-parameter, 169,919 rows, seq len 2048, effective batch 32, LR 1e-5 cosine, bf16 |
98
-
99
- The SFT mixture is Uzbek-dominant with English retention data, benchmark-format
100
- tasks, translation pairs, and 151 hand-written identity/social records
101
- (uz/en/ru).
102
-
103
- ## Evaluation
104
-
105
- ### Versus alloma models — alloma's own card protocol, full test sets
106
-
107
- Their prompts, their datasets, single backend (vLLM). alloma runs use the `APST`
108
- apostrophe preprocessing their card requires; this model does not (it was never
109
- trained on it). The harness reproduces alloma's published numbers within ~1.3 on
110
- every metric except news (see *Caveats*).
111
-
112
- | metric | alloma-1B | alloma-3B | **this model** | alloma-8B |
113
- |---|---|---|---|---|
114
- | params | 1.24B | 3.21B | **4.21B** | 8.03B |
115
- | BLEU Uz→En | 16.10 | 24.62 | 29.64 | **30.02** |
116
- | BLEU En→Uz | 8.70 | 14.54 | 14.34 | **15.25** |
117
- | COMET Uz→En | 80.08 | 85.14 | 86.63 | **87.13** |
118
- | COMET En→Uz | 81.20 | 86.80 | **87.89** | 87.79 |
119
- | Sentiment | 58.05 | 81.53 | **89.50** | 79.94 |
120
- | News | 15.85 | 33.78 | **58.34** | 57.77 |
121
- | MMLU (English) | 28.61 | 45.34 | **63.20** | 53.47 |
122
-
123
- Four of seven metrics beat an 8B model; the three losses are all under 1 point.
124
-
125
- ### Internal 8-task suite (own prompts, full sets)
126
-
127
- | task | score | invalid |
128
- |---|---|---|
129
- | uzlib (Uzbek knowledge MCQ) | 0.6040 | 0% |
130
- | tumlu | 0.4357 | 0% |
131
- | MMLU English | 0.6402 | 0% |
132
- | MMLU Uzbek | 0.5667 | 0% |
133
- | news | 0.7979 | 0% |
134
- | sentiment | 0.9615 | 0% |
135
- | FLORES en→uz | BLEU 12.85 (len-ratio 0.961) | — |
136
- | FLORES uz→en | BLEU 29.23 (len-ratio 1.024) | — |
137
- | **weighted** | **0.5162** | |
138
-
139
- ### Checkpoint selection
140
-
141
- Held-out loss and downstream benchmarks **disagree** on this run, so checkpoints
142
- were chosen by benchmark:
143
-
144
- | epoch | eval_loss | weighted score |
145
- |---|---|---|
146
- | 0.75 | **1.430** (minimum) | 0.5081 |
147
- | **1.25** | 1.433 | **0.5162** ← this checkpoint |
148
- | 1.75 | 1.449 | 0.5156 |
149
-
150
- eval_loss bottomed at epoch 0.75 while benchmarks kept improving to 1.25. Picking
151
- by eval_loss would have shipped the weaker model.
152
-
153
- ## Caveats
154
-
155
- - **BLEU understates into-Uzbek quality.** Uzbek is agglutinative, so one correct
156
- meaning has many valid surface forms and n-gram overlap collapses. This model
157
- has the *highest* En→Uz COMET of the models compared while sitting mid-pack on
158
- En→Uz BLEU. Prefer COMET for into-Uzbek.
159
- - **News numbers are not comparable to alloma's published card.** Our replication
160
- ran +5.1 to −7.9 off their published news figures with no consistent direction,
161
- likely an input-truncation or label-matching difference. Model-to-model
162
- comparisons in the table above are still like-for-like.
163
- - **Prompt sensitivity is high on classification.** Swapping prompt formats moved
164
- news accuracy by up to 32 points on some models. Invest in prompt design for
165
- your actual application rather than assuming these numbers transfer.
166
- - **English is traded for Uzbek.** Continued pretraining costs English relative to
167
- the base model; a Base+SFT control scored higher on English MMLU.
168
- - Not safety-tuned or RLHF-aligned. Can produce incorrect or outdated facts.
169
- - No Cyrillic-Uzbek or Russian-instruction coverage in the SFT mixture, so quality
170
- in those settings is unverified.
 
 
 
 
 
 
 
 
 
 
 
 
3
  - uz
4
  - en
5
  - ru
6
+ license: cc-by-nc-4.0
7
  library_name: transformers
8
  pipeline_tag: text-generation
9
+ base_model: Qwen/Qwen3.5-4B
10
  tags:
11
+ - qwen3.5
12
  - uzbek
13
+ - conversational
 
14
  - translation
15
+ - text-generation-inference
16
+ - non-commercial
17
+ datasets:
18
+ - HuggingFaceFW/fineweb-2
19
+ - tahrirchi/uz-books
20
+ - tahrirchi/uz-crawl
21
+ - HuggingFaceFW/fineweb-edu
22
+ - HuggingFaceTB/finemath
23
  ---
24
 
25
+ # NeuronAI-4B
26
+
27
+ **NeuronAI-4B** is an Uzbek-first, bilingual assistant model built from
28
+ Qwen3.5-4B. It combines an Uzbek tokenizer retrofit, continued pretraining,
29
+ annealing, and assistant-only supervised fine-tuning. The published weights are
30
+ fully merged—no LoRA adapter is needed.
31
 
32
+ ![Strict eight-task benchmark comparison](assets/overall_score.png)
 
 
33
 
34
+ > **License:** free for non-commercial use under
35
+ > [CC BY-NC 4.0](https://creativecommons.org/licenses/by-nc/4.0/).
36
+ > Commercial use requires a separate written license. Contact
37
+ > **[neuronaiuz@gmail.com](mailto:neuronaiuz@gmail.com)** to discuss commercial terms.
38
 
39
  ## Quick start
40
 
41
+ Install a recent Transformers build with Qwen3.5 support:
42
+
43
+ ```bash
44
+ pip install -U "transformers>=5.1" accelerate torch
45
+ ```
46
+
47
  ```python
48
  import torch
49
  from transformers import AutoModelForCausalLM, AutoTokenizer
50
 
51
+ model_id = "NeuronUz/NeuronAI-4B"
52
+ tokenizer = AutoTokenizer.from_pretrained(model_id)
53
  model = AutoModelForCausalLM.from_pretrained(
54
+ model_id,
55
+ dtype=torch.bfloat16,
56
+ device_map="auto",
57
  ).eval()
58
 
59
+ messages = [
60
+ {"role": "system", "content": "Siz foydali va aniq AI yordamchisiz."},
61
+ {"role": "user", "content": "Alisher Navoiy haqida qisqacha aytib bering."},
62
+ ]
63
+ inputs = tokenizer.apply_chat_template(
64
+ messages,
65
+ add_generation_prompt=True,
66
+ enable_thinking=False,
67
+ return_tensors="pt",
68
+ return_dict=True,
69
+ ).to(model.device)
70
+
71
+ with torch.inference_mode():
72
+ output = model.generate(**inputs, max_new_tokens=256, do_sample=False)
73
+
74
+ reply = tokenizer.decode(
75
+ output[0, inputs["input_ids"].shape[1]:],
76
+ skip_special_tokens=True,
77
+ ).strip()
78
+ print(reply)
79
  ```
80
 
81
+ The generation metadata already registers `<|im_end|>` and `<|endoftext|>` as
82
+ end-of-sequence tokens.
 
83
 
84
+ ### Serve with vLLM
85
 
86
+ ```bash
87
+ pip install -U vllm
88
+ vllm serve NeuronUz/NeuronAI-4B \
89
+ --dtype bfloat16 \
90
+ --max-model-len 4096 \
91
+ --language-model-only \
92
+ --mamba-block-size 16 \
93
+ --mamba-cache-mode align
94
+ ```
95
 
96
+ ```bash
97
+ curl http://localhost:8000/v1/chat/completions \
98
+ -H "Content-Type: application/json" \
99
+ -d '{"model":"NeuronUz/NeuronAI-4B","messages":[{"role":"user","content":"O‘zbekiston haqida uchta fakt ayting."}]}'
 
 
 
 
 
 
 
 
 
100
  ```
101
 
102
+ ## Benchmarks
103
+
104
+ All four models below were evaluated on the same full eight-task suite with
105
+ greedy vLLM decoding. Classification and multiple-choice tasks use accuracy;
106
+ FLORES+ translation uses COMET. The weighted score is normalized by the 0.95
107
+ sum of the published task weights. All eight NeuronAI-4B tasks completed and
108
+ passed the invalid-output gate.
109
+
110
+ ![Per-task comparison](assets/tasks_comparison.png)
111
+
112
+ | Benchmark | Metric | Weight | **NeuronAI-4B** | Qwen3.5-4B | alloma-3B | alloma-1B |
113
+ | --- | --- | ---: | ---: | ---: | ---: | ---: |
114
+ | UzLiB | accuracy | 0.20 | **61.20%** | 40.30% | 32.08% | 23.32% |
115
+ | TUMLU-Uzbek | accuracy | 0.20 | **45.00%** | 40.43% | 27.71% | 22.00% |
116
+ | FLORES+ en→uz | COMET | 0.15 | **0.8965** | 0.8555 | 0.8673 | 0.7383 |
117
+ | Uzbek news | accuracy | 0.10 | **79.15%** | 67.34% | 13.60% | 25.41% |
118
+ | MMLU English | accuracy | 0.10 | 64.06% | **72.66%** | 38.73% | 21.98% |
119
+ | MMLU Uzbek | accuracy | 0.10 | **57.01%** | 52.58% | 32.74% | 21.11% |
120
+ | FLORES+ uz→en | COMET | 0.05 | **0.8763** | 0.8618 | 0.7954 | 0.7636 |
121
+ | Uzbek sentiment | accuracy | 0.05 | **95.75%** | 84.82% | 38.85% | 79.54% |
122
+ | **Normalized weighted score** | | 1.00 | **0.6724** | 0.5978 | 0.4147 | 0.3661 |
123
+
124
+ Alloma runs used the `APST` apostrophe preprocessing required by their model
125
+ cards; NeuronAI and stock Qwen did not. The raw archived scores and run IDs are
126
+ included in [`benchmark_results.json`](benchmark_results.json).
127
+
128
+ ### Run the benchmarks on your computer
129
+
130
+ The repository includes a portable Alloma-style benchmark runner. It covers
131
+ FLORES+ (both directions), Uzbek sentiment, Uzbek news, MMLU English, MMLU Uzbek,
132
+ and TUMLU-Uzbek.
133
+
134
+ ```bash
135
+ pip install -r https://huggingface.co/NeuronUz/NeuronAI-4B/resolve/main/benchmark-requirements.txt
136
+ wget https://huggingface.co/NeuronUz/NeuronAI-4B/resolve/main/benchmark.py
137
+ python benchmark.py --limit 200 --output quick-results.json
138
+ ```
139
 
140
+ The quick command uses the same seed on 200 examples per dataset. Run all public
141
+ examples and add COMET with:
 
142
 
143
+ ```bash
144
+ pip install unbabel-comet
145
+ python benchmark.py --limit 0 --comet --output full-results.json
146
  ```
 
 
 
 
147
 
148
+ Run one task when you only need a short check:
149
 
150
+ ```bash
151
+ python benchmark.py --tasks mmlu-uz --limit 200 --output mmlu-uz.json
152
+ python benchmark.py --tasks flores --limit 200 --output flores.json
153
  ```
154
 
155
+ `--limit 0` means the full dataset. Only full runs are comparable with the table
156
+ above; 200-example quick runs are sanity checks. COMET downloads the
157
+ `Unbabel/wmt22-comet-da` evaluator and needs additional disk/RAM.
158
+
159
+ ## Why this checkpoint
160
+
161
+ Four retained checkpoints were run through the complete suite. Checkpoint 4749
162
+ (one SFT epoch) produced the best downstream score even though development loss
163
+ was fractionally lower at checkpoint 3564.
164
+
165
+ ![Checkpoint selection](assets/checkpoint_selection.png)
166
+
167
+ | Checkpoint | Epoch | Dev loss | Normalized score |
168
+ | ---: | ---: | ---: | ---: |
169
+ | 1188 | 0.25 | 1.4316 | 0.6643 |
170
+ | 2376 | 0.50 | 1.3910 | 0.6600 |
171
+ | 3564 | 0.75 | **1.3669** | 0.6609 |
172
+ | **4749** | **1.00** | 1.3675 | **0.6724** |
173
+
174
+ ## Uzbek tokenizer efficiency
175
+
176
+ The tokenizer is an in-place Uzbek retrofit rather than a vocabulary extension.
177
+ On a fixed seed-42 sample of 20,000 uz-crawl documents (4.79 million whitespace
178
+ words), it used 2.41 tokens per word versus 3.43 for stock Qwen3.5—a 29.7%
179
+ reduction for the same Uzbek text.
180
+
181
+ ![Uzbek tokenizer fertility](assets/tokenizer_fertility.png)
182
+
183
+ This corpus was part of continued pretraining, so the fertility result is
184
+ in-domain. It measures tokenization efficiency, not model quality or decoding
185
+ speed. The 4B and 2B NeuronAI releases use byte-identical tokenizer files.
186
+
187
+ ## Model lineage and training
188
+
189
+ 1. `Qwen/Qwen3.5-4B`
190
+ 2. Uzbek tokenizer retrofit with embedding re-initialization
191
+ 3. Uzbek continued pretraining
192
+ 4. Instruction-data annealing
193
+ 5. Assistant-only SFT with LoRA r64
194
+ 6. Checkpoint-4749 adapter merged into the base weights
195
+
196
+ | Item | Value |
197
+ | --- | --- |
198
+ | Parameters | 4.206B |
199
+ | Train examples | 152,152 |
200
+ | Grouped dev examples | 1,537 |
201
+ | Train/dev prompt-group overlap | 0 |
202
+ | Sequence length during SFT | 2,048 |
203
+ | SFT epochs | 1 |
204
+ | Effective batch size | 32 |
205
+ | Learning rate | 1e-4, cosine, 3% warmup |
206
+ | LoRA | rank 64, alpha 128, dropout 0.05, all linear layers |
207
+ | Loss | assistant turns only; `<|im_end|>` explicitly supervised |
208
+ | Precision | bf16; selected recurrent parameters retained in fp32 |
209
+
210
+ The mixture is Uzbek-first and includes general assistant conversations,
211
+ translation, Uzbek language and literature, spelling, classification, math,
212
+ and English-retention examples. Training data is not distributed with this
213
+ model repository.
214
+
215
+ ## Intended use
216
+
217
+ Good fits include non-commercial Uzbek research, education, prototyping,
218
+ translation experiments, writing assistance, retrieval-augmented generation,
219
+ and local/offline demonstrations.
220
+
221
+ Commercial deployment, paid products or services, internal business use, and
222
+ other activity primarily intended for commercial advantage require a separate
223
+ license from NeuronUz. Email [neuronaiuz@gmail.com](mailto:neuronaiuz@gmail.com).
224
+
225
+ ## Limitations
226
+
227
+ - This is a public-suite-selected checkpoint. The benchmark results are useful
228
+ for reproducibility and relative comparison, but they are not a locked,
229
+ independent estimate of real-world generalization.
230
+ - Stock Qwen3.5-4B remains stronger on English MMLU in this evaluation.
231
+ - TUMLU-Uzbek is the weakest reported Uzbek task and should not be treated as
232
+ solved at 45% accuracy.
233
+ - The model can hallucinate, repeat biases in its data, or produce unsafe or
234
+ outdated content. It has not been comprehensively safety-evaluated.
235
+ - Do not rely on it without expert review for medical, legal, financial, public
236
+ safety, or other high-stakes decisions.
237
+ - SFT used sequences up to 2,048 tokens; serving at longer inherited context
238
+ lengths has not been validated here. The published inference examples use
239
+ 4,096 tokens.
240
+
241
+ ## License
242
+
243
+ NeuronAI-4B is released under
244
+ [Creative Commons Attribution-NonCommercial 4.0 International](https://creativecommons.org/licenses/by-nc/4.0/).
245
+ You may share and adapt it for non-commercial purposes with attribution. This
246
+ summary does not replace the license text. See [`LICENSE.md`](LICENSE.md) and
247
+ contact [neuronaiuz@gmail.com](mailto:neuronaiuz@gmail.com) for commercial terms.
__pycache__/benchmark.cpython-311.pyc ADDED
Binary file (26.1 kB). View file
 
assets/checkpoint_selection.png ADDED

Git LFS Details

  • SHA256: 520aad86eba7b15e695ba59a16402a0283c5b4092dd586c6d102b0f0ebca308e
  • Pointer size: 131 Bytes
  • Size of remote file: 110 kB
assets/overall_score.png ADDED
assets/tasks_comparison.png ADDED
assets/tokenizer_fertility.png ADDED
benchmark-requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ torch
2
+ transformers>=5.1
3
+ accelerate
4
+ datasets
5
+ sacrebleu
6
+ vllm
7
+
8
+ # Optional, only for `python benchmark.py --comet`:
9
+ # unbabel-comet
benchmark.py ADDED
@@ -0,0 +1,355 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Portable NeuronAI-4B / Alloma-style Uzbek benchmark runner.
3
+
4
+ Examples:
5
+ python benchmark.py --limit 200 --output quick-results.json
6
+ python benchmark.py --limit 0 --comet --output full-results.json
7
+
8
+ `--limit 0` evaluates every example. The default 200-example run is a quick,
9
+ deterministically sampled sanity check and must not be compared with the full
10
+ scores in the model card.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import json
17
+ import re
18
+ from datetime import datetime, timezone
19
+ from pathlib import Path
20
+
21
+ import torch
22
+ from datasets import concatenate_datasets, load_dataset
23
+ from transformers import AutoModelForCausalLM, AutoTokenizer
24
+
25
+
26
+ MODEL_ID = "NeuronUz/NeuronAI-4B"
27
+ LETTERS = "ABCD"
28
+
29
+ TRANSLATION_PROMPTS = {
30
+ "uz-en": (
31
+ "Translate each Uzbek sentence into English.\n\n"
32
+ "1991-yilning 1-sentabrida O'zbekiston mustaqilligini e'lon qildi.\n"
33
+ "-> On 1 September 1991, Uzbekistan declared its independence.\n\n"
34
+ "Tadqiqotchilar yangi usul samaradorligi 47 foizga oshganini aniqladilar.\n"
35
+ "-> Researchers found that the new method improved efficiency by 47 percent.\n\n"
36
+ "{text}\n->"
37
+ ),
38
+ "en-uz": "Translate into Uzbek:\n\n{text}",
39
+ }
40
+
41
+ SENTIMENT_PROMPT = (
42
+ "Given the following Uzbek text, determine the sentiment as either "
43
+ "'Positive' or 'Negative'. Respond with only one label.\n\nText: {text}\n\nLabel:"
44
+ )
45
+
46
+ NEWS_PROMPT = """Classify the given Uzbek news article into one category. Respond with only the category number.
47
+
48
+ 0 - Siyosat
49
+ 1 - Iqtisodiyot
50
+ 2 - Texnologiya
51
+ 3 - Sport
52
+ 4 - Madaniyat
53
+ 5 - Salomatlik
54
+ 6 - Oila va Jamiyat
55
+ 7 - Ta'lim
56
+ 8 - Ekologiya
57
+ 9 - Xorijiy Yangiliklar
58
+
59
+ Article: {text}
60
+
61
+ Answer:"""
62
+
63
+ MCQ_SUFFIX = {
64
+ "uz": "Variantlarni diqqat bilan solishtiring. Javobni A, B, C yoki D harfi bilan boshlang.",
65
+ "en": "Compare the options carefully. Start with the answer letter A, B, C, or D.",
66
+ }
67
+
68
+ MCQ_TASKS = {
69
+ "mmlu-en": ("cais/mmlu", "all", "test", "en"),
70
+ "mmlu-uz": ("murodbek/MMLU-uz", "default", "test", "uz"),
71
+ "tumlu": ("jafarisbarov/TUMLU-mini", "uzbek", "test", "uz"),
72
+ }
73
+
74
+
75
+ def choose_rows(dataset, limit: int, seed: int):
76
+ if limit and len(dataset) > limit:
77
+ return dataset.shuffle(seed=seed).select(range(limit))
78
+ return dataset
79
+
80
+
81
+ def strip_thinking(text: str) -> str:
82
+ return re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL).strip()
83
+
84
+
85
+ class Generator:
86
+ def __init__(self, args: argparse.Namespace):
87
+ self.backend = args.backend
88
+ self.tokenizer = AutoTokenizer.from_pretrained(args.model, trust_remote_code=True)
89
+ self.tokenizer.padding_side = "left"
90
+ if self.tokenizer.pad_token_id is None:
91
+ self.tokenizer.pad_token_id = self.tokenizer.eos_token_id
92
+
93
+ if args.backend == "vllm":
94
+ from vllm import LLM, SamplingParams
95
+
96
+ self.sampling_cls = SamplingParams
97
+ self.model = LLM(
98
+ model=args.model,
99
+ dtype=args.dtype,
100
+ trust_remote_code=True,
101
+ gpu_memory_utilization=args.gpu_memory_utilization,
102
+ max_model_len=args.max_model_len,
103
+ language_model_only=True,
104
+ mamba_block_size=16,
105
+ mamba_cache_mode="align",
106
+ )
107
+ else:
108
+ dtype = torch.bfloat16 if args.dtype == "bfloat16" else torch.float16
109
+ self.model = AutoModelForCausalLM.from_pretrained(
110
+ args.model,
111
+ dtype=dtype,
112
+ device_map="auto",
113
+ trust_remote_code=True,
114
+ ).eval()
115
+
116
+ def render(self, prompt: str) -> str:
117
+ messages = [{"role": "user", "content": prompt}]
118
+ try:
119
+ return self.tokenizer.apply_chat_template(
120
+ messages,
121
+ tokenize=False,
122
+ add_generation_prompt=True,
123
+ enable_thinking=False,
124
+ )
125
+ except TypeError:
126
+ return self.tokenizer.apply_chat_template(
127
+ messages, tokenize=False, add_generation_prompt=True
128
+ )
129
+
130
+ def generate(self, prompts: list[str], max_new_tokens: int, batch_size: int) -> list[str]:
131
+ rendered = [self.render(prompt) for prompt in prompts]
132
+ if self.backend == "vllm":
133
+ params = self.sampling_cls(temperature=0.0, max_tokens=max_new_tokens)
134
+ outputs = self.model.generate(rendered, params)
135
+ return [strip_thinking(item.outputs[0].text) for item in outputs]
136
+
137
+ results: list[str] = []
138
+ for start in range(0, len(rendered), batch_size):
139
+ batch = rendered[start : start + batch_size]
140
+ encoded = self.tokenizer(
141
+ batch,
142
+ return_tensors="pt",
143
+ padding=True,
144
+ truncation=True,
145
+ max_length=4096,
146
+ ).to(self.model.device)
147
+ prompt_width = encoded["input_ids"].shape[1]
148
+ with torch.inference_mode():
149
+ output = self.model.generate(
150
+ **encoded,
151
+ max_new_tokens=max_new_tokens,
152
+ do_sample=False,
153
+ )
154
+ results.extend(
155
+ strip_thinking(text)
156
+ for text in self.tokenizer.batch_decode(
157
+ output[:, prompt_width:], skip_special_tokens=True
158
+ )
159
+ )
160
+ return results
161
+
162
+
163
+ def load_flores(direction: str, limit: int, seed: int) -> list[dict[str, str]]:
164
+ langs = {"uz-en": ("uzn_Latn", "eng_Latn"), "en-uz": ("eng_Latn", "uzn_Latn")}
165
+ src_lang, ref_lang = langs[direction]
166
+ src = concatenate_datasets([
167
+ load_dataset("openlanguagedata/flores_plus", src_lang, split="dev"),
168
+ load_dataset("openlanguagedata/flores_plus", src_lang, split="devtest"),
169
+ ])
170
+ ref = concatenate_datasets([
171
+ load_dataset("openlanguagedata/flores_plus", ref_lang, split="dev"),
172
+ load_dataset("openlanguagedata/flores_plus", ref_lang, split="devtest"),
173
+ ])
174
+ pairs = [
175
+ {"src": src[index]["text"].strip(), "ref": ref[index]["text"].strip()}
176
+ for index in range(min(len(src), len(ref)))
177
+ ]
178
+ if limit and len(pairs) > limit:
179
+ import random
180
+
181
+ random.Random(seed).shuffle(pairs)
182
+ pairs = pairs[:limit]
183
+ return pairs
184
+
185
+
186
+ def score_comet(sources: list[str], hypotheses: list[str], references: list[str]) -> float:
187
+ from comet import download_model, load_from_checkpoint
188
+
189
+ checkpoint = download_model("Unbabel/wmt22-comet-da")
190
+ model = load_from_checkpoint(checkpoint)
191
+ rows = [
192
+ {"src": src, "mt": hypothesis, "ref": reference}
193
+ for src, hypothesis, reference in zip(sources, hypotheses, references, strict=True)
194
+ ]
195
+ return float(model.predict(rows, batch_size=8, gpus=1 if torch.cuda.is_available() else 0).system_score)
196
+
197
+
198
+ def evaluate_flores(generator: Generator, args: argparse.Namespace) -> dict:
199
+ import sacrebleu
200
+
201
+ results = {}
202
+ for direction in ("uz-en", "en-uz"):
203
+ pairs = load_flores(direction, args.limit, args.seed)
204
+ prompts = [TRANSLATION_PROMPTS[direction].format(text=row["src"]) for row in pairs]
205
+ hypotheses = generator.generate(prompts, max_new_tokens=160, batch_size=args.batch_size)
206
+ references = [row["ref"] for row in pairs]
207
+ sources = [row["src"] for row in pairs]
208
+ row = {
209
+ "total": len(pairs),
210
+ "bleu": float(sacrebleu.corpus_bleu(hypotheses, [references]).score),
211
+ "samples": [
212
+ {"source": src, "prediction": hyp, "reference": ref}
213
+ for src, hyp, ref in zip(sources[:3], hypotheses[:3], references[:3])
214
+ ],
215
+ }
216
+ if args.comet:
217
+ row["comet"] = score_comet(sources, hypotheses, references)
218
+ results[direction] = row
219
+ print(f"FLORES+ {direction}: BLEU={row['bleu']:.2f}" + (f", COMET={row['comet']:.4f}" if args.comet else ""))
220
+ return results
221
+
222
+
223
+ def label_to_int(raw, names: list[str]) -> int | None:
224
+ if isinstance(raw, int) and 0 <= raw < len(names):
225
+ return raw
226
+ cleaned = str(raw).strip().casefold().replace("’", "'")
227
+ for index, name in enumerate(names):
228
+ if cleaned == name.casefold():
229
+ return index
230
+ return None
231
+
232
+
233
+ def evaluate_classification(generator: Generator, args: argparse.Namespace, task: str) -> dict:
234
+ if task == "sentiment":
235
+ dataset = load_dataset("behbudiy/uzbek-sentiment-analysis", split="train")
236
+ names = ["Negative", "Positive"]
237
+ rows = [
238
+ {"text": row["text"], "gold": label_to_int(row["label"], names)}
239
+ for row in choose_rows(dataset, args.limit, args.seed)
240
+ ]
241
+ prompt_template = SENTIMENT_PROMPT
242
+ parser = lambda text: 1 if text.casefold().startswith("positive") else (0 if text.casefold().startswith("negative") else None)
243
+ else:
244
+ dataset = load_dataset("risqaliyevds/uzbek-zero-shot-classification", split="train")
245
+ names = ["Siyosat", "Iqtisodiyot", "Texnologiya", "Sport", "Madaniyat",
246
+ "Salomatlik", "Oila va Jamiyat", "Ta'lim", "Ekologiya", "Xorijiy Yangiliklar"]
247
+ rows = [
248
+ {"text": row["text"], "gold": label_to_int(row["class"], names)}
249
+ for row in choose_rows(dataset, args.limit, args.seed)
250
+ ]
251
+ prompt_template = NEWS_PROMPT
252
+ parser = lambda text: int(match.group()) if (match := re.search(r"\d", text)) else None
253
+
254
+ rows = [row for row in rows if row["gold"] is not None]
255
+ prompts = [prompt_template.format(text=row["text"][: args.max_text_chars]) for row in rows]
256
+ outputs = generator.generate(prompts, max_new_tokens=8, batch_size=args.batch_size)
257
+ predictions = [parser(output.strip()) for output in outputs]
258
+ correct = sum(prediction == row["gold"] for prediction, row in zip(predictions, rows, strict=True))
259
+ invalid = sum(prediction is None for prediction in predictions)
260
+ result = {
261
+ "accuracy": correct / len(rows),
262
+ "correct": correct,
263
+ "total": len(rows),
264
+ "invalid_rate": invalid / len(rows),
265
+ }
266
+ print(f"{task}: accuracy={result['accuracy']:.2%} ({correct}/{len(rows)}), invalid={invalid}")
267
+ return result
268
+
269
+
270
+ def answer_letter(raw) -> str | None:
271
+ if isinstance(raw, int) and 0 <= raw < 4:
272
+ return LETTERS[raw]
273
+ cleaned = str(raw).strip().upper()
274
+ return cleaned[0] if cleaned and cleaned[0] in LETTERS else None
275
+
276
+
277
+ def evaluate_mcq(generator: Generator, args: argparse.Namespace, task: str) -> dict:
278
+ dataset_name, config, split, language = MCQ_TASKS[task]
279
+ dataset = choose_rows(load_dataset(dataset_name, config, split=split), args.limit, args.seed)
280
+ rows = []
281
+ for row in dataset:
282
+ choices = row.get("choices") or [row.get(f"option_{letter.lower()}") for letter in LETTERS]
283
+ choices = [str(choice) for choice in choices if choice is not None]
284
+ gold = answer_letter(row.get("answer"))
285
+ if row.get("question") and len(choices) >= 4 and gold:
286
+ rows.append({"question": row["question"], "choices": choices[:4], "gold": gold})
287
+ prompts = []
288
+ for row in rows:
289
+ choices = "\n".join(f"{letter}) {choice}" for letter, choice in zip(LETTERS, row["choices"]))
290
+ prompts.append(f"{row['question']}\n\n{choices}\n\n{MCQ_SUFFIX[language]}")
291
+ outputs = generator.generate(prompts, max_new_tokens=12, batch_size=args.batch_size)
292
+ predictions = []
293
+ for output in outputs:
294
+ match = re.search(r"[ABCD]", output.upper())
295
+ predictions.append(match.group() if match else None)
296
+ correct = sum(prediction == row["gold"] for prediction, row in zip(predictions, rows, strict=True))
297
+ invalid = sum(prediction is None for prediction in predictions)
298
+ result = {
299
+ "accuracy": correct / len(rows),
300
+ "correct": correct,
301
+ "total": len(rows),
302
+ "invalid_rate": invalid / len(rows),
303
+ }
304
+ print(f"{task}: accuracy={result['accuracy']:.2%} ({correct}/{len(rows)}), invalid={invalid}")
305
+ return result
306
+
307
+
308
+ def parse_args() -> argparse.Namespace:
309
+ parser = argparse.ArgumentParser(description=__doc__)
310
+ parser.add_argument("--model", default=MODEL_ID)
311
+ parser.add_argument("--tasks", default="flores,sentiment,news,mmlu-en,mmlu-uz,tumlu")
312
+ parser.add_argument("--backend", choices=["vllm", "transformers"], default="vllm")
313
+ parser.add_argument("--limit", type=int, default=200, help="Examples per dataset; 0 means full dataset.")
314
+ parser.add_argument("--batch-size", type=int, default=16)
315
+ parser.add_argument("--max-text-chars", type=int, default=4000)
316
+ parser.add_argument("--max-model-len", type=int, default=4096)
317
+ parser.add_argument("--gpu-memory-utilization", type=float, default=0.85)
318
+ parser.add_argument("--dtype", choices=["bfloat16", "float16"], default="bfloat16")
319
+ parser.add_argument("--seed", type=int, default=42)
320
+ parser.add_argument("--comet", action="store_true", help="Download WMT22-COMET-DA and score FLORES+.")
321
+ parser.add_argument("--output", type=Path, default=Path("neuronai-4b-benchmark.json"))
322
+ return parser.parse_args()
323
+
324
+
325
+ def main() -> None:
326
+ args = parse_args()
327
+ if args.limit < 0:
328
+ raise ValueError("--limit must be 0 or greater")
329
+ tasks = {task.strip() for task in args.tasks.split(",") if task.strip()}
330
+ unknown = tasks - {"flores", "sentiment", "news", *MCQ_TASKS}
331
+ if unknown:
332
+ raise ValueError(f"Unknown tasks: {sorted(unknown)}")
333
+ generator = Generator(args)
334
+ results = {
335
+ "model": args.model,
336
+ "timestamp": datetime.now(timezone.utc).isoformat(timespec="seconds"),
337
+ "limit_per_dataset": args.limit,
338
+ "seed": args.seed,
339
+ "backend": args.backend,
340
+ "results": {},
341
+ }
342
+ if "flores" in tasks:
343
+ results["results"]["flores"] = evaluate_flores(generator, args)
344
+ for task in ("sentiment", "news"):
345
+ if task in tasks:
346
+ results["results"][task] = evaluate_classification(generator, args, task)
347
+ for task in MCQ_TASKS:
348
+ if task in tasks:
349
+ results["results"][task] = evaluate_mcq(generator, args, task)
350
+ args.output.write_text(json.dumps(results, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
351
+ print(f"Wrote {args.output}")
352
+
353
+
354
+ if __name__ == "__main__":
355
+ main()
benchmark_results.json ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "schema_version": 1,
3
+ "metric_note": "Accuracy for non-translation tasks; COMET for FLORES+; weighted sum normalized by 0.95.",
4
+ "weights": {
5
+ "uzlib_slice_full": 0.2,
6
+ "tumlu_uzbek_full": 0.2,
7
+ "flores_en_uz_full": 0.15,
8
+ "news_full": 0.1,
9
+ "mmlu_english_full": 0.1,
10
+ "mmlu_uz_full": 0.1,
11
+ "flores_uz_en_full": 0.05,
12
+ "sentiment_binary_full": 0.05
13
+ },
14
+ "labels": {
15
+ "uzlib_slice_full": "UzLiB",
16
+ "tumlu_uzbek_full": "TUMLU-Uzbek",
17
+ "flores_en_uz_full": "FLORES+ en to uz",
18
+ "news_full": "News",
19
+ "mmlu_english_full": "MMLU English",
20
+ "mmlu_uz_full": "MMLU Uzbek",
21
+ "flores_uz_en_full": "FLORES+ uz to en",
22
+ "sentiment_binary_full": "Sentiment"
23
+ },
24
+ "models": {
25
+ "NeuronAI-4B": {
26
+ "run_id": "qwen35_4b_lora_r64_grouped_ckpt4749_public",
27
+ "suite_id": "public_uzbek_full_comet_vllm_v2",
28
+ "tasks": {
29
+ "uzlib_slice_full": 0.6120365394948952,
30
+ "tumlu_uzbek_full": 0.45,
31
+ "flores_en_uz_full": 0.8965457885374531,
32
+ "news_full": 0.7914612766835104,
33
+ "mmlu_english_full": 0.6406494801310355,
34
+ "mmlu_uz_full": 0.5700754878222476,
35
+ "flores_uz_en_full": 0.8762970898743587,
36
+ "sentiment_binary_full": 0.9575
37
+ },
38
+ "invalid_rate": {
39
+ "uzlib_slice_full": 0.0,
40
+ "tumlu_uzbek_full": 0.0,
41
+ "flores_en_uz_full": null,
42
+ "news_full": 0.0,
43
+ "mmlu_english_full": 0.0,
44
+ "mmlu_uz_full": 0.0,
45
+ "flores_uz_en_full": null,
46
+ "sentiment_binary_full": 0.0
47
+ },
48
+ "normalized_weighted_score": 0.6724185843547309
49
+ },
50
+ "Qwen3.5-4B": {
51
+ "run_id": "qwen35_4b_instruct_full_20260817_081728",
52
+ "suite_id": "public_uzbek_full_vllm",
53
+ "tasks": {
54
+ "uzlib_slice_full": 0.4030091348737238,
55
+ "tumlu_uzbek_full": 0.4042857142857143,
56
+ "flores_en_uz_full": 0.8554783304304908,
57
+ "news_full": 0.6734350830153656,
58
+ "mmlu_english_full": 0.7266058965959266,
59
+ "mmlu_uz_full": 0.5257798034468024,
60
+ "flores_uz_en_full": 0.8618190287356948,
61
+ "sentiment_binary_full": 0.8482
62
+ },
63
+ "invalid_rate": {
64
+ "uzlib_slice_full": 0.0,
65
+ "tumlu_uzbek_full": 0.0,
66
+ "flores_en_uz_full": null,
67
+ "news_full": 0.0,
68
+ "mmlu_english_full": 0.0,
69
+ "mmlu_uz_full": 0.0,
70
+ "flores_uz_en_full": null,
71
+ "sentiment_binary_full": 0.0145
72
+ },
73
+ "normalized_weighted_score": 0.5977513148832162
74
+ },
75
+ "alloma-3B": {
76
+ "run_id": "alloma_3B_apst_full_20260815_142714",
77
+ "suite_id": "public_uzbek_full_vllm_apst",
78
+ "tasks": {
79
+ "uzlib_slice_full": 0.32079527135948416,
80
+ "tumlu_uzbek_full": 0.27714285714285714,
81
+ "flores_en_uz_full": 0.8673252673692642,
82
+ "news_full": 0.1359802000618748,
83
+ "mmlu_english_full": 0.3872667711152257,
84
+ "mmlu_uz_full": 0.32744623273038026,
85
+ "flores_uz_en_full": 0.7954316840740269,
86
+ "sentiment_binary_full": 0.3885
87
+ },
88
+ "invalid_rate": {
89
+ "uzlib_slice_full": 0.0010746910263299302,
90
+ "tumlu_uzbek_full": 0.28714285714285714,
91
+ "flores_en_uz_full": null,
92
+ "news_full": 7.218727441476746e-05,
93
+ "mmlu_english_full": 0.06900726392251816,
94
+ "mmlu_uz_full": 0.07726819541375872,
95
+ "flores_uz_en_full": null,
96
+ "sentiment_binary_full": 0.4405
97
+ },
98
+ "normalized_weighted_score": 0.41468665305295505
99
+ },
100
+ "alloma-1B": {
101
+ "run_id": "alloma_1B_apst_full_20260811_114037",
102
+ "suite_id": "public_uzbek_full_vllm_apst",
103
+ "tasks": {
104
+ "uzlib_slice_full": 0.23320795271359485,
105
+ "tumlu_uzbek_full": 0.22,
106
+ "flores_en_uz_full": 0.7383331350348018,
107
+ "news_full": 0.2541404558110756,
108
+ "mmlu_english_full": 0.21976926363765845,
109
+ "mmlu_uz_full": 0.21108104258652613,
110
+ "flores_uz_en_full": 0.7636193416348852,
111
+ "sentiment_binary_full": 0.7954
112
+ },
113
+ "invalid_rate": {
114
+ "uzlib_slice_full": 0.04567436861902203,
115
+ "tumlu_uzbek_full": 0.36714285714285716,
116
+ "flores_en_uz_full": null,
117
+ "news_full": 0.0003506239043002991,
118
+ "mmlu_english_full": 0.09393248824953711,
119
+ "mmlu_uz_full": 0.17882068081469876,
120
+ "flores_uz_en_full": null,
121
+ "sentiment_binary_full": 0.0063
122
+ },
123
+ "normalized_weighted_score": 0.3661490569296942
124
+ }
125
+ },
126
+ "checkpoints": {
127
+ "0.25 epoch": {
128
+ "run_id": "qwen35_4b_lora_r64_grouped_ckpt1188_public",
129
+ "suite_id": "public_uzbek_full_comet_vllm_v2",
130
+ "tasks": {
131
+ "uzlib_slice_full": 0.6093498119290703,
132
+ "tumlu_uzbek_full": 0.44571428571428573,
133
+ "flores_en_uz_full": 0.8898104962171044,
134
+ "news_full": 0.772197586882541,
135
+ "mmlu_english_full": 0.6312491098134169,
136
+ "mmlu_uz_full": 0.5549779233727389,
137
+ "flores_uz_en_full": 0.8765152560768583,
138
+ "sentiment_binary_full": 0.9377
139
+ },
140
+ "invalid_rate": {
141
+ "uzlib_slice_full": 0.0,
142
+ "tumlu_uzbek_full": 0.0,
143
+ "flores_en_uz_full": null,
144
+ "news_full": 0.0,
145
+ "mmlu_english_full": 0.0,
146
+ "mmlu_uz_full": 0.0,
147
+ "flores_uz_en_full": null,
148
+ "sentiment_binary_full": 0.0001
149
+ },
150
+ "normalized_weighted_score": 0.6642501250231047
151
+ },
152
+ "0.50 epoch": {
153
+ "run_id": "qwen35_4b_lora_r64_grouped_ckpt2376_public",
154
+ "suite_id": "public_uzbek_full_comet_vllm_v2",
155
+ "tasks": {
156
+ "uzlib_slice_full": 0.6012896292315959,
157
+ "tumlu_uzbek_full": 0.44571428571428573,
158
+ "flores_en_uz_full": 0.8914647964086623,
159
+ "news_full": 0.7503764050737342,
160
+ "mmlu_english_full": 0.621919954422447,
161
+ "mmlu_uz_full": 0.5524854009400371,
162
+ "flores_uz_en_full": 0.8756085765367719,
163
+ "sentiment_binary_full": 0.9524
164
+ },
165
+ "invalid_rate": {
166
+ "uzlib_slice_full": 0.0,
167
+ "tumlu_uzbek_full": 0.0,
168
+ "flores_en_uz_full": null,
169
+ "news_full": 0.0,
170
+ "mmlu_english_full": 0.0,
171
+ "mmlu_uz_full": 0.0,
172
+ "flores_uz_en_full": null,
173
+ "sentiment_binary_full": 0.0
174
+ },
175
+ "normalized_weighted_score": 0.6599990603378274
176
+ },
177
+ "0.75 epoch": {
178
+ "run_id": "qwen35_4b_lora_r64_grouped_seed42_control_public",
179
+ "suite_id": "public_uzbek_full_comet_vllm_v2",
180
+ "tasks": {
181
+ "uzlib_slice_full": 0.6104245029554003,
182
+ "tumlu_uzbek_full": 0.39285714285714285,
183
+ "flores_en_uz_full": 0.8960352256901529,
184
+ "news_full": 0.8070434154893266,
185
+ "mmlu_english_full": 0.640364620424441,
186
+ "mmlu_uz_full": 0.5671556758296539,
187
+ "flores_uz_en_full": 0.8757696719001098,
188
+ "sentiment_binary_full": 0.9514
189
+ },
190
+ "invalid_rate": {
191
+ "uzlib_slice_full": 0.0,
192
+ "tumlu_uzbek_full": 0.0,
193
+ "flores_en_uz_full": null,
194
+ "news_full": 0.0,
195
+ "mmlu_english_full": 0.0,
196
+ "mmlu_uz_full": 0.0,
197
+ "flores_uz_en_full": null,
198
+ "sentiment_binary_full": 0.0
199
+ },
200
+ "normalized_weighted_score": 0.6609225976688203
201
+ },
202
+ "1.00 epoch": {
203
+ "run_id": "qwen35_4b_lora_r64_grouped_ckpt4749_public",
204
+ "suite_id": "public_uzbek_full_comet_vllm_v2",
205
+ "tasks": {
206
+ "uzlib_slice_full": 0.6120365394948952,
207
+ "tumlu_uzbek_full": 0.45,
208
+ "flores_en_uz_full": 0.8965457885374531,
209
+ "news_full": 0.7914612766835104,
210
+ "mmlu_english_full": 0.6406494801310355,
211
+ "mmlu_uz_full": 0.5700754878222476,
212
+ "flores_uz_en_full": 0.8762970898743587,
213
+ "sentiment_binary_full": 0.9575
214
+ },
215
+ "invalid_rate": {
216
+ "uzlib_slice_full": 0.0,
217
+ "tumlu_uzbek_full": 0.0,
218
+ "flores_en_uz_full": null,
219
+ "news_full": 0.0,
220
+ "mmlu_english_full": 0.0,
221
+ "mmlu_uz_full": 0.0,
222
+ "flores_uz_en_full": null,
223
+ "sentiment_binary_full": 0.0
224
+ },
225
+ "normalized_weighted_score": 0.6724185843547309
226
+ }
227
+ },
228
+ "tokenizer_fertility": {
229
+ "NeuronAI-4B": 2.4068250407285743,
230
+ "Qwen3.5-4B": 3.4255944034933097
231
+ },
232
+ "tokenizer_fertility_provenance": {
233
+ "corpus": "uz-crawl",
234
+ "documents": 20000,
235
+ "words": 4792704,
236
+ "seed": 42,
237
+ "note": "NeuronAI-4B and NeuronAI-2B tokenizer.json files are byte-identical (SHA-256 e4731e5d...)."
238
+ }
239
+ }