当前位置:   article > 正文

《推荐系统笔记(六)》svd在推荐系统中的应用推广(FunkSVD,BiasSVD以及SVD++)及简单实战(surprise库)_基于funksvd分解的动漫推荐

基于funksvd分解的动漫推荐

前言

奇异值分解(SVD)可以将任意矩阵分解成两个方阵和一个对角矩阵的乘积。借助于SVD,我们可以将推荐系统中的用户-评分矩阵进行分解,通过推广的SVD方法(FunkSVD,BiasSVD和SVD++),我们还可以进一步做评分矩阵的补全。根据补全的评分矩阵,我们将可以给用户推荐他没有购买过的产品。因此,SVD方法在推荐系统中具有重要的应用价值。

下面,我们将逐个介绍 原始SVDFunkSVDBiasSVD以及SVD++。介绍完这些算法之后,我们将在movielens数据集上,用第三方库surprise进行简单的评分预测。

SVD

1. 原始SVD

对于任意 m × n m\times n m×n矩阵 A A A,通过奇异值分解,有严格等式
A = P Σ Q T A=P\Sigma Q^T A=PΣQT

其中, P P P Q Q Q是方阵, Σ \Sigma Σ是对角矩阵。

进一步的, P P P的列向量是 m × m m\times m m×m方阵 A A T AA^T AAT的特征向量; Q Q Q的列向量是 n × n n\times n n×n方阵 A T A A^TA ATA的特征向量; m × n m\times n m×n矩阵 Σ \Sigma Σ的对角线元素是 A A A的特征值。

通过上面的奇异值分解,我们可以将用户-评分矩阵 R R R分解为三个矩阵的乘积。

虽然SVD能够将评分矩阵 R R R分解,但是存在以下问题

  • 原始SVD过程要求评分矩阵 R R R是稠密的,但是通常评分矩阵是很稀疏的,这就无法做SVD
  • 可以通过填充来让评分矩阵稠密,但是,无论如何填充,都会引入噪音

实际上,原始SVD实际上更加适合于数据降维,可以参考我的博客 用SVD做图像降维

2. FunkSVD

用户-评分矩阵 R R R通常是很稀疏的,原始SVD无法使用,因此,我们提出一个近似的矩阵分解算法,FunkSVD。

与MF类似,对于 m × n m\times n m×n评分矩阵 R R R,我们假设由两个矩阵 P k × m P_{k\times m} Pk×m Q k × n Q_{k\times n} Qk×n的乘积近似得到,这就意味着下式的成立
R ≈ P T Q R\approx P^TQ RPTQ

对于上面的式子,我们可以这样理解

  • 评分矩阵 R R R m × n m\times n m×n的,这就是说,有 m m m个用户和 n n n个商品,每个用户不可能使用所有商品,因此仅仅能对其中少量的商品进行打分,这也就是评分矩阵稀疏的原因
  • P P P k ∗ m k*m km维的,这就是说,我们可以把第 i i i列向量当做用户 i i i的特征 p i p_i pi,这个特征 p i p_i pi k k k维的
  • Q Q Q k ∗ n k*n kn维的,这就是说,我们可以把第 j j j列向量当做商品 j j j的特征 q j q_j qj,这个特征 q j q_j qj k k k维的
  • 一个比较通俗的例子是这样的:
  1. 我们有100个用户和1000部电影,用户对看过的电影打分,从而形成用户-商品评分矩阵 R 100 × 1000 R_{100\times1000} R100×1000
  2. 现在,为了更好的区分不同用户和电影,我们给出3种特征( k = 3 k=3 k=3),分别为 动作,爱情,悬疑;
  3. 第1个用户的特征 p 1 = [ 0.8 , 0.2 , 0.1 ] T p_1=[0.8, 0.2, 0.1]^T p1=[0.8,0.2,0.1]T,意思就是这个用户更加偏爱动作,第2部电影的特征 q 2 = [ 0.3 , 0.2 , 0.6 ] T q_2=[0.3, 0.2, 0.6]^T q2=[0.3,0.2,0.6]T,意思是这个电影更加偏向于悬疑;
  4. 那么,第一个用户看完第二部电影之后的评分,我们预测为 p 1 T q 2 = 0.34 p_1^Tq_2=0.34 p1Tq2=0.34,这就是一个综合的喜爱程度。

我们想得到这样的 P P P Q Q Q,并使得 P T Q P^TQ PTQ尽量接近 R R R。注意, R R R中仅有少量位置存在值,我们求的 P T Q P^TQ PTQ就是尽量接近这些值。

假设 R R R中非空位置集合为 K K K,我们有如下最优化问题
min ⁡ P , Q L = 1 2 ∑ ( i , j ) ∈ K ( R i , j − p i T q j ) 2 + λ 2 ( ∑ i = 1 m ∣ p i ∣ 2 + ∑ j = 1 n ∣ q j ∣ 2 ) \min_{P,Q}L= \frac{1}{2}\sum_{(i, j)\in K}(R_{i, j}-p_i^Tq_j)^2+\frac{\lambda}{2}(\sum_{i=1}^m\lvert p_i \rvert^2+\sum_{j=1}^n|q_j|^2) P,QminL=21(i,j)K(Ri,jpiTqj)2+2λ(i=1mpi2+j=1nqj2)

直接对损失函数求导,我们有
∂ L ∂ p i = ∑ j ( p i T q j − R i , j ) q j + λ p i = ( ∑ j q j q j T + λ I ) p i − ∑ j R i , j q j

Lpi=j(piTqjRi,j)qj+λpi=(jqjqjT+λI)pijRi,jqj
piL==j(piTqjRi,j)qj+λpi(jqjqjT+λI)pijRi,jqj

∂ L ∂ q j = ∑ i ( p i T q j − R i , j ) p i + λ q j = ( ∑ i p i p i T + λ I ) q j − ∑ i R i , j p i
Lqj=i(piTqjRi,j)pi+λqj=(ipipiT+λI)qjiRi,jpi
qjL==i(piTqjRi,j)pi+λqj(ipipiT+λI)qjiRi,jpi

因此,有更新策略
p i ← p i − α ∂ L ∂ p i p_i\leftarrow p_i-\alpha\frac{\partial L}{\partial p_i} pipiαpiL

q j ← q j − α ∂ L ∂ q j q_j\leftarrow q_j-\alpha\frac{\partial L}{\partial q_j} qjqjαqjL

其中, α \alpha α是学习率。

通过以上迭代更新,可以得到 P P P Q Q Q,从而得到近似的评分矩阵 P T Q P^TQ PTQ,从而补全评分矩阵。

3. BiasSVD

在FunkSVD的基础上,我们进一步考虑用户偏好和商品偏好。

比如,对于某些用户而言,他们打分一向比较高,而对于较为苛刻的用户,他们打分又偏低;对于某些高质量商品而言,给它们的评分一般偏高,比如泰坦尼克电影评分4.9,可能这部电影没有那么好,被高估了。

基于以上观察,我们有,不同用户有自己的打分习惯,或者偏高或者偏低;不同电影也有自己的分数倾向,或者倾向于低分或者倾向于高分。我们需要在模型中加以体现。

μ \mu μ为评分的平均值, b i b_i bi为用户 i i i的偏好带来的评分偏置, b j b_j bj为商品 j j j的质量带来的评分偏置。这样,用户 i i i对商品 j j j的评分可以写为
μ + b i + b j + p i T q j \mu+b_i+b_j+p_i^Tq_j μ+bi+bj+piTqj

因此,我们的目标函数可以写为
min ⁡ P , Q , b i , b j L = 1 2 ∑ ( i , j ) ∈ K ( R i , j − μ − b i − b j − p i T q j ) 2 + λ 2 ( ∑ i = 1 m ∣ b i ∣ 2 + ∑ j = 1 n ∣ b j ∣ 2 + ∑ i = 1 m ∣ p i ∣ 2 + ∑ j = 1 n ∣ q j ∣ 2 )

minP,Q,bi,bjL=12(i,j)K(Ri,jμbibjpiTqj)2+λ2(i=1m|bi|2+j=1n|bj|2+i=1m|pi|2+j=1n|qj|2)
minP,Q,bi,bjL=21(i,j)K(Ri,jμbibjpiTqj)2+2λ(i=1mbi2+j=1nbj2+i=1mpi2+j=1nqj2)

类似的,对目标函数求导,可以写出更新策略,这里略过。

4. SVD++

在BiasSVD的基础上,我们进一步考虑用户隐式反馈的影响。

用户除了对商品有评分这一显式反馈之外,还有诸如浏览、点击等隐式反馈。一个用户可能对许多商品有隐式反馈,我们将用户 i i i有过隐式反馈的商品集合记为 N ( i ) N(i) N(i),每一次对于特定商品 s ∈ N ( i ) s\in N(i) sN(i) 的点击或者浏览,都带来对于用户特征 p i p_i pi的某些偏置 y s y_s ys

这样,对于用户 i i i,最终他的特征 p i p_i pi可以写为
p i + ∑ s ∈ N ( i ) y s p_i+\sum_{s\in N(i)}y_s pi+sN(i)ys

因此,用户 i i i对商品 j j j的评分可以写为
μ + b i + b j + q j T ( p i + ∑ s ∈ N ( i ) y s ) \mu+b_i+b_j+q_j^T(p_i+\sum_{s\in N(i)}y_s) μ+bi+bj+qjT(pi+sN(i)ys)

这样, 我们的目标函数可以写为
min ⁡ P , Q , b i , b j , y s L = 1 2 ∑ ( i , j ) ∈ K ( R i , j − μ − b i − b j − q j T ( p i + ∑ s ∈ N ( i ) y s ) ) 2 + λ 2 ( ∑ i = 1 m ∣ b i ∣ 2 + ∑ j = 1 n ∣ b j ∣ 2 + ∑ i = 1 m ∣ p i ∣ 2 + ∑ j = 1 n ∣ q j ∣ 2 + ∑ i = 1 m ∑ s ∈ N ( i ) ∣ y s ∣ 2 )

minP,Q,bi,bj,ysL=12(i,j)K(Ri,jμbibjqjT(pi+sN(i)ys))2+λ2(i=1m|bi|2+j=1n|bj|2+i=1m|pi|2+j=1n|qj|2+i=1msN(i)|ys|2)
minP,Q,bi,bj,ysL=21(i,j)K(Ri,jμbibjqjT(pi+sN(i)ys))2+2λ(i=1mbi2+j=1nbj2+i=1mpi2+j=1nqj2+i=1msN(i)ys2)

同样用梯度下降方法,我们可以对变量进行迭代。

简单实战

这里,我们选用movielens的ratings数据集,利用第三方库surprise进行简单的TopN预测。

movielens数据集在本人的免费资源里面,请自取。

# 第三方库
import pandas as pd
from surprise import SVD, SVDpp
from surprise import Dataset, Reader
from surprise import accuracy
from surprise.model_selection import KFold, train_test_split
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
# 数据读取
# 注意:surprise读取数据需要自己的阅读器和格式,直接使用pandas不行的

# 定义阅读器框架
reader = Reader(line_format='user item rating')

# 利用pandas读取数据
df_data = pd.read_csv(r'D:\myfile\开课吧\推荐系统\第六节\movielens\ratings.csv', usecols=['userId', 'movieId', 'rating'])

# 将数据转化为surprise特定数据集
data = Dataset.load_from_df(df_data, reader)
print(data)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
1. 比较不同SVD算法
# 比较funkSVD,BiasSVD和SVDpp的预测效果,指标为rmse

# funkSVD
algo = SVD(n_factors=30, biased=False)
kf = KFold(n_splits=5)
error = []
for trainset, testset in kf.split(data):
    algo.fit(trainset) # 训练模型
    predictions = algo.test(testset) # 预测
    cur_error = accuracy.rmse(predictions) # 计算误差
    error.append(cur_error)

mean_error = sum(error) / len(error)
print('funkSVD的平均误差为', mean_error)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
# funkSVD误差
RMSE: 0.8478
RMSE: 0.8509
RMSE: 0.8495
RMSE: 0.8487
RMSE: 0.8495
funkSVD的平均误差为 0.8492727275931049
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
# biasSVD
algo = SVD(n_factors=30, biased=True)
kf = KFold(n_splits=5)
error = []
for trainset, testset in kf.split(data):
    algo.fit(trainset) # 训练模型
    predictions = algo.test(testset) # 预测
    cur_error = accuracy.rmse(predictions) # 计算误差
    error.append(cur_error)

mean_error = sum(error) / len(error)
print('biasSVD的平均误差为', mean_error)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
# biasSVD误差
RMSE: 0.8321
RMSE: 0.8327
RMSE: 0.8312
RMSE: 0.8323
RMSE: 0.8320
biasSVD的平均误差为 0.8320637234052113
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
# SVD++
algo = SVDpp(n_factors=30)
kf = KFold(n_splits=5)
error = []
for trainset, testset in kf.split(data):
    algo.fit(trainset) # 训练模型
    predictions = algo.test(testset) # 预测
    cur_error = accuracy.rmse(predictions) # 计算误差
    error.append(cur_error)

mean_error = sum(error) / len(error)
print('SVD++的平均误差为', mean_error)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

这里SVD++算法相对复杂,需要运行较长时间,这里我就没有等到运算出结果了,可以预期到误差应该更低的。

2. TopN预测

这里,我们的思路是

  • 创建字典user_items,记录用户看过的电影
  • 创建userID和itemID,分别记录所有用户ID和电影ID
  • 训练BiasSVD,有alg
  • 遍历所有可能的用户-商品组合,找出用户没有购买过的商品,并用alg预测评分,将这一结果保存在字典user_newitems_ratings中
  • 通过评分排序,找出user评分最高的N个商品
# 创立字典user_items,记录user用过哪些movie
user_items = {}
# 创建userID和itemID,分别记录所有的user和item
userID, itemID = [], []

# 将data数据集变成操作的数据集
data = data.build_full_trainset()

# 遍历数据集data,生成user_items列表
for user, item, rating in data.all_ratings():
    user_items.setdefault(user, [])
    user_items[user].append(item)
    
    if item not in itemID:
        itemID.append(item)
        
userID = list(user_items.keys())
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
# TopN推荐
# 遍历数据集,找出user没有购买过的商品
# 用user_newitems_ratings列表记录对没有购买过的商品的预估评分
user_newitems_ratings = {}

# biasSVD
alg = SVD(n_factors=30)
alg.fit(data) # 训练算法  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
# 遍历所有可能的用户-物品对,找出用户没有购买过的商品
for user in userID:
    user_newitems_ratings.setdefault(user, {})
    
    for item in itemID:
        if item not in user_items[user]: # user没有购买过的商品 
            # 预测用户评分
            user_newitems_ratings[user][item] = alg.predict(user, item, verbose=False).est    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
# 对用户评分进行排序
for user in user_newitems_ratings.keys():
    user_newitems_ratings[user] = sorted(user_newitems_ratings[user].items(), key=lambda x: x[1], reverse=True)
  • 1
  • 2
  • 3
# 获取user的topN推荐
def get_topN(user, N):
    return user_newitems_ratings[user][:N]
  • 1
  • 2
  • 3
# 例子:user=0,N=4
get_topN(0, 4)
  • 1
  • 2
# 预测结果
[(318, 4.460564726496344),
 (6271, 4.427441820009547),
 (3090, 4.416865860322598),
 (7502, 4.409934931201513)]
  • 1
  • 2
  • 3
  • 4
  • 5
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/菜鸟追梦旅行/article/detail/713995
推荐阅读
相关标签
  

闽ICP备14008679号