Wykład wygładzanie

This commit is contained in:
Filip Graliński 2024-04-03 11:46:46 +02:00
parent f88edaf1f4
commit 9cc475b7c2
9 changed files with 1350 additions and 0 deletions

862
wyk/07_Wygladzanie.ipynb Normal file
View File

@ -0,0 +1,862 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"![Logo 1](https://git.wmi.amu.edu.pl/AITech/Szablon/raw/branch/master/Logotyp_AITech1.jpg)\n",
"<div class=\"alert alert-block alert-info\">\n",
"<h1> Modelowanie języka</h1>\n",
"<h2> 07. <i>Wygładzanie w n-gramowych modelach języka</i> [wykład]</h2> \n",
"<h3> Filip Graliński (2022)</h3>\n",
"</div>\n",
"\n",
"![Logo 2](https://git.wmi.amu.edu.pl/AITech/Szablon/raw/branch/master/Logotyp_AITech2.jpg)\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Wygładzanie w n-gramowych modelach języka\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Dlaczego wygładzanie?\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Wyobraźmy sobie urnę, w której znajdują się kule w $m$ kolorach\n",
"(ściślej: w co najwyżej $m$ kolorach, może w ogóle nie być kul w danym\n",
"kolorze). Nie wiemy, ile jest ogółem kul w urnie i w jakiej liczbie\n",
"występuje każdy z kolorów.\n",
"\n",
"Losujemy ze zwracaniem (to istotne!) $T$ kul, załóżmy, że\n",
"wylosowaliśmy w poszczególnych kolorach $\\{k_1,\\dots,k_m\\}$ kul\n",
"(tzn. pierwszą kolor wylosowaliśmy $k_1$ razy, drugi kolor — $k_2$ razy itd.).\n",
"Rzecz jasna, $\\sum_{i=1}^m k_i = T$.\n",
"\n",
"Jak powinniśmy racjonalnie szacować prawdopodobieństwa wylosowania kuli w $i$-tym kolorze ($p_i$)?\n",
"\n",
"Wydawałoby się, że wystarczy liczbę wylosowanych kul w danym kolorze\n",
"podzielić przez liczbę wszystkich prób:\n",
"\n",
"$$p_i = \\frac{k_i}{T}.$$\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Wygładzanie — przykład\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Rozpatrzmy przykład z 3 kolorami (wiemy, że w urnie mogą być kule\n",
"żółte, zielone i czerwone, tj. $m=3$) i 4 losowaniami ($T=4$):\n",
"\n",
"![img](./07_Wygladzanie/urna.drawio.png)\n",
"\n",
"Gdybyśmy w prosty sposób oszacowali prawdopodobieństwa, doszlibyśmy do\n",
"wniosku, że prawdopodobieństwo wylosowania kuli czerwonej wynosi 3/4, żółtej — 1/4,\n",
"a zielonej — 0. Wartości te są jednak dość problematyczne:\n",
"\n",
"- Za bardzo przywiązujemy się do naszej skromnej próby,\n",
" potrzebowalibyśmy większej liczby losowań, żeby być bardziej pewnym\n",
" naszych estymacji.\n",
"- W szczególności stwierdzenie, że prawdopodobieństwo wylosowania kuli\n",
" zielonej wynosi 0, jest bardzo mocnym stwierdzeniem (twierdzimy, że\n",
" **NIEMOŻLIWE** jest wylosowanie kuli zielonej), dopiero większa liczba\n",
" prób bez wylosowania zielonej kuli mogłaby sugerować\n",
" prawdopodobieństwo bliskie zeru.\n",
"- Zauważmy, że niemożliwe jest wylosowanie ułamka kuli, jeśli w\n",
" rzeczywistości 10% kul jest żółtych, to nie oznacza się wylosujemy\n",
" $4\\frac{1}{10} = \\frac{2}{5}$ kuli. Prawdopodobnie wylosujemy jedną\n",
" kulę żółtą albo żadną. Wylosowanie dwóch kul żółtych byłoby możliwe,\n",
" ale mniej prawdopodobne. Jeszcze mniej prawdopodobne byłoby\n",
" wylosowanie 3 lub 4 kul żółtych.\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Idea wygładzania\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Wygładzanie (ang. *smoothing*) polega na tym, że „uszczknąć” nieco\n",
"masy prawdopodobieństwa zdarzeniom wskazywanym przez eksperyment czy\n",
"zbiór uczący i rozdzielić ją między mniej prawdopodobne zdarzenia.\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Wygładzanie +1\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Najprostszy sposób wygładzania to wygładzania +1, nazywane też wygładzaniem\n",
"Laplace'a, zdefiniowane za pomocą następującego wzoru:\n",
"\n",
"$$p_i = \\frac{k_i+1}{T+m}.$$\n",
"\n",
"W naszym przykładzie z urną prawdopodobieństwo wylosowania kuli\n",
"czerwonej określimy na $\\frac{3+1}{4+3} = \\frac{4}{7}$, kuli żółtej —\n",
"$\\frac{1+1}{4+3}=2/7$, zielonej — $\\frac{0+1}{4+3}=1/7$. Tym samym,\n",
"kula zielona uzyskała niezerowe prawdopodobieństwo, żółta — nieco\n",
"zyskała, zaś czerwona — straciła.\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"##### Własności wygładzania +1\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Zauważmy, że większa liczba prób $m$, tym bardziej ufamy naszemu eksperymentowi\n",
"(czy zbiorowi uczącemu) i tym bardziej zbliżamy się do niewygładzonej wartości:\n",
"\n",
"$$\\lim_{m \\rightarrow \\infty} \\frac{k_i +1}{T + m} = \\frac{k_i}{T}.$$\n",
"\n",
"Inna dobra, zdroworozsądkowo, własność to to, że prawdopodobieństwo nigdy nie będzie zerowe:\n",
"\n",
"$$\\frac{k_i + 1}{T + m} > 0.$$\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Wygładzanie w unigramowym modelu języku\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Analogia do urny\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Unigramowy model języka, abstrakcyjnie, dokładnie realizuje scenariusz\n",
"losowania kul z urny: $m$ to liczba wszystkich wyrazów (czyli rozmiar słownika $|V|$),\n",
"$k_i$ to ile razy w zbiorze uczącym pojawił się $i$-ty wyraz słownika,\n",
"$T$ — długość zbioru uczącego.\n",
"\n",
"![img](./07_Wygladzanie/urna-wyrazy.drawio.png)\n",
"\n",
"A zatem przy użyciu wygładzania +1 w następujący sposób estymować\n",
"będziemy prawdopodobieństwo słowa $w$:\n",
"\n",
"$$P(w) = \\frac{\\# w + 1}{|C| + |V|}.$$\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Wygładzanie $+\\alpha$\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"W modelowaniu języka wygładzanie $+1$ daje zazwyczaj niepoprawne\n",
"wyniki, dlatego częściej zamiast wartości 1 używa się współczynnika $0\n",
"< \\alpha < 1$:\n",
"\n",
"$$P(w) = \\frac{\\# w + \\alpha}{|C| + \\alpha|V|}.$$\n",
"\n",
"W innych praktycznych zastosowaniach statystyki\n",
"przyjmuje się $\\alpha = \\frac{1}{2}$, ale w przypadku n-gramowych\n",
"modeli języka i to będzie zbyt duża wartość.\n",
"\n",
"W jaki sposób ustalić wartość $\\alpha$? Można $\\alpha$ potraktować $\\alpha$\n",
"jako hiperparametr i dostroić ją na odłożonym zbiorze.\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Jak wybrać wygładzanie?\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Jak ocenić, który sposób wygładzania jest lepszy? Jak wybrać $\\alpha$\n",
"w czasie dostrajania?\n",
"\n",
"Najprościej można sprawdzić estymowane prawdopodobieństwa na zbiorze\n",
"strojącym (developerskim). Dla celów poglądowych bardziej czytelny\n",
"będzie podział zbioru uczącego na dwie równe części — będziemy\n",
"porównywać częstości estymowane na jednej połówce korpusu z\n",
"rzeczywistymi, empirycznymi częstościami z drugiej połówki.\n",
"\n",
"Wyniki będziemy przedstawiać w postaci tabeli, gdzie w poszczególnych\n",
"wierszach będziemy opisywać częstości estymowane dla wszystkich\n",
"wyrazów, które pojawiły się określoną liczbę razy w pierwszej połówce korpusu.\n",
"\n",
"Ostatecznie możemy też po prostu policzyć perplexity na zbiorze testowym\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Przykład\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Użyjemy polskiej części z korpusu równoległego Open Subtitles:\n",
"\n",
" wget -O en-pl.txt.zip 'https://opus.nlpl.eu/download.php?f=OpenSubtitles/v2018/moses/en-pl.txt.zip'\n",
" unzip en-pl.txt.zip\n",
"\n",
"Usuńmy duplikaty (zachowując kolejność):\n",
"\n",
" nl OpenSubtitles.en-pl.pl | sort -k 2 -u | sort -k 1 | cut -f 2- > opensubtitles.pl.txt\n",
"\n",
"Korpus zawiera ponad 28 mln słów, zdania są krótkie, jest to język potoczny, czasami wulgarny.\n",
"\n",
" $ wc opensubtitles.pl.txt\n",
" 28154303 178866171 1206735898 opensubtitles.pl.txt\n",
" $ head -n 10 opensubtitles.pl.txt\n",
" Lubisz curry, prawda?\n",
" Nałożę ci więcej.\n",
" Hey!\n",
" Smakuje ci?\n",
" Hey, brzydalu.\n",
" Spójrz na nią.\n",
" - Wariatka.\n",
" - Zadałam ci pytanie!\n",
" No, tak lepiej!\n",
" - Wygląda dobrze!\n",
"\n",
"Podzielimy korpus na dwie części:\n",
"\n",
" head -n 14077151 < opensubtitles.pl.txt > opensubtitlesA.pl.txt\n",
" tail -n 14077151 < opensubtitles.pl.txt > opensubtitlesB.pl.txt\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"##### Tokenizacja\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Stwórzmy generator, który będzie wczytywał słowa z pliku, dodatkowo:\n",
"\n",
"- ciągi znaków interpunkcyjnych będziemy traktować jak tokeny,\n",
"- sprowadzimy wszystkie litery do małych,\n",
"- dodamy specjalne tokeny na początek i koniec zdania (`<s>` i `</s>`).\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"['<s>', 'lubisz', 'curry', ',', 'prawda', '?', '</s>', '<s>', 'nałożę', 'ci', 'więcej', '.', '</s>', '<s>', 'hey', '!', '</s>', '<s>', 'smakuje', 'ci', '?', '</s>', '<s>', 'hey', ',', 'brzydalu', '.', '</s>', '<s>', 'spójrz', 'na', 'nią', '.', '</s>', '<s>', '-', 'wariatka', '.', '</s>', '<s>', '-', 'zadałam', 'ci', 'pytanie', '!', '</s>', '<s>', 'no', ',', 'tak', 'lepiej', '!', '</s>', '<s>', '-', 'wygląda', 'dobrze', '!', '</s>', '<s>', '-', 'tak', 'lepiej', '!', '</s>', '<s>', 'pasuje', 'jej', '.', '</s>', '<s>', '-', 'hey', '.', '</s>', '<s>', '-', 'co', 'do', '...?', '</s>', '<s>', 'co', 'do', 'cholery', 'robisz', '?', '</s>', '<s>', 'zejdź', 'mi', 'z', 'oczu', ',', 'zdziro', '.', '</s>', '<s>', 'przestań', 'dokuczać']"
]
}
],
"source": [
"from itertools import islice\n",
"import regex as re\n",
"import sys\n",
"\n",
"def get_words_from_file(file_name):\n",
" with open(file_name, 'r') as fh:\n",
" for line in fh:\n",
" line = line.rstrip()\n",
" yield '<s>'\n",
" for m in re.finditer(r'[\\p{L}0-9\\*]+|\\p{P}+', line):\n",
" yield m.group(0).lower()\n",
" yield '</s>'\n",
"\n",
"list(islice(get_words_from_file('opensubtitlesA.pl.txt'), 0, 100))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"##### Empiryczne wyniki\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Zobaczmy, ile razy, średnio w drugiej połówce korpusu występują\n",
"wyrazy, które w pierwszej wystąpiły określoną liczbę razy.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"from collections import Counter\n",
"\n",
"counterA = Counter(get_words_from_file('opensubtitlesA.pl.txt'))"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"48113"
]
}
],
"source": [
"counterA['taki']"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"max_r = 10\n",
"\n",
"buckets = {}\n",
"for token in counterA:\n",
" buckets.setdefault(counterA[token], 0)\n",
" buckets[counterA[token]] += 1\n",
"\n",
"bucket_counts = {}\n",
"\n",
"counterB = Counter(get_words_from_file('opensubtitlesB.pl.txt'))\n",
"\n",
"for token in counterB:\n",
" bucket_id = counterA[token] if token in counterA else 0\n",
" if bucket_id <= max_r:\n",
" bucket_counts.setdefault(bucket_id, 0)\n",
" bucket_counts[bucket_id] += counterB[token]\n",
" if bucket_id == 0:\n",
" buckets.setdefault(0, 0)\n",
" buckets[0] += 1\n",
"\n",
"nb_of_types = [buckets[ix] for ix in range(0, max_r+1)]\n",
"empirical_counts = [bucket_counts[ix] / buckets[ix] for ix in range(0, max_r)]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Policzmy teraz jakiej liczby wystąpień byśmy oczekiwali, gdyby użyć wygładzania +1 bądź +0.01.\n",
"(Uwaga: zwracamy liczbę wystąpień, a nie względną częstość, stąd przemnażamy przez rozmiar całego korpusu).\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"926594"
]
}
],
"source": [
"def plus_alpha_smoothing(alpha, m, t, k):\n",
" return t*(k + alpha)/(t + alpha * m)\n",
"\n",
"def plus_one_smoothing(m, t, k):\n",
" return plus_alpha_smoothing(1.0, m, t, k)\n",
"\n",
"vocabulary_size = len(counterA)\n",
"corpus_size = counterA.total()\n",
"\n",
"plus_one_counts = [plus_one_smoothing(vocabulary_size, corpus_size, ix) for ix in range(0, max_r)]\n",
"\n",
"plus_alpha_counts = [plus_alpha_smoothing(0.01, vocabulary_size, corpus_size, ix) for ix in range(0, max_r)]\n",
"\n",
"data = list(zip(nb_of_types, empirical_counts, plus_one_counts, plus_alpha_counts))\n",
"\n",
"vocabulary_size"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"liczba tokenów średnia częstość w części B estymacje +1 estymacje +0.01\n",
"0 388334 1.900495 0.993586 0.009999\n",
"1 403870 0.592770 1.987172 1.009935\n",
"2 117529 1.565809 2.980759 2.009870\n",
"3 62800 2.514268 3.974345 3.009806\n",
"4 40856 3.504944 4.967931 4.009741\n",
"5 29443 4.454098 5.961517 5.009677\n",
"6 22709 5.232023 6.955103 6.009612\n",
"7 18255 6.157929 7.948689 7.009548\n",
"8 15076 7.308039 8.942276 8.009483\n",
"9 12859 8.045649 9.935862 9.009418"
]
}
],
"source": [
"import pandas as pd\n",
"\n",
"pd.DataFrame(data, columns=[\"liczba tokenów\", \"średnia częstość w części B\", \"estymacje +1\", \"estymacje +0.01\"])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Wygładzanie Gooda-Turinga\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Inna metoda — wygładzanie Gooda-Turinga — polega na zliczaniu, ile\n",
"$n$-gramów (na razie rozpatrujemy model unigramowy, więc po prostu pojedynczych\n",
"wyrazów) wystąpiło zadaną liczbę razy. Niech $N_r$ oznacza właśnie,\n",
"ile $n$-gramów wystąpiło dokładnie $r$ razy; na przykład $N_1$ oznacza liczbę *hapax legomena*.\n",
"\n",
"W metodzie Gooda-Turinga używamy następującej estymacji:\n",
"\n",
"$$p(w) = \\frac{\\# w + 1}{|C|}\\frac{N_{r+1}}{N_r}.$$\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"##### Przykład\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"liczba tokenów średnia częstość w części B estymacje +1 Good-Turing\n",
"0 388334 1.900495 0.993586 1.040007\n",
"1 403870 0.592770 1.987172 0.582014\n",
"2 117529 1.565809 2.980759 1.603009\n",
"3 62800 2.514268 3.974345 2.602293\n",
"4 40856 3.504944 4.967931 3.603265\n",
"5 29443 4.454098 5.961517 4.627721\n",
"6 22709 5.232023 6.955103 5.627064\n",
"7 18255 6.157929 7.948689 6.606847\n",
"8 15076 7.308039 8.942276 7.676506\n",
"9 12859 8.045649 9.935862 8.557431"
]
}
],
"source": [
"good_turing_counts = [(ix+1)*nb_of_types[ix+1]/nb_of_types[ix] for ix in range(0, max_r)]\n",
"\n",
"data2 = list(zip(nb_of_types, empirical_counts, plus_one_counts, good_turing_counts))\n",
"\n",
"pd.DataFrame(data2, columns=[\"liczba tokenów\", \"średnia częstość w części B\", \"estymacje +1\", \"Good-Turing\"])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Wygładzanie metodą Gooda-Turinga, mimo prostoty, daje wyniki zaskakująco zbliżone do rzeczywistych.\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Wygładzanie dla $n$-gramów\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Rzadkość danych\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"W wypadku bigramów, trigramów, tetragramów itd. jeszcze dotkliwy staje się problem\n",
"**rzadkości** danych (*data sparsity*). Przestrzeń możliwych zdarzeń\n",
"jest jeszcze większa ($|V|^2$ dla bigramów), więc estymacje stają się\n",
"jeszcze mniej pewne.\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Back-off\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Dla $n$-gramów, gdzie $n>1$, nie jesteśmy ograniczeni do wygładzania $+1$, $+k$ czy Gooda-Turinga.\n",
"W przypadku rzadkich $n$-gramów, w szczególności, gdy $n$-gram w ogóle się nie pojawił w korpusie,\n",
"możemy „zejść” na poziom krótszych $n$-gramów. Na tym polega **back-off**.\n",
"\n",
"Otóż jeśli $\\# w_{i-n+1}\\ldots w_{i-1} > 0$, wówczas estymujemy prawdopodobieństwa\n",
" w tradycyjny sposób:\n",
"\n",
"$$P_B(w_i|w_{i-n+1}\\ldots w_{i-1}) = d_n(w_{i-n+1}\\ldots w_{i-1}\\ldots w_{i-1}) P(w_i|w_{i-n+1}\\ldots w_{i-1})$$\n",
"\n",
"W przeciwnym razie rozpatrujemy rekurencyjnie krótszy $n$-gram:\n",
"\n",
"$$P_B(w_i|w_{i-n+1}\\ldots w_{i-1}) = \\delta_n(w_{i-n+1}\\ldots w_{i-1}\\ldots w_{i-1}) P_B(w_i|w_{i-n+2}\\ldots w_{i-1}).$$\n",
"\n",
"Technicznie, aby $P_B$ stanowiło rozkład prawdopodobieństwa, trzeba dobrać współczynniki $d$ i $\\delta$.\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Interpolacja\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Alternatywą do metody back-off jest **interpolacja** — zawsze z pewnym współczynnikiem uwzględniamy\n",
"prawdopodobieństwa dla krótszych $n$-gramów:\n",
"\n",
"$$P_I(w_i|w_{i-n+1}\\ldots w_{i-1}) = \\lambda P(w_i|w_{i-n+1}\\dots w_{i-1}) + (1-\\lambda)\n",
" P_I(w_i|w_{i-n+2}\\dots w_{i-1}).$$\n",
"\n",
"Na przykład, dla trigramów:\n",
"\n",
"$$P_I(w_i|w_{i-2}w_{i-1}) = \\lambda P_(w_i|w_{i-2}w_{i-1}) + (1-\\lambda)(\\lambda P(w_i|w_{i-1}) + (1-\\lambda)P_I(w_i)).$$\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Uwzględnianie różnorodności\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Różnorodność kontynuacji\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Zauważmy, że słowa mogą bardzo różnić się co do różnorodności\n",
"kontynuacji. Na przykład po słowie *szop* spodziewamy się raczej tylko\n",
"słowa *pracz*, każde inne, niewidziane w zbiorze uczącym, będzie\n",
"zaskakujące. Dla porównania słowo *seledynowy* ma bardzo dużo\n",
"możliwych kontynuacji i powinniśmy przeznaczyć znaczniejszą część masy\n",
"prawdopodobieństwa na kontynuacje niewidziane w zbiorze uczącym.\n",
"\n",
"Różnorodność kontynuacji bierze pod uwagę metoda wygładzania\n",
"Wittena-Bella, będącą wersją interpolacji.\n",
"\n",
"Wprowadźmy oznaczenie na liczbę możliwych kontynuacji $n-1$-gramu $w_1\\ldots w_{n-1}$:\n",
"\n",
"$$N_{1+}(w_1\\ldots w_{n-1}\\dot\\bullet) = |\\{w_n : \\# w_1\\ldots w_{n-1}w_n > 0\\}|.$$\n",
"\n",
"Teraz zastosujemy interpolację z następującą wartością parametru\n",
"$1-\\lambda$, sterującego wagą, jaką przypisujemy do krótszych $n$-gramów:\n",
"\n",
"$$1 - \\lambda = \\frac{N_{1+}(w_1\\ldots w_{n-1}\\dot\\bullet)}{N_{1+}(w_1\\ldots w_{n-1}\\dot\\bullet) + \\# w_1\\ldots w_{n-1}}.$$\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Wygładzanie Knesera-Neya\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Zamiast brać pod uwagę różnorodność kontynuacji, możemy rozpatrywać\n",
"różnorodność **historii** — w momencie liczenia prawdopodobieństwa dla\n",
"unigramów dla interpolacji (nie ma to zastosowania dla modeli\n",
"unigramowych). Na przykład dla wyrazu *Jork* spodziewamy się tylko\n",
"bigramu *Nowy Jork*, a zatem przy interpolacji czy back-off prawdopodobieństwo\n",
"unigramowe powinno być niskie.\n",
"\n",
"Wprowadźmy oznaczenia na liczbę możliwych historii:\n",
"\n",
"$$N_{1+}(\\bullet w) = |\\{w_j : \\# w_jw > 0\\}|$$.\n",
"\n",
"W metodzie Knesera-Neya w następujący sposób estymujemy prawdopodobieństwo unigramu:\n",
"\n",
"$$P(w) = \\frac{N_{1+}(\\bullet w)}{\\sum_{w_j} N_{1+}(\\bullet w_j)}.$$\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[('k', 'o', 't'), ('o', 't', 'e'), ('t', 'e', 'k')]"
]
}
],
"source": [
"def ngrams(iter, size):\n",
" ngram = []\n",
" for item in iter:\n",
" ngram.append(item)\n",
" if len(ngram) == size:\n",
" yield tuple(ngram)\n",
" ngram = ngram[1:]\n",
"\n",
"list(ngrams(\"kotek\", 3))"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"histories = { }\n",
"for prev_token, token in ngrams(get_words_from_file('opensubtitlesA.pl.txt'), 2):\n",
" histories.setdefault(token, set())\n",
" histories[token].add(prev_token)"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"len(histories['jork'])\n",
"len(histories['zielony'])\n",
"histories['jork']"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Narzędzia $n$-gramowego modelowania języka\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Istnieje kilka narzędzie do modelowania, ze starszych warto wspomnieć\n",
"pakiety [SRILM](http://www.speech.sri.com/projects/srilm/) i [IRSTLM](https://github.com/irstlm-team/irstlm).\n",
"Jest to oprogramowanie bogate w opcje, można wybierać różne opcje wygładzania.\n",
"\n",
"Szczytowym osiągnięciem w zakresie $n$-gramowego modelowania języka\n",
"jest wspomniany już KenLM. Ma on mniej opcji niż SRILM czy ISRLM, jest\n",
"za to precyzyjnie zoptymalizowany zarówno jeśli chodzi jakość, jak i\n",
"szybkość działania. KenLM implementuje nieco zmodyfikowane wygładzanie\n",
"Knesera-Neya połączone z **przycinaniem** słownika n-gramów (wszystkie\n",
"*hapax legomena* dla $n \\geq 3$ są domyślnie usuwane).\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Przykładowe wyniki dla KenLM i korpusu Open Subtitles\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"##### Zmiana perplexity przy zwiększaniu zbioru testowego\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"![img](./07_Wygladzanie/size-perplexity.gif \"Perplexity dla różnych rozmiarów zbioru testowego\")\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"##### Zmiana perplexity przy zwiększaniu zbioru uczącego\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"![img](./07_Wygladzanie/size-perplexity2.gif \"Perplexity dla różnych rozmiarów zbioru uczącego\")\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"##### Zmiana perplexity przy zwiększaniu rządu modelu\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"![img](./07_Wygladzanie/order-perplexity.gif \"Perplexity dla różnych wartości rządu modelu\")\n",
"\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.5"
},
"org": null
},
"nbformat": 4,
"nbformat_minor": 1
}

486
wyk/07_Wygladzanie.org Normal file
View File

@ -0,0 +1,486 @@
* Wygładzanie w n-gramowych modelach języka
** Dlaczego wygładzanie?
Wyobraźmy sobie urnę, w której znajdują się kule w $m$ kolorach
(ściślej: w co najwyżej $m$ kolorach, może w ogóle nie być kul w danym
kolorze). Nie wiemy, ile jest ogółem kul w urnie i w jakiej liczbie
występuje każdy z kolorów.
Losujemy ze zwracaniem (to istotne!) $T$ kul, załóżmy, że
wylosowaliśmy w poszczególnych kolorach $\{k_1,\dots,k_m\}$ kul
(tzn. pierwszą kolor wylosowaliśmy $k_1$ razy, drugi kolor — $k_2$ razy itd.).
Rzecz jasna, $\sum_{i=1}^m k_i = T$.
Jak powinniśmy racjonalnie szacować prawdopodobieństwa wylosowania kuli w $i$-tym kolorze ($p_i$)?
Wydawałoby się, że wystarczy liczbę wylosowanych kul w danym kolorze
podzielić przez liczbę wszystkich prób:
$$p_i = \frac{k_i}{T}.$$
*** Wygładzanie — przykład
Rozpatrzmy przykład z 3 kolorami (wiemy, że w urnie mogą być kule
żółte, zielone i czerwone, tj. $m=3$) i 4 losowaniami ($T=4$):
[[./07_Wygladzanie/urna.drawio.png]]
Gdybyśmy w prosty sposób oszacowali prawdopodobieństwa, doszlibyśmy do
wniosku, że prawdopodobieństwo wylosowania kuli czerwonej wynosi 3/4, żółtej — 1/4,
a zielonej — 0. Wartości te są jednak dość problematyczne:
- Za bardzo przywiązujemy się do naszej skromnej próby,
potrzebowalibyśmy większej liczby losowań, żeby być bardziej pewnym
naszych estymacji.
- W szczególności stwierdzenie, że prawdopodobieństwo wylosowania kuli
zielonej wynosi 0, jest bardzo mocnym stwierdzeniem (twierdzimy, że
*NIEMOŻLIWE* jest wylosowanie kuli zielonej), dopiero większa liczba
prób bez wylosowania zielonej kuli mogłaby sugerować
prawdopodobieństwo bliskie zeru.
- Zauważmy, że niemożliwe jest wylosowanie ułamka kuli, jeśli w
rzeczywistości 10% kul jest żółtych, to nie oznacza się wylosujemy
$4\frac{1}{10} = \frac{2}{5}$ kuli. Prawdopodobnie wylosujemy jedną
kulę żółtą albo żadną. Wylosowanie dwóch kul żółtych byłoby możliwe,
ale mniej prawdopodobne. Jeszcze mniej prawdopodobne byłoby
wylosowanie 3 lub 4 kul żółtych.
*** Idea wygładzania
Wygładzanie (ang. /smoothing/) polega na tym, że „uszczknąć” nieco
masy prawdopodobieństwa zdarzeniom wskazywanym przez eksperyment czy
zbiór uczący i rozdzielić ją między mniej prawdopodobne zdarzenia.
*** Wygładzanie +1
Najprostszy sposób wygładzania to wygładzania +1, nazywane też wygładzaniem
Laplace'a, zdefiniowane za pomocą następującego wzoru:
$$p_i = \frac{k_i+1}{T+m}.$$
W naszym przykładzie z urną prawdopodobieństwo wylosowania kuli
czerwonej określimy na $\frac{3+1}{4+3} = \frac{4}{7}$, kuli żółtej —
$\frac{1+1}{4+3}=2/7$, zielonej — $\frac{0+1}{4+3}=1/7$. Tym samym,
kula zielona uzyskała niezerowe prawdopodobieństwo, żółta — nieco
zyskała, zaś czerwona — straciła.
**** Własności wygładzania +1
Zauważmy, że większa liczba prób $m$, tym bardziej ufamy naszemu eksperymentowi
(czy zbiorowi uczącemu) i tym bardziej zbliżamy się do niewygładzonej wartości:
$$\lim_{m \rightarrow \infty} \frac{k_i +1}{T + m} = \frac{k_i}{T}.$$
Inna dobra, zdroworozsądkowo, własność to to, że prawdopodobieństwo nigdy nie będzie zerowe:
$$\frac{k_i + 1}{T + m} > 0.$$
** Wygładzanie w unigramowym modelu języku
*** Analogia do urny
Unigramowy model języka, abstrakcyjnie, dokładnie realizuje scenariusz
losowania kul z urny: $m$ to liczba wszystkich wyrazów (czyli rozmiar słownika $|V|$),
$k_i$ to ile razy w zbiorze uczącym pojawił się $i$-ty wyraz słownika,
$T$ — długość zbioru uczącego.
[[./07_Wygladzanie/urna-wyrazy.drawio.png]]
A zatem przy użyciu wygładzania +1 w następujący sposób estymować
będziemy prawdopodobieństwo słowa $w$:
$$P(w) = \frac{\# w + 1}{|C| + |V|}.$$
*** Wygładzanie $+\alpha$
W modelowaniu języka wygładzanie $+1$ daje zazwyczaj niepoprawne
wyniki, dlatego częściej zamiast wartości 1 używa się współczynnika $0
< \alpha < 1$:
$$P(w) = \frac{\# w + \alpha}{|C| + \alpha|V|}.$$
W innych praktycznych zastosowaniach statystyki
przyjmuje się $\alpha = \frac{1}{2}$, ale w przypadku n-gramowych
modeli języka i to będzie zbyt duża wartość.
W jaki sposób ustalić wartość $\alpha$? Można $\alpha$ potraktować $\alpha$
jako hiperparametr i dostroić ją na odłożonym zbiorze.
*** Jak wybrać wygładzanie?
Jak ocenić, który sposób wygładzania jest lepszy? Jak wybrać $\alpha$
w czasie dostrajania?
Najprościej można sprawdzić estymowane prawdopodobieństwa na zbiorze
strojącym (developerskim). Dla celów poglądowych bardziej czytelny
będzie podział zbioru uczącego na dwie równe części — będziemy
porównywać częstości estymowane na jednej połówce korpusu z
rzeczywistymi, empirycznymi częstościami z drugiej połówki.
Wyniki będziemy przedstawiać w postaci tabeli, gdzie w poszczególnych
wierszach będziemy opisywać częstości estymowane dla wszystkich
wyrazów, które pojawiły się określoną liczbę razy w pierwszej połówce korpusu.
Ostatecznie możemy też po prostu policzyć perplexity na zbiorze testowym
*** Przykład
Użyjemy polskiej części z korpusu równoległego Open Subtitles:
#+BEGIN_SRC
wget -O en-pl.txt.zip 'https://opus.nlpl.eu/download.php?f=OpenSubtitles/v2018/moses/en-pl.txt.zip'
unzip en-pl.txt.zip
#+END_SRC
Usuńmy duplikaty (zachowując kolejność):
#+BEGIN_SRC
nl OpenSubtitles.en-pl.pl | sort -k 2 -u | sort -k 1 | cut -f 2- > opensubtitles.pl.txt
#+END_SRC
Korpus zawiera ponad 28 mln słów, zdania są krótkie, jest to język potoczny, czasami wulgarny.
#+BEGIN_SRC
$ wc opensubtitles.pl.txt
28154303 178866171 1206735898 opensubtitles.pl.txt
$ head -n 10 opensubtitles.pl.txt
Lubisz curry, prawda?
Nałożę ci więcej.
Hey!
Smakuje ci?
Hey, brzydalu.
Spójrz na nią.
- Wariatka.
- Zadałam ci pytanie!
No, tak lepiej!
- Wygląda dobrze!
#+END_SRC
Podzielimy korpus na dwie części:
#+BEGIN_SRC
head -n 14077151 < opensubtitles.pl.txt > opensubtitlesA.pl.txt
tail -n 14077151 < opensubtitles.pl.txt > opensubtitlesB.pl.txt
#+END_SRC
**** Tokenizacja
Stwórzmy generator, który będzie wczytywał słowa z pliku, dodatkowo:
- ciągi znaków interpunkcyjnych będziemy traktować jak tokeny,
- sprowadzimy wszystkie litery do małych,
- dodamy specjalne tokeny na początek i koniec zdania (~<s>~ i ~</s>~).
#+BEGIN_SRC ipython :session mysession :exports both :results raw drawer
from itertools import islice
import regex as re
import sys
def get_words_from_file(file_name):
with open(file_name, 'r') as fh:
for line in fh:
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>'
list(islice(get_words_from_file('opensubtitlesA.pl.txt'), 0, 100))
#+END_SRC
#+RESULTS:
:results:
['<s>', 'lubisz', 'curry', ',', 'prawda', '?', '</s>', '<s>', 'nałożę', 'ci', 'więcej', '.', '</s>', '<s>', 'hey', '!', '</s>', '<s>', 'smakuje', 'ci', '?', '</s>', '<s>', 'hey', ',', 'brzydalu', '.', '</s>', '<s>', 'spójrz', 'na', 'nią', '.', '</s>', '<s>', '-', 'wariatka', '.', '</s>', '<s>', '-', 'zadałam', 'ci', 'pytanie', '!', '</s>', '<s>', 'no', ',', 'tak', 'lepiej', '!', '</s>', '<s>', '-', 'wygląda', 'dobrze', '!', '</s>', '<s>', '-', 'tak', 'lepiej', '!', '</s>', '<s>', 'pasuje', 'jej', '.', '</s>', '<s>', '-', 'hey', '.', '</s>', '<s>', '-', 'co', 'do', '...?', '</s>', '<s>', 'co', 'do', 'cholery', 'robisz', '?', '</s>', '<s>', 'zejdź', 'mi', 'z', 'oczu', ',', 'zdziro', '.', '</s>', '<s>', 'przestań', 'dokuczać']
:end:
**** Empiryczne wyniki
Zobaczmy, ile razy, średnio w drugiej połówce korpusu występują
wyrazy, które w pierwszej wystąpiły określoną liczbę razy.
#+BEGIN_SRC ipython :session mysession :exports both :results raw drawer
from collections import Counter
counterA = Counter(get_words_from_file('opensubtitlesA.pl.txt'))
#+END_SRC
#+RESULTS:
:results:
:end:
#+BEGIN_SRC ipython :session mysession :exports both :results raw drawer
counterA['taki']
#+END_SRC
#+RESULTS:
:results:
48113
:end:
#+BEGIN_SRC ipython :session mysession :exports both :results raw drawer
max_r = 10
buckets = {}
for token in counterA:
buckets.setdefault(counterA[token], 0)
buckets[counterA[token]] += 1
bucket_counts = {}
counterB = Counter(get_words_from_file('opensubtitlesB.pl.txt'))
for token in counterB:
bucket_id = counterA[token] if token in counterA else 0
if bucket_id <= max_r:
bucket_counts.setdefault(bucket_id, 0)
bucket_counts[bucket_id] += counterB[token]
if bucket_id == 0:
buckets.setdefault(0, 0)
buckets[0] += 1
nb_of_types = [buckets[ix] for ix in range(0, max_r+1)]
empirical_counts = [bucket_counts[ix] / buckets[ix] for ix in range(0, max_r)]
#+END_SRC
#+RESULTS:
:results:
:end:
Policzmy teraz jakiej liczby wystąpień byśmy oczekiwali, gdyby użyć wygładzania +1 bądź +0.01.
(Uwaga: zwracamy liczbę wystąpień, a nie względną częstość, stąd przemnażamy przez rozmiar całego korpusu).
#+BEGIN_SRC ipython :session mysession :exports both :results raw drawer
def plus_alpha_smoothing(alpha, m, t, k):
return t*(k + alpha)/(t + alpha * m)
def plus_one_smoothing(m, t, k):
return plus_alpha_smoothing(1.0, m, t, k)
vocabulary_size = len(counterA)
corpus_size = counterA.total()
plus_one_counts = [plus_one_smoothing(vocabulary_size, corpus_size, ix) for ix in range(0, max_r)]
plus_alpha_counts = [plus_alpha_smoothing(0.01, vocabulary_size, corpus_size, ix) for ix in range(0, max_r)]
data = list(zip(nb_of_types, empirical_counts, plus_one_counts, plus_alpha_counts))
vocabulary_size
#+END_SRC
#+RESULTS:
:results:
926594
:end:
#+BEGIN_SRC ipython :session mysession :exports both :results raw drawer
import pandas as pd
pd.DataFrame(data, columns=["liczba tokenów", "średnia częstość w części B", "estymacje +1", "estymacje +0.01"])
#+END_SRC
#+RESULTS:
:results:
liczba tokenów średnia częstość w części B estymacje +1 estymacje +0.01
0 388334 1.900495 0.993586 0.009999
1 403870 0.592770 1.987172 1.009935
2 117529 1.565809 2.980759 2.009870
3 62800 2.514268 3.974345 3.009806
4 40856 3.504944 4.967931 4.009741
5 29443 4.454098 5.961517 5.009677
6 22709 5.232023 6.955103 6.009612
7 18255 6.157929 7.948689 7.009548
8 15076 7.308039 8.942276 8.009483
9 12859 8.045649 9.935862 9.009418
:end:
*** Wygładzanie Gooda-Turinga
Inna metoda — wygładzanie Gooda-Turinga — polega na zliczaniu, ile
$n$-gramów (na razie rozpatrujemy model unigramowy, więc po prostu pojedynczych
wyrazów) wystąpiło zadaną liczbę razy. Niech $N_r$ oznacza właśnie,
ile $n$-gramów wystąpiło dokładnie $r$ razy; na przykład $N_1$ oznacza liczbę /hapax legomena/.
W metodzie Gooda-Turinga używamy następującej estymacji:
$$p(w) = \frac{\# w + 1}{|C|}\frac{N_{r+1}}{N_r}.$$
**** Przykład
#+BEGIN_SRC ipython :session mysession :exports both :results raw drawer
good_turing_counts = [(ix+1)*nb_of_types[ix+1]/nb_of_types[ix] for ix in range(0, max_r)]
data2 = list(zip(nb_of_types, empirical_counts, plus_one_counts, good_turing_counts))
pd.DataFrame(data2, columns=["liczba tokenów", "średnia częstość w części B", "estymacje +1", "Good-Turing"])
#+END_SRC
#+RESULTS:
:results:
liczba tokenów średnia częstość w części B estymacje +1 Good-Turing
0 388334 1.900495 0.993586 1.040007
1 403870 0.592770 1.987172 0.582014
2 117529 1.565809 2.980759 1.603009
3 62800 2.514268 3.974345 2.602293
4 40856 3.504944 4.967931 3.603265
5 29443 4.454098 5.961517 4.627721
6 22709 5.232023 6.955103 5.627064
7 18255 6.157929 7.948689 6.606847
8 15076 7.308039 8.942276 7.676506
9 12859 8.045649 9.935862 8.557431
:end:
Wygładzanie metodą Gooda-Turinga, mimo prostoty, daje wyniki zaskakująco zbliżone do rzeczywistych.
** Wygładzanie dla $n$-gramów
*** Rzadkość danych
W wypadku bigramów, trigramów, tetragramów itd. jeszcze dotkliwy staje się problem
*rzadkości* danych (/data sparsity/). Przestrzeń możliwych zdarzeń
jest jeszcze większa ($|V|^2$ dla bigramów), więc estymacje stają się
jeszcze mniej pewne.
*** Back-off
Dla $n$-gramów, gdzie $n>1$, nie jesteśmy ograniczeni do wygładzania $+1$, $+k$ czy Gooda-Turinga.
W przypadku rzadkich $n$-gramów, w szczególności, gdy $n$-gram w ogóle się nie pojawił w korpusie,
możemy „zejść” na poziom krótszych $n$-gramów. Na tym polega *back-off*.
Otóż jeśli $\# w_{i-n+1}\ldots w_{i-1} > 0$, wówczas estymujemy prawdopodobieństwa
w tradycyjny sposób:
$$P_B(w_i|w_{i-n+1}\ldots w_{i-1}) = d_n(w_{i-n+1}\ldots w_{i-1}\ldots w_{i-1}) P(w_i|w_{i-n+1}\ldots w_{i-1})$$
W przeciwnym razie rozpatrujemy rekurencyjnie krótszy $n$-gram:
$$P_B(w_i|w_{i-n+1}\ldots w_{i-1}) = \delta_n(w_{i-n+1}\ldots w_{i-1}\ldots w_{i-1}) P_B(w_i|w_{i-n+2}\ldots w_{i-1}).$$
Technicznie, aby $P_B$ stanowiło rozkład prawdopodobieństwa, trzeba dobrać współczynniki $d$ i $\delta$.
*** Interpolacja
Alternatywą do metody back-off jest *interpolacja* — zawsze z pewnym współczynnikiem uwzględniamy
prawdopodobieństwa dla krótszych $n$-gramów:
$$P_I(w_i|w_{i-n+1}\ldots w_{i-1}) = \lambda P(w_i|w_{i-n+1}\dots w_{i-1}) + (1-\lambda)
P_I(w_i|w_{i-n+2}\dots w_{i-1}).$$
Na przykład, dla trigramów:
$$P_I(w_i|w_{i-2}w_{i-1}) = \lambda P_(w_i|w_{i-2}w_{i-1}) + (1-\lambda)(\lambda P(w_i|w_{i-1}) + (1-\lambda)P_I(w_i)).$$
** Uwzględnianie różnorodności
*** Różnorodność kontynuacji
Zauważmy, że słowa mogą bardzo różnić się co do różnorodności
kontynuacji. Na przykład po słowie /szop/ spodziewamy się raczej tylko
słowa /pracz/, każde inne, niewidziane w zbiorze uczącym, będzie
zaskakujące. Dla porównania słowo /seledynowy/ ma bardzo dużo
możliwych kontynuacji i powinniśmy przeznaczyć znaczniejszą część masy
prawdopodobieństwa na kontynuacje niewidziane w zbiorze uczącym.
Różnorodność kontynuacji bierze pod uwagę metoda wygładzania
Wittena-Bella, będącą wersją interpolacji.
Wprowadźmy oznaczenie na liczbę możliwych kontynuacji $n-1$-gramu $w_1\ldots w_{n-1}$:
$$N_{1+}(w_1\ldots w_{n-1}\dot\bullet) = |\{w_n : \# w_1\ldots w_{n-1}w_n > 0\}|.$$
Teraz zastosujemy interpolację z następującą wartością parametru
$1-\lambda$, sterującego wagą, jaką przypisujemy do krótszych $n$-gramów:
$$1 - \lambda = \frac{N_{1+}(w_1\ldots w_{n-1}\dot\bullet)}{N_{1+}(w_1\ldots w_{n-1}\dot\bullet) + \# w_1\ldots w_{n-1}}.$$
*** Wygładzanie Knesera-Neya
Zamiast brać pod uwagę różnorodność kontynuacji, możemy rozpatrywać
różnorodność *historii* — w momencie liczenia prawdopodobieństwa dla
unigramów dla interpolacji (nie ma to zastosowania dla modeli
unigramowych). Na przykład dla wyrazu /Jork/ spodziewamy się tylko
bigramu /Nowy Jork/, a zatem przy interpolacji czy back-off prawdopodobieństwo
unigramowe powinno być niskie.
Wprowadźmy oznaczenia na liczbę możliwych historii:
$$N_{1+}(\bullet w) = |\{w_j : \# w_jw > 0\}|$$.
W metodzie Knesera-Neya w następujący sposób estymujemy prawdopodobieństwo unigramu:
$$P(w) = \frac{N_{1+}(\bullet w)}{\sum_{w_j} N_{1+}(\bullet w_j)}.$$
#+BEGIN_SRC ipython :session mysession :exports both :results raw drawer
def ngrams(iter, size):
ngram = []
for item in iter:
ngram.append(item)
if len(ngram) == size:
yield tuple(ngram)
ngram = ngram[1:]
list(ngrams("kotek", 3))
#+END_SRC
#+RESULTS:
:results:
[('k', 'o', 't'), ('o', 't', 'e'), ('t', 'e', 'k')]
:end:
#+BEGIN_SRC ipython :session mysession :exports both :results raw drawer
histories = { }
for prev_token, token in ngrams(get_words_from_file('opensubtitlesA.pl.txt'), 2):
histories.setdefault(token, set())
histories[token].add(prev_token)
#+END_SRC
#+RESULTS:
:results:
:end:
#+BEGIN_SRC ipython :session mysession :exports both :results raw drawer
len(histories['jork'])
len(histories['zielony'])
histories['jork']
#+END_SRC
#+RESULTS:
:results:
:end:
** Narzędzia $n$-gramowego modelowania języka
Istnieje kilka narzędzie do modelowania, ze starszych warto wspomnieć
pakiety [[http://www.speech.sri.com/projects/srilm/][SRILM]] i [[https://github.com/irstlm-team/irstlm][IRSTLM]].
Jest to oprogramowanie bogate w opcje, można wybierać różne opcje wygładzania.
Szczytowym osiągnięciem w zakresie $n$-gramowego modelowania języka
jest wspomniany już KenLM. Ma on mniej opcji niż SRILM czy ISRLM, jest
za to precyzyjnie zoptymalizowany zarówno jeśli chodzi jakość, jak i
szybkość działania. KenLM implementuje nieco zmodyfikowane wygładzanie
Knesera-Neya połączone z *przycinaniem* słownika n-gramów (wszystkie
/hapax legomena/ dla $n \geq 3$ są domyślnie usuwane).
*** Przykładowe wyniki dla KenLM i korpusu Open Subtitles
**** Zmiana perplexity przy zwiększaniu zbioru testowego
#+CAPTION: Perplexity dla różnych rozmiarów zbioru testowego
[[./07_Wygladzanie/size-perplexity.gif]]
**** Zmiana perplexity przy zwiększaniu zbioru uczącego
#+CAPTION: Perplexity dla różnych rozmiarów zbioru uczącego
[[./07_Wygladzanie/size-perplexity2.gif]]
**** Zmiana perplexity przy zwiększaniu rządu modelu
#+CAPTION: Perplexity dla różnych wartości rządu modelu
[[./07_Wygladzanie/order-perplexity.gif]]

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -0,0 +1 @@
<mxfile host="app.diagrams.net" modified="2022-03-25T16:34:28.406Z" agent="5.0 (X11)" etag="svYcTK43C9TQyvdVhZz3" version="17.1.3" type="device"><diagram id="tmWn3La8Q77e07_Ldhyo" name="Page-1">zZfLjpswGIWfhmUrwJgky4ZO28VUM6NIrWZVOeCAFWMjxxTI09cO5jZO1KmUNBApss/v63csXxwQ5fVXgYrsO08wdXw3qR3w2fH9VeCpfy00rQB92AqpIEkreYOwIUdsRNeoJUnwYVJQck4lKaZizBnDsZxoSAheTYvtOJ32WqAUW8ImRtRWf5JEZq269BeD/g2TNOt69sJVG8lRV9jM5JChhFcjCTw4IBKcyzaV1xGmml3Hpa335UK0H5jATL6nwuNL/fojptX66SX9haq0eiXgg2nlN6KlmbAZrGw6AmrchU7GDSUswQI4YF1lROJNgWIdqJTxSstkTlXOU8ktL1XJ5HHbCyjep0KrT6VUzWCjH1q/PajSO0JpxCkXp27BDuqf1jmTI739dF0p+B6PIuHpUxEzJywkri/C8noL1NLFPMdSNKpIVwEY18yy9V2Tr4ZF0GvZaAH4gRGRWXhp3/bgjUoYe/7BqqVlFbK8Ei14Q/cvLl0DE3yDaWVjguco3QqS59uUtmiP2OxQAe/eqIJzqJwIOuuoFPPjBe7NC9p75QlW40SBs1zMj9ji3sTCS8SOrJkfrrvvXQsL10f9WaTUnOUUx/QsZFwfsJMD1UiIkpTps1wxwkpfa4JE3XY+mUBOkkR3c5b/4JB7JQsCOLUgsC0Iz1gAbmaBfchWFn7Vjrp9XoI0cgUdivZKuiO1xnYNZAAGE2TeO5GFN0O2spA98yNDjJTzIhcE7rzIdRvJfBcb9OeGzH6vPCOaEyQYmRe60P1v6FR2eEWeYqOnOHj4Aw==</diagram></mxfile>

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -0,0 +1 @@
<mxfile host="app.diagrams.net" modified="2022-03-25T15:44:28.362Z" agent="5.0 (X11)" etag="xIpOjT2ABrjxa-i-IVjn" version="17.1.3" type="device"><diagram id="tmWn3La8Q77e07_Ldhyo" name="Page-1">3Zddb5swFEB/DY+Tgo0pvDbrtodOVRVpU58mgy9g1WDkOIPs18/EBsJClU4KbVQiRfbxJ+deiOPhddl+VbQuvksGwkMr1nr4s4dQHPjmuwN7CwgiFuSKM4v8EWz4H3Bw5eiOM9hOOmopheb1FKayqiDVE0aVks20WybFdNWa5nACNikVp/QnZ7qwNEI3I/8GPC/6lf0wti0l7Tu7O9kWlMnmCOE7D6+VlNqWynYNonPXe7HjvrzQOmxMQaVfM+D+sX36kYrm9uEx/0WbvHni+JOb5TcVO3fDbrN63xsw+667YroXvGKgsIdvm4Jr2NQ07RoaE3jDCl0KU/NNMZE705PdJwOg6XOuOvqw02YacHxr4+0TU864EGsppDosizPSfTouK33E7dWN1Uo+w1FLeLhMi7snUBraF2X5QwhM6oIsQau96dIPwC5qLm3RytWbMQkGVhwlAAocpC7x8mHuMTam4MLzH6FC50NlZjHPBZwPEN3W9mHJeAtsRn6UQprOSU4iEpDVZSRjNJXsk1PJZMYxWUpx8PEUR/F1KSZvqTjL0LxiFiYhudCrIgivLIvDD5fFBF+Z4ui8YmV/A53HM5rfJXEHi84qfqVVtJTVeFmrjEDEgjmrEUrwxU4O/1q9eWer/Ul6sWR9k1fAidZ4Ma2mOp7KD21Hf23w3V8=</diagram></mxfile>

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB