🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

하이퍼파라미터 튜닝 완벽 가이드 Grid Search와 Random Search - 슬라이드 1/9
A

AI Generated

2025. 12. 6. · 17 Views

하이퍼파라미터 튜닝 완벽 가이드 Grid Search와 Random Search

머신러닝 모델의 성능을 극대화하는 하이퍼파라미터 튜닝 기법을 알아봅니다. Grid Search와 Random Search의 원리부터 실전 활용법까지, 초급 개발자도 쉽게 이해할 수 있도록 설명합니다.


목차

  1. 하이퍼파라미터란_무엇인가
  2. Grid_Search_완전_탐색의_정석
  3. Random_Search_효율적인_탐색_전략
  4. 교차_검증의_중요성
  5. 탐색_범위_설정_전략
  6. 실전_파이프라인_구축
  7. 결과_분석과_시각화
  8. 실무_팁과_주의사항

1. 하이퍼파라미터란 무엇인가

김개발 씨는 처음으로 머신러닝 프로젝트를 맡게 되었습니다. 열심히 모델을 만들었는데, 정확도가 겨우 70%에 머물렀습니다.

선배인 박시니어 씨가 "하이퍼파라미터 튜닝 해봤어요?"라고 물었을 때, 김개발 씨는 고개를 갸웃거렸습니다.

하이퍼파라미터는 모델이 학습하기 전에 우리가 직접 설정해야 하는 값입니다. 마치 요리할 때 불의 세기나 조리 시간을 요리사가 직접 정하는 것과 같습니다.

이 값을 어떻게 설정하느냐에 따라 모델의 성능이 크게 달라집니다.

다음 코드를 살펴봅시다.

from sklearn.ensemble import RandomForestClassifier

# 하이퍼파라미터를 직접 설정하는 예제
model = RandomForestClassifier(
    n_estimators=100,      # 트리의 개수
    max_depth=10,          # 트리의 최대 깊이
    min_samples_split=2,   # 노드 분할에 필요한 최소 샘플 수
    min_samples_leaf=1,    # 리프 노드의 최소 샘플 수
    random_state=42        # 재현성을 위한 시드값
)

# 모델 학습
model.fit(X_train, y_train)
print(f"정확도: {model.score(X_test, y_test):.2f}")

김개발 씨는 입사 6개월 차 주니어 데이터 사이언티스트입니다. 첫 번째 머신러닝 프로젝트에서 RandomForest 모델을 사용해 고객 이탈 예측 모델을 만들었습니다.

그런데 아무리 데이터를 정제하고 피처를 추가해도 정확도가 70%를 넘지 못했습니다. 선배 개발자 박시니어 씨가 김개발 씨의 코드를 살펴보더니 물었습니다.

"하이퍼파라미터는 어떻게 설정했어요?" 김개발 씨는 당황했습니다. "그냥 기본값 그대로 썼는데요..." 박시니어 씨가 빙긋 웃으며 설명을 시작했습니다.

"머신러닝에서 파라미터하이퍼파라미터는 다른 개념이에요." 파라미터는 모델이 데이터를 학습하면서 스스로 찾아내는 값입니다. 예를 들어 선형 회귀에서 기울기와 절편이 여기에 해당합니다.

반면 하이퍼파라미터는 모델이 학습을 시작하기 전에 우리가 직접 설정해야 하는 값입니다. 쉽게 비유하자면, 파라미터는 학생이 시험 공부를 통해 얻는 지식이고, 하이퍼파라미터는 공부 환경을 세팅하는 것과 같습니다.

어떤 참고서를 쓸지, 하루에 몇 시간씩 공부할지, 어떤 순서로 공부할지는 학생이 공부하기 전에 미리 정해야 합니다. RandomForest를 예로 들면, n_estimators는 숲을 구성하는 나무의 개수입니다.

나무가 많을수록 예측이 안정적이지만 학습 시간이 오래 걸립니다. max_depth는 각 나무가 얼마나 깊게 뻗어나갈 수 있는지를 결정합니다.

너무 깊으면 과적합이 발생하고, 너무 얕으면 패턴을 제대로 학습하지 못합니다. 위의 코드를 살펴보면, RandomForestClassifier를 생성할 때 여러 하이퍼파라미터를 직접 지정하고 있습니다.

n_estimators=100은 100개의 트리를 만들겠다는 의미이고, max_depth=10은 각 트리의 깊이를 최대 10단계로 제한하겠다는 뜻입니다. 실제 현업에서는 이러한 하이퍼파라미터 값에 따라 모델 성능이 크게 달라집니다.

같은 데이터, 같은 알고리즘이라도 하이퍼파라미터 설정만 바꾸면 정확도가 10% 이상 차이 나기도 합니다. 문제는 어떤 하이퍼파라미터 조합이 최적인지 미리 알 수 없다는 것입니다.

데이터의 특성, 문제의 종류, 모델의 복잡도에 따라 최적값이 달라지기 때문입니다. 그래서 다양한 조합을 체계적으로 시험해보는 하이퍼파라미터 튜닝이 필요합니다.

박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다. "그렇군요, 그래서 튜닝이 필요한 거였군요!"

실전 팁

💡 - 하이퍼파라미터는 모델과 데이터에 따라 최적값이 다르므로 항상 실험이 필요합니다

  • 기본값으로 시작하되, 성능 향상이 필요하면 체계적인 튜닝을 시도하세요

2. Grid Search 완전 탐색의 정석

박시니어 씨가 김개발 씨에게 물었습니다. "가능한 모든 조합을 다 시험해보면 어떨까요?" 김개발 씨는 눈이 휘둥그레졌습니다.

"그게 가능한가요?" 박시니어 씨가 웃으며 대답했습니다. "물론이죠, 그게 바로 Grid Search예요."

Grid Search는 지정한 하이퍼파라미터의 모든 조합을 빠짐없이 탐색하는 방법입니다. 마치 바둑판의 모든 교차점을 하나씩 확인하는 것처럼, 모든 가능성을 체계적으로 검토합니다.

확실하게 최적의 조합을 찾을 수 있지만, 조합의 수가 많아지면 시간이 오래 걸립니다.

다음 코드를 살펴봅시다.

from sklearn.model_selection import GridSearchCV
from sklearn.ensemble import RandomForestClassifier

# 탐색할 하이퍼파라미터 그리드 정의
param_grid = {
    'n_estimators': [50, 100, 200],
    'max_depth': [5, 10, 15],
    'min_samples_split': [2, 5, 10]
}

# Grid Search 설정 (3x3x3 = 27가지 조합 탐색)
grid_search = GridSearchCV(
    estimator=RandomForestClassifier(random_state=42),
    param_grid=param_grid,
    cv=5,              # 5-fold 교차 검증
    scoring='accuracy',
    n_jobs=-1          # 모든 CPU 코어 사용
)

grid_search.fit(X_train, y_train)
print(f"최적 파라미터: {grid_search.best_params_}")
print(f"최고 정확도: {grid_search.best_score_:.4f}")

김개발 씨는 하이퍼파라미터의 중요성을 깨달았지만, 새로운 고민이 생겼습니다. "n_estimators를 50으로 할지 100으로 할지, max_depth를 5로 할지 10으로 할지...

어떻게 정하죠?" 박시니어 씨가 설명을 이어갔습니다. "가장 확실한 방법은 모든 조합을 다 시험해보는 거예요.

이걸 Grid Search라고 해요." Grid Search를 이해하려면 바둑판을 떠올려보면 됩니다. 가로줄과 세로줄이 만나는 모든 교차점을 하나씩 확인하는 것처럼, 하이퍼파라미터의 가능한 모든 조합을 체계적으로 탐색합니다.

예를 들어 n_estimators에 3가지 값(50, 100, 200), max_depth에 3가지 값(5, 10, 15), min_samples_split에 3가지 값(2, 5, 10)을 시험해보고 싶다고 가정해봅시다. 이 경우 총 3 x 3 x 3 = 27가지 조합이 만들어집니다.

Grid Search는 이 27가지 조합을 하나씩 모두 시험해봅니다. 첫 번째 조합인 (50, 5, 2)로 모델을 학습시키고 성능을 측정합니다.

그 다음 (50, 5, 5)로 시험하고, 또 (50, 5, 10)으로 시험합니다. 이런 식으로 27가지 조합을 모두 시험한 후, 가장 성능이 좋았던 조합을 알려줍니다.

위의 코드에서 param_grid는 탐색할 하이퍼파라미터와 그 후보값들을 딕셔너리로 정의합니다. GridSearchCV는 이 그리드를 받아서 모든 조합을 자동으로 탐색해줍니다.

여기서 cv=5는 5-fold 교차 검증을 의미합니다. 각 조합마다 데이터를 5등분하여 5번 학습과 검증을 반복하고, 평균 성능을 계산합니다.

이렇게 하면 특정 데이터 분할에 운 좋게 성능이 높게 나오는 것을 방지할 수 있습니다. n_jobs=-1은 컴퓨터의 모든 CPU 코어를 활용하여 병렬로 탐색하라는 의미입니다.

27가지 조합을 순차적으로 처리하면 오래 걸리지만, 병렬로 처리하면 시간을 크게 단축할 수 있습니다. Grid Search의 가장 큰 장점은 완전성입니다.

지정한 범위 내에서 최적의 조합을 반드시 찾아냅니다. 하지만 단점도 있습니다.

하이퍼파라미터의 종류나 후보값이 늘어나면 조합의 수가 기하급수적으로 증가합니다. 만약 5개의 하이퍼파라미터에 각각 10개의 후보값이 있다면, 총 10^5 = 100,000가지 조합을 시험해야 합니다.

여기에 5-fold 교차 검증까지 적용하면 50만 번의 모델 학습이 필요합니다. 이것이 Grid Search의 차원의 저주입니다.

김개발 씨가 물었습니다. "그럼 하이퍼파라미터가 많으면 어떻게 해요?" 박시니어 씨가 웃으며 대답했습니다.

"그럴 때 쓰는 방법이 따로 있어요. Random Search라고요."

실전 팁

💡 - Grid Search는 탐색 범위가 작을 때 효과적입니다

  • cv 값을 너무 크게 설정하면 시간이 오래 걸리므로 3~5 정도가 적당합니다

3. Random Search 효율적인 탐색 전략

김개발 씨가 Grid Search를 돌렸더니 2시간이나 걸렸습니다. 게다가 탐색하고 싶은 하이퍼파라미터가 더 있었습니다.

"이러다간 밤을 새야겠어요..." 한숨을 쉬는 김개발 씨에게 박시니어 씨가 다가왔습니다. "Random Search 써봤어요?

훨씬 빠르면서도 비슷한 결과를 얻을 수 있어요."

Random Search는 하이퍼파라미터 공간에서 무작위로 조합을 샘플링하여 탐색하는 방법입니다. 마치 넓은 바다에서 물고기를 잡을 때, 모든 곳에 그물을 치는 대신 여러 지점에 낚싯대를 무작위로 던지는 것과 같습니다.

놀랍게도 이 방법이 Grid Search보다 효율적인 경우가 많습니다.

다음 코드를 살펴봅시다.

from sklearn.model_selection import RandomizedSearchCV
from sklearn.ensemble import RandomForestClassifier
from scipy.stats import randint, uniform

# 탐색할 하이퍼파라미터 분포 정의
param_distributions = {
    'n_estimators': randint(50, 300),      # 50~300 사이 정수
    'max_depth': randint(3, 20),           # 3~20 사이 정수
    'min_samples_split': randint(2, 20),   # 2~20 사이 정수
    'min_samples_leaf': randint(1, 10),    # 1~10 사이 정수
    'max_features': uniform(0.1, 0.9)      # 0.1~1.0 사이 실수
}

# Random Search 설정 (100번만 무작위 탐색)
random_search = RandomizedSearchCV(
    estimator=RandomForestClassifier(random_state=42),
    param_distributions=param_distributions,
    n_iter=100,        # 100가지 조합만 탐색
    cv=5,
    scoring='accuracy',
    n_jobs=-1,
    random_state=42
)

random_search.fit(X_train, y_train)
print(f"최적 파라미터: {random_search.best_params_}")
print(f"최고 정확도: {random_search.best_score_:.4f}")

박시니어 씨가 화이트보드에 그림을 그리기 시작했습니다. "왜 무작위가 더 효율적인지 설명해줄게요." 2012년, Bergstra와 Bengio라는 두 연구자가 흥미로운 논문을 발표했습니다.

대부분의 머신러닝 문제에서 모든 하이퍼파라미터가 똑같이 중요하지 않다는 것입니다. 어떤 하이퍼파라미터는 성능에 큰 영향을 주고, 어떤 것은 거의 영향이 없습니다.

Grid Search의 문제는 여기서 발생합니다. 예를 들어 n_estimators와 max_depth 두 개의 하이퍼파라미터를 각각 3가지 값으로 탐색한다고 가정해봅시다.

Grid Search는 9가지 조합을 탐색하지만, 실제로 n_estimators의 서로 다른 값은 3가지, max_depth의 서로 다른 값도 3가지뿐입니다. 만약 max_depth가 성능에 결정적인 영향을 주고 n_estimators는 별로 중요하지 않다면 어떨까요?

Grid Search는 3가지 max_depth 값만 시험해본 셈입니다. 9번의 실험 중 6번은 사실상 낭비인 것입니다.

반면 Random Search로 9번 무작위 탐색을 하면, max_depth 값이 9가지 모두 다를 수 있습니다. 중요한 하이퍼파라미터에 대해 더 다양한 값을 시험해볼 수 있는 것입니다.

위의 코드를 살펴보면, Grid Search와 다른 점이 눈에 띕니다. param_grid 대신 param_distributions를 사용합니다.

여기서는 특정 값의 목록이 아니라, 값이 따르는 분포를 지정합니다. **randint(50, 300)**은 50부터 299까지의 정수 중 하나를 무작위로 선택하라는 의미입니다.

**uniform(0.1, 0.9)**는 0.1부터 1.0 사이의 실수를 균등하게 선택합니다. 이렇게 하면 연속적인 범위에서 다양한 값을 탐색할 수 있습니다.

n_iter=100은 100번만 무작위 탐색하겠다는 설정입니다. Grid Search에서 5개 하이퍼파라미터에 각각 10개 값이면 10만 번 탐색해야 했지만, Random Search는 100번만으로 충분히 좋은 결과를 얻을 수 있습니다.

실제로 많은 연구에서 Random Search가 Grid Search의 5%도 안 되는 계산량으로 비슷한 성능을 달성했다고 보고합니다. 특히 하이퍼파라미터가 많을수록 Random Search의 효율성이 빛납니다.

물론 Random Search에도 단점이 있습니다. 무작위성 때문에 실행할 때마다 결과가 달라질 수 있습니다.

그래서 random_state를 지정하여 재현 가능하게 만드는 것이 좋습니다. 김개발 씨가 Random Search를 돌려봤더니 30분 만에 결과가 나왔습니다.

놀랍게도 2시간 걸린 Grid Search보다 정확도가 더 높았습니다. "와, 이게 가능하군요!"

실전 팁

💡 - n_iter 값은 시간과 성능의 트레이드오프입니다. 보통 50~200 사이에서 시작하세요

  • 연속적인 하이퍼파라미터는 uniform이나 loguniform 분포를 사용하면 효과적입니다

4. 교차 검증의 중요성

김개발 씨가 하이퍼파라미터 튜닝을 마치고 기뻐했습니다. "정확도 95%예요!" 하지만 박시니어 씨는 고개를 저었습니다.

"그거 테스트 데이터로 측정한 거 아니죠? 교차 검증 결과를 봐야 해요." 김개발 씨는 교차 검증이 왜 그렇게 중요한지 궁금해졌습니다.

**교차 검증(Cross-Validation)**은 데이터를 여러 부분으로 나누어 학습과 검증을 반복하는 방법입니다. 마치 학생이 여러 모의고사를 보면서 실력을 검증하는 것과 같습니다.

하이퍼파라미터 튜닝에서 교차 검증 없이 성능을 측정하면, 특정 데이터 분할에 과적합된 조합을 선택할 위험이 있습니다.

다음 코드를 살펴봅시다.

from sklearn.model_selection import cross_val_score, KFold
from sklearn.ensemble import RandomForestClassifier

# K-Fold 교차 검증 설정
kfold = KFold(n_splits=5, shuffle=True, random_state=42)

model = RandomForestClassifier(n_estimators=100, max_depth=10, random_state=42)

# 교차 검증 실행
scores = cross_val_score(model, X_train, y_train, cv=kfold, scoring='accuracy')

print(f"각 폴드별 정확도: {scores}")
print(f"평균 정확도: {scores.mean():.4f}")
print(f"표준편차: {scores.std():.4f}")

# 표준편차가 크면 모델이 불안정하다는 신호
if scores.std() > 0.05:
    print("주의: 폴드별 편차가 큽니다. 모델이 불안정할 수 있습니다.")

박시니어 씨가 김개발 씨에게 질문했습니다. "만약 학생이 딱 한 번의 시험만 보고 대학에 간다면 어떨까요?" 김개발 씨가 대답했습니다.

"음... 그날따라 운이 좋았을 수도 있으니까 정확한 실력을 알기 어렵겠네요." "맞아요.

그래서 수능도 모의고사를 여러 번 보잖아요. 머신러닝에서 교차 검증도 같은 원리예요." 하이퍼파라미터 튜닝을 할 때, 단순히 학습 데이터와 검증 데이터를 한 번만 나누면 문제가 생깁니다.

우연히 그 분할에서만 잘 맞는 하이퍼파라미터를 선택할 수 있기 때문입니다. 이것을 검증 데이터에 대한 과적합이라고 합니다.

K-Fold 교차 검증은 이 문제를 해결합니다. 데이터를 K개의 부분(폴드)으로 나눈 뒤, 각 폴드를 한 번씩 검증 데이터로 사용합니다.

예를 들어 5-Fold면 데이터를 5등분하고, 5번의 학습-검증 사이클을 수행합니다. 첫 번째 사이클에서는 1번 폴드가 검증, 나머지 4개가 학습에 사용됩니다.

두 번째 사이클에서는 2번 폴드가 검증, 나머지가 학습에 사용됩니다. 이런 식으로 5번 반복하면, 모든 데이터가 한 번씩 검증에 사용됩니다.

위의 코드에서 **KFold(n_splits=5, shuffle=True)**는 데이터를 섞은 후 5개로 나누라는 설정입니다. shuffle=True가 중요한데, 데이터가 특정 순서로 정렬되어 있을 경우 섞지 않으면 편향된 결과가 나올 수 있기 때문입니다.

cross_val_score는 자동으로 K-Fold 교차 검증을 수행하고 각 폴드별 점수를 반환합니다. 평균뿐만 아니라 표준편차도 중요합니다.

표준편차가 크다면 모델이 데이터 분할에 따라 성능이 크게 달라진다는 의미입니다. 이는 모델이 불안정하거나, 데이터가 불균형하다는 신호일 수 있습니다.

GridSearchCV와 RandomizedSearchCV에서 cv=5를 설정하면, 내부적으로 5-Fold 교차 검증이 자동으로 수행됩니다. 각 하이퍼파라미터 조합에 대해 5번의 학습-검증을 하고, 평균 성능으로 조합들을 비교합니다.

실무에서는 데이터의 양과 계산 자원에 따라 K값을 조절합니다. 데이터가 충분히 많으면 K=3~5가 적당하고, 데이터가 적으면 K=10이나 Leave-One-Out까지 사용하기도 합니다.

김개발 씨가 교차 검증 결과를 확인하니, 표준편차가 0.03으로 안정적이었습니다. "이제 좀 더 신뢰할 수 있는 결과네요!"

실전 팁

💡 - 분류 문제에서는 StratifiedKFold를 사용하면 각 폴드에서 클래스 비율이 유지됩니다

  • 표준편차가 0.05 이상이면 모델이나 데이터를 점검해보세요

5. 탐색 범위 설정 전략

김개발 씨가 Random Search를 설정하다가 막혔습니다. "n_estimators 범위를 어떻게 잡아야 하죠?

1부터 10000까지요?" 박시니어 씨가 웃었습니다. "범위 설정도 전략이 필요해요.

무작정 넓게 잡으면 비효율적이고, 너무 좁게 잡으면 최적값을 놓칠 수 있어요."

하이퍼파라미터 탐색 범위를 설정하는 것은 튜닝의 성패를 좌우하는 중요한 결정입니다. 마치 금광을 찾을 때 어느 지역을 탐사할지 정하는 것과 같습니다.

범위가 너무 넓으면 자원이 낭비되고, 너무 좁으면 금맥을 놓칠 수 있습니다.

다음 코드를 살펴봅시다.

from scipy.stats import randint, uniform, loguniform

# 효과적인 탐색 범위 설정 예시
param_distributions = {
    # 정수형: 일반적인 범위로 시작
    'n_estimators': randint(50, 500),

    # 정수형: 작은 값이 중요한 경우
    'max_depth': randint(3, 30),

    # 실수형: 로그 스케일이 효과적인 경우 (학습률 등)
    'learning_rate': loguniform(1e-4, 1e-1),  # 0.0001 ~ 0.1

    # 비율형: 균등 분포 사용
    'subsample': uniform(0.6, 0.4),  # 0.6 ~ 1.0

    # 카테고리형: 리스트로 지정
    'criterion': ['gini', 'entropy']
}

# 2단계 탐색 전략: 넓은 범위 -> 좁은 범위
# 1단계: 넓은 범위로 대략적인 영역 파악
param_coarse = {
    'max_depth': randint(1, 50),
    'min_samples_split': randint(2, 50)
}

# 2단계: 1단계 결과를 바탕으로 좁은 범위 탐색
# (1단계에서 max_depth=15 근처가 좋았다면)
param_fine = {
    'max_depth': randint(10, 20),
    'min_samples_split': randint(5, 15)
}

박시니어 씨가 화이트보드에 그래프를 그렸습니다. "하이퍼파라미터마다 특성이 달라요.

그에 맞는 분포를 사용해야 해요." 먼저 정수형 하이퍼파라미터를 살펴봅시다. n_estimators나 max_depth 같은 값은 정수여야 합니다.

이런 경우 randint를 사용합니다. 범위는 보통 알고리즘의 특성과 데이터 크기를 고려해서 정합니다.

n_estimators는 트리의 개수인데, 일반적으로 50~500 사이면 충분합니다. 너무 적으면 성능이 낮고, 너무 많으면 학습 시간만 늘어나고 성능 향상은 미미합니다.

max_depth는 과적합을 제어하는 핵심 파라미터로, 3~30 정도가 적당합니다. **학습률(learning_rate)**처럼 작은 값이 중요한 하이퍼파라미터는 로그 스케일이 효과적입니다.

학습률은 보통 0.0001부터 0.1 사이에서 탐색하는데, 균등 분포로 탐색하면 0.0001~0.001 구간은 거의 탐색되지 않습니다. loguniform을 사용하면 0.0001, 0.001, 0.01, 0.1 근처가 균등하게 탐색됩니다.

비율형 파라미터인 subsample이나 max_features는 0과 1 사이의 값을 가집니다. 이런 경우 uniform 분포가 적합합니다.

uniform(0.6, 0.4)는 0.6부터 시작해서 0.4만큼의 범위, 즉 0.6~1.0을 의미합니다. 카테고리형 파라미터는 단순히 리스트로 지정합니다.

criterion은 'gini'나 'entropy' 중 하나를 선택하면 되므로 리스트로 후보를 나열합니다. 실무에서 자주 사용하는 전략은 2단계 탐색입니다.

처음에는 넓은 범위로 대략적인 최적 영역을 파악합니다. 그 다음 그 영역 주변을 좁은 범위로 정밀 탐색합니다.

마치 지도에서 넓은 지역을 훑어본 후, 유망한 지역을 집중적으로 탐사하는 것과 같습니다. 1단계에서 max_depth를 150으로 넓게 탐색했더니 15 근처에서 성능이 좋았다면, 2단계에서는 1020으로 범위를 좁혀서 더 정밀하게 탐색합니다.

이 방법은 효율적으로 최적값에 접근할 수 있게 해줍니다. 김개발 씨가 고개를 끄덕였습니다.

"아, 그래서 처음부터 범위를 잘 잡는 게 중요하군요!"

실전 팁

💡 - 학습률 같은 파라미터는 반드시 로그 스케일로 탐색하세요

  • 첫 탐색은 넓게, 이후 유망한 영역을 좁혀가는 2단계 전략을 권장합니다

6. 실전 파이프라인 구축

김개발 씨의 모델이 드디어 완성되었습니다. 정확도도 높고, 교차 검증 결과도 안정적이었습니다.

하지만 박시니어 씨가 또 한 가지를 지적했습니다. "데이터 전처리와 모델 튜닝을 분리하면 데이터 누수 문제가 생길 수 있어요.

Pipeline으로 묶어야 해요."

Pipeline은 데이터 전처리와 모델 학습을 하나의 흐름으로 연결하는 구조입니다. 마치 공장의 조립 라인처럼, 데이터가 여러 단계를 순서대로 거쳐 최종 예측까지 도달합니다.

하이퍼파라미터 튜닝 시 Pipeline을 사용하면 데이터 누수를 방지하고 재현 가능한 실험을 할 수 있습니다.

다음 코드를 살펴봅시다.

from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import randint

# 파이프라인 구성
pipeline = Pipeline([
    ('scaler', StandardScaler()),           # 1단계: 정규화
    ('pca', PCA()),                         # 2단계: 차원 축소
    ('classifier', RandomForestClassifier(random_state=42))  # 3단계: 분류
])

# 파이프라인 전체 하이퍼파라미터 탐색
param_distributions = {
    'pca__n_components': randint(5, 50),           # PCA 컴포넌트 수
    'classifier__n_estimators': randint(50, 300),  # 트리 개수
    'classifier__max_depth': randint(3, 20),       # 트리 깊이
}

# 파이프라인 + Random Search
search = RandomizedSearchCV(
    pipeline, param_distributions,
    n_iter=50, cv=5, n_jobs=-1, random_state=42
)

search.fit(X_train, y_train)
print(f"최적 파라미터: {search.best_params_}")
print(f"테스트 정확도: {search.score(X_test, y_test):.4f}")

박시니어 씨가 중요한 문제를 지적했습니다. "김개발 씨, StandardScaler를 어디에 적용했어요?" 김개발 씨가 대답했습니다.

"학습 데이터 전체에 fit_transform을 하고, 그 다음에 train_test_split으로 나눴어요." 박시니어 씨가 고개를 저었습니다. "그게 바로 **데이터 누수(Data Leakage)**예요.

테스트 데이터의 정보가 학습 과정에 새어 들어간 거예요." 데이터 누수를 이해하려면 시험을 생각해보면 됩니다. 학생이 모의고사를 볼 때, 실제 수능 문제를 미리 본 상태라면 공정하지 않겠죠?

마찬가지로 모델도 테스트 데이터의 정보를 미리 알면 안 됩니다. StandardScaler의 경우, fit 단계에서 데이터의 평균과 표준편차를 계산합니다.

전체 데이터에 fit을 하면, 테스트 데이터의 평균과 표준편차 정보가 학습에 포함됩니다. 사소해 보이지만, 이것이 성능 추정을 왜곡시킵니다.

Pipeline은 이 문제를 우아하게 해결합니다. 전처리 단계와 모델 학습 단계를 하나로 묶어서, 교차 검증의 각 폴드에서 전처리도 함께 수행됩니다.

학습 폴드에만 fit하고, 검증 폴드에는 transform만 적용합니다. 위의 코드에서 Pipeline은 세 단계로 구성됩니다.

첫 번째는 StandardScaler로 데이터를 정규화합니다. 두 번째는 PCA로 차원을 축소합니다.

세 번째는 RandomForestClassifier로 분류합니다. 데이터가 이 세 단계를 순서대로 통과합니다.

하이퍼파라미터 이름이 특이한 것을 눈치채셨나요? pca__n_components처럼 언더스코어 두 개로 단계 이름과 파라미터 이름을 연결합니다.

이렇게 하면 파이프라인 내부의 어떤 단계든 하이퍼파라미터를 튜닝할 수 있습니다. Pipeline의 또 다른 장점은 재현성입니다.

전처리부터 예측까지의 모든 과정이 하나의 객체에 담기므로, 저장하고 배포하기 쉽습니다. 실제 서비스에서 새로운 데이터가 들어오면, 같은 Pipeline을 그대로 적용하면 됩니다.

김개발 씨가 Pipeline을 적용하고 다시 튜닝을 돌렸습니다. 정확도가 약간 낮아졌지만, 이것이 더 정직한 결과입니다.

"이제 진짜 성능을 알게 됐네요!"

실전 팁

💡 - 반드시 Pipeline 내부에서 전처리를 수행하여 데이터 누수를 방지하세요

  • 파이프라인 단계 이름과 파라미터는 이중 언더스코어로 연결합니다

7. 결과 분석과 시각화

김개발 씨의 하이퍼파라미터 튜닝이 완료되었습니다. 최적의 파라미터를 찾았지만, 박시니어 씨가 물었습니다.

"결과를 시각화해서 분석해봤어요? 숫자만 보면 놓치는 인사이트가 많아요." 김개발 씨는 결과를 어떻게 분석해야 하는지 배우게 되었습니다.

하이퍼파라미터 튜닝 결과를 시각화하면 각 파라미터가 성능에 미치는 영향을 직관적으로 파악할 수 있습니다. 마치 실험 데이터를 그래프로 그려야 경향성이 보이는 것처럼, 튜닝 결과도 시각화해야 숨은 패턴을 발견할 수 있습니다.

다음 코드를 살펴봅시다.

import pandas as pd
import matplotlib.pyplot as plt

# Grid Search 결과를 DataFrame으로 변환
results = pd.DataFrame(grid_search.cv_results_)

# 주요 컬럼 선택
cols = ['param_n_estimators', 'param_max_depth', 'mean_test_score', 'std_test_score']
results_summary = results[cols].sort_values('mean_test_score', ascending=False)
print(results_summary.head(10))

# 하이퍼파라미터별 성능 시각화
fig, axes = plt.subplots(1, 2, figsize=(12, 4))

# n_estimators vs 성능
results.groupby('param_n_estimators')['mean_test_score'].mean().plot(
    kind='bar', ax=axes[0], title='n_estimators vs Performance'
)

# max_depth vs 성능
results.groupby('param_max_depth')['mean_test_score'].mean().plot(
    kind='bar', ax=axes[1], title='max_depth vs Performance'
)

plt.tight_layout()
plt.savefig('tuning_results.png')

박시니어 씨가 김개발 씨의 화면을 보며 말했습니다. "best_params_만 보면 아쉬워요.

cv_results_에 금광 같은 정보가 숨어 있거든요." GridSearchCV와 RandomizedSearchCV는 모든 탐색 결과를 cv_results_라는 딕셔너리에 저장합니다. 이것을 DataFrame으로 변환하면 다양한 분석이 가능해집니다.

위의 코드에서 **pd.DataFrame(grid_search.cv_results_)**는 탐색 결과를 표 형태로 변환합니다. 여기에는 각 조합의 파라미터 값, 평균 점수, 표준편차, 순위 등이 포함되어 있습니다.

mean_test_score는 각 조합의 교차 검증 평균 점수입니다. 이 값으로 정렬하면 상위 조합들을 한눈에 볼 수 있습니다.

std_test_score는 폴드 간 점수의 표준편차인데, 이 값이 작을수록 안정적인 조합입니다. 시각화를 하면 흥미로운 패턴을 발견할 수 있습니다.

예를 들어 n_estimators를 그래프로 그려보면, 어느 시점부터 성능 향상이 둔화되는지 알 수 있습니다. 100에서 200으로 늘렸을 때는 성능이 올랐지만, 200에서 300으로 늘려도 거의 차이가 없다면?

100이나 200을 선택하는 것이 효율적입니다. max_depth 그래프에서는 과적합의 징후를 발견할 수 있습니다.

깊이가 깊어질수록 성능이 좋아지다가, 어느 시점부터 오히려 나빠진다면 그게 과적합의 시작점입니다. 또 하나 주목할 점은 파라미터 간 상호작용입니다.

n_estimators=100일 때 최적의 max_depth와 n_estimators=200일 때 최적의 max_depth가 다를 수 있습니다. 히트맵으로 두 파라미터의 조합별 성능을 시각화하면 이런 상호작용을 발견할 수 있습니다.

실무에서는 이런 분석 결과를 팀원들과 공유합니다. 그래프 한 장이 숫자 나열보다 훨씬 설득력 있습니다.

또한 이 분석은 다음 튜닝의 방향을 잡는 데도 도움이 됩니다. 김개발 씨가 결과를 시각화해보니, max_depth가 성능에 가장 큰 영향을 미친다는 것을 발견했습니다.

"다음엔 max_depth 주변을 더 정밀하게 탐색해봐야겠어요!"

실전 팁

💡 - cv_results_를 DataFrame으로 변환하면 다양한 분석이 가능합니다

  • 시각화로 파라미터별 성능 경향과 과적합 징후를 파악하세요

8. 실무 팁과 주의사항

김개발 씨가 프로젝트를 마무리하고 있을 때, 박시니어 씨가 마지막 조언을 건넸습니다. "하이퍼파라미터 튜닝에서 초보자들이 흔히 하는 실수들이 있어요.

미리 알아두면 삽질을 줄일 수 있죠." 김개발 씨는 귀를 기울였습니다.

하이퍼파라미터 튜닝은 강력한 기법이지만, 잘못 사용하면 오히려 독이 될 수 있습니다. 마치 좋은 약도 과용하면 해가 되는 것처럼, 튜닝도 올바른 방법으로 적절히 사용해야 합니다.

흔한 실수들을 피하면 더 효율적이고 신뢰할 수 있는 결과를 얻을 수 있습니다.

다음 코드를 살펴봅시다.

from sklearn.model_selection import train_test_split, RandomizedSearchCV
from sklearn.ensemble import RandomForestClassifier
import numpy as np

# 올바른 데이터 분할: 테스트 세트는 최종 평가용으로 분리
X_temp, X_test, y_temp, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

# 튜닝은 임시 데이터로만 수행
random_search = RandomizedSearchCV(
    RandomForestClassifier(random_state=42),
    param_distributions={'n_estimators': [50, 100, 200], 'max_depth': [5, 10, 15]},
    n_iter=10, cv=5, n_jobs=-1, random_state=42
)

random_search.fit(X_temp, y_temp)

# 최종 평가는 테스트 세트로 딱 한 번만!
final_score = random_search.score(X_test, y_test)
print(f"최종 테스트 정확도: {final_score:.4f}")

# 주의: 테스트 점수가 cv 점수보다 많이 낮으면 과적합 의심
cv_score = random_search.best_score_
if cv_score - final_score > 0.05:
    print("경고: 교차 검증과 테스트 점수 차이가 큽니다. 과적합을 의심해보세요.")

박시니어 씨가 화이트보드에 큰 글씨로 썼습니다. "테스트 세트는 신성하다." 가장 흔한 실수는 테스트 세트로 여러 번 평가하는 것입니다.

하이퍼파라미터를 조금 바꾸고 테스트, 또 바꾸고 테스트... 이러면 테스트 세트에 맞추는 튜닝을 하게 됩니다.

결국 테스트 세트에서만 잘 되고 실제 데이터에서는 성능이 떨어집니다. 올바른 방법은 테스트 세트를 완전히 분리해두고, 모든 튜닝이 끝난 후 딱 한 번만 평가하는 것입니다.

위의 코드처럼 먼저 테스트 세트를 떼어놓고, 나머지 데이터로만 교차 검증 기반 튜닝을 수행합니다. 두 번째 실수는 과도한 튜닝입니다.

하이퍼파라미터를 너무 많이, 너무 정밀하게 튜닝하면 오히려 역효과가 납니다. 교차 검증 데이터에 과적합되어 일반화 성능이 떨어집니다.

대부분의 경우 가장 중요한 2~3개의 하이퍼파라미터만 튜닝해도 충분합니다. 세 번째 실수는 random_state를 고정하지 않는 것입니다.

재현 가능한 실험을 위해 모델과 탐색 모두에 random_state를 설정해야 합니다. 그래야 나중에 같은 결과를 다시 얻을 수 있고, 동료들과 결과를 공유할 수 있습니다.

네 번째 실수는 교차 검증 점수만 보는 것입니다. 최종 테스트 점수와 교차 검증 점수를 비교해야 합니다.

차이가 크다면 과적합의 신호입니다. 위의 코드처럼 두 점수의 차이를 확인하는 습관을 들이세요.

다섯 번째로 주의할 점은 계산 자원 관리입니다. 무작정 많은 조합을 탐색하면 시간과 비용이 낭비됩니다.

먼저 적은 데이터로 빠르게 실험하고, 유망한 범위가 좁혀지면 전체 데이터로 정밀 탐색하는 것이 효율적입니다. 마지막으로, 도메인 지식을 활용하세요.

무작정 넓은 범위를 탐색하기보다, 알고리즘과 데이터의 특성을 이해하고 합리적인 범위를 설정하면 훨씬 효율적입니다. 김개발 씨가 고개를 끄덕였습니다.

"처음에는 무조건 많이 시도하면 좋은 줄 알았는데, 전략이 필요하군요!"

실전 팁

💡 - 테스트 세트는 최종 평가용으로 딱 한 번만 사용하세요

  • 중요한 하이퍼파라미터 2~3개에 집중하는 것이 효율적입니다
  • 항상 random_state를 고정하여 재현 가능한 실험을 하세요

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Python#HyperparameterTuning#GridSearch#RandomSearch#MachineLearning#Data Science

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.