aitech-moj-2023/wyk/15_Pozycyjne_zanurzenia.ipynb
2022-07-05 22:23:46 +02:00

497 lines
16 KiB
Plaintext

{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"![Logo 1](https://git.wmi.amu.edu.pl/AITech/Szablon/raw/branch/master/Logotyp_AITech1.jpg)\n",
"<div class=\"alert alert-block alert-info\">\n",
"<h1> Modelowanie języka</h1>\n",
"<h2> 15. <i>Sieci Transformer — pozycyjne zanurzenia</i> [wykład]</h2> \n",
"<h3> Filip Graliński (2022)</h3>\n",
"</div>\n",
"\n",
"![Logo 2](https://git.wmi.amu.edu.pl/AITech/Szablon/raw/branch/master/Logotyp_AITech2.jpg)\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Pozycyjne zanurzenia\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Atencja nie uwzględnia kolejności wyrazów\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"W przeciwieństwie do sieci rekurencyjnych sama atencja nie ma\n",
"naturalnego, wbudowanego pojęcia kolejności, porządku czy „strzałki\n",
"czasu”. Oznacza to, że sieci Transformer w postaci przedstawionej do\n",
"tej pory właściwie operowałyby na tekście jak na worku słów (dowolna\n",
"permutacja tekstu dawałaby identyczny wynik.\n",
"\n",
"Oznacza to, że pozycje wyrazów (tokenów) muszą być w jakiś sposób,\n",
"celowo „wstrzyknięte” do sieci Transformer. Standardowa procedura\n",
"polega na uzupełnieniu zanurzeń o element pozycyjny. Taki element\n",
"sieci neuronowej nazywamy **zanurzeniami (embeddingami) pozycyjnymi**\n",
"(*position(al) embeddings, encodings*).\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Rodzaje zanurzeń pozycyjnych\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Opracowano kilka różnych typów embeddingów pozycyjnych, najczęściej stosowane:\n",
"\n",
"- właściwe zanurzenia pozycyjne zależne wprost od bezwzględnej pozycji tokena\n",
" w zdaniu\n",
" - wyuczalne embeddingi pozycyjne\n",
" - embeddingi sinusoidalne\n",
"- zanurzenia względne,\n",
"- zanurzenia obrotowe.\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Zanurzenia zależne od bezwzględnej pozycji tokena\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Najprostszy sposób uwzględnienia pozycji tokena polega na\n",
"zdefiniowaniu pewnej funkcji $E_p(i) \\colon \\mathcal{N} \\rightarrow\n",
"\\mathcal{R}^m$, to jest zanurzenia zależnego tylko od pozycji tokena\n",
"$i$.\n",
"\n",
"Następnie takie zanurzenie $E^p$ jest po prostu dodawane do standardowego\n",
"embeddingu „semantycznego” $E^s$, by ostatecznie otrzymać embeddingu\n",
"tokena na konkretnej pozycji:\n",
"\n",
"$$E(w_i) = E^p(i) + E^s(w_i).$$\n",
"\n",
"Rzecz jasna rozmiar embeddingu pozycyjnego musi być identyczny z\n",
"rozmiarem zwykłego embeddingu ($m$).\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Wyuczalne embeddingi pozycyjne\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Najprostszy sposób definiowania funkcji $E_p$ to po prostu przypisanie\n",
"każdej pozycji (siłą rzeczy ze skończonego zbioru\n",
"$\\{1,\\dots,p_{\\operatorname{max}}\\}$) osobnego wyuczalnego wektora.\n",
"Przypomina to zwykły embedding wyuczalny dla poszczególnych słów:\n",
"podobnie jak słowa *a*, *aby*, *abrakadabra*,…,/żyzny/ mają swoje\n",
"embeddingi, tak i pozycje będą miały swoje embeddingi (pozycja 1 ma\n",
"swój embedding, pozycja 2 — inny itd., oczywiście nie należy tego\n",
"mylić z embeddingami tokenów złożonych z cyfr *1*, *2* itd.).\n",
"\n",
"Zaletą tego podejścia jest prostota, wady to:\n",
"\n",
"- nie generalizuje się na sekwencje dowolnej długości\n",
" - jeśli zdarzy się zdanie dłuższe niż $p_{\\operatorname{max}}$,\n",
" embeddingu po prostu… nie ma\n",
" - … wprawdzie można po prostu zdefiniować cyklicznie embeddingi\n",
" $E^p_{p{\\operatorname{max}}+i} = E^p(i)$\n",
" - … ma to jednak swoje wady (na pozycji $p{\\operatorname{max}}+1$ sztucznie\n",
" „wracamy” na początek tekstu,\n",
"- sieć musi uczyć się osobno dla sąsiadujących pozycji, na przykład\n",
" embedding pozycji 38 musi zostać wyuczony zupełnie niezależnie od pozycji 39,\n",
"- … co jest szczególnie problematyczne dla pozycji o wyższych numerach pojawiających\n",
" się rzadziej,\n",
"- tego rodzaju embeddingów nie da się stosować relatywnie, a chcielibyśmy, żeby\n",
" atencja z wyrazu na pozycji 14 kierowana na wyraz 12 była w pewnym stopniu podobna\n",
" do atencji z wyrazu 27 kierowanej na wyraz 25.\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Embeddingi sinusoidalne\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Byłoby pożądane, gdyby za pomocą embeddingów pozycyjnych możliwe było wyrażenie pozycji wyrazów\n",
"w różnych **skalach** — zarówno bezpośrednio poprzedzanie/następowanie, jak również\n",
"relację typu „słowo X występuje około 8 słów wcześniej od słowa Y” (i kontrastowania z\n",
"relacją typu „słowo X występuje około 15 słów wcześniej od słowa Y”).\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"##### Analogie z reprezentacją czasu\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Co ciekawe, istnieją analogie między sposobami, w jakie można reprezentować pozycji\n",
"i technikami reprezentacji czasu, wypracowywanymi przez ludzkość (od setek lat!).\n",
"\n",
"Przede wszystkim, jesteśmy przyzwyczajeni do reprezentacji czasu, z\n",
"natury ciągłego, za pomocą serii coraz dłuższych cykli (dotyczy to nie\n",
"tylko kultury zachodniej, lecz także na przykład cywilizacji Majów!).\n",
"\n",
"Na przykład znacznik czasowy dla konkretnej chwili w czasie może mieć postać: `2022-06-13 09:11:23.12`,\n",
"co można by reprezentować za pomocą 7-elementowego wektora:\n",
"\n",
"$$[2022,06,13,09,11,23,12].$$\n",
"\n",
"Dla spójności elementy wektora można by znormalizować względem czasu\n",
"trwania danego cyklu, otrzymamy wówczas (załóżmy, że dla lat nadrzędnym cyklem są stulecia):\n",
"\n",
"$$[0.220,0.500,0.433,0.375,0.183,0.383,0.120]$$\n",
"\n",
"(np. dla godzin $9/24=0,375$).\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"##### Analogowa reprezentacja czasu\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Zauważmy, że powyższa reprezentacja jest „cyfrowa”, nieciągła. Na\n",
"przykład w sylwestra 2022 roku pierwsza pozycja nagle „przeskoczy”\n",
"na 2023. Matematycznie oznacza to nieciągłość, która nie jest pożądana\n",
"z punktu widzenia procesu propagacji wstecznej. Lepiej gdyby wszystkie\n",
"pozycje wektora zmieniają się w sposób ciągły, analogowy, jak\n",
"wskazówki zegara! W zwykłym bowiem zegarze analogowym wszystkie\n",
"wskazówki się cały czas obracają, jedne szybciej, drugie wolniej. Na\n",
"przykład 30 czerwca 2022 roku nie oznaczałby tak naprawdę roku 2022,\n",
"tylko 2022,5.\n",
"\n",
"A zatem właściwa reprezentacja wektorowa przykładowego momentu w czasie powinna mieć raczej postać:\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[0.22537174740226337, 0.5371747402263375, 0.4460968827160494, 0.3829064814814815, 0.18975555555555554, 0.38533333333333336, 0.12]"
]
}
],
"source": [
"def to_analog(tv):\n",
" cycles = [100,12,30,24,60,60,100]\n",
" analog_tv = []\n",
"\n",
" prev = 0.0\n",
"\n",
" for ix in range(len(tv)-1, -1, -1):\n",
" v = tv[ix]/cycles[ix] + prev * (1 / cycles[ix])\n",
" analog_tv.append(v)\n",
" prev = v\n",
"\n",
" analog_tv = analog_tv[-1::-1]\n",
" return analog_tv\n",
"\n",
"tv = to_analog([22,6,13,9,11,23,12])\n",
"tv"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"##### Dodawanie czasu\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Podstawowa operacja dotycząca czasu to przesunięcie momentu czasu o\n",
"pewien okres (wprzód lub w tył), właściwie przypomina to zwykłe dodawanie czy odejmowanie,\n",
"pojawia się jednak pewna trudność.\n",
"Proste dodawanie wektorów nie zawsze da dobry wyniku, np.\n",
"jeśli dodamy do naszego znacznika 20 godzin:\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[2.3148148148148147e-05, 0.0023148148148148147, 0.02777777777777778, 0.8333333333333334, 0.0, 0.0, 0.0]"
]
}
],
"source": [
"delta_v = to_analog([0,0,0,20,0,0,0])\n",
"delta_v"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[0.22539489555041153, 0.5394895550411523, 0.4738746604938272, 1.2162398148148148, 0.18975555555555554, 0.38533333333333336, 0.12]"
]
}
],
"source": [
"def vsum(a, b):\n",
" return [ai+bi for ai, bi in zip(a, b)]\n",
"\n",
"vsum(tv, delta_v)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Problem polega na tym, że na czwartej pozycji pojawiła się wartość\n",
"większa od 1. Powinniśmy zostawić tylko część ułamkową\n",
"(0.2162398148148148). Możemy tak uczynić, ale wtedy oznacza to\n",
"nieciągłość między 0.99 a 1.00 (czyli 0.00 bo obcięciu do części\n",
"ułamkowej). Znowu powracamy do problemu nieciągłości po wypełnieniu\n",
"cyklu (tak naprawdę „rozwiązaliśmy” go tylko dla najdłuższego cyklu).\n",
"Gdybyśmy mogli tylko utożsamić wartość 1 z 0… podobnie jak $2\\pi$ z 0\n",
"dla funkcji trygonometrycznych. Albo gdyby reprezentować czas\n",
"dosłownie za pomocą wskazówek, które po wypełnieniu cyklu w ciągły\n",
"sposób powracają do pozycji początkowej…\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"##### Czas reprezentowany przez wskazówki zegara\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Pozycja wskazówki jest reprezentowana w sposób jednoznaczny przez\n",
"współrzędne końca, dla każdego cyklu możemy wprowadzić dwie wartości\n",
"(znormalizowane) względem długości wskazówki. To znaczy, gdyby\n",
"przyjąć, że długość wskazówki wynosi 1, wówczas współrzędne jej końca mają wartość:\n",
"\n",
"$$x = \\cos\\phi, y = \\sin\\phi,$$\n",
"\n",
"gdzie $phi$ jest kątem wskazówki.\n",
"\n",
"W ten sposób otrzymamy dwa razy dłuższy wektor:\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[0.9379890645739346, 0.34666484497236694, 0.6646342658032391, 0.7471688515457462, 0.7643734166510766, 0.6447738207442666, 0.8245057772081804, 0.5658535352459454, 0.9559058477167625, 0.2936733053937619, 0.8223427069797843, 0.5689925063453478, 0.9822872507286887, 0.1873813145857246]"
]
}
],
"source": [
"import math\n",
"\n",
"def to_hand_coord(atv):\n",
" out = []\n",
" for v in atv:\n",
" phi = v / 2*math.pi\n",
" out.append(math.cos(phi))\n",
" out.append(math.sin(phi))\n",
"\n",
" return out\n",
"\n",
"to_hand_coord(tv)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"##### Obrót jako mnożenie macierzy\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Okazuje się, że obrót w przestrzeni euklidesowej można wyrazić za pomocą mnożenia macierzy,\n",
"na przykład, aby obliczyć współrzędne na płaszczyźnie po obrocie o 90 stopnia, wystarczy przemnożyć\n",
"wektor współrzędnych przez macierz:\n",
"\n",
"\\begin{matrix}\n",
"0 & -1 \\\\\n",
"1 & 0 \\\\\n",
"\\end{matrix}\n",
"\n",
"W ogólnym przypadku obrót o kąt $\\phi$ oznacza przemnożenie przez macierz:\n",
"\n",
"\\begin{matrix}\n",
"\\cos\\phi & -\\sin\\phi \\\\\n",
"\\sin\\phi & \\cos\\phi \\\\\n",
"\\end{matrix}\n",
"\n",
"Jest to bardzo dobra wiadomość! Okazuje się, że „przemieszczanie” się\n",
"w czasie można zrealizować za pomocą mnożenia macierzy — operacji, do\n",
"której stworzono karty graficzne!\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"##### Reprezentacja pozycji tokenów\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Analogicznie do czasu, można reprezentować pozycję wyrazów. Zamiast naturalnych cykli\n",
"lat, miesięcy, dni, itd. musimy tylko narzucić z góry cykle. Jeśli rozmiar\n",
"embeddingu wynosi $m$, musimy wprowadzić $m/2$ cykli (dlaczego?).\n",
"\n",
"Można na przykład wprowadzić narastające geometrycznie cykle o\n",
"długości od 1 do 10000 (10000 to arbitralnie wybrana liczba), tj.\n",
"$k$-ty cykl będzie miał długość $10000^{2k/m}$, wtedy dla parzystej pozycji wektora zanurzeń:\n",
"\n",
"$$E_{p(i)_2k} = \\sin(\\frac{i}{10000^{2k/m}}),$$\n",
"\n",
"dla nieparzystej zaś:\n",
"\n",
"$$E_{p(i)_{2k+1}} = \\sin(\\frac{i}{10000^{2k/m}}),$$\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Zanurzenia względne\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Inne podejście polega na skupieniu się na tym, by Transformer był w\n",
"stanie operować na **względnych** pozycjach. W tym podejściu informacja\n",
"o pozycji nie jest dodawana do zanurzeń, lecz jest „wstrzykiwana” do\n",
"atencji jako pozycyjne obciążenie (*positional bias*), tj. np. w modelu T5 model uczy\n",
"się serii skalarnych obciążeń $b_{\\Delta}$, gdzie $&Delta; &isin;\n",
"\\\\{-&Delta;<sub>\\operatorname{max}</sub>,&hellip;,-1,0,1,&hellip;,&Delta;<sub>\\operatorname{max}</sub>\\\\}.\n",
"\n",
"Obciążenia te po prostu są dodawane do atencji:\n",
"\n",
"$$\\hat{\\alpha}_{i,j} = \\vec{q_i}^T\\vec{k_j} + b_{j-i}.$$\n",
"\n",
"Zalety takiego podejścia:\n",
"\n",
"- stosunkowo niski koszt obliczeniowy,\n",
"- względne pozycje są uwzględniane nie tylko na początku obliczeń, lecz w każdej warstwie.\n",
"\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.5"
},
"org": null
},
"nbformat": 4,
"nbformat_minor": 1
}