使用SVD奇异值分解求解PCA+Python实现

这几天在看有关PCA的博客时,感觉文章中针对如何用SVD解PCA的过程的讲解不是很清晰,自己捋了捋思路,把自己对于SVD解PCA的步骤分享出来

关于PCA的思想和原理,这里不阐述,推荐这篇文章,当初就是看这篇文章入坑PCA的
http://blog.codinglabs.org/articles/pca-tutorial.html

1.直接解PCA

设有n组m维数据,组成 m*n 的矩阵 X m ∗ n \Chi_{m*n} Xmn , 则 在去中心化后, X \Chi X 的协方差矩阵
            C \mathcal{C} C = 1 n \frac{1}{n} n1 X \Chi X X T \Chi^{T} XT
目标:将 C \mathcal{C} C 对角化
如何对角化 C \mathcal{C} C
P \mathcal{P} P是正交基按行组成的矩阵(最终希望得到的转换矩阵, P \mathcal{P} P的前 k \mathcal{k} k行就是要寻找的基,可对 X \Chi X降维),则 Y \mathcal{Y} Y= P \mathcal{P} P X \Chi X中, Y \mathcal{Y} Y X \Chi X P \mathcal{P} P做基变换得到的结果
Y \mathcal{Y} Y 的协方差矩阵 D \mathcal{D} D = 1 n \frac{1}{n} n1 Y \mathcal{Y} Y Y T \mathcal{Y}^{T} YT = P \mathcal{P} P C \mathcal{C} C P T \mathcal{P}^{T} PT 应该是一个对角阵。

已知知道一个 m ∗ \mathcal{m}* m m \mathcal{m} m的实对称矩阵一定可以找到 m \mathcal{m} m个正交特征向量,特征向量组成的矩阵为 E \mathcal{E} E

则有   E T \mathcal{E}^{T} ET C \mathcal{C} C E \mathcal{E} E = Λ \Lambda Λ,对比上式,可以得到   P \mathcal{P} P = E T \mathcal{E}^{T} ET

即我们现在的目标是求出 C \mathcal{C} C的特征向量矩阵,并将其转置

但是,高维数据的协方差矩阵非常庞大,比如单位样本是一副100*100的uint8图像,则一副图像就要表示为一个 10000 * 1 的列向量,当样本数量少于10000时,协方差矩阵的大小为10000 *10000(800m左右),计算机求解这个矩阵会非常困难,我尝试过,用Matlab求解要用5分钟左右的时间,如果图像更大,比如时 70000 * 1 的情况,则协方差矩阵大小达到45G,求解成为了几乎不可能的事情

这时候我们就要借助一个工具 SVD特征值分解,来曲线求解协方差矩阵的特征值矩阵

2. 用SVD特征值分解来解PCA

这里不阐述SVD的原理,同样我推荐这篇文章
https://mp.weixin.qq.com/s/Dv51K8JETakIKe5dPBAPVg

X \Chi X经过分解后,可以得到 
        X m ∗ n \Chi_{m*n} Xmn = U m ∗ m \mathcal{U}_{m*m} Umm Σ m ∗ n \Sigma_{m*n} Σmn V n ∗ n T \mathcal{V}^{T}_{n*n} VnnT

其中 U m ∗ m \mathcal{U}_{m*m} Umm X \Chi X X T \Chi^{T} XT的特征向量按列组成的矩阵
V n ∗ n \mathcal{V}_{n*n} Vnn X T \Chi^{T} XT X \Chi X的特征向量按列组成的矩阵
Σ m ∗ n \Sigma_{m*n} Σmn的对角线上是奇异值 σ \sigma σ,奇异值与特征值的关系为 σ 2 \sigma^{2} σ2 = λ \lambda λ

按以上的性质,可以得到 P \mathcal{P} P = U T \mathcal{U}^{T} UT
所以我们现在的目标是求出 U \mathcal{U} U

SVD的分解思想是
        X m ∗ n \Chi_{m*n} Xmn ≈ \approx U m ∗ r \mathcal{U}_{m*r} Umr Σ r ∗ r \Sigma_{r*r} Σrr V r ∗ n T \mathcal{V}^{T}_{r*n} VrnT

我们现在将 X m ∗ n \Chi_{m*n} Xmn近似为
X m ∗ n \Chi_{m*n} Xmn ≈ \approx U m ∗ n \mathcal{U}_{m*n} Umn Σ n ∗ n \Sigma_{n*n} Σnn V n ∗ n T \mathcal{V}^{T}_{n*n} VnnT (对于n <= m)
V n ∗ n \mathcal{V}_{n*n} Vnn的特征值矩阵为 Σ n ∗ n 2 \Sigma_{n*n}^{2} Σnn2

所以我们可以求得
        U m ∗ n \mathcal{U}_{m*n} Umn = X m ∗ n \Chi_{m*n} Xmn ( Σ n ∗ n (\Sigma_{n*n} (Σnn V n ∗ n ) − 1 \mathcal{V}_{n*n})^{-1} Vnn)1
从而就可以依据特征值大小将 U m ∗ n \mathcal{U}_{m*n} Umn的特征值排序( U \mathcal{U} U V \mathcal{V} V拥有相同的特征值),就可以降维至 U m ∗ k \mathcal{U}_{m*k} Umk,再将其转置就可以得到变换矩阵 P \mathcal{P} P

这时可能会有疑问
U m ∗ m \mathcal{U}_{m*m} Umm V n ∗ n \mathcal{V}_{n*n} Vnn维数不一样,为什么特征值个数是min{m, n},其实,若直接对前者进行特征值求解,排序后,会发现,前者只有前n个特征值是实数,后面都是虚数

可以看到,在用SVD求解PCA时,我们只需要计算 V n ∗ n \mathcal{V}_{n*n} Vnn的特征值和特征向量,通常情况下,数据样本 n 是远小于数据维数 m 的,因此计算 n*n 矩阵的特征值和特征向量,将可以大大减小计算量,SVD“曲线救国”的目的也就达到了

3.Python实现PCA

在实际进行PCA的求解中,需要根据数据的维度与样本数来进行直接求解PCA与用SVD求解PCA的权衡

import numpy as np

class PCA():
    """
    PCA降维
    """
    def __init__(self):
        self.X = None
        self.k = None

    def getTransMat(self, X, k, svd):
        """
        训练得到特征空间中的主成分转换矩阵
        args:
        X -- 训练数据 shape=(features, samples)
        k -- 要降维的维度,若小于0,则自动选择, 选择阈值为特征值总和95%
        svd -- 是否使用svd求解PCA, true or false 如果训练样本数多于特征数,选择直接解更省内存(faster),反之选择svd奇异值分解更好
        Returns:
        transMat -- 转换矩阵 shape=(降维后维度, 降维前维度)
        """
        # 将X的每一维进行零均值化
        self.X = X
        self.k = k
        m = np.mean(self.X, 1)
        m = np.array([m])
        self.X = self.X - m.T

        if svd:
            # 用SVD特征值分解解出变换矩阵
            # 求出 X' * X 的特征值矩阵V,并求出奇异值对角阵Σ(奇异值从大到小排序)
            C = np.dot(self.X.T, self.X)

            # 求出 X' * X 的特征值矩阵及对应的特征向量和奇异值矩阵
            [eigValue, V] = np.linalg.eig(C)
            eigValue = abs(np.array([eigValue]))
            sinValue = np.sqrt(eigValue)
            temp = np.zeros([np.size(sinValue, 1), np.size(sinValue, 1)])
            for i in range(0, np.size(sinValue, 1)):
                temp[i, i] = sinValue[0, i]
            sinValue = temp

            # 将特征向量归一化
            V = V / np.linalg.norm(V, axis=0)

            # 求出变换矩阵
            U = np.dot(self.X, np.linalg.inv(np.dot(sinValue, V.T)))
            U = U.T
        
        else:
            # 直接解PCA
            C = np.dot(self.X, self.X.T)
            [eigValue, V] = np.linalg.eig(C)
            eigValue = np.array([eigValue])

            # 将特征向量归一化
            V = V / np.linalg.norm(V, axis=0)

            # 求出变换矩阵
            U = V.T

        # 按特征值大小给变换矩阵行向量重新排序
        index = np.argsort(-eigValue)
        eigValue.tolist()[0].reverse()
        temp = U.copy()
        for i in range(0, np.size(U, 0)):
            U[i, :] = temp[index[0][i], :]

        # 自动选择降维维度
        self.autoChooseDim(eigValue)

        # 获得降维变换矩阵
        transMat = U[0:self.k, :]
        return np.real(transMat)
        
    def autoChooseDim(self, sortV):
        """
        自适应选择降维维度
        args:
        sortV -- 已经排好序的特征值序列
        returns:
        None
        """
        if self.k <= 0:
            threshold = sum(sum(sortV)) * 0.95
            total = 0
            for i in range(0, len(sortV[0])):
                total += sortV[0][i]
                if total > threshold:
                    break
            self.k = i
        else:
            pass

if __name__ == "__main__":
    pass
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值