Zadání znělo jednoduše: vezmi smlouvu z Registru smluv, tak jak ji stahuje a OCRuje Hlídač státu, a zařaď ji do jedné z 39 věcných kategorií. IT hardware, software, stavby, energie, léčiva, dotace, úklid... Podle obsahu, ne podle názvu. A s co nejvyšší přesností vůči ručně anotovanému golden datasetu.

Zní to jako učebnicová klasifikace textu. V praxi je to jeden z nejzajímavějších datasetů, na kterých jsem kdy dělal. České smlouvy jsou dlouhé, OCR je místy rozpadlé ("M I N I S T E R S T V O"), názvy v registru nic neříkají a hranice kategorií jsou mlhavé i pro lidi. A navíc se na tom dalo poctivě porovnat všechno, co se v klasifikaci v roce 2026 nabízí: od dvacet let starého TF-IDF po decision modely, které vyšly před pár týdny.

Názvy smluv skoro nic neříkají
reálné záznamy z golden datasetu
0 Ostatní 0
1 IT hardware 0
2 Software a licence 0
3 IT vývoj 0
4 IT provoz a cloud 0
5 Kyberbezpečnost 0
6 Telekomunikace 0
7 Pozemní stavby 0
8 Dopravní stavby 0
9 Inženýrské sítě 0
10 Projektové služby 0
11 Vozidla a servis 0
12 Přeprava a pošta 0
13 Stroje a přístroje 0
14 Léčiva 0
15 Zdravotnické prostředky 0
16 Zdravotní a sociální péče 0
17 Potraviny a nápoje 0
18 Stravování a ubytování 0
19 Bezpečnost a PO 0
20 Energie, paliva, voda 0
21 Suroviny a chemie 0
22 Zemědělství a zeleň 0
23 Kancelář a nábytek 0
24 Tisk a publikace 0
25 Oděvy a textil 0
26 Odpady 0
27 Úklid a správa objektů 0
28 Finance a pojištění 0
29 Účetnictví a poradenství 0
30 Právní služby 0
31 Nemovitosti 0
32 Věda a výzkum 0
33 Školení a kurzy 0
34 Kultura a sport 0
35 Reklama a marketing 0
36 Administrativa a HR 0
37 Dary a dotace 0
38 Spolupráce 0
Tři smlouvy se stejným názvem "Objednávka" patří do tří různých kategorií (Software, Kancelář, Energie). Podle názvu to nepoznáte, musíte do textu přílohy. Přesně proto dostával každý model na vstup metadata i text smlouvy.

Výsledek v kostce

Nejdřív to, co vás nejspíš zajímá nejvíc: jak si proti sobě vedly jednotlivé přístupy. Nejlepší řešení z každé rodiny, všechno na stejných 1 754 validačních smlouvách, které žádný model při tréninku neviděl:

Zero-shot encoderyGLiClass, podobnost s popisy kategorií
79.4 %
TF-IDF + lineární modelbaseline, CPU, milisekundy
83.6 %
Malý fine-tuned encodermmBERT-base, 307M parametrů
88.8 %
Embedding + lineární hlavaQwen3-Embedding-8B, harrier-27B (zmražené)
91.6 %
Decision modely bez tréninkuJev 1.13 (uzavřený); nejlepší open pplx-decider 90.8 %
92.1 %
Velký fine-tuned encoderQwen3-Embedding-8B + LoRA, open-weight
92.8 %
Kombinace dvou modelůJev + Gemini Embedding 2; nejlepší open pplx + TF-IDF 92.9 %
94.0 %
70 %95 %

Ve zkratce:

  • TF-IDF je překvapivě silný základ. 83.6 % zadarmo na CPU je laťka, kterou musí každé AI řešení přeskočit.
  • Decision modely jsou nejlepší start bez dat. Jev bez jediného trénovacího příkladu 92.1 %. Open-weight konkurence (pplx-decider, Clef) je 1 až 3 body za ním.
  • Velký zmražený embedding s obyčejnou lineární hlavou je skoro stejně dobrý a stojí haléře.
  • Malý dotrénovaný encoder nestačil, velký ano. mmBERT (307M) skončil na 88.8 %, Qwen3-Embedding-8B s LoRA na 92.8 %.
  • Nejvyšší čísla dávají kombinace modelu, který rozumí textu, a modelu, který zná konvence anotátorů. Ale jen u slabších modelů.
  • Do provozu nešlo nejpřesnější řešení, ale nejlepší poměr přesnost/rychlost: Qwen3-Embedding-8B s LoRA. Kombinace s 27B decision modelem byla na hardwaru Hlídače víc než 15x pomalejší.

A tady je celá mapa. Každá tečka je jedno řešení, z každého modelu jen nejlepší varianta:

Mapa všech řešení
open-weight (jde provozovat u sebe) uzavřený / jen přes API počítáno na DGX Sparku TF-IDF baseline 83.6 %
Přesnost na stejných 1 754 validačních smlouvách, z každého modelu jen nejlepší varianta. Body pod 70 % jsou přišpendlené k levému okraji. Rozdíly do ~1.3 bodu jsou v šumu (směrodatná chyba je asi 0.7 bodu).

Zbytek článku jsou detaily: jak jsme k těm číslům došli, čemu věřit a čemu ne, a pár věcí, které vás na klasifikaci dlouhých dokumentů nejspíš překvapí.

Jak číst čísla: všechny přesnosti v článku jsou na stejné validační sadě 1 754 smluv (20 % golden datasetu, které žádný model při tréninku ani ladění neviděl). Směrodatná chyba přesnosti je při tomhle počtu asi 0.7 bodu, takže rozdíly kolem jednoho bodu jsou v šumu. Závěry platí pro české smlouvy z Registru smluv a tuhle konkrétní taxonomii, ne pro "klasifikaci textu obecně". Stav k 6. 11. 2026.

O co šlo a na čem to běželo

Hlídač státu je nezávislý projekt, který od roku 2017 zpřístupňuje a analyzuje data o hospodaření státu. Jeho největší datovou sadou je právě Registr smluv: k dnešku přes 9.3 milionu platných smluv, v průměru 3 042 nových denně a jen za rok 2025 rekordních 1 089 475 smluv (statistiky Hlídače). Ručně to nikdo neroztřídí. A bez třídění se v tom špatně hledá, co stát kupuje, od koho a za kolik.

Hlídač státu vede jeho zakladatel Michal Bláha a já jsem jim s klasifikací pomáhal: návrh metodiky, výběr a srovnání modelů, vyhodnocení a doporučení pro provoz. Výpočty běžely na třech místech podle toho, co kde dávalo smysl:

  • Notebook s RTX 4060 (8 GB VRAM): TF-IDF, menší fine-tuningy, lokální embeddingy, vyhodnocení a kombinace.
  • DGX Spark Hlídače: všechno, co se do 8 GB nevejde. 27B embedding model, mmBERT na 8 192 tokenech a LoRA trénink Qwen3-Embedding-8B.
  • OpenRouter: decision modely a embeddingy přes API. Za celý projekt zhruba 40 dolarů.

Data a golden dataset

Každá smlouva jsou dvě volání API Hlídače: metadata z /api/v2/smlouvy/{id} (předmět, plátce, příjemce, hodnota, názvy příloh) a texty příloh z /api/v2/smlouvy/text/{id}. Ten druhý endpoint je důležitý, protože detail smlouvy u anonymizovaných smluv místo textu vrací jen značku. Celkem 543 MB surových dat, z toho jen 33 smluv úplně bez textu.

Jedno rozhodnutí platilo od prvního dne pro všechno: každý model dostává metadata i text. Samotný předmět z registru je často jen "Objednávka č. 123" a oddíl "Předmět smlouvy" najde regulární výraz jen zhruba u 42 % smluv. TF-IDF jen na předmětu dalo 74.2 %, jen na textu 81.4 % a na obojím 83.6 %.

Moje první hypotéza byla, že smlouvy jsou z velké části právní boilerplate ("smluvní strany se dohodly...") společný všem kategoriím a stačí ho vyhodit. Data ji rychle zabila: věty doslova shodné napříč kategoriemi tvoří kolem 1 % slov. Boilerplate je parafrázovaný, ne kopírovaný, a filtr by skoro nic neodřízl.

Co je golden dataset? Sada smluv, u kterých člověk ručně určil správnou kategorii. Slouží jako "pravda", proti které se měří všechny modely.

Golden tady připravil člověk z Michalova týmu, který se dřív profesionálně živil anotací dat pro AI. Díky tomu byly vyřešené základy, na kterých podobné projekty často padají: vzorek smluv není zaujatý k pár velkým úřadům ani k jednomu období, kategorie jsou rozumně vyvážené (i ty nejmenší mají desítky příkladů, ne jednotky) a značení drží jednotná pravidla. Tým Hlídače ho během projektu ještě dočistil: z původních 8 818 smluv odebral 51 a 768 labelů opravil. Zůstalo 8 767 smluv, 27 až 643 na kategorii.

Jak moc je golden pravdivý

Než začnete měřit modely proti "pravdě", měli byste vědět, jak moc je ta pravda pravdivá. Udělali jsme proto audit: přes 190 smluv z golden ve více kolech naslepo klasifikoval Claude Sonnet (bez znalosti golden labelu, jen s popisy 39 kategorií a textem) a neshody pak naslepo posuzoval Claude Opus, který nevěděl, která z nabízených kategorií je ta z golden. Stejným postupem jsme si navíc označili 500 smluv mimo golden pro pozdější kontrolu.

Sonnet se s golden shodl zhruba u devíti smluv z deseti. Zajímavější ale byl zbytek:

  • Opus rozhodoval neshody systematicky proti golden. To není důkaz chyb v datech, ale typický otisk společného biasu dvou modelů stejné rodiny. Verdikt AI arbitra je kandidát na chybu, ne chyba.
  • Skutečných chyb v golden je odhadem jen jednotky procent. Tým Hlídače většinu kandidátů zkontroloval a opravil.
  • Většina neshod nebyla chyba ani modelu, ani golden. Byl to rozdíl mezi tím, co říká popis kategorie, a tím, jak anotátor reálně značí. Když jsme modelu ukázali podobné smlouvy z golden, shoda výrazně vzrostla, protože se naučil konvence.

Moje oblíbená hraniční kauza: lyžáky a školy v přírodě. Popis kategorie "Kultura, sport a volný čas" na ně sedí jako ulitý a všechny chytré modely je tam dávají. Golden ale drtivou většinu takových smluv značí jako "Stravování a ubytování", protože se platí hlavně za pobyt. Ani jedna strana nemá "chybu". Je to rozhodnutí o taxonomii, které musí udělat člověk, ne model.

Protokol: 80:20 a na validaci se nesahá

Tohle je nudná sekce, ale bez ní by zbytek článku nic neznamenal. Pravidla, která platila pro každý z těch 184 běhů:

  • Split 80:20 stratifikovaný podle kategorie, zafixovaný jednou pro všech 8 766 smluv s labelem: 7 012 train, 1 754 validace. Split má kontrolní otisk, který musel sedět na všech strojích.
  • Validace se nepoužívá k výběru. Hyperparametry se ladí křížovou validací na train, varianty promptů se screenují na smlouvách z train (decision modely jsou zero-shot, takže train je pro ně volná dev sada). Na validaci jde každá finální konfigurace jednou.
  • Párové testy. Všechny metody se hodnotí na stejných smlouvách, takže u každého rozdílu jde spočítat McNemarův test. Bez něj byste v tabulce s půlbodovými rozdíly četli horoskop.
  • Doplňková metrika "nový plátce": přesnost jen na smlouvách úřadů, které v tréninku nebyly. Kontrola, jestli se model neučí šablony konkrétních úřadů.

Co je macro-F1? F1 (harmonický průměr precision a recall) spočítané pro každou kategorii zvlášť a pak prostě zprůměrované. Malá kategorie s 8 smlouvami ve validaci váží stejně jako velká se 132. Accuracy vám řekne, kolik smluv model trefí. Macro-F1 vám řekne, jestli neignoruje malé kategorie.

Co přesně jsme testovali

TF-IDF + lineární model

Spočítá, jak typická jsou slova a znakové n-gramy pro dokument, a naučí nad tím LinearSVC. Klasická baseline, běží na CPU v milisekundách.

Decision model

Nová kategorie modelů ("System One"). Dostane text a seznam možností a jedním průchodem vrátí pravděpodobnost každé. Bez generování textu, bez tréninku na našich datech.

Embedding + naučená hlava

Velký zmražený model převede smlouvu na vektor (u Qwen3-8B 4 096 čísel) a nad ním se na 7 012 smlouvách naučí obyčejný lineární klasifikátor.

Fine-tuned encoder

Model se dotrénuje přímo na klasifikaci: celý (mmBERT) nebo přes LoRA adaptér (Qwen3-Embedding-8B). Pattern z mého červencového článku.

Zero-shot encoder

Žádný trénink: smlouva se porovná s textovými popisy kategorií (GLiClass, podobnost embeddingů).

Kombinace

Pravděpodobnosti dvou modelů se spojí vážením logaritmů. Typicky decision model, který rozumí textu, a naučený model, který zná konvence anotátora.

Generativní LLM typu "napiš mi kategorii" jsme do srovnání vědomě nedali. Sonnet a Opus posloužily na audit golden, ale na klasifikaci milionů smluv je to drahá okružní jízda (víc o tom v rozcestníku encoder vs LLM).

Decision modely: 39 pravděpodobností jedním průchodem

Tahle kategorie modelů je pro mě největší novinka roku. Jev od TypeSafe vyšel 15. 9. 2026, open-weight Clef od Cloudflare 1. 10. a pplx-decider od Perplexity zhruba ve stejné době.

Princip je pro klasifikaci ideální. Model dostane smlouvu (metadata + prvních 12 000 znaků), seznam 39 kategorií s krátkými popisy a vrátí rozdělení pravděpodobnosti. Žádný JSON k parsování, žádné "omlouvám se, ale...". A protože vrací pravděpodobnosti, dá se s nimi dál počítat. Proklikejte si dva skutečné běhy Jevu:

Jak decision model rozhoduje (a co udělá zamíchané pořadí)
Jev 1.13 · skutečné běhy na validaci
Smlouva
Pořadí 39 kategorií v promptu

Model dostane smlouvu a seznam 39 kategorií a jedním průchodem vrátí pravděpodobnost každé z nich. Žádný generovaný text. Pozice v seznamu vlevo od pruhu je místo, kde kategorie v promptu skutečně stála.

Na celé validaci (1 754 smluv) změnilo náhodné pořadí odpověď Jevu u 2.1 % smluv, u Clef 27B jen u 0.3 %. Přesnost na pozici správné kategorie nezávisí (χ² p = 0.67 a 0.94), takže klasická preference prvních nebo posledních možností se neprokázala.

Všechny decision modely šly přes jedno API se stejným promptem a stejným vstupem, na celé validaci:

decision model (1 754 smluv)přesnostmacro-F1medián latence (API)cena za validaci
Jev 1.13 uzavřený92.0 %0.8800.37 s0.90 $
Jev 1.14 uzavřený91.8 %0.8770.4 s0.90 $
Mercury Decide uzavřený90.6 %0.8671.13 s0.34 $
GPT-6 Luna Decisions uzavřený90.5 %0.860-1.66 $
pplx-decider v1.2 open89.5 %0.8530.83 s0.34 $
Clef 27B open88.8 %0.8431.11 s0.91 $
Clef-flash 9B open86.6 %0.8150.67 s0.39 $
Clef-omni open76.7 %0.722-3.32 $

Celou validaci projede Jev za necelý dolar. S celým textem smlouvy místo prvních 12 000 znaků dal 92.1 %, tedy v šumu stejně. Má to ale háček: Jev je uzavřený a Hlídač chtěl model, který si provozuje sám. Proto mě nejvíc zajímala open-weight trojice Clef 27B, pplx-decider a jejich kombinace.

  • Jev 1.14 není lepší než 1.13. Shodují se v 98.1 % smluv a rozdíl je v šumu. Nejnovější verze neznamená automaticky lepší na vašich datech.
  • Pravděpodobnosti Jevu jsou použitelné. Při pravděpodobnosti aspoň 0.8 (88 % validačních smluv) má Jev přesnost 96.4 %, pod tím zhruba 61 %. Hotový práh pro to, co poslat člověku na kontrolu.
  • Na promptu záleží i tady. Pro pplx-decider jsme na smlouvách z train porovnali šest variant promptu. Obecné definice kategorií místo výčtu klíčových slov zvedly samotný pplx na validaci z 89.5 na 90.8 % (průkazně, p = 0.005).

A jedna zajímavost k benchmarkům. Na veřejném JevBench mají některé open-weight modely výrazně vyšší "intelligence index" než Jev. Na našich českých smlouvách to skoro nic nepředpovídalo: JevBench je převážně anglický, s krátkými vstupy a malým počtem možností. Benchmark si změřte na svých datech, jinak měříte cizí úlohu.

Dlouhé smlouvy: kolik textu model uvidí a co s tím dělat

Tohle je sekce, která platí pro jakoukoli klasifikaci dlouhých dokumentů. Smlouvy mají medián kolem 8 000 znaků, ale chvost je extrémní: 90 % smluv má pod 40 000 znaků a nejdelší má 4.7 milionu. Každý model má jiné kontextové okno a ne každý vám dá vědět, když ho přetečete.

Kolik smlouvy model vůbec uvidí
8 766 smluv · logaritmická osa
-smluv se do okna nevejde celých

Medián délky textu je 8 057 znaků, 90 % smluv má pod 39 712 znaků a nejdelší smlouva v golden má 4.7 milionu znaků. Čeština má u Qwen a OpenAI tokenizerů zhruba 2 znaky na token, u bge-m3 a e5 asi 3.2.

Než jsme začali okna u všech modelů systematicky ověřovat, našli jsme tři tichá selhání:

  1. bge-m3 (8 192 tokenů): delší smlouvy API odmítlo a jejich výsledky v logu prostě chyběly. Žádná výjimka, jen díra v datech.
  2. multilingual-e5-large: API ořezávalo na 512 tokenů, tedy asi 1 380 znaků. Žádné varování.
  3. pplx-decider v1.1: katalog uvádí 262k kontextu, ale limit na jedno rozhodnutí podle model card je 8 192 tokenů. Po přeměření v limitu přidal 3 body.

context_length v katalogu poskytovatele není limit, který reálně platí pro váš request. Před během si ověřte okno z model card, změřte počet tokenů na vašich datech (čeština má u Qwen a OpenAI tokenizerů ~2 znaky na token, u bge a e5 ~3.2) a při porušení limitu přeměřte. Jinak porovnáváte modely, které viděly jiná data.

Strategie pro dlouhé dokumenty: co jsme zkoušeli a co ne

Když se dokument do okna nevejde, máte v zásadě pět možností. Část jsme změřili, část ne. Shrnuté včetně toho, jak to dopadlo:

testovali

Ořez na začátek

Výchozí volba pro všechny modely: metadata + prvních 12 000 znaků (u encoderů podle okna). Funguje nejlépe, protože smlouva skoro vždy začíná stranami a předmětem plnění.

testovali

Začátek + konec

Klasický trik z Sun a kol. (2019): část okna dát začátku, část konci. U nás na TF-IDF poměr 75:25 při 4 000 znacích 83.5 % proti 82.7 % jen ze začátku, ale neprůkazně. Samotný konec je vždy horší, 50:50 taky.

testovali

Delší kontext

Jev s celou smlouvou až do 200 000 znaků, harrier a Qwen na 32k tokenech, mmBERT na 2k, 4k a 8k. Celkově v šumu (Jev 92.0 -> 92.1 %, mmBERT 88.7 / 88.5 / 88.8 %). Mírně to pomohlo jen u nejdelších smluv a stojí to násobně víc výpočtu.

testovali

Kusy + agregace

Smlouva rozsekaná na kusy po 5 500 znacích, každý zvlášť embedding a pak průměr, maximum nebo první kus, jako v hierarchických transformerech. Výsledek 89.1 až 89.5 %, tedy o kousek hůř než jeden embedding prvních 12 000 znaků (89.7 %).

nepomohlo

Naučený attention pooling

Stejné kusy, ale váhy kusů se model učí sám (multiple instance learning). Doufal jsem, že se naučí ignorovat boilerplate. Výsledek 88.6 %, průkazně hůř. Ten signál v datech skoro není.

netestovali

Výběr relevantní pasáže

Najít oddíl "Předmět smlouvy" nebo klíčové věty a klasifikovat jen je (např. CogLTX). Regulární výraz oddíl najde jen u ~42 % smluv a naučený selektor by potřeboval vlastní anotace. Kandidát na další kolo.

netestovali

Shrnutí LLM před klasifikací

Nechat LLM smlouvu zkrátit na pár vět a klasifikovat shrnutí. Na 9 milionech smluv drahé, pomalé a s rizikem, že shrnutí vynechá přesně to rozhodující. U nás ani nebylo potřeba.

netestovali

Long-document architektury

Modely se sparse attention (Longformer, BigBird a spol., srovnání v Dai a kol. 2022). Kvalitní česká varianta chybí a vzhledem k výsledkům výš od nich nečekám zázrak.

Proč je to u téhle klasifikace tak jednoznačné? O kategorii smlouvy rozhoduje začátek a metadata. Zbytek jsou obchodní podmínky, podpisy, ceníky, výkresy a OCR šum z příloh. Delší vstup modelu nedává víc signálu, jen víc šumu a delší výpočet. U jiného typu dokumentů (posudky, zprávy, kde je podstata uprostřed) to může vyjít úplně jinak, tak si to změřte.

Embeddingy s hlavou: 4 000 smluv ve 3D

Druhý hlavní přístup je nejméně "AI" a nejvíc inženýrský: vezmete velký předtrénovaný embedding model, nic na něm netrénujete, jen z každé smlouvy uděláte vektor a nad vektory naučíte obyčejný LinearSVC. Embeddingy se dají koupit přes API za drobné. Celý golden přes Qwen3-Embedding-8B stál desítky centů.

Jak to vypadá uvnitř? Tady je 4 000 skutečných smluv z golden datasetu ve vektorovém prostoru Qwen3-Embedding-8B, promítnutých z 4 096 rozměrů do 3D:

4 000 smluv v prostoru embeddingů
Načítám 3D mapu…
Každá tečka je jedna skutečná smlouva z golden datasetu. Blízko u sebe jsou smlouvy, které Qwen3-Embedding-8B vidí jako podobné (t-SNE projekce 4 096 rozměrů do 3D). Režim Chyby modelu ukáže jen validační smlouvy: šedá = nejlepší open model (Qwen3-Emb-8B + LinearSVC, 91.6 %) trefil, červená = netrefil. Táhněte myší pro otočení, najetím uvidíte název smlouvy.

Zkuste si přepnout na "Chyby modelu". Červené tečky (smlouvy, kde se nejlepší zmražený open model spletl) nejsou rozházené náhodně. U správně zařazené smlouvy je v průměru 87 % z jejích 10 nejbližších sousedů ze stejné kategorie, u chybné jen 17 %. Chyby leží uvnitř cizích shluků, typicky u "Darů a dotací", "Úklidu a správy objektů", "IT provozu" a zbytkové kategorie "Ostatní". Model se plete hlavně tam, kde smlouva svým obsahem opravdu vypadá jako jiná kategorie.

embedding + hlava (1 754 smluv)přesnostmacro-F1
Qwen3-Embedding-8B + instrukce + LinearSVC open91.6 %0.867
harrier-oss-v1-27b, 32k tokenů open91.6 %0.871
Voyage 4 large + LinearSVC API90.9 %0.866
Gemini Embedding 2 + LogReg API90.2 %0.848
pplx-embed-4B open89.3 %0.841
harrier-oss-v1-0.6b, 32k tokenů open88.3 %0.838
bge-m3 open84.2 %0.785
multilingual-e5-large open82.8 %0.769

Nejlevnější trik celého projektu: instrukce na začátku vstupu. Qwen3-Embedding je trénovaný tak, že na začátek dotazu můžete napsat, k čemu embedding je. Jedna česká věta "Urči kategorii veřejné zakázky / smlouvy z českého Registru smluv podle předmětu plnění (co se dodává, poskytuje nebo financuje)." přidala průkazně přes bod. Anglická verze té samé instrukce dala skoro stejně. Pomáhá samotná přítomnost instrukce, jazyk je jedno. harrier, se kterým mám dobré zkušenosti z češtiny, ve 27B verzi Qwen3-8B vyrovnal.

Co naopak nepomohlo, i když to na papíře vypadalo skvěle:

  • Historie plátce a dodavatele jako rysy. Na train to vypadalo jako zlepšení, na validaci zhoršení. Učebnicový overfit.
  • Popisy kategorií jako další rysy. Křížová validace jim sama dala váhu nula.
  • MLP místo lineární hlavy. Horší u Qwen i Voyage. Nad dobrým embeddingem stačí přímka.
  • kNN místo naučené hlavy. Vždy o 5 až 9 bodů horší.
Co pomohlo a co ne
procentní body přesnosti
pomohlo (průkazně) uškodilo (průkazně) v šumu nešlo otestovat
Průkazné = párový McNemarův test na stejných smlouvách, p < 0.05. U několika běhů na DGX Sparku nemáme predikce po smlouvách, takže je nešlo párově otestovat.

Malý encoder prohrál, velký vyhrál

V červenci jsem napsal rozcestník, kde jsem tvrdil, že na uzavřenou klasifikaci s daty máte sáhnout po dotrénovaném encoderu. Tady mi fine-tuned mmBERT-base nejdřív skončil na 88.8 %, tři body za zmraženým Qwen3-Embedding s obyčejnou hlavou. Chvíli jsem si myslel, že budu psát opravu vlastního článku.

Trénink přitom rozbitý nebyl: na trénovacích smlouvách měl mmBERT přes 98 %, učil se dobře, jen se silně přeučoval. Brzdí ho dvě věci:

  1. Velikost. 307M parametrů proti 8B (Qwen3-Embedding) nebo 27B (harrier, Clef, pplx). Na 39 kategoriích s mlhavými hranicemi a 7 012 příklady se ten rozdíl v "znalosti světa" projeví. Velký model ví, že "drť 4-8 zimní" je posypový materiál, i když to v trénovacích datech nikdy neviděl.
  2. Čeština v tokenizeru. 2 048 tokenů mmBERT je jen ~6 000 znaků. Delší okno ale, jak už víme, nepomohlo.

Řešení bylo dotrénovat velký encoder. Qwen3-Embedding-8B je technicky decoder-only model, ale používá se přesně jako encoder: text dovnitř, vektor ven, žádné generování. Na DGX Sparku jsme na něm natrénovali LoRA (r = 16 na attention i MLP) s lineární hlavou na 39 tříd a vstupem 8 192 tokenů. Dvě epochy trvaly zhruba 29 hodin:

Qwen3-Embedding-8B (1 754 smluv)přesnostmacro-F1nový plátce
zmražený, 8k tokenů + LinearSVC90.9 %0.860-
+ LoRA, 2 epochy92.8 %0.89094.8 %

92.8 % je nejlepší samostatný open-weight model celého projektu. Je lepší než Jev bez tréninku (92.1 %) a jediným modelem dorovná kombinaci pplx-decider + TF-IDF (92.9 %). Červencový závěr "na uzavřenou klasifikaci s daty dotrénuj encoder" tady nakonec platí, jen s jednou opravou: na dlouhých českých dokumentech s 39 mlhavými kategoriemi musí být ten encoder velký. mmBERT má pořád své místo tam, kde potřebujete desítky milisekund na smlouvu nebo běh na CPU.

Proč jsme nezkoušeli RobeCzech nebo XLM-R? Mají okno 512 tokenů a RobeCzech navíc nekomerční licenci. Na smlouvy, kde 96 % dokumentů je delších, je to k ničemu. A EuroBERT ani LFM2.5-Encoder češtinu v seznamu jazyků nemají.

Kombinace: proč 1 + 1 = 94

Nejvyšší čísla v celém projektu dávají dvojice decision model + naučený model. Proč to funguje tak dobře?

  • Decision model rozumí textu a popisům kategorií, ale nezná konvence anotátora (lyžáky do ubytování).
  • Naučený model (TF-IDF nebo embedding s hlavou) viděl 7 012 smluv z golden a ty konvence zná, jen hůř zvládá nové formulace.
  • Chybují na jiných smlouvách. U Jevu a TF-IDF je teoretický strop "aspoň jeden má pravdu" 95.7 %.

Spojení je jednoduché: vážený součet logaritmů pravděpodobností. Jediný parametr je váha w. Tady je skutečná křivka z validace, posuňte si ji:

Kolik věřit decision modelu a kolik TF-IDF
skóre = w · log pdecision + (1 - w) · log pTF-IDF
Křivka je spočítaná na celé validaci, takže její vrchol je mírně optimistický (váhu jsme si "vybrali" na stejných datech). Poctivé číslo je to s vahou zvolenou mimo hodnocené smlouvy, v grafu jako tečka.

Všimněte si, jak málo stačí. Samotný pplx-decider má 90.8 %, samotné TF-IDF 83.6 %, a už s váhou kolem 0.2 (tedy "věř hlavně TF-IDF, ale nech decision model rozhodovat, když si je jistý") je kombinace na 92.9 %. Pravděpodobnosti TF-IDF jsou totiž hodně ploché a i malá váha decision modelu je dokáže přetlačit.

nejlepší kombinace (1 754 smluv)přesnostmacro-F1
Jev 1.13 (dlouhý text) + Gemini Embedding 2 uzavřené94.0 %0.906
Jev 1.13 + TF-IDF uzavřené93.6 %0.899
Mercury Decide + Gemini Embedding 2 uzavřené93.3 %0.894
pplx-decider v1.2 + TF-IDF open92.9 %-
Clef 27B + Qwen3-Embedding-8B open91.9 %0.878
Clef 27B + TF-IDF open91.8 %0.875

Poctivá poznámka k těm 94.0 %: vítěz byl vybraný z ~184 běhů na stejné validaci, takže je mírně optimistický. Váha kombinace se volila 5-fold křížovou validací uvnitř validace, takže žádná smlouva nebyla hodnocená váhou vyladěnou na ní samotné. Výběr nejlepšího z mnoha ale tenhle trik neřeší. Třetí model do kombinace jsme mimochodem nedávali: přidával 0 až 0.6 bodu za další závislost v produkci.

A lekce, která přišla až na konci: kombinace pomáhá slabším modelům, ne silným. Decision modelům bez tréninku TF-IDF přidá 1.2 až 3.5 bodu, protože jim dodá konvence anotátora. Dotrénovaný Qwen s LoRA ale ty konvence už zná. Jeho kombinace s TF-IDF vyšla na 92.5 %, tedy o 0.3 bodu hůř než Qwen samotný.

Poziční bias: díky research oddělení Seznamu

Když jsem průběžný report sdílel dál, ozvali se mi lidé z research oddělení Seznamu s pár připomínkami k metodice. Ruku na srdce: proti tomu, jak měří lidi z pořádného ML výzkumu, jsem metodická bačkora. Ale bačkora, která má párové testy a fixní split, takže se dalo rychle ověřit, co z připomínek čísla posouvá a co ne. Ve všem podstatném měli pravdu. A jednu věc jsem vůbec netestoval: poziční bias.

Co je poziční bias? Tendence modelu preferovat možnosti podle jejich pozice v seznamu (typicky první nebo poslední), ne podle obsahu. U modelů, které vybírají z 39 možností, je to legitimní obava. Do té doby jsme všem modelům dávali kategorie ve stejném pevném pořadí: podle ID, tematicky seskupené, "Ostatní" na konci.

Test: celá validace znovu, ale v dalších dvou bězích dostala každá smlouva vlastní náhodné pořadí všech 39 kategorií. Díky tomu je pozice nezávislá na kategorii a dá se měřit přímo. Pustili jsme to na Jev 1.13 a Clef 27B, stálo to 5.5 $:

model (1 754 smluv)pevné pořadínáhodné 1 / 2změněné odpovědizávislost na pozici
Jev 1.1392.1 %91.6 / 91.4 %2.1 %χ² p = 0.67
Clef 27B88.8 %88.9 / 88.9 %0.3 %χ² p = 0.94

Klasická preference prvních nebo posledních možností se neprokázala u žádného modelu. Přesnost nezávisí na tom, kde v seznamu stála správná kategorie, a podíl predikcí v každém pásmu pozic odpovídá jeho velikosti. Clef je na pořadí prakticky necitlivý. Jev mění odpověď asi u 2 % smluv a s náhodným pořadím vychází o 0.5 až 0.7 bodu hůř (jednou průkazně, p = 0.03).

To zajímavé je kde se Jev mění. Viděli jste to výš na eRecept: hraniční smlouvy mezi IT provozem a IT vývojem, kde je model na 52:47 a pořadí ho přehoupne. Pevné tematické pořadí (IT u sebe, stavby u sebe) dává modelu strukturu, kterou náhodné pořadí rozbije. Průměr přes dvě náhodná pořadí rozdíl zmenší, ale pevné pořadí nepřekoná a stojí dvojnásobek volání. Doporučení: pevné tematické pořadí, "Ostatní" na konci. A reportovaná čísla platí.

Research oddělení Seznamu, díky. Bez vás bych tohle číslo nikdy neměl a teď ho mám černé na bílém. Stejný test jsem pak rovnou přidal do zadání pro každý další decision model.

Kde model chybuje: taxonomie, ne text

Analýza chyb nejlepšího modelu potvrdila to, co naznačil už audit golden. Nejslabší jsou málo četné a mlhavé kategorie ("Ostatní", "Věda a výzkum", "IT provoz"), a stejný vzorec má i Jev, který se na našich datech nikdy netrénoval. Hlavní příčinou tedy není nevyvážený trénink, ale hranice kategorií.

Nejlepší příklad: kategorie 2 (Software a licence), 4 (IT provoz, podpora a cloud) a 5 (Kybernetická bezpečnost) třídí každá podle jiného hlediska. Dvojka podle toho, co se kupuje, čtyřka podle činnosti, pětka podle účelu. "Podpora bezpečnostního produktu" je podpora výrobce (2), podpora systému (4) i bezpečnostní technologie (5) zároveň a definice neříkají, které hledisko má přednost. Golden pak takové smlouvy dává jednou sem, jednou tam a model se to nemá podle čeho naučit.

Tohle je strop, přes který žádný model nepřeskočí. Další body už nepřinese větší model, ale jasnější taxonomie: rozhodnout, jestli má přednost forma (dotace, spolupráce), nebo obor, a podle toho sjednotit popisy i anotace. Hlídač teď kategorie zjednodušuje a popisky zpřesňuje. Pozor ale při srovnávání: model na nové taxonomii řeší jinou úlohu, takže jeho čísla nejdou porovnávat s tabulkami v tomhle článku.

Jak to dopadlo v provozu

Na papíře byla nejlepší open-weight volba kombinace pplx-decider + TF-IDF. Na hardwaru, kde to má běžet, se ale ukázalo, co žádná tabulka přesnosti neukáže: 27B decision model, který přes API odpovídá za 0.83 s, potřeboval na DGX Sparku 9 až 15 sekund na smlouvu a 49 GB paměti. To je pár tisíc smluv denně, tedy roky na celý registr. Kvantizace na 4 bity paměť snížila, ale přesnost spadla a rychlejší to nebylo.

Do provozu proto šel Qwen3-Embedding-8B s LoRA, sám, bez TF-IDF. Je víc než třikrát menší, nic negeneruje, jen jednou projde vstup, a ve vLLM zvládne smlouvu za zhruba půl až tři čtvrtě sekundy na jedné GPU. Přesnost se proti pplx + TF-IDF prakticky nezměnila (92.8 proti 92.9 %), rychlost o řád.

Hlídač teď klasifikuje celý archiv na čtyřech GPU. Podle Michalova odhadu bude celý registr zklasifikovaný zhruba za deset dní.

Co si z toho odnést

  1. Začněte TF-IDF. 83.6 % zadarmo na CPU je laťka, kterou musí každé AI řešení přeskočit.
  2. Decision modely jsou nejlepší start bez dat. Jev bez jediného trénovacího příkladu 92.0 % za necelý dolar na 1 754 smluv.
  3. Velký zmražený embedding + lineární hlava je nejlepší poměr cena/výkon pro rychlý start. Qwen3-Embedding-8B s jednou větou instrukce: 91.6 %.
  4. Když máte data, dotrénujte velký encoder. Qwen3-Embedding-8B s LoRA: 92.8 % jedním modelem, bez kombinací, pod sekundu na smlouvu na jedné GPU.
  5. Kombinujte dva různé druhy znalostí, ale jen u slabších modelů. Decision modelu TF-IDF přidá 1.2 až 3.5 bodu. Dotrénovanému Qwenu ubere 0.3.
  6. U dlouhých dokumentů nejdřív zkuste obyčejný ořez na začátek. Ověřte reálná kontextová okna a změřte, jestli delší vstup vůbec pomáhá. U nás nepomohl.
  7. Rychlost změřte na hardwaru, kde to poběží. 0.83 s přes API se na vlastním GPU změnilo na 12 s.
  8. Nevěřte slepě ani golden datasetu, ani AI arbitrovi. Největší strop není model, je to taxonomie.
  9. Párové testy, nebo horoskop. Na 1 754 smlouvách je ±0.7 bodu šum. Víc než polovina rozdílů v grafu "co pomohlo" je v šumu.
  10. Nechte si to zkontrolovat někým, kdo měří líp než vy. Já jsem měl research oddělení Seznamu.

Z "máme Excel s 8 818 smlouvami" jsme se dostali k modelu, který trefí 92.8 % smluv, běží na běžné GPU a celý registr zklasifikuje zhruba za deset dní. Nejpřesnější kombinace z tabulky do provozu nešla, protože byla na reálném hardwaru víc než 15x pomalejší. Nevyhrál jeden zázračný model. Vyhrálo to, že jsme poctivě změřili 184 nezázračných, věděli, kterým číslům věřit, a nakonec je změřili i tam, kde to má běžet.

Velký dík Michalovi Bláhovi a týmu Hlídače státu za data, přístup k API, dočištěný golden a výpočetní kapacitu, a research oddělení Seznamu za připomínky, které z reportu udělaly lepší výzkum. Pokud vás zajímá teorie za tím, mám dva související rozcestníky: Encoder, nebo LLM? a Vyhledávání pomocí vektorů v roce 2026. Předchůdce téhle práce je mimochodem diplomka Jakuba Marouška Automatická klasifikace smluv pro portál HlidacSmluv.cz z roku 2020. Najdete mě na LinkedIn nebo na emailu.