LDA를 사용하여 한글 데이터 토픽 모델링
(워드 클라우드, LDAvis로 결과 시각화)
안녕하세요 여러분 오늘은 LDA 알고리즘을 사용해서 토픽 모델링을 해봅니다!
그리고 그 결과로 워드 클라우드와 LDAvis로 모델 결과를 시각화를 해보겠습니다.
토픽 모델링이란?
Latent Dirichlet Allocation (LDA)는 토픽모델링에 이용되는 대표적인 알고리즘입니다.
토픽 모델링을 통해 우리는 문서가 어떤 토픽(주제)의 문서인지 알 수 있습니다.
토픽 모델링이 제공하는 토픽은 어떤 주제를 구성하는 단어들입니다.
즉, 문서 집합에서 이 단어 집합을 찾는 것이 토픽 모델링입니다.
LDA를 사용하여 토픽 모델링을 하려면 전 처리하는 과정이 필요합니다
한글 데이터를 가지고 토픽 모델링을 해보도록 하겠습니다.
1. 데이터 불러오기, 데이터 전처리 하기
사용할 데이터는 트위터 API를 사용하여 수집한 2020년 3월 1일부터 3월 10일까지의 트윗 데이터입니다.
json형태로 되어있는 상태입니다.
<json 데이터 불러오기>
import pandas as pd
from pandas import DataFrame as df
#데이터 불러오기 시간이랑 트윗 텍스트만
Data = df(columns=['timestamp','text'])
#파일이 여러 개 일때, 하나의 데이터 프레임으로 만들기 파일명이 파일명01.json,파일명02.json...일 때
date=['01','02','03','04','05','06','07','08','09','10']
for i in date:
print(i)
df= pd.read_json('경로/파일명'+i+'.json',lines=False)
df = df[['timestamp','text']]
Data=pd.concat([Data,df],ignore_index=True,axis = 0)
Data
<결과>
pandas를 이용하면 일자와 시간을 손쉽게 처리할 수가 있습니다.
데이터에 여러 일자가 있고 기간을 정해 해당 날짜 행의 데이터 프레임을 추출할 수도 있습니다.
예를 들어 현재는 2020-03-01~2020-03-10까지의 데이터가 있는데 2020-03-05까지의 데이터만 사용하고 싶다면
아래처럼 데이터 프레임을 추출할 수 있습니다.
<데이터 프레임 추출>
start_year = 2020
start_month = 3
start_day = 1
finish_year = 2020
finish_month = 3
finish_day = 5
Data['timestamp']= pd.to_datetime(Data["timestamp"])
Data = Data[(Data.timestamp >= datetime(start_year,start_month , start_day)) & (Data.timestamp <= datetime(finish_year, finish_month, finish_day))]
Data.reset_index(drop=True, inplace=True)
Data
<결과>
각 일자별로 몇 개의 트윗이 있는지 살펴보겠습니다
<데이터 프레임 특정 값을 가진 행 개수 세기>
#특정 열의 값 별 몇 개의 행이 있는지 세기
element_count = {}
for item in Data['timestamp']:
element_count.setdefault(item,0)
element_count[item] += 1
tweet_count = pd.DataFrame.from_dict(element_count, orient = 'index',columns=["tweet_count"])
tweet_count
<결과>
데이터 프레임을 정리합니다
우선 text행의 값들을 str 형식으로 바꿔줍니다
그리고 만약에 데이터 프레임에서 특정 키워드가 포함된 행만 추출하거나
특정 키워드가 포함된 행을 삭제하고 싶다면 삭제해 주세요
혹시 중복된 행이 있을 수 있으니 삭제합니다.
한글이 아닌 문자들은 모두 삭제합니다
<데이터 정리>
#데이터 프레임의 'text' 열의 값들을 str 형식으로 바꾸기
Data.text = Data.text.astype(str)
#데이터 프레임의 'text' 열의 값 중 keyword1이나 keyword 2가 포함된 행만 Data에 저장
#clean_Data = Data.loc[Data['text'].str.contains('keyword1|keyword2')]
#데이터 프레임의 'text' 열의 값 중 keyword1이나 keyword 2가 포함된 행은 삭제
#clean_Data = Data[~Data['text'].str.contains('keyword1|keyword2')]
#text와 timestamp 열을 기준으로 중복된 데이터를 삭제, inplace : 데이터 프레임을 변경할지 선택(원본을 대체)
clean_Data.drop_duplicates(subset=['text','timestamp'], inplace=True)
#한글이 아니면 빈 문자열로 바꾸기
clean_Data['text'] = clean_Data['text'].str.replace('[^ㄱ-ㅎㅏ-ㅣ가-힣]',' ',regex=True)
#빈 문자열 NAN 값으로 바꾸기
clean_Data = clean_Data.replace({'': np.nan})
clean_Data = clean_Data.replace(r'^\s*$', None, regex=True)
#NAN 이 있는 행은 삭제
clean_Data.dropna(how='any', inplace=True)
#인덱스 차곡차곡
clean_Data = clean_Data.reset_index (drop = True)
#데이터 프레임에 null 값이 있는지 확인
print(clean_Data.isnull().values.any())
<결과>
2. 데이터 전 처리하기
데이터를 알맞게 처리를 합니다
KoNLPy는 한국어의 자연어 처리(NLP)를 위한 Python 패키지입니다.
KoNLPy 사용법은 나중에 자세히 포스팅하도록 하겠습니다.
여기서는 okt 클래스의 nouns를 사용하여 명사 추출을 합니다.
트윗(문장)에서 명사만 추출하여 리스트 형식으로 저장합니다.
<데이터 전처리>
#텍스트 데이터를 리스트로 변환
Data_list=clean_Data.text.values.tolist()
#리스트를 요소별로(트윗 하나) 가져와서 명사만 추출한 후 리스트로 저장
data_word=[]
for i in range(len(Data_list)):
try:
data_word.append(okt.nouns(Data_list[i]))
except Exception as e:
continue
#트윗에서 명사만 추출해서 만든 리스트
data_word
<결과>
명사 사전을 기반으로 명사가 추출되는데
명사 추출이 부자연스러운 경우에는 아래 블로그를 참고하셔서 사전에 명사를 직접 추가하시면 됩니다.
https://now0930.pe.kr/wordpress/?p=3134
3. LDA 모델에 들어갈 객체(dictionary, corpus) 만들고 학습하기
gensim.models.wrappers.LdaMallet 모듈을 사용하면
깁스 샘플링을 사용하여 훈련 코퍼스에서 LDA 모델을 추정할 수 있습니다.
검색 창에 mallet.cs.umass.edu/dist/mallet-2.0.8.zip를 복사 붙여 넣기 하시고
mallet 모듈을 다운로드하고 경로를 기억해 주세요
LDA모델에 들어갈 객체를 만듭니다.
id2 word: Dictionary에 list of list of str 형식의 documents를 입력하면 Dictioanry 가 학습됩니다. 전체 말뭉치에서 단어가 겹치지 않도록 하나씩 dictionary에 저장됩니다. (단어를 int 형식의 idx로 변환)
corpus : 트윗 리스트 안의 단어를 bag-of-words 형태-> list of (token_id, token_count) 2-tuples로 변환합니다.
[[(id2 word [id], freq) for id, freq in cp] for cp in corpus [:50]]
로 사람이 볼 수 있는 딕셔너리를 확인할 수 있습니다.
gensim.models.wrappers.LdaMallet에서 사용할 수 있는 매개 변수입니다.
- mallet_path ( str ) : mallet 바이너리의 경로
- corpus (iterable of iterable of (int, int), optional):BoW 형식의 텍스트 모음
- num_topics (int, optional) – Number of topics.:주제 수
- alpha (int, optional) – Alpha parameter of LDA.:LDA의 알파 매개 변수
- id2 word (Dictionary, optional): 토큰 ID와 코퍼스에서 단어 사이의 매핑, 지정되지 않은 경우 -에서 유추
- optimize_interval (int, optional): optimize_interval 반복마다 하이퍼 파라미터를 최적화
- iterations (int, optional) :훈련 반복 횟수
- topic_threshold (float, optional): 주제를 고려할 확률의 임계 값
<LDA모델에 들어갈 객체 만들기>
id2word=corpora.Dictionary(data_word)
id2word.filter_extremes(no_below = 20) #20회 이하로 등장한 단어는 삭제
texts = data_word
corpus=[id2word.doc2bow(text) for text in texts]
mallet_path = 'mallet-2.0.8/bin/mallet'
ldamallet = gensim.models.wrappers.LdaMallet(mallet_path, corpus=corpus, num_topics=10, id2word=id2word)
ldamallet에 학습된 lda모델이 저장됩니다.
4. 토픽 수 별로 일관성 점수를 계산해서 가장 좋은 토픽 수의 모델 찾기
models.coherencemodel 주제 모델에 대한 주제 일관성을 계산
- model :주제가 제공되지 않은 경우 사전 훈련된 주제 모델을 제공해야 합니다. 현재 지원 LdaModel, LdaMulticore, LdaMallet와 LdaVowpalWabbit.
- topics(list of list of str, optional) :토큰 화 된 토픽의 목록
- texts (list of list of str, optional) :슬라이딩 창 기반 (예 : coherence =c_something) 확률 추정 기를 사용하는 일관성 모델에 필요한 토큰 화 된 텍스트.
- corpus (iterable of list of (int, number), optional) :BoW 형식의 코퍼스.
- dictionary (Dictionary, optional) : Gensim dictionary mapping of id word to create corpus. If model.id2 word is present, this is not needed. If both are provided, passed dictionary will be used.
- coherence ({'u_mass', 'c_v', 'c_uci', 'c_npmi'}, optional) :
- topn (int, optional) : 각 주제에서 추출할 최상위 단어 수에 해당하는 정수
- processes (int, optional) : 확률 추정 단계에 사용할 프로세스 수
https://radimrehurek.com/gensim/auto_examples/core/run_topics_and_transformations.html
<Coherence 점수를 계산하여 좋은 LDA 모델 찾기>
coherence_model_ldamallet = CoherenceModel(model=ldamallet, texts=texts, dictionary=id2word, coherence='c_v')
coherence_ldamallet = coherence_model_ldamallet.get_coherence()
def compute_coherence_values(dictionary, corpus, texts, limit, start=4, step=2):
coherence_values = []
model_list = []
for num_topics in range(start, limit, step):
model = gensim.models.wrappers.LdaMallet(mallet_path, corpus=corpus, num_topics=num_topics, id2word=id2word)
model_list.append(model)
coherencemodel = CoherenceModel(model=model, texts=data_word, dictionary=dictionary, coherence='c_v')
coherence_values.append(coherencemodel.get_coherence())
return model_list, coherence_values
# Can take a long time to run.
model_list, coherence_values = compute_coherence_values(dictionary=id2word, corpus=corpus, texts=texts, start=4, limit=21, step=2)
limit=21; start=4; step=2;
x = range(start, limit, step)
topic_num = 0
count = 0
max_coherence = 0
for m, cv in zip(x, coherence_values):
print("Num Topics =", m, " has Coherence Value of", cv)
coherence = cv
if coherence >= max_coherence:
max_coherence = coherence
topic_num = m
model_list_num = count
count = count+1
# Select the model and print the topics
optimal_model = model_list[model_list_num]
model_topics = optimal_model.show_topics(formatted=False)
#print(optimal_model.print_topics(num_words=10))
def format_topics_sentences(ldamodel=optimal_model, corpus=corpus, texts=texts):
# Init output
sent_topics_df = pd.DataFrame()
# Get main topic in each document
#ldamodel[corpus]: lda_model에 corpus를 넣어 각 토픽 당 확률을 알 수 있음
for i, row in enumerate(ldamodel[corpus]):
row = sorted(row, key=lambda x: (x[1]), reverse=True)
# Get the Dominant topic, Perc Contribution and Keywords for each document
for j, (topic_num, prop_topic) in enumerate(row):
if j == 0: # => dominant topic
wp = ldamodel.show_topic(topic_num,topn=10)
topic_keywords = ", ".join([word for word, prop in wp])
sent_topics_df = sent_topics_df.append(pd.Series([int(topic_num), round(prop_topic,4), topic_keywords]), ignore_index=True)
else:
break
sent_topics_df.columns = ['Dominant_Topic', 'Perc_Contribution', 'Topic_Keywords']
print(type(sent_topics_df))
# Add original text to the end of the output
#contents = pd.Series(texts)
#sent_topics_df = pd.concat([sent_topics_df, contents], axis=1)
sent_topics_df = pd.concat([sent_topics_df, Data['text'],Data['timestamp'],Data['tweet_url'],Data['screen_name'],Data['label']], axis=1)
return(sent_topics_df)
df_topic_sents_keywords = format_topics_sentences(ldamodel=optimal_model, corpus=corpus, texts=Data_list)
# Format
df_topic_tweet = df_topic_sents_keywords.reset_index()
df_topic_tweet.columns = ['Document_No', 'Dominant_Topic', 'Topic_Perc_Contrib', 'Keywords', 'Text','Timestamp', 'Tweet_url','Screen_name','label']
# Show각 문서에 대한 토픽
#df_dominant_topic=df_dominant_topic.sort_values(by=['Dominant_Topic'])
#df_topic_tweet
# Group top 5 sentences under each topic
sent_topics_sorteddf_mallet = pd.DataFrame()
sent_topics_outdf_grpd = df_topic_sents_keywords.groupby('Dominant_Topic')
for i, grp in sent_topics_outdf_grpd:
sent_topics_sorteddf_mallet = pd.concat([sent_topics_sorteddf_mallet,
grp.sort_values(['Perc_Contribution'], ascending=[0]).head(1)],
axis=0)
# Reset Index
sent_topics_sorteddf_mallet.reset_index(drop=True, inplace=True)
topic_counts = df_topic_sents_keywords['Dominant_Topic'].value_counts()
topic_counts.sort_index(inplace=True)
topic_contribution = round(topic_counts/topic_counts.sum(), 4)
topic_contribution
lda_inform = pd.concat([sent_topics_sorteddf_mallet, topic_counts, topic_contribution], axis=1)
lda_inform.columns=["Topic_Num", "Topic_Perc_Contrib", "Keywords", "Text", "timestamp", "tweet_url","screen_name","label","Num_Documents", "Perc_Documents"]
lda_inform = lda_inform[["Topic_Num","Keywords","Num_Documents","Perc_Documents"]]
lda_inform
#lda_inform.Topic_Num = lda_inform.Topic_Num.astype(int)
lda_inform['Topic_Num'] =lda_inform['Topic_Num'] +1
lda_inform.Topic_Num = lda_inform.Topic_Num.astype(str)
lda_inform['Topic_Num'] =lda_inform['Topic_Num'].str.split('.').str[0]
df_topic_tweet['Dominant_Topic'] =df_topic_tweet['Dominant_Topic'] +1
df_topic_tweet.Dominant_Topic = df_topic_tweet.Dominant_Topic.astype(str)
df_topic_tweet['Dominant_Topic'] =df_topic_tweet['Dominant_Topic'].str.split('.').str[0]
5. 토픽 모델링 결과 확인하기
<토픽 별 트윗 퍼센트 확인>
lda_inform.to_csv ("./Result/lda_inform.csv", index = None)
lda_inform
<토픽 별 트윗 저장하기>
#토픽별 트윗 저장
for i in range(1,topic_num+1):
globals()['df_{}'.format(i)]=df_topic_tweet.loc[df_topic_tweet.Dominant_Topic==str(i)]
globals()['df_{}'.format(i)].sort_values('Topic_Perc_Contrib',ascending=False,inplace = True)
globals()['df_{}'.format(i)].to_csv ("./Result/topic("+str(i)+")_tweet.csv", index = None)
df_1
<토픽 별 word cloud >
#토픽별 word cloud
def flatten(l):
flatList=[]
for elem in l:
if type(elem) == list:
for e in elem:
flatList.append(e)
else:
flatList.append(elem)
return flatList
for i in range(1,topic_num+1):
data_list = globals()['df_{}'.format(i)].Text.values.tolist()
data_word=[]
for j in range(len(data_list)):
try:
data_word.append(okt.nouns(data_list[j]))
except Exception as e:
continue
data_word=flatten(data_word)
data_word=[x for x in data_word if not x.isdigit()]
freq=pd.Series(data_word).value_counts().head(50)
freq=dict(freq)
wordcloud = WordCloud(font_path="./Font/BMHANNA_11yrs_ttf.ttf",
relative_scaling = 0.2,
background_color = 'white',
).generate_from_frequencies(freq)
plt.figure(figsize=(16,8))
plt.imshow(wordcloud)
plt.axis("off")
plt.show()
plt.savefig("./Result/topic("+str(i)+")wordcloud.png")
<결과>
6. LDAvis로 토픽 모델 시각화
LDAvis는 LDA 모델의 학습 결과를 시각적으로 표현하는 라이브러리입니다.
차원 축소 방법인 Principal Component Analysis (PCA) 와 키워드추출 방법을 이용하여
토픽 간의 관계와 토픽 키워드를 손쉽게 이해할 수 있도록 도와줍니다.
LDAvis를 사용하면 다음과 같이 위의 LDA모델의 학습 결과를 시각화 할 수 있습니다.
<LDAvis로 시각화한 결과>
클릭과 커서 이동을 통해 토픽 별 단어 분포와 토픽 간의 유사성 등을 확인할 수 있습니다.
아래에서 자세히 설명하겠습니다.
<LDAvis 사용 방법 및 해석>
LDAvis를 보면 크게 왼쪽(Intertopic Distance Map (via multidimensional scaling))과
오른쪽(Top-30 Most Relevant Terms for Topic) 두 부분이 있습니다.
<Intertopic Distance Map (via multidimensional scaling)>
- 토픽은 단어 개수의 차원을 가지고 있음->2차원으로 압축하기위해서는 차원 축소 방법 이용
- LDAvis는 Principal Component Analysis (PCA)를 이용하여 n_terms 차원의 벡터들을 2차원으로 압축
- 원의 크기 : 토픽의 단어들이 얼마나 속해 있고 어떻게 분포되어있는지
- 원의 거리 : 토픽 간의 유사성 (두 개의 원이 겹친다면 , 두 개의 토픽은 유사한 토픽이라는 의미)
- 버블 중 하나로 커서를 이동하면 오른쪽의 단어와 막대가 업데이트
<Top-30 Most Relevant Terms for Topic>
- 막대 : 토픽을 형성하는 주도적인 키워드 확인, 키워드에 커서를 올리면 해당 키워드와 관련된 토픽 확인 가능
keyword extraction(키워드 추출) 기준 두 가지
1) salience
한 토픽의 키워드라면, 각 토픽에 속한 많은 문서들에서 등장해야 함
P(w|t)가 커야 함 (단어 w를 갖고 있는 모든 문서들 중 토픽 t가 할당된 비율)
문제점: P(w|t)가 가장높은 단어는 ‘a, the, -은, -는, -이, -가’ 와가’와 같은문법적인 단어일 것
하지만 ‘a’라는 단어는 어떤 토픽을 명확히 지시해주지 않음
차별성이 없는 단어는 키워드로 부적함
2) discriminative power
salience의 문제점 -> LDAvis에서는 P(w|t)를 P(w)로 나눔
한 토픽에서 자주 등장하는 단어라 하더라도 본래 자주 등장하는 단어라면 그 중요도를 낮추겠다는 의미
문제점: 최고의 discriminative power를 지닌 단어는 infrequent terms(드문 단어) 일 가능성이 높음
한 토픽에서만 등장한 단어는 전체에서도 몇 번 등장하지 않을 가능성이 높음
즉, salience와와 discriminative power 사이에는 negative correlation 이 있음
앙면을 모두 고려하여 키워드를 선택해야 함
이 문제점을 보완하기 위해서 LDAvis에서는 λ 값을 사용하여 두 관점의 중요도를 사람이 직접 정할 수 있습니다.
- λ 값 (Slide to adjust relevance metric)
오른쪽 상단에 λ 값 [0, 1] 사이에서 조절 가능
키워드 랭킹(막대그래프 순서)은 다음의 점수로 계산.
λ⋅P(w|t)+(1−λ)⋅P(w|t) P(w)λ⋅P(w|t)+(1−λ)⋅P(w|t) P(w)
λ =1 일수록, 토픽 별로 가장 자주 등장하는 단어들을 우선적으로 키워드로 선택한다는 의미이고
λ = 0 일수록, 토픽 간에 차이가 많이 나는 단어를 선택한다는 의미(해당 토픽에서 많이 등장한 단어)입니다.
<LDAvis로 토픽 모델링 결과 시각화>
from gensim.models import LdaModel
def mallet_to_lda(optimal_model):
model_gensim = LdaModel(
id2word=optimal_model.id2word, num_topics=optimal_model.num_topics,
alpha=optimal_model.alpha, eta=0, iterations=1000,
gamma_threshold=0.001,
dtype=np.float32
)
model_gensim.sync_state()
model_gensim.state.sstats = optimal_model.wordtopics
return model_gensim
model = mallet_to_lda(optimal_model)
import pyLDAvis
import pyLDAvis.gensim
pyLDAvis.enable_notebook()
vis = pyLDAvis.gensim.prepare(model, corpus, id2word)
vis
부족한 포스팅 봐주셔서 감사합니다
<이 분석 결과로 참가했던 데이콘 - 코로나 데이터 시각화 AI 경진대회 >
https://dacon.io/competitions/official/235590/codeshare/1078?page=1&dtype=recent
<참고한 문서들>
blog.naver.com/soowon0109/221607786747
https://www.machinelearningplus.com/nlp/topic-modeling-gensim-python/
https://lovit.github.io/nlp/2018/09/27/pyldavis_lda/
https://bhumikabhatt.org/analyzing-hidden-themes-in-the-retracted-biomedical-literature.html
https://www.youtube.com/watch?v=AFIO92N9xm4
https://ratsgo.github.io/from%20frequency%20to%20semantics/2017/06/01/LDA/