이 글은 아래 링크의 글에 이어서 작성되는 글입니다.
https://dsbook.tistory.com/63이

 

(SA) 자연어 처리를 이용한 IMDB 영화 리뷰 감정분석-Part 1-(1)

자연어 처리(NLP, natural language processing)란? 자연어는 사람이 일상적으로 사용하는 언어를 의미한다. 자연어는 일반적으로 컴퓨와 같은 기계는 이해하기 힘들다. 이런 자연어를 형태소 분석, 품사

dsbook.tistory.com

데이터 벡터화

 이전 단계에서 리뷰 텍스트들을 정제해 주었지만 아직 컴퓨터는 단어들을 이해하지 못한다. 따라서 정제한 텍스트들을 컴퓨터가 이해할 수 있느 숫자 형식의 벡터 값으로 만들어 주어야한다.
Part1에서는 이 벡터화 작업을 Bag of words(Bow) 기법을 이용할 것이다.

Bag of Words(BoW)란?

주머니 속에 단어들을 넣고 각 문장마다 구문 상관없이 단순히 주머니에 있는 단어들이 몇번 나오는지 세주는 방식이다.
예를 들자면 

(1) John likes to watch movies. Mary likes movies too.

(2) Mary also likes to watch football games.

다음 두 문장이 있을 때 나오는 단어들을 주머니 속에 넣어주자
["John","likes","to","watch","movies","Mary","too","also","football","games"]

이를 바탕으로 위 문장들을 순서대로 단어의 갯수를 세어주면 다음과 같이 벡터화 해준다.
(1) John likes to watch movies. Mary likes movies too.
->[1, 2, 1, 1, 2, 1, 1, 0, 0, 0]
(2) Mary also likes to watch football games.
->[0, 1, 1, 1, 0, 1, 0, 1, 1, 1]

참고로 BoW를 사용할 때는 대문자,소문자 통일, 어간추출 등 과 같은 텍스트들을 정제한 후에 사용하여 의미있는 데이터를 얻을 수 있다. 우리는 이미 앞단계에서 다 해주었기 때문에 지금 단계에서 해주지 않아도 된다.

N-gram

 BoW 기법의 큰 단점은 구문 순서를 신경쓰지 않고 단어의 갯수만을 벡터화 하기 때문에 단어의 순서가 달라져도 똑같은 문장으로인식한다는 것이다. 예를 들어

3) John dosen't likes to watch movies. Mary likes movies.

4) John likes to watch movies and Mary dosen't likes movies.

두 문장의 뜻은 다르지만 BoW기법을 사용하면 컴퓨터는 두 문장이 같은 문장이라고 이해할 것이다.

이때 이를 방지하기 위해 n-gram을 이용한다. 1개단위로 묶으면 unigram 2개면 bigram 3개면 trigram이라고 한다.
다음은 (1)문장을 bigram으로 묶은 경우 만들어지는 사전이다.

[ "John likes", "likes to", "to watch", "watch movies", "Mary likes", "likes movies", "movies too", ]

하지만 n-gram을 사용하더라도 BoW기법에는 한계가 존재한다.
이 BoW기법 자체가 값이 0인 데이터가 상당히 많다. 이런한 행렬을 sparse matrix 라고 하는데 0 값이 많기 때문에
상당히 비효율적이라고 할 수 있다.

 예를 들어 책 한권을 BoW기법을 사용하여 문장들을 벡터로 나타내는 작업을 한다고 생각해보자 단어들의 종류는 엄청나게 많지만 한 문장에 들어가는 단어는 얼마 안되기 때문에 쓸데없는 0값이 공간을 많이 차지하게 될 것이다.

 따라서 BoW기법은 엄청 성능이 좋은 기법은 아니다. 그래도 비교적 간단하고 성능도 어느정도 나오기 때문에 널리 쓰이는 기법이라고 할 수 있다.

BoW 기법 적용

 이제 train의 리뷰를 정제한 데이터를 BoW 기법을 이용해 벡터 데이터로 만들어보자
우리는 sklearn의 CounterVectorizer를 사용할 것이다.

먼저 이전 단계에서 만든  review_to_words 함수를 이용해 모든 trian,test의 리뷰들을 정제해주자. 조금 시간이 걸린다.

clean_train = []
for i in range(0, num_reviews):
    if (i + 1)%5000 == 0: #실행이 잘되는지 확인하기 위해 5000개 실행될때 마다 확인문구를 print
        print('Review {} of {} '.format(i+1, num_reviews))
    clean_train.append(review_to_words(train['review'][i]))
#실행시간을 재보자    
%time train['review_clean'] = train['review'].apply(review_to_words)

필자 같은 경우에는 2분 1초가 걸렸다.

sklearn의 CountVectorizer를 사용 하여 BoW기법을 적용해보자
튜토리얼에서는 CounterVectorizer의 파라미터 값들을 수정하지 않았지만 파라미터 값만 수정해줘도 캐글 점수 차이가 꽤 많이 난다. 필자는 먼저 튜토리얼의 코드로 실행한 후에 파라미터값들을 변경해 본 후 캐글 점수를 비교해 볼 것이다.
아래는 캐글 튜토리얼에서 사용된 코드이다. 참고로 n-gram기법을 사용하지 않았다.

vfrom sklearn.feature_extraction.text import CountVectorizer
#튜토리얼에 사용된 코드
vectorizer = CountVectorizer(analyzer = "word",   
                             tokenizer = None,    
                             preprocessor = None,
                             stop_words = None,   
                             max_features = 5000) 

stopwords는 이미 리뷰를 정제할 때 사용했으므로 이 단계에서는 사용하지 않기 위해 stop_words = None를 이용했다..
max_features=5000 을 이용해 빈도수가 높은 단어 5000개만 선정한다.

train_data_features = vectorizer.fit_transform(clean_train_reviews)
train_data_features.shape

train의 리뷰데이터 25000개 이고 feature의 수,즉 단어의 수가 max_features=5000으로 설정해주었기 때문에 5000개임을 알 수 있다.

get_feature_names()를 통해 어떤 단어들이 들어 있는지 알 수 있다. 이를 이용해 사전을 만들어 주자.

vocab = vectorizer.get_feature_names()
vocab[:10] #맨앞 10개의 단어를 보자

numpy를 이용해 각 단어가 몇 번 사용되었는지 대략 알아보자.

import numpy as np

# 각 단어가 몇번 쓰였는지 세어보자
dist = np.sum(train_data_features, axis=0)
#데이터 프레임 형식으로 출력
pd.DataFrame(dist, columns=vocab)

아래 코드를 통해 row에 리뷰,column에 단어들을 배정해 각 리뷰에 어떤 단어들이 몇번 쓰였는지 데이터 프레임으로 정리해주었다.

pd.DataFrame(train_data_features.toarray(), columns=vocab)

보이는 부분이 상당히 작지만 모두 0 값이다. 위에서 말했듯이 BoW 기법을 사용하면  sparse matrix가 된다.

랜덤포레스트를 활용한 기계학습 및 예측

랜덤 포레스트(random forest)란?

우리는 랜덤 포레스트를 이용하여 학습을 시키고 예측을 할 것이다. 랜덤 포레스트에 대해 간단한 설명만 하고 넘어가도록 하겠다

  랜덤 포레스트는 지도 학습(Supervised Learning)의 일종이다. 지도 학습이란 training datatraining label을 통해 학습을 하고 학습이 끝나고test data를 이용해 test label를 예측하는 과정을 말한다.
그림으로 나타내면 다음과 같다.

출처:https://github.com/amueller/odscon-2015

여기서 우리가 하고 있는 것에 대응을 시키며 training datatrain의 리뷰를 말하고 training labeltrainsentiment를 말한다.마찬가지로 test data,test label은 각각 test의 리뷰와 sentiment이다.

 또한 랜덤 포레스트는 간단히 말해 다수의 결정 트리(decision tree)를 사용한다고 생각하면 된다. 랜덤 포레스트는 다수의 트리들을 랜덤하게 사용함으로써 다양한 하나의 트리만을 사용될 때 발생되는 편향적인 일반화를 방지할 수 있어 성능을 향상 시킨다.

자세한 정보는 아래 링크를 참조하길 바란다.상당히 설명이 잘 되어있다.
https://ko.wikipedia.org/wiki/%EB%9E%9C%EB%8D%A4_%ED%8F%AC%EB%A0%88%EC%8A%A4%ED%8A%B8

랜덤 포레스트 사용

sklearn의 RandomForestClassifier를 사용한다.

from sklearn.ensemble import RandomForestClassifier

# 랜덤포레스트 분류기를 사용
forest = RandomForestClassifier(
    n_estimators = 100, n_jobs = -1, random_state=2018)
forest

n_estimators 결정 트리의 갯수를 정한다. 웬만하면 숫자를 크게 지정할수록 좋은 성능으로 돌아간다.
n_jobs=-1 CPU의 모든 코어를 사용 1이라면 1개의 코어, 2라면 2개의 코어를 사용한다는 뜻이다.
random_state 랜덤값 랜덤포레스트를 여러번 돌리면 항상다른값이나오는데 같은값이 나와야 명확하게 학습이 되기때문에 지정해준다.
이외에도 다양한 파라미터가 있고 파라미터를 지정해주는 것에 따라 캐글 점수가 달라질 것이다.

%time forest = forest.fit(train_data_features, train['sentiment'])

from sklearn.model_selection import cross_val_score
%time score = np.mean(cross_val_score(
    forest, train_data_features,
    train['sentiment'], cv=10, scoring='roc_auc'))#roc 커브로 평가
score

train 데이터를 학습시킨 결과 예측 성공률이 0.919294496이 나왔다.
이때 이 점수가 캐글 점수, 즉 test label 예측 성공률과는 다르다. 간단하게 생각해보면 우리가 기출 문제를 풀어보면서 수능을 대비하는데 이미 학습한 기출문제들을 풀었을 때 점수와 수능 점수의 차이를 생각하면 이해하기 쉽다.

이때 주의할 점은 과소적합(Underfirring)과 과대적합(Overfitting)을 피해 최적점을 찾아야 된다는 것이다.
과소적합은 학습이 부족하여 특성을 파악하지 못해 일반화가 잘 되지 않은 경우이고 과대적합은 너무 학습을 많이해 학습데이터에만 맞춰져 있어 일반화가 잘 되지 않은 경우이다. 두 경우 모두 일반화가 잘 되지 않았기 때문에 당연히 성능이 떨어진다. 아래 그림을 보면 이해하기 쉬울 것이다.

위 그림에서 최적점(Sweet spot)일때 학습을 통해 가장 일반화가 잘되는 경우이다. 우리는 앞서 말했듯이 파라미터 값 변경을 통해 학습조건을 바꿔 줄 수 있고 최적점에 가까워야 성능이 좋아질 것이다.

예측

다시 본론으로 돌아가면 이제 train 데이터를 이용해 학습했으므로 학습한 것을 이용해 test의 sentiment를 예측해보자.
똑같이 test의 리뷰들을 정제해주고

clean_test_reviews = []
for i in range(0, num_reviews):
    if (i + 1)%5000 == 0:
        print('Review {} of {} '.format(i+1, num_reviews))
    clean_test_reviews.append(review_to_words(test['review'][i]))

아까 사용했던 vectorizer를 사용해 정제한 리뷰들을 벡터화 시켜주자

test_data_features = vectorizer.transform(clean_test_reviews)
test_data_features = test_data_features.toarray()
#잘만들어 졌는지 확인
test_data_features[5][:50]

벡터화한 데이터들과 랜덤포레스트를 이용해 test의 sentiment를 예측해보자

# 테스트 데이터를 이용해 예측
result = forest.predict(test_data_features)
result[:10]

이제 예측한 값을 캐글에 제출하기 위해 result값을 데이터 프레임 형태로 변환 후 csv 파일 형태로 저장해주었다.

#result 값을 데이터 프레임 형태로 변경
output = pd.DataFrame( data={"id":test["id"], "sentiment":result} )
#output을 캐글 제출을 위해 csv파일로 저장
output.to_csv('nlp_data/part1_{0:.5f}.csv'.format(score), index=False, quoting=3)

이때 csv을 저장할때 quoting=3을 통해 처음 csv파일을 읽어올때와 형태를 맞추어 주었고 파일 제목에 train 데이터로 학습 했을 때의 점수가 제목에 들어가게 해주었다.

파일이 만들어진 모습

csv 파일 내부 모습이다.
이제 csv파일을 캐글에 제출해서 평가를 받아보자.

캐글에 들어가 사진에 보이는 빨간 동그라미 부분에 csv파일을 넣고 제출을 하면 캐글에서 평가를 해준다.
아래는 평가 결과이다.

필자같은 경우는 첫번째 제출때 실수로 캡쳐를 못해 다시 제출해서 wait time이 0초인데 원래는 평가하는데 시간이 좀 걸린다. 점수는 0.84740이 나온 모습이다.

5년전인 대회기간때 랭킹을 볼 수 있는데 지금 받은 점수는 전체 577명중 348등 정도의 기록이다.

파라미터 변경

성능을 높히기 위해서는 위에서 말했듯이 몇몇 파라미터들을 변경해가면서 과소적합과 과대적합을 피해 최적점을 찾아야 한다.

 아래 코드에서 CounterVectorize,RandomForestClassifier의 기존 파라미터 값 변경, 신규 파라미터를 추가 등을 통해 최적점을 찾아낸다면 더 높은 캐글 점수를 받을 수 있다. 

#CounterVectorizer 사용
vectorizer = CountVectorizer(analyzer = "word",   
                             tokenizer = None,    
                             preprocessor = None,
                             stop_words = None,   
                             max_features = 10000) 
#BoW 적용
train_data_features = vectorizer.fit_transform(clean_train_reviews)
#사전 만들기
vocab = vectorizer.get_feature_names()
#랜덤 포레스트 사용
forest = RandomForestClassifier(
    n_estimators = 100, n_jobs = -1, random_state=2018)
#적용
forest = forest.fit(train_data_features, train['sentiment'])
#점수 계산
score = np.mean(cross_val_score(
    forest, train_data_features,
    train['sentiment'], cv=10, scoring='roc_auc'))
print(score)
#테스트 데이터를 이용해 평가
test_data_features = vectorizer.transform(clean_test_reviews)
test_data_features = test_data_features.toarray()
result = forest.predict(test_data_features)
#결과 저장
output = pd.DataFrame( data={"id":test["id"], "sentiment":result} )
output.to_csv('nlp_data/part1_{0:.5f}.csv'.format(score), index=False, quoting=3)

위 코드에서는 기존 코드에서 max_features = 5000 에서 max_features = 10000으로 바꿔 주었다.

score값은 올라갔지만 과대 적합이 되어 캐글 점수는 오히려 떨어진 모습이다.

N-gram,min_df 사용

#CounterVectorizer 사용
vectorizer = CountVectorizer(analyzer = 'word', 
                             tokenizer = None,
                             preprocessor = None, 
                             stop_words = None, 
                             min_df = 2, 
                             ngram_range=(1, 3),
                             max_features = 20000
                            )
#BoW 적용
train_data_features = vectorizer.fit_transform(clean_train_reviews)
#사전 만들기
vocab = vectorizer.get_feature_names()
#랜덤 포레스트 사용
forest = RandomForestClassifier(
    n_estimators = 100, n_jobs = -1, random_state=2018)
#적용
forest = forest.fit(train_data_features, train['sentiment'])
#점수 계산
score = np.mean(cross_val_score(
    forest, train_data_features,
    train['sentiment'], cv=10, scoring='roc_auc'))
print(score)
#테스트 데이터를 이용해 평가
test_data_features = vectorizer.transform(clean_test_reviews)
test_data_features = test_data_features.toarray()
result = forest.predict(test_data_features)
#결과 저장
output = pd.DataFrame( data={"id":test["id"], "sentiment":result} )
output.to_csv('nlp_data/part1_{0:.5f}.csv'.format(score), index=False, quoting=3)

min_df=2를 통해 단어가 나타내는 최소 문서의 수를 2로 지정해 주었다. 사전에 단어가 들어가기 위해서는 최소한 2개의 리뷰에 있어야 한다는 뜻이다. 예를 들어 어떠한 하나의 리뷰에 특정 단어가 수백개가 들어가 있어도 다른 리뷰에 그 특정단어 하나도 존재하지 않는다면 그 특정단어는 사전에 넣지 않는다.

ngram_range(1,3)을 통해 unigram,bigram,trigram을 사용해 학습하게 했다. ngram_range(a,b) 이면 a<=n<=b 라고 생각하면 된다.예를 들어 ngram_range(2,2)면 bigram만을 사용하는 것이다.

그 이외의 다양한 CounterVectorize,RandomForestClassifier의 파라미터들을 알고 싶다면 아래링크를 참조하면 된다.
CounterVectorize:
https://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.CountVectorizer.html

RandomForestClassifier:
https://scikit-learn.org/stable/modules/generated/sklearn.ensemble.RandomForestClassifier.html?highlight=randomforestclassifier#sklearn.ensemble.RandomForestClassifier

제출 결과는 아래와 같다.

캐글 제출 결과 0.85260점을 받아 꽤 많이 점수가 올랐고 등수는 326등 가량 되었다. 이와 같이 몇몇 파라미터 값들을 변경하여 더 높은 캐글 점수를 노려보길 바란다.

다음 글에서는 BoW 기법 대신 성능이 더 좋은 word2vec 기법을 사용하는 법을 다루겠다.

728x90
반응형
  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 라이프코리아트위터 공유하기
  • shared
  • 카카오스토리 공유하기