확률 분포 (Probability Distributions)¶
데이터 생성 과정을 모델링하는 수학적 함수. 적절한 분포 선택은 ML 모델의 성능에 직접적인 영향을 미친다.
왜 확률 분포가 중요한가¶
- 손실 함수 설계: MSE는 정규분포, Cross-Entropy는 베르누이/카테고리
- 생성 모델: VAE, GAN, Diffusion 모델의 핵심
- 베이지안 추론: 사전 분포 선택
- 데이터 이해: 현상의 생성 메커니즘 파악
이산 확률 분포¶
베르누이 분포 (Bernoulli Distribution)¶
성공/실패 두 가지 결과만 있는 단일 시행.
| 통계량 | 값 |
|---|---|
| 평균 | \(E[X] = p\) |
| 분산 | \(Var(X) = p(1-p)\) |
직관: 동전 던지기 한 번. 앞면(1) 또는 뒷면(0).
from scipy import stats
import numpy as np
# 베르누이 분포
p = 0.7
bernoulli = stats.bernoulli(p)
# 샘플링
samples = bernoulli.rvs(1000)
print(f"표본 평균: {samples.mean():.3f}, 이론값: {p}")
# PMF (확률 질량 함수)
print(f"P(X=0) = {bernoulli.pmf(0):.3f}")
print(f"P(X=1) = {bernoulli.pmf(1):.3f}")
ML 활용: - 이진 분류의 출력 (로지스틱 회귀) - Dropout 마스크 - 클릭/미클릭, 구매/미구매
이항 분포 (Binomial Distribution)¶
n번의 독립적인 베르누이 시행에서 성공 횟수.
| 통계량 | 값 |
|---|---|
| 평균 | \(E[X] = np\) |
| 분산 | \(Var(X) = np(1-p)\) |
직관: 동전을 n번 던져서 앞면이 나온 횟수.
n, p = 100, 0.3
binomial = stats.binom(n, p)
# PMF 시각화
import matplotlib.pyplot as plt
x = np.arange(0, n+1)
pmf = binomial.pmf(x)
plt.figure(figsize=(10, 4))
plt.bar(x, pmf, width=1, edgecolor='black')
plt.xlabel('k (성공 횟수)')
plt.ylabel('P(X=k)')
plt.title(f'Binomial(n={n}, p={p})')
plt.xlim(0, 60)
# 누적 확률
print(f"P(X <= 30) = {binomial.cdf(30):.4f}")
print(f"P(25 <= X <= 35) = {binomial.cdf(35) - binomial.cdf(24):.4f}")
ML 활용: - A/B 테스트 (전환 수) - 샘플링된 정확도 분포
포아송 분포 (Poisson Distribution)¶
단위 시간/공간당 사건 발생 횟수. 드문 사건의 모델.
| 통계량 | 값 |
|---|---|
| 평균 | \(E[X] = \lambda\) |
| 분산 | \(Var(X) = \lambda\) |
직관: 1시간에 평균 5건의 전화가 온다면, 정확히 3건 올 확률은?
핵심 특성: 평균 = 분산 (등분산성)
# 시간당 평균 5건의 이벤트
lambda_ = 5
poisson = stats.poisson(lambda_)
# P(10건 이상)
p_10_or_more = 1 - poisson.cdf(9)
print(f"P(X >= 10) = {p_10_or_more:.4f}")
# 포아송 과정 시뮬레이션
# 시간 간격 0.01로 24시간 시뮬레이션
hours = 24
dt = 0.01
rate_per_dt = lambda_ * dt # 작은 구간의 발생률
events = []
for t in np.arange(0, hours, dt):
if np.random.random() < rate_per_dt:
events.append(t)
print(f"발생 이벤트 수: {len(events)} (기대값: {lambda_ * hours})")
ML 활용: - 웹 트래픽 모델링 - 텍스트의 단어 빈도 (희귀 단어) - 이상 탐지 (비정상적 이벤트 수)
이항 → 포아송 근사: n이 크고 p가 작을 때, \(\lambda = np\)로 포아송 근사 가능.
기하 분포 (Geometric Distribution)¶
첫 번째 성공까지의 시행 횟수.
| 통계량 | 값 |
|---|---|
| 평균 | \(E[X] = 1/p\) |
| 분산 | \(Var(X) = (1-p)/p^2\) |
직관: 처음으로 앞면이 나올 때까지 몇 번 던져야 하나?
무기억성: 이미 k번 실패했어도, 앞으로의 기대 시행 횟수는 여전히 1/p.
p = 0.2
geometric = stats.geom(p)
print(f"E[X] = {1/p}") # 평균 5번 시행 필요
print(f"P(X <= 3) = {geometric.cdf(3):.4f}") # 3번 이내 성공 확률
ML 활용: - 사용자가 처음 구매까지 걸리는 방문 횟수 - 토큰 생성에서 특정 토큰까지의 거리
음이항 분포 (Negative Binomial Distribution)¶
r번째 성공까지의 시행 횟수. 기하 분포의 일반화.
ML 활용: - 과산포(overdispersion) 카운트 데이터 (분산 > 평균) - 포아송보다 유연한 모델
# 음이항: r번 성공까지
r, p = 5, 0.3
nbinom = stats.nbinom(r, p)
# 포아송 vs 음이항 비교
lambda_ = r * (1-p) / p # 같은 평균
poisson = stats.poisson(lambda_)
print(f"포아송 분산: {lambda_:.2f}")
print(f"음이항 분산: {nbinom.var():.2f}") # 더 큼
카테고리/다항 분포 (Categorical/Multinomial)¶
K개 범주 중 하나를 선택.
직관: 주사위 던지기 (6개 범주). LLM의 다음 토큰 선택.
# 다항 분포 (여러 번 시행)
probs = [0.2, 0.3, 0.5] # 3개 범주
n_trials = 100
multinomial = stats.multinomial(n_trials, probs)
# 샘플링: 각 범주의 발생 횟수
sample = multinomial.rvs(1)
print(f"각 범주 발생 횟수: {sample}")
# LLM의 출력 분포가 카테고리 분포
# Softmax 출력 = 어휘의 각 토큰에 대한 확률
연속 확률 분포¶
균등 분포 (Uniform Distribution)¶
구간 내 모든 값이 동일한 확률.
| 통계량 | 값 |
|---|---|
| 평균 | \(E[X] = (a+b)/2\) |
| 분산 | \(Var(X) = (b-a)^2/12\) |
직관: 버스가 0~10분 사이에 균등하게 온다면?
a, b = 0, 1
uniform = stats.uniform(loc=a, scale=b-a)
# 난수 생성의 기본
samples = uniform.rvs(1000)
# 파라미터 초기화에 활용
# Xavier 초기화: U(-sqrt(6/(n_in+n_out)), sqrt(6/(n_in+n_out)))
ML 활용: - 신경망 가중치 초기화 - 하이퍼파라미터 랜덤 서치 - 무정보적 사전 분포
정규 분포 (Normal/Gaussian Distribution)¶
가장 중요한 분포. 중심극한정리에 의해 자연적으로 발생.
| 통계량 | 값 |
|---|---|
| 평균 | \(E[X] = \mu\) |
| 분산 | \(Var(X) = \sigma^2\) |
왜 정규분포가 특별한가? 1. 중심극한정리: 독립적인 것들의 합은 정규분포로 수렴 2. 최대 엔트로피: 평균과 분산만 주어졌을 때, 정규분포가 가장 불확실(가정 최소) 3. 재생성: 정규분포의 합도 정규분포
mu, sigma = 0, 1
normal = stats.norm(mu, sigma)
# PDF 시각화
x = np.linspace(-4, 4, 100)
plt.plot(x, normal.pdf(x))
plt.title('Standard Normal Distribution')
# z-점수 변환: 어떤 정규분포든 표준 정규분포로
data = np.random.normal(10, 2, 1000)
z_scores = (data - data.mean()) / data.std()
# 확률 계산
print(f"P(X < 1.96) = {normal.cdf(1.96):.4f}") # 0.975
print(f"P(-1.96 < X < 1.96) = {normal.cdf(1.96) - normal.cdf(-1.96):.4f}") # 0.95
68-95-99.7 법칙: - 68%: \(\mu \pm 1\sigma\) 내 - 95%: \(\mu \pm 2\sigma\) 내 - 99.7%: \(\mu \pm 3\sigma\) 내
ML 활용: - 가중치 초기화 (Kaiming, Xavier) - 배치 정규화의 목표 분포 - VAE의 잠재 공간 - Gaussian Noise 추가
지수 분포 (Exponential Distribution)¶
사건 사이의 대기 시간.
| 통계량 | 값 |
|---|---|
| 평균 | \(E[X] = 1/\lambda\) |
| 분산 | \(Var(X) = 1/\lambda^2\) |
핵심 특성: 무기억성 (Memoryless) $\(P(X > s+t | X > s) = P(X > t)\)$
직관: 이미 10분 기다렸어도, 앞으로 기다릴 시간 분포는 처음과 동일.
lambda_ = 0.5 # rate (1/평균)
exponential = stats.expon(scale=1/lambda_) # scipy는 scale = 1/rate
# 서버 응답 시간 모델링
response_times = exponential.rvs(1000)
print(f"평균 응답 시간: {response_times.mean():.2f}s (이론값: {1/lambda_})")
# P(응답 > 2초)
print(f"P(X > 2) = {1 - exponential.cdf(2):.4f}")
포아송-지수 관계: - 포아송: 단위 시간당 사건 수 - 지수: 사건 간 시간 간격 - rate \(\lambda\)가 동일
감마 분포 (Gamma Distribution)¶
지수 분포의 일반화. k번째 사건까지의 대기 시간.
| 파라미터 | 의미 |
|---|---|
| \(\alpha\) (shape) | 사건 수, 분포 형태 결정 |
| \(\beta\) (rate) | 발생률 |
alpha, beta = 2, 0.5
gamma_dist = stats.gamma(a=alpha, scale=1/beta)
# 다양한 shape에 따른 분포 형태
fig, ax = plt.subplots(figsize=(10, 4))
x = np.linspace(0, 20, 200)
for alpha in [1, 2, 5, 10]:
gamma = stats.gamma(a=alpha, scale=2)
ax.plot(x, gamma.pdf(x), label=f'α={alpha}')
ax.legend()
ax.set_title('Gamma Distribution')
ML 활용: - 대기 시간, 수명 모델링 - 베이지안에서 정밀도(precision)의 사전 분포 - 양수 데이터 모델링
베타 분포 (Beta Distribution)¶
[0, 1] 구간의 확률/비율 모델링.
| 통계량 | 값 |
|---|---|
| 평균 | \(\frac{\alpha}{\alpha + \beta}\) |
| 분산 | \(\frac{\alpha\beta}{(\alpha+\beta)^2(\alpha+\beta+1)}\) |
직관: \(\alpha\)번 성공, \(\beta\)번 실패 관측 후 성공률에 대한 믿음.
# 다양한 베타 분포 형태
params = [(0.5, 0.5), (1, 1), (2, 2), (2, 5), (5, 2), (5, 1)]
fig, ax = plt.subplots(figsize=(10, 4))
x = np.linspace(0.01, 0.99, 100)
for alpha, beta in params:
beta_dist = stats.beta(alpha, beta)
ax.plot(x, beta_dist.pdf(x), label=f'α={alpha}, β={beta}')
ax.legend()
ax.set_title('Beta Distribution')
# Beta(1, 1) = Uniform(0, 1): 무정보적 사전
# Beta(0.5, 0.5): Jeffreys prior
ML 활용: - 이항 분포의 켤레 사전 (베이지안) - 클릭률, 전환율 추정 - 확률 값 모델링
로그 정규 분포 (Log-Normal Distribution)¶
로그를 취하면 정규분포.
직관: 곱셈적 효과가 누적될 때 발생. (덧셈적 → 정규, 곱셈적 → 로그정규)
mu, sigma = 0, 0.5
lognorm = stats.lognorm(s=sigma, scale=np.exp(mu))
# 양수, 오른쪽 꼬리
samples = lognorm.rvs(1000)
print(f"평균: {samples.mean():.3f}")
print(f"중앙값: {np.median(samples):.3f}") # 평균 < 중앙값
ML 활용: - 금융 수익률 - 파일/문서 크기 - 소득 분포 - 주택 가격
다변량 정규 분포 (Multivariate Normal)¶
여러 변수의 결합 분포.
# 2변량 정규분포
mean = [0, 0]
cov = [[1, 0.8], [0.8, 1]] # 강한 양의 상관
mvn = stats.multivariate_normal(mean, cov)
samples = mvn.rvs(1000)
plt.figure(figsize=(6, 6))
plt.scatter(samples[:, 0], samples[:, 1], alpha=0.5)
plt.title('Bivariate Normal (corr=0.8)')
plt.axis('equal')
ML 활용: - 임베딩 공간 모델링 - GMM (Gaussian Mixture Model) - Kalman Filter
Student's t 분포¶
정규분포보다 두꺼운 꼬리. 소표본이나 이상치에 강건.
- \(\nu\) (자유도)가 클수록 정규분포에 가까워짐
- \(\nu = 1\): Cauchy 분포 (평균 없음)
- \(\nu \to \infty\): 정규분포
# 자유도에 따른 변화
x = np.linspace(-5, 5, 200)
plt.figure(figsize=(10, 4))
for df in [1, 3, 10, 30]:
t_dist = stats.t(df)
plt.plot(x, t_dist.pdf(x), label=f'df={df}')
plt.plot(x, stats.norm.pdf(x), 'k--', label='Normal')
plt.legend()
plt.title('Student t Distribution')
ML 활용: - 이상치에 강건한 회귀 (t-분포 likelihood) - 소표본 추론 - 불확실성 모델링
분포 선택 가이드¶
데이터 특성에 따른 선택¶
| 데이터 특성 | 권장 분포 | 예시 |
|---|---|---|
| 이진 결과 | 베르누이, 이항 | 클릭, 구매 |
| 카운트 (분산≈평균) | 포아송 | 방문 수, 오류 수 |
| 카운트 (분산>평균) | 음이항 | 댓글 수 (과산포) |
| 대기 시간 | 지수, 감마 | 응답 시간 |
| 비율 (0~1) | 베타 | 클릭률, 전환율 |
| 무제한 연속값 | 정규, t | 키, 온도 |
| 양수, 오른쪽 꼬리 | 로그정규, 감마 | 소득, 가격 |
| K개 범주 | 카테고리, 다항 | 감정 분류 |
| 이상치 있는 연속값 | t 분포 | 금융 수익률 |
분포 적합도 검정¶
from scipy.stats import kstest, shapiro, normaltest, anderson
data = np.random.normal(0, 1, 500)
# Shapiro-Wilk (n <= 5000, 정규성)
stat, p = shapiro(data)
print(f"Shapiro-Wilk: p={p:.4f}")
# D'Agostino-Pearson (대표본, 정규성)
stat, p = normaltest(data)
print(f"D'Agostino: p={p:.4f}")
# Kolmogorov-Smirnov (임의의 분포)
stat, p = kstest(data, 'norm')
print(f"KS test (norm): p={p:.4f}")
# Anderson-Darling (꼬리 민감)
result = anderson(data, dist='norm')
print(f"Anderson-Darling: stat={result.statistic:.4f}")
Q-Q Plot¶
from scipy import stats
# 데이터
data = np.random.exponential(1, 500)
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
# 정규분포와 비교
stats.probplot(data, dist="norm", plot=axes[0])
axes[0].set_title('Q-Q Plot vs Normal')
# 지수분포와 비교
stats.probplot(data, dist="expon", plot=axes[1])
axes[1].set_title('Q-Q Plot vs Exponential')
# 직선에 가까우면 해당 분포에 적합
ML/DL에서의 활용¶
손실 함수와 분포¶
핵심 통찰: 손실 함수는 암묵적으로 분포를 가정함.
| 손실 함수 | 가정하는 분포 | 수식 연결 |
|---|---|---|
| MSE | 정규분포 | $-\log N(y |
| MAE | 라플라스 분포 | $-\log Laplace(y |
| Cross-Entropy | 베르누이/카테고리 | $-\log p(y |
| Poisson Loss | 포아송 분포 | $-\log Poisson(y |
| Huber | 정규 + 라플라스 혼합 | 중간에서는 정규, 꼬리에서는 라플라스 |
# MSE = Gaussian NLL (상수 제외)
def gaussian_nll(y_true, y_pred, sigma=1):
"""Gaussian Negative Log-Likelihood"""
return 0.5 * np.mean((y_true - y_pred)**2 / sigma**2)
def mse(y_true, y_pred):
return np.mean((y_true - y_pred)**2)
# 비례 관계 확인
y_true = np.random.randn(100)
y_pred = y_true + np.random.randn(100) * 0.5
print(f"MSE: {mse(y_true, y_pred):.4f}")
print(f"Gaussian NLL: {gaussian_nll(y_true, y_pred):.4f}")
변분 오토인코더 (VAE)¶
import torch
import torch.nn.functional as F
# VAE의 잠재 공간은 정규분포 가정
# encoder: q(z|x) = N(μ(x), σ(x))
# prior: p(z) = N(0, I)
def reparameterize(mu, log_var):
"""Reparameterization trick: 샘플링을 미분 가능하게"""
std = torch.exp(0.5 * log_var)
eps = torch.randn_like(std) # N(0, 1)에서 샘플
return mu + eps * std # N(mu, std)에서 샘플과 동치
def kl_divergence_gaussian(mu, log_var):
"""KL(q(z|x) || p(z)) where p(z) = N(0, I)
해석적 계산 가능:
KL = -0.5 * sum(1 + log(σ²) - μ² - σ²)
"""
return -0.5 * torch.sum(1 + log_var - mu.pow(2) - log_var.exp())
Dropout의 확률적 해석¶
# Dropout = 베르누이 마스킹
def dropout(x, p=0.5, training=True):
"""
p: drop 확률 (비활성화 확률)
training: 학습 시에만 적용
"""
if not training or p == 0:
return x
# 각 뉴런을 1-p 확률로 유지
mask = np.random.binomial(1, 1-p, x.shape)
# Inverted dropout: 학습 시 스케일 조정
# 추론 시 변경 불필요
return x * mask / (1 - p)
# MC Dropout: 추론 시에도 dropout → 불확실성 추정
LLM의 출력 분포¶
def softmax_with_temperature(logits, temperature=1.0):
"""
Temperature로 카테고리 분포의 날카로움 조절
T = 0: deterministic (argmax)
T = 1: 원래 분포
T > 1: 더 균등 (다양성 증가)
T < 1: 더 날카로움 (자신감 증가)
"""
logits = logits / temperature
exp_logits = np.exp(logits - np.max(logits)) # 수치 안정
return exp_logits / np.sum(exp_logits)
# 예시: 3개 토큰에 대한 logits
logits = np.array([2.0, 1.0, 0.5])
for T in [0.5, 1.0, 2.0]:
probs = softmax_with_temperature(logits, T)
print(f"T={T}: {probs.round(3)}")
분포 변환¶
Box-Cox 변환¶
양수 데이터를 정규분포에 가깝게.
from scipy.stats import boxcox, skew
data = np.random.exponential(1, 1000) + 0.001 # 양수여야 함
# Box-Cox 변환
data_transformed, lambda_opt = boxcox(data)
print(f"최적 lambda: {lambda_opt:.3f}")
print(f"변환 전 왜도: {skew(data):.3f}")
print(f"변환 후 왜도: {skew(data_transformed):.3f}")
Yeo-Johnson 변환¶
Box-Cox의 확장. 음수도 처리 가능.
from scipy.stats import yeojohnson
data = np.random.randn(1000) # 음수 포함 가능
data_transformed, lambda_opt = yeojohnson(data)
분위수 변환¶
어떤 분포든 균등/정규로 변환.
from sklearn.preprocessing import QuantileTransformer
# 정규분포로 변환
transformer = QuantileTransformer(output_distribution='normal',
n_quantiles=1000, random_state=42)
data_transformed = transformer.fit_transform(data.reshape(-1, 1))
# 비선형이지만 강력한 정규화
흔한 실수와 오해¶
1. 정규분포 만능주의¶
# "데이터가 정규분포일 것이다"라는 가정은 위험
# 실제 데이터:
# - 소득, 가격: 로그정규 (양수, 오른쪽 꼬리)
# - 대기 시간: 지수 분포
# - 카운트: 포아송, 음이항
# - 금융 수익률: fat tail (t-분포 또는 더 두꺼운 꼬리)
# 항상 시각화 + 검정으로 확인
2. 포아송에서 과산포 무시¶
# 포아송의 핵심: 평균 = 분산
# 실제 카운트 데이터에서는 분산 > 평균인 경우가 많음 (과산포)
data = np.array([0, 0, 0, 1, 2, 0, 15, 0, 0, 3]) # 과산포
print(f"평균: {data.mean():.2f}")
print(f"분산: {data.var():.2f}")
# 과산포 시 → 음이항 분포 사용
3. 연속 분포의 "확률"¶
# 연속 분포에서 P(X = 특정값) = 0 항상!
# PDF 값은 확률이 아니라 확률 밀도
normal = stats.norm(0, 1)
print(f"pdf(0) = {normal.pdf(0):.4f}") # 이건 확률이 아님!
# 확률은 구간으로 계산
print(f"P(-0.1 < X < 0.1) = {normal.cdf(0.1) - normal.cdf(-0.1):.4f}")
4. 베르누이와 이항 혼동¶
# 베르누이: 단일 시행 (0 또는 1)
# 이항: n번 시행의 성공 횟수 (0, 1, ..., n)
# 베르누이(p)의 합 = 이항(n, p)
bernoulli_sum = sum(np.random.binomial(1, 0.3) for _ in range(10))
binomial_sample = np.random.binomial(10, 0.3)
# 두 값은 같은 분포에서 나옴
5. 지수 분포의 rate vs scale¶
# 혼란의 원인: 교재마다 파라미터화가 다름
# rate 파라미터화: f(x) = λ exp(-λx), E[X] = 1/λ
# scale 파라미터화: f(x) = (1/β) exp(-x/β), E[X] = β
# scipy는 scale 사용
lambda_rate = 2
scale = 1 / lambda_rate
exp_dist = stats.expon(scale=scale) # scale = 1/rate
print(f"E[X] = {exp_dist.mean():.4f} (이론값: {1/lambda_rate})")
참고 자료¶
- SciPy Statistical Distributions
- Distribution Explorer - 인터랙티브 분포 탐색
- Probabilistic Machine Learning (Murphy) - 분포와 ML 연결
- Stan Functions Reference - 베이지안 모델링 관점
- Seeing Theory - 시각적 확률/통계 학습