확률론 (Probability Theory)¶
불확실성을 수학적으로 다루는 이론. ML/DL의 수학적 기반이며, 손실 함수, 샘플링, 베이지안 추론 등 모든 곳에 적용됨.
왜 확률이 필요한가¶
머신러닝은 본질적으로 불확실성을 다룬다:
- 데이터: 노이즈가 있고, 샘플은 모집단의 일부
- 모델: 실제 현상의 근사, 완벽할 수 없음
- 예측: 미래는 확정적이지 않음
확률은 이 불확실성을 정량화하는 도구다.
확률의 기초¶
표본 공간과 사건¶
- 표본 공간 (Sample Space, \(\Omega\)): 가능한 모든 결과의 집합
- 사건 (Event, A): 표본 공간의 부분집합
- 확률 (Probability): 사건에 할당된 0~1 사이의 값
# 주사위 예시
sample_space = {1, 2, 3, 4, 5, 6}
event_even = {2, 4, 6}
event_greater_than_4 = {5, 6}
P_even = len(event_even) / len(sample_space) # 3/6 = 0.5
확률의 공리 (Kolmogorov Axioms)¶
모든 확률은 이 세 가지 공리를 만족:
- 비음성: \(P(A) \geq 0\)
- 정규화: \(P(\Omega) = 1\)
- 가산 가법성: 상호 배타적 사건들에 대해 \(P(A_1 \cup A_2 \cup ...) = \sum P(A_i)\)
직관: "확률은 0 이상이고, 전체는 1이며, 겹치지 않으면 더할 수 있음."
기본 확률 규칙¶
import numpy as np
# 시뮬레이션으로 확률 추정 (Monte Carlo)
n_simulations = 100000
die1 = np.random.randint(1, 7, n_simulations)
die2 = np.random.randint(1, 7, n_simulations)
# P(합이 7)
p_sum_7 = np.mean(die1 + die2 == 7)
print(f"P(합=7) = {p_sum_7:.4f} (이론값: {1/6:.4f})")
# P(적어도 하나가 6)
p_at_least_one_6 = np.mean((die1 == 6) | (die2 == 6))
print(f"P(적어도 하나 6) = {p_at_least_one_6:.4f} (이론값: {11/36:.4f})")
확률의 해석: 빈도주의 vs 베이지안¶
| 관점 | 확률의 의미 | 예시 |
|---|---|---|
| 빈도주의 | 장기적 빈도 | "동전을 무한히 던지면 앞면 비율이 0.5에 수렴" |
| 베이지안 | 믿음의 정도 | "내 생각에 이 동전이 앞면일 확률은 0.5" |
ML에서는 두 관점 모두 사용됨.
조건부 확률¶
정의와 직관¶
사건 B가 발생했다는 정보가 주어졌을 때, 사건 A가 발생할 확률.
직관적 해석: - 전체 표본 공간을 B로 축소 - 그 축소된 공간에서 A가 차지하는 비율
예시: 카드 뽑기¶
# P(두 번째가 에이스 | 첫 번째가 에이스)
# 분석적 계산:
# 첫 카드가 에이스면, 남은 51장 중 에이스는 3장
# P = 3/51 ≈ 0.059
# 시뮬레이션
deck = list(range(52)) # 0-3: 에이스
n_sim = 100000
count_both_aces = 0
count_first_ace = 0
for _ in range(n_sim):
cards = np.random.choice(deck, 2, replace=False)
first_ace = cards[0] < 4
second_ace = cards[1] < 4
if first_ace:
count_first_ace += 1
if second_ace:
count_both_aces += 1
p_conditional = count_both_aces / count_first_ace
print(f"P(2nd Ace | 1st Ace) = {p_conditional:.4f} (이론값: {3/51:.4f})")
독립 사건¶
두 사건이 서로 영향을 주지 않는 경우:
동치 조건: $\(P(A|B) = P(A) \quad \text{또는} \quad P(B|A) = P(B)\)$
직관: B가 일어났다는 정보가 A의 확률에 영향을 주지 않음.
# 독립성 검정 (대략적)
def check_independence(data_a, data_b, tol=0.05):
"""두 이진 변수의 독립성 확인"""
p_a = np.mean(data_a)
p_b = np.mean(data_b)
p_ab = np.mean(data_a & data_b)
independent = np.isclose(p_ab, p_a * p_b, rtol=tol)
print(f"P(A) = {p_a:.4f}")
print(f"P(B) = {p_b:.4f}")
print(f"P(A)P(B) = {p_a * p_b:.4f}")
print(f"P(A∩B) = {p_ab:.4f}")
return independent
# 독립인 경우
data_a = np.random.binomial(1, 0.3, 10000)
data_b = np.random.binomial(1, 0.5, 10000)
print("독립 검정:", check_independence(data_a, data_b))
조건부 독립¶
A와 B가 C가 주어졌을 때 조건부 독립:
ML에서의 중요성: Naive Bayes는 특성들이 클래스가 주어졌을 때 조건부 독립임을 가정.
베이즈 정리 (Bayes' Theorem)¶
공식¶
전체 확률 법칙으로 분모 풀어쓰기: $\(P(A|B) = \frac{P(B|A) \cdot P(A)}{P(B|A) \cdot P(A) + P(B|A') \cdot P(A')}\)$
용어¶
| 용어 | 기호 | 의미 |
|---|---|---|
| 사전 확률 (Prior) | \(P(A)\) | 데이터 보기 전 믿음 |
| 우도 (Likelihood) | \(P(B\|A)\) | 가설이 참일 때 데이터가 나올 확률 |
| 사후 확률 (Posterior) | \(P(A\|B)\) | 데이터 본 후 업데이트된 믿음 |
| 증거 (Evidence) | \(P(B)\) | 정규화 상수 |
직관: "새로운 데이터를 보고 믿음을 업데이트함"
예시 1: 스팸 필터¶
# P(스팸 | "무료" 포함) = ?
# 주어진 정보
p_spam = 0.2 # P(스팸): 전체 중 스팸 비율
p_free_given_spam = 0.8 # P("무료"|스팸)
p_free_given_ham = 0.1 # P("무료"|정상)
# 베이즈 정리 적용
p_free = p_free_given_spam * p_spam + p_free_given_ham * (1 - p_spam)
p_spam_given_free = (p_free_given_spam * p_spam) / p_free
print(f"P(스팸|'무료') = {p_spam_given_free:.3f}") # 0.667
예시 2: 의료 진단 (중요!)¶
# 질병 검사
# 민감도(Sensitivity): P(양성|질병) = 0.99 (질병 있을 때 양성 나올 확률)
# 특이도(Specificity): P(음성|정상) = 0.95 (정상일 때 음성 나올 확률)
# 유병률: P(질병) = 0.001 (1000명 중 1명)
sensitivity = 0.99
specificity = 0.95
prevalence = 0.001
# P(질병|양성) = ?
p_positive_given_disease = sensitivity
p_positive_given_healthy = 1 - specificity # 위양성률
p_positive = p_positive_given_disease * prevalence + \
p_positive_given_healthy * (1 - prevalence)
p_disease_given_positive = (sensitivity * prevalence) / p_positive
print(f"P(질병|양성검사) = {p_disease_given_positive:.3f}") # 약 0.019!
핵심 통찰: - 유병률이 낮으면 양성예측도(PPV)도 낮음 - 99% 정확한 검사라도, 희귀 질병에서는 양성의 대부분이 오진 - 기저율 오류 (Base Rate Fallacy): 사전 확률을 무시하는 흔한 실수
예시 3: Naive Bayes 분류기¶
from sklearn.naive_bayes import GaussianNB
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
# 데이터
iris = load_iris()
X_train, X_test, y_train, y_test = train_test_split(
iris.data, iris.target, test_size=0.3, random_state=42
)
# Naive Bayes
# 가정: P(x1, x2, ..., xn | y) = P(x1|y) * P(x2|y) * ... * P(xn|y)
# 특성들이 클래스가 주어졌을 때 조건부 독립
clf = GaussianNB()
clf.fit(X_train, y_train)
print(f"Accuracy: {clf.score(X_test, y_test):.3f}")
기댓값과 분산¶
기댓값 (Expected Value)¶
확률 변수의 "평균적인 값".
직관적 해석: 실험을 무한히 반복했을 때 평균.
# 이산 확률 변수: 주사위
values = np.array([1, 2, 3, 4, 5, 6])
probs = np.array([1/6] * 6)
expected = np.sum(values * probs) # 3.5
print(f"E[주사위] = {expected}")
# 시뮬레이션 확인
samples = np.random.choice(values, 100000, p=probs)
print(f"표본 평균 = {np.mean(samples):.4f}")
기댓값의 성질: - 선형성: \(E[aX + b] = aE[X] + b\) - 합의 기댓값: \(E[X + Y] = E[X] + E[Y]\) (항상 성립, 독립 불필요) - 곱의 기댓값: \(E[XY] = E[X]E[Y]\) (독립일 때만)
분산 (Variance)¶
확률 변수가 기댓값에서 얼마나 퍼져 있는지.
직관적 해석: 기댓값에서의 "평균적인 제곱 거리".
# 주사위 분산
values = np.array([1, 2, 3, 4, 5, 6])
probs = np.array([1/6] * 6)
mu = np.sum(values * probs) # 3.5
variance = np.sum((values - mu)**2 * probs) # 2.917
std_dev = np.sqrt(variance)
print(f"Var(X) = {variance:.4f}, SD = {std_dev:.4f}")
분산의 성질: - \(Var(aX + b) = a^2 Var(X)\) (상수 b는 사라짐) - \(Var(X + Y) = Var(X) + Var(Y) + 2Cov(X,Y)\) - 독립일 때: \(Var(X + Y) = Var(X) + Var(Y)\)
공분산과 상관계수¶
# 공분산 행렬
np.random.seed(42)
X = np.random.randn(1000)
Y = 2 * X + np.random.randn(1000) # Y는 X와 상관
cov_matrix = np.cov(X, Y)
print(f"Cov(X,Y) = {cov_matrix[0,1]:.3f}")
corr_matrix = np.corrcoef(X, Y)
print(f"Corr(X,Y) = {corr_matrix[0,1]:.3f}")
확률 부등식¶
모수를 모를 때도 확률의 범위를 제한하는 도구.
마르코프 부등식¶
직관: 양수 확률변수가 평균의 k배 이상일 확률은 1/k 이하.
체비쇼프 부등식¶
직관: 평균에서 k 표준편차 이상 떨어질 확률의 상한.
# 체비쇼프 부등식 시뮬레이션
mu, sigma = 0, 1
X = np.random.normal(mu, sigma, 100000)
for k in [1, 2, 3]:
actual = np.mean(np.abs(X - mu) >= k * sigma)
bound = 1 / k**2
print(f"k={k}: 실제={actual:.4f}, 상한={bound:.4f}")
# 정규분포는 체비쇼프보다 훨씬 좁게 집중
# 체비쇼프는 분포와 무관한 "최악의 경우" 보장
대수의 법칙 (Law of Large Numbers)¶
표본 평균이 모평균에 수렴.
직관: 샘플을 많이 모을수록 평균은 참값에 가까워진다.
import matplotlib.pyplot as plt
# 대수의 법칙 시각화
true_mean = 0.5
sample_means = []
cumulative_sum = 0
for n in range(1, 10001):
cumulative_sum += np.random.binomial(1, true_mean)
sample_means.append(cumulative_sum / n)
plt.figure(figsize=(10, 4))
plt.plot(sample_means)
plt.axhline(y=true_mean, color='r', linestyle='--', label='True Mean')
plt.xlabel('Sample Size')
plt.ylabel('Sample Mean')
plt.legend()
plt.title('Law of Large Numbers')
ML에서의 의미: SGD가 수렴하는 이론적 근거.
중심 극한 정리 (Central Limit Theorem)¶
표본 평균의 분포가 정규분포에 수렴.
직관: 원래 분포가 뭐든, 평균을 여러 번 내면 정규분포처럼 됨.
from scipy.stats import norm
# CLT 시연: 균등분포에서 표본 추출
def demonstrate_clt(n_samples=1000, sample_sizes=[1, 2, 5, 30]):
fig, axes = plt.subplots(1, len(sample_sizes), figsize=(15, 3))
for ax, sample_size in zip(axes, sample_sizes):
sample_means = []
for _ in range(n_samples):
sample = np.random.uniform(0, 1, sample_size)
sample_means.append(np.mean(sample))
ax.hist(sample_means, bins=30, density=True, alpha=0.7)
# 이론적 정규분포 오버레이
mu = 0.5
se = (1/np.sqrt(12)) / np.sqrt(sample_size)
x = np.linspace(0, 1, 100)
ax.plot(x, norm.pdf(x, mu, se), 'r-', linewidth=2)
ax.set_title(f'n={sample_size}')
plt.tight_layout()
plt.show()
demonstrate_clt()
ML에서의 의미: - 왜 배치 평균이 안정적인지 설명 - 신뢰구간 계산의 근거 - 정규분포 가정이 "괜찮은" 이유
정보 이론¶
엔트로피 (Entropy)¶
불확실성의 측정. "평균적으로 몇 비트가 필요한가?"
직관: - 결과가 뻔하면 엔트로피 낮음 (정보가 적음) - 결과가 불확실하면 엔트로피 높음 (정보가 많음)
def entropy(probs):
"""Shannon Entropy 계산"""
probs = np.array(probs)
probs = probs[probs > 0] # log(0) 방지
return -np.sum(probs * np.log2(probs))
# 예시
uniform = [0.25, 0.25, 0.25, 0.25] # 완전 불확실
skewed = [0.97, 0.01, 0.01, 0.01] # 거의 확실
deterministic = [1.0, 0.0, 0.0, 0.0] # 완전 확실
print(f"균등: {entropy(uniform):.3f} bits") # 2.0 (최대)
print(f"치우침: {entropy(skewed):.3f} bits") # ~0.24
print(f"결정적: {entropy(deterministic):.3f} bits") # 0.0 (최소)
크로스 엔트로피 (Cross-Entropy)¶
참 분포 p로 샘플링하고, 분포 q로 인코딩할 때 필요한 평균 비트 수.
ML에서의 의미: 분류 손실 함수!
def cross_entropy(p, q):
"""Cross-entropy 계산"""
p = np.array(p)
q = np.array(q)
q = np.clip(q, 1e-10, 1) # log(0) 방지
return -np.sum(p * np.log(q))
# 분류 문제에서의 손실
true_label = [0, 0, 1, 0] # 원-핫 (클래스 3)
pred_good = [0.05, 0.05, 0.85, 0.05]
pred_bad = [0.25, 0.25, 0.25, 0.25]
pred_wrong = [0.85, 0.05, 0.05, 0.05]
print(f"좋은 예측: {cross_entropy(true_label, pred_good):.3f}")
print(f"나쁜 예측: {cross_entropy(true_label, pred_bad):.3f}")
print(f"틀린 예측: {cross_entropy(true_label, pred_wrong):.3f}")
KL 발산 (KL Divergence)¶
두 분포의 "거리" (비대칭).
직관: p를 q로 근사할 때 손실되는 정보량.
def kl_divergence(p, q):
"""KL Divergence 계산"""
p = np.array(p) + 1e-10
q = np.array(q) + 1e-10
return np.sum(p * np.log(p / q))
p = [0.4, 0.3, 0.2, 0.1]
q = [0.25, 0.25, 0.25, 0.25]
print(f"KL(p||q) = {kl_divergence(p, q):.4f}")
print(f"KL(q||p) = {kl_divergence(q, p):.4f}") # 비대칭!
ML에서의 활용: - VAE의 정규화 항 - 지식 증류 (Knowledge Distillation) - 정책 그래디언트에서 정책 차이 측정
흔한 실수와 오해¶
1. 조건부 확률 방향 혼동¶
# P(A|B) ≠ P(B|A) 일반적으로!
# 예: 의료 진단
# P(양성 | 질병) = 0.99 (민감도)
# P(질병 | 양성) = 0.02 (양성예측도, 유병률 낮을 때)
# 검사가 99% 정확해도, 양성이라고 질병일 확률은 2%일 수 있음
2. 독립과 배타 혼동¶
# 배타적 (Mutually Exclusive): A와 B가 동시에 일어날 수 없음
# P(A ∩ B) = 0
# 독립 (Independent): A와 B가 서로 영향을 주지 않음
# P(A ∩ B) = P(A) * P(B)
# 배타적 사건은 독립이 아님!
# A가 일어나면 B가 안 일어난다는 정보를 줌
3. 도박사의 오류 (Gambler's Fallacy)¶
4. 기저율 무시 (Base Rate Neglect)¶
5. 작은 표본에서 확률 과신¶
# "10번 중 8번 성공했으니 성공률은 80%!"
# 표본이 작으면 추정 오차가 큼
# 신뢰구간 계산
from scipy.stats import beta
posterior = beta(8+1, 2+1) # Bayesian with uniform prior
ci = posterior.ppf([0.025, 0.975])
print(f"95% CI: [{ci[0]:.3f}, {ci[1]:.3f}]") # 매우 넓음
LLM에서의 확률 활용¶
언어 모델의 확률¶
직관: 문장의 확률 = 각 단어가 이전 문맥에서 나올 확률의 곱.
# 다음 토큰 확률 계산
def softmax(logits, temperature=1.0):
"""Softmax로 확률 변환"""
logits = logits / temperature
exp_logits = np.exp(logits - np.max(logits)) # 수치 안정성
return exp_logits / np.sum(exp_logits)
# Perplexity 계산
def perplexity(log_probs):
"""문장의 Perplexity = exp(-평균 log 확률)"""
return np.exp(-np.mean(log_probs))
# 낮은 perplexity = 모델이 더 확신 = 더 예측 가능한 텍스트
샘플링 전략¶
def sample_from_logits(logits, strategy='greedy', **kwargs):
"""다양한 샘플링 전략"""
probs = softmax(logits)
if strategy == 'greedy':
# 항상 가장 확률 높은 토큰 선택
return np.argmax(probs)
elif strategy == 'temperature':
# Temperature로 분포 날카로움 조절
temp = kwargs.get('temperature', 1.0)
probs = softmax(logits, temperature=temp)
return np.random.choice(len(probs), p=probs)
elif strategy == 'top_k':
# 상위 k개 중에서 샘플링
k = kwargs.get('k', 50)
top_k_idx = np.argsort(probs)[-k:]
top_k_probs = probs[top_k_idx]
top_k_probs /= top_k_probs.sum() # 재정규화
return np.random.choice(top_k_idx, p=top_k_probs)
elif strategy == 'nucleus':
# Top-p (Nucleus) 샘플링: 누적 확률 p까지만
p = kwargs.get('p', 0.9)
sorted_idx = np.argsort(probs)[::-1]
sorted_probs = probs[sorted_idx]
cumsum = np.cumsum(sorted_probs)
cutoff = np.searchsorted(cumsum, p) + 1
nucleus_idx = sorted_idx[:cutoff]
nucleus_probs = probs[nucleus_idx]
nucleus_probs /= nucleus_probs.sum()
return np.random.choice(nucleus_idx, p=nucleus_probs)
# Temperature 효과:
# T = 0.1: 거의 greedy (높은 확률에 집중)
# T = 1.0: 원래 분포
# T = 2.0: 더 균등 (다양성 증가)
불확실성 추정¶
# 여러 번 샘플링해서 불확실성 추정
def estimate_uncertainty(model, prompt, n_samples=10):
"""MC Sampling으로 예측 불확실성 추정"""
outputs = []
for _ in range(n_samples):
output = model.generate(prompt, temperature=0.7)
outputs.append(output)
# 출력 다양성 = 불확실성 지표
unique_ratio = len(set(outputs)) / len(outputs)
return outputs, unique_ratio
참고 자료¶
- Probability Theory: The Logic of Science (Jaynes) - 베이지안 관점의 확률론
- Statistics 110: Probability (Harvard) - Blitzstein 교수의 명강의
- Information Theory, Inference, and Learning Algorithms (MacKay) - 정보 이론과 ML 연결
- 3Blue1Brown - Bayes Theorem - 직관적 시각화
- StatQuest - Probability - 친근한 설명