오늘은 저의 학위논문 CODE를 올려봅니다.

논문 분야는 텍스트 마이닝으로, 크게 감성분석, 토픽모델링, 대응분석으로 구성됩니다.

대응분석은 SAS로 수행했고 감성분석, 토픽모델링은 Google Colab 환경에서 Python 3.0으로 진행했습니다.

 

당시 학위논문을 쓰면서 텍스트 마이닝에 흥미를 더욱 가지며 졸업 후, Word Embedding과 Deep Learning으로 공부 영역을 확장했습니다. 현재도, 텍스트 마이닝, NLP 는 가장 관심있는 분야입니다.

 

그럼, 토픽모델링 CODE를 올려보도록 하겠습니다.

 

목차

1. 데이터 불러오기 및 형태소 분석

2. 단어 전처리

2.1. 형태소 분석에서 분할된 단어 하나로 합치기

2.2. 너무 많이 나오는 단어 제거하기

2.3. 의미없는 단어 제거 및 한 글자인 경우 제거하기

3. 기사별로 단어들을 합치기

4. 빈도수가 높은 단어, 너무 낮은 단어 제거하기

5. Gensim 패키지를 이용해 토픽모델링하기

5.1. 토픽수 구하기

5.2. 토픽모델링하기

5.3. 기사별 토픽 할당하기

 

1. 데이터 불러오기 및 형태소 분석

from google.colab import drive
drive.mount('/content/drive')
import pandas as pd
link = pd.read_excel('/content/drive/MyDrive/방역 기사 추출.xlsx')
link.head()

#한글제외 제거
import re
hangul = re.compile('[^ ㄱ-ㅣ가-힣]+') # 한글과 띄어쓰기를 제외한 모든 글자 
for i in range(len(link)):
  link.iloc[i,0] = hangul.sub(' ', link.iloc[i,0]) # 한글과 띄어쓰기를 제외한 모든 부분을 제거
  
#Komoran 이용하여 형태소 분석 수행
!apt-get update 
!apt-get install g++ openjdk-8-jdk python-dev python3-dev 
!pip3 install JPype1-py3 
!pip3 install konlpy 
!JAVA_HOME="C:\Users\tyumi\Downloads"
from konlpy.tag import * # class  
komoran=Komoran()
for i in range(0,len(link)):
  link.iloc[i,0]=re.sub('\n|\r','',link.iloc[i,0])

text_1=[]
article=[]
index=[]
paper=[]
title=[]
date=[]
for j in range(0,len(link)):
  ex_pos=list(komoran.pos(link.iloc[j,0]))
  for (text, tclass) in ex_pos : # ('형태소', 'NNG') 
    if tclass == 'VA' or tclass == 'NNG' or tclass == 'NNP' or  tclass == 'MAG' : 
      text_1.append(text) 
      index+=[j]
      article+=[link.iloc[j,0]]
      paper+=[link.iloc[j,3]]
      title+=[link.iloc[j,4]]
      date+=[link.iloc[j,2]]
link=pd.DataFrame({'Index':index,'Phrase':text_1,'Text':article,'Paper':paper,'Title':title,'Date':date})

[link 데이터 프레임 예시]

2. 단어 전처리

 

2.1. 형태소 분석에서 분할된 단어 하나로 합치기

 

for i in range(0,len(link)):
  if str(link.iloc[i,1])=='코로나':
    if str(link.iloc[i+1,1])=='바이러스':
      link.iloc[i,1]=''
      link.iloc[i+1,1]=''
for i in range(0,len(link)):
  if str(link.iloc[i,1])=='미래':
    if str(link.iloc[i+1,1])=='통합':
        if str(link.iloc[i+2,1])=='당':
          link.iloc[i,1]='미래통합당'
          link.iloc[i+1,1]=''
          link.iloc[i+2,1]=''
for i in range(0,len(link)):
  if str(link.iloc[i,1])=='자유':
    if str(link.iloc[i+1,1])=='한국당':
      link.iloc[i,1]='자유한국당'
      link.iloc[i+1,1]=''
for i in range(0,len(link)):
  if str(link.iloc[i,1])=='사회':
    if str(link.iloc[i+1,1])=='거리':
      if str(link.iloc[i+2,1])=='두기':
        link.iloc[i,1]='거리두기'
        link.iloc[i+1,1]=''
        link.iloc[i+2,1]=''
for i in range(0,len(link)):
  if str(link.iloc[i,1])=='거리':
    if str(link.iloc[i+1,1])=='두기':
      link.iloc[i,1]='거리두기'
      link.iloc[i+1,1]=''
for i in range(0,len(link)):
  if str(link.iloc[i,1])=='중앙':
    if str(link.iloc[i+1,1])=='방역':
      if str(link.iloc[i+2,1])=='대책':
        if str(link.iloc[i+3,1])=='본부':
          link.iloc[i,1]=''
          link.iloc[i+1,1]=''
          link.iloc[i+2,1]=''
          link.iloc[i+3,1]=''
for i in range(0,len(link)):
  if str(link.iloc[i,1])=='보건':
    if str(link.iloc[i+1,1])=='복지':
      if str(link.iloc[i+2,1])=='가족부':
        link.iloc[i,1]=''
        link.iloc[i+1,1]=''
        link.iloc[i+2,1]=''
for i in range(0,len(link)):
  if str(link.iloc[i,1])=='보건':
    if str(link.iloc[i+1,1])=='복지부':
      link.iloc[i,1]='보건복지부'
      link.iloc[i+1,1]=''
for i in range(0,len(link)):
  if str(link.iloc[i,1])=='박':
    if str(link.iloc[i+1,1])=='능':
      if str(link.iloc[i+2,1])=='후':
        link.iloc[i,1]='박능후'
        link.iloc[i+1,1]=''
        link.iloc[i+2,1]=''
for i in range(0,len(link)):
  if str(link.iloc[i,1])=='정은':
    if str(link.iloc[i+1,1])=='경':
      link.iloc[i,1]='정은경'
      link.iloc[i+1,1]=''
for i in range(0,len(link)):
  if str(link.iloc[i,1])=='문':
    if str(link.iloc[i+1,1])=='대통령':
      link.iloc[i,1]='문재인'
      link.iloc[i+1,1]=''
for i in range(0,len(link)):
  if str(link.iloc[i,1])=='공공':
    if str(link.iloc[i+1,1])=='의대':
      link.iloc[i,1]='공공의대'
      link.iloc[i+1,1]=''
for i in range(0,len(link)):
  if str(link.iloc[i,1])=='공공':
    if str(link.iloc[i+1,1])=='의료':
      link.iloc[i,1]='공공의료'
      link.iloc[i+1,1]=''
for i in range(0,len(link)):
  if str(link.iloc[i,1])=='소상':
    if str(link.iloc[i+1,1])=='공인':
      link.iloc[i,1]='소상공인'
      link.iloc[i+1,1]=''
for i in range(0,len(link)):
  if str(link.iloc[i,1])=='자가':
    if str(link.iloc[i+1,1])=='격리':
      link.iloc[i,1]='자가격리'
      link.iloc[i+1,1]=''
for i in range(0,len(link)):
  if str(link.iloc[i,1])=='자가':
    if str(link.iloc[i+1,1])=='진단':
      link.iloc[i,1]='자가진단'
      link.iloc[i+1,1]=''
for i in range(0,len(link)):
  if str(link.iloc[i,1])=='사랑':
    if str(link.iloc[i+1,1])=='제일':
      if str(link.iloc[i+2,1])=='교회':
        link.iloc[i,1]='사랑제일교회'
        link.iloc[i+1,1]=''
        link.iloc[i+2,1]=''
for i in range(0,len(link)):
  if str(link.iloc[i,1])=='세계':
    if str(link.iloc[i+1,1])=='보건':
      if str(link.iloc[i+2,1])=='기구':
        link.iloc[i,1]='세계보건기구'
        link.iloc[i+1,1]=''
        link.iloc[i+2,1]=''
for i in range(0,len(link)):
  if str(link.iloc[i,1])=='박':
    if str(link.iloc[i+1,1])=='시장':
      link.iloc[i,1]='박원순'
      link.iloc[i+1,1]=''
for i in range(0,len(link)):
  if str(link.iloc[i,1])=='우':
    if str(link.iloc[i+1,1])=='한':
      link.iloc[i,1]='우한'
      link.iloc[i+1,1]=''
for i in range(0,len(link)):
  if str(link.iloc[i,1])=='강':
    if str(link.iloc[i+1,1])=='대변인':
      link.iloc[i,1]='강경화'
      link.iloc[i+1,1]=''
for i in range(0,len(link)):
  if str(link.iloc[i,1])=='한국':
    if str(link.iloc[i+1,1])=='발':
      link.iloc[i,1]='한국발'
      link.iloc[i+1,1]=''
for i in range(0,len(link)):
  if str(link.iloc[i,1])=='검':
    if str(link.iloc[i+1,1])=='체':
      link.iloc[i,1]='검체'
      link.iloc[i+1,1]=''
for i in range(0,len(link)):
  if str(link.iloc[i,1])=='강경':
    if str(link.iloc[i+1,1])=='외교부':
      if str(link.iloc[i+2,1])=='장관':
        link.iloc[i,1]='강경화'
        link.iloc[i+1,1]=''
        link.iloc[i+2,1]=''
    elif str(link.iloc[i+1,1])=='외교':
      if str(link.iloc[i+2,1])=='장관':
        link.iloc[i,1]='강경화'
        link.iloc[i+1,1]=''
        link.iloc[i+2,1]=''
    elif str(link.iloc[i+1,1])=='장관':
      link.iloc[i,1]='강경화'
      link.iloc[i+1,1]=''
for i in range(0,len(link)):
  if str(link.iloc[i,1])=='국':
    if str(link.iloc[i+1,1])=='격':
      link.iloc[i,1]='국격'
      link.iloc[i+1,1]=''
for i in range(0,len(link)):
  if str(link.iloc[i,1])=='차':
    if str(link.iloc[i+1,1])=='벽':
      link.iloc[i,1]='차벽'
      link.iloc[i+1,1]=''
for i in range(0,len(link)):
  if str(link.iloc[i,1])=='중국':
    if str(link.iloc[i+1,1])=='발':
      link.iloc[i,1]='중국발'
      link.iloc[i+1,1]=''
for i in range(0,len(link)):
  if str(link.iloc[i,1])=='국민':
    if str(link.iloc[i+1,1])=='힘':
      link.iloc[i,1]='국민의힘'
      link.iloc[i+1,1]=''
for i in range(0,len(link)):
  if str(link.iloc[i,1])=='개천절':
    if str(link.iloc[i+1,1])=='집회':
      link.iloc[i,1]='개천절집회'
      link.iloc[i+1,1]=''
for i in range(0,len(link)):
  if str(link.iloc[i,1])=='광화문':
    if str(link.iloc[i+1,1])=='집회':
      link.iloc[i,1]='광화문집회'
      link.iloc[i+1,1]=''
for i in range(0,len(link)):
  if str(link.iloc[i,1])=='엄마':
    if str(link.iloc[i+1,1])=='부대':
      link.iloc[i,1]='엄마부대'
      link.iloc[i+1,1]=''
for i in range(0,len(link)):
  if str(link.iloc[i,1])=='전광훈':
    if str(link.iloc[i+1,1])=='목사':
      link.iloc[i,1]='전광훈목사'
      link.iloc[i+1,1]=''
for i in range(0,len(link)):
  if str(link.iloc[i,1])=='베이':
    link.iloc[i,1]='후베이'
  elif str(link.iloc[i,1])=='신천지예수교':
    link.iloc[i,1]='신천지'
  elif str(link.iloc[i,1])=='민주당':
    link.iloc[i,1]='더불어민주당'
  elif str(link.iloc[i,1])=='신천지예수교 증거장막성전':
    link.iloc[i,1]='신천지'
  elif str(link.iloc[i,1])=='의과대학':
    link.iloc[i,1]='의대'
  elif str(link.iloc[i,1])=='대한의사협회':
    link.iloc[i,1]='의협'
  elif str(link.iloc[i,1])=='중동호흡기증후군':
    link.iloc[i,1]='메르스'
  elif str(link.iloc[i,1])=='비대':
    link.iloc[i,1]='비대면'
  elif str(link.iloc[i,1])=='우하':
    link.iloc[i,1]='우한'

2.2. 너무 많이 나오는 단어 제거하기

for i in range(0,len(link)):
  if str(link.iloc[i,1])=='뉴시스':
    link.iloc[i,1]=''
  elif str(link.iloc[i,1])=='국민일보':
    link.iloc[i,1]=''
  elif str(link.iloc[i,1])=='연합뉴스':
    link.iloc[i,1]=''
  elif str(link.iloc[i,1])=='중대본':
    link.iloc[i,1]=''
  elif str(link.iloc[i,1])=='질본':
    link.iloc[i,1]=''
  elif str(link.iloc[i,1])=='질병관리본부':
    link.iloc[i,1]=''
  elif str(link.iloc[i,1])=='질병관리본부장':
    link.iloc[i,1]=''  
  elif str(link.iloc[i,1])=='코로나바이러스':
    link.iloc[i,1]=''  
  elif str(link.iloc[i,1])=='코로나':
    link.iloc[i,1]=''  
  elif str(link.iloc[i,1])=='신종':
    link.iloc[i,1]=''
  elif str(link.iloc[i,1])=='이날':
    link.iloc[i,1]=''
  elif str(link.iloc[i,1])=='기자':
    link.iloc[i,1]=''        
  elif str(link.iloc[i,1])=='매일일보':
    link.iloc[i,1]=''        
  elif str(link.iloc[i,1])=='의원':
    link.iloc[i,1]=''        
  elif str(link.iloc[i,1])=='대표':
    link.iloc[i,1]=''        
  elif str(link.iloc[i,1])=='장관':
    link.iloc[i,1]=''        
  elif str(link.iloc[i,1])=='총리':
    link.iloc[i,1]=''        
  elif str(link.iloc[i,1])=='위원장':
    link.iloc[i,1]=''

2.3. 의미없는 단어 제거 및 한 글자인 경우 제거하기

from google.colab import drive
drive.mount('/content/drive')
import pandas as pd
dd = pd.read_excel('/content/drive/MyDrive/제거문자.xlsx')

def find_index(data, target):
  res = []
  lis = data
  while True:
    try:
      res.append(lis.index(target) + (res[-1]+1 if len(res)!=0 else 0)) #+1의 이유 : 0부터 시작이니까
      lis = data[res[-1]+1:]
    except:
      break     
  return res
aa=[]
for i in range(0,len(dd)):
  aa+=find_index(link.iloc[:,1].tolist(),dd.iloc[i,0])
for j in range(0,len(aa)):
  link.iloc[aa[j],1]=''

ii=[]
for i in range(0,len(link)):
  if link.iloc[i,1]!='':
    ii+=[i]
link_1=link.iloc[ii,:]

for i in range(0,len(link_1)):
  if len(link_1.iloc[i,1])==1:
    link_1.iloc[i,1]=''
ii=[]
for i in range(0,len(link_1)):
  if link_1.iloc[i,1]!='':
    ii+=[i]
link_2=link_1.iloc[ii,:]
link_2

[link_2 데이터프레임 예시]

3. 기사별로 단어들을 합치기

!pip3 install gensim
import gensim
link_2=link_2.reset_index(drop=True)
grouped =link_2.groupby(['Index'])
for key, group in grouped:
  globals()['Phrase_{}'.format(key)]=list(group.Phrase)
  globals()['Index_{}'.format(key)]=list(group.Index)[0]
  globals()['Text_{}'.format(key)]=list(group.Text)[0]  
  globals()['Title_{}'.format(key)]=list(group.Title)[0]  
  globals()['Paper_{}'.format(key)]=list(group.Paper)[0]  
  globals()['Date_{}'.format(key)]=list(group.Date)[0] 
Phrase=[]
Index=[]
Text=[]
Title=[]
Paper=[]
Date=[]
for key in range(470):
  Phrase.append(globals()['Phrase_{}'.format(key)])
  Index.append(globals()['Index_{}'.format(key)])
  Text.append(globals()['Text_{}'.format(key)])
  Title.append(globals()['Title_{}'.format(key)])
  Paper.append(globals()['Paper_{}'.format(key)])
  Date.append(globals()['Date_{}'.format(key)])
link_3=pd.DataFrame({'Index':Index,'Phrase':Phrase,'Text':Text,'Title':Title,'Paper':Paper,'Date':Date})
link_3

[link_3 데이터프레임 예시]

4. 빈도수가 높은 단어, 너무 낮은 단어 제거하기

from gensim import corpora
tokenized_doc = link_3.Phrase
dictionary = corpora.Dictionary(tokenized_doc)
dictionary.filter_extremes(no_below=5,no_above=0.8)
text=tokenized_doc
corpus = [dictionary.doc2bow(text) for text in tokenized_doc]

5. Gensim 패키지를 이용해 토픽모델링하기

5.1. 토픽수 구하기

#토픽모델링-토픽수
per=[]
import matplotlib.pyplot as plt
from gensim import models
for i in range(1,11):
  ldamodel = gensim.models.ldamodel.LdaModel(corpus, num_topics = i, id2word=dictionary, passes=500,random_state=2500,gamma_threshold=0.01,alpha='auto')
  cm = models.CoherenceModel(model=ldamodel, corpus=corpus, coherence='u_mass')
  coherence = cm.get_coherence()
  per.append(coherence)
limit=11; start=1; step=1;
x = range(start, limit, step)
plt.plot(x, per)
plt.xlabel("Num Topics")
plt.ylabel("Coherence score")
plt.legend(("per"), loc='best')
plt.show()

[토픽 수 그래프]

5.2. 토픽모델링하기

NUM_TOPICS = 3 
ldamodel = gensim.models.ldamodel.LdaModel(corpus, num_topics = NUM_TOPICS, id2word=dictionary, passes=500,random_state=2500,gamma_threshold=0.01,alpha='auto')
topics = ldamodel.print_topics(num_words=20)
for topic in topics:
    print(topic)

5.3. 기사별 토픽 할당하기

#토픽모델링-문서
def make_topictable_per_doc(ldamodel, corpus):
    topic_table = pd.DataFrame()
    for i, topic_list in enumerate(ldamodel[corpus]):
        doc = topic_list[0] if ldamodel.per_word_topics else topic_list            
        doc = sorted(doc, key=lambda x: (x[1]), reverse=True)
        for j, (topic_num, prop_topic) in enumerate(doc): 
            if j == 0:  
                topic_table = topic_table.append(pd.Series([int(topic_num), round(prop_topic,4), topic_list]), ignore_index=True)
            else:
                break
    return(topic_table)
topictable = make_topictable_per_doc(ldamodel, corpus)
topictable = topictable.reset_index() 
a=topictable.iloc[:,0]
b=topictable.iloc[:,1]
c=topictable.iloc[:,2]
link_3['Topic']=b.tolist()
link_3['Frequency']=c.tolist()

[기사별 토픽 할당 결과]

6. 최종 결과

주제 주제명 주제비중
주제1 코로나 19 발생 초기 정부 대응에 대한 국내외 평가 50%
주제2 코로나 19 장기화로 인한 정부의 방역 정책 논란 24%
주제3 정부의 집회 대응에 대한 논란 26%

오늘은 저의 학위논문 CODE를 올려봅니다.

논문 분야는 텍스트 마이닝으로, 크게 감성분석, 토픽모델링, 대응분석으로 구성됩니다.

대응분석은 SAS로 수행했고 감성분석, 토픽모델링은 Google Colab 환경에서 Python 3.0으로 진행했습니다.

 

당시 학위논문을 쓰면서 텍스트 마이닝에 흥미를 더욱 가지며 졸업 후, Word Embedding과 Deep Learning으로 공부 영역을 확장했습니다. 현재도, 텍스트 마이닝, NLP 는 가장 관심있는 분야입니다.

 

그럼, 감성분석 CODE과 토픽모델링 CODE를 올려보도록 하겠습니다.

 

목차

1. 필요한 패키지 설치

2. 감성 사전 불러오기(감성사전은 분석하는 텍스트가 '뉴스 기사'인 점을 고려해 Bing 감성사전을 번역해 활용했습니다)

3. 감성 사전 품사 추출하기

4. 방역 기사 불러오기

5. 방역 기사 품사 추출하기

6. 기사에 감성을 적용해, 감성 점수 구하기

6-1. 방역 기사 속 품사들과 사전 속 품사를 매치하여 감성 점수를 구하기

6-2. 기사별로 감성 점수를 합치기

 

 

1. 필요한 패키지 설치

!apt-get update 
!apt-get install g++ openjdk-8-jdk python-dev python3-dev 
!pip3 install JPype1-py3 
!pip3 install konlpy 
!JAVA_HOME="C:\Users\tyumi\Downloads"

 

2. 감성 사전 불러오기

from google.colab import files
myfile = files.upload()
import io
import pandas as pd
dict_0 = pd.read_excel(io.BytesIO(myfile['Bing 번역 최종.xlsx']))
dict_0.head()

감성사전 예시

 

3. 감성 사전 품사 추출하기

from konlpy.tag import * # class  
komoran=Komoran()
index=[]
words=[]
score=[]
text_1=[]
for j in range(0,len(dict_0)):
    ex_pos=komoran.pos(dict_0.iloc[j,0])
    for (text, tclass) in ex_pos : # ('형태소', 'NNG') 
        if tclass == 'VV' or tclass == 'VA' or tclass == 'NNG' or tclass=='XPN' or tclass == 'NNP'or tclass=='VX' or tclass == 'VCP' or tclass == 'VCN' or tclass == 'MAG' or tclass == 'XR': 
            index+=[j]
            words+=[dict_0.iloc[j,0]]
            score+=[dict_0.iloc[j,1]]
            text_1.append(text)
dict_1=pd.DataFrame({'Index':index,'Text':text_1,'Words':words,'Score':score})

품사 추출한 감성사전 예시

 

4. 방역 기사 불러오기

from google.colab import files
myfile = files.upload()
import io
import pandas as pd
article_0 = pd.read_excel(io.BytesIO(myfile['방역 기사 추출.xlsx']))
article_0.head()

방역 기사 예시

 

5. 방역 기사 품사 추출하기

import re
index=[]
paper=[]
text_1=[]
text_2=[]
title=[]
for i in range(0,len(article_0)):
  article_0.iloc[i,0]=re.sub('\n|\r','',article_0.iloc[i,0]) #문장에서 필요없는 문구 삭제
  ex_pos=komoran.pos(article_0.iloc[i,0]) #komoran 품사 태그 적용
  for (text, tclass) in ex_pos : # ('형태소', 'NNG') 
      if tclass == 'VV' or tclass == 'VA' or tclass == 'NNG' or tclass == 'NNP'or tclass=='XPN' or tclass=='VX' or tclass == 'VCP' or tclass == 'VCN' or tclass == 'MAG' or tclass == 'XR': 
          index+=[i]
          paper+=[article_0.iloc[i,3]]
          text_1+=[article_0.iloc[i,0]]
          text_2.append(text)
          title+=[article_0.iloc[i,4]]
article_1=pd.DataFrame({'Index':index,'Text':text_2,'Article':text_1,'Paper':paper,'Title':title})

품사 추출된 방역 기사 예시

 

6. 기사에 감성을 적용해, 감성 점수 구하기

6-1. 방역 기사 속 품사들과 사전 속 품사를 매치하여 감성 점수를 구하기

article_1.loc[:,'Score']=[0]*len(article_1)
dict_list=list(dict_1.loc[:,'Text'])
article_list=list(article_1.loc[:,'Text'])
for i in range(len(dict_list)):
  equal_list = list(filter(lambda x: (article_list[x]==dict_list[i]) and (len(article_list[x])>=2), range(len(article_list))))
  for j in equal_list:
    article_1.loc[j,'Score']=dict_1.loc[i,'Score']

품사별 감성 점수 예시

 

6-2. 기사별로 감성 점수를 합치기

article_0['Score']=article_1['Score'].groupby(article_1['Index']).sum()
article_0

기사별 감성 점수 예시

 

본 게시물은 리뷰기반 식당 추천 리스트를 생성하는 과정으로, 사용한 데이터는 망고플레이트의 제주도 맛집 데이터입니다. 리뷰 기반 감성분석으로 감성점수를 산출하고, [가격, 맛, 서비스, 분위기, 웨이팅] 카테고리별 음식점 추천 리스트를 생성했습니다.

 

*사용자의 위치를 반영하기 위해 Geocoding을 해서 반경 30Km이내 음식점으로 필터링했습니다.

Geocoding 은 네이버 Api를 이용했으며 R 코딩을 했습니다.

 

아이디어 구현 과정은 다음과 같습니다.

 

 

[목차]

1. 필요한 패키지 설치 및 임포트

2. 망고플레이트 제주도 맛집 링크 수집

3. 망고플레이트 제주도 맛집 식당명, 리뷰, 평점, 주소 등등 수집

4. 맛집 리뷰 형태소분석

5. 맛집 리뷰 단어 빈도 추출

6. Word2Vec과 단어-빈도 행렬을 이용한 리뷰와 카테고리별 연관성 점수 계산

7. 카테고리와 단어의 코사인 유사도 행렬 구하기

8. 단어=문서 행렬 (Term-Document Matrix)

9. 감성분석

10. 생성된 고객 A에 대한 추천리스트

 


 

1. 필요한 패키지 설치 및 임포트

 

!pip3 install beautifulsoup4

import csv
import time
import pandas as pd
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.common.exceptions import NoSuchElementException
from selenium import webdriver
import time
import pandas as pd
from selenium.webdriver.common.by import By
from bs4 import BeautifulSoup

 

2. 망고플레이트 제주도 맛집 링크 수집

 

wd = webdriver.Chrome('chromedriver',chrome_options=chrome_options)
wd.get("https://www.mangoplate.com/top_lists/1095_summerguide_jeju")

post_list = []
title_list=[]

for i in range(1,11):
  ll = wd.find_element(By.XPATH, "//*[@id='contents_list']/ul/li["+str(i)+"]/div/figure/figcaption/div/span/a").get_attribute('href')
  post_list.append(ll)

pageNum = 1

while pageNum < 7:

    element=wd.find_element(By.XPATH, '//*[@id="contents_list"]/div/button')
    wd.execute_script("arguments[0].click();", element)
    time.sleep(1)
    for i in range(pageNum*10+1,pageNum*10+11):
      try:
        ll = wd.find_element(By.XPATH, "//*[@id='contents_list']/ul/li["+str(i)+"]/div/figure/figcaption/div/span/a").get_attribute('href')
        post_list.append(ll)
      except NoSuchElementException:
        print(i)  
    pageNum += 1
    
post_infos = pd.DataFrame({'link':post_list})

post_infos.to_csv('data_공모전_link.csv',index=False)

 

3. 망고플레이트 제주도 맛집 식당명, 리뷰, 평점, 주소 등등 수집

 
title=[]
review=[]
ID=[]
taste=[]
load=[]
t_1=[] #가고싶다수
t_2=[] #평점

for jj in range(len(post_infos)):
  pageNum=0

  wd = webdriver.Chrome('chromedriver',chrome_options=chrome_options)
  wd.get(post_infos.loc[jj,'link'])
  N=wd.find_element(By.XPATH,'/html/body/main/article/div[1]/div[1]/div/section[3]/header/ul/li[1]/button/span').text

  if int(N)<=5:  
    req = wd.page_source
    soup=BeautifulSoup(req, 'html.parser')
    aa = soup.find_all('p',"RestaurantReviewItem__ReviewText")
    bb= soup.find_all('span','RestaurantReviewItem__RatingText')
    cc=soup.find_all('span','RestaurantReviewItem__UserNickName')
    dd=soup.find('h1','restaurant_name').text
    ee=soup.find('div','Restaurant__InfoItemLabel--RoadAddressText').text
    ff=soup.find('span','cnt favorite').text
    gg=soup.find('strong','rate-point').text

    for i in range(len(aa)):
      review.append(aa[i].text)
      taste.append(bb[i].text)
      ID.append(cc[i].text) 
      title.append(dd)
      load.append(ee)
      t_1.append(ff)
      t_2.append(gg)

  if int(N)%5==0:
    N_1=int(N)//5-1
  else:
    N_1=int(N)//5

  for _ in range(N_1+1):
    element=wd.find_element(By.XPATH, '/html/body/main/article/div[1]/div[1]/div/section[3]/div[2]')
    wd.execute_script("arguments[0].click();", element)
    time.sleep(3)

  req = wd.page_source
  soup=BeautifulSoup(req, 'html.parser')
  aa = soup.find_all('p',"RestaurantReviewItem__ReviewText")
  bb= soup.find_all('span','RestaurantReviewItem__RatingText')
  cc=soup.find_all('span','RestaurantReviewItem__UserNickName')
  dd=soup.find('h1','restaurant_name').text
  ee=soup.find('div','Restaurant__InfoItemLabel--RoadAddressText').text
  ff=soup.find('span','cnt favorite').text
  gg=soup.find('strong','rate-point').text

  for i in range(len(aa)):
    review.append(aa[i].text)
    taste.append(bb[i].text)
    ID.append(cc[i].text) 
    title.append(dd)
    load.append(ee)
    t_1.append(ff)
    t_2.append(gg)

TOTAL = pd.DataFrame({'review':review, 'ID':ID, 'taste':taste, 'title':title,'load':load, '가고싶다':t_1, '평점':t_2})

for i in range(len(TOTAL['review'])-1):
  if TOTAL.loc[i,'review'] in TOTAL.loc[i+1:,'review'].tolist():
    print(TOTAL.loc[i,'review'])

TOTAL.to_csv('data_공모전_RAW.csv',index=False)
 
 

4. 맛집 리뷰 형태소분석

 

from google.colab import drive
drive.mount('/content/drive')
import pandas as pd
TOTAL=pd.read_csv('/content/drive/MyDrive/data_공모전_RAW_1116.csv',sep=',',encoding="cp949")
import re
okt=Okt()
ii=[]
data_1=TOTAL.dropna(axis=0)
PP=[]
AA=[]
BB=[]
CC=[]
DD=[]
EE=[]
FF=[]
PP_1=[]

for i in range(len(data_1)):
  PP.append(data_1.iloc[i,0]) #review
  PP_1.append(data_1.iloc[i,0]) #raw
  AA.append(data_1.iloc[i,1]) #ID
  BB.append(data_1.iloc[i,2]) #title
  CC.append(data_1.iloc[i,3]) #taste
  DD.append(data_1.iloc[i,4]) #load
  EE.append(data_1.iloc[i,5])
  FF.append(data_1.iloc[i,6])

for i in range(len(PP)):
  PP[i]=re.sub("[^ㄱ-ㅎㅏ-ㅣ가-힣 ]"," ", PP[i])
  PP[i]=okt.morphs(PP[i],norm=True, stem=True)
  PP[i]=[x for x in PP[i] if len(x)>1]
  PP[i]=' '.join(PP[i])

data_2=pd.DataFrame({'review':PP,'ID':AA,'title':BB,'taste':CC,'raw':PP_1,'load':DD,'가고싶다':EE,'평점':FF})

hangul = re.compile('[ㄱ-ㅣ가-힣]+')

for i in range(len(data_2)):
  if len(hangul.findall(data_2.iloc[i,0]))>1:
    if len(data_2.iloc[i,0])>1:
      ii.append(i)

data_3=data_2.iloc[ii,:]
data_3.to_csv('data_공모전_Noun_1116.csv',index=False)

 

5. 맛집 리뷰 단어 빈도 추출

 

##단어 빈도 추출 결과, 카테고리: 가격, 맛, 분위기, 서비스, 웨이팅 카테고리를 생성함
A=data_3.review.to_list()
B=[]
for i in range(len(A)):
  B+=A[i].split(' ')
words_count={}
for word in B:
    if word in words_count:
        words_count[word] += 1
    else:
        words_count[word] = 1
sorted_words = sorted([(k,v) for k,v in words_count.items()], key=lambda word_count: -word_count[1])
print([w[0] for w in sorted_words])

 

6. Word2Vec과 단어-빈도 행렬을 이용한 리뷰와 카테고리별 연관성 점수 계산

 

##Word2Vec 카테고리별 코사인 유사도가 높은 단어를 구할 수 있다.
X=[[]]*len(data_3)
for i in range(len(data_3)):
    X[i] = data_3.iloc[i,0].split(' ')

from gensim.models import Word2Vec
model_w = Word2Vec(sentences=X,size=500,window=5,min_count=2,workers=4,sg=1)
df1=pd.DataFrame(model_w.wv.most_similar('맛있다',topn=20),columns=['단어','맛있다'])
df2=pd.DataFrame(model_w.wv.most_similar('가격',topn=20),columns=['단어','가격'])
df3=pd.DataFrame(model_w.wv.most_similar('웨이팅',topn=20),columns=['단어','웨이팅'])
df4=pd.DataFrame(model_w.wv.most_similar('서비스',topn=20),columns=['단어','서비스'])
df5=pd.DataFrame(model_w.wv.most_similar('분위기',topn=20),columns=['단어','분위기'])

 

A=list(set(df1.단어.tolist()+df2.단어.tolist()+df3.단어.tolist()+df4.단어.tolist()+df5.단어.tolist()))
A=sorted(A)
pd.DataFrame(A).to_csv('data_공모전_단어리스트_ver4.csv',index=False)

 

7. 카테고리와 단어의 코사인 유사도 행렬 구하기

 

##카테고리와 단어의 코사인 유사도 행렬을 구할 수 있다.
from google.colab import drive
drive.mount('/content/drive')
import pandas as pd
A=pd.read_csv('/content/drive/MyDrive/data_공모전_단어리스트_ver4_1.csv',sep=',',encoding="cp949")
A=A.iloc[:,0].tolist()
A=sorted(A)

B_맛있다=[]
B_가격=[]
B_웨이팅=[]
B_서비스=[]
B_분위기=[]
for i in range(len(A)):
  B_맛있다.append(model_w.wv.similarity('맛있다',A[i]))
  B_가격.append(model_w.wv.similarity('가격',A[i]))
  B_웨이팅.append(model_w.wv.similarity('웨이팅',A[i]))
  B_서비스.append(model_w.wv.similarity('서비스',A[i]))
  B_분위기.append(model_w.wv.similarity('분위기',A[i]))
B_total=pd.DataFrame({'맛있다':B_맛있다,'가격':B_가격,'웨이팅':B_웨이팅,'서비스':B_서비스,'분위기':B_분위기}).transpose()
B_total.columns=[A]
B_total[B_total<0.8]=0
B_total.to_csv('data_공모전_단어리스트_ver3_1.csv',index=False)

 

8. 단어=문서 행렬 (Term-Document Matrix)

 

from sklearn.feature_extraction.text import CountVectorizer
cv=CountVectorizer()
for i in range(len(X)):
  globals()['val_{}'.format(i)]=[]
B=[]
for i in range(len(X)):
  for j in X[i]:
    if j in A:
      globals()['val_{}'.format(i)].append(j)
      B.append(i)
C=[]
for j in range(len(X)):
  if j not in list(set(B)):
    C.append('')
  else:
    C.append(' '.join(globals()['val_{}'.format(j)]))
 DTM_array=cv.fit_transform(C).toarray()
 feature_names=cv.get_feature_names()
 DTM_DataFrame=pd.DataFrame(DTM_array,columns=feature_names)
##카테고리별 리뷰의 연관성을 알 수 있음
import numpy as np
Result=B_total.to_numpy() @ DTM_DataFrame.transpose().to_numpy()
round(pd.DataFrame(Result),4).to_csv('공모전_Relev_결과_1119_2.csv')
B_total.to_csv('공모전_Word2Vec_결과_1119_2.csv')
DTM_DataFrame.to_csv('공모전_DTM_결과_1119_2.csv')

 

9. 감성분석

 

##KNU 감성사전에서 팀원과 의논에 음식특화사전을 구축함, lexicon_1_최종사전
from google.colab import drive
drive.mount('/content/drive')
import pandas as pd
lexicon=pd.read_csv('/content/drive/MyDrive/lexicon_1_최종사전.csv',sep=',',encoding="cp949")
X=[[]]*len(data_3)
for i in range(len(data_3)):
    X[i] = data_3.iloc[i,0].split(' ')
    
##감성점수를 부여하는 과정
A=[0]*len(data_3)
B=[0]*len(data_3)
C=[0]*len(data_3)
for i in range(len(data_3)):
  for j in range(len(X[i])):
    if X[i][j] in lexicon.word.tolist():
      A[i]+=lexicon.polarity.tolist()[lexicon.word.tolist().index(X[i][j])]
      B[i]+=1
for i in range(len(C)):
  if B[i]!=0:
    C[i]=round(int(A[i])/int(B[i]),2)
data_3[['감성점수']]=C
data_3.to_csv('data_공모전_Sent_1121_1.csv',index=False)

 

10. 생성된 고객A에 대한 추천리스트

추천점수 계산 방식: [(감성점수 * 연관성점수)+0.5(가고싶다수+평점)]

감성점수와 가고싶다수, 평점을 PCA분석한 결과, 감성점수 변수의 영향력이 컸습니다.

따라서 나머지 변수에 가중치를 0.5 곱했습니다.

 

제주도 맛집 추천리스트는 아래와 같습니다.

 순위 전체 가격 웨이팅 서비스 분위기
1 밀리우 스시호시카이 스시호시카이 밀리우 밀리우 밀리우
2 상춘재 상춘재 상춘재 상춘재 상춘재 상춘재
3 미영이네식당 밀리우 밀리우 스시호시카이 스시호시카이 스시호시카이
4 중문수두리보말칼국수 미영이네식당 숙성도 미영이네식당 미영이네식당 미영이네식당
5 숙성도 숙성도 오드랑베이커리 숙성도 숙성도 숙성도
6 스시호시카이 중문수두리보말칼국수 중문수두리보말칼국수 더스푼 중문수두리보말칼국수
7 진아떡집 더스푼 더스푼 더스푼 어우늘 더스푼
8 어우늘 우진해장국 어우늘 천짓골식당 중문수두리보말칼국수 어우늘
9 국수만찬 어우늘 천짓골식당 우진해장국 우진해장국 우진해장국
10 더스푼 천짓골식당 국수만찬 진아떡집 만선식당 진아떡집

 

 

* 데이콘(DACON)에서 주최한 '뉴스 토픽 분류 AI 경진대회' 모형을 공유하고자 게시물을 올렸습니다.

* 이 대회는 특정 모형으로 한국어 뉴스 헤드라인 데이터를 학습한 후 주제를 분류하는 것입니다.

 

Bi-LSTM 기법을 이용하여 한국어 뉴스 토픽 분류 모형을 만들었습니다. 임베딩은 Fasttext를 사용하였습니다.

 

먼저 Fasttext 임베딩 코드를 보여드리며 설명한 후 Bi-LSTM 기법을 적용하는 코드를 보여드리며 설명하겠습니다.

*코드는 Google Colaboratory 기준입니다

 

항목은 다음과 같습니다.

 

1. 필요한 패키지 설치

2. 데이터 설명 및 전처리

3. Fasttext 이용, 단어를 벡터로 변환

4. Bi-LSTM 기법 적용

 

그럼 시작하겠습니다.

 

1. 필요한 패키지 설치


[CODE]

!apt-get update 
!apt-get install g++ openjdk-8-jdk python-dev python3-dev 
!pip3 install JPype1-py3 
!pip3 install konlpy 
!JAVA_HOME="C:\Users\tyumi\Downloads"
from konlpy.tag import *  #한국어 형태소 분석
okt=Okt()
!pip3 install fasttext #fasttext 설치
import fasttext
import pandas as pd 
import re #정규표현식
from tqdm import tqdm_notebook
import numpy as np
from keras.layers import LSTM, Activation, Dropout, Dense, Input,Bidirectional
from keras.layers.embeddings import Embedding
from keras.models import Model
from keras import layers
from keras.callbacks import EarlyStopping
import keras
from keras.models import Sequential
from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences

 

2. 데이터 설명 및 전처리


[CODE 2-1]

from google.colab import drive
drive.mount('/content/drive') 


test=pd.read_csv('/content/drive/MyDrive/test_data.csv')
train=pd.read_csv('/content/drive/MyDrive/train_data.csv')
sample_sub=pd.read_csv('/content/drive/MyDrive/sample_submission.csv')
topic_dict=pd.read_csv('/content/drive/MyDrive/topic_dict.csv')

 

[Explanation 2-1]

 

[CODE 2-1]은 구글 마운트를 이용해 데이터를 불러오는 과정입니다.

 

[그림 1] train data
[그림 2] 주제

- [그림 1]은 train data 입니다. 45654개의 관측값을 갖고 있는 것을 알 수 있습니다.

- title은 저희가 분석할 뉴스 헤드라인이고 topic_idx는 기사에 할당된 주제입니다.

- [그림 2]에 주제에 대한 설명이 있습니다.

- test data는 9131개의 관측값을 갖고 있습니다.

 

[CODE 2-2]

t_list=[]

for t in range(len(train['title'])):

  train['title'][t]=' '.join(re.compile('[가-힣]+').findall(train['title'][t]))



t_list=[]

for t in range(len(test['title'])):

  test['title'][t]=' '.join(re.compile('[가-힣]+').findall(test['title'][t]))



for i in range(len(train)):

  train['title'][i]=okt.nouns(train['title'][i])



for i in range(len(test)):

  test['title'][i]=okt.nouns(test['title'][i])



data_train = train['title']

data_test = test['title']

 

[Explanation 2-2]

 

- 정규표현식을 이용하여 train, test data의 title에서 한글을 제외한 나머지를 제거합니다.

- okt를 이용하여 처리된 train, test data의 title에서 명사만 추출합니다.

 

[그림 3] data_train

- data_train은 [그림 3]과 같습니다. 이로써 각 문장은 명사 단위로 구성된 리스트가 되었습니다.

 

3. Fasttext 이용, 단어를 벡터로 변환


[CODE 3-1]

with open('/content/drive/MyDrive/data_train.txt', "w") as f:
  for i in range(len(data_train)):
    f.write(' '.join(data_train.iloc[i])+'\n')
  f.close()
with open('/content/drive/MyDrive/data_test.txt', "w") as f:
  for i in range(len(data_test)):
    f.write(' '.join(data_test.iloc[i])+'\n')
  f.close()


model = fasttext.train_unsupervised(input='/content/drive/MyDrive/data_train.txt',model = 'skipgram', lr = 0.05,
dim = 100, ws = 5, epoch = 50,
minn = 1, word_ngrams = 6)

 

[Explanation 3-1]

 

[그림 4]data_train

- fasttext 모델의 입력값 형태로 만들기 위해 데이터를 메모장 파일로 저장합니다.([그림 4]와 같습니다.)

- 이후 data_train 파일로 fasttext 모델을 훈련시킵니다. (초모수 설정은 이명호님의 석사 학위 논문 '단어 임베딩과 LSTM을 활용한 비속어 판별 방법'을 참고하였습니다.)

 

[CODE 3-2]

#정수인덱싱과정
tokenizer = Tokenizer()
tokenizer.fit_on_texts(data_train)
vocab_size=len(tokenizer.word_index)+1
data_train = tokenizer.texts_to_sequences(data_train)
data_test = tokenizer.texts_to_sequences(data_test) 

#모든 문장에서 가장 긴 단어 벡터 길이 구하기
max_len=max(len(l) for l in data_train)

#max_len에 맞춰서 패딩하기
X_train = pad_sequences(data_train, maxlen = max_len)
X_test = pad_sequences(data_test, maxlen = max_len)

 

[Explanation 3-2]

 

- Tokenizer를 이용해서 data_train/data_test 에 있는 고유한 단어에 번호를 할당합니다. 

- 즉, data_train/data_test 를 정수 인덱싱 벡터로 변환하는 것입니다.

- 모든 문장의 단어 벡터 길이는 같아야 됩니다. 같지 않는 경우, 0으로 패딩해서 길이를 맞춰줍니다.

- 따라서 모든 문장에서 최대 단어 벡터 길이를 구한 후 패딩합니다.

 

[CODE 3-3]

#임베딩행렬 구하기
dd=list(tokenizer.word_index.keys())
embedding_matrix=[np.zeros(shape=(100,),dtype=float)]
for i in range(len(dd)):
  embedding_matrix.append(model.get_word_vector(dd[i]))
embedding_matrix=np.array(embedding_matrix)

 

[Explanation 3-3]

 

- 학습시킨 fasttext model로 data_train을 구성하는 각 단어의 벡터를 구합니다.

- 모두 합친 벡터를 embedding_matrix라고 합니다.

 

[그림 5] embedding_matrix

단어 단어벡터
대통령 [-0.04712268, -0.03183127, -0.24798341, ..., 0.10834852, -0.00924296, -0.08876684]
[-0.01681509, -0.02997315, -0.04271703, ..., 0.1574153 , 0.00186034, -0.07043125]

 

- 위의 표는 단어벡터를 생성한 예시입니다. 각 단어벡터는 15차원으로 구성됩니다.

- [그림 5]는 embedding_matrix로, (24418, 100) 차원을 가졌습니다. 24418은 data_train을 구성하는 고유한 단어의 수를

   나타냅니다. 100은 각 단어의 벡터 차원을 의미합니다.

 

4. Bi-LSTM  계층 구현


앞에서 구한 embedding_matrix를 이용해 Bi-LSTM 계층을 구현하겠습니다.

 

[CODE 4-1]

target = pd.get_dummies(train['topic_idx']).values

x_train=X_train
x_test=X_test
y_train=target

 

[Explanation 4-1]

 

- 뉴스 기사의 주제 분류를 목적으로 분석하기 때문에 'topic_idx'를 Y변수로 놓습니다.

- topic_idx를 더미 변수로 변환합니다.

- 앞에서 구한 embedding_matrix를 임베딩 계층의 가중치로 입력할 것입니다.

 

[CODE 4-2]

max_len=15
vocab_size=24418
embedding_dim=100
model_5=keras.Sequential([
keras.layers.Embedding(vocab_size, embedding_dim, weights=[embedding_matrix],input_length=max_len,trainable=True), 
keras.layers.Bidirectional(LSTM(100, dropout=0.2, recurrent_dropout=0.2,return_sequences=True)),
keras.layers.Bidirectional(LSTM(100, dropout=0.2, recurrent_dropout=0.2,return_sequences=True)),
keras.layers.Bidirectional(LSTM(100, dropout=0.2, recurrent_dropout=0.2)),
keras.layers.Dense(7, activation='softmax')])

 

[Explanation 4-2]

 

- Keras는 인공 신경망 층을 구성하기 위해 Sequential()을 사용합니다.

- 첫번째 층은 Embedding 계층으로, 정수 인덱싱한 x_train 을 밀집 벡터로 맵핑합니다.

- 설명드리자면, 이 밀집 벡터는 신경망의 학습 과정에서 가중치가 학습되는 것과 같은 방식으로 훈련됩니다. 훈련 과정에서 단어는 모델이 풀고자하는 작업에 맞는 값으로 업데이트 됩니다.

- 두번째 층은 Bidirectional 계층이 감싼 LSTM 계층으로, 첫번째 요소값 100은 LSTM 계층을 거치며 출력될 차원값을 의미합니다.

- 세 개의 Bi-LSTM 계층을 쌓은 후 나오는 결과에 softmax함수를 적용합니다. 

- 이를 통해 각 관측값이 7개의 주제에 할당될 확률을 구할 수 있습니다.

 

[그림 6] model_5.summary() 결과

- [그림 6]은 model_5.summary() 결과로, 각 계층의 출력 차원을 확인하실 수 있습니다.

 

[CODE 4-3]

es = EarlyStopping(monitor='val_loss', mode='min', verbose=1, patience=4)
mc = ModelCheckpoint('best_model.h5', monitor='val_acc', mode='max', verbose=1, save_best_only=True)

from sklearn.model_selection import train_test_split
X_train, X_valid, y_train, y_valid = train_test_split(X_train, y_train, test_size=0.2, random_state=1000)

from tensorflow.keras.utils import to_categorical
model_5.compile(optimizer='sgd', loss='categorical_crossentropy', metrics=['accuracy'])
history = model_5.fit(X_train, y_train, epochs=15, callbacks=[es, mc], validation_data=(X_valid,y_valid),batch_size=60, validation_split=0.2)

predictions = model_5.predict(x_test)

pred_1=[]
for p in predictions:
  pred_1.append(np.argmax(p))

sample_sub['topic_idx']=pred_1
sample_sub.to_csv('Fasttext와3개BILSTM계층.csv',index=False)

[Explanation 4-3]

 

- 예측 결과 정확도가 81% 나왔습니다. 

+ Recent posts