또르르's 개발 Story

[16-1] NaiveBayes Classifier Using Konlpy 본문

부스트캠프 AI 테크 U stage/실습

[16-1] NaiveBayes Classifier Using Konlpy

또르르21 2021. 2. 16. 01:09

1️⃣ 설정

 

konlpy를 설치합니다.

konlpy는 다양한 한국어 형태소 분석기가 클래스로 구현되어 있습니다.

!pip install konlpy

설치한 konlpy와 나머지 필요한 모듈을 import 합니다.

from tqdm import tqdm

from konlpy import tag # 다양한 한국어 형태소 분석기가 클래스로 구현되어 있음

from collections import defaultdict


import math

 

 

2️⃣ 학습 및 테스트 데이터 전처리

 

NaiveBayes Classifier를 통해 해당 Sentence가 긍정인지 부정인지를 분류할 것입니다.

따라서 class는 긍정(1), 부정(0) 2가지 class로 구성되어 있습니다.

train_labels에서 1은 긍정, 0은 부정을 나타내고, test_data를 통해 긍정과 부정을 추론할 것입니다.

train_data = [
  "정말 맛있습니다. 추천합니다.",
  "기대했던 것보단 별로였네요.",
  "다 좋은데 가격이 너무 비싸서 다시 가고 싶다는 생각이 안 드네요.",
  "완전 최고입니다! 재방문 의사 있습니다.",
  "음식도 서비스도 다 만족스러웠습니다.",
  "위생 상태가 좀 별로였습니다. 좀 더 개선되기를 바랍니다.",
  "맛도 좋았고 직원분들 서비스도 너무 친절했습니다.",
  "기념일에 방문했는데 음식도 분위기도 서비스도 다 좋았습니다.",
  "전반적으로 음식이 너무 짰습니다. 저는 별로였네요.",
  "위생에 조금 더 신경 썼으면 좋겠습니다. 조금 불쾌했습니다."
]

train_labels = [1, 0, 0, 1, 1, 0, 1, 1, 0, 0]

test_data = [
  "정말 좋았습니다. 또 가고 싶네요.",
  "별로였습니다. 되도록 가지 마세요.",
  "다른 분들께도 추천드릴 수 있을 만큼 만족했습니다.",
  "서비스가 좀 더 개선되었으면 좋겠습니다. 기분이 좀 나빴습니다."
]

KoNLPy 패키지에서 제공하는 Twitter(Okt) tokenizer를 사용하여 tokenization 합니다.

tokenizer = tag.Okt()
def make_tokenized(data):

  tokenized = []  # 단어 단위로 나뉜 리뷰 데이터.
  

  for sent in tqdm(data):
  
    tokens = tokenizer.morphs(sent)
    
    tokenized.append(tokens)
    

  return tokenized
train_tokenized = make_tokenized(train_data)

test_tokenized = make_tokenized(test_data)
>>> train_tokenized   # token은 띄어쓰기 구분이 아닌 의미단위로 설정


[['정말', '맛있습니다', '.', '추천', '합니다', '.'],
 ['기대했던', '것', '보단', '별로', '였네요', '.'],
 ['다',
  '좋은데',
  '가격',
  '이',
  '너무',
  '비싸서',
  '다시',
  '가고',
  '싶다는',
  '생각',
  '이',
  '안',
  '드네',
  '요',
  '.'],
 ['완전', '최고', '입니다', '!', '재', '방문', '의사', '있습니다', '.'],
 ['음식', '도', '서비스', '도', '다', '만족스러웠습니다', '.'],
 ['위생',
  '상태',
  '가',
  '좀',
  '별로',
  '였습니다',
  '.',
  '좀',
  '더',
  '개선',
  '되',
  '기를',
  '바랍니다',
  '.'],
 ['맛', '도', '좋았고', '직원', '분들', '서비스', '도', '너무', '친절했습니다', '.'],
 ['기념일',
  '에',
  '방문',
  '했는데',
  '음식',
  '도',
  '분위기',
  '도',
  '서비스',
  '도',
  '다',
  '좋았습니다',
  '.'],
 ['전반', '적', '으로', '음식', '이', '너무', '짰습니다', '.', '저', '는', '별로', '였네요', '.'],
 ['위생', '에', '조금', '더', '신경', '썼으면', '좋겠습니다', '.', '조금', '불쾌했습니다', '.']]

학습 데이터 기준으로 가장 많이 등장한 단어부터 순서대로 vocab에 추가합니다.

word_count = defaultdict(int)  # Key: 단어, Value: 등장 횟수


for tokens in tqdm(train_tokenized):

  for token in tokens:
  
    word_count[token] += 1
word_count = sorted(word_count.items(), key=lambda x: x[1], reverse=True)

>>> print(len(word_count))

66

w2i는 key: 단어, value:단어 index로 구성된 vocabulary입니다.

w2i = {}  # Key: 단어, Value: 단어의 index

for pair in tqdm(word_count):

  if pair[0] not in w2i:
  
    w2i[pair[0]] = len(w2i)
>>> w2i

{'!': 35,
 '.': 0,
 '가': 41,
 '가격': 23,
 '가고': 26,
 '개선': 43,
 '것': 20,
 '기념일': 52,
 '기대했던': 19,
 '기를': 45,
 
 ...
 
 '직원': 49,
 '짰습니다': 59,
 '최고': 33,
 '추천': 17,
 '친절했습니다': 51,
 '합니다': 18,
 '했는데': 53}

 

 

3️⃣ 모델 Class 구현

 

NaiveBayes Classifier 모델 클래스를 구현합니다.

 

  • self.k: Smoothing을 위한 상수.
  • self.w2i: 사전에 구한 vocab.
  • self.priors: 각 class의 prior 확률.
  • self.likelihoods: 각 token의 특정 class 조건 내에서의 likelihood.

여기서 self.k 즉, smoothing은 Laplace Smoothing을 뜻하며, $P(x|c)$에서 $x$가 vocab에는 존재하지만 $c$에는 존재하지 않는 경우 항상 $P(x|c)$는 0이 나오는 문제점을 해결합니다. 수식 $P(x_{i}|c)$가 0이 되지 않도록 일정한 상수 $k$를 더하는 방법을 사용합니다.

 

class NaiveBayesClassifier():

  def __init__(self, w2i, k=0.1): # k는 적당한 값으로
  
    self.k = k
    
    self.w2i = w2i
    
    self.priors = {}
    
    self.likelihoods = {}
    

  def train(self, train_tokenized, train_labels):
  
    self.set_priors(train_labels)  # Priors 계산.
    
    self.set_likelihoods(train_tokenized, train_labels)  # Likelihoods 계산.
    

  def inference(self, tokens):
  
    log_prob0 = 0.0   # 확률 값이기 때문에 0~1 사이만 가능한데 곱셈을 하게되면 0으로 수렴하는 현상 / 따라서 log를 사용해 곱셈을 덧셈으로 바꿔줘서 더해줌
    
    log_prob1 = 0.0
    

    for token in tokens:
    
      if token in self.likelihoods:  # 학습 당시 추가했던 단어에 대해서만 고려.
      
        log_prob0 += math.log(self.likelihoods[token][0])   # log를 취해서 더해줌
        
        log_prob1 += math.log(self.likelihoods[token][1])
        

    # 마지막에 prior를 고려.
    
    log_prob0 += math.log(self.priors[0])
    
    log_prob1 += math.log(self.priors[1])
    

    if log_prob0 >= log_prob1:
    
      return 0
      
    else:
    
      return 1
      

  # 문장 안에 단어가 얼마나 존재하는지의 확률
  
  def set_priors(self, train_labels):
  
    class_counts = defaultdict(int)
    
    for label in tqdm(train_labels):
    
      class_counts[label] += 1
      
    
    for label, count in class_counts.items():
    
      self.priors[label] = class_counts[label] / len(train_labels)
      

  # likelihoods {key (token) : value{key (class : 0,1) : value (확률)}}
  
  def set_likelihoods(self, train_tokenized, train_labels):
  
    token_dists = {}  # 각 단어의 특정 class 조건 하에서의 등장 횟수.
    
    class_counts = defaultdict(int)  # 특정 class에서 등장한 모든 단어의 등장 횟수.
    

    for i, label in enumerate(tqdm(train_labels)):
    
      count = 0
      
      for token in train_tokenized[i]:
      
        if token in self.w2i:  # 학습 데이터로 구축한 vocab에 있는 token만 고려.
        
          if token not in token_dists:
          
            token_dists[token] = {0:0, 1:0}
            
          token_dists[token][label] += 1
          
          count += 1
          
      class_counts[label] += count
      

    for token, dist in tqdm(token_dists.items()):
    
      if token not in self.likelihoods:
      
        self.likelihoods[token] = {
        
            0:(token_dists[token][0] + self.k) / (class_counts[0] + len(self.w2i)*self.k),
            
            1:(token_dists[token][1] + self.k) / (class_counts[1] + len(self.w2i)*self.k),
            
            # k는 Smoothing을 위한 상수.

            # Laplace Smoothing
        }

함수 set_priors는 문장 안에 단어가 얼마나 존재하는지의 확률을 의미합니다.

def set_priors(self, train_labels):

    class_counts = defaultdict(int)
    
    for label in tqdm(train_labels):
    
      class_counts[label] += 1
      
    
    for label, count in class_counts.items():
    
      self.priors[label] = class_counts[label] / len(train_labels)

함수 set_likelihoods는

 

self.liklihoods = {key (token) : value {key (class : 0,1) : value (확률)}}

 

형태로 만들어집니다.

Liklihoods는 각 token에 대해 class (0,1)의 확률 값을 가지고 있는 vocabulary입니다.

 

Likelihoods $P(x_{i}|c)$는 아래 수식으로 계산됩니다.

 

$$P(x_{i}|c) = \frac {count(x_{i},c)+1} {\sum_{x \in V} (count(x,c)+1)} = \frac {count(x_{i},c)+k} {\sum_{x \in V} (count(x,c)) + k*V} $$

 

위 수식을 코드로 만들면 다음과 같습니다.

self.likelihoods[token] = {

            0:(token_dists[token][0] + self.k) / (class_counts[0] + len(self.w2i)*self.k),
            
            1:(token_dists[token][1] + self.k) / (class_counts[1] + len(self.w2i)*self.k),
            
            # k는 Smoothing을 위한 상수.
            
            # Laplace Smoothing
            
        }

 

 

4️⃣ 모델 학습 및 테스트

 

모델 객체를 만들고 학습 데이터로 학습시킵니다.

classifier = NaiveBayesClassifier(w2i)

classifier.train(train_tokenized, train_labels)

Test sample에 대한 결과는 다음과 같습니다.

preds = []

for test_tokens in tqdm(test_tokenized):

  pred = classifier.inference(test_tokens)
  
  preds.append(pred)
>>> preds


[1, 0, 1, 0] 긍정, 부정, 긍정, 부정

test_data = [
  "정말 좋았습니다. 또 가고 싶네요.", (긍정)
  "별로였습니다. 되도록 가지 마세요.", (부정)
  "다른 분들께도 추천드릴 수 있을 만큼 만족했습니다.",(긍정)
  "서비스가 좀 더 개선되었으면 좋겠습니다. 기분이 좀 나빴습니다."(부정)
]
Comments