인지야공

인지야공/인공 지능 공부 치트 시트 정리/14번째 글

scikit-learn 치트시트를 다시 쓴다 — 잡음만 있는 데이터에서 85.8%

실행: python 딥러닝/cheatsheet/CH_sklearn.py (검증 환경: scikit-learn 1.6.1) 이 글의 수치와 에러 메시지는 전부 그 스크립트를 돌려 얻은 것이다.


scikit-learn 치트시트는 대부분 0.2x 시절에 쓰였다. 그 사이에 이름이 바뀌고 사라진 것이 꽤 되는데, 그런 줄들은 에러가 나므로 금방 고친다.

이 글의 절반은 13편(PyTorch)과 같은 이유로 쓴다 — 에러 없이 점수를 부풀리는 자리들. scikit-learn 쪽의 그것은 한 단어로 데이터 누수다.


1. [A] 지금 치면 에러가 나는 줄들

치트시트에 흔히 나오는 줄1.6에서고치면
from sklearn.datasets import load_bostonImportError — 제거됨fetch_california_housing()
from sklearn.metrics import plot_confusion_matrixImportError — 제거됨ConfusionMatrixDisplay.from_estimator(m, X, y)
OneHotEncoder(sparse=True)TypeErrorsparse_output=True
RandomForestClassifier(max_features='auto')InvalidParameterError'sqrt' (기존 기본값) 또는 생략
LinearRegression(normalize=True)TypeErrorStandardScaler 를 파이프라인에
GridSearchCV(..., iid=True)TypeError그냥 뺀다
AdaBoostClassifier(base_estimator=...)TypeErrorestimator=...
model.n_features_AttributeErrormodel.n_features_in_

load_boston 이 제거된 이유는 기술적인 것이 아니다. 그 데이터셋에 인종 구성을 집값 예측에 쓰는 변수(B)가 들어 있어서, 윤리 문제로 1.2에서 경고, 1.4에서 삭제됐다. 예제에서 무심코 쓰던 것이 문제가 된 사례라 기억해 둘 만하다.

max_features='auto' 는 조용한 함정이었다. RandomForestClassifier 에서는 'sqrt' 였는데 RandomForestRegressor 에서는 1.0(전부)이라 같은 글자가 다른 뜻이었다. 그래서 아예 없앴다.


2. [B] 에러 없이 점수를 부풀리는 자리들

B1. 잡음만 있는 데이터에서 85.8%를 만들기

가장 극적인 예부터.

N, P = 120, 6000
X = rng.standard_normal((N, P))     # 라벨과 아무 관계도 없는 순수 잡음
y = rng.integers(0, 2, N)           # 동전 던지기

진짜 정확도는 50%여야 한다. 이 데이터에는 배울 것이 없다.

# (가) 전체 데이터로 특징을 고른 뒤 교차검증  ← 누수
sel = SelectKBest(f_classif, k=20).fit(X, y)
score = cross_val_score(LogisticRegression(), sel.transform(X), y, cv=5).mean()

# (나) 특징 선택을 파이프라인에 넣어 폴드 안에서만
pipe = make_pipeline(SelectKBest(f_classif, k=20), LogisticRegression())
score = cross_val_score(pipe, X, y, cv=5).mean()
정확도
(가) 전체로 고른 뒤 교차검증85.8%
(나) 파이프라인 안에서 고름39.2%

순수한 잡음에서 85.8%가 나왔다. 논문이나 보고서에 실리기 딱 좋은 숫자다.

일어난 일은 이렇다. 특징 6000개 중에는 순전히 우연으로 yy 와 상관이 높아 보이는 것이 반드시 있다. SelectKBest 를 전체 데이터에 돌리면 그 우연을 테스트 폴드의 정답까지 보면서 고른다. 그 뒤에 교차검증을 해도 이미 답을 훔쳐본 뒤다.

(나)가 39.2%로 50%보다 낮은 것도 정상이다. 표본이 120개뿐이라 우연히 아래로 벗어났을 뿐, “배울 것이 없다”는 사실과 모순되지 않는다.

규칙: yy 를 보는 모든 전처리는 교차검증 안에 들어가야 한다. 특징 선택, 타깃 인코딩, 오버샘플링(SMOTE)이 전부 해당한다.

B2. 스케일러를 테스트에도 fit_transform

sc = StandardScaler()
model.fit(sc.fit_transform(X_train), y_train)
model.score(sc.fit_transform(X_test), y_test)   # ← 틀림
model.score(sc.transform(X_test), y_test)       # ← 맞음
정확도
테스트에 fit_transform92.5%
테스트에 transform93.3%

이 예에서는 차이가 작고, 심지어 틀린 쪽이 더 낮다. 그래도 고쳐야 하는 이유는 점수가 아니라 방향이다. 테스트셋의 평균·표준편차를 모델이 알게 되는 것이고, 그것은 배포 뒤에는 절대 얻을 수 없는 정보다. 요청이 하나씩 들어오는 서비스에서는 “테스트셋의 평균”이라는 것 자체가 없다.

파이프라인을 쓰면 실수할 여지가 사라진다.

pipe = make_pipeline(StandardScaler(), LogisticRegression())
pipe.fit(X_train, y_train)      # 스케일러는 학습셋으로만 fit
pipe.score(X_test, y_test)      # 예측 때는 transform 만

B3. stratify 를 빼먹기

양성이 6.7%인 데이터에서 train_test_split 을 시드만 바꿔 12번 돌렸다.

테스트셋 양성 비율표준편차
stratify 없음0.0% ~ 14.7%3.60%p
stratify=y6.7% ~ 6.7%0.00%p

stratify 가 없으면 양성이 하나도 없는 테스트셋이 나올 수 있다(0.0%). 그런 분할에서 잰 재현율은 정의되지 않거나 0이다. 시드만 바꿨는데 결과가 달라지면 모델 비교가 무의미해진다.

train_test_split 은 stratify=y, 교차검증은 StratifiedKFold 가 기본 선택이다. 분류에서 cross_val_score 의 기본 CV는 이미 stratified지만, shuffle=False 가 기본이라 데이터가 라벨 순으로 정렬돼 있으면 여전히 위험하다 — StratifiedKFold(5, shuffle=True, random_state=0) 를 명시한다.

B4. 결측 대치를 나누기 전에

X_imputed = SimpleImputer().fit_transform(X)        # ← 전체 평균에 테스트 행이 섞인다
X_tr, X_te, y_tr, y_te = train_test_split(X_imputed, y)
정확도
전체에 대치한 뒤 나눔83.3%
파이프라인 안에서 대치81.7%

1.6%p. 작아 보이지만 B1과 똑같은 종류의 실수다. 특징 선택에서는 85.8% 대 39.2%였다. 차이의 크기는 상황이 정하고, 잘못됐다는 사실은 그대로다.

외울 것은 하나다. 나눈 뒤에, 학습셋만으로. 그리고 그것을 보장하는 도구가 Pipeline 이다. 전처리를 파이프라인 밖에서 하고 있다면 일단 의심한다.


3. 한 문단 요약

scikit-learn 1.6에서 load_boston·plot_confusion_matrix 는 제거됐고, OneHotEncoder(sparse=)·max_features='auto'·normalize=True·iid=·base_estimator=· n_features_ 는 이름이 바뀌었다. 이런 것들은 에러가 나므로 금방 고친다. 진짜 문제는 데이터 누수다 — 라벨과 아무 관계도 없는 순수 잡음(표본 120개 × 특징 6000개)에서 특징 선택을 교차검증 밖에 두자 85.8% 가 나왔고, 파이프라인 안에 넣으니 39.2%였다. 스케일러를 테스트에 다시 fit 하는 것, stratify 를 빼서 양성이 0%인 테스트셋을 만드는 것, 결측 대치를 나누기 전에 하는 것이 전부 같은 실수의 변형이다. 규칙은 하나다 — yy 를 모르는 척해야 하는 모든 계산은 나눈 뒤에, 학습셋만으로.


참고

표시는 이 브라우저에만 남는다. 서버로 가는 것은 없다.