Linear Algebra/개발자를 위한 실전 선형대수학

[파이썬과 선형대수] 주성분 분석(PCA)의 정의와 의미, 직접 구현하기

WooJi 2024. 12. 22. 11:32
반응형

본 글을 이해하기 위해서는 고윳값 분해와 특이값 분해에 대한 지식이 필요합니다.

이전 글을 읽고 이 글을 읽으시기 바랍니다.

https://aaaaaaaaaaayowooji.tistory.com/54

 

[파이썬과 선형대수]고유값과 고유벡터, eig 안쓰고 직접 구현해보기

고유값과 고유벡터정의기하학적으로 볼 때, 어떤 벡터 $v_{2}$에 행렬 $A$를 곱하면 벡터의 크기와 방향이 변하게 된다. 그런데 어떤 벡터($v_{1})$를 곱했더니 방향이 바뀌지 않고 크기만 변하는 벡

aaaaaaaaaaayowooji.tistory.com

 

https://aaaaaaaaaaayowooji.tistory.com/55

 

[파이썬과 선형대수]특잇값 분해의 정의, svd 안쓰고 직접 구현해보기

특잇값 분해(Singular Value Decomposition)정의$$A=U \Sigma V^T$$$M \times N$ 행렬을 $U,\Sigma, V^T$ 의 세 개의 행렬로 분해하는 방법이다.$AV=U\Sigma$에서 $V$가 정규직교행렬이기 때문에 넘겨서 위 식과 같은 꼴로

aaaaaaaaaaayowooji.tistory.com

 

주성분 분석(Principal Component Analysis)

정의

고차원의 데이터를 저차원의 데이터로 환원시키는 기법

 

위키백과에서의 정의는 위와 같다.
고차원의 데이터를 어떻게 저차원으로 차원을 축소한다는 것일까.

조금 복잡한 코드이지만 예시 자체는 간단하다. 한번 확인해보자.

코드

import numpy as np
import matplotlib.pyplot as plt

# 데이터 생성
np.random.seed(0)
mean = [5, 5]
cov = [[3, 2], [2, 3]]  # 두 축 사이에 상관성이 있음
data = np.random.multivariate_normal(mean, cov, 500)

# 데이터 중심 계산
center = np.mean(data, axis=0)
mean_centered = data - center
cov_matrix = np.cov(mean_centered, rowvar=False)

# PCA 계산
eigvals, eigvecs = np.linalg.eigh(cov_matrix)
sorted_indices = np.argsort(eigvals)[::-1]
eigvals = eigvals[sorted_indices]
eigvecs = eigvecs[:, sorted_indices]

# 방향 리스트
direction_list = [eigvecs[:, 0], eigvecs[:, 1], np.array([1, 0])]
color_list = ["blue", "red", "green"]

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

# 원본 데이터
plt.scatter(data[:, 0], data[:, 1], alpha=0.5, label="Original Data")


for i, direction in enumerate(direction_list):
    # 방향 벡터 정규화
    direction = direction / np.linalg.norm(direction)

    # 정사영
    projections = np.array([
        center + ((point - center) @ direction) * direction
        for point in data
    ])

    # 정사영 후 분산 계산
    proj_centered = projections - np.mean(projections, axis=0)
    var = np.var(proj_centered @ direction)  # 방향성에 따른 분산 계산

    # 직선 그리기
    slope = direction[1] / direction[0] 
    x_vals = np.array([0, 10])  # x축 범위
    y_vals = center[1] + slope * (x_vals - center[0])
    plt.plot(x_vals, y_vals, color=color_list[i], linestyle="--", label=f"Direction {i+1}")

    # 정사영된 점 표시
    plt.scatter(projections[:, 0], projections[:, 1], color=color_list[i], alpha=0.6,
                label=f"Projections onto Direction {i+1} (Variance: {np.round(var, 2)})")

plt.xlabel("X-axis")
plt.ylabel("Y-axis")
plt.grid()
plt.legend()
plt.axis("equal")
plt.show()

결과


결과를 살펴보자.
타원처럼 퍼져 있는 2차원 점들을 만들어서 찍어보았다.
이 점들을 1차원으로 차원을 축소하고 싶어서 직선들에 정사영을 해보았다. 어떤 직선에 정사영을 해야 이 데이터들의 분포를 가장 잘 반영한다고 할 수 있을까.

예시로 직선 세 개를 그었다. Direction 1은 나중에 알겠지만 $X^TX$의 고유벡터 중 가장 큰 고윳값에 해당하는 벡터다. Direction 2는 [-1,1]방향의 직선이다. Direction 3는 [1,0]방향의 직선이다.

정사영된 점들이 분포된 정도를 평가하기 위해서 분산에 대해 알아야 한다.
데이터와 평균의 차(편차)를 제곱해서 모두 더한 값이 분산이다.

다시 그림을 보면, 파란색 직선에 정사영된 점들이 가장 길게 퍼져 있다. 그 다음으로 초록색, 빨간색 순이다. 그리고 분산을 계산해본 결과 실제로 파란색 직선에 정사영된 점들의 분산이 4.74로 가장 크다.

차원을 줄이면서도 점들의 분포를 가장 잘 나타낼 수 있는 방향은 Direction 1 방향이라고 할 수 있다.
이처럼 차원을 줄이면서 변수 간의 상관관계를 표현하는 방법Principal Component Analysis, 줄여서 PCA, 주성분 분석 이라고 한다.

다변량 공분산 행렬

https://aaaaaaaaaaayowooji.tistory.com/33

 

[파이썬과 선형대수] 다변량 공분산 행렬에 대해 알아보자!

오늘은 다변량 공분산 행렬에 대해 알아봅시다.공분산2개의 확률변수의 선형 관계를 나타내는 값분산이 하나의 변수에 대한 값이라면 공분산은 2개의 확률변수에 대한 값이다.공분산 값이 양수

aaaaaaaaaaayowooji.tistory.com

 

PCA를 하는 근본적인 이유는 데이터 간의 상관관계를 알아보기 위함이다. 그렇다면 상관관계를 표현하는 지표를 만들어야 계산이 가능할 것이다. 그리고 그 지표는 공분산 행렬이다.

이전 글에서 알아보았듯이 공분산은 두 변수간의 상관관계를 나타내는 지표다. 두 변수뿐 아니라 여러 변수끼리의 상관관계를 나타낸 행렬을 다변량 공분산 행렬이라 하는 것이다.

계산하는 법

  1. 변수가 N개인 데이터 행렬 dataMat $M \times N$가 있다고 해보자 (각 변수에 대한 데이터가 M개)
  2. 각 열을 평균중심화를 해줘야 함.(데이터의 단위가 열인 경우.)
  3. 내적을 해주어야 한다. 내적을 하면 $N \times N$ 행렬이 되었을 것이다.($X^TX$를 진행)
    $$Cov(X)=\frac{1}{N}X^TX=\frac{1}{N} \begin{bmatrix}-x_1-\-x_2-\ \vdots \-x_n-\end{bmatrix} \begin{bmatrix}x_1&x_2&\cdots&x_n \ \ \ \ \end{bmatrix}$$
  4. N-1로 나누어주어야 한다.

PCA의 수학적 이해

고윳값 분해와의 연관성

PCA를 구현하기에 앞서서 이론적으로 먼저 이해해보자.
사이즈가 $M\times N$이고 평균중심화를 해준 데이터 행렬 $X$가 있다고 가정하자.
PCA의 핵심은 해당 방향으로의 분산이 최대가 되도록 하는 방향을 찾는 것이다.

평균이 0이므로 분산 $\lambda=||XW||^2$이라고 쓸 수 있다.($W$는 정규방향벡터들의 행렬)
$$\lambda=(XW)^T(XW)=W^TX^TXW$$
(고유벡터의 방향만 확인하면 되므로 N으로 나누는 과정은 생략.)

$\lambda=W^TX^TXW$ 식은 어디서 많이 본 식이다. 바로 이전에 [[17. 특잇값 분해(Sigular Value Decomposition) (1)|특잇값 분해]]에 대해 공부했을 때, $A=USV^T$에서 $V$를 구하는 과정에서 $A^TA=V\Sigma^2 V^{T}$였고 $V$가 $A^TA$의 고유벡터 행렬임을 알고 있다.

$X^TX$는 대칭행렬이기 때문에 고유벡터는 서로 직교한다. 유사하게 식을 정리해보면 $\lambda$가 $X^TX$의 고윳값이고 $W$가 고유벡터 행렬임을 알 수 있다.

특잇값 분해와의 연관성

위의 내용을 SVD와 연관을 시켜보면, $X$를 $X=USV^T$로 특잇값 분해를 했을 때, $V$가 $X^TX$의 고유벡터 행렬이고, $S^2$이 $X^TX$의 고윳값 행렬이었다.
이를 PCA와 연관시켜보면 PCA를 진행하려는 데이터 행렬 $X$를 특잇값 분해한 후에 $S^2,V$를 이용해도 동일하다는 것이다.

결론

PCA를 하는 방법: 고윳값 분해(EIG) 이용

$M\times N$이고 평균중심화를 해준 데이터 행렬 $X$가 있을 때,
평균 중심화를 해주고 $Cov(X)=\frac{1}{N}X^TX$로 공분산 행렬을 구한다. $Cov(X)$의 고윳값 분해를 통해 고윳값 행렬과 고유벡터 행렬을 구한다.
고윳값 행렬을 내림차순으로 정렬해주어야 한다. 고유벡터 행렬도 고윳값 행렬의 순서에 맞게 정렬해주어야 PCA를 진행할 수 있다.

PCA를 하는 방법: 특잇값 분해(SVD) 이용

혹은 평균 중심화된 데이터 행렬을 특잇값 분해를 진행한다. $V$를 고유벡터 행렬로서 사용하면 된다. $\frac{1}{N}X^TX=\frac{1}{N}V\Sigma^{2}V^T$이므로 고윳값은 $\sigma^2$을 그대로 사용하면 안되고 $N$으로 나누어 사용해주어야 한다.

차원 축소에 대하여

PCA의 최대 장점은 변수를 없앨 수 있다는 것. 차원 축소를 어떻게 하는지에 대해 알아보자.

공분산 행렬을 구해서 고유벡터 행렬과 고윳값 행렬을 구했다고 가정하자.
고윳값은 내림차순으로 정렬되어 있어야 하고, 고유벡터도 고윳값 순서에 맞추어서 정렬되어 있어야 한다.

가장 큰 고윳값과 그에 따른 고유벡터는 무엇을 의미할까.

고유벡터는 데이터의 분산이 최대가 되는 방향을 의미하고 데이터의 분포를 잘 반영하는 새로운 기저벡터가 된다.
고윳값은 그 방향으로의 데이터의 분산의 크기를 의미하고 크기가 클수록 데이터의 중요한 정보를 많이 담고 있다.

만약 3차원에서 2차원으로 차원을 축소하고 싶다면 3개의 고유벡터 중 상위 2개의 고유벡터 쌍을 데이터에 곱해서 차원을 줄이면 된다.
$N$차원에서 $k$차원으로 줄이고 싶다면, $X:M\times N$이고 $V:n=N\times k$일 때 $$XV:M\times k$$
로 차원을 줄일 수 있다.

파이썬으로 구현하기

PCA 클래스 이용하기

PCA도 이미 라이브러리에 구현이 되어 있다.
from sklearn.decomposition import PCA를 통해 PCA 클래스를 가져올 수 있다.
클래스에 인자가 많아서 주요 인자 몇가지와 메소드만 설명을 해보자면,
PCA(n_components=2) 이런 식으로 클래스를 만들게 된다. n_components인자에 int형으로 2를 넣게 되면 상위 고윳값 두 개를 주성분으로 선택하라는 의미이다.
n_components=0.95를 입력할 수도 있는데 이는 전체 데이터의 95% 이상의 분산을 유지하는 주성분을 선택하라는 의미이다. 만일 주성분 4개를 합쳐야 95%이상이 된다면 자동으로 주성분을 4개 택하게 된다.

메소드로 fit_transform(data)를 사용한 모습을 하단 코드에서 확인할 수 있다. 이는 데이터를 학습하면서 동시에 주성분 공간으로 변환한 결과를 반환한다.

  1. 고윳값 분해를 통한 PCA 에서 components_svd=mean_centered@eigvecs_eig까지 진행하고 반환한 것이라 생각하면 된다.

추가로 따로따로 fit(data)transform(data)로 나누어서 사용할 수도 있다.
fit(data)는 주어진 데이터로 PCA를 진행하는 메서드다. 고유벡터와 고윳값을 계산하는 과정을 진행한다.
transform(data)fit()를 사용한 PCA를 진행하고 데이터를 주성분 공간으로 변환하는 메서드다.

코드

import numpy as np
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA

# 데이터 생성
np.random.seed(0)
mean = [5, 5]
cov = [[3, 2], [2, 3]]  # 두 축 사이에 상관성이 있음
data = np.random.multivariate_normal(mean, cov, 500)

# 1. 고유값 분해를 통한 PCA
mean_centered = data - np.mean(data, axis=0)
cov_matrix = np.cov(mean_centered, rowvar=False)
eigvals_eig, eigvecs_eig = np.linalg.eigh(cov_matrix)
sorted_indices = np.argsort(eigvals_eig)[::-1]
eigvals_eig = eigvals_eig[sorted_indices]
eigvecs_eig = eigvecs_eig[:, sorted_indices]
components_eig = mean_centered @ eigvecs_eig

# 2. SVD를 통한 PCA
U, S, Vt = np.linalg.svd(mean_centered)
eigvecs_svd = Vt.T
components_svd = mean_centered @ eigvecs_svd

# 3. PCA 라이브러리를 통한 PCA
pca = PCA(n_components=2)
components_pca = pca.fit_transform(data)

# 시각화
plt.figure(figsize=(16, 12))

# 1번: 원본 데이터
plt.subplot(2, 2, 1)
plt.scatter(data[:, 0], data[:, 1], alpha=0.5, label="Original Data")
plt.quiver(np.mean(data[:, 0]), np.mean(data[:, 1]), eigvecs_eig[0, 0]*3, eigvecs_eig[1, 0]*3, color="red", angles="xy", scale_units="xy", scale=1, label="PC1")
plt.quiver(np.mean(data[:, 0]), np.mean(data[:, 1]), eigvecs_eig[0, 1]*3, eigvecs_eig[1, 1]*3, color="blue", angles="xy", scale_units="xy", scale=1, label="PC2")
plt.title("Original Data with PCA Directions")
plt.xlabel("X-axis")
plt.ylabel("Y-axis")
plt.legend()

# 2번: 고유값 분해 PCA
plt.subplot(2, 2, 2)
#결과가 뒤집혀 나와서 -를 붙여주었음.
plt.scatter(components_eig[:, 0], -
            components_eig[:, 1], alpha=0.5, label="PCA (Eigen Decomposition)")
plt.title("PCA (Eigen Decomposition)")
plt.xlabel("PC1")
plt.ylabel("PC2")
plt.legend()

# 3번: SVD PCA
plt.subplot(2, 2, 3)
plt.scatter(components_svd[:, 0], components_svd[:, 1], alpha=0.5, label="PCA (SVD)")
plt.title("PCA (SVD)")
plt.xlabel("PC1")
plt.ylabel("PC2")
plt.legend()

# 4번: PCA 함수
plt.subplot(2, 2, 4)
plt.scatter(components_pca[:, 0], components_pca[:, 1], alpha=0.5, label="PCA (scikit-learn)")
plt.title("PCA (scikit-learn)")
plt.xlabel("PC1")
plt.ylabel("PC2")
plt.legend()

plt.tight_layout()
plt.show()

결과

2차원 데이터를 잘 나타내는 축 두 개를 찾고 해당 축을 x,y축으로 변환하는 코드다.
축을 찾는 과정으로 고윳값 분해, SVD, PCA함수 세 가지 경우로 진행을 해보았고 결과는 모두 동일했다.
해당 코드는 차원을 축소하지는 않았다. 데이터의 분포를 잘 나타낼 수 있는 축을 찾아서 변환해본 실험일 뿐이다.

차원 축소 예시

이번에는 3차원에서 2차원으로 차원을 축소하는 예시를 살펴보자

코드

import numpy as np
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA
from sklearn.datasets import make_blobs

# 1. 3차원 데이터 생성
np.random.seed(42)  
n_samples = 300
centers = [[-5, -5, -5], [0, 0, 0], [5, 5, 5]]
X, labels = make_blobs(n_samples=n_samples, centers=centers, cluster_std=1.5, random_state=42)

# 2. Figure 생성 및 Subplots 정의
fig = plt.figure(figsize=(12, 6))
ax1 = fig.add_subplot(121, projection='3d')  # 3D 플롯
ax2 = fig.add_subplot(122)  # 2D 플롯

# 3. 3D 데이터 플로팅
ax1.scatter(X[:, 0], X[:, 1], X[:, 2], c=labels, cmap='viridis', s=50, alpha=0.8)
ax1.set_title("3D Scatter Plot with Clusters")
ax1.set_xlabel("X-axis")
ax1.set_ylabel("Y-axis")
ax1.set_zlabel("Z-axis")

# 4. PCA를 통해 2차원으로 축소
pca = PCA(n_components=2)
X_2D = pca.fit_transform(X)

# 5. 2D 데이터 플로팅
ax2.scatter(X_2D[:, 0], X_2D[:, 1], c=labels, cmap='viridis', s=50, alpha=0.8)
ax2.set_title("2D Scatter Plot after PCA")
ax2.set_xlabel("Principal Component 1")
ax2.set_ylabel("Principal Component 2")

plt.tight_layout()
plt.show()

결과

3차원에 군집을 이루고 있는 데이터를 2차원으로 차원을 줄여서 나타냈다. 데이터의 중요한 정보를 유지하면서 계산의 효율성도 높이고 시각화도 용이해진 것을 알 수 있다.

PCA를 한 결과에서 데이터 간의 거리가 커질수록 데이터 간의 상관관계가 작아진다.
노란색 군집과 청록색 군집 간의 수평거리보다 노란색 군집과 보라색 군집 간의 수평거리가 더 작기 때문에 노란색과 청록색 군집의 상관관계가 더 크다는 것을 그래프를 보고 알 수 있다.


차원 축소 알고리즘으로 알려져 있는 PCA에 대해서 알아보았다.
차원을 축소할 수 있다는 점은 PCA의 가장 큰 장점이다. 하지만 이름이 Principal Component Analysis인 만큼 차원축소는 PCA의 기능이지 정의는 아니다.
주어진 데이터의 상관관계를 최대한 표현할 수 있는 기저벡터를 찾는 기법라고 정의하는 것이 더 맞다고 생각한다.

출처

서적

개발자를 위한 실전 선형대수학/ 한빛미디어 / 마이크,코헨 지음

WebSite

sklearn.decomposition PCA
PCA 위키백과

 

PCA에 대해 공부하는 데에 도움이 된 다른 블로그 글
https://angeloyeo.github.io/2019/07/27/PCA.html
https://ddongwon.tistory.com/114
https://maloveforme.tistory.com/221

반응형