Pythonでの主成分分析


112

次元削減のために主成分分析(PCA)を使用したいと思います。numpyまたはscipyはすでにそれを持っていますか、それとも自分で自分でロールする必要がありnumpy.linalg.eighますか?

入力データが非常に高次元(約460次元)であるため、特異値分解(SVD)を使用したくないだけなので、SVDは共分散行列の固有ベクトルを計算するよりも遅くなると思います。

私は、どの方法をいつ使用するか、そしておそらく私が知らない他の最適化をいつ行うかについて正しい決定をすでに行っている、事前に作成されデバッグされた実装を見つけることを望んでいました。

回答:


28

あなたはMDPを見ているかもしれません。

自分でテストする機会はありませんでしたが、PCA機能のために正確にブックマークしました。


8
MDPは2012年以降維持されておらず、最適なソリューションのようには見えません。
Marc Garcia、

最新のアップデートは、IRのみのバグ修正リリースであることを2016年9月3日からですが、ノートの操作を行います。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.
ガブリエル

65

数か月後、ここに小さなクラスの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

ここに画像の説明を入力してください


3
Fyinfo、2011
denis

このコードはその画像(Iris PCA)を出力しますか?そうでない場合は、その画像が出力される別のソリューションを投稿できますか。私はpythonで新しいため、このコードをc ++に変換するのにいくつかの問題があります:)
Orvyl

44

PCAの使用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()

2
私はここで少し遅れていることに気づきましたが、OP 特異値分解を回避するソリューションを具体的に要求しました。
Alex A.

1
@アレックス私はそれを理解していますが、SVDが依然として正しいアプローチであると確信しています。これは、OPのニーズに対して十分に速く(上記の私の例、262144の寸法は通常のラップトップで約7.5秒しかかかりません)、固有分解法よりも数値的にはるかに安定しています(以下のdwfのコメントを参照)。また、受け入れられた回答はSVDも使用していることにも注意してください。
ali_m

SVDが進むべき道であることに私は異論はありません。質問が述べられているので、答えは質問を扱っていないというだけでした。良い答えですが、いい仕事です。
Alex A.

5
@アレックスフェア十分。これはXY問題の別のバリアントだと思います-OPは、SVD が遅すぎると思ったため、おそらくまだ試していないため、SVDベースのソリューションは望んでいないと述べました。このような場合、私は個人的に、質問を元の狭い形で正確に答えるのではなく、より広い問題に取り組む方法を説明する方が役立つと思います。
ali_m 2015

svdsドキュメントに関する限り、すでに降順でソートされて返されます。(おそらくこれは2012年には
当てはまり

34

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、2013年

定数値(std = 0)の列を削除することもできます。そのためには、以下を使用する必要があります。remove_cols= np.where(np.all(x == np.mean(x、0)、0))[0]そして、x = np.delete(x、remove_cols、1)
Noam Peled


14

SVDは460次元で正常に動作するはずです。Atomネットブックで約7秒かかります。EIG()メソッドは、かかる複数(それは、それがより多くの浮動小数点演算を使用しなければならないように)時間を、ほぼ常にあまり正確であろう。

例が460未満の場合は、散布行列(x-データ平均)^ T(x-平均)を対角化し、データポイントが列であると仮定して、(x-データ平均)で左乗算します。データよりも多くのディメンションがある場合、それより速いかもしれません


データよりも多くのディメンションがある場合、このトリックを詳細に説明できますか?
mrgloom

1
基本的に、固有ベクトルはデータベクトルの線形結合であると想定します。Sirovich(1987)を参照してください。「乱流とコヒーレント構造のダイナミクス。」
dwf 2014年

11

scipy.linalg(事前に中央揃えされたデータセットを想定している)を使用して、非常に簡単に独自の「ロール」を行うことができますdata

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

次に、evsは固有値であり、evmatは投影行列です。

d次元を維持する場合は、最初のd固有値と最初のd固有ベクトルを使用します。

それscipy.linalgが分解を持ち、行列の乗算をナンピーした場合、他に何が必要ですか?


cov行列はnp.dot(data.T、data、out = covmat)です。ここで、データは中央の行列でなければなりません。
mrgloom

2
共分散行列で使用することの危険性については、この回答に関する@dwfのコメントをご覧くださいeig()
Alex A.

8

私は「機械学習:アルゴリズムの視点」という本を読み終えました。本のすべてのコード例はPythonによって(そしてほとんどがNumpyを使って)作成されました。chatper10.2主成分分析のコードスニペットは、読む価値があるかもしれません。numpy.linalg.eigを使用します。
ところで、SVDは460 * 460の寸法を非常にうまく処理できると思います。非常に古いPC:Pentium III 733mHzでnumpy / scipy.linalg.svdを使用して6500 * 6500 SVDを計算しました。正直なところ、SVDの結果を取得するには、スクリプトに大量のメモリ(約1.xG)と大量の時間(約30分)が必要です。しかし、現代のPCの460 * 460は、SVDを何度も実行する必要がない限り、大きな問題にはならないと思います。


28
単純にsvd()を使用できる場合は、共分散行列に対してeig()を使用しないでください。使用を計画しているコンポーネントの数とデータマトリックスのサイズによっては、前者(より多くの浮動小数点演算を行う)によって導入された数値誤差が大きくなる可能性があります。同じ理由で、本当に関心があるのがベクトルまたは行列の逆数である場合は、inv()を使用して行列を明示的に反転しないでください。代わりにsolve()を使用してください。
dwf 2009年

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
共分散行列を計算する必要があるため、実際には公平な比較ではありません。また、密な行列から疎な行列を作成するのは非常に遅いように思われるため、非常に大きな行列に対して疎なlinalgのものを使用する価値があるだけでしょう。たとえば、eigsh実際にはeigh非スパース行列よりも〜4倍遅くなります。scipy.sparse.linalg.svds対についても同じことが言えますnumpy.linalg.svd。@dwfが言及した理由により、私は常に固有値分解よりもSVDを使用し、行列が非常に大きくなる場合は、SVDのスパースバージョンを使用する可能性があります。
ali_m

2
密行列から疎行列を計算する必要はありません。sparse.linalgモジュールで提供されるアルゴリズムは、Operatorオブジェクトのmatvecメソッドによる行列ベクトル乗算演算のみに依存します。密な行列の場合、これはmatvec = dot(A、x)のようなものです。同じ理由で、あなたは、共分散行列を計算する必要はありませんが、唯一のAの操作ドット(AT、ドット(A、X))を提供する
ニコラ・バービー

ああ、今では、スパースメソッドと非スパースメソッドの相対速度が行列のサイズに依存していることがわかります。私はあなたの例を使用している場合はAはその後、1000年×1000行列であるeigshsvdsより速いですeighsvd約3倍、しかしAが小さい場合は、その後、* 100 100を言うeighsvd、それぞれ〜4と〜1.5の要因によって迅速です。ただし、スパース固有値分解ではスパースSVDを使用します。
ali_m

2
確かに、私は大きな行列に偏っていると思います。私にとって、大きな行列は1000 * 1000よりも10⁶*10⁶に近いものです。その場合、共分散行列を保存することさえできません...
Nicolas Barbey

4

これは、numpy、scipy、およびC-extensionsを使用したPython用のPCAモジュールの別の実装です。このモジュールは、Cで実装されているSVDまたはNIPALS(非線形反復部分最小二乗)アルゴリズムを使用してPCAを実行します。


0

3Dベクトルを使用している場合は、toolbelt vgを使用してSVDを簡潔に適用できます。numpyの上にある軽いレイヤーです。

import numpy as np
import vg

vg.principal_components(data)

最初の主成分のみが必要な場合にも便利なエイリアスがあります。

vg.major_axis(data)

私は最後のスタートアップでライブラリを作成しました。このライブラリは、NumPyで冗長または不透明なシンプルなアイデアのような使用によって動機付けられました。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.