姗姗来迟的第二篇博客,最近在了解有关推荐系统方面的基本知识和算法,先总结其中一类经典常用的算法——协同过滤法。网上已有很多介绍其原理的好文章,所以本文用较多篇幅来写一些自身对算法实现的理解和疑惑,希望有经验的小伙伴能够解惑,感激不尽!
写的比较仓促,如有描述或理解错误,敬请指出,望共同进步~
先介绍下推荐系统中经常出现的两个术语:User 和 Item,即用户和物品,这里的物品可以是书籍、电影、新闻、视频等。
什么情况下需要推荐系统?——信息过载且用户没有明确的需求。简单理解信息过载就是Item数量非常多,用户一时半会儿不知道想要什么,这时候推荐系统就可以主动向用户提供他们可能感兴趣的物品。所以在做推荐系统前需要了解两件事儿:
(1)平台上Item的数量是否过多;如果数量不大,可以用分类目录的形式对Item进行分门别类,更方便用户找到适合自己的Item.
(2)用户每次使用平台时,自身的需求是否明确.
以抖音app为例子,里面的短视频千千万,用户一般并不清楚自己具体想看什么,推荐就成了留住用户的最好方式,所以抖音的主界面就是个推荐场景(其实还有个搜索界面);还有淘宝app和云音乐app等,里面的推荐场景随处可见。
回归正题,推荐系统一般包含两个算法部分:召回阶段(recall)和 排序阶段(rank)。先说排序阶段的作用,也是传统机器学习、深度学习在推荐系统中发挥威力的地方,它们可以预测用户对物品的点击率、转化率等(类似广告中CTR模型),再以特定的规则进行排序后展示给用户,更符合规则的物品做优先展示。但是不可能把所有的物品一次性喂入排序模型做预测排序(数量太多,算完用户都跑了),所以召回阶段的作用就是把所有物品做初步的筛选形成一个物品候选集,再进入排序阶段。
排序阶段里往往都是高大上的模型,似乎直接决定推荐结果,其实召回阶段产生的Item候选集也会对最终推荐结果产生很大影响。比如小明购买过一个水杯,然后召回阶段产生的候选集几乎由杯子这一类的物品组成,导致排序阶段输出的Top-N物品全是五花八门的杯子。。。显然这样的召回策略缺乏多样性,使得排序模型无论多精确都无法摆脱只推荐杯子的结局。
本文重点介绍召回阶段最常见的一类算法——协同过滤法(Collaborative Filtering),做推荐的小伙伴们都应该很熟悉吧。相比ML、DL这些模型算法,CF几乎没有什么深奥的数学原理,注重的是背后原理和思想。
协同过滤法属于召回阶段的算法,简单有效的特点使其仍未过时,现有的各大推荐场景中都能看到这类算法的影子。基本思想就是基于用户或物品的相似度做推荐,下面就介绍其中一种。
基于用户的协同过滤法,基本思想是寻找用户A的相似用户B,把用户B接触过的物品b推荐给用户A。借鉴《推荐系统实践》一书中的说法,该算法主要包括两个步骤:
(1)找到和目标用户A兴趣相似的用户集合;
(2)找到这个集合中的用户喜欢的,且目标用户A没有发生行为的物品推荐给目标用户A。
如何衡量用户的相似度呢?首先确定计算相似度所需的用户数据来源,我第一反应就是用户的画像数据(性别、身高、收入等),但是仔细想想画像数据并不能完全反映一个用户在平台里的兴趣偏好。相比之下,用户在平台上的行为数据更能体现其在平台中的兴趣偏好,也更适用于计算用户相似度,比如抖音中的点赞、页面停留时长;淘宝中的浏览、加购、购买、评分等行为数据。
其次确定采用何种相似度指标,以下列举几个常见的相似度计算公式:
(一)对于停留时长、评分等连续型行为数据
假设用户a和用户b对所有Item的评分(停留时长)向量分别为 A 和 B,其中 A = ( a 1 , a 2 , . . . , a n ) A=(a_{1},a_{2},...,a_{n}) A=(a1,a2,...,an), B = ( b 1 , b 2 , . . . , b n ) B=(b_{1},b_{2},...,b_{n}) B=(b1,b2,...,bn),则有:
余弦相似度
c o s ⟨ A , B ⟩ = A ⋅ B ∣ ∣ A ∣ ∣ × ∣ ∣ B ∣ ∣ = ∑ i = 1 n a i b i ∑ i = 1 n ( a i ) 2 × ∑ i = 1 n ( b i ) 2 cos\left \langle A,B \right \rangle={\frac {A \cdot B} {||A||\times||B||}}={\frac {\sum_{i=1}^{n}a_{i} b_{i}} {\sqrt {\sum_{i=1}^{n}\left (a_{i} \right )^2} \times \sqrt{\sum_{i=1}^{n}\left (b_{i} \right )^2}}} cos⟨A,B⟩=∣∣A∣∣×∣∣B∣∣A⋅B=∑i=1n(ai)2×∑i=1n(bi)2∑i=1naibi
最初用于文本相似度的计算。从二维和三维的角度来看,该指标就是两个向量间夹角的余弦值,取值范围为[0,1],越接近1说明两个向量越相似。
公式中分母做了类似标准化(去量纲)的操作,图中可看出该指标以向量间的夹角来衡量相似度,所以忽略了向量本身的模长。
欧式距离(Euclidean)
d ( A , B ) = ( a 1 − b 1 ) 2 + ( a 2 − b 2 ) 2 + . . . + ( a n − b n ) 2 = ( ∑ i = 1 n ∣ a i − b i ∣ 2 ) 1 / 2 d(A,B)=\sqrt {(a_{1}-b_{1})^2+(a_{2}-b_{2})^2+...+(a_{n}-b_{n})^2}=(\sum_{i=1}^{n} {|a_{i}-b_{i}|^2})^{1/2} d(A,B)=(a1−b1)2+(a2−b2)2+...+(an−bn)2=(i=1∑n∣ai−bi∣2)1/2
欧氏距离是非常直观的距离指标,属于闵可夫斯基距离(Minkowski)的一种特殊形式,类似的还有曼哈顿距离(Manhattan)、切比雪夫距离(Chebyshev),这些距离指标在K-means、KNN等算法中一般都有详细介绍。
可将欧氏距离做变型: 1 1 + d ( A , B ) \frac {1} {1+d(A,B)} 1+d(A,B)1,用该值衡量A,B向量的相似度更合适。相比余弦相似度,欧氏距离从向量各个分量间的差值体现两者距离(差异)。
Pearson相关系数
c o r ( A , B ) = E [ ( A − μ A ) ( B − μ B ) ] σ A ⋅ σ B = 1 n ∑ i = 1 n ( a i − a ˉ ) ( b i − b ˉ ) 1 n ∑ i = 1 n ( a i − a ˉ ) 2 × 1 n ∑ i = 1 n ( b i − b ˉ ) 2 cor\left( A,B\right)=\frac {E[(A-\mu_{A})(B-\mu_{B})]} {\sigma_{A}\cdot\sigma_{B}}=\frac {\frac {1} {n}\sum_{i=1}^{n}{(a_{i}-\bar{a})(b_{i}-\bar{b})}} {\sqrt{\frac {1} {n}\sum_{i=1}^{n}{(a_{i}-\bar{a})^2}}\times\sqrt{\frac {1} {n}\sum_{i=1}^{n}{(b_{i}-\bar{b})^2}}} cor(A,B)=σA⋅σBE[(A−μA)(B−μB)]=n1∑i=1n(ai−aˉ)2×n1∑i=1n(bi−bˉ)2n1∑i=1n(ai−aˉ)(bi−bˉ)
其中 a ˉ = 1 n ∑ i = 1 n a i \bar{a}=\frac {1} {n}\sum_{i=1}^{n}{a_{i}} aˉ=n1∑i=1nai, b ˉ = 1 n ∑ i = 1 n b i \bar{b}=\frac {1} {n}\sum_{i=1}^{n}{b_{i}} bˉ=n1∑i=1nbi 。
这是统计学中常用的相关性指标,用于描述两个变量间的线性相关性。在余弦相似度的基础上,对分子分母均做中心化(去均值)处理,所以也被称为调整余弦相似度。
个人对采用这个指标有些疑惑:首先相关系数取值为[-1,1],如果 c o r ( A , B ) < 0 cor(A,B)<0 cor(A,B)<0,说明用户A和用户B对item的某行为数据呈负相关关系,假设该行为是浏览时长,然后把用户B浏览时长短的item推给用户A?还是只截取与用户A相关系数为正的用户,作为推荐给A的相似用户集?
(二)对于购买、浏览等离散型行为数据
假设用户A和用户B的购买(浏览等)物品集合分别为N(a)和N(b),则有:
Jaccard相似度
w A B = ∣ N ( a ) ∩ N ( b ) ∣ ∣ N ( a ) ∪ N ( b ) ∣ w_{AB}=\frac {|N(a) \cap N(b)|} {|N(a) \cup N(b)|} wAB=∣N(a)∪N(b)∣∣N(a)∩N(b)∣
余弦相似度
w A B = ∣ N ( a ) ∩ N ( b ) ∣ ∣ N ( a ) ∣ ∣ N ( b ) ∣ w_{AB}=\frac {|N(a) \cap N(b)|} {\sqrt{|N(a)||N(b)|}} wAB=∣N(a)∣∣N(b)∣∣N(a)∩N(b)∣
以上两个指标中,冷门物品和热门物品对用户相似度的权重是相同的,但实际上两个用户对冷门物品的行为更能说明他们兴趣的相似度——《推荐系统实践》,所以John S. Breese提出了一种改进的相似度指标。
改进的相似度
w A B = ∑ i ∈ N ( a ) ∩ N ( b ) 1 l o g ( 1 + ∣ N ( i ) ∣ ) ∣ N ( a ) ∣ ∣ N ( b ) ∣ w_{AB}=\frac {\sum_{i \in N(a)\cap N(b)}^{} \frac {1} {log\left (1+\left |N(i)\right | \right)}} {\sqrt {|N(a)||N(b)|}} wAB=∣N(a)∣∣N(b)∣∑i∈N(a)∩N(b)log(1+∣N(i)∣)1
其中 N ( i ) N(i) N(i)为对物品i有过购买(浏览等)行为的用户集, 1 l o g ( 1 + ∣ N ( i ) ∣ ) \frac {1} {log(1+|N(i)|)} log(1+∣N(i)∣)1表示物品i对相似度的影响,不难发现物品的购买人群越多(越热门),其对相似度的影响越小,反之冷门物品的影响越大;所以改进后的指标更能刻画用户间的个人偏好相似度。
本节总结和思考
1、一般平台埋点数据返回的行为字段有很多,可以计算用户间多个行为的相似度 w 1 、 w 2 . . . w N w_{1}、w_{2}...w_{N} w1、w2...wN,比如购买行为的相似度、浏览时长的相似度等等,那么如何整合这些相似度来得到最终的候选集呢?
(a)利用每个相似度指标 w i , i ∈ 1 , 2 , . . . N w_{i},i \in {1,2,...N} wi,i∈1,2,...N,产生 N N N个候选集,取并集作为召回阶段最终输出的候选集;
(b)根据实际业务对每个相似度赋予合适的权重 r i ∈ [ 0 , 1 ] r_{i}\in [0,1] ri∈[0,1],比如认为购买行为比浏览行为更反应出用户的兴趣偏好,那么用购买行为数据算出的相似度指标权重应该更大,再通过加权平均的方式 w = ∑ i = 1 n r i w i w=\sum_{i=1}^{n}{r_{i}w_{i}} w=∑i=1nriwi作为最终用户间的相似度,然后再得到输出的候选集.
【第一种得到的候选集感觉更偏重多样性,第二种做法中如果各权重定的好,找相似用户似乎更准确;所以不知道用哪种方式整合两种相似度,或者说实际应用中还有更好的方案.】
2、各类指标各有优劣,可根据对具体业务的理解进行取舍,如果不同指标对最终的推荐结果影响不大,优先采取计算时间短的。(对于高维稀疏向量,余弦相似度的计算速度应该比较快.)
3、实际中离散型的行为数据应该更常见吧,业务平台初期可能没有太多的行为数据埋点,但肯定有最基本的交易日志数据(买卖交易平台),就可以从购买行为来计算用户的相似度,后期再增加新的行为埋点数据。
4、冷启动问题,计算相似度的前提是用户发生过行为,所以对于新用户而言,该算法暂时会失效。列举两种常见的解决办法:
(a)直接向新用户推送近期的热门物品。毕竟受大部分用户喜爱的物品能大概率激发新用户的兴趣,或者说被新用户厌恶的概率很低,但显然缺乏个性化;
(b)当新用户第一次进入平台时,让其选择自身感兴趣的Item类别。我记得CSDN就有这个功能,给出了Java开发、人工智能等几个选项,然后根据新用户的选项做博客推送。
【也有很多平台会接入第三方的数据库得到新用户在其他平台(如vx、微博等)的画像数据,用于解决冷启动问题。】
推荐系统需要具备一定的实时性,即用户进入业务平台后会立即给出对应的推荐结果,或者推荐结果随着用户的行为时时更新。比如在淘宝app中用户刚浏览过某物品后,类似的商品下一秒就出现在推荐列表中,用户不仅觉得体验很棒,还会惊叹这个系统好 “ 智能 ”的样子 。
为了尽量实现上述流程的实时性,一般会把算法中涉及到的复杂对象提前计算好,需要时再做调用可大大降低在线的推荐耗时。
用户物品倒排表
当给定一个用户id时,如何快速获取其近期发生过行为的Item集合?每次都用SQL语句获取感觉效率不高,所以需提前离线构建一个用户物品倒排表。
用户相似矩阵
输入用户id,都要获取该用户与所有其他用户的相似度,为避免大量重复的计算,可以离线计算好用户间的相似度矩阵,并设置合适的时间间隔进行更新。
步骤:
离线部分:
(1)构建用户物品倒排表并进行存储;
(2)计算好用户的相似度矩阵并进行存储;
在线部分:
输入:用户A的唯一id
(1)根据相似度矩阵找出用户A对应的其他用户相似度,排序并截取前k个相似用户id;
(2)利用用户物品倒排表找出k个相似用户的物品集;
(3)对(2)中得到的物品集做进一步过滤,如删除A已购的物品、已下架的物品等,得到最终候选集a;
输出:物品候选集a
数据说明
代码所用数据是随机产生的成交数据,字段包括用户id(user_id)和物品id(item_id),所以只有一种行为数据。数据包含5000+个不同用户,2100+个不同物品,成交记录一共有8w条。【数据有点简洁,毕竟本文重点是了解算法应用的细节…】
用户物品倒排表(user_item.py)
这里我用字典(dict)来储存用户物品倒排表,形如:{‘user 1’:[‘item 1’,‘item2’,…],‘user 2’:[‘item 2’,‘item 3’,…],…} 。
import pandas as pd
import numpy as np
'''
离线计算用户物品倒排表 user_item
'''
data = pd.read_csv('./data.csv')
user_item = dict() # 创建倒排表
for i in set(data.user_id.tolist()):
user_item[i] = data[data['userid'] == i]['itemid'].tolist()
用户相似矩阵(user_similar.py)
代码里用了离散行为数据情况下的余弦相似度公式,以下两类情况把相似度直接标0:(1)多数用户间的已购物品无交集;(2)用户间的相似度为1,说明两者的已购物品集完全相同,没有可以互相推荐的物品,后续通过对相似度进行排序查找相似用户时应该排除此类用户,所以提前把这类相似度标0。
import pandas as pd
import numpy as np
'''
离线计算用户的相似度矩阵
'''
def common(A, B):
'''
计算集合A,B中共同元素的个数
输出形式:int
'''
if len(set(A)) >= len(set(B)):
min_set = set(B)
else:
min_set = set(A)
num = 0
for i in min_set:
num += min(A.count(i),B.count(i))
return num
user_id = list(set(data.userid)) # 所有用户id集合
user_similar = np.zeros(shape=(len(user_id), len(user_id))) # 用户相似度矩阵
for i in range(len(user_id)):
for j in range(i):
common_num = common(user_item[user_id[i]],user_item[user_id[j]])
if common_num ==0:
continue
consin_index = num / np.sqrt(len(user_item[user_id[i]]) * len(user_item[user_id[j]]))
if consin_index == 1: # 用户相似度为1说明两者购买物品完全相同
continue
else:
user_similar[i][j] = user_similar[j][i] = consin_index
user_similar = pd.DataFrame(user_similar, columns=user_id, index=user_id)
【说实话,代码中对用户id做遍历的两层for循环让我有点慌了,一时半会儿也想不到优化的方法…】
计算推荐物品候选集(User_CF.py)
这里将UserCF写成函数的形式进行调用,即输入一个用户后返回相应的推荐物品候选集。【应用主程序写成接口是不是更方便其他语言的调用?】
import pandas as pd
import numpy as np
'''
给定用户id,在线计算其推荐物品候选集
'''
def UserCF(userid, k=20):
item = [] # 创建物品候选集
similar = user_similar[userid].values # 获取输入用户与其他用户的相似度
top_k_user_id = user_similar.columns[-np.argsort(-similar)][:k] # 排序截取前k个用户id
for i in top_k_user_id:
item += user_item[i]
item = set(item) - set(user_item[userid]) # 过滤输入用户已购的物品
return list(item)
注:以上代码分文件跑会报错!
本节总结和思考
1、显然在主文件User_CF.py中,用户物品倒排表user_item和用户相似度矩阵user_similar作为其全局变量参与其中,那么3个文件的变量如何共享呢?
【目前本人只想到利用数据库存储和读取user_item和user_similar的方式进行共享。但是用户物品倒排表是字典的形式,不知道转化成什么样的表格形式存入数据库?主程序读取后还要转回成字典形式,想想都觉得麻烦…】
2、应用程序文件User_CF.py如何实现实时计算?
(a)把所有用户的候选集提前算好存入数据库,然后Java实时调用做推荐展示(暂时忽略排序阶段);
(b)把python代码写成接口供Java调用,有需求时才调用计算推荐结果.
【第一种方案其实不是实时计算,提前一次性算好,而且感觉资源浪费,可能大部分用户的推荐结果没用到;第二种方案看上去更理想,不过对算法代码的优化要求貌似更高,毕竟要控制计算耗时在ms级别,所以上线时一般会采用哪种方式呢或者有更好的方法?】
3、用户物品倒排表和用户相似度矩阵能否做到实时更新?
【很多文章中都提到相似度矩阵是离线计算的,这就意味着如果用户相似度矩阵不更新,UserCF计算得到的Item候选集就不会变,所以不能只用UserCF做召回。】
有很多指标可以评测召回算法的好坏,比如召回率、覆盖率、准确率等,本文未作详细介绍。最近处于刚接触项目阶段,所以文中内容更偏重算法的落地工程方面,没有细究算法的质量和数学原理。
另外文中很多本人没想明白的地方希望有小伙伴能解惑,感激不尽!下一篇会介绍另一种常见的CF算法——基于Item的协同过滤,日期不定…