推荐系统实践-利用用户行为数据

通过算法自动发掘用户行为数据,从用户的行为中推测出用户的兴趣,从而给用户推荐满足他们兴趣的物品。
基于用户行为分析的推荐算法是个性化推荐系统的重要算法,学术界一般将这种类型的算法称为协同过滤算法。
用户行为在个性化推荐系统中一般分两种:显性反馈行为(explicit feedback)和隐性反馈行为(implicit feedback)。显性反馈行为包括用户明确表示对物品喜好的行为。隐性反馈行为指的是那些不能明确反应用户喜好的行为。
互联网的很多数据都存在Power Law长尾分布。
基于用户行为分析的推荐算法一般称作协同过滤算法,他们分为:基于领域的方法(neighborhood-based)、隐语义模型(latent factor model)、基于图的随机游走算法(random walk on graph)等。
基于领域的方法主要分为基于用户的协同过滤算法基于物品的协同过滤算法
本章采用GroupLens提供的MovieLens数据集介绍和评测算法。将数据集分为M份,其中M-1份作为训练集,1份作为测试集。下面的代码是将数据集随机分为训练集和测试集的过程:

def SplitData(data, M, k, seed):
    test = []
    train = []
    random.seed(seed)
    for user, item in data:
        if random.randint(0, M) == k:
            test.append([user, item])
        else:
            train.append([user,item])
    return train, test

这是为了进行M次实验,防止过拟合。
对用户u推荐N个物品,公式见课本42页,代码如下:

def Recall(train, test, N):
    hit = 0
    all = 0
    for user in train.keys():
        tu = test[user]
        rank = GetRecommendation(user, N)
        for item, pui in rank:
            if item in tu:
                hit += 1
        all += len(tu)
    return hit / (all * 1.0)

def Precision(train, test, N):
    hit = 0
    all = 0
    for user in train.keys():
        tu = test[user]
        rank = GetRecommendation(user, N)
        for item, pui in rank:
            if item in tu:
                hit += 1
        all += N
    return hit/(all*1.0)

覆盖率表示最终的推荐列表中包含多大比例的物品,如果所有物品都被推荐给至少一个用户,那么覆盖率就是100%:

def Coverage(train, test, N):
    recommend_items = set()
    all_items = set()
    for user in train.keys():
        for item in train[user].keys():
            all_item.add(items)
        rank = GetRecommendation(user, N)
        for item, pui in rank:
            recommend_items.add(item)
    return len(recommend_items)/(len(all_items)*1.0)

最后,我们还需要评测推荐的新颖度,这里用推荐列表中物品的平均流行度度量推荐结果的新颖度:

def Popularity(train, test, N):
    item_popularity = dict()
    for user, items in train.items():
        for item in items.keys():
            if item not in item_popularity:
                item_popularity[item]=0
        net = 0
        n = 0
        for user in train.keys():
            rank = GetRecommendataion(user, N)
            for item, pui in rank:
                ret += math.log(1+item_popularity[items])
                n += 1
        ret /= n*1.0
        return ret

这里,平均流行度对每个物品的流行度取对数,这是因为物品的流行度分布满足长尾分布,在取对数后,流行度的平均值更加稳定。

基于领域的算法分类两大类,一类是基于用户的协同过滤算法,另一类是基于物品的协同过滤算法。

基于用户的协同过滤算法分为两个步骤:
(1)找到和目标用户兴趣相似的用户集合;
(2)找到这个集合中的用户喜好的,且目标用户没有听说过的物品推荐给用户。
可以使用余弦公式计算相似度,代码如下:

def UserSimilarity(train):
    W = dict()
    for u in train.keys():
        for v in train.keys():
            if u == v:
                continue
            W[u][v] = len(train[u] & train[v])
            W[u][v] /= math.sqrt(len(train[u])*len(train[v])*1.0)
    return W

但是这种算法时间复杂度是O(|U|*|U|),事实上很多用户相互之间并没有对同样的物品产生过行为。我们可以首先计算出对同样的物品产生过购买行为的进行计算。
可以首先建立物品到用户的倒排表,对于每个物品都保存对其产生过行为的用户列表。使用系数矩阵C[u][v],假设用户u和用户v同时属于倒排表中K个物品对应的用户列表,就有其等于K:

def UserSimilarity(train):
    #从物品列表建立倒排表
    item_users = dict()
    for u, items in train.items():
        for i in items.keys():
            if i not in item_users:
                item_users[i] = set()
            item_users[i].add(u)
        
    #计算用户间共同评价过的物品
    C = dict()
    N = dict()
    for i, users in item_users.items():
        for u in users():
            N[u] += 1
            for v in users:
                if u == v:
                    continue
                c[u][v] += 1
                
    #计算最终相似度矩阵
    W = dict()
    for u, related_users in C.items():
        for v, cuv in related_users.items():
            W[u][v] = cuv/math.sqrt(N[u]*N[v])
    return W

UserCF算法公式见课本47页,代码如下:

def Recommend(user, train, W):
    rank = dict()
    interacted_items = train[user]
    for v, wuv in sorted(W[u],items, key=itemgetter(1), reverse=True)[0:K]:
        for i, rvi in train[v].items():
            if i in interacted_items:
                #我们应该在继续之前过滤相关联的物品和用户
            rank[i] += wuv * rvi
    return rank

我们尝试从moives的ml-1m数据中读取数据:

import pandas as pd  
 
unames = ['user_id', 'gender', 'age', 'occupation', 'zip']  
users = pd.read_table('ml-1m/users.dat', sep='::', header=None, names=unames)#p数167  
 
rnames = ['user_id', 'movie_id', 'rating', 'timestamp']  
ratings = pd.read_table('ml-1m/ratings.dat', sep='::', header=None, names=rnames)  
 
mnames = ['movie_id', 'title', 'genres']  
movies = pd.read_table('ml-1m/movies.dat', sep='::', header=None, names=mnames) 

测试一下:

In [5]: users[:5]
Out[5]: 
   user_id gender  age  occupation    zip
0        1      F    1          10  48067
1        2      M   56          16  70072
2        3      M   25          15  55117
3        4      M   45           7  02460
4        5      M   25          20  55455

In [6]: ratings[:5]
Out[6]: 
   user_id  movie_id  rating  timestamp
0        1      1193       5  978300760
1        1       661       3  978302109
2        1       914       3  978301968
3        1      3408       4  978300275
4        1      2355       5  978824291

In [7]: movies[:5]
Out[7]: 
   movie_id                               title                        genres
0         1                    Toy Story (1995)   Animation|Children's|Comedy
1         2                      Jumanji (1995)  Adventure|Children's|Fantasy
2         3             Grumpier Old Men (1995)                Comedy|Romance
3         4            Waiting to Exhale (1995)                  Comedy|Drama
4         5  Father of the Bride Part II (1995)                        Comedy

MostPopular算法则是按照物品的流行度给用户推荐他没有产生过行为的物品。其准确率和召回率远远高于Random算法,但是覆盖率低。
K越大,流行度越高,覆盖率越低。

两个用户对冷门物品采取过同样的行为更能说明他们的兴趣的相似度,新公式(P.49)惩罚了用户u和用户v的共同热门物品的相似度影响。

def UserSimilarity(train):
    #建立物品—_用户的倒排表
    item_users = dict()
    for u, items in trains.items():
        for i in items_keys():
            if i not in item_users:
                item_users[i] = set()
            item_users[i].add(u)
     
    #计算用户间,共同评分过的物品
    C = dict()
    N = dict()
    for i, users in item_users.items():
        for u in  users:
            N[u] +=1
            for v in users:
                 if u == v:
                     continue
                 C[u][v] += 1/math.log(1+len(users))
                 
    #计算最终的相似度矩阵W
    W = dict()
    for u, related_users in C.items():
        for v, cuv in elated_users.items():
            W[u][v] = cuv/math.sqrt(N[u]*N[v])
    return W

基于物品的协同过滤算法主要分为两步:
(1)计算物品之间的相似度;
(2)根据物品的相似度和用户的历史行为给用户生成推荐列表。
物品相似度公式要惩罚热门物品。

def ItemSimilarity(train):
    #统计对同一个物品评分过的用户
    C = dict()
    N = dict()
    for u, items in train.items():
        for u in users:
            N[i] += 1
            for j in users:
                if i == j:
                    continue
                C[i][j] += 1
    
    #计算最终相似度矩阵
    W = dict()
    for i, related_items in C.items():
        for j, cij in related_items.items():
            W[u][v] = cij / math.sqrt(N[i]*N[j])
    return W

在得到物品之间的相似度吼,ItemCF通过公式(55)计算用户对一个物品的相似度,代码如下:

def Recommendation(train, user_id, W, K):
    rank = dict()
    ru = train[user_id]
    for i, pi in ru.items():
        for j, wj in sorted(W[i].items(), key = itemgetter(1), reverse = True)[0:K]:
            if j in ru:
                continue
            rank[j] += pi*wj
    return rank

ItemCF的一个优势就是可以提供推荐解释,如下代码实现了解释的过程:

def Recommendation(train, user_id, W, K):
    rank = dict()
    ru = train[user_id]
    for i,pi in ru.items():
        for j, wj in sorted(W[i].items(), key=itemgetter(1), reverse=True)[0:K]:
            if j in ru:
                continue
            rank[j].weight += pi*wj
            rank[j].reason[i]=pi*wj
    return rank

ItemCF的推荐结果精度也不是和K成正相关胡总负相关的。
随着K的提高,流行度逐渐提高,但当增加到一定程度以后,流行度不在有明显变化。
K的增加会降低系统的覆盖率。
用户活跃度对物品相似度的邮箱
购买较多图书的用户对相似度的贡献应该远远小于一个只买了十几本自己喜欢的书的文学青年。John S.Breese在论文中提出了IUF(Inverse User Frequence),见课本57页:

def ItemSimilarity(train):
    #计算对同一个物品评分过的用户
    C = dict()
    N = dict()
    for u, items in train.items():
        for i in users:
            N[i] += 1
            for j in users:
                if i == j:
                    continue
                C[i][j] += 1/math.log(1+len(items)*1.0)
                
    #计算最终相似度矩阵
    W = dict()
    for i, related_items in C.items():
        for j, cij in related_items.items():
            W[u][v] = cij/math.sqrt(N[i]*N[j])
    return W

如果将相似度矩阵按最大值归一化,可以提高推荐的准确度。
UserCF的推荐结果着重于反映和用户兴趣相似的小群体的热点,而ItemCF的推荐结果着重于维系用户的历史兴趣。

两种算法优缺点比较

两个不同领域的最热门物品往往具有比较高的相似度。
隐语义模型(LFM,latent factor model)该算法最早用于在文本挖掘中,用于找到文本的隐含语义。编辑很难决定一个物品在某一个分类中的权重,但隐含语义分析技术可以通过统计用户行为决定物品在每个类中的权重,如果喜欢某个类的用户都会喜欢某个维度,那么LFM给出的类也是相同的维度。
对于每个用户,要保证正负样本的平衡。
对于每个用户采样负样本时,要选取那些很热门,而用户却没有行为的物品。
一般认为,很热门而用户却没有行为更加代表用户对这个物品不感兴趣。
下面的Python代码实现了负样本采样过程:

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(itens_pool)-1)]
        if item in ret:
            continue
        ret[item] = 0
          n += 1
          if n > len(items):
              break
    return ret

上述代码根据物品的流行度采样出了那些热门的、但用户却没有过行为的物品。
要最小化损失函数(68页),可以利用随机梯度下降算法,其中alpha是学习速率,需要反复试验得到:

def LatentFactorModel(user_items, F, N, alpha, lamda):
    [P, Q] = InitModel(user_items, F)
    for step in range(0, N):
        for user, items in user_items.items():
            samples = RandSelectNegativeSample(items)
            for items, rui in samples.items():
                eui = rui - Predict(user, items)
                for f in range(0, F):
                    P[user][f] += alpha*(eui*Q[item][f]- lamda*P[user][f])
                    Q[item][f] += alpha*(eui*P[user][f]- lamda*Q[item][f])
        alpha *= 0.9
        
def Recommend(user, P, Q):
    rank = dict()
    for f, puf in range(0, len(P[user])):
        for i, qfi in Q[f]:
            if i not in rank:
                rank[i] = Predict(user, i)
     return rank 

实际我们发现LFM算法中,重要的参数有4个:
隐特征的个数F
学习速率alpha
正则化参数lambda
负样本/正样本比例ratio
ratio参数控制了推荐算法发掘长尾的能力
数据非常稀疏的时候,LFM的性能会明显下降

LFM和基于领域的方法的比较:
理论基础:LFM是一种学习方法,而基于领域的方法更多的是一种基于统计的方法
离线计算的空间复杂度:基于领域的方法需要维护一张离线的相关表,将会占据很大的内存。而在LFM建模过程中,则会很大节省离线计算的内存
离线时间复杂度:总体上没有质的区别
在线实时推荐:LFM不适用于物品数非常庞大的系统
推荐解释:ItemCF算法支持很好的推荐解释,而LFM无法提供这样的解释

基于图的模型
两个相关性高的一对顶点具有以下特征:
两个顶点之间有很多路径相连
连接两个顶点之间的路径长度都很短
连接两个顶点之间的路径不会经过出度比较大的顶点
下面介绍一种随机游走的PersonalRank算法:
假设要给用户进行个性化推荐,可以从用户u对应的节点v0开始在用户物品二分图上进行随机游走。游走到任何一个节点时,首先按照概率α决定是继续游走,还是停止这次游走并从vu节点开始重新游走。如果决定继续游走,那么就从当前节点指向的节点中按照均匀分布随机游走选择一个节点作为游走下次经过的节点。这样经过很多次的游走后,每个物品节点被访问到的概率会收敛到一个数。最终的推荐列表中物品的权重就是物品节点的访问概率。公式见75页。

def PersonRank(G, alpha, root):
    rank = dict()
    rank = {x:0 for x in G.keys()}
    rank[root] = 1
    for k in range(20):
        tmp = {x:0 for x in G.keys()}
        for i, ri in G.items():
            for j wij in ri.items():
                if j not in temp:
                    tmp[j] = 0
                tmp[j] += 0.6 * rank[i] / (1.0*len(ri))
                if j == root:
                    tmp[j] += 1 - alpha
        rank = tmp
    return rank

虽然PR算法可以通过随机游走进行比较好的理论解释但该算法需要在整个用户物品二分图上进行迭代,时间复杂度非常高,不适用于实时推荐。
对于这种麻烦,有两种解决方法,一是减少迭代次数,二 从矩阵论出发,重新设计算法,见课本76页。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,265评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,274评论 1 288
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,087评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,479评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,782评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,218评论 1 207
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,594评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,316评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,955评论 1 237
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,274评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,803评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,177评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,732评论 3 229
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,953评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,687评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,263评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,189评论 2 258

推荐阅读更多精彩内容