핸즈온 머신러닝 2장에서는 머신러닝의 프로젝트의 개발 단계 및 배포까지 일련의 단계를 설명하고 있습니다.

 

출처 : https://github.com/rickiepark/handson-ml3

핸즈온 머신러닝은 유투브에서 온라인 강의도 제공되어, ADP공부시 유용한 교재입니다.

 

해당 데이터셋을 기반으로, ADP 시험 관점에서 단계를 재구성해보았습니다.

요건정의, 전처리, 모델링, 검증 테스트 배포 등 개발 단계가 ADP나 빅분기등에서 자주 출제되지만,  CRISP-DM 등 표준화된 프로세스는 현실과 동떨어져 보입니다.

 

교제에 딱히 프로세스가 정의 되어 있지, 않지만 캐글등 일반적인 단계를 정리하면 개략 다음과 같습니다.(정해진 항목이나 순서는 없음)

순서나 단계가 실제 정해져있지는 않고, 개략적인 경험적인 순서입니다. 

단계 설명 예시
1. 문제 정의 예측할 목표 정하기 캘리포니아 지역의 주택 중간가격 (median_house_value) 예측
2. 데이터 수집 데이터 로딩 housing.csv 불러오기
3. EDA (탐색적 데이터 분석) 데이터 구조 파악 및 시각화 결측치, 분포, 상관관계 분석 등
4. 데이터 전처리 결측치, 범주형 처리, 파생변수 생성 total_rooms 등으로 방 개수 비율 파생
5. 이상치 처리 특이값 제거 또는 조정 housing_median_age의 극단값 등
6. Feature Engineering 중요 Feature 도출, 파생 Feature 생성 rooms_per_household 등
7. Feature Scaling 정규화, 표준화 StandardScaler 또는 MinMaxScaler 사용
8. 데이터 분할 훈련/검증/테스트 셋 분리 train_test_split() 또는 계층 샘플링
9. 모델 선택 및 학습 선형 회귀, 결정 트리, 랜덤포레스트 등 LinearRegression, RandomForestRegressor
10. 모델 평가 RMSE, MAE, R² 등 사용 검증 세트로 비교
11. 하이퍼파라미터 튜닝 GridSearchCV, RandomSearch 등 GridSearchCV로 파라미터 최적화
12. 최종 테스트 테스트 세트에서 성능 확인 과적합 여부 확인
13. 배포 및 서빙 모델 저장, 예측 API 제공 등 joblib, Flask, FastAPI 등 사용

 

핸즈온 머신러닝 2장의 코드가 일반적인 캐글의 모든 단계를 포함하여, 초보자가 코드를 보기는 너무 어려워 보입니다.

개발자마다 자신만의 코딩 스타일이 있어, 이를 활용해 자신만의 스타일을 구축해 나가는 것이 필요합니다.

아래는 핸즈온 머신러닝의 코드를 제 스타일로 변경해 간략화한 코드입니다.

import pandas as pd
import numpy as np
import os
import os
import urllib.request
import tarfile
import matplotlib.pyplot as plt 
import seaborn as sns
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.linear_model import LinearRegression
from sklearn.ensemble import RandomForestRegressor
from sklearn.tree import DecisionTreeRegressor
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.metrics import r2_score

# (1) 데이터 로드
os.makedirs("datasets/housing", exist_ok=True)

url = "https://raw.githubusercontent.com/ageron/handson-ml2/master/datasets/housing/housing.tgz"
urllib.request.urlretrieve(url, "datasets/housing/housing.tgz")

with tarfile.open("datasets/housing/housing.tgz") as housing_tgz:
    housing_tgz.extractall(path="datasets/housing")
df = pd.read_csv("datasets/housing/housing.csv") 

# (2) 계층분할용 중간소득 카테고리 생성
income_cat = pd.cut(df["median_income"], bins=[0., 1.5, 3.0, 4.5, 6., np.inf], labels=[1, 2, 3, 4, 5])

# (3) train_test_split (계층 기반 stratify)
X = df.drop(columns=["median_house_value"])
y = df["median_house_value"]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42, stratify=income_cat)

# (4) 수치형 및 범주형 특성 정의
num_col = X.select_dtypes(exclude=object).columns
cat_col = X.select_dtypes(object).columns

# (5) 사용자 정의 Transformer (파생 변수 생성)
class CombinedAttributesAdder(BaseEstimator, TransformerMixin):
    def __init__(self, add_bedrooms_per_room=True):
        self.add_bedrooms_per_room = add_bedrooms_per_room

    def fit(self, X, y=None):
        return self

    def transform(self, X):
        rooms_per_household = X[:, 3] / X[:, 5]
        population_per_household = X[:, 4] / X[:, 5]
        if self.add_bedrooms_per_room:
            bedrooms_per_room = X[:, 2] / X[:, 3]
            return np.c_[X, rooms_per_household, population_per_household, bedrooms_per_room]
        else:
            return np.c_[X, rooms_per_household, population_per_household]

# (6) 수치형 파이프라인
num_pipeline = Pipeline([
    ('imputer', SimpleImputer(strategy="median")),
    ('attribs_adder', CombinedAttributesAdder()),
    ('std_scaler', StandardScaler()),
])

# (7) 전체 전처리 파이프라인 구성
full_pipeline = ColumnTransformer([
    ("num", num_pipeline, num_col),
    ("cat", OneHotEncoder(handle_unknown='ignore'), cat_col),
])

# (8) 모델 정의 
models = {
    "LinearRegression": LinearRegression(),
    "RandomForest": RandomForestRegressor(random_state=42),
    "DecisionTree": DecisionTreeRegressor(random_state=42)
}

# (10) 하이퍼파라미터 튜닝 (GridSearch용)
param_grid = {
    "RandomForest": {
        "model__n_estimators": [10, 50],
        "model__max_features": [4, 6, 8]
    },
    "DecisionTree": {
        "model__max_depth": [5, 10, 20]
    }
}

# (11) 모델 학습 및 평가 (R2로 평가)
results = {}

for name, model in models.items():
    pipeline = Pipeline([
        ("preprocessing", full_pipeline),
        ("model", model)
    ])
    grid = GridSearchCV(pipeline, param_grid.get(name, {}), cv=3, scoring="r2", return_train_score=True)
    grid.fit(X_train, y_train)
    y_pred = grid.predict(X_test)
    results[name] = {
        "best_params": grid.best_params_,
        "cv_r2": grid.best_score_,
        "test_r2": r2_score(y_test, y_pred)
    }

pd.DataFrame(results).T

 

 

코드의 각부분을 살펴보겠습니다.

1. 데이터 불러오기

. 최근 ADP 시험에서는 pd.read_csv로 단순히 데이터를 불러오지는 않는 것 같습니다.
. merge나 groupby, melt, concat등으로 몇개의 데이터를 가공하고 합쳐서 데이터셋을 생성하는 문제가 많은것 같습니다.

import pandas as pd 
import numpy as np 
import matplotlib.pyplot as plt 
import seaborn as sns
import os
import urllib.request
import tarfile

os.makedirs("datasets/housing", exist_ok=True)

url = "https://raw.githubusercontent.com/ageron/handson-ml2/master/datasets/housing/housing.tgz"
urllib.request.urlretrieve(url, "datasets/housing/housing.tgz")

with tarfile.open("datasets/housing/housing.tgz") as housing_tgz:
    housing_tgz.extractall(path="datasets/housing")
df = pd.read_csv("datasets/housing/housing.csv")

 

  • 2. 탐색적 데이터 분석 (EDA)
    • ADP 머신러닝에서 항상 EDA를 수행하라는 소문제가 등장합니다.
    • EDA는 대이터의 결측치 이상치 분포등을 시각화 등으로 수행 후 결과를 서술해야합니다.
    • 현업에서나 실무시 EDA는 중요한 단계이지만, 분석을 시작하면 매우 오랜 시간이 소요됩니다.
    • 실제 시험에서는 info, desciibe. corr, heatmap, sns pairplot 등의 몇가지만 수행 후 결과를 분석하여 서술해서 기본적인 실력은 가지고 있다 정도만 보여주면 됩니다.
    • EDA기능을 제공하는 Pandas profiling(ydata_profiling으로 변경)을 수행하고, 선택적으로 결과를 가져오는 방법도 좋습니다. ADP 시험장에서 pandas_profiling이 제대로 동작하지 않는 다는 이야기가 있어, 시험 시작전 패키지 ydata_profiling을 설치 후 동작여부 확인이 필요할 것 같습니다. ydata_profiling을 쥬피터 노트북을 하나 더 만들어 수행 수 내용을 선택적으로 포함해야됩니다.
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 20640 entries, 0 to 20639
Data columns (total 10 columns):
 #   Column              Non-Null Count  Dtype  
---  ------              --------------  -----  
 0   longitude           20640 non-null  float64
 1   latitude            20640 non-null  float64
 2   housing_median_age  20640 non-null  float64
 3   total_rooms         20640 non-null  float64
 4   total_bedrooms      20433 non-null  float64
 5   population          20640 non-null  float64
 6   households          20640 non-null  float64
 7   median_income       20640 non-null  float64
 8   median_house_value  20640 non-null  float64
 9   ocean_proximity     20640 non-null  object 
dtypes: float64(9), object(1)
memory usage: 1.6+ MB

결측치 확인

df.isnull().sum().plot(kind='bar')

명목형 변수 확인

df['ocean_proximity'].value_counts().plot(kind='barh')
for i, v in enumerate(df['ocean_proximity'].value_counts()):
    plt.text(v + 100, i, str(v), va='center')  # v+100은 막대 오른쪽 여백
plt.show()

데이터 분포 확인

df.describe().round(3).T

df.hist(bins=50, figsize=(20, 15));

수치형 데이터의 왜도가 심할 경우에는 로그 변환이나 지수변환이 필요하다고 설명하면 좋습니다.

hist로 이상치등의 설명도 추가할수 있습니다.

ADP 서술 예제) total_rooms등의 수치형 데이터는 오른쪽 긴꼬리의 분포를 가지고 있습니다. 머신러닝에서 정규형 데이터에서 학습 성능이 좋으므로 로그변환 적용등의 검토가 필요합니다.

 

seaborn의 pairplot은 데이터 분포나 상관관계 이상치를 시각적으로 매우 쉽게 설명하기 좋습니다.

데이터가 크면 오래걸리고, 동작이 제대로 안될 수 있어 필요한 컬럼만 선정하여 설명하는것이 좋습니다.

sns.pairplot(df);

heatmap

상관관계를 표시할 때 가장 유용한 그래프입니다.

ADP에 익혀두면 설명할 때 좋습니다. 

# 상관관계 계산
corr = df.corr(numeric_only=True)

# 히트맵 시각화
plt.figure(figsize=(10, 8))
sns.heatmap(corr, annot=True, fmt=".2f", cmap="coolwarm", square=True)
plt.title("Correlation Heatmap")
plt.tight_layout()
plt.show()

median_income이 종속변수와 상관관계가 높습니다. 중간소득만으로도 회귀성능이 높게 나올것 같습니다.

3. 데이터 정제 

핸즈온 머신러닝에서 결측치 처리에, SimpleImputer, 명목형은 OrdinalEncoder(LabelEncoder), OneHotEncoder등을 사용했습니다.
판다스에서 제공하는 fillna나, pd.get_dummies를 사용할 수 있는데 Sklearn에서 제공되는 라이브러리를 사용하면 pipeline를 정의하여 한번에 fit과 transform으로 처리할 수 있습니다.
아래 방법은 매우 유용한 방법이지만, 시험에서 detail한 요구사항이 나오면 사용하기가 어렵습니다.
ADP시험이 시간이 매우 부족하여 사용한다면 기계적으로 코드를 작성할 수 있을 정도로 숙련이 필요합니다.
pipeline과 columntransformer를 사용한다면 고득점에 유리할 것 같습니다.

  1. 결측치는 중앙값으로 대치
  2. 명목형 변수는 onehot 사용.
  3. 왜도를 확인하고, log나 exp를 수행하는 사용자 정의 변환기를 생성
  4. 표준화를 진행

SimpleImputer(결측치 처리)

from sklearn.impute import SimpleImputer 
imputer = SimpleImputer(strategy='median')
print(imputer.fit_transform(X_train.select_dtypes(exclude='object')))
print(imputer.strategy)
print(imputer.statistics_)
[[-118.41     33.89     31.     ...  677.      331.        7.2316]
 [-117.83     34.11     29.     ... 1484.      445.        4.9844]
 [-118.33     34.04     31.     ...  955.      239.        2.913 ]
 ...
 [-116.34     33.36     24.     ...  731.      295.        3.3214]
 [-118.29     33.71     36.     ... 1815.      697.        3.7596]
 [-118.13     33.92     36.     ...  615.      206.        4.1786]]
median
[-118.51    34.26    29.    2111.     430.    1159.     406.       3.536]

 

명목형 처리에서 단순이 숫자로 변경해주는 OrdinalEncoder(LabelEncoder)나 OnehotEncoder를 사용할 수 있습니다.

OrdinalEncoder 는 항목들이 독립인데 숫자로 존재하지 않는 크기를 만드는 단점이 있고, OnehotEncoder는 컬럼을 많이 생성하므로 사용이 어려울 수 있습니다.

OnehotEncoder의 컬럼의 수를 설정가능하므로 가급적 onehot을 사용하는 것이 좋습니다.

from sklearn.preprocessing import OrdinalEncoder
ord_encoder = OrdinalEncoder() 
print(ord_encoder.fit_transform(df[['ocean_proximity']]))
print(ord_encoder.categories_)
[[3.]
 [3.]
 [3.]
 ...
 [1.]
 [1.]
 [1.]]
[array(['<1H OCEAN', 'INLAND', 'ISLAND', 'NEAR BAY', 'NEAR OCEAN'],
      dtype=object)]

 

from sklearn.preprocessing import OneHotEncoder 
onehot_enc = OneHotEncoder(sparse_output=False)
pd.DataFrame(onehot_enc.fit_transform(df[['ocean_proximity']]), columns=onehot_enc.get_feature_names_out()).head()

 

4. 사용자 정의 변환기(Custom Transformer)

사용자 정의 변환기(Custom Transformer)**는 Scikit-Learn의 전처리 파이프라인에 자신만의 데이터 전처리 기능을 추가할 수 있도록 만들어진 클래스입니다. 두 컬럼을 조합하여 비율 계산, 로그 변환, 도메인 특화 파생변수 생성 등 기존 StandardScaler, SimpleImputer 등으로 처리 불가한 경우 사용할 수있습니다. Pipeline 또는 ColumnTransformer에 넣어서 fit/transform을 자동 실행할 수 있습니다.

사용자 정의 변환기를 사용하기 위해서는 BaseEstimator, TransformerMixin의 클래스를 상속받은 후 init, fit, transform 메소드를 오버라이딩하면 됩니다. BaseEstimator 는 get_params, set_params등의 Estimator기능을 구현하여, GridSearchCV등에 필수적인 기능이고 TransformerMixin 는 fit과 transform기능을 구현합니다. fit_transform을 fit과 transform을 이용하여 지원됩니다.

from sklearn.base import BaseEstimator, TransformerMixin

class MyTransformer(BaseEstimator, TransformerMixin):
    def __init__(self, ...):       # 하이퍼파라미터 정의
        ...

    def fit(self, X, y=None):      # 학습할 것이 있으면 여기서 처리
        return self

    def transform(self, X):        # 실제 데이터 변환 로직
        return 변환된_X

 

핸즈온 머신러닝 책에서는 방 개수 비율계산하여 파생변수를 생성하는 코드를 사용자 정의 변환기를 통해 구현하였습니다.

rooms_per_household와 population_per_household 2개의 파생변수가 사용자 정의 변환기에 의해 추가됩니다.

from sklearn.base import BaseEstimator, TransformerMixin 

room_ix, bedrooms_ix, population_ix, households_ix = 3,4,5,6 

class CombinedAttributeAdder(BaseEstimator, TransformerMixin):
    def __init__(self, add_bedrooms_per_room = True): 
        self.add_bedrooms_per_room = add_bedrooms_per_room 

    def fit(self, X, y=None): 
        return self # 아무것도 하지 않는다. 

    def transform(self, X): 
        rooms_per_household = X[:, room_ix] / X[:, households_ix] 
        population_per_household = X[:, population_ix] / X[:, households_ix] 

        if self.add_bedrooms_per_room: 
            beedrooms_per_room = X[:, bedrooms_ix] / X[:, room_ix] 
            return np.c_[X, rooms_per_household, population_per_household, beedrooms_per_room]
        else: 
            return np.c_[X, rooms_per_household, population_per_household]

attr_adder = CombinedAttributeAdder(add_bedrooms_per_room=False)
housing_extra_attribs = attr_adder.transform(df.to_numpy())
pd.DataFrame(housing_extra_attribs, columns=list(df.columns)+['rooms_per_household','population_per_household'])

 

5. Pipeline

  • Pipeline은 머신러닝 워크플로우를 단계별로 구성하고 자동화하기 위한 도구입니다.
  • 여러 개의 전처리 및 모델 학습 단계를 하나의 객체처럼 묶어 처리하는 도구입니다.
  • fit, transform, predict를 한 번에 처리할 수 있어, 코드가 간결해지고 모듈화가 가능해지는 장점이 있습니다.
from sklearn.pipeline import Pipeline

pipe = Pipeline([
    ('step1', transformer1),
    ('step2', transformer2),
    ('model', estimator)
])
from sklearn.pipeline import Pipeline 
from sklearn.preprocessing import StandardScaler 

num_pipeline = Pipeline([
    ('imputer', SimpleImputer(strategy='median')), 
    ('attr_adder', CombinedAttributeAdder()), 
    ('std_scaler', StandardScaler()), 
])

housing_num_tr = num_pipeline.fit_transform(df.select_dtypes(exclude=object))
housing_num_tr
array([[-1.32783522,  1.05254828,  0.98214266, ...,  0.62855945,
        -0.04959654, -1.02998783],
       [-1.32284391,  1.04318455, -0.60701891, ...,  0.32704136,
        -0.09251223, -0.8888972 ],
       [-1.33282653,  1.03850269,  1.85618152, ...,  1.15562047,
        -0.02584253, -1.29168566],
       ...,
       [-0.8237132 ,  1.77823747, -0.92485123, ..., -0.09031802,
        -0.0717345 ,  0.02113407],
       [-0.87362627,  1.77823747, -0.84539315, ..., -0.04021111,
        -0.09122515,  0.09346655],
       [-0.83369581,  1.75014627, -1.00430931, ..., -0.07044252,
        -0.04368215,  0.11327519]])

6. ColumnTransformer

  • 열별로 서로 다른 전처리기(transformer)를 적용할 수 있게 하는 도구입니다.
  • 범주형은 OneHot, 수치형은 정규화 등 열 타입별로 전처리를 분리 적용할 수 있게 해줍니다.
  • Pipeline과 결합하여 자동화된 워크플로우 구성할 수 있습니다.
from sklearn.compose import ColumnTransformer

ct = ColumnTransformer([
    ("num", 수치형_파이프라인, 수치형_컬럼리스트),
    ("cat", 범주형_파이프라인, 범주형_컬럼리스트)
])
from sklearn.compose import ColumnTransformer 

num_col = df.select_dtypes(include=np.number).columns 
cat_col = df.select_dtypes(object).columns

col_trans = ColumnTransformer([
    ('num', num_pipeline, num_col),
    ('cat', OneHotEncoder(), cat_col),
]) 

housing_prepared = col_trans.fit_transform(df)
array([[-1.32783522,  1.05254828,  0.98214266, ...,  0.        ,
         1.        ,  0.        ],
       [-1.32284391,  1.04318455, -0.60701891, ...,  0.        ,
         1.        ,  0.        ],
       [-1.33282653,  1.03850269,  1.85618152, ...,  0.        ,
         1.        ,  0.        ],
       ...,
       [-0.8237132 ,  1.77823747, -0.92485123, ...,  0.        ,
         0.        ,  0.        ],
       [-0.87362627,  1.77823747, -0.84539315, ...,  0.        ,
         0.        ,  0.        ],
       [-0.83369581,  1.75014627, -1.00430931, ...,  0.        ,
         0.        ,  0.        ]])

 

모델은 여러가지를 사용할 수 있는데, 우선 선형 linear, 의사결정나무, 랜덤포레스트 3개를 사용해보겠습니다.  
ADP시험에서 보통 2개 이상의 모델의 성능을 비교하라는 문제가 나옵니다. 

models = {
    "LinearRegression": LinearRegression(),
    "RandomForest": RandomForestRegressor(random_state=42),
    "DecisionTree": DecisionTreeRegressor(random_state=42)
}

 

GridSearchCV

  • 하이퍼파라미터 조합을 그리드 탐색(grid search) 방식으로 모두 시도해보고, 가장 좋은 성능을 내는 모델을 자동으로 선택해줍니다.
  • 파라미터가 많을 경우 RandomSearchCV 등도 사용됩니다.
  • 시험에서 GridSearch CV를 사용하라는 문제도 출제됩니다.
  • param_grid로 dict형식으로 하이퍼파라미터를 설정 후 fit하면 최적의 성능을 내는 하이퍼 파라미터 조합을 반환합니다.
  • scoreing을 통해 r2나 neg_mean_squared_error 성능을 측정할 수 있고, cv로 교차 검증을 수행합니다.
from sklearn.model_selection import GridSearchCV
from sklearn.ensemble import RandomForestRegressor

param_grid = {
    "n_estimators": [10, 50],
    "max_features": [4, 6, 8]
}

model = RandomForestRegressor()
grid_search = GridSearchCV(model, param_grid, cv=3, scoring="r2")
grid_search.fit(X_train, y_train)

best_model = grid_search.best_estimator_
print(grid_search.best_params_)

 

# (10) 하이퍼파라미터 튜닝 (GridSearch용)
param_grid = {
    "RandomForest": {
        "model__n_estimators": [10, 50],
        "model__max_features": [4, 6, 8]
    },
    "DecisionTree": {
        "model__max_depth": [5, 10, 20]
    }
}

# (11) 모델 학습 및 평가 (R2로 평가)
results = {}

for name, model in models.items():
    pipeline = Pipeline([
        ("preprocessing", full_pipeline),
        ("model", model)
    ])
    grid = GridSearchCV(pipeline, param_grid.get(name, {}), cv=3, scoring="r2", return_train_score=True)
    grid.fit(X_train, y_train)
    y_pred = grid.predict(X_test)
    results[name] = {
        "best_params": grid.best_params_,
        "cv_r2": grid.best_score_,
        "test_r2": r2_score(y_test, y_pred)
    }

pd.DataFrame(results).T

 

핸즈온 머신러닝의 코드 예제가 매우 많은데, 간략화하였습니다. 

캐글에서도 모두 코딩 스타일이 달라서, 자신의 루틴을 완성하고 익숙해지는것이 중요합니다. 

PipeLine과 ColumnTransformer는 캐글에서 자동화된 워크플로우 구성하는데 자주 사용됩니다.

익숙해진다면 ADP의 머신러닝에서 고득점을 얻을 수 있습니다.

+ Recent posts