推荐算法之--矩阵分解(Matrix Factorization)

推荐算法之–矩阵分解(Matrix Factorization)

在众多推荐算法或模型的发展演化脉络中,基于矩阵分解的推荐算法,处在了一个关键的位置:

  • 向前承接了协同率波的主要思想,一定程度上提高了处理稀疏数据的能力和模型泛化能力,缓解了头部效应;
  • 向后可以作为Embedding思想的一种简单实现,可以很方便、灵活地扩展为更加复杂的深度学习模型。

因此,矩阵分解模型虽然简单,但也值得深入理解和思考。

1. 共现矩阵

推荐问题中,不同人对不同物品的倾向/喜欢程度,可表示为共现矩阵,即记录某个人和某个物品共同出现的 频次/频率/概率。

例如,商品推荐|新闻推荐|视频推荐 场景中,矩阵第u行第i列的元素,可以表示第u个人对第i个商品|新闻|视频 的 购买次数|点击次数|观看时长。

直观地,假设有5个用户,5个商品,根据是否存在购买行为(买过为1,否则0),可表示为共现矩阵 Y g t Y_{gt} Ygt

Y_gt = [1, 1, 1, 0, 0], # 用户1
       [1, 1, 1, 0, 0], # 用户2
       [1, 1, 1, 0, 0], # 用户3
       [0, 0, 0, 1, 1], # 用户4
       [0, 0, 0, 1, 1], # 用户5

其中每一行对应一个用户,每一列对应一个商品,即用户123买过商品123,用户45买过商品45。

上面的矩阵中,所有元素都是已知的,然而实际场景中,仅有少数元素是已知的,大部分位置是空缺和未知的,例如,几乎没有人买过某宝/某东商品列表中的所有商品。

因此,推荐算法的应用场景,则是对上述矩阵中存在的未知元素进行预测,例如电商场景中,预测某个用户对某个商品的购买倾向。

一个简单的例子,从上面的矩阵 Y g t Y_{gt} Ygt中“挖去”一些元素,得到如下矩阵 Y Y Y,其中问号“?”表示待估计值;

Y = [1, 1, 1, 0, ?], # 用户1
    [1, 1, 1, ?, 0], # 用户2
    [1, 1, ?, 0, 0], # 用户3
    [0, ?, 0, 1, 1], # 用户4
    [?, 0, 0, 1, 1], # 用户5

2. 矩阵分解(MF)

“物以类聚,人以群分”,购买过相同物品的人,往往有着相同的购买倾向或兴趣。

例如,根据有“空洞”的共现矩阵 Y Y Y,用户123都买过物品12,有着相同的购买倾向,同时用户12都买过物品3,于是可以推测用户3可能也会喜欢物品3.

人群的“兴趣模式”,通常少于人的个数或物品的个数,且随着人数和物品数量的增多,这种效果会越来越明显,即人群的“兴趣模式”是稀疏的;

从矩阵的角度,也不难看出,对于上面的例子,矩阵 Y g t Y_{gt} Ygt只有2,小于矩阵的行数/列数,即矩阵中存在着大量冗余信息。

从谱分析的角度,矩阵分解模型相当于一个低通滤波器:通过对共现矩阵进行低秩分解,滤掉了低能量的高频信息,保留了高能量的低频信息。

3. SVD实现矩阵分解(MF)

对于小型矩阵,通过SVD的方式可简单实现矩阵分解。然而,此类方式存在一些问题:

  1. 不适用于大矩阵,容易爆内存;
  2. 需要预先对缺失值进行填充;
  3. 缺失值填充后会和已知数据混淆,不能区分开。

一个简单的例子如下:

import numpy as np
# 参数
num_user = 5     # 用户个数
num_item = 5     # 商品个数
latent   = 2     # 隐向量维度(Embedding)
## Y_gt
Y_gt = np.array([
    [1, 1, 1, 0, 0],
    [1, 1, 1, 0, 0],
    [1, 1, 1, 0, 0],
    [0, 0, 0, 1, 1],
    [0, 0, 0, 1, 1],
], dtype=np.float)
# Y
Y = Y_gt.copy()
Y[0, 4] = Y[1, 3] = Y[2, 2] = Y[3, 1] = Y[4, 0] = None
# 填充为0.5
Y_padding = np.nan_to_num(Y, nan=0.5)
# SVD分解
U, S, Vh = np.linalg.svd(Y_padding, full_matrices=True)
# 把奇异值S平分到用户矩阵P和物品矩阵Q上
P = U[:, :latent] * np.sqrt(S[:latent]).reshape(1, -1)
Q = Vh[:latent, :]* np.sqrt(S[:latent]).reshape(-1, 1)
## 最终得到的 用户矩阵P、物品矩阵Q
print('P = \n', np.around(P,2))
print('Q = \n', np.around(Q,2))
# 对Y进行重建,实现缺失值估计
Y_re = P @ Q
# -------------------------------------输出结果
# P = 
#  [[-0.99  0.27]   # 用户/物品隐向量的 几何特征;
#  [-0.99  0.27]    # 用户123 & 物品123,隐向量基本同向,内积≈1;
#  [-0.78  0.4 ]    # 用户45  & 物品45 ,隐向量基本同向,内积≈1;
#  [-0.48 -0.88]    # 用户123 & 物品45 , 隐向量基本正交,内积≈0;
#  [-0.48 -0.88]]   # 用户45  & 物品123, 隐向量基本正交,内积≈0;
# Q = 
#  [[-0.99 -0.99 -0.78 -0.48 -0.48]
#  [ 0.27  0.27  0.4  -0.88 -0.88]]
# -------------------------------------

这里准备一个作图的函数,简化代码,方便展示结果,后面还会用到:

import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = 'SimHei'
def show_result(curve, curve_name, Y, Y_re, figname, save=False):
    fig, axs = plt.subplots(1, 3, figsize=(15, 5), num=figname)
    axs[0].plot(curve)              # 曲线
    axs[0].grid()
    axs[0].set_title(curve_name)
    axs[1].imshow(Y, cmap='gray')   # 待预测/填充数据
    axs[1].set_title('待填充数据')
    axs[2].imshow(Y_re, cmap='gray')# 填充/预测结果
    axs[2].set_title('预测填充')
    for u in range(num_user):
        for i in range(num_item):
            if np.isnan(Y[u,i]):
                axs[1].text(u-0.1, i+0.1, '?', fontsize=20, color='r')
                axs[2].text(u-0.25, i+0.1, '%1.2f'%Y_re[u,i], fontsize=12, color='r')
            else:
                axs[1].text(u-0.2, i+0.1, str(Y[u,i]), fontsize=12, color='g')
                axs[2].text(u-0.25, i+0.1, '%1.2f'%Y_re[u,i], fontsize=12, color='g')
    if save:
        fig.savefig('./image/%s.png'%(figname))

调用作图函数,展示结果:

show_result(curve=S, curve_name='奇异值', Y=Y, Y_re=Y_re, figname='SVD', save=True)

在这里插入图片描述

可以看到,SVD后,只有两个较大奇异值,因此信息大多保留在 左/右奇异矩阵的前 2列/行中。丢掉 左/右奇异矩阵的 后3列/行,实现了低通滤波和数据重建;

4. 梯度下降 实现 矩阵分解(MF)

因为SVD求解时存在的问题,实际场景中一般不用SVD,而是采用数值迭代和梯度下降的方式,实现矩阵分解。

同时,可以为用户、物品、和用户-物品整体 各添加偏置项(有点类似batch normalization的作用),让被分解对象尽量有0均值,降低模型复杂度。

4.1 前向推理 & 符号表示

对于共现矩阵 Y Y Y的第 u u u行第 i i i列,即第 u u u个用户对第 i i i个物品的 点击率/购买概率,将模型对它的估计值记作 y ^ u , i \hat y_{u,i} y^u,i

y ^ u , i = ∑ k = 0 K P u , k ⋅ Q k , i + p u + q i + c \hat y_{u,i} = \sum_{k=0}^K P_{u,k} \cdot Q_{k,i} + p_u + q_i + c y^u,i=k=0KPu,kQk,i+pu+qi+c

其中,待求解的参数集合 Θ = { P , Q , p , q , c } \Theta= \{ P, Q, p, q, c \} Θ={ P,Q,p,q,c}

P P P – 用户矩阵,共M行K列,M为用户个数。 P u , k P_{u,k} Pu,k表示其第 u u u行第 k k k列的元素;
Q Q Q – 物品矩阵,共K行N列,N为物品个数, Q k , i Q_{k,i} Qk,i表示其第 k k k行第 i i i列的元素;
p p p – 用户偏置向量,共M个元素;
q q q – 物品偏置向量,共N个元素;
c c c – 整体偏置,标量;
K K K – 相当于Embeding / 隐向量维度,通常 K K K远小于 M , N M,N M,N

4.2 损失函数

y u , i y_{u,i} yu,i为真值(Ground Truth),误差损失(MSE):

L e = ∥ y ^ − y ∥ 2 = ∑ u , i ( y ^ u , i − y u , i ) 2 L_e = \|\hat y - y\|^2 = \sum_{u,i} (\hat y_{u,i} - y_{u,i})^2 Le=y^y2=u,i(y^u,iyu,i)2

正则化损失:

L 2 = ∥ P ∥ 2 + ∥ Q ∥ 2 + ∥ p ∥ 2 + ∥ q ∥ 2 + ∥ c ∥ 2 L_2 = \|P\|^2 + \|Q\|^2 + \|p\|^2 + \|q\|^2 + \|c\|^2 L2=P2+Q2+p2+q2+c2

综上,整体损失

L = L e + λ L 2 L = L_e + \lambda L_2 L=Le+λL2

4.3 梯度计算

关于损失误差 L e L_e Le

∂ L e ∂ y ^ u , i = 2 ( y ^ u , i − y u , i ) ∂ y ^ u , i ∂ P u , k = Q k , i ∂ y ^ u , i ∂ Q k , i = P u , k ∂ y ^ u , i ∂ p u = ∂ y ^ u , i ∂ q i = ∂ y ^ u , i ∂ c = 1 \begin{aligned} \frac{\partial L_e}{\partial \hat y_{u,i}} &= 2 (\hat y_{u,i} - y_{u, i}) \\ \frac{\partial \hat y_{u,i}}{\partial P_{u,k}} &= Q_{k,i} \\ \frac{\partial \hat y_{u,i}}{\partial Q_{k,i}} &= P_{u,k} \\ \frac{\partial \hat y_{u,i}}{\partial p_u} &= \frac{\partial \hat y_{u,i}}{\partial q_i} = \frac{\partial \hat y_{u,i}}{\partial c} =1 \\ \end{aligned} y^u,iLePu,ky^u,iQk,iy^u,ipuy^u,i=2(y^u,iyu,i)=Qk,i=Pu,k=qiy^u,i=cy^u,i=1

于是有:

∂ L e ∂ P u , k = ∂ L e ∂ y ^ u , i ∂ y ^ u , i ∂ P u , k = 2 ( y ^ u

  • 10
    点赞
  • 135
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值