Part5 矩阵分解SVD

系列文章目录

Part1 推荐系统基础
Part2 Movienles介
Part3 协同过滤基础
Part4 协同过滤进阶
Part5 矩阵分解SVD



前言

之前几篇文章介绍的协同过滤算法,协同过滤算法不以依赖用户或者物品本身的属性,而依赖于物品和用户之间的交互信息,它的可解释性很强,同时也很直观。但是协同过滤的问题就是,它处理稀疏矩阵的能力很弱,会浪费大量计算资源,所以为了解决这个问题,协同过滤衍生出了矩阵分解模型(Matrix Factorization,MF)或者叫隐语义模型。它的思想就是,在协同过滤的基础上,使用稠密的隐向量来表示用户和物品,挖掘用户的隐含兴趣和物品的隐含特征,从而弥补协同过滤的不足之处。


一、MF(隐语义模型)是什么?

隐语义模型最早在文本领域被提出,用于找到文本的隐含语义。在2006年, 被用于推荐中, 它的核心思想是通过隐含特征(latent factor)联系用户兴趣和物品(item), 基于用户的行为找出潜在的主题和分类, 然后对item进行自动聚类,划分到不同类别/主题(用户的兴趣)。
下面拿项亮老师《推荐系统实践》里面的那个例子来解释一下:
如果我们知道了用户A和用户B两个用户在豆瓣的读书列表, 从他们的阅读列表可以看出,用户A的兴趣涉及侦探小说、科普图书以及一些计算机技术书, 而用户B的兴趣比较集中在数学和机器学习方面。 那么如何给A和B推荐图书呢?

先说说协同过滤算法, 这样好对比不同:
对于UserCF,首先需要找到和他们看了同样书的其他用户(兴趣相似的用户),然后给他们推荐那些用户喜欢的其他书。
对于ItemCF,需要给他们推荐和他们已经看的书相似的书,比如作者B看了很多关于数据挖掘的书,可以给他推荐机器学习或者模式识别方面的书。
而如果是隐语义模型的话, 它会先通过一些角度把用户兴趣和这些书归一下类, 当来了用户之后, 首先得到他的兴趣分类, 然后从这个分类中挑选他可能喜欢的书籍。

这里就看到了隐语义模型和协同过滤的不同, 这里说的角度其实就是这个隐含特征, 对于一本书来说,它的内容, 作者, 年份, 主题等都可以算隐含特征。下面放上王喆老师《深度学习推荐系统》的一个原理图作为对比, 帮助我们更直观的理解两种算法:
在这里插入图片描述
我们下面拿一个音乐评分的例子来具体看一下隐特征矩阵的含义。
假设每个用户都有自己的听歌偏好, 比如A喜欢带有小清新的, 吉他伴奏的, 王菲的歌曲,如果一首歌正好是王菲唱的, 并且是吉他伴奏的小清新, 那么就可以将这首歌推荐给这个用户。 也就是说是小清新, 吉他伴奏, 王菲这些元素连接起了用户和歌曲。 当然每个用户对不同的元素偏好不同, 每首歌包含的元素也不一样, 所以我们就希望找到下面的两个矩阵:

  • 潜在因子—— 用户矩阵Q 这个矩阵表示不同用户对于不同元素的偏好程度, 1代表很喜欢, 0代表不喜欢, 比如下面这样:
    在这里插入图片描述
  • 潜在因子——音乐矩阵P 表示每种音乐含有各种元素的成分, 比如下表中, 音乐A是一个偏小清新的音乐, 含有小清新的Latent Factor的成分是0.9, 重口味的成分是0.1, 优雅成分0.2…
    在这里插入图片描述

利用上面的这两个矩阵, 我们就能得出张三对音乐A的喜欢程度:
张三对小清新的偏好 * 音乐A含有小清新的成分 + 张三对重口味的偏好 * 音乐A含有重口味的成分 + 张三对优雅的偏好 * 音乐A含有优雅的成分…,
下面是对应的两个隐向量:
在这里插入图片描述
根据隐向量其实就可以得到张三对音乐A的打分,即: 0.6 ∗ 0.9 + 0.8 ∗ 0.1 + 0.1 ∗ 0.2 + 0.1 ∗ 0.4 + 0.7 ∗ 0 = 0.69 0.6 * 0.9 + 0.8 * 0.1 + 0.1 * 0.2 + 0.1 * 0.4 + 0.7 * 0 = 0.69 0.60.9+0.80.1+0.10.2+0.10.4+0.70=0.69 按照这个计算方式, 每个用户对每首歌其实都可以得到这样的分数, 最后就得到了我们的评分矩阵:
在这里插入图片描述
这里的红色表示用户没有打分,我们通过隐向量计算得到的。

上面例子中的小清晰, 重口味, 优雅这些就可以看做是隐含特征, 而通过这个隐含特征就可以把用户的兴趣和音乐的进行一个分类, 其实就是找到了每个用户每个音乐的一个隐向量表达形式(embedding的原理其实也是这样, 那里是找到每个词的隐向量表达), 这个隐向量就可以反映出用户的兴趣和物品的风格,并能将相似的物品推荐给相似的用户等。 有没有感觉到是把协同过滤算法进行了一种延伸, 把用户的相似性和物品的相似性通过了一个叫做隐向量的方式进行表达

但是, 真实的情况下我们其实是没有上面那两个矩阵的, 音乐那么多, 用户那么多, 我们没有办法去找一些隐特征去表示出这些东西, 另外一个问题就是即使能表示也不一定准, 对于每个用户或者每个物品的风格,我们每个人都有不同的看法。 所以事实上, 我们有的只有用户的评分矩阵, 也就是最后的结果, 并且一般这种矩阵长这样:
在这里插入图片描述
这种矩阵非常的稀疏,如果直接基于用户相似性或者物品相似性去填充这个矩阵是不太容易的, 并且很容易出现长尾问题, 所以矩阵分解就可以比较容易的解决这个问题。
矩阵分解模型其实就是在想办法基于这个评分矩阵去找到上面例子中的那两个矩阵, 也就是用户兴趣和物品的隐向量表达, 然后就把这个评分矩阵分解成Q和P两个矩阵乘积的形式, 这时候就可以基于这两个矩阵去预测某个用户对某个物品的评分了。 然后基于这个评分去进行推荐。这就是矩阵分解算法的原理。

三、矩阵分解算法的求解

谈到矩阵分解, 最常用的方法是特征值分解(EVD)或者奇异值分解(SVD), 可以先理解一下到底是怎么回事奇异值分解(SVD)的原理详解及推导, 但是这种方法在这里不适用。

首先是EVD, 它要求分解的矩阵是方阵, 显然用户-物品矩阵不满足这个要求, 而传统的SVD分解, 会要求原始矩阵是稠密的, 而我们这里的这种矩阵一般情况下是非常稀疏的, 如果想用奇异值分解, 就必须对缺失的元素进行填充, 而一旦补全, 空间复杂度就会非常高, 且补的不一定对。 而且SVD分解计算复杂度非常高, 而我们的用户-物品矩阵非常大, 所以基本上无法使用。那应该如何解决这个问题呢?

3.1 Basic SVD

2006年的Netflix Prize之后, Simon Funk公布了一个矩阵分解算法叫做Funk-SVD, 后来被Netflix Prize的冠军Koren称为Latent Factor Model(LFM)。 Funk-SVD的思想很简单: 把求解上面两个矩阵的参数问题转换成一个最优化问题, 可以通过训练集里面的观察值利用最小化来学习用户矩阵和物品矩阵。

我们上面已经知道了, 如果有了用户矩阵和物品矩阵的话, 我们就知道了如果想计算用户 u u u对物品 i i i的评分, 只需要 Preference ⁡ ( u , i ) = r u i = p u T q i = ∑ f = 1 F p u , k q k , i \operatorname{Preference}(u, i)=r_{u i}=p_{u}^{T} q_{i}=\sum_{f=1}^{F} p_{u, k} q_{k,i} Preference(u,i)=rui=puTqi=f=1Fpu,kqk,i 而现在, 我们有真实的 r u , i r_{u,i} ru,i, 但是没有 p u T q i p_{u}^{T} q_{i} puTqi, 那么我们可以初始化一个啊, 随机初始化一个用户矩阵 U U U和一个物品矩阵 V V V, 然后不就有 p u T q i p_{u}^{T} q_{i} puTqi了? 当然你说, 随机初始化的肯定不准啊, 但是, 有了 p u T q i p_{u}^{T} q_{i} puTqi之后, 我们就可以计算一个猜测的 r ^ u i \hat{r}{u i} r^ui, 即 r ^ u i = p u T q i \hat{r}{u i}=p_{u}^{T} q_{i} r^ui=puTqi

这时候, 肯定是不准, 那么这个猜测的和真实值之间就会有一个误差: e u i = r u i − r ^ u i e_{u i}=r_{u i}-\hat{r}_{u i} eui=ruir^ui

有了误差, 我们就可以计算出总的误差平方和: SSE ⁡ = ∑ u , i e u i 2 = ∑ u , i ( r u i − ∑ k = 1 K p u , k q k , i ) 2 \operatorname{SSE}=\sum_{u, i} e_{u i}^{2}=\sum_{u, i}\left(r_{u i}-\sum_{k=1}^{K} p_{u,k} q_{k, i}\right)^{2} SSE=u,ieui2=u,i(ruik=1Kpu,kqk,i)2 有了损失, 我们就可以想办法进行训练, 把SSE降到最小, 那么我们的两个矩阵参数就可以算出来。所以就把这个问题转成了最优化的的问题, 而我们的目标函数就是:

min ⁡ q , p ∑ ( u , i ) ∈ K ( r u i − p u T q i ) 2 \min {\boldsymbol{q}^{}, \boldsymbol{p}^{}} \sum{(u, i) \in K}\left(\boldsymbol{r}{\mathrm{ui}}-p{u}^{T} q_{i}\right)^{2} minq,p(u,i)K(ruipuTqi)2

这里的 K K K表示所有用户评分样本的集合。

这里其实就和训练模型的思路差不多, 我们拿到了一个用户物品的评分矩阵, 而我们要去计算两个参数矩阵U和V, 我们就可以用求解神经网络模型参数的思路计算这两个矩阵:
	1.首先先初始化这两个矩阵
	2.把用户评分矩阵里面已经评过分的那些样本当做训练集的label, 把对应的用户和物品的隐向量当做features, 这样就会得到(features, label)相当于训练集
	3.通过两个隐向量乘积得到预测值pred
	4.根据label和pred计算损失
	5.然后反向传播, 通过梯度下降的方式,更新两个隐向量的值
	6.未评过分的那些样本当做测试集, 通过两个隐向量就可以得到测试集的label值
	7。这样就填充完了矩阵, 下一步就可以进行推荐了
	————————————————
	版权声明:本文为CSDN博主「翻滚的小@强」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
	原文链接:https://blog.csdn.net/wuzhongqiang/article/details/108173885

有了目标函数, 那么我们就可以使用梯度下降算法来降低损失。 那么我们需要对目标函数求偏导, 得到梯度。 我们的目标函数如果是上面的SSE, 我们下面来推导一下最后的导数:

SSE ⁡ = ∑ u , i e u i 2 = ∑ u , i ( r u i − ∑ k = 1 K p u , k q k , i ) 2 \operatorname{SSE}=\sum_{u, i} e_{u i}^{2}=\sum_{u, i}\left(r_{u i}-\sum_{k=1}^{K} p_{u,k} q_{k,i}\right)^{2} SSE=u,ieui2=u,i(ruik=1Kpu,kqk,i)2 首先我们求SSE在 p u , k p_{u,k} pu,k(也就是Q矩阵的第 u u u k k k列)的梯度: ∂ ∂ p u , k S S E = ∂ ∂ p u , k ( e u i 2 ) = 2 e u i ∂ ∂ p u , k e u i = 2 e u i ∂ ∂ p u , k ( r u i − ∑ k = 1 K p u , k q k , i ) = − 2 e u i q k , i \frac{\partial}{\partial p_{u,k}} S S E=\frac{\partial}{\partial p_{u,k}}\left(e_{u i}^{2}\right) =2e_{u i} \frac{\partial}{\partial p_{u,k}} e_{u i}=2e_{u i} \frac{\partial}{\partial p_{u,k}}\left(r_{u i}-\sum_{k=1}^{K} p_{u,k} q_{k,i}\right)=-2e_{u i} q_{k,i} pu,kSSE=pu,k(eui2)=2euipu,keui=2euipu,k(ruik=1Kpu,kqk,i)=2euiqk,i 然后求SSE在 q k , i q_{k,i} qk,i处(也就是V矩阵的第 k k k i i i列)的梯度:

∂ ∂ q k , i S S E = ∂ ∂ p k , i ( e u i 2 ) = 2 e u i ∂ ∂ p k , i e u i = 2 e u i ∂ ∂ p k , i ( r u i − ∑ k = 1 K p u , k q k , i ) = − 2 e u i p u , k \frac{\partial}{\partial q_{k,i}} S S E=\frac{\partial}{\partial p_{k,i}}\left(e_{u i}^{2}\right) =2e_{u i} \frac{\partial}{\partial p_{k,i}} e_{u i}=2e_{u i} \frac{\partial}{\partial p_{k,i}}\left(r_{u i}-\sum_{k=1}^{K} p_{u,k} q_{k,i}\right)=-2e_{u i} p_{u,k} qk,iSSE=pk,i(eui2)=2euipk,ieui=2euipk,i(ruik=1Kpu,kqk,i)=2euipu,k 为了让公式更为简单, 把前面的2给他越掉, 即可以令SSE等于: SSE ⁡ = 1 2 ∑ u , i e u i 2 = 1 2 ∑ u , i ( r u i − ∑ k = 1 K p u k q k i ) 2 \operatorname{SSE}=\frac{1}{2} \sum_{u, i} e_{u i}^{2}=\frac{1}{2} \sum_{u, i}\left(r_{u i}-\sum_{k=1}^{K} p_{u k} q_{k i}\right)^{2} SSE=21u,ieui2=21u,i(ruik=1Kpukqki)2

这时候, 梯度就没有前面的系数了, 有了梯度, 接下来我们就可以用梯度下降算法更新梯度了: p u , k = p u , k − η ( − e u i q k , i ) = p u , k + η e u i q k , i   q k , i = q k , i − η ( − e u i p u , k ) = q k , i + η e u i p u , k p_{u, k}=p_{u,k}-\eta (-e_{ui}q_{k,i})=p_{u,k}+\eta e_{ui}q_{k,i} \ q_{k, i}=q_{k, i}-\eta (-e_{ui}p_{u,k})=q_{k, i}+\eta e_{ui}p_{u,k} pu,k=pu,kη(euiqk,i)=pu,k+ηeuiqk,i qk,i=qk,iη(euipu,k)=qk,i+ηeuipu,k

这里的 η \eta η是学习率, 控制步长用的, 但上面这个有个问题就是当参数很多的时候, 就是两个矩阵很大的时候, 往往容易陷入过拟合的困境, 这时候, 就需要在目标函数上面加上正则化的损失, 就变成了RSVD。

3.2 RSVD

在目标函数中加入正则化参数(加入惩罚项),对于目标函数来说,Q矩阵和V矩阵中的所有值都是变量,这些变量在不知道哪个变量会带来过拟合的情况下,对所有变量都进行惩罚:
在这里插入图片描述
这时候目标函数对参数的导数就发生了变化, 前面的那块没变, 无非就是加入了后面的梯度。 所以此时对p求导, 得到:
在这里插入图片描述
这样, 正则化之后, 梯度的更新公式如下:
在这里插入图片描述

3.3 消除用户和物品打分的偏差

但在实际中, 单纯的 r ^ u i = p u T q i \hat{r}{u i}=p{u}^{T} q_{i} r^ui=puTqi也是不够的, 还要考虑其他的一些因素, 比如一个评分系统, 有些固有的属性和用户物品无关, 而用户也有些属性和物品无关, 物品也有些属性和用户无关。 因此, Netfix Prize中提出了另一种LFM, 在原来的基础上加了偏置项, 来消除用户和物品打分的偏差, 即预测公式如下: r ^ u i = μ + b u + b i + p u T ⋅ q i \hat{r}{u i}=\mu+b{u}+b_{i}+p_{u}^{T} \cdot q_{i} r^ui=μ+bu+bi+puTqi 这个预测公式加入了3项偏置 μ , b u , b i \mu,b_u,b_i μ,bu,bi, 作用如下:

μ \mu μ: 训练集中所有记录的评分的全局平均数。 在不同网站中, 因为网站定位和销售物品不同, 网站的整体评分分布也会显示差异。 比如有的网站中用户就喜欢打高分, 有的网站中用户就喜欢打低分。 而全局平均数可以表示网站本身对用户评分的影响。
b u b_u bu: 用户偏差系数, 可以使用用户 u u u给出的所有评分的均值, 也可以当做训练参数。 这一项表示了用户的评分习惯中和物品没有关系的那种因素。 比如有些用户比较苛刻, 对什么东西要求很高, 那么他评分就会偏低, 而有些用户比较宽容, 对什么东西都觉得不错, 那么评分就偏高
b i b_i bi: 物品偏差系数, 可以使用物品 i i i收到的所有评分的均值, 也可以当做训练参数。 这一项表示了物品接受的评分中和用户没有关系的因素。 比如有些物品本身质量就很高, 因此获得的评分相对比较高, 有的物品本身质量很差, 因此获得的评分相对较低。
加了用户和物品的打分偏差之后, 矩阵分解得到的隐向量更能反映不同用户对不同物品的“真实”态度差异, 也就更容易捕捉评价数据中有价值的信息, 从而避免推荐结果有偏。 注意此时的 S S E SSE SSE会发生变化: SSE ⁡ = 1 2 ∑ u , i e u i 2 + 1 2 λ ∑ u ∣ p u ∣ 2 + 1 2 λ ∑ i ∣ q i ∣ 2 + 1 2 λ ∑ u b u 2 + 1 2 λ ∑ u b i 2   = 1 2 ∑ u , i ( r u i − μ − b u − b i − ∑ k = 1 K p u k q k i ) 2 + 1 2 λ ∑ u ∣ p u ∣ 2 + 1 2 λ ∑ i ∣ q i ∣ 2 + 1 2 λ ∑ u b u 2 + 1 2 λ ∑ u b i 2 \begin{array}{l} \operatorname{SSE}=\frac{1}{2} \sum_{u, i} e_{u i}^{2}+\frac{1}{2} \lambda \sum_{u}\left|\boldsymbol{p}{u}\right|^{2}+\frac{1}{2} \lambda \sum{i}\left|\boldsymbol{q}{i}\right|^{2}+\frac{1}{2} \lambda \sum{u} \boldsymbol{b}{u}^{2}+\frac{1}{2} \lambda \sum{u} \boldsymbol{b}{i}^{2} \ =\frac{1}{2} \sum{u, i}\left(\boldsymbol{r}{u i}-\boldsymbol{\mu}-\boldsymbol{b}{u}-\boldsymbol{b}{i}-\sum{k=1}^{K} \boldsymbol{p}{u k} \boldsymbol{q}{k i}\right)^{2}+\frac{1}{2} \lambda \sum_{u}\left|\boldsymbol{p}{u}\right|^{2}+\frac{1}{2} \lambda \sum{i}\left|\boldsymbol{q}{i}\right|^{2}+\frac{\mathbf{1}}{2} \lambda \sum{u} \boldsymbol{b}{u}^{2}+\frac{1}{2} \lambda \sum{u} \boldsymbol{b}_{i}^{2} \end{array} SSE=21u,ieui2+21λupu2+21λiqi2+21λubu2+21λubi2 =21u,i(ruiμbubik=1Kpukqki)2+21λupu2+21λiqi2+21λubu2+21λubi2 此时如果把 b u b_u bu b i b_i bi当做训练参数的话, 那么它俩的梯度是:

∂ ∂ b u S S E = − e u i + λ b u   ∂ ∂ b i S S E = − e u i + λ b i \frac{\partial}{\partial b_{u}} SSE=-e_{u i}+\lambda b_{u} \ \frac{\partial}{\partial b_{i}} S S E=-e_{u i}+\lambda b_{i} buSSE=eui+λbu biSSE=eui+λbi 更新公式为: b u = b u + η ( e u i − λ b u )   b i = b i + η ( e u i − λ b i ) \begin{aligned} \boldsymbol{b}{u}&=\boldsymbol{b}{\boldsymbol{u}}+\boldsymbol{\eta}\left(\boldsymbol{e}{u i}-\lambda \boldsymbol{b}{\boldsymbol{u}}\right) \ \boldsymbol{b}{\boldsymbol{i}} &=\boldsymbol{b}{\boldsymbol{i}}+\boldsymbol{\eta}\left(\boldsymbol{e}{\boldsymbol{u} i}-\lambda \boldsymbol{b}{\boldsymbol{i}}\right) \end{aligned} bu=bu+η(euiλbu) bi=bi+η(euiλbi) 而对于 p u , k p_{u,k} pu,k p k , i p_{k,i} pk,i, 导数没有变化, 更新公式也没有变化。

3.4 SVD++

前面的LFM模型中并没有显示的考虑用户的历史行为对用户评分预测的影响, 而我们知道, 如果某个用户喜欢电子产品, 并且已经买了很多个电子产品了, 如果我们预测该用户对一个电子产品和对一本书的评分, 那相应的电子产品的评分就会高一些, 而这里面, 他之前的历史行为记录也可以做一个参考。 所以Netflix Prize中提出了一个模型叫做SVD++, 它将用户历史评分的物品加入到了LFM模型里。也就是说, 上面的那些矩阵分解, 是只分解的当前的共现矩阵, 比如某个用户u uu对于某个物品i ii的评分, 就单纯的分解成用户u uu的隐向量与物品i ii的隐向量乘积再加上偏置项。 这时候注意并没有考虑该用户评分的历史物品, 所以这时候SVD++把这个考虑了进去。参考:隐语义模型(LFM)和矩阵分解(MF)

四. 代码实践

我们这里用代码实现一下上面的算法来预测上一篇文章里面的那个预测Alice对物品5的评分, 看看矩阵分解到底是怎么进行预测或者是推荐的。 我把之前的例子拿过来:
在这里插入图片描述
我们要做的事情就是根据这个评分矩阵, 猜测Alice对物品5的打分。
在实现SVD之前, 先来回忆一下ItemCF和UserCF对于这个问题的做法, 首先ItemCF的做法, 根据已有的用户打分计算物品之间的相似度, 得到物品的相似度矩阵, 根据这个相似度矩阵, 选择出前K个与物品5最相似的物品, 然后基于Alice对这K个物品的得分, 猜测Alice对物品5的得分, 有一个加权的计算公式。 UserCF的做法是根据用户对其他物品的打分, 计算用户之间的相似度, 选择出与Alice最相近的K个用户, 然后基于那K个用户对物品5的打分计算出Alice对物品5的打分。 But, 这两种方式有个问题, 就是如果矩阵非常稀疏的话, 当然这个例子是个特例, 一般矩阵都是非常稀疏的, 那么预测效果就不好, 因为两个相似用户对同一物品打分的概率以及Alice同时对两个相似物品打分的概率可能都比较小。 另外, 这两种方法显然没有考虑到全局的物品或者用户, 只是基于了最相似的例子, 很可能有偏。

那么SVD在解决这个问题上是这么做的:

  1. 首先, 它会先初始化用户矩阵P和物品矩阵Q, P的维度是[users_num, F], Q的维度是[item_nums, F], 这个F是隐向量的维度。 也就是把通过隐向量的方式把用户的兴趣和F的特点关联了起来。 初始化这两个矩阵的方式很多, 但根据经验, 随机数需要和1/sqrt(F)成正比。 下面代码中会发现。
  2. 有了两个矩阵之后, 我就可以根据用户已经打分的数据去更新参数, 这就是训练模型的过程, 方法很简单, 就是遍历用户, 对于每个用户, 遍历它打分的物品, 这样就拿到了该用户和物品的隐向量, 然后两者相乘加上偏置就是预测的评分, 这时候与真实评分有个差距, 根据上面的梯度下降就可以进行参数的更新

这样训练完之后, 我们就可以得到用户Alice和物品5的隐向量, 根据这个就可以预测Alice对物品5的打分。 下面的代码的逻辑就是上面这两步, 这里使用带有偏置项和正则项的那个SVD算法:

class BasicSVD():
    def __init__(self, rating_data, F=5, alpha=0.1, lmbda=0.1, max_iter=100):
        self.F = F           # 这个表示隐向量的维度
        self.P = dict()          #  用户矩阵P  大小是[users_num, F]
        self.Q = dict()     # 物品矩阵Q  大小是[item_nums, F]
        self.bu = dict()   # 用户偏差系数
        self.bi = dict()    # 物品偏差系数
        self.mu = 1.0        # 全局偏差系数
        self.alpha = alpha   # 学习率
        self.lmbda = lmbda    # 正则项系数
        self.max_iter = max_iter    # 最大迭代次数
        self.rating_data = rating_data # 评分矩阵
        
        # 初始化矩阵P和Q, 方法很多, 一般用随机数填充, 但随机数大小有讲究, 根据经验, 随机数需要和1/sqrt(F)成正比
        cnt = 0    # 统计总的打分数, 初始化mu用
        for user, items in self.rating_data.items():
            self.P[user] = [random.random() / math.sqrt(self.F)  for x in range(0, F)]
            self.bu[user] = 0
            cnt += len(items) 
            for item, rating in items.items():
                if item not in self.Q:
                    self.Q[item] = [random.random() / math.sqrt(self.F) for x in range(0, F)]
                    self.bi[item] = 0
        self.mu /= cnt
        
    # 有了矩阵之后, 就可以进行训练, 这里使用随机梯度下降的方式训练参数P和Q
    def train(self):
        for step in range(self.max_iter):
            for user, items in self.rating_data.items():
                for item, rui in items.items():
                    rhat_ui = self.predict(user, item)   # 得到预测评分
                    # 计算误差
                    e_ui = rui - rhat_ui
                    
                    self.bu[user] += self.alpha * (e_ui - self.lmbda * self.bu[user])
                    self.bi[item] += self.alpha * (e_ui - self.lmbda * self.bi[item])
                    # 随机梯度下降更新梯度
                    for k in range(0, self.F):
                        self.P[user][k] += self.alpha * (e_ui*self.Q[item][k] - self.lmbda * self.P[user][k])
                        self.Q[item][k] += self.alpha * (e_ui*self.P[user][k] - self.lmbda * self.Q[item][k])
                    
            self.alpha *= 0.1    # 每次迭代步长要逐步缩小
    
    # 预测user对item的评分, 这里没有使用向量的形式
    def predict(self, user, item):
        return sum(self.P[user][f] * self.Q[item][f] for f in range(0, self.F)) + self.bu[user] + self.bi[item] + self.mu   

RSVD的代码如下:

class RSVD():
    def __init__(self,rating_data,F=5,alpha=0.1,lmbda=0.1,max_iter=100):
        self.F = F #隐向量的纬度 k的大小 也就是特征的个数
        self.P = dict() # 用户矩阵P 大小是 user_n * F
        self.Q = dict() # 物品矩阵 大小是 item_n * F
        self.alpha = alpha  #学习率
        self.lmbda = lmbda  #正则系数项
        self.max_iter = max_iter #最大迭代次数
        self.rating_data = rating_data #评分矩阵

        # 初始化矩阵P和Q,方法很多,一般用随机数填充,但是随机数的大小也有讲究,根据经验,随机数需要和1/sqrt(F) 成正比
        for user,items in self.rating_data.items():
            #对每一个user初始化k个特征
            self.P[user] = [random.random()/math.sqrt(self.F) for x in range(0,F)]
            for item,rating in items.items():
                if item not in self.Q:
                    #对每个item初始化k个特征
                    self.Q[item] = [random.random()/math.sqrt(self.F) for x in range(0,F)]
        print(self.P)
        print(self.Q)
    # 梯度递减简化成了 使用误差来代替梯度
    def train(self):
        for step in range(self.max_iter):
            for user,items in self.rating_data.items():
                for item,rui in items.items():
                    #计算预测值
                    rhat_ui = self.predict(user,item)
                    e_ui = rui-rhat_ui
                    for k in range(0,self.F):
                        self.P[user][k] += self.alpha *(e_ui*self.Q[item][k]-self.lmbda*self.P[user][k])
                        self.Q[item][k] += self.alpha *(e_ui*self.P[user][k]-self.lmbda*self.Q[item][k])

            self.alpha *= 0.1
    def predict(self,user,item):
        return sum(self.P[user][f]*self.Q[item][f] for f in range(0,self.F))
class RSVDPlus():
    def __init__(self,rating_data,F=5,alpha=0.1,lmbda=0.1,max_iter=100):
        self.F = F #隐向量的纬度 k的大小 也就是特征的个数
        self.P = dict() # 用户矩阵P 大小是 user_n * F
        self.Q = dict() # 物品矩阵 大小是 item_n * F
        self.bu = dict()
        self.bi = dict()
        self.miu = 0
        self.alpha = alpha  #学习率
        self.lmbda = lmbda  #正则系数项
        self.max_iter = max_iter #最大迭代次数
        self.rating_data = rating_data #评分矩阵

        # 初始化矩阵P和Q,方法很多,一般用随机数填充,但是随机数的大小也有讲究,根据经验,随机数需要和1/sqrt(F) 成正比
        cnt = 0
        for user,items in self.rating_data.items():
            #对每一个user初始化k个特征
            self.P[user] = [random.random()/math.sqrt(self.F) for x in range(0,F)]
            self.bu[user] = 0
            cnt += len(items)
            for item,rating in items.items():
                self.miu += rating
                if item not in self.Q:
                    #对每个item初始化k个特征
                    self.bi[item] = 0
                    self.Q[item] = [random.random()/math.sqrt(self.F) for x in range(0,F)]
        self.miu /= cnt
        print(self.miu)

SVD++:

class SVDPP():
    def __init__(self,rating_data,F=5,alpha=0.1,lmbda=0.1,max_iter=100):
        self.F = F #隐向量的纬度 k的大小 也就是特征的个数
        self.P = dict() # 用户矩阵P 大小是 user_n * F
        self.Q = dict() # 物品矩阵 大小是 item_n * F
        self.bu = dict() #用户偏差系数
        self.bi = dict() #物品偏差系数
        self.Y = dict() #用户购买的历史记录
        self.miu = 0 #全局偏差系数
        self.alpha = alpha  #学习率
        self.lmbda = lmbda  #正则系数项
        self.max_iter = max_iter #最大迭代次数
        self.rating_data = rating_data #评分矩阵

        # 初始化矩阵P和Q,方法很多,一般用随机数填充,但是随机数的大小也有讲究,根据经验,随机数需要和1/sqrt(F) 成正比
        cnt = 0
        for user,items in self.rating_data.items():
            #对每一个user初始化k个特征
            self.P[user] = [random.random()/math.sqrt(self.F) for x in range(0,F)]
            self.bu[user] = 0
            cnt += len(items)
            for item,rating in items.items():
                self.miu += rating
                if item not in self.Q:
                    #对每个item初始化k个特征
                    self.bi[item] = 0
                    self.Q[item] = [random.random()/math.sqrt(self.F) for x in range(0,F)]
                    self.Y[item] = [random.random()/math.sqrt(self.F) for x in range(0,F)]
        self.miu /= cnt
        print(self.miu)

总结

由于协同过滤存在计算量过大,不擅长处理稀疏矩阵,因此,在协同过滤的基础上衍生出了矩阵分解算法,旨在使用更稠密的隐向量表示用户和物品, 挖掘用户和物品的隐含兴趣和隐含特征, 在一定程度上弥补协同过滤模型的不足之处。
之后,文章介绍了什么是隐向量,通过例子来帮助理解,用户矩阵和物品矩阵的隐含特征是怎么一回事。接下来介绍了隐语义模型工作的原理,就是基于现有的评分矩阵,将其分解成用户矩阵和物品矩阵相乘的方式。求解特征矩阵的常用方法就是SVD算法,但是SVD的计算量太大,不适合应用于实际问题当中,因此,把隐向量的求解问题转化为了和神经网络参数求解类似的最优化问题。之后又介绍了相关算法的改进,比如RSVD和SVD++。
深入了解了矩阵分解算法的原理之后,我们就可以理解为什么矩阵分解算法比协同过滤有更好的泛化能力,矩阵分解算法中,我们可以利用隐向量来预测任意用户和对物品的评分,求解隐向量的过程相当于是对评分矩阵进行全局拟合,在这个过程中,涉及到所有用户对物品的评分,因此隐向量是用全局信息生成的,具有更好的泛化能力。而协同过滤算法,只是基于相似的局部个体,而且,不论是相似用户对同一物品打分的概率,还是同一用户对相似物品打分的概率都比较小,这样就无法得到全局信息。
那么矩阵分解算法有什么优缺点呢?
优点:

  1. 泛化能力强: 一定程度上解决了稀疏问题
  2. 空间复杂度低: 由于用户和物品都用隐向量的形式存放, 少了用户和物品相似度矩阵, 空间复杂度由n 2 n^2n2 降到了( n + m ) ∗ f (n+m)*f(n+m)∗f
  3. 更好的扩展性和灵活性:矩阵分解的最终产物是用户和物品隐向量, 这个深度学习的embedding思想不谋而合, 因此矩阵分解的结果非常便于与其他特征进行组合和拼接, 并可以与深度学习无缝结合。

缺点:
矩阵分解算法依然是只用到了评分矩阵, 没有考虑到用户特征, 物品特征和上下文特征, 这使得矩阵分解丧失了利用很多有效信息的机会, 同时在缺乏用户历史行为的时候, 无法进行有效的推荐。 所以为了解决这个问题, 逻辑回归模型及后续的因子分解机模型, 凭借其天然的融合不同特征的能力, 逐渐在推荐系统领域得到了更广泛的应用。
参考:
王喆 - 《深度学习推荐系统》
项亮 - 《推荐系统实践》
翻滚的小@强 AI上推荐 之 隐语义模型(LFM)和矩阵分解(MF)

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值