What it does
Input = structured player facts (name, position, nationality, birth date, height, jersey number,
career clubs with apps/goals/assists, career totals, national-team caps, current availability;
goalkeepers get clean sheets instead of goals). Output = a grounded multi-sentence bio that uses
only the provided facts (no invented clubs/dates/stats), scaling length to the data.
Training (v4)
- Base:
Qwen/Qwen3.5-4B • Method: LoRA (Unsloth SFT via TRL), bf16 + 4-bit, on DGX Spark (GB10).
- LoRA: r = 16, alpha = 16, dropout = 0, no bias; targets
q,k,v,o,gate,up,down_proj (all 32 layers).
- Optim: adamw_8bit, LR 2e-4 linear, warmup 10, seq_len 1536, batch 8, 2 epochs (1750 steps).
- Data:
Livesport/bio-sft-dataset — 7,000 chat examples (cs/en/pt), completion rendered with
enable_thinking=False to match inference.
- Result: mean
train_loss ≈ 0.087 (final steps ≈ 0.04); clean, stable convergence.
Change vs v2: v2 was trained with MLX LoRA (gentle LR 2e-5) on ~4.2k examples and converted to
PEFT. v4 is a fresh Unsloth SFT run (LR 2e-4, 2 epochs, 7k examples) on the DGX Spark.
Eval (held-out valid.jsonl, Claude Sonnet fact-checker)
368 generated (greedy, HF/Unsloth — image vLLM predates qwen3_5; adapter identical to served weights).
- Overall faithful: 356/367 (97.0%), avg score 0.994 (1 judge parse-skip).
- Per-language: cs 94.7% • en 97.2% • pl 97.6% • pt-BR 96.7% • sk 98.7%.
- No name/nationality hallucinations. Model generalizes to pl/sk beyond the trained cs/en/pt.
Known limitations & mitigations
Remaining ~3% unfaithful fall into two classes:
- Secondary-stat number copy (yellow/red cards, per-club apps) — occasional digit errors
(e.g. 50→5). Persists under bf16 too → not a quantization artifact.
- Transfer order / dates — mis-ordered or off-by-one transfer years, aggravated by a
systematic ~1-year offset between career-stint and transfer years in the source data.
Mitigations (in the training repo): verify.py — deterministic post-gen check of
numbers/cards/fees/years (gate via gen_hf.py VERIFY=1, regenerates on mismatch);
restructure.py — one-line-per-stint data format removing cross-row joins (for the next data build).
Serving (vLLM, multi-LoRA)
vllm serve Qwen/Qwen3.5-4B --enable-lora --max-lora-rank 16 \
--lora-modules football-v4=/models/adapters/football-v4
# request: {"model": "football-v4", "messages": [...]}
Prompt = system (bio rules) + user Target language: <cs|en|pt-BR>\n\nStructured player data:\n<fields>.
Internal Livesport model. Not for redistribution.