압축 알고리즘
저장 공간이 제한될 경우 파일을 압축할 필요가 있으며, 이 때 압축 알고리즘이 사용된다.
다양한 압축 알고리즘이 존재하지만, 궁극적으로는 다음 기능을 구현해야 한다.
-
압축(compress)
부호화(encoding) 과정을 거쳐 원본 파일을 압축 파일로 변환한다.
-
압축 풀기(decomporess)
복호화(decoding) 과정을 거쳐 압축 파일을 원본 파일로 변환한다.
유전 정보 압축
DNA는 4가지 종류(A, C, G, T)의 뉴클레오타이드(nucleotide)로 구성된다.
만약 n
개의 뉴클레오타이드로 구성된 염기서열을 문자열로 작성할 경우, 각 문자의 크기는 8비트이므로 총 8n
비트의 저장공간이 필요하다. (UTF-8 기준)
문자 | 8bit |
---|---|
A | 01000001 |
C | 01000011 |
G | 01000111 |
T | 01010100 |
하지만 4가지 종류의 문자를 나타내는 데에는 2개의 비트만 있어도 충분하다.
각 문자의 크기를 2비트로 압축한다면 필요한 저장공간은2n
비트로 감소하며, 최대 75%의 저장 공간을 절약할 수 있다.
문자 | 2bit |
---|---|
A | 00 |
C | 01 |
G | 10 |
T | 11 |
이 아이디어로 유전 정보를 압축하는 알고리즘을 작성한다.
1. compress
뉴클레오타이드의 수만큼 2비트 문자열을 추가하며, 추가할 때마다 쉬프트 연산자 <<
를 사용해 2비트씩 왼쪽으로 이동한다.
예를 들어 문자열 ATGC을 인수로 넘기면, 대응하는 2비트 문자열을 초기값 1 뒤에 차례대로 추가한다.
비트 단위 연산에서 비트 추가는 |=
연산자를 사용한다.
압축 과정을 파이썬으로 구현하면 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
def compress(gene:str) -> str:
bit_string:int = 1
for nucleotide in gene.upper():
bit_string <<= 2
if nucleotide == "A": bit_string |= 0b00
elif nucleotide == "C": bit_string |= 0b01
elif nucleotide == "G": bit_string |= 0b10
elif nucleotide == "T": bit_string |= 0b11
else:
raise ValueError(f"Invalid nucleotide: { nucleotide }")
return bit_string
1
2
3
4
5
6
from sys import getsizeof
original = "ATGC"
compressed = compress(original)
print(f"{ original }: { getsizeof(original) }바이트")
print(f"{ bin(compressed) }: { getsizeof(compressed) }바이트")
1
2
ATGC: 53바이트
0b100111001: 28바이트
sys.getsizeof()
메서드를 사용해 바이트를 확인할 수 있다.
파이썬 객체 시스템의 내제된 오버헤드 때문에 저장공간의 크기를 28바이트 미만으로 줄일 수는 없다.
bit_string의 초기값을 0으로 설정해도 괜찮지 않을까? 단순히 첫 번째 자리만 1에서 0으로 바뀐 값인 000111001을 반환할 것이라고 예측할 수도 있다. 하지만 반환되는 값은 111001이다.
이유는 간단하다. bit_string은 정수형이기 때문에 0으로 시작하는 앞 자릿수를 모두 제거해 반환하기 때문이다. 따라서 변환 오류를 방지하기 위해서 bit_string의 초기값은 반드시 1로 설정해야 한다.
2. decompress
bit_string의 끝 부분부터 2개씩 비트 문자열을 읽은 뒤, 뉴클레오타이드로 변환한 값을 gene에 추가한다.
처음 bit_string을 만들 때 비트 1로 시작했기 때문에 해당 비트를 제외해야 하므로, 전체 길이에서 1을 뺀 값만큼 반복을 시행한다.
복호화 과정에서 역순으로 읽었기 때문에 gene에는 뒤집힌 값이 저장되어 있다. 따라서 [::1]
을 사용해 원래 값으로 변환한 뒤 반환해야 한다.
압축 해제 과정을 파이썬으로 구현하면 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
def decompress(bit_string:str) -> str:
gene:str = ""
for i in range(0, bit_string.bit_length() - 1, 2):
bits:int = bit_string >> i & 0b11
if bits == 0b00: gene += "A"
elif bits == 0b01: gene += "C"
elif bits == 0b10: gene += "G"
elif bits == 0b11: gene += "T"
else:
raise ValueError(f"Invalid bits: { bits }")
return gene[::-1] # Reverse
1
2
decompressed = decompress(compressed)
print(f"{ decompressed }: { getsizeof(decompressed) }")
1
ATGC: 53
원본 데이터 original
와, 압축 후 해제 과정을 거친 데이터 decompressed
가 같음을 확인할 수 있다.
3. 시간 복잡도 분석
n
: 부호화 및 복호화 과정에서의 입력값의 길이
메서드 | 시간복잡도 |
---|---|
compress | O(n) |
decompress | O(n) |
4. Dictionary vs If statement
조건문 대신 파이썬의 사전 자료형를 사용해 구현할 수도 있다. 이 때 Dictionary는 뉴클레오타이드와 그에 해당하는 비트를 각각 Key와 Value로 갖는다.
조건문을 사용하면 각 반복 당 최대 4번 비교해야 하며, 사전 자료형을 사용하면 해시 충돌이 발생할 수 있다.
하지만 결과적으로는 두 방법 모두 O(n)
의 시간 복잡도를 가진다.
허프만 부호화
유전 데이터는 4가지 종류의 문자로만 구성된 고정 길이 코드(fixed length code)이다. 압축 과정이 어렵지 않지만 문자의 종류가 많아질수록 이 방법은 한계에 부딪힌다. 예를 들어 ASCII 문서는 최대 128가지의 문자로 구성되며, 유니코드의 경우 한글 수만 해도 11,172개이다. 각 문자에 해당하는 비트 문자열을 일일이 지정하는 것은 비효율적이며 높은 압축률을 기대할 수 없다.
가변 길이 코드(variable length code)를 사용하면 고정 길이 코드의 문제점을 해결할 수 있다. 대표적인 알고리즘인 허프만 부호화(Huffman coding) 과정에서는 각 문자의 등장 빈도를 바탕으로 트리를 만든 뒤, 노드의 위치에 따라 서로 다른 비트 문자열을 부여한다. 이 때 부여되는 가변 길이 코드는 어떤 비트 문자열이 다른 비트 문자열의 접두어가 될 수 없다는 규칙을 갖는 접두어 코드(prefix code)이다.
예를 들어 문자 A와 B에 다음과 같은 비트 문자열을 부여한다. 이 때 11은 110의 접두어이므로 접두어 코드의 조건을 만족하지 못한다.
1
2
A: 11
B: 110
접두어 코드를 사용하면 복호화 과정에서 빠른 해석을 할 수 있다.
1. 과정
허프만 부호화 과정을 통해 문자열 GOOGOLPLEX를 압축한다.
우선 주어진 문자열을 문자 단위로 분해한 다음, 각 문자가 등장하는 빈도수를 기준으로 오름차순으로 정렬한다.
데이터 | 빈도수 |
---|---|
E | 1 |
P | 1 |
X | 1 |
G | 2 |
L | 2 |
O | 3 |
가장 작은 빈도수를 가진 두 문자 E, P를 자식 노드로 갖는 서브트리를 만든다. 이후 빈도수를 기준으로 다시 오름차순으로 정렬한다. 트리가 완성될 때까지 이 과정을 반복한다.
완성된 허프만 트리에서 데이터 노드는 항상 리프에 위치한다.
특정 노드에 해당하는 비트 문자열을 구하기 위해서는 루트에서 노드까지의 경로를 모두 계산해야 한다. 이 때 루트를 기준으로 왼쪽 노드는 0, 오른쪽 노드는 1의 비트를 갖는다. 예를 들어 데이터 E가 위치한 노드의 비트 문자열은 루트에서 출발해 오른쪽(1
) -> 왼쪽(0
) -> 오른쪽(1
) -> 왼쪽(0
)의 순서로 이동 후 각각의 비트를 모두 합친 값인 1010
이다.
데이터 | 비트 문자열 |
---|---|
E | 1010 |
P | 1011 |
X | 100 |
G | 00 |
L | 01 |
O | 11 |
데이터의 빈도수가 높을수록 짧은 비트 문자열이 부여된 것을 확인할 수 있다. 또한 각 비트 문자열은 접두어 코드의 조건을 만족한다.
2. 구현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
from copy import deepcopy
from sys import getsizeof
def find_key(dict, val):
return next(key for key, value in dict.items() if value == val)
class TreeNode:
def __init__(self, key, value, left=None, right=None, parent=None):
self.key = key
self.value = value
self.left = left if (left == None) else left.key
self.right = right if (right == None) else right.key
self.parent = parent if (parent == None) else parent.key
def __repr__(self):
return f'({self.key}:{self.value} left<{self.left}>-right<{self.right}>-parent<{self.parent}>)\n'
def set_parent(self, parent):
self.parent = parent.key
def get_left(self):
return self.left
def get_right(self):
return self.right
class HuffTree:
def __init__(self, text:str):
self.text = text
self.frequency_map = self.get_frequency_map()
self.huffTree = self.get_huffTree()
self.binary_map = self.get_binary_map()
self.compressed = self.compress()
self.compressed_bin = bin(self.compressed)
def get_frequency_map(self) -> dict[str:int]:
frequency_map:dict[str:int] = {}
for i in self.text:
if i not in frequency_map:
frequency_map[i] = 1
else:
frequency_map[i] += 1
return frequency_map
def get_huffTree(self) -> dict[str:TreeNode]:
frequency_map:dict = self.frequency_map
x_list:list = list(frequency_map.keys())
LENGTH = deepcopy(len(frequency_map))
# -------------------------------------------------- 허프만 트리 생성
hTree:dict[str:TreeNode] = {}
for i in range(len(frequency_map)):
ky = x_list[i]
vl = frequency_map[ky]
hTree[ky] = TreeNode(ky, vl)
# -------------------------------------------------- 갱신 루프
for x in range(LENGTH - 1):
print('-'*40,f'\n루프 {x}')
# -------------------------------------------------- 1. 작업 목록 정렬
print(f'정렬 전 : {frequency_map}')
frequency_map = dict(sorted(frequency_map.items(), key=lambda item: item[1])) # ?
x_list = list(frequency_map.keys())
print(f'정렬 후 : {frequency_map}')
# -------------------------------------------------- 2. 노드 병합
first, second = x_list[0], x_list[1]
newNodeKey = f'*{x+1000}'
newNodeValue = frequency_map[first] + frequency_map[second]
print(f'노드 병합 : {first} + {second} -> {newNodeKey}')
# -------------------------------------------------- 3. 허프만 트리 갱신
hTree[newNodeKey] = TreeNode(newNodeKey, newNodeValue, hTree[first], hTree[second])
hTree[first].set_parent(hTree[newNodeKey])
hTree[second].set_parent(hTree[newNodeKey])
print(f'갱신된 허프만 트리 :\n {hTree}')
# -------------------------------------------------- 4. 작업 목록 갱신
del frequency_map[first]
del frequency_map[second]
frequency_map[newNodeKey] = newNodeValue
return hTree
def get_binary_map(self) -> dict[str:int]:
binary_map:dict[str:int] = {}
for data in self.frequency_map:
data_key = deepcopy(data)
nes = ""
while True:
temp = self.huffTree[data].parent
if temp:
if data == self.huffTree[temp].left:
nes += "0"
else:
nes += "1"
data = temp
else:
binary_map[data_key] = nes[::-1]
break
return binary_map
def compress(self) -> int:
compressed:int = 1
for char in self.text:
bit = self.binary_map[char]
compressed <<= len(bit)
compressed |= int(bit, 2)
return compressed
def decompress(self) -> str:
decompressed:str = ""
bit_list = list(self.binary_map.values())
check_list = self.compressed_bin[3:]
while len(check_list) >= 1:
temp = ""
for i in check_list:
temp += i
if temp in bit_list:
check_list = check_list[len(temp):]
break
continue
decompressed += find_key(self.binary_map, temp)
return decompressed
def find_key(dict, val):
return next(key for key, value in dict.items() if value == val)
3. 실행
1
2
3
4
5
6
7
8
9
10
11
12
13
if __name__ == "__main__":
original = "GOOGOLPLEX"
# original = 'In the beginning God created the heavens and the earth.Now the earth was formless and empty, darkness was over the surface of the deep, and the Spirit of God was hovering over the waters.And God said, "Let there be light," and there was light.God saw that the light was good, and He separated the light from the darkness.God called the light "day," and the darkness he called "night." And there was evening, and there was morning--the first day.'
machine = HuffTree(original)
compressed= machine.compress()
decompressed = machine.decompress()
print(f' original: <{ getsizeof(original) }바이트> { original } ')
print(f' compressed: <{ getsizeof(compressed) }바이트> { compressed } ')
print(f'decompressed: <{ getsizeof(decompressed) }바이트> { decompressed } ')
print(original == decompressed)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
루프 0
정렬 전 : {'G': 2, 'O': 3, 'L': 2, 'P': 1, 'E': 1, 'X': 1}
정렬 후 : {'P': 1, 'E': 1, 'X': 1, 'G': 2, 'L': 2, 'O': 3}
노드 병합 : P + E -> *1000
갱신된 허프만 트리 :
{'G': (G:2 left<None>-right<None>-parent<None>)
, 'O': (O:3 left<None>-right<None>-parent<None>)
, 'L': (L:2 left<None>-right<None>-parent<None>)
, 'P': (P:1 left<None>-right<None>-parent<*1000>)
, 'E': (E:1 left<None>-right<None>-parent<*1000>)
, 'X': (X:1 left<None>-right<None>-parent<None>)
, '*1000': (*1000:2 left<P>-right<E>-parent<None>)
}
----------------------------------------
루프 1
정렬 전 : {'X': 1, 'G': 2, 'L': 2, 'O': 3, '*1000': 2}
정렬 후 : {'X': 1, 'G': 2, 'L': 2, '*1000': 2, 'O': 3}
노드 병합 : X + G -> *1001
갱신된 허프만 트리 :
{'G': (G:2 left<None>-right<None>-parent<*1001>)
, 'O': (O:3 left<None>-right<None>-parent<None>)
, 'L': (L:2 left<None>-right<None>-parent<None>)
, 'P': (P:1 left<None>-right<None>-parent<*1000>)
, 'E': (E:1 left<None>-right<None>-parent<*1000>)
, 'X': (X:1 left<None>-right<None>-parent<*1001>)
, '*1000': (*1000:2 left<P>-right<E>-parent<None>)
, '*1001': (*1001:3 left<X>-right<G>-parent<None>)
}
----------------------------------------
루프 2
정렬 전 : {'L': 2, '*1000': 2, 'O': 3, '*1001': 3}
정렬 후 : {'L': 2, '*1000': 2, 'O': 3, '*1001': 3}
노드 병합 : L + *1000 -> *1002
갱신된 허프만 트리 :
{'G': (G:2 left<None>-right<None>-parent<*1001>)
, 'O': (O:3 left<None>-right<None>-parent<None>)
, 'L': (L:2 left<None>-right<None>-parent<*1002>)
, 'P': (P:1 left<None>-right<None>-parent<*1000>)
, 'E': (E:1 left<None>-right<None>-parent<*1000>)
, 'X': (X:1 left<None>-right<None>-parent<*1001>)
, '*1000': (*1000:2 left<P>-right<E>-parent<*1002>)
, '*1001': (*1001:3 left<X>-right<G>-parent<None>)
, '*1002': (*1002:4 left<L>-right<*1000>-parent<None>)
}
----------------------------------------
루프 3
정렬 전 : {'O': 3, '*1001': 3, '*1002': 4}
정렬 후 : {'O': 3, '*1001': 3, '*1002': 4}
노드 병합 : O + *1001 -> *1003
갱신된 허프만 트리 :
{'G': (G:2 left<None>-right<None>-parent<*1001>)
, 'O': (O:3 left<None>-right<None>-parent<*1003>)
, 'L': (L:2 left<None>-right<None>-parent<*1002>)
, 'P': (P:1 left<None>-right<None>-parent<*1000>)
, 'E': (E:1 left<None>-right<None>-parent<*1000>)
, 'X': (X:1 left<None>-right<None>-parent<*1001>)
, '*1000': (*1000:2 left<P>-right<E>-parent<*1002>)
, '*1001': (*1001:3 left<X>-right<G>-parent<*1003>)
, '*1002': (*1002:4 left<L>-right<*1000>-parent<None>)
, '*1003': (*1003:6 left<O>-right<*1001>-parent<None>)
}
----------------------------------------
루프 4
정렬 전 : {'*1002': 4, '*1003': 6}
정렬 후 : {'*1002': 4, '*1003': 6}
노드 병합 : *1002 + *1003 -> *1004
갱신된 허프만 트리 :
{'G': (G:2 left<None>-right<None>-parent<*1001>)
, 'O': (O:3 left<None>-right<None>-parent<*1003>)
, 'L': (L:2 left<None>-right<None>-parent<*1002>)
, 'P': (P:1 left<None>-right<None>-parent<*1000>)
, 'E': (E:1 left<None>-right<None>-parent<*1000>)
, 'X': (X:1 left<None>-right<None>-parent<*1001>)
, '*1000': (*1000:2 left<P>-right<E>-parent<*1002>)
, '*1001': (*1001:3 left<X>-right<G>-parent<*1003>)
, '*1002': (*1002:4 left<L>-right<*1000>-parent<*1004>)
, '*1003': (*1003:6 left<O>-right<*1001>-parent<*1004>)
, '*1004': (*1004:10 left<*1002>-right<*1003>-parent<None>)
}
1
2
3
4
original: <59바이트> GOOGOLPLEX
compressed: <28바이트> 65782302
decompressed: <59바이트> GOOGOLPLEX
True
허프만 트리를 만드는 과정을 출력한다. 이후 원본 데이터를 압축하고 해제하기까지의 각각의 과정의 바이트를 표시하고 원본과 결과물이 같음을 증명한다.