티스토리 뷰

최근 몇년간 인공지능이 핫한 기술로 떠오르고 있는데, 인공지능의 전통적인 활용 분야 중 하나로 텍스트 마이닝 (Text Mining) 이 있다.

여기서 '마이닝' 이란 본래 광산 (Mine) 에서 채굴을 한다는 의미로, 텍스트 마이닝이라 하면 텍스트 속에서 뭔가 의미있는 정보를 뽑아내는 작업을 뜻한다.

 

텍스트 마이닝의 대표적인 사례로 SNS를 분석해서 현재 핫이슈가 무엇인지, 어떤 사건에 대한 여론이 어떤지를 파악하거나 상품에 대한 리뷰를 분석해서 해당 상품의 특성이나 문제점이 무엇인지를 파악하는 것 등이 있을 수 있겠다. 이외에도 텍스트 마이닝은 현대 사회에서 정말 많이 쓰이는 기술이다.

 

본 포스팅에서는 인공지능 분야에서 최근 가장 많이 쓰이는 언어 중 하나인 파이썬을 이용해서 영화 리뷰에 대한 텍스트 마이닝을 하는 프로그램을 만들어보는 과정을 다룬다.

 


 

먼저 과정을 생각해보자.

일단 분석할 영화 리뷰가 준비되어 있어야 한다. 그런데 내가 영화 리뷰 사이트를 운영하는게 아닌 이상, 내 컴퓨터에 영화 리뷰가 저장되어 있을 리는 없다. 영화 리뷰는 인터넷 상에 올라와 있다. 이걸 긁어와서 쓸 것이다.

이렇게 인터넷, 웹 페이지에 올라와있는 정보를 내가 만든 프로그램에서 가져다 쓰는 것을 웹 크롤링 (Web Crawling) 이라 부른다.

 

영화 리뷰 사이트에 써 있는 내용들을 내 프로그램으로 가져왔으면 이제 이걸 가공해서 써먹어야 한다. 여기서부터 쓰이는 기술은 자연어 처리 (NLP: Natural Language Processing) 라고 부른다. 텍스트 마이닝이라는 용어랑은 비슷하면서도 좀 다른데, 텍스트 마이닝은 텍스트로부터 의미있는 정보를 추출하는 활동에 초점이 맞춰진 용어인 반면, NLP라고 하면 정보를 뽑아내는 것 뿐 아니라 그걸 편하게 하기 위해 문장을 쪼개고 변형시키고 정리하는 등의 활동을 포괄한다.

 

영화 리뷰를 어떻게 써먹을까? 여러 가지가 있겠지만 여기서는 다음의 두 가지를 해볼 것이다.

1) 리뷰 텍스트가 얼마나 긍정적/부정적인지 분석하기. 이러한 과정을 감정 분석 (Sentiment Analysis) 이라 부른다.

2) 영화의 주요 키워드를 분석하고, 워드클라우드 (wordcloud) 를 이용해서 시각적으로 나타내기.

 

그리고 이걸 쉽게 하기 위해서 긁어온 텍스트 뭉텅이를 리뷰별로 쪼개고 문장별로 쪼개는 등의 과정이 들어갈 수 있는데 이러한 활동은 NLP에서 전처리 (Preprocessing) 과정이라 부른다.

정리하면 아래 그림과 같다.

 

 


 

1. BeautifulSoup4를 이용한 웹 크롤링

 

웹 페이지는 기본적으로 HTML 파일이다. HTML 파일의 내용을 그대로 긁어올 수도 있겠지만, 우리가 원하는 건 페이지 내에서 필요한 내용만 뽑아서 가져오는 것이다. 그러기 위해서는 태그들로 구성된 HTML 파일을 해석 (parsing) 해야 하는데, 이걸 도와주는 파이썬 라이브러리로 BeautifulSoup라는 게 있다.

 

파이썬 라이브러리니까 pip를 이용해서 설치한다.

 

pip install bs4

 

먼저 BeautifulSoup 사용의 기본이 되는 생성자 함수부터 살펴보자.

 

source = BeautifulSoup(webpage, 'html.parser', from_encoding='utf-8')

 

첫번째 파라미터는 갖고올 웹 페이지에 대한 객체를 넣는 부분인데, 그냥 웹 페이지의 URL을 넣으면 되는게 아니라 웹 페이지 객체를 넣어야 한다. URL을 통해 웹 페이지 객체를 얻기 위해서는 urllib라는 파이썬 기본 라이브러리의 내장 함수 urlopen() 의 도움을 받으면 된다.

 

webpage = urlopen('웹 페이지 URL 주소')

 

두번째 파라미터는 파이썬에 기본적으로 내장된 HTML parser를 쓰겠다는 지정이다. 이걸 바꾸면 XML같은 다른 포맷을 분석할 수도 있지만 이 포스팅에서 다루는 웹 크롤링에서는 html.parser면 충분하다.

세번째 파라미터는 웹 페이지의 인코딩 형식을 지정해주는 부분인데 일반적인 영어 웹페이지라면 utf-8로 대부분 될 것이다. 다만 특수문자나 다른 언어가 들어있을 경우 이걸 바꿔줘야 할 수도 있다.

 

생성자가 제대로 호출되었다면 'source' 객체에 웹 페이지의 내용이 담기게 된다. 이 source 객체로부터 텍스트를 추출해내기 위해서는 find, findAll 함수를 사용할 수 있다. 이 함수들의 기본 사용법은 이렇다.

 

source.findAll('태그이름')

 

source.findAll('div') 라고 쓰면 <div> 태그로 둘러싸여 있는 내용을 갖고오며, <div> 태그가 여러개 존재할 경우 다 갖고와서 리스트를 리턴한다. findAll 말고 그냥 find를 쓰면 여러개 있을때 다 갖고오는게 아니라 제일 처음 나오는 하나만 갖고온다.

 

예를 들어서 우리가 영화 리뷰를 가져올 사이트의 URL은 여기다. 세계 최대 영화 리뷰 사이트인 IMDB의 <어벤져스: 인피니티 워> 유저 리뷰 페이지다.

 

https://www.imdb.com/title/tt4154756/reviews?ref_=tt_ql_3

 

저 페이지에서 오른쪽 클릭 후 '페이지 소스 보기' 를 눌러보자. HTML 소스 코드가 나올텐데, Ctrl+F로 div를 검색해보면 div 태그가 들어가있는 위치가 쭉 보일 것이다. 이렇게 div로 둘러싸여있는 부분을 싹 가져온다는 것이다.

 

그런데 HTML 태그라는 것이 사실 unique하게 특정한 부분을 지칭하기 위해 쓰이는게 아니라, 원래는 다른 특정한 기능을 하기 위해 쓰이는 거다보니 태그만으로 필요한 부분을 정확하게 딱 집어내기는 쉽지 않다. 그래서 두 번째 파라미터로 태그와 함께 쓰이는 class나 id를 이용해 더 정확하게 위치를 지정해줄 수도 있다.

 

위 페이지에서 '리뷰 하나' 에 해당하는 HTML 코드가 어디부터 어디까지인지 알아보자.

 

왼쪽 사진은 실제 웹 페이지상에서 '리뷰 하나' 에 해당하는 영역을 캡처한 것이고, 오른쪽 사진은 저 부분에 해당하는 HTML 코드를 캡처한 것이다 (가독성을 위해 끝까지 캡처하진 않았고 시작 부분에서 제목, 날짜 나오는 부분까지만 캡처했다). HTML 코드에서도 왼쪽 사진과 같은 리뷰 제목, 날짜를 확인할 수 있다.

 

사진에 완전히 다 나오진 않았지만 오른쪽 사진의 첫 줄, <div class="lister-item mode-detail... 하는 이 부분부터, 이 div 태그가 닫히는 부분까지가 '리뷰 하나' 의 영역에 해당한다. 저 첫줄과 같은 레벨의 들여쓰기가 적용된 </div> 태그를 찾아보면 그 다음부터 똑같은 패턴이 반복되는 것을 볼 수 있다.

 

저 div 태그에 적용된 몇 개의 클래스들 중 이 부분의 특성을 가장 잘 나타내고 있는 단어는 'imdb-user-review' 일 것이다. 그러니까 '이 부분이 리뷰 영역입니다' 라고 써놓은 것이다. 이 클래스를 이용해서 리뷰 영역을 다음과 같이 빼올 수 있다.

 

review_list = source.findAll('div', {'class': 'imdb-user-review'})

 

이렇게 써주면 div 태그와 함께 imdb-user-review라는 클래스 이름으로 지정되어 있는 부분, 즉 각 리뷰 영역들을 가져오게 된다. review_list의 각 아이템이 리뷰 하나에 해당될 것이다.

 

각 리뷰 영역에 해당하는 HTML 코드를 갖고오긴 했는데, 여전히 군더더기가 많다. 왼쪽 사진을 다시 보자. 우리가 갖고와서 의미가 있을만한 내용은 다음과 같다.

 

- 점수 (별점)

- 리뷰 제목

- 작성자 닉네임

- 작성 날짜

- 리뷰 내용

 

그 외에 HTML 코드 중간에 star icon의 path가 어쩌구 하는 부분이라던지 마지막의 Was this review helpful? 같은 문장은 갖고와봐야 쓸모가 없다. 필요한 내용만 빼내기 위해서, 가져온 review_list의 각 아이템에다가 find, findAll 함수를 한번 더 적용할 수 있다. 

 

오른쪽 사진을 보면 점수는 <span> 태그로 둘러싸여 있고, 딱히 class라던지 id같은게 지정되어있진 않다. 근데 점수 부분 말고도 <span> 이 쓰인 곳이 위에 또 있다. 정확히는 위의 <span>이 점수 부분도 둘러싸고 있긴 한데, 점수 외의 다른 내용까지 포함하고 있는 것 같다. 다만, 위의 <span> 영역 안에 검은색으로 표시되는 직접적인 텍스트는 '10' 이랑 '/10' 밖에 없고 나머지는 다 HTML 태그다. 이런 경우엔 쉽게 빼올 수가 있는데,

 

score = review.find('span').get_text()

 

get_text() 함수를 쓰면 갖고온 객체에서 HTML 태그같은 건 빼버리고 직접 화면에 표시되는 텍스트만 추려낼 수 있다. 즉 '10/10' 을 갖고올 수 있다.

 

동일한 방식으로 리뷰 제목, 작성자, 날짜, 리뷰 내용도 가져올 수 있다.

 

2. NLTK tokenize() 를 이용한 전처리

 

NLTK (Natural Language ToolKit) 는 파이썬에서 자연어 처리에 쓰이는 가장 유명한 라이브러리이다. 워낙 담고있는 기능이 많아서 pip로 NLTK를 설치한다고 해서 모든 기능이 바로 설치되는게 아니라, NLTK Installer라는걸 한번 더 거쳐서 내부 모듈들을 선택적으로 설치할 수 있게 되어 있다.

 

NLTK를 설치하고,

 

from nltk import tokenize

 

위에서 뽑았던 리뷰 내용 텍스트 (content) 를 문장 단위로 잘라보자. tokenize.sent_tokenize() 함수를 써서 할 수 있다.

NLTK 내부 모듈이 설치되지 않았다는 에러 메시지가 뜰 수도 있는데, 만약 그런 메시지를 만난다면 그 메시지에서 시키는대로 파이썬을 따로 실행해서 import nltk -> nltk.download('모듈이름') 을 실행해주자.

 

lines_list = tokenize.sent_tokenize(content)

 

lines_list라는 리스트에 content를 구성하고 있던 각 문장들이 들어갔으면 성공이다. 이제 이 문장들을 이용해서 긍정, 부정을 분석해보자.

 

3. VADER를 이용한 감정 분석

 

VADER (Valence Aware Dictionary and sEntiment Reasoner) 는 NLTK에 내장된 모듈 중 하나로, 사전 기반 감정 분석 툴이다.

 

사전 기반이라는 것은 긍정적인 단어, 부정적인 단어의 리스트를 미리 사전 식으로 정리해두고 그 리스트를 바탕으로 긍정/부정을 확인하는 방법이다. 예를 들면 good은 +0.1점, awful은 -0.1점, perfect는 +0.2점 이런 식으로 미리 정해두고 문장에서 저런 단어들이 나올 때마다 점수를 더하고 빼서 점수가 양수면 긍정, 음수면 부정적인 문장으로 평가하는 것이다. 전통적으로 많이 사용되어 왔던 방법이지만 요즘은 머신러닝 기반의 분석 방법이 뜨면서 좀 시들한 느낌이긴 하다. 그래도 이 포스팅에선 기본적인 방법을 소개하는 거니까 이걸 써보자.

 

from nltk.sentiment.vader import SentimentIntensityAnalyzer

sid = SentimentIntensityAnalyzer()

 

이 sid라는 객체가 내장하고 있는 polarity_scores() 함수는 문장을 단어별로 분석해서 그 문장이 얼마나 긍정적인지 (pos), 중립적인지 (neu), 부정적인지 (neg) 에 대한 점수를 내주고 종합 점수 (compound) 도 내준다. 우리는 여기서 종합 점수를 써먹기로 하자. 리턴값이 딕셔너리 형식이기 때문에 'compound' 라는 키로 접근한다.

 

for sent in lines_list:

ss = sid.polarity_scores(sent)

print(ss['compound'])

 

원한다면 저 compound 점수값을 리뷰 옆에 따로 저장하거나, 리뷰의 원래 평점에 더해서 계산하거나 등의 일을 할 수가 있다.

 

4. wordcloud를 이용한 키워드 분석

 

wordcloud는 단어들을 그 중요도에 따라 크기를 달리하여 구름 모양으로 배치한 그림을 의미하는데, 뉴스 기사에서 SNS 동향분석 결과라느니 하면서 자주 써먹기 때문에 친숙할 것이다. 이걸 파이썬에서 만들어주기 위해서는 이름 그대로 wordcloud라는 라이브러리를 설치해주면 된다. 아, wordcloud를 그래픽으로 띄워줄 때 matplotlib라는 시각화 라이브러리를 이용하므로 matplotlib도 설치해줘야 한다.

 

그럼 wordcloud를 만들 수 있는 함수에 대해서 알아보자.

 

    wordcloud = WordCloud(font_path='framd.ttf', 
     width=2400, height=1800, 
     ranks_only=None, 
                          relative_scaling = 0.5, 
                          stopwords = set(STOPWORDS)
                          ).generate(text)

 

파라미터들을 하나씩 살펴보면 일단 font_path는 wordcloud에 뜰 단어들의 폰트를 설정해주는 부분이다. 예시처럼 쓰면 프로그램이랑 같은 폴더에 폰트 파일이 있어야 하고, 그게 아니라면 절대경로로 써주자.

 

width랑 height는 말 그대로 wordcloud가 뜨는 창의 크기를 설정해주는 부분이다.

 

ranks_only라는게 있는데 이 옵션을 켜주면 등장 빈도의 값이 아니라 순위에 따라 단어의 글자 크기가 결정된다. 뭔 소리냐면, ranks_only가 None인 경우에는 압도적으로 많이 등장하는 단어가 하나 있으면 그 단어의 크기가 엄청나게 커지고 다른 단어들은 작아지지만, 이 옵션을 켜주면 그런 단어가 있더라도 바로 다음으로 많이 등장하는 단어랑 순위는 1밖에 차이 안나기 때문에 글자 크기가 조금밖에 차이가 안나게 된다.

 

relative_scaling도 비슷한 역할인데 단어 등장 빈도에 따른 크기 차이를 조절하는 비율값이다. 0으로 설정하면 ranks_only를 켠 것과 같게 되고 숫자가 커질수록 많이 나오는 단어가 더 크게 나온다.

 

stopword란 문장을 분석할 때 의미가 없는 단어들의 모음을 뜻한다. 예를 들면 to, of, the, and 같은 전치사/관사/접속사들은 문장에 엄청나게 자주 나오지만 키워드라고 할 수 없으므로 단어를 카운트할 때 제외한다는 것이다. 이 stopword를 따로 지정해줄 수도 있지만, set(STOPWORDS) 라고 쓰면 기본적으로 wordcloud 라이브러리에 내장된 stopword 리스트를 사용한다.

 

이외에도 여러가지 파라미터들이 더 있지만 이정도만 써주기로 하자. generate(text) 에서 text는 모든 리뷰에 대한 content를 다 합친 문자열이다. 그리고 화면에 Graphical하게 띄우기 위해서는 저 생성 함수만 쓰는게 아니라 matplotlib의 함수를 이용해줘야 한다.

 

plt.imshow(wordcloud)

plt.axis("off")

plt.show()

 

movie, film, character같은 단어들이 별로 의미가 없다고 느껴진다면 이런 단어들도 stopword로 더 추가해줄 수도 있겠다. 그러면 타노스, 어벤져, 인피니티 스톤 같은 단어들이 더 부각될테니까.

 

아래는 프로그램의 전체 코드이다. 일반적인 텍스트 마이닝 과정과는 약간 동떨어진 내용이라 위에서 설명하진 않았는데, openpyxl이라는 엑셀 파일을 다루는 라이브러리를 추가해서 가져온 리뷰 내용과 분석 결과 점수를 엑셀 파일로 저장하도록 만들었다.

 

from urllib2 import urlopen  # 파이썬 3의 경우 from urllib.request import urlopen
from bs4 import BeautifulSoup
from nltk import tokenize
from nltk.sentiment.vader import SentimentIntensityAnalyzer
from openpyxl import Workbook
import matplotlib.pyplot as plt
from wordcloud import WordCloud, STOPWORDS

# openpyxl을 이용해 엑셀 파일로 저장하기 위한 준비 과정
excell = Workbook(write_only=True)
ws = excell.create_sheet()
ws.append(['score', 'title', 'writer', 'date', 'review', 'senti_score'])

# 웹 페이지 불러오기
url = 'https://www.imdb.com/title/tt4154756/reviews?ref_=tt_ql_3'
print(url)
webpage = urlopen(url)
source = BeautifulSoup(webpage, 'html.parser', from_encoding='utf-8')

review_list = source.findAll('div', {'class': 'imdb-user-review'})

sid = SentimentIntensityAnalyzer()   # VADER 감정분석기 미리 준비

sum_review = ''   # wordcloud 띄워줄때 쓸 모든 리뷰 텍스트 다 합친 문자열

 

# 각 리뷰마다...
for review in review_list:
    # 리뷰 내에서 태그, 클래스 이용해서 정보 뽑아내는 부분
    list1 = []
    score = review.find('span').get_text()
    title = review.find('a').get_text().replace('\n', '')
    writer = review.find('span', {'class': 'display-name-link'}).get_text()
    date = review.find('span', {'class': 'review-date'}).get_text()
    content = review.find('div', {'class': 'text show-more__control'}).get_text()

    # 엑셀 파일에 저장하기 위해 list에 각 정보를 추가

    list1.append(score)
    list1.append(title)
    list1.append(writer)
    list1.append(date)
    list1.append(content)
    sum_review = sum_review + content

    lines_list = tokenize.sent_tokenize(content)  # 리뷰 텍스트를 문장별로 쪼개는 전처리 함수

 

    sum = 0
    for sent in lines_list:  # 한 리뷰의 각 문장마다 감정 점수 계산 
        ss = sid.polarity_scores(sent)
        print(ss['compound'])
        sum = sum+ss['compound']
    sum1 = str(sum/len(lines_list))  # 문장들의 평균점수가 그 리뷰의 감정 점수
    list1.append(sum1)
    ws.append(list1)  # 지금까지 뽑아냈던 내용들을 openpyxl worksheet에 저장

excell.save('imdb_.xlsx')  # imdb_.xlsx라는 파일명으로 엑셀파일 저장

def generate_wordcloud(text):  # 워드클라우드 만드는 부분
    wordcloud = WordCloud(font_path='framd.ttf',
     width=2400, height=1800,
     ranks_only=None,
                          relative_scaling = 0.8,
                          stopwords = set(STOPWORDS)
                          ).generate(text)
    plt.imshow(wordcloud)
    plt.axis("off")
    plt.show()  # 화면에 띄워주기 위한 matplotlib 함수
generate_wordcloud(sum_review)

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함