aitech-eks-pub/wyk/12_bpe.org
2021-06-12 15:47:37 +02:00

14 KiB
Raw Blame History

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…

! 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
! 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

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.

! 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

Inaczej, zobaczmy, ile różnych wyrazów jest w jakimś rzeczywistym zbiorze tekstów, rozpatrzmy teksty zebrane na potrzeby identyfikacji płci autora tekstu:

! git clone --single-branch --depth 1 git://gonito.net/petite-difference-challenge2
! xzcat petite-difference-challenge2/train/in.tsv.xz | perl -C -ne 'print "$&\n" while/\p{L}+/g;'  | sort -u > vocab.txt
! head -n 50 vocab.txt
! wc -l vocab.txt

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.

! 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

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”:

! 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

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.

! 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
  %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

[[file:# Out[25]:

'word-distribution.png'

/s444820/aitech-eks-pub/media/commit/4437a7f71b4a8e27b6943102470f8e4429d01204/wyk/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”).

/s444820/aitech-eks-pub/media/commit/4437a7f71b4a8e27b6943102470f8e4429d01204/wyk/bpe.png

Implementacja w Pythonie

  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
['e$', 'to', 'to$', 'be$', 't$', 'th', 'or', 'or$', 'no', 'not$']

Słownik jednostek podwyrazowych możemy zastosować do dowolnego tekstu, np. do tekstu, na którym słownik był wyuczony:

  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'))
'to$ be$ or$ not$ to$ be$ th a t$ i s $ th e$ q u e s t i o n $'

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:

' '.join(apply_bpe_vocab(vocab1, 'tom will be the best'))
'to m $ w i l l $ be$ th e$ b e s t$'

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).

! pip install subword-nmt

Wyindukujmy słownik dla zbioru uczącego zadania identyfikacji płci autora tekstu:

! 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

Procedura trwa kilka minut, trzeba uzbroić się w cierpliwość (ale wypisywanie bigramów przyspieszy!).

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

Zastosujmy teraz wyindukowany słownik BPE dla jakiegoś rzeczywistego tekstu.

! 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

Ta konkretna implementacja zaznacza za pomocą sekwencji ~@@ ~ koniec jednostki podwyrazowej.