Poziční kódování

Sinusoida → learned → RoPE → prodlužování kontextu.

Co se naučíš: Pochopíš, jak se do modelu dostane pořadí slov, a ověříš si na kódu, proč RoPE funguje.

9 min čteníNavazuje na:✍️ Attention ručně na číslech

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.py a 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:

MetodaPrincip
Position interpolationPozice 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 scalingUpraví theta tak, aby se vysoké frekvence (blízké vztahy) zachovaly a natáhly se hlavně nízké.
YaRNKombinace 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.