TIL(Today I Learned)
1️⃣최종 프로젝트(15)
피처 선택 파이프라인과 Nested CV
오늘은 수천 개의 센서 변수 중 필요한 피처만 선택하고, 하이퍼파라미터 튜닝까지 진행한 뒤 Nested CV로 모델을 평가하는 과정을 배웠다.
기존에는 피처 선택과 모델 평가를 각각 따로 생각했는데, 피처를 고르는 과정도 데이터를 보고 결정하는 하나의 학습 과정이라는 점이 중요했다.
1. Nested CV를 사용하는 이유
전체 데이터를 이용해 피처를 선택한 뒤 같은 데이터로 모델 성능을 평가하면 데이터 누수가 발생할 수 있다.
피처를 선택하는 과정에서 이미 전체 데이터의 정보를 확인했기 때문에, 모델 입장에서는 시험 문제를 일부 미리 본 것과 비슷하다.
이를 방지하기 위해 교차검증을 두 단계로 나누는 Nested CV를 사용한다.
구분역할
| Outer CV | 최종 모델 성능 평가 |
| Inner CV | 피처 선택과 하이퍼파라미터 탐색 |
Outer CV의 테스트 데이터는 피처 선택과 튜닝 과정에 전혀 사용하지 않는다.
각 Outer fold마다 훈련 데이터만 이용해 피처를 다시 선택하고 모델을 튜닝한 뒤, 남겨둔 테스트 데이터로 최종 성능을 평가한다.
핵심은 탐색에 사용하는 데이터와 최종 채점에 사용하는 데이터를 분리하는 것이다.
2. 피처 선택 4단계
이번 실습에서는 피처 선택을 네 단계로 나누어 진행했다.
단계내용기준
| STEP 1 | 정보가 거의 없는 피처 제거 | 한 값의 비율이 99.9% 이상 |
| STEP 2 | 비슷한 피처 정리 | 상관계수 0.90 이상 |
| STEP 3 | 피처 중요도 순위 계산 | Permutation Importance |
| STEP 4 | 사용할 피처 개수 결정 | 성능 곡선과 1-SE 규칙 |
STEP 1과 STEP 2는 모델과 관계없이 동일하게 적용할 수 있다.
반면 STEP 3과 STEP 4는 모델마다 다르게 진행해야 한다.
LightGBM, XGBoost, CatBoost가 피처를 활용하는 방식이 다르기 때문에 중요도 순위와 최적 피처 개수도 달라질 수 있다.
코드 흐름은 다음과 같다.
def select_features(data, model_type):
features = remove_useless_features(data)
features = remove_high_correlation(data, features)
ranked_features = rank_by_importance(data, features, model_type)
selected_features = choose_feature_count(
data, ranked_features, model_type
)
return selected_features
불필요한 피처 제거부터 중요도 계산, 최종 피처 개수 결정까지 하나의 함수로 연결했다.
3. 1-SE 규칙
STEP 4에서는 RMSE가 가장 낮은 지점의 피처 개수를 바로 선택하지 않았다.
대신 최저 RMSE에 표준오차를 더한 범위 안에서 가장 적은 피처 개수를 선택하는 1-SE 규칙을 사용했다.
best_idx = np.argmin(mean_rmse)
limit = mean_rmse[best_idx] + standard_error[best_idx]
selected_idx = next(
i for i, score in enumerate(mean_rmse)
if score <= limit
)
예를 들어 피처 30개와 10개의 성능 차이가 거의 없다면, 더 단순한 10개를 선택하는 방식이다.
피처 수가 늘어날수록 성능이 계속 좋아지는 것처럼 보일 수 있지만, 성능 곡선이 평탄한 구간에서는 작은 차이가 단순한 노이즈일 수도 있다.
1-SE 규칙은 이런 미세한 차이를 무리하게 따라가지 않고, 성능을 유지하면서 더 단순한 모델을 선택하는 기준이다.
4. 하이퍼파라미터 튜닝
피처 선택 후에는 Randomized Search 방식으로 모델별 하이퍼파라미터를 탐색했다.
이번 과정에서 중요했던 점은 기본 모델과 튜닝 모델이 같은 피처를 사용하도록 고정한 것이다.
피처 선택은 기본 하이퍼파라미터로 한 번 수행하고, 선택된 피처를 기본 모델과 튜닝 모델이 공통으로 사용했다.
features = select_features(data, model_type)
base_score = evaluate(default_model, data[features])
for params in parameter_candidates:
model = make_model(model_type, params)
score = evaluate(model, data[features])
if score < best_score:
best_score = score
best_params = params
피처까지 각각 다르게 선택하면 성능이 좋아진 이유가 피처 때문인지, 하이퍼파라미터 때문인지 구분하기 어렵다.
같은 피처를 사용해야 두 모델의 차이를 순수한 튜닝 효과로 볼 수 있다.
또한 탐색한 파라미터가 기본 모델보다 좋은 성능을 내지 못하면 기본값을 그대로 유지했다.
튜닝을 수행했다고 해서 반드시 튜닝 모델을 사용해야 하는 것은 아니다.
5. 기본 모델과 튜닝 모델 비교
Nested CV의 각 Outer fold에서 동일한 피처를 사용해 기본 모델과 튜닝 모델을 각각 학습했다.
for model_type in models:
features = select_features(train_data, model_type)
for variant in ["default", "tuned"]:
model = build_model(model_type, variant)
model.fit(train_data[features], y_train)
prediction = model.predict(test_data[features])
save_metrics(model_type, variant, prediction)
같은 fold와 같은 피처를 사용했기 때문에 기본 모델과 튜닝 모델의 성능을 공정하게 비교할 수 있었다.
6. 평가 지표는 Macro-F1
이번 데이터의 원래 타깃은 연속형 품질값이기 때문에 회귀 모델로 예측했다.
이후 회귀 예측값을 미리 정한 임계값으로 변환해 품질 등급 0, 1, 2로 분류하고 Macro-F1을 계산했다.
지표특징
| Accuracy | 전체 데이터 중 맞힌 비율 |
| Macro-F1 | 각 등급의 F1을 동일한 비중으로 평균 |
클래스가 불균형한 데이터에서는 Accuracy만 보면 다수 클래스에 치우친 모델도 성능이 높게 보일 수 있다.
Macro-F1은 각 등급을 동일하게 평가하기 때문에 소수 등급을 제대로 예측하지 못하면 점수가 바로 낮아진다.
따라서 이번 실습에서는 Accuracy보다 Macro-F1을 주요 평가 지표로 사용했다.
7. 제품군 A와 TO 실습 결과
전처리가 끝난 제품군 A와 TO 데이터를 이용해 피처 선택, 하이퍼파라미터 튜닝, Nested CV 평가를 진행했다.
사용한 모델은 다음과 같다.
- LightGBM Regressor
- XGBoost Regressor
- CatBoost Regressor
- 세 모델의 예측값을 평균한 앙상블
결측치를 별도로 대체하지 않고 처리할 수 있는 트리 기반 모델을 중심으로 비교했다.
제품군별 최적 모델
제품군 A에서는 XGBoost가 Macro-F1 0.718로 가장 높았다.
제품군 TO에서는 CatBoost가 Macro-F1 0.692로 가장 좋은 성능을 보였다.
제품군마다 데이터 구조와 중요한 센서가 다르기 때문에 하나의 모델을 모든 제품군에 동일하게 적용하는 것보다 제품군별로 적합한 모델을 선택하는 것이 더 합리적이었다.
앙상블 결과
세 모델의 예측값을 평균한 앙상블은 두 제품군에서 모두 2위를 기록했다.
여러 모델을 결합한다고 항상 단일 모델보다 좋은 성능이 나오는 것은 아니었다.
이번 결과에서는 각 제품군에 가장 잘 맞는 단일 모델이 앙상블보다 높은 성능을 보여 최종 모델은 단일 모델로 선택했다.
선택된 피처 개수
1-SE 규칙을 적용한 결과 제품군 A는 12개, TO는 3개의 피처만 선택됐다.
처음에는 수천 개의 센서 변수가 필요할 것이라고 생각했지만, 실제로는 소수의 핵심 피처가 성능 대부분을 설명하고 있었다.
성능 곡선도 초반에는 빠르게 개선되다가 일정 개수 이후부터 거의 평탄해졌다.
많은 변수를 사용하는 것이 항상 좋은 것이 아니라는 점을 결과로 확인할 수 있었다.
피처 중요도
TO에서는 X_43 하나의 중요도가 53.5%로 매우 높았다.
특정 센서 하나가 예측 결과에 큰 영향을 주고 있다는 뜻이다.
반면 A에서는 다음 세 피처의 중요도가 비교적 고르게 분산됐다.
- X_189: 17.5%
- X_318: 16.2%
- X_2841: 13.7%
상위 세 피처가 전체 중요도의 약 47%를 차지했지만, TO처럼 하나의 센서가 절대적인 비중을 차지하지는 않았다.
정확도의 착시
TO의 등급 분포는 48 / 490 / 54로 가운데 등급에 데이터가 많이 몰려 있었다.
Accuracy는 0.868로 높았지만 Macro-F1은 0.692였다.
다수 등급은 잘 맞혔지만 소수 등급의 성능이 상대적으로 낮았기 때문이다.
Accuracy만 확인했다면 모델이 전체 등급을 잘 예측한다고 잘못 판단할 수 있었다.
튜닝 효과
튜닝은 전체 점수의 큰 상승보다는 소수 등급의 예측 성능 개선에서 효과를 확인할 수 있었다.
TO의 등급 2 F1은 0.53에서 0.56으로 상승했고, A의 등급 0 F1은 0.66에서 0.68로 상승했다.
상승 폭은 크지 않았지만, 학습 가중치와 하이퍼파라미터 튜닝이 적은 등급을 예측하는 데 도움을 준 것으로 보인다.
남은 과제
TO 제품군의 등급 0과 2는 여전히 F1 점수가 0.5~0.6 수준이었다.
추가 개선을 위해 다음 방법을 고려할 수 있다.
- 품질 등급 분류 임계값 재조정
- 소수 등급 데이터 추가 확보
- 소수 등급에 대한 가중치 조정
- 회귀 모델과 분류 모델의 성능 비교
- 중요도가 높은 센서의 실제 공정 의미 분석
특히 TO는 X_43의 중요도가 지나치게 높기 때문에, 실제로 중요한 공정 센서인지 데이터 누수 가능성이 있는 변수인지 추가 확인할 필요가 있다.
오늘의 정리
피처 선택도 데이터를 이용해 결정하는 학습 과정이기 때문에 교차검증 안에서 수행해야 한다.
전체 데이터로 피처를 선택한 뒤 같은 데이터로 평가하면 성능이 실제보다 높게 나올 수 있다.
피처 선택 과정은 다음 네 단계로 구성했다.
정보가 없는 피처 제거 → 비슷한 피처 정리 → 중요도 순위 계산 → 피처 개수 결정
피처 개수는 최저 RMSE만 따라가지 않고 1-SE 규칙을 사용해 성능이 비슷한 범위 안에서 가장 단순한 구성을 선택했다.
하이퍼파라미터 튜닝을 비교할 때는 기본 모델과 튜닝 모델이 같은 피처를 사용하도록 고정했다.
그래야 성능 차이가 피처 선택 때문이 아니라 순수한 튜닝 효과라는 것을 확인할 수 있다.
또한 불균형 데이터에서는 Accuracy가 높더라도 소수 등급을 제대로 예측하지 못할 수 있기 때문에 Macro-F1을 함께 확인해야 한다.
💡 느낀점
이번 실습에서는 모델을 만드는 것보다 실험 조건을 공정하게 설정하는 과정이 더 중요하게 느껴졌다. 처음에는 좋은 피처와 좋은 파라미터를 많이 찾으면 성능이 자연스럽게 올라갈 것이라고 생각했다. 하지만 피처 선택과 튜닝을 한꺼번에 바꾸면 어떤 요소 때문에 성능이 좋아졌는지 설명하기 어려웠다. 같은 피처와 같은 fold를 사용하고 파라미터만 바꿔야 튜닝 효과를 제대로 비교할 수 있었다. 또 수천 개의 센서가 있어도 실제 예측에 필요한 피처는 생각보다 매우 적었다. A는 12개, TO는 3개만으로도 대부분의 성능을 유지할 수 있었고, 피처가 많다고 무조건 좋은 모델이 되는 것은 아니라는 점을 다시 확인했다.
'최종 프로젝트' 카테고리의 다른 글
| Chapter.9 최종 실전 자동화 프로젝트 (16) (0) | 2026.07.23 |
|---|---|
| Chapter.9 최종 실전 자동화 프로젝트 (15) (0) | 2026.07.22 |
| Chapter.9 최종 실전 자동화 프로젝트 (14) (0) | 2026.07.21 |
| Chapter.9 최종 실전 자동화 프로젝트 (13) (0) | 2026.07.20 |
| Chapter.9 최종 실전 자동화 프로젝트 (12) (1) | 2026.07.13 |