Merge branch 'master' of git.wmi.amu.edu.pl:filipg/aitech-moj

This commit is contained in:
Jakub Pokrywka 2022-07-05 11:25:01 +02:00
commit 0b5a7c18c7
5 changed files with 1278 additions and 29 deletions

495
wyk/11_Transformer.ipynb Normal file
View File

@ -0,0 +1,495 @@
{
"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> 11. <i>Transformer</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": [
"## Transformer\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Atencja jako „miękka” baza danych\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"O atencji można myśleć metaforycznie jako o odpytywaniu „miękkiej”, wektorowej\n",
"bazy danych. Możemy sobie wyobrazić, że słowa $w_1,\\dots,w_{j-1}$ są\n",
"naszą bazą danych, a słowo $w_j$ (z którego kierujemy „snop” uwagi)\n",
"jest **zapytaniem** (*query*). To zapytanie dopasowujemy do **kluczy**\n",
"(*keys*), w najprostszym ujęciu po prostu słów $w_1,\\dots,w_{j-1}$ (a\n",
"właściwie ich zanurzeń). Jeśli klucz pasuje do zapytania, odpowiednia\n",
"wartość (*value*) jest wydobywana z bazy. Nasza baza jest jednak\n",
"„miękka”, nie — zerojedynkowa, zapytanie pasuje do klucza w pewnym\n",
"stopniu, mniej lub bardziej.\n",
"\n",
"W najprostszym ujęciu wartości są tym samym co klucze, czyli z naszej\n",
"bazy wydobywamy te same zanurzenia słów, których używamy jako kluczy.\n",
"Można jednak skomplikować schemat, rozróżniając klucze i wartości —\n",
"mogą one powstawać przez rzutowanie podstawowych zanurzeń różnymi\n",
"macierzami:\n",
"\n",
"$$\\vec{k_i} = W_k E(w_i),$$\n",
"\n",
"$$\\vec{v_i} = W_v E(w_i).$$\n",
"\n",
"Również samo zapytanie może powstać przez rzutowanie:\n",
"\n",
"$$\\vec{q_i} = W_q E(w_i).$$\n",
"\n",
"Jeśli zanurzenie $E(w_i)$ o rozmiarze $m$ przedstawimy w postaci\n",
"kolumnowej, wówczas macierze będą $W_k$ i $W_q$ będą miały rozmiar\n",
"$d_k \\times m$, gdzie $d_k$ jest rozmiarem kluczy i zapytań (dlaczego\n",
"wektory kluczy i zapytań powinny mieć raczej ten sam rozmiar?), zaś macierz\n",
"$W_v$ — $d_v \\times m$, gdzie $d_v$ to rozmiar zanurzenia wektora wartości.\n",
"Zazwyczaj $d_k = d_v = m$, ale nie jest to obligatoryjne.\n",
"\n",
"Teraz nieznormalizowane wagi atencji przyjmą postać:\n",
"\n",
"$$\\hat{\\alpha}_{i,j} = \\vec{q_i}^T\\vec{k_j} = (W_q E(w_i))(W_k E(k_j)).$$\n",
"\n",
"Zauważmy, że ciąg $\\hat{\\alpha}_{1,j},\\dots,\\hat{\\alpha}_{j-1,j}$ można potraktować jako wektor\n",
"$\\hat{\\vec{\\alpha}_{*,j}}$ i wyliczać w postaci zwartej:\n",
"\n",
"$$\\hat{\\vec{\\alpha}_{*,j}} = \\vec{q_j}^T K$$\n",
"\n",
"gdzie $K$ to macierz kluczy złożona z wektorów\n",
"$\\vec{k_1},\\dots,\\vec{k_{j-1}}$, tj. macierz o rozmiarze $d_k \\times (j-1)$.\n",
"\n",
"Wektor znormalizowanych wag atencji będzie miał wówczas postać:\n",
"\n",
"$$\\vec{\\alpha}_{*,j} = \\operatorname{softmax}(\\vec{q_j}^T K).$$\n",
"\n",
"Dokonajmy teraz agregacji wartości — obliczamy średnią wektorów\n",
"wartości ($\\vec{v_i}$) ważoną atencją:\n",
"\n",
"$$A(w_1,\\dots,j-1) = \\alpha_{1,j} \\vec{v_1} + \\dots + \\alpha_{j-1,j} \\vec{v_{j-1}} = \\sum_{i=1}^{j-1} \\alpha_{i,j} v_i.$$\n",
"\n",
"Jeśli $j-1$ wektorów wartości ułożymy w macierz $V$ (o rozmiarze\n",
"$(j-1) \\times d_v$), powyższy wzór będziemy mogli zapisać jako iloczyn wektora wag atencji i macierzy $V$:\n",
"\n",
"$$A(w_1,\\dots,j-1) = \\vec{\\alpha}_{*,j}^T V = \\operatorname{softmax}(\\vec{q_j}^T K)^T V.$$\n",
"\n",
"Sposób patrzenia na atencję przez pryzmat trójki\n",
"zapytania-klucze-wartości okaże się niezwykle ważny w wypadku modelu Transformer.\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Model Transformer — historia\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Architekturę Transformer opracowano, pierwotnie, na potrzeby\n",
"tłumaczenia automatycznego (rok 2017, artykuł [Attention Is All You\n",
"Need]([https://arxiv.org/abs/1706.03762](https://arxiv.org/abs/1706.03762))). Szybko okazało się, że\n",
"podobnie jak w wypadku modelu ELMo dla sieci LSTM, można **pretrenować**\n",
"duże modele Transformer (po prostu na dużych korpusach tekstowych, w\n",
"sposób nienadzorowany), a potem dostrajać pod konkretne zadanie\n",
"przetwarzania języka naturalnego. Jednym z pierwszych przykładów\n",
"takiego podejścia był model BERT (rok 2018, artykuł [BERT:\n",
"Pre-training of Deep Bidirectional Transformers for Language\n",
"Understanding]([https://arxiv.org/abs/1810.04805](https://arxiv.org/abs/1810.04805))). To podejście było\n",
"później rozwinięte w postaci różnych modeli Transformer, również dla innych\n",
"języków niż angielski (RoBERTa, XLM, Polish RoBERTa itd.).\n",
"\n",
"Na tym wykładzie my skupimy się na innej odnodze modeli Transformer —\n",
"modelach generatywnych, takich jak na przykład GPT-2 czy GPT-3. To\n",
"podejście jest bliższe duchowi czystego modelowania języka — model\n",
"języka jest używany wprost jako generator.\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### GPT-2 — przykład działania\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Dokonajmy najpierw tokenizacji:\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'input_ids': tensor([[ 464, 2159, 1810, 6711, 481, 2221, 287, 1160, 2078, 287]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]])}"
]
}
],
"source": [
"from transformers import AutoTokenizer\n",
"tokenizer = AutoTokenizer.from_pretrained(\"gpt2\")\n",
"text = \"The World War III will begin in 2028 in\"\n",
"encoded_input = tokenizer(text, return_tensors='pt')\n",
"encoded_input"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"['The', ' World', ' War', ' III', ' will', ' begin', ' in', ' 20', '28', ' in']"
]
}
],
"source": [
"[tokenizer.decode(i) for i in encoded_input.input_ids[0]]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Zwróćmy uwagę, że w GPT-2 tokeny obejmują spacje!\n",
"\n",
"Teraz uruchommy zasadniczy model:\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"from transformers import AutoModelForCausalLM\n",
"model = AutoModelForCausalLM.from_pretrained(\"gpt2\")\n",
"outputs = model(**encoded_input)"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"softmax(outputs[0][0][-1])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Z modelu GPT-2 otrzymamy rozkład prawdopodobieństwa kolejnego wyrazu, najpierw w postaci\n",
"nieznormalizowanych **logitów**:\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"tensor([-130.2947, -129.5677, -136.4030, ..., -138.3791, -138.8967,\n",
" -131.6319], grad_fn=<SelectBackward0>)"
]
}
],
"source": [
"logits = outputs[0][0][-1]\n",
"logits"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[[' earnest', 0.07378227263689041], [' the', 0.06698606163263321], [' 1945', 0.043497972190380096], [' September', 0.024068640545010567], [' March', 0.0228887926787138], [' October', 0.02232857048511505], [' Europe', 0.02032744698226452], [' 2020', 0.018564637750387192], [' Japan', 0.018423961475491524], [' December', 0.016560807824134827], [' January', 0.015074416995048523], [' July', 0.014139187522232533], [' April', 0.013183596543967724], [' November', 0.012901309877634048], [' 20', 0.012770282104611397], [' Afghanistan', 0.012765118852257729], [' 1944', 0.01266297698020935], [' June', 0.012072316370904446], [' 1914', 0.011765970848500729], [' May', 0.011659453622996807]]"
]
}
],
"source": [
"from torch import softmax, topk\n",
"\n",
"k = 20\n",
"\n",
"t = topk(softmax(logits, -1), k)\n",
"\n",
"tb = [[tokenizer.decode(t.indices[ix]), t.values[ix].item()] for ix in range(k)]\n",
"tb"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Generowanie tekstu za pomocą GPT-2\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"from transformers import pipeline\n",
"generator = pipeline('text-generation', model='gpt2')\n",
"generator('Hello, I\\'m a language model,', max_length=30, num_return_sequences=1)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Model Transformer — podstawowa idea\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Model Transformer sprowadza się właściwie do atencji; nie posiada\n",
"żadnego komponentu rekurencyjnego, ani nawet nie stosujemy czegoś w\n",
"rodzaju połączenia modelu worka słów i modelu n-gramowego.\n",
"\n",
"W pierwszym przybliżeniu przy obliczaniu rozkładu prawdopodobieństwa\n",
"dla kolejnego wyrazu, to jest:\n",
"\n",
"$$P(w_j|w_1\\dots w_{j-1})$$\n",
"\n",
"na $j$-tym miejscu (w miejscu przewidywanego wyrazu) doklejamy\n",
"specjalny token, powiedzmy `<mask>`. Token ten będzie „atendował” do\n",
"innych wszystkich wcześniejszych tokenów w zdaniu:\n",
"\n",
"$$\\vec{\\alpha}_{*,j}^T V = \\operatorname{softmax}(\\vec{q_j}^T K)^T V.$$\n",
"\n",
"Samo to byłoby oczywiście zbyt proste:\n",
"\n",
"1. Otrzymalibyśmy model (ważonego) worka słów, w dodatku każde słowo\n",
" miałoby zawsze taką samą wagę! — token $w_j$, który atenduje jest\n",
" zawsze ten sam (`<mask>`). Musimy wzbogacić reprezentację wektorową\n",
" słów i specjalnego tokenu (`<mask>`).\n",
"\n",
"2. Model Transformer w swojej podstawowej postaci w ogóle nie jest\n",
" wyposażony w pojęcie sekwencji — w przeciwieństwie do sieci\n",
" rekurencyjnych, które w sposób inherentny operują krok po kroku, w\n",
" sekwencji (w czasie). Musimy pozycję tokenów wprowadzić do sieci\n",
" Transformer nie przez modyfikację jej architektury, lecz przez dołączenie\n",
" informacji pozycyjnej do początkowych zanurzeń.\n",
"\n",
"3. Model Transformer nie powinien mieć żadnych tokenów OOV/UNK. Musimy\n",
" wrócić do kwestii tokenizacji tekstu i wprowadzić podział rzadszych\n",
" tokenów na mniejsze, **podwyrazowe** jednostki.\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Atencja wsobna\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Jeśli chodzi problem (1), rozwiążemy go przez wprowadzenie\n",
"****skontekstualizowanych reprezentacji**** tokenów.\n",
"\n",
"Na przykład słowo *mysz* ma jedno wejściowe (*statyczne*) zanurzenie\n",
"(embedding) — bez względu na to, czy chodzi o zwierzę czy urządzenie\n",
"peryferyjne, tymczasem dość łatwo ustalić na podstawie kontekstu, o\n",
"które znaczenie chodzi.\n",
"\n",
"Rozwiązanie polega na tym, że wszystkim tokenom będziemy przypisywać kolejne\n",
"zanurzenia skontekstualizowane — zależne od innych tokenów w zdaniu. W\n",
"tym celu zastosujemy atencję wsobną (samo-atencję, *self-attention*).\n",
"Każdy token będzie atendował potencjalnie do każdego innego tokenu,\n",
"również do samego siebie (!).\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Wzory\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Rozpatrywać zatem będziemy nie tylko pojedynczy wektor znormalizowanych atencji\n",
"\n",
"$$\\vec{\\alpha}_{*,j}^T V = \\operatorname{softmax}(\\vec{q_j}^T K)^T V,$$\n",
"\n",
"lecz całą serię wektorów:\n",
"\n",
"$$\\vec{\\alpha}_{*,1},\\dots,\\vec{\\alpha}_{*,i},\\dots,\\vec{\\alpha}_{*,j},$$\n",
"\n",
"gdzie:\n",
"\n",
"$$\\vec{\\alpha}_{*,i} = \\operatorname{softmax}(\\vec{q_i}^T K)$$\n",
"\n",
"i $K$ jest macierzą kluczy o rozmiarze $d_k \\times j$ (tym razem obejmuje również sam $i$-ty token).\n",
"\n",
"Nowa, skontekstualizowana reprezentacja $i$-tego tokenu będzie po prostu średnią wszystkich\n",
"wektorów ważoną atencją:\n",
"\n",
"$$E_1(w_i) = \\operatorname{softmax}(\\vec{q_i}^T K)^T V,$$\n",
"\n",
"gdzie:\n",
"\n",
"- $E_1(w_i)$ — skontekstualizowane zanurzenie $i$-tego tokenu; używając indeksu $_1$\n",
" zaznaczamy, że to jest pierwszy skonstekstualizowany embedding, rekurencyjnie będziemy budowali\n",
" kolejne $E_2(w_i)$, $E_3(w_i)$ itd. (zaś wejściowy statyczny embedding możemy oznaczyć przez $E_0(w_i)$);\n",
"- $V$ — macierz wartości o rozmiarze $j \\times d_v$.\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"##### Zwarta postać macierzowa atencji wsobnej\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Z praktycznych powodów (szybkość obliczeń na kartach graficznych) dużą\n",
"zaletą atencji wsobnej jest to, że wyliczenie skonstekstualizowanych zanurzeń dla wszystkich tokenów\n",
"w tekście da się zapisać w postaci zwartego wzoru macierzowego:\n",
"\n",
"$$E_1 = \\operatorname{Attention}(Q, K, V) = \\operatorname{softmax}(QK)^T V,$$\n",
"\n",
"gdzie $Q$ to macierz wszystkich zapytań o rozmiarze $j \\times d_k$ (wektory ułożone poziomo).\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"##### Skalowanie atencji\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Twórcy modelu Transformer odkryli, że lepsze wyniki daje skalowanie atencji\n",
"przez stałą zależną od rozmiaru wektora klucza/zapytania $d_k$:\n",
"\n",
"$$\\operatorname{Attention}(Q, K, V) = \\operatorname{softmax}(\\frac{QK}{d_k})^T V,$$\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Wielogłowicowa atencja\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Od samego początku w Transformerze zamiast jednej atencji zaproponowano wiele ****głowic atencji****\n",
"$(\\operatorname{head}_1,\\dots,\\operatorname{head}_h)$, każda głowica atencji działa w następujący sposób:\n",
"\n",
"$$\\operatorname{head_i} = \\operatorname{Attention}(QW_i^Q, KW_i^K,VW_i^V),$$\n",
"\n",
"to znaczy każda głowica atencji działa tak samo, tylko przed jej zastosowaniem mnożymy\n",
"wektory zapytań, kluczy i wartości przez różne wyuczalne macierze, odpowiednio,\n",
"$W_i^Q$, $W_i^K$, $W_i^V$. Otrzymamy w ten sposób $h$ wektorów, konkatenujemy je po prostu i mnożymy\n",
"przez dodatkową wyuczalną macierz $W^O$:\n",
"\n",
"$$\\operatorname{MultiHead}(Q, K, V) = [\\operatorname{head}_1,...,\\operatorname{head}_n]W^O.$$\n",
"\n",
"Przyjmujemy, że $d_k = d_v = m/h$, wtedy rozmiary macierzy $W_i^Q$ i $W_i^K$ będą wynosiły\n",
"$m \\times d_k$, macierzy $W_i^V$ — $m \\times d_v$, $W^O$ — $hd_v \\times m$.\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.4"
},
"org": null
},
"nbformat": 4,
"nbformat_minor": 1
}

View File

@ -15,7 +15,7 @@ stopniu, mniej lub bardziej.
W najprostszym ujęciu wartości są tym samym co klucze, czyli z naszej
bazy wydobywamy te same zanurzenia słów, których używamy jako kluczy.
Można jednak skomplikować schemat rozróżniając klucze i wartości —
Można jednak skomplikować schemat, rozróżniając klucze i wartości —
mogą one powstawać przez rzutowanie podstawowych zanurzeń różnymi
macierzami:
@ -55,7 +55,7 @@ wartości ($\vec{v_i}$) ważoną atencją:
$$A(w_1,\dots,j-1) = \alpha_{1,j} \vec{v_1} + \dots + \alpha_{j-1,j} \vec{v_{j-1}} = \sum_{i=1}^{j-1} \alpha_{i,j} v_i.$$
Jeśli $j-1$ wektorów wartości ułożyłem w macierz $V$ (o rozmiarze
Jeśli $j-1$ wektorów wartości ułożymy w macierz $V$ (o rozmiarze
$(j-1) \times d_v$), powyższy wzór będziemy mogli zapisać jako iloczyn wektora wag atencji i macierzy $V$:
$$A(w_1,\dots,j-1) = \vec{\alpha}_{*,j}^T V = \operatorname{softmax}(\vec{q_j}^T K)^T V.$$
@ -87,35 +87,33 @@ języka jest używany wprost jako generator.
Dokonajmy najpierw tokenizacji:
#+BEGIN_SRC python :session mysession :exports both :results raw drawer
#+BEGIN_SRC ipython :session mysession :exports both :results raw drawer
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("gpt2")
text = "The World War III will begin in"
text = "The World War III will begin in 2028 in"
encoded_input = tokenizer(text, return_tensors='pt')
encoded_input
#+END_SRC
#+RESULTS:
:results:
{'input_ids': tensor([[ 464, 2159, 1810, 6711, 481, 2221, 287]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1]])}
{'input_ids': tensor([[ 464, 2159, 1810, 6711, 481, 2221, 287, 1160, 2078, 287]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]])}
:end:
Możemy podejrzeć uzyskane tokeny:
#+BEGIN_SRC python :session mysession :exports both :results raw drawer
#+BEGIN_SRC ipython :session mysession :exports both :results raw drawer
[tokenizer.decode(i) for i in encoded_input.input_ids[0]]
#+END_SRC
#+RESULTS:
:results:
['The', ' World', ' War', ' III', ' will', ' begin', ' in']
['The', ' World', ' War', ' III', ' will', ' begin', ' in', ' 20', '28', ' in']
:end:
Zwróćmy uwagę, że w GPT-2 tokeny obejmują spacje!
Teraz uruchommy zasadniczy model:
#+BEGIN_SRC python :session mysession :exports both :results raw drawer
#+BEGIN_SRC ipython :session mysession :exports both :results raw drawer
from transformers import AutoModelForCausalLM
model = AutoModelForCausalLM.from_pretrained("gpt2")
outputs = model(**encoded_input)
@ -125,10 +123,19 @@ Teraz uruchommy zasadniczy model:
:results:
:end:
#+BEGIN_SRC ipython :session mysession :exports both :results raw drawer
softmax(outputs[0][0][-1])
#+END_SRC
#+RESULTS:
:results:
:end:
Z modelu GPT-2 otrzymamy rozkład prawdopodobieństwa kolejnego wyrazu, najpierw w postaci
nieznormalizowanych *logitów*:
#+BEGIN_SRC python :session mysession :exports both :results raw drawer
#+BEGIN_SRC ipython :session mysession :exports both :results raw drawer
logits = outputs[0][0][-1]
logits
#+END_SRC
@ -139,7 +146,7 @@ tensor([-130.2947, -129.5677, -136.4030, ..., -138.3791, -138.8967,
-131.6319], grad_fn=<SelectBackward0>)
:end:
#+BEGIN_SRC python :session mysession :exports both :results raw drawer
#+BEGIN_SRC ipython :session mysession :exports both :results raw drawer
from torch import softmax, topk
k = 20
@ -157,7 +164,7 @@ tensor([-130.2947, -129.5677, -136.4030, ..., -138.3791, -138.8967,
*** Generowanie tekstu za pomocą GPT-2
#+BEGIN_SRC python :session mysession :exports both :results raw drawer
#+BEGIN_SRC ipython :session mysession :exports both :results raw drawer
from transformers import pipeline
generator = pipeline('text-generation', model='gpt2')
generator('Hello, I\'m a language model,', max_length=30, num_return_sequences=1)
@ -187,7 +194,7 @@ $$\vec{\alpha}_{*,j}^T V = \operatorname{softmax}(\vec{q_j}^T K)^T V.$$
Samo to byłoby oczywiście zbyt proste:
1. Otrzymalibyśmy model (ważonego) worka słów, w dodatku każde słowo
miałoby zawsze taką samą wagę! — Token $w_j$, który atenduje jest
miałoby zawsze taką samą wagę! — token $w_j$, który atenduje jest
zawsze ten sam (~<mask>~). Musimy wzbogacić reprezentację wektorową
słów i specjalnego tokenu (~<mask>~).
@ -212,8 +219,8 @@ Na przykład słowo /mysz/ ma jedno wejściowe (/statyczne/) zanurzenie
peryferyjne, tymczasem dość łatwo ustalić na podstawie kontekstu, o
które znaczenie chodzi.
Rozwiązanie polega na tym, że tokenom będziemy przypisywać kolejne
zanurzenia skontekstualizowane — zależne od innych wyrazów w zdaniu. W
Rozwiązanie polega na tym, że wszystkim tokenom będziemy przypisywać kolejne
zanurzenia skontekstualizowane — zależne od innych tokenów w zdaniu. W
tym celu zastosujemy atencję wsobną (samo-atencję, /self-attention/).
Każdy token będzie atendował potencjalnie do każdego innego tokenu,
również do samego siebie (!).
@ -222,11 +229,11 @@ również do samego siebie (!).
Rozpatrywać zatem będziemy nie tylko pojedynczy wektor znormalizowanych atencji
$$\vec{\alpha}_{*,j}^T V = \operatorname{softmax}(\vec{q_j}^T K)^T V$$,
$$\vec{\alpha}_{*,j}^T V = \operatorname{softmax}(\vec{q_j}^T K)^T V,$$
lecz całą serię wektorów:
$$\vec{\alpha}_{*,1},\dots,\vec{\alpha}_{*,i},\dots,\vec{\alpha}_{*,j}$$
$$\vec{\alpha}_{*,1},\dots,\vec{\alpha}_{*,i},\dots,\vec{\alpha}_{*,j},$$
gdzie:
@ -244,7 +251,7 @@ gdzie:
- $E_1(w_i)$ — skontekstualizowane zanurzenie $i$-tego tokenu; używając indeksu $_1$
zaznaczamy, że to jest pierwszy skonstekstualizowany embedding, rekurencyjnie będziemy budowali
kolejne $E_2(w_i)$, $E_3(w_i)$ itd. (zaś wejściowy statyczny embedding możemy oznaczyć przez $E_0(w_i)$);
- $V$ to macierz wartości o rozmiarze $j \times d_v$.
- $V$ macierz wartości o rozmiarze $j \times d_v$.
**** Zwarta postać macierzowa atencji wsobnej
@ -261,18 +268,18 @@ gdzie $Q$ to macierz wszystkich zapytań o rozmiarze $j \times d_k$ (wektory uł
Twórcy modelu Transformer odkryli, że lepsze wyniki daje skalowanie atencji
przez stałą zależną od rozmiaru wektora klucza/zapytania $d_k$:
$\operatorname{Attention}(Q, K, V) = \operatorname{softmax}(\frac{QK}{d_k})^T V,$$
$$\operatorname{Attention}(Q, K, V) = \operatorname{softmax}(\frac{QK}{d_k})^T V,$$
** Wielogłowicowa atencja
Od samego początku w Transformerze zamiast jednej atencji zaproponowano wiele **głowic atencji**
$(\operatorname{head}_1,\dots,\operatorname{head}_h$, każda głowica atencji działa w następujący sposób:
$(\operatorname{head}_1,\dots,\operatorname{head}_h)$, każda głowica atencji działa w następujący sposób:
$$\operatorname{head_i} = \operatorname{Attention}(QW_i^Q, KW_i^K,VW_i^V),$$
to znaczy każda głowica atencji działa tak samo, tylko przed jej zastosowaniem mnożymy
wektory zapytań, kluczy i wartości przez różne wyuczalne macierze, odpowiednio,
W_i^Q, W_i^K,W_i^V. Otrzymamy w ten sposób $h$ wektorów, konkatenujemy po prostu i mnożymy
$W_i^Q$, $W_i^K$, $W_i^V$. Otrzymamy w ten sposób $h$ wektorów, konkatenujemy je po prostu i mnożymy
przez dodatkową wyuczalną macierz $W^O$:
$$\operatorname{MultiHead}(Q, K, V) = [\operatorname{head}_1,...,\operatorname{head}_n]W^O.$$

View File

@ -0,0 +1,481 @@
{
"cells": [
{
"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ń do 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 tokenu\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",
"tokenu 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 jak\n",
"rozmiar 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<sup>p</sup><sub>p{\\operatorname{max}}+i</sub> = E<sup>p</sup>(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 pewne 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ążenie 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
}

View 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.

View File

@ -8,7 +8,7 @@
"![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> 10. <i>Atencja</i> [wykład]</h2> \n",
"<h2> 11. <i>Transformer</i> [wykład]</h2> \n",
"<h3> Filip Graliński (2022)</h3>\n",
"</div>\n",
"\n",