From 137340b1ab97e9a855e0870fe49a93239a1b2ed9 Mon Sep 17 00:00:00 2001 From: Filip Gralinski Date: Sat, 28 May 2022 14:15:35 +0200 Subject: [PATCH] Atencja --- wyk/09_Rekurencyjny_model_jezyka.ipynb | 415 ++++++++++++++++++++++++- wyk/10_Atencja.org | 214 ++++++++++++- 2 files changed, 621 insertions(+), 8 deletions(-) diff --git a/wyk/09_Rekurencyjny_model_jezyka.ipynb b/wyk/09_Rekurencyjny_model_jezyka.ipynb index e053d57..2617202 100644 --- a/wyk/09_Rekurencyjny_model_jezyka.ipynb +++ b/wyk/09_Rekurencyjny_model_jezyka.ipynb @@ -1 +1,414 @@ -{"cells":[{"cell_type":"markdown","metadata":{},"source":["## Model języka oparty na rekurencyjnej sieci neuronowej\n\n"]},{"cell_type":"markdown","metadata":{},"source":["### Podejście rekurencyjne\n\n"]},{"cell_type":"markdown","metadata":{},"source":["Na poprzednim wykładzie rozpatrywaliśmy różne funkcje\n$A(w_1,\\dots,w_{i-1})$, dzięki którym możliwe było „skompresowanie” ciągu słów\n(a właściwie ich zanurzeń) o dowolnej długości w wektor o stałej długości.\n\nFunkcję $A$ moglibyśmy zdefiniować w inny sposób, w sposób ****rekurencyjny****.\n\nOtóż moglibyśmy zdekomponować funkcję $A$ do\n\n- pewnego stanu początkowego $\\vec{s_0} \\in \\mathcal{R}^p$,\n- pewnej funkcji rekurencyjnej $R : \\mathcal{R}^p \\times \\mathcal{R}^m \\rightarrow \\mathcal{R}^p$.\n\nWówczas funkcję $A$ można będzie zdefiniować rekurencyjnie jako:\n\n$$A(w_1,\\dots,w_t) = R(A(w_1,\\dots,w_{t-1}), E(w_t)),$$\n\nprzy czym dla ciągu pustego:\n\n$$A(\\epsilon) = \\vec{s_0}$$\n\nPrzypomnijmy, że $m$ to rozmiar zanurzenia (embeddingu). Z kolei $p$ to rozmiar wektora stanu\n(często $p=m$, ale nie jest to konieczne).\n\nPrzy takim podejściu rekurencyjnym wprowadzamy niejako „strzałkę\nczasu”, możemy mówić o przetwarzaniu krok po kroku.\n\nW wypadku modelowania języka możemy końcowy wektor stanu zrzutować do wektora o rozmiarze słownika\ni zastosować softmax:\n\n$$\\vec{y} = \\operatorname{softmax}(CA(w_1,\\dots,w_{i-1})),$$\n\ngdzie $C$ jest wyuczalną macierzą o rozmiarze $|V| \\times p$.\n\n"]},{"cell_type":"markdown","metadata":{},"source":["### Worek słów zdefiniowany rekurencyjnie\n\n"]},{"cell_type":"markdown","metadata":{},"source":["Nietrudno zdefiniować model „worka słów” w taki rekurencyjny sposób:\n\n- $p=m$,\n- $\\vec{s_0} = [0,\\dots,0]$,\n- $R(\\vec{s}, \\vec{x}) = \\vec{s} + \\vec{x}.$\n\nDodawanie (również wektorowe) jest operacją przemienną i łączną, więc\nto rekurencyjne spojrzenie niewiele tu wnosi. Można jednak zastosować\ninną funkcję $R$, która nie jest przemienna — w ten sposób wyjdziemy poza\nnieuporządkowany worek słów.\n\n"]},{"cell_type":"markdown","metadata":{},"source":["### Związek z programowaniem funkcyjnym\n\n"]},{"cell_type":"markdown","metadata":{},"source":["Zauważmy, że stosowane tutaj podejście jest tożsame z zastosowaniem funkcji typu `fold`\nw językach funkcyjnych:\n\n![img](./09_Rekurencyjny_model_jezyka/fold.png \"Opis funkcji foldl w języku Haskell\")\n\nW Pythonie odpowiednik `fold` jest funkcja `reduce` z pakietu `functools`:\n\n"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[{"name":"stdout","output_type":"stream","text":"18"}],"source":["from functools import reduce\n\ndef product(ns):\n return reduce(lambda a, b: a * b, ns, 1)\n\nproduct([2, 3, 1, 3])"]},{"cell_type":"markdown","metadata":{},"source":["### Sieci rekurencyjne\n\n"]},{"cell_type":"markdown","metadata":{},"source":["W jaki sposób „złamać” przemienność i wprowadzić porządek? Jedną z\nnajprostszych operacji nieprzemiennych jest konkatenacja — możemy\ndokonać konkatenacji wektora stanu i bieżącego stanu, a następnie\nzastosować jakąś prostą operację (na wyjściu musimy mieć wektor o\nrozmiarze $p$, nie $p + m$!), dobrze przy okazji „złamać” też\nliniowość operacji. Możemy po prostu zastosować rzutowanie (mnożenie\nprzez macierz) i jakąś prostą funkcję aktywacji (na przykład sigmoidę):\n\n$$R(\\vec{s}, \\vec{e}) = \\sigma(W[\\vec{s},\\vec{e}] + \\vec{b}).$$\n\nDodatkowo jeszcze wprowadziliśmy wektor obciążeń $\\vec{b}$, a zatem wyuczalne wagi obejmują:\n\n- macierz $W \\in \\mathcal{R}^p \\times \\mathcal{R}^{p+m}$,\n- wektor obciążeń $b \\in \\mathcal{R}^p$.\n\nOlbrzymią zaletą sieci rekurencyjnych jest fakt, że liczba wag nie zależy od rozmiaru wejścia!\n\n"]},{"cell_type":"markdown","metadata":{},"source":["#### Zwykła sieć rekurencyjna\n\n"]},{"cell_type":"markdown","metadata":{},"source":["Wyżej zdefiniową sieć nazywamy „zwykłą” siecią rekurencyjną (*Vanilla RNN*).\n\n**Uwaga**: przez RNN czasami rozumie się taką „zwykłą” sieć\nrekurencyjną, a czasami szerszą klasę sieci rekurencyjnych\nobejmujących również sieci GRU czy LSTM (zob. poniżej).\n\n![img](./09_Rekurencyjny_model_jezyka/rnn.drawio.png \"Schemat prostego modelu języka opartego na zwykłej sieci rekurencyjnych\")\n\n**Uwaga**: powyższy schemat nie obejmuje już „całego” działania sieci,\n tylko pojedynczy krok czasowy.\n\n"]},{"cell_type":"markdown","metadata":{},"source":["#### Praktyczna niestosowalność prostych sieci RNN\n\n"]},{"cell_type":"markdown","metadata":{},"source":["Niestety w praktyce proste sieci RNN sprawiają duże trudności jeśli\nchodzi o propagację wsteczną — pojawia się zjawisko zanikającego\n(rzadziej: eksplodującego) gradientu. Dlatego zaproponowano różne\nmodyfikacje sieci RNN. Zacznijmy od omówienia stosunkowo prostej sieci GRU.\n\n"]},{"cell_type":"markdown","metadata":{},"source":["### Sieć GRU\n\n"]},{"cell_type":"markdown","metadata":{},"source":["GRU (*Gated Recurrent Unit*) to sieć z dwiema ****bramkami**** (*gates*):\n\n- bramką resetu (*reset gate*) $\\Gamma_\\gamma \\in \\mathcal{R}^p$ — która określa, w jakim\n stopniu sieć ma pamiętać albo zapominać stan z poprzedniego kroku,\n- bramką aktualizacji (*update gate*) $\\Gamma_u \\in \\mathcal{R}^p$ — która określa wpływ\n bieżącego wyrazu na zmianę stanu.\n\nTak więc w skrajnym przypadku:\n\n- jeśli $\\Gamma_\\gamma = [0,\\dots,0]$, sieć całkowicie zapomina\n informację płynącą z poprzednich wyrazów,\n- jeśli $\\Gamma_u = [0,\\dots,0]$, sieć nie bierze pod uwagę\n bieżącego wyrazu.\n\nZauważmy, że bramki mogą selektywnie, na każdej pozycji wektora stanu,\nsterować przepływem informacji. Na przykład $\\Gamma_\\gamma =\n[0,1,\\dots,1]$ oznacza, że pierwsza pozycja wektora stanu jest\nzapominana, a pozostałe — wnoszą wkład w całości.\n\n"]},{"cell_type":"markdown","metadata":{},"source":["#### Wzory\n\n"]},{"cell_type":"markdown","metadata":{},"source":["Najpierw zdefiniujmy pośredni stan $\\vec{\\xi} \\in \\mathcal{R}^p$:\n\n$$\\vec{\\xi_t} = \\operatorname{tanh}(W_{\\xi}[\\Gamma_\\gamma \\bullet \\vec{s_{t-1}}, E(w_t)] + b_{\\xi}),$$\n\ngdzie $\\bullet$ oznacza iloczyn Hadamarda (nie iloczyn skalarny!) dwóch wektorów:\n\n$$[x_1,\\dots,x_n] \\bullet [y_1,\\dots,y_n] = [x_1 y_1,\\dots,x_n y_n].$$\n\nJak widać, obliczanie $\\vec{\\xi_t}$ bardzo przypomina zwykłą sieć rekurencyjną,\njedyna różnica polega na tym, że za pomocą bramki $\\Gamma_\\gamma$\nmodulujemy wpływ poprzedniego stanu.\n\nOstateczna wartość stanu jest średnią ważoną poprzedniego stanu i bieżącego stanu pośredniego:\n\n$$\\vec{s_t} = \\Gamma_u \\bullet \\vec{\\xi_t} + (1 - \\Gamma_u) \\bullet \\vec{s_{t-1}}.$$\n\nSkąd się biorą bramki $\\Gamma_\\gamma$ i $\\Gamma_u$? Również z poprzedniego stanu i z biężacego wyrazu.\n\n$$\\Gamma_\\gamma = \\sigma(W_\\gamma[\\vec{s_{t-1}},E(w_t)] + \\vec{b_\\gamma}),$$\n\n$$\\Gamma_u = \\sigma(W_u[\\vec{s_{t-1}},E(w_t)] + \\vec{b_u}),$$\n\n"]},{"cell_type":"markdown","metadata":{},"source":["### Sieć LSTM\n\n"]},{"cell_type":"markdown","metadata":{},"source":["Architektura LSTM (*Long Short-Term Memory*), choć powstała wcześniej\nniż GRU, jest od niej nieco bardziej skomplikowana.\n\n- zamiast dwóch bramek LSTM zawiera ****trzy bramki****: bramkę wejścia (*input gate*),\n bramkę wyjścia (*output gate*) i bramkę zapominania (*forget gate*),\n- oprócz ukrytego stanu $\\vec{s_t}$ sieć LSTM posiada również ****komórkę pamięci**** (*memory cell*),\n $\\vec{c_t}$, komórka pamięci, w przeciwieństwie do stanu, zmienia się wolniej (intuicyjnie:\n *jeśli nie zrobimy nic specjalnego, wartość komórki pamięci się nie zmieni*).\n\n"]},{"cell_type":"markdown","metadata":{},"source":["#### Wzory\n\n"]},{"cell_type":"markdown","metadata":{},"source":["Komórka pamięci modulowana jest za pomocą bramki zapominania ($\\Gamma_f$) i bramki\nwejścia ($\\Gamma_i$), bramki te określają na ile uwzględniamy, odpowiednio,\npoprzednią wartość komórki pamięci $\\vec{c_{t-1}}$ i wejście, a\nwłaściwie wejście w połączeniu z poprzednim stanem:\n\n$$\\vec{c_t} = \\Gamma_f \\bullet \\vec{c_{t-1}} + \\Gamma_i \\bullet \\vec{\\xi_t},$$\n\ngdzie wektor pomocniczy $\\vec{\\xi_t}$ wyliczany jest w następujący sposób:\n\n$$\\vec{\\xi_t} = \\operatorname{tanh}(W_{\\xi}[\\vec{s_{t-1}}, E(w_t)] + \\vec{b_\\xi}.$$\n\nNowa wartość stanu sieci nie zależy bezpośrednio od poprzedniej wartości stanu, lecz\njest równa komórce pamięci modulowanej bramką wyjścia:\n\n$$\\vec{h_t} = \\Gamma_o \\bullet \\operatorname{tanh}(\\vec{c_t}).$$\n\n"]},{"cell_type":"markdown","metadata":{},"source":["#### Obliczanie bramek\n\n"]},{"cell_type":"markdown","metadata":{},"source":["Wartości wszystkie trzech bramek są liczone w identyczny sposób (wzory\nróżnią się tylko macierzami wag i wektorem obciążeń):\n\n$$\\Gamma_f = \\sigma(W_f[\\vec{s_{t-1}}, E(w_t)] + \\vec{b_f}),$$\n\n$$\\Gamma_i = \\sigma(W_i[\\vec{s_{t-1}}, E(w_t)] + \\vec{b_i}),$$\n\n$$\\Gamma_o = \\sigma(W_o[\\vec{s_{t-1}}, E(w_t)] + \\vec{b_o}).$$\n\n"]},{"cell_type":"markdown","metadata":{},"source":["#### Wartości początkowe\n\n"]},{"cell_type":"markdown","metadata":{},"source":["Początkowe wartości stanu i komórki pamięci mogą być ustawione na zero:\n\n$$\\vec{s_0} = \\vec{0},$$\n\n$$\\vec{c_0} = \\vec{0}.$$\n\n"]},{"cell_type":"markdown","metadata":{},"source":["#### Podsumowanie\n\n"]},{"cell_type":"markdown","metadata":{},"source":["Sieci LSTM dominowały w zagadnieniach przetwarzania języka naturalnego\n(ogólniej: przetwarzania sekwencji) do czasu pojawienia się\narchitektury Transformer w 2017 roku.\n\nNa sieci LSTM oparty był ELMo, jeden z pierwszych dużych\n****pretrenowanych modeli języka****, dostrajanych później pod konkretne\nzadania (na przykład klasyfikację tekstu), zob. artykuł [Deep\ncontextualized word\nrepresentations]([https://arxiv.org/pdf/1802.05365.pdf](https://arxiv.org/pdf/1802.05365.pdf)). Dokładniej\nmówiąc, ELMo był siecią ****BiLSTM****, połączeniem dwóch sieci, jednej\ndziałającej z lewej strony na prawą, drugiej — z prawej do lewej.\n\n"]}],"metadata":{"org":null,"kernelspec":{"display_name":"Python 3","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.5.2"}},"nbformat":4,"nbformat_minor":0} \ No newline at end of file +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Model języka oparty na rekurencyjnej sieci neuronowej\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Podejście rekurencyjne\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Na poprzednim wykładzie rozpatrywaliśmy różne funkcje\n", + "$A(w_1,\\dots,w_{i-1})$, dzięki którym możliwe było „skompresowanie” ciągu słów\n", + "(a właściwie ich zanurzeń) o dowolnej długości w wektor o stałej długości.\n", + "\n", + "Funkcję $A$ moglibyśmy zdefiniować w inny sposób, w sposób ****rekurencyjny****.\n", + "\n", + "Otóż moglibyśmy zdekomponować funkcję $A$ do\n", + "\n", + "- pewnego stanu początkowego $\\vec{s_0} \\in \\mathcal{R}^p$,\n", + "- pewnej funkcji rekurencyjnej $R : \\mathcal{R}^p \\times \\mathcal{R}^m \\rightarrow \\mathcal{R}^p$.\n", + "\n", + "Wówczas funkcję $A$ można będzie zdefiniować rekurencyjnie jako:\n", + "\n", + "$$A(w_1,\\dots,w_t) = R(A(w_1,\\dots,w_{t-1}), E(w_t)),$$\n", + "\n", + "przy czym dla ciągu pustego:\n", + "\n", + "$$A(\\epsilon) = \\vec{s_0}$$\n", + "\n", + "Przypomnijmy, że $m$ to rozmiar zanurzenia (embeddingu). Z kolei $p$ to rozmiar wektora stanu\n", + "(często $p=m$, ale nie jest to konieczne).\n", + "\n", + "Przy takim podejściu rekurencyjnym wprowadzamy niejako „strzałkę\n", + "czasu”, możemy mówić o przetwarzaniu krok po kroku.\n", + "\n", + "W wypadku modelowania języka możemy końcowy wektor stanu zrzutować do wektora o rozmiarze słownika\n", + "i zastosować softmax:\n", + "\n", + "$$\\vec{y} = \\operatorname{softmax}(CA(w_1,\\dots,w_{i-1})),$$\n", + "\n", + "gdzie $C$ jest wyuczalną macierzą o rozmiarze $|V| \\times p$.\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Worek słów zdefiniowany rekurencyjnie\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Nietrudno zdefiniować model „worka słów” w taki rekurencyjny sposób:\n", + "\n", + "- $p=m$,\n", + "- $\\vec{s_0} = [0,\\dots,0]$,\n", + "- $R(\\vec{s}, \\vec{x}) = \\vec{s} + \\vec{x}.$\n", + "\n", + "Dodawanie (również wektorowe) jest operacją przemienną i łączną, więc\n", + "to rekurencyjne spojrzenie niewiele tu wnosi. Można jednak zastosować\n", + "inną funkcję $R$, która nie jest przemienna — w ten sposób wyjdziemy poza\n", + "nieuporządkowany worek słów.\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Związek z programowaniem funkcyjnym\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Zauważmy, że stosowane tutaj podejście jest tożsame z zastosowaniem funkcji typu `fold`\n", + "w językach funkcyjnych:\n", + "\n", + "![img](./09_Rekurencyjny_model_jezyka/fold.png \"Opis funkcji foldl w języku Haskell\")\n", + "\n", + "W Pythonie odpowiednik `fold` jest funkcja `reduce` z pakietu `functools`:\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "18" + ] + } + ], + "source": [ + "from functools import reduce\n", + "\n", + "def product(ns):\n", + " return reduce(lambda a, b: a * b, ns, 1)\n", + "\n", + "product([2, 3, 1, 3])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Sieci rekurencyjne\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "W jaki sposób „złamać” przemienność i wprowadzić porządek? Jedną z\n", + "najprostszych operacji nieprzemiennych jest konkatenacja — możemy\n", + "dokonać konkatenacji wektora stanu i bieżącego stanu, a następnie\n", + "zastosować jakąś prostą operację (na wyjściu musimy mieć wektor o\n", + "rozmiarze $p$, nie $p + m$!), dobrze przy okazji „złamać” też\n", + "liniowość operacji. Możemy po prostu zastosować rzutowanie (mnożenie\n", + "przez macierz) i jakąś prostą funkcję aktywacji (na przykład sigmoidę):\n", + "\n", + "$$R(\\vec{s}, \\vec{e}) = \\sigma(W[\\vec{s},\\vec{e}] + \\vec{b}).$$\n", + "\n", + "Dodatkowo jeszcze wprowadziliśmy wektor obciążeń $\\vec{b}$, a zatem wyuczalne wagi obejmują:\n", + "\n", + "- macierz $W \\in \\mathcal{R}^p \\times \\mathcal{R}^{p+m}$,\n", + "- wektor obciążeń $b \\in \\mathcal{R}^p$.\n", + "\n", + "Olbrzymią zaletą sieci rekurencyjnych jest fakt, że liczba wag nie zależy od rozmiaru wejścia!\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Zwykła sieć rekurencyjna\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Wyżej zdefiniową sieć nazywamy „zwykłą” siecią rekurencyjną (*Vanilla RNN*).\n", + "\n", + "**Uwaga**: przez RNN czasami rozumie się taką „zwykłą” sieć\n", + "rekurencyjną, a czasami szerszą klasę sieci rekurencyjnych\n", + "obejmujących również sieci GRU czy LSTM (zob. poniżej).\n", + "\n", + "![img](./09_Rekurencyjny_model_jezyka/rnn.drawio.png \"Schemat prostego modelu języka opartego na zwykłej sieci rekurencyjnych\")\n", + "\n", + "**Uwaga**: powyższy schemat nie obejmuje już „całego” działania sieci,\n", + " tylko pojedynczy krok czasowy.\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Praktyczna niestosowalność prostych sieci RNN\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Niestety w praktyce proste sieci RNN sprawiają duże trudności jeśli\n", + "chodzi o propagację wsteczną — pojawia się zjawisko zanikającego\n", + "(rzadziej: eksplodującego) gradientu. Dlatego zaproponowano różne\n", + "modyfikacje sieci RNN. Zacznijmy od omówienia stosunkowo prostej sieci GRU.\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Sieć GRU\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "GRU (*Gated Recurrent Unit*) to sieć z dwiema ****bramkami**** (*gates*):\n", + "\n", + "- bramką resetu (*reset gate*) $\\Gamma_\\gamma \\in \\mathcal{R}^p$ — która określa, w jakim\n", + " stopniu sieć ma pamiętać albo zapominać stan z poprzedniego kroku,\n", + "- bramką aktualizacji (*update gate*) $\\Gamma_u \\in \\mathcal{R}^p$ — która określa wpływ\n", + " bieżącego wyrazu na zmianę stanu.\n", + "\n", + "Tak więc w skrajnym przypadku:\n", + "\n", + "- jeśli $\\Gamma_\\gamma = [0,\\dots,0]$, sieć całkowicie zapomina\n", + " informację płynącą z poprzednich wyrazów,\n", + "- jeśli $\\Gamma_u = [0,\\dots,0]$, sieć nie bierze pod uwagę\n", + " bieżącego wyrazu.\n", + "\n", + "Zauważmy, że bramki mogą selektywnie, na każdej pozycji wektora stanu,\n", + "sterować przepływem informacji. Na przykład $\\Gamma_\\gamma =\n", + "[0,1,\\dots,1]$ oznacza, że pierwsza pozycja wektora stanu jest\n", + "zapominana, a pozostałe — wnoszą wkład w całości.\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Wzory\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Najpierw zdefiniujmy pośredni stan $\\vec{\\xi} \\in \\mathcal{R}^p$:\n", + "\n", + "$$\\vec{\\xi_t} = \\operatorname{tanh}(W_{\\xi}[\\Gamma_\\gamma \\bullet \\vec{s_{t-1}}, E(w_t)] + b_{\\xi}),$$\n", + "\n", + "gdzie $\\bullet$ oznacza iloczyn Hadamarda (nie iloczyn skalarny!) dwóch wektorów:\n", + "\n", + "$$[x_1,\\dots,x_n] \\bullet [y_1,\\dots,y_n] = [x_1 y_1,\\dots,x_n y_n].$$\n", + "\n", + "Jak widać, obliczanie $\\vec{\\xi_t}$ bardzo przypomina zwykłą sieć rekurencyjną,\n", + "jedyna różnica polega na tym, że za pomocą bramki $\\Gamma_\\gamma$\n", + "modulujemy wpływ poprzedniego stanu.\n", + "\n", + "Ostateczna wartość stanu jest średnią ważoną poprzedniego stanu i bieżącego stanu pośredniego:\n", + "\n", + "$$\\vec{s_t} = \\Gamma_u \\bullet \\vec{\\xi_t} + (1 - \\Gamma_u) \\bullet \\vec{s_{t-1}}.$$\n", + "\n", + "Skąd się biorą bramki $\\Gamma_\\gamma$ i $\\Gamma_u$? Również z poprzedniego stanu i z biężacego wyrazu.\n", + "\n", + "$$\\Gamma_\\gamma = \\sigma(W_\\gamma[\\vec{s_{t-1}},E(w_t)] + \\vec{b_\\gamma}),$$\n", + "\n", + "$$\\Gamma_u = \\sigma(W_u[\\vec{s_{t-1}},E(w_t)] + \\vec{b_u}),$$\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Sieć LSTM\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Architektura LSTM (*Long Short-Term Memory*), choć powstała wcześniej\n", + "niż GRU, jest od niej nieco bardziej skomplikowana.\n", + "\n", + "- zamiast dwóch bramek LSTM zawiera ****trzy bramki****: bramkę wejścia (*input gate*),\n", + " bramkę wyjścia (*output gate*) i bramkę zapominania (*forget gate*),\n", + "- oprócz ukrytego stanu $\\vec{s_t}$ sieć LSTM posiada również ****komórkę pamięci**** (*memory cell*),\n", + " $\\vec{c_t}$, komórka pamięci, w przeciwieństwie do stanu, zmienia się wolniej (intuicyjnie:\n", + " *jeśli nie zrobimy nic specjalnego, wartość komórki pamięci się nie zmieni*).\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Wzory\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Komórka pamięci modulowana jest za pomocą bramki zapominania ($\\Gamma_f$) i bramki\n", + "wejścia ($\\Gamma_i$), bramki te określają na ile uwzględniamy, odpowiednio,\n", + "poprzednią wartość komórki pamięci $\\vec{c_{t-1}}$ i wejście, a\n", + "właściwie wejście w połączeniu z poprzednim stanem:\n", + "\n", + "$$\\vec{c_t} = \\Gamma_f \\bullet \\vec{c_{t-1}} + \\Gamma_i \\bullet \\vec{\\xi_t},$$\n", + "\n", + "gdzie wektor pomocniczy $\\vec{\\xi_t}$ wyliczany jest w następujący sposób:\n", + "\n", + "$$\\vec{\\xi_t} = \\operatorname{tanh}(W_{\\xi}[\\vec{s_{t-1}}, E(w_t)] + \\vec{b_\\xi}.$$\n", + "\n", + "Nowa wartość stanu sieci nie zależy bezpośrednio od poprzedniej wartości stanu, lecz\n", + "jest równa komórce pamięci modulowanej bramką wyjścia:\n", + "\n", + "$$\\vec{h_t} = \\Gamma_o \\bullet \\operatorname{tanh}(\\vec{c_t}).$$\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Obliczanie bramek\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Wartości wszystkie trzech bramek są liczone w identyczny sposób (wzory\n", + "różnią się tylko macierzami wag i wektorem obciążeń):\n", + "\n", + "$$\\Gamma_f = \\sigma(W_f[\\vec{s_{t-1}}, E(w_t)] + \\vec{b_f}),$$\n", + "\n", + "$$\\Gamma_i = \\sigma(W_i[\\vec{s_{t-1}}, E(w_t)] + \\vec{b_i}),$$\n", + "\n", + "$$\\Gamma_o = \\sigma(W_o[\\vec{s_{t-1}}, E(w_t)] + \\vec{b_o}).$$\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Wartości początkowe\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Początkowe wartości stanu i komórki pamięci mogą być ustawione na zero:\n", + "\n", + "$$\\vec{s_0} = \\vec{0},$$\n", + "\n", + "$$\\vec{c_0} = \\vec{0}.$$\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Podsumowanie\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Sieci LSTM dominowały w zagadnieniach przetwarzania języka naturalnego\n", + "(ogólniej: przetwarzania sekwencji) do czasu pojawienia się\n", + "architektury Transformer w 2017 roku.\n", + "\n", + "Na sieci LSTM oparty był ELMo, jeden z pierwszych dużych\n", + "****pretrenowanych modeli języka****, dostrajanych później pod konkretne\n", + "zadania (na przykład klasyfikację tekstu), zob. artykuł [Deep\n", + "contextualized word\n", + "representations]([https://arxiv.org/pdf/1802.05365.pdf](https://arxiv.org/pdf/1802.05365.pdf)). Dokładniej\n", + "mówiąc, ELMo był siecią ****BiLSTM****, połączeniem dwóch sieci, jednej\n", + "działającej z lewej strony na prawą, drugiej — z prawej do lewej.\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.2" + }, + "org": null + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/wyk/10_Atencja.org b/wyk/10_Atencja.org index dde1008..e976b7c 100644 --- a/wyk/10_Atencja.org +++ b/wyk/10_Atencja.org @@ -23,17 +23,17 @@ model języka z workiem słów. Przyjmijmy bigramowy model języka ($n=2$), wów $$y = \operatorname{softmax}(C[E(w_{i-1}),A(w_1,\dots,w_{i-2})]),$$ gdzie $A$ była prostą agregacją (np. sumą albo średnią) embeddingów -$E(w_1),\dots,E(w_{i-2})$. Aby wyjść z prostego nieuporządkowanego +$E(w_1),\dots,E(w_{i-2})$. Aby wyjść z nieuporządkowanego modelu worka słów, próbowaliśmy w prosty sposób uwzględnić pozycję wyrazów czy ich istotność (za pomocą odwrotnej częstości -dokumentowej). Oba te sposoby niestety zupełnie nie uwzględniają kontekstu. +dokumentowej). Oba te sposoby niestety zupełnie nie uwzględniają *kontekstu*. Innymi słowy, chcielibyśmy mieć sumę ważoną zanurzeń: -$$A(w_1,\dots,j) = \omega_1 E(w_1) + \dots \omega_j E(w_j) = \sum_{k=1}^j \omega_k E(w_k),$$ +$$A(w_1,\dots,j) = \omega_1 E(w_1) + \dots + \omega_j E(w_j) = \sum_{k=1}^j \omega_k E(w_k),$$ tak by $\omega_k$ w sposób bardziej zasadniczy zależały od lokalnego kontekstu, a -nie tylko od pozycji $k$ czy słowa $w_k$. W naszym prostym przypadku +nie tylko od pozycji $k$ czy słowa $w_k$. W naszym uproszczonym przypadku jako kontekst możemy rozpatrywać słowo bezpośrednio poprzedzające odgadywane słowa (kontekstem jest $w_{i-1}$). @@ -43,10 +43,10 @@ Wygodnie również przyjąć, że $\sum_{k=1}^j \omega_k = 1$, wówczas mamy do Będziemy liczyć nieznormalizowane **wagi atencji** $\hat{\alpha}_{k,j}$. Określają one, jak bardzo słowo $w_j$ „zwraca -uwagę” na poszczególne, inne słowa. Innymi słowy, wagi je opisują, jak +uwagę” na poszczególne, inne słowa. Innymi słowy, wagi opisują, jak bardzo słowo $w_k$ pasuje do naszego kontekstu, czyli słowa $w_j$. -Najprostszy sposób mierzenia dopasowania to po prostu iloczyn skalarn: +Najprostszy sposób mierzenia dopasowania to po prostu iloczyn skalarny: $$\hat{\alpha}_{k,j} = E(w_k)E(w_j),$$ @@ -78,10 +78,210 @@ Teraz jako wagi $\omega$ w naszym modelu języka możemy przyjąć: $$\omega_k = \alpha_{k,i-1}.$$ Oznacza to, że z naszego worka będziemy „wyjmowali” słowa w sposób -selektywny w zależności od wyrazu, który bezpośrednio poprzedza +selektywny, w zależności od wyrazu, który bezpośrednio poprzedza słowo odgadywane. *** Diagram #+CAPTION: Atencja użyta w prostym neuronowym modelu języka [[./10_Atencja/simple-attention.drawio.png]] + +** Atencja jako „miękka” baza danych + +O atencji można myśleć metaforycznie jako o odpytywaniu „miękkiej”, wektorowej +bazy danych. Możemy sobie wyobrazić, że słowa $w_1,\dots,w_{j-1}$ są +naszą bazą danych, a słowo $w_j$ (z którego kierujemy „snop” uwagi) +jest *zapytaniem* (/query/). To zapytanie dopasowujemy do *kluczy* +(/keys/), w najprostszym ujęciu po prostu słów $w_1,\dots,w_{j-1}$ (a +właściwie ich zanurzeń). Jeśli klucz pasuje do zapytania, odpowiednia +wartość (/value/) jest wydobywana z bazy. Nasza baza jest jednak +„miękka”, nie — zerojedynkowa, zapytanie pasuje klucza w pewnym +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 — +mogą one powstawać przez rzutowanie podstawowe zanurzenia różnymi +macierzami: + +$$\vec{k_i} = W_k E(w_i),$$ + +$$\vec{v_i} = W_v E(w_i).$$ + +Również samo zapytanie może powstać przez rzutowanie: + +$$\vec{q_i} = W_q E(w_i).$$ + +Jeśli zanurzenie $E(w_i)$ o rozmiarze $m$ przedstawimy w postaci +kolumnowej, wówczas macierze będą $W_k$ i $W_q$ będą miały rozmiar +$d_k \times m$, gdzie $d_k$ jest rozmiarem kluczy i zapytań (dlaczego +wektory kluczy i zapytań powinny mieć raczej ten sam rozmiar?), zaś macierz +$W_v$ — $d_v \times m$, gdzie $d_v$ to rozmiar zanurzenia wektora wartości. +Zazwyczaj $d_k = d_v = m$, ale nie jest to obligatoryjne. + +Teraz nieznormalizowane wagi atencji przyjmą postać: + +$$\hat{\alpha}_{i,j} = \vec{q_i}^T\vec{k_j} = (W_q E(w_i))(W_k E(k_j)).$$ + +Zauważmy, że ciąg $\hat{\alpha}_{1,j},\dots,\hat{\alpha}_{j-1,j}$ można potraktować jako wektor +$\hat{\vec{\alpha}_{*,j}}$ i wyliczać w postaci zwartej: + +$$\hat{\vec{\alpha}_{*,j}} = \vec{k_j}^T K$$ + +gdzie $K$ to macierz kluczy złożona z wektorów +$\vec{k_1},\dots,\vec{k_{j-1}}$, tj. macierz o rozmiarze $d_k \times (j-1)$. + +Wektor znormalizowanych wag atencji będzie miał wówczas postać: + +$$\vec{\alpha}_{*,j} = \operatorname{softmax}(\vec{k_j}^T K).$$ + +Dokonajmy teraz agregacji wartości — obliczeniamy średnią wektorów +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 +$(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{k_j}^T K)^T V.$$ + +Sposób patrzenia na atencję przez pryzmat trójki +zapytania-klucze-wartości okaże się niezwykle ważny w wypadku modelu Transformer (zob. kolejny wykład). + +** Atencja jako składnik sieci rekurencyjnej + +Atencję wprowadzono pierwotnie jako uzupełnienie sieci rekurencyjnej. +Potrzeba ta pojawiła się na początku rozwoju *neuronowego tłumaczenia +maszynowego* (/neural machine translation/, /NMT/), czyli tłumaczenia +maszynowego (automatycznego) realizowanego za pomocą sieci neuronowych. + +Neuronowe tłumaczenie maszynowe jest właściwie rozszerzeniem idei +modelowania języka na biteksty (teksty równoległe). Omówmy najpierw +podstawy generowania tekstu. + +*** Model języka jako generator + +Jak pamiętamy, model języka $M$ wylicza prawdopodobieństwo tekstu $w_1,\dots,w_N$: + +$$P_M(w_1,\dots,w_N) = ?.$$ + +Zazwyczaj jest to równoważne obliczaniu rozkładu prawdopodobieństwa kolejnego słowa: + +$$P_M(w_j|w_1,\dots,w_{j-1}) = ?.$$ + +Załóżmy, że mamy pewien początek (*prefiks*) tekstu o długości $p$: +$w_1,\dots,w_p$. Powiedzmy, że naszym celem jest *wygenerowanie* +dokończenia czy kontynuacji tego tekstu (nie określamy z góry tej +długości tej kontynuacji). + +Najprostszy sposób wygenerowania pierwszego wyrazu dokończenia polega +na wzięciu wyrazu maksymalizującego prawdopodobieństwo według modelu języka: + +$$w_{p+1} = \operatorname{argmax}_w P_M(w|w_1,\dots,w_p).$$ + +*Pytanie*: Dlaczego \operatorname{argmax}, a nie \operatorname{max}? + +Słowo $w_{p+1}$ możemy dołączyć do prefiksu i powtórzyć procedurę: + +$$w_{p+2} = \operatorname{argmax}_w P_M(w|w_1,\dots,w_p,w_{p+1}),$$ + +i tak dalej. + +*Pytanie*: Kiedy zakończymy procedurę generowania? + +Omawiana procedura jest najprostszym sposobem, czasami nie daje +najlepszego wyniku, na przykład może pojawić się efekt „jąkania” +(model generuje w kółko ten sam wyraz), dlatego opracowano bardziej +wymyślne sposoby generowania w oparciu o modele języka. Omówimy je później. + +*** Zastosowania generatora opartego na modelu języka + +Mogłoby się wydawać, że generator tekstu ma raczej ograniczone +zastosowanie (generowanie fake newsów?). Okazuje się jednak, że +zaskakująco wiele zadań przetwarzania języka naturalnego można +przedstawić jako zadanie generowania tekstu. Przykładem jest tłumaczenie maszynowe. + +*** Tłumaczenie maszynowe jako zadanie generowania tekstu + +W tłumaczeniu maszynowym (tłumaczeniu automatycznym, ang. /machine +translation/) na wejściu podawany jest tekst (na ogół pojedyncze +zdanie) źródłowy (/source sentence/) $S = (u_1,\dots,u_|S|)$, celem +jest uzyskanie tekstu docelowego (/target sentence/) +$T=(w_1,\dots,w_|T|). Zakładamy, że $S$ jest tekstem w pewnym języku +źródłowym (/source language/), zaś $T$ — w innym języku, języku +docelowym (/target language/). + +Współczesne tłumaczenie maszynowe jest oparte na metodach +statystycznych — system uczy się na podstawie obszernego zbioru +odpowiadających sobie zdań w obu językach. Taki zbiór nazywamy +korpusem równoległym (/parallel corpus/). Duży zbiór korpusów +równoległych dla wielu języków można znaleźć na stronie projektu [OPUS](https://opus.nlpl.eu/). +Zobaczmy na przykład fragment EUROPARL (protokoły Parlamentu Europejskiego): + +#+BEGIN_SRC +$ wget 'https://opus.nlpl.eu/download.php?f=Europarl/v8/moses/en-pl.txt.zip' -O en-pl.txt.zip +$ unzip en-pl.txt.zip +$ paste Europarl.en-pl.en Europarl.en-pl.pl | shuf -n 5 +The adoption of these amendments by the Committee on the Environment meant that we could place more emphasis on patients' rights to information, rather than make it an option for the pharmaceutical industries to provide that information. Przyjęcie tych poprawek przez Komisję Ochrony Środowiska Naturalnego oznaczało, że mogliśmy położyć większy nacisk na prawo pacjentów do informacji, zamiast uczynić zeń możliwość, z której branża farmaceutyczna może skorzystać w celu dostarczenia informacji. +I hope that the High Representative - who is not here today - will raise this episode with China and also with Nepal, whose own nascent democracy is kept afloat partly by EU taxpayers' money in the form of financial aid. Mam nadzieję, że nieobecna dzisiaj wysoka przedstawiciel poruszy tę kwestię w rozmowach z Chinami, ale również z Nepalem, którego młoda demokracja funkcjonuje częściowo dzięki finansowej pomocy pochodzącej z pieniędzy podatników w UE. +Immunity and privileges of Renato Brunetta (vote) Wniosek o obronę immunitetu parlamentarnego Renata Brunetty (głosowanie) +The 'new Member States' - actually, the name continues to be sort of conditional, making it easier to distinguish between the 'old' Member States and those that acceded to the EU after two enlargement rounds, owing to their particular historical background and perhaps the fact that they are poorer than the old ones."Nowe państwa członkowskie” - ta nazwa nadal ma w pewnym sensie charakter warunkowy i ułatwia rozróżnienie pomiędzy "starszymi” państwami członkowskimi oraz tymi, które przystąpiły do UE po dwóch rundach rozszerzenia, które wyróżnia ich szczególna historia, a zapewne także fakt, że są uboższe, niż starsze państwa członkowskie. +The number of armed attacks also rose by 200% overall. Także liczba ataków zbrojnych wzrosła łącznie o 200 %. +#+END_SRC + +Zauważmy, że możemy taki tekst modelować po prostu traktując jako +jeden. Innymi słowy, nie modelujemy tekstu angielskiego ani polskiego, +tylko angielsko-polską mieszankę, to znaczy uczymy model, który najpierw modeluje prawdopodobieństwo +po stronie źródłowej (powiedzmy — angielskiej): + +#+BEGIN_SRC +The number of armed attacks also ? +#+END_SRC + +W momencie napotkania specjalnego tokenu końca zdania źródłowego (powiedzmy ~~) model +powinien nauczyć się, że musi przerzucić się na modelowanie tekstu w języku docelowym (powiedzmy — polskim): + +#+BEGIN_SRC +The number of armed attacks also rose by 200% overall.Także liczba ataków ? +#+END_SRC + +W czasie uczenia wykorzystujemy korpus równoległy traktując go po prostu jako zwykły ciągły tekst +(dodajemy tylko specjalne tokeny końca zdania źródłowego i końca zdania docelowego). + +W fazie inferencji (w tłumaczeniu maszynowym tradycyjnie nazywaną +*dekodowaniem*) zamieniamy nasz model języka w generator i podajemy +tłumaczone zdanie jako prefiks, doklejając tylko token ~~. + +**** Neuronowe modele języka jako translatory + +Jako że N-gramowego modelu języka ani modelu opartego na worku słów +nie da się użyć w omawiany sposób w tłumaczeniu maszynowym +(dlaczego?), jako pierwszych użyto w neuronowym tłumaczeniu maszynowym +sieci LSTM, przy użyciu omawianego wyżej sposobu. + +System tłumaczenia oparte na sieciach LSTM działały zaskakująco +dobrze, zważywszy na to, że cała informacja o zdaniu źródłowym musi +zostać skompresowana do wektora o stałym rozmiarze. (Dlaczego? W +momencie osiągnięcia tokenu ~~ cały stan sieci to kombinacja +właściwego stanu $\vec{s_i}$ i komórki pamięci $\vec{c_i}$.) + +Neuronowe tłumaczenie oparte na sieciach LSTM działa względnie dobrze +dla krótkich zdań, dla dłuższych rezultaty są gorsze — po prostu sieć +nie jest w stanie skompresować w wektorze o stałej długości znaczenia +całego zdania. Na początku rozwoju neuronowego tłumaczenia maszynowego +opracowano kilka metod radzenia sobie z tym problemem (np. zaskakująco +dobrze działa odwrócenie zdania źródłowego — siec LSTM łatwiej zacząć +generować zdanie docelowe, jeśli niedawno „widziała” początek zdania +źródłowego, przynajmniej dla pary języków o podobnym szyku). + +Najlepsze efekty dodało dodanie atencji do modelu LSTM + +**** Atencja w sieciach rekurencyjnych + +Funkcję rekurencyjną można rozbudować o trzeci argument, w którym +podany będzie wynik działania atencji $A'$ względem ostatniego wyrazu, tj.: + +$$A(w_1,\dots,w_t) = R(A(w_1,\dots,w_{t-1}), A'(w_1,\dots,w_{t-1}), E(w_t)),$$ + +W czasie tłumaczenia model może kierować swoją uwagę na wyrazy +powiązane z aktualnie tłumaczonym fragmentem (zazwyczaj — po prostu odpowiedniki).