通过算法自动发掘用户行为数据,从用户的行为中推测出用户的兴趣,从而给用户推荐满足他们兴趣的物品。
基于用户行为分析的推荐算法是个性化推荐系统的重要算法,学术界一般将这种类型的算法称为协同过滤算法。
用户行为在个性化推荐系统中一般分两种:显性反馈行为(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页。