서론: scikit-learn 1.8이 가져온 변화
데이터 과학 워크플로우에서 데이터 전처리와 모델 학습은 떼려야 뗄 수 없는 관계입니다. pandas로 데이터를 정제하고, Polars로 대규모 데이터를 빠르게 처리한 다음... 그다음은요? 바로 scikit-learn으로 머신러닝 모델을 구축하는 단계죠.
2025년 12월에 릴리스된 scikit-learn 1.8은 단순한 버그 수정 업데이트가 아닙니다. 솔직히 말하면, 이번 릴리스는 꽤 오래 기다려온 기능들이 한꺼번에 쏟아진 느낌이에요. GPU 가속을 위한 Array API 지원 확대, 프리 스레드 CPython 3.14 호환, 그리고 온도 스케일링 기반 확률 보정까지 — 현대 머신러닝 워크플로우에 꼭 필요한 기능들이 대거 추가되었습니다.
이 글에서는 scikit-learn 1.8의 핵심 신기능을 살펴보고, 실무에서 바로 활용할 수 있는 현대적 ML 파이프라인 구축법을 단계별로 안내하겠습니다. 데이터 전처리부터 모델 학습, GPU 가속, 하이퍼파라미터 튜닝까지 — 실전 예제 코드와 함께 하나씩 짚어볼게요.
설치 및 환경 설정
scikit-learn 1.8 설치하기
먼저 최신 버전을 설치합시다. pip 한 줄이면 끝입니다.
# scikit-learn 1.8 설치
pip install -U scikit-learn
# GPU 가속을 위한 PyTorch 설치 (선택)
pip install torch
# CuPy 설치 (NVIDIA GPU 사용 시)
pip install cupy-cuda12x
# 버전 확인
python -c "import sklearn; print(sklearn.__version__)"
# 1.8.0
필수 라이브러리 임포트
이 글 전체에서 사용할 기본 임포트를 먼저 정리해 두겠습니다. 복사해서 노트북 상단에 붙여넣기 하면 편합니다.
import numpy as np
import pandas as pd
import sklearn
from sklearn.pipeline import Pipeline, make_pipeline
from sklearn.compose import ColumnTransformer, make_column_selector
from sklearn.preprocessing import StandardScaler, OneHotEncoder, OrdinalEncoder
from sklearn.impute import SimpleImputer
from sklearn.model_selection import (
train_test_split, cross_validate,
GridSearchCV, RandomizedSearchCV
)
from sklearn.metrics import (
accuracy_score, classification_report,
confusion_matrix, roc_auc_score
)
print(f"scikit-learn 버전: {sklearn.__version__}")
print(f"NumPy 버전: {np.__version__}")
print(f"pandas 버전: {pd.__version__}")
scikit-learn 1.8 핵심 신기능
1. Array API를 통한 GPU 가속
scikit-learn 1.8의 가장 주목할 만한 변화는 Array API 표준의 본격적인 도입입니다. 이걸 통해 PyTorch 텐서나 CuPy 배열을 직접 scikit-learn 추정기(estimator)에 전달할 수 있고, GPU에서 연산이 수행됩니다.
개인적으로 이 기능이 제일 반가웠는데요, 기존에는 GPU를 쓰려면 별도의 라이브러리로 갈아타야 했거든요.
1.8에서 Array API를 새롭게 지원하는 추정기와 함수들은 다음과 같습니다:
preprocessing.StandardScaler— 표준화 스케일링preprocessing.PolynomialFeatures— 다항 특성 생성linear_model.RidgeCV/RidgeClassifierCV— 릿지 교차 검증mixture.GaussianMixture— 가우시안 혼합 모델calibration.CalibratedClassifierCV— 확률 보정naive_bayes.GaussianNB— 나이브 베이즈metrics.confusion_matrix,metrics.roc_curve등 — 평가 지표
그럼 실제로 GPU를 활용하는 파이프라인 예제를 살펴보겠습니다.
import os
# SciPy의 Array API 지원을 활성화 (scikit-learn 임포트 전에 설정)
os.environ["SCIPY_ARRAY_API"] = "1"
import torch
import numpy as np
import sklearn
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import FunctionTransformer, StandardScaler
from sklearn.linear_model import RidgeClassifierCV
from sklearn.calibration import CalibratedClassifierCV
from sklearn.model_selection import cross_validate
# GPU 사용 가능 여부 확인
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"사용 디바이스: {device}")
# CPU에서 전처리 후 GPU로 데이터를 이동하는 파이프라인
def to_gpu_tensor(X):
"""NumPy 배열을 GPU PyTorch 텐서로 변환"""
if hasattr(X, 'to_numpy'):
X = X.to_numpy()
return torch.tensor(X.astype(np.float32), device=device)
gpu_pipeline = make_pipeline(
StandardScaler(), # CPU에서 표준화
FunctionTransformer(to_gpu_tensor), # GPU로 이동
CalibratedClassifierCV(
RidgeClassifierCV(alphas=np.logspace(-3, 3, 10)),
method="temperature" # 1.8 신기능: 온도 스케일링
),
)
# Array API 디스패치를 활성화하여 교차 검증 수행
with sklearn.config_context(array_api_dispatch=True):
cv_results = cross_validate(gpu_pipeline, X_train, y_train, cv=5)
print(f"평균 정확도: {cv_results['test_score'].mean():.4f}")
공식 벤치마크에 따르면, Colab GPU 환경에서 CPU 단일 코어 대비 약 10배의 속도 향상을 달성할 수 있다고 합니다. 대규모 데이터셋을 다루는 실무 환경에서는 이 차이가 정말 결정적이에요.
다만 주의할 점이 하나 있습니다. 트리 기반 모델(결정 트리, 랜덤 포레스트, 그래디언트 부스팅 등)은 알고리즘 특성상 Array API를 통한 GPU 가속의 혜택을 받기 어렵습니다. GPU 가속은 주로 선형 모델, 스케일러, 가우시안 혼합 모델 같은 배열 기반 연산에 효과적이라는 점 기억해 두세요.
2. 프리 스레드 CPython 3.14 지원
scikit-learn 1.8부터 프리 스레드(free-threaded) CPython을 공식 지원합니다. 모든 플랫폼에서 프리 스레드 휠(wheel)을 제공하며, 특히 Python 3.14 사용을 권장하고 있어요.
프리 스레드 Python이 왜 중요할까요? 기존 CPython에는 GIL(Global Interpreter Lock)이 있어서 진정한 멀티스레딩이 불가능했습니다. 병렬 처리를 위해 n_jobs 파라미터를 사용하면 내부적으로 프로세스 기반 병렬화가 일어나는데, 이건 프로세스 간 통신 오버헤드가 꽤 큽니다.
프리 스레드 Python에서는 GIL이 없으므로 스레드 기반 병렬화가 가능해지고, 그 오버헤드가 사라집니다. 간단히 말해서, 같은 코드가 더 빨라진다는 거죠.
import joblib
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score
# 프리 스레드 Python 3.14에서 스레드 백엔드 사용
with joblib.parallel_config(backend="threading"):
rf = RandomForestClassifier(n_estimators=100, n_jobs=-1)
scores = cross_val_score(rf, X_train, y_train, cv=5, n_jobs=-1)
print(f"교차 검증 정확도: {scores.mean():.4f} (+/- {scores.std():.4f})")
아직 모든 scikit-learn 추정기가 프리 스레드 환경에서 완벽히 최적화된 것은 아닙니다. 하지만 점진적으로 개선되고 있고, 커뮤니티의 피드백을 적극적으로 받고 있으니 앞으로가 기대됩니다.
3. 온도 스케일링(Temperature Scaling) 확률 보정
CalibratedClassifierCV에 온도 스케일링(method="temperature") 옵션이 새롭게 추가되었습니다. 기존의 "sigmoid"(Platt 스케일링)이나 "isotonic"(등위 회귀) 방법과 비교했을 때 뭐가 좋을까요?
- 단 하나의 자유 파라미터만 사용하므로 과적합 위험이 낮음
- 다중 클래스 문제에서 특히 효과적 (One-vs-Rest 분할이 필요 없음)
- Array API와 호환되어 GPU 가속 가능
특히 다중 클래스에서 One-vs-Rest 없이 바로 보정할 수 있다는 게 실무에서 상당히 편리합니다.
from sklearn.calibration import CalibratedClassifierCV
from sklearn.svm import LinearSVC
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
# 다중 클래스 분류 데이터 생성
X, y = make_classification(
n_samples=5000, n_features=20,
n_informative=15, n_classes=5,
n_clusters_per_class=1,
random_state=42
)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42
)
# 온도 스케일링으로 확률 보정
calibrated_svc = CalibratedClassifierCV(
LinearSVC(max_iter=10000),
method="temperature", # scikit-learn 1.8 신기능
cv=5
)
calibrated_svc.fit(X_train, y_train)
# 보정된 확률 확인
probas = calibrated_svc.predict_proba(X_test)
print(f"확률 합계 (1.0이어야 함): {probas[0].sum():.4f}")
print(f"예측 확률 분포 (상위 3개 샘플):\n{probas[:3]}")
4. 결정 트리 회귀기 성능 대폭 향상
DecisionTreeRegressor에서 criterion="absolute_error"를 사용할 때 시간 복잡도가 O(n²)에서 O(n log n)으로 개선되었습니다. 이게 체감상 얼마나 다르냐면, 수백만 개의 데이터 포인트도 이제 현실적인 시간 안에 학습할 수 있게 되었습니다.
from sklearn.tree import DecisionTreeRegressor
import time
# 대규모 데이터 생성
np.random.seed(42)
X_large = np.random.randn(500_000, 10)
y_large = np.random.randn(500_000)
# absolute_error 기준으로 학습 (1.8에서 대폭 빨라짐)
start = time.time()
dt = DecisionTreeRegressor(criterion="absolute_error", max_depth=10)
dt.fit(X_large, y_large)
elapsed = time.time() - start
print(f"학습 시간: {elapsed:.2f}초 (50만 행, 10개 특성)")
5. ElasticNetCV 학습 속도 개선
ElasticNetCV의 학습 시간이 갭 세이프 스크리닝 규칙(gap safe screening rules) 덕분에 크게 개선되었습니다. 좌표 하강 솔버가 특성의 계수를 조기에 0으로 설정할 수 있게 되어, L1 패널티가 강할수록 불필요한 특성을 더 빨리 제외합니다. 고차원 데이터를 자주 다루는 분이라면 체감이 확 될 거예요.
from sklearn.linear_model import ElasticNetCV
# 고차원 데이터에서 ElasticNetCV 학습
np.random.seed(42)
X_high_dim = np.random.randn(1000, 500)
y_reg = X_high_dim[:, :5].sum(axis=1) + np.random.randn(1000) * 0.1
start = time.time()
elastic_cv = ElasticNetCV(
l1_ratio=[0.1, 0.5, 0.7, 0.9, 0.95, 1.0],
cv=5,
max_iter=10000
)
elastic_cv.fit(X_high_dim, y_reg)
elapsed = time.time() - start
print(f"학습 시간: {elapsed:.2f}초")
print(f"최적 l1_ratio: {elastic_cv.l1_ratio_}")
print(f"0이 아닌 계수 수: {np.sum(elastic_cv.coef_ != 0)}")
현대적 ML 파이프라인 구축: 기초부터 실전까지
자, 이제 본론으로 들어가 볼까요? scikit-learn의 진정한 강점은 Pipeline과 ColumnTransformer를 통한 체계적인 워크플로우 구축에 있습니다. 실전 프로젝트를 통해 단계별로 파이프라인을 만들어 보겠습니다.
예제 데이터: 직원 이직 예측
HR 데이터를 기반으로 직원 이직을 예측하는 시나리오입니다. 실무에서 흔히 접할 수 있는 형태의 데이터를 생성해서 사용할게요.
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
# 실전 시나리오: 직원 이직 예측 데이터 생성
np.random.seed(42)
n_samples = 5000
data = pd.DataFrame({
'나이': np.random.randint(22, 60, n_samples),
'근속연수': np.random.randint(0, 30, n_samples),
'월급여': np.random.normal(400, 100, n_samples).astype(int) * 10000,
'부서': np.random.choice(
['영업', '개발', '마케팅', '인사', '재무'], n_samples
),
'직급': np.random.choice(
['사원', '대리', '과장', '차장', '부장'], n_samples
),
'성과등급': np.random.choice(['A', 'B', 'C', 'D'], n_samples,
p=[0.1, 0.3, 0.4, 0.2]),
'초과근무시간': np.random.exponential(10, n_samples).round(1),
'만족도점수': np.random.uniform(1, 10, n_samples).round(1),
'출장빈도': np.random.choice(
['없음', '가끔', '자주'], n_samples, p=[0.3, 0.5, 0.2]
),
})
# 결측값 인위적 추가 (실전에서는 항상 결측값이 있으므로)
mask = np.random.random(n_samples) < 0.05
data.loc[mask, '월급여'] = np.nan
mask2 = np.random.random(n_samples) < 0.08
data.loc[mask2, '만족도점수'] = np.nan
# 이직 여부 (타겟 변수)
이직확률 = (
(data['만족도점수'].fillna(5) < 4).astype(float) * 0.3 +
(data['초과근무시간'] > 20).astype(float) * 0.25 +
(data['월급여'].fillna(4000000) < 3000000).astype(float) * 0.2 +
np.random.random(n_samples) * 0.25
)
data['이직여부'] = (이직확률 > 0.5).astype(int)
print(f"데이터 크기: {data.shape}")
print(f"이직률: {data['이직여부'].mean():.2%}")
print(f"\n결측값 현황:")
print(data.isnull().sum()[data.isnull().sum() > 0])
print(f"\n데이터 미리보기:")
print(data.head())
단계 1: 특성 유형별 전처리 파이프라인 정의
실전 데이터에는 수치형, 명목형(순서 없는 범주), 순서형(순서 있는 범주) 등 다양한 타입이 섞여 있죠. ColumnTransformer를 사용하면 각 타입에 맞는 전처리를 한 번에 적용할 수 있습니다. 이 부분이 처음엔 좀 복잡해 보이는데, 한번 익숙해지면 정말 편해요.
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import (
StandardScaler, OneHotEncoder, OrdinalEncoder
)
from sklearn.impute import SimpleImputer
# 특성 분류
수치형_특성 = ['나이', '근속연수', '월급여', '초과근무시간', '만족도점수']
명목형_특성 = ['부서', '출장빈도']
순서형_특성 = ['직급', '성과등급']
# 수치형 전처리: 결측값 중앙값 대체 → 표준화
수치형_파이프라인 = Pipeline(steps=[
('imputer', SimpleImputer(strategy='median')),
('scaler', StandardScaler())
])
# 명목형 전처리: 원-핫 인코딩
명목형_파이프라인 = Pipeline(steps=[
('imputer', SimpleImputer(strategy='most_frequent')),
('encoder', OneHotEncoder(
handle_unknown='ignore', # 학습 시 없던 범주 처리
sparse_output=False # 밀집 배열 반환
))
])
# 순서형 전처리: 순서 인코딩
순서형_파이프라인 = Pipeline(steps=[
('imputer', SimpleImputer(strategy='most_frequent')),
('encoder', OrdinalEncoder(categories=[
['사원', '대리', '과장', '차장', '부장'], # 직급 순서
['D', 'C', 'B', 'A'] # 성과등급 순서
]))
])
# ColumnTransformer로 통합
전처리기 = ColumnTransformer(
transformers=[
('수치형', 수치형_파이프라인, 수치형_특성),
('명목형', 명목형_파이프라인, 명목형_특성),
('순서형', 순서형_파이프라인, 순서형_특성),
],
remainder='drop', # 선택되지 않은 열 제거
verbose_feature_names_out=True # 변환 후 특성 이름 추적
)
print("전처리기 구조:")
print(전처리기)
단계 2: 전체 파이프라인 조립
전처리기를 분류 모델과 결합하여 하나의 완전한 파이프라인을 만듭니다. 이렇게 하면 데이터 누수(data leakage)를 원천적으로 방지할 수 있어요. (데이터 누수 때문에 모델 성능이 실전에서 급락하는 경험, 다들 한 번쯤 있으시죠?)
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.linear_model import LogisticRegression
# 특성과 타겟 분리
X = data.drop('이직여부', axis=1)
y = data['이직여부']
# 학습/테스트 분할
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
# 로지스틱 회귀 파이프라인
lr_파이프라인 = Pipeline(steps=[
('전처리', 전처리기),
('분류기', LogisticRegression(max_iter=1000, random_state=42))
])
# 랜덤 포레스트 파이프라인
rf_파이프라인 = Pipeline(steps=[
('전처리', 전처리기),
('분류기', RandomForestClassifier(
n_estimators=200, random_state=42, n_jobs=-1
))
])
# 그래디언트 부스팅 파이프라인
gb_파이프라인 = Pipeline(steps=[
('전처리', 전처리기),
('분류기', GradientBoostingClassifier(
n_estimators=200, random_state=42
))
])
# 세 모델 비교 학습 및 평가
모델목록 = {
'로지스틱 회귀': lr_파이프라인,
'랜덤 포레스트': rf_파이프라인,
'그래디언트 부스팅': gb_파이프라인
}
print("=" * 60)
print("모델 비교 결과")
print("=" * 60)
for 이름, 파이프라인 in 모델목록.items():
scores = cross_validate(
파이프라인, X_train, y_train,
cv=5, scoring=['accuracy', 'roc_auc'],
return_train_score=True
)
print(f"\n{이름}:")
print(f" 학습 정확도: {scores['train_accuracy'].mean():.4f}")
print(f" 검증 정확도: {scores['test_accuracy'].mean():.4f} "
f"(+/- {scores['test_accuracy'].std():.4f})")
print(f" 검증 ROC-AUC: {scores['test_roc_auc'].mean():.4f} "
f"(+/- {scores['test_roc_auc'].std():.4f})")
단계 3: 하이퍼파라미터 튜닝
파이프라인의 큰 장점 중 하나는 전처리 단계의 파라미터와 모델의 하이퍼파라미터를 동시에 튜닝할 수 있다는 것입니다. 파라미터 이름은 단계이름__파라미터이름 형식으로 지정하면 됩니다. 처음에 더블 언더스코어가 헷갈릴 수 있는데, 금방 손에 익어요.
from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import randint, uniform
# 랜덤 포레스트 파이프라인으로 하이퍼파라미터 튜닝
param_distributions = {
# 전처리 파라미터: 수치형 결측값 대체 전략
'전처리__수치형__imputer__strategy': ['mean', 'median'],
# 모델 하이퍼파라미터
'분류기__n_estimators': randint(100, 500),
'분류기__max_depth': [5, 10, 15, 20, None],
'분류기__min_samples_split': randint(2, 20),
'분류기__min_samples_leaf': randint(1, 10),
'분류기__max_features': ['sqrt', 'log2', None],
}
search = RandomizedSearchCV(
rf_파이프라인,
param_distributions=param_distributions,
n_iter=50, # 50개 조합 탐색
cv=5,
scoring='roc_auc',
n_jobs=-1,
random_state=42,
verbose=1
)
search.fit(X_train, y_train)
print(f"\n최적 ROC-AUC: {search.best_score_:.4f}")
print(f"\n최적 파라미터:")
for param, value in search.best_params_.items():
print(f" {param}: {value}")
단계 4: 최종 모델 평가
튜닝이 끝났으면 테스트 세트로 최종 성능을 확인할 차례입니다.
from sklearn.metrics import (
classification_report, confusion_matrix,
roc_auc_score
)
# 최적 모델로 테스트 세트 평가
best_model = search.best_estimator_
y_pred = best_model.predict(X_test)
y_proba = best_model.predict_proba(X_test)[:, 1]
print("분류 보고서:")
print(classification_report(
y_test, y_pred,
target_names=['잔류', '이직']
))
print(f"테스트 ROC-AUC: {roc_auc_score(y_test, y_proba):.4f}")
print(f"\n혼동 행렬:")
cm = confusion_matrix(y_test, y_pred)
print(f" 실제 잔류 → 잔류 예측: {cm[0][0]}")
print(f" 실제 잔류 → 이직 예측: {cm[0][1]} (1종 오류)")
print(f" 실제 이직 → 잔류 예측: {cm[1][0]} (2종 오류)")
print(f" 실제 이직 → 이직 예측: {cm[1][1]}")
고급 파이프라인 기법
make_column_selector로 자동 열 선택
열 이름을 하드코딩하는 건 솔직히 좀 귀찮고 실수하기도 쉽습니다. 데이터 타입 기반으로 자동으로 열을 선택하는 방법이 있어요. 스키마가 자주 변경되는 환경에서 특히 유용합니다.
from sklearn.compose import make_column_selector as selector
# 데이터 타입 기반 자동 선택
자동_전처리기 = ColumnTransformer(
transformers=[
('수치형', Pipeline([
('imputer', SimpleImputer(strategy='median')),
('scaler', StandardScaler())
]), selector(dtype_include='number')),
('범주형', Pipeline([
('imputer', SimpleImputer(strategy='most_frequent')),
('encoder', OneHotEncoder(handle_unknown='ignore'))
]), selector(dtype_include='object')),
],
remainder='drop'
)
# 자동 전처리기 활용 파이프라인
auto_pipeline = Pipeline([
('전처리', 자동_전처리기),
('분류기', GradientBoostingClassifier(random_state=42))
])
# 데이터 타입만 맞으면 어떤 데이터프레임에도 적용 가능
auto_pipeline.fit(X_train, y_train)
print(f"자동 파이프라인 테스트 정확도: "
f"{auto_pipeline.score(X_test, y_test):.4f}")
FeatureUnion으로 특성 결합
서로 다른 특성 추출 기법의 결과를 하나로 합치고 싶을 때 FeatureUnion을 사용합니다. 원본 특성과 PCA 결과, 선택된 특성을 모두 결합하는 식이죠.
from sklearn.pipeline import FeatureUnion
from sklearn.decomposition import PCA
from sklearn.feature_selection import SelectKBest, f_classif
# 원본 스케일링 특성 + PCA 특성 + 선택된 특성을 결합
특성_결합기 = FeatureUnion(transformer_list=[
('스케일링', StandardScaler()),
('PCA', PCA(n_components=5)),
('특성선택', SelectKBest(f_classif, k=3))
])
# 전처리 후 특성 결합 파이프라인
고급_파이프라인 = Pipeline([
('전처리', 전처리기),
('특성결합', 특성_결합기),
('분류기', LogisticRegression(max_iter=1000))
])
scores = cross_validate(
고급_파이프라인, X_train, y_train,
cv=5, scoring='roc_auc'
)
print(f"FeatureUnion 파이프라인 ROC-AUC: "
f"{scores['test_score'].mean():.4f}")
커스텀 변환기 만들기
실무에서는 scikit-learn에 내장되지 않은 전처리가 필요할 때가 정말 많습니다. 이상치 처리라든가 도메인 특화 변환이라든가요. BaseEstimator와 TransformerMixin을 상속하면 파이프라인에 끼워 넣을 수 있는 자신만의 변환기를 만들 수 있습니다.
from sklearn.base import BaseEstimator, TransformerMixin
class OutlierClipper(BaseEstimator, TransformerMixin):
"""IQR 기반 이상치를 상한/하한으로 클리핑하는 변환기"""
def __init__(self, factor=1.5):
self.factor = factor
def fit(self, X, y=None):
X = np.asarray(X)
Q1 = np.percentile(X, 25, axis=0)
Q3 = np.percentile(X, 75, axis=0)
IQR = Q3 - Q1
self.lower_ = Q1 - self.factor * IQR
self.upper_ = Q3 + self.factor * IQR
return self
def transform(self, X):
X = np.asarray(X).copy()
X = np.clip(X, self.lower_, self.upper_)
return X
# 커스텀 변환기를 파이프라인에 통합
수치형_고급_파이프라인 = Pipeline([
('imputer', SimpleImputer(strategy='median')),
('outlier_clip', OutlierClipper(factor=1.5)),
('scaler', StandardScaler())
])
# ColumnTransformer에 커스텀 변환기 포함
고급_전처리기 = ColumnTransformer(
transformers=[
('수치형', 수치형_고급_파이프라인, 수치형_특성),
('명목형', 명목형_파이프라인, 명목형_특성),
('순서형', 순서형_파이프라인, 순서형_특성),
]
)
커스텀_파이프라인 = Pipeline([
('전처리', 고급_전처리기),
('분류기', RandomForestClassifier(n_estimators=200, random_state=42))
])
커스텀_파이프라인.fit(X_train, y_train)
print(f"커스텀 변환기 파이프라인 정확도: "
f"{커스텀_파이프라인.score(X_test, y_test):.4f}")
파이프라인 저장과 배포
joblib를 이용한 직렬화
학습이 완료된 파이프라인은 통째로 직렬화하여 저장할 수 있습니다. 전처리 로직과 모델이 하나의 객체에 포함되어 있으니 배포가 놀라울 정도로 간단해요. 더 이상 전처리 코드를 따로 관리할 필요가 없습니다.
import joblib
from pathlib import Path
# 모델 저장
model_path = Path("models")
model_path.mkdir(exist_ok=True)
joblib.dump(search.best_estimator_, model_path / "attrition_model_v1.joblib")
print("모델 저장 완료!")
# 모델 로드 및 예측
loaded_model = joblib.load(model_path / "attrition_model_v1.joblib")
# 새로운 직원 데이터로 예측
신규직원 = pd.DataFrame({
'나이': [28],
'근속연수': [2],
'월급여': [3500000],
'부서': ['개발'],
'직급': ['대리'],
'성과등급': ['B'],
'초과근무시간': [25.0],
'만족도점수': [3.5],
'출장빈도': ['자주']
})
예측결과 = loaded_model.predict(신규직원)
이직확률 = loaded_model.predict_proba(신규직원)[0][1]
print(f"\n예측 결과: {'이직 위험' if 예측결과[0] == 1 else '잔류 예상'}")
print(f"이직 확률: {이직확률:.1%}")
ONNX 형식으로 프로덕션 배포
보다 범용적인 배포를 위해 파이프라인을 ONNX 형식으로 변환할 수도 있습니다. ONNX는 프레임워크 독립적인 모델 교환 포맷으로, Java나 C++ 환경에서도 추론이 가능합니다. 마이크로서비스 아키텍처에서 Python 서버를 쓰지 않아도 되니 배포 옵션이 넓어지죠.
# ONNX 변환 (skl2onnx 패키지 필요)
# pip install skl2onnx onnxruntime
from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import FloatTensorType
# 전처리된 데이터의 특성 수 기준으로 입력 타입 정의
n_features = len(전처리기.get_feature_names_out())
initial_types = [("input", FloatTensorType([None, n_features]))]
# 참고: 전체 파이프라인 변환은 전처리기 타입에 따라
# 추가 설정이 필요할 수 있습니다
# onnx_model = convert_sklearn(model, initial_types=initial_types)
# with open("model.onnx", "wb") as f:
# f.write(onnx_model.SerializeToString())
실전 팁과 모범 사례
1. 데이터 누수 방지의 원칙
파이프라인을 사용하는 가장 큰 이유 중 하나가 바로 데이터 누수 방지입니다. 이건 아무리 강조해도 지나치지 않아요.
# 잘못된 방식: 데이터 누수 발생
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X) # 전체 데이터로 fit!
X_train_s, X_test_s = train_test_split(X_scaled)
# 올바른 방식: 파이프라인 사용
pipeline = Pipeline([
('scaler', StandardScaler()),
('model', LogisticRegression())
])
# cross_validate 내부에서 학습 폴드로만 fit
scores = cross_validate(pipeline, X, y, cv=5)
2. 파이프라인 시각화
scikit-learn 1.8에서는 HTML 표현이 더욱 개선되어, 파이프라인의 구조를 시각적으로 확인할 수 있습니다. 하이퍼파라미터에 공식 문서 링크가 포함되고, 도구 설명(tooltip)으로 파라미터 설명이 표시되는데 이게 꽤 유용합니다.
# Jupyter 노트북에서 파이프라인 시각화
from sklearn import set_config
set_config(display='diagram')
# 파이프라인 객체를 셀에서 실행하면 다이어그램 표시
커스텀_파이프라인
3. 변환된 특성 이름 추적
전처리 후 특성 이름을 추적하는 것은 모델 해석에 필수적입니다. 원-핫 인코딩 후에 특성 이름이 뒤죽박죽되면 나중에 해석이 곤란해지거든요.
# 전처리기를 먼저 fit
전처리기.fit(X_train)
# 변환 후 특성 이름 확인
feature_names = 전처리기.get_feature_names_out()
print(f"전처리 후 총 특성 수: {len(feature_names)}")
print(f"\n특성 목록 (처음 10개):")
for name in feature_names[:10]:
print(f" - {name}")
4. 특성 중요도 분석
모델 학습이 끝나면 어떤 특성이 예측에 가장 큰 영향을 미쳤는지 확인해 봐야겠죠.
# 최적 모델에서 특성 중요도 추출
if hasattr(best_model.named_steps['분류기'], 'feature_importances_'):
importances = best_model.named_steps['분류기'].feature_importances_
feature_names = best_model.named_steps['전처리'].get_feature_names_out()
# 중요도 상위 10개 특성
indices = np.argsort(importances)[::-1][:10]
print("특성 중요도 상위 10개:")
print("-" * 45)
for i, idx in enumerate(indices, 1):
print(f" {i:2d}. {feature_names[idx]:30s} {importances[idx]:.4f}")
5. 메모리 효율적인 대규모 데이터 처리
데이터가 정말 크다면 메모리 사용을 최적화해야 합니다. 희소 행렬을 적극 활용하고, SGD 기반 모델처럼 미니배치 학습이 가능한 알고리즘을 선택하는 게 좋습니다.
from sklearn.preprocessing import MaxAbsScaler
from sklearn.compose import make_column_selector as selector
# 희소 행렬 친화적인 파이프라인
대규모_전처리기 = ColumnTransformer(
transformers=[
('수치형', Pipeline([
('imputer', SimpleImputer(strategy='median')),
('scaler', MaxAbsScaler()) # 희소 행렬 지원
]), selector(dtype_include='number')),
('범주형', Pipeline([
('imputer', SimpleImputer(strategy='most_frequent')),
('encoder', OneHotEncoder(
handle_unknown='ignore',
sparse_output=True # 희소 행렬 출력 유지
))
]), selector(dtype_include='object')),
],
sparse_threshold=0.3 # 희소 비율 30% 이상이면 희소 행렬 유지
)
# SGD 기반 분류기 — 대규모 데이터에 적합
from sklearn.linear_model import SGDClassifier
대규모_파이프라인 = Pipeline([
('전처리', 대규모_전처리기),
('분류기', SGDClassifier(loss='log_loss', random_state=42))
])
print("대규모 데이터용 파이프라인 준비 완료!")
scikit-learn 1.8로 마이그레이션하기
주요 변경 사항 체크리스트
기존 프로젝트를 scikit-learn 1.8로 업그레이드할 때 확인해야 할 사항들을 정리했습니다. 업그레이드 전에 꼭 한번 훑어보세요.
# 1. 폐기 예정(deprecated) API 확인
import warnings
warnings.filterwarnings('error', category=FutureWarning)
# 2. Array API 활용 시 환경 변수 설정 확인
import os
os.environ["SCIPY_ARRAY_API"] = "1" # SciPy 임포트 전 설정 필수
# 3. sparse_output 파라미터명 변경 확인
# 이전: OneHotEncoder(sparse=True)
# 현재: OneHotEncoder(sparse_output=True)
# 4. set_output API 활용 (pandas DataFrame 출력)
from sklearn import set_config
set_config(transform_output="pandas")
# 이제 변환 결과가 DataFrame으로 반환됨
전처리기.fit(X_train)
결과 = 전처리기.transform(X_train[:3])
print(type(결과)) # pandas.core.frame.DataFrame
print(결과.columns) # 변환된 특성 이름이 열 이름으로 설정됨
버전 호환성 요약
- Python: 3.10 이상 권장, 3.14 프리 스레드 지원
- NumPy: 1.21 이상
- SciPy: 1.9 이상 (Array API 사용 시 최신 버전 권장)
- pandas: 2.0 이상 (3.0과 완벽 호환)
- PyTorch: GPU 가속 사용 시 2.0 이상
결론: 현대적 ML 워크플로우의 핵심
scikit-learn 1.8은 "전통적인 ML 라이브러리"라는 인식을 깨는 의미 있는 릴리스입니다. Array API를 통한 GPU 가속, 프리 스레드 Python 지원, 온도 스케일링 보정 등 현대적인 ML 요구사항에 적극적으로 대응하고 있어요.
이 글에서 다룬 핵심 내용을 정리하면:
- GPU 가속: Array API를 통해 PyTorch/CuPy 텐서를 직접 활용, 선형 모델 학습에서 최대 10배 속도 향상
- 프리 스레드 지원: Python 3.14에서 GIL 없는 진정한 멀티스레딩으로 병렬 처리 효율 개선
- Pipeline + ColumnTransformer: 체계적인 전처리 워크플로우로 데이터 누수 방지 및 코드 재사용성 확보
- 하이퍼파라미터 동시 튜닝: 전처리 파라미터와 모델 하이퍼파라미터를 하나의 탐색 공간에서 최적화
- 커스텀 변환기: 도메인 특화 전처리 로직을 표준 인터페이스로 통합
- 간편한 배포: joblib 직렬화로 전처리 + 모델을 하나의 파일로 저장
pandas 3.0이나 Polars로 데이터를 정제한 다음, scikit-learn 1.8의 파이프라인으로 모델을 구축하는 것이 2026년 현재 가장 효율적인 Python ML 워크플로우라고 생각합니다. 특히 pandas 3.0의 Copy-on-Write와 scikit-learn 1.8의 파이프라인을 결합하면, 메모리 효율성과 코드 안정성 모두를 잡을 수 있죠.
다음 단계로는 이 파이프라인을 MLflow나 DVC 같은 MLOps 도구와 연동하거나, SHAP를 활용한 모델 설명 가능성 분석을 시도해 보시길 추천합니다. 한번 파이프라인의 맛을 보면, 다시는 코드를 흩뿌려 놓고 싶지 않아질 거예요.