diff --git a/wyk/11_Transformer.ipynb b/wyk/11_Transformer.ipynb new file mode 100644 index 0000000..b5e0f8a --- /dev/null +++ b/wyk/11_Transformer.ipynb @@ -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", + "
\n", + "

Modelowanie języka

\n", + "

11. Transformer [wykład]

\n", + "

Filip Graliński (2022)

\n", + "
\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=)" + ] + } + ], + "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 ``. 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 (``). Musimy wzbogacić reprezentację wektorową\n", + " słów i specjalnego tokenu (``).\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 +} diff --git a/wyk/11_Transformer.org b/wyk/11_Transformer.org index 3a41ea9..bd9be02 100644 --- a/wyk/11_Transformer.org +++ b/wyk/11_Transformer.org @@ -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 - [tokenizer.decode(i) for i in encoded_input.input_ids[0]] +#+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,15 +146,15 @@ tensor([-130.2947, -129.5677, -136.4030, ..., -138.3791, -138.8967, -131.6319], grad_fn=) :end: -#+BEGIN_SRC python :session mysession :exports both :results raw drawer - from torch import softmax, topk +#+BEGIN_SRC ipython :session mysession :exports both :results raw drawer + from torch import softmax, topk - k = 20 + k = 20 - t = topk(softmax(logits, -1), k) + t = topk(softmax(logits, -1), k) - tb = [[tokenizer.decode(t.indices[ix]), t.values[ix].item()] for ix in range(k)] - tb + tb = [[tokenizer.decode(t.indices[ix]), t.values[ix].item()] for ix in range(k)] + tb #+END_SRC #+RESULTS: @@ -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 (~~). Musimy wzbogacić reprezentację wektorową słów i specjalnego tokenu (~~). @@ -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.$$ diff --git a/wyk/15_Pozycyjne_zanurzenia.ipynb b/wyk/15_Pozycyjne_zanurzenia.ipynb new file mode 100644 index 0000000..50dda59 --- /dev/null +++ b/wyk/15_Pozycyjne_zanurzenia.ipynb @@ -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", + " $Epp{\\operatorname{max}}+i = Ep(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 $Δ ∈\n", + "\\\\{-Δ\\operatorname{max},…,-1,0,1,…,Δ\\operatorname{max}\\\\}.\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 +} diff --git a/wyk/15_Pozycyjne_zanurzenia.org b/wyk/15_Pozycyjne_zanurzenia.org new file mode 100644 index 0000000..43bda8d --- /dev/null +++ b/wyk/15_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. diff --git a/wyk/helpers/intro b/wyk/helpers/intro index 6da9943..7d82176 100644 --- a/wyk/helpers/intro +++ b/wyk/helpers/intro @@ -8,7 +8,7 @@ "![Logo 1](https://git.wmi.amu.edu.pl/AITech/Szablon/raw/branch/master/Logotyp_AITech1.jpg)\n", "
\n", "

Modelowanie języka

\n", - "

10. Atencja [wykład]

\n", + "

11. Transformer [wykład]

\n", "

Filip Graliński (2022)

\n", "
\n", "\n",