TFIDF와 코사인 유사도를 통한 문서 추천하기

허준현·2021년 8월 8일
1

DeepLearning

목록 보기
1/5
post-thumbnail

학습 계기

현재 가지고 있는 일기 데이터를 통해서 자신이 작성한 일기와 유사한 일기를 추천하는 시스템을 만들고자 하였습니다. TFIDF로 유사도를 측정하면서 문제점은 또 어떠한 것이 있는지 알아보겠습니다.

가상환경 설정하기

먼저 프로젝트에 사용하는 모듈을 불러오기 위해서 가상환경을 설정해 줍니다.

python3 -m venv venv

현재 프로젝트에서 sklearn 과 전처리를 위한 mecab을 설치합니다.
mecab은 맥에서 잘 돌아가서 맥에서 진행하였습니다.
추가적인 모듈은 밑에 깃헙 requirements.txt 를 확인하면 되겠습니다.

데이터 전처리 하기

우선적으로 전에 가져왔던 일기 데이터에 정규 표현식을 사용하여 한글, 숫자, 영어를 제외하였습니다.

그리고 mecab을 활용하여 토큰나이저를 따로 만들었으며
명사,동사,형용사만을 추출하였습니다.
그리고 별도의 불용어 100개의 사전을 참고하였으며 단어 사전을 보면서 추가하고 있습니다.

def tokenizer(raw, pos=["NNG","NNP","VV","VA"], stopword=stopwords): 
from konlpy.tag import 
Mecab m = Mecab() 
return [word for word, tag in m.pos(raw) if len(word) > 1 and tag in pos and word not in stopword ]

TFIDF 사용하기

그리고 사이컷런에서 제공하는 tfidf vectorizer을 생성합니다.

tf = TfidfVectorizer(
    stop_words=stopwords, tokenizer=tokenizer, ngram_range=(1, 2), max_features=200000, sublinear_tf=True)

위에서 생성한 vectorizer을 인스턴스화 하고 shape를 살펴보면 2개의 컬럼으로 이루어져 있는 것을 알 수 있습니다. ex) (9321,301506)
이는 coo_matrix에서 개선된 매트릭스 이며 앞에 9321는 학습한 문자의 수 , 뒤 301506은 학습에 사용된 단어 갯수라는 것을 알 수 있습니다.

생성된 인스턴스 및 vectorizer 저장

일기 데이터를 처음에 수집한 데이터로 학습하되 추가로 학습을 하기 위해서는 vectorizer 객체와 인스턴스 한 것을 저장해야 합니다.
저는 파이썬 내부 라이브러리로 제공하는 pickle을 사용하였습니다.

X = tf.fit_transform(article)
    tf.n_docs = len(article)
    with open(mtx_path, "wb") as fw:
        pickle.dump(X, fw)
    with open(tf_path, "wb") as fw:
        pickle.dump(tf, fw)

이 후 추가적인 데이터가 들어왔을 때 객체를 다시 불러오면 되겠습니다.

with open(mtx_path, "rb") as fr:
        X = pickle.load(fr)
    with open(tf_path, "rb") as fr:
        tf = pickle.load(fr)

새로운 데이터와 비교하기

이 후 새로 들어온 일기에 대해서 위와 동일하게 전처리를 하고 저장한 vectorizer에서 transform을 하여 기존의 저장한 사전으로 백터를 생성 하도록 합니다.

example_vector = tf.transform([more])
    tf.partial_fit([more])

여기서 주의해야 할 점은 위에서 말한 것처럼 transform 을 사용하는 것이 아니라 처음에 학습 할 때처럼 fit_transform을 사용하게 된다면 기존의 사전을 사용하는 것이 아니기 때문에 주의 하셔야 합니다. :)

def recommand(more):
    with open(mtx_path, "rb") as fr:
        X = pickle.load(fr)
    with open(tf_path, "rb") as fr:
        tf = pickle.load(fr)
    # X = mmread(mtx_path).tocsr()
    example_vector = tf.transform([more])
    tf.partial_fit([more])
    with open(tf_path, "wb") as fw:
        pickle.dump(tf, fw)

    cos_similar = linear_kernel(example_vector, X).flatten()

    sim_rank_idx = cos_similar.argsort()[::-1]
    sim_rank_idx = sim_rank_idx[:5]
    for i in sim_rank_idx:
        print(cos_similar[i])

위에서 보이는 것과 같이 transform을 하고 나서 변경되 vectorizer를 저장해주며 추가적으로 X데이터를 추후에도 비교할 예정이면 X데이터도 수정 해주어야 합니다. X.vocabulary 값을 example_vector.vocabulary 값으로 변경해주어야 하며 X 데이터에 vstack을 사용하여 문장의 갯수도 추가해주면 됩니다.

추가 개선사항 생각하기

처음에는 새로 생긴 일기 데이터가 10개 이상 들어오게 되면 fit_transform 을 이용하여 다시 학습을 하려고 하였습니다. 하지만 기존에 학습했던 데이터에 대해서 다시 학습을 하기 때문에 이는 비효율적이었습니다. stackoverflow를 찾아보던 중 좋은 코드가 있어서 링크 남깁니다.
https://stackoverflow.com/questions/39109743/adding-new-text-to-sklearn-tfidif-vectorizer-python

import re
import numpy as np
from scipy.sparse.dia import dia_matrix
def partial_fit(self, X):
    max_idx = max(self.vocabulary_.values())
    for a in X:
        # update vocabulary_
        
        tokens = re.findall(self.token_pattern, a)
        for w in tokens:
            if w not in self.vocabulary_:
                max_idx += 1    
                self.vocabulary_[w] = max_idx

        # update idf_
        df = (self.n_docs + self.smooth_idf) / \
            np.exp(self.idf_ - 1) - self.smooth_idf
        self.n_docs += 1
        df.resize(len(self.vocabulary_))
        for w in tokens:
            df[self.vocabulary_[w]] += 1
        idf = np.log((self.n_docs + self.smooth_idf) /
                     (df + self.smooth_idf)) + 1
        self._tfidf._idf_diag = dia_matrix(
            (idf, 0), shape=(len(idf), len(idf)))

우리는 한글 데이터를 사용하기 때문에 대소문자 변경 코드만 바꾸고 적용하였습니다. 이를 사용하면 새로 들어온 데이터만 추가적으로 vocabulary에 추가 할 수 있게 되었습니다. :0

TFIDF 의 문제점

현재 보는 것과 같이 새로운 글이 들어올때 추가하면 되어서 OOV는 발생하지 않을 것 같습니다. 하지만 단어만 보고 그 속에 있는 비교하지 않는 다는 단점이 있다. 또 한 document의 topic을 아는데 한계가 있으며 동음이의어에 약할 거 같습니다.
처음에 학습할때에는 사이킷런에서 제공하는 TFIDF-Vectorizer 가 word2vec와 차이점이 없는 줄 알았습니다. TFIDF에서도 단어를 word-document 식으로 맵핑하기 때문입니다. 하지만 이는 단어의 순서를 무시하며 순수 단어에 대해서만 평가를 하고 word2vec는 단어 근처에 등장하는 특정 단어에 대한 특수한 백터를 제공합니다. 따라서 TF-IDF는 TDM을 만들 때 사용하며 word2vec야 말로 단어를 백터로 바꾸는 것입니다.

https://www.quora.com/Is-TF-IDF-and-Word2vec-the-same
http://doc.mindscale.kr/km/unstructured/qna11.html

마지막으로 깃허브 주소 및 참고 링크를 남기고 마무리 하겠습니다!

깃허브 주소

https://github.com/denhur62/TFIDF_mecab

참고 자료

https://towardsdatascience.com/tf-term-frequency-idf-inverse-document-frequency-from-scratch-in-python-6c2b61b78558
https://bkshin.tistory.com/entry/NLP-8-%EB%AC%B8%EC%84%9C-%EC%9C%A0%EC%82%AC%EB%8F%84-%EC%B8%A1%EC%A0%95-%EC%BD%94%EC%82%AC%EC%9D%B8-%EC%9C%A0%EC%82%AC%EB%8F%84
https://wikidocs.net/book/2155
https://sikaleo.tistory.com/62

profile
best of best

0개의 댓글