[ML] Regression with Supervised Learning

jul ee·2025년 5월 14일

데이터 성장기

목록 보기
93/139

🖇  Gradient Descent
🖇  선형 회귀
🖇  릿지 회귀
🖇  라쏘 회귀
🖇  엘라스틱넷 회귀
🖇  랜덤포레스트 & XGBoost
🖇  하이퍼파라미터 튜닝
🖇  평가(회귀)


회귀(Regression)는 수치 예측 문제를 해결하는 대표적인 지도학습 기법이다.

이 글에서는 당뇨병 데이터셋(Diabetes Dataset)을 활용해 다양한 회귀 알고리즘을 실습하고 비교해 보았다. 선형 회귀(Linear Regression)를 시작으로, 릿지(Ridge), 라쏘(Lasso), 엘라스틱넷(ElasticNet) 등 정규화 기법이 적용된 회귀 모델들을 직접 적용해 보았다.

이어서 랜덤포레스트와 XGBoost 같은 트리 기반 회귀 모델도 함께 비교하고, GridSearchCV 및 RandomizedSearchCV를 활용한 하이퍼파라미터 튜닝 및 회귀 모델 성능 평가 지표를 알아보았다.

자세한 실습 과정 및 출력 결과는 GitHub repository에서 확인할 수 있다.



모델 비교에 앞서 회귀 모델이 학습하는 핵심 원리인 비용 함수(Cost Function)와 이를 최적화하는 경사하강법(Gradient Descent) 개념을 정리하고 넘어가겠다.

🖇  Gradient Descent

회귀 모델을 학습할 때 가장 중요한 것은 모델이 얼마나 정확한 예측을 하고 있는지 평가하는 기준을 정하는 것이다. 이때 비용 함수(Cost Function)라는 지표를 사용한다. 비용 함수는 다음과 같은 여러 용어로도 불린다.

비용 함수(Cost Function) = 손실 함수(Loss Function) = 목적 함수(Objective Function)

모두 같은 개념이며 예측 값과 실제 값 사이의 오차(error)를 수치화해서 모델의 성능을 평가하는 데 사용된다.

회귀 문제에서는 일반적으로 예측 오차의 제곱 평균(MSE, Mean Squared Error)을 비용 함수로 사용한다. 모델은 이 값을 최소화하는 방향으로 학습되며, 이를 통해 가장 적절한 회귀 계수(파라미터)를 찾게 된다.

수식으로 표현하면 다음과 같다.

J(θ)=1ni=1n(yiy^i)2J(\theta) = \frac{1}{n} \sum_{i=1}^{n} (y_i - \hat{y}_i)^2
  • yiy_i:  실제 값
  • y^i\hat{y}_i:  예측 값
  • nn:  데이터 샘플 수
  • θ\theta:  회귀 계수 (모델의 파라미터)

모델이 예측값을 점점 더 실제값에 가깝게 맞추기 위해서는 비용 함수의 값을 줄여야 한다. 그렇다면 어떻게 하면 비용 함수를 가장 작게 만들 수 있을까?

여기서 경사하강법(Gradient Descent) 이라는 개념이 등장한다.

경사하강법은 말 그대로, 기울기를 따라 내려가는 방식으로 비용 함수를 최소화하는 최적의 파라미터를 찾는 알고리즘이다. 수학적으로는 비용 함수의 미분값(기울기)을 활용하여 함수가 가장 낮은 지점을 향해 조금씩 파라미터를 이동시키는 방식이다.

  • 지금 내가 서 있는 위치가 어느 방향으로 기울어져 있는지를 보고
  • 가장 가파르게 내려가는 방향으로 조금씩 이동하다 보면
  • 언젠가 바닥(최솟값)에 도달하게 된다.

이 과정을 반복하면서 모델의 파라미터(가중치)들이 업데이트되고, 점점 더 오차가 줄어드는 방향으로 학습이 진행된다.

수식으로 표현하면 다음과 같다.

θ:=θαθJ(θ)\theta := \theta - \alpha \cdot \nabla_{\theta} J(\theta)
oror
θ:=θαJ(θ)θ\theta := \theta - \alpha \cdot \frac{\partial J(\theta)}{\partial \theta}
  • θ\theta:  모델의 파라미터 (가중치)

  • α\alpha:  학습률 (learning rate), 한 번에 얼마나 이동할지를 결정

  • J(θ)θ\frac{\partial J(\theta)}{\partial \theta}:  비용 함수 J(θ)J(\theta)에 대한 θ\theta의 미분값 (기울기) = ∇θ


여기서 학습률(learning rate, α)이 중요하다. 너무 크면 바닥을 지나쳐버려서 발산할 수 있고, 너무 작으면 바닥까지 도달하는 데 시간이 너무 오래 걸린다.

이처럼 경사하강법은 머신러닝 모델이 비용 함수를 최소화하는 파라미터를 자동으로 찾아가는 핵심 알고리즘이며 대부분의 회귀 모델에서 학습의 기본 원리로 사용된다.


이제 이 원리를 바탕으로 선형 회귀, 릿지, 라쏘, 엘라스틱넷 등의 회귀 모델이 어떤 식으로 학습하고 어떻게 오차를 줄여가는지 알아보자.


🖇  선형 회귀

선형 회귀는 가장 기본적인 회귀 알고리즘으로, 독립 변수와 종속 변수 사이에 선형적인 관계가 있다고 가정한다. 이 모델은 주어진 입력 피처들을 기반으로 출력 값을 예측하기 위해 각 피처에 가중치(회귀 계수)를 곱한 값의 합으로 출력 값을 계산한다.

이때 모델이 학습하는 것은 각 피처의 가중치 θ 값을 얼마로 설정해야 예측 값과 실제 값 간의 차이(오차)가 가장 작아지는지이다. 오차는 보통 평균 제곱 오차(MSE, Mean Squared Error) 를 기준으로 측정되며, 이를 최소화하는 방향으로 모델이 학습된다.

모델의 예측 식은 다음과 같다.

y^=θ0+θ1x1+θ2x2++θnxn\hat{y} = \theta_0 + \theta_1 x_1 + \theta_2 x_2 + \cdots + \theta_n x_n

선형 회귀는 간단하고 해석이 쉬우며 연속적인 숫자형 변수 예측 문제에서 기본적인 시작점이 되는 모델이다. 하지만 피처 수가 많아지거나 피처 간 다중공선성이 존재하는 경우에는 성능이 급격히 저하될 수 있으며 과적합의 가능성도 있다.

  • 단순 선형 회귀: 독립변수(피처)가 1개
  • 다중 선형 회귀: 독립변수(피처)가 2개 이상
# 라이브러리 불러오기
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error

# 데이터 생성
from sklearn.datasets import load_diabetes  # 당뇨병 환자 데이터

def make_dataset():
    dataset = load_diabetes()
    df = pd.DataFrame(dataset.data, columns=dataset.feature_names)
    df['target'] = dataset.target
    X_train, X_test, y_train, y_test = train_test_split(
        df.drop('target', axis=1), df['target'], test_size=0.2, random_state=1004)
    return X_train, X_test, y_train, y_test

X_train, X_test, y_train, y_test = make_dataset()
X_train.shape, X_test.shape, y_train.shape, y_test.shape
# 선형 회귀
from sklearn.linear_model import LinearRegression

lin_reg = LinearRegression()
lin_reg.fit(X_train, y_train)
lin_reg_pred = lin_reg.predict(X_test)

mse = mean_squared_error(y_test, lin_reg_pred)
print(f'MSE: {mse}')



🖇  릿지 회귀

릿지 회귀는 선형 회귀에서 발생할 수 있는 과적합 문제를 완화하기 위해 L2 정규화(regularization) 항을 추가한 모델이다. L2 정규화는 회귀 계수들이 너무 커지는 것을 방지하기 위해 각 회귀 계수의 제곱 값을 비용 함수에 추가로 포함시킨다.

예측 성능을 높이는 동시에 모델의 복잡도도 함께 고려하는 방식이다. 이렇게 함으로써 모델이 특정 피처에만 과도하게 의존하지 않게 되고, 전체적으로 균형 잡힌 계수 분포를 가지게 된다.

릿지 회귀의 목적 함수는 다음과 같다.

J(θ)=i=1n(yiy^i)2+αj=1nθj2J(\theta) = \sum_{i=1}^{n} (y_i - \hat{y}_i)^2 + \alpha \sum_{j=1}^{n} \theta_j^2

여기서 α는 정규화 강도를 조절하는 하이퍼파라미터이다. α가 0이면 일반 선형 회귀와 동일하고, 값이 클수록 정규화 효과가 강해져 계수들이 더 작아진다.

릿지 회귀는 모든 피처를 사용하면서도 계수의 크기를 조절함으로써 모델의 안정성과 일반화 성능을 높이고자 할 때 효과적이다. 특히 피처 간 다중공선성이 있는 경우에 성능 향상에 유리하다.

# 릿지 회귀 (alpha=1: default)
from sklearn.linear_model import Ridge

ridge = Ridge()
ridge.fit(X_train, y_train)
ridge_pred = ridge.predict(X_test)

mse = mean_squared_error(y_test, ridge_pred)
print(f'MSE: {mse}')


# 회귀 계수 (alpha=1: default)
ridge_coef = pd.DataFrame(data=ridge.coef_, index=X_train.columns, columns=['alpha1'])
ridge_coef


# 릿지 회귀 (alpha=0.05)
ridge = Ridge(alpha=0.05)
ridge.fit(X_train, y_train)
ridge_pred = ridge.predict(X_test)

mse = mean_squared_error(y_test, ridge_pred)
print(f'MSE: {mse}')


# 회귀 계수 (alpha=0.05)
ridge_coef['alpha0.05'] = ridge.coef_
ridge_coef



🖇  라쏘 회귀

라쏘 회귀는 릿지 회귀처럼 정규화를 적용하지만, L1 정규화를 사용한다는 점에서 다르다. 이 L1 정규화는 계수의 제곱이 아니라 절댓값의 합을 비용 함수에 포함시킨다.

라쏘 회귀의 핵심은 정규화 항이 회귀 계수를 0으로 수렴시킬 수 있다는 점이다. 이 특징 덕분에 라쏘 회귀는 단순히 과적합을 막는 것 뿐만 아니라 자동으로 피처 선택(feature selection)을 수행하는 효과도 있다. 중요하지 않은 피처는 계수를 0으로 만들어 모델에서 제외되는 것이다.

라쏘 회귀의 목적 함수는 다음과 같다.

J(θ)=i=1n(yiy^i)2+αj=1nθjJ(\theta) = \sum_{i=1}^{n} (y_i - \hat{y}_i)^2 + \alpha \sum_{j=1}^{n} |\theta_j|

라쏘 회귀는 피처 수가 매우 많고 그 중 일부만 예측에 중요한 영향을 줄 것이라고 예상되는 경우, 예를 들어 텍스트 벡터화처럼 수백~수천 개의 피처가 존재할 때 매우 유용하다. 그러나 다중공선성이 심한 상황에서는 계수 선택이 불안정해질 수 있다는 점은 유의해야 한다.

# 라쏘 회귀 (alpha=1: default)
from sklearn.linear_model import Lasso

lasso = Lasso()
lasso.fit(X_train, y_train)
lasso_pred = lasso.predict(X_test)

mse = mean_squared_error(y_test, lasso_pred)
print(f'MSE: {mse}')

# 회귀 계수 (alpha=1: default)
lasso_coef = pd.DataFrame(data=lasso.coef_, index=X_train.columns, columns=['alpha1'])
lasso_coef



🖇  엘라스틱넷 회귀

엘라스틱넷 회귀는 릿지 회귀와 라쏘 회귀의 단점을 보완하고 장점을 결합한 방식이다. L1 정규화와 L2 정규화를 혼합하여 동시에 적용하는 모델이다.

L1 규제를 통해 중요하지 않은 피처를 제거하고(Lasso), L2 규제를 통해 전체적인 계수 값을 조절해 모델을 안정화한다(Ridge). 이 두 가지 효과를 균형 있게 섞어 사용하면 단독 L1이나 L2보다 더 좋은 결과를 얻는 경우가 많다.

엘라스틱넷의 목적 함수는 다음과 같다.

J(θ)=i=1n(yiy^i)2+α(rj=1nθj+1r2j=1nθj2)J(\theta) = \sum_{i=1}^{n} (y_i - \hat{y}_i)^2 + \alpha \left( r \sum_{j=1}^{n} |\theta_j| + \frac{1 - r}{2} \sum_{j=1}^{n} \theta_j^2 \right)
  • α:  전체 정규화의 강도
  • r:  L1 비중 조절 (0이면 릿지, 1이면 라쏘)

엘라스틱넷은 특히 피처 수가 많고 상관관계가 높은 피처들이 존재하는 경우에 안정적으로 작동한다. Lasso는 상관된 피처 중 하나만 선택하는 반면, ElasticNet은 그 중 여러 개를 함께 선택할 수 있기 때문에 더 유연하게 작동한다.

# 엘라스틱넷 회귀
from sklearn.linear_model import ElasticNet

elastic = ElasticNet()
elastic.fit(X_train, y_train)
elastic_pred = elastic.predict(X_test)

mse = mean_squared_error(y_test, elastic_pred)
print(f'MSE: {mse}')


# 엘라스틱넷 회귀
from sklearn.linear_model import ElasticNet

elastic = ElasticNet(alpha=0.0001, l1_ratio=0.6)
elastic.fit(X_train, y_train)
elastic_pred = elastic.predict(X_test)

mse = mean_squared_error(y_test, elastic_pred)
print(f'MSE: {mse}')



🖇  랜덤포레스트 & XGBoost

랜덤포레스트

  • 여러 개의 의사결정 트리로 구성
  • 앙상블 방법 중 배깅(bagging) 방식
  • 부트스트랩 샘플링 (데이터셋 중복 허용)
  • 최종 다수결 투표

XGBoost

  • eXtreme Gradient Boosting
  • 트리 앙상블 중 성능이 좋은 알고리즘
  • 약한 학습기가 계속해서 업데이트를 하며 좋은 모델을 만들어 간다.
# 랜덤포레스트
from sklearn.ensemble import RandomForestRegressor

rf_reg = RandomForestRegressor()
rf_reg.fit(X_train, y_train)
rf_reg_pred = rf_reg.predict(X_test)

mse = mean_squared_error(y_test, rf_reg_pred)
print(f'MSE: {mse}')


# XGBoost
from xgboost import XGBRegressor

xgb_reg = XGBRegressor()
xgb_reg.fit(X_train, y_train)
xgb_reg_pred = xgb_reg.predict(X_test)

mse = mean_squared_error(y_test, xgb_reg_pred)
print(f'MSE: {mse}')



🖇  하이퍼파라미터 튜닝

GridSearchCV

  • grid search를 통해 최적의 하이퍼파라미터를 찾음
  • 모든 경우의 수를 탐색하기 때문에 시간 오래 걸림

RandomizedSearchCV

  • GridSearchCV와 달리 랜덤으로 N개의 조합만 탐색함
  • 시간 내 최적의 하이퍼파라미터 탐색
# 라이브러리 불러오기
from sklearn.model_selection import GridSearchCV, RandomizedSearchCV

# 하이퍼파라미터
params={'learning_rate': [0.07, 0.05],
        'max_depth': [3, 5, 7],
        'n_estimators': [100, 200],
        'subsample': [0.9, 0.8, 0.7]
        }

# 데이터셋 로드
def make_dataset2():
    dataset = load_diabetes()
    df = pd.DataFrame(dataset.data, columns=dataset.feature_names)
    df['target'] = dataset.target
    return df.drop('target', axis=1), df['target']

X, y = make_dataset2()

GridSearch

# GridSearch
xgb_reg = XGBRegressor()
grid = GridSearchCV(xgb_reg, params, cv=3, n_jobs=-1)
grid.fit(X, y)

# 최적의 하이퍼파라미터
grid.best_params_

# 하이퍼파라미터 튜닝
xgb_reg = XGBRegressor(
    learning_rate=0.05,
    max_depth=3,
    n_estimators=100,
    subsample=0.7
)

xgb_reg.fit(X_train, y_train)
xgb_reg_pred = xgb_reg.predict(X_test)

mse = mean_squared_error(y_test, xgb_reg_pred)
print(f'MSE: {mse}')

RandomSearch

# RandomSearch
xgb_reg = XGBRegressor()
grid = RandomizedSearchCV(xgb_reg, params, cv=3, n_iter=10, n_jobs=-1)
grid.fit(X, y)

# 최적의 하이퍼파라미터
grid.best_params_

# 하이퍼파라미터 튜닝
xgb_reg = XGBRegressor(
    learning_rate=0.07,
    max_depth=3,
    n_estimators=100,
    subsample=0.7
)

xgb_reg.fit(X_train, y_train)
xgb_reg_pred = xgb_reg.predict(X_test)

mse = mean_squared_error(y_test, xgb_reg_pred)
print(f'MSE: {mse}')



🖇  평가(회귀)

평가 지표-설명추가 설명
MAEMean Absolute Error
평균 절대 오차
실제 값과 예측 값의 차이 → 절댓값 평균단위 그대로 해석 가능, 이상치에 강함
MSEMean Squared Error
평균 제곱 오차
실제 값과 예측 값의 차이 → 제곱해 평균오차가 클수록 더 큰 패널티, 이상치에 민감
RMSERoot Mean Squared Error
MSE에 루트를 씌운 값
MSE가 실제 오차보다 커지는 특성 있어 루트로 보정
RMSLERoot Mean Squared Log Error
RMSE에 로그 적용
예측 값이 실제 값보다 작을 때 더 큰 패널티e.g., 배달 20분을 예측했는데 40분 걸리면 문제가 됨
R Squared Score
결정계수
실제 값의 분산 대비 예측 값의 분산 계산1에 가까울수록 설명력이 높음(성능이 좋음)

  • MAE: 1ni=1nyiy^i\frac{1}{n} \sum_{i=1}^{n} \left| y_i - \hat{y}_i \right|

  • MSE: 1ni=1n(yiy^i)2\frac{1}{n} \sum_{i=1}^{n} \left( y_i - \hat{y}_i \right)^2

  • RMSE: 1ni=1n(yiy^i)2\sqrt{ \frac{1}{n} \sum_{i=1}^{n} \left( y_i - \hat{y}_i \right)^2 }

  • RMSLE: 1ni=1n(log(y^i+1)log(yi+1))2\sqrt{ \frac{1}{n} \sum_{i=1}^{n} \left( \log(\hat{y}_i + 1) - \log(y_i + 1) \right)^2 }

  • R²: R2=1i=1n(yiy^i)2i=1n(yiyˉ)2R^2 = 1 - \frac{ \sum_{i=1}^{n} \left( y_i - \hat{y}_i \right)^2 }{ \sum_{i=1}^{n} \left( y_i - \bar{y} \right)^2 }

# MAE
from sklearn.metrics import mean_absolute_error

mae = mean_absolute_error(y_test, xgb_reg_pred)
print(f'MAE: {mae}')

# MSE
from sklearn.metrics import mean_squared_error

mse = mean_squared_error(y_test, xgb_reg_pred)
print(f'MSE: {mse}')

# RMSE
import numpy as np
np.sqrt(mean_squared_error(y_test, xgb_reg_pred))
# np.sqrt(mse)

# RMSLE
from sklearn.metrics import mean_squared_log_error

rmsle = np.sqrt(mean_squared_log_error(y_test, xgb_reg_pred))
print(f'RMSLE: {rmsle}')

# R2
from sklearn.metrics import r2_score

r2 = r2_score(y_test, xgb_reg_pred)
print(f'R2: {r2}')



인사이트 및 회고

앞서 살펴본 회귀 모델의 특징을 정리해 보았다.

회귀 모델정규화 방식특징대표 사용 예
선형 회귀없음기본 선형 모델단순 관계 예측
릿지 회귀L2모든 피처 유지, 계수 축소다중공선성 완화
라쏘 회귀L1중요 피처만 선택피처 선택 필요할 때
엘라스틱넷L1 + L2두 장점 혼합피처 많고 상관 높은 경우

단순한 선형 회귀는 빠르고 해석이 용이하지만 성능에는 한계가 있으며, 릿지와 라쏘는 과적합을 줄이면서 피처 선택 또는 안정화 측면에서 장점이 있는 것을 알 수 있었다. 특히 라쏘는 중요하지 않은 피처를 제거하는 특성 때문에 피처 선택이 중요한 문제에서 유용할 수 있다.

랜덤포레스트와 XGBoost는 비선형 관계까지 포착해 성능 면에서는 뛰어난 결과를 보이지만, 해석력이 떨어지고 학습 시간이 길다는 단점도 있다.

예측 정확도와 해석 가능성 사이에서 문제 상황에 맞는 적절한 알고리즘을 선택하기 위해서 많은 연습이 필요하겠다.

profile
AI에 관심을 가지고, 데이터로 가치를 만들어 나가는 과정을 기록합니다.

0개의 댓글