n-gram extraction

문장이나 문서를 bag-of-words model 로 나타낼 때 일반적으로 unigram 이 이용됩니다. 그런데 문서 분류 문제에서는 unigram 보다 bigram 이 정보력이 더 좋습니다. Bigram 은 연속된 두 개의 단어를 하나의 단어로 이용하는 것입니다. 그리고 연속된 n 개의 단어를 하나의 단어로 이용하는 것을 n-gram 이라 합니다. 그러나 데이터에 등장하는 모든 n-grams 를 이용하면 bag-of-words model 의 차원이 기하급수적으로 커집니다. 이번 포스트에서는 유의미한 n-grams 을 추출하는 방법에 대하여 다뤄봅니다.

why n-gram ?

아래 문장을 단어열로 분리하면 아래와 같이 나뉘어 질 수 있습니다. 그리고 bag-of-words model 은 분리된 단어들의 빈도수를 문장의 벡터로 표현합니다.

tokenize('라라랜드는 재미있는 영화입니다')    
# 라라랜드, 는, 재미, 있는, 영화, 입니다

위 문장의 긍/부정을 판별하기 위해서 Logistic Regression 에 bag-of-words model 와 긍/부정 레이블을 함께 학습시킵니다. 학습된 Logistic Regression 의 coefficient 는 해당 단어가 긍정 혹은 부정에 얼만큼 기여하는지에 대한 의미를 지닙니다. 그런데 위 단어열만 이용하면 긍/부정을 판단하기에 모호한 점이 있습니다. ‘재미’라는 단어가 등장하였다 하여 반드시 긍정이 아닙니다. 재미-없는’ 이라는 연속된 두 단어는 부정의 의미를 표현하기 때문입니다. 또한 ‘있는’ 이라는 단어 만으로는 무엇이 있다는 것인지 알 수 없습니다. 하지만 ‘재미-있는’ 처럼 연속된 두 단어는 이 문장의 긍/부정을 좀 더 명확히 표현합니다.

이처럼 연속된 단어열은 하나의 단어보다 표현할 수 있는 정보가 뚜렷합니다. 각각의 단어를 unigram 이라 하며, 위 예시처럼 연속된 두 단어를 bigram 이라 합니다. 연속된 세 개의 단어열 trigram 이라 부르기도 하지만, 주로 세 개 이상 연속된 단어를 n-gram 이라 합니다.

n-gram 은 표현력이 좋기 때문에 문서 분류의 base model 로 이용됩니다. 특히 uni/bigram + Logistic Regression 혹은 Naive Bayes classifier 는 Wang and Manning (2012) 과 Joulin et al (2016) 에서 추천하는 base model 입니다.

문장 혹은 문서를 분류할 때에는 특정 카테고리를 잘 설명하는 단어들이 얼마나 많이 존재하는지에 대한 정보를 이용하는 것은 매우 합리적인 방법입니다. Logistic Regression 이나 Naive Bayes 는 특정 단어 혹은 bigram 이 존재했는지에 대한 정보를 직접적으로 이용합니다. 그렇기 때문에 사람이 수작업으로 하는 것과 매우 비슷한 성능을 낼 수 있으며, 사실 더 복잡한 알고리즘들을 이용하여도 성능 향상의 폭이 크지 않기도 합니다.

그러나 데이터에 존재하는 모든 n-grams 을 이용하면 bag-of-words model 의 차원이 매우 커집니다. 그렇기 때문에 분석에 유의미한 n-grams 만을 선택적으로 이용해야 합니다.

n-gram extraction by counting

가장 간단한 방법은 n-gram 빈도수가 최소값 이상인 것들을 추리는 것입니다. 이를 위하여 영화평 데이터를 이용합니다. KoNLPy 의 Komoran 을 이용하여 문장을 단어/품사열로 나눈 뒤, 이를 바탕으로 n-gram 을 추출합니다.

docs 는 list of str 형식의 문장들입니다. 각 문장을 unigram 인 words 로 나눕니다.

from konlpy.tag import Komoran
komoran = Komoran()
words = komoran.pos(doc, join=True)

그리고 이 words 를 n-grams 으로 변형하는 to_ngrams 함수를 만듭니다. 시작점 b 부터 뒤의 n 개의 단어를 취하고 이를 tuple 로 만듭니다. list 의 slice 결과는 list 입니다. Python 의 tuple 은 hashing 이 되지만, list 는 hashing 이 되지 않습니다. 이후 dict 를 이용하여 n-grams 의 빈도수를 계산할 것이기 때문입니다.

def to_ngrams(words, n):
    ngrams = []
    for b in range(0, len(words) - n + 1):
        ngrams.append(tuple(words[b:b+n]))
    return ngrams

ngram_counter 를 이용하여 to_ngrams 를 이용하여 만든 n-grams 의 빈도수를 계산합니다.

ngram_counter = defaultdict(int)
words = komoran.pos(doc, join=True)
for n in range(n_begin, n_end + 1):
    for ngram in to_ngrams(words, n):
        ngram_counter[ngram] += 1

그리고 최소 빈도수 이상의 n-grams 만을 남긴 ngram_counter 를 return 합니다.

n_range 를 변수로 둠으로써 n-grams 의 길이를 선택할 수 있도록 합니다. 아래 예시는 uni, bi, tri-grams 의 빈도수를 계산하는 코드입니다.

from collections import defaultdict

def get_ngram_counter(docs, min_count=10, n_range=(1,3)):

    def to_ngrams(words, n):
        ngrams = []
        for b in range(0, len(words) - n + 1):
            ngrams.append(tuple(words[b:b+n]))
        return ngrams

    n_begin, n_end = n_range
    ngram_counter = defaultdict(int)
    for doc in docs:
        words = komoran.pos(doc, join=True)
        for n in range(n_begin, n_end + 1):
            for ngram in to_ngrams(words, n):
                ngram_counter[ngram] += 1

    ngram_counter = {
        ngram:count for ngram, count in ngram_counter.items()
        if count >= min_count
    }

    return ngram_counter

ngram_counter = get_ngram_counter(docs)

ngram_counter 에는 uni, bi, tri-grams 이 함께 섞여 있습니다.

sorted(ngram_counter, key=lambda x:-ngram_counter[x])[5000:5010]
# [('말/VX', '라/EC'),
#  ('가/VV', '아야/EC'),
#  ('ㄹ지/EC', '궁금/XR', '하/XSA'),
#  ('슬픔/NNG',),
#  ('들/VV', '네요/EC'),
#  ('받/VV', '은/ETM'),
#  ('속/NNG', '에서/JKB'),
#  ('여배우/NNG',),
#  ('정말/MAG', '재미있/VA', '게/EC'),
#  ('ㄹ/JKO', '보/VV')]

n-gram tokenizer

빈도수를 기준으로 선택한 n-grams 를 출력하는 토크나이저를 만듭니다. NgramTokenizer 는 기본으로 이용할 토크나이저와 학습된 n-grams 사전, 그리고 사용할 n-grams 의 범위를 입력받습니다.

tokenize 함수는 위의 예시처럼 base tokenizer 를 이용하여 만든 unigram 을 만듭니다.

_to_ngrams 에서 n-gram 후보를 만든 다음, self.ngrams 에 등록된 n-grams 인지 확인하여 최종적인 n-grams 를 선정합니다.

n-gram 안에 띄어쓰기가 포함되면 unigram 과 혼란이 되기 때문에 이를 ‘-‘ 으로 연결합니다.

NgramTokenizer 를 scikit-learn 의 CountVectorizer 의 함수로 입력하기 위하여 call 함수를 구현합니다. 문장이 입력되면 tokenize 함수로 넘겨줍니다.

class NgramTokenizer:

    def __init__(self, ngrams, base_tokenizer, n_range=(1, 3)):
        self.ngrams = ngrams
        self.base_tokenizer = base_tokenizer
        self.n_range = n_range

    def __call__(self, sent):
        return self.tokenize(sent)

    def tokenize(self, sent):
        if not sent:
            return []

        unigrams = self.base_tokenizer.pos(sent, join=True)

        n_begin, n_end = self.n_range
        ngrams = []
        for n in range(n_begin, n_end + 1):
            for ngram in self._to_ngram(unigrams, n):
                ngrams.append('-'.join(ngram))
        return ngrams

    def _to_ngrams(self, words, n):
        ngrams = []
        for b in range(0, len(words) - n + 1):
            ngram = tuple(words[b:b+n])
            if ngram in self.ngrams:
                ngrams.append(ngram)
        return ngrams

ngram_tokenizer = NgramTokenizer(ngram_counter, komoran)

예문에 대하여 코모란을 이용한 unigrams 는 아래와 같습니다.

sent = '재미있는 영화들에 대한 리뷰가 많군요'
komoran.pos(sent, join=True)
# ['재미있/VA',
#  '는/ETM',
#  '영화/NNG',
#  '들/XSN',
#  '에/JKB',
#  '대하/VV',
#  'ㄴ/ETM',
#  '리뷰/NNP',
#  '가/JKS',
#  '많/VA',
#  '군요/EC']

앞서 만든 n-gram tokenizer 를 이용하면 uni, bi, tri-grams 로 이뤄진 단어열이 출력됩니다.

ngram_tokenizer(sent)
# ['재미있/VA',
#  '는/ETM',
#  '영화/NNG',
#  '들/XSN',
#  '에/JKB',
#  '대하/VV',
#  'ㄴ/ETM',
#  '리뷰/NNP',
#  '가/JKS',
#  '많/VA',
#  '군요/EC',
#  '재미있/VA-는/ETM',
#  '는/ETM-영화/NNG',
#  '영화/NNG-들/XSN',
#  '들/XSN-에/JKB',
#  '에/JKB-대하/VV',
#  '대하/VV-ㄴ/ETM',
#  '가/JKS-많/VA',
#  '재미있/VA-는/ETM-영화/NNG',
#  '들/XSN-에/JKB-대하/VV',
#  '에/JKB-대하/VV-ㄴ/ETM']

직접 만든 n-grams tokenizer 를 CountVectorizer 에 입력하여 bag-of-words model 을 학습합니다. tokenizer 에 우리가 만든 ngram_tokenizer 를 입력합니다. lowercase 의 기본값은 True 이며, 이 때에는 모든 영어를 소문자로 변환합니다. 품사 태그의 형식을 보존하기 위하여 lowercase = False 로 설정합니다. fit_transform 함수에 list of str 형식의 문장을 입력하면 bag-of-words model 이 학습됩니다. 그 크기는 30,417 개의 uni/bi/tri-grams 로 이뤄진 86,000 개의 문장입니다.

from sklearn.feature_extraction.text import CountVectorizer

vectorizer = CountVectorizer(
    tokenizer = ngram_tokenizer,
    lowercase = False,
)
x = vectorizer.fit_transform(texts)
x.shape # (86000, 30417)

그런데 빈도수만 이용하여 n-grams 을 추출하면 유익하지 않은 n-grams 도 추출됩니다. n-grams 의 마지막 단어가 ‘영화/NNG’ 인 경우에 대하여 빈도수 기준 상위 10 개를 살펴보면 조사나 어미 다음에 ‘영화/NNG’ 가 등장한 경우가 많습니다. 빈도수 기준으로는 조사나 어미가 자주 등장하기 때문입니다.

for ngram, count in sorted(ngram_counter.items(), key=lambda x:-x[1]):
    if ngram[-1] == '영화/NNG':
        print(ngram, count)
('영화/NNG',) 17792
('는/ETM', '영화/NNG') 3141
('ㄴ/ETM', '영화/NNG') 2425
('이/MM', '영화/NNG') 1233
('은/ETM', '영화/NNG') 1194
('의/JKG', '영화/NNG') 890
('되/XSV', '는/ETM', '영화/NNG') 590
('하/XSA', 'ㄴ/ETM', '영화/NNG') 530
('최고/NNG', '의/JKG', '영화/NNG') 487
('ㄹ/ETM', '영화/NNG') 432
...

n-gram extraction by Point Mutual Information

(‘의/JKG’, ‘영화/NNG’) 에서 ‘의/JKG’ 는 영화 앞이 아니어도 자주 등장하기 때문에 그 영향력을 줄여줘야 합니다. 한 가지 방법으로 다음처럼 bigram 의 점수를 만들 수 있습니다. 단어 가 등장한 빈도수에서 를 뺍니다. 그리고 각 단어의 빈도수 로 이 값을 나눕니다. 는 최소 빈도수 역할을 합니다. 보다 작은 경우에는 음의 값을 지니기 때문입니다. 그리고 각 단어의 빈도수로 이를 나눠줌으로써 자주 등장하기 때문에 n-gram 빈도수가 높았던 단어들의 점수를 낮춰줍니다.

이 방법은 Point Mutual Information (PMI) 를 변형한 방법입니다. PMI 는 의 log 로 정의되는데, 이 값에 전체 단어 수 을 곱하면 입니다.

그리고 위 식을 이용하여 trigram 의 score 도 정의할 수 있습니다. trigram 의 앞에 위치한 bigram 을 으로, 뒤에 위치한 bigram 을 로 이용하면 아래와 같이 trigram 의 n-gram 점수를 정의할 수 있습니다.

get_ngram_score 는 이를 구현한 함수입니다. 단어의 개수가 2 이상인 n-grams 에 대하여 마지막 단어를 제거한 sub n-gram 의 빈도수를 first 로, 앞의 한 단어를 제거한 sub n-gram 의 빈도수를 second 로 정의합니다. 그리고 n-gram 의 빈도수에서 를 빼고, first, second 의 곱으로 이 값을 나눠줍니다.

first = ngram_counter[ngram[:-1]]
second = ngram_counter[ngram[1:]]
score = (count - delta) / (first * second)

우리는 score 가 0 보다 큰 경우에만 n-grams 으로 추출합니다.

def get_ngram_score(ngram_counter, delta=30):
    ngrams_ = {}
    for ngram, count in ngram_counter.items():
        if len(ngram) == 1:
            continue
        first = ngram_counter[ngram[:-1]]
        second = ngram_counter[ngram[1:]]
        score = (count - delta) / (first * second)
        if score > 0:
            ngrams_[ngram] = (count, score)
    return ngrams_

ngram_scores = get_ngram_score(ngram_counter)

trigram 에 대해서 점수가 높은 상위 10 개의 예시를 살펴봅니다. 영화 ‘미션 임파서블’이 제대로 토크나이징이 되지 않아 ‘서브/NNP’ 로 인식되었지만, 이 세 단어는 유독 함께 등장하기 때문에 높은 n-gram 점수를 얻었습니다.

('미션/NNP', '임파/NNG', '서브/NNP')

비슷하게 영화제목 ‘맨 오브 스틸’ 이나 대화체인 ‘같애요’의 형태소들이 높은 점수를 얻었습니다.

('맨/XPN', '오브/NNG', '스틸/NNP')
('같/VA', '애/EF', '요/JX')

이처럼 유독 n-grams 의 단어들이 함께 등장하는지에 대한 경향을 이용하면 여러 단어로 표현되는 개념 (multi-words expression) 들이 n-grams 으로 추출되기도 합니다.

trigram_scores = {
    ngram:score for ngram, score in ngram_scores.items()
    if len(ngram) == 3
}

sorted(trigram_scores.items(), key=lambda x:-x[1][1])[:10]
[(('같/VA', '애/EF', '요/JX'), (57, 0.008310249307479225)),
 (('엇/EP', '데/EC', '이/NP'), (65, 0.008158508158508158)),
 (('맨/XPN', '오브/NNG', '스틸/NNP'), (56, 0.007864488808227465)),
 (('위/NNP', 'ㄹ/ETM', '스미스/NNP'), (74, 0.007623007623007623)),
 (('미션/NNP', '임파/NNG', '서브/NNP'), (53, 0.007603305785123967)),
 (('아/EC', '세/VV', '요/EC'), (80, 0.007437156031533542)),
 (('도/JX', '불구/XR', '하/XSA'), (49, 0.0070266272189349116)),
 (('암/NNG', '니스/NNP', 'ㄴ/JX'), (87, 0.0069614069369809475)),
 (('임파/NNG', '서브/NNP', 'ㄹ/JKO'), (49, 0.006909090909090909)),
 (('을/JKO', '떼/VV', 'ㄹ/ETM'), (65, 0.0067528458421763455))]

Bigram 에 대해서도 살펴보면 ‘무대인사’ 와 같은 복합명사나 ‘히스 레저’, ‘팀 버튼’과 같은 사람 이름이 높은 점수를 받습니다. 하지만 이들을 ‘레저’나 ‘버튼’의 unigram 으로 표현하면 오히려 다른 맥락으로 인식될 수 있습니다.

bigram_scores = {
    ngram:score for ngram, score in ngram_scores.items()
    if len(ngram) == 2
}

sorted(bigram_scores.items(), key=lambda x:-x[1][1])[:10]
[(('임파/NNG', '서브/NNP'), (55, 0.007431629013079667)),
 (('무대/NNP', '인사/NNP'), (87, 0.006884057971014493)),
 (('한스/NNP', '짐머/NNP'), (50, 0.006493506493506494)),
 (('한국영/NNP', '화중/NNP'), (46, 0.0060790273556231)),
 (('미션/NNP', '임파/NNG'), (55, 0.005387931034482759)),
 (('누/NNP', '군가/NNP'), (42, 0.005291005291005291)),
 (('히스/NNP', '레저/NNP'), (44, 0.004908835904628331)),
 (('강/NNG', '추하/VA'), (134, 0.004855275443510738)),
 (('사도/NNP', '세자/NNG'), (63, 0.0046452702702702705)),
 (('팀/NNG', '버튼/NNP'), (88, 0.004220944618295612))]

n-gram extraction by templates

때로는 우리가 원하는 n-grams 의 품사 태그 구조가 있기도 합니다. ‘바람-의-나라’와 같은 고유명사는 ‘Noun - Josa - Noun’ 입니다. 이처럼 특정한 품사열의 n-grams 만을 선택하는 것도 유용한 n-grams 을 추출하는 방법입니다.

이를 위하여 match 함수를 만듭니다. ngram 과 template 의 길이가 같고 모든 품사도 포함되어 있어야 True 를 return 합니다. n-gram 의 각 단어에 품사를 표현하는 substring 이 존재하는지 확인함으로써 해당 품사인지 확인할 수 있습니다. 이 때 templates 에 ‘/NNG’가 아닌 ‘/NN’ 을 이용하면 고유 명사 (NNP) 와 일반 명사 (NNG) 를 모두 포함할 수 있습니다. ‘/J’ 를 이용하면 모든 종류의 조사를 포함할 수 있습니다. 이러한 템플릿은 사용하는 형태소 분석기에 따라 다르게 정의되야 합니다.

templates = [
    ('/NN', '/J', '/NN'),
    ('/NN', '/NN')
]

def match(ngram, template):
    if len(ngram) != len(template):
        return False
    for n, t in zip(ngram, template):
        if not (t in n):
            return False
    return True

ngram = ('최고/NNG', '의/JKG', '영화/NNG')
template = templates[0]
match(ngram, template) # True

빈도수와 템플릿을 모두 이용하는 n-gram 추출 함수를 만듭니다. 앞서 만든 get_ngram_counter 함수에 find_matched_ngram 함수를 추가하는 부분만 변경합니다. n-grams 후보를 찾은 뒤, 템플릿이 매칭된 n-grams 만 선택합니다.

for n in range(n_begin, n_end + 1):
    ngrams = to_ngrams(words, n)
    ngrams = find_matched_ngram(ngrams)
def get_matched_ngram_counter(docs, templates, min_count=10, n_range=(2,3)):

    def to_ngrams(words, n):
        ngrams = []
        for b in range(0, len(words) - n + 1):
            ngrams.append(tuple(words[b:b+n]))
        return ngrams

    def find_matched_ngram(ngrams):
        matcheds = []
        for ngram in ngrams:
            for template in templates:
                if match(ngram, template):
                    matcheds.append(ngram)
        return matcheds

    n_begin, n_end = n_range
    ngram_counter = defaultdict(int)
    for doc in docs:
        words = komoran.pos(doc, join=True)
        for n in range(n_begin, n_end + 1):
            ngrams = to_ngrams(words, n)
            ngrams = find_matched_ngram(ngrams)
            for ngram in ngrams:
                ngram_counter[ngram] += 1

    ngram_counter = {
        ngram:count for ngram, count in ngram_counter.items()
        if count >= min_count
    }

    return ngram_counter

ngram_counter = get_matched_ngram_counter(docs, templates)

n-gram 을 이용한 긍/부정 표현 탐색

Logistic Regression 을 이용하여 영화평의 긍/부정 분류기를 학습하면 coefficient 에 어떤 단어가 긍정 혹은 부정적인 표현인지 학습됩니다.

학습한 ngram tokenizer 를 이용하여 train_x 를 만듭니다.

ngram_counter = get_ngram_counter(train_texts)
ngram_tokenizer = NgramTokenizer(ngram_counter, komoran)

vectorizer = CountVectorizer(tokenizer = ngram_tokenizer)
train_x = vectorizer.fit_transform(train_texts)

Logistic Regression 을 학습하여 긍, 부정 단어 50 개를 선택합니다.

from sklearn.linear_model import LogisticRegression

classifier = LogisticRegression()
classifier.fit(train_x, train_label)

idxs_coef = list(enumerate(classifier.coef_[0]))
positive_idxs = sorted(idxs_coef, key=lambda x:-x[1])[:50]
negative_idxs = sorted(idxs_coef, key=lambda x:x[1])[:50]

vocab2idx = vectorizer.vocabulary_
idx2vocab = list(sorted(vocab2idx, key=lambda x:vocab2idx[x]))

긍정적인 단어로 ‘최고/NNG’ 나 ‘굿/NNG’ 도 포함되지만, ‘안-아깝’ 이라던지 ‘아깝-지-않’, ‘믿-고-보’와 같이 긍정적인 구문들도 대표적인 긍정 단어로 선택됩니다.

최고/NNG (2.514) 중력/NNP (1.704) 또/MAJ-다른/MM (1.566) 눈물/NNG-이/JKS (1.468)
굿/NNG (2.372) 아깝/VA-지/EC-않/VX (1.695) 5/SN-개/NNB (1.558) 기대/NNG-중/NNB (1.466)
1/SN-점/NNB-주/NNP (2.133) 잘/MAG-만들/VV (1.680) 드뎌/NA (1.537) 여운/NNP (1.463)
대박/NNP (2.115) 드디어/MAG (1.676) 명작/NNG (1.534) 멋지/VA (1.461)
안/MAG-보/VV-ㄴ/ETM (2.037) 짱/MAG (1.670) 기대/NNG (1.515) 인생/NNP (1.457)
만점/NNG (1.938) 레알/NNP (1.646) 세/MM (1.513) 10/SN-점/NNB (1.456)
안/MAG-아깝/VA (1.913) 보고싶다/NNP (1.638) 무도/NNP (1.510) 흥/NNG-하/XSV-아라/EC (1.453)
트리/NNP (1.870) 황진미/NNP (1.633) 캡틴/NNP (1.505) 별로/MAG-없/VA (1.449)
재밋을듯/NA (1.870) 마녀사냥/NNP (1.632) 데드풀/NNP (1.497) 장난/NNG-아니/VCN (1.447)
평가/NNG-하/XSV (1.804) 알이즈웰/NA (1.608) 꼭/MAG (1.487) 아깝/VA-지/EC (1.447)
흥/NNG-하/XSV (1.799) 재밌/VA (1.607) 기다리/VV (1.484) 곡성/NNP (1.444)
평점/NNG-깍/VV (1.732) 믿/VV-고/EC-보/VV (1.605) 이/JKS-다르/VA (1.482) 알바/NNP-라고/NNP (1.435)
재미있/VA (1.712) 느라/EC (1.592) 믿/VV-고/EC-보/VX (1.469) 별로/MAG-재미없/VA (1.434)

반대로 ‘이건-아니’나 ‘안-보-ㄴ다’와 같은 부정적인 관용구도 선택됩니다.

최악/NNG (-3.444) 뭥미/NA (-2.210) 여주인공/NNG (-1.927) 막장/NNG (-1.780)
10/SN-점/NNB-주/NNP (-3.028) 쓰레기/NNG (-2.194) 민폐/NNG (-1.912) 왜/MAG-보/VV (-1.768)
쓰레기/NNP (-2.781) 신세경/NNP (-2.138) 글쎄/IC (-1.884) 미화/NNG (-1.767)
별루/MAG (-2.763) 베끼/VV (-2.125) 실망/NNG-하/XSV-았/EP (-1.861) 폭동/NNP (-1.759)
기대/NNG-도/JX (-2.702) 거르/VV (-2.121) 동참/NNG (-1.858) 안함/NNP (-1.751)
이석기/NNP (-2.679) 배신자/NNG (-2.118) 글쎄요/IC (-1.852) 잘/MAG-하/XSV-는/ETM (-1.748)
망/NNG (-2.572) 차라리/MAG (-2.066) 너무/MAG-높/VA (-1.845) 구려/NNP (-1.743)
성지/NNP (-2.508) 안/MAG-보/VV-ㄴ다/EC (-2.029) 밀리/VV (-1.839) 유치/NNP (-1.743)
지겹/VA (-2.479) 이건/NNP-아니/VCN (-2.012) 기대/NNG-도안/NNG (-1.826) 황당/XR (-1.737)
별로/MAG (-2.437) 박유천/NNP (-1.984) 알바천국/NNP (-1.820) 고/EC-있/VX-는/ETM (-1.735)
기대/NNG-안함/NNP (-2.356) 석기/NNP (-1.966) 노/NNG-재/VV (-1.815) 도/JX-개봉/NNP (-1.691)
재미없/VA (-2.289) 기대/NNG-가/JKS-안/NNG (-1.949) 노/NNG-재/VV-ㅁ/ETN (-1.815) 로맨틱/NNP (-1.690)
장훈/NNP (-2.236) 실망/NNP (-1.941) 권상우/NNP (-1.790) 노/NNP-재/VV (-1.664)

Reference

  • Wang, S. and Manning, C. D. (2012). Baselines and bigrams: Simple, good sentiment and topic classification. In Proceedings of the 50th Annual Meeting of the Association for Computational Linguistics 82
  • Joulin, A., Grave, E., Bojanowski, P., and Mikolov, T. (2016). Bag of tricks for efficient text classification. arXiv preprint arXiv:1607.01759.