隐语义模型与矩阵分解 协同过滤算法的特点: 协同过滤算法的特点就是完全没有利用到物品本身或者是用户自身的属性, 仅仅利用了用户与物品的交互信息就可以实现推荐,是一个可解释性很强, 非常直观的模型。 但是也存在一些问题,处理稀疏矩阵的能力比较弱。 为了使得协同过滤更好处理稀疏矩阵问题, 增强泛化能力。从协同过滤中衍生出矩阵分解模型(Matrix Factorization, MF)或者叫隐语义模型: 在协同过滤共现矩阵的基础上, 使用更稠密的隐向量表示用户和物品。 通过挖掘用户和物品的隐含兴趣和隐含特征, 在一定程度上弥补协同过滤模型处理稀疏矩阵能力不足的问题。 隐语义模型 隐语义模型最早在文本领域被提出,用于找到文本的隐含语义。
协同过滤算法的特点:
为了使得协同过滤更好处理稀疏矩阵问题, 增强泛化能力。从协同过滤中衍生出矩阵分解模型(Matrix Factorization, MF)或者叫隐语义模型:
隐语义模型最早在文本领域被提出,用于找到文本的隐含语义。在2006年, 被用于推荐中, 它的核心思想是通过隐含特征(latent factor)联系用户兴趣和物品(item), 基于用户的行为找出潜在的主题和分类, 然后对物品进行自动聚类,划分到不同类别/主题(用户的兴趣)。
以项亮老师《推荐系统实践》书中的内容为例:
如果我们知道了用户A和用户B两个用户在豆瓣的读书列表, 从他们的阅读列表可以看出,用户A的兴趣涉及侦探小说、科普图书以及一些计算机技术书, 而用户B的兴趣比较集中在数学和机器学习方面。 那么如何给A和B推荐图书呢? 先说说协同过滤算法, 这样好对比不同:
- 对于UserCF,首先需要找到和他们看了同样书的其他用户(兴趣相似的用户),然后给他们推荐那些用户喜欢的其他书。
- 对于ItemCF,需要给他们推荐和他们已经看的书相似的书,比如作者B看了很多关于数据挖掘的书,可以给他推荐机器学习或者模式识别方面的书。
而如果是隐语义模型的话, 它会先通过一些角度把用户兴趣和这些书归一下类, 当来了用户之后, 首先得到他的兴趣分类, 然后从这个分类中挑选他可能喜欢的书籍。
隐语义模型和协同过滤的不同主要体现在隐含特征上, 比如书籍的话它的内容, 作者, 年份, 主题等都可以算隐含特征。
以王喆老师《深度学习推荐系统》中的一个原理图为例,看看是如何通过隐含特征来划分开用户兴趣和物品的。
假设每个用户都有自己的听歌偏好, 比如用户 A 喜欢带有小清新的, 吉他伴奏的, 王菲的歌曲,如果一首歌正好是王菲唱的, 并且是吉他伴奏的小清新, 那么就可以将这首歌推荐给这个用户。 也就是说是小清新, 吉他伴奏, 王菲这些元素连接起了用户和歌曲。
当然每个用户对不同的元素偏好不同, 每首歌包含的元素也不一样, 所以我们就希望找到下面的两个矩阵:
利用上面的这两个矩阵,将对应向量进行内积计算,我们就能得出张三对音乐A的喜欢程度:
张三对小清新的偏好 * 音乐A含有小清新的成分 + 张三对重口味的偏好 * 音乐A含有重口味的成分 + 张三对优雅的偏好 * 音乐A含有优雅的成分...
根据隐向量其实就可以得到张三对音乐A的打分,即:
计算所有用户对不同音乐的喜爱程度
按照这个计算方式, 每个用户对每首歌其实都可以得到这样的分数, 最后就得到了我们的评分矩阵:
小结
上面例子中的小清晰, 重口味, 优雅这些就可以看做是隐含特征, 而通过这个隐含特征就可以把用户的兴趣和音乐的进行一个分类, 其实就是找到了每个用户每个音乐的一个隐向量表达形式(与深度学习中的embedding等价)
这个隐向量就可以反映出用户的兴趣和物品的风格,并能将相似的物品推荐给相似的用户等。 有没有感觉到是把协同过滤算法进行了一种延伸, 把用户的相似性和物品的相似性通过了一个叫做隐向量的方式进行表达
现实中,类似于上述的矩阵 P,Q 一般很难获得。有的只是用户的评分矩阵,如下:
矩阵分解模型:
在矩阵分解的算法框架下, 可以通过分解协同过滤的共现矩阵(评分矩阵)来得到用户和物品的隐向量,原理如下:。
在分解得到用户矩阵和物品矩阵后,若要计算用户 u 对物品 i 的评分,公式如下:
常用的矩阵分解方法有特征值分解(EVD)或者奇异值分解(SVD), 具体原理可参考:
2006年的Netflix Prize之后, Simon Funk公布了一个矩阵分解算法叫做Funk-SVD, 后来被 Netflix Prize 的冠军Koren称为Latent Factor Model(LFM)。
Funk-SVD的思想很简单: 把求解上面两个矩阵的参数问题转换成一个最优化问题, 可以通过训练集里面的观察值利用最小化来学习用户矩阵和物品矩阵。
算法过程
根据前面提到的,在有用户矩阵和物品矩阵的前提下,若要计算用户 u 对物品 i 的评分, 可以根据公式:
随机初始化一个用户矩阵 U 和一个物品矩阵 V,获取每个用户和物品的初始隐语义向量。
将用户和物品的向量内积 p_{u}^{T} q_{i}, 作为用户对物品的预测评分 \hat{r}_{u i}。
对于评分矩阵中的每个元素,计算预测误差 e_{u i}=r_{u i}-\hat{r}_{u i},对所有训练样本的平方误差进行累加:
从上述公式可以看出,SSE 建立起了训练数据和预测模型之间的关系。
如果我们希望模型预测的越准确,那么在训练集(已有的评分矩阵)上的预测误差应该仅可能小。
为方便后续求解,给 SSE 增加系数 1/2 :
前面提到,模型预测越准确等价于预测误差越小,那么优化的目标函数变为:
对于给定的目标函数,可以通过梯度下降法对参数进行优化。
求解目标函数 SSE 关于用户矩阵中参数 p_{u,k} 的梯度:
求解目标函数 SSE 关于 q_{i,k} 的梯度:
参数梯度更新
加入正则项
为了控制模型的复杂度。在原有模型的基础上,加入 l2 正则项,来防止过拟合。
当模型参数过大,而输入数据发生变化时,可能会造成输出的不稳定。
l2 正则项等价于假设模型参数符合0均值的正态分布,从而使得模型的输出更加稳定。
在推荐系统中,评分预测除了与用户的兴趣偏好、物品的特征属性相关外,与其他的因素也相关。例如:
因此, Netfix Prize中提出了另一种LFM, 在原来的基础上加了偏置项, 来消除用户和物品打分的偏差, 即预测公式如下:
这个预测公式加入了3项偏置参数 \mu,b_u,b_i, 作用如下:
加了用户和物品的打分偏差之后, 矩阵分解得到的隐向量更能反映不同用户对不同物品的“真实”态度差异, 也就更容易捕捉评价数据中有价值的信息, 从而避免推荐结果有偏。
优化函数
在加入正则项的FunkSVD的基础上,BiasSVD 的目标函数如下:
可得偏置项的梯度更新公式如下:
本小节,使用如下图表来预测Alice对物品5的评分:
[users_num, K],Q 的维度是[items_num, K],其中,F表示隐向量的维度。 也就是把通过隐向量的方式把用户的兴趣和F的特点关联了起来。
初始化这两个矩阵的方式很多, 但根据经验, 随机数需要和1/sqrt(F)成正比。
完整代码如下:
import random import math class BiasSVD(): 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 = 0 # 全局偏置系数 self.alpha = alpha # 学习率 self.lmbda = lmbda # 正则项系数 self.max_iter = max_iter # 最大迭代次数 self.rating_data = rating_data # 评分矩阵 for user, items in self.rating_data.items(): # 初始化矩阵P和Q, 随机数需要和1/sqrt(F)成正比 self.P[user] = [random.random() / math.sqrt(self.F) for x in range(0, F)] self.bu[user] = 0 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 # 采用随机梯度下降的方式训练模型参数 def train(self): cnt, mu_sum = 0, 0 for user, items in self.rating_data.items(): for item, rui in items.items(): mu_sum, cnt = mu_sum + rui, cnt + 1 self.mu = mu_sum / cnt 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 # 评分预测 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 # 通过字典初始化训练样本,分别表示不同用户(1-5)对不同物品(A-E)的真实评分 def loadData(): rating_data={1: {'A': 5, 'B': 3, 'C': 4, 'D': 4}, 2: {'A': 3, 'B': 1, 'C': 2, 'D': 3, 'E': 3}, 3: {'A': 4, 'B': 3, 'C': 4, 'D': 3, 'E': 5}, 4: {'A': 3, 'B': 3, 'C': 1, 'D': 5, 'E': 4}, 5: {'A': 1, 'B': 5, 'C': 5, 'D': 2, 'E': 1} } return rating_data # 加载数据 rating_data = loadData() # 建立模型 basicsvd = BiasSVD(rating_data, F=10) # 参数训练 basicsvd.train() # 预测用户1对物品E的评分 for item in ['E']: print(item, basicsvd.predict(1, item)) # 预测结果:E 3.685084274454321
矩阵分解算法后续有哪些改进呢?针对这些改进,是为了解决什么的问题呢?请大家自行探索RSVD,消除用户和物品打分偏差等。
矩阵分解的优缺点分析