네이버 영화 리뷰 감정 분석

elice에서 진행한 데이터사이언티스트 에듀챌린지에서 참여했던 프로젝트입니다. 경진대회 이후 학습을 목적으로 다시 한번 정리해 보았습니다.


목적

  • 네이버 영화 리뷰를 아래 평점 기반으로 학습하여 주어진 영화 리뷰의 평점(감정)을 예측해 본다.
    • NEG: 1 - 3
    • NEU: 4 - 7
    • POS: 8 - 10

데이터셋

  • 영화 리뷰 데이터 : 140자 한줄평
  • 갯수 : 700,000개
  • 기간 : 2015년
  • 영화 평점 : 0 ~ 10점

과정

  • 자연어 처리는 word embedding 기준으로 크게 1) 단어의 갯수를 세는 방식과 2) 단어의 벡터 거리를 기준으로 유사도를 확인할 수 있는 방식로 나뉨
  • 1)의 경우 TF-IDF 알고리즘을 사용하여 분석
  • 2)의 경우 gensim 패키지의 doc2vec 를 사용하여 분석

1. 네이버 영화 리뷰 데이터 처리

라이브러리 불러오기

import json
import pandas as pd
import numpy as np
from konlpy.tag import Komoran
from konlpy.tag import Twitter
from time import time
import pickle
import os

json 데이터 처리

  • json 데이터 불러오기
with open('./train_data.json') as data_file:
    data = json.load(data_file)
with open("./test.input",'r') as f_x:
    x = f_x.read().splitlines()
with open("./test.output", 'r') as f_y:
    y = f_y.read().splitlines()
  • 네이버 영화 리뷰의 json 데이터는 아래와 같은 구성으로 되어 있음
{'date': '15.08.26',
 'movie_id': 92575,
 'rating': 4,
 'review': '종합 평점은 4점 드립니다.'}
  • 따라서, 필요한 ‘리뷰’, ‘평점’에 대한 데이터만을 가져와 데이터프레임 형식으로 저장
df_movie = pd.DataFrame({
    "rating" : [ data[i]['rating'] for i in range(len(data))],
    "review" : [ data[i]['review'] for i in range(len(data))],
})
  • ‘평점’은 범수 범위에 따라서 ‘NEG’, ‘NEU’, ‘POS’ 세 가지로 분류되므로 해당 평점에 대해서 분류된 새로운 컬럼을 생성
emotion_class = [
    "POS" if df.iloc[i]['rating'] >= 8
    else
    "NEU" if df.iloc[i]['rating'] >= 4
    else
    "NEG"
    for i in range(df.shape[0])
]
df_movie["class"] = emotion_class
  • 학습 데이터프레임 저장
df_movie.to_pickle("./df_movie_review.pkl")
  • 저장된 학습 데이터프레임 불러오기
df_movie = pd.read_pickle("./df_movie_review.pkl")
  • 테스트 데이터프레임 만들기
df_test = pd.DataFrame({
    "X_test" : x[0:10000],
    "y_test" : y,
})

2. 리뷰 데이터 탐색 및 처리(자연어 다루기)

리뷰 문장 토큰으로 만들기

  • 한글 자연어 처리 라이브러리 중에서 가장 유명한 KoNLPy을 활용
  • 토큰으로 만드는 함수 중에서 Twitter 형태소 기준으로 나눠주는 twittter.morphs()를 사용
twitter = Twitter()
def tokenizer_twitter_morphs(doc):
    return twitter.morphs(doc)

def tokenizer_twitter_noun(doc):
    return twitter.nouns(doc)

def tokenizer_twitter_pos(doc):
    return twitter.pos(doc, norm=True, stem=True)
  • KoNLPy에는 한글 자연어 관련 다양한 함수가 있음
komoran = Komoran()
def tokenizer_noun(doc):
    return komoran.nouns(doc)

def tokenizer_morphs(doc):
    return komoran.morphs(doc)

  • 학습 데이터 ‘리뷰’ 토큰화하기
df_movie['token_review'] = df_movie['review'].apply(tokenizer_twitter_morphs)
  • 테스트 데이터프레임의 ‘리뷰’도 토큰화하기
df_test['X_test_tokkened'] = df_test['X_test'].apply(tokenizer_twitter_morphs)
  • 토큰화 결과
df_movie.head()
rating review class token_review
0 4 종합 평점은 4점 드립니다. NEU [종합, 평점, 은, 4, 점, 드립니, 다, .]
1 1 원작이 칭송받는 이유는 웹툰 계 자체의 질적 저하가 심각하기 때문. 원작이나 영화... NEG [원작, 이, 칭송, 받는, 이유, 는, 웹툰, 계, 자체, 의, 질적, 저하, 가...
2 10 나름의 감동도 있고 안타까운 마음에 가슴도 먹먹 배우들의 연기가 good 김수현... POS [나름, 의, 감동, 도, 있고, 안타까운, 마음, 에, 가슴, 도, 먹, 먹, 배...
3 1 이런걸 돈주고 본 내자신이 후회스럽다 최악의 쓰레기 영화 김수현 밖에없는 저질 삼류영화 NEG [이런, 걸, 돈, 주고, 본, 내, 자신, 이, 후회, 스럽, 다, 최악, 의, ...
4 7 초반엔 코미디, 후반엔 액션, 결론은 코미디. NEU [초반, 엔, 코미디, ,, 후반, 엔, 액션, ,, 결론, 은, 코미디, .]
  • 문장 단위의 토큰을 하나의 리스트로 모아서 모든 리뷰의 문장의 토큰을 확인
tokens = [ t for d in df_movie['token_review'] for t in d]
print(len(tokens))
11427433

토큰 데이터 탐색

  • nltk 라이브러리를 사용하면 토큰 데이터를 살펴볼 수 있음
import nltk
  • 토큰 데이터를 nltk.Text로 묶는다.
text = nltk.Text(tokens, name='NMSC')
print(len(text.tokens))
11427433
  • 중복을 제거한 토큰 갯수
print(len(set(text.tokens)))
119849
  • 가장 많이 나온 상위 10개의 토큰
print(text.vocab().most_common(10))
[('.', 339275), ('이', 278531), ('다', 235733), ('영화', 232438), ('의', 172695), ('을', 149037), ('가', 144290), ('에', 133350), ('..', 130105), ('는', 122824)
  • 가장 적게 나온 하위 10개 토큰
print(text.vocab().most_common()[:-20:-1])
[('요약해준', 1), ('터트렸던', 1), ('위일', 1), ('고헤이', 1), ('~~~~~~~!!!', 1), ('Indiana', 1), ('Flies', 1), ('콘드', 1), ('DmarkII', 1), ('전개하긴', 1), ('수은중독', 1), ('헤실데실거리', 1), ('써주셨', 1), ('dimitri', 1), ('반청', 1), ('앏', 1), ('믄스', 1), ('편하기', 1), ('Christus', 1)]
  • 그래프 스타일 설정
sns.set_style("darkgrid")
sns.set_palette('hls')
text.plot(20)

png

  • 입력한 키워드와 동시에 나온 토큰을 보여준다.
text.concordance('영화')
Displaying 25 of 232438 matches:
유 는 웹툰 계 자체 의 질적 저하 가 심각하기 때문 . 원작 이나 영화 나 별로 인 건 마찬가지 . 나름 의 감동 도 있고 안타까운 마음
(생략)

3. Word Embedding: TF-IDF

필요 라이브러리 불러오기

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.naive_bayes import MultinomialNB
from sklearn.linear_model import SGDClassifier
from sklearn.pipeline import Pipeline
from sklearn.metrics import accuracy_score

데이터 준비

X_train = df_movie.loc[:, 'review'].values
y_train = df_movie.loc[:, 'class'].values

4. 모델 학습 및 평가하기: TF-IDF

  • Tfidfvectorizer를 사용하여 TF-IDF를 만든다. 이때 토큰은 KoNLPyKomoran을 사용한다.
tfidf = TfidfVectorizer(tokenizer=tokenizer_morphs)
  • Pipeline을 사용하여 한번에 word embedding을 하고 학습하도록 한다.
multi_nbc = Pipeline([('vect', tfidf), ('nbc', MultinomialNB())])
start = time()
multi_nbc.fit(X_train, y_train)
end = time()
print('Time: {:f}s'.format(end-start))
Time: 554.465914s
y_pred = multi_nbc.predict(df_test["X_test"])
print("테스트 정확도: {:.3f}".format(accuracy_score(df_test["y_test"], y_pred)))
테스트 정확도: 0.696

SGD(Stochastic Gradient Descent)

  • Pipeline을 사용하여 한번에 word embedding을 하고 학습하도록 한다.
sgd_clf = Pipeline([('vect', tfidf), ('sgd', SGDClassifier(loss='hinge', penalty='l2', alpha=1e-3, random_state=42))])
start = time()
sgd_clf.fit(X_train, y_train)
end = time()
print('Time: {:f}s'.format(end-start))
y_pred = multi_nbc.predict(df_test["X_test"])
print("테스트 정확도: {:.3f}".format(accuracy_score(df_test["y_test"], y_pred)))
테스트 정확도: 0.668

5. Word Embedding: doc2vec

  • 리뷰 문장을 토큰으로 만든 후 벡터화를 해야한다. 이때 gensim 라이브러리의 doc2vec을 사용한다.

데이터 준비

  • collections 라이브러리의 namedtuple은 지정한 지정한 이름으로 튜플형태로 데이터를 묶어준다.
  • 즉, 딕셔너리와 같이 이름으로 접근이 가능하지만 데이터를 튜플 형태로 저장할 수 있다.
from collections import namedtuple
  • TaggedDocument라는 이름으로 wordstags라는 라벨을 가지고 있는 namedtuple을 클래스를 생성한다.
TaggedDocument = namedtuple('TaggedDocument', 'words tags')
  • TaggedDocument클래스를 이용하여 학습 및 테스트 리뷰 데이터의 토큰과 taget 데이터를 튜플로 묶어 변수로 만든다.
tagged_train_docs = [TaggedDocument(d, c) for d, c in df_movie[['token_review', 'class']].values]
tagged_test_docs = [TaggedDocument(d, c) for d, c in df_test[['X_test_tokkened', 'y_test']].values]
  • 학습 데이터와 테스트 데이터가 제대로 들어갔는지 갯수로 확인
print(len(tagged_test_docs), len(tagged_train_docs))
10000, 700000
  • multiprocessing을 사용해 병렬 연산 처리
import multiprocessing
cores = multiprocessing.cpu_count()
cores

로그 데이터 출력하기

  • gensim 문서에 보면 벡터화 하기 전에 아래의 코드를 실행시켜 로그 값을 출력하도록 명시하고 있다.
import logging
logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)

토큰 벡터화(word embedding) 하기

  • doc2vec 인스턴스 생성
    • hyper parameter는 참고자료를 따라 설정함
doc_vectorizer = Doc2Vec(
    dm=0,            # PV-DBOW / default 1
    dbow_words=1,    # w2v simultaneous with DBOW d2v / default 0
    window=8,        # distance between the predicted word and context words
    size=300,        # vector size
    alpha=0.025,     # learning-rate
    seed=1234,
    min_count=20,    # ignore with freq lower
    min_alpha=0.025, # min learning-rate
    workers=cores,   # multi cpu
    hs = 1,          # hierarchical softmax / default 0
    negative = 10,   # negative sampling / default 5
)
  • 먼저 doc2vec에 토큰을 로드시켜야 한다.
doc_vectorizer.build_vocab(tagged_train_docs)
print(str(doc_vectorizer))
2018-01-19 20:42:43,299 : INFO : collecting all words and their counts
2018-01-19 20:42:43,304 : INFO : PROGRESS: at example #0, processed 0 words (0/s), 0 word types, 0 tags
2018-01-19 20:42:43,660 : INFO : PROGRESS: at example #10000, processed 164582 words (464809/s), 14952 word types, 7 tags
(중략)
2018-01-19 20:43:05,705 : INFO : PROGRESS: at example #690000, processed 11262855 words (553380/s), 119019 word types, 7 tags
2018-01-19 20:43:05,994 : INFO : collected 119849 word types and 7 unique tags from a corpus of 700000 examples and 11427433 words
2018-01-19 20:43:05,995 : INFO : Loading a fresh vocabulary
2018-01-19 20:43:06,218 : INFO : min_count=20 retains 19110 unique words (15% of original 119849, drops 100739)
2018-01-19 20:43:06,220 : INFO : min_count=20 leaves 11100829 word corpus (97% of original 11427433, drops 326604)
2018-01-19 20:43:06,311 : INFO : deleting the raw counts dictionary of 119849 items
2018-01-19 20:43:06,320 : INFO : sample=0.001 downsamples 52 most-common words
2018-01-19 20:43:06,322 : INFO : downsampling leaves estimated 9032081 word corpus (81.4% of prior 11100829)
2018-01-19 20:43:06,324 : INFO : estimated required memory for 19110 words and 300 dimensions: 82182800 bytes
2018-01-19 20:43:06,369 : INFO : constructing a huffman tree from 19110 words
2018-01-19 20:43:07,384 : INFO : built huffman tree with maximum node depth 19
2018-01-19 20:43:07,476 : INFO : resetting layer weights

Doc2Vec(dbow+w,d300,n10,hs,w8,mc20,s0.001,t4)
  • 벡터화를 하기 위해서는 신경망을 통해 학습을 시켜야 하는데 이때 필요한 파라미터를 확인한다.
doc_vectorizer.corpus_count
700000
doc_vectorizer.iter
5
  • 벡터 문서를 학습한다.
# 벡터 문서 학습

start = time()
for epoch in range(10):
    doc_vectorizer.train(tagged_train_docs, total_examples=doc_vectorizer.corpus_count, epochs=doc_vectorizer.iter)
    doc_vectorizer.alpha -= 0.002 # decrease the learning rate
    doc_vectorizer.min_alpha = doc_vectorizer.alpha # fix the learning rate, no decay
end = time()
print("During Time: {}".format(end-start))
2018-01-19 21:05:38,440 : INFO : training model with 4 workers on 19110 vocabulary and 300 features, using sg=1 hs=1 sample=0.001 negative=10 window=8
2018-01-19 21:05:39,911 : INFO : PROGRESS: at 0.02% examples, 7167 words/s, in_qsize 8, out_qsize 0
(중략)
2018-01-20 02:14:40,898 : INFO : PROGRESS: at 99.84% examples, 37503 words/s, in_qsize 7, out_qsize 0
2018-01-20 02:14:41,974 : INFO : PROGRESS: at 99.94% examples, 37516 words/s, in_qsize 4, out_qsize 0
2018-01-20 02:14:42,043 : INFO : worker thread finished; awaiting finish of 3 more threads
2018-01-20 02:14:42,341 : INFO : worker thread finished; awaiting finish of 2 more threads
2018-01-20 02:14:42,641 : INFO : worker thread finished; awaiting finish of 1 more threads
2018-01-20 02:14:42,663 : INFO : worker thread finished; awaiting finish of 0 more threads
2018-01-20 02:14:42,664 : INFO : training on 57137165 raw words (55657478 effective words) took 1483.4s, 37521 effective words/s


During Time: 18544.261703014374

doc2vec 모델 저장

  • 학습된 doc2vec 모델을 저장한다
model_name = 'Doc2vec(dbow+w,d300,n10,hs,w8,mc20,s0.001,t24).model'
doc_vectorizer.save(model_name)
2018-01-20 10:08:43,858 : INFO : saving Doc2Vec object under Doc2vec(dbow+w,d300,n10,hs,w8,mc20,s0.001,t24).model, separately None
2018-01-20 10:08:43,875 : INFO : not storing attribute syn0norm
2018-01-20 10:08:43,887 : INFO : not storing attribute cum_table
2018-01-20 10:08:51,072 : INFO : saved Doc2vec(dbow+w,d300,n10,hs,w8,mc20,s0.001,t24).model

doc2vec 모델 불러오기

# Load model
doc_vectorizer = Doc2Vec.load(model_name)
2018-01-20 10:09:33,833 : INFO : loading Doc2Vec object from Doc2vec(dbow+w,d300,n10,hs,w8,mc20,s0.001,t24).model
2018-01-20 10:09:35,841 : INFO : loading docvecs recursively from Doc2vec(dbow+w,d300,n10,hs,w8,mc20,s0.001,t24).model.docvecs.* with mmap=None
2018-01-20 10:09:35,843 : INFO : loading wv recursively from Doc2vec(dbow+w,d300,n10,hs,w8,mc20,s0.001,t24).model.wv.* with mmap=None
2018-01-20 10:09:35,844 : INFO : setting ignored attribute syn0norm to None
2018-01-20 10:09:35,846 : INFO : setting ignored attribute cum_table to None
2018-01-20 10:09:35,849 : INFO : loaded Doc2vec(dbow+w,d300,n10,hs,w8,mc20,s0.001,t24).model

doc2vec 모델 탐색

  • ‘송중기’ 단어와 유사한 단어 확인
print(doc_vectorizer.wv.most_similar('송중기'))
[('한예슬', 0.43920424580574036), ('주지훈', 0.3719869554042816), ('김재원', 0.359089195728302), ('박유천', 0.3584408164024353), ('김수로', 0.34886208176612854), ('유아인', 0.34874391555786133), ('정석원', 0.3465726971626282), ('이동욱', 0.3393930494785309), ('송승헌', 0.3369532525539398), ('박용우', 0.3354697823524475)]
  • ‘쓰레기’ 단어와 유사한 단어 확인
print(doc_vectorizer.wv.most_similar('쓰레기'))
[('최악', 0.5095640420913696), ('삼류', 0.4683603346347809), ('OOO', 0.4344627559185028), ('ㅡㅡ', 0.4267716407775879), ('냐', 0.42107945680618286), ('딴', 0.4155307412147522), ('ㅄ', 0.4098493456840515), ('졸작', 0.40709546208381653), ('ㅉㅉ', 0.40385541319847107), ('쓰래', 0.4034300446510315)]
  • 벡터 연산으로 유사 단어 확인( 영화 + 남자배우 - 여배우 )
print(doc_vectorizer.wv.most_similar(positive=['영화', '남자배우'], negative=['여배우']))
[('정말', 0.36897340416908264), ('..', 0.3629817068576813), ('너무', 0.33302849531173706), ('~', 0.3224291205406189), ('도', 0.31746652722358704), ('다', 0.3103169798851013), ('참', 0.30535614490509033), ('.', 0.3039996325969696), ('이', 0.2995396554470062), ('은', 0.295747846364975)]
  • 벡터 연산으로 유사 단어 확인( 시간 + 남자 - 재미 )
print(doc_vectorizer.most_similar(positive=['시간', '남자'], negative=['재미']))
[('주인공', 0.368991881608963), ('짧은', 0.3451322317123413), ('여자', 0.34247997403144836), ('여인', 0.3287543058395386), ('서로', 0.3248821496963501), ('사랑', 0.3239684998989105), ('분', 0.3211422562599182), ('자신', 0.30553731322288513), ('러닝', 0.2998654246330261), ('둘', 0.2934240400791168)]

6. 모델 학습 및 평가하기: doc2vec

데이터셋 나누기

  • 학습과 테스트 데이터를 준비
X_train = [doc_vectorizer.infer_vector(doc.words) for doc in tagged_train_docs]
y_train = [doc.tags for doc in tagged_train_docs]
X_test = [doc_vectorizer.infer_vector(doc.words) for doc in tagged_test_docs]
y_test = [doc.tags for doc in tagged_test_docs]
  • 데이터 셋 확인
print(len(X_train), len(y_train))
700000, 700000
  • 학습 / 테스트 배열 데이터 만들기

    • tensorflow을 통해 딥러닝으로 학습할 경우 행렬 데이터가 필요하므로 데이터셋을 행렬로 만든다.

    • 이때 문자 클래스의 경우 숫자로 변환하여 배열로 만든다.

y_train_np = np.asarray([0 if c == 'NEG' else 1 if c == 'NEU' else 2 for c in y_train], dtype=int)
y_test_np = np.asarray([0 if c == 'NEG' else 1 if c == 'NEU' else 2 for c in y_test], dtype=int)
X_train_np = np.asarray(X_train)
X_test_np = np.array(X_test)
  • 배열의 열의 갯수는 앞에서 지정한 hyper parameter에서 최소 벡터의 갯수를 300으로 지정했기 때문에 아래와 같이 확인할 수 있다.
print(len(X_train_np[0]), len(X_test_np[0]))
300, 300
  • 입력값의 배열 크기 확인
X_train_np.shape
(700000, 300)
y_train_np
array([1, 0, 2, ..., 2, 1, 2])
  • 출력값의 경우 신경망에서 3개의 열로 나와야 하므로 diagonal 행렬을 만들어주는 numpynp.eye 함수를 사용하여 one-hot encoding을 해준다.

    • numpy를 이용한 one-hot encoding은 클래스의 갯수만큼 행이 있는 단위 행렬을 만들어 순서가 있는 번호로 표시된 클래스를 행의 Index처럼 사용하여 해당 행을 선택하는 방식이다.
      • ex) np.eye(3)[2] 3개의 행중 마지막 행을 선택하여 [0, 0, 1]이 결과값이 됨
y_train_np = np.eye(3)[y_train_np.reshape(-1)]
y_test_np = np.eye(3)[y_test_np.reshape(-1)]

Logistic Regression

  • 가장 간단한 모델로 logistic regression을 활용하여 학습 후 평가를 한다.
from sklearn.linear_model import LogisticRegression
clf = LogisticRegression(random_state=1234)
start = time()
clf.fit(X_train, y_train)
end = time()
print('Time: {:f}s'.format(end-start))
Time: 411.692407s
y_pred = clf.predict(X_test)
print("테스트 정확도: {:.3f}".format(accuracy_score(y_pred, y_test)))
테스트 정확도: 0.718

sklearn Neural network 학습하기

  • scikit-learn에서 제공하는 신경망을 사용해 학습 후 평가한다.
from sklearn.neural_network import MLPClassifier
mlp_clf = MLPClassifier(
    hidden_layer_sizes=(50,),
    max_iter=10,
    alpha=1e-4,
    solver='sgd',
    verbose=10,
    tol=1e-4,
    random_state=1,
    learning_rate_init=.1
)
start = time()
mlp_clf.fit(X_train, y_train)
end = time()
print('Time: {:f}s'.format(end-start))
Iteration 1, loss = 0.61686134
Iteration 2, loss = 0.60212346
Iteration 3, loss = 0.59672963
Iteration 4, loss = 0.59344720
Iteration 5, loss = 0.59059438
Iteration 6, loss = 0.58863372
Iteration 7, loss = 0.58730914
Iteration 8, loss = 0.58579955
Iteration 9, loss = 0.58503818
Iteration 10, loss = 0.58411448

Time: 177.780865s
y_pred = mlp_clf.predict(X_test)
print("테스트 정확도: {:.3f}".format(accuracy_score(y_pred, y_test)))
테스트 정확도: 0.726

Deep Neural Learning로 학습하기

  • tensorflow를 사용하여 딥러닝 모델로 학습 후 평가를 한다.
import tensorflow as tf
import random
tf.reset_default_graph()
# hyper Parameter
learning_rate = 0.001
training_epochs = 15
batch_size = 100
# input layer
X = tf.placeholder(tf.float32, [None, 300])
Y = tf.placeholder(tf.float32, [None, 3])

# dropout
keep_prob = tf.placeholder(tf.float32)

# Xavier_Initializer
xavier_init = tf.contrib.layers.xavier_initializer()
# Hidden layers and Output layer
W1 = tf.get_variable("W1", shape=[300, 256], initializer=xavier_init)
b1 = tf.Variable(tf.random_normal([256]))
L1 = tf.nn.relu(tf.matmul(X, W1)+b1)
dropout1 = tf.nn.dropout(L1, keep_prob=keep_prob)

W2 = tf.get_variable("W2", shape=[256, 256], initializer=xavier_init)
b2 = tf.Variable(tf.random_normal([256]))
L2 = tf.nn.relu(tf.matmul(dropout1, W2)+b2)
dropout2 = tf.nn.dropout(L2, keep_prob=keep_prob)

W3 = tf.get_variable("W3", shape=[256, 256], initializer=xavier_init)
b3 = tf.Variable(tf.random_normal([256]))
L3 = tf.nn.relu(tf.matmul(dropout2, W3)+b3)
dropout3 = tf.nn.dropout(L3, keep_prob=keep_prob)

W4 = tf.get_variable("W4", shape=[256, 256], initializer=xavier_init)
b4 = tf.Variable(tf.random_normal([256]))
L4 = tf.nn.relu(tf.matmul(dropout3, W4)+b4)
dropout4 = tf.nn.dropout(L4, keep_prob=keep_prob)

W5 = tf.get_variable("W5", shape=[256, 3], initializer=xavier_init)
b5 = tf.Variable(tf.random_normal([3]))
hypothesis = tf.matmul(dropout4, W5)+b5
# define cost/loss & optimizer
cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=hypothesis, labels=Y))
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(cost)

# Initialize
sess = tf.Session()
sess.run(tf.global_variables_initializer())
# Train Model
for epoch in range(training_epochs):
    avg_cost = 0
    total_batch = int(len(X_train_np) / batch_size)

    for i in range(0, len(X_train_np), batch_size):
        batch_xs = X_train_np[i:i+batch_size]
        batch_ys = y_train_np[i:i+batch_size]

        feed_dict = {X:batch_xs, Y: batch_ys, keep_prob:0.7}
        c, _ = sess.run([cost, optimizer], feed_dict=feed_dict)
        avg_cost += c / total_batch

    print('Epoch:', '{:04d}'.format(epoch +1), 'cost =', '{:.9f}'.format(avg_cost))

print('Training Finished')
Epoch: 0001 cost = 0.647052357
Epoch: 0002 cost = 0.617735932
Epoch: 0003 cost = 0.610164529
Epoch: 0004 cost = 0.604577072
Epoch: 0005 cost = 0.600691421
Epoch: 0006 cost = 0.597966505
Epoch: 0007 cost = 0.595382590
Epoch: 0008 cost = 0.593750767
Epoch: 0009 cost = 0.591372721
Epoch: 0010 cost = 0.589826050
Epoch: 0011 cost = 0.588337293
Epoch: 0012 cost = 0.587722256
Epoch: 0013 cost = 0.586452949
Epoch: 0014 cost = 0.585668081
Epoch: 0015 cost = 0.584730640
Training Finished
# Test Model and check Accuracy
correct_prediction = tf.equal(tf.argmax(hypothesis, 1), tf.argmax(Y, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
print('테스트 정확도: ', sess.run(accuracy, feed_dict={X: X_test_np ,Y:y_test_np, keep_prob:1 }))
테스트 정확도:  0.7239

CNN 활용 학습

  • tensorflowlayers 클래스를 사용하여 CNN을 만들어 학습하고 평가한다.
from tensorflow.contrib.layers import fully_connected, batch_norm, dropout
from tensorflow.contrib.framework import arg_scope
tf.reset_default_graph()
# hyper parameters
learning_rate = 0.01
training_epochs = 15
batch_size = 100
keep_prob = 0.7

# Input Layer
X = tf.placeholder(tf.float32, [None, 300])
Y = tf.placeholder(tf.float32, [None, 3])
train_mode = tf.placeholder(tf.bool, name='train_mode')

# Layer output size
hidden_output_size = 300
final_ouput_size = 3
bn_params = {
    'is_training': train_mode,
    'decay':0.9,
    'updates_collections':None
}
with arg_scope(
    [fully_connected],
    activation_fn=tf.nn.relu,
    weights_initializer = xavier_init,
    biases_initializer=None,
    normalizer_fn=batch_norm,
    normalizer_params=bn_params):

    h1 = fully_connected(X, hidden_output_size, scope='h1')
    dropout1 = dropout(h1, keep_prob, is_training=train_mode)

    h2 = fully_connected(dropout1, hidden_output_size, scope='h2')
    dropout2 = dropout(h2, keep_prob, is_training=train_mode)

    h3 = fully_connected(dropout2, hidden_output_size, scope='h3')
    dropout3 = dropout(h3, keep_prob, is_training=train_mode)

    h4 = fully_connected(dropout3, hidden_output_size, scope='h4')
    dropout4 = dropout(h4, keep_prob, is_training=train_mode)

    hypothesis = fully_connected(dropout4, final_ouput_size, activation_fn=None, scope='hypothesis')
# define Cost/Loss and Optimizer
cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=hypothesis, labels=Y))
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(cost)

# Initialize
sess = tf.Session()
sess.run(tf.global_variables_initializer())
# Train model
for epoch in range(training_epochs):
    avg_cost = 0
    total_batch = int(len(X_train_np)/batch_size)

    for i in range(0, len(X_train_np), batch_size):
        batch_xs = X_train_np[i:i+batch_size]
        batch_ys = y_train_np[i:i+batch_size]

        feed_dict_train = {X: batch_xs, Y: batch_ys, train_mode:True }
        feed_dict_cost = {X: batch_xs, Y: batch_ys, train_mode:False}

        c, _ = sess.run([cost, optimizer], feed_dict=feed_dict_train)
        avg_cost += c / total_batch
    print("Epoch: {:4d} cost={:.9f}".format(epoch+1, avg_cost))

print("Learning Finished")
Epoch:    1 cost=0.710980876
Epoch:    2 cost=0.687709728
Epoch:    3 cost=0.674387194
Epoch:    4 cost=0.666406668
Epoch:    5 cost=0.660655218
Epoch:    6 cost=0.656183415
Epoch:    7 cost=0.651884490
Epoch:    8 cost=0.648307085
Epoch:    9 cost=0.645472027
Epoch:   10 cost=0.642615740
Epoch:   11 cost=0.639964649
Epoch:   12 cost=0.638305325
Epoch:   13 cost=0.635997878
Epoch:   14 cost=0.633812798
Epoch:   15 cost=0.632047535
Learning Finished
# Test Model and Check Accuracy
correct_prediction = tf.equal(tf.argmax(hypothesis, 1), tf.argmax(Y, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

print('Accuracy:', sess.run(accuracy, feed_dict= {X:X_test_np, Y:y_test_np, train_mode:False}))
Accuracy: 0.7205

7. 전체 모델별 테스트 결과

[TF-IDF]

  • Multi-Nominal Naive Bayes : 0.696
  • SGDClassifier : 0.668

[doc2vec]

  • Logistic Regression : 0.718
  • MLPClassifier : 0.726
  • DNN(Deep Neural Network) : 0.7239
  • CNN : 0.7205

결과

  • TF-IDF 보다는 doc2vec이 성능이 더 좋았으며, 이는 doc2vecword embedding이 단어 사이의 유사도를 포함할 수 있도록 신경망으로 학습하여 만들어졌기 때문이다.(참고: 자연어(NLP) 처리 기초 정리)

  • doc2vec에서 모델별 성능 차이는 크지 않았지만 신경망을 사용한 모델이 성능이 좋았다.

  • scikit-learnMLPClassifier가 가장 성능이 좋았으며, 검색해본 결과 CNN이 더 결과가 좋다고 한다. hyper parameter를 조절하면 성능이 더 좋아질 것으로 예상된다.

  • word embedding에 따라 성능 차이가 크므로, 향후 자연어 처리 학습 방향에 있어서 word embedding의 알고리즘에 대한 이해가 중요하다는 것을 배울 수 있었다.

  • 더불어, 모델 성능을 향상시키기 위해서 토큰(단어)를 만들 때 전처리를 통해 중요도가 낮은 토큰(단어)를 제거하고, 모델의 hyper parameter를 최적화할 필요가 있다.

  • 참고로 자연어 처리에서 word embedding은 시간이 너무 오래 걸린다. GPU도 아니고, 나의 맥 에어로는 엄청난 시간이 필요했다. (특히, Pipeline을 이용한 TF-IDF 후 모델을 학습하는 것은 최악이다…)


Reference