1. 미적분, 2. 선형대수 , 3. 확률·통계, 4. 회귀·추정, 5. 이산수학·그래프
# ML 수학 with NumPy
# 머신러닝 수학 5대 핵심 개념
# 1) 미적분: MSE 손실 미분, 경사하강법으로 선형회귀 파라미터 학습
# 2) 선형대수: y = W@x + b 선형 변환, 공분산의 고유분해를 이용한 간단한 PCA
# 3) 확률·통계: 로지스틱 회귀 확률(sigmoid)과 이진 크로스엔트로피, 그라디언트 업데이트
# 4) 회귀·추정: OLS(정규방정식) vs 경사하강법 비교, 정규분포의 MLE(평균, 분산)
# 5) 이산수학·그래프: 인접행렬의 행정규화를 활용한 간단한 메시지 패싱(GNN 유사)
# 필요 라이브러리: NumPy
import numpy as np
# 출력 보기 좋게 포맷
np.set_printoptions(precision=4, suppress=True, linewidth=120)
def main():
print("====================================")
print("1) 미적분 — 기울기와 경사하강법 (선형회귀)")
print("====================================")
rng = np.random.default_rng(42)
# 작은 선형 데이터셋 생성: y = 3x + 2 + 잡음
x = rng.uniform(-2, 2, size=100)
true_w, true_b = 3.0, 2.0
y = true_w * x + true_b + rng.normal(0, 0.3, size=x.shape[0])
# 손실함수(MSE): L = mean( (y - (w*x + b))^2 )
# 미분:
# dL/dw = -(2/N) * sum( x * (y - (w*x + b)) )
# dL/db = -(2/N) * sum( (y - (w*x + b)) )
def mse_and_grads(w, b, x, y):
y_hat = w * x + b # 모델 예측
residual = y - y_hat # 잔차(오차)
N = x.shape[0]
loss = np.mean(residual**2) # 평균제곱오차
dL_dw = -2.0 / N * np.sum(x * residual) # w에 대한 기울기
dL_db = -2.0 / N * np.sum(residual) # b에 대한 기울기
return loss, dL_dw, dL_db
# 경사하강법으로 w, b 학습
w, b = 0.0, 0.0
lr = 0.1
history = []
for step in range(80):
loss, dL_dw, dL_db = mse_and_grads(w, b, x, y)
w -= lr * dL_dw
b -= lr * dL_db
history.append(loss)
print(f"정답 파라미터: w={true_w:.3f}, b={true_b:.3f}")
print(f"학습된 파라미터: w={w:.3f}, b={b:.3f}")
print("MSE 처음→마지막:", f"{history[0]:.4f} → {history[-1]:.4f}")
print("설명: 손실의 기울기를 이용해 (w,b)를 오차가 줄어드는 방향으로 갱신합니다.\n")
# ---------------------------------------------------------------
print("====================================")
print("2) 선형대수 — 행렬연산 & 간단한 PCA")
print("====================================")
# 한 층의 선형 변환: y = W@x + b
W = np.array([[1.0, 2.0],
[3.0, 4.0]])
x_vec = np.array([5.0, 6.0])
b_vec = np.array([0.5, -1.0])
y_vec = W @ x_vec + b_vec
print("선형 계층 y = W@x + b:", y_vec)
# 상관이 있는 2차원 데이터 생성 → 공분산 행렬의 고유분해로 PCA
X = rng.normal(0, 1, size=(200, 2))
X[:, 1] = 0.8*X[:, 0] + 0.2*rng.normal(0, 1, size=200) # 상관 유도
# 평균 0으로 센터링
X_centered = X - X.mean(axis=0, keepdims=True)
# 공분산 행렬(2x2): (X^T X)/(N-1)
Cov = (X_centered.T @ X_centered) / (X_centered.shape[0]-1)
# 고유분해: Cov v = λ v (대칭행렬이므로 eigh 사용)
eigvals, eigvecs = np.linalg.eigh(Cov)
# 고유값 내림차순 정렬
idx = np.argsort(eigvals)[::-1]
eigvals, eigvecs = eigvals[idx], eigvecs[:, idx]
print("공분산 행렬:\n", Cov)
print("고유값(내림차순):", eigvals)
print("첫 번째 주성분(단위벡터):", eigvecs[:, 0])
# 첫 번째 주성분 방향으로 데이터 투영
PC1 = X_centered @ eigvecs[:, 0]
print("PC1 투영의 평균/표준편차:", np.mean(PC1).round(4), np.std(PC1).round(4))
print("설명: 공분산의 고유벡터가 분산이 가장 큰 방향(주성분)을 나타냅니다.\n")
# ---------------------------------------------------------------
print("====================================")
print("3) 확률·통계 — 로지스틱 회귀 확률 & 크로스엔트로피")
print("====================================")
# 시그모이드: 실수 → (0,1) 확률
def sigmoid(z):
return 1.0 / (1.0 + np.exp(-z))
# 이진분류 토이 데이터 (선형 경계 + 잡음)
X_cls = rng.normal(0, 1, size=(200, 2))
scores_true = 1.5*X_cls[:, 0] - 0.7*X_cls[:, 1] + 0.3 + rng.normal(0, 0.3, size=200)
y_cls = (scores_true > 0).astype(np.float32)
# 초기 파라미터
W_lr = rng.normal(0, 0.1, size=(2,)) # 가중치
b_lr = 0.0 # 바이어스
lr = 0.2
# 이진 크로스엔트로피: L = -[ y*log(p) + (1-y)*log(1-p) ]
def binary_cross_entropy(y_true, y_prob, eps=1e-8):
y_prob = np.clip(y_prob, eps, 1.0-eps) # 숫자 안정화
return -np.mean(y_true*np.log(y_prob) + (1.0-y_true)*np.log(1.0-y_prob))
# 로지스틱 회귀의 한 스텝 업데이트(경사하강법)
# p = sigmoid(XW + b)
# dL/dW = X^T (p - y) / N, dL/db = mean(p - y)
def step_logreg(W, b, X, y, lr):
z = X @ W + b
p = sigmoid(z)
N = X.shape[0]
grad_W = (X.T @ (p - y)) / N
grad_b = np.mean(p - y)
W = W - lr * grad_W
b = b - lr * grad_b
loss = binary_cross_entropy(y, p)
return W, b, loss
losses = []
for t in range(120):
W_lr, b_lr, loss = step_logreg(W_lr, b_lr, X_cls, y_cls, lr)
losses.append(loss)
probs = sigmoid(X_cls @ W_lr + b_lr)
preds = (probs > 0.5).astype(np.float32)
acc = (preds == y_cls).mean()
print("최종 크로스엔트로피 손실:", round(losses[-1], 4))
print("토이 데이터 정확도:", round(acc, 3))
print("설명: 확률 p=σ(XW+b)와 크로스엔트로피 손실을 이용해 분류기를 학습합니다.\n")
# ---------------------------------------------------------------
print("====================================")
print("4) 회귀·추정 — OLS(정규방정식) & 정규분포 MLE")
print("====================================")
# 선형 회귀 데이터 (y = 2.5x - 1.2 + 잡음)
N = 150
X_reg = rng.uniform(-3, 3, size=(N, 1))
y_reg = 2.5*X_reg[:, 0] - 1.2 + rng.normal(0, 0.5, size=N)
# 정규방정식(닫힌해): w = (X^T X)^(-1) X^T y
X_design = np.c_[np.ones(N), X_reg] # [1, x] (바이어스 포함)
w_hat = np.linalg.inv(X_design.T @ X_design) @ X_design.T @ y_reg # [b, w]
b_hat, w1_hat = w_hat[0], w_hat[1]
# 경사하강법으로도 비교 (반복적, 수치적 방법)
w_gd, b_gd = 0.0, 0.0
lr = 0.05
for _ in range(200):
y_hat = w_gd*X_reg[:, 0] + b_gd
residual = y_reg - y_hat
dL_dw = -2.0/N * np.sum(X_reg[:, 0] * residual)
dL_db = -2.0/N * np.sum(residual)
w_gd -= lr * dL_dw
b_gd -= lr * dL_db
print(f"OLS(닫힌해) → w={w1_hat:.3f}, b={b_hat:.3f}")
print(f"경사하강법 → w={w_gd:.3f}, b={b_gd:.3f}")
print("설명: OLS는 해석적(닫힌) 해, GD는 반복적 수치 최적화 방법입니다.\n")
# 정규분포 MLE: x_i ~ N(μ, σ^2) 가정 시
# μ의 MLE = 표본평균, σ^2의 MLE = (1/N) * Σ(x_i - μ)^2
X_gauss = rng.normal(loc=1.7, scale=0.9, size=500)
mu_mle = np.mean(X_gauss)
sigma2_mle = np.mean((X_gauss - mu_mle)**2) # MLE는 1/N (편향보정 아님)
print(f"Gaussian MLE → μ^={mu_mle:.3f}, σ^2^={sigma2_mle:.3f}")
print("설명: 우도(likelihood)를 최대화하여 μ, σ^2의 추정치를 얻습니다.\n")
# ---------------------------------------------------------------
print("====================================")
print("5) 이산수학·그래프 — 메시지 패싱(GNN 유사)")
print("====================================")
# 4개 노드의 무방향 그래프(체인): 0-1-2-3
A = np.array([
[0, 1, 0, 0],
[1, 0, 1, 0],
[0, 1, 0, 1],
[0, 0, 1, 0]
], dtype=float)
# 각 노드의 초기 특징(2차원)
H0 = np.array([
[1.0, 0.0], # node 0
[0.0, 1.0], # node 1
[1.0, 1.0], # node 2
[0.5, 0.2], # node 3
])
# 차수행렬 D (대각원소: 노드별 이웃 수)
D = np.diag(A.sum(axis=1))
# 행정규화 인접행렬: A_norm = D^{-1} A (이웃 평균)
D_inv = np.linalg.inv(D) # 체인 그래프라 0차수 노드 없음 → 역행렬 존재
A_norm = D_inv @ A
# 메시지 패싱 한 단계: H' = ReLU( A_norm @ H @ W )
W_msg = np.array([[1.0, -0.5],
[0.2, 0.8]]) # 실제 GNN에서는 학습되는 가중치
def relu(z): return np.maximum(0.0, z)
H1 = relu(A_norm @ H0 @ W_msg) # 1층 메시지 패싱
H2 = relu(A_norm @ H1 @ W_msg) # 2층 메시지 패싱
print("A (인접행렬):\n", A)
print("A_norm (행정규화):\n", A_norm)
print("H0 (초기 특징):\n", H0)
print("H1 (1단계 후):\n", H1)
print("H2 (2단계 후):\n", H2)
print("설명: 각 단계에서 이웃의 특징을 평균한 뒤 선형변환+비선형 함수를 적용합니다.\n")
if __name__ == "__main__":
main()
'AI > 참고자료' 카테고리의 다른 글
[AI] 선형대수와 머신러닝 (0) | 2025.10.23 |
---|---|
[AI] 미적분과 머신러닝 (0) | 2025.10.23 |
[AI] 머신러닝에서 미분이 필요한 이유 (0) | 2025.10.22 |
[AI] 개발자가 알아야 할 8대 핵심 구조와 개념 체계 (0) | 2025.10.10 |
[AI] 응용 개발 vs 모델 연구·개발 (1) | 2025.05.06 |