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