22  회귀 모델

Keywords

python, 전처리, 통계, 가설검정, 기계학습, 회귀, 분류, 군집, 모델 학습, 모델 평가

회귀 모델(Regression Models)은 입력 변수(X)와 연속형 타겟 변수(y) 사이의 관계를 학습하여 새로운 입력에 대한 수치값을 예측하는 지도 학습 모델이다. 예측 대상이 범주가 아닌 수치이며, 관계를 수학적 함수 형태로 모델링한다. 이 장에서는 선형 회귀부터 정규화 회귀(Ridge, Lasso, ElasticNet)까지 주요 회귀 모델의 원리와 실무 활용법을 학습한다.

예제: 데이터 로드

import seaborn as sns
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import mean_squared_error, r2_score, mean_absolute_error

# 데이터 로드
df = sns.load_dataset("penguins").dropna()

# 특성과 타겟 준비
X = df[["bill_length_mm", "bill_depth_mm", "flipper_length_mm"]]
y = df["body_mass_g"]

print("데이터 크기:", df.shape)
print("\n특성 변수:", X.columns.tolist())
print("타겟 변수: body_mass_g (연속형)")
print(f"\n타겟 범위: {y.min():.0f}g ~ {y.max():.0f}g")
데이터 크기: (333, 7)

특성 변수: ['bill_length_mm', 'bill_depth_mm', 'flipper_length_mm']
타겟 변수: body_mass_g (연속형)

타겟 범위: 2700g ~ 6300g

22.1 회귀의 개념

회귀는 입력 변수와 출력 변수 간의 관계를 모델링하여 예측하는 기법이다.

회귀 vs 분류

구분 회귀 (Regression) 분류 (Classification)
출력 타입 연속형 (수치) 범주형 (클래스)
예측값 실수 값 클래스 라벨 또는 확률
예시 체중, 가격, 온도 종, 합격/불합격, 질병 유무
평가 지표 MSE, MAE, R² 정확도, 정밀도, 재현율
대표 모델 선형 회귀, Ridge, Lasso 로지스틱 회귀, 결정 트리

회귀 문제의 예

분야 입력 변수 출력 변수
부동산 면적, 방 개수, 위치 주택 가격
의료 나이, BMI, 혈압 혈당 수치
마케팅 광고비, 계절, 프로모션 매출액
생물학 부리 길이, 날개 길이 체중

22.2 데이터 준비 및 분할

예제: 학습/테스트 분할

# 데이터 분할
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

print("=== 데이터 분할 ===")
print(f"학습 데이터: {X_train.shape}")
print(f"테스트 데이터: {X_test.shape}")

# 타겟 분포 확인
print("\n=== 타겟 분포 ===")
print(f"학습셋 평균: {y_train.mean():.2f}g, 표준편차: {y_train.std():.2f}g")
print(f"테스트셋 평균: {y_test.mean():.2f}g, 표준편차: {y_test.std():.2f}g")
=== 데이터 분할 ===
학습 데이터: (266, 3)
테스트 데이터: (67, 3)

=== 타겟 분포 ===
학습셋 평균: 4214.76g, 표준편차: 807.92g
테스트셋 평균: 4176.49g, 표준편차: 799.68g

22.3 선형 회귀 (Linear Regression)

선형 회귀는 입력 변수와 출력 변수 사이의 관계를 선형 결합으로 표현하는 가장 기본적인 회귀 모델이다.

선형 회귀 수식

\[ y = \beta_0 + \beta_1 x_1 + \beta_2 x_2 + \cdots + \beta_p x_p + \varepsilon \]

여기서: - \(y\): 타겟 변수 - \(x_i\): 입력 변수 - \(\beta_0\): 절편 (intercept) - \(\beta_i\): 회귀 계수 (coefficient) - \(\varepsilon\): 오차항

선형 회귀의 가정

가정 설명 확인 방법
선형성 X와 y가 선형 관계 잔차 플롯
독립성 관측치들이 서로 독립 실험 설계 확인
등분산성 오차의 분산이 일정 잔차 플롯
정규성 오차가 정규분포를 따름 Q-Q plot, 히스토그램
다중공선성 없음 독립변수들이 서로 독립 VIF, 상관행렬

현실에서는 완벽히 만족하지 않는 경우가 많지만, 어느 정도 근사하면 사용 가능하다.

22.3.1 선형 회귀 실습

예제: 선형 회귀 학습

from sklearn.linear_model import LinearRegression

# 모델 생성 및 학습
lr = LinearRegression()
lr.fit(X_train, y_train)

# 모델 파라미터 확인
print("=== 선형 회귀 계수 ===")
coef_df = pd.DataFrame({
    'Feature': X.columns,
    'Coefficient': lr.coef_
}).sort_values('Coefficient', key=abs, ascending=False)
print(coef_df)
print(f"\n절편 (intercept): {lr.intercept_:.2f}")

# 예측
y_pred_train = lr.predict(X_train)
y_pred_test = lr.predict(X_test)

# 평가
print("\n=== 모델 성능 ===")
print(f"학습 R²: {r2_score(y_train, y_pred_train):.4f}")
print(f"테스트 R²: {r2_score(y_test, y_pred_test):.4f}")
print(f"테스트 RMSE: {np.sqrt(mean_squared_error(y_test, y_pred_test)):.2f}g")
print(f"테스트 MAE: {mean_absolute_error(y_test, y_pred_test):.2f}g")
=== 선형 회귀 계수 ===
             Feature  Coefficient
2  flipper_length_mm    50.247255
1      bill_depth_mm    10.058133
0     bill_length_mm     3.857683

절편 (intercept): -6227.69

=== 모델 성능 ===
학습 R²: 0.7551
테스트 R²: 0.7981
테스트 RMSE: 356.65g
테스트 MAE: 289.69g

예제: 회귀 계수 시각화

# 계수 시각화
plt.figure(figsize=(10, 5))
plt.barh(coef_df['Feature'], coef_df['Coefficient'])
plt.xlabel('Coefficient Value')
plt.title('Linear Regression Coefficients')
plt.axvline(x=0, color='red', linestyle='--', linewidth=1)
plt.tight_layout()
plt.show()

예제: 예측 vs 실제값

# 예측 vs 실제값 플롯
plt.figure(figsize=(10, 6))
plt.scatter(y_test, y_pred_test, alpha=0.6, edgecolors='k')
plt.plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], 
         'r--', linewidth=2, label='Perfect Prediction')
plt.xlabel('Actual Body Mass (g)')
plt.ylabel('Predicted Body Mass (g)')
plt.title('Linear Regression: Actual vs Predicted')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

# 상관계수
corr = np.corrcoef(y_test, y_pred_test)[0, 1]
print(f"예측-실제 상관계수: {corr:.4f}")

예측-실제 상관계수: 0.8943

예제: 잔차 분석

# 잔차 계산
residuals = y_test - y_pred_test

# 잔차 플롯
fig, axes = plt.subplots(1, 3, figsize=(18, 5))

# 잔차 vs 예측값
axes[0].scatter(y_pred_test, residuals, alpha=0.6, edgecolors='k')
axes[0].axhline(y=0, color='r', linestyle='--', linewidth=2)
axes[0].set_xlabel('Predicted Values')
axes[0].set_ylabel('Residuals')
axes[0].set_title('Residual Plot')
axes[0].grid(True, alpha=0.3)

# 잔차 히스토그램
axes[1].hist(residuals, bins=20, edgecolor='black', alpha=0.7)
axes[1].axvline(x=0, color='r', linestyle='--', linewidth=2)
axes[1].set_xlabel('Residuals')
axes[1].set_ylabel('Frequency')
axes[1].set_title('Residual Distribution')

# Q-Q plot
from scipy import stats
stats.probplot(residuals, dist="norm", plot=axes[2])
axes[2].set_title('Q-Q Plot of Residuals')

plt.tight_layout()
plt.show()

22.3.2 선형 회귀의 한계

선형 회귀의 문제점

문제 설명 증상
다중공선성 독립변수 간 높은 상관 계수가 불안정, 부호 반전
과적합 학습 데이터에 과도 최적화 학습 성능 >> 테스트 성능
고차원 문제 변수가 샘플보다 많음 해가 무수히 많음
이상치 민감 극단값에 크게 영향받음 계수가 왜곡됨

이러한 문제를 해결하기 위해 정규화 회귀(Regularization)를 사용한다.

22.4 정규화(Regularization)의 개념

정규화는 모델의 복잡도를 제한하여 과적합을 방지하는 기법이다.

정규화가 필요한 이유

  • 계수가 너무 커지는 것을 방지
  • 불필요한 변수의 영향 축소
  • 과적합 감소
  • 모델의 일반화 성능 향상

정규화 손실 함수

\[ \text{Loss} = \text{MSE} + \lambda \times \text{Penalty} \]

여기서: - MSE: 평균제곱오차 (예측 오차) - λ (lambda 또는 alpha): 규제 강도 - Penalty: L1, L2, 또는 둘의 조합

λ (규제 강도) 효과

λ 값 효과
λ = 0 일반 선형 회귀 (규제 없음)
λ 작음 약한 규제, 계수 조금 축소
λ 적절 최적의 편향-분산 균형
λ 큼 강한 규제, 과소적합 위험

22.5 Ridge 회귀 (L2 정규화)

Ridge 회귀는 계수의 제곱합(L2 norm)에 패널티를 부여하는 방법이다.

Ridge 손실 함수

\[ \text{Loss}_{\text{Ridge}} = \text{MSE} + \alpha \sum_{i=1}^{p} \beta_i^2 \]

Ridge의 특징

특징 설명
계수 축소 모든 계수를 0에 가깝게 만듦
변수 유지 계수를 0으로 만들지는 않음
다중공선성 완화 상관 높은 변수들 안정화
해석 모든 변수가 기여
적용 상황 변수 간 상관 높을 때

예제: Ridge 회귀

from sklearn.linear_model import Ridge

# 데이터 표준화 (Ridge는 스케일에 민감)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

# Ridge 회귀
ridge = Ridge(alpha=1.0)
ridge.fit(X_train_scaled, y_train)

# 계수 비교
print("=== Ridge vs 선형 회귀 계수 비교 ===")
comparison_df = pd.DataFrame({
    'Feature': X.columns,
    'Linear Regression': lr.coef_,
    'Ridge (α=1.0)': ridge.coef_
})
print(comparison_df.round(2))

# 예측 및 평가
y_pred_ridge = ridge.predict(X_test_scaled)
print(f"\n=== Ridge 성능 ===")
print(f"테스트 R²: {r2_score(y_test, y_pred_ridge):.4f}")
print(f"테스트 RMSE: {np.sqrt(mean_squared_error(y_test, y_pred_ridge)):.2f}g")
=== Ridge vs 선형 회귀 계수 비교 ===
             Feature  Linear Regression  Ridge (α=1.0)
0     bill_length_mm               3.86          24.89
1      bill_depth_mm              10.06          16.44
2  flipper_length_mm              50.25         690.57

=== Ridge 성능 ===
테스트 R²: 0.7977
테스트 RMSE: 356.98g

예제: Alpha 값에 따른 계수 변화

# 다양한 alpha 값으로 실험
alphas = [0.01, 0.1, 1.0, 10.0, 100.0]
coefs = []

for alpha in alphas:
    ridge_temp = Ridge(alpha=alpha)
    ridge_temp.fit(X_train_scaled, y_train)
    coefs.append(ridge_temp.coef_)

# 계수 변화 시각화
coefs_df = pd.DataFrame(coefs, columns=X.columns, index=[f'α={a}' for a in alphas])

plt.figure(figsize=(12, 6))
for col in coefs_df.columns:
    plt.plot(range(len(alphas)), coefs_df[col], marker='o', label=col, linewidth=2)

plt.xlabel('Regularization Strength')
plt.ylabel('Coefficient Value')
plt.title('Ridge: Coefficient Path')
plt.xticks(range(len(alphas)), [f'α={a}' for a in alphas])
plt.legend()
plt.grid(True, alpha=0.3)
plt.axhline(y=0, color='black', linestyle='--', linewidth=1)
plt.tight_layout()
plt.show()

22.6 Lasso 회귀 (L1 정규화)

Lasso 회귀는 계수의 절댓값 합(L1 norm)에 패널티를 부여하며, 일부 계수를 정확히 0으로 만든다.

Lasso 손실 함수

\[ \text{Loss}_{\text{Lasso}} = \text{MSE} + \alpha \sum_{i=1}^{p} |\beta_i| \]

Lasso의 특징

특징 설명
계수 0화 일부 계수를 정확히 0으로 만듦
자동 변수 선택 중요 변수만 남김
희소 모델 적은 변수로 설명
해석 용이 선택된 변수만 해석
적용 상황 변수 선택이 필요할 때

예제: Lasso 회귀

from sklearn.linear_model import Lasso

# Lasso 회귀
lasso = Lasso(alpha=0.1)
lasso.fit(X_train_scaled, y_train)

# 계수 비교
print("=== Lasso vs Ridge vs 선형 회귀 계수 비교 ===")
comparison_df = pd.DataFrame({
    'Feature': X.columns,
    'Linear Regression': lr.coef_,
    'Ridge (α=1.0)': ridge.coef_,
    'Lasso (α=0.1)': lasso.coef_
})
print(comparison_df.round(2))

# 0이 아닌 계수 개수
n_nonzero = np.sum(lasso.coef_ != 0)
print(f"\nLasso가 선택한 변수 수: {n_nonzero}/{len(X.columns)}")

# 예측 및 평가
y_pred_lasso = lasso.predict(X_test_scaled)
print(f"\n=== Lasso 성능 ===")
print(f"테스트 R²: {r2_score(y_test, y_pred_lasso):.4f}")
print(f"테스트 RMSE: {np.sqrt(mean_squared_error(y_test, y_pred_lasso)):.2f}g")
=== Lasso vs Ridge vs 선형 회귀 계수 비교 ===
             Feature  Linear Regression  Ridge (α=1.0)  Lasso (α=0.1)
0     bill_length_mm               3.86          24.89          20.83
1      bill_depth_mm              10.06          16.44          19.63
2  flipper_length_mm              50.25         690.57         697.66

Lasso가 선택한 변수 수: 3/3

=== Lasso 성능 ===
테스트 R²: 0.7980
테스트 RMSE: 356.68g

예제: Lasso 변수 선택 시각화

# 다양한 alpha 값에서 변수 선택
alphas_lasso = [0.01, 0.1, 1.0, 10.0, 50.0]
coefs_lasso = []
n_features = []

for alpha in alphas_lasso:
    lasso_temp = Lasso(alpha=alpha, max_iter=10000)
    lasso_temp.fit(X_train_scaled, y_train)
    coefs_lasso.append(lasso_temp.coef_)
    n_features.append(np.sum(lasso_temp.coef_ != 0))

# 계수 경로 시각화
plt.figure(figsize=(12, 6))
coefs_lasso_df = pd.DataFrame(coefs_lasso, columns=X.columns)

for col in coefs_lasso_df.columns:
    plt.plot(range(len(alphas_lasso)), coefs_lasso_df[col], marker='o', label=col, linewidth=2)

plt.xlabel('Regularization Strength')
plt.ylabel('Coefficient Value')
plt.title('Lasso: Coefficient Path (Automatic Feature Selection)')
plt.xticks(range(len(alphas_lasso)), [f'α={a}\n({n}개 변수)' for a, n in zip(alphas_lasso, n_features)])
plt.legend()
plt.grid(True, alpha=0.3)
plt.axhline(y=0, color='black', linestyle='--', linewidth=1)
plt.tight_layout()
plt.show()

22.7 ElasticNet (L1 + L2)

ElasticNet은 Ridge와 Lasso의 패널티를 결합한 방법이다.

ElasticNet 손실 함수

\[ \text{Loss}_{\text{ElasticNet}} = \text{MSE} + \alpha \rho \sum_{i=1}^{p} |\beta_i| + \alpha \frac{1-\rho}{2} \sum_{i=1}^{p} \beta_i^2 \]

여기서: - α (alpha): 전체 규제 강도 - ρ (l1_ratio): L1 비율 (0~1)

ElasticNet 하이퍼파라미터

파라미터 범위 효과
alpha 0 ~ ∞ 전체 규제 강도
l1_ratio = 0 - Ridge와 동일
l1_ratio = 1 - Lasso와 동일
l1_ratio = 0.5 - 균형 잡힌 규제

ElasticNet의 특징

  • Ridge와 Lasso의 장점 결합
  • 상관 높은 변수들을 함께 선택/제거
  • 고차원 데이터에 적합
  • 변수 수 > 샘플 수인 경우 유용

예제: ElasticNet

from sklearn.linear_model import ElasticNet

# ElasticNet
elastic = ElasticNet(alpha=0.1, l1_ratio=0.5)
elastic.fit(X_train_scaled, y_train)

# 계수 비교
print("=== 모든 모델 계수 비교 ===")
comparison_final = pd.DataFrame({
    'Feature': X.columns,
    'Linear': lr.coef_,
    'Ridge': ridge.coef_,
    'Lasso': lasso.coef_,
    'ElasticNet': elastic.coef_
})
print(comparison_final.round(2))

# 예측 및 평가
y_pred_elastic = elastic.predict(X_test_scaled)
print(f"\n=== ElasticNet 성능 ===")
print(f"테스트 R²: {r2_score(y_test, y_pred_elastic):.4f}")
print(f"테스트 RMSE: {np.sqrt(mean_squared_error(y_test, y_pred_elastic)):.2f}g")
=== 모든 모델 계수 비교 ===
             Feature  Linear   Ridge   Lasso  ElasticNet
0     bill_length_mm    3.86   24.89   20.83       64.22
1      bill_depth_mm   10.06   16.44   19.63      -17.30
2  flipper_length_mm   50.25  690.57  697.66      616.16

=== ElasticNet 성능 ===
테스트 R²: 0.7891
테스트 RMSE: 364.47g

22.8 회귀 모델 종합 비교

모델 특성 비교

모델 패널티 변수 선택 계수 크기 다중공선성 적용 상황
Linear Regression 없음 제한 없음 취약 기준 모델
Ridge (L2) 제곱합 축소 완화 상관 높은 변수
Lasso (L1) 절댓값 0 포함 보통 변수 선택 필요
ElasticNet (L1+L2) 혼합 0 포함 완화 고차원, 복합 상황

예제: 모든 모델 성능 비교

# 모든 모델 평가
models = {
    'Linear Regression': lr,
    'Ridge': ridge,
    'Lasso': lasso,
    'ElasticNet': elastic
}

results = []
for name, model in models.items():
    if name == 'Linear Regression':
        pred = model.predict(X_test)
    else:
        pred = model.predict(X_test_scaled)
    
    results.append({
        'Model': name,
        'R²': r2_score(y_test, pred),
        'RMSE': np.sqrt(mean_squared_error(y_test, pred)),
        'MAE': mean_absolute_error(y_test, pred),
        'Non-zero Coef': np.sum(model.coef_ != 0)
    })

results_df = pd.DataFrame(results)
print("=== 모델 성능 비교 ===")
print(results_df.round(4))

# 시각화
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# R² 비교
axes[0].bar(results_df['Model'], results_df['R²'])
axes[0].set_ylabel('R² Score')
axes[0].set_title('Model Comparison: R²')
axes[0].set_xticklabels(results_df['Model'], rotation=45, ha='right')

# RMSE 비교
axes[1].bar(results_df['Model'], results_df['RMSE'])
axes[1].set_ylabel('RMSE')
axes[1].set_title('Model Comparison: RMSE')
axes[1].set_xticklabels(results_df['Model'], rotation=45, ha='right')

plt.tight_layout()
plt.show()
=== 모델 성능 비교 ===
               Model      R²      RMSE       MAE  Non-zero Coef
0  Linear Regression  0.7981  356.6518  289.6890              3
1              Ridge  0.7977  356.9827  290.2179              3
2              Lasso  0.7980  356.6800  289.7059              3
3         ElasticNet  0.7891  364.4700  297.0190              3

22.9 모델 선택 가이드

상황별 모델 선택

상황 권장 모델 이유
변수 수 적고 해석 중요 Linear Regression 단순하고 명확
변수 간 상관 높음 Ridge 다중공선성 완화
변수 선택 필요 Lasso 자동 변수 선택
고차원 데이터 (p > n) ElasticNet 안정적 변수 선택
예측만 중요 교차 검증으로 선택 성능 기준

의사결정 흐름

변수 선택이 필요한가?
├─ Yes → 변수가 매우 많은가?
│         ├─ Yes → ElasticNet
│         └─ No → Lasso
└─ No → 다중공선성이 문제인가?
          ├─ Yes → Ridge
          └─ No → Linear Regression

22.10 하이퍼파라미터 튜닝

예제: GridSearchCV로 최적 Alpha 찾기

from sklearn.model_selection import GridSearchCV
from sklearn.pipeline import Pipeline

# 파이프라인 구성 (스케일링 + 모델)
pipeline = Pipeline([
    ('scaler', StandardScaler()),
    ('model', Ridge())
])

# 하이퍼파라미터 그리드
param_grid = {
    'model__alpha': [0.001, 0.01, 0.1, 1.0, 10.0, 100.0]
}

# GridSearchCV
grid_search = GridSearchCV(
    pipeline, param_grid, cv=5, scoring='r2', n_jobs=-1
)
grid_search.fit(X_train, y_train)

print("=== GridSearchCV 결과 ===")
print(f"최적 alpha: {grid_search.best_params_['model__alpha']}")
print(f"최적 R² (CV): {grid_search.best_score_:.4f}")

# 테스트 성능
best_model = grid_search.best_estimator_
y_pred_best = best_model.predict(X_test)
print(f"테스트 R²: {r2_score(y_test, y_pred_best):.4f}")

# Alpha에 따른 성능 변화
results_cv = pd.DataFrame(grid_search.cv_results_)
plt.figure(figsize=(10, 6))
plt.semilogx(results_cv['param_model__alpha'], results_cv['mean_test_score'], 
             marker='o', linewidth=2)
plt.xlabel('Alpha (log scale)')
plt.ylabel('Mean R² (5-fold CV)')
plt.title('Ridge: Alpha Tuning')
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
=== GridSearchCV 결과 ===
최적 alpha: 1.0
최적 R² (CV): 0.7450
테스트 R²: 0.7977

22.11 전처리 주의사항

정규화 회귀 사용 시 필수 사항

  • 반드시 표준화: Ridge, Lasso, ElasticNet은 스케일에 민감
  • 파이프라인 사용: 전처리와 모델을 하나로 묶기
  • 테스트셋 분리: 전처리는 학습셋 기준으로
  • 교차 검증: 하이퍼파라미터 튜닝 시 필수

예제: 올바른 파이프라인

# 최종 파이프라인
final_pipeline = Pipeline([
    ('scaler', StandardScaler()),
    ('model', Ridge(alpha=1.0))
])

# 학습
final_pipeline.fit(X_train, y_train)

# 평가
print("=== 파이프라인 최종 성능 ===")
print(f"학습 R²: {final_pipeline.score(X_train, y_train):.4f}")
print(f"테스트 R²: {final_pipeline.score(X_test, y_test):.4f}")
=== 파이프라인 최종 성능 ===
학습 R²: 0.7551
테스트 R²: 0.7977

22.12 요약

이 장에서는 연속형 예측을 위한 회귀 모델을 학습했다. 주요 내용은 다음과 같다.

회귀 모델 핵심

  • 선형 회귀: 기준 모델, 규제 없음
  • Ridge: L2 정규화, 다중공선성 완화, 모든 변수 유지
  • Lasso: L1 정규화, 자동 변수 선택, 희소 모델
  • ElasticNet: L1+L2, 균형 잡힌 접근, 고차원 데이터

실무 체크리스트

주의사항

  • 정규화 회귀는 반드시 표준화 후 사용
  • 계수 해석 시 원본 스케일 고려
  • 과적합 방지가 최우선 목표
  • 모델 선택보다 전처리가 더 중요할 수 있음

회귀 모델은 연속형 예측의 기본이며, 정규화는 과적합 방지의 핵심이다. 데이터 특성과 목적에 맞는 모델을 선택하고, 적절한 전처리와 하이퍼파라미터 튜닝을 통해 최적의 성능을 달성하는 것이 중요하다.