• 【推荐系统入门到项目实战】(五):SVD矩阵分解


    【推荐系统入门到项目实战】(五):SVD矩阵分解


    • 🌸个人主页:JOJO数据科学
    • 📝个人介绍:统计学top3高校统计学硕士在读
    • 💌如果文章对你有帮助,欢迎✌关注、👍点赞、✌收藏、👍订阅专栏
    • ✨本文收录于【推荐系统入门到项目实战】本系列主要分享一些学习推荐系统领域的方法和代码实现。

    引言

    之前我们介绍了矩阵分解ALS算法,并介绍了几个案例,下面我们来看看另一种使用广泛的矩阵分解方法——SVD,及其在推荐系统上的应用。

    老规矩,我们首先来回顾一下推荐算法的常见方法框架。主要分为两大类

    • 1.基于内容的推荐
    • 2.基于协同过滤的推荐

    image-20221004172538096

    基于协同过滤的推荐是推荐系统的主流思想之一。其中矩阵分解是一个重要的模块。本文我们来讨论一下SVD矩阵分解的原理与实现。

    矩阵分解前置知识(MF)

    首先,矩阵分解是将一个原始的大的用户矩阵进行分解成多个矩阵的乘积。在推荐系统中,矩阵分解属于隐语义模型一个大的分支。之前我们讨论的ALS算法和推荐系统,我们是以一个电影评分数据集为例,将其分解为user矩阵和item矩阵。然后我们使用ALS算法将其估计。如下图所示。

    image-20221004215919054

    之前我们是假设评分矩阵可以分解成User矩阵和Item矩阵。这一章我们来讨论一些背后的数学知识和直觉。

    现在我们来看看SVD(奇异值分解) 是怎么做的。首先,我们来回顾一下一些线性代数的基本知识:

    矩阵的特征分解:

    特征分解,是将矩阵分解为特征值和特征向量表示的矩阵之积的方法,也称为谱分解

    N 维非零向量 v 是 N×N 的矩阵 A 的特征向量,当且仅当下式成立
    A v = λ v Av=\lambda{v} Av=λv
    λ \lambda λ特征值(标量),v为特征值对应的特征向量。特征向量被施以线性变换 A 只会使向量伸长或缩短,而方向保持不变。

    接下来我们怎么求解这个特征向量呢?在线性代数中,我们知道,求解特征方程 ∣ A − λ I ∣ = 0 |A-\lambda I|=0 AλI=0即可。

    p ( λ ) : = ∣ A − λ I ∣ = 0 p(\lambda):=|A-\lambda I|=0 p(λ):=AλI=0称作矩阵的特征多项式

    这个特征多项式是关于 λ \lambda λ的N次多项式,用N个解
    p ( λ ) = ( λ − λ 1 ) n 1 . . . ( λ − λ k ) k n = 0 p(\lambda)=(\lambda-\lambda_1)^{n_1}...(\lambda-\lambda_k)^n_k=0 p(λ)=(λλ1)n1...(λλk)kn=0
    其中 ∑ i = 1 k n i = N \sum_{i=1}^{k}n_i=N i=1kni=N,对于每一个特征值,我们都有 ( A − λ i I ) v = 0 (A-\lambda_iI)v=0 (AλiI)v=0

    可能大家对于这些公式看起来觉得有些枯燥,下面我们来通过一个具体的例子来理解。

    image-20221022172600655

    我们要求矩阵A的特征值和对应的特征向量,即求解下述方程

    image-20221022172616177

    得到:

    image-20221022172640702

    求解的 λ 1 = 1 , λ 2 = λ 3 = 0 \lambda_1=1,\lambda_2=\lambda_3=0 λ1=1,λ2=λ3=0

    λ = 1 \lambda=1 λ=1

    image-20221022172734529

    简化得到:

    在这里插入图片描述

    然后有:

    image-20221022172805464

    即:

    image-20221022172833946

    x 1 = 1 x_1=1 x1=1,则有特征向量为:

    image-20221022172851355

    同理,当 λ 2 = λ 3 = 0 \lambda_2=\lambda_3=0 λ2=λ3=0,计算可得特征矩阵

    image-20221022172901035

    现在我们知道了如何计算特征值和特征向量为什么我们要讲这些呢? 是因为当 A A A是一个 N × N N\times N N×N的方阵时,那么有A的特征分解如下:

    image-20221022173042940

    • U 的列向量是 A 的特征向量

    • Λ 是对角矩阵,元素是特征向量的特征值

    如下所示,对于这个方阵,我们可以很轻松的得到矩阵分解后的结果。

    image-20221022173227245

    具体的计算步骤大家不用担心,可以直接在python中轻松实现,代码如下:

    import numpy as np
    A = np.array([[5,3],[1,1]])
    
    lam,U=np.linalg.eig(A)#特征分解
    inv = np.linalg.inv(U)#求逆矩阵
    print(A)
    print('特征值:',lam)
    print('特征向量',U)
    print('特征向量的逆',inv)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    [[5 3]
     [1 1]]
    特征值: [5.64575131 0.35424869]
    特征向量 [[ 0.97760877 -0.54247681]
     [ 0.21043072  0.84007078]]
    特征向量的逆 [[ 0.89807344  0.5799321 ]
     [-0.2249599   1.04510774]]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    现在,我们已经解决了对于对称的矩阵分解问题。但是,在实际中很多矩阵都是 非对称的矩阵,A不是方阵,维度为m*n。这种情况就需要使用我们的SVD(奇异值分解)方法

    SVD矩阵分解

    基本理论

    对于非对称矩阵,我们可以通过下面的方式将其变换为对称矩阵 A A T AA^T AAT A T A A^TA ATA

    A和A的转置矩阵进行相乘,得到对称方阵,然后我们有

    image-20221022175009435

    此时 Λ 1 , Λ 2 \Lambda_1,\Lambda_2 Λ1,Λ2均为对角矩阵,具有相同的非零特征值。

    假设这些特征值为 σ 1 , σ 2 , . . . σ k \sigma_1,\sigma_2,...\sigma_k σ1,σ2,...σkk不超过m和n,也就是k<=min(m,n)

    此时矩阵A的特征值

    image-20221022175246892

    我们可以得到为奇异值分解

    image-20221022175315614

    P为左奇异矩阵,m*m维

    Q为右奇异矩阵,n*n维。如下图所示

    image-20221022175353793

    Λ对角线上的非零元素为特征值λ1, λ2, … , λk

    在推荐系统中

    左奇异矩阵:User矩阵

    右奇异矩阵:Item矩阵

    接下来我们来看一个具体的例子

    image-20221022175620049

    奇异值分解:

    image-20221022175658303

    λ 1 λ_1 λ1为特征值, p 1 p_1 p1为左奇异矩阵的特征向量, q 1 q_1 q1为右奇异矩阵的特征向量

    具体的数学推导大家不用太过于纠结,我们一样在python中实现。

    from scipy.linalg import svd
    import numpy as np
    from scipy.linalg import svd
    A = np.array([[1,2],
    	    [1,1],
    	    [0,0]])
    p,s,q = svd(A,full_matrices=False)
    print('P=', p)
    print('S=', s)
    print('Q=', q)
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    P= [[-0.85065081 -0.52573111]
     [-0.52573111  0.85065081]
     [ 0.          0.        ]]
    S= [2.61803399 0.38196601]
    Q= [[-0.52573111 -0.85065081]
     [ 0.85065081 -0.52573111]]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    Excellent!现在给我们一个任意的矩阵,我们都可以使用SVD将其进行分解,那么我们如何理解SVD的作用呢?

    对于奇异值,它跟我们特征分解中的特征值类似,在奇异值矩阵中也是按照从大到小排列,并且往往前10%甚至1%的奇异值的和就占了全部的奇异值之和的90%以上。也就是说,我们也可以用最大的k个的奇异值和对应的奇异向量来近似描述矩阵。如下图所示,这样大大的减少了计算成本。直观上也就是用较少的特征反应了绝大部分信息,因此在本质上SVD是一个降纬的作用,类似我们的主成分分析。

    image-20221022181719567

    SVD的应用

    现在我们来看看对于特征值矩阵,我们如果只包括某部分特征值,结果会怎样?

    矩阵A:大小为1440*1080的图片

    • Step1,将图片转换为矩阵

    • Step2,对矩阵进行奇异值分解,得到p,s,q

    • Step3,包括特征值矩阵中的K个最大特征值,其余特征值设置为0

    • Step4,通过p,s’,q得到新的矩阵A’,对比A’与A的差别

    import numpy as np
    from scipy.linalg import svd
    from PIL import Image
    import matplotlib.pyplot as plt
    
    # 取前k个特征,对图像进行还原
    def get_image_feature(s, k):
    	# 对于S,只保留前K个特征值
    	s_temp = np.zeros(s.shape[0])
    	s_temp[0:k] = s[0:k]
    	s = s_temp * np.identity(s.shape[0])
    	# 用新的s_temp,以及p,q重构A
    	temp = np.dot(p,s)
    	temp = np.dot(temp,q)
    	plt.imshow(temp, cmap=plt.cm.gray, interpolation='nearest')
    	plt.show()
    	print(A-temp)
    
    
    # 加载256色图片
    image = Image.open('./256.bmp') 
    A = np.array(image)
    # 显示原图像
    plt.imshow(A, cmap=plt.cm.gray, interpolation='nearest')
    plt.show()
    # 对图像矩阵A进行奇异值分解,得到p,s,q
    p,s,q = svd(A, full_matrices=False)
    # 取前k个特征,对图像进行还原
    get_image_feature(s, 5)
    get_image_feature(s, 50)
    get_image_feature(s, 500)
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    image-20221022182358145

    可以看出,当k=50的时候,效果就很不错了, 此时,我们只需要保存 (1440+1+1080)*50=126050个元素,占比126050/(1440*1080)=8%

    因此,我们只用少量的信息(比如10%),可以还原大部分图像信息(比如99%)

    将user-item评分问题,转化为SVD矩阵分解

    image-20221022183641048

    如果我们想要看user2对item3评分,可以得到

    4 = − 0.46 ∗ 16.47 ∗ ( − 0.29 ) + ( − 0.30 ) ∗ 6.21 ∗ ( − 0.38 ) + ( − 0.65 ) ∗ 4.40 ∗ ( − 0.13 ) + 0.28 ∗ 2.90 ∗ 0.87 + 0.02 ∗ 1.58 ∗ ( − 0.03 ) 4=-0.46*16.47*(-0.29)+(-0.30)*6.21*(-0.38)+(-0.65)*4.40*(-0.13)+0.28*2.90*0.87+0.02*1.58*(-0.03) 4=0.4616.47(0.29)+(0.30)6.21(0.38)+(0.65)4.40(0.13)+0.282.900.87+0.021.58(0.03)

    A中各元素=user行向量,item列向量,奇异值的加权内积

    实际上,我们发现user矩阵的最后一列是没有用到的,而且我们还可以使用更少的特征,比如特征个数=2

    得到近似解A’

    image-20221022183728536

    传统SVD在推荐系统中的应用

    我们可以通过k来对矩阵降维

    image-20221022183807116

    第i个用户对第j个物品的评分

    image-20221022183810756

    完整的SVD,可以将M无损的分解成为三个矩阵

    为了简化矩阵分解,我们可以使用k,远小于min(m,n),对矩阵M近似还原。乍一看,感觉没什么问题,但是传统SVD在使用上是有局限

    • SVD分解要求矩阵是稠密的 => 矩阵中的元素不能有缺失。

    • 因此实际上传统SVD更适合做降维

    SVD的要求是稠密的,但是一般的推荐问题中,数据都是很稀疏的。 因此我们需要使用一个近似的方法FunkSVD

    FunkSVD

    FunkSVD的基本思想如下:

    • 避开稀疏问题,而且只用两个矩阵进行相乘,如下所示

    M m × n ≈ P m × k T Q k × n M_{m\times n}\approx P_{m\times k}^TQ_{k\times n} Mm×nPm×kTQk×n

    • 损失函数=P和Q矩阵乘积得到的评分,与实际用户评分之差
    • 让损失函数最小化 => 最优化问题。

    然后我们就有以下

    m i n ∑ p , q ( m i j − q j T p i ) 2 + λ ( ∣ ∣ p i ∣ ∣ 2 + ∣ ∣ q j ∣ ∣ 2 ) min\sum_{p,q}(m_{ij}-q_j^Tp_i)^2+\lambda(||p_i||^2+||q_j||^2) minp,q(mijqjTpi)2+λ(∣∣pi2+∣∣qj2)

    到这里是不是有一种很熟悉的感觉,这里与我们之介绍的ALS矩阵分解是一样的,只不过当时我们使用ALS方法进行优化,在这里我们使用梯度下降法(SGD)

    Step1,通过梯度下降法,求解P和Q使得损失函数最小化

    Step2,通过P和Q将矩阵补全

    Step3,针对某个用户i,查找之前值缺失的位置,按照补全值从大到小进行推荐

    具体代码实现如下:

    数据集:MovieLens

    下载地址:https://www.kaggle.com/jneupane12/movielens/download

    主要使用的文件:ratings.csv

    格式:userId, movieId, rating, timestamp

    记录了用户在某个时间对某个movieId的打分情况

    在这里我们使用的数据依然是MovieLens数据集,大家可以自行下载。下面看具体代码,要注意的是,我们要设置algo = SVD(biased=False)

    !pip install surprise
    from surprise import SVD
    from surprise import Dataset
    from surprise.model_selection import cross_validate
    from surprise import Reader
    from surprise import accuracy
    from surprise.model_selection import KFold
    import pandas as pd
    import time
    
    time1=time.time()
    
    # 数据读取
    reader = Reader(line_format='user item rating timestamp', sep=',', skip_lines=1)
    data = Dataset.load_from_file('/content/drive/MyDrive/02名企/4.1 SVD矩阵分解与基于内容的推荐/L4-code/L4/MovieLens/ratings.csv', reader=reader)
    train_set = data.build_full_trainset()
    
    # 使用funkSVD
    algo = SVD(biased=False)#这里默认是True,使用的是biasSVD
    
    # 定义K折交叉验证迭代器,K=3
    kf = KFold(n_splits=3)
    for trainset, testset in kf.split(data):
        # 训练并预测
        algo.fit(trainset)
        predictions = algo.test(testset)
        # 计算RMSE
        accuracy.rmse(predictions, verbose=True)
    
    uid = str(196)
    iid = str(302)
    # 输出uid对iid的预测结果
    pred = algo.predict(uid, iid, r_ui=4, verbose=True)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    RMSE: 0.8724
    RMSE: 0.8712
    RMSE: 0.8728
    user: 196        item: 302        r_ui = 4.00   est = 4.33   {'was_impossible': False}
    
    • 1
    • 2
    • 3
    • 4

    biasSVD算法

    BiasSVD算法原理:

    用户有自己的偏好(Bias),比如乐观的用户打分偏高

    商品也有自己的偏好(Bias),比如质量好的商品,打分偏高

    将与个性化无关的部分,设置为偏好(Bias)部分
    m i n ∑ i , j ( m i j − μ − b i − b j − q j T p i ) 2 + λ ( ∣ ∣ b i ∣ ∣ 2 + ∣ ∣ b j ∣ ∣ 2 + ∣ ∣ p i ∣ ∣ 2 + ∣ ∣ q j ∣ ∣ 2 ) min\sum_{i,j}(m_{ij}-\mu-b_i-b_j-q_j^Tp_i)^2+\lambda(||b_i||^2+||b_j||^2+||p_i||^2+||q_j||^2) mini,j(mijμbibjqjTpi)2+λ(∣∣bi2+∣∣bj2+∣∣pi2+∣∣qj2)

    u u u:表示表示所有数据的平均值

    b i b_i bi:表示用户的评分倾向

    b j b_j bj:表示商品的评分倾向

    p i q j T p_iq_j^T piqjT:表示个性化推荐部分

    这里和我们之前的baseline方法很类似,但是在这里,我们增加了一个个性化推荐部分

    下面来看代码实现

    biasSVD的实现和funkSVD基本一致,但是这个时候我们直接设置algo = SVD()

    !pip install surprise
    from surprise import SVD
    from surprise import Dataset
    from surprise.model_selection import cross_validate
    from surprise import Reader
    from surprise import accuracy
    from surprise.model_selection import KFold
    import pandas as pd
    import time
    
    time1=time.time()
    
    # 数据读取
    reader = Reader(line_format='user item rating timestamp', sep=',', skip_lines=1)
    data = Dataset.load_from_file('/content/drive/MyDrive/02名企/4.1 SVD矩阵分解与基于内容的推荐/L4-code/L4/MovieLens/ratings.csv', reader=reader)
    train_set = data.build_full_trainset()
    
    # 使用biasSVD
    algo = SVD()
    
    # 定义K折交叉验证迭代器,K=3
    kf = KFold(n_splits=3)
    for trainset, testset in kf.split(data):
        # 训练并预测
        algo.fit(trainset)
        predictions = algo.test(testset)
        # 计算RMSE
        accuracy.rmse(predictions, verbose=True)
    
    uid = str(196)
    iid = str(302)
    # 输出uid对iid的预测结果
    pred = algo.predict(uid, iid, r_ui=4, verbose=True)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    RMSE: 0.8437
    RMSE: 0.8466
    RMSE: 0.8432
    user: 196        item: 302        r_ui = 4.00   est = 4.09   {'was_impossible': False}
    
    • 1
    • 2
    • 3
    • 4

    从这个结果来看,BiasSVD比FunkSVD效果是要好一点的,因为我们增加了要估计的参数。

    SVD++

    在bias的基础上加入了隐性评分

    SVD++算法原理:

    在BiasSVD算法基础上进行了改进,考虑用户的隐式反馈

    隐式反馈:没有具体的评分,但可能有点击浏览等行为

    对于某一个用户i,假设他的隐式反馈item集合为I(i)

    用户i对商品j对应的隐式反馈修正值为 c i j c_{ij} cij

    用户i所有的隐式反馈修正值之和为 ∑ s ∈ N ( i ) c s j \sum_{s\in N(i)}c_{sj} sN(i)csj

    优化目标函数:
    在这里插入图片描述

    在考虑用户隐式反馈的情况下,最终得到P和Q

    代码实现如下:

    需要注意的是SVD++模型需要导入SVDpp,其余用法一致。SVDpp跑的时间要久一些

    #!pip install surprise
    from surprise import SVDpp
    from surprise import Dataset
    from surprise.model_selection import cross_validate
    from surprise import Reader
    from surprise import accuracy
    from surprise.model_selection import KFold
    import pandas as pd
    import time
    
    time1=time.time()
    
    # 数据读取
    reader = Reader(line_format='user item rating timestamp', sep=',', skip_lines=1)
    data = Dataset.load_from_file('/content/drive/MyDrive/02名企/4.1 SVD矩阵分解与基于内容的推荐/L4-code/L4/MovieLens/ratings.csv', reader=reader)
    train_set = data.build_full_trainset()
    
    # 使用svd++
    algo = SVDpp()
    
    # 定义K折交叉验证迭代器,K=3
    kf = KFold(n_splits=3)
    for trainset, testset in kf.split(data):
        # 训练并预测
        algo.fit(trainset)
        predictions = algo.test(testset)
        # 计算RMSE
        accuracy.rmse(predictions, verbose=True)
    
    uid = str(196)
    iid = str(302)
    # 输出uid对iid的预测结果
    pred = algo.predict(uid, iid, r_ui=4, verbose=True)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    RMSE: 0.8427
    RMSE: 0.8416
    RMSE: 0.8412
    user: 196        item: 302        r_ui = 4.00   est = 4.08   {'was_impossible': False}
    
    • 1
    • 2
    • 3
    • 4

    可以看出,SVD++相对于BiasSVD效果差不多。但是需要计算的时间要长。

    总结

    奇异值分解(SVD)可以对矩阵进行无损分解

    在实际中,我们可以抽取前K个特征,对矩阵进行降维,使得只使用较少的特征就可以包含大部分信息 (10%的特征包含99%的信息)

    在评分预测中我们使用funkSVD,只根据实际评分误差进行目标最优化,这里其实就是我们上一章所用的矩阵分解方法,只不过在这里我们使用的优化方法不再是ALS而是SGD(随机梯度下降)

    funkSVD的基础上,加入用户/商品偏好 => BiasSVD,这里可以类比上一章介绍的baseline方法。

    BiasSVD的基础上,考虑用户的隐式反馈 => SVD++

    现在,我们基本把矩阵分解的基本介绍完成了,但是矩阵分解(MF)存在不足,因为它只考虑了user和item特征,对于其他特征的利用我们需要使用新的工具,后续会介绍因子分解机(FM)和DeepFM模型

  • 相关阅读:
    Flutter开发指南
    计算机网络传输层常见问题总结
    开启新的旅途啦~
    【C语言】文件相关函数详解
    PTE-RA总结
    Splunk serverclass 没有生效
    第五篇、Callable接口实现多线程
    【.Net Core】程序相关各种全局文件
    Spring基础(八):注解方式创建对象IOC
    Redis的发布订阅在SpringMVC(或xml配置)项目中使用(注意版本兼容问题)
  • 原文地址:https://blog.csdn.net/weixin_45052363/article/details/127635847