이번에는 토픽모델링에 대해 공부한 내용을
회고하고자 한다.
토픽모델링은 주로 LDA 모델을 이용한다.
LDA
LDA는 LatentDirichletAllocation의 약자로,
문서들이 쓰여질 때, 그 문서를 구성하는 몇 개의 토픽이 존재하며
각 토픽은 단어의 집합으로 구성되어있다를 가정으로 한다.
여기서 가정은 가정일 뿐, 각 문서에서 토픽이 무엇이고
각 토픽은 어떤 단어들로 이루어졌다는 사실이 명시적으로 드러나지는 않는다.
그래서 '내재된 주제 혹은 토픽'이라고 부르며
LDA는 이와 같이 내재된 토픽들을 유추하고자 하는 통계적 방법론이라고 부를 수 있다.
이때, 토픽은 문서마다 개별적으로 전혀 다른 토픽이 있는 것이 아니라
전체 말뭉치를 관통하는 토픽들이 있으며,
문서는 이러한 공통적인 토픽들이 다양한 비중으로 결합된 것으로,
문서에 따른 토픽의 확률 분포를 추정하는 것이다
각 문서의 토픽 분포는 디리클레 분포를 따른다.
LDA에서는 문서의 토픽분포와 토픽의 단어분포는 디리클레 분포를 따른다고 가정한다.
디리클레 모형
각 벡터값이 양수이고 모든 값을 더하면 1이 되는 모형을 의미한다.
예를 들어 특정 문서에 3개의 토픽이 있다고 가정했을 때
각 토픽에 대한 비중이 [0.5, 0.3,0.2]라고 가정해보자.
위 숫자들을 다 합치면 1이 되는 것을 확인할 수 있다.
이때 각 토픽에 대한 단어분포는 "디리클레"분포를 따른다고 말할 수 있다.
토픽모델링 성능 척도를 의미하는 지표로는
2가지가 있다
Perplexity
혼란도
어떤 특정 확률 모형이 실제로 관측된 값을 얼마나 유사하게
예측해내는지 평가할 때 사용
토픽모델링에서 혼란도는 우리가 추정한 디리클레 모형이
주어진 문서 집합을 얼마나 유사하게 생성할 수 있는지 나타냄
작을수록 잘 생성한다는 뜻이다.
Coherence
토픽 응집도
각 토픽에서 상위 비중을 차지하는 단어들 간의 의미적 유사도를 나타내는 척도
토픽이 단일 주제를 잘 표현한다면
상위 비중을 차지하는 단어들 간의 의미적 유사도가 높을 것이다
(유사한 단어들이 상위 비중을 차지)
혼란도와 반대로 클수록 성능이 좋다는 것을 의미한다.
사이킷런을 이용한 토픽 모델링
사이킷런은 혼잡도 계산만 제공, 토픽 응집도는 제공하지 않는다.
이와 다르게 또다른 토픽모델링 라이브러리인 Genism는 둘 다 제공한다.
그래서 일반적으로는 Genism를 사용한다.
사이킷런에서는 LDA를 위한 클래스, LatentDirichletAllocation을 지원
이때 주의할 점은 인수로 카운트 벡터를 넘겨줘야 한다는 것이다.
LatentDirichletAllocation의 매개변수를 살펴보자
1. n_components
: 토픽의 수를 지정 (제일 중요한 매개변수로 최적의 값을 찾는 것이 중요)
2. learning_method : batch or online
* batch가 성능이 더 좋지만 속도가 느리다는 단점이 존재
3. topic_word_prior
: 베타를 의미 ( 토픽의 사전 단어 분포 결정)
: default값은 1/n_components
4. doc_topic_prior
: 알파를 의미 (문서의 사전 토픽 분포를 결정)
: default값은 1/n_components
⇒ topic_word_prior, doc_topic_prior 이 두 값을 얼마로 설정하는 것이
좋은지에 대한 정답은 없다.
(유명 논문에서는 베타를 0.1, 알파를 50/n_components 값을 사용)
사이킷런은 LDA결과로 각 문서에 대한 토픽의 비중을 반환
from sklearn.decomposition import LatentDirichletAllocation
import numpy as np
np.set_printoptions(precision=3)
lda = LatentDirichletAllocation(n_components = 10, #추출할 topic의 수
max_iter=5, #알고리즘 반복 횟수
topic_word_prior=0.1, doc_topic_prior=1.0,
learning_method='online',
n_jobs= -1, #사용 processor 수
random_state=0)
review_topics = lda.fit_transform(review_cv)#LDA는 카운트 벡터를 입력 인수로 받음
print('#shape of review_topics:', review_topics.shape)
print('#Sample of review_topics:', review_topics[0])
gross_topic_weights = np.mean(review_topics, axis=0) # 각 열에 대한 값들의 평균(axis=0)
print('#Sum of topic weights of documents:', gross_topic_weights)
`#shape of review_topics: (3219, 10)
#Sample of review_topics: [0.903 0.007 0.027 0.008 0.007 0.008 0.007 0.007 0.007 0.018]
#Sum of topic weights of documents: [0.087 0.083 0.085 0.115 0.115 0.126 0.098 0.072 0.07 0.148]`
위의 결과를 가지고 각 토픽의 내용을 알 수 없음(왜냐하면 토픽의 비중만 나와있기 때문에!)
따라서 우리는 각 토픽별로 어떤 단어가 상위 비중을 차지했는지 알아야 한다.
lda.components_를 통해 각 토픽의 단어 분포(비중)를 알 수 있음
lda.components_ 의 shape : (토픽의 개수, 특성 단어의 개수(카운트 벡터에서 사용된 특성 단어의 개수))
def print_top_words(model, feature_names, n_top_words):
for topic_idx, topic in enumerate(model.components_):
print("Topic #%d: " % topic_idx, end='')
print(", ".join([feature_names[i] for i in np.argsort(-topic)[:n_top_words]]))#arsort : 오름차순 정렬 후 인덱스 반환
print()
print_top_words(lda,cv.get_feature_names_out(), 10)
`Topic #0: com, morality, keith, article, sgi, think, sandvik, objective, caltech, moral
Topic #1: image, file, graphics, files, ftp, available, software, use, data, mail
Topic #2: space, nasa, access, launch, earth, orbit, shuttle, digex, lunar, satellite
Topic #3: article, com, just, don't, like, i'm, nntp, university, host, posting
Topic #4: key, clipper, chip, encryption, com, government, law, keys, use, escrow
Topic #5: scsi, com, bit, ibm, bus, know, windows, thanks, card, university
Topic #6: host, gov, nntp, posting, university, distribution, nasa, ___, world, com
Topic #7: drive, com, disk, hard, controller, drives, dos, tape, floppy, problem
Topic #8: key, public, message, faq, mail, pgp, des, group, uni, ripem
Topic #9: god, people, don't, jesus, believe, just, does, say, think, know`
결과를 보고 토픽이 제대로 분류됐는지 확인하는 것은 분석가의 몫이며
예를 들자면 1번 토픽 같은 경우에는 image, graphics, files등의 단어를 통해
말뭉치를 이루는 카테고리들 중 graphics에 해당하는 토픽임을 짐작할 수 있다.
최적의 토픽 수 선택하기
토픽 모델링에서 가장 중요한 매개변수는 토픽의 수이다.
토픽 수를 결정하기 위해
다양한 토픽의 수를 적용한 모형들에 대해 혼란도와 토픽 응집도를 계산하는데,
사이킷런은 공식적으로 혼란도만 제공한다.
사이킷런에서는 혼잡도를 계산해주는 perplexity()함수를 제공
인수로는 카운트벡터를 넘겨주면 된다
이때 주의할 점은 perplexity를 적용하기 전에 먼저 모델을 학습시켜야 함
lda.fit_transform() or lda.fit()
import matplotlib.pyplot as plt
%matplotlib inline
#토픽의 수를 변화시키면서 LDA를 수행하고 perplexity를 계산 한 후 그래프로 그려주는 함수 구현
def show_perplexity(cv, start=10, end=30, max_iter=5, topic_word_prior= 0.1,
doc_topic_prior=1.0):
iter_num = []
per_value = []
for i in range(start, end + 1):
lda = LatentDirichletAllocation(n_components = i, max_iter=max_iter,
topic_word_prior= topic_word_prior,
doc_topic_prior=doc_topic_prior,
learning_method='batch', n_jobs= -1,
random_state=7)
lda.fit_transform(cv)#먼저 LDA수행
iter_num.append(i)
pv = lda.perplexity(cv) #혼잡도 반환
per_value.append(pv)
print(f'n_components: {i}, perplexity: {pv:0.3f}')
plt.plot(iter_num, per_value, 'g-') #g-- : 초록색 선으로 plot을 그리라는 뜻
plt.show()
return start + per_value.index(min(per_value)) # 혼잡도가 최솟값인 n_components를 반환
#예를 들어 1번째 인덱스가 최솟값이라면 그때의 토픽의 수는 7(6+1)
print("n_components with minimum perplexity:",
show_perplexity(review_cv, start=6, end=15))

위 결과를 보면 토픽의 수가 9일 때
Perplexity가 가장 작다는 것을 알 수 있다.
따라서 최적의 토픽의 수를 9로 지정하고 토픽모델링을 진행해보자
💡 위에서 찾은 최적의 토픽의 수(9개)로 LDA수행
lda = LatentDirichletAllocation(n_components = 9, #추출할 topic의 수를 지정
max_iter=20,
topic_word_prior= 0.1,
doc_topic_prior=1.0,
learning_method='batch',
n_jobs= -1,
random_state=7)
review_topics = lda.fit_transform(review_cv)
print_top_words(lda, cv.get_feature_names_out(), 10)
`Topic #0: image, available, file, ftp, mail, data, files, information, graphics, internet
Topic #1: nasa, space, gov, ___, center, orbit, earth, research, jpl, 1993
Topic #2: com, keith, morality, caltech, sgi, objective, article, think, moral, don't
Topic #3: jesus, god, just, com, know, article, john, good, don't, bible
Topic #4: people, god, don't, does, think, say, believe, just, way, evidence
Topic #5: drive, scsi, card, disk, ide, hard, controller, bus, bit, drives
Topic #6: space, access, article, year, launch, just, digex, like, henry, toronto
Topic #7: key, encryption, clipper, chip, government, com, keys, security, use, public
Topic #8: com, posting, nntp, host, university, article, i'm, know, thanks, ibm`
Gensim을 이용한 토픽 모델링
사이킷런 이외에 Gensim을 이용하여 토픽모델링 수행 가능
Gensim에서는 혼란도와 응집도에 대한 함수 둘 다 제공한다.
Gensim에서는 사이킷런과 다르게
카운트벡터를 생성해주는 자체 함수가 있다.
Gensim에서 카운트 벡터 리스트 생성하는 과정(모델 학습 전까지의 과정)
1. 먼저 문서에 대해서 토큰화를 진행
2. 토큰화 결과로부터 토큰과 gensim 자체에서 사용하는 id를 매칭
-> 이 역할을 Dictionary클래스가 해줌
3. filter_extremes() 함수로 토큰을 필터링하여 특성을 선택할 수 있다
* keep_n : max_features와 같은 역할(특성 단어 개수 지정)
* no_below : min_df와 같은 역할 (최소 빈도수 지정)
* no_above : max_df와 같은 역할(최대 빈도수 지정)
4. doc2bow()함수로 토큰화된 결과로부터 카운트 벡터 생성 ( CountVectorizer/Tfidf와 같은 역할 )
#1번 과정
먼저 dictionary클래스를 통해 토큰화된 결과와 id를 매칭시킴
인수로는 토큰을 넘겨주면 된다.
from gensim.corpora.dictionary import Dictionary
# 토큰화 결과로부터 dictionay 생성
dictionary = Dictionary(texts)
print('#Number of initial unique words in documents:', len(dictionary))
print(dictionary)
# 2번과정
그런 다음 단어들을 filter_extremes함수에서 매개변수를 조절하여 필터링
결과를 보면 단어의 수가 46466 → 2000으로 줄어듬 (keep_n매개변수를 통해 단어의 수를 2000개로 조절했기 때문!)
# 문서 빈도수가 너무 적거나 높은 단어를 필터링하고 특성을 단어의 빈도 순으로 선택
dictionary.filter_extremes(keep_n=2000, no_below=5, no_above=0.5)
print('#Number of unique words after removing rare and common words:', len(dictionary))
print(dictionary)
`#Number of unique words after removing rare and common words: 2000
Dictionary<2000 unique tokens: ['accept', 'act', 'action', 'allan', 'allow']...>`
# 3번과정
doc2bow()를 통해 카운트 벡터 리스트 생성 < 인수로 토큰화된 결과를 넘겨줘야 함
이때 결과로 각 문서에 대한 카운트 벡터 “리스트”를 반환해주는데,
빈도수가 0인 단어들은 반환을 안 해주고 최소1인 단어들만을 반환해준다.
# 카운트 벡터로 변환
corpus = [dictionary.doc2bow(text) for text in texts] #토큰화 결과를 texts라고 부름(gensim에서는)
print('#Number of unique tokens: %d' % len(dictionary))
print('#Number of documents: %d' % len(corpus))#doc2bow()의 결과로 반환된 카운트벡터를 corpus로 지칭함
Gensim에서의 LDA
Gensim에서는 LDA를 LdaModel을 통해 수행
* num_topics : 토픽의 개수
* corpus : doc2bow()를 통해 반환된 결과를 인수로 넘겨주면 된다
* id2word : dictionary를 넘겨주면 된다
* passes : 반복횟수(max_iter 동일)
from gensim.models import LdaModel #gensim에서 LDA는 LdaModel을 통해 수행
num_topics = 10
passes = 5
#% time : 주피터 노트북의 명령어 : 해당문장을 실행하는데 소요된 시간 출력
%time model = LdaModel(corpus=corpus, id2word=dictionary,\\
passes=passes, num_topics=num_topics, \\
random_state=7)
gensim에서는 print_topics 함수 제공
: 각 토픽의 번호와 함께 토픽별 상위 단어와 그 단어의 비중을 결과로 반환
* num_topics: 보고싶은 토픽의 수
* num_words : 각 토픽별 상위 단어 수 지정 -> 보통은 num_words만 사용
model.print_topics(num_words=10) #print_topics() : 각 토픽별 단어 비중 반환
`[(0,
'0.023*"com" + 0.018*"keith" + 0.016*"caltech" + 0.013*"sgi" + 0.013*"nntp" + 0.013*"posting" + 0.013*"host" + 0.012*"would" + 0.012*"system" + 0.011*"livesey"'),
(1,
'0.020*"morality" + 0.018*"objective" + 0.015*"one" + 0.015*"say" + 0.014*"uiuc" + 0.012*"frank" + 0.012*"values" + 0.010*"faq" + 0.010*"article" + 0.008*"cso"'),
(2,
'0.026*"com" + 0.025*"access" + 0.025*"posting" + 0.023*"host" + 0.023*"nntp" + 0.017*"digex" + 0.015*"article" + 0.013*"cwru" + 0.013*"___" + 0.013*"net"'),
(3,
'0.021*"university" + 0.017*"posting" + 0.015*"host" + 0.015*"nntp" + 0.013*"article" + 0.010*"com" + 0.009*"know" + 0.009*"i\'m" + 0.009*"would" + 0.009*"thanks"'),
(4,
'0.032*"com" + 0.015*"would" + 0.011*"article" + 0.010*"one" + 0.010*"get" + 0.009*"people" + 0.009*"ibm" + 0.008*"government" + 0.007*"good" + 0.007*"i\'m"'),
(5,
'0.025*"key" + 0.017*"encryption" + 0.014*"clipper" + 0.014*"chip" + 0.009*"keys" + 0.009*"use" + 0.008*"security" + 0.008*"government" + 0.008*"public" + 0.007*"escrow"'),
(6,
'0.024*"scsi" + 0.024*"drive" + 0.013*"com" + 0.012*"ide" + 0.011*"controller" + 0.010*"bus" + 0.010*"card" + 0.010*"disk" + 0.009*"one" + 0.009*"drives"'),
(7,
'0.017*"graphics" + 0.012*"image" + 0.012*"ftp" + 0.011*"file" + 0.010*"files" + 0.009*"available" + 0.009*"data" + 0.009*"pub" + 0.008*"software" + 0.008*"use"'),
(8,
'0.014*"god" + 0.013*"people" + 0.012*"one" + 0.009*"would" + 0.007*"jesus" + 0.007*"com" + 0.007*"think" + 0.006*"many" + 0.006*"even" + 0.006*"say"'),
(9,
'0.033*"space" + 0.019*"nasa" + 0.009*"gov" + 0.007*"first" + 0.007*"launch" + 0.006*"moon" + 0.006*"earth" + 0.006*"orbit" + 0.006*"shuttle" + 0.006*"would"')]`
gensim에서는 get_document_topics() 함수도 제공
: 문서별 토픽의 비중을 결과로 반환 (토픽번호, 비중)
* minimum_probability : 비중의 최소 임계값 설정 가능
인수로 corpus( doc2bow()의 결과 )를 넘겨주면 되고
비중이 0에 매우 수렴한 토픽은 보여주지 않는다.
#get_document_topics() : 문서별 토픽의 비중 반환
print("#topic distribution of the first document: ", model.get_document_topics(corpus[0]))
print("#topic distribution of the Third document: ", model.get_document_topics(corpus[2]))
`#topic distribution of the first document: [(0, 0.7257827), (8, 0.26993728)]
#topic distribution of the Third document: [(0, 0.013864181), (5, 0.08809217), (6, 0.88709956)]`
혼란도와 토픽 응집도를 이용한 최적값 선택
> gensim에서는 혼란도와 토픽 응집도 둘 다 제공
혼잡도는 log_perplexity()함수를 이용하여 쉽게 구현 가능 : model.log_perplexity()
인수로 corpus를 넘겨주면 된다.
#혼잡도는 log_perplexity()로 쉽게 구현가능
#인수로 corpus를 넘겨주면 된다
model.log_perplexity(corpus) #토픽수10개에 대한 혼잡도
#결과 : 6.870640401487444
토픽응집도는 CoherenceModel로 구현 가능
매개변수로는 model, corpus, texts, coherence가 있다
* model : 사전 학습된 토픽모델링 모형
* corpus : 카운트 벡터 리스트
* texts : 토큰화된 결과
* coherence : 응집도를 계산하는 척도
coherence에 값을 넘겨줄 때 주의할 사항이 있다.
* u-mass : 반드시 인수로 corpus가 있어야 함
* c_v : 반드시 인수로 texts가 있어야 함(default)
> c_v와 같이 coherence에 c로 시작하는 애들을 넘겨줄 때 인수로 texts가 있어야 한다
모델을 생성한 다음에 get_herence()로 응집도 확인 가능하다
from gensim.models import CoherenceModel
cm = CoherenceModel(model=model, corpus=corpus, coherence='u_mass')
coherence = cm.get_coherence()
print(coherence)# 토픽수 10개에 대한 토픽응집도
#결과 : 1.7493528544065975
Gensim은 혼잡도와 응집도 둘 다 제공한다.
따라서 이 두개의 값을 고려하여 최적의 토픽의 수를 찾는 것이 중요하다.
def show_coherence(corpus, dictionary, start=6, end=15):
iter_num = []
per_value = []
coh_value = []
for i in range(start, end + 1):
model = LdaModel(corpus=corpus, id2word=dictionary,
chunksize=1000, num_topics=i,
random_state=7)
iter_num.append(i)
pv = model.log_perplexity(corpus)
per_value.append(pv)
cm = CoherenceModel(model=model, corpus=corpus,
coherence='u_mass')
cv = cm.get_coherence()
coh_value.append(cv)
print(f'num_topics: {i}, perplexity: {pv:0.3f}, coherence: {cv:0.3f}')
plt.plot(iter_num, per_value, 'g-')
plt.xlabel("num_topics")
plt.ylabel("perplexity")
plt.show()
plt.plot(iter_num, coh_value, 'r--')
plt.xlabel("num_topics")
plt.ylabel("coherence")
plt.show()
show_coherence(corpus, dictionary, start=6, end=15)

혼잡도는 작을수록, 응집도는 클수록 좋은데
혼잡도는 토픽의 수가 6일때 좋은데 응집도는 8일때가 가장 좋다
토픽 수의 최종 선택은 분석가의 몫이며
두개로 토픽모델링을 진행해보고 더 유의미한 결과가 나오는 쪽으로 선택해야 한다.
'✍️ STUDY > NLP' 카테고리의 다른 글
[cs224n] Lecture1 - Intro & Word Vectors (1) | 2023.10.06 |
---|---|
[Text Mining] 감성분석 (1) | 2023.03.22 |
[Text Mining] 차원 축소 (0) | 2023.02.27 |
[Text Mining] BOW 기반의 문서 분류 (0) | 2023.02.21 |
[Text Mining] CountVectorizer와 TF-IDF (0) | 2023.02.13 |