* Podział na jednostki podwyrazowe ** Słownik nie może być za duży… Jeśli używamy wyuczalnych zanurzeń słów (embeddingów), wówczas musimy je dopisać do listy parametrów całego modelu — jest to $|V|n$ wag, gdzie $n$ to rozmiar embeddingów; w wypadku uczenia dodatkowo musimy jeszcze pamiętać związane z embeddingami gradienty. Pamięć RAM karty graficznej jest rzecz jasna ograniczona, słownik więc nie może być dowolnie duży. Dla danego modelu karty graficznej dość łatwo ustalić maksymalny rozmiar słownika — jest „twarde” ograniczenie, które musimy spełnić. *** Czy rzeczywiście słownik może być taki duży? Ile jest różnych form fleksyjnych w języku polskim? Zobaczmy w słowniku PoliMorf… #+BEGIN_SRC ipython :session mysession :exports both :results raw drawer ! wget -q 'http://zil.ipipan.waw.pl/PoliMorf?action=AttachFile&do=get&target=PoliMorf-0.6.7.tab.gz' -O - | zcat | cut -f 1 | uniq | head -n 20 #+END_SRC #+RESULTS: :results: # Out[2]: :end: #+BEGIN_SRC ipython :session mysession :exports both :results raw drawer ! wget -q 'http://zil.ipipan.waw.pl/PoliMorf?action=AttachFile&do=get&target=PoliMorf-0.6.7.tab.gz' -O - | zcat | cut -f 1 | sort -u | wc -l #+END_SRC #+RESULTS: :results: # Out[3]: :end: *Pytanie* W którym języku europejskim wyrazów będzie jeszcze więcej niż języku polskim? Tak naprawdę form jest jeszcze więcej, oczywiście PoliMorf nie wyczerpuje zbioru… *Pytanie* Podaj przykłady „oczywistych” wyrazów, których nie ma w PoliMorfie. Jak w sposób systematyczny szukać takich wyrazów? Z drugiej strony, w PoliMorfie jest dużo dziwnych, „sztucznych” wyrazów. #+BEGIN_SRC ipython :session mysession :exports both :results raw drawer ! wget -q 'http://zil.ipipan.waw.pl/PoliMorf?action=AttachFile&do=get&target=PoliMorf-0.6.7.tab.gz' -O - | zcat | cut -f 1 | shuf -n 20 #+END_SRC #+RESULTS: :results: # Out[4]: :end: Inaczej, zobaczmy, ile różnych wyrazów jest w jakimś rzeczywistym zbiorze tekstów, rozpatrzmy teksty zebrane na potrzeby identyfikacji płci autora tekstu: #+BEGIN_SRC ipython :session mysession :exports both :results raw drawer ! git clone --single-branch --depth 1 git://gonito.net/petite-difference-challenge2 #+END_SRC #+RESULTS: :results: # Out[7]: :end: #+BEGIN_SRC ipython :session mysession :exports both :results raw drawer ! xzcat petite-difference-challenge2/train/in.tsv.xz | perl -C -ne 'print "$&\n" while/\p{L}+/g;' | sort -u > vocab.txt #+END_SRC #+BEGIN_SRC ipython :session mysession :exports both :results raw drawer ! head -n 50 vocab.txt #+END_SRC #+RESULTS: :results: # Out[11]: :end: #+BEGIN_SRC ipython :session mysession :exports both :results raw drawer ! wc -l vocab.txt #+END_SRC #+RESULTS: :results: # Out[9]: :end: Co gorsza, nawet jak weźmiemy cały taki słownik bez ograniczeń i tak nie pokryje on sporej części tekstów przetwarzanych w czasie inferencji. Zobaczmy, ilu wyrazów ze zbioru deweloperskiego nie będzie w słowniku. #+BEGIN_SRC ipython :session mysession :exports both :results raw drawer ! cat petite-difference-challenge2/dev-0/in.tsv | perl -C -ne 'print "$&\n" while/\p{L}+/g;' | sort -u | comm vocab.txt - -13 | wc -l #+END_SRC Takie wyrazy nazywamy wyrazami *OOV* (/out-of-vocabulary/). ** Obcięcie słownika Najprostszy sposób ograniczenia słownika to po prostu obcięcie do $N$ najczęstszych słów. Spróbujmy zastosować do korpusu „płci”: #+BEGIN_SRC ipython :session mysession :exports both :results raw drawer ! xzcat petite-difference-challenge2/train/in.tsv.xz | perl -C -ne 'print "$&\n" while/\p{L}+/g;' | sort | uniq -c | sort -k 1rn | head -n 50000 | sort -k 2 > vocab50000.txt #+END_SRC #+RESULTS: :results: # Out[8]: :end: Daje to lepszy efekt niż można się spodziewać. Odrzucamy w ten sposób tylko bardzo rzadkie słowa (albo takie, które wystąpiły tylko raz w korpusie — tzw. /hapax legomena/), choć tych słów jest bardzo dużo. *Zagadka*: 50000 najczęstszych słów (1,9% *typów*) pokrywa jaki odsetek *wystąpień*? Rozkład normalny w języku nie jest… normalny — nie spotkamy się z nim badając języki. W tekstach dominują „skrzywione” rozkłady z długimi, „chudymi” ogonami. #+BEGIN_SRC ipython :session mysession :exports both :results raw drawer ! xzcat petite-difference-challenge2/train/in.tsv.xz | perl -C -ne 'print "$&\n" while/\p{L}+/g;' | sort | uniq -c | sort -k 1rn | cut -f 1 > freqs.txt #+END_SRC #+BEGIN_SRC ipython :session mysession :results file %matplotlib inline import matplotlib.pyplot as plt import re from math import log freqs = [] with open('freqs.txt', 'r') as fh: for line in fh: m = re.match(r'\s*(\d+)', line) if m: freqs.append(int(m.group(1))) plt.plot(range(len(freqs)), freqs) fname = 'word-distribution.png' plt.savefig(fname) fname #+END_SRC #+RESULTS: [[file:# Out[25]: : 'word-distribution.png' [[file:./obipy-resources/c0TrCn.png]]]] ** Lematyzacja Lematyzacja wydaje się dobrym pomysłem, zwłaszcza dla języków dla bogatej fleksji: - znacznie redukujemy słownik, - formy fleksyjne tego samego wyrazu są traktowane tak samo (co wydaje się słuszne). W praktyce współcześnie *nie* stosuje się lematyzacji (w połączeniu z metodami opartymi na sieciach neuronowych): - lematyzacja wymaga wiedzy językowej (reguł lub słownika), wytworzenie takiej wiedzy może być kosztowne, obecnie preferowane są metody niezależne od języka; - tracimy pewną informację niesioną przez formę fleksyjną (co w szczególnych przypadkach może być niefortunne, np. /aspiracja/ i /aspiracje/); - lematyzacja nie jest trywialnym problemem ze względu na niejednoznaczności (/Lekarzu, lecz się sam/); - niektóre niejednoznaczności są seryjne, wybór lematu może być arbitralny, np. czy /posiadanie/, /gotowanie/, /skakanie/ to rzeczowniki czy czasowniki? a /urządzenie/, /mieszkanie/? - zazwyczaj sieci neuronowe (czy nawet prostsze modele typu Word2vec) są w stanie nauczyć się rekonstruowania zależności między formami fleksyjnymi (i więcej: błędnych form, błędów ortograficznych, form archaicznych itd.) ** Zejście na poziom znaków Skoro słownik wyrazów jest zbyt duży, to może zejść na poziom znaków? - pojedynczy znak alfabetu wprawdzie nic nie znaczy (co znaczy /h/?) - … ale rozmiar wejścia przy kodowaniu gorącą jedynką dramatycznie się zmniejsza - może działać, jeśli dodać wielowarstwową sieć neuronową - … ale może być bardzo kosztowne obliczeniowo A może coś pośredniego między znakami a wyrazami? ** BPE Ani znaki, ani wyrazy — coś pomiędzy: jednostki podwyrazowe (/subword units/). Moglibyśmy np. dzielić wyraz /superkomputera/ na dwie jednostki /super/+/komputera/, a może nawet trzy: /super/+/komputer/+/a/? Najpopularniejszy algorytm podziału na jednostki podwyrazowe to BPE (/byte-pair encoding/), zainspirowany algorytmami kompresji danych. Lista jednostek jest automatycznie indukowana na podstawie tekstu (nie potrzeba żadnej wiedzy o języku!). Ich liczba musi być natomiast z góry określona. W kroku początkowym zaznaczamy końce wyrazów (tokenów), robimy to po to, żeby jednostki podwyrazowe nie przekraczały granic wyrazów. Następnie wykonujemy tyle kroków iteracji, ile wynosi rozmiar zadanego słownika. W każdym kroku szukamy najczęstszego bigramu, od tego momentu traktujemy go jako całostkę (wkładamy go do „pudełka”). [[./bpe.png]] *** Implementacja w Pythonie #+BEGIN_SRC ipython :session mysession :exports both :results raw drawer from collections import Counter def replace_bigram(l, b, r): i = 0 while i < len(l) - 1: if (l[i], l[i+1]) == b: l[i:i+2] = [r] i += 1 return l def learn_bpe_vocab(d, max_vocab_size): d = list(d.replace(' ', '$') + '$') vocab = [] for ix in range(0, max_vocab_size): bigrams = [(d[i], d[i+1]) for i in range(0, len(d) - 1) if d[i][-1] != '$'] selected_bigram = Counter(bigrams).most_common(1)[0][0] new_subword = selected_bigram[0] + selected_bigram[1] d = replace_bigram(d, selected_bigram, new_subword) vocab.append(new_subword) return vocab vocab1 = learn_bpe_vocab('to be or not to be that is the question', 10) vocab1 #+END_SRC #+RESULTS: :results: # Out[1]: : ['e$', 'to', 'to$', 'be$', 't$', 'th', 'or', 'or$', 'no', 'not$'] :end: Słownik jednostek podwyrazowych możemy zastosować do dowolnego tekstu, np. do tekstu, na którym słownik był wyuczony: #+BEGIN_SRC ipython :session mysession :exports both :results raw drawer def apply_bpe_vocab(vocab, d): d = list(d.replace(' ', '$') + '$') vocab_set = set(vocab) modified = True while modified: ix = 0 modified = False while ix < len(d) - 1: bigram = d[ix] + d[ix+1] if bigram in vocab_set: d[ix:ix+2] = [bigram] modified = True else: ix += 1 return d ' '.join(apply_bpe_vocab(vocab1, 'to be or not to be that is the question')) #+END_SRC #+RESULTS: :results: # Out[5]: : 'to$ be$ or$ not$ to$ be$ th a t$ i s $ th e$ q u e s t i o n $' :end: Zauważmy, że oprócz jednostek podwyrazowych zostały izolowane litery, zazwyczaj dodajemy je do słownika. (I zazwyczaj, słownik jest trochę większy niż wartość podana jako parametr przy uczeniu BPE — jest większy o znaki i specjalne tokeny typu ~UNK~, ~BOS~, ~EOS~, ~PAD~.) *Pytanie*: Jaki problem może pojawić przy zastosowaniu BPE dla tekstu, gdzie pojawiają się chińskie znaki? Jak można sobie z nim poradzić? Słownik jednostek podwyrazowych można stosować dla dowolnego tekstu: #+BEGIN_SRC ipython :session mysession :exports both :results raw drawer ' '.join(apply_bpe_vocab(vocab1, 'tom will be the best')) #+END_SRC #+RESULTS: :results: # Out[6]: : 'to m $ w i l l $ be$ th e$ b e s t$' :end: Jak można zauważyć algorytm BPE daje dwa rodzaje jednostek podwyrazowych: - jednostki, które mogą doklejane na początku wyrazu; - jednostki, które stanowią koniec wyrazu, w szczególności są całym wyrazem. *** Gotowa implementacja Po raz pierwszy BPE użyto do neuronowego tłumaczenia maszynowego. Użyjmy modułu autorstwa Rica Sennricha (https://github.com/rsennrich/subword-nmt). #+BEGIN_SRC ipython :session mysession :exports both :results raw drawer ! pip install subword-nmt #+END_SRC Wyindukujmy słownik dla zbioru uczącego zadania identyfikacji płci autora tekstu: #+BEGIN_SRC ipython :session mysession :exports both :results raw drawer ! xzcat petite-difference-challenge2/train/in.tsv.xz | perl -C -ne 'print "$&\n" while/\p{L}+/g;' | python -m subword_nmt.learn_bpe -s 50000 -v > bpe_vocab.txt #+END_SRC Procedura trwa kilka minut, trzeba uzbroić się w cierpliwość (ale wypisywanie bigramów przyspieszy!). #+BEGIN_SRC pair 0: n i -> ni (frequency 17625075) pair 1: i e -> ie (frequency 11471590) pair 2: c z -> cz (frequency 9143490) pair 3: ni e -> nie (frequency 7901783) pair 4: p o -> po (frequency 7790826) pair 5: r z -> rz (frequency 7542046) pair 6: s t -> st (frequency 7269069) pair 7: e m -> em (frequency 7207280) pair 8: d z -> dz (frequency 6860931) pair 9: s z -> sz (frequency 6609907) pair 10: r a -> ra (frequency 6601618) pair 11: o w -> ow (frequency 6395963) pair 12: i e -> ie (frequency 5906869) pair 13: n a -> na (frequency 5300380) pair 14: r o -> ro (frequency 5181363) pair 15: n a -> na (frequency 5125807) pair 16: a ł -> ał (frequency 4786696) pair 17: j e -> je (frequency 4599579) pair 18: s i -> si (frequency 4300984) pair 19: a l -> al (frequency 4276823) pair 20: t e -> te (frequency 4033344) pair 21: w i -> wi (frequency 3939063) pair 22: c h -> ch (frequency 3919410) pair 23: c h -> ch (frequency 3661410) pair 24: k o -> ko (frequency 3629840) pair 25: z a -> za (frequency 3625424) pair 26: t a -> ta (frequency 3570094) pair 27: p rz -> prz (frequency 3494551) pair 28: g o -> go (frequency 3279997) pair 29: a r -> ar (frequency 3081492) pair 30: si ę -> się (frequency 2973681) ... pair 49970: brz mieniu -> brzmieniu (frequency 483) pair 49971: bieżą cych -> bieżących (frequency 483) pair 49972: biegu nkę -> biegunkę (frequency 483) pair 49973: ban kowości -> bankowości (frequency 483) pair 49974: ba ku -> baku (frequency 483) pair 49975: ba cznie -> bacznie (frequency 483) pair 49976: Przypad kowo -> Przypadkowo (frequency 483) pair 49977: MA Ł -> MAŁ (frequency 483) pair 49978: Lep pera -> Leppera (frequency 483) pair 49979: Ko za -> Koza (frequency 483) pair 49980: Jak byś -> Jakbyś (frequency 483) pair 49981: Geni alne -> Genialne (frequency 483) pair 49982: Że nada -> Żenada (frequency 482) pair 49983: ń czykiem -> ńczykiem (frequency 482) pair 49984: zwie ń -> zwień (frequency 482) pair 49985: zost ałaś -> zostałaś (frequency 482) pair 49986: zni szczona -> zniszczona (frequency 482) pair 49987: ze stawi -> zestawi (frequency 482) pair 49988: za sób -> zasób (frequency 482) pair 49989: węd rówkę -> wędrówkę (frequency 482) pair 49990: wysko czyła -> wyskoczyła (frequency 482) pair 49991: wyle czenia -> wyleczenia (frequency 482) pair 49992: wychowaw cze -> wychowawcze (frequency 482) pair 49993: w t -> wt (frequency 482) pair 49994: un da -> unda (frequency 482) pair 49995: udzie lałem -> udzielałem (frequency 482) pair 49996: tę czy -> tęczy (frequency 482) pair 49997: tro sce -> trosce (frequency 482) pair 49998: słusz ności -> słuszności (frequency 482) pair 49999: su me -> sume (frequency 482 #+END_SRC Zastosujmy teraz wyindukowany słownik BPE dla jakiegoś rzeczywistego tekstu. #+BEGIN_SRC ipython :session mysession :exports both :results raw drawer ! echo 'Cierpiałem na straszne lagi – kilkanaście sekund lub dłużej czarnego ekranu przy próbie przełączenia się / uruchomienia prawie każdej aplikacji. Dodatkowo telefon mi się wyłączał czasem bez powodu – sam z siebie, albo resetował. Ostatnio nawet przeglądarka zaczęła się często zawieszać i Android proponował wymuszone zamknięcie. Do tego te problemy z połączeniem do komputera przez USB.' | perl -C -ne 'print "$& " while/\p{L}+/g;' | python -m subword_nmt.apply_bpe -c bpe_vocab.txt #+END_SRC Ta konkretna implementacja zaznacza za pomocą sekwencji ~@@ ~ koniec jednostki podwyrazowej.