fullfish 2025. 11. 20. 10:22

TF-IDF (Term Frequency – Inverse Document Frequency)

 

TF (Term Frequency)

  • 한 문서 안에서 얼마나 자주 나왔나
    → BoW에서 쓰던 “횟수” 혹은 “비율”

IDF (Inverse Document Frequency)

전체 문서 중에서 얼마나 희귀한 단어인가
→ 모든 문서에 다 나오는 단어는 중요도가 떨어짐

 

핵심 아이디어

  • 한 문서 안에서 자주 나오고 (TF ↑),
  • 다른 문서에서는 잘 안 나오는 단어 (IDF ↑)
    → 그 문서를 잘 대표하는 “좋은 단어”로 가중치를 크게 준다.

TF(w) = (특정 단어 w의 문서 내 등장 횟수) / (해당 문서 내 총 단어 수)

IDF(w) = log(총 문서 수 / 특정 단어 w를 포함한 문서 수)

TF-IDF(w) = TF(w) × IDF(w)

 

장점

  • 중요 단어 식별: 문서에서 중요한 단어를 구분하는 데 유용하며, 불필요한 단어의 가중치를 낮출 수 있음
  • 정보 검색에 유용: 검색 엔진에서 문서의 관련성을 평가하는 데 자주 사용됨

단점

  • 희소 행렬 문제: 큰 코퍼스에서는 매우 큰 희소 행렬이 생겨 메모리 사용량이 증가함
  • 단어 의미 무시: 단어의 순서나 의미적 관계를 고려하지 않아 문맥 정보를 반영하기 어려움
vectorizer = TfidfVectorizer(
    tokenizer=tokenize_korean,  # 우리가 만든 함수
    token_pattern=None,  # 기본 정규식 비활성화
    min_df=1,  # 최소 문서 빈도
    ngram_range=(1, 1),  # 일단 unigram만
)

CountVectorizer와 공통 속성

속성                                                         설명                                                                      예시

vocabulary_ 단어 → 인덱스 매핑 딕셔너리 {'오늘': 0, '날씨': 1, '좋다': 2}
stop_words_ 불용어로 제거된 단어 목록 {'the', 'is', 'and'}
get_feature_names_out() 인덱스 순서대로 정렬된 단어 리스트 ['날씨', '오늘', '좋다']
dtype TF-IDF 행렬의 데이터 타입 <class 'numpy.float64'>
ngram_range n-gram 범위 (예: (1,2)) (1, 2)
analyzer 분석 단위(word/char 등) 'word'
lowercase 영문 소문자 변환 여부 True

 

TfidfVectorizer 전용 속성

속성                              설명                                                                                                                     예시

idf_ 단어별 IDF(역문서빈도) 값 (단어 희귀도) [1.29, 1.58, 1.11]
use_idf IDF를 적용할지 여부 (True면 TF×IDF 계산) True
smooth_idf IDF 계산 시 0 나누기 방지를 위한 스무딩 사용 여부 True
sublinear_tf TF를 1 + log(tf) 형태로 스케일링할지 여부 False
norm TF-IDF 벡터를 정규화할 방법 ('l2', 'l1', None) 'l2'

 

코드

from sklearn.feature_extraction.text import TfidfVectorizer

docs = ["오늘 날씨 날씨 정말 좋다", "오늘 기분 정말 좋다", "오늘은 기분이 좋지 않다"]

tfidf = TfidfVectorizer()

X_tfidf = tfidf.fit_transform(docs)
print("vocab : ", tfidf.get_feature_names_out())
print("shape : ", X_tfidf.shape)
print("X_tfidf : \n", X_tfidf.toarray().round(3))

'''
vocab :  ['기분' '기분이' '날씨' '않다' '오늘' '오늘은' '정말' '좋다' '좋지']
shape :  (3, 9)
X_tfidf : 
 [[0.    0.    0.835 0.    0.318 0.    0.318 0.318 0.   ]
 [0.605 0.    0.    0.    0.46  0.    0.46  0.46  0.   ]
 [0.    0.5   0.    0.5   0.    0.5   0.    0.    0.5  ]]
 3번째 0.835는 날씨가 첫 문장에 2번 혼자만 있음. 중요도 높음'''

 

Cosine_similarity(문장 유사도)

# TF-IDF로 문서를 벡터화한 뒤, 코사인 유사도로 가장 비슷한 문서를 찾는 예제

from sklearn.feature_extraction.text import TfidfVectorizer  # TF-IDF 벡터라이저
from sklearn.metrics.pairwise import cosine_similarity        # 코사인 유사도 계산
import numpy as np                                            # argmax 등 수치 연산

# 비교할 문서(코퍼스) 리스트
docs = [
    "오늘 날씨가 좋아서 산책을 했다",
    "오늘은 비가 와서 우울하다",
    "점심에 맛있는 파스타를 먹었다",
    "저녁에 산책하면서 음악을 들었다"  # 따옴표 통일 (직접 입력 시 스마트쿼트 주의)
]

# TF-IDF 벡터라이저 생성 (기본 설정: 공백 기반 토큰화)
vectorizer = TfidfVectorizer()

# 문서 코퍼스를 학습(fit)하고, TF-IDF 희소행렬로 변환(transform)
X = vectorizer.fit_transform(docs)  # 형태: (문서 수, 단어 수)

# 검색 쿼리(비교 대상) 문장
query = ["산책을 하면서 날씨를 즐겼다"]

# 코퍼스에서 학습한 같은 단어 사전으로 쿼리를 TF-IDF 벡터로 변환
X_query = vectorizer.transform(query)  # 형태: (1, 단어 수)

# 쿼리와 각 문서 간 코사인 유사도 계산 → 결과 형태: (1, 문서 수)
sim = cosine_similarity(X_query, X)

# 유사도 행렬 출력 (한 줄짜리 벡터)
print("similarity:", sim)

# 가장 유사도가 높은 문서의 인덱스 선택
most_sim_idx = np.argmax(sim[0])

# 원 쿼리 문장 출력
print("Query:", query[0])

# 가장 유사한 문서 출력
print("Most similar doc:", docs[most_sim_idx])

 

실습

import re
import numpy as np
import pandas as pd
from konlpy.tag import Okt
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, classification_report

"""
1. movie_reviews.csv 읽어서 DataFrame (결측치 삭제)
2. train_test_split() 함수로 층화추출로 샘플 10000개 추출
3. 샘플 10000개에 대해서 train, test로 분할
4. 텍스트전처리 (함수로 구현)
  - 영문자(소문자)
  - 한글/영문/숫자/공백 외 특수문자 제거
  - 형태소 분석 + 품사 태깅
  - 사용할 품사만 선택 (명사, 동사, 형용사)
  - 불용어 제거
5. TfdifVectorizer()로 훈련/테스트 데이터 벡터화
6. 모델 훈련(LogisticRegression)
7. 모델 테스트 검증 점수 출력"""

# 1. movie_reviews.csv 읽어서 DataFrame (결측치 삭제)
df = pd.read_csv(
    "stat_nlp/document_vector/movie_reviews.csv",
    header=0,
    names=["id", "review", "label"],
)

df = df.dropna()

# label을 정수형으로 변환 (0, 1). 지금 정수형이긴 하지만 혹시 모르니까
df["label"] = df["label"].astype(int)

print(df.head())
# print(df["label"].value_counts()) # 개수 확인
print(df["label"].value_counts(normalize=True))  # 비율 확인

# 2. train_test_split() 함수로 층화추출로 샘플 10000개 추출
_, df_sample = train_test_split(
    df,
    test_size=10000,
    stratify=df["label"],
    shuffle=True,  # 기본값이라 사실 안 써도 됨
    random_state=42,
)
print(len(df_sample))
print(len(_))

X = df_sample["review"]
y = df_sample["label"]

# 3. 샘플 10000개에 대해서 train, test로 분할
x_train, x_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.2,  # 0.2 → 20퍼센트 test
    stratify=y,  # 샘플 안에서도 0/1 비율 유지
    shuffle=True,
    random_state=42,
)

"""
4. 텍스트전처리 (함수로 구현)
  - 영문자(소문자)
  - 한글/영문/숫자/공백 외 특수문자 제거
  - 형태소 분석 + 품사 태깅
  - 사용할 품사만 선택 (명사, 동사, 형용사)
  - 불용어 제거"""

okt = Okt()
# 불용어 가져오기
with open(
    "/Users/choimanseon/Documents/multicampus/example/nlp/stat_nlp/stopwords-ko.txt",
    encoding="utf-8",
) as f:
    basic_stopwords = set(w.strip() for w in f if w.strip())

possible_pos = ["Noun", "Verb", "Adjective"]


def preprocess(text):
    str_reg = re.sub(r"[^가-힝0-9a-zA-Z\s]", "", text).lower()
    pos = okt.pos(str_reg, norm=True, stem=True, join=True)
    pos = [word.split("/") for word in pos]
    filtered_pos = [
        word
        for word, tag in pos
        if word and word not in basic_stopwords and tag in possible_pos
    ]
    return filtered_pos


# 5. TfdifVectorizer()로 훈련/테스트 데이터 벡터화
vectorizer = TfidfVectorizer(
    tokenizer=preprocess,
    token_pattern=None,  # 기본 정규식 토큰 분리 끔
    ngram_range=(1, 2),
)

x_train_vec = vectorizer.fit_transform(x_train)
x_test_vec = vectorizer.transform(x_test)

# 6. 모델 훈련(LogisticRegression)
clf = LogisticRegression(max_iter=1000)
clf.fit(x_train_vec, y_train)

# 7. 모델 테스트 검증 점수 출력
y_pred = clf.predict(x_test_vec)
print("y_pred", y_pred)

print(classification_report(y_test, y_pred, digits=3))


# 문장 받아서 긍정 부정 출력
test_sents = [
    "와 진짜 너무 재밌고 감동적이었다",
    "스토리가 너무 지루하고 시간 아깝네",
    "배우 연기는 좋았는데 결말이 별로다",
    "미친 영화다... 올해 최고",
    "연기 연습좀 해야할듯",
    "친구 추천해줘야 겠다",
]


def predict_sentences(sent_list):
    vec = vectorizer.transform(sent_list)
    preds = clf.predict(vec)
    # 긍정 확률 (1일 확률)
    probs = clf.predict_proba(vec)[:, 1]

    for sent, label, prob in zip(sent_list, preds, probs):
        print("문장:", sent)
        print("예측 라벨:", label, "(1=긍정, 0=부정)")
        print(f"긍정 확률: {prob:.3f}")
        print()


predict_sentences(test_sents)