데이터 전처리를 공부하다 보면 처음에는 결측치 처리, 이상치 제거, 인코딩, 스케일링처럼 데이터의 형태를 바꾸는 작업에 집중하게 된다.
그런데 실제 머신러닝에서는 입력 변수의 개수가 많아질수록 또 다른 문제가 생긴다. 모든 변수가 예측에 도움이 되는 것은 아니고, 비슷한 정보를 반복해서 담고 있는 변수도 있으며, 오히려 불필요한 변수가 모델의 복잡도와 연산량만 늘릴 수도 있다.
이때 사용하는 방법이 특징 선택(Feature Selection) 이다.
전체 특징
↓
예측에 도움이 되는 특징 평가
↓
필요한 특징만 선택
↓
더 단순한 입력 데이터
↓
모델 학습
특징 선택의 핵심은 새로운 변수를 만드는 것이 아니라, 이미 존재하는 변수 중 어떤 것을 남길지 결정하는 것이다.
입력 변수가 많다고 해서 항상 모델이 좋아지는 것은 아니다.
예를 들어 사용자 이탈 예측 모델에 다음과 같은 값이 있다고 해보자.
최근 로그인 경과일
최근 30일 접속 횟수
평균 세션 시간
페이지 조회 수
검색 횟수
장바구니 횟수
구매 횟수
문의 횟수
푸시 알림 수신 여부
앱 테마 설정
프로필 배경 색상
...
이 중 어떤 변수는 이탈 여부와 밀접한 관계가 있을 수 있지만, 어떤 값은 거의 영향을 주지 않을 수도 있다.
모든 변수를 무조건 사용하면 다음과 같은 문제가 생길 수 있다.
따라서 특징 선택은 단순히 컬럼을 줄이는 작업이 아니라 모델에 필요한 정보의 밀도를 높이는 과정으로 이해하는 것이 좋다.
두 개념은 비슷해 보이지만 목적이 다르다.
| 구분 | Feature Engineering | Feature Selection |
|---|---|---|
| 목적 | 새로운 특징 생성 | 기존 특징 중 필요한 특징 선택 |
| 예시 | 구매 수 / 방문 수 → 구매 전환율 | 30개 변수 중 8개만 선택 |
| 결과 | 새로운 컬럼이 추가될 수 있음 | 컬럼 수가 줄어듦 |
| 핵심 질문 | 어떤 새로운 정보를 만들까? | 어떤 정보만 남길까? |
예를 들어 접속 시간에서 요일, 시간대, 주말 여부를 만드는 것은 Feature Engineering이다.
반대로 만들어진 여러 특징 중 실제 이탈 예측에 도움이 되는 값만 남기는 것은 Feature Selection이다.
특징 선택은 크게 Filter, Wrapper, Embedded 방식으로 나눠볼 수 있다.
| 방식 | 판단 기준 | 모델 학습 필요 | 장점 | 단점 |
|---|---|---|---|---|
| Filter | 통계적 관계 | 보통 필요 없음 | 빠르고 단순함 | 변수 간 조합 효과를 놓칠 수 있음 |
| Wrapper | 실제 모델 성능 | 필요 | 모델과 잘 맞는 조합 탐색 가능 | 계산 비용이 큼 |
| Embedded | 모델 학습 과정 | 필요 | 학습과 선택을 함께 수행 | 특정 모델 특성에 영향을 받음 |
세 방법 중 하나가 항상 가장 좋은 것은 아니다.
데이터 크기, 변수 수, 사용할 모델, 계산 시간에 따라 적합한 방법이 달라진다.
Filter 방식은 각각의 특징을 통계적인 기준으로 평가하고 점수나 순위를 이용해 선택한다.
feature_1 ─→ 통계 점수 ─→ 유지
feature_2 ─→ 통계 점수 ─→ 제거
feature_3 ─→ 통계 점수 ─→ 유지
특정 머신러닝 모델을 반복해서 학습할 필요가 없기 때문에 비교적 빠르다.
대표적으로 다음과 같은 방법을 생각할 수 있다.
연속형 변수와 목표값 사이의 관계를 확인하거나, 입력 변수끼리 지나치게 높은 상관관계를 가지는지 살펴볼 수 있다.
corr = df.corr(numeric_only=True)
target_corr = (
corr["churn"]
.abs()
.sort_values(ascending=False)
)
print(target_corr)
상관계수의 절댓값이 큰 변수는 선형적인 관점에서 목표값과 관계가 강하다고 볼 수 있다.
다만 상관계수 하나만으로 변수의 중요성을 확정하면 안 된다.
상관관계는 주로 선형 관계를 보여주기 때문에 비선형 패턴이나 여러 변수의 상호작용은 놓칠 수 있다.
범주형 또는 이산형 데이터와 분류 목표 사이의 관계를 확인할 때 Chi-square(카이제곱) 계열의 방법을 사용할 수 있다.
Scikit-learn에서는 SelectKBest와 chi2를 조합할 수 있다.
from sklearn.feature_selection import SelectKBest, chi2
selector = SelectKBest(
score_func=chi2,
k=5
)
X_selected = selector.fit_transform(X_train, y_train)
chi2를 사용할 때 입력 값은 음수가 없어야 한다.
따라서 표준화 결과처럼 음수가 포함될 수 있는 데이터에 그대로 적용하기보다는 데이터 특성과 변환 방법을 먼저 확인해야 한다.
Filter 방식은 각 특징을 상대적으로 독립적으로 평가한다.
반면 Wrapper 방식은 실제 머신러닝 모델을 반복적으로 학습하면서 어떤 특징 조합이 좋은지 찾는다.
특징 조합 생성
↓
모델 학습
↓
성능 평가
↓
특징 추가 / 제거
↓
다시 학습
모델 입장에서 좋은 특징 조합을 찾을 수 있다는 장점이 있지만, 반복 학습이 필요하기 때문에 변수 수가 많아질수록 비용이 커진다.
대표적인 방법 중 하나가 RFE(Recursive Feature Elimination) 다.
RFE는 지정한 모델을 학습하고, 상대적으로 중요도가 낮은 특징을 제거하는 과정을 반복한다.
예를 들어 10개의 특징 중 4개만 남기고 싶다면 개념적으로 다음과 같이 진행된다.
10개 특징
↓
모델 학습
↓
중요도가 낮은 특징 제거
↓
다시 학습
↓
...
↓
4개 특징
Scikit-learn에서는 RFE를 사용할 수 있다.
아래 예시는 사용자 행동 데이터를 분류하는 상황을 가정한 최소 예제다.
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC
from sklearn.feature_selection import RFE
X, y = make_classification(
n_samples=600,
n_features=12,
n_informative=6,
n_redundant=3,
random_state=1
)
X_train, X_test, y_train, y_test = train_test_split(
X,
y,
random_state=1
)
SVM은 입력 변수의 스케일 차이에 영향을 받을 수 있기 때문에 먼저 표준화를 적용한다.
scaler = StandardScaler()
scaler.fit(X_train)
X_train_scaled = scaler.transform(X_train)
X_test_scaled = scaler.transform(X_test)
여기서 중요한 점은 학습 데이터에만 fit()을 수행한다는 것이다.
X_train
↓
scaler.fit()
X_train, X_test
↓
같은 scaler로 transform()
테스트 데이터까지 포함해 스케일 기준을 계산하면 평가 데이터의 정보가 학습 과정에 들어갈 수 있다.
RFE에서 특징의 중요도를 판단하려면 사용하는 모델이 특징 중요도 또는 계수 정보를 제공할 수 있어야 한다.
Linear SVM을 사용하면 다음처럼 적용할 수 있다.
estimator = SVC(kernel="linear")
rfe = RFE(
estimator=estimator,
n_features_to_select=5
)
rfe.fit(X_train_scaled, y_train)
print(rfe.ranking_)
ranking_에서 값이 1인 특징이 최종 선택된 특징이다.
컬럼명이 있는 DataFrame이라면 다음처럼 확인할 수 있다.
selected_features = X_train.columns[rfe.support_]
print(selected_features)
30개의 입력 특징을 가진 데이터에서 Linear SVM 기반 RFE를 실행하고 5개의 특징을 선택하도록 했을 때 다음 결과를 확인할 수 있었다.
선택 특징 수: 5
Accuracy: 0.95105
AUC: 0.94659
같은 조건에서 선택 특징 수를 10개로 늘렸을 때는 다음과 같았다.
선택 특징 수: 10
Accuracy: 0.93706
AUC: 0.93523
이 결과에서 기억할 점은 특징을 많이 남긴다고 성능이 반드시 높아지는 것은 아니라는 것이다.
특징 선택은 최대한 많은 변수를 보존하는 작업이 아니라, 모델에 필요한 정보를 효율적으로 남기는 작업이다.
참고로 ROC AUC는 일반적으로 클래스 예측값 자체보다 모델의 score나 probability를 이용해 계산하는 경우가 많다. 분류 모델을 비교할 때는 어떤 값을 AUC 입력으로 사용했는지도 함께 확인하는 것이 좋다.
Embedded 방식은 모델 학습 과정에서 얻어지는 중요도나 계수를 이용해 특징을 선택한다.
Wrapper처럼 다양한 조합을 반복해서 탐색하지 않고, 모델 자체가 학습한 정보를 활용한다.
대표적인 예가 다음과 같다.
L1 Regularization
Decision Tree
Random Forest
Gradient Boosting
트리 계열 모델은 각 특징이 분할에 얼마나 기여했는지를 기반으로 중요도를 계산할 수 있다.
Scikit-learn의 SelectFromModel을 사용하면 모델이 계산한 특징 중요도를 기준으로 필요한 변수만 선택할 수 있다.
from sklearn.ensemble import RandomForestClassifier
from sklearn.feature_selection import SelectFromModel
rf = RandomForestClassifier(
n_estimators=100,
random_state=1
)
selector = SelectFromModel(rf)
selector.fit(X_train, y_train)
선택 여부는 get_support()로 확인할 수 있다.
support = selector.get_support()
selected_features = X_train.columns[support]
print(selected_features)
직접 실행한 결과에서는 30개의 입력 특징 중 9개 특징이 선택되는 것을 확인했다.
30 features
↓ Random Forest + SelectFromModel
9 selected features
RFE가 모델을 반복적으로 학습하면서 특징을 제거했다면, Embedded 방식은 모델이 한 번의 학습 과정에서 계산한 중요도 정보를 특징 선택에 활용한다는 차이가 있다.
특징 선택을 공부하면서 흥미로웠던 방법이 Boruta다.
Boruta는 Random Forest 계열 모델을 이용하며, 원래 특징을 무작위로 섞어 만든 Shadow Feature와 실제 특징의 중요도를 비교한다.
개념을 단순화하면 다음과 같다.
실제 특징
+
무작위 Shadow 특징
↓
Random Forest 학습
↓
특징 중요도 비교
↓
Shadow보다 의미 있는 실제 특징 선택
단순히 상위 몇 개를 자르는 방식과 달리, 무작위 특징보다 충분히 의미가 있는지 비교한다는 점이 핵심이다.
처음 import를 실행했을 때 다음 오류가 발생했다.
ModuleNotFoundError: No module named 'boruta'
현재 Python 환경에 패키지가 설치되지 않았다는 의미다.
현재 노트북 커널에 설치하려면 다음처럼 실행할 수 있다.
%pip install Boruta
실행 결과에서는 Boruta-0.4.3이 정상적으로 설치되는 것을 확인했다.
설치 후 다시 import한다.
from boruta import BorutaPy
노트북 환경에 따라 설치 직후에도 import가 되지 않는다면 현재 Notebook 커널과 패키지를 설치한 Python 환경이 같은지 먼저 확인하는 것이 좋다.
Windows CMD, PowerShell, macOS, Linux에서는 현재 사용 중인 Python에 명시적으로 설치하는 방식이 안전하다.
python -m pip install Boruta
Conda 환경을 사용한다면 먼저 원하는 환경을 활성화한 뒤 설치한다.
conda activate data-env
python -m pip install Boruta
Random Forest 모델을 만든 뒤 BorutaPy에 전달한다.
from sklearn.ensemble import RandomForestClassifier
from boruta import BorutaPy
rf = RandomForestClassifier(
random_state=1
)
boruta = BorutaPy(
estimator=rf,
n_estimators="auto",
random_state=1
)
boruta.fit(
X_train.to_numpy(),
y_train.to_numpy().ravel()
)
특징별 순위는 ranking_으로 확인할 수 있다.
print(boruta.ranking_)
일반적으로 1로 표시된 특징을 선택된 특징으로 확인할 수 있다.
selected = X_train.columns[
boruta.ranking_ == 1
]
print(selected)
30개의 특징을 대상으로 실행한 결과에서는 23개 특징이 선택되고 7개 특징이 제외되는 것을 확인했다.
전체 특징: 30개
Boruta 선택:
23개
Boruta 제외:
7개
앞서 Random Forest의 SelectFromModel에서는 9개만 선택됐지만, Boruta에서는 23개가 선택됐다.
두 방법의 결과가 다른 것은 이상한 일이 아니다.
특징을 중요하다고 판단하는 기준 자체가 다르기 때문이다.
SelectFromModel은 모델 중요도의 임계값을 이용해 특징을 줄이는 방식이고, Boruta는 Shadow Feature와 반복적으로 비교하면서 관련성이 있는 특징을 찾는 방식이다.
따라서 숫자만 보고 어느 방법이 무조건 더 낫다고 판단하기보다는 선택 목적과 이후 모델 성능을 함께 확인해야 한다.
실무에서는 특징 선택 알고리즘 이름보다 현재 문제에서 무엇을 원하는지를 먼저 정하는 것이 좋다.
| 상황 | 고려할 방법 |
|---|---|
| 변수가 매우 많고 빠르게 1차 정리하고 싶음 | Filter |
| 특정 모델에서 좋은 변수 조합을 찾고 싶음 | Wrapper / RFE |
| 트리 모델의 중요도를 바로 활용하고 싶음 | Embedded |
| 관련 있는 특징을 비교적 폭넓게 찾고 싶음 | Boruta |
| 계산 비용이 중요한 상황 | Filter 또는 Embedded |
| 특징 간 조합까지 적극적으로 탐색하고 싶음 | Wrapper |
특징 선택 결과는 사용하는 모델에 따라 달라질 수 있다.
예를 들어 SVM-RFE로 선택된 특징이 Random Forest에서도 동일하게 중요한 특징이라는 보장은 없다.
즉,
특징 중요도
≠
데이터 자체에 고정된 절대적인 값
이라고 이해하는 편이 좋다.
머신러닝 전처리에서 가장 중요하게 기억할 부분 중 하나가 Data Leakage다.
특징 선택도 학습 과정의 일부이므로 전체 데이터에서 먼저 특징을 고른 뒤 Train/Test를 나누면 테스트 데이터의 정보가 선택 과정에 반영될 수 있다.
잘못된 흐름은 다음과 같다.
전체 데이터
↓
Feature Selection
↓
Train / Test Split
일반적으로는 다음과 같은 흐름이 필요하다.
전체 데이터
↓
Train / Test Split
↓
Train 데이터로 Feature Selection 학습
↓
같은 선택 기준을 Test에 적용
Scikit-learn에서는 이런 실수를 줄이기 위해 Pipeline을 활용할 수 있다.
Linear SVM과 RFE를 예로 들면 다음처럼 구성할 수 있다.
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.feature_selection import RFE
from sklearn.svm import SVC
pipeline = Pipeline([
(
"scaler",
StandardScaler()
),
(
"feature_selection",
RFE(
estimator=SVC(kernel="linear"),
n_features_to_select=5
)
),
(
"classifier",
SVC(kernel="linear")
)
])
pipeline.fit(X_train, y_train)
Pipeline으로 묶어두면 학습 데이터에서 계산해야 하는 전처리 과정을 하나의 흐름으로 관리하기 쉬워진다.
교차 검증과 함께 사용할 때도 데이터 누수를 방지하는 데 도움이 된다.
특징 선택에서 정확도나 AUC는 중요한 기준이지만 그것만 볼 필요는 없다.
실제 서비스에서는 다음과 같은 요소도 함께 고려하게 된다.
모델 성능
+
특징 개수
+
데이터 수집 비용
+
추론 속도
+
특징 계산 비용
+
해석 가능성
예를 들어 정확도 차이가 거의 없는데 30개 변수가 필요한 모델과 5개 변수만 필요한 모델이 있다면, 후자가 운영 측면에서 더 유리할 수도 있다.
특히 실시간 추천이나 API 기반 추론에서는 한 개의 특징을 얻기 위해 추가 데이터베이스 조회나 외부 API 요청이 필요한 경우도 있다.
따라서 특징 하나를 선택한다는 것은 단순히 DataFrame의 컬럼 하나를 선택하는 문제가 아닐 수 있다.
웹 서비스의 사용자 이탈 예측을 만든다고 가정해보자.
원본 데이터에 100개의 행동 특징이 있다고 해서 100개를 모두 모델에 전달할 필요는 없다.
100개 서비스 지표
↓
Filter로 빠른 후보 정리
↓
RFE 또는 Embedded 방식으로 재평가
↓
실제 모델 성능 검증
↓
운영 비용까지 고려
↓
최종 특징 결정
이 과정에서 중요한 것은 선택 알고리즘의 결과를 정답처럼 받아들이지 않는 것이다.
RFE가 선택한 변수, Random Forest가 중요하다고 판단한 변수, Boruta가 남긴 변수는 서로 다를 수 있다.
그 차이를 확인하면서 어떤 특징이 문제를 실제로 설명하는지 분석하는 과정이 필요하다.
SelectFromModel에서는 30개 중 9개의 특징이 선택됐다.특징 선택을 처음 보면 단순히 컬럼을 삭제하는 작업처럼 보이지만, 실제로는 모델이 어떤 정보를 보고 판단하게 할 것인지 결정하는 과정에 가깝다.
Filter는 빠르게 후보를 좁히는 데 유용하고, Wrapper는 특정 모델과 잘 맞는 조합을 찾는 데 도움이 되며, Embedded 방식은 모델 학습과 특징 선택을 자연스럽게 연결한다.
Boruta처럼 랜덤 특징과 비교하는 방법까지 살펴보면 특징의 중요도를 판단하는 방법이 하나만 있는 것이 아니라는 점도 알 수 있다.
결국 중요한 것은 특정 알고리즘을 무조건 사용하는 것이 아니라,
왜 이 특징을 남겼는가?
왜 이 특징을 제거했는가?
선택 결과가 실제 모델 성능과 운영 환경에서도 유효한가?
를 설명할 수 있는 것이다.