본문 바로가기

Data Science/추천시스템과 NLP

[NLP] LDA를 활용한 Topic Modeling 구현하기

반응형

이번 포스팅에서는 주어진 수많은 단어들을 기반으로 토픽(Topic) 별 단어들의 분포를 확인하고 또 문서(Document)별 토픽들의 분포를 계산해서 문서들의 주제가 무엇인지 예측하는 Topic Modeling에 대해 다루려고 한다. 해당 포스팅은 토픽모델링에 대한 개념적인 깊이보다 파이썬으로 구현하는 내용에 초점이 맞추어져 있으므로 토픽 모델링, 그중에서도 LDA(Latent Dirichlet Allocation)의 이론에 대해 알고 싶다면 여기를 참고하자. 목차는 다음과 같다.

 

1. Topic Modeling의 종류

2. Python으로 LDA 구현해보기

1. Topic Modeling의 종류

토픽 모델링은 크게 확률에 기반한 모델행렬분해에 기반한 모델 2가지로 나뉘어 진다. 간단하게 종류에 대해 알아보자. 단, 이번 포스팅에서는 LDA(Latent Dirichlet Allocation)에 대해서만 다룬다. 여기서의 LDA가 차원을 축소해 클래스를 잘 분류해주는 선형을 찾아주는 LDA(Linear Discriminant Analysis) 즉, 선형판별분석과는 완전 다른 개념이다.(잡담이지만 두 개의 개념을 구분해주는 또 하나의 차이점은 선형판별분석은 Fisher라는 사람이 고안해낸 방법이라는 것이다.)

 

https://towardsdatascience.com/topic-modeling-with-nlp-on-amazon-reviews-an-application-of-latent-dirichlet-allocation-lda-ae42a4c8b369

 

  • 행렬 분해 기반의 토픽 모델링
    • LSA(Latent Semantic Analysis) 
    • NMF(Non-negative Matrix Factorization)
  • 확률 기반의 토픽 모델링
    • pLSA(Probabilistic Latent Semantic Analysis)
    • LDA(Latent Dirichlet Allocation)

이 중 LDA모델은 문서별 단어 분포만을 가지고 Document-Term 행렬을 만들어 베이즈 추론을 이용해 '토픽별 단어 분포' 와 '문서별 토픽 분포' 이 2가지를 알아내야 한다. 베이즈 추론을 사용할 때 사전 확률(Prior)분포로 사용하는 것이 디리클레 분포(Drichlet Distribution)이다. 이외에 LDA의 구성요소에 여러가지가 존재하며 이 구성요소들을 이용해서 어떻게 결과를 도출하는지는 인트로에 언급했던 LDA 이론 링크를 참고하자.

 

간단하게 LDA의 수행 프로세스에 대해 알아보자. 

 

  1. 단순 Count 기반 Document-Term 행렬을 생성 : 주어진 단어들의 빈도수에 기반하므로 Tf-idf 방법이 아닌 Count에 기반한다.
  2. 토픽의 개수를 사전에 설정 
  3. 각 단어들을 임의의 토픽으로 최초 할당한 후 문서별 토픽 분포와 토픽별 단어 분포가 결정이 된다.
  4. 특정 단어를 하나 추출하고 추출한 해당 단어를 제외하고 문서의 토픽 분포와 토픽별  단어 분포를 다시 계산한다.(이 과정을 깁스 샘플링이라고 한다.) 그리고 추출된 단어는 새롭게 토픽 할당 분포를 계산한다.
  5. 다른 단어를 추출하고 4번 단계를 다시 수행한다. 그리고 또 다른 단어를 추출하고 계속적으로 모든 단어들이 재 계산되도록 반복한다.
  6. 지정된 반복 횟수(하이퍼파라미터로 지정)만큼 4,5번 단계를 수행하면서 모든 단어들의 토픽 할당 분포가 변경되지 않고 수렴할 때까지 수행한다.

2. Python으로 LDA 구현하기

Scikit-learn에서는 LDA의 API를 제공한다. 간단하게 API의 파라미터의 의미에 대해서 알아보자.

 

  • n_components : 사전에 설정해주는 토픽의 개수
  • doc_topic_prior : 문서의 토픽 분포의 초기 하이퍼파라미터
  • topic_word_prior : 토픽의 단어 분포의 초기 하이퍼파라미터
  • max_iter : 위 LDA 수행 프로세스에서 6번에 해당하는 '지정된 반복횟수'가 이것을 의미

참고로 doc_topic_prior과 topic_word_prior 라는 하이퍼파라미터의 값에 따라 어떻게 변화하는지에 대한 내용도 인트로 이론 링크에 간단하게 들어가 있다. 해당 내용을 참고해보자.

 

그러면 이제 Python을 이용해서 본격적으로 LDA를 구현해보자. 데이터는 이전 포스팅에서 텍스트 분류 문제로 활용했던 Sklearn의 내장 데이터인 fetch_20newsgroup 데이터를 활용했다. 참고로 해당 데이터에서 8개의 카테고리에 대한 문서들만 추출했다.

 

from sklearn.datasets import fetch_20newsgroups
# LDA는 빈도수에만 기반하는 CountVectorizer사용함!
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.decomposition import LatentDirichletAllocation

# 주어진 데이터셋의 일부 카테고리 데이터만 추출하므로 카테고리 사전에 설정
cats = ['rec.motorcycles', 'rec.sport.baseball', 'comp.graphics', 'comp.windows.x',
        'talk.politics.mideast', 'soc.religion.christian', 'sci.electronics', 'sci.med'  ]
# 설정해준 카테고리의 데이터들만 추출
news_df = fetch_20newsgroups(subset='all', remove=('headers','footers','quotes'),
                            categories=cats, random_state=12)
# CountVectorizer로 텍스트 데이터들 단어 빈도수에 기반해 벡터화시키기(fit_transform까지!)
count_vect = CountVectorizer(max_df=0.95, max_features=1000,
                            min_df=2, stop_words='english',
                            ngram_range=(1,2))
ftr_vect = count_vect.fit_transform(news_df.data)
# LDA클래스를 이용해서 피처 벡터화시킨 것을 토픽모델링 시키기
# 8개의 주제만 뽑았으니 n_components(토픽개수) 8로 설정
lda = LatentDirichletAllocation(n_components=8, random_state=42)
lda.fit(ftr_vect)
# components_속성은 8개의 토픽별(row)로 1000개의 feature(단어)들의 분포수치(column)를 보여줌
print(lda.components_.shape)
print(lda.components_)

 

Sklearn에서 제공하는 LDA는 fit과 transform 함수를 제공하는데, fit 호출까지는 '토픽별 단어들의 분포'를 반환해주고 transform 호출까지하게 된다면 '문서별 토픽들의 분포' 까지 계산해 반환해준다. 위 코드는 '토픽별 단어들의 분포' 까지만 반환해주고 어떻게 우리에게 시각적으로 제공해주는지 살펴보자. 위 코드의 결과값은 다음과 같다.

 

토픽별 단어들의 분포

 

shape가 8개의 행, 1000개의 칼럼으로 이루어진 array를 반환했다. 여기서각각의 토픽, 단어들을 벡터화시킨 feature들이다.

이제 토픽별로 어떤 단어들이 많이 분포하는지 시각적으로 보기 편하게 하도록 함수 하나를 개별적으로 정의했다. 

 

# 이 때 lda_model이란, 벡터화시킨 텍스트 데이터를 fit까지만 적용한 모델!
def display_topic_words(lda_model, feature_names, num_top_words):
    for topic_idx, topic in enumerate(lda_model.components_):
        print('\nTopic #', topic_idx+1)
        
        # Topic별로 1000개의 단어들(features)중에서 높은 값 순으로 정렬 후 index를 반환해줌!
        # argsort()는 디폴트가 오름차순임(1,2,3,...) 그래서 [::-1]로 내림차순으로 바꿔주기
        topic_word_idx = topic.argsort()[::-1]
        top_idx = topic_word_idx[:num_top_words]
        
        # CountVectorizer함수 할당시킨 객체에 get_feature_names()로 벡터화시킨 feature(단어들)볼 수 있음!
        # 이 벡터화시킨 단어들(features)은 숫자-알파벳순으로 정렬되며, 단어들 순서는 fit_transform시키고 난 이후에도 동일!
        # '문자열'.join 함수로 특정 문자열 사이에 끼고 문자열 합쳐줄 수 있음.
        feature_concat = '+'.join([str(feature_names[i])+'*'+str(round(topic[i], 1)) for i in top_idx])
        print(feature_concat)        
feature_names = count_vect.get_feature_names()
display_topic_words(lda, feature_names, 15)

 

결과값은 다음과 같다.

 

토픽별 단어들의 분포

 

위 결과값에서 Topic # 3에 해당하는 단어들의 분포를 보자. 가장 많이 분포하는 상위 단어들 중 'medical', 'health', 'research','disease', 'cancer' 의 단어를 볼 수 있다. 따라서 이러한 단어들로 Topic # 3은 '의학'과 관련된 주제일 것임을 추론해볼 수 있다. 하지만 다른 토픽들, 단적인 예로 Topic # 1이나 Topic # 4를 보게 되면 일명 '불용어'라고 일컬어지는 단어들이 많이 등장하기 때문에 이 단어들만을 보고 어떤 주제인지 추론하기가 힘들다. 

 

이제 transform 함수를 호출해서 '문서별 토픽들의 분포' 까지 살펴보자. 코드는 첫 번째 코드블럭에 이어 작성해주면 되겠다.

 

# transform까지 수행하면, 문서별(row)로 토픽들(column)의 분포를 알려줌
doc_topics = lda.transform(ftr_vect)
print(doc_topics.shape)
print(doc_topics[:2])

문서별 토픽들의 분포는 어떤 형태로 나오는지 살펴보자.

 

문서별 토픽들의 분포

 

7862개의 행과 8개의 열로 구성되어 있는 array다. 여기서 '각 문서들'을, '각 토픽들'을 의미한다. 이제 그렇다면 주어진 내장 데이터의 특성상  각 문서들의 카테고리 즉, 실제 무슨 내용으로 구성되어 있는 문서들인지 알기 때문에 이것들을 기반으로 Topic # 1 부터 Topic # 8까지 각각 어떤 내용인지 추론해볼 수 있다.(실제로는 이렇지 않음을 기억하자!)

 

import pandas as pd
# 주어진 내장 텍스트데이터의 문서이름에는 카테고리가 labeling되어있음. 
# 따라서, 카테고리가 무엇인지 아는 상태이니까 어떤 문서들이 어떤 토픽들이 높은지 확인해보자.
# 그리고 그 토픽들이 각각 무엇을 내용으로 하는지 추측해보자.
# 주어진 데이터셋의 filename속성을 이용해서 카테고리값들 가져오기
def get_filename_list(newsdata):
    filename_lst = []
    for file in newsdata.filenames:
        filename_temp = file.split('/')[-2:]
        filename = '.'.join(filename_temp)
        filename_lst.append(filename)
    return filename_lst
 
filename_lst = get_filename_list(news_df)
# Dataframe형태로 만들어보기
topic_names = ['Topic #'+ str(i) for i in range(0,8)]
topic_df = pd.DataFrame(data=doc_topics, columns=topic_names,
                       index=filename_lst)
print(topic_df.head(20))

결과값은 다음과 같다.

 

각 토픽은 어떤 주제를 하고 있는걸까?

 

첫 번째 행을 살펴보자. 첫 번째 행의 인덱스는 해당 문서의 내용을 의미한다. 즉, 컴퓨터 그래픽에 관한 내용이다. 첫 번째 행의 칼럼값들을 살펴보면 Topic # 1 과 Topic # 5 두 개의 내용이 주를 이룬다는 것을 알 수 있고 각각의 주제가 '컴퓨터', '그래픽'에 관련된 주제임을 추론해볼 수 있다.

 

지금까지 확률에 기반한 토픽 모델링의 종류로서 LDA를 구현하는 방법에 대해 알아보았다. LDA를 통해 방대한 양의 문서를 주제별로 쉽게 분류할 수 있지만 추출된 토픽을 사람이 주관적으로 추론해야 한다는 점최적의 초기 하이퍼파라미터값 설정하는 문제Document-term 행렬 필터링 최적화에 문제가 있는 단점들도 존재한다.

반응형