회사 상품 추천 모델 만들기 (4) 피처를 고르는 법, greedy가 놓치는 조합
단독 ablation이 1위로 꼽은 피처가 최종 조합 1위가 아니었던 경험. greedy forward selection의 한계와 전수 부분집합 탐색의 가치, 그리고 4개 결합이 모든 단독보다 나빴던 과적합 사례.
회사 상품 추천 모델 만들기 (4) 피처를 고르는 법, greedy가 놓치는 조합
앞 편 요약: #3에서 콘텐츠 Two-Tower를 세워 recall@20을 0.0048에서 0.0534로 끌어올렸습니다.
이 편의 핵심 트레이드오프는 “단독 ablation으로 충분한가, 아니면 모든 조합을 다 돌려봐야 하는가”였습니다. 결론부터 말하면 단독 ablation만 믿었다면 최종 모델을 잘못 골랐을 거라는 게 결과적으로 드러났습니다.
1. 추가 후보 피처들
#3의 1차 재설계 후 모델이 동작하기 시작했으니, 다음 질문은 “여기에 어떤 피처를 더 넣을 수 있나”였습니다. 회사 DB를 다시 뒤져 후보를 모았습니다.
| 피처군 | 의미 | 추가 데이터 |
|---|---|---|
wishs_brands | 유저가 찜한 브랜드 목록 | 91,068건, 전 기간 |
request_signal | 견적요청(request_product, status=‘완료’)을 인터랙션 시그널에 추가 | 17,016건, 전 기간 |
brands_feats | 브랜드 메타데이터(브랜드 자체의 등급, 등록일 등) | 47K 브랜드 |
brand_metrics | 외부 소스(1688, vvic)에서 가져온 브랜드 단위 지표 (return_rate, cancel_rate 등) | 외부 metrics 테이블 |
products_1688_metrics | 1688 상품 단위 지표 | 외부 metrics |
| 이미지(CLIP) | 상품 썸네일을 CLIP(이미지-텍스트 결합 임베딩 모델)으로 인코딩한 시각 임베딩 | CDN 99.97% 보유 |
후보가 6개였습니다. 한 번에 다 넣고 학습하면 안 된다는 건 직관적으로 알았습니다. 어떤 피처가 진짜 도움이 되고, 어떤 피처는 노이즈인지 모르면 다음 번에 데이터가 바뀌었을 때 디버깅이 불가능합니다.
2. 첫 접근: 단독 ablation
추천 시스템 문헌에서 가장 흔한 ablation(피처 하나씩 빼거나 더해서 비교하는 방식) 방법은 단독 비교라고 합니다. baseline M0에 피처 하나씩만 추가해 비교합니다.
M0 (베이스): wb_rs도 다 끄고 #3의 콘텐츠 Two-Tower만
M0 + wishs_brands
M0 + request_signal
M0 + brands_feats
M0 + brand_metrics
M0 + products_1688_metrics각각 학습하고 recall@20을 측정. 통계적 유의성을 위해 paired permutation test(짝지은 순열 검정, n=10,000)와 Holm-Bonferroni 보정(다중 비교 보정)을 적용했습니다. 이 통계 도구를 왜 쓰는지는 #5에서 자세히 다룹니다.
결과는 다음과 같았습니다.
| 피처군 | recall@20 | diff vs M0 | perm p | Holm 보정 후 | 판정 |
|---|---|---|---|---|---|
| brand_metrics | 0.0611 | +0.0111 | 0.000 | 유의 | 채택 |
| request_signal | 0.0596 | +0.0095 | 0.001 | 유의 | 채택 |
| brands_feats | 0.0560 | +0.0059 | 0.018 | 유의 | 채택 |
| wishs_brands | 0.0551 | +0.0050 | 0.008 | 유의 | 채택 |
| products_1688_metrics | 0.0526 | +0.0025 | 0.153 | 비유의 | 드롭 |
M0 = 0.0501 (이 단계의 깨끗한 baseline).
products_1688_metrics는 p=0.153으로 통계적으로 무시할 만한 차이라고 합니다. 우연일 가능성이 큽니다. 드롭.
나머지 4개는 모두 유의미하게 baseline을 초과했습니다. 직관적으로는 “단독 1위 brand_metrics를 베이스로, 나머지 3개를 차례로 더하면 더 좋아지겠지”라고 생각하기 쉬웠습니다. 그게 greedy forward selection(욕심쟁이 전진 선택)의 직관입니다.
3. 비교 대상: greedy forward selection vs 전수 부분집합
피처 선택의 표준 방법으로 두 가지가 있다고 합니다.
| 방법 | 설명 | 비용 | 위험 |
|---|---|---|---|
| Greedy forward selection (욕심쟁이 전진 선택) | 단독 1위를 base로, 다음 단계에서 가장 큰 개선을 주는 피처를 추가. 개선이 없을 때까지 반복 | O(n²) 학습 | 지역 최적해(local optimum)에 갇힘 |
| 전수 부분집합 탐색 | 2ⁿ - 1개의 모든 가능한 조합을 다 학습하고 비교 | O(2ⁿ) 학습 | 비용. 검정 횟수 폭증으로 다중비교 문제 |
O(n²)와 O(2ⁿ), 비용이 늘어나는 속도
빅오 표기는 “입력이 커질 때 계산량이 얼마나 빠르게 늘어나는가”를 나타내는 약식 표현입니다. 절대 시간이 아니라 증가 속도를 봅니다.
피처가 n개일 때 O(n²) 은 n이 2배가 되면 일이 대략 4배가 된다는 뜻입니다(greedy는 단계마다 남은 피처를 전부 한 번씩 시험하므로 대략 n × n번). O(2ⁿ) 은 n이 1 늘 때마다 일이 2배가 된다는 뜻입니다(전수 탐색은 각 피처를 넣거나 빼는 모든 경우의 수 2ⁿ개). 피처 4개면 전수 탐색은 2⁴ = 16에 가까운 조합으로 끝나지만, 10개면 2¹⁰ = 1,024개로 폭증합니다. 그래서 피처가 적을 때만 전수 탐색이 현실적이고, 많아지면 greedy 같은 근사가 불가피해집니다. 이 글에서 후보가 4개라 전수 탐색을 감당할 수 있었던 이유가 바로 이것입니다.
scikit-learn의 SequentialFeatureSelector가 greedy 방식의 대표 구현이라고 합니다. random forest 기반의 Boruta는 피처 중요도 평가 방식이고, ablation의 직접 대응은 아니라고 합니다.
4개 피처면 전수 부분집합은 2⁴ - 1 = 15개 조합입니다. 학습 1회가 약 3분이고 5-seed 평균을 내는 데 15분 정도라면, 15개 조합은 약 4시간이었습니다. 부담스럽지만 불가능하지는 않았습니다.
비용을 감수하기로 한 이유는, 이 결정이 최종 모델을 좌우하는데 greedy의 지역 최적해 위험을 감수할 자신이 없었기 때문입니다.
4. 전수 부분집합 탐색 결과: 직관이 틀렸다
15개 조합을 다 돌린 결과 중 핵심 부분만 보면 이렇습니다.
| 조합 | recall@20 |
|---|---|
| wishs_brands + request_signal | 0.0627 🥇 |
| brand_metrics 단독 | 0.0611 |
| brand_metrics + wishs_brands | 0.0610 |
| brand_metrics + request_signal | 0.0598 |
| brands_feats + request_signal + brand_metrics | 0.0590 |
| wishs_brands + request_signal + brand_metrics | 0.0557 |
| brands_feats + wishs_brands | 0.0552 |
| 4개 전부 결합 | 0.0516 (과적합, 모든 단독보다 나쁨) |
발견이 두 가지 있었는데, 둘 다 직관과 어긋났습니다.
발견 1: 단독 1위가 최종 1위가 아니다
단독 ablation의 1위는 brand_metrics(0.0611)였습니다. greedy forward selection이면 여기서 시작합니다. 그런데 전수 탐색의 1위는 wishs_brands + request_signal(0.0627)이고, 이 조합은 단독 ablation에서 3위와 4위인 두 피처의 조합이었습니다.
이걸 갈라보면 greedy가 절대 못 찾는 경로였습니다. greedy는 brand_metrics에서 시작하므로 wb_rs 조합에는 도달하지 못합니다. brand_metrics에 wishs_brands를 더하면 0.0610(거의 그대로), request_signal을 더하면 0.0598(오히려 감소)이라 greedy는 거기서 멈춥니다.
피처가 서로 상호작용하기 때문에 단독 성능과 조합 성능이 일치하지 않는다고 합니다. brand_metrics는 단독으로 강하지만, 다른 피처와 조합되면 정보 중복(redundancy)이 생깁니다. 반대로 wishs_brands와 request_signal은 단독으로는 중간 정도지만 서로 보완적이었습니다.
발견 2: 많이 합칠수록 좋다는 직관도 틀렸다
단독으로 다 유의미했던 4개 피처를 모두 넣으면 0.0516. 모든 단독보다 나쁩니다. 과적합이었습니다.
원인을 추정하면 두 가지가 동시에 작용한 것으로 보였습니다.
- 정보 중복(redundancy): 4개 피처가 같은 신호를 다른 형태로 반복 표현. 모델이 어디에 가중치를 둬야 할지 헷갈림.
- 입력 차원 폭증: ItemTower의 입력 차원이 커져 학습이 어려워짐. Dropout 0.35가 충분히 흡수하지 못함.
“피처가 많으면 좋다”는 흔한 직관이 있는데, 정확히는 정보적으로 독립적인 피처가 많으면 좋다는 게 자료에서 본 정리였습니다. 중복된 피처는 양만 늘리고 학습을 방해한다고 합니다.
5. wb_rs 선택과 최종 사유
위 결과로 후보가 두 개로 좁혀졌습니다.
| 후보 | recall@20 | 장점 | 단점 |
|---|---|---|---|
| wishs_brands + request_signal (wb_rs) | 0.0627 | 자사 행동 데이터, 단순, 누수 안전성 통제 | 외부 metrics보다 노이즈 위험 |
| brand_metrics 단독 | 0.0611 | 단독 1위 | 외부 데이터 의존, 갱신 주기 불명확 |
두 후보의 head-to-head를 paired permutation으로 비교하면 통계적으로 동률(p=0.216)이라고 나왔습니다. 차이가 우연 범위라는 뜻입니다. 그럼 무엇을 보고 골라야 할까요?
최종 선택은 wb_rs였습니다. 이유 세 가지.
- 자사 행동 데이터: 외부 1688/vvic 지표는 외부 회사의 산정 방식과 갱신 주기에 의존합니다. 우리가 통제할 수 없는 변수. 자사 행동 데이터는 우리가 정의·갱신·검증 가능합니다.
- 누수 안전성: 자사 데이터는
created_at이 명확해 #2에서 만든 시간 기반 split 가드가 깔끔하게 작동합니다. 외부 metrics는updated_at의미가 모호한 경우가 있어 누수 위험이 더 큽니다. - 단순성: 2피처가 4피처보다 단순. 과적합 위험 낮음. 디버깅 쉬움.
성능이 통계적으로 동률이라면, 통제 가능성과 단순성이 결정 기준이라고 봤습니다. 이 우선순위는 도메인 감각이고, 다른 환경에서는 다를 수 있습니다.
6. 이미지(CLIP)를 보류한 결정
이미지는 별도 결정이었습니다. 우리 서비스 상품의 99.97%가 CDN(콘텐츠 전송 네트워크)에 썸네일을 가지고 있고, CLIP(이미지와 텍스트를 같은 공간에 임베딩하는 모델)으로 인코딩하면 시각 정보를 추가할 수 있다고 합니다.
해보지 않은 이유는 비용/효과 추정에서 답이 안 나왔기 때문입니다.
| 항목 | 추정 |
|---|---|
| 인코딩 비용 (MPS, 142K 이미지) | 1.5 ~ 4시간 (해상도·배치에 따라) |
| 추가 디스크 | CLIP 512차원 × 142K × fp16 ≈ 145MB |
| 기대 효과 | 불확실. style/color/material 태그가 이미 시각 속성을 텍스트로 표현 중. CLIP이 그 위에 얼마나 더 줄지 미지수 |
| GPU 부재 (MPS만 사용 가능) | 인코딩 시간이 길고 재인코딩 비용도 큼 |
기대 효과가 불확실한 상황에서 비용이 큰 결정은 보류했습니다. GPU 확보 후 부분 인코딩으로 효과 검증 → 전량 인코딩 순으로 재시도하는 게 합리적이라는 판단이었습니다.
이 결정에서 알게 된 것: “할 수 있다”와 “지금 해야 한다”는 다릅니다. 토이 셋 실습에서는 이 둘이 거의 같았던 것 같습니다. 실무에서는 기대 효과의 불확실성이 측정 비용과 곱해져 의사결정의 무게가 달라진다는 걸 알게 됐습니다.
7. 이 편에서 결정된 것
- 단독 ablation 결과: 5개 후보 중 4개 채택, 1개(products_1688_metrics, p=0.153) 드롭
- 전수 부분집합 탐색: 비-greedy 발견. 단독 3·4위 조합(wb_rs)이 단독 1위(brand_metrics)를 능가
- 4개 전부 결합 = 과적합: recall@20 0.0516, 모든 단독보다 나쁨
- 최종 선택: wb_rs (wishs_brands + request_signal), recall@20=0.0581~0.0627 밴드
- 이미지 보류: 비용 대비 기대 효과의 불확실성으로 GPU 확보 후 재시도
이 편에서 새로 알게 된 것
- greedy의 한계: 피처가 상호작용하면 단독 1위에서 시작한 greedy는 더 좋은 조합에 도달하지 못한다
- 전수 부분집합의 가치: O(2ⁿ) 비용을 감수할 만한 결정(최종 모델 좌우)에는 충분히 가치 있다
- 정보 중복과 과적합: 단독으로 유의미한 피처를 다 합쳐도 중복이 있으면 오히려 나빠진다
- 통계적 동률에서의 선택 기준: 성능이 동률이면 통제 가능성·단순성·누수 안전성이 결정 기준
- “할 수 있다”와 “지금 해야 한다”의 차이: 비용·기대 효과·불확실성의 곱이 의사결정의 무게
여전히 모르는 것
- 6개 후보 중 일부는 결합하지 않은 영역이 있다. 5개 이상 결합의 전수 탐색은 비용 때문에 안 했다.
- wb_rs의 0.0627과 brand_metrics의 0.0611이 통계적으로 동률이라고 했지만, 더 많은 seed(예: 10-seed)로 측정하면 차이가 갈릴 가능성도 있다.
- 이미지를 안 넣었기 때문에, 시각 정보가 얼마나 도움이 됐을지는 지금은 알 수 없다. GPU 확보 후 검증 예정.
- #6에서 다룰 내용: wb_rs로 채택한 피처가, 다른 데이터 가설(계절성)이나 다른 학습 기법(EMA, ensemble)과 결합될 때 어떻게 작동했는지, 그 결과가 직관과 어떻게 어긋났는지를 6편에서 다룹니다.
다음 편 예고
wb_rs 조합이 정말 유의미한 개선인지, ‘유의미’를 어떻게 정의하고 측정하는지를 #5에서 파고듭니다. MPS에서 같은 코드를 두 번 돌리면 결과가 ±0.005 흔들리는 환경에서, “정말 나아졌다”를 어떻게 말할 수 있는가의 이야기입니다.