[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란 행렬 M∈Rm×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()
'Study > 머신러닝' 카테고리의 다른 글
머신러닝 BASIC _ 모델 평가와 개선 (3) | 2024.11.04 |
---|---|
머신러닝 BASIC _ 다양한 데이터 처리 (13) | 2024.11.01 |
머신러닝 BASIC _ 지도학습 방법론 (4) | 2024.10.30 |
머신러닝 BASIC _ 모델과 데이터 (2) | 2024.10.30 |
머신러닝 BASIC _ 머신러닝이란? (8) | 2024.10.29 |