135 lines
7.3 KiB
Markdown
135 lines
7.3 KiB
Markdown
---
|
||
language:
|
||
- uz
|
||
- en
|
||
license: cc-by-nc-4.0
|
||
datasets:
|
||
- yakhyo/uz-wiki
|
||
- tahrirchi/uz-books-v2
|
||
- tahrirchi/uz-crawl
|
||
- saillab/alpaca_uzbek_taco
|
||
- behbudiy/alpaca-cleaned-uz
|
||
- UAzimov/uzbek-instruct-llm
|
||
- CohereLabs/aya_collection_language_split
|
||
- med-alex/qa_mt_ru_to_uzn
|
||
- med-alex/qa_mt_tr_to_uzn
|
||
library_name: transformers
|
||
pipeline_tag: text-generation
|
||
base_model: Qwen/Qwen3-4B
|
||
tags:
|
||
- uzbek
|
||
- qwen3
|
||
- lora
|
||
- merged
|
||
- sft
|
||
- continued-pretraining
|
||
---
|
||
|
||
# qwen3-4b-uzbek-v2
|
||
|
||
merged bf16 uzbek fine-tune of `Qwen/Qwen3-4B`. standalone — loadable without peft.
|
||
|
||
## usage
|
||
|
||
```python
|
||
from transformers import AutoModelForCausalLM, AutoTokenizer
|
||
import torch
|
||
|
||
tok = AutoTokenizer.from_pretrained("inspirebek/qwen3-4b-uzbek-v2")
|
||
model = AutoModelForCausalLM.from_pretrained(
|
||
"inspirebek/qwen3-4b-uzbek-v2",
|
||
dtype=torch.bfloat16,
|
||
device_map="auto",
|
||
)
|
||
|
||
messages = [{"role": "user", "content": "O‘zbekiston poytaxti qayer?"}]
|
||
inputs = tok.apply_chat_template(messages, return_tensors="pt", add_generation_prompt=True).to(model.device)
|
||
out = model.generate(inputs, max_new_tokens=256, do_sample=True, temperature=0.7)
|
||
print(tok.decode(out[0][inputs.shape[1]:], skip_special_tokens=True))
|
||
```
|
||
|
||
## training
|
||
|
||
a two-stage lora fine-tune of `Qwen/Qwen3-4B`. the hard parts weren't the training loop — they were figuring out why v1 failed, then engineering around a 16-hour compute timeout that couldn't fit the full run.
|
||
|
||
### the v1 lesson: why plain lora collapsed to random
|
||
|
||
v1 scored **26.92%** on mmlu-uz, statistically indistinguishable from 25% random baseline. the adapter was training and loss was descending, but the model learned nothing useful in uzbek. root cause: lora only touched the attention and mlp projections. for a base model with english-dominant pretraining, learning a new language requires **re-mapping the vocabulary itself** — which lives in `embed_tokens` (input embeddings) and `lm_head` (output projection). freezing both means the model can't reshape its token distribution over uzbek morphology, only nudge how it attends within the english-shaped geometry it already has.
|
||
|
||
v2 adds `embed_tokens` and `lm_head` to `target_modules`. this expands the lora to ~2 gb (vs ~200 mb in v1) but finally lets the model actually learn uzbek. result: **mmlu-uz jumped to 40.50%** (+13.58 pp over random).
|
||
|
||
### recipe
|
||
|
||
**lora configuration** (`unsloth` + `peft`):
|
||
- `r=64`, `alpha=128` — alpha = 2·r (more aggressive updates than the conservative alpha=r default)
|
||
- `use_rslora=True` — alpha scales by `alpha/sqrt(r)` instead of `alpha/r`; without this the per-parameter update magnitude at r=64 is too small and training stagnates
|
||
- target modules: `q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj, embed_tokens, lm_head`
|
||
- `use_gradient_checkpointing="unsloth"` to fit the expanded adapter on a single l4
|
||
|
||
**dual learning rate** (via `UnslothTrainer`):
|
||
- base lr for projection layers, `embedding_learning_rate` at 1/10th for `embed_tokens` + `lm_head`
|
||
- embeddings are the highest-leverage layer — a full-lr update can catastrophically drift the model's token geometry in the first few hundred steps
|
||
|
||
**stage a — continued pretraining** on native uzbek text (3 datasets, ~2m rows):
|
||
- `lr=5e-5`, `embedding_lr=5e-6`, 1 epoch, `packing=False`
|
||
- the goal is to reshape the token distribution so the model fluently predicts uzbek sequences, not to learn task behavior
|
||
|
||
**stage b — supervised fine-tune** on chat-formatted uzbek instructions (6 datasets, ~4.3m rows):
|
||
- loaded from the stage a adapter (continues training, not a fresh start)
|
||
- `lr=1e-4`, `embedding_lr=1e-5`, 1 epoch
|
||
- `train_on_responses_only=True`: the loss is masked on the user side of the qwen3 chat template so gradient only flows through assistant responses; prevents the model from memorizing prompt patterns and gives +1–2% accuracy empirically
|
||
|
||
**stable batching**:
|
||
- `per_device_train_batch_size=1`, `gradient_accumulation_steps=16` — effective batch 16
|
||
- at r=64 with embedding lora, batch 2 oom's on 24gb. the accumulation trick keeps gradient signal while staying inside the vram budget
|
||
|
||
**optimization**: `adamw_8bit` (bitsandbytes) + cosine schedule + 3% warmup, weight decay 0.01, seed 3407.
|
||
|
||
### infra: surviving a 16-hour timeout on serverless gpu
|
||
|
||
modal functions have a 16-hour hard cap. stage b doesn't fit in 16 hours on a single l4, and a mid-epoch restart on a fresh container would lose everything (the `/ckpts/` volume persists, but the container that owns the training state doesn't).
|
||
|
||
the fix: a `TrainerCallback` that pushes each checkpoint to a private hugging face repo (`inspirebek/qwen3-4b-uzbek-ckpts`) on every `save_steps` fire. when the container timed out at step 7258 / 8242 (88%), the next run loaded `checkpoint-7000` from the hf backup and resumed — on a different modal account, after the first account's $30 credit ran out. account switches are invisible to huggingface; the backup was the portable state.
|
||
|
||
total stage b runtime: ~10 h first leg + ~3.2 h resume leg = ~13.2 h on a single l4.
|
||
|
||
### evaluation
|
||
|
||
- **mmlu-uz** (murodbek/MMLU-uz, zero-shot, logit-based multiple choice): **40.50%** overall
|
||
- social sciences 45.43%, other 45.07%, business 42.42%, stem 41.67%, medical 39.67%, humanities 35.60%
|
||
- **uzlib** (tahrirchi/uzlib, uzbek linguistic benchmark, sampled generation t=1.0 p=0.95): **33.42%** overall
|
||
- correct_word 34.98%, fill_in 30.77%, meaning 26.69%, meaning_in_context 25.00%
|
||
- 8.17% of responses didn't parse cleanly as `A/B/C/D` — a fraction of the score loss is format drift, not knowledge
|
||
|
||
|
||
## datasets
|
||
|
||
**stage a — fluency (continued pretraining):**
|
||
|
||
- [`yakhyo/uz-wiki`](https://huggingface.co/datasets/yakhyo/uz-wiki) · MIT
|
||
- [`tahrirchi/uz-books-v2`](https://huggingface.co/datasets/tahrirchi/uz-books-v2) · MIT
|
||
- [`tahrirchi/uz-crawl`](https://huggingface.co/datasets/tahrirchi/uz-crawl) · Apache-2.0
|
||
|
||
**stage b — instruct (sft):**
|
||
|
||
- [`saillab/alpaca_uzbek_taco`](https://huggingface.co/datasets/saillab/alpaca_uzbek_taco) · CC-BY-NC-4.0
|
||
- [`behbudiy/alpaca-cleaned-uz`](https://huggingface.co/datasets/behbudiy/alpaca-cleaned-uz) · CC-BY-4.0
|
||
- [`UAzimov/uzbek-instruct-llm`](https://huggingface.co/datasets/UAzimov/uzbek-instruct-llm) · Apache-2.0
|
||
- [`CohereLabs/aya_collection_language_split`](https://huggingface.co/datasets/CohereLabs/aya_collection_language_split) · Apache-2.0
|
||
- [`med-alex/qa_mt_ru_to_uzn`](https://huggingface.co/datasets/med-alex/qa_mt_ru_to_uzn) · unspecified
|
||
- [`med-alex/qa_mt_tr_to_uzn`](https://huggingface.co/datasets/med-alex/qa_mt_tr_to_uzn) · unspecified
|
||
|
||
> ⚠️ licensing note: `saillab/alpaca_uzbek_taco` is cc-by-nc-4.0, which restricts commercial use of derivative models. downstream users who need a fully permissive license should retrain without that subset.
|
||
|
||
## sibling formats
|
||
|
||
- [`inspirebek/qwen3-4b-uzbek-v2`](https://huggingface.co/inspirebek/qwen3-4b-uzbek-v2)
|
||
- [`inspirebek/qwen3-4b-uzbek-v2-lora`](https://huggingface.co/inspirebek/qwen3-4b-uzbek-v2-lora)
|
||
- [`inspirebek/qwen3-4b-uzbek-v2-bnb-4bit`](https://huggingface.co/inspirebek/qwen3-4b-uzbek-v2-bnb-4bit)
|
||
- [`inspirebek/qwen3-4b-uzbek-v2-awq`](https://huggingface.co/inspirebek/qwen3-4b-uzbek-v2-awq)
|
||
- [`inspirebek/qwen3-4b-uzbek-v2-GGUF`](https://huggingface.co/inspirebek/qwen3-4b-uzbek-v2-GGUF)
|
||
|
||
## intended use & limitations
|
||
|
||
uzbek-first chat assistant. capable in english as well. not aligned for safety — treat as a research artifact. knowledge cutoff inherits from `Qwen/Qwen3-4B`.
|