by museonghwang

평균 이동(Mean Shift)

|

평균 이동(Mean Shift)K-평균과 유사하게 중심을 군집의 중심으로 지속적으로 움직이면서 군집화를 수행 합니다. 하지만 K-평균이 중심에 소속된 데이터의 평균 거리 중심으로 이동하는 데 반해, 평균 이동은 중심을 데이터가 모여 있는 밀도가 가장 높은 곳으로 이동시킵니다.

평균 이동 군집화는 데이터의 분포도를 이용 해 군집 중심점을 찾으며 군집 중심점은 데이터 포인트가 모여있는 곳이라는 생각 에서 착안하여, 이를 위해 확률 밀도 함수(probability density function)를 이용 합니다. 가장 집중적으로 데이터가 모여있어 확률 밀도 함수가 피크인 점을 군집 중심점으로 선정 하며 일반적으로 주어진 모델의 확률 밀도 함수를 찾기 위해서 KDE(Kernel Density Estimation)를 이용 합니다.

평균 이동 군집화는 특정 데이터를 반경 내의 데이터 분포 확률 밀도가 가장 높은 곳으로 이동하기 위해 주변 데이터와의 거리 값을 KDE 함수 값으로 입력한 뒤 그 반환 값을 현재 위치에서 업데이트하면서 이동하는 방식 을 취합니다. 이러한 방식을 전체 데이터에 반복적으로 적용하면서 데이터의 군집 중심점을 찾아냅니다.

image


KDE(Kernel Density Estimation)커널(Kernel) 함수를 통해 어떤 변수의 확률 밀도 함수를 추정하는 대표적인 방법 입니다. 관측된 데이터 각각에 커널 함수를 적용한 값을 모두 더한 뒤 데이터 건수로 나눠 확률 밀도 함수를 추정 합니다. 확률 밀도 함수 PDF(Probability Density Function)는 확률 변수의 분포를 나타내는 함수로, 확률 밀도 함수를 알면 특정 변수가 어떤 값을 갖게 될지에 대한 확률을 알게 되므로 이를 통해 변수의 특성(평균, 분산 등), 확률 분포 등 변수의 많은 요소를 알 수 있습니다.

KDE는 개별 관측 데이터에 커널 함수를 적용한 뒤, 이 적용 값을 모두 더한 후 개별 관측 데이터의 건수로 나눠 확률 밀도 함수를 추정하며, 대표적인 커널 함수로서 가우시안 분포 함수가 사용됩니다. 다음 그림의 왼쪽은 개별 관측 데이터에 가우시안 커널 함수를 적용한 것이고 오른쪽은 적용 값을 모두 더한 KDE 결과입니다.

image


KDE는 다음과 같은 커널 함수식으로 표현됩니다. 다음 식에서 $K$ 는 커널 함수, $x$는 확률 변숫값, $x_i$ 는 관측값, $h$ 는 대역폭(bandwidth) 입니다.

[KDE = \cfrac{1}{n}\sum^n_{i=1}K_h(x-x_i)=\cfrac{1}{nh}\sum^n_{i=1}K\left(\cfrac{x-x_i}{h}\right)]


대역폭 $h$ 는 KDE 형태를 부드러운(또는 뾰족한) 형태로 평활화(Smoothing) 하는 데 적용되며, 이 $h$ 를 어떻게 설정하느냐에 따라 확률 밀도 추정 성능을 크게 좌우 할 수 있습니다.

다음 그림은 $h$ 값을 증가시키면서 변화되는 KDE 를 나타냅니다.

  • 작은 값(h=1.0)
    • 좁고 뾰족한 KDE를 가짐.
    • 이는 변동성이 큰 방식으로 확률 밀도 함수를 추정하므로 과적합(over-fitting)하기 쉬움.
  • 매우 큰 h 값(h=10)
    • 과도하게 평활화(smoothing)된 KDE로 인해 지나치게 단순화된 방식으로 확률 밀도 함수를 추정
    • 결과적으로 과소적합(under-fitting)하기 쉬움.


image


따라서 적절한 KDE의 대역폭 $h$ 를 계산하는 것KDE 기반의 평균 이동(Mean Shift) 군집화에서 매우 중요 합니다.

일반적으로 평균 이동 군집화대역폭이 클수록 평활화된 KDE로 인해 적은 수의 군집 중심점 을 가지며 대역폭이 적을수록 많은 수의 군집 중심점 을 가집니다. 또한 평균 이동 군집화는 군집의 개수를 지정하지 않으며, 오직 대역폭의 크기에 따라 군집화를 수행합니다.


사이킷런은 평균 이동 군집화를 위해 MeanShift 클래스를 제공합니다. MeanShift 클래스의 가장 중요한 초기화 파라미터는 bandwidth 이며 이 파라미터는 KDE 의 대역폭 $h$ 와 동일합니다. 대역폭 크기 설정이 군집화의 품질에 큰 영향 을 미치기 때문에 사이킷런은 최적의 대역폭 계산을 위해 estimate_bandwidth() 함수를 제공합니다.

다음 예제는 make_blobs()cluster_std 를 0.7로 정한 3개 군집의 데이터에 대해 bandwidth 를 0.8로 설정한 평균 이동 군집화 알고리즘을 적용한 예제입니다.

import numpy as np
from sklearn.datasets import make_blobs
from sklearn.cluster import MeanShift

X, y = make_blobs(
    n_samples=200,
    n_features=2,
    centers=3,
    cluster_std=0.7,
    random_state=0
)

meanshift = MeanShift(bandwidth=0.8)
cluster_labels = meanshift.fit_predict(X)
print('cluster labels 유형:', np.unique(cluster_labels))
[output]
cluster labels 유형: [0 1 2 3 4 5]


군집이 0부터 5까지 6개로 분류됐습니다. 지나치게 세분화돼 군집화됐습니다. 일반적으로 bandwidth 값을 작게 할수록 군집 개수가 많아집니다. 이번에 bandwidth 를 살짝 높인 1.0으로 해서 MeanShift 를 수행해 보겠습니다.

meanshift = MeanShift(bandwidth=1)
cluster_labels = meanshift.fit_predict(X)
print('cluster labels 유형:', np.unique(cluster_labels))
[output]
cluster labels 유형: [0 1 2]


3개의 군집으로 잘 군집화됐습니다. 데이터의 분포 유형에 따라 bandwidth 값의 변화는 군집화 개수에 큰 영향을 미칠 수 있습니다. 따라서 MeanShift에서는 이 bandwidth를 최적화 값으로 설정하는 것이 매우 중요합니다.

사이킷런은 최적화된 bandwidth 값을 찾기 위해서 estimate_bandwidth() 함수를 제공하며, 파라미터로 피처 데이터 세트를 입력해주면 최적화된 bandwidth 값을 반환해줍니다.

from sklearn.cluster import estimate_bandwidth

bandwidth = estimate_bandwidth(X)
print('bandwidth 값:', round(bandwidth, 3))
[output]
bandwidth 값: 1.816


estimate_bandwidth() 로 측정된 bandwidth 를 평균 이동 입력값으로 적용해 동일한 make_blobs() 데이터 세트에 군집화를 수행해 보겠습니다.

import pandas as pd

clusterDF = pd.DataFrame(data=X, columns=['ftr1', 'ftr2'])
clusterDF['target'] = y

# estimate_bandwidth()로 최적의 bandwidth 계산
best_bandwidth = estimate_bandwidth(X)

meanshift = MeanShift(bandwidth=best_bandwidth)
cluster_labels = meanshift.fit_predict(X)
print('cluster labels 유형:', np.unique(cluster_labels))
[output]
cluster labels 유형: [0 1 2]


3개의 군집으로 구성됨을 알 수 있습니다. 구성된 3개의 군집을 시각화해 보겠습니다. 평균 이동도 K-평균 과 유사하게 중심을 가지고 있으므로 cluster_centers_ 속성으로 군집 중심 좌표를 표시할 수 있습니다.

import matplotlib.pyplot as plt
%matplotlib inline

clusterDF['meanshift_label'] = cluster_labels
centers = meanshift.cluster_centers_
unique_labels = np.unique(cluster_labels)
markers = ['o', 's', '^', 'x', '*']

for label in unique_labels:
    label_cluster = clusterDF[clusterDF['meanshift_label']==label]
    center_x_y = centers[label]
    
    # 군집별로 다른 마커로 산점도 적용
    plt.scatter(x=label_cluster['ftr1'], y=label_cluster['ftr2'], edgecolor='k', marker=markers[label] )
    
    # 군집별 중심 표현
    plt.scatter(x=center_x_y[0], y=center_x_y[1], s=200, color='gray', alpha=0.9, marker=markers[label])
    plt.scatter(x=center_x_y[0], y=center_x_y[1], s=70, color='k', edgecolor='k', marker='$%d$' % label)

plt.show()

image


target 값과 군집 label 값을 비교해 보겠습니다. Target 값과 군집 label 값이 1:1로 잘 매칭됐습니다.

print(clusterDF.groupby('target')['meanshift_label'].value_counts())
[output]
target  meanshift_label
0       0                  67
1       1                  67
2       2                  66
Name: meanshift_label, dtype: int64


평균 이동의 장점 은 데이터 세트의 형태를 특정 형태로 가정한다든가, 특정 분포도 기반의 모델로 가정하지 않기 때문에 좀 더 유연한 군집화가 가능 하고, 이상치의 영향력도 크지 않으며, 미리 군집의 개수를 정할 필요도 없습니다. 하지만 알고리즘의 수행 시간이 오래 걸리고 무엇보다도 bandwidth의 크기에 따른 군집화 영향도가 매우 큽니다.

Read more

군집 평가(Cluster Evaluation)

|

군집화 결과는 레이블과 비교해 군집화가 얼마나 효율적으로 됐는지 짐작할 수 있지만, 대부분의 군집화 데이터 세트는 이렇게 비교할 만한 타깃 레이블을 가지고 있지 않습니다. 또한 군집화는 분류(Classification)와 유사해 보일 수 있으나 성격이 많이 다릅니다. 데이터 내에 숨어 있는 별도의 그룹을 찾아서 의미를 부여하거나 동일한 분류 값에 속하더라도 그 안에서 더 세분화된 군집화를 추구하거나 서로 다른 분류 값의 데이터도 더 넓은 군집화 레벨화 등의 영역을 가지고 있습니다.

비지도학습의 특성상 어떠한 지표라도 정확하게 성능을 평가하기는 어렵습니다. 그럼에도 불구하고 군집화의 성능을 평가하는 대표적인 방법으로 실루엣 분석을 이용합니다.


실루엣 분석

군집화 평가 방법으로 실루엣 분석(silhouette analysis) 이 있습니다. 실루엣 분석은 각 군집 간의 거리가 얼마나 효율적으로 분리돼 있는지 를 나타냅니다. 효율적으로 잘 분리됐다는 것은 다른 군집과의 거리는 떨어져 있고 동일 군집끼리의 데이터는 서로 가깝게 잘 뭉쳐 있다는 의미 입니다. 군집화가 잘될수록 개별 군집은 비슷한 정도의 여유공간을 가지고 떨어져 있을 것입니다.

실루엣 분석은 실루엣 계수(silhouette coefficient)를 기반 으로 합니다. 실루엣 계수는 개별 데이터가 가지는 군집화 지표 입니다. 개별 데이터가 가지는 실루엣 계수해당 데이터가 같은 군집 내의 데이터와 얼마나 가깝게 군집화돼 있고, 다른 군집에 있는 데이터와는 얼마나 멀리 분리돼 있는지를 나타내는 지표입니다.

image


특정 데이터 포인트의 실루엣 계수 값 은 해당 데이터 포인트와 같은 군집 내에 있는 다른 데이터 포인트와의 거리를 평균한 값 a(i), 해당 데이터 포인트가 속하지 않은 군집 중 가장 가까운 군집과의 평균거리 b(i) 를 기반으로 계산됩니다. 두 군집 간의 거리가 얼마나 떨어져 있는가의 값은 b(i) - a(i) 이며, 이 값을 정규화하기 위해 MAX(a(i), b(i)) 값으로 나눕니다. 따라서 i 번째 데이터 포인트의 실루엣 계수값 s(i) 는 다음과 같이 정의합니다.

[s(i)=\cfrac{b(i)-a(i)}{max(a(i), b(i))}]


실루엣 계수-1 에서 1 사이의 값 을 가집니다.

  • 1에 가까움 : 근처의 군집과 더 멀리 떨어져 있다는 것
  • 0에 가까움 : 근처의 군집과 가까워진다는 것
  • 음수 : 아예 다른 군집에 데이터 포인트가 할당됐음을 뜻함.


사이킷런은 이러한 실루엣 분석을 위해 다음과 같은 메서드를 제공합니다.

sklearn.metrics.silhouette_samples(X, labels, metric='euclidean', **kwds)
  • 인자로 X feature 데이터 세트와 각 피처 데이터 세트가 속한 군집 레이블 값인 labels 데이터를 입력해주면 각 데이터 포인트의 실루엣 계수를 계산해 반환합니다.
sklearn.metrics.silhouette_score(X, labels, metric='euclidean', sample_size=None, **kwds)
  • 인자로 X feature 데이터 세트와 각 피처 데이터 세트가 속한 군집 레이블 값인 labels 데이터를 입력해주면 전체 데이터의 실루엣 계수 값을 평균해 반환합니다. 일반적으로 이 값이 높을수록 군집화가 어느정도 잘 됐다고 판단할 수 있습니다. 하지만 무조건 이 값이 높다고 해서 군집화가 잘 됐다고 판단할 수는 없습니다.


좋은 군집화가 되려면 다음 기준 조건을 만족해야 합니다.

  1. 전체 실루엣 계수의 평균값, 즉 사이킷런의 silhouette_score() 값은 0 ~ 1 사이의 값을 가지며, 1에 가까울수록 좋습니다.
  2. 하지만 전체 실루엣 계수의 평균값과 더불어 개별 군집의 평균값의 편차가 크지 않아야 합니다. 즉, 개별 군집의 실루엣 계수 평균값이 전체 실루엣 계수의 평균값에서 크게 벗어나지 않는 것이 중요합니다. 만약 전체 실루엣 계수의 평균값은 높지만, 특정 군집의 실루엣 계수 평균값만 유난히 높고 다른 군집들의 실루엣 계수 평균값은 낮으면 좋은 군집화 조건이 아닙니다.


붓꽃 데이터 세트를 이용한 군집 평가

앞의 붓꽃 데이터 세트의 군집화 결과를 실루엣 분석으로 평가해 보겠습니다. 이를 위해 sklearn.metrics 모듈의 silhouette_samples()silhouette_score() 를 이용합니다.

from sklearn.preprocessing import scale
from sklearn.datasets import load_iris
from sklearn.cluster import KMeans
# 실루엣 분석 metric 값을 구하기 위한 API 추가
from sklearn.metrics import silhouette_samples, silhouette_score

import matplotlib.pyplot as plt
%matplotlib inline
import numpy as np
import pandas as pd

iris = load_iris()
feature_names = ['sepal_length', 'sepal_width', 'petal_length', 'petal_width']
irisDF = pd.DataFrame(data=iris.data, columns=feature_names)

kmeans = KMeans(
    n_clusters=3,
    init='k-means++',
    max_iter=300,
    random_state=0
).fit(irisDF)
irisDF['cluster'] = kmeans.labels_

# iris 의 모든 개별 데이터에 실루엣 계수값을 구함. 
score_samples = silhouette_samples(iris.data, irisDF['cluster'])
print('silhouette_samples( ) return 값의 shape' , score_samples.shape)

# irisDF에 실루엣 계수 컬럼 추가
irisDF['silhouette_coeff'] = score_samples

# 모든 데이터의 평균 실루엣 계수값을 구함. 
average_score = silhouette_score(iris.data, irisDF['cluster'])
print('붓꽃 데이터셋 Silhouette Analysis Score:{0:.3f}'.format(average_score))

irisDF.head(3)
[output]
silhouette_samples( ) return 값의 shape (150,)
붓꽃 데이터셋 Silhouette Analysis Score:0.553

image


붓꽃 데이터 세트의 평균 실루엣 계수 값은 약 0.553 입니다. irisDF의 맨 처음 3개 로우는 1번 군집에 해당하고 개별 실루엣 계수 값이 0.8529, 0.8154, 0.8293 일 정도로 1번 군집의 경우 평균적으로 약 0.8 정도의 높은 실루엣 계수 값을 나타냅니다. 하지만 1번 군집이 아닌 다른 군집의 경우 실루엣 계수값이 낮기 때문에 전체 평균 실루엣 계수 값이 0.553 정도가 되었습니다.

군집별 평균 실루엣 계수 값으로 확인해 보겠습니다.

irisDF.groupby('cluster')['silhouette_coeff'].mean()
[output]
cluster
0    0.417320
1    0.798140
2    0.451105
Name: silhouette_coeff, dtype: float64


1번 군집은 실루엣 계수 평균 값이 약 0.79인데 반해, 0번은 약 0.41, 2번은 0.45로 상대적으로 평균값이 1번에 비해 낮습니다.


군집별 평균 실루엣 계수의 시각화를 통한 군집 개수 최적화 방법

전체 데이터의 평균 실루엣 계수 값이 높다고 해서 반드시 최적의 군집 개수로 군집화가 잘 됐다고 볼 수는 없습니다. 특정 군집 내의 실루엣 계수값만 너무 높고, 다른 군집은 내부 데이터끼리의 거리가 너무 떨어져 있어 실루엣 계수값이 낮아져도 평균적으로 높은 값을 가질 수 있습니다. 개별 군집별로 적당히 분리된 거리를 유지하면서도 군집 내의 데이터가 서로 뭉쳐 있는 경우에 K-평균 의 적절한 군집 개수가 설정됐다고 판단할 수 있습니다.


여러 개의 군집 개수가 주어졌을 때 평균 실루엣 계수로 군집 개수를 최적화하는 방법을 알아보겠습니다. 사이킷런 문서 중 시각적으로 지원해주는 좋은 예제가 있습니다.

첫 번째 경우는 다음 그림과 같이 주어진 데이터에 대해서 군집의 개수 2개를 정했을 때, 평균 실루엣 계수, 즉 silhouette_score는 약 0.704로 매우 높게 나타났습니다. 다음 그림에서 왼쪽 부분은 개별 군집에 속하는 데이터의 실루엣 계수를 2차원으로 나타낸 것입니다. X축은 실루엣 계수 값이고, Y축은 개별 군집과 이에 속하는 데이터입니다. 개별 군집은 Y축에 숫자 값으로 0, 1로 표시돼 있으며 이에 해당하는 데이터는 Y축 높이로 추측할 수 있습니다. 그리고 점선으로 표시된 선은 전체 평균 실루엣 계수값을 나타냅니다. 이로 판단해 볼 때 1번 군집의 모든 데이터는 평균 실루엣 계수값 이상이지만, 2번 군집의 경우는 평균보다 적은 데이터 값이 매우 많습니다.

오른쪽에 있는 그림으로 그 이유를 보충해서 설명할 수 있습니다. 1번 군집의 경우는 0번 군집과 멀리 떨어져 있고, 내부 데이터끼리도 잘 뭉쳐 있습니다. 하지만 0번 군집의 경우는 내부 데이터끼리 많이 떨어져 있는 모습입니다.

image


다음 그림은 군집 개수가 3개일 경우입니다. 전체 데이터의 평균 실루엣 계수 값은 약 0.588입니다. 1번, 2번 군집의 경우 평균보다 높은 실루엣 계수 값을 가지고 있지만, 0번의 경우 모두 평균보다 낮습니다. 오른쪽 그림을 보면 0번의 경우 내부 데이터 간의 거리도 멀지만, 2번 군집과도 가깝게 위치하고있기 때문입니다.

image


다음으로 군집이 4개인 경우를 보겠습니다. 이때의 평균 실루엣 계수 값은 약 0.65 입니다. 왼쪽 그림에서 보듯이 개별 군집의 평균 실루엣 계수값이 비교적 균일하게 위치하고 있습니다. 1번 군집의 경우 모든 데이터가 평균보다 높은 계수 값을 가지고 있으며, 0번, 2번의 경우는 절반 이상이 평균보다 높은 계수 값을, 3번 군집의 경우만 약 1/3 정도가 평균보다 높은 계수값을 가지고 있습니다. 군집이 2개인 경우보다는 평균 실루엣 계수값이 작지만 4개인 경우가 가장 이상적인 군집화 개수로 판단할 수 있습니다.

image


군집별 평균 실루엣 계수 값을 구하는 부분을 시각화해 보겠습니다. visualize_silhouette() 함수는 군집 개수를 변화시키면서 K-평균 군집을 수행했을 때 개별 군집별 평균 실루엣 계수 값을 시각화해서 군집의 개수를 정하는 데 도움을 줍니다.

make_blobs() 함수를 통해 4개 군집 중심의 500개 2차원 데이터 세트를 만들고 이를 K-평균 으로 군집화할 때 2개, 3개, 4개, 5개 중 최적의 군집 개수를 시각화로 알아보겠습니다.

**visualize_silhouette()**
### 여러개의 클러스터링 갯수를 List로 입력 받아 각각의 실루엣 계수를 면적으로 시각화한 함수 작성
def visualize_silhouette(cluster_lists, X_features): 
    
    from sklearn.datasets import make_blobs
    from sklearn.cluster import KMeans
    from sklearn.metrics import silhouette_samples, silhouette_score

    import matplotlib.pyplot as plt
    import matplotlib.cm as cm
    import math
    
    # 입력값으로 클러스터링 갯수들을 리스트로 받아서, 각 갯수별로 클러스터링을 적용하고 실루엣 개수를 구함
    n_cols = len(cluster_lists)
    
    # plt.subplots()으로 리스트에 기재된 클러스터링 수만큼의 sub figures를 가지는 axs 생성 
    fig, axs = plt.subplots(figsize=(4*n_cols, 4), nrows=1, ncols=n_cols)
    
    # 리스트에 기재된 클러스터링 갯수들을 차례로 iteration 수행하면서 실루엣 개수 시각화
    for ind, n_cluster in enumerate(cluster_lists):
        
        # KMeans 클러스터링 수행하고, 실루엣 스코어와 개별 데이터의 실루엣 값 계산. 
        clusterer = KMeans(n_clusters = n_cluster, max_iter=500, random_state=0)
        cluster_labels = clusterer.fit_predict(X_features)
        
        sil_avg = silhouette_score(X_features, cluster_labels)
        sil_values = silhouette_samples(X_features, cluster_labels)
        
        y_lower = 10
        axs[ind].set_title('Number of Cluster : '+ str(n_cluster)+'\n' \
                          'Silhouette Score :' + str(round(sil_avg,3)) )
        axs[ind].set_xlabel("The silhouette coefficient values")
        axs[ind].set_ylabel("Cluster label")
        axs[ind].set_xlim([-0.1, 1])
        axs[ind].set_ylim([0, len(X_features) + (n_cluster + 1) * 10])
        axs[ind].set_yticks([])  # Clear the yaxis labels / ticks
        axs[ind].set_xticks([0, 0.2, 0.4, 0.6, 0.8, 1])
        
        # 클러스터링 갯수별로 fill_betweenx( )형태의 막대 그래프 표현. 
        for i in range(n_cluster):
            ith_cluster_sil_values = sil_values[cluster_labels==i]
            ith_cluster_sil_values.sort()
            
            size_cluster_i = ith_cluster_sil_values.shape[0]
            y_upper = y_lower + size_cluster_i
            
            color = cm.nipy_spectral(float(i) / n_cluster)
            axs[ind].fill_betweenx(np.arange(y_lower, y_upper), 0, ith_cluster_sil_values, \
                                facecolor=color, edgecolor=color, alpha=0.7)
            axs[ind].text(-0.05, y_lower + 0.5 * size_cluster_i, str(i))
            y_lower = y_upper + 10
            
        axs[ind].axvline(x=sil_avg, color="red", linestyle="--")


# make_blobs 을 통해 clustering 을 위한 4개의 클러스터 중심의 500개 2차원 데이터 셋 생성  
from sklearn.datasets import make_blobs
X, y = make_blobs(
    n_samples=500,
    n_features=2,
    centers=4,
    cluster_std=1,
    center_box=(-10.0, 10.0),
    shuffle=True,
    random_state=1
)  

# cluster 개수를 2개, 3개, 4개, 5개 일때의 클러스터별 실루엣 계수 평균값을 시각화 
visualize_silhouette([2, 3, 4, 5], X)

image


앞에서 소개한 바와 마찬가지로 4개의 군집일 때 가장 최적이 됨을 알 수 있습니다. 이번에는 붓꽃 데이터를 이용해 K-평균 수행 시 최적의 군집 개수를 알아보겠습니다.

from sklearn.datasets import load_iris

iris=load_iris()
visualize_silhouette([2, 3, 4, 5], iris.data)

image


붓꽃 데이터를 K-평균으로 군집화할 경우에는 군집 개수를 2개로 하는 것이 가장 좋아 보입니다. 3개의 경우 평균 실루엣 계수 값도 2개보다 작을뿐더러 1번 군집과 다른 0번, 2번 군집과의 실루엣 계수의 편차가 큽니다. 4개, 5개의 경우도 마찬가지입니다.

실루엣 계수를 통한 K-평균 군집 평가 방법은 직관적으로 이해하기 쉽지만, 단점으로 각 데이터별로 다른 데이터와의 거리를 반복적으로 계산해야 하므로 데이터양이 늘어나면 수행 시간이 크게 늘어납니다.

Read more

K-평균 알고리즘

|

K-평균군집화(Clustering)에서 가장 일반적으로 사용되는 알고리즘 으로 군집 중심점(centroid)이라는 특정한 임의의 지점을 선택해 해당 중심에 가장 가까운 포인트들을 선택하는 군집화 기법 입니다.

군집 중심점 은 선택된 포인트의 평균 지점으로 이동하고 이동된 중심점에서 다시 가까운 포인트를 선택, 다시 중심점을 평균 지점으로 이동하는 프로세스를 반복적으로 수행합니다. 모든 데이터 포인트에서 더 이상 중심점의 이동이 없을 경우에 반복을 멈추고 해당 중심점에 속하는 데이터 포인트들을 군집화 하는 기법입니다.

image


  1. 먼저 중심을 구성하려는 군집화 개수만큼 임의의 위치에 가져다 놓습니다. 전체 데이터를 2개로 군집화하려면 2개의 중심을 임의의 위치에 가져다 놓습니다.
  2. 각 데이터는 가장 가까운 곳에 위치한 중심점에 소속됩니다. 위 그림에서는 A, B 데이터가 같은 중심점에 소속되며,C, E, F 데이터가 같은 중심점에 소속됩니다.
  3. 이렇게 소속이 결정되면 군집 중심점을 소속된 데이터의 평균 중심으로 이동합니다. 위 그림에서는 A, B 데이터 포인트의 평균 위치로 중심점이 이동했고, 다른 중심점 역시 C, E, F 데이터 포인트의 평균 위치로 이동했습니다.
  4. 각 데이터는 기존에 속한 중심점보다 더 가까운 중심점이 있다면 해당 중심점으로 다시소속을 변경합니다. 위 그림에서는 C 데이터가 기존의 중심점보다 더 가까운 중심점으로 변경됐습니다.
  5. 다시 중심을 소속된 데이터의 평균 중심으로 이동합니다. 위 그림에서는 데이터 C가 중심 소속이 변경되면서 두 개의 중심이 모두 이동합니다.
  6. 중심점을 이동했는데 데이터의 중심점 소속 변경이 없으면 군집화를 종료합니다. 그렇지 않다면 다시 4번 과정을거쳐서 소속을 변경하고 이 과정을 반복합니다.


K-평균의 장점

  • 일반적인 군집화에서 가장 많이 활용되는 알고리즘
  • 알고리즘이 쉽고 간결

K-평균의 단점

  • 거리 기반 알고리즘으로 속성의 개수가 매우 많을 경우 군집화 정확도가 떨어짐
  • 반복을 수행하는데, 반복 횟수가 많을 경우 수행 시간이 매우 느려짐
  • 몇 개의 군집(cluster)을 선택해야 할지 가이드하기 어려움


사이킷런 KMeans 클래스

사이킷런 패키지는 K-평균을 구현하기 위해 KMeans 클래스를 제공합니다. KMeans 클래스는 다음과 같은 초기화 파라미터를 가지고 있습니다.

class sklearn.cluster.KMeans(
    n_clusters=8,
    init='k-means++',
    n_init=10,
    max_iter=300,
    tol=0.0001,
    precompute_distances='auto',
    verbose=0,
    random_state=None,
    copy_x=True,
    n_jobs=1,
    algorithm='auto'
)


이 중 중요한 파라미터는 다음과 같습니다.

  • n_clusters : 군집화할 개수, 즉 군집 중심점의 개수를 의미
  • init : 초기에 군집 중심점의 좌표를 설정할 방식, 일반적으로 k-means++방식으로 최초 설정
  • max_iter : 최대 반복 횟수, 이 횟수 이전에 모든 데이터의 중심점 이동이 없으면 종료


KMeans 는 사이킷런의 비지도학습 클래스와 마찬가지로 fit(데이터 세트) 또는 fit_transform(데이터세트) 메서드를 이용해 수행하면 됩니다. 이렇게 수행된 KMeans 객체는 군집화 수행이 완료돼 군집화와 관련된 주요 속성을 알 수가 있습니다. 다음은 이 주요 속성 정보입니다.

  • labels_ : 각 데이터 포인트가 속한 군집 중심점 레이블
  • cluster_centers_ : 각 군집 중심점 좌표(Shape는 [군집 개수, 피처 개수]).

K-평균을 이용한 붓꽃 데이터 세트 군집화

붓꽃 데이터를 이용해 K-평균 군집화를 수행해 보겠습니다. 꽃받침(sepal), 꽃잎(petal)의 길이에 따라 각 데이터의 군집화가 어떻게 결정되는지 확인해 보고, 이를 분류 값과 비교해 보겠습니다.

from sklearn.preprocessing import scale
from sklearn.datasets import load_iris
from sklearn.cluster import KMeans
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
%matplotlib inline

iris = load_iris()
# 보다 편리한 데이터 Handling을 위해 DataFrame으로 변환
irisDF = pd.DataFrame(
    data=iris.data,
    columns=['sepal_length', 'sepal_width', 'petal_length', 'petal_width'])
irisDF.head(3)

image


붓꽃 데이터 세트를 3개 그룹으로 군집화해 보겠습니다. 이를 위해 n_cluster 는 3, 초기 중심 설정 방식은 디폴트 값인 k-means++, 최대 반복 횟수 역시 디폴트 값인 max_iter=300 으로 설정한 KMeans 객체를 만들고, 여기에 fit() 를 수행하겠습니다.

kmeans = KMeans(
    n_clusters=3,
    init='k-means++',
    max_iter=300,
    random_state=0
)
kmeans.fit(irisDF)


fit() 을 수행해 irisDF 데이터에 대한 군집화 수행 결과가 kmeans 객체 변수로 반환됐습니다. kmeanslabels_ 속성값을 확인해 보면 irisDF 의 각 데이터가 어떤 중심에 속하는지를 알 수 있습니다. labels 속성값을 출력해 보겠습니다.

print(kmeans.labels_)
[output]
[1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
 0 0 0 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 0 2 2 2 2 0 2 2 2 2
 2 2 0 0 2 2 2 2 0 2 0 2 0 2 2 0 0 2 2 2 2 2 0 2 2 2 2 0 2 2 2 0 2 2 2 0 2
 2 0]


labels_ 의 값이 0, 1, 2 로 돼 있으며, 이는 각 레코드가 첫 번째 군집, 두 번째 군집, 세 번째 군집 에 속함을 의미합니다.

실제 붓꽃 품종 분류 값과 얼마나 차이가 나는지로 군집화가 효과적으로 됐는지, 실제 분류값인 target 과 군집화 분류값인 cluster 를 이용하여 확인해 보겠습니다.

irisDF['target'] = iris.target
irisDF['cluster'] = kmeans.labels_
irisDF.groupby(['target','cluster']).count()

image


sepal_length를 기준으로 분류 타깃이 0값인 데이터는 1번 군집으로 모두 잘 그루핑됐습니다. Target 1 값 데이터는 2개만 2번군집으로 그루핑됐고, 나머지 48개는 모두 0번 군집으로 그루핑됐습니다. 하지만 Target 2값 데이터는 0번 군집에 14개, 2번 군집에 36개로 분산돼 그루핑됐습니다.

이번에는 붓꽃 데이터 세트의 군집화를 시각화해 보겠습니다. 2차원 평면상에서 개별 데이터의 군집화을 시각적으로 표현하려고 합니다. 붓꽃 데이터 세트의 속성이 4개이므로 2차원 평면에 적합치 않아 PCA 를 이용해 4개의 속성을 2개로 차원 축소한 뒤에 X 좌표, Y 좌표로 개별 데이터를 표현하도록 하겠습니다.

from sklearn.decomposition import PCA

pca = PCA(n_components=2)
pca_transformed = pca.fit_transform(iris.data)

irisDF['pca_x'] = pca_transformed[:, 0]
irisDF['pca_y'] = pca_transformed[:, 1]
irisDF.head(3)

image


pca_x는 X 좌표 값, pca_y 는 Y 좌표 값을 나타냅니다. 각 군집별로 cluster 0 은 마커 ‘o’, cluster 1 은 마커 ‘s’, cluster 2 는 마커 ‘^’로 표현합니다.

# 군집 값이 0, 1, 2인 경우마다 별도의 인덱스로 추출
marker0_ind = irisDF[irisDF['cluster']==0].index
marker1_ind = irisDF[irisDF['cluster']==1].index
marker2_ind = irisDF[irisDF['cluster']==2].index

# 군집 값 0, 1, 2에 해당하는 인덱스로 각 군집 레벨의 pca_x, pca_y 값 추출. o, s, ^ 로 마커 표시
plt.scatter(x=irisDF.loc[marker0_ind, 'pca_x'], y=irisDF.loc[marker0_ind, 'pca_y'], marker='o')
plt.scatter(x=irisDF.loc[marker1_ind, 'pca_x'], y=irisDF.loc[marker1_ind, 'pca_y'], marker='s')
plt.scatter(x=irisDF.loc[marker2_ind, 'pca_x'], y=irisDF.loc[marker2_ind, 'pca_y'], marker='^')

plt.xlabel('PCA 1')
plt.ylabel('PCA 2')
plt.title('3 Clusters Visualization by 2 PCA Components')
plt.show()

image


Cluster 1 을 나타내는 네모(‘s’)는 명확히 다른 군집과 잘 분리돼 있습니다. Cluster 0 을 나타내는 동그라미(‘o’)와 Cluster 2 를 나타내는 세모(‘^’)는 상당 수준 분리돼 있지만, 네모만큼 명확하게는 분리돼 있지 않음을 알 수 있습니다. Cluster 0과 1의 경우 속성의 위치 자체가 명확히 분리되기 어려운 부분이 존재합니다.


군집화 알고리즘 테스트를 위한 데이터 생성

사이킷런은 다양한 유형의 군집화 알고리즘을 테스트해 보기 위한 간단한 데이터 생성기를 제공합니다. 대표적인 군집화용 데이터 생성기로 make_blobs() API가 있습니다. make_blobs() 은 여러 개의 클래스에 해당하는 데이터 세트를 만드는데, 하나의 클래스에 여러 개의 군집이 분포될 수 있게 데이터를 생성할 수 있으며, 개별 군집의 중심점과 표준 편차 제어 기능이 있고, 분류 용도로도 테스트 데이터 생성이 가능하다는 특징이 있습니다.

make_blobs() 의 간략한 사용법을 알아보면서 군집화를 위한 테스트 데이터 세트를 만드는 방법을 살펴보겠습니다. make_blobs() 를 호출하면 피처 데이터 세트와 타깃 데이터 세트가 튜플(Tuple)로 반환되며, 호출 파라미터는 다음과 같습니다.

  • n_samples : 생성할 총 데이터의 개수, 디폴트는 100개
  • n_features : 데이터의 피처 개수
  • centers : int 값으로 설정하면 군집의 개수를 나타냄. ndarray 형태로 표현할 경우 개별 군집 중심점의 좌표를 의미
  • cluster_std : 생성될 군집 데이터의 표준 편차를 의미, 군집별로 서로 다른 표준 편차를 가진 데이터 세트를 만들 때 사용


X, y = make_blobs(
    n_samples=200,
    n_features=2,
    centers=3,
    random_state=0
)

위 함수를 호출하면 총 200개의 레코드와 2개의 피처가 3개의 군집화 기반 분포도를 가진 피처 데이터 세트 X와 동시에 3개의 군집화 값을 가진 타깃 데이터 세트가 반환됩니다.

import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs
%matplotlib inline

X, y = make_blobs(
    n_samples=200,
    n_features=2,
    centers=3,
    cluster_std=0.8,
    random_state=0
)
print(X.shape, y.shape)

# y target 값의 분포를 확인
unique, counts = np.unique(y, return_counts=True)
print(unique, counts)
[output]
(200, 2) (200,)
[0 1 2] [67 67 66]


피처 데이터 세트 X 는 200개의 레코드와 2개의 피처를 가지므로 shape(200, 2), 군집 타깃 데이터 세트인 yshape(200,), 그리고 3개의 cluster 의 값은 [0, 1, 2] 이며 각각 67, 67, 66개로 균일하게 구성돼 있습니다. 좀 더 데이터 가공을 편리하게 하기 위해서 피처의 이름을 피처의 이름은 ftr1, ftr2 로 변경하겠습니다.

import pandas as pd

clusterDF = pd.DataFrame(data=X, columns=['ftr1', 'ftr2'])
clusterDF['target'] = y
clusterDF.head(3)

image


이제 make_blob() 으로 만든 피처 데이터 세트가 어떠한 군집화 분포를 가지고 만들어졌는지 확인해보겠습니다. 타깃값 0, 1, 2에 따라 마커를 다르게 해서 산점도를 그려보면 다음과 같이 3개의 구분될수 있는 군집 영역으로 피처 데이터 세트가 만들어졌음을 알 수 있습니다.

target_list = np.unique(y)
# 각 타깃별 산점도의 마커 값.
markers=['o', 's', '^', 'P', 'D', 'H', 'x']
# 3개의 군집 영역으로 구분한 데이터 세트를 생성했으므로 target_list는 [0, 1, 2]
# target==0, target==1, target==2 로 scatter plot을 marker별로 생성.
for target in target_list:
    target_cluster = clusterDF[clusterDF['target']==target]
    plt.scatter(
        x=target_cluster['ftr1'],
        y=target_cluster['ftr2'],
        edgecolor='k',
        marker=markers[target]
    )

plt.show()

image


이번에는 이렇게 만들어진 데이터 세트에 KMeans 군집화를 수행한 뒤에 군집별로 시각화해 보겠습니다. 먼저 KMeans 객체에 fit_predict(X) 를 수행해 make_blobs() 의 피처 데이터 세트인 X 데이터를 군집화합니다.

# KMeans 객체를 이용하여 X 데이터를 K-Means 클러스터링 수행 
kmeans = KMeans(
    n_clusters=3,
    init='k-means++',
    max_iter=200,
    random_state=0
)
cluster_labels = kmeans.fit_predict(X)
clusterDF['kmeans_label'] = cluster_labels

# cluster_centers_ 는 개별 클러스터의 중심 위치 좌표 시각화를 위해 추출
centers = kmeans.cluster_centers_
unique_labels = np.unique(cluster_labels)
markers=['o', 's', '^', 'P', 'D', 'H', 'x']

# 군집된 label 유형별로 iteration 하면서 marker 별로 scatter plot 수행. 
for label in unique_labels:
    label_cluster = clusterDF[clusterDF['kmeans_label']==label]
    center_x_y = centers[label]
    plt.scatter(
        x=label_cluster['ftr1'],
        y=label_cluster['ftr2'],
        edgecolor='k',
        marker=markers[label]
    )
    
    # 군집별 중심 위치 좌표 시각화 
    plt.scatter(x=center_x_y[0], y=center_x_y[1], s=200, color='white',
                alpha=0.9, edgecolor='k', marker=markers[label])
    plt.scatter(x=center_x_y[0], y=center_x_y[1], s=70, color='k', edgecolor='k', 
                marker='$%d$' % label)

plt.show()

image


make_blobs() 의 타깃과 kmeans_label 은 군집 번호를 의미하므로 서로 다른 값으로 매핑될 수 있습니다.

print(clusterDF.groupby('target')['kmeans_label'].value_counts())
[output]
target  kmeans_label
0       0               66
        1                1
1       2               67
2       1               65
        2                1
Name: kmeans_label, dtype: int64


Target 0cluster label 0 으로, target 1label 2 로, target 2label 1 로 거의 대부분 잘 매핑됐습니다.

make_blobs()cluster_std 파라미터로 데이터의 분포도를 조절합니다. 다음 그림은 cluster_std 가 0.4, 0.8, 1.2, 1.6일 때의 데이터를 시각화한 것입니다. cluster_std 가 작을수록 군집 중심에 데이터가 모여 있으며, 클수록 데이터가 퍼져 있음을 알 수 있습니다.

image


Read more

규제 선형 모델 - Ridge, Lasso, Elastic Net

|

규제 선형 모델

선형 모델의 비용 함수를 최소화(RSS 최소화, 실제 값과 예측값의 차이를 최소화)하는 것만 고려한다면, 학습 데이터에 지나치게 맞추게 되고, 회귀 계수가 쉽게 커지는 현상이 발생하는데, 이럴경우 변동성이 오히려 심해져서 테스트 데이터 세트에서는 예측 성능이 저하되기 쉽습니다. 이를 반영해 비용 함수학습 데이터의 잔차 오류 값을 최소로 하는 RSS 최소화 방법과적합을 방지하기 위해 회귀 계수 크기를 제어하는 방법서로 균형 을 이뤄야 합니다.

image


위 사항을 고려하여 비용(Cost) 함수의 목표 를 다음과 같이 변경할 수 있습니다.


[Cost\ Function\ Objective = Min(RSS(W) + alpha *   W   ^2_2)]


비용 함수의 목표가 $RSS(W) + alpha * ||W||^2_2$ 를 최소화하는 $W$ 벡터를 찾는 것으로 변경 되며, 여기서 alpha학습 데이터 적합 정도와 회귀 계수 값의 크기 제어를 수행하는 튜닝 파라미터 입니다.

  • alpha = 0(또는 매우 작은 값)인 경우
    • 비용 함수 식은 기존과 동일한 $Min(RSS(W) + 0)$ 이 될 것입니다.
    • 즉, $W$ 가 커도 $alpha * \lVert W \rVert^2_2$ 가 0이 되어 비용 함수는 $Min(RSS(W))$
  • alpha = 무한대(또는 매우 큰 값)인 경우
    • 비용 함수 식은 $RSS(W)$ 에 비해 $alpha * \lVert W \rVert^2_2$ 값이 너무 커지게 되므로 $W$ 값을 0(또는 매우 작게)으로 만들어야 Cost가 최소화되는 비용 함수 목표를 달성할 수 있습니다.
    • 즉, $alpha * \lVert W \rVert^2_2$ 가 무한대가 되므로 비용 함수는 $W$ 를 0에 가깝게 최소화 해야 함.


즉, alpha 값을 크게 하면 비용 함수는 회귀 계수 $W$ 의 값을 작게 해 과적합을 개선 할 수 있으며, alpha 값을 작게 하면 회귀 계수 $W$ 의 값이 커져도 어느 정도 상쇄가 가능하므로 학습 데이터 적합을 더 개선 할 수 있습니다.

즉, alpha를 0에서부터 지속적으로 값을 증가시키면 회귀 계수 값의 크기를 감소시킬 수 있습니다.

image


이처럼 비용 함수에 alpha 값으로 페널티를 부여해 회귀 계수 값의 크기를 감소시켜 과적합을 개선하는 방식규제(Regularization) 라고 부릅니다.

  • L2 규제
    • 릿지(Ridge) 회귀
    • $alpha * \lVert W \rVert^2_2$ 와 같이 $W$ 의 제곱에 대해 페널티를 부여하는 방식
  • L1 규제
    • 라쏘(Lasso) 회귀
    • $alpha * \lVert W \rVert_1$ 와 같이 $W$ 의 절댓값에 대해 페널티를 부여
    • L1 규제를 적용하면 영향력이 크지 않은 회귀 계수 값을 0으로 변환
    • 적절한 피처만 회귀에 포함시키는 피처 선택의 특성을 가짐
  • L1, L2 결합 규제
    • 엘라스틱넷(Elastic Net) 회귀
    • L2 규제와 L1 규제를 결합한 회귀
    • 엘라스틱넷 회귀 비용함수의 목표는 $RSS(W) + alpha2 * \lVert W \rVert_2^2 + alpha1*\lVert W \rVert_1$ 식을 최소화하는 $W$ 를 찾는 것


릿지(Ridge) 회귀

사이킷런은 Ridge 클래스를 통해 릿지 회귀를 구현합니다. Ridge 클래스의 주요 생성 파라미터는 alpha 이며, 이는 릿지 회귀의 alpha L2 규제 계수 에 해당합니다. 보스턴 주택 가격을 Ridge 클래스를 이용해 예측하고, 예측 성능을 cross_val_score() 로 평가해 보겠습니다.

import numpy as np
import pandas as pd
from sklearn.linear_model import Ridge
from sklearn.model_selection import cross_val_score
from sklearn.datasets import fetch_openml
import warnings
warnings.filterwarnings('ignore')

# boston 데이타셋 로드
boston = fetch_openml(name='boston')

# boston 데이타셋 DataFrame 변환 
bostonDF = pd.DataFrame(boston.data, columns = boston.feature_names)
bostonDF['RAD']=bostonDF['RAD'].astype(int)
bostonDF['CHAS']=bostonDF['CHAS'].astype(int)

# boston dataset의 target array는 주택 가격임. 이를 PRICE 컬럼으로 DataFrame에 추가함. 
bostonDF['PRICE'] = boston.target

y_target = bostonDF['PRICE']
X_data = bostonDF.drop(['PRICE'],axis=1, inplace=False)
from sklearn.linear_model import Ridge
from sklearn.model_selection import cross_val_score

# alpha=10으로 설정해 릿지 회귀 수행.
ridge = Ridge(alpha = 10)
neg_mse_scores = cross_val_score(
    ridge,
    X_data,
    y_target,
    scoring="neg_mean_squared_error",
    cv=5
)
rmse_scores = np.sqrt(-1 * neg_mse_scores)
avg_rmse = np.mean(rmse_scores)

print('5 folds 의 개별 Negative MSE scores: ', np.round(neg_mse_scores, 3))
print('5 folds 의 개별 RMSE scores : ', np.round(rmse_scores,3))
print('5 folds 의 평균 RMSE : {0:.3f} '.format(avg_rmse))
[output]
5 folds 의 개별 Negative MSE scores:  [-11.422 -24.294 -28.144 -74.599 -28.517]
5 folds 의 개별 RMSE scores :  [3.38  4.929 5.305 8.637 5.34 ]
5 folds 의 평균 RMSE : 5.518 


릿지의 5개 폴드 세트의 평균 RMSE가 5.518으로, 규제가 없는 LinearRegression의 RMSE 평균인 5.829보다 더 뛰어난 예측 성능을 보여줍니다. 이번에는 릿지의 alpha 값을 0, 0.1, 1, 10, 100으로 변화시키면서 RMSE와 회귀 계수 값의 변화를 살펴보겠습니다.

# 릿지에 사용될 alpha 파라미터의 값을 정의
alphas = [0, 0.1, 1, 10, 100]

# alphas list 값을 반복하면서 alpha에 따른 평균 rmse를 구함.
for alpha in alphas:
    ridge = Ridge(alpha=alpha)
    
    # cross_val_score를 이용해 5 폴드의 평균 RMSE를 계산
    neg_mse_scores = cross_val_score(
        ridge,
        X_data,
        y_target,
        scoring="neg_mean_squared_error",
        cv=5
    )
    avg_rmse = np.mean(np.sqrt(-1 * neg_mse_scores))
    
    print('alpha {0} 일 때 5 folds 의 평균 RMSE : {1:.3f} '.format(alpha, avg_rmse))
[output]
alpha 0 일 때 5 folds 의 평균 RMSE : 5.829 
alpha 0.1 일 때 5 folds 의 평균 RMSE : 5.788 
alpha 1 일 때 5 folds 의 평균 RMSE : 5.653 
alpha 10 일 때 5 folds 의 평균 RMSE : 5.518 
alpha 100 일 때 5 folds 의 평균 RMSE : 5.330 


alpha가 100일 때 평균 RMSE가 5.330으로 가장 좋습니다. 이번에는 alpha 값의 변화에 따른 피처의 회귀 계수값을 가로 막대 그래프로 시각화해 보겠습니다.

import matplotlib.pyplot as plt
%matplotlib inline
import seaborn as sns

# 각 alpha에 따른 회귀 계수 값을 시각화하기 위해 5개의 열로 된 맷플롯립 축 생성  
fig, axs = plt.subplots(figsize=(20, 8), nrows=1, ncols=5)
# 각 alpha에 따른 회귀 계수 값을 데이터로 저장하기 위한 DataFrame 생성  
coeff_df = pd.DataFrame()

# alphas 리스트 값을 차례로 입력해 회귀 계수 값 시각화 및 데이터 저장. pos는 axis의 위치 지정
for pos, alpha in enumerate(alphas):
    ridge = Ridge(alpha=alpha)
    ridge.fit(X_data, y_target)
    
    # alpha에 따른 피처별 회귀 계수를 Series로 변환하고 이를 DataFrame의 컬럼으로 추가.  
    coeff = pd.Series(data=ridge.coef_, index=X_data.columns)
    colname = 'alpha:' + str(alpha)
    coeff_df[colname] = coeff
    
    # 막대 그래프로 각 alpha 값에서의 회귀 계수를 시각화. 회귀 계수값이 높은 순으로 표현
    coeff = coeff.sort_values(ascending=False)
    axs[pos].set_title(colname)
    axs[pos].set_xlim(-3, 6)
    sns.barplot(x=coeff.values, y=coeff.index, ax=axs[pos])

# for 문 바깥에서 맷플롯립의 show 호출 및 alpha에 따른 피처별 회귀 계수를 DataFrame으로 표시
plt.show()

image


alpha 값을 계속 증가시킬수록 회귀 계수 값은 지속적으로 작아짐을 알 수 있습니다. 특히 NOX 피처의 경우 alpha 값을 계속 증가시킴에 따라 회귀 계수가 크게 작아지고 있습니다. alpha 값의 변화에 따른 릿지 회귀 계수 값을 구해 보겠습니다.

ridge_alphas = [0, 0.1, 1, 10, 100]
sort_column = 'alpha:' + str(ridge_alphas[0])
coeff_df.sort_values(by=sort_column, ascending=False)

image


alpha 값이 증가하면서 회귀 계수가 지속적으로 작아지고 있음 을 알 수 있습니다. 하지만 릿지 회귀의 경우에는 회귀 계수를 0으로 만들지는 않습니다.


라쏘(Lasso) 회귀

사이킷런은 Lasso 클래스를 통해 라쏘 회귀를 구현합니다. Lasso 클래스의 주요 생성 파라미터는 alpha 이며, 이는 라쏘 회귀의 alpha L1 규제 계수 에 해당합니다. Lasso 클래스를 이용해 바로 이전의 릿지 회귀 예제 코드와 유사하게 라쏘의 alpha 값을 변화시키면서 RMSE와 각 피처의 회귀 계수를 출력해 보겠습니다.

get_linear_reg_eval() 라는 별도의 함수를 만들어 회귀 모델의 이름, alpha값들의 리스트, 피처 데이터 세트와 타깃 데이터 세트를 입력받아서 alpha값에 따른 폴드 평균 RMSE를 출력하고 회귀 계수값들을 DataFrame으로 반환하겠습니다.

from sklearn.linear_model import Lasso, ElasticNet

# alpha값에 따른 회귀 모델의 폴드 평균 RMSE를 출력하고 회귀 계수값들을 DataFrame으로 반환 
def get_linear_reg_eval(
    model_name,
    params=None,
    X_data_n=None,
    y_target_n=None,
    verbose=True,
    return_coeff=True
):
    coeff_df = pd.DataFrame()
    if verbose: print('####### ', model_name, '#######')
    for param in params:
        if model_name =='Ridge': model = Ridge(alpha=param)
        elif model_name =='Lasso': model = Lasso(alpha=param)
        elif model_name =='ElasticNet': model = ElasticNet(alpha=param, l1_ratio=0.7)
        
        neg_mse_scores = cross_val_score(
            model,
            X_data_n,
            y_target_n,
            scoring="neg_mean_squared_error",
            cv=5
        )
        avg_rmse = np.mean(np.sqrt(-1 * neg_mse_scores))
        
        print('alpha {0}일 때 5 폴드 세트의 평균 RMSE: {1:.3f} '.format(param, avg_rmse))
        # cross_val_score는 evaluation metric만 반환하므로 모델을 다시 학습하여 회귀 계수 추출
        
        model.fit(X_data_n, y_target_n)
        if return_coeff:
            # alpha에 따른 피처별 회귀 계수를 Series로 변환하고 이를 DataFrame의 컬럼으로 추가. 
            coeff = pd.Series(data=model.coef_ , index=X_data_n.columns)
            colname='alpha:' + str(param)
            coeff_df[colname] = coeff
    
    return coeff_df


함수를 생성했으면 이를 이용해 alpha값의 변화에 따른 RMSE와 그때의 회귀계수들을 출력해 보겠습니다. get_linear_reg_eval() 에 모델명을 Lasso로 입력하면 라쏘 모델 기반으로 수행합니다.

# 라쏘에 사용될 alpha 파라미터의 값들을 정의하고 get_linear_reg_eval() 함수 호출
lasso_alphas = [ 0.07, 0.1, 0.5, 1, 3]
coeff_lasso_df = get_linear_reg_eval(
    'Lasso',
    params=lasso_alphas,
    X_data_n=X_data,
    y_target_n=y_target
)
[output]
#######  Lasso #######
alpha 0.07일 때 5 폴드 세트의 평균 RMSE: 5.612 
alpha 0.1일 때 5 폴드 세트의 평균 RMSE: 5.615 
alpha 0.5일 때 5 폴드 세트의 평균 RMSE: 5.669 
alpha 1일 때 5 폴드 세트의 평균 RMSE: 5.776 
alpha 3일 때 5 폴드 세트의 평균 RMSE: 6.189 


alpha 가 0.07일 때 5.612로 가장 좋은 평균 RMSE를 보여줍니다. 앞의 릿지 평균 5.518 보다는 약간 떨어지는 수치지만, LinearRegression 평균인 5.829 보다는 향상됐습니다. 다음은 alpha 값에 따른 피처별 회귀 계수입니다.

# 반환된 coeff_lasso_df를 첫번째 컬럼순으로 내림차순 정렬하여 회귀계수 DataFrame출력
sort_column = 'alpha:' + str(lasso_alphas[0])
coeff_lasso_df.sort_values(by=sort_column, ascending=False)

image


alpha의 크기가 증가함에 따라 일부 피처의 회귀 계수는 아예 0으로 바뀌고 있습니다. 회귀 계수가 0인 피처는 회귀 식에서 제외되면서 피처 선택의 효과 를 얻을수 있습니다.


엘라스틱(Elastic Net)넷 회귀

사이킷런은 ElasticNet 클래스를 통해서 엘라스틱넷 회귀를 구현합니다. ElasticNet 클래스의 주요생성 파라미터는 alphal1_ratio 입니다.

ElasticNet 클래스의 alphaRidgeLasso 클래스의 alpha 값과는 다릅니다. 엘라스틱넷의 규제는 a * L1 + b * L2 로 정의될 수 있습니다.

  • a : L1 규제의 alpha 값
  • b : L2 규제의 alpha 값
  • ElasticNet 클래스의 alpha 파라미터 값 : a + b
  • l1_ratio 파라미터 값 : a / (a + b)
    • l1_ratio 가 0 : a가 0이므로 L2 규제와 동일
    • l1_ratio 가 1 : b가 0이므로 L1 규제와 동일


ElasticNet 클래스를 이용해 바로 이전의 릿지, 라쏘 회귀 예제 코드와 유사하게 엘라스틱넷 alpha 값을 변화시키면서 RMSE와 각 피처의 회귀 계수를 출력해 보겠습니다.

# 엘라스틱넷에 사용될 alpha 파라미터의 값들을 정의하고 get_linear_reg_eval() 함수 호출
# l1_ratio는 0.7로 고정
elastic_alphas = [ 0.07, 0.1, 0.5, 1, 3]
coeff_elastic_df =get_linear_reg_eval(
    'ElasticNet',
    params=elastic_alphas,
    X_data_n=X_data,
    y_target_n=y_target
)
[output]
#######  ElasticNet #######
alpha 0.07일 때 5 폴드 세트의 평균 RMSE: 5.542 
alpha 0.1일 때 5 폴드 세트의 평균 RMSE: 5.526 
alpha 0.5일 때 5 폴드 세트의 평균 RMSE: 5.467 
alpha 1일 때 5 폴드 세트의 평균 RMSE: 5.597 
alpha 3일 때 5 폴드 세트의 평균 RMSE: 6.068 


# 반환된 coeff_elastic_df를 첫번째 컬럼순으로 내림차순 정렬하여 회귀계수 DataFrame출력
sort_column = 'alpha:' + str(elastic_alphas[0])
coeff_elastic_df.sort_values(by=sort_column, ascending=False)

image


alpha 0.5일 때 RMSE가 5.467로 가장 좋은 예측 성능을 보이고 있습니다. alpha값에 따른 피처들의 회귀 계수들 값이 라쏘보다는 상대적으로 0이 되는 값이 적음 을 알 수 있습니다.

지금까지 규제 선형 회귀의 가장 대표적인 기법인 릿지, 라쏘, 엘라스틱넷 회귀를 살펴봤습니다. 이들중 어떤 것이 가장 좋은지는 상황에 따라 다르며, 각각의 알고리즘에서 하이퍼 파라미터를 변경해 가면서 최적의 예측 성능을 찾아내야 합니다. 하지만 선형 회귀의 경우 최적의 하이퍼 파라미터를 찾아내는 것 못지않게 먼저 데이터 분포도의 정규화와 인코딩 방법이 매우 중요합니다.


선형 회귀 모델을 위한 데이터 변환

선형 모델은 피처값과 타깃값의 분포가 정규 분포 형태를 매우 선호합니다. 따라서 선형 회귀 모델을 적용하기 전에 먼저 데이터에 대한 스케일링/정규화 작업을 수행하는 것이 일반적 입니다. 이러한 스케일링/정규화 작업을 선행한다고 해서 무조건 예측 성능이 향상되는 것은 아니지만, 일반적으로 중요 피처들이나 타깃값의 분포도가 심하게 왜곡됐을 경우에 이러한 변환 작업을 수행합니다.

  • 피처 데이터 세트에 적용하는 주요 변환 작업
    1. 스케일링/정규화를 수행
      • StandardScaler 클래스 : 평균이 0, 분산이 1인 표준 정규 분포를 가진 데이터 세트로 변환
      • MinMaxScaler 클래스 : 최솟값이 0이고 최댓값이 1인 값으로 정규화를 수행
    2. 스케일링/정규화를 수행한 데이터 세트에 다시 다항 특성을 적용하여 변환
      • 보통 1번 방법 후 예측 성능에 향상이 없을 경우 적용
    3. 로그 변환(Log Transformation)
  • 타킷값에 적용하는 주요 변환 작업
    • 로그 변환(Log Translormation) : 원래 값에 log 함수를 적용하면 보다 정규 분포에 가까운 형태로 값이 분포됨.
    • 앞 1, 2번 방법보다 훨씬 많이 사용


보스턴 주택가격 피처 데이터 세트에 위에서 언급한 표준 정규 분포 변환, 최댓값/최솟값 정규화, 로그 변환을 차례로 적용한 후에 RMSE로 각 경우별 예측 성능을 측정해 보겠습니다. 이를 위해 get_scaled_data() 함수를 생성합니다.

from sklearn.preprocessing import StandardScaler, MinMaxScaler, PolynomialFeatures

# method는 표준 정규 분포 변환(Standard), 최대값/최소값 정규화(MinMax), 로그변환(Log) 결정
# p_degree는 다향식 특성을 추가할 때 적용. p_degree는 2이상 부여하지 않음. 
def get_scaled_data(method='None', p_degree=None, input_data=None):
    if method == 'Standard':
        scaled_data = StandardScaler().fit_transform(input_data)
    elif method == 'MinMax':
        scaled_data = MinMaxScaler().fit_transform(input_data)
    elif method == 'Log':
        scaled_data = np.log1p(input_data)
    else:
        scaled_data = input_data

    if p_degree != None:
        scaled_data = PolynomialFeatures(
            degree=p_degree,
            include_bias=False
        ).fit_transform(scaled_data)
    
    return scaled_data


이제 Ridge 클래스의 alpha 값을 변화시키면서 피처 데이터 세트를 여러 가지 방법으로 변환한 데이터 세트를 입력받을 경우에 RMSE 값이 어떻게 변하는지 살펴보겠습니다.

# Ridge의 alpha값을 다르게 적용하고 다양한 데이터 변환방법에 따른 RMSE 추출. 
alphas = [0.1, 1, 10, 100]

# 변환 방법은 모두 6개, 원본 그대로, 표준정규분포, 표준정규분포+다항식 특성
# 최대/최소 정규화, 최대/최소 정규화+다항식 특성, 로그변환 
scale_methods=[
    (None, None),
    ('Standard', None),
    ('Standard', 2),
    ('MinMax', None),
    ('MinMax', 2),
    ('Log', None)
]

for scale_method in scale_methods:
    X_data_scaled = get_scaled_data(
        method=scale_method[0],
        p_degree=scale_method[1],
        input_data=X_data
    )
    
    print(X_data_scaled.shape, X_data.shape)
    print('\n## 변환 유형:{0}, Polynomial Degree:{1}'.format(scale_method[0], scale_method[1]))
    
    get_linear_reg_eval(
        'Ridge',
        params=alphas,
        X_data_n=X_data_scaled,
        y_target_n=y_target,
        verbose=False,
        return_coeff=False
    )

image


일반적으로 선형 회귀를 적용하려는 데이터 세트에 데이터 값의 분포가 심하게 왜곡되어 있을 경우, 로그 변환을 적용하는 것이 좋은 결과를 기대할 수 있습니다.

Read more

다항 회귀와 과(대)적합/과소적합 이해

|

다항 회귀 이해

회귀가 독립변수의 단항식이 아닌 2차, 3차 방정식과 같은 다항식으로 표현되는 것다항(Polynomial) 회귀 라고 합니다. 즉, 다항 회귀는 다음과 같이 표현할 수 있습니다.


[y = w_0 + w_1 * x_1 + w_2 * x_2 + w_3 * x_1 * x_2 + w_4 * x_1^2 + w_5 * x_2^2]


한 가지 주의할 것은 다항 회귀는 선형 회귀 입니다. 회귀에서 선형 회귀/비선형 회귀를 나누는 기준은 회귀 계수가 선형/비선형 인지에 따른 것 이지, 독립변수의 선형/비선형 여부와는 무관합니다. 새로운 변수인 Z를 $z = [x_1, x_2, x_1*x_2, x_1^2, x_2^2]$ 라고 한다면 다음과 같습니다.


[y = w_0 + w_1 * z_1 + w_2 * z_2 + w_3 * z_3 + w_4 * z_4 + w_5 * z_5]


위와 같이 표현할 수 있기에 다항 회귀는 선형 회귀 입니다. 다음 그림을 보면 데이터 세트에 대해서 피처 X에 대해 Target Y 값의 관계를 단순 선형 회귀 직선형으로 표현한 것보다 다항 회귀 곡선형으로 표현한 것이 더 예측 성능이 높습니다.

image


사이킷런은 다항 회귀를 위한 클래스를 명시적으로 제공하지 않습니다. 대신 다항 회귀 역시 선형 회귀이기 때문에 비선형 함수를 선형 모델에 적용시키는 방법을 사용 해 구현합니다.

이를 위해 사이킷런은 PolynomialFeatures 클래스를 통해 피처를 Polynomial(다항식) 피처로 변환 합니다. PolynomialFeatures 클래스는 degree 파라미터를 통해 입력받은 단항식 피처를 degree 에 해당하는 다항식 피처로 변환합니다. 다른 전처리 변환 클래스와 마찬가지로 PolynomialFeatures 클래스는 fit(), transform() 메서드를 통해 이 같은 변환 작업을 수행합니다.

다음 예제는 PolynomialFeatures 를 이용해 단항값 $[x_1, x_2]$ 2차 다항값으로 $[1, x_1, x_2, x_1^2, x_1x_2, x_2^2]$ 로 변환하는 예제입니다.

from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import PolynomialFeatures
import numpy as np

# 다항식으로 변환한 단항식 생성, [[0,1],[2,3]]의 2X2 행렬 생성
X = np.arange(4).reshape(2,2)
print('일차 단항식 계수 feature:\n', X)

# degree = 2 인 2차 다항식으로 변환하기 위해 PolynomialFeatures를 이용하여 변환
poly = PolynomialFeatures(degree=2)
poly.fit(X)
poly_ftr = poly.transform(X)
print('변환된 2차 다항식 계수 feature:\n', poly_ftr)
[output]
일차 단항식 계수 feature:
 [[0 1]
 [2 3]]
변환된 2차 다항식 계수 feature:
 [[1. 0. 1. 0. 0. 1.]
 [1. 2. 3. 4. 6. 9.]]


단항 계수 피처 $[x_1, x_2]$ 를 2차 다항 계수 $[1, x_1, x_2, x_1^2, x_1x_2, x_2^2]$ 로 변경하므로 첫 번째 입력 단항 계수 피처 $[x_1=0, x_2=1]$ 은 $[1, x_1=0, x_2=1, x_1^2=0, x_1x_2=0, x_2^2=1]$ 형태인 $[1, 0, 1, 0, 0, 1]$ 로 변환되고, 두 번째 입력 단항 계수 피처 $[x_1=2, x_2=3]$ 은 $[1, 2, 3, 4, 6, 9]$ 로 변환됩니다.


이렇게 변환된 Polynomial 피처에 선형 회귀를 적용해 다항 회귀를 구현 합니다. 이번에는 3차 다항 계수를 이용해 3차 다항 회귀 함수식을 PolynomialFeaturesLinearRegression 클래스를 이용해 유도해 보겠습니다.

이를 위해 3차 다항 회귀 함수를 임의로 설정하고 회귀 계수를 예측할 것 입니다. 먼저 3차 다항회귀의 결정 함수식은 다음과 같이 $y = 1 + 2x_1 + 3x_1^2 + 4x_2^3$ 로 설정하고 이를 위한 함수 polynomial_func() 를 만듭니다. 해당 함수는 3차 다항 계수 피처 값이 입력되면 결정 값을 반환합니다.

def polynomial_func(X):
    y = 1 + 2*X[:,0] + 3*X[:,0]**2 + 4*X[:,1]**3
    return y

X = np.arange(0,4).reshape(2,2)
print('일차 단항식 계수 feature: \n', X)
y = polynomial_func(X)
print('삼차 다항식 결정값: \n', y)
[output]
일차 단항식 계수 feature: 
 [[0 1]
 [2 3]]
삼차 다항식 결정값: 
 [  5 125]


이제 일차 단항식 계수를 삼차 다항식 계수로 변환하고, 이를 선형 회귀에 적용하면 다항 회귀로 구현됩니다. PolynomialFeatures(degree=3) 은 단항 계수 피처 $[x_1, x_2]$ 를 3차 다항 계수 $[1, x_1, x_2, x_1^2, x_1x_2, x_2^2, x_1^3, x_1^2x_2, x_1x_2^2, x_2^3]$ 과 같이 10개의 다항 계수로 변환합니다.

# 3 차 다항식 변환 
poly_ftr = PolynomialFeatures(degree=3).fit_transform(X)
print('3차 다항식 계수 feature: \n', poly_ftr)

# Linear Regression에 3차 다항식 계수 feature와 3차 다항식 결정값으로 학습 후 회귀 계수 확인
model = LinearRegression()
model.fit(poly_ftr, y)
print('Polynomial 회귀 계수\n', np.round(model.coef_, 2))
print('Polynomial 회귀 Shape :', model.coef_.shape)
[output]
3차 다항식 계수 feature: 
 [[ 1.  0.  1.  0.  0.  1.  0.  0.  0.  1.]
 [ 1.  2.  3.  4.  6.  9.  8. 12. 18. 27.]]
Polynomial 회귀 계수
 [0.   0.18 0.18 0.36 0.54 0.72 0.72 1.08 1.62 2.34]
Polynomial 회귀 Shape : (10,)


일차 단항식 계수 피처는 2개였지만, 3차 다항식 Polynomial 변환 이후에는 다항식 계수 피처가 10개로 늘어납니다. 이 피처 데이터 세트에 LinearRegression 을 통해 3차 다항 회귀 형태의 다항 회귀를 적용하면 회귀 계수가 10개로 늘어납니다. 10개의 회귀 계수 [0, 0.18, 0.18, 0.36, 0.54, 0.72, 0.72, 1.08, 1.62, 2.34] 가 도출됐으며 원래 다항식 $y = 1 + 2x_1 + 3x_1^2 + 4x_2^3$ 의 계수 값인 [1, 2, 0, 3, 0, 0, 0, 0, 0, 4] 와는 차이가 있지만 다항 회귀로 근사하고 있음 을 알 수 있습니다. 이처럼 사이킷런은 PolynomialFeatures 로 피처를 변환한 후에 LinearRegression 클래스로 다항 회귀를 구현합니다.

바로 이전 예제와 같이 피처 변환과 선형 회귀 적용을 각각 별도로 하는 것보다는 사이킷런의 Pipeline객체를 이용 하여 한 번에 다항 회귀를 구현할 수 있습니다.

from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.pipeline import Pipeline
import numpy as np

def polynomial_func(X):
    y = 1 + 2*X[:,0] + 3*X[:,0]**2 + 4*X[:,1]**3 
    return y

# Pipeline 객체로 Streamline 하게 Polynomial Feature변환과 Linear Regression을 연결
model = Pipeline(
    [
        ('poly', PolynomialFeatures(degree=3)),
        ('linear', LinearRegression())
    ]
)

X = np.arange(4).reshape(2,2)
y = polynomial_func(X)

model = model.fit(X, y)
print('Polynomial 회귀 계수\n', np.round(model.named_steps['linear'].coef_, 2))
[output]
Polynomial 회귀 계수
 [0.   0.18 0.18 0.36 0.54 0.72 0.72 1.08 1.62 2.34]


다항 회귀를 이용한 과소적합 및 과적합 이해

다항 회귀는 피처의 직선적 관계가 아닌 복잡한 다항 관계를 모델링할 수 있으며, 다항식의 차수가 높아질수록 매우 복잡한 피처 간의 관계까지 모델링이 가능합니다. 하지만 다항 회귀의 차수(degree)를 높일수록 학습 데이터에만 너무 맞춘 학습이 이뤄져서 정작 테스트 데이터 환경에서는 오히려 예측 정확도가 떨어집니다. 즉, 차수가 높아질수록 과적합의 문제가 크게 발생합니다.

다항 회귀를 이용해 과소적합과 과적합의 문제를 잘 보여주는 예제를 살펴보겠습니다. 피처 X와 target y가 잡음(Noise)이 포함된 코사인(Cosine) 그래프 관계 를 가지도록 만들고, 이에 기반해 다항 회귀의 차수를 변화시키면서 그에 따른 회귀 예측 곡선과 예측 정확도를 비교하는 예제 입니다.

import numpy as np
np.random.seed(0)
import matplotlib.pyplot as plt
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import cross_val_score
%matplotlib inline

# 임의의 값으로 구성된 X값에 대해 코사인 변환 값을 반환.
def true_fun(X):
    return np.cos(1.5 * np.pi * X)

# X는 0부터 1까지 30개의 임의의 값을 순서대로 샘플링한 데이터입니다.
n_samples = 30
X = np.sort(np.random.rand(n_samples))

# y 값은 코사인 기반의 true_fun()에서 약간의 노이즈 변동 값을 더한 값입니다.
y = true_fun(X) + np.random.randn(n_samples) * 0.1
plt.scatter(X, y)

image


이제 예측 결과를 비교할 다항식 차수를 각각 1, 4, 15로 변경하면서 예측 결과를 비교하겠습니다. 다항식 차수별로 학습을 수행한 뒤 cross_val_score() 로 MSE 값을 구해 차수별 예측 성능을 평가하고, 0부터 1까지 균일하게 구성된 100개의 테스트용 데이터 세트를 이용해 차수별 회귀 예측 곡선을 그려보겠습니다

plt.figure(figsize=(14, 5))
degrees = [1, 4, 15]

# 다항 회귀의 차수(degree)를 1, 4, 15로 각각 변화시키면서 비교합니다.
for i in range(len(degrees)):
    ax = plt.subplot(1, len(degrees), i + 1)
    plt.setp(ax, xticks=(), yticks=())
    
    # 개별 degree별로 Polynomial 변환합니다.
    polynomial_features = PolynomialFeatures(degree=degrees[i], include_bias=False)
    linear_regression = LinearRegression()
    pipeline = Pipeline(
        [
            ("polynomial_features", polynomial_features),
            ("linear_regression", linear_regression)
        ]
    )
    pipeline.fit(X.reshape(-1, 1), y)
    
    # 교차 검증으로 다항 회귀를 평가합니다.
    scores = cross_val_score(
        pipeline,
        X.reshape(-1, 1),
        y,
        scoring="neg_mean_squared_error",
        cv=10
    )
    
    # Pipeline을 구성하는 세부 객체를 접근하는 named_steps['객체명']을 이용해 회귀계수 추출
    coefficients = pipeline.named_steps['linear_regression'].coef_
    print('\nDegree {0} 회귀 계수는 {1} 입니다.'.format(degrees[i], np.round(coefficients, 2)))
    print('Degree {0} MSE 는 {1} 입니다.'.format(degrees[i], -1*np.mean(scores)))
          
    # 0 부터 1까지 테스트 데이터 세트를 100개로 나눠 예측을 수행합니다.
    # 테스트 데이터 세트에 회귀 예측을 수행하고 예측 곡선과 실제 곡선을 그려서 비교합니다.
    X_test = np.linspace(0, 1, 100)
    
    # 예측값 곡선
    plt.plot(X_test, pipeline.predict(X_test[:, np.newaxis]), label="Model")
    
    # 실제 값 곡선
    plt.plot(X_test, true_fun(X_test), '--', label="True function")
    plt.scatter(X, y, edgecolor='b', s=20, label="Samples")
    plt.xlabel("x"); plt.ylabel("y"); plt.xlim((0, 1)); plt.ylim((-2, 2)); plt.legend(loc="best")
    plt.title("Degree {}\nMSE = {:.2e}(+/- {:.2e})".format(degrees[i], -scores.mean(), scores.std()))
    
plt.show()
[output]
Degree 1 회귀 계수는 [-1.61] 입니다.
Degree 1 MSE 는 0.4077289625098685 입니다.

Degree 4 회귀 계수는 [  0.47 -17.79  23.59  -7.26] 입니다.
Degree 4 MSE 는 0.04320874987232064 입니다.

Degree 15 회귀 계수는 [-2.98293000e+03  1.03899390e+05 -1.87416123e+06  2.03716219e+07
 -1.44873283e+08  7.09315363e+08 -2.47065792e+09  6.24561050e+09
 -1.15676510e+10  1.56894936e+10 -1.54006023e+10  1.06457264e+10
 -4.91377530e+09  1.35919645e+09 -1.70380786e+08] 입니다.
Degree 15 MSE 는 181238256.56423894 입니다.

image


  • Degree 1 예측 곡선(MSE : 약 0.41)
    • 과소적합 : 예측 곡선이 학습 데이터의 패턴을 제대로 반영하지 못하고 있음.
    • 고편향(High Bias)성을 가짐 : 매우 단순화된 모델로서 지나치게 한 방향성으로 치우친 경향
  • Degree 4 예측 곡선(MSE : 약 0.04)
    • 적합 : 학습 데이터 세트를 비교적 잘 반영해 코사인 곡선 기반으로 테스트 데이터를 잘 예측.
  • Degree 15 예측 곡선(MSE : 약 182581084.83)
    • 과적합 : 예측 곡선이 데이터 세트의 변동 잡음값까지 지나치게 반영함.
    • 예측 곡선이 학습 데이터 세트만 정확히 예측하고, 테스트 값의 실제 곡선과는 완전히 다른 형태의 예측 곡선이 만들어짐.
    • 복잡한 다항식을 만족하기 위해 계산된 회귀 계수는 현실과 너무 동떨어진 예측 결과를 보여줌.
    • 고분산(High Variance)성을 가짐 : 학습 데이터 하나하나의 특성을 반영하면서 매우 복잡한 모델이 되어, 지나치게 높은 변동성을 가짐.


결국 좋은 예측 모델 이란 Degree 1과 같이 학습 데이터의 패턴을 지나치게 단순화한 과소적합 모델도 아니고 Degree 15와 같이 모든 학습 데이터의 패턴을 하나하나 감안한 지나치게 복잡한 과적합 모델도 아닌, 학습 데이터의 패턴을 잘 반영하면서도 복잡하지 않은 균형 잡힌(Balanced) 모델을 의미 합니다.


편향-분산 트레이드오프(Bias-Variance Trade off)

편향-분산 트레이드오프(Bias-Variance Trade off) 는 머신러닝이 극복해야 할 가장 중요한 이슈 중 하나입니다.

image


위 그림은 편향과 분산의 고/저의 의미를 직관적으로 잘 표현하고 있습니다.

  • 상단 왼쪽의 저편향/저분산(Low Bias/Low Variance)
    • 예측 결과가 실제 결과에 매우 잘 근접, 예측 변동이 크지 않고 특정 부분에 집중돼 있는 아주 뛰어난 성능을 보여줌.
  • 상단 오른쪽의 저편향/고분산(Low Bias/High Variance)
    • 예측 결과가 결과에 비교적 근접, 예측 결과가 실제 결과를 중심으로 꽤 넓은 부분에 분포돼 있음.
  • 하단 왼쪽의 고편향/저분산(High Bias/Low Variance)
    • 정확한 결과에서 벗어나면서, 예측이 특정 부분에 집중돼 있음.
  • 하단 오른쪽의 고편향/고분산(High Bias/High Variance)
    • 정확한 예측 결과를 벗어나면서, 넓은 부분에 분포돼 있음.


일반적으로 편향과 분산은 한쪽이 높으면 한쪽이 낮아지는 경향(Bias-Variance Trade off) 이 있습니다.

  • 과소적합
    • 편향이 높으면 분산은 낮아지는 경향
    • 즉, 높은 편향/낮은 분산에서 과소적합되기 쉬움.
  • 과적합
    • 분산이 높으면 편향이 낮아지는 경향
    • 즉, 낮은 편향/높은 분산에서 과적합되기 쉬움.


image


위 그림은 편향과 분산의 관계에 따른 전체 오류 값(Total Error) 의 변화를 잘 보여줍니다. 편향이 너무 높으면 전체 오류가 높습니다. 편향을 점점 낮추면 동시에 분산이 높아지고 전체 오류도 낮아지게 됩니다. 편향을 낮추고 분산을 높이면서 전체 오류가 가장 낮아지는 ‘골디락스’ 지점을 통과하면서 분산을 지속적으로 높이면 전체 오류 값이 오히려 증가하면서 예측 성능이 다시 저하됩니다.

편향과 분산이 서로 트레이드오프를 이루면서, 오류 Cost 값이 최대로 낮아지는 모델을 구축하는 것 이 가장 효율적인 머신러닝 예측 모델을 만드는 방법 입니다.

Read more