TF-IDF

정의

  • 단순 빈도 뿐만 아니라, 그 단어가 전체 문서 집합에서 얼마나 희귀하게 나타나는지를 함께 고려해 단어의 중요도를 측정하고, 이를 바탕으로 텍스트를 벡터로 변환하는 것.

만드는 방법

  • TF 와 IDF 의 곱으로 계산한다.
  • TF : Term Frequency(용어 빈도). 문서 내 단어 빈도를 비율로 나타낸 것.
  • DF : Document Frequency(문서 빈도). 특정 단어가 포함된 문서의 빈도를 비율로 나타낸 것.
  • IDF : Inverse Document Frequency(역문서 빈도). DF의 역분. 특정 단어가 포함된 문서의 수가 작을수록 증가한다.
  • 예시 : 단어 A가 B문서에서 다른 단어에 비해 많이 등장하면서, A단어를 포함하는 다른 문서의 수가 적을수록 B 문서에서 A 단어에 대한 TF-IDF 값이 증가한다.

계산식

(1) TF (Term Frequency)

\[TF(w,d) = \frac{f_{w,d}}{\sum_{w\in d}f_{w,d}}\]

하나의 문서 d 안에서 특정 단어 w가 얼마나 자주 등장했는지를 나타내는 비율

  • $TF(w,d)$ : 문서 $d$ 안에서 단어 $w$가 얼마나 자주 등장했는지를 나타내는 비율
  • $f_{w,d}$ : 문서 $d$ 안에서 단어 $w$가 등장한 횟수
  • $\sum_{w\in d}f_{w,d}$ : 문서 $d$ 를 이루는 전체 단어 등장 횟수 총합
1
2
3
4
5
6
7
8
9
10
# TF 예시
corpus = [
  ['human', 'interface', 'computer'],
  ['survey', 'user', 'computer', 'system', 'response', 'time'],
  ['eps', 'user', 'interface', 'system'],
  ['system', 'human', 'system', 'eps'],
]

TF("human", corpus[0]) = 1/3
TF("system", corpus[3]) = 2/4 = 1/2

(2) IDF (Inverse Document Frequency)

\[IDF(w, D) = log\frac{|D|}{f_{w, D}}\]

전체 문서 집합(corpus, D)에서 단어 w 가 얼마나 희귀하게 나타나는지의 정도

  • $IDF(w, D)$ : 단어 w가 전체 문서 집합(corpus) D에서 얼마나 희귀하게 나타나는지의 정도
  • $ D $ : corpus 안의 전체 문서의 개수
  • $f(w,D)$ : corpus 안에서 단어 w를 포함하는 문서의 개수
  • 희귀한 단어일수록 값이 높아져 중요도가 올라간다.
  • 값의 변별력을 높이기 위해 log를 취한다.(보통 자연로그 ln을 사용함)
1
2
3
4
5
6
7
8
9
# IDF 예시
corpus = [
  ['survey', 'user', 'computer', 'system', 'response', 'time'],
  ['eps', 'user', 'interface', 'system'],
  ['system', 'human', 'system', 'eps'],
]

IDF("system", corpus) = log(3/3) = log(1) = 0
IDF("survey", corpus) = log(3/1) = log(3) = 1.09861229
  • 보통 IDF 계산의 결과가 위 system 처럼 0이 나오지 않게 스무딩 처리를 한다.
  • 예를 들어 scikit-learn 의 TF-IDF 는 아래와 같이 최소 1이 나오게끔 한다.

IDF = log((전체문서개수 + 1) / (단어를포함하는문서개수+1)) + 1

\[(스무딩 \, 적용) \,\,\,\,\,\,\,\, IDF(w, D) = log\frac{|D| + 1}{f_{w,D} + 1} \, + \, 1 \,\]
1
2
3
4
5
6
7
8
9
# 스무딩 적용 IDF 예시
corpus = [
  ['survey', 'user', 'computer', 'system', 'response', 'time'],
  ['eps', 'user', 'interface', 'system'],
  ['system', 'human', 'system', 'eps'],
]

IDF("system", corpus) = log((3+1)/(3+1)) + 1 = log(1) + 1 = 1
IDF("survey", corpus) = log((3+1)/(1+1)) + 1 = log(2) + 1 = 1.69314718

(3) TF-IDF

\[w_{d} = tf(w,d) * idf(w,D) = \frac{f_{w,d}}{\sum_{w\in d}f_{w,d}} * log\frac{|D|}{f_{w, D}}\]
  • TF 와 IDF의 곱
  • $w$ : TF-IDF 를 측정하려는 단어
  • $d$ : 측정하려는 단어 $w$ 가 속한 문서
  • $D$ : 전체 문서 (corpus)
  • 문서 $d$ 안에 단어 $w$가 여러 번 등장할수록 tf 값이 증가하며
  • corpus($D$) 안에 단어 $w$를 포함하는 문서가 적을수록 idf 값이 증가한다.
  • 반대로 $f_{w,D}$ 가 $ D $와 비슷해지면(일반적으로 많이 쓰이면) idf 값이 0에 가까워져, 해당 단어가 무시된다.

TF-IDF의 특징

  • 문서를 단어들의 빈도로 나타낸다는 점에서 BoW와 유사하다.
  • 다른 점은, 일반적으로 많이 쓰이는 단어일수록 무시하는 페널티를 준다는 점에서 차이가 있음
  • TF-IDF의 전제는 많은 문서들에 공통적으로 포함된 단어가 어느 특정 문서에서만 등장하는 단어보다 문서의 고유한 특성을 나타내기에 제공하는 정보가 적다라는 것

TF-IDF 의 종류

  • 현실에서는 TF-IDF의 많은 variation 들이 있다.
  • 스무딩(IDF가 0이 되지 않게 하는 처리)와 정규화(L2norm)가 바로 그것이다.

실습

설치

  • gensim 라이브러리를 설치한다.
  • gensim은 자연어 처리를 위한 오픈소스 파이썬 라이브러리로 주로 토픽 모델링과 단어 임베딩을 효율적으로 처리하기 위해 설계되었다.
1
pip install gensim

실습 데이터

  • gensim 의 샘플 데이터를 활용한다.
  • text corpus 는 9개의 문장으로 이루어져 있다.

gensim - example data

1
2
3
4
5
6
7
8
9
10
11
text_corpus = [
    "Human machine interface for lab abc computer applications",
    "A survey of user opinion of computer system response time",
    "The EPS user interface management system",
    "System and human system engineering testing of EPS",
    "Relation of user perceived response time to error measurement",
    "The generation of random binary unordered trees",
    "The intersection graph of paths in trees",
    "Graph minors IV Widths of trees and well quasi ordering",
    "Graph minors A survey",
]

전처리

이번의 전처리 조건은 다음과 같다.

1
2
3
4
(1) 토크나이징 : white space(공백)을 기준으로 토큰을 나눈다.
(2) 불용어 처리 : for, a, of, the, and 등의 stop-words를 문장에서 제거한다.  
(3) 텍스트 정규화 : 문장에서 등장하는 단어를 소문자로 바꾼다.  
(4) 단어사전 : 단어의 등장 빈도를 count 하고, 전체 문장에서 두 번 이상 등장한 단어만을 유지한다.  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# tokenizer
class WhitespaceTokenizer():
    def tokenize(self, input:str) -> list[str]:
        if isinstance(input, str):
            result = input.split(" ")
        return result

# Text Cleaner
class TextCleaner:
    def __init__(self):
        # Create a set of frequent words
        self.stopwords = set('for a of the and to in'.split(' '))
    def clean_text(self, words:list[str]) -> list[str]:
        # Lowercase each document, split it by white space and filter out stopwords
        words = [word.lower() for word in words if word.lower() not in self.stopwords]
        return words

# filter by frequency
class FilterByFrequency:
    def __init__(self):
        # Count word frequencies
        from collections import defaultdict
        self.frequency_dict = defaultdict(int)
    def make_filter(self, docs:list[list[str]]):
        for text in docs:
            for token in text:
                self.frequency_dict[token] += 1
    def filter(self, words:list[str], threshold:int=1):
        # Only keep words that appear more than once
        filtered_words = [token for token in words if self.frequency_dict[token] > threshold]
        return filtered_words
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# (1) 토크나이징 : 공백을 기준으로
tokenizer = WhitespaceTokenizer()
tokenized_docs = [tokenizer.tokenize(doc) for doc in text_corpus]
# (2) 텍스트 클리닝 - lower + stopwords
text_cleaner = TextCleaner()
cleaned_docs = [text_cleaner.clean_text(words) for words in tokenized_docs]
# (3) 빈도 기반 필터링 : 1회 발생 단어는 제외
filter = FilterByFrequency()
filter.make_filter(cleaned_docs)
processed_corpus = [filter.filter(doc, 1) for doc in cleaned_docs]

print(processed_corpus)
# 결과
[['human', 'interface', 'computer'],
 ['survey', 'user', 'computer', 'system', 'response', 'time'],
 ['eps', 'user', 'interface', 'system'],
 ['system', 'human', 'system', 'eps'],
 ['user', 'response', 'time'],
 ['trees'],
 ['graph', 'trees'],
 ['graph', 'minors', 'trees'],
 ['graph', 'minors', 'survey']]

사전 만들기

  • 전처리를 통해 걸러진 단어들에 대해 고유의 인덱스를 부여한다.
  • gensim.corporaDictionary 클래스를 이용해 인덱스 - 단어 쌍으로 이루어진 사전을 제작한다.
  • 사전이 만들어졌다면, BoW를 위한 기본적인 준비는 완료됐다.
  • corpora 는 gensim 라이브러리에서 말뭉치(corpus)와 사전(dctionary)를 다루는 모듈이다.
1
2
3
4
5
6
7
8
9
10
# bow
class BagOfWords:
    def __init__(self):
        self.dictionary:dict[str,int]|None=None
    def create_dictionary(self, input:list[list[str]]):
        from gensim import corpora
        self.dictionary = corpora.Dictionary(input)
    def represent_bow(self, input:list[list[str]]):
        bow_corpus = [self.dictionary.doc2bow(text) for text in input]
        return bow_corpus
1
2
3
4
5
6
7
8
9
10
11
# (4) BoW 생성
bow_model = BagOfWords()
bow_model.create_dictionary(processed_corpus)
bow = bow_model.represent_bow(processed_corpus)

print(bow_model.dictionary.token2id)
# 출력
{'computer': 0, 'human': 1, 'interface': 2,
 'response': 3, 'survey': 4, 'system': 5,
 'time': 6, 'user': 7, 'eps': 8, 'trees': 9,
 'graph': 10, 'minors': 11}

샘플 문장들을 BoW로 표현하기

  • 만들어진 단어 사전을 이용해 처음에 주어진 샘플 문장들을 BoW를 통해 벡터로 만들어보자.
  • 4번 문장에서 system이 2회 등장함을 볼 수 있다.
  • 빈도가 0인 단어들은 표시되지 않는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# (4) BoW 생성
bow_model = BagOfWords()
bow_model.create_dictionary(processed_corpus)
bow = bow_model.represent_bow(processed_corpus)

print("===== corpus(Words) =====")
print(processed_corpus)
print("===== BoW Token Dictionary =====")
print(bow_model.dictionary.token2id)

# 출력
===== corpus(words) =====
[['human', 'interface', 'computer'],
['survey', 'user', 'computer', 'system', 'response', 'time'],
['eps', 'user', 'interface', 'system'],
['system', 'human', 'system', 'eps'],
['user', 'response', 'time'],
['trees'],
['graph', 'trees'],
['graph', 'minors', 'trees'],
['graph', 'minors', 'survey']]
===== BoW =====
[[(0, 1), (1, 1), (2, 1)],
[(0, 1), (3, 1), (4, 1), (5, 1), (6, 1), (7, 1)],
[(2, 1), (5, 1), (7, 1), (8, 1)],
[(1, 1), (5, 2), (8, 1)],
[(3, 1), (6, 1), (7, 1)],
[(9, 1)],
[(9, 1), (10, 1)],
[(9, 1), (10, 1), (11, 1)],
[(4, 1), (10, 1), (11, 1)]]

전통적 TF-IDF 직접 구현하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# TF-IDF (전통적 TF-IDF)
from math import log
class StandardTFIDF:
    def __init__(self, Documents):
        self.Documents:list[list[str]] = Documents
    def tf(self, document:list[str]):
        result = []
        all_word_count = len(document)
        for word in document:
            target_word_count = len([dw for dw in document if dw == word])
            result.append((word, target_word_count/all_word_count))
        return result
    def idf(self, document:list[str]):
        from math import log
        result = []
        document_count = len(self.Documents)
        for word in document:
            include_word_document_count = len([doc for doc in self.Documents if word in doc])
            result.append((word, log(document_count/include_word_document_count)))
        return result
    def tfidf(self, document:list[str]):
        result = []
        for tf, idf in zip(self.tf(document), self.idf(document)):
            result.append((tf[0], tf[1] * idf[1]))
        return result
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 신규 문장의 특정 단어 TF-IDF 계산

# 신규 문장
new_sentence = "system minors"
cleaned_words = filter.filter(text_cleaner.clean_text(tokenizer.tokenize(new_sentence)))
print(cleaned_words)

# TF-IDF 계산
tfidf_model = StandardTFIDF(processed_corpus)
tf = tfidf_model.tf(cleaned_words)
idf = tfidf_model.idf(cleaned_words)
tfidf = tfidf_model.tfidf(cleaned_words)
print(f"tf : {tf}")
print(f"idf : {idf}")
print(f"tfidf : {tfidf}")

# 출력
['system', 'minors']
tf : [('system', 0.5), ('minors', 0.5)]
idf : [('system', 1.0986122886681098), ('minors', 1.5040773967762742)]
tfidf : [('system', 0.5493061443340549), ('minors', 0.7520386983881371)]

IDF 계산

  • 9개 문서 중 system 이라는 단어가 등장하는 문서는 3개, minors 는 2개다.
  • 따라서 system 이라는 단어의 IDF 는 log(9/3) = log3 = 1.0986 이 된다.
  • 그리고 minors 이라는 단어의 IDF 는 log(9/2) = log4.5 = 1.5041 이 된다.

TF 계산

  • 문서는 2개의 각기 다른 단어를 가지고 있다.
  • 따라서 각 단어의 TF 값은 1/2 = 0.5 가 된다.

TF-IDF 계산

  • system 에 대한 TF-IDF 는 TF와 IDF의 곱인 0.5 * log3 = 0.5493 이 된다.
  • minors 에 대한 TF-IDF 는 TF와 IDF의 곱인 0.5 * log4.5 = 0.7520 이 된다.

gensim을 이용한 TF-IDF 계산

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# gensim 라이브러리를 이용하는 경우

from gensim import models

# train tf-idf model from corpus
bow_corpus = [bow_model.dictionary.doc2bow(doc) for doc in processed_corpus]
tfidf = models.TfidfModel(bow_corpus)

# test doc
new_sentence = "system minors"
cleaned_words = filter.filter(text_cleaner.clean_text(tokenizer.tokenize(new_sentence)))
new_doc_bow = bow_model.dictionary.doc2bow(cleaned_words)
new_doc_tfidf = tfidf[new_doc_bow]
print(new_doc_tfidf)
1
2
# 출력
[(5, np.float64(0.5898341626740045)), (11, np.float64(0.8075244024440723))]
  • 표준적인 계산식을 통해 계산한 TF-IDF와 gensim을 이용해 계산한 결과값은 서로 다름을 볼 수 있다.

커스텀 스무딩을 적용한 TF-IDF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 스무딩 적용 TF-IDF
class SmoothingTFIDF:
    def __init__(self, Documents):
        self.Documents:list[list[str]] = Documents
    def tf(self, document:list[str]):
        result = []
        all_word_count = len(document)
        for word in document:
            target_word_count = len([dw for dw in document if dw == word])
            result.append((word, target_word_count/all_word_count))
        return result
    def idf(self, document:list[str]):
        from math import log
        result = []
        document_count = len(self.Documents) + 1
        for word in document:
            include_word_document_count = len([doc for doc in self.Documents if word in doc]) + 1
            result.append((word, log(document_count/include_word_document_count) + 1))
        return result
    def tfidf(self, document:list[str]):
        result = []
        for tf, idf in zip(self.tf(document), self.idf(document)):
            result.append((tf[0], tf[1] * idf[1]))
        return result

# 신규 문장의 특정 단어 TF-IDF 계산

# 신규 문장
new_sentence = "system minors"
cleaned_words = filter.filter(text_cleaner.clean_text(tokenizer.tokenize(new_sentence)))
print(cleaned_words)

# TF-IDF 계산
tfidf_model = SmoothingTFIDF(processed_corpus)
tf = tfidf_model.tf(cleaned_words)
idf = tfidf_model.idf(cleaned_words)
tfidf = tfidf_model.tfidf(cleaned_words)
print(f"tf : {tf}")
print(f"idf : {idf}")
print(f"tfidf : {tfidf}")
1
2
3
4
5
# 출력
['system', 'minors']
tf : [('system', 0.5), ('minors', 0.5)]
idf : [('system', 1.916290731874155), ('minors', 2.203972804325936)]
tfidf : [('system', 0.9581453659370776), ('minors', 1.101986402162968)]

Reference

방송통신대학교 - 자연언어처리 수업 (유찬우 교수)
gensim 샘플 데이터 및 코드

Comments