267 lines
10 KiB
Org Mode
267 lines
10 KiB
Org Mode
* Sieci Transformer — 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ń o 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 tokena
|
|
$i$.
|
|
|
|
Następnie takie zanurzenie $E^p$ jest po prostu dodawane do standardowego
|
|
embeddingu „semantycznego” $E^s$, by ostatecznie otrzymać embeddingu
|
|
tokena na konkretnej pozycji:
|
|
|
|
$$E(w_i) = E^p(i) + E^s(w_i).$$
|
|
|
|
Rzecz jasna rozmiar embeddingu pozycyjnego musi być identyczny z
|
|
rozmiarem 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 ipython :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 pewna trudność.
|
|
Proste dodawanie wektorów nie zawsze da dobry wyniku, np.
|
|
jeśli dodamy do naszego znacznika 20 godzin:
|
|
|
|
#+BEGIN_SRC ipython :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 ipython :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 ipython :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ążenia 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.
|