fit_transform과 transform에 차이
ssafy ai 강의 실습중 궁금증 해결
scikit-learn의 fit_transform vs transform, 언제 뭘 써야 할까?
ssafy ai 강의 실습중 sklearn의 매서드 중 fit_transform, transform 두개의 매서드가 비슷하게 생기기도 했고 다른점이 뭘까 궁금해서 찾아봤다.
정리
바쁘신 분들을 위해 결과 먼저 말하자면
fit()= 기준 학습,transform()= 기준 적용,fit_transform()= 둘 다- 학습 데이터에만
fit_transform, 나머지는 전부transform - 테스트 데이터에 fit하면 데이터 누수 + 변환 기준 붕괴 + 차원 불일치
- 헷갈릴 바엔 그냥
Pipeline을 쓰자. 실수를 구조적으로 막아준다
StandardScaler를 예로 들면 이렇다.
from sklearn.preprocessing import StandardScaler
import numpy as np
X = np.array([[10.0], [20.0], [30.0]])
scaler = StandardScaler()
scaler.fit(X)
print(scaler.mean_) # [20.] ← fit이 학습한 파라미터
print(scaler.scale_) # [8.16...]
print(scaler.transform(X))
# [[-1.22]
# [ 0. ]
# [ 1.22]]
fit()은 평균 20, 표준편차 8.16이라는 기준을 만들어 객체 안에 저장한다. transform()은 그 기준을 가져다 (x - 20) / 8.16 계산을 수행할 뿐이다.
💡
fit()이후에 생기는 속성들은 관례적으로 뒤에 언더스코어가 붙는다.mean_,scale_,n_features_in_처럼. 이게 붙어있으면 "학습된 결과물"이라는 뜻이다.
규칙은 딱 하나
학습 데이터에는
fit_transform, 그 외 모든 데이터에는transform
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train) # ✅ 학습 데이터로 기준을 만들고 변환
X_test_scaled = scaler.transform(X_test) # ✅ 그 기준을 그대로 적용
검증 데이터, 테스트 데이터, 그리고 나중에 실제 서비스에 들어오는 요청 데이터까지 — 학습 데이터가 아닌 모든 것은 transform만 호출한다.
근대 이쯤에서 하나의 궁금증이 더 생겼다. 왜 테스트 데이터(validation_data)에는 fit을 하면 안되는걸까? 그 이유에 대해 찾아보았다.
왜 테스트 데이터에 fit을 하면 안 될까?
1. 데이터 누수 (Data Leakage)
X_test에 fit_transform을 쓰면, 테스트 데이터의 평균과 분산이라는 정보가 모델 평가 과정에 흘러 들어간다.
우리가 테스트 세트를 따로 떼어놓는 이유는 "모델이 처음 보는 데이터에서 얼마나 잘하는지"를 재기 위해서다. 그런데 테스트 데이터의 통계를 미리 계산해서 쓰는 순간, 그건 더 이상 처음 보는 데이터가 아니다.
실제 서비스에서는 미래에 들어올 데이터의 평균을 미리 알 수 없다. 그러니 이렇게 측정한 성능은 실제보다 낙관적으로 부풀려진 숫자다.
2. 변환 기준이 달라진다
이게 더 직관적으로 와닿는 문제다.
X_train = np.array([[100.0], [110.0], [90.0]]) # 평균 100
X_test = np.array([[120.0], [130.0], [110.0]]) # 평균 120
# ❌ 잘못된 방식
StandardScaler().fit_transform(X_train) # → [0, 1.22, -1.22]
StandardScaler().fit_transform(X_test) # → [0, 1.22, -1.22] 똑같아졌다!
원래 test 데이터는 train보다 전체적으로 값이 큰 데이터였다. 그런데 각각 fit해버리니 그 차이가 통째로 사라졌다. 모델 입장에서는 완전히 다른 의미의 숫자가 들어오는 셈이다.
# ✅ 올바른 방식
scaler = StandardScaler()
scaler.fit_transform(X_train) # → [0, 1.22, -1.22]
scaler.transform(X_test) # → [2.45, 3.67, 1.22] 값이 크다는 정보가 유지됨
3. 아예 에러가 나기도 한다
OneHotEncoder나 CountVectorizer처럼 카테고리나 어휘 목록을 학습하는 변환기는 상황이 더 심각하다.
from sklearn.feature_extraction.text import CountVectorizer
train_docs = ["머신러닝 공부", "딥러닝 공부"]
test_docs = ["강화학습 공부하기"]
vec = CountVectorizer()
X_train = vec.fit_transform(train_docs)
print(X_train.shape) # (2, 4)
# ❌ test에 다시 fit하면
X_test_wrong = CountVectorizer().fit_transform(test_docs)
print(X_test_wrong.shape) # (1, 2) ← 컬럼 개수가 다름! 모델에 넣으면 에러
# ✅ 학습된 어휘 사전을 그대로 사용
X_test = vec.transform(test_docs)
print(X_test.shape) # (1, 4) ← 차원 유지
학습 때 4개 피처로 훈련한 모델에 2개 피처를 넣으면 당연히 터진다. 그리고 이런 에러는 그나마 다행이다. StandardScaler처럼 조용히 잘못된 값을 뱉는 경우가 훨씬 무섭다.
Pipeline을 쓰자
문제는 교차검증(cross validation)이다. fold를 나눌 때마다 매번 수동으로 fit/transform을 구분해주는 건 실수하기 딱 좋다.
# 😰 이렇게 하면 CV의 각 fold마다 누수가 생긴다
X_scaled = StandardScaler().fit_transform(X) # 전체 데이터로 fit!
cross_val_score(model, X_scaled, y, cv=5)
위 코드는 검증 fold의 정보까지 포함해서 스케일링 기준을 만들었기 때문에 이미 누수가 발생했다. 이럴 때 Pipeline을 쓰면 sklearn이 알아서 처리해준다.
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
pipe = Pipeline([
('scaler', StandardScaler()),
('clf', LogisticRegression())
])
# 각 fold의 train 부분에만 fit_transform, valid 부분엔 transform이 자동 적용됨
scores = cross_val_score(pipe, X_train, y_train, cv=5)
pipe.fit(X_train, y_train)
pipe.predict(X_test) # transform도 알아서 호출됨
Pipeline을 쓰면 predict() 할 때 전처리를 깜빡하는 실수도 원천 차단된다. 모델 저장할 때도 pipeline 통째로 저장하면 되니까 배포도 훨씬 깔끔해진다.
알아두면 좋은 예외들
transform이 아예 없는 변환기
TSNE, SpectralEmbedding 같은 일부 알고리즘은 transform() 메서드 자체가 없고 fit_transform()만 있다. 새로운 데이터 포인트를 기존 임베딩 공간에 매핑한다는 개념이 성립하지 않기 때문이다. 이런 건 애초에 시각화·탐색용이지 학습 파이프라인에 넣는 용도가 아니다.
fit_transform이 더 빠른 경우
PCA 같은 일부 변환기는 fit_transform()이 fit() 후 transform()을 따로 호출하는 것보다 계산량이 적도록 최적화되어 있다. 결과는 동일하지만, 학습 데이터에는 fit_transform을 쓰는 게 성능상 이득이다.
타겟(y)에 대한 변환
LabelEncoder나 TargetEncoder처럼 y를 다루는 경우도 원칙은 같다. 학습 데이터로 fit하고 나머지는 transform. 다만 회귀 문제에서 y를 스케일링했다면 예측 후에 inverse_transform()으로 원래 스케일로 되돌리는 걸 잊지 말자.