포스트

머신러닝 기초 (6) - 규제: Ridge와 Lasso

과적합을 손실 함수에 페널티를 더해 억제하는 규제를 정리합니다. 계수를 수축시키는 L2(Ridge)와 계수를 0으로 만들어 특성을 고르는 L1(Lasso), 강도 alpha의 역할을 다항 회귀 예제로 확인합니다.

머신러닝 기초 (6) - 규제: Ridge와 Lasso

머신러닝 기초 시리즈의 6편입니다. 5편의 “교차검증과 하이퍼파라미터 튜닝”에 이어집니다.

과적합을 손실 함수로 되받아친다

4편에서 과적합을 훈련 점수와 검증 점수의 벌어진 간격으로 진단했고, 5편에서 교차검증으로 하이퍼파라미터를 골랐습니다. 이번 편은 과적합을 애초에 억제하는 방법 하나를 다룹니다. 규제(regularization)입니다.

지금까지 모델은 2편에서 정의한 손실, 즉 훈련 데이터에 대한 오차만 최소화했습니다. 회귀라면 MSE입니다. 규제는 이 손실에 항을 하나 더 붙입니다.

규제된 손실 = 데이터 적합 오차 + alpha × 계수 크기에 대한 페널티

모델은 이제 두 항의 합을 최소화합니다. 훈련 오차를 줄이려는 힘과, 계수를 작게 유지하려는 힘이 맞섭니다. alpha는 그 힘의 세기를 정하는 값입니다. alpha가 0이면 페널티가 사라져 보통 회귀와 같고, alpha가 커질수록 “계수를 작게 유지하라”는 압력이 세집니다.

왜 하필 계수 크기를 벌하는가

4편에서 본 과적합의 전형은 고차 다항 회귀입니다. 곡선이 훈련 데이터의 잡음까지 따라가느라 심하게 요동쳤습니다. 그 요동을 만드는 것이 큰 계수입니다.

계수가 크다는 것은 해당 특성이 조금만 변해도 예측이 크게 흔들린다는 뜻입니다. 잡음에 민감하게 반응하는 모델, 즉 4편에서 말한 분산이 큰 모델입니다. 계수를 억누르면 반응이 완만해지고 분산이 줄어듭니다. 대신 데이터에 덜 맞추게 되니 편향은 조금 늘어납니다. 규제는 편향-분산 트레이드오프를 alpha 하나로 조절합니다.

L2 규제: Ridge

Ridge는 페널티로 계수 제곱의 합을 씁니다.

Ridge 손실 = MSE + alpha × (w12 + w22 + … + wn**2)

제곱이라 큰 계수일수록 더 크게 벌받습니다. 그래서 Ridge는 계수를 전반적으로 0 쪽으로 수축(shrink)시킵니다. 다만 완전히 0으로 만들지는 않습니다. 모든 특성이 조금씩 기여하는 상태로 계수만 작아집니다. 특성들이 서로 상관되어 있을 때 특히 안정적으로 동작합니다.

L1 규제: Lasso

Lasso는 페널티로 계수 절댓값의 합을 씁니다.

Lasso 손실 = MSE + alpha × (w1+w2+ … +wn)

Ridge와의 결정적 차이는 결과의 모양입니다. Lasso는 기여가 작은 특성의 계수를 정확히 0으로 만듭니다. 계수가 0이면 그 특성은 예측에서 아예 빠지므로, Lasso는 규제와 특성 선택(feature selection)을 동시에 합니다.

왜 0까지 가는가는 두 페널티가 0 근처에서 어떻게 행동하는지로 설명됩니다. 절댓값 페널티는 계수가 0에서 멀어질 때 일정한 비율로 벌점이 붙습니다. 그래서 기여가 작은 특성은 계수를 0으로 밀어버리는 편이 손실상 이득입니다. 반면 제곱 페널티는 계수가 0에 가까워질수록 벌점이 급격히 작아져서, 끝까지 0으로 밀 유인이 약합니다. 결과적으로 Lasso의 계수 벡터는 희소(sparse)해집니다. 특성이 많고 그중 일부만 실제로 중요할 때 유용합니다.

ElasticNet은 L1과 L2 페널티를 함께 쓰는 모델로, l1_ratio로 둘의 비율을 정합니다. 특성 선택(L1)과 상관된 특성에 대한 안정성(L2)을 동시에 원할 때 씁니다.

규제 전에는 표준화가 필요하다

규제는 계수 크기를 벌하는데, 특성마다 스케일이 다르면 계수 크기의 의미가 특성마다 달라집니다. 단위가 큰 특성은 계수가 자연히 작아지고 단위가 작은 특성은 계수가 커지므로, 페널티가 특성마다 불공평하게 걸립니다. 그래서 규제 모델 앞에는 거의 항상 표준화(StandardScaler)를 둬서 모든 특성을 같은 스케일로 맞춥니다. 표준화를 비롯한 스케일링은 12편에서 따로 다룹니다.

스케일러는 반드시 파이프라인 안에 넣어 훈련 데이터에만 fit 해야 합니다. 테스트 데이터의 통계까지 보고 스케일을 맞추면 데이터 누수가 됩니다. 자세한 내용은 13편에서 다룹니다.

코드: 다항 회귀에 Ridge와 Lasso 적용

먼저 과적합하기 쉬운 상황을 만듭니다. 당뇨병 데이터(load_diabetes)의 특성 10개를 2차 다항으로 확장하면 65개가 됩니다. 훈련 표본 309개에 특성 65개라, 규제 없이는 잡음까지 맞추기 좋은 조건입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
import numpy as np
from sklearn.datasets import load_diabetes
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import PolynomialFeatures, StandardScaler
from sklearn.linear_model import LinearRegression, Ridge, Lasso
from sklearn.pipeline import make_pipeline

X, y = load_diabetes(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42)

poly = PolynomialFeatures(degree=2, include_bias=False)
poly.fit_transform(X_train).shape   # (309, 65), 특성 10개가 65개로

규제 없는 다항 회귀를 기준선으로 둡니다. 특성을 확장한 뒤 표준화하고 선형 회귀를 붙입니다. 표준화를 다항 확장 뒤에 두는 이유는 앞 절과 같습니다. 제곱항과 교차항의 스케일이 제각각이라 페널티를 공평하게 걸려면 스케일부터 맞춰야 합니다.

1
2
3
4
5
6
7
8
lin = make_pipeline(
    PolynomialFeatures(degree=2, include_bias=False),
    StandardScaler(),
    LinearRegression())
lin.fit(X_train, y_train)

lin.score(X_train, y_train)   # 훈련 R2 = 0.608
lin.score(X_test, y_test)     # 테스트 R2 = 0.413. 간격이 벌어진 과적합

훈련 0.608, 테스트 0.413. 4편에서 본 과적합 신호입니다. 여기에 Ridge를 걸고 alpha를 키워 봅니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
for a in [0.1, 1, 10, 100]:
    m = make_pipeline(
        PolynomialFeatures(degree=2, include_bias=False),
        StandardScaler(),
        Ridge(alpha=a))
    m.fit(X_train, y_train)
    print(a, round(m.score(X_train, y_train), 3),
             round(m.score(X_test, y_test), 3))

# 0.1   0.606 0.429
# 1     0.603 0.447
# 10    0.593 0.469
# 100   0.562 0.488   <- alpha를 키우면 훈련은 내려가고 테스트는 오른다

alpha가 커질수록 훈련 점수는 조금씩 내려가고(적합을 포기) 테스트 점수는 올라갑니다(분산 감소). 규제가 과적합을 실제로 줄이고 있습니다. 계수의 크기도 함께 줄어드는 것을 직접 확인할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
for a in [0.1, 10, 100]:
    m = make_pipeline(
        PolynomialFeatures(degree=2, include_bias=False),
        StandardScaler(),
        Ridge(alpha=a))
    m.fit(X_train, y_train)
    coef = m.named_steps["ridge"].coef_
    print(a, round(np.abs(coef).max(), 1))

# 0.1   63.1
# 10    21.4
# 100   17.2   <- 가장 큰 계수의 절댓값이 alpha와 함께 작아진다

이제 같은 자리에 Lasso를 넣습니다. Ridge와 달리 계수가 0이 되는 개수를 함께 셉니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for a in [0.1, 1, 10]:
    m = make_pipeline(
        PolynomialFeatures(degree=2, include_bias=False),
        StandardScaler(),
        Lasso(alpha=a, max_iter=10000))
    m.fit(X_train, y_train)
    coef = m.named_steps["lasso"].coef_
    nz = int((coef != 0).sum())
    print(a, round(m.score(X_train, y_train), 3),
             round(m.score(X_test, y_test), 3),
             f"{nz}/{len(coef)}")

# 0.1   0.599 0.461 55/65
# 1     0.575 0.493 33/65   <- 65개 중 32개를 0으로: 특성 선택
# 10    0.461 0.458 5/65    <- 너무 강한 규제. 5개만 남아 과소적합

alpha가 커질수록 살아남는 특성 수(0이 아닌 계수)가 65 → 55 → 33 → 5로 줄어듭니다. alpha=1에서 특성을 절반으로 줄이고도 테스트 점수는 오히려 가장 좋습니다. 그러나 alpha=10처럼 너무 세게 걸면 필요한 특성까지 버려 과소적합으로 넘어갑니다. 규제는 많을수록 좋은 것이 아니라 적당한 지점이 있습니다.

정리하면, alpha 하나가 편향-분산을 조절하는 하이퍼파라미터입니다. 최적값은 눈대중이 아니라 5편의 교차검증으로 고릅니다. scikit-learn은 이를 내장한 RidgeCV, LassoCV를 제공해 여러 alpha를 교차검증으로 한 번에 비교할 수 있습니다.

정리

개념한 줄 요약
규제손실에 계수 크기 페널티를 더해 과적합을 억제
규제 손실데이터 적합 오차 + alpha × 계수 페널티
L2 (Ridge)계수 제곱 합을 벌점. 계수를 0쪽으로 수축하되 0으로 만들지 않음
L1 (Lasso)계수 절댓값 합을 벌점. 일부 계수를 정확히 0으로 → 특성 선택
ElasticNetL1과 L2를 l1_ratio로 섞은 모델
alpha규제 강도. 0이면 보통 회귀, 커질수록 계수↓, 편향↑, 분산↓
표준화페널티를 특성마다 공평하게 걸려면 규제 전에 스케일을 맞춰야 함
alpha 선택눈대중이 아니라 교차검증으로(RidgeCV, LassoCV)

다음 편에서는 회귀에서 잠시 벗어나, 분류 모델을 어떻게 평가하는지, 혼동행렬과 ROC-AUC를 정리합니다.

다음 글: 머신러닝 기초 (7) - 분류 평가: 혼동행렬과 ROC-AUC

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.