brunch

매거진 Spark+Python

You can make anything
by writing

C.S.Lewis

by 보나벤투라 Dec 01. 2017

PCA in Machine Learning

Preprocess for Prediction&Classification

  머신러닝 기법 중, 비지도 학습의 일종으로서 PCA(Principal Component Analysis) 기법이 있습니다. 이번 포스트에서는, PCA 알고리즘을 이해하고, 직접 구현하여 sklearn library와 비교를 해보겠습니다. 물론 성능에 있어 최적의 조건을 보장하는 sklearn library가 더 우위에 있겠지만, 알고리즘을 완벽히 이해하고 사용하는데 이번 포스트의 목적을 두겠습니다.

  PCA의 알고리즘을 작성하기 위해 'Eigen Decomposition(고유분해)'와 'Spectrum theory(스펙트럼 정리)'를 이해하는 과정은 필수적이기 때문에, 정리된 개념을 꼭 확인하시기 바랍니다. 파일도 함께 첨부하겠습니다.


  이번 포스트는 크게, 다음과 같이 진행됩니다.

1) Test data로, k-means에 의해 segmentation된 group_cluster.csv를 사용

2) 직접 구현한 PCA 알고리즘과 sklearn의 PCA

3) 두 방법으로 Test data를 적합시켜, 도출된 PCA 결과비교

4) 설명된 분산비율에 따라, 주성분의 개수 k 정하기

5)  PCA 결과해석(=important feature extraction)


PCA 알고리즘


1) 먼저 데이터를 이루는 벡터 간 scale을 맞추기 위해, 표준화를 시도한다.

2) 표준화된 데이터의 '공분산 행렬'을 구한다.

3) 공분산 행렬의 고유분해를 통해, 고유벡터행렬과 고유값을 구한다.

4) '고유값의 크기 순'(=자료의 변화량이 가장 큰 순)대로 고유값과 고유벡터를 정렬한다.

5) '데이터를 설명할 수 있는 충분한 변화량'을 갖는 주성분의 개수 k를 구한다.

6) k개의 고유벡터만 유지하고, 나머지 작은 고유 벡터는 제거한다.

7) 표준화된 데이터에 k개의 고유벡터(=고유벡터행렬)를 곱하여 고유공간으로 projection 시킨다.

  PCA는 원래 데이터의 차원을 축소하면서도, 그 데이터의 특징을 최대한 손실 없이 살리는 기법입니다. 따라서, 고차원의 데이터를 다루는데 '차원의 저주'를 피해갈 수 있는 매우 유용한 기법입니다. 이러한 특징 때문에, KNN 머신러닝의 전처리 단계로 쓰이기도 합니다. 단점이 있다면, PCA를 통해 반환된 각각의 주성분을 해석하는데 어려움이 있습니다. 그러나 전문 도메인 지식이 함께 동반된다면 이 부분도 충분히 해결될 수 있음을 말씀드립니다.


def pca(input,ReduceDim=0):

     #rescaling feature vectors to have the same scale

     from sklearn.preprocessing import StandardScaler #(x_i-x_bar)/sigma

     data=StandardScaler().fit_transform(input)

     #공분산 행렬

     C=np.cov(data.T)

     #고유벡터와 값 계산(고유분해)

     evals,evecs=np.linalg.eig(C)

     #큰 고유값 순서대로, 고유값과 고유벡터 정렬

     indices=np.argsort(evals)[::-1]

     evecs=evecs[:,indices]

     evals=evals[indices]

     #정렬된 고유벡터를 축소시킬 차원만큼만 유지

     #'방향성'만 갖도록 정규화(길이1)시켜 크기를 제거

     #사실, np.linalg.eig()은 이미 정규화된 고유벡터를 반환

     if ReduceDim>0:

         evecs=evecs[:,:ReduceDim]

        #for i in range(np.shape(evecs)[1]):

            #evecs[:,i]=evecs[:,i]/np.linalg.norm(evecs[:,i])

     #주성분의 변화량이 전체 변화량을 설명하는 정도

     Explained_var_ratio=(sum(evals[:ReduceDim])/sum(evals))     

     #표준화된 데이터에 고유벡터행렬을 곱해 고유공간으로 회전변환(projection)

     x=np.dot(data,evecs)

     #위 변환된 데이터에 역변환 행렬(고유벡터행렬.T)을 곱하여 원래의 데이터로 근사

     y=np.dot(x,evecs.T)

     return np.array([data,x,y,evals,evecs.T,Explained_var_ratio])

위 알고리즘의 순서와 주석을 잘 참고해주시길 바랍니다.

  위 정리된 논리에 따라 작성된 알고리즘과, sklearn library에서 제공하는 PCA의 결과를 비교해 보겠습니다. 실습 데이터로 이전 포스트에서 만들어진 group_cluster를 사용하고, 주성분의 개수는 2개라 가정하겠습니다. 


>> group=pd.read_csv("group_cluster.csv",header=0)

>> labels=pd.Series(np.array(group["Closet_Cluster"]),dtype="category")

>> labels=labels.cat.rename_categories(["loyalist","big_spender","new_potentials"])

>> group=group[["mean","var","size"]]

>> print(group.head(3))

>> print(labels[:3])

>> from sklearn.preprocessing import StandardScaler

>> std_group=StandardScaler().fit_transform(group) 

>> std_group[:3] #standardized data

mean, var, size의 scale을 맞춰주기 위해 표준화를 시켰습니다.

  직접 작성한 알고리즘과 sklearn에서 제공하는 PCA 알고리즘이 동일한 결과를 반환했습니다.

1) 2개의 고유벡터(2개의 PC)

2) 고유공간으로 projection된 원데이터

3) 설명된 분산비율

  위에서 결과를 확인하기 위해 주성분의 개수를 2개로 가정했지만 사실 PCA를 시도하기 전, '몇 개의 주성분을 유지할 것인가?"라는 부분을 알아야 합니다. 

  주성분의 분산은 고유값과 일치합니다. 따라서 유지할 주성분의 개수 k를 정하는 것은, 'k개의 고유값이 전체 고유값의 합 중 차지하는 비율',  k개의 주성분이 설명하는 분산비율에 따라 결정됩니다. 다음으로 설명된 분산비율을 각 주성분별로, 그리고 누적해서 살펴보겠습니다.


def explained_var_ratio(eigen_values):

     x_axis=range(1,len(eigen_values)+1)

     total_evals=sum(eigen_values)

     y_axis=[(eigen_values[c])/total_evals for c in range(len(eigen_values))]

     plt.plot(x_axis,y_axis,marker="o",color="c")

     plt.xticks(x_axis)

     plt.ylim(0,1)

     plt.xlabel("Eigein_Values")

     plt.ylabel("Explained_var_ratio")

     plt.title("Explained_var_ratio as #PC")

     plt.show()

def cumulative_explained_var_ratio(eigen_values):

     x_axis=range(1,len(eigen_values)+1)

     total_evals=sum(eigen_values)

     y_axis=[(sum(eigen_values[:c])/total_evals) for c in range(1,len(eigen_values)+1)]

     plt.plot(x_axis,y_axis,marker="o",color="c")

     plt.xticks(x_axis)

     plt.xlabel("Eigein_Values")

     plt.ylabel("Explained_var_ratio")

     plt.title("Explained_var_ratio as #PC")

     plt.show()

>> explained_var_ratio(EVals) #plotting

>> cumulative_explained_var_ratio(EVals) #Finally, we can select 2 principal components

  2개의 주성분만으로도, 80%의 정보량을 설명할 수 있기 때문에 3차원의 데이터를 2차원으로 축소시키겠습니다. 물론 PCA는 더 높은 고차원에서 차원을 축소할 때, 힘을 발휘합니다. 이렇게 구성된 2개의 주성분을 가지고 해석을 해보겠습니다.


> pd.DataFrame(EVecs,columns=[["mean","var","size"]],index=[["PC1","PC2"]])

  EigenVector를 열 기준으로 보았을 때, 각각의 row가 원 데이터의 벡터에 해당됩니다. 그리고 각각의 주성분에 해당되는 벡터의 절대값을 기준으로 해석을 시도합니다.

  위 주성분 df를 보면, 첫 번째 주성분은 mean와 큰 음(-)의 연관성, var와 큰 양(+)의 연관성이 있으므로 관람객의 영화 호불호 정도(평점 기반)를 측정합니다. 두 번째 주성분은 var(-)에 비해 size와 매우 큰 양(+)의 연관성이 있으므로, 영화에 대한 꾸준한 관심도 또는 라이프스타일을 측정합니다.

  좀 더 자세히, 중요한 특징을 추출해보겠습니다.


def find_important_features(P_F, P_Components, columns):

    #P_F is original data Projected on eigen space

    #we will find which important features have the most powerful effect

    import math

    n_columns = len(columns)

    ix_max1=np.argmax(np.abs(P_F[:,0]))

    ix_max2=np.argmax(np.abs(P_F[:,1]))

    # Scale the p_components by the max abs_transformed_feature

    scaled_pc1 = P_Components[0] * P_F[ix_max1,0]

    scaled_pc2 = P_Components[1] * P_F[ix_max2,1]

    # Sort each column by it's norm.

    # These are exactly not original data, but approximate it

    # Because those dimension are reduced.

    # Nevertheless, those contain important information of original dataset

    important_features = { columns[i] : math.sqrt(scaled_pc1[i]**2 + scaled_pc2[i]**2) for i in range(n_columns) }

    important_features = sorted(zip(important_features.keys(),important_features.values()), reverse=True)

    return (scaled_pc1,scaled_pc2, important_features)


>> columns=group.columns

>> feature_extraction=find_important_features(P_F, EVecs,columns)

>> print('scaled_important_pc1 by max(P_F_col1)\n%s' %feature_extraction[0])

>> print('scaled_important_pc2 by max(P_F_col2)\n%s' %feature_extraction[1]) 

>> print('feature magnitude by [mean,var,size] \n%s' %feature_extraction[2])

   

1)  -3.69574616(mean), 3.48664526(var)은 원래의 데이터로 근사한, 첫 번째 주성분에서 중요한 특징

2) 5.55634708(size)는 원래의 데이터로 근사한 두 번째 주성분에서 중요한 특징입니다. 

3) mean, var, size의 변수별 특징 중요도를 pca plotting에서 살펴보았을 때 그것은 좌표축 (0,0)을 기준으로 (scaled_pc1,scaled_pc2)로 그은 직선의 길이입니다. 이를 기반으로 의미있는 특징 추출을 시도할 수 있습니다. 결과적으로, 이전 포스트에서 주목해왔던 'size'를 가장 중요한 특징으로 생각할 수 있습니다. 

  아마도 숫자로만 확인하기보다, plotting을 통해 시각화를 시키는 것이 해석을 하는데 매우 효과적인 방법일 수 있습니다. 

>> labels=np.array(labels)[:,np.newaxis] #cluster_label

>> PF_df=pd.DataFrame(np.hstack([P_F,labels]),columns=[['PC1','PC2','labels']])

>> PF_df.head()


>> import seaborn as sns

>> sns.lmplot("PC1","PC2",data=PF_df,hue="labels",fit_reg=False,legend_out=False,markers=["^","v","o"],palette="Set1")

>> plt.legend(loc="upper right")

for i in range(len(columns)):

     plt.arrow(0,0,feature_extraction[0][i],feature_extraction[1][i],color='brown',alpha=0.75)

     plt.text(feature_extraction[0][i],feature_extraction[1][i],columns[i],color='brown',alpha=0.75) 

>> plt.show()

  주성분 df와 important feature extraction과정을 통해 예견된 바, plotting의 결과와 일치합니다. 즉, 첫 번째 주성분은 mean와 큰 음(-), var와 큰 양(+)의 연관성이 있으므로 관람객의 영화 호불호(평점 기반)를 측정합니다. 두 번째 주성분은 size와 매우 큰 양(+)의 연관성이 있으므로, 영화에 대한 꾸준한 관심도 또는 라이프스타일을 측정합니다.

  즉, '영화에 대한 꾸준한 관심이 있는, 라이프스타일의 관람객'들은 loyalist로서 평점 부여의 많은 활동(size 高)을 해왔다고 할 수 있습니다. 하지만, 그에 비해 영화를 가끔 보는 라이프스타일의 관람객은 평점 자체가 본인의 만족도에 매우 의존하는 경향이 있습니다.

  또한 차원을 축소하였음에도 불구하고, 이전 3차원의 k-means(k=3)를 시각화한 결과와 다르지 않을 만큼 매우 유사한 형태로 데이터의 분포를 보여주고 있습니다. 즉, 저차원에서도 고객집단별로 데이터들이 구분되어 분포하고 있습니다.

  이러한 PCA의 장점은 '차원의 저주'를 피해가는데 강력한 장점을 보여줍니다. KNN은 차원의 저주에 의해 성능이 쉽게 저하되는데, PCA를 통해 전처리 후 KNN을 시도하면 성능이 훨씬 좋아질 수 있습니다.

  이번 포스트는 이것으로 마치겠습니다.



브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari