协同过滤可以解决我们关注的很多问题,但是仍然有一些问题存在,比如:
上述两个问题,在矩阵分解中可以得到解决。原始的矩阵分解只适用于评分预测问题,这里所讨论的也只是针对于评分预测问题。常用的分解算法有SVD和SVD++。
目前推荐系统中用的最多的就是矩阵分解方法,在Netflix Prize推荐系统大赛中取得突出效果。以用户-项目评分矩阵为例,矩阵分解就是预测出评分矩阵中的缺失值,然后根据预测值以某种方式向用户推荐。常见的矩阵分解方法有
用公式来描述就是:
其中 R 表示真实的用户评分矩阵,一般有很多缺失值(缺失值表示用户没有对该物品评分),带尖帽的 R 表示使用分解矩阵预测的用户评分矩阵,它补全了所有的缺失值。
矩阵分解就是把用户和物品都映射到一个 k 维空间中(这里映射后的结果用户用矩阵P表示,物品用矩阵Q表示),这个 k 维空间不是我们直接看得到的,也不一定具有非常好的可解释性,每一个维度也没有名字,所以常常叫做隐因子。用户向量代表了用户的兴趣,物品向量代表了物品的特点,且每一个维度相互对应,两个向量的内积表示用户对该物品的喜好程度。
请查看我的另一篇博客
请查看我的另一篇博客
对于如何补全一个矩阵,历史上有过很多的研究。一个空的矩阵有很多种补全方法,而我们要找的是一种对矩阵扰动最小的补全方法。那么什么才算是对矩阵扰动最小呢?一般认为,如果补全后矩阵的特征值和补全之前矩阵的特征值相差不大,就算是扰动比较小。所以,最早的矩阵分解模型就是从数学上的SVD(奇异值分解)开始的。给定m个用户和n个物品,和用户对物品
奇异值分解将矩阵分解为奇异值和奇异向量
R是一个mn的矩阵,U是一个mm的矩阵,S是一个mn的矩阵,V是一个nn的矩阵
矩阵S对角元素上的值称为矩阵A的奇异值,U的列向量称为左奇异向量,V的列向量称为右奇异向量
左奇异向量是RRT的特征向量,右奇异向量是RTR的特征向量,A的非零奇异值是和特征值的平方根
SVD分解是早期推荐系统研究常用的矩阵分解方法,不过该方法具有以下缺点,因此很难在实际系统中应用。
想具体了解推荐系统中常用的LFM算法,可以参考我的另一篇博客
LFM提出之后获得了很大的成功,后来很多著名的模型都是通过对LFM修修补补获得的,下面的各节将分别介绍一下改进LFM的各种方法。这些改进有些是对模型的改进,有些是将新的数据引入到模型当中。
前面的LFM模型中并没有显式地考虑用户的历史行为对用户评分预测的影响。为此,Koren在Netflix Prize比赛中提出了一个模型,将用户历史评分的物品加入到了LFM模型中,Koren将该模型称为SVD++。
在介绍SVD++之前,我们首先讨论一下如何将基于邻域的方法也像LFM那样设计成一个可以学习的模型。其实很简单,我们可以将ItemCF的预测算法改成如下方式:
可以看到评分函数加了用户对有过评分商品的行为隐式yj反馈之后,式子变得复杂了一些,但是改变的也不是太多,因此 SVD++ 的代码就是从SVD的代码修改而来。主要改了预测函数p(Ui,Mj),增加了用户对商品的隐式反馈向量的计算,训练,更新;以及用户对商品的行为集合构建。
无论是MovieLens数据集还是Netflix Prize数据集都包含时间信息,对于用户每次的评分行为,都给出了行为发生的时间。因此,在Netflix Prize比赛期间,很多研究人员提出了利用时间信息降低预测误差的方法。
利用时间信息的方法也主要分成两种,一种是将时间信息应用到基于邻域的模型中,另一种是将时间信息应用到矩阵分解模型中。下面将分别介绍这两种算法。
可以看到,改进后的TimesSVD++ 均方根误差最低为0.8824,性能最优!
class SVDPP(object):
"""
implementation of SVD++ for CF
"""
def __init__(self, epoch, eta, userNums, itemNums, ku=0.001, km=0.001, \
f=30,save_model=False):
super(SVDPP, self).__init__()
self.epoch = epoch
self.userNums = userNums
self.itemNums = itemNums
self.eta = eta
self.ku = ku
self.km = km
self.f = f
self.save_model = save_model
self.U = None
self.M = None
def fit(self, train, val=None):
# 构造每个用户有过评分的行为字典
self.Udict = {}
for i in range(train.shape[0]):
uid = train[i,0]
iid = train[i,1]
self.Udict.setdefault(uid,[])
self.Udict[uid].append(iid)
rateNums = train.shape[0]
self.meanV = np.sum(train[:,2]) / rateNums
initv = np.sqrt((self.meanV - 1) / self.f)
self.U = initv + np.random.uniform(-0.01,0.01,(self.userNums+1,self.f))
self.M = initv + np.random.uniform(-0.01,0.01,(self.itemNums+1,self.f))
self.bu = np.zeros(self.userNums + 1)
self.bi = np.zeros(self.itemNums + 1)
self.y = np.zeros((self.itemNums+1, self.f)) + 0.1
start = time.time()
for i in range(self.epoch):
sumRmse = 0.0
for sample in train:
uid = sample[0]
iid = sample[1]
vij = float(sample[2])
sumYj, sqrt_Ni = self.get_Yi(uid)
# p(U_i,M_j) = \mu + b_i + b_u + U_i^T (M_j + \frac{1}{\sqrt{|N_i|^2}}*\sum_{j\in N_i}y_j)
p = self.meanV + self.bu[uid] + self.bi[iid] + \
np.sum(self.M[iid] * (self.U[uid] + sumYj))
error = vij - p
sumRmse += error**2
# 计算Ui,Mj的梯度
deltaU = error * self.M[iid] - self.ku * self.U[uid]
deltaM = error * (self.U[uid] + sumYj) - self.km * self.M[iid]
# 更新参数
self.U[uid] += self.eta * deltaU
self.M[iid] += self.eta * deltaM
self.bu[uid] += self.eta * (error - self.ku * self.bu[uid])
self.bi[iid] += self.eta * (error - self.km * self.bi[iid])
# for j in self.Udict[uid]:
# self.y[j] += self.eta * (error * self.M[j]/sqrt_Ni - self.ku * self.y[j])
rating_list = self.Udict[uid]
self.y[rating_list] += self.eta * (error * self.M[rating_list]/sqrt_Ni - \
self.ku * self.y[rating_list])
trainRmse = np.sqrt(sumRmse/rateNums)
if val.any():
_ , valRmse = self.evaluate(val)
print("Epoch %d cost time %.4f, train RMSE: %.4f, validation RMSE: %.4f" % \
(i, time.time()-start, trainRmse, valRmse))
else:
print("Epoch %d cost time %.4f, train RMSE: %.4f" % \
(i, time.time()-start, trainRmse))
if self.save_model:
model = (self.meanV, self.bu, self.bi, self.U, self.M)
pickle.dump(model, open(save_model + '/svcRecModel.pkl', 'wb'))
def evaluate(self, val):
loss = 0
pred = []
for sample in val:
uid = sample[0]
iid = sample[1]
if uid > self.userNums or iid > self.itemNums:
continue
sumYj, _ = self.get_Yi(uid)
predi = self.meanV + self.bu[uid] + self.bi[iid] \
+ np.sum(self.M[iid] * (self.U[uid] + sumYj))
if predi < 1:
predi = 1
elif predi > 5:
predi = 5
pred.append(predi)
if val.shape[1] == 3:
vij = sample[2]
loss += (predi - vij)**2
if val.shape[1] == 3:
rmse = np.sqrt(loss/val.shape[0])
return pred, rmse
return pred
def predict(self,test):
return self.evaluate(test)
# 计算 sqrt_Ni 和 ∑yj
def get_Yi(self,uid):
Ni = self.Udict[uid]
numNi = len(Ni)
sqrt_Ni = np.sqrt(numNi)
yj = np.zeros((1, self.f))
if numNi == 0:
sumYj = yj + 0.1
else:
yj = np.mean(self.y[Ni],axis=0)
sumYj = yj / sqrt_Ni
return sumYj, sqrt_Ni
[1]:项亮《推荐系统实战》