[ Topic Modeling ]

( 참고 : “딥러닝을 이용한 자연어 처리 입문” (https://wikidocs.net/book/2155) )

Topic Modeling은 말 그대로 글 속의 숨겨진 “주제”를 통계적인 기법을 사용해서 찾아내는 것이라고 할 수 있다. 이러한 Topic Modeling의 대표적인 방법으로는 크게 (1) LSA (Latent Semantic Analysis, 잠재의미분석)와, (2) LDA (Latent Dirichlet Allocation)가 있다.

1. LSA (Latent Semantic Analysis)

1) Introduction

  • Topic Modeling에 아이디어를 제공한 알고리즘
  • 등장 배경 : DTM(Document-Term Matrix), TF-IDF(Term-Frequency Inverse Document Matrix) 등은 ‘단어의 빈도 수’를 기반으로 하지만, ‘단어의 의미’는 고려하지 못함. 그래서 이를 위한 대안으로, DTM의 잠재된(latent) 의미를 끌어내는 방법으로서 등장!
  • 추후, LDA가 LSA의 단점을 보완

2) Singular Value Decomposition

  • LSA의 경우, SVD ( Singular Value Decomposition, 특이값 분해 )를 이용한다

SVD (Singular Value Decomposition)

A가 mxn의 행렬일 때, 다음과 같이 3개의 행렬 곱으로 분해하는 것!

( \(A = UDV^T\) , 자세한 내용은 googling! )

3) Truncated SVD

  • Full SVD에서 나온 3개의 행렬에서, 일부 벡터들을 삭제시킨 ‘Truncated SVD(절단된 SVD)”를 사용하여 원래의 행렬을 근사한다.

    ( 대각 행렬 D의 대각 원소의 값 중, 상위값 t개만 남기고 나머지는 지운다 )

    ( + 마찬가지로, U 행렬과 V행렬도 t열까지만 남기고 지운다 )

  • 여기서 ‘t’는 찾고자 하는 Topic의 수를 의미하는 hyperparameter이다

( t를 크게 잡으면 기존의 행렬 A로부터 다양한 의미를 가져갈 수 있지만, t를 작게 잡아야 노이즈를 제거할 수 있다 )

4) SVD with python

import numpy as np
A=np.array([[0,0,0,1,0,1,1,0,0],[0,0,0,1,1,0,1,0,0],[0,1,1,0,2,0,0,0,0],[1,0,0,0,0,0,0,1,1]])
A, A.shape

(array([[0, 0, 0, 1, 0, 1, 1, 0, 0],
        [0, 0, 0, 1, 1, 0, 1, 0, 0],
        [0, 1, 1, 0, 2, 0, 0, 0, 0],
        [1, 0, 0, 0, 0, 0, 0, 1, 1]]), (4, 9))

a. Full SVD

U, s, VT = np.linalg.svd(A, full_matrices = True)
U.shape, s.shape, VT.shape


((4, 4), (4,), (9, 9))
U.round(2)

array([[-0.24,  0.75,  0.  , -0.62],
       [-0.51,  0.44, -0.  ,  0.74],
       [-0.83, -0.49, -0.  , -0.27],
       [-0.  , -0.  ,  1.  ,  0.  ]])
s.round(2)

array([2.69, 2.05, 1.73, 0.77])

b. Truncated SVD

t를 2로 정하여 Full SVD를 truncate

S2 = S[:2,:2]
U2 = U[:,:2]
VT2 = VT[:2,:]


U,S,V가 절단된 것을 확인할 수 있다

U2 

array([[-2.39751712e-01,  7.51083898e-01],
       [-5.06077194e-01,  4.44029376e-01],
       [-8.28495619e-01, -4.88580485e-01],
       [-9.04299898e-17, -4.11929620e-17]])
S2

array([[2.68731789, 0.        ],
       [0.        , 2.04508425]])


5) 실습 (fetch_20newsgroups)

(1) import dataset

import pandas as pd
from sklearn.datasets import fetch_20newsgroups
dataset = fetch_20newsgroups(shuffle=True,random_state=1, remove=('headers','footers','quotes'))
documents = dataset.data

11,314개의 문서가 있음을 확인할 수 있다

len(documents)

그 중, 두 번째 문서의 내용을 확인해보자.

( 불용어, 대/소문자, punctuation 등의 전처리가 필요해보인다. )

documents[1]

"\n\n\n\n\n\n\nYeah, do you expect people to read the FAQ, etc. and actually accept hard\natheism?  No, you need a little leap of faith, Jimmy.  Your logic runs out\nof steam!\n\n\n\n\n\n\n\nJim,\n\nSorry I can't pity you, Jim.  And I'm sorry that you have these feelings of\ndenial about the faith you need to get by.  Oh well, just pretend that it will\nall end happily ever after anyway.  Maybe if you start a new newsgroup,\nalt.atheist.hard, you won't be bummin' so much?\n\n\n\n\n\n\nBye-Bye, Big Jim.  Don't forget your Flintstone's Chewables!  :) \n--\nBake Timmons, III"

문서의 주제들은 아래와 같이 다양하다.

dataset.target_names

['alt.atheism',
 'comp.graphics',
 'comp.os.ms-windows.misc',
 'comp.sys.ibm.pc.hardware',
 'comp.sys.mac.hardware',
 'comp.windows.x',
 'misc.forsale',
 'rec.autos',
 'rec.motorcycles',
 'rec.sport.baseball',
 'rec.sport.hockey',
 'sci.crypt',
 'sci.electronics',
 'sci.med',
 'sci.space',
 'soc.religion.christian',
 'talk.politics.guns',
 'talk.politics.mideast',
 'talk.politics.misc',
 'talk.religion.misc']


(2) Data preprocessing

  • 1) 영어가 아닌 모든 글자들은 공백으로 대체
  • 2) 네 글자 이상의 단어들만 남기고 지우기
  • 3) 모두 소문자

  • 4) **불용어 제거 **( 우선 tokenize해줘야 )

    ( + 다시 detokenize )

news_df = pd.DataFrame({'document':documents})

news_df['clean_doc'] = news_df['document'].str.replace("[^a-zA-Z]", " ")
news_df['clean_doc'] = news_df['clean_doc'].apply(lambda x: ' '.join([w for w in x.split() if len(w)>3]))
news_df['clean_doc'] = news_df['clean_doc'].apply(lambda x: x.lower())

불용어 (stop words) 처리

‘the’,’a’,’she’등. 어디에나 자주 등장하여 주제를 결정하는 데에 영향을 미친다고 보기 어려운 용어들을 제거해준다!

불용어 처리를 위해, 우선 문서의 모든 단어들을 tokenize해준다 ( = 단어 단위로 split한다 )

from nltk.corpus import stopwords

stop_words = stopwords.words('english') # 1) 불용어 설정
tokenized_doc = news_df['clean_doc'].apply(lambda x : x.split()) # 2) tokenize
tokenized_doc = tokenized_doc.apply(lambda x : [item for item in x if item not in stop_words])


(3) TF-IDF matrix

  • 지금까지, 불용어를 제거하기 위해 tokenize했었다

    하지만 TFIDF-Vectorizer는 (token화 되지 않은) text data를 입력으로 받기 때문에, 다시 역토큰화(Detokenization)을 해줘야 한다

detokenized_doc = []

for i in range(len(news_df)):
    t = ' '.join(tokenized_doc[i]) # token화된 단어들을 다시 연결시켜줌
    detokenized_doc.append(t)

news_df['clean_doc'] = detokenized_doc

이렇게 연결된 단어들을 이제 TFIDF Vectorizer에 집어 넣는다.

hyperparameter :

  • max_features = 1000 ( 상위 1000개 단어만 보존한다 )
  • max_df = 0.5 ( 전체 문서의 50% 이상에서 등장하는 단어들은 제외한다 )
  • smooth_idf = True ( smoothing term! document frequency에 +1을 해준다 )
from sklearn.feature_extraction.text import TfidfVectorizer

vectorizer = TfidfVectorizer(stop_words='english',
                            max_features=1000, 
                            max_df = 0.5,
                             smooth_idf=True)

X = vectorizer.fit_transform(news_df['clean_doc'])
X.shape

(11314, 1000)
  • (row) 11314개의 document
  • (col) 상위 1000개의 단어


(4) Topic Modeling

  • 위에서 만들어진 TF-IDF **행렬을, **Truncated SVD를 이용하여 분해한다

  • 원래 기존 news data가 20개의 news category를 가지고 있었기 때문에, 20개의 topic을 가졌다고 가정하고 Topic Modeling을 한다

    ( topic의 숫자는 hyperparameter ‘n_components’로 지정해준다 )

from sklearn.decomposition import TruncatedSVD

svd_model = TruncatedSVD(n_components=20, algorithm='randomized',
                        n_iter=100,random_state=122)
svd_model.fit(X)

20개의 topic이, 1000개의 단어의 일정 비율의 합으로 이루어진 것을 확인할 수 있다!

( 중요한 가정 : “Topic은 단어들의 weighted sum으로 구성된다”)

svd_model.components_.shape

(20, 1000)
terms = vectorizer.get_feature_names()
len(terms)

1000


(5) Result

각각의 (20개의) 주제가 어떠한 단어들로 구성되었는지 확인할 수 있다

def get_topics(components, feature_names, n=5):
    for idx, topic in enumerate(components):
        print("Topic %d :" %(idx+1), [(feature_names[i], topic[i].round(3)) for i in topic.argsort()[:-n-1:-1]])


get_topics(svd_model.components_, terms)
Topic 1 : [('like', 0.214), ('know', 0.2), ('people', 0.193), ('think', 0.178), ('good', 0.151)]
Topic 2 : [('thanks', 0.329), ('windows', 0.291), ('card', 0.181), ('drive', 0.175), ('mail', 0.151)]
Topic 3 : [('game', 0.371), ('team', 0.324), ('year', 0.282), ('games', 0.254), ('season', 0.184)]
....


6) LSA의 장.단점

장점

  • 쉽고 빠르게 구현 가능하다
  • 단어의 잠재적 의미를 이끌어낼 수 있다
  • 문서의 유사도 계산 등에서 좋은 성능을 보인다


단점

  • 새로운 데이터가 가될 경우, 다시 계산해야한다

( 새로운 정보 update가 어려움! )