TIL(Today I Learned)
1️⃣실전 자동화 프로젝트 (7)
문제 설정
- 과제: 품질을 3등급(
Y_Class= 0/1/2)으로 분류, 제품군 2개(A_31316×551,TO_31592×334)를 독립 처리 - 클래스 불균형: A_31은 24/48/29%로 비교적 균형, TO_31은 8/83/9%로 극심(소수 클래스 각 48·54건)
- 결측 처리: 트리 모델의 native 결측 처리를 살리려
NaN을 일부러 그대로 둔다
피처 선택 파이프라인
- 상관 덴드로그램 클러스터링 — 강상관 컬럼을 묶어 클러스터당 분산 최대 1개만 남김. 상관은 pairwise-complete로 계산하되 모델 입력 데이터의 NaN은 건드리지 않는다.
- native
feature_importances_사전 필터 — 학습 시 나오는 중요도로 상위 200개. - Permutation importance — 그 200개에 대해서만 계산해 상위 50개.
- SHAP (TreeExplainer) — 최종 50개에만 적용.
리키지 방지의 핵심: 2~4는 타깃을 쓰므로 반드시 CV 안쪽에서만 돌린다. 1번은 비지도 단계지만 그래도 train으로만 계산했다.
검증 전략: train test split vs nested CV
작은 데이터에서 성능 숫자를 일반화하여 내는게 계속 문제였다. 최종적으로 두 제품군을 다르게 처리했다.
- A_31 (균형) → train test split: 8:2로 한 번 나누고 test는 피처 선택에 관여시키지 않다가 마지막 참고 평가에만 사용.
- TO_31 (불균형) → Nested CV: outer 5-fold로 나눠 outer-test는 피처 선택에서 배제, outer-train 안 inner 5-fold로 피처를 고른 뒤 outer-test로 딱 한 번 평가.
발견 1: 단일 split시 문제 — "recall 0"은 우연이었다
TO_31을 sealed split으로 평가하니 클래스 0 recall이 0.000이었다. test에 클래스 0이 10건뿐인데 하나도 못 맞춘 것. 이게 "모델이 원래 못하는 것"인지 "이번 10건이 유독 어려웠던 것"인지 단일 split으로는 알 수 없었다. 그래서 TO_31만 nested CV로 소수 클래스를 5 fold에 골고루 나눠 평가했다.
| 지표 | Sealed split | Nested CV (5-fold 평균) |
|---|---|---|
| macro-F1 | 0.478 | 0.624 ± 0.049 |
| 클래스 0 recall | 0.000 | 0.440 (fold별 0.2~0.7) |
| 클래스 2 recall | 0.455 | 0.315 (fold별 0.18~0.36) |
클래스 0의 "0"은 우연이었다(평균 44%). 반면 클래스 2는 5 fold 모두 18~36%로 꾸준히 낮았다 — 이쪽이 진짜 문제였다.
교훈: 불균형 + 작은 데이터에서 단일 split은 믿을 게 못 된다. fold를 반복해 분산을 봐야 우연과 구조를 구분한다.
발견 2: 이론적으로 맞는 개선이 실측에서 틀릴 수 있다
파이프라인 리뷰 중 "버그"를 찾았다고 생각했다: permutation_importance에 scoring을 안 줘서 기본값 accuracy로 피처를 고르고 있었다. TO_31은 83%가 다수 클래스라 accuracy가 다수에 쏠린다. 평가 지표는 macro-F1인데 피처는 accuracy로 뽑으니 소수 클래스용 피처가 탈락하는 것 아닐까? 그럴듯해서 scoring='f1_macro'로 바꿔 동일 조건 A/B 실험을 돌렸다.
| 지표 | accuracy (기존) | f1_macro (수정안) |
|---|---|---|
| macro-F1 | 0.624 ± 0.049 | 0.600 ± 0.042 |
| 클래스 0 recall | 0.440 | 0.356 |
| 클래스 2 recall | 0.315 | 0.295 |
예상과 반대로 전부 나빠졌다. inner validation fold 하나에 소수 클래스가 8~9건뿐이라, 표본이 적으면 f1_macro는 몇 건에 요동치는 노이즈 큰 scorer가 되고 permutation importance가 그 흔들리는 값으로 피처를 골라 더 불안정해진다. accuracy는 편향돼 있어도 표본이 많아 안정적이라 오히려 더 나은 피처를 골랐다.
발견 3: 혼동행렬이 알려준 진짜 원인
마지막으로 class_weight를 실험했다(소수 클래스 가중치를 balanced의 여러 배율로).
| 배율 | macro-F1 | 0 recall | 1 recall | 2 recall |
|---|---|---|---|---|
| 1.0 | 0.624 ± 0.049 | 0.440 | 0.971 | 0.315 |
| 2.0 | 0.629 ± 0.055 | 0.440 | 0.967 | 0.333 |
| 3.0 | 0.637 ± 0.075 | 0.484 | 0.953 | 0.351 |
소폭 오르지만 이득이 표준편차 안에 들고 불안정성은 커졌다.
예측0 예측1 예측2
실제0 [ 21 26 1 ] ← 48건 중 26건이 다수 클래스로 흡수
실제1 [ 7 476 7 ] ← 거의 완벽
실제2 [ 2 35 17 ] ← 54건 중 35건(65%)이 클래스 1로 흡수
클래스 2의 65%가 클래스 1로 빨려들어가고, 가중치를 3배로 올려도 그 35건 그대로. 이 35건은 모델이 보기에 클래스 1과 구분이 안 되는 샘플. 즉 가중치 문제가 아니라 피처가 클래스 2를 클래스 1과 분리할 신호를 못 담은 것 — 튜닝으로 개선할 수 없는 한계
교훈: 성능이 안 나올 때 "어디로 틀리는지(혼동행렬)"를 봐야 원인을 안다. 클래스 2는 데이터·피처의 한계였고, 개선하려면 T계열 세부 품질을 구분하는 새 도메인 피처나 소수 클래스 데이터가 더 필요하다.
결론
- 데이터 누수: 피처 선택도 타깃을 쓰면 CV 안에서. 밖에서 고르면 성능이 부풀려진다.
- 불균형 + 작은 데이터: 단일 split 숫자는 믿지 말고 반복해 분산을 볼 것.
- 가설은 실험으로: 이론적으로 맞는 수정(
f1_macro)이 실측에선 해로웠다. - 한계: 혼동행렬이 클래스 2의 한계가 튜닝 밖에 있음을 보여줬다.
💡 느낀점
솔직히 코드 짜는 시간보다 이 숫자 믿어도 되나 의심하는 시간이 더 길었다. recall 0 보고 좌절했는데 알고 보니 그냥 운 나쁜 split이었고, 맞다고 확신한 f1_macro 수정은 오히려 성능을 깎았다. 실험 안 돌렸으면 둘 다 틀린 결론으로 넘어갔을 거다. 클래스 2는 결국 못 살렸지만, 튜닝으로 안 되는 걸 붙잡는 대신 데이터 문제인 근거를 명확히하고 이를 인지하며 개선하는 방향을 찾아봐야할 것 같다. 추후에는 트리모델 기반으로 다양화하여 성적을 비교해볼 예정이다.
'최종 프로젝트' 카테고리의 다른 글
| Chapter.9 최종 실전 자동화 프로젝트 (11) (0) | 2026.07.10 |
|---|---|
| Chapter.9 최종 실전 자동화 프로젝트 (10) (1) | 2026.07.09 |
| Chapter.9 최종 실전 자동화 프로젝트 (8) (0) | 2026.07.07 |
| Chapter.9 최종 실전 자동화 프로젝트 (7) (0) | 2026.07.02 |
| Chapter.9 최종 실전 자동화 프로젝트 (6) (0) | 2026.07.01 |