diff --git a/wyk/11_rnn.org b/wyk/11_rnn.org new file mode 100644 index 0000000..8f5221d --- /dev/null +++ b/wyk/11_rnn.org @@ -0,0 +1,328 @@ + +* Rekurencyjne sieci neuronowe +** Inne spojrzenie na sieci przedstawione do tej pory +*** Regresja liniowa/logistyczna lub klasyfikacja wieloklasowa na całym tekście + +W regresji liniowej czy logistycznej bądź w klasyfikacji wieloklasowej +(z funkcją Softmax) stosowaliśmy następujący schemat: + +Do tej pory patrzyliśmy na to tak, że po prostu cały tekst jest od +razu przetwarzany przez (prostą) sieć neuronową, popatrzmy na ten +przypadek, jak na sytuację przetwarzania sekwencyjnego. Będzie to +trochę sztuczne, ale uogólnimy to potem w sensowny sposób. + +**** Wektoryzacja + +Po pierwsze, zauważmy, że w wielu schematach wektoryzacji (np. tf), wektor +dokumentów jest po prostu sumą wektorów poszczególnych składowych: + +$$\vec{v}(d) = \vec{v}(t^1,\ldots,t^K) = \vec{v}(t^1) \+ \ldots + \vec{v}(t^K) = \sum_{k=1}^K \vec{v}(t^i),$$ + +gdzie w schemacie tf \vec{v}(t^i) to po prostu wektor /one-hot/ dla słowa. + +*Pytanie* Jak postać przyjmie w \vec{v}(t^i) dla wektoryzacji tf-idf? + +Wektory $\vec{v}(t^k)$ mogą być również gęstymi wektorami +($\vec{v}(t^k) \in \mathcal{R}^n$, gdzie $n$ jest rzędu 10-1000), np. +w modelu Word2vec albo mogą to być *wyuczalne* wektory (zanurzenia +słów, /embeddings/), tzn. wektory, które są parametrami uczonej sieci! + +*Pytanie* Ile wag (parametrów) wnoszą wyuczalne wektory do sieci? + +**** Prosta wektoryzacja wyrażona w modelu sekwencyjnym + +Jak zapisać równoważnie powyższą wektoryzację w modelu *sekwencyjnym*, tj. przy założeniu, że +przetwarzamy wejście token po tokenie (a nie „naraz”)? Ogólnie wprowadzimy bardzo +ogólny model sieci *rekurencyjnej*. + +Po pierwsze zakładamy, że sieć ma pewien stan $\vec{s^k} \in +\mathcal{R}^m$ (stan jest wektorem o długości $m$), który może +zmieniać się z każdym krokiem (przetwarzanym tokenem). Zmiana stanu +jest określona przez pewną funkcję $R : \mathcal{R}^m \times +\mathcal{R}^n \rightarrow \mathcal{R}^m$ ($n$ to rozmiar wektorów +$\vec{v}(t^k)$): + +$$\vec{s^k} = R(\vec{s^{k-1}}, \vec{v}(t^k)).$$ + +W przypadku wektoryzacji tf-idf mamy do czynienia z prostym +sumowaniem, więc $R$ przyjmuje bardzo prostą postać: + +$$\vec{s^0} = [0,\dots,0],$$ + +$$R(\vec{s}, \vec{x}) = \vec{s} + \vec{x}.$$ + +**** Wyjście z modelu + +Dla regresji liniowej/logistycznej, oprócz funkcji $R$, która określa +zmianę stanu, potrzebujemy funkcji $O$, która określa wyjście systemu w każdym kroku. + +$$y^k = O(\vec{s^k})$$ + +W zadaniach klasyfikacji czy regresji, kiedy patrzymy na cały tekst w +zasadzie wystarczy wziąć /ostatnią/ wartość (tj. $y^K$). Można sobie +wyobrazić sytuację, kiedy wartości $y^k$ dla $k < k$ również mogą być jakoś przydatne +(np. klasyfikujemy na bieżąco tekst wpisywany przez użytkownika). + +W każdym razie dla regresji liniowej funkcja $O$ przyjmie postać: + +$$O(\vec{s}) = \vec{w}\vec{s}$$, + +gdzie $\vec{w}$ jest wektorem wyuczylnych wag, dla regresji zaś logistycznej: + +$$O(\vec{s}) = \OperatorName(softmax)(\vec{w}\vec{s})$$ + +*Pytanie*: jaką postać przyjmie $O$ dla klasyfikacji wieloklasowej + +** Prosta sieć rekurencyjna + +W najprostszej sieci rekurencyjnej (/Vanilla RNN/, sieć Elmana, +czasami po prostu RNN) w każdym kroku oprócz właściwego wejścia +($\vec{v}(t^k)$) będziemy również podawać na wejściu poprzedni stan +sieci ($\vec{s^{k-1}}$). + +Innymi słowy, funkcje $R$ przyjmie następującą postać: + +$$s^k = \sigma(W\langle\vec{v}(t^k), \vec{s^{k-1}}\rangle + \vec{b}),$$ + +gdzie: + +- $\langle\vec{x},\vec{y}\rangle$ to konkatenacja dwóch wektorów, +- $W \in \mathcal{R}^m \times \mathcal{R}^{n+m} $ — macierz wag, +- $b \in \mathcal{R}^m$ — wektor obciążeń (/biases/). + +Taką sieć RNN można przedstawić schematycznie w następujący sposób: + +[[./img-rnn.png]] + +Zauważmy, że zamiast macierzy $W$ działającej na konkatenacji wektorów można wprowadzić dwie +macierze $U$ i $V$ i tak zapisać wzór: + +$$s^k = \sigma(U\vec{v}(t^k) + V\vec{s^{k-1}} + \vec{b}).$$ + +Jeszcze inne spojrzenie na sieć RNN: + +[[./rnn.png]] + +Powyższy rysunek przedstawia pojedynczy krok sieci RNN. Dla całego +wejścia (powiedzmy, 3-wyrazowego) możemy sieć rozwinąć (/unroll/): + +[[./rnn-seq.png]] + +*** Zastosowanie sieci RNN do etykietowania sekwencji + +*** Problemy z prostymi sieciami RNN + +W praktyce proste sieci RNN są bardzo trudne w uczenia, zazwyczaj +pojawia się problem *zanikających* (rzadziej: *eksplodujących*) +gradientów: w propagacji wstecznej błąd szybko zanika i nie jest w +stanie dotrzeć do początkowych wejść. + +** Sieci RNN z bramkami + +W prostych sieciach RNN podstawowa trudność polega na tym, że mamy +niewielką kontrolę nad tym jak pamięć (stan) jest aktualizowana. Aby +zwiększyć tę kontrolę, potrzebujemy *bramek*. + +*** Bramki + +Zazwyczaj do tej pory rozpatrywaliśmy iloczyn skalarny wektorów, w +wyniku którego otrzymujemy liczbę (w PyTorchu wyrażany za pomocą operatora ~@~), np. + +#+BEGIN_SRC ipython :session mysession :exports both :results raw drawer + import torch + + a = torch.tensor([-1, 0, 3]) + b = torch.tensor([2, 5, -1]) + a @ b +#+END_SRC + +#+RESULTS: +:results: +# Out[2]: +: tensor(-5) +:end: + +Czasami przydatny jest *iloczyn Hadamarda*, czyli przemnożenie +wektorów (albo macierzy) po współrzędnych. W PyTorchu taki iloczyn +wyrażany jest za pomocą operatora ~*~, w notacji matematycznej będziemy używali +znaku $\odot$. + +#+BEGIN_SRC ipython :session mysession :exports both :results raw drawer + import torch + + a = torch.tensor([-1, 0, 3]) + b = torch.tensor([2, 5, -1]) + a * b +#+END_SRC + +#+RESULTS: +:results: +# Out[3]: +: tensor([-2, 0, -3]) +:end: + +Zauważmy, że iloczyn Hadamarda przez wektor złożony z zer i jedynek daje nam /filtr/, możemy +selektywnie wygaszać pozycje wektora, np. tutaj wyzerowaliśmy 2. i 5. pozycję wektora: + +#+BEGIN_SRC ipython :session mysession :exports both :results raw drawer + import torch + + a = torch.tensor([1., 2., 3., 4., 5.]) + b = torch.tensor([1., 0., 1., 1., 0.]) + a * b +#+END_SRC + +#+RESULTS: +:results: +# Out[4]: +: tensor([1., 0., 3., 4., 0.]) +:end: + + +Co więcej, za pomocą bramki możemy selektywnie kontrolować, co +zapamiętujemy, a co zapominamy. Rozpatrzmy mianowicie wektor zer i +jedynek $\vec{g} \in \{0,1}^m$, dla stanu (pamięci) $\vec{s}$ i nowej informacji +$\vec{x}$ możemy dokonywać aktualizacji w następujący sposób: + +$$\vec{s} \leftarrow \vec{g} \odot \vec{x} + (1 - \vec{g}) \odot \vec{s}$$ + +Na przykład, za pomocą bramki można wpisać nową wartość na 2. i 5. pozycję wektora. + +#+BEGIN_SRC ipython :session mysession :exports both :results raw drawer + import torch + + s = torch.tensor([1., 2., 3., 4., 5.]) + x = torch.tensor([8., 7., 15., -3., -8.]) + + g = torch.tensor([0., 1., 0., 0., 1.]) + + s = g * x + (1 - g) * s + s +#+END_SRC + +#+RESULTS: +:results: +# Out[8]: +: tensor([ 1., 7., 3., 4., -8.]) +:end: + +Wektor bramki nie musi być z góry określony, może być wyuczalny. Wtedy +jednak lepiej założyć, że bramka jest „miękka”, np. jej wartości +pochodzi z sigmoidy zastosowanej do jakiejś wcześniejszej warstwy. + +#+BEGIN_SRC ipython :session mysession :exports both :results raw drawer + import torch + + s = torch.tensor([1., 2., 3., 4., 5.]) + x = torch.tensor([8., 7., 15., -3., -8.]) + + pre_g = torch.tensor([-2.5, 10.0, -1.2, -101., 1.3]) + g = torch.sigmoid(pre_g) + + s = g * x + (1 - g) * s + s +#+END_SRC + +#+RESULTS: +:results: +# Out[14]: +: tensor([ 1.5310, 6.9998, 5.7777, 4.0000, -5.2159]) +:end: + +*Pytanie:* dlaczego sigmoida zamiast tanh? + +*** Sieć LSTM + +Architektura LSTM (/Long Short-Term Memory/) pozwala rozwiązać problem +znikających gradientów — za cenę komplikacji obliczeń. + +W sieci LSTM stan $\vec{s^k}$ ma dwie połówki, tj. $\vec{s^k} = +\langle\vec{c^k},\vec{h^k}\rangle$, gdzie + +- $\vec{c^k}$ to *komórka pamięci*, która nie zmienia swojej, chyba że celowo zmodyfikujemy jej wartość + za pomocą bramek, +- $\vec{h^k}$ to ukryty stan (przypominający $\vec{s^k}$ ze zwykłej sieci RNN). + +Sieć LSTM zawiera 3 bramki: + +- bramkę zapominania (/forget gate/), która steruje wymazywaniem informacji z komórki + pamięci $\vec{c^k}$, +- bramkę wejścia (/input gate/), która steruje tym, na ile nowe informacje aktualizują + komórkę pamięci $\vec{c^k}$, +- bramkę wyjścia (/output gate/), która steruje tym, co z komórki + pamięci przekazywane jest na wyjście. + +Wszystkie trzy bramki definiowane są za pomocą bardzo podobnego wzoru — warstwy liniowej na +poprzedniej wartości warstwy ukrytej i bieżącego wejścia. + +$$\vec{i} = \sigma(W_i\langle\vec{v}(t^k),\vec{\vec{h}^{k-1}}\rangle)$$ + +$$\vec{f} = \sigma(W_f\langle\vec{v}(t^k),\vec{\vec{h}^{k-1}}\rangle)$$ + +$$\vec{o} = \sigma(W_f\langle\vec{v}(t^k),\vec{\vec{h}^{k-1}}\rangle)$$ + +Jak widać, wzory różnią się tylko macierzami wag $W_*$. + +Zmiana komórki pamięci jest zdefiniowana jak następuje: + +$$\vec{c^k} = \vec{f} \odot \vec{c^{k-1}} + \vec{i} \vec{z^k}$$, + +gdzie + +$$\vec{z^k} = \OperatorName{tanh}(W_z\langle\vec{v}(t^k),\vec{\vec{h}^{k-1}}\rangle)$$ + +Stan ukryty zmienia się w następujący sposób: + +$$\vec{h^K} = \vec{o} \odot \OperatorName{tanh}(\vec{c^k})$$. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +** Literatura + +Yoav Goldberg, /Neural Network Methods for Natural Language +Processing/, Morgan & Claypool Publishers, 2017