자연어 처리(NLP)에서 TF-IDF는 단어의 중요도를 평가하는 대표적인 기법입니다.
그런데, 여기에는 **로그(logarithm)**라는 수학 개념이 핵심적으로 쓰입니다.
이번 글에서는 TF-IDF와 로그의 원리, 그리고 왜 로그를 쓰는지까지 자세히 살펴보겠습니다.
1. TF-IDF란?
TF-IDF는 Term Frequency–Inverse Document Frequency의 약자입니다.
- TF (Term Frequency): 특정 문서에서 단어가 얼마나 자주 등장하는지
- IDF (Inverse Document Frequency): 전체 문서 집합에서 해당 단어가 얼마나 희소한지(드문지)
수식
TF-IDF(t,d)=TF(t,d)×log(Ndf(t))\text{TF-IDF}(t, d) = \text{TF}(t, d) \times \log\left(\frac{N}{df(t)}\right)TF-IDF(t,d)=TF(t,d)×log(df(t)N)
- ttt: 단어
- ddd: 문서
- NNN: 전체 문서 수
- df(t)df(t)df(t): 단어 ttt가 등장한 문서 수
2. 왜 IDF에 로그를 쓰나?
IDF는 단어의 희소성을 의미합니다.
- 단어가 거의 모든 문서에 등장 → IDF 작음 → 중요도 낮음
- 단어가 소수 문서에만 등장 → IDF 큼 → 중요도 높음
그런데, 로그를 쓰지 않으면 단어의 등장 문서 수에 따라 값이 기하급수적으로 변동합니다.
이러면 극단값이 너무 커져서 모델 학습에 불안정성을 줄 수 있습니다.
로그의 효과
- 스케일 안정화
1000, 100, 10 같은 큰 차이를 6.9, 4.6, 2.3처럼 부드럽게 줄여줍니다. - 계산 편리성
곱셈을 덧셈으로, 나눗셈을 뺄셈으로 바꿔줍니다. - 지수적 증가를 선형화
데이터의 분포를 다루기 쉽게 만들어줍니다.
3. 로그의 정의와 성질
로그는 다음과 같이 정의됩니다. logb(x)=y⟺by=x\log_b(x) = y \quad \Longleftrightarrow \quad b^y = xlogb(x)=y⟺by=x
성질
- 곱셈 → 덧셈: logb(xy)=logb(x)+logb(y)\log_b(xy) = \log_b(x) + \log_b(y)logb(xy)=logb(x)+logb(y)
- 나눗셈 → 뺄셈: logb(x/y)=logb(x)−logb(y)\log_b(x/y) = \log_b(x) – \log_b(y)logb(x/y)=logb(x)−logb(y)
- 거듭제곱 → 곱셈: logb(xr)=rlogb(x)\log_b(x^r) = r \log_b(x)logb(xr)=rlogb(x)
4. TF-IDF에서의 로그 적용 예시
단어 | 전체 문서 수(N=1000) | 등장 문서 수(df) | IDF(로그X) | IDF(로그O) |
---|---|---|---|---|
the | 1000 | 990 | 1.01 | 0.01 |
earthquake | 1000 | 10 | 100 | 4.6 |
rareword | 1000 | 1 | 1000 | 6.9 |
→ 로그를 씌우면 값의 순위는 유지하면서 편차를 완만하게 조정합니다.
5. 시각적 비교
- 빨간선: 로그 없이 계산 → 급격한 증가
- 파란선: 로그 적용 → 완만한 곡선
- 초록선: 로그 + 1 보정 → 0 이하 방지
6. 결론
- 로그는 TF-IDF에서 필수
→ 극단값 억제, 계산 안정성, 해석 용이성 - TF-IDF 점수는 절대값이 아니라 상대적인 중요도를 의미
- “문서 내부에서 자주 나오고, 다른 문서에는 드문” 단어일수록 TF-IDF 값이 높음
💡 팁:
TF-IDF는 단어 중요도를 측정하는 데 유용하지만, 의미(semantic)를 반영하지는 못합니다.
더 깊은 의미 분석이 필요하다면 Word2Vec, FastText, BERT 같은 임베딩 기법과 함께 사용하는 것이 좋습니다.
–– coding: utf-8 ––
“””
TF-IDF & Log(IDF) Visualization – paste & run
- 수작업 BoW/IDF/TF-IDF 계산
- sklearn(CountVectorizer, TfidfTransformer) 비교
- 로그 적용 전/후 IDF 곡선 시각화
“””
import math
import numpy as np
import pandas as pd
import re
시각화
import matplotlib.pyplot as plt
sklearn 벡터화
from sklearn.feature_extraction.text import CountVectorizer, TfidfTransformer
———————————————————
1) 샘플 문서
———————————————————
docs = [
“fire fire fire rescue help”,
“earthquake fire help”,
“the help is here”,
“wildfire responders and earthquake rescue teams”,
“flood flood rescue operation started”
]
print(“총 문서 수:”, len(docs))
print(pd.Series(docs, name=”docs”))
———————————————————
2) 간단 전처리 & 토큰화 (데모용)
———————————————————
def tokenize(s: str):
tokens = re.sub(r”[^\w]”, ” “, s.lower()).split()
return [t for t in tokens if len(t) > 1] # 길이 1 토큰 제거
tokens_list = [tokenize(d) for d in docs]
———————————————————
3) 수작업 BoW / 단어장
———————————————————
vocab = sorted(set([t for ts in tokens_list for t in ts]))
word2idx = {w:i for i, w in enumerate(vocab)}
bag = np.zeros((len(docs), len(vocab)), dtype=float)
for i, ts in enumerate(tokens_list):
for t in ts:
bag[i, word2idx[t]] += 1.0
bag_df = pd.DataFrame(bag, columns=vocab)
print(“\n[수작업] BoW (count) 상위 일부:\n”, bag_df.iloc[:3, :].head())
———————————————————
4) 수작업 IDF / TF-IDF (log 전/후 비교)
———————————————————
N = len(docs)
df_count = (bag > 0).sum(axis=0) # 각 단어가 등장한 문서 수
df_count = np.maximum(df_count, 1) # 0분모 방지
idf_raw = N / df_count # 로그 없이
idf_log = np.log(N / df_count) # 자연로그
idf_log_plus1 = np.log(N / df_count) + 1 # 일부 구현에서 +1 보정
tfidf_raw = bag * idf_raw
tfidf_log = bag * idf_log
tfidf_log_p1 = bag * idf_log_plus1
print(“\n[수작업] df(문서수):\n”, pd.Series(df_count, index=vocab))
print(“\n[수작업] IDF(raw/log/log+1) 상위 몇 개:\n”,
pd.DataFrame({“idf_raw”: idf_raw, “idf_log”: idf_log, “idf_log+1”: idf_log_plus1},
index=vocab).head())
———————————————————
5) 시각화: 로그 적용 전/후 IDF 곡선
———————————————————
df_vals = np.arange(1, N+1) # 가능한 df(문서 등장 수) 범위
idf_no_log_curve = N / df_vals
idf_log_curve = np.log(N / df_vals)
idf_log_p1_curve = np.log(N / df_vals) + 1
plt.figure(figsize=(8, 5))
plt.plot(df_vals, idf_no_log_curve, label=”IDF (no log)”)
plt.plot(df_vals, idf_log_curve, label=”IDF (log)”)
plt.plot(df_vals, idf_log_p1_curve, label=”IDF (log + 1)”, linestyle=”–“)
plt.xlabel(“등장 문서 수 df(t)”)
plt.ylabel(“IDF 값”)
plt.title(“로그 적용에 따른 IDF 스케일 변화”)
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()
———————————————————
6) 수작업 vs sklearn 비교
———————————————————
cv = CountVectorizer(tokenizer=tokenize, preprocessor=lambda x: x, lowercase=False)
X_counts = cv.fit_transform(docs) # (num_docs, vocab_size)
sk_vocab = cv.get_feature_names_out()
tt = TfidfTransformer()
X_tfidf = tt.fit_transform(X_counts)
print(“\n[sklearn] vocab:”, list(sk_vocab))
print(“[sklearn] counts shape:”, X_counts.shape, ” tfidf shape:”, X_tfidf.shape)
수작업과 sklearn의 단어장 정렬 맞추기
(CountVectorizer가 만든 순서대로 수작업 행렬 재정렬)
align_idx = [vocab.index(w) for w in sk_vocab]
bag_aligned = bag[:, align_idx]
tfidf_log_aligned = tfidf_log[:, align_idx]
sklearn TF-IDF와 수작업(log) TF-IDF 간 상관(코사인 유사도 대략 확인)
from numpy.linalg import norm
def cosine(a, b):
na = norm(a) + 1e-12
nb = norm(b) + 1e-12
return float(np.dot(a, b) / (na * nb))
cosines = []
for i in range(len(docs)):
cosines.append(cosine(tfidf_log_aligned[i], X_tfidf.toarray()[i]))
print(“\n수작업 TF-IDF(log) vs sklearn TF-IDF 문서별 코사인 유사도:\n”, cosines)
———————————————————
7) 상위 TF-IDF 키워드 보기 (문서별)
———————————————————
def top_k_terms(tfidf_row, vocab_list, k=5):
idxs = np.argsort(-tfidf_row)[:k]
return [(vocab_list[i], tfidf_row[i]) for i in idxs if tfidf_row[i] > 0]
print(“\n[수작업(log)] 문서별 상위 키워드:”)
for i in range(len(docs)):
print(f”doc{i}:”, top_k_terms(tfidf_log[i], vocab, k=5))
print(“\n[sklearn] 문서별 상위 키워드:”)
sk_arr = X_tfidf.toarray()
for i in range(len(docs)):
print(f”doc{i}:”, top_k_terms(sk_arr[i], list(sk_vocab), k=5))
———————————————————
8) 미니 요약 테이블 출력
———————————————————
summary = pd.DataFrame({
“doc”: [f”doc{i}” for i in range(len(docs))],
“len(tokens)”: [len(tokens_list[i]) for i in range(len(docs))],
“top_terms_manual”: [“, “.join([w for w,_ in top_k_terms(tfidf_log[i], vocab, 5)]) for i in range(len(docs))],
“top_terms_sklearn”: [“, “.join([w for w,_ in top_k_terms(sk_arr[i], list(sk_vocab), 5)]) for i in range(len(docs))]
})
print(“\n요약:”)
print(summary)