Algorytm kodowania arytmetycznego Algorytmy Kompresji Danych wykład 4 Roman Starosolski Plan wykładu Historia kodowania arytmetycznego Idea kodowania arytmetycznego Koncepcja implementacji dla liczb o ograniczonej precyzji Wybrane algorytmy MQ-Coder Range-Coder Szybki model dla kodera arytmetycznego Historia kodowania arytmetycznego Historia Shannon (1948) ― „Podstawowe twierdzenie ...”, wzmianka o możliwości kodowania arytmetycznego Elias (196x) ― dalsze prace Jelinek (1968) ― dalsze prace Rissanen i Pascoe (1976, niezależnie) ― idea implementacji w arytmetyce stałopozycyjnej IBM (198x) ― algorytmy zorientowane na sprzętowe kodowanie alfabetu binarnego (w większości opatentowane) Witten (1987) i inni ― implementacje programowe dla alfabetów binarnych i nie-binarnych „Podstawowe twierdzenie Shannona o kodowaniu bezszumowym” (jedna z postaci twierdzenia, wg.: Drozdek: „Wprowadzenie do kompresji danych”) Dla bezpamięciowego źródła S o entropii H(S) możliwe jest przypisanie ciągom k symboli źródła, słów kodu przedrostkowego tak, że spełnione jest H(S) ≤ Lk / k < H(S) + 1 / k asymptotycznie, możliwe jest uzyskanie średniej długości kodu (w przeliczeniu na pojedynczy symbol) równej entropii źródła optymalna długość słowa kodowego dla symbolu o prawdopodobieństwie p równa jest –log (p) (czyli autoinformacji dla tego symbolu) można zbudować koder entropijny o efektywności bliskiej 100% Idea kodowania arytmetycznego Cały ciąg zostanie zakodowany za pomocą jednej liczby rzeczywistej z lewostronnie domkniętego przedziału zawartego w przedziale [0, 1) (Jeżeli długość ciągu nie jest znana dekoderowi, to należy ją przetransmitować, bądź uzupełnić alfabet źródła o symbol oznaczający koniec ciągu.) Zaczynamy od przedziału [0, 1) Czytając kolejne symbole ciągu zawężamy stopniowo początkowy przedział; dla kolejnego symbolu pobranego z ciągu przedział dzieli się na lewostronnie domknięte podprzedziały o długościach wprost proporcjonalnych do prawdopodobieństw poszczególnych symboli alfabetu źródła wybiera się przedział odpowiadający odczytanemu symbolowi. Wyprowadzamy dowolną liczbę z przedziału wyznaczonego dla ciągu Przykład komdujemy ciąg abaca, Używamy stałego modelu danych alfabet źródła to {a, b, c} P(a)=0.6, P(b)=0.2, P(c)=0.2 Przyjmujemy, iż długość ciągu jest znana dekoderowi. Zaczynamy od przedziału [0, 1). Dla kolejnego symbolu z ciągu odczytaliśmy a ... przedział dzieli się na lewostronnie domknięte podprzedziały o długościach wprost proporcjonalnych do prawdopodobieństw poszczególnych symboli alfabetu źródła, a: [0, 0.6) b: [0.6, 0.8) c: [0.8, 1) ... wybiera się przedział odpowiadający temu symbolowi. przedział dla ciągu a: [0, 0.6) 1 0.6 c 0.8 b 0.6 a 0 0 przedział dla ciągu a: [0, 0.6). Dla kolejnego symbolu z ciągu odczytaliśmy b ... przedział dzieli się na lewostronnie domknięte podprzedziały o długościach wprost proporcjonalnych do prawdopodobieństw poszczególnych symboli alfabetu źródła, aa: [0, 0.36) ab: [0.36, 0.48) ac: [0.48, 0.6) ... wybiera się przedział odpowiadający temu symbolowi. przedział dla ciągu ab: [0.36, 0.48) 1 0.6 c c 0.8 0.48 b b 0.6 0.36 a a 0 0 0.48 0.36 przedział dla ciągu ab: [0.36, 0.48). Dla kolejnego symbolu z ciągu odczytaliśmy a ... przedział dzieli się na lewostronnie domknięte podprzedziały o długościach wprost proporcjonalnych do prawdopodobieństw poszczególnych symboli alfabetu źródła, aba: [0.36, 0.432) abb: [0.432, 0.456) abc: [0.456, 0.48) ... wybiera się przedział odpowiadający temu symbolowi. przedział dla ciągu aba: [0.36, 0.432) 1 0.6 0.48 c c c 0.8 0.48 b b 0.6 0.36 a a 0 0 0.432 0.456 b 0.432 a 0.36 0.36 ... po przetworzeniu całego ciągu abaca otrzymujemy przedział dla ciągu abaca: [0.4176, 0.432) Wyprowadzamy dowolną liczbę z przedziału wyznaczonego dla ciągu 1 np. 0.42 (a wiedząc, że liczba jest z [0, 1), wystarczy wyprowadzić znaczące cyfry mantysy: 42) 0.6 0.48 c c c 0.8 0.48 b b 0.6 0.36 a a 0 0 0.456 b 0.432 0.432 c 0.4176 b 0.4032 a a 0.36 0.36 0.432 0.42624 c 0.42912 b 0.42624 a 0.4176 0.4176 Przedziału uzyskany po wykonaniu algorytmu długość przedziału równa jest prawdopodobieństwu wygenerowania odczytanego ciągu dla każdego możliwego ciągu o długości n symboli otrzymamy inny przedział przedziały te nie pokrywają się ich suma jest przedziałem [0, 1) dla podprzedziału o długości p można znaleźć taką liczbę, że binarne zakodowanie jej mantysy wymagać będzie nie więcej niż –log(p) + 1 bitów Dekodowanie Znając rozkład prawdopodobieństwa symboli alfabetu i długość ciągu, pobieramy zakodowany ciąg, czyli kod 42 → liczba 0.42 zaczynamy od przedziału [0, 1) cyklicznie, aż nie zdekodujemy ciągu o danej długości: 1 analogicznie do kodowania, dzielimy przedział na podprzedziały dla wszystkich symboli alfabetu wybieramy podprzedział który zawiera liczbę 0.42 wyprowadzamy symbol odpowiadający temu podprzedziałowi 0.6 0.48 c c c 0.8 0.48 b b 0.6 0.36 0.432 c 0.456 0.42 0.4176 b b 0.432 0.4032 0.432 0.42 0.42624 c 0.42912 b 0.42624 0.42 a a a a a 0.42 0.42 0 0 0.36 0.36 0.4176 0.4176 Cechy kodowania arytmetycznego efektywność bliska 100%, asymptotycznie 100% bez ograniczeń co do rozkładu prawdopodobieństwa symboli (ani co do rozmiaru alfabetu) niezależność kodera od modelu można stosować różne modele probabilistyczne można stosować wiele modeli nadaje się do algorytmów adaptacyjnych (trzeba uzupełnić alfabet o symbol EOF) nie da się go zaimplementować wprost dla liczb o ograniczonej precyzji Koncepcja implementacji dla liczb o ograniczonej precyzji Część cyfr liczby z wnętrza przedziału można wyprowadzać już w trakcie kodowania 1 gdy początkowe cyfry górnego i dolnego kresu przedziału są takie same, to takie będą również cyfry każdej liczby wewnątrz przedziału można więc te cyfry wyprowadzić (i odpowiednio przeskalować przedział aby korzystać z pełnej dostępnej dokładności) 0.6 0.48 c c c 0.8 0.48 b b 0.6 0.36 a a 0 0 0.456 b 0.432 0.432 c 0.4176 b 0.4032 a a 0.36 0.36 0.432 0.42624 c 0.42912 b 0.42624 a 0.4176 0.4176 Koncepcja implementacji dla liczb o ograniczonej precyzji Model danych model oparty o liczby całkowite zamiast prawdopodobieństw zlicza liczby wystąpień symboli do wyznaczenia podprzedziału dla kolejnego symbolu wystarczy znać jego prawdopodobieństwo oraz łączne prawdopodobieństwo wszystkich symboli poprzedzających go w alfabecie ponieważ łatwiej to robić w modelu, zazwyczaj to model wyznacza skumulowane prawdopodobieństwa Pi = ∑ki= 1 pi , gdzie pi to prawdopodobieństwo symbolu si , tj. i-tego symbolu alfabetu; tu symbole numerujemy od 1 Koncepcja implementacji dla liczb o ograniczonej precyzji Reprezentacja przedziału kresy pamiętamy na m-bitowych liczbach całkowitych, M = 2 m –1 przedziały traktujemy jako obustronnie domknięte, kres górny danego podprzedziału jest o 1 mniejszy od kresu dolnego następnego podprzedziału w przypadku skalowania przedziału trzeba zadbać aby powyższa własność została zachowana (po pomnożeniu kresów przez 2 odległość między nimi wzrośnie do 2) zaczynamy od przedziału [0, M], tj. [0000...0 i 1111...1] prawdopodobieństwo symbolu wyznaczone przez model nie może spowodować wyznaczenia przedziału o długości 0 Normalizacja podprzedziału zawartego całkowicie w górnej lub dolnej połówce przedziału [0, M] wyprowadź 1 (górna), lub 0 (dolna) i przeskaluj co zrobić gdy podprzedział zawiera środek przedziału [0, M]? Normalizacja podprzedziału zawierającego środek przedziału [0, M] koduj alfabet binarny, to nie będzie takich problemów (przy odpowiedniej normalizacji), albo gdy długość podprzedziału będzie mniejsza od M/2 przeskaluj zliczaj przeskalowania, ale nie wyprowadzaj bitów (na podstawie: A. Drozdek, Wprowadzenie do kompresji danych, WNT, Warszawa, 1999) Kodujemy symbol si (i-ty symbolu alfabetu), aktualny przedział to [L, R] bieżącyPrzedział = [ L + Pi-1(R – L + 1) , L + Pi (R – L + 1) ] Normalizacja przedziału (gdy przedział jest mały wykonywana kilkakrotnie) while(1) if bieżącyPrzedział [0, M/2] zwróc 0 i licznikBitów jedynek licznikBitów = 0 elseif bieżącyPrzedział [M/2, M] zwróc 1 i licznikBitów zer licznikBitów = 0 odejmij M/2 od obu kresów bieżącegoPrzedziału elseif bieżącyPrzedział [M/4, 3M/4] licznikBitów ++ // tylko tu możliwe przepełnienie odejmij M/4 od obu kresów bieżącegoPrzedziału else break endif pomnóż przez 2 oba kresy bieżącegoPrzedziału dodaj 1 do górnego kresu bieżącegoPrzedziału endwhile (na podstawie: A. Drozdek, Wprowadzenie do kompresji danych, WNT, Warszawa, 1999) Zakończenie kodowania (wyprowadź resztę „stanu” kodera arytmetycznego, czyli końcówkę rozwinięcia binarnego liczby z bieżącego przedeziału) licznikBitów ++ if kres dolny bieżącegoPrzedziału < M/4 zwróc 0 i licznikBitów jedynek else zwróc 1 i licznikBitów zer endif Wybrane Implementacje Witten, Neal, Cleary (1987) (tzw. CACM, zasada działania pokazana na poprzednich slajdach) ftp://ftp.cpsc.ucalgary.ca/pub/projects/ar.cod/cacm-87.shar Moffat, Neal, Witten (1998) (udoskonalony CACM, bez dzielenia, mniej mnożeń, shift, +, –) http://www.cs.mu.oz.au/~alistair/arith_coder/ MQ Coder (binarny, opatentowany /IBM i in./, użyty w JBIG2, JPEG2000 i innych) Range Coder (szybki, prosty, alfabety wielosymbolowe) http://www.compressconsult.com/rangecoder/ Binarny koder arytmetyczny MQ Coder Kodujemy symbole alfabetu binarnego bardzo proste modelowanie, nie trzeba liczyć prawdopodobieństwa kumulatywnego wystarczy szacować prawdopodobieństwo tylko jednego symbolu bardzo proste kodowanie nowy symbol to zmiana tylko jednego kresu przedziału podprzedział o długości M/2 zawsze jest całkowicie zawarty w górnej lub dolnej połówce przedziału [0, M] (przy odpowiedniej normalizacji) MQ Coder – dalsze uproszczenia normalizacja utrzymuje długość podprzedziału między 0.75 a 1.5, średnio 1.0 faktycznie kodujemy alfabet {MPS, LPS} – czyli bardziej i mniej prawdopodobny symbol; to, czy MPS to 0 czy 1 jest określa flaga uaktualniana po każdym symbolu unikamy mnożenia; nowa długość podprzedziału dla prawdopodobieństwa p, to nie p * stara_długość, a po prostu p pojawienie się LPS to zawsze skalowanie i wyprowadzenie bitu MPS może, ale nie musi powodować skalowania i wyprowadzania bitów model szacuje tylko prawdopodobieństwo LPS jest ściśle powiązany z koderem i jest aktualizowany tylko w razie normalizacji prawdopodobieństwo LPS pamiętane z niewielką precyzją (np. 7 bitów) precyzja jednocześnie odpowiada za adaptacyjność modelu (szybkość zapominania) aktualizacja modelu również bez mnożeń, stablicowane prawdopodobieństwa po normalizacji wywołanej przez LPS/MPS (2 tablice indeksowane prawdopodobieństwem LPS) MQ Coder bardzo szybki dla większych alfabetów symbol kodowany jest jako ciąg bitów niewielkie wymagania pamięciowe szczególnie istotne dla modelu; model dla kontekstu pamięta jedynie: prawdopodobieństwo LPS (np. 7 bitów) który bit jest LPS (1 bit) można stosować złożone modele z wielką liczbą kontekstów co nadal jest szybkie mimo uproszczeń, efektywność kodowania bliska 100% Range Coder – koncepcja Martin (1979) Koncepcja opracowana niezależnie od klasycznego kodowania arytmetycznego podział pewnego przedziału liczb analogicznie jak w kodowaniu arytmetycznym, ale przedział liczb całkowitych odpowiednio duży przedział, np. [0, 1000000] po przetworzeniu ciągu wyprowadzamy najbardziej znaczące cyfry pewnej liczby z wnętrza przedziału (nie wszystkie, wystarczające do jednoznacznego określenia przedziału) Range Coder – realizacja Zakodowanie ciągów dłuższych niż bardzo krótkie, wymagałoby przedziału zbyt wielkiego by jego krańce reprezentować wprost jako liczby używamy przedziału reprezentowalnego jako liczba o stałej precyzji w systemie narnym; gdy aktualna długość przedziału spada Otrzymujemy koder podobny do kodera CACM inna (szybsza) operacja normalizacji nadal nie trywialna, mogą wystąpić niedomiary, zliczmy je (analogia do licznikaBitów) alfabet kodu to cyfra w systemie n-arnym wyprowadzamy pokrywające się najbardziej znaczące cyfry kresów odpowiednio skalujemy przedział mnożąc jego kresy przez podstawę systemu liczbowego wygląda znajomo? n=256, a więc we/wy bajtowe, prostsze i szybsze niż bitowe w CACM precyzja podziału przedziału na podprzedziały typowo mniejsza niż np. w CACM Shindler: http://www.compressconsult.com/rangecoder (koder, dekoder, prosty model bezpamięciowy, przykłady) Szybki model dla kodera arytmetycznego Potrzebne operacje dla si wyznacz Pi-1 dla si wyznacz pi dla Pi-1 wyznacz si (dekodowanie) powyższe łatwe do zrealizowania, gdy potrafimy szybko wyznaczyć Ci – łączną liczbę wystąpień symboli sx, x ≤ i uaktualnij model (po przetworzeniu si) podziel liczniki w modelu (zwykle przez 2) Struktura Fenwicka dla alfabetu n symboli, tablica T [0..n-1] o indeksie i zawiera sumę liczb wystąpień symboli od i – 2j + 1 do i gdzie j to pozycja najmniej znaczącej jedynki w binarnie zakodowanym i komórka np. i = 6 = 110b, j = 1 2j = i & – i Struktura Fenwicka i i binarnie j 2j suma liczników w T [i] 0 0000 0 1 0–0 1 0001 0 1 1–1 2 0010 1 2 1–2 3 0011 0 1 3–3 4 0100 2 4 1–4 5 0101 0 1 5–5 6 0110 1 2 5–6 7 0111 0 1 7–7 8 1000 3 8 1–8 9 1001 0 1 9–9 10 1010 1 2 09 – 10 11 1011 0 1 11 – 11 12 1100 2 4 09 – 12 Struktura Fenwicka Fenwick złożoność pamięciowa: n złożoność czasowa dla si wyznacz Pi-1 O(log2n) dla si wyznacz pi O(1) dla Pi-1 wyznacz si O(log2n) uaktualnij model O(log2n) podziel liczniki w modelu O(log2n) Struktura Moffata W tablicy sumowanie „w przód” złożoność pamięciowa: n (z sortowaniem 3n) złożoność czasowa dla si wyznacz Pi-1 O(log2i) dla si wyznacz pi O(1) dla Pi-1 wyznacz si O(log2i) uaktualnij model O(log2i) podziel liczniki w modelu O(log2n) (w praktyce struktury podobnie szybkie, Moffata będzie szybsza gdy alfabet będzie naturalnie posortowany wg rosnących częstości występowania symboli)