diff --git a/wyk/05_Wygladzanie.ipynb b/wyk/05_Wygladzanie.ipynb new file mode 100644 index 0000000..d3fb5f9 --- /dev/null +++ b/wyk/05_Wygladzanie.ipynb @@ -0,0 +1,1174 @@ +{ + "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 (`` i ``).\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['',\n", + " 'lubisz',\n", + " 'curry',\n", + " ',',\n", + " 'prawda',\n", + " '?',\n", + " '',\n", + " '',\n", + " 'nałożę',\n", + " 'ci',\n", + " 'więcej',\n", + " '.',\n", + " '',\n", + " '',\n", + " 'hey',\n", + " '!',\n", + " '',\n", + " '',\n", + " 'smakuje',\n", + " 'ci',\n", + " '?',\n", + " '',\n", + " '',\n", + " 'hey',\n", + " ',',\n", + " 'brzydalu',\n", + " '.',\n", + " '',\n", + " '',\n", + " 'spójrz',\n", + " 'na',\n", + " 'nią',\n", + " '.',\n", + " '',\n", + " '',\n", + " '-',\n", + " 'wariatka',\n", + " '.',\n", + " '',\n", + " '',\n", + " '-',\n", + " 'zadałam',\n", + " 'ci',\n", + " 'pytanie',\n", + " '!',\n", + " '',\n", + " '',\n", + " 'no',\n", + " ',',\n", + " 'tak',\n", + " 'lepiej',\n", + " '!',\n", + " '',\n", + " '',\n", + " '-',\n", + " 'wygląda',\n", + " 'dobrze',\n", + " '!',\n", + " '',\n", + " '',\n", + " '-',\n", + " 'tak',\n", + " 'lepiej',\n", + " '!',\n", + " '',\n", + " '',\n", + " 'pasuje',\n", + " 'jej',\n", + " '.',\n", + " '',\n", + " '',\n", + " '-',\n", + " 'hey',\n", + " '.',\n", + " '',\n", + " '',\n", + " '-',\n", + " 'co',\n", + " 'do',\n", + " '...?',\n", + " '',\n", + " '',\n", + " 'co',\n", + " 'do',\n", + " 'cholery',\n", + " 'robisz',\n", + " '?',\n", + " '',\n", + " '',\n", + " 'zejdź',\n", + " 'mi',\n", + " 'z',\n", + " 'oczu',\n", + " ',',\n", + " 'zdziro',\n", + " '.',\n", + " '',\n", + " '',\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 ''\n", + " for m in re.finditer(r'[\\p{L}0-9\\*]+|\\p{P}+', line):\n", + " yield m.group(0).lower()\n", + " yield ''\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": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
liczba tokenówśrednia częstość w części Bestymacje +1estymacje +0.01
03883341.9004950.9935860.009999
14038700.5927701.9871721.009935
21175291.5658092.9807592.009870
3628002.5142683.9743453.009806
4408563.5049444.9679314.009741
5294434.4540985.9615175.009677
6227095.2320236.9551036.009612
7182556.1579297.9486897.009548
8150767.3080398.9422768.009483
9128598.0456499.9358629.009418
\n", + "
" + ], + "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": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
liczba tokenówśrednia częstość w części Bestymacje +1Good-Turing
03883341.9004950.9935861.040007
14038700.5927701.9871720.582014
21175291.5658092.9807591.603009
3628002.5142683.9743452.602293
4408563.5049444.9679313.603265
5294434.4540985.9615174.627721
6227095.2320236.9551035.627064
7182556.1579297.9486896.606847
8150767.3080398.9422767.676506
9128598.0456499.9358628.557431
\n", + "
" + ], + "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 +}