modelowanie-jezykowe-aitech-cw/wyk/05_Wygladzanie.ipynb

1175 lines
34 KiB
Plaintext
Raw Normal View History

2022-04-03 22:22:24 +02:00
{
"cells": [
{
"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](./05_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](./05_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": [
{
"data": {
"text/plain": [
"['<s>',\n",
" 'lubisz',\n",
" 'curry',\n",
" ',',\n",
" 'prawda',\n",
" '?',\n",
" '</s>',\n",
" '<s>',\n",
" 'nałożę',\n",
" 'ci',\n",
" 'więcej',\n",
" '.',\n",
" '</s>',\n",
" '<s>',\n",
" 'hey',\n",
" '!',\n",
" '</s>',\n",
" '<s>',\n",
" 'smakuje',\n",
" 'ci',\n",
" '?',\n",
" '</s>',\n",
" '<s>',\n",
" 'hey',\n",
" ',',\n",
" 'brzydalu',\n",
" '.',\n",
" '</s>',\n",
" '<s>',\n",
" 'spójrz',\n",
" 'na',\n",
" 'nią',\n",
" '.',\n",
" '</s>',\n",
" '<s>',\n",
" '-',\n",
" 'wariatka',\n",
" '.',\n",
" '</s>',\n",
" '<s>',\n",
" '-',\n",
" 'zadałam',\n",
" 'ci',\n",
" 'pytanie',\n",
" '!',\n",
" '</s>',\n",
" '<s>',\n",
" 'no',\n",
" ',',\n",
" 'tak',\n",
" 'lepiej',\n",
" '!',\n",
" '</s>',\n",
" '<s>',\n",
" '-',\n",
" 'wygląda',\n",
" 'dobrze',\n",
" '!',\n",
" '</s>',\n",
" '<s>',\n",
" '-',\n",
" 'tak',\n",
" 'lepiej',\n",
" '!',\n",
" '</s>',\n",
" '<s>',\n",
" 'pasuje',\n",
" 'jej',\n",
" '.',\n",
" '</s>',\n",
" '<s>',\n",
" '-',\n",
" 'hey',\n",
" '.',\n",
" '</s>',\n",
" '<s>',\n",
" '-',\n",
" 'co',\n",
" 'do',\n",
" '...?',\n",
" '</s>',\n",
" '<s>',\n",
" 'co',\n",
" 'do',\n",
" 'cholery',\n",
" 'robisz',\n",
" '?',\n",
" '</s>',\n",
" '<s>',\n",
" 'zejdź',\n",
" 'mi',\n",
" 'z',\n",
" 'oczu',\n",
" ',',\n",
" 'zdziro',\n",
" '.',\n",
" '</s>',\n",
" '<s>',\n",
" 'przestań',\n",
" 'dokuczać']"
]
},
"execution_count": 1,
"metadata": {},
"output_type": "execute_result"
}
],
"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": 2,
"metadata": {},
"outputs": [],
"source": [
"from collections import Counter\n",
"\n",
"counterA = Counter(get_words_from_file('opensubtitlesA.pl.txt'))"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"48113"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"counterA['taki']"
]
},
{
"cell_type": "code",
"execution_count": 4,
"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": 5,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"926594"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"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": 6,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>liczba tokenów</th>\n",
" <th>średnia częstość w części B</th>\n",
" <th>estymacje +1</th>\n",
" <th>estymacje +0.01</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>388334</td>\n",
" <td>1.900495</td>\n",
" <td>0.993586</td>\n",
" <td>0.009999</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>403870</td>\n",
" <td>0.592770</td>\n",
" <td>1.987172</td>\n",
" <td>1.009935</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>117529</td>\n",
" <td>1.565809</td>\n",
" <td>2.980759</td>\n",
" <td>2.009870</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>62800</td>\n",
" <td>2.514268</td>\n",
" <td>3.974345</td>\n",
" <td>3.009806</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4</th>\n",
" <td>40856</td>\n",
" <td>3.504944</td>\n",
" <td>4.967931</td>\n",
" <td>4.009741</td>\n",
" </tr>\n",
" <tr>\n",
" <th>5</th>\n",
" <td>29443</td>\n",
" <td>4.454098</td>\n",
" <td>5.961517</td>\n",
" <td>5.009677</td>\n",
" </tr>\n",
" <tr>\n",
" <th>6</th>\n",
" <td>22709</td>\n",
" <td>5.232023</td>\n",
" <td>6.955103</td>\n",
" <td>6.009612</td>\n",
" </tr>\n",
" <tr>\n",
" <th>7</th>\n",
" <td>18255</td>\n",
" <td>6.157929</td>\n",
" <td>7.948689</td>\n",
" <td>7.009548</td>\n",
" </tr>\n",
" <tr>\n",
" <th>8</th>\n",
" <td>15076</td>\n",
" <td>7.308039</td>\n",
" <td>8.942276</td>\n",
" <td>8.009483</td>\n",
" </tr>\n",
" <tr>\n",
" <th>9</th>\n",
" <td>12859</td>\n",
" <td>8.045649</td>\n",
" <td>9.935862</td>\n",
" <td>9.009418</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" 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"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"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": 7,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>liczba tokenów</th>\n",
" <th>średnia częstość w części B</th>\n",
" <th>estymacje +1</th>\n",
" <th>Good-Turing</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>388334</td>\n",
" <td>1.900495</td>\n",
" <td>0.993586</td>\n",
" <td>1.040007</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>403870</td>\n",
" <td>0.592770</td>\n",
" <td>1.987172</td>\n",
" <td>0.582014</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>117529</td>\n",
" <td>1.565809</td>\n",
" <td>2.980759</td>\n",
" <td>1.603009</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>62800</td>\n",
" <td>2.514268</td>\n",
" <td>3.974345</td>\n",
" <td>2.602293</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4</th>\n",
" <td>40856</td>\n",
" <td>3.504944</td>\n",
" <td>4.967931</td>\n",
" <td>3.603265</td>\n",
" </tr>\n",
" <tr>\n",
" <th>5</th>\n",
" <td>29443</td>\n",
" <td>4.454098</td>\n",
" <td>5.961517</td>\n",
" <td>4.627721</td>\n",
" </tr>\n",
" <tr>\n",
" <th>6</th>\n",
" <td>22709</td>\n",
" <td>5.232023</td>\n",
" <td>6.955103</td>\n",
" <td>5.627064</td>\n",
" </tr>\n",
" <tr>\n",
" <th>7</th>\n",
" <td>18255</td>\n",
" <td>6.157929</td>\n",
" <td>7.948689</td>\n",
" <td>6.606847</td>\n",
" </tr>\n",
" <tr>\n",
" <th>8</th>\n",
" <td>15076</td>\n",
" <td>7.308039</td>\n",
" <td>8.942276</td>\n",
" <td>7.676506</td>\n",
" </tr>\n",
" <tr>\n",
" <th>9</th>\n",
" <td>12859</td>\n",
" <td>8.045649</td>\n",
" <td>9.935862</td>\n",
" <td>8.557431</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" 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"
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"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": 8,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[('k', 'o', 't'), ('o', 't', 'e'), ('t', 'e', 'k')]"
]
},
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
],
"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": 9,
"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": 12,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"321"
]
},
"execution_count": 12,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"len(histories['jork'])\n",
"len(histories['zielony'])"
]
},
{
"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](./05_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](./05_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](./05_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.2"
},
"org": null
},
"nbformat": 4,
"nbformat_minor": 1
}