[ 핸즈 온 머신러닝 2 ] 비지도 학습의 모든 것 (K-Means)
어제보다 나은 사람이 되기

걱정보단 실행을, 그러나 계획적으로

Box World 자세히보기

AI/Hands-On Machine Learning 2판

[ 핸즈 온 머신러닝 2 ] 비지도 학습의 모든 것 (K-Means)

Box형 2021. 2. 8. 16:37
반응형

약해지지 말자.
- 래리 페이지 (구글 공동창립자) -


시작하며

 이전 포스팅에서는 대부분 레이블($y$)이 존재하는 데이터에 대해 다뤄봤습니다. 하지만 우리가 사용하는 대부분의 데이터는 레이블이 없습니다.

 그렇다면 레이블이 없다면 붙이면 되지 않을까요? 예를 들어 제품 결함을 감지하는 시스템을 만들어야 한다고 가정해보겠습니다. 시스템을 학습시키기 위해 수천장의 제품 사진 데이터를 마련하는 건 쉬울지 몰라도 이것들에 각각 결함과 정상을 판단하여 레이블링을 하는 것은 높은 cost를 요합니다.

 물론 최근에는 데이터에 대한 중요성이 대두되면서 정부 차원에서 이러한 데이터 생성 및 가공에 힘을 쓰고 있지만, 이번 포스팅에선 사람의 도움없이 알고리즘이 레이블이 없는 데이터를 바로 사용하는 비지도 학습에 대해 살펴보겠습니다.


9.1 군집 (Clustering)

 길을 걷다보면 많은 꽃들을 보게 됩니다. 이들이 모두 동일하지는 않지만, 외형적인 모습의 유사성을 통해 비슷한 종류의 꽃들끼리 그룹을 짓는건 굳이 전문가가 필요한 일이 아닙니다. 우리는 이러한 비슷한 특성을 가지는 데이터들끼리 묶는 것을 군집(clustering)이라고 합니다. 그리고 이때 만들어진 그룹을 Cluster라고 합니다.

 Clustering은 결국 각각의 데이터를 하나의 Cluster에 할당하는 작업입니다. 다음 그림은 동일한 붓꽃 데이터셋에 대해서 한쪽은 레이블링이 되어있고, 다른 한쪽은 그렇지 않은 데이터를 시각화한 것입니다. 

 왼족은 레이블링 되어있는 덕분에 Logistic Regression, SVM 등의 Classification 알고리즘이 잘 맞습니다. 그러나 오른쪽은 레이블링이 없기 때문에 Clustering 알고리즘이 필요합니다. 

 이제 본격적으로 Clustering 알고리즘에 대해 알아보겠습니다.

 

 

 

 

 


 9.1.1 k-평균

  아래 보이는 데이터셋은 레이블이 없고, 육안으로 볼때 샘플 덩어리 5개가 잘 구분되어있는 것을 확인할 수 있습니다. 우리가 배울 첫번째 Clustering 알고리즘인 k-means 알고리즘은 몇번의 반복으로 이런 종류의 데이터셋을 효율적으로 클러스터로 묶을 수 있습니다.

 우선 구체적인 작동 원리를 살펴보기 전 sklearn의 코드를 살펴보겠습니다.

from sklearn.cluster import KMeans

k = 5
kmeans = KMeans(n_clusters=k, random_state=42)
y_pred = kmeans.fit_predict(X)

 첫번째로는 알고리즘이 찾을 클러스터 개수 $k$를 지정해야 합니다. 지금은 육안으로 보고 $5$라고 설정하였지만, 실제로는 이런 방식으로 정하지 못하는데 이 부분은 조금 이따가 살펴보겠습니다.

 어쨌든 $k=5$라고 할당이 되었다면, 각각의 데이터는 5개의 클러스터 중 하나에 할당되는데, 바로 할당되는 클러스터의 index가 데이터의 레이블이 됩니다.(Classification의 클래스 레이블과는 분명히 다릅니다.) 

 다음 코드처럼 labels_ 변수를 이용하면 훈련된 데이터의 레이블을 확인할 수 있습니다.

y_pred
>>
array([0, 4, 1, ..., 2, 1, 4], dtype=int32)

y_pred is kmeans.labels_
>>
True

 다음 코드에서는 알고리즘이 찾은 다섯 개의 센트로이드를 나열합니다. 이때 센트로이드는 하나의 클러스터 내 데이터들의 중심이 되는 점입니다.

kmeans.cluster_centers_

>>
array([[-2.80037642,  1.30082566],
       [ 0.20876306,  2.25551336],
       [-2.79290307,  2.79641063],
       [-1.46679593,  2.28585348],
       [-2.80389616,  1.80117999]])

 만약 새로운 데이터가 들어온다면, 다음과 같이 해당 위치에서 가장 가까운 센트로이드가 속해있는 클러스터에 할당할 수 있습니다.

X_new = np.array([[0, 2], [3, 2], [-3, 3], [-3, 2.5]])
kmeans.predict(X_new)

>>
array([1, 1, 2, 2], dtype=int32)

 그리고 클러스터의 결정 경계를 그리면 다음과 같은 다이어그램을 얻을 수 있습니다. 이때 센트로이드는 $X$로 표시되어있습니다. 대부분의 데이터는 잘 할당이 되어있는데, 클러스터의 경계 부근 특히 핑크색과 노란색 클러스터 사이의 데이터 몇개는 잘못된 레이블이 부여되어있습니다.

 여기서 알 수 있는건 K-Means 알고리즘은 데이터에서 센트로이드까지의 거리를 고려하는 것이 전부이기 때문에, 클러스터 간의 크기가 많이 다르면 성능이 좋지 못합니다.

  이렇게 각각의 데이터를 하나의 Cluster에 할당하는 방식을 Hard Clustering이라고 합니다. 그리고 각각의 데이터마다 모든 클러스터마다 점수를 부여해 이중 하나를 선택하는 방식은 Soft Clustering이라고 합니다.

 이때 점수를 매기는 방식은 거리가 될 수도 있고, Gaussian RBF Kernel함수를 이용한 유사도 점수가 될 수도 있습니다. 다음은 transform() 메서드를 통해 데이터와 센트로이드 사이의 거리를 반환합니다.

 만약 고차원 데이터셋을 이런 방식으로 변환하면, 클러스터 개수가 k일때 k-차원 데이터셋으로 변환하여 효과적인 차원 축소(Dimension Reduction)을 수행할 수 있습니다.

kmeans.transform(X_new)

>>
array([[2.88633901, 0.32995317, 2.9042344 , 1.49439034, 2.81093633],
       [5.84236351, 2.80290755, 5.84739223, 4.4759332 , 5.80730058],
       [1.71086031, 3.29399768, 0.29040966, 1.69136631, 1.21475352],
       [1.21567622, 3.21806371, 0.36159148, 1.54808703, 0.72581411]])

K-Means 알고리즘의 작동 방법 

 이제 본격적으로 K-Means 알고리즘의 작동 방법에 대해 알아보겠습니다. 센트로이드가 주어진다면, 각 데이터별로 가장 가까운 센트로이드의 클러스터에 할당하면 됩니다. 반대로 샘플의 레이블이 주어져있다면, 평균을 계산하여 모든 센트로이드를 구할 수도 있습니다.

 그러나 둘다 주어지지 않는다면 어떻게 해야할까요? 처음에는 무작위로 k개의 데이터를 골라 그 위치를 센트로이드로 지정합니다. 그리고 지정된 이 센트로이드에 따라 각 데이터별로 레이블링을 하고 센트로이드를 다시 업데이트합니다. 이 과정을 센트로이드에 변화가 없을 때까지 계속 하는데, 이 알고리즘은 제한된 횟수안에 수렴함을 보장합니다.

 다음은 세 번의 반복으로 최적의 클러스터링을 만들어내는 과정입니다.

 알고리즘의 수렴성은 보장되지만, 그것이 반드시 최적의 솔루션인 것은 아닙니다. 이것의 성공 여부는 센트로이드의 초기화에 달려있기 때문입니다.


센트로이드 초기화 방법

 센트로이드 초기화 방법에는 몇가지가 있는데, 어떻게 센트로이드를 초기화하느냐에 따라 알고리즘의 성능에 큰 영향을 미칩니다. 첫번째 방법은 n_init = 1로 설정하고, 하나의 numpy 배열로 센트로이드 리스트를 초기화하는 것입니다.

good_init = np.array([[-3, 3], [-3, 2], [-3, 1], [-1, 2], [0, 2]])
kmeans = KMeans(n_clusters=5, init=good_init, n_init=1)

 두 번째 방법은 매번 다른 랜덤 초기화를 하여 여러 번 알고리즘을 실행하고, 이중 가장 좋은 솔루션을 채택하는 것입니다. n_init이 바로 랜덤 초기화 횟수를 지정하는 변수이며 기본 값은 10입니다.

 그렇다면 가장 좋은 솔루션이라고 판단하는 성능 지표는 무엇일까요? 그것은 각 데이터와 센트로이드 사이의 평균 제곱 거리로 도출한 모델의 이너셔(inertia)입니다.

 다음 그림에서 왼쪽 솔루션의 이너셔는 223.3, 오른쪽은 237.5입니다. KMeans 클래스는 n_init번 실행하여 이너셔가 가장 낮은 모델을 리턴합니다. 

 그리고 inertia_ 변수로 해당 모델의 이너셔를 확인할 수 있습니다.

kmeans.inertia_

>>
211.62337889822362

 score() 메서드는 이너셔의 음숫값을 반환합니다. 이것은 "큰 값이 좋은 것이다"라는 sklearn의 규칙을 따르기 위해 더 좋은 모델이 높은 값을 갖도록 하기 위함입니다.

kmeans.score(X)

>>
-211.62337889822362

K-Means++ 알고리즘

 이 개선된 알고리즘은 센트로이드 초기화 단계에서 다른 센트로이드와 거리가 먼 센트로이드를 선택하는 방식으로 기존 알고리즘의 성능을 높이고 최적이 아닌 솔루션으로 수렴할 가능성을 크게 낮췄습니다.

 게다가 최적의 솔루션을 찾기 위한 반복 횟수도 크게 줄이기 때문에 충분한 가치가 있는데, 작동 원리는 다음과 같습니다.

 sklearn에서는 원래 이 초기화 방법을 사용하는데, 앞서 배웠던 방식을 사용하고 싶다면 init = "random"으로 설정하면 됩니다.


K-Means 속도 개선과 mini-batch

 2013년도에 한 논문을 통해 불필요한 거리 계산을 피함으로써 알고리즘의 속도를 상당히 높이는 방법이 제안되었습니다. 여기서 사용된 것은 삼각 부등식을 사용하였고, 데이터와 센트로이드 사이의 거리를 위한 하한선과 상한선을 두었습니다.

 또 다른 논문에서는 전체 데이터셋을 사용해 반복하는 것이 아닌, 각 반복마다 mini-batch를 사용해 센트로이드를 조금씩 이동합니다. 이는 알고리즘의 속도를 3~4배 정도 높이며 큰 데이터셋을 다룰 때 용이합니다. 다음은 코드입니다.

from sklearn.cluster import MiniBatchKMeans

minibatch_kmeans = MiniBatchKMeans(n_clusters=5, random_state=42)
minibatch_kmeans.fit(X)

 mini-batch K-Means 알고리즘은 속도는 빠르나 이너셔는 조금 더 나쁩니다. 특히 클러스터의 개수가 증가할 때 그렇습니다. 다음 그림에서 왼쪽 그래프는 클러스터 개수 $k$에 따른 mini-batch k-means와 k-means의 이너셔를 비교한 것입니다. 

 왼쪽 그래프의 경우 곡선의 차이가 일정하게 유지되는 듯 보이지만, 클러스터의 개수가 늘어날 수록 이너셔가 점점 줄어들기 때문에, 차이가 차지하는 비율은 점점 커집니다. 그러나 오른쪽 그래프를 보면 mini-batch K-Means가 일반적인 알고리즘보다 훨씬 빠르다는 것을 확인할 수 있습니다.


최적의 클러스터 개수 찾기

 클러스터의 개수는 K-Means 알고리즘의 성능을 결정짓는 매우 중요한 요소입니다. 다음과 같이 클러스터의 개수가 너무 적다면 여러 클러스터가 합쳐지고, 그렇다고 너무 많다면 하나의 클러스터가 여러 개로 나눠질 수도 있습니다.

 그렇다면 가장 적은 이너셔를 가진 모델을 선택하면 되는걸까요? 다음 그림을 보면 $k=3$일 때 이너셔는 $653.2$이고, $k$가 늘어날 수록 이너셔는 점점 작아지기 때문에 이너셔가 그다지 좋은 성능 지표가 아니라는 걸 알 수 있습니다.

 어차피 클러스터가 늘어날 수록 각각의 데이터는 가까운 센트로이드에 가까워지기 때문에 이너셔가 작아지는 건 당연한 결과입니다.

 위 그림에서 보듯이 $k = 4$까지는 빠르게 이너셔가 감소하는걸 확인할 수 있습니다. 이 지점을 Elbow라 칭하는데, 보통 이 지점을 넘어서면 이너셔가 줄어드는 속도가 매우 줄어들기 때문에 $4$를 넘어선 클러스터의 개수는 크게 도움이 되지 않습니다. 따라서 보통 이 Elbow를 최적의 클러스터 개수로 고르게 됩니다.

 그러나 방금과 같은 방법은 조금은 엉성하게 느껴지기도 합니다. 더 정확한 방법은 실루엣 점수(silghouette score)입니다. 이 점수는 모든 데이터에 대한 실루엣 계수의 평균입니다. 각 데이터의 실루엣 계수는 다음과 같습니다.

 $a$는 동일한 클러스터 내 다른 데이터와 자기 자신 데이터와의 평균 거리입니다(클러스터 내부의 평균 거리). $b$는 자기가 속한 클러스터를 제외하고 가장 가까운 클러스터의 데이터까지 평균 거리입니다. 계수는 $-1$에서 $1$까지 바뀔 수 있습니다. 

 $+1$에 가까우면 $b$ 즉 다른 클러스터와 멀면서, $a$ 자기가 속한 클러스터 내 데이터들과 가깝고 잘 뭉쳐져있다는 뜻이고, $-1$에 가깝다면, 반대이기 때문에 잘못된 클러스터에 할당되었다는 의미입니다.

 실루엣 점수를 계산하려면 sklearn의 silhouette_score() 함수를 사용합니다.

from sklearn.metrics import silhouette_score

silhouette_score(X, kmeans.labels_)
>>
0.655517642572828

 클러스터의 개수에 따라 상이한 실루엣 점수를 살펴보겠습니다. 클수록 잘 클러스터링된 것입니다.

 모든 데이터의 실루엣 계수를 할당된 클러스터와 계수값으로 정렬하여 그리면 더 많은 정보가 있는 그래프를 얻을 수 있습니다. 이를 실루엣 다이어그램이라고 합니다. 

 클러스터마다 칼 모양이 그려지는 이 그래프의 높이는 클러스터가 포함하고 있는 데이터의 개수를 의미하고, 너비는 클러스터 내 데이터의 정렬된 실루엣 계수를 나타냅니다.(넓을 수록 좋습니다.)

 빨간색으로 표시된 수직 파선은 해당 클러스터 개수에 따른 실루엣 점수를 나타냅니다. 특정 클러스터의 데이터 대부분이 이 점수보다 낮은 계수를 가지면(파선의 왼쪽에 있다면) 나쁜 클러스터입니다.

 위 그림의 경우 $k = 4$와 $k=5$일 때 클러스터가 상당히 좋아보이는데, 특히 $k = 5$일 때, 모든 클러스터의 크기가 비슷하기 때문에 이것을 선택하는게 좀 더 좋습니다.

 

 

 

 

 


9.1.2 K-Means의 한계

 K-Means 알고리즘는 몇 가지 단점이 있습니다.

  • 최적의 솔루션을 도출하기 위해 여러번 알고리즘을 실행해야 합니다.
  • 클러스터 개수를 지정해야 합니다.
  • 클러스터의 크기나 밀집도가 서로 다르거나, 원형이 아닐 경우 잘 작동하지 않습니다.

 위 단점들 중 세 번째에 대해 좀 더 살펴보겠습니다. 다음 그림은 크기, 밀집도, 방향이 다른 세 개의 타원형 클러스터를 가진 데이터에 대해 K-Means를 적용한 결과입니다.

  보다시피 둘 다 좋은 솔루션은 아닙니다. 즉 우리는 데이터의 형태에 따라 K-Means가 아닌 다른 알고리즘들도 고려해야할 시점이 온 것입니다. 이 경우 잘 작동하는 것은 가우시안 혼합 모델(Gaussian Mixture Model)입니다.(이 부분은 다음 포스팅에서 다룹니다.)

더보기

k-Means에선 input feature의 스케일을 맞추는 것이 중요합니다. 그렇지 않는다면 클러스터가 길쭉해지고, 좋지 않은 결과를 유발할 수 있습니다.


9.1.3 클러스터링을 활용한 Image Segmentation

 Image Segmentation은 이미지를 여러 Segment로 분할하는 작업입니다. 여기서 말하는 Segment란 사람, 자동차 등의 Object입니다. 이러한 Segmentation에서 최고 수준의 성능을 내려면 Neural network를 사용해야하는데, 여기선 훨씬 쉬운 작업인 Color Segmentation을 수행해보겠습니다.

 우선 이미지를 읽어옵니다.

images_path = os.path.join(PROJECT_ROOT_DIR, "images", "unsupervised_learning")
os.makedirs(images_path, exist_ok=True)
DOWNLOAD_ROOT = "https://raw.githubusercontent.com/rickiepark/handson-ml2/master/"
filename = "ladybug.png"
print("Downloading", filename)
url = DOWNLOAD_ROOT + "images/unsupervised_learning/" + filename
urllib.request.urlretrieve(url, os.path.join(images_path, filename))

from matplotlib.image import imread
image = imread(os.path.join(images_path, filename))
image.shape

 이미지는 3차원 배열로 표현됩니다(height, width, number of color channel). 이 경우엔 RGB로 표현된 3차원 벡터가 color channel입니다. 다만 흑백 이미지의 경우는 채널이 하나이고 투명도를 조절하는 경우 사용하는 알파 채널의 경우는 훨씬 더 많은 채널을 가집니다.

 다음은 color channel은 RGB 색상의 긴 리스트로 변환 다음 K-Means를 사용해 각각의 색상을 클러스터로 모읍니다.

X = image.reshape(-1, 3)
kmeans = KMeans(n_clusters=8, random_state=42).fit(X)
segmented_img = kmeans.cluster_centers_[kmeans.labels_]
segmented_img = segmented_img.reshape(image.shape)

 예를 들어 모든 초록색을 하나의 컬러 클러스터로 만들 수 있습니다. 그 후 해당 클러스터의 평균 컬러를 찾고, 모든 초록색을 해당 평균 컬러로 바꿀 수도 있습니다. 그 후 이 긴 Color 리스트를 원본 이미지와 동일한 크기로 바꿉니다.

 그 결과는 다음과 같습니다. 8개보다 클러스터 개수를 작게 하면 무당벌레의 화려한 빨간색이 독자적인 클러스터를 만들지 못하고 주위 색에 합쳐집니다. 나눌 수 있는 클러스터의 개수가 적어진다는 건 표현할 수 있는 색깔의 종류도 줄어든다는 의미이기 때문입니다.


9.1.4 클러스터링을 사용한 전처리

 클러스터링은 Supervised Learning을 위한 전처리 단계로 사용할 수 있습니다. 차원 축소에 클러스터링을 사용하는 예시를 위해 MNIST 데이터셋을 다뤄보겠습니다. 우선 데이터를 받아옵니다.

from sklearn.datasets import load_digits

X_digits, y_digits = load_digits(return_X_y=True)

 이를 Training과 Test set으로 나눕니다.

from sklearn.model_selection import train_test_split


X_train, X_test, y_train, y_test = train_test_split(X_digits, y_digits, random_state=42)

 그 후 Logistic Regression 모델을 훈련합니다.

from sklearn.linear_model import LogisticRegression

log_reg = LogisticRegression(multi_class="ovr", solver="lbfgs", max_iter=5000, random_state=42)
log_reg.fit(X_train, y_train)

 Test set에서 정확도를 평가해보겠습니다. 우선 별도의 전처리 없이 $96.9%$ 정확도를 얻었습니다.

log_reg.score(X_test, y_test)
>>
0.9688888888888889

  이제 K-Means를 전처리로 사용하여 성능이 더 좋아지는지 확인해보겠습니다. 우선 Training set을 50개의 클러스터로 나누고, 이미지를 50개 클러스터까지 거리로 바꿉니다.

from sklearn.pipeline import Pipeline


pipeline = Pipeline([
    ("kmeans", KMeans(n_clusters=50, random_state=42)),
    ("log_reg", LogisticRegression(multi_class="ovr", solver="lbfgs", max_iter=5000, random_state=42)),
])
pipeline.fit(X_train, y_train)

 정확도도 상당히 개선된 것을 확인할 수 있습니다.

pipeline.score(X_test, y_test)
>>
0.98

 클러스터링은 데이터셋의 Dimension을 64에서 50으로 감소시켰지만, 성능이 향상된 이유는 원본보다 변환된 데이터셋이 더 잘 구분할 수 있기 때문입니다.

 이번엔 GridSearchCV를 사용해 최적의 클러스터 개수를 찾은 후 다시 적용해보겠습니다.

from sklearn.model_selection import GridSearchCV

param_grid = dict(kmeans__n_clusters=range(2, 100))
grid_clf = GridSearchCV(pipeline, param_grid, cv=3, verbose=2)
grid_clf.fit(X_train, y_train)

 이제 최선의 $k$값과 파이프라인의 성능을 확인해보겠습니다. $k = 99$일 때 정확도가 크게 향상되고, Test set에서 98.22%를 달성했습니다.

grid_clf.best_params_
>>
{'kmeans__n_clusters': 57}


grid_clf.score(X_test, y_test)
>>
0.98

9.1.5 클러스터링을 사용한 Semi-Supervised Learning

 준지도학습(Semi-Supervised Learning)은 레이블이 없는 데이터가 많고, 레이블이 있는 데이터는 적을 때 사용합니다. MNIST 데이터셋에서 레이블된 50개의 데이터에 50개 데이터에 Logistic Regression을 훈련시켜보겠습니다.

n_labeled = 50
log_reg = LogisticRegression(multi_class="ovr", solver="lbfgs", random_state=42)
log_reg.fit(X_train[:n_labeled], y_train[:n_labeled])
log_reg.score(X_test, y_test)

>>
0.8333333333333334

 겨우 83.3%의 정확도입니다. 전체 데이터셋이 아닌 일부만 사용했기에 나온 당연한 결과입니다. 이제 이를 개선해보겠습니다. 우선 Training set을 50개의 클러스터로 모은 후, 각 클러스터에서 센트로이드에 가장 가까운 이미지를 찾습니다. 우리는 이를 대표 이미지라고 부르겠습니다.

k = 50
kmeans = KMeans(n_clusters=k, random_state=42)
X_digits_dist = kmeans.fit_transform(X_train)
representative_digit_idx = np.argmin(X_digits_dist, axis=0)
X_representative_digits = X_train[representative_digit_idx]

 다음이 바로 50개의 대표 이미지들입니다.

 이미지를 보고 수동으로 레이블을 할당해보겠습니다.

y_representative_digits = np.array([
    0, 1, 3, 2, 7, 6, 4, 6, 9, 5,
    1, 2, 9, 5, 2, 7, 8, 1, 8, 6,
    3, 1, 5, 4, 5, 4, 0, 3, 2, 6,
    1, 7, 7, 9, 1, 8, 6, 5, 4, 8,
    5, 3, 3, 6, 7, 9, 7, 8, 4, 9])

 이제 레이블된 50개의 또다른 데이터셋이 준비되었습니다. 다만 무작위가 아니라 각 클러스터들을 대표하는 이미지입니다.

log_reg = LogisticRegression(multi_class="ovr", solver="lbfgs", max_iter=5000, random_state=42)
log_reg.fit(X_representative_digits, y_representative_digits)
log_reg.score(X_test, y_test)
>>
0.9244444444444444

 성능은 분명 좋아졌습니다. 문제는 각 클러스터 내 데이터들은 해당 클러스터의 대표 이미지의 레이블이 부여되어있습니다. 이렇게 되면 클러스터 경계에 가깝게 위치한 데이터가 포함되어 있고, 잘못 레이블이 부여되어있을 확률이 농후합니다.

 따라서 모든 데이터가 아닌 각 클러스터에서 센트로이드에 가까운 20%의 데이터에만 대표 이미지의 레이블을 부여해보겠습니다.

percentile_closest = 20

X_cluster_dist = X_digits_dist[np.arange(len(X_train)), kmeans.labels_]
for i in range(k):
    in_cluster = (kmeans.labels_ == i)
    cluster_dist = X_cluster_dist[in_cluster]
    cutoff_distance = np.percentile(cluster_dist, percentile_closest)
    above_cutoff = (X_cluster_dist > cutoff_distance)
    X_cluster_dist[in_cluster & above_cutoff] = -1
    
partially_propagated = (X_cluster_dist != -1)
X_train_partially_propagated = X_train[partially_propagated]
y_train_partially_propagated = y_train_propagated[partially_propagated]

 이제 이 데이터셋을 모델에 훈련시켜보겠습니다.

log_reg = LogisticRegression(multi_class="ovr", solver="lbfgs", max_iter=5000, random_state=42)
log_reg.fit(X_train_partially_propagated, y_train_partially_propagated)
log_reg.score(X_test, y_test)

>>
0.9222222222222223

 레이블된 데이터 50개만으로 92%의 정확도를 얻었습니다. 이는 그만큼 잘 레이블된 데이터를 사용했기 때문입니다.

 

 

 

 

 


9.1.6 DBSCAN

 이 알고리즘은 모여있는 연속된 작은 지역들을 하나의 클러스터로 묶습니다. 작동 방식은 다음과 같습니다. 

  • 우선 알고리즘이 각 데이터마다 매우 작은 거리인 $\varepsilon$ 내에 몇개의 다른 데이터가 함께 있는지 셉니다. 이 지역을 데이터의 핵심 샘플($\varepsilon$-neighborhood)라고 부릅니다. (입실론은 보통 매우 작은 수를 말하므로 neighborhood라는건 데이터들끼리 매우 가까이 있다는 뜻입니다.)
  • (자기 자신을 포함해) 핵심 샘플 내에 적어도 min_samples개 데이터가 있다면 neighborhood라 칭하는 이 밀집된 지역을 Core-instance로 간주합니다.
  • 이렇게 전체 데이터셋에 핵심 샘플들을 파악했으면, 이젠 데이터가 아닌 핵심 샘플끼리 $varepsilon$ 내에 위치한다면 서로 이웃으로 판단하여 하나의 클러스터를 형성합니다.
  • 이렇게 데이터가 모여 핵심 샘플이 되고, 핵심 샘플이 모여 하나의 클러스터를 형성합니다. 그런데 핵심 샘플에도, neighborhood도 아닌 데이터는 이상치(Anomally data)로 판단합니다.

 이 알고리즘은 각 클러스터가 충분히 밀집되어 있고, 이것이 밀집되어있지 않은 지역과 잘 구분될 때 사용하면 좋습니다. 다음은 반달 모양 데이터셋에 sklearn의 DBSCAN을 적용한 코드입니다.

from sklearn.datasets import make_moons
from sklearn.cluster import DBSCAN

X, y = make_moons(n_samples=1000, noise=0.05, random_state=42)
dbscan = DBSCAN(eps=0.05, min_samples=5)
dbscan.fit(X)

 모든 데이터의 레이블은 labels_ 변수에 저장되어 있습니다.

dbscan.labels_[:10]
>>
array([ 0,  2, -1, -1,  1,  0,  0,  0,  2,  5])

 데이터의 레이블이 $-1$이란 것은 이상치라는 뜻입니다. 핵심 샘플의 인덱스는 core_sample_indices에서 확인할 수 있으며, 핵심 샘플 자체는 components_에 저장되어 있습니다.

len(dbscan.core_sample_indices_)
>> 
808

dbscan.core_sample_indices_[:10]
>>
array([ 0,  4,  5,  6,  7,  8, 10, 11, 12, 13])

dbscan.components_[:3]
>>
array([[-0.02137124,  0.40618608],
       [-0.84192557,  0.53058695],
       [ 0.58930337, -0.32137599]])

 다음 클러스터링 결과입니다. 왼쪽의 경우 클러스터를 7개 만들었고, 많은 데이터를 이상치로 판단했습니다. $X$는 이상치를 의미하는데 상당히 많습니다. 그러나 eps = 0.2로 설정하여 데이터의 neighborhood 범위를 넓히면 오른쪽처럼 완벽한 클러스터링을 얻습니다.

 한 가지 유의할 점이 있습니다. DBSCAN 클래스는 predict() 메서드가 아닌 fit_predict() 메서드를 제공합니다. 즉 이 알고리즘은 새로운 데이터에 대한 클러스터를 예측할 수 없습니다. 이러한 일은 다른 알고리즘이 더 잘 수행할 수 있기 때문입니다. 그러므로 별도의 예측기를 정의해야합니다. 여기선 KNeiborsClassifier를 사용합니다.

from sklearn.neighbors import KNeighborsClassifier


knn = KNeighborsClassifier(n_neighbors=50)
knn.fit(dbscan.components_, dbscan.labels_[dbscan.core_sample_indices_])

 이제 데이터 몇개를 전달하여 어떤 클러스터에 속할 가능성이 높은지 예측하고 각 클러스터에 대한 확률을 추정해보겠습니다.

X_new = np.array([[-0.5, 0], [0, 0.5], [1, -0.1], [2, 1]])
knn.predict(X_new)
>>
array([1, 0, 1, 0])

knn.predict_proba(X_new)
>>
array([[0.18, 0.82],
       [1.  , 0.  ],
       [0.12, 0.88],
       [1.  , 0.  ]])

 위 Classifier는 핵심 샘플만 훈련했지만, 모든 데이터 혹은 이상치를 제외한 데이터만을 훈련시킬 수도 있습니다. 다음은 이상치를 제외한 데이터를 훈련시켜 만든 결정 경계입니다. 

 이상치가 없기 때문에 Classifier는 무조건 하나의 클러스터를 선택합니다. knn.kneighbors() 메서드에 데이터를 전달하면 가장 가까운 k개 이웃의 거리와 인덱스를 반환합니다.

y_dist, y_pred_idx = knn.kneighbors(X_new, n_neighbors=1)
y_pred = dbscan.labels_[dbscan.core_sample_indices_][y_pred_idx]
y_pred[y_dist > 0.2] = -1
y_pred.ravel()
>>
array([-1,  0,  1, -1])

 DBSCAN은 매우 간단하고 강력합니다. 또한 클러스터의 모양이나 개수에 상관없이 감지할 수 있는 능력이 있으며 이상치에 안정적이고 하이퍼파라미터도 적습니다.

 그러나 클러스터 간 밀집도가 크게 다르면 모든 클러스터를 올바르게 잡아내는 것이 불가능합니다.


다음 시간에는 이어서 Gaussian mixture model에 대해서 알아보겠습니다. 긴 글 읽어주셔서 감사합니다. 행복한 하루 보내세요 :)

반응형