🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

PCA 주성분 분석과 차원 축소 완벽 가이드 - 슬라이드 1/10
A

AI Generated

2025. 12. 6. · 15 Views

PCA 주성분 분석과 차원 축소 완벽 가이드

고차원 데이터를 다루는 데이터 과학자와 머신러닝 엔지니어를 위한 PCA 입문서입니다. 복잡한 수학 공식 대신 직관적인 비유와 실무 예제로 차원 축소의 핵심을 이해할 수 있습니다.


목차

  1. 차원의_저주와_차원_축소의_필요성
  2. PCA의_핵심_원리_분산_최대화
  3. sklearn으로_PCA_구현하기
  4. 최적의_주성분_개수_결정하기
  5. 데이터_스케일링의_중요성
  6. PCA를_활용한_데이터_시각화
  7. 머신러닝_파이프라인에서_PCA_활용
  8. PCA의_한계와_대안_기법들
  9. 실무_프로젝트_완성하기

1. 차원의 저주와 차원 축소의 필요성

김개발 씨는 최근 고객 이탈 예측 모델을 만들고 있습니다. 데이터를 살펴보니 무려 500개가 넘는 특성이 있었습니다.

모델을 학습시켰지만 성능이 형편없고, 학습 시간도 너무 오래 걸립니다. 도대체 무엇이 문제일까요?

차원의 저주란 데이터의 특성(차원)이 늘어날수록 분석과 학습이 기하급수적으로 어려워지는 현상입니다. 마치 방이 커질수록 물건을 찾기 어려워지는 것과 같습니다.

차원 축소는 이 문제를 해결하기 위해 중요한 정보는 유지하면서 차원을 줄이는 기법입니다.

다음 코드를 살펴봅시다.

import numpy as np
from sklearn.datasets import make_classification

# 고차원 데이터 생성 (1000개 샘플, 100개 특성)
X, y = make_classification(n_samples=1000, n_features=100,
                           n_informative=10, n_redundant=50,
                           random_state=42)

# 실제 의미 있는 특성은 10개뿐
# 나머지 50개는 중복, 40개는 노이즈
print(f"원본 데이터 형태: {X.shape}")
# 출력: 원본 데이터 형태: (1000, 100)

# 이 100개를 모두 사용할 필요가 있을까요?

김개발 씨는 입사 6개월 차 데이터 분석가입니다. 이번 프로젝트는 고객 이탈을 예측하는 머신러닝 모델을 만드는 것이었습니다.

마케팅팀에서 받은 데이터에는 고객의 나이, 성별, 구매 이력, 접속 빈도 등 무려 500개가 넘는 특성이 담겨 있었습니다. "특성이 많으면 많을수록 좋은 거 아닌가요?" 김개발 씨는 자신 있게 모든 특성을 넣고 모델을 학습시켰습니다.

그런데 결과는 참담했습니다. 학습 시간은 몇 시간이 걸렸고, 정작 예측 정확도는 60%도 되지 않았습니다.

선배 데이터 사이언티스트 박시니어 씨가 다가와 말했습니다. "차원의 저주에 걸렸네요.

특성이 많다고 좋은 게 아니에요." 그렇다면 차원의 저주란 정확히 무엇일까요? 쉽게 비유하자면, 넓은 창고에서 물건을 찾는 상황을 떠올려 보세요.

창고가 작을 때는 물건을 금방 찾을 수 있습니다. 하지만 창고가 커지면 커질수록 물건 사이의 거리가 멀어지고, 원하는 물건을 찾기가 점점 어려워집니다.

데이터도 마찬가지입니다. 차원이 늘어나면 데이터 포인트들 사이의 거리가 멀어지고, 의미 있는 패턴을 찾기가 어려워집니다.

더 큰 문제가 있습니다. 차원이 늘어나면 필요한 데이터의 양도 기하급수적으로 늘어납니다.

10차원 공간을 제대로 채우려면 1차원의 10배가 아니라 수천, 수만 배의 데이터가 필요합니다. 우리가 가진 데이터는 한정되어 있으니, 고차원에서는 데이터가 듬성듬성 흩어져 있게 됩니다.

바로 이런 문제를 해결하기 위해 차원 축소가 등장했습니다. 차원 축소의 핵심 아이디어는 간단합니다.

500개의 특성 중에서 정말 중요한 정보만 담고 있는 것은 몇 개 되지 않습니다. 나머지는 중복되거나 노이즈일 가능성이 높습니다.

그렇다면 핵심 정보만 추출해서 차원을 줄이면 되지 않을까요? 실제로 위의 코드를 보면, 100개의 특성 중 실제 의미 있는 정보를 담고 있는 것은 10개뿐입니다.

50개는 그 10개의 중복이고, 나머지 40개는 순수한 노이즈입니다. 이런 상황에서 100개 전체를 사용하는 것은 비효율적입니다.

차원 축소를 사용하면 계산 비용이 크게 줄어듭니다. 또한 과적합 위험도 낮아집니다.

무엇보다 데이터의 핵심 구조를 더 명확하게 파악할 수 있습니다. 김개발 씨는 고개를 끄덕였습니다.

"그럼 어떻게 차원을 줄이면 되나요?" 박시니어 씨가 미소 지으며 대답했습니다. "바로 PCA를 사용하면 됩니다."

실전 팁

💡 - 특성이 50개가 넘어가면 차원 축소를 고려해보세요

  • 모든 특성을 무조건 사용하는 것보다 핵심 특성만 선별하는 것이 더 좋은 성능을 낼 수 있습니다

2. PCA의 핵심 원리 분산 최대화

박시니어 씨가 화이트보드 앞에 섰습니다. "PCA를 이해하려면 먼저 분산이라는 개념을 알아야 해요." 김개발 씨는 통계 시간에 배웠던 분산 공식을 떠올리며 긴장했습니다.

하지만 박시니어 씨의 설명은 예상과 달랐습니다.

**PCA(Principal Component Analysis)**는 데이터의 분산이 가장 큰 방향을 찾아 그 방향으로 데이터를 투영하는 기법입니다. 분산이 크다는 것은 그 방향에 정보가 많다는 의미입니다.

마치 사진을 찍을 때 가장 특징이 잘 드러나는 각도를 찾는 것과 같습니다.

다음 코드를 살펴봅시다.

import numpy as np
import matplotlib.pyplot as plt

# 2차원 데이터 생성 (타원형으로 퍼진 데이터)
np.random.seed(42)
X = np.random.randn(100, 2)
X = X @ np.array([[3, 1], [1, 1]])  # 데이터에 기울기 부여

# 분산이 가장 큰 방향 = 첫 번째 주성분
# 데이터가 가장 넓게 퍼진 방향을 찾습니다
mean = np.mean(X, axis=0)
X_centered = X - mean

# 공분산 행렬의 고유벡터가 주성분 방향
cov_matrix = np.cov(X_centered.T)
eigenvalues, eigenvectors = np.linalg.eig(cov_matrix)
print(f"분산(고유값): {eigenvalues}")
# 큰 고유값 = 그 방향에 정보가 많음

박시니어 씨가 화이트보드에 점들을 그렸습니다. 점들은 대각선 방향으로 길쭉하게 퍼져 있었습니다.

"여기 데이터가 있어요. 이 2차원 데이터를 1차원으로 줄여야 한다면, 어느 방향으로 투영하는 게 좋을까요?" 김개발 씨는 잠시 생각했습니다.

수평으로 투영할까요? 수직으로 투영할까요?

박시니어 씨가 설명을 이었습니다. "핵심은 분산이에요.

분산이 크다는 건 데이터가 넓게 퍼져 있다는 뜻이고, 그만큼 다양한 정보를 담고 있다는 의미예요." 비유를 들어보겠습니다. 여러분이 건물 사진을 찍는다고 생각해보세요.

정면에서 찍으면 건물의 폭과 높이를 알 수 있습니다. 하지만 완전히 옆에서 찍으면 두께만 보이고 나머지 정보는 사라집니다.

가장 좋은 각도는 건물의 특징이 가장 잘 드러나는 각도입니다. PCA도 마찬가지입니다.

데이터를 낮은 차원으로 투영할 때, 정보가 가장 잘 보존되는 방향을 찾습니다. 그 방향이 바로 분산이 가장 큰 방향입니다.

위의 코드를 살펴보겠습니다. 먼저 2차원 데이터를 생성하고 기울기를 부여했습니다.

이렇게 하면 데이터가 대각선 방향으로 길쭉하게 퍼집니다. 다음으로 데이터의 평균을 빼서 중심을 원점으로 이동시켰습니다.

이 과정을 센터링이라고 합니다. PCA를 적용하기 전에 반드시 수행해야 하는 전처리 단계입니다.

그 다음 공분산 행렬을 계산했습니다. 공분산 행렬은 각 특성들이 서로 어떻게 변하는지를 나타냅니다.

이 행렬의 고유벡터가 바로 주성분의 방향이 됩니다. 고유값은 각 방향의 분산 크기를 나타냅니다.

고유값이 크면 그 방향에 정보가 많다는 뜻입니다. PCA는 고유값이 큰 순서대로 주성분을 정렬합니다.

김개발 씨가 물었습니다. "그럼 첫 번째 주성분이 가장 중요한 거네요?" 박시니어 씨가 고개를 끄덕였습니다.

"맞아요. 첫 번째 주성분이 데이터의 분산을 가장 많이 설명해요.

두 번째는 그 다음으로 많이, 이런 식으로 순서가 매겨져요." 이것이 PCA가 차원 축소에 효과적인 이유입니다. 처음 몇 개의 주성분만 사용해도 데이터의 대부분의 정보를 보존할 수 있습니다.

실전 팁

💡 - 분산이 크다 = 정보량이 많다는 직관을 기억하세요

  • PCA 적용 전 반드시 데이터 센터링(평균 빼기)을 수행해야 합니다

3. sklearn으로 PCA 구현하기

이론을 이해한 김개발 씨는 이제 직접 코드를 작성해보고 싶어졌습니다. "선배, 매번 고유벡터를 직접 계산해야 하나요?" 박시니어 씨가 웃으며 대답했습니다.

"아니요, scikit-learn을 쓰면 한 줄이면 돼요."

scikit-learn의 PCA 클래스를 사용하면 복잡한 수학 계산 없이도 쉽게 차원 축소를 수행할 수 있습니다. n_components 매개변수로 원하는 차원 수를 지정하면 됩니다.

fit_transform 메서드 하나로 학습과 변환을 동시에 수행합니다.

다음 코드를 살펴봅시다.

from sklearn.decomposition import PCA
from sklearn.datasets import load_digits
import numpy as np

# 손글씨 숫자 데이터 로드 (64차원)
digits = load_digits()
X = digits.data
print(f"원본 차원: {X.shape}")  # (1797, 64)

# PCA로 10차원으로 축소
pca = PCA(n_components=10)
X_reduced = pca.fit_transform(X)
print(f"축소 후 차원: {X_reduced.shape}")  # (1797, 10)

# 각 주성분이 설명하는 분산 비율
print(f"설명된 분산 비율: {pca.explained_variance_ratio_[:3]}")
# 처음 3개 주성분이 전체 분산의 약 40% 설명

박시니어 씨가 김개발 씨의 노트북 앞에 앉았습니다. "실제로 PCA를 사용하는 건 정말 간단해요.

scikit-learn이 모든 복잡한 계산을 대신해주거든요." 위의 코드에서는 손글씨 숫자 데이터셋을 사용했습니다. 이 데이터셋은 0부터 9까지의 손글씨 숫자 이미지로 구성되어 있습니다.

각 이미지는 8x8 픽셀이므로 64차원의 데이터입니다. 먼저 PCA 객체를 생성합니다.

n_components 매개변수에 원하는 차원 수를 지정합니다. 여기서는 64차원을 10차원으로 줄이기로 했습니다.

fit_transform 메서드는 두 가지 일을 동시에 수행합니다. 먼저 fit 단계에서 데이터를 분석하여 주성분 방향을 학습합니다.

그 다음 transform 단계에서 학습된 주성분으로 데이터를 투영합니다. 결과를 보면 (1797, 64) 형태였던 데이터가 (1797, 10) 형태로 줄어들었습니다.

샘플 수는 그대로이고 특성 수만 64개에서 10개로 줄었습니다. 여기서 중요한 개념이 하나 있습니다.

바로 explained_variance_ratio_ 입니다. 이 속성은 각 주성분이 전체 분산에서 얼마나 많은 비율을 설명하는지 알려줍니다.

예를 들어 첫 번째 주성분의 설명 비율이 0.15라면, 이 하나의 주성분이 전체 정보의 15%를 담고 있다는 뜻입니다. 처음 10개의 주성분을 모두 합하면 대략 70~80%의 정보를 보존할 수 있습니다.

"그런데 선배, 10차원이 적절한지 어떻게 알 수 있어요?" 김개발 씨가 물었습니다. 박시니어 씨가 대답했습니다.

"좋은 질문이에요. 보통 두 가지 방법을 사용해요.

첫째, 설명된 분산 비율의 합이 일정 수준(예: 95%) 이상이 되도록 차원을 선택해요. 둘째, 엘보우 차트를 그려서 기울기가 완만해지는 지점을 찾아요." 이 두 가지 방법에 대해서는 다음 카드에서 자세히 다루겠습니다.

실무에서는 일단 fit_transform으로 변환을 수행하고, 그 결과를 머신러닝 모델의 입력으로 사용합니다. 64차원 전체를 사용하는 것보다 10차원만 사용해도 비슷한 성능을 낼 수 있고, 학습 속도는 훨씬 빨라집니다.

실전 팁

💡 - fit_transform은 학습용 데이터에, transform은 테스트 데이터에 사용하세요

  • PCA 객체를 저장해두면 새로운 데이터에도 같은 변환을 적용할 수 있습니다

4. 최적의 주성분 개수 결정하기

김개발 씨는 PCA를 적용해봤지만 새로운 고민이 생겼습니다. "주성분을 몇 개나 사용해야 할까요?

너무 적으면 정보 손실이 크고, 너무 많으면 차원 축소의 의미가 없잖아요." 박시니어 씨가 화면에 그래프를 띄웠습니다.

최적의 주성분 개수를 결정하는 방법은 크게 두 가지입니다. 누적 설명 분산 비율이 일정 기준(보통 90~95%)에 도달하는 지점을 선택하거나, **스크리 플롯(엘보우 차트)**에서 기울기가 완만해지는 지점을 찾습니다.

두 방법을 함께 사용하면 더 신뢰할 수 있는 결정을 내릴 수 있습니다.

다음 코드를 살펴봅시다.

from sklearn.decomposition import PCA
from sklearn.datasets import load_digits
import numpy as np

digits = load_digits()
X = digits.data

# 모든 주성분 계산
pca = PCA()
pca.fit(X)

# 누적 설명 분산 비율 계산
cumulative_variance = np.cumsum(pca.explained_variance_ratio_)
print("누적 설명 분산 비율:")
for i in [5, 10, 20, 30]:
    print(f"  {i}개 주성분: {cumulative_variance[i-1]:.2%}")

# 95% 분산을 설명하는 최소 주성분 개수
n_95 = np.argmax(cumulative_variance >= 0.95) + 1
print(f"\n95% 분산 설명에 필요한 주성분: {n_95}개")
# 64차원 -> 약 30차원으로 축소 가능

박시니어 씨가 화면에 그래프를 띄웠습니다. x축에는 주성분 개수가, y축에는 누적 설명 분산 비율이 표시되어 있었습니다.

"이 그래프가 바로 누적 설명 분산 비율 그래프예요. 주성분을 하나씩 추가할 때마다 설명할 수 있는 분산이 얼마나 늘어나는지 보여줘요." 그래프를 보니 처음에는 가파르게 올라가다가 점점 완만해지는 곡선이었습니다.

첫 번째 주성분만으로도 전체 분산의 약 12%를 설명했고, 10개를 사용하면 약 70%, 30개를 사용하면 약 95%를 설명했습니다. 김개발 씨가 물었습니다.

"그럼 95%가 기준인가요?" "정해진 답은 없어요. 하지만 실무에서는 보통 90%에서 95% 사이를 기준으로 많이 사용해요.

95%면 대부분의 정보를 보존하면서도 차원을 크게 줄일 수 있거든요." 위의 코드를 살펴보겠습니다. 먼저 n_components를 지정하지 않고 PCA를 생성하면 모든 주성분을 계산합니다.

그 다음 np.cumsum으로 누적 합을 계산합니다. 결과를 보면 5개 주성분으로 약 49%, 10개로 약 70%, 20개로 약 87%, 30개로 약 95%의 분산을 설명합니다.

64차원 데이터를 30차원으로 줄여도 95%의 정보를 보존할 수 있다는 뜻입니다. **np.argmax(cumulative_variance >= 0.95)**는 처음으로 95%를 넘는 인덱스를 찾습니다.

인덱스는 0부터 시작하므로 1을 더해서 실제 개수를 구합니다. 또 다른 방법은 스크리 플롯(scree plot)입니다.

이 그래프는 각 주성분의 고유값(설명 분산)을 순서대로 표시합니다. 마치 팔꿈치 모양처럼 꺾이는 지점이 있는데, 이를 엘보우 포인트라고 합니다.

이 지점 이후로는 주성분을 추가해도 정보 증가량이 미미합니다. 실무에서는 두 방법을 함께 사용하는 것이 좋습니다.

누적 분산 비율로 대략적인 범위를 정하고, 스크리 플롯으로 최종 결정을 내립니다. 박시니어 씨가 덧붙였습니다.

"물론 이것도 절대적인 기준은 아니에요. 최종적으로는 축소된 데이터로 모델을 학습시켜보고 성능을 비교하는 게 가장 확실해요."

실전 팁

💡 - 95% 분산 설명을 기본 기준으로 삼되, 상황에 따라 조정하세요

  • n_components에 0과 1 사이의 실수를 넣으면 해당 비율만큼 설명하는 최소 개수를 자동 선택합니다

5. 데이터 스케일링의 중요성

김개발 씨는 신이 나서 자신의 프로젝트에 PCA를 적용해봤습니다. 그런데 결과가 이상했습니다.

첫 번째 주성분이 거의 모든 분산을 설명하고, 나머지는 의미가 없어 보였습니다. 박시니어 씨가 데이터를 보더니 바로 문제를 발견했습니다.

"스케일링을 안 했네요."

PCA는 분산을 기준으로 주성분을 찾기 때문에 특성의 스케일에 매우 민감합니다. 나이(0~100)와 연봉(수천만 원)처럼 단위가 다른 특성을 그대로 사용하면, 값이 큰 특성이 주성분을 지배하게 됩니다.

반드시 StandardScaler로 정규화한 후 PCA를 적용해야 합니다.

다음 코드를 살펴봅시다.

from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
import numpy as np

# 스케일이 다른 데이터 (나이: 0-100, 연봉: 0-100000000)
np.random.seed(42)
age = np.random.randint(20, 60, 100).reshape(-1, 1)
salary = np.random.randint(30000000, 80000000, 100).reshape(-1, 1)
X = np.hstack([age, salary])

# 스케일링 없이 PCA
pca_no_scale = PCA()
pca_no_scale.fit(X)
print("스케일링 없이:")
print(f"  분산 비율: {pca_no_scale.explained_variance_ratio_}")
# 연봉이 99.99% 차지 -> 잘못된 결과!

# 스케일링 후 PCA (올바른 방법)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
pca_scaled = PCA()
pca_scaled.fit(X_scaled)
print("\n스케일링 후:")
print(f"  분산 비율: {pca_scaled.explained_variance_ratio_}")
# 두 특성이 균형 있게 반영됨

김개발 씨의 데이터에는 다양한 특성이 있었습니다. 고객의 나이는 20세에서 60세 사이였고, 연봉은 3천만 원에서 8천만 원 사이였습니다.

접속 횟수는 0에서 100 사이였고, 구매 금액은 0원에서 수백만 원까지 다양했습니다. 박시니어 씨가 설명했습니다.

"PCA가 찾는 건 분산이 큰 방향이에요. 그런데 연봉의 분산은 수십억 단위이고, 나이의 분산은 겨우 수백 단위예요.

당연히 연봉이 모든 분산을 독차지하겠죠?" 마치 키와 몸무게를 비교하는 상황을 생각해보세요. 키를 센티미터로 측정하면 170, 몸무게를 그램으로 측정하면 70000입니다.

단순히 숫자만 보면 몸무게가 훨씬 중요해 보이지만, 실제로는 두 특성 모두 중요합니다. StandardScaler는 이 문제를 해결합니다.

각 특성에서 평균을 빼고 표준편차로 나눕니다. 그 결과 모든 특성이 평균 0, 표준편차 1인 동일한 스케일을 갖게 됩니다.

위의 코드에서 스케일링을 하지 않은 경우를 보세요. 첫 번째 주성분이 전체 분산의 99.99%를 설명합니다.

이것은 사실상 연봉 특성 하나만 사용하는 것과 같습니다. 나이라는 정보는 완전히 무시되었습니다.

반면 스케일링을 한 후에는 두 주성분이 각각 약 50%씩 분산을 설명합니다. 두 특성이 균형 있게 주성분에 반영된 것입니다.

실무에서는 항상 다음과 같은 파이프라인을 사용합니다. 먼저 StandardScaler로 정규화를 수행하고, 그 다음 PCA를 적용합니다.

scikit-learn의 Pipeline을 사용하면 이 두 단계를 하나로 묶을 수 있습니다. 주의할 점이 있습니다.

스케일링과 PCA 모두 학습 데이터로 fit한 뒤, 테스트 데이터에는 transform만 적용해야 합니다. 테스트 데이터에 fit을 하면 데이터 누출이 발생합니다.

김개발 씨가 다시 시도했습니다. 이번에는 StandardScaler를 먼저 적용한 후 PCA를 수행했습니다.

결과가 훨씬 합리적으로 나왔습니다.

실전 팁

💡 - PCA 전에 StandardScaler는 필수입니다. 이것을 잊으면 잘못된 결과가 나옵니다

  • Pipeline으로 스케일링과 PCA를 묶으면 실수를 방지할 수 있습니다

6. PCA를 활용한 데이터 시각화

팀 회의에서 김개발 씨는 분석 결과를 발표해야 했습니다. 500차원의 고객 데이터를 어떻게 보여줄 수 있을까요?

그래프로 그리려 해도 2차원이나 3차원까지밖에 표현할 수 없습니다. 박시니어 씨가 귀띔해줬습니다.

"PCA로 2차원으로 줄여서 시각화하면 돼요."

고차원 데이터를 2차원 또는 3차원으로 축소하면 산점도로 시각화할 수 있습니다. PCA로 축소한 좌표에 클래스 레이블을 색으로 표시하면 데이터의 구조와 클러스터를 한눈에 파악할 수 있습니다.

이것은 탐색적 데이터 분석(EDA)에서 매우 유용한 기법입니다.

다음 코드를 살펴봅시다.

from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
from sklearn.datasets import load_iris
import matplotlib.pyplot as plt

# 붓꽃 데이터 로드 (4차원)
iris = load_iris()
X, y = iris.data, iris.target

# 스케일링 후 2차원으로 축소
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
pca = PCA(n_components=2)
X_pca = pca.fit_transform(X_scaled)

# 시각화
plt.figure(figsize=(8, 6))
for i, name in enumerate(iris.target_names):
    mask = y == i
    plt.scatter(X_pca[mask, 0], X_pca[mask, 1], label=name, alpha=0.7)
plt.xlabel(f'PC1 ({pca.explained_variance_ratio_[0]:.1%})')
plt.ylabel(f'PC2 ({pca.explained_variance_ratio_[1]:.1%})')
plt.legend()
plt.title('PCA로 시각화한 붓꽃 데이터')
plt.savefig('pca_visualization.png')

발표 자료를 준비하던 김개발 씨는 난감했습니다. 500개의 특성을 가진 고객 데이터를 어떻게 시각화할 수 있을까요?

엑셀 차트로는 한계가 있었습니다. 박시니어 씨가 해결책을 제시했습니다.

"PCA로 2차원으로 줄인 다음에 산점도를 그리면 돼요. 물론 정보 손실은 있지만, 데이터의 전체적인 구조를 파악하기에는 충분해요." 위의 코드에서는 유명한 붓꽃(Iris) 데이터셋을 사용했습니다.

이 데이터셋은 꽃받침 길이, 꽃받침 너비, 꽃잎 길이, 꽃잎 너비라는 4개의 특성을 가지고 있습니다. 세 종류의 붓꽃(setosa, versicolor, virginica)을 분류하는 것이 목표입니다.

4차원 데이터를 그래프로 그릴 수는 없습니다. 하지만 PCA로 2차원으로 축소하면 평면 위에 점을 찍을 수 있습니다.

각 점을 종류별로 다른 색으로 표시하면 세 종류가 어떻게 분포하는지 한눈에 볼 수 있습니다. 시각화 결과를 보면 재미있는 패턴이 보입니다.

setosa 종은 다른 두 종과 확실히 분리되어 있습니다. 반면 versicolor와 virginica는 일부 겹치는 영역이 있습니다.

이것은 분류 모델을 만들 때 setosa는 쉽게 구분할 수 있지만, 나머지 두 종은 혼동될 수 있다는 것을 시사합니다. 축 레이블에 각 주성분의 설명 분산 비율을 표시하면 더 좋습니다.

예를 들어 "PC1 (72.9%)"라고 표시하면 첫 번째 주성분이 전체 분산의 72.9%를 설명한다는 것을 알 수 있습니다. 이 기법은 **탐색적 데이터 분석(EDA)**에서 매우 유용합니다.

모델을 만들기 전에 데이터의 구조를 파악할 수 있습니다. 클래스가 잘 분리되어 있는지, 이상치가 있는지, 어떤 패턴이 있는지를 시각적으로 확인할 수 있습니다.

3차원으로 축소해서 3D 산점도를 그릴 수도 있습니다. 하지만 2차원이 해석하기 더 쉽고, 대부분의 경우 충분한 정보를 제공합니다.

김개발 씨는 PCA 시각화 결과를 발표 자료에 넣었습니다. 기획팀도 데이터 구조를 쉽게 이해할 수 있었고, 발표는 성공적이었습니다.

실전 팁

💡 - 축 레이블에 설명 분산 비율을 표시하면 정보가 더 풍부해집니다

  • 이상치 탐지에도 PCA 시각화가 유용합니다. 다른 점들과 멀리 떨어진 점이 이상치일 수 있습니다

7. 머신러닝 파이프라인에서 PCA 활용

이제 김개발 씨는 실제 모델에 PCA를 적용해보려 합니다. 하지만 스케일링, PCA, 모델 학습을 각각 따로 하다 보니 코드가 복잡해지고 실수하기 쉬웠습니다.

박시니어 씨가 Pipeline을 소개해줬습니다. "이걸 사용하면 모든 단계를 하나로 묶을 수 있어요."

scikit-learn의 Pipeline을 사용하면 전처리와 모델 학습을 하나의 객체로 관리할 수 있습니다. 스케일링, PCA, 분류기를 순서대로 연결하면 fit과 predict 메서드 하나로 모든 과정이 자동으로 처리됩니다.

교차 검증도 파이프라인 전체에 적용할 수 있어 데이터 누출을 방지할 수 있습니다.

다음 코드를 살펴봅시다.

from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
from sklearn.datasets import load_digits

# 손글씨 숫자 데이터
digits = load_digits()
X, y = digits.data, digits.target

# 파이프라인 구성: 스케일링 -> PCA -> 분류기
pipeline = Pipeline([
    ('scaler', StandardScaler()),
    ('pca', PCA(n_components=0.95)),  # 95% 분산 유지
    ('classifier', LogisticRegression(max_iter=1000))
])

# 교차 검증으로 성능 평가
scores = cross_val_score(pipeline, X, y, cv=5)
print(f"교차 검증 정확도: {scores.mean():.3f} (+/- {scores.std():.3f})")

# 전체 데이터로 학습
pipeline.fit(X, y)
n_components = pipeline.named_steps['pca'].n_components_
print(f"선택된 주성분 개수: {n_components}")  # 약 30개

김개발 씨가 작성한 코드는 이런 식이었습니다. 먼저 StandardScaler로 학습 데이터를 변환하고, 그 결과로 PCA를 학습시키고, 또 그 결과로 모델을 학습시킵니다.

테스트할 때도 같은 순서로 변환을 적용해야 합니다. 이 과정에서 실수하기 쉬운 부분이 많습니다.

테스트 데이터에 스케일러를 새로 fit하면 안 됩니다. PCA도 마찬가지입니다.

순서를 잘못 바꾸거나 하나라도 빠뜨리면 결과가 완전히 달라집니다. Pipeline은 이 모든 과정을 하나로 묶어줍니다.

위의 코드를 보면 세 단계를 리스트로 연결했습니다. 각 단계는 (이름, 객체) 형태의 튜플입니다.

Pipeline의 fit 메서드를 호출하면 순서대로 각 단계의 fit_transform이 실행됩니다. 마지막 단계(분류기)만 fit이 실행됩니다.

predict 메서드를 호출하면 각 단계의 transform이 순서대로 실행되고, 마지막에 predict가 실행됩니다. 특히 주목할 부분은 cross_val_score와 함께 사용한 것입니다.

교차 검증에서 각 폴드마다 스케일링과 PCA가 새로 fit됩니다. 이것이 올바른 방법입니다.

만약 전체 데이터로 스케일링을 먼저 하고 교차 검증을 하면 데이터 누출이 발생합니다. 코드에서 **PCA(n_components=0.95)**라고 지정했습니다.

0과 1 사이의 실수를 넣으면 그 비율만큼의 분산을 설명하는 최소 개수의 주성분을 자동으로 선택합니다. 몇 개가 선택되었는지는 학습 후 n_components_ 속성에서 확인할 수 있습니다.

파이프라인의 중간 단계에 접근하려면 named_steps 딕셔너리를 사용합니다. 예를 들어 PCA 객체에 접근하려면 pipeline.named_steps['pca']라고 하면 됩니다.

이 패턴은 실무에서 매우 자주 사용됩니다. 전처리와 모델을 하나의 객체로 관리할 수 있어서 코드가 깔끔해지고, 저장과 배포도 간편해집니다.

모델 파일 하나만 저장하면 전처리 로직까지 함께 저장됩니다.

실전 팁

💡 - n_components에 0.95 같은 비율을 넣으면 최적 개수를 자동으로 선택합니다

  • Pipeline을 pickle로 저장하면 전처리 로직과 모델을 함께 배포할 수 있습니다

8. PCA의 한계와 대안 기법들

PCA에 익숙해진 김개발 씨는 모든 데이터에 PCA를 적용하기 시작했습니다. 그런데 어떤 데이터에서는 PCA가 잘 작동하지 않았습니다.

특히 비선형적인 구조를 가진 데이터에서 문제가 생겼습니다. 박시니어 씨가 설명했습니다.

"PCA는 만능이 아니에요. 한계를 알고 대안도 알아둬야 해요."

PCA는 선형 변환만 수행할 수 있어서 비선형 구조를 포착하지 못합니다. 또한 특성 간의 상관관계가 없는 데이터에서는 효과가 제한적입니다.

이런 경우 t-SNE, UMAP, 커널 PCA 같은 비선형 차원 축소 기법이 대안이 될 수 있습니다.

다음 코드를 살펴봅시다.

from sklearn.manifold import TSNE
from sklearn.decomposition import KernelPCA
from sklearn.datasets import make_swiss_roll
import numpy as np

# 스위스 롤 데이터 (비선형 구조)
X, color = make_swiss_roll(n_samples=1000, random_state=42)

# PCA는 비선형 구조를 펴지 못함
from sklearn.decomposition import PCA
pca = PCA(n_components=2)
X_pca = pca.fit_transform(X)
print("PCA: 비선형 구조가 뭉개짐")

# t-SNE는 비선형 구조 보존
tsne = TSNE(n_components=2, random_state=42, perplexity=30)
X_tsne = tsne.fit_transform(X)
print("t-SNE: 비선형 구조 보존")

# 커널 PCA도 비선형 변환 가능
kpca = KernelPCA(n_components=2, kernel='rbf', gamma=0.01)
X_kpca = kpca.fit_transform(X)
print("Kernel PCA: RBF 커널로 비선형 변환")

박시니어 씨가 화이트보드에 나선형 모양을 그렸습니다. "이런 데이터가 있다고 생각해봐요.

3차원 공간에서 롤케이크처럼 말려 있는 형태예요. 이걸 스위스 롤 데이터라고 불러요." PCA로 이 데이터를 2차원으로 줄이면 어떻게 될까요?

PCA는 직선 방향으로만 투영할 수 있기 때문에 나선 구조가 완전히 뭉개집니다. 원래 멀리 떨어져 있던 점들이 서로 가까워지고, 가까웠던 점들이 멀어지기도 합니다.

이것이 PCA의 근본적인 한계입니다. PCA는 선형 변환만 수행할 수 있습니다.

데이터가 직선이나 평면에 잘 놓여 있으면 효과적이지만, 곡선이나 곡면 위에 있으면 제대로 작동하지 않습니다. 이런 경우에 사용할 수 있는 대안들이 있습니다.

첫째, t-SNE입니다. t-SNE는 데이터 포인트 간의 유사성을 보존하면서 차원을 줄입니다.

가까운 점은 가깝게, 먼 점은 멀게 유지합니다. 비선형 구조도 잘 포착하기 때문에 시각화에 많이 사용됩니다.

둘째, UMAP입니다. t-SNE와 비슷한 원리이지만 더 빠르고, 전역적인 구조도 더 잘 보존합니다.

최근에는 t-SNE 대신 UMAP을 사용하는 경우가 많아졌습니다. 셋째, 커널 PCA입니다.

PCA에 커널 트릭을 적용한 것입니다. RBF 커널 같은 비선형 커널을 사용하면 비선형 변환이 가능해집니다.

하지만 이 대안들도 만능은 아닙니다. t-SNE와 UMAP은 계산 비용이 크고, 하이퍼파라미터에 민감합니다.

또한 새로운 데이터에 대해 transform을 직접 적용하기 어렵습니다. 커널 PCA는 커널 선택과 파라미터 튜닝이 필요합니다.

그래서 실무에서는 보통 다음과 같은 전략을 사용합니다. 먼저 PCA로 시도해보고 결과를 확인합니다.

만약 PCA로 충분하다면 그대로 사용합니다. 비선형 구조가 의심되거나 시각화 목적이라면 t-SNE나 UMAP을 사용합니다.

김개발 씨가 물었습니다. "그럼 언제 PCA를 쓰고 언제 다른 기법을 써야 하나요?" 박시니어 씨가 대답했습니다.

"머신러닝 전처리 목적이라면 PCA가 대부분 충분해요. 빠르고 안정적이거든요.

시각화나 클러스터 분석이 목적이고 복잡한 구조가 있다면 t-SNE나 UMAP을 써보세요."

실전 팁

💡 - 머신러닝 전처리에는 PCA, 시각화에는 t-SNE나 UMAP을 기본으로 고려하세요

  • 여러 기법을 시도해보고 결과를 비교하는 것이 가장 확실합니다

9. 실무 프로젝트 완성하기

이제 김개발 씨는 배운 모든 것을 종합해서 실제 프로젝트에 적용할 차례입니다. 고객 이탈 예측 모델을 처음부터 다시 만들기로 했습니다.

이번에는 제대로 된 파이프라인을 구축하고, PCA로 차원을 줄이고, 최적의 주성분 개수도 찾아보겠습니다.

실무에서는 PCA를 단독으로 사용하지 않고 전체 머신러닝 파이프라인의 일부로 사용합니다. GridSearchCV를 활용하면 최적의 주성분 개수를 자동으로 찾을 수 있습니다.

이 예제에서는 데이터 로드부터 모델 평가까지 전체 과정을 다룹니다.

다음 코드를 살펴봅시다.

from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import GridSearchCV, train_test_split
from sklearn.datasets import make_classification
from sklearn.metrics import classification_report

# 고객 이탈 시뮬레이션 데이터 (100개 특성)
X, y = make_classification(n_samples=2000, n_features=100,
                           n_informative=20, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42)

# 파이프라인 구성
pipeline = Pipeline([
    ('scaler', StandardScaler()),
    ('pca', PCA()),
    ('clf', RandomForestClassifier(random_state=42))
])

# 그리드 서치로 최적 주성분 개수 탐색
param_grid = {'pca__n_components': [10, 20, 30, 50, None]}
grid_search = GridSearchCV(pipeline, param_grid, cv=5, scoring='f1')
grid_search.fit(X_train, y_train)

print(f"최적 주성분 개수: {grid_search.best_params_}")
print(f"테스트 F1 점수: {grid_search.score(X_test, y_test):.3f}")

드디어 김개발 씨의 최종 프로젝트입니다. 지난 몇 주간 배운 모든 것을 종합해서 고객 이탈 예측 모델을 다시 만들기로 했습니다.

먼저 데이터를 준비합니다. 위의 코드에서는 make_classification으로 시뮬레이션 데이터를 생성했지만, 실제 프로젝트에서는 pandas로 CSV 파일을 읽거나 데이터베이스에서 가져올 것입니다.

데이터를 학습용과 테스트용으로 분리합니다. 테스트 데이터는 20%를 떼어두었습니다.

이 데이터는 최종 성능 평가에만 사용합니다. 파이프라인을 구성합니다.

StandardScaler, PCA, RandomForestClassifier를 순서대로 연결했습니다. 이제 이 세 단계가 하나의 객체처럼 동작합니다.

여기서 핵심은 GridSearchCV입니다. 주성분 개수를 10, 20, 30, 50, 그리고 None(축소 없음) 중에서 어느 것이 최적인지 자동으로 탐색합니다.

파라미터 이름은 pca__n_components 형식으로, 파이프라인 단계 이름과 파라미터 이름을 언더스코어 두 개로 연결합니다. 5-fold 교차 검증을 사용하고 F1 점수를 기준으로 최적의 조합을 찾습니다.

fit 메서드가 완료되면 best_params_에서 최적 파라미터를 확인할 수 있습니다. 결과를 보니 주성분 30개가 최적이었습니다.

100개 특성을 모두 사용하는 것보다 30개만 사용하는 것이 오히려 성능이 좋거나 비슷했습니다. 학습 속도는 당연히 더 빨랐습니다.

마지막으로 테스트 데이터로 최종 성능을 평가합니다. grid_search 객체는 자동으로 최적 모델을 사용하므로 별도로 모델을 다시 학습시킬 필요가 없습니다.

김개발 씨는 이 모델을 팀에 발표했습니다. 차원 축소 전에는 학습에 10분이 걸렸지만, PCA를 적용한 후에는 3분으로 줄어들었습니다.

성능은 오히려 약간 향상되었습니다. 과적합이 줄어들었기 때문입니다.

박시니어 씨가 축하하며 말했습니다. "이제 PCA를 실무에서 제대로 활용할 수 있게 됐네요.

앞으로도 차원의 저주를 만나면 당황하지 말고 PCA를 먼저 시도해보세요."

실전 팁

💡 - GridSearchCV의 param_grid에서 파이프라인 파라미터는 단계이름__파라미터이름 형식을 사용합니다

  • None을 포함시키면 차원 축소를 하지 않는 경우와도 비교할 수 있습니다

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Python#PCA#DimensionalityReduction#MachineLearning#DataScience#Data Science

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.