MLOps 부트캠프 by 한경+토스뱅크/Machine Learning

차원 축소 - PCA(주성분분석)

나니니 2024. 8. 17. 22:01

PCA(Principal Component Analysis)

: 대표적인 차원 축소 기법 중 하나로, 여러 차원들의 특징을 가장 잘 설명해주는 차원인 주성분(Principal Component)을 이용하여 차원을 축소하는 방법

 

PCA의 동작 과정

1. 데이터 표준화 및 원점 이동

PCA를 하기 위해선 데이터의 단위를 통일해야 한다.

 

아래 예시를 보면 키를 표시한 단위는 m이고 신발의 단위는 mm인데, 이렇게 변수의 단위가 다르면 두 변수의 영향이 PCA 결과에 고르게 반영되지 않고 상대적으로 큰 값이 들어간 변수의 영향이 더 많이 반영되게 된다. 

따라서 PCA를 적용하기 전에는 변수 간 단위를 통일해주는 '표준화' 과정이 필요하다. 

 

데이터를 표준화하면 변수의 단위 뿐만 아니라 데이터의 중심 지점이 원점이 되도록 위치가 이동되는 효과도 있다. 

 

좌: 표준화 전 / 우: 표준화 후

2. 주성분 찾기

이제 두 개의 차원(키, 신발 사이즈)로 표현되는 데이터들을 가장 잘 설명해주는 주성분을 찾아야 한다. 

주성분은 데이터들의 중심(원점)을 지나면서 모든 데이터들에서의 수직 거리의 합이 가장 가깝도록 하는 선을 의미하는데, 아래 이미지와 같이 데이터들과 선의 거리의 합이 가장 작아지도록 한 주성분은 PC1으로 확인할 수 있다. 

 

n차원의 데이터에는 총 n개의 주성분이 존재할 수 있다. 그러므로 두 번째 주성분을 찾아볼 수 있다. 

두 번째 주성분은 첫 번째로 찾은 메인 주성분에 수직이면서 원점을 지나고, 데이터들과의 거리 합이 가장 작은 선을 찾으면 된다.

좌: 첫번째 주성분 / 우: 두번째 주성분


i) 주성분 찾는 방법

표준화를 통해 변수 간 단위를 맞추고 원점 이동까지 한 데이터가 아래와 같을 때, 주성분을 찾아보자.

주어진 데이터의 특징을 나타내면서 원점을 지나는 선은 많다.

그 중에서도 데이터의 특성을 가장 잘 나타내는 선은 only 하나이며, 이 선을 찾는 기준은 모든 데이터와 찾아진 선 사이의 거리 합이 가장 작아지도록 하는 것이다. 

아래 우측 이미지를 보면 좌측 이미지에 비해 각 데이터에서 그려진 축에 직교하는 거리의 합이 많이 작은 것을 알 수 있다. 이렇게 데이터와 성분 사이의 수직 거리가 더 가까울수록 해당 축이 데이터의 분포를 잘 반영하게 된다. 

좌: 데이터와 선 사이가 최소 거리가 아닐 때 / 우: 최소 거리 일 때

또, 데이터와 주성분 사이의 거리가 최소가 되도록 선을 찾으면 주성분으로 데이터들을 투영했을 때 해당 데이터가 퍼져있는 정도, 즉 '분산'이 커진다. 

피타고라스 정의를 생각해보면 데이터와 원점 사이의 거리인 C는 고정이기 때문에, 삼각형의 높이에 해당하는 B가 작아지면 선의 길이, 즉 분산인 A가 늘어나는 형태이기 때문이다.

정리해보자면 PCA에서 데이터의 특징을 가장 잘 설명하는 주성분을 찾는 것은 데이터들과 선 사이의 수직 거리 합이 최소가 되는 선을 찾으면 되는 것이고, 그것은 곧 임의의 선에 데이터를 투영했을 때 데이터들의 분산이 최대가 되는 선을 찾는 것과 같은 문제인 것이다. 

데이터들의 분산이 최대가 되도록 하는 선이 데이터에 대한 설명력이 가장 높은 선이 되는 것이며, 실제로 PCA연산에서는 투영하였을 때 데이터들 사이의 분산이 최대가 되도록 하는 선을 찾는 방식으로 주성분을 찾는다. 

 

다음으로,

두 번째 주성분은 첫 번째로 찾은 메인 주성분에 수직이면서 원점을 지나고, 데이터들과의 거리 합이 가장 작은 선을 찾으면 된다.

주성분끼리 수직으로 직교하게 하는 이유는 새롭게 찾아지는 두 축이 서로 상관이 없도록 하여 조금 더 많은 정보를 살린 상태로 차원을 축소하기 위함이다. 예를 들어 아래 우측 이미지와 같이 두 번째 주성분이 그려진다면 데이터에 대하여 첫 번째 주성분과 비슷한 정보를 더 많이 나타내게 된다. 

좌: 직교하는 선 / 우: 주성분끼리 비슷한 선

이는 차원이 높아지더라도 그대로 적용된다. 

예를 들어, 3차원 데이터를 PCA를 이용하여 차원 축소한다고 가정했을 때, 3차원 데이터의 특성을 가장 잘 반영하는 주성분 PC1, PC2, PC3를 위의 방법대로 찾으면 된다. 

또 1차원으로 차원을 줄이고 싶다면 PC1으로만 데이터를, 2차원으로 줄이고 싶다면 PC1, PC2까지 2개의 축으로 데이터를 투영하면 된다. 

3차원 -> 2차원으로 PCA

ii) 적절한 주성분 수 구하기

PCA를 적용할 때 몇 개의 주성분으로 차원을 축소시킬 지 정해야 하는데 이때 Scree plot이 사용된다. 

Scree plot은 각 주성분이 전체 데이터에 대해 갖는 설명력 비율을 시각화한 플롯으로, 전체 주성분의 분산 대비 특정 주성분의 분산의 비율을 시각화한 것이다.

pca = PCA(n_components = 6)
pca.fit(scaled_df)  # PCA 학습
scaled_df_pc = pca.transform(scaled_df)  # PC로 데이터 변환
pca_df = pd.DataFrame(scaled_df_pc)
pca_df.columns = ['PC1', 'PC2', 'PC3', 'PC4', 'PC5', 'PC6']

pca_df.head()

이제 6개의 PC가 각각 전체 데이터에 대해서 어느 정도의 설명력을 가지는지, 즉 전체 분산 대비 어느 정도의 분산 비율을 가지는 지 확인해보자. 각 주성분의 분산 비율을 계산하기 위해서는 sckit-learn의 PCA 에 있는 explained_variance_ration 속성을 활용하면 된다. 

import numpy as np

# PCA 개수
num_components = len(pca.explained_variance_ratio_)

x = np.arange(num_components)
var = pca.explained_variance_ratio_ 

plt.bar(x, var) # Bar plot 그리기

plt.xlabel('PC')
plt.ylabel('Variance_Ratio')
plt.title('Scree plot')

plt.show()

Scree plot

PC1은 전체 주성분이 설명해 주는 데이터의 특성 대비 약 40$ 정도를 설명하고, PC2는 약 20% 정도를 설명하고 있다. 

위 플롯의 결과를 바탕으로 최적의 주성분 개수를 구해보자. 

 

기준은 간단하다. 

PC1부터 PC_N까지 분산 비율의 합을 누적했을 때 전체 대비 70% 이상이 되는 PC_N을 고르면 된다. 이는 전체 분산 대비 분산 비율이 70%는 되어야 원본 데이터가 충분히 설명된다고 보기 때문이다.

cum_var = np.cumsum(var)   # 누적 분산비율 구하기
cum_vars = pd.DataFrame({'cum_vars': cum_var}, index = pca_df.columns)

cum_vars

누적 분산비율

위 결과를 보니 3개의 주성분(PC1, PC2, PC3)이 있을 때 전체 주성분이 데이터를 설명하는 정도 대비 약 80% 정도를 설명하는 것으로 나온다. 즉, 해당 겨웅에는 주성분을 3개로 차원을 축소하는게 적절하다고 할 수 있다. 

 

다만, 70%가 절대적인 기준은 아니다. 데이터의 복잡성과 분석 목적에 따라 충분히 달라질 수 있다. 

예를 들어 데이터가 상대적으로 단순하면 70% 이상의 누적 설명력을 충족하는 주성분 개수를 선택하는 것으로 충분하지만 데이터가 복잡하고 누적 설명력이 높아야 하는 경우에는 80%  이상, 혹은 더 높은 누적 설명력을 가지는 주성분 개수를 선택해야 할 수도 있다. 

또한 분석의 목적 상 차원이 높은 데이터를 시각화하여 데이터의 특징을 파악하는게 필요할 수 도 있는데, 이때는 Scree plot의 결과와 상관없이 차원을 2개 또는 3개로 축소시키기도 한다. 

 

예제

sklearn에 있는 wine 데이터셋입니다. 해당 데이터셋은 와인 품질을 예측하기 위한 와인의 화학적 특성 13개가 변수로 저장되어 있습니다.

- alcohol: 알코올 도수
- malic_acid: 사과산 농도
- ash: 회분
- alcalinity_of_ash: 회분의 알칼리도
- magnesium: 마그네슘 함량
- total_phenols: 총 페놀 함량
- flavanoids: 플라보노이드 함량
- nonflavanoid_phenols: 비 플라보노이드 페놀 함량
- proanthocyanins: 프로안토시아닌 함량
- color_intensity: 색상 강도
- hue: 색상
- od280/od315_of_diluted_wines: 희석 와인의 OD280/OD315 비율
- proline: 프롤린 함량

 

주성분의 개수에 따라 설명된 분산의 누적 비율을 확인해 보기 위해 n_components는 13으로 설정해 주세요.

%matplotlib inline
from sklearn.datasets import load_wine
from sklearn.decomposition import PCA
import seaborn as sns
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np

wine = load_wine()
df = pd.DataFrame(wine.data, columns=wine.feature_names)

# 스케일링
df_mean = df.mean()  # 각 컬럼의 평균값
df_std = df.std()  # 각 컬럼의 표준편차
scaled_df = (df - df_mean)/df_std  # 컬럼별 표준화 진행

# PCA 학습
pca = PCA(n_components = 13)
pca.fit(scaled_df)    

# PC로 데이터 변환
scaled_df_pc = pca.transform(scaled_df)
pca_df = pd.DataFrame(scaled_df_pc)

# PCA 개수
num_components = len(pca.explained_variance_ratio_)

# Scree plot 시각화: 주성분의 개수와 누적된 설명된 분산의 비율을 시각적으로 보여줌
	# 주성분 개수가 증가함에 따라 설명된 분산의 비율이 얼마나 증가하는지 확인 할 수 있음
    
x = np.arange(num_components)
var = pca.explained_variance_ratio_

plt.bar(x, var)  # Bar plot 그리기

plt.xlabel('PC')
plt.ylabel('Variance_Ratio')
plt.title('Scree plot')

plt.show()


3. 데이터 투영하기

 찾아진 주성분으로 데이터를 투영(Projection)한다. 투영이란, 데이터들을 주성분 위로 옮기는 과정을 의미하는데, 투영을 하면 아래 이미지와 같이 모든 데이터들이 주성분 위에 위치하게 된다. 

 

4. 새로운 축 기준으로 데이터 회전하기

마지막으로 첫 번째 주성분(PC1)과 두 번째 주성분(PC2)가 각각 x축과 y축이 될 수있도록 축과 데이터 전체를 회전시킨다. 

그러면 아래 이미지와 같이 데이터들의 분포도 함께 변하게 된다. 

 

그럼 아래 이미지와 같이 키와 신발 사이즈라는 2개의 변수는 사라지고 PC1과 PC2라는 새로운 변수로 데이터를 표현할 수 있게 된다. PC1과 PC2가 무엇인지는 잘 모르지만 해당 축이 데이터의 특성을 가장 잘 설명한다고 할 수 있는 축이 찾아졌다. 

 

이렇게 주성분이 찾아지면, 차원을 축소하는 것은 쉽다. 

만약 2차원인 데이터를 1차원으로 줄이고 싶다면 PC1만 사용해서 데이터를 나타내면 된다. 

만약 2차원으로 나타내려면 PC1과 PC2 두 개의 축으로 이루어진 좌표 평면에 데이터들을 아래와 같이 표현할 수 있다. 

 

 

예시

# scikit-learn에서 PCA를 import하면 PCA를 사용할 수 있다. 

from sklearn.decomposition import PCA

# 모델 생성
pca = PCA(n_components=2) # n_components파라미터 값 지정을 통해 차원을 몇 개로 축소할 것인지 나타냄

# 다음으로 fit() 메소드와 transfrom()메소드를 차례로 실행

pca.fit(scaled_df) # fit() 메소드는 데이터를 통해 PCA를 학습함 -> 이 과정에서 미리 설정한 n_components개의 주성분을 찾음
scaled_df_pca = pca.transform(scaled_df) # transform() 메소드는 학습된 PCA모델을 바탕으로 데이터를 변환함
pca_df = pd.DataFrame(scaled_df_pca) # 데이터프레임으로 변환

pca_df.columns = ['PC1', 'PC2'] # 데이터프레임 컬럼 이름 지정

pca_df.head()

# PCA 시각화
sns.scatterplot(data=pca_df, x='PC1', y='PC2')

좌: PCA 후 / 우: 좌측 데이터 시각화

예제

실습할 데이터는 sklearn에 있는 wine 데이터셋입니다. 해당 데이터셋은 와인 품질을 예측하기 위한 와인의 화학적 특성 13개가 변수로 저장되어 있습니다.

- alcohol: 알코올 도수
- malic_acid: 사과산 농도
- ash: 회분
- alcalinity_of_ash: 회분의 알칼리도
- magnesium: 마그네슘 함량
- total_phenols: 총 페놀 함량
- flavanoids: 플라보노이드 함량
- nonflavanoid_phenols: 비 플라보노이드 페놀 함량
- proanthocyanins: 프로안토시아닌 함량
- color_intensity: 색상 강도
- hue: 색상
- od280/od315_of_diluted_wines: 희석 와인의 OD280/OD315 비율
- proline: 프롤린 함량

주어진 데이터를 활용해 PCA를 진행해 주세요. PCA의 주성분은 2로 설정해 주시기 바랍니다.

%matplotlib inline
from sklearn.datasets import load_wine
from sklearn.decomposition import PCA
import seaborn as sns
import pandas as pd

wine = load_wine()
df = pd.DataFrame(wine.data, columns=wine.feature_names)

# 스케일링
df_mean = df.mean()  # 각 컬럼의 평균값
df_std = df.std()  # 각 컬럼의 표준편차
scaled_df = (df - df_mean)/df_std  # 컬럼별 표준화 진행

# PCA 적용
pca = PCA(n_components=2)  # 모델 생성

pca.fit(scaled_df)  # PCA 학습
wine_pca = pca.transform(scaled_df)  # PC로 데이터 변환
pca_df = pd.DataFrame(wine_pca)  # 데이터프레임으로 변환

# 시각화
sns.scatterplot(data=pca_df, x = 0, y = 1)

 

단점

PCA의 결과로 찾아진 주성분을 해석하기 어렵다. 

차원이 높은 데이터를 축소시킬 경우, 계산 비용이 급격히 증가되며 연산 시간이 오래 걸린다. 

 

따라서 PCA를 사용하기 전에는 데이터의 차원과 크기, 연산 환경을 고려하는 것이 필요하다.