diff --git a/wyk/07_Wygladzanie.ipynb b/wyk/07_Wygladzanie.ipynb new file mode 100644 index 0000000..2515fa2 --- /dev/null +++ b/wyk/07_Wygladzanie.ipynb @@ -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", + "
\n", + "

Modelowanie języka

\n", + "

07. Wygładzanie w n-gramowych modelach języka [wykład]

\n", + "

Filip Graliński (2022)

\n", + "
\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 (`` i ``).\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['', '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', '!', '', '', '-', 'tak', 'lepiej', '!', '', '', 'pasuje', 'jej', '.', '', '', '-', 'hey', '.', '', '', '-', 'co', 'do', '...?', '', '', 'co', 'do', 'cholery', 'robisz', '?', '', '', 'zejdź', 'mi', 'z', 'oczu', ',', 'zdziro', '.', '', '', '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 ''\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": 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 +} diff --git a/wyk/07_Wygladzanie.org b/wyk/07_Wygladzanie.org new file mode 100644 index 0000000..4128060 --- /dev/null +++ b/wyk/07_Wygladzanie.org @@ -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 (~~ i ~~). + + +#+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 '' + for m in re.finditer(r'[\p{L}0-9\*]+|\p{P}+', line): + yield m.group(0).lower() + yield '' + + list(islice(get_words_from_file('opensubtitlesA.pl.txt'), 0, 100)) +#+END_SRC + +#+RESULTS: +:results: +['', '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', '!', '', '', '-', 'tak', 'lepiej', '!', '', '', 'pasuje', 'jej', '.', '', '', '-', 'hey', '.', '', '', '-', 'co', 'do', '...?', '', '', 'co', 'do', 'cholery', 'robisz', '?', '', '', 'zejdź', 'mi', 'z', 'oczu', ',', 'zdziro', '.', '', '', '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]] diff --git a/wyk/07_Wygladzanie/order-perplexity.gif b/wyk/07_Wygladzanie/order-perplexity.gif new file mode 100644 index 0000000..1c52fbf Binary files /dev/null and b/wyk/07_Wygladzanie/order-perplexity.gif differ diff --git a/wyk/07_Wygladzanie/size-perplexity.gif b/wyk/07_Wygladzanie/size-perplexity.gif new file mode 100644 index 0000000..d52c9ba Binary files /dev/null and b/wyk/07_Wygladzanie/size-perplexity.gif differ diff --git a/wyk/07_Wygladzanie/size-perplexity2.gif b/wyk/07_Wygladzanie/size-perplexity2.gif new file mode 100644 index 0000000..53f2002 Binary files /dev/null and b/wyk/07_Wygladzanie/size-perplexity2.gif differ diff --git a/wyk/07_Wygladzanie/urna-wyrazy.drawio b/wyk/07_Wygladzanie/urna-wyrazy.drawio new file mode 100644 index 0000000..d7d7bab --- /dev/null +++ b/wyk/07_Wygladzanie/urna-wyrazy.drawio @@ -0,0 +1 @@ +zZfLjpswGIWfhmUrwJgky4ZO28VUM6NIrWZVOeCAFWMjxxTI09cO5jZO1KmUNBApss/v63csXxwQ5fVXgYrsO08wdXw3qR3w2fH9VeCpfy00rQB92AqpIEkreYOwIUdsRNeoJUnwYVJQck4lKaZizBnDsZxoSAheTYvtOJ32WqAUW8ImRtRWf5JEZq269BeD/g2TNOt69sJVG8lRV9jM5JChhFcjCTw4IBKcyzaV1xGmml3Hpa335UK0H5jATL6nwuNL/fojptX66SX9haq0eiXgg2nlN6KlmbAZrGw6AmrchU7GDSUswQI4YF1lROJNgWIdqJTxSstkTlXOU8ktL1XJ5HHbCyjep0KrT6VUzWCjH1q/PajSO0JpxCkXp27BDuqf1jmTI739dF0p+B6PIuHpUxEzJywkri/C8noL1NLFPMdSNKpIVwEY18yy9V2Tr4ZF0GvZaAH4gRGRWXhp3/bgjUoYe/7BqqVlFbK8Ei14Q/cvLl0DE3yDaWVjguco3QqS59uUtmiP2OxQAe/eqIJzqJwIOuuoFPPjBe7NC9p75QlW40SBs1zMj9ji3sTCS8SOrJkfrrvvXQsL10f9WaTUnOUUx/QsZFwfsJMD1UiIkpTps1wxwkpfa4JE3XY+mUBOkkR3c5b/4JB7JQsCOLUgsC0Iz1gAbmaBfchWFn7Vjrp9XoI0cgUdivZKuiO1xnYNZAAGE2TeO5GFN0O2spA98yNDjJTzIhcE7rzIdRvJfBcb9OeGzH6vPCOaEyQYmRe60P1v6FR2eEWeYqOnOHj4Aw== \ No newline at end of file diff --git a/wyk/07_Wygladzanie/urna-wyrazy.drawio.png b/wyk/07_Wygladzanie/urna-wyrazy.drawio.png new file mode 100644 index 0000000..d300e14 Binary files /dev/null and b/wyk/07_Wygladzanie/urna-wyrazy.drawio.png differ diff --git a/wyk/07_Wygladzanie/urna.drawio b/wyk/07_Wygladzanie/urna.drawio new file mode 100644 index 0000000..802ffae --- /dev/null +++ b/wyk/07_Wygladzanie/urna.drawio @@ -0,0 +1 @@ +3Zddb5swFEB/DY+Tgo0pvDbrtodOVRVpU58mgy9g1WDkOIPs18/EBsJClU4KbVQiRfbxJ+deiOPhddl+VbQuvksGwkMr1nr4s4dQHPjmuwN7CwgiFuSKM4v8EWz4H3Bw5eiOM9hOOmopheb1FKayqiDVE0aVks20WybFdNWa5nACNikVp/QnZ7qwNEI3I/8GPC/6lf0wti0l7Tu7O9kWlMnmCOE7D6+VlNqWynYNonPXe7HjvrzQOmxMQaVfM+D+sX36kYrm9uEx/0WbvHni+JOb5TcVO3fDbrN63xsw+667YroXvGKgsIdvm4Jr2NQ07RoaE3jDCl0KU/NNMZE705PdJwOg6XOuOvqw02YacHxr4+0TU864EGsppDosizPSfTouK33E7dWN1Uo+w1FLeLhMi7snUBraF2X5QwhM6oIsQau96dIPwC5qLm3RytWbMQkGVhwlAAocpC7x8mHuMTam4MLzH6FC50NlZjHPBZwPEN3W9mHJeAtsRn6UQprOSU4iEpDVZSRjNJXsk1PJZMYxWUpx8PEUR/F1KSZvqTjL0LxiFiYhudCrIgivLIvDD5fFBF+Z4ui8YmV/A53HM5rfJXEHi84qfqVVtJTVeFmrjEDEgjmrEUrwxU4O/1q9eWer/Ul6sWR9k1fAidZ4Ma2mOp7KD21Hf23w3V8= \ No newline at end of file diff --git a/wyk/07_Wygladzanie/urna.drawio.png b/wyk/07_Wygladzanie/urna.drawio.png new file mode 100644 index 0000000..e69fcd1 Binary files /dev/null and b/wyk/07_Wygladzanie/urna.drawio.png differ