思路
假设有用户A的喜好列表SA[1,2,3]和用户B的喜好列表SB[3,4,5],通过len(SA^SB)/len(SA)就能简单地得到一个能代表B相对于A的相似度,虽然不一定很精准,但胜在简单。
127.0.0.1:6379> sinter a:like b:like
1) "3"
len(SA^SB)=1
len(SA)=3
因而相似度为1/3
复制代码
对于某个用户A,用A对其他用户C[2,3,6],D[1,2,3,4],E[1,2,3,4,5,6,7],求相似度,通过对列表排序,就能得到关于A的相似度优先列表[D,E,C,B]
相似度分别为2/3,1,1(此处有点问题,远多于这种情形应该是不怎么相似才对)
因而排序是D,E,C,B
复制代码
使用差集操作,B相对于A的差集就是除并集外B的剩余部分,此场景下通俗点来讲就是从相似好友中推荐A可能会喜欢的物品。 sdiff key1 key2,求出key1相对于key2的差集
127.0.0.1:6379> sdiff d:like a:like
1) "4"
127.0.0.1:6379> sdiff e:like a:like
1) "4"
2) "5"
3) "6"
4) "7"
127.0.0.1:6379> sdiff c:like a:like
1) "6"
127.0.0.1:6379> sdiff b:like a:like
1) "4"
2) "5"
(integer) 2
复制代码
这个时候,将所有的差集求并集之后就能得到用户a的推荐列表了
简单实现
用golang做了一个简单的实现版本,代码放在这
实现过程遇到的比较有趣点分别是,相似度的计算逻辑,怎么得到某个用户与所有好友差集的全集,以及如何给用户推荐物品
相似度的计算逻辑
按照思路,本意是通过len(SA^SB)/len(SA)来算,但是考虑到SA较少而SB较多甚至SB完全覆盖了SA的场景,不能那么简单就算了,罗列一下发现有以下情形
- A和B的元素几乎没重合,例如[1,3,5,7,9]和[2,3,6,8,10]
- A和B的元素有部分重合,例如[1,3,5,7,9]和[2,3,5,7,10]
- A和B的元素完全重合且B中存在少量不存在A的元素,例如[1,2,3]和[1,2,3,4]
- A和B的元素完全重合且B中存在大量量不存在A的元素,例如[1,2,3]和[1,2,3,4,5,6,7,8]
若是简单的通过len(SA^SB)/len(SA)来算,那么第四种情况也会被当做很高相似度,这并不是正确的做法,于是就想到减去B中非交集的部分占B元素的比例,即变为len(SA^SB)/len(SA) - len(SB-SA^SB)/len(SB)
虽然勉强能用,但是纠结了一下,如果len(SA^SB)/len(SA)的意义是交集能代表A的程度数值,-len(SB-SA^SB)/len(SB)代表除去B不能代表A的程度数值,那么len(SA^SB)/len(SB),也是交集能代表B的程度数值,如果同时使用能代表A的程度数值与能代表B的程度数值,直觉更为精准
于是公式就变成了len(SA^SB)/len(SA)-len(SB-SA^SB)/len(SB)+len(SA^SB)/len(SB),最后各加个参数,来表示各个数值占相似度的权重,变成了plen(SA^SB)/len(SA)-qlen(SB-SA^SB)/len(SB)+r*len(SA^SB)/len(SB)
然而到这,要得到一个很是精准的相似度计算算法,真的不简单,参数数值的选择让人头疼,然而又缺乏一点必要的数学知识来推导这个值(每到这里就羡慕研究生),于是就随便测试了下,最后确定了一个做法
if float64(len(s))/float64(likeLen) >= 1 {
similarity = 0.66 * float64(len(s))/float64(likeLen) - 0.38 *float64(friendLikeLen-len(s))/float64(friendLikeLen) + 0.45 * float64(len(s))/float64(friendLikeLen)
}else{
similarity = float64(len(s))/float64(likeLen) - 0.3 *float64(friendLikeLen-len(s))/float64(friendLikeLen) + 0.35 * float64(len(s))/float64(friendLikeLen)
}
复制代码
likeLen为用户喜好列表的长度,friendLikeLen为好友的喜好列表的长度,len(s)为交集的长度,代码大意就是若是B远远覆盖了A,则采用p=0.66,q=0.38,r=0.45,若B没覆盖到A,则p=1,q=0.3,r=0.35
得到某个用户与所有好友差集的全集
根据相似度对好友排序之后,遍历好友列表,对每个好友与用户求差集,求出的差集应如何得到一个给用户推荐的全集
首先想到的是暴力方法,每取到一个差集,就使用redis sadd命令,伪代码如下
for friend := friends {
list := redis.sdiff(friend, user)
redis.add(user:recommend, list)
}
复制代码
这样既能保证次序,实现起来又很简单,但是缺点就是用户存在多少个好友,就得操作多少次redis,这实在不是一个好选择
于是在这个基础上,想到一个方法,先把差集都存放到一个slice中,最后for循环退出时只需要执行一次sadd即可,伪代码如下
for friend := friends {
list := redis.sdiff(friend, user)
recommendList.append(list)
}
redis.sadd(recommendList)
复制代码
仔细想了下,假设用户的好友数量很大,好友的喜好列表也很大,各个好友间的喜好情况几乎一样,那么在控制内存不爆满的情况,很极端的时候会出现存放到redis的用户的推荐列表,并没有多少个选项.
比如[1,2,3....,1,2,3]
,n个123,最后得到的用户推荐列表只有1,2,3,于是明白还是得在代码里得到一个set
想到了用map[int]struct和[]int
结合的办法,把物品ID作为key,以此甄别是否已存在元素(若是直接用slice则需要每次都遍历slice检验存在,时间复杂度比较高),元素放置到slice中,最后redis.add
m := make(map[string]*struct{},len(simFriends))
indexes := make([]string,0,len(simFriends))
for _, friend := range simFriends {
redisClient.Append("sdiff", fmt.Sprintf(userLikeKey, friend.ID), fmt.Sprintf(userLikeKey, userID))
r := redisClient.GetReply()
if err := r.Err; err != nil {
panic(err)
}
values, err := r.List()
if err != nil {
panic(err)
}
for _, val := range values {
if m[val] != nil {
continue
}
m[val] = &struct{}{}
indexes = append(indexes, val)
if len(indexes) >= 500 {
break
}
}
}
redisClient.Append("lpush", fmt.Sprintf(userRecommendKey,userID), indexes)
复制代码
一种空间换时间的做法,保证了推荐的次序,一定程度上也保证了推荐的个数,仔细的你可能看到了,没用sadd命令,原因在下节
给用户推荐物品
想到的推荐场景有两种,一种是类似网易云私人FM的一个一个的推荐,一个是类似淘宝京东这种猜你喜欢几个几个的推荐
当使用redis set的数据结构,虽然能做到一个一个地推荐,但是要么就是srandmember命令,随机成员,没有按相似度排序,要么就是Spop key 1,这个推荐了一次就不会在推荐第二次,或许不是很友好
然后想到其实我代码里面已经做了set的操作,存放在redis即便不是set也没问题,用list结构,无论是lrange,lindex又或者很极端的用rpop都没问题,可以说是很适合了,代码
func getUserRecommend(userID,pageSize,page int) []string{
redisClient.Append("lrange", fmt.Sprintf(userRecommendKey,userID), pageSize*(page -1),pageSize*page - 1)
r := redisClient.GetReply()
if r.Err != nil {
log.Println(r.Err)
return nil
}
recommendList,err := r.List()
if err != nil {
log.Println(err)
return nil
}
return recommendList
}
复制代码
总结
主要还是增加了对redis数据结构的适用场景的思考。此外越接触兴趣推荐,越是对研究生的研究课题感兴趣,越是涌现读在职的欲望,越是对数学界和计算机界的大神表示敬畏。
水平有限,欢迎拍砖