선형대수학
[R] PCA(주성분분석) 모티베이션,증명,실습,해석방법
JSMATH
2025. 4. 7. 03:37
PCA : Principal Component Analysis
머신러닝,통계적 학습,딥러닝에서까지 자주 등장하는 기법이다.
다들 파이썬으로 sklearn.decomposition.PCA() 이런 함수로 그냥 구하고는 한다. 자매품으로 R에서는 prcomp()라는 함수가 있다. 물론, 이 방식이 틀린 방식이라고 말하는게 절대 아니다.
말의 요지는 데이터 전처리를 할 때 그냥 위와 같이하면 편하다.
솔직히 이 글을 쓰고 있는 사람도 prcomp()으로 그냥 쓴다.
하지만 알고는 써야하지 않을까??
다행스럽게도, 그렇게 깊은 선형대수적 지식을 요구하지 않는다. 이참에 확실히 배워두고 가면 좋을 것 같다.
흔히 분산을 최대화하는 문제로 잘 알려져있는 PCA를 A~Z까지 설명 할 것이고 이를 통한 실습코드도 준비해두었다.
다음은 이 글을 읽는데 필요한 선수지식이다.
1. 선형대수의 벡터 정사영
2. 최적근사정리 (정사영 벡터를 이용한 오차벡터가 가장 작다는 것)
2. 벡터 미분
3. 라그랑주 승수법
여기서 중요한 가정은 이미 센터링(평균=0)이라는 점이다.
이론적 증명은 이정도로 마친다.
다음은 R코드로 이론적으로 전개되는 코드와 ,prcomp()함수를 쓰는 딸깍(?) 코드를 소개하겠다.
#install.package(palmerpenguins)
library(palmerpenguins)
data("penguins")
# 사용할 변수: 부리 길이, 부리 깊이, 지느러미 길이, 몸무게
penguins_numeric <- penguins[, c("bill_length_mm", "bill_depth_mm", "flipper_length_mm", "body_mass_g")]
# 결측치 제거
penguins_numeric <- na.omit(penguins_numeric)
# 데이터 중심화 및 스케일링 (평균 0, 표준편차 1)
X <- scale(penguins_numeric, center = TRUE, scale = TRUE)
# 공분산 행렬 계산
cov_mat <- cov(X) #smallsum = ({1}over{n})*(t(X)%*%X) R에서 cov(X)는 표본공분산이라 (n-1)을 나눔.
cat("공분산 행렬:\n")
print(cov_mat)
# 고유값분해 수행 (대칭행렬이므로 eigen() 사용 가능)
eigen_decomp <- eigen(cov_mat) #smallsum(cov(X))의 고유값 lambda를 이용.
eigen_values <- eigen_decomp$values
eigen_vectors <- eigen_decomp$vectors
cat("\n고유값:", eigen_values)
cat("\n고유벡터 (각 열이 한 고유벡터):")
print(matrix(c(eigen_vectors),byrow=F,ncol=4))
# 주성분 점수(PC scores) 계산: 각 샘플을 고유벡터(주성분) 축으로 투영
PC_scores <- X %*% eigen_vectors
X %>% head()
PC_scores %>% head()
# 각 주성분이 설명하는 분산 비율 계산
explained_variance <- eigen_values / sum(eigen_values)
cat("\n각 주성분의 분산 설명 비율:\n",explained_variance)
# PC1과 PC2를 이용한 산점도
plot(PC_scores[,1], PC_scores[,2],
main = "PCA on Palmer Penguins Data",
xlab = paste0("PC1 (", round(explained_variance[1]*100, 1), "%)"),
ylab = paste0("PC2 (", round(explained_variance[2]*100, 1), "%)"),
pch = 19, col = "steelblue")
library(palmerpenguins)
library(magrittr)
data("penguins")
penguins_num <- penguins[,3:6]
penguins_num %<>% na.omit()
# PCA : prcomp() 함수 이용, 변수 스케일링 포함(scale. = TRUE)
pca_result <- prcomp(penguins_num, scale. = TRUE)
pca_result %>% summary()
# 주성분 점수를 이용하여 1,2번째 주성분 산점도 그리기
plot(pca_result$x[, 1:2],
main = "prcomp() : PCA of Palmer Penguins Data ",
xlab = "PC1",
ylab = "PC2",
pch = 19, col = "steelblue")
*분산비율 해석
> pca_result %>% summary()
Importance of components:
PC1 PC2 PC3 PC4
Standard deviation 1.6594 0.8789 0.60435 0.32938
Proportion of Variance 0.6884 0.1931 0.09131 0.02712
Cumulative Proportion 0.6884 0.8816 0.97288 1.00000
#위에서의 proportion of variance와 아래를 비교해보자.
> cat("\n각 주성분의 분산 설명 비율:\n",explained_variance)
각 주성분의 분산 설명 비율:
0.6884388 0.1931292 0.09130898 0.02712305
해석 : PC1만으로 전체데이터의 약 68% 설명. PC2만으로는 약 19%설명이 가능하다.
*그럼 무엇을 가지고 PCA를 써야 할 지 생각을 해야 할까?
library(corrplot)
corrplot(cov_mat, method = "color", is.corr = FALSE)
부리(bill) 길이와 몸 질량, 펭귄지느러미(flipper) 길이와 몸 질량,.. 이런식으로 강한 상관관계가 나타난다. 이들을 묶어서 차원을 줄이려고 할 때 쓴다.
제일 중요한 점은 PC1,PC2 무슨 스코어를 매겼는데, 이들이 의미하는 바가 무엇인지 아직 밝히지 않았다.
*PC1,PC2 의미와 투영벡터(고유벡터)의 의미
PC1 : 분산이 가장 큰 고유벡터를 이용 -> Xw1 (w1은 람다값 중 최댓값에 대응하는 고유벡터)
eigen_vectors[,1]은 람다값 중 최댓값에 대응하는 고유벡터 즉, w1에 해당한다. 이를 기여도(loading)이라고 표현한다.
X의 컬럼을 c1,c2,c3,c4로 할 때 w11*c1+w12*c2+w13*c3+w14*c4 이런식으로 열공간으로 나타나기 때문이라고 생각한다.
w11*(bill_length) + w12*(bill_depth) + w13*(flipper_length) +w14*(body_mass) 흔히 선형회귀의 beta값들과 같은 역할을 하는 가중치느낌이라고 본다.
eigen_vectors
[,1] [,2] [,3] [,4]
[1,] 0.4552503 -0.597031143 0.6443012 0.1455231 ← bill_length_mm
[2,] -0.4003347 -0.797766572 -0.4184272 -0.1679860 ← bill_depth_mm
[3,] 0.5760133 -0.002282201 -0.2320840 -0.7837987 ← flipper_length_mm
[4,] 0.5483502 -0.084362920 -0.5966001 0.5798821 ← body_mass_g
> eigen_vectors[,1]
[1] 0.4552503 -0.4003347 0.5760133 0.5483502
이를 표로 한번 나타내보자.
변수 | 기여도(loading) | 해석 |
bill_length_mm | +0.455 | 좀 큼 |
bill_depth_mm | −0.400 | - 중에서는 큼 |
flipper_length_mm | +0.576 | + 중에서는 제일 큼 |
body_mass_g | +0.548 | 좀 많이 큼 |
사실대로 적었다.
이를 볼 때 PC1은 전반적으로 큰 펭귄이고 부리 깊이가 작아진다는 것을 나타내는 축으로 볼 수 있겠다. 아래는 위에 내용에 대한 시각화다.
barplot(eigen_vectors[,1],
names.arg = colnames(penguins_numeric),
main = "PC1 Loadings",
col = "skyblue")
> eigen_vectors[,2]
[1] -0.597031143 -0.797766572 -0.002282201 -0.084362920
이를 표로 한번 나타내보자.
변수 | 기여도(loading) | 해석 |
bill_length_mm | −0.597 | - 중에서 좀 많이 큼 |
bill_depth_mm | −0.798 | - 중에서 제일 큼 |
flipper_length_mm | −0.002 | 영향력 적음 |
body_mass_g | −0.084 | 영향력 적음 |
PC2는 부리(bill)과 관련된 축으로 알 수 있다. 아래의 그래프는 위에 대한 시각화코드다.
barplot(eigen_vectors[,2],
names.arg = colnames(penguins_numeric),
main = "PC2 Loadings",
col = "orange")
*고유값 해석
> eigen_values
[1] 2.7537551 0.7725168 0.3652359 0.1084922
위의 고유값은 각 분산을 뜻한다. 제일 큰 분산을 담당하는 고유값이 2.7이다.
*PC_score 해석
PC_scores <- X %*% eigen_vectors
PC_scores[1,]
# [1] -1.840748 -0.04763243 -0.2324536 0.5231365
행렬 X의 행은 각 하나의 펭귄을 뜻한다. 5행은 5번째 펭귄 이런식이다.
아래는 X의 출처다.
# 사용할 변수: 부리 길이, 부리 깊이, 지느러미 길이, 몸무게
penguins_numeric <- penguins[, c("bill_length_mm", "bill_depth_mm", "flipper_length_mm", "body_mass_g")]
# 결측치 제거
penguins_numeric <- na.omit(penguins_numeric)
# 데이터 중심화 및 스케일링 (평균 0, 표준편차 1)
X <- scale(penguins_numeric, center = TRUE, scale = TRUE)
아무튼, PC_socres[1,]은 1번째 펭귄이 각 스코어별로 어떤 점수(스코어별 고유벡터로 투영된 거리)를 가졌는지에 대한 내용이다.
1번째 펭귄은 PC1에 대해서 -1.84, PC2에 대해서는 -0.04, PC3에 대해서는 -0.23, PC4에 대해서는 0.523이다. 아~ 그러면 PC1하고 PC4가 여기선 중요하구나! 라고 할 수 있지만.. 우리는 앞서 PC4의 분산설명비율이 2.7%임을 보았다. 제아무리 0.523이여도 전체 분산설명비율이 2.7%면 의미없다.
그렇다면 0.027*0.523으로 의미있는 값이 나올 수 있을까? 예로들어 사과1개에 500g인데 4개면? 4(개)*500(g) 이런식으로 말이다. 결론은 곱할 수는 있으나.. 우리가 원하는 해석(분산기여도,정보량)은 아니다. PCA의 본질은 "분산"이므로 정보량을 따지려면 제곱으로 해야한다.
PC4에서 1번째 샘플의 기여도 = PC4_{i4}^2/sum_{i=1}^{n} {PC_k4 ^2}
> # PC score의 4번째 주성분(PC4) 점수 벡터
> PC_scores %>% head()
bill_length_mm bill_depth_mm flipper_length_mm body_mass_g
[1,] -1.840748 -0.04763243 -0.2324536 0.5231365
[2,] -1.304850 0.42772154 -0.0295191 0.4018377
[3,] -1.367178 0.15425039 0.1983816 -0.5272343
[4,] -1.876078 0.00204541 -0.6176912 -0.4776785
[5,] -1.908951 -0.82799642 -0.6855795 -0.2071241
[6,] -1.760446 0.35096537 0.0276311 0.5039784
> pc4_scores <- PC_scores[, 4]
>
> # i번째 (예: 1번째) 펭귄의 기여도
> i <- 1
> individual_contribution <- (pc4_scores[i])^2 / sum(pc4_scores^2)
> individual_contribution
[1] 0.007397365
즉, 첫번째 펭귄이 PC4축에 0.7%기여했음을 알 수 있다. 다음은 더 나아가 상위 6명까지 PC4에 얼마나 기여하는지 확인해보자.
# PC4 점수
pc4_scores <- PC_scores[, 4]
# 상위 6개 샘플의 PC4 기여도 계산
contributions <- (pc4_scores[1:6])^2 / sum(pc4_scores^2)
contributions
> contributions
[1] 0.007397365 0.004364639 0.007513708 0.006167631 0.001159601 0.006865479
PC4 축에 첫 번째 펭귄이 0.7%, 두 번째 펭귄이 0.4% , 세 번째 펭귄이 0.7%, 네번째 펭귄이 0.6%, .. 이런식으로 기여했음을 알 수 있다.
다만, 첫번째 펭귄이 PC4가 점수가 크고 고유값이 작다는 것은 이 개체가 튀는개체(outliner)라는 것으로 나중에 특이값 탐지나 잔차분석에 쓸 수는 있겠다.