Python과 확률

자연어 처리를 위한 TF-IDF

Yoo Sung Hyun 2021. 12. 19. 22:15
728x90

막연하게 나이브 베이즈 분류로 무언가 주제를 잡으려고 하던 와중에, 조금 생각해보니 기왕이면 Counter 기반 말고, 문장과 단어를 더 잘 표현할 수 있는 방법이 무엇이 있을까? 하다가 잠깐 들르게 되었다.

 

일단 최초 목적은 Native Python으로 TF-IDF를 구현하려고 했던 것이 었으나, 간단하다면 간단할 수 있는 예제로도 Native Python은 시간이 너무 오래걸려 동작하지 않았다.

from sklearn.datasets import fetch_20newsgroups
newsdata = fetch_20newsgroups(subset='train')
print(newsdata.keys())

뉴스 카테고리 분류를 위한, 10000개정도 docs를 처리하는데도 한 세월 걸린다.

그도 그럴게, TF-IDF를 처리하는 순서를 생각해보면,

    1. 등장 가능한 Vocab을 먼저 찾는다. (전체 docs에 대한 for문 중첩 최소 1회 이상 필요) 최소 O(N)

    2. (TF) Vocab이 각 문서에서 얼마나 등장했는지를 찾는다.

    (전체 docs 수 X 각 docs의 단어 수 X 각 Vocab수 + 비교하는데 드는 if문 공수 등) 최소 O((NxMxV)+@)

        => 이 부분에서 일단 시간소요가 무척오래걸린다.

    3. (IDF) Vocab이 등장한 문서를 찾아 log 계산을 취한다.

    (전체 Vocab 수 X 전체 docs 수 X 각 docs의 단어 수 + 비교하는데 드는 if문 공수 등) 최소 O((VxNxM)+@)

        => 2번이랑 사실상 동일하다.

    4. (TF-IDF) TF*IDF

        => TF와 IDF의 길이는 동일할거고 각 곱셈만 수행하면 되므로 선형 시간 소비 O(N)

docs 10000개에 vocab이 16만개 정도 나왔던거 같고, 각 문장의 단어수는 아무리 짧아도 5글자 이상은 될테니,

10억 이상의 시간이 소비되므로, 정말 오래 걸릴 수 밖에 없다....ㅋㅋㅋ

 

Scikit-Learn Git을 찾아봤는데, 구하는 방식은 위키의 예제와 동일 한 것 같으나, Numpy나 Scipy를 이용해서 훨씬 효과적으로 구하고, 여러가지 성능향상을 위한 방법들이 있는 것 같은데, 당장 굵직굵직한 내용만 봤을 때 방식과 사상은 동일한 것 같으니, Native Python으로 작성하는 것은 맛만 보도록 하고, 넘어가도록 하겠다.

(아마 딥러닝 코테같은 곳에서 TF-IDF를 구하라고 한다면, Vocab의 종류가 매우 제약적이거나, docs가 제약적이지 않을까 사료되며, Numpy나 Scipy정도는 사용할 수 있는 환경으로 주지 않을까 싶다. Counter나 defaultdict를 이용해서 최대한 구해보는 식으로 짜야할 듯 싶다.)

https://github.com/scikit-learn/scikit-learn/blob/0d378913be6d7e485b792ea36e9268be31ed52d0/sklearn/feature_extraction/text.py#L1184

 

GitHub - scikit-learn/scikit-learn: scikit-learn: machine learning in Python

scikit-learn: machine learning in Python. Contribute to scikit-learn/scikit-learn development by creating an account on GitHub.

github.com

 

일단 내가 참고한 위키는 아래와 같다.

https://wikidocs.net/31698

 

4) TF-IDF(Term Frequency-Inverse Document Frequency)

이번 챕터에서는 DTM 내에 있는 각 단어에 대한 중요도를 계산할 수 있는 TF-IDF 가중치에 대해서 알아보도록 하겠습니다. TF-IDF를 사용하면, 기존의 DTM을 사용 ...

wikidocs.net

해당 위키의 소스만 떼놓고 보면 아래와 같다.

docs = [
  '먹고 싶은 사과',
  '먹고 싶은 바나나',
  '길고 노란 바나나 바나나',
  '저는 과일이 좋아요'
] 
vocab = list(set(w for doc in docs for w in doc.split()))
vocab.sort()

N = len(docs) # 총 문서의 수

from math import log

def tf(t, d):
    return d.count(t)

def idf(t):
    df = 0
    for doc in docs:
        df += t in doc
    return log(N/(df + 1))

def tfidf(t, d):
    return tf(t,d)* idf(t)

tf_result = []
for i in range(N): # 각 문서에 대해서 아래 명령을 수행
    tf_result.append([])
    d = docs[i]
    for j in range(len(vocab)):
        t = vocab[j]        
        tf_result[-1].append(tf(t, d))
print(vocab)
print(tf_result)

idf_result = []
for j in range(len(vocab)):
    t = vocab[j]
    idf_result.append(idf(t))
    
print(vocab)
print(idf_result)

tfidf_result = []
for i in range(N):
    tfidf_result.append([])
    d = docs[i]
    for j in range(len(vocab)):
        t = vocab[j]

        tfidf_result[-1].append(tfidf(t,d))

print(vocab)
print(tfidf_result)

Counter 기반으로만 구하면, 그냥 무지성으로 가장 많이 나온 단어라면 가장 중요하다고 생각할 것이다. (물론 상황이 그렇다면 Counter기반을 써도 좋다.)

TF-IDF의 사상은 이렇다.

단어가 아무리 많이 나온들, 평이하게 여러 문서에서 중복되는 단어라면, 사실 중요하지 않은 것 아닐까?

TF는 문서에서의 단어의 빈도다. (Counter로도 충분히 구할 수 있다.)

다만 Key가 vocab_list가 되어야 한다는 것만 기억한다.

 

IDF는 TF에 가중치로 부여할 조절값이라고 보면된다. (여러 문서에서 나오면 중요도를 낮춰버리자!)

IDF를 구하는 방법론에는 여러가지가 있으며, 만약 임베딩 등을 쓰지 않고, TF-IDF로 통계적 접근을 하기 바란다면, 여러가지 방법론을 찾아보는 것도 좋겠다.

평이하게 알려진 방법으로는,

log(문서수/해당 vocab가들어간 문서수)

로 표현되는데, 통상 0으로 나눠지는 것과 같은 처리를 하기위해 라플라스 스무딩 기법 등을 사용한다. (라플라스 스무딩이래봐야 그냥 1 더해줘서 0 안나오게 방지하는 방식이다. Scikit-Learn Default)

log는 좀 상세하게 알면 좋긴한데, 일단 여기서 사용된 방식으로는 분모나 분자가 무자비하게 커서, 값이 너무 작아지거나 너무 커지는 것을 방지하기 위해 스케일링을 위해서 사용되었다.

 

TF와 IDF를 곱함으로써, 많은 문서에서 나오는 단어는 가중치(IDF)를 일단 낮게 주지만, 문서에서 나온 TF의 갯수에 곱하게 되므로써, 대중적인 단어일 수록, 더 많이 언급되어야 중요하다고 생각하고, 대중적이지 않은 단어는 비교적 덜 언급되어도 중요하게 생각하게 된다.

 

문제에 따라 적용할지 말지 의사결정의 차이일 것 같긴한데, 일단 몇가지 분명한 단점은 있다.

    1. 단어가 나온 위치나, 앞뒤 관계, 문맥, 문서상의 순서 등을 전혀 고려하지 못한다.

        => 순수하게 횟수에만 기반한다는 것은 한국어에서 양날의 검이 될 수 있다.

    2. 머신러닝, 딥러닝, 기계학습과 같은 단어는 다른 단어 취급이 되어버린다.

        => 1번 문제에서 파생되는 것이긴 하나, 위 3개의 글자는 유사도가 0이 되어버린다.

    3. Data Size문제

        => 위에서 얘기한 것 처럼, 시간 소모가 엄청 크고, Data가 적다면 제대로 동작하지 않을 지도 모른다.

 

그래도 지금도 거론되며, 간혹 실무에서 사용도 되는 이유는, 직관적이고 간단하며, 문제가 복잡하지 않은 케이스에 쉽게 사용해볼 수 있기 때문은 아닐까?

 

https://jiho-ml.com/weekly-nlp-2/

 

Week 2 - 단어를 가방에 때려 넣으면 문장이 된다

단어가 모여 문장을 이루고, 문장이 모여 문서를 이룹니다. 문장을 이해하는데 단어의 순서가 중요할까요?

jiho-ml.com

 

Scikit-Learn에서 제공하는 TFIDFTransformer나 CounterVectorizer 등이 있으니, 나이브 베이즈 분류를 하기 위한 데이터 전처리로는, 개념은 현재 잡았으니, 위의 방법들로 진행하도록 하겠다.

728x90