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