Study/머신러닝

머신러닝 BASIC _ 비지도학습 방법론

김 도경 2024. 10. 31. 21:20

[2024.10.31] 필수 온라인 강의 Part15 Machine Learning Basic CH05 비지도학습 방법론

 

비지도학습

 

  • 비지도학습(Unsupervised Learning)
    Supervision, 즉 학습용 라벨(Y_train)를 활용하지 않고 입력 데이터(x)로만 모델을 학습하는 방법론

    - 입력 데이터 사이의 유사성, 관계를 활용
    - 행렬분해 등, 별도 방법으로 데이터 행렬의 구조를 분석
    - 입력값에 내재된 정보로 가상의 라벨을 생성
    - 자기 자신(x)을 라벨로 활용
    평가용 라벨(Y_test)은?
         -> 활용하는 경우도 있고, 아닌 경우도 있음… 즉, 상관없음.

  • 군집화 (Clustering)
     - 데이터로부터 패턴을 파악해 데이터를 여러 개의 군집(cluster)으로 나누는 것.
     - 서로 비슷한 데이터는 같은 그룹, 서로 다른 데이터는 다른 그룹으로.
       -> 데이터들의 유사성을 기반으로 분리. 비슷한(가까운) 데이터는 같은 그룹으로, 다른(먼) 데이터는 다른 그룹으로
    - 주어진 샘플들을 어떻게 나누어야 가장 유사한 샘플끼리 묶이게 될지 데이터 간 관계를 비교해 학습

  • 분류 문제(classification)
    - 모델의 출력값과 레이블을 비교하여 모델이 점점 레이블에 가까운 출력을 내놓도록 학습
    - 주어진 피쳐를 입력값으로 사용해, 클래스를 추론하고 모델의 결과와 레이블을 비교해 학습

  • 비지도 학습이 필요한 이유
    1. 데이터 라벨링
       - 데이터 라벨링(어노테이션) 작업은 상당한 비용과 시간을 필요로 하는데, 비지도학습은 이 작업이 필요 없으므로 많은 데이터를 쉽게 구할수 있음.
    2. Manifold Hypothesis
       - 일반적인 고차원 데이터들은 가능한 모든 데이터의 공간에 완전히 고르게 분포하는게 아니라, 상대적으로 매우 적은 차원의 매니폴드(곡면과 유사한 개념)를 이루고 있을 것
       - Manifold란? 고차원의 공간 속에서 저차원 공간처럼 보이는 수학적 공간을 의미

  • 차원축소 (Dimensionality Reduction)
    - 고차원 데이터의 정보를 최대한 보존하면서 훨씬 적은 차원으로 표현할 수 있는 방법을 찾는것
        ex) PCA (주성분분석, Principal Component Analysis)

     - 노이즈 제거 (Denoising)
        - 차원축소를 하면 정보가 유실된다는 사실을 역으로 활용해, 원본 데이터의 노이즈를 제거하는 방식으로 활용

    - 시각화 (Visualization)
         - 고차원의 데이터를 차원축소해 그 구조를 사람이 알기쉽도록 보여주는 방법
         - t-SNE (t-distributed Stochastic Neighbor Embedding)

  • 매니폴드의 구조
     - 군집화 (Clustering)
          - 데이터 간의 유사성, 관계를 통해 여러 개의 자연스러운 군집(cluster)으로 나누는 문제
          - k-Means  , DBSCAN (Density-based Spatial Clustering of Applications with Noise)
               * 앞의 차원축소와 달리, 데이터를 변형하지 않는다.

    - 이상값 탐지 (Anomaly Detection)
       - 일반적인 인풋값과 전혀 다른, 이상값을 찾아내는 문제 매니폴드 구조를 학습하고 거기에서 벗어나있는 데이터포인트를 찾음
            - 엑스레이 사진들을 모아둔 데이터셋에서, 목걸이를 하고 찍은 것 등 판독불가 샘플을 걸러내기
            - 공장 컨베이어벨트에서 센서 데이터를 통해 불량품을 확인

  • embedding된 저차원 공간에서 각 피쳐의 의미
    - 자연어처리(NLP, Natural Language Process)에서 사용하는 방법론
    - 영어나 한국어같은 자연어 단어들을 의미적인 연산이 가능한 임베딩 공간에 맵핑시키는 기술

       - 생성모델 (Generative Models)
           - 저차원 임베딩 공간에서 원래의 이미지 공간으로가는 함수를 학습해, 기존에 없던, 새로운 feature를 가진 샘플을 만듬
       - Representation Learning         
고차원 데이터의 이해
  • 고차원(high-dimensional)데이터
      - 데이터 내 feature가 많아 차원(dimension)이 높은 데이터
      - 현실은 원래 고차원
          - resize 등으로 차원수를 낮추는게 가능하지만, 정보 손실이 생김

    -> 데이터를 표현하는데 필요한 차원이 부족할 경우, 제대로된 분석이 힘들 수 있다.

  • 첫번째 문제: 컴퓨터 자원의 사용량
     - 데이터의 차원 수가 커질수록, 
     - 더 많은 정보를 학습하기 위해 모델도 더 커지는데,
     - 그러면 연산량, 메모리 사용량 등이 같이 증가하고,
     - 결과를 보기 위해 더 오래 기다리거나 더 좋은 장비… 즉 더 많은 비용이 필요하게 된다.

  • 두번째: 차원에 따른 데이터의 Sparsity 문제 : 차원의 저주
     - 고차원 데이터에서는 데이터 간의 거리가 지수적으로 증가
     - 공간 내 데이터의 밀도가 낮아져 모델 학습이 어려워짐

  • 세번째: 직관적으로 이해하기 너무나 힘든 고차원 공간
    - 익숙하다고 생각한 개념들이 고차원 공간에서는 말도 안되는 특성을 보이는 경우가 많음
    - 정규분포(normal distribution) , 구(Sphere), 상자(Box)

PCA를 이용한 차원축소와 시각화

- 고차원 데이터가 가진 정보를 최대한 보존하면서 저차원으로 표현하는 방법인 차원축소
- PCA(Principal Component Analysis)와 그 이론적 기반인 SVD(Singular Vector Decomposition) 행렬분해
- t-SNE(t-distributed Stochastic Neighbor Embedding)를 사용해 고차원 데이터를 2차원 평면에 시각화

  • SVD 구현을 위한 이론
    - SVD (Singular Vector Decomposition)
       : 특이값 행렬분해, SVD란 행렬 MRm×n를 다음과 같이 특별한 성질을 가진 3개의 행렬들의 곱으로 나타내는 과정

        - U는 m차원 정규 직교 행렬 (orthonormal matrix)
        - Σ(sigma)는 singular value를 성분으로 하는 대각 행렬(diagonal matrix)
        - 
    V는 n차원 정규 직교 행렬 (orthonormal matrix)

  • SVD 구현을 위한 보조함수 정의
    - 행렬 시각화를 위한 보조함수 plot_matrix 정의
    - 랜덤 데이터행렬을 생성한 뒤 plot_matrix함수를 통해 시각화
# 행렬 시각화를 위한 보조함수 정의
def plot_matrix(matrix, numbers=True, size_scale=0.7):
    n_rows, n_cols = matrix.shape
    # 행렬 크기에 비례하도록 figure의 사이즈 설정
    figure_size = (size_scale * n_cols, size_scale * n_rows)
    fig, ax = plt.subplots(figsize=figure_size)
    # 불필요한 부분들 비활성화
    viz_args = dict(cmap='Purples', cbar=False, xticklabels=False, yticklabels=False)
    sns.heatmap(data=matrix, annot=numbers, fmt='.2f', linewidths=.5, **viz_args)

- matrix로 주어진 행렬을, 보기쉽게 그려주는 함수입니다.
  - 행렬의 크기가 큰 경우 numbers를 False로 설정해 각 원소의 값이 표시되지 않도록 하고, size_scale을 줄여 전체 크기를 조절

# 실습파일을 여러번 실행해도 같은 결과가 나오도록 random seed를 고정합니다.
np.random.seed(1234)

# 6x9 크기의 랜덤 행렬을 생성합니다.
M = np.random.randn(6, 9)

# 단순 print을 이용하여 행렬 시각화
print(M)

 

보조함수를 활용하여 행렬 시각화

plot_matrix(M)

 

  • SVD 구현 실습
    - numpy 라이브러리를 활용하여 수행
    - numpy에서 SVD를 수행해주는 이 함수(np.linalg.svd)는 대각행렬 sigma의 0인 부분을 생략하고 대각성분인 singular value들만 array형태로 리턴해주도록 구현
def full_svd(matrix):
    # numpy를 이용한 SVD를 수행합니다.
    U, singular_values, V = np.linalg.svd(matrix)

    # numpy의 svd 결과로 나오는 sigma의 diagonal 성분을 가지고 diagonal matrix를 복원해줍니다.
    m, n = matrix.shape       # matrix 행렬의 차원
    sigma = np.zeros([m, n])  # matrix 행렬과 같은 차원의 영행렬을 만들어둡니다.

    rank = len(singular_values)  # rank 계산
    sigma[:rank, :rank] = np.diag(singular_values)  # rank까지만 복원
    return U, sigma, V.T

 

- SVD 수행

U, Sigma, V = full_svd(M)

 

- 분해 결과 행렬을 통해 원본 행렬이 복원되는지 확인

# 파이썬의 @ 연산자를 이용하면 np.dot 함수와 같은 행렬곱이나 내적을 편리하게 호출할 수 있습니다.
restored = U @ Sigma @ V.T # T는 전치행렬(transpose)

# 행렬 시각화
plot_matrix(restored)
print("Maximum diff: ", np.abs(M - restored).max())

 

  • SVD 행렬분해 결과의 특성
    - 행렬 U, Σ, V는 각각 다음과같은 shape을 가지고 있어, 순서대로 곱했을 때 원래의 행렬과 같은 크기
# U, Sigma, V의 shape 확인
print(U.shape, Sigma.shape, V.shape)

 

- 정규직교행렬(orthonormal matrix) U, V
   - 행렬 U와 V는 모두 선형대수학에서 정규직교행렬(orthonormal matrix)이라고 부르는 행렬
   - 행렬은 시각화 결과에서 보이듯이 육안으로 눈에 띄는 특성이 있는것은 아니지만, 각 행을 별개의 벡터로 볼때 모두가 서로 직교하고, 길이(norm)가 1인 행렬로 정의
   - 자기자신의 transpose와 곱했을 때 단위행렬이 나온다는, 즉 단순히 transpose만 해주면 역행렬이 된다는 편리한 특성을 가짐

# 행렬 U를 시각화
plot_matrix(U)

# 행렬의 서로 다른 row를 골라 내적하면 0이 나옵니다.
print(U[1] @ U[3])

# 자기 자신과의 내적(벡터의 norm, 길이라고 생각할수 있는 개념)은 항상 1이 나옵니다.
print(U[1] @ U[1])

 

# U @ U.T 행렬 시각화
plot_matrix(U @ U.T)

 

# Sigma 행렬 시각화
plot_matrix(Sigma)

Σ는 singular value를 대각성분으로 하는 대각행렬
-> SVD의 결과에서 이 대각선 성분은 보시다시피 뒤로 갈수록 값이 작아지도록 정렬
singular value는 행렬의 eigen value와 관련된 개념

# 행렬의 (k,k)번째 원소만 남기고 나머지를 0으로 만드는 함수 정의
def select_diag(sigma, k):
    result = np.zeros_like(sigma)  # 영행렬 만들어두기
    result[k, k] = sigma[k, k]  # sigma 행렬의 (k,k) 원소만 남김
    return result

# Sigma의 (k,k)번째 원소만 남기고 나머지를 0으로 만들기
sigma_k = select_diag(Sigma, 3)
# 결과 행렬 시각화
plot_matrix(sigma_k)

 

- k번째의 값만 남기고 나머지를 전부 0으로 만든 Σk라는 행렬

# 원래 행렬의 rank
r = np.linalg.matrix_rank(M)

# 결과 행렬을 미리 initialize
result = np.zeros_like(M)
for k in range(r):
    # Sigma_k를 계산 후 결과행렬에 더해줌.
    sigma_k = select_diag(Sigma, k)
    result += U @ sigma_k @ V.T

# 결과 행렬과 원래 행렬의 차이를 계산
print("Maximum diff: ", np.abs(M - result).max())
# 결과 행렬 시각화
plot_matrix(result)

 

- Σk는 결국 (k, k) 원소만 0이 아닌 행렬

# k번째 열벡터를 가져오는 함수를 정의
def col_vec(matrix, k):
    return matrix[:, [k]]

# 원래 행렬의 rank
r = np.linalg.matrix_rank(M)

# 결과 행렬을 미리 initialize
result = np.zeros_like(M)
for k in range(r):
    # k번째 singular value, 스칼라값
    sig_k = Sigma[k, k]
    # U, V에서 한개의 column vector만을 가져와 사용해도 위 셀과 동일한 결과
    result += sig_k * col_vec(U, k) @ col_vec(V, k).T

# 결과 행렬과 원래 행렬의 차이를 계산
print("Maximum diff: ", np.abs(M - result).max())
# 결과 행렬 시각화
plot_matrix(result)

 

-  정보량은 곱해지는 벡터 두개와 동일하다는 특성을 가지며, 행렬을 만드는 기본단위처럼 사용되는 개념

# 임의의 k를 선택합니다.
k = 2
# 곱해지는 U, V의 열벡터를 표시합니다.
print(col_vec(U, k).shape)
print(col_vec(V, k).T.shape)

# 두 벡터의 행렬곱으로 rank-1 matrix를 만듭니다.
rank1_mat = col_vec(U, k) @ col_vec(V, k).T

print(rank1_mat.shape)

# 이름처럼 rank는 1로 나옵니다.
print("Rank of resulting Rank-1 Matrix: ", np.linalg.matrix_rank(rank1_mat))

# matrix의 값들을 살펴보면, 첫번째 row에 스칼라곱을 해서 나머지 모든
# row를 만들수 있고(linearly dependence), 이는 column도 마찬가지입니다.
plot_matrix(rank1_mat)

 

  • Truncated SVD를 통한 행렬의 rank 축소
    - rank가 r인(r개의 singular value를 가지는) 행렬 M이 있을 때, SVD를 활용하면 이를 r개의 rank1 matrix들의 합으로 표현
    - r개의 성분들 중 크기가 작은 것들을 버리고 행렬 M을 낮은 rank의 행렬로 나타내는 차원축소 방법론
def reduce_dim(M, n_components=None):
    # 주어진 행렬 M을 SVD합니다.
    U, Sigma, V = full_svd(M)

    r = np.linalg.matrix_rank(M)
    if n_components is None:
        # 몇개의 성분을 남길지 주어지지 않으면 아무것도 버리지 않고 전체 성분을 남깁니다.
        n_components = r

    # 남길 component 수가 전체 랭크보다 크면 에러 메시지 보여줍니다.
    assert n_components <= r, \
        f"남길 component의 개수({n_components})는 전체 랭크{r}보다 클 수 없습니다."

    # 결과 행렬을 미리 initialize
    result = np.zeros_like(M, dtype=np.float64)
    # 이번에는 r개가 아니라, 첫 n_components개까지만 rank-1 matrix들을 더해줍니다.
    for k in range(n_components):
        # k번째 singular value, 스칼라값과
        sig_k = Sigma[k, k]
        # rank1 행렬에 위 sig_k 스칼라값을 곱해 결과에 더함.
        result += sig_k * col_vec(U, k) @ col_vec(V, k).T
    return result

 

- 첫 k개의 성분만 남긴 행렬과 원래 행렬을 비교

# 남길 성분의 수. 이 값을 0~6까지 직접 값을 조절해볼수 있습니다.
n_components = 5
size_scale = 0.6 # 비교해보기 편하도록 figure크기를 살짝 줄입니다.

# 원래 행렬의 rank를 구해봅니다.
print("Original rank: ", np.linalg.matrix_rank(M))

# TruncatedSVD를 수행해 n_component개의 성분만 남깁니다.
result = reduce_dim(M, n_components)

# TruncatedSVD 결과 행렬의 rank와 원본 행렬과의 값 차이를 프린트합니다.
print("Result rank: ", np.linalg.matrix_rank(result))
print("Maximum diff: ", np.abs(M - result).max())
# 원래의 행렬
plot_matrix(M, size_scale=size_scale)
plt.title('original matrix M')  # 구분을 위해 시각화 제목 넣기
plt.show()                      # 그림 보여주기

# truncated svd 결과 행렬
plot_matrix(result, size_scale=size_scale)
plt.title('truncated SVD result')  # 구분을 위해 시각화 제목 넣기
plt.show()               # 그림 보여주기

# 원래 행렬과의 차이(absolute diff)
plot_matrix(np.abs(M - result), size_scale=size_scale)
plt.title('absolute diff: |M-trSVD|')  # 구분을 위해 시각화 제목 넣기
plt.show()                  # 그림 보여주기

  • PCA 구현 실습: EigenFace
# 데이터 불러오기
faces, _ = datasets.fetch_olivetti_faces(return_X_y=True, shuffle=True, random_state=1234)

n_samples, n_features = faces.shape
print('데이터 수:', n_samples) # 데이터 수 확인
print('차원 수:', n_features) # 차원수 확인

 

# 첫번째 데이터 확인하기
faces[0, :]

- 일반적인 0~255값(uint8타입)의 이미지들이 아니라 400x4096의 커다란 float타입 행렬로 불러옴

- 이 데이터셋이 이미지를 미리 가공된 상태로 제공하기 때문

# 데이터 범위 확인하기
print('평균:', faces.mean())
print('최댓값:', faces.max())
print('최솟값:', faces.min())
# 가져올 샘플의 번호를 랜덤으로 고름
index = np.random.choice(len(faces))
print(f"{index}-th row of matrix")

# 원본 이미지의 크기
img_h, img_w = (64, 64)

# 데이터(faces)에서 샘플(row)을 선택해 가져오기
face_vector = faces[index]

# 이미지를 원래의 크기로 변환한 후 display
face_image = face_vector.reshape(img_h, img_w)
plt.imshow(face_image, cmap="gray")

- 0~1 범위의 float 숫자도 이미지로 쉽게 변환해볼 수 있는 기능을 지원

- 값의 범위를 0~255 범위의 픽셀값(int)으로 변환하는 작업은 하지 않아도 괜찮

- PCA분석을 위해서는 다음 각 데이터의 feature-wise, sample-wise로 평균이 모두 0이 되어야하므로 이에 맞춰 다음과 같이 전처리를 수행해준 이후에는 별도의 scaling 과정을 거쳐야 제대로 이미지를 확인

# 전체 샘플단위의 평균을 구하고, 이를 원본 데이터에서 빼서 평균을 0으로 맞춰줌
samplewise_mean = faces.mean(axis=0) # (4096, )
faces_centered = faces - samplewise_mean

# 각 이미지마다 모든 픽셀값의 평균을 구하고, 이를 원본 이미지에서 빼는 방식으로 평균을 0으로 맞춰줌
pixelwise_mean = faces_centered.mean(axis=1).reshape(n_samples, -1) # (400, )
faces_centered -= pixelwise_mean

 

전처리 진행

# 시각화를 위한 함수 정의하기
def plot_faces(title, images, n_cols=3, n_rows=2, shuffle=False, cmap="gray", size_scale=2.0, random_seed=0, image_shape=(64, 64)):
    # plot할 이미지(벡터)들을 랜덤으로 선택
    if shuffle:
        np.random.seed(random_seed)
        indices = np.random.choice(len(images), n_cols * n_rows)
    else:
        indices = np.arange(n_cols * n_rows)

    # figure관련 설정
    fig, axs = plt.subplots(
        nrows=n_rows,
        ncols=n_cols,
        figsize=(n_cols * size_scale, n_rows * size_scale),
        facecolor="white",
        constrained_layout=True,
    )
    fig.set_constrained_layout_pads(w_pad=0.01, h_pad=0.02, hspace=0, wspace=0)
    fig.set_edgecolor("black")
    fig.suptitle(title, size=16)

    # 각 자리에 들어가는 얼굴 이미지를 plot
    for ax, idx in zip(axs.flat, indices):
        face_vec = images[idx]
        vmax = max(face_vec.max(), - face_vec.min())
        im = ax.imshow(
            face_vec.reshape(image_shape),
            cmap=cmap,
            interpolation="nearest",
            vmin=-vmax,
            vmax=vmax,
        )
        ax.axis("off")
    fig.colorbar(im, ax=axs, orientation="horizontal", shrink=0.99, aspect=40, pad=0.01)
    plt.show()

 

이미지 시각화

# 이미지 6개 시각화하기
plot_faces("Faces from dataset", faces_centered, shuffle=True, random_seed=1234)

 

줄일 차원의 수 지정하기

# 줄일 차원의 수 지정하기
n_components = 20

# PCA 수행하기
pca_estimator = PCA(n_components=n_components, svd_solver="full", whiten=True)
pca_estimator.fit(faces_centered)

# PCA 결과 (Eigenface) 시각화
plot_faces("Components of PCA", pca_estimator.components_, n_rows=2, n_cols=4)

 

# 원본 이미지중에 임의로 하나를 고릅니다.
index = 123
indices = np.random.choice(n_samples, 6)
# 원본 이미지를 보여줍니다.
plt.title(f"Original Face Image at index: {index}")
plt.imshow(faces[index].reshape(64, 64), cmap="gray")

-> 4096(64*64)차원에서, PCA estimator를 통해 차원축소되어 다음과 같은 n_components 차원 벡터로 표현

# 차원축소된 벡터 계산하기
reduced_vec = pca_estimator.transform(faces_centered[index].reshape(1, -1))
print(reduced_vec)
print('차원 축소된 벡터의 크기:', reduced_vec.shape)

 

- fitting 된 PCA의 component들을, 위 reduced_vec의 원소를 계수로 하는 선형결합하면, 다음과 같이 원본 이미지를 복원

# 결과 행렬 미리 initialize
canvas = np.zeros([64, 64], dtype=np.float64)
for value, comp in zip(reduced_vec[0], pca_estimator.components_):
    # 각 component 벡터를 이미지 크기로 resize한 뒤, 이를 차원축소된 벡터의 각 값과 선형결합
    canvas += comp.reshape(64, 64) * value

vmax = max(canvas.max(), - canvas.min())

plt.imshow(canvas, cmap="gray", vmax=vmax, vmin=-vmax)

- 축소된 차원의 수, n_components가 너무 작아 복원된 이미지가 원본과 같은 이미지라는 것은 겨우 알아볼 수 있을 정도지만, 원본 이미지의 디테일한 부분들은 상당히 소실된 상태
- n_components 값에 따라 차원축소된 이미지가 어떤 형태로 나타나는지 아래에서 비교

# 원본 이미지와 차원축소된 이미지들 비교하기
def compare_reduced_faces(title, images, index=123, n_components_list=[5, 20, 100], n_cols=4, n_rows=1, shuffle=False, cmap="gray", size_scale=2.5, random_seed=0, image_shape=(64, 64)):
    # 그림 관련 설정
    fig, axs = plt.subplots(
        nrows=n_rows, ncols=n_cols,
        figsize=(n_cols * size_scale, n_rows * size_scale),
        facecolor="white",
        constrained_layout=True,
    )
    fig.set_constrained_layout_pads(w_pad=0.01, h_pad=0.02, hspace=0, wspace=0)
    fig.set_edgecolor("black")

    # 보여줄 이미지 선정
    face_vec = faces[index]

    # 첫 이미지로 원본 이미지를 보여줍니다.
    axs[0].set_title("Original Face Image", y=-0.2)
    axs[0].imshow(face_vec.reshape(image_shape), cmap="gray")
    axs[0].axis("off")

    # 다음 이미지부터는 PCA를 이용해 차원축소된 이미지를 보여줍니다.
    # 각 차원마다 보여주므로 줄일 차원의 수 리스트 중 하나씩 지정하여 PCA를 수행합니다.
    for img_index, n_components in enumerate(n_components_list):

        # PCA 수행하기
        pca_estimator = PCA(n_components=n_components, svd_solver="full", whiten=True)
        pca_estimator.fit(images)

        # 차원축소된 벡터 계산하기
        reduced_vec = pca_estimator.transform(face_vec.reshape(1, -1))
        # 결과 행렬 미리 initialize
        canvas = np.zeros([64, 64], dtype=np.float64)
        for value, comp in zip(reduced_vec[0], pca_estimator.components_):
            # 각 component 벡터를 이미지 크기로 resize한 뒤, 이를 차원축소된 벡터의 각 값과 선형결합
            canvas += comp.reshape(64, 64) * value

        # PCA 결과 (Eigenface) 시각화
        vmax = max(canvas.max(), - canvas.min())
        im = axs[img_index+1].imshow(
            canvas.reshape(image_shape),
            cmap=cmap,
            interpolation="nearest",
            vmin=-vmax,
            vmax=vmax,
        )
        axs[img_index+1].axis("off")
        axs[img_index+1].set_title(f'Dimension={n_components}', y=-0.2)

    # 최종 이미지 보여주기
    plt.suptitle(title + f': images at index {index}', fontsize=20)
    plt.show()

 

compare_reduced_faces('Comparisons of different dimensions', faces_centered, n_components_list=[5, 20, 100])

- n_components가 5일 때, 즉 5차원으로 축소시키면 사람이라는 이미지는 인식할 수 있으나 원본 이미지에 나타난 사람과는 다소 차이가 있고 안경도 인식할 수 없음
- n_components가 100일 때는 원본 이미지의 여러 디테일한 정보들까지 함께 복원
- 좀더 세세한 정보들까지 유지하면서 차원축소를 진행할 수 있지만, 대신 그만큼 차원 수가 덜 줄어드는 것이므로 어느정도 적당한 선에서 결정 해야함

- 어느정도의 n_components의 값이 적절한지는 데이터가 가지고있는 정보량, 즉 얼마나 예측 가능한 정보인가에 따라 달라짐

 

# singular value 확인하기
pca_estimator.singular_values_
# 기존 faces데이터셋과 같은 크기의 랜덤 정규분포 데이터를 생성
random_noises = np.random.randn(*faces_centered.shape)

# 이 랜덤 포인트에 대해 PCA 수행하기
pca_estimator = PCA(n_components=n_components, svd_solver="full", whiten=True)
pca_estimator.fit(random_noises)
# singular value 확인하기
pca_estimator.singular_values_

 

-  랜덤하게 생성된 데이터 행렬의 signular value의 감소폭보다 얼굴 데이터의 감소폭

# 얼굴 데이터에 대해 최대 n_components로 PCA 수행하기
pca_faces = PCA(n_components=400, svd_solver="full", whiten=True)
pca_faces.fit(faces_centered)
sv_faces = pca_faces.singular_values_

# 랜덤 데이터에 대해 최대 n_components로 PCA 수행하기
pca_random = PCA(n_components=400, svd_solver="full", whiten=True)
pca_random.fit(random_noises)
sv_random = pca_random.singular_values_


# 각각의 singular value들을 plot해 비교.
plt.subplot(121)
plt.title("Singular Values(Random)")
plt.xlim(0, 399)
plt.ylim(0, 100)
plt.plot(sv_random)

plt.subplot(122)
plt.title("Singular Values(Faces)")
plt.xlim(0, 399)
plt.ylim(0, 100)
plt.plot(sv_faces)

- PCA가 수행하는 작업이 결국 샘플, 피쳐 간의 상호 연관성, 즉, 같은 이미지의 다른 부분이나 데이터셋 내의 다른 이미지를 가지고 얼마나 쉽게 이미지의 특정 부분을 추측할 수 있느냐에 따라 달라지게 된다는 것을 의미

- 많은 유사성을 갖고 있기에 첫 몇 개의 components의 영향을 크게 받는 것이고 그래서 차원축소를 많이 해서 성분의 수를 많이 줄이더라도 원본 이미지가 알아볼 수 있게 복원

- 랜덤으로 생성한 행렬의 경우는 데이터셋의 한 영역으로부터 다른 영역을 복원하는 것이 거의 불가능에 가깝기 때문에, 차원을 줄였을 때 손실되는 정보의 비율이 매우 크고, 차원축소가 거의 동작하지 않는다고 보아도 무방

- 현실의 데이터에 PCA를 수행할때는 이런 데이터의 패턴과 예측 가능성을 보고 몇 개 정도의 components를 사용할지 결정

 

  • t-SNE를 이용한 데이터셋 시각화
    - 고차원의 데이터를 유의미하게 축소시킨 차원 축소를 이용하여 우리 눈에 이해하기 쉬운 형태로 전달
    - 시각화를 위해 t-SNE (t-distributed Stochastic Neighbor Embedding)을 이용

    - 고차원 공간에서 비슷한 데이터는 저차원 공간에서 가깝고, 다른 데이터는 서로 멀리 떨어져있도록 학습
    - 서로 유사하고 다른 각 데이터를 '구분'하고자 할 때 많이 사용
    - 데이터를 새로운 다른 낮은 차원으로 변환하면 임베딩(embedding)했다고 하는데, 이 임베딩된(embedded) 공간에서 데이터 간 유사성은 Student t-분포로 표현
    - PCA가 선형부분공간에 투영시키는 방법인데 반해 t-SNE는 비선형적인 부분공간을 찾으므로 복잡한 데이터를 시각화할 때 유용하게 사용
# 데이터 불러오기
digits = datasets.load_digits(n_class=6)
X, y = digits.data, digits.target
n_samples, n_features = X.shape
print('데이터 수:', n_samples) # 데이터 수 확인
print('차원 수:', n_features) # 차원수 확인
# 첫번째 데이터 확인하기
X[[0]]
# 숫자 이미지 시각화
fig, axs = plt.subplots(nrows=10, ncols=10, figsize=(5, 5))
for idx, ax in enumerate(axs.ravel()):
    ax.imshow(X[idx].reshape((8, 8)), cmap=plt.cm.binary)
    ax.axis("off")
_ = fig.suptitle("A selection from the 64-dimensional digits dataset", fontsize=16)

 

- plot을 도와주는 함수 정의

# plot helper 함수 정의
def plot_embedding(X, title):
    _, ax = plt.subplots()
    # 정규화
    X = MinMaxScaler().fit_transform(X)
    # 색깔로 숫자로 scatter 표시
    for digit in digits.target_names:
        ax.scatter(
            *X[y == digit].T,
            marker=f"${digit}$",
            s=60,
            color=plt.cm.Dark2(digit),
            alpha=0.425,
            zorder=2,
        )
    # 이미지 그림 표시
    shown_images = np.array([[1.0, 1.0]])
    for i in range(X.shape[0]):
        # 모든 숫자 임베딩을 scatter하고, 숫자 그룹에 annotation box를 보기
        dist = np.sum((X[i] - shown_images) ** 2, 1)
        # 보기 쉽게 하기 위해 너무 가까운 데이터는 보여주지 않기
        if np.min(dist) < 4e-3:
            continue
        # 이미지 합치기
        shown_images = np.concatenate([shown_images, [X[i]]], axis=0)
        imagebox = offsetbox.AnnotationBbox(
            offsetbox.OffsetImage(digits.images[i], cmap=plt.cm.gray_r), X[i]
        )
        imagebox.set(zorder=1)
        ax.add_artist(imagebox)

    ax.set_title(title)
    ax.axis("off")
# t-SNE 적용
transformer = TSNE(n_components=2, random_state=0)
projection = transformer.fit_transform(X, y)

# t-SNE 결과 시각화
plot_embedding(projection, 't-SNE embedding')
plt.show()
# Truncated SVD 적용
transformer = TruncatedSVD(n_components=2)
projection = transformer.fit_transform(X, y)

# TruncatedSVD 결과 시각화
plot_embedding(projection, 'TruncatedSVD embedding')
plt.show()

 

 

데이터의 군집화 방법론

- 데이터로부터 패턴을 파악해 데이터를 여러 개의 집단(cluster)으로 나눔

- k-means clustering 알고리즘

  • 데이터셋을 불러와 전처리 적용
# 데이터 불러오기
iris = datasets.load_iris()

# 인풋, 아웃풋을 X, y변수로 할당
X = iris.data
y = iris.target

# 대략적인 샘플 수, 피쳐 수를 확인
print(X.shape)
print(y.shape)
# 분석에 사용할 첫 두개 피쳐만을 선택
X = X[:, :2]

- k-means clustering은 분석 과정에서 거리의 개념을 사용

- 변수는 0~1 범위에서 소수점 자리에서만 변동이 있는 변수 : 다른 변수는 수백, 수천 단위로 매우 큰 변동폭을 보인다면 두 변수를 함께 활용

# min-max scaling을 이용해 정규화하기
scaler = MinMaxScaler()
X = scaler.fit_transform(X)
  •  k-means 군집화의 단계별 이해
    - k-means clustering : 군집화 방법 중 하나
       - k개의 군집을 나눌 때 평균(mean)을 이용하는 방법
       - 각각의 데이터 포인트로부터 가장 가까운 군집의 중심까지 거리의 제곱합의 가중합이 최소가 되도록 하는 것
       - 군집의 중심을 잘 찾고 각 데이터 포인트들을 각 군집에 할당

    - 먼저 rik에 대해서 J를 최소화
    -  rik 값을 고정하고 μk에 대해서 J를 최소화
    - 반복하여 형성된 군집이 더이상 바뀌지 않을 때까지 수행하여 최적의 군집을 형성

- 군집 수 k 설정

# 재현성을 위해 랜덤시드를 고정
np.random.seed(1234)

# 군집화를 수행할 군집 수
n_cluster = 3

# 기존 데이터 중 랜덤으로 선택하여 초기 군집의 중심을 설정
init_idx = np.random.randint(X.shape[0], size=n_cluster)
init_data = X[init_idx, :]

 

- 기존 데이터중 k개를 랜덤으로 골라 초기 군집의 중심 설정

# 정규화된 데이터 시각화
plt.figure(figsize=(5, 4))
plt.scatter(X[:, 0], X[:, 1], c='C0', label='normalized data', edgecolor="k")

# 초기 군집 중심을 별표로 시각화
plt.scatter(init_data[:, 0], init_data[:, 1], edgecolor="k", c=['C1','C2','C3'], label='initial data', marker='*', s=100)
plt.legend()
plt.show()

 

- 군집 중심을 활용해 초기 데이터를 할당

# 군집화 초기화: 0th iteration
iter = 0

# 군집의 중심을 위에서 설정한 초기 데이터로 정의 (mu_k)
centroid = init_data

# step 1: 각 데이터 포인트에서 각 군집 중심까지의 거리를 계산 후, 가장 가까운 군집중심을 가진 군집으로 할당 (r_ik)
diff = X.reshape(-1, 1, 2) - centroid
distances = np.linalg.norm(diff, 2, axis=-1)
clusters = np.argmin(distances, axis=-1)
# 군집화 결과 시각화 도움 함수
def plot_cluster(X, clusters, centroid, iter=0):
    plt.figure(figsize=(5, 4))
    # 군집화된 데이터
    plt.scatter(X[:, 0], X[:, 1], c=clusters, cmap=plt.cm.Set1, edgecolor="k", label='data')
    # 군집의 중심 시각화
    plt.scatter(centroid[:, 0], centroid[:, 1], marker='*', s=100, edgecolor="k", c=['C1','C2','C3'], label='centroid')
    plt.title(f'Clustering results (iter={iter})')
    plt.legend()
    plt.show()
    
    
# 군집화 결과 시각화
plot_cluster(X, clusters, centroid, iter)

 

- 군집화 과정의 반복

iter += 1  # 다음 iteration

# step2: 군집의 중심을 각 군집에 할당된 데이터들의 평균으로 정의 (mu_k)
clusters = np.array(clusters)
centroid = np.array([
    X[clusters == 0, :].mean(axis=0),
    X[clusters == 1, :].mean(axis=0),
    X[clusters == 2, :].mean(axis=0),
])

# step 1: 각 데이터 포인트에서 각 군집 중심까지의 거리를 계산 후, 가장 가까운 군집중심을 가진 군집으로 할당 (r_ik)
diff = X.reshape(-1, 1, 2) - centroid
distances = np.linalg.norm(diff, 2, axis=-1)
clusters = np.argmin(distances, axis=-1)

# 결과를 시각화
plot_cluster(X, clusters, centroid, iter)
# step3: step2 반복: iteration 3~11
fig = plt.figure(figsize=(15, 10))
for iter in range(6):
    # 군집의 중심을 각 군집에 할당된 데이터들의 평균으로 정의 (mu_k)
    clusters = np.array(clusters)
    centroid = np.array([
        X[clusters == 0, :].mean(axis=0),
        X[clusters == 1, :].mean(axis=0),
        X[clusters == 2, :].mean(axis=0),
    ])

    # step 1 반복: 각 데이터 포인트와 군집의 중심과의 거리를 계산하여 가장 가까운 군집으로 할당 (r_ik)
    diff = X.reshape(-1, 1, 2) - centroid
    distances = np.linalg.norm(diff, 2, axis=-1)
    clusters = np.argmin(distances, axis=-1)

    # 결과 시각화
    ax = fig.add_subplot(2, 3, iter + 1)
    # 군집화된 데이터 시각화
    ax.scatter(X[:, 0], X[:, 1], c=clusters, cmap=plt.cm.Set1, edgecolor="k")
    # 군집의 중심 시각화
    plt.scatter(centroid[:, 0], centroid[:, 1], marker='*', s=100, edgecolor="k", c=['C1','C2','C3'], label='centroid')

    ax.set_title(f'Clustering results (iter={iter + 3})')

plt.show()

 

def k_means_clustering(data, n_cluster, num_iter):
    '''
    k-means clustering 구현 및 결과값 저장
    '''
    result = {}
    # 반복횟수만큼 반복
    for iter in range(0, num_iter + 1):
        # 군집의 중심 정의
        if iter == 0:
            # 초기화: 첫번째 iteration에서는 군집의 중심을 위에서 설정한 초기 데이터로 정의 (mu_k)
            np.random.seed(0)
            init_idx = np.random.randint(data.shape[0], size=n_cluster)
            centroid = data[init_idx, :]
        else:
            # step2: 군집의 중심을 각 군집에 할당된 데이터들의 평균으로 정의 (mu_k)
            clusters = np.array(clusters)
            centroid = np.array([
                data[clusters == 0, :].mean(axis=0),
                data[clusters == 1, :].mean(axis=0),
                data[clusters == 2, :].mean(axis=0),
            ])

        # step 1: 각 데이터 포인트와 군집의 중심과의 거리를 계산하여 가장 가까운 군집으로 할당 (r_ik)
        diff = X.reshape(-1, 1, 2) - centroid
        distances = np.linalg.norm(diff, 2, axis=-1)
        clusters = np.argmin(distances, axis=-1)

        # clustering 결과를 dictionary에 iteration index 별로 저장
        result[iter] = {
            'clusters': clusters,
            'centroid': centroid,
        }
    return result

 

# iris 데이터 X에 대해 k-means clustering 수행
result = k_means_clustering(X, n_cluster=3, num_iter=10)

 

# plotly 버전
def plotly_results(data, result):

    # 데이터 전처리
    df = pd.DataFrame()
    for idx, res in result.items():

        # 각 데이터의 군집 인덱스
        cluster = pd.DataFrame(data, columns=['X','Y'])
        cluster['cluster_idx'] = res['clusters']
        cluster['iter'] = idx
        cluster['label'] = 'data'
        cluster['label_size'] = 0.1
        df = pd.concat([df, cluster], axis=0)

        # 군집 중심 데이터
        centroid = pd.DataFrame(res['centroid'], columns=['X','Y'])
        centroid.reset_index(inplace=True)
        centroid.columns = ['cluster_idx']+list(centroid.columns[1:])
        centroid['iter'] = idx
        centroid['label'] = 'cluster_centroid'
        centroid['label_size'] = 1
        df = pd.concat([df, centroid], axis=0)

    # 애니메이션 시각화
    fig = px.scatter(df, x="X", y="Y", animation_frame="iter", color="cluster_idx", size='label_size', symbol='label', width=1000, height=800, symbol_sequence= ['circle', 'star'])
    fig.update_coloraxes(showscale=False)
    fig.show()

plotly_results(X, result)

 

  • 사이킷런을 활용한 k-means 군집화
     k-means clustering을 sklearn 라이브러리를 이용해 수행
# k-means clustering 수행하기
kmeans_clustering = KMeans(n_clusters=n_cluster, n_init="auto")
kmeans_clustering.fit(X)

# k-means clustering 에 의해 정의된 군집(cluster)과 군집의 중심(centroid)
clusters = kmeans_clustering.labels_
centroid = kmeans_clustering.cluster_centers_

 

# 시각화
fig = plt.figure(figsize=(10, 4))

# ground truth 시각화
ax = fig.add_subplot(1, 2, 1)
for name, label in [("Setosa", 0), ("Versicolour", 1), ("Virginica", 2)]:
    ax.text(
        X[y == label, 0].mean(),
        X[y == label, 1].mean() + 0.15,
        name,
        horizontalalignment="center",
        bbox=dict(alpha=0.2, edgecolor="w", facecolor="w"),
    )
# Reorder the labels to have colors matching the cluster results
y = np.choose(y, [2, 0, 1]).astype(float)
ax.scatter(X[:, 0], X[:, 1], c=y, edgecolor="k", cmap=plt.cm.Set1)

ax.set_xlabel("Sepal length")
ax.set_ylabel("Sepal width")
ax.set_title("Ground Truth")

# clustering 결과 시각화
ax = fig.add_subplot(1, 2, 2)
# 군집화된 데이터 시각화
ax.scatter(X[:, 0], X[:, 1], c=np.array(clusters).astype(float), edgecolor="k")
# 군집의 중심 시각화
plt.scatter(centroid[:, 0], centroid[:, 1], marker='*', s=200, edgecolor="k", c=['C1','C2','C3'], label='centroid')
ax.set_xlabel("Sepal length")
ax.set_ylabel("Sepal width")
ax.set_title("Clustering result")

plt.subplots_adjust(wspace=0.25, hspace=0.25)
plt.show()

 

  • 랜덤 합성데이터의 군집화
# 재현성을 위한 랜덤시드 설정
np.random.seed(123)

# 실제 데이터의 군집 수와 k-means에서 지정할 군집의 수
n_centers = 5
n_clusters = 3

# 데이터 생성
X, Y = datasets.make_blobs(n_features=2, centers=n_centers)
# 인풋 데이터와 라벨 시각화
plt.figure(figsize=(10, 4))
plt.subplot(121)
plt.title("Three blobs", fontsize="small")
plt.scatter(X[:, 0], X[:, 1], marker="o", c=Y, s=25, edgecolor="k")

# k-means 군집화 수행
kmeans_clustering = KMeans(n_clusters=n_clusters, n_init="auto")
kmeans_clustering.fit(X)
clusters = kmeans_clustering.labels_

# 군집화 결과 시각화
plt.subplot(122)
plt.title('Clustering result', fontsize='small')
plt.scatter(X[:, 0], X[:, 1], marker="o", c=clusters, s=25, edgecolor="k")

plt.show()

 

- 군집 수, k의 설정

# 군집수(n_cluster)를 변화시켜가며 k-means clustering 수행
results = []
for n in range(2, 10):
    kmeans_clustering = KMeans(n_clusters=n, n_init="auto")
    kmeans_clustering.fit(X)
    results.append(kmeans_clustering.inertia_)  # inertia_: Sum of squared distances of samples to their closest cluster center, weighted by the sample weights if provided.
# 군집수(n_cluster)를 변화시켜가며 k-means clustering 을 수행한 결과의 거리 시각화
plt.plot([*range(2, 10)], results, '-o')
plt.xlabel('number of clusters')
plt.ylabel('Within Cluster Sum of Square')
plt.title('Elbow method')
plt.show()