Life Logs && *Timeline

  • TF-IDF와 로그(log) 완전 정복

    자연어 처리(NLP)에서 TF-IDF는 단어의 중요도를 평가하는 대표적인 기법입니다.
    그런데, 여기에는 **로그(logarithm)**라는 수학 개념이 핵심적으로 쓰입니다.
    이번 글에서는 TF-IDF와 로그의 원리, 그리고 왜 로그를 쓰는지까지 자세히 살펴보겠습니다.


    1. TF-IDF란?

    TF-IDFTerm 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 큼 → 중요도 높음

    그런데, 로그를 쓰지 않으면 단어의 등장 문서 수에 따라 값이 기하급수적으로 변동합니다.
    이러면 극단값이 너무 커져서 모델 학습에 불안정성을 줄 수 있습니다.

    로그의 효과

    1. 스케일 안정화
      1000, 100, 10 같은 큰 차이를 6.9, 4.6, 2.3처럼 부드럽게 줄여줍니다.
    2. 계산 편리성
      곱셈을 덧셈으로, 나눗셈을 뺄셈으로 바꿔줍니다.
    3. 지수적 증가를 선형화
      데이터의 분포를 다루기 쉽게 만들어줍니다.

    3. 로그의 정의와 성질

    로그는 다음과 같이 정의됩니다. log⁡b(x)=y⟺by=x\log_b(x) = y \quad \Longleftrightarrow \quad b^y = xlogb​(x)=y⟺by=x

    성질

    • 곱셈 → 덧셈: log⁡b(xy)=log⁡b(x)+log⁡b(y)\log_b(xy) = \log_b(x) + \log_b(y)logb​(xy)=logb​(x)+logb​(y)
    • 나눗셈 → 뺄셈: log⁡b(x/y)=log⁡b(x)−log⁡b(y)\log_b(x/y) = \log_b(x) – \log_b(y)logb​(x/y)=logb​(x)−logb​(y)
    • 거듭제곱 → 곱셈: log⁡b(xr)=rlog⁡b(x)\log_b(x^r) = r \log_b(x)logb​(xr)=rlogb​(x)

    4. TF-IDF에서의 로그 적용 예시

    단어전체 문서 수(N=1000)등장 문서 수(df)IDF(로그X)IDF(로그O)
    the10009901.010.01
    earthquake1000101004.6
    rareword1000110006.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)