한양과학기술고등학교

에듀테크 프로젝트 대시보드

나이브 베이즈 스팸 필터

나이브 베이즈 스팸 필터 (Naive Bayes)

"무료 쿠폰 지금 클릭!" 이 메일은 스팸일까? 나이브 베이즈는 메일에 등장한 단어들만 보고, 학습해 둔 통계로 스팸일 확률을 계산한다. 20년 넘게 실제 메일함을 지켜 온, 놀랄 만큼 단순한 확률 분류기다.

핵심 아이디어: 스팸에 자주 나오는 단어(무료·당첨·클릭)가 많으면 스팸, 평범한 단어(회의·숙제·내일)가 많으면 정상 메일(햄). 각 단어의 "스팸다움"을 곱해서 최종 판정한다.

1. 베이즈 정리 — 증거를 보고 믿음을 갱신

알고 싶은 것: 메일 내용(단어들)이 주어졌을 때 스팸일 확률 P(스팸 | 단어들). 하지만 우리가 데이터에서 쉽게 세는 건 그 반대 방향 — P(단어 | 스팸)이다. 베이즈 정리가 방향을 뒤집어 준다.

P(스팸 | 단어들)  =  P(단어들 | 스팸) · P(스팸) P(단어들)

2. "나이브" — 단어들이 서로 독립이라고 가정

P(w₁, w₂, …, wₙ | 스팸)을 정직하게 세려면 그 정확한 단어 조합이 나온 스팸이 필요하다 — 거의 없다. 그래서 과감하게 가정한다: 스팸이라는 조건 아래에서 각 단어의 등장은 서로 독립이다.

P(w₁…wₙ | c)  ≈  P(w₁ | c) · P(w₂ | c) · … · P(wₙ | c)

"무료" 다음에 "클릭"이 잘 온다는 순서·상관관계를 통째로 무시하는 것 — 그래서 "순진한(naive)" 분류기다. 현실에서 단어는 당연히 독립이 아니지만, 분류 정답만 맞으면 되므로 실전 성능은 훌륭하다.

3. 학습 — 메일 10통에서 단어 세기

아래가 학습 데이터다. 스팸 5통, 햄(정상) 5통. 각 클래스에서 단어별 등장 횟수를 센다.

스팸 (5)

햄 · 정상 (5)

— 체크 해제 시 학습에 없던 단어 하나만 나와도 확률이 0으로 붕괴
단어스팸 횟수햄 횟수P(w | 스팸)P(w | 햄)스팸 : 햄 우도비
P(w | c) = (count(w, c) + α) / (Σ count(·, c) + α·V)  · V = 어휘 크기,

4. 분류 — 새 메일에 점수 매기기

메일을 골라 보거나 직접 입력해 보라. 각 단어가 판정을 스팸 쪽(분홍) 또는 햄 쪽(파랑)으로 얼마나 끌어당기는지 막대로 보여준다.

–
–
이 단어는 스팸을 시사 (우도비 > 1) 이 단어는 햄을 시사 (우도비 < 1)

5. 로그 확률 — 언더플로를 막는 실전 요령

단어가 30개면 0.001 같은 값을 30번 곱한다 → 10⁻⁹⁰. 컴퓨터의 실수 표현으로는 그냥 0이 되어버린다(언더플로). 해결책: 곱을 로그로 바꿔 덧셈으로 만든다.

log P(c | 단어들)  ∝  log P(c)  +  Σ log P(wᵢ | c)

log는 단조 증가 함수라 대소 관계가 그대로 보존된다 → 어느 클래스가 더 큰지 비교하는 데 아무 문제 없다. 위 4번 데모의 계산식도 전부 로그 합으로 돌아간다. 확률로 되돌리고 싶으면 두 로그 점수에 softmax를 씌운다.

6. 코드로 정리

import math, re
from collections import Counter, defaultdict

def train(docs):                       # docs = [(text, label), ...]
    wc     = defaultdict(Counter)         # wc[label][word] = 횟수
    total  = defaultdict(int)             # 클래스별 총 단어 수
    n_docs = Counter()
    for text, label in docs:
        n_docs[label] += 1
        for w in re.findall(r"\w+", text.lower()):
            wc[label][w] += 1; total[label] += 1
    vocab = {w for c in wc for w in wc[c]}
    return wc, total, n_docs, vocab

def predict(text, model, alpha=1.0):
    wc, total, n_docs, vocab = model
    V = len(vocab); N = sum(n_docs.values())
    words = [w for w in re.findall(r"\w+", text.lower()) if w in vocab]
    best, scores = None, {}
    for c in n_docs:
        logp = math.log(n_docs[c] / N)                    # log 사전확률
        for w in words:
            logp += math.log((wc[c][w] + alpha) /
                             (total[c] + alpha * V))       # log 우도, 합산
        scores[c] = logp
    return max(scores, key=scores.get), scores
한 줄 요약 — 나이브 베이즈는 "단어들이 조건부 독립"이라는 순진한 가정으로 우도를 단어별 확률의 곱(로그에서는 합)으로 쪼개고, 라플라스 평활로 0 확률을 막아, 스팸 vs 햄의 로그 점수를 비교한다. 관련: 공개키 암호는 아니고 😅 — 결정 트리, 퍼셉트론, k-평균, 블룸 필터(확률적 판정), 허프만 코딩(로그·확률).