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
** 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 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?
** 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
@ -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~)
*** *Pytanie*: czy zadanie tłumaczenia maszynowego można potraktować jako problem etykietowania sekwencji?
*** Przykładowe wyzwanie 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~)
*** Metryka F1
*** Etykietowanie za pomocą klasyfikacji wieloklasowej
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
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$.
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}$.
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
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
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
@ -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
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ą
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.
$$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