{"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 c_{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\nObliczanie $\\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{c_t} = \\Gamma_u \\bullet \\vec{\\xi_t} + (1 - \\Gamma_u) \\bullet \\vec{c_{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{c_{t-1}},E(w_t)] + b_\\gamma),$$\n\n$$\\Gamma_u = \\sigma(W_u[\\vec{c_{t-1}},E(w_t)] + b_u),$$\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}