나이브 베이즈(Naive Bayes) — Multinomial, Gaussian, Bernoulli

"순진한(naive)" 가정은 틀렸습니다. 그런데도 잘 작동합니다. 그것이 이 방법의 아름다움입니다.

유형: Build 언어: Python 선수 지식: Phase 2, Lessons 01-07 (분류(classification), 베이즈 정리(Bayes' theorem)) 소요 시간: 약 75분

학습 목표

  • 텍스트 분류(text classification)를 위해 라플라스 스무딩(Laplace smoothing)을 적용한 다항 나이브 베이즈(Multinomial Naive Bayes)를 처음부터 구현합니다.
  • 순진한 독립 가정(naive independence assumption)이 수학적으로 틀렸음에도 실제로는 올바른 클래스 순위(class ranking)를 만들어 내는 이유를 설명합니다.
  • Multinomial, Bernoulli, Gaussian Naive Bayes 변형을 비교하고 특성 유형(feature type)에 맞는 변형을 선택합니다.
  • 고차원의 희소 데이터(sparse data)에서 나이브 베이즈(Naive Bayes)와 로지스틱 회귀(logistic regression)를 비교하고, 그 뒤에 작동하는 편향-분산 균형(bias-variance tradeoff)을 설명합니다.

문제

텍스트를 분류해야 하는 상황이 있습니다. 이메일을 스팸(spam)과 비스팸(not-spam)으로 나누거나, 고객 리뷰를 긍정과 부정으로 나누거나, 고객 지원 티켓(support ticket)을 카테고리(category)별로 분류하는 작업이 대표적입니다. 특성(feature)은 수천 개, 즉 단어당 하나씩 있고, 학습 데이터(training data)는 제한되어 있습니다.

많은 분류기는 이런 상황에서 어려움을 겪습니다. 로지스틱 회귀(logistic regression)는 수천 개의 가중치(weight)를 안정적으로 추정하려면 충분한 표본(sample)이 필요합니다. 의사결정 트리(decision tree)는 한 번에 단어 하나씩 분할(split)하면서 심하게 과대적합(overfitting)됩니다. 10,000차원의 K-최근접 이웃(KNN)은 모든 점(point)이 서로 비슷하게 멀어져 의미를 잃습니다.

나이브 베이즈는 이 상황을 잘 처리합니다. 수학적으로 틀린 가정, 즉 클래스가 주어지면 모든 특성이 서로 독립이라는 가정을 두지만, 특히 학습 데이터가 적은 텍스트 분류에서는 더 "똑똑한" 모델보다 잘 작동하는 경우가 많습니다. 데이터를 한 번만 훑으면(single pass) 학습이 끝나고, 수백만 개의 특성까지 확장되며, 보정(calibration)은 약하지만 확률 추정값(probability estimate)도 제공합니다.

틀린 가정이 좋은 예측으로 이어지는 이유를 이해하면 머신러닝(machine learning)의 중요한 사실을 배울 수 있습니다. 최고의 모델은 가장 "정확한" 모델이 아니라, 주어진 데이터에서 편향-분산 균형이 가장 좋은 모델이라는 점입니다.

사전 테스트

2문제 · 이 강의를 시작하기 전에 얼마나 알고 있는지 확인해보세요

1.나이브 베이즈(Naive Bayes)에서 '나이브(naive)'하다고 부르는 가정은 무엇인가요?

2.라플라스 스무딩(Laplace smoothing)은 나이브 베이즈(Naive Bayes)에서 무엇을 방지하나요?

0/2 답변 완료

개념

베이즈 정리(Bayes' Theorem) 빠른 복습

베이즈 정리는 조건부 확률(conditional probability)을 뒤집습니다.

P(class | features) = P(features | class) * P(class) / P(features)

우리가 알고 싶은 것은 P(class | features)입니다. 즉, 문서 안의 단어가 주어졌을 때 그 문서가 어떤 클래스에 속할 확률입니다. 이는 다음 값들로 계산할 수 있습니다.

  • P(features | class): 이 클래스의 문서에서 이런 단어들을 볼 가능도(likelihood)
  • P(class): 클래스의 사전 확률(prior probability). 예를 들어 스팸이 일반적으로 얼마나 흔한지를 나타냅니다.
  • P(features): 증거(evidence). 모든 클래스 비교에서 같은 값이므로 무시할 수 있습니다.

가장 높은 P(class | features)를 가진 클래스가 선택됩니다.

순진한 독립 가정(Naive Independence Assumption)

P(features | class)를 정확히 계산하려면 모든 특성 조합의 결합 확률(joint probability)을 추정해야 합니다. 어휘(vocabulary)가 10,000개라면 2^10,000개의 가능한 조합에 대한 분포(distribution)를 추정해야 합니다. 사실상 불가능합니다.

순진한 가정(naive assumption)은 클래스가 주어지면 모든 특성이 조건부 독립(conditional independence)이라고 봅니다.

P(w1, w2, ..., wn | class) = P(w1 | class) * P(w2 | class) * ... * P(wn | class)

불가능한 하나의 결합 분포 대신, 특성별 단순 분포 n개를 추정하는 방식으로 바뀝니다. 각 분포에는 빈도(count)만 있으면 됩니다.

이 가정은 명백히 틀렸습니다. 문서 안에서 "machine"과 "learning"은 독립이 아닙니다. 그러나 분류기(classifier)에 필요한 것은 정확한 확률 추정값이 아니라 올바른 순위입니다. 독립 가정은 체계적인 오차를 만들어 내지만, 그 오차가 모든 클래스에 비슷하게 작용하면 순위는 그대로 유지됩니다.

그래도 작동하는 이유

크게 세 가지 이유가 있습니다.

  1. 보정(calibration)보다 순위가 중요합니다. 분류는 최상위 순위(top-ranked) 클래스가 맞으면 됩니다. 실제 P(spam)이 0.7인데 모델이 0.99999로 추정해도 스팸을 고르기만 하면 분류 결과는 맞습니다. 정확한 확률보다 올바른 승자(winner)가 중요합니다.

  2. 높은 편향, 낮은 분산입니다. 독립 가정은 강한 사전(prior)입니다. 모델을 강하게 제약해 과대적합을 막아 줍니다. 학습 데이터가 적을 때는 이론적으로 더 정확하지만 불안정한 모델보다, 조금 틀려도 안정적인 모델이 더 낫습니다. 이것이 바로 편향-분산 균형이 실제로 작동하는 모습입니다.

  3. 특성 중복(feature redundancy)이 상쇄됩니다. 상관된 특성(correlated feature)은 중복된 증거를 제공합니다. 분류기가 그 증거를 이중으로 셈(double-count)하더라도, 올바른 클래스에 대해서도 같은 방식으로 이중으로 셉니다. "machine"과 "learning"이 항상 함께 나오고 둘 다 "tech" 클래스의 증거라면, 나이브 베이즈가 둘을 두 번 세더라도 올바른 클래스를 두 번 강화하는 셈입니다.

네 번째로, 현실적인 이유도 있습니다. 나이브 베이즈는 매우 빠릅니다. 학습은 빈도를 세기 위해 데이터를 한 번 훑는 것이 전부이고, 예측은 행렬 곱(matrix multiplication)으로 끝납니다. 백만 개 문서도 몇 초 안에 학습할 수 있을 정도입니다. 이런 속도 덕분에 더 빠르게 반복 실험하고, 더 많은 특성 조합과 실험을 시도해 볼 수 있습니다.

수학을 단계별로 보기

클래스가 스팸(spam)과 비스팸(not-spam) 두 개라고 가정해 봅시다. 어휘(vocabulary)에는 "free", "money", "meeting" 세 단어가 있습니다.

학습 데이터:

  • 스팸 이메일에는 "free"가 80회, "money"가 60회, "meeting"이 10회 등장합니다. 총 150단어입니다.
  • 비스팸 이메일에는 "free"가 5회, "money"가 10회, "meeting"이 100회 등장합니다. 총 115단어입니다.
  • 전체 이메일 중 40%가 스팸이고 60%가 비스팸입니다.

라플라스 스무딩(Laplace smoothing, alpha=1)을 적용하면 다음과 같습니다.

P(free | spam)    = (80 + 1) / (150 + 3) = 81/153 = 0.529
P(money | spam)   = (60 + 1) / (150 + 3) = 61/153 = 0.399
P(meeting | spam) = (10 + 1) / (150 + 3) = 11/153 = 0.072

P(free | not-spam)    = (5 + 1) / (115 + 3) = 6/118 = 0.051
P(money | not-spam)   = (10 + 1) / (115 + 3) = 11/118 = 0.093
P(meeting | not-spam) = (100 + 1) / (115 + 3) = 101/118 = 0.856

새 이메일에는 "free"가 2번, "money"가 1번, "meeting"이 0번 들어 있습니다.

log P(spam | email) = log(0.4) + 2*log(0.529) + 1*log(0.399) + 0*log(0.072)
                    = -0.916 + 2*(-0.637) + (-0.919) + 0
                    = -3.109

log P(not-spam | email) = log(0.6) + 2*log(0.051) + 1*log(0.093) + 0*log(0.856)
                        = -0.511 + 2*(-2.976) + (-2.375) + 0
                        = -8.838

스팸이 큰 차이로 이깁니다. "free"가 두 번 등장한 것은 스팸에 대한 강한 증거입니다. 다항 나이브 베이즈(Multinomial NB)에서는 등장하지 않은 "meeting"이 로그 합(log sum)에 0 * log(P) 형태로 더해지므로 영향을 주지 않는다는 점에 주의하세요. 단어의 부재(absence)를 명시적으로 모델링하는 것은 베르누이 나이브 베이즈(Bernoulli NB)입니다.

세 가지 변형

나이브 베이즈에는 세 가지 주요 변형이 있습니다. 각각 P(feature | class)를 서로 다르게 모델링합니다.

다항 나이브 베이즈(Multinomial Naive Bayes)

각 특성을 빈도(count)로 봅니다. 단어 빈도(word frequency)나 TF-IDF 값처럼 텍스트 데이터에 가장 적합합니다.

P(word_i | class) = (count of word_i in class + alpha) / (total words in class + alpha * vocab_size)

alpha는 라플라스 스무딩 값입니다. 이 변형은 텍스트 분류의 핵심 기준 모델(baseline)로 사용됩니다.

가우시안 나이브 베이즈(Gaussian Naive Bayes)

각 특성을 정규분포(normal distribution)로 봅니다. 연속형 특성에 적합합니다.

P(x_i | class) = (1 / sqrt(2 * pi * var)) * exp(-(x_i - mean)^2 / (2 * var))

각 클래스는 특성별로 자신의 평균(mean)과 분산(variance)을 가집니다. 클래스 안에서 특성이 실제로 종 모양(bell curve) 분포에 가까울 때 잘 작동합니다.

베르누이 나이브 베이즈(Bernoulli Naive Bayes)

각 특성을 이진값(binary), 즉 등장(present)/비등장(absent)으로 봅니다. 짧은 텍스트나 이진 특성 벡터(binary feature vector)에 적합합니다.

P(word_i | class) = (docs in class containing word_i + alpha) / (total docs in class + 2 * alpha)

다항(Multinomial)과 달리 베르누이(Bernoulli)는 단어가 없는 것도 명시적으로 모델링합니다. "free"가 보통 스팸에 등장하는데 이번 이메일에는 없다면, 베르누이는 그것을 스팸에 반대되는 증거로 셉니다.

어떤 변형을 언제 쓸까

변형특성 유형적합한 경우예시
다항(Multinomial)빈도(count) 또는 도수(frequency)텍스트 분류, 단어 가방(bag-of-words)이메일 스팸, 주제 분류
가우시안(Gaussian)연속값(continuous)대체로 정규분포에 가까운 표 형식(tabular) 특성붓꽃(Iris) 분류, 센서 데이터
베르누이(Bernoulli)이진(0/1)짧은 텍스트, 이진 특성 벡터SMS 스팸, 등장/비등장(presence/absence) 특성

라플라스 스무딩(Laplace Smoothing)

테스트 데이터에 어떤 단어가 등장했는데 특정 클래스의 학습 데이터에서는 한 번도 등장하지 않았다면 어떻게 될까요?

스무딩이 없으면 P(word | class) = 0/N = 0이 됩니다. 곱셈 전체에 0이 한 번 들어가는 순간 P(class | features) = 0이 되어 버립니다. 다른 증거가 아무리 강해도, 보지 못한 단어 하나가 전체 예측을 무너뜨립니다.

라플라스 스무딩은 모든 특성 빈도에 작은 상수 alpha(보통 1)를 더합니다.

P(word_i | class) = (count(word_i, class) + alpha) / (total_words_in_class + alpha * vocab_size)

alpha=1이면 모든 단어가 최소한 아주 작은 확률을 갖습니다. 테스트 이메일에 "discombobulate" 같은 단어가 등장해도 스팸 확률이 0으로 떨어지지 않습니다. 이 스무딩은 베이지언(Bayesian) 관점에서 단어 분포에 균일 디리클레 사전(uniform Dirichlet prior)을 두는 것과 동일한 효과를 가집니다.

alpha가 클수록 스무딩이 강해져 분포가 더 균일해지고, 작을수록 데이터를 더 신뢰합니다. alpha는 튜닝해야 하는 하이퍼파라미터(hyperparameter)입니다.

alpha의 효과를 정리하면 다음과 같습니다.

alpha효과언제 쓰는가
0.001스무딩 거의 없음, 데이터를 신뢰매우 큰 학습 집합, 미관측 특성이 거의 없을 때
0.1약한 스무딩큰 학습 집합
1.0표준 라플라스 스무딩기본 시작점
10.0강한 스무딩, 분포를 평평하게 만듦매우 작은 학습 집합, 미관측 특성이 많을 때

로그 공간(Log-space) 계산

1보다 작은 확률 수백 개를 곱하면 부동소수점 언더플로(floating-point underflow)가 발생합니다. 실제 값은 아주 작은 양수인데 부동소수점에서는 0으로 떨어집니다.

해결책은 로그 공간(log space)에서 계산하는 것입니다. 확률을 곱하는 대신 로그를 더합니다.

log P(class | x1, x2, ..., xn) = log P(class) + sum_i log P(xi | class)

이렇게 하면 예측이 내적(dot product)으로 바뀝니다.

log_scores = X @ log_feature_probs.T + log_class_priors
prediction = argmax(log_scores)

행렬 곱입니다. 나이브 베이즈의 예측이 빠른 이유가 바로 이 점이며, 이는 단층 선형 모델(single-layer linear model)과 동일한 연산입니다.

나이브 베이즈와 로지스틱 회귀

둘 다 텍스트에 적용되는 선형 분류기(linear classifier)입니다. 차이는 무엇을 모델링하느냐에 있습니다.

관점나이브 베이즈(Naive Bayes)로지스틱 회귀(Logistic Regression)
유형생성 모델(generative). P(X|Y)를 모델링판별 모델(discriminative). P(Y|X)를 모델링
학습빈도 계수(frequency count)손실 함수(loss function) 최적화
작은 데이터강한 사전 덕분에 더 유리함가중치 추정이 어려움
큰 데이터잘못된 가정이 발목을 잡음더 유연한 결정 경계
특성독립 가정상관관계(correlation) 처리 가능
속도한 번 훑기로 끝, 매우 빠름반복 최적화
보정(calibration)확률이 약함확률이 더 안정적

경험칙은 간단합니다. 먼저 나이브 베이즈로 시작합니다. 데이터가 충분해지고 나이브 베이즈가 한계에 도달하면(plateau) 로지스틱 회귀로 넘어갑니다.

분류 파이프라인(Classification Pipeline)

flowchart LR
    A[원시 텍스트] --> B[토큰화]
    B --> C[어휘 구성]
    C --> D[단어 빈도 계산]
    D --> E[스무딩 적용]
    E --> F[로그 확률 계산]
    F --> G[예측: 클래스별 P를 비교해 argmax]

    style A fill:#f9f,stroke:#333
    style G fill:#9f9,stroke:#333

실제로는 부동소수점 언더플로를 피하기 위해 로그 공간에서 작업합니다. 작은 확률을 여러 번 곱하는 대신 로그값을 더하는 방식입니다.

log P(class | features) = log P(class) + sum_i log P(feature_i | class)

만들어 보기

code/naive_bayes.pyMultinomialNBGaussianNB를 처음부터 구현합니다.

MultinomialNB

처음부터 구현한 흐름은 다음과 같습니다.

  1. fit(X, y): 클래스별로 각 특성의 빈도를 셉니다. 라플라스 스무딩을 더하고, 로그 확률을 계산하며, 클래스 사전(class prior), 즉 클래스 빈도의 로그값을 저장합니다.

  2. predict_log_proba(X): 각 표본과 클래스에 대해 log P(class) + sum log P(feature_i | class)를 계산합니다. 이는 X @ log_probs.T + log_priors 형태의 행렬 곱과 같습니다.

  3. predict(X): 로그 확률이 가장 높은 클래스를 반환합니다.

class MultinomialNB:
    def __init__(self, alpha=1.0):
        self.alpha = alpha

    def fit(self, X, y):
        classes = np.unique(y)
        n_classes = len(classes)
        n_features = X.shape[1]

        self.classes_ = classes
        self.class_log_prior_ = np.zeros(n_classes)
        self.feature_log_prob_ = np.zeros((n_classes, n_features))

        for i, c in enumerate(classes):
            X_c = X[y == c]
            self.class_log_prior_[i] = np.log(X_c.shape[0] / X.shape[0])
            counts = X_c.sum(axis=0) + self.alpha
            self.feature_log_prob_[i] = np.log(counts / counts.sum())

        return self

핵심은 학습(fit) 이후의 예측이 행렬 곱에 편향(bias)을 더하는 연산일 뿐이라는 점입니다. 그래서 나이브 베이즈가 그토록 빠릅니다.

GaussianNB

연속형 특성에 대해서는 클래스별, 특성별로 평균과 분산을 추정합니다.

class GaussianNB:
    def __init__(self):
        pass

    def fit(self, X, y):
        classes = np.unique(y)
        self.classes_ = classes
        self.means_ = np.zeros((len(classes), X.shape[1]))
        self.vars_ = np.zeros((len(classes), X.shape[1]))
        self.priors_ = np.zeros(len(classes))

        for i, c in enumerate(classes):
            X_c = X[y == c]
            self.means_[i] = X_c.mean(axis=0)
            self.vars_[i] = X_c.var(axis=0) + 1e-9
            self.priors_[i] = X_c.shape[0] / X.shape[0]

        return self

예측은 특성별 가우시안 확률 밀도(Gaussian PDF)를 사용하며, 곱하는 대신 로그 공간에서 더합니다.

데모: 텍스트 분류

코드는 기술 분야 기사(tech article)와 스포츠 기사(sports article) 두 클래스를 흉내 내는 합성 단어 가방(synthetic bag-of-words) 데이터를 만듭니다. 클래스마다 서로 다른 단어 빈도 분포를 가지며, MultinomialNB가 단어 빈도를 이용해 클래스를 분류합니다.

합성 데이터는 200개의 특성 열(feature column)을 "단어"처럼 사용합니다. 0-39번 단어는 기술 기사에서 자주 나오고 스포츠에서는 적게 나옵니다. 80-119번 단어는 반대로 스포츠에서 자주 나오고 기술에서는 적게 나옵니다. 40-79번 단어는 양쪽에서 비슷한 빈도로 등장합니다. 일부 단어는 강한 클래스 지표(class indicator)가 되고, 다른 단어는 잡음(noise)이 되는 현실적인 상황을 만들어 줍니다.

데모: 연속형 특성

코드는 붓꽃(Iris)과 유사한 데이터를 만듭니다. 3개의 클래스와 4개의 특성을 가진 가우시안 군집(Gaussian cluster) 형태입니다. GaussianNB는 클래스별 평균과 분산으로 분류합니다. 각 클래스는 서로 다른 중심(평균 벡터)과 퍼짐(분산)을 가지며, 측정값이 카테고리에 따라 체계적으로 달라지는 실제 데이터의 모습을 흉내 냅니다.

코드는 다음도 함께 보여줍니다.

  • 스무딩 비교(smoothing comparison): 서로 다른 alpha 값으로 MultinomialNB를 학습해 스무딩 강도가 정확도(accuracy)에 주는 영향을 살펴봅니다.
  • 학습 데이터 크기 실험(training size experiment): 학습 데이터가 20개에서 1600개로 늘어날 때 정확도가 어떻게 향상되는지 확인합니다. 나이브 베이즈는 표본이 매우 적어도 그럴듯한 정확도에 빠르게 도달하는데, 이것이 가장 큰 장점입니다.
  • 혼동 행렬(confusion matrix): 클래스별 정밀도(precision), 재현율(recall), F1 점수(F1 score)를 통해 나이브 베이즈가 어디서 틀리는지 확인합니다.

예측 속도

나이브 베이즈의 예측은 결국 행렬 곱입니다. 표본 수가 n, 특성 수가 d, 클래스 수가 k일 때 다음과 같이 정리됩니다.

  • MultinomialNB: (n x d) @ (d x k) = O(n * d * k)인 행렬 곱 한 번
  • GaussianNB: 클래스별로 d개 특성 위에서 가우시안 PDF를 평가하므로 O(n * d * k)

둘 다 모든 차원에 대해 선형입니다. 모든 학습 점과 거리를 계산해야 하는 K-최근접 이웃(KNN)이나, 모든 서포트 벡터와 커널(kernel)을 평가해야 하는 방사 기저 함수(RBF) 커널 SVM과 비교하면, 예측 시점에서 수십 배 이상 빠릅니다.

사용하기

sklearn에서는 두 변형 모두 한두 줄의 코드로 사용할 수 있습니다.

from sklearn.naive_bayes import GaussianNB, MultinomialNB

gnb = GaussianNB()
gnb.fit(X_train, y_train)
print(f"GaussianNB 정확도: {gnb.score(X_test, y_test):.3f}")

mnb = MultinomialNB(alpha=1.0)
mnb.fit(X_train_counts, y_train)
print(f"MultinomialNB 정확도: {mnb.score(X_test_counts, y_test):.3f}")

sklearn으로 텍스트 분류를 할 때는 다음처럼 파이프라인(pipeline)을 구성합니다.

from sklearn.feature_extraction.text import CountVectorizer
from sklearn.naive_bayes import MultinomialNB
from sklearn.pipeline import Pipeline

text_clf = Pipeline([
    ("vectorizer", CountVectorizer()),
    ("classifier", MultinomialNB(alpha=1.0)),
])

text_clf.fit(train_texts, train_labels)
accuracy = text_clf.score(test_texts, test_labels)

naive_bayes.py는 같은 데이터에서 처음부터 구현한 결과와 sklearn 결과를 비교해 구현이 맞는지 확인합니다.

나이브 베이즈와 TF-IDF

원시 단어 빈도(raw word count)는 단어가 한 번 나올 때마다 같은 가중치를 줍니다. 그러나 "the", "is" 같은 흔한 단어는 모든 클래스에서 자주 등장하며 정보를 거의 담고 있지 않습니다. TF-IDF(Term Frequency - Inverse Document Frequency)는 흔한 단어의 가중치를 낮추고, 드물지만 구분력이 있는 단어의 가중치를 높입니다.

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.naive_bayes import MultinomialNB
from sklearn.pipeline import Pipeline

text_clf = Pipeline([
    ("tfidf", TfidfVectorizer()),
    ("classifier", MultinomialNB(alpha=0.1)),
])

TF-IDF 값은 음수가 아니므로 MultinomialNB와 함께 쓸 수 있습니다. TF-IDF와 MultinomialNB의 조합은 텍스트 분류에서 가장 강력한 기준 모델 중 하나입니다. 학습 표본이 10,000개보다 적은 데이터셋에서는 더 복잡한 모델을 자주 능가합니다.

짧은 텍스트에는 BernoulliNB

짧은 텍스트(트윗, SMS, 채팅 메시지)에서는 BernoulliNBMultinomialNB보다 더 나은 결과를 낼 수 있습니다. 짧은 텍스트는 단어 빈도가 낮아 빈도 정보 자체가 잡음(noise)에 가깝습니다. BernoulliNB는 단어의 등장 여부만 보기 때문에, 짧은 텍스트에서 더 안정적으로 동작합니다.

from sklearn.naive_bayes import BernoulliNB
from sklearn.feature_extraction.text import CountVectorizer

text_clf = Pipeline([
    ("vectorizer", CountVectorizer(binary=True)),
    ("classifier", BernoulliNB(alpha=1.0)),
])

CountVectorizerbinary=True는 모든 빈도를 0/1로 바꿉니다. 이 플래그(flag) 없이도 BernoulliNB는 실행되긴 하지만, 설계 의도와 다른 빈도 값을 보게 됩니다.

나이브 베이즈 확률 보정하기

나이브 베이즈의 확률은 보정이 좋지 않습니다. 나이브 베이즈가 P(spam) = 0.95라고 말해도 실제 확률은 0.7일 수 있습니다. 임계값(threshold)을 정하거나 다른 모델과 결합하기 위해 신뢰할 수 있는 확률 추정이 필요하다면 sklearn의 CalibratedClassifierCV를 사용합니다.

from sklearn.calibration import CalibratedClassifierCV

calibrated_nb = CalibratedClassifierCV(MultinomialNB(), cv=5, method="sigmoid")
calibrated_nb.fit(X_train, y_train)
proba = calibrated_nb.predict_proba(X_test)

이 방법은 교차 검증(cross-validation)을 사용해 나이브 베이즈의 원시 점수(raw score) 위에 로지스틱 회귀를 학습시킵니다. 결과적으로 산출되는 확률이 실제 클래스 빈도에 훨씬 가까워집니다.

흔한 주의점

  1. 음수 특성 값. MultinomialNB는 음수가 아닌(non-negative) 특성을 요구합니다. 일부 TF-IDF 설정이나 표준화된 특성처럼 음수 값이 있다면 GaussianNB를 사용하거나, 특성을 양수 범위로 평행이동(shift)해야 합니다.

  2. 분산이 0인 특성. GaussianNB는 분산으로 나누는 연산을 합니다. 어떤 클래스에서 특정 특성의 분산이 0이면(값이 모두 같으면) 확률 계산이 깨집니다. 코드는 모든 분산에 작은 스무딩 항(1e-9)을 더해 이를 방지합니다.

  3. 클래스 불균형. 이메일의 99%가 비스팸이면 P(not-spam)=0.99라는 사전이 너무 강해 가능도 증거(likelihood evidence)를 압도할 수 있습니다. sklearn에서는 class_prior 매개변수로 클래스 사전을 수동으로 설정할 수 있습니다.

  4. 특성 스케일링. MultinomialNB는 스케일링이 필요 없습니다. 빈도 위에서 작동하기 때문입니다. GaussianNB 역시 특성별 통계를 직접 추정하므로 스케일링이 꼭 필요하지 않습니다. 이는 스케일에 민감한 로지스틱 회귀나 SVM 대비 장점입니다.

산출물 만들기

이 lesson의 최종 산출물은 다음과 같습니다.

  • outputs/skill-naive-bayes-chooser.md: 적절한 나이브 베이즈 변형을 고르기 위한 의사결정 스킬(decision skill)
  • code/naive_bayes.py: MultinomialNBGaussianNB를 처음부터 구현하고 sklearn과 비교하는 예제

나이브 베이즈가 실패하는 경우

나이브 베이즈는 독립 가정이 확률뿐 아니라 클래스 순위까지 망가뜨릴 때 실패합니다. 주로 다음과 같은 상황에서 발생합니다.

  1. 강한 특성 상호작용(feature interaction). 클래스가 두 특성의 조합에 따라 달라지고 각각의 특성만으로는 정보가 없는 XOR과 같은 패턴이라면, 나이브 베이즈는 이를 놓칩니다. 각 특성이 단독으로는 어떤 증거도 제공하지 못하고, 나이브 베이즈는 이를 비선형적으로 결합할 수 없기 때문입니다.

  2. 서로 반대되는 증거를 주는, 강하게 상관된 특성. 특성 A는 스팸을, 특성 B는 비스팸을 가리키는데 실제로는 A와 B가 항상 같이 움직이는(완벽히 상관된) 경우, 나이브 베이즈는 존재하지 않는 충돌을 만들어 낼 수 있습니다.

  3. 매우 큰 학습 데이터. 데이터가 충분히 많아지면 로지스틱 회귀 같은 판별 모델이 실제 결정 경계를 더 잘 학습해 나이브 베이즈를 능가합니다. 작은 데이터에서 도움이 되던 독립 가정이 이제는 모델의 발목을 잡게 됩니다.

실제로 텍스트 분류에서는 이런 실패 양상이 상대적으로 드뭅니다. 텍스트 특성은 그 수가 많고 개별적으로는 약하며, 독립 가정의 오류가 상쇄되는 경향이 있기 때문입니다. 다만 특성 수가 적고 상관이 강한 표 형식 데이터에서는 로지스틱 회귀나 트리 기반(tree-based) 모델을 먼저 고려하는 것이 좋습니다.

연습문제

  1. 스무딩 실험 (쉬움). alpha 값을 0.01, 0.1, 1.0, 10.0, 100.0으로 바꿔 가며 텍스트 데이터에서 MultinomialNB를 학습합니다. alpha에 따른 정확도를 시각화합니다. 성능이 어디서 정점을 찍는지, 매우 큰 alpha가 성능을 떨어뜨리는 이유는 무엇인지 설명해 보세요.

  2. 특성 독립성 검증 (중간). 실제 텍스트 데이터셋에서 "machine"과 "learning"처럼 명백히 상관된 단어 두 개를 고릅니다. P(word1 | class) * P(word2 | class)P(word1 AND word2 | class)를 비교해 보세요. 독립 가정은 얼마나 틀렸나요? 그 차이가 분류 정확도에도 영향을 주나요?

  3. 베르누이 구현 (중간). 코드에 BernoulliNB 클래스를 추가합니다. 단어 가방을 이진(등장/비등장) 형태로 바꾸고 텍스트 데이터에서 MultinomialNB와 정확도를 비교합니다. 베르누이가 더 나은 경우는 언제인가요?

  4. 나이브 베이즈와 로지스틱 회귀 비교 (중간). 두 모델을 같은 텍스트 데이터에서 학습합니다. 학습 표본을 100개에서 시작해 10,000개까지 늘립니다. 학습 집합 크기에 따른 정확도를 시각화합니다. 어느 지점에서 로지스틱 회귀가 나이브 베이즈를 앞서나요?

  5. 스팸 필터 (어려움). 원시 이메일 텍스트를 토큰화(tokenize)하고, 어휘를 만들고, 단어 가방 특성을 생성한 뒤 MultinomialNB를 학습하는 완전한 스팸 분류기를 구현합니다. 정밀도(precision)와 재현율(recall)로 평가합니다. 정확도만 보면 안 되는 이유는 무엇인가요?

핵심 용어

용어흔한 설명실제 의미
나이브 베이즈(Naive Bayes)"단순한 확률 분류기"클래스가 주어지면 특성이 조건부 독립이라는 가정으로 베이즈 정리를 적용하는 분류기
조건부 독립(Conditional Independence)"특성들이 서로 영향이 없음"P(A, B | C) = P(A | C) * P(B | C). C를 알고 나면 B를 알아도 A에 대한 새로운 정보가 없다는 뜻
라플라스 스무딩(Laplace Smoothing)"1 더하기 스무딩(add-one smoothing)"0 확률이 예측을 지배하지 않도록 모든 특성 빈도에 작은 상수를 더하는 기법
사전 확률(Prior)"데이터를 보기 전의 믿음"P(class). 특성을 보기 전 각 클래스의 확률
가능도(Likelihood)"데이터가 얼마나 그럴듯한가"P(features | class). 클래스를 알 때 이런 특성들을 볼 확률
사후 확률(Posterior)"데이터를 본 뒤의 믿음"P(class | features). 특성을 본 뒤 갱신된 클래스 확률
생성 모델(Generative model)"데이터 생성 방식을 모델링한다"P(X | Y)P(Y)를 학습한 뒤 베이즈 정리로 P(Y | X)를 얻는 모델
판별 모델(Discriminative model)"결정 경계를 모델링한다"X가 어떻게 생성되는지 모델링하지 않고 P(Y | X)를 직접 학습하는 모델
로그 확률(Log probability)"언더플로 방지"작은 확률들의 곱이 부동소수점에서 0이 되는 것을 막기 위해 P 대신 log P로 계산하는 것

더 읽을거리

실습 코드

이 강의의 실습 코드 1개

naive bayes
Code

산출물

이 강의에서 생성된 프롬프트, 스킬, 코드 산출물 1개

skill-naive-bayes-chooser

Choose the right Naive Bayes variant for your classification task

Skill

확인 문제

3문제 · 모두 맞추면 완료 표시가 가능합니다

1.텍스트(text)에서 나이브 독립 가정(naive independence assumption)은 분명히 틀렸습니다. 그런데도 나이브 베이즈(Naive Bayes)가 분류를 잘하는 이유는 무엇인가요?

2.다항 나이브 베이즈(Multinomial NB)와 가우시안 나이브 베이즈(Gaussian NB)는 각각 언제 사용해야 하나요?

3.이메일(email)에 'free'가 두 번, 'money'가 한 번 들어 있습니다. 로그 확률(log probability)을 쓰는 다항 나이브 베이즈(Multinomial NB)에서 스팸 점수(spam score)는 어떻게 계산되나요?

0/3 답변 완료

추가 문제 풀기

AI가 강의 내용을 바탕으로 새로운 문제를 생성합니다