신경망이나 학습된 파라미터 없이 gzip 같은 압축기가 텍스트를 이어 쓰는 데 활용될 수 있는지, 그리고 압축과 예측의 관계가 어떻게 생성으로 이어지는지를 설명합니다.
얼마 전 나는 신경망 없는 언어 모델링에 대해 글을 썼다. 그 글에서는 무한 n-그램 모델로 셰익스피어를 생성했다. 가중치도 없고, 학습도 없고, 그저 세기만 했다. 그러다 운 좋게 Language Modeling is Compression 논문을 보게 되었는데, 거기서 압축–예측 동치성을 언급하고 있었다:
모든 예측 모델은 본질적으로 압축기이며, 모든 압축 알고리즘은 예측 모델이다.
이것은 자연스럽게 이런 질문으로 이어졌다. gzip도 언어 모델링을 할 수 있을까?1 신경망도 없고, 학습된 파라미터도 없고, 아무것도 없다. 그저 운영체제와 함께 제공되는 압축기뿐이다. 여기에 코퍼스를 미리 넣어 두고, 평범한 텍스트 프롬프트를 주면, 가장 잘 압축되는 바이트 시퀀스를 탐색해서 그 프롬프트를 이어 쓴다. 다음은 tiny Shakespeare로 미리 준비한 뒤 얻은 실제 편집 없는 출력이다:
gzipt --corpus data/tinyshakespeare.txt --prompt $'MENENIUS:\n' --length 200
MENENIUS:
'Though all at once canq
MARCIUS:
Pray now, nocamest thou to a morsel .
LARTIUS:
Hence, and
I' the end admire, where G
again; and after it ag .
결과는, 어느 정도는 그렇다. 정확히 일관된 텍스트는 아니지만, 분명히 텍스트에 대해 무언가를 알고 있다. gzip이 알고 있을 거라고 내가 기대했던 것보다 훨씬 더 많다.2 그렇다면 압축기가 어떻게 이런 것을 생성할 수 있을까?
압축기가 하는 일을 생각해 보자. 압축기는 자신이 “예상한” 데이터에는 적은 바이트를 쓰고, 그렇지 않은 데이터에는 많은 바이트를 쓴다. 만약 누군가가 문자 A를 백만 번 반복한 파일을 준다면, 그것은 한 문장으로 설명할 수 있다. 반면 백만 개의 무작위 바이트에는 활용할 구조가 없어서 거의 압축되지 않는다.
이것은 우연이 아니라 정보 이론의 핵심이다. 어떤 기호를 부호화하는 데 필요한 비트 수는 $- \left(log \right)_{2} p$이며, 여기서 $p$는 모델이 그 기호에 할당하는 확률이다. 확률이 높을수록 필요한 비트 수는 적다. 따라서 어떤 압축기든, 누가 명시적으로 적어 두었는지와 관계없이 내부에는 확률 모델이 숨어 있다.
gzip은 DEFLATE를 사용하는데, 이것은 32 KiB 슬라이딩 윈도우 안의 최근 텍스트와 일치하는 부분을 찾아 다음 바이트들을 압축한다. 어떤 이어지는 문자열이 이미 윈도우 안에 있는 내용을 되풀이하면, DEFLATE는 그것을 리터럴 바이트 대신 값싼 역참조로 부호화한다. 따라서:
gzip이 자기 윈도우 안에 이미 있는 텍스트를 되풀이하기 때문에 “예상한” 이어지는 문자열은 거의 아무 비용 없이 압축된다.
이제 점수를 정의할 수 있다. 어떤 문맥이 있고 후보 이어쓰기의 품질을 알고 싶다면, 다음을 측정하면 된다:
압축된 길이가 작을수록, 그 후보는 더 잘 “예측된” 것이다. 모델을 미리 준비하려면 gzip의 윈도우 안에 코퍼스를 포함시키면 된다. 코퍼스를 닮은 이어쓰기는 작게 압축되고, 그렇지 않은 이어쓰기는 크게 압축된다.
점수를 매기는 것은 한 가지 문제이고, 생성은 또 다른 문제다. 가장 잘 압축되는 다음 한 바이트만 고르는 순진한 접근은 심하게 실패하는데, 그 이유는 미묘하다. gzip은 오직 정수 바이트 길이만 제공할 뿐이다. 소수점은 없다. 바이트 하나를 추가해도 압축된 길이가 전혀 바뀌지 않는 경우가 많아서, 많은 후보가 동점이 되고 신호는 양자화 잡음 속에 묻힌다.
해결책은 결정을 내리기 전에 한 덩어리 전체를 미리 내다보는 것이다. gzipt는 바이트 시퀀스 위에서 빔 서치를 수행한다. 각 단계에서 현재 문맥은 다음과 같다:
corpus window + recent tail of (prompt + generated bytes)
그다음 gzipt는 가능한 다음 바이트들을 시도한다. 각 후보 이어쓰기는 context + candidate를 압축하고, 압축 결과가 몇 바이트를 차지하는지 확인해서 점수를 매긴다.
반복 과정은 다음과 같다:
beam_width개를 유지한다. 각각을 코퍼스에 등장하는 모든 바이트로 확장하고, 압축 길이로 전부 점수를 매긴 다음, 다시 최상위 beam_width개만 남긴다. 이것을 horizon 바이트까지 반복한다.temperature가 양수라면 최종 후보들 가운데서 샘플링한다), 그것을 덧붙인 뒤 처음부터 반복한다.중요한 세부 사항 하나는 생성된 출력 중 마지막 tail 바이트만 점수 계산 문맥에 남는다는 점이다. DEFLATE는 가까운 일치를 먼 일치보다 더 싸게 부호화하므로, 만약 gzip이 자신의 전체 이력을 볼 수 있다면 가장 싼 선택은 방금 출력한 텍스트를 계속 복사하는 축어적 반복 루프에 빠지는 경우가 많다.
위의 애니메이션에서 디코딩과 점수 계산 과정을 볼 수 있는데, 맨 위에 나온 것과 같은 재생이다. 전체 구현은 순수한 표준 라이브러리 Python 한 파일(zlib만 사용)이다. 직접 해 보고 싶다면 코드는 GitHub에 있다.
논문에서도 이것을 시도했지만, 결국 성능이 좋지 않았다. 빔 서치를 추가하자 생성 품질이 크게 향상되었는데(논문에서도 언급한 아이디어다), 이에 대해서는 아래에서 논의한다.↩︎
실제 코드는 gzip 프로세스를 띄우는 대신 zlib를 사용하지만, GziPT라는 이름이 너무 좋았다. 둘 다 내부적으로 같은 DEFLATE 알고리즘을 사용한다고 생각한다.↩︎
1
논문에서도 이것을 시도했지만, 결국 성능이 좋지 않았다. 빔 서치를 추가하자 생성 품질이 크게 향상되었는데(논문에서도 언급한 아이디어다), 이에 대해서는 아래에서 논의한다.
2
실제 코드는 gzip 프로세스를 띄우는 대신 zlib를 사용하지만, GziPT라는 이름이 너무 좋았다. 둘 다 내부적으로 같은 DEFLATE 알고리즘을 사용한다고 생각한다.