Post

PCA (Principal Component Analysis)

PCA (Principal Component Analysis)

PCA (Principal Component Analysis)

231004 학습한 내용 정리

PCA 개요

정의

  • Principal Component Analysis (주성분 분석)
  • 고차원 데이터를 저차원으로 변환하는 차원 축소 기법
  • 데이터의 분산을 최대한 보존하면서 차원을 줄임

특징

  • 분산 보존: 데이터의 분산을 최대한 유지
  • 직교성: 주성분들은 서로 직교
  • 선형 변환: 선형 변환을 통한 차원 축소
  • 비지도 학습: 레이블이 필요하지 않음

장점

  • 차원 축소: 고차원 데이터를 저차원으로 변환
  • 노이즈 제거: 주요 성분만 추출하여 노이즈 감소
  • 시각화: 고차원 데이터를 2D/3D로 시각화
  • 계산 효율성: 차원 축소로 계산 속도 향상

PCA 수학적 원리

1. 주성분 (Principal Components)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import numpy as np
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler

# 샘플 데이터 생성
np.random.seed(42)
X = np.random.randn(100, 3)

# 데이터 표준화
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# PCA 적용
pca = PCA()
X_pca = pca.fit_transform(X_scaled)

# 주성분 확인
print(f"주성분 수: {pca.n_components_}")
print(f"설명 분산 비율: {pca.explained_variance_ratio_}")
print(f"누적 설명 분산 비율: {np.cumsum(pca.explained_variance_ratio_)}")

2. 공분산 행렬과 고유값 분해

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 공분산 행렬 계산
cov_matrix = np.cov(X_scaled.T)
print(f"공분산 행렬:\n{cov_matrix}")

# 고유값과 고유벡터 계산
eigenvalues, eigenvectors = np.linalg.eig(cov_matrix)

# 고유값 정렬
idx = eigenvalues.argsort()[::-1]
eigenvalues = eigenvalues[idx]
eigenvectors = eigenvectors[:, idx]

print(f"고유값: {eigenvalues}")
print(f"고유벡터:\n{eigenvectors}")

# 설명 분산 비율 계산
explained_variance_ratio = eigenvalues / np.sum(eigenvalues)
print(f"설명 분산 비율: {explained_variance_ratio}")

PCA 구현 및 사용법

1. 기본 PCA

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from sklearn.datasets import load_iris

# 아이리스 데이터 로드
iris = load_iris()
X, y = iris.data, iris.target

# 데이터 표준화
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# PCA 적용
pca = PCA(n_components=2)
X_pca = pca.fit_transform(X_scaled)

# 결과 확인
print(f"원본 데이터 형태: {X.shape}")
print(f"PCA 후 데이터 형태: {X_pca.shape}")
print(f"설명 분산 비율: {pca.explained_variance_ratio_}")
print(f"누적 설명 분산 비율: {np.cumsum(pca.explained_variance_ratio_)}")

# 시각화
plt.figure(figsize=(10, 6))
colors = ['red', 'green', 'blue']
for i, color in enumerate(colors):
    plt.scatter(X_pca[y == i, 0], X_pca[y == i, 1], 
                c=color, label=iris.target_names[i], alpha=0.7)
plt.xlabel('첫 번째 주성분')
plt.ylabel('두 번째 주성분')
plt.title('PCA 결과 시각화')
plt.legend()
plt.grid(True)
plt.show()

2. 주성분 수 선택

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 모든 주성분에 대한 설명 분산 비율 계산
pca_full = PCA()
pca_full.fit(X_scaled)

# 스크리 플롯
plt.figure(figsize=(10, 6))
plt.plot(range(1, len(pca_full.explained_variance_ratio_) + 1), 
         pca_full.explained_variance_ratio_, 'bo-')
plt.xlabel('주성분 수')
plt.ylabel('설명 분산 비율')
plt.title('스크리 플롯')
plt.grid(True)
plt.show()

# 누적 설명 분산 비율
plt.figure(figsize=(10, 6))
plt.plot(range(1, len(pca_full.explained_variance_ratio_) + 1), 
         np.cumsum(pca_full.explained_variance_ratio_), 'ro-')
plt.xlabel('주성분 수')
plt.ylabel('누적 설명 분산 비율')
plt.title('누적 설명 분산 비율')
plt.axhline(y=0.95, color='k', linestyle='--', label='95%')
plt.legend()
plt.grid(True)
plt.show()

3. 주성분 수 자동 선택

1
2
3
4
5
6
7
8
9
10
11
12
# 95% 분산을 설명하는 주성분 수 선택
pca_95 = PCA(n_components=0.95)
X_pca_95 = pca_95.fit_transform(X_scaled)

print(f"95% 분산을 설명하는 주성분 수: {pca_95.n_components_}")
print(f"실제 설명 분산 비율: {np.sum(pca_95.explained_variance_ratio_):.4f}")

# 2개 주성분으로 제한
pca_2 = PCA(n_components=2)
X_pca_2 = pca_2.fit_transform(X_scaled)

print(f"2개 주성분 설명 분산 비율: {np.sum(pca_2.explained_variance_ratio_):.4f}")

PCA 고급 기능

1. 역변환 (Inverse Transform)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# PCA 역변환
X_reconstructed = pca_2.inverse_transform(X_pca_2)

# 원본과 재구성된 데이터 비교
mse = np.mean((X_scaled - X_reconstructed) ** 2)
print(f"재구성 오차 (MSE): {mse:.6f}")

# 시각화
fig, axes = plt.subplots(1, 2, figsize=(15, 6))

# 원본 데이터
axes[0].scatter(X_scaled[:, 0], X_scaled[:, 1], c=y, cmap='viridis', alpha=0.7)
axes[0].set_title('원본 데이터 (첫 2개 특성)')
axes[0].set_xlabel('특성 1')
axes[0].set_ylabel('특성 2')

# 재구성된 데이터
axes[1].scatter(X_reconstructed[:, 0], X_reconstructed[:, 1], c=y, cmap='viridis', alpha=0.7)
axes[1].set_title('재구성된 데이터')
axes[1].set_xlabel('특성 1')
axes[1].set_ylabel('특성 2')

plt.tight_layout()
plt.show()

2. 주성분 분석

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 주성분 계수 분석
components = pca_2.components_
feature_names = iris.feature_names

print("주성분 계수:")
for i, component in enumerate(components):
    print(f"PC{i+1}: {dict(zip(feature_names, component))}")

# 주성분 계수 시각화
plt.figure(figsize=(10, 6))
plt.bar(range(len(feature_names)), components[0], alpha=0.7, label='PC1')
plt.bar(range(len(feature_names)), components[1], alpha=0.7, label='PC2')
plt.xlabel('특성')
plt.ylabel('계수')
plt.title('주성분 계수')
plt.xticks(range(len(feature_names)), feature_names, rotation=45)
plt.legend()
plt.grid(True)
plt.show()

3. 희소 PCA (Sparse PCA)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from sklearn.decomposition import SparsePCA

# 희소 PCA 적용
sparse_pca = SparsePCA(n_components=2, alpha=0.1, random_state=42)
X_sparse_pca = sparse_pca.fit_transform(X_scaled)

# 결과 비교
plt.figure(figsize=(15, 5))

# 일반 PCA
plt.subplot(1, 3, 1)
plt.scatter(X_pca[:, 0], X_pca[:, 1], c=y, cmap='viridis', alpha=0.7)
plt.title('일반 PCA')
plt.xlabel('PC1')
plt.ylabel('PC2')

# 희소 PCA
plt.subplot(1, 3, 2)
plt.scatter(X_sparse_pca[:, 0], X_sparse_pca[:, 1], c=y, cmap='viridis', alpha=0.7)
plt.title('희소 PCA')
plt.xlabel('PC1')
plt.ylabel('PC2')

# 주성분 계수 비교
plt.subplot(1, 3, 3)
plt.bar(range(len(feature_names)), sparse_pca.components_[0], alpha=0.7, label='Sparse PC1')
plt.bar(range(len(feature_names)), pca_2.components_[0], alpha=0.7, label='PC1')
plt.xlabel('특성')
plt.ylabel('계수')
plt.title('주성분 계수 비교')
plt.xticks(range(len(feature_names)), feature_names, rotation=45)
plt.legend()
plt.grid(True)

plt.tight_layout()
plt.show()

실무 적용 예시

1. 이미지 압축

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
from sklearn.datasets import fetch_olivetti_faces

# 얼굴 이미지 데이터 로드
faces = fetch_olivetti_faces()
X_faces = faces.data
y_faces = faces.target

# 데이터 표준화
X_faces_scaled = StandardScaler().fit_transform(X_faces)

# PCA 적용
pca_faces = PCA(n_components=100)
X_faces_pca = pca_faces.fit_transform(X_faces_scaled)

# 재구성
X_faces_reconstructed = pca_faces.inverse_transform(X_faces_pca)

# 원본과 재구성된 이미지 비교
fig, axes = plt.subplots(2, 5, figsize=(15, 6))
for i in range(5):
    # 원본 이미지
    axes[0, i].imshow(X_faces[i].reshape(64, 64), cmap='gray')
    axes[0, i].set_title('원본')
    axes[0, i].axis('off')
    
    # 재구성된 이미지
    axes[1, i].imshow(X_faces_reconstructed[i].reshape(64, 64), cmap='gray')
    axes[1, i].set_title('재구성')
    axes[1, i].axis('off')

plt.suptitle('PCA를 이용한 이미지 압축')
plt.tight_layout()
plt.show()

# 압축률 계산
compression_ratio = pca_faces.n_components_ / X_faces.shape[1]
print(f"압축률: {compression_ratio:.2%}")
print(f"설명 분산 비율: {np.sum(pca_faces.explained_variance_ratio_):.4f}")

2. 차원 축소 후 분류

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split, cross_val_score

# 데이터 분할
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# 원본 데이터로 분류
rf_original = RandomForestClassifier(random_state=42)
scores_original = cross_val_score(rf_original, X_train, y_train, cv=5)
print(f"원본 데이터 분류 정확도: {scores_original.mean():.4f} (+/- {scores_original.std() * 2:.4f})")

# PCA 적용 후 분류
X_train_scaled = StandardScaler().fit_transform(X_train)
X_test_scaled = StandardScaler().fit_transform(X_test)

pca_classifier = PCA(n_components=2)
X_train_pca = pca_classifier.fit_transform(X_train_scaled)
X_test_pca = pca_classifier.transform(X_test_scaled)

rf_pca = RandomForestClassifier(random_state=42)
scores_pca = cross_val_score(rf_pca, X_train_pca, y_train, cv=5)
print(f"PCA 적용 후 분류 정확도: {scores_pca.mean():.4f} (+/- {scores_pca.std() * 2:.4f})")

# 시각화
plt.figure(figsize=(10, 6))
plt.bar(['원본 데이터', 'PCA 적용'], [scores_original.mean(), scores_pca.mean()])
plt.ylabel('정확도')
plt.title('PCA 적용 전후 분류 성능 비교')
plt.ylim(0, 1)
plt.grid(True)
plt.show()

3. 이상치 탐지

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# 이상치가 포함된 데이터 생성
np.random.seed(42)
X_normal = np.random.multivariate_normal([0, 0], [[1, 0.5], [0.5, 1]], 100)
X_outliers = np.random.multivariate_normal([3, 3], [[0.1, 0], [0, 0.1]], 10)
X_mixed = np.vstack([X_normal, X_outliers])

# PCA 적용
pca_outlier = PCA(n_components=2)
X_pca_outlier = pca_outlier.fit_transform(X_mixed)

# 재구성 오차 계산
X_reconstructed_outlier = pca_outlier.inverse_transform(X_pca_outlier)
reconstruction_error = np.sum((X_mixed - X_reconstructed_outlier) ** 2, axis=1)

# 이상치 탐지
threshold = np.percentile(reconstruction_error, 95)
outliers = reconstruction_error > threshold

# 시각화
plt.figure(figsize=(15, 5))

# 원본 데이터
plt.subplot(1, 3, 1)
plt.scatter(X_mixed[:, 0], X_mixed[:, 1], c=outliers, cmap='viridis', alpha=0.7)
plt.title('원본 데이터')
plt.xlabel('특성 1')
plt.ylabel('특성 2')

# PCA 결과
plt.subplot(1, 3, 2)
plt.scatter(X_pca_outlier[:, 0], X_pca_outlier[:, 1], c=outliers, cmap='viridis', alpha=0.7)
plt.title('PCA 결과')
plt.xlabel('PC1')
plt.ylabel('PC2')

# 재구성 오차
plt.subplot(1, 3, 3)
plt.scatter(range(len(reconstruction_error)), reconstruction_error, c=outliers, cmap='viridis', alpha=0.7)
plt.axhline(y=threshold, color='r', linestyle='--', label='임계값')
plt.title('재구성 오차')
plt.xlabel('샘플 인덱스')
plt.ylabel('재구성 오차')
plt.legend()

plt.tight_layout()
plt.show()

print(f"탐지된 이상치 수: {np.sum(outliers)}")
print(f"실제 이상치 수: {len(X_outliers)}")

PCA 주의사항 및 모범 사례

1. 데이터 전처리

  • 표준화 필수: PCA는 스케일에 민감하므로 반드시 표준화
  • 결측값 처리: PCA 적용 전 결측값 제거 또는 대체
  • 이상치 처리: 이상치가 주성분에 영향을 줄 수 있음

2. 주성분 수 선택

  • 스크리 플롯: 설명 분산 비율이 급격히 감소하는 지점
  • 누적 분산: 95% 또는 99% 분산을 설명하는 주성분 수
  • 도메인 지식: 비즈니스 요구사항에 맞는 주성분 수

3. 해석 주의사항

  • 선형 관계: PCA는 선형 관계만 포착
  • 비선형 관계: 비선형 관계는 PCA로 포착 불가
  • 해석성: 주성분의 해석이 어려울 수 있음

마무리

PCA는 고차원 데이터를 저차원으로 변환하는 강력한 차원 축소 기법입니다. 데이터의 분산을 최대한 보존하면서 차원을 줄여 시각화, 노이즈 제거, 계산 효율성 향상 등의 목적을 달성할 수 있습니다. 적절한 데이터 전처리와 주성분 수 선택을 통해 실무에서 효과적으로 활용할 수 있습니다. 다만 선형 관계만 포착할 수 있다는 한계를 인지하고, 필요에 따라 비선형 차원 축소 기법도 고려해야 합니다.

This post is licensed under CC BY 4.0 by the author.