diff --git a/wyk/05_Wygladzanie.org b/wyk/05_Wygladzanie.org index a0d4c63..f30668a 100644 --- a/wyk/05_Wygladzanie.org +++ b/wyk/05_Wygladzanie.org @@ -96,7 +96,11 @@ $$P(w) = \fraq{\# w + 1}{|C| + |V|}.$$ 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$. W innych praktycznych zastosowaniach statystyki +< \alpha < 1$: + +$$P(w) = \fraq{\# 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ść. @@ -120,6 +124,178 @@ wyrazów, które pojawiły się określoną liczbę razy w pierwszej połówce k 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 python :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) + 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 python :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 python :session mysession :exports both :results raw drawer +counterA['taki'] +#+END_SRC + +#+RESULTS: +:results: +42330 +:end: + +#+BEGIN_SRC python :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 python :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: +1181065 +:end: + +#+BEGIN_SRC python :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 494664 1.805294 0.991839 0.009999 +1 528998 0.591116 1.983678 1.009917 +2 154689 1.574443 2.975517 2.009835 +3 81398 2.512285 3.967356 3.009752 +4 52899 3.502240 4.959196 4.009670 +5 37917 4.433763 5.951035 5.009588 +6 28921 5.280834 6.942874 6.009506 +7 23267 6.209438 7.934713 7.009423 +8 19014 7.265909 8.926552 8.009341 +9 15849 8.193135 9.918391 9.009259 +:end: + *** Wygładzanie Gooda-Turinga Inna metoda — wygładzanie Gooda-Turinga — polega na zliczaniu, ile @@ -131,6 +307,34 @@ 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 python :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 494664 1.805294 0.991839 1.069409 +1 528998 0.591116 1.983678 0.584838 +2 154689 1.574443 2.975517 1.578613 +3 81398 2.512285 3.967356 2.599523 +4 52899 3.502240 4.959196 3.583905 +5 37917 4.433763 5.951035 4.576470 +6 28921 5.280834 6.942874 5.631513 +7 23267 6.209438 7.934713 6.537671 +8 19014 7.265909 8.926552 7.501893 +9 15849 8.193135 9.918391 8.670579 +:end: + +Wygładzanie metodą Gooda-Turinga, mimo prostoty, daje wyniki zaskakująco zbliżone do rzeczywistych. + + ** Wygładzanie dla $n$-gramów *** Rzadkość danych