如果需要完整代码可以关注下方公众号,后台回复“代码”即可获取,阿光期待着您的光临~
文章目录
- 1.矩阵分解算法的求解
- 2.优化函数的修改
- 3.BASIC-SVD编程实现
2021人工智能领域新星创作者,带你从入门到精通,该博客每天更新,逐渐完善推荐系统各个知识体系的文章,帮助大家更高效学习。
1.矩阵分解算法的求解
那么到底如何才能将评分矩阵进行分解呢?
主成分分析利用到了奇异值分解(SVD)进行分解特征矩阵,那么这里我们可不可以使用SVD进行分解呢?
答案是不行的,因为对于我们的评分矩阵是稀疏的,存在大量用户和物品无交互的情况,所以我们在使用SVD之前需要进行填充,其次是SVD的计算复杂度非常高,需要计算矩阵的特征值,而且当维度较高时,计算会非常复杂。
那么对于矩阵分解还有一种方式就是特征值分解,在这里也是显然不行的,因为使用特征值分解的前提要求是矩阵必须是方阵,这个要求对于我们的评分矩阵显然一般情况是不能够满足的。
那么应如何分解呢?
其实这个和神经网络的反向传播很像,我们定义一个优化函数,然后进行优化,那么我们就想办法将我们矩阵分解转化成一个优化问题,然后使用随机梯度下降进行求解。
2006年的Netflix Prize之后, Simon Funk公布了一个矩阵分解算法叫做Funk-SVD, 后来被Netflix Prize的冠军Koren称为Latent Factor Model(LFM)。 Funk-SVD的思想很简单:把求解上面两个矩阵的参数问题转换成一个最优化问题, 可以通过训练集里面的观察值利用最小化来学习用户矩阵和物品矩阵。
我们之前说到可以用用户矩阵与物品矩阵的乘积作为最终评分,那么现在是没有这个矩阵,那么我们刚开始可以随机初始化一个,里面随机赋些值,然后真实值与初始化矩阵得到的评分会有误差,因为我们这两个矩阵是随机初始化的最终计算结果是肯定不准的,那么就会产生一个误差:
那么此时就可以利用所有用户对所有物品的误差平方和作为优化函数:
现在有了可以评估精度的函数了,此时我们就可以利用最优化的算法进行求解其中的参数使其最小化:
对于这种优化函数,我们一般采用的是随机梯度下降(stochastic gradient descent),所以此时我们需要知道我们需要优化的参数的导数:
所以我们最终得到的参数更新公式为:
然后就可以基于梯度下降求其最优解,就可以获得分解后的矩阵了,因为我们求解的参数分别是两个矩阵的列向量。
这里没有考虑正则化,如果我们的数据过少或者说要防止过拟合,我们还需要对参数进行惩罚使用正则化。
2.优化函数的修改
上面我们已经获得了优化方程以及梯度更新公式,然后可以使用梯度下降法进行求解,但是此时会存在问题,结果可能有所偏差。
原因是有一些因素还没有考虑到,我们只是考虑用户矩阵和物品矩阵,这两个矩阵分别代表用户、物品与那些隐特征之间的联系,没有考虑到一些个人或者物品自身的一些影响。
比如说现在长津湖电影是非常火的,那么它的评分一定是很高的,但是它不一定都与用户有关系,一定程度上有它自身的因素影响,比如说里面的演员很出名或者上映时间等,这些与用户是没有关系的。
再比如一个用户是比较批判的,不管他观看什么类型的电影最终给出的评分都是比较低的,这个与电影就没什么关系了,单纯是个人因素影响。
所以针对这些问题,只考虑两个矩阵显然是不科学的,还需要在原来基础上加入一些偏置项来消除用户和物品打分的偏差,即:
这个预测公式加入了3项偏置, 作用如下:
- : 训练集中所有记录的评分的全局平均数。 在不同网站中, 因为网站定位和销售物品不同, 网站的整体评分分布也会显示差异。 比如有的网站中用户就喜欢打高分, 有的网站中用户就喜欢打低分。 而全局平均数可以表示网站本身对用户评分的影响。
- : 用户偏差系数, 可以使用用户给出的所有评分的均值, 也可以当做训练参数。 这一项表示了用户的评分习惯中和物品没有关系的那种因素。 比如有些用户比较苛刻, 对什么东西要求很高, 那么他评分就会偏低, 而有些用户比较宽容, 对什么东西都觉得不错, 那么评分就偏高
- : 物品偏差系数, 可以使用物品收到的所有评分的均值, 也可以当做训练参数。 这一项表示了物品接受的评分中和用户没有关系的因素。 比如有些物品本身质量就很高, 因此获得的评分相对比较高, 有的物品本身质量很差, 因此获得的评分相对较低。
加入偏置项后的优化函数为:
对于新加入的偏置项的梯度为:
3.BASIC-SVD编程实现
"""
* Created with PyCharm
* 作者: Laura
* 日期: 2021/11/5
* 时间: 11:10
* 描述: BASIC-SVD
"""
import numpy as np
import pandas as pd
users = {
"Tom":{"物品1":5,"物品2":3,"物品3":4,"物品4":4},
"用户1":{"物品1":3,"物品2":1,"物品3":2,"物品4":3,"物品5":3},
"用户2":{"物品1":4,"物品2":3,"物品3":4,"物品4":3,"物品5":5},
"用户3":{"物品1":3,"物品2":3,"物品3":1,"物品4":5,"物品5":4},
"用户4":{"物品1":1,"物品2":5,"物品3":5,"物品4":2,"物品5":1},
}
user_df = pd.DataFrame(users).T
user_df = user_df.fillna(0)
class SVD():
def __init__(self, rating_data, k = 5, alpha = 0.1, lmbda = 0.1, max_iter = 2000):
self.rating_data = rating_data
self.mu = 1.0
self.bu = None # (user,1)
self.bi = None # (item,1)
self.P = None # (user,k)
self.Q =None # (item,k)
self.alpha = alpha
self.lmbda = lmbda
self.k = k
self.max_iter = max_iter
def init_params(self):
self.mu = self.rating_data.mean().mean()
self.bu = np.zeros((self.rating_data.shape[0], 1))
self.bi = np.zeros((self.rating_data.shape[1], 1))
self.P = np.random.rand(self.rating_data.shape[0], self.k) / np.sqrt(self.k)
self.Q = np.random.rand(self.rating_data.shape[1], self.k) / np.sqrt(self.k)
def train(self):
self.init_params()
for step in range(self.max_iter):
r_hat = self.compute()
error = self.rating_data.values - r_hat
self.P = self.P + self.alpha * error @ self.Q
self.Q = self.Q + self.alpha * error.T @ self.P
self.bu = self.bu + self.alpha * (error.mean(axis=1).reshape(-1,1) - self.lmbda * self.bu)
self.bi = self.bi + self.alpha * (error.mean(axis=1).reshape(-1,1) - self.lmbda * self.bi)
self.alpha *= 0.1
def compute(self):
return self.mu + self.bu + self.bi + self.P @ self.Q.T
def predict(self,user,item):
return self.mu + self.bu[user][0] + self.bi[item][0] + np.dot(self.P[user], self.Q[item])
model = SVD(user_df, k=5)
model.train()
model.predict(0,0)