본문 바로가기
TYLER SONGBlog
← Blog index
LLM Research6 min read

주간 리뷰 (Week 3): 수학·머신러닝 개념을 계산과 실험으로 다시 연결하기

이번 주에 배운 엔트로피, 교차 엔트로피, KL 발산을 직접 계산하며 차이를 정리한다. 다항 회귀 실험을 다시 작성해 모델이 복잡해질수록 훈련 데이터에만 지나치게 맞을 수 있음을 확인한다. 실험 조건을 같은 형식으로 기록해 결과를 재현하는 습관도 만든다.

By 송민성

오늘의 목표

Week 3에서 배운 수학과 머신러닝 개념을 암기 대신 계산과 코드로 복습한다. 확률분포의 차이를 수치로 측정하고, 다항 회귀의 과적합을 실험으로 확인한 뒤 재현 가능한 기록 형식을 확정한다.

개념과 직관

오늘은 새로운 개념을 추가하지 않는다. 이번 주 핵심 개념 사이의 연결을 다시 확인한다.

엔트로피

엔트로피(entropy, 확률분포의 불확실성을 나타내는 값)는 결과를 얼마나 예측하기 어려운지 측정한다.

동전이 항상 앞면만 나온다면 다음 결과를 쉽게 예측할 수 있다. 반면 앞면과 뒷면이 절반씩 나온다면 어느 쪽이 나올지 알기 어렵다.

그림으로 생각하면 다음과 같다.

text
확률 [1.0, 0.0]  → 한 칸에 모두 모임 → 불확실성 낮음
확률 [0.5, 0.5]  → 두 칸에 고르게 퍼짐 → 불확실성 높음

확률분포 P의 엔트로피는 다음처럼 계산한다.

text
H(P) = -각 결과에 대해 P(x) × log(P(x))를 더한 값

확률이 0인 항은 계산에서 제외한다. log(0)은 정의되지 않지만, 엔트로피에서는 해당 항의 기여도를 0으로 처리한다.

교차 엔트로피

교차 엔트로피(cross entropy, 실제 분포를 예측 분포로 표현할 때 드는 비용)는 정답 분포 P와 모델의 예측 분포 Q를 비교한다.

text
H(P, Q) = -각 결과에 대해 P(x) × log(Q(x))를 더한 값

예를 들어 정답이 고양이라면 실제 분포를 [1, 0]으로 나타낼 수 있다.

text
정답 P       = [1.0, 0.0]
좋은 예측 Q  = [0.9, 0.1] → 교차 엔트로피 작음
나쁜 예측 Q  = [0.2, 0.8] → 교차 엔트로피 큼

정답에 높은 확률을 줄수록 비용이 작아진다. 모델 학습에서는 이 비용을 손실(loss, 예측이 정답에서 얼마나 벗어났는지 나타내는 값)로 사용할 수 있다.

KL 발산

KL 발산(Kullback–Leibler divergence, 한 확률분포가 다른 확률분포와 얼마나 다른지 측정하는 값)은 실제 분포 P를 예측 분포 Q로 대신했을 때 추가로 드는 비용이다.

text
KL(P || Q) = 교차 엔트로피 H(P, Q) - 엔트로피 H(P)

또는 다음처럼 직접 계산한다.

text
KL(P || Q) = 각 결과에 대해 P(x) × log(P(x) / Q(x))를 더한 값

KL(P || Q)와 KL(Q || P)는 일반적으로 다르다. 두 점 사이의 거리처럼 대칭인 값이 아니다.

다항 회귀와 과적합

다항 회귀(polynomial regression, 입력값의 거듭제곱을 사용해 곡선을 맞추는 회귀 방법)는 직선보다 복잡한 관계를 표현할 수 있다.

text
1차 모델: y = a0 + a1x
3차 모델: y = a0 + a1x + a2x² + a3x³

차수가 높아지면 더 구불구불한 곡선을 만들 수 있다. 너무 높은 차수는 훈련 데이터의 우연한 흔들림까지 외울 수 있다. 이를 과적합(overfitting, 훈련 데이터에는 잘 맞지만 새로운 데이터에서는 성능이 나빠지는 현상)이라고 한다.

text
낮은 차수:   데이터 흐름을 충분히 표현하지 못함
적절한 차수: 전체 흐름을 포착함
높은 차수:   각 훈련점을 쫓아 곡선이 심하게 흔들림

코드로 직접 해보기

1. 엔트로피·교차 엔트로피·KL 발산 계산

먼저 공식을 그대로 파이썬 반복문으로 옮긴다.

python
import math def entropy(p): total = 0.0 for probability in p: if probability > 0: total -= probability * math.log(probability) return total def cross_entropy(p, q): total = 0.0 for actual, predicted in zip(p, q): if actual > 0: if predicted <= 0: return math.inf total -= actual * math.log(predicted) return total def kl_divergence(p, q): total = 0.0 for actual, predicted in zip(p, q): if actual > 0: if predicted <= 0: return math.inf total += actual * math.log(actual / predicted) return total actual = [1.0, 0.0] good_prediction = [0.9, 0.1] bad_prediction = [0.2, 0.8] print("정답 엔트로피:", entropy(actual)) print("좋은 예측의 교차 엔트로피:", cross_entropy(actual, good_prediction)) print("나쁜 예측의 교차 엔트로피:", cross_entropy(actual, bad_prediction)) print("좋은 예측의 KL:", kl_divergence(actual, good_prediction)) print("나쁜 예측의 KL:", kl_divergence(actual, bad_prediction)) left = kl_divergence(actual, good_prediction) right = cross_entropy(actual, good_prediction) - entropy(actual) print("두 계산이 같은가:", math.isclose(left, right))

확인할 내용:

  • 좋은 예측의 교차 엔트로피가 나쁜 예측보다 작아야 한다.
  • 좋은 예측의 KL 발산이 나쁜 예측보다 작아야 한다.
  • KL(P || Q) = H(P, Q) - H(P) 관계가 코드에서도 성립해야 한다.
  • 자연로그를 사용했으므로 값의 단위는 냇(nat)이다. 밑이 2인 로그를 쓰면 비트(bit)가 된다.

2. 다항 회귀 과적합 실험 다시 작성

넘파이(NumPy, 수치 계산용 파이썬 라이브러리)로 다항식의 계수를 구한다. 훈련 오차와 테스트 오차를 따로 계산한다.

python
import numpy as np def mean_squared_error(actual, predicted): return np.mean((actual - predicted) ** 2) rng = np.random.default_rng(42) # 실제 관계는 부드러운 곡선이지만 관측값에는 잡음이 섞여 있다. x_train = np.linspace(-1, 1, 12) y_train = x_train**2 + rng.normal(0, 0.12, size=len(x_train)) x_test = np.linspace(-1, 1, 200) y_test = x_test**2 for degree in [1, 2, 5, 10]: coefficients = np.polyfit(x_train, y_train, degree) model = np.poly1d(coefficients) train_prediction = model(x_train) test_prediction = model(x_test) train_mse = mean_squared_error(y_train, train_prediction) test_mse = mean_squared_error(y_test, test_prediction) print( f"차수={degree:2d} | " f"훈련 MSE={train_mse:.4f} | " f"테스트 MSE={test_mse:.4f}" )

평균 제곱 오차(mean squared error, 예측값과 실제값 차이를 제곱해 평균낸 값)는 작을수록 좋다.

결과를 볼 때 훈련 오차만 비교하지 않는다. 높은 차수에서 훈련 오차는 계속 작아져도 테스트 오차가 커질 수 있다. 이것이 과적합 신호다.

오늘의 실험

다항식 차수만 바꾸고 나머지 조건은 고정한다.

  1. degree를 1, 2, 5, 10으로 바꾼다.
  2. 각 차수의 훈련 MSE와 테스트 MSE를 기록한다.
  3. seed를 42, 43, 44로 바꿔 결과가 얼마나 달라지는지 확인한다.
  4. 훈련 오차는 감소하지만 테스트 오차가 증가하는 지점을 찾는다.

실험 기록은 다음 형식으로 통일한다.

필드기록 내용
experiment_id실험을 구분하는 고유 이름
model모델 종류와 다항식 차수
dataset데이터 생성 방식과 훈련·테스트 크기
lr학습률, 사용하지 않으면 N/A
batch배치 크기, 사용하지 않으면 N/A
seed난수 생성 시드
metric훈련 MSE와 테스트 MSE
GPU사용한 GPU, 없으면 CPU
VRAM사용한 GPU 메모리, 없으면 N/A
time실행 시간

기록 예시:

text
experiment_id: week3_poly_degree_10_seed42
model: polynomial_regression_degree_10
dataset: synthetic_y=x^2+noise, train=12, test=200
lr: N/A
batch: N/A
seed: 42
metric: train_mse=실행결과, test_mse=실행결과
GPU: CPU
VRAM: N/A
time: 실행시간

실험 하나에는 한 가지 주요 변화만 둔다. 차수와 시드를 동시에 바꾸면 결과 차이가 어느 조건에서 생겼는지 판단하기 어렵다.

왜 이걸 배우나 (LLM / Post-training 연결)

언어 모델은 다음 토큰에 대한 확률분포를 출력하며, 학습에서는 정답 토큰과 예측 분포 사이의 교차 엔트로피를 자주 사용한다. KL 발산은 기존 모델의 분포와 학습 중인 모델의 분포가 지나치게 멀어지는지 측정하는 데 활용된다.

Post-training 실험도 훈련 지표만 좋아졌다고 성공한 것이 아니다. 새로운 평가 데이터에서 성능이 유지되는지 확인하고, 모델·데이터·시드·장비·시간을 기록해야 결과를 비교하고 재현할 수 있다.

초보자가 막히는 지점

log(0) 오류

확률 0의 로그는 계산할 수 없다. 실제 확률 P(x)가 0인 항은 건너뛰고, 정답 확률이 양수인데 예측 확률 Q(x)가 0이면 손실을 무한대로 처리한다.

확률의 합이 1이 아님

[0.7, 0.7]은 합이 1.4이므로 확률분포가 아니다. 계산 전에 모든 값이 0 이상이고 합이 1인지 확인한다.

KL 발산을 거리로 오해함

KL 발산은 방향에 따라 값이 달라진다. 함수 인수 p, q의 순서를 바꾸면 다른 결과가 나올 수 있다.

훈련 오차만 보고 모델을 선택함

훈련 오차가 가장 작은 모델이 가장 좋은 모델은 아니다. 모델 선택에서는 학습에 쓰지 않은 테스트 데이터의 오차도 함께 확인한다.

높은 차수에서 경고나 불안정한 값이 나옴

적은 데이터에 너무 높은 차수의 다항식을 맞추면 계산이 불안정해질 수 있다. 우선 입력 범위를 -1부터 1 사이로 유지하고, 차수를 데이터 수보다 지나치게 높이지 않는다.

알고리즘 (병행 1시간)

이번 주에 틀린 문제만 다시 푼다. 해설을 보기 전에 문제당 최대 20분 동안 처음부터 풀이하고, 다음 세 항목을 기록한다.

  1. 접근: 어떤 자료구조와 풀이 순서를 선택했는가
  2. 시간복잡도: 입력 크기가 커질 때 연산 횟수가 어떻게 증가하는가
  3. 틀린 이유: 개념 부족, 조건 누락, 경계값 실수, 구현 실수 중 무엇이었는가

같은 방식으로 다시 틀렸다면 정답 코드만 옮기지 않는다. 실패한 접근과 정답 접근의 차이를 한 문장으로 적고, 입력이 비어 있거나 값이 하나뿐인 경계 사례를 직접 만들어 다시 실행한다.