실습_3. 나이브 베이즈 분류

Updated:

확률로 π 계산하기

원주율은 수학에서 가장 중요한 숫자 중 하나입니다. 원주율은 원 둘레와 지름의 비율로 3.141592653.14159265… 입니다.

2차원 평면에 점을 무작위로 찍어서 원주율을 구해보겠습니다. 원리는 원의 넓이를 구하는 것입니다.

반지름이 11인 원이 있다고 가정해보겠습니다. 이 원은 높이, 너비가 22인 사각형에 들어갑니다. 이 때 원의 넓이는 다음과 같이 구할 수 있습니다.

πr^2 = π

즉, 반지름이 11인 원의 넓이는 π 입니다.

우리가 무작위로 찍은 점 중 몇 %가 원 안에 들어가 있을지를 알게 된다면, 그 원의 넓이를 추측해 낼 수 있습니다. 예로, 75%의 점이 원 안에 있다면 그 원의 넓이는 0.75 \times 4 = 3.00.75×4=3.0 이 됩니다. 원의 넓이가 \piπ 라고 했으므로, \piπ 는 3.03.0이라고 추측할 수 있습니다.

N을 10배씩 증가시키며 실행 버튼을 눌러 파이 값이 어떻게 변경되는지 확인해보세요.

import matplotlib as mpl
mpl.use("Agg")
import matplotlib.pyplot as plt
import numpy as np

import elice_utils

def main():
    plt.figure(figsize=(5,5))
    
    X = []
    Y = []
    
    # N을 10배씩 증가할 때 파이 값이 어떻게 변경되는지 확인해보세요.
    N = 10000
    
    for i in range(N):
        X.append(np.random.rand() * 2 - 1)
        Y.append(np.random.rand() * 2 - 1)
    X = np.array(X)
    Y = np.array(Y)
    distance_from_zero = np.sqrt(X * X + Y * Y)
    is_inside_circle = distance_from_zero <= 1
    
    print("Estimated pi = %f" % (np.average(is_inside_circle) * 4))
    
    plt.scatter(X, Y, c=is_inside_circle)
    plt.savefig('circle.png')
    elice_utils.send_image('circle.png')

if __name__ == "__main__":
    main()

유방암 검사 키트

40대 여성이 mammogram(X-ray) 검사를 통해 유방암 양성 의심 판정을 받았을 때 유방암을 실제로 가지고 있을 확률은 어떻게 될까요?

mammogram_test() 함수를 구현하며 베이즈 법칙을 직접 응용해보겠습니다. mammogram_test() 함수는 세 가지 숫자를 입력 받습니다.

  • sensitivity - 검사의 민감성을 뜻합니다. 유방암 보유자를 대상으로 검사 결과가 양성이 표시될 확률입니다. 0부터 1 사이의 값을 갖습니다.
  • prior_prob - 총 인구를 기준으로 유방암을 가지고 있을 사전 확률(prior probability)입니다. 0.004 정도로 매우 낮은 값입니다.
  • false_alarm - 실제로는 암을 갖고 있지 않지만 유방암이라고 진단될 확률입니다. 0.1 정도로 생각보다 높은 값입니다.

나이브 법칙을 이용해 입력 받은 세 값을 바탕으로 유방암 보유 여부를 확률로 출력합니다.

>>> 0.8
>>> 0.004
>>> 0.1
3.11%
  • 실습

A=1A=1 은 Mammogram 검사를 통해 암으로 진단되는 경우, B = 1B=1 은 실제로 유방암을 가지고 있는 경우입니다.

sensitivity는 P(A=11B=1)P(A=11B=1)로 표현할 수 있습니다. 암을 실제로 가지고 있을 때 암으로 진단될 확률이 80%라면 P(A=11B=1) = 0.8P(A=11B=1)=0.8입니다.

일반적으로 유방암을 가지고 있을 확률은, 즉 prior_prob의 값은 매우 낮습니다: P(B=1) = 0.004P(B=1)=0.004

유방암을 가지고 있지 않을 확률은 1에서 prior_prob를 빼면 됩니다: P(B=0)=1 - P(B=1)=0.996P(B=0)=1−P(B=1)=0.996

실제로 암을 가지고 있지 않지만 암으로 진단되는 확률, false_alarm는 생각보다 매우 높습니다: P(A=11B=0)=0.1P(A=1∣B=0)=0.1

Mammogram 검사를 통해 암으로 진단되는 경우의 확률, P(A=1)P(A=1)를 구해보겠습니다: P(A=1) =P(A=1)= P(A=11B=0)P(B=0) +P(A=1∣B=0)P(B=0)+ P(A=11B=1)P(B=1) =P(A=11B=1)P(B=1)= 0.1 \times 0.996 + 0.8 \times 0.004 =0.1×0.996+0.8×0.004= 0.10280.1028.

def main():
    sensitivity = float(input())
    prior_prob = float(input())
    false_alarm = float(input())

    print("%.2lf%%" % (100 * mammogram_test(sensitivity, prior_prob, false_alarm)))

def mammogram_test(sensitivity, prior_prob, false_alarm):
    p_a1_b1 = sensitivity # p(A = 1 | B = 1)

    p_b1 = prior_prob    # p(B = 1)

    p_b0 = 1 - prior_prob    # p(B = 0)

    p_a1_b0 = false_alarm # p(A = 1|B = 0)

    p_a1 = p_a1_b0*p_b0 + p_a1_b1*p_b1   # p(A = 1)

    p_b1_a1 = p_a1_b1*p_b1/p_a1 # p(B = 1|A = 1)

    return p_b1_a1

if __name__ == "__main__":
    main()

나이브 베이즈 분류기

나이브 베이즈 분류기를 직접 구현해 보겠습니다. 두 개의 사탕 기계를 살펴보겠습니다.

  • 실습

위의 설명을 읽고, naive_bayes() 함수를 직접 구현해보세요.

이 함수는 두 개의 원소로 이루어진 리스트 하나를 출력합니다. 첫번째 원소는 P(M11test)P(M1∣test), 두 번째 숫자는 P(M21test)P(M2∣test)의 표준화된 값입니다. 즉, 두 값을 합은 1이 되어야 합니다.

import re
import math
import numpy as np

def main():
    M1 = {'r': 0.7, 'g': 0.2, 'b': 0.1} # M1 기계의 사탕 비율
    M2 = {'r': 0.3, 'g': 0.4, 'b': 0.3} # M2 기계의 사탕 비율
    
    test = {'r': 4, 'g': 3, 'b': 3}

    print(naive_bayes(M1, M2, test, 0.7, 0.3))

def naive_bayes(M1, M2, test, M1_prior, M2_prior):

    R1 = M1['r']**test['r']*M1['g']**test['g']*M1['b']**test['b']*M1_prior
    R2 = M2['r']**test['r']*M2['g']**test['g']*M2['b']**test['b']*M2_prior
    
    R1,R2 = R1/(R1+R2), R2/(R1+R2)
    
    return [R1,R2]

if __name__ == "__main__":
    main()

Bag-of-Words

Bag of Words 모델을 직접 구현하겠습니다.

create_BOW() 함수를 완성하세요. create_BOW()는 문장 한 줄이 입력 받고 해당 문장의 단어 사전과 Bag of Words 모델을 순서대로 리턴합니다.

>>> sentence = "Bag-of-Words 모델을 Python으로 직접 구현하겠습니다."
>>> create_bow(sentence)
{'bag': 1, 'of': 1, 'words': 1, '모델을': 1, 'Python으로': 1, '직접': 1, '구현하겠습니다': 1}
  • 실습

create_BOW() 함수의 조건은 다음과 같습니다.

  1. 단어는 공백(space)으로 나뉘어지며 모두 소문자로 치환되어야 합니다. .lower()을 사용하세요.
  2. 특수문자를 모두 제거합니다. remove_special_characters() 함수를 이용합니다. 정규식표현(re)를 사용하세요.
  3. 단어는 space 를 기준으로 잘라내어 만듭니다. split()을 사용하세요.
  4. 단어는 한 글자 이상이어야 합니다. 단어의 길이를 체크하기 위해 len()을 사용하세요.
import re

special_chars_remover = re.compile("[^\w'|_]")

def main():
    sentence = input()
    bow = create_BOW(sentence)

    print(bow)


def create_BOW(sentence):
    
    sentence = sentence.lower()
    
    sentence = remove_special_characters(sentence)
    
    sentence = sentence.split()
    
    print(sentence)
    
    bow = {}
    
    for s in sentence:
        if s in bow:
            bow[s] +=1
        else:
            bow[s] = 1
    
    print(bow)
    
    return bow


def remove_special_characters(sentence):
    return special_chars_remover.sub(' ', sentence)


if __name__ == "__main__":
    main()

네이버 영화평 감정분석

실제 영화 리뷰를 이용해 나이브 베이즈 분류기를 학습시키고, 입력 받은 영화평이 긍정 또는 부정적일 확률을 구하는 감정 분석(Sentiment Analysis)을 해보겠습니다.

데이터는 네이버 개발자 Lucy Park님의 Naver Sentiment Movie Corpus v1.0를 사용합니다. 네이버 영화의 140자 영화평을 모은 것으로 총 100,000개의 부정 리뷰, 100,000개의 긍정 리뷰로 구성되어 있습니다.

id, document, label은 각각 사용자 아이디(정수), 리뷰 본문(문자열), 긍정·부정(1·0)을 나타내며 탭(\t)으로 나누어져 있습니다.

id    document    label
9251303    와.. 연기가 진짜 개쩔구나.. 지루할거라고 생각했는데 몰입해서 봤다.. 그래 이런게 진짜 영화지    1
10067386    안개 자욱한 밤하늘에 떠 있는 초승달 같은 영화.    1
2190435    사랑을 해본사람이라면 처음부터 끝까지 웃을수 있는영화    1
  • 과제

강의를 보면서 차근차근히 따라오세요.

  1. 데이터 입력을 위해 read_data() 함수를 구현합니다. read_data()는 두 개의 문자열이 담긴 리스트를 리턴합니다. 첫번째 문자열은 모든 부정 리뷰의 집합, 두번째 문자열은 긍정 리뷰의 집합입니다. 리뷰 사이는 공백으로 나뉘어 집니다. 긴 문자열을 만드는 방법에 따라 속도 차이가 많이 나니 주의하세요!
["유치하고 촌스러운 애니메이션 도대체 왜 찍었는지 의문이 가는 영화 ㅎ 튼튼한 각본을 살리지 못한 그때의 기술력을 원망할뿐",
"안개 자욱한 밤하늘에 떠 있는 초승달 같은 영화. 사랑을 해본사람이라면 처음부터 끝까지 웃을수 있는영화"]
  • α 값은 0.1 을 사용합니다.
  • P(pos) = 0.5P(pos)=0.5 및
  • P(neg) = 0.5P(neg)=0.5 를 사용하겠습니다.
  1. create_BOW() 함수를 작성합니다.

  2. calculate_doc_prob() 함수를 작성합니다. 이 함수는 training_sentence(문자열), testing_sentence(문자열), 그리고 alpha 값을 입력 받아, training_sentence로 학습된 된 모델에서 testing_sentence를 만들어 낼 로그 확률을 반환합니다. 트레이닝 모델에 없는 단어가 나올 경우, 확률은 alphaalpha / 전체 토큰의 개수로 합니다.

  3. normalize_log_prob() 함수는 입력 받은 두 개의 로그 확률값을 표준화하여 로그값이 아닌 실수값으로 변환합니다. training_sentence가 부정적 문장들로 트레이닝될 모델, 그리고 긍정적 문장들로 트레이닝된 모델에서 만들어질 확률값을 구합니다.

  4. 마지막으로 [부정적일 확률, 긍정적일 확률] 형태의 리스트를 변수 prob_pair에 저장합니다.

import io
import numpy
import matplotlib as mpl
mpl.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import re
import math
import elice_utils

special_chars_remover = re.compile("[^\w'|_]")
def remove_special_characters(sentence):
    return special_chars_remover.sub(' ', sentence)

def main():
    training_sentences = read_data()
    testing_sentence = "마음이 따뜻해지는 최고의 영화"
    prob_pair = naive_bayes(training_sentences, testing_sentence)
    
    plot_title = testing_sentence
    if len(plot_title) > 50: plot_title = plot_title[:50] + "..."
    visualize_boxplot(plot_title,
                  list(prob_pair),
                  ['Negative', 'Positive'])

def naive_bayes(training_sentences, testing_sentence):
    log_prob_negative = calculate_doc_prob(training_sentences[0], testing_sentence, 0.1) + math.log(0.5)
    log_prob_positive = calculate_doc_prob(training_sentences[1], testing_sentence, 0.1) + math.log(0.5)
    prob_pair = normalize_log_prob(log_prob_negative, log_prob_positive)
    
    return prob_pair

def read_data():
    training_sentences = [[], []]
    
    '''
    숙제 1
    여기서 파일을 읽어 training_sentences에 저장합니다.
    '''
    
    with open("./ratings.txt",mode='r') as file:
        next(file)
        for f in file:
            arr = f.split('\t')
            if arr[-1] == '0\n':
                training_sentences[0].append(arr[1])
            elif arr[-1] == '1\n':
                training_sentences[1].append(arr[1])
    
    return [' '.join(training_sentences[0]), ' '.join(training_sentences[1])]

def normalize_log_prob(prob1, prob2):
    
    '''
    숙제 4
    로그로 된 확률값을 표준화합니다.
    이 부분은 이미 작성되어 있습니다.
    '''
    
    maxprob = max(prob1, prob2)

    prob1 -= maxprob
    prob2 -= maxprob
    prob1 = math.exp(prob1)
    prob2 = math.exp(prob2)
    
    #print(prob1, prob2)

    normalize_constant = 1.0 / float(prob1 + prob2)
    prob1 *= normalize_constant
    prob2 *= normalize_constant

    return (prob1, prob2)

def calculate_doc_prob(training_sentence, testing_sentence, alpha):
    logprob = 0

    training_model = create_BOW(training_sentence)
    testing_model = create_BOW(testing_sentence)

    '''
    숙제 3
    training_sentence로 만들어진 모델이,
    testing_sentence를 만들어 낼 **로그 확률** 을 구합니다.
    일반 숫자에서 로그값을 만들기 위해서는 math.log() 를 사용합니다.
    
    일반 숫자에서의 곱셈이 로그에서는 덧셈, 나눗셈은 뺄셈이 된다는 점에 유의하세요.
    예) 3 * 5 = 15
        log(3) + log(5) = log(15)
        
        5 / 2 = 2.5
        log(5) - log(2) = log(2.5)
    '''
    
    Sum = sum(training_model.values())
    #print(training_model['영화']/Sum)
    
    
    for i in testing_model:
        #print(i)
        if i in training_model:
            #print(training_model[i]/Sum)
            logprob+=math.log(training_model[i]/Sum)
        else:
            #print(alpha/Sum)
            logprob+=math.log(alpha/Sum)
    #print(logprob)
    return logprob

def create_BOW(sentence):
    bow = {}
    
    '''
    숙제 2
    이전 실습과 동일하게 bag of words를 만듭니다.
    '''
    sentence = sentence.lower()
    
    sentence = remove_special_characters(sentence)
    
    sentence = sentence.split()
    
    for s in sentence:
        if s in bow:
            bow[s] +=1
        else:
            bow[s] = 1

    return bow

'''
이 밑의 코드는 시각화를 위한 코드입니다.
궁금하다면 살펴보세요.
'''
def visualize_boxplot(title, values, labels):
    width = .35

    print(title)
    
    fig, ax = plt.subplots()
    ind = numpy.arange(len(values))
    rects = ax.bar(ind, values, width)
    ax.bar(ind, values, width=width)
    ax.set_xticks(ind + width/2)
    ax.set_xticklabels(labels)

    def autolabel(rects):
        # ach some text labels
        for rect in rects:
            height = rect.get_height()
            ax.text(rect.get_x()+rect.get_width()/2., height + 0.01, '%.2lf%%' % (height * 100), ha='center', va='bottom')

    autolabel(rects)

    plt.savefig("image.svg", format="svg")
    elice_utils.send_image("image.svg")

if __name__ == "__main__":
    main()

Leave a comment