베이즈 추론: 증거로 믿음을 갱신하기
확률을 "증거가 들어올 때마다 고쳐 쓰는 믿음"으로 보는 베이즈 추론을 익혀요. 베이즈 정리와 오즈로 하는 빠른 암산, 나이브 베이즈 스팸 필터, 동전 편향을 분포째로 추정하는 베타 분포, 리뷰 2개짜리 별점 5점을 믿으면 안 되는 이유, 톰슨 샘플링 A/B 테스트, 그리고 딥러닝의 L2 정규화가 사실 사전 분포라는 연결까지 다뤄요.
의사는 환자를 처음 볼 때 "이 증상이면 흔히 감기"라는 믿음에서 시작해요. 검사 결과가 하나씩 나올 때마다 그 믿음을 고쳐 가죠. 열이 높으면 독감 쪽으로, 엑스레이에 그림자가 보이면 폐렴 쪽으로요. 이미 가진 믿음에 새 증거를 반영해 믿음을 갱신하는 것, 이게 베이즈 추론이에요.
지금까지 본 확률은 주로 "여러 번 반복하면 그 비율로 일어난다"는 빈도의 관점이었어요. 베이즈 관점은 확률을 믿음의 정도로도 봐요. 그래서 "이 동전의 앞면 확률이 0.6에서 0.8 사이일 확률은 70%"처럼, 한 번뿐인 일이나 모르는 값에 대해서도 확률로 말할 수 있어요.
베이즈 정리
확률의 기초 강의에서 배운 곱셈 규칙 를 정리하면 바로 나와요.
각 항에는 이름이 있어요. 는 가설, 는 증거예요.
| 항 | 이름 | 뜻 |
|---|---|---|
| 사전 확률 | 증거를 보기 전의 믿음 | |
| 가능도 | 가설이 맞다면 이 증거가 나올 확률 | |
| 증거 | 모든 가설을 통틀어 이 증거가 나올 확률 | |
| 사후 확률 | 증거를 본 뒤의 믿음 |
한 문장으로 줄이면 **"사후 확률은 사전 확률 곱하기 가능도에 비례한다"**예요. 분모 는 전체 합이 1이 되게 맞춰 주는 역할만 해요.
양성 판정을 다시 보기
확률의 기초 강의의 검사 문제를 베이즈 정리로 다시 풀어 볼게요. 유병률 1%, 민감도 99%, 위양성 5%였어요.
def update(prior, sens=0.99, fpr=0.05):
"""양성이 한 번 나왔을 때 병이 있을 확률"""
return sens * prior / (sens * prior + fpr * (1 - prior))
p = 0.01
for k in range(1, 4):
p = update(p)
print(k, "번 양성:", round(p, 3))
| 검사 | 병이 있을 확률 |
|---|---|
| 검사 전 | 1% |
| 1번 양성 | 16.7% |
| 2번 연속 양성 | 79.8% |
| 3번 연속 양성 | 98.7% |
여기서 베이즈 추론의 핵심 습관이 보여요. 어제의 사후 확률이 오늘의 사전 확률이 돼요. 증거가 쌓일 때마다 같은 식을 반복하면 돼요. 반대로 양성 다음에 음성이 나오면 확률은 0.2%로 뚝 떨어져요. 믿음은 증거에 따라 양쪽으로 움직여요.
오즈로 보면 암산이 된다
확률 대신 오즈(일어날 확률 대 안 일어날 확률의 비)로 쓰면 베이즈 정리가 곱셈 하나가 돼요.
양성의 가능도비는 이에요. 유병률 1%는 오즈로 1 대 99예요.
odds = 1 / 99 # 유병률 1%를 오즈로 (1 대 99)
lr = 0.99 / 0.05 # 양성의 가능도비 19.8
for k in range(1, 4):
odds *= lr
print(k, round(odds, 2), round(odds / (1 + odds), 3))
머릿속으로도 돼요. 1 대 99에 약 20을 곱하면 20 대 99, 대략 1 대 5예요. 6명 중 1명, 약 17%죠. 의사들이 검사 결과를 해석할 때 실제로 쓰는 방법이에요. 이 방식의 장점은 증거의 힘(가능도비)과 출발점(사전 오즈)이 깔끔하게 분리된다는 거예요.
스팸 필터: 나이브 베이즈
2000년대 초 스팸 메일을 크게 줄인 방법이 베이즈 정리였어요. 메일에 나온 단어들을 증거로 "스팸일 확률"을 갱신하는 거예요.
| 단어 | 스팸에 나올 확률 | 정상 메일에 나올 확률 |
|---|---|---|
| 무료 | 30% | 1% |
| 당첨 | 20% | 0.2% |
| 회의 | 1% | 10% |
단어들이 서로 독립이라고 순진하게 가정하면, 각 단어의 가능도를 그냥 곱하면 돼요. 그래서 이름이 나이브 베이즈예요. 실제로는 "무료"와 "당첨"이 함께 나오는 경향이 있으니 틀린 가정이지만, 그래도 놀랄 만큼 잘 작동해요.
import numpy as np
p_spam = 0.4
# 단어: (스팸에 나올 확률, 정상 메일에 나올 확률)
words = {"무료": (0.30, 0.01), "당첨": (0.20, 0.002), "회의": (0.01, 0.10)}
def spam_prob(ws):
s = np.log(p_spam) + sum(np.log(words[w][0]) for w in ws)
h = np.log(1 - p_spam) + sum(np.log(words[w][1]) for w in ws)
return 1 / (1 + np.exp(h - s))
for ws in (["무료"], ["무료", "당첨"], ["회의"], ["무료", "회의"]):
print(ws, round(spam_prob(ws), 4))
| 메일에 나온 단어 | 스팸일 확률 |
|---|---|
| 무료 | 95.2% |
| 무료, 당첨 | 99.95% |
| 회의 | 6.3% |
| 무료, 회의 | 66.7% |
확률을 직접 곱하지 않고 로그를 더한 이유가 중요해요. 단어가 수백 개면 아주 작은 확률을 수백 번 곱하게 되고, 컴퓨터가 표현할 수 있는 가장 작은 수보다 작아져 0이 돼 버려요(언더플로). 로그를 쓰면 곱셈이 덧셈이 돼서 안전해요. 수학 기초 트랙의 로그 강의에서 본 성질이 여기서 쓰여요. 딥러닝에서 확률을 거의 언제나 로그로 다루는 것도 같은 이유예요.
값 하나가 아니라 분포를 추정하기
동전을 10번 던졌더니 앞면이 7번 나왔어요. 이 동전의 앞면 확률 는 얼마일까요?
빈도 관점의 답은 이라는 숫자 하나예요. 베이즈 관점은 에 대한 믿음 전체를 분포로 구해요. 가 0.5일 수도, 0.9일 수도 있는데, 각각이 얼마나 그럴듯한지를요.
격자 근사: 가장 투명한 방법
의 후보를 0부터 1까지 잘게 나눠 놓고, 각 후보마다 "사전 믿음 × 가능도"를 계산하면 끝이에요.
theta = np.linspace(0, 1, 1001) # 앞면 확률 후보 1001개
prior = np.ones_like(theta) # 사전: 모든 후보가 똑같이 그럴듯함
likelihood = theta ** 7 * (1 - theta) ** 3 # 10번 중 앞면 7번이 나올 가능도
post = prior * likelihood
post /= post.sum() # 합이 1이 되게
print(round((theta * post).sum(), 3)) # 사후 평균
print(round(theta[post.argmax()], 3)) # 가장 그럴듯한 값
print(round(post[theta > 0.5].sum(), 3)) # 앞면 쪽으로 치우쳤을 확률
사후 평균은 0.667, 가장 그럴듯한 값은 0.7이에요. 그리고 빈도 관점으로는 바로 답하기 어려운 질문에도 답할 수 있어요. "이 동전이 앞면 쪽으로 치우쳤을 확률은?" 약 89%예요. 10번 던진 걸로는 아직 공정한 동전일 가능성도 11%쯤 남아 있다는 뜻이죠.
베타 분포: 공식으로 바로
확률을 추정할 때는 베타 분포를 사전 분포로 쓰면 계산이 아주 깔끔해져요.
와 를 "이미 본 것으로 치는 가상의 앞면, 뒷면 횟수"로 생각하면 돼요. Beta(1, 1)은 평평한 분포라 "아무것도 모른다"는 뜻이고, Beta(50, 50)은 "공정한 동전을 100번쯤 본 것 같은 강한 믿음"이에요.
from scipy import stats
for a, b in ((1, 1), (50, 50)):
for heads, n in ((7, 10), (70, 100), (700, 1000)):
d = stats.beta(a + heads, b + n - heads)
lo, hi = d.ppf([0.025, 0.975])
print((a, b), n, round(d.mean(), 3), round(lo, 3), round(hi, 3))
| 던진 횟수 (앞면 70%) | 평평한 사전 Beta(1, 1) | 강한 사전 Beta(50, 50) |
|---|---|---|
| 10번 | 0.667 (0.39에서 0.89) | 0.518 (0.43에서 0.61) |
| 100번 | 0.696 (0.60에서 0.78) | 0.600 (0.53에서 0.67) |
| 1,000번 | 0.700 (0.67에서 0.73) | 0.682 (0.65에서 0.71) |
괄호 안은 95% 신용 구간이에요. 세 가지가 보여요.
- 데이터가 적을 때는 사전 믿음이 결론을 크게 좌우해요. 강한 사전은 10번 결과에 거의 흔들리지 않아요.
- 데이터가 쌓이면 사전 분포의 차이가 점점 사라져요. 1,000번이면 두 사람이 거의 같은 결론에 도착해요.
- 구간이 데이터가 늘수록 좁아져요. 표본과 추정 강의의 과 같은 이야기예요.
신용 구간과 신뢰 구간
베이즈의 95% 신용 구간은 말 그대로 "가 이 구간에 있을 확률이 95%"라고 읽어도 돼요. 가설 검정 강의에서 신뢰 구간은 이렇게 읽으면 안 된다고 했던 것과 대비돼요. 대신 베이즈는 사전 분포라는 가정을 하나 더 얹은 대가를 치러요.
참고로 10번 중 7번을 빈도 관점의 가장 단순한 공식()으로 계산하면 0.42에서 0.98이에요. 표본이 작으면 이 근사 공식은 구간이 엉뚱하게 넓어지거나 1을 넘기도 해요. 작은 표본에서 베이즈 방법이 실무적으로 편한 이유 중 하나예요.
사전 분포는 주관적이지 않나
가장 흔한 비판이에요. 사람마다 사전 분포가 다르면 결론도 다르지 않냐는 거죠. 답은 세 가지예요.
- 사전 분포를 명시적으로 적어 두기 때문에, 누가 어떤 가정을 했는지 투명하게 보여요. 빈도 방법도 가정이 있지만 덜 드러날 뿐이에요.
- 위 표처럼 데이터가 충분하면 사전 분포의 영향은 사라져요.
- 사전 분포를 과거 데이터에서 가져오면 주관이 아니라 경험이 돼요.
별점 5.0, 리뷰 2개를 믿어도 될까
쇼핑에서 바로 쓸 수 있는 예예요. 상품 A는 별점 5.0(리뷰 2개), 상품 B는 4.8(리뷰 500개)이에요. 정말 A가 더 좋을까요?
비슷한 상품들의 평균이 4.0이라는 사전 지식을 "가상의 리뷰 10개"만큼 반영하면
def bayes_avg(mean, n, prior_mean=4.0, c=10):
return (c * prior_mean + n * mean) / (c + n)
print(round(bayes_avg(5.0, 2), 2), round(bayes_avg(4.8, 500), 2)) # 4.17 4.78
A는 4.17, B는 4.78이에요. 리뷰 2개의 5.0은 운일 가능성이 크니 평균 쪽으로 많이 당겨지고, 500개의 4.8은 거의 그대로예요. 많은 사이트가 순위를 매길 때 이런 베이즈 평균을 써요. 평균으로의 회귀를 미리 반영하는 셈이에요.
비슷한 고전으로 라플라스의 연속 법칙이 있어요. 10번 시도해서 10번 다 성공했다면 다음 성공 확률은 100%가 아니라 로 보자는 거예요. 평평한 사전 분포 Beta(1, 1)에서 나오는 답이에요. 한 번도 본 적 없는 일에 확률 0을 주지 않는 겸손함이죠.
베이즈로 하는 A/B 테스트: 톰슨 샘플링
가설 검정 강의의 A/B 테스트는 실험이 끝날 때까지 방문자를 반반 나눠야 했어요. 그동안 나쁜 버튼을 본 절반의 방문자는 손해예요. 톰슨 샘플링은 실험하면서 동시에 좋은 쪽으로 점점 더 많이 보내요.
- 버튼마다 클릭률의 사후 분포 Beta를 가지고 있어요. 처음엔 Beta(1, 1)이에요.
- 방문자가 올 때마다, 각 버튼의 사후 분포에서 값을 하나씩 뽑아 더 큰 버튼을 보여 줘요.
- 클릭 여부로 그 버튼의 분포를 갱신해요.
rng = np.random.default_rng(7)
true = np.array([0.10, 0.12]) # 진짜 클릭률 (알고리즘은 몰라요)
a, b = np.ones(2), np.ones(2) # 두 버튼의 사후 분포 Beta(1, 1)
clicks, shown = 0, np.zeros(2)
for _ in range(10_000):
k = int(np.argmax(rng.beta(a, b))) # 각 분포에서 하나씩 뽑아 큰 쪽을 보여 줘요
hit = rng.random() < true[k]
clicks += hit
shown[k] += 1
a[k] += hit
b[k] += 1 - hit
print(clicks, shown)
rng2 = np.random.default_rng(8)
arm = rng2.integers(0, 2, 10_000) # 비교: 끝까지 반반 나누기
print((rng2.random(10_000) < true[arm]).sum())
1만 명 중 약 89%(8,933명)가 더 좋은 버튼을 봤고, 클릭은 1,181번이에요. 끝까지 반반 나눈 경우는 1,047번이에요. 처음부터 정답을 알았다면 얻었을 1,200번에 훨씬 가까워요. 불확실할 때는 두 버튼 모두 뽑힐 기회가 있어서 탐색이 일어나고, 확신이 생길수록 좋은 쪽을 활용해요. 이 탐색과 활용의 균형은 추천 시스템, 광고, 그리고 강화 학습의 핵심 문제예요.
딥러닝의 정규화는 사전 분포다
사후 분포에서 가장 높은 점을 고르는 것을 MAP 추정이라고 해요. 로그를 취하고 부호를 바꾸면 최소화 문제가 돼요.
- 오차가 정규 분포를 따른다고 하면 첫 항은 제곱 오차가 돼요. 회귀 강의의 최소제곱이에요.
- 가중치의 사전 분포를 "0 근처의 정규 분포"로 두면 둘째 항은 , 즉 L2 정규화(딥러닝에서는 가중치 감쇠)가 돼요.
- 사전 분포를 라플라스 분포로 두면 L1 정규화가 되고, 가중치가 정확히 0이 되는 경향이 생겨요.
정말 같은지 확인해 볼게요.
from scipy.optimize import minimize
rng = np.random.default_rng(1)
X = rng.normal(size=(20, 5))
w_true = np.array([2.0, -1.0, 0.0, 0.0, 0.5])
y = X @ w_true + rng.normal(0, 1.0, 20)
w_ols = np.linalg.lstsq(X, y, rcond=None)[0] # 최소제곱
lam = 1.0 / 0.5 # 잡음 분산 / 사전 분산
w_ridge = np.linalg.solve(X.T @ X + lam * np.eye(5), X.T @ y) # L2 정규화(릿지)
neg_log_post = lambda w: ((y - X @ w) ** 2).sum() / 2 + (w ** 2).sum() / (2 * 0.5)
w_map = minimize(neg_log_post, np.zeros(5)).x # 베이즈 MAP
print(w_ridge.round(3))
print(w_map.round(3))
print(round(((w_ols - w_true) ** 2).sum(), 2), round(((w_ridge - w_true) ** 2).sum(), 2))
릿지 회귀의 답과 베이즈 MAP의 답이 소수점 셋째 자리까지 같아요. 그리고 데이터가 20개뿐일 때, 진짜 가중치와의 거리(제곱합)가 최소제곱은 0.71인데 정규화를 하면 0.37로 줄어요. "가중치는 아마 작을 것"이라는 믿음이 적은 데이터에 과하게 맞추는 것(과적합)을 막아 준 거예요. 머신러닝 트랙에서 정규화를 배울 때, 그게 사전 분포라는 걸 기억해 두세요.
베이즈와 AI
- 언어 모델이 내놓는 것은 "앞 문맥이 주어졌을 때 다음 토큰의 조건부 확률"이에요. 확률을 다루는 언어가 곧 AI의 언어예요.
- 하이퍼파라미터 튜닝에 쓰는 베이즈 최적화는 "어떤 설정이 좋을지"에 대한 믿음을 실험 결과로 갱신하며 다음 실험을 골라요.
- 모델이 "얼마나 확신하는지"를 알고 싶을 때 베이즈 방법과 그 근사(여러 모델의 앙상블 등)를 써요. 자율 주행이나 의료처럼 틀리면 큰일 나는 곳에서 특히 중요해요.
- 언어 모델이 프롬프트 속 예시 몇 개를 보고 과제를 알아채는 현상을 "숨은 과제에 대한 암묵적 베이즈 추론"으로 설명하는 연구도 있어요. 아직 하나의 해석이지만, 베이즈 관점이 AI를 이해하는 렌즈로 계속 쓰인다는 예예요.
실습
- 양성 판정 문제에서 유병률을 0.1%와 10%로 바꿔 1번, 2번 양성 후 확률을 구해 보세요. 사전 확률이 결론에 얼마나 큰 영향을 주나요?
- 격자 근사 코드에서 사전 분포를 "0.5 근처일 것" 같은 모양(예:
np.exp(-((theta - 0.5) ** 2) / 0.01))으로 바꾸고 사후 평균이 어떻게 변하는지 보세요. - 톰슨 샘플링에서 두 버튼의 진짜 클릭률을 똑같이(0.10, 0.10) 두면 방문자가 어떻게 나뉘는지 보세요.
- 나이브 베이즈 표에 단어를 하나 추가하고, 그 단어가 스팸 확률을 얼마나 움직이는지 가능도비로 먼저 예상한 뒤 계산으로 확인해 보세요.
자주 하는 실수
- 사전 확률을 빼먹기. 증거만 보고 판단하면 기저율 무시에 빠져요.
- 확률 0이나 1을 사전으로 주기. 그러면 어떤 증거가 와도 믿음이 절대 안 바뀌어요.
- 같은 증거를 두 번 세기. 같은 검사 결과를 두 번 반영하면 확신이 부풀려져요. 증거는 서로 독립일 때만 그대로 곱할 수 있어요.
- 확률을 그대로 곱하다 언더플로 내기. 로그를 쓰세요.
점검 문제
- 사전 확률이 20%이고 가능도비가 4인 증거를 봤어요. 사후 확률은?
- Beta(2, 2)를 사전으로 쓰고 앞면 3번, 뒷면 1번이 나왔어요. 사후 분포와 그 평균은?
- 데이터가 아주 많아지면 사전 분포의 영향은 어떻게 되나요?
- L2 정규화는 가중치에 대해 어떤 사전 믿음을 표현하나요?
- 톰슨 샘플링이 반반 나누는 A/B 테스트보다 클릭을 더 많이 얻는 이유는?
정답
- 사전 오즈 에 4를 곱하면 1, 즉 1 대 1이라 50%예요.
- Beta(5, 3)이고 평균은 예요.
- 점점 사라져요. 서로 다른 사전 분포에서 출발해도 같은 결론으로 모여요.
- 가중치가 0 근처의 작은 값일 것이라는 믿음(평균 0인 정규 분포)이에요.
- 실험하는 동안에도 좋아 보이는 버튼에 방문자를 더 많이 보내기 때문이에요. 불확실할 때만 탐색하고, 확신이 생기면 활용해요.
다음 강의
이 트랙의 마지막 강의에서는 정보 이론을 배워요. "놀라움"을 숫자로 재는 법, 엔트로피, 그리고 딥러닝 분류 모델의 손실 함수인 교차 엔트로피와 KL 발산이 어디서 왔는지 확인해요. 언어 모델의 성능을 재는 퍼플렉서티도 여기서 나와요.