Algorytmy bezstratnej kompresji danych
Transkrypt
Algorytmy bezstratnej kompresji danych
Kodowanie Shannona-Fano Kodowanie Shannona-Fano znane było jeszcze przed kodowaniem Huffmana i w praktyce można dzięki niemu osiągnąć podobne wyniki, pomimo, że kod generowany tą metodą nie jest optymalny. Zasada: Symbolom przypisujemy wagi proporcjonalne do prawdopodobieństwa ich wystąpienia w ciągu wejściowym. Następnie budujemy binarne drzewo kodowe, dzieląc zbiór przypisany do danej gałęzi na dwa podzbiory, które mają w przybliżeniu jednakową sumaryczną wagę. Przykład: Poniżej podaną mamy przykładową charakterystykę źródła, uzyskane kody oraz drzewo kodowe z wyszczególnionymi kolejnymi podziałami zbioru symboli: Symbol Waga (prawopodobieństwo) Kod s1 0,05 000 s2 0,1 100 s3 0,1 110 s4 0,15 101 s5 0,15 111 s6 0,2 001 s7 0,25 01 s1, s2, s3, s4, s5, s6, s7 sumaryczna waga: 1 0 s1, s6, s7 s2, s3, s4, s5 sumaryczna waga: 0,5 sumaryczna waga: 0,5 0 1 s1, s6 sumaryczna waga: 0,25 0 s1 1 s7 s2, s4 s3, s5 sumaryczna waga: 0,25 sumaryczna waga: 0,25 1 s6 1 0 0 s2 1 s4 0 s3 1 s5 Algorytm Lempela-Ziva Algorytm Lempela-Ziva (w skrócie LZ) jest, w odróżnieniu do algorytmów Huffmana i ShannonaFano, algorytmem pobierającym bloki o zmiennej długości, kodując je za pomocą kodów o stałej lub zmiennej długości. Należy on do grupy algorytmów słownikowych. Zaletą w porównaniu do Huffmana jest to, że chcąc zakodować dane algorytmem LZ nie musimy znać lub przewidywać rozkładu prawdopodobieństw występowania poszczególnych symboli w ciągu wejściowym. Sam algorytm LZ występuje w dwóch wersjach - LZ77 i LZ78 (poniżej opisana jest ta druga z nich). Dodatkowo, zarówno LZ77 jak i LZ78 doczekały się licznych modyfikacji, z których jedna (LZW) również opisana jest w niniejszym opracowaniu. Podsumowanie istniejących wariantów algorytmu LZ zawiera tabelka: Warianty LZ77 LZR LZSS LZB LZH Warianty LZ78 LZW LZC LZT LZMW LZJ LZFG Algorytm LZ78 Zasada: dzielimy ciąg wejściowy na części, które są najkrótszymi podciągami (blokami) nie napotkanymi do tej pory. Innymi słowy, dzieląc ciąg na bloki zatrzymujemy się po pierwszym elemencie, który sprawia, że nasz blok „nie pasuje” do żadnego napotkanego wcześniej. Załóżmy, że mamy ciąg symboli o dwuelementowym alfabecie D={a, b} aaababbbaaabaaaaaaabaabb Zgodnie z powyższą zasadą pierwszym blokiem naszego ciągu będzie ‘a’, następnym ‘aa’, później ‘b’, ‘ab’ itd. W rezultacie otrzymamy następujący podział: a|a a|b|a b|b b|a a a|b a|a a a a|a a b|a a b b Chcąc zakodować ciąg, indeksujemy kolejno bloki od 1 do n, dodając dodatkowo „pusty” blok o indeksie 0. indeks 0 1 2 3 4 5 6 7 8 9 10 ø|a|a a|b|a b|b b|a a a|b a|a a a a|a a b|a a b b Ciąg wyjściowy kodujemy w formie dwuelementowej indeks|symbol, gdzie indeks jest numerem bloku napotkanego wcześniej, a symbol ostatnim elementem bloku. W tym przypadku kod pierwszego bloku ‘a’ będzie miał postać 0a (pusy ciąg + ‘a’), nastęnego bloku ‘aa’ - 1a (blok o indeksie 1 + ‘a’) itd. W rezultacie otrzymujemy ciąg wyjściowy: indeks 01 2 3 4 5 6 7 8 9 10 ciąg wyjściowy 0a|1a|0b|1b|3b|2a|3a|6a|2b|9b Jak widać, „słownikiem” w algorytmie LZ jest sam zakodowany ciąg symboli, nie ma więc konieczności dodatkowego przesyłania słownika do dekodera. Dodatkowo zauważmy, że nie musimy zakładać stałej liczby bitów na zakodowanie indeksu (przy długich ciągach musiałaby być duża), ponieważ liczba bitów może rosnąć wraz z kolejnym indeksem. Zauważmy, że indeks na pozycji n może być równy co najwyżej n-1, więc liczba bitów potrzebna do jego zakodowania wynosi log 2 n−1 zaokrąglone w górę do najbliższej liczby całkowitej. Uwaga do przykładu: Podany przykładowy ciąg wejściowy jest zbyt krótki, a przede wszystkim dysponuje zbyt ubogim (bo tylko dwuelementowym) alfabetem, przez co kodowanie LZ nie zaowocuje w tym przypadku realną kompresją danych, chociaż już na tak prostym przykładzie można zaobserwować zasadę zastępowania coraz dłuższych ciągów symboli zestawem indeks/symbol. W realnych implementacjach stosuje się najczęściej symbole o rozmiarze jednego bajtu, a sam ciąg wejściowy ma przynajmniej kilka kB długości. W takim przypadku kod LZ jest zazwyczaj wydajniejszy od kodu Huffmana, zwłaszcza w przypadku ciągów, w których istnieją zależności międzysymbolowe (takich jak np. tekst w języku naturalnym), których kod Huffmana ze swojej natury nie jest w stanie uwzględnić. Algorym LZW Jest to modyfikacja algorytmu LZ78 zwana od nazwisk twórców Lempel-Ziv-Welch. Stosowana jest ona m.in. W popularnym formacie plików graficznych gif, a także w kompresorach spotykanych w systemie UNIX. Algorytm: 1. Zainicjuj słownik wszystkimi blokami o długości jeden (tj. wszystkimi symbolami z podstawowego alfabetu źródła) 2. Wyszukaj w ciągu wejściowym jak najdłuższy blok W, który występuje w słowniku 3. Zakoduj W za pomocą jego indeksu w słowniku 4. Dodaj W z dołączonym pierwszym symbolem następnego bloku do słownika 5. Idź do punktu 2 Dekodowanie odbywa się w odwrotnej kolejności. Dekoder „wie”, że ostatni symbol z najświeższego wpisu do słownika jest pierwszym symbolem z następnego bloku do zdekodowania. Wiedza ta umożliwia rozwiązanie potencjalnych konfliktów w dekoderze i zbudowanie słownika „na bieżąco”. Przykład: Załóżmy, że mamy ciąg o dwuelementowym alfabecie D={a,b}, taki sam jak ten, który służył za ilustrację algorytmu LZ. aaababbbaaabaaaaaaabaabb Słownik, zainicjowany wstępnie blokami o długości jeden, wygląda natępująco: indeks blok 0 1 a b Przeglądamy ciąg wejściowy. Pierwszym symbolem jest a, które oczywiście istnieje w słowniku na pozycji 0. Kodujemy więc ‘a’ za pomocą 0, następnie blok wraz z następnym symbolem (również ‘a’ - zaznaczony podkreśleniem) dodajemy do słownika. ciąg wejściowy: a|a a b a b b b a a a b a a a a a a a b a a b b ciąg wyjściowy: 0 słownik: indeks 0 1 2 ciąg a b aa Przeglądając dalej ciąg wejściowy napotykamy na istniejący już w słowniku ciąg ‘aa’, który również kodujemy za pomocą jego indeksu, tym razem 2, a do słownika dodajemy ‘aab’. ciąg wejściowy: a|a a|b a b b b a a a b a a a a a a a b a a b b ciąg wyjściowy: 0 2 słownik: indeks 0 1 2 3 ciąg a b aa aab Postępując tak dalej otrzymujemy zakodowany ciąg: ciąg wejściowy: a|a a|b|a|b|b b|a a|a b|a a a|a a a a|b a|a b|b ciąg wyjściowy: 0 2 1 0 1 6 2 5 8 10 4 5 0 słownik: indeks 0 1 2 3 4 5 6 7 8 9 10 11 12 ciąg a b aa aab ba ab bb bba aaa aba aaaa aaaab baa Teoretycznie słownik może osiągać dowolnie duże rozmiary, w praktyce ogranicza się go np. do 4096 elementów (12 bitów/indeks). Po przekroczeniu tej liczby kolejne wpisy przestają być dodawane. Zmieniając rozmiar słownika możemy regulować stopień kompresji kosztem jej szybkości. Tak samo jak w algorytmie LZ możemy zastosować zmienną długość indeksu, co pozwoli zaoszczędzić dokładnie 2m–1 bitów przy słowniku o rozmiarze 2m. Jak widać, algorytm LZW dokonuje nieco innego (można powiedzieć: mniej optymalnego) podziału ciągu wejściowego niż LZ, za to w ciągu wyjściowym występują tylko same indeksy, co w praktyce czyni algorytm LZW nieco bardziej wydajnym od LZ. Metoda arytmetyczna Metoda arytmetyczna należy, podobnie jak kodowanie Huffmana, do grupy metod statystycznych, czyli opiera się na statystycznym rozkładzie prawdopodobieństwa źródła. Jednak w przeciwieństwie do Huffmana metoda arytmetyczna nie przydziela każdemu z symboli kodu o długości wyrażonej w bitach, ale dąży do obliczenia na podstawie statystyki źródła liczby kodowej opisującej dane źródło - cały ciąg wejściowy kodujemy za pomocą jednej liczby rzeczywistej. Brzmi to efektownie, jednak należy mieć na uwadze, że liczba ta może wymagać bardzo dużej precyzji - dokładność zwykle używanych liczb typu double skończy się po zakodowaniu góra kilkunastu symboli (jednak i z tym można sobie poradzić stosując notację stałoprzecinkową). W rezultacie efektywna długość kodu dla pojedynczego symbolu może wynosić nawet ułamek bita! Osiągnięte to zostało poprzez całkowicie odmienne podejście do kodowania. Niech X = {x(i)} będzie dyskretną zmienną losową, której wartościami będą symbole z alfabetu źródła (w najbardziej typowym przypadku, dla symboli będących bajtami x(i) = i = 0, 1, .., 255). Zaczynamy od podziału przedziału [0, 1) na rozłączne podprzedziały o długości równej prawdopodobieństwu wystąpienia każdego z symboli. Znając statystykę zmiennej X, możemy w tym celu posłużyć się się dystrybuantą: S i =[ F X i−1 , F X i , (1) gdzie Si jest podprzedziałem odpowiedzialnym za symbol o indeksie i. Każda liczba z tego przedziału będzie arytmetyczną reprezentacją danego symbolu. Pobierając teraz symbole ze strumienia wejściowego, korzystając z wyznaczonych podprzedziałów, dla każdego symbolu dokonujemy sukcesywnego zawężania przedziału głównego, w którym mieści się poszukiwania liczba kodowa, będąca wynikiem kodowania. Algorytm kodowania: 1. Dokonaj podziału przedziału [0, 1) na podstawie statystyki źródła za pomocą wzoru (1). Niech S [i] będzie tablicą rekordów o polach beg i end, opisujących przedział dla symbolu o indeksie i, 2. zmiennym m_beg oraz m_end opisującym przedział główny przypisz wartości początkowe odpowiednio 0 i 1, 3. wczytaj symbol z wejścia (oznaczmy go c) i dokonaj korekty górnej i dolnej granicy przedziału głównego za pomocą wzorów: m_beg := m_beg + (m_end – m_beg) * S [c].beg, m_end := m_end + (m_end – m_beg) * S [c].end, 4. powtarzaj punkt 3 aż do wyczerpania się źródła, 5. zapisz do strumienia wyjściowego statystykę źródła oraz liczbę z przedziału [m_beg, m_end), będzie to poszukiwana liczba kodowa. Uwaga: Aby dekoder mógł rozpoznać miejsce, w którym należy przerwać dekodowanie, należy w dodatkowo w strumieniu wyjściowym umieścić informację o liczbie zakodowanych symboli, albo wydzielić w alfabecie dodatkowy symbol EOF. Przykład: Zakodujmy za pomocą podanego powyżej algorytmu ciąg ARYTMETYKA. Oto statystyka tekstu i przypisane na jej podstawie podprzedziały dla kolejnych symboli: Znak Prawd. wystąpienia Podprzedział A 0,2 [0; 0.2) E 0,1 [0,2; 0,3) K 0,1 [0,3; 0,4) M 0,1 [0,4; 05) R 0,1 [0,5; 0,6) T 0,2 [0,6; 0,8) Y 0,2 [0,8; 1) Zawężanie przedziału głównego w kolejnych krokach algorytmu: Ciąg wejściowy m_beg m_end Start 0 1 A 0 0,2 R 0,1 0,12 Y 0,16 0,12 T 0,1184 0,1192 M 0,11872 0,1188 E 0,118736 0,118744 T 0,1187408 0,1187424 Y 0,11874208 0,1187424 K 0,118742176 0,118742208 A 0,118742176 0,1187421824 Zatem w rezultacie mamy przedział główny do którego należy poszukiwana liczba kodowa: [0,118742176, 0,1187421824). Możemy przyjąć dla uproszczenia, że poszukiwaną liczbą będzie dolna granica przedziału, czyli K = 0,118742176 (w praktycznej realizacji opłaca się poszukać w przedziale liczby, dla zapisania której potrzeba jak najmniej miejsc po przecinku). Algorytm dekodowania: 1. Dokonaj podziału przedziału [0, 1), na podstawie odczytanej ze strumienia wejściowego statystyki źródła, za pomocą wzoru (1). Niech S [i] będzie tablicą rekordów o polach beg i end, opisujących przedział dla symbolu o indeksie i (identycznie jak przy kodowaniu), 2. wczytaj liczbę kodową K, 3. zdekoduj symbol opisywany przez K (poprzez sprawdzenie, do jakiego podprzedziału należy K, jeśli K ∈S c to zdekodowanym symbolem jest c). 4. Zmodyfikuj K tak by wyeliminować wpływ zdekodowanego symbolu K= K −S [c ]. beg S [c ]. end −S [c]. beg 5. Powtarzaj kroki 3 i 4 aż do zdekodowania wszystkich znaków (lub napotkania symbolu EOF). Przykład: Korzystając z wyników poprzedniego przykładu: Liczba kodowa K (kolejne modyfikacje) Przedział do którego należy K Symbol odpowiadający przedziałowi 0,118742176 [0; 0,2) A 0,59371088 [0,5; 0,6) R 0,9371088 [0,8; 1) Y 0,685544 [0,6; 0,8) T 0,42772 [0,4; 0,5) M 0,2772 [0,2; 0,3) E 0,772 [0,6; 0,8) T 0,86 [0,8; 1) Y 0,3 [0,3; 0,4) K [0; 0,2) A 0 Jak widać, dysponując tylko jedną liczbą (oraz statystyką źródła, która jednak zajmuje zawsze tyle samo miejsca, niezależnie od długości ciągu) można dokonać jednoznacznego zdekodowania i otrzymać źródłowy ciąg symboli. Zauważmy, że część całkowita liczby K jest zawsze równa 0, wystarczy więc zapamiętać część ułamkową, do reprezentacji której z kolei można wykorzystać notację stałoprzecinkową (kolejne bity odpowiadają za wartości 2-1, 2-2, 2-3 itd.). Ma to tą dodatkową zaletę, że taki ciąg bitów można wydłużać w nieskończoność, w przeciwieństwie do liczby zmiennoprzecinkowej, która ma zawsze skończoną i z góry określoną precyzję. Opracowanie: Łukasz Wołowiec