推荐系统实践 0x08 隐语义模型LFM

隐语义模型(LFM)

LFM(latent factor model)隐语义模型是前几年比较火的模型,它的核心思想是通过隐含特征来联系用户兴趣和物品。我们先给出LFM通过公式计算用户(u)对物品(i)的兴趣:

[mathrm{Preference} = r_{ui} = p_u^T q_i=sum_{f=1}^{F}p_{u,k}q_{i,k} ]

这个公式中(p_{u,k})(q_{i,k})是模型的参数,其中(p_{u,k})度量了用户(u)的兴趣和第(k)个隐类的关系,而(q_{i,k})度量了第(k)个隐类和物品(i)之间的关系。这两个参数需要在优化目标函数的过程中拟合出来。LFM在显性反馈数据(评分)上可以达到很好的精度,而在目前的隐性反馈数据(只有正样本,没有负样本)还需要进一步探索。我们先聊聊怎么在这种数据集上获取负样本。

负样本采集

我们认为,用户没有过行为的物品可以视作为负样本。一般认为,很热门而用户却没有行为更加代表用户对这个物品不感兴趣。因为对于冷门的物品,用户可能是压根没在网站中发现这个物品,所以谈不上是否感兴趣。在每个训练批次上的正负样本的比例对结果也会产生很大影响,这里的负采样就按照流行度进行采样。

def RandomSelectNegativeSample(self, items):
    ret = dict()
    for i in items.keys():
        ret[i] = 1
    n = 0
    for i in range(0, len(items) * 3):
        item = items_pool[random.randint(0, len(items_pool) - 1)]
        # items_pool为候选物品列表,物品出现的次数与物品流行度成正比
        if item in ret:
            continue
        ret[item] = 0
        n += 1
        if n > len(items):
            break
    return ret

目标函数

所需要优化的目标函数

[C=sum_{(u,i)in K}(r_{ui}-hat{r}_{ui})^2=sum_{(u,i)in K}(r_{ui}-sum_{k=1}^{K}p_{u,k}q_{i,k})^2+lambda ||p_u||^2 +lambda ||q_i||^2 ]

后面的正则化项是为了防止训练的过拟合,可以使用随机梯度下降法(SGD)来优化上述的目标函数。

首先对它们分别求偏导数

[frac{partial C}{partial p_{uk}}=-2q_{ik}+2lambda p_{uk} ]

[frac{partial C}{partial q_{ik}}=-2q_{uk}+2lambda p_{ik} ]

对模型参数进行更新

[p_{uk}=p_{uk} + alpha(q_{ik}-lambda p_{uk}) ]

[q_{ik}=q_{ik}+ alpha(q_{uk}-lambda p_{ik}) ]

def LFM(train, ratio, K, lr, step, lmbda, N):
    '''
    :params: train, 训练数据
    :params: ratio, 负采样的正负比例
    :params: K, 隐语义个数
    :params: lr, 初始学习率
    :params: step, 迭代次数
    :params: lmbda, 正则化系数
    :params: N, 推荐TopN物品的个数
    :return: GetRecommendation, 获取推荐结果的接口
    '''
    
    all_items = {}
    for user in train:
        for item in train[user]:
            if item not in all_items:
                all_items[item] = 0
            all_items[item] += 1
    all_items = list(all_items.items())
    items = [x[0] for x in all_items]
    pops = [x[1] for x in all_items]
    
    # 负采样函数(注意!!!要按照流行度进行采样)
    def nSample(data, ratio):
        new_data = {}
        # 正样本
        for user in data:
            if user not in new_data:
                new_data[user] = {}
            for item in data[user]:
                new_data[user][item] = 1
        # 负样本
        for user in new_data:
            seen = set(new_data[user])
            pos_num = len(seen)
            item = np.random.choice(items, int(pos_num * ratio * 3), pops)
            item = [x for x in item if x not in seen][:int(pos_num * ratio)]
            new_data[user].update({x: 0 for x in item})
        
        return new_data
                
    # 训练
    P, Q = {}, {}
    for user in train:
        P[user] = np.random.random(K)
    for item in items:
        Q[item] = np.random.random(K)
            
    for s in trange(step):
        data = nSample(train, ratio)
        for user in data:
            for item in data[user]:
                eui = data[user][item] - (P[user] * Q[item]).sum()
                P[user] += lr * (Q[item] * eui - lmbda * P[user])
                Q[item] += lr * (P[user] * eui - lmbda * Q[item])
        lr *= 0.9 # 调整学习率
        
    # 获取接口函数
    def GetRecommendation(user):
        seen_items = set(train[user])
        recs = {}
        for item in items:
            if item not in seen_items:
                recs[item] = (P[user] * Q[item]).sum()
        recs = list(sorted(recs.items(), key=lambda x: x[1], reverse=True))[:N]
        return recs
    
    return GetRecommendation

目前的算法有这么几个超参数:

  • 隐特征的个数(F)
  • 学习速率(alpha)
  • 正则化参数(lambda)
  • 负样本正样本比例ratio

通过实验发现,ratio参数对LFM的性能影响最大。

小结

LFM模型在实际使用中有一个困难,那就是它很难实现实时的推荐。经典的LFM模型 每次训练时都需要扫描所有的用户行为记录,这样才能计算出用户隐类向量((p_u))和物品隐类向量((q_i))。所以LFM的冷启动问题也十分明显,如在新闻类推荐系统中就会遇到这种问题。对于yahoo的新闻推荐系统来说,他们为了解决冷启动问题,将解决方案分为两部分。首先,他们利用新闻链接的内容属性(关键词、类别等)得到链接(i)的内容特征向量(y_i)。其次,他们会实时地收集用户对链接的行为,并且用这些数据得到链接(i)的隐特征向量(q_i)。然后,他们会利用如下公式预测用户(u)是否会单击链接:

[r_{ui}=x_{u}^{T}cdot y_{i} + p_u^Tcdot q_i ]

参考

《推荐系统实践》(项亮等著) —— 代码实现

原文地址:https://www.cnblogs.com/nomornings/p/14044635.html