본문으로 건너뛰기

067 Custom Learner - sklearn 모델 래핑

키워드: sklearn 래핑, 모델 통합, 커스터마이징

개요

sklearn에는 FLAML이 기본 지원하지 않는 다양한 모델이 있습니다. 이 글에서는 여러 sklearn 모델을 Custom Learner로 래핑하여 FLAML에 통합하는 방법을 알아봅니다.

실습 환경

  • Python 버전: 3.11 권장
  • 필요 패키지: flaml[automl], scikit-learn
pip install flaml[automl] scikit-learn pandas numpy

기본 데이터 준비

import numpy as np
import pandas as pd
from sklearn.datasets import make_classification, make_regression
from sklearn.model_selection import train_test_split
from flaml import AutoML
from flaml.automl.model import SKLearnEstimator

# 067 분류 데이터
X_clf, y_clf = make_classification(n_samples=2000, n_features=20,
n_informative=10, n_classes=2,
random_state=42)
X_clf_train, X_clf_test, y_clf_train, y_clf_test = train_test_split(
X_clf, y_clf, test_size=0.2, random_state=42
)

# 067 회귀 데이터
X_reg, y_reg = make_regression(n_samples=2000, n_features=20,
noise=10, random_state=42)
X_reg_train, X_reg_test, y_reg_train, y_reg_test = train_test_split(
X_reg, y_reg, test_size=0.2, random_state=42
)

print(f"분류 데이터: {X_clf_train.shape}")
print(f"회귀 데이터: {X_reg_train.shape}")

서포트 벡터 머신 (SVM)

SVC (분류)

from sklearn.svm import SVC

class SVCLearner(SKLearnEstimator):
"""SVM 분류기 Custom Learner"""

@classmethod
def search_space(cls, data_size, task):
return {
'C': {
'domain': np.logspace(-3, 3, 100),
'init_value': 1.0,
},
'kernel': {
'domain': ['rbf', 'linear', 'poly'],
'init_value': 'rbf',
},
'gamma': {
'domain': np.logspace(-4, 1, 50),
'init_value': 0.1,
},
'degree': {
'domain': [2, 3, 4, 5],
'init_value': 3,
},
}

def config2params(self, config):
params = config.copy()
# poly 커널이 아니면 degree 제거
if params.get('kernel') != 'poly':
params.pop('degree', None)
# linear 커널이면 gamma 제거
if params.get('kernel') == 'linear':
params.pop('gamma', None)
return params

def __init__(self, task='classification', **config):
super().__init__(task, **config)
self.estimator_class = SVC
# 확률 예측 활성화
self.params['probability'] = True

# 067 SVC 테스트
automl = AutoML()
automl.add_learner('svc', SVCLearner)

automl.fit(
X_clf_train, y_clf_train,
task="classification",
time_budget=60,
estimator_list=['svc'],
metric='accuracy',
verbose=1
)

print(f"\nSVC 최적 설정: {automl.best_config}")
print(f"정확도: {1 - automl.best_loss:.4f}")

SVR (회귀)

from sklearn.svm import SVR

class SVRLearner(SKLearnEstimator):
"""SVM 회귀 Custom Learner"""

@classmethod
def search_space(cls, data_size, task):
return {
'C': {
'domain': np.logspace(-2, 3, 50),
'init_value': 1.0,
},
'kernel': {
'domain': ['rbf', 'linear', 'poly'],
'init_value': 'rbf',
},
'gamma': {
'domain': np.logspace(-4, 1, 50),
'init_value': 'scale',
},
'epsilon': {
'domain': np.logspace(-3, 0, 30),
'init_value': 0.1,
},
}

def __init__(self, task='regression', **config):
super().__init__(task, **config)
self.estimator_class = SVR

# 067 SVR 테스트
automl_svr = AutoML()
automl_svr.add_learner('svr', SVRLearner)

automl_svr.fit(
X_reg_train, y_reg_train,
task="regression",
time_budget=60,
estimator_list=['svr'],
metric='mae',
verbose=1
)

print(f"\nSVR 최적 설정: {automl_svr.best_config}")

K-최근접 이웃 (KNN)

from sklearn.neighbors import KNeighborsClassifier, KNeighborsRegressor

class KNNClassifierLearner(SKLearnEstimator):
"""KNN 분류기 Custom Learner"""

@classmethod
def search_space(cls, data_size, task):
return {
'n_neighbors': {
'domain': list(range(1, 31)),
'init_value': 5,
'low_cost_init_value': 1,
},
'weights': {
'domain': ['uniform', 'distance'],
'init_value': 'uniform',
},
'metric': {
'domain': ['euclidean', 'manhattan', 'minkowski'],
'init_value': 'euclidean',
},
'p': {
'domain': [1, 2, 3],
'init_value': 2,
},
}

def config2params(self, config):
params = config.copy()
# minkowski가 아니면 p 제거
if params.get('metric') != 'minkowski':
params.pop('p', None)
return params

def __init__(self, task='classification', **config):
super().__init__(task, **config)
self.estimator_class = KNeighborsClassifier


class KNNRegressorLearner(SKLearnEstimator):
"""KNN 회귀 Custom Learner"""

@classmethod
def search_space(cls, data_size, task):
return {
'n_neighbors': {
'domain': list(range(1, 31)),
'init_value': 5,
},
'weights': {
'domain': ['uniform', 'distance'],
'init_value': 'distance',
},
'metric': {
'domain': ['euclidean', 'manhattan'],
'init_value': 'euclidean',
},
}

def __init__(self, task='regression', **config):
super().__init__(task, **config)
self.estimator_class = KNeighborsRegressor

# 067 KNN 테스트
automl_knn = AutoML()
automl_knn.add_learner('knn', KNNClassifierLearner)

automl_knn.fit(
X_clf_train, y_clf_train,
task="classification",
time_budget=30,
estimator_list=['knn'],
verbose=1
)

print(f"\nKNN 최적 설정: {automl_knn.best_config}")

나이브 베이즈

from sklearn.naive_bayes import GaussianNB, MultinomialNB

class GaussianNBLearner(SKLearnEstimator):
"""가우시안 나이브 베이즈 Custom Learner"""

@classmethod
def search_space(cls, data_size, task):
return {
'var_smoothing': {
'domain': np.logspace(-12, -6, 50),
'init_value': 1e-9,
},
}

def __init__(self, task='classification', **config):
super().__init__(task, **config)
self.estimator_class = GaussianNB

# 067 나이브 베이즈 테스트
automl_nb = AutoML()
automl_nb.add_learner('gaussian_nb', GaussianNBLearner)

automl_nb.fit(
X_clf_train, y_clf_train,
task="classification",
time_budget=20,
estimator_list=['gaussian_nb'],
verbose=1
)

print(f"\nGaussianNB 최적 설정: {automl_nb.best_config}")

결정 트리

from sklearn.tree import DecisionTreeClassifier, DecisionTreeRegressor

class DecisionTreeClassifierLearner(SKLearnEstimator):
"""결정 트리 분류기 Custom Learner"""

@classmethod
def search_space(cls, data_size, task):
return {
'max_depth': {
'domain': list(range(2, 30)) + [None],
'init_value': 10,
'low_cost_init_value': 3,
},
'min_samples_split': {
'domain': list(range(2, 50)),
'init_value': 2,
},
'min_samples_leaf': {
'domain': list(range(1, 20)),
'init_value': 1,
},
'criterion': {
'domain': ['gini', 'entropy'],
'init_value': 'gini',
},
'splitter': {
'domain': ['best', 'random'],
'init_value': 'best',
},
}

def __init__(self, task='classification', **config):
super().__init__(task, **config)
self.estimator_class = DecisionTreeClassifier


class DecisionTreeRegressorLearner(SKLearnEstimator):
"""결정 트리 회귀 Custom Learner"""

@classmethod
def search_space(cls, data_size, task):
return {
'max_depth': {
'domain': list(range(2, 30)) + [None],
'init_value': 10,
'low_cost_init_value': 3,
},
'min_samples_split': {
'domain': list(range(2, 50)),
'init_value': 2,
},
'min_samples_leaf': {
'domain': list(range(1, 20)),
'init_value': 1,
},
'criterion': {
'domain': ['squared_error', 'friedman_mse', 'absolute_error'],
'init_value': 'squared_error',
},
}

def __init__(self, task='regression', **config):
super().__init__(task, **config)
self.estimator_class = DecisionTreeRegressor

# 067 결정 트리 테스트
automl_dt = AutoML()
automl_dt.add_learner('dt_clf', DecisionTreeClassifierLearner)

automl_dt.fit(
X_clf_train, y_clf_train,
task="classification",
time_budget=30,
estimator_list=['dt_clf'],
verbose=1
)

print(f"\nDecisionTree 최적 설정: {automl_dt.best_config}")

앙상블 모델

AdaBoost

from sklearn.ensemble import AdaBoostClassifier, AdaBoostRegressor

class AdaBoostClassifierLearner(SKLearnEstimator):
"""AdaBoost 분류기 Custom Learner"""

@classmethod
def search_space(cls, data_size, task):
return {
'n_estimators': {
'domain': list(range(10, 300, 10)),
'init_value': 50,
'low_cost_init_value': 10,
},
'learning_rate': {
'domain': np.logspace(-3, 1, 50),
'init_value': 1.0,
},
'algorithm': {
'domain': ['SAMME', 'SAMME.R'],
'init_value': 'SAMME.R',
},
}

def __init__(self, task='classification', **config):
super().__init__(task, **config)
self.estimator_class = AdaBoostClassifier

# 067 AdaBoost 테스트
automl_ada = AutoML()
automl_ada.add_learner('adaboost', AdaBoostClassifierLearner)

automl_ada.fit(
X_clf_train, y_clf_train,
task="classification",
time_budget=60,
estimator_list=['adaboost'],
verbose=1
)

print(f"\nAdaBoost 최적 설정: {automl_ada.best_config}")

Bagging

from sklearn.ensemble import BaggingClassifier, BaggingRegressor

class BaggingClassifierLearner(SKLearnEstimator):
"""Bagging 분류기 Custom Learner"""

@classmethod
def search_space(cls, data_size, task):
return {
'n_estimators': {
'domain': list(range(10, 200, 10)),
'init_value': 50,
'low_cost_init_value': 10,
},
'max_samples': {
'domain': np.linspace(0.5, 1.0, 10),
'init_value': 1.0,
},
'max_features': {
'domain': np.linspace(0.5, 1.0, 10),
'init_value': 1.0,
},
'bootstrap': {
'domain': [True, False],
'init_value': True,
},
}

def __init__(self, task='classification', **config):
super().__init__(task, **config)
self.estimator_class = BaggingClassifier

print("Bagging Learner 정의 완료")

Gradient Boosting

from sklearn.ensemble import GradientBoostingClassifier, GradientBoostingRegressor

class GBClassifierLearner(SKLearnEstimator):
"""Gradient Boosting 분류기 Custom Learner"""

@classmethod
def search_space(cls, data_size, task):
return {
'n_estimators': {
'domain': list(range(50, 500, 25)),
'init_value': 100,
'low_cost_init_value': 50,
},
'learning_rate': {
'domain': np.logspace(-3, 0, 50),
'init_value': 0.1,
},
'max_depth': {
'domain': list(range(3, 15)),
'init_value': 5,
'low_cost_init_value': 3,
},
'min_samples_split': {
'domain': list(range(2, 20)),
'init_value': 2,
},
'subsample': {
'domain': np.linspace(0.6, 1.0, 10),
'init_value': 1.0,
},
}

def __init__(self, task='classification', **config):
super().__init__(task, **config)
self.estimator_class = GradientBoostingClassifier


class GBRegressorLearner(SKLearnEstimator):
"""Gradient Boosting 회귀 Custom Learner"""

@classmethod
def search_space(cls, data_size, task):
return {
'n_estimators': {
'domain': list(range(50, 500, 25)),
'init_value': 100,
'low_cost_init_value': 50,
},
'learning_rate': {
'domain': np.logspace(-3, 0, 50),
'init_value': 0.1,
},
'max_depth': {
'domain': list(range(3, 15)),
'init_value': 5,
},
'loss': {
'domain': ['squared_error', 'absolute_error', 'huber'],
'init_value': 'squared_error',
},
}

def __init__(self, task='regression', **config):
super().__init__(task, **config)
self.estimator_class = GradientBoostingRegressor

# 067 GB 테스트
automl_gb = AutoML()
automl_gb.add_learner('gb_clf', GBClassifierLearner)

automl_gb.fit(
X_clf_train, y_clf_train,
task="classification",
time_budget=60,
estimator_list=['gb_clf'],
verbose=1
)

print(f"\nGradientBoosting 최적 설정: {automl_gb.best_config}")

여러 Custom Learner 동시 사용

# 067 여러 Custom Learner 등록
automl_multi = AutoML()
automl_multi.add_learner('svc', SVCLearner)
automl_multi.add_learner('knn', KNNClassifierLearner)
automl_multi.add_learner('dt', DecisionTreeClassifierLearner)
automl_multi.add_learner('gb', GBClassifierLearner)

# 067 모든 커스텀 + 기본 모델 함께 탐색
automl_multi.fit(
X_clf_train, y_clf_train,
task="classification",
time_budget=120,
estimator_list=['lgbm', 'rf', 'svc', 'knn', 'dt', 'gb'],
metric='accuracy',
verbose=1
)

print(f"\n최적 모델: {automl_multi.best_estimator}")
print(f"최적 설정: {automl_multi.best_config}")
print(f"정확도: {1 - automl_multi.best_loss:.4f}")

# 067 테스트 평가
from sklearn.metrics import accuracy_score, classification_report
y_pred = automl_multi.predict(X_clf_test)
print(f"\n테스트 정확도: {accuracy_score(y_clf_test, y_pred):.4f}")

sklearn 모델 래핑 템플릿

def create_sklearn_learner(estimator_class, search_space_dict, task='classification'):
"""sklearn 모델을 Custom Learner로 래핑하는 팩토리 함수"""

class SklearnLearner(SKLearnEstimator):
@classmethod
def search_space(cls, data_size, task_type):
return search_space_dict

def __init__(self, task=task, **config):
super().__init__(task, **config)
self.estimator_class = estimator_class

return SklearnLearner

# 067 팩토리 함수 사용 예
from sklearn.linear_model import LogisticRegression

lr_space = {
'C': {
'domain': np.logspace(-4, 4, 100),
'init_value': 1.0,
},
'penalty': {
'domain': ['l1', 'l2'],
'init_value': 'l2',
},
'solver': {
'domain': ['liblinear', 'saga'],
'init_value': 'liblinear',
},
}

LogisticRegressionLearner = create_sklearn_learner(
LogisticRegression, lr_space, task='classification'
)

# 067 사용
automl_lr = AutoML()
automl_lr.add_learner('logreg', LogisticRegressionLearner)

automl_lr.fit(
X_clf_train, y_clf_train,
task="classification",
time_budget=30,
estimator_list=['logreg'],
verbose=1
)

print(f"\nLogisticRegression 최적 설정: {automl_lr.best_config}")

정리

모델클래스주요 하이퍼파라미터
SVMSVC/SVRC, kernel, gamma
KNNKNeighborsn_neighbors, weights
나이브 베이즈GaussianNBvar_smoothing
결정 트리DecisionTreemax_depth, criterion
AdaBoostAdaBoostn_estimators, learning_rate
Gradient BoostingGradientBoostingn_estimators, learning_rate, max_depth
  • sklearn 모델을 SKLearnEstimator로 래핑
  • search_space()에서 하이퍼파라미터 정의
  • config2params()로 조건부 파라미터 처리
  • add_learner()로 등록 후 estimator_list에 추가

다음 글 예고

다음 글에서는 Custom Learner - XGBoost 커스터마이징에 대해 알아보겠습니다. XGBoost의 세부 파라미터를 FLAML에서 튜닝하는 방법을 다룹니다.


FLAML AutoML 마스터 시리즈 #067