Wyszukiwanie wyrazów w pliku tekstowym Celem ćwiczenia jest: 1. Poznanie i realizacja praktyczna procedur operacji wejścia/ wyjścia na plikach danych 2. Przegląd algorytmów wyszukiwania wyrazów w plikach 3. Zastosowanie funkcji skrótu w rozwiązywaniu problemów obliczeniowych 4. Oszacowanie czasu obliczeń i wprowadzenie do metod optymalizacji wydajności obliczeń W części praktycznej połączenie uzyskanej wiedzy i umiejętności w celu realizacji praktycznej efektywnego algorytmu Karpa-Rabina [1]. 1. Operacje na plikach tekstowych w języku ADA Biblioteka Ada.Text_IO realizuje funkcje odczytu nie tylko ze urządzeń wejściowych i wyjściowych o typie standardowym. Pozwala na odczyt znaków z plików i pisanie tekstów do plików. Operacje na plikach tekstowych w języku Ada są dobrze opisane w podręczniku. W skrócie są to operacje podobne do dobrze znanych używanych do czytania znaków i linii tekstu z klawiatury. W uproszczeniu, zasadnicza różnica polega na tym, że przed wykonaniem tych operacji należy dokonać jednokrotnie czynności otwarcia pliku przy podaniu jego nazwy, dalej przy operacjach podawać odniesienie do identyfikatora pliku otrzymanego przy otwarciu i w końcu dokonać zamknięcia pliku.ej Infile : File_Type; --- deklaracja zmiennej identyfikującej plik Open(Infile, In_File, "test.txt"); -- otwarcie pliku Infile do odczytu In_File while not End_Of_File(infile) loop Get_Line(Infile, Line, Llength); --- czytanie linia po linii end loop 2. Algorytmy wyszukiwania wzorca w tekście Problem: Dany jest tekst złożony z m znaków T[1..m] oraz przeszukiwany ciąg x[1..n]. złożony z n znaków; gdzie n > m. Problem dopasowania wzorca polega na znalezieniu takiego indeksu i, dla którego T[i.. i+m-1] = x[1.. m]. To oznacza, że wzorzec x jest fragmentem łańcucha T występującym na pozycji i-tej. Często interesujące jest wskazanie wszystkich wystąpień wzorca x w tekście T Rozwiązanie naiwne. 2.1 Algorytm naiwny wersja z wyszukiwania wszystkich wystąpień wzorca. Ustawiamy indeks i na początku tekstu T. Czyli i=1, wybieramy ciąg znaków o długości n poczynając od pierwszej pozycji w tekście T. Porównujemy T[1..n] i wzorzec x[1..n] znak po znaku maksymalnie n razy. Jeśli porównanie przebiegło pomyślnie I=1 jest pierwszym znalezionym rozwiązaniem. Następnie przesuwamy
okno o długości n do następnego znaku czyli i= i+1 i porównujemy nowy wycinek T[i..i+n-1] ze wzorcem jak poprzednio. Zwiększanie i zatrzymujemy, gdy dojdziemy do końca tekstu T. Czyli gdy i > m-n. Złożoność obliczeniowej algorytmu naiwnego jest równa O(n m), gdzie n oznacza liczbę znaków tekstu, a m liczbę znaków wzorca. Jednakże w typowych warunkach algorytm pracuje w czasie O(n), ponieważ najczęściej wystarczy już porównanie kilku początkowych znaków wycinka z wzorcem, aby stwierdzić, iż są one niezgodne. 2.2 Algorytm Rabina-Karpa Istnieje szereg innych algorytmów, ale skoncentrujemy się na algorytmie Karpa-Rabina [1]. Algorytm Rabina- Karpa jest gorszy w wyszukiwaniu pojedynczego wzorca od algorytmów takich jak Knutha-Morrisa-Pratta lub Boyer a-moore a, ponieważ jest najwolniejszy w przypadku pesymistycznego ułożenia ciągu. Jednak jest wyjątkowo efektywny przy wyszukiwaniu wielo -wzorcowym. Czyli jest on szczególnie wydajny jeśli poszukujemy w tekście T[1..m] wielu ciągów o długości n, powiedzmy szukamy j wzorców X j [1..n]. Idea algorytmu polega na wykorzystaniu funkcji skrótu zwanej często funkcją mieszającą a w języku angielskim hash function. Funkcja skrótu jest to funkcja, która przyporządkowuje dowolnie dużej strukturze danych krótką, zwykle posiadającą stały rozmiar, niespecyficzną, pseudo-losową wartość, tzw. skrót nieodwracalny. Algorytm tworzy funkcje skrótu z poszukiwanych ciągów i dalej podobnie jak algorytm naiwny wykorzystując wędrujące znak po znaku okno wyszukuje w tekście T ciągi n wyrazowe i oblicza z nich funkcje skrótu [1]. Porównanie następuje w dwóch etapach: najpierw sprawdzane są funkcje skrótu. a przy ich zgodności przechodzimy do szczegółowego porównania znak po znaku jak w algorytmie naiwnym. Przyspieszenie działania uzyskuje się ponieważ porównanie skrótów jest znacznie szybsze niż całych ciągów szczególnie gdy są bardzo złożone lub gdy raz obliczony skrót może być wykorzystywany wielokrotnie. Efektywność praktycznie przygotowanego algorytmu dwufazowego zależy od wyboru funkcji skrótu. Istotnymi parametrami są: - nakład obliczeniowy potrzebny do wyznaczenia skrótów - algorytmu liczenia skrótów kolizyjność [5] omówiona także poniżej w p.3. poniżej 3. Funkcje skrótu Funkcja skrótu, jednokierunkowa funkcja mieszająca lub funkcja haszująca to funkcja, która przyporządkowuje dowolnie dużej wiadomości krótką, zwykle posiadającą stały rozmiar wartość skrót wiadomości. Kolizja funkcji skrótu H to taka para różnych wiadomości m1, m2, że mają one taką samą wartość skrótu, tj. H(m1) = H(m2) Bezkolizyjna funkcja skrótu to taka w której brak jest kolizji, czyli sytuacji w których z dwóch różnych wiadomości wygenerowany zostanie taki sam skrót. 3.2. Zastosowanie funkcji skrótu w rozwiązywaniu problemów obliczeniowych W informatyce funkcje skrótu pozwalają na ustalenie krótkich i łatwych do weryfikacji sygnatur dla dowolnie dużych zbiorów danych. Takie sygnatury mogą chronić przed przypadkowymi lub celowo wprowadzonymi
modyfikacjami danych (sumy kontrolne), pozwalają się przekonać czy pobrane dane i dokumenty elektroniczne np. programy, dokumentacje, dokumenty urzędowe nie zostały uszkodzone lub celowo zmienione. Powszechnie stosuje się funkcje skrótu w sieciach komputerowych i telekomunikacji zabezpieczając pakiety i ramki przez zniekształceniem w wyniku zakłóceń przy przesyłaniu. Numery systemu IBAN (International Bank Account Number) zostały wprowadzone normą ISO-13616 w celu weryfikacji numerów kont bankowych w międzynarodowym przepływie pieniędzy. W numerach rachunków bankowych IBAN dwie pierwsze cyfry są liczbą kontrolną wynikającą z przeliczenia istotnej informacyjnie zawartości i chronią przed pomyłką pisarską. Często na stronach internetowych podaje się wyliczone skróty w celu weryfikacji (najczęściej są nimi CRC32, MD5 lub SHA-1). Funkcje skrótu mają również zastosowania przy optymalizacji dostępu do struktur danych w programach komputerowych (tablice haszujące), a także wykorzystywane są w podpisie cyfrowym. W podpisie cyfrowym, często spotykanym w życiu codziennym przy przesyłaniu plików PDF z banków. Funkcja skrótu liczona jest z zawartości informacyjnej dokumentu, dalej szyfrowana utajnionym kluczem prywatnym banku i dołączana do pliku PDF, To jest podpis elektroniczny. Taki dokument oraz zaszyfrowany skrót, podpis elektroniczny, mogą być przesyłane bezpiecznie także w niebezpiecznym otoczeniu. Na przykład publiczną siecią komputerową. Odbiorca wykonuje własnego przeliczenia funkcji skrótu i odszyfrowania podpisu za pomocą znanego klucza publicznego osoby podpisującej, banku, w celu uzyskania wartości skrótu obliczonego przez nadawcę. Proste porównania utwierdza nas w przekonaniu, że integralność dokumentu nie został naruszona i że autorem jest posiadacz klucza prywatnego.
Pojawią się wątpliwość dlaczego zaszyfrowano tylko skrót? Przecież można zaszyfrować całą wiadomość i dołączyć tak jak powyżej postąpiono ze skrótem. Odbiorca wykona operację odszyfrowania i porówna wiadomości ze sobą. Jest to możliwe i unika się problemu kolizji. Jednak metoda jest niepraktyczna przy wiadomościach o dużym rozmiarze, ponieważ wtedy sam podpis jest dużego rozmiaru (przy zastosowaniu skrótu podpis ma rozmiar stały i zwykle niewielki w zestawieniu z wiadomością)oraz nakład obliczeniowy potrzebny do sporządzenia podpisu przez nadawcę i do deszyfrowania przez odbiorcę jest duży i zmienny (przy funkcji skrótu stały i typowo znacznie mniejszy). 3.3 Krocząca funkcja skrótu rolling hash. Jak wspomniano do skrócenia czasu obliczeń lub uzyskania dużej efektywności, szybkości obliczeń przy implementacji algorytmu Rabina-Karpa warto starannie dobrać funkcję skrótu w taki sposób, aby obliczać ją możliwe szybko i zadbać o niewielkie prawdopodobieństwo kolizji. W tekście T[1..m] poszukujemy wielu ciągów o długości n, powiedzmy szukamy j wzorców X j [1..j, 1..n]. Jeśli w pierwszym kroku obliczono funkcję skrótu dla T[1..n] to w kolejnym potrzebna jest wartość dla tekstu T[2..n+1] i tak dalej. Widać, że bez względu na długość n szukanego ciągu w kolejnych iteracjach ubył 1 znak i dodany został także jeden Dobrym rozwiązaniem jest zastosowanie funkcji w której nie trzeba za każdym razem obliczać funkcji skrótu, ale można uprościć obliczenia znając wartość poprzednią. Podobnie realizuje się obliczenia przy wyznaczaniu ruchomej średniej zwanej także średnią kroczącą. W naszym przypadku jej odpowiednik, czyli suma krocząca numerów porządkowych znaków w kodzie ASCII jest jednym z możliwych przykładów kroczącej funkcji skrótu. Niezwykle prostą w implementacji oraz znamienną tym, że obliczenie wartości następnej może się opierać na użyciu wartości poprzedniej i zaledwie dwóch operacjach dodatkowych. Dodanie nowej wartości i odjęcie jednej wartości. Przykład. Poszukiwana fraza to: x [1..3] = { m, a, ]; W tekście T T[1..10} = [ A, l, a,, m, a,, A, s, a ]; Funkcja skrótu to suma pozycji trzech kolejnych znaków. Hash := 0; for ix In 1..3 loop Hask := Hash + x[ix]; end loop; Patrząc w tabelę kodowania znaków ASCII otrzymamy H(x) = 109 + 97 + 32 = 238; // ma spacja 238 to poszukiwany skrót Obliczamy skrót napisu T[1..3] sumując pozycje H(T,1) = 65 + 108 + 97 = 270 // Ala 271 wyklucza zgodność, zatem postępujemy dalej licząc skrót dla T[2..4}.
możemy policzyć sumę ale możemy wyznaczając H(T,2) jako H(T,2) = 108 + 97 + 32 = 237 // la spacja także możemy H(T,2) = H(t,1) - 65 + 32 = 270 65 + 32 = 237 // odjąć A dodać spacja. Przy krótkim wzorcu zysk jest niewielki, ale zaoszczędzimy obliczania wraz ze wzrostem długości wzorca, Sprawdzimy jeszcze tylko jeden kolejny krok licząc skrót dla T[3..5] napis a m ; H(T,3) = H(t,3) - 108 + 109 = 237 65 + 32 = 238!!! // odejmujemy znikające l dodajemy m 238 jest poszukiwaną funkcją skrótu zatem wykonujemy fazę drugą czyli porównanie napisów znak po znaku, widać, że to porównanie nie da wyniku pozytywnego. Uwaga1. widać, że litery l i m sąsiadują w alfabecie stąd wynik jest łatwy do przewidzenia. Uwaga 2: podana funkcja skrótu powoduje znaczą ilość kolizji przy swojej prostocie. Wniosek: Prostota jest okupiona częstymi kolizjami. Poszukując frazy Pies i kot wartość funkcji skrótu dla niej będzie równa kot i Pies ale także pies i Kot oraz pies i kot (dlaczego, patrz konstrukcja kodu ASCII i numery porządkowe liter wielkich i małych). Polecaną modyfikacją [8] jest zastąpienie sumy wielomianem. Wybieramy całkowitą liczbę N, ze względu na szybkość liczenia w urządzeniach opartych o zapis binarny N powinno być wielokrotnością liczby 2. Funkcja skrótu, hash, policzona dla tekstu T przy wzorcu o długości m przyjmie postać H (T,1) = N**(m-1)*T(1) + N**(m-2)*T(1) +... N*T(m-1) + T(m); Tu możemy także łatwo obliczyć kolejną wartość skrótu na podstawie poprzedniej w trzech operacjach arytmetycznych. Dodatkowo umiejętnie dobierając liczbę N lub stosując operację modulo można ograniczyć liczbę operacji tylko do dwóch. Proszę porównać z przykładem w [8]. 4. Oszacowanie czasu obliczeń i wprowadzenie do metod skrócenia czasu obliczeń Dotychczas opracowane programy pracowały w oderwaniu od czasu realnego czy astronomicznego. Jeśli chcemy oszacować realny programu musimy skorzystać z funkcji odczytu zegara czasu astronomicznego. Funkcji dostępu do zegara czasu astronomicznego dostarcza moduł Real_Time.. Implementacja tego modułu bywa różna i stąd często różne wyniki zależnie od wersji kompilatora i systemu operacyjnego. W szczególności należy się liczyć z ograniczoną dokładnością funkcji odmierzania czasu. Problemy te opisano także w rozdziale 14.6 skryptu. Poniższy przykład pokaże jakie rozbieżności musimy zaakceptować. Program wykonuje dziesięciokrotnie najkrótsze opóźnienie i oblicza jakie było to opóźnienie w istocie. To świadomie nie użyto określenia w rzeczywistości ponieważ błędy dotyczą w równym stopniu procedury oczekiwania delay jak i procedur odczytu czasu Clock.
with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; use ada.integer_text_io; with Ada.Real_Time; use Ada.Real_Time; procedure Run_time is StartTime, StopTime : Time; begin -- put_line("start"); for ix in 1..10 loop StartTime := Clock; delay Duration'Small; StopTime := Clock; New_Line; put (" "); Put(Natural(To_Duration(StopTime - StartTime)*10**9)); put(" ns"); end loop; end Run_Time; I oto przykładowy wynik pracy. Za pierwszym razem Kolejne uruchomienie 294614 ns 5699 ns 4344 ns 4262 ns 4014 ns 4655 ns 4123 ns 4304 ns 4115 ns 4380 ns 4085 ns 4240 ns 4017 ns 4412 ns 4197 ns 4108 ns 4037 ns 4270 ns 3967 ns 4293 ns 4.1. Wersje debug i release oraz opcje optymalizacji. Projektanci systemów kompilacji opracowując je podążają za typowym tokiem przygotowania programów przez programistów. W początkowej fazie przygotowania programów istotne jest wyłowienie możliwie dużej liczby błędów z ang. bug i usunięcie ich. Ten proces nazywamy debuggowaniem i dobry system kompilacji powinien w tym okresie wykonywać możliwie wiele testów kontrolnych w czasie biegu programu, co odbywa się kosztem wydajności obliczeń. W tej fazie przygotowań warto zastosować profil złożony z różnych opcji. W adagide jest to profil debug. Po zakończeniu fazy uruchamiania i testowania gotowy program można pozbawić opcji kontroli i dodatkowo pozwolić kompilatorowi dokonać optymalizacji czasu wykonania lub optymalizacji zasobów wykorzystywanych przez program. Czyli przygotować wersję produkcyjną po angielsku określaną jako release. Do opracowania Wprawka 1.
Proszę przygotować program testowy jak wyżej i a. sprawdzić jakie czasy wykonania otrzymujemy w przypadku zastosowania opcji debug z parametrem kontroli przekroczeń zakresów gnato oraz w opcji release i przy zastosowaniu optymalizacji o2. b. Czy uwinięcie komentarza z linii linii wypisującej tekst start i zmienia wyniki eksperymentu. Jaki jest wynik obserwacji i jak możemy wytłumaczyć obserwowane wyniki? Zadanie 1. Przygotować program wyszukujący i zliczający słowa w tekście zawartym w pliku metodą naiwną i metodą Rabina-Karpa. Porównać czas obliczeń i dokonać optymalizacji programu od kątem czasu wykonania wykorzystując obserwacje z realizacji wprawki. Wybór funkcji skrótu jest dowolny. Należy pamiętać, że wybór ten i realizacja decyduje o czacie wykonania wyszukiwań. Eksperyment przeprowadzić na anglojęzycznym przekładzie Illiady Homera dostępnym pod adresem http://www.gutenberg.org/files/2199/2199-h/2199-h.htm (zapisać w pliku tekstowym) poszukiwane wyrazy, które należy policzyć to, ciągi sześcioznakowe "Apollo", "Nestor ", "Ulysse", "Saturn Dodatkowo podąć należy liczbę przeczytanych linii tekstu, czyli wykonania procedury Get_line. Poniżej wynik pracy dwóch algorytmów, bez optymalizacji znaleziono: 142 wyrazow Apollo znaleziono: 95 wyrazow Nestor znaleziono: 128 wyrazow Ulysse znaleziono: 84 wyrazow Saturn w czasie 691 ms liczba akapitów xxxx znaleziono: 142 wyrazow Apollo znaleziono: 95 wyrazow Nestor znaleziono: 128 wyrazow Ulysse znaleziono: 84 wyrazow Saturn w czasie 322 ms liczba akapitów xxxx porównać uzyskane wyniki z wynikami osiąganymi przez programy innych grup i dokonać optymalizacji czasu obliczeń.
Literatura [1] Richard M. Karp, Michael O. Rabin. Efficient randomized pattern-matching algorithms. IBM Journal of Research and Development. 31 (2), marzec 1987. [2] EXACT STRING MATCHING ALGORITHMS, http://www-igm.univ-mlv.fr/~lecroq/string/index.html [3] Wirth, N. (2001). Algorytmy + struktury danych = programy. WNT, Warszawa (tłum. zang.). [4] Sedgewick, R. (1983). Algorithms. Addison-Wesley, Reading, Massachussets.. [5] przegląd funkcji skrótu http://www.azillionmonkeys.com/qed/hash.html [6] Rodwald, P. : KRYPTOGRAFICZNE FUNKCJE SKRÓTU. www.rodwald.pl/publikacje/zeszyty_naukowe_amw_2013_2_11.pd [7] Rivest R., The MD4 message-digest algorithm, Advances in Cryptology, Proc. Crypto 90, LNCS 597, Springer- Verlag, 1991. [8] Rolling Hash (Rabin-Karp Algorithm) http://people.csail.mit.edu/alinush/6.006-spring-2014/rec06-rabin-karp-spring2011.pdf