forked from filipg/aitech-eks-pub
397 lines
14 KiB
Org Mode
397 lines
14 KiB
Org Mode
* 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</w> -> nie</w> (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</w> -> em</w> (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</w> -> ie</w> (frequency 5906869)
|
||
pair 13: n a -> na (frequency 5300380)
|
||
pair 14: r o -> ro (frequency 5181363)
|
||
pair 15: n a</w> -> na</w> (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</w> -> ch</w> (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</w> -> go</w> (frequency 3279997)
|
||
pair 29: a r -> ar (frequency 3081492)
|
||
pair 30: si ę</w> -> się</w> (frequency 2973681)
|
||
...
|
||
pair 49970: brz mieniu</w> -> brzmieniu</w> (frequency 483)
|
||
pair 49971: bieżą cych</w> -> bieżących</w> (frequency 483)
|
||
pair 49972: biegu nkę</w> -> biegunkę</w> (frequency 483)
|
||
pair 49973: ban kowości</w> -> bankowości</w> (frequency 483)
|
||
pair 49974: ba ku</w> -> baku</w> (frequency 483)
|
||
pair 49975: ba cznie</w> -> bacznie</w> (frequency 483)
|
||
pair 49976: Przypad kowo</w> -> Przypadkowo</w> (frequency 483)
|
||
pair 49977: MA Ł -> MAŁ (frequency 483)
|
||
pair 49978: Lep pera</w> -> Leppera</w> (frequency 483)
|
||
pair 49979: Ko za -> Koza (frequency 483)
|
||
pair 49980: Jak byś</w> -> Jakbyś</w> (frequency 483)
|
||
pair 49981: Geni alne</w> -> Genialne</w> (frequency 483)
|
||
pair 49982: Że nada</w> -> Żenada</w> (frequency 482)
|
||
pair 49983: ń czykiem</w> -> ńczykiem</w> (frequency 482)
|
||
pair 49984: zwie ń -> zwień (frequency 482)
|
||
pair 49985: zost ałaś</w> -> zostałaś</w> (frequency 482)
|
||
pair 49986: zni szczona</w> -> zniszczona</w> (frequency 482)
|
||
pair 49987: ze stawi -> zestawi (frequency 482)
|
||
pair 49988: za sób</w> -> zasób</w> (frequency 482)
|
||
pair 49989: węd rówkę</w> -> wędrówkę</w> (frequency 482)
|
||
pair 49990: wysko czyła</w> -> wyskoczyła</w> (frequency 482)
|
||
pair 49991: wyle czenia</w> -> wyleczenia</w> (frequency 482)
|
||
pair 49992: wychowaw cze</w> -> wychowawcze</w> (frequency 482)
|
||
pair 49993: w t -> wt (frequency 482)
|
||
pair 49994: un da -> unda (frequency 482)
|
||
pair 49995: udzie lałem</w> -> udzielałem</w> (frequency 482)
|
||
pair 49996: tę czy</w> -> tęczy</w> (frequency 482)
|
||
pair 49997: tro sce</w> -> trosce</w> (frequency 482)
|
||
pair 49998: słusz ności</w> -> słuszności</w> (frequency 482)
|
||
pair 49999: su me</w> -> sume</w> (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.
|