Raport z projektu zaliczeniowego, Sztuczna Inteligencja '07

Rozpoznawanie zniekształconego słowa w zdaniu

Jakub Michaliszyn
Uniwersytet Wrocławski
10 czerwca 2006

Zadanie

Dane: Zdanie, w którym jedno ze słów jest zastąpione wyrażeniem regularnym.
Zadanie: Wyznaczyć najbardziej prawdopodobne w kontekście tego zdania słowo, które pasuje do zadanego wyrażenia regularnego.

Przykładowe zastosowania

Opis metody

Wprowadzenie

Rozważany problem jest mocno zbliżony do problemu dezambiguacji semantycznej, opisanej w [1]. Podobieństwo wynika z tego, że jeśli będziemy traktować wyrażenie regularne jako zadane słowo, a pasujące doń wyrazy jako domniemane znaczenia tego słowa, to problem wyboru odpowiedniego słowa sprowadza się do wyboru odpowiedniego znaczenia. Zatem do rozwiązania mojego problemu wykorzystałem wiedzę, jaką specjaliści z NLP już zgromadzili, uzupełniając znane algorytmy własnymi pomysłami

Problem dezambiguacji może być rozwiązany na kilka sposobów, ale większość wymaga pewnej wiedzy o języku (np. słownika morfosyntaktycznego, tezaurusa) i przez to nie może być łatwo przeniesiona na rozważany wariant problemu. Uniwersalną metodą jest wyliczanie wartości ze wzoru wynikającego (przy pewnych założeniach) ze wzoru Bayesa, opisanego niżej.

Metoda Bayesa - wprowadzenie teoretyczne

Zauważmy, że interesuje nas wybór takiego słowa, które jest najbardziej prawdopodobne w swoim kontekście, czyli obliczenie argmaxw(P(w|k))., gdzie k jest kontekstem, a w przebiega wszystkie pasujące słowa. Z definicji prawdopodobieństwa warunkowego mamy P(w|k) = P(k|w)P(w)/P(k), zatem argmaxw(P(w|k)) = argmaxw(P(k|w)*P(w)). Załóżmy teraz, że k = {k1,k2, ..., ks} (zatem tracimy informację o tym, w jakiej kolejności były słowa - później tę informację będziemy starali się uwzględnić). Naiwne założenie Bayesowskie wygląda następująco:

P(k|w)=P(k1|w)*P(k2|w)*...*P(ks|w)
zatem zakładamy, że wystąpienia poszczególnych słów w zdaniu są niezależne. Z definicji prawdopodobieństwa warunkowego mamy P(ki|w) = P(k i w)/P(w) Zatem otrzymujemy ostatecznie:
argmaxw(P(w|k))=argmaxw(P(w)*P(k1|w)*P(k2|w)*...*P(ks|w))
Z powodu tego, że prawdopodobieństwa mogą być bardzo małe, na ich wartości nakłada się logarytm:
argmaxw(P(w|k))=argmaxw(log (P(w)) + log(P(k1|w)) + ... + log (P(ks|w)))
Dla uniknięcia wartości zerowych stosuje się wygłazdanie prawdopodobieństw. Wyprowadzenie nieco bardziej ogólnego wzoru można znaleźć w [2].

Ograniczanie słownika

Główną przeszkodą przy implementowaniu algorytmu było to, że, w odróżnieniu od zwykłej dezambiguacji, mój zbiór "znaczeń" słowa może być duży, a nawet potencjalnie nieskończony (np. dla wyrażenia .*). Z punktu widzenia samego algorytmu ten problem został ominięty - aplikacja po prostu przyjmuje jako parametr funkcję, która zwraca odpowiedni zbiór słów na podstawie wyrażenia regularnego i kontekstu. Najprostsza funkcja może zwracać wszystkie słowa ze słownika pasujące do wyrażenia, bardziej złożone funkcje mogą korzystać z kontekstu słowa oraz z częstości wystąpień słowa w korpusie. Bardziej szczegółowe omówienie zaimplementowanych funkcji jest w dziale "Uwagi implementacyjne".

Główna pętla algorytmu

W swojej zasadniczej części algorytm przegląda pliki, traktując ich kolejne linie jako konteksty językowe - to mogą być zdania, akapity, wpisy słownikowe, bigramy itp. Niech LSK oznacza liczbę słów kontekstu wejściowego, a LPS oznacza liczbę pasujących słów, zwróconych przez funkcję omawianą w poprzednim podpunkcie. Algorytm wypełnia tablicę macierz[LSK+1][LPS+1] tak, że dla i<LSK i j<LPS w pole macierz[i][j] jest wstawiana liczba wystąpień i-tego słowa z kontekstu wejściowego w jednym językowym wraz z j-tym słowem. Ponadto macierz[LSK][i] zawiera liczbę kontekstów językowych, w których wystąpiło i-te proponowane słowo, a macierz[i][LPS] zawiera liczbę kontekstów językowych, w których wystąpiło i-te słowo z kontekstu wejściowego. Wpis macierz[LSK][LPS] zawiera liczbę wszystkich kontekstów językowych wczytanych przez program przy generowaniu macierzy.

Obliczanie wyników

Po przygotowaniu takiej macierzy algorytm powierza zadanie zwrócenia wyniku drugiej funkcji podanej jako parametr wywołania i kończy wynik. W najprostszej wersji ta funkcja podstawia po prostu odpowiednie liczby do wzoru Bayesa, a w bardziej skomplikowanej różnym czynnikom są przypisywane dodatkowo odpowiednie wagi.

Wykorzystane źródła

Do generowania kontekstów językowych wykorzystałem darmowe materiały dostępne w internecie, w szczególności konkordacje , korpus frekwencyjny oraz słownik słów do gry literaki dostępny na stronie kurnik.pl. Do testów wykorzystałem również stosunkowo mały zbiór bigramów, przygotowany jako jedno z zadań z NLP w minionym semestrze na podstawie bardzo ograniczonego korpusu IPAN ze strony korpus.pl. Do testów zostały wykorzystane losowe strony Wikipedii oraz teksty znalezione w darmowej wersji korpusu PWN . Alternatywne źródła mogą być podawane jako argument w linii poleceń.

Uwagi implementacyjne

Rozwiązanei zostało zaimplementowane w OCamlu. Szczegóły dotyczące kompilacji znajdują się w nagłówku pliku si.ml. Skompilowany program można wywoływać z następuacymi parametrami:
si [-h1 liczba] [-plik1 nazwa] [-h2 liczba] [-w0] [-t] [-k nazwa]* [nazwa_pliku_wejsciowego]
gdzie:

Wpisanie si --help wyświetla pomoc. Jeśli nie zostanie podana nazwa pliku wejściowego, program poprosi o wpisanie zdania z klawiatury. Zdanie wejściowe powinno zawierać jedynie litery i spacje z wyjątkiem wyrażenia regularnego, poprzedzonego znakiem "#".

Dostępne są następujące funkcje heurystyczne wybierające słowa ze słownika:

Natomiast dostępne funkcje obliczające wyniki są następujące:

Program zasadniczo nie obsługuje polskich znaków - po uruchomieniu będzie się starał je usunąć, co uda mu się pod warunkiem, że kodowanie pliku wejściowego będzie identyczne z kodowaniem w pliku plfonts.

Aplikacja wymaga, by pliki podane jako korpus zawierały kolejne konteksty w postaci wyrazów oddzielonych spacją, jeden kontekst w jednej linii, bez polskich znaków. Dodatkowo do aplikacji zostały dołączone dwa programy: tworz_slownik.ml, który z danego korpusu tworzy słownik (opcjonalnie z informacją o częstotliwościach słów) oraz sms.ml, który "psuje" podane zdanie na jeden z kilku sposobów. Szczegóły dotyczące tych programów znajdują się w nagłówkach kodów źródłowych.

Przykładowe testy

Przykładowe sesje

Na początek dwa przykłady pokazujące, że wybór słowa rzeczywiście zależy od kontekstu:

Sesja 1.
Sesja 2.

Teraz przykład wykorzystania programu do rozstrzygania wątpliwości ortograficznych

Sesja 3.

Najczęstsze słowo:

Sesja 4.
Sesja 5.

Teraz test, który miał pokazać, z czym najczęściej występuje słowo "lubi". Uwydatnie on jedną z wad algorytmu - jeśli słowo z konteksty może być wynikiem, to jest ono wysoko punktowane.

Sesja 6.

Przykład na użycie parametrów -w0 i -t

Sesja 7.

Przykład literacki

Duże testy

Do przeprowadzenia testów stworzyłem specjalny program, który "losowo" psuje podane zdanie, a konkretnie zamienia losowo wybrane słowo z podanego zdania na wyrażenie regularne według wybranego schematu - od zamiany słowa na format smsowy (np. kot -> [jkl][mno][tuv]) przez zamianę poszczególnych liter na "." do zamiany słowa na .*. Do testów użyłem zdań z różnych źródeł - stron korpus.pwn.pl, gazeta.pl, wikipedia.org (losowe strony), google.pl i tej dokumentacji. Wyniki obliczałem wywołując program dla kolejnych plików wejściowych w pętli. Szczegółowe wyniki dostępne są w załączonym pliku, natomiast statystyczne wyniki pokazuje poniższa tabelka:

Wynikih2 = 0h2 = 1h2 = 2h2 = 3h2 = 4h2 = 5 Suma
h1 = 0 60,6%58,6%59,6%54,5%57,6%55,6% 57,8%
h1 = 2 57,6%57,6%58,6%55,6%54,5%54,5% 56,4%
h1 = 3 33,48%
Suma* 59,1%58,1%59,1%55,1%56,1%55,1% 57,1%
* - z pominięciem h1 = 3

Wynik dla h1=3 odzwierciedla skuteczność, jaką byśmy uzyskali, gdybyśmy po prostu losowali słowo. Wyniki są do siebie bardzo zbliżone, co wynika po części z faktu, że w wielu przypadkach program znajdował w słowniku co najwyżej jedno pasujące słowo w słowniku, przez co wybór funkcji nie miał znaczenia. Statystyka, która nie uwzględnia tych sytuacji, jest nieco bardziej zróżnicowana:

Wyniki h2 = 0 h2 = 1 h2 = 2 h2 = 3 h2 = 4 h2 = 5 Suma
h1 = 0 50% 47,1% 48,6% 41,4% 45,7% 42,9% 46%
h1 = 2 45,7% 45,7% 47,1% 42,9% 41,4% 41,4% 44%
h1 = 3 11,64%
Suma* 48,3% 46,4% 47,8% 42,2% 43,6% 42,2% 45%
* - z pominięciem h1 = 3

Warto również wspomnieć, że średni czas obliczania przy użyciu funkcji h1 oznaczonej numerem 0 wyniósł 7,5 sekundy, natomiast dla funkcji oznaczonej liczbą 2 - 3,7 sekundy. Po odrzuceniu testów z oczywistą odpowiedzią mamy odpowiednio 10,5 sekundy oraz 5,1 sekundy.

Wnioski i propozycje rozszerzeń

Otrzymana przeze mnie skuteczność w najlepszym przypadku rzędu 60% nie wygląda imponująco. Jednakże nawet dla człowieka problem odzyskania utraconego słowa nie zawsze jest łatwy do rozwiązania, np. w teście "wyznaczyc najbardziej prawdopodobne w kontekscie tego zdania slowo ktore pasuje do #.* wyrazenia regularnego " można wymyślić bardzo wiele pasujących słów (zadanego, badanego, danego, bieżącego, wybranego, ustalonego itd.). Testy pokazują, że obliczanie wyników zgodnie ze wzorem Bayesa jest dużo lepsze, niż naiwne sumowanie wspólnych wystąpień, natomiast najlepsze wyniki uzyskuje się premiując dodatkowo wyrazy bliskie wyrażeniu regularnemu.

Znaczącą poprawę funkcjonalności aplikacji można by uzyskać przez dodanie do niej sprawdzania poprawności gramatycznej, jednakże wymaga to zdobycia pełnego opisu języka. Ma to też tę wadę, że program straci przez to uniwersalność - obecnie zmiana języka wymaga jedynie wymiany używanych plików słownika oraz korpusu. Zauważmy ponadto, że sprawdzanie poprawności gramatycznej można zaszyć w każdej z funkcji heurystycznych. Znaczną poprawę powinno przynieść użycie większego korpusu, jednakże takie korpusy nie są darmowe. Gdyby jednak program miał przetwarzać korpusy o rozmiarze rzędu 1 GB, to konieczne byłoby stworzenie odpowiedniego indeksu odwróconego w celu szybkiego przeszukiwania danych.

Literatura

  1. Word sense disambiguation, Wikipedia angielska.
  2. Naive Bayes classifier, Wikipedia angielska.

Valid HTML 4.01! Valid CSS!