主成分分析(Principle Component Analysis,PCA)是常用的一种矩阵分解算法,PCA通过旋转原始空间来使得数据在各个正交轴上的投影最大,通过选择前几个正交轴可以实现数据降维的目的。
PCA数学原理 优化问题 PCA的优化问题如下:
arg max W t r a c e ( W T X X T W ) s.t. W T W = I \begin{equation} \begin{split} \argmax_{\mathbf{W}}\quad &\mathrm{trace}\left(\mathbf{W}^T\mathbf{X}\mathbf{X}^T\mathbf{W}\right)\\ \textrm{s.t.}\quad &\mathbf{W}^T\mathbf{W} = \mathbf{I} \end{split} \end{equation} W arg max s.t. trace ( W T X X T W ) W T W = I
其中X ∈ R M × N \mathbf{X} \in \mathbb{R}^{M \times N} X ∈ R M × N 是数据,W ∈ R M × M \mathbf{W} \in \mathbb{R}^{M \times M} W ∈ R M × M 是投影矩阵,N N N 是样本点个数,M M M 是特征个数。PCA要求数据X \mathbf{X} X 做零均值处理,优化问题的解可以转化为如下特征值分解问题的解:
( X X T ) W = W Λ \begin{equation} \left(\mathbf{X}\mathbf{X}^T\right)\mathbf{W} = \mathbf{W}\mathbf{\Lambda} \end{equation} ( X X T ) W = WΛ
这里假设W \mathbf{W} W 的列向量按相应特征值的大小从大到小排列,保留W \mathbf{W} W 前K列即前K个成分的列向量W ^ \hat{\mathbf{W}} W ^ ,降维后的数据特征为:
X ^ = W ^ T X \begin{equation} \hat{\mathbf{X}} = \hat{\mathbf{W}}^T\mathbf{X} \end{equation} X ^ = W ^ T X
其中X ^ ∈ R K × N \hat{\mathbf{X}} \in \mathbb{R}^{K \times N} X ^ ∈ R K × N 。
实现分析 svd替代eig sklearn中的PCA 实现并未使用eig而是使用svd,主要原因是svd比eig具有更好的数值稳定性(当然代价是其计算时间要比eig更长)。使用svd代替eig也是很多学者如Andrew Ng建议的策略,在StackExchange上也有关于svd和eig的相关讨论讨论1 、讨论2 。sklearn中直接对数据矩阵X \mathbf{X} X 而不是协方差矩阵X X T \mathbf{X}\mathbf{X}^T X X T 做svd,其等价关系如下:
X = U Σ V T X X T = U Σ 2 U T W = U Λ = Σ 2 \begin{equation} \begin{split} \mathbf{X} &= \mathbf{U} \mathbf{\Sigma} \mathbf{V}^T\\ \mathbf{X}\mathbf{X}^T &= \mathbf{U} \mathbf{\Sigma}^2 \mathbf{U}^T\\ \mathbf{W} &= \mathbf{U}\\ \mathbf{\Lambda} &= \mathbf{\Sigma}^2 \end{split} \end{equation} X X X T W Λ = UΣ V T = U Σ 2 U T = U = Σ 2
sign ambiguity问题 sklearn的PCA代码中还考虑了svd的sign ambiguity问题,即每个奇异向量的符号在求解过程中是不确定的(例如,将u k \mathbf{u}_k u k 和v k \mathbf{v}_k v k 同时乘以-1也满足求解条件)。svd算法(包括eig)中的奇异向量符号只是确保数值稳定性的副产品,类似随机分配符号,并无实际意义。
sklearn使用svd_flip(u, v, u_based_descision=True)
函数来确保输出确定性的奇异向量符号,例如,如果u_based_decision=True
,则要求U \mathbf{U} U 的每一列奇异向量中绝对值最大的元素的符号始终为正,V \mathbf{V} V 也要相对的做出调整。
鉴于MATLAB是算法开发的标准之一,我很好奇MATLAB是如何处理SVD的sign ambiguity问题的。MATLAB的svd函数的官方文档 中有这样一句话:
Different machines and releases of MATLAB® can produce different singular vectors that are still numerically accurate. Corresponding columns in U and V can flip their signs, since this does not affect the value of the expression A = US V’.
MATLAB的eig函数的官方文档 中亦提到:
For real eigenvectors, the sign of the eigenvectors can change.
可以看出MATLAB也未保证符号的确定性。同样在MATALB的社区里也有人问了这个问题 ,并引导我看了这篇Resolving the Sign Ambiguity in the Singular Value Decompostion 的文献。
文献中指出,sklearn的svd_flip
方法是一种临时方案(ad hoc),并未从数据分析或者解释的角度来解决sign ambiguity问题。解决sign ambiguity的核心是如何为奇异向量选择一个“有意义”的符号。什么叫“有意义”?比方说我们要研究4种品牌汽车的每公里耗油量,做了4次抽样,构成数据矩阵:
X = [ 4 22 3 5 1 5 1 1 11 69 10 14 11 69 10 14 ] \begin{equation} \begin{split} \mathbf{X} = \begin{bmatrix} 4 &22&3 &5 \\ 1 &5 &1 &1 \\ 11&69&10&14 \\ 11&69&10&14 \\ \end{bmatrix} \end{split} \end{equation} X = 4 1 11 11 22 5 69 69 3 1 10 10 5 1 14 14
其中每一列是一种品牌汽车的耗油量,每一行为抽样情况,svd分解有X = U Σ V T \mathbf{X}=\mathbf{U} \mathbf{\Sigma} \mathbf{V}^T X = UΣ V T 。我们来看一下numpy.linalg.svd
计算得到的V \mathbf{V} V 的第一个奇异向量:
v 0 = [ − 0.15 − 0.96 − 0.14 − 0.20 ] \begin{equation} \begin{split} \mathbf{v}_0 = \begin{bmatrix} -0.15 \\ -0.96 \\ -0.14 \\ -0.20 \\ \end{bmatrix} \end{split} \end{equation} v 0 = − 0.15 − 0.96 − 0.14 − 0.20
v 0 \mathbf{v}_0 v 0 实际指明了耗油量空间的一个向量,然而我们知道耗油量没有负值(如果有的话人类就拥有无限能源了),一个完全指向负的方向意义不大,如果改变v 0 \mathbf{v}_0 v 0 的符号,变为:
v 0 = [ 0.15 0.96 0.14 0.20 ] \begin{equation} \begin{split} \mathbf{v}_0 = \begin{bmatrix} 0.15 \\ 0.96 \\ 0.14 \\ 0.20 \\ \end{bmatrix} \end{split} \end{equation} v 0 = 0.15 0.96 0.14 0.20
结果就合理多了。
文献中指出,奇异向量的符号应当与大多数数据样本向量的符号相同 ,从几何上来看,奇异向量应当指向大多数向量指向的方向 。下图是我从文献中截取的,深色蓝线是正确的奇异向量方向,浅色蓝线是数据向量。
翻译成数学语言(我按照自己的理解和习惯转化成优化问题,与文献的原始表述并不一致,不一定对,有兴趣的读者可以看原始文献 😃),纠正符号算法的核心是对于每一对奇异向量u k \mathbf{u}_k u k 和v k \mathbf{v}_k v k ,寻找符号s k s_k s k 优化以下目标函数
arg max s k ∈ { 1 , − 1 } s k ( ∑ j = 1 N u k T X ⋅ , j + ∑ i = 1 M X i , ⋅ v k ) \begin{equation} \argmax_{s_k \in \{1,-1\}}\quad s_k \left(\sum_{j=1}^N \mathbf{u}_k^T\mathbf{X}_{\cdot,j} + \sum_{i=1}^M\mathbf{X}_{i,\cdot}\mathbf{v}_k\right) \end{equation} s k ∈ { 1 , − 1 } arg max s k ( j = 1 ∑ N u k T X ⋅ , j + i = 1 ∑ M X i , ⋅ v k )
根据两项求和项的符号即可决定s k s_k s k 的符号。对于可能存在的左右奇异向量符号冲突的情况(例如单独看左奇异向量有意义的符号是-1,单独看右奇异向量有意义的符号为1),该算法选择以求和绝对值最大的一项的符号为主(反应在上式就是两项求和)。文献中指出,该算法仅在上述求和项不为0的情况下有效(即在0附近奇异向量的符号可以为任意情况)。
Python版具体算法实现如下,Matlab可以使用这个版本 :
import warningsimport numpy as npdef sign_flip (u, s, vh=None ): """Flip signs of SVD or EIG. """ left_proj = 0 if vh is not None : left_proj = np.sum (s[:, np.newaxis]*vh, axis=-1 ) right_proj = np.sum (u*s, axis=0 ) total_proj = left_proj + right_proj signs = np.sign(total_proj) random_idx = (signs==0 ) if np.any (random_idx): signs[random_idx] = 1 warnings.warn("The magnitude is close to zero, the sign will become arbitrary." ) u = u*signs if vh is not None : vh = signs[:, np.newaxis]*vh return u, s, vh