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

앞서 우리는 리뷰 텍스트를 정제한 후 BoW기법을 이용하여 데이터를 벡터화 해준 뒤 랜덤 포레스트를 이용해 학습 및 예측을 한 후 캐글에 제출까지 해보았다. 여기까지가 튜토리얼 part1의 내용이다.

튜토리얼 part2와 part1의 차이는 BoW기법 대신 Word2Vec 기법을 이용하여 데이터를 벡터화해준다는 점이다.

Word2Vec(Word Embedding to Vector)이란?

 part1의 BoW기법 사용했을 때 나오는 데이터는 고차원의 sparse 한 벡터이기 때문에 neural net 성능이 잘 나오지 않는다. 이에 반해 Word2 Vec 기법을 사용했을 때의 데이터는 저차원의 dense vector가 된다. 이를 워드 임베딩(word embedding)이라고 한다.
 예를 들어 "I like Movie"라는 문장을 벡터로 나타낼 때
BoW 기법을 사용해 고차원으로 벡터화한다면 [0,0,1,0,0,0,0,... 중략... 00101... 중략.... 00] 이런 식으로 나타나지만
Word2 Vec 기법을 사용하면 [[0.5,0.3,0.4], [0.1,-0.9,0.3], [0.2,0.3,0.4]] 이런 식으로 저 차원의 고밀도 벡터로 나타낼 수 있다.

 Word2Vec은'주위 단어가 비슷하면 해당 단어의 의미는 유사하다'라는 아이디어에서 시작한다.
딥러닝을 통해 단어를 의미에 따라 벡터화하기 때문에 단어 사이의 거리를 잴 수 있고 거리가 가까울수록 의미가 비슷한 단어라고 해석할 수 있다.

아래 그림은 학습 후 나타나는 단어들 사이의 관계이다.

출처: https://datascienceschool.net/view-notebook/6927b0906f884a67b0da9310d3a581ee/

 위 그림에서 성별을 보면 king과 queen의 거리와 man, woman의 거리가 비슷하다.
이때 중요한 점은 king과 man의 거리와 queen과 woman의 거리도 비슷하다는 점이다.
king-man+woman=queen이 되는 셈이다. 

Word2Vec에는 CBow(continuous bag-of-words)와 skip-gram 기법이 있다.
-CBow는 전체 텍스트를 이용해 문장 속의 빈칸에 들어갈 단어를 예측한다.
예를 들면 나는 __을 먹었다. 에서 __속에 들어갈 단어를 유추한다.
전체 텍스트를 활용하기 때문에 작은 데이터 셋 일 수록 유리하다.

Skip-Gram은 CBOW와는 반대로 특정한 단어를 이용해 주변에 올 만한 단어들을 유추한다.
다음 문장들을 통해 배 주변에 올만한 단어들을 유치한다.
1) *배*가 맛있다. 2) *배*를 타는 것이 재미있다. 3) 평소보다 두 *배*로 많이 먹어서 *배*가 아프다.

학습과정을 그림으로 나타내면 다음과 같다.

출처:  https://arxiv.org/pdf/1301.3781.pdf  

자세히 Word2Vec과 워드 임베딩에 대해 공부하고 싶다면 아래 링크를 참고하면 된다

- 논문: https://arxiv.org/pdf/1301.3781.pdf 
-word2vec 모델 · 텐서플로우 문서 한글 번역본
-Word2Vec으로 문장 분류하기 · ratsgo's blog
-Efficient Estimation of Word Representations in Vector Space
-Word2Vec Tutorial - The Skip-Gram Model · Chris McCormick

아래 링크는 단어의 임베딩 과정을 시물레이션을 돌려볼 수 있다.
-  word embedding visual inspector

모델 학습

파이썬에서 지원하는 Gensim 라이브러리를 이용하여 Word2Vec 모델 학습을 할 것이다.
- Gensim 공식 홈페이지: gensim: models.word2vec – Deep learning with word2vec

먼저 데이터를 불러오자 이때 더 많은 학습을 위해 unlabeled_train 데이터까지 불러준다.

import pandas as pd
train = pd.read_csv('nlp_data/labeledTrainData.tsv', 
                    header=0, delimiter='\t', quoting=3)
test = pd.read_csv('nlp_data/testData.tsv', 
                   header=0, delimiter='\t', quoting=3)
unlabeled_train = pd.read_csv('nlp_data/unlabeledTrainData.tsv', 
                              header=0, delimiter='\t', quoting=3)
                              
print(train.shape)
print(test.shape)
print(unlabeled_train.shape)

unlabeled_train 데이터는 test데이터와 같이 id와 review만 있다.

모델 학습을 하기 위해서 일단 리뷰 데이터들을 정제해줘야 한다 이때 part1에서 리뷰를 데이터 정제해주는 함수를 이용하자. 편리하게 이용하기 위해 다음과 같이 KaggleWord2Vec 클래스를 만들어 주었다.

import re
import nltk

import pandas as pd
import numpy as np

from bs4 import BeautifulSoup
from nltk.corpus import stopwords
from nltk.stem.snowball import SnowballStemmer

class KaggleWord2Vec(object):

    def review_to_wordlist(review, remove_stopwords=False):
        # 1. HTML 제거
        review_text = BeautifulSoup(review, "html.parser").get_text()
        # 2. 특수문자를 공백으로 바꿔줌
        review_text = re.sub('[^a-zA-Z]', ' ', review_text)
        # 3. 소문자로 변환 후 나눈다.
        words = review_text.lower().split()
        # 4. 불용어 제거
        if remove_stopwords:
            stops = set(stopwords.words('english'))
            words = [w for w in words if not w in stops]
        # 5. 어간추출
        stemmer = SnowballStemmer('english')
        words = [stemmer.stem(w) for w in words]
        # 6. 리스트 형태로 반환
        return(words)
        
    def review_to_sentences( review, tokenizer,remove_stopwords=False ):
        # 1. nltk tokenizer를 사용해서 단어로 토큰화 하고 공백 등을 제거
        raw_sentences = tokenizer.tokenize(review.strip())
        # 2. 각 문장을 순회한다.
        sentences = []
        for raw_sentence in raw_sentences:
            # 비어있다면 skip
            if len(raw_sentence) > 0:
                # 태그제거, 알파벳문자가 아닌 것은 공백으로 치환, 불용어제거
                sentences.append(KaggleWord2Vec.review_to_wordlist(raw_sentence,remove_stopwords))
        return sentences

 이때 part1과 다르게 Word2Vec 기법은 단어의 순서에 따라 의미를 결정하기 때문에 불용어 처리를 하지 않는 게 성능이 더 좋으므로 불용어 처리를 할지 안 할지 선택할 수 있게 했다.

KaggleWord2Vec.review_to_wordlist(train['review'][0],remove_stopwords=False)[:10]

KaggleWord2Vec.reviKaggleWord2Vec.review_to_wordlist(train['review'][1],remove_stopwords=False)[:10]

 

'this', 'the', 'of', 'is'와 같은 불용어들이 그대로 있음을 알 수 있다.

KaggleWord2Vec 클래스의 두 함수를 이용하여 모델 학습에 용이하게 모든 리뷰를 문장단위로 나누어서 하나의 리스트에 합치자 이때 학습에 이용하는 리뷰는 train, unlabeled_data의 리뷰들이다. 
한 단락을 문장으로 바꾸기 위해서 우리는 punkt tokenizers를 사용할 것이다.

import nltk.data
nltk.download()
sentences = []
tokenizer = nltk.data.load('tokenizers/punkt/english.pickle')
print ("Parsing sentences from training set")
for review in train["review"]:
    sentences += KaggleWord2Vec.review_to_sentences(review, tokenizer,remove_stopwords=False)

print("Parsing sentences from unlabeled set")
for review in unlabeled_train["review"]:
    sentences += KaggleWord2Vec.review_to_sentences(review, tokenizer,remove_stopwords=False)

 

실행하면 다음과 같은 punkt tokenizers를 다운로드하는창이 뜰 텐데 최초 실행 때 다운로드해주면 된다.

참고로  BeautifulSoup으로부터 문장의 URL에 대해 몇 가지 경고 문구가 뜨는데 딱히 신경 쓸 필요는 없다. 
만약 경고 문구를 보기 싫다면 아래 코드를 입력해주면 된다.

import warnings
warnings.filterwarnings('ignore')

실행 후 sentences의 있는 문장의 개수가 총 795538개 임을 알 수 있다.

len(sentences)

다음과 같이 저장되어 있음을 알 수 있다.

print(sentences[0][:10])
print(sentences[1][:10])

이제 gensim을 이용하여 Word2Vec기법을 활용한 모델 학습을 할 것이다.

먼저 Word2Vec이  학습 시 출력 메시지를 생성하도록 기본 로깅 모듈을 가져와서 구성하고

import logging
logging.basicConfig(
    format='%(asctime)s : %(levelname)s : %(message)s', 
    level=logging.INFO)

Word2Vec의 파라미터 값들을 정해주고 모델 학습을 한다. 파라미터가 무엇을 의미하는지 정리해보았다.

Word2Vec의 파라미터

-아키텍처 : 아키텍처 옵션은 skip-gram 또는 CBOW 모델이다. skip-gram 이 default 값이고 느리지만 더 나은 결과를 낸다.

-학습 알고리즘 : Hierarchical softmax 또는 negative 샘플링. Hierarchical softmax가 defalut값이고 지금 단계에서는 Hierarchical softmax가 더 잘 동작한다.

-빈번한 단어의 다운 샘플링 : Google 문서는 00001에서. 001 사이의 값을 권장한다. 지금 단계에서는  0.001에 가까운 값이 최종 모델의 정확도를 향상하는 것처럼 보였다.

-단어 벡터 차원 :  feature가 많을수록 실행시간이 길어지고 항상은 아니지만 대체로 좋은 모델이 된다. 합리적인 값은 수십에서 수백까지 될 수 있고 우리는 300개를 사용했다.

-컨텍스트 / 창 크기 : 학습 알고리즘이 고려해야 하는 컨텍스트의 단어 수는 얼마나 될까? hierarchical softmax를 위해 좀 더 큰 수가 좋지만 10 정도가 적당하다.

-Worker threads : 실행할 병렬 프로세스의 수이다. 이것은 컴퓨터마다 다르지만 대부분의 시스템에서 4에서 6 사이의 값을 사용한다.

-최소 단어 수 : 좀 더 의미 있는 단어로만 간추리기 위해 사용된다. 모든 문서에서 적어도 주어진 값만큼 발생하지 않는 단어는 무시된다. 10에서 100 사이 값을 권장한다. 우리가 사용하는 데이터는 각 영화가 30개씩의 리뷰가 있기 때문에 각 영화 제목에 너무 많은 중요성을 부여하지 않도록 최소 단어 수를 40으로 설정했다. 이로 인해 전체 어휘 크기는 약 15,000 단어가 된다.  또한 값이 클수록 실행시간을 줄이는데 도움이 된다.

num_features = 300 # 단어 벡터 차원의 수
min_word_count = 40 # 최소 문자 수
num_workers = 4 # 병렬로 실행할 thread의 수
context = 10 # 문자열 창 크기 
downsampling = 1e-3 # 문자 빈도 수에 대한 Downsample

# 초기화 및 모델 학습
from gensim.models import word2vec

# 모델 학습
model = word2vec.Word2Vec(sentences, 
                          workers=num_workers, 
                          size=num_features, 
                          min_count=min_word_count,
                          window=context,
                          sample=downsampling)
model

# 학습이 완료 되면 필요없는 메모리를 unload 시킨다.
model.init_sims(replace=True)

#모델 save
model_name = '300features_40minwords_10text'
model.save(model_name)

모델 결과 탐색

학습한 것을 토대로 문장에서 가장 유사도가 없는 단어가 무엇인지 알 수 있다.

model.wv.doesnt_match('man woman child kitchen'.split())

나머지는 사람을 지칭하는 단어이고 kitchen만 장소를 뜻하는 단어이기 때문에 가장 유사도가 없는 단어로 선정되었다.

model.wv.doesnt_match("france england germany berlin".split())

나머지는 국가이고 berlin만 도시이기 때문에 가장 유사도가 없는 단어로 선정되었다. 

다음 코드는 주어진 단어와 가장 유사한 단어들을 모델 사전에서 찾아 준다.

model.wv.most_similar("man")

model.wv.most_similar("film")

film과 유사한 단어로 'movi', 'documentari', 'pictur', 'cinema' 등등 연관이 있는 단어가 나오는 모습이다. 뒤에 있는 숫자는 유사도를 뜻한다. 이때 단어가 movie, picture 가 아닌 movi, pictur로 나타나는 이유는 스테밍을 해주었기 때문이다.

데이터 벡터화

학습된 모델을 이용하여 정제한 리뷰 데이터들을 벡터화해보자. 

import numpy as np
# 문장에서 단어 벡터의 평균을 구하는 함수
def makeFeatureVec(words, model, num_features):

    # 0으로 채운 배열로 초기화 한다.(속도를 위해)
    featureVec = np.zeros((num_features,),dtype="float32")

    nwords = 0.
    # Index2word는 모델 사전에 있는 단어명을 담은 리스트이다. 또한 속도를 위해 set 형태로 초기화 한다.
    index2word_set = set(model.wv.index2word)
    # 루프를 돌며 모델 사전에 포함이 되는 단어라면 피처에 추가한다.
    for word in words:
        if word in index2word_set:
            nwords = nwords + 1.
            featureVec = np.add(featureVec,model[word])
    # 결과를 단어수로 나누어 평균을 구한다.
    featureVec = np.divide(featureVec,nwords)
    return featureVec
 # 리뷰 단어 목록의 각각에 대한 평균 feature 벡터를 계산하고 2D numpy 배열을 반환해주는 함수
def getAvgFeatureVecs(reviews, model, num_features):
	#counter 초기화
    counter = 0.
    # 속도를 위해 0으로 채워진 2D numpy 배열을 사전 할당한다.
    reviewFeatureVecs = np.zeros(
        (len(reviews),num_features),dtype="float32")
    
    for review in reviews:
       # 매 5000개 리뷰마다 상태를 출력
       if counter%5000. == 0.:
           print("Review %d of %d" % (counter, len(reviews)))
       # 평균 피처 벡터를 만드는 함수(위에서 정의됨)를 호출한다.
       reviewFeatureVecs[int(counter)] = makeFeatureVec(review, model, \
           num_features)
       # counter를 증가시킨다.
       counter = counter + 1.
    return reviewFeatureVecs
#리뷰들을 정제해주는 함수
def getCleanReviews(reviews):
    clean_reviews = []
    for review in reviews["review"]:
        clean_reviews.append( KaggleWord2Vec.review_to_wordlist( review, remove_stopwords=True ))
    return clean_reviews

train, test의 리뷰들을 정제하고 getAvgFeatureVec 함수를 이용해 벡터화 해주자

%time trainDataVecs = getAvgFeatureVecs(\
    getCleanReviews(train), model, num_features )

%time testDataVecs = getAvgFeatureVecs(\
        getCleanReviews(test), model, num_features )

아래는 trainDataVecs의 모습이다.

너무 길어 일부분만 가져왔다

이제 part1에서 했던 것처럼 랜덤 포레스트를 이용해 벡터화된 데이터를 이용해 학습 후 예측을 하자
아래 과정은 이전 글과 똑같은 과정이므로 설명은 생략하겠다.

from sklearn.ensemble import RandomForestClassifier

forest = RandomForestClassifier(
    n_estimators = 100, n_jobs = -1, random_state=2018)
%time forest = forest.fit( trainDataVecs, train["sentiment"] )

from sklearn.model_selection import cross_val_score
%time score = np.mean(cross_val_score(\
    forest, trainDataVecs, \
    train['sentiment'], cv=10, scoring='roc_auc'))
print(score)

result = forest.predict( testDataVecs )
output = pd.DataFrame( data={"id":test["id"], "sentiment":result} )
output.to_csv('nlp_data/part2_{0:.5f}.csv'.format(score), 
              index=False, quoting=3 )

캐글 제출 결과

577팀 중에 534등 정도이다.

오히려 BoW기법을 사용한 part1 때보다 점수가 더 낮아진 모습이다.
이제 part3에서는 Word2Vec를 이용해 높은 점수를 받기 위해 Word2Vec의 응용 및 심화과정을 다룰 것이다.

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