Análise de componentes principais em Python

112

Eu gostaria de usar a análise de componente principal (PCA) para redução de dimensionalidade. Numpy ou scipy já o tem, ou tenho que rolar o meu próprio usandonumpy.linalg.eigh ?

Não quero apenas usar a decomposição de valor singular (SVD) porque meus dados de entrada são bem dimensionais (~ 460 dimensões), então acho que SVD será mais lento do que calcular os autovetores da matriz de covariância.

Eu esperava encontrar uma implementação pré-fabricada e depurada que já toma as decisões corretas sobre quando usar qual método e que talvez faça outras otimizações que eu não conheço.

Vebjorn Ljosa
fonte

Respostas:

28

Você pode dar uma olhada no MDP .

Não tive a chance de testá-lo sozinho, mas marquei-o exatamente para a funcionalidade do PCA.

ChristopheD
fonte
8
O MDP não é mantido desde 2012, não parece ser a melhor solução.
Marc Garcia
A atualização mais recente é de 09/03/2016, mas observe que é apenas uma versão de correção de bug:Note that from this release MDP is in maintenance mode. 13 years after its first public release, MDP has reached full maturity and no new features are planned in the future.
Gabriel
65

Meses depois, aqui está um pequeno PCA de classe e uma foto:

#!/usr/bin/env python
""" a small class for Principal Component Analysis
Usage:
    p = PCA( A, fraction=0.90 )
In:
    A: an array of e.g. 1000 observations x 20 variables, 1000 rows x 20 columns
    fraction: use principal components that account for e.g.
        90 % of the total variance

Out:
    p.U, p.d, p.Vt: from numpy.linalg.svd, A = U . d . Vt
    p.dinv: 1/d or 0, see NR
    p.eigen: the eigenvalues of A*A, in decreasing order (p.d**2).
        eigen[j] / eigen.sum() is variable j's fraction of the total variance;
        look at the first few eigen[] to see how many PCs get to 90 %, 95 % ...
    p.npc: number of principal components,
        e.g. 2 if the top 2 eigenvalues are >= `fraction` of the total.
        It's ok to change this; methods use the current value.

Methods:
    The methods of class PCA transform vectors or arrays of e.g.
    20 variables, 2 principal components and 1000 observations,
    using partial matrices U' d' Vt', parts of the full U d Vt:
    A ~ U' . d' . Vt' where e.g.
        U' is 1000 x 2
        d' is diag([ d0, d1 ]), the 2 largest singular values
        Vt' is 2 x 20.  Dropping the primes,

    d . Vt      2 principal vars = p.vars_pc( 20 vars )
    U           1000 obs = p.pc_obs( 2 principal vars )
    U . d . Vt  1000 obs, p.obs( 20 vars ) = pc_obs( vars_pc( vars ))
        fast approximate A . vars, using the `npc` principal components

    Ut              2 pcs = p.obs_pc( 1000 obs )
    V . dinv        20 vars = p.pc_vars( 2 principal vars )
    V . dinv . Ut   20 vars, p.vars( 1000 obs ) = pc_vars( obs_pc( obs )),
        fast approximate Ainverse . obs: vars that give ~ those obs.


Notes:
    PCA does not center or scale A; you usually want to first
        A -= A.mean(A, axis=0)
        A /= A.std(A, axis=0)
    with the little class Center or the like, below.

See also:
    http://en.wikipedia.org/wiki/Principal_component_analysis
    http://en.wikipedia.org/wiki/Singular_value_decomposition
    Press et al., Numerical Recipes (2 or 3 ed), SVD
    PCA micro-tutorial
    iris-pca .py .png

"""

from __future__ import division
import numpy as np
dot = np.dot
    # import bz.numpyutil as nu
    # dot = nu.pdot

__version__ = "2010-04-14 apr"
__author_email__ = "denis-bz-py at t-online dot de"

#...............................................................................
class PCA:
    def __init__( self, A, fraction=0.90 ):
        assert 0 <= fraction <= 1
            # A = U . diag(d) . Vt, O( m n^2 ), lapack_lite --
        self.U, self.d, self.Vt = np.linalg.svd( A, full_matrices=False )
        assert np.all( self.d[:-1] >= self.d[1:] )  # sorted
        self.eigen = self.d**2
        self.sumvariance = np.cumsum(self.eigen)
        self.sumvariance /= self.sumvariance[-1]
        self.npc = np.searchsorted( self.sumvariance, fraction ) + 1
        self.dinv = np.array([ 1/d if d > self.d[0] * 1e-6  else 0
                                for d in self.d ])

    def pc( self ):
        """ e.g. 1000 x 2 U[:, :npc] * d[:npc], to plot etc. """
        n = self.npc
        return self.U[:, :n] * self.d[:n]

    # These 1-line methods may not be worth the bother;
    # then use U d Vt directly --

    def vars_pc( self, x ):
        n = self.npc
        return self.d[:n] * dot( self.Vt[:n], x.T ).T  # 20 vars -> 2 principal

    def pc_vars( self, p ):
        n = self.npc
        return dot( self.Vt[:n].T, (self.dinv[:n] * p).T ) .T  # 2 PC -> 20 vars

    def pc_obs( self, p ):
        n = self.npc
        return dot( self.U[:, :n], p.T )  # 2 principal -> 1000 obs

    def obs_pc( self, obs ):
        n = self.npc
        return dot( self.U[:, :n].T, obs ) .T  # 1000 obs -> 2 principal

    def obs( self, x ):
        return self.pc_obs( self.vars_pc(x) )  # 20 vars -> 2 principal -> 1000 obs

    def vars( self, obs ):
        return self.pc_vars( self.obs_pc(obs) )  # 1000 obs -> 2 principal -> 20 vars


class Center:
    """ A -= A.mean() /= A.std(), inplace -- use A.copy() if need be
        uncenter(x) == original A . x
    """
        # mttiw
    def __init__( self, A, axis=0, scale=True, verbose=1 ):
        self.mean = A.mean(axis=axis)
        if verbose:
            print "Center -= A.mean:", self.mean
        A -= self.mean
        if scale:
            std = A.std(axis=axis)
            self.std = np.where( std, std, 1. )
            if verbose:
                print "Center /= A.std:", self.std
            A /= self.std
        else:
            self.std = np.ones( A.shape[-1] )
        self.A = A

    def uncenter( self, x ):
        return np.dot( self.A, x * self.std ) + np.dot( x, self.mean )


#...............................................................................
if __name__ == "__main__":
    import sys

    csv = "iris4.csv"  # wikipedia Iris_flower_data_set
        # 5.1,3.5,1.4,0.2  # ,Iris-setosa ...
    N = 1000
    K = 20
    fraction = .90
    seed = 1
    exec "\n".join( sys.argv[1:] )  # N= ...
    np.random.seed(seed)
    np.set_printoptions( 1, threshold=100, suppress=True )  # .1f
    try:
        A = np.genfromtxt( csv, delimiter="," )
        N, K = A.shape
    except IOError:
        A = np.random.normal( size=(N, K) )  # gen correlated ?

    print "csv: %s  N: %d  K: %d  fraction: %.2g" % (csv, N, K, fraction)
    Center(A)
    print "A:", A

    print "PCA ..." ,
    p = PCA( A, fraction=fraction )
    print "npc:", p.npc
    print "% variance:", p.sumvariance * 100

    print "Vt[0], weights that give PC 0:", p.Vt[0]
    print "A . Vt[0]:", dot( A, p.Vt[0] )
    print "pc:", p.pc()

    print "\nobs <-> pc <-> x: with fraction=1, diffs should be ~ 0"
    x = np.ones(K)
    # x = np.ones(( 3, K ))
    print "x:", x
    pc = p.vars_pc(x)  # d' Vt' x
    print "vars_pc(x):", pc
    print "back to ~ x:", p.pc_vars(pc)

    Ax = dot( A, x.T )
    pcx = p.obs(x)  # U' d' Vt' x
    print "Ax:", Ax
    print "A'x:", pcx
    print "max |Ax - A'x|: %.2g" % np.linalg.norm( Ax - pcx, np.inf )

    b = Ax  # ~ back to original x, Ainv A x
    back = p.vars(b)
    print "~ back again:", back
    print "max |back - x|: %.2g" % np.linalg.norm( back - x, np.inf )

# end pca.py

insira a descrição da imagem aqui

Denis
fonte
3
Fyinfo, há uma excelente palestra sobre Robust PCA por C. Caramanis, janeiro de 2011.
denis
este código irá gerar essa imagem (Iris PCA)? Se não, você pode postar uma solução alternativa em que a saída seja essa imagem. Estou tendo algumas dificuldades em converter este código para c ++ porque sou novo em python :)
Orvyl
44

Usar o PCA numpy.linalg.svdé super fácil. Aqui está uma demonstração simples:

import numpy as np
import matplotlib.pyplot as plt
from scipy.misc import lena

# the underlying signal is a sinusoidally modulated image
img = lena()
t = np.arange(100)
time = np.sin(0.1*t)
real = time[:,np.newaxis,np.newaxis] * img[np.newaxis,...]

# we add some noise
noisy = real + np.random.randn(*real.shape)*255

# (observations, features) matrix
M = noisy.reshape(noisy.shape[0],-1)

# singular value decomposition factorises your data matrix such that:
# 
#   M = U*S*V.T     (where '*' is matrix multiplication)
# 
# * U and V are the singular matrices, containing orthogonal vectors of
#   unit length in their rows and columns respectively.
#
# * S is a diagonal matrix containing the singular values of M - these 
#   values squared divided by the number of observations will give the 
#   variance explained by each PC.
#
# * if M is considered to be an (observations, features) matrix, the PCs
#   themselves would correspond to the rows of S^(1/2)*V.T. if M is 
#   (features, observations) then the PCs would be the columns of
#   U*S^(1/2).
#
# * since U and V both contain orthonormal vectors, U*V.T is equivalent 
#   to a whitened version of M.

U, s, Vt = np.linalg.svd(M, full_matrices=False)
V = Vt.T

# PCs are already sorted by descending order 
# of the singular values (i.e. by the
# proportion of total variance they explain)

# if we use all of the PCs we can reconstruct the noisy signal perfectly
S = np.diag(s)
Mhat = np.dot(U, np.dot(S, V.T))
print "Using all PCs, MSE = %.6G" %(np.mean((M - Mhat)**2))

# if we use only the first 20 PCs the reconstruction is less accurate
Mhat2 = np.dot(U[:, :20], np.dot(S[:20, :20], V[:,:20].T))
print "Using first 20 PCs, MSE = %.6G" %(np.mean((M - Mhat2)**2))

fig, [ax1, ax2, ax3] = plt.subplots(1, 3)
ax1.imshow(img)
ax1.set_title('true image')
ax2.imshow(noisy.mean(0))
ax2.set_title('mean of noisy images')
ax3.imshow((s[0]**(1./2) * V[:,0]).reshape(img.shape))
ax3.set_title('first spatial PC')
plt.show()
ali_m
fonte
2
Sei que estou um pouco atrasado aqui, mas o OP solicitou especificamente uma solução que evita a decomposição de valores singulares.
Alex A.
1
@Alex Eu sei disso, mas estou convencido de que SVD ainda é a abordagem certa. Deve ser rápido o suficiente para as necessidades do OP (meu exemplo acima, com 262144 dimensões leva apenas ~ 7,5 segundos em um laptop normal), e é muito mais estável numericamente do que o método de decomposição automática (veja o comentário de dwf abaixo). Também observo que a resposta aceita também usa SVD!
ali_m
Eu não discordo que SVD seja o caminho a percorrer, eu estava apenas dizendo que a resposta não aborda a pergunta como a pergunta é feita. É uma boa resposta, no entanto, bom trabalho.
Alex A.
5
@Alex Muito justo. Acho que essa é outra variante do problema XY - o OP disse que não queria uma solução baseada em SVD porque achava que o SVD seria muito lento, provavelmente sem ter tentado ainda. Em casos como esse, pessoalmente acho que é mais útil explicar como você lidaria com o problema mais amplo, em vez de responder à pergunta exatamente em sua forma original mais restrita.
ali_m
svdjá retorna sordenado em ordem decrescente, de acordo com a documentação. (Talvez não fosse o caso em 2012, mas hoje é)
Etienne Bruines
34

Você pode usar o sklearn:

import sklearn.decomposition as deco
import numpy as np

x = (x - np.mean(x, 0)) / np.std(x, 0) # You need to normalize your data first
pca = deco.PCA(n_components) # n_components is the components number after reduction
x_r = pca.fit(x).transform(x)
print ('explained variance (first %d components): %.2f'%(n_components, sum(pca.explained_variance_ratio_)))
Noam Peled
fonte
Votado porque funciona bem para mim - tenho mais de 460 dimensões e, embora sklearn use SVD e a pergunta solicitada não SVD, acho que 460 dimensões provavelmente está OK.
Dan Stowell
Você também pode querer remover colunas com um valor constante (std = 0). Para isso você deve usar: remove_cols = np.where (np.all (x == np.mean (x, 0), 0)) [0] E então x = np.delete (x, remove_cols, 1)
Noam Peled
14

SVD deve funcionar bem com 460 dimensões. Demora cerca de 7 segundos no meu netbook Atom. O método eig () leva mais tempo (como deveria, ele usa mais operações de ponto flutuante) e quase sempre será menos preciso.

Se você tem menos de 460 exemplos, então o que você quer fazer é diagonalizar a matriz de dispersão (x - datamean) ^ T (x - média), assumindo que seus pontos de dados são colunas, e então multiplicando à esquerda por (x - datamean). Isso pode ser mais rápido no caso em que você tem mais dimensões do que dados.

dwf
fonte
você pode descrever mais detalhadamente esse truque quando você tem mais dimensões do que dados?
mrgloom
1
Basicamente, você assume que os vetores próprios são combinações lineares dos vetores de dados. Veja Sirovich (1987). "Turbulência e a dinâmica de estruturas coerentes."
dwf
11

Você pode facilmente "rolar" seu próprio usando scipy.linalg(assumindo um conjunto de dados pré-centrado data):

covmat = data.dot(data.T)
evs, evmat = scipy.linalg.eig(covmat)

Então, evssão seus autovalores e evmatsua matriz de projeção.

Se você quiser manter as ddimensões, use os primeiros dautovalores e os primeiros dautovetores.

Dado que scipy.linalgtem decomposição e numpy as multiplicações da matriz, o que mais você precisa?

Tem QUIT - Anony-Mousse
fonte
cov matrix é np.dot (data.T, data, out = covmat), onde os dados devem ser uma matriz centrada.
mrgloom
2
Você deve dar uma olhada no comentário de @dwf sobre esta resposta para os perigos do uso eig()em uma matriz de covariância.
Alex A.
8

Acabei de terminar de ler o livro Machine Learning: An Algorithmic Perspective . Todos os exemplos de código do livro foram escritos por Python (e quase com Numpy). O trecho de código da análise de componentes principais do chatper10.2 talvez valha a pena uma leitura. Ele usa numpy.linalg.eig.
A propósito, acho que o SVD pode lidar com 460 * 460 dimensões muito bem. Calculei um 6500 * 6500 SVD com numpy / scipy.linalg.svd em um PC muito antigo: Pentium III 733mHz. Para ser honesto, o script precisa de muita memória (cerca de 1.xG) e muito tempo (cerca de 30 minutos) para obter o resultado SVD. Mas acho que 460 * 460 em um PC moderno não será um grande problema, a menos que você precise fazer SVD um grande número de vezes.

Sunqiang
fonte
28
Você nunca deve usar eig () em uma matriz de covariância quando você pode simplesmente usar svd (). Dependendo de quantos componentes você planeja usar e do tamanho de sua matriz de dados, o erro numérico introduzido pela primeira (ela faz mais operações de ponto flutuante) pode se tornar significativo. Pela mesma razão, você nunca deve inverter explicitamente uma matriz com inv () se o que realmente interessa é o inverso vezes um vetor ou matriz; você deve usar solve () em vez disso.
dwf
5

Você não precisa da Decomposição de Valor Singular (SVD) completa, pois ela calcula todos os valores próprios e vetores próprios e pode ser proibitiva para matrizes grandes. scipy e seu módulo esparso fornecem funções de algrebra linear genérica trabalhando em matrizes esparsas e densas, entre as quais existe a família de funções eig *:

http://docs.scipy.org/doc/scipy/reference/sparse.linalg.html#matrix-factorizations

O Scikit-learn fornece uma implementação Python PCA que suporta apenas matrizes densas por enquanto.

Horários:

In [1]: A = np.random.randn(1000, 1000)

In [2]: %timeit scipy.sparse.linalg.eigsh(A)
1 loops, best of 3: 802 ms per loop

In [3]: %timeit np.linalg.svd(A)
1 loops, best of 3: 5.91 s per loop
Nicolas Barbey
fonte
1
Não é realmente uma comparação justa, já que você ainda precisa calcular a matriz de covariância. Além disso, provavelmente só vale a pena usar o material linalg esparso para matrizes muito grandes, uma vez que parece ser bastante lento construir matrizes esparsas a partir de matrizes densas. por exemplo, eigshé na verdade ~ 4x mais lento do que eighmatrizes não esparsas. O mesmo é válido para scipy.sparse.linalg.svdsversus numpy.linalg.svd. Eu sempre usaria SVD sobre a decomposição de autovalor pelos motivos mencionados por @dwf e talvez usasse uma versão esparsa de SVD se as matrizes ficarem realmente grandes.
ali_m
2
Você não precisa calcular matrizes esparsas a partir de matrizes densas. Os algoritmos fornecidos no módulo sparse.linalg contam apenas com a operação de multiplicação do vetor de matrizes por meio do método matvec do objeto Operator. Para matrizes densas, é apenas algo como matvec = ponto (A, x). Pelo mesmo motivo, você não precisa calcular a matriz de covariância, mas apenas fornecer o ponto de operação (AT, ponto (A, x)) para A.
Nicolas Barbey
Ah, agora vejo que a velocidade relativa dos métodos esparso vs não esparso depende do tamanho da matriz. Se eu usar seu exemplo em que A é uma matriz de 1000 * 1000, então eigshe svdssão mais rápidos do que eighe svdpor um fator de ~ 3, mas se A for menor, digamos 100 * 100, então eighe svdsão mais rápidos por fatores de ~ 4 e ~ 1,5 respectivamente . T ainda usaria SVD esparsa sobre decomposição de autovalor esparsa embora.
ali_m
2
Na verdade, acho que estou inclinado para grandes matrizes. Para mim, matrizes grandes são mais parecidas com 10⁶ * 10⁶ do que 1000 * 1000. Nesse caso, muitas vezes você não consegue nem mesmo armazenar as matrizes de covariância ...
Nicolas Barbey
4

Aqui está outra implementação de um módulo PCA para python usando extensões numpy, scipy e C. O módulo executa PCA usando um SVD ou o algoritmo NIPALS (Nonlinear Iterative Partial Least Squares) que é implementado em C.

rcs
fonte
0

Se estiver trabalhando com vetores 3D, você pode aplicar SVD de forma concisa usando o toolbelt vg . É uma camada leve em cima do entorpecido.

import numpy as np
import vg

vg.principal_components(data)

Também há um alias conveniente se você quiser apenas o primeiro componente principal:

vg.major_axis(data)

Eu criei a biblioteca na minha última inicialização, onde ela foi motivada por usos como este: ideias simples que são prolixas ou opacas no NumPy.

Paulmelnikow
fonte