RNN
This commit is contained in:
parent
86a5fbe20c
commit
1071d5ba44
328
wyk/11_rnn.org
Normal file
328
wyk/11_rnn.org
Normal file
@ -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
|
Loading…
Reference in New Issue
Block a user