LabHub

블로그

AI/ML을 위한 수학 완전 정복: 선형대수, 미적분, 확률통계

한국어English日本語中文

AI/ML을 위한 수학 완전 정복

머신러닝과 딥러닝을 깊이 이해하려면 그 바탕에 깔린 수학을 알아야 합니다. 많은 개발자들이 "왜 이 수식이 이렇게 작동하는가?"라는 질문에 명확한 답을 얻지 못한 채로 프레임워크를 사용합니다. 이 글은 그 공백을 채우기 위한 시도입니다.

단순히 수식을 나열하는 것이 아니라, 각 개념이 중요한지, 딥러닝의 어느 부분에서 어떻게 쓰이는지를 중심으로 설명합니다. Python/NumPy 코드를 함께 제공하여 수식을 직접 확인할 수 있습니다.


1. 선형대수(Linear Algebra)

선형대수는 딥러닝의 가장 기본적인 언어입니다. 신경망의 순전파(Forward Pass)는 사실 행렬 곱셈의 연속이고, 임베딩은 고차원 벡터 공간의 한 점입니다.

1.1 벡터(Vector)

벡터의 정의와 기하학적 의미

벡터는 크기(magnitude)와 방향(direction)을 동시에 가진 양입니다. 수학적으로는 수들의 순서 있는 목록이며, nn차원 공간의 한 점을 가리키는 화살표로 시각화할 수 있습니다.

v=[v1v2vn]\mathbf{v} = \begin{bmatrix} v_1 \\ v_2 \\ \vdots \\ v_n \end{bmatrix}

딥러닝에서 벡터는 도처에 등장합니다. 입력 데이터, 은닉층의 활성화값, 임베딩 표현 등이 모두 벡터입니다.

벡터 덧셈과 스칼라 곱

두 벡터의 덧셈은 성분별(element-wise)로 이루어집니다.

a+b=[a1+b1a2+b2an+bn]\mathbf{a} + \mathbf{b} = \begin{bmatrix} a_1 + b_1 \\ a_2 + b_2 \\ \vdots \\ a_n + b_n \end{bmatrix}

스칼라 cc와 벡터의 곱은 각 성분에 cc를 곱합니다.

cv=[cv1cv2cvn]c \cdot \mathbf{v} = \begin{bmatrix} c \cdot v_1 \\ c \cdot v_2 \\ \vdots \\ c \cdot v_n \end{bmatrix}

내적(Dot Product)

내적은 두 벡터의 "유사성"을 측정하는 연산입니다.

ab=i=1naibi=a1b1+a2b2++anbn\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의 코사인으로도 표현됩니다.

ab=abcosθ\mathbf{a} \cdot \mathbf{b} = \|\mathbf{a}\| \|\mathbf{b}\| \cos\theta

내적이 양수이면 두 벡터는 같은 방향을, 음수이면 반대 방향을, 0이면 직각을 이룹니다.

코사인 유사도(Cosine Similarity)

두 벡터의 방향 유사성만을 측정하려면 크기로 정규화합니다.

cosine similarity(a,b)=abab\text{cosine similarity}(\mathbf{a}, \mathbf{b}) = \frac{\mathbf{a} \cdot \mathbf{b}}{\|\mathbf{a}\| \|\mathbf{b}\|}

값의 범위는 [1,1][-1, 1]이며, 1이면 완전히 같은 방향, -1이면 완전히 반대 방향을 의미합니다. 자연어 처리에서 단어 임베딩의 유사도를 측정할 때 자주 사용됩니다.

벡터 노름(Vector Norm)

노름은 벡터의 "크기(길이)"를 측정하는 함수입니다.

정규화(Regularization)에서 L1L_1은 희소성(sparsity)을, L2L_2는 가중치의 전반적 크기 축소를 유도합니다.

import numpy as np

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×nm \times n 행렬은 mm개의 행(row)과 nn개의 열(column)을 가집니다.

A=[a11a12a1na21a22a2nam1am2amn]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}

딥러닝에서 가중치 행렬 WW는 레이어 간의 선형 변환을 나타냅니다.

행렬 곱셈(Matrix Multiplication)

AAm×km \times k 행렬이고 BBk×nk \times n 행렬일 때, 곱 C=ABC = ABm×nm \times n 행렬이며:

Cij=p=1kAipBpjC_{ij} = \sum_{p=1}^{k} A_{ip} B_{pj}

핵심은 앞 행렬의 열 수 = 뒤 행렬의 행 수가 일치해야 한다는 것입니다. 결과 행렬의 shape은 (m,n)(m, n)입니다.

행렬 곱셈은 교환법칙이 성립하지 않습니다: ABBAAB \neq BA (일반적으로).

전치행렬(Transpose)

행과 열을 교환한 행렬입니다.

(AT)ij=Aji(A^T)_{ij} = A_{ji}

성질: (AB)T=BTAT(AB)^T = B^T A^T

역행렬(Inverse Matrix)

정방행렬 AA의 역행렬 A1A^{-1}AA1=A1A=IAA^{-1} = A^{-1}A = I를 만족합니다. 역행렬이 존재하려면 행렬식이 0이 아니어야 합니다.

행렬식(Determinant)

2×22 \times 2 행렬의 행렬식:

det(abcd)=adbc\det\begin{pmatrix} a & b \\ c & d \end{pmatrix} = ad - bc

행렬식은 해당 행렬이 나타내는 선형 변환이 공간을 얼마나 "늘리거나 줄이는지"를 나타냅니다. det(A)=0\det(A) = 0이면 차원이 축소되어 역행렬이 존재하지 않습니다.

랭크(Rank)

행렬의 랭크는 선형 독립인 행(또는 열)의 최대 수입니다. 랭크가 낮을수록 행렬이 나타내는 선형 변환의 출력 공간 차원이 낮습니다. Low-rank approximation은 모델 압축(LoRA 등)의 핵심 개념입니다.

import numpy as np

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)

정의

정방행렬 AA에 대해, 다음 방정식을 만족하는 비영 벡터 v\mathbf{v}고유벡터(eigenvector), 스칼라 λ\lambda고유값(eigenvalue)이라 합니다.

Av=λvA\mathbf{v} = \lambda \mathbf{v}

이 식의 의미는 강력합니다: 행렬 AA가 벡터 v\mathbf{v}를 변환할 때, 방향은 변하지 않고 크기만 λ\lambda배 바뀝니다.

고유값 분해(Eigendecomposition)

AA가 대칭 행렬이면 직교하는 고유벡터들로 분해할 수 있습니다.

A=QΛQTA = Q \Lambda Q^T

여기서 QQ는 고유벡터들을 열로 가지는 직교 행렬이고, Λ\Lambda는 고유값들을 대각에 가지는 대각 행렬입니다.

PCA(주성분 분석)와의 관계

데이터 행렬 XX의 공분산 행렬 Σ=1nXTX\Sigma = \frac{1}{n} X^T X의 고유벡터들이 주성분(Principal Components)입니다. 가장 큰 고유값에 대응하는 고유벡터가 데이터의 분산이 가장 큰 방향, 즉 가장 중요한 특징 방향입니다.

SVD(특이값 분해, Singular Value Decomposition)

고유값 분해는 정방행렬에만 적용되지만, SVD는 모든 행렬에 적용됩니다.

A=UΣVTA = U \Sigma V^T

SVD는 행렬 근사, 노이즈 제거, 추천 시스템, 자연어 처리의 LSA 등에 핵심적으로 사용됩니다. LoRA(Low-Rank Adaptation)도 SVD의 저랭크 근사 아이디어를 활용합니다.

import numpy as np

# 고유값과 고유벡터
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)는 본질적으로 선형 변환입니다.

y=Wx+b\mathbf{y} = W\mathbf{x} + \mathbf{b}

여기서:

여러 레이어를 거치는 것은 연속된 선형 변환입니다. 활성화 함수가 없으면 여러 레이어를 쌓아도 단일 선형 변환과 동일하므로, 비선형 활성화 함수(ReLU, Sigmoid 등)가 필수적입니다.

배치 처리와 행렬 곱셈

BB개의 샘플을 동시에 처리할 때:

Y=XWT+bY = XW^T + \mathbf{b}

여기서 XRB×nX \in \mathbb{R}^{B \times n}이고 YRB×mY \in \mathbb{R}^{B \times m}입니다. GPU는 이런 대규모 행렬 곱셈을 병렬로 처리하는 데 최적화되어 있습니다.


2. 미적분(Calculus)

미적분은 딥러닝에서 "어떻게 배우는가"를 담당합니다. 손실 함수를 최소화하는 경사 하강법은 전적으로 미분에 의존합니다.

2.1 미분(Differentiation)

극한과 미분의 정의

함수 f(x)f(x)xx에서의 미분은 극한으로 정의됩니다.

f(x)=limh0f(x+h)f(x)hf'(x) = \lim_{h \to 0} \frac{f(x+h) - f(x)}{h}

이는 xx에서 함수의 순간 변화율, 즉 그래프의 접선 기울기입니다.

기본 미분 규칙

딥러닝에서 자주 쓰이는 함수들의 미분:

편미분(Partial Derivative)

다변수 함수에서 하나의 변수에 대한 미분입니다. 다른 변수들은 상수로 취급합니다.

fxi\frac{\partial f}{\partial x_i}

예를 들어, f(x,y)=x2+3xy+y2f(x, y) = x^2 + 3xy + y^2이면:

fx=2x+3y,fy=3x+2y\frac{\partial f}{\partial x} = 2x + 3y, \quad \frac{\partial f}{\partial y} = 3x + 2y

그래디언트(Gradient)

모든 편미분을 모은 벡터입니다.

f=[fx1fx2fxn]\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) 행렬

벡터 함수 f:RnRm\mathbf{f}: \mathbb{R}^n \to \mathbb{R}^m의 모든 1차 편미분을 담은 행렬입니다.

J=[f1x1f1xnfmx1fmxn]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차 편미분을 담은 정방 행렬입니다.

Hij=2fxixjH_{ij} = \frac{\partial^2 f}{\partial x_i \partial x_j}

헤시안의 고유값은 함수의 곡률을 나타냅니다. 모든 고유값이 양수이면 극소, 모두 음수이면 극대, 혼재하면 안장점(Saddle Point)입니다.

import numpy as np

# 수치 미분 (기울기 확인용)
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:
    import torch
    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)

함수의 합성에서 미분을 계산하는 규칙입니다.

dzdx=dzdydydx\frac{dz}{dx} = \frac{dz}{dy} \cdot \frac{dy}{dx}

다변수로 확장하면:

zxi=jzyjyjxi\frac{\partial z}{\partial x_i} = \sum_j \frac{\partial z}{\partial y_j} \cdot \frac{\partial y_j}{\partial x_i}

역전파 알고리즘

신경망의 훈련에서 손실 함수 LL을 각 가중치 ww에 대해 미분해야 합니다. 네트워크가 여러 레이어의 합성 함수이므로, 체인 룰을 반복 적용합니다.

2-레이어 신경망의 예:

L=(softmax(W2ReLU(W1x+b1)+b2),y)L = \ell(\text{softmax}(W_2 \cdot \text{ReLU}(W_1 \mathbf{x} + \mathbf{b}_1) + \mathbf{b}_2), \mathbf{y})

LW1\frac{\partial L}{\partial W_1}을 구하려면:

LW1=La2a2hhW1\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}

역전파는 출력에서 입력 방향으로 이 체인을 효율적으로 계산합니다.

import numpy as np

# 간단한 신경망 역전파 예제
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 최적화 이론

극값 조건

볼록 함수(Convex Function)

함수 ff가 볼록이면 두 점의 직선보다 함수값이 항상 낮거나 같습니다.

f(λx+(1λ)y)λf(x)+(1λ)f(y),λ[0,1]f(\lambda x + (1-\lambda)y) \leq \lambda f(x) + (1-\lambda) f(y), \quad \lambda \in [0,1]

볼록 함수의 극소는 항상 전역 최솟값입니다. 딥러닝의 손실 함수는 일반적으로 볼록하지 않아서 전역 최솟값을 보장할 수 없습니다.

경사 하강법(Gradient Descent)

θt+1=θtαθL(θt)\theta_{t+1} = \theta_t - \alpha \nabla_\theta L(\theta_t)

여기서 α\alpha는 학습률(learning rate)입니다. 학습률이 너무 크면 발산하고, 너무 작으면 수렴이 느립니다.

변형들:

mt=β1mt1+(1β1)gtm_t = \beta_1 m_{t-1} + (1-\beta_1) g_t vt=β2vt1+(1β2)gt2v_t = \beta_2 v_{t-1} + (1-\beta_2) g_t^2 θt+1=θtαv^t+ϵm^t\theta_{t+1} = \theta_t - \frac{\alpha}{\sqrt{\hat{v}_t} + \epsilon} \hat{m}_t

import numpy as np
import matplotlib.pyplot as plt

# 경사 하강법 구현
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)0P(A) \geq 0 (비음성)
  2. P(Ω)=1P(\Omega) = 1 (전체 확률은 1)
  3. 서로소인 사건들에 대해 P(AB)=P(A)+P(B)P(A \cup B) = P(A) + P(B) (가산 가법성)

조건부 확률

사건 BB가 발생했다는 조건 하에 사건 AA의 확률:

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

베이즈 정리(Bayes' Theorem)

P(AB)=P(BA)P(A)P(B)P(A|B) = \frac{P(B|A) \cdot P(A)}{P(B)}

베이즈 정리는 "증거를 바탕으로 믿음을 업데이트"하는 원리입니다.

이는 분류 모델 평가, 나이브 베이즈 분류기, 베이지안 신경망 등에 광범위하게 사용됩니다.

확률의 전체 법칙

P(B)=iP(BAi)P(Ai)P(B) = \sum_i P(B|A_i) P(A_i)

3.2 확률 분포

이산 확률 분포

베르누이 분포(Bernoulli Distribution)

결과가 0 또는 1인 단일 시행:

P(X=k)=pk(1p)1k,k{0,1}P(X=k) = p^k (1-p)^{1-k}, \quad k \in \{0, 1\}

E[X]=pE[X] = p, Var(X)=p(1p)Var(X) = p(1-p)

이항 분포(Binomial Distribution)

nn번의 베르누이 시행에서 성공 횟수:

P(X=k)=(nk)pk(1p)nkP(X=k) = \binom{n}{k} p^k (1-p)^{n-k}

포아송 분포(Poisson Distribution)

단위 시간/공간에서 사건 발생 횟수:

P(X=k)=λkeλk!P(X=k) = \frac{\lambda^k e^{-\lambda}}{k!}

E[X]=Var(X)=λE[X] = Var(X) = \lambda

연속 확률 분포

균등 분포(Uniform Distribution)

구간 [a,b][a, b]에서 모든 값이 동일한 확률 밀도:

f(x)=1ba,axbf(x) = \frac{1}{b-a}, \quad a \leq x \leq b

정규 분포(Normal/Gaussian Distribution)

f(x)=1σ2πexp((xμ)22σ2)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(x)=1(2π)d/2Σ1/2exp(12(xμ)TΣ1(xμ))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)의 잠재 공간 모델링에 핵심적으로 사용됩니다.

import numpy as np
from scipy import stats
import matplotlib.pyplot as plt

# 정규 분포
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]=xxP(X=x)(이산)E[X] = \sum_x x \cdot P(X=x) \quad \text{(이산)} E[X]=xf(x)dx(연속)E[X] = \int_{-\infty}^{\infty} x \cdot f(x) dx \quad \text{(연속)}

기댓값의 선형성: E[aX+b]=aE[X]+bE[aX + b] = aE[X] + b

분산(Variance)

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

표준편차: σ=Var(X)\sigma = \sqrt{Var(X)}

공분산(Covariance)

두 확률변수의 선형 관계를 측정합니다.

Cov(X,Y)=E[(XμX)(YμY)]=E[XY]E[X]E[Y]Cov(X, Y) = E[(X - \mu_X)(Y - \mu_Y)] = E[XY] - E[X]E[Y]

상관계수(Correlation)

ρXY=Cov(X,Y)σXσY\rho_{XY} = \frac{Cov(X, Y)}{\sigma_X \sigma_Y}

값의 범위는 [1,1][-1, 1]이며, 크기와 무관하게 선형 관계의 강도만을 측정합니다.

공분산 행렬(Covariance Matrix)

nn차원 확률벡터의 모든 쌍의 공분산:

Σij=Cov(Xi,Xj)\Sigma_{ij} = Cov(X_i, X_j)

PCA의 핵심 입력이며, 데이터의 분산 구조를 담고 있습니다.

3.4 최대우도추정(Maximum Likelihood Estimation, MLE)

우도함수(Likelihood Function)

파라미터 θ\theta가 주어졌을 때, 관측된 데이터 D={x1,...,xn}\mathcal{D} = \{x_1, ..., x_n\}가 발생할 확률:

L(θ;D)=P(Dθ)=i=1nP(xiθ)L(\theta; \mathcal{D}) = P(\mathcal{D}|\theta) = \prod_{i=1}^n P(x_i|\theta)

MLE는 이 우도를 최대화하는 θ\theta를 찾습니다.

θ^MLE=argmaxθL(θ;D)\hat{\theta}_{MLE} = \arg\max_\theta L(\theta; \mathcal{D})

로그 우도(Log-Likelihood)

곱셈을 덧셈으로 변환하여 계산을 쉽게 합니다.

logL(θ)=i=1nlogP(xiθ)\log L(\theta) = \sum_{i=1}^n \log P(x_i|\theta)

로그는 단조 증가 함수이므로 로그 우도를 최대화하는 θ\theta와 우도를 최대화하는 θ\theta는 동일합니다.

MLE 유도 예제: 정규 분포

데이터 {x1,...,xn}\{x_1, ..., x_n\}N(μ,σ2)\mathcal{N}(\mu, \sigma^2)를 따른다고 가정할 때:

logL(μ,σ2)=n2log(2πσ2)12σ2i=1n(xiμ)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으로 놓으면: μ^MLE=xˉ=1nxi\hat{\mu}_{MLE} = \bar{x} = \frac{1}{n}\sum x_i (표본 평균)

σ2\sigma^2에 대해 미분하면: σ^MLE2=1n(xixˉ)2\hat{\sigma}^2_{MLE} = \frac{1}{n}\sum (x_i - \bar{x})^2 (표본 분산)

딥러닝의 Cross-Entropy와 MLE

분류 문제에서 Cross-Entropy 손실을 최소화하는 것은 MLE와 동치입니다.

LCE=1ni=1nc=1Cyiclogp^ic\mathcal{L}_{CE} = -\frac{1}{n}\sum_{i=1}^n \sum_{c=1}^C y_{ic} \log \hat{p}_{ic}

이는 실제 데이터 분포 하에서 모델의 로그 우도를 최대화하는 것과 같습니다.

import numpy as np
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)

확률 분포 PP의 불확실성 또는 정보량:

H(X)=xP(x)logP(x)=E[logP(X)]H(X) = -\sum_{x} P(x) \log P(x) = -E[\log P(X)]

균등 분포일 때 엔트로피가 최대이고, 하나의 결과만 가능할 때 0입니다.

KL Divergence

두 분포 PPQQ 사이의 "거리"(단, 비대칭):

DKL(PQ)=xP(x)logP(x)Q(x)=EP[logP(X)Q(X)]D_{KL}(P \| Q) = \sum_x P(x) \log \frac{P(x)}{Q(x)} = E_P\left[\log \frac{P(X)}{Q(X)}\right]

DKL(PQ)0D_{KL}(P \| Q) \geq 0이며 P=QP = Q일 때만 0입니다. VAE의 손실 함수, 정책 최적화(PPO 등)에 사용됩니다.

크로스 엔트로피(Cross-Entropy)

실제 분포 PP와 모델 분포 QQ 사이의 크로스 엔트로피:

H(P,Q)=xP(x)logQ(x)=H(P)+DKL(PQ)H(P, Q) = -\sum_x P(x) \log Q(x) = H(P) + D_{KL}(P \| Q)

H(P)H(P)는 상수이므로 Cross-Entropy를 최소화하는 것은 KL Divergence를 최소화하는 것과 동치입니다.

상호 정보량(Mutual Information)

두 확률변수가 공유하는 정보:

$I(X;Y)=H(X)H(XY)=H(Y)H(YX)I(X; Y) = H(X) - H(X|Y) = H(Y) - H(Y|X)$

특징 선택, 표현 학습의 이론적 기반이 됩니다.

import numpy as np

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 (단정밀도)

FP16 (반정밀도)

BF16 (Brain Float 16)

import numpy as np

# 부동소수점 정밀도 이슈
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 함수:

softmax(xi)=exijexj\text{softmax}(x_i) = \frac{e^{x_i}}{\sum_j e^{x_j}}

큰 값이 입력되면 exie^{x_i}가 오버플로우를 일으킵니다.

안정적인 Softmax 구현:

softmax(xi)=eximax(x)jexjmax(x)\text{softmax}(x_i) = \frac{e^{x_i - \max(x)}}{\sum_j e^{x_j - \max(x)}}

수학적으로 동치이지만 수치적으로 훨씬 안정합니다.

import numpy as np

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)

데이터 {(xi,yi)}i=1n\{(\mathbf{x}_i, y_i)\}_{i=1}^n에 대해 선형 모델 y^=wTx+b\hat{y} = \mathbf{w}^T \mathbf{x} + b를 학습합니다.

손실 함수(Mean Squared Error):

L(w)=1ni=1n(yiy^i)2=1nyXw2L(\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으로 놓으면:

wL=2nXT(yXw)=0\nabla_\mathbf{w} L = -\frac{2}{n} X^T (y - X\mathbf{w}) = 0

XTXw=XTy\Rightarrow X^T X \mathbf{w} = X^T y

w^=(XTX)1XTy\Rightarrow \hat{\mathbf{w}} = (X^T X)^{-1} X^T y

이것이 최소제곱 해입니다. 행렬 XTXX^T X가 가역(invertible)이어야 하며, 이는 특징들이 선형 독립일 때 성립합니다.

5.2 Ridge와 Lasso 회귀

Ridge 회귀(L2 정규화)

Lridge(w)=yXw2+λw22L_{ridge}(\mathbf{w}) = \|y - X\mathbf{w}\|^2 + \lambda \|\mathbf{w}\|_2^2

정규 방정식: w^=(XTX+λI)1XTy\hat{\mathbf{w}} = (X^T X + \lambda I)^{-1} X^T y

λI\lambda I를 더하면 XTXX^T X가 항상 가역이 됩니다. 가중치들이 0에 가깝게 유지되어 과적합을 방지합니다.

Lasso 회귀(L1 정규화)

Llasso(w)=yXw2+λw1L_{lasso}(\mathbf{w}) = \|y - X\mathbf{w}\|^2 + \lambda \|\mathbf{w}\|_1

L1 정규화는 일부 가중치를 정확히 0으로 만드는 희소 해(sparse solution)를 유도합니다. 이는 자동 특징 선택(feature selection)의 효과가 있습니다.

import numpy as np
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 기울기의 기하학적 의미

등고선과 그래디언트의 관계

그래디언트 f(x)\nabla f(\mathbf{x})는 항상 등고선에 수직(직교)합니다. 경사 하강법에서 이 방향의 반대로 이동하면 가장 빠르게 함수값을 줄일 수 있습니다.

정규화의 기하학적 해석

Ridge 회귀는 w22t\|\mathbf{w}\|_2^2 \leq t 구 안에서 MSE를 최소화하는 것과 동치입니다. 원형 제약 조건이므로 희소 해를 만들지 않습니다.

Lasso는 w1t\|\mathbf{w}\|_1 \leq t 다이아몬드 모양 안에서 MSE를 최소화합니다. 다이아몬드의 꼭짓점에서 해가 자주 발생하므로 희소 해가 나타납니다.


6. 종합: 딥러닝 수학 통합

6.1 신경망의 수학적 관점

완전 연결 신경망 LL개 레이어:

h(0)=x\mathbf{h}^{(0)} = \mathbf{x} z(l)=W(l)h(l1)+b(l)\mathbf{z}^{(l)} = W^{(l)} \mathbf{h}^{(l-1)} + \mathbf{b}^{(l)} h(l)=σ(z(l))\mathbf{h}^{(l)} = \sigma(\mathbf{z}^{(l)}) y^=h(L)\hat{y} = \mathbf{h}^{(L)}

순전파: 선형대수(행렬 곱) + 활성화 함수

손실 계산: 정보 이론(Cross-Entropy) or 통계(MSE)

역전파: 미적분(체인 룰) → 그래디언트 계산

파라미터 업데이트: 최적화 이론(경사 하강법, Adam)

6.2 Attention 메커니즘과 행렬 연산

트랜스포머의 Attention:

Attention(Q,K,V)=softmax(QKTdk)V\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V

import numpy as np

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. 수치 해석: 이론을 실제 컴퓨터에서 안정적으로 구현하기 위한 지식

다음 학습 단계를 추천합니다:

수학은 이해의 도구입니다. 모든 수식을 외울 필요는 없지만, 각 개념이 존재하는지, 딥러닝의 어느 부분에서 어떻게 작동하는지를 이해하면 훨씬 강력한 실무 능력을 갖출 수 있습니다.


참고 자료

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다