{ "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", "

12. Model języka oparty na rekurencyjnej sieci neuronowej [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": [ "## 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](./12_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](./12_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 bieżącego 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.5" }, "org": null }, "nbformat": 4, "nbformat_minor": 1 }