This commit is contained in:
Filip Gralinski 2021-05-26 13:29:45 +02:00
parent 90b0947029
commit 10981fc2bc
7 changed files with 1275 additions and 500 deletions

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,29 @@
* Neurozoo * Neurozoo
** Kilka uwag dotyczących wektorów
Wektor wierszowy $\left[x_1,\dots,x_n\right]$ czy kolumnowy $\left[\begin{array}{c}
x_1 \\ \vdots \\ x_n\end{array}\right]$?
Często zakłada się wektor kolumny, będziemy używać *transpozycji*, by otrzymać wektor
wierszowy $\vec{x}^T = \left[x_1,\dots,x_n\right]$.
W praktyce, np. w PyTorchu, może to nie mieć wielkiego znaczenia:
#+BEGIN_SRC ipython :session mysession :exports both :results raw drawer
import torch
x = torch.tensor([1.0, -0.5, 2.0])
x
#+END_SRC
#+RESULTS:
:results:
# Out[2]:
: tensor([ 1.0000, -0.5000, 2.0000])
:end:
Musimy tylko uważać, jeśli przemnażamy wektor przez macierz!
** Funkcja sigmoidalna ** Funkcja sigmoidalna
Funkcja sigmoidalna zamienia dowolną wartość („sygnał”) w wartość z przedziału $(0,1)$, czyli wartość, która może być interperetowana jako prawdopodobieństwo. Funkcja sigmoidalna zamienia dowolną wartość („sygnał”) w wartość z przedziału $(0,1)$, czyli wartość, która może być interperetowana jako prawdopodobieństwo.
@ -101,6 +125,210 @@ Funkcja sigmoidalna nie ma żadnych wyuczalnych wag.
**** *Pytanie*: Czy można rozszerzyć funkcję sigmoidalną o jakieś wyuczalne wagi? **** *Pytanie*: Czy można rozszerzyć funkcję sigmoidalną o jakieś wyuczalne wagi?
** Regresja liniowa ** Regresja liniowa
*** Iloczyn skalarny — przypomnienie
$$\left[1.0, -0.5, 2.0\right]
\left[\begin{array}{c}
3.0 \\
1.5 \\
0.0\end{array}\right]
=
1.0 \cdot 3.0 + -0.5 \cdot 1.5 + 2.0 \cdot 0.0 = 2.25$$
**** Intuicje
- $\vec{a}^T \vec{b}$ mierzy jak bardzo $\vec{a}$ „pasuje” do
$\vec{b}$,
- … zwłaszcza gdy znormalizujemy wektory dzieląc przez $|\vec{a}|$ i $|\vec{b}|$:
$\frac{\vec{a}^T \vec{b}}{|\vec{a}||\vec{b}|} = \cos \theta$,
gdzie $\theta$ to kąt pomiędzy $\vec{a}$ and $\vec{b}$ (podobieństwo kosinusowe!)
- co, jeśli if $\vec{a}^T \vec{b} = 0$? — $\vec{a}$ i $\vec{b}$ są prostopadłe, np.
$\left[1, 2\right] \cdot \left[-2, -1\right]^T = 0$
- a co, jeśli $\vec{a}^T \vec{b} = -1$ — wektor są skierowane w przeciwnym kierunku, jeśli dodatkowo $|\vec{a}|=|\vec{b}|=1$, np.
$\left[\frac{\sqrt{2}}{2},\frac{\sqrt{2}}{2}\right] \cdot \left[-\frac{\sqrt{2}}{2},-\frac{\sqrt{2}}{2}\right]^T = -1$
**** W PyTorchu
#+BEGIN_SRC ipython :session mysession :exports both :results raw drawer
import torch
x = torch.tensor([1.0, -0.5, 2.0])
y = torch.tensor([3.0, 1.5, 0.0])
x @ y
#+END_SRC
#+RESULTS:
:results:
# Out[3]:
: tensor(2.2500)
:end:
*** Regresja liniowa jako element sieci neuronowej
Przypomnijmy sobie wzór na regresję liniową:
$$y = w_0 + w_1x_1 + w_2x_2 + \dots + w_{|V|}x_{|v|}$$
Jeśli wprowadzimy sztuczny element wektora $\vec{x}$ ustawiony zawsze na 1 ($x_0 = 1$), wówczas
wzór może przyjąc bardziej zwartą postać:
$$y = \sum_{i=0}^{|V|} w_ix_i = \vec{w}\vec{x}$$
*** PyTorch
**** Implementacja w PyTorchu
Zakładamy, że wektor wejściowy *nie* obejmuje dodatkowego elementu $x_0 = 1$.
#+BEGIN_SRC ipython :session mysession :exports both :results raw drawer
import torch
import torch.nn as nn
class MyLinearRegressor(nn.Module):
def __init__(self, vlen):
super(MyLinearRegressor, self).__init__()
self.register_parameter(name='w', param=torch.nn.Parameter(
torch.zeros(vlen, dtype=torch.double, requires_grad=True)))
self.register_parameter(name='b', param=torch.nn.Parameter(
torch.tensor(0., dtype=torch.double, requires_grad=True)))
def forward(self, x):
return self.b + x @ self.w
regressor = MyLinearRegressor(3)
regressor(torch.tensor([0.3, 0.4, 1.0], dtype=torch.double))
#+END_SRC
#+RESULTS:
:results:
# Out[11]:
: tensor(0., dtype=torch.float64, grad_fn=<AddBackward0>)
:end:
**** Gotowy moduł w PyTorchu
Możemy skorzystać z ogólniejszej konstrukcji — warstwy liniowej (ale,
uwaga!, na wyjściu będzie wektor jednoelementowy).
#+BEGIN_SRC ipython :session mysession :exports both :results raw drawer
import torch
import torch.nn as nn
regressor = torch.nn.Linear(in_features=3, out_features=1, bias=True)
regressor(torch.tensor([0.3, 0.4, 1.0]))
#+END_SRC
#+RESULTS:
:results:
# Out[18]:
: tensor([0.1882], grad_fn=<AddBackward0>)
:end:
*** Zastosowania
Bezpośrednio możemy zastosować do zadania regresji dla tekstu (np.
przewidywanie roku publikacji tekstu).
[[./img-linear-regression.png]]
W połączeniu z sigmoidą otrzymamy regresją logistyczną, np. dla zadania klasyfikacji tekstu:
$$p(c|\vec{x}) = \sigma(w_0 + w_1x_1 + w_2x_2 + \dots + w_{|V|}x_{|v})
= \sigma(\Sigma_{i=0}^{|V|} w_ix_i) = \sigma(\vec{w}\vec{x})$$
[[./img-logistic-regression.png]]
Tak sieć będzie aktywowana dla tekstu _aardvark in Aachen_:
[[./img-logistic-regression-aardvark.png]]
Regresje logistyczną (liniową zresztą też) dla tekstu możemy połączyć z trikiem z haszowaniem:
\[p(c|\vec{x}) = \sigma(w_0 + w_1x_1 + w_2x_2 + \dots + w_{2^b}x_{2^b})
= \sigma(\Sigma_{i=0}^{2^b} w_ix_i) = \sigma(\vec{w}\vec{x})\] \\
{\small hashing function $H : V \rightarrow \{1,\dots,2^b\}$,
e.g. MurmurHash3}
[[./img-logistic-regression-hashing.png]]
**Pytanie:** Jaki tekst otrzyma na pewno taką samą klasę jak _aardvark in Aachen_?
*** Wagi
Liczba wag jest równa rozmiarowi wektora wejściowego (oraz opcjonalnie
obciążenie).
Każda waga odpowiada wyrazowi ze słownika, możemy więc interpretować
wagi jako jednowymiarowy parametr opisujący słowa.
** Warstwa liniowa
*** Mnożenie macierzy przez wektor — przypomnienie
Mnożenie macierzy przez wektor można interpretować jako zrównolegloną operację mnożenie wektora przez wektor.
$$\left[\begin{array}{ccc}
\alert<2>{1.0} & \alert<2>{-2.0} & \alert<2>{3.0} \\
\alert<3>{-2.0} & \alert<3>{0.0} & \alert<3>{10.0}\end{array}\right]
\left[\begin{array}{c}
\alert<2-3>{1.0} \\
\alert<2-3>{-0.5} \\
\alert<2-3>{2.0}\end{array}\right]
=
\left[\begin{array}{c}
\uncover<2->{\alert<2>{8.0}} \\
\uncover<3->{\alert<3>{18.0}}\end{array}\right]$$
Jeśli przemnożymy macierz $n \times m$ przez wektor kolumnowy o długości
$m$, otrzymamy wektor o rozmiarze $n$.
W PyTorchu:
#+BEGIN_SRC ipython :session mysession :results file
import torch
m = torch.tensor([[1.0, -2.0, 3.0],
[-2.0, 0.0, 10.0]])
x = torch.tensor([1.0, -0.5, 2.0])
m @ x
#+END_SRC
#+RESULTS:
[[file:# Out[19]:
: tensor([ 8., 18.])]]
*** Definicja warstwy liniowej
Warstwa liniowa polega na przemnożeniu wejścia przez macierz. Można
to intepretować jako zrównolegloną operację regresji liniowej (równolegle
uczymy czy wykonujemy $n$ regresji liniowych).
*** PyTorch
Warstwa liniowa, która przyjmuje wektor o rozmiarze 3 i zwraca wektor o rozmiarze 2.
#+BEGIN_SRC ipython :session mysession :exports both :results raw drawer
import torch
import torch.nn as nn
regressor = torch.nn.Linear(in_features=3, out_features=2, bias=True)
regressor(torch.tensor([0.3, 0.4, 1.0]))
#+END_SRC
#+RESULTS:
:results:
# Out[23]:
: tensor([-1.1909, -0.5831], grad_fn=<AddBackward0>)
:end:
*Pytanie*: Ile wag (parametrów) ma powyżej użyta warstwa?
*** Zastosowania
Warstwa liniowa jest podstawowym elementem sieci neuronowych —
począwszy od prostych sieci neuronowych feed-forward, gdzie warstwy
liniowe łączymy używając funkcji aktywacji (np. sigmoidy).
Oto przykład prostej dwuwarstwowej sieci neuronowej do klasyfikacji binarnej.
[[./img-feed-forward.png]]
** Softmax ** Softmax
@ -576,7 +804,6 @@ Przykłady zastosowań:
- oznaczanie etykiet nazw w zadaniu NER (nazwisko, kwoty, adresy — najwięcej tokenów będzie miało etykietę pustą, zazwyczaj oznaczaną przez ~O~) - oznaczanie etykiet nazw w zadaniu NER (nazwisko, kwoty, adresy — najwięcej tokenów będzie miało etykietę pustą, zazwyczaj oznaczaną przez ~O~)
*** *Pytanie*: czy zadanie tłumaczenia maszynowego można potraktować jako problem etykietowania sekwencji? *** *Pytanie*: czy zadanie tłumaczenia maszynowego można potraktować jako problem etykietowania sekwencji?
*** Przykładowe wyzwanie NER CoNLL-2003 *** Przykładowe wyzwanie NER CoNLL-2003
Zob. <https://gonito.net/challenge/en-ner-conll-2003>. Zob. <https://gonito.net/challenge/en-ner-conll-2003>.
@ -590,7 +817,6 @@ W pierwszym polu oczekiwany wynik zapisany za pomocą notacji *BIO*.
Jako metrykę używamy F1 (z pominięciem tagu ~O~) Jako metrykę używamy F1 (z pominięciem tagu ~O~)
*** Metryka F1 *** Metryka F1
*** Etykietowanie za pomocą klasyfikacji wieloklasowej *** Etykietowanie za pomocą klasyfikacji wieloklasowej
Można potraktować problem etykietowania dokładnie tak jak problem Można potraktować problem etykietowania dokładnie tak jak problem
@ -598,7 +824,7 @@ klasyfikacji wieloklasowej (jak w przykładzie klasyfikacji dyscyplin
sportowych powyżej), tzn. rozkład prawdopodobieństwa możliwych etykiet sportowych powyżej), tzn. rozkład prawdopodobieństwa możliwych etykiet
uzyskujemy poprzez zastosowanie prostej warstwy liniowej i funkcji softmax: uzyskujemy poprzez zastosowanie prostej warstwy liniowej i funkcji softmax:
$$p(l^k=i) = s(\vec{w}\vec{v}(t^k))_i = \frac{e^{\vec{w}\vec{v}(t^k)}}{Z},$$ $$p(l^k=i) = s(W\vec{v}(t^k))_i = \frac{e^{W\vec{v}(t^k)}}{Z},$$
gdzie $\vec{v}(t^k)$ to reprezentacja wektorowa tokenu $t^k$. gdzie $\vec{v}(t^k)$ to reprezentacja wektorowa tokenu $t^k$.
Zauważmy, że tutaj (w przeciwieństwie do klasyfikacji całego tekstu) Zauważmy, że tutaj (w przeciwieństwie do klasyfikacji całego tekstu)
@ -626,7 +852,7 @@ Za pomocą wektora można przedstawić nie pojedynczy token $t^k$, lecz
cały kontekst, dla /okna/ o długości $c$ będzie to kontekst $t^{k-c},\dots,t^k,\dots,t^{k+c}$. cały kontekst, dla /okna/ o długości $c$ będzie to kontekst $t^{k-c},\dots,t^k,\dots,t^{k+c}$.
Innymi słowy klasyfikujemy token na podstawie jego samego oraz jego kontekstu: Innymi słowy klasyfikujemy token na podstawie jego samego oraz jego kontekstu:
$$p(l^k=i) = \frac{e^{\vec{w}\vec{v}(t^{k-c},\dots,t^k,\dots,t^{k+c})}}{Z_k}.$$ $$p(l^k=i) = \frac{e^{W\vec{v}(t^{k-c},\dots,t^k,\dots,t^{k+c})}}{Z_k}.$$
Zauważmy, że w tej metodzie w ogóle nie rozpatrujemy sensowności Zauważmy, że w tej metodzie w ogóle nie rozpatrujemy sensowności
sekwencji wyjściowej (etykiet), np. może być bardzo mało sekwencji wyjściowej (etykiet), np. może być bardzo mało
@ -635,7 +861,12 @@ prawdopodobne, że bezpośrednio po nazwisku występuje data.
Napiszmy wzór określający prawdopodobieństwo całej sekwencji, nie Napiszmy wzór określający prawdopodobieństwo całej sekwencji, nie
tylko pojedynczego tokenu. Na razie będzie to po prostu iloczyn poszczególnych wartości. tylko pojedynczego tokenu. Na razie będzie to po prostu iloczyn poszczególnych wartości.
$$p(l) = \prod_{k=1}^K \frac{e^{\vec{w}\vec{v}(t^{k-c},\dots,t^k,\dots,t^{k+c})}}{Z_k} = \frac{e^{\sum_{k=1}^K\vec{w}\vec{v}(t^{k-c},\dots,t^k,\dots,t^{k+c})}}{\prod_{k=1}^K Z_k}$$ $$p(l) = \prod_{k=1}^K \frac{e^{W\vec{v}(t^{k-c},\dots,t^k,\dots,t^{k+c})}}{Z_k} = \frac{e^{\sum_{k=1}^KW\vec{v}(t^{k-c},\dots,t^k,\dots,t^{k+c})}}{\prod_{k=1}^K Z_k}$$
Reprezentacja kontekstu może być funkcją embeddingów wyrazów
(zakładamy, że embedding nie zależy od pozycji słowa).
$$\vec{v}(t^{k-c},\dots,t^k,\dots,t^{k+c}) = f(\vec{E}(t^{k-c}),\dots,\vec{E}(t^k),\dots,\vec{E}({t^{k+c}})$$
** Warunkowe pola losowe ** Warunkowe pola losowe
@ -646,13 +877,178 @@ graf wyrażający „następowanie po” (czyli sekwencje). Do poprzedniego
wzoru dodamy składnik $V_{i,j}$ (który można interpretować jako wzoru dodamy składnik $V_{i,j}$ (który można interpretować jako
macierz) określający prawdopodobieństwo, że po etykiecie o numerze $i$ wystąpi etykieta o numerze $j$. macierz) określający prawdopodobieństwo, że po etykiecie o numerze $i$ wystąpi etykieta o numerze $j$.
*** *Pytanie*: Czy macierz $V$ musi być symetryczna? Czy $V_{i,j} = V_{j,i}$? Czy jakieś specjalne wartości występują na przekątnej? *Pytanie*: Czy macierz $V$ musi być symetryczna? Czy $V_{i,j} = V_{j,i}$? Czy jakieś specjalne wartości występują na przekątnej?
Macierz $V$ wraz z wektorem $\vec{w}$ będzie stanowiła wyuczalne wagi w naszym modelu. Macierz $V$ wraz z macierzą $W$ będzie stanowiła wyuczalne wagi w naszym modelu.
Wartości $V_{i,j}$ nie stanowią bezpośrednio prawdopodobieństwa, mogą Wartości $V_{i,j}$ nie stanowią bezpośrednio prawdopodobieństwa, mogą
przyjmować dowolne wartości, które będę normalizowane podobnie jak to się dzieje w funkcji Softmax. przyjmować dowolne wartości, które będę normalizowane podobnie, tak jak to się dzieje w funkcji Softmax.
W takiej wersji warunkowych pól losowych otrzymamy następujący wzór na prawdopodobieństwo całej sekwencji. W takiej wersji warunkowych pól losowych otrzymamy następujący wzór na prawdopodobieństwo całej sekwencji.
$$p(l) = \frac{e^{\sum_{k=1}^K\vec{w}\vec{v}(t^{k-c},\dots,t^k,\dots,t^{k+c}) + \sum_{k=1}^{K-1} V_{l_k,l_{k+1}}}}{\prod_{k=1}^K Z_k}$$ $$p(l) = \frac{e^{\sum_{k=1}^KW\vec{v}(t^{k-c},\dots,t^k,\dots,t^{k+c}) + \sum_{k=1}^{K-1} V_{l_k,l_{k+1}}}}{\prod_{k=1}^K Z_k}$$
** Algorytm Viterbiego
W czasie inferencji mamy ustalone wagi funkcji $\vec{v}(\dots)$ oraz
macierz $V$. Szukamy sekwencji $y$ która maksymalizuje prawdopodobieństwo estymowane przez model:
$$y = \underset{l}{\operatorname{argmax}} \hat{p}(l|t^1,\dots,t^K)$$
Naiwne podejście polegające na obliczeniu prawdopodobieństw wszystkich możliwych sekwencji miałoby
nieakceptowalną złożoność czasową $O(|L|^K)$.
Na szczęście, możemy użyć *algorytmu Viterbiego* o lepszej złożoności
obliczeniowej, algorytmu opartego na idei programowania dynamicznego.
W algorytmie będziemy wypełniać dwuwymiarowe tabele $s[i, j]$ i $b[i, j]$:
- $s[i, j]$ — będzie zawierać maksymalne prawdopodobieństwo (właściwie: nieznormalizowaną wartość,
która jest monotoniczna względem prawdopodobieństwa)
dla ciągów o długości $i$ zakończonych etykietą $l_j$,
- $b[i, j]$ — będzie zawierać „wskaźnik” wsteczny (/backpointer/) do podciągu o długości $i-1$, dla którego
razem z $l_j$ jest osiągana maksymalna wartość $s[i, j]$.
Inicjalizacja:
- $s[1, j] = (W\vec{v}(t^k,\dots,t^{k+c}))_j$,
- $b[1, j]$ — nie musimy wypełniać tej wartości.
Dla $i > 1$ i dla każdego $j$ będziemy teraz szukać:
$$\underset{q \in \{1,\dots,|V|}} \operatorname{max} s[i-1, q] + (W\vec{v}(t^{k-c},\dots,t^k,\dots,t^{k+c}))_j + V_{q, j}$$
Tę wartość przypiszemy do $s[i, j]$, z kolei do $b[i, j]$ — indeks
$q$, dla którego ta największa wartość jest osiągnięta.
Najpierw obliczenia wykonujemy wprzód wypełniając tabelę dla coraz większych wartości $j$.
W ten sposób otrzymamy największą wartość (nieznormalizowanego) prawdopodobieństwa:
$$\underset{q \in \{1,\dots,|V|}} \operatorname{max} s[K, q]$$
oraz ostatnią etykietę:
$$y^K = \underset{q \in \{1,\dots,|V|}} \operatorname{argmax} s[K, q]$$
Aby uzyskać cały ciąg, kierujemy się /wstecz/ używając wskaźników:
$$y^i = b[i, y^{i+1}]$$
*** Złożoność obliczeniowa
Zauważmy, że rozmiar tabel $s$ i $b$ wynosi $K \times |L|$, a koszt
wypełnienia każdej komórki to $|L|$, a zatem złożoność algorytmu jest wielomianowa:
$O(K|L|^2)$.
*Pytanie:* Czy gdyby uzależnić etykietę nie tylko od poprzedniej
etykiety, lecz również od jeszcze wcześniejszej, to złożoność
obliczeniowa byłaby taka sama?
*** Przykład
Rozpatrzmy uproszczony przykład tagowania częściami mowy:
- słownik $V=\{\mathit{Ala}, \mathit{powieść}, \mathit{ma}\}$,
- zbiór etykiet $L=\{\mathit{C}, \mathit{P}, \mathit{R}\}$,
- kontekst nie jest uwzględniany ($c = 0$).
(To, że liczba słów i etykiet jest taka sama, jest przypadkowe, nie ma znaczenia)
Zakładamy, że słowa reprezentujemy wektorowo za pomocą prostej reprezentacji one-hot.
#+BEGIN_SRC ipython :session mysession :exports both :results raw drawer
import torch
vocab = ['Ala', 'ma', 'powieść']
labels = ['C', 'P', 'R']
onehot = {
'Ala': torch.tensor([1., 0., 0.]),
'ma': torch.tensor([0., 1., 0.]),
'powieść': torch.tensor([0., 0., 1.])
}
onehot['ma']
#+END_SRC
#+RESULTS:
:results:
# Out[2]:
: tensor([0., 1., 0.])
:end:
Przyjmijmy, że w czasie uczenia zostały ustalone następujące wartości
macierzy $W$ i $V$ (samego procesu uczenia nie pokazujemy tutaj):
#+BEGIN_SRC ipython :session mysession :exports both :results raw drawer
import torch
import torch.nn as nn
matrixW = torch.tensor(
[[-1., 3.0, 3.0],
[0., 2.0, -2.0],
[4., -2.0, 3.0]])
# rozkład prawdopodobieństwa, gdyby patrzeć tylko na słowo
nn.functional.softmax(matrixW @ onehot['powieść'], dim=0)
#+END_SRC
#+RESULTS:
:results:
# Out[9]:
: tensor([0.4983, 0.0034, 0.4983])
:end:
#+BEGIN_SRC ipython :session mysession :exports both :results raw drawer
import torch
import torch.nn as nn
matrixV = torch.tensor(
[[-0.5, 1.5, 2.0],
[0.5, 0.8, 2.5],
[2.0, 0.8, 0.2]])
# co występuje po przymiotniku? - rozkład prawdopodobieństwa
nn.functional.softmax(matrixV[1], dim=0)
#+END_SRC
#+RESULTS:
:results:
# Out[10]:
: tensor([0.1027, 0.1386, 0.7587])
:end:
Algorytm Viterbiego:
#+BEGIN_SRC ipython :session mysession :exports both :results raw drawer
d = ['Ala', 'ma', 'powieść']
s = []
b = []
# inicjalizacja
s.append(matrixW @ onehot[d[0]])
b.append(None)
# wprzód
i = 1
os = []
ob = []
for j in range(0, len(labels)):
z = s[i-1] + matrixV[:,j] + matrixW @ onehot[d[i]]
ns = torch.max(z).item()
nb = torch.argmax(z).item()
os.append(ns)
ob.append(nb)
os
#+END_SRC
#+RESULTS:
:results:
# Out[16]:
: [4.0, 3.5, 4.5]
:end:

BIN
wyk/img-feed-forward.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB