[kaggle] Bag of Words Meet Bags of Popcorn - (4) Part 4: Compare

Part 4: Comparing deep and non-deep learning methods

Results

Method Accuracy
Bag of Words 0.84380
Average Vectors 0.83220
Bag of Centroids 0.84216

차이는 거의 없는데, 미세하게 Bag of Words가 제일 좋다.

Why is Bag of Words better?

가장 큰 이유는, 세 메소드 다 단어의 순서를 무시하는 Bag of Words형태의 피처를 사용했기 때문에 전부 비슷한 결과가 나왔다.

A few things to try:

먼저, Word2Vec을 더 많은 데이터를 사용해서 트레이닝 하면 더 좋은 성능을 낼 것이다. 구글의 Word2Vec은 10억개의 단어를 트레이닝했지만, 우리 데이터셋에서는 오직 1800만 개를 트레니이 했을 뿐이다. 다행히도, Word2Vec은 트레이닝된 모델을 불러오는 함수를 제공하고 이 함수는 원래 구글이 C로 만들었기 때문에, 마찬가지로 C로 트레이닝한 모델도 파이썬에서 불러올 수 있다.

두번째로, 최근 논문에서 분산 단어 벡터 테크닉(distributed word vector techniques)이 Bag of Words 모델보다 더 좋은 결과를 보였다. 거기에서는 Paragraph Vector라는 알고리즘을 IMDB 데이터셋에 적용하였다. 우리의 접근법과는 다르게 Paragraph Vector는 단어의 순서 정보를 보존한다.

[kaggle] Bag of Words Meet Bags of Popcorn - (3) Part 3: More Fun With Word Vectors

Part 3: More Fun With Word Vectors

Code

Numeric Representations of Words

이제 우리에겐 단어의 감정적 의미에 대해 이해하는 모델이 있다. 어떻게 써야 할까? 파트 2에서 학습된 Word2Vec 모델이 “syn0”이라는 numpy 배열에 저장된다.

>>> # Load the model that we created in Part 2
>>> from gensim.models import Word2Vec
>>> model = Word2Vec.load("300features_40minwords_10context")
2014-08-03 14:50:15,126 : INFO : loading Word2Vec object from 300features_40min_word_count_10context
2014-08-03 14:50:15,777 : INFO : setting ignored attribute syn0norm to None

>>> type(model.syn0)
<type 'numpy.ndarray'>

>>> model.syn0.shape
(16492, 300)

참고로, 모델을 트레이닝 할 때 myhashfxn을 사용했다면 로드하기 전에 동일한 해쉬함수를 정의해 놓아야 제대로 불러온다.

syn0의 row의 수 16,490은 파트 2에서 최소 word count를 40으로 설정한 것에 따른 모델의 vocabulary에 들어 있는 단어의 수이고, column의 수 300은 파트 2에서 설정한 특성(feature) 벡터의 크기다. 각 단어 벡터는 아래와 같이 살펴볼 수 있다:

In[5]: model["flower"]

1x300 크기의 numpy array가 리턴된다.

From Words To Paragraphs, Attempt 1: Vector Averaging

IMDB 데이터셋의 한 가지 문제는 리뷰의 길이가 변한다는 것이다. 우리는 여기서 각각의 단어 벡터들을 각 리뷰를 나타내는 동일한 크기의 특성 셋으로 나타내어야 한다.

모든 단어들이 전부 300차원 벡터이므로, 간단하게 각 리뷰의 벡터들을 평균 내는 방법을 사용할 수 있다 (이를 위해 stop word를 제거했다. 이러한 경우에 stop word는 노이즈가 된다).

아래 코드들은 벡터들을 평균내는 함수다:

import numpy as np  # Make sure that numpy is imported

def makeFeatureVec(words, model, num_features):
    # Function to average all of the word vectors in a given
    # paragraph
    #
    # Pre-initialize an empty numpy array (for speed)
    featureVec = np.zeros((num_features,),dtype="float32")
    #
    nwords = 0.
    #
    # Index2word is a list that contains the names of the words in
    # the model's vocabulary. Convert it to a set, for speed
    index2word_set = set(model.index2word)
    #
    # Loop over each word in the review and, if it is in the model's
    # vocaublary, add its feature vector to the total
    for word in words:
        if word in index2word_set:
            nwords = nwords + 1.
            featureVec = np.add(featureVec,model[word])
    #
    # Divide the result by the number of words to get the average
    featureVec = np.divide(featureVec,nwords)
    return featureVec


def getAvgFeatureVecs(reviews, model, num_features):
    # Given a set of reviews (each one a list of words), calculate
    # the average feature vector for each one and return a 2D numpy array
    #
    # Initialize a counter
    counter = 0.
    #
    # Preallocate a 2D numpy array, for speed
    reviewFeatureVecs = np.zeros((len(reviews),num_features),dtype="float32")
    #
    # Loop through the reviews
    for review in reviews:
       #
       # Print a status message every 1000th review
       if counter%1000. == 0.:
           print("Review %d of %d" % (counter, len(reviews)))
       #
       # Call the function (defined above) that makes average feature vectors
       reviewFeatureVecs[counter] = makeFeatureVec(review, model, num_features)
       #
       # Increment the counter
       counter = counter + 1.
    return reviewFeatureVecs

이제 이 함수로 각 리뷰에 대한 평균 벡터를 구할 수 있다. 몇 분 걸릴 수 있다:

import numpy as np  # Make sure that numpy is imported

def makeFeatureVec(words, model, num_features):
    # Function to average all of the word vectors in a given
    # paragraph
    #
    # Pre-initialize an empty numpy array (for speed)
    featureVec = np.zeros((num_features,),dtype="float32")
    #
    nwords = 0.
    #
    # Index2word is a list that contains the names of the words in
    # the model's vocabulary. Convert it to a set, for speed
    index2word_set = set(model.index2word)
    #
    # Loop over each word in the review and, if it is in the model's
    # vocaublary, add its feature vector to the total
    for word in words:
        if word in index2word_set:
            nwords = nwords + 1.
            featureVec = np.add(featureVec,model[word])
    #
    # Divide the result by the number of words to get the average
    featureVec = np.divide(featureVec,nwords)
    return featureVec

def getAvgFeatureVecs(reviews, model, num_features):
    # Given a set of reviews (each one a list of words), calculate
    # the average feature vector for each one and return a 2D numpy array
    #
    # Initialize a counter
    counter = 0.
    #
    # Preallocate a 2D numpy array, for speed
    reviewFeatureVecs = np.zeros((len(reviews),num_features),dtype="float32")
    #
    # Loop through the reviews
    for review in reviews:
        #
        # Print a status message every 1000th review
        if counter%1000. == 0.:
            print("Review %d of %d" % (counter, len(reviews)))
        #
        # Call the function (defined above) that makes average feature vectors
        reviewFeatureVecs[counter] = makeFeatureVec(review, model, num_features)
        #
        # Increment the counter
        counter = counter + 1.
    return reviewFeatureVecs

# ****************************************************************
# Calculate average feature vectors for training and testing sets,
# using the functions we defined above. Notice that we now use stop word
# removal.

clean_train_reviews = []
for c, review in enumerate(train["review"]):
    if c%1000. == 0.:
        print("Training set {} of {}".format(c, train.shape[0]))
    clean_train_reviews.append( review_to_wordlist( review, remove_stopwords=True ))

trainDataVecs = getAvgFeatureVecs( clean_train_reviews, model, num_features )

print("Creating average feature vecs for test reviews")
clean_test_reviews = []
for c, review in enumerate(test["review"]):
    if c%1000. == 0.:
        print("Test set {} of {}".format(c, test.shape[0]))
    clean_test_reviews.append( review_to_wordlist( review, remove_stopwords=True ))

testDataVecs = getAvgFeatureVecs( clean_test_reviews, model, num_features )

주석으로 설명이 적혀 있지만, 간단히 설명하자면 makeFeatureVec은 리뷰 파라그래프를 받아서 각 단어들에 대해 model이 포함하는 단어인지 검사하여 평균을 구한다. 즉, 특정 리뷰에 대해 특성 벡터를 구하는 함수이고 getAvgFeatureVecs는 모든 리뷰에 대해 makeFeatureVec함수를 적용하여 특성 벡터 리스트를 구하는 함수다.

자, 그럼 이제 각 리뷰들의 특성 벡터를 추출하였으니 이 값으로 머신러닝 알고리즘을 돌릴 수 있다. Bag of Words에서 했던 것처럼 랜덤 포레스트를 적용해 보자.

# Fit a random forest to the training data, using 100 trees
from sklearn.ensemble import RandomForestClassifier
forest = RandomForestClassifier( n_estimators = 100 )

print("Fitting a random forest to labeled training data...")
forest = forest.fit( trainDataVecs, train["sentiment"] )

# Test & extract results
result = forest.predict( testDataVecs )

# Write the test results
output = pd.DataFrame( data={"id":test["id"], "sentiment":result} )
output.to_csv( "Word2Vec_AverageVectors.csv", index=False, quoting=3 )

이제 이 결과를 제출하면 얼마나 잘 예측했는지를 볼 수 있는데, 오히려 Bag of Words보다 결과가 안 좋다!

원소별로 평균을 내는 방법이 썩 좋은 결과를 보이지 못했다. 어떻게 이를 개선할 수 있을까? 일반적인 방법은 tf-dif를 사용해서 단어 벡터에 가중치를 부여하는 방법이다. scikit-learn에서 제공하는 TfidfVectorizer를 사용해서 간단하게 구현할 수 있다. 그런데 실제로 적용해 보았을 때 별다른 성능 향상이 없었다.

From Words to Paragraphs, Attempt 2: Clustering

Word2Vec은 의미가 유사한 단어들의 클러스터를 만든다. 이를 이용해서 클러스터에서 단어 유사도를 살펴보는 접근방법을 적용해 보자. 이렇게 벡터들을 그루핑(grouping) 하는 방법을 “vector quantization” 이라고 한다. 이를 위해 K-Means 클러스터링 알고리즘을 사용한다.

K-Means 알고리즘에서는 클러스터의 수 “K”를 설정해 주어야 하는데, 이를 어떻게 정할까? 여러 K를 시도해 본 결과, 평균적으로 클러스터당 5개 단어 정도의 작은 클러스터가 적합했다. 작은 클러스터를 사용한다는 것은 반대로 클러스터의 수, 즉 K가 굉장히 크다는 것이고 이는 오랜 트레이닝 시간을 필요로 한다. 원문에서 저자의 컴퓨터에서는 40분 이상이 걸렸다고 하는데, 내 컴퓨터에서는 11분(675 초) 정도 걸렸다.

from sklearn.cluster import KMeans
import time

start = time.time() # Start time

# Set "k" (num_clusters) to be 1/5th of the vocabulary size, or an
# average of 5 words per cluster
word_vectors = model.syn0
num_clusters = int(word_vectors.shape[0] / 5)

print("the number of clusters: {}".format(num_clusters))

# Initalize a k-means object and use it to extract centroids
kmeans_clustering = KMeans( n_clusters = num_clusters )
idx = kmeans_clustering.fit_predict( word_vectors )

# Get the end time and print how long the process took
end = time.time()
elapsed = end - start
print("Time taken for K Means clustering: ", elapsed, "seconds.")

여러 가지로 테스트 해 본 결과, 500개의 단어를 하나의 클러스터로 하면 32개의 클러스터가 나오며 30초 가량 걸린다. 클러스터당 50개의 단어로 하면 329개의 클러스터가 나오고 90초 정도 걸린다. 클러스터당 5개의 단어인 경우에는 3298개의 클러스터가 나오고, 11분이 걸린다.

클러스터링이 끝나면, 각 클러스터에 포함된 단어의 인덱스들이 idx배열에 저장된다. 이를 model.index2word와 묶어서 단어와 매핑하자.

# Create a Word / Index dictionary, mapping each vocabulary word to
# a cluster number                                                                                            
word_centroid_map = dict(zip( model.index2word, idx ))

word_centroid_map에는 단어가 어떤 클러스터 소속인지 저장된다.

In[19]: word_centroid_map
Out[19]: 
{'toys': 444,
 'overly': 401,
 'devil': 1079,
 'rightful': 2008,
 'suburban': 719,
 ...

클러스터 별로 조금 더 자세히 살펴보자.

# For the first 10 clusters
for cluster in range(0, 10):
    #
    # Print the cluster number
    print("\nCluster {}".format(cluster))
    #
    # Find all of the words for that cluster number, and print them out
    words = []

    for k, v in word_centroid_map.items():
        if v == cluster:
            words.append(k)
    print(words)

이 코드를 돌려보면, 아래와 같은 결과를 얻을 수 있다.

Cluster 0
['noble', 'brave']

Cluster 1
['unworthy', 'pretentiousness', 'ineptness', 'notwithstanding', 'overwhelm']

Cluster 2
['tomas', 'milian', 'nero', 'franco', 'jess']

Cluster 3
['poker', 'picnic', 'carnival', 'golf', 'pond', 'cafe', 'digs', 'parlor', 'cane']

Cluster 4
['companionship', 'friendships', 'individuality', 'frailty', 'passions', 'harmony', 'inspires', 'pleasures', 'elusive']

Cluster 5
['edna']

Cluster 6
['straight', 'direct']

Cluster 7
['crafty', 'manic']

Cluster 8
['matched', 'impeccable', 'vocal', 'matching', 'sparkling']

Cluster 9
['beats', 'messes', 'cooks', 'nuts', 'lighten']

살펴보면 클러스터마다 퀄리티가 다양하다. 비슷한 단어끼리 묶인 클러스터가 있는가 하면, 쌩뚱맞은 조합도 존재한다. 원문의 결과와는 완전히 다른데, 이는 word_centroid_map이 dictionary라서 10개를 뽑으면 랜덤하게 뽑히기 때문에 그렇다.

다음 단계로 넘어가기 전에, K-Means 클러스터링에 너무 오랜 시간이 걸리므로 word_centroid_map을 피클링하자.

# 자 그럼 이제 word_centroid_map을 피클링하자.
print("word_centroid_map dumping ...")
with open("word_centroid_map.pickle", "wb") as f:
    import pickle
    pickle.dump(word_centroid_map, f)

이제 클러스터링을 새로 하지 않고 저장된 파일로부터 word_centroid_map을 불러올 수 있다.

# load word_centroid_map
print("word_centroid_map loading ...")
with open("word_centroid_map.pickle", "rb") as f:
    import pickle
    word_centroid_map = pickle.load(f)

자 이제 클러스터간 퀄리티가 왔다갔다하긴 하지만, 클러스터링된 단어들, 바꿔 말하면 각각 centroid를 갖고 있는 단어들을 확보했다. 이제 이를 사용해서 bags-of-centroids를 만들 수 있다!

결국 비슷한 단어들끼리 묶어서 Bag of Words를 하는 것이다. 비슷한 단어를 묶었으니 Bag of Clusters가 되는 셈이고, cluster가 곧 centroid이니 Bag of Centroids이다. 같은 단어의 형변환을 묶어주는 Stemming이나 Lemmatizing에서 한 단계 더 나아간 형태라고 볼 수 있다.

def create_bag_of_centroids( wordlist, word_centroid_map ):
    #
    # The number of clusters is equal to the highest cluster index
    # in the word / centroid map
    num_centroids = max( word_centroid_map.values() ) + 1
    #
    # Pre-allocate the bag of centroids vector (for speed)
    bag_of_centroids = np.zeros( num_centroids, dtype="float32" )
    #
    # Loop over the words in the review. If the word is in the vocabulary,
    # find which cluster it belongs to, and increment that cluster count
    # by one
    for word in wordlist:
        if word in word_centroid_map:
            index = word_centroid_map[word]
            bag_of_centroids[index] += 1
    #
    # Return the "bag of centroids"
    return bag_of_centroids

Bag of Words와 유사하게, Bag of Centroids를 계산하는 함수다. 마찬가지로 이 함수를 아까 단어 리스트로 정제한 리뷰들에 적용해서 우리의 데이터셋에 대한 Bag of Centroids를 만들자.

# Pre-allocate an array for the training set bags of centroids (for speed)
train_centroids = np.zeros( (train["review"].size, num_clusters), dtype="float32" )

# Transform the training set reviews into bags of centroids
counter = 0
for review in clean_train_reviews:
    train_centroids[counter] = create_bag_of_centroids( review, word_centroid_map )
    counter += 1

# Repeat for test reviews
test_centroids = np.zeros(( test["review"].size, num_clusters), dtype="float32" )

counter = 0
for review in clean_test_reviews:
    test_centroids[counter] = create_bag_of_centroids( review, word_centroid_map )
    counter += 1

이렇게 만든 Bag of Centroids를 사용해서 다시 랜덤 포레스트를 돌려보자.

# Fit a random forest and extract predictions
forest = RandomForestClassifier(n_estimators = 100)

# Fitting the forest may take a few minutes
print("Fitting a random forest to labeled training data...")
forest = forest.fit(train_centroids,train["sentiment"])
result = forest.predict(test_centroids)

# Write the test results
output = pd.DataFrame(data={"id":test["id"], "sentiment":result})
output.to_csv( "BagOfCentroids.csv", index=False, quoting=3 )

이렇게 돌리면 파트 1의 Bag of Words와 비슷하거나 살짝 안좋은 결과를 보여준다.

[kaggle] Bag of Words Meet Bags of Popcorn - (2) Part 2: Word Vectors

Part 2: Word Vectors

Code

파트 2의 코드는 여기서 확인할 수 있다.

들어가기 전에, 윈도우 환경 세팅

따로 명시하지는 않았지만, 나는 이 튜토리얼을 윈도우 환경에서 anaconda를 사용해서 돌리고 있다(원문은 맥에서 작성되었다). 지금까지 사용한 패키지들은 전부 아나콘다에 기본적으로 포함되어 있지만, 앞으로 사용할 패키지 중 gensim은 그렇지 않다. 아나콘다에 gensim을 설치해야 한다. 설치법은 여기에 나와 있는데, 매우 간단하다.

conda install -c https://conda.binstar.org/anaconda gensim

Introducing Distributed Word Vectors

이번 파트에서는 Word2Vec 알고리즘으로 생성되는 distributed word vector를 사용하는 데에 초점을 맞춘다.

이번 파트에서 사용하는 코드들은 인텔 i5 윈도우 기반으로 작성되었다. 원문은 듀얼코어 맥북 프로 기반으로 작성되었다. 원문과 환경이 다르기 때문에 조금 왔다갔다 할 수 있다.

Word2vec은 2013년에 Google이 퍼블리쉬한 distributed representations 를 단어에 대해 학습하는 뉴럴 네트워크 임플레멘테이션이다. 이전에도 다른 deep or recurrent neural network(RNN) 구조가 제안되었었으나, 모델을 학습하기 위해 필요한 시간이 너무 길다는 문제가 있었다. Word2vec은 이러한 방법들에 비해 훨씬 빨리 학습한다.

Word2Vec은 유의미한 표현(meaningful representation)을 하기 위해 클래스 라벨을 필요로 하지 않는다. 이는 매우 유용한데, 실제 데이터는 대부분이 라벨이 없기 때문이다(unlabeled). 네트워크에 충분한 트레이닝 데이터(수백억개의 단어들)를 넣으면, 네트워크는 아주 흥미로운 특징을 지닌 단어 벡터를 생성한다. 이 단어 벡터에 따라, 비슷한 의미를 가진 단어들은 클러스터를 형성하고, 클러스터들은 단어들의 관계나, 유사도에 따라 배치된다. 그러면 이런 짓이 가능하다: “king - man + woman = queen”.

Google’s code, writeup, and the accompanying papers를 체크하자. 이 프레젠테이션도 도움이 될 것이다. 오리지널 코드는 C지만, 파이썬을 포함해서 많은 다른 언어들로 포팅되었다. C를 쓰는 것도 좋겠지만 조금 까다롭다(수동적으로 헤더파일을 수정하고 컴파일해야 한다).

스탠포드의 Deep Learning for Natural Language Processing 도 살펴보자. 내용은 좋으나 양이 너무 많다…

스탠포드의 최근의 연구는 딥러닝을 감정분석에 적용했다; 코드는 자바로 되어 있다. 그러나, 문장 파싱에 의존하는 그들의 접근법은 임의의 길이의 문단에 간단히 적용할 수 없다.

Distributed word vector는 강력하고 여러 어플리케이션에서 사용할 수 있다. 특히, 단어 예측과 번역에서. 여기에서는, 우리는 이를 감정분석에 적용한다.

Using word2vec in Python

gensim 패키지를 사용하면 word2vec 임플레멘테이션을 사용할 수 있다. 여기에 좋은 튜토리얼이 있다.

Word2Vec이 GPU를 사용하지는 않지만, 매우 많은 연산을 필요로 한다. 구글 버전이나 파이썬 버전 둘 다 멀티쓰레딩을 사용한다. 우리의 모델을 적당한 시간 안에 학습시키기 위해서, cython이 필요하다. Word2Vec은 cython없이도 작동하지만 몇분 걸릴 모델 학습이 며칠이 걸리게 될 수 있다.

Preparing to Train a Model

이제 핵심으로 들어가보자! 먼저, 파트 1에서 했던 것처럼 데이터를 pandas로 읽자. 단, 이번에는 50,000개의 unlabeled 리뷰들을 담고 있는 unlabeledTrain.tsv 도 같이 사용한다. Bag of Words 모델을 만들었던 파트 1에서는 unlabeled 데이터가 쓸모없었지만, Word2Vec은 unlabeled 데이터를 사용해서 학습할 수 있으므로, 이제 50,000개의 리뷰를 추가적으로 사용할 수 있다.

import pandas as pd

# Read data from files
train = pd.read_csv( "labeledTrainData.tsv", header=0, delimiter="\t", quoting=3 )
test = pd.read_csv( "testData.tsv", header=0, delimiter="\t", quoting=3 )
unlabeled_train = pd.read_csv( "unlabeledTrainData.tsv", header=0, delimiter="\t", quoting=3 )

# Verify the number of reviews that were read (100,000 in total)
print("Read {0} labeled train reviews, {1} labeled test reviews, and {2} unlabeled reviews\n"\
      .format(train["review"].size,  test["review"].size, unlabeled_train["review"].size ))

데이터 클리닝 함수는 파트 1과 비슷하지만 약간 차이가 있다. 먼저, Word2Vec는 문장의 문맥(context)을 고려하여 하이퀄리티 단어 벡터를 생성하기 때문에, stop word를 제거하는 것이 안 좋을 수 있다. 따라서 아래 함수에서 stop word 제거를 옵셔널하게 바꾸었다. 마찬가지의 이유로 숫자도 남겨두는 것이 더 좋을 수 있는데, 이는 독자들이 직접 해보도록 하자.

# Import various modules for string cleaning
from bs4 import BeautifulSoup
import re
from nltk.corpus import stopwords

def review_to_wordlist( review, remove_stopwords=False ):
    # Function to convert a document to a sequence of words,
    # optionally removing stop words.  Returns a list of words.
    #
    # 1. Remove HTML
    review_text = BeautifulSoup(review).get_text()
    #  
    # 2. Remove non-letters
    review_text = re.sub("[^a-zA-Z]"," ", review_text)
    #
    # 3. Convert words to lower case and split them
    words = review_text.lower().split()
    #
    # 4. Optionally remove stop words (false by default)
    if remove_stopwords:
        stops = set(stopwords.words("english"))
        words = [w for w in words if not w in stops]
    #
    # 5. Return a list of words
    return words

다음으로, 인풋 포멧을 맞추어야 한다. Word2Vec은 각 문장이 단어 list로 구성된 list를 인풋으로 받는다. 즉, 인풋 포멧은 2중 list 구조다.

문단(paragraph)을 문장(sentence)으로 나누는 것은 간단한 작업이 아니다. 자연어(natural language)에는 수많은 변수들이 존재한다. 영어 문장은 “?”, “!”, “”“, “.” 등 다양한 문자로 끝날 수 있고, 띄어쓰기나 대문자는 별로 신뢰할만한 기준이 되지 못한다. 이러한 이유로, 문장 분리를 위해 NLTKpunkt tokenizer를 사용한다.

import nltk.data

# Load the punkt tokenizer
tokenizer = nltk.data.load('tokenizers/punkt/english.pickle')

# Define a function to split a review into parsed sentences
def review_to_sentences( review, tokenizer, remove_stopwords=False ):
    # Function to split a review into parsed sentences. Returns a
    # list of sentences, where each sentence is a list of words
    #
    # 1. Use the NLTK tokenizer to split the paragraph into sentences
    raw_sentences = tokenizer.tokenize(review.strip())
    #
    # 2. Loop over each sentence
    sentences = []
    for raw_sentence in raw_sentences:
        # If a sentence is empty, skip it
        if len(raw_sentence) > 0:
            # Otherwise, call review_to_wordlist to get a list of words
            sentences.append( review_to_wordlist( raw_sentence, remove_stopwords ))
    #
    # Return the list of sentences (each sentence is a list of words, so this returns a list of lists
    return sentences

이제, 이 함수들을 사용해서 데이터들을 Word2Vec의 인풋에 맞게 정제하자.

sentences = []  # Initialize an empty list of sentences

print("Parsing sentences from training set")
for i, review in enumerate(train["review"]):
    if (i+1) % 1000 == 0:
        print("[training set] {} of {}".format(i+1, train["review"].size))
    sentences += review_to_sentences(review, tokenizer)

print("Parsing sentences from unlabeled set")
for i, review in enumerate(unlabeled_train["review"]):
    if (i+1) % 1000 == 0:
        print("[unlabeled set] {} of {}".format(i+1, unlabeled_train["review"].size))
    sentences += review_to_sentences(review, tokenizer)

BeautifulSoup이 문장에 포함된 URL들에 대해 경고(warning)하겠지만 걱정하지 않아도 된다. 상당히 오래 걸리는 작업이므로 중간중간 진행과정을 출력하도록 했다.

이제 결과물을 출력해 보고 파트 1과 어떻게 다른지 살펴보자:

In[14]: print(len(sentences))
795538
In[15]: print(sentences[0])
['with', 'all', 'this', 'stuff', 'going', 'down', 'at', 'the', 'moment', 'with', 'mj', 'i', 've', 'started', 'listening', 'to', 'his', 'music', 'watching', 'the', 'odd', 'documentary', 'here', 'and', 'there', 'watched', 'the', 'wiz', 'and', 'watched', 'moonwalker', 'again']
In[16]: print(sentences[1])
['maybe', 'i', 'just', 'want', 'to', 'get', 'a', 'certain', 'insight', 'into', 'this', 'guy', 'who', 'i', 'thought', 'was', 'really', 'cool', 'in', 'the', 'eighties', 'just', 'to', 'maybe', 'make', 'up', 'my', 'mind', 'whether', 'he', 'is', 'guilty', 'or', 'innocent']

원문에는 len(sentences)가 85000+ 이라고 되어 있는데 어째선지 여기서는 80000개도 나오지 않는다. NLTK의 stop word가 추가된 것으로 짐작해본다. 혹은, 원문이 작성된 시점 이후에 데이터의 변화가 있었을 수도 있다.

지금까지의 소스를 잘 살펴보면 “+=”와 “append”가 혼용되는 것을 볼 수 있는데, 이는 두 명령의 기능적 차이 때문이다. 리스트에 변수를 더할 때는 이 두 명령이 동일하게 작동하나, 리스트에 리스트를 더할 때는 달라진다. 이 때 “+=”는 리스트의 원소들끼리 합치는 작업이고, “append”는 기존의 리스트에 새로운 리스트를 통째로 하나의 원소로 추가한다.

실제로 코드를 돌려 보면 위 작업이 엄청 오래 걸린다. 계속 코딩을 해 나가면서 위 작업을 수차례에 걸쳐 반복적으로 돌려야 하는데 그러기에는 너무 오랜 시간이다. 이를 pickle패키지를 통해 해결할 수 있다. pickle패키지는 파이썬의 객체를 통째로 파일에 덤프하고 로드하는 기능을 제공한다.

with open("sentences.pickle", "wb") as f:
    import pickle
    pickle.dump(sentences, f)

파일을 “wb”로 열어야 한다는 점을 주의하자! 피클링 한 객체는 바이트이기 때문에 byte를 의미하는 “wb”를 써야 한다. 한 번 이 코드를 실행하고 나면 이제 sentences.pickle 파일이 생기고, 다음부터는 위 전처리 과정들을 처음부터 돌릴 필요 없이 sentences.pickle로부터 불러오면 된다.

print("Load sentences from pickle ...")
with open("sentences.pickle", "rb") as f:
    import pickle
    sentences = pickle.load(f)

불러올 때도 마찬가지로 “rb”를 사용한다.

Training and Saving Your Model

이제 잘 파싱된 문장들을 갖췄으니, 모델을 학습할 준비가 되었다. 실행시간과 최종 모델의 정확도에 영향을 끼치는 파라메터들의 값을 선택해야 한다. 아래 알고리즘의 자세한 내용은 word2vec API documentationGoogle documentation을 참고하자.

  • Architecture: 아키텍처 옵션은 skip-gram (default) 와 continuous bag of words가 있다. skip-gram이 미세하게 느리지만 더 좋은 결과를 보여준다.
  • Training algorithm: hierarchical softmax (default) 와 negative sampling이 있다. 여기서는, 디폴트가 좋다.
  • Downsampling of frequent words: 구글 도큐먼트에서 .00001에서 .001 사이의 값을 추천한다. 여기서는, 0.001에 가까운 값이 좋아 보인다.
  • Word vector dimensionality: 많은 특성(feature)은 더 많은 학습시간을 요구하지만, 보통 더 좋은 결과를 낸다(항상 그런것은 아니다). 수십에서 수백 정도가 적당한 값이다; 우리는 300개의 특성을 사용한다.;
  • Context / window size: word2vec은 어떤 단어 주변의 단어들, 즉 문맥을 고려해서 해당 단어의 의미를 파악한다. 이 때 얼마나 많은 단어를 고려해야 할까? 10 정도가 hierarchical softmax에 적당하다. 이 값도 어느정도까지는 높을수록 좋다.
  • Worker threads: 패러렐 쓰레드의 수. 컴퓨터마다 다르겠지만, 일반적으로 4~6 정도가 적당하다.
  • Minimum word count: meaningful word를 규정하는 최소 word count. 이 수치 미만으로 등장하는 단어는 무시한다. 10에서 100 사이의 값이 적당하다. 우리의 경우, 각 영화가 30번씩 등장하므로, 영화 제목에 너무 많은 의미 부여를 피하기 위해 minimum word count를 40으로 설정하였다. 그 결과로 vocabulary size는 약 15,000개의 단어다.

파라메터를 선택하는 건 쉽지 않지만, 선택하고 나면 바로 Word2Vec 모델을 만들 수 있다.

# Import the built-in logging module and configure it so that Word2Vec
# creates nice output messages
import logging
logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)

# Set values for various parameters
num_features = 300    # Word vector dimensionality
min_word_count = 40   # Minimum word count
num_workers = 4       # Number of threads to run in parallel
context = 10          # Context window size
downsampling = 1e-3   # Downsample setting for frequent words

# Initialize and train the model (this will take some time)
from gensim.models import word2vec
print("Training model...")
model = word2vec.Word2Vec(sentences, workers=num_workers, \
            size=num_features, min_count = min_word_count, \
            window = context, sample = downsampling)

# If you don't plan to train the model any further, calling
# init_sims will make the model much more memory-efficient.
model.init_sims(replace=True)

# It can be helpful to create a meaningful model name and
# save the model for later use. You can load it later using Word2Vec.load()
model_name = "300features_40minwords_10context"
model.save(model_name)

혹시 이 코드를 돌렸을 때 OverflowError: Python int too large to convert to C long 가 난다면, gensim github issue page에 관련한 쓰레드가 올라와 있다. 제일 마지막에 적혀있는 대로 문제를 해결할 수 있다. (해쉬함수를 바꿨으니 작동도 달라질 수 있는데 거기까진 모르겠다)

def myhashfxn(obj):
    return hash(obj) % (2 ** 32)

word2vec.Word2Vec(hashfxn=myhashfxn)

듀얼코어 맥북 프로에서, 이 작업은 4개의 워커쓰레드를 돌릴 때 15분 이하로 걸린다. 이는 컴퓨터마다 상당히 다를 수 있다. 다행히도, 우리가 설정한 logging모듈이 친절하게 진행 상황을 알려준다.

만약 맥이나 리눅스 환경이라면, 터미널에서 “top”명령어를 통해 패러렐라이징이 잘 동작하는 것을 확인할 수 있다. 윈도우 환경이라면, PowerShell에서 “While(1) {ps | sort -des cpu | select -f 20 | ft -a; sleep 2; cls}”으로 비슷한 결과를 볼 수 있다.

# Linux or Mac
> top -o cpu

# Windows
> While(1) {ps | sort -des cpu | select -f 20 | ft -a; sleep 2; cls}

이 명령어를 통해 CPU 상태를 확인해 보면, 리스트의 제일 위에 파이썬이 있을 것이다! 4개의 워커를 사용하기 때문에 300-400%의 CPU usage를 보여준다.

CPU usage

윈도우에서 돌렸을 땐 결과가 영 딴판이었는데, 추후 다시 체크해 보자.

만약 CPU usage가 낮게 나온다면, cython이 제대로 동작하고 있지 않은 것이다.

소스의 끝을 보면, save함수를 통해 모델을 저장하는 것을 볼 수 있다. 실제로 실행 후에 파일이 생성되는데, 모델을 새로 트레이닝 하지 않고 이 파일을 로드할 수 있다.

model = word2vec.Word2Vec.load(model_name)

myhashfxn을 사용했다면, 로드하기 전에 마찬가지로 해쉬펑션을 정의해 놓아야 한다!

Exploring the Model Result

자, 그럼 이제 우리가 75,000 개의 트레이닝 리뷰를 통해 생성한 모델을 살펴보자.

“doesnt_match” 함수는 주어진 단어 셋(set) 중에서 가장 비슷하지 않은 단어를 추정한다:

In[3]: model.doesnt_match("man women child kitchen".split())
Out[3]: 'kitchen'

우리 모델이 이 의미 차이를 구분해낸다! man, women, children이 비슷하고 kitchen과는 다르다는 것을 알고 있다. 이제 도시와 나라같은 미묘한 차이도 구분하는지 확인해 보자:

In[4]: model.doesnt_match("france england germany berlin".split())
Out[4]: 'berlin'

우리가 비교적 작은 트레이닝 셋을 사용했기 때문인지, 이 모델은 완벽하지 않다:

In[7]: model.doesnt_match("paris berlin london austria".split())
Out[7]: 'london'

원문에선 “paris”를 찾는데, 위 전처리 단계에서 데이터가 달라졌으므로 이후 모델링 결과도 다른게 당연하다. 아무튼 둘 다 “austria”를 찾아내지 못한다.

이번엔 “most_similar” 함수를 써 보자. 우리의 모델이 갖고 있는 단어 클러스터(word cluster)를 살펴볼 수 있다.

In[9]: model.most_similar("man")
Out[9]: 
[('woman', 0.6386926770210266),
 ('guy', 0.4642142355442047),
 ('boy', 0.4619269073009491),
 ('person', 0.4530017077922821),
 ('men', 0.45294448733329773),
 ('lady', 0.44111737608909607),
 ('girl', 0.4240424931049347),
 ('himself', 0.42025846242904663),
 ('son', 0.39986851811408997),
 ('he', 0.3897513747215271)]

In[10]: model.most_similar("queen")
Out[10]: 
[('princess', 0.5116116404533386),
 ('latifah', 0.4850308299064636),
 ('victoria', 0.41973501443862915),
 ('widow', 0.39943596720695496),
 ('england', 0.3919205963611603),
 ('bride', 0.3856983184814453),
 ('selena', 0.3763607144355774),
 ('king', 0.3756728768348694),
 ('bee', 0.3743290305137634),
 ('rudolf', 0.3727717995643616)]

“Latifa”가 “Queen”과 비슷하다고 나오는 건 우리 데이터셋을 살펴보면 놀랍지 않다.

또는, 감정분석을 위해서는 이런 걸 찾아보자.

In[11]: model.most_similar("awful")
Out[11]: 
[('horrible', 0.6424727439880371),
 ('terrible', 0.6269798874855042),
 ('atrocious', 0.5686882734298706),
 ('dreadful', 0.5647668242454529),
 ('laughable', 0.531086266040802),
 ('appalling', 0.526667058467865),
 ('horrid', 0.5050047636032104),
 ('amateurish', 0.5010923743247986),
 ('abysmal', 0.5010562539100647),
 ('horrendous', 0.49665677547454834)]

지금까지 확인해 본 바에 따르면 이 모델은 감정분석을 하기에 충분해 보인다. 적어도 Bag of Words만큼! 하지만 우리가 어떻게 이 팬시한 단어 벡터(fancy distributed word vectors)를 supervised learning에 사용할 수 있을까? 다음 섹션에선 그 부분을 다룬다.

[kaggle] Bag of Words Meet Bags of Popcorn - (1) Part 1: Bag of Words

Part 1: For Beginners - Bag of Words

What is NLP?

NLP는 텍스트 문제에 접근하는 테크닉들의 집합이다. 이 페이지에서는 IMDB 영화 리뷰를 로드하고, 클리닝하고, 간단한 Bag of Words모델을 적용하여 리뷰가 긍정인지 부정인지 예측해본다.

Code

파트 1의 코드는 여기서 확인할 수 있다.

Reading the Data

데이터 페이지에서 필요한 파일들을 받을 수 있다. 일단 25,000개의 IMDB 영화 리뷰가 들어 있는 unlabeldTrainData.tsv가 필요하다.

그럼 이제 탭으로 구분되어 있는(tab-delimited) 파일을 파이썬으로 읽어보자. 이를 위해 pandas를 사용한다.

# Import the pandas package, then use the "read_csv" function to read
# the labeled training data
import pandas as pd       
train = pd.read_csv("labeledTrainData.tsv", header=0, \
                    delimiter="\t", quoting=3)

“header=0”은 파일의 첫번째 줄이 컬럼의 이름이라는 것을 나타내고, “delimiter=\t”는 구분자가 탭이라는 것을, “quoting=3”은 쌍따옴표(doubled quote)를 무시하라는 것을 의미한다. quoting을 주지 않으면 데이터를 불러올 때 쌍따옴표를 제거하고 불러온다.

아래와 같이 데이터를 출력해볼 수 있다.

print(train["review"][0])

Data Cleaning and Text Preprocessing

Removing HTML Markup: The BeautifulSoup Package

데이터를 보면 알겠지만 리뷰에 HTML태그가 포함되어 있다. 이를 제거하기 위해 Beautiful Soup를 사용하자.

# Import BeautifulSoup into your workspace
from bs4 import BeautifulSoup

# Initialize the BeautifulSoup object on a single movie review
example1 = BeautifulSoup(train["review"][0])

# Print the raw review and then the output of get_text(), for
# comparison
print(train["review"][0])
print(example1.get_text())

get_text함수는 html문서에서 text만 뽑아내는 것으로, 위와 같이 출력해 보면 태그가 삭제된 것을 확인할 수 있다.

Dealing with Punctuation, Numbers and Stopwords: NLTK and regular expressions

텍스트를 클리닝 할 때는 우리가 해결하고자 하는 문제가 무엇인지에 대해 생각해 보아야 한다. 많은 문제에서는 구두점(punctuation)을 제거하는 것이 일반적이지만, 이 문제에서는 감정분석 문제를 다루고 있고, “!!!”이나 “:-(” 등이 감정표현이 될 수 있으므로 구두점도 단어로 다뤄야 한다. 그러나 이 튜토리얼에서는 문제의 단순화를 위해 전부 제거할 것이다.

마찬가지로 이 튜토리얼에서는 숫자를 제거할 것이지만, 실제로는 숫자를 다루는 여러 방법이 있다. 숫자 또한 단어로 취급한다거나, 모든 숫자를 “NUM”이라는 플레이스홀더로 대체한다거나.

모든 구두점과 숫자를 제거하기 위해, 정규표현식(regular expression) 패키지 re를 사용하자.

import re
# Use regular expressions to do a find-and-replace
letters_only = re.sub("[^a-zA-Z]",           # The pattern to search for
                      " ",                   # The pattern to replace it with
                      example1.get_text() )  # The text to search
print(letters_only)

해보면 모든 구두점들이 공백으로 바뀐 것을 확인할 수 있다.

이제, 각 단어들을 소문자로 바꾸고 단어별로 분리하자. (NLP 에서는 “tokenization”이라 한다)

lower_case = letters_only.lower()        # Convert to lower case
words = lower_case.split()               # Split into words

자 이제 마지막으로, “stop words“라고 불리는 큰 의미가 없는 단어들을 어떻게 다룰 것인지 결정해야 한다. 영어에서 “a”, “and”, “is”, “the” 등이 여기에 속한다. 이를 처리하기 위해 Natural Language Toolkit (NLTK)를 사용한다. 참고로, nltk는 설치 후 nltk.download()를 통해 구성요소들을 다운로드 해 주어야 한다. (하지 않으면 하라고 에러 메시지가 뜬다)

import nltk
# nltk.download()  # Download text data sets, including stop words

from nltk.corpus import stopwords # Import the stop word list
print(stopwords.words("english"))

# Remove stop words from "words"
words = [w for w in words if not w in stopwords.words("english")]
print(words)

nltk는 stopwords corpus를 가지고 있다. 이를 불러와서 우리의 리뷰 데이터에서 stopword를 제거해주자. example1, 즉 첫번째 리뷰의 단어가 437개에서 222개로 절반 가까이 줄어드는 것을 확인할 수 있다!

텍스트 데이터에 할 수 있는 더 많은 것들이 있는데, 예를 들면 Porter Stemming and Lemmatizing(둘 다 nltk에서 할 수 있다)은 “messages”, “message”, “messaging”등의 단어들을 한 단어로 다룰 수 있게 해주는 매우 유용한 도구다. 하지만 간단함을 위해, 이 튜토리얼에서는 여기까지만 하도록 하자.

Putting it all together

이 코드들을 재사용할 수 있도록 함수로 합치자:

def review_to_words( raw_review ):
    # Function to convert a raw review to a string of words
    # The input is a single string (a raw movie review), and
    # the output is a single string (a preprocessed movie review)
    #
    # 1. Remove HTML
    review_text = BeautifulSoup(raw_review).get_text()
    #
    # 2. Remove non-letters
    letters_only = re.sub("[^a-zA-Z]", " ", review_text)
    #
    # 3. Convert to lower case, split into individual words
    words = letters_only.lower().split()
    #
    # 4. In Python, searching a set is much faster than searching
    #   a list, so convert the stop words to a set
    stops = set(stopwords.words("english"))
    #
    # 5. Remove stop words
    meaningful_words = [w for w in words if not w in stops]
    #
    # 6. Join the words back into one string separated by space,
    # and return the result.
    return( " ".join( meaningful_words ))

함수로 합치면서 두 가지가 달라졌는데, 첫번째는 속도를 위해 사용하는 set이고, 두번째는 최종적으로 추출한 단어들을 join으로 한 문장으로 합쳐 리턴한다.

clean_review = review_to_words( train["review"][0] )
print(clean_review)

이렇게 써 보면 동일한 결과를 확인할 수 있다. 이제, 모든 데이터를 클리닝 해 보자. 데이터가 25,000개나 되기 때문에 시간이 좀 걸린다:

# Get the number of reviews based on the dataframe column size
num_reviews = train["review"].size

# Initialize an empty list to hold the clean reviews
clean_train_reviews = []

# Loop over each review; create an index i that goes from 0 to the length of the movie review list
for i in range(num_reviews):
    if (i+1) % 1000 == 0:
        print("Review {0} of {1}".format(i+1, num_reviews))
    # Call our function for each one, and add the result to the list of clean reviews
    clean_train_reviews.append( review_to_words( train["review"][i] ) )

Creating Features from a Bag of Words (Using scikit-learn)

자, 그럼 이제 우리에겐 깔끔하게 정리된 리뷰들이 있다. 그럼 이걸 어떻게 숫자로 표현(numeric representation)할 것인가? 수치화 시켜야 데이터 마이닝 알고리즘을 적용할 수 있다. 가장 기본적인 방법은 Bag of Words다. 이 방법은 모든 도큐먼트의 단어들을 모아서 bag 벡터(원문에서는 vocabulary라고 표현한다)를 만들고, 각 도큐먼트의 단어 등장 횟수를 세어 bag 벡터로 표현한다.

예를 들어 “hello world”, “hello city”가 있다면, bag은 [hello, world, city]로 구성되고 따라서 위 두 도큐먼트는 각각 [1, 1, 0], [1, 0, 1]로 표현할 수 있다.

IMDB 데이터에는 수많은 리뷰가 있고 이는 커다란 bag을 형성한다. 이를 제한하기 위해 최대 bag의 크기를 정해야 한다. 여기서는, 5000개의 빈발 단어를 사용하기로 하자.

bag-of-words 특성을 추출하기 위해 scikit-learnfeature_extraction 모듈을 사용한다.

print("Creating the bag of words...")
from sklearn.feature_extraction.text import CountVectorizer

# Initialize the "CountVectorizer" object, which is scikit-learn's bag of words tool.
vectorizer = CountVectorizer(analyzer = "word",   \
                             tokenizer = None,    \
                             preprocessor = None, \
                             stop_words = None,   \
                             max_features = 5000)

# fit_transform() does two functions: First, it fits the model
# and learns the vocabulary; second, it transforms our training data
# into feature vectors. The input to fit_transform should be a list of
# strings.
train_data_features = vectorizer.fit_transform(clean_train_reviews)

# Numpy arrays are easy to work with, so convert the result to an array
train_data_features = train_data_features.toarray()
print(train_data_features.shape)

이후에 train_data_features.shape을 찍어 보면 (25000, 5000)의 매트릭스임을 확인할 수 있다. 위에서 볼 수 있듯이, CountVectorzier는 preprocessing, tokenization, stop word removal 등의 옵션을 제공한다. 자세한 건 function documentation에서 확인하자. 이 튜토리얼에서는 과정을 단계별로 보여주기 위해 직접 구현하였다.

이제, Bag of Words 모델을 학습했으니 bag의 구성요소를 살펴보자.

# Take a look at the words in the vocabulary
vocab = vectorizer.get_feature_names()
print(vocab)

원한다면, 각 단어들이 얼마나 등장했는지도 세어 볼 수 있다:

# Sum up the counts of each vocabulary word
dist = np.sum(train_data_features, axis=0)

# For each, print the vocabulary word and the number of times it
# appears in the training set
for tag, count in zip(vocab, dist):
    print(count, tag)

Random Forest

자, 이제 우리에겐 Bag of Words로 수치화된 특성들과 각 특성에 해당하는 오리지널 감정 라벨들이 있다. 그럼 이제 supervised learning을 해 보자! 여기서는 scikit-learn에서 제공하는 Random Forest classifier를 사용하고, 트리의 개수는 적절한 기본값인 100개로 설정한다. 트리의 개수를 늘리면 더 정확한 결과가 나오겠으나 더 오래 걸린다. 특성의 수도 마찬가지다. 참고로 100개도 충분히 오래 걸린다. 몇 분 걸릴 수 있으니 기다리자.

print("Training the random forest...")
from sklearn.ensemble import RandomForestClassifier

# Initialize a Random Forest classifier with 100 trees
forest = RandomForestClassifier(n_estimators = 100)

# Fit the forest to the training set, using the bag of words as
# features and the sentiment labels as the response variable
#
# This may take a few minutes to run
forest = forest.fit( train_data_features, train["sentiment"] )

Creating a Submission

이제 남은 것은 학습된 Random Forest에 테스트셋을 넣어 결과를 뽑고, 이를 submission file로 출력하는 일이다. testData.tsv를 보면 25,000개의 리뷰와 아이디가 있다. 각각의 감정 라벨을 예측해야 한다.

아래에서 Bag of Words를 테스트셋에 적용할 때는 트레이닝셋에 사용했던 “fit_transform”이 아니라 “transform”을 사용한다는 것을 알아두자. “fit_transform”을 사용하면 우리의 모델이 테스트셋에 fit하게 되고, 다시 말해 테스트셋에 overfitting하게 된다. 이러한 이유로 테스트셋은 prediction을 하기 전에는 사용하지 않는다.

코드를 잘 보면, vectorizer가 fit_transform을 사용한다. 일반적으로 overfitting이라 함은 모델이 데이터에 overfitting하는 것이고, vectorzier는 단순히 데이터를 수치화 하는 것인데? 라는 의문이 들 수 있다.

그러나 다시 한번 생각해 보면, Bag of Words또한 데이터로부터 “학습” 하는 것이다. 즉, 이 경우에 테스트셋에 대해 fit_transform을 사용하게 되면 Random Forest classifier는 그대로지만 Bag of Words모델이 테스트셋에 오버피팅하게 되는 것이다. 결국, 테스트셋을 수치화 할 때에도 트레이닝 데이터로 만든 bag 벡터(vocabulary)를 기반으로 해야 한다는 것을 알 수 있다.

# Read the test data
test = pd.read_csv("testData.tsv", header=0, delimiter="\t",quoting=3 )

# Verify that there are 25,000 rows and 2 columns
print(test.shape)

# Create an empty list and append the clean reviews one by one
num_reviews = len(test["review"])
clean_test_reviews = []

print("Cleaning and parsing the test set movie reviews...\n")
for i in range(num_reviews):
    if( (i+1) % 1000 == 0 ):
        print("Review {0} of {1}".format(i+1, num_reviews))
    clean_review = review_to_words( test["review"][i] )
    clean_test_reviews.append( clean_review )

# Get a bag of words for the test set, and convert to a numpy array
test_data_features = vectorizer.transform(clean_test_reviews)
test_data_features = test_data_features.toarray()

# Use the random forest to make sentiment label predictions
result = forest.predict(test_data_features)

# Copy the results to a pandas dataframe with an "id" column and a "sentiment" column
output = pd.DataFrame( data={"id":test["id"], "sentiment":result} )

# Use pandas to write the comma-separated output file
output.to_csv( "Bag_of_Words_model.csv", index=False, quoting=3 )

이제 드디어 submission을 할 수 있다! 여러가지를 수정해보고 결과를 비교해보자. 리뷰 클리닝을 다르게 해 보고, Bag of Words의 단어 수를 다르게 해 보고, Porter Stemming을 써 보고, 다른 classifier를 써 보는 등 다양한 걸 해 보자. 다른 데이터셋을 다뤄 보고 싶으면, Rotten Tomatoes competition에 도전해 보자. 또는, 완전히 다른 것에 대한 준비가 되었다면 Deep Learning and Word Vector 페이지로 가자!

[kaggle] Bag of Words Meet Bags of Popcorn - (0) Description

Bag of Words Meet Bags of Popcorn - (0) Description

들어가기 전에

kaggle tutorial. 요약번역. 원문의 코드는 python 2지만 여기서는 python 3이다(별 차이는 없다).

원문도 쉽게 되어 있기 때문에 원문을 보는 것도 좋다. 공부하는 겸 해서 옮겨 보았다. 이 튜토리얼을 한번 훑고 나면, kaggle competition에 참가하는 방법도 알 수 있고, nltk와 scikit-learn을 사용하는 python에서의 텍스트 마이닝 기본 과정도 경험할 수 있으며, 딥러닝을 통한 워드 임베딩인 word2vec도 살펴볼 수 있다. 지난번에는 scikit-learn에서 제공하는 20 newspaper tutorial을 해 보았는데 Bags of Popcorn까지 하고 나면 확실히 텍스트 마이닝에 입문할 수 있을 것이다.

무엇보다도, 쉽기 때문에 머리 아플때 보기 좋다!

Introduction

이 튜토리얼은 감정 분석(sentiment analysis)을 조금 “deeper”하게 살펴본다. 구글의 Word2Vec은 단어의 의미에 초점을 맞춘 멋진 딥러닝 메소드다. Word2Vec은 단어의 의미를 이해하고 단어간의 의미 관계를 파악한다. Word2Vec은 RNN(recurrent neural network)이나 DNN(deep neural nets) 등의 deep approach처럼 작동하지만 그보다 효율적이다. 이 튜토리얼에서는 Word2Vec의 감정분석에 초점을 맞춘다.

감정분석은 머신러닝에서 중요한 주제 중 하나다. 사람들의 표현은 애매모호한 경우가 많기 때문에 사람에게나 컴퓨터에게나 오해하기 쉽다. 영화 리뷰를 감정분석하는 또다른 캐글 컴페티션가 있다. 이 튜토리얼에서는 이와 비슷한 문제에 Word2Vec을 어떻게 적용하는지를 살펴본다.

drug discoverycat and dog image recognition 등 딥러닝에 관련된 몇가지 캐글 컴페티션들이 더 있으니 관심 있으면 살펴보도록 하자.

Tutorial Overview

이 튜토리얼은 두가지 목표로 구성되어 있다:
Basic NLP(Natural Language Processing): Part 1. 기본적인 NLP 프로세싱 테크닉을 다룬다.
Deep Learning for Text Understanding: Part 2, 3. Word2Vec을 사용해서 어떻게 모델을 학습하고 결과로 나오는 워드 벡터를 사용해서 어떻게 감정분석을 하는지 자세히 살펴본다.

딥러닝은 아직 정립되지 않은 빠르게 발전하고 있는 분야이기 때문에, 파트 3 또한 정확한 정답이라기 보다 Word2Vec을 사용하는 여러가지 방법을 실험하고 제시한다.

이 튜토리얼에서는 IMDB의 감정분석 데이터셋(IMDB sentiment analysis data set)을 사용한다. 이 데이터셋은 100,000개의 영화 리뷰로 구성되어 있다.

Data Set

라벨링된 데이터셋은 50,000개의 IMDB 영화 리뷰로 구성되어 있다. 이 리뷰들은 감정분석을 위해 특별히 선택되었다. 리뷰의 감정은 binary로 되어 있는데, IMDB 평점이 5 미만이면 0, 7이상이면 1로 되어 있다. 각 영화들의 리뷰가 30개를 넘지 않으며, 25,000개의 라벨링된 트레이닝셋과 테스트셋은 같은 영화가 전혀 없다. 추가로, 50,000개의 라벨링되지 않은 데이터가 제공된다.

File descriptions

  • labeldTrainData - 라벨링된 트레이닝 셋. 내용은 tab으로 구분되며 id, sentiment, review로 구성된 header row와 25,000개의 row들이 존재한다.
  • testData - 25,000개의 id, review. 모델을 트레이닝 할 때 테스트셋으로 사용하라는 게 아니라 최종적으로 이 데이터셋을 사용해서 판단한다는 의미로 보인다 (라벨링이 안 되어 있음).
  • unlabeledTrainData - 50,000개의 추가적인 라벨링되지 않은 트레이닝 셋.
  • sampleSubmission - 제출 포멧.

Code

github repo