基于协同过滤的推荐算法
本推荐系统采用中等大小的MovieLens数据集,该数据集包含6000多用户对4000多部电影的100万条评分。该数据集是一个评分数据集,用户可以给电影评5个不同等级的分数(1~5分)。本篇文章主要研究隐反馈数据集中的TopN推荐问题,因此将会忽略数据集中的评分记录。也就是说,TopN推荐的任务是预测用户会不会对某部电影评分,而不是预测用户在准备对某部电影评分的前提下对电影评多少分。
实验设计——训练集M折交叉验证
from numpy import random
def split_data(data, M, k, seed):
"""
切割训练集,防止过拟合
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
评测指标
准确率/召回率
对用户$u$推荐$N$个物品,记作$R(u)$,用户$u$在测试集上喜欢的物品集合为$T(u)$,可以使用准确率/召回率评测推荐算法的精度。
准确率公式为:
$$
\text{Precision}=\frac{\sum{u\in{U}}|R(u)\bigcap{T(u)}|}{\sum{u\in{U}}|R(u)|}
$$
其中$R(u)$是用户在训练集上的行为给用户作出的推荐列表。
召回率公式为:
$$
\text{Recall}=\frac{\sum{u\in{U}}|R(u)\bigcap{T(u)}|}{\sum{u\in{U}}|T(u)|}
$$
其中$T(u)$是用户在测试集上的行为给用户作出的推荐列表。
准确率描述最终的推荐列表中有多少比例是发生过的用户-物品评分记录;召回率描述有多少比例的用户-物品评分记录包含在最终的推荐列表中。
def recall_(train, test, N):
"""计算召回率"""
hit = 0
all_ = 0
for user in train.keys():
tu = test[user]
rank = get_recommendation(user, N)
for item, pui in rank:
if item in tu:
hit += 1
all_ += len(tu)
return hit/(all_*1.)
def precision(train, test, N):
"""计算准确率"""
hit = 0
all_ = 0
for user in train.keys():
tu = test[user]
rank = get_recommendation(user, N)
for item, pui in rank:
if item in tu:
hit += 1
all_ += N
return hit/(all_*1.)
覆盖率
覆盖率反映了推荐算法发掘长尾的能力,覆盖率越高,说明推荐算法越能够将长尾中的物品推荐给用户。
$$
\text{Coverate}=\frac{|\bigcup_{u\in{U}}R(u)|}{|I|}
$$
def coverage(train, test, N):
"""计算覆盖率"""
recommend_items = set()
all_items = set()
for user in train.keys():
for item in train[user].keys():
all_items.add(item)
rank = get_recommendation(user, N)
for item, pui in rank:
recommend_items.add(item)
return len(recommend_items)/len((all_items)*1.)
新颖度
如果推荐出的物品都很热门,说明推荐的新颖度较低;否则说明推荐结果比较新颖。
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
item_popularity[item] += 1
ret = 0
n = 0
for user in train.keys():
rank = get_recommendation(user, N)
for item, pui in rank:
# 物品的流行度分布满足长尾分布,对流行度取对数后,流行度的平均值更稳定
ret += math.log(1+item_popularity[item])
n += 1
ret /= n*1.
return ret
基于领域的算法
基于领域的算法分为两大类,一类是基于用户的协同过滤算法,另一类是基于物品的协同过滤算法。
基于用户的协同过滤算法
在一个基于用户的协同过滤算法的在线个性化推荐系统中,当一个用户A需要个性化推荐时,一般需要以下两个步骤:
- 先找到和他有相似兴趣的其他用户(找到和目标用户兴趣相似的用户集合)
- 把其他用户喜欢的且用户A没有听说过的物品推荐给用户A(找到这个集合中的用户喜欢的且目标用户没有听说过的物品推荐给目标用户)
对于步骤1,我们需要计算两个用户的兴趣相似程度,协同过滤算法主要利用用户的行为的相似度计算用户间兴趣的相似度,可以使用以下两种方法计算用户间的兴趣相似度(其中$u$和$v$表示不同的用户,$N(u)$表示用户$u$曾经拥有正反馈的物品集合;$N(v)$表示用户$v$曾经拥有正反馈的物品集合。):
-
Jaccard公式:
$$
w_{uv}=\frac{|N(u)\bigcap{N(v)}|}{|N(u)\bigcup{N(v)}|}
$$ -
余弦相似度:
$$
w_{uv}=\frac{|N(u)\bigcap{N(v)}|}{\sqrt{|N(u)||{N(v)}|}}
$$
UserCF推荐算法
假设上图为某个网站用户行为记录,UserCF(基于用户的协同过滤算法)时使用余弦相似度计算用户A和用户B的兴趣相似度为:
$$
w_{AB} = \frac{|{a,b,d}\bigcap{{a,c}|}}{\sqrt{|{a,b,d}||{a,c}|}} = \frac{1}{\sqrt{6}}
$$
同理我们可以计算出用户A和用户C、D的相似度为:
$$
\begin{aligned}
& w{AC} = \frac{|{a,b,d}\bigcap{{b,e}|}}{\sqrt{|{a,b,d}||{b,e}|}} = \frac{1}{\sqrt{6}} \
& w{AD} = \frac{|{a,b,d}\bigcap{{c,d,e}|}}{\sqrt{|{a,b,d}||{c,d,e}|}} = \frac{1}{\sqrt{9}} = \frac{1}{3}
\end{aligned}
$$
def user_similarity(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.)
return W
对于上面的余弦相似度计算,有着很大的计算开销。因为很多用户之间并没有对同样的物品产生过行为,即有时候$|N(u)\bigcap{N(v)}|=0$.因此我们可以考虑首先计算$|N(u)\bigcap{N(v)}|\neq0$的用户对$(u,v)$,然后再对这些用户进行余弦相似度计算,即下面改进的余弦相似度计算。
改进的余弦相似度计算,需要以下三个步骤:
- 构建物品到用户的倒排表,即键值对为{物品:对该物品产生行为的用户列表}。
- 令稀疏矩阵$C[u][v]=|N(u)\bigcap{N(v)}|$。假设用户$u$和用户$v$同时属于倒排表中$K$个物品对应的用户列表,则会有$C[u][v]=K$。因此可以扫描倒排表中每个物品对应你的用户列表,将用户表中的两两用户对应的$C[u][v]$加1.
- 计算$|N(u)\bigcap{N(v)}|\neq0$用户$u$和用户$v$之间的余弦相似度。
def user_similarity(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
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
File "", line 19
C[u][v] = +=1
^
SyntaxError: invalid syntax
通过上述得到用户之间的兴趣相似度之后,UserCF算法会给用户推荐和他兴趣最相似的K个用户喜欢的物品。并且可以通过下述公式计算用户$u$对物品$i$的感兴趣程度:
$$
p(u,i) = \sum{v\in{S}(u,K)\bigcap{N(i)}}w{uv}r{vi}
$$
其中$S(u,K)$包含和用户$u$兴趣最接近的$K$个用户,$N(i)$是对物品$i$有过行为的用户集合,$w{uv}$是用户$u$和用户$v$的兴趣相似度,$r{vi}$代表用户$v$对物品$i$的兴趣,因为使用的是单一行为的隐反馈数据,因此所有的$r{vi}=1$。
def recommend(user, train, W):
"""实现UserCF算法"""
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
User-IIF推荐算法
如果两个用户都买过《新华字典》,并不能证明两个人兴趣相似,因为中国绝大多数人都买过《新华字典》,但是两个用户都买过冷门商品,如《机器学习》,则可以认为两个用户的兴趣相似。因此我们可以通过如下公式计算用户间的兴趣相似度:
$$
w{uv}=\frac{\sum{i\in{N(u)}\bigcap{N(v)}}\frac{1}{\log{(1+|N(i)|)}}}{\sqrt{|N(u)||N(v)|}}
$$
其中$\frac{1}{\log{(1+|N(i)|)}}$惩罚了用户$u$和用户$v$共同兴趣列表中热门物品对他们相似度的影响。
基于上述用户间兴趣相似度则可以改造UserCF算法为User-IIF算法。
def user_similarity(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/math.log(1+len(users))
# 计算最后的余弦相似度矩阵W
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
基于物品的协同过滤算法
基于物品的协同过滤算法的推荐系统主要分为两步:
- 计算物品之间的相似度
- 根据物品的相似度和用户的历史行为给用户生成推荐列表
由于基于物品的协同过滤算法的思想是——购买了该商品的用户也会经常购买其他的商品。根据这句话,可以用下面的公式定义物品的相似度:
$$
w_{ij} = \frac{|N(i)\bigcap{N(j)|}}{|N(j)|}
$$
其中$N(i)$是喜欢物品$i$的用户数,$|N(i)\bigcap{N(j)|}$是同时喜欢物品$i$和物品$j$的用户数。
对于上述简单的公式,如果物品$j$很热门,则该公式会造成任何物品都会和热门的物品有很大的相似度,这对于致力于挖掘长尾信息的推荐系统来说不是一个好的特性,因此可以把上述公式改造成:
$$
w_{ij} = \frac{|N(i)\bigcap{N(j)}|}{\sqrt{|N(i)||N(j)|}}
$$
其中$\sqrt{|N(i)||N(j)|}$相当于惩罚了物品$j$的权重,减轻了热门物品会和很多物品相似的可能性。
ItemCF算法
ItemCF算法的思想是,假设每个用户的兴趣都局限在某几个方面。如果两个物品属于一个用户的兴趣列表,那么这两个物品可能就属于有限的几个领域;如果两个物品属于很多用户的兴趣列表,则它们可能就属于同一个领域,因而有很大的相似度。
ItemCF算法类似于UserCF算法,也需要建立用户-物品倒排表,建立该表流程如下:
- 先对每个用户建立一个包含他喜欢物品的列表
- 对于每个用户,将他物品列表中的物品两两在共现矩阵C中加1
import math
def item_similarity(train):
"""计算物品间的相似度"""
# 计算用户共同拥有的物品
C = dict()
N = dict()
for u, items in train.items():
for i in items:
try:
N[i] += 1
except:
N[i] = 1
for j in items:
if i == j:
continue
try:
try:
if not isinstance(C[i], dict):
C[i] = dict()
except:
C[i] = dict()
C[i][j] += 1
except:
C[i][j] = 1
# 计算最后的相似矩阵W
W = dict()
for i, related_items in C.items():
for j, cij in related_items.items():
W[i] = dict()
W[i][j] = cij / math.sqrt(N[i]*N[j])
return W
train = {'A': ['a', 'b', 'd'], 'B': ['b', 'c', 'e'], 'C': [
'c', 'd'], 'd': ['b', 'c', 'd'], 'e': ['a', 'd']}
W = item_similarity(train)