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