Lemma
수학, 거꾸로
도입 · 같은 픽셀 수, 다른 파일 크기

어떤 이미지는 왜 압축이 잘되고, 어떤 건 왜 안 될까?

같은 크기의 사진 두 장, 같은 픽셀 수 — 표면적으로는 같은 데이터. PNG로 저장해 보면 한쪽은 18KB, 다른 한쪽은 312KB. 두 번째가 압축에 버티는 이유는 무엇일까? 답은 엔트로피 모듈의 한 양을 두 번 쓰는 것이다 — 한 번은 히스토그램에, 한 번은 히스토그램이 버리고 간 구조에.

파일의 크기는 픽셀 수가 아니라 정보량에 묶인다. 색 히스토그램은 픽셀 값 위의 다.

위젯 — 이미지 압축
H · 히스토그램4.00 bits/px
H · 이웃 차이1.00 bits/px
비율0.25×
히스토그램 (값별 개수)

세 패턴 모두 같은 히스토그램 — 각 값은 16번씩 등장.

세 패치 모두 같은 256개의 픽셀 값을 쓴다 — 16단계 회색 각각이 16번씩 등장. 그러니까 히스토그램 엔트로피는 동일: 4.00 bits/픽셀, 어느 패턴에서나. 하지만 이웃 차이 엔트로피는 다르다: 그라디언트는 0 근처 (인접 행은 0 또는 1만 차이), 블록은 조금 높음 (블록 경계에서 점프), 뒤섞임은 ~3.72 bits까지 치솟는다 — 인접 픽셀이 본질적으로 독립이라는 뜻. 히스토그램은 *심볼*을 세고, 이웃 차이는 *구조*를 본다. 실제 압축기 (PNG, JPEG, gzip) 는 두 번째 관점을 이용한다. 그래서 히스토그램이 같은 이미지가 전혀 다른 크기로 압축될 수 있다.
흐름
1

같은 데이터, 다른 정보

16×16 흑백 이미지는 픽셀 256개 — 픽셀당 1바이트씩이면 256바이트. 그게 _데이터_다. 그런데 이 이미지가 실제로 담은 _정보의 비트 수_는 얼마일까? 그건 다른 질문이고, 답은 픽셀당 8비트보다 한참 적을 수 있다. 모든 픽셀이 같은 색이라면 비트가 거의 필요 없다 — “전부 다, 값 7.” 모든 픽셀이 서로 독립인 무작위 잡음이라면 픽셀당 8비트 가까이 필요하다 — 써먹을 패턴이 없으니까. 실제 사진은 그 사이 어딘가에 있고, _데이터_와 정보 사이의 이 간격이 바로 압축기가 거두는 수확이다.

2

히스토그램 한계

처음 들여다볼 데는 픽셀 값 분포히스토그램이다 (이 항목은 곧 정리한다). 각 값에 픽셀이 몇 개씩 있는지 세고, 총 개수로 나눠 확률을 얻고, H=Σplog2pH = -Σ p \log₂ p에 집어넣는다. 그러면 각 픽셀을 이웃과 무관하게 따로따로 인코딩할 때 픽셀당 평균 몇 가 드는지가 나온다.

16단계 패치에서 각 단계가 딱 16번씩 등장한다면, 모든 단계의 확률은 1/161/16, H=log216=4H = \log₂ 16 = 4 bits/픽셀. 이게 히스토그램 한계 — 심볼 단위 인코딩의 섀넌 한계다. 픽셀당 8비트를 4비트로 반토막 내는 것만으로도, 256색 이미지가 16색 이미지의 두 배 크기인 이유가 설명된다.

import numpy as np
from collections import Counter
from math import log2

# A 16x16 patch with each of 16 grayscale levels appearing exactly 16 times.
# We'll build three layouts that share this histogram and compare entropies.
def gradient():
    img = np.zeros((16, 16), dtype=np.int32)
    for i in range(16):
        img[i, :] = i           # row i is constant level i
    return img

def blocks():
    img = np.zeros((16, 16), dtype=np.int32)
    for i in range(16):
        for j in range(16):
            img[i, j] = (i // 4) * 4 + (j // 4)
    return img

def scrambled(seed=0):
    rng = np.random.default_rng(seed)
    flat = gradient().flatten()
    rng.shuffle(flat)
    return flat.reshape(16, 16)

# Histogram entropy — bits/pixel if you encoded each pixel independently.
def histogram_entropy(img):
    flat = img.flatten()
    counts = Counter(flat)
    N = len(flat)
    return sum(-(c / N) * log2(c / N) for c in counts.values() if c > 0)

[histogram_entropy(p()) for p in (gradient, blocks, scrambled)]
# → [4.0, 4.0, 4.0]   identical — same histogram, same H.
3

히스토그램이 못 보는 것

위젯에서 그라디언트, 블록, 뒤섞임을 번갈아 눌러보자. 픽셀의 다중집합은 정확히 같다 — 단계마다 16개씩, 똑같이. 히스토그램 엔트로피도 세 패턴 모두 4.04.0 bits/픽셀. 완전히 같다.

그런데 그림은 같지 않고, 압축 결과도 같지 않다. 그라디언트는 매끈하다 — 인접한 두 픽셀이 같거나 1만큼만 차이 난다. 뒤섞인 패치는 이웃 픽셀끼리 어긋난다 — 옆에 붙은 단계가 사실상 독립이다. 같은 히스토그램, 그러나 _공간 구조_는 전혀 다르다. 그리고 실제 압축은 거의 다 이 구조에서 일어난다.

# Neighbor-difference entropy — what a real compressor sees.
# For each pair of adjacent pixels (right + down), count abs(diff). The
# resulting distribution carries the spatial structure the histogram missed.
def neighbor_diff_entropy(img):
    diffs = []
    H, W = img.shape
    for i in range(H):
        for j in range(W):
            if j + 1 < W: diffs.append(abs(int(img[i, j]) - int(img[i, j+1])))
            if i + 1 < H: diffs.append(abs(int(img[i, j]) - int(img[i+1, j])))
    counts = Counter(diffs)
    N = len(diffs)
    return sum(-(c / N) * log2(c / N) for c in counts.values() if c > 0)

[neighbor_diff_entropy(p()) for p in (gradient, blocks, scrambled)]
# → [0.34, 1.34, 4.07]   gradient ~0, scrambled ~log2(16) ≈ 4.
# Histograms saw three identical sources; neighbor differences see three
# very different ones. That gap is the entire point of spatial compression.
4

이웃 차이 — 다른 알파벳의 엔트로피

요령은 이렇다. 각 픽셀을 0..15 값 그 자체로 인코딩하는 대신, 왼쪽이나 위쪽 이웃과의 _차이_로 인코딩한다. 첫 픽셀은 여전히 비트를 다 써야 하지만, 이후 픽셀은 대부분 작은 수 — 보통은 0이다. 이제 이 차이의 분포에 H=Σplog2pH = -Σ p \log₂ p를 적용한다. 그라디언트라면: 차이가 거의 0 아니면 1이고, 엔트로피는 1 bit/픽셀에 한참 못 미친다. 뒤섞임이라면: 차이가 전체 범위에 고르게 퍼져 있어 엔트로피가 다시 4 가까이로 올라간다. 위젯에 두 값이 나란히 뜬다. 그 비율이 곧 구조를 보는 압축기와 히스토그램만 보는 압축기 사이의 격차다.

이 페이지를 한 문장으로 줄이면 이렇다. 어떤 픽셀이 등장하는지 보고, 이웃 차이는 그 픽셀들이 어떻게 나란히 놓였는지 본다. 실제 압축기는 (PNG, 원시 픽셀 배열에 대한 gzip) 두 번째 관점으로 일한다.

5

무손실 vs 손실 — 히스토그램 한계 밖으로 나가는 두 길

히스토그램 엔트로피는 압축의 단단한 바닥이다 — 어떤 방식으로도 평균적으로 원천의 엔트로피보다 적은 비트로 인코딩할 수는 없다. 하지만 이 바닥을 우회하는 길이 두 갈래 있다. 첫째, 알파벳을 바꾼다. 원시 픽셀 대신 이웃과의 차이를 인코딩한다. 픽셀 하나 대신 두 개를 묶어 인코딩한다. 8×8 블록을 변환해서 인코딩한다 (JPEG의 DCT, JPEG-2000의 웨이블릿). 새 알파벳은 엔트로피가 더 낮다 — 실제 이미지는 심볼끼리 독립이 아니니까. PNG와 FLAC가 이쪽이다.

둘째, 정확함을 포기한다. 압축은 사람이 알아채지 못할 부분 — 미세한 색 차이, 고주파 질감, 밀리초 이하의 소리 — 을 골라서 버린다. 압축을 풀어 얻은 파일은 원본이 아니라 지각적 근사다. JPEG, MP3, H.264가 이쪽이다. 이쪽은 엔트로피에 묶이지 않는다 — 품질을 어디까지 내줄 의향이 있느냐, 그것에만 묶인다.

# Real-world check: encode all three with PNG and compare file sizes.
# PNG uses filter prediction (essentially neighbor differences) followed by
# DEFLATE (LZ77 + Huffman). Same pixel multiset, very different output.
import io
from PIL import Image

def png_size(img):
    buf = io.BytesIO()
    Image.fromarray((img * 16).astype('uint8'), mode='L').save(buf, 'PNG')
    return len(buf.getvalue())

sizes = {p.__name__: png_size(p()) for p in (gradient, blocks, scrambled)}
# → {'gradient': ~120, 'blocks': ~140, 'scrambled': ~310}
#
# The histograms predicted 4 bits × 256 pixels = 128 bits = 16 bytes of
# *symbol payload*; PNG's framing overhead is fixed. The interesting number
# is the ratio of payloads: gradient ≈ 1×, scrambled ≈ 2-3× larger. Same
# histogram, different file size — the signature of spatial compression.
이제 깨봐

히스토그램 엔트로피는 심볼끼리 독립이라고 가정한 인코딩의 하한이지, 실제 코덱이 도달하는 값의 상한은 아니다. 그라디언트 패치를 PNG로 저장하면 4 bits/픽셀 × 256 픽셀 = 128바이트보다 훨씬 작아진다 — PNG는 LZ77도 함께 쓰기 때문이고, LZ77은 이웃 차이 엔트로피가 아예 포착하지 못하는 똑같은 리터럴이 반복되는 구간을 찾는다. 히스토그램은 4 비트라 말하고, 이웃 차이는 ~0.4라고 말하고, LZ77까지 얹은 PNG는 사실상 ~0.1이라 말한다. 한 단계 내려갈 때마다, 그림이 실제로 무엇인가에 대한 더 풍부한 모델이 생긴다. 같은 이미지가 어떤 질문을 던지느냐에 따라 더 압축되기도 하고 덜 압축되기도 한다.

파일의 크기는 픽셀 수가 아니라 정보량에 묶인다. 히스토그램은 어떤 값이 나오는지를 보고, 이웃 차이는 구조를 보고, 변환 코덱은 주파수 성분을 본다. 관점을 바꿀 때마다 정보 예산이 더 작아진다 — 그리고 그것이 곧 인코더가 쓸 수 있는 예산이다.

exercises · 손으로 풀기
1손으로 계산 · 히스토그램 엔트로피계산기 없이

4색 이미지에 픽셀 100개: 빨강 50, 초록 25, 파랑 15, 노랑 10. 픽셀당 비트로 히스토그램 엔트로피를 계산하라 (대략값: log2(2)=1\log₂(2) = 1, log2(4)2\log₂(4) ≈ 2, log2(6.67)2.74\log₂(6.67) ≈ 2.74, log2(10)3.32\log₂(10) ≈ 3.32).

2뒤섞인 게 왜 더 어려운가

100×100 이미지 두 장. 둘 다 정확히 같은 픽셀 값 10,000개를 담고 있다: 검은 픽셀 5,000, 흰 픽셀 5,000. 이미지 A는 깔끔한 체스판 패턴, 이미지 B는 같은 픽셀들을 무작위로 흩뿌린 것. 히스토그램 엔트로피는 동일하다 (H=1H = 1 bit/픽셀, p(검) = p(흰) = 0.5). PNG로 저장하면 어느 쪽이 더 작은가, 이유는? 대략적인 비율을 예측하라.

3사악한 것 · 히스토그램은 눈이 멀었다

위젯에서 그라디언트뒤섞임을 번갈아 눌러보자. 둘 다 Hhistogram=4.00H_histogram = 4.00 bits/픽셀로 표시된다. 그러면 순진한 엔지니어는 이렇게 결론짓는다. “두 이미지 모두 256 × 4 = 1024 비트 = 128바이트 페이로드에 PNG 오버헤드를 얹으면 끝.” arc 4의 코드가 실제 PNG 크기를 잰다. 히스토그램이 같다는데, 뒤섞인 파일은 거의 두 배 크기로 나오는가?

4손실은 다른 게임

사진의 JPEG는 같은 사진을 무손실로 인코딩할 때의 히스토그램 한계보다 작아지는 일이 흔하다. 어떻게 가능한가? 한 문장으로, 엔트로피가 무엇을 묶고 무엇은 묶지 못하는지 가르라.

왜 교과서는 이렇게 안 가르치나

정보이론 강의는 엔트로피를 i.i.d. 원천 위에서 정의하고 거기서 멈춘다 — 픽셀, 오디오 샘플, 언어 토큰을 모두 독립적인 추출인 듯 다룬다. 실제 신호는 한 번도 그런 적이 없다. 심볼 엔트로피맥락 엔트로피 (실제 코덱이 써먹는 것) 사이의 격차, 그것이 4MB 사진이 눈에 띄는 손실 없이 400KB로 줄어드는 이유 전부다. “압축”이 하나의 수가 아니라 사다리인 이유도 거기 있다. Lemma는 같은 위젯에 히스토그램 관점과 이웃 차이 관점을 함께 두어, 그 격차를 처음부터 눈에 보이게 했다 — 같은 히스토그램, 같은 H, 그러나 그림은 셋, 파일 크기도 셋. 엔트로피 모듈이 한계에 이름을 붙이고, 이 페이지는 그 한계가 놓치는 것을 보여준다.

용어집 · 이 페이지에서 쓰임 · 5
distribution·분포
_불확실성의 모양_. 가능한 결과들에 확률이 어떻게 배분돼 있는가. 이산 분포는 각 결과에 수를 매긴다 — `P(X = "고양이") = 0.6, P(X = "강아지") = 0.3, P(X = "새") = 0.1`. 연속 분포는 구간 위에 *밀도*를 배분한다 — 정확히 한 점에 확률은 없고, 구간 위에서만 있다. 이산은 합이, 연속은 적분이 1이어야 한다 — _무엇인가는 일어나야_ 하니까. 확률 하나는 수 한 개이고, 분포는 그 뒤에 있는 모양 전체다. 모델이 예측하는 양, 자산이 낼 수 있는 수익, 픽셀이 가질 수 있는 값 — 대부분은 단일 수가 아니라 분포다. 그리고 그 분포의 *퍼짐*이 종종 중심값보다 더 중요하다.
bit (information)·비트 (정보)
로그 밑이 2일 때의 정보 단위. 동전 던지기는 정확히 1 bit — 같은 확률의 두 결과를 가리려면 예/아니오 질문 1번이 필요해서. "이진 숫자"로서의 bit와는 다른 개념: 이진 숫자 한 자리는 1 bit를 _담을 수 있지만_, 90% 앞면이 나오는 편향 동전을 저장하는 이진 숫자 한 자리는 평균 1 bit *미만*의 정보만 담는다. 정보의 bit와 저장의 bit는 같은 단어를 쓰는 다른 것.
histogram·히스토그램
데이터에서 각 *값*이 얼마나 자주 등장하는지 센 것. 이미지의 경우: 이 정도 어두운 픽셀이 몇 개, 저 정도 밝은 픽셀이 몇 개. 그 픽셀들이 그림 안 _어디에_ 있는지는 묻지 않는다. 히스토그램은 일부러 공간 구조를 버린다 — "밝기 분포는 어떤가?"에는 답하지만, "그림이 매끈한가 거친가?"에는 답하지 않는다. 히스토그램 위에서 *엔트로피*를 계산하면, 각 값을 _독립적으로_ 인코딩할 때 필요한 심볼당 비트 수의 하한이 나온다. 실제 이미지에서 이 하한은 거의 항상 *느슨한 하한*이다 — 실제 픽셀은 이웃 픽셀과 독립이 아니니까.
lossless·무손실
_아무것도 버리지 않는_ 압축 방식: 압축을 풀면 원본 비트가 그대로 돌아온다. PNG, ZIP, FLAC이 무손실이다. 파일이 작아진 이유는 인코더가 *같은 정보*를 더 짧게 표현하는 방법을 찾아냈기 때문이지, 정보를 버려서가 아니다. 섀넌 한계가 바닥선 — 어떤 무손실 방법도, 아무리 영리해도, 원천의 엔트로피를 넘지 못한다. *손실*과 정반대 — 손실 압축은 크기를 위해 충실도를 거래한다.
lossy·손실
일부러 _정보를 버리는_ 압축 방식. 사람이 잘 못 알아채는 것 — 미세한 색상 차이, 고주파 질감, 밀리초 이하의 음향 디테일 — 부터 골라서 버린다. JPEG, MP3, H.264가 손실 압축이다. 압축을 풀면 원본 비트가 _나오지 않는다_ — 훨씬 작은 크기에 비슷해 보이게 (또는 비슷하게 들리게) 고른 근사치가 나온다. 손실 압축은 원천 엔트로피에 매이지 않는다 — 인코더가 어느 정도의 지각적 품질을 희생할 의향이 있는지에만 매인다. *무손실*의 반대.