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