forked from filipg/aitech-eks-pub
bpe
This commit is contained in:
parent
a004360ec5
commit
98012914c4
386
wyk/12_bpe.org
Normal file
386
wyk/12_bpe.org
Normal file
@ -0,0 +1,386 @@
|
||||
* 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 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.xz | 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[4]:
|
||||
: ['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)
|
||||
|
||||
ix = 0
|
||||
while ix < len(d) - 1:
|
||||
bigram = d[ix] + d[ix+1]
|
||||
if bigram in vocab_set:
|
||||
d[ix:ix+2] = [bigram]
|
||||
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$ b e$ or$ no t$ to$ b e$ 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
|
||||
|
||||
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.
|
Loading…
Reference in New Issue
Block a user