구글은 왜 검색 결과를 그 순서로 보여줄까?
30년 동안 그 판정 규칙은 거의 그대로다.
희귀함이 신호다. 나머지는 따라온다. 단어 빈도는 어휘 위의
빈도로 세면, 길이로 순위가 매겨진다
로 검색한다. 모든 문서에 가 있다. 가장 긴 문서에 가장 많이. 빈도만 세면 결국 단어 수가 많은 문서가 이긴다 — 관련성이 아니라 길이가 순위를 정한다. 어떤 단어가 의미를 짊어지는지를 아는 점수가 필요하다.
TF — 빈도, 길이로 나눈
# 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.IDF — 희귀함이 신호다
(모든 문서에 등장): 비트. Stopword. 기여도는 0.
(한 문서에만 등장): 비트. 최대 신호. 그 단어 하나가 그 문서를 콕 집어낸다.
위 위젯의 4-문서 코퍼스를 보자. 는 4개 중 3개에 등장하니 IDF는 비트 — 거의 0이다. 는 4개 중 1개에만 등장하니 IDF는 비트. 쿼리 안에서 “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.왜 log? — 엔트로피의 그 log
log는 곡선을 매끄럽게 만드는 트릭이 아니다. 을 “아무 문서나 하나 골랐을 때 단어 가 들어 있을 확률”로 보면,
idf(t) = log₂(N / df(t)) = −log₂ P(t) =
이건 엔트로피 모듈의 자기정보 그 자체다. 아무 문서를 펼쳤는데 그 단어가 들어 있었다면 느낄
이 프레임은 휴리스틱이다. “아무 문서”가 코퍼스 위에서 균등 분포라고 가정하고, 등장 여부도 0/1로만 본다. 실제 코퍼스는 훨씬 지저분하다. 그래도 이 프레임이 주는 직관은 엄밀하고, 그래서 log는 자리를 비키지 않는다. 희귀함은 빈도를 로그적으로 따돌린다 — 매번.
왜 곱하기 (tf × idf) ? — 로그-확률에서 빌려오기
독립 사건의 확률은 곱셈으로 결합하고, 그 log는 덧셈으로 결합한다. 문서가 각 쿼리 단어에 대해 독립 확률을 가진다면, 결합 확률의 log는 깔끔한 합 가 된다. TF-IDF는 바로 이 모양을 흉내 낸다.
score(q, d) = Σt ∈ q tf(t, d) · idf(t)
그러나 흉내는 진짜가 아니다. 는 빈도지 확률이 아니다. 그걸 와 곱한다고 해서 결합 확률의 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]벡터의 관점 — 코사인, 유클리드 아님
어휘의 모든 단어에 열을 하나씩 잡아 둔다. 각 문서는 TF-IDF 가중치의
왜 유클리드 거리가 아니라
왜 이게 패러다임이 됐고, 그 다음엔 무엇이 왔는가
TF-IDF는 세 경주에서 동시에 이겼다. 빨랐고 (희소 벡터, 역색인), 잘 확장됐고 (웹 규모 코퍼스가 평범한 하드웨어에 올라갔고), 해석이 됐다 (모든 점수에 단어별 내역이 붙는다 — 위젯이 보여 주는 바로 그것이다). 순위 자체도 부끄럽지 않았다. 강한 가정 하나 (단어 독립) 와 확률을 닮은 직관 위에 서 있어서, 어떻게 틀리는지가 늘 익숙한 모양이었다.
ML로 건너가는 다리이기도 하다. TF-IDF는 텍스트의 첫 번째 특징 벡터였다. 나이브 베이즈, 텍스트 위의 로지스틱 회귀, 잠재 의미 분석 — 모두 이 표현 위에 서 있다. 요즘의 밀집 신경 검색기 (DPR, ColBERT, 듀얼 인코더 계열) 는 희소 TF-IDF 벡터를 밀집 임베딩으로 갈아 끼웠지만, 여전히 BM25를 기준선 삼아 비교한다. 실제 검색 과제에서 30년 된 기준선을 못 이기는 새 방법은 새 방법이 아니다. log는 아직 그 자리에 있다.
독립 가정은 가끔 우리를 속인다. TF-IDF 색인에 으로 쿼리를 던지면 “new”나 “york” 둘 중 하나라도 들어 있는 문서가 높은 점수로 올라온다 — 영국 York에서 일어난 새로운 일들 같은 문서까지 섞여서. TF-IDF는 “new york”이 한 덩어리라는 걸 모른다. 구절 매칭, n-그램, 그리고 (결국) 밀집 임베딩이 검색엔진들이 이 구멍을 메운 방법이다. 어느 것도 TF-IDF를 밀어내지 않았다. 모두 그 위에 얹혔을 뿐이다.
검색 점수는 비트의 합처럼 움직인다. 흔한 단어는 0에 가깝게, 희귀한 단어는 잔뜩. log는 트릭이 아니라 정보의 단위다. 하지만 tf × idf 자체가 비트의 합인 건 아니다. 로그-확률에서 모양만 빌려 쓴 휴리스틱이다. 그 빌려 쓴 모양이 30년간 웹을 돌렸고, 지금도 모든 신경 검색기가 넘어야 할 기준선을 정의한다.
위젯에서 를 원시 빈도 순위로 검색해 보자. 어느 문서가 이기고, 왜? 이제 TF-IDF로 바꿔 보자. 순위는 어떻게 바뀌고, IDF 패널은 에 대해 무어라 말하는가?
위젯의 4-문서 코퍼스와 쿼리 에 대해 (a) 각 문서의 , (b) , (c) 각 문서의 TF-IDF 점수를 구해 보자. 어느 문서가 이기는가? cat이 들어 있는 두 문서 중 긴 쪽인가, 짧은 쪽인가?
문서 1 (“the cat sat on the mat”) 의 사본 100개를 코퍼스에 더해 N이 4 → 104가 됐다고 하자. 쿼리 에 대해 (다시 계산하지 말고) 예측해 보자. (a) idf(cat) 은 어떻게 변하는가? (b) 문서 3 (“cat and dog are friends”) 의 점수는? (c) 문서 3과 (이제 한 무더기가 된) 문서 1 사본들의 순위는?
어떤 쿼리 단어가 코퍼스 안의 모든 문서에 등장한다고 하자. IDF를 구하고, 그 단어의 점수가 모든 문서에서 정확히 0인 이유를 설명해 보자. 그리고 이게 버그가 아니라 직관에 들어맞는 이유를 한 문장으로 써 보자.
정보검색 교과서는 보통 TF-IDF를 정의의 나열로 풀어 놓는다. tf, idf, 곱, 코사인. 곱은 한 문장으로 처리된다 (“공식이 그러니까 곱한다”). Lemma는 곱을 정직하게 다룬다. tf × idf는 로그 확률이 아니라 — 그 모양을 빌려 썼을 뿐이다. 이 구분을 가리는 것이 “왜 log지?”를 정보의 단위가 아니라 트릭처럼 느끼게 만든다.