Vzorkování a řízené dekódování

Z logits na token a jak se formát vynutí maskováním.

Co se naučíš: Uvidíš celou cestu od logits k tokenu a na kódu si ověříš, co dělá temperature a co ořezy.

9 min čteníNavazuje na:🎲 Predikce dalšího tokenu

Model nevydává slova, ale logits, skóre pro každý token ve slovníku. Cesta od nich ke konkrétnímu tokenu je krátká, ale je v ní překvapivě hodně praktické páky. A na jejím konci stojí trik, díky kterému jde formát výstupu vynutit se stoprocentní jistotou, ne o něj jen poprosit.


Od logits k tokenu

logits (jedno číslo pro každý z ~100 000 tokenů)
  │
  ├─ 1. logit bias / penalizace   (ruční úpravy skóre)
  ├─ 2. maskování                 (zakázané tokeny → −∞)
  ├─ 3. dělení temperature        (zplošti / zaostři rozdělení)
  ├─ 4. softmax                   (skóre → pravděpodobnosti)
  ├─ 5. ořez top-k / top-p / min-p
  └─ 6. losování jednoho tokenu

Každý krok je jednoduchý a všechny se dají ovlivnit z API.


Na pořadí záleží. Temperature se aplikuje na logits před softmaxem, ořezy až na hotové pravděpodobnosti. Kdybys to prohodil, temperature by ořez zrušila.


Temperature a ořezy

Temperature dělí logits před softmaxem. Nízká hodnota rozdíly zvětší (model se drží nejpravděpodobnějšího), vysoká je srovná (víc náhody). temperature = 0 znamená prostě vzít maximum.

Top-k nechá jen k nejlepších tokenů. Jednoduché, ale hloupé: u jasného pokračování zbytečně připouští alternativy, u nejisté situace jich naopak odřízne moc.

Top-p (nucleus) bere tokeny, dokud se jejich pravděpodobnosti nesečtou na p. Adaptivní, u jistého pokračování zbude jeden token, u nejistého klidně padesát. Proto vytlačil top-k.

Min-p je novější varianta: nech jen tokeny, které mají aspoň daný zlomek pravděpodobnosti toho nejlepšího. Chová se lépe při vyšších teplotách.

💡 Používej jednu páku. Kombinace temperature + top-p + top-k naráz vede k tomu, že nevíš, co ti co způsobilo.


Zkus si, kolik tokenů vlastně přežije

▶ Spustitelné. Ulož jako vzorkovani.py a pusť python3 vzorkovani.py. Potřebuješ jen NumPy.

"""Od logits k tokenu: co přesně udělá temperature a co ořezy."""
import numpy as np

def softmax(z):
    e = np.exp(z - z.max()); return e / e.sum()

rng = np.random.default_rng(0)
V = 50000
logits = rng.normal(0, 2, V)          # syrová skóre pro celý slovník
logits[[7, 42, 123, 999]] += [9.0, 8.2, 7.5, 6.9]   # pár kandidátů, co dávají smysl

def top_p(logits, temp=1.0, p=0.9):
    probs = softmax(logits / temp)
    order = np.argsort(-probs)                    # od nejpravděpodobnějšího
    cum = np.cumsum(probs[order])
    keep = order[:np.searchsorted(cum, p) + 1]    # nejmenší množina se součtem >= p
    out = np.zeros_like(probs)
    out[keep] = probs[keep] / probs[keep].sum()   # přeškálovat na součet 1
    return out, len(keep)

for temp in (0.2, 0.7, 1.0, 1.5):
    for p in (0.9, 0.95):
        _, n = top_p(logits, temp, p)
        print(f'temperature {temp}  top-p {p}  ->  přežije {n:5d} tokenů z {V}')

print()
probs = softmax(logits)
print(f'bez ořezu: nejlepší token má {probs.max():.3f}, '
      f'ale zbylých {V-1} tokenů dohromady {1-probs.max():.3f}')
print(f'to znamená, že šance na nesmysl je při čistém losování {1-probs.max():.1%}')

Výstup:

temperature 0.2  top-p 0.9  ->  přežije     2 tokenů z 50000
temperature 0.7  top-p 0.9  ->  přežije   106 tokenů z 50000
temperature 0.7  top-p 0.95 ->  přežije   694 tokenů z 50000
temperature 1.0  top-p 0.9  ->  přežije  9424 tokenů z 50000
temperature 1.5  top-p 0.9  ->  přežije 23603 tokenů z 50000

bez ořezu: nejlepší token má 0.109, ale zbylých 49999 tokenů dohromady 0.891
to znamená, že šance na nesmysl je při čistém losování 89.1%

Tady je vidět, proč se ta dvě čísla ladí společně a ne zvlášť. Při temperature 0,2 je top_p úplně jedno, protože po zaostření rozdělení zbydou stejně dva kandidáti. A naopak při temperature 1,5 ti top_p 0,9 pustí přes dvacet tisíc tokenů, takže ořez prakticky nic neořezává.

Poslední dva řádky jsou důvod, proč se vůbec ořezává: i u rozumného rozdělení má ten nejlepší token jen deset procent, zatímco dlouhý ocas nesmyslů má dohromady devadesát. Bez ořezu bys z něj losoval skoro pokaždé.


Penalizace opakování

Tři mechanismy, které často pletou dohromady:

  • presence_penalty: odečte konstantu od logitu tokenu, který se už objevil (jednou a dost).
  • frequency_penalty: odečte tím víc, čím častěji se token objevil.
  • repetition_penalty: logit už použitého tokenu vydělí (nebo vynásobí) konstantou.

Všechny jsou hrubé: nerozlišují mezi „opakuješ se“ a „tohle slovo se prostě v textu vyskytuje často“. Když se model zacyklí, bývá lepší opravit prompt nebo zvýšit teplotu než sahat po penalizacích.


Beam search drží několik nejlepších pokračování a vybere celkově nejpravděpodobnější sekvenci. Pro překlad je to výborné. Pro konverzaci ne: nejpravděpodobnější text je nudný a repetitivní, model sklouzne k obecným frázím a opakování. Proto se u chatu vzorkuje.


Řízené dekódování: jak se formát vynutí

Tohle je nejelegantnější část celé kapitoly. Chceš-li mít jistotu, že výstup bude validní JSON, nepotřebuješ prosbu v promptu, stačí zakázat tokeny, které by ho porušily.

stav: právě jsme napsali {"jmeno": "Ja
povolené pokračování podle JSON gramatiky:
   libovolný znak řetězce, nebo uzavírací uvozovka
zakázané: {  [  ,  :  …
   → logity zakázaných tokenů nastav na −∞
   → model si prostě nemůže vybrat špatně

Funguje to tak, že se schéma (nebo gramatika) převede na konečný automat. V každém kroku automat řekne, které tokeny jsou přípustné, a ostatní se maskují. Výsledek je zaručeně platný podle konstrukce, ne podle štěstí.

Používá se to na:

  • JSON schema: API poskytovatelů dnes běžně nabízí,
  • volání nástrojů: argumenty musí sedět na deklarované schéma,
  • výběr z možností: když chceš jen ANO nebo NE, zakaž všechno ostatní,
  • vlastní gramatiky (regulární výrazy, SQL, DSL) u lokálních modelů.

⚠️ Vynucený tvar není vynucený obsah. Model může vrátit dokonale validní JSON s vymyšleným ID. Kontrola hodnot zůstává na tobě, viz strukturovaný výstup.


Proč ani temperature 0 nedá pokaždé totéž

Několik důvodů dohromady: pořadí operací s plovoucí čárkou na GPU není deterministické, výsledek závisí na tom, s kým se tvůj požadavek sešel v dávce, a poskytovatel může model kdykoli vyměnit za novější build. Ber to jako fakt a testuj strukturu a význam, ne doslovnou shodu.


Shrnutí

  • Z logits se dostaneš k tokenu přes penalizace, maskování, temperature, softmax a ořez.
  • Top-p je adaptivní a v praxi lepší než top-k; kombinuj co nejméně pák naráz.
  • Beam search se u konverzace nepoužívá, protože nejpravděpodobnější text je nudný.
  • Formát se vynucuje maskováním logitů podle gramatiky, je to zaručené konstrukcí, ne prosbou.
Jak API zaručí, že výstup bude validní JSON?

Schéma se převede na konečný automat, který v každém kroku určí, které tokeny jsou přípustné. Logity všech ostatních se nastaví na minus nekonečno, takže si je model nemůže vybrat. Platnost tak vyplývá z konstrukce, ne ze štěstí, hodnoty uvnitř ale správné být nemusí.

Proč se u chatovacích modelů nepoužívá beam search?

Protože hledá nejpravděpodobnější celou sekvenci, a ta bývá nudná a repetitivní, model sklouzne k obecným frázím. U překladu, kde je jedna správná odpověď, dává smysl; u konverzace se proto vzorkuje.

Jaký je rozdíl mezi top-k a top-p?

Top-k vždy nechá pevný počet nejlepších tokenů bez ohledu na situaci. Top-p bere tokeny, dokud jejich pravděpodobnosti nedosáhnou zvoleného podílu, takže se adaptuje: u jistého pokračování nechá jeden token, u nejistého mnoho. Proto se dnes používá spíš top-p.