머신러닝·신경망

DL / 16번째 글

편향과 분산: 과소적합·과대적합의 진단과 처방

기대 오차를 편향·분산·환원불가 오차로 나누고, 학습 곡선으로 어느 쪽이 문제인지 가려낸 뒤, 복잡도 조절·정규화·조기 종료·증강·앙상블 중 무엇을 꺼내야 하는지까지 한 편에서 정리한다.

PALDYN Team43 MIN READ

지난 글에서 데이터를 나누고 교차 검증으로 성능을 정직하게 재는 법을 다뤘다. 재고 나면 곧바로 다음 질문이 온다 — 숫자가 나쁠 때 무엇을 고쳐야 하는가. 훈련 정확도 95%에 검증 정확도 72%인 모델과, 훈련·검증이 나란히 68%인 모델은 처방이 정반대다. 앞쪽은 데이터를 더 넣으면 나아지지만 뒤쪽은 데이터를 열 배 넣어도 그대로다.

이 두 상태를 가르는 축이 편향과 분산이다. 이 글은 그 축을 세우는 일(진단)과 각 방향으로 무엇을 꺼내는지(처방)를 한자리에서 다룬다. 둘을 갈라 두면 진단 없이 처방부터 뒤지게 되는데, 실무에서 가장 자주 나는 사고가 정확히 그것이다 — 과소적합인 모델에 정규화를 한 겹 더 거는 것.

편향·분산 분해

편향·분산·환원불가 오차

같은 데이터 분포에서 훈련 세트를 여러 번 뽑아 매번 모델을 새로 학습시킨다고 하자. 그 모델들이 어떤 입력 하나에 대해 내는 예측의 기대 제곱 오차는 세 조각으로 정확히 분해된다.

E[(y−f^(x))2]=(E[f^(x)]−f(x))2⏟bias2+E[(f^(x)−E[f^(x)])2]⏟variance+σ2\mathbb{E}\big[(y - \hat f(x))^2\big] = \underbrace{\big(\mathbb{E}[\hat f(x)] - f(x)\big)^2}_{\text{bias}^2} + \underbrace{\mathbb{E}\big[(\hat f(x) - \mathbb{E}[\hat f(x)])^2\big]}_{\text{variance}} + \sigma^2

편향(Bias)은 모델들의 예측 평균이 정답에서 얼마나 떨어져 있는지다. 데이터의 실제 관계를 모델이 구조적으로 잘못 가정할 때 생긴다. 비선형 관계를 직선으로 학습하면 데이터를 백만 개 모아도 직선은 곡선이 되지 못한다. 이 「아무리 해도 안 되는 몫」이 편향이다.

분산(Variance)은 훈련 세트가 바뀔 때 예측이 얼마나 흔들리는지다. 복잡한 모델은 데이터의 규칙뿐 아니라 그 표본에만 있는 잡음까지 따라 그리므로, 표본이 조금만 달라져도 완전히 다른 함수가 나온다.

환원불가 오차(Irreducible Error)는 데이터 자체가 가진 잡음이다. 측정 오차, 라벨링 실수, 애초에 입력에 담기지 않은 요인 — 어떤 모델을 써도 이 몫은 남는다. 검증 오차가 0.03에서 더 안 내려간다고 모델을 계속 키우는 것은, 그 0.03이 σ2\sigma^2 인지 아닌지를 먼저 물었어야 하는 상황이다.

여기서 중요한 것은 앞의 두 조각만 우리가 손댈 수 있다는 사실이다. 그리고 손대는 방향이 서로 반대다.

과녁 비유

과녁 비유가 이 구도를 한눈에 보여 준다. 중앙이 정답이고, 각 점은 서로 다른 훈련 세트로 만든 모델 하나의 예측이다.

편향 분산 과녁 모양 이름
낮음 낮음 중앙에 촘촘히 모임 이상적
높음 낮음 한쪽에 모이지만 중앙에서 멂 과소적합
낮음 높음 중앙 둘레에 넓게 퍼짐 과대적합
높음 높음 흩어지고 중앙에서도 멂 최악

과소적합(Underfitting)은 모델이 데이터의 패턴을 담아내기에 표현력이 모자란 상태이고, 과대적합(Overfitting)은 반대로 훈련 데이터의 특정 패턴과 잡음까지 외워 새 데이터에서 무너지는 상태다. 앞의 것은 평균이 빗나가 있고, 뒤의 것은 평균은 맞는데 개별 예측이 튄다.

세 번째 줄, 즉 낮은 편향과 높은 분산이 실무에서 가장 자주 만나는 자리다. 요즘 쓰는 모델은 대체로 표현력이 넘치기 때문이다. 그래서 「모델을 어떻게 키울까」보다 「어떻게 억제할까」에 시간이 더 든다.

가장 자주 만나는 자리라면 거기로 가는 길도 함께 묶어 두는 편이 낫다. 분산이 커지는 경로는 셋뿐이다. 첫째, 모델이 데이터에 비해 너무 복잡하다 — 표본 200개짜리 문제에 파라미터 2,000개짜리 모델을 얹으면 그 모델은 데이터를 설명하는 대신 외운다. 둘째, 훈련 데이터가 너무 적다 — 같은 모델이라도 표본이 줄면 그 표본에만 있는 우연이 규칙처럼 보인다. 셋째, 훈련을 너무 오래 한다 — 반복 학습하는 모델은 에폭이 늘수록 쓸 수 있는 함수의 범위가 함께 넓어져서, 층 하나 건드리지 않아도 시간만으로 오른쪽 끝까지 간다.

앞의 둘은 모델과 데이터의 비율 문제이고 셋째는 시간 문제다. 갈라 두는 이유는 처방이 서로 다른 서랍에서 나오기 때문이다 — 비율이 문제면 모델을 줄이거나 데이터를 늘려야 하고, 시간이 문제면 구조를 그대로 둔 채 멈출 자리만 찾으면 된다. 뒤에서 볼 조기 종료가 유독 값싼 처방인 이유가 여기 있다. 그리고 셋 다 같은 축의 오른쪽으로 미는 힘이므로, 어느 경로로 왔든 진단에 쓰는 그림은 하나로 족하다.

트레이드오프 곡선

편향-분산 트레이드오프 곡선

모델 복잡도 — 파라미터 개수, 트리 깊이, 다항식 차수, 학습 에폭 수 — 를 올리면 편향은 단조롭게 줄고 분산은 단조롭게 는다. 둘의 합이 U자를 그리고, 그 바닥이 최적 복잡도다. 이것이 트레이드오프라는 이름의 뜻이다: 한쪽을 줄이는 행위가 다른 쪽을 늘린다.

선형 회귀는 왼쪽 끝에 있다. 어떤 표본을 줘도 비슷한 직선을 그리므로 분산이 거의 없지만, 데이터가 곡선이면 그만큼이 통째로 편향이다. 깊이 제한 없는 의사결정 트리는 오른쪽 끝이다. 훈련 점을 하나도 빠짐없이 맞히므로 편향이 0에 가깝지만, 표본 하나를 빼고 다시 학습하면 전혀 다른 트리가 나온다.

U자의 바닥이 어디인지는 데이터 양이 정한다. 데이터가 늘면 분산 곡선이 통째로 아래로 내려가고, 그러면 바닥이 오른쪽으로 옮겨 간다. 같은 문제라도 샘플이 1,000개일 때의 최적 트리 깊이와 100만 개일 때의 최적 깊이가 다른 이유다. 「이 모델이 좋다」가 아니라 「이 데이터 크기에서 이 복잡도가 좋다」가 맞는 문장이다.

학습 곡선 진단

오차 수준과 간격

과대적합 학습 곡선 진단

진단에 필요한 숫자는 둘뿐이다. 훈련 오차의 절대 수준과 훈련·검증 오차의 간격(gap)이다. 이 둘의 조합이 네 칸을 만들고, 각 칸이 다른 처방을 부른다.

훈련 오차 간격 진단 방향
높다 작다 고편향 (과소적합) 복잡도를 올린다
낮다 크다 고분산 (과대적합) 복잡도를 내리거나 데이터를 늘린다
낮다 작다 균형 배포 가능
높다 크다 데이터·분할 문제 누수·분포 차이를 먼저 본다

마지막 줄은 이론적으로 나오기 어려운 조합이라 대개 모델이 아니라 파이프라인의 문제다. 훈련·검증 분포가 다르거나(시간 순서를 무시하고 나눴거나), 전처리를 분할 전에 해서 통계가 새어 나갔거나, 검증 세트가 너무 작아 추정 자체가 흔들리는 경우다.

반복 학습하는 모델에서는 이 간격이 에폭에 따라 변한다. 훈련 손실은 계속 내려가는데 검증 손실이 어느 지점에서 반등하기 시작하면, 그 지점이 모델이 규칙 대신 잡음을 외우기 시작한 자리다. 조기 종료의 기준점이 여기서 나온다.

학습 곡선의 모양

과소적합 · 적정 적합 · 과대적합 비교

학습 곡선(Learning Curve)은 훈련 샘플 수를 조금씩 늘려 가며 훈련 점수와 검증 점수를 함께 그린 그림이다. 한 시점의 숫자 두 개보다 정보가 많다 — 데이터를 더 모으는 것이 효과가 있을지를 알려 주기 때문이다.

고편향 곡선은 두 선이 일찍 만나 높은 자리에서 나란히 평평해진다. 샘플을 두 배로 늘려도 두 선이 그대로다. 데이터 수집 예산을 여기에 쓰면 낭비다.

고분산 곡선은 두 선 사이가 벌어진 채 좁혀지지 않는다. 다만 샘플이 늘수록 검증 선이 조금씩 내려오고 간격도 줄어드는 기울기가 남아 있다. 그 기울기가 곧 「데이터를 더 모으면 얼마나 좋아지는가」의 예고편이다. 기울기가 아직 살아 있으면 수집이 최선의 투자이고, 이미 평평해졌으면 다른 처방으로 넘어간다.

균형 곡선은 두 선이 낮은 자리에서 좁은 간격으로 만난다.

from sklearn.model_selection import learning_curve
import numpy as np

def diagnose(model, X, y):
    sizes, train_scores, val_scores = learning_curve(
        model, X, y, train_sizes=np.linspace(0.1, 1.0, 10),
        cv=5, scoring='neg_mean_squared_error', n_jobs=-1)
    train, val = -train_scores.mean(axis=1), -val_scores.mean(axis=1)
    gap = val[-1] - train[-1]

    if train[-1] > 0.2 and gap < 0.05:
        return "고편향: 복잡도를 올리거나 특성을 늘린다"
    if gap > 0.1:
        # 검증 선의 마지막 기울기가 살아 있으면 데이터 수집이 먼저다
        slope = val[-1] - val[-3]
        return "고분산: " + ("데이터 수집" if slope < -0.01 else "정규화·단순화")
    return "균형"

다항식 차수 실험

숫자를 한 번 따라가 보면 U자가 몸에 붙는다. xx 를 0과 10 사이에 고르게 200개 놓고 y=sin⁡(x)+εy = \sin(x) + \varepsilon, ε∼N(0,0.32)\varepsilon \sim N(0, 0.3^2) 으로 만든 표본에 다항 회귀를 차수만 바꿔 가며 맞춘 결과다. 잡음의 표준편차 0.3은 우리가 직접 넣은 값이라 환원불가 오차가 얼마인지를 알고 시작하는 실험이다 — 실제 문제에서는 결코 알 수 없는 값이지만, 여기서는 알기 때문에 표의 숫자가 어디서 멈추는지를 눈으로 확인할 수 있다.

차수 훈련 MSE 검증 MSE 간격 상태
1 0.4235 0.4260 0.0025 고편향
3 0.0920 0.0985 0.0065 균형
10 0.0112 0.1240 0.1128 고분산
20 0.0041 0.5800 0.5759 극단적 고분산

읽는 법이 셋이다. 첫째, 차수 1은 훈련 오차 자체가 높다. 검증 오차와의 간격은 0.0025로 사실상 없는데도 성능이 나쁘다 — 간격만 보면 「과적합이 아니니 괜찮다」고 오독하게 되는 자리다. 둘째, 차수 20은 훈련 MSE가 차수 3의 22분의 1인데 검증 MSE는 여섯 배다. 훈련 성능 개선이 전부 잡음 암기였다는 뜻이다. 셋째, 최적점인 차수 3의 검증 MSE 0.0985는 잡음의 분산 0.32=0.090.3^2 = 0.09 와 거의 같다. 환원불가 오차에 닿았으니 여기서 더 짜내려는 시도는 무의미하다.

세 번째 읽기가 가장 값지다. 목표 숫자를 정할 때 잡음 수준을 먼저 추정해 두면, 도달 불가능한 목표를 쫓느라 몇 주를 쓰는 일을 막는다.

import numpy as np
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import cross_validate

np.random.seed(42)
X = np.linspace(0, 10, 200).reshape(-1, 1)
y = np.sin(X.ravel()) + np.random.randn(200) * 0.3   # 잡음 분산 0.09

for degree in [1, 3, 10, 20]:
    pipe = make_pipeline(PolynomialFeatures(degree), LinearRegression())
    cv = cross_validate(pipe, X, y, cv=5,
                        scoring='neg_mean_squared_error',
                        return_train_score=True)
    train, val = -cv['train_score'].mean(), -cv['test_score'].mean()
    print(f"{degree:2d}  train={train:.4f}  val={val:.4f}  gap={val-train:.4f}")

고편향 처방

표현력의 손잡이

고편향은 모델이 담을 수 있는 함수의 집합이 정답을 포함하지 않는 상태다. 그러니 처방은 하나로 모인다 — 집합을 넓힌다.

가장 직접적인 것은 모델 자체를 바꾸는 것이다. 선형 회귀에서 그래디언트 부스팅으로, 얕은 트리에서 깊은 트리로, 층 둘짜리 신경망에서 넷짜리로. 다음은 입력을 늘리는 것이다. 기존 특성의 제곱·곱·로그를 넣거나, 도메인 지식으로 새 특성을 만든다. 「면적」과 「방 개수」만 있던 자리에 「방당 면적」을 더하는 식인데, 모델이 나눗셈을 스스로 배우기 어려운 구조라면 이 한 칸이 층 하나보다 낫다.

셋째는 학습을 더 오래 하는 것이다. 반복 학습 모델의 훈련 손실이 아직 내려가는 중인데 에폭 한도에 걸려 멈췄다면 그건 과소적합이지 모델의 한계가 아니다. 이 셋을 시도하기 전에 학습률이 너무 커서 수렴하지 못하는 것은 아닌지를 먼저 본다. 손실이 위아래로 튀며 안 내려가는 그림은 표현력 부족과 겉모습이 비슷하지만 처방이 전혀 다르다.

정규화 완화

정규화(Regularization)는 모델이 복잡해지는 것에 값을 매겨 억제하는 장치다. 뒤의 「고분산 처방」이 전부 여기 속하는데, 고편향 상태에서는 반대로 풀어야 한다.

Ridge(alpha=10)을 alpha=0.1로 내리고, SVR(C=1)을 C=100으로 올리고(SVM의 C는 패널티의 역수라 클수록 규제가 약하다), 트리의 min_samples_leaf를 20에서 5로 내린다. 신경망이라면 드롭아웃 비율과 가중치 감쇠 계수를 낮춘다.

여기서 자주 나는 실수가 손잡이의 방향을 헷갈리는 것이다. alpha는 클수록 규제가 세지고 C는 클수록 약해진다. 이름만 보고 「값을 올렸으니 강해졌겠지」로 넘어가면 진단과 정반대의 실험을 하고 결과를 해석하게 된다. 검증 점수가 예상과 반대로 움직이면 손잡이 방향부터 확인한다.

데이터 수집의 한계

고편향 상태에서 데이터를 더 모으는 것은 거의 효과가 없다. 학습 곡선이 이미 평평해졌다는 것은 현재 모델이 이 데이터에서 뽑아낼 수 있는 것을 다 뽑았다는 뜻이기 때문이다. 직선에 점을 더 준다고 곡선이 되지 않는다.

그런데도 「데이터를 더 모으자」가 기본 답으로 나오는 이유는, 과대적합이 훨씬 흔해서 그 처방이 습관이 되었기 때문이다. 진단을 건너뛰고 처방부터 꺼내면 이 자리에서 예산과 시간을 함께 잃는다. 학습 곡선을 한 번 그리는 데 드는 몇 분이 그것을 막는다.

같은 이유로 고편향 모델에는 데이터 증강도 효과가 없다. 증강은 분산을 줄이는 장치이므로, 편향이 문제인 자리에서는 훈련 시간만 몇 배로 늘린다.

고분산 처방

L1·L2 정규화

가장 값싼 처방은 손실 함수에 가중치 크기의 벌점을 더하는 것이다. 손실이 LL 일 때 실제로 최소화하는 값은 다음과 같다.

Ltotal=L+α∑iwi2(L2)Ltotal=L+α∑i∣wi∣(L1)L_{\text{total}} = L + \alpha \sum_i w_i^2 \quad (\text{L2}) \qquad L_{\text{total}} = L + \alpha \sum_i |w_i| \quad (\text{L1})

L2(Ridge)는 가중치를 0 쪽으로 당기되 0에 닿지는 않는다. 미분하면 당기는 힘이 2αw2\alpha w 라 가중치가 작아질수록 힘도 함께 약해지기 때문이다. 모든 특성이 조금씩 기여하는 부드러운 모델이 나온다.

L1(Lasso)은 일부 가중치를 정확히 0으로 만든다. ∣w∣|w| 의 기울기는 부호뿐이라 0에 가까워져도 당기는 힘이 줄지 않기 때문이다. 그래서 특성 선택 효과가 따라온다. 특성이 수백 개인데 실제로 쓰이는 것은 몇십 개일 것 같은 표 데이터에서 쓸모가 크다. 둘을 섞은 ElasticNet은 l1_ratio로 비율을 정하며, 0이면 Ridge, 1이면 Lasso다.

가중치를 억제하는 것이 왜 분산을 줄이는지는 한 문장으로 설명된다. 가중치가 작으면 입력이 조금 달라졌을 때 출력이 크게 흔들리지 않는다. 잡음까지 따라 그리려면 큰 가중치가 필요한데, 벌점이 그 대가를 매겨 놓았으므로 잡음을 좇는 이득이 벌점을 넘을 때만 그렇게 한다.

from sklearn.linear_model import Ridge, Lasso
from sklearn.model_selection import cross_val_score

# X_train, y_train은 train_test_split으로 떼어 둔 훈련 세트다.
for alpha in [0.01, 0.1, 1.0, 10.0, 100.0]:
    score = cross_val_score(Ridge(alpha=alpha), X_train, y_train,
                            cv=5, scoring='r2').mean()
    print(f"Ridge alpha={alpha:6.2f}  CV R²={score:.4f}")

lasso = Lasso(alpha=0.1, max_iter=10000).fit(X_train, y_train)
print(f"Lasso가 남긴 특성: {(lasso.coef_ != 0).sum()}/{X_train.shape[1]}")

alpha는 로그 눈금으로 훑는다. 0.01에서 100까지 열 배씩 다섯 점을 보고 검증 점수가 꺾이는 구간을 찾은 뒤, 그 안에서 촘촘히 다시 본다. 그리고 L1·L2는 특성 스케일에 민감하므로 표준화가 선행되어야 한다 — 단위가 큰 특성은 계수가 작게 나오므로 벌점을 덜 받고, 그러면 규제가 특성마다 다르게 걸린다.

트리의 복잡도 손잡이

트리 모델에는 손실에 항을 더하는 대신 자라는 것 자체를 막는 손잡이가 여럿 있다. 어느 것을 쓰든 목적은 하나 — 잎 하나가 소수의 샘플로 결정되는 일을 막는 것이다.

손잡이 하는 일 감이 잡히는 값
max_depth 트리 깊이 상한 표 데이터에서 4~8
min_samples_leaf 잎의 최소 샘플 수 5~20
min_samples_split 분기에 필요한 최소 샘플 수 잎의 두 배쯤
max_features 분기 때 볼 특성 수 'sqrt'
ccp_alpha 사후 가지치기 강도 0.001~0.05

앞의 넷은 자라기 전에 막는 사전 제약이고, ccp_alpha는 다 키운 뒤 잘라 내는 가지치기(Pruning)다. 사전 제약은 「지금은 이득이 작지만 한 단계 더 내려가면 큰 이득이 있는」 분기를 미리 잘라 버릴 수 있는 반면, 가지치기는 전체를 보고 자르므로 그 함정이 없다. 대신 큰 트리를 한 번 다 키워야 한다.

min_samples_leaf가 가장 먼저 손대기 좋은 값이다. 잎에 샘플이 하나뿐인 트리는 정의상 그 샘플의 잡음을 그대로 예측으로 내보낸다. 이 값을 10으로 올리면 잎마다 최소 열 개의 평균을 쓰게 되어 분산이 곧바로 줄어든다.

조기 종료

에폭을 손잡이로 보면, 학습을 오래 하는 것 자체가 복잡도를 올리는 일이다. 앞에서 분산이 커지는 세 경로 가운데 시간 쪽이라고 불렀던 그것이다. 조기 종료(Early Stopping)는 검증 손실이 정해진 횟수만큼 연속으로 나아지지 않으면 학습을 멈추고 가장 좋았던 가중치로 되돌린다. U자 곡선의 바닥에서 멈추는 장치다.

이 글이 든 처방 가운데 유일하게 추가 비용이 거의 0이라는 점이 조기 종료의 특별한 자리다. 나머지는 전부 무언가를 치른다 — 데이터 수집은 돈과 시간을, 증강은 훈련 시간을, 정규화는 손잡이 탐색을, 앙상블은 모델 개수만큼의 계산을 요구한다. 조기 종료가 요구하는 것은 검증 세트 하나와 「나아지지 않은 에폭을 세는」 계수기 하나뿐이고, 오히려 학습을 일찍 끝내므로 계산이 줄어든다. 그래서 반복 학습 모델이라면 진단 결과와 무관하게 기본으로 켜 두는 편이 낫다.

손잡이는 인내값(patience) 하나다. 너무 짧으면 학습률이 잠시 흔들린 구간을 과적합으로 오해해 일찍 멈추고, 너무 길면 최적점을 한참 지나 계산을 낭비한다. 20 안팎에서 시작해 검증 곡선의 출렁임 폭을 보고 조정한다.

그리고 가장 좋았던 가중치를 저장해 두지 않으면 조기 종료는 반쪽이다. 멈추기만 하면 이미 나빠진 상태로 끝나기 때문이다. 사이킷런의 MLPClassifier는 이것을 내부에서 처리한다.

from sklearn.neural_network import MLPClassifier

# X_train, y_train은 위와 같은 훈련 세트다.
mlp = MLPClassifier(
    hidden_layer_sizes=(256, 128),
    max_iter=1000,
    early_stopping=True,        # 내부에서 검증 세트를 떼어 감시한다
    validation_fraction=0.1,
    n_iter_no_change=20,        # 인내값
    random_state=42,
).fit(X_train, y_train)

print(f"실제 학습 에폭: {mlp.n_iter_} / 최고 검증 점수: {mlp.best_validation_score_:.4f}")

n_iter_가 max_iter와 같게 찍히면 조기 종료가 한 번도 걸리지 않았다는 뜻이다. 그건 아직 과적합 구간에 들어가지도 않았다는 신호이므로 한도를 올려야 한다.

데이터 증강

과대적합 방지 기법

훈련 데이터를 늘리는 것이 분산 문제의 가장 근본적인 해법이다. 학습 곡선의 검증 선이 아직 내려가는 중이라면 다른 무엇보다 이것이 낫다. 새로 수집할 수 없을 때 쓰는 대체재가 데이터 증강(Data Augmentation) — 기존 샘플을 변형해 새 샘플을 만드는 것이다.

증강이 하는 일은 개수를 늘리는 것처럼 보이지만 실제로는 불변성을 주입하는 것이다. 좌우를 뒤집어도 같은 라벨이라고 가르치면 「좌우가 바뀌어도 고양이는 고양이」라는 지식이 모델에 들어간다. 그래서 정규화로 분류된다 — 모델이 가질 수 있는 함수의 범위를 우리가 아는 만큼 좁힌다.

어떤 변형이 허용되는지는 도메인이 정한다. 손글씨 숫자를 180도 돌리면 6이 9가 되고, 의료 영상의 좌우 뒤집기는 장기의 위치를 바꾼다. 수치 표 데이터에서 쓰는 방식은 크게 둘이다. 하나는 각 특성에 작은 가우시안 잡음을 더해 원본 주위에 흩뿌리는 것이고, 다른 하나는 같은 클래스의 두 샘플을 잇는 선분 위에 새 점을 놓는 보간이다. 아래 코드가 앞의 것이다.

import numpy as np

# X_train, y_train은 앞서 분할해 둔 훈련 세트이며 표준화가 끝난 상태다.
def jitter(X, y, std=0.02, times=5):
    """수치 특성에 작은 잡음을 더해 증강한다. std는 표준화된 스케일 기준."""
    Xs = [X] + [X + np.random.randn(*X.shape) * std for _ in range(times)]
    return np.vstack(Xs), np.hstack([y] * (times + 1))

X_aug, y_aug = jitter(X_train, y_train)
print(f"{len(X_train)} → {len(X_aug)}")

잡음의 세기를 잘못 잡으면 역효과가 난다. 너무 작으면 원본의 복사본이라 아무 정보도 더하지 않고, 너무 크면 클래스 경계를 넘어가 라벨이 틀린 샘플을 만든다. 증강 후 검증 점수가 떨어지면 세기부터 의심한다. 표준화가 끝난 데이터에서 0.02는 특성 하나의 표준편차의 2%이니 아주 조심스러운 값이고, 0.2쯤 되면 원본과 다른 점이라 부를 만하지만 그만큼 경계를 넘을 위험도 커진다. 검증 점수를 세기별로 한 줄씩 찍어 보면 어디까지가 안전한지가 바로 보인다.

뒤의 보간 방식은 식 한 줄이면 되지만 성격이 다르다. 두 샘플 x1x_1, x2x_2 와 0과 1 사이의 난수 λ\lambda 로 λx1+(1−λ)x2\lambda x_1 + (1 - \lambda) x_2 를 만들면 새 점이 두 점을 잇는 선분 위에 놓인다. 잡음 주입이 각 점 주위에 구름을 만드는 것이라면, 보간은 이미 있는 점들 사이를 메우는 것이다. 이 선분을 소수 클래스의 샘플끼리만 그어 개수를 채우는 것이 SMOTE(Synthetic Minority Over-sampling Technique)이고, 양성이 전체의 2%뿐인 표 데이터처럼 클래스가 크게 기운 자리에서 자주 쓰인다.

보간에도 같은 종류의 함정이 있다. 두 클래스가 특성 공간에서 뒤엉켜 있으면 소수 클래스의 두 점을 잇는 선분이 다수 클래스의 영역을 가로지르고, 그 위에 놓인 합성 샘플은 라벨이 틀린 점이 된다. 잡음을 너무 크게 넣었을 때와 정확히 같은 사고다. 그래서 증강은 두 방식 모두 「모델이 무엇을 배우기를 바라는가」를 먼저 정한 뒤에 손대야 하고, 켜 놓고 잊어버리면 안 되는 처방에 속한다.

앙상블

배깅

배깅(Bagging)은 훈련 데이터를 복원 추출로 여러 번 뽑아 각각 모델을 학습시킨 뒤 예측을 평균 내는 방법이다. 랜덤 포레스트가 여기에 특성 무작위 선택을 더한 것이다.

평균이 분산에 하는 일이 핵심이다. 분산이 σ2\sigma^2 인 예측 kk 개가 서로 독립이면 평균의 분산은 σ2/k\sigma^2/k 다. 트리 100개면 이론상 100분의 1이다. 그러면서 편향은 거의 그대로다 — 각 트리가 불편(unbiased)에 가까우면 그 평균도 그렇기 때문이다. 편향-분산 평면에서 아래로만 내려가는 흔치 않은 이동이다.

다만 실제 이득은 100분의 1에 한참 못 미친다. 트리들이 같은 데이터에서 나왔으므로 독립이 아니고, 상관계수 ρ\rho 를 넣으면 평균의 분산은 ρσ2+(1−ρ)σ2/k\rho\sigma^2 + (1-\rho)\sigma^2/k 가 된다. 앞의 항은 kk 를 아무리 키워도 남는다. 랜덤 포레스트가 분기마다 특성의 일부만 보는 이유가 이것이다 — 트리 사이의 상관을 억지로 낮춰 첫 항을 깎으려는 것이다.

독립일 때와 상관이 있을 때의 평균 분산 곡선

그래서 랜덤 포레스트에서 트리 개수를 늘리는 것은 안전하지만 어느 지점부터 이득이 없다. 200 언저리에서 검증 점수가 평평해지면 거기서 멈춘다.

부스팅

부스팅(Boosting)은 반대 방향이다. 깊이 3짜리처럼 일부러 약한 모델을 하나씩 순차적으로 쌓되, 각 모델이 앞선 모델들이 남긴 잔차를 학습한다. 편향을 한 걸음씩 깎아 내려가는 구조다.

대가는 분산이다. 뒤로 갈수록 남은 잔차는 점점 잡음에 가까워지므로, 계속 쌓으면 결국 잡음을 학습한다. 그래서 부스팅에는 억제 장치가 함께 붙는다. learning_rate를 0.05처럼 낮춰 한 걸음의 폭을 줄이고, subsample=0.8로 각 단계에서 80%만 써서 무작위성을 넣고, 검증 세트로 조기 종료를 건다.

learning_rate와 n_estimators는 하나의 손잡이의 양면이다. 학습률을 절반으로 내리면 같은 지점에 닿기 위해 트리가 대략 두 배 필요하다. 실무에서는 학습률을 먼저 낮게 고정하고 트리 개수를 조기 종료에 맡기는 조합을 쓴다.

배깅과 부스팅의 선택

배깅 부스팅
기본 모델 깊은 트리 (고분산) 얕은 트리 (고편향)
학습 방식 병렬·독립 순차·의존
줄이는 것 분산 편향
과적합 위험 낮다 높다 (조절 필요)
튜닝 난이도 낮다 높다

진단 결과가 곧 선택이다. 고분산이면 배깅, 고편향이면 부스팅이다. 그리고 배깅을 먼저 시도하는 편이 낫다 — 기본값으로도 잘 동작하고 과적합 위험이 낮아 기준선을 세우기에 좋다. 그 기준선을 넘어야 할 때 부스팅으로 옮겨 가 튜닝 비용을 치른다.

처방의 순서

처방 우선순위

고분산 진단이 나왔을 때 꺼내는 순서다. 위쪽이 효과 대비 부작용이 적다.

순서 처방 쓰는 자리
1 훈련 데이터 추가 수집 학습 곡선의 검증 선이 아직 내려갈 때
2 데이터 증강 수집이 불가능하고 허용 변환이 분명할 때
3 모델 복잡도 축소 깊이·층·파라미터를 줄인다
4 L1·L2 정규화 선형 모델·신경망
5 조기 종료 반복 학습 모델 전부
6 드롭아웃 신경망
7 앙상블(배깅) 단일 고분산 모델을 대체할 때
8 특성 개수 축소 PCA·중요도 기반 선택 — 특성이 샘플 수에 비해 많을 때

한꺼번에 켜지 않는다. 하나씩 켜고 검증 곡선이 어떻게 움직이는지 본 뒤 다음으로 넘어간다. 셋을 동시에 켜서 점수가 올랐다면 그중 무엇이 일했는지, 어느 하나가 오히려 해를 끼치고 있는지 알 수 없다.

여섯 번째 드롭아웃은 학습 중 뉴런 일부를 무작위로 꺼서 매 스텝 다른 서브네트워크를 훈련시키는 기법이라, 손실이나 데이터가 아니라 순전파 자체를 바꾼다는 점에서 앞의 것들과 층위가 다르다. 자세한 것은 드롭아웃에서 다룬다. 손실에 항을 더하는 쪽의 자세한 이야기는 손실 함수와 정규화에 있다.

여덟 번째 특성 개수 축소는 방법이 둘로 갈린다. PCA(주성분 분석)는 원래 특성들을 분산이 큰 방향의 조합으로 다시 써서 차원을 줄이는 방법이고, 중요도 기반 선택은 트리 모델의 특성 중요도나 Lasso가 살려 둔 계수를 보고 쓸모없는 열을 통째로 버리는 방법이다. 앞의 것은 정보를 최대한 남기지만 새 축이 무엇을 뜻하는지 설명하기 어려워지고, 뒤의 것은 남은 열이 원래 이름을 그대로 유지하므로 결과를 사람에게 설명해야 하는 자리에서 유리하다. 이 줄이 맨 아래인 이유도 여기 있다 — 특성을 지우는 것은 되돌리기 어려운 결정이고, 위의 일곱을 다 해 본 뒤에도 특성 수가 샘플 수를 압박할 때 꺼내는 마지막 수단이다.

오진 유형

앞의 둘은 이미 지나온 표에서 본 자리라 한 줄이면 된다. 간격만 보고 판단하는 것 — 다항식 표의 차수 1이 간격 0.0025로 가장 좁으면서 가장 나쁜 모델이었다. 과소적합에 정규화를 더 거는 것 — 「정규화 완화」에서 손잡이 방향과 함께 다뤘다. 둘 다 진단표의 첫 줄과 둘째 줄을 헷갈린 것이므로 그 표로 돌아가면 답이 있다.

셋째는 앞에서 나오지 않은 것이다. 같은 검증 세트를 반복해서 보며 튜닝하는 것이다. 손잡이를 수십 번 돌려 그중 가장 좋은 값을 고르는 순간, 그 검증 세트의 우연까지 함께 고른 것이 된다. 검증 점수는 계속 올라가는데 실제 배포 성능은 꿈쩍도 않는 상황이 여기서 나온다.

숫자로 보면 규모가 실감난다. 검증 세트가 500개이고 실제 정확도가 0.80인 모델을 잰다면 추정값의 표준오차는 0.8×0.2/500≈0.018\sqrt{0.8 \times 0.2 / 500} \approx 0.018 이다. 이제 실제로는 성능이 전부 똑같은 후보 50개를 이 세트로 재고 최고점을 고른다고 하자. 정규분포 50개의 최댓값은 평균에서 표준편차 두 개 남짓 떨어진 자리에 오므로, 고른 값은 0.84 언저리로 찍힌다. 아무것도 나아지지 않았는데 4%p가 생긴 것이다. 튜닝 그리드를 넓힐수록 이 거품은 커진다.

막는 방법은 셋이다. 첫째, 테스트 세트는 마지막에 한 번만 연다. 두 번째로 여는 순간 그것도 검증 세트가 된다. 둘째, 검증 점수를 한 번의 분할이 아니라 교차 검증의 평균으로 재면 추정의 표준오차가 줄어 거품도 함께 줄어든다. 셋째, 후보 사이의 점수 차이가 표준오차보다 작으면 그 차이는 없는 것으로 친다 — 그럴 때는 점수가 아니라 단순함을 기준으로 고른다. 앞의 예에서 0.836과 0.828 사이를 고민하는 것은 0.018짜리 자로 0.008을 재려는 일이다.

진단과 처방이 갖춰지면 남은 것은 무엇으로 잴 것인가다. 지금까지는 MSE와 정확도를 아무렇지 않게 써 왔지만, 양성이 1%뿐인 문제에서 정확도 99%는 전부 음성이라고 답한 모델도 받는 점수다. 다음 글에서는 예측을 네 칸으로 갈라 세는 표에서 출발해, 어떤 문제에서 어떤 지표를 봐야 하는지를 정리한다.


읽어주셔서 감사합니다. 😊

LATEST

머신러닝·신경망의 최신 글

머신러닝·신경망2026.05.08

문맥적 임베딩: ELMo부터 BERT까지

정적 임베딩의 다의어 문제를 해결하는 문맥적 임베딩의 원리, ELMo의 양방향 LSTM 레이어 표현, BERT의 트랜스포머 기반 서브워드 임베딩 추출법을 수식과 코드로 완전히 해설한다.

12 MIN
머신러닝·신경망2026.05.08

FastText: 부분 단어로 OOV를 정복하다

FastText가 문자 n-gram 기반의 부분 단어 모델로 OOV 문제를 해결하는 방법, 한국어 형태론에서의 강점, 실전 학습과 추론 코드를 완전히 해설한다.

11 MIN
머신러닝·신경망2026.05.08

GloVe: 전역 공기 통계로 단어 벡터를 만들다

GloVe가 공기 행렬의 전역 통계와 국소 문맥 창의 장점을 결합하는 방법, 목적 함수의 수학적 의미, 사전 학습 벡터 활용법을 깊이 있게 다룬다.

11 MIN