Ridge & Lasso Regression (정규화 선형 회귀)¶
개요¶
문제 정의¶
일반 선형 회귀(OLS)의 한계:
- 다중공선성: 특성 간 상관이 높으면 계수 추정 불안정
- 과적합: 특성이 많으면 훈련 데이터에 과적합
- 고차원: n < d (샘플 < 특성)이면 해가 유일하지 않음
핵심 아이디어¶
손실 함수에 가중치 크기를 제한하는 정규화 항 추가:
| 방법 | 정규화 항 | 특징 |
|---|---|---|
| Ridge (L2) | lambda * ||w||_2^2 | 계수 축소 |
| Lasso (L1) | lambda * ||w||_1 | 희소 해 (특성 선택) |
| Elastic Net | alpha * ||w||_1 + (1-alpha) * ||w||_2^2 | L1 + L2 조합 |
알고리즘/수식¶
Ridge Regression (L2 정규화)¶
목적 함수:
해석적 해:
- lambdaI 항이 X^TX의 대각 원소에 추가
- 항상 역행렬 존재 (양의 정부호)
- 다중공선성 문제 해결
기하학적 해석:
OLS 해를 원점 방향으로 축소 (shrinkage): - lambda -> 0: OLS 해에 수렴 - lambda -> inf: w -> 0 (모든 계수 0)
Lasso Regression (L1 정규화)¶
목적 함수:
해석적 해 없음 -> 반복적 최적화 필요
희소 해 (Sparse Solution): - L1 페널티의 기하학적 특성으로 일부 계수가 정확히 0 - 자동 특성 선택 효과
Elastic Net¶
목적 함수:
- alpha = 1: Lasso
- alpha = 0: Ridge
- 0 < alpha < 1: 둘의 조합
장점: - Lasso의 희소성 + Ridge의 안정성 - 상관된 특성 그룹을 함께 선택/제거 - Lasso의 "n개 특성만 선택" 제한 극복
비교: Ridge vs Lasso¶
Ridge (L2) | Lasso (L1)
------------------------------|------------------------------
제약 영역: 원 (구) | 제약 영역: 다이아몬드
모든 계수 축소 | 일부 계수 = 0 (희소)
해석적 해 존재 | 반복 최적화 필요
다중공선성에 강건 | 특성 선택 가능
특성 선택 없음 | 그룹 중 하나만 선택
미분 가능 | 원점에서 미분 불가
정규화 경로 (Regularization Path)¶
lambda 변화에 따른 계수 변화:
시간 복잡도¶
| 방법 | 복잡도 |
|---|---|
| Ridge (해석적) | O(n*d^2 + d^3) |
| Lasso (좌표 하강) | O(nditer) |
| Elastic Net | O(nditer) |
하이퍼파라미터 가이드¶
Ridge¶
| 파라미터 | 설명 | 권장 범위 | 기본값 |
|---|---|---|---|
| alpha | 정규화 강도 (lambda) | 0.01 ~ 1000 | 1.0 |
| solver | 최적화 알고리즘 | 'auto', 'svd', 'cholesky', 'lsqr', 'saga' | 'auto' |
| fit_intercept | 절편 학습 | True/False | True |
Lasso¶
| 파라미터 | 설명 | 권장 범위 | 기본값 |
|---|---|---|---|
| alpha | 정규화 강도 | 0.0001 ~ 10 | 1.0 |
| max_iter | 최대 반복 | 1000 ~ 10000 | 1000 |
| tol | 수렴 임계값 | 1e-4 ~ 1e-6 | 1e-4 |
| selection | 좌표 선택 | 'cyclic', 'random' | 'cyclic' |
Elastic Net¶
| 파라미터 | 설명 | 권장 범위 | 기본값 |
|---|---|---|---|
| alpha | 정규화 강도 | 0.0001 ~ 10 | 1.0 |
| l1_ratio | L1 비율 | 0 ~ 1 | 0.5 |
Python 코드 예시¶
기본 사용법¶
import numpy as np
import pandas as pd
from sklearn.linear_model import Ridge, Lasso, ElasticNet, LinearRegression
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import mean_squared_error, r2_score
import matplotlib.pyplot as plt
# 데이터 로드
data = fetch_california_housing()
X, y = data.data, data.target
# Train/test split
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# 스케일링 (정규화에 필수!)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
# 모델 비교
models = {
'OLS': LinearRegression(),
'Ridge': Ridge(alpha=1.0),
'Lasso': Lasso(alpha=0.01),
'ElasticNet': ElasticNet(alpha=0.01, l1_ratio=0.5)
}
print("=== Model Comparison ===")
print(f"{'Model':<15} {'R2':>8} {'RMSE':>8} {'Non-zero coef':>15}")
print("-" * 50)
for name, model in models.items():
model.fit(X_train_scaled, y_train)
y_pred = model.predict(X_test_scaled)
r2 = r2_score(y_test, y_pred)
rmse = np.sqrt(mean_squared_error(y_test, y_pred))
n_nonzero = np.sum(model.coef_ != 0)
print(f"{name:<15} {r2:>8.4f} {rmse:>8.4f} {n_nonzero:>15}")
정규화 경로 시각화¶
# Ridge 정규화 경로
alphas_ridge = np.logspace(-4, 4, 100)
coefs_ridge = []
for alpha in alphas_ridge:
ridge = Ridge(alpha=alpha)
ridge.fit(X_train_scaled, y_train)
coefs_ridge.append(ridge.coef_)
coefs_ridge = np.array(coefs_ridge)
# Lasso 정규화 경로
alphas_lasso = np.logspace(-4, 1, 100)
coefs_lasso = []
for alpha in alphas_lasso:
lasso = Lasso(alpha=alpha, max_iter=10000)
lasso.fit(X_train_scaled, y_train)
coefs_lasso.append(lasso.coef_)
coefs_lasso = np.array(coefs_lasso)
# 시각화
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
for i, name in enumerate(data.feature_names):
axes[0].plot(alphas_ridge, coefs_ridge[:, i], label=name)
axes[0].set_xscale('log')
axes[0].set_xlabel('alpha')
axes[0].set_ylabel('Coefficients')
axes[0].set_title('Ridge Regularization Path')
axes[0].legend(loc='upper right', fontsize=8)
for i, name in enumerate(data.feature_names):
axes[1].plot(alphas_lasso, coefs_lasso[:, i], label=name)
axes[1].set_xscale('log')
axes[1].set_xlabel('alpha')
axes[1].set_ylabel('Coefficients')
axes[1].set_title('Lasso Regularization Path')
axes[1].legend(loc='upper right', fontsize=8)
plt.tight_layout()
plt.savefig('regularization_path.png', dpi=150)
plt.show()
교차 검증으로 최적 alpha 찾기¶
from sklearn.linear_model import RidgeCV, LassoCV, ElasticNetCV
# RidgeCV
ridge_cv = RidgeCV(
alphas=np.logspace(-4, 4, 50),
cv=5
)
ridge_cv.fit(X_train_scaled, y_train)
print(f"Ridge best alpha: {ridge_cv.alpha_:.4f}")
print(f"Ridge CV R2: {ridge_cv.score(X_test_scaled, y_test):.4f}")
# LassoCV
lasso_cv = LassoCV(
alphas=np.logspace(-4, 1, 50),
cv=5,
max_iter=10000
)
lasso_cv.fit(X_train_scaled, y_train)
print(f"\nLasso best alpha: {lasso_cv.alpha_:.4f}")
print(f"Lasso CV R2: {lasso_cv.score(X_test_scaled, y_test):.4f}")
print(f"Lasso selected features: {np.sum(lasso_cv.coef_ != 0)}/{len(lasso_cv.coef_)}")
# ElasticNetCV
enet_cv = ElasticNetCV(
l1_ratio=[0.1, 0.5, 0.7, 0.9, 0.95, 0.99],
alphas=np.logspace(-4, 1, 50),
cv=5,
max_iter=10000
)
enet_cv.fit(X_train_scaled, y_train)
print(f"\nElasticNet best alpha: {enet_cv.alpha_:.4f}")
print(f"ElasticNet best l1_ratio: {enet_cv.l1_ratio_:.2f}")
print(f"ElasticNet CV R2: {enet_cv.score(X_test_scaled, y_test):.4f}")
Lasso를 통한 특성 선택¶
# Lasso로 중요 특성 식별
lasso = Lasso(alpha=0.01, max_iter=10000)
lasso.fit(X_train_scaled, y_train)
feature_selection = pd.DataFrame({
'feature': data.feature_names,
'coefficient': lasso.coef_,
'selected': lasso.coef_ != 0
}).sort_values('coefficient', key=abs, ascending=False)
print("\nLasso Feature Selection:")
print(feature_selection.to_string(index=False))
# 선택된 특성만으로 재학습
selected_features = feature_selection[feature_selection['selected']]['feature'].tolist()
selected_idx = [list(data.feature_names).index(f) for f in selected_features]
X_train_selected = X_train_scaled[:, selected_idx]
X_test_selected = X_test_scaled[:, selected_idx]
lr_selected = LinearRegression()
lr_selected.fit(X_train_selected, y_train)
y_pred_selected = lr_selected.predict(X_test_selected)
print(f"\nOLS with selected features:")
print(f"R2: {r2_score(y_test, y_pred_selected):.4f}")
print(f"Features used: {len(selected_features)}/{len(data.feature_names)}")
다중공선성 시뮬레이션¶
# 다중공선성 있는 데이터 생성
np.random.seed(42)
n = 200
x1 = np.random.randn(n)
x2 = x1 + np.random.randn(n) * 0.1 # x1과 높은 상관
x3 = np.random.randn(n)
y_sim = 2*x1 + 3*x3 + np.random.randn(n) * 0.5
X_sim = np.column_stack([x1, x2, x3])
feature_names_sim = ['x1', 'x2 (corr with x1)', 'x3']
# OLS vs Ridge
lr_sim = LinearRegression()
lr_sim.fit(X_sim, y_sim)
ridge_sim = Ridge(alpha=1.0)
ridge_sim.fit(X_sim, y_sim)
print("\n=== Multicollinearity Simulation ===")
print(f"Correlation between x1 and x2: {np.corrcoef(x1, x2)[0,1]:.4f}")
print(f"\n{'Feature':<20} {'OLS coef':>12} {'Ridge coef':>12}")
print("-" * 50)
for i, name in enumerate(feature_names_sim):
print(f"{name:<20} {lr_sim.coef_[i]:>12.4f} {ridge_sim.coef_[i]:>12.4f}")
print("\nNote: Ridge는 상관된 특성에 계수를 분산시킴")
언제 쓰나?¶
Ridge¶
적합한 상황: - 다중공선성이 있을 때 - 모든 특성이 어느 정도 중요할 때 - 예측 성능 향상이 목적일 때 - 해석적 해가 필요할 때
Lasso¶
적합한 상황: - 특성 선택이 필요할 때 - 희소 모델이 필요할 때 (많은 특성 중 소수만 중요) - 해석 가능한 모델이 필요할 때 - 고차원 데이터 (d >> n)
Elastic Net¶
적합한 상황: - 상관된 특성 그룹이 있을 때 - Lasso가 불안정할 때 - Ridge와 Lasso 사이 균형이 필요할 때
장단점¶
| 방법 | 장점 | 단점 |
|---|---|---|
| Ridge | 안정적, 해석적 해, 다중공선성 해결 | 특성 선택 불가 |
| Lasso | 희소 해, 특성 선택, 해석 용이 | 불안정할 수 있음, 그룹 중 하나만 선택 |
| Elastic Net | Ridge + Lasso 장점 조합 | 추가 하이퍼파라미터 (l1_ratio) |
관련 논문/참고¶
- Hoerl, A.E., & Kennard, R.W. (1970). "Ridge regression: Biased estimation for nonorthogonal problems". Technometrics.
- Tibshirani, R. (1996). "Regression shrinkage and selection via the lasso". Journal of the Royal Statistical Society.
- Zou, H., & Hastie, T. (2005). "Regularization and variable selection via the elastic net". Journal of the Royal Statistical Society.
- scikit-learn documentation: https://scikit-learn.org/stable/modules/linear_model.html