본문 바로가기
머신러닝/NLP(자연어처리, Natural language processing)

자연어 처리 - 나이브베이즈 분류기

by 미생22 2024. 5. 30.
728x90

나이브베이즈 분류기는 Naive Bayes Classifier라고 합니다. 나이브한 베이즈라는 사람이 만든 분류기라고...하네요

모든 특성을 독립이라고 보고 분류하는 것이고 1950년대 만들어진 오래된 분류기지만 아직도 잘쓰이고 있습니다.

 

나이브 베이즈 분류기는 베이즈 이론을 기반으로 하며, 입력 데이터에 대한 각 클래스의 조건부 확률을 계산하여 가장 높은 확률을 가진 클래스로 분류합니다. 이를 표현하는 수식은 다음과 같습니다:

 

  •  는 입력 데이터  이 주어졌을 때 클래스  가 발생할 조건부 확률을 나타냅니다.
  •  는 클래스  에 속하는 데이터에서  을 관찰할 확률, 즉 likelihood 입니다.
  •  는 클래스  가 발생할 사전 확률을 나타냅니다.
  •  는 주어진 데이터  에 대한 증거 확률로, 모든 클래스에 대해 likelihood 와 prior 확률을 곱하여 총합한 값입니다.
나이브 베이즈 분류기에서 '나이브'라는 용어는 각 특성(또는 변수)이 서로 독립이라는 가정을 나타냅니다.

 

 

우선 들어가기 전에 저번시간부터 계속 쓴 token이 뭔지, 토큰화가 뭔지 알아보겠습니다.

 

우리가 이번에 쓸 모듈 nltk.tokenize는 NLTK(Natural Language Toolkit) 라이브러리의 모듈 중 하나로, 텍스트 데이터를 토큰화하는 도구들을 제공합니다. 토큰화란 문장을 단어, 구두점, 숫자 등의 개별 단위로 분리하는 작업을 의미합니다. nltk.tokenize 모듈에는 다양한 토큰화 도구들이 포함되어 있어 사용자가 다양한 요구에 맞게 텍스트를 토큰화할 수 있습니다.

 

1. nltk.tokenize 도구

주요 nltk.tokenize 도구들은 다음과 같습니다:

 

1. word_tokenize: 문장을 단어 단위로 토큰화합니다.

from nltk.tokenize import word_tokenize
text = "Hello, world! This is a test."
tokens = word_tokenize(text)
print(tokens)

Output: ['Hello', ',', 'world', '!', 'This', 'is', 'a', 'test', '.']

 

2. sent_tokenize: 텍스트를 문장 단위로 토큰화합니다.

from nltk.tokenize import sent_tokenize
text = "Hello, world! This is a test. Let's see how it works."
sentences = sent_tokenize(text)
print(sentences)

Output: ['Hello, world!', 'This is a test.', "Let's see how it works."]

 

3. RegexpTokenizer: 정규표현식을 사용하여 맞춤형 토큰화를 수행합니다.

from nltk.tokenize import RegexpTokenizer
tokenizer = RegexpTokenizer(r'\w+')
tokens = tokenizer.tokenize("Hello, world! This is a test.")
print(tokens)

Output: ['Hello', 'world', 'This', 'is', 'a', 'test']

 

4. TweetTokenizer: 트위터와 같은 소셜 미디어 텍스트를 토큰화하는 데 특화된 토크나이저입니다.

from nltk.tokenize import TweetTokenizer
tknzr = TweetTokenizer()
tokens = tknzr.tokenize("Hello, world! This is a #test :)")
print(tokens)

Output: ['Hello', ',', 'world', '!', 'This', 'is', 'a', '#test', ':)']

 

5. TreebankWordTokenizer: Penn Treebank에서 사용되는 표준에 따라 단어를 토큰화합니다.

from nltk.tokenize import TreebankWordTokenizer
tokenizer = TreebankWordTokenizer()
tokens = tokenizer.tokenize("This is a test.")
print(tokens)

Output: ['This', 'is', 'a', 'test', '.']

 

이 외에도 NLTK는 텍스트 토큰화를 위한 다양한 도구들을 제공합니다. 

 

2. 나이브 베이즈 분류기 - 영어버전

from nltk.tokenize import word_tokenize
import nltk

 

nltk에서 tokenize를 가져와서 word_tokenize 함수를 가져옵니다.

당연히 nltk도 가져옵니다.

 

이제 train이라는 문장을 완성할겁니다. 그리고 나이브 베이즈는 분류기니까 label이 있어야 합니다. pos나 neg를 줘서 이게 긍정적인지 부정적인지 알아야 합니다.

 

train = [
    ('i like you', 'pos'),
    ('i hate you', 'neg'),
    ('you like me', 'neg'),
    ('i like her', 'pos')
]

 

 

이제 말뭉치를 만들겁니다. 말뭉치는 all_words라고 하겠습니다.

이후에도 말뭉치라는 말을 계속 할건데, 말뭉치는 train에 나온 단어들을 set로 만든 상태를 말합니다. 즉 나왔던 단어들의 집합이 되겠네요.

 

all_words = set(
    word.lower() for sentence in train for word in word_tokenize(sentence[0])
)

 

train에 4개를 각각 sentence라고 하고 그 sentence에서 0번째 즉 pos나 neg가 아닌 문장들에서 단어(word)를 가져와서 소문자화도 시킨 뒤 set()시켜줍니다.

 

이렇게하면 all_words는 {'hate', 'her', 'i', 'like', 'me', 'you'}가 나오게 됩니다.

 

스니펫이 처음이라 어색한 경우에는 아래 코드를 통해 확인하면 좋습니다.

sentence = train[0]
word_tokenize(sentence[0])

['i', 'like', 'you']

 

train의 첫번째 문장에서 첫번째것, 즉 pos없이 문장만 가져와서 tokenize한 겁니다.

이렇게 set시킨 것이 말뭉치입니다.

이 말뭉치의 의미는, 썼다 안썼다하면 train에 있는 모든 문장을 만들 수 있습니다.

 

이 말뭉치에서 각 sentence에 있으면 True, 없으면 False를 엮어줍니다.

True나 False를 반환하기 위해서는 in을 쓰면 되겠네요.

말뭉치에서 word를 가져와서, train에 있는 sentence들의 [0]번째에 있는것을 word_tokenize시킨 것에 들어있느냐.라는 식을 적용시키려고 합니다.

 

t = [{word : (word in word_tokenize(x[0])) for word in all_words}, x[1] for x in train]
t

[({'her': False,
   'i': True,
   'like': True,
   'hate': False,
   'you': True,
   'me': False},
  'pos'),
 ({'her': False,
   'i': True,
   'like': False,
   'hate': True,
   'you': True,
   'me': False},
  'neg'),
 ({'her': False,
   'i': False,
   'like': True,
   'hate': False,
   'you': True,
   'me': True},
  'neg'),
 ({'her': True,
   'i': True,
   'like': True,
   'hate': False,
   'you': False,
   'me': False},
  'pos')]

 

그러면 이렇게 말뭉치에서 문장에 있냐 없냐를 {}로 묶어 True/False로 반환하고 pos인지, neg인지 ()로 묶어서 전체 list[]안에 담아줄 수 있습니다.

 

you가 있을때와 없을때는 pos일수도 neg일수도 있습니다.

이래서 각 특성을 독립적으로 보는 나이브베이즈가 좋은겁니다.

이제 나이브 베이즈에 넣을 데이터들을 준비했습니다.

 

우리가 머신러닝을 배울때는 fit()함수를 썼었죠. nltk에서는 fit()이 아니고 train()이라는 함수를 씁니다.

있다 없다를 나이브 베이즈 분류기에 집어넣어줄겁니다.

 

classifier = nltk.NaiveBayesClassifier.train(t)
classifier.show_most_informative_features()

 

이렇게 훈련시킨 classifier한테 니가 봤을때 가장 중요한 특성을 확인해보라고 시킵니다.

Most Informative Features
                    hate = False             pos : neg    =      1.7 : 1.0
                     her = False             neg : pos    =      1.7 : 1.0
                       i = True              pos : neg    =      1.7 : 1.0
                    like = True              pos : neg    =      1.7 : 1.0
                      me = False             pos : neg    =      1.7 : 1.0
                     you = True              neg : pos    =      1.7 : 1.0

 

네, hate가 없다는 것이 1.7대 1의 확률로 pos이고, her가 없다는 것이 1.7대 1의 확률로 neg하고, ... 이렇게 분류하는 겁니다.

 

자, 이제 테스트해보겠습니다. 새로운 문장을 하나 가지고옵니다. 그리고 말뭉치에서 있다없다를 판별해줍니다.

 

test_sentence = 'i like MeRui'
test_sent_features = {
    word.lower() : (word in word_tokenize(test_sentence.lower())) for word in all_words
}

이렇게하면 test_sent_features는 

{'her': False,
 'i': True,
 'like': True,
 'hate': False,
 'you': False,
 'me': False}

 

이렇게 나오게 됩니다. 이걸로  pos인지 neg인지 확인할 수 있다는 것이죠. 이제 predict..가 아닌 classify를 시켜보겠습니다. scikit learn에선 predict 였지만, nltk에서는 classify라는 명령어를 씁니다.

 

classifier.classify(test_sent_features)

'pos'

i like MeRui는 pos가 나왔네요. 이렇게 한걸 우리는 감성분석이라고 합니다. 즉 pos, neg한 문장들을 나열해주고, 새로운 문장이 어떤 느낌인지 예측하는 것이죠.

 

3. 나이브 베이즈 분류기 - 한글버전

영어버전과 한글버전이 나누어져 있는 이유는, 한글은 형태소 분석부터 이루어져야 하기 때문입니다. konlpy에서 형태소 분석을 Okt()의 .pos()함수를 통해 하는것이 좋습니다.

 

3-1. 형태소 분석을 안하고 그냥 해보겠습니다.

Twitter? 아니죠, Okt()를 import 시킵니다.

from konlpy.tag import Okt

pos_tagger = Okt()

 

이번에는 Okt()를 t라고 안하고 pos_tagger라고 instanciation 시켰습니다.

 

train = [
    ("메리가 좋아", 'pos'),
    ("고양이도 좋아", 'pos'),
    ("난 수업이 지루해", 'neg'),
    ("메리는 이쁜 고양이야", 'pos'),
    ("난 마치고 메리랑 놀거야", 'pos')
]

 

네 이렇게 train 데이터를 입력해놓구요, 있다없다를 하기위해선 우선 말뭉치를 만들어야겠죠?

 

all_words = set(
	word for sentence in train for word in word_tokenize(sentence[0])
)

{'고양이도',
 '고양이야',
 '난',
 '놀거야',
 '마치고',
 '메리가',
 '메리는',
 '메리랑',
 '수업이',
 '이쁜',
 '좋아',
 '지루해'}

 

all_words는 이렇게 나오네요.

형태소 분석을 안하고 word_tokenize를 하니, 메리가, 메리는, 메리랑이 다 다른 단어가 되었습니다. 영어는 형태소가 없기 때문에 with merry, Merry is 이런식으로 나뉘겠지만... 한글을 그렇지 않습니다.
이번에도 형태소 분석없이 한번 있다없다를 나눠보겠습니다.

 

t = [({word : (word in word_tokenize(sentence[0])) for word in all_words}, sentence[1]) for sentence in train]
t

[({'놀거야': False,
   '이쁜': False,
   '고양이도': False,
   '메리랑': False,
   '난': False,
   '지루해': False,
   '마치고': False,
   '메리가': True,
   '수업이': False,
   '고양이야': False,
   '좋아': True,
   '메리는': False},
  'pos'),
 ({'놀거야': False,
   '이쁜': False,
   '고양이도': True,
   '메리랑': False,
   '난': False,
   '지루해': False,
   '마치고': False,
   '메리가': False,
   '수업이': False,
   '고양이야': False,
   '좋아': True,
   '메리는': False},
  'pos'),
 ({'놀거야': False,
   '이쁜': False,
   '고양이도': False,
   '메리랑': False,
   '난': True,
   '지루해': True,
   '마치고': False,
   '메리가': False,
   '수업이': True,
   '고양이야': False,
   '좋아': False,
   '메리는': False},
  'neg'),
 ({'놀거야': False,
   '이쁜': True,
   '고양이도': False,
   '메리랑': False,
   '난': False,
   '지루해': False,
...
   '수업이': False,
   '고양이야': False,
   '좋아': False,
   '메리는': False},
  'pos')]

 

이렇게 있다없다를 완료했습니다... 이 상태에서 학습(train)시켜보겠습니다.

 

classifier = nltk.NaiveBayesClassifier.train(t)
classifier.show_most_informative_features()

Most Informative Features
                       난 = True              neg : pos    =      2.5 : 1.0
                      좋아 = False             neg : pos    =      1.5 : 1.0
                    고양이도 = False             neg : pos    =      1.1 : 1.0
                    고양이야 = False             neg : pos    =      1.1 : 1.0
                     놀거야 = False             neg : pos    =      1.1 : 1.0
                     마치고 = False             neg : pos    =      1.1 : 1.0
                     메리가 = False             neg : pos    =      1.1 : 1.0
                     메리는 = False             neg : pos    =      1.1 : 1.0
                     메리랑 = False             neg : pos    =      1.1 : 1.0
                      이쁜 = False             neg : pos    =      1.1 : 1.0

 

벌써부터 이상해보이죠
test sentence를 가지고 돌려보겠습니다.

말뭉치(all_words)에서 있다없다를 해야합니다.

 

test_sentence = '난 수업이 마치면 메리랑 놀거야'

test_sent_features = {
    word : word in word_tokenize(test_sentence) for word in all_words
}
test_sent_features

{'놀거야': True,
 '이쁜': False,
 '고양이도': False,
 '메리랑': True,
 '난': True,
 '지루해': False,
 '마치고': False,
 '메리가': False,
 '수업이': True,
 '고양이야': False,
 '좋아': False,
 '메리는': False}

 

이제 classify()시켜보겠습니다.

 

classifier.classify(test_sent_features)

'neg'

 

감성분석 결과 부정적이라고 나왔습니다. 분명히 긍정적인 문장인데 왜 그럴까요?

이래서 한글은 형태소 분석이 필수입니다. 너가, 너라서, 너는, 이렇게 띄어쓰지 않았는데도 작은 변화만으로도 느낌이 달라지는 거죠.

 

 

 

3-1. 형태소 분석을 하고 돌려보겠습니다.

def tokenize(doc):
    return ['/'.join(t) for t in pos_tagger.pos(doc, norm=True, stem=True)]

 

Okt()인 pos_tagger에서 .pos()함수를 써 doc을 조사를 분류한 뒤 이렇게 생긴 조사를 뒤에 붙여넣는 함수입니다.

형태소 분석을 한 후 품사를 단어 뒤에 붙여넣는 방법으로, 결론적으로 원하는 형태는 '메리/Noun', '가/Josa' 와 같은 형태가 됩니다.

 

train_docs = [(tokenize(row[0]), row[1]) for row in train]
train_docs

 

[(['메리/Noun', '가/Josa', '좋다/Adjective'], 'pos'),
 (['고양이/Noun', '도/Josa', '좋다/Adjective'], 'pos'),
 (['난/Noun', '수업/Noun', '이/Josa', '지루하다/Adjective'], 'neg'),
 (['메리/Noun', '는/Josa', '이쁘다/Adjective', '고양이/Noun', '야/Josa'], 'pos'),
 (['난/Noun', '마치/Noun', '고/Josa', '메리/Noun', '랑/Josa', '놀다/Verb'], 'pos')]

 

이렇게하면 train에서 앞의 부분만 형태소 분석을 마치게 됩니다.

참고로 어떻게 조사가 뒤에 붙는지 궁금하면 이 코드를 살펴보면 됩니다.

 

pos_tagger.pos(train[0][0])

[('메리', 'Noun'), ('가', 'Josa'), ('좋아', 'Adjective')]

 

pos를 시키면 tuple 형태로 나오고,

'/'.join(('메리', 'Noun'))

'메리/Noun'

 

join 함수를 통해 tuple을 /로 붙여버릴 수 있는거죠.

 

tokens = [t for d in train_docs for t in d[0]]
tokens

['메리/Noun',
 '가/Josa',
 '좋다/Adjective',
 '고양이/Noun',
 '도/Josa',
 '좋다/Adjective',
 '난/Noun',
 '수업/Noun',
 '이/Josa',
 '지루하다/Adjective',
 '메리/Noun',
 '는/Josa',
 '이쁘다/Adjective',
 '고양이/Noun',
 '야/Josa',
 '난/Noun',
 '마치/Noun',
 '고/Josa',
 '메리/Noun',
 '랑/Josa',
 '놀다/Verb']

 

pos, neg를 제외한 앞의 부분을 모두 모아봅니다. 이렇게 말뭉치를 만들었습니다.

 

def term_exist(doc):
    return {word : (word in set(doc)) for word in tokens}

이제 tokens에서 가져온 단어들이 doc에 있는지 없는지를 표시하는 함수를 만듭니다.

tokens가 all_words와 같은 맥락인거죠. doc은 문장의 형태소분석은 마친 상태이겠구요.

 

train_xy = [(term_exist(d), c) for d, c in train_docs]
train_xy

 

있다없다 함수를 가지고 train_docs에서 d는 문장의 형태소 분석한 상태, c는 pos/neg인데 이걸 들고와서 train_xy에 담아줍니다. 이제 완벽하게 있다없다가 끝났네요. 이제 나이브 베이즈 분류기에 train 시켜보겠습니다. 그리고 중요한 feature를 보이라는 함수를 통해 어느정도의 neg/pos를 보이는지 확인해보겠습니다.

 

classifier = nltk.NaiveBayesClassifier.train(train_xy)
classifier.show_most_informative_features()

Most Informative Features
                  난/Noun = True              neg : pos    =      2.5 : 1.0
                 메리/Noun = False             neg : pos    =      2.5 : 1.0
                고양이/Noun = False             neg : pos    =      1.5 : 1.0
            좋다/Adjective = False             neg : pos    =      1.5 : 1.0
                  가/Josa = False             neg : pos    =      1.1 : 1.0
                  고/Josa = False             neg : pos    =      1.1 : 1.0
                 놀다/Verb = False             neg : pos    =      1.1 : 1.0
                  는/Josa = False             neg : pos    =      1.1 : 1.0
                  도/Josa = False             neg : pos    =      1.1 : 1.0
                  랑/Josa = False             neg : pos    =      1.1 : 1.0

 

이제 새로운 문장을 들고와서 classify 시켜볼까요?

test_sentence = '난 수업이 마치면 메리랑 놀거야'

test_docs = pos_tagger.pos(test_sentence)
test_docs

[('난', 'Noun'),
 ('수업', 'Noun'),
 ('이', 'Josa'),
 ('마치', 'Noun'),
 ('면', 'Josa'),
 ('메리', 'Noun'),
 ('랑', 'Josa'),
 ('놀거야', 'Verb')]

 

네, 새로운 문장을 들고와서 형태소 분석을 하고 말뭉치를 만들었습니다. 이번에는 /로 묶지않고 이상태에서 바로 있다없다를 만들어 classify 시켜볼게요.

 

test_sent_features = {word : (word in tokens) for word in test_docs}
test_sent_features

{('난', 'Noun'): False,
 ('수업', 'Noun'): False,
 ('이', 'Josa'): False,
 ('마치', 'Noun'): False,
 ('면', 'Josa'): False,
 ('메리', 'Noun'): False,
 ('랑', 'Josa'): False,
 ('놀거야', 'Verb'): False}

 

classifier.classify(test_sent_features)

'pos'

 

네 긍정적인 문장이라고 하네요 :D

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

728x90