推荐系统- 评分预测问题

          内容来源《推荐系统实践》

         目前为止都是在讨论TopN推荐,即给定一个用户,如何给他生成一个长度为N的推荐 列表,使该推荐列表能够尽量满足用户的兴趣和需求。本书之所以如此重视TopN推荐,是因为 它非常接近于满足实际系统的需求,实际系统绝大多数情况下就是给用户提供一个包括N个物品 的个性化推荐列表。 但是,很多从事推荐系统研究的同学最早接触的却是评分预测问题。从GroupLens到Netflix Prize到Yahoo! Music的KDD Cup,评分预测问题都是推荐系统研究的核心。评分预测问题最基本 的数据集就是用户评分数据集。该数据集由用户评分记录组成,每一条评分记录是一个三元组(u, i, r),表示用户u 给物品 i  赋予了评分 r,本章用 r_{u}_{i} 表示用户 u 对物品 i 的评分。因为用户不可能对所 有物品都评分,因此评分预测问题就是如何通过已知的用户历史评分记录预测未知的用户评分记 录。表8-1是一个评分预测问题的例子,在该例子中每个用户都对一些电影给出了评分,比如用 户A给《虎口脱险》评了1分,给《唐山大兄》评了5分,给《少林足球》评了4分,给《大话西 游》评了5分。但是,每个用户都没有对所有电影评分,比如用户A没有给《变形金刚》和《黑 客帝国》评分。那么,当用户浏览网页并看到《变形金刚》和《黑客帝国》时,我们希望能够给 用户一个分数表明我们认为用户是否会喜欢这部电影,而这个分数也可以帮助用户决策是否要看 这部电影,而如何提高这个分数的预测精度就是评分预测要解决的主要问题

        

离线实验方法:

    评分预测问题基本都通过离线实验进行研究。在给定用户评分数据集后,研究人员会将数据 集按照一定的方式分成训练集和测试集,然后根据测试集建立用户兴趣模型来预测测试集中的用 户评分。对于测试集中的一对用户和物品(u, i),用户u对物品i的真实评分是 r_{u}_{i},而推荐算法预 测的用户u对物品 i 的评分为 \widehat{r}_{u}_{i},那么一般可以用均方根误差RMSE度量预测的精度:

                                              

                 评分预测的目的就是找到最好的模型最小化测试集的RMSE。 关于如何划分训练集和测试集,如果是和时间无关的预测任务,可以以均匀分布随机划分数 据集,即对每个用户,随机选择一些评分记录作为测试集,剩下的记录作为测试集。如果是和时 间相关的任务,那么需要将用户的旧行为作为训练集,将用户的新行为作为测试集。Netflix通过 如下方式划分数据集,首先将每个用户的评分记录按照从早到晚进行排序,然后将用户最后10% 的评分记录作为测试集,前90%的评分记录作为训练集.

 评分预测算法

           平均值:

               

 

                 

                      

def PredictAll(records, user_cluster, item_cluster):
    total = dict()
    count = dict()
    for r in records:
        if r.test != 0:
            continue
        gu = user_cluster.GetGroup(r.user)
        gi = item_cluster.GetGroup(r.item)
        basic.AddToMat(total, gu, gi, r.vote)
        basic.AddToMat(count, gu, gi, 1)
    for r in records:
        gu = user_cluster.GetGroup(r.user)
        gi = item_cluster.GetGroup(r.item) 
        average = total[gu][gi] / (1.0 * count[gu][gi] + 1.0)
        r.predict = average 
"""
在这段代码中,user_cluster.GetGroup函数接收一个用户ID,然后根据一定的算法返回
用户的类别。item_cluster.GetGroup函数接收一个物品的ID,然后根据一定的算法返回物品的类
别。total[gu][gi]/count[gu][gi]记录了第gu类用户给第gi类物品评分的平均分。
上文提到,user_cluster和item_cluster有很多不同的定义方式,下面的Python代码给
出了不同的user_cluster和item_cluster定义方式。其中,Cluster是基类,对于任何用户和
物品,它的GetGroup函数都返回0,因此如果user_cluster和item_cluter都是Cluster类型,
那么最终的预测函数就是全局平均值。IdCluster的GetGroup函数接收一个ID,会返回这个ID,
那么如果user_cluster是Cluster类型,而item_cluster是IdCluster类型,那么最终的预
测函数给出的就是物品平均值。以此类推,表8-2展示了MovieLens数据集中利用不同平均值方法
得到的RMSE,实验结果表明对用户使用UserVoteCluster,对物品采用ItemVoteCluster,
可以获得最小的RMSE。


"""

class Cluster:
    def __init__(self,records):
        self.group = dict()
    def GetGroup(self, i):
        return 0
class IdCluster(Cluster):
    def __init__(self, records):
        Cluster.__init__(self, records)
    def GetGroup(self, i):
        return i
class UserActivityCluster(Cluster):
    def __init__(self, records):
        Cluster.__init__(self, records)
        activity = dict()
        for r in records:
            if r.test != 0:
                continue
            basic.AddToDict(activity, r.user, 1)
         k = 0
         for user, n in sorted(activity.items(), \
             key=itemgetter(1), reverse=False):
             c = int((k * 5) / (1.0 * 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] 


class ItemPopularityCluster(Cluster):
    def __init__(self, records):
        Cluster.__init__(self, records)
        popularity = dict()
        for r in records:
            if r.test != 0:
                continue
        basic.AddToDict(popularity, r.item, 1)
        k = 0
        for item, n in sorted(popularity.items(), \
            key=itemgetter(1), reverse=False):
            c = int((k * 5) / (1.0 * len(popularity)))
            self.group[item] = c
            k += 1
    def GetGroup(self, item):
        if item not in self.group:
            return -1
        else:
            return self.group[item] 



class UserVoteCluster(Cluster):
    def __init__(self, records):
        Cluster.__init__(self, records)
        vote = dict()
        count = dict()
        for r in records:
            if r.test != 0:
                continue
            basic.AddToDict(vote, r.user, r.vote)
            basic.AddToDict(count, r.user, 1)
        k = 0
        for user, v in vote.items():
            ave = v / (count[user] * 1.0)
            c = int(ave * 2)
            self.group[user] = c
    def GetGroup(self, uid):
        if uid not in self.group:
            return -1
        else:
             return self.group[uid] 


class ItemVoteCluster(Cluster):
    def __init__(self, records):
        Cluster.__init__(self, records)
        vote = dict()
        count = dict()
        for r in records:
            if r.test != 0:
                continue
            basic.AddToDict(vote, r.item, r.vote)
            basic.AddToDict(count, r.item, 1)
        k = 0 
        for item, v in vote.items():
            ave = v / (count[item] * 1.0)
            c = int(ave * 2)
            self.group[item] = c 

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





         

  基于邻域的方法 

       基于用户的邻域算法和基于物品的邻域算法都可以应用到评分预测中。基于用户的邻域算法 认为预测一个用户对一个物品的评分,需要参考和这个用户兴趣相似的用户对该物品的评分,即:

                                         

def UserSimilarity(records):
    item_users = dict()
    ave_vote = dict()
    activity = dict()
    for r in records:
        addToMat(item_users, r.item, r.user, r.value)
        addToVec(ave_vote, r.user, r.value)
        addToVec(activity, r.user, 1)
        ave_vote = {x:y/activity[x] for x,y in ave_vote.items()}
        nu = dict()
        W = dict()
    for i,ri in item_users.items():
        for u,rui in ri.items():
            addToVec(nu, u, (rui - ave_vote[u])*(rui - ave_vote[u]))
            for v,rvi in ri.items():
                if u == v:
                    continue
                addToMat(W, u, v, (rui - ave_vote[u])*(rvi - ave_vote[v]))
    for u in W:
        W[u] = {x:y/math.sqrt(nu[x]*nu[u]) for x,y in W[u].items()}

    return W 


def PredictAll(records, test, ave_vote, W, K):
    user_items = dict()
    for r in records:
        addToMat(user_items, r.user, r.item, r.value)
    for r in test:
        r.predict = 0
        norm = 0
        for v,wuv in sorted(W[r.user].items(),key=itemgetter(1), reverse=True)[0:K]:
            if r.item in user_items[v]:
               rvi = user_items[v][r.item]
               r.predict += wuv * (rvi - ave_vote[v])
               norm += abs(wuv)
        if norm > 0:
            r.predict /= norm
        r.predict += ave_vote[r.user] 

     基于物品的邻域算法在预测用户u对物品i的评分时,会参考用户u对和物品i相似的其他物品 的评分,即:

                    

  这里,S(i, K)是和i最相似的物品集合,N(u)是用户u评过分的物品集合,W_{i}_{j}是物品之间的相 似度, \overline{r}_{i}是物品i的平均分。对于如何计算物品的相似度,文章比较了3种主要的相似度:

                  

   Sarwar利用MovieLens最小的数据集对3种相似度进行了对比,并将MAE作为评测指标。实 验结果表明利用修正后的余弦相似度进行评分预测可以获得最优的MAE。不过需要说明的是, 在一个数据集上的实验并不意味着在其他数据集上也能获得相同的结果。

隐语义模型与矩阵分解模型

      最近这几年做机器学习和数据挖掘研究的人经常会看到下面的各种名词,即隐含类别模型 (Latent Class Model)、隐语义模型(Latent Factor Model)、pLSA、LDA、Topic Model、Matrix Factorization、Factorized Model。 这些名词在本质上应该是同一种思想体系的不同扩展。在推荐系统领域,提的最多的就是潜 语义模型和矩阵分解模型。其实,这两个名词说的是一回事,就是如何通过降维的方法将评分矩 阵补全。 用户的评分行为可以表示成一个评分矩阵R,其中R[u][i]就是用户u对物品i的评分。但是,用 户不会对所有的物品评分,所以这个矩阵里有很多元素都是空的,这些空的元素称为缺失值 (missing value)。因此,评分预测从某种意义上说就是填空,如果一个用户对一个物品没有评过 分,那么推荐系统就要预测这个用户是否是否会对这个物品评分以及会评几分.

传统的SVD分解

   对于如何补全一个矩阵,历史上有过很多的研究。一个空的矩阵有很多种补全方法,而我们 要找的是一种对矩阵扰动最小的补全方法。那么什么才算是对矩阵扰动最小呢?一般认为,如果 补全后矩阵的特征值和补全之前矩阵的特征值相差不大,就算是扰动比较小。所以,最早的矩阵 分解模型就是从数学上的SVD(奇异值分解)开始的。② 给定m个用户和n个物品,和用户对物品

Simon Funk的SVD分

     由于上面的两个缺点,SVD分解算法提出几年后在推荐系统领域都没有得到广泛的关 注。直到2006年Netflix Prize开始后,Simon Funk在博客上公布了一个算法①(称为Funk-SVD), 一下子引爆了学术界对矩阵分解类方法的关注。而且,Simon Funk的博客也成为了很多学术论文 经常引用的对象。Simon Funk提出的矩阵分解方法后来被Netflix Prize的冠军Koren称为Latent Factor Model(简称为LFM)

代码实现了学习LFM模型时的迭代过程。在LearningLFM函数中,输入train是训 练集中的用户评分记录,F是隐类的格式,n是迭代次数:

def LearningLFM(train, F, n, alpha, lambda):
    [p,q] = InitLFM(train, F)
    for step in range(0, n):
        for u,i,rui in train.items():
            pui = Predict(u, i, p, q)
            eui = rui - pui
            for f in range(0,F):
                p[u][k] += alpha * (q[i][k] * eui - lambda * p[u][k])
                q[i][k] += alpha * (p[u][k] * eui - lambda * q[i][k])
        alpha *= 0.9
    return list(p, q) 

如上面的代码所示,LearningLFM主要包括两步。首先,需要对P、Q矩阵进行初始化,然后需要通过随机梯度下降法的迭代得到最终的P、Q矩阵。在迭代时,需要在每一步对学习参数α  进行衰减(alpha *= 0.9),这是随机梯度下降法算法要求的,其目的是使算法尽快收敛。如果 形象一点说就是,如果需要在一个区域找到极值,一开始可能需要大范围搜索,但随着搜索的进 行,搜索范围会逐渐缩小。 初始化P、Q矩阵的方法很多,一般都是将这两个矩阵用随机数填充,但随机数的大小还是 有讲究的,根据经验,随机数需要和1/sqrt(F)成正比。下面的代码实现了初始化功能

def InitLFM(train, F):
    p = dict()
    q = dict()
    for u, i, rui in train.items():
        if u not in p:
            p[u] = [random.random()/math.sqrt(F) for x in range(0,F)]
        if i not in q:
            q[i] = [random.random()/math.sqrt(F) for x in range(0,F)]
    return list(p, q)
# 而预测用户u对物品i的评分可以通过如下代码实现:
def Predict(u, i, p, q):
     return sum(p[u][f] * q[i][f] for f in range(0,len(p[u])) 

加入偏置项后的LFM:

      

       

def LearningBiasLFM(train, F, n, alpha, lambda, mu):
    [bu, bi, p,q] = InitLFM(train, F)
    for step in range(0, n):
        for u,i,rui in train.items():
            pui = Predict(u, i, p, q, bu, bi, mu)
            eui = rui - pui
            bu[u] += alpha * (eui - lambda * bu[u])
            bi[i] += alpha * (eui - lambda * bi[i])
           for f in range(0,F):
                p[u][k] += alpha * (q[i][k] * eui - lambda * p[u][k])
                q[i][k] += alpha * (p[u][k] * eui - lambda * q[i][k])
         alpha *= 0.9
    return list(bu, bi, p, q) 

      

def InitBiasLFM(train, F):
    p = dict()
    q = dict()
    bu = dict()
    bi = dict()
    for u, i, rui in train.items():
        bu[u] = 0
        bi[i] = 0
        if u not in p:
            p[u] = [random.random()/math.sqrt(F) for x in range(0,F)]
         if i not in q:
            q[i] = [random.random()/math.sqrt(F) for x in range(0,F)] return
list(p, q)

def Predict(u, i, p, q, bu, bi, mu):
    ret = mu + bu[u] + bi[i]
    ret += sum(p[u][f] * q[i][f] for f in range(0,len(p[u]))
    return ret 

考虑邻域影响的LFM:
    
前面的LFM模型中并没有显式地考虑用户的历史行为对用户评分预测的影响。为此,Koren 在Netflix Prize比赛中提出了一个模型,将用户历史评分的物品加入到了LFM模型中,Koren将 该模型称为SVD++。 在介绍SVD++之前,我们首先讨论一下如何将基于邻域的方法也像LFM那样设计成一个可以学习的模型。其实很简单,我们可以将ItemCF的预测算法改成如下方式:

            

def LearningBiasLFM(train_ui, F, n, alpha, lambda, mu):
     [bu, bi, p, q, y] = InitLFM(train, F)
     z = dict()
     for step in range(0, n):
         for u,items in train_ui.items():
             z[u] = p[u]
             ru = 1 / math.sqrt(1.0 * len(items))
             for i,rui in items items():
                 for f in range(0,F):
                     z[u][f] += y[i][f] * ru
             sum = [0 for i in range(0,F)]
             for i,rui in items items():
                 pui = Predict()
                 eui = rui - pui
                 bu[u] += alpha * (eui - lambda * bu[u])
                 bi[i] += alpha * (eui - lambda * bi[i])
                 for f in range(0,F):
                     sum[k] += q[i][k] * eui * ru
                     p[u][k] += alpha * (q[i][k] * eui - lambda * p[u][k])
                     q[i][k] += alpha * ((z[u][k] + p[u][k]) * eui - lambda * q[i][k])
             for i,rui in items items():
                 for f in range(0,F):
                     y[i][f] += alpha * (sum[f] - lambda * y[i][f])
         alpha *= 0.9
     return list(bu, bi, p, q) 

加入时间信息:

模型融合

  

   生一个新模型,按照一定的参数加到旧模型上去,从而使训练集误差最小化。不同的是,这里每 次生成新模型时并不对样本集采样,针对那些预测错的样本,而是每次都还是利用全样本集进行 预测,但每次使用的模型都有区别。 一般来说,级联融合的方法都用于简单的预测器,比如前面提到的平均值预测器。下面的 Python代码实现了利用平均值预测器进行级联融合的方法。

  

def Predict(train, test, alpha):
 total = dict()
 count = dict()
 for record in train:
     gu = GetUserGroup(record.user)
     gi = GetItemGroup(record.item)
     AddToMat(total, gu, gi, record.vote - record.predict)
     AddToMat(count, gu, gi, 1)
 for record in test:
     gu = GetUserGroup(record.user)
     gi = GetUserGroup(record.item)
     average = total[gu][gi] / (1.0 * count[gu][gi] + alpha)
     record.predict += average

表8-3展示了MovieLens数据集上对平均值方法采用级联融合后的RMSE。如果和表8-2的结果 对比就可以发现,采用级联融合后,测试集的RMSE从0.9342下降到了0.9202。由此可见,即使 是利用简单的算法进行级联融合,也能得到比较低的评分预测误差。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章