This commit is contained in:
Filip Gralinski 2022-04-23 11:52:15 +02:00
parent fa25bcc34d
commit 219d0fc56b
12 changed files with 746 additions and 0 deletions

338
wyk/07_Zanurzenia_slow.org Normal file
View File

@ -0,0 +1,338 @@
* Zanurzenia słów
W praktyce stosowalność słowosieci okazała się zaskakująco
ograniczona. Większy przełom w przetwarzaniu języka naturalnego przyniosły
wielowymiarowe reprezentacje słów, inaczej: zanurzenia słów.
** „Wymiary” słów
Moglibyśmy zanurzyć (ang. /embed/) w wielowymiarowej przestrzeni, tzn. zdefiniować odwzorowanie
$E \colon V \rightarrow \mathcal{R}^m$ dla pewnego $m$ i określić taki sposób estymowania
prawdopodobieństw $P(u|v)$, by dla par $E(v)$ i $E(v')$ oraz $E(u)$ i $E(u')$ znajdujących się w pobliżu
(według jakiejś metryki odległości, na przykład zwykłej odległości euklidesowej):
$$P(u|v) \approx P(u'|v').$$
$E(u)$ nazywamy zanurzeniem (embeddingiem) słowa.
*** Wymiary określone z góry?
Można by sobie wyobrazić, że $m$ wymiarów mogłoby być z góry
określonych przez lingwistę. Wymiary te byłyby związane z typowymi
„osiami” rozpatrywanymi w językoznawstwie, na przykład:
- czy słowo jest wulgarne, pospolite, potoczne, neutralne czy książkowe?
- czy słowo jest archaiczne, wychodzące z użycia czy jest neologizmem?
- czy słowo dotyczy kobiet, czy mężczyzn (w sensie rodzaju gramatycznego i/lub
socjolingwistycznym)?
- czy słowo jest w liczbie pojedynczej czy mnogiej?
- czy słowo jest rzeczownikiem czy czasownikiem?
- czy słowo jest rdzennym słowem czy zapożyczeniem?
- czy słowo jest nazwą czy słowem pospolitym?
- czy słowo opisuje konkretną rzecz czy pojęcie abstrakcyjne?
-
W praktyce okazało się jednak, że lepiej, żeby komputer uczył się sam
możliwych wymiarów — z góry określamy tylko $m$ (liczbę wymiarów).
** Bigramowy model języka oparty na zanurzeniach
Zbudujemy teraz najprostszy model język oparty na zanurzeniach. Będzie to właściwie najprostszy
*neuronowy model języka*, jako że zbudowany model można traktować jako prostą sieć neuronową.
*** Słownik
W typowym neuronowym modelu języka rozmiar słownika musi być z góry
ograniczony. Zazwyczaj jest to liczba rzędu kilkudziesięciu wyrazów —
po prostu będziemy rozpatrywać $|V|$ najczęstszych wyrazów, pozostałe zamienimy
na specjalny token ~<unk>~ reprezentujący nieznany (/unknown/) wyraz.
Aby utworzyć taki słownik użyjemy gotowej klasy ~Vocab~ z pakietu torchtext:
#+BEGIN_SRC python :session mysession :exports both :results raw drawer
from itertools import islice
import regex as re
import sys
from torchtext.vocab import build_vocab_from_iterator
def get_words_from_line(line):
line = line.rstrip()
yield '<s>'
for m in re.finditer(r'[\p{L}0-9\*]+|\p{P}+', line):
yield m.group(0).lower()
yield '</s>'
def get_word_lines_from_file(file_name):
with open(file_name, 'r') as fh:
for line in fh:
yield get_words_from_line(line)
vocab_size = 20000
vocab = build_vocab_from_iterator(
get_word_lines_from_file('opensubtitlesA.pl.txt'),
max_tokens = vocab_size,
specials = ['<unk>'])
vocab['jest']
#+END_SRC
#+RESULTS:
:results:
16
:end:
#+BEGIN_SRC python :session mysession :exports both :results raw drawer
vocab.lookup_tokens([0, 1, 2, 10, 12345])
#+END_SRC
#+RESULTS:
:results:
['<unk>', '</s>', '<s>', 'w', 'wierzyli']
:end:
*** Definicja sieci
Naszą prostą sieć neuronową zaimplementujemy używając frameworku PyTorch.
#+BEGIN_SRC python :session mysession :exports both :results raw drawer
from torch import nn
import torch
embed_size = 100
class SimpleBigramNeuralLanguageModel(nn.Module):
def __init__(self, vocabulary_size, embedding_size):
super(SimpleBigramNeuralLanguageModel, self).__init__()
self.model = nn.Sequential(
nn.Embedding(vocabulary_size, embedding_size),
nn.Linear(embedding_size, vocabulary_size),
nn.Softmax()
)
def forward(self, x):
return self.model(x)
model = SimpleBigramNeuralLanguageModel(vocab_size, embed_size)
vocab.set_default_index(vocab['<unk>'])
ixs = torch.tensor(vocab.forward(['pies']))
out[0][vocab['jest']]
#+END_SRC
#+RESULTS:
:results:
:end:
Teraz wyuczmy model. Wpierw tylko potasujmy nasz plik:
#+BEGIN_SRC
shuf < opensubtitlesA.pl.txt > opensubtitlesA.pl.shuf.txt
#+END_SRC
#+BEGIN_SRC python :session mysession :exports both :results raw drawer
from torch.utils.data import IterableDataset
import itertools
def look_ahead_iterator(gen):
prev = None
for item in gen:
if prev is not None:
yield (prev, item)
prev = item
class Bigrams(IterableDataset):
def __init__(self, text_file, vocabulary_size):
self.vocab = build_vocab_from_iterator(
get_word_lines_from_file(text_file),
max_tokens = vocabulary_size,
specials = ['<unk>'])
self.vocab.set_default_index(self.vocab['<unk>'])
self.vocabulary_size = vocabulary_size
self.text_file = text_file
def __iter__(self):
return look_ahead_iterator(
(self.vocab[t] for t in itertools.chain.from_iterable(get_word_lines_from_file(self.text_file))))
train_dataset = Bigrams('opensubtitlesA.pl.shuf.txt', vocab_size)
#+END_SRC
#+RESULTS:
:results:
:end:
#+BEGIN_SRC python :session mysession :exports both :results raw drawer
from torch.utils.data import DataLoader
next(iter(train_dataset))
#+END_SRC
#+RESULTS:
:results:
(2, 5)
:end:
#+BEGIN_SRC python :session mysession :exports both :results raw drawer
from torch.utils.data import DataLoader
next(iter(DataLoader(train_dataset, batch_size=5)))
#+END_SRC
#+RESULTS:
:results:
[tensor([ 2, 5, 51, 3481, 231]), tensor([ 5, 51, 3481, 231, 4])]
:end:
#+BEGIN_SRC python :session mysession :exports both :results raw drawer
device = 'cuda'
model = SimpleBigramNeuralLanguageModel(vocab_size, embed_size).to(device)
data = DataLoader(train_dataset, batch_size=5000)
optimizer = torch.optim.Adam(model.parameters())
criterion = torch.nn.NLLLoss()
model.train()
step = 0
for x, y in data:
x = x.to(device)
y = y.to(device)
optimizer.zero_grad()
ypredicted = model(x)
loss = criterion(torch.log(ypredicted), y)
if step % 100 == 0:
print(step, loss)
step += 1
loss.backward()
optimizer.step()
torch.save(model.state_dict(), 'model1.bin')
#+END_SRC
#+RESULTS:
:results:
None
:end:
Policzmy najbardziej prawdopodobne kontynuację dla zadanego słowa:
#+BEGIN_SRC python :session mysession :exports both :results raw drawer
device = 'cuda'
model = SimpleBigramNeuralLanguageModel(vocab_size, embed_size).to(device)
model.load_state_dict(torch.load('model1.bin'))
model.eval()
ixs = torch.tensor(vocab.forward(['dla'])).to(device)
out = model(ixs)
top = torch.topk(out[0], 10)
top_indices = top.indices.tolist()
top_probs = top.values.tolist()
top_words = vocab.lookup_tokens(top_indices)
list(zip(top_words, top_indices, top_probs))
#+END_SRC
#+RESULTS:
:results:
[('ciebie', 73, 0.1580502986907959), ('mnie', 26, 0.15395283699035645), ('<unk>', 0, 0.12862136960029602), ('nas', 83, 0.0410110242664814), ('niego', 172, 0.03281523287296295), ('niej', 245, 0.02104802615940571), ('siebie', 181, 0.020788608118891716), ('którego', 365, 0.019379809498786926), ('was', 162, 0.013852755539119244), ('wszystkich', 235, 0.01381855271756649)]
:end:
Teraz zbadajmy najbardziej podobne zanurzenia dla zadanego słowa:
#+BEGIN_SRC python :session mysession :exports both :results raw drawer
vocab = train_dataset.vocab
ixs = torch.tensor(vocab.forward(['kłopot'])).to(device)
out = model(ixs)
top = torch.topk(out[0], 10)
top_indices = top.indices.tolist()
top_probs = top.values.tolist()
top_words = vocab.lookup_tokens(top_indices)
list(zip(top_words, top_indices, top_probs))
#+END_SRC
#+RESULTS:
:results:
[('.', 3, 0.404473215341568), (',', 4, 0.14222915470600128), ('z', 14, 0.10945753753185272), ('?', 6, 0.09583134204149246), ('w', 10, 0.050338443368673325), ('na', 12, 0.020703863352537155), ('i', 11, 0.016762692481279373), ('<unk>', 0, 0.014571071602404118), ('...', 15, 0.01453721895813942), ('</s>', 1, 0.011769450269639492)]
:end:
#+BEGIN_SRC python :session mysession :exports both :results raw drawer
cos = nn.CosineSimilarity(dim=1, eps=1e-6)
embeddings = model.model[0].weight
vec = embeddings[vocab['poszedł']]
similarities = cos(vec, embeddings)
top = torch.topk(similarities, 10)
top_indices = top.indices.tolist()
top_probs = top.values.tolist()
top_words = vocab.lookup_tokens(top_indices)
list(zip(top_words, top_indices, top_probs))
#+END_SRC
#+RESULTS:
:results:
[('poszedł', 1087, 1.0), ('idziesz', 1050, 0.4907470941543579), ('przyjeżdża', 4920, 0.45242372155189514), ('pojechałam', 12784, 0.4342481195926666), ('wrócił', 1023, 0.431664377450943), ('dobrać', 10351, 0.4312002956867218), ('stałeś', 5738, 0.4258835017681122), ('poszła', 1563, 0.41979148983955383), ('trafiłam', 18857, 0.4109022617340088), ('jedzie', 1674, 0.4091658890247345)]
:end:
*** Zapis przy użyciu wzoru matematycznego
Powyżej zaprogramowaną sieć neuronową można opisać następującym wzorem:
$$\vec{y} = \operatorname{softmax}(CE(w_{i-1}),$$
gdzie:
- $w_{i-1}$ to pierwszy wyraz w bigramie (poprzedzający wyraz),
- $E(w)$ to zanurzenie (embedding) wyrazy $w$ — wektor o rozmiarze $m$,
- $C$ to macierz o rozmiarze $|V| \times m$, która rzutuje wektor zanurzenia w wektor o rozmiarze słownika,
- $\vec{y}$ to wyjściowy wektor prawdopodobieństw o rozmiarze $|V|$.
**** Hiperparametry
Zauważmy, że nasz model ma dwa hiperparametry:
- $m$ — rozmiar zanurzenia,
- $|V|$ — rozmiar słownika, jeśli zakładamy, że możemy sterować
rozmiarem słownika (np. przez obcinanie słownika do zadanej liczby
najczęstszych wyrazów i zamiany pozostałych na specjalny token, powiedzmy, ~<UNK>~.
Oczywiście możemy próbować manipulować wartościami $m$ i $|V|$ w celu
polepszenia wyników naszego modelu.
*Pytanie*: dlaczego nie ma sensu wartość $m \approx |V|$ ? dlaczego nie ma sensu wartość $m = 1$?
*** Diagram sieci
Jako że mnożenie przez macierz ($C$) oznacza po prostu zastosowanie
warstwy liniowej, naszą sieć możemy interpretować jako jednowarstwową
sieć neuronową, co można zilustrować za pomocą następującego diagramu:
#+CAPTION: Diagram prostego bigramowego neuronowego modelu języka
[[./07_Zanurzenia_slow/bigram1.drawio.png]]
*** Zanurzenie jako mnożenie przez macierz
Uzyskanie zanurzenia ($E(w)$) zazwyczaj realizowane jest na zasadzie
odpytania (_look-up_). Co ciekawe, zanurzenie można intepretować jako
mnożenie przez macierz zanurzeń (embeddingów) $E$ o rozmiarze $m \times |V|$ — jeśli słowo będziemy na wejściu kodowali przy użyciu
wektora z gorącą jedynką (_one-hot encoding_), tzn. słowo $w$ zostanie
podane na wejściu jako wektor $\vec{1_V}(w) = [0,\ldots,0,1,0\ldots,0]$ o rozmiarze $|V|$
złożony z samych zer z wyjątkiem jedynki na pozycji odpowiadającej indeksowi wyrazu $w$ w słowniku $V$.
Wówczas wzór przyjmie postać:
$$\vec{y} = \operatorname{softmax}(CE\vec{1_V}(w_{i-1})),$$
gdzie $E$ będzie tym razem macierzą $m \times |V|$.
*Pytanie*: czy $\vec{1_V}(w)$ intepretujemy jako wektor wierszowy czy kolumnowy?
W postaci diagramu można tę interpretację zilustrować w następujący sposób:
#+CAPTION: Diagram prostego bigramowego neuronowego modelu języka z wejściem w postaci one-hot
[[./07_Zanurzenia_slow/bigram2.drawio.png]]

View File

@ -0,0 +1,403 @@
* Neuronowy n-gramowy model języka
Omówiony w poprzedniej części neuronowy bigramowy model języka
warunkuje kolejny wyraz jedynie względem bezpośrednio poprzedzającego
— jak w każdym bigramowym modelu przyjmujemy założenie, że $w_i$
zależy tylko od $w_{i-1}$. Rzecz jasna jest to bardzo duże
ograniczenie, w rzeczywiście bardzo często prawdopodobieństwo
kolejnego wyrazu zależy od wyrazu dwie, trzy, cztery itd. pozycje
wstecz czy w ogólności od wszystkich wyrazów poprzedzających (bez
względu na ich pozycje).
*Pytanie*: Wskaż zależności o zasięgu większym niż 1 wyraz w zdaniu
_Zatopieni w kłębach dymu cygar i pochyleni nad butelkami z ciemnego
szkła obywatele tej dzielnicy, jedni zakładali się o wygranę lub
przegranę Anglii, drudzy o bankructwo Wokulskiego; jedni nazywali
geniuszem Bismarcka, drudzy — awanturnikiem Wokulskiego; jedni
krytykowali postępowanie prezydenta MacMahona, inni twierdzili, że
Wokulski jest zdecydowanym wariatem, jeżeli nie czymś gorszym…_
** Trigramowy neuronowy model języka
Spróbujmy najpierw rozszerzyć nasz model na trigramy, to znaczy
będziemy przewidywać słowo $w_i$ na podstawie słów $w_{i-2}$ i
$w_{i-1}$.
Najprostsze rozwiązanie polegałoby na zanurzeniu pary $(w_{i-2},
w_{i-1})$ w całości i postępowaniu jak w przypadku modelu bigramowego.
Byłoby to jednak zupełnie niepraktyczne, jako że:
- liczba zanurzeń do wyuczenia byłaby olbrzymia ($|V|^2$ — byłoby to
ewentualnie akceptowalne dla modeli operujących na krótszych
jednostkach niż słowno, np. na znakach),
- w szczególności zanurzenia dla par $(v, u)$, $(u, v)$, $(u, u)$ i
$(v, v)$ nie miałyby ze sobą nic wspólnego.
*** Konketanacja zanurzeń
Właściwsze rozwiązanie polega na zanurzenia dalej pojedynczych słów i
następnie ich *konkatenowaniu*.
Przypomnijmy, że konkatenacja wektorów $\vec{x_1}$ i $\vec{x_2}$ to wektor o rozmiarze
$|\vec{x_1}| + |\vec{x_2}|$ powstały ze „sklejania” wektorów $\vec{x_1}$ i $\vec{x_2}$.
Konkatenację wektorów $\vec{x_1}$ i $\vec{x_2}$ będziemy oznaczać za pomocą $[\vec{x_1}, \vec{x_2}]$.
Przykład: jeśli $\vec{x_1} = [-1, 2, 0]$ i $\vec{x_2} = [3, -3]$
wówczas $[\vec{x_1}, \vec{x_2}] = [-1, 2, 0, 3, -3]$
Oznacza to, że nasza macierz „kontekstowa” $C$ powinna mieć w modelu trigramowy rozmiar nie
$|V| \times m$, lecz $|V| \times m+m$ = $|V| \times 2m$ i wyjście będę zdefiniowane za pomocą wzoru:
$$\vec{y} = \operatorname{softmax}(C[E(w_{i-2}),E(w_{i-1})]),$$
co można przedstawić za pomocą następującego schematu:
#+CAPTION: Diagram prostego bigramowego neuronowego modelu języka
[[./08_Neuronowy_ngramowy_model/trigram1.drawio.png]]
**** Rozbicie macierzy $C$
Zamiast mnożyć macierz $C$ przez konkatenację dwóch wektorów, można
rozbić macierz $C$ na dwie, powiedzmy $C_{-2}$ i $C_{-1}$, przemnażać
je osobno przez odpowiadający im wektory i następnie _dodać_ macierze,
tak aby:
$$C[E(w_{i-2}),E(w_{i-1})] = C_{-2}E(w_{i-2}) + C_{-1}E(w_{i-1}).$$
Macierze $C_{-2}$ i $C_{-1}$ będą miały rozmiar $|V| \times m$.
Przy tym podejściu możemy powiedzieć, że ostatni i przedostatni wyraz
mają swoje osobne macierze o potencjalnie różnych wagach — co ma sens,
jako że na inne aspekty zwracamy uwagę przewidując kolejne słowo na
podstawie wyrazu bezpośrednio poprzedzającego, a na inne — na
podstawie słowa występującego dwie pozycje wcześniej.
** Uogólnienie na $n$-gramowy model języka dla dowolnego $n$
Łatwo uogólnić opisany wyżej trigramowy model języka dla dowolnego $n$.
Uogólniony model można przedstawić za pomocą wzoru:
$$\vec{y} = \operatorname{softmax}(C[E(w_{i-n+1}),\dots,E(w_{i-1})]),$$
gdzie macierz $C$ ma rozmiar $|V| \times nm$ lub za pomocą wzoru:
$$\vec{y} = \operatorname{softmax}(C_{-(i-n+1)}E(w_{i-n+1}) + \dots + C_{-1}E(w_{i-1}),$$
gdzie macierze $C_{-(i-n+1)$, \ldots, $C_{-1}$ mają rozmiary $|V| \times m$.
Por. diagram:
#+CAPTION: Diagram prostego n-gramowego neuronowego modelu języka
[[./08_Neuronowy_ngramowy_model/ngram.drawio.png]]
** Dodanie kolejnej warstwy
W wypadku trigramowego czy — ogólniej — n-gramowego modelu języka dla
$n \geq 3$ warto dodać kolejną (*ukrytą*) warstwę, na którą będziemy rzutować
skonkatenowane embeddingi, zanim zrzutujemy je do długiego wektora
prawdopodobieństw.
Zakładamy, że warstwa ukryta zawiera $h$ neuronów. Wartość $h$ powinna być mniejsza
niż $nm$ (a może nawet od $m$).
*Pytanie*: Dlaczego wartość $h > nm$ nie jest racjonalnym wyborem?
*Pytanie*: Dlaczego dodanie kolejnej warstwy nie ma sensu dla modelu bigramowego?
*** Funkcja aktywacji
Aby warstwa ukryta wnosiła coś nowego, na wyjściu z tej funkcji musimy
zastosować nieliniową *funkcji aktywacji*. Zazwyczaj jako funkcji
aktywacji w sieciach neuronowych używa się funkcji ReLU albo funkcji
sigmoidalnej. W prostych neuronowych modelach języka sprawdza się też
*tangens hiperboliczny* (tgh, w literaturze anglojęzycznej tanh):
$$\operatorname{tgh}(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}}.$$
#+BEGIN_SRC python :session mysession :results file
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
x = torch.linspace(-5,5,100)
plt.xlabel("x")
plt.ylabel("y")
a = torch.Tensor(x.size()[0]).fill_(2.)
m = torch.stack([x, a])
plt.plot(x, nn.functional.tanh(m)[0])
fname = '08_Neuronowy_ngramowy_model/tanh.png'
plt.savefig(fname)
fname
#+END_SRC
#+RESULTS:
[[file:08_Neuronowy_ngramowy_model/tanh.png]]
**** Tangens hiperboliczny zastosowany dla wektora
Tangens hiperboliczny wektora będzie po prostu wektorem tangensów
hiperbolicznych poszczególnych wartości.
#+BEGIN_SRC python :session mysession :results file
import torch
import torch.nn as nn
v = torch.Tensor([-100, -2.0, 0.0, 0.5, 1000.0])
nn.functional.tanh(v)
#+END_SRC
#+RESULTS:
[[file:tensor([-1.0000, -0.9640, 0.0000, 0.4621, 1.0000])]]
*** Wzór i schemat dwuwarstwowego n-gramowego neuronowego modelu języka
Dwuwarstwowy model języka będzie określony następującym wzorem:
$$\vec{y} = \operatorname{softmax}(C\operatorname{tgh}(W[E(w_{i-n+1}),\dots,E(w_{i-1})])),$$
gdzie:
- $W$ jest wyuczalną macierzą wag o rozmiarze $h \times nm$,
- $C$ będzie macierzą o rozmiarze $|V| \times h$.
Zmodyfikowaną się można przedstawić za pomocą następującego schematu:
#+CAPTION: Dwuwarstwowy n-gramowy neuronowy model języka
[[./08_Neuronowy_ngramowy_model/ngram-tgh.drawio.png]]
*** Liczba wag w modelu dwuwarstwowym
Na wagi w modelu dwuwarstwowym składają się:
- zanurzenia: $m|V|$,
- wagi warstwy ukrytej: $hnm$,
- wagi warstwy wyjściowej: $|V|h$,
a zatem łącznie:
$$m|V| + hnm + |V|h$$
Jeśli $h \approx m$ (co jest realistyczną opcją), wówczas otrzymamy oszacowanie:
$$O(m|V| + nm^2).$$
Zauważmy, że względem $n$ oznacza to bardzo korzystną złożoność
$O(n)$! Oznacza to, że nasz model może działać dla dużo większych
wartości $n$ niż tradycyjny n-gramowy model języka (dla którego
wartości $n > 5$ zazwyczaj nie mają sensu).
** Model worka słów
Jak stwierdziliśmy przed chwilą, dwuwarstwowy n-gramowym modelu języka
może działać dla stosunkowo dużego $n$. Zauważmy jednak, że istnieje
pewna słabość tego modelu. Otóż o ile intuicyjnie ma sens odróżniać
słowo poprzedzające, słowo występujące dwie pozycje wstecz i zapewne
trzy pozycje wstecz, a zatem uczyć się osobnych macierzy $C_{-1}$,
$C_{-2}$, $C_{-3}$ to zapewne różnica między wpływem słowa
występującego cztery pozycje wstecz i pięć pozycji wstecz jest już
raczej nieistotna; innymi słowy różnica między macierzami $C_{-4}$ i
$C_{-5}$ będzie raczej niewielka i sieć niepotrzebnie będzie uczyła
się dwukrotnie podobnych wag. Im dalej wstecz, tym różnica wpływu
będzie jeszcze mniej istotna, można np. przypuszczać, że różnica
między $C_{-10}$ i $C_{-13}$ nie powinna być duża.
Spróbujmy najpierw zaproponować radykalne podejście, w którym nie
będziemy w ogóle uwzględniać pozycji słów (lub będziemy je uwzględniać
w niewielkim stopniu), później połączymy to z omówionym wcześniej
modelem $n$-gramowym.
*** Agregacja wektorów
Zamiast patrzeć na kilka poprzedzających słów, można przewidywać na
cały ciąg słów poprzedzających przewidywane słowo. Zauważmy jednak, że
sieć neuronowa musi mieć ustaloną strukturę, nie możemy zmieniać jej
rozmiaru. Musimy zatem najpierw zagregować cały ciąg do wektora o
*stałej* długości. Potrzebujemy zatem pewnej funkcji agregującej $A$, takiej by
$A(w_1,\dots,w_{i-1})$ było wektorem o stałej długości, niezależnie od $i$.
*** Worek słów
Najprostszą funkcją agregującą jest po prostu… suma. Dodajemy po
prostu zanurzenia słów:
$$A(w_1,\dots,w_{i-1}) = E(w_1) + \dots + E(w_{i-1}) = \sum_{j=1}^{i-1} E(w_j).$$
*Uwaga*: zanurzenia słów nie zależą od pozycji słowa (podobnie było w wypadku n-gramowego modelu!).
Jeśli rozmiar zanurzenia (embeddingu) wynosi $m$, wówczas rozmiar
wektora uzyskanego dla całego poprzedzającego tekstu wynosi również $m$.
Proste dodawanie wydaje się bardzo „prostacką” metodą, a jednak
dodawanie wektorów słów jest *zaskakująco skuteczną metodą zanurzenia
(embedowania) całych tekstów (doc2vec)*. Prostym wariantem dodawania jest obliczanie *średniej wektorów*:
$$A(w_1,\dots,w_{i-1}) = \frac{E(w_1) + \dots + E(w_{i-1})}{i-1} = \frac{\sum_{j=1}^{i-1} E(w_j)}{i-1}.$$
Tak czy siak uzyskany wektor *nie zależy od kolejności słów*
(dodawanie jest przemienne i łączne!). Mówimy więc o *worku słów*
(/bag of words/, /BoW/) — co ma symbolizować fakt, że słowa są
przemieszane, niczym produkty w torbie na zakupy.
**** Schemat graficzny modelu typu worek słów
Po zanurzeniu całego poprzedzającego tekstu postępujemy podobnie jak w
modelu bigramowym — rzutujemy embedding na długi wektor wartości, na
którym stosujemy funkcję softmax:
#+CAPTION: Model typu worek słów
[[./08_Neuronowy_ngramowy_model/bow1.drawio.png]]
Odpowiada to wzorowi:
$$y = \operatorname{softmax}(C\sum_{j=1}^{i-1} E(w_j)).$$
*** Jak traktować powtarzające się słowa?
Według wzoru podanego wyżej, jeśli słowo w poprzedzającym tekście
pojawia się więcej niż raz, jego embedding zostanie zsumowany odpowiednią liczbę razy.
Na przykład embedding tekstu _to be or not to be_ będzie wynosił:
$$E(\mathrm{to}) + E(\mathrm{be}) + E(\mathrm{or}) + E(\mathrm{not}) + E(\mathrm{to}) + E(\mathrm{be}) = 2E(\mathrm{to}) + 2E(\mathrm{be}) + E(\mathrm{or}) + E(\mathrm{not}).$$
Innymi słowy, choć w worku słów nie uwzględniamy kolejności słów, to
*liczba wystąpień* ma dla nas znaczenie. Można powiedzieć, że
traktujemy poprzedzający tekst jako *multizbiór* (struktura
matematyczna, w której nie uwzględnia się kolejności, choć zachowana
jest informacja o liczbie wystąpień).
**** Zbiór słów
Oczywiście moglibyśmy przy agregowaniu zanurzeń pomijać powtarzające
się słowa, a zatem zamiast multizbioru słów rozpatrywać po prostu ich zbiór:
$$A(w_1,\dots,w_{i-1}) = \sum_{w \in \{w_1,\dots,w_{i-1}\}} E(w).$$
Jest kwestią dyskusyjną, czy to lepsze czy gorsze podejście — w końcu
liczba wystąpień np. słów _Ukraina_ czy _Polska_ może wpływać w jakimś
stopniu na prawdopodobieństwo kolejnego słowa (_Kijów_ czy
_Warszawa_?).
*** Worek słów a wektoryzacja tf
Wzór na sumę zanurzeń słów można przekształcić w taki sposób, by
sumować po wszystkich słowach ze słownika, zamiast po słowach rzeczywiście występujących w tekście:
$$A(w_1,\dots,w_{i-1}) = \sum_{j=1}^{i-1} E(w_j) = \sum_{w \in V} \#wE(w)$$
gdzie $\#w$ to liczba wystąpień słowa $w$ (w wielu przypadkach równa zero!).
Jeśli teraz zanurzenia będzie reprezentować jako macierz $E$ (por. poprzedni wykład),
wówczas sumę można przedstawić jako iloczyn macierzy $E$ i pewnego wektora:
$$A(w_1,\dots,w_{i-1}) = E(w) [\#w^1,\dots,\#w^{|V|}]^T.$$
(Odróżniamy $w^i$ jako $i$-ty wyraz w słowniku $V$ od $w_i$ jako $i$-ty wyraz w ciągu).
Zwróćmy uwagę, że wektor $[\#w_1,\dots,\#w_{|V|}]$ to po prostu
reprezentacja wektora poprzedzającego tekstu (tj. ciągu
$(w_1,\dots,w_{i-1})$) przy użyciu schematu wektoryzacji tf (_term
frequency_). Przypomnijmy, że tf to reprezentacja tekstu przy użyciu
wektorów o rozmiarze $|V|$ — na każdej pozycji odnotowujemy liczbę wystąpień.
Wektory tf są *rzadkie*, tj. na wielu pozycjach zawierają zera.
Innymi słowy, nasz model języka _bag of words_ można przedstawić za pomocą wzoru:
$$y = \operatorname{softmax}(C\operatorname{tf}(w_1,\dots,w_{i-1})),$$
co można zilustrować w następujący sposób:
#+CAPTION: Model typu worek słów — alternatywna reprezentacja
[[./08_Neuronowy_ngramowy_model/bow2.drawio.png]]
Można stwierdzić, że zanurzenie tekstu przekształca rzadki, długi wektor
tf w gęsty, krótki wektor.
** Ważenie słów
Czy wszystkie słowa są tak samo istotne? Rzecz jasna, nie:
- jak już wiemy z naszych rozważań dotyczących n-gramowych modeli języka, słowa bezpośrednio
poprzedzające odgadywany wyraz mają większy wpływ niż słowa wcześniejsze;
intuicyjnie, wpływ słów stopniowo spada — tym bardziej, im bardziej słowo jest oddalone od słowa odgadywanego;
- jak wiemy z wyszukiwania informacji, słowa, które występują w wielu tekstach czy dokumentach, powinny mieć
mniejsze znaczenie, w skrajnym przypadku słowa występujące w prawie każdym tekście (_że_, _w_, _i_ itd.) powinny
być praktycznie pomijane jako stop words (jeśli rozpatrywać je w „masie” worka słów — oczywiście
to, czy słowo poprzedzające odgadywane słowo to _że_, _w_ czy _i_ ma olbrzymie znaczenie!).
Zamiast po prostu dodawać zanurzenia, można operować na sumie (bądź średniej) ważonej:
$$\sum_{j=1}^{i-1} \omega(j, w_j)E(w_j), gdzie$$
$\omega(j, w_j)$ jest pewną wagą, która może zależeć od pozycji $j$ lub samego słowa $w_j$.
*** Uwzględnienie pozycji
Można w pewnym stopniu złamać „workowatość” naszej sieci przez proste
uwzględnienie pozycji słowa, np. w taki sposób:
$$\omega(j, w_j) = \beta^{i-j-1},$$
dla pewnego hiperparametru $\beta$. Na przykład jeśli $\beta=0,9$,
wówczas słowo bezpośrednio poprzedzające dane słowo ma $1 / 0,9^9 \approx 2.58$
większy wpływ niż słowo występujące 10 pozycji wstecz.
*** Odwrócona częstość dokumentowa
Aby większą wagę przykładać do słów występujących w mniejszej liczbie
dokumentów, możemy użyć, znanej w wyszukiwaniu informacji
odwrotnej częstości dokumentowej (_inverted document frequency_, _idf_):
$$\omega(j, w_j) = \operatorname{idf}_S(w_j) = \operatorname{log}\frac{|S|}{\operatorname{df}_S(w_j)},$$
gdzie:
- $S$ jest pewną kolekcją dokumentów czy tekstów, z którego pochodzi przedmiotowy ciąg słów,
- $\operatorname{df}_S(w)$ to częstość dokumentowa słowa $w$ w kolekcji $S$, tzn. odpowiedź na pytanie,
w ilu dokumentach występuje $w$.
Rzecz jasna, ten sposób ważenia oznacza tak naprawdę zastosowanie wektoryzacji tf-idf zamiast tf,
nasza sieć będzie dana wzorem:
$$y = \operatorname{softmax}(C\operatorname{tfidf}(w_1,\dots,w_{i-1})).$$
*** Bardziej skomplikowane sposoby ważenia słów
Można oczywiście połączyć odwrotną częstość dokumentową z uwzględnieniem pozycji słowa:
$$\omega(j, w_j) = \beta^{i-j-1}\operatorname{idf}_S(w_j).$$
*Uwaga*: „wagi” $\omega(j, w_j)$ nie są tak naprawdę wyuczalnymi
wagami (parametrami) naszej sieci neuronowej, terminologia może być
tutaj myląca. Z punktu widzenia sieci neuronowej $\omega(j, w_j)$ są
stałe i są optymalizowane w procesie propagacji wstecznej. Innymi
słowy, tak zdefiniowane $\omega(j, w_j)$ zależą tylko od:
- hiperparametru $\beta$, który może być optymalizowany już poza siecią (w procesie *hiperoptymalizacji*),
- wartości $\operatorname{idf}_S(w_j)$ wyliczanych wcześniej na podstawie kolekcji $S$.
*Pytanie*: czy wagi $\omega(j, w_j)$ mogłyby sensownie uwzględniać
jakieś parametry wyuczalne z całą siecią?
** Modelowanie języka przy użyciu bardziej złożonych neuronowych sieci _feed-forward_
Można połączyć zalety obu ogólnych podejść (n-gramowego modelu i worka
słów) — można _równocześnie_ traktować w specjalny sposób (na
przykład) dwa poprzedzające wyrazy, zaś wszystkie inne wyrazy
reprezentować jako „tło” modelowane za pomocą worka słów lub podobnej
reprezentacji. Osiągamy to poprzez konkatenację wektora
poprzedzającego słowa, słowa występującego dwie pozycje wstecz oraz
zagregowanego zanurzenia całego wcześniejszego tekstu:
$$y = \operatorname{softmax}(C[E(w_{i-1}),E(w_{i-2}),A(w_1,\dots,w_{i-3})]),$$
lepiej z dodatkową warstwą ukrytą:
$$y = \operatorname{softmax}(C\operatorname{tg}(W[E(w_{i-1}),E(w_{i-2}),A(w_1,\dots,w_{i-3})])),$$
W tak uzyskanym dwuwarstwowym neuronowym modelu języka, łączącym model
trigramowy z workiem słów, macierz $W$ ma rozmiar $h \times 3m$.
** Literatura
Skuteczny n-gramowy neuronowy model języka opisano po raz pierwszy
w pracy [[https://www.jmlr.org/papers/volume3/bengio03a/bengio03a.pdf][A Neural Probabilistic Language Model]] autorstwa Yoshua Bengio i in.

View File

@ -0,0 +1 @@
<mxfile host="app.diagrams.net" modified="2022-04-23T08:10:01.287Z" agent="5.0 (X11)" etag="DuwX-avzVRfuxOlr24Vt" version="17.4.6" type="device"><diagram id="UNyY5TJjaPirS19H5Hif" name="Page-1">7VpZd9o4GP01PJLjDWM/BgKZmaaTtjmnU+alR8XCVjCWI4sY8utHQjLexNKwk3kB6Wr/tvvJdsPsTmb3BMTBZ+zBsGFo3qxh3jUMw7V09suBuQB027UE4hPkSSwHntAblKAm0SnyYFLqSDEOKYrL4BBHERzSEgYIwWm52wiH5VVj4MMa8DQEYR39B3k0EKhjtHP8D4j8IFuZHVC0TEDWWU6RBMDDqYAWhzN7DbNLMKaiNJl1YciFl8lFSKC/onW5MQIjutWAbu/xCwqcTz//fvZerAc4TN6aUhkJnWcHhh47v6xiQgPs4wiEvRztEDyNPMhn1Vgt7/OAccxAnYHPkNK5VCaYUsyggE5C2QpniP7gw29asjaQk/Hy3axYmWeViJL5j2JlkM/Aq/mwRS0bN8IRlRvhIu+I8/JDrhRjpi88JUPZ6+vj5/Txfjy+f/gz+vbS96nuPDQNaY6A+JCukbG+VDbzEognkO2PjSMwBBS9lvcBpLn6y365RllBKlWt4HWbfAXhVK5U03iuT66cNEAUPsVgcfSUOXVZdyvF9QoJhbMCVD+xbDU06dsyJpiurKe5g+mmxIKCc2XY3oVkKoRkh2zVjodeWdGni5MzZ7HSn412BzGxtu8EkPVkC5c6V4TMZEPLkkwowWPYxSEmDIlwxH1rhMKwAoEQ+RGrDpnEIcM7XNKIRahb2TBBnrdwTJXqys66B+1ZFe1ZTl179jGV515KCHt/pLHrkUbZz1FGt1OFHvv8Qo/ulo1Xb9WNd2nQR7Fe/QIZOCfdQTbfGgZ+v9k7W5q9bp6V3Tvnb/enN3ttJeWijENDjMfNaVwgWHT99GrrFXq1Tkyvej2DTPCITsDsepVgVZSga4oMVTuqFrZKUQl+Gze6rYZjAI/LgIDUwzH+heACtRKaAn4ZD/nvGHCw0/Wgjxv87ijSW8TLrS6K2P/39SmuIkUW0C9SRT5Kdmw4BmPHou0sQ13Bdgy3dRjbUd4/zYvLMJYpxTHv+EPsPPe/uoM++itwv7eM3sTrZLI7mzv+uk2eU8LR0k94x1cK6ePm2ftLi+XQLxixJXJdt91yytKu6FB4mRxVfEK5aSKjMpFww9pEt4SAeaFbzDskv7FhS/u9fZX7s4LYQW6cS+G+315Vt+f1z6T0D/tMqmW1K6x7zKdSSkKoh+ArCzf7Id30SXvRSWwPevff/gWfsNW3ps3sPc+5kO66TZ4T6Zp22QmOSrpKIV1e6nkppGsZbjXkvZd2FVMdiHgVK22g3o0jdiZfpd3Wyffm5uZ6+dPcIls/FHsq5d9WBFeR6ehZhnOtujDM02UySl04/8fwA8XwpR53jeC1iQ4Uv2vrbIjeG/rvHLuVWWHrUux1Pxn0QTLjFWRsVmhC287M9qVZVcotLrpclCWd2y9TnDU0k4WQb1kHZuKzvDG/GBefU4u5VlyZ2S5RnKwig4JBgCQWn6ON0IwbVmcFDyj0u/VrCdOsU4OloAbrUJfc9qW42mkvuUrZbftNh72jK++k4MN8wrGTE1QvuUb7iJdcpZDq7/tTQOSrthBFCPPSGaeru8UkqxyTDMXrrn29KmXV/HNZQSr5R8dm7z8=</diagram></mxfile>

View File

@ -0,0 +1 @@
<mxfile host="app.diagrams.net" modified="2022-04-23T09:02:43.180Z" agent="5.0 (X11)" etag="6a48B_aLc62Epg4yxUQr" version="17.4.6" type="device"><diagram id="UNyY5TJjaPirS19H5Hif" name="Page-1">5Vpdc+I2FP01nmkfkkGyAftxcYC2m22ym5lt05cdrS1sgbGIEDHw6ythGX/IEDYECOQBkI6vJOueo6srDYbpjud9hibhF+rjyIANf26YNwaEjgXEtwQWKQBajpUiASO+wnLggSyxAhsKnREfT0uGnNKIk0kZ9GgcY4+XMMQYTcpmAxqVR52gAGvAg4ciHf2H+DxMURu2c/wPTIIwG1lMMH0yRpmx6mIaIp8mKbSanNk1TJdRytPSeO7iSDov80vqgd6Gp+sXYzjmuzT4evclueuPRv3bP+NvT72AA/v2ykx7eUbRTE3YgK1I9NfxybMoBrJoQMGZlfww2h1yBY32TQpklmLIkvFqsnyReZDjucRDPo4EAERxyhkdYZdGlAkkprGw7AxIFFUgFJEgFlVPzBALvPOMGSeCm0/qwZj4vhymk4SE44cJ8uSYiVCiwBidxT6Wk2+sX6voLeVA2SeeFyDlvT6mY8zZQpiop1ZLaVJp2bJVPcmFkZmEBU2YCkNKisG655wtUVCE/QJ5juZr7AvxqiplPKQBjVHUzdGKV3KbW0onip4h5nyhViKacVomD88J/1c2v26q2qPqTJZv5sXK4iXnT+mMeXjLFFtqySMWYL7Fzk7t5Py3UslwhDh5Li/uNyemVbOqKkzlPIAN+i34/A3EC5yyeEFTF+9a0EdRL7DORb7C62xRaCSrj1l/spI3W9X2l729o+yB+a50b79/3Z9e9lBz0pQO+BjNL3jnBJXg09BZWGPHYWGnxIfR5chwm4YNkS99wFDi0wn9SfAKtaY8QTK5jeT3CEmw4/o4oIbMxdKkichy0yWx+P2+PXGqSbxS6CerIh8l5xKZtoi5Re1cAV070GkeRjsetYe9r85jj/wVOt+bsDv2O7W7+/acGXzYnLlptSv8HTNrTh4aT4BNWo/d/rf/0Gdq9axZduQ5q7TDcpxfSTxWtXvMiPCYFMF2NivJQzntAG737p6E9ucffw/9J+sWe9NlevDYI8lQTe8pEa+S7xLQqWqlXRFBmjipdsWz7stdwUpX6SS1rj4xhhYFs4k0mGqiW8/69TrUw8j19fXlRgKzkgOYzonjQLsmjKcxG2Sx+lK5gObpYnJtQGmeS0we0JirDqWDOxsi6AtnvmMGVbOy7Bq7xcGXO6qKYUNsfkW4rHVP+1wk8trbggNKa9dbtNaeEtwrBhzm0kwLijUe3LxntcpxErZrzq3mMQOlfsOSIKaOoRGJCV2VfnN/f9eb116kWFY5EMGaAyGAxyTF0UgRI7nFD6jU5QdusKmzPdQnG2uVebqNky+46m2dWZOYHOy6rpbbTFxnlKbvF/Iq2/zJU0MANAISPOKULZbIG8poxweXS4el35+YOiEHuzutZ0S/wV6ieMaWOCZY4EM0khegtRtT9wNtTGYNU/C4TOm33GyJ/JG8k04X0eWyUT2u1MSxIy8bSyMjMFzLcBzh/o9HSG0u/UaEiGr+v5r0AJr/O8ns/g8=</diagram></mxfile>

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -0,0 +1 @@
<mxfile host="app.diagrams.net" modified="2022-04-23T07:25:35.437Z" agent="5.0 (X11)" etag="wQ0k74k1QtIDXs4Ob3ey" version="17.4.6" type="device"><diagram id="UNyY5TJjaPirS19H5Hif" name="Page-1">7Vpbc5s4FP41PCZjxMXwWBzb6W4y3TQz3c2+dFSjgGoZUVkOdn99pSDMTb6kviVxZzKxdDi68J2PcxEYVm8yHzKYxrc0RMQAnXBuWFcGAKYNOuJHShZK4ttWLokYDpWsFNzjn0gJ1cBohkM0rSlySgnHaV04okmCRrwmg4zRrK72SEl91RRGqCW4H0HSlv6LQx7nUg90S/k1wlFcrGy6fn5lAgtlNcU0hiHNctHzzVl9w+oxSnnemsx7iEj0ClxyBAYrri43xlDCtxlw9+k2+zQcj4c3H5PPPwYRN72bC5DP8gTJTN2w2ixfFAgwOktCJCcxDSvIYszRfQpH8momjC5kMZ8Qdbm9KbXPJ8Q4mldEapNDRCeIs4VQUVeBpUyvKANs1c9K/M1CJ65gX8igMnm0nLpERTQUMC8AydKA5BKxahDiJ9GM+POd2+Iv+2p0Ayxg7V7lgkJTLFxTboAssOF1JKec0THqUUKZkCQ0EZrBIyakIYIER4nojgTiSMgDiTQWBP6gLkxwGMpltKYrjdvZj/WWT7yynmW1rece03h+C2sUiidcdSnjMY1oAkm/lDZQKXVuKE2Veb4jzhfKXcEZp3XjoTnm/8nhl47qPajJZPtqXu0sNoE/pTM2Qmtu0VV+EbII8TV6Xq4n73+tKRkikOOnugfcu2HcV+h6OnXyXjht8treMdlr2m+FvgJ1tqgMkt2HYj7ZKYc993anvbcl7U3rVfHeewO8N09P/M7KoIuLKEooHV/M0kqIxe8/wLp2Iz3yTxxgTbNlqQyyKc+gEBKcYCpb79UcdjNb7Wiy1c5R7dHO6af0kU/g/B1bATQd2OnNsFXVwOjPsdFzDA/AUILAYBbSlH7D6Flqq8coJPL/GEph0AtRRA15x3nFgWXb6eFE/H5ZX3VoqpZc9I01JedSsAAfiISlRp7l4UOFPMB3DkOeEfW+D+78hwH+K/a/OKA/CQNtxXniHMEx6ygdtSzXgnS+qfH+Mlk19B+KxRKlrb1GEQ8aNsxzczWqeua0YSLQbUyUJ++tiT4wBhcVtVQqTLffcJEUbb2vur5o5DsoybkE9/f5qit41x8jmWd7jOQ4DQYe8RiJumlggX7wlc5ur3vh3dXH61FxUHoqZwOO5W0armVjZZ7dd36YLHUf+sPP/8O/qT2wZxfFQX2lMteCeqw6fN0mX1OMtbwTxlgtSNafGHugGGvbfiPv/O0o257qUHFWs9KGSLtxxM6xVsvbdqy9vLx8v+HSah4KaRzHocKlFv/ui3MdUb0G55vvALfhC46Y72gN6P1x/Ady/MsD213dfnOiQzn91jobXP4G/Z0dvjaV7J4HX1+anq9Ou7dIz50d+b9TwXWY9LzlkTUIro6yTt1Jm5pzQrN7zKrUfSus3yN7nTZ7V39fcCr2Oi328ihuG4sQnE5XJRkVyOE0zT9ue8Rzabp9sLn5zkT3ylfDZftQXG6/F5+N2YLLFx9v633iblaxukW9VPUqzn68iuiWHxzmobf8btPq/wI=</diagram></mxfile>

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -0,0 +1 @@
<mxfile host="app.diagrams.net" modified="2022-04-23T06:52:34.125Z" agent="5.0 (X11)" etag="F7oHlS30BOIVpfAPOky4" version="17.1.3" type="device"><diagram id="UNyY5TJjaPirS19H5Hif" name="Page-1">7VpZc9owEP41fkwGn9iPhQA90mmTzKRNXzoqVmwV4XWFiKG/vhKW4zMYypUmfQHp0+r6dtlDg2b2p4sRQ3H4EXxMNaPjLzTzQjMMz9LFpwSWKaA7npUiASO+wnLghvzGCuwodE58PCsJcgDKSVwGxxBFeMxLGGIMkrLYPdDyrjEKcA24GSNaR78Qn4cp6hrdHH+LSRBmO4sLpiNTlAmrJWYh8iFJodXlzIFm9hkAT1vTRR9TSV7GS8rA8InRx4MxHPFNJlx9+ph8Gk0mo8t30fWvYcB19/LMSFd5QHSuLqwOy5cZAwzmkY/lIrpm9pKQcHwTo7EcTYTOBRbyKVXD9UOpcz5gxvGiAKlDjjBMMWdLIaJGDVOpXpmMYal+kvOvZzJhgfsMQ0rlwePSOSuioYjZgiSzgSSHil17PnkQzYCvbi4M20q+a90eEbR2L1IgkxQbl4QrJAtueJnJGWcwwX2gwAQSQSQke/eE0gqEKAki0R0LxrHAe5JpIgz4jRqYEt+X2zSqLlduZz/as4yy9kyzrj3nmMrzalxjX/zCVRcYDyGACNFBjlZYyWUuAWKlnp+Y86VyV2jOoaw8vCD8q5x+bqvenVpMti8Wxc6yjfwZzNkYr7mio/wiYgHma+TcVE7ef60qGaaIk4eyB9y7Ypzn53p0r2y8ul03Xss9pvXq1r9ivoJ1tixMkt27bD3Zyaeterubvbuh2evms7J79/nb/enNvvNkyCVZDKUAk7N5XAiw5OWHV8eqJEfeicOrrtc0lSA24wkSICURAdl6qeqwqrlqpyFX7RxVH/WMfgb3fIoWL1gLeiVsn14LG5UMDH5PtL6tuQbyJQcMJT7E8IPgFWqpX5FP5ecESbDX93EAmkyy03KDyLbdJ5H4vl1fcjSULCn0g1WR11KtiEJeZCtF23kMPQXbMTz7MLYzBvfn8Mq7G5L3oXdrG4Op32usNk+cH9h6maSjluSNJL3etHh/Waya+hmI2CLXtVsp4I2KDtO8XM0qvje1LGR0KwuliXttoTeMoWVBLJYCs80PnKVEG5+rLC8a6Qly43wk9+/ttanYXf+EpL/aJyTbrljgEZ+QkpvOL53Fzt1gdP0NfQBraM3P7NM6G2PbN6SKi2itrhsvnT22t1XXzo5eaCfN1IuPfcTKmqk2MPik9ZruCWNlI0nm64iV7Va/91hpWV4lffzraFlf6lDxsmGnlojZOmPnmNlot/WYeX5+/qzD3m6Oo/q00+A4jhr2ulvnLKII7f3TectOCjScii84dd7i/nf8B3L8hrenIqm60KGcfm2fFpffIr+twxfd/I8OqXj+dxFz8Ac=</diagram></mxfile>

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -0,0 +1 @@
<mxfile host="app.diagrams.net" modified="2022-04-22T20:26:07.083Z" agent="5.0 (X11)" etag="6MNvURN8OzIm5K8R91Ac" version="17.1.3" type="device"><diagram id="UNyY5TJjaPirS19H5Hif" name="Page-1">5VldU+IwFP01fdTpJ5RHQXB3R8d1ndldn3YiiW0k7WVDsOCv34SmtKWBRVERfYHk5Puec3OT1PJ6yeyMo3F8AZgwy7XxzPJOLdft+I78VcA8B5xWx8+RiFOssRK4po9Eg7ZGpxSTSa2iAGCCjuvgENKUDEUNQ5xDVq92B6w+6hhFpAFcDxFror8oFnGOhm67xL8QGsXFyHKBeUmCisq6i0mMMGQ5tFic17e8HgcQeSqZ9QhTxivskltgsKZ0OTFOUrFNgyGE94Orzs2Afos7PwO3n+DukV7GRMyLBRMs16+zwEUMEaSI9Uu0y2GaYqJ6tWWurHMOMJagI8F7IsRck4mmAiQUi4TpUjKj4nclfaO7UunTWTUzLzKp4HPVxD4OiuxN0YPKlM0WuaJdvjq1pLVGK9iBKR/qWleXF9nl2Wh0dv41/fF3EAknPD9ytfgQj4jYUK+1pFb6BIGEyPnIdpwwJOhDfR5IizNa1iv5kwlNoZnOTZN8QGyqR2rwW7KnjJfFVJDrMVosPZMuXGdqrbkeCBdkVoGaK9alnq09We8Arq/zWelOjqexuOJKBbaLkYyab9rkDTWvBXwoqve2VL27o+oXTU84R/NKhTHQVEwqPX9XQEVaraAmLa++A8pE3mMpleXUnu9insHFWkzapovpg0xGYsGAjHJ+9sdqd6m0Tfs0B4qacuBa5RU5Ss8SdfVMBIcR6QEDLpEUUqXJO8rYCoQYjVKZHUqiicS7yk+pjGYnuiChGC8EbXL8ushfwvd920hQ1fdbr+T6RvI6h+L6z/fY1pYeGxp3iX0Frtb7C1zFeaoQrxM0xeu7b6lexz8U+e4WuZ4j+3BL2Tveu9J9+P51v3/Z22tDLi1iKAMYHU3HlQBLP354DZyVo3W45/DqOA2mMsQnIkMSZDSloFIflQ7fW6HDNtx07Dflo3kfnMCdSNDsA7Ow4hTO/lnY6srA4XFk9QIrdBFWNuAowzCGW0oWqK+9CDP1O0IK7PYwicBSm3F+3aAqHfRoKv9/br5yGK4sOXTLV5HPcltxQ/e4fqFchp6KdtxO8DraMb5VmKSz+/mgYROD5bbf5vb+oPNJjsX/f9DZ9QGm8cLih/6x+Y1l2Ul+MtftSiK36Mptr3SVH94bXT31WcgwUsd+6tzqLXZ+SjLq1nTp3fyU5BzyU9Juu06wRocvf9aV2fKrTM51+W3L6/8D</diagram></mxfile>

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB