Elastic Net과 유전 알고리즘으로 고차원 피처 선택 설계하기
고차원 데이터에서 Elastic Net 기반 임베디드 선택과 유전 알고리즘 Wrapper 탐색을 비교하고, 검증·운영 설계와 구현 예제를 정리한다.
2026-08-14 · 최초 발행 2024-04-29
고차원 피처 선택은 학습 과정 안에서 결정된다
피처 수가 많아질수록 모델 성능만이 아니라 일반화 가능성, 추론 비용, 운영 복잡도까지 함께 흔들린다. 피처 선택은 이 문제를 줄이는 절차이며, 학습 중 계수를 제어하는 Embedded 방식과 후보 부분집합을 직접 탐색하는 Wrapper 방식은 서로 다른 장단점을 가진다.
Elastic Net과 Lasso는 모델의 목적함수에 규제를 포함해 학습과 선택을 동시에 수행한다. L1 규제는 불필요한 계수를 0으로 유도하고, L1과 L2를 함께 쓰는 Elastic Net은 상관된 피처 그룹을 함께 선택하는 성질을 제공한다. l1_ratio와 규제 강도인 C 또는 α를 조정해 희소성과 안정성의 균형을 맞춘다.
유전 알고리즘(Genetic Algorithm, GA)은 피처 부분집합을 하나의 해로 취급한다. AUC, F1, RMSE 같은 교차검증 점수를 적합도로 삼고, 선택·교차·돌연변이를 거치며 세대별 후보를 진화시킨다. 여기에 피처 수 패널티를 넣으면 성능과 모델 복잡도를 함께 최적화할 수 있다.
규제 기반 선택과 조합 탐색의 경계
Lasso는 강한 희소성을 제공하며, 다중공선성이 있는 데이터에서는 Elastic Net이 더 안정적인 선택을 보인다. l1_ratio를 0.2~0.8 범위에서 두고 α 또는 C를 그리드 탐색이나 베이지안 탐색으로 조정하면 선택 결과와 일반화 성능 사이의 균형을 찾을 수 있다.
Embedded 방법에서는 규제항이 목적함수에 포함된다. 해석성과 일반화 성능을 높이면서 계산 효율을 유지하기 쉽다. 해석적 또는 경사 기반 최적화로 빠르게 수렴하고, 정규화 경로·조기중단·Warm-start를 활용해 계산량을 줄일 수 있다.
GA Wrapper에서는 다음과 같은 적합도 구성이 가능하다.
적합도 = CV 성능 − λ·|S|
여기서 λ는 피처 수를 억제하는 하이퍼파라미터다. 초기 개체군의 크기, 토너먼트 선택, Uniform 또는 One-point 교차, 비트플립 돌연변이 설정이 탐색 결과를 좌우한다. 엘리티즘과 다양성 유지는 조기수렴을 피하는 데 사용한다.
운영 비용도 다르다. Embedded 방식은 O(p)~O(p log p) 수준으로 확장성이 높아 대규모 특성이나 실시간·배치 환경에 맞는다. 반면 GA는 반복적인 교차검증 학습이 필요하므로 비용이 크다. 병렬화와 캐싱으로 완화할 수 있지만, 매우 고차원인 경우 계산 비용이 급격히 증가한다.
| 항목 | Lasso/Elastic Net (Embedded) | GA Wrapper |
|---|---|---|
| 성능(일반화) | 규제 기반 안정 성능, 과적합 억제 용이 | 조합 최적화로 높은 성능 잠재, 튜닝 품질 의존 |
| 확장성 | 고차원에 강함, 빠른 학습 | 고비용(다중 CV 반복), 병렬화 필요 |
| 일관성(재현성) | 규제 경로 기반, 변동성 낮음 | 탐색적 특성, 초기조건·랜덤성 영향 큼 |
| 안정성(노이즈 내성) | L2 혼합 시 안정성↑ | 목적함수·패널티 설계에 좌우 |
| 운영 편의 | 파이프라인화 용이, 해석성 우수 | 운영 복잡, 계산 자원 요구 |
데이터 성격에 맞춰 선택 전략을 배치한다
신용평가나 리스크 모델처럼 다중공선성이 존재하는 환경에서는 규제 기반 선택이 해석성과 규제 준수를 함께 다루는 데 적합하다. 이 경우 Elastic Net 기반 로지스틱 회귀를 사용할 수 있다.
유전자·오믹스 데이터처럼 p≫n인 문제에서는 희소성 기반 변수 선택이 필요하다. 안정성 선택(Stability Selection)을 병행하면 재현성을 강화할 수 있다. 텍스트나 광고 CTR의 희소 고차원 벡터에서는 L1 또는 Elastic Net으로 차원을 축소하고 비용을 줄인 뒤, GA로 소수 핵심 피처의 조합을 탐색할 수 있다. IoT·센서 예지보전에서는 Elastic Net으로 상관 피처 그룹을 먼저 선택하고 GA로 상호작용 조합을 미세 조정하는 방식도 가능하다.
실무 입력은 학습 데이터 (X, y), 전처리 규칙, AUC·F1·RMSE 같은 메트릭, 최대 피처 수 등의 제약으로 구성된다. 데이터 분리와 스케일링을 포함한 파이프라인을 먼저 만들고, Embedded 방식에서는 교차검증으로 하이퍼파라미터를 탐색해 비영(非零) 계수 피처를 확정한다. GA에서는 이진 염색체로 부분집합을 표현하고, CV 성능과 패널티를 결합한 적합도를 수렴·조기중단 조건 아래에서 최적화한다. 성능 추정은 Nested CV로 수행해 선택 편향을 줄인다.
최종 산출물에는 선택 피처 목록, 최종 모델, 교차검증 성능, 계수 또는 중요도, 재현성 시드와 설정 값을 포함한다.
클래스 불균형에서는 Stratified CV를 적용하고 평가 메트릭을 AUC-PR 또는 F1로 바꾸며 class_weight를 적용할 수 있다. 스케일링 누락은 파이프라인 내부의 StandardScaler로 막아야 데이터 누수를 피할 수 있다. GA의 다양성이 무너지면 돌연변이율을 높이고, 엘리티즘·토너먼트 파라미터와 초기 개체군을 조정한다. 계산이 과도해질 때는 평가 캐싱, n_jobs>1 병렬 처리, 최대 세대와 조기중단 임계값을 사용한다.
Elastic Net을 교차검증 파이프라인에 넣기
전제조건은 Python 3.10+, scikit-learn 1.4+, numpy/pandas, joblib, deap 1.4.1(Wrapper 예제)이며, 예제 데이터는 sklearn.datasets.load_breast_cancer다.
# Python 3.10+, scikit-learn 1.4+
from sklearn.datasets import load_breast_cancer
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegressionCV
from sklearn.pipeline import Pipeline
from sklearn.model_selection import StratifiedKFold
import numpy as np
X, y = load_breast_cancer(return_X_y=True)
# Elastic Net penalty requires solver='saga'
pipe = Pipeline(steps=[
("scaler", StandardScaler(with_mean=True)),
("clf", LogisticRegressionCV(
Cs=10, # inverse of regularization strength grid
penalty="elasticnet",
solver="saga",
l1_ratios=[0.2, 0.5, 0.8],
scoring="roc_auc",
cv=StratifiedKFold(n_splits=5, shuffle=True, random_state=42),
max_iter=5000,
n_jobs=-1,
refit=True,
random_state=42
))
])
pipe.fit(X, y)
clf = pipe.named_steps["clf"]
coef = clf.coef_.ravel()
selected_idx = np.where(coef != 0)[0]
print(f"선택 피처 수: {len(selected_idx)} / {X.shape[1]}")
print("선택 피처 인덱스:", selected_idx.tolist())
print("CV 최고 성능(AUC):", clf.scores_[1].mean(axis=0).max())
분류 문제에서 Elastic Net을 사용할 때는 saga 솔버가 필요하다. 다중클래스 문제는 multinomial 설정을 검토한다. 회귀 문제에서는 ElasticNetCV 또는 LassoCV로 같은 패턴을 구현할 수 있다.
GA로 부분집합 적합도를 최적화하기
# Python 3.10+, scikit-learn 1.4+, deap 1.4.1
import random
import numpy as np
from deap import base, creator, tools
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
# 데이터
X, y = load_breast_cancer(return_X_y=True)
n_features = X.shape[1]
# 모델 파이프라인(고정)
base_model = Pipeline([
("scaler", StandardScaler()),
("clf", LogisticRegression(
solver="saga", penalty="l2", max_iter=5000, class_weight="balanced", random_state=42))
])
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
# 적합도: AUC - λ * (선택 피처 비율)
LAMBDA = 0.01
def evaluate(individual):
if sum(individual) == 0:
return -1.0, # 빈 집합 패널티
cols = [i for i, bit in enumerate(individual) if bit == 1]
X_sub = X[:, cols]
scores = cross_val_score(base_model, X_sub, y, cv=cv, scoring="roc_auc", n_jobs=-1)
fitness = scores.mean() - LAMBDA * (sum(individual) / n_features)
return fitness,
# DEAP 설정
random.seed(42)
creator.create("FitnessMax", base.Fitness, weights=(1.0,))
creator.create("Individual", list, fitness=creator.FitnessMax)
toolbox = base.Toolbox()
toolbox.register("attr_bool", lambda: 1 if random.random() < 0.15 else 0) # 초기 희소도 15%
toolbox.register("individual", tools.initRepeat, creator.Individual, toolbox.attr_bool, n_features)
toolbox.register("population", tools.initRepeat, list, toolbox.individual)
toolbox.register("evaluate", evaluate)
toolbox.register("select", tools.selTournament, tournsize=3)
toolbox.register("mate", tools.cxUniform, indpb=0.5)
toolbox.register("mutate", tools.mutFlipBit, indpb=1.0/n_features)
# 진화 루프
POP, NGEN, CXPB, MUTPB = 40, 30, 0.7, 0.2
pop = toolbox.population(n=POP)
hof = tools.HallOfFame(1)
for gen in range(NGEN):
offspring = toolbox.select(pop, len(pop))
offspring = list(map(toolbox.clone, offspring))
# 교차
for c1, c2 in zip(offspring[::2], offspring[1::2]):
if random.random() < CXPB:
toolbox.mate(c1, c2)
del c1.fitness.values, c2.fitness.values
# 돌연변이
for mut in offspring:
if random.random() < MUTPB:
toolbox.mutate(mut)
del mut.fitness.values
# 평가
invalid = [ind for ind in offspring if not ind.fitness.valid]
for ind in invalid:
ind.fitness.values = toolbox.evaluate(ind)
pop[:] = offspring
hof.update(pop)
best = hof[0]
best_cols = [i for i, b in enumerate(best) if b == 1]
print(f"GA 선택 피처 수: {len(best_cols)} / {n_features}")
print("선택 인덱스:", best_cols)
print("최고 적합도(AUC-패널티):", evaluate(best)[0])
deap의 map에 joblib 또는 멀티프로세싱을 등록하면 평가를 병렬화할 수 있다. 세대별 최고 적합도 개선이 정체될 때는 조기중단 기준을 적용하고, 이미 평가한 동일 부분집합은 캐싱해 중복 계산을 줄인다.
선택 결과가 바꾸는 성능과 비용
패널티와 탐색을 적절히 설계하면 AUC/F1이 13pt 개선될 수 있으며, 효과는 데이터와 문제 난이도에 따라 다르다. 피처 수를 5095% 축소하고 학습·추론 시간을 2~10배 단축한 사례도 있다.
고차원·상관 피처 환경에서는 Elastic Net 기반 Embedded 방법을 먼저 적용해 빠른 수렴과 안정적인 성능을 확보할 수 있다. 상호작용이나 비선형 조합에서 추가 이득이 기대되면, GA Wrapper를 병렬화·조기중단·패널티 설계와 함께 제한된 자원 안에서 사용한다. 어느 방식을 택하더라도 Nested CV, 누수 방지 파이프라인, 재현성 시드 고정이 실험 신뢰성의 기준이 된다.