본문 바로가기
로봇/인공지능, AI

[핸즈온 머신러닝 3판] 2.머신러닝 프로젝트 처음부터 끝까지 2.5~2.9

by 33곰탱 2024. 9. 15.

2.5 머신러닝 알고리즘을 위한 데이터 준비

이제 머신러닝 알고리즘을 위한 데이터 준비를 할 차례이다.

 

예측 변수와 타깃값에 같은 변형을 적용하지 않기 위해 예측 변수와 레이블을 분리하고 시작하자!!

housing = strat_train_set.drop("median_house_value", axis=1)
housing_labels = strat_train_set["median_house_value"].copy()

2.5.1 데이터 정제

앞서 2.3.6에서 housing.info()로 데이터를 확인했을 때 total_bedrooms에 207개의 데이터가 빠져있었다.

 

대부분의 머신러닝 알고리즘은 누락된 특성을 다루지 못하므로 이를 처리를 해야하는데 어떻게 해야할까?

크게 3가지로 나눌 수 있다!

옵션 1: 결측값이 있는 행 제거

옵션 2: 열 자체를 제거

옵션 3: 결측값을 중앙값으로 대체

housing.dropna(subset=["total_bedrooms"], inplace=True)    # 옵션 1

housing.drop("total_bedrooms", axis=1)                     # 옵션 2

median = housing["total_bedrooms"].median()                # 옵션 3
housing["total_bedrooms"].fillna(median, inplace=True)

 

이 중에서 옵션 3이 데이터를 최대한 유지하므로 책에서는 3을 선택하는 줄 알았는데..?

사이킷런에 있는 SimpleImputer 클래스를 사용한다고 한다.

 

SimpleImputer 

scikit-learn 라이브러리에서 결측값(missing values)을 대체하기 위해 제공되는 클래스입니다. 이는 결측값이 포함된 데이터를 처리할 때, 그 값을 다른 값(평균, 중앙값, 최빈값 등)으로 대체하는 데 사용됩니다

from sklearn.impute import SimpleImputer
imputer = SimpleImputer(strategy="median")

housing_num = housing.select_dtypes(include=[np.number])

imputer.fit(housing_num)

 

 

위의 코드를 설명해 보자면

수치형 특성에서만 중간값이 계산되기 때문에 수치 특성만 가진 데이터 복사본을 만들고 fit() 메서드를 사용해 훈련 데이터에 중간값(strategy="median" 부분, most_frequent 또는 constant 등으로도 바꿀 수 있다고 한다..!) 으로 계산해서 그 결과를 객체의 statistics_에 저장한다고 한다. 그래서 이것을 확인 하려면

imputer.statistics_

 

혹은

housing_num.median().values

 

직접 중간값 명령어로 확인할 수 있다.

2.5.2 텍스트와 범주형 특성 다루기

이제 수치형 데이터말고 텍스트 특성을 살펴보자.

 

이 데이터셋에는 ocean_proximity라는 특성만 텍스트 특성인데 확인 해보자

housing_cat = housing[["ocean_proximity"]]
housing_cat.head(8)

여러가지 값들이 나오는 것을 확인할 수 있다.

 

책에 의하면 가능한 값을 제한된 개수로 나열한 것이고 각 값은 카테고리를 나타내서 범주형 특성이라고 말한다.. 이게 뭔소린가 해서 나의 친구 GPT에게 물어봤다 ㅋㅋㅋ

 

텍스트 데이터범주형 데이터는 서로 비슷하게 보일 수 있지만, 그 목적과 성격에 있어 몇 가지 차이가 있습니다. 텍스트 데이터는 문자로 이루어진 데이터로, 일반적으로 비구조적이고 가공되지 않은 정보를 담고 있는 반면, 범주형 데이터는 값이 몇 가지 고정된 범주로 제한된 구조적인 데이터입니다.

 

그렇다고 하고 일단 넘어가보자

 

머신러닝 알고리즘은 대부분 숫자를 다루기 때문에 범주형 데이터를 수치형 데이터로 변환해야 한다. 

 

OrdinalEncoder, OneHotEncoder등이 있는데

 

OrdinalEncoder

from sklearn.preprocessing import OrdinalEncoder

ordinal_encoder = OrdinalEncoder()
housing_cat_encoded = ordinal_encoder.fit_transform(housing_cat)

 

 

  • OrdinalEncoder는 범주형 데이터를 순서대로 숫자로 변환하지만, 이 방식은 범주 사이에 순서가 없는 경우에는 부적절할 수 있다.
    • 예를 들어, "INLAND"와 "NEAR BAY" 사이에는 순서가 존재하지 않으므로, 이 두 값을 1, 3으로 인코딩하면 머신러닝 알고리즘이 잘못된 가정을 할 수 있게 된다.
    • GPT 예시: 만약 OrdinalEncoder를 사용해 INLAND1, NEAR BAY3으로 변환한다고 가정해 봅시다. 머신러닝 모델이 이 값을 해석할 때, INLAND가 NEAR BAY보다 작거나 NEAR BAY가 더 우위에 있다는 식으로 순서를 해석할 수 있습니다. 이는 잘못된 해석입니다. 실제로는 이 값들이 크기나 순서로 해석될 수 없는 단순한 범주형 데이터이기 때문입니다.

 

OneHotEncoder

from sklearn.preprocessing import OneHotEncoder

cat_encoder = OneHotEncoder()
housing_cat_1hot = cat_encoder.fit_transform(housing_cat)
# 사이킷런 1.2버전에서 `sparse_output` 매개변수가 추가되었고 `sparse` 매개변수는 1.4버전에서 삭제됩니다.
# 이에 대한 경고를 피하려면 `sparse_output`을 사용하세요.
cat_encoder = OneHotEncoder(sparse_output=False)
housing_cat_1hot = cat_encoder.fit_transform(housing_cat)
housing_cat_1hot

이런 경우에는 OneHotEncoder를 사용하는 것이 더 적합하다! OneHotEncoder는 각 범주를 별도의 열로 변환하고, 해당 범주에 해당하는 열에 1을, 나머지 열에는 0을 채워넣는다.

  • 예를 들어, "INLAND"와 "NEAR BAY"가 있으면 "INLAND"는 [1, 0], "NEAR BAY"는 [0, 1]로 변환된다.
  • OneHotEncoder의 결과는 대부분 0으로 채워진 행렬이기 때문에, 이 데이터를 저장하는 데 많은 메모리를 필요로 한다.. 이를 해결하기 위해 기본적으로 출력은 넘파이 배열이 아닌 희소 행렬(sparse matrix) 형태로 데이터를 저장하는 방법을 제공하는데 희소 행렬은 행에서 하나만 1이고 나머지는 0으로 채워진 매우 큰 행렬을 만드는 방식이다. 이런 경우 많은 메모리를 절약하고 계산 속도를 높여줄 수 있다고 한다..!
    • 만약 넘파이 배열을 반환하게 하고 싶으면 두번 째 코드처럼 "sparse_output=False"로 설정하면 된다.

get_dummies

또한 판다스에서는 get_dummies() 함수를 제공하여 범주형 데이터를 OnehotEncoder할 수 있지만,  간단한 데이터 전처리에는 get_dummies()가 더 편리하고, 복잡한 머신러닝 파이프라인에서는 OneHotEncoder가 더 적합하다고 한다.  

  • 아래는 두 함수의 차이이다.

기타 인코딩 방법

  • One-Hot 인코딩은 카테고리의 수가 적을 때는 효과적이지만, 너무 많은 카테고리를 가지는 경우 성능이 저하될 수 있다고 한다. 예를 들어, 국가 코드나 직업, 제품 코드와 같은 데이터는 수천 개의 카테고리를 가질 수 있으니 유의해서 사용해야 한다.
  • 이 경우 임베딩(Embedding) 등의 다른 인코딩 방법을 사용하는 것이 더 적합할 수 있고, 임베딩은 범주형 데이터를 고차원 벡터로 변환하는 방법으로, 신경망을 사용한 학습에 많이 사용된다고 한다

 

2.5.3 특성 스케일과 변환

데이터에 적용할 중요한 변환중 한 가지는 특성 스케일링이라고 한다.

특성 스케일링이 뭘까? 

 

특성 스케일링(feature scaling)은 머신러닝에서 각 특성(피처)의 값 범위를 조정하는 과정이다. 다양한 특성들이 서로 다른 스케일을 가지고 있을 때, 스케일 차이가 너무 크면 일부 알고리즘이 제대로 작동하지 않거나 성능이 저하될 수 있기 때문에, 특성 스케일링을 적용해 특성들이 동일하거나 비슷한 범위 내에 있도록 변환하는 것이 필요하다고 한다.

 

예를 들어, 한 특성의 범위가 0~1인데 다른 특성의 범위가 1000~100000이라면, 모델은 큰 값의 특성에 너무 많은 가중치를 부여할 수 있다.

 

책에서 사용하는 데이터의 전체 방 개수의 범위는 6~39320인 반면, 중간 소득의 범위는 0에서 15까지라고 말한다. 따라서 특성 스케일링을 통해 각 특성의 값 범위를 맞추어 주는 것이 중요하다

 

대표적인 스케일링 방법

두 가지 일반적인 스케일링 방법이 있다.

(1) Min-Max 스케일링 (정규화, normalization):

  • 설명: 각 특성의 최소값을 0, 최대값을 1로 맞추는 방식입니다.
  • 공식: $X' = \frac{X - X_{\text{min}}}{X_{\text{max}} - X_{\text{min}}}$
  • 장점: 특성들이 0과 1 사이의 동일한 범위에 있도록 합니다.
  • 단점: 이상치(outliers)에 민감합니다. 큰 이상치가 있으면 스케일링에 왜곡을 줄 수 있습니다.

(2) 표준화 (standardization): 로그 변환

  • 설명: 각 특성의 평균을 0, 표준 편차를 1로 맞추는 방식입니다.
  • 공식: $X' = \frac{X - \mu}{\sigma}$
  • 장점: 표준화는 특성의 분포와 상관없이 스케일을 조정할 수 있어 이상치에 덜 민감합니다.
  • 단점: Min-Max 스케일링과 달리 특정 값의 범위로 값을 제한하지 않습니다.
from sklearn.preprocessing import MinMaxScaler

min_max_scaler = MinMaxScaler(feature_range=(-1, 1))
housing_num_min_max_scaled = min_max_scaler.fit_transform(housing_num)

사이킷런에서는 이 역할을 하는 MinMaxScaler 변환기를 제공하고 feature_range 매개변수를 사용해 0~1이 아닌 다른 범위로 변경할 수 있다고 한다.

from sklearn.preprocessing import StandardScaler

std_scaler = StandardScaler()
housing_num_std_scaled = std_scaler.fit_transform(housing_num)

표준화 역시 사이킷런에 StandardScaler 변환기가 있다.

 

스케일링과 특성 변환에 따른 문제점

스케일링을 적용할 때 특성의 분포가 두꺼운 꼬리를 가질 경우, 대부분의 값들이 작은 범위에 압축되는 현상이 발생할 수 있다. 이런 경우 특성의 분포를 먼저 변환한 후 스케일링을 적용하는 것이 좋다. 그러면 어떤 방법들이 있을까!?

꼬리 분포 처리

  • 로그 변환: 특성의 분포가 긴 꼬리를 가지고 있을 경우, 로그 변환을 적용하여 특성 값을 압축시킬 수 있습니다. 이는 종종 지수적 분포파워 로우 분포(멱법칙 분포) 에서 자주 사용됩니다.
  • 예시: 인구 분포처럼 일부 구간에서 값이 치우친 경우, 로그 변환을 통해 분포를 더 균일하게 만들 수 있습니다. 이렇게 변환된 값에 스케일링을 적용하면 더 나은 성능을 기대할 수 있습니다.

멱법칙 분포(오른쪽) 예시 (https://www.flickr.com/photos/morville/84894758/)

# 추가 코드 – 이 셀은 그림 2–17을 생성합니다
fig, axs = plt.subplots(1, 2, figsize=(8, 3), sharey=True)
housing["population"].hist(ax=axs[0], bins=50)
housing["population"].apply(np.log).hist(ax=axs[1], bins=50)
axs[0].set_xlabel("Population")
axs[1].set_xlabel("Log of population")
axs[0].set_ylabel("Number of districts")
save_fig("long_tail_plot")
plt.show()

 

버킷화(Bucketizing)

  • 설명: 연속적인 값을 일정한 구간으로 나누어 범주형 값으로 변환하는 방법입니다. 예를 들어, 중간 소득을 특정 구간으로 나누어 데이터의 복잡성을 줄일 수 있습니다.
  • 장점: 버킷화를 통해 각 값을 범주형 데이터로 변환할 수 있고, 모델이 이를 더 쉽게 학습할 수 있습니다.

RBF 커널을 사용한 변환

  • 설명: RBF 커널(Radial Basis Function)을 사용하여 특성 사이의 유사도를 계산하는 방법입니다. RBF 커널특성 공간에서의 두 데이터 포인트 사이의 거리를 측정하여 유사도를 계산합니다. 이러한 거리 기반 함수는 주어진 데이터 포인트들이 얼마나 비슷한지 또는 가까운지를 나타냅니다. 예를 들어, 중간 주택 연도와 특정 연도 사이의 유사도를 측정할 수 있습니다.
  • 장점: 특정 기준점과의 거리를 계산하여 특성 간 비선형 관계를 학습할 수 있도록 돕습니다.
  • 예시: 중간 주택 연도 35와의 유사도를 RBF 커널로 계산하여 모델이 주택 가격과의 관계를 더 정확히 학습할 수 있도록 할 수 있습니다.
from sklearn.metrics.pairwise import rbf_kernel

age_simil_35 = rbf_kernel(housing[["housing_median_age"]], [[35]], gamma=0.1)
# 추가 코드 – 이 셀은 그림 2–18을 생성 합니다

ages = np.linspace(housing["housing_median_age"].min(),
                   housing["housing_median_age"].max(),
                   500).reshape(-1, 1)
gamma1 = 0.1
gamma2 = 0.03
rbf1 = rbf_kernel(ages, [[35]], gamma=gamma1)
rbf2 = rbf_kernel(ages, [[35]], gamma=gamma2)

fig, ax1 = plt.subplots()

ax1.set_xlabel("Housing median age")
ax1.set_ylabel("Number of districts")
ax1.hist(housing["housing_median_age"], bins=50)

ax2 = ax1.twinx()  # x축을 공유 하는 쌍둥이 축을 만듭니다
color = "blue"
ax2.plot(ages, rbf1, color=color, label="gamma = 0.10")
ax2.plot(ages, rbf2, color=color, label="gamma = 0.03", linestyle="--")
ax2.tick_params(axis='y', labelcolor=color)
ax2.set_ylabel("Age similarity", color=color)

plt.legend(loc="upper left")
save_fig("age_similarity_plot")
plt.show()

역변환(Inverse Transform)

스케일링된 데이터를 다시 원래 값으로 돌려야 하는 경우가 종종 발생합니다. 이때 inverse_transform() 메서드를 사용하여 변환된 데이터를 원래 값으로 복원할 수 있습니다. 예를 들어, 모델의 예측값이 스케일링된 상태로 나오면 이를 다시 원래 값으로 변환해야 실제 의미를 해석할 수 있습니다.

  • 역변환의 필요성: 스케일링된 상태에서 훈련된 모델의 출력값을 원래 값으로 돌려야만 결과를 이해할 수 있습니다.
  • 예시: StandardScaler를 사용해 스케일링한 후, inverse_transform을 사용해 원래 범위의 값으로 돌릴 수 있습니다.

선형 회귀와 변환

  • 설명: 스케일링된 레이블(종속 변수)로 선형 회귀 모델을 학습시키는 방법도 존재합니다. 변환된 데이터를 사용해 모델을 훈련한 후, 예측 결과를 다시 원래 스케일로 되돌리기 위해 역변환을 적용할 수 있습니다.
  • TransformTargetRegressor: 이 클래스는 회귀 모델의 타겟 값을 스케일링한 후, 모델 훈련이 끝나면 자동으로 원래 값으로 변환합니다. 이 방식은 선형 회귀를 포함한 여러 회귀 알고리즘에서 사용할 수 있습니다.

2.5.4 사용자 정의 변환기

너무 복잡해서 GPT에게 요약해달라고 했어요...

 

여기서는 데이터 변환을 위한 커스터마이즈된 변환기를 어떻게 정의하고 사용할 수 있는지 설명하고 있습니다.

1. 사용자 정의 변환기 (FunctionTransformer)

이 기능을 통해 변환할 수식을 직접 정의하여 사용할 수 있습니다. 예를 들어, 로그 변환을 하거나 특정 함수(지수 함수 등)를 적용할 수 있습니다. 사용자 정의 변환기는 FunctionTransformer 클래스를 사용하여 만들 수 있으며, 이 클래스는 변환 과정에 입력 데이터를 전달하여 원하는 형태로 바꾸어줍니다.

2. K-Means와 사용자 정의 변환

K-Means 클러스터링을 사용해 각 지역의 클러스터 중심과 RBF 유사도를 측정할 수 있습니다. 여기서 클러스터 개수를 10으로 설정하여 데이터에 맞는 클러스터 중심을 찾고, 해당 중심과 각 데이터 포인트 사이의 거리에 따라 유사도를 계산하는 방식입니다. 이를 통해 구역의 중심을 기준으로 각 주택 데이터의 RBF 유사도를 시각적으로 확인할 수 있습니다.

2.5.5 변환 파이프라인

앞서 보았듯이 변환 단계는 올바른 순서대로 실행이 되어야한다.사이킷런은 이를 위해서 변환을 순서대로 처리하도록 도와주는 Pipeline 클래스를 제공한다!!

 

Pipeline은 전에 말했지만 데이터를 단계별로 변환할 때 변환 순서를 설정하고 처리할 수 있는 강력한 도구이다.

 

파이프라인 객체를 생성하면, 여러 변환 단계를 순차적으로 수행할 수 있다. 여기서 각 단계는 이름/추정기 쌍으로 이루어지며, 추정기는 fit_transform() 메서드를 사용해 변환을 수행한다. 예시로 SimpleImputer와 StandardScaler를 사용한 파이프라인이 설명됩니다.

from sklearn.pipeline import Pipeline

num_pipeline = Pipeline([
    ("impute", SimpleImputer(strategy="median")),
    ("standardize", StandardScaler()),
])

 

make_pipeline() 함수는 파이프라인의 이름을 자동으로 생성해 주는 편리한 도구로, 이 도구는 이름을 지정하지 않고도 간단하게 파이프라인을 만들 수 있게 도와준다.

from sklearn.pipeline import make_pipeline

num_pipeline = make_pipeline(SimpleImputer(strategy="median"), StandardScaler())

 

fit_transform() 메서드는 각 변환기를 순차적으로 실행하여 데이터를 처리한다. 마지막 단계는 주로 예측 모델이 될 수 있지만, 이 예제에서는 StandardScaler로 데이터를 변환하는 역할을 한다. fit_transform() 메서드를 실행한 후 변환된 데이터를 확인할 수 있다.

housing_num_prepared = num_pipeline.fit_transform(housing_num)
housing_num_prepared[:2].round(2)

 

ColumnTransformer와 범주형 및 수치형 변환

ColumnTransformer는 수치형 데이터와 범주형 데이터를 각각 다른 변환기로 처리할 수 있게 해준다. 여기서는 수치형 데이터는 SimpleImputerStandardScaler로, 범주형 데이터는 OneHotEncoder로 처리한다.

 

이후 결과물에서 변환된 데이터를 확인할 수 있고, 각 열에 대해 적절한 이름이 부여된 것을 확인할 수 있다.

또한 get_feature_names_out() 메서드를 사용해 변환된 데이터의 특성 이름을 가져올 수 있다.

from sklearn.compose import ColumnTransformer

num_attribs = ["longitude", "latitude", "housing_median_age", "total_rooms",
               "total_bedrooms", "population", "households", "median_income"]
cat_attribs = ["ocean_proximity"]

cat_pipeline = make_pipeline(
    SimpleImputer(strategy="most_frequent"),
    OneHotEncoder(handle_unknown="ignore"))

preprocessing = ColumnTransformer([
    ("num", num_pipeline, num_attribs),
    ("cat", cat_pipeline, cat_attribs),
])

 

코드에서 사용된 다양한 변환기

  • SimpleImputer: 누락된 값을 처리합니다.
  • StandardScaler: 데이터를 스케일링하여 표준화합니다.
  • OneHotEncoder: 범주형 데이터를 원-핫 인코딩으로 변환합니다.
  • ColumnTransformer: 여러 열에 대해 각각 다른 변환기를 적용합니다.

2.6 모델 선택과 훈련

머신러닝을 할 때 데이터와 모델을 잘 선택하는 것이 중요하다. 알죠?

2.6.1 훈련 세트에서 훈련하고 평가하기

전에 말했듯이 이 데이터에는 선형 회귀 모델을 적용할 예정이니 코드를 가져와서 훈련시켜보자

from sklearn.linear_model import LinearRegression

lin_reg = make_pipeline(preprocessing, LinearRegression())
lin_reg.fit(housing, housing_labels)

 

housing_predictions = lin_reg.predict(housing)
housing_predictions[:5].round(-2)  # -2 = 십의 자리에서 반올림

housing_labels.iloc[:5].values

# 추가 코드 – 책에서 언급한 에러 비율을 계산합니다
error_ratios = housing_predictions[:5].round(-2) / housing_labels.iloc[:5].values - 1
print(", ".join([f"{100 * ratio:.1f}%" for ratio in error_ratios]))

이제 RSME로 평가를 해보자

 

from sklearn.metrics import mean_squared_error

lin_rmse = mean_squared_error(housing_labels, housing_predictions,
                              squared=False)
lin_rmse

68647달러의 오차가 난다 약 1억원..? ㄷㄷㄷ

 

다음은 결정트리로 훈련시켜보도록 하겠다.

from sklearn.tree import DecisionTreeRegressor

tree_reg = make_pipeline(preprocessing, DecisionTreeRegressor(random_state=42))
tree_reg.fit(housing, housing_labels)

housing_predictions = tree_reg.predict(housing)
tree_rmse = mean_squared_error(housing_labels, housing_predictions,
                              squared=False)
tree_rmse

 

(??? 오차가 0이다 이럴 순 없어... 과적합이다..!)

 

이러면 모델이 훈련 데이터에만 너무 치중해 일반화된 성능이 부족할 수 있다.. 이를 확인하기 위해 훈련 데이터의 일부를 모델 검증에 사용해야 한다.

2.6.2 교차 검증으로 평가하기

모델 검증을 할 때

훈련 데이터를 더 작은 훈련 세트와 검증 세트로 나누어 모델을 평가할 수 있다. 이 과정에서 k-폴드 교차 검증을 사용할 수 있다.

이는 데이터를 k개의 서브셋으로 나누어 각 서브셋을 한 번씩 검증 세트로 사용하고 나머지를 훈련 세트로 사용하는 방식입니다.

  • 예시: 10-폴드 교차 검증에서는 데이터를 10개의 서브셋으로 나누고, 매번 하나의 폴드를 검증에 사용하고 나머지 9개는 훈련에 사용하게 된다. 이를 반복하여 10개의 결과를 얻는다.
from sklearn.model_selection import cross_val_score

tree_rmses = -cross_val_score(tree_reg, housing, housing_labels,
                              scoring="neg_root_mean_squared_error", cv=10)
                              
pd.Series(tree_rmses).describe()

결정트리 결과가 이전처럼 0이 뜨지 않았다. 선형 회귀 모델이랑 비슷한듯 하다.

mean이 평균 RSME이기 때문에 이 결정트리는 평균 RSME가 67153$이고 표준 편차는 약 1963이다. 

 

다음으로는 랜덤포레스트를 적용해보겠다.

from sklearn.ensemble import RandomForestRegressor

forest_reg = make_pipeline(preprocessing,
                           RandomForestRegressor(random_state=42))
forest_rmses = -cross_val_score(forest_reg, housing, housing_labels,
                                scoring="neg_root_mean_squared_error", cv=10)

 

 

차 검증으로 측정한 RMSE(검증 에러)와 훈련 세트로 측정한 RMSE(훈련 에러)를 비교해 보자!

forest_reg.fit(housing, housing_labels)
housing_predictions = forest_reg.predict(housing)
forest_rmse = mean_squared_error(housing_labels, housing_predictions,
                                 squared=False)
forest_rmse

훈련 세트에서 검증한 RSME는 낮은 값을 갖는 것을 확인할 수 있다. 따라서 여전히 과적합 되어있는 것으로 해석할 수 있고 이를 줄이는 방법은 1장에서 알아보긴 했지만, 모델을 단순화 하거나 규제 혹은 더 많은 데이터를 모으는 방법이었다. 책에서는 하이퍼파라미터 조정에 너무 많은 시간보다는 다양한 모델을 시도해보는 것이 중요하다고 말한다.

2.7 모델 미세 튜닝

이제 몇가지 모델들을 시도해서 모델을 선정했다고 가정해보자, 그렇다면 이제 모델을 미세 튜닝해야 한다. 어떻게 좋은 미세 튜닝을 찾을 수 있을까?

2.7.1 그리드 서치

가장 간단한 방법은 뭐 수동(...) 이다. 좋을때 까지 찾는거야~~~

이는 진짜 시간이 남아돌지 않거나 심심하지 않는 이상 추천하지 않겠다..

 

대신 책에서는 사이킷런의 GridSearchCV를 사용하는 것을 추천한다.

 

GridSearchCV

GridSearchCV는 하이퍼파라미터의 여러 조합을 시도하여 가장 성능이 좋은 모델을 찾는 방법이다.

 

이 예제에서는 RandomForestRegressor에 대한 하이퍼파라미터 튜닝을 보여주고 있으며, n_clusters와 max_features의 조합을 바꿔가며 최적의 값을 찾는다.

 

n_clusters는 클러스터의 수를 나타내며, max_features는 랜덤 포레스트가 각 트리에서 사용할 최대 피처 수를 의미한다.

from sklearn.model_selection import GridSearchCV

full_pipeline = Pipeline([
    ("preprocessing", preprocessing),
    ("random_forest", RandomForestRegressor(random_state=42)),
])
param_grid = [
    {'preprocessing__geo__n_clusters': [5, 8, 10],
     'random_forest__max_features': [4, 6, 8]},
    {'preprocessing__geo__n_clusters': [10, 15],
     'random_forest__max_features': [6, 8, 10]},
]
grid_search = GridSearchCV(full_pipeline, param_grid, cv=3,
                           scoring='neg_root_mean_squared_error')
grid_search.fit(housing, housing_labels)

 

RandomForestRegressor와 함께 파이프라인을 사용하여 여러 전처리 단계를 결합한 후, 하이퍼파라미터 그리드를 설정한다.

이 파이프라인은 데이터의 전처리와 모델 훈련 단계를 한 번에 처리할 수 있게 해준다!

 

  • param_grid는 시도할 하이퍼파라미터의 값들을 리스트 형식으로 설정한 것으로, 이 값들은 교차 검증을 통해 여러 번 반복하여 각 조합에 대한 모델 성능을 측정하고, 최적의 하이퍼파라미터 값을 찾는다.
  • 교차 검증은 모델이 특정한 데이터에 과적합되지 않도록 하기 위해 데이터를 여러 번 나누어 훈련하고 평가합니다. GridSearchCV는 이러한 교차 검증을 사용해 각 하이퍼파라미터 조합의 평균 성능을 측정합니다.
grid_search.best_params_
  • best_params_는 최종적으로 가장 성능이 좋았던 하이퍼파라미터 조합을 보여준다.

 

이 예제에서는 n_clusters=15와 max_features=6이 최적의 값으로 선택된 모습을 볼 수 있다.

cv_res = pd.DataFrame(grid_search.cv_results_)
cv_res.sort_values(by="mean_test_score", ascending=False, inplace=True)

# 추가 코드 – 데이터프레임을 깔끔하게 출력하기 위한 코드입니다
cv_res = cv_res[["param_preprocessing__geo__n_clusters",
                 "param_random_forest__max_features", "split0_test_score",
                 "split1_test_score", "split2_test_score", "mean_test_score"]]
score_cols = ["split0", "split1", "split2", "mean_test_rmse"]
cv_res.columns = ["n_clusters", "max_features"] + score_cols
cv_res[score_cols] = -cv_res[score_cols].round().astype(np.int64)

cv_res.head()

  • 이 과정을 통해 최적화된 모델의 평균 RMSE43953로, 기본 하이퍼파라미터 값을 사용한 모델보다 성능이 개선되었다!!!

 

2.7.2 랜덤 서치

  • 그리드 서치는 모든 가능한 하이퍼파라미터 조합을 시도하여 최적의 하이퍼파라미터를 찾는 방식이다. 하지만 탐색해야 할 조합의 수가 기하급수적으로 늘어나기 때문에 시간이 오래 걸릴 수 있다는 단점이 있다.
  • 랜덤 서치는 일정한 범위 내에서 하이퍼파라미터 값을 무작위로 샘플링해 최적값을 찾는다. 즉, 하이퍼파라미터 값이 연속적이거나 매우 많은 경우 랜덤 서치가 더 적합하다고 할 수 있다.
from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import randint

param_distribs = {'preprocessing__geo__n_clusters': randint(low=3, high=50),
                  'random_forest__max_features': randint(low=2, high=20)}

rnd_search = RandomizedSearchCV(
    full_pipeline, param_distributions=param_distribs, n_iter=10, cv=3,
    scoring='neg_root_mean_squared_error', random_state=42)

rnd_search.fit(housing, housing_labels)

 

 

# 추가 코드 – 랜덤 탐색 결과를 출력합니다
cv_res = pd.DataFrame(rnd_search.cv_results_)
cv_res.sort_values(by="mean_test_score", ascending=False, inplace=True)
cv_res = cv_res[["param_preprocessing__geo__n_clusters",
                 "param_random_forest__max_features", "split0_test_score",
                 "split1_test_score", "split2_test_score", "mean_test_score"]]
cv_res.columns = ["n_clusters", "max_features"] + score_cols
cv_res[score_cols] = -cv_res[score_cols].round().astype(np.int64)
cv_res.head()

 

평균 RSME 41987이 나온 모습을 확인할 수 있다.

HalvingRandomSearchCV와 HalvingGridSearchCV

HalvingRandomSearchCV와 HalvingGridSearchCV도 있는데 이 것들은

  • 이 방법들은 탐색 과정에서 성능이 나쁜 모델을 일찍 제외하고 자원을 절약하는 방식으로, 더 큰 하이퍼파라미터 공간을 탐색할 때 효율적이라고 한다.
  • 이 기법들은 기본적으로 자원이 적은 단계에서부터 시작하여 자원이 많이 필요한 모델로 점진적으로 훈련을 확장하는 방식이다.

2.7.3 앙상블 방법

앙상블 방법은 여러 개의 모델을 결합하여 단일 모델보다 더 나은 성능을 발휘하게 만드는 기법이다.!

 

  • 개별 모델의 성능을 개선하고자 할 때 앙상블을 사용한다. 앙상블은 여러 모델을 결합하여 하나의 강력한 모델을 만들 수 있으며, 이를 통해 단일 모델로는 얻을 수 없는 높은 성능을 기대할 수 있다.. (기대 기대)
  • 앙상블 방식은 특히 개별 모델이 서로 다른 특징을 가질 때 유리한데, 예를 들어, 결정 트리와 k-최근접 이웃 모델처럼 서로 다른 알고리즘을 사용한 모델을 결합하면 더 나은 예측 성능을 얻을 수 있다고 한다.

2.7.4 최상의 모델과 오차 분석

최적의 모델을 찾은 후, 이 모델이 어떤 특성을 얼마나 중요하게 다루는지 분석하는 과정으로

아래의 코드를 실행 했을때

final_model = rnd_search.best_estimator_  # 전처리 포함됨
feature_importances = final_model["random_forest"].feature_importances_
feature_importances.round(2)

sorted(zip(feature_importances,
           final_model["preprocessing"].get_feature_names_out()),
           reverse=True)

예를 들어, RandomForestRegressor 모델을 사용해 데이터에서 어떤 변수가 가장 예측에 중요한 역할을 하는지 알아낼 수 있다.

 

이를 통해 "log_median_income"과 같은 변수들이 예측에 중요한 기여를 한다는 점을 파악할 수 있고, 이 정보를 활용해 불필요한 변수를 제거하거나, 더 유용한 변수만 남겨 더 효율적인 모델을 구축할 수 있게 된다..!

2.7.5 테스트 세트로 시스템 평가하기

최종 모델을 선택한 후, 이 모델을 테스트 세트에 적용해 실제 성능을 평가하는 과정이다.

 

이 과정에서 모델의 최종 예측 정확도를 확인하고, 이를 통해 모델이 과적합이 발생했는지, 또는 일반화에 적합한지를 평가를 한다.  또한 평균 제곱 오차(RMSE)를 사용하여 모델의 예측력이 얼마만큼 정확한지 수치화하여 분석을 진행하면 된다.

X_test = strat_test_set.drop("median_house_value", axis=1)
y_test = strat_test_set["median_house_value"].copy()

final_predictions = final_model.predict(X_test)

final_rmse = mean_squared_error(y_test, final_predictions, squared=False)
print(final_rmse)

여기서 이 RSME가 얼마나 정확한지 궁금할 수가 있을 것이다.

 

이럴 때는 scipy의 통계적 방법을 사용하여 95% 신뢰 구간을 계산함으로써, 모델이 예측하는 값이 실제로 얼마나 신뢰할 수 있는지 평가할 수 있다. 이를 통해 예측 결과의 안정성을 더욱 높일 수 있다고 한다.

from scipy import stats

confidence = 0.95
squared_errors = (final_predictions - y_test) ** 2
np.sqrt(stats.t.interval(confidence, len(squared_errors) - 1,
                         loc=squared_errors.mean(),
                         scale=stats.sem(squared_errors)))

2.8 론칭, 모니터링, 시스템 유지 보수

스킵

2.9 직접 해보셈!

 

이로써 2장 정리가 끝이 났다.. 기억이 안나는 것도 있고 처음보는 것도 있는 것 같고.. 어지럽다 그냥

 

다음 장도 화이팅!!!!!!!!!!!! 할  수 이 ㅆ 겠지?