在TensorFlow中使用SVD做主成分分析

为啥标题会有TensorFlow?因为它的API设计叕把我惊喜(吓)到了。这部分放到后面来说,先上点前菜。

1. 主成分分析

首先来讲什么是主成分分析(Principal Component Analysis),关于这个概念的介绍网上有很多,推荐最大方差解释最小平方误差解释这两篇博客。

用最概况的话来说:主成分是一种用简单矩阵变换来近似模拟复杂矩阵变换的方法。

稍啰嗦一点的说法是,向量表示的是某个高维空间的位置(或状态),矩阵的现实意义是表示线性坐标空间的变换(参见孟岩老师经典的“理解矩阵”三部曲,链接:【1】/【2】/【3】)。所以向量与矩阵相乘即为位置的移动(变换到另一个位置),矩阵与矩阵相乘相当于对原矩阵所有空间坐标的一次映射,是一种对空间坐标轴方向和各方向单位长度的变换。对于同一种变换结果,通常可以使用几种不同的变换方式获得,这些变换所代表的矩阵互为相似矩阵,它们具有相同的维度。或者,我们也可以找出一些变换,使得变换后的空间降维,却保持与原矩阵空间所能表示的位置集合尽可能接近(这类变换矩阵通常不是方阵,因为一个矩阵只能乘以长度相同,宽度更小的矩阵才会变得更小)。

我们可以将矩阵中的每一行的向量看做该矩阵的一个坐标轴方向,大部分矩阵代表的坐标轴之间并没有正交。假如每个坐标表示一个影响最终状态的因素,则一种因素的变化将引起该空间中的向量(表示一种状态)在两个或以上坐标方向上移动。因此若想用最少的坐标表达原有空间的所有状态,就是找到一个矩阵将这个空间变换为正交空间

2. 特征值分解

特征值分解奇异值分解是实现空间坐标正交变换的方法,关于这两种数学方法的详细解释,推荐一篇写得很好的文章:强大的矩阵奇异值分解及其应用。简要的说明一下结论,特征值分解是指任意一个可对角化的方阵M都可以被分解为以下形式:

M = Q x Σ x I(Q)

其中Σ是一个对角矩阵,对角上的每个值称为特征值Q是一个方阵,上面每一行称为特征向量,这些向量都是正交的。I(Q)表示对矩阵Q取逆矩阵。

TensorFlow提供了求特征值分解的方法tf.self_adjoint_eig,效果上和Numpy的np.linalg.eig差不多(返回的特征值也都没排序)。Numpy的array写起来方便一点,同时为了向等下要说的另一种网上改进版算法致敬,这里用Numpy举例:

>>> import numpy as np

>>> M = np.array([[ 5, 8, 6, 7],
              [ 8, 5, 7, 4],
              [ 6, 7, 5, 3],
              [ 7, 4, 3, 5]])
>>> s, Q = np.linalg.eig(M)

# 打印所有特征值
>>> print(np.diag(s))
[[ 22.7986798    0.           0.           0.        ]
 [  0.           2.8476628    0.           0.        ]
 [  0.           0.          -3.86490633   0.        ]
 [  0.           0.           0.          -1.78143627]]
# 打印所有特征向量
>>> print(Q)
[[ 0.56359593  0.17366844  0.7480347  -0.3043731 ]
 [ 0.53290936 -0.31019456 -0.55620238 -0.55715874]
 [ 0.47049019 -0.53931934  0.05405539  0.69631289]
 [ 0.42072107  0.76338278 -0.35799584  0.33478277]]

# 验证性质
>>> print(M)
[[5 8 6 7]
 [8 5 7 4]
 [6 7 5 3]
 [7 4 3 5]]
>> print(Q.dot(np.diag(s).dot(Q.I)))
[[ 5.  8.  6.  7.]
 [ 8.  5.  7.  4.]
 [ 6.  7.  5.  3.]
 [ 7.  4.  3.  5.]]

在有的地方经常会见到下面这个公式:

M x v = λ x v

这里的λ是任意的一个特征值,v是任意的一个特征向量。同样可以用Numpy验证这个特性(注意Numpy返回的特性向量是列向量):

>>> print(np.matmul(m, v[:,0]))
[[ 12.84924318]
 [ 12.14962988]
 [ 10.72655529]
 [  9.59188488]]
>>> print(l[0] * v[:,0])
[[ 12.84924318]
 [ 12.14962988]
 [ 10.72655529]
 [  9.59188488]]

后面这个公式我们现在不关心,回到M = Q x Σ x I(Q)这里来,我们把两边同时乘以矩阵Q,得到:

M x Q = Q x Σ

这个等式的意义在于,右侧的内容变为一个矩阵Q乘以对角矩阵Σ,根据矩阵乘法,矩阵乘以对角矩阵等同于,将被乘矩阵的每一行分别乘以对角矩阵相应行上的数值。同时由于矩阵Q的每一行都是一个正交空间坐标轴的向量,所以为了最大限度的保留变换结果,我们应该将数值绝对值最小的特征值所对应的行去除。

假设原本矩阵M表示的是n个数据样本(每行是一个样本),每个样本有n个特征(每列是一个特征),降维后的每个特征维度为r(即将原本n x n的矩阵用n x r的矩阵表示),则上述过程可表示为:

import numpy as np 
  
def pca(M, r):
    s, Q = np.linalg.eig(M)         # 求特征值和特征向量
    idx = np.argsort(np.fabs(s))    # 对特征值绝对值进行排序
    top_idx = idx[:-(r + 1):-1]     # 取出特征值绝对值最大的下标
    rs = s[top_idx]                 # 取出排序后下标对应的特征值
    rQ = Q[:, top_idx]              # 取出排序后下标对应的特征向量
    rM = rQ * np.diag(rs)           # 得到降维后的矩阵
    bM = rM * rQ.I                  # 使用降维后的矩阵还原原始矩阵
    return rM, bM                   # 返回降维后的矩阵和还原结果

测试一下效果:

# 这是一个能够包含四维数据特征的矩阵
>>> M = np.array([[ 5, 8, 6, 7],
              [ 8, 5, 7, 4],
              [ 6, 7, 5, 3],
              [ 7, 4, 3, 5]])

# 降到三维,查看还原结果
>>> rM, bM = pca(m, 3)
>>> print(bM)
[[ 5.16503758  8.30210333  5.62244433  6.81847366]
 [ 8.30210333  5.5530039   6.30887965  3.66771378]
 [ 5.62244433  6.30887965  5.86373231  3.41527695]
 [ 6.81847366  3.66771378  3.41527695  5.19966249]]

# 降到两维,查看还原结果
>>> rM, bM = pca(m, 2)
>>> print(bM)
[[ 5.07914999  8.45550979  5.88916425  6.44094335]
 [ 8.45550979  5.27899989  5.83248297  4.3420323 ]
 [ 5.88916425  5.83248297  5.03544588  4.58767992]
 [ 6.44094335  4.3420323   4.58767992  3.5401777 ]]

# 降到一维,查看还原结果
>>> rM, bM = pca(m, 1)
>>> print(bM)
[[ 7.24178118  6.84748197  6.04544292  5.40594729]
 [ 6.84748197  6.4746515   5.71628173  5.11160524]
 [ 6.04544292  5.71628173  5.04673908  4.51288778]
 [ 5.40594729  5.11160524  4.51288778  4.03550804]]

我们忽略降维后的矩阵rM值的输出,单看还原效果。可以看出,在下降维度不多时,从低维矩阵可以近似的还原回原本矩阵的数值。

除了照搬原始公式实现的PCA降维方法,在网上还有一个不明觉厉的版本,例如主成分分析(PCA)—基于python+numpyPython实现PCA等博客都介绍了这种算法,实测效果更佳。

3. 奇异值分解

看完特征值分解,再来看它的扩展形式:奇异值分解。

两者的区别在网上一搜一大把。一句话概括,特征值分解很好用,但只能用于长宽相等的方阵,对于非方阵的矩阵呢,就要使用奇异值分解了。实际上,特征值分解只是奇异值分解的一种特例。

直接上公式:

M = U x Σ x T(V)

同样的,M是原始矩阵。UV是两个特征向量矩阵,分别称为“左奇异向量”和“右奇异向量”(其实分别是M x T(M)T(M) x M方阵的特征向量,所以形状一大一小),T(V)即矩阵V的转置。Σ又是一个奇异值组成的对角矩阵(长宽不一致,所以会有一些空的行/列),而且还是已经由顶向下依据数值绝对值大小降序排列的。

关于这几个矩阵的大小,一般教科书是这样写的(网上查到的版本也都是这个):

  • 若M是m行n列矩阵
  • 则U是m行m列矩阵
  • 且Σ是m行n列矩阵
  • 且V是n行n列矩阵

这样右侧的等式符合矩阵乘法要求的大小,最后会得到一个m行n列矩阵,与左边一致。如果在Numpy里验证这个数值也是如此(注意Numpy里返回的V是已经转置过的,无需再次转置):

>>> import numpy as np

>>> M = np.mat([[1,2,3,4],[5,6,7,8],[2,3,4,5]])
>>> u, s, v = np.linalg.svd(M)

>>> print(M.shape)
(3, 4)
>>> print(u.shape)
(3, 3)
>>> print(s.shape)
(3,)   # 根据矩阵乘法,这里实际表示的对角阵shape是(3, 4)
>>> print(v.shape)
(4, 4)

# 验证一下公式
>>> print(u.dot(np.column_stack((diag(s), np.zeros(3))).dot(v)))
[[ 1.  2.  3.  4.]
 [ 5.  6.  7.  8.]
 [ 2.  3.  4.  5.]]

此时TensorFlow再一次奇葩了!首先请注意它返回值中的usv矩阵顺序,这是新手很容易被坑的地方。其次,它返回的矩阵大小十分的另类:

>>> import tensorflow as tf
>>> tf.InteractiveSession()
>>> M = tf.constant([[1,2,3,4],[5,6,7,8],[2,3,4,5]], dtype=tf.float32)
>>> s, u, v = tf.svd(M)

>>> print(M.shape)
(3, 4)
>>> print(u.shape)
(3, 3)
>>> print(s.shape)
(3,)   # 根据矩阵乘法,这里实际表示的对角阵shape是(3, 3)
>>> print(v.shape)
(4, 3)

带进公式里计算一下,证实你没有看错。

>>> print(tf.matmul(tf.matmul(u, tf.diag(s)), tf.transpose(v)).eval())
[[ 1.00000107  2.00000095  3.00000143  4.00000191]
 [ 5.00000191  6.00000191  7.00000286  8.00000286]
 [ 2.00000048  3.00000095  4.00000095  5.00000095]]

不论怎样,我们现在得到了一个与公式结构一致的奇异值分解式。怎样提取特征矩阵的主成分呢?继续照猫画虎,不难发现,等式右侧的U是一个由正交向量组成的矩阵,而且Σ是对角矩阵,所以U x Σ是和特征值分解中的Q x Σ如出一辙。然后还是多出一个矩阵V,将两边同时乘以T(V)的转置,相当于把V挪到等号左边,等式变成:

M x I(T(V)) = U x Σ

接下来等号左边的部分就不用看了,等号右边的就是我们要的主成分矩阵。假设将原始矩阵的n维特征减少到r维,由于返回的s奇异值是已经排序的,直接取出最前面的r个值即可,同时相应裁剪uv矩阵,得到:

u2 = u[:, :r]
s2 = s[:r]
v2 = v[:, :r]
M ≈ u2 x tf.diag(s2) x tf.transpose(v2)   # 注意是约等于

代码写出来就是下面这个样子(忽略还原原始矩阵的计算):

def pca_via_svd(M, r):
    '''
    M: 原始矩阵
    r: 新的矩阵长度
    '''
    # 将输入矩阵做奇异值分解
    s, u, v = tf.svd(M)
    # 使用奇异值将矩阵的第二维压缩到指定长度
    return tf.matmul(u[:, :r], tf.diag(s[:r]))

结论非常简单,两行代码的事情,只是为了摸索清楚相关的概念和最后这个API的用法,的确没少绕路。且行且分享,将踏过的路记录下来,让更多的人避免踩同样的坑。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容