공분산 행렬과 주성분 분석(PCA)
- 공분산 행렬은 말 그대로 공분산에 대한 행렬을 뜻합니다.
다차원 multy feature 데이터에 존재하는 각 2개씩의 특징 사이에서 생겨나는 공분산을 구하기 위한 행렬 연산을 뜻하는데, 당연히 이를 이해하려면 공분산에 대해 알아야겠죠?
요약해보자면, 공분산은 특징 2개의 축으로 이루어진 2차원 데이터 분포의 특성을 파악하기 위한 방법으로,
COV(x,y) = E{sum(X-E(x))*(Y-E(y))}
로 계산이 가능합니다.
기대값, 즉 여기선 그냥 평균값이라 생각하면 되는데,
현재 데이터 분포의 평균을 0으로 정렬하기 위해서 이를 각 원소에 빼주고, 이 값을 곱해주어 더해줍니다.
그리고 그 값에 대한 평균을 구하는 방식으로 해당 데이터 분포의 성질을 알아내는 것으로,
이 값이 양수이면 두 특징은 양의 상관관계를 가지고, 음수이면 두 특징이 음의 상관관계를 가지며, 이 값이 0에 가까우면 상관관계가 뚜렷하지 않고, 크기가 커질수록 상관관계가 뚜렷하다고 판단이 가능해집니다.
위 링크에서 조금 더 알아보시고,
- 공분산 행렬을 구하려면,
먼저 데이터가 준비되어 있어야겠죠?
x, y로만 이루어진 데이터가 아니라,
x1, x2, x3, x4 등으로 이루어진 multy feature 데이터입니다.
각 특징이 서로에 대해 공분산을 구하고 이를 행렬로 나타내는 것인데,
쉽게 계산을 하려면,
X = x1, x2, x3, x4
X` = X - E(X)
이로써 각 특징에 대해서 각각의 기대값을 빼준 결과가 X`에 담기겠죠?
그러면 이를 각 특징에 곱해주기 위해
X` * X`.T
를 해줍니다.
.T는 전치행렬입니다.
X`와 그 전치행렬을 곱함으로써
(dot(x1,x1), dot(x1, x2), dot(x1, x3), dot(x1, x4)
dot(x2,x1), dot(x2, x2), dot(x2, x3), dot(x2, x4)
dot(x3,x1), dot(x3, x2), dot(x3, x3), dot(x3, x4)
dot(x4,x1), dot(x4, x2), dot(x4, x3), dot(x4, x4))
이러한 행렬을 구할수 있습니다.
각각을 해석해보자면, dot(x1,x1)은 x1과 x1의 공분산, 즉 분산이며 dot(x1, x2)는 x1과 x2의 공분산... 이런식으로 해석이 가능하죠.
그런데 그냥 dot product만 하면 안되겠죠?
공분산이니 당연히 각각에 원소 개수인 n을 나누어야합니다.
cov(X`) = (X` * X`.T) / n
공분산만 이해하면 매우 쉬운 내용입니다.
각 특징이 서로에게 미치는 영향을 공분산으로 얻어오고 그것을 행렬로 계산하고 나타낸 것입니다.
아, 참고로 dot(x1,x2)와 dot(x2,x1)은 서로 결과가 같으므로 중앙 대각선(분산)을 기준으로 양옆은 서로 거울상입니다.
또한 결국 공분산을 행렬로 만든 것 뿐이므로 공분산 행렬은 공분산과 같은 약점을 공유합니다.
그리고 통계쪽에 보시면 알수 있듯이, 표본 분산은 n이 아니라 n-1로 나누기에,
표본 데이터에 있어서는
cov(X`) = (X` * X`.T) / n-1
을 해주는 것이 정석이지만 큰 상관은 없습니다.
- PCA(Principal Component Analysis)란,
우리말로 주성분 분석이며, 차원 축소와 연관이 높습니다.
이전에 딥러닝 모델중에서 Auto Encoder를 다룬적이 있는데,
딥러닝으로 구현하기는 쉬운 이 모델 내에서 일어나는 것이 바로 많은 특징에 대해 주요한 특징들을 추려내서 인코딩하는 차원축소의 내용이 있었습니다.
PCA는 이에 대한 내용으로,
데이터 구조를 잘 살리면서 차원 축소를 할수 있게끔 해주는 방법을 의미합니다.
차원을 축소하는 것 뿐 아니라 얼마나 차원을 축소해야 정보 손실이 안 일어나는지에 대해서도 파악이 가능한 방법이라, 딥러닝의 차원 축소를 이해하는 데에도 유용할 것 같습니다.
- 차원 축소의 예시
angeloyeo.github.io/2019/07/27/PCA.html의 유튜브 강의에서 나온 예시를 사용하자면,
어떠한 성적 데이터로 '국어' '영어' '수학' '과학'이 있을 때,
'국어' '영어'를 '문과 성적' '수학' '과학'을 '이과 성적'으로 축약이 가능합니다.
물론 그러기 위해서는 국어와 영어라는 축을 지닌 2차원 공간에서 행렬이 지니는 형태와 의미를 파악해야하고,
그것을 가장 잘 표현하는 1차원 벡터인 Principal Axis를 구해내야 합니다.
이게 무슨 의미냐면, 자, 생각해봅시다.
둘이 상관관계가 있는 데이터 둘 존재합니다.
하나가 커지면 하나가 무조건 커지는 상관관계죠.
그러면 우리가 데이터를 구해올때 둘 모두를 구해오고 고려하는게 나을까요?
아니면 그냥 둘중 하나를 가져오는게 나을까요?
당연히 하나를 알면 다른 하나는 자동으로 알게되는 것이라 구해오지 않아도 됩니다.
차원 축소는 이러한 것입니다.
둘 사이에 상관관계가 존재하면 다른 데이터는 보지 않아도 뻔하다...
이를 2차원 평면 상의 산점도를 1차원에 정사영 시킨다고 표현을 하는데,
이와 같습니다.
2차원 평면상의 행렬 데이터는 저 산점도의 각 좌표 데이터를 의미하며,
((x1, y1), (x1, y2), ..., (xn, yn))
이런식의 데이터이며,
이를 대표하는 주요축은,
(a, b)
의 형태로 위와 같이 선으로 나와있는 것입니다.
빨간 선은 (5, 5)이며,
파란 선은 (6, 4)의 비율을 지니네요.
과연 어떤 1차원 데이터가 저 데이터를 대표하는 값인 걸까요?
- 저러한 주요축의 선을 구하는데에 있어서 잘 구해졌는지를 알려주는 척도는 바로 Variance, 분산입니다.
해당 선을 기준으로 각 데이터가 얼마나 퍼져있는지를 나타내는 지표인데,
데이터를 정사영하다...
즉 어떠한 기준 축 위에 해당 데이터를 올려놓았을 때에 분산값이 최소가 된다는 것은 해당 축이 데이터의 방향성과 형태를 전혀 고려하지 않고 겹쳐있다는 것을 의미하며,
분산값이 최대가 된다는 것은 해당 축이 데이터를 잘 표현해서 겹치는 부분이 적다는 것을 의미하므로 정사영 후의 분산이 큰 것이 좋습니다.
예를들어 위와 같은 데이터와, 예측한 주 축 4개가 있을 때에, 데이터가 해당 축 방향으로 떨어져내린다 생각해보세요.
축이 데이터의 방향에 맞게 설정되어 있다면 균등하게 떨어질 것이고, 엉뚱하게 설정되어 있다면 축에 떨어질때 겹치는 부분이 많을 것입니다.
- 위와 같은 이론에 따라서 좋은 주축을 구하기 위해서는 공분산 행렬을 통해 선형변환을 할때의 주축에 대해 정사영 하는 것이 좋다고 합니다.
무슨 의미냐면,
공분산은 데이터의 형태를 수치적으로 나타내는 값이며,
0을 중심으로 데이터가 어느 방향성을 띄고 퍼짐 정도를 갖는지를 나타낸다고 했습니다.
공분산 행렬이 있으면 각 특징들에 대한 형태를 알수 있습니다.
그리고 고유벡터라는 것은 행렬의 방향성을 유지하는 선형변환의 주축을 의미하는데(선형변환 이후 크기만 바뀌고 방향은 바뀌지 않는 벡터),
즉 이 두가지 개념을 합쳐서 공분산 행렬의 고유벡터를 구하면 해당 데이터를 대표하는 주축을 발견하는게 됩니다.
쉽게 말하자면 공분산 행렬의 고유벡터가 바로 주축이 된다는 뜻입니다.
- 수식을 정리하자면,
cov(X`) = (X` * X`.T) / n
로 공분산 행렬을 구하고,
거기서 나온 행렬 A를 가지고,
고유벡터 x를 구하는 것입니다.
Ax=λx
수식은 위와 같으며, Ax가 x에 고유값 λ를 곱한 것과 같으니,
이를 이항해서
(A−λI)x=0
가 되도록 하고,
이중 고유벡터 x가 0이 아닌 상황을 만들기 위하여,
det(A−λI)=0
를 해줘야 하며, 행렬식 det에 대한 정의인 아래 식에 따라서,
이를 만족하도록 예를들면
detA=a11detC11−a12detC12+a13detC13
와 같은 방식으로 방정식을 풀어나가며,
여기서 나온 연립방정식을 풀어 λ를 구한 후,
그 λ를 Ax=λx에 대입하여 이를 만족시키는 고유벡터 x를 구하면 되죠.
고유벡터는 두 축에 대해 2개가 나옵니다.
그 중 고유값이 큰 것... 사이즈가 큰 것... 즉 분산이 큰 값을 사용하면 그게 바로 주성분이 됩니다.
해당 축이 2차원 데이터를 축약한 데이터가 되는 것이죠.
이것이 가장 이상적인 주축이 되는데, 이 선을 정사영해서 분산값이 많이 나올수록 우리가 축약한 두 축은 서로 연관이 많다는 것을 의미합니다. (물론 공분산의 원리상 이상치 등에 취약한 평균값을 사용하기에 절대적이지는 않습니다.)
- PCA의 좋은점은 2차원만이 아니라 다차원 데이터에서 더 효과가 나옵니다.
예를들어 3차원 데이터만 하더라도, x1과 x2에는 뚜렷한 상관관계가 존재할때, x2와 x3에는 전혀 아무런 상관관계가 존재하지 않을수도 있습니다.
이 경우에 위와 같이 주축을 구해서 이를 정사영 시켰을 때에 분산값이 크게 나오는 부분에 대해서는 뚜렷한 상관관계가 존재함으로 해당 주축을 통해 축약을 시킬수가 있다는 뜻이고, 서로 상관관계가 적어서 분산이 작게 나온다면 축약시키지 않으면 되는 것입니다.
- 아래 PCA코드는 github.com/minsuk-heo/python_tutorial/blob/master/data_science/pca/PCA.ipynb이 출처입니다.
1. 준비
import pandas as pd
# Eating, exercise habbit and their body shape
df = pd.DataFrame(columns=['calory', 'breakfast', 'lunch', 'dinner', 'exercise', 'body_shape'])
df.loc[0] = [1200, 1, 0, 0, 2, 'Skinny']
df.loc[1] = [2800, 1, 1, 1, 1, 'Normal']
df.loc[2] = [3500, 2, 2, 1, 0, 'Fat']
df.loc[3] = [1400, 0, 1, 0, 3, 'Skinny']
df.loc[4] = [5000, 2, 2, 2, 0, 'Fat']
df.loc[5] = [1300, 0, 0, 1, 2, 'Skinny']
df.loc[6] = [3000, 1, 0, 1, 1, 'Normal']
df.loc[7] = [4000, 2, 2, 2, 0, 'Fat']
df.loc[8] = [2600, 0, 2, 0, 0, 'Normal']
df.loc[9] = [3000, 1, 2, 1, 1, 'Fat']
간단하게 판다스의 데이터 프레임 객체에 행렬 데이터를 준비합니다.
저렇게 준비한 데이터를 df.head(10)로 파악하면,
이러한 구조가 나오는 것을 볼수 있습니다.
calory나 breakfast와 같은 컬럼들이 바로 우리가 줄여줄 특징차원입니다.
마지막 body_shape는 앞의 특징들에 대한 분류 결과인데,
앞의 특징들의 조합에 따라 이 사람이 말랐는지 뚱뚱한지를 예측 가능하죠.
# X is feature vectors
X = df[['calory', 'breakfast', 'lunch', 'dinner', 'exercise']]
이렇게 특징만 추려내서 X에 담고,
Y = df[['body_shape']]
정답을 추려내 Y에 넣습니다.
2. 리스케일링
먼저 해야할 것은 각 특징의 스케일을 맞추는 것입니다.
어떤 특징은 만 단위인데, 어떤건 소수점 단위라면 연산이나 시각화에 불리하기에 이 스케일을 비율을 맞춰서 동일한 스케일로 맞춰주는 것입니다.
from sklearn.preprocessing import StandardScaler
x_std = StandardScaler().fit_transform(X)
대표적인 머신러닝 라이브러리인 사이킷런을 사용할 것입니다.
preprocessing 모듈의 StandardScaler 객체를 만들어 그 안의 메소드를 사용해 X를 넣어주면 Standardization된 데이터가 나옵니다.
이를 확인하면 위와 같이 데이터 정규화가 일어납니다.
3. 공분산 행렬 구하기
import numpy as np
# features are columns from x_std
features = x_std.T
covariance_matrix = np.cov(features)
print(covariance_matrix)
넘파이에는 공분산을 쉽게 구해주는 메소드가 있으니 그것을 사용하면 위와 같이 매우 쉽게 구할수 있습니다.
4. 공분산 행렬의 고유벡터, 고유값 구하기
eig_vals, eig_vecs = np.linalg.eig(covariance_matrix)
print('Eigenvectors \n%s' %eig_vecs)
print('\nEigenvalues \n%s' %eig_vals)
넘파이에는 또한 선형대수의 고유값, 고유벡터를 구하는 eig 메소드가 있습니다.
공분산 행렬을 이에 넣어주면 이 역시 매우 쉽게 구할수 있고, 결과는 위와 같습니다.
# We reduce dimension to 1 dimension, since 1 eigenvector has 73% (enough) variances
eig_vals[0] / sum(eig_vals)
# 0.73183217314275439
5. 고유벡터에 데이터 정사영시키기
projected_X = x_std.dot(eig_vecs.T[0])
array([-2.22600943, -0.0181432 , 1.76296611, -2.73542407, 3.02711544,
-2.14702579, -0.37142473, 2.59239883, -0.39347815, 0.50902498])
result = pd.DataFrame(projected_X, columns=['PC1'])
result['y-axis'] = 0.0
result['label'] = Y
6. 시각화
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline
sns.lmplot('PC1', 'y-axis', data=result, fit_reg=False, # x-axis, y-axis, data, no line
scatter_kws={"s": 50}, # marker size
hue="label") # color
# title
plt.title('PCA result')
결과를 보시면 의미 없는 y축은 제외하고, 일종의 1차원 벡터화 된 PC1이라는 축약된 하나의 특징 위로 데이터들이 잘 분류되는 것을 볼수 있습니다.
차원 축을 줄이니 이렇게 시각화 해서 파악하기도 쉽군요.
- 다음으론,
위의 코드를 사용할 것도 없이 사이킷런에서 PCA를 한번에 해주는 api도 존재하는데,
from sklearn import decomposition
pca = decomposition.PCA(n_components=1)
sklearn_pca_x = pca.fit_transform(x_std)
decomposition의 PCA를 하고 n값을 1로 하면 1차원으로 차원을 줄여줍니다.
sklearn_result = pd.DataFrame(sklearn_pca_x, columns=['PC1'])
sklearn_result['y-axis'] = 0.0
sklearn_result['label'] = Y
sns.lmplot('PC1', 'y-axis', data=sklearn_result, fit_reg=False, # x-axis, y-axis, data, no line
scatter_kws={"s": 50}, # marker size
hue="label") # color
결과는 위와 동일합니다.
차원 n값을 달리줘가면서 고유값 분산을 확인하면 어디까지 축소하는 것이 좋은지를 알수 있습니다.
- 이상입니다.