Python中的主成分分析

115
我想使用主成分分析(PCA)进行降维。NumPy或SciPy中已经有了它吗?还是我需要使用numpy.linalg.eigh自己编写代码?
我不想只使用奇异值分解(SVD),因为我的输入数据非常高维(~460个维度),所以我认为计算协方差矩阵的特征向量会比SVD更快。
我希望找到一个现成的、调试过的实现,它已经做出了正确的决策,何时使用哪种方法,并且可能做出了我不知道的其他优化。
11个回答

66
几个月后,这里有一个小的主成分分析类(PCA),以及一张图片:
#!/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

enter image description here


3
Fyinfo,这里有一个关于鲁棒主成分分析(Robust PCA)的精彩演讲视频,演讲者是C. Caramanis,时间是2011年1月。链接 - denis
这段代码会输出那张图片(Iris PCA)吗?如果不是,你能否提供另一种解决方案,使得输出结果是那张图片。我因为刚接触Python而在将这段代码转换成C++时遇到了一些困难 :) - Orvyl

45

使用numpy.linalg.svd进行主成分分析非常容易。以下是一个简单的演示:

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

3
我明白我来晚了,但该帖的发起人特别请求避免使用奇异值分解的解决方案。 - Alex A.
2
@Alex 我知道这一点,但我坚信SVD仍然是正确的方法。它应该足够快以满足OP的需求(我的上面的例子,具有262144个维度,在普通笔记本电脑上只需要约7.5秒),而且比特征分解方法更加数值稳定(请参见下面dwf的评论)。我还注意到,被接受的答案也使用了SVD! - ali_m
我并不反对使用SVD,我只是想说这个答案并没有回答问题。不过这是一个很好的答案,做得很好。 - Alex A.
6
好的。我认为这是“XY问题”的另一种变体 - 问问题的人说他不想使用基于奇异值分解(SVD)的解决方案,因为他认为SVD可能会太慢,而实际上可能还没有尝试过。在这种情况下,我个人认为更有帮助的是解释如何解决更广泛的问题,而不是仅仅回答原来较狭窄形式的问题。 - ali_m
根据文档,svd函数已经按降序排列返回s。也许在2012年不是这样,但现在是这样的。 - Etienne Bruines

35

你可以使用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_)))

因为这对我非常有效,所以我点了赞 - 我有超过460个维度,即使sklearn使用SVD而问题要求非SVD,但我认为460个维度可能是可以的。 - Dan Stowell
你可能还想删除具有常量值(std=0)的列。 为此,您应该使用: remove_cols = np.where(np.all(x == np.mean(x, 0), 0))[0] 然后 x = np.delete(x, remove_cols, 1) - Noam Peled

31

5
matplotlib的PCA链接已更新。 - Developer
3
matplotlib.mlab实现PCA使用SVD。 - Aman
3
这里是其功能和使用方法的更详细描述 - Dolan Antenucci

28

你可以看一下MDP

我自己还没有测试过,但我已经收藏了它,因为它具有PCA功能。


8
自2012年以来,MDP没有被维护,看起来不是最佳解决方案。 - Marc Garcia
最新更新日期为2016年9月3日,但请注意这只是一个错误修复版本:“请注意,从此版本开始,MDP处于维护模式。在首次公开发布13年后,MDP已经达到了完全成熟的状态,并且未来不会计划添加新功能。” - Gabriel

14

SVD 在 460 维下表现良好。在我的 Atom netbook 上大约需要 7 秒钟的时间。eig() 方法需要更多的时间(因为它使用更多的浮点运算),并且几乎总是不太精确。

如果你的数据点少于 460 个,那么你需要对散布矩阵进行对角化(x - datamean)^T(x - mean),假设你的数据点是列向量,然后通过左乘 (x - datamean) 来得到结果。在你的维度比数据点多的情况下,这可能会更快。


当您的维度比数据更多时,您能否更详细地描述这个技巧? - mrgloom
1
基本上你假设特征向量是数据向量的线性组合。请参阅Sirovich(1987)的“湍流和连贯结构的动力学”。 - dwf

11

你可以使用scipy.linalg相对容易地"滚动"自己的数据(假设数据集data已经过中心化):

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

那么evs是您的特征值,evmat是您的投影矩阵。

如果您想保留d个维度,请使用前d个特征值和前d个特征向量。

考虑到scipy.linalg提供了分解功能,而numpy则提供了矩阵乘法,您还需要什么呢?


协方差矩阵为np.dot(data.T,data,out=covmat),其中data必须是中心化矩阵。 - mrgloom
2
你应该看一下@dwf在这个答案中对使用eig()处理协方差矩阵的风险所发表的评论。 - Alex A.

8

我刚刚读完了一本名为《机器学习:算法视角》的书。书中的所有代码示例都是用Python编写的(几乎都用了Numpy)。其中第10.2章 主成分分析的代码片段值得一读,它使用了numpy.linalg.eig。
顺便说一下,我认为奇异值分解(SVD)可以很好地处理460*460维度的数据。我曾在一台非常老旧的PC上(Pentium III 733mHz)使用numpy/scipy.linalg.svd计算了一个6500*6500的SVD。老实说,这个脚本需要大量的内存(约1.xG)和时间(约30分钟)才能得到SVD结果。但我认为在现代PC上进行460*460的计算不会是一个大问题,除非你需要大量地进行SVD计算。


29
当你可以简单地使用svd()时,不应该对协方差矩阵使用eig()。根据你计划使用的成分数量和数据矩阵的大小,前者引入的数值误差(它执行更多的浮点运算)可能会变得显著。出于同样的原因,如果你真正感兴趣的是逆乘以向量或矩阵,而不是明确地求逆一个矩阵,那么你不应该使用inv(),而应该使用solve()。 - dwf

5
你不需要完整的奇异值分解(SVD),因为它计算所有特征值和特征向量,对于大矩阵来说可能会受限。scipy及其稀疏模块提供了通用的线性代数函数,可在稀疏和密集矩阵上运行,其中包括eig*系列函数:

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

Scikit-learn提供了一个Python PCA实现, 目前只支持密集矩阵。

时间:

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
这并不是一个公平的比较,因为你仍然需要计算协方差矩阵。此外,只有在矩阵非常大时,才值得使用稀疏线性代数库,因为从密集矩阵构造稀疏矩阵似乎相当慢。例如,对于非稀疏矩阵,eigsh实际上比eigh慢了约4倍。对于scipy.sparse.linalg.svdsnumpy.linalg.svd也是如此。我总是会选择SVD而不是特征值分解,原因就像@dwf提到的那样,并且如果矩阵变得非常巨大,可能会使用SVD的稀疏版本。 - ali_m
2
你不需要从稠密矩阵中计算出稀疏矩阵。在sparse.linalg模块中提供的算法仅依赖于通过Operator对象的matvec方法执行的矩阵向量乘法操作。对于稠密矩阵而言,这只是类似于matvec=dot(A, x)的操作。出于同样的原因,你也不需要计算协方差矩阵,而只需要为A提供操作dot(A.T, dot(A, x))即可。 - Nicolas Barbey
啊,现在我明白了,稀疏和非稀疏方法的相对速度取决于矩阵的大小。如果我使用您的示例,其中A是一个10001000的矩阵,则eigshsvdseighsvd快约3倍,但如果A更小,比如100100,则eighsvd分别快4倍和1.5倍左右。不过,我仍然会使用稀疏SVD而不是稀疏特征值分解。 - ali_m
2
实际上,我认为我对大矩阵有偏见。对我而言,大矩阵更像是10⁶ * 10⁶而不是1000 * 1000。在这些情况下,你甚至都无法存储协方差矩阵... - Nicolas Barbey

4

这里提供了另一种使用numpy、scipy和C扩展库实现PCA模块的Python代码。该模块可以利用SVD或NIPALS(非线性迭代偏最小二乘)算法进行PCA计算,其中NIPALS算法已经在C语言中实现。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接