4 분 소요

텍스트 분석

토픽 모델링(Topic Modeling) - 20 뉴스그룹

토픽 모델링의 이해

토픽 모델링은 문서들에 잠재되어 있는 공통된 토픽(주제)들을 추출해 내는 기법을 의미한다.
공통된 유사성을 도출한다는 측면에서 문서 군집화/유사도와 비슷한 기법일 수 있지만 토픽 모델링은 문서들이 가지는 주요 토픽의 분포도와 개별 토픽이 어떤 의미인지(단어들의 분포)를 제공하는 특징을 가지고 있다.

토픽 모델링 알고리즘 유형

  • LSA(Latent Semantic Analysis)
  • LDA(Latent Dirichlet Allocation)
  • NMF(Non Negative Factorization)

-> LSA와 NMF는 행렬 분해 기반 토픽 모델링
-> pLSA와 LDA는 확률 기반의 토픽 모델링

어떤 토픽 모델링 알고리즘이든 아래 2개의 가정을 기반으로 하고 있다.

  • 개별 문서(Document)는 혼합된 여러 개의 주제로 구성되어 있다.
  • 개별 주제는 여러 개의 단어로 구성되어 있다.

LDA(Latent Dirichlet Allocation)의 이해

LDA는 관찰된 문서 내 단어들을 이용하여 베이즈 추론을 통해 잠재된 문서 내 토픽 분포와 토픽별 단어 분포를 추론하는 방식이다.
이때 LDA 베이즈 추론의 사전 확률 분포를 사용되는 것이 디리클레 분포(Dirichlet Distribution)이다.

LDA 구성 요소

스크린샷 2024-05-21 163110

  • $\theta$ (문서의 토픽 분포), $\phi$ (토픽의 단어 분포)는 디리클레 분포를 따른다고 가정
  • $\alpha$는 문서의 토픽 분포 $\theta$의 초기 하이퍼 파라미터로 문서별로 주제 분포가 얼마나 밀집되어 있는지 조절
  • $\beta$는 토픽의 단어 분포 $\phi$의 초기 파라미터로 주제별로 단어들이 얼마나 모여있는지 조절
  • W는 관측 단어
  • 상자는 반복을 의미

LDA 수행 프로세스

  1. 단순 Count 기반 문서-단어 행렬을 생성
  2. 토픽의 개수를 먼저 설정
  3. 각 단어들을 임의의 주제로 최초로 할당 후 문서별 토픽 분포와 토픽별 단어 분포가 결정됨
  4. 특정 단어를 하나 추출하고 해당 단어를 제외하고 문서의 토픽 분포와 토픽별 단어 분포를 재계산하고 추출된 단어는 새롭게 토픽 할당 분포 계산
  5. 다른 단어를 추출하고 3 Step을 다시 수행하여 모든 단어들의 토픽 할당 분포를 재계산
  6. 지정된 반복 횟수만큼 3, 4 Step을 수행하면서 모든 단어들의 토픽 할당 분포가 변경되지 않고 수렴될 때까지 수행

LDA 단점

  • 추출된 토픽은 다시 사람의 주관적인 해석이 필요
  • 초기화 파라미터(토픽 개수, $\alpha$, $\beta$) 및 Document-Term 행렬의 단어 필터링 최적화가 어려움

<실습>

20개 중 8개의 주제 데이터 로드 및 Count기반 피처 벡터화. LDA는 Count기반 Vectorizer만 적용합니다

from sklearn.datasets import fetch_20newsgroups
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.decomposition import LatentDirichletAllocation

# 모토사이클, 야구, 그래픽스, 윈도우즈, 중동, 기독교, 전자공학, 의학 등 8개 주제를 추출. 
cats = ['rec.motorcycles', 'rec.sport.baseball', 'comp.graphics', 'comp.windows.x',
        'talk.politics.mideast', 'soc.religion.christian', 'sci.electronics', 'sci.med'  ]

# 위에서 cats 변수로 기재된 category만 추출. featch_20newsgroups( )의 categories에 cats 입력
news_df = fetch_20newsgroups(subset='all',remove=('headers', 'footers', 'quotes'), 
                            categories=cats, random_state=0)

#LDA 는 Count기반의 Vectorizer만 적용합니다.  
count_vect = CountVectorizer(max_df=0.95, max_features=1000, min_df=2, stop_words='english', ngram_range=(1,2))
feat_vect = count_vect.fit_transform(news_df.data)
print('CountVectorizer Shape:', feat_vect.shape)
CountVectorizer Shape: (7862, 1000)

LDA 객체 생성 후 Count 피처 벡터화 객체로 LDA수행

lda = LatentDirichletAllocation(n_components=8, random_state=0)
lda.fit(feat_vect)
LatentDirichletAllocation(n_components=8, random_state=0)

각 토픽 모델링 주제별 단어들의 연관도 확인
lda객체의 components_ 속성은 주제별로 개별 단어들의 연관도 정규화 숫자가 들어있음

shape는 주제 개수 X 피처 단어 개수

components_ 에 들어 있는 숫자값은 각 주제별로 단어가 나타난 횟수를 정규화 하여 나타냄

숫자가 클 수록 토픽에서 단어가 차지하는 비중이 높음

print(lda.components_.shape)
lda.components_
(8, 1000)





array([[3.60992018e+01, 1.35626798e+02, 2.15751867e+01, ...,
        3.02911688e+01, 8.66830093e+01, 6.79285199e+01],
       [1.25199920e-01, 1.44401815e+01, 1.25045596e-01, ...,
        1.81506995e+02, 1.25097844e-01, 9.39593286e+01],
       [3.34762663e+02, 1.25176265e-01, 1.46743299e+02, ...,
        1.25105772e-01, 3.63689741e+01, 1.25025218e-01],
       ...,
       [3.60204965e+01, 2.08640688e+01, 4.29606813e+00, ...,
        1.45056650e+01, 8.33854413e+00, 1.55690009e+01],
       [1.25128711e-01, 1.25247756e-01, 1.25005143e-01, ...,
        9.17278769e+01, 1.25177668e-01, 3.74575887e+01],
       [5.49258690e+01, 4.47009532e+00, 9.88524814e+00, ...,
        4.87048440e+01, 1.25034678e-01, 1.25074632e-01]])

각 토픽별 중심 단어 확인

def display_topic_words(model, feature_names, no_top_words):
    for topic_index, topic in enumerate(model.components_):
        print('\nTopic #',topic_index)

        # components_ array에서 가장 값이 큰 순으로 정렬했을 때, 그 값의 array index를 반환. 
        topic_word_indexes = topic.argsort()[::-1]
        top_indexes=topic_word_indexes[:no_top_words]
        
        # top_indexes대상인 index별로 feature_names에 해당하는 word feature 추출 후 join으로 concat
        feature_concat = ' '.join([str(feature_names[i]) for i in top_indexes])
        #feature_concat = ' + '.join([str(feature_names[i])+'*'+str(round(topic[i],1)) for i in top_indexes])                
        print(feature_concat)

# CountVectorizer객체내의 전체 word들의 명칭을 get_features_names( )를 통해 추출
feature_names = count_vect.get_feature_names()

# Topic별 가장 연관도가 높은 word를 15개만 추출
display_topic_words(lda, feature_names, 15)

# 모토사이클, 야구, 그래픽스, 윈도우즈, 중동, 기독교, 전자공학, 의학 등 8개 주제를 추출. 
Topic # 0
year 10 game medical health team 12 20 disease cancer 1993 games years patients good

Topic # 1
don just like know people said think time ve didn right going say ll way

Topic # 2
image file jpeg program gif images output format files color entry 00 use bit 03

Topic # 3
like know don think use does just good time book read information people used post

Topic # 4
armenian israel armenians jews turkish people israeli jewish government war dos dos turkey arab armenia 000

Topic # 5
edu com available graphics ftp data pub motif mail widget software mit information version sun

Topic # 6
god people jesus church believe christ does christian say think christians bible faith sin life

Topic # 7
use dos thanks windows using window does display help like problem server need know run

개별 문서별 토픽 분포 확인

lda객체의 transform()을 수행하면 개별 문서별 토픽 분포를 반환함.

doc_topics = lda.transform(feat_vect)
print(doc_topics.shape)
print(doc_topics[:3])
(7862, 8)
[[0.01389701 0.01394362 0.01389104 0.48221844 0.01397882 0.01389205
  0.01393501 0.43424401]
 [0.27750436 0.18151826 0.0021208  0.53037189 0.00212129 0.00212102
  0.00212113 0.00212125]
 [0.00544459 0.22166575 0.00544539 0.00544528 0.00544039 0.00544168
  0.00544182 0.74567512]]

개별 문서별 토픽 분포도를 출력

20newsgroup으로 만들어진 문서명을 출력.

fetch_20newsgroups()으로 만들어진 데이터의 filename속성은 모든 문서의 문서명을 가지고 있음

filename속성은 절대 디렉토리를 가지는 문서명을 가지고 있으므로 ‘\‘로 분할하여 맨 마지막 두번째 부터 파일명으로 가져옴

def get_filename_list(newsdata):
    filename_list=[]

    for file in newsdata.filenames:
            #print(file)
            filename_temp = file.split('\\')[-2:]
            filename = '.'.join(filename_temp)
            filename_list.append(filename)
    
    return filename_list

filename_list = get_filename_list(news_df)
print("filename 개수:",len(filename_list), "filename list 10개만:",filename_list[:10])
filename 개수: 7862 filename list 10개만: ['soc.religion.christian.20630', 'sci.med.59422', 'comp.graphics.38765', 'comp.graphics.38810', 'sci.med.59449', 'comp.graphics.38461', 'comp.windows.x.66959', 'rec.motorcycles.104487', 'sci.electronics.53875', 'sci.electronics.53617']
news_df.filenames
array(['C:\\Users\\q\\scikit_learn_data\\20news_home\\20news-bydate-train\\soc.religion.christian\\20630',
       'C:\\Users\\q\\scikit_learn_data\\20news_home\\20news-bydate-test\\sci.med\\59422',
       'C:\\Users\\q\\scikit_learn_data\\20news_home\\20news-bydate-test\\comp.graphics\\38765',
       ...,
       'C:\\Users\\q\\scikit_learn_data\\20news_home\\20news-bydate-train\\rec.sport.baseball\\102656',
       'C:\\Users\\q\\scikit_learn_data\\20news_home\\20news-bydate-train\\sci.electronics\\53606',
       'C:\\Users\\q\\scikit_learn_data\\20news_home\\20news-bydate-train\\talk.politics.mideast\\76505'],
      dtype='<U91')

DataFrame으로 생성하여 문서별 토픽 분포도 확인

import pandas as pd 

topic_names = ['Topic #'+ str(i) for i in range(0, 8)]
doc_topic_df = pd.DataFrame(data=doc_topics, columns=topic_names, index=filename_list)
doc_topic_df.head(20)
Topic #0 Topic #1 Topic #2 Topic #3 Topic #4 Topic #5 Topic #6 Topic #7
soc.religion.christian.20630 0.013897 0.013944 0.013891 0.482218 0.013979 0.013892 0.013935 0.434244
sci.med.59422 0.277504 0.181518 0.002121 0.530372 0.002121 0.002121 0.002121 0.002121
comp.graphics.38765 0.005445 0.221666 0.005445 0.005445 0.005440 0.005442 0.005442 0.745675
comp.graphics.38810 0.005439 0.005441 0.005449 0.578959 0.005440 0.388387 0.005442 0.005442
sci.med.59449 0.006584 0.552000 0.006587 0.408485 0.006585 0.006585 0.006588 0.006585
... ... ... ... ... ... ... ... ...
sci.electronics.53785 0.015652 0.015648 0.015635 0.429298 0.476854 0.015641 0.015643 0.015629
talk.politics.mideast.77381 0.003574 0.003585 0.003573 0.128361 0.670401 0.003573 0.183359 0.003573
rec.motorcycles.103140 0.004823 0.966293 0.004810 0.004815 0.004818 0.004813 0.004813 0.004814
rec.motorcycles.105381 0.025030 0.025057 0.025035 0.824793 0.025018 0.025012 0.025023 0.025032
comp.windows.x.67546 0.001182 0.001181 0.001180 0.001181 0.001181 0.085647 0.043141 0.865307

100 rows × 8 columns