前段时间开始入门机器学习,《机器学习实战》(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\} r≤min{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\} r≤min{m,n},且 0 < k < r 0
A ≈ U k Σ k V k T A\approx U_{k}\Sigma_{k} V_{k}^{T} A≈UkΣ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}} ∥A∥F=(∑i=1m∑j=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 0
秩为 k k k的矩阵 X ∈ M X\in M X∈M满足
∥ 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} ∥A−X∥F=minS∈M∥A−S∥F
则 ∥ 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}} ∥A−X∥F=(σk+12+σk+22+⋯+σn2)21
特别得,若 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} ∥A−Ak∥F=minS∈M∥A−S∥F
定理2说明了一件事情,在弗罗贝尼乌斯范数的意义下,截断奇异值分解是所有秩不超过 k k k的 m × n m\times n m×n矩阵中,对原始矩阵 A A A的最优近似。这就为我们通过奇异值分解压缩数据提供了理论保证。
这里放上《统计学习方法》第十五章奇异值分解的思维导图,这一章还有一些内容这里为涉及到,日后如果需要,再扩展一下。
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[(4−4)2+(3−3)2+(2−1)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[(4−2)2+(3−5)2+(2−2)2=2.83。可以使用“相似度=1/(1+距离)”来将相似度控制在 0 ∼ 1 0\sim1 0∼1之间。
皮尔逊相关系数:该方法较欧氏距离的优势在于,它对用户评分的量级不敏感,例如所有评分都是5分和所有评分都是1分在这里是相同的。numpy中皮尔逊相关系数计算由corrcoef()方法完成,通过 0.5 + 0.5 ∗ c o r r c o e f ( ) 0.5+0.5*corrcoef() 0.5+0.5∗corrcoef()控制相似度在 0 ∼ 1 0\sim1 0∼1之间。
余弦相似度:对于两个向量,计算其夹角余弦值来比较相似程度。numpy中提供了linalg.norm()方法用于计算单个向量的2范数(平方和取根)。因为 c o s cos cos在 − 1 ∼ 1 -1\sim1 −1∼1之间,同样用 0.5 + 0.5 ∗ c o s 0.5+0.5*cos 0.5+0.5∗cos来控制相似度在 0 ∼ 1 0\sim1 0∼1之间。
上面几种相似度计算的代码如下,分别为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)]
先对数据矩阵进行奇异值分解:
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)]
优点:简化数据。去除噪声,提高算法的结果。
缺点:数据的转换可能难以理解,在大数据集上会显著降低程序速度。