🖇 Gradient Descent
🖇 선형 회귀
🖇 릿지 회귀
🖇 라쏘 회귀
🖇 엘라스틱넷 회귀
🖇 랜덤포레스트 & XGBoost
🖇 하이퍼파라미터 튜닝
🖇 평가(회귀)
회귀(Regression)는 수치 예측 문제를 해결하는 대표적인 지도학습 기법이다.
이 글에서는 당뇨병 데이터셋(Diabetes Dataset)을 활용해 다양한 회귀 알고리즘을 실습하고 비교해 보았다. 선형 회귀(Linear Regression)를 시작으로, 릿지(Ridge), 라쏘(Lasso), 엘라스틱넷(ElasticNet) 등 정규화 기법이 적용된 회귀 모델들을 직접 적용해 보았다.
이어서 랜덤포레스트와 XGBoost 같은 트리 기반 회귀 모델도 함께 비교하고, GridSearchCV 및 RandomizedSearchCV를 활용한 하이퍼파라미터 튜닝 및 회귀 모델 성능 평가 지표를 알아보았다.
자세한 실습 과정 및 출력 결과는 GitHub repository에서 확인할 수 있다.
모델 비교에 앞서 회귀 모델이 학습하는 핵심 원리인 비용 함수(Cost Function)와 이를 최적화하는 경사하강법(Gradient Descent) 개념을 정리하고 넘어가겠다.
회귀 모델을 학습할 때 가장 중요한 것은 모델이 얼마나 정확한 예측을 하고 있는지 평가하는 기준을 정하는 것이다. 이때 비용 함수(Cost Function)라는 지표를 사용한다. 비용 함수는 다음과 같은 여러 용어로도 불린다.
비용 함수(Cost Function) = 손실 함수(Loss Function) = 목적 함수(Objective Function)
모두 같은 개념이며 예측 값과 실제 값 사이의 오차(error)를 수치화해서 모델의 성능을 평가하는 데 사용된다.
회귀 문제에서는 일반적으로 예측 오차의 제곱 평균(MSE, Mean Squared Error)을 비용 함수로 사용한다. 모델은 이 값을 최소화하는 방향으로 학습되며, 이를 통해 가장 적절한 회귀 계수(파라미터)를 찾게 된다.
수식으로 표현하면 다음과 같다.
모델이 예측값을 점점 더 실제값에 가깝게 맞추기 위해서는 비용 함수의 값을 줄여야 한다. 그렇다면 어떻게 하면 비용 함수를 가장 작게 만들 수 있을까?
여기서 경사하강법(Gradient Descent) 이라는 개념이 등장한다.
경사하강법은 말 그대로, 기울기를 따라 내려가는 방식으로 비용 함수를 최소화하는 최적의 파라미터를 찾는 알고리즘이다. 수학적으로는 비용 함수의 미분값(기울기)을 활용하여 함수가 가장 낮은 지점을 향해 조금씩 파라미터를 이동시키는 방식이다.
이 과정을 반복하면서 모델의 파라미터(가중치)들이 업데이트되고, 점점 더 오차가 줄어드는 방향으로 학습이 진행된다.
수식으로 표현하면 다음과 같다.
: 모델의 파라미터 (가중치)
: 학습률 (learning rate), 한 번에 얼마나 이동할지를 결정
: 비용 함수 에 대한 의 미분값 (기울기) = ∇θ
이처럼 경사하강법은 머신러닝 모델이 비용 함수를 최소화하는 파라미터를 자동으로 찾아가는 핵심 알고리즘이며 대부분의 회귀 모델에서 학습의 기본 원리로 사용된다.
선형 회귀는 가장 기본적인 회귀 알고리즘으로, 독립 변수와 종속 변수 사이에 선형적인 관계가 있다고 가정한다. 이 모델은 주어진 입력 피처들을 기반으로 출력 값을 예측하기 위해 각 피처에 가중치(회귀 계수)를 곱한 값의 합으로 출력 값을 계산한다.
이때 모델이 학습하는 것은 각 피처의 가중치 θ 값을 얼마로 설정해야 예측 값과 실제 값 간의 차이(오차)가 가장 작아지는지이다. 오차는 보통 평균 제곱 오차(MSE, Mean Squared Error) 를 기준으로 측정되며, 이를 최소화하는 방향으로 모델이 학습된다.
모델의 예측 식은 다음과 같다.
선형 회귀는 간단하고 해석이 쉬우며 연속적인 숫자형 변수 예측 문제에서 기본적인 시작점이 되는 모델이다. 하지만 피처 수가 많아지거나 피처 간 다중공선성이 존재하는 경우에는 성능이 급격히 저하될 수 있으며 과적합의 가능성도 있다.
# 라이브러리 불러오기
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 정규화는 회귀 계수들이 너무 커지는 것을 방지하기 위해 각 회귀 계수의 제곱 값을 비용 함수에 추가로 포함시킨다.
예측 성능을 높이는 동시에 모델의 복잡도도 함께 고려하는 방식이다. 이렇게 함으로써 모델이 특정 피처에만 과도하게 의존하지 않게 되고, 전체적으로 균형 잡힌 계수 분포를 가지게 된다.
릿지 회귀의 목적 함수는 다음과 같다.
여기서 α는 정규화 강도를 조절하는 하이퍼파라미터이다. α가 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으로 만들어 모델에서 제외되는 것이다.
라쏘 회귀의 목적 함수는 다음과 같다.
라쏘 회귀는 피처 수가 매우 많고 그 중 일부만 예측에 중요한 영향을 줄 것이라고 예상되는 경우, 예를 들어 텍스트 벡터화처럼 수백~수천 개의 피처가 존재할 때 매우 유용하다. 그러나 다중공선성이 심한 상황에서는 계수 선택이 불안정해질 수 있다는 점은 유의해야 한다.
# 라쏘 회귀 (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보다 더 좋은 결과를 얻는 경우가 많다.
엘라스틱넷의 목적 함수는 다음과 같다.
엘라스틱넷은 특히 피처 수가 많고 상관관계가 높은 피처들이 존재하는 경우에 안정적으로 작동한다. 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
# 랜덤포레스트
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
RandomizedSearchCV
# 라이브러리 불러오기
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
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
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}')
| 평가 지표 | - | 설명 | 추가 설명 |
|---|---|---|---|
| MAE | Mean Absolute Error 평균 절대 오차 | 실제 값과 예측 값의 차이 → 절댓값 평균 | 단위 그대로 해석 가능, 이상치에 강함 |
| MSE | Mean Squared Error 평균 제곱 오차 | 실제 값과 예측 값의 차이 → 제곱해 평균 | 오차가 클수록 더 큰 패널티, 이상치에 민감 |
| RMSE | Root Mean Squared Error MSE에 루트를 씌운 값 | MSE가 실제 오차보다 커지는 특성 있어 루트로 보정 | |
| RMSLE | Root Mean Squared Log Error RMSE에 로그 적용 | 예측 값이 실제 값보다 작을 때 더 큰 패널티 | e.g., 배달 20분을 예측했는데 40분 걸리면 문제가 됨 |
| R² | R Squared Score 결정계수 | 실제 값의 분산 대비 예측 값의 분산 계산 | 1에 가까울수록 설명력이 높음(성능이 좋음) |
MAE:
MSE:
RMSE:
RMSLE:
R²:
# 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는 비선형 관계까지 포착해 성능 면에서는 뛰어난 결과를 보이지만, 해석력이 떨어지고 학습 시간이 길다는 단점도 있다.
예측 정확도와 해석 가능성 사이에서 문제 상황에 맞는 적절한 알고리즘을 선택하기 위해서 많은 연습이 필요하겠다.