redis常用demo收集(二)——基于redis的简单用户协同推荐

思路

假设有用户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数据结构的适用场景的思考。此外越接触兴趣推荐,越是对研究生的研究课题感兴趣,越是涌现读在职的欲望,越是对数学界和计算机界的大神表示敬畏。

水平有限,欢迎拍砖

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值