gzip이 언어 모델이 될 수 있을까?
신경망 없이 운영체제에 기본 탑재된 gzip 압축기만으로 셰익스피어 텍스트를 생성하는 실험을 다룬 글입니다. 압축과 예측의 동등성이라는 정보이론적 원리를 이용해, 코퍼스를 윈도우에 넣고 빔 서치로 압축이 가장 잘 되는(=모델이 예상하는) 후보 텍스트를 선택합니다. 신경망 없는 언어 모델링의 가능성을 보여주는 흥미로운 연구 사례입니다.
얼마 전 저는 신경망 없이 언어 모델링을 하는 방법에 대해 글을 쓴 적이 있습니다. 가중치도 없고 학습도 없이, 오직 카운팅만으로 무경계(unbounded) n-gram 모델을 사용해 셰익스피어를 생성하는 것이었죠. 우연히 'Language Modeling is Compression'이라는 논문을 접하게 됐는데, 여기서 압축-예측 동등성(compression–prediction equivalence)을 언급하고 있었습니다. 모든 예측 모델은 본질적으로 압축기이고, 모든 압축 알고리즘은 예측 모델이라는 것이죠. 그래서 자연스럽게 이런 질문이 떠올랐습니다. gzip으로 언어 모델링을 할 수 있을까? 신경망도 없고, 학습된 파라미터도 없이, 운영체제에 기본 포함된 압축기만으로 말입니다. 코퍼스를 먼저 넣어주고, 일반 텍스트 프롬프트를 주면, 가장 잘 압축되는 바이트 시퀀스를 찾아 프롬프트를 이어갑니다. 다음은 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 .
결과는 뭐, 그럭저럭 됩니다. 완전히 coherent한 텍스트는 아니지만, 분명히 텍스트에 대해 뭔가 알고 있습니다. 제가 gzip에 기대했던 것보다 훨씬 많이요.
그렇다면 압축기가 어떻게 이런 텍스트를 생성할 수 있을까요?
압축은 곧 예측이다
압축기가 하는 일을 생각해 봅시다. 압축기는 '예상되는' 데이터에는 적은 바이트를 쓰고, 예상 밖의 데이터에는 많은 바이트를 씁니다. 문자 A가 백만 번 반복된 파일을 준다면 한 문장으로 설명할 수 있습니다. 반면 백만 개의 랜덤 바이트는 활용할 구조가 없어 거의 압축되지 않죠. 이건 우연이 아니라 정보이론의 핵심입니다. 심볼 하나를 인코딩하는 데 필요한 비트 수는 $-\log_2 p$이고, 여기서 $p$는 모델이 그 심볼에 부여하는 확률입니다. 확률이 높으면 적은 비트가 듭니다. 따라서 모든 압축기에는 누군가 명시적으로 작성했든 아니든, 확률 모델이 내장되어 있습니다.
gzip은 DEFLATE 알고리즘을 사용하는데, 이는 32 KiB 슬라이딩 윈도우 내의 최근 텍스트에서 매칭을 찾아 다음 바이트들을 압축합니다. 이어지는 텍스트가 윈도우에 이미 있는 내용을 반복하면, DEFLATE는 그것을 리터럴 바이트 대신 저렴한 백-레퍼런스(back-reference)로 인코딩합니다. 즉:
윈도우에 이미 있는 텍스트를 반복하는, gzip이 '예상한' 이어짐은 거의 0바이트에 가깝게 압축됩니다. 이게 바로 점수가 됩니다. 어떤 컨텍스트가 있고 후보 이어짐이 얼마나 좋은지 알고 싶다면, 그냥 이렇게 측정하면 됩니다:
$$\text{score}(\text{candidate}) = \texttt{len(gzip(context + candidate))}$$
압축 길이가 작을수록 후보가 더 '예측된' 것입니다. 모델을 프라이밍하려면 gzip의 윈도우에 코퍼스를 포함시킵니다. 코퍼스와 비슷한 이어짐은 작게 압축되고, 그렇지 않은 것은 크게 압축됩니다.
빔 서치로 생성하기
점수 매기기와 생성은 별개의 문제입니다. 가장 잘 압축되는 다음 바이트 하나만 선택하는 순진한 접근은 끔찍하게 실패하는데, 이유가 묘합니다. gzip은 정수 바이트 길이만 제공합니다(소수점 없음). 바이트 하나를 추가해도 압축 길이가 전혀 변하지 않는 경우가 많아, 많은 후보가 동점이 되고 신호가 양자화 노이즈에 묻혀 버리는 것이죠.
해결책은 확정하기 전에 전체 구간(span)을 내다보는 것입니다. gzipt는 바이트 시퀀스에 대해 빔 서치를 수행합니다. 각 단계에서 현재 컨텍스트는 다음과 같습니다:
코퍼스 윈도우 + (프롬프트 + 생성된 바이트)의 최근 꼬리 부분
그런 다음 gzipt는 가능한 다음 바이트들을 시도합니다. 각 후보 이어짐은 컨텍스트 + 후보를 압축해서 압축 결과가 몇 바이트인지 확인함으로써 점수가 매겨집니다. 루프는 다음과 같습니다:
프롬프트. 사용자의 프롬프트를 이어갈 초기 텍스트로 시작합니다. 시작 토큰 같은 건 없습니다. 프롬프트 바이트는 그냥 gzip이 보는 컨텍스트의 일부입니다.
컨텍스트. gzip에게 코퍼스 윈도우와 프롬프트/생성 텍스트의 최근 꼬리를 보여줍니다.
검색. 가장 잘 압축되는 beam_width개의 부분 이어짐을 유지합니다. 각각을 코퍼스에 등장하는 모든 바이트로 확장하고, 압축 길이로 전부 점수를 매긴 뒤, 다시 최고의 beam_width개로 가지치기합니다. 이를 horizon 바이트만큼 반복합니다.