Metody słownikowe KOMPRESJA

58  Download (0)

Pełen tekst

(1)

KOMPRESJA

Metody słownikowe

Aleksandra Kwiecień, nr albumu: 283483

(2)

Kompresja danych oznacza zmniejszenie bądź usunięcie redundancji, czyli nadmiarowości w ich pierwotnej reprezentacji.

Redundancja zależy od typu danych, stąd wiele algorytmów służących do kompresji powinno być stosowanych tylko do określonego typu danych wejściowych.

Metody słownikowe, stanowiące temat tego referatu, są jednak algorytmami ogólnymi, które z powodzeniem można stosować do różnych typów danych. Dodatkowo ich przewagą w stosunku do metod statystycznych, które również stanowią metody ogólne, jest to, że nie opierają się na modelach statystycznych, od których jakości zależy później jakość kompresji. W zamian pozwalają one na wyodrębnienie oraz zakodowanie ciągów symboli jako tokenu, przy pomocy słownika. Metody słownikowe pozwalają na kompresję ciągu n znaków do nH bitów, gdzie H oznacza entropię tego ciągu.

WSTĘP

(3)

SŁOWNIKI STATYCZNE

Słowniki stosowane w metodach słownikowych mogą być statyczne bądź dynamiczne:

1. Słowniki statyczne:

Słowniki statyczne są stałe. Dopuszcza się dodawanie do nich nowych elementów, ale nie można uprzednio dodanych elementów z nich usunąć.

Przykład: Słownik językowy używany do kompresji tekstu. Przyjmijmy, że w słowniku znajduje się 500 000 słów pochodzących z języka angielskiego. Słowa są wyodrębniane z tekstu, a następnie dochodzi do przeszukania słownika:

• Jeżeli dane słowo zostanie odnalezione, wówczas indeks tego słowa w słowniku zostaje zapisany do danych wyjściowych,

• W przeciwnym wypadku słowo zostaje wypisane w danych wyjściowych oraz istnieje możliwość dodania go do słownika.

(4)

SŁOWNIKI DYNAMICZNE

2. Słowniki dynamiczne:

Słowniki dynamiczne charakteryzują się tym, że elementy w nich zawarte nie są stałe. Możliwe jest dodawanie oraz usuwanie elementów w czasie czytania danych.

Przykład: Kompresja pliku zawierającego kod źródłowy programu komputerowego. W tym przypadku użycie słownika języka angielskiego nie jest wyborem optymalnym ze względu na występowanie słów wywodzących się z danego języka programowania, a niemożliwych do odnalezieniu w słowniku języka angielskiego. Wówczas zastosowanie słownika dynamicznego pozwala na rozpoczęcie pracy z pustym słownikiem, bądź słownikiem zawierającym ograniczoną liczbę wyrazów typowych dla rozważanego języka, dodawanie nowych słów, które zostaną rozpoznane w czasie czytania, a także usuwania takich, które nie pojawiły się w tekście, bądź pojawiały się najrzadziej.

(5)

IDEA

Metody słownikowe wymagają wykorzystania pętli, za pomocą której rozważany tekst dzielony jest na ciągi symboli (mogą być rozpoznawane na przykład przez obecność spacji). Kiedy słowo zostanie odnalezione dochodzi do przeszukania słownika w celu ustalenia czy dany element w nim występuje:

• W przypadku odnalezienia elementu w słowniku, do outputu zapisywany jest jego token,

• Jeżeli słowo nie zostanie odnalezione, to zostaje wypisane do outputu. Istnieje również możliwość dodania rozważanego nieskompresowanego słowa do słownika. Dodatkowo, w przypadku słowników dynamicznych, na końcu każdej iteracji następuje sprawdzenie czy istnieje „stary” element w słowniku, który powinien zostać usunięty (takie działanie pozwala na kontrolę rozmiaru słownika).

Typ słownika dobiera się w zależności od potrzeb, jednak to słowniki dynamiczne są preferowane w praktyce ze względu na swoją elastyczność.

(6)

ZALETY METOD SŁOWNIKOWYCH

Metody słownikowe mają dwie główne zalety:

1. Nie wymagają obliczeń numerycznych. W zamian pozwalają na osiągnięcie kompresji za pomogą operacji na ciągach znaków.

2. Dekoder w metodach słownikowych (nazywany metodą asymetryczną) jest prosty.

Dane wyjściowe, otrzymane za pomocą metody słownikowej, mogą zawierać informacje różnego typu, na przykład indeksy oraz elementy nieskompresowane. Istotnym jest zatem, aby dekoder rozważanej metody był w stanie je od siebie odróżnić.

W przypadku metod słownikowych, dekoder musi zdecydować czy dane słowo jest tokenem czy danymi nieskompresowanymi, następnie użyć tokenu w celu uzyskania danych ze słownika i zwrócić dane nieskompresowane. Nie musi zatem przeszukiwać ponownie słownika oraz nie musi w sposób skomplikowany dzielić danych wejściowych.

(7)

TOKEN

Postać tokenu może się różnić w zależności od przyjętej metody słownikowej.

Rozważmy w pierwszej kolejności przykładowy token dla słownika posiadającego 219 = 524.288 elementów. Wówczas możemy przyjąć 20-bitowy token, z czego pierwszy bit stanowi flagę (na przykład 0 – element odnaleziony w słowniku, 1 w przeciwnym przypadku), natomiast pozostałych 19 bitów przechowuje:

• Indeks w zapisie binarnym elementu odnalezionego w słowniku,

• Rozmiar elementu oraz sam element w przypadku nieodnalezienia go w słowniku w zapisie binarnym. W przypadku konieczności wypisania elementu często wykorzystuje się kod ASCII.

(8)

PRZYKŁAD

W ramach przykładu przyjmijmy, że słowo „bet” zostało odnalezione w słowniku na 1025 pozycji. Wówczas flagą, znajdującą się w pierwszym bicie tokenu jest 0, natomiast pozostałych 19 bitów przechowuje liczbę 1025 w zapisie binarnym:

0|0000000010000000001.

Załóżmy teraz, że słowo „xet” nie zostało odnalezione w słowniku. Wówczas jego token zawiera flagę 1 w swoim pierwszym bicie, w kolejnych siedmiu bitach przechowuje liczbę znaków w słowie

„xet” (zatem 3 w zapisie binarnym), a 3 ostatnie bajty 8-bitowe oznaczają symbole, z których składa się słowo „xet” zapisane za pomocą zapisu binarnego ich kodów ASCII:

1 0000011 01111000|01100101|01110100.

(9)

OSIĄGNIĘCIE KOMPRESJII

Osiągnięcie kompresji zależy od ilości dopasowań elementów ze strumienia danych wejściowych do elementów znajdujących się w słowniku.

Zatem istotnym jest ustalenie minimalnej liczby dopasowań jaka musi zajść, aby udało się osiągnąć ogólną kompresję.

Zakładając, że rozmiar słowa może zostać zapisany za pomocą 7 bitów, a średnia długość słowa to 5 liter, gdzie każda może zostać zapisana za pomocą 8 bitów, wówczas słowo nieskompresowane może zajmować średnio 48 bitów.

Skompresowanie 48 do 20 bitów jest zatem bardzo korzystne, pod warunkiem, że zachodzi wystarczająco często.

Jako P oznaczmy prawdopodobieństwo dopasowania słowa do pozycji w słowniku oraz przyjmijmy, że kompresujemy N słów. Wówczas rozmiar outputu będzie wynosił w bitach:

𝑁 20𝑃 + 48 1 − 𝑃 = 𝑁 48 − 28𝑃 .

Rozmiar całego inputu wynosi w takim przypadku 40N bitów, stąd zakładamy, że kompresja jest osiągnięta, gdy:

𝑁 48 − 28𝑃 < 40𝑁 ⇒ 𝑃 > 0.29.

W rozważanym przypadku potrzebujemy zatem wskaźnika dopasowania na poziomie 29%.

(10)

KOMPRESJA

STRINGÓW

(11)

WPROWADZENIE

Metody kompresji oparte na stringach mogą być bardziej wydajne niż takie, które dokonują kompresji pojedynczych symboli.

Dzieje się tak dlatego, że szanse na udaną kompresję rosną, gdy elementy w alfabecie mają bardzo różne prawdopodobieństwa występowania w danych wejściowych. Łatwo zatem zauważyć, że prawdopodobieństwa występowania różnych słów są od siebie bardziej odległe, niż prawdopodobieństwa występowania różnych liter.

W celu wykazania tej własności rozważmy alfabet zawierający 2 litery 𝑎1 oraz 𝑎2 , gdzie prawdopodobieństwo występowania słowa 𝑎1 oznaczamy jako 𝑝1 = 0.8, natomiast słowa 𝑎2 jako 𝑝2 = 0.2. W tym przypadku średnie prawdopodobieństwo wynosi 0.5, natomiast pojęcie o wariancji możemy mieć wyliczając:

0.8 − 0.5 + 0.2 − 0.5 = 0.6.

(12)

PRZYKŁAD

Teraz wyznaczmy wszystkie możliwe słowa uzyskane za pomocą rozważanego alfabetu:

Wówczas średni rozmiar kodu Huffmana wynosi:

1 ∙ 0.64 + 2 ∙ 0.16 + 3 ∙ 0.16 + 3 ∙ 0.04 = 1.56 bitów, co daje 0.78 bitu na symbol.

(13)

WNIOSKI

Przeprowadźmy teraz analogiczne rozumowanie dla słów 3-literowych. Średnie prawdopodobieństwo wynosi 0.125, wariancja 0.792, średni rozmiar kodu Huffmana 2.184 bitów, a to daje 0.728 bitu na symbol.

Generując słowa składające się z coraz większej

ilości liter, prawdopodobieństwa tych słów zaczynają coraz

bardziej odbiegać od średniej, a dodatkowo poprawia się

średnia wielkość kodu Huffmana. Dlatego też kompresja

stringów daje lepsze rezultaty, niż kompresja pojedynczych

symboli. Z tego faktu wynika przede wszystkim popularność

metod słownikowych.

(14)

PROSTA KOMPRESJA

SŁOWNIKOWA

(15)

SCHEMAT

Jest to prosta, 2-etapowa metoda. W pierwszym etapie dochodzi do przeczytania pliku źródłowego i przygotowania na jego podstawie listy wszystkich, różnych od siebie, bajtów znajdujących się w pliku. Następnie w drugim etapie lista ta staje się słownikiem, na podstawie którego przeprowadzana jest kompresja. Metoda ta składa się z następujących kroków:

1. Plik źródłowy jest czytany i tworzona jest lista (może mieć co najwyżej 256 elementów). Lista poza znalezionymi bajtami zawiera również informację o liczbie wystąpień danego bajtu w pliku.

2. Lista zostaje posortowana w kolejności malejącej ze względu na liczbę wystąpień bajtów.

3. Posortowana lista staje się słownikiem, który zostaje zapisany w skompresowanym pliku, poprzedzony swoją długością.

4. Plik źródłowy zostaje przeczytany ponownie bajt po bajcie. Dla każdego bajtu z listy zostaje przeszukany słownik w celu ustalenia jego indeksu w słowniku.

Indeks ten zostaje następnie zapisywany w skompresowanym pliku. Ponieważ indeks jest liczbą z przedziału [0,255], to wymaga 1-8 bitów. Dlatego oprócz samego indeksu, w pliku skompresowanym zostaje zapisany również kod oznaczający jego długość (000 oznacza indeks 1-bitowy, natomiast 111 indeks 8- bitowy).

(16)

KOMPRESOR I DEKOMPRESOR

Kompresor w tej metodzie zawiera krótki, 2-bajtowy bufor, w którym przechowuje bity, które należy zapisać w skompresowanym pliku. Gdy pierwszy bajt jest z bufora zostanie zapisany do pliku, wówczas drugi bajt zostaje przeniesiony na miejsce pierwsze.

Dekompresor rozpoczyna pracę od czytania długości

słownika i samego słownika. Następnie rozkodowuje każdy

bajt czytając jego 3-bitowy kod i indeks, którego używa do

zlokalizowania kolejnego bajtu w słowniku.

(17)

PODSUMOWANIE

Kompresja w prostej metodzie słownikowej zostaje osiągnięta dzięki posortowaniu bajtów ze względu na częstość ich występowania.

Każdy bajt jest zastępowany 4-11 bitami (kod 3- bitowy oraz dodatkowe 1-8 bitów). Wielkość 4-bitowa odpowiada w przypadku tej metody wskaźnikowi kompresji wysokości 0.5, natomiast wielkość 11-bitowa wskaźnikowi kompresji równemu 1.375 (oznaczającemu rozbudowę).

Najgorszy wynik można otrzymać próbując skompresować plik, który zawiera 256 bajtów o tych samych rozkładach.

Dodatkowo należy zwrócić uwagę, że metoda ta

dokonuje kompresji wolno, natomiast dekompresji

zdecydowanie szybciej.

(18)

METODA LZ77

(19)

Częstym źródłem redundancji w danych cyfrowych, niezależnie od ich typu, są powtarzające się wyrażenia. Możemy je znaleźć w danych audio, zdjęciach, danych video czy w tekście. Stanowią one również bazę wszystkich metod słownikowych pozwalając na konstrukcję słownika na swojej podstawie.

Metoda LZ77 (Ziv i Lempel 1977) zakłada zastosowanie słownika dynamicznego, który uzupełniany jest w trakcie czytania danych. Ważną częścią tej metody jest również zagadnienie przesuwającego się okna (ang. sliding window). Zaprezentujmy je na przykładzie kompresji tekstu:

… 𝑠𝑖𝑟_𝑠𝑖𝑑_𝑒𝑎𝑠𝑡𝑚𝑎𝑛_𝑒𝑎𝑠𝑖𝑙𝑦_𝑡|𝑒𝑎𝑠𝑒𝑠_𝑠𝑒𝑎_𝑠𝑖𝑐𝑘_𝑠𝑒𝑎𝑙𝑠 …

Powyższe okno dzieli się na dwie części – bufor słownikowy (ang. search buffer) oraz bufor wejściowy (ang. look-ahead buffer).

WPROWADZENIE

(20)

Bufor słownikowy znajduje się po lewej stronie okna i zawiera bajty, które zostały uprzednio przeczytane i zakodowane. W metodzie LZ77 stanowi on słownik dynamiczny, na podstawie którego kodowane są nowe wyrażenia.

Bufor wejściowy znajduje się natomiast po prawej stronie okna i zawiera bajty, które mają zostać dopiero skompresowane.

Po zakodowaniu wyrażenia z bufora wejściowego, przechodzi ono do bufora słownikowego. Przesuwa się zatem w lewo. Stąd mówi się o przechodzeniu okna.

Zwróćmy dodatkowo uwagę, że bufor słownikowy ma w praktyce zdecydowanie większy rozmiar, niż bufor wejściowy. Może on sięgać kilku tysięcy bajtów i powinien być dobierany do potrzeb.

Oczywiście bufory słownikowe o większych rozmiarach pozwalają na zapamiętanie większej ilości elementów, stąd są bardziej korzystne przy kompresji.

WPROWADZENIE

(21)

KODOWANIE

Przedstawmy teraz logikę kodowania wyrażeń za pomocą metody LZ77 na wprowadzonym wcześniej przykładzie:

… 𝑠𝑖𝑟_𝑠𝑖𝑑_𝑒𝑎𝑠𝑡𝑚𝑎𝑛_𝑒𝑎𝑠𝑖𝑙𝑦_𝑡|𝑒𝑎𝑠𝑒𝑠_𝑠𝑒𝑎_𝑠𝑖𝑐𝑘_𝑠𝑒𝑎𝑙𝑠 …

Kodowanie rozpoczyna się od pobrania pierwszego symbolu z bufora wejściowego. W rozważanym przypadku jest to pierwsza litera „e” w wyrażeniu „eases”. Następnie przeszukany od strony prawej do lewej zostaje bufor słownikowy, w celu odnalezienia znaku

„e”. Jako pierwsze odnalezione zostaje więc „e” w słowie „easily”.

Znajduje się ono na pozycji 8 (licząc od strony prawej).

Po odnalezieniu symbolu w buforze słownikowym, zostaje podjęta próba dopasowania jak największej części wspólnej rozważanego słowa „eases” oraz „easily”, w którym zostało odnalezione pierwsze „e”. Łatwo zauważyć, że częścią wspólną obu wyrażeń jest „eas” o długości 3.

(22)

Następnie podejmowana jest próba wyszukania kolejnego „e”

w następnych wyrażeniach w buforze słownikowym. Przede wszystkim ważne jest jednak odnalezienie takiego „e”, które zapewni dłuższą część wspólną.

W przykładzie kolejnym „e”, które możemy znaleźć w buforze słownikowym jest „e” w słowie „eastman” na pozycji 16. Część wspólna tego słowa i wyrażenia „eases” ma jednak taką samą długość jak wcześniej, czyli 3.

Jest to ostatnie możliwe dopasowanie, stąd kolejnym krokiem jest stworzenie tokenu. W pierwszej kolejności należy podjąć jednak decyzję i przyjąć jedno z dwóch dopasowań:

• Gdy oba dopasowania mają taką samą długość, wybierane jest to na dalszej pozycji,

• Jeżeli dopasowania różnią się długością, to wybrane zostaje to dłuższe.

KODOWANIE

(23)

Po ustaleniu właściwego dopasowania metoda LZ77 przechodzi do wygenerowania tokenu. Składa się on w tym przypadku z 3 pól:

• Pozycji odnalezionego znaku w buforze słownikowym,

• Długości części wspólnej,

• Kolejnego symbolu w buforze wejściowym.

Zatem dla rozważanego przykładu token będzie miał postać:

16, 3, 𝑒 ,

gdzie „e” odnosi się oczywiście do drugiego „e” w wyrażeniu „eases”.

Token zostaje następnie wypisany w outpucie, a okno przesuwa się w prawo o cztery pozycje pozycje – trzy dopasowane i jedną oznaczającą kolejny symbol.

KODOWANIE

(24)

Rozważmy teraz przypadek, w którym nie zostanie znalezione żadne dopasowanie. Wówczas do outputu również zwracany jest token o trzech polach następującej postaci:

• Pozycja w słowniku = 0,

• Długość części wspólnej = 0,

• Nieodnaleziony symbol.

Ze względu na częste występowanie takiej sytuacji, w szczególności na początku kompresji, trzecie pole w słowniku jest niezbędne.

KODOWANIE

(25)

Dekoder w metodzie LZ77 jest zdecydowanie prostszy. Zamiast dwóch buforów, które zawiera koder, ma tylko jeden zawierający wyrażenia z całego okna.

Dekoder pobiera zatem pierwszy token i przeszukuje swój bufor. W outpucie wypisuje następnie dopasowanie (część wspólną) oraz symbol z trzeciego pola w tokenie.

Następnie pobiera kolejny token.

Ze względu na prostotę dekodera, można łatwo zauważyć, że jest on główną zaletą metody LZ77.

DEKODOWANIE

(26)

Rozważmy sytuację, w której token koduje 1 symbol.

Pierwsze pole w tokenie ma rozmiar:

log2 𝑆 ,

gdzie S oznacza długość bufora słownikowego. Przyjmując, że w praktyce może on mieć rozmiar kilku tysięcy bitów, pierwsze pole może mieć około 10-12 bitów, Następnie rozmiar drugiego pola to:

log2 𝐿 − 1 ,

gdzie L jest długością słownika wejściowego, co daje zwykle kilka bitów. Ostatnie pole najczęściej ma rozmiar 8 bitów, jednak można przyjąć, że dla alfabetu długości A, ma ono rozmiar:

log

2

𝐴 .

Zatem ostatecznie rozmiar tokenu jest zdecydowanie większy niż 8 bitów, które stanowią rozmiar kodowanego symbolu.

WADY METODY LZ77

(27)

Opisana metoda LZ77 ma swoje plusy, jak i minusy. Przede wszystkim jej zaletą jest dekoder, który pozwala na łatwą i szybką dekompresję pliku.

Poza uprzednio wspomnianymi wadami tej metody, należy również zwrócić uwagę na to, że koder tej metody może mieć problemy z kompresją pewnych plików. Ponieważ w jego buforze słownikowym znajduje się tylko pewna liczba ostatnich wyrażeń, pliki w których powtarzające się słowa są od siebie bardzo odległe mogą nie zostać wystarczająco dobrze skompresowane.

Można zatem przyjąć, ze metoda ta jest najbardziej korzystna w przypadku, gdy mamy do czynienia ze skompresowanym raz

plikiem, który wymaga częstego dekompresowania.

PODSUMOWANIE

(28)

Istnieje wiele wariantów metody LZ77, które stanowią jej ulepszenie. Niektóre z nich nie są jednak wolne od wad, jak na przykład metoda LZR, która zakłada, że rozmiar obu bufferów jest nieograniczony. Oznacza to oczywiście większą możliwość odnalezienia dopasowania, nawet w przypadku dużej odległości, jednak w praktyce posiadanie nieograniczonych buforów może być niemożliwe lub problematyczne.

Innym przykładem może być metoda LZH, która wybiera najbliższe odnalezione dopasowanie w buforze słownikowym.

Algorytm w tej metodzie jest bardziej skomplikowany, niż w metodzie LZ77, jednak mniejsze wartości w tokenie mogą prowadzić do lepszej kompresji.

Warto zwrócić również uwagę na metodę LZSS, która ulepsza LZ77 w trzech obszarach. Trzyma ona bufor wejściowy w kolejce cyklicznej, a bufor słownikowy w drzewie binarnym oraz tworzy tokeny z dwoma polami, a nie trzema.

WARIANTY LZ77

(29)

METODA LZ78

(30)

WPROWADZENIE

Metoda LZ78 (Ziv i Lempel 1978) również buduje swój słownik na podstawie uprzednio przeczytanych wyrazów. W odróżnieniu jednak od metody LZ77, nie zawiera ona przesuwającego się okna, zatem nie istnieje w niej również podział na bufor słownikowy oraz wejściowy.

Kompresja rozpoczyna się od pustego lub prawie pustego słownika, którego rozmiar jest ograniczony tylko przez ilość dostępnej pamięci. W outpucie otrzymujemy token o dwóch polach. Pierwsze z nich zawiera wskaźnik do słownika, drugie natomiast kod symbolu (na przykład może to być kod ASCII). Każdy token odnosi się do stringa złożonego z symboli, który zapisywany jest w słowniku po zapisaniu tokena w outpucie. W tej metodzie nie dochodzi do usuwania starych elementów ze słownika, jak w metodzie LZ77, co z jednej strony pozwala na kompresję stringów przez elementy słownika, które były dodane w dalekiej przeszłości, ale z drugiej doprowadza do szybkiego wypełniania się słownika oraz pamięci.

(31)

KODOWANIE

Na początku kompresji słownik zawiera tylko pustego stringa na pozycji 0. Następnie nowe elementy są do niego dopisywane od pozycji 1.

Przyjmijmy, że czytamy symbol „x”. Następnie dochodzi do przeszukania słownika w celu odnalezienia tego symbolu. Wtedy:

• Jeżeli element „x” nie zostanie znaleziony w słowniku, dochodzi do dodania go na pierwszym dostępnym miejscu i zapisaniu tokenu postaci 0, 𝑥 , który oznacza „null x”.

• Jeżeli element „x” zostanie jednak znaleziony, na przykład na pozycji 37, to wówczas następny symbol po x, przyjmijmy „y”, jest czytany i rozpoczynają się poszukiwania w słowniku stringa postaci

„xy”. Jeżeli zostanie znaleziony, to oczywiście kolejny symbol jest doklejany do stringa i następnie szukany w słowniku. Jeżeli jednak

„xy” nie zostanie znalezione, to jest dodawane do słownika i zwracany jest token postaci 37, 𝑦 .

(32)

DEKODOWANIE

Dekoder w metodzie LZ78 buduje i utrzymuje słownik

w taki sam sposób co koder. Oznacza to, że jest on

zdecydowanie bardziej skomplikowany, niż dekoder metody

LZ77, stąd w przypadku LZ77 dekodowanie jest prostsze niż

kodowanie, a w przypadku LZ78 jedno i drugie ma podobną

złożoność.

(33)

STRUKTURA SŁOWNIKA

W przypadku metody LZ78 najlepszym wyborem struktury słownika jest struktura drzewa (ale nie binarnego). Taka struktura słownika prowadzi do uproszczenia przeszukiwania go w trakcie trwania kompresji.

Korzeniem rozważanego drzewa zostaje pusty string, który był przyjmowany jako początek słownika. Następnie wszystkie stringi, które nie zostały odnalezione w słowniku (mają wskaźnik równy 0) zostają dodane do drzewa jako dzieci korzenia. Każde z nich staje się korzeniem poddrzewa. Zakładając, że rozważamy alfabet o 8-bitowych symbolach, dzieci korzenia może być maksymalnie 256.

Proces dodawania dzieci korzenia do drzewa powinien być dynamiczny, czyli gdy korzeń jest tworzony, nie powinien rezerwować pamięci na ewentualne dzieci. Żądanie pamięci powinno następować dopiero, gdy dziecko korzenia zostanie znalezione. Ponieważ nie usuwa się żadnych węzłów, to taka praktyka prowadzi do uproszczenia zarządzania pamięcią.

(34)

PRZYKŁAD

Rozważmy kompresję poniższego tekstu za pomocą metody LZ78:

𝑠𝑖𝑟_𝑠𝑖𝑑_𝑒𝑎𝑠𝑡𝑚𝑎𝑛_𝑒𝑎𝑠𝑖𝑙𝑦.

Koder tworzy tokeny w następujący sposób:

SŁOWNIK TOKEN

0 null

1 „s” (0, „s”)

2 „i” (0, „i”)

3 „r” (0, „r”)

4 „_” (0, „_”)

5 „si” (1, „i”)

6 „d” (0, „d”)

7 „_e” (4, „e”)

8 „a” (0, „a”)

9 „st” (1, „t”)

(35)

PRZYKŁAD

SŁOWNIK TOKEN

10 „m” (0, „m”)

11 „an” (8, „n”)

12 „_ea” (7, „a”)

13 „sil” (5, „l”)

14 „y” (0, „y”)

(36)

ZARZĄDZANIE PAMIĘCIĄ

Poza przypadkiem, gdy dane wejściowe są wyjątkowo małych rozmiarów, metoda LZ78 prowadzi do szybkiego zapełniania się słownika. Wówczas w celu uzyskania pamięci można wprowadzić jedno z następujących rozwiązań:

1. Zamrożenie słownika w momencie zapełnienia. Staje się on słownikiem statycznym i w takiej formie może nadal służyć do kodowania wyrazów.

2. Najbardziej naturalnym rozwiązaniem w przypadku wypełnienia się słownika jest usunięcie jego elementów, które był używane najrzadziej. Nie ma jednak jednego wystarczająco dobrego algorytmu, który mógłby zdecydować, które wyrazy i jak dużo z nich usunąć.

(37)

ZARZĄDZANIE PAMIĘCIĄ

3. Usunięcie całego drzewa i rozpoczęcie nowego. Takie rozwiązanie dzieli dane wejściowe na bloki, które posiadają własne słowniki.

Jeżeli zawartość inputu różni się między blokami to zapewnia to dobrą kompresję, ponieważ eliminuje słowniki, które są mało prawdopodobne. Możemy powiedzieć, że w tym rozwiązaniu zakładamy, że kolejne symbole będą bardziej korzystać z nowych danych niż ze starych, podobnie jak w metodzie LZ77, czyli, że te same elementy znajdują się w niedużej odległości od siebie.

(38)

METODA LZW

(39)

WPROWADZENIE

Metoda LZW jest popularnym wariantem wprowadzonej wcześniej metody LZ78. Charakteryzuje się eliminacją drugiego pola tokenu, co oznacza, że token w metodzie LZW zawiera jedynie wskaźnik do słownika.

Metoda LZW daje bardzo dobre rezultaty, będąc jednocześnie całkiem łatwa do zaprogramowania. Wykorzystywana jest między innymi w formacie GIF, PDF i PostScript. Dodatkowo metoda ta przyczyniła się do powstania formatu PNG, ze względu na to, że była przez pewnie czas objęta patentem i z tego powodu zaistniała potrzeba podjęcia prac nad nowym algorytmem kompresji obrazów.

(40)

SŁOWNIK

W celu pełnego zrozumienia metody LZW, przyjmijmy najpierw, że słownik ma strukturę arraya, którego elementami są stringi.

Metoda LZW rozpoczyna swoje działanie od inicjalizacji

słownika. Dodawane są do niej wszystkie znaki rozważanego

alfabetu, stąd w najczęstszym przypadku 8-bitowym, pierwsze

256 pozycji słownika (od 0 do 255) jest zajętych jeszcze przed

rozpoczęciem czytania danych wejściowych. Dodatkowo ze

względu na sposób inicjalizacji słownika, pierwszy przeczytany

element zawsze jest możliwy do odnalezienia w słowniku. Z

tego powodu token tej metody może posiadać tylko jedno pole.

(41)

KODOWANIE

Koder w metodzie LZW działa w podobny sposób, co w metodzie LZ78. Pobiera on pierwszy symbol i przeszukuje słownik.

Oczywiście pierwszy element danych wejściowych zawsze będzie możliwy do odnalezienia. Następnie czytany jest kolejny symbol i, tak jak w przypadku LZ78, dołączany do pierwszego symbolu tworząc stringa.

• Jeżeli taki string zostanie odnaleziony w słowniku, wówczas czytany i dołączany jest kolejny symbol,

• Jeżeli string nie zostanie jednak odnaleziony, koder zwraca wskaźnik do ostatniego odnalezionego stringa i zapisuje w słowniku string nieodnaleziony. Następnie inicjalizuje nowy string, którego początkiem jest ostatni symbol (ten, przez którego słowo nie zostało odnalezione).

(42)

KODOWANIE

W ramach przykładu rozważmy ponownie dane wejściowe następującej postaci:

𝑠𝑖𝑟_𝑠𝑖𝑑_𝑒𝑎𝑠𝑡𝑚𝑎𝑛_𝑒𝑎𝑠𝑖𝑙𝑦_𝑡𝑒𝑎𝑠𝑒𝑠_𝑠𝑒𝑎_𝑠𝑖𝑐𝑘_𝑠𝑒𝑎𝑙𝑠

Metoda LZW dla powyższego inputu podejmuje następujące kroki:

1. Inicjalizuje słownik i zapisuje w nim wszystkie 256 elementów na pozycjach 0-255.

2. Czyta pierwszy symbol „s” i poszukuje go w słowniku. Symbol „s”

zostaje znaleziony na miejscu 115 (na podstawie kodu ASCII).

Następnie czytany jest kolejny symbol „i” oraz doklejany do „s”. W taki sposób metoda otrzymuje stringa postaci „si”, którego nie może znaleźć już w słowniku. Wówczas:

– Zwracany jest token postaci 115,

– String „si” zostaje zapisany na miejscy 256 w słowniku,

– Zainicjalizowany zostaje string, którego początkiem jest „i”.

(43)

KODOWANIE

3. Metoda czyta kolejny symbol postaci „r” i znajduje go w słowniku na miejscu 105. Następnie dokleja go do „i” otrzymując stringa postaci „ir”, którego nie może znaleźć w słowniku. Wówczas:

– Zwraca token postaci 105,

– Zapisuje string „ir” na miejscu 257 w słowniku, – Inicjalizuje stringa dla „r”.

4. Metoda pracuje w opisany powyżej sposób do momentu przeczytania wszystkich symboli z danych wejściowych.

Otatecznie jako output otrzymujemy:

115 (s), 105 (i), 114 (r), 32 (_), 256 (si), 100 (d), 32 (_), 101 (e), 97 (a), 115 (s), 116 (t), 109 (m), 97 (a), 110 (n), 262 (_e), 264 (as), 105 (i), 108 (l), 121 (y), 32 (_), 116 (t), 263 (ea), 115 (s), 101 (e), 115 (s), 259 (_s), 263 (ea), 259 (_s), 105 (i), 99 (c), 107 (k), 280 (_se), 97 (a), 108

(l), 115 (s), eof.

(44)

KODOWANIE

Ponieważ pierwsze 256 elementów słownika jest wypełnionych przed rozpoczęciem czytania pliku źródłowego, to wskaźniki do słownika muszą być dłuższe niż 8 bitów. W prostej implementacji rozważa się zwykle wskaźniki 16- bitowe, co daje słownik rozmiaru 64 kilobajtów.

Słownik ten, poza przypadkami z danymi

wejściowymi bardzo małych rozmiarów, będzie wypełniany w

bardzo szybkim tempie. Oczywiście podobna sytuacja

zachodziła w przypadku metody LZ78, stąd wszystkie metody

na radzenie sobie z tym problemem wspomniane w części

dotyczącej metody LZ78, mogą zostać zastosowane również

w przypadku metody LZW.

(45)

KODOWANIE

Ze względu na specyfikę tworzenia nowych elementów w słowniku w metodzie LZW, zapis długich stringów w słoniku może zająć dużo czasu. Każdy z nich zwiększa się bowiem za każdym razem tylko o jeden symbol.

Długi czas zapisu elementów w słowniku oznacza również

długi czas kompresji. Stąd przyjęto, że cechą metody LZW

jest wolne dostosowywanie się do danych wejściowych.

(46)

DEKODOWANIE

Dekoder, podobnie jak koder, startuje inicjalizując słownik ze wszystkimi znakami w alfabecie na pozycjach 0 – 255. Następnie czyta dane wejściowe zawierające wskaźniki i używa ich do odzyskania nieskompresowanych symboli, które zwraca.

Rozważmy działanie dekodera na tym samym przykładzie:

𝑠𝑖𝑟_𝑠𝑖𝑑_𝑒𝑎𝑠𝑡𝑚𝑎𝑛_𝑒𝑎𝑠𝑖𝑙𝑦_𝑡𝑒𝑎𝑠𝑒𝑠_𝑠𝑒𝑎_𝑠𝑖𝑐𝑘_𝑠𝑒𝑎𝑙𝑠

Pierwszym pobranym przez dekoder wskaźnikiem jest 115, który może odnaleźć w swoim słowniku. Odpowiada on znakowi „s”, który zostaje zapisany w stringu S, a następnie w w outpucie. Następnym wskaźnikiem jest 105, symbol „i” zostaje zapisany w stringu J oraz również w outpucie. Dekoder łączy stringi S i J otrzymując „si”, którego nie ma w słowniku dekodera. Zostaje zatem dodane na miejscu 256.

Wartość stringa J jest przeniesiona do stringa S, który zamiast wartości „s”, przechowuje teraz wartość „i”.

(47)

DEKODOWANIE

Kolejnym wskaźnikiem, który pobiera dekoder jest 114, odnajduje on więc literę „r” w swoim słowniku, zapisuje w stringu J i w outpucie. Łączy następnie ze stringiem S otrzymując „ir”, którego nie ma w słowniku. Dodaje je na miejscu 257 i przenosi wartość J do S, zatem S przechowuje teraz symbol „r”. Taka logika jest przeprowadzana do momentu wyczerpania się tokenów.

Pierwszym krokiem dekodera jest zatem pobranie pierwszego wskaźnika i uzyskanie stringa S, który zostaje wypisany do outputu. W każdym kolejnym kroku dekoder pobiera następny wskaźnik, odnajduje string J ze słownika, wpisuje do outputu, izoluje jego pierwszy symbol „x” i zapisuje string Sx w słowniku (o ile nie został tam wcześniej zapisany). Dekoder przenosi następnie wartość J do S i jest gotowy na kolejny krok.

(48)

STRUKTURA SŁOWNIKA

Dotychczas zakładaliśmy, że słownik w metodzie LZW jest arrayem złożonym ze stringów. Oczywiście lepszym wyborem jest struktura drzewa, podobna jak w przypadku metody LZ78.

Dodanie nowego stringa postaci Sx do drzewa wykonywane przez dodanie dziecka „x” węzła S. Kwestią problematyczną w metodzie LZW jest to, że każdy węzeł może mieć wiele dzieci.

Struktura drzewa powinna być zatem zaprojektowana tak, że węzeł może mieć dowolną liczbę dzieci, bez konieczności rezerwowania dla nich pamięci na zapas.

Sposobem na zaprojektowanie takiej struktury jest umieszczenie drzewa w arrayu z węzłami, takimi, że każdy z nich ma dwa pola – symbol i wskaźnik do swojego rodzica. Węzeł nie ma więc żadnych wskaźników do dzieci. Schodzenie po drzewie do jednego z dzieci danego węzła odbywa się przez proces, w którym wskaźnik do węzła i symbol dziecka zostają zmieszane w celu utworzenia nowego wskaźnika.

(49)

KODOWANIE

Jako przykład rozważmy string postaci „abc”, który został już przeczytany, symbol po symbolu, i zapisany w drzewie w trzech węzłach: 97 („a”), 266 („ab”) i 284 („abc”).

Następnie koder pobiera znak „d” i poszukuje stringa

„abcd” w drzewie. Tak naprawdę poszukuje on węzła o wartości „d”, którego rodzicem jest węzeł na pozycji 284.

Koder miesza więc pozycję 284 rodzica oraz 100, oznaczające „d” w kodzie ASCII, w celu stworzenia nowego wskaźnika, przyjmijmy 299. Koder sprawdza potem węzeł 299. Istnieją 3 możliwości:

1. Węzeł jest nieużywany. Oznacza to, że wartość „abcd”

nie została wcześniej dodana do słownika. Koder dodaje

zatem węzeł postaci (284:d).

(50)

KODOWANIE

2. Rozważany węzeł 299 zawiera wskaźnik do rodzica 284 i kod ASCII dla symbolu „d”, co oznacza, że „abcd” należy już do drzewa. Wówczas koder pobiera kolejny znak, przyjmijmy „e” i przeszukuje drzewo, aby znaleźć stringa postaci „abcde”.

3. Węzeł zawiera inną informację. To oznacza kolizję i może być rozwiązane w różny sposób. Najprostszym jest zwiększenie wskaźnika 299 i sprawdzenie węzłów 300, 301, … , aż zostanie znaleziony pusty węzeł lub węzeł z informacją (284:d). W praktyce często budowane są węzły, które zawierają 3 pola – wskaźnik z procesu mieszania, kod (na przykład ASCII) i symbol, który zawiera węzeł. Drugie pole jest konieczne ze względu na możliwość kolizji.

(51)

PRZYKŁAD KODOWANIA

Rozważmy przykład postaci:

𝑎𝑙𝑓_𝑒𝑎𝑡𝑠_𝑎𝑙𝑓𝑎𝑙𝑓𝑎.

Dla powyższego przykładu dochodzi do 15 kroków mieszania (pierwszy element znajduje się już w słowniku):

1. Hash(l,97) → 278. Pozycja 278 zawiera teraz (97, 278, l).

2. Hash(f,108) → 266. Pozycja 266 zawiera teraz (108, 266, f).

3. Hash(_,102) → 269. Pozycja 269 zawiera teraz (102,269,_).

4. Hash(e,32) → 267. Pozycja 267 zawiera teraz (32, 267, e).

5. Hash(a,101) → 265. Pozycja 265 zawiera teraz (101, 265, a).

6. Hash(t,97) → 272. Pozycja 272 zawiera teraz (97, 272, t).

7. Hash(s,116) → 265 - kolizja! Przejście do następnej wolnej pozycji, 268, i ustalenie jej jako (116, 265, s). Widać zatem potrzebę

pozostawienia pola dotyczącego indeksu.

8. Hash(_,115) → 270 Pozycja 270 zawiera teraz (115, 270,_).

(52)

PRZYKŁAD KODOWANIA

9. Hash(a,32) → 268 - kolizja! Przejście do następnej wolnej pozycji, 271, i ustalenie jej jako (32, 268, a).

10. Hash(l,97) → 278. Pozycja 278 zawiera już indeks 278 oraz symbol „l”, zatem nowy węzeł nie zostaje dodany.

11. Hash(f,278) → 276. Pozycja 276 zawiera teraz (278, 276, f).

12. Hash(a,102) → 274. Pozycja 274 zawiera teraz (102, 274, a).

13. Hash(l,97) → 278. Pozycja 278 zawiera już indeks 278 oraz symbol „l”, zatem nowy węzeł nie zostaje dodany.

14. Hash(f,278) → 276. Pozycja 276 zawiera już indeks 276 oraz symbol „f”, zatem nowy węzeł nie zostaje dodany.

15. Hash(a,276) → 274 - kolizja! Przejście do następnej wolnej pozycji, 275 , i ustalenie jej jako (276, 274, a).

(53)

DEKODOWANIE

Dekoder również używa tej samej struktury słownika jak koder, jednak nie dochodzi w niej do procesu mieszania.

Zaczyna on od zainicjalizowania pierwszych 256 elementów.

Następnie czyta wskaźniki z danych wejściowych i używa ich do zlokalizowania stringów w słowniku.

Pierwszym krokiem dekodera jest pobranie pierwszego wskaźnika i wykorzystanie go do odzyskania elementu S. Dekoder po uzyskaniu rozważanego elementu wypisuje go w outpucie. Następnie rozważa string postaci Sx, który powinien być zapisany w słowniku.

Symbol „x” jest jeszcze nieznany – będzie to pierwszy symbol kolejnego stringa otrzymanego ze słownika.

W każdym kroku dekodowania (po pierwszym) dekoder pobiera kolejny wskaźnik i używa go do uzyskania następnego stringa J ze słownika, po czym wypisuje go w outpucie.

(54)

DEKODOWANIE

Jeżeli wskaźnikiem jest na przykład 8, dekoder sprawdza pole dict[8].index. Jeżeli to pole jest równe 8, to jest to właściwy węzeł. W innym wypadku dekoder sprawdza kolejne pozycje do momentu znalezienia tej prawidłowej.

Kiedy odpowiedni węzeł zostanie znaleziony, pole rodzica jest wykorzystywane do cofnięcia się po drzewie i uzyskania kolejnych symboli stringa (symbole uzyskuje się od końca). Symbole są później zapisywane w zmiennej J w poprawnej kolejności. Dekoder izoluje „x”, który stanowi pierwszy symbol w J i zapisuje string Sx w kolejnej dostępnej pozycji (zakładając, że string S został znaleziony w poprzednim kroku, dekoder musi dodać jedynie węzeł z symbolem

„x”). Dekoder przenosi później J do S i jest gotowy na kolejny krok.

Odzyskanie pełnego stringa z drzewa metody LZW wymaga podążania za wskaźnikami w polu rodzica. Jest to równoważne do przejścia w górę drzewa, zatem funkcja mieszająca nie już potrzebna.

(55)

PRZYKŁAD DEKODOWANIA

Rozważmy działanie dekodera na wcześniejszym przykładzie:

𝑎𝑙𝑓_𝑒𝑎𝑡𝑠_𝑎𝑙𝑓𝑎𝑙𝑓𝑎.

W ostatnim kroku kodowania otrzymaliśmy na pozycji 275 wartość (276,274,a). Wskaźnik jest czytany przez dekoder jako ostatni. Dekoder odnajduje symbol „a” w polu symbolu na pozycji 275 (stąd zatem wie, że string przechowywany w pozycji 275 kończy się na „a”) oraz wskaźnik z pola rodzica, który jest równy 276. Dekoder sprawdza następnie pozycję 276, gdzie odnajduje symbol „f” i wskaźnik do rodzica o wartości 278. Na pozycji 278 dekoder odnajduje symbol „l” i wskaźnik 97. Ostatecznie na pozycji 97 dekoder odnajduje symbol „a” ze wskaźnikiem, który jest nullem. Otrzymuje zatem string postaci „afla”, czyli szukany string w odwrotnej kolejności.

Możemy zatem zaobserwować, że nie ma potrzeby, aby dekoder dokonywał mieszania bądź używał indeksów pól.

(56)

PRZYKŁAD DEKODOWANIA

Ostatnią kwestią jest zatem odwrócenie otrzymanego stringa. Dwoma powszechnie stosowanymi metodami są:

1. Użycie stosu z buforem typu FIFO (ang. First In First Out).

Symbole, które zostały uzyskane ze słownika zostają odłożone na stos, a po odłożeniu wszystkich symboli z danego stringa, rozpoczyna się ściąganie elementów ze stosu od ostatniego odłożonego elementu. Ściągane elementy są zapisywane do zmiennej J. Pusty stos oznacza oczywiście ułożenie całego stringa.

2. Dołączanie uzyskanych symboli ze słownika w zmiennej J od strony prawej do lewej. Wówczas szukany string będzie zapisywany w odpowiedniej kolejności. Należy jednak rozważyć długość zmiennej J, aby móc rozważać nawet najdłuższe słowa.

(57)

WARIANTY METODY LZW

Publikacja metody LZW w 1984 roku była bardzo ważnym wydarzeniem i wpłynęła silnie na społeczność zajmującą się kompresją danych. Z tego powodu powstało wiele wersji i ulepszeń tej metody.

Metoda LZMW jest ulepszeniem LZW, które dopasowuje się do danych wejściowych znacznie szybciej. Zamiast dodawać S z dodatkowym znakiem do słownika, dodaje S i całe następne wyrażenie.

Metoda LZAP działa w taki sposób, że zamiast łączyć ostatnie dwa wyrażenia i zapisywać ich wynik w słowniku, zapisuje wszystkie przedrostki łączenia w słowniku, czyli jeżeli S i T są dwoma ostatnimi dopasowaniami, dodaje do słownika wszystkie St, gdzie t jest każdym możliwym prefiksem z T (może również oznaczać T).

Warto również nadmienić, że wariant metody LZW jest wykorzystywany w plikach formatu GIF.

(58)

PODSUMOWANIE

Znanych dzisiaj metod słownikowych jest oczywiście zdecydowanie więcej. W większości są one jednak wariantami metod przedstawionych w tym referacie. Zwróćmy również uwagę na przewagę metod słownikowych nad metodami statystycznymi - przy uważnym zaimplementowaniu pozwalają one na lepszą kompresję od metod statystycznych.

Bazują one dodatkowo na operacjach na stringach, stąd też nie wymagają żadnych obliczeń numerycznych, które bywają problematyczne. Są również metodami ogólnymi, stąd możliwe jest użycie ich w każdym przypadku, niezależnie od typu danych wejściowych. Następnie dekodery tych metod zwykle są schematami prostymi.

Wszystkie powyższe spostrzeżenia pozwalają przyjąć, że metody słownikowe stanowią bardzo istotną klasę metod służących do kompresji.

Obraz

Updating...

Cytaty

Powiązane tematy :
Outline : SŁOWNIK