hyväksymispäivä arvosana arvostelija Toteutusdokumentti: Huffman-koodaus Tuomas Husu <thusu@cs.helsinki.fi> Helsinki 25.6.2009 Tietorakenteiden harjoitustyö, kesä 2009. Ohjaaja Tomi Jylhä-Ollila. HELSINGIN YLIOPISTO Tietojenkäsittelytieteen laitos
Sisältö i 1 Johdanto 1 2 Käyttöohje 1 2.1 Ohjelman kääntäminen.......................... 2 2.2 Ohjelman suorittaminen......................... 2 2.2.1 Tiedoston pakkaaminen..................... 2 2.2.2 Tiedoston purkaminen...................... 3 2.2.3 Ohjeen tulostaminen....................... 3 2.3 Laitteistovaatimukset........................... 4 3 Toteutus 4 4 Testaus 5 4.1 Yksikkötestaus.............................. 5 4.2 Toiminnan testaus............................ 5 4.2.1 Generoidut testiaineistot..................... 5 4.2.2 Vertailu muihin pakkausohjelmiin................ 7 5 Puutteet, rajoitukset ja parannusehdotukset 7 Lähteet 8 Liitteet 1 Tehtävänanto 2 Tuntikirjanpito
1 Johdanto 1 Huffman-koodaus on 1950-luvulta peräisin oleva häviötön ja yleiskäyttöinen tiedontiivistystekniikka. Tietokoneiden käsitellessä merkkejä (kirjaimet, numerot, välimerkit, erikoismerkit jne.) normaalisti kiinteänpituisina bittijonoina, vaatii jokainen merkki esiintymistiheydestään riippumatta saman verran tilaa. Kahdeksan bittiä (tavu) on tavanomainen yhden merkin koodin pituus, joten esimerkiksi sadan merkin mittaisen tekstin koodaamiseen tarvittaisiin 800 bittiä (l. 100 tavua). Erilaisten merkkien esiintymistiheyden vaihtelua voidaan käyttää hyväksi tietoa tiivistettäessä (ja siten saavuttaa säästöä esimerkiksi tiedonsiirrossa ja talletuksessa). Huffman-koodausta käytettäessä yleisimmät (l. useiten esiintyvät) merkit korvataan mahdollisimman lyhyillä bittijonoilla, kun taas harvinaisemmat (l. harvemmin esiintyvät) esitetään pitemmillä jonoilla. Näin tieto saadaan esitettyä kokonaisuudessaan pienemmällä bittimäärällä välttämällä tarpeetonta toistoa. Ennen alkuperäisten tavujen korvaamista lyhyemmillä koodeilla, pitää merkeille johtaa sopivat bittijonoesitykset. Huffman-koodauksessa koodit muodostetaan käyttäen Huffman-puita, jotka muodostetaan merkkien lukumäärätaulukon (ts. esiintymistiheyden) perusteella. Jokaiselle merkille muodostetaan lehtisolmu, johon on liitetty sekä itse merkki että ko. merkin esiintymismäärä. Huffman-puun rakentaminen aloitetaan kahdesta pienimmän esiintymismäärän omaavasta solmuista (jotka eivät ole jo jonkun solmun lapsia) ja niille luodaan uusi yhteinen vanhempi, ja vanhemman esiintymislukumääräksi asetetaan lapsisolmujen esiintymislukumäärien summa. Solmujen lisäämistä esiintymismääräjärjestyksessä jatketaan, kunnes kaikki solmut sisältävä yhtenäinen puu on luotu. Koodi riippuu solmun sijainnista Huffman-puussa siten, että vasemmat haarat puussa vastaa bittiä 0 ja oikeat haarat bittiä 1. Liikuttaessa puussa kerran vasemmalle ja kerran oikealle halutun merkin löytääksemme, on ko. merkin koodi on 01. 2 Käyttöohje Ohjelman suorittaminen onnistuu siirtymällä ohjelmatiedostot sisältävään hakemistoon ja antamalla oheiset komennot. Oltaessa tekemisissä Java-tiedostojen kanssa, on komentojen kirjoitusasun kanssa oltava tarkkana.
2 2.1 Ohjelman kääntäminen # javac *.java Kuva 1: Ohjelman kääntäminen. 2.2 Ohjelman suorittaminen Ohjelma käynnistetään komennolla java Huffman <parametrit>. Parametreina annettavan lähde- ja kohdetiedostot voivat sijaita joko suoritushakemistossa (oletus) tai jossain muualla, jolloin tiedoston nimi on annettava polkuineen. 2.2.1 Tiedoston pakkaaminen # java Huffman -pakkaa <tiedosto> [-t <kohdetiedosto>] Kuva 2: Tiedoston pakkaaminen. Ilman kohdetiedoston määrittelyä pakattu tiedosto tallennetaan.huff-päätteiseen tiedostoon (l. <lähdetiedosto>.huff).
3 2.2.2 Tiedoston purkaminen # java Huffman -pura <tiedosto> [-t <kohdetiedosto>] Kuva 3: Tiedoston purkaminen. Ilman kohdetiedoston määrittelyä ohjelma olettaa lähdetiedoston olevan.huff-päätteinen ja kohdetiedosto luodaan poistamalla.huff-pääte. Purettaessa jokin muu kuin.huff-päätteinen tiedosto, täytyy kohdetiedosto määritellä -t -parametrilla. 2.2.3 Ohjeen tulostaminen # java Huffman --help Yksityiskohtainen käyttöohje tulostetaan tyypillisillä avunpyyntöparametreilla, kuten -help, -h ja /?. Suppeampi käyttöohje tulostetaan käynnistettäessä ohjelma ilman parametreja (l. java Huffman) tai virheellisillä parametreilla. Kuva 4: Yksityiskohtainen ohje.
4 2.3 Laitteistovaatimukset Ohjelma tarvitsee toimiakseen Java-virtuaalikoneen. Virtuaalikoneita on saatavilla lähes kaikkiin laitteisto- ja käyttöjärjestelmäympäristöihin. Java-virtuaalikoneen viimeisin versio (tällä hetkellä 1.6.14) on ladattavissa ilmaiseksi verkosta osoitteesta http://www.java.com/getjava/. 3 Toteutus Harjoitustyö on kirjoitettu Java-kielellä ja se koostuu kuudesta eri luokasta. Huffman on pääohjelmaluokka. Luokka saa komentoriviparametrina halutun toimintatavan (pakkaus, purku tai ohje) ja pakattavan tai purettavan tiedoston nimen sekä optiona tiedoston, johon käsitelty tiedosto tallennetaan. Tiedosto hoitaa Bitit-luokan kanssa tiedoston luku- ja kirjoitusoperaatiot. Javan BufferedInputStream- ja BufferedOutputStream -luokkien avulla isotkin lukuja kirjoitusoperaatiot sujuvat joutuisasti. Tiedosto-luokka luo tiedoston alkuun otsakkeen, johon se kirjaa jokaisen merkin esiintyvyyden tiedostossa (l. bitit 0-255, esiintyvyys kyllä [1] tai ei [0]) sekä esiintymistiheydet. Bitit avustaa Tiedosto-luokkaa bittien käsittelyssä. Bitit- ja Tiedosto-luokkien bittimuunnosmetodien (bitstobyte ja bytetobits) toteutus on vohkittu Esa Junttilan bittiohjeesta [Jun05]. Keko luo O(logn) -ajassa toimivan solmuista koostuvan keon. Vaikutteita toteutukseen imetty Arto Wiklan kurssimateriaalin javakielisistä esimerkeistä [Wik01]. Puu hoitaa pakattaessa varsinaisen Huffman-puun rakentamisen sekä rekursiivisen läpikäynnin. Tiedostoa purettaessa käydään läpi rekursiosta hieman poikkeava purku-aksessori. Huffman-puun rakentaminen on toteutettu käyttäen parametrina annettavaa lehtikekoa, joka on jo valmiiksi järjestetty minimikeoksi, sekä sisäsolmukekoa, johon lisätään aina solmujen vanhempi. Toteutus olisi yhtä lailla voinut perustua jonoihin tai taulukkoon, mutta ao. tapauksessa päädyttiin kahden keon (+ viitteet lapsisolmuihin) ratkaisuun. Solmu sisältää aksessorit lapsien kertomiseen sekä merkin ja tiheyden lisäämiseen.
4 Testaus 5 4.1 Yksikkötestaus Ajanpuutteen vuoksi metoditasoinen koodin oikean toiminnan osoittava yksikkötestaus jäi alunperin suunniteltua vähemmälle, ja vähäinen testausaika projektin loppuvaiheessa käytettiin kokonaisuuden oikean toiminnan osoittamiseen. Metodit testattiin luontivaiheessa sekalaisilla parametrikombinaatioilla ja oikeasta toiminnasta vakuututtua siirryttiin eteenpäin. Sisäänrakennetut yksikkötestit olisivat epäilemättä olleet tarpeen jatkettaessa ohjelman kehitystä, mutta pienehkön ja kertaluontoisen projektin kyseessä ollen tyydyttiin kädestä suuhun -tyyppiseen yksikkötestaukseen. 4.2 Toiminnan testaus Ohjelman toimintaa kokonaisuutena on testattu erityyppisillä ja -kokoisilla syötetiedostoilla sekä verrattu ohjelman toimintaa yleisimpiin pakkausohjelmiin. 4.2.1 Generoidut testiaineistot Testiaineistoja ohjelman toiminnan testaamiseksi on tuotettavissa esimerkiksi linuxkomennolla dd. Käyttämällä syötteenä /dev/zeroa, voi haluamansa kokoisen, testaamiseen käytettävän tiedoston täyttää NULL-merkeillä ja testata ohjelman selviytymistä vain yhtä merkkiä sisältävän tiedoston pakkaamisesta. Esimerkiksi 1000-kilotavuinen, pelkkiä NULL-merkkejä sisältävä testiaineisto tuotetaan siis komennolla dd if=/dev/zero of=tiedosto_zero bs=1k count=1000 Vastakohtana edelliselle, satunnaisia merkkejä sisältävän vastaavan aineiston luonti onnistuu käyttämällä syötteenä vaikkapa /dev/urandom -satunnaislukugeneraattoria eli komennolla dd if=/dev/urandom of=tiedosto_random bs=1k count=1000
6 Kuva 5: 1000-kilotavuisten testiaineistojen luonti ja pakkaantumisen testaus. Sisältö Koko (t) Pakattu koko (t) Pakkaus-% Yhtä merkkiä 1 024 000 128 037 87,5 Satunnainen 1 024 000 1 025 057-0,1 Taulukko 1: Pakkaustestin tulokset. Kuvaa ja taulukkoa tarkastelemalla huomaa yhtä merkkiä sisältäneen tiedoston pakkautuneen noin kahdeksasosaan ( 1 ) alkuperäisestä koostaan. Yksi ainoa tiedostossa 8 esiintyvä merkki voidaan korvata yksibittisellä koodilla, jolloin pakkautuminen kahdeksasosaan on ennustettavissa ollut tulos. Paljon erilaisia merkkejä sisältävä satunnaistiedosto ei sen sijaan pakkaannu lainkaan, vaan vie jopa hieman enemmän tilaa pakattuna. Merkkien esiintymistiheyden vaihteluun perustuva Huffman-koodaus ei sovellu merkeiltään heterogeenisen aineiston pakkaamiseen, kuten eivät juuri muutkaan tiedon tiivistämiseen käytetyt tekniikat. Täysin satunnaisia sekä yhtä ainoaa merkkiä sisältävät tiedostot ovat Huffmankoodauksen kannalta ns. ääriaineistot. Muunlaiset aineistot sijoittuvat pakkautuvuudessaan jonnekin ääripäiden välille.
7 4.2.2 Vertailu muihin pakkausohjelmiin Oheisessa, hieman kiireellä tuotetussa pylväsdiagrammissa on esitetty BMP-kuvatiedoston (pakkaamaton bittikarttakuva) ja MP3-audiotiedoston (pakattu häviöllinen MPEG- 1 Audio Layer 3) pakkautuminen käyttäen Huffman-koodausta, Zip 2.31:ä (Linux), WinRAR 3.80:aa (Windows) sekä bzip2 1.0.4:ää (Linux). MVBAR chart of PAKKAUS2 1200000 800000 400000 0 BMP MP3 Alkuperäinen Huffman RAR ZIP BZIP2 Kuva 6: BMP-kuvatiedoston ja MP3-äänitiedoston pakkautuminen. Häviötön, paljon turhaa informaatiota (tässä tapauksessa väriltään valkoisia pikseleitä) sisältävä bittikarttakuva pakkautuu odotetusti hyvin kaikilla menetelmillä; parhaiten bzip2:lla (pakkaus 75 %) ja heikoiten Huffmanilla (pakkaus 56 %). Häviöllisesti pakattu MP3-tiedosto ei sen sijaan pakkaudu juuri lainkaan millään pakkausmenetelmistä. Jälleen paras on bzip2 (pakkaus 2 %) ja heikoin Huffman (pakkaus 0,2 %), mutta erot ovat pieniä. Testaukseen käytetyt tiedostot (roope.bmp ja grandiosa.mp3) ovat ladattavissa verkosta osoitteesta http://cs.helsinki.fi/u/thusu/tiralabra/. 5 Puutteet, rajoitukset ja parannusehdotukset Ohjelma ei tarkista tiedostoja kirjoittaessaan onko samanniminen tiedosto jo olemassa, ja ylikirjoittaa tarpeen tullen kyselemättä. Ohjelmaa voisi kehittää lisäämällä siihen tarkistuksen ja esimerkiksi vahvistuspyynnön ( Haluatko korvata? Kyllä
8 / Ei ) löydettäessä samanniminen tiedosto. Vuorovaikutteinen käyttöliittymä voisi myös pyytää syöttämään uuden tiedostonimen, jos käyttäjä ei haluaisi ylikirjoittaa olemassa olevaa tiedostoa. Ohjelma olisi monikäyttöisempi, jos sillä voisi pakata useita tiedostoja yhdellä komennolla (esim. java Huffman -pakkaa *.txt) omiin tiedostoihinsa. Samoin useiden tiedostojen (ja hakemistorakenteiden) pakkaus yhteen tiedostoon (esim. java Huffman -pakkaa *.txt -t tekstit.huff) lisäisi ohjelman käytettävyyttä runsaasti. Ohjelma voisi myös pakkauksen suoritettuaan kertoa, montako prosenttia pienempään tilaan se sai lähdeaineiston sullottua. Kuvatun kaltaisten ominaisuuksien toteuttaminen ei varsinaisesti palvellut tietorakenneharjoitustyön tavoitteita, joten ne rajautuivat pois jo suunnitteluvaiheessa. Nykyaikaisiin pakkausohjelmiin tottuneen on helppo keksiä lukemattomia parannuksia ohjelmaan; edellä mainittujen lisäksi lähdetiedostojen poisto, luodun paketin salasanasuojaus, pakkaus useaan määrätyn kokoiseen tiedostoon, graafinen käyttöliittymä, pakkausprosessin edistymisen visuaalinen esittäminen, kommenttien lisääminen pakattuun tiedostoon sekä monet muut ovat varsin yleisiä ja monien tarvitsemia toimintoja. Lähteet CLR01 Jun05 Mac03 Cormen, T. H., Leiserson, C. E., Rivest, R. L. ja Stein, C., Introduction to Algorithms. MIT Press, toinen painos, 2001. Junttila, E., Ohje bittien käsittelyyn, 2005. http://www.cs.helsinki. fi/u/ejunttil/opetus/tiraharjoitus/bittiohje.txt. [5.5.2009] MacKay, D. J. C. Information Theory, Inference, and Learning Algorithms, luku Symbol Codes, sivut 91 108. Cambridge University Press, 2003. [Myös http://www.inference.phy.cam.ac.uk/mackay/ itila/book.html]. Wik01 Wikla, A., Kekoja eli prioriteettijonoja, 2001. https://www.cs. helsinki.fi/i/wikla/tira/sisalto/5/keko.html. [5.5.2009]
Liite 1. Tehtävänanto Pakkausohjelma: Huffman Tehtävänä on toteuttaa yksinkertainen pakkausohjelma, joka lukee yhden tiedoston ja pakkaa sen toiseen tiedostoon, sekä sitä vastaava purkuohjelma. Huffman-koodauksessa pakattavaa dataa käsitellään yksi merkki kerrallaan. Kukin merkki koodataan bittijonoksi, joka on sitä lyhyempi, mitä useammin merkki esiintyy datassa. Merkkien Huffman-koodit määritellään muodostamalla binääripuu seuraavasti. Aluksi jokainen merkki on oma puunsa. Niin kauan kuin puita on vähintään kaksi, otetaan ne kaksi puuta, joissa olevia merkkejä esiintyy datassa yhteensä vähiten, ja yhdistetään nämä uudeksi puuksi liittämällä ne uuden solmun alipuiksi. Lopullisessa puussa merkkejä vastaavat solmut ovat puun lehtiä, ja merkin koodi määräytyy sen sijainnin mukaan niin, että juuresta lähdettäessä vasempaan lapseen siirtyminen vastaa 0-bittiä ja oikeaan lapseen siirtyminen 1-bittiä. Mitä yleisempi merkki on, sitä lähempänä juurta se puussa on ja sen lyhempi koodi sillä siis on. Jos tiedosto halutaan pakata yhdellä läpiluvulla tai merkkien esiintymistiheyksiä ei haluta tallentaa mukaan pakattuun tiedostoon, voidaan käyttää adaptiivista Huffmankoodausta. Tällöin puuta päivitetään jokaisen luetun merkin jälkeen. Lisätietoja Huffman-koodauksesta löytyy esimerkiksi Tietorakenteet-kurssiltakin tutusta kirjasta Cormen et al: Introduction to Algorithms tai kurssilta Merkkijonomenetelmät. Apua adaptiivisen koodauksen toteuttamiseen löytyy verkosta etsimällä. Myös Wikipediaan kannattaa tutustua. Lähde http://www.cs.helsinki.fi/u/jltsiren/tiralab/huffman.html.
Liite 2. Tuntikirjanpito Päivämäärä Ajankäyttö Tarkoitus -------------------------------------------------- ti 19.5. 1,5 h Aloitustilaisuus ke 20.5. 2,0 h Aiheen määrittely -dokumentti to 21.5. 1,0 h Määrittelydokumentin muutokset pe 22.5. 4,0 h Aiheeseen perehtyminen to 28.5. 3,0 h Suunnittelu, etätapaaminen pe 29.5. 5,5 h Toteutuksen aloittaminen ma 1.6. 1,5 h Toteutus ke 3.6. 2,0 h Toteutus (mm. bittimuunnokset) to 4.6. 2,0 h Toteutus (mm. tiedostonkäsittely) pe 5.6. 1,5 h Palautus ma 8.6. 3,0 h Testaus ja toteutus ti 9.6. 3,5 h Keko ke 10.6. 4,5 h Keko to 11.6. 5,5 h Tietorakenteet pe 12.6. 2,0 h Toimivan väliversion palautus ti 16.6. 4,0 h Dokumentointi ke 17.6. 3,0 h Tietorakenteiden hienosäätö ma 22.6. 1,0 h Tuntikirjanpidon takautuva generointi :-) ti 23.6. 3,0 h Toteutusdokumentin viimeistely ke 24.6. 6,0 h Ohjelman ja dokumentaation viimeistely to 25.6. + 7,5 h Palautustilaisuus ja työn palautus -------- = 67,0 h