TL;DR
- 단어 빈도를 기반으로, 모든 문서에 흔히 나오는 단어에는 페널티를 주어 중요한 단어를 점수화한다.
- 값이 클수록 그 문서에서 중요한 단어다.
- 단어와 문서가 많아질수록 결과 행렬은 sparse matrix가 된다.
TF-IDF
TF-IDF(Term Frequency-Inverse Document Frequency)는 문서 안에서 한 단어가 얼마나 중요한지를 점수화하는 가중 방식이다. 특정 문서에 자주 등장할수록(TF) 점수를 높이고, 모든 문서에 흔히 등장할수록(IDF) 점수를 낮춘다.

Term Frequency (TF)
Term Frequency는 한 문서 안에서 특정 단어가 얼마나 자주 등장하는지를 나타낸다. 보통 raw count를 그대로 쓰기보다, 문서 길이 차이를 보정하기 위해 문서 안 전체 단어 수로 나눈 상대 빈도로 정의한다.
여기서 는 문서 안에서 단어 가 등장한 횟수다. 여러 문서의 TF를 모으면 행이 문서, 열이 단어인 DTM(Document Term Matrix) 형태로 표현된다.
Document Frequency (DF)
Document Frequency는 특정 단어 가 등장한 문서의 수 다. 핵심은 단어가 몇 번 등장했는지가 아니라 몇 개의 문서에 등장했는지다. 예를 들어 "사과"가 세 문서에 나타났다면, 그 세 문서 안에서 총 100번 등장하더라도 이다. DF가 크다는 것은 그 단어가 다수의 문서에 두루 쓰인다는 뜻이고, 따라서 특정 문서를 구별하는 힘이 약하다는 신호다.
Inverse Document Frequency (IDF)
Inverse Document Frequency는 DF가 큰 단어, 즉 여러 문서에 흔히 나오는 단어의 가중치를 낮추는 값이다. 단순히 DF의 역수를 쓰기보다, 전체 문서 수 을 DF로 나눈 뒤 로그를 취해 정의하는 것이 일반적이다.
여기서 로그는 밑을 정하지 않은 일반적인 로그다. 보통 자연로그(밑 )나 밑 10을 쓴다. 밑을 바꾸면 모든 단어의 IDF에 같은 수가 곱해질 뿐이라() 단어 사이의 비율과 순위는 달라지지 않는다.
DF가 클수록 IDF는 작아지므로, 조사나 불용어처럼 거의 모든 문서에 나오는 단어는 낮은 가중치를 받는다. 로그를 취하는 이유는 DF가 아주 큰 단어의 영향을 완만하게 누르기 위해서다.
TF-IDF
TF-IDF는 이 두 값을 곱해 단어의 중요도를 매긴다.
특정 문서에 자주 등장하면 TF가 커져 점수가 올라가고, 그 단어가 모든 문서에 흔하면 IDF가 작아져 점수가 깎인다. 그 결과 "이 문서에서만 두드러지는 단어"가 높은 점수를 받는다. 값이 클수록 해당 문서를 잘 대표하는 단어이고, 작을수록 변별력이 낮은 단어다.
구현
from sklearn.feature_extraction.text import TfidfVectorizer
corpus = [
"안녕하세요 이것은 tf-idf 테스트입니다.",
"정말 제대로 이해하고 있을까요?",
"예제를 통해 알아 봅시다."
]
# 1. init TfidfVectorizer and fit
vectorizer = TfidfVectorizer()
X = vectorizer.fit_transform(corpus) # X is a DTM(Document Term Matrix) that contains a tf-idf score element
feature_names = vectorizer.get_feature_names_out() # feature_names is a list of terms(words)
#2. Extract the top-k most important words(those with the highest TF-IDF socres) from each document
for i, doc in enumerate(corpus):
print(f"\nTop words in document {i + 1}:")
# Extract TF-IDF scores for the respective document
tfidf_scores = X[i].toarray().flatten()
# Sort TF-IDF scores in descending order and output the top-k words
# So, sort in ascending order, take the top 3, and reverse for the top 3 words
top_indices = tfidf_scores.argsort()[-3:][::-1]
for idx in top_indices:
print(f"{feature_names[idx]}: {tfidf_scores[idx]}")scikit-learn 결과가 위 수식과 다른 이유
위 코드를 돌려 보면 앞에서 설명한 수식으로 직접 계산한 값과 점수가 다르게 나온다. scikit-learn의 TfidfVectorizer가 TF, IDF, 정규화마다 조금씩 다른 식을 기본으로 쓰기 때문이다.1
| 단계 | 앞에서 설명한 수식 | scikit-learn 기본값 | 바꾸는 옵션 |
|---|---|---|---|
| TF | 상대 빈도 | 등장 횟수 그대로 | sublinear_tf=True면 |
| IDF | smooth_idf=False면 | ||
| 정규화 | 하지 않음 | 문서 벡터마다 L2 정규화 | norm=None이면 하지 않음 |
가장 크게 차이 나는 곳은 IDF다. 문서가 3개이고 어떤 단어가 세 문서 모두에 나온다고 하자. 앞의 수식으로는 이라 이 단어의 점수는 0이 된다. scikit-learn으로 계산하면 같은 단어의 IDF가 이다. 한 문서에만 나오는 단어도 자연로그 기준 대신 를 받는다. 분자와 분모에 1을 더하는 것은 모든 단어가 한 번씩 들어 있는 문서가 하나 더 있다고 가정한 것이다. 이렇게 하면 0으로 나누는 일이 생기지 않는다. 마지막에 1을 더하기 때문에 모든 문서에 나오는 단어도 IDF가 1보다 작아지지 않는다.2
나머지 두 가지는 한 문서 안의 단어 순위를 바꾸지 않는다. 문서 길이로 나누는 것도, L2 정규화도 그 문서의 모든 단어 점수를 같은 비율로 줄이기 때문이다. L2 정규화는 문서의 점수 벡터를 그 벡터의 길이로 나눠 길이를 1로 맞춘다.
"사과 사과 배 감"이라는 문서로 두 방식을 계산해 보면 이렇다(IDF는 모두 1로 둔다).
| 단어 | 등장 횟수 | 문서 길이로 나누기 (횟수 ÷ 4) | L2 정규화 (횟수 ÷ ) |
|---|---|---|---|
| 사과 | 2 | 0.5 | 0.82 |
| 배 | 1 | 0.25 | 0.41 |
| 감 | 1 | 0.25 | 0.41 |
어느 쪽이든 사과는 배와 감의 두 배다. L2 정규화가 1로 맞추는 것은 문서 전체의 크기다. 그래서 문서가 길다는 이유만으로 점수가 커지지 않고 모든 문서를 같은 크기에서 비교하게 된다.
같은 TF-IDF라도 도구마다 계산 방식이 조금씩 다르다. 다른 도구의 결과나 직접 계산한 값과 비교할 때는 TF, IDF, 정규화 옵션부터 확인한다.
Discussion
Comments
댓글은 승인 후 공개됩니다.