推荐系统实战(4)——基于模型的协同过滤算法(隐语义模型LFM)(代码实现)

LFM模型通过降维的方法将user-item评分矩阵缺失值补全。


 

1 基本原理

LFM(Latent Factor Model)隐语义模型是最近几年推荐系统领域最为热门的研究话题,它的核心思想是通过隐含特征(Latent Factor)联系用户兴趣和物品。那这种模型跟ItemCF或UserCF有什么不同呢?这里可以做一个对比:

  • 对于UserCF,我们可以先计算和目标用户兴趣相似的用户,之后再根据计算出来的用户喜欢的物品给目标用户推荐物品。
  • 而ItemCF,我们可以根据目标用户喜欢的物品,寻找和这些物品相似的物品,再推荐给用户。
  • 还有一种方法,先对所有的物品进行分类,再根据用户的兴趣分类给用户推荐该分类中的物品,LFM就是用来实现这种方法。

    就是根据用户的当前偏好信息,得到用户的兴趣偏好,将该类兴趣对应的物品推荐给当前用户。比 如,用户A喜欢的《数据挖掘导论》属于计算机类的书籍,那我们可以将其他的计算机类书籍推荐给用户A;用户B喜欢的是文学类数据,可将《巴黎圣母院》等这 类文字作品推荐给用户B。这就是隐语义模型,依据“兴趣”这一隐含特征将用户与物品进行连接,需要说明的是此处的“兴趣”其实是对物品类型的一个分类而 已。



     

这个基于兴趣分类的方法大概需要解决3个问题。

  • 如何给物品进行分类?
  • 如何确定用户对哪些类的物品感兴趣,以及感兴趣的程度?
  • 对于一个给定的类,选择哪些属于这个类的物品推荐给用户,以及如何确定这些物品在一个类中的权重?

2 隐语义模型的数学理解

我们从数学角度来理解隐语义模型。如下图所示,R矩阵是用户对物品的偏好信息(Rij表示的是user i对item j的兴趣度),P矩阵是用户对各物品类别的一个偏好信息(Pij表示的是user i对class j的兴趣度),Q矩阵是各物品所归属的的物品类别的信息(Qij表示的是item j在class i中的权重)。隐语义模型就是要将矩阵R分解为矩阵P和矩阵Q的乘积,即通过矩阵中的物品类别(class)将用户user和物品item联系起来。实际 上我们需要根据用户当前的物品偏好信息R进行计算,从而得到对应的矩阵P和矩阵Q。


 

 

3 隐语义模型所解决的问题

这个基于兴趣分类的方法大概需要解决3个问题。

  • 如何给物品进行分类?
  • 如何确定用户对哪些类的物品感兴趣,以及感兴趣的程度?
  • 对于一个给定的类,选择哪些属于这个类的物品推荐给用户,以及如何确定这些物品在一个类中的权重?

 3.1 物品分类

对于第一个问题的简单解决方案是找编辑给物品分类。以图书为例,每本书出版时,编辑都会给书一个分类。为了给图书分类,出版界普遍遵循中国图书分类法。但是,即使有很系统的分类体系,编辑给出的分类仍然具有以下缺点:

  • 编辑的意见不能代表各种用户的意见。比如,对于《具体数学》应该属于什么分类,有人认为应该属于数学,有些人认为应该属于计算机。从内容看,这本书是关于数学的,但从用户看,这本书的读大部分是做计算机出身的。编辑的分类大部分是从书的内容出发,而不是从书的读者群出发。
  • 编辑很难控制分类的粒度。我们知道分类是有不同粒度的,《数据挖掘导论》在粗粒度的分类中可能属于计算机技术,但在细粒度的分类中可能属于数据挖掘。对于不同的用户,我们可能需要不同的粒度。比如对于一位初学者,我们粗粒度地给他做推荐就可以了,而对于一名资深研究人员,我们就需要深入到他的很细分的领域给他做个性化推荐。
  • 编辑很难给一个物品多个分类。有的书不仅属于一个类,而是可能属于很多的类。
  • 编辑很难给出多维度的分类。我们知道,分类是可以有很多维度的,比如按照作者分类、按照译者分类、按照出版社分类。比如不同的用户看《具体数学》原因可能不同,有些人是因为它是数学方面的书所以才看的,而有些人是因为它是大师Knuth的著作所以才去看,因此在不同人的眼中这本书属于不同的分类。
  • 编辑很难决定一个物品在某一个分类中的权重。比如编辑可以很容易地决定《数据挖掘导论》属于数据挖掘类图书,但这本书在这类书中的定位是什么样的,编辑就很难给出一个准确的数字来表示。

为了解决上面的问题,研究人员提出:为什么我们不从数据出发,自动地找到那些类,然后进行个性化推荐?于是,隐含语义分析技术(latent variable analysis)出现了。隐含语义分析技术因为采取基于用户行为统计的自动聚类,较好地解决了上面提出的5个问题。

最简单的办法是直接矩阵分解:

我们假定隐藏因子F的个数小于user和item数。因为如果每个user都关联一个独立的隐藏因子,那就没法做预测了。

3.2 用户对物品感兴趣程度

 

 

4 代码实现 

 

import pandas as pd
import numpy as np
critics={'Lisa Rose': {'Lady in the Water': 2.5, 'Snakes on a Plane': 3.5,
 'Just My Luck': 3.0, 'Superman Returns': 3.5, 'You, Me and Dupree': 2.5, 
 'The Night Listener': 3.0},
'Gene Seymour': {'Lady in the Water': 3.0, 'Snakes on a Plane': 3.5, 
 'Just My Luck': 1.5, 'Superman Returns': 5.0, 'The Night Listener': 3.0, 
 'You, Me and Dupree': 3.5}, 
'Michael Phillips': {'Lady in the Water': 2.5, 'Snakes on a Plane': 3.0,
 'Superman Returns': 3.5, 'The Night Listener': 4.0},
'Claudia Puig': {'Snakes on a Plane': 3.5, 'Just My Luck': 3.0,
 'The Night Listener': 4.5, 'Superman Returns': 4.0, 
 'You, Me and Dupree': 2.5},
'Mick LaSalle': {'Lady in the Water': 3.0, 'Snakes on a Plane': 4.0, 
 'Just My Luck': 2.0, 'Superman Returns': 3.0, 'The Night Listener': 3.0,
 'You, Me and Dupree': 2.0}, 
'Jack Matthews': {'Lady in the Water': 3.0, 'Snakes on a Plane': 4.0,
 'The Night Listener': 3.0, 'Superman Returns': 5.0, 'You, Me and Dupree': 3.5},
'Toby': {'Snakes on a Plane':4.5,'You, Me and Dupree':1.0,'Superman Returns':4.0}}
criticsdf = pd.DataFrame(critics).stack()
criticsdf=pd.DataFrame([criticsdf.index.get_level_values(1),criticsdf.index.get_level_values(0),criticsdf.values],index=['user','movies','rating']).T
criticsdf

 

4.1 正负反馈的item

首先我们需要计算包含用户喜欢与不喜欢物品的数据集,采用不计算评分的隐反馈方式,只要用户评过分均认为用户对该物品有兴趣,而没有评分则可能没兴趣。用户正反馈数据:

def getUserPositiveItem(frame, user):
    '''
    获取用户正反馈物品:用户评分过的物品
    :param frame: 数据集
    :param user: 用户
    :return: 正反馈物品
    '''    
    columns = list(frame.columns)
    series = frame[frame[columns[0]] == user][columns[1]]
    positiveItemList = list(series.values)
    return positiveItemList
getUserPositiveItem(criticsdf,'Toby')

 用户负反馈数据,根据用户无评分物品进行推荐,越热门的物品用户却没有进行过评分,认为用户越有可能对这物品没有兴趣

def getUserNegativeItem(frame, user):  
    ''''' 
    获取用户负反馈物品:热门但是用户没有进行过评分 与正反馈数量相等 
    :param frame: 数据集 
    :param user:用户
    :return: 负反馈物品 
    '''      
    columns = list(frame.columns)
    userItemlist = list(set(frame[frame[columns[0]] == user][columns[1]])) #用户评分过的物品  
    otherItemList = [item for item in set(frame[columns[1]].values) if item not in userItemlist] #用户没有评分的物品  
    itemCount = [len(frame[frame[columns[1]] == item][columns[0]]) for item in otherItemList]      #物品热门程度  
    series = pd.Series(itemCount, index=otherItemList)  
    series = series.sort_values(ascending=False)[:len(userItemlist)]                            #获取正反馈物品数量的负反馈物品  
    negativeItemList = list(series.index)  
    return negativeItemList  
getUserNegativeItem(criticsdf,'Toby')

 4.2 初始化数据集


初始化用户正负反馈物品,正反馈标签为1,负反馈为0



def initUserItem(frame, userList):
    '''
    :param frame: ratings数据
    :param userList: 用户集
    :return: 正负反馈物品字典
    '''
    userItem={}
    for user in userList:
        positiveItem = getUserPositiveItem(frame, user)
        negativeItem = getUserNegativeItem(frame, user)
        itemDict = {}
        for item in positiveItem: itemDict[item] = 1
        for item in negativeItem: itemDict[item] = 0
        userItem[user]=itemDict
    return userItem
initUserItem(criticsdf,set(criticsdf['user']))

初始化参数q,p矩阵, 这里我们采用随机初始化的方式,将p和q取值在[0,1]之间
def initPara(userList, itemList, classCount):
    '''
    :param userList:用户集
    :param itemList:物品集
    :param classCount: 隐类数量
    :return: 参数p,q
    '''
    arrayp = np.random.rand(len(userList), classCount)
    arrayq = np.random.rand(classCount, len(itemList))
    p = pd.DataFrame(arrayp, columns=range(0,classCount), index=userList)
    q = pd.DataFrame(arrayq, columns=itemList, index=range(0,classCount))
    return p,q
initPara(set(criticsdf['user']),set(criticsdf['movies']),classCount=3)

初始化模型:参数p,q,样本数据
def initModel(frame, classCount):
    '''
    :param frame: 源数据
    :param classCount: 隐类数量
    :return:
    '''
    columns = list(frame.columns)    
    userList = list(set(frame[columns[0]].values))
    itemList = list(set(frame[columns[1]].values))
    p, q = initPara(userList, itemList, classCount)
    userItem = initUserItem(frame,userList)
    return p, q, userItem
p,q,userItem=initModel(criticsdf,3)
userItem

 4.3 隐语义模型


定义函数,计算用户对物品的兴趣

from math import exp
def sigmod(x):
    '''
    单位阶跃函数,将兴趣度限定在[0,1]范围内
    :param x: 兴趣度
    :return: 兴趣度
    '''
    y = 1.0/(1+exp(-x))
    return y


def lfmPredict(p, q, user,item):
    '''
    利用参数p,q预测目标用户对目标物品的兴趣度
    :param p: 用户兴趣和隐类的关系
    :param q: 隐类和物品的关系
    :param user: 目标用户
    :param item: 目标物品
    :return: 预测兴趣度
    '''
    p = np.mat(p.loc[user].values)
    q = np.mat(q.loc[:,item].values).T
    r = (p * q).sum()
    r = sigmod(r)
    return r

lfmPredict(p,q,'Toby','You, Me and Dupree')

from math import exp
def sigmod(x):
    '''
    单位阶跃函数,将兴趣度限定在[0,1]范围内
    :param x: 兴趣度
    :return: 兴趣度
    '''
    y = 1.0/(1+exp(-x))
    return y


def lfmPredict(p, q, user,item):
    '''
    利用参数p,q预测目标用户对目标物品的兴趣度
    :param p: 用户兴趣和隐类的关系
    :param q: 隐类和物品的关系
    :param user: 目标用户
    :param item: 目标物品
    :return: 预测兴趣度
    '''
    p = np.mat(p.loc[user].values)
    print(p)
    q = np.mat(q.loc[:,item].values).T
    print(q)
    print(p*q)
    r = (p * q).sum()
    r = sigmod(r)
    return r

lfmPredict(p,q,'Toby','Just My Luck')

用梯度下降法计算较优的隐语义模型计算参数p,q
def latenFactorModel(frame, classCount, iterCount, alpha, lamda):
    '''
    :param frame: 源数据
    :param classCount: 隐类数量
    :param iterCount: 迭代次数
    :param alpha: 步长,学习率
    :param lamda: 正则化参数
    :return: 参数p,q
    '''
    p, q, userItem = initModel(frame, classCount)
    for step in range(0, iterCount):
        for user in userItem:
            for item,rui in userItem[user].items():
                eui = rui - lfmPredict(p=p, q=q, user=user, item=item)
                for f in range(0, classCount):
                    print('step %d user %s class %d' % (step, user, f))
                    p[f][user] += alpha * (eui * q[item][f] - lamda * p[f][user])
                    q[item][f] += alpha * (eui * p[f][user] - lamda * q[item][f])
        alpha *= 0.9
    return p, q
p,q=latenFactorModel(criticsdf, 3, 10, 0.02, 0.01)

 

4.4 推荐商品

最后根据计算出来的p和q参数对用户进行物品的推荐

def recommend(frame, user, p, q, TopN=5):
    '''
    推荐TopN个物品给目标用户
    :param frame: 源数据
    :param user: 目标用户
    :param p: 用户兴趣和隐类的关系
    :param q: 隐类和物品的关系
    :param TopN: 推荐数量
    :return: 推荐物品
    '''    
    columns = list(frame.columns) 
    userItemlist = list(set(frame[frame[columns[0]] == user][columns[1]]))
    otherItemList = [item for item in set(frame[columns[1]].values) if item not in userItemlist]
    predictList = [lfmPredict(p, q, user, item) for item in otherItemList]#对无评分物品的兴趣度预测
    series = pd.Series(predictList, index=otherItemList)
    series = series.sort_values(ascending=False)[:TopN]
    return series
recommend(criticsdf, 'Toby', p, q)

 

 

5、优缺点分析

隐语义模型在实际使用中有一个困难,那就是它很难实现实时推荐。经典的隐语义模型每次训练时都需要扫描所有的用户行为记录,这样才能计算出用户对于 每个隐分类的喜爱程度矩阵P和每个物品与每个隐分类的匹配程度矩阵Q。而且隐语义模型的训练需要在用户行为记录上反复迭代才能获得比较好的性能,因此 LFM的每次训练都很耗时,一般在实际应用中只能每天训练一次,并且计算出所有用户的推荐结果。从而隐语义模型不能因为用户行为的变化实时地调整推荐结果 来满足用户最近的行为。

 

 

参考:https://blog.csdn.net/dooonald/article/details/80269073  推荐系统初学者系列(3)-- 隐语义模型(LFM)与矩阵分解模型

https://blog.csdn.net/shinecjj/article/details/82286386?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task   推荐系统总结

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值