Attention sama o sobě nezná pořadí. Kdybys tokeny zamíchal, spočítá totéž, pro ni je vstup množina, ne posloupnost. Informaci o pozici tam proto musíš dodat zvlášť a způsob, jakým se to dělá, přímo rozhoduje o tom, jak dlouhý kontext model zvládne a jestli se dá dodatečně prodloužit.
Tři generace řešení
1. Sinusoidální (2017). Ke každému embeddingu se přičte vektor spočítaný ze sinů a kosinů o různých frekvencích. Elegantní, deterministické, ale model se vzdálenosti učí jen nepřímo.
2. Naučené absolutní (GPT-2 a spol.). Prostě tabulka „pozice → vektor“, která se natrénuje. Funguje dobře, ale má zásadní vadu: pro pozice mimo trénovací délku neexistuje řádek. Model s naučenými pozicemi na 2 048 tokenů se nedá jednoduše roztáhnout na 8 000.
3. RoPE, rotační poziční kódování (dnešní standard). Pozice se nepřičítá; místo toho se vektory Q a K pootočí o úhel úměrný pozici tokenu.
Proč je RoPE tak dobré
Skalární součin dvou pootočených vektorů závisí jen na rozdílu jejich úhlů. Když je jeden token na pozici 100 a druhý na 105, výsledek je stejný, jako by byly na 1000 a 1005.
Model tak z konstrukce vnímá relativní vzdálenost, ne absolutní pozici. To je přesně to, co u jazyka chceš: záleží na tom, že podmět je tři slova před slovesem, ne že je zrovna sedmnáctý v pořadí.
Dva praktické důsledky:
- Nepotřebuješ tabulku, takže není tvrdý strop daný počtem řádků.
- Rotace se aplikuje uvnitř attention na Q a K, takže s residual streamem nic nedělá.
Ověř si, že RoPE opravdu dělá, co se o něm říká
Tvrzení „skalární součin po rotaci závisí jen na rozdílu pozic“ zní jako matematické kouzlo. Je to osm řádků a dá se to zkusit:
▶ Spustitelné. Ulož jako
rope.pya pusťpython3 rope.py. Potřebuješ jen NumPy.
"""RoPE: proč po otočení závisí skalární součin jen na vzdálenosti pozic."""
import numpy as np
rng = np.random.default_rng(0)
hd = 8 # head_dim, musí být sudý
inv = 1.0 / (10000 ** (np.arange(0, hd, 2) / hd)) # frekvence pro každý pár dimenzí
def rope(v, pos):
"""Otoč každý pár sousedních dimenzí o úhel pos * frekvence."""
ang = pos * inv
c, s = np.cos(ang), np.sin(ang)
out = np.empty_like(v)
out[0::2] = v[0::2] * c - v[1::2] * s
out[1::2] = v[0::2] * s + v[1::2] * c
return out
q, k = rng.normal(size=hd), rng.normal(size=hd)
print('stejná dvojice vektorů, různé absolutní pozice se stejným rozdílem:')
for i, j in [(3, 1), (10, 8), (100, 98), (5000, 4998)]:
print(f' q na pozici {i:5d}, k na pozici {j:5d} (rozdíl {i-j}) -> q·k = {rope(q, i) @ rope(k, j):+.6f}')
print('\nrůzné rozdíly:')
for d in (0, 1, 2, 5, 20):
print(f' rozdíl {d:3d} -> q·k = {rope(q, 100 + d) @ rope(k, 100):+.6f}')
print(f'\nbez rotace je q·k = {q @ k:+.6f} a nezávisí na pozici vůbec')
stejná dvojice vektorů, různé absolutní pozice se stejným rozdílem:
q na pozici 3, k na pozici 1 (rozdíl 2) -> q·k = -1.763129
q na pozici 10, k na pozici 8 (rozdíl 2) -> q·k = -1.763129
q na pozici 100, k na pozici 98 (rozdíl 2) -> q·k = -1.763129
q na pozici 5000, k na pozici 4998 (rozdíl 2) -> q·k = -1.763129
různé rozdíly:
rozdíl 0 -> q·k = -1.467987
rozdíl 2 -> q·k = -1.763129
rozdíl 20 -> q·k = -0.930017
bez rotace je q·k = -1.467987 a nezávisí na pozici vůbec
Šest platných číslic stejně, ať jsi na pozici 3 nebo 5 000. Model se tedy neučí „token číslo 98“, učí se „token dvě zpátky“, a to je přesně ten důvod, proč RoPE zvládá kontext, na jaký nebyl trénovaný, líp než naučená tabulka pozic.
Všimni si taky posledního řádku: při rozdílu nula vyjde přesně to samé jako bez rotace. Rotace o nulový úhel je identita, což je hezká kontrola, že jsi to napsal správně.
Prodlužování kontextu
RoPE má parametr theta (často 10 000), který určuje, jak rychle se úhly s pozicí mění. Když
chceš natrénovaný model na 8 k roztáhnout na 128 k, nemůžeš to udělat jen tak, model by se ocitl
v úhlech, jaké nikdy neviděl. Používají se proto tyhle metody:
| Metoda | Princip |
|---|---|
| Position interpolation | Pozice se „stlačí“ do rozsahu, který model zná (z 32 k udělá 8 k). Jednoduché, ale zhoršuje rozlišení blízkých vztahů. |
| NTK-aware scaling | Upraví theta tak, aby se vysoké frekvence (blízké vztahy) zachovaly a natáhly se hlavně nízké. |
| YaRN | Kombinace obojího s korekcí podle frekvenčních pásem; dnes nejběžnější způsob, jak se dělá „long context“ varianta modelu. |
Po roztažení vždycky následuje krátké dotrénování na dlouhých textech, jinak model sice technicky přijme 128 k tokenů, ale kvalita v druhé polovině je mizerná.
⚠️ Když stahuješ „128k“ variantu modelu, ptej se, jestli byla na dlouhém kontextu skutečně dotrénovaná, nebo jen přeškálovaná. Rozdíl v použitelnosti je propastný a projeví se přesně tím, čemu se říká lost in the middle.
ALiBi a další cesty
ALiBi místo rotace přičítá k attention skóre penalizaci úměrnou vzdálenosti, čím dál od sebe tokeny jsou, tím menší váha. Extrapoluje na delší kontext přirozeně a je velmi jednoduché. V praxi ho ale většina velkých modelů opustila ve prospěch RoPE, protože RoPE si vede lépe při dotrénování na dlouhé texty.
Kam dál
Jak pozice vstupuje do výpočtu pozornosti, uvidíš v attention ručně na číslech a v transformeru v kódu. Proč se o délku kontextu bojuje, vysvětluje attention do hloubky a kontextové okno.
Shrnutí
- Attention je bez poziční informace slepá k pořadí; musí se dodat zvlášť.
- RoPE kóduje pozici rotací Q a K, takže model vnímá relativní vzdálenosti a nemá tvrdý strop.
- Prodloužení kontextu = přeškálování RoPE (interpolace, NTK, YaRN) plus dotrénování.
- „128k varianta“ bez dotrénování bývá jen formálně dlouhá.
Proč by transformer bez pozičního kódování považoval „pes kousl muže“ a „muže kousl pes“ za totéž?
Protože attention pracuje s množinou vektorů a sama o sobě neví nic o pořadí, skóre mezi tokeny nezávisí na jejich pozici. Informaci o pořadí je nutné dodat zvlášť, dnes nejčastěji rotací vektorů Q a K podle pozice (RoPE).
Čím je RoPE lepší než naučené absolutní pozice?
Kóduje relativní vzdálenost mezi tokeny místo absolutní pozice a nepotřebuje tabulku s pevným počtem řádků. Díky tomu nemá tvrdý strop délky a dá se přeškálovat na delší kontext, což u naučených pozic prakticky nejde.
Model deklaruje kontext 128k, ale kvalita uprostřed dlouhých dokumentů je špatná. Co se pravděpodobně stalo?
Kontext byl roztažen přeškálováním RoPE bez pořádného dotrénování na dlouhých textech. Model formálně tolik tokenů přijme, ale nenaučil se s takovou vzdáleností pracovat, projeví se to typicky ztrátou informací uprostřed kontextu.
