我们把用户的评分行为表示成一个评分矩阵 R R ,其中 R[u][i] R [ u ] [ i ] 就是用户 u u 对物品 i i 的评分。但用户不会对所有的物品评分,所以这个矩阵里有很多元素都是空的
因此,评分预测从某种意义上说就是对这些元素填空,如果一个用户对一个物品没有评过分,那么推荐系统就要预测这个用户是否是否会对这个物品评分以及会评几分。
所以我们需要寻找一种对矩阵扰动小的补全矩阵的方法。一般认为,如果 补全后矩阵的特征值和补全之前矩阵的特征值相差不大,就算是扰动比较小。所以,早期的矩阵分解模型就是从数学上的SVD(奇异值分解)开始的。但奇异值分解由于计算复杂度很高,所以很难在实际系统上有所应用。
在多次探索之后,我们选择了Simon Funk提出的的SVD分解法(该方法简称为LFM)。其原理简要如下:
从矩阵分解的角度说,如果我们将评分矩阵 R R 分解为两个低维矩阵相乘:
其中 P∈Rf∗m P ∈ R f ∗ m , Q∈Rf∗n Q ∈ R f ∗ n 是两个降维后的矩阵。则对用户 u u 对物品 i i 的评价预测值 R^(u,i)=r^ui R ^ ( u , i ) = r ^ u i ,可通过以下公式计算:
既然我用RMSE作为评测指标,那么如果能找到合适的 P P 、 Q Q 来小化训练集的预测误差,那么应该也能小化测试集的预测误差。因此,我们定义损失函数为:
直接优化上面的损失函数可能会导致学习的过拟合,因此还需要加入防止过拟合项 λ(||pu||2+||qi||2) λ ( | | p u | | 2 + | | q i | | 2 ) ,其中 λ λ 为正则化参数,从而得到:
要最小化上面的损失函数,我们可以利用随机梯度下降法,它首先通过求参数的偏导数找到速下降方向,然后通过迭代法不断地优化参数。
上面定义的损失函数里有两组参数( puf p u f 和 qif q i f ),最速下降法需要首先对它们分别求偏导数, 可以得到:
然后,根据随机梯度下降法,需要将参数沿着速下降方向向前推进,因此可以得到如下递推公式:
加入偏置项后的LFM
上面的LFM预测公式通过隐类将用户和物品联系在了一起。但是,实际情况下,一个评分系统有些 固有属性和用户物品无关,而用户也有些属性和物品无关,物品也有些属性和用户无关。因此, Netflix Prize中提出了另一种LFM,其预测公式如下:
考虑邻域影响的LFM
前面的LFM模型中并没有显式地考虑用户的历史行为对用户评分预测的影响。为此,Koren 在Netflix Prize比赛中提出了一个模型,将用户历史评分的物品加入到了LFM模型中,Koren将该模型称为SVD++。
我们将将ItemCF的预测算法改成如下方式:
这里, wij w i j 不再是根据ItemCF算法计算出的物品相似度矩阵,而是一个和 P P 、 Q Q 一样的参数,它可以通过优化如下的损失函数进行优化:
不过,这个模型有一个缺点,就是 w w 将是一个比较稠密的矩阵,存储它需要比较大的空间。此外,如果有 n n 个物品,那么该模型的参数个数就是 n2 n 2 个,这个参数个数比较大,容易造成结果 的过拟合。因此,Koren提出应该对 w w 矩阵也进行分解,将参数个数降低到 2∗n∗F 2 ∗ n ∗ F 个,模型如下:
这里 xi x i 、 yi y i 是两个 F F 维的向量。由此可见,该模型用 xTiyj x i T y j 代替了 wij w i j ,从而大大降低了参 数的数量和存储空间。
再进一步,我们可以将前面的LFM和上面的模型相加,从而得到如下模型:
在LearningLFM函数中,输入train是训练集中的用户评分记录,F是隐类的格式,n是迭代次数。
def LearningBiasLFM():
step=0
InitBiasLFM(S,F)
#15为迭代次数
for step in range(15):
for message,rui in S.items():
u=message[0]
i=message[1]
pui = Predict(u,i)
eui=rui-pui
bu[u]+=alpha*(eui-lambd*bu[u])
bi[i]+=alpha*(eui-lambd*bi[i])
pi=p[u]
qi=q[i]
p[u]=pi+alpha*(qi*eui-lambd*pi)
q[i]=qi+alpha*(pi*eui-lambd*qi)
print(step,getrmse())
如上面的代码所示,LearningBiasLFM主要包括两步。
首先,需要对P、Q矩阵进行初始化。
然后后需要通过随机梯度下降法的迭代得到终的P、Q矩阵。在迭代时,需要在每一步对学习参数 α α 进行衰减。
初始化P、Q矩阵的方法很多,一般都是将这两个矩阵用随机数填充,但随机数的大小还是有讲究的,根据经验,随机数需要和 1/sqrt(F) 1 / s q r t ( F ) 成正比。下面的代码实现了初始化功能:
def InitBiasLFM(S,F):
for message,rui in S.items():
u=message[0]
i=message[1]
bu[u]=0
bi[i]=0
if u not in p:
p[u] =np.array([0.1*random.random()/math.sqrt(F) for k in range(0,F)])
if i not in q:
q[i] =np.array([0.1*random.random()/math.sqrt(F) for k in range(0,F)])
def Predict(u,i):
if u not in bu and i not in bi:
return mu
if u not in bu:
return mu+bi[i]
if i not in bi:
return mu+bu[u]
return mu+bu[u]+bi[i]+np.dot(p[u],q[i])