原文没有写版权声明,所以我对原作者进行了署名,并且并非进行商业活动。

不过鉴于原文没有注明不得增加额外限制,所以读者使用本翻译文本需要遵守署名-非商业性使用-相同方式共享 BY-NC-SA的协议。

原文的作者: Nov 16, 2019 • Casey Juanxi Li

翻译: Nov 19, 2019 • VCVina

原文链接https://notsquirrel.com/pca/

PCA是我在学数据分析的时候一头雾水的门类,不过那是去年的事了。

今天我在复习PCA的时候,偶然找到自己喜欢的科普博主@3Blue1Brown 转发了一个博客,就是有关这个的,我发现写的蛮好的,于是一边学一边翻译,可能有疏漏,以后会改的。

图像的协方差矩阵

什么是图像的协方差矩阵?让我们开始有关这个问题的探讨。
假设我们有一个X,这其实是一个图像,只不过被从D × D压缩为了1 × D²。这个过程就如下文的GIF演示一般。

gif

为了让问题更加简单,我们假设图像没有颜色通道(比如RGB之类的),这是一个纯灰度的图片,每个像素点带着从0到255的灰度值。我们接下来的工作就是进行「归一化」(normalize),将每个像素的灰度值变成0到1的浮点数。

gif

所以一个图像X可以被表示成一个1 × D²维度的向量(接下来,为了方便描述,我们假设D为8,D²自然为 8乘9等于 64)

X=[0.0,0.7,0.65,…0.7,0.9]

很好。所以什么是协方差矩阵?

我们可以询问维基百科。对于一对像素集合Xi和Xj来说,这就意味着:

cov(Xi,Xj)=E[(Xi−E[Xi])(Xj−E[Xj])]

一般来说,我们可以把这个写成一个点乘式回避E(X)这个符号:

cov(X,X)=E[(X−μX)(X−μX)]

等下…期望?μ?

我们刚才不是说了,我们在讨论的是一张图片里面的点阵吗?

确实,如果我们只有一张图片,那么Xi自然是等于E[Xi],并且X也等于μx。计算单一图片的协方差是没有意义的,这种行为就像你给一个单一的样本计算平均数一样,所以不要着急,请继续看下去。

如果我们有更多的图像X,会发生什么?

假如我们有很多N个不同的样本Xi,比如说你现在有一堆手写的「2」的图像数据。

我们现在就有了一个N × 64的阵列(N个1 × 64),我们把这个集合命名为X,这看上去就像这样:

gif

这个X看上去就复杂的多了,为了方便我们之后的理解我们现在先梳理一下这个X即将用到的具体的属性和参数:

  • 存在一个索引值i,从1到64,可以索引到X其中一个图像中的所有像素点。
    • 我们会把i用于Xi
  • 存在另一个索引值n,从1到N,可以索引到X中任何一个图像。
    • 我们会把n用于 X(n)
  • 所以,X3(5)意味着「第五个图像的第三个像素点的灰度值」。

计算μx:

现在我们可以有意义的计算E[Xi]了,我们可以得到如下计算式:

image

数学符号什么的,那个是数学家的工作,我们只需要尽可能简单地了解这里究竟在干什么就好了!我们会如此描述:我们在计算所有图像中第i个像素位的灰度值的平均值,E[Xi]如下图所示一样简单明了:

gif

我们用这个算式从1加到64,我们就得到了64个不同的平均数(分别是所有图像的第一个像素平均值、所有图像的第二个像素平均值、所有图像的第三个像素平均值… …)

μX=[E[X1],E[X2],…E[X784]]

gif

计算cov(X, X)

我们尝试去理解cov(X, X) = E[(X−μX)(X−μX)]。
我们先看看cov(Xi, Xj) = E[ (Xi−E[Xi]) (Xj−E[Xj]) ]:一个特定像素点组i(所有图第i个像素的组合)和像素点组j的协方差。

对于每个单独的图像来说,我们用这个方法计算出这个像素点组i的内容究竟有多么偏离(或者说,多么独立于)像素组j。

对于每一个我们计算中产生的i和j的对,我们都要将这两个数相乘。我们有多少个这样的对呢?因为它可以自己配上自己,所以有64×64个对。这些对可以如下图般整齐地排列成这样的正方形矩阵。

gif

别忘了,在我们的集合X中,有N个图像需要如这样一般进行处理,换句话说,我们会如此处理出N个方阵。
关于协方差还有几个性质需要补充(用通俗的话来形容,不一定是很严格的说法):

  • i变大,同时j也变大,说明两个组内的元素大体是同向变化,协方差就是正。
  • i变小,j变大,说明两个组内的元素反向变化,协方差是负的。
  • 上述两点都是协方差的性质,不过同向和反向其实不是一个科学的概念,因此用这两个词主要重在理解那种暧昧的「趋势」意义。
  • 而当这两种情况之间几乎没有什么联系时,cov是接近于0的。换言之,在这种情况下i和j没啥关系,你看到像素i的高值并不意味着就很好知道j的值了——因为协方差不会给你很多关于像素j的有用信息。

一个思想实验:在画布上做填充

如果i是黑的,是否意味着j也是黑的、或者趋向于黑的?或者两者之间根本没有关系?我们设立了一些协方差 cov(Xi,Xj),帮助我们衡量两个数字之间的「相关性」。这个协方差中的X实际上是很多很多别的样本中第i个元素所组成的集合,因此cov(Xi,Xj)的意思是:

所有样本中第i个元素的集合、和所有样本中第j个元素的集合之间的协方差。这听上去似乎很好,因为很显然这里面包含了一种数据预测的思想。

你可以通过别的样本那边进行统计,并用得到的协方差预测你自己这里的像素值。

当然我们也给你了协方差矩阵作为帮助:

first row of cov(X,X)=[cov(X1,X1),cov(X1,X2)…cov(X1,X64)]

我们还可以像这个一样,一路建立下去,直到建立完第64行,但是现在只有一行。

所以和我一起做一个思想实验吧:假设我给你了一个8×8的像素画布,和一个协方差矩阵(这个矩阵由你不知道的某个样本中的像素计算而成,与你眼前的这个画布用协方差关联)。你必须尝试填涂你眼前这个像素画布,也就是说,通过我给你的协方差矩阵,来猜测这个画布到底长什么样。

我先告诉你:你现在手上第五十个像素是纯黑的,所以你在第五十个像素中也填上了纯黑色,但是你能通过这个猜出剩下所有的像素吗?

hummm… …协方差矩阵cov(X,X)的第五十行就蕴含了第五十个像素和其他64个像素之间的关系,如果cov(X50,X64)是一个非常负的值,这实际上告诉了你第64号像素(在最右下角)实际上应该是非常白的,因为协方差非常负就表示该值和第五十号像素的颜色是几乎完全不同的。如果cov(X50,X19)是一个非常正值,那就说明19号像素应该是非常暗的(也就是说和,第五十号像素差不多一样,毕竟协方差就是这个意思)。

你应该发现了,因为我给了你一个第五十号像素的值,你就可以举一反六四,迅速将合理的猜测推广到整个图像上。

我们继续这个思想实验,我顺便又告诉你了第51号像素,并且又告诉你了52,53… …实际上你可以这样做64次,每次都可以调整你的对于整个图像的判断。

可是我这样子做的目的是什么?

假如我是用手写的「2」来给你提供协方差的,也就是说我给你的协方差矩阵是用很多个「2」得到的。虽然可能我用不同样本算出的协方差矩阵效果都不统一,但是你得到的东西应该看起来还是像一个「2」。

现在,如果我要你还原的图像和「2」真的没啥太大关系——比如说,只是图像顶部的一条黑线——会怎么样呢?通常情况下,我们不会期望这样一条黑线出现在「2」的图像中,毕竟他真的和「2」没啥关系,这鬼才能想到。所以它的像素协方差应该在0左右,也就是「完全没有关联」的意思,得到的只是一些噪声。

如果我们将协方差矩阵视为一种转换的方法,这能有什么作用?

回想上面的思想实验,我们可以观察到以下内容。请记住,我们的协方差矩阵是使用一堆「2」的不同图像计算的。

  • 将该协方差矩阵应用于2的图像,将趋于点亮与2关联的像素关系。
  • 将协方差矩阵应用于看起来不太像2的图像只会产生噪点。

因此,我们可以把这个协方差矩阵想像成一​​个天真的孩子,它只学会画2。如果我们给它看似与2一致的东西(例如,顶部曲线或底部直线),它将根据“这东西最好是「2」”的思维来填充图像的其余部分。如果我们给它一个与2不一致的东西,它可能只会增加一些毫无意义的噪音。

那么协方差矩阵的特征向量(Eigenvectors) 是什么?

我们先要知道一些和特征向量有关的常识:

  • 矩阵其实可以理解为是一个变换。
  • 矩阵的特征向量能够帮助该矩阵进行不改变方向只改变幅度的变换。(这是线性代数-「特征值和特征向量」中学到的内容)
  • 相应的特征值就是幅度的变化。
  • 如果你还没有学过线性代数,现在就可以放弃了(或者说已经来不及了——毕竟前面已经涉及到了线代的内容,真是难为你了)。
    “慢着……窝老人家不懂你所说的改变方向是什么意思?”

实际上,人脑在展现三维以上物体的能力并不出色(其实根本没法做到),因此让我们考虑三像素图像的情况:

  • 想象一下值为[0,0.2,0.1],[0,0.4,0.2]和[0,1.0,0.5]的9像素点阵灰度图。其实这些像素都指向同一个方向,只不过大小不一样而已(用初中数学来说,是向量长度)。

image

  • 现在想象一下值[0,1,1],[1,1,0],[1,0,1]的图。即使它们的大小相同,这些向量也不会指向相同的方向。

image

  • 特征向量的「不改变方向」的意义其实在于,表示这些像素可能被点得更亮或者变得更暗,但是他们之间的比例是保持不变的。这就是我们为什么使用特征向量作为新的基底的原因。

现在应该更清楚一点:协方差矩阵的特征向量能够配合变换该矩阵,并且只会变亮或变暗图像 – 但是像素关系不会改变。

具体来说,使用具有大且正特征值的特征向量,其像素之间的对比在通过协方差矩阵变化后变得更加强烈。

我们可以想象,如果我们在一组特定的图像(即仅某个数字)上计算了协方差矩阵,则特征向量必然会突出这个图的代表型结构(至少在您的数据集中)。

实际上,我们应该能够将一定数量的这些特征向量一起使用,并得到非常像我们原始图像的东西。

换句话说,如果我们稍微「不像一点」,是不是可以降低计算量的同时保有我们所要的特征呢?

使用特征值和特征向量进行「去相关」和「降维」:(Principal Components Analysis的本质)

如果我们试图通过累加一堆特征向量来构造图像,那么我们投入的每个特征向量都会添加其他特征向量中没有办法表达的信息。
例如:如果您在A点,而B点在您的西北方,那么仅向北走就不可能到达B点。您还必须向西走一些,这就是我们说的北和西相互正交。(因为单纯靠「北」无法走到西边的任何地方)

这就是PCA利用特征向量进行去相关的意义:通过使用特征向量作为表示图像的新基础,我们可以自动确保这个基底由正交向量组成,这个是线性代数中极为重要的内容。两个正交向量虽然不能互相表示,但是这恰恰表明了另外一个好处:

虽然两者不能互相表示,但是两者相互配合,恰好能够以最精简最方便处理的姿态表示出这个线性空间其余所有的向量。

那么降维呢?一种k×k协方差矩阵将会具有K个特征向量,但是你当然不需要全部使用。当从特定类别的数据计算特征向量时,您可以使用最少4个特征向量来获得MNIST数字的重构:

image

译者注释: 其实降维的意思可以理解为抛弃其余特征向量,选择高效的其中一部分特征向量。因为在线性空间中,特征向量的个数其实就是意味着线性空间中元素所保有的维度,当特征向量减少了维度就会降低,这也是线性代数中,秩(Rank)为什么会和特征向量永远保持个数相等的几何原因。但是遗憾的是中华人民共和国境内很多线代教材都没有提到这一点,因此作为补充…

如何找到特征向量?如何使用特征向量?(非重点)

奇异值分解(SVD)是指线性代数中对矩阵M运算并将其分解为以下组件的方式:

M = U Σ V

M 是我们试图为其寻找特征值的矩阵(在这种情况下,这是我们从所有图像中计算出的协方差矩阵)

U Σ V,中U的列是左奇异向量,V的列是右奇异向量。在协方差矩阵为半正定的情况下,U = V,其列是我们希望找到的特征向量。

一旦我们有了U,我们只需要从中获取所需要的特征向量即可,我们通常会选择特征值排名前n名的特征向量,然后我们将其应用于矩阵乘法,就成功完成了降维工作。

实际上,在Python中实现此操作非常简单-这是一个示例,说明了如何实现。

class Pca:
    '''
    A simple class to run PCA and store the truncated U matrix for use on the test set.
    '''

    def __init__(self):
        self.W = None

    def pca_train(self, X, n=5):
        '''
        Helper function to perform PCA on X and store the chosen eigenvectors from U.

        If input X is T x d,

        transformed X' is T x d'

        Also saves the chosen columns of U to self.W.
        
        Based heavily on http://cs231n.github.io/neural-networks-2/.
        '''

        # PCA down to top n eigenvectors
        # Assume input data matrix X of size [T x d]
        cov = np.dot(X.T, X) / X.shape[0] # get the data covariance matrix
        U,S,V = np.linalg.svd(cov)

        # eigenvectors are returned sorted by eigenvalue; get top n
        X_prime = np.dot(X, U[:,:n]) 
        
        # save W = U[:,:n] for use again in testing; we only train
        # PCA on the TRAIN SET to prevent data leakage!
        self.W = U[:,:n]

        return X_prime

    def pca_transform(self, X):
        '''
        Use the previously trained U to transform an input X (i.e. test data)
        '''
        assert self.W is not None, "pca_test() called when self.W is None. Did you call pca_train() first?"
        return np.dot(X, self.W)

本网页的所有文章及其内容除了与相关内容的原作者联系后允许被共享以外,均禁止任何形式的转载、转发、复制以及任何形式的商业使用。

VCVina copyrights this specification. No part of this specification may be reproduced in any form or means, without the prior written consent of VCVina.

当ウェブサイト上のコンテンツ(ブログ)の著作権は、特別の断りが無い限り、ブログや画像の原作者/会社が保有します。 原作者から事前承諾を得ることなく、これらの情報を使用(複製、送信、頒布、改変、販売、出版、転用、掲示等)することはできません、ご了承ください。

日本和中国的大学教育 上一篇
Memory Management 下一篇

 目录