주제

0. 비지도 학습
1. K-means 클러스터링

2. 이상 탐지
3. Isolation Forest
4. K-means 클러스터링과 이상 탐지 실습

 

 

0. 비지도 학습(Un-supervised Learning)

  • 정답 레이블이 지정되지 않은 데이터로부터 구조나 패턴을 자동으로 탐색하는 학습 방법론
  • 고객 세분화, 이상 탐지, 대규모 데이터셋의 구조 파악 등의 사용
  • 특징 및 장점 :
    • 수동으로 데이터의 정답을 생성할 필요가 없음. 비용과 시간 절약
    • 데이터 내부의 구조를 탐색 → 다양한 통찰
    • 다양한 데이터 유형과 복잡한 구조에도 적용 가능
  • 단점 :
    • 결과를 해석하기 어려울 수 있음
    • 명확한 정답이 없으므로 모델 성능의 객관적인 평가가 어려움
    • 노이즈에 매우 민감함

대표 문제

  • 군집화(Clustering) : 데이터를 유사한 특성을 가진 하위 그룹(sub-group) 또는 클러스터로 분할
  • 차원 축소(Dimensionality Reduction) : 고차원 데이터의 특성을 줄여 더 낮은 차원의 표현으로 만드는 과정
  • 이상 탐지(Anomaly Detection) : 데이터에서 비정상적인 패턴, 이상치, 또는 예외적인 사례를 탐지

대표 알고리즘

  • 군집화(Clustering)
    • K-means : 데이터를 K개의 클러스터로 세분화
    • 계층적 군집화(Hierarchical Clustering) : 데이터 포인트를 개별 클러스터로 가정하여 점차 유사한 클러스터를 병합하거나 큰 클러스터를 세분화하는 방식
    • DBSCAN(Density-Based Spatial Clustering of Applications with Noise) : 데이터가 모여있는 밀도를 기반으로 클러스터 형성
  • 차원 축소(Dimensionailty Reduction)
    • 주성분 분석(PCA) : 데이터의 분산을 보존하는 방향의 축을 찾아 해당 축을 기준으로 고차원 데이터를 저차원으로 변환
    • t-SNE : 고차원의 데이터를 보존하며 저차원으로 매핑. 시각화 과정에서 매우 유용
    • 오토인코더(Autoencoder) : 신경망(딥러닝)을 이용하여 저차원으로 압축 후, 다시 원래 차원으로 복원하는 방식
  • 이상 탐지(Anomaly Detection)
    • Isolation Forest : Tree를 기반으로 특정 데이터 포인트를 격리시키는 데 필요한 분할 수를 기준으로 이상치 탐지
    • One-Class SVM : 정상 데이터만을 활용해 "정상"이라는 클래스로 SVM을 학습하고, 정상 패턴에서 벗어나는 데이터를 이상치로 판단
    • LOF(Local Outlier Factor) : 데이터 밀도를 계산하여 정상 데이터는 주변에 높은 데이터 밀도를 갖고, 이상치 데이터는 낮은 밀도를 갖고 있음을 활용

 

 

1. K-means 클러스터링

  • 전체 데이터를 K개의 덩어리(클러스터)로 나누는 비지도 학습법
  • 사용되는 알고리즘 :
    • 로이드(Lloyd) 알고리즘
    • 엘칸(Elkan) 알고리즘
  • 로이드 알고리즘이 가장 기본적 : 
    • 초기화 → 할당 → 업데이트 → 반복
  • 엘칸 알고리즘 :
    • 데이터 포인트와 클러스터 중심 거리를 계산하는 삼각 부등식을 사용
    • |a + b| ≤ |a| + |b|

1. 초기화

  • K개의 클러스터 중심점을 임의로 선택
  • 초기 위치는 최종 결과에 큰 영향을 미칠 수 있음
  • k-means++ 초기화 방법을 사용 → 초기 중심점의 위치를 멀리 떨어지게 설정. 임의의 랜덤 위치보다 좋은 결과를 보임

초기화로 인한 3개의 K(클러스터 중심점) 설정

2. 할당

  • 각 데이터 포인트를 가장 가까운 클러스터 중심에 할당
  • 유클리드 거리(Euclidean Distance)를 사용하여 거리 계산 진행
  • 대안은 코사인 유사도, 맨해튼 거리가 있음

각 중심점에 가까운 데이터 포인트 할당

3. 업데이트

  • 각 클러스터에 속한 데이터들의 평균점 위치로 클러스터 중심의 위치를 업데이트

평균점으로 중심점 업데이트

4. 반복

  • 클러스터 중심의 변화가 미미할 때까지 할당 과정과 업데이트 단계 반복
  • 변화가 미미함?
    • 위치의 변화가 없거나
    • 클러스터에 할당되는 데이터 포인트의 변화가 없거나
    • 동일한 데이터 포인트 할당 과정이 반복되거나
    • 지정된 횟수에 도달하거나 등

반복한 후의 모습

 

1.1 엘보우 방법(Elbow Method)

  • K를 적절하게 선택한다면 좋은 결과를 보여줄 수 있음
  • 하지만 K가 너무 작거나 크다면 여러 문제에 봉착함
  • 따라서 적절한 K를 골라야 하는데, 이에 대한 근거를 제공할 수 있는 방법이 엘보우 방법
  • 엘보우 방법 : 클러스터 수를 늘려가며 각각에 대한 클러스터링 성능을 측정해, 클러스터에 따른 성능 변화를 분석
  • K는 일반적으로 극히 작은 값(1)에서부터 매우 큰 값까지 사용 가능
  • 클러스터링 성능 평가 척도 :
    • SSE(Sum of Squared Errors) : 각 클러스터 내의 데이터 포인트와 클러스터 중심 간의 거리의 제곱 합
    • 실루엣 계수(Silhouette Coefficient) : 응집도와 분리도를 동시에 고려해 군집화의 품질을 평가
  • 그래프 상 SSE의 감소율이 급격히 줄어드는 지점이 최적 클러스터 수

1.1.1 실루엣 계수

  • 응집도(Cohesion) : a(i)
    • 특정 데이터 i에 대해, 동일한 클러스터 안에 들어있는 다른 데이터들과의 평균 거리
    • 클러스터 내부의 데이터가 얼마나 모여있는지를 나타냄
  • 분리도(Separation) : b(i)
    • 특정 데이터 i에 대해, i가 들어있는 클러스터 말고 다른 클러스터 중 가장 가까운 클러스터 중심까지의 거리
    • 다른 클러스터와 얼마나 떨어져있는지를 나타냄
  • 실루엣 계수 : s(i)
    • 최대값 : 1 → a(i)가 거의 0에 근접해 b(i)만 남는 상황 : 제일 좋은 상황
    • 최소값 : -1 → b(i)가 값이 작아지고 오히려 a(i)가 커지는 경우 : 제일 나쁜 상황

 

실루엣 계수

 

실습

  • sklearn의 make_blobs로 가사의 군집 데이터 생성
from sklearn.datasets import make_blobs

# 가상 데이터 생성
X, y_true = make_blobs(n_samples=300,
                       centers=4, # Cluster의 수 혹은 Cluster의 중심 위치 좌표
                       cluster_std=0.60, # 각 클러스터가 얼마나 퍼져있게 할건지
                       n_features=2, # 사용할 feature의 수, 시각화를 위해 2차원 사용
                       random_state=0)
# 데이터 시각화
plt.scatter(X[:, 0], X[:, 1], s=20)
plt.title("Generated Data")
plt.show()

생성된 데이터

  • K-means 클러스터링 진행하여 중심점과 같이 시각화
from sklearn.cluster import KMeans

# K-Means 클러스터링 수행 (k=4)
kmeans = KMeans(n_clusters=4,
                init='k-means++', # 초기 클러스터 중심 위치를 잡는 초기화 방법
                n_init=10, # 초기 중심점 잡기를 얼마나 많은 다른 방법으로 설정할지를 결정 / 10번의 시도를 하겠다는 것이고, 10번 중 최고의 SSE 성능을 보이는 모델을 최종 모델로 사용
                random_state=0)
kmeans.fit(X)
y_kmeans = kmeans.predict(X)

클러스터링 결과

  • 엘보우 방법으로 최적의 K 찾기
# 1~10 까지의 K를 설정하고 각 경우에 맞춰 학습을 진행
inertia = []
K_range = range(1, 10)
for k in K_range:
    kmeans = KMeans(n_clusters=k, init='k-means++', n_init=10, random_state=0)
    kmeans.fit(X)
    inertia.append(kmeans.inertia_) # SSE 값을 저장

4에서 급격한 기울기 변화

  • 실루엣 계수 이용하기
from sklearn.metrics import silhouette_score

silhouette_avg = silhouette_score(X, y_kmeans)
print("Silhouette Score: {:.2f}".format(silhouette_avg))

일반적으로 0.7정도면 좋은 점수라고 본다

 

 

 

2. 이상 탐지(Anomaly Detection)

  • 데이터에서 비정상적인 패턴, 이상치, 또는 예외적인 사례를 탐지하는 과정
전통적인 통계 기반 이상 탐지 머신 러닝을 활용한 이상 탐지
계산이 복잡하지 않고 해석이 직관적 복잡한 데이터의 패턴에 적응적, 지속적인 학습과 개선이 가능
데이터가 대칭이 아니거나 복잡한 분포를 갖고 있다면 오인식이 늘어남 처리할 모델이 복잡할 수 있고, 사람이 해석하기 어려운 패턴이 존재할 가능성 있음

 

 

3. Isolation Forest(고립숲)

  • 개념
    • 정상 데이터는 비슷한 특성 패턴을 갖는 비슷한 데이터와 밀도 있게 모여있을 것.
    • 반면 이상치는 밀도가 낮은 공간에 존재
    • 하나의 데이터를 '고립'하도록 어떤 특성과 그것의 분할 값을 기준으로 나눔
    • 정상 데이터는 밀도가 ↑, 많은 분할 과정이 지나야 고립됨
    • 이상치 데이터는 밀도가 ↓, 적은 분할 과정으로도 쉽게 고립됨

3.1 작동 과정

  • 데이터 준비 : 전체 데이터에서 무작위로 서브 데이터셋을 선택(어떤 데이터는 여러 서브 데이터셋에 속할 수 있음)

무작위로 선택한 서브 데이터 셋 3그룹

  • 트리 생성 : 하나의 서브 데이터셋마다 개별 Isolation Tree를 준비

각자의 트리 준비

  • 고립 시작 : 각 트리에 소속된 데이터셋을 하나씩 고립
    • 임의의 특성과 임의의 특성 분할 값을 선택
    • 데이터셋이 리프 노드에 도달할 때까지 반복
    • 각 데이터 포인트는 리프 노드까지 도달한 깊이를 저장함

각 데이터 포인트의 깊이 저장

  • 고립 완료 : 하나의 트리에 소속된 모든 데이터셋을 전부 고립
  • 트리 계산 완료 : 모든 트리에서 고립 과정이 완료됨. 여러 트리에 소속된 데이터 포인트는 서로 다른 트리에서 계산된 깊이가 저장됨

각 트리의 깊이 계산 완료

  • 이상치 점수 계산 : 평균 깊이를 바탕으로 각 데이터 포인트의 이상치 점수를 계산
  • 이상치 추출 : 이상치 점수와 특정 임계값을 비교해 이상치 추출

이상치 추출

 

실습

  • IsolationForest() 사용해보기
    • n_estimators : 고립 트리의 수. 서브 데이터셋의 수와 같음. 더 많은 트리를 사용할수록 탐지의 안정성과 정확성 증가
    • max_samples : 서브 데이터셋에 포함될 최대 데이터 포인트의 수
    • contamination : 전체 데이터셋에서 이상치 데이터가 차지할 비율
      • 이는 데이터 내의 이상치가 어느 정도 있음을 알고 있는 상황에서 유용
      • 만약 모른다면 'auto' 사용
  • 이상치 데이터 정답을 알고 있다면 즉, 어떤 데이터가 이상치인지 알고 있다면 이상 데이터를 양성으로 놓고 분류 형태의 문제로 평가 가능
  • 정확도, 정밀도, 재현율, F1값으로 평가 진행
  • 만약 정답이 없다면 시각화를 통한 전문가 검토를 진행
# Isolation Forest 모델 생성을 위한 매개 변수
n_estimators = 100  # 사용할 트리의 수
max_samples = 'auto'  # 각 트리당 최대 샘플 수
contamination = 0.15  # 전체 데이터셋에서 이상치가 차지하는 비율의 추정치
                      # 이 값을 이용해 추후 이상치를 찾아냄
                      # 이상치 점수가 이 임계값보다 높은 데이터 포인트는 이상치로 간주

# Isolation Forest 생성 및 학습
from sklearn.ensemble import IsolationForest
IForest = IsolationForest(n_estimators=n_estimators,
                          max_samples=max_samples,
                          contamination=contamination,
                          random_state=seed)
IForest.fit(X)

이상 탐지를 위해 생성한 데이터

 

  • 이상치 추정한 후 시각화
# 모든 데이터에 대한 이상치 점수 계산
# 이 값은 트리 내에서의 평균 경로 depth를 기반으로 계산됨
# 정규화 된 값으로,
# 점수가 낮으면 평균 길이가 짧다고 보면 되며 이상치일 가능성이 ↑

scores_pred = IForest.decision_function(X)

이상치로 추정되는 데이터 포인트는 동그라미로 표시

  • 이상치 여부 결정한 후 정상 데이터와 이상치 데이터 분리
# 이상치 여부 결정
IForest.predict(X)

is_outlier = IForest.predict(X) == -1
is_outlier

# 정상 데이터와 이상치 데이터 분리
X_normal_detected = X[~is_outlier]
X_outliers_detected = X[is_outlier]

 

 

 

4. K-means 클러스터링과 Isolation Forest 실습

4.1 K-means clustering

 

Mall customers

We use K-means clustering algorithm majorly in this kernel.

www.kaggle.com

  • 문제 : 주어진 고객 데이터를 바탕으로 고객을 세분화 군집화

군집화는 어떤 문제의 결론이 되어서는 안된다!! 군집화는 결론에 도달하기 위한 수단일 뿐. 해당 데이터셋의 경우 군집화를 통한 타겟 마케팅 전략 수립, 신규 고객 유치 등의 결론으로 이어질 수 있다.

 

4.1.1 데이터 전처리 및 EDA

  • 데이터 생김새 확인

데이터

  • 고객 ID는 고유 식별자로 학습에서 제외
  • 성별은 인코딩 필요
  • 나이, 연간 소득, 쇼핑 점수는 각자 서로 다른 스케일을 갖고 있음
  • 성별 인코딩
# 성별을 이진 변수로 변환
encoding_map = {'Female': 1, 'Male': 0}
categori_data_encode = pd.DataFrame(customers['Genre'].replace(encoding_map))
categori_data_encode.columns = ['Gender']
categori_data_encode
  • 결측치 없음, 유의미한 이상치 없음
  • 상관관계 확인 → 유의미한 관계 없음
  • 분포 확인

수치형 데이터 분포 확인

  • 수치형 데이터 스케일링 → 세 컬럼 모두 정규 분포와 비슷한 모습을 갖고 있어 StandardScaler 사용 결정
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()

# 수치형 데이터 스케일링
numeric_data = customers[numeric_columns]
numeric_data_scaled = scaler.fit_transform(numeric_data)
numeric_data_scaled = pd.DataFrame(numeric_data_scaled)
numeric_data_scaled.columns = numeric_columns
numeric_data_scaled

 

4.1.2 모델 구축 및 결과 확인

  • 엘보우 방법을 이용한 K value 서칭(시각화 부분은 편의상 생략)
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score

inertia, silhouette = [], []
K_range = range(2, 11)
for k in K_range:
    kmeans = KMeans(n_clusters=k, init='k-means++', n_init=10, random_state=0)
    kmeans.fit(customers_combined)
    inertia.append(kmeans.inertia_)

    y_kmeans_silhouette = kmeans.predict(customers_combined)
    silhouette.append(silhouette_score(customers_combined, y_kmeans_silhouette))

SSE와 실루엣 계수 시각화

  • 4와 6이 가장 높지만 이 경우 K=6을 선택
  • 실루엣 계수의 변동이 있는 것은 매우 일반적인 현상
    • 데이터 구조의 복잡성, 잡음과 이상치, 균일하지 않은 밀도 & 데이터 간 거리
  • 실루엣 계수 간 차이가 크지 않다면 작은 K를 기준으로 하는 게 좋은 선택(이번 실습은 예외)

 

  • 학습 진행
kmeans = KMeans(n_clusters=6,
                init='k-means++',
                n_init=10,
                random_state=0)
kmeans.fit(customers_combined)
  • 평가 진행
    • 0.36 정도는 어느 정도 군집화가 의미있게 진행됐다고 볼 수 있음
    • 개선의 여지는 있음
y_pred = kmeans.predict(customers_combined)
silhouette_avg = silhouette_score(customers_combined, y_pred)

print("SSE Value : {:.2f}".format(kmeans.inertia_))
print("Silhouette Score: {:.2f}".format(silhouette_avg))

  • 결과 해석을 위해 차원 축소
    • 4차원 데이터를 시각화하기 위해 t-SNE를 이용하여 2차원으로 축소 과정 진행
# 우리가 사용한 4차원 데이터를 시각화하기 위해 2차원 데이터로 변환 (t-SNE 알고리즘 활용)
from sklearn.manifold import TSNE

tsne = TSNE(n_components=2, random_state=seed)
customers_tsne = tsne.fit_transform(customers_combined)

# 2차원으로 변화된 데이터에 feature 이름을 넣어주고
# K-means가 예측한 각 데이터의 클러스터링 인덱스를 제공
customers_tsne_df = pd.DataFrame(data=customers_tsne, columns=['TSNE1', 'TSNE2'])
customers_tsne_df['Cluster'] = y_pred

customers_tsne_df
# 시각화
np.random.seed(seed)

plt.figure(figsize=(10, 8))

for idx in range(kmeans.n_clusters):
    _color = (np.random.random(), np.random.random(), np.random.random())
    cluster_data = customers_tsne_df[customers_tsne_df['Cluster'] == idx]
    plt.scatter(cluster_data['TSNE1'],
                cluster_data['TSNE2'],
                color=_color,
                label=f'Cluster {idx+1}',
                marker='o')
plt.title('Customer Segments - t-SNE 2D')

plt.legend()
plt.show()

군집화가 잘 진행된 모습

  • 클러스터 별로 의미를 찾으려면 원래 데이터로 재건하는 과정을 거쳐야 함(지금은 스케일링 데이터이기 때문)
# 원래 데이터에서 재건하기
customers_combined['Cluster'] = kmeans.labels_

# 수치형 데이터 스케일링 작업을 역으로 수행하기
original_numeric_data = pd.DataFrame(scaler.inverse_transform(customers_combined[numeric_columns]))
original_numeric_data.columns = numeric_columns

# 범주형 데이터 역 인코딩 (필요시)
# 수치형으로 되어있던 데이터를 다시 범주형으로 바꾸면 특성 파악이 눈에 안들어올 수 있음!
reverse_encoding_map = {v: k for k, v in encoding_map.items()}
original_category_data = pd.DataFrame(customers_combined['Gender'].replace(reverse_encoding_map))
original_category_data.columns = category_columns

# pandas로 원래 데이터 다시 만들기
labeled_origin_date = pd.concat([customers_combined['Cluster'],
                                 original_numeric_data,
                                 customers_combined['Gender']],
                                #  original_category_data], # 범주형 데이터 필요시 사용
                                axis=1)
labeled_origin_date

이런식으로 클러스터별 통계를 확인하여 해석을 진행할 수 있음

 

4.1.3 결과 해석

  • 실습에서는 포함되지 않았지만 타겟 마케팅 전략 도출이라는 문제로 개인적인 해석 도출을 해보자면 :
    • 2번 클러스터 : 젊은 나이대(평균 나이 20대)의 고객들은 연간 소득이 높지 않음에도 소비를 많이 하는 것(77점)으로 나타났다
    • 4번 클러스터 : 반면, 중년(평균 나이 45세) 고객들은 연간 소득이 2번 그룹과 비슷함에도 소비를 하지 않는다(19점)
    • 1번 클러스터 : 평균 연간 소득이 높으면서 평균 나이가 32세인 그룹은 소비도 많이 한다
    • 5번 클러스터 : 평균 연간 소득이 가장 높으면서 평균 나이가 41세인 그룹은 반대로 소비도 가장 적게 하는 모습이다.
  • 이에 따르면 연령이 20-30대로 젊은 고객들은 연간 소득에 구애받지 않고 높은 소비율을 보이는데
  • 반면 40대 이상의 고객들은 연간 소득에 상관없이 낮은 소비율을 보인다.
  • 충성 고객이 될 확률이 높은 젊은층 고객들을 위한 아이템을 더 선보이는 방안을 제시해볼 수 있다
  • 높은 연령대의 고객들의 경우, 그들의 소비 아이템의 트렌드를 파악하여 소비를 격려할 수 있는 쿠폰이나 할인 이벤트를 진행해볼 수 있다.

 

 

4.2 Isolation Forest를 이용한 이상 탐지 실습

 

Credit Card Fraud Detection

Anonymized credit card transactions labeled as fraudulent or genuine

www.kaggle.com

  • 문제 : 주어진 거래 관련 데이터를 바탕으로 이상 거래 데이터 탐지
  • 입력 :
    • Time : 첫 거래 후 각 거래 사이 경과 시간(초)
    • Amount : 거래 금액
    • V1 ~ V28 : PCA로 얻은 수치형 입력 변수
  • 출력 : 
    • 각 데이터 포인트마다 할당된 이상치 점수

 

4.2.1 데이터 전처리 및 EDA

  • Time은 주기성을 나타내어 주기 함수를 적용하는 것이 좋지만 이 경우에는 큰 이상치가 없어보이므로 MinMaxScaling 사용
  • Amount은 치우침이 있어 log 스케일링 진행

Time과 Amount의 분포 확인

# Amount에 Log scaling 적용
credit['Log_Amount'] = np.log(credit['Amount'] + 1)
credit

# Time 변수를 Min Max Scaling
from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler()

credit_time = scaler.fit_transform(credit[['Time']])
credit_time = pd.DataFrame(credit_time)
credit_time.columns = ['Time']
credit_time
  • V1 ~ V28은 분포가 대부분 정규분포를 따름. 따라서 스케일링을 진행하지 않고 그대로 사용
  • 스케일링을 한 데이터를 합친 데이터 프레임 생성

 

4.2.2 모델 구축 및 결과 확인

  • Isolation Forest 학습 진행
    • 원본 데이터에서 이상치의 비율을 이미 알려주었기 때문에 contamination은 해당 값을 사용
n_estimators = 100
max_samples = 'auto'
# contamination = 'auto'
contamination = num_Fraud/(num_Normal+num_Fraud)

# Isolation Forest 생성 및 학습
from sklearn.ensemble import IsolationForest
IForest = IsolationForest(n_estimators=n_estimators,
                          max_samples=max_samples,
                          contamination=contamination,
                          random_state=seed)
IForest.fit(credit_combined)
  • 모델 평가 진행
y_true = credit['Class']

y_pred = IForest.predict(credit_combined)
y_pred = np.where(y_pred == 1, 0, 1)

# 성능 평가
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score

accuracy = accuracy_score(y_true, y_pred)
precision = precision_score(y_true, y_pred)
recall = recall_score(y_true, y_pred)
f1 = f1_score(y_true, y_pred)

print(f'정확도 Accuracy : {accuracy*100:.2f} %')
print(f'정밀도 Precision : {precision*100:.2f} %')
print(f'재현율 Recall : {recall*100:.2f} %')
print(f'F1 : {f1*100:.2f} %')

  • 이상치 데이터는 전체 데이터에서 극 소수(0.172%)이므로 정밀도, 재현율, F1 값이 높지 않음
  • Confusion Matrix를 통해 확인
    • 실제 사기 거래인 34건의 거래 중 11건만 True Positive로 탐지

 

 

 

 

 

 


느낀점

최근에 개인적으로 RFM Segmentation을 이용한 고객 세분화 프로젝트를 진행중이었는데 다른 사람들의 노트북을 보니 K-means가 자주 눈에 띄었다. 그래서 궁금했는데 이번 강의를 통해서 어떤 것인지 알게 되었고 해당 프로젝트에 오늘 배운 내용을 적용해볼 수 있을 것 같아서 기대된다. 

+ Recent posts