Analisi delle componenti principali in Python


112

Vorrei utilizzare l'analisi delle componenti principali (PCA) per la riduzione della dimensionalità. Numpy o scipy ce l'hanno già o devo farlo da solo numpy.linalg.eigh?

Non voglio solo usare la scomposizione del valore singolare (SVD) perché i miei dati di input sono abbastanza dimensionali (~ 460 dimensioni), quindi penso che SVD sarà più lento del calcolo degli autovettori della matrice di covarianza.

Speravo di trovare un'implementazione premade e debug che prenda già le decisioni giuste per quando utilizzare quale metodo e che forse esegua altre ottimizzazioni che non conosco.

Risposte:


28

Potresti dare un'occhiata a MDP .

Non ho avuto la possibilità di provarlo da solo, ma l'ho aggiunto ai preferiti esattamente per la funzionalità PCA.


8
MDP non è stato mantenuto dal 2012, non sembra la soluzione migliore.
Marc Garcia

L'ultimo aggiornamento è del 09.03.2016, ma tieni presente che ir è solo una versione per la correzione di 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

Mesi dopo, ecco un PCA di piccola classe e un'immagine:

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

inserisci qui la descrizione dell'immagine


3
Fyinfo, c'è un eccellente discorso su Robust PCA di C. Caramanis, gennaio 2011.
denis

questo codice produrrà quell'immagine (Iris PCA)? In caso contrario, puoi pubblicare una soluzione alternativa in cui il risultato sarebbe quell'immagine. Ho qualche difficoltà nel convertire questo codice in c ++ perché sono nuovo in python :)
Orvyl

44

L'utilizzo di PCA numpy.linalg.svdè semplicissimo. Ecco una semplice demo:

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()

2
Mi rendo conto di essere un po 'in ritardo qui, ma l'OP ha specificamente richiesto una soluzione che eviti la scomposizione di valori singolari.
Alex A.

1
@Alex me ne rendo conto, ma sono convinto che SVD sia ancora l'approccio giusto. Dovrebbe essere abbastanza veloce per le esigenze dell'OP (il mio esempio sopra, con dimensioni 262144 richiede solo ~ 7,5 secondi su un normale laptop) ed è molto più stabile numericamente rispetto al metodo di decomposizione automatica (vedi il commento di dwf sotto). Noto anche che anche la risposta accettata utilizza SVD!
ali_m

Non sono d'accordo sul fatto che SVD sia la strada da percorrere, stavo solo dicendo che la risposta non affronta la domanda mentre la domanda viene espressa. È una buona risposta, però, bel lavoro.
Alex A.

5
@Alex abbastanza giusto. Penso che questa sia un'altra variante del problema XY : l'OP ha detto che non voleva una soluzione basata su SVD perché pensava che SVD sarebbe stato troppo lento, probabilmente senza averlo ancora provato. In casi come questo, personalmente penso che sia più utile spiegare come affronteresti il ​​problema più ampio, piuttosto che rispondere alla domanda esattamente nella sua forma originale e più ristretta.
ali_m

svdrestituisce già scome ordinato in ordine decrescente, per quanto riguarda la documentazione. (Forse non era così nel 2012, ma oggi lo è)
Etienne Bruines,

34

Puoi usare 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_)))

Votato perché funziona bene per me: ho più di 460 dimensioni, e anche se sklearn usa SVD e la domanda ha richiesto non-SVD, penso che 460 dimensioni sia probabilmente OK.
Dan Stowell

Potresti anche voler rimuovere le colonne con un valore costante (std = 0). Per questo dovresti usare: remove_cols = np.where (np.all (x == np.mean (x, 0), 0)) [0] E poi x = np.delete (x, remove_cols, 1)
Noam Peled


14

SVD dovrebbe funzionare bene con 460 dimensioni. Ci vogliono circa 7 secondi sul mio netbook Atom. Il metodo eig () richiede più tempo (come dovrebbe, utilizza più operazioni in virgola mobile) e sarà quasi sempre meno preciso.

Se hai meno di 460 esempi, ciò che vuoi fare è diagonalizzare la matrice di dispersione (x - datamean) ^ T (x - mean), assumendo che i tuoi punti dati siano colonne, e quindi moltiplicando a sinistra per (x - datamean). Questo potrebbe essere più veloce nel caso in cui si ha più dimensioni di dati.


puoi descrivere più in dettaglio questo trucco quando hai più dimensioni che dati?
mrgloom

1
Fondamentalmente si assume che gli autovettori siano combinazioni lineari dei vettori di dati. Vedi Sirovich (1987). "Turbolenza e dinamica delle strutture coerenti".
dwf

11

Puoi facilmente "rotolare" il tuo usando scipy.linalg(assumendo un set di dati precentrato data):

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

Allora evssono i tuoi autovalori ed evmatè la tua matrice di proiezione.

Se vuoi mantenere le ddimensioni, usa i primi dautovalori e primi dautovettori.

Dato che scipy.linalgha la decomposizione e numpy le moltiplicazioni di matrici, cos'altro ti serve?


cov matrix è np.dot (data.T, data, out = covmat), dove i dati devono essere matrice centrata.
mrgloom

2
Dovresti dare un'occhiata al commento di @ dwf su questa risposta per i pericoli dell'utilizzo eig()su una matrice di covarianza.
Alex A.

8

Ho appena finito di leggere il libro Machine Learning: An Algorithmic Perspective . Tutti gli esempi di codice nel libro sono stati scritti da Python (e quasi con Numpy). Lo snippet di codice di chatper10.2 Analisi dei componenti principali forse merita una lettura. Usa numpy.linalg.eig.
A proposito, penso che SVD possa gestire molto bene le dimensioni 460 * 460. Ho calcolato un SVD 6500 * 6500 con numpy / scipy.linalg.svd su un PC molto vecchio: Pentium III 733mHz. Ad essere onesti, lo script richiede molta memoria (circa 1.xG) e molto tempo (circa 30 minuti) per ottenere il risultato SVD. Ma penso che 460 * 460 su un PC moderno non sarà un grosso problema a meno che non sia necessario eseguire SVD un numero enorme di volte.


28
Non dovresti mai usare eig () su una matrice di covarianza quando puoi semplicemente usare svd (). A seconda del numero di componenti che prevedi di utilizzare e della dimensione della tua matrice di dati, l'errore numerico introdotto dal primo (esegue più operazioni in virgola mobile) può diventare significativo. Per lo stesso motivo non dovresti mai invertire esplicitamente una matrice con inv () se ciò che ti interessa veramente sono i tempi inversi di un vettore o di una matrice; dovresti invece usare risolvere ().
dwf

5

Non è necessaria la decomposizione dei valori singolari (SVD) completa in quanto calcola tutti gli autovalori e gli autovettori e può essere proibitiva per matrici di grandi dimensioni. scipy e il suo modulo sparse forniscono funzioni algrebra lineari generiche che lavorano su matrici sia sparse che dense, tra le quali vi è la famiglia di funzioni eig *:

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

Scikit-learn fornisce un'implementazione Python PCA che per ora supporta solo matrici dense.

Tempistiche:

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

1
Non proprio un confronto equo, poiché è ancora necessario calcolare la matrice di covarianza. Inoltre, probabilmente vale la pena usare la roba linalg sparsa solo per matrici molto grandi, poiché sembra essere piuttosto lento costruire matrici sparse da matrici dense. ad esempio, eigshè in realtà ~ 4x più lento rispetto eighalle matrici non sparse. Lo stesso vale per il scipy.sparse.linalg.svdsversus numpy.linalg.svd. Vorrei sempre andare con SVD sulla decomposizione degli autovalori per i motivi menzionati da @dwf, e forse usare la versione sparsa di SVD se le matrici diventano davvero enormi.
ali_m

2
Non è necessario calcolare matrici sparse da matrici dense. Gli algoritmi forniti nel modulo sparse.linalg si basano esclusivamente sull'operazione di moltiplicazione del vettore matrice tramite il metodo matvec dell'oggetto Operator. Per matrici dense, questo è solo qualcosa come matvec = punto (A, x). Per lo stesso motivo, non è necessario calcolare la matrice di covarianza ma solo fornire l'operazione punto (AT, punto (A, x)) per A.
Nicolas Barbey

Ah, ora vedo che la velocità relativa dei metodi sparse vs non sparse dipende dalla dimensione della matrice. Se uso il tuo esempio in cui A è una matrice 1000 * 1000, eigshe svdssono più veloci di eighe svddi un fattore ~ ​​3, ma se A è più piccolo, diciamo 100 * 100, allora eighe svdsono più veloci rispettivamente di ~ 4 e ~ 1,5 . Tuttavia, T userebbe ancora SVD sparse su decomposizione di autovalori sparse.
ali_m

2
In effetti, penso di essere orientato verso matrici di grandi dimensioni. Per me le matrici di grandi dimensioni sono più come 10⁶ * 10⁶ che 1000 * 1000. In questi casi, spesso non è nemmeno possibile memorizzare le matrici di covarianza ...
Nicolas Barbey

4

Ecco un'altra implementazione di un modulo PCA per Python che utilizza numpy, scipy e estensioni C. Il modulo esegue la PCA utilizzando un algoritmo SVD o NIPALS (Nonlinear Iterative Partial Least Squares) implementato in C.


0

Se stai lavorando con vettori 3D, puoi applicare SVD in modo conciso usando il toolbelt vg . È uno strato leggero sopra numpy.

import numpy as np
import vg

vg.principal_components(data)

C'è anche un comodo alias se vuoi solo il primo componente principale:

vg.major_axis(data)

Ho creato la libreria al mio ultimo avvio, dove era motivata da usi come questo: idee semplici che sono prolisse o opache in NumPy.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.