Implementação passo a passo do PCA em R usando o tutorial de Lindsay Smith

13

Estou trabalhando no R através de um excelente tutorial de PCA de Lindsay I Smith e estou ficando paralisado na última etapa. O script R abaixo nos leva ao estágio (na p.19) em que os dados originais estão sendo reconstruídos a partir do (principal neste caso) Componente Principal, que deve gerar um gráfico de linha reta ao longo do eixo PCA1 (dado que os dados possui apenas 2 dimensões, sendo a segunda eliminada intencionalmente).

d = data.frame(x=c(2.5,0.5,2.2,1.9,3.1,2.3,2.0,1.0,1.5,1.1),
               y=c(2.4,0.7,2.9,2.2,3.0,2.7,1.6,1.1,1.6,0.9))

# mean-adjusted values 
d$x_adj = d$x - mean(d$x)
d$y_adj = d$y - mean(d$y)

# calculate covariance matrix and eigenvectors/values
(cm = cov(d[,1:2]))

#### outputs #############
#          x         y
# x 0.6165556 0.6154444
# y 0.6154444 0.7165556
##########################

(e = eigen(cm))

##### outputs ##############
# $values
# [1] 1.2840277 0.0490834
#
# $vectors
#          [,1]       [,2]
# [1,] 0.6778734 -0.7351787
# [2,] 0.7351787  0.6778734
###########################


# principal component vector slopes
s1 = e$vectors[1,1] / e$vectors[2,1] # PC1
s2 = e$vectors[1,2] / e$vectors[2,2] # PC2

plot(d$x_adj, d$y_adj, asp=T, pch=16, xlab='x', ylab='y')
abline(a=0, b=s1, col='red')
abline(a=0, b=s2)

insira a descrição da imagem aqui

# PCA data = rowFeatureVector (transposed eigenvectors) * RowDataAdjust (mean adjusted, also transposed)
feat_vec = t(e$vectors)
row_data_adj = t(d[,3:4])
final_data = data.frame(t(feat_vec %*% row_data_adj)) # ?matmult for details
names(final_data) = c('x','y')

#### outputs ###############
# final_data
#              x           y
# 1   0.82797019 -0.17511531
# 2  -1.77758033  0.14285723
# 3   0.99219749  0.38437499
# 4   0.27421042  0.13041721
# 5   1.67580142 -0.20949846
# 6   0.91294910  0.17528244
# 7  -0.09910944 -0.34982470
# 8  -1.14457216  0.04641726
# 9  -0.43804614  0.01776463
# 10 -1.22382056 -0.16267529
############################

# final_data[[1]] = -final_data[[1]] # for some reason the x-axis data is negative the tutorial's result

plot(final_data, asp=T, xlab='PCA 1', ylab='PCA 2', pch=16)

insira a descrição da imagem aqui

Isso é o máximo que eu tenho, e tudo bem até agora. Mas não consigo descobrir como os dados são obtidos para o gráfico final - a variação atribuível ao PCA 1 - que Smith plota como:

insira a descrição da imagem aqui

Isto é o que eu tentei (que ignora a adição dos meios originais):

trans_data = final_data
trans_data[,2] = 0
row_orig_data = t(t(feat_vec[1,]) %*% t(trans_data))
plot(row_orig_data, asp=T, pch=16)

.. e tem um erro:

insira a descrição da imagem aqui

.. porque eu perdi uma dimensão de dados de alguma forma na multiplicação da matriz. Ficaria muito grato por ter uma ideia do que está acontecendo de errado aqui.


* Editar *

Gostaria de saber se esta é a fórmula certa:

row_orig_data = t(t(feat_vec) %*% t(trans_data))
plot(row_orig_data, asp=T, pch=16, cex=.5)
abline(a=0, b=s1, col='red')

Mas estou um pouco confuso se sim, porque (a) entendo que as rowVectorFeaturenecessidades devem ser reduzidas à dimensionalidade desejada (o vetor próprio para PCA1) e (b) não está alinhado com o abline PCA1:

insira a descrição da imagem aqui

Qualquer vista muito apreciada.

geotoria
fonte
s1y/xx/y
Sobre a reconstrução de dados originais dos principais componentes principais, consulte este novo thread: stats.stackexchange.com/questions/229092 .
Ameba diz Reinstate Monica

Respostas:

10

Você estava muito perto e foi pego por um problema sutil ao trabalhar com matrizes na R. Eu trabalhei com você final_datae obtive os resultados corretos independentemente. Então, olhei mais de perto o seu código. Para encurtar uma longa história, onde você escreveu

row_orig_data = t(t(feat_vec[1,]) %*% t(trans_data))

você teria ficado bem se tivesse escrito

row_orig_data = t(t(feat_vec) %*% t(trans_data))

trans_data2×12×10t(feat_vec[1,])1×2row_orig_data = t(as.matrix(feat_vec[1,],ncol=1,nrow=2) %*% t(trans_data))non-conformable arguments

row_orig_data = t(as.matrix(feat_vec[1,],ncol=1,nrow=2) %*% t(trans_data)[1,])

2×11×10final_data20=2×10row_orig_data12=2×1+1×10

(XY)T=YTXTt(t(p) %*% t(q)) = q %*% t

x/yy/x


Escrever

d_in_new_basis = as.matrix(final_data)

para recuperar seus dados em sua base original, você precisa

d_in_original_basis = d_in_new_basis %*% feat_vec

Você pode zerar as partes de seus dados projetadas ao longo do segundo componente usando

d_in_new_basis_approx = d_in_new_basis
d_in_new_basis_approx[,2] = 0

e você pode transformar como antes

d_in_original_basis_approx = d_in_new_basis_approx %*% feat_vec

A plotagem no mesmo gráfico, junto com a linha do componente principal em verde, mostra como a aproximação funcionou.

plot(x=d_in_original_basis[,1]+mean(d$x),
     y=d_in_original_basis[,2]+mean(d$y),
     pch=16, xlab="x", ylab="y", xlim=c(0,3.5),ylim=c(0,3.5),
     main="black=original data\nred=original data restored using only a single eigenvector")
points(x=d_in_original_basis_approx[,1]+mean(d$x),
       y=d_in_original_basis_approx[,2]+mean(d$y),
       pch=16,col="red")
points(x=c(mean(d$x)-e$vectors[1,1]*10,mean(d$x)+e$vectors[1,1]*10), c(y=mean(d$y)-e$vectors[2,1]*10,mean(d$y)+e$vectors[2,1]*10), type="l",col="green")

insira a descrição da imagem aqui

Vamos voltar ao que você tinha. Esta linha estava ok

final_data = data.frame(t(feat_vec %*% row_data_adj))

feat_vec %*% row_data_adjY=STXSXYYXYX

Então você teve

trans_data = final_data
trans_data[,2] = 0

Tudo bem: você está zerando as partes dos dados projetadas ao longo do segundo componente. Onde isso dá errado é

row_orig_data = t(t(feat_vec[1,]) %*% t(trans_data))

Y^Ye1t(feat_vec[1,]) %*% t(trans_data)e1Y^

2×12×10Y^Yy1e1y1Eue1y1e1Eu

TooTone
fonte
Obrigado TooTone, isso é muito abrangente e resolve as ambigüidades no meu entendimento do cálculo da matriz e do papel do featureVector na fase final.
Geotheory
Ótimo :). Respondi a essa pergunta porque estou estudando a teoria de SVD / PCA no momento e queria entender como ela funciona com um exemplo: sua pergunta foi boa. Depois de trabalhar em todos os cálculos de matriz, fiquei um pouco surpreso por se tratar de um problema de R - então estou feliz que você tenha gostado do aspecto das matrizes também.
TooTone
4

Eu acho que você tem a idéia certa, mas tropeçou em um recurso desagradável de R. Aqui, novamente, a parte do código relevante como você a declarou:

trans_data = final_data
trans_data[,2] = 0
row_orig_data = t(t(feat_vec[1,]) %*% t(trans_data))
plot(row_orig_data, asp=T, pch=16)

Essencialmente final_datacontém as coordenadas dos pontos originais em relação ao sistema de coordenadas definido pelos vetores próprios da matriz de covariância. Para reconstruir os pontos originais, é necessário multiplicar cada vetor próprio pela coordenada transformada associada, por exemplo

(1) final_data[1,1]*t(feat_vec[1,] + final_data[1,2]*t(feat_vec[2,])

o que produziria as coordenadas originais do primeiro ponto. Na sua pergunta, você define o segundo componente corretamente como zero trans_data[,2] = 0,. Se você (como você já editou) calcula

(2) row_orig_data = t(t(feat_vec) %*% t(trans_data))

você calcula a fórmula (1) para todos os pontos simultaneamente. Sua primeira abordagem

row_orig_data = t(t(feat_vec[1,]) %*% t(trans_data))

calcula algo diferente e só funciona porque R elimina automaticamente o atributo de dimensão para feat_vec[1,], portanto, não é mais um vetor de linha, mas tratado como um vetor de coluna. A transposição subsequente o transforma em um vetor de linha novamente e é por isso que pelo menos o cálculo não produz um erro, mas se você passar pela matemática, verá que é algo diferente de (1). Em geral, é uma boa idéia em multiplicações de matrizes suprimir a queda do atributo de dimensão que pode ser alcançado pelo dropparâmetro, por exemplo feat_vec[1,,drop=FALSE].

Δy/Δx

s1 = e$vectors[2,1] / e$vectors[1,1] # PC1
s2 = e$vectors[2,2] / e$vectors[1,2] # PC2
Georg Schnabel
fonte
Muito obrigado Georg. Você está certo sobre a inclinação da PCA1. Dica muito útil também sobre o drop=Fargumento.
geotheory
4

Depois de explorar este exercício, você pode tentar as maneiras mais fáceis em R. Existem duas funções populares para executar o PCA: princompe prcomp. A princompfunção faz a decomposição do autovalor, como foi feito no seu exercício. A prcompfunção usa decomposição de valor singular. Ambos os métodos fornecerão os mesmos resultados quase o tempo todo: esta resposta explica as diferenças em R, enquanto que essa resposta explica a matemática . (Obrigado ao TooTone pelos comentários agora integrados a esta postagem.)

Aqui usamos os dois para reproduzir o exercício em R. Primeiro, usando princomp:

d = data.frame(x=c(2.5,0.5,2.2,1.9,3.1,2.3,2.0,1.0,1.5,1.1), 
               y=c(2.4,0.7,2.9,2.2,3.0,2.7,1.6,1.1,1.6,0.9))

# compute PCs
p = princomp(d,center=TRUE,retx=TRUE)

# use loadings and scores to reproduce with only first PC
loadings = t(p$loadings[,1]) 
scores = p$scores[,1] 

reproduce = scores %*% loadings  + colMeans(d)

# plots
plot(reproduce,pch=3,ylim=c(-1,4),xlim=c(-1,4))
abline(h=0,v=0,lty=3)
mtext("Original data restored using only a single eigenvector",side=3,cex=0.7)

biplot(p)

insira a descrição da imagem aqui insira a descrição da imagem aqui

Segundo uso prcomp:

d = data.frame(x=c(2.5,0.5,2.2,1.9,3.1,2.3,2.0,1.0,1.5,1.1), 
               y=c(2.4,0.7,2.9,2.2,3.0,2.7,1.6,1.1,1.6,0.9))

# compute PCs
p = prcomp(d,center=TRUE,retx=TRUE)

# use loadings and scores to reproduce with only first PC
loadings = t(p$rotation[,1])
scores = p$x[,1]

reproduce = scores %*% loadings  + colMeans(d)

# plots
plot(reproduce,pch=3,ylim=c(-1,4),xlim=c(-1,4))
abline(h=0,v=0,lty=3)
mtext("Original data restored using only a single eigenvector",side=3,cex=0.7)

biplot(p)

insira a descrição da imagem aqui insira a descrição da imagem aqui

Claramente, os sinais são invertidos, mas a explicação da variação é equivalente.

mrbcuda
fonte
Obrigado mrbcuda. Seu biplot parece idêntico ao de Lindsay Smith, então eu presumo que ele / ela usou o mesmo método há 12 anos! Também estou ciente de alguns outros métodos de nível superior , mas, como você aponta corretamente, este é um exercício para tornar explícitas as matemáticas subjacentes do PCA.
Geotheory