메뉴
HN
Hacker News • 4일 전

gzip이 언어 모델이 될 수 있을까?

IMP
5/10
핵심 요약

신경망 없이 운영체제에 기본 탑재된 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 바이트만큼 반복합니다.

원문 보기
원문 보기 (영어)
A while back I wrote about language modeling without neural networks , where I generated Shakespeare with an unbounded n-gram model: no weights, no training, just counting. Fortuitously, I came across the paper Language Modeling is Compression , which mentioned the compression–prediction equivalence : every prediction model is inherently a compressor, and all compression algorithms are prediction models . This led to the natural question: can gzip do language modeling? 1 No neural network, no learned parameters, nothing. Just the compressor that ships with your operating system. You prime it with a corpus, give it a normal text prompt, and it continues that prompt by searching for the byte sequences that compress best. Here’s some real, unedited output after priming it on 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 . It turns out, kind of? It’s not exactly coherent text, but it clearly knows something about the text. Much more than I expected gzip to know . 2 So how can a compressor generate this? Compression is prediction # Think about what a compressor does. It spends few bytes on data it “expects” and many bytes on data it doesn’t. If I hand you a file that’s the letter A repeated a million times, you can describe it in one sentence. A million random bytes, on the other hand, have no structure to exploit and barely compress at all. This is not a coincidence; it’s the core of information theory. The number of bits needed to encode a symbol is $-\log_2 p$, where $p$ is the probability the model assigns to it. High probability means few bits. So any compressor has a probability model hiding inside it, whether or not anyone wrote one down. gzip uses DEFLATE , which compresses the next bytes by finding matches against the recent text in a 32 KiB sliding window. If a continuation echoes something already in the window, DEFLATE encodes it as a cheap back-reference instead of literal bytes. So: A continuation that gzip “expected”, because it echoes text already in its window, compresses to almost nothing. That gives us a score. If I have some context and I want to know how good a candidate continuation is, I just measure: $$\text{score}(\text{candidate}) = \texttt{len(gzip(context + candidate))}$$ The smaller the compressed length, the more “predicted” the candidate is. To prime the model, I include a corpus in gzip’s window. Any continuation that looks like the corpus compresses small, and any continuation that doesn’t compresses large. Generating by beam search # Scoring is one thing; generating is another. The naive approach of picking the single next byte that compresses best fails badly, and for a subtle reason: gzip only gives an integer byte length (no fractions). Adding one byte often doesn’t change the compressed length at all, so many candidates tie and the signal is buried in quantization noise. The fix is to look ahead a whole span before committing. gzipt runs a beam search over byte sequences. At each step, the current context is: corpus window + recent tail of (prompt + generated bytes) Then gzipt tries possible next bytes. Each candidate continuation is scored by compressing context + candidate and checking how many bytes the compressed result takes. The loop is: Prompt. Start with the user’s prompt as the initial text to continue. There is no start token; the prompt bytes are just part of the context gzip sees. Context. Show gzip the corpus window plus the recent tail of the prompt/generated text. Search. Keep the beam_width most-compressible partial continuations. Extend each by every byte that occurs in the corpus, score all of them by compressed length, and prune back down to the best beam_width . Repeat for horizon bytes. Commit. Take the most-compressible full span (or sample among the finalists if temperature is positive), append it, and start the loop over. One detail that matters is that only the last tail bytes of generated output stay in the scoring context. DEFLATE codes nearby matches more cheaply than far ones, so if gzip could see its entire history, the cheapest thing to do is often to fall into verbatim loops, repeatedly copying text it just emitted. You can see the decoding and scoring process in the animation above, which is the same replay shown at the top. The whole thing is one file of pure standard-library Python (just zlib ). Code’s on GitHub if you want to play with it. The paper did try this, but it ended up performing poorly. Adding beam search significantly improved generation quality (an idea they mentioned), which is discussed below.  ↩︎ The code actually uses zlib instead of spawning a gzip process, but the name GziPT was too good. I believe they both use the same DEFLATE algorithm under the hood.  ↩︎