机器学习之奇异值分解

目录

  • 一.绪言
  • 二.什么是奇异值分解
  • 三.数学原理
    • 奇异值分解的定义与性质
    • 计算方法
    • 紧奇异值分解与截断奇异值分解
    • 矩阵近似
    • 思维导图
  • 四.算法步骤
  • 五.代码实现
    • 矩阵分解
    • 基于协同过滤的推荐引擎
      • 相似度计算
      • 基于用户还是物品
    • 餐馆菜肴推荐引擎
      • 推荐引擎评价
      • 标准推荐引擎
      • SVD推荐引擎
  • 六.算法评价
  • 参考资料

一.绪言

前段时间开始入门机器学习,《机器学习实战》(Peter Harrington 著)差不多过了一遍,第一遍主要关注算法的实现和应用。现在把注意力转移到算法的原理,这一次争取把算法总结得尽量全面,便于以后查阅。

这是本系列的第二篇文章——奇异值分解。

二.什么是奇异值分解

跟主成分分解一样,奇异值分解也是一种降维方法,

很多情况下数据中的一小段携带了数据集中的大部分信息,其他信息要么是噪声,要么是毫不相关的信息。矩阵分解可以将原始矩阵表示成新的易于处理的形式,过程类似代数中的因子分解。SVD是最常见的一种矩阵分解技术,SVD将原始数据集Data分解为三个矩阵: U , Σ , V T U,\Sigma,V^{T} U,Σ,VT。这三个矩阵分别是m行m列,m行n列,n行n列。其中, Σ \Sigma Σ是对角矩阵,且对角元素从多大到小排列,这些对角元素称为奇异值,它们对应了原始数据 X X X的奇异值,这些奇异值就是 X X T XX^{T} XXT的特征值的平方根。

奇异值都非负,在k个奇异值之后,其他奇异值都为0,这意味着数据集只有k个重要特征,其余特征都是噪声或者冗余特征。

三.数学原理

奇异值分解的定义与性质

定义1
矩阵的奇异值分解是指,将一个非零的 m × n m\times n m×n实矩阵 A A A表示成以下上实矩阵乘积形式的运算,即进行矩阵的因子分解:
A = U Σ V T A=U\Sigma V^{T} A=UΣVT
其中, U U U m m m阶正交矩阵, V V V n n n阶正交矩阵, Σ \Sigma Σ是由降序排列的肺腑的对角线元素组成的 m × n m\times n m×n阶矩形对角矩阵,满足
U U T = I UU^{T}=I UUT=I
V V T = I VV^{T}=I VVT=I
Σ = d i a g { σ 1 , σ 2 , … , σ p } \Sigma=diag\{\sigma_{1},\sigma_{2},\dots,\sigma_{p}\} Σ=diag{σ1,σ2,,σp}
σ 1 ≥ σ 2 ≥ ⋯ ≥ σ p \sigma_{1}\geq\sigma_{2}\geq\dots\geq\sigma_{p} σ1σ2σp
p = m i n { m , n } p=min\{m,n\} p=min{m,n}

对任意矩阵A来说,这种分解到底存不存在?
下面这个定理给予了保证。

定理1
A A A是一 m × n m\times n m×n实矩阵,则 A A A的奇异值分解存在
A = U Σ V T A=U\Sigma V^{T} A=UΣVT
其中 U U U m m m阶正交矩阵, V V V n n n阶正交矩阵, Σ \Sigma Σ对角元素非负,且降序排列。

这个定理的证明是构造性的,较难理解,下面介绍一种比较简单的求法。

计算方法

A = U Σ V T A=U\Sigma V^{T} A=UΣVT,及 U U U V V V正交,有下面两个式子:
A T A = ( V Σ T U T ) ( U Σ V T ) = V Σ T Σ V T A^{T}A=(V\Sigma^{T} U^{T})(U\Sigma V^{T})=V\Sigma^{T} \Sigma V^{T} ATA=(VΣTUT)(UΣVT)=VΣTΣVT
A A T = ( U Σ V T ) ( V Σ T U T ) = U Σ Σ T U T AA^{T}=(U\Sigma V^{T})(V\Sigma^{T} U^{T})=U\Sigma \Sigma^{T} U^{T} AAT=(UΣVT)(VΣTUT)=UΣΣTUT
于是,我们知道, Σ T Σ \Sigma^{T} \Sigma ΣTΣ的对角元素是 A T A A^{T}A ATA的特征值, V V V的列向量是 A T A A^{T}A ATA的特征向量, Σ Σ T \Sigma \Sigma^{T} ΣΣT的对角元素是 A A T AA^{T} AAT的特征值, U U U的列向量是 A T A A^{T}A ATA的特征向量.

对一个矩阵求特征值和特征向量是比较容易的,所以我们可以对 A T A A^{T}A ATA A A T AA^{T} AAT进行特征值分解,从而得到 U U U V V V Σ \Sigma Σ

紧奇异值分解与截断奇异值分解

从上面的定理看,不就是把一个矩阵分解三个矩阵,好像也没有进行降维。

其实在应用的时候,主要用下面两个分解:

定义2:紧奇异值分解
A A A是一 m × n m\times n m×n实矩阵,其秩为 r a n k ( A ) = r rank(A)=r rank(A)=r r ≤ m i n { m , n } r\leq min\{m,n\} rmin{m,n},则称 U r Σ r V r T U_{r}\Sigma_{r} V_{r}^{T} UrΣrVrT A A A的紧奇异值分解,即
A = U r Σ r V r T A=U_{r}\Sigma_{r} V_{r}^{T} A=UrΣrVrT
其中, U r U_{r} Ur m × r m\times r m×r矩阵, Σ r \Sigma_{r} Σr r r r阶对角矩阵, V r V_{r} Vr n × r n\times r n×r矩阵,矩阵 U r U_{r} Ur定义1 U U U的前 r r r列得到,矩阵 V r V_{r} Vr定义1 V V V的前 r r r列得到, Σ r \Sigma_{r} Σr定义1 Σ \Sigma Σ的前 r r r个对角元素得到。

定义3:截断奇异值分解
A A A是一 m × n m\times n m×n实矩阵,其秩为 r a n k ( A ) = r rank(A)=r rank(A)=r r ≤ m i n { m , n } r\leq min\{m,n\} rmin{m,n},且 0 < k < r 00<k<r,则称 U k Σ k V k T U_{k}\Sigma_{k} V_{k}^{T} UkΣkVkT A A A的截断奇异值分解,
A ≈ U k Σ k V k T A\approx U_{k}\Sigma_{k} V_{k}^{T} AUkΣkVkT
其中, U k U_{k} Uk m × k m\times k m×k矩阵, Σ k \Sigma_{k} Σk k k k阶对角矩阵, V k V_{k} Vk n × k n\times k n×k矩阵,矩阵 U k U_{k} Uk定义1 U U U的前 k k k列得到,矩阵 V k V_{k} Vk定义1 V V V的前 k k k列得到, Σ k \Sigma_{k} Σk定义1 Σ \Sigma Σ的前 k k k个对角元素得到。对角矩阵 Σ k \Sigma_{k} Σk比原始矩阵的秩低。

可以看到,上面两种分解,对原始矩阵进行了压缩。

紧奇异值分解后,矩阵的秩不变,是一种无损压缩,而截断奇异值分解后矩阵的秩缩小,两者不相等,所以是约等号,但是这个约等于到底有多接近说不清。需要一个定理来说明。

矩阵近似

先定义一个范数。
定义4:弗罗贝尼乌斯范数
A A A是一 m × n m\times n m×n实矩阵, A = [ a i j ] m × n A=[a_{ij}]_{m\times n} A=[aij]m×n,定义矩阵 A A A的弗罗贝尼乌斯范数为
∥ A ∥ F = ( ∑ i = 1 m ∑ j = 1 n ( a i j 2 ) ) 1 2 \left \| A \right \|_{F}=(\sum_{i=1}^{m}\sum_{j=1}^{n}(a_{ij}^{2}))^{\frac{1}{2}} AF=(i=1mj=1n(aij2))21

定理2
A A A是一 m × n m\times n m×n实矩阵,其秩为 r a n k ( A ) = r rank(A)=r rank(A)=r,有奇异值分解 A = U Σ V T A=U\Sigma V^{T} A=UΣVT,并设 M M M m × n m\times n m×n矩阵中所有秩不超过 k k k的矩阵的集合, 0 < k < r 00<k<r,若
秩为 k k k的矩阵 X ∈ M X\in M XM满足
∥ A − X ∥ F = m i n S ∈ M ∥ A − S ∥ F \left \| A-X \right \|_{F}=min_{S\in M}\left \| A-S \right \|_{F} AXF=minSMASF
∥ A − X ∥ F = ( σ k + 1 2 + σ k + 2 2 + ⋯ + σ n 2 ) 1 2 \left \| A-X \right \|_{F}=(\sigma_{k+1}^{2}+\sigma_{k+2}^{2}+\dots+\sigma_{n}^{2})^{\frac{1}{2}} AXF=σk+12+σk+22++σn221
特别得,若 A k = U k Σ k V k T A_{k}=U_{k}\Sigma_{k} V_{k}^{T} Ak=UkΣkVkT A A A的截断奇异值分解,则
∥ A − A k ∥ F = m i n S ∈ M ∥ A − S ∥ F \left \| A-A_{k} \right \|_{F}=min_{S\in M}\left \| A-S \right \|_{F} AAkF=minSMASF

定理2说明了一件事情,在弗罗贝尼乌斯范数的意义下,截断奇异值分解是所有秩不超过 k k k m × n m\times n m×n矩阵中,对原始矩阵 A A A的最优近似。这就为我们通过奇异值分解压缩数据提供了理论保证。

思维导图

这里放上《统计学习方法》第十五章奇异值分解的思维导图,这一章还有一些内容这里为涉及到,日后如果需要,再扩展一下。

四.算法步骤

  1. 对矩阵进行奇异值分解
  2. 计算奇异值的平方和
  3. 计算前n个奇异值的平方和的占比
  4. 当占比足够大时,确定要保留的奇异值的个数
  5. 计算截断的左奇异向量和右奇异向量

五.代码实现

矩阵分解

numpy中的linalg有一个svd方法:U, Sigma, VT = linalg.svd(Data)。注意 Σ \Sigma Σ虽然是矩阵,但为了节约空间以array的形式返回对角线元素。下面的代码展示了对样例矩阵求 Σ \Sigma Σ的过程。

from numpy import *

def loadExData():
    return [[1, 1, 1, 0, 0],
            [2, 2, 2, 0, 0],
            [1, 1, 1, 0, 0],
            [5, 5, 5, 0, 0],
            [1, 1, 0, 2, 2],
            [0, 0, 0, 3, 3],
            [0, 0, 0, 1, 1]]

Data = loadExData()

U, Sigma, VT = linalg.svd(Data)
print(Sigma)

结果:

[9.72140007e+00 5.29397912e+00 6.84226362e-01 1.50962387e-15
 1.15387192e-31]

从上面返回的 Σ \Sigma Σ矩阵可以看出,前三个数值比最后两个值大了很多,因此可以将最后两个值去掉,构成一个3x3的对角矩阵Sig3。若要从 U U U Σ \Sigma Σ V T V^{T} VT中构造原始矩阵的近似矩阵,只需要用 U U U的前三列和 V T V^{T} VT的前三行。实际操作中,确定要保留的奇异值的个数有两种方法:一是将所有的奇异值map成平方和,之后从前向后叠加,直到累加到总值的90%;二是启发式策略,当矩阵有上万的奇异值时,只保留前3000个,前提是对数据有足够的了解,确保3000个奇异值足够覆盖总平方和的90%。

Sig3 = eye(3)*Sigma[:3]
print(Sig3)
print(U[:, :3]*mat(Sig3)*VT[:3, :])

结果:

[[9.72140007 0.         0.        ]
 [0.         5.29397912 0.        ]
 [0.         0.         0.68422636]]
[[ 1.00000000e+00  1.00000000e+00  1.00000000e+00 -2.84366098e-16
  -2.94015497e-16]
 [ 2.00000000e+00  2.00000000e+00  2.00000000e+00  4.47489534e-16
   4.28190736e-16]
 [ 1.00000000e+00  1.00000000e+00  1.00000000e+00  3.09573758e-16
   2.99924358e-16]
 [ 5.00000000e+00  5.00000000e+00  5.00000000e+00 -1.47703573e-16
  -1.95842150e-16]
 [ 1.00000000e+00  1.00000000e+00 -5.70229711e-16  2.00000000e+00
   2.00000000e+00]
 [-7.49390630e-17  9.96896569e-16 -1.34350906e-15  3.00000000e+00
   3.00000000e+00]
 [-8.18314124e-17  2.75447132e-16 -3.13743829e-16  1.00000000e+00
   1.00000000e+00]]

基于协同过滤的推荐引擎

相似度计算

利用不同用户对某一件物品的评分来计算相似度。举例如下表。

人名 鳗鱼饭 日式炸鸡 寿司饭 烤牛肉 手撕猪肉
Jim 2 0 0 4 4
John 5 5 5 3 3
Sally 2 4 2 1 2

可以使用多种方法计算相似度。

  • 欧氏距离:计算手撕牛肉和烤牛肉的距离为 s q r t [ ( 4 − 4 ) 2 + ( 3 − 3 ) 2 + ( 2 − 1 ) 2 ] = 1 sqrt[(4-4)^2+(3-3)^2+(2-1)^2]=1 sqrt[(44)2+(33)2+(21)2]=1,手撕牛肉和鳗鱼饭的距离为 s q r t [ ( 4 − 2 ) 2 + ( 3 − 5 ) 2 + ( 2 − 2 ) 2 = 2.83 sqrt[(4-2)^2+(3-5)^2+(2-2)^2=2.83 sqrt[(42)2+(35)2+(22)2=2.83。可以使用“相似度=1/(1+距离)”来将相似度控制在 0 ∼ 1 0\sim1 01之间。

  • 皮尔逊相关系数:该方法较欧氏距离的优势在于,它对用户评分的量级不敏感,例如所有评分都是5分和所有评分都是1分在这里是相同的。numpy中皮尔逊相关系数计算由corrcoef()方法完成,通过 0.5 + 0.5 ∗ c o r r c o e f ( ) 0.5+0.5*corrcoef() 0.5+0.5corrcoef()控制相似度在 0 ∼ 1 0\sim1 01之间。

  • 余弦相似度:对于两个向量,计算其夹角余弦值来比较相似程度。numpy中提供了linalg.norm()方法用于计算单个向量的2范数(平方和取根)。因为 c o s cos cos − 1 ∼ 1 -1\sim1 11之间,同样用 0.5 + 0.5 ∗ c o s 0.5+0.5*cos 0.5+0.5cos来控制相似度在 0 ∼ 1 0\sim1 01之间。

上面几种相似度计算的代码如下,分别为eulidSim,pearsSim和cosSim。

from numpy import *
from numpy import linalg as la

def eulidSim(inA, inB):
    return 1.0/(1.0 + la.norm(inA - inB))

def pearsSim(inA, inB):
    if len(inA) < 3: return 1.0  # 此时两个向量完全相关
    return 0.5 + 0.5*corrcoef(inA, inB, rowvar = 0)[0][1]

def cosSim(inA, inB):
    num = float(inA.T * inB)
    denom = la.norm(inA) * la.norm(inB)
    return 0.5+0.5*(num/denom)

myMat = mat(loadExData())

print(eulidSim(myMat[:,0], myMat[:,4]))
print(cosSim(myMat[:,0], myMat[:,4]))
print(pearsSim(myMat[:,0], myMat[:,4]))

结果:

0.13367660240019172
0.5472455591261534
0.23768619407595815

基于用户还是物品

通过计算两种菜肴之间的距离是基于物品的相似度。另一种计算用户距离的方法是基于用户的相似度。在上面的表格示例中,行与行之间的比较是基于用户的相似度,列与列之间的比较是基于物品的相似度。使用哪一种相似度取决于用户或物品的数目。基于X的相似度计算所需的时间会随着X的增长而增长,通常如果用户数目很多并且会不断增长,我们倾向于使用基于物品的相似度计算。

餐馆菜肴推荐引擎

给定一个用户,系统会为此用户选择N个最好的推荐菜。整个流程需要做到:寻找用户没有评分的菜肴,在这些没有评分的所有菜肴中,对每种菜计算一个可能的评级分数,即预测用户会对该菜肴做出的评分。最后将评分从高到低排序,返回前N个物品。

推荐引擎评价

通常用于推荐引擎评价的指标是“最小方均根误差(RMSE)”,它计算均方误差的平均值并开根。若评级在1~5分,而RMSE的结果为1.0,说明预测值和用户给出的真实评价差了一分。

标准推荐引擎

这里用的数据是《机器学习实战》给的11阶矩阵。

def loadExData():
    return [[0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 5],
            [0, 0, 0, 3, 0, 4, 0, 0, 0, 0, 3],
            [0, 0, 0, 0, 4, 0, 0, 1, 0, 4, 0],
            [3, 3, 4, 0, 0, 0, 0, 2, 2, 0, 0],
            [5, 4, 5, 0, 0, 0, 0, 5, 5, 0, 0],
            [0, 0, 0, 0, 5, 0, 1, 0, 0, 5, 0],
            [4, 3, 4, 0, 0, 0, 0, 5, 5, 0, 1],
            [0, 0, 0, 4, 0, 4, 0, 0, 0, 0, 4],
            [0, 0, 0, 2, 0, 2, 5, 0, 0, 1, 2],
            [0, 0, 0, 0, 5, 0, 0, 0, 0, 4, 0],
            [1, 0, 0, 0, 0, 0, 0, 1, 2, 0, 0]]

推荐没有品尝过的菜肴:两个函数,standEst()用于在给定计算相似度方法的前提下,计算用户对某种物品的可能评分;recommend()是推荐引擎,调用standEst函数,并返回前N个最好物品。在standEst的执行过程中,假设要计算用户 u u u对其未打分的菜肴 i i i的可能评分,则需要通过其他物品 j j j和物品 i i i建立联系。扫描所有 n n n个物品,如果用户 u u u对某个物品 j j j有过评分,则寻找所有用户中即对 i i i又对 j j j评分过的用户群体 u s e r s users users,根据 u s e r s users users的打分,计算出物品 i i i和物品 j j j的相似度。最后将这个相似度乘以用户 u u u对物品j的评分累加到 r a t S i m T o t a l ratSimTotal ratSimTotal变量,将相似度累加到 s i m T o t a l simTotal simTotal变量。最后返回 r a t S i m T o t a l / s i m T o t a l ratSimTotal/simTotal ratSimTotal/simTotal就是可能评分。

ef standEst(dataMat, user, simMeas, item):
    # 得到数据集中的物品数目
    n = np.shape(dataMat)[1]
    # 初始化两个评分值
    simTotal = 0.0
    ratSimTotal = 0.0
    # 遍历行中的每个物品(对用户评过分的物品进行遍历,并将它与其他物品进行比较)
    for j in range(n):
        userRating = dataMat[user, j]
        # 如果某个物品的评分值为0,则跳过这个物品
        if userRating == 0:
            continue
        # 寻找两个用户都评级的物品
        # 变量overLap给出的是两个物品当中已经被评分的那个元素的索引ID
        # logical_and计算x1和x2元素的逻辑与
        overLap = np.nonzero(np.logical_and(dataMat[:, item].A > 0, dataMat[:, j].A > 0))[0]
        # 如果相似度为0,则两者没有任何重合元素,终止本次循环
        if len(overLap) == 0:
            similarity = 0
        # 如果存在重合的物品,则基于这些重合物重新计算相似度
        else:
            similarity = simMeas(dataMat[overLap, item], dataMat[overLap, j])
        print('the %d and %d similarity is: %f' % (item, j, similarity))
        # 相似度会不断累加,每次计算时还考虑相似度和当前用户评分的乘积
        # similarity 用户相似度   userRating 用户评分
        simTotal += similarity
        ratSimTotal += similarity * userRating
    if simTotal == 0:
        return 0
    # 通过除以所有的评分和,对上述相似度评分的乘积进行归一化,使得最后评分在0~5之间,这些评分用来对预测值进行排序
    else:
        return ratSimTotal / simTotal

ef recommend(dataMat, user, N=3, simMeas=cosSim, estMethod=standEst):
    # 寻找未评级的物品
    # 对给定的用户建立一个未评分的物品列表
    unratedItems = np.nonzero(dataMat[user, :].A == 0)[1]
    # 如果不存在未评分物品,那么就退出函数
    if len(unratedItems) == 0:
        return ('you rated everything')
    # 物品的编号和评分值
    itemScores = []
    # 在未评分的物品上进行循环
    for item in unratedItems:
        estimatedScore = estMethod(dataMat, user, simMeas, item)
        # 寻找前N个未评级的物品,调用standEst()来产生该物品的预测得分,该物品的编号和估计值会放在一个元素列表itemScores中
        itemScores.append((item, estimatedScore))
    # 返回元素列表,第一个就是最大值
    return sorted(itemScores, key=lambda jj: jj[1], reverse=True)[: N]

试一下:

myMat = np.mat(loadExData())
    A = recommend(myMat, 1)
    print(A)

结果:

[(6, 3.3333333333333335), (9, 3.3333333333333335), (0, 3.0)]

SVD推荐引擎

先对数据矩阵进行奇异值分解:

myMat = np.mat(loadExData())
U, Sigma, VT = la.svd(np.mat(myMat))
print(Sigma)

结果:

[15.77075346 11.40670395 11.03044558  4.84639758  3.09292055  2.58097379
  1.00413543  0.72817072  0.43800353  0.22082113  0.07367823]

发现前四个奇异值已经包含了90%的能量,因此可以将11维数据缩减为4维。添加svdEst方法用于简化数据。

def svdEst(dataMat, user, simMeas, item):
    # 得到数据集中的物品数目
    n = np.shape(dataMat)[1]
    # 初始化两个评分值
    simTotal = 0.0
    ratSimTotal = 0.0
    # 奇异值分解
    # 在SVD分解之后,我们只利用包含90%能量值的奇异值,这些奇异值会以Numpy数组形式得以保存
    U, Sigma, VT = la.svd(dataMat)
    # 如果要进行矩阵运算,就必须要用这些奇异值构造出一个对角阵
    Sig4 = np.mat(np.eye(4) * Sigma[: 4])
    # 利用U矩阵将物品转换到低维空间中,构建转换后的物品(物品的4个主要特征)
    xformedItems = dataMat.T * U[:, :4] * Sig4.I
    # 遍历行中的每个物品(对用户评过分的物品进行遍历,并将它与其他物品进行比较)
    for j in range(n):
        userRating = dataMat[user, j]
        # 如果某个物品的评分值为0,则跳过这个物品
        if userRating == 0:
            continue
        # 相似度的计算也会作为一个参数传递给该函数
        similarity = simMeas(xformedItems[item, :].T, xformedItems[j, :].T)
        print('the %d and %d similarity is: %f' % (item, j, similarity))
        # 相似度会不断累加,每次计算时还考虑相似度和当前用户评分的乘积
        # similarity 用户相似度   userRating 用户评分
        simTotal += similarity
        ratSimTotal += similarity * userRating
    if simTotal == 0:
        return 0
    # 通过除以所有的评分和,对上述相似度评分的乘积进行归一化,使得最后评分在0~5之间,这些评分用来对预测值进行排序
    else:
        return ratSimTotal / simTotal

试一下:

A = recommend(myMat, 1, estMethod=svdEst, simMeas=pearsSim)
    print(A)

结果:

[(4, 3.346952186702173), (9, 3.3353796573274694), (6, 3.3071930278130366)]

六.算法评价

优点:简化数据。去除噪声,提高算法的结果。
缺点:数据的转换可能难以理解,在大数据集上会显著降低程序速度。

参考资料

  1. 《统计学习方法》第二版 李航 著
  2. 《机器学习实战》Peter Harrington 著
  3. 机器学习实战——SVD(奇异值分解)
  4. 如何让奇异值分解变得不“奇异”
  5. 矩阵论笔记:奇异值分解SVD(Singular Value Decomposition)以及应用总结!

你可能感兴趣的:(机器学习,《机器学习实战》学习笔记,奇异值分解,机器学习,统计学习方法)