Analyse des composants principaux en Python

112

J'aimerais utiliser l'analyse en composantes principales (ACP) pour la réduction de la dimensionnalité. Est-ce que Numpy ou Scipy l'a déjà, ou dois-je utiliser le mien numpy.linalg.eigh?

Je ne veux pas simplement utiliser la décomposition en valeurs singulières (SVD) car mes données d'entrée sont assez dimensionnelles (~ 460 dimensions), donc je pense que SVD sera plus lent que le calcul des vecteurs propres de la matrice de covariance.

J'espérais trouver une implémentation prédéfinie et déboguée qui prend déjà les bonnes décisions pour savoir quand utiliser quelle méthode, et qui peut peut-être faire d'autres optimisations que je ne connais pas.

Vebjorn Ljosa
la source

Réponses:

28

Vous pourriez jeter un œil à MDP .

Je n'ai pas eu la chance de le tester moi-même, mais je l'ai mis en signet exactement pour la fonctionnalité PCA.

ChristopheD
la source
8
MDP n'a pas été maintenu depuis 2012, ne semble pas être la meilleure solution.
Marc Garcia
La dernière mise à jour date du 09.03.2016, mais notez que ir n'est qu'une version 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

Quelques mois plus tard, voici un petit PCA de classe et une photo:

#!/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

entrez la description de l'image ici

denis
la source
3
Fyinfo, il y a une excellente conférence sur Robust PCA par C. Caramanis, janvier 2011.
denis
ce code produira-t-il cette image (Iris PCA)? Sinon, pouvez-vous publier une solution alternative dans laquelle la sortie serait cette image. IM ayant quelques difficultés à convertir ce code en c ++ car je suis nouveau en python :)
Orvyl
44

L'utilisation de PCA numpy.linalg.svdest super facile. Voici une démo simple:

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
la source
2
Je me rends compte que je suis un peu en retard ici, mais l'OP a spécifiquement demandé une solution qui évite la décomposition de valeurs singulières.
Alex A.
1
@Alex Je m'en rends compte, mais je suis convaincu que SVD est toujours la bonne approche. Il devrait être assez rapide pour les besoins de l'OP (mon exemple ci-dessus, avec 262144 dimensions ne prend que ~ 7,5 secondes sur un ordinateur portable normal), et il est beaucoup plus stable numériquement que la méthode eigendecomposition (voir le commentaire de dwf ci-dessous). Je note également que la réponse acceptée utilise également SVD!
ali_m
Je ne conteste pas que SVD est la voie à suivre, je disais simplement que la réponse ne répond pas à la question telle que la question est posée. C'est une bonne réponse, cependant, un beau travail.
Alex A.
5
@Alex Assez bien. Je pense que c'est une autre variante du problème XY - l'OP a dit qu'il ne voulait pas d'une solution basée sur SVD parce qu'il pensait que SVD serait trop lent, probablement sans l'avoir encore essayé. Dans des cas comme celui-ci, je pense personnellement qu'il est plus utile d'expliquer comment vous aborderiez le problème plus large, plutôt que de répondre à la question exactement dans sa forme originale et plus étroite.
ali_m
svdrenvoie déjà strié par ordre décroissant, dans la mesure où la documentation va. (Ce n'était peut-être pas le cas en 2012, mais c'est le cas aujourd'hui)
Etienne Bruines
34

Vous pouvez utiliser 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
la source
J'ai voté pour parce que cela fonctionne bien pour moi - j'ai plus de 460 dimensions, et même si sklearn utilise SVD et la question demandée non SVD, je pense que 460 dimensions sont probablement OK.
Dan Stowell
Vous pouvez également supprimer les colonnes avec une valeur constante (std = 0). Pour cela, vous devez utiliser: remove_cols = np.where (np.all (x == np.mean (x, 0), 0)) [0] Et puis x = np.delete (x, remove_cols, 1)
Noam Peled
14

SVD devrait fonctionner correctement avec 460 dimensions. Cela prend environ 7 secondes sur mon netbook Atom. La méthode eig () prend plus de temps (comme il se doit, elle utilise plus d'opérations en virgule flottante) et sera presque toujours moins précise.

Si vous avez moins de 460 exemples, ce que vous voulez faire est de diagonaliser la matrice de dispersion (x - datamean) ^ T (x - mean), en supposant que vos points de données sont des colonnes, puis multiplier à gauche par (x - datamean). Cela peut être plus rapide dans le cas où vous avez plus de dimensions que de données.

dwf
la source
pouvez-vous décrire plus en détail cette astuce lorsque vous avez plus de dimensions que de données?
mrgloom
1
Fondamentalement, vous supposez que les vecteurs propres sont des combinaisons linéaires des vecteurs de données. Voir Sirovich (1987). "Turbulence et dynamique des structures cohérentes."
dwf
11

Vous pouvez facilement "rouler" le vôtre en utilisant scipy.linalg(en supposant un jeu de données pré-centré data):

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

Puis evssont vos valeurs propres et evmatvotre matrice de projection.

Si vous souhaitez conserver les ddimensions, utilisez les premières dvaleurs propres et les premiers dvecteurs propres.

Étant donné que scipy.linalgla décomposition et la numpy des multiplications de la matrice sont associées, de quoi d'autre avez-vous besoin?

A QUITTER - Anony-Mousse
la source
matrice cov est np.dot (data.T, data, out = covmat), où les données doivent être centrées sur la matrice.
mrgloom
2
Vous devriez jeter un œil au commentaire de @ dwf sur cette réponse pour les dangers de l'utilisation eig()sur une matrice de covariance.
Alex A.
8

Je viens de finir de lire le livre Machine Learning: An Algorithmic Perspective . Tous les exemples de code du livre ont été écrits par Python (et presque avec Numpy). L'extrait de code de l' analyse des composants principaux de chatper10.2 vaut peut-être la peine d'être lu. Il utilise numpy.linalg.eig.
Au fait, je pense que SVD peut très bien gérer les dimensions 460 * 460. J'ai calculé un SVD 6500 * 6500 avec numpy / scipy.linalg.svd sur un très vieux PC: Pentium III 733mHz. Pour être honnête, le script a besoin de beaucoup de mémoire (environ 1.xG) et de beaucoup de temps (environ 30 minutes) pour obtenir le résultat SVD. Mais je pense que 460 * 460 sur un PC moderne ne sera pas un gros problème à moins que vous n'ayez besoin de faire SVD un grand nombre de fois.

sunqiang
la source
28
Vous ne devriez jamais utiliser eig () sur une matrice de covariance lorsque vous pouvez simplement utiliser svd (). En fonction du nombre de composants que vous prévoyez d'utiliser et de la taille de votre matrice de données, l'erreur numérique introduite par la première (elle effectue plus d'opérations en virgule flottante) peut devenir significative. Pour la même raison, vous ne devriez jamais inverser explicitement une matrice avec inv () si ce qui vous intéresse vraiment, ce sont les temps inverses d'un vecteur ou d'une matrice; vous devriez utiliser à la place résoudre ().
dwf
5

Vous n'avez pas besoin de la décomposition en valeurs singulières (SVD) complète car elle calcule toutes les valeurs propres et tous les vecteurs propres et peut être prohibitive pour les grandes matrices. scipy et son module sparse fournissent des fonctions d'algèbre linéaire génériques fonctionnant à la fois sur des matrices clairsemées et denses, parmi lesquelles se trouve la famille de fonctions eig *:

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

Scikit-learn fournit une implémentation Python PCA qui ne prend en charge que les matrices denses pour le moment.

Horaires:

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
la source
1
Pas vraiment une comparaison juste, car vous devez encore calculer la matrice de covariance. De plus, cela ne vaut probablement la peine d'utiliser le truc de linalg clairsemé que pour les très grandes matrices, car il semble être assez lent de construire des matrices clairsemées à partir de matrices denses. par exemple, eigshest en fait ~ 4x plus lent que eighpour les matrices non analysées. La même chose est vraie pour scipy.sparse.linalg.svdsversus numpy.linalg.svd. J'irais toujours avec SVD sur la décomposition des valeurs propres pour les raisons mentionnées par @dwf, et j'utiliserais peut-être une version éparse de SVD si les matrices deviennent vraiment énormes.
ali_m
2
Vous n'avez pas besoin de calculer des matrices éparses à partir de matrices denses. Les algorithmes fournis dans le module sparse.linalg reposent uniquement sur l'opération de multiplication des vecteurs matriciels via la méthode matvec de l'objet Operator. Pour les matrices denses, c'est juste quelque chose comme matvec = dot (A, x). Pour la même raison, vous n'avez pas besoin de calculer la matrice de covariance mais seulement de fournir l'opération point (AT, point (A, x)) pour A.
Nicolas Barbey
Ah, maintenant je vois que la vitesse relative des méthodes clairsemées vs non analysées dépend de la taille de la matrice. Si j'utilise votre exemple où A est une matrice de 1000 * 1000 alors eigshet svdssont plus rapides que eighet svdd'un facteur de ~ 3, mais si A est plus petit, disons 100 * 100, alors eighet svdsont plus rapides par des facteurs de ~ 4 et ~ 1,5 respectivement . Cependant, T utiliserait encore une SVD clairsemée sur une décomposition en valeurs propres clairsemée.
ali_m
2
En effet, je pense que je suis biaisé vers les grandes matrices. Pour moi, les grandes matrices ressemblent plus à 10⁶ * 10⁶ qu'à 1000 * 1000. Dans ce cas, vous ne pouvez souvent même pas stocker les matrices de covariance ...
Nicolas Barbey
4

Voici une autre implémentation d'un module PCA pour python utilisant numpy, scipy et C-extensions. Le module exécute l'ACP en utilisant soit un SVD soit l'algorithme NIPALS (Nonlinear Iterative Partial Least Squares) qui est implémenté en C.

rcs
la source
0

Si vous travaillez avec des vecteurs 3D, vous pouvez appliquer SVD de manière concise à l'aide de la toolbelt vg . C'est une couche légère au-dessus de numpy.

import numpy as np
import vg

vg.principal_components(data)

Il existe également un alias pratique si vous ne voulez que le premier composant principal:

vg.major_axis(data)

J'ai créé la bibliothèque lors de ma dernière startup, où elle était motivée par des usages comme celui-ci: des idées simples qui sont verbeuses ou opaques dans NumPy.

paulmelnikow
la source