Mainly Tech projects on Python and Electronic Design Automation.

Saturday, March 28, 2009

Huffman Encoding in Python



Huffman encoding came up on href="http://www.rosettacode.org/wiki/Huffman_codes">Rosetta
Code.

Huffman encoding is a way to assign binary codes to symbols
that
reduces the overall number of bits used to encode a typical string of
of those symbols.


For example, if you use letters as symbols and have details of
the frequency of occurence of those letters in typical strings, then
you could just encode each letter with a fixed number of bits, such as
in ASCII codes. You can do better than this by encoding more frequently
occurring letters such as e and a, with smaller bit strings; and less
frequently occurring letters such as q and x with longer bit strings.


Any string of letters will be encoded as a string of bits that
are no-longer of the same length per letter. To successfully decode
such as string, the smaller codes assigned to letters such as 'e'
cannot occur as a prefix in the larger codes such as that for 'x'.



If you were to assign a code 01 for 'e' and code 011 for
'x',
then if the bits to decode started as 011... then you would not know
iif you should decode an 'e' or an 'x'.


The Huffman coding scheme takes each symbol and its weight (or
frequency of occurrence), and generates proper encodings for each
symbol taking account of the weights of each symbol, so that higher
weighted symbols have less bits in their encoding. (See the href="http://en.wikipedia.org/wiki/Huffman_coding" class="extiw"
title="wp:Huffman_coding">WP article for more
information).


A Huffman encoding can be computed by first creating a tree of
nodes:


cellpadding="1" cellspacing="1">









  1. Create a leaf node for each symbol and add it to the
    priority queue.

  2. While there is more than one node in the queue:

    1. Remove the node of highest priority (lowest
      probability) twice to get two nodes.

    2. Create a new internal node with these two nodes
      as children
      and with probability equal to the sum of the two nodes' probabilities.

    3. Add the new node to the queue.



  3. The remaining node is the root node and the tree is
    complete.


Traverse the constructed binary tree from root to leaves
assigning
and accumulating a '0' for one branch and a '1' for the other at each
node. The accumulated zeroes and ones at each leaf constitute a Huffman
encoding for those symbols and weights.


href="http://www.rosettacode.org/wiki/Image:Huffman_coding_example.jpg"
class="image" title="Huffman coding example.jpg"> alt=""
src="http://www.rosettacode.org/w/images/thumb/8/8b/Huffman_coding_example.jpg/250px-Huffman_coding_example.jpg"
border="0" height="120" width="250">


In Python


I orginally gave an an example that matched a definition that
was later found to be insufficient, so substituted my own definition
above.. My href="http://www.rosettacode.org/w/index.php?title=Huffman_codes&oldid=26720#Python">first
Python solution  on RC to the wrong definition, did
have the advantage, (as I saw it), of not having to traverse a tree.


My 'true' Huffman code creator assembles each symbol and its
weight into the following structure initially (the style="font-weight: bold;">leaf structure):

style="font-family: monospace;">[ weight, [ symbol, []]]

The weight applies to every (in this case only one), of the style="font-family: monospace;"> [symbol, []]
pairs after it in the same list.

The empty list is used to accumulate the Huffman code for the symbol as
we manipulate the heap, without having to walk a constructed tree
structure.



There are two types of input to the program that I am running examples
with:



  1. A string of space separated symbol, weight pairs, as used
    in small examples.

  2. A sample of text for which letters and letter frequencies
    are extracted.


The if statement at line 23
 allows me to switch between the two types of input whilst
exploring the algorithm.

The tutor argument to the encode function shows what is happening in
the loop around the heap pops


 1 
color="#804040"> 2 from heapq color="#a020f0">import heappush, heappop, heapify
color="#804040"> 3
color="#804040"> 4 def color="#008080">codecreate(symbol2weights, tutor= False):
color="#804040"> 5 ''' color="#ff00ff"> Huffman encode the given dict mapping symbols to weights '''
color="#804040"> 6 heap = [ [float(wt), [sym, []]] color="#804040">for sym, wt color="#804040">in symbol2weights.iteritems() ]
color="#804040"> 7 heapify(heap)
color="#804040"> 8 if tutor: color="#804040">print " color="#ff00ff">ENCODING:", sorted(symbol2weights.iteritems())
color="#804040"> 9 while len(heap) >1:
color="#804040">10 lo = heappop(heap)
color="#804040">11 hi = heappop(heap)
color="#804040">12 color="#804040">if tutor: color="#804040">print " color="#ff00ff"> COMBINING:", lo, ' color="#6a5acd">\n AND:', hi
color="#804040">13 color="#804040">for i color="#804040">in lo[1:]: i[1].insert(0, ' color="#ff00ff">0')
color="#804040">14 color="#804040">for i color="#804040">in hi[1:]: i[1].insert(0, ' color="#ff00ff">1')
color="#804040">15 lohi = [ lo[0] + hi[0] ] + lo[1:] + hi[1:]
color="#804040">16 color="#804040">if tutor: color="#804040">print " color="#ff00ff"> PRODUCING:", lohi, ' color="#6a5acd">\n'
color="#804040">17 heappush(heap, lohi)
color="#804040">18 codes = heappop(heap)[1:]
color="#804040">19 for i color="#804040">in codes: i[1] = ''.join(i[1])
color="#804040">20 return sorted(codes, key= color="#804040">lambda x: (len(x[-1]), x))
color="#804040">21
color="#804040">22 # Input types
color="#804040"> style="font-weight: bold; background-color: rgb(255, 255, 102);">23 color="#804040">if 1:
color="#804040">24 readin = " color="#ff00ff">B 25 C 2.5 D 12.5 A 5 color="#6a5acd">\n"
color="#804040">25 #readin = "a .1 b .15 c .3 d .16 e .29" # Wikipedia sample
color="#804040">26 #readin = "a1 .4 a2 .35 a3 .2 a4 .05" # Wikipedia sample
color="#804040">27 #readin = "A 50 B 25 C 12.5 D 12.5" # RC example
color="#804040">28
color="#804040">29 cleaned = readin.strip().split()
color="#804040">30 symbol2weights = dict((symbol, wt)
color="#804040">31 color="#804040">for symbol, wt color="#804040">in zip(cleaned[0::2], cleaned[1::2]) )
color="#804040">32 else:
color="#804040">33 astring = " color="#ff00ff">this is an example for huffman encoding"
color="#804040">34 symbol2weights = dict((ch, astring.count(ch)) color="#804040">for ch color="#804040">in set(astring)) color="#0000ff"># for astring
color="#804040">35
color="#804040">36 huff = codecreate(symbol2weights, True)
color="#804040">37 print " color="#6a5acd">\nSYMBOL color="#6a5acd">\tWEIGHT color="#6a5acd">\tHUFFMAN CODE"
color="#804040">38 for h color="#804040">in huff:
color="#804040">39 print " color="#ff00ff">%s\t color="#ff00ff">%s\t color="#ff00ff">%s" % (h[0], symbol2weights[h[0]], h[1])




A run, with the tutor enabled gives the following output:

style="font-family: monospace;">ENCODING: [('A', '5'), ('B',
'25'), ('C', '2.5'), ('D', '12.5')] style="font-family: monospace;">
 
COMBINING: [2.5, ['C', []]]
style="font-family: monospace;">
       
AND: [5.0, ['A', []]]


 
PRODUCING: [7.5, ['C', ['0']], ['A', ['1']]]
style="font-family: monospace;">


 
COMBINING: [7.5, ['C', ['0']], ['A', ['1']]]
style="font-family: monospace;">
       
AND: [12.5, ['D', []]]
style="font-family: monospace;">
 
PRODUCING: [20.0, ['C', ['0', '0']], ['A', ['0', '1']], ['D', ['1']]]
style="font-family: monospace;">


 
COMBINING: [20.0, ['C', ['0', '0']], ['A', ['0', '1']], ['D', ['1']]]
style="font-family: monospace;">
       
AND: [25.0, ['B', []]]
style="font-family: monospace;">
 
PRODUCING: [45.0, ['C', ['0', '0', '0']], ['A', ['0', '0', '1']], ['D',
['0', '1']], ['B', ['1']]]
style="font-family: monospace;">




SYMBOL   
WEIGHT    HUFFMAN CODE
style="font-family: monospace;">
B   
     25   
    1
style="font-family: monospace;">
D   
     12.5      01
style="font-family: monospace;">
A   
     5   
     001
style="font-family: monospace;">
C   
     2.5   
   000





Encode/Decode Round-tripping


I realised that I could use a method similar to how I accumulate the
codes in the heap loop, to generate a single function that can
recognise a single symbol from the beginning of the encoded symbols. By
using the function in a loop, I could regenerate the symbol list.



In the codecreate function, the leaf
structure
is modified:

style="font-family: monospace;">[weight, [ [symbol, []] ],
repr(sym)]

Their is an extra level of list around the [symbol, code accumulation
list pair]  as well as a new item: 'repr(sym)' it is the third
item in the outer list and will always be the  function to
generate the item as accumulated so far.. This function starts off by
returning just the symbol and an outer if/then/else expression is added
as we go around the heap loop (see line style="font-weight: bold;">43)



After the outer expression is accumulated, it is turned into a lambda
expression, the string eval'd, and assigned to a global variable (see
line 47)



I use function probchoice (from earlier RC work), to create an
arbitrary sequence of symbols to in the given weighting then encode and
decode it as well as giving some stats on space saving.




  1 
color="#804040"> 2 from heapq color="#a020f0">import heappush, heappop, heapify
color="#804040"> 3 import random, bisect
color="#804040"> 4
color="#804040"> 5
color="#804040"> 6 # Helper routine for generating test sequences
color="#804040"> style="background-color: rgb(255, 255, 102);"> 7 color="#804040">def color="#008080">probchoice(items, probs):
color="#804040"> 8 ''' color="#6a5acd">\
color="#804040"> 9 Splits the interval 0.0-1.0 in proportion to probs
color="#804040"> 10 then finds where each random.random() choice lies
color="#804040"> 11 (This routine, probchoice, was released under the
color="#804040"> 12 GNU Free Documentation License 1.2)
color="#804040"> 13 '''
color="#804040"> 14
color="#804040"> 15 prob_accumulator = 0
color="#804040"> 16 accumulator = []
color="#804040"> 17 for p color="#804040">in probs:
color="#804040"> 18 prob_accumulator += p
color="#804040"> 19 accumulator.append(prob_accumulator)
color="#804040"> 20
color="#804040"> 21 while True:
color="#804040"> 22 r = random.random()
color="#804040"> 23 yield items[bisect.bisect(accumulator, r)]
color="#804040"> 24
color="#804040"> 25
color="#804040"> 26 # placeholder
color="#804040"> 27 decode = color="#804040">lambda : None
color="#804040"> 28
color="#804040"> 29 def color="#008080">codecreate(symbol2weights, tutor= False):
color="#804040"> 30 ''' color="#ff00ff"> Huffman encode the given dict mapping symbols to weights '''
color="#804040"> 31 global decode
color="#804040"> 32
color="#804040"> 33 heap = [ [float(wt), [[sym, []]], repr(sym)] color="#804040">for sym, wt color="#804040">in symbol2weights.iteritems() ]
color="#804040"> 34 heapify(heap)
color="#804040"> 35 if tutor: color="#804040">print " color="#ff00ff">ENCODING:", sorted(symbol2weights.iteritems())
color="#804040"> 36 while len(heap) >1:
color="#804040"> 37 lo = heappop(heap)
color="#804040"> 38 hi = heappop(heap)
color="#804040"> 39 color="#804040">if tutor: color="#804040">print " color="#ff00ff"> COMBINING:", lo, ' color="#6a5acd">\n AND:', hi
color="#804040"> 40 color="#804040">for i color="#804040">in lo[1]: i[1].insert(0, ' color="#ff00ff">0')
color="#804040"> 41 color="#804040">for i color="#804040">in hi[1]: i[1].insert(0, ' color="#ff00ff">1')
color="#804040"> 42 lohi = [ lo[0] + hi[0] ] + [lo[1] + hi[1]]
color="#804040"> style="background-color: rgb(255, 255, 102);"> 43 lohi.append(' color="#ff00ff">(%s if nextbit() else %s)' % (hi[2], lo[2]))
color="#804040"> 44 color="#804040">if tutor: color="#804040">print " color="#ff00ff"> PRODUCING:", lohi, ' color="#6a5acd">\n'
color="#804040"> 45 heappush(heap, lohi)
color="#804040"> 46 wt, codes, decoder = heappop(heap)
color="#804040"> style="background-color: rgb(255, 255, 102);"> 47 decode = eval(' color="#ff00ff">lambda :' + decoder, globals())
color="#804040"> 48 decode.__doc__ = decoder
color="#804040"> 49 for i color="#804040">in codes: i[1] = ''.join(i[1])
color="#804040"> 50 #for i in codes: i[::] = i[:2]
color="#804040"> 51 return sorted(codes, key= color="#804040">lambda x: (len(x[-1]), x))
color="#804040"> 52
color="#804040"> 53 # Input types
color="#804040"> style="background-color: rgb(255, 255, 102);"> 54 color="#804040">if 1:
color="#804040"> 55 tutor = True
color="#804040"> 56 sequencecount = 50
color="#804040"> 57 readin = " color="#ff00ff">B 25 C 2.5 D 12.5 A 5 color="#6a5acd">\n"
color="#804040"> 58 #readin = "a .1 b .15 c .3 d .16 e .29" # Wikipedia sample
color="#804040"> 59 #readin = "a1 .4 a2 .35 a3 .2 a4 .05" # Wikipedia sample
color="#804040"> 60 #readin = "A 50 B 25 C 12.5 D 12.5" # RC example
color="#804040"> 61
color="#804040"> 62 cleaned = readin.strip().split()
color="#804040"> 63 symbol2weights = dict((symbol, wt)
color="#804040"> 64 color="#804040">for symbol, wt color="#804040">in zip(cleaned[0::2], cleaned[1::2]) )
color="#804040"> 65 else:
color="#804040"> 66 tutor = False
color="#804040"> 67 sequencecount = 500
color="#804040"> 68 astring = " color="#ff00ff">this is an example for huffman encoding"
color="#804040"> 69 symbol2weights = dict((ch, astring.count(ch)) color="#804040">for ch color="#804040">in set(astring)) color="#0000ff"># for astring
color="#804040"> 70
color="#804040"> 71 huff = codecreate(symbol2weights, tutor= tutor)
color="#804040"> 72 print " color="#6a5acd">\nSYMBOL color="#6a5acd">\tWEIGHT color="#6a5acd">\tHUFFMAN CODE"
color="#804040"> 73 for h color="#804040">in huff:
color="#804040"> 74 print " color="#ff00ff">%s\t color="#ff00ff">%s\t color="#ff00ff">%s" % (h[0], symbol2weights[h[0]], h[1])
color="#804040"> 75
color="#804040"> 76 ##
color="#804040"> 77 ## encode-decode check
color="#804040"> 78 ##
color="#804040"> 79 symbol2code = dict(huff)
color="#804040"> 80 symbols, weights = zip(*symbol2weights.iteritems())
color="#804040"> 81 # normalize weights
color="#804040"> 82 weights = [float(wt) color="#804040">for wt color="#804040">in weights]
color="#804040"> 83 tot = sum(weights)
color="#804040"> 84 weights = [wt/tot color="#804040">for wt color="#804040">in weights]
color="#804040"> 85 # Generate a sequence
color="#804040"> 86 nxt = probchoice(symbols, weights).next
color="#804040"> 87 symbolsequence = [nxt() color="#804040">for i color="#804040">in range(sequencecount)]
color="#804040"> 88 # encode it
color="#804040"> 89 bitsequence = ''.join(symbol2code[sym] color="#804040">for sym color="#804040">in symbolsequence)
color="#804040"> 90
color="#804040"> 91 sslen, slen, blen = len(symbolsequence), len(symbols), len(bitsequence)
color="#804040"> 92 countlen = len(bin(slen-1)[2:])
color="#804040"> 93 print '''
color="#804040"> 94
color="#804040"> 95
color="#804040"> 96 ROUND-TRIPPING
color="#804040"> 97 ==============
color="#804040"> 98 I have generated a random sequence of %i symbols to the given weights.
color="#804040"> 99 If I use a binary count to encode each of the %i symbols I would need
color="#804040">100 %i * %i = %i bits to encode the sequence.
color="#804040">101 Using the Huffman code, I need only %i bits.
color="#804040">102 ''' % (sslen, slen, sslen, countlen, sslen * countlen, blen )
color="#804040">103
color="#804040">104 ## decoding
color="#804040">105 nextbit = (bit==' color="#ff00ff">1' for bit color="#804040">in bitsequence).next
color="#804040">106
color="#804040">107 decoded = []
color="#804040">108 try:
color="#804040">109 while 1:
color="#804040">110 decoded.append(decode())
color="#804040">111 except StopIteration:
color="#804040">112 pass
color="#804040">113
color="#804040">114 print " color="#ff00ff">Comparing the decoded sequence with the original I get:", decoded == symbolsequence




This short run is in tutor mode, so you can track the accumulation of
the decode function:

style="font-family: monospace;">ENCODING: [('A', '5'), ('B',
'25'), ('C', '2.5'), ('D', '12.5')] style="font-family: monospace;">
 
COMBINING: [2.5, [['C', []]], "'C'"]


       
AND: [5.0, [['A', []]], "'A'"]
style="font-family: monospace;">
 
PRODUCING: [7.5, [['C', ['0']], ['A', ['1']]], style="font-weight: bold;">"('A' if nextbit() else 'C')"
]




 
COMBINING: [7.5, [['C', ['0']], ['A', ['1']]], "('A' if nextbit() else
'C')"]


       
AND: [12.5, [['D', []]], "'D'"]
style="font-family: monospace;">
 
PRODUCING: [20.0, [['C', ['0', '0']], ['A', ['0', '1']], ['D', ['1']]],
"('D' if nextbit() else
('A' if nextbit() else 'C'))"
]
style="font-family: monospace;">


 
COMBINING: [20.0, [['C', ['0', '0']], ['A', ['0', '1']], ['D', ['1']]],
"('D' if nextbit() else ('A' if nextbit() else 'C'))"]
style="font-family: monospace;">
       
AND: [25.0, [['B', []]], "'B'"]
style="font-family: monospace;">
 
PRODUCING: [45.0, [['C', ['0', '0', '0']], ['A', ['0', '0', '1']],
['D', ['0', '1']], ['B', ['1']]], "('B'
if nextbit() else ('D' if nextbit() else ('A' if nextbit() else 'C')))"
]






SYMBOL   
WEIGHT    HUFFMAN CODE
style="font-family: monospace;">
B   
     25   
style="font-family: monospace;"> 
   style="font-family: monospace;">1 style="font-family: monospace;">
D   
 
   
style="font-family: monospace;">12.5  
   01 style="font-family: monospace;">
A   
 
   
style="font-family: monospace;">5   
     001 style="font-family: monospace;">
C   
 
   
style="font-family: monospace;">2.5   
   000 style="font-family: monospace;">






ROUND-TRIPPING style="font-family: monospace;">
============== style="font-family: monospace;">
I have generated a
random sequence of 50 symbols to the given weights.
style="font-family: monospace;">
If I use a binary
count to encode each of the 4 symbols I would need
style="font-family: monospace;">
50 * 2 = style="font-weight: bold;">100 bits to encode
the sequence.

Using the Huffman
code, I need only 90
bits.




Comparing the
decoded sequence with the original I get: style="font-weight: bold;">True



And if I change line 54
to be False, I get the following:

style="font-family: monospace;">SYMBOL   
WEIGHT    HUFFMAN CODE style="font-family: monospace;">
    
6    101
style="font-family: monospace;">
n   
4    010
style="font-family: monospace;">
a   
3    1001
style="font-family: monospace;">
e   
3    1100
style="font-family: monospace;">
f   
3    1101
style="font-family: monospace;">
h   
2    0001
style="font-family: monospace;">
i   
3    1110
style="font-family: monospace;">
m   
2    0010
style="font-family: monospace;">
o   
2    0011
style="font-family: monospace;">
s   
2    0111
style="font-family: monospace;">
g   
1    00000
style="font-family: monospace;">
l   
1    00001
style="font-family: monospace;">
p   
1    01100
style="font-family: monospace;">
r   
1    01101
style="font-family: monospace;">
t   
1    10000
style="font-family: monospace;">
u   
1    10001
style="font-family: monospace;">
x   
1    11110
style="font-family: monospace;">
c   
1    111110
style="font-family: monospace;">
d   
1    111111
style="font-family: monospace;">






ROUND-TRIPPING style="font-family: monospace;">
============== style="font-family: monospace;">
I have generated a
random sequence of 500 symbols to the given weights.
style="font-family: monospace;">
If I use a binary
count to encode each of the 19 symbols I would need
style="font-family: monospace;">
500 * 5 = 2500 bits
to encode the sequence.
style="font-family: monospace;">
Using the Huffman
code, I need only 2012 bits.
style="font-family: monospace;">


Comparing the
decoded sequence with the original I get: True
style="font-family: monospace;">





Note: The purpose of the program is to teach me more about Huffman
coding and is not an exercise in speed!



I am the author of all the Python on this page. The diagram is from
Wikipedia. Please refrain from passing-off my code as your own (that
one is mainly for students).

3 comments:

  1. The bitarray module
    http://pypi.python.org/pypi/bitarray/
    efficiently represents an array of booleans as bits. It also provides example code for performing Huffman encoding and decoding.

    ReplyDelete
  2. It takes doing my own program for me to truly appreciate what is being given in something like the bitarray module.

    - Paddy.

    ReplyDelete
  3. Holy broken html, batman.

    ReplyDelete

Followers

Subscribe Now: google

Add to Google Reader or Homepage

Go deh too!

whos.amung.us

Blog Archive