AI/ML을 위한 수학 완전 정복
머신러닝과 딥러닝을 깊이 이해하려면 그 바탕에 깔린 수학을 알아야 합니다. 많은 개발자들이 "왜 이 수식이 이렇게 작동하는가?"라는 질문에 명확한 답을 얻지 못한 채로 프레임워크를 사용합니다. 이 글은 그 공백을 채우기 위한 시도입니다.
단순히 수식을 나열하는 것이 아니라, 각 개념이 **왜** 중요한지, 딥러닝의 어느 부분에서 **어떻게** 쓰이는지를 중심으로 설명합니다. Python/NumPy 코드를 함께 제공하여 수식을 직접 확인할 수 있습니다.
1. 선형대수(Linear Algebra)
선형대수는 딥러닝의 가장 기본적인 언어입니다. 신경망의 순전파(Forward Pass)는 사실 행렬 곱셈의 연속이고, 임베딩은 고차원 벡터 공간의 한 점입니다.
1.1 벡터(Vector)
**벡터의 정의와 기하학적 의미**
벡터는 크기(magnitude)와 방향(direction)을 동시에 가진 양입니다. 수학적으로는 수들의 순서 있는 목록이며, $n$차원 공간의 한 점을 가리키는 화살표로 시각화할 수 있습니다.
$$\mathbf{v} = \begin{bmatrix} v_1 \\ v_2 \\ \vdots \\ v_n \end{bmatrix}$$
딥러닝에서 벡터는 도처에 등장합니다. 입력 데이터, 은닉층의 활성화값, 임베딩 표현 등이 모두 벡터입니다.
**벡터 덧셈과 스칼라 곱**
두 벡터의 덧셈은 성분별(element-wise)로 이루어집니다.
$$\mathbf{a} + \mathbf{b} = \begin{bmatrix} a_1 + b_1 \\ a_2 + b_2 \\ \vdots \\ a_n + b_n \end{bmatrix}$$
스칼라 $c$와 벡터의 곱은 각 성분에 $c$를 곱합니다.
$$c \cdot \mathbf{v} = \begin{bmatrix} c \cdot v_1 \\ c \cdot v_2 \\ \vdots \\ c \cdot v_n \end{bmatrix}$$
**내적(Dot Product)**
내적은 두 벡터의 "유사성"을 측정하는 연산입니다.
$$\mathbf{a} \cdot \mathbf{b} = \sum_{i=1}^{n} a_i b_i = a_1 b_1 + a_2 b_2 + \cdots + a_n b_n$$
기하학적으로, 내적은 두 벡터의 크기와 사이각 $\theta$의 코사인으로도 표현됩니다.
$$\mathbf{a} \cdot \mathbf{b} = \|\mathbf{a}\| \|\mathbf{b}\| \cos\theta$$
내적이 양수이면 두 벡터는 같은 방향을, 음수이면 반대 방향을, 0이면 직각을 이룹니다.
**코사인 유사도(Cosine Similarity)**
두 벡터의 방향 유사성만을 측정하려면 크기로 정규화합니다.
$$\text{cosine similarity}(\mathbf{a}, \mathbf{b}) = \frac{\mathbf{a} \cdot \mathbf{b}}{\|\mathbf{a}\| \|\mathbf{b}\|}$$
값의 범위는 $[-1, 1]$이며, 1이면 완전히 같은 방향, -1이면 완전히 반대 방향을 의미합니다. 자연어 처리에서 단어 임베딩의 유사도를 측정할 때 자주 사용됩니다.
**벡터 노름(Vector Norm)**
노름은 벡터의 "크기(길이)"를 측정하는 함수입니다.
- $L_1$ 노름(맨해튼 거리): $\|\mathbf{v}\|_1 = \sum_i |v_i|$
- $L_2$ 노름(유클리드 거리): $\|\mathbf{v}\|_2 = \sqrt{\sum_i v_i^2}$
- $L_\infty$ 노름(최댓값 노름): $\|\mathbf{v}\|_\infty = \max_i |v_i|$
정규화(Regularization)에서 $L_1$은 희소성(sparsity)을, $L_2$는 가중치의 전반적 크기 축소를 유도합니다.
a = np.array([3.0, 4.0])
b = np.array([1.0, 2.0])
내적
dot_product = np.dot(a, b) # 3*1 + 4*2 = 11
print(f"내적: {dot_product}")
노름
l2_norm_a = np.linalg.norm(a) # sqrt(9+16) = 5
print(f"L2 노름: {l2_norm_a}")
코사인 유사도
cosine_sim = np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
print(f"코사인 유사도: {cosine_sim:.4f}")
다양한 노름
v = np.array([3.0, -4.0, 1.0])
print(f"L1 노름: {np.linalg.norm(v, ord=1)}") # 8.0
print(f"L2 노름: {np.linalg.norm(v, ord=2)}") # 5.099...
print(f"Linf 노름: {np.linalg.norm(v, ord=np.inf)}") # 4.0
1.2 행렬(Matrix)
**행렬 표기법**
행렬은 수들을 직사각형 형태로 배열한 것입니다. $m \times n$ 행렬은 $m$개의 행(row)과 $n$개의 열(column)을 가집니다.
$$A = \begin{bmatrix} a_{11} & a_{12} & \cdots & a_{1n} \\ a_{21} & a_{22} & \cdots & a_{2n} \\ \vdots & \vdots & \ddots & \vdots \\ a_{m1} & a_{m2} & \cdots & a_{mn} \end{bmatrix}$$
딥러닝에서 가중치 행렬 $W$는 레이어 간의 선형 변환을 나타냅니다.
**행렬 곱셈(Matrix Multiplication)**
$A$가 $m \times k$ 행렬이고 $B$가 $k \times n$ 행렬일 때, 곱 $C = AB$는 $m \times n$ 행렬이며:
$$C_{ij} = \sum_{p=1}^{k} A_{ip} B_{pj}$$
핵심은 **앞 행렬의 열 수 = 뒤 행렬의 행 수**가 일치해야 한다는 것입니다. 결과 행렬의 shape은 $(m, n)$입니다.
행렬 곱셈은 교환법칙이 성립하지 않습니다: $AB \neq BA$ (일반적으로).
**전치행렬(Transpose)**
행과 열을 교환한 행렬입니다.
$$(A^T)_{ij} = A_{ji}$$
성질: $(AB)^T = B^T A^T$
**역행렬(Inverse Matrix)**
정방행렬 $A$의 역행렬 $A^{-1}$는 $AA^{-1} = A^{-1}A = I$를 만족합니다. 역행렬이 존재하려면 행렬식이 0이 아니어야 합니다.
**행렬식(Determinant)**
$2 \times 2$ 행렬의 행렬식:
$$\det\begin{pmatrix} a & b \\ c & d \end{pmatrix} = ad - bc$$
행렬식은 해당 행렬이 나타내는 선형 변환이 공간을 얼마나 "늘리거나 줄이는지"를 나타냅니다. $\det(A) = 0$이면 차원이 축소되어 역행렬이 존재하지 않습니다.
**랭크(Rank)**
행렬의 랭크는 선형 독립인 행(또는 열)의 최대 수입니다. 랭크가 낮을수록 행렬이 나타내는 선형 변환의 출력 공간 차원이 낮습니다. Low-rank approximation은 모델 압축(LoRA 등)의 핵심 개념입니다.
A = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
B = np.array([[9, 8, 7],
[6, 5, 4],
[3, 2, 1]])
행렬 곱
C = np.dot(A, B)
print("행렬 곱 결과:\n", C)
전치행렬
print("A의 전치행렬:\n", A.T)
행렬식
A2 = np.array([[3.0, 1.0],
[2.0, 4.0]])
print(f"행렬식: {np.linalg.det(A2):.2f}") # 3*4 - 1*2 = 10
역행렬
A_inv = np.linalg.inv(A2)
print("역행렬:\n", A_inv)
print("A * A_inv (항등행렬):\n", np.round(A2 @ A_inv))
랭크
print(f"A의 랭크: {np.linalg.matrix_rank(A)}") # 2 (종속 행이 있으므로)
1.3 고유값과 고유벡터(Eigenvalues & Eigenvectors)
**정의**
정방행렬 $A$에 대해, 다음 방정식을 만족하는 비영 벡터 $\mathbf{v}$를 **고유벡터(eigenvector)**, 스칼라 $\lambda$를 **고유값(eigenvalue)**이라 합니다.
$$A\mathbf{v} = \lambda \mathbf{v}$$
이 식의 의미는 강력합니다: 행렬 $A$가 벡터 $\mathbf{v}$를 변환할 때, 방향은 변하지 않고 크기만 $\lambda$배 바뀝니다.
**고유값 분해(Eigendecomposition)**
$A$가 대칭 행렬이면 직교하는 고유벡터들로 분해할 수 있습니다.
$$A = Q \Lambda Q^T$$
여기서 $Q$는 고유벡터들을 열로 가지는 직교 행렬이고, $\Lambda$는 고유값들을 대각에 가지는 대각 행렬입니다.
**PCA(주성분 분석)와의 관계**
데이터 행렬 $X$의 공분산 행렬 $\Sigma = \frac{1}{n} X^T X$의 고유벡터들이 주성분(Principal Components)입니다. 가장 큰 고유값에 대응하는 고유벡터가 데이터의 분산이 가장 큰 방향, 즉 가장 중요한 특징 방향입니다.
**SVD(특이값 분해, Singular Value Decomposition)**
고유값 분해는 정방행렬에만 적용되지만, SVD는 모든 행렬에 적용됩니다.
$$A = U \Sigma V^T$$
- $U$: $m \times m$ 직교 행렬 (왼쪽 특이벡터)
- $\Sigma$: $m \times n$ 대각 행렬 (특이값, 0 이상)
- $V^T$: $n \times n$ 직교 행렬 (오른쪽 특이벡터)
SVD는 행렬 근사, 노이즈 제거, 추천 시스템, 자연어 처리의 LSA 등에 핵심적으로 사용됩니다. LoRA(Low-Rank Adaptation)도 SVD의 저랭크 근사 아이디어를 활용합니다.
고유값과 고유벡터
A = np.array([[4.0, 2.0],
[1.0, 3.0]])
eigenvalues, eigenvectors = np.linalg.eig(A)
print("고유값:", eigenvalues)
print("고유벡터 (열 벡터):\n", eigenvectors)
검증: A*v = lambda*v
v0 = eigenvectors[:, 0]
lam0 = eigenvalues[0]
print("A*v:", A @ v0)
print("lambda*v:", lam0 * v0)
SVD
M = np.array([[1, 2, 3],
[4, 5, 6]])
U, S, Vt = np.linalg.svd(M, full_matrices=False)
print(f"U shape: {U.shape}")
print(f"S (특이값): {S}")
print(f"Vt shape: {Vt.shape}")
저랭크 근사 (rank-1 approximation)
rank1_approx = np.outer(S[0] * U[:, 0], Vt[0, :])
print("원본 행렬:\n", M)
print("rank-1 근사:\n", rank1_approx)
PCA 예제
from numpy.linalg import eig
데이터 생성
np.random.seed(42)
data = np.random.randn(100, 2)
data[:, 1] = data[:, 0] * 0.8 + data[:, 1] * 0.2 # 상관관계 추가
공분산 행렬
cov = np.cov(data.T)
eigenvals, eigenvecs = eig(cov)
첫 번째 주성분
idx = np.argsort(eigenvals)[::-1]
principal_component = eigenvecs[:, idx[0]]
print("첫 번째 주성분:", principal_component)
print("설명 분산 비율:", eigenvals[idx[0]] / eigenvals.sum())
1.4 딥러닝과 선형대수
**선형 변환으로서의 레이어**
신경망의 완전 연결 레이어(Fully Connected Layer)는 본질적으로 선형 변환입니다.
$$\mathbf{y} = W\mathbf{x} + \mathbf{b}$$
여기서:
- $\mathbf{x} \in \mathbb{R}^n$: 입력 벡터
- $W \in \mathbb{R}^{m \times n}$: 가중치 행렬
- $\mathbf{b} \in \mathbb{R}^m$: 편향 벡터
- $\mathbf{y} \in \mathbb{R}^m$: 출력 벡터
여러 레이어를 거치는 것은 연속된 선형 변환입니다. 활성화 함수가 없으면 여러 레이어를 쌓아도 단일 선형 변환과 동일하므로, 비선형 활성화 함수(ReLU, Sigmoid 등)가 필수적입니다.
**배치 처리와 행렬 곱셈**
$B$개의 샘플을 동시에 처리할 때:
$$Y = XW^T + \mathbf{b}$$
여기서 $X \in \mathbb{R}^{B \times n}$이고 $Y \in \mathbb{R}^{B \times m}$입니다. GPU는 이런 대규모 행렬 곱셈을 병렬로 처리하는 데 최적화되어 있습니다.
2. 미적분(Calculus)
미적분은 딥러닝에서 "어떻게 배우는가"를 담당합니다. 손실 함수를 최소화하는 경사 하강법은 전적으로 미분에 의존합니다.
2.1 미분(Differentiation)
**극한과 미분의 정의**
함수 $f(x)$의 $x$에서의 미분은 극한으로 정의됩니다.
$$f'(x) = \lim_{h \to 0} \frac{f(x+h) - f(x)}{h}$$
이는 $x$에서 함수의 순간 변화율, 즉 그래프의 접선 기울기입니다.
**기본 미분 규칙**
- 거듭제곱 법칙: $(x^n)' = nx^{n-1}$
- 합 법칙: $(f+g)' = f' + g'$
- 곱 법칙: $(fg)' = f'g + fg'$
- 몫 법칙: $(f/g)' = (f'g - fg') / g^2$
- 연쇄 법칙(Chain Rule): $[f(g(x))]' = f'(g(x)) \cdot g'(x)$
딥러닝에서 자주 쓰이는 함수들의 미분:
- $\frac{d}{dx}(e^x) = e^x$
- $\frac{d}{dx}(\ln x) = \frac{1}{x}$
- $\frac{d}{dx}(\sigma(x)) = \sigma(x)(1 - \sigma(x))$ (Sigmoid)
- $\frac{d}{dx}(\tanh(x)) = 1 - \tanh^2(x)$
- $\frac{d}{dx}(\text{ReLU}(x)) = \begin{cases} 1 & x > 0 \\ 0 & x \leq 0 \end{cases}$
**편미분(Partial Derivative)**
다변수 함수에서 하나의 변수에 대한 미분입니다. 다른 변수들은 상수로 취급합니다.
$$\frac{\partial f}{\partial x_i}$$
예를 들어, $f(x, y) = x^2 + 3xy + y^2$이면:
$$\frac{\partial f}{\partial x} = 2x + 3y, \quad \frac{\partial f}{\partial y} = 3x + 2y$$
**그래디언트(Gradient)**
모든 편미분을 모은 벡터입니다.
$$\nabla f = \begin{bmatrix} \frac{\partial f}{\partial x_1} \\ \frac{\partial f}{\partial x_2} \\ \vdots \\ \frac{\partial f}{\partial x_n} \end{bmatrix}$$
그래디언트는 함수값이 **가장 빠르게 증가하는 방향**을 가리킵니다. 따라서 손실 함수를 최소화하려면 그래디언트의 반대 방향으로 이동합니다(경사 하강법).
**야코비안(Jacobian) 행렬**
벡터 함수 $\mathbf{f}: \mathbb{R}^n \to \mathbb{R}^m$의 모든 1차 편미분을 담은 행렬입니다.
$$J = \begin{bmatrix} \frac{\partial f_1}{\partial x_1} & \cdots & \frac{\partial f_1}{\partial x_n} \\ \vdots & \ddots & \vdots \\ \frac{\partial f_m}{\partial x_1} & \cdots & \frac{\partial f_m}{\partial x_n} \end{bmatrix}$$
**헤시안(Hessian) 행렬**
스칼라 함수의 2차 편미분을 담은 정방 행렬입니다.
$$H_{ij} = \frac{\partial^2 f}{\partial x_i \partial x_j}$$
헤시안의 고유값은 함수의 곡률을 나타냅니다. 모든 고유값이 양수이면 극소, 모두 음수이면 극대, 혼재하면 안장점(Saddle Point)입니다.
수치 미분 (기울기 확인용)
def numerical_gradient(f, x, h=1e-5):
grad = np.zeros_like(x)
for i in range(len(x)):
x_plus = x.copy()
x_plus[i] += h
x_minus = x.copy()
x_minus[i] -= h
grad[i] = (f(x_plus) - f(x_minus)) / (2 * h)
return grad
예제 함수: f(x, y) = x^2 + 2y^2
def f(x):
return x[0]**2 + 2 * x[1]**2
x0 = np.array([3.0, 4.0])
grad = numerical_gradient(f, x0)
print(f"수치 그래디언트: {grad}") # [6.0, 16.0]
print(f"해석적 그래디언트: [{2*x0[0]}, {4*x0[1]}]")
PyTorch를 이용한 자동 미분
try:
x = torch.tensor([3.0, 4.0], requires_grad=True)
y = x[0]**2 + 2 * x[1]**2
y.backward()
print(f"PyTorch 그래디언트: {x.grad}")
except ImportError:
print("PyTorch 미설치")
2.2 체인 룰과 역전파(Backpropagation)
**체인 룰(Chain Rule)**
함수의 합성에서 미분을 계산하는 규칙입니다.
$$\frac{dz}{dx} = \frac{dz}{dy} \cdot \frac{dy}{dx}$$
다변수로 확장하면:
$$\frac{\partial z}{\partial x_i} = \sum_j \frac{\partial z}{\partial y_j} \cdot \frac{\partial y_j}{\partial x_i}$$
**역전파 알고리즘**
신경망의 훈련에서 손실 함수 $L$을 각 가중치 $w$에 대해 미분해야 합니다. 네트워크가 여러 레이어의 합성 함수이므로, 체인 룰을 반복 적용합니다.
2-레이어 신경망의 예:
$$L = \ell(\text{softmax}(W_2 \cdot \text{ReLU}(W_1 \mathbf{x} + \mathbf{b}_1) + \mathbf{b}_2), \mathbf{y})$$
$\frac{\partial L}{\partial W_1}$을 구하려면:
$$\frac{\partial L}{\partial W_1} = \frac{\partial L}{\partial \mathbf{a}_2} \cdot \frac{\partial \mathbf{a}_2}{\partial \mathbf{h}} \cdot \frac{\partial \mathbf{h}}{\partial W_1}$$
역전파는 출력에서 입력 방향으로 이 체인을 효율적으로 계산합니다.
간단한 신경망 역전파 예제
class SimpleNet:
def __init__(self):
self.W1 = np.random.randn(4, 3) * 0.1
self.b1 = np.zeros(4)
self.W2 = np.random.randn(2, 4) * 0.1
self.b2 = np.zeros(2)
def relu(self, x):
return np.maximum(0, x)
def relu_grad(self, x):
return (x > 0).astype(float)
def forward(self, x):
self.x = x
self.z1 = self.W1 @ x + self.b1
self.h = self.relu(self.z1)
self.z2 = self.W2 @ self.h + self.b2
return self.z2
def backward(self, dL_dz2):
레이어 2 그래디언트
dL_dW2 = np.outer(dL_dz2, self.h)
dL_db2 = dL_dz2
dL_dh = self.W2.T @ dL_dz2
ReLU 그래디언트
dL_dz1 = dL_dh * self.relu_grad(self.z1)
레이어 1 그래디언트
dL_dW1 = np.outer(dL_dz1, self.x)
dL_db1 = dL_dz1
return dL_dW1, dL_db1, dL_dW2, dL_db2
net = SimpleNet()
x = np.array([1.0, 2.0, 3.0])
output = net.forward(x)
dL_dout = np.array([1.0, -1.0]) # 손실의 그래디언트
grads = net.backward(dL_dout)
print(f"W1 그래디언트 shape: {grads[0].shape}")
print(f"W2 그래디언트 shape: {grads[2].shape}")
2.3 최적화 이론
**극값 조건**
- 1차 조건(필요 조건): 극값에서 $\nabla f = \mathbf{0}$
- 2차 조건(충분 조건): 헤시안 $H$의 부호로 극소/극대 판단
**볼록 함수(Convex Function)**
함수 $f$가 볼록이면 두 점의 직선보다 함수값이 항상 낮거나 같습니다.
$$f(\lambda x + (1-\lambda)y) \leq \lambda f(x) + (1-\lambda) f(y), \quad \lambda \in [0,1]$$
볼록 함수의 극소는 항상 전역 최솟값입니다. 딥러닝의 손실 함수는 일반적으로 볼록하지 않아서 전역 최솟값을 보장할 수 없습니다.
**경사 하강법(Gradient Descent)**
$$\theta_{t+1} = \theta_t - \alpha \nabla_\theta L(\theta_t)$$
여기서 $\alpha$는 학습률(learning rate)입니다. 학습률이 너무 크면 발산하고, 너무 작으면 수렴이 느립니다.
변형들:
- **확률적 경사 하강법(SGD)**: 매 스텝마다 무작위 샘플 하나(또는 미니배치)를 사용
- **모멘텀(Momentum)**: 이전 그래디언트 방향을 누적하여 가속
- **Adam**: 적응적 학습률, 현재 딥러닝에서 가장 널리 쓰임
$$m_t = \beta_1 m_{t-1} + (1-\beta_1) g_t$$
$$v_t = \beta_2 v_{t-1} + (1-\beta_2) g_t^2$$
$$\theta_{t+1} = \theta_t - \frac{\alpha}{\sqrt{\hat{v}_t} + \epsilon} \hat{m}_t$$
경사 하강법 구현
def gradient_descent(grad_f, init_x, learning_rate=0.1, steps=100):
x = init_x.copy()
history = [x.copy()]
for _ in range(steps):
grad = grad_f(x)
x -= learning_rate * grad
history.append(x.copy())
return x, np.array(history)
예제: f(x, y) = x^2 + 4y^2 최소화
def f(x):
return x[0]**2 + 4 * x[1]**2
def grad_f(x):
return np.array([2*x[0], 8*x[1]])
init_x = np.array([3.0, 3.0])
final_x, history = gradient_descent(grad_f, init_x, learning_rate=0.1, steps=50)
print(f"시작점: {init_x}")
print(f"수렴 지점: {final_x}")
print(f"최솟값: {f(final_x):.6f}")
Adam 옵티마이저 구현
def adam(grad_f, init_x, alpha=0.01, beta1=0.9, beta2=0.999, eps=1e-8, steps=100):
x = init_x.copy()
m = np.zeros_like(x)
v = np.zeros_like(x)
for t in range(1, steps + 1):
g = grad_f(x)
m = beta1 * m + (1 - beta1) * g
v = beta2 * v + (1 - beta2) * g**2
m_hat = m / (1 - beta1**t)
v_hat = v / (1 - beta2**t)
x -= alpha * m_hat / (np.sqrt(v_hat) + eps)
return x
final_adam = adam(grad_f, init_x)
print(f"Adam 수렴 지점: {final_adam}")
3. 확률과 통계(Probability & Statistics)
확률과 통계는 딥러닝의 불확실성을 다루는 언어입니다. 손실 함수의 설계, 정규화, 모델 평가 모두 확률적 사고에 기반합니다.
3.1 확률 기초
**확률 공리(Kolmogorov Axioms)**
1. $P(A) \geq 0$ (비음성)
2. $P(\Omega) = 1$ (전체 확률은 1)
3. 서로소인 사건들에 대해 $P(A \cup B) = P(A) + P(B)$ (가산 가법성)
**조건부 확률**
사건 $B$가 발생했다는 조건 하에 사건 $A$의 확률:
$$P(A|B) = \frac{P(A \cap B)}{P(B)}, \quad P(B) > 0$$
**베이즈 정리(Bayes' Theorem)**
$$P(A|B) = \frac{P(B|A) \cdot P(A)}{P(B)}$$
베이즈 정리는 "증거를 바탕으로 믿음을 업데이트"하는 원리입니다.
- $P(A)$: 사전 확률(Prior) - 증거 이전의 믿음
- $P(B|A)$: 우도(Likelihood) - 가설 A 하에서 증거 B의 확률
- $P(A|B)$: 사후 확률(Posterior) - 증거를 본 후 업데이트된 믿음
이는 분류 모델 평가, 나이브 베이즈 분류기, 베이지안 신경망 등에 광범위하게 사용됩니다.
**확률의 전체 법칙**
$$P(B) = \sum_i P(B|A_i) P(A_i)$$
3.2 확률 분포
**이산 확률 분포**
_베르누이 분포(Bernoulli Distribution)_
결과가 0 또는 1인 단일 시행:
$$P(X=k) = p^k (1-p)^{1-k}, \quad k \in \{0, 1\}$$
$E[X] = p$, $Var(X) = p(1-p)$
_이항 분포(Binomial Distribution)_
$n$번의 베르누이 시행에서 성공 횟수:
$$P(X=k) = \binom{n}{k} p^k (1-p)^{n-k}$$
_포아송 분포(Poisson Distribution)_
단위 시간/공간에서 사건 발생 횟수:
$$P(X=k) = \frac{\lambda^k e^{-\lambda}}{k!}$$
$E[X] = Var(X) = \lambda$
**연속 확률 분포**
_균등 분포(Uniform Distribution)_
구간 $[a, b]$에서 모든 값이 동일한 확률 밀도:
$$f(x) = \frac{1}{b-a}, \quad a \leq x \leq b$$
_정규 분포(Normal/Gaussian Distribution)_
$$f(x) = \frac{1}{\sigma\sqrt{2\pi}} \exp\left(-\frac{(x-\mu)^2}{2\sigma^2}\right)$$
평균 $\mu$, 표준편차 $\sigma$로 특징지어집니다. 중심 극한 정리(Central Limit Theorem)에 의해 많은 자연 현상이 정규 분포를 따릅니다. 딥러닝에서 가중치 초기화, 노이즈 모델링 등에 자주 사용됩니다.
_다변량 정규 분포(Multivariate Normal Distribution)_
$$f(\mathbf{x}) = \frac{1}{(2\pi)^{d/2}|\Sigma|^{1/2}} \exp\left(-\frac{1}{2}(\mathbf{x}-\boldsymbol{\mu})^T \Sigma^{-1} (\mathbf{x}-\boldsymbol{\mu})\right)$$
여기서 $\boldsymbol{\mu}$는 평균 벡터, $\Sigma$는 공분산 행렬입니다. VAE(Variational Autoencoder)의 잠재 공간 모델링에 핵심적으로 사용됩니다.
from scipy import stats
정규 분포
mu, sigma = 0, 1
x = np.linspace(-4, 4, 100)
pdf = stats.norm.pdf(x, mu, sigma)
print(f"P(-1 < X < 1) = {stats.norm.cdf(1) - stats.norm.cdf(-1):.4f}") # 68.27%
print(f"P(-2 < X < 2) = {stats.norm.cdf(2) - stats.norm.cdf(-2):.4f}") # 95.45%
print(f"P(-3 < X < 3) = {stats.norm.cdf(3) - stats.norm.cdf(-3):.4f}") # 99.73%
다변량 정규 분포 샘플링
mean = np.array([0, 0])
cov = np.array([[1, 0.7],
[0.7, 1]])
samples = np.random.multivariate_normal(mean, cov, size=1000)
print(f"샘플 형태: {samples.shape}")
print(f"샘플 평균: {samples.mean(axis=0)}")
print(f"샘플 공분산:\n{np.cov(samples.T)}")
이항 분포
n, p = 10, 0.5
k = np.arange(0, n+1)
pmf = stats.binom.pmf(k, n, p)
print(f"\n이항 분포 B(10, 0.5):")
print(f"P(X=5) = {stats.binom.pmf(5, n, p):.4f}")
3.3 기댓값과 분산
**기댓값(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{(연속)}$$
기댓값의 선형성: $E[aX + b] = aE[X] + b$
**분산(Variance)**
$$Var(X) = E[(X - \mu)^2] = E[X^2] - (E[X])^2$$
표준편차: $\sigma = \sqrt{Var(X)}$
**공분산(Covariance)**
두 확률변수의 선형 관계를 측정합니다.
$$Cov(X, Y) = E[(X - \mu_X)(Y - \mu_Y)] = E[XY] - E[X]E[Y]$$
**상관계수(Correlation)**
$$\rho_{XY} = \frac{Cov(X, Y)}{\sigma_X \sigma_Y}$$
값의 범위는 $[-1, 1]$이며, 크기와 무관하게 선형 관계의 강도만을 측정합니다.
**공분산 행렬(Covariance Matrix)**
$n$차원 확률벡터의 모든 쌍의 공분산:
$$\Sigma_{ij} = Cov(X_i, X_j)$$
PCA의 핵심 입력이며, 데이터의 분산 구조를 담고 있습니다.
3.4 최대우도추정(Maximum Likelihood Estimation, MLE)
**우도함수(Likelihood Function)**
파라미터 $\theta$가 주어졌을 때, 관측된 데이터 $\mathcal{D} = \{x_1, ..., x_n\}$가 발생할 확률:
$$L(\theta; \mathcal{D}) = P(\mathcal{D}|\theta) = \prod_{i=1}^n P(x_i|\theta)$$
MLE는 이 우도를 최대화하는 $\theta$를 찾습니다.
$$\hat{\theta}_{MLE} = \arg\max_\theta L(\theta; \mathcal{D})$$
**로그 우도(Log-Likelihood)**
곱셈을 덧셈으로 변환하여 계산을 쉽게 합니다.
$$\log L(\theta) = \sum_{i=1}^n \log P(x_i|\theta)$$
로그는 단조 증가 함수이므로 로그 우도를 최대화하는 $\theta$와 우도를 최대화하는 $\theta$는 동일합니다.
**MLE 유도 예제: 정규 분포**
데이터 $\{x_1, ..., x_n\}$이 $\mathcal{N}(\mu, \sigma^2)$를 따른다고 가정할 때:
$$\log L(\mu, \sigma^2) = -\frac{n}{2}\log(2\pi\sigma^2) - \frac{1}{2\sigma^2}\sum_{i=1}^n (x_i - \mu)^2$$
$\mu$에 대해 미분하고 0으로 놓으면: $\hat{\mu}_{MLE} = \bar{x} = \frac{1}{n}\sum x_i$ (표본 평균)
$\sigma^2$에 대해 미분하면: $\hat{\sigma}^2_{MLE} = \frac{1}{n}\sum (x_i - \bar{x})^2$ (표본 분산)
**딥러닝의 Cross-Entropy와 MLE**
분류 문제에서 Cross-Entropy 손실을 최소화하는 것은 MLE와 동치입니다.
$$\mathcal{L}_{CE} = -\frac{1}{n}\sum_{i=1}^n \sum_{c=1}^C y_{ic} \log \hat{p}_{ic}$$
이는 실제 데이터 분포 하에서 모델의 로그 우도를 최대화하는 것과 같습니다.
from scipy.optimize import minimize_scalar
MLE 예제: 베르누이 분포
data = np.array([1, 0, 1, 1, 0, 1, 1, 1, 0, 1]) # 동전 던지기
n = len(data)
k = data.sum()
해석적 MLE: p_hat = k/n
p_mle = k / n
print(f"MLE 추정값: p = {p_mle:.2f}") # 0.7
로그 우도 함수
def neg_log_likelihood(p):
if p <= 0 or p >= 1:
return np.inf
return -(k * np.log(p) + (n - k) * np.log(1 - p))
result = minimize_scalar(neg_log_likelihood, bounds=(0.01, 0.99), method='bounded')
print(f"수치 최적화 MLE: p = {result.x:.4f}")
Cross-Entropy 손실 구현
def cross_entropy(y_true, y_pred, eps=1e-15):
y_pred = np.clip(y_pred, eps, 1 - eps)
return -np.mean(y_true * np.log(y_pred) + (1 - y_true) * np.log(1 - y_pred))
이진 분류 예제
y_true = np.array([1, 0, 1, 1, 0])
y_pred = np.array([0.9, 0.1, 0.8, 0.7, 0.2])
loss = cross_entropy(y_true, y_pred)
print(f"\nCross-Entropy 손실: {loss:.4f}")
3.5 정보 이론(Information Theory)
**엔트로피(Entropy)**
확률 분포 $P$의 불확실성 또는 정보량:
$$H(X) = -\sum_{x} P(x) \log P(x) = -E[\log P(X)]$$
균등 분포일 때 엔트로피가 최대이고, 하나의 결과만 가능할 때 0입니다.
**KL Divergence**
두 분포 $P$와 $Q$ 사이의 "거리"(단, 비대칭):
$$D_{KL}(P \| Q) = \sum_x P(x) \log \frac{P(x)}{Q(x)} = E_P\left[\log \frac{P(X)}{Q(X)}\right]$$
$D_{KL}(P \| Q) \geq 0$이며 $P = Q$일 때만 0입니다. VAE의 손실 함수, 정책 최적화(PPO 등)에 사용됩니다.
**크로스 엔트로피(Cross-Entropy)**
실제 분포 $P$와 모델 분포 $Q$ 사이의 크로스 엔트로피:
$$H(P, Q) = -\sum_x P(x) \log Q(x) = H(P) + D_{KL}(P \| Q)$$
$H(P)$는 상수이므로 Cross-Entropy를 최소화하는 것은 KL Divergence를 최소화하는 것과 동치입니다.
**상호 정보량(Mutual Information)**
두 확률변수가 공유하는 정보:
$$I(X; Y) = H(X) - H(X|Y) = H(Y) - H(Y|X)$$
특징 선택, 표현 학습의 이론적 기반이 됩니다.
def entropy(p):
"""이산 분포의 엔트로피 계산"""
p = np.array(p)
p = p[p > 0] # 0 제거 (0*log(0) = 0으로 취급)
return -np.sum(p * np.log2(p))
def kl_divergence(p, q, eps=1e-15):
"""KL Divergence D_KL(P||Q) 계산"""
p = np.array(p) + eps
q = np.array(q) + eps
p = p / p.sum()
q = q / q.sum()
return np.sum(p * np.log(p / q))
def cross_entropy_distributions(p, q, eps=1e-15):
"""분포 간 Cross-Entropy 계산"""
p = np.array(p)
q = np.array(q) + eps
return -np.sum(p * np.log(q))
균등 분포 (최대 엔트로피)
uniform = [0.25, 0.25, 0.25, 0.25]
print(f"균등 분포 엔트로피: {entropy(uniform):.4f} bits") # 2.0 bits
집중된 분포 (낮은 엔트로피)
concentrated = [0.97, 0.01, 0.01, 0.01]
print(f"집중 분포 엔트로피: {entropy(concentrated):.4f} bits")
KL Divergence
true_dist = [0.4, 0.3, 0.2, 0.1]
model_dist = [0.35, 0.35, 0.15, 0.15]
kl = kl_divergence(true_dist, model_dist)
print(f"\nKL Divergence: {kl:.4f}")
Cross-Entropy = H(P) + KL(P||Q)
ce = cross_entropy_distributions(true_dist, model_dist)
h_p = entropy(true_dist) * np.log(2) # nats로 변환
print(f"Cross-Entropy: {ce:.4f}")
print(f"H(P) + KL: {h_p + kl:.4f}")
4. 수치 해석(Numerical Methods)
딥러닝 구현에서 수학적 이론과 실제 컴퓨터 연산 사이의 간극을 이해하는 것은 중요합니다.
4.1 부동소수점 표현
컴퓨터에서 실수는 부동소수점으로 표현됩니다.
**FP32 (단정밀도)**
- 32비트: 부호 1비트, 지수 8비트, 가수 23비트
- 정밀도: 약 7자리 십진수
- 범위: 약 $\pm 3.4 \times 10^{38}$
**FP16 (반정밀도)**
- 16비트: 부호 1비트, 지수 5비트, 가수 10비트
- 정밀도: 약 3자리 십진수
- GPU 메모리 절약, 속도 향상 (Mixed Precision Training)
- 단점: 오버플로우/언더플로우 위험
**BF16 (Brain Float 16)**
- 16비트: 부호 1비트, 지수 8비트, 가수 7비트
- FP32와 같은 지수 범위 (오버플로우 위험 낮음)
- Google TPU에서 도입, 현재 AI 훈련에 널리 사용
부동소수점 정밀도 이슈
a = np.float32(0.1)
b = np.float32(0.2)
print(f"0.1 + 0.2 = {a + b}") # 정확히 0.3이 아닐 수 있음
수치 오버플로우
x_large = np.float16(65000)
print(f"FP16 큰 값: {x_large}")
overflow = np.float16(70000)
print(f"FP16 오버플로우: {overflow}") # inf
numpy dtype 비교
for dtype in [np.float16, np.float32, np.float64]:
info = np.finfo(dtype)
print(f"{dtype.__name__}: max={info.max:.3e}, min_pos={info.tiny:.3e}")
4.2 수치 안정성
**Softmax의 수치 불안정성과 해결**
Softmax 함수:
$$\text{softmax}(x_i) = \frac{e^{x_i}}{\sum_j e^{x_j}}$$
큰 값이 입력되면 $e^{x_i}$가 오버플로우를 일으킵니다.
**안정적인 Softmax 구현**:
$$\text{softmax}(x_i) = \frac{e^{x_i - \max(x)}}{\sum_j e^{x_j - \max(x)}}$$
수학적으로 동치이지만 수치적으로 훨씬 안정합니다.
def naive_softmax(x):
"""불안정한 Softmax"""
return np.exp(x) / np.sum(np.exp(x))
def stable_softmax(x):
"""수치적으로 안정한 Softmax"""
x_shifted = x - np.max(x) # 최댓값을 빼서 안정화
return np.exp(x_shifted) / np.sum(np.exp(x_shifted))
일반적인 경우
x_normal = np.array([1.0, 2.0, 3.0])
print("일반 입력:")
print(f" Naive: {naive_softmax(x_normal)}")
print(f" Stable: {stable_softmax(x_normal)}")
큰 값의 경우
x_large = np.array([1000.0, 2000.0, 3000.0])
print("\n큰 입력:")
try:
result = naive_softmax(x_large)
print(f" Naive: {result}")
except RuntimeWarning as e:
print(f" Naive: 오버플로우 경고!")
print(f" Stable: {stable_softmax(x_large)}")
Log-Softmax (Cross-Entropy와 함께 사용)
def log_softmax(x):
x_shifted = x - np.max(x)
return x_shifted - np.log(np.sum(np.exp(x_shifted)))
logits = np.array([2.0, 1.0, 0.1])
print(f"\nLog-Softmax: {log_softmax(logits)}")
5. 선형 회귀의 수학적 분석
선형 회귀는 딥러닝의 기본 블록입니다. 수학적으로 철저히 이해하면 더 복잡한 모델을 이해하는 기초가 됩니다.
5.1 최소제곱법(Ordinary Least Squares)
데이터 $\{(\mathbf{x}_i, y_i)\}_{i=1}^n$에 대해 선형 모델 $\hat{y} = \mathbf{w}^T \mathbf{x} + b$를 학습합니다.
**손실 함수(Mean Squared Error)**:
$$L(\mathbf{w}) = \frac{1}{n} \sum_{i=1}^n (y_i - \hat{y}_i)^2 = \frac{1}{n} \|y - X\mathbf{w}\|^2$$
**정규 방정식(Normal Equation)**
행렬 형태로 손실 함수를 미분하고 0으로 놓으면:
$$\nabla_\mathbf{w} L = -\frac{2}{n} X^T (y - X\mathbf{w}) = 0$$
$$\Rightarrow X^T X \mathbf{w} = X^T y$$
$$\Rightarrow \hat{\mathbf{w}} = (X^T X)^{-1} X^T y$$
이것이 최소제곱 해입니다. 행렬 $X^T X$가 가역(invertible)이어야 하며, 이는 특징들이 선형 독립일 때 성립합니다.
5.2 Ridge와 Lasso 회귀
**Ridge 회귀(L2 정규화)**
$$L_{ridge}(\mathbf{w}) = \|y - X\mathbf{w}\|^2 + \lambda \|\mathbf{w}\|_2^2$$
정규 방정식: $\hat{\mathbf{w}} = (X^T X + \lambda I)^{-1} X^T y$
$\lambda I$를 더하면 $X^T X$가 항상 가역이 됩니다. 가중치들이 0에 가깝게 유지되어 과적합을 방지합니다.
**Lasso 회귀(L1 정규화)**
$$L_{lasso}(\mathbf{w}) = \|y - X\mathbf{w}\|^2 + \lambda \|\mathbf{w}\|_1$$
L1 정규화는 일부 가중치를 정확히 0으로 만드는 희소 해(sparse solution)를 유도합니다. 이는 자동 특징 선택(feature selection)의 효과가 있습니다.
from sklearn.linear_model import LinearRegression, Ridge, Lasso
데이터 생성
np.random.seed(42)
n, d = 100, 10
X = np.random.randn(n, d)
true_w = np.array([1.0, -2.0, 3.0, 0.0, 0.0, 0.0, 0.5, -0.5, 0.0, 0.0])
y = X @ true_w + np.random.randn(n) * 0.5
OLS (정규 방정식)
X_b = np.column_stack([np.ones(n), X]) # 편향 항 추가
w_ols = np.linalg.lstsq(X_b, y, rcond=None)[0]
print("OLS 계수 (편향 제외):", w_ols[1:].round(2))
Ridge
ridge = Ridge(alpha=1.0)
ridge.fit(X, y)
print("Ridge 계수:", ridge.coef_.round(2))
Lasso (희소 해)
lasso = Lasso(alpha=0.1)
lasso.fit(X, y)
print("Lasso 계수:", lasso.coef_.round(2))
print("0이 된 계수 수:", (np.abs(lasso.coef_) < 0.01).sum())
정규 방정식 직접 구현
def ridge_normal_equation(X, y, lam=1.0):
n, d = X.shape
A = X.T @ X + lam * np.eye(d)
b = X.T @ y
return np.linalg.solve(A, b)
w_ridge = ridge_normal_equation(X, y, lam=1.0)
print("\n정규 방정식 Ridge:", w_ridge.round(2))
5.3 기울기의 기하학적 의미
**등고선과 그래디언트의 관계**
그래디언트 $\nabla f(\mathbf{x})$는 항상 등고선에 수직(직교)합니다. 경사 하강법에서 이 방향의 반대로 이동하면 가장 빠르게 함수값을 줄일 수 있습니다.
**정규화의 기하학적 해석**
Ridge 회귀는 $\|\mathbf{w}\|_2^2 \leq t$ 구 안에서 MSE를 최소화하는 것과 동치입니다. 원형 제약 조건이므로 희소 해를 만들지 않습니다.
Lasso는 $\|\mathbf{w}\|_1 \leq t$ 다이아몬드 모양 안에서 MSE를 최소화합니다. 다이아몬드의 꼭짓점에서 해가 자주 발생하므로 희소 해가 나타납니다.
6. 종합: 딥러닝 수학 통합
6.1 신경망의 수학적 관점
완전 연결 신경망 $L$개 레이어:
$$\mathbf{h}^{(0)} = \mathbf{x}$$
$$\mathbf{z}^{(l)} = W^{(l)} \mathbf{h}^{(l-1)} + \mathbf{b}^{(l)}$$
$$\mathbf{h}^{(l)} = \sigma(\mathbf{z}^{(l)})$$
$$\hat{y} = \mathbf{h}^{(L)}$$
**순전파**: 선형대수(행렬 곱) + 활성화 함수
**손실 계산**: 정보 이론(Cross-Entropy) or 통계(MSE)
**역전파**: 미적분(체인 룰) → 그래디언트 계산
**파라미터 업데이트**: 최적화 이론(경사 하강법, Adam)
6.2 Attention 메커니즘과 행렬 연산
트랜스포머의 Attention:
$$\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V$$
- $Q, K, V$: 쿼리, 키, 값 행렬 (선형대수)
- $\frac{1}{\sqrt{d_k}}$ 스케일링: 수치 안정성 (수치 해석)
- Softmax: 확률 분포 변환 (확률 이론)
- 내적 $QK^T$: 유사도 측정 (벡터 내적)
def attention(Q, K, V, mask=None):
"""
Scaled Dot-Product Attention
Q, K, V: (seq_len, d_k) 또는 (batch, heads, seq_len, d_k)
"""
d_k = Q.shape[-1]
스케일된 내적 유사도
scores = Q @ K.T / np.sqrt(d_k) # (seq_len, seq_len)
마스킹 (선택적)
if mask is not None:
scores = np.where(mask, scores, -1e9)
Softmax로 확률 분포 변환
attention_weights = stable_softmax_2d(scores)
가중합
output = attention_weights @ V
return output, attention_weights
def stable_softmax_2d(x):
x_max = x.max(axis=-1, keepdims=True)
x_shifted = x - x_max
exp_x = np.exp(x_shifted)
return exp_x / exp_x.sum(axis=-1, keepdims=True)
예제: 3개 토큰, d_k=4
seq_len, d_k = 3, 4
Q = np.random.randn(seq_len, d_k)
K = np.random.randn(seq_len, d_k)
V = np.random.randn(seq_len, d_k)
output, weights = attention(Q, K, V)
print(f"Attention 출력 shape: {output.shape}")
print(f"Attention 가중치 (합 = 1):\n{weights.round(3)}")
print(f"각 행의 합: {weights.sum(axis=-1)}")
마무리: 다음 단계
이 가이드에서는 AI/ML의 핵심 수학 기초를 살펴봤습니다. 정리하면:
1. **선형대수**: 데이터와 모델 파라미터를 표현하고 변환하는 언어
2. **미적분**: 모델이 "어떻게 배우는가"를 설명하는 도구
3. **확률과 통계**: 불확실성을 다루고, 손실 함수의 이론적 근거를 제공
4. **수치 해석**: 이론을 실제 컴퓨터에서 안정적으로 구현하기 위한 지식
다음 학습 단계를 추천합니다:
- **Gilbert Strang의 선형대수**: MIT OCW에서 무료 제공
- **Pattern Recognition and Machine Learning (Bishop)**: 확률론적 관점의 ML
- **Deep Learning (Goodfellow et al.)**: 딥러닝 이론의 바이블
- **실습**: NumPy로 신경망 바닥부터 구현해보기
수학은 이해의 도구입니다. 모든 수식을 외울 필요는 없지만, 각 개념이 **왜** 존재하는지, 딥러닝의 어느 부분에서 **어떻게** 작동하는지를 이해하면 훨씬 강력한 실무 능력을 갖출 수 있습니다.
참고 자료
- Gilbert Strang, _Introduction to Linear Algebra_, 6th Edition
- Ian Goodfellow et al., _Deep Learning_, MIT Press (2016)
- Christopher Bishop, _Pattern Recognition and Machine Learning_, Springer (2006)
- 3Blue1Brown YouTube - Essence of Linear Algebra, Essence of Calculus
- fast.ai - Practical Deep Learning for Coders (수학 직관 설명)
현재 단락 (1/595)
머신러닝과 딥러닝을 깊이 이해하려면 그 바탕에 깔린 수학을 알아야 합니다. 많은 개발자들이 "왜 이 수식이 이렇게 작동하는가?"라는 질문에 명확한 답을 얻지 못한 채로 프레임워크를...