aitech-eks-pub/wyk/11_rnn.ipynb

543 lines
15 KiB
Plaintext

{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"![Logo 1](https://git.wmi.amu.edu.pl/AITech/Szablon/raw/branch/master/Logotyp_AITech1.jpg)\n",
"<div class=\"alert alert-block alert-info\">\n",
"<h1> Ekstrakcja informacji </h1>\n",
"<h2> 11. <i>Sieci rekurencyjne</i> [wykład]</h2> \n",
"<h3> Filip Graliński (2021)</h3>\n",
"</div>\n",
"\n",
"![Logo 2](https://git.wmi.amu.edu.pl/AITech/Szablon/raw/branch/master/Logotyp_AITech2.jpg)"
]
},
{
"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": {
"author": "Filip Graliński",
"email": "filipg@amu.edu.pl",
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"lang": "pl",
"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.6"
},
"org": null,
"subtitle": "11.Sieci rekurencyjne[wykład]",
"title": "Ekstrakcja informacji",
"year": "2021"
},
"nbformat": 4,
"nbformat_minor": 4
}