推荐算法(7)缺失的评分预测问题

推荐算法(1):协同过滤总结
推荐算法(2):基于内容的推荐
推荐算法(3):利用用户标签数据
推荐算法(4)利用上下文信息
推荐算法(5)利用社交网络数据
推荐算法(6) 实例
推荐算法(7)缺失的评分预测问题
推荐算法(8)评测指标

评分预测问题:
就是user-item矩阵是一个稀疏的矩阵,我们要根据已知值来预测出未知项的值。

评测标准:
测试集的均分误差。
训练集,测试集的划分:
如果不和时间有关,就随机选;如果和时间有关就将最后10%作测试。

评分预测算法:
1.平均值
1.1全局平均值
在这里插入图片描述
1.2 用户评分平均值
在这里插入图片描述
1.3物品评分平均值
在这里插入图片描述
1.4用户分类对物品分类的平均值
假设有两个分类函数,一个是用户分类函数ϕ ,一个是物品分类函数ψ。ϕ(u) 定义了用户u所属的类,ψ(i定义了物品i所属的类。则可以利用训练集中同类用户对同类物品评分的平均值预测用户对物品的评分,即:
在这里插入图片描述

其实前面所有的平均值都是这种类类平均值的特例。除了这3种特殊的平均值,在用户评分数据上还可以定义很多不同的分类函数。

1.用户和物品的平均分 对于一个用户,可以计算他的评分平均分。然后将所有用户按照评分平均分从小到大排序,并将用户按照平均分平均分成N类。物品也可以用同样的方式分类。
2.用户活跃度和物品流行度 对于一个用户,将他评分的物品数量定义为他的活跃度。得到用户活跃度之后,可以将用户通过活跃度从小到大排序,然后平均分为N类。物品的流行度定义为给物品评分的用户数目,物品也可以按照流行度均匀分成N类。

2.基于邻域的方法
基于用户的邻域算法和基于物品的邻域算法都可以应用到评分预测中。
1.基于用户的邻域算法
该算法认为预测一个用户对一个物品的评分,需要参考和这个用户兴趣相似的用户对该物品的评分,即:
在这里插入图片描述
这里,S(u,K) 是和用户u兴趣最相似的K个用户的集合,N(i)是对物品i评过分的用户集合。 rvi 是用户v对物品i的评分,r¯v 是用户v对他评过分的所有物品评分的平均值。
用户之间的相似度wuv 可以通过皮尔逊系数计算:
在这里插入图片描述
2.基于物品的邻域算法
该算法在预测用户u对物品i的评分时,会参考用户u对和物品i相似的其他物品的评分,即
在这里插入图片描述
这里,S(i,K) 是和i最相似的物品集合,N(u) N(u)N(u)是用户u评过分的物品集合, wij 是物品之间的相似度,r¯i 是物品i的平均分。

至于如何计算物品之间的相似度,有如下三种方式:
a. 余弦相似度
在这里插入图片描述
b. 皮尔逊系数
在这里插入图片描述
c. 被Sarwar称为修正的余弦相似度
在这里插入图片描述

3 隐语义模型及矩阵分解
用户的评分行为可以表示成一个评分矩阵R,其中R[u][i]就是用户u对物品i的评分。但是,用户不会对所有的物品评分,所以这个矩阵里有很多元素都是空的,这些空的元素称为缺失值(missing value)。因此,评分预测从某种意义上说就是填空,如果一个用户对一个物品没有评过分,那么推荐系统就要预测这个用户是否是否会对这个物品评分以及会评几分。

3.1.传统SVD
补全之后的矩阵特征值和补全之前的特征值相差不大,就是扰动不大

对缺失值简单的补全,再进行SVD分解成R = USV,取最大的f个奇异值对角矩阵Sf及特征向量,构成新矩阵:
在这里插入图片描述
其中,R′f(u,i) 就是用户u对物品i评分的预测值。

这种早期的方法有如下两个缺点:

a. 该方法首先需要用一个简单的方法补全稀疏评分矩阵,,这种空间的需求在实际系统中是不可能接受的。
b. 该方法依赖的SVD分解方法的计算复杂度很高,特别是在稠密的大规模矩阵上更是非常慢。

3.2 Simon Funk的SVD分解 (基于NMF)
其实就是LFM,针对上面两个问题进行解决,直接将评分矩阵R RR分解为两个低维矩阵相乘:
在这里插入图片描述
最后化简式子:
在这里插入图片描述

3.3 加入偏置项后的LFM(BiasSVD)
相比于上面的LFM,这里为预测公式加入了偏置项,如下:
在这里插入图片描述
公式中加入了三项偏置,μ 和bi 。其中μ \muμ是训练集中所有记录的评分的全局平均数,表示网站本身对用户评分的影响;bu是用户偏置项,表示用户的评分习惯中和物品没有关系的那种个人因素;bi是物品偏置项,表示了物品接受的评分中和用户没有什么关系的因素。

3.4 考虑邻域影响的LFM(SVD++)
协同过滤算法
在这里插入图片描述

4.加入时间信息
4.1基于邻域的模型融合时间信息(TItemCF)
通过如下公式预测用户在某一个时刻会给物品什么评分:在这里插入图片描述
其中,Δt=tui −tuj 是用户u对物品i和物品j评分的时间差,wij是物品i和j的相似度,f(wij,Δt)是一个考虑了时间衰减后的相似度函数,可以用如下公式:其中的σ是sigmoid函数。
在这里插入图片描述
可以发现,随着Δt 增加,f(wij,Δt)会越来越小,也就是说用户很久之前的行为对预测用户当前评分的影响越来越小。

4.2 基于矩阵分解的模型融合时间信息(TSVD)
这里其实就是对(User, Item, Time)三维矩阵进行分解,前面的BiasSVD模型为:
在这里插入图片描述

则加入时间信息的可以变为TSVD:得到一个张量,时间的数据,加上为一个三维矩阵
在这里插入图片描述
这里bt建模了系统整体平均分随时间变化的效应,xTu⋅yt 建模了用户平均分随时间变化的效应,sTizt 建模了物品平均分随时间变化的效应,而∑fgu,fhi,flt,f建模了用户兴趣随时间影响的效应。

同样的,对SVD++模型也可以加入时间信息为:在这里插入图片描述
这里,tu 是用户所有评分的平均时间,period(t) 考虑了季节效应,可以定义为时刻t所在的月份。

5.模型融合
一般模型融合都是数据比赛最后的大杀器。
5.1级联融合
这个有点儿像AdaBoost,即每次产生一个新模型,按照一定的参数加到旧模型上去,从而使训练集误差最小化。不同的是,这里每次生成新模型时并不对样本集采样,针对那些预测错的样本,而是每次都还是利用全样本集进行预测,但每次使用的模型都有区别,用来预测上一次的误差,并最后联合在一起预测。

假设已经有一个预测器rˆ(k) ,对于每个用户—物品对(u, i)都给出预测值,那么可以在这个预测器的基础上设计下一个预测器rˆ(k+1) 来最小化损失函数:
在这里插入图片描述

5.2 加权融合
上面那个是串行的,这个就是并行的。假设有K个不同的预测器rˆ(1),rˆ(2),…,rˆ(K)
,最简单的融合算法是线性融合,即最终的预测器rˆ 是这K个预测器的线性加权:
在这里插入图片描述
系数的选取一般采用如下方法:

1.假设数据集已经被分为了训练集A和测试集B,那么首先需要将训练集A按照相同的分割方法分为A1和A2,其中A2的生成方法和B的生成方法一致,且大小相似。
2.在A1上训练K个不同的预测器,在A2上作出预测。因为我们知道A2上的真实评分值,所以可以在A2上利用最小二乘法计算出线性融合系数αk 。
3.在A上训练K个不同的预测器,在B上作出预测,并且将这K个预测器在B上的预测结果按照已经得到的线性融合系数加权融合,以得到最终的预测结果。

# 导入包
import random
import math
import time
from tqdm import tqdm

# 定义装饰器,监控运行时间
def timmer(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        res = func(*args, **kwargs)
        stop_time = time.time()
        print('Func %s, run time: %s' % (func.__name__, stop_time - start_time))
        return res
    return wrapper


class Data():

    def __init__(self, user, item, rate, test=False, predict=0.0):
        self.user = user
        self.item = item
        self.rate = rate
        self.test = test
        self.predict = predict


class Dataset():

    def __init__(self, fp):
        # fp: data file path
        self.data = self.loadData(fp)

    def loadData(self, fp):
        data = []
        for l in open(fp):
            data.append(tuple(map(int, l.strip().split('::')[:3])))
        data = [Data(*d) for d in data]
        return data

    def splitData(self, M, k, seed=1):
        '''
        :params: data, 加载的所有数据条目
        :params: M, 划分的数目,最后需要取M折的平均
        :params: k, 本次是第几次划分,k~[0, M)
        :params: seed, random的种子数,对于不同的k应设置成一样的
        :return: train, test
        '''
        random.seed(seed)
        for i in range(len(self.data)):
            # 这里与书中的不一致,本人认为取M-1较为合理,因randint是左右都覆盖的
            if random.randint(0, M - 1) == k:
                self.data[i].test = True

def RMSE(records):
    rmse = {'train_rmse': [], 'test_rmse': []}
    for r in records:
        if r.test:
            rmse['test_rmse'].append((r.rate - r.predict) ** 2)
        else:
            rmse['train_rmse'].append((r.rate - r.predict) ** 2)
    rmse = {'train_rmse': math.sqrt(sum(rmse['train_rmse']) / len(rmse['train_rmse'])),
            'test_rmse': math.sqrt(sum(rmse['test_rmse']) / len(rmse['test_rmse']))}
    return rmse


# 1. Cluster
class Cluster:

    def __init__(self, records):
        self.group = {}

    def GetGroup(self, i):
        return 0


# 2. IdCluster
class IdCluster(Cluster):

    def __init__(self, records):
        Cluster.__init__(self, records)

    def GetGroup(self, i):
        return i


# 3. UserActivityCluster
class UserActivityCluster(Cluster):

    def __init__(self, records):
        Cluster.__init__(self, records)
        activity = {}
        for r in records:
            if r.test: continue
            if r.user not in activity:
                activity[r.user] = 0
            activity[r.user] += 1
        # 按照用户活跃度进行分组
        k = 0
        for user, n in sorted(activity.items(), key=lambda x: x[-1], reverse=False):
            c = int((k * 5) / len(activity))
            self.group[user] = c
            k += 1

    def GetGroup(self, uid):
        if uid not in self.group:
            return -1
        else:
            return self.group[uid]


# 3. ItemPopularityCluster
class ItemPopularityCluster(Cluster):

    def __init__(self, records):
        Cluster.__init__(self, records)
        popularity = {}
        for r in records:
            if r.test: continue
            if r.item not in popularity:
                popularity[r.item] = 0
            popularity[r.item] += 1
        # 按照物品流行度进行分组
        k = 0
        for item, n in sorted(popularity.items(), key=lambda x: x[-1], reverse=False):
            c = int((k * 5) / len(popularity))
            self.group[item] = c
            k += 1

    def GetGroup(self, iid):
        if iid not in self.group:
            return -1
        else:
            return self.group[iid]


# 4. UserVoteCluster
class UserVoteCluster(Cluster):

    def __init__(self, records):
        Cluster.__init__(self, records)
        vote, cnt = {}, {}
        for r in records:
            if r.test: continue
            if r.user not in vote:
                vote[r.user] = 0
                cnt[r.user] = 0
            vote[r.user] += r.rate
            cnt[r.user] += 1
        # 按照物品平均评分进行分组
        for user, v in vote.items():
            c = v / (cnt[user] * 1.0)
            self.group[user] = int(c * 2)

    def GetGroup(self, uid):
        if uid not in self.group:
            return -1
        else:
            return self.group[uid]


# 5. ItemVoteCluster
class ItemVoteCluster(Cluster):

    def __init__(self, records):
        Cluster.__init__(self, records)
        vote, cnt = {}, {}
        for r in records:
            if r.test: continue
            if r.item not in vote:
                vote[r.item] = 0
                cnt[r.item] = 0
            vote[r.item] += r.rate
            cnt[r.item] += 1
        # 按照物品平均评分进行分组
        for item, v in vote.items():
            c = v / (cnt[item] * 1.0)
            self.group[item] = int(c * 2)

    def GetGroup(self, iid):
        if iid not in self.group:
            return -1
        else:
            return self.group[iid]

# 返回预测接口函数
def PredictAll(records, UserGroup, ItemGroup):
    '''
    :params: records, 数据集
    :params: UserGroup, 用户分组类
    :params: ItemGroup, 物品分组类
    '''
    userGroup = UserGroup(records)
    itemGroup = ItemGroup(records)
    group = {}
    for r in records:
        ug = userGroup.GetGroup(r.user)
        ig = itemGroup.GetGroup(r.item)
        if ug not in group:
            group[ug] = {}
        if ig not in group[ug]:
            group[ug][ig] = []
        group[ug][ig].append(r.rate)
    for ug in group:
        for ig in group[ug]:
            group[ug][ig] = sum(group[ug][ig]) / (1.0 * len(group[ug][ig]) + 1.0)
    # predict
    for r in records:
        ug = userGroup.GetGroup(r.user)
        ig = itemGroup.GetGroup(r.item)
        r.predict = group[ug][ig]


class Experiment():

    def __init__(self, M, UserGroup, ItemGroup, fp='../dataset/ml-1m/ratings.dat'):
        '''
        :params: M, 划分数据集的比例
        :params: UserGroup, ItemGroup, 聚类算法类型
        :params: fp, 数据文件路径
        '''
        self.M = M
        self.userGroup = UserGroup
        self.itemGroup = ItemGroup
        self.fp = fp

    # 定义单次实验
    def worker(self, records):
        '''
        :params: train, 训练数据集
        :params: test, 测试数据集
        :return: train和test的rmse值
        '''
        PredictAll(records, self.userGroup, self.itemGroup)
        metric = RMSE(records)
        return metric

    # 多次实验取平均
    def run(self):
        dataset = Dataset(self.fp)
        dataset.splitData(self.M, 0)
        metric = self.worker(dataset.data)
        print('Result (UserGroup={}, ItemGroup={}): {}'.format( \
            self.userGroup.__name__, \
            self.itemGroup.__name__, metric))


UserGroups = [Cluster, IdCluster, Cluster, UserActivityCluster, UserActivityCluster, Cluster, IdCluster, UserActivityCluster, UserVoteCluster, UserVoteCluster, Cluster, IdCluster, UserVoteCluster]
ItemGroups = [Cluster, Cluster, IdCluster, Cluster, IdCluster, ItemPopularityCluster, ItemPopularityCluster, ItemPopularityCluster, Cluster, IdCluster, ItemVoteCluster, ItemVoteCluster, ItemVoteCluster]
M = 10
for i in range(len(UserGroups)):
    exp = Experiment(M, UserGroups[i], ItemGroups[i])
    exp.run()
  • 3
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值