Up
This commit is contained in:
parent
1c6e13ff5b
commit
d008173ce2
266
wyk/12_Pozycyjne_zanurzenia.org
Normal file
266
wyk/12_Pozycyjne_zanurzenia.org
Normal file
@ -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.
|
Loading…
Reference in New Issue
Block a user