[NLP] TFIDF

7 minute read

tf-idf란

정보 검색과 텍스트 마이닝에서 이용하는 가중치로, 여러 문서로 이루어진 문서군이 있을 때 어떤 단어가 특정 문서 내에서 얼마나 중요한 것인지 를 나타내는 통계적 수치.

  • TF(Term Frequency): 문서 내에서 특정 단어가 나오는 빈도(문서 내에서 중요하게 생각)
  • IDF(Inverse Document Frequency): DF(문서군 내에서 자주 등장하는 단어의 빈도)가 높으면 각 문서들을 세분화하여 구분할 수 없어 역수를 취해(여기까지가 IDF 정의) TF와 곱해주어 여러 문서로 이루어진 문서군이 있을 때 어떤 단어가 특정 문서 내에서 얼마나 중요한 것인지를 나타내는 통계적 수치에 사용.(IDF의 존재 의의 - 전체 문서 중 특정 단어가 존재하는 문서가 많이 존재 할수록 그 단어는 중요한 단어가 아닌 흔한 단어라 생각하여 tf에 나누어 주어 통계적 수치를 줄여준다.)

결론: 특정 문서 내에서 단어 빈도가 높을 수록, 그리고 전체 문서들 중 그 단어를 포함한 문서가 적을수록 TF-IDF값이 높아진다. 이 값을 이용하면 모든 문서에 흔하게 나타나는 단어를 걸러내는 효과를 얻을 수 있다.

ex) 10개의 문서가 존재, target 문서 내의 youtube 단어 수 10개

  • tf(t,d) = 10
  • idf(t,D) = log( D / 1 + 단어가 포함된 문서의 수)
    • 분모가 0이 되는 것을 방지하기 위해 1을 더해준다.
  • tfidf(t,d,D) = tf(t,d) X idf(t,D
  • youtube 단어가 존재하는 문서가
    • 2개일 때: 10 x log(10/3) = 10 x 0.522 = 5.22
    • 7개일 떄: 10 x log(10/8) = 10 x 0.096 = 0.96

이므로 문서군 내에서 여러 문서에 target 단어가 분포할 시 tf-idf는 0에 가까워지고 이는 덜 중요한 단어라는 것을 나타낸다.

실제로 다른 문서에도 많이 존재할거 같은 a, an, the, and, or, of 같은 관사나 접속사같은 단어들의 중요도를 낮추는 역할을 한다.

정규화 필요

tf의 값이 천차만별로 나올 수 있기 때문에 정규화를 해 주어야 한다.

  • 불린 빈도: tf(t,d) = t가 d에 한번이라도 나타나면 1, 아니면 0
  • 로그 스케일 빈도: tf(t,d) = log( f(t,d) + 1 )
  • 증가 빈도: 최빈 단어를 분모로 target 단어의 tf를 나눈 값으로, 일반적으로는 문서의 길이가 상대적으로 길 경우, 단어 빈도값을 조절하기 위해 사용한다. tf(t,d) = 0.5 + { ( 0.5 x f(t,d) ) / ( 최빈 단어 수 ) }

ex) 10개의 문서가 존재, target 문서 내의 youtube 단어수 100000개

  • tf(t,d) = 10
  • youtube 단어가 존재하는 문서가
    • 2개일 때: 100000 x log( 10 / 2 ) = 69897
    • 8개일 때: 100000 x 1og( 10 / 8) = 9691 숫자가 너무 커져버릴 수 있다.
  • 정규화 사용
  • log scale
  • 2개일 때: log( 100000 + 1 ) x log( 10 / 2) = 3.49
  • 8개일 때: log( 100000 + 1 ) x log( 10 / 8) = 0.48

코딩.

import nltk
import re
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize, sent_tokenize
import math
# nltk.download('punkt')

text1 = '''
If you like tuna and tomato sauce- try combinaning the two.
It's really not as bad as it sounds.
If the Easter Bunny and the Tooth Fairy had babies would they take
your teeth and leave chocolate for you?
'''
  • nltk.tokenize 라이브러리 사용하여 문장별로 토큰화하여 나눠준다. - 전처리 전 단계
text_sents = sent_tokenize(text1)
print(text_sents)

['\nIf you like tuna and tomato sauce- try combinaning the two.', "It's really not as bad as it sounds.", 'If the Easter Bunny and the Tooth Fairy had babies would they take\nyour teeth and leave chocolate for you?']
  • A very simple pre-processing function
def remove_string_special_characters(s):
    '''
    This function removes special characters from within a string

    parameters:
        s(str): single input string.

    return:
        stripped(str): A string with special characters removed
    '''

    # Replace special character with ' '
    # [^\s\w] 문자와 space를 제외하고 다 제거한다.
    stripped = re.sub('[^\w\s]', '', s)
    stripped = re.sub('_', '', stripped) # ??

    # Change any withespace to one space
    stripped = re.sub('\s+', ' ', stripped)

    # Remove start and end's white space
    stripped = stripped.strip()

    return stripped
text_sents_clean = [remove_string_special_characters(s) for s in text_sents]
print(text_sents_clean)

['If you like tuna and tomato sauce try combinaning the two', 'Its really not as bad as it sounds', 'If the Easter Bunny and the Tooth Fairy had babies would they take your teeth and leave chocolate for you']
  • make document function - 1개의 sentence를 1개의 document로 보고 document당 단어의 갯수를 세어줌.
def get_doc(sents):
    '''
    this function splits the text into sentences and
    considering each sentence as a document,
    calculates the total word count of each
    '''
    doc_info = []
    i = 0
    for sent in sents:
        i += 1
        count = count_words(sent)
        temp = {'doc_id' : i, 'doc_length' : count}
        doc_info.append(temp)
    return doc_info
doc_info = get_doc(text_sents_clean)
print(doc_info)

[{'doc_id': 1, 'doc_length': 11}, {'doc_id': 2, 'doc_length': 8}, {'doc_id': 3, 'doc_length': 20}]
  • count_words() function
def count_words(sent):
    '''
    This function returns the
    total number of words in the input text.
    '''
    count = 0
    words = word_tokenize(sent)
    for word in words:
        count += 1
    return count
  • document 안에 있는 동일 단어 빈도 count 함수
def create_freq_dict(sents):
    '''
    This function creates a frequency dictionary
    for each word in each document.
    '''
    i = 0
    freqDict_list = []
    for sent in sents:
        i += 1
        freq_dict = {}
        words = word_tokenize(sent)
        for word in words:
            word = word.lower()
            if word in freq_dict:
                freq_dict[word] += 1
            else:
                freq_dict[word] = 1
            temp = {'doc_id' : i, 'freq_dict' : freq_dict}
        freqDict_list.append(temp)
    return freqDict_list
freqDict_list = create_freq_dict(text_sents_clean)
print(freqDict_list)

[{'doc_id': 1, 'freq_dict': {'if': 1, 'you': 1, 'like': 1, 'tuna': 1, 'and': 1, 'tomato': 1, 'sauce': 1, 'try': 1, 'combinaning': 1, 'the': 1, 'two': 1}}, {'doc_id': 2, 'freq_dict': {'its': 1, 'really': 1, 'not': 1, 'as': 2, 'bad': 1, 'it': 1, 'sounds': 1}}, {'doc_id': 3, 'freq_dict': {'if': 1, 'the': 2, 'easter': 1, 'bunny': 1, 'and': 2, 'tooth': 1, 'fairy': 1, 'had': 1, 'babies': 1, 'would': 1, 'they': 1, 'take': 1, 'your': 1, 'teeth': 1, 'leave': 1, 'chocolate': 1, 'for': 1, 'you': 1}}]
  • TF 계산
def computeTF(doc_info, freqDict_list):
    """
    tf = (frequency of the term in the doc / total number of term in the doc)
    """
    TF_scores = []
    for tempDict in freqDict_list:
        id = tempDict['doc_id']
        for k in tempDict['freq_dict']:
            temp = {'doc_id' : id,
                    'TF_score' : tempDict['freq_dict'][k]/doc_info[id-1]['doc_length'],
                    'key' : k}
            TF_scores.append(temp)
    return TF_scores
TF_scores = computeTF(doc_info, freqDict_list)
for i in TF_scores:
    print(i)

    {'doc_id': 1, 'TF_score': 0.09090909090909091, 'key': 'if'}
    {'doc_id': 1, 'TF_score': 0.09090909090909091, 'key': 'you'}
    {'doc_id': 1, 'TF_score': 0.09090909090909091, 'key': 'like'}
    {'doc_id': 1, 'TF_score': 0.09090909090909091, 'key': 'tuna'}
    {'doc_id': 1, 'TF_score': 0.09090909090909091, 'key': 'and'}
    {'doc_id': 1, 'TF_score': 0.09090909090909091, 'key': 'tomato'}
    {'doc_id': 1, 'TF_score': 0.09090909090909091, 'key': 'sauce'}
    {'doc_id': 1, 'TF_score': 0.09090909090909091, 'key': 'try'}
    {'doc_id': 1, 'TF_score': 0.09090909090909091, 'key': 'combinaning'}
    {'doc_id': 1, 'TF_score': 0.09090909090909091, 'key': 'the'}
    {'doc_id': 1, 'TF_score': 0.09090909090909091, 'key': 'two'}
    {'doc_id': 2, 'TF_score': 0.125, 'key': 'its'}
    {'doc_id': 2, 'TF_score': 0.125, 'key': 'really'}
    {'doc_id': 2, 'TF_score': 0.125, 'key': 'not'}
    {'doc_id': 2, 'TF_score': 0.25, 'key': 'as'}
    {'doc_id': 2, 'TF_score': 0.125, 'key': 'bad'}
    {'doc_id': 2, 'TF_score': 0.125, 'key': 'it'}
    {'doc_id': 2, 'TF_score': 0.125, 'key': 'sounds'}
    {'doc_id': 3, 'TF_score': 0.05, 'key': 'if'}
    {'doc_id': 3, 'TF_score': 0.1, 'key': 'the'}
    {'doc_id': 3, 'TF_score': 0.05, 'key': 'easter'}
    {'doc_id': 3, 'TF_score': 0.05, 'key': 'bunny'}
    {'doc_id': 3, 'TF_score': 0.1, 'key': 'and'}
    {'doc_id': 3, 'TF_score': 0.05, 'key': 'tooth'}
    {'doc_id': 3, 'TF_score': 0.05, 'key': 'fairy'}
    {'doc_id': 3, 'TF_score': 0.05, 'key': 'had'}
    {'doc_id': 3, 'TF_score': 0.05, 'key': 'babies'}
    {'doc_id': 3, 'TF_score': 0.05, 'key': 'would'}
    {'doc_id': 3, 'TF_score': 0.05, 'key': 'they'}
    {'doc_id': 3, 'TF_score': 0.05, 'key': 'take'}
    {'doc_id': 3, 'TF_score': 0.05, 'key': 'your'}
    {'doc_id': 3, 'TF_score': 0.05, 'key': 'teeth'}
    {'doc_id': 3, 'TF_score': 0.05, 'key': 'leave'}
    {'doc_id': 3, 'TF_score': 0.05, 'key': 'chocolate'}
    {'doc_id': 3, 'TF_score': 0.05, 'key': 'for'}
    {'doc_id': 3, 'TF_score': 0.05, 'key': 'you'}  
  • IDF 계산
def computeIDF(doc_info, freqDict_list):
    '''
    idf = ln(total number of docs / number of docs with term in it)
    '''
    IDF_scores = []
    counter = 0
    for dict in freqDict_list:
        counter += 1
        for k in dict['freq_dict'].keys():
            count = sum([k in tempDict['freq_dict'] for tempDict in freqDict_list])
            temp = {'doc_id' : counter, 'IDF_score' : math.log(len(doc_info)/count), 'key' : k}

            IDF_scores.append(temp)

    return IDF_scores
IDF_scores = computeIDF(doc_info, freqDict_list)
print(IDF_scores)

[{'doc_id': 1, 'IDF_score': 0.4054651081081644, 'key': 'if'},
 {'doc_id': 1, 'IDF_score': 0.4054651081081644, 'key': 'you'},
 {'doc_id': 1, 'IDF_score': 1.0986122886681098, 'key': 'like'},
 {'doc_id': 1, 'IDF_score': 1.0986122886681098, 'key': 'tuna'},
 {'doc_id': 1, 'IDF_score': 0.4054651081081644, 'key': 'and'},
 {'doc_id': 1, 'IDF_score': 1.0986122886681098, 'key': 'tomato'},
 {'doc_id': 1, 'IDF_score': 1.0986122886681098, 'key': 'sauce'},
 {'doc_id': 1, 'IDF_score': 1.0986122886681098, 'key': 'try'},
 {'doc_id': 1, 'IDF_score': 1.0986122886681098, 'key': 'combinaning'},
 {'doc_id': 1, 'IDF_score': 0.4054651081081644, 'key': 'the'},
 {'doc_id': 1, 'IDF_score': 1.0986122886681098, 'key': 'two'},
 {'doc_id': 2, 'IDF_score': 1.0986122886681098, 'key': 'its'},
 {'doc_id': 2, 'IDF_score': 1.0986122886681098, 'key': 'really'},
 {'doc_id': 2, 'IDF_score': 1.0986122886681098, 'key': 'not'},
 {'doc_id': 2, 'IDF_score': 1.0986122886681098, 'key': 'as'},
 {'doc_id': 2, 'IDF_score': 1.0986122886681098, 'key': 'bad'},
 {'doc_id': 2, 'IDF_score': 1.0986122886681098, 'key': 'it'},
 {'doc_id': 2, 'IDF_score': 1.0986122886681098, 'key': 'sounds'},
 {'doc_id': 3, 'IDF_score': 0.4054651081081644, 'key': 'if'},
 {'doc_id': 3, 'IDF_score': 0.4054651081081644, 'key': 'the'},
 {'doc_id': 3, 'IDF_score': 1.0986122886681098, 'key': 'easter'},
 {'doc_id': 3, 'IDF_score': 1.0986122886681098, 'key': 'bunny'},
 {'doc_id': 3, 'IDF_score': 0.4054651081081644, 'key': 'and'},
 {'doc_id': 3, 'IDF_score': 1.0986122886681098, 'key': 'tooth'},
 {'doc_id': 3, 'IDF_score': 1.0986122886681098, 'key': 'fairy'},
 {'doc_id': 3, 'IDF_score': 1.0986122886681098, 'key': 'had'},
 {'doc_id': 3, 'IDF_score': 1.0986122886681098, 'key': 'babies'},
 {'doc_id': 3, 'IDF_score': 1.0986122886681098, 'key': 'would'},
 {'doc_id': 3, 'IDF_score': 1.0986122886681098, 'key': 'they'},
 {'doc_id': 3, 'IDF_score': 1.0986122886681098, 'key': 'take'},
 {'doc_id': 3, 'IDF_score': 1.0986122886681098, 'key': 'your'},
 {'doc_id': 3, 'IDF_score': 1.0986122886681098, 'key': 'teeth'},
 {'doc_id': 3, 'IDF_score': 1.0986122886681098, 'key': 'leave'},
 {'doc_id': 3, 'IDF_score': 1.0986122886681098, 'key': 'chocolate'},
 {'doc_id': 3, 'IDF_score': 1.0986122886681098, 'key': 'for'},
 {'doc_id': 3, 'IDF_score': 0.4054651081081644, 'key': 'you'}]
  • TFIDF 계산
def computeTFIDF(TF_socres, IDF_scores):
    TFIDF_scores = []
    for j in IDF_scores:
        for i in TF_scores:
            if j['Key'] == i['key'] and j['doc_id'] == i['doc_id']:
                temp = {'doc_id' : j['doc_id'],
                       'TFIDF_scores' : j['IDF_score'] * i['TF_score'],
                       'key' : i['key']}
        TFIDF_scores.append(temp)
    return TFIDF_scores

REFERENCE

paper: Using TF-IDF to Determin Word Relevance in Document Queries

  • high TF-IDF numbers imply a strong relationship with the document they appear in
  • tf-idf가 높은 단어가 있는 문서는 읽는이에게 관심이 높은 문서일 가능성이 있다.
  • naive can result that it approach only to make high sum of f(w,d) - 한 문서에 존재하는 단어 수
  • tf-idf: considering both f(w,d) and f(w,D) results high quality.
  • Clearly, IF-IDF is much more powerful that its naive counterpart
  • Have limitation : ex) synonyms and (dru <-> drugs) - For large document collections, this could present an escalating problem actiav

Categories:

Updated:

Leave a comment