인지야공/인공 지능 공부 치트 시트 정리/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_boston | ImportError — 제거됨 | fetch_california_housing() |
from sklearn.metrics import plot_confusion_matrix | ImportError — 제거됨 | ConfusionMatrixDisplay.from_estimator(m, X, y) |
OneHotEncoder(sparse=True) | TypeError | sparse_output=True |
RandomForestClassifier(max_features='auto') | InvalidParameterError | 'sqrt' (기존 기본값) 또는 생략 |
LinearRegression(normalize=True) | TypeError | StandardScaler 를 파이프라인에 |
GridSearchCV(..., iid=True) | TypeError | 그냥 뺀다 |
AdaBoostClassifier(base_estimator=...) | TypeError | estimator=... |
model.n_features_ | AttributeError | model.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개 중에는 순전히 우연으로 와 상관이 높아 보이는 것이 반드시 있다.
SelectKBest 를 전체 데이터에 돌리면 그 우연을 테스트 폴드의 정답까지 보면서 고른다. 그 뒤에
교차검증을 해도 이미 답을 훔쳐본 뒤다.
(나)가 39.2%로 50%보다 낮은 것도 정상이다. 표본이 120개뿐이라 우연히 아래로 벗어났을 뿐, “배울 것이 없다”는 사실과 모순되지 않는다.
규칙: 를 보는 모든 전처리는 교차검증 안에 들어가야 한다. 특징 선택, 타깃 인코딩, 오버샘플링(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_transform | 92.5% |
테스트에 transform | 93.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=y | 6.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%인 테스트셋을 만드는 것, 결측 대치를 나누기 전에
하는 것이 전부 같은 실수의 변형이다. 규칙은 하나다 — 를 모르는 척해야 하는 모든 계산은
나눈 뒤에, 학습셋만으로.