modelowanie-jezykowe-aitech-cw/wyk/06_Zanurzenia_slow.org

391 lines
14 KiB
Org Mode
Raw Normal View History

2022-04-11 10:12:01 +02:00
* Zanurzenia słów
** Słabości $n$-gramowych modeli języka
Podstawowa słabość $n$-gramowych modeli języka polega na tym, że każde
słowo jest traktowane w izolacji. W, powiedzmy, bigramowym modelu
języka każda wartość $P(w_2|w_1)$ jest estymowana osobno, nawet dla —
w jakimś sensie podobnych słów. Na przykład:
- $P(\mathit{zaszczekał}|\mathit{pies})$, $P(\mathit{zaszczekał}|\mathit{jamnik})$, $P(\mathit{zaszczekał}|\mathit{wilczur})$ są estymowane osobno,
- $P(\mathit{zaszczekał}|\mathit{pies})$, $P(\mathit{zamerdał}|\mathit{pies})$, $P(\mathit{ugryzł}|\mathit{pies})$ są estymowane osobno,
- dla każdej pary $u$, $v$, gdzie $u$ jest przyimkiem (np. /dla/), a $v$ — osobową formą czasownika (np. /napisał/) model musi się uczyć, że $P(v|u)$ powinno mieć bardzo niską wartość.
** Podobieństwo słów jako sposób na słabości $n$-gramowych modeli języka?
Intuicyjnie wydaje się, że potrzebujemy jakiegoś sposobu określania podobieństwa słów, tak aby
w naturalny sposób, jeśli słowa $u$ i $u'$ oraz $v$ i $v'$ są bardzo podobne, wówczas
$P(u|v) \approx P(u'|v')$.
Można wskazać trzy sposoby określania podobieństwa słów: odległość
edycyjna Lewensztajna, hierarchie słów i odległość w przestrzeni wielowymiarowej.
*** Odległość Lewensztajna
Słowo /dom/ ma coś wspólnego z /domem/, /domkiem/, /domostwem/,
/domownikami/, /domowym/ i /udomowieniem/ (?? — tu już można mieć
wątpliwości). Więc może oprzeć podobieństwa na powierzchownym
podobieństwie?
Możemy zastosować tutaj *odległość Lewensztajna*, czyli minimalną liczbę operacji edycyjnych, które
są potrzebne, aby przekształcić jedno słowo w drugie. Zazwyczaj jako elementarne operacje edycyjne
definiuje się:
- usunięcie znaku,
- dodanie znaku,
- zamianu znaku.
Na przykład odległość edycyjna między słowami /domkiem/ i /domostwem/
wynosi 4: zamiana /k/ na /o/, /i/ na /s/, dodanie /t/, dodanie /w/.
#+BEGIN_SRC python :session mysession :exports both :results raw drawer
import Levenshtein
Levenshtein.distance('domkiem', 'domostwem')
#+END_SRC
#+RESULTS:
:results:
4
:end:
Niestety, to nie jest tak, że słowa są podobne wtedy i tylko wtedy, gdy /wyglądają/ podobnie:
- /tapet/ nie ma nic wspólnego z /tapetą/,
- słowo /sowa/ nie wygląda jak /ptak/, /puszczyk/, /jastrząb/, /kura/ itd.
Powierzchowne podobieństwo słów łączy się zazwyczaj z relacjami
*fleksyjnymi* i *słowotwórczymi* (choć też nie zawsze, por. np. pary
słów będące przykładem *supletywizmu*: /człowiek/-/ludzie/,
/brać/-/zwiąć/, /rok/-/lata/). A co z innymi własnościami wyrazów czy
raczej bytów przez nie denotowanych (słowa oznaczające zwierzęta
należące do gromady ptaków chcemy traktować jako, w jakiejś mierze przynajmnie, podobne)?
Dodajmy jeszcze, że w miejsce odległości Lewensztajna warto czasami
używać podobieństwa Jaro-Winklera, które mniejszą wagę przywiązuje do zmian w końcówkach wyrazów:
#+BEGIN_SRC python :session mysession :exports both :results raw drawer
import Levenshtein
Levenshtein.jaro_winkler('domu', 'domowy')
Levenshtein.jaro_winkler('domowy', 'maskowy')
#+END_SRC
#+RESULTS:
:results:
0.6626984126984127
:end:
*** Klasy i hierarchie słów
Innym sposobem określania podobieństwa między słowami jest zdefiniowanie klas słów.
Słowa należące do jednej klasy będą podobne, do różnych klas — niepodobne.
**** Klasy gramatyczne
Klasy mogą odpowiadać standardowym kategoriom gramatycznym znanym z
językoznawstwa, na przykład *częściom mowy* (rzeczownik, przymiotnik,
czasownik itd.). Wiele jest niejednoznacznych jeśli chodzi o kategorię części mowy:
- /powieść/ — rzeczownik czy czasownik?
- /komputerowi/ — rzeczownik czy przymiotnik?
- /lecz/ — spójnik, czasownik (!) czy rzeczownik (!!)?
Oznacza to, że musimy dysponować narzędziem, które pozwala
automatycznie, na podstawie kontekstu, tagować tekst częściami mowy
(ang. /POS tagger/). Takie narzędzia pozwalają na osiągnięcie wysokiej
dokładności, niestety zawsze wprowadzają jakieś błędy, które mogą
propagować się dalej.
**** Klasy indukowane automatycznie
Zamiast z góry zakładać klasy wyrazów można zastosować metody uczenia
nienadzorowanego (podobne do analizy skupień) w celu *wyindukowanie*
automatycznie klas (tagów) z korpusu.
**** Użycie klas słów w modelu języka
Najprostszy sposób uwzględnienia klas słów w $n$-gramowym modelowaniu
języka polega stworzeniu dwóch osobnych modeli:
- tradycyjnego modelu języka $M_W$ operującego na słowach,
- modelu języka $M_T$ wyuczonego na klasach słów (czy to częściach
mowy, czy klasach wyindukowanych automatycznie).
Zauważmy, że rząd modelu $M_T$ ($n_T$) może dużo większy niż rząd modelu $M_W$ ($n_W$) — klas będzie
dużo mniej niż wyrazów, więc problem rzadkości danych jest dużo mniejszy i można rozpatrywać dłuższe
$n$-gramy.
Dwa modele możemy połączyć za pomocą prostej kombinacji liniowej sterowanej hiperparametrem $\lambda$:
$$P(w_i|w_{i-n_T}+1\ldots w_{i-1}) = \lambda P_{M_T}(w_i|w_{i-n_W}+1\ldots w_{i-1}) + (1 - \lambda) P_{M_W}(w_i|w_{i-n_T}+1\ldots w_{i-1}).$$
**** Hierarchie słów
Zamiast płaskiej klasyfikacji słów można zbudować hierarchię słów czy
pojęć. Taka hierarchia może dotyczyć właściwości gramatycznych
(na przykład rzeczownik w liczbie pojedynczej w dopełniaczu będzie podklasą
rzeczownika) lub własności denotowanych bytów.
Niekiedy dość łatwo stworzyć hierarchie (taksonomię) pojęć. Na
przykład jamnik jest rodzajem psa (słowo /jamnik/ jest *hiponimem*
słowa /pies/, zaś słowo /pies/ hiperonimem słowa /jamnik/), pies —
ssaka, ssak — zwierzęcia, zwierzę — organizmu żywego, organizm — bytu
materialnego.
***** Analityczny język Johna Wilkinsa
Już od dawna filozofowie myśleli o stworzenie języka uniwersalnego, w
którym hierarchia bytów jest ułożona w „naturalny” sposób.
Przykładem jest angielski uczony John Wilkins (1614-1672). W dziele
/An Essay towards a Real Character and a Philosophical Language/
zaproponował on rozbudowaną hierarchię bytów.
#+CAPTION: Fragment dzieła Johna Wilkinsa
[[./06_Zanurzenia_slow/wilkins.png]]
***** Słowosieci
Współczesnym odpowiednik hierarchii Wilkinsa są *słowosieci* (ang. /wordnets).
Przykłady:
- dla języka polskiego: [[http://plwordnet.pwr.wroc.pl][Słowosieć]],
- dla języka angielskiego: [[https://wordnet.princeton.edu/][Princeton Wordnet]] (i Słowosieć!)
#+CAPTION: Fragment Słowosieci
[[./06_Zanurzenia_slow/slowosiec.png]]
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
len(vocab)
#+END_SRC
#+RESULTS:
:results:
20000
: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(['mieszkam', 'w', 'londynie']))
out = model(ixs)
out.size()
#+END_SRC
#+RESULTS:
:results:
torch.Size([3, 20000])
: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, 19922)
: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, 19922, 114, 888, 1152]), tensor([19922, 114, 888, 1152, 3])]
: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=8000)
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:
#+BEGIN_SRC python :session mysession :exports both :results raw drawer
vocab = train_dataset.vocab
ixs = torch.tensor(vocab.forward(['jest', 'mieszkam', 'w', 'londynie'])).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:
[('jorku', 1079, 0.41101229190826416), ('.', 3, 0.07469522953033447), ('<unk>', 0, 0.04370327666401863), (',', 4, 0.023186953738331795), ('...', 15, 0.0091575738042593), ('?', 6, 0.008711819536983967), ('tym', 30, 0.0047738500870764256), ('to', 7, 0.004259662237018347), ('do', 17, 0.004140778910368681), ('w', 10, 0.003930391278117895)]
:end: