Lemma
수학, 거꾸로
도입 · 빈도 → 비트 → 순위

구글은 왜 검색 결과를 그 순서로 보여줄까?

30년 동안 그 판정 규칙은 거의 그대로다. 는 확률의 합처럼 보이는 점수로 문서 순위를 매긴다 — 그 안의 log는 엔트로피 모듈을 관통하는 바로 그 log다. 희귀한 쿼리 단어는 를 많이 나르고, 흔한 단어는 거의 0비트다. 누가 stopword 목록을 따로 박아 넣지 않았는데도, 점수는 stopword를 알아서 무시하도록 자랐다.

희귀함이 신호다. 나머지는 따라온다. 단어 빈도는 어휘 위의 다.

위젯 — TF-IDF 요리기
쿼리예시:
코퍼스 N = 4 · idf = log₂(N / df)
단어dfidf (bits)cat21.00
#1cat and dog are friends길이= 5
점수0.200cat: 0.200 × 1.00 = 0.200
#2the cat sat on the mat길이= 6
점수0.167cat: 0.167 × 1.00 = 0.167
#3the dog ran fast길이= 4
점수0.000cat: 0.000 × 1.00 = 0.000
#4the quick brown fox길이= 4
점수0.000cat: 0.000 × 1.00 = 0.000
이걸 해보자. the원시 빈도로 검색 — 문서 1이 "the"를 두 번 써서 이긴다. TF-IDF로 바꾸면: 모든 문서의 점수가 거의 0으로 무너진다. "the"는 4개 문서 중 3개에 등장하므로 log₂(4/3) ≈ 0.42비트, 거의 신호가 없기 때문. 이번엔 fox: log₂(4/1) = 2비트, 그래서 fox를 가진 단 하나의 문서가 위로 솟는다. 희귀함이 신호다.
흐름
1

빈도로 세면, 길이로 순위가 매겨진다

"thecatsat""the cat sat"로 검색한다. 모든 문서에 thethe가 있다. 가장 긴 문서에 가장 많이. 빈도만 세면 결국 단어 수가 많은 문서가 이긴다 — 관련성이 아니라 길이가 순위를 정한다. 어떤 단어가 의미를 짊어지는지를 아는 점수가 필요하다.

2

TF — 빈도, 길이로 나눈

: tf(t,d)=count(t,d)/length(d)tf(t, d) = count(t, d) / length(d). 문서 dd에서 tt가 차지하는 비율이다. 길이로 나누지 않으면 긴 문서가 그저 단어를 더 많이 썼다는 이유로 매번 이긴다. 길이 정규화는 출발선을 맞추는 일 — 문서가 길든 짧든 단어 하나당 같은 무게의 증거가 된다.

# tf — count, normalized by doc length.
docs = [
    "the cat sat on the mat",
    "the dog ran fast",
    "cat and dog are friends",
    "the quick brown fox",
]
toks = [d.split() for d in docs]

def tf(term, doc):
    return doc.count(term) / len(doc)

# 'cat' in each doc — note doc 0 is longer, so its tf is smaller
# even though both contain 'cat' once.
[round(tf("cat", d), 3) for d in toks]
# → [0.167, 0.0, 0.2, 0.0]
#
# That length normalization is the whole reason: doc 2 "wins" between
# the two cat-containing docs because its sentence is shorter.
3

IDF — 희귀함이 신호다

: idf(t)=log2(N/df(t))idf(t) = \log₂(N / df(t)). NN은 코퍼스 크기, tt를 담은 문서의 수다. 양 극단을 따라가 보자.

  • df=Ndf = N (모든 문서에 등장): log2(1)=0\log₂(1) = 0비트. Stopword. 기여도는 0.

  • df=1df = 1 (한 문서에만 등장): log2(N)\log₂(N)비트. 최대 신호. 그 단어 하나가 그 문서를 콕 집어낸다.

위 위젯의 4-문서 코퍼스를 보자. "the""the"는 4개 중 3개에 등장하니 IDF는 log2(4/3)0.42\log₂(4/3) ≈ 0.42비트 — 거의 0이다. "fox""fox"는 4개 중 1개에만 등장하니 IDF는 log2(4/1)=2\log₂(4/1) = 2비트. 쿼리 안에서 “fox”는 “the”보다 5배 값어치를 한다 — 누가 따로 정해 준 적도 없는데.

from math import log2

N = len(toks)

def df(term):
    return sum(1 for d in toks if term in d)

def idf(term):
    d = df(term)
    return log2(N / d) if d else 0.0

[(t, df(t), round(idf(t), 2)) for t in ("the", "cat", "fox")]
# → [('the', 3, 0.42),    # 3 of 4 docs → barely any signal
#    ('cat', 2, 1.00),    # half the corpus → 1 bit
#    ('fox', 1, 2.00)]    # rarest → 2 bits
#
# A query word that lives in every doc gives 0 bits — the score
# learned to ignore stopwords without anyone hard-coding them.
4

왜 log? — 엔트로피의 그 log

log는 곡선을 매끄럽게 만드는 트릭이 아니다. df/Ndf/N을 “아무 문서나 하나 골랐을 때 단어 tt가 들어 있을 확률”로 보면,

idf(t) = log₂(N / df(t)) = −log₂ P(t) = of seeing t

이건 엔트로피 모듈의 자기정보 그 자체다. 아무 문서를 펼쳤는데 그 단어가 들어 있었다면 느낄 의 비트 수. 적은 문서에만 등장하는 “놀라운” 단어는 비트를 많이 나르고, 거의 모든 문서에 깔린 “지루한” 단어는 0비트에 가깝다. IDF는 옷만 갈아입은 자기정보다.

이 프레임은 휴리스틱이다. “아무 문서”가 코퍼스 위에서 균등 분포라고 가정하고, 등장 여부도 0/1로만 본다. 실제 코퍼스는 훨씬 지저분하다. 그래도 이 프레임이 주는 직관은 엄밀하고, 그래서 log는 자리를 비키지 않는다. 희귀함은 빈도를 로그적으로 따돌린다 — 매번.

5

왜 곱하기 (tf × idf) ? — 로그-확률에서 빌려오기

독립 사건의 확률은 곱셈으로 결합하고, 그 log는 덧셈으로 결합한다. 문서가 각 쿼리 단어에 대해 독립 확률을 가진다면, 결합 확률의 log는 깔끔한 합 ΣilogP(ti)Σᵢ −\log P(tᵢ)가 된다. TF-IDF는 바로 이 모양을 흉내 낸다.

score(q, d) = Σt ∈ q tf(t, d) · idf(t)

그러나 흉내는 진짜가 아니다. tftf는 빈도지 확률이 아니다. 그걸 idfidf와 곱한다고 해서 결합 확률의 log를 문자 그대로 취하는 건 아니다. TF-IDF는 “독립 확률들의 log는 더해진다”는 모양을 빌려 쓴 유용한 순위 점수일 뿐, 진짜 가능도가 아니다. 정보이론에서 영감을 받은 휴리스틱 — 딱 그만큼만 믿으면 된다.

# The score: tf · idf, summed over query terms.
def score(query, doc):
    return sum(tf(t, doc) * idf(t) for t in query.split())

ranking = sorted(
    range(N),
    key=lambda i: -score("the", toks[i]),  # try also "fox", "cat dog"
)

# Compare to the raw-count baseline.
def raw(query, doc):
    return sum(doc.count(t) for t in query.split())

# Query "the": raw count picks doc 0 (uses 'the' twice, longest doc).
# TF-IDF picks... barely anything — every score is near zero, because
# log2(4/3) ≈ 0.42 bits. The trap of "popular doc wins" is closed.
[round(raw("the", d), 3)   for d in toks]   # → [2, 1, 0, 1]
[round(score("the", d), 3) for d in toks]   # → [0.139, 0.104, 0, 0.104]
6

벡터의 관점 — 코사인, 유클리드 아님

어휘의 모든 단어에 열을 하나씩 잡아 둔다. 각 문서는 TF-IDF 가중치의 가 된다 — 대부분의 자리는 0, 그 문서가 실제로 쓰는 몇몇 단어 자리에서만 0이 아닌 값을 갖는다. (이 희소 벡터 표현이 다. 빈도는 살리고 순서는 버린다.) 쿼리도 같은 모양의 벡터다. 그러면 순위 매기기는 “쿼리 벡터에 가장 가까운 문서 벡터를 찾는 일”이 된다.

왜 유클리드 거리가 아니라 일까? 문서 길이가 점수를 흔들어선 안 되기 때문이다. 문서 dd와 자기 자신을 이어 붙인 d++dd ++ d를 보자. 뒤쪽은 모든 성분이 두 배다 — 방향은 그대로, 길이만 두 배인 벡터. 두 벡터 사이 각의 코사인은 11 (같은 방향). 유클리드 거리는 d\|d\| (0이 아니다). 문서를 두 번 이어 붙였다고 의미까지 두 배가 되진 않는다. 코사인은 그 직관에 동의하는 지표다.

7

왜 이게 패러다임이 됐고, 그 다음엔 무엇이 왔는가

TF-IDF는 세 경주에서 동시에 이겼다. 빨랐고 (희소 벡터, 역색인), 잘 확장됐고 (웹 규모 코퍼스가 평범한 하드웨어에 올라갔고), 해석이 됐다 (모든 점수에 단어별 내역이 붙는다 — 위젯이 보여 주는 바로 그것이다). 순위 자체도 부끄럽지 않았다. 강한 가정 하나 (단어 독립) 와 확률을 닮은 직관 위에 서 있어서, 어떻게 틀리는지가 늘 익숙한 모양이었다.

= TF-IDF + 경험적 보정 두 가지. (a) 포화: 다섯 번째 출현이 첫 번째만큼 무거우면 곤란하다. tftf(k1+1)tf/(k1+tf)(k₁+1)·tf / (k₁+tf)로 바꿔 k11.2k₁ ≈ 1.2 근처에서 포화시킨다. (b) 길이 정규화: 평균보다 긴 문서는 tf를 한 번 더 깎는다. 둘 다 1990년대 TREC 평가에서 튜닝된 채 그대로 살아남았다.

ML로 건너가는 다리이기도 하다. TF-IDF는 텍스트의 첫 번째 특징 벡터였다. 나이브 베이즈, 텍스트 위의 로지스틱 회귀, 잠재 의미 분석 — 모두 이 표현 위에 서 있다. 요즘의 밀집 신경 검색기 (DPR, ColBERT, 듀얼 인코더 계열) 는 희소 TF-IDF 벡터를 밀집 임베딩으로 갈아 끼웠지만, 여전히 BM25를 기준선 삼아 비교한다. 실제 검색 과제에서 30년 된 기준선을 못 이기는 새 방법은 새 방법이 아니다. log는 아직 그 자리에 있다.

이제 깨봐

독립 가정은 가끔 우리를 속인다. TF-IDF 색인에 "newyork""new york"으로 쿼리를 던지면 “new”나 “york” 둘 중 하나라도 들어 있는 문서가 높은 점수로 올라온다 — 영국 York에서 일어난 새로운 일들 같은 문서까지 섞여서. TF-IDF는 “new york”이 한 덩어리라는 걸 모른다. 구절 매칭, n-그램, 그리고 (결국) 밀집 임베딩이 검색엔진들이 이 구멍을 메운 방법이다. 어느 것도 TF-IDF를 밀어내지 않았다. 모두 그 위에 얹혔을 뿐이다.

검색 점수는 비트의 합처럼 움직인다. 흔한 단어는 0에 가깝게, 희귀한 단어는 잔뜩. log는 트릭이 아니라 정보의 단위다. 하지만 tf × idf 자체가 비트의 합인 건 아니다. 로그-확률에서 모양만 빌려 쓴 휴리스틱이다. 그 빌려 쓴 모양이 30년간 웹을 돌렸고, 지금도 모든 신경 검색기가 넘어야 할 기준선을 정의한다.

exercises · 손으로 풀기
1위젯 읽기

위젯에서 thethe원시 빈도 순위로 검색해 보자. 어느 문서가 이기고, 왜? 이제 TF-IDF로 바꿔 보자. 순위는 어떻게 바뀌고, IDF 패널은 "the""the"에 대해 무어라 말하는가?

2손으로 계산 · TF-IDF계산기 없이

위젯의 4-문서 코퍼스와 쿼리 catcat에 대해 (a) 각 문서의 tf(cat,d)tf(cat, d), (b) idf(cat)idf(cat), (c) 각 문서의 TF-IDF 점수를 구해 보자. 어느 문서가 이기는가? cat이 들어 있는 두 문서 중 긴 쪽인가, 짧은 쪽인가?

3예측 · 사본 추가

문서 1 (“the cat sat on the mat”) 의 사본 100개를 코퍼스에 더해 N이 4 → 104가 됐다고 하자. 쿼리 catcat에 대해 (다시 계산하지 말고) 예측해 보자. (a) idf(cat) 은 어떻게 변하는가? (b) 문서 3 (“cat and dog are friends”) 의 점수는? (c) 문서 3과 (이제 한 무더기가 된) 문서 1 사본들의 순위는?

4사악한 것 · 신호 0

어떤 쿼리 단어가 코퍼스 안의 모든 문서에 등장한다고 하자. IDF를 구하고, 그 단어의 점수가 모든 문서에서 정확히 0인 이유를 설명해 보자. 그리고 이게 버그가 아니라 직관에 들어맞는 이유를 한 문장으로 써 보자.

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

정보검색 교과서는 보통 TF-IDF를 정의의 나열로 풀어 놓는다. tf, idf, 곱, 코사인. 곱은 한 문장으로 처리된다 (“공식이 그러니까 곱한다”). Lemma는 곱을 정직하게 다룬다. tf × idf는 로그 확률이 아니라 — 그 모양을 빌려 썼을 뿐이다. 이 구분을 가리는 것이 “왜 log지?”를 정보의 단위가 아니라 트릭처럼 느끼게 만든다.

용어집 · 이 페이지에서 쓰임 · 11
TF-IDF·TF-IDF
`tf(단어, 문서) · idf(단어, 코퍼스)`를 쿼리의 각 단어에 대해 합한 문서 점수. tf는 문서가 그 단어를 많이 쓸수록 점수를 더하고, idf는 여러 문서에 흔히 나타나는 단어를 깎는다. 이 곱은 "결합 확률의 로그" 직관을 *흉내*낸다 — 하지만 흉내일 뿐: tf는 빈도지 확률이 아니라서 TF-IDF는 정보이론에서 영감을 받은 휴리스틱이지 진짜 확률모델은 아니다. 20년 동안 검색 순위 매기기의 지배적 점수였고, 지금도 모든 신경 검색기 (BM25 = TF-IDF + 포화 + 길이 정규화) 가 넘어야 할 베이스라인.
bit (information)·비트 (정보)
로그 밑이 2일 때의 정보 단위. 동전 던지기는 정확히 1 bit — 같은 확률의 두 결과를 가리려면 예/아니오 질문 1번이 필요해서. "이진 숫자"로서의 bit와는 다른 개념: 이진 숫자 한 자리는 1 bit를 _담을 수 있지만_, 90% 앞면이 나오는 편향 동전을 저장하는 이진 숫자 한 자리는 평균 1 bit *미만*의 정보만 담는다. 정보의 bit와 저장의 bit는 같은 단어를 쓰는 다른 것.
distribution·분포
_불확실성의 모양_. 가능한 결과들에 확률이 어떻게 배분돼 있는가. 이산 분포는 각 결과에 수를 매긴다 — `P(X = "고양이") = 0.6, P(X = "강아지") = 0.3, P(X = "새") = 0.1`. 연속 분포는 구간 위에 *밀도*를 배분한다 — 정확히 한 점에 확률은 없고, 구간 위에서만 있다. 이산은 합이, 연속은 적분이 1이어야 한다 — _무엇인가는 일어나야_ 하니까. 확률 하나는 수 한 개이고, 분포는 그 뒤에 있는 모양 전체다. 모델이 예측하는 양, 자산이 낼 수 있는 수익, 픽셀이 가질 수 있는 값 — 대부분은 단일 수가 아니라 분포다. 그리고 그 분포의 *퍼짐*이 종종 중심값보다 더 중요하다.
term frequency·단어 빈도 (TF)
`tf(t, d) = count(t, d) / length(d)`. 문서 `d`에서 단어들 중 `t`인 것의 비율. 문서 길이로 나누는 건 "공정하게 만드는" 단계다 — 안 그러면 긴 문서가 모든 단어를 더 많이 썼다는 이유로 점수를 더 받는다. 변형 (원시 빈도, 로그 스케일, 증강 형) 도 있지만, 정규화된 형태가 표준 직관이다.
inverse document frequency·역문서빈도 (IDF)
`idf(t) = log(N / df(t))`, 여기서 `N`은 코퍼스 크기, `df(t)`는 단어 `t`를 포함하는 문서 수. 모든 문서에 있는 단어는 `log(1) = 0` — 신호 없음. 한 문서에만 있는 단어는 `log(N)` — 최대 신호. log는 튜닝 손잡이가 아니다: `df/N`을 "무작위 문서가 이 단어를 포함할 확률"로 보면, IDF는 엔트로피 모듈의 자기정보 `−log P(t)` 그 자체 — 단어가 나르는 놀람도의 비트 수. 확률 프레임은 휴리스틱이지만 (균등 무작위 문서·이진 존재 가정), 직관은 엄밀하다: 희귀함이 곧 신호다.
document frequency·문서빈도 (df)
`df(t)` = 코퍼스에서 단어 `t`를 한 번이라도 포함하는 문서의 수. 문서당 이진 존재 (단어가 열 번 나와도 여전히 1로 셈). _집합 빈도_ (코퍼스 전체에서의 총 출현 수) 와는 다르다. IDF는 집합 빈도가 아니라 `df`로 만든다 — "있느냐 없느냐"가 쿼리가 신경 쓰는 신호이기 때문.
surprise·놀람도
`I(x) = −log₂ p(x)`, 결과 `x`가 일어났음을 알게 됐을 때 얻는 정보의 비트 수. 드문 결과(작은 p)는 큰 놀람도를 나르고, 확실한 결과(p = 1)는 0을 나른다. 엔트로피는 _기대_ 놀람도 — `H = E[I(X)] = Σ p log₂(1/p)`. 모스 부호의 직관이 여기 산다: 영어에서 'E'가 가장 자주 나오므로 등장당 놀람도가 작고, 그래서 짧은 부호를 받는다.
vector·벡터
크기와 방향을 모두 가진 양. 구체적으로는 수의 튜플 — 2D에서 `(3, 4)`, 3D에서 `(1, 0, −2)` — 이지만, 그 수가 *무엇을 의미하는가*는 무엇을 하느냐에 따라 다르다. 그래픽에서는 제어점의 오프셋, 물리에서는 속도나 힘, ML에서는 매개변수 갱신이나 특징 표현. 튜플은 _같다_. *역할*이 다르다. 산술 — 성분별 덧셈, 수로의 스칼라배 — 은 모든 역할에서 _같기_ 때문에, 한 벌의 수학이 모두를 받친다.
bag of words·bag of words (BoW)
문서를 토큰의 다중집합으로 보는 표현 — 빈도만 세고 순서는 버린다. "the cat sat"과 "sat the cat"은 같은 가방을 만든다. 거칠지만 (구문, 부정, 지시 관계를 잃는다), TF-IDF가 푸는 문제 — 어떤 단어를 포함하느냐로 문서 순위를 매기기 — 에는 딱 맞는 추상도. 텍스트의 모든 단어-세기 특징 (TF-IDF, 텍스트 나이브 베이즈, LDA) 이 이 표현 위에 얹힌다.
cosine similarity·코사인 유사도
`cos(u, v) = (u · v) / (‖u‖ ‖v‖)`. 두 벡터 사이 각도의 코사인 — `[−1, 1]` 범위 (TF-IDF처럼 성분이 모두 비음수일 때는 `[0, 1]`). *방향*을 재고 *크기*는 안 잰다: 문서를 두 배로 늘려도 (자기 자신과 이어 붙여도) 코사인은 그대로지만 유클리드 거리는 폭발한다. 이 길이 불변성 덕에 검색은 원시 내적이나 유클리드가 아니라 코사인으로 순위를 매긴다.
BM25·BM25
`Okapi BM25` — TF-IDF에 경험적 보정 두 개를 더한 것. (1) **포화:** 원시 `tf`는 선형으로 자라지만, 한 문서에서 단어가 다섯 번째 나오는 것이 첫 번째만큼 중요하진 않다. BM25는 `tf`를 `(k₁+1)·tf / (k₁+tf)`로 바꿔 `k₁ ≈ 1.2` 근처에서 포화시킨다. (2) **길이 정규화:** 평균보다 긴 문서는 `tf`를 더 깎는다. 두 보정 모두 1990년대 검색 평가 (TREC) 에서 튜닝되어 살아남았고, 지금도 BM25는 신경 검색기가 넘어야 할 희소 검색의 기본 베이스라인이다.