Validation with Visualization (1)

  • 데이터 시각화는 머신러닝 과정을 확인하기 좋습니다.
  • 하이퍼파라미터에 따라 확인할 값이 여럿 있고,
  • 숫자로 확인할 수도 있지만 눈에 잘 들어오지 않아 그림으로 표현해 보았습니다.

1. 데이터 & 분석 설정

What’s new in Matplotlib 3.4.0

  • 필요한 라이브러리들을 불러옵니다.
  • 업데이트된 matplotlib 버전 3.4.1을 사용합니다.
  • 새로 생긴 기능을 사용해 볼 것입니다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    %matplotlib inline

    import numpy as np
    import pandas as pd
    import matplotlib.pyplot as plt
    import seaborn as sns

    sns.set_context("talk")
    sns.set_style("white")
    sns.set_palette("Pastel1")

    import matplotlib as mpl
    mpl.__verion__
  • 실행 결과 :
    1
    '3.4.1'
  • 현업 데이터를 가져왔습니다.
  • 범주형 2개, 수치형 3개, 400줄짜리 단촐한 데이터입니다.
  • 수치형 변수를 맞추는 regression 문제입니다.
  • train : test = 8 : 2로 분리합니다.
  • 제겐 굳이 index를 뽑아서 분리하는 버릇이 있습니다.
  • 여차할 때 확인해보기 좋습니다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    df_vcv = pd.read_csv("df_vcv.csv")

    train_portion = 0.8

    np.random.seed(42)
    idx_train = np.random.choice(X.index, size=int(X.shape[0]*train_portion))
    idx_test = list(set(X.index) - set(idx_train))

    X_train = X.loc[idx_train]
    X_test = X.loc[idx_test]

    y_train = y.loc[idx_train]
    y_test = y.loc[idx_test]

2. 데이터 분포 확인

  • 쪼갠 trainset, testset을 확인합니다.
  • 데이터 분포를 비율로 그려서 trainset과 testset이 비슷한지 봅니다.
  • train을 green, test를 magenta로 나란히 그려서 비교합니다.
    코드 보기/접기
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    fig, axes = plt.subplots(ncols=3, nrows=2, figsize=(12, 8))
    axs = axes.ravel()

    for ax, col in zip(axs, df_vcv.columns):

    # X features
    if col != "target":
    if df_vcv[col].dtype == "O": # categorical
    df_train = df_vcv.loc[idx_train, col].value_counts().reset_index()
    df_test = df_vcv.loc[idx_test, col].value_counts().reset_index()

    width_cat = 0.4
    ax.bar(df_train.index-width_cat/2, df_train[col]/len(idx_train), width=width_cat, ec="g", fc="#AAFFAADD")
    ax.bar(df_test.index+width_cat/2, df_test[col]/len(idx_test), width=width_cat, ec="m", fc="#FFAAFFDD")
    ax.set_xticks(list(range(df_train.shape[0])))
    ax.set_xticklabels(df_train["index"].values)

    else: # numerical
    bins = np.linspace(df_vcv[col].min(), df_vcv[col].max(), 20)
    bins_center = (bins[:-1] + bins[1:])/2
    bins_delta = bins[1] - bins[0]
    counts_train, bins_train = np.histogram(df_vcv.loc[idx_train, col], bins=bins)
    counts_test, bins_test = np.histogram(df_vcv.loc[idx_test, col], bins=bins)

    width_num = bins_delta/2
    ax.bar(bins_center-width_num/2, counts_train/len(idx_train), width=width_num, ec="g", fc="#AAFFAADD")
    ax.bar(bins_center+width_num/2, counts_test/len(idx_test), width=width_num, ec="m", fc="#FFAAFFDD")

    # y feature
    else:
    sns.kdeplot(df_vcv.loc[idx_train, col], color="g", fill=True, ax=ax, label="train")
    sns.kdeplot(df_vcv.loc[idx_test, col], color="m", fill=True, ax=ax, label="test")
    ax.set_ylabel("")

    if col in ["A", "D"]:
    coltype = "categorical"
    else:
    coltype = "numerical"

    ax.set_title(f"{col} ({coltype})", pad=12)

    handles, labels = axs[5].get_legend_handles_labels()
    axs[2].legend(handles, labels, loc="upper left", bbox_to_anchor=(1,0.9))
    fig.tight_layout()
    fig.savefig("6_vcv_01.png")


3. Machine Learning

  • 데이터 전처리와 선형회귀에 필요한 라이브러리를 불러옵니다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # encoder
    from sklearn.preprocessing import OneHotEncoder

    # machine learning models
    from sklearn.preprocessing import StandardScaler
    from sklearn.preprocessing import PolynomialFeatures
    from sklearn.linear_model import LinearRegression

    # pipeline
    from sklearn.pipeline import Pipeline
    from sklearn.compose import ColumnTransformer

3.1. Pipeline 구축

sklearn: Column Transformer with Mixed Types

  • scaler와 one-hot encoder 등을 수행하며 데이터가 바뀌는 과정을 바라볼 수 있습니다.
  • 변환이 눈과 손에 익지 않았을 때는 유용한 방법이지만 깊이 분석하기엔 한계가 있습니다.
  • 예를 들어 feature importance를 도출할 때, one-hot encoding이 수행된 범주형 인자는 원소 하나하나에 대한 중요도가 나와버려 정작 인자의 중요도를 구하기 어렵습니다.
  • 체계적으로 분석할 수 있도록 pipeline을 구축합시다.
  • 범주형 데이터와 수치형 데이터에 다른 전처리를 수행합니다.
  • sklearn의 set_config(display='diagram')을 이용하면 그림으로 볼 수 있습니다.
    코드 보기/접기
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    # linear regression model
    def linear(degree):

    # categorical and numerical features
    cat_features = ["A", "D"]
    cat_transformer = OneHotEncoder(sparse=False)

    num_features = ["B", "C", "E"]
    num_transformer = Pipeline(steps=[("scaler", StandardScaler()),
    ("polynomial", PolynomialFeatures(degree=degree,
    include_bias=True))])

    # preprocessor
    preprocessor = ColumnTransformer(transformers=[("num", num_transformer, num_features),
    ("cat", cat_transformer, cat_features)
    ])

    # modeling
    model = Pipeline(steps=[("preprocessor", preprocessor),
    ("linear", LinearRegression())
    ])

    return model

    # pipeline output
    from sklearn import set_config
    set_config(display='diagram')

    model = linear(3)
    display(model)

pipeline 구조

  • set_config(display='text')를 통해 출력 모드를 바꿀 수 있습니다.
    1
    2
    set_config(display='text') 
    model.fit(X_train, y_train)
  • 실행 결과
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    Pipeline(steps=[('preprocessor',
    ColumnTransformer(transformers=[('num',
    Pipeline(steps=[('scaler',
    StandardScaler()),
    ('polynomial',
    PolynomialFeatures(degree=3))]),
    ['B', 'C', 'E']),
    ('cat',
    OneHotEncoder(sparse=False),
    ['A', 'D'])])),
    ('linear', LinearRegression())])

3.2. 평가: metrics

  • 파이프라인으로 구축한 머신러닝 모델의 성능을 평가합니다.

  • 평가 지표로는 MAE(mean absolute error), RMSE(root mean squared error), R2(coefficient of determination)를 사용합니다.

  • 모델과 데이터를 넣으면 예측결과평가지표를 뽑아주는 함수를 만듭니다.

    코드 보기/접기
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    # evaluation metrics
    from sklearn.metrics import mean_absolute_error as mae_
    from sklearn.metrics import mean_squared_error as mse_
    from sklearn.metrics import r2_score as r2_

    # 평가지표 계산 함수
    def get_metrics(model, X_train, X_test, y_train, y_test):
    # train
    y_pred_train = model.predict(X_train)
    mae_train = mae_(y_train, y_pred_train)
    rmse_train = np.sqrt(mse_(y_train, y_pred_train))
    r2_train = r2_(y_train, y_pred_train)

    # test
    y_pred_test = model.predict(X_test)
    mae_test = mae_(y_test, y_pred_test)
    rmse_test = mse_(y_test, y_pred_test, squared=False)
    r2_test = r2_(y_test, y_pred_test)

    return y_pred_train,y_pred_test, mae_train, mae_test, rmse_train, rmse_test, r2_train, r2_test

    # 평가지표 도출
    y_pred_train, y_pred_test, mae_train, mae_test, rmse_train, rmse_test, r2_train, r2_test = \
    get_metrics(model, X_train, X_test, y_train, y_test)

    # 평가지표 출력
    print("# train dataset")
    print(f" MAE : {mae_train:.3f}")
    print(f" RMSE: {rmse_train:.3f}")
    print(f" R2 : {r2_train:.3f}")
    print("\n# test dataset")
    print(f" MAE : {mae_test:.3f}")
    print(f" RMSE: {rmse_test:.3f}")
    print(f" R2 : {r2_test:.3f}")
  • 실행 결과:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # train dataset
    MAE : 0.394
    RMSE: 0.537
    R2 : 0.513

    # test dataset
    MAE : 0.427
    RMSE: 0.568
    R2 : 0.379

3.3. Visualization: Parity Plot

  • 머신러닝 모델의 예측력은 평가 지표와 같은 수치로 도출되지만 충분하지 않습니다.
  • 어떤 부분이 잘 맞고 어디가 틀린지 확인해야 보완할 수 있습니다.
  • 데이터 시각화를 통해 확인해 봅시다.
  • 참값과 예측값을 비교하는 방법으로 parity plot을 흔히 사용합니다.
  • 참값과 예측값이 $$y = x$$에 가까울 수록 좋습니다.
  • x와 y축 범위를 일치시켜 그립니다.
  • return ax로 axes를 받을 수 있도록 설계합니다.
    코드 보기/접기
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    def plot_parity(true, predict, c="c", mae=None, rmse=None, r2=None, equal=False,
    title=None, xlabel="true", ylabel="predict", ax=None):

    # if axes not defined, create one.
    if not ax:
    fig, ax = plt.subplots(figsize=(4, 4))

    # scatter plot
    ax.scatter(true, predict, c=c, s=10, alpha=0.3)

    # x, y limits
    min_ = min(ax.get_xbound()[0], ax.get_ybound()[0])
    max_ = max(ax.get_xbound()[1], ax.get_ybound()[1])
    ax.set_xlim(min_, max_)
    ax.set_ylim(min_, max_)

    # x, y ticks
    lb, ub = ax.get_ybound()

    ticks =[x for x in ax.get_xticks() if x >= lb and x <= ub]
    ax.set_xticks(ticks)
    ax.set_xticklabels(ticks)
    ax.set_yticks(ticks)
    ax.set_yticklabels(ticks)

    # grids
    ax.set_aspect("equal")
    ax.grid(axis="both", c="lightgray")
    if equal:
    ax.plot([lb, ub], [lb, ub], c="k", alpha=0.3)

    # x, y labels and title
    ax.set_xlabel(xlabel, fontsize=16, labelpad=8)
    ax.set_ylabel(ylabel, fontsize=16, labelpad=8)
    ax.set_title(title, fontsize=16, pad=8)

    # metrics
    ax.text(0.95, 0.3, f" MAE ={mae:0.3f}",
    transform=ax.transAxes, fontsize=16, ha="right")
    ax.text(0.95, 0.22, f"RMSE ={rmse:0.3f}",
    transform=ax.transAxes, fontsize=16, ha="right")
    ax.text(0.95, 0.14, f"R2 ={r2:0.3f}",
    transform=ax.transAxes, fontsize=16, ha="right")

    return ax
1
2
3
4
ax = plot_parity(y_train, y_pred_train, mae=mae_train, rmse=rmse_train, r2=r2_train, equal=True)
fig = ax.figure # axes가 속한 figure 접근
fig.tight_layout()
fig.savefig("6_vcv_02.png")


3.4. Visualization: Parity Plots

  • train data와 test 데이터를 함께 그리려고 합니다.
  • 방금 만든 plot_parity() 함수를 두 번 사용합니다.
    코드 보기/접기
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    def plot_parities(X_train, X_test, y_train, y_test, title):

    # get metrics
    y_pred_train, y_pred_test, mae_train, mae_test, rmse_train, rmse_test, r2_train, r2_test = \
    get_metrics(model, X_train, X_test, y_train, y_test)

    # figure prepartion
    fig, axs = plt.subplots(ncols=2, figsize=(8, 5), sharex=True, sharey=True)

    c_train, c_test = "g", "m"
    axs[0] = plot_parity(y_train, y_pred_train, c_train,
    mae_train, rmse_train, r2_train, title=f"train", ax=axs[0])
    min0, max0 = axs[0].get_xbound()

    axs[1] = plot_parity(y_test, y_pred_test, c_test,
    mae_test, rmse_test, r2_test, title=f"test", ax=axs[1])
    min1, max1 = axs[1].get_xbound()

    # mis setting
    axs[1].set_ylabel(None)
    min_, max_ = min(min0, min1), max(max0, max1)
    axs[1].set_xlim(min_, max_)
    axs[1].set_ylim(min_, max_)
    for ax in axs:
    ax.plot([min_, max_], [min_, max_], "k", alpha=0.3)

    fig.suptitle(title, fontsize=20, ha="center")
    fig.tight_layout()
    fig.savefig(f"{title}.png")
1
plot_parities(X_train, X_test, y_train, y_test, "linear regression (degree=3)")


  • train, test 결과가 모두 마음에 들지 않습니다.
  • 선형회귀 차수를 바꿔서 확인합시다.
  • 먼저, 1차 함수를 시도해 봅니다.
    코드 보기/접기
    1
    2
    3
    4
    5
    6
    7
    model = linear(1)
    model.fit(X_train, y_train)

    y_pred_train, y_pred_test, mae_train, mae_test, rmse_train, rmse_test, r2_train, r2_test = \
    get_metrics(model, X_train, X_test, y_train, y_test)

    plot_parities(X_train, X_test, y_train, y_test, "linear regression (degree=1)")


  • 역시 별로입니다.
  • 이번에는 5차 함수를 시도합니다.
    코드 보기/접기
    1
    2
    3
    4
    5
    6
    7
    model = linear(5)
    model.fit(X_train, y_train)

    y_pred_train, y_pred_test, mae_train, mae_test, rmse_train, rmse_test, r2_train, r2_test = \
    get_metrics(model, X_train, X_test, y_train, y_test)

    plot_parities(X_train, X_test, y_train, y_test, "linear regression (degree=5)")


  • 1, 3, 5차 모두 마음에 들지 않습니다.
  • 차수에 대한 경향을 보면 답에 가까워질 것 같습니다.
  • Grid Search를 사용하면 좋을 것 같습니다.

3.5. Grid Search: polynomial order

  • sklearn에서 제공하는 GridSearchCV는 다음에 씁시다.
  • 변수가 하나뿐이니 수동으로 바꾸어가면서 성능을 평가합니다.
    코드 보기/접기
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    def plot_metrics(X_train, X_test, y_train, y_test, title):
    maes_train, maes_test = [], []
    rmses_train, rmses_test = [], []
    r2s_train, r2s_test = [], []

    # polynomial degree
    for degree in range(1, 6):
    # model build
    model = linear(degree)
    model.fit(X_train, y_train)

    # model evaluation
    y_pred_train, y_pred_test, mae_train, mae_test, rmse_train, rmse_test, r2_train, r2_test = \
    get_metrics(model, X_train, X_test, y_train, y_test)

    # store metrics
    maes_train.append(mae_train)
    maes_test.append(mae_test)
    rmses_train.append(rmse_train)
    rmses_test.append(rmse_test)
    r2s_train.append(r2_train)
    r2s_test.append(r2_test)

    # data visualization
    fig, axs = plt.subplots(ncols=3, figsize=(12, 5))

    axs[0].plot(maes_train, maes_test, "ro-", lw=3)
    axs[1].plot(rmses_train, rmses_test, "go-", lw=3)
    axs[2].plot(r2s_train, r2s_test, "bo-", lw=3)

    # numbering: degree
    for i in range(len(maes_train)):
    axs[0].text(maes_train[i], maes_test[i], str(i+1), ha="center", fontsize=14,
    bbox = {"boxstyle":"circle", "facecolor":"w", "edgecolor":"r"})
    axs[1].text(rmses_train[i], rmses_test[i], str(i+1), ha="center", fontsize=14,
    bbox = {"boxstyle":"circle", "facecolor":"w", "edgecolor":"g"})
    axs[2].text(r2s_train[i], r2s_test[i], str(i+1), ha="center", fontsize=14,
    bbox = {"boxstyle":"circle", "facecolor":"w", "edgecolor":"b"})

    # mis
    for ax, metric in zip(axs, ["MAE", "RMSE", "R2"]):
    min_ = min(ax.get_xlim()[0], ax.get_ylim()[0])
    max_ = max(ax.get_xlim()[1], ax.get_ylim()[1])
    ax.plot([min_, max_], [min_, max_], "-k", alpha=0.3)
    ax.set_xlim(min_, max_)
    ax.set_ylim(min_, max_)
    ax.set_title(metric, pad=12)
    ax.set_xlabel("train")
    ticks = [round(x,1) for x in ax.get_xticks() if x <= ax.get_xbound()[1] and x >= ax.get_xbound()[0]]
    ax.set_xticks(ticks)
    ax.set_xticklabels(ticks)
    ax.set_yticks(ticks)
    ax.set_yticklabels(ticks)
    ax.grid()


    axs[0].set_ylabel("test")

    fig.suptitle(f"{title}", fontsize=24)
    fig.tight_layout()
    fig.savefig(f"{title.replace('(','_').replace(')','_')}.png")


  • 가로축은 trainset, 세로축은 testset입니다.
  • 차수가 거듭될수록 MAE와 RMSE, R2 모두 overfitting이 관찰됩니다.
  • 1차2차를 고르면 되나 싶지만, 한번 더 확인합시다.
  • train과 test 데이터 분할을 다시 해서 확인합니다.
  • split에 사용된 numpy random seed를 교체해서 데이터를 다시 만들고 똑같이 진행합니다.
    코드 보기/접기
    1
    2
    3
    4
    5
    6
    7
    8
    9
    np.random.seed(0)
    idx_train_2 = np.random.choice(X.index, size=int(X.shape[0]*train_portion))
    idx_test_2 = list(set(X.index) - set(idx_train_2))

    X_train_2 = X.loc[idx_train_2]
    X_test_2 = X.loc[idx_test_2]

    y_train_2 = y.loc[idx_train_2]
    y_test_2 = y.loc[idx_test_2]
1
plot_metrics(X_train_2, X_test_2, y_train_2, y_test_2, "polynomial degree vs metrics (2)")


  • 1차보다 2차가 $$y = x$$에 가깝습니다.
  • 그런데 그게 문제가 아닐 것 같습니다. 거동이 완전히 바뀌었습니다.
  • 데이터에 따른 편차가 상당히 큽니다.

3.6. Grid Search Cross Validation: polynomial order

sklearn: GridSearchCV

  • 그런데 하나 잊은 것이 있습니다.
  • test data는 최종 검증용으로만 사용해야지, 이렇게 성능을 테스트셋에 맞추면 결국 테스트셋에 오버피팅될 뿐입니다.
  • 이건 안봤다고 치고 덮어놓고, train data만 가지고 최적을 찾아봅시다.
  • Grid Search + Cross Validation이 필요합니다.
  • 다항식의 차수만 바꾸면서 각각의 cross validataion score를 측정합니다.
  • cv=5로 다섯 번 수행한 교차검증의 평균표준편차를 구합니다.
  • 성능이 좋으려면 평균이 높아야 하고, 표준편차는 작아야 합니다.
  • 교차검증을 신뢰하려면 적어도 표준편차라도 작아야 합니다.
  • 과연 그럴지 확인합니다.
    코드 보기/접기
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    def plot_cv(X_train, y_train, title):
    fig, axs = plt.subplots(ncols=3, figsize=(12, 5))

    # polynomial degree
    for degree in range(1, 6):
    # model build
    model = linear(degree)

    # cross validation
    for scoring, ax in zip(["neg_mean_absolute_error", "neg_mean_squared_error", "r2"], axs):
    scores = cross_val_score(model, X_train, y_train, scoring=scoring, cv=10)
    if scoring.startswith("neg_"):
    scores = -scores

    # bar plot
    cvs = ax.bar(degree, scores.mean())
    # bar label : bar마다 데이터값 표기
    ax.bar_label(cvs, fmt="%.2f", fontsize=14, padding=5)
    # error bar 표현
    ax.errorbar(degree, scores.mean(), yerr=scores.std(),
    ecolor="darkgray", capsize=5, capthick=2)

    for title_, ax in zip(["MAE", "RMSE", "R2"], axs):
    ax.set_title(title_, pad=12)
    ax.grid(axis="y", c="lightgray", ls=":")
    xticks = list(range(1, 6))
    ax.set_xticks(xticks)
    ax.set_xticklabels(xticks)

    fig.suptitle(f"{title}", fontsize=24)
    fig.tight_layout()
    fig.savefig(f"{title.replace('(','_').replace(')','_')}.png")
  • ax.bar()yerr를 사용하면 굳이 ax.errorbar() 없이도 에러바 표현이 가능합니다.
  • 그러나 이 경우 모처럼 matplotlib 3.4에 처음 도입된 ax.bar_label()errorbar 위에 붙어 보기 힘듭니다.
  • Grid Search 결과를 곧장 그려봅니다.
    1
    plot_cv(X_train, y_train, "polynomial order vs metrics (cross validation)")

  • 바꾸었던 데이터를 적용해 봅니다.
    1
    plot_cv(X_train, y_train, "polynomial order vs metrics 2 (cross validation)")

  • 두 경우 모두, 결과를 신뢰하기에는 RMSE와 R2의 표준편차가 너무나 큽니다.
  • 특히 MAE보다 RMSE의 표준편차가 크다는 점에서 이상치의 역할이 큼을 짐작할 수 있습니다.
  • 5차에서는 $$R^2 < 0$$ 마저 등장하는데, 모든 범위에서 선형 회귀가 힘을 쓰지 못합니다.
  • 선형 회귀가 엉망이라는 점에서 비선형성이 매우 강하다는 추측을 할 수 있습니다.
  • 다음 글에서는 비선형 회귀에 도전하겠습니다.


도움이 되셨나요? 카페인을 투입하시면 다음 포스팅으로 변환됩니다

Share