콘텐츠로 이동
Data Prep
상세

확률론 (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)

모든 확률은 이 세 가지 공리를 만족:

  1. 비음성: \(P(A) \geq 0\)
  2. 정규화: \(P(\Omega) = 1\)
  3. 가산 가법성: 상호 배타적 사건들에 대해 \(P(A_1 \cup A_2 \cup ...) = \sum P(A_i)\)

직관: "확률은 0 이상이고, 전체는 1이며, 겹치지 않으면 더할 수 있음."

기본 확률 규칙

합의 규칙: P(A ∪ B) = P(A) + P(B) - P(A ∩ B)
여사건:    P(A') = 1 - P(A)
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가 발생할 확률.

\[P(A|B) = \frac{P(A \cap B)}{P(B)}, \quad P(B) > 0\]

직관적 해석: - 전체 표본 공간을 B로 축소 - 그 축소된 공간에서 A가 차지하는 비율

probability diagram 1

예시: 카드 뽑기

# 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 \cap B) = P(A) \cdot P(B)\]

동치 조건: $\(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가 주어졌을 때 조건부 독립:

\[P(A \cap B | C) = P(A|C) \cdot P(B|C)\]

ML에서의 중요성: Naive Bayes는 특성들이 클래스가 주어졌을 때 조건부 독립임을 가정.


베이즈 정리 (Bayes' Theorem)

공식

\[P(A|B) = \frac{P(B|A) \cdot P(A)}{P(B)}\]

전체 확률 법칙으로 분모 풀어쓰기: $\(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)

확률 변수의 "평균적인 값".

\[E[X] = \sum_{x} x \cdot P(X=x) \quad \text{(이산)}$$ $$E[X] = \int_{-\infty}^{\infty} x \cdot f(x) dx \quad \text{(연속)}\]

직관적 해석: 실험을 무한히 반복했을 때 평균.

# 이산 확률 변수: 주사위
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]\) (독립일 때만)

# 선형성 예시: 두 주사위 합의 기댓값
# E[D1 + D2] = E[D1] + E[D2] = 3.5 + 3.5 = 7

분산 (Variance)

확률 변수가 기댓값에서 얼마나 퍼져 있는지.

\[Var(X) = E[(X - E[X])^2] = E[X^2] - (E[X])^2\]

직관적 해석: 기댓값에서의 "평균적인 제곱 거리".

# 주사위 분산
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)\)

공분산과 상관계수

\[Cov(X, Y) = E[(X - E[X])(Y - E[Y])] = E[XY] - E[X]E[Y]$$ $$\rho_{XY} = \frac{Cov(X, Y)}{\sigma_X \sigma_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}")

확률 부등식

모수를 모를 때도 확률의 범위를 제한하는 도구.

마르코프 부등식

\[P(X \geq a) \leq \frac{E[X]}{a}, \quad X \geq 0, a > 0\]

직관: 양수 확률변수가 평균의 k배 이상일 확률은 1/k 이하.

체비쇼프 부등식

\[P(|X - \mu| \geq k\sigma) \leq \frac{1}{k^2}\]

직관: 평균에서 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)

표본 평균이 모평균에 수렴.

\[\bar{X}_n = \frac{1}{n}\sum_{i=1}^{n}X_i \xrightarrow{p} \mu \quad \text{as } n \to \infty\]

직관: 샘플을 많이 모을수록 평균은 참값에 가까워진다.

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)

표본 평균의 분포가 정규분포에 수렴.

\[\frac{\bar{X}_n - \mu}{\sigma/\sqrt{n}} \xrightarrow{d} N(0, 1)\]

직관: 원래 분포가 뭐든, 평균을 여러 번 내면 정규분포처럼 됨.

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)

불확실성의 측정. "평균적으로 몇 비트가 필요한가?"

\[H(X) = -\sum_{x} P(x) \log_2 P(x)\]

직관: - 결과가 뻔하면 엔트로피 낮음 (정보가 적음) - 결과가 불확실하면 엔트로피 높음 (정보가 많음)

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로 인코딩할 때 필요한 평균 비트 수.

\[H(p, q) = -\sum_{x} p(x) \log q(x)\]

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)

두 분포의 "거리" (비대칭).

\[D_{KL}(p||q) = \sum_{x} p(x) \log \frac{p(x)}{q(x)} = H(p, q) - H(p)\]

직관: 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)

# "앞면이 10번 연속 나왔으니, 이번엔 뒷면이 나올 확률이 높다"
# 틀림! 동전은 기억이 없음.

# 매 시행은 독립 → P(뒷면) = 0.5 항상

4. 기저율 무시 (Base Rate Neglect)

# "이 검사는 99% 정확하니까, 양성이면 99% 확률로 병이 있다"
# 틀림! 유병률(기저율)을 고려해야 함.

# 베이즈 정리를 사용해야 정확한 확률 계산 가능

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에서의 확률 활용

언어 모델의 확률

\[P(w_1, w_2, ..., w_n) = \prod_{i=1}^{n} P(w_i | w_1, ..., w_{i-1})\]

직관: 문장의 확률 = 각 단어가 이전 문맥에서 나올 확률의 곱.

# 다음 토큰 확률 계산
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

참고 자료